diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000000..a4dcc74a36a --- /dev/null +++ b/.clang-format @@ -0,0 +1,105 @@ +--- +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: None +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: Right +AlignOperands: false +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: false +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: false + IndentBraces: false +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: true +BreakStringLiterals: true +ColumnLimit: 120 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: true +IndentExternBlock: AfterExternBlock +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Left +ReflowComments: true +SortIncludes: CaseSensitive +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: c++20 +TabWidth: 4 +UseTab: Never +StatementMacros: + - META_Object + - META_StateAttribute + - META_Node diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000000..d6300633156 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,18 @@ +Checks: > + -*, + boost-*, + portability-*, + clang-analyzer-*, + -clang-analyzer-optin*, + -clang-analyzer-cplusplus.NewDeleteLeaks, + -clang-analyzer-core.CallAndMessage, + -modernize-avoid-bind +WarningsAsErrors: > + -*, + boost-*, + portability-*, + clang-analyzer-*, + -clang-analyzer-optin*, + -clang-analyzer-cplusplus.NewDeleteLeaks, + -clang-analyzer-core.CallAndMessage +HeaderFilterRegex: '^(apps|components)' diff --git a/.editorconfig b/.editorconfig index 307f5e58f6d..97db8e0b548 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,12 @@ indent_style = space indent_size = 4 insert_final_newline = true -[*.glsl] +[*.{glsl,vert,tesc,tese,geom,frag,comp}] indent_style = space indent_size = 4 -insert_final_newline = false \ No newline at end of file +insert_final_newline = true + +[{CMakeLists.txt,*.cmake}] +indent_style = space +indent_size = 4 +insert_final_newline = true diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..c3e22b84125 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,19 @@ +# This file lists revisions meant to be ignored by `git blame`. +# Pass `--ignore-revs-file .git-blame-ignore-revs` to `git blame` to make your life easier. + +# Author: Alexei Kotov +# Date: Fri Sep 2 02:52:49 2022 +0000 +# Reformat NIF record type mapping +8df0587793a07ec556dc9cb575cd2af4204c456b + +# Author: AnyOldName3 +# Date: Fri Sep 16 00:53:24 2022 +0100 +# Renormalise line endings +84f8a6848a8b05502d7618ca7af8cca74f2c3bae + +# Author: clang-format-bot +# Date: 9/22/2022 9:26:05 PM +# Apply clang-format to code base +ddb0522bbf2aa8aa7c9e139ff7395fb8ed6a841f + +88ec8a95231341e7962b85716510d414e9f0c424 diff --git a/.github/workflows/openmw.yml b/.github/workflows/openmw.yml new file mode 100644 index 00000000000..9114ae7711b --- /dev/null +++ b/.github/workflows/openmw.yml @@ -0,0 +1,253 @@ +name: CMake + +on: +- push +- pull_request + +env: + BUILD_TYPE: RelWithDebInfo + VCPKG_DEPS_REVISION: 65ef3a6db0e01983efc7d8286f44020beeee2ea3 + +jobs: + Ubuntu: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Add OpenMW PPA Dependencies + run: sudo add-apt-repository ppa:openmw/openmw; sudo apt-get update + + - name: Install Building Dependencies + run: sudo CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic + + - name: Prime ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ matrix.os }}-${{ env.BUILD_TYPE }} + max-size: 1000M + + - name: Configure + run: > + cmake . + -D CMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + -D OPENMW_USE_SYSTEM_RECASTNAVIGATION=ON + -D USE_SYSTEM_TINYXML=ON + -D BUILD_COMPONENTS_TESTS=ON + -D BUILD_OPENMW_TESTS=ON + -D BUILD_OPENCS_TESTS=ON + -D CMAKE_INSTALL_PREFIX=install + + - name: Build + run: cmake --build . -- -j$(nproc) + + - name: Run components tests + run: ./components-tests + + - name: Run OpenMW tests + run: ./openmw-tests + + - name: Run OpenMW-CS tests + run: ./openmw-cs-tests + + # - name: Install + # shell: bash + # run: cmake --install . + + # - name: Create Artifact + # shell: bash + # working-directory: install + # run: | + # ls -laR + # 7z a ../build_artifact.7z . + + # - name: Upload Artifact + # uses: actions/upload-artifact@v1 + # with: + # path: ./build_artifact.7z + # name: build_artifact.7z + + MacOS: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install Building Dependencies + run: CI/before_install.osx.sh + + - name: Prime ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ matrix.os }}-${{ env.BUILD_TYPE }} + max-size: 1000M + + - name: Configure + run: | + rm -fr build # remove the build directory + CI/before_script.osx.sh + - name: Build + run: | + cd build + make -j $(sysctl -n hw.logicalcpu) package + + Windows: + strategy: + fail-fast: true + matrix: + image: + - windows-2019 + - windows-2022 + + name: ${{ matrix.image }} + + runs-on: ${{ matrix.image }} + + steps: + - uses: actions/checkout@v2 + + - name: Create directories for dependencies + run: | + mkdir -p ${{ github.workspace }}/deps + mkdir -p ${{ github.workspace }}/deps/Qt + + - name: Download prebuilt vcpkg packages + working-directory: ${{ github.workspace }}/deps + run: > + curl --fail --retry 3 -L + -o vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_REVISION }}.7z + https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_REVISION }}.7z + + - name: Extract archived prebuilt vcpkg packages + working-directory: ${{ github.workspace }}/deps + run: 7z x -y -ovcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_REVISION }} vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_REVISION }}.7z + + - name: Cache Qt + id: qt-cache + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64 + key: qt-cache-6.6.3-msvc2019_64-v1 + + - name: Download aqt + if: steps.qt-cache.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/deps/Qt + run: > + curl --fail --retry 3 -L + -o aqt_x64.exe + https://github.com/miurahr/aqtinstall/releases/download/v3.1.15/aqt_x64.exe + + - name: Install Qt with aqt + if: steps.qt-cache.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/deps/Qt + run: .\aqt_x64.exe install-qt windows desktop 6.6.3 win64_msvc2019_64 + + - uses: ilammy/msvc-dev-cmd@v1 + + - uses: seanmiddleditch/gha-setup-ninja@master + + - name: Configure OpenMW + run: > + cmake + -S . + -B ${{ github.workspace }}/build + -G Ninja + -D CMAKE_BUILD_TYPE=RelWithDebInfo + -D CMAKE_TOOLCHAIN_FILE='${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_REVISION }}/scripts/buildsystems/vcpkg.cmake' + -D CMAKE_PREFIX_PATH='${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64' + -D LuaJit_INCLUDE_DIR='${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_REVISION }}/installed/x64-windows/include/luajit' + -D LuaJit_LIBRARY='${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_REVISION }}/installed/x64-windows/lib/lua51.lib' + -D BUILD_BENCHMARKS=ON + -D BUILD_COMPONENTS_TESTS=ON + -D BUILD_OPENMW_TESTS=ON + -D BUILD_OPENCS_TESTS=ON + -D OPENMW_USE_SYSTEM_SQLITE3=OFF + -D OPENMW_USE_SYSTEM_YAML_CPP=OFF + -D OPENMW_LTO_BUILD=ON + + - name: Build OpenMW + run: cmake --build ${{ github.workspace }}/build + + - name: Install OpenMW + run: cmake --install ${{ github.workspace }}/build --prefix ${{ github.workspace }}/install + + - name: Copy missing DLLs + run: | + cp ${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_REVISION }}/installed/x64-windows/bin/Release/MyGUIEngine.dll ${{ github.workspace }}/install + cp -Filter *.dll -Recurse ${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_REVISION }}/installed/x64-windows/bin/osgPlugins-3.6.5 ${{ github.workspace }}/install + cp ${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_REVISION }}/installed/x64-windows/bin/*.dll ${{ github.workspace }}/install + + - name: Copy Qt DLLs + working-directory: ${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64 + run: | + cp bin/Qt6Core.dll ${{ github.workspace }}/install + cp bin/Qt6Gui.dll ${{ github.workspace }}/install + cp bin/Qt6Network.dll ${{ github.workspace }}/install + cp bin/Qt6OpenGL.dll ${{ github.workspace }}/install + cp bin/Qt6OpenGLWidgets.dll ${{ github.workspace }}/install + cp bin/Qt6Widgets.dll ${{ github.workspace }}/install + cp bin/Qt6Svg.dll ${{ github.workspace }}/install + mkdir ${{ github.workspace }}/install/styles + cp plugins/styles/qwindowsvistastyle.dll ${{ github.workspace }}/install/styles + mkdir ${{ github.workspace }}/install/platforms + cp plugins/platforms/qwindows.dll ${{ github.workspace }}/install/platforms + mkdir ${{ github.workspace }}/install/imageformats + cp plugins/imageformats/qsvg.dll ${{ github.workspace }}/install/imageformats + mkdir ${{ github.workspace }}/install/iconengines + cp plugins/iconengines/qsvgicon.dll ${{ github.workspace }}/install/iconengines + + - name: Move pdb files + run: | + robocopy install pdb *.pdb /MOVE + if ($lastexitcode -lt 8) { + $global:LASTEXITCODE = $null + } + + - name: Remove extra pdb files + shell: bash + run: | + rm -rf install/bin + rm -rf install/_deps + + - name: Generate CI-ID.txt + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + job_url=$(gh run --repo ${{ github.repository }} view ${{ github.run_id }} --json jobs --jq '.jobs[] | select(.name == "${{ matrix.image }}") | .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 + + - name: Store OpenMW archived pdb files + uses: actions/upload-artifact@v4 + with: + name: openmw-${{ matrix.image }}-pdb-${{ github.sha }} + path: ${{ github.workspace }}/pdb/* + + - name: Store OpenMW build artifacts + uses: actions/upload-artifact@v4 + with: + name: openmw-${{ matrix.image }}-${{ github.sha }} + path: ${{ github.workspace }}/install/* + + - name: Add install directory to PATH + shell: bash + run: echo '${{ github.workspace }}/install' >> ${GITHUB_PATH} + + - name: Run components tests + run: build/components-tests.exe + + - name: Run OpenMW tests + run: build/openmw-tests.exe + + - name: Run OpenMW-CS tests + run: build/openmw-cs-tests.exe + + - name: Run detournavigator navmeshtilescache benchmark + run: build/openmw_detournavigator_navmeshtilescache_benchmark.exe + + - name: Run settings access benchmark + run: build/openmw_settings_access_benchmark.exe + + - name: Run esm refid benchmark + run: build/openmw_esm_refid_benchmark.exe diff --git a/.gitignore b/.gitignore index 1905957d9b5..39033bd725d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,13 +28,13 @@ Doxygen .idea cmake-build-* files/windows/*.aps +.cache/clangd ## qt-creator CMakeLists.txt.user* .vs .vscode ## resources -data resources /*.cfg /*.desktop @@ -72,6 +72,7 @@ components/ui_contentselector.h docs/mainpage.hpp docs/Doxyfile docs/DoxyfilePages +docs/source/reference/lua-scripting/generated_html moc_*.cxx *.cxx_parameters *qrc_launcher.cxx @@ -84,3 +85,7 @@ moc_*.cxx *.[ao] *.so venv/ + +## operating system files +.DS_Store +Thumbs.db diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ceba2841fe4..e1065908381 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,131 +1,502 @@ +default: + interruptible: true + # Note: We set `needs` on each job to control the job DAG. # See https://docs.gitlab.com/ee/ci/yaml/#needs stages: + - checks - build + - test -.Debian_Image: +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS + when: never + - if: $CI_COMMIT_BRANCH + +# https://blog.nimbleways.com/let-s-make-faster-gitlab-ci-cd-pipelines/ +variables: + FF_USE_NEW_SHELL_ESCAPE: "true" + FF_USE_FASTZIP: "true" + # These can be specified per job or per pipeline + ARTIFACT_COMPRESSION_LEVEL: "fast" + CACHE_COMPRESSION_LEVEL: "fast" + FF_TIMESTAMPS: "true" + +.Ubuntu_Image: tags: - - docker - - linux - image: debian:bullseye + - saas-linux-medium-amd64 + image: ubuntu:22.04 + rules: + - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" + +Ubuntu_GCC_preprocess: + extends: .Ubuntu_Image + cache: + key: Ubuntu_GCC_preprocess.ubuntu_22.04.v1 + paths: + - apt-cache/ + - .cache/pip/ + stage: build + variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + before_script: + - CI/install_debian_deps.sh openmw-deps openmw-deps-dynamic gcc_preprocess + - pip3 install --user click termtables + script: + - CI/ubuntu_gcc_preprocess.sh -.Debian: - extends: .Debian_Image +.Ubuntu: + extends: .Ubuntu_Image cache: paths: - apt-cache/ - ccache/ stage: build + variables: + CMAKE_EXE_LINKER_FLAGS: -fuse-ld=mold script: + - df -h - export CCACHE_BASEDIR="`pwd`" - export CCACHE_DIR="`pwd`/ccache" && mkdir -pv "$CCACHE_DIR" - ccache -z -M "${CCACHE_SIZE}" - CI/before_script.linux.sh - cd build - cmake --build . -- -j $(nproc) + - df -h + - du -sh . + - find . | grep '\.o$' | xargs rm -f + - df -h + - du -sh . - cmake --install . - - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw_test_suite; fi - - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw_detournavigator_navmeshtilescache_benchmark; fi + - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./components-tests --gtest_output="xml:components-tests.xml"; fi + - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw-tests --gtest_output="xml:openmw-tests.xml"; fi + - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw-cs-tests --gtest_output="xml:openmw-cs-tests.xml"; fi + - 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 + - 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 './{}' + - cd .. + - df -h + - du -sh build/ + - du -sh build/install/ + - du -sh apt-cache/ + - du -sh ccache/ artifacts: paths: - build/install/ Coverity: - extends: .Debian_Image + tags: + - saas-linux-medium-amd64 + image: ubuntu:22.04 stage: build rules: - - if: '$CI_PIPELINE_SOURCE == "schedule"' + - if: $CI_PIPELINE_SOURCE == "schedule" + cache: + key: Coverity.ubuntu_22.04.v1 + paths: + - apt-cache/ + - ccache/ + variables: + CCACHE_SIZE: 2G + CC: clang-12 + CXX: clang++-12 + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -O0 before_script: - - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic coverity - - curl -o /tmp/cov-analysis-linux64.tgz https://scan.coverity.com/download/linux64 --form project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN + - CI/install_debian_deps.sh coverity openmw-deps openmw-deps-dynamic + - curl -o /tmp/cov-analysis-linux64.tgz https://scan.coverity.com/download/linux64 + --form project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN - tar xfz /tmp/cov-analysis-linux64.tgz script: + - export CCACHE_BASEDIR="$(pwd)" + - export CCACHE_DIR="$(pwd)/ccache" + - export COVERITY_NO_LOG_ENVIRONMENT_VARIABLES=1 + - mkdir -pv "${CCACHE_DIR}" + - ccache -z -M "${CCACHE_SIZE}" - CI/before_script.linux.sh - # Add more than just `openmw` once we can build everything under 3h - - cov-analysis-linux64-*/bin/cov-build --dir cov-int cmake --build build -- -j $(nproc) openmw + - 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 after_script: - tar cfz cov-int.tar.gz cov-int - curl https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN --form email=$GITLAB_USER_EMAIL - --form file=@cov-int.tar.gz --form version="`git describe --tags`" - --form description="`git describe --tags` / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID" + --form file=@cov-int.tar.gz --form version="$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA" + --form description="CI_COMMIT_SHORT_SHA / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID" + artifacts: + paths: + - /builds/OpenMW/openmw/cov-int/build-log.txt + +Ubuntu_GCC: + extends: .Ubuntu + cache: + key: Ubuntu_GCC.ubuntu_22.04.v1 + before_script: + - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic variables: CC: gcc CXX: g++ - timeout: 8h + CCACHE_SIZE: 3G + # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. + timeout: 2h + +Ubuntu_GCC_asan: + extends: Ubuntu_GCC + cache: + key: Ubuntu_GCC_asan.ubuntu_22.04.v1 + variables: + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -g -O1 -fno-omit-frame-pointer -fsanitize=address -fsanitize=pointer-subtract -fsanitize=leak + CMAKE_EXE_LINKER_FLAGS: -fsanitize=address -fsanitize=pointer-subtract -fsanitize=leak -fuse-ld=mold + BUILD_OPENMW_ONLY: 1 + +Clang_Format: + extends: .Ubuntu_Image + stage: checks + cache: + key: Ubuntu_Clang_Format.ubuntu_22.04.v1 + paths: + - apt-cache/ + variables: + CLANG_FORMAT: clang-format-14 + before_script: + - CI/install_debian_deps.sh openmw-clang-format + script: + - CI/check_cmake_format.sh + - CI/check_file_names.sh + - CI/check_clang_format.sh + +Lupdate: + extends: .Ubuntu_Image + stage: checks + cache: + key: Ubuntu_lupdate.ubuntu_22.04.v1 + paths: + - apt-cache/ + variables: + LUPDATE: lupdate + before_script: + - CI/install_debian_deps.sh openmw-qt-translations + script: + - CI/check_qt_translations.sh -Debian_GCC: - extends: .Debian +Teal: + stage: checks + extends: .Ubuntu_Image + before_script: + - apt-get update + - apt-get -y install curl wget make build-essential libreadline-dev git-core zip unzip + script: + - CI/teal_ci.sh + artifacts: + paths: + - teal_declarations.zip + +Ubuntu_GCC_Debug: + extends: .Ubuntu cache: - key: Debian_GCC.v2 + key: Ubuntu_GCC_Debug.ubuntu_22.04.v2 before_script: - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic variables: CC: gcc CXX: g++ CCACHE_SIZE: 3G + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -O0 + BUILD_SHARED_LIBS: 1 # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. timeout: 2h -Debian_GCC_tests: - extends: Debian_GCC +Ubuntu_GCC_tests: + extends: Ubuntu_GCC + cache: + key: Ubuntu_GCC_tests.ubuntu_22.04.v1 + variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/*_tests.xml + +.Ubuntu_GCC_tests_Debug: + extends: Ubuntu_GCC + cache: + key: Ubuntu_GCC_tests_Debug.ubuntu_22.04.v1 + variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -g -O0 -D_GLIBCXX_ASSERTIONS + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/*_tests.xml + +Ubuntu_GCC_tests_asan: + extends: Ubuntu_GCC + cache: + key: Ubuntu_GCC_tests_asan.ubuntu_22.04.v1 + variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -g -O1 -fno-omit-frame-pointer -fsanitize=address -fsanitize=pointer-subtract -fsanitize=leak + CMAKE_EXE_LINKER_FLAGS: -fsanitize=address -fsanitize=pointer-subtract -fsanitize=leak -fuse-ld=mold + ASAN_OPTIONS: halt_on_error=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/*_tests.xml + +Ubuntu_GCC_tests_ubsan: + extends: Ubuntu_GCC + cache: + key: Ubuntu_GCC_tests_ubsan.ubuntu_22.04.v1 + variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -g -O0 -fsanitize=undefined + UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/*_tests.xml + +.Ubuntu_GCC_tests_tsan: + extends: Ubuntu_GCC cache: - key: Debian_GCC_tests.v2 + key: Ubuntu_GCC_tests_tsan.ubuntu_22.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -g -O2 -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=thread -fPIE + CMAKE_EXE_LINKER_FLAGS: -pthread -pie -fsanitize=thread -fuse-ld=mold + TSAN_OPTIONS: second_deadlock_stack=1:halt_on_error=1 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/*_tests.xml -Debian_GCC_Static_Deps: - extends: Debian_GCC +Ubuntu_GCC_tests_coverage: + extends: .Ubuntu_GCC_tests_Debug + cache: + key: Ubuntu_GCC_tests_coverage.ubuntu_22.04.v1 + variables: + BUILD_WITH_CODE_COVERAGE: 1 + before_script: + - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic openmw-coverage + coverage: /^\s*lines:\s*\d+.\d+\%/ + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml + junit: build/*_tests.xml + +.Ubuntu_Static_Deps: + extends: Ubuntu_Clang + rules: + - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" + changes: + - "**/CMakeLists.txt" + - "cmake/**/*" + - "CI/**/*" + - ".gitlab-ci.yml" cache: - key: Debian_GCC_Static_Deps + key: Ubuntu_Static_Deps.ubuntu_22.04.v1 paths: - - apt-cache/ - - ccache/ - - build/extern/fetched/ + - apt-cache/ + - ccache/ + - build/extern/fetched/ before_script: - - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-static + - CI/install_debian_deps.sh clang openmw-deps openmw-deps-static variables: CI_OPENMW_USE_STATIC_DEPS: 1 + CC: clang + CXX: clang++ + CXXFLAGS: -O0 + timeout: 3h -Debian_GCC_Static_Deps_tests: - extends: Debian_GCC_Static_Deps +.Ubuntu_Static_Deps_tests: + extends: .Ubuntu_Static_Deps cache: - key: Debian_GCC_Static_Deps_tests + key: Ubuntu_Static_Deps_tests.ubuntu_22.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 + CC: clang + CXX: clang++ + CXXFLAGS: -O0 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/*_tests.xml -Debian_Clang: - extends: .Debian +Ubuntu_Clang: + extends: .Ubuntu before_script: - CI/install_debian_deps.sh clang openmw-deps openmw-deps-dynamic cache: - key: Debian_Clang.v2 + key: Ubuntu_Clang.ubuntu_22.04.v2 variables: CC: clang CXX: clang++ CCACHE_SIZE: 2G # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. - timeout: 2h + timeout: 3h + +.Ubuntu_Clang_Tidy_Base: + extends: Ubuntu_Clang + before_script: + - CI/install_debian_deps.sh clang clang-tidy openmw-deps openmw-deps-dynamic + cache: + key: Ubuntu_Clang_Tidy.ubuntu_22.04.v1 + variables: + CMAKE_BUILD_TYPE: Debug + CMAKE_CXX_FLAGS_DEBUG: -O0 + CI_CLANG_TIDY: 1 + CCACHE_BASEDIR: $CI_PROJECT_DIR + CCACHE_DIR: $CI_PROJECT_DIR/ccache + script: + - mkdir -pv "${CCACHE_DIR}" + - ccache -z -M "${CCACHE_SIZE}" + - CI/before_script.linux.sh + - cd build + - find . -name *.o -exec touch {} \; + - cmake --build . -- -j $(nproc) ${BUILD_TARGETS} + - ccache -s + artifacts: + paths: + - build/ + expire_in: 12h + timeout: 3h + +Ubuntu_Clang_Tidy_components: + extends: .Ubuntu_Clang_Tidy_Base + variables: + BUILD_TARGETS: components components_qt oics osg-ffmpeg-videoplayer osgQt + timeout: 3h + +Ubuntu_Clang_Tidy_openmw: + extends: .Ubuntu_Clang_Tidy_Base + needs: + - Ubuntu_Clang_Tidy_components + variables: + BUILD_TARGETS: openmw + timeout: 3h + +Ubuntu_Clang_Tidy_openmw-cs: + extends: .Ubuntu_Clang_Tidy_Base + needs: + - Ubuntu_Clang_Tidy_components + variables: + BUILD_TARGETS: openmw-cs openmw-cs-tests + timeout: 3h + +Ubuntu_Clang_Tidy_other: + extends: .Ubuntu_Clang_Tidy_Base + 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 + timeout: 3h + +.Ubuntu_Clang_tests: + extends: Ubuntu_Clang + cache: + key: Ubuntu_Clang_tests.ubuntu_22.04.v1 + variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/*_tests.xml -Debian_Clang_tests: - extends: Debian_Clang +Ubuntu_Clang_tests_Debug: + extends: Ubuntu_Clang cache: - key: Debian_Clang_tests.v2 + key: Ubuntu_Clang_tests_Debug.ubuntu_22.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + artifacts: + paths: [] + name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} + when: always + reports: + junit: build/*_tests.xml + +.Ubuntu_integration_tests_base: + extends: .Ubuntu_Image + stage: test + variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + EXAMPLE_SUITE_REVISION: f51b832e033429a7cdc520e0e48d7dfdb9141caa + cache: + paths: + - .cache/pip + - apt-cache/ + before_script: + - CI/install_debian_deps.sh $OPENMW_DEPS + - pip3 install --user numpy matplotlib termtables click + script: + - CI/run_integration_tests.sh + after_script: + - if [[ -f /tmp/openmw-crash.log ]]; then cat /tmp/openmw-crash.log; fi + +Ubuntu_Clang_integration_tests: + extends: .Ubuntu_integration_tests_base + needs: + - Ubuntu_Clang + cache: + key: Ubuntu_Clang_integration_tests.ubuntu_22.04.v2 + variables: + OPENMW_DEPS: openmw-integration-tests + +Ubuntu_GCC_integration_tests_asan: + extends: .Ubuntu_integration_tests_base + needs: + - Ubuntu_GCC_asan + cache: + key: Ubuntu_GCC_integration_tests_asan.ubuntu_22.04.v1 + variables: + OPENMW_DEPS: openmw-integration-tests libasan6 + ASAN_OPTIONS: halt_on_error=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:detect_leaks=0 .MacOS: - image: macos-11-xcode-12 - tags: - - shared-macos-amd64 stage: build - only: - variables: - - $CI_PROJECT_ID == "7107382" + rules: + - if: $CI_PROJECT_ID == "7107382" cache: paths: - ccache/ @@ -138,266 +509,380 @@ Debian_Clang_tests: - ccache -z -M "${CCACHE_SIZE}" - CI/before_script.osx.sh - cd build; make -j $(sysctl -n hw.logicalcpu) package - - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}.dmg"; done + - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME##*/}_${CI_JOB_ID}.dmg"; done + - | + if [[ -n "${AWS_ACCESS_KEY_ID}" ]]; then + artifactDirectory="${CI_PROJECT_NAMESPACE//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_REF_NAME//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_SHORT_SHA//[\"<>|$'\t'\/\\?*]/_}-${CI_JOB_ID//[\"<>|$'\t'\/\\?*]/_}/" + for dmg in *.dmg; do + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "${dmg}" s3://openmw-artifacts/${artifactDirectory} + done + fi - ccache -s artifacts: paths: - build/OpenMW-*.dmg - "build/**/*.log" -macOS11_Xcode12: +macOS14_Xcode15_arm64: extends: .MacOS - image: macos-11-xcode-12 - allow_failure: true + image: macos-14-xcode-15 + tags: + - saas-macos-medium-m1 cache: - key: macOS11_Xcode12.v1 + key: macOS14_Xcode15_arm64.v1 variables: CCACHE_SIZE: 3G -macOS10.15_Xcode11: - extends: .MacOS - image: macos-10.15-xcode-11 - cache: - key: macOS10.15_Xcode11.v1 +.Compress_And_Upload_Symbols_Base: + extends: .Ubuntu_Image + stage: build variables: - CCACHE_SIZE: 3G - -variables: &engine-targets - targets: "openmw,openmw-essimporter,openmw-iniimporter,openmw-launcher,openmw-wizard" - package: "Engine" - -variables: &cs-targets - targets: "openmw-cs,bsatool,esmtool,niftest" - package: "CS" - -variables: &tests-targets - targets: "openmw_test_suite,openmw_detournavigator_navmeshtilescache_benchmark" - package: "Tests" + GIT_STRATEGY: none + script: + - apt-get update + - apt-get install -y curl gcab unzip + - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscli-exe-linux-x86_64.zip + - unzip -d awscli-exe-linux-x86_64 awscli-exe-linux-x86_64.zip + - pushd awscli-exe-linux-x86_64 + - ./aws/install + - popd + - aws --version + - unzip -d sym_store *sym_store.zip + - shopt -s globstar + - | + for file in sym_store/**/*.exe; do + if [[ -f "$file" ]]; then + gcab --create --zip --nopath "${file%.exe}.ex_" "$file" + fi + done + - | + for file in sym_store/**/*.dll; do + if [[ -f "$file" ]]; then + gcab --create --zip --nopath "${file%.dll}.dl_" "$file" + fi + done + - | + for file in sym_store/**/*.pdb; do + if [[ -f "$file" ]]; then + gcab --create --zip --nopath "${file%.pdb}.pd_" "$file" + fi + done + - | + if [[ -v AWS_ACCESS_KEY_ID ]]; then + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp --recursive --exclude '*' --include '*.ex_' --include '*.dl_' --include '*.pd_' sym_store s3://openmw-sym + fi .Windows_Ninja_Base: tags: - - windows + - saas-windows-medium-amd64 + rules: + - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" before_script: + - Get-Volume - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 + - choco source disable -n=chocolatey - choco install git --force --params "/GitAndUnixToolsOnPath" -y - choco install 7zip -y - - choco install cmake.install --installargs 'ADD_CMAKE_TO_PATH=System' -y + - choco install ccache -y - choco install vswhere -y - choco install ninja -y - choco install python -y + - choco install awscli -y - refreshenv + - | + function Make-SafeFileName { + param( + [Parameter(Mandatory=$true)] + [String] + $FileName + ) + [IO.Path]::GetInvalidFileNameChars() | ForEach-Object { + $FileName = $FileName.Replace($_, '_') + } + return $FileName + } stage: build script: + - Get-Volume - $time = (Get-Date -Format "HH:mm:ss") - echo ${time} - echo "started by ${GITLAB_USER_NAME}" - - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -N -b -t - - cd MSVC2019_64_Ninja + - $env:CCACHE_BASEDIR = Get-Location + - $env:CCACHE_DIR = "$(Get-Location)\ccache" + - New-Item -Type Directory -Force -Path $env:CCACHE_DIR + - New-Item -Type File -Force -Path MSVC2022_64_Ninja\.cmake\api\v1\query\codemodel-v2 + - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2022 -k -V -N -b -t -C $multiview -E + - Get-Volume + - cd MSVC2022_64_Ninja - .\ActivateMSVC.ps1 - - cmake --build . --config $config --target ($targets.Split(',')) + - cmake --build . --config $config --target $targets + - ccache --show-stats -v - cd $config - echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt + - $artifactDirectory = "$(Make-SafeFileName("${CI_PROJECT_NAMESPACE}"))/$(Make-SafeFileName("${CI_COMMIT_REF_NAME}"))/$(Make-SafeFileName("${CI_COMMIT_SHORT_SHA}-${CI_JOB_ID}"))/" + - Get-ChildItem -Recurse *.ilk | Remove-Item - | if (Get-ChildItem -Recurse *.pdb) { - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb' CI-ID.txt + 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt + if (Test-Path env:AWS_ACCESS_KEY_ID) { + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} + } + Push-Location .. + ..\CI\Store-Symbols.ps1 -SkipCompress + 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt + Pop-Location Get-ChildItem -Recurse *.pdb | Remove-Item } - - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}.zip '*' + - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' + - | + if (Test-Path env:AWS_ACCESS_KEY_ID) { + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} + } - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: + - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: ninja-v2 + key: ninja-2022-v11 paths: + - ccache - deps - - MSVC2019_64_Ninja/deps/Qt + - MSVC2022_64_Ninja/deps/Qt artifacts: when: always paths: - "*.zip" - "*.log" - - MSVC2019_64_Ninja/*.log - - MSVC2019_64_Ninja/*/*.log - - MSVC2019_64_Ninja/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*/*/*.log - - MSVC2019_64_Ninja/*/*/*/*/*/*/*/*.log - -Windows_Ninja_Engine_Release: - extends: - - .Windows_Ninja_Base + - MSVC2022_64_Ninja/*.log + - MSVC2022_64_Ninja/**/*.log variables: - <<: *engine-targets - config: "Release" + targets: all + # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. + timeout: 2h -Windows_Ninja_Engine_Debug: +.Windows_Ninja_Release: extends: - .Windows_Ninja_Base variables: - <<: *engine-targets - config: "Debug" + config: "Release" -Windows_Ninja_Engine_RelWithDebInfo: +.Windows_Compress_And_Upload_Symbols_Ninja_Release: extends: - - .Windows_Ninja_Base - variables: - <<: *engine-targets - config: "RelWithDebInfo" + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_Ninja_Release" + artifacts: true -Windows_Ninja_CS_Release: +.Windows_Ninja_Release_MultiView: extends: - .Windows_Ninja_Base variables: - <<: *cs-targets + multiview: "-M" config: "Release" -Windows_Ninja_CS_Debug: +.Windows_Compress_And_Upload_Symbols_Ninja_Release_MultiView: + extends: + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_Ninja_Release_MultiView" + artifacts: true + +.Windows_Ninja_Debug: extends: - .Windows_Ninja_Base variables: - <<: *cs-targets config: "Debug" -Windows_Ninja_CS_RelWithDebInfo: +.Windows_Compress_And_Upload_Symbols_Ninja_Debug: + extends: + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_Ninja_Debug" + artifacts: true + +.Windows_Ninja_RelWithDebInfo: extends: - .Windows_Ninja_Base variables: - <<: *cs-targets config: "RelWithDebInfo" + # Gitlab can't successfully execute following binaries due to unknown reason + # executables: "components-tests.exe,openmw-tests.exe,openmw-cs-tests.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" -Windows_Ninja_Tests_RelWithDebInfo: - extends: .Windows_Ninja_Base - stage: build +.Windows_Compress_And_Upload_Symbols_Ninja_RelWithDebInfo: + extends: + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_Ninja_RelWithDebInfo" + artifacts: true + +.Windows_Ninja_CacheInit: + # currently, Windows jobs for all configs share the same cache key as we only cache the dependencies + extends: + - .Windows_Ninja_Base variables: - <<: *tests-targets config: "RelWithDebInfo" - # Gitlab can't successfully execute following binaries due to unknown reason - # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" + targets: "get-version" + when: manual .Windows_MSBuild_Base: tags: - - windows + - saas-windows-medium-amd64 + rules: + - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" before_script: + - Get-Volume - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 + - choco source disable -n=chocolatey - choco install git --force --params "/GitAndUnixToolsOnPath" -y - choco install 7zip -y - - choco install cmake.install --installargs 'ADD_CMAKE_TO_PATH=System' -y - choco install vswhere -y - choco install python -y + - choco install awscli -y - refreshenv + - | + function Make-SafeFileName { + param( + [Parameter(Mandatory=$true)] + [String] + $FileName + ) + [IO.Path]::GetInvalidFileNameChars() | ForEach-Object { + $FileName = $FileName.Replace($_, '_') + } + return $FileName + } stage: build script: + - Get-Volume - $time = (Get-Date -Format "HH:mm:ss") - echo ${time} - echo "started by ${GITLAB_USER_NAME}" - - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -b -t - - cd MSVC2019_64 - - cmake --build . --config $config --target ($targets.Split(',')) + - New-Item -Type File -Force -Path MSVC2022_64\.cmake\api\v1\query\codemodel-v2 + - sh CI/before_script.msvc.sh -c $config -p Win64 -v 2022 -k -V -b -t -C $multiview -E + - cd MSVC2022_64 + - Get-Volume + - cmake --build . --config $config --target $targets - cd $config - echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt + - $artifactDirectory = "$(Make-SafeFileName("${CI_PROJECT_NAMESPACE}"))/$(Make-SafeFileName("${CI_COMMIT_REF_NAME}"))/$(Make-SafeFileName("${CI_COMMIT_SHORT_SHA}-${CI_JOB_ID}"))/" + - Get-ChildItem -Recurse *.ilk | Remove-Item - | if (Get-ChildItem -Recurse *.pdb) { - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb' CI-ID.txt + 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt + if (Test-Path env:AWS_ACCESS_KEY_ID) { + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory} + } + Push-Location .. + ..\CI\Store-Symbols.ps1 -SkipCompress + 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt + Pop-Location Get-ChildItem -Recurse *.pdb | Remove-Item } - - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}.zip '*' + - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' + - | + if (Test-Path env:AWS_ACCESS_KEY_ID) { + aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory} + } - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: + - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: msbuild-v2 + key: msbuild-2022-v11 paths: - deps - - MSVC2019_64/deps/Qt + - MSVC2022_64/deps/Qt artifacts: when: always paths: - "*.zip" - "*.log" - - MSVC2019_64/*.log - - MSVC2019_64/*/*.log - - MSVC2019_64/*/*/*.log - - MSVC2019_64/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*/*/*.log - - MSVC2019_64/*/*/*/*/*/*/*/*.log - -Windows_MSBuild_Engine_Release: + - MSVC2022_64/*.log + - MSVC2022_64/**/*.log + variables: + targets: ALL_BUILD + # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. + timeout: 2h + +.Windows_MSBuild_Release: extends: - .Windows_MSBuild_Base variables: - <<: *engine-targets config: "Release" -Windows_MSBuild_Engine_Debug: +.Windows_Compress_And_Upload_Symbols_MSBuild_Release: + extends: + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_MSBuild_Release" + artifacts: true + +.Windows_MSBuild_Debug: extends: - .Windows_MSBuild_Base variables: - <<: *engine-targets config: "Debug" -Windows_MSBuild_Engine_RelWithDebInfo: +.Windows_Compress_And_Upload_Symbols_MSBuild_Debug: extends: - - .Windows_MSBuild_Base - variables: - <<: *engine-targets - config: "RelWithDebInfo" + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_MSBuild_Debug" + artifacts: true -Windows_MSBuild_CS_Release: +Windows_MSBuild_RelWithDebInfo: extends: - .Windows_MSBuild_Base variables: - <<: *cs-targets - config: "Release" + config: "RelWithDebInfo" + # Gitlab can't successfully execute following binaries due to unknown reason + # executables: "components-tests.exe,openmw-tests.exe,openmw-cs-tests.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" + # temporarily enabled while we're linking these on the downloads page + rules: + # run this for both pushes and schedules so 'latest successful pipeline for branch' always includes it + - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_PIPELINE_SOURCE == "schedule" -Windows_MSBuild_CS_Debug: +Windows_Compress_And_Upload_Symbols_MSBuild_RelWithDebInfo: extends: - - .Windows_MSBuild_Base - variables: - <<: *cs-targets - config: "Debug" + - .Compress_And_Upload_Symbols_Base + needs: + - job: "Windows_MSBuild_RelWithDebInfo" + artifacts: true + # temporarily enabled while we're linking the above on the downloads page + rules: + # run this for both pushes and schedules so 'latest successful pipeline for branch' always includes it + - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_PIPELINE_SOURCE == "schedule" -Windows_MSBuild_CS_RelWithDebInfo: +Windows_MSBuild_CacheInit: + # currently, Windows jobs for all configs share the same cache key as we only cache the dependencies extends: - .Windows_MSBuild_Base variables: - <<: *cs-targets - config: "RelWithDebInfo" - -Windows_MSBuild_Tests_RelWithDebInfo: - extends: .Windows_MSBuild_Base - stage: build - variables: - <<: *tests-targets config: "RelWithDebInfo" - # Gitlab can't successfully execute following binaries due to unknown reason - # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" + targets: "get-version" + when: manual -Debian_AndroidNDK_arm64-v8a: +.Ubuntu_AndroidNDK_arm64-v8a: tags: - - linux - image: debian:bullseye + - saas-linux-medium-amd64 + image: psi29a/android-ndk:focal-ndk22 + rules: + - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" variables: CCACHE_SIZE: 3G cache: - key: Debian_AndroidNDK_arm64-v8a.v3 + key: Ubuntu__Focal_AndroidNDK_r22b_arm64-v8a.v2 paths: - apt-cache/ - ccache/ - build/extern/fetched/ before_script: - - export APT_CACHE_DIR=`pwd`/apt-cache && mkdir -pv $APT_CACHE_DIR - - echo "deb http://deb.debian.org/debian unstable main contrib" > /etc/apt/sources.list - - echo "google-android-ndk-installer google-android-installers/mirror select https://dl.google.com" | debconf-set-selections - - apt-get update -yq - - apt-get -q -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake ccache curl unzip git build-essential google-android-ndk-installer + - CI/install_debian_deps.sh android stage: build script: + - df -h - export CCACHE_BASEDIR="`pwd`" - export CCACHE_DIR="`pwd`/ccache" && mkdir -pv "$CCACHE_DIR" - ccache -z -M "${CCACHE_SIZE}" @@ -405,10 +890,55 @@ Debian_AndroidNDK_arm64-v8a: - CI/before_script.android.sh - cd build - cmake --build . -- -j $(nproc) - - cmake --install . + # - cmake --install . # no one uses builds anyway, disable until 'no space left' is resolved - ccache -s + - df -h + - ls | grep -v -e '^extern$' -e '^install$' | xargs -I '{}' rm -rf './{}' + - cd .. + - df -h + - du -sh build/ + # - du -sh build/install/ # no install dir because it's commented out above + - du -sh apt-cache/ + - du -sh ccache/ + - du -sh build/extern/fetched/ artifacts: paths: - build/install/ # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. timeout: 1h30m + +.FindMissingMergeRequests: + image: python:latest + stage: build + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + cache: + key: FindMissingMergeRequests.v1 + paths: + - .cache/pip + before_script: + - pip3 install --user requests click discord_webhook + script: + - scripts/find_missing_merge_requests.py --project_id=$CI_PROJECT_ID --ignored_mrs_path=$CI_PROJECT_DIR/.resubmitted_merge_requests.txt + +.flatpak: + image: 'docker.io/bilelmoussaoui/flatpak-github-actions' + stage: build + script: + - flatpak install -y flathub org.kde.Platform/x86_64/5.15-21.08 + - flatpak install -y flathub org.kde.Sdk/x86_64/5.15-21.08 + - flatpak-builder --ccache --force-clean --repo=repo build CI/org.openmw.OpenMW.devel.yaml + - flatpak build-bundle ./repo openmw.flatpak org.openmw.OpenMW.devel + cache: + key: flatpak + paths: + - ".flatpak-builder" + artifacts: + untracked: false + paths: + - "openmw.flatpak" + # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. + # Flatpak Builds compile all dependencies aswell so need more time. Build results of libraries are cached + timeout: 4h diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000000..962b34f5163 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 + +sphinx: + configuration: docs/source/conf.py + +python: + install: + - requirements: docs/requirements.txt + +build: + os: ubuntu-22.04 + tools: + python: "3.8" diff --git a/.resubmitted_merge_requests.txt b/.resubmitted_merge_requests.txt new file mode 100644 index 00000000000..1585a60ec1d --- /dev/null +++ b/.resubmitted_merge_requests.txt @@ -0,0 +1,8 @@ +1471 +1450 +1420 +1314 +1216 +1172 +1160 +1051 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1322dfca1bf..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,80 +0,0 @@ -language: cpp -branches: - only: - - master - - /openmw-.*$/ -cache: ccache -addons: - apt: - sources: - - sourceline: 'ppa:openmw/openmw' - packages: [ - # Dev - build-essential, cmake, clang-tools-9, ccache, - # Boost - libboost-filesystem-dev, libboost-iostreams-dev, libboost-program-options-dev, libboost-system-dev, - # FFmpeg - libavcodec-dev, libavformat-dev, libavutil-dev, libswresample-dev, libswscale-dev, - # Audio, Video and Misc. deps - libsdl2-dev, libqt5opengl5-dev, libopenal-dev, libunshield-dev, libtinyxml-dev, liblz4-dev, - # The other ones from OpenMW ppa - libbullet-dev, libopenscenegraph-dev, libmygui-dev - ] -matrix: - include: - - name: OpenMW (all) on MacOS 10.15 with Xcode 11.6 - os: osx - osx_image: xcode11.6 - - name: OpenMW (all) on Ubuntu Focal with GCC - os: linux - dist: focal - - name: OpenMW (tests only) on Ubuntu Focal with GCC - os: linux - dist: focal - env: - - BUILD_TESTS_ONLY: 1 - - name: OpenMW (openmw) on Ubuntu Focal with Clang's Static Analysis - os: linux - dist: focal - env: - - MATRIX_EVAL="CC=clang-9 && CXX=clang++-9" - - ANALYZE="scan-build-9 --force-analyze-debug-code --use-cc clang-9 --use-c++ clang++-9" - compiler: clang - -before_install: - - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then eval "${MATRIX_EVAL}"; fi - - ./CI/before_install.${TRAVIS_OS_NAME}.sh -before_script: - - ccache -z - - ./CI/before_script.${TRAVIS_OS_NAME}.sh -script: - - cd ./build - - ${ANALYZE} make -j3; - - if [ "${TRAVIS_OS_NAME}" = "osx" ]; then make package; fi - - if [ "${TRAVIS_OS_NAME}" = "osx" ]; then ../CI/check_package.osx.sh; fi - - if [ "${TRAVIS_OS_NAME}" = "linux" ] && [ "${BUILD_TESTS_ONLY}" ]; then ./openmw_test_suite; fi - - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then cd .. && ./CI/check_tabs.sh; fi - - cd "${TRAVIS_BUILD_DIR}" - - ccache -s -deploy: - provider: script - script: ./CI/deploy.osx.sh - skip_cleanup: true - on: - branch: master - condition: "$TRAVIS_EVENT_TYPE = cron && $TRAVIS_OS_NAME = osx" - repo: OpenMW/openmw -notifications: - email: - if: repository_slug = OpenMW/openmw AND branch = master - recipients: - - corrmage+travis-ci@gmail.com - on_success: change - on_failure: always - irc: - if: repository_slug = OpenMW/openmw AND branch = master - channels: - - "irc.libera.chat#openmw" - on_success: change - on_failure: always - use_notice: true diff --git a/AUTHORS.md b/AUTHORS.md index 75302908eaf..e5caf5fa586 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -15,6 +15,7 @@ Programmers Nicolay Korslund - Project leader 2008-2010 scrawl - Top contributor + AbduSharif Adam Hogan (aurix) Aesylwinn aegis @@ -27,8 +28,12 @@ Programmers Alexander Olofsson (Ananace) Alex Rice Alex S (docwest) + Alexey Yaryshev (skeevert) Allofich + Andreas Stöckel Andrei Kortunov (akortunov) + Andrew Appuhamy (andrew-app) + Andrzej Głuszak (agluszak) AnyOldName3 Ardekantur Armin Preiml @@ -42,6 +47,7 @@ Programmers Austin Salgat (Salgat) Ben Shealy (bentsherman) Berulacks + Bo Svensson Britt Mathis (galdor557) Capostrophic Carl Maxwell @@ -49,13 +55,16 @@ Programmers Cédric Mocquillon Chris Boyce (slothlife) Chris Robinson (KittyCat) + Chris Vigil Cody Glassman (Wazabear) Coleman Smith (olcoal) Cory F. Cohen (cfcohen) Cris Mihalache (Mirceam) crussell187 - DanielVukelich + Sam Hellawell (cykoder) + Dan Vukelich (sanchezman) darkf + Dave Corley (S3ctor) David Cernat (davidcernat) Declan Millar (declan-millar) devnexen @@ -67,16 +76,20 @@ Programmers David Teviotdale (dteviot) Diggory Hardy Dmitry Marakasov (AMDmi3) + Duncan Frost (duncans_pumpkin) Edmondo Tommasina (edmondo) Eduard Cot (trombonecot) Eli2 Emanuel Guével (potatoesmaster) + Epoch + Eris Caffee (eris) eroen escondida Evgeniy Mineev (sandstranger) Federico Guerra (FedeWar) Fil Krynicki (filkry) Finbar Crago (finbar-crago) + Florent Teppe (Tetramir) Florian Weber (Florianjw) Frédéric Chardon (fr3dz10) Gaëtan Dezeiraud (Brouilles) @@ -86,17 +99,21 @@ Programmers gugus/gus guidoj Haoda Wang (h313) + holorat hristoast Internecine + Ivan Beloborodov (myrix) Jackerty Jacob Essex (Yacoby) Jacob Turnbull (Tankinfrank) Jake Westrip (16bitint) James Carty (MrTopCat) + James Deciutiis (JamesDeciutiis) James Moore (moore.work) James Stephens (james-h-stephens) Jan-Peter Nilsson (peppe) Jan Borsodi (am0s) + JanuarySnow Jason Hooks (jhooks) jeaye jefetienne @@ -108,6 +125,7 @@ Programmers John Blomberg (fstp) Jordan Ayers Jordan Milne + Josquin Frei Josua Grawitter Jules Blok (Armada651) julianko @@ -118,6 +136,7 @@ Programmers Kurnevsky Evgeny (kurnevsky) Lars Söderberg (Lazaroth) lazydev + Léo Peltier Leon Krieg (lkrieg) Leon Saunders (emoose) logzero @@ -125,6 +144,8 @@ Programmers Lordrea Łukasz Gołębiewski (lukago) Lukasz Gromanowski (lgro) + Mads Sandvei (Foal) + Maksim Eremenko (Max Yari) Marc Bouvier (CramitDeFrog) Marcin Hulist (Gohan) Mark Siewert (mark76) @@ -135,6 +156,7 @@ Programmers Mateusz Malisz (malice) Max Henzerling (SaintMercury) megaton + Mehdi Yousfi-Monod (mym) Michael Hogan (Xethik) Michael Mc Donnell Michael Papageorgiou (werdanith) @@ -146,6 +168,7 @@ Programmers Miroslav Puda (pakanek) MiroslavR Mitchell Schwitzer (schwitzerm) + Mitten.O naclander Narmo Nat Meo (Utopium) @@ -154,8 +177,10 @@ Programmers Nialsy Nick Crawford (nighthawk469) Nikolay Kasyanov (corristo) + Noah Gooder nobrakal Nolan Poe (nopoe) + Nurivan Gomez (Nuri-G) Oleg Chkan (mrcheko) Paul Cercueil (pcercuei) Paul McElroy (Greendogo) @@ -168,8 +193,10 @@ Programmers pkubik PLkolek PlutonicOverkill + Qlonever Radu-Marius Popovici (rpopovici) Rafael Moura (dhustkoder) + Randy Davin (Kindi) rdimesio rexelion riothamus @@ -186,6 +213,7 @@ Programmers Sergey Shambir (sergey-shambir) sergoz ShadowRadiance + Shihan42 Siimacore Simon Meulenbeek (simonmb) sir_herrbatka @@ -205,23 +233,28 @@ Programmers thegriglat Thomas Luppi (Digmaster) tlmullis + trav tri4ng1e Thoronador + Tobias Tribble (zackhasacat) + Tom Lowe (Vulpen) Tom Mason (wheybags) Torben Leif Carrington (TorbenC) unelsson uramer viadanna + Vidi_Aquam Vincent Heuken Vladimir Panteleev (CyberShadow) + vocollapse Wang Ryu (bzzt) Will Herrmann (Thunderforge) - vocollapse + Wolfgang Lieff xyzz Yohaulticetl Yuri Krupenin + Yury Stepovikov zelurker - Noah Gooder Documentation ------------- @@ -236,6 +269,7 @@ Documentation Joakim Berg (lysol90) Ryan Tucker (Ravenwing) sir_herrbatka + David Nagy (zuzaman) Packagers --------- diff --git a/CHANGELOG.md b/CHANGELOG.md index e11d05d3d4a..aef21779d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,505 @@ +0.49.0 +------ + + Bug #2623: Snowy Granius doesn't prioritize conjuration spells + Bug #3438: NPCs can't hit bull netch with melee weapons + Bug #3842: Body part skeletons override the main skeleton + Bug #4127: Weapon animation looks choppy + Bug #4204: Dead slaughterfish doesn't float to water surface after loading saved game + Bug #4207: RestoreHealth/Fatigue spells have a huge priority even if a success chance is near 0 + Bug #4382: Sound output device does not change when it should + Bug #4508: Can't stack enchantment buffs from different instances of the same self-cast generic magic apparel + Bug #4610: Casting a Bound Weapon spell cancels the casting animation by equipping the weapon prematurely + Bug #4683: Disposition decrease when player commits crime is not implemented properly + Bug #4742: Actors with wander never stop walking after Loopgroup Walkforward + Bug #4743: PlayGroup doesn't play non-looping animations correctly + Bug #4754: Stack of ammunition cannot be equipped partially + Bug #4816: GetWeaponDrawn returns 1 before weapon is attached + Bug #4822: Non-weapon equipment and body parts can't inherit time from parent animation + Bug #4898: Odd/Incorrect lighting on meshes + Bug #5057: Weapon swing sound plays at same pitch whether it hits or misses + Bug #5062: Root bone rotations for NPC animation don't work the same as for creature animation + Bug #5065: Actors with scripted animation still try to wander and turn around without moving + Bug #5066: Quirks with starting and stopping scripted animations + Bug #5129: Stuttering animation on Centurion Archer + Bug #5280: Unskinned shapes in skinned equipment are rendered in the wrong place + Bug #5371: Keyframe animation tracks are used for any file that begins with an X + Bug #5413: Enemies do a battlecry everytime the player summons a creature + Bug #5714: Touch spells cast using ExplodeSpell don't always explode + Bug #5755: Reset friendly hit counter + Bug #5849: Paralysis breaks landing + Bug #5870: Disposing of actors who were selected in the console doesn't deselect them like vanilla + Bug #5883: Immobile creatures don't cause water ripples + Bug #5977: Fatigueless NPCs' corpse underwater changes animation on game load + Bug #6025: Subrecords cannot overlap records + Bug #6027: Collisionshape becomes spiderweb-like when the mesh is too complex + Bug #6146: Lua command `actor:setEquipment` doesn't trigger mwscripts when equipping or unequipping a scripted item + Bug #6156: 1ft Charm or Sound magic effect vfx doesn't work properly + Bug #6190: Unintuitive sun specularity time of day dependence + Bug #6222: global map cell size can crash openmw if set to too high a value + Bug #6240: State sharing sometimes prevents the use of the same texture file for different purposes in shaders + Bug #6313: Followers with high Fight can turn hostile + Bug #6402: The sound of a thunderstorm does not stop playing after entering the premises + Bug #6427: Enemy health bar disappears before damaging effect ends + Bug #6550: Cloned body parts don't inherit texture effects + Bug #6574: Crash at far away from world origin coordinates + Bug #6645: Enemy block sounds align with animation instead of blocked hits + Bug #6665: The kobolds in the skyrim: home of the nords mod are oversized + Bug #6657: Distant terrain tiles become black when using FWIW mod + Bug #6661: Saved games that have no preview screenshot cause issues or crashes + Bug #6716: mwscript comparison operator handling is too restrictive + Bug #6723: "Turn to movement direction" makes the player rotate wildly with COLLADA + Bug #6754: Beast to Non-beast transformation mod is not working on OpenMW + Bug #6758: Main menu background video can be stopped by opening the options menu + Bug #6807: Ultimate Galleon is not working properly + Bug #6846: Launcher only works with default config paths + Bug #6893: Lua: Inconsistent behavior with actors affected by Disable and SetDelete commands + Bug #6894: Added item combines with equipped stack instead of creating a new unequipped stack + Bug #6932: Creatures flee from my followers and we have to chase after them + Bug #6939: OpenMW-CS: ID columns are too short + Bug #6949: Sun Damage effect doesn't work in quasi exteriors + Bug #6964: Nerasa Dralor Won't Follow + Bug #6973: Fade in happens after the scene load and is shown + Bug #6974: Only harmful effects are reflected + Bug #6977: Sun damage implementation does not match research + Bug #6985: Issues with Magic Cards numbers readability + Bug #6986: Sound magic effect does not make noise + Bug #6987: Set/Mod Blindness should not darken the screen + Bug #6992: Crossbow reloading doesn't look the same as in Morrowind + Bug #6993: Shooting your last round of ammunition causes the attack animation to cancel + Bug #7009: Falling actors teleport to the ground without receiving any damage on cell loading + Bug #7034: Misc items defined in one content file are not treated as keys if another content file uses them as such + Bug #7042: Weapon follow animations that immediately follow the hit animations cause multiple hits + Bug #7044: Changing a class' services does not affect autocalculated NPCs + Bug #7053: Running into objects doesn't trigger GetCollidingPC + Bug #7054: Quests aren't sorted by name + Bug #7064: NPCs don't report crime if the player is casting offensive spells on them while sneaking + Bug #7077: OpenMW fails to load certain particle effects in .osgt format + Bug #7084: Resurrecting an actor doesn't take into account base record changes + Bug #7088: Deleting last save game of last character doesn't clear character name/details + Bug #7092: BSA archives from higher priority directories don't take priority + Bug #7102: Some HQ Creatures mod models can hit the 8 texture slots limit with 0.48 + Bug #7103: Multiple paths pointing to the same plugin but with different cases lead to automatically removed config entries + Bug #7122: Teleportation to underwater should cancel active water walking effect + Bug #7131: MyGUI log spam when post processing HUD is open + Bug #7134: Saves with an invalid last generated RefNum can be loaded + Bug #7163: Myar Aranath: Wheat breaks the GUI + Bug #7168: Fix average scene luminance + Bug #7172: Current music playlist continues playing indefinitely if next playlist is empty + Bug #7202: Post-processing normals for terrain, water randomly stop rendering + Bug #7204: Missing actor scripts freeze the game + Bug #7229: Error marker loading failure is not handled + Bug #7243: Supporting loading external files from VFS from esm files + Bug #7284: "Your weapon has no effect." message doesn't always show when the player character attempts to attack + Bug #7292: Weather settings for disabling or enabling snow and rain ripples don't work + Bug #7298: Water ripples from projectiles sometimes are not spawned + Bug #7307: Alchemy "Magic Effect" search string does not match on tool tip for effects related to attributes + Bug #7309: Sunlight scattering is visible in inappropriate situations + Bug #7322: Shadows don't cover groundcover depending on the view angle and perspective with compute scene bounds = primitives + Bug #7354: Disabling post processing in-game causes a crash + Bug #7364: Post processing is not reflected in savegame previews + Bug #7380: NiZBufferProperty issue + Bug #7413: Generated wilderness cells don't spawn fish + Bug #7415: Unbreakable lock discrepancies + Bug #7416: Modpccrimelevel is different from vanilla + Bug #7428: AutoCalc flag is not used to calculate enchantment costs + Bug #7447: OpenMW-CS: Dragging a cell of a different type (from the initial type) into the 3D view crashes OpenMW-CS + Bug #7450: Evading obstacles does not work for actors missing certain animations + Bug #7459: Icons get stacked on the cursor when picking up multiple items simultaneously + Bug #7472: Crash when enchanting last projectiles + Bug #7475: Equipping a constant effect item doesn't update the magic menu + Bug #7502: Data directories dialog (0.48.0) forces adding subdirectory instead of intended directory + Bug #7505: Distant terrain does not support sample size greater than cell size + Bug #7535: Bookart paths for textures in OpenMW vs vanilla Morrowind + Bug #7553: Faction reaction loading is incorrect + Bug #7557: Terrain::ChunkManager::createChunk is called twice for the same position, lod on initial loading + Bug #7573: Drain Fatigue can't bring fatigue below zero by default + Bug #7585: Difference in interior lighting between OpenMW with legacy lighting method enabled and vanilla Morrowind + Bug #7587: Quick load related crash + Bug #7603: Scripts menu size is not updated properly + Bug #7604: Goblins Grunt becomes idle once injured + Bug #7609: ForceGreeting should not open dialogue for werewolves + Bug #7611: Beast races' idle animations slide after turning or jumping in place + Bug #7617: The death prompt asks the player if they wanted to load the character's last created save + Bug #7619: Long map notes may get cut off + Bug #7623: Incorrect placement of the script info in the engraved ring of healing tooltip + Bug #7630: Charm can be cast on creatures + Bug #7631: Cannot trade with/talk to Creeper or Mudcrab Merchant when they're fleeing + Bug #7633: Groundcover should ignore non-geometry Drawables + Bug #7636: Animations bug out when switching between 1st and 3rd person, while playing a scripted animation + Bug #7637: Actors can sometimes move while playing scripted animations + Bug #7639: NPCs don't use hand-to-hand if their other melee skills were damaged during combat + Bug #7641: loopgroup loops the animation one time too many for actors + Bug #7642: Items in repair and recharge menus aren't sorted alphabetically + Bug #7643: Can't enchant items with constant effect on self magic effects for non-player character + Bug #7646: Follower voices pain sounds when attacked with magic + Bug #7647: NPC walk cycle bugs after greeting player + Bug #7654: Tooltips for enchantments with invalid effects cause crashes + Bug #7660: Some inconsistencies regarding Invisibility breaking + Bug #7661: Player followers should stop attacking newly recruited actors + Bug #7665: Alchemy menu is missing the ability to deselect and choose different qualities of an apparatus + Bug #7675: Successful lock spell doesn't produce a sound + Bug #7676: Incorrect magic effect order in alchemy + Bug #7679: Scene luminance value flashes when toggling shaders + Bug #7685: Corky sometimes doesn't follow Llovyn Andus + Bug #7707: (OpenCS): New landscape records do not contain appropriate flags + Bug #7712: Casting doesn't support spells and enchantments with no effects + Bug #7721: CS: Special Chars Not Allowed in IDs + Bug #7723: Assaulting vampires and werewolves shouldn't be a crime + Bug #7724: Guards don't help vs werewolves + Bug #7733: Launcher shows incorrect data paths when there's two plugins with the same name + Bug #7737: OSG stats are missing some data on loading screens + Bug #7742: Governing attribute training limit should use the modified attribute + Bug #7753: Editor: Actors Don't Scale According to Their Race + Bug #7758: Water walking is not taken into account to compute path cost on the water + Bug #7761: Rain and ambient loop sounds are mutually exclusive + Bug #7763: Bullet shape loading problems, assorted + Bug #7765: OpenMW-CS: Touch Record option is broken + Bug #7769: Sword of the Perithia: Broken NPCs + Bug #7770: Sword of the Perithia: Script execution failure + Bug #7780: Non-ASCII texture paths in NIF files don't work + Bug #7785: OpenMW-CS initialising Skill and Attribute fields to 0 instead of -1 on non-FortifyStat spells + Bug #7794: Fleeing NPCs name tooltip doesn't appear + Bug #7796: Absorbed enchantments don't restore magicka + Bug #7823: Game crashes when launching it. + Bug #7832: Ingredient tooltips show magnitude for Fortify Maximum Magicka effect + Bug #7840: First run of the launcher doesn't save viewing distance as the default value + Bug #7841: Editor: "Dirty" water heights are saved in modified CELLs + Bug #7859: AutoCalc flag is not used to calculate potion value + Bug #7861: OpenMW-CS: Incorrect DIAL's type in INFO records + Bug #7872: Region sounds use wrong odds + Bug #7886: Equip and unequip animations can't share the animation track section + Bug #7887: Editor: Mismatched reported script data size and actual data size causes a crash during save + Bug #7896: Editor: Loading cellrefs incorrectly transforms Refnums, causing load failures + Bug #7898: Editor: Invalid reference scales are allowed + Bug #7899: Editor: Doors can't be unlocked + Bug #7901: Editor: Teleport-related fields shouldn't be editable if a ref does not teleport + Bug #7908: Key bindings names in the settings menu are layout-specific + Bug #7943: Using "addSoulGem" and "dropSoulGem" commands to creatures works only with "Weapon & Shield" flagged ones + Bug #7970: Difference of GetPCSleep (?) behavior between vanilla and OpenMW + Bug #7980: Paralyzed NPCs' lips move + Bug #7993: Cannot load Bloodmoon without Tribunal + Bug #7997: Can toggle perspective when paralyzed + Bug #8002: Portable light sources held by creatures do not emit lighting + Bug #8005: F3 stats bars are sorted not according to their place in the timeline + Bug #8018: Potion effects should never explode and always apply on self + Bug #8021: Player's scale doesn't reset when starting a new game + Bug #8048: Actors can generate negative collision extents and have no collision + Bug #8063: menu_background.bik video with audio freezes the game forever + Bug #8064: Lua move360 script doesn't respect the enableZoom/disableZoom Camera interface setting + Bug #8085: Don't search in scripts or shaders directories for "Select directories you wish to add" menu in launcher + Bug #8097: GetEffect doesn't detect 0 magnitude spells + Bug #8124: Normal weapon resistance is applied twice for NPCs + Bug #8132: Actors without hello responses turn to face the player + Feature #1415: Infinite fall failsafe + Feature #2566: Handle NAM9 records for manual cell references + Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking + Feature #3537: Shader-based water ripples + Feature #5173: Support for NiFogProperty + Feature #5492: Let rain and snow collide with statics + Feature #5926: Refraction based on water depth + Feature #5944: Option to use camera as sound listener + Feature #6009: Animation blending - smooth animation transitions with modding support + Feature #6152: Playing music via lua scripts + Feature #6188: Specular lighting from point light sources + Feature #6411: Support translations in openmw-launcher + Feature #6447: Add LOD support to Object Paging + Feature #6491: Add support for Qt6 + Feature #6556: Lua API for sounds + Feature #6679: Design a custom Input Action API + Feature #6726: Lua API for creating new objects + Feature #6727: Lua API for records of all object types + Feature #6864: Lua file access API + Feature #6922: Improve launcher appearance + Feature #6933: Support high-resolution cursor textures + Feature #6945: Support S3TC-compressed and BGR/BGRA NiPixelData + Feature #6979: Add support of loading and displaying LOD assets purely based on their filename extension + Feature #6983: PCVisionBonus script functions + Feature #6995: Localize the "show effect duration" option + Feature #7058: Implement TestModels (T3D) console command + Feature #7087: Block resolution change in the Windowed Fullscreen mode + Feature #7125: Remembering console commands between sessions + Feature #7129: Add support for non-adaptive VSync + Feature #7130: Ability to set MyGUI logging verbosity + Feature #7142: MWScript Lua API + Feature #7148: Optimize string literal lookup in mwscript + Feature #7161: OpenMW-CS: Make adding and filtering TopicInfos easier + Feature #7194: Ori to show texture paths + Feature #7214: Searching in the in-game console + Feature #7248: Searching in the console with regex and toggleable case-sensitivity + Feature #7468: Factions API for Lua + Feature #7477: NegativeLight Magic Effect flag + Feature #7499: OpenMW-CS: Generate record filters by drag & dropping cell content to the filters field + Feature #7546: Start the game on Fredas + Feature #7554: Controller binding for tab for menu navigation + Feature #7568: Uninterruptable scripted music + Feature #7590: [Lua] Ability to deserialize YAML data from scripts + Feature #7606: Launcher: allow Shift-select in Archives tab + Feature #7608: Make the missing dependencies warning when loading a savegame more helpful + Feature #7618: Show the player character's health in the save details + Feature #7625: Add some missing console error outputs + Feature #7634: Support NiParticleBomb + Feature #7648: Lua Save game API + Feature #7652: Sort inactive post processing shaders list properly + Feature #7698: Implement sAbsorb, sDamage, sDrain, sFortify and sRestore + Feature #7709: Improve resolution selection in Launcher + Feature #7777: Support external Bethesda material files (BGSM/BGEM) + Feature #7792: Support Timescale Clouds + Feature #7795: Support MaxNumberRipples INI setting + Feature #7805: Lua Menu context + Feature #7860: Lua: Expose NPC AI settings (fight, alarm, flee) + Feature #7875: Disable MyGUI windows snapping + Feature #7914: Do not allow to move GUI windows out of screen + Feature #7916: Expose all AiWander options to Lua, extend other packages as well + Feature #7923: Don't show non-existent higher ranks for factions with fewer than 9 ranks + Feature #7932: Support two-channel normal maps + Feature #7936: Scalable icons in Qt applications + Feature #7953: Allow to change SVG icons colors depending on color scheme + Feature #7964: Add Lua read access to MW Dialogue records + Feature #7971: Make save's Time Played value display hours instead of days + Feature #7985: Support dark mode on Windows + Feature #8034: (Lua) Containers should have respawning/organic flags + Feature #8067: Support Game Mode on macOS + Feature #8078: OpenMW-CS Terrain Equalize Tool + Feature #8087: Creature movement flags are not exposed + Feature #8145: Starter spell flag is not exposed + Task #5896: Do not use deprecated MyGUI properties + Task #6085: Replace boost::filesystem with std::filesystem + Task #6149: Dehardcode Lua API_REVISION + Task #6505: UTF-8 support in Lua scripts + Task #6624: Drop support for saves made prior to 0.45 + Task #7048: Get rid of std::bind + Task #7113: Move from std::atoi to std::from_char + Task #7117: Replace boost::scoped_array with std::vector + Task #7151: Do not use std::strerror to get errno error message + Task #7394: Drop support for --fs-strict + Task #7720: Drop 360-degree screenshot support + +0.48.0 +------ + + Bug #1751: Birthsign abilities increase modified attribute values instead of base ones + Bug #1930: Followers are still fighting if a target stops combat with a leader + Bug #2036: SetStat and ModStat instructions aren't implemented the same way as in Morrowind + Bug #3246: ESSImporter: Most NPCs are dead on save load + Bug #3488: AI combat aiming is too slow + Bug #3514: Editing a reference's position after loading an esp file makes the reference disappear + Bug #3737: Scripts from The Underground 2 .esp do not play (all patched versions) + Bug #3792: 1 frame late magicka recalc breaks early scripted magicka reactions to Intelligence change + Bug #3846: Strings starting with "-" fail to compile if not enclosed in quotes + Bug #3855: AI sometimes spams defensive spells + Bug #3867: All followers attack player when one follower enters combat with player + Bug #3905: Great House Dagoth issues + Bug #4175: Objects "vibrate" when extremely far from (0,0) + Bug #4203: Resurrecting an actor doesn't close the loot GUI + Bug #4227: Spellcasting restrictions are checked before spellcasting animations are played + Bug #4310: Spell description is centered + Bug #4374: Player rotation reset when nearing area that hasn't been loaded yet + Bug #4376: Moved actors don't respawn in their original cells + Bug #4389: NPC's lips do not move if his head model has the NiBSAnimationNode root node + Bug #4526: Crash when additional maps are applied over a model with out of bounds UV + Bug #4602: Robert's Bodies: crash inside createInstance() + Bug #4700: OpenMW-CS: Incorrect command implementation + Bug #4744: Invisible particles aren't always processed + Bug #4949: Incorrect particle lighting + Bug #5054: Non-biped creatures don't use spellcast equip/unequip animations + Bug #5088: Sky abruptly changes direction during certain weather transitions + Bug #5100: Persuasion doesn't always clamp the resulting disposition + Bug #5120: Scripted object spawning updates physics system + Bug #5192: Actor turn rate is too slow + Bug #5207: Loose summons can be present in scene + Bug #5279: Ingame console stops auto-scrolling after clicking output + Bug #5318: Aiescort behaves differently from vanilla + Bug #5377: Console does not appear after using menutest in inventory + Bug #5379: Wandering NPCs falling through cantons + Bug #5394: Windows snapping no longer works + Bug #5434: Pinned windows shouldn't cover breath progress bar + Bug #5453: Magic effect VFX are offset for creatures + Bug #5483: AutoCalc flag is not used to calculate spells cost + Bug #5508: Engine binary links to Qt without using it + Bug #5592: Weapon idle animations do not work properly + Bug #5596: Effects in constant spells should not be merged + Bug #5621: Drained stats cannot be restored + Bug #5766: Active grid object paging - disappearing textures + Bug #5788: Texture editing parses the selected indexes wrongly + Bug #5801: A multi-effect spell with the intervention effects and recall always favors Almsivi intervention + Bug #5842: GetDisposition adds temporary disposition change from different actors + Bug #5858: Visible modal windows and dropdowns crashing game on exit + Bug #5863: GetEffect should return true after the player has teleported + Bug #5913: Failed assertion during Ritual of Trees quest + Bug #5937: Lights always need to be rotated by 90 degrees + Bug #5976: Invisibility is broken when the attack starts instead of when it ends + Bug #5978: NPCs and Creatures talk to and headtrack a player character with a 75% chameleon effect or more + Bug #5989: Simple water isn't affected by texture filter settings + Bug #6037: Launcher: Morrowind content language cannot be set to English + Bug #6049: Main Theme on OpenMW should begin on the second video like Vanilla. + Bug #6051: NaN water height in ESM file is not handled gracefully + Bug #6054: Hotkey items can be equipped while in ready to attack stance + Bug #6066: Addtopic "return" does not work from within script. No errors thrown + Bug #6067: ESP loader fails for certain subrecord orders + Bug #6087: Bound items added directly to the inventory disappear if their corresponding spell effect ends + Bug #6101: Disarming trapped unlocked owned objects isn't considered a crime + Bug #6107: Fatigue is incorrectly recalculated when fortify effect is applied or removed + Bug #6109: Crash when playing a custom made menu_background file + Bug #6115: Showmap overzealous matching + Bug #6118: Creature landing sound counts as a footstep + Bug #6123: NPC with broken script freezes the game on hello + Bug #6129: Player avatar not displayed correctly for large window sizes when GUI scaling active + Bug #6131: Item selection in the avatar window not working correctly for large window sizes + Bug #6133: Cannot reliably sneak or steal in the sight of the NPCs siding with player + Bug #6142: Groundcover plugins change cells flags + Bug #6143: Capturing a screenshot renders the engine temporarily unresponsive + Bug #6154: Levitating player character is floating rather than on the floor when teleported back from Magas Volar + Bug #6165: Paralyzed player character can pickup items when the inventory is open + Bug #6168: Weather particles flicker for a frame at start of storms + Bug #6172: Some creatures can't open doors + Bug #6174: Spellmaking and Enchanting sliders differences from vanilla + Bug #6177: Followers of player follower stop following after waiting for a day + Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla + Bug #6191: Encumbrance messagebox timer works incorrectly + Bug #6197: Infinite Casting Loop + Bug #6253: Multiple instances of Reflect stack additively + Bug #6255: Reflect is different from vanilla + Bug #6256: Crash on exit with enabled shadows and statically linked OpenSceneGraph + Bug #6258: Barter menu glitches out when modifying prices + Bug #6273: Respawning NPCs rotation is inconsistent + Bug #6276: Deleted groundcover instances are not deleted in game + Bug #6282: Laura craft doesn't follow the player character + Bug #6283: Avis Dorsey follows you after her death + Bug #6285: OpenMW-CS: Brush template drawing and terrain selection drawing performance is very bad + Bug #6289: Keyword search in dialogues expected the text to be all ASCII characters + Bug #6291: Can't pickup the dead mage's journal from the mysterious hunter mod + Bug #6302: Teleporting disabled actor breaks its disabled state + Bug #6303: After "go to jail" weapon can be stuck in the ready to attack state + Bug #6307: Pathfinding in Infidelities quest from Tribunal addon is broken + Bug #6321: Arrow enchantments should always be applied to the target + Bug #6322: Total sold/cost should reset to 0 when there are no items offered + Bug #6323: Wyrmhaven: Alboin doesn't follower the player character out of his house + Bug #6324: Special Slave Companions: Can't buy the slave companions + Bug #6326: Detect Enchantment/Key should detect items in unresolved containers + Bug #6327: Blocking roots the character in place + Bug #6333: Werewolf stat changes should be implemented as damage/fortifications + Bug #6343: Magic projectile speed doesn't take race weight into account + Bug #6347: PlaceItem/PlaceItemCell/PlaceAt should work with levelled creatures + Bug #6354: SFX abruptly cut off after crossing max distance + Bug #6358: Changeweather command does not report an error when entering non-existent region + Bug #6363: Some scripts in Morrowland fail to work + Bug #6376: Creatures should be able to use torches + Bug #6386: Artifacts in water reflection due to imprecise screen-space coordinate computation + Bug #6389: Maximum light distance setting doesn't affect water reflections + Bug #6395: Translations with longer tab titles may cause tabs to disappear from the options menu + Bug #6396: Inputting certain Unicode characters triggers an assertion + Bug #6416: Morphs are applied to the wrong target + Bug #6417: OpenMW doesn't always use the right node to accumulate movement + Bug #6429: Wyrmhaven: Can't add AI packages to player + Bug #6433: Items bound to Quick Keys sometimes do not appear until the Quick Key menu is opened + Bug #6451: Weapon summoned from Cast When Used item will have the name "None" + Bug #6473: Strings from NIF should be parsed only to first null terminator + Bug #6493: Unlocking owned but not locked or unlocked containers is considered a crime + Bug #6517: Rotations for KeyframeData in NIFs should be optional + Bug #6519: Effects tooltips for ingredients work incorrectly + Bug #6523: Disintegrate Weapon is resisted by Resist Magicka instead of Sanctuary + Bug #6544: Far from world origin objects jitter when camera is still + Bug #6545: Player character momentum is preserved when going to a different cell + Bug #6559: Weapon condition inconsistency between melee and ranged critical / sneak / KO attacks + Bug #6579: OpenMW compilation error when using OSG doubles for BoundingSphere + Bug #6606: Quests with multiple IDs cannot always be restarted + Bug #6653: With default settings the in-game console doesn't fit into screen + Bug #6655: Constant effect absorb attribute causes the game to break + Bug #6667: Pressing the Esc key while resting or waiting causes black screen. + Bug #6670: Dialogue order is incorrect + Bug #6680: object.cpp handles nodetree unsafely, memory access with dangling pointer + Bug #6682: HitOnMe doesn't fire as intended + Bug #6697: Shaders vertex lighting incorrectly clamped + Bug #6705: OpenMW CS: A typo in the Creature levelled list + Bug #6711: Log time differs from real time + Bug #6717: Broken script causes interpreter stack corruption + Bug #6718: Throwable weapons cause arrow enchantment effect to be applied to the whole body + Bug #6730: LoopGroup stalls animation after playing :Stop frame until another animation is played + Bug #6753: Info records without a DATA subrecords are loaded incorrectly + Bug #6794: Light sources are attached to mesh bounds centers instead of mesh origins when AttachLight NiNode is missing + Bug #6799: Game crashes if an NPC has no Class attached + Bug #6849: ImageButton texture is not scaled properly + Bug #6860: Sinnammu randomly strafes while running on water + Bug #6869: Hits queue stagger during swing animation + Bug #6890: SDL_PeepEvents errors are not handled + Bug #6895: Removing a negative number of items from a script, makes the script terminate with an error + Bug #6896: Sounds played using PlaySound3D are cut off as the emitter leaves the cell + Bug #6898: Accessing the Quick Inventory menu does not work while in menu mode + Bug #6901: Morrowind.exe soul gem usage discrepancy + Bug #6909: Using enchanted items has no animation + Bug #6910: Torches should not be extinguished when not being held + Bug #6913: Constant effect enchanted items don't break invisibility + Bug #6923: Dispose of corpse prevents respawning after load + Bug #6937: Divided by Nix Hounds quest is broken + Bug #7008: Race condition on initializing a vector of reserved node names + Bug #7121: Crash on TimeStamp construction with invalid hour value + Bug #7251: Force shaders setting still renders some drawables with FFP + Feature #890: OpenMW-CS: Column filtering + Feature #1465: "Reset" argument for AI functions + Feature #2491: Ability to make OpenMW "portable" + Feature #2554: OpenMW-CS: Modifying an object in the cell view should trigger the instances table to scroll to the corresponding record + Feature #2766: Warn user if their version of Morrowind is not the latest. + Feature #2780: A way to see current OpenMW version in the console + Feature #2858: Add a tab to the launcher for handling datafolders + Feature #3180: Support uncompressed colour-mapped TGA files + Feature #3245: OpenMW-CS: Instance editing grid + Feature #3616: Allow Zoom levels on the World Map + Feature #3668: Support palettized DDS files + Feature #4067: Post Processing + Feature #4297: Implement APPLIED_ONCE flag for magic effects + Feature #4414: Handle duration of EXTRA SPELL magic effect + Feature #4595: Unique object identifier + Feature #4974: Overridable MyGUI layout + Feature #4975: Built-in TrueType fonts + Feature #5198: Implement "Magic effect expired" event + Feature #5454: Clear active spells from actor when he disappears from scene + Feature #5489: MCP: Telekinesis fix for activators + Feature #5701: Convert osgAnimation::RigGeometry to double-buffered custom version + Feature #5737: OpenMW-CS: Handle instance move from one cell to another + Feature #5928: Allow Glow in the Dahrk to be disabled + Feature #5996: Support Lua scripts in OpenMW + Feature #6017: Separate persistent and temporary cell references when saving + Feature #6019: Add antialias alpha test to the launcher or enable by default if possible + Feature #6032: Reverse-z depth buffer + Feature #6078: Do not clear depth buffer for first-person meshes + Feature #6128: Soft Particles + Feature #6171: In-game log viewer + Feature #6189: Navigation mesh disk cache + Feature #6199: Support FBO Rendering + Feature #6248: Embedded error marker mesh + Feature #6249: Alpha testing support for Collada + Feature #6251: OpenMW-CS: Set instance movement based on camera zoom + Feature #6288: OpenMW-CS: Preserve "blocked" record flags when saving + Feature #6360: More realistic raindrop ripples + Feature #6380: Treat commas as whitespace in scripts + Feature #6419: Don't grey out topics if they can produce another topic reference + Feature #6443: Support NiStencilProperty + Feature #6496: Handle NCC flag in NIF files + Feature #6534: Shader-based object texture blending + Feature #6541: Gloss-mapping + Feature #6557: Add support for controller gyroscope + Feature #6592: Support for NiTriShape particle emitters + Feature #6600: Support NiSortAdjustNode + Feature #6631: Support FFMPEG 5 + Feature #6684: Support NiFltAnimationNode + Feature #6699: Support Ignored flag + Feature #6700: Support windowed fullscreen + Feature #6706: Save the size of the Options window + Feature #6721: OpenMW-CS: Add option to open records in new window + Feature #6823: Animation layering for osgAnimation formats + Feature #6867: Add a way to localize hardcoded strings in GUI + Feature #6888: Add switch for armor degradation fix + Feature #6925: Allow to use a mouse wheel to rotate a head in the race selection menu + Feature #6941: Allow users to easily change font size and ttf resolution + Feature #7434: Exponential fog + Feature #7435: Sky blending + Task #5534: Remove support for OSG 3.4 + Task #6161: Refactor Sky to use shaders and be GLES/GL3 friendly + Task #6162: Refactor GUI to use shaders and to be GLES and GL3+ friendly + Task #6435: Add support for MSVC 2022 + Task #6564: Remove predefined data paths `data="?global?data"`, `data=./data` + 0.47.0 ------ @@ -7,7 +509,6 @@ Bug #2069: Fireflies in Fireflies invade Morrowind look wrong Bug #2311: Targeted scripts are not properly supported on non-unique RefIDs Bug #2473: Unable to overstock merchants - Bug #2798: Mutable ESM records Bug #2976: [reopened]: Issues combining settings from the command line and both config files Bug #3137: Walking into a wall prevents jumping Bug #3372: Projectiles and magic bolts go through moving targets @@ -20,7 +521,6 @@ Bug #4039: Multiple followers should have the same following distance Bug #4055: Local scripts don't inherit variables from their base record Bug #4083: Door animation freezes when colliding with actors - Bug #4201: Projectile-projectile collision Bug #4247: Cannot walk up stairs in Ebonheart docks Bug #4357: OpenMW-CS: TopicInfos index sorting and rearranging isn't fully functional Bug #4363: OpenMW-CS: Defect in Clone Function for Dialogue Info records @@ -89,7 +589,7 @@ Bug #5644: Summon effects running on the player during game initialization cause crashes Bug #5656: Sneaking characters block hits while standing Bug #5661: Region sounds don't play at the right interval - Bug #5675: OpenMW-cs. FRMR subrecords are saved with the wrong MastIdx + Bug #5675: OpenMW-CS: FRMR subrecords are saved with the wrong MastIdx Bug #5680: Bull Netches incorrectly aim over the player character's head and always miss Bug #5681: Player character can clip or pass through bridges instead of colliding against them Bug #5687: Bound items covering the same inventory slot expiring at the same time freezes the game @@ -102,7 +602,7 @@ Bug #5739: Saving and loading the save a second or two before hitting the ground doesn't count fall damage Bug #5758: Paralyzed actors behavior is inconsistent with vanilla Bug #5762: Movement solver is insufficiently robust - BUG #5800: Equipping a CE enchanted ring deselects an already equipped and selected enchanted ring from the spell menu + Bug #5800: Equipping a CE enchanted ring deselects an already equipped and selected enchanted ring from the spell menu Bug #5807: Video decoding crash on ARM Bug #5821: NPCs from mods getting removed if mod order was changed Bug #5835: OpenMW doesn't accept negative values for NPC's hello, alarm, fight, and flee @@ -132,14 +632,20 @@ Bug #6036: OpenMW-CS: Terrain selection at the border of cells omits certain corner vertices Bug #6043: Actor can have torch missing when torch animation is played Bug #6047: Mouse bindings can be triggered during save loading + Bug #6136: Game freezes when NPCs try to open doors that are about to be closed + Bug #6294: Game crashes with empty pathgrid Feature #390: 3rd person look "over the shoulder" Feature #832: OpenMW-CS: Handle deleted references Feature #1536: Show more information about level on menu + Feature #2159: "Graying out" exhausted dialogue topics Feature #2386: Distant Statics in the form of Object Paging Feature #2404: Levelled List can not be placed into a container Feature #2686: Timestamps in openmw.log + Feature #2798: Mutable ESM records Feature #3171: OpenMW-CS: Instance drag selection Feature #3983: Wizard: Add link to buy Morrowind + Feature #4201: Projectile-projectile collision + Feature #4486: Handle crashes on Windows Feature #4894: Consider actors as obstacles for pathfinding Feature #4899: Alpha-To-Coverage Anti-Aliasing for alpha testing Feature #4917: Do not trigger NavMesh update when RecastMesh update should not change NavMesh @@ -157,6 +663,7 @@ Feature #5519: Code Patch tab in launcher Feature #5524: Resume failed script execution after reload Feature #5545: Option to allow stealing from an unconscious NPC during combat + Feature #5551: Do not reboot PC after OpenMW installation on Windows Feature #5563: Run physics update in background thread Feature #5579: MCP SetAngle enhancement Feature #5580: Service refusal filtering @@ -171,12 +678,13 @@ Feature #5814: Bsatool should be able to create BSA archives, not only to extract it Feature #5828: Support more than 8 lights Feature #5910: Fall back to delta time when physics can't keep up + Feature #5980: Support Bullet with double precision instead of one with single precision Feature #6024: OpenMW-CS: Selecting terrain in "Terrain land editing" should support "Add to selection" and "Remove from selection" modes Feature #6033: Include pathgrid to navigation mesh Feature #6034: Find path based on area cost depending on NPC stats + Feature #7161: OpenMW-CS: Make adding and filtering TopicInfos easier Task #5480: Drop Qt4 support Task #5520: Improve cell name autocompleter implementation - Task #5844: Update 'toggle sneak' documentation 0.46.0 ------ @@ -186,7 +694,7 @@ Bug #2395: Duplicated plugins in the launcher when multiple data directories provide the same plugin Bug #2679: Unable to map mouse wheel under control settings Bug #2969: Scripted items can stack - Bug #2976: Data lines in global openmw.cfg take priority over user openmw.cfg + Bug #2976: [reopened in 0.47] Data lines in global openmw.cfg take priority over user openmw.cfg Bug #2987: Editor: some chance and AI data fields can overflow Bug #3006: 'else if' operator breaks script compilation Bug #3109: SetPos/Position handles actors differently @@ -204,7 +712,6 @@ Bug #4009: Launcher does not show data files on the first run after installing Bug #4077: Enchanted items are not recharged if they are not in the player's inventory Bug #4141: PCSkipEquip isn't set to 1 when reading books/scrolls - Bug #4202: Open .omwaddon files without needing toopen openmw-cs first Bug #4240: Ash storm origin coordinates and hand shielding animation behavior are incorrect Bug #4262: Rain settings are hardcoded Bug #4270: Closing doors while they are obstructed desyncs closing sfx @@ -294,7 +801,6 @@ Bug #4964: Multiple effect spell projectile sounds play louder than vanilla Bug #4965: Global light attenuation settings setup is lacking Bug #4969: "Miss" sound plays for any actor - Bug #4971: OpenMW-CS: Make rotations display as degrees instead of radians Bug #4972: Player is able to use quickkeys while disableplayerfighting is active Bug #4979: AiTravel maximum range depends on "actors processing range" setting Bug #4980: Drowning mechanics is applied for actors indifferently from distance to player @@ -392,7 +898,6 @@ Bug #5350: An attempt to launch magic bolt causes "AL error invalid value" error Bug #5352: Light source items' duration is decremented while they aren't visible Feature #1724: Handle AvoidNode - Feature #2159: "Graying out" exhausted dialogue topics Feature #2229: Improve pathfinding AI Feature #3025: Analogue gamepad movement controls Feature #3442: Default values for fallbacks from ini file @@ -405,6 +910,7 @@ Feature #4001: Toggle sneak controller shortcut Feature #4068: OpenMW-CS: Add a button to reset key bindings to defaults Feature #4129: Beta Comment to File + Feature #4202: Open .omwaddon files without needing to open openmw-cs first Feature #4209: Editor: Faction rank sub-table Feature #4255: Handle broken RepairedOnMe script function Feature #4316: Implement RaiseRank/LowerRank functions properly @@ -427,6 +933,7 @@ Feature #4958: Support eight blood types Feature #4962: Add casting animations for magic items Feature #4968: Scalable UI widget skins + Feature #4971: OpenMW-CS: Make rotations display as degrees instead of radians Feature #4994: Persistent pinnable windows hiding Feature #5000: Compressed BSA format support Feature #5005: Editor: Instance window via Scene window @@ -1903,6 +2410,7 @@ Bug #2025: Missing mouse-over text for non affordable items Bug #2028: [MOD: Tamriel Rebuilt] Crashing when trying to enter interior cell "Ruinous Keep, Great Hall" Bug #2029: Ienith Brothers Thiev's Guild quest journal entry not adding + Bug #3066: Editor doesn't check if IDs and other strings are longer than their hardcoded field length Feature #471: Editor: Special case implementation for top-level window with single sub-window Feature #472: Editor: Sub-Window re-use settings Feature #704: Font colors import from fallback settings diff --git a/CHANGELOG_PR.md b/CHANGELOG_PR.md deleted file mode 100644 index 100bc376ff7..00000000000 --- a/CHANGELOG_PR.md +++ /dev/null @@ -1,53 +0,0 @@ -*** PLEASE PUT YOUR ISSUE DESCRIPTION FOR DUMMIES HERE FOR REVIEW *** - -- I'm just a placeholder description (#1337) -- I'm also just a placeholder description, but I'm a more recent one (#42) - -*** - -0.47.0 ------- - -The OpenMW team is proud to announce the release of version 0.47.0! Grab it from our Downloads Page for all operating systems. ***short summary: XXX *** - -Check out the release video (***add link***) and the OpenMW-CS release video (***add link***) by the ***add flattering adjective*** Atahualpa, and see below for the full list of changes. - -Known Issues: -- To use generic Linux binaries, Qt4 and libpng12 must be installed on your system -- On macOS, launching OpenMW from OpenMW-CS requires OpenMW.app and OpenMW-CS.app to be siblings - -New Features: -- Dialogue to split item stacks now displays the name of the trapped soul for stacks of soul gems (#5362) -- Basics of Collada animations are now supported via osgAnimation plugin (#5456) - -New Editor Features: -- Instance selection modes are now implemented (centred cube, corner-dragged cube, sphere) with four user-configurable actions (select only, add to selection, remove from selection, invert selection) (#3171) - -Bug Fixes: -- NiParticleColorModifier in NIF files is now properly handled which solves issues regarding particle effects, e.g., smoke and fire (#1952, #3676) -- Targetting non-unique actors in scripts is now supported (#2311) -- Guards no longer ignore attacks of invisible players but rather initiate dialogue and flee if the player resists being arrested (#4774) -- Changing the dialogue window without closing it no longer clears the dialogue history in order to allow, e.g., emulation of three-way dialogue via ForceGreeting (#5358) -- Scripts which try to start a non-existent global script now skip that step and continue execution instead of breaking (#5364) -- Selecting already equipped spells or magic items via hotkey no longer triggers the equip sound to play (#5367) -- 'Scale' argument in levelled creature lists is now taken into account when spawning creatures from such lists (#5369) -- Morrowind legacy madness: Using a key on a trapped door/container now only disarms the trap if the door/container is locked (#5370) - -Editor Bug Fixes: -- Deleted and moved objects within a cell are now saved properly (#832) -- Disabled record sorting in Topic and Journal Info tables, implemented drag-move for records (#4357) -- Topic and Journal Info records can now be cloned with a different parent Topic/Journal Id (#4363) -- Verifier no longer checks for alleged 'race' entries in clothing body parts (#5400) -- Cell borders are now properly redrawn when undoing/redoing terrain changes (#5473) -- Loading mods now keeps the master index (#5675) -- Flicker and crashing on XFCE4 fixed (#5703) -- Collada models render properly in the Editor (#5713) -- Terrain-selection grid is now properly updated when undoing/redoing terrain changes (#6022) -- Tool outline and select/edit actions in "Terrain land editing" mode now ignore references (#6023) -- Primary-select and secondary-select actions in "Terrain land editing" mode now behave like in "Instance editing" mode (#6024) -- Using the circle brush to select terrain in the "Terrain land editing" mode no longer selects vertices outside the circle (#6035) -- Vertices at the NW and SE corners of a cell can now also be selected in "Terrain land editing" mode if the adjacent cells aren't loaded yet (#6036) - -Miscellaneous: -- Prevent save-game bloating by using an appropriate fog texture format (#5108) -- Ensure that 'Enchantment autocalc" flag is treated as flag in OpenMW-CS and in our esm tools (#5363) diff --git a/CI/Store-Symbols.ps1 b/CI/Store-Symbols.ps1 new file mode 100644 index 00000000000..53751213b83 --- /dev/null +++ b/CI/Store-Symbols.ps1 @@ -0,0 +1,84 @@ +param ( + [switch] $SkipCompress +) + +$ErrorActionPreference = "Stop" + +if (-Not (Test-Path CMakeCache.txt)) +{ + Write-Error "This script must be run from the build directory." +} + +if (-Not (Test-Path .cmake\api\v1\reply\index-*.json) -Or -Not ((Get-Content -Raw .cmake\api\v1\reply\index-*.json | ConvertFrom-Json).reply.PSObject.Properties.Name -contains "codemodel-v2")) +{ + Write-Output "Running CMake query..." + New-Item -Type File -Force .cmake\api\v1\query\codemodel-v2 + cmake . + if ($LASTEXITCODE -ne 0) { + Write-Error "Command exited with code $LASTEXITCODE" + } + Write-Output "Done." +} + +try +{ + Push-Location .cmake\api\v1\reply + + $index = Get-Content -Raw index-*.json | ConvertFrom-Json + + $codemodel = Get-Content -Raw $index.reply."codemodel-v2".jsonFile | ConvertFrom-Json + + $targets = @() + $codemodel.configurations | ForEach-Object { + $_.targets | ForEach-Object { + $target = Get-Content -Raw $_.jsonFile | ConvertFrom-Json + if ($target.type -eq "EXECUTABLE" -or $target.type -eq "SHARED_LIBRARY") + { + $targets += $target + } + } + } + + $artifacts = @() + $targets | ForEach-Object { + $_.artifacts | ForEach-Object { + $artifacts += $_.path + } + } +} +finally +{ + Pop-Location +} + +if (-not (Test-Path symstore-venv)) +{ + python -m venv symstore-venv + if ($LASTEXITCODE -ne 0) { + Write-Error "Command exited with code $LASTEXITCODE" + } +} +$symstoreVersion = "0.3.4" +if (-not (Test-Path symstore-venv\Scripts\symstore.exe) -or -not ((symstore-venv\Scripts\pip show symstore | Select-String '(?<=Version: ).*').Matches.Value -eq $symstoreVersion)) +{ + symstore-venv\Scripts\pip install symstore==$symstoreVersion + if ($LASTEXITCODE -ne 0) { + Write-Error "Command exited with code $LASTEXITCODE" + } +} + +$artifacts = $artifacts | Where-Object { Test-Path $_ } + +Write-Output "Storing symbols..." + +$optionalArgs = @() +if (-not $SkipCompress) { + $optionalArgs += "--compress" +} + +symstore-venv\Scripts\symstore $optionalArgs --skip-published .\SymStore @artifacts +if ($LASTEXITCODE -ne 0) { + Write-Error "Command exited with code $LASTEXITCODE" +} + +Write-Output "Done." diff --git a/CI/activate_msvc.sh b/CI/activate_msvc.sh index 233f0174331..c62ea4ca6d8 100644 --- a/CI/activate_msvc.sh +++ b/CI/activate_msvc.sh @@ -23,11 +23,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then fi command -v unixPathAsWindows >/dev/null 2>&1 || function unixPathAsWindows { - if command -v cygpath >/dev/null 2>&1; then - cygpath -w $1 - else - echo "$1" | sed "s,^/\([^/]\)/,\\1:/," | sed "s,/,\\\\,g" - fi + if command -v cygpath >/dev/null 2>&1; then + cygpath -w $1 + else + echo "$1" | sed "s,^/\([^/]\)/,\\1:/," | sed "s,/,\\\\,g" + fi } diff --git a/CI/before_install.android.sh b/CI/before_install.android.sh index 0243a960921..712ded2769c 100755 --- a/CI/before_install.android.sh +++ b/CI/before_install.android.sh @@ -1,4 +1,4 @@ #!/bin/sh -ex -curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201129.zip -o ~/openmw-android-deps.zip -unzip -o ~/openmw-android-deps -d /usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null +curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20211114.zip -o ~/openmw-android-deps.zip +unzip -o ~/openmw-android-deps -d /android-ndk-r22/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null diff --git a/CI/before_install.linux.sh b/CI/before_install.linux.sh deleted file mode 100755 index eff3fd7196f..00000000000 --- a/CI/before_install.linux.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -ex - -#sudo ln -sf /usr/bin/clang-6 /usr/local/bin/clang -#sudo ln -sf /usr/bin/clang++-6 /usr/local/bin/clang++ diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 1ca0fc61195..660ecf4adce 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -1,22 +1,34 @@ #!/bin/sh -ex -# workaround python issue on travis -[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.8 || true -[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.9 || true -[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies qt@6 || true +export HOMEBREW_NO_EMOJI=1 +export HOMEBREW_NO_INSTALL_CLEANUP=1 +export HOMEBREW_AUTOREMOVE=1 + +# workaround for gitlab's pre-installed brew +# purge large and unnecessary packages that get in our way and have caused issues +brew uninstall ruby php openjdk node postgresql maven curl || true + +brew tap --repair +brew update --quiet # Some of these tools can come from places other than brew, so check before installing +brew install curl xquartz gd fontconfig freetype harfbuzz brotli + 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="/usr/local/opt/qt@5/bin:$PATH" # needed to use qmake in none default path as qt now points to qt6 +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 -curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20210617.zip -o ~/openmw-deps.zip -unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null - -# additional libraries -[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew install fontconfig \ No newline at end of file +if [[ "${MACOS_AMD64}" ]]; then + curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20221113.zip -o ~/openmw-deps.zip +else + curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240802_arm64.zip -o ~/openmw-deps.zip +fi +unzip -o ~/openmw-deps.zip -d /tmp > /dev/null diff --git a/CI/before_script.android.sh b/CI/before_script.android.sh index 3ea429f1bb8..cbf8d649050 100755 --- a/CI/before_script.android.sh +++ b/CI/before_script.android.sh @@ -3,13 +3,31 @@ # hack to work around: FFmpeg version is too old, 3.2 is required sed -i s/"NOT FFVER_OK"/"FALSE"/ CMakeLists.txt +# Silence a git warning +git config --global advice.detachedHead false + mkdir -p build cd build +# Build a version of ICU for the host so that it can use the tools during the cross-compilation +mkdir -p icu-host-build +cd icu-host-build +if [ -r ../extern/fetched/icu/icu4c/source/configure ]; then + ICU_SOURCE_DIR=../extern/fetched/icu/icu4c/source +else + wget https://github.com/unicode-org/icu/archive/refs/tags/release-70-1.zip + unzip release-70-1.zip + ICU_SOURCE_DIR=./icu-release-70-1/icu4c/source +fi +${ICU_SOURCE_DIR}/configure --disable-tests --disable-samples --disable-icuio --disable-extras CC="ccache gcc" CXX="ccache g++" +make -j $(nproc) +cd .. + cmake \ --DCMAKE_TOOLCHAIN_FILE=/usr/lib/android-sdk/ndk-bundle/build/cmake/android.toolchain.cmake \ +-DCMAKE_TOOLCHAIN_FILE=/android-ndk-r22/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a \ -DANDROID_PLATFORM=android-21 \ +-DANDROID_LD=deprecated \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DCMAKE_INSTALL_PREFIX=install \ @@ -21,7 +39,11 @@ cmake \ -DBUILD_ESSIMPORTER=0 \ -DBUILD_OPENCS=0 \ -DBUILD_WIZARD=0 \ +-DBUILD_NAVMESHTOOL=OFF \ +-DBUILD_BULLETOBJECTTOOL=OFF \ -DOPENMW_USE_SYSTEM_MYGUI=OFF \ --DOPENMW_USE_SYSTEM_OSG=OFF \ --DOPENMW_USE_SYSTEM_BULLET=OFF \ +-DOPENMW_USE_SYSTEM_SQLITE3=OFF \ +-DOPENMW_USE_SYSTEM_YAML_CPP=OFF \ +-DOPENMW_USE_SYSTEM_ICU=OFF \ +-DOPENMW_ICU_HOST_BUILD_DIR="$(pwd)/icu-host-build" \ .. diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh index 17292e4e982..2589c2807eb 100755 --- a/CI/before_script.linux.sh +++ b/CI/before_script.linux.sh @@ -4,33 +4,67 @@ set -xeo pipefail free -m -BUILD_UNITTESTS=OFF -BUILD_BENCHMARKS=OFF - -if [[ "${BUILD_TESTS_ONLY}" ]]; then - export GOOGLETEST_DIR="${PWD}/googletest/build/install" - env GENERATOR='Unix Makefiles' CONFIGURATION=Release CI/build_googletest.sh - BUILD_UNITTESTS=ON - BUILD_BENCHMARKS=ON -fi +# Silence a git warning +git config --global advice.detachedHead false +# setup our basic cmake build options declare -a CMAKE_CONF_OPTS=( -DCMAKE_C_COMPILER="${CC:-/usr/bin/cc}" -DCMAKE_CXX_COMPILER="${CXX:-/usr/bin/c++}" -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_INSTALL_PREFIX=install - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DBUILD_SHARED_LIBS=OFF + -DBUILD_SHARED_LIBS="${BUILD_SHARED_LIBS:-OFF}" -DUSE_SYSTEM_TINYXML=ON - -DCMAKE_INSTALL_PREFIX=install + -DOPENMW_USE_SYSTEM_RECASTNAVIGATION=ON + -DOPENMW_CXX_FLAGS="-Werror -Werror=implicit-fallthrough" # flags specific to OpenMW project ) +if [[ "${CMAKE_EXE_LINKER_FLAGS}" ]]; then + CMAKE_CONF_OPTS+=( + -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" + ) +fi + if [[ $CI_OPENMW_USE_STATIC_DEPS ]]; then CMAKE_CONF_OPTS+=( -DOPENMW_USE_SYSTEM_MYGUI=OFF -DOPENMW_USE_SYSTEM_OSG=OFF -DOPENMW_USE_SYSTEM_BULLET=OFF + -DOPENMW_USE_SYSTEM_SQLITE3=OFF + -DOPENMW_USE_SYSTEM_RECASTNAVIGATION=OFF + ) +fi + +if [[ $CI_CLANG_TIDY ]]; then + CMAKE_CONF_OPTS+=( + -DCMAKE_CXX_CLANG_TIDY="clang-tidy;--warnings-as-errors=*" + -DBUILD_COMPONENTS_TESTS=ON + -DBUILD_OPENMW_TESTS=ON + -DBUILD_OPENCS_TESTS=ON + -DBUILD_BENCHMARKS=ON + ) +fi + +if [[ "${CMAKE_BUILD_TYPE}" ]]; then + CMAKE_CONF_OPTS+=( + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + ) +else + CMAKE_CONF_OPTS+=( + -DCMAKE_BUILD_TYPE=RelWithDebInfo + ) +fi + +if [[ "${CMAKE_CXX_FLAGS_DEBUG}" ]]; then + CMAKE_CONF_OPTS+=( + -DCMAKE_CXX_FLAGS_DEBUG="${CMAKE_CXX_FLAGS_DEBUG}" + ) +fi + +if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then + CMAKE_CONF_OPTS+=( + -DBUILD_WITH_CODE_COVERAGE="${BUILD_WITH_CODE_COVERAGE}" ) fi @@ -38,6 +72,16 @@ mkdir -p build cd build if [[ "${BUILD_TESTS_ONLY}" ]]; then + + # flags specific to our test suite + CXX_FLAGS="-Wno-error=deprecated-declarations -Wno-error=nonnull -Wno-error=deprecated-copy" + if [[ "${CXX}" == 'clang++' ]]; then + CXX_FLAGS="${CXX_FLAGS} -Wno-error=unused-lambda-capture -Wno-error=gnu-zero-variadic-macro-arguments" + fi + CMAKE_CONF_OPTS+=( + -DCMAKE_CXX_FLAGS="${CXX_FLAGS}" + ) + ${ANALYZE} cmake \ "${CMAKE_CONF_OPTS[@]}" \ -DBUILD_OPENMW=OFF \ @@ -48,10 +92,28 @@ if [[ "${BUILD_TESTS_ONLY}" ]]; then -DBUILD_ESSIMPORTER=OFF \ -DBUILD_OPENCS=OFF \ -DBUILD_WIZARD=OFF \ - -DBUILD_UNITTESTS=${BUILD_UNITTESTS} \ - -DBUILD_BENCHMARKS=${BUILD_BENCHMARKS} \ - -DGTEST_ROOT="${GOOGLETEST_DIR}" \ - -DGMOCK_ROOT="${GOOGLETEST_DIR}" \ + -DBUILD_NAVMESHTOOL=OFF \ + -DBUILD_BULLETOBJECTTOOL=OFF \ + -DBUILD_NIFTEST=OFF \ + -DBUILD_COMPONENTS_TESTS=ON \ + -DBUILD_OPENMW_TESTS=ON \ + -DBUILD_OPENCS_TESTS=ON \ + -DBUILD_BENCHMARKS=ON \ + .. +elif [[ "${BUILD_OPENMW_ONLY}" ]]; then + ${ANALYZE} cmake \ + "${CMAKE_CONF_OPTS[@]}" \ + -DBUILD_OPENMW=ON \ + -DBUILD_BSATOOL=OFF \ + -DBUILD_ESMTOOL=OFF \ + -DBUILD_LAUNCHER=OFF \ + -DBUILD_MWINIIMPORTER=OFF \ + -DBUILD_ESSIMPORTER=OFF \ + -DBUILD_OPENCS=OFF \ + -DBUILD_WIZARD=OFF \ + -DBUILD_NAVMESHTOOL=OFF \ + -DBUILD_BULLETOBJECTTOOL=OFF \ + -DBUILD_NIFTEST=OFF \ .. else ${ANALYZE} cmake \ diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index bb662c9de19..f16f241ff12 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -14,16 +14,6 @@ MISSINGTOOLS=0 command -v 7z >/dev/null 2>&1 || { echo "Error: 7z (7zip) is not on the path."; MISSINGTOOLS=1; } command -v cmake >/dev/null 2>&1 || { echo "Error: cmake (CMake) is not on the path."; MISSINGTOOLS=1; } -MISSINGPYTHON=0 -if ! command -v python >/dev/null 2>&1; then - echo "Warning: Python is not on the path, automatic Qt installation impossible." - MISSINGPYTHON=1 -elif ! python --version >/dev/null 2>&1; then - echo "Warning: Python is (probably) fake stub Python that comes bundled with newer versions of Windows, automatic Qt installation impossible." - echo "If you think you have Python installed, try changing the order of your PATH environment variable in Advanced System Settings." - MISSINGPYTHON=1 -fi - if [ $MISSINGTOOLS -ne 0 ]; then wrappedExit 1 fi @@ -54,7 +44,6 @@ function unixPathAsWindows { fi } -APPVEYOR=${APPVEYOR:-} CI=${CI:-} STEP=${STEP:-} @@ -62,6 +51,7 @@ VERBOSE="" STRIP="" SKIP_DOWNLOAD="" SKIP_EXTRACT="" +USE_CCACHE="" KEEP="" UNITY_BUILD="" VS_VERSION="" @@ -71,9 +61,10 @@ PDBS="" PLATFORM="" CONFIGURATIONS=() TEST_FRAMEWORK="" -GOOGLE_INSTALL_ROOT="" INSTALL_PREFIX="." BUILD_BENCHMARKS="" +USE_WERROR="" +USE_CLANG_TIDY="" ACTIVATE_MSVC="" SINGLE_CONFIG="" @@ -100,6 +91,9 @@ while [ $# -gt 0 ]; do e ) SKIP_EXTRACT=true ;; + C ) + USE_CCACHE=true ;; + k ) KEEP=true ;; @@ -112,7 +106,7 @@ while [ $# -gt 0 ]; do n ) NMAKE=true ;; - + N ) NINJA=true ;; @@ -137,6 +131,12 @@ while [ $# -gt 0 ]; do b ) BUILD_BENCHMARKS=true ;; + E ) + USE_WERROR=true ;; + + T ) + USE_CLANG_TIDY=true ;; + h ) cat < + -v <2019/2022> Choose the Visual Studio version to use. -n Produce NMake makefiles instead of a Visual Studio solution. Cannot be used with -N. @@ -173,6 +175,12 @@ Options: CMake install prefix -b Build benchmarks + -M + Use a multiview build of OSG + -E + Use warnings as errors (/WX) + -T + Run clang-tidy EOF wrappedExit 0 ;; @@ -197,16 +205,8 @@ if [ -z $VERBOSE ]; then STRIP="> /dev/null 2>&1" fi -if [ -z $APPVEYOR ]; then - echo "Running prebuild outside of Appveyor." - - DIR=$(windowsPathAsUnix "${BASH_SOURCE[0]}") - cd $(dirname "$DIR")/.. -else - echo "Running prebuild in Appveyor." - - cd "$APPVEYOR_BUILD_FOLDER" -fi +DIR=$(windowsPathAsUnix "${BASH_SOURCE[0]}") +cd $(dirname "$DIR")/.. run_cmd() { CMD="$1" @@ -217,13 +217,7 @@ run_cmd() { eval $CMD $@ > output.log 2>&1 || RET=$? if [ $RET -ne 0 ]; then - if [ -z $APPVEYOR ]; then - echo "Command $CMD failed, output can be found in $(real_pwd)/output.log" - else - echo - echo "Command $CMD failed;" - cat output.log - fi + echo "Command $CMD failed, output can be found in $(real_pwd)/output.log" else rm output.log fi @@ -258,10 +252,10 @@ download() { if [ -z $VERBOSE ]; then RET=0 - curl --silent --retry 10 -Ly 5 -o $FILE $URL || RET=$? + curl --silent --fail --retry 10 -Ly 5 -o $FILE $URL || RET=$? else RET=0 - curl --retry 10 -Ly 5 -o $FILE $URL || RET=$? + curl --fail --retry 10 -Ly 5 -o $FILE $URL || RET=$? fi if [ $RET -ne 0 ]; then @@ -333,39 +327,56 @@ add_qt_style_dlls() { QT_STYLES[$CONFIG]="${QT_STYLES[$CONFIG]} $@" } +declare -A QT_IMAGEFORMATS +QT_IMAGEFORMATS["Release"]="" +QT_IMAGEFORMATS["Debug"]="" +QT_IMAGEFORMATS["RelWithDebInfo"]="" +add_qt_image_dlls() { + local CONFIG=$1 + shift + QT_IMAGEFORMATS[$CONFIG]="${QT_IMAGEFORMATS[$CONFIG]} $@" +} + +declare -A QT_ICONENGINES +QT_ICONENGINES["Release"]="" +QT_ICONENGINES["Debug"]="" +QT_ICONENGINES["RelWithDebInfo"]="" +add_qt_icon_dlls() { + local CONFIG=$1 + shift + QT_ICONENGINES[$CONFIG]="${QT_ICONENGINES[$CONFIG]} $@" +} + if [ -z $PLATFORM ]; then PLATFORM="$(uname -m)" fi if [ -z $VS_VERSION ]; then - VS_VERSION="2017" + VS_VERSION="2019" fi case $VS_VERSION in + 17|17.0|2022 ) + GENERATOR="Visual Studio 17 2022" + MSVC_TOOLSET="vc143" + MSVC_REAL_VER="17" + MSVC_DISPLAY_YEAR="2022" + + QT_MSVC_YEAR="2019" + ;; + 16|16.0|2019 ) GENERATOR="Visual Studio 16 2019" - TOOLSET="vc142" + MSVC_TOOLSET="vc142" MSVC_REAL_VER="16" - MSVC_VER="14.2" - MSVC_YEAR="2015" - MSVC_REAL_YEAR="2019" MSVC_DISPLAY_YEAR="2019" - BOOST_VER="1.71.0" - BOOST_VER_URL="1_71_0" - BOOST_VER_SDK="107100" + + QT_MSVC_YEAR="2019" ;; 15|15.0|2017 ) - GENERATOR="Visual Studio 15 2017" - TOOLSET="vc141" - MSVC_REAL_VER="15" - MSVC_VER="14.1" - MSVC_YEAR="2015" - MSVC_REAL_YEAR="2017" - MSVC_DISPLAY_YEAR="2017" - BOOST_VER="1.67.0" - BOOST_VER_URL="1_67_0" - BOOST_VER_SDK="106700" + echo "Visual Studio 2017 is no longer supported" + wrappedExit 1 ;; 14|14.0|2015 ) @@ -398,10 +409,6 @@ case $PLATFORM in ;; esac -if [ $BITS -eq 64 ] && [ $MSVC_REAL_VER -lt 16 ]; then - GENERATOR="${GENERATOR} Win64" -fi - if [ -n "$NMAKE" ]; then GENERATOR="NMake Makefiles" SINGLE_CONFIG=true @@ -485,7 +492,7 @@ for i in ${!CONFIGURATIONS[@]}; do esac done -if [ $MSVC_REAL_VER -ge 16 ] && [ -z "$NMAKE" ] && [ -z "$NINJA" ]; then +if [ -z "$NMAKE" ] && [ -z "$NINJA" ]; then if [ $BITS -eq 64 ]; then add_cmake_opts "-G\"$GENERATOR\" -A x64" else @@ -499,17 +506,44 @@ if [ -n "$SINGLE_CONFIG" ]; then add_cmake_opts "-DCMAKE_BUILD_TYPE=${CONFIGURATIONS[0]}" fi -if ! [ -z $UNITY_BUILD ]; then +if [[ -n "$UNITY_BUILD" ]]; then add_cmake_opts "-DOPENMW_UNITY_BUILD=True" fi +if [ -n "$USE_CCACHE" ]; then + if [ -n "$NMAKE" ] || [ -n "$NINJA" ]; then + add_cmake_opts "-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DPRECOMPILE_HEADERS_WITH_MSVC=OFF" + else + echo "Ignoring -C (CCache) as it is incompatible with Visual Studio CMake generators" + fi +fi + +# turn on LTO by default +add_cmake_opts "-DOPENMW_LTO_BUILD=True" + +if [[ -n "$USE_WERROR" ]]; then + add_cmake_opts "-DOPENMW_MSVC_WERROR=ON" +fi + +if [[ -n "$USE_CLANG_TIDY" ]]; then + add_cmake_opts "-DCMAKE_CXX_CLANG_TIDY=\"clang-tidy --warnings-as-errors=*\"" +fi + +QT_VER='6.6.3' +AQT_VERSION='v3.1.15' + +VCPKG_REVISION='65ef3a6db0e01983efc7d8286f44020beeee2ea3' +VCPKG_PATH="vcpkg-x64-windows-${VS_VERSION:?}-${VCPKG_REVISION:?}" +VCPKG_ARCHIVE="${VCPKG_PATH:?}.7z" +VCPKG_PDB_PATH="vcpkg-x64-windows-${VS_VERSION:?}-pdb-${VCPKG_REVISION:?}" +VCPKG_PDB_ARCHIVE="${VCPKG_PDB_PATH:?}.7z" + echo echo "===================================" echo "Starting prebuild on MSVC${MSVC_DISPLAY_YEAR} WIN${BITS}" echo "===================================" echo -# cd OpenMW/AppVeyor-test mkdir -p deps cd deps @@ -519,70 +553,14 @@ if [ -z $SKIP_DOWNLOAD ]; then echo "Downloading dependency packages." echo - # Boost - if [ -z $APPVEYOR ]; then - download "Boost ${BOOST_VER}" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/boost_${BOOST_VER_URL}-msvc-${MSVC_VER}-${BITS}.exe" \ - "boost-${BOOST_VER}-msvc${MSVC_VER}-win${BITS}.exe" - fi + download "${VCPKG_PATH:?}" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/${VCPKG_ARCHIVE:?}" \ + "${VCPKG_ARCHIVE:?}" - # Bullet - download "Bullet 2.89" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/Bullet-2.89-msvc${MSVC_YEAR}-win${BITS}-double.7z" \ - "Bullet-2.89-msvc${MSVC_YEAR}-win${BITS}-double.7z" - - # FFmpeg - download "FFmpeg 4.2.2" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/ffmpeg-4.2.2-win${BITS}.zip" \ - "ffmpeg-4.2.2-win${BITS}.zip" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/ffmpeg-4.2.2-dev-win${BITS}.zip" \ - "ffmpeg-4.2.2-dev-win${BITS}.zip" - - # MyGUI - download "MyGUI 3.4.0" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" \ - "MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" - - if [ -n "$PDBS" ]; then - download "MyGUI symbols" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" \ - "MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" - fi - - # OpenAL - download "OpenAL-Soft 1.20.1" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OpenAL-Soft-1.20.1.zip" \ - "OpenAL-Soft-1.20.1.zip" - - # OSG - download "OpenSceneGraph 3.6.5" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" \ - "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" - - if [ -n "$PDBS" ]; then - download "OpenSceneGraph symbols" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" \ - "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" - fi - - # SDL2 - download "SDL 2.0.12" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/SDL2-2.0.12.zip" \ - "SDL2-2.0.12.zip" - - # LZ4 - download "LZ4 1.9.2" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/lz4_win${BITS}_v1_9_2.7z" \ - "lz4_win${BITS}_v1_9_2.7z" - - # Google test and mock - if [ ! -z $TEST_FRAMEWORK ]; then - echo "Google test 1.10.0..." - if [ -d googletest ]; then - printf " Google test exists, skipping." - else - git clone -b release-1.10.0 https://github.com/google/googletest.git - fi + if [ -n "${PDBS}" ]; then + download "${VCPKG_PDB_PATH:?}" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/${VCPKG_PDB_ARCHIVE:?}" \ + "${VCPKG_PDB_ARCHIVE:?}" fi fi @@ -619,374 +597,114 @@ echo "Extracting dependencies, this might take a while..." echo "---------------------------------------------------" echo - -# Boost -if [ -z $APPVEYOR ]; then - printf "Boost ${BOOST_VER}... " -else - printf "Boost ${BOOST_VER} AppVeyor... " -fi -{ - if [ -z $APPVEYOR ]; then - cd $DEPS_INSTALL - - BOOST_SDK="$(real_pwd)/Boost" - - # Boost's installer is still based on ms-dos API that doesn't support larger than 260 char path names - # We work around this by installing to root of the current working drive and then move it to our deps - # get the current working drive's root, we'll install to that temporarily - CWD_DRIVE_ROOT="$(powershell -command '(get-location).Drive.Root')Boost_temp" - CWD_DRIVE_ROOT_BASH=$(windowsPathAsUnix "$CWD_DRIVE_ROOT") - if [ -d CWD_DRIVE_ROOT_BASH ]; then - printf "Cannot continue, ${CWD_DRIVE_ROOT_BASH} aka ${CWD_DRIVE_ROOT} already exists. Please remove before re-running. "; - wrappedExit 1; - fi - - if [ -d ${BOOST_SDK} ] && grep "BOOST_VERSION ${BOOST_VER_SDK}" Boost/boost/version.hpp > /dev/null; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf Boost - CI_EXTRA_INNO_OPTIONS="" - [ -n "$CI" ] && CI_EXTRA_INNO_OPTIONS="//SUPPRESSMSGBOXES //LOG='boost_install.log'" - "${DEPS}/boost-${BOOST_VER}-msvc${MSVC_VER}-win${BITS}.exe" //DIR="${CWD_DRIVE_ROOT}" //VERYSILENT //NORESTART ${CI_EXTRA_INNO_OPTIONS} - mv "${CWD_DRIVE_ROOT_BASH}" "${BOOST_SDK}" - fi - add_cmake_opts -DBOOST_ROOT="$BOOST_SDK" \ - -DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}" - add_cmake_opts -DBoost_COMPILER="-${TOOLSET}" - echo Done. - else - # Appveyor has all the boost we need already - BOOST_SDK="c:/Libraries/boost_${BOOST_VER_URL}" - - add_cmake_opts -DBOOST_ROOT="$BOOST_SDK" \ - -DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}.1" - add_cmake_opts -DBoost_COMPILER="-${TOOLSET}" - - echo Done. - fi -} -cd $DEPS -echo -# Bullet -printf "Bullet 2.89... " -{ - cd $DEPS_INSTALL - if [ -d Bullet ]; then - printf -- "Exists. (No version checking) " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf Bullet - eval 7z x -y "${DEPS}/Bullet-2.89-msvc${MSVC_YEAR}-win${BITS}-double.7z" $STRIP - mv "Bullet-2.89-msvc${MSVC_YEAR}-win${BITS}-double" Bullet - fi - add_cmake_opts -DBULLET_ROOT="$(real_pwd)/Bullet" - echo Done. -} cd $DEPS echo -# FFmpeg -printf "FFmpeg 4.2.2... " +printf "vcpkg packages ${VCPKG_REVISION:?}... " { - cd $DEPS_INSTALL - if [ -d FFmpeg ] && grep "4.2.2" FFmpeg/README.txt > /dev/null; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf FFmpeg - eval 7z x -y "${DEPS}/ffmpeg-4.2.2-win${BITS}.zip" $STRIP - eval 7z x -y "${DEPS}/ffmpeg-4.2.2-dev-win${BITS}.zip" $STRIP - mv "ffmpeg-4.2.2-win${BITS}-shared" FFmpeg - cp -r "ffmpeg-4.2.2-win${BITS}-dev/"* FFmpeg/ - rm -rf "ffmpeg-4.2.2-win${BITS}-dev" - fi - export FFMPEG_HOME="$(real_pwd)/FFmpeg" - for config in ${CONFIGURATIONS[@]}; do - add_runtime_dlls $config "$(pwd)/FFmpeg/bin/"{avcodec-58,avformat-58,avutil-56,swresample-3,swscale-5}.dll - done - if [ $BITS -eq 32 ]; then - add_cmake_opts "-DCMAKE_EXE_LINKER_FLAGS=\"/machine:X86 /safeseh:no\"" - fi - echo Done. -} -cd $DEPS -echo -# MyGUI -printf "MyGUI 3.4.0... " -{ - cd $DEPS_INSTALL - if [ -d MyGUI ] && \ - grep "MYGUI_VERSION_MAJOR 3" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \ - grep "MYGUI_VERSION_MINOR 4" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \ - grep "MYGUI_VERSION_PATCH 0" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null - then + if [[ -d "${VCPKG_PATH:?}" ]]; then printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf MyGUI - eval 7z x -y "${DEPS}/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" $STRIP - [ -n "$PDBS" ] && eval 7z x -y "${DEPS}/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" $STRIP - mv "MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}" MyGUI - fi - export MYGUI_HOME="$(real_pwd)/MyGUI" - for CONFIGURATION in ${CONFIGURATIONS[@]}; do - if [ $CONFIGURATION == "Debug" ]; then - SUFFIX="_d" - MYGUI_CONFIGURATION="Debug" - else - SUFFIX="" - MYGUI_CONFIGURATION="RelWithDebInfo" + else + eval 7z x -y -o"${VCPKG_PATH:?}" "${VCPKG_ARCHIVE:?}" ${STRIP} + + if [ -n "${PDBS}" ]; then + eval 7z x -y -o"${VCPKG_PATH:?}" "${VCPKG_PDB_ARCHIVE:?}" ${STRIP} fi - add_runtime_dlls $CONFIGURATION "$(pwd)/MyGUI/bin/${MYGUI_CONFIGURATION}/MyGUIEngine${SUFFIX}.dll" - done - echo Done. -} -cd $DEPS -echo -# OpenAL -printf "OpenAL-Soft 1.20.1... " -{ - if [ -d openal-soft-1.20.1-bin ]; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf openal-soft-1.20.1-bin - eval 7z x -y OpenAL-Soft-1.20.1.zip $STRIP fi - OPENAL_SDK="$(real_pwd)/openal-soft-1.20.1-bin" - add_cmake_opts -DOPENAL_INCLUDE_DIR="${OPENAL_SDK}/include/AL" \ - -DOPENAL_LIBRARY="${OPENAL_SDK}/libs/Win${BITS}/OpenAL32.lib" - for config in ${CONFIGURATIONS[@]}; do - add_runtime_dlls $config "$(pwd)/openal-soft-1.20.1-bin/bin/WIN${BITS}/soft_oal.dll:OpenAL32.dll" - done - echo Done. -} -cd $DEPS -echo -# OSG -printf "OSG 3.6.5... " -{ - cd $DEPS_INSTALL - if [ -d OSG ] && \ - grep "OPENSCENEGRAPH_MAJOR_VERSION 3" OSG/include/osg/Version > /dev/null && \ - grep "OPENSCENEGRAPH_MINOR_VERSION 6" OSG/include/osg/Version > /dev/null && \ - grep "OPENSCENEGRAPH_PATCH_VERSION 5" OSG/include/osg/Version > /dev/null - then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf OSG - eval 7z x -y "${DEPS}/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" $STRIP - [ -n "$PDBS" ] && eval 7z x -y "${DEPS}/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" $STRIP - mv "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}" OSG - fi - OSG_SDK="$(real_pwd)/OSG" - add_cmake_opts -DOSG_DIR="$OSG_SDK" + + 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" + for CONFIGURATION in ${CONFIGURATIONS[@]}; do - if [ $CONFIGURATION == "Debug" ]; then - SUFFIX="d" + if [[ ${CONFIGURATION:?} == "Debug" ]]; then + VCPKG_DLL_BIN="$(pwd)/${VCPKG_PATH:?}/installed/x64-windows/debug/bin" + + add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/Debug/MyGUIEngine_d.dll" else - SUFFIX="" + VCPKG_DLL_BIN="$(pwd)/${VCPKG_PATH:?}/installed/x64-windows/bin" + + add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/Release/MyGUIEngine.dll" fi - add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{OpenThreads,zlib,libpng}${SUFFIX}.dll \ - "$(pwd)/OSG/bin/osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow}${SUFFIX}.dll - add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_"{bmp,dds,freetype,jpeg,osg,png,tga}${SUFFIX}.dll - add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_serializers_osg"{,animation,fx,ga,particle,text,util,viewer,shadow}${SUFFIX}.dll + + add_osg_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/osgPlugins-3.6.5/*.dll" + add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/*.dll" done + echo Done. } + cd $DEPS echo -# Qt -if [ -z $APPVEYOR ]; then - printf "Qt 5.15.0... " -else - printf "Qt 5.13 AppVeyor... " -fi +printf "Qt ${QT_VER}... " { if [ $BITS -eq 64 ]; then SUFFIX="_64" else SUFFIX="" fi - if [ -z $APPVEYOR ]; then - cd $DEPS_INSTALL - - qt_version="5.15.0" - if [ "win${BITS}_msvc${MSVC_REAL_YEAR}${SUFFIX}" == "win64_msvc2017_64" ]; then - echo "This combination of options is known not to work. Falling back to Qt 5.14.2." - qt_version="5.14.2" - fi - - QT_SDK="$(real_pwd)/Qt/${qt_version}/msvc${MSVC_REAL_YEAR}${SUFFIX}" - if [ -d "Qt/${qt_version}" ]; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - if [ $MISSINGPYTHON -ne 0 ]; then - echo "Can't be automatically installed without Python." - wrappedExit 1 - fi + cd $DEPS_INSTALL - pushd "$DEPS" > /dev/null - if ! [ -d 'aqt-venv' ]; then - echo " Creating Virtualenv for aqt..." - run_cmd python -m venv aqt-venv - fi - if [ -d 'aqt-venv/bin' ]; then - VENV_BIN_DIR='bin' - elif [ -d 'aqt-venv/Scripts' ]; then - VENV_BIN_DIR='Scripts' - else - echo "Error: Failed to create virtualenv in expected location." - wrappedExit 1 - fi - # check version - aqt-venv/${VENV_BIN_DIR}/pip list | grep 'aqtinstall\s*1.1.3' || [ $? -ne 0 ] - if [ $? -eq 0 ]; then - echo " Installing aqt wheel into virtualenv..." - run_cmd "aqt-venv/${VENV_BIN_DIR}/pip" install aqtinstall==1.1.3 - fi - popd > /dev/null + QT_SDK="$(real_pwd)/Qt/${QT_VER}/msvc${QT_MSVC_YEAR}${SUFFIX}" - rm -rf Qt + if [ -d "Qt/${QT_VER}" ]; then + printf "Exists. " + elif [ -z $SKIP_EXTRACT ]; then + pushd "$DEPS" > /dev/null + if ! [ -f "aqt_x64-${AQT_VERSION}.exe" ]; then + download "aqt ${AQT_VERSION}"\ + "https://github.com/miurahr/aqtinstall/releases/download/${AQT_VERSION}/aqt_x64.exe" \ + "aqt_x64-${AQT_VERSION}.exe" + fi + popd > /dev/null - mkdir Qt - cd Qt + rm -rf Qt - run_cmd "${DEPS}/aqt-venv/${VENV_BIN_DIR}/aqt" install $qt_version windows desktop "win${BITS}_msvc${MSVC_REAL_YEAR}${SUFFIX}" + mkdir Qt + cd Qt - printf " Cleaning up extraneous data... " - rm -rf Qt/{aqtinstall.log,Tools} + run_cmd "${DEPS}/aqt_x64-${AQT_VERSION}.exe" install-qt windows desktop ${QT_VER} "win${BITS}_msvc${QT_MSVC_YEAR}${SUFFIX}" - echo Done. - fi + printf " Cleaning up extraneous data... " + rm -rf Qt/{aqtinstall.log,Tools} - cd $QT_SDK - add_cmake_opts -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" \ - -DCMAKE_PREFIX_PATH="$QT_SDK" - for CONFIGURATION in ${CONFIGURATIONS[@]}; do - if [ $CONFIGURATION == "Debug" ]; then - DLLSUFFIX="d" - else - DLLSUFFIX="" - fi - add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${DLLSUFFIX}.dll - add_qt_platform_dlls $CONFIGURATION "$(pwd)/plugins/platforms/qwindows${DLLSUFFIX}.dll" - add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" - done - echo Done. - else - QT_SDK="C:/Qt/5.13/msvc2017${SUFFIX}" - add_cmake_opts -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" \ - -DCMAKE_PREFIX_PATH="$QT_SDK" - for CONFIGURATION in ${CONFIGURATIONS[@]}; do - if [ $CONFIGURATION == "Debug" ]; then - DLLSUFFIX="d" - else - DLLSUFFIX="" - fi - DIR=$(windowsPathAsUnix "${QT_SDK}") - add_runtime_dlls $CONFIGURATION "${DIR}/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${DLLSUFFIX}.dll - add_qt_platform_dlls $CONFIGURATION "${DIR}/plugins/platforms/qwindows${DLLSUFFIX}.dll" - add_qt_style_dlls $CONFIGURATION "${DIR}/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" - done echo Done. fi -} -cd $DEPS -echo -# SDL2 -printf "SDL 2.0.12... " -{ - if [ -d SDL2-2.0.12 ]; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf SDL2-2.0.12 - eval 7z x -y SDL2-2.0.12.zip $STRIP - fi - export SDL2DIR="$(real_pwd)/SDL2-2.0.12" - for config in ${CONFIGURATIONS[@]}; do - add_runtime_dlls $config "$(pwd)/SDL2-2.0.12/lib/x${ARCHSUFFIX}/SDL2.dll" - done - echo Done. -} -cd $DEPS -echo -# LZ4 -printf "LZ4 1.9.2... " -{ - if [ -d LZ4_1.9.2 ]; then - printf "Exists. " - elif [ -z $SKIP_EXTRACT ]; then - rm -rf LZ4_1.9.2 - eval 7z x -y lz4_win${BITS}_v1_9_2.7z -o$(real_pwd)/LZ4_1.9.2 $STRIP - fi - export LZ4DIR="$(real_pwd)/LZ4_1.9.2" - add_cmake_opts -DLZ4_INCLUDE_DIR="${LZ4DIR}/include" \ - -DLZ4_LIBRARY="${LZ4DIR}/lib/liblz4.lib" + + QT_MAJOR_VER=$(echo "${QT_VER}" | awk -F '[.]' '{printf "%d", $1}') + QT_MINOR_VER=$(echo "${QT_VER}" | awk -F '[.]' '{printf "%d", $2}') + + cd $QT_SDK for CONFIGURATION in ${CONFIGURATIONS[@]}; do if [ $CONFIGURATION == "Debug" ]; then - LZ4_CONFIGURATION="Debug" + DLLSUFFIX="d" else - SUFFIX="" - LZ4_CONFIGURATION="Release" + DLLSUFFIX="" fi - add_runtime_dlls $CONFIGURATION "$(pwd)/LZ4_1.9.2/bin/${LZ4_CONFIGURATION}/liblz4.dll" - done - echo Done. -} -cd $DEPS -echo -# Google Test and Google Mock -if [ ! -z $TEST_FRAMEWORK ]; then - printf "Google test 1.10.0 ..." - - cd googletest - mkdir -p build${MSVC_REAL_YEAR} - cd build${MSVC_REAL_YEAR} + if [ "${QT_MAJOR_VER}" -eq 6 ]; then + add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,OpenGLWidgets,Widgets,Svg}${DLLSUFFIX}.dll - GOOGLE_INSTALL_ROOT="${DEPS_INSTALL}/GoogleTest" - - for CONFIGURATION in ${CONFIGURATIONS[@]}; do - # FindGMock.cmake mentions Release explicitly, but not RelWithDebInfo. Only one optimised library config can be used, so go for the safer one. - GTEST_CONFIG=$([ $CONFIGURATION == "RelWithDebInfo" ] && echo "Release" || echo "$CONFIGURATION" ) - if [ $GTEST_CONFIG == "Debug" ]; then - DEBUG_SUFFIX="d" + # Since Qt 6.7.0 plugin is called "qmodernwindowsstyle" + if [ "${QT_MINOR_VER}" -ge 7 ]; then + add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qmodernwindowsstyle${DLLSUFFIX}.dll" + else + add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" + fi else - DEBUG_SUFFIX="" - fi - - if [ ! -f "$GOOGLE_INSTALL_ROOT/lib/gtest${DEBUG_SUFFIX}.lib" ]; then - # Always use MSBuild solution files as they don't need the environment activating - cmake .. -DCMAKE_USE_WIN32_THREADS_INIT=1 -G "Visual Studio $MSVC_REAL_VER $MSVC_REAL_YEAR$([ $BITS -eq 64 ] && [ $MSVC_REAL_VER -lt 16 ] && echo " Win64")" $([ $MSVC_REAL_VER -ge 16 ] && echo "-A $([ $BITS -eq 64 ] && echo "x64" || echo "Win32")") -DBUILD_SHARED_LIBS=1 - cmake --build . --config "${GTEST_CONFIG}" - cmake --install . --config "${GTEST_CONFIG}" --prefix "${GOOGLE_INSTALL_ROOT}" + add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,Widgets,Svg}${DLLSUFFIX}.dll + add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" fi - add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gtest_main${DEBUG_SUFFIX}.dll" - add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gtest${DEBUG_SUFFIX}.dll" - add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gmock_main${DEBUG_SUFFIX}.dll" - add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gmock${DEBUG_SUFFIX}.dll" + add_qt_platform_dlls $CONFIGURATION "$(pwd)/plugins/platforms/qwindows${DLLSUFFIX}.dll" + add_qt_image_dlls $CONFIGURATION "$(pwd)/plugins/imageformats/qsvg${DLLSUFFIX}.dll" + add_qt_icon_dlls $CONFIGURATION "$(pwd)/plugins/iconengines/qsvgicon${DLLSUFFIX}.dll" done - - add_cmake_opts -DBUILD_UNITTESTS=yes - # FindGTest and FindGMock do not work perfectly on Windows - # but we can help them by telling them everything we know about installation - add_cmake_opts -DGMOCK_ROOT="$GOOGLE_INSTALL_ROOT" - add_cmake_opts -DGTEST_ROOT="$GOOGLE_INSTALL_ROOT" - add_cmake_opts -DGTEST_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gtest.lib" - add_cmake_opts -DGTEST_MAIN_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gtest_main.lib" - add_cmake_opts -DGMOCK_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gmock.lib" - add_cmake_opts -DGMOCK_MAIN_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gmock_main.lib" - add_cmake_opts -DGTEST_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gtestd.lib" - add_cmake_opts -DGTEST_MAIN_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gtest_maind.lib" - add_cmake_opts -DGMOCK_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gmockd.lib" - add_cmake_opts -DGMOCK_MAIN_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gmock_maind.lib" - add_cmake_opts -DGTEST_LINKED_AS_SHARED_LIBRARY=True - add_cmake_opts -DGTEST_LIBRARY_TYPE=SHARED - add_cmake_opts -DGTEST_MAIN_LIBRARY_TYPE=SHARED - echo Done. +} -fi +add_cmake_opts -DCMAKE_PREFIX_PATH="\"${QT_SDK}\"" echo cd $DEPS_INSTALL/.. @@ -994,6 +712,8 @@ echo echo "Setting up OpenMW build..." add_cmake_opts -DOPENMW_MP_BUILD=on add_cmake_opts -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" +add_cmake_opts -DOPENMW_USE_SYSTEM_SQLITE3=OFF +add_cmake_opts -DOPENMW_USE_SYSTEM_YAML_CPP=OFF if [ ! -z $CI ]; then case $STEP in components ) @@ -1070,6 +790,20 @@ fi cp "$DLL" "${DLL_PREFIX}styles" done echo + echo "- Qt Image Format DLLs..." + mkdir -p ${DLL_PREFIX}imageformats + for DLL in ${QT_IMAGEFORMATS[$CONFIGURATION]}; do + echo " $(basename $DLL)" + cp "$DLL" "${DLL_PREFIX}imageformats" + done + echo + echo "- Qt Icon Engine DLLs..." + mkdir -p ${DLL_PREFIX}iconengines + for DLL in ${QT_ICONENGINES[$CONFIGURATION]}; do + echo " $(basename $DLL)" + cp "$DLL" "${DLL_PREFIX}iconengines" + done + echo done #fi @@ -1077,6 +811,12 @@ if [ "${BUILD_BENCHMARKS}" ]; then add_cmake_opts -DBUILD_BENCHMARKS=ON fi +if [ -n "${TEST_FRAMEWORK}" ]; then + add_cmake_opts -DBUILD_COMPONENTS_TESTS=ON + add_cmake_opts -DBUILD_OPENCS_TESTS=ON + add_cmake_opts -DBUILD_OPENMW_TESTS=ON +fi + if [ -n "$ACTIVATE_MSVC" ]; then echo -n "- Activating MSVC in the current shell... " command -v vswhere >/dev/null 2>&1 || { echo "Error: vswhere is not on the path."; wrappedExit 1; } diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index 265e05b8ee0..d3e3698ab2a 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -1,29 +1,52 @@ #!/bin/sh -e -export CXX=clang++ -export CC=clang +# Silence a git warning +git config --global advice.detachedHead false -DEPENDENCIES_ROOT="/private/tmp/openmw-deps/openmw-deps" -QT_PATH=$(brew --prefix qt@5) -CCACHE_EXECUTABLE=$(brew --prefix ccache)/bin/ccache 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 \ --D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH" \ --D CMAKE_C_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" \ --D CMAKE_CXX_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" \ --D CMAKE_CXX_FLAGS="-stdlib=libc++" \ --D CMAKE_C_FLAGS_RELEASE="-g -O0" \ --D CMAKE_CXX_FLAGS_RELEASE="-g -O0" \ --D CMAKE_OSX_DEPLOYMENT_TARGET="10.14" \ --D CMAKE_BUILD_TYPE=RELEASE \ --D OPENMW_OSX_DEPLOYMENT=TRUE \ +"${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/build.msvc.sh b/CI/build.msvc.sh deleted file mode 100644 index eac969b0d40..00000000000 --- a/CI/build.msvc.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/bin/bash - -APPVEYOR="" -CI="" - -PACKAGE="" -PLATFORM="" -CONFIGURATION="" -VS_VERSION="" - -if [ -z $PLATFORM ]; then - PLATFORM=`uname -m` -fi - -if [ -z $CONFIGURATION ]; then - CONFIGURATION="Debug" -fi - -case $VS_VERSION in - 14|14.0|2015 ) - GENERATOR="Visual Studio 14 2015" - MSVC_YEAR="2015" - MSVC_VER="14.0" - ;; - -# 12|2013| - * ) - GENERATOR="Visual Studio 12 2013" - MSVC_YEAR="2013" - MVSC_VER="12.0" - ;; -esac - -case $PLATFORM in - x64|x86_64|x86-64|win64|Win64 ) - BITS=64 - ;; - - x32|x86|i686|i386|win32|Win32 ) - BITS=32 - ;; -esac - -case $CONFIGURATION in - debug|Debug|DEBUG ) - CONFIGURATION=Debug - ;; - - release|Release|RELEASE ) - CONFIGURATION=Release - ;; - - relwithdebinfo|RelWithDebInfo|RELWITHDEBINFO ) - CONFIGURATION=RelWithDebInfo - ;; -esac - -if [ -z $APPVEYOR ]; then - echo "Running ${BITS}-bit MSVC${MSVC_YEAR} ${CONFIGURATION} build outside of Appveyor." - - DIR=$(echo "$0" | sed "s,\\\\,/,g" | sed "s,\(.\):,/\\1,") - cd $(dirname "$DIR")/.. -else - echo "Running ${BITS}-bit MSVC${MSVC_YEAR} ${CONFIGURATION} build in Appveyor." - - cd $APPVEYOR_BUILD_FOLDER -fi - -BUILD_DIR="MSVC${MSVC_YEAR}_${BITS}" -cd ${BUILD_DIR} - -which msbuild > /dev/null -if [ $? -ne 0 ]; then - msbuild() { - /c/Program\ Files\ \(x86\)/MSBuild/${MSVC_VER}/Bin/MSBuild.exe "$@" - } -fi - -if [ -z $APPVEYOR ]; then - msbuild OpenMW.sln //t:Build //p:Configuration=${CONFIGURATION} //m:8 -else - msbuild OpenMW.sln //t:Build //p:Configuration=${CONFIGURATION} //m:8 //logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" -fi - -RET=$? -if [ $RET -eq 0 ] && [ ! -z $PACKAGE ]; then - msbuild PACKAGE.vcxproj //t:Build //m:8 - RET=$? -fi - -exit $RET diff --git a/CI/build_googletest.sh b/CI/build_googletest.sh deleted file mode 100755 index a9a50fee7a6..00000000000 --- a/CI/build_googletest.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -ex - -git clone -b release-1.10.0 https://github.com/google/googletest.git -cd googletest -mkdir build -cd build -cmake \ - -D CMAKE_C_COMPILER="${CC}" \ - -D CMAKE_CXX_COMPILER="${CXX}" \ - -D CMAKE_C_COMPILER_LAUNCHER=ccache \ - -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -D CMAKE_BUILD_TYPE="${CONFIGURATION}" \ - -D CMAKE_INSTALL_PREFIX="${GOOGLETEST_DIR}" \ - -G "${GENERATOR}" \ - .. -cmake --build . --config "${CONFIGURATION}" -- -j $(nproc) -cmake --install . --config "${CONFIGURATION}" diff --git a/CI/check_clang_format.sh b/CI/check_clang_format.sh new file mode 100755 index 00000000000..53d4ca4a64b --- /dev/null +++ b/CI/check_clang_format.sh @@ -0,0 +1,8 @@ +#!/bin/bash -ex + +set -o pipefail + +CLANG_FORMAT="${CLANG_FORMAT:-clang-format}" +git ls-files -- ':(exclude)extern/' '*.cpp' '*.hpp' '*.h' | + xargs -I '{}' -P $(nproc) bash -ec "\"${CLANG_FORMAT:?}\" --dry-run -Werror \"\${0:?}\" &> /dev/null || \"${CLANG_FORMAT:?}\" \"\${0:?}\" | git diff --color=always --no-index \"\${0:?}\" -" '{}' || + ( echo "clang-format differences detected"; exit -1 ) diff --git a/CI/check_cmake_format.sh b/CI/check_cmake_format.sh new file mode 100755 index 00000000000..40cd0b77f62 --- /dev/null +++ b/CI/check_cmake_format.sh @@ -0,0 +1,6 @@ +#!/bin/bash -ex + +git ls-files -- ':(exclude)extern/' 'CMakeLists.txt' '*.cmake' | + xargs grep -P '^\s*\t' && + ( echo 'CMake files contain leading tab character. Use only spaces for indentation'; exit -1 ) +exit 0 diff --git a/CI/check_file_names.sh b/CI/check_file_names.sh new file mode 100755 index 00000000000..b04416659ea --- /dev/null +++ b/CI/check_file_names.sh @@ -0,0 +1,7 @@ +#!/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 && + ( 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/check_qt_translations.sh b/CI/check_qt_translations.sh new file mode 100755 index 00000000000..1fc2e190025 --- /dev/null +++ b/CI/check_qt_translations.sh @@ -0,0 +1,11 @@ +#!/bin/bash -ex + +set -o pipefail + +LUPDATE="${LUPDATE:-lupdate}" + +${LUPDATE:?} -locations none apps/wizard -ts files/lang/wizard_*.ts +${LUPDATE:?} -locations none apps/launcher -ts files/lang/launcher_*.ts +${LUPDATE:?} -locations none components/contentselector components/process -ts files/lang/components_*.ts + +! (git diff --name-only | grep -q "^") || (echo -e "\033[0;31mBuild a 'translations' CMake target to update Qt localization for these files:\033[0;0m"; git diff --name-only | xargs -i echo -e "\033[0;31m{}\033[0;0m"; exit -1) diff --git a/CI/check_tabs.sh b/CI/check_tabs.sh deleted file mode 100755 index 1e88b57fd48..00000000000 --- a/CI/check_tabs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -OUTPUT=$(grep -nRP '\t' --include=\*.{cpp,hpp,c,h} --exclude=ui_\* apps components) - -if [[ $OUTPUT ]] ; then - echo "Error: Tab characters found!" - echo $OUTPUT - exit 1 -fi diff --git a/CI/deploy.osx.sh b/CI/deploy.osx.sh deleted file mode 100755 index 88804d8587a..00000000000 --- a/CI/deploy.osx.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -# This script expect the following environment variables to be set: -# - OSX_DEPLOY_KEY: private SSH key, must be encoded like this before adding it to Travis secrets: https://github.com/travis-ci/travis-ci/issues/7715#issuecomment-433301692 -# - OSX_DEPLOY_HOST: string specifying SSH of the following format: ssh-user@ssh-host -# - OSX_DEPLOY_PORT: SSH port, it can't be a part of the host string because scp doesn't accept hosts with ports -# - OSX_DEPLOY_HOST_FINGERPRINT: fingerprint of the host, can be obtained by using ssh-keygen -F [host]:port & putting it in double quotes when adding to Travis secrets - -SSH_KEY_PATH="$HOME/.ssh/openmw_deploy" -REMOTE_PATH="\$HOME/nightly" - -echo "$OSX_DEPLOY_KEY" > "$SSH_KEY_PATH" -chmod 600 "$SSH_KEY_PATH" -echo "$OSX_DEPLOY_HOST_FINGERPRINT" >> "$HOME/.ssh/known_hosts" - -cd build || exit 1 - -DATE=$(date +'%d%m%Y') -SHORT_COMMIT=$(git rev-parse --short "${TRAVIS_COMMIT}") -TARGET_FILENAME="OpenMW-${DATE}-${SHORT_COMMIT}.dmg" - -if ! ssh -p "$OSX_DEPLOY_PORT" -i "$SSH_KEY_PATH" "$OSX_DEPLOY_HOST" "ls \"$REMOTE_PATH\"" | grep "$SHORT_COMMIT" > /dev/null; then - scp -P "$OSX_DEPLOY_PORT" -i "$SSH_KEY_PATH" ./*.dmg "$OSX_DEPLOY_HOST:$REMOTE_PATH/$TARGET_FILENAME" -else - echo "An existing nightly build for commit ${SHORT_COMMIT} has been found, skipping upload." -fi diff --git a/CI/file_name_exceptions.txt b/CI/file_name_exceptions.txt new file mode 100644 index 00000000000..14d106169b1 --- /dev/null +++ b/CI/file_name_exceptions.txt @@ -0,0 +1,48 @@ +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/install_debian_deps.sh b/CI/install_debian_deps.sh index 2f905314b5f..d29f16f55fc 100755 --- a/CI/install_debian_deps.sh +++ b/CI/install_debian_deps.sh @@ -9,26 +9,39 @@ print_help() { } declare -rA GROUPED_DEPS=( - [gcc]="binutils gcc g++ libc-dev" - [clang]="binutils clang" + [gcc]="binutils gcc build-essential cmake ccache curl unzip git pkg-config mold" + [clang]="binutils clang make cmake ccache curl unzip git pkg-config mold" + [coverity]="binutils clang-12 make cmake ccache curl unzip git pkg-config" + [gcc_preprocess]=" + binutils + build-essential + clang + cmake + curl + gcc + git + libclang-dev + ninja-build + python3-clang + python3-pip + unzip + " # Common dependencies for building OpenMW. [openmw-deps]=" - make cmake ccache git pkg-config - - libboost-filesystem-dev libboost-program-options-dev + libboost-program-options-dev libboost-system-dev libboost-iostreams-dev - + libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev - libsdl2-dev libqt5opengl5-dev libopenal-dev libunshield-dev libtinyxml-dev - libbullet-dev liblz4-dev libpng-dev libjpeg-dev - ca-certificates + libsdl2-dev libqt5opengl5-dev qttools5-dev qttools5-dev-tools libopenal-dev + libunshield-dev libtinyxml-dev libbullet-dev liblz4-dev libpng-dev libjpeg-dev + libluajit-5.1-dev librecast-dev libsqlite3-dev ca-certificates libicu-dev + libyaml-cpp-dev libqt5svg5 libqt5svg5-dev " - # TODO: add librecastnavigation-dev when debian is ready # These dependencies can alternatively be built and linked statically. - [openmw-deps-dynamic]="libmygui-dev libopenscenegraph-dev" - [coverity]="curl" + [openmw-deps-dynamic]="libmygui-dev libopenscenegraph-dev libsqlite3-dev libcollada-dom-dev" + [clang-tidy]="clang-tidy" # Pre-requisites for building MyGUI and OSG for static linking. # @@ -40,10 +53,59 @@ declare -rA GROUPED_DEPS=( # * JPEG: libjpeg-dev # * PNG: libpng-dev [openmw-deps-static]=" - make cmake - ccache curl unzip libcollada-dom-dev libfreetype6-dev libjpeg-dev libpng-dev + libcollada-dom-dev libfreetype6-dev libjpeg-dev libpng-dev libsdl2-dev libboost-system-dev libboost-filesystem-dev libgl-dev " + + [openmw-coverage]="gcovr" + + [openmw-integration-tests]=" + ca-certificates + gdb + git + git-lfs + libavcodec58 + libavformat58 + libavutil56 + libboost-iostreams1.74.0 + libboost-program-options1.74.0 + libboost-system1.74.0 + libbullet3.24 + libcollada-dom2.5-dp0 + libicu70 + libjpeg8 + libluajit-5.1-2 + liblz4-1 + libmyguiengine3debian1v5 + libopenal1 + libopenscenegraph161 + libpng16-16 + libqt5opengl5 + librecast1 + libsdl2-2.0-0 + libsqlite3-0 + libswresample3 + libswscale5 + libtinyxml2.6.2v5 + libyaml-cpp0.8 + python3-pip + xvfb + " + + [libasan6]="libasan6" + + [android]="binutils build-essential cmake ccache curl unzip git pkg-config" + + [openmw-clang-format]=" + clang-format-14 + git-core + " + + [openmw-qt-translations]=" + qttools5-dev + qttools5-dev-tools + git-core + " ) if [[ $# -eq 0 ]]; then @@ -61,7 +123,13 @@ for group in "$@"; do done export APT_CACHE_DIR="${PWD}/apt-cache" +export DEBIAN_FRONTEND=noninteractive set -x mkdir -pv "$APT_CACHE_DIR" -apt-get update -yq -apt-get -q -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}" +apt-get update -yqq +apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends software-properties-common gnupg >/dev/null +add-apt-repository -y ppa:openmw/openmw +add-apt-repository -y ppa:openmw/openmw-daily +add-apt-repository -y ppa:openmw/staging +apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}" >/dev/null +apt list --installed diff --git a/CI/org.openmw.OpenMW.devel.yaml b/CI/org.openmw.OpenMW.devel.yaml new file mode 100644 index 00000000000..9f4d921cf14 --- /dev/null +++ b/CI/org.openmw.OpenMW.devel.yaml @@ -0,0 +1,200 @@ +--- +app-id: org.openmw.OpenMW.devel +runtime: org.kde.Platform +runtime-version: '5.15-21.08' +sdk: org.kde.Sdk +command: openmw-launcher +rename-appdata-file: openmw.appdata.xml +finish-args: + - "--share=ipc" + - "--socket=x11" + - "--device=all" + - "--filesystem=host" + - "--socket=pulseaudio" +build-options: + cflags: "-O2 -g" + cxxflags: "-O2 -g" +cleanup: + - "/include" + - "/lib/pkgconfig" + - "/lib/cmake" + - "/share/pkgconfig" + - "/share/aclocal" + - "/share/doc" + - "/man" + - "/share/man" + - "/share/gtk-doc" + - "/share/vala" + - "*.la" + - "*.a" + +modules: + - name: boost + buildsystem: simple + build-commands: + - ./bootstrap.sh --prefix=/app --with-libraries=filesystem,iostreams,program_options,system + - ./b2 headers + - ./b2 install + sources: + - type: archive + url: https://boostorg.jfrog.io/artifactory/main/release/1.75.0/source/boost_1_75_0.tar.gz + sha256: aeb26f80e80945e82ee93e5939baebdca47b9dee80a07d3144be1e1a6a66dd6a + + - name: collada-dom + buildsystem: cmake-ninja + config-opts: + - "-DOPT_COLLADA14=1" + - "-DOPT_COLLADA15=0" + sources: + - type: archive + url: https://github.com/rdiankov/collada-dom/archive/c1e20b7d6ff806237030fe82f126cb86d661f063.zip + sha256: 6c51cd068c7d6760b587391884942caaac8a515d138535041e42d00d3e5c9152 + + - name: ffmpeg + config-opts: + - "--disable-static" + - "--enable-shared" + - "--disable-programs" + - "--disable-doc" + - "--disable-avdevice" + - "--disable-avfilter" + - "--disable-postproc" + + - "--disable-encoders" + - "--disable-muxers" + - "--disable-protocols" + - "--disable-indevs" + - "--disable-devices" + - "--disable-filters" + sources: + - type: archive + url: http://ffmpeg.org/releases/ffmpeg-4.3.2.tar.xz + sha256: 46e4e64f1dd0233cbc0934b9f1c0da676008cad34725113fb7f802cfa84ccddb + cleanup: + - "/share/ffmpeg" + + - name: openscenegraph + buildsystem: cmake-ninja + config-opts: + - "-DBUILD_OSG_PLUGINS_BY_DEFAULT=0" + - "-DBUILD_OSG_PLUGIN_OSG=1" + - "-DBUILD_OSG_PLUGIN_DDS=1" + - "-DBUILD_OSG_PLUGIN_DAE=1" + - "-DBUILD_OSG_PLUGIN_TGA=1" + - "-DBUILD_OSG_PLUGIN_BMP=1" + - "-DBUILD_OSG_PLUGIN_JPEG=1" + - "-DBUILD_OSG_PLUGIN_PNG=1" + - "-DBUILD_OSG_DEPRECATED_SERIALIZERS=0" + - "-DBUILD_OSG_APPLICATIONS=0" + - "-DCMAKE_BUILD_TYPE=Release" + build-options: + env: + COLLADA_DIR: /app/include/collada-dom2.5 + sources: + - type: archive + url: https://github.com/openmw/osg/archive/76e061739610bc9a3420a59e7c9395e742ce2f97.zip + sha256: fa1100362eae260192819d65d90b29ec0b88fdf80e30cee677730b7a0d68637e + + - name: bullet + # The cmake + ninja buildsystem doesn't install the required binaries correctly + buildsystem: cmake + config-opts: + - "-DBUILD_BULLET2_DEMOS=0" + - "-DBUILD_BULLET3=0" + - "-DBUILD_CPU_DEMOS=0" + - "-DBUILD_EXTRAS=0" + - "-DBUILD_OPENGL3_DEMOS=0" + - "-DBUILD_UNIT_TESTS=0" + - "-DCMAKE_BUILD_TYPE=Release" + - "-DUSE_GLUT=0" + - "-DUSE_GRAPHICAL_BENCHMARK=0" + - "-DUSE_DOUBLE_PRECISION=on" + - "-DBULLET2_MULTITHREADING=on" + sources: + - type: archive + url: https://github.com/bulletphysics/bullet3/archive/93be7e644024e92df13b454a4a0b0fcd02b21b10.zip + sha256: 82968fbf20a92c51bc71ac9ee8f6381ecf3420c7cbb881ffb7bb633fa13b27f9 + + - name: mygui + buildsystem: cmake-ninja + config-opts: + - "-DCMAKE_BUILD_TYPE=Release" + - "-DMYGUI_RENDERSYSTEM=1" + - "-DMYGUI_BUILD_DEMOS=0" + - "-DMYGUI_BUILD_TOOLS=0" + - "-DMYGUI_BUILD_PLUGINS=0" + sources: + - type: archive + url: https://github.com/MyGUI/mygui/archive/refs/tags/MyGUI3.4.3.tar.gz + sha256: 33c91b531993047e77cace36d6fea73634b8c17bd0ed193d4cd12ac7c6328abd + + - name: libunshield + buildsystem: cmake-ninja + config-opts: + - "-DCMAKE_BUILD_TYPE=Release" + sources: + - type: archive + url: https://github.com/twogood/unshield/archive/1.4.3.tar.gz + sha256: aa8c978dc0eb1158d266eaddcd1852d6d71620ddfc82807fe4bf2e19022b7bab + + - name: lz4 + buildsystem: simple + build-commands: + - "make lib" + - "PREFIX=/app make install" + sources: + - type: archive + url: https://github.com/lz4/lz4/archive/refs/tags/v1.9.3.tar.gz + sha256: 030644df4611007ff7dc962d981f390361e6c97a34e5cbc393ddfbe019ffe2c1 + + - name: recastnavigation + buildsystem: cmake-ninja + config-opts: + - "-DCMAKE_BUILD_TYPE=Release" + - "-DRECASTNAVIGATION_DEMO=no" + - "-DRECASTNAVIGATION_TESTS=no" + - "-DRECASTNAVIGATION_EXAMPLES=no" + sources: + - type: archive + url: https://github.com/recastnavigation/recastnavigation/archive/c5cbd53024c8a9d8d097a4371215e3342d2fdc87.zip + sha256: 53dacfd7bead4d3b0c9a04a648caed3e7c3900e0aba765c15dee26b50f6103c6 + + - name: yaml-cpp + buildsystem: cmake-ninja + sources: + - type: archive + url: https://github.com/jbeder/yaml-cpp/archive/refs/tags/yaml-cpp-0.7.0.zip + sha256: 4d5e664a7fb2d7445fc548cc8c0e1aa7b1a496540eb382d137e2cc263e6d3ef5 + + - name: LuaJIT + buildsystem: simple + build-commands: + - make install PREFIX=/app + sources: + - type: archive + url: https://github.com/LuaJIT/LuaJIT/archive/refs/tags/v2.0.5.zip + sha256: 2adbe397a5b6b8ab22fa8396507ce852a2495db50e50734b3daa1ffcadd9eeb4 + + - name: openmw + builddir: true + buildsystem: cmake-ninja + config-opts: + - "-DBUILD_BSATOOL=no" + - "-DBUILD_ESMTOOL=no" + - "-DCMAKE_BUILD_TYPE=Release" + - "-DICONDIR=/app/share/icons" + - "-DOPENMW_USE_SYSTEM_RECASTNAVIGATION=yes" + sources: + - type: dir + path: .. + - type: shell + commands: + - "sed -i 's:/wiki:/old-wiki:' ./files/openmw.appdata.xml" + - "sed -i 's:>org.openmw.launcher.desktop<:>org.openmw.OpenMW.devel.desktop<:' ./files/openmw.appdata.xml" + - "sed -i 's:Icon=openmw:Icon=org.openmw.OpenMW.devel.png:' ./files/org.openmw.launcher.desktop" + - "sed -i 's:Icon=openmw-cs:Icon=org.openmw.OpenMW.OpenCS.devel.png:' ./files/org.openmw.cs.desktop" + post-install: + - "mv /app/share/applications/org.openmw.launcher.desktop /app/share/applications/org.openmw.OpenMW.devel.desktop" + - "mv /app/share/applications/org.openmw.cs.desktop /app/share/applications/org.openmw.OpenMW.OpenCS.devel.desktop" + - "mv /app/share/icons/openmw.png /app/share/icons/org.openmw.OpenMW.devel.png" + - "mv /app/share/icons/openmw-cs.png /app/share/icons/org.openmw.OpenMW.OpenCS.devel.png" diff --git a/CI/run_integration_tests.sh b/CI/run_integration_tests.sh new file mode 100755 index 00000000000..e79408926a1 --- /dev/null +++ b/CI/run_integration_tests.sh @@ -0,0 +1,16 @@ +#!/bin/bash -ex + +mkdir example-suite +cd example-suite +git init +git remote add origin https://gitlab.com/OpenMW/example-suite.git +git fetch --depth=1 origin ${EXAMPLE_SUITE_REVISION} +git checkout FETCH_HEAD +cd .. + +xvfb-run --auto-servernum --server-args='-screen 0 640x480x24x60' \ + scripts/integration_tests.py --omw build/install/bin/openmw --workdir integration_tests_output example-suite/ + +ls integration_tests_output/*.osg_stats.log | while read v; do + scripts/osg_stats.py --stats '.*' --regexp_match < "${v}" +done diff --git a/CI/teal_ci.sh b/CI/teal_ci.sh new file mode 100755 index 00000000000..17e13e544e8 --- /dev/null +++ b/CI/teal_ci.sh @@ -0,0 +1,15 @@ +#!/bin/bash -e + +docs/source/install_luadocumentor_in_docker.sh +PATH=$PATH:~/luarocks/bin + +pushd . +echo "Install Teal Cyan" +git clone https://github.com/teal-language/cyan.git +cd cyan +git checkout 51649e4a814c05deaf5dde929ba82803f5170bbc +luarocks make cyan-dev-1.rockspec +popd + +scripts/generate_teal_declarations.sh ./teal_declarations +zip teal_declarations.zip -r teal_declarations diff --git a/CI/ubuntu_gcc_preprocess.sh b/CI/ubuntu_gcc_preprocess.sh new file mode 100755 index 00000000000..d519d178aa5 --- /dev/null +++ b/CI/ubuntu_gcc_preprocess.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +set -xeo pipefail + +SRC="${PWD:?}" +VERSION=$(git rev-parse HEAD) + +mkdir -p build +cd build + +cmake \ + -G Ninja \ + -D CMAKE_C_COMPILER=gcc \ + -D CMAKE_CXX_COMPILER=g++ \ + -D USE_SYSTEM_TINYXML=ON \ + -D OPENMW_USE_SYSTEM_RECASTNAVIGATION=ON \ + -D CMAKE_BUILD_TYPE=Release \ + -D CMAKE_C_FLAGS_RELEASE='-DNDEBUG -E -w' \ + -D CMAKE_CXX_FLAGS_RELEASE='-DNDEBUG -E -w' \ + -D CMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -D BUILD_BENCHMARKS=ON \ + -D BUILD_BSATOOL=ON \ + -D BUILD_BULLETOBJECTTOOL=ON \ + -D BUILD_ESMTOOL=ON \ + -D BUILD_ESSIMPORTER=ON \ + -D BUILD_LAUNCHER=ON \ + -D BUILD_LAUNCHER_TESTS=ON \ + -D BUILD_MWINIIMPORTER=ON \ + -D BUILD_NAVMESHTOOL=ON \ + -D BUILD_NIFTEST=ON \ + -D BUILD_OPENCS=ON \ + -D BUILD_OPENCS_TESTS=ON \ + -D BUILD_OPENMW=ON \ + -D BUILD_OPENMW_TESTS=ON \ + -D BUILD_COMPONENTS_TESTS=ON \ + -D BUILD_WIZARD=ON \ + "${SRC}" +cmake --build . --parallel + +cd .. + +scripts/preprocessed_file_size_stats.py --remove_prefix "${SRC}/" build > "${VERSION:?}.json" +ls -al "${VERSION:?}.json" + +if [[ "${GENERATE_ONLY}" ]]; then + exit 0 +fi + +git remote add target "${CI_MERGE_REQUEST_PROJECT_URL:-https://gitlab.com/OpenMW/openmw.git}" + +TARGET_BRANCH="${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-master}" + +git fetch target "${TARGET_BRANCH:?}" + +if [[ "${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}" ]]; then + git remote add source "${CI_MERGE_REQUEST_SOURCE_PROJECT_URL}" + git fetch --unshallow source "${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}" +elif [[ "${CI_COMMIT_BRANCH}" ]]; then + git fetch origin "${CI_COMMIT_BRANCH:?}" +else + git fetch origin +fi + +BASE_VERSION=$(git merge-base "target/${TARGET_BRANCH:?}" "${VERSION:?}") + +# Save and use scripts from this commit because they may be absent or different in the base version +cp scripts/preprocessed_file_size_stats.py scripts/preprocessed_file_size_stats.bak.py +cp CI/ubuntu_gcc_preprocess.sh CI/ubuntu_gcc_preprocess.bak.sh +git checkout "${BASE_VERSION:?}" +mv scripts/preprocessed_file_size_stats.bak.py scripts/preprocessed_file_size_stats.py +mv CI/ubuntu_gcc_preprocess.bak.sh CI/ubuntu_gcc_preprocess.sh +env GENERATE_ONLY=1 CI/ubuntu_gcc_preprocess.sh +git checkout --force "${VERSION:?}" + +scripts/preprocessed_file_size_stats_diff.py "${BASE_VERSION:?}.json" "${VERSION:?}.json" diff --git a/CMakeLists.txt b/CMakeLists.txt index ee7e29bd7a4..5c7348b4079 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,23 +1,32 @@ -project(OpenMW) -cmake_minimum_required(VERSION 3.1.0) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +cmake_minimum_required(VERSION 3.16.0) -# for link time optimization, remove if cmake version is >= 3.9 -if(POLICY CMP0069) # LTO - cmake_policy(SET CMP0069 NEW) +# set the timestamps of extracted contents to the time of extraction +# remove if cmake version is >= 3.24 +if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) endif() -# for position-independent executable, remove if cmake version is >= 3.14 -if(POLICY CMP0083) - cmake_policy(SET CMP0083 NEW) -endif() +project(OpenMW) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +include(GNUInstallDirs) option(OPENMW_GL4ES_MANUAL_INIT "Manually initialize gl4es. This is more reliable on platforms without a windowing system. Requires gl4es to be configured with -DNOEGL=ON -DNO_LOADER=ON -DNO_INIT_CONSTRUCTOR=ON." OFF) if(OPENMW_GL4ES_MANUAL_INIT) add_definitions(-DOPENMW_GL4ES_MANUAL_INIT) endif() +if (APPLE OR WIN32) + set(DEPLOY_QT_TRANSLATIONS_DEFAULT ON) +else () + set(DEPLOY_QT_TRANSLATIONS_DEFAULT OFF) +endif () + +option(DEPLOY_QT_TRANSLATIONS "Deploy standard Qt translations to resources folder. Needed when OpenMW applications are deployed with Qt libraries" ${DEPLOY_QT_TRANSLATIONS_DEFAULT}) + # Apps and tools option(BUILD_OPENMW "Build OpenMW" ON) option(BUILD_LAUNCHER "Build Launcher" ON) @@ -30,15 +39,20 @@ option(BUILD_ESMTOOL "Build ESM inspector" ON) option(BUILD_NIFTEST "Build nif file tester" ON) option(BUILD_DOCS "Build documentation." OFF ) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) -option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF) +option(BUILD_COMPONENTS_TESTS "Build components library tests" OFF) option(BUILD_BENCHMARKS "Build benchmarks with Google Benchmark" OFF) +option(BUILD_NAVMESHTOOL "Build navmesh tool" ON) +option(BUILD_BULLETOBJECTTOOL "Build Bullet object tool" ON) +option(BUILD_OPENCS_TESTS "Build OpenMW Construction Set tests" OFF) +option(BUILD_OPENMW_TESTS "Build OpenMW tests" OFF) +option(PRECOMPILE_HEADERS_WITH_MSVC "Precompile most common used headers with MSVC (alternative to ccache)" ON) set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up. -if (NOT BUILD_LAUNCHER AND NOT BUILD_OPENCS AND NOT BUILD_WIZARD) - set(USE_QT FALSE) +if (BUILD_LAUNCHER OR BUILD_OPENCS OR BUILD_WIZARD OR BUILD_OPENCS_TESTS) + set(USE_QT TRUE) else() - set(USE_QT TRUE) + set(USE_QT FALSE) endif() # If the user doesn't supply a CMAKE_BUILD_TYPE via command line, choose one for them. @@ -50,6 +64,7 @@ IF(NOT CMAKE_BUILD_TYPE) ENDIF() if (APPLE) + set(CMAKE_FIND_FRAMEWORK LAST) # prefer dylibs over frameworks set(APP_BUNDLE_NAME "${CMAKE_PROJECT_NAME}.app") set(APP_BUNDLE_DIR "${OpenMW_BINARY_DIR}/${APP_BUNDLE_NAME}") @@ -65,8 +80,10 @@ endif() message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) -set(OPENMW_VERSION_MINOR 47) +set(OPENMW_VERSION_MINOR 49) set(OPENMW_VERSION_RELEASE 0) +set(OPENMW_LUA_API_REVISION 68) +set(OPENMW_POSTPROCESSING_API_REVISION 2) set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_TAGHASH "") @@ -74,7 +91,7 @@ set(OPENMW_VERSION_COMMITDATE "") set(OPENMW_VERSION "${OPENMW_VERSION_MAJOR}.${OPENMW_VERSION_MINOR}.${OPENMW_VERSION_RELEASE}") -set(OPENMW_DOC_BASEURL "https://openmw.readthedocs.io/en/stable/") +set(OPENMW_DOC_BASEURL "https://openmw.readthedocs.io/en/") set(GIT_CHECKOUT FALSE) if(EXISTS ${PROJECT_SOURCE_DIR}/.git) @@ -107,9 +124,7 @@ include(WholeArchive) configure_file ("${OpenMW_SOURCE_DIR}/docs/mainpage.hpp.cmake" "${OpenMW_BINARY_DIR}/docs/mainpage.hpp") -option(BOOST_STATIC "Link static build of Boost into the binaries" FALSE) -option(SDL2_STATIC "Link static build of SDL into the binaries" FALSE) -option(QT_STATIC "Link static build of QT into the binaries" FALSE) +option(QT_STATIC "Link static build of Qt into the binaries" FALSE) option(OPENMW_USE_SYSTEM_BULLET "Use system provided bullet physics library" ON) if(OPENMW_USE_SYSTEM_BULLET) @@ -138,39 +153,64 @@ option(MYGUI_STATIC "Link static build of Mygui into the binaries" ${_mygui_stat option(OPENMW_USE_SYSTEM_RECASTNAVIGATION "Use system provided recastnavigation library" OFF) if(OPENMW_USE_SYSTEM_RECASTNAVIGATION) set(_recastnavigation_static_default OFF) - find_package(RecastNavigation REQUIRED) + find_package(RecastNavigation REQUIRED CONFIG) else() set(_recastnavigation_static_default ON) endif() option(RECASTNAVIGATION_STATIC "Build recastnavigation static libraries" ${_recastnavigation_static_default}) +option(OPENMW_USE_SYSTEM_SQLITE3 "Use system provided SQLite3 library" ON) + +option(OPENMW_USE_SYSTEM_BENCHMARK "Use system Google Benchmark library." OFF) +option(OPENMW_USE_SYSTEM_GOOGLETEST "Use system Google Test library." OFF) + option(OPENMW_UNITY_BUILD "Use fewer compilation units to speed up compile time" FALSE) option(OPENMW_LTO_BUILD "Build OpenMW with Link-Time Optimization (Needs ~2GB of RAM)" OFF) -# what is necessary to build documentation -IF( BUILD_DOCS ) - # Builds the documentation. - FIND_PACKAGE( Sphinx REQUIRED ) - FIND_PACKAGE( Doxygen REQUIRED ) -ENDIF() - # OS X deployment option(OPENMW_OSX_DEPLOYMENT OFF) if (MSVC) option(OPENMW_MP_BUILD "Build OpenMW with /MP flag" OFF) + if (OPENMW_MP_BUILD) + add_compile_options(/MP) + endif() + + # \bigobj is required: + # 1) for OPENMW_UNITY_BUILD; + # 2) to compile lua bindings in components, openmw and tests, because sol3 is heavily templated. + # there should be no relevant downsides to having it on: + # https://docs.microsoft.com/en-us/cpp/build/reference/bigobj-increase-number-of-sections-in-dot-obj-file + add_compile_options(/bigobj) + + add_compile_options(/Zc:__cplusplus) + + if (CMAKE_CXX_COMPILER_LAUNCHER OR CMAKE_C_COMPILER_LAUNCHER) + if (CMAKE_GENERATOR MATCHES "Visual Studio") + message(STATUS "A compiler launcher was specified, but will be unused by the current generator (${CMAKE_GENERATOR})") + else() + foreach (config_lower ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER "${config_lower}" config) + if (CMAKE_C_COMPILER_LAUNCHER STREQUAL "ccache") + string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_${config} "${CMAKE_C_FLAGS_${config}}") + endif() + if (CMAKE_CXX_COMPILER_LAUNCHER STREQUAL "ccache") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_${config} "${CMAKE_CXX_FLAGS_${config}}") + endif() + endforeach() + endif() + endif() endif() # Set up common paths if (APPLE) - set(MORROWIND_DATA_FILES "./data" CACHE PATH "location of Morrowind data files") set(OPENMW_RESOURCE_FILES "../Resources/resources" CACHE PATH "location of OpenMW resources files") elseif(UNIX) # Paths SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin" CACHE PATH "Where to install binaries") SET(LIBDIR "${CMAKE_INSTALL_PREFIX}/lib${LIB_SUFFIX}" CACHE PATH "Where to install libraries") - SET(DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE PATH "Sets the root of data directories to a non-default location") - SET(GLOBAL_DATA_PATH "${DATAROOTDIR}/games/" CACHE PATH "Set data path prefix") + SET(DATAROOTDIR "${CMAKE_INSTALL_DATAROOTDIR}" CACHE PATH "Sets the root of data directories to a non-default location") + SET(GLOBAL_DATA_PATH "${CMAKE_INSTALL_PREFIX}/share/games/" CACHE PATH "Set data path prefix") SET(DATADIR "${GLOBAL_DATA_PATH}/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") SET(ICONDIR "${DATAROOTDIR}/pixmaps" CACHE PATH "Set icon dir") SET(LICDIR "${DATAROOTDIR}/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") @@ -181,43 +221,58 @@ elseif(UNIX) ENDIF() SET(SYSCONFDIR "${GLOBAL_CONFIG_PATH}/openmw" CACHE PATH "Set config dir") - set(MORROWIND_DATA_FILES "${DATADIR}/data" CACHE PATH "location of Morrowind data files") set(OPENMW_RESOURCE_FILES "${DATADIR}/resources" CACHE PATH "location of OpenMW resources files") else() - set(MORROWIND_DATA_FILES "data" CACHE PATH "location of Morrowind data files") set(OPENMW_RESOURCE_FILES "resources" CACHE PATH "location of OpenMW resources files") endif(APPLE) if (WIN32) - option(USE_DEBUG_CONSOLE "whether a debug console should be enabled for debug builds, if false debug output is redirected to Visual Studio output" ON) + option(USE_DEBUG_CONSOLE "Whether a console should be displayed if OpenMW isn't launched from the command line. Does not affect the Release configuration." ON) +endif() + +if(MSVC) + add_compile_options("/utf-8") endif() # Dependencies -find_package(OpenGL REQUIRED) +if (APPLE) + # Force CMake to use the installed version of OpenGL on macOS + set(_SAVE_CMAKE_FIND_FRAMEWORK ${CMAKE_FIND_FRAMEWORK}) + set(CMAKE_FIND_FRAMEWORK ONLY) + find_package(OpenGL REQUIRED) + set(CMAKE_FIND_FRAMEWORK ${_SAVE_CMAKE_FIND_FRAMEWORK}) + unset(_SAVE_CMAKE_FIND_FRAMEWORK) +else() + find_package(OpenGL REQUIRED) +endif() find_package(LZ4 REQUIRED) if (USE_QT) - find_package(Qt5Core 5.12 REQUIRED) - find_package(Qt5Widgets REQUIRED) - find_package(Qt5Network REQUIRED) - find_package(Qt5OpenGL REQUIRED) - # Instruct CMake to run moc automatically when needed. - #set(CMAKE_AUTOMOC ON) + find_package(QT REQUIRED COMPONENTS Core NAMES Qt6 Qt5) + if (QT_VERSION_MAJOR VERSION_EQUAL 5) + find_package(Qt5 5.15 COMPONENTS Core Widgets Network OpenGL LinguistTools Svg REQUIRED) + else() + find_package(Qt6 COMPONENTS Core Widgets Network OpenGL OpenGLWidgets LinguistTools Svg REQUIRED) + endif() + message(STATUS "Using Qt${QT_VERSION}") endif() set(USED_OSG_COMPONENTS + osgAnimation osgDB - osgViewer - osgText osgGA + osgFX osgParticle + osgText osgUtil - osgFX osgShadow - osgAnimation) + osgSim + osgViewer + ) set(USED_OSG_PLUGINS osgdb_bmp + osgdb_dae osgdb_dds osgdb_freetype osgdb_jpeg @@ -226,6 +281,37 @@ set(USED_OSG_PLUGINS osgdb_serializers_osg osgdb_tga) +if(NOT COLLADA_DOM_VERSION_MAJOR) + set(COLLADA_DOM_VERSION_MAJOR 2) +endif() + +if(NOT COLLADA_DOM_VERSION_MINOR) + set(COLLADA_DOM_VERSION_MINOR 5) +endif() + +find_package(collada_dom 2.5) + +option(OPENMW_USE_SYSTEM_ICU "Use system ICU library instead of internal. If disabled, requires autotools" ON) +if(OPENMW_USE_SYSTEM_ICU) + find_package(ICU REQUIRED COMPONENTS uc i18n data) +endif() + +option(OPENMW_USE_SYSTEM_YAML_CPP "Use system yaml-cpp library instead of internal." ON) +if(OPENMW_USE_SYSTEM_YAML_CPP) + set(_yaml_cpp_static_default OFF) +else() + set(_yaml_cpp_static_default ON) +endif() +option(YAML_CPP_STATIC "Link static build of yaml-cpp into the binaries" ${_yaml_cpp_static_default}) +if (OPENMW_USE_SYSTEM_YAML_CPP) + find_package(yaml-cpp REQUIRED) +endif() + +if ((BUILD_COMPONENTS_TESTS OR BUILD_OPENCS_TESTS OR BUILD_OPENMW_TESTS) AND OPENMW_USE_SYSTEM_GOOGLETEST) + find_package(GTest 1.10 REQUIRED) + find_package(GMock 1.10 REQUIRED) +endif() + add_subdirectory(extern) # Sound setup @@ -238,8 +324,8 @@ find_package(FFmpeg REQUIRED COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE SWRESAMP if(FFmpeg_FOUND) SET(FFVER_OK TRUE) - # Can not detect FFmpeg version on Windows for now - if (NOT WIN32) + # Can not detect FFmpeg version on Windows or Android for now + if (NOT WIN32 AND NOT ANDROID) if(FFmpeg_AVFORMAT_VERSION VERSION_LESS "57.56.100") message(STATUS "libavformat is too old! (${FFmpeg_AVFORMAT_VERSION}, wanted 57.56.100)") set(FFVER_OK FALSE) @@ -278,7 +364,7 @@ endif() # Required for building the FFmpeg headers add_definitions(-D__STDC_CONSTANT_MACROS) -# Reqiuired for unity build +# Required for unity build add_definitions(-DMYGUI_DONT_REPLACE_NULLPTR) # TinyXML @@ -291,16 +377,16 @@ endif() # Platform specific if (WIN32) - if(NOT MINGW) - set(Boost_USE_STATIC_LIBS ON) - add_definitions(-DBOOST_ALL_NO_LIB) - endif(NOT MINGW) - # Suppress WinMain(), provided by SDL add_definitions(-DSDL_MAIN_HANDLED) # Get rid of useless crud from windows.h - add_definitions(-DNOMINMAX -DWIN32_LEAN_AND_MEAN) + add_definitions( + -DNOMINMAX # name conflict with std::min, std::max + -DWIN32_LEAN_AND_MEAN + -DNOMB # name conflict with MWGui::MessageBox + -DNOGDI # name conflict with osgAnimation::MorphGeometry::RELATIVE + ) endif() if(OPENMW_USE_SYSTEM_BULLET) @@ -355,10 +441,7 @@ if(NOT HAVE_STDINT_H) endif() if(OPENMW_USE_SYSTEM_OSG) - find_package(OpenSceneGraph 3.4.0 REQUIRED ${USED_OSG_COMPONENTS}) - if (${OPENSCENEGRAPH_VERSION} VERSION_GREATER 3.6.2 AND ${OPENSCENEGRAPH_VERSION} VERSION_LESS 3.6.5) - message(FATAL_ERROR "OpenSceneGraph version ${OPENSCENEGRAPH_VERSION} has critical regressions which cause crashes. Please upgrade to 3.6.5 or later. We strongly recommend using the tip of the official 'OpenSceneGraph-3.6' branch or the tip of '3.6' OpenMW/osg (OSGoS).") - endif() + find_package(OpenSceneGraph 3.6.5 REQUIRED ${USED_OSG_COMPONENTS}) # Bump to 3.6.6 when released if(OSG_STATIC) find_package(OSGPlugins REQUIRED COMPONENTS ${USED_OSG_PLUGINS}) @@ -371,44 +454,56 @@ if(OSG_STATIC) add_definitions(-DOSG_LIBRARY_STATIC) endif() -set(BOOST_COMPONENTS system filesystem program_options iostreams) -if(WIN32) - set(BOOST_COMPONENTS ${BOOST_COMPONENTS} locale) - if(MSVC) - # boost-zlib is not present (nor needed) in vcpkg version of boost. - # there, it is part of boost-iostreams instead. - set(BOOST_OPTIONAL_COMPONENTS zlib) - endif(MSVC) -endif(WIN32) +include(cmake/CheckOsgMultiview.cmake) +if(HAVE_MULTIVIEW) + add_definitions(-DOSG_HAS_MULTIVIEW) +endif(HAVE_MULTIVIEW) -IF(BOOST_STATIC) - set(Boost_USE_STATIC_LIBS ON) -endif() +set(BOOST_COMPONENTS iostreams program_options system) -set(Boost_NO_BOOST_CMAKE ON) +find_package(Boost 1.70.0 CONFIG REQUIRED COMPONENTS ${BOOST_COMPONENTS} OPTIONAL_COMPONENTS ${BOOST_OPTIONAL_COMPONENTS}) -find_package(Boost 1.6.2 REQUIRED COMPONENTS ${BOOST_COMPONENTS} OPTIONAL_COMPONENTS ${BOOST_OPTIONAL_COMPONENTS}) if(OPENMW_USE_SYSTEM_MYGUI) - find_package(MyGUI 3.2.2 REQUIRED) + find_package(MyGUI 3.4.3 REQUIRED) endif() find_package(SDL2 2.0.9 REQUIRED) find_package(OpenAL REQUIRED) +find_package(ZLIB REQUIRED) + +option(USE_LUAJIT "Switch Lua/LuaJit (TRUE is highly recommended)" TRUE) +if(USE_LUAJIT) + find_package(LuaJit REQUIRED) + set(LUA_INCLUDE_DIR ${LuaJit_INCLUDE_DIR}) + set(LUA_LIBRARIES ${LuaJit_LIBRARIES}) +else(USE_LUAJIT) + find_package(Lua REQUIRED) + add_compile_definitions(NO_LUAJIT) +endif(USE_LUAJIT) +if (NOT WIN32 AND NOT ANDROID) + include(cmake/CheckLuaCustomAllocator.cmake) +endif() + +# C++ library binding to Lua +set(SOL_INCLUDE_DIR ${OpenMW_SOURCE_DIR}/extern/sol3) +set(SOL_CONFIG_DIR ${OpenMW_SOURCE_DIR}/extern/sol_config) include_directories( BEFORE SYSTEM "." - ${SDL2_INCLUDE_DIR} - ${Boost_INCLUDE_DIR} ${MyGUI_INCLUDE_DIRS} ${OPENAL_INCLUDE_DIR} ${OPENGL_INCLUDE_DIR} ${BULLET_INCLUDE_DIRS} + ${LUA_INCLUDE_DIR} + ${SOL_INCLUDE_DIR} + ${SOL_CONFIG_DIR} + ${ICU_INCLUDE_DIRS} ) -link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS}) +link_directories(${COLLADA_DOM_LIBRARY_DIRS}) if(MYGUI_STATIC) - add_definitions(-DMYGUI_STATIC) + add_definitions(-DMYGUI_STATIC) endif (MYGUI_STATIC) if (APPLE) @@ -419,9 +514,8 @@ if (APPLE) "${APP_BUNDLE_DIR}/Contents/Resources/OpenMW.icns" COPYONLY) endif (APPLE) -if (NOT APPLE) - set(OPENMW_MYGUI_FILES_ROOT ${OpenMW_BINARY_DIR}) - set(OPENMW_SHADERS_ROOT ${OpenMW_BINARY_DIR}) +if (NOT APPLE) # this is modified for macOS use later in "apps/open[mw|cs]/CMakeLists.txt" + set(OPENMW_RESOURCES_ROOT ${OpenMW_BINARY_DIR}) endif () add_subdirectory(files/) @@ -458,8 +552,8 @@ else () "${OpenMW_BINARY_DIR}/openmw.cfg") endif () -configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw-cs.cfg - "${OpenMW_BINARY_DIR}" "openmw-cs.cfg") +pack_resource_file(${OpenMW_SOURCE_DIR}/files/openmw-cs.cfg + "${OpenMW_BINARY_DIR}" "defaults-cs.bin") # Needs the copy version because the configure version assumes the end of the file has been reached when a null character is reached and there are no CMake expressions to evaluate. copy_resource_file(${OpenMW_SOURCE_DIR}/files/opencs/defaultfilters @@ -478,27 +572,22 @@ if (NOT WIN32 AND NOT APPLE) endif() if(OPENMW_LTO_BUILD) - if(NOT CMAKE_VERSION VERSION_LESS 3.9) - include(CheckIPOSupported) - check_ipo_supported(RESULT HAVE_IPO OUTPUT HAVE_IPO_OUTPUT) - if(HAVE_IPO) - message(STATUS "LTO enabled for Release configuration.") - set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) - else() - message(WARNING "Requested option OPENMW_LTO_BUILD not supported by this compiler: ${HAVE_IPO_OUTPUT}") - if(MSVC) - message(STATUS "Note: Flags used to be set manually for this setting with MSVC. We now rely on CMake for this. Upgrade CMake to at least 3.13 to re-enable this setting.") - endif() - endif() + include(CheckIPOSupported) + check_ipo_supported(RESULT HAVE_IPO OUTPUT HAVE_IPO_OUTPUT) + if(HAVE_IPO) + message(STATUS "LTO enabled for Release configuration.") + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) else() - message(WARNING "Requested option OPENMW_LTO_BUILD not supported by this cmake version: ${CMAKE_VERSION}. Upgrade CMake to at least 3.9 to enable support for certain compilers. Newer CMake versions support more compilers.") + message(WARNING "Requested option OPENMW_LTO_BUILD not supported by this compiler: ${HAVE_IPO_OUTPUT}") + if(MSVC) + message(STATUS "Note: Flags used to be set manually for this setting with MSVC. We now rely on CMake for this. Upgrade CMake to at least 3.13 to re-enable this setting.") + endif() endif() endif() if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wundef -Wno-unused-parameter -pedantic -Wno-long-long") - add_definitions( -DBOOST_NO_CXX11_SCOPED_ENUMS=ON ) + set(OPENMW_CXX_FLAGS "-Wall -Wextra -Wundef -Wextra-semi -Wno-unused-parameter -pedantic -Wno-long-long -Wnon-virtual-dtor -Wunused ${OPENMW_CXX_FLAGS}") if (APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") @@ -507,12 +596,12 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) 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(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-potentially-evaluated-expression") + set(OPENMW_CXX_FLAGS "${OPENMW_CXX_FLAGS} -Wno-potentially-evaluated-expression") endif () endif() - if (CMAKE_CXX_COMPILER_ID STREQUAL GNU AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.6 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 4.6) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-but-set-parameter") + 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) @@ -521,185 +610,234 @@ endif (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clan add_subdirectory (extern/osg-ffmpeg-videoplayer) add_subdirectory (extern/oics) add_subdirectory (extern/Base64) -if (BUILD_OPENCS) +if (BUILD_OPENCS OR BUILD_OPENCS_TESTS) add_subdirectory (extern/osgQt) endif() +if (OPENMW_CXX_FLAGS) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OPENMW_CXX_FLAGS}") +endif() + # Components add_subdirectory (components) -target_compile_definitions(components PRIVATE OPENMW_DOC_BASEURL="${OPENMW_DOC_BASEURL}") # Apps and tools -if (BUILD_OPENMW) +if (BUILD_OPENMW OR BUILD_OPENMW_TESTS) add_subdirectory( apps/openmw ) endif() if (BUILD_BSATOOL) - add_subdirectory( apps/bsatool ) + add_subdirectory( apps/bsatool ) endif() if (BUILD_ESMTOOL) - add_subdirectory( apps/esmtool ) + add_subdirectory( apps/esmtool ) endif() if (BUILD_LAUNCHER) - add_subdirectory( apps/launcher ) + add_subdirectory( apps/launcher ) endif() if (BUILD_MWINIIMPORTER) - add_subdirectory( apps/mwiniimporter ) + add_subdirectory( apps/mwiniimporter ) endif() if (BUILD_ESSIMPORTER) - add_subdirectory (apps/essimporter ) + add_subdirectory (apps/essimporter ) endif() -if (BUILD_OPENCS) - add_subdirectory (apps/opencs) +if (BUILD_OPENCS OR BUILD_OPENCS_TESTS) + add_subdirectory (apps/opencs) endif() if (BUILD_WIZARD) - add_subdirectory(apps/wizard) + add_subdirectory(apps/wizard) endif() if (BUILD_NIFTEST) add_subdirectory(apps/niftest) -endif(BUILD_NIFTEST) +endif() -# UnitTests -if (BUILD_UNITTESTS) - add_subdirectory( apps/openmw_test_suite ) +if (BUILD_COMPONENTS_TESTS) + add_subdirectory(apps/components_tests) endif() if (BUILD_BENCHMARKS) - add_subdirectory(apps/benchmarks) + add_subdirectory(apps/benchmarks) +endif() + +if (BUILD_NAVMESHTOOL) + add_subdirectory(apps/navmeshtool) +endif() + +if (BUILD_BULLETOBJECTTOOL) + add_subdirectory(apps/bulletobjecttool) +endif() + +if (BUILD_OPENCS_TESTS) + add_subdirectory(apps/opencs_tests) +endif() + +if (BUILD_OPENMW_TESTS) + add_subdirectory(apps/openmw_tests) endif() if (WIN32) - if (MSVC) - if (OPENMW_MP_BUILD) - set( MT_BUILD "/MP") - endif() + if (MSVC) + foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} ) + string( TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG ) + set( CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "$(SolutionDir)$(Configuration)" ) + set( CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "$(ProjectDir)$(Configuration)" ) + endforeach( OUTPUTCONFIG ) + + if (USE_DEBUG_CONSOLE AND BUILD_OPENMW) + set_target_properties(openmw PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE") + set_target_properties(openmw PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:CONSOLE") + elseif (BUILD_OPENMW) + # Turn off implicit console, you won't see stdout unless launching OpenMW from a command line shell or look at openmw.log + set_target_properties(openmw PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:WINDOWS") + set_target_properties(openmw PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:WINDOWS") + endif() - foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} ) - string( TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG ) - set( CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "$(SolutionDir)$(Configuration)" ) - set( CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "$(ProjectDir)$(Configuration)" ) - endforeach( OUTPUTCONFIG ) - - if (USE_DEBUG_CONSOLE AND BUILD_OPENMW) - set_target_properties(openmw PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE") - set_target_properties(openmw PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:CONSOLE") - set_target_properties(openmw PROPERTIES COMPILE_DEFINITIONS $<$:_CONSOLE>) - elseif (BUILD_OPENMW) - # Turn off debug console, debug output will be written to visual studio output instead - set_target_properties(openmw PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:WINDOWS") - set_target_properties(openmw PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:WINDOWS") - endif() + if (BUILD_OPENMW) + # Release builds don't use the debug console + set_target_properties(openmw PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") + set_target_properties(openmw PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") + endif() - if (BUILD_OPENMW) - # Release builds don't use the debug console - set_target_properties(openmw PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") - set_target_properties(openmw PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") - endif() + # Play a bit with the warning levels - # Play a bit with the warning levels + set(WARNINGS "/W4") - 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 + ) - set(WARNINGS_DISABLE - 4100 # Unreferenced formal parameter (-Wunused-parameter) - 4127 # Conditional expression is constant - 4996 # Function was declared deprecated - ) - - if( "${MyGUI_VERSION}" VERSION_LESS_EQUAL "3.4.0" ) - set(WARNINGS_DISABLE ${WARNINGS_DISABLE} - 4866 # compiler may not enforce left-to-right evaluation order for call - ) - endif() + foreach(d ${WARNINGS_DISABLE}) + list(APPEND WARNINGS "/wd${d}") + endforeach(d) - if( "${MyGUI_VERSION}" VERSION_LESS_EQUAL "3.4.1" ) - set(WARNINGS_DISABLE ${WARNINGS_DISABLE} - 4275 # non dll-interface class 'MyGUI::delegates::IDelegateUnlink' used as base for dll-interface class 'MyGUI::Widget' - ) - endif() + if(OPENMW_MSVC_WERROR) + list(APPEND WARNINGS "/WX") + endif() - foreach(d ${WARNINGS_DISABLE}) - set(WARNINGS "${WARNINGS} /wd${d}") - endforeach(d) + target_compile_options(components PRIVATE ${WARNINGS}) + target_compile_options(osg-ffmpeg-videoplayer PRIVATE ${WARNINGS}) - set_target_properties(components PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - set_target_properties(osg-ffmpeg-videoplayer PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - - if (MSVC_VERSION GREATER_EQUAL 1915 AND MSVC_VERSION LESS 1920) - target_compile_definitions(components INTERFACE _ENABLE_EXTENDED_ALIGNED_STORAGE) - endif() + if (MSVC_VERSION GREATER_EQUAL 1915 AND MSVC_VERSION LESS 1920) + target_compile_definitions(components INTERFACE _ENABLE_EXTENDED_ALIGNED_STORAGE) + endif() - if (BUILD_BSATOOL) - set_target_properties(bsatool PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - endif() + if (BUILD_BSATOOL) + target_compile_options(bsatool PRIVATE ${WARNINGS}) + endif() - if (BUILD_ESMTOOL) - set_target_properties(esmtool PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - endif() + if (BUILD_ESMTOOL) + target_compile_options(esmtool PRIVATE ${WARNINGS}) + endif() - if (BUILD_ESSIMPORTER) - set_target_properties(openmw-essimporter PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - endif() + if (BUILD_ESSIMPORTER) + target_compile_options(openmw-essimporter PRIVATE ${WARNINGS}) + endif() - if (BUILD_LAUNCHER) - set_target_properties(openmw-launcher PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - endif() + if (BUILD_LAUNCHER) + target_compile_options(openmw-launcher PRIVATE ${WARNINGS}) + endif() - if (BUILD_MWINIIMPORTER) - set_target_properties(openmw-iniimporter PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - endif() + if (BUILD_MWINIIMPORTER) + target_compile_options(openmw-iniimporter PRIVATE ${WARNINGS}) + endif() - if (BUILD_OPENCS) - set_target_properties(openmw-cs PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - endif() + if (BUILD_OPENCS) + target_compile_options(openmw-cs PRIVATE ${WARNINGS}) + endif() - if (BUILD_OPENMW) - if (OPENMW_UNITY_BUILD) - set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD} /bigobj") - else() - set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + if (BUILD_OPENMW) + target_compile_options(openmw PRIVATE ${WARNINGS}) endif() - endif() - if (BUILD_WIZARD) - set_target_properties(openmw-wizard PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - endif() + if (BUILD_WIZARD) + target_compile_options(openmw-wizard PRIVATE ${WARNINGS}) + endif() - if (BUILD_BENCHMARKS) - set_target_properties(openmw_detournavigator_navmeshtilescache_benchmark PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") - endif() - endif(MSVC) + 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 - #set_target_properties(openmw PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") - #set_target_properties(openmw PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") + # TODO: At some point release builds should not use the console but rather write to a log file + #set_target_properties(openmw PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") + #set_target_properties(openmw PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") +endif() + +if (APPLE) + target_compile_definitions(components PRIVATE GL_SILENCE_DEPRECATION=1) + target_compile_definitions(openmw PRIVATE GL_SILENCE_DEPRECATION=1) endif() # Apple bundling if (OPENMW_OSX_DEPLOYMENT AND APPLE) - if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.13 AND CMAKE_VERSION VERSION_LESS 3.13.4) - message(FATAL_ERROR "macOS packaging is broken in early CMake 3.13 releases, see https://gitlab.com/OpenMW/openmw/issues/4767. Please use at least 3.13.4 or an older version like 3.12.4") + if (CMAKE_VERSION VERSION_LESS 3.19) + message(FATAL_ERROR "macOS packaging requires CMake 3.19 or higher to sign macOS app bundles.") endif () - get_property(QT_COCOA_PLUGIN_PATH TARGET Qt5::QCocoaIntegrationPlugin PROPERTY LOCATION_RELEASE) + get_property(QT_COCOA_PLUGIN_PATH TARGET Qt${QT_VERSION_MAJOR}::QCocoaIntegrationPlugin PROPERTY LOCATION_RELEASE) get_filename_component(QT_COCOA_PLUGIN_DIR "${QT_COCOA_PLUGIN_PATH}" DIRECTORY) get_filename_component(QT_COCOA_PLUGIN_GROUP "${QT_COCOA_PLUGIN_DIR}" NAME) get_filename_component(QT_COCOA_PLUGIN_NAME "${QT_COCOA_PLUGIN_PATH}" NAME) configure_file("${QT_COCOA_PLUGIN_PATH}" "${APP_BUNDLE_DIR}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}" COPYONLY) + + get_property(QT_QMACSTYLE_PLUGIN_PATH TARGET Qt${QT_VERSION_MAJOR}::QMacStylePlugin PROPERTY LOCATION_RELEASE) + get_filename_component(QT_QMACSTYLE_PLUGIN_DIR "${QT_QMACSTYLE_PLUGIN_PATH}" DIRECTORY) + get_filename_component(QT_QMACSTYLE_PLUGIN_GROUP "${QT_QMACSTYLE_PLUGIN_DIR}" NAME) + get_filename_component(QT_QMACSTYLE_PLUGIN_NAME "${QT_QMACSTYLE_PLUGIN_PATH}" NAME) + configure_file("${QT_QMACSTYLE_PLUGIN_PATH}" "${APP_BUNDLE_DIR}/Contents/PlugIns/${QT_QMACSTYLE_PLUGIN_GROUP}/${QT_QMACSTYLE_PLUGIN_NAME}" COPYONLY) + + get_property(QT_QSVG_PLUGIN_PATH TARGET Qt${QT_VERSION_MAJOR}::QSvgPlugin PROPERTY LOCATION_RELEASE) + get_filename_component(QT_QSVG_PLUGIN_DIR "${QT_QSVG_PLUGIN_PATH}" DIRECTORY) + get_filename_component(QT_QSVG_PLUGIN_GROUP "${QT_QSVG_PLUGIN_DIR}" NAME) + get_filename_component(QT_QSVG_PLUGIN_NAME "${QT_QSVG_PLUGIN_PATH}" NAME) + configure_file("${QT_QSVG_PLUGIN_PATH}" "${APP_BUNDLE_DIR}/Contents/PlugIns/${QT_QSVG_PLUGIN_GROUP}/${QT_QSVG_PLUGIN_NAME}" COPYONLY) + + get_property(QT_QSVG_ICON_PLUGIN_PATH TARGET Qt${QT_VERSION_MAJOR}::QSvgIconPlugin PROPERTY LOCATION_RELEASE) + get_filename_component(QT_QSVG_ICON_PLUGIN_DIR "${QT_QSVG_ICON_PLUGIN_PATH}" DIRECTORY) + get_filename_component(QT_QSVG_ICON_PLUGIN_GROUP "${QT_QSVG_ICON_PLUGIN_DIR}" NAME) + get_filename_component(QT_QSVG_ICON_PLUGIN_NAME "${QT_QSVG_ICON_PLUGIN_PATH}" NAME) + configure_file("${QT_QSVG_ICON_PLUGIN_PATH}" "${APP_BUNDLE_DIR}/Contents/PlugIns/${QT_QSVG_ICON_PLUGIN_GROUP}/${QT_QSVG_ICON_PLUGIN_NAME}" COPYONLY) + configure_file("${OpenMW_SOURCE_DIR}/files/mac/qt.conf" "${APP_BUNDLE_DIR}/Contents/Resources/qt.conf" COPYONLY) if (BUILD_OPENCS) - get_property(OPENCS_BUNDLE_NAME_TMP TARGET openmw-cs PROPERTY OUTPUT_NAME) - set(OPENCS_BUNDLE_NAME "${OPENCS_BUNDLE_NAME_TMP}.app") - configure_file("${QT_COCOA_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}" COPYONLY) - configure_file("${OpenMW_SOURCE_DIR}/files/mac/qt.conf" "${OPENCS_BUNDLE_NAME}/Contents/Resources/qt.conf" COPYONLY) + get_property(OPENCS_BUNDLE_NAME_TMP TARGET openmw-cs PROPERTY OUTPUT_NAME) + set(OPENCS_BUNDLE_NAME "${OPENCS_BUNDLE_NAME_TMP}.app") + configure_file("${QT_COCOA_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}" COPYONLY) + configure_file("${QT_QMACSTYLE_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_QMACSTYLE_PLUGIN_GROUP}/${QT_QMACSTYLE_PLUGIN_NAME}" COPYONLY) + configure_file("${QT_QSVG_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_QSVG_PLUGIN_GROUP}/${QT_QSVG_PLUGIN_NAME}" COPYONLY) + configure_file("${QT_QSVG_ICON_PLUGIN_PATH}" "${OPENCS_BUNDLE_NAME}/Contents/PlugIns/${QT_QSVG_ICON_PLUGIN_GROUP}/${QT_QSVG_ICON_PLUGIN_NAME}" COPYONLY) + configure_file("${OpenMW_SOURCE_DIR}/files/mac/qt.conf" "${OPENCS_BUNDLE_NAME}/Contents/Resources/qt.conf" COPYONLY) endif () install(DIRECTORY "${APP_BUNDLE_DIR}" USE_SOURCE_PERMISSIONS DESTINATION "." COMPONENT Runtime) @@ -767,8 +905,15 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE) install_plugins_for_bundle("${APP_BUNDLE_NAME}" PLUGINS) install_plugins_for_bundle("${OPENCS_BUNDLE_NAME}" OPENCS_PLUGINS) + INSTALL(FILES "${OpenMW_SOURCE_DIR}/CHANGELOG.md" DESTINATION "/${INSTALLED_OPENMW_APP}/..") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/README.md" DESTINATION "/${INSTALLED_OPENMW_APP}/..") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/LICENSE" DESTINATION "/${INSTALLED_OPENMW_APP}/..") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/AUTHORS.md" DESTINATION "/${INSTALLED_OPENMW_APP}/..") + set(PLUGINS ${PLUGINS} "${INSTALLED_OPENMW_APP}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}") + set(PLUGINS ${PLUGINS} "${INSTALLED_OPENMW_APP}/Contents/PlugIns/${QT_QMACSTYLE_PLUGIN_GROUP}/${QT_QMACSTYLE_PLUGIN_NAME}") set(OPENCS_PLUGINS ${OPENCS_PLUGINS} "${INSTALLED_OPENCS_APP}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}") + set(OPENCS_PLUGINS ${OPENCS_PLUGINS} "${INSTALLED_OPENCS_APP}/Contents/PlugIns/${QT_QMACSTYLE_PLUGIN_GROUP}/${QT_QMACSTYLE_PLUGIN_NAME}") install(CODE " function(gp_item_default_embedded_path_override item default_embedded_path_var) @@ -781,6 +926,9 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE) fixup_bundle(\"${INSTALLED_OPENMW_APP}\" \"${PLUGINS}\" \"\") fixup_bundle(\"${INSTALLED_OPENCS_APP}\" \"${OPENCS_PLUGINS}\" \"\") " COMPONENT Runtime) + + set(CPACK_PRE_BUILD_SCRIPTS ${CMAKE_SOURCE_DIR}/cmake/SignMacApplications.cmake) + include(CPack) elseif(NOT APPLE) get_generator_is_multi_config(multi_config) @@ -813,7 +961,7 @@ elseif(NOT APPLE) INSTALL(FILES "${OpenMW_SOURCE_DIR}/CHANGELOG.md" DESTINATION "." RENAME "CHANGELOG.txt") INSTALL(FILES "${OpenMW_SOURCE_DIR}/README.md" DESTINATION "." RENAME "README.txt") INSTALL(FILES "${OpenMW_SOURCE_DIR}/LICENSE" DESTINATION "." RENAME "LICENSE.txt") - INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/mygui/DejaVuFontLicense.txt" DESTINATION ".") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/AUTHORS.md" DESTINATION "." RENAME "AUTHORS.txt") INSTALL(FILES "${INSTALL_SOURCE}/defaults.bin" DESTINATION ".") INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION ".") @@ -908,9 +1056,12 @@ elseif(NOT APPLE) IF(BUILD_WIZARD) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-wizard" DESTINATION "${BINDIR}" ) ENDIF(BUILD_WIZARD) - - # Install licenses - INSTALL(FILES "files/mygui/DejaVuFontLicense.txt" DESTINATION "${LICDIR}" ) + if(BUILD_NAVMESHTOOL) + install(PROGRAMS "${INSTALL_SOURCE}/openmw-navmeshtool" DESTINATION "${BINDIR}" ) + endif() + IF(BUILD_BULLETOBJECTTOOL) + INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-bulletobjecttool" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_BULLETOBJECTTOOL) # Install icon and desktop file INSTALL(FILES "${OpenMW_BINARY_DIR}/org.openmw.launcher.desktop" DESTINATION "${DATAROOTDIR}/applications" COMPONENT "openmw") @@ -924,25 +1075,28 @@ elseif(NOT APPLE) # Install global configuration files INSTALL(FILES "${INSTALL_SOURCE}/defaults.bin" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") INSTALL(FILES "${INSTALL_SOURCE}/openmw.cfg.install" DESTINATION "${SYSCONFDIR}" RENAME "openmw.cfg" COMPONENT "openmw") - INSTALL(FILES "${INSTALL_SOURCE}/resources/version" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") IF(BUILD_OPENCS) - INSTALL(FILES "${INSTALL_SOURCE}/openmw-cs.cfg" DESTINATION "${SYSCONFDIR}" COMPONENT "opencs") + INSTALL(FILES "${INSTALL_SOURCE}/defaults-cs.bin" DESTINATION "${SYSCONFDIR}" COMPONENT "opencs") ENDIF(BUILD_OPENCS) # Install resources INSTALL(DIRECTORY "${INSTALL_SOURCE}/resources" DESTINATION "${DATADIR}" COMPONENT "Resources") - INSTALL(DIRECTORY DESTINATION "${DATADIR}/data" COMPONENT "Resources") endif(WIN32) endif(OPENMW_OSX_DEPLOYMENT AND APPLE) -# Doxygen Target -- simply run 'make doc' or 'make doc_pages' -# output directory for 'make doc' is "${OpenMW_BINARY_DIR}/docs/Doxygen" -# output directory for 'make doc_pages' is "${DOXYGEN_PAGES_OUTPUT_DIR}" if defined -# or "${OpenMW_BINARY_DIR}/docs/Pages" otherwise -find_package(Doxygen) -if (DOXYGEN_FOUND) +# what is necessary to build documentation +if ( BUILD_DOCS ) + # Builds the documentation. + FIND_PACKAGE( Sphinx REQUIRED ) + FIND_PACKAGE( Doxygen REQUIRED ) + + # Doxygen Target -- simply run 'make doc' or 'make doc_pages' + # output directory for 'make doc' is "${OpenMW_BINARY_DIR}/docs/Doxygen" + # output directory for 'make doc_pages' is "${DOXYGEN_PAGES_OUTPUT_DIR}" if defined + # or "${OpenMW_BINARY_DIR}/docs/Pages" otherwise + # determine output directory for doc_pages if (NOT DEFINED DOXYGEN_PAGES_OUTPUT_DIR) set(DOXYGEN_PAGES_OUTPUT_DIR "${OpenMW_BINARY_DIR}/docs/Pages") @@ -959,3 +1113,78 @@ if (DOXYGEN_FOUND) WORKING_DIRECTORY ${OpenMW_BINARY_DIR} COMMENT "Generating documentation for the github-pages at ${DOXYGEN_PAGES_OUTPUT_DIR}" VERBATIM) endif () + +# Qt localization +if (USE_QT) + file(GLOB LAUNCHER_TS_FILES ${CMAKE_SOURCE_DIR}/files/lang/launcher_*.ts) + file(GLOB WIZARD_TS_FILES ${CMAKE_SOURCE_DIR}/files/lang/wizard_*.ts) + file(GLOB COMPONENTS_TS_FILES ${CMAKE_SOURCE_DIR}/files/lang/components_*.ts) + get_target_property(QT_LUPDATE_EXECUTABLE Qt::lupdate IMPORTED_LOCATION) + add_custom_target(translations + COMMAND ${QT_LUPDATE_EXECUTABLE} -locations none ${CMAKE_SOURCE_DIR}/components/contentselector ${CMAKE_SOURCE_DIR}/components/process -ts ${COMPONENTS_TS_FILES} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/components + VERBATIM + COMMAND_EXPAND_LISTS + + COMMAND ${QT_LUPDATE_EXECUTABLE} -locations none ${CMAKE_SOURCE_DIR}/apps/wizard -ts ${WIZARD_TS_FILES} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/apps/wizard + VERBATIM + COMMAND_EXPAND_LISTS + + COMMAND ${QT_LUPDATE_EXECUTABLE} -locations none ${CMAKE_SOURCE_DIR}/apps/launcher -ts ${LAUNCHER_TS_FILES} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/apps/launcher + VERBATIM + COMMAND_EXPAND_LISTS) + + if (BUILD_LAUNCHER OR BUILD_WIZARD) + if (APPLE) + set(QT_OPENMW_TRANSLATIONS_PATH "${APP_BUNDLE_DIR}/Contents/Resources/resources/translations") + else () + get_generator_is_multi_config(multi_config) + if (multi_config) + set(QT_OPENMW_TRANSLATIONS_PATH "${OpenMW_BINARY_DIR}/$/resources/translations") + else () + set(QT_OPENMW_TRANSLATIONS_PATH "${OpenMW_BINARY_DIR}/resources/translations") + endif () + endif () + + file(GLOB TS_FILES ${COMPONENTS_TS_FILES}) + + if (BUILD_LAUNCHER) + set(TS_FILES ${TS_FILES} ${LAUNCHER_TS_FILES}) + endif () + + if (BUILD_WIZARD) + set(TS_FILES ${TS_FILES} ${WIZARD_TS_FILES}) + endif () + + qt_add_translation(QM_FILES ${TS_FILES} OPTIONS -silent) + + if (DEPLOY_QT_TRANSLATIONS) + # Once we set a Qt 6.2.0 as a minimum required version, we may use "qtpaths --qt-query" instead. + get_target_property(QT_QMAKE_EXECUTABLE Qt::qmake IMPORTED_LOCATION) + execute_process(COMMAND "${QT_QMAKE_EXECUTABLE}" -query QT_INSTALL_TRANSLATIONS + OUTPUT_VARIABLE QT_TRANSLATIONS_DIR OUTPUT_STRIP_TRAILING_WHITESPACE) + + foreach(QM_FILE ${QM_FILES}) + get_filename_component(QM_BASENAME ${QM_FILE} NAME) + string(REGEX REPLACE "[^_]+_(.*)\\.qm" "\\1" LANG_NAME ${QM_BASENAME}) + if (EXISTS "${QT_TRANSLATIONS_DIR}/qtbase_${LANG_NAME}.qm") + set(QM_FILES ${QM_FILES} "${QT_TRANSLATIONS_DIR}/qtbase_${LANG_NAME}.qm") + elseif (EXISTS "${QT_TRANSLATIONS_DIR}/qt_${LANG_NAME}.qm") + set(QM_FILES ${QM_FILES} "${QT_TRANSLATIONS_DIR}/qt_${LANG_NAME}.qm") + else () + message(FATAL_ERROR "Qt translations for '${LANG_NAME}' locale are not found in the '${QT_TRANSLATIONS_DIR}' folder.") + endif () + endforeach(QM_FILE) + + list(REMOVE_DUPLICATES QM_FILES) + endif () + + add_custom_target(qm-files + COMMAND ${CMAKE_COMMAND} -E make_directory ${QT_OPENMW_TRANSLATIONS_PATH} + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${QM_FILES} ${QT_OPENMW_TRANSLATIONS_PATH} + DEPENDS ${QM_FILES} + COMMENT "Copy *.qm files to resources folder") + endif () +endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 264db49cc19..8aeb455543c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,37 +8,37 @@ https://gitlab.com/OpenMW/openmw/issues Currently, we are focused on completing the MW game experience and general polishing. Features out of this scope may be approved in some cases, but you should probably start a discussion first. Note: -* Tasks set to 'openmw-future' are usually out of the current scope of the project and can't be started yet. -* Bugs that are not 'Confirmed' should be confirmed first. +* Issues that have the 'Future' label are usually out of the current scope of the project. Corresponding submissions are unlikely to be merged or even properly reviewed. +* Newly reported bugs should be attempted to be reproduced on the latest code and on the latest available stable release. Both can be found [here](https://openmw.org/downloads/). * Often, it's best to start a discussion about possible solutions before you jump into coding, especially for larger features. -Aside from coding, you can also help by triaging the issues list. Check for bugs that are 'Unconfirmed' and try to confirm them on your end, working out any details that may be necessary. Check for bugs that do not conform to [Bug reporting guidelines](https://wiki.openmw.org/index.php?title=Bug_Reporting_Guidelines) and improve them to do so! +Aside from coding, you can also help by triaging the issues list. Check for unconfirmed bugs and try to reproduce them on your end, working out any details that may be necessary. Check for bugs that do not conform to [Bug Reporting Guidelines](https://wiki.openmw.org/index.php?title=Bug_Reporting_Guidelines) and improve them to do so! There are various [Tools](https://wiki.openmw.org/index.php?title=Tools) to facilitate testing/development. -Pull Request Guidelines +Merge request guidelines ======================= -To facilitate the review process, your pull request description should include the following, if applicable: +To facilitate the review process, your merge request description should include the following, if applicable: -* A link back to the bug report or forum discussion that prompted the change. Note: when linking bugs, use the syntax ```[Bug #xyz](https://gitlab.com/OpenMW/openmw/issues/#xyz)``` to create a clickable link. Writing only 'Bug #xyz' will unfortunately create a link to the Github pull request with that number instead. -* Summary of the changes made -* Reasoning / motivation behind the change -* What testing you have carried out to verify the change +* A link back to the bug report or discussion that prompted the change. +* Summary of the changes made. +* Reasoning / motivation behind the change. +* What testing you have carried out to verify the change. Furthermore, we advise to: -* Avoid stuffing unrelated commits into one pull request. As a rule of thumb, each feature and each bugfix should go into a separate PR, unless they are closely related or dependent upon each other. Small pull requests are easier to review, and are less likely to require further changes before we can merge them. A "mega" pull request with lots of unrelated commits in it is likely to get held up in review for a long time. -* Feel free to submit incomplete pull requests. Even if the work can not be merged yet, pull requests are a great place to collect early feedback. Just make sure to mark it as *[Incomplete]* or *[Do not merge yet]* in the title. +* Avoid stuffing unrelated commits into one merge request. As a rule of thumb, each feature and each bugfix should go into a separate MR, unless they are closely related or dependent upon each other. Small merge requests are easier to review and are less likely to require further changes before we can merge them. A "mega" merge request with lots of unrelated commits in it is likely to get held up in review for a long time. +* Feel free to submit incomplete merge requests. Even if the work cannot be merged yet, merge requests are a great place to collect early feedback. Just make sure to mark it as [draft](https://docs.gitlab.com/ee/user/project/merge_requests/drafts/). * If you plan on contributing often, please read the [Developer Reference](https://wiki.openmw.org/index.php?title=Developer_Reference) on our wiki, especially the [Policies and Standards](https://wiki.openmw.org/index.php?title=Policies_and_Standards). * Make sure each of your changes has a clear objective. Unnecessary changes may lead to merge conflicts, clutter the commit history and slow down review. Code formatting 'fixes' should be avoided, unless you were already changing that particular line anyway. -* Reference the bug / feature ticket(s) in your commit message (e.g. 'Bug #123') to make it easier to keep track of what we changed for what reason. Our bugtracker will show those commits next to the ticket. If your commit message includes 'Fixes #123', that bug/feature will automatically be set to 'Closed' when your commit is merged. -* When pulling changes from master, prefer rebase over merge. Consider using a merge if there are conflicts or for long-running PRs. +* Reference the bug / feature ticket(s) in your commit message or merge request description (e.g. 'Bug #123') to make it easier to keep track of what we changed for what reason. Our bugtracker will show those commits next to the ticket. If your merge request's description includes 'Fixes #123', that issue will automatically be closed when your commit is merged. +* When pulling changes from master, prefer rebase over merge. Consider using a merge if there are conflicts or for long-running MRs. Guidelines for original engine "fixes" ================================= -From time to time you may be tempted to "fix" what you think was a "bug" in the original game engine. +From time to time, you may be tempted to "fix" what you think was a "bug" in the original game engine. Unfortunately, the definition of what is a "bug" is not so clear. Consider that your "bug" is actually a feature unless proven otherwise: @@ -51,57 +51,63 @@ OpenMW, in its default configuration, is meant to be a faithful reimplementation That said, we may sometimes evaluate such issues on an individual basis. Common exceptions to the above would be: -* Issues so glaring that they would severely limit the capabilities of the engine in the future (for example, the scripting engine not being allowed to access objects in remote cells) -* Bugs where the intent is very obvious, and that have little to no balancing impact (e.g. the bug were being tired made it easier to repair items, instead of harder) -* Bugs that were fixed in an official patch for Morrowind +* Issues so glaring that they would severely limit the capabilities of the engine in the future (for example, the scripting engine not being allowed to access objects in remote cells). +* Bugs where the intent is very obvious, and that have little to no balancing impact (e.g. the bug where being tired made it easier to repair items, instead of harder). +* Bugs that were fixed in an official patch for Morrowind. Feature additions policy ===================== -We get it, you have waited so long for feature XYZ to be available in Morrowind and now that OpenMW is here you can not wait to implement your ingenious idea and share it with the world. +We get it: you have waited so long for feature XYZ to be available in Morrowind, and now that OpenMW is here, you cannot wait to implement your ingenious idea and share it with the world. Unfortunately, since maintaining features comes at a cost and our resources are limited, we have to be a little selective in what features we allow into the main repository. Generally: * Features should be as generic and non-redundant as possible. * Any feature that is also possible with modding should be done as a mod instead. -* In the future, OpenMW plans to expand the scope of what is possible with modding, e.g. by moving certain game logic into editable scripts. -* Currently, modders can edit OpenMW's GUI skins and layout XML files, although there are still a few missing hooks (e.g. scripting support) in order to make this into a powerful way of modding. -* If a feature introduces new game UI strings, that reduces its chance of being accepted because we do not currently have any way of localizing these to the user's Morrowind installation language. +* Through moving certain game logic into built-in scripting, OpenMW will expand the scope of what is possible with modding. +* Modders can edit OpenMW's GUI skins and layout XML files as well as create new widgets through the Lua API, but it is expected that existing C++ widgets will also be recreated through built-in scripting. +* If a feature introduces new game UI strings, you will need to become acquainted with OpenMW's YAML localisation system and expose them. Read about it [here](https://openmw.readthedocs.io/en/latest/reference/modding/localisation.html). If you are in doubt of your feature being within our scope, it is probably best to start a forum discussion first. See the [settings documentation](https://openmw.readthedocs.io/en/stable/reference/modding/settings/index.html) and [Features list](https://wiki.openmw.org/index.php?title=Features) for some examples of features that were deemed acceptable. -Reviewing pull requests +Reviewing merge requests ======================= -We welcome any help in reviewing open PRs. You don't need to be a developer to comment on new features. We also encourage ["junior" developers to review senior's work](https://pagefault.blog/2018/04/08/why-junior-devs-should-review-seniors-commits/). +We welcome any help in reviewing open MRs. You don't need to be a developer to comment on new features. We also encourage ["junior" developers to review senior's work](https://pagefault.blog/2018/04/08/why-junior-devs-should-review-seniors-commits/). -This review process is divided into two sections because complaining about code or style issues hardly makes sense until the functionality of the PR is deemed OK. Anyone can help with the Functionality Review while most parts of the Code Review require you to have programming experience. +This review process is divided into two sections because complaining about code or style issues hardly makes sense until the functionality of the MR is deemed OK. Anyone can help with the **functionality review** while most parts of the **code review** require you to have programming experience. -In addition to the checklist below, make sure to check that the Pull Request Guidelines (first half of this document) were followed. +In addition to the checklist below, make sure to check that the **merge request guidelines** (first half of this document) were followed. -First review +Functionality review ============ 1. Ask for missing information or clarifications. Compare against the project's design goals and roadmap. -2. Check if the automated tests are passing. If they are not, make the PR author aware of the issue and potentially link to the error line on Travis CI or Appveyor. If the error appears unrelated to the PR and/or the master branch is failing with the same error, our CI has broken and needs to be fixed independently of any open PRs. Raise this issue on the forums, bug tracker or with the relevant maintainer. The PR can be merged in this case as long as you've built it yourself to make sure it does build. -3. Make sure that the new code has been tested thoroughly, either by asking the author or, preferably, testing yourself. In a complex project like OpenMW, it is easy to make mistakes, typos, etc. Therefore, prefer testing all code changes, no matter how trivial they look. When you have tested a PR that no one has tested so far, post a comment letting us know. -4. On long running PRs, request the author to update its description with the current state or a checklist of things left to do. +2. Check if the automated tests are passing. If they are not, make the MR author aware of the issue and potentially quote the error line. If the error appears unrelated to the MR and/or the master branch is failing with the same error, our CI might be broken and needs to be fixed independently of any open MRs. Raise this issue on one of the following resources: + * Our [forums](https://forum.openmw.org/) + * [Discord](https://discord.com/servers/openmw-260439894298460160) + * [IRC](https://web.libera.chat/#openmw) + * [Issue tracker](https://gitlab.com/OpenMW/openmw/-/issues) -Code Review +3. Make sure that the new code has been tested thoroughly, either by asking the author or, preferably, testing yourself. In a complex project like OpenMW, it is easy to make mistakes, typos, etc. Therefore, prefer testing all code changes, no matter how trivial they look. When you have tested a MR that no one has tested so far, post a comment letting us know. +4. On long-running MRs, request the author to update its description with the current state or a checklist of things left to do. + +Code review =========== 1. Carefully review each line for issues the author may not have thought of, paying special attention to 'special' cases. Often, people build their code with a particular mindset and forget about other configurations or unexpected interactions. 2. If any changes are workarounds for an issue in an upstream library, make sure the issue was reported upstream so we can eventually drop the workaround when the issue is fixed and the new version of that library is a build dependency. -3. Make sure PRs do not turn into arguments about hardly related issues. If the PR author disagrees with an established part of the project (e.g. supported build environments), they should open a forum discussion or bug report and in the meantime adjust the PR to adhere to the established way, rather than leaving the PR hanging on a dispute. +3. Make sure MRs do not turn into arguments about hardly related issues. If the MR author disagrees with an established part of the project (e.g. supported build environments), they should open a forum discussion or bug report and in the meantime adjust the MR to adhere to the established way, rather than leaving the MR hanging on a dispute. 4. Check if the code matches our style guidelines. 5. Check to make sure the commit history is clean. Squashing should be considered if the review process has made the commit history particularly long. Commits that don't build should be avoided because they are a nuisance for ```git bisect```. Merging ======= -To be able to merge PRs, commit priviledges are required. If you do not have the priviledges, just ping someone that does have them with a short comment like "Looks good to me @user". +To be able to merge MRs, commit privileges are required. If you do not have the privileges, just ping someone that does have them with a short comment like "Looks good to me @user". -The person to merge the PR may either use github's Merge button or if using the command line, use the ```--no-ff``` flag (so a merge commit is created, just like with Github's merge button) and include the pull request number in the commit description. +In general case, you should not merge MRs prematurely even if you are sure they just work or if they receive a senior member's approval. +The rule of thumb is to give at least 24 hours to a couple days of a window for reviews to come through. For more technically involved MRs, 24 hours might not be enough. Dealing with regressions ======================== diff --git a/README.md b/README.md index 54fd59f0044..67ba2ce0037 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,26 @@ OpenMW ====== -[![Build Status](https://api.travis-ci.org/OpenMW/openmw.svg)](https://travis-ci.org/OpenMW/openmw) [![Build status](https://ci.appveyor.com/api/projects/status/github/openmw/openmw?svg=true)](https://ci.appveyor.com/project/psi29a/openmw) [![Coverity Scan Build Status](https://scan.coverity.com/projects/3740/badge.svg)](https://scan.coverity.com/projects/3740) [![pipeline status](https://gitlab.com/OpenMW/openmw/badges/master/pipeline.svg)](https://gitlab.com/OpenMW/openmw/commits/master) - -OpenMW is an open-source game engine that supports playing Morrowind by Bethesda Softworks. You need to own the game for OpenMW to play Morrowind. +OpenMW is an open-source open-world RPG game engine that supports playing Morrowind by Bethesda Softworks. You need to own the game for OpenMW to play Morrowind. OpenMW also comes with OpenMW-CS, a replacement for Bethesda's Construction Set. -* Version: 0.47.0 -* License: GPLv3 (see [LICENSE](https://github.com/OpenMW/openmw/blob/master/LICENSE) for more information) +* Version: 0.49.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 +* Discord: https://discord.gg/bWuqq2e + Font Licenses: -* DejaVuLGCSansMono.ttf: custom (see [files/mygui/DejaVuFontLicense.txt](https://github.com/OpenMW/openmw/blob/master/files/mygui/DejaVuFontLicense.txt) for more information) +* DejaVuLGCSansMono.ttf: custom (see [files/data/fonts/DejaVuFontLicense.txt](https://gitlab.com/OpenMW/openmw/-/raw/master/files/data/fonts/DejaVuFontLicense.txt) for more information) +* DemonicLetters.ttf: SIL Open Font License (see [files/data/fonts/DemonicLettersFontLicense.txt](https://gitlab.com/OpenMW/openmw/-/raw/master/files/data/fonts/DemonicLettersFontLicense.txt) for more information) +* MysticCards.ttf: SIL Open Font License (see [files/data/fonts/MysticCardsFontLicense.txt](https://gitlab.com/OpenMW/openmw/-/raw/master/files/data/fonts/MysticCardsFontLicense.txt) for more information) Current Status -------------- -The main quests in Morrowind, Tribunal and Bloodmoon are all completable. Some issues with side quests are to be expected (but rare). Check the [bug tracker](https://gitlab.com/OpenMW/openmw/issues?label_name%5B%5D=1.0) for a list of issues we need to resolve before the "1.0" release. Even before the "1.0" release however, OpenMW boasts some new [features](https://wiki.openmw.org/index.php?title=Features), such as improved graphics and user interfaces. +The main quests in Morrowind, Tribunal and Bloodmoon are all completable. Some issues with side quests are to be expected (but rare). Check the [bug tracker](https://gitlab.com/OpenMW/openmw/-/issues/?milestone_title=openmw-1.0) for a list of issues we need to resolve before the "1.0" release. Even before the "1.0" release however, OpenMW boasts some new [features](https://wiki.openmw.org/index.php?title=Features), such as improved graphics and user interfaces. Pre-existing modifications created for the original Morrowind engine can be hit-and-miss. The OpenMW script compiler performs more thorough error-checking than Morrowind does, meaning that a mod created for Morrowind may not necessarily run in OpenMW. Some mods also rely on quirky behaviour or engine bugs in order to work. We are considering such compatibility issues on a case-by-case basis - in some cases adding a workaround to OpenMW may be feasible, in other cases fixing the mod will be the only option. If you know of any mods that work or don't work, feel free to add them to the [Mod status](https://wiki.openmw.org/index.php?title=Mod_status) wiki page. @@ -26,7 +28,7 @@ Getting Started --------------- * [Official forums](https://forum.openmw.org/) -* [Installation instructions](https://wiki.openmw.org/index.php?title=Installation_Instructions) +* [Installation instructions](https://openmw.readthedocs.io/en/latest/manuals/installation/index.html) * [Build from source](https://wiki.openmw.org/index.php?title=Development_Environment_Setup) * [Testing the game](https://wiki.openmw.org/index.php?title=Testing) * [How to contribute](https://wiki.openmw.org/index.php?title=Contribution_Wanted) @@ -83,8 +85,6 @@ Command line options --skip-menu [=arg(=1)] (=0) skip main menu on game startup --new-game [=arg(=1)] (=0) run new game sequence (ignored if skip-menu=0) - --fs-strict [=arg(=1)] (=0) strict file system handling (no case - folding) --encoding arg (=win1252) Character encoding used in OpenMW game messages: diff --git a/apps/benchmarks/CMakeLists.txt b/apps/benchmarks/CMakeLists.txt index f9aa9aad49f..8039f2a3d26 100644 --- a/apps/benchmarks/CMakeLists.txt +++ b/apps/benchmarks/CMakeLists.txt @@ -1,27 +1,7 @@ -cmake_minimum_required(VERSION 3.11) - -set(BENCHMARK_ENABLE_TESTING OFF) -set(BENCHMARK_ENABLE_INSTALL OFF) -set(BENCHMARK_ENABLE_GTEST_TESTS OFF) - -set(SAVED_CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") - -string(REPLACE "-Wsuggest-override" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") -string(REPLACE "-Wundef" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") -include(FetchContent) -FetchContent_Declare(benchmark - URL https://github.com/google/benchmark/archive/refs/tags/v1.5.2.zip - URL_HASH MD5=49395b757a7c4656d70f1328d93efd00 - SOURCE_DIR fetched/benchmark -) -FetchContent_MakeAvailableExcludeFromAll(benchmark) - -set(CMAKE_CXX_FLAGS "${SAVED_CMAKE_CXX_FLAGS}") - -openmw_add_executable(openmw_detournavigator_navmeshtilescache_benchmark detournavigator/navmeshtilescache.cpp) -target_compile_features(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE cxx_std_17) -target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark benchmark::benchmark components) - -if (UNIX AND NOT APPLE) - target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark ${CMAKE_THREAD_LIBS_INIT}) +if(OPENMW_USE_SYSTEM_BENCHMARK) + find_package(benchmark REQUIRED) endif() + +add_subdirectory(detournavigator) +add_subdirectory(esm) +add_subdirectory(settings) diff --git a/apps/benchmarks/detournavigator/CMakeLists.txt b/apps/benchmarks/detournavigator/CMakeLists.txt new file mode 100644 index 00000000000..ffe7818a5a6 --- /dev/null +++ b/apps/benchmarks/detournavigator/CMakeLists.txt @@ -0,0 +1,15 @@ +openmw_add_executable(openmw_detournavigator_navmeshtilescache_benchmark navmeshtilescache.cpp) +target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark benchmark::benchmark components) + +if (UNIX AND NOT APPLE) + target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark ${CMAKE_THREAD_LIBS_INIT}) +endif() + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE ) +endif() + +if (BUILD_WITH_CODE_COVERAGE) + target_compile_options(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE --coverage) + target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark gcov) +endif() diff --git a/apps/benchmarks/detournavigator/navmeshtilescache.cpp b/apps/benchmarks/detournavigator/navmeshtilescache.cpp index 2c7a981ad5e..26873d9a039 100644 --- a/apps/benchmarks/detournavigator/navmeshtilescache.cpp +++ b/apps/benchmarks/detournavigator/navmeshtilescache.cpp @@ -1,10 +1,12 @@ #include #include +#include +#include #include +#include #include -#include namespace { @@ -12,41 +14,36 @@ namespace struct Key { - osg::Vec3f mAgentHalfExtents; + AgentBounds mAgentBounds; TilePosition mTilePosition; RecastMesh mRecastMesh; - std::vector mOffMeshConnections; }; struct Item { Key mKey; - NavMeshData mValue; + PreparedNavMeshData mValue; }; - template - TilePosition generateTilePosition(int max, Random& random) + osg::Vec2i generateVec2i(int max, auto& random) { std::uniform_int_distribution distribution(0, max); - return TilePosition(distribution(random), distribution(random)); + return osg::Vec2i(distribution(random), distribution(random)); } - template - osg::Vec3f generateAgentHalfExtents(float min, float max, Random& random) + osg::Vec3f generateAgentHalfExtents(float min, float max, auto& random) { std::uniform_int_distribution distribution(min, max); return osg::Vec3f(distribution(random), distribution(random), distribution(random)); } - template - void generateVertices(OutputIterator out, std::size_t number, Random& random) + void generateVertices(std::output_iterator auto out, std::size_t number, auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, 3 * (number - number % 3), [&] { return distribution(random); }); } - template - void generateIndices(OutputIterator out, int max, std::size_t number, Random& random) + void generateIndices(std::output_iterator auto out, int max, std::size_t number, auto& random) { std::uniform_int_distribution distribution(0, max); std::generate_n(out, number - number % 3, [&] { return distribution(random); }); @@ -56,88 +53,114 @@ namespace { switch (index) { - case 0: return AreaType_null; - case 1: return AreaType_water; - case 2: return AreaType_door; - case 3: return AreaType_pathgrid; - case 4: return AreaType_ground; + case 0: + return AreaType_null; + case 1: + return AreaType_water; + case 2: + return AreaType_door; + case 3: + return AreaType_pathgrid; + case 4: + return AreaType_ground; } return AreaType_null; } - template - AreaType generateAreaType(Random& random) + AreaType generateAreaType(auto& random) { std::uniform_int_distribution distribution(0, 4); - return toAreaType(distribution(random));; + return toAreaType(distribution(random)); } - template - void generateAreaTypes(OutputIterator out, std::size_t triangles, Random& random) + void generateAreaTypes(std::output_iterator auto out, std::size_t triangles, auto& random) { std::generate_n(out, triangles, [&] { return generateAreaType(random); }); } - template - void generateWater(OutputIterator out, std::size_t count, Random& random) + void generateWater(std::output_iterator auto out, std::size_t count, auto& random) { - std::uniform_real_distribution distribution(0.0, 1.0); + std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, count, [&] { - const btVector3 shift(distribution(random), distribution(random), distribution(random)); - return RecastMesh::Water {1, btTransform(btMatrix3x3::getIdentity(), shift)}; + return CellWater{ generateVec2i(1000, random), Water{ ESM::Land::REAL_SIZE, distribution(random) } }; }); } - template - void generateOffMeshConnection(OutputIterator out, std::size_t count, Random& random) + Mesh generateMesh(std::size_t triangles, auto& random) { - std::uniform_real_distribution distribution(0.0, 1.0); - std::generate_n(out, count, [&] { - const osg::Vec3f start(distribution(random), distribution(random), distribution(random)); - const osg::Vec3f end(distribution(random), distribution(random), distribution(random)); - return OffMeshConnection {start, end, generateAreaType(random)}; - }); + std::uniform_real_distribution distribution(0.0, 1.0); + std::vector vertices; + std::vector indices; + std::vector areaTypes; + if (distribution(random) < 0.939) + { + generateVertices(std::back_inserter(vertices), triangles * 2.467, random); + generateIndices(std::back_inserter(indices), static_cast(vertices.size() / 3) - 1, + vertices.size() * 1.279, random); + generateAreaTypes(std::back_inserter(areaTypes), indices.size() / 3, random); + } + return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes)); + } + + Heightfield generateHeightfield(auto& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + Heightfield result; + result.mCellPosition = generateVec2i(1000, random); + result.mCellSize = ESM::Land::REAL_SIZE; + result.mMinHeight = distribution(random); + result.mMaxHeight = result.mMinHeight + 1.0; + result.mLength = static_cast(ESM::Land::LAND_SIZE); + std::generate_n( + std::back_inserter(result.mHeights), ESM::Land::LAND_NUM_VERTS, [&] { return distribution(random); }); + result.mOriginalSize = ESM::Land::LAND_SIZE; + result.mMinX = 0; + result.mMinY = 0; + return result; + } + + FlatHeightfield generateFlatHeightfield(auto& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + FlatHeightfield result; + result.mCellPosition = generateVec2i(1000, random); + result.mCellSize = ESM::Land::REAL_SIZE; + result.mHeight = distribution(random); + return result; } - template - Key generateKey(std::size_t triangles, Random& random) + Key generateKey(std::size_t triangles, auto& random) { + const CollisionShapeType agentShapeType = CollisionShapeType::Aabb; const osg::Vec3f agentHalfExtents = generateAgentHalfExtents(0.5, 1.5, random); - const TilePosition tilePosition = generateTilePosition(10000, random); - const std::size_t generation = std::uniform_int_distribution(0, 100)(random); - const std::size_t revision = std::uniform_int_distribution(0, 10000)(random); - std::vector vertices; - generateVertices(std::back_inserter(vertices), triangles * 1.98, random); - std::vector indices; - generateIndices(std::back_inserter(indices), static_cast(vertices.size() / 3) - 1, vertices.size() * 1.53, random); - std::vector areaTypes; - generateAreaTypes(std::back_inserter(areaTypes), indices.size() / 3, random); - std::vector water; - generateWater(std::back_inserter(water), 2, random); - RecastMesh recastMesh(generation, revision, std::move(indices), std::move(vertices), - std::move(areaTypes), std::move(water)); - std::vector offMeshConnections; - generateOffMeshConnection(std::back_inserter(offMeshConnections), 300, random); - return Key {agentHalfExtents, tilePosition, std::move(recastMesh), std::move(offMeshConnections)}; + const TilePosition tilePosition = generateVec2i(10000, random); + const Version version{ + .mGeneration = std::uniform_int_distribution(0, 100)(random), + .mRevision = std::uniform_int_distribution(0, 10000)(random), + }; + Mesh mesh = generateMesh(triangles, random); + std::vector water; + generateWater(std::back_inserter(water), 1, random); + RecastMesh recastMesh(version, std::move(mesh), std::move(water), { generateHeightfield(random) }, + { generateFlatHeightfield(random) }, {}); + return Key{ AgentBounds{ agentShapeType, agentHalfExtents }, tilePosition, std::move(recastMesh) }; } - constexpr std::size_t trianglesPerTile = 310; + constexpr std::size_t trianglesPerTile = 239; - template - void generateKeys(OutputIterator out, std::size_t count, Random& random) + void generateKeys(std::output_iterator auto out, std::size_t count, auto& random) { std::generate_n(out, count, [&] { return generateKey(trianglesPerTile, random); }); } - template - void fillCache(OutputIterator out, Random& random, NavMeshTilesCache& cache) + void fillCache(std::output_iterator auto out, auto& random, NavMeshTilesCache& cache) { std::size_t size = cache.getStats().mNavMeshCacheSize; while (true) { Key key = generateKey(trianglesPerTile, random); - cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections, NavMeshData()); + cache.set(key.mAgentBounds, key.mTilePosition, key.mRecastMesh, std::make_unique()); *out++ = std::move(key); const std::size_t newSize = cache.getStats().mNavMeshCacheSize; if (size >= newSize) @@ -156,22 +179,53 @@ namespace generateKeys(std::back_inserter(keys), keys.size() * (100 - hitPercentage) / 100, random); std::size_t n = 0; - while (state.KeepRunning()) + for (auto _ : state) { const auto& key = keys[n++ % keys.size()]; - const auto result = cache.get(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections); + auto result = cache.get(key.mAgentBounds, key.mTilePosition, key.mRecastMesh); benchmark::DoNotOptimize(result); } } - constexpr auto getFromFilledCache_1m_100hit = getFromFilledCache<1 * 1024 * 1024, 100>; - constexpr auto getFromFilledCache_4m_100hit = getFromFilledCache<4 * 1024 * 1024, 100>; - constexpr auto getFromFilledCache_16m_100hit = getFromFilledCache<16 * 1024 * 1024, 100>; - constexpr auto getFromFilledCache_64m_100hit = getFromFilledCache<64 * 1024 * 1024, 100>; - constexpr auto getFromFilledCache_1m_70hit = getFromFilledCache<1 * 1024 * 1024, 70>; - constexpr auto getFromFilledCache_4m_70hit = getFromFilledCache<4 * 1024 * 1024, 70>; - constexpr auto getFromFilledCache_16m_70hit = getFromFilledCache<16 * 1024 * 1024, 70>; - constexpr auto getFromFilledCache_64m_70hit = getFromFilledCache<64 * 1024 * 1024, 70>; + void getFromFilledCache_1m_100hit(benchmark::State& state) + { + getFromFilledCache<1 * 1024 * 1024, 100>(state); + } + + void getFromFilledCache_4m_100hit(benchmark::State& state) + { + getFromFilledCache<4 * 1024 * 1024, 100>(state); + } + + void getFromFilledCache_16m_100hit(benchmark::State& state) + { + getFromFilledCache<16 * 1024 * 1024, 100>(state); + } + + void getFromFilledCache_64m_100hit(benchmark::State& state) + { + getFromFilledCache<64 * 1024 * 1024, 100>(state); + } + + void getFromFilledCache_1m_70hit(benchmark::State& state) + { + getFromFilledCache<1 * 1024 * 1024, 70>(state); + } + + void getFromFilledCache_4m_70hit(benchmark::State& state) + { + getFromFilledCache<4 * 1024 * 1024, 70>(state); + } + + void getFromFilledCache_16m_70hit(benchmark::State& state) + { + getFromFilledCache<16 * 1024 * 1024, 70>(state); + } + + void getFromFilledCache_64m_70hit(benchmark::State& state) + { + getFromFilledCache<64 * 1024 * 1024, 70>(state); + } template void setToBoundedNonEmptyCache(benchmark::State& state) @@ -187,15 +241,31 @@ namespace while (state.KeepRunning()) { const auto& key = keys[n++ % keys.size()]; - const auto result = cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections, NavMeshData()); + auto result = cache.set( + key.mAgentBounds, key.mTilePosition, key.mRecastMesh, std::make_unique()); benchmark::DoNotOptimize(result); } } - constexpr auto setToBoundedNonEmptyCache_1m = setToBoundedNonEmptyCache<1 * 1024 * 1024>; - constexpr auto setToBoundedNonEmptyCache_4m = setToBoundedNonEmptyCache<4 * 1024 * 1024>; - constexpr auto setToBoundedNonEmptyCache_16m = setToBoundedNonEmptyCache<16 * 1024 * 1024>; - constexpr auto setToBoundedNonEmptyCache_64m = setToBoundedNonEmptyCache<64 * 1024 * 1024>; + void setToBoundedNonEmptyCache_1m(benchmark::State& state) + { + setToBoundedNonEmptyCache<1 * 1024 * 1024>(state); + } + + void setToBoundedNonEmptyCache_4m(benchmark::State& state) + { + setToBoundedNonEmptyCache<4 * 1024 * 1024>(state); + } + + void setToBoundedNonEmptyCache_16m(benchmark::State& state) + { + setToBoundedNonEmptyCache<16 * 1024 * 1024>(state); + } + + void setToBoundedNonEmptyCache_64m(benchmark::State& state) + { + setToBoundedNonEmptyCache<64 * 1024 * 1024>(state); + } } // namespace BENCHMARK(getFromFilledCache_1m_100hit); diff --git a/apps/benchmarks/esm/CMakeLists.txt b/apps/benchmarks/esm/CMakeLists.txt new file mode 100644 index 00000000000..9b5afd649da --- /dev/null +++ b/apps/benchmarks/esm/CMakeLists.txt @@ -0,0 +1,15 @@ +openmw_add_executable(openmw_esm_refid_benchmark benchrefid.cpp) +target_link_libraries(openmw_esm_refid_benchmark benchmark::benchmark components) + +if (UNIX AND NOT APPLE) + target_link_libraries(openmw_esm_refid_benchmark ${CMAKE_THREAD_LIBS_INIT}) +endif() + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw_esm_refid_benchmark PRIVATE ) +endif() + +if (BUILD_WITH_CODE_COVERAGE) + target_compile_options(openmw_esm_refid_benchmark PRIVATE --coverage) + target_link_libraries(openmw_esm_refid_benchmark gcov) +endif() diff --git a/apps/benchmarks/esm/benchrefid.cpp b/apps/benchmarks/esm/benchrefid.cpp new file mode 100644 index 00000000000..b12f494ab9e --- /dev/null +++ b/apps/benchmarks/esm/benchrefid.cpp @@ -0,0 +1,249 @@ +#include + +#include "components/esm/refid.hpp" + +#include +#include +#include +#include +#include + +namespace +{ + constexpr std::size_t refIdsCount = 64 * 1024; + + template + std::string generateText(std::size_t size, Random& random) + { + std::uniform_int_distribution distribution('A', 'z'); + std::string result; + result.reserve(size); + std::generate_n(std::back_inserter(result), size, [&] { return distribution(random); }); + return result; + } + + template + std::vector generateStringRefIds(std::size_t size, Random& random) + { + std::vector result; + result.reserve(refIdsCount); + std::generate_n( + std::back_inserter(result), refIdsCount, [&] { return ESM::StringRefId(generateText(size, random)); }); + return result; + } + + template + std::vector generateSerializedRefIds(const std::vector& generated, Serialize&& serialize) + { + std::vector result; + result.reserve(generated.size()); + for (ESM::RefId refId : generated) + result.push_back(serialize(refId)); + return result; + } + + template + std::vector generateSerializedStringRefIds(std::size_t size, Random& random, Serialize&& serialize) + { + return generateSerializedRefIds(generateStringRefIds(size, random), serialize); + } + + template + std::vector generateIndexRefIds(Random& random) + { + std::vector result; + result.reserve(refIdsCount); + std::uniform_int_distribution distribution(0, std::numeric_limits::max()); + std::generate_n(std::back_inserter(result), refIdsCount, + [&] { return ESM::IndexRefId(ESM::REC_ARMO, distribution(random)); }); + return result; + } + + template + std::vector generateSerializedIndexRefIds(Random& random, Serialize&& serialize) + { + return generateSerializedRefIds(generateIndexRefIds(random), serialize); + } + + template + std::vector generateGeneratedRefIds(Random& random) + { + std::vector result; + result.reserve(refIdsCount); + std::uniform_int_distribution distribution(0, std::numeric_limits::max()); + std::generate_n( + std::back_inserter(result), refIdsCount, [&] { return ESM::GeneratedRefId(distribution(random)); }); + return result; + } + + template + std::vector generateSerializedGeneratedRefIds(Random& random, Serialize&& serialize) + { + return generateSerializedRefIds(generateGeneratedRefIds(random), serialize); + } + + template + std::vector generateESM3ExteriorCellRefIds(Random& random) + { + std::vector result; + result.reserve(refIdsCount); + std::uniform_int_distribution distribution(-100, 100); + std::generate_n(std::back_inserter(result), refIdsCount, + [&] { return ESM::ESM3ExteriorCellRefId(distribution(random), distribution(random)); }); + return result; + } + + template + std::vector generateSerializedESM3ExteriorCellRefIds(Random& random, Serialize&& serialize) + { + return generateSerializedRefIds(generateESM3ExteriorCellRefIds(random), serialize); + } + + void serializeRefId(benchmark::State& state) + { + std::minstd_rand random; + std::vector refIds = generateStringRefIds(state.range(0), random); + std::size_t i = 0; + for (auto _ : state) + { + benchmark::DoNotOptimize(refIds[i].serialize()); + if (++i >= refIds.size()) + i = 0; + } + } + + void deserializeRefId(benchmark::State& state) + { + std::minstd_rand random; + std::vector serializedRefIds + = generateSerializedStringRefIds(state.range(0), random, [](ESM::RefId v) { return v.serialize(); }); + std::size_t i = 0; + for (auto _ : state) + { + benchmark::DoNotOptimize(ESM::RefId::deserialize(serializedRefIds[i])); + if (++i >= serializedRefIds.size()) + i = 0; + } + } + + void serializeTextStringRefId(benchmark::State& state) + { + std::minstd_rand random; + std::vector refIds = generateStringRefIds(state.range(0), random); + std::size_t i = 0; + for (auto _ : state) + { + benchmark::DoNotOptimize(refIds[i].serializeText()); + if (++i >= refIds.size()) + i = 0; + } + } + + void deserializeTextStringRefId(benchmark::State& state) + { + std::minstd_rand random; + std::vector serializedRefIds + = generateSerializedStringRefIds(state.range(0), random, [](ESM::RefId v) { return v.serializeText(); }); + std::size_t i = 0; + for (auto _ : state) + { + benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i])); + if (++i >= serializedRefIds.size()) + i = 0; + } + } + + void serializeTextGeneratedRefId(benchmark::State& state) + { + std::minstd_rand random; + std::vector refIds = generateGeneratedRefIds(random); + std::size_t i = 0; + for (auto _ : state) + { + benchmark::DoNotOptimize(refIds[i].serializeText()); + if (++i >= refIds.size()) + i = 0; + } + } + + void deserializeTextGeneratedRefId(benchmark::State& state) + { + std::minstd_rand random; + std::vector serializedRefIds + = generateSerializedGeneratedRefIds(random, [](ESM::RefId v) { return v.serializeText(); }); + std::size_t i = 0; + for (auto _ : state) + { + benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i])); + if (++i >= serializedRefIds.size()) + i = 0; + } + } + + void serializeTextIndexRefId(benchmark::State& state) + { + std::minstd_rand random; + std::vector refIds = generateIndexRefIds(random); + std::size_t i = 0; + for (auto _ : state) + { + benchmark::DoNotOptimize(refIds[i].serializeText()); + if (++i >= refIds.size()) + i = 0; + } + } + + void deserializeTextIndexRefId(benchmark::State& state) + { + std::minstd_rand random; + std::vector serializedRefIds + = generateSerializedIndexRefIds(random, [](ESM::RefId v) { return v.serializeText(); }); + std::size_t i = 0; + for (auto _ : state) + { + benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i])); + if (++i >= serializedRefIds.size()) + i = 0; + } + } + + void serializeTextESM3ExteriorCellRefId(benchmark::State& state) + { + std::minstd_rand random; + std::vector refIds = generateESM3ExteriorCellRefIds(random); + std::size_t i = 0; + for (auto _ : state) + { + benchmark::DoNotOptimize(refIds[i].serializeText()); + if (++i >= refIds.size()) + i = 0; + } + } + + void deserializeTextESM3ExteriorCellRefId(benchmark::State& state) + { + std::minstd_rand random; + std::vector serializedRefIds + = generateSerializedESM3ExteriorCellRefIds(random, [](ESM::RefId v) { return v.serializeText(); }); + std::size_t i = 0; + for (auto _ : state) + { + benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i])); + if (++i >= serializedRefIds.size()) + i = 0; + } + } +} + +BENCHMARK(serializeRefId)->RangeMultiplier(4)->Range(8, 64); +BENCHMARK(deserializeRefId)->RangeMultiplier(4)->Range(8, 64); +BENCHMARK(serializeTextStringRefId)->RangeMultiplier(4)->Range(8, 64); +BENCHMARK(deserializeTextStringRefId)->RangeMultiplier(4)->Range(8, 64); +BENCHMARK(serializeTextGeneratedRefId); +BENCHMARK(deserializeTextGeneratedRefId); +BENCHMARK(serializeTextIndexRefId); +BENCHMARK(deserializeTextIndexRefId); +BENCHMARK(serializeTextESM3ExteriorCellRefId); +BENCHMARK(deserializeTextESM3ExteriorCellRefId); + +BENCHMARK_MAIN(); diff --git a/apps/benchmarks/settings/CMakeLists.txt b/apps/benchmarks/settings/CMakeLists.txt new file mode 100644 index 00000000000..51e2d2b0fdb --- /dev/null +++ b/apps/benchmarks/settings/CMakeLists.txt @@ -0,0 +1,18 @@ +openmw_add_executable(openmw_settings_access_benchmark access.cpp) +target_link_libraries(openmw_settings_access_benchmark benchmark::benchmark components) + +target_compile_definitions(openmw_settings_access_benchmark + PRIVATE OPENMW_PROJECT_SOURCE_DIR=u8"${PROJECT_SOURCE_DIR}") + +if (UNIX AND NOT APPLE) + target_link_libraries(openmw_settings_access_benchmark ${CMAKE_THREAD_LIBS_INIT}) +endif() + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw_settings_access_benchmark PRIVATE ) +endif() + +if (BUILD_WITH_CODE_COVERAGE) + target_compile_options(openmw_settings_access_benchmark PRIVATE --coverage) + target_link_libraries(openmw_settings_access_benchmark gcov) +endif() diff --git a/apps/benchmarks/settings/access.cpp b/apps/benchmarks/settings/access.cpp new file mode 100644 index 00000000000..7660d0d55e9 --- /dev/null +++ b/apps/benchmarks/settings/access.cpp @@ -0,0 +1,168 @@ +#include + +#include "components/misc/strings/conversion.hpp" +#include "components/settings/parser.hpp" +#include "components/settings/settings.hpp" +#include "components/settings/values.hpp" + +namespace +{ + void settingsManager(benchmark::State& state) + { + for (auto _ : state) + { + benchmark::DoNotOptimize(Settings::Manager::getFloat("sky blending start", "Fog")); + } + } + + void settingsManager2(benchmark::State& state) + { + for (auto _ : state) + { + benchmark::DoNotOptimize(Settings::Manager::getFloat("near clip", "Camera")); + benchmark::DoNotOptimize(Settings::Manager::getBool("transparent postpass", "Post Processing")); + } + } + + void settingsManager3(benchmark::State& state) + { + for (auto _ : state) + { + benchmark::DoNotOptimize(Settings::Manager::getFloat("near clip", "Camera")); + benchmark::DoNotOptimize(Settings::Manager::getBool("transparent postpass", "Post Processing")); + benchmark::DoNotOptimize(Settings::Manager::getInt("reflection detail", "Water")); + } + } + + void localStatic(benchmark::State& state) + { + for (auto _ : state) + { + static float v = Settings::Manager::getFloat("sky blending start", "Fog"); + benchmark::DoNotOptimize(v); + } + } + + void localStatic2(benchmark::State& state) + { + for (auto _ : state) + { + static float v1 = Settings::Manager::getFloat("near clip", "Camera"); + static bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing"); + benchmark::DoNotOptimize(v1); + benchmark::DoNotOptimize(v2); + } + } + + void localStatic3(benchmark::State& state) + { + for (auto _ : state) + { + static float v1 = Settings::Manager::getFloat("near clip", "Camera"); + static bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing"); + static int v3 = Settings::Manager::getInt("reflection detail", "Water"); + benchmark::DoNotOptimize(v1); + benchmark::DoNotOptimize(v2); + benchmark::DoNotOptimize(v3); + } + } + + void settingsStorage(benchmark::State& state) + { + for (auto _ : state) + { + float v = Settings::fog().mSkyBlendingStart.get(); + benchmark::DoNotOptimize(v); + } + } + + void settingsStorage2(benchmark::State& state) + { + for (auto _ : state) + { + bool v1 = Settings::postProcessing().mTransparentPostpass.get(); + float v2 = Settings::camera().mNearClip.get(); + benchmark::DoNotOptimize(v1); + benchmark::DoNotOptimize(v2); + } + } + + void settingsStorage3(benchmark::State& state) + { + for (auto _ : state) + { + bool v1 = Settings::postProcessing().mTransparentPostpass.get(); + float v2 = Settings::camera().mNearClip.get(); + int v3 = Settings::water().mReflectionDetail.get(); + benchmark::DoNotOptimize(v1); + benchmark::DoNotOptimize(v2); + benchmark::DoNotOptimize(v3); + } + } + + void settingsStorageGet(benchmark::State& state) + { + for (auto _ : state) + { + benchmark::DoNotOptimize(Settings::get("Fog", "sky blending start")); + } + } + + void settingsStorageGet2(benchmark::State& state) + { + for (auto _ : state) + { + benchmark::DoNotOptimize(Settings::get("Post Processing", "transparent postpass")); + benchmark::DoNotOptimize(Settings::get("Camera", "near clip")); + } + } + + void settingsStorageGet3(benchmark::State& state) + { + for (auto _ : state) + { + benchmark::DoNotOptimize(Settings::get("Post Processing", "transparent postpass")); + benchmark::DoNotOptimize(Settings::get("Camera", "near clip")); + benchmark::DoNotOptimize(Settings::get("Water", "reflection detail")); + } + } +} + +BENCHMARK(settingsManager); +BENCHMARK(localStatic); +BENCHMARK(settingsStorage); +BENCHMARK(settingsStorageGet); + +BENCHMARK(settingsManager2); +BENCHMARK(localStatic2); +BENCHMARK(settingsStorage2); +BENCHMARK(settingsStorageGet2); + +BENCHMARK(settingsManager3); +BENCHMARK(localStatic3); +BENCHMARK(settingsStorage3); +BENCHMARK(settingsStorageGet3); + +int main(int argc, char* argv[]) +{ + const std::filesystem::path settingsDefaultPath = std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "files" + / Misc::StringUtils::stringToU8String("settings-default.cfg"); + + Settings::SettingsFileParser parser; + parser.loadSettingsFile(settingsDefaultPath, Settings::Manager::mDefaultSettings); + + Settings::StaticValues::initDefaults(); + + Settings::Manager::mUserSettings = Settings::Manager::mDefaultSettings; + Settings::Manager::mUserSettings.erase({ "Camera", "near clip" }); + Settings::Manager::mUserSettings.erase({ "Post Processing", "transparent postpass" }); + Settings::Manager::mUserSettings.erase({ "Water", "reflection detail" }); + + Settings::StaticValues::init(); + + benchmark::Initialize(&argc, argv); + benchmark::RunSpecifiedBenchmarks(); + benchmark::Shutdown(); + + return 0; +} diff --git a/apps/bsatool/CMakeLists.txt b/apps/bsatool/CMakeLists.txt index ec0615ff9c0..b2ad8f16b22 100644 --- a/apps/bsatool/CMakeLists.txt +++ b/apps/bsatool/CMakeLists.txt @@ -1,20 +1,27 @@ set(BSATOOL - bsatool.cpp + bsatool.cpp ) source_group(apps\\bsatool FILES ${BSATOOL}) # Main executable openmw_add_executable(bsatool - ${BSATOOL} + ${BSATOOL} ) target_link_libraries(bsatool - ${Boost_PROGRAM_OPTIONS_LIBRARY} - ${Boost_FILESYSTEM_LIBRARY} - components + Boost::program_options + components ) if (BUILD_WITH_CODE_COVERAGE) - add_definitions (--coverage) - target_link_libraries(bsatool gcov) + target_compile_options(bsatool PRIVATE --coverage) + target_link_libraries(bsatool gcov) +endif() + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(bsatool PRIVATE + + + + ) endif() diff --git a/apps/bsatool/bsatool.cpp b/apps/bsatool/bsatool.cpp index 8e8cf891864..171e5606c4b 100644 --- a/apps/bsatool/bsatool.cpp +++ b/apps/bsatool/bsatool.cpp @@ -1,63 +1,69 @@ -#include +#include +#include #include +#include #include #include -#include -#include +#include +#include #include -#include +#include +#include +#include +#include #define BSATOOL_VERSION 1.1 // Create local aliases for brevity namespace bpo = boost::program_options; -namespace bfs = boost::filesystem; struct Arguments { std::string mode; - std::string filename; - std::string extractfile; - std::string addfile; - std::string outdir; + std::filesystem::path filename; + std::filesystem::path extractfile; + std::filesystem::path addfile; + std::filesystem::path outdir; bool longformat; bool fullpath; }; -bool parseOptions (int argc, char** argv, Arguments &info) +bool parseOptions(int argc, char** argv, Arguments& info) { - bpo::options_description desc("Inspect and extract files from Bethesda BSA archives\n\n" - "Usages:\n" - " bsatool list [-l] archivefile\n" - " List the files presents in the input archive.\n\n" - " bsatool extract [-f] archivefile [file_to_extract] [output_directory]\n" - " Extract a file from the input archive.\n\n" - " bsatool extractall archivefile [output_directory]\n" - " Extract all files from the input archive.\n\n" - " bsatool add [-a] archivefile file_to_add\n" - " Add a file to the input archive.\n\n" - " bsatool create [-c] archivefile\n" - " Create an archive.\n\n" - "Allowed options"); - - desc.add_options() - ("help,h", "print help message.") - ("version,v", "print version information and quit.") - ("long,l", "Include extra information in archive listing.") - ("full-path,f", "Create directory hierarchy on file extraction " - "(always true for extractall).") - ; + bpo::options_description desc(R"(Inspect and extract files from Bethesda BSA archives + +Usages: + bsatool list [-l] archivefile\n + List the files presents in the input archive. + + bsatool extract [-f] archivefile [file_to_extract] [output_directory] + Extract a file from the input archive. + + bsatool extractall archivefile [output_directory] + Extract all files from the input archive. + + bsatool add [-a] archivefile file_to_add + Add a file to the input archive. + + bsatool create [-c] archivefile + Create an archive. +Allowed options)"); + + auto addOption = desc.add_options(); + addOption("help,h", "print help message."); + addOption("version,v", "print version information and quit."); + addOption("long,l", "Include extra information in archive listing."); + addOption("full-path,f", "Create directory hierarchy on file extraction (always true for extractall)."); // input-file is hidden and used as a positional argument bpo::options_description hidden("Hidden Options"); - hidden.add_options() - ( "mode,m", bpo::value(), "bsatool mode") - ( "input-file,i", bpo::value< std::vector >(), "input file") - ; + auto addHiddenOption = hidden.add_options(); + addHiddenOption("mode,m", bpo::value(), "bsatool mode"); + addHiddenOption("input-file,i", bpo::value(), "input file"); bpo::positional_options_description p; p.add("mode", 1).add("input-file", 3); @@ -69,81 +75,82 @@ bool parseOptions (int argc, char** argv, Arguments &info) bpo::variables_map variables; try { - bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv) - .options(all).positional(p).run(); + bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv).options(all).positional(p).run(); bpo::store(valid_opts, variables); } - catch(std::exception &e) + catch (std::exception& e) { - std::cout << "ERROR parsing arguments: " << e.what() << "\n\n" - << desc << std::endl; + std::cout << "ERROR parsing arguments: " << e.what() << "\n\n" << desc << std::endl; return false; } bpo::notify(variables); - if (variables.count ("help")) + if (variables.count("help")) { std::cout << desc << std::endl; return false; } - if (variables.count ("version")) + if (variables.count("version")) { std::cout << "BSATool version " << BSATOOL_VERSION << std::endl; return false; } if (!variables.count("mode")) { - std::cout << "ERROR: no mode specified!\n\n" - << desc << std::endl; + std::cout << "ERROR: no mode specified!\n\n" << desc << std::endl; return false; } info.mode = variables["mode"].as(); - if (!(info.mode == "list" || info.mode == "extract" || info.mode == "extractall" || info.mode == "add" || info.mode == "create")) + if (!(info.mode == "list" || info.mode == "extract" || info.mode == "extractall" || info.mode == "add" + || info.mode == "create")) { - std::cout << std::endl << "ERROR: invalid mode \"" << info.mode << "\"\n\n" - << desc << std::endl; + std::cout << std::endl << "ERROR: invalid mode \"" << info.mode << "\"\n\n" << desc << std::endl; return false; } if (!variables.count("input-file")) { - std::cout << "\nERROR: missing BSA archive\n\n" - << desc << std::endl; + std::cout << "\nERROR: missing BSA archive\n\n" << desc << std::endl; return false; } - info.filename = variables["input-file"].as< std::vector >()[0]; + auto inputFiles = variables["input-file"].as(); + + info.filename = inputFiles[0].u8string(); // This call to u8string is redundant, but required to build on MSVC 14.26 + // due to implementation bugs. // Default output to the working directory - info.outdir = "."; + info.outdir = std::filesystem::current_path(); if (info.mode == "extract") { - if (variables["input-file"].as< std::vector >().size() < 2) + if (inputFiles.size() < 2) { - std::cout << "\nERROR: file to extract unspecified\n\n" - << desc << std::endl; + std::cout << "\nERROR: file to extract unspecified\n\n" << desc << std::endl; return false; } - if (variables["input-file"].as< std::vector >().size() > 1) - info.extractfile = variables["input-file"].as< std::vector >()[1]; - if (variables["input-file"].as< std::vector >().size() > 2) - info.outdir = variables["input-file"].as< std::vector >()[2]; + if (inputFiles.size() > 1) + info.extractfile = inputFiles[1].u8string(); // This call to u8string is redundant, but required to build on + // MSVC 14.26 due to implementation bugs. + if (inputFiles.size() > 2) + info.outdir = inputFiles[2].u8string(); // This call to u8string is redundant, but required to build on + // MSVC 14.26 due to implementation bugs. } else if (info.mode == "add") { - if (variables["input-file"].as< std::vector >().size() < 1) + if (inputFiles.empty()) { - std::cout << "\nERROR: file to add unspecified\n\n" - << desc << std::endl; + std::cout << "\nERROR: file to add unspecified\n\n" << desc << std::endl; return false; } - if (variables["input-file"].as< std::vector >().size() > 1) - info.addfile = variables["input-file"].as< std::vector >()[1]; + if (inputFiles.size() > 1) + info.addfile = inputFiles[1].u8string(); // This call to u8string is redundant, but required to build on + // MSVC 14.26 due to implementation bugs. } - else if (variables["input-file"].as< std::vector >().size() > 1) - info.outdir = variables["input-file"].as< std::vector >()[1]; + else if (inputFiles.size() > 1) + info.outdir = inputFiles[1].u8string(); // This call to u8string is redundant, but required to build on + // MSVC 14.26 due to implementation bugs. info.longformat = variables.count("long") != 0; info.fullpath = variables.count("full-path") != 0; @@ -151,65 +158,14 @@ bool parseOptions (int argc, char** argv, Arguments &info) return true; } -int list(std::unique_ptr& bsa, Arguments& info); -int extract(std::unique_ptr& bsa, Arguments& info); -int extractAll(std::unique_ptr& bsa, Arguments& info); -int add(std::unique_ptr& bsa, Arguments& info); - -int main(int argc, char** argv) -{ - try - { - Arguments info; - if(!parseOptions (argc, argv, info)) - return 1; - - // Open file - std::unique_ptr bsa; - - Bsa::BsaVersion bsaVersion = Bsa::CompressedBSAFile::detectVersion(info.filename); - - if (bsaVersion == Bsa::BSAVER_COMPRESSED) - bsa = std::make_unique(Bsa::CompressedBSAFile()); - else - bsa = std::make_unique(Bsa::BSAFile()); - - if (info.mode == "create") - { - bsa->open(info.filename); - return 0; - } - - bsa->open(info.filename); - - if (info.mode == "list") - return list(bsa, info); - else if (info.mode == "extract") - return extract(bsa, info); - else if (info.mode == "extractall") - return extractAll(bsa, info); - else if (info.mode == "add") - return add(bsa, info); - else - { - std::cout << "Unsupported mode. That is not supposed to happen." << std::endl; - return 1; - } - } - catch (std::exception& e) - { - std::cerr << "ERROR reading BSA archive\nDetails:\n" << e.what() << std::endl; - return 2; - } -} - -int list(std::unique_ptr& bsa, Arguments& info) +template +int list(std::unique_ptr& bsa, Arguments& info) { // List all files - const Bsa::BSAFile::FileList &files = bsa->getList(); + const auto& files = bsa->getList(); for (const auto& file : files) { - if(info.longformat) + if (info.longformat) { // Long format std::ios::fmtflags f(std::cout.flags()); @@ -225,48 +181,58 @@ int list(std::unique_ptr& bsa, Arguments& info) return 0; } -int extract(std::unique_ptr& bsa, Arguments& info) +template +int extract(std::unique_ptr& bsa, Arguments& info) { - std::string archivePath = info.extractfile; - Misc::StringUtils::replaceAll(archivePath, "/", "\\"); + auto archivePath = info.extractfile.u8string(); + Misc::StringUtils::replaceAll(archivePath, u8"/", u8"\\"); - std::string extractPath = info.extractfile; - Misc::StringUtils::replaceAll(extractPath, "\\", "/"); + auto extractPath = info.extractfile.u8string(); + Misc::StringUtils::replaceAll(extractPath, u8"\\", u8"/"); - if (!bsa->exists(archivePath.c_str())) + Files::IStreamPtr stream; + // Get a stream for the file to extract + for (auto it = bsa->getList().rbegin(); it != bsa->getList().rend(); ++it) { - std::cout << "ERROR: file '" << archivePath << "' not found\n"; - std::cout << "In archive: " << info.filename << std::endl; + auto streamPath = Misc::StringUtils::stringToU8String(it->name()); + if (Misc::StringUtils::ciEqual(streamPath, archivePath) || Misc::StringUtils::ciEqual(streamPath, extractPath)) + { + stream = bsa->getFile(&*it); + break; + } + } + if (!stream) + { + std::cout << "ERROR: file '" << Misc::StringUtils::u8StringToString(archivePath) << "' not found\n"; + std::cout << "In archive: " << Files::pathToUnicodeString(info.filename) << std::endl; return 3; } // Get the target path (the path the file will be extracted to) - bfs::path relPath (extractPath); - bfs::path outdir (info.outdir); + std::filesystem::path relPath(extractPath); - bfs::path target; + std::filesystem::path target; if (info.fullpath) - target = outdir / relPath; + target = info.outdir / relPath; else - target = outdir / relPath.filename(); + target = info.outdir / relPath.filename(); // Create the directory hierarchy - bfs::create_directories(target.parent_path()); + std::filesystem::create_directories(target.parent_path()); - bfs::file_status s = bfs::status(target.parent_path()); - if (!bfs::is_directory(s)) + std::filesystem::file_status s = std::filesystem::status(target.parent_path()); + if (!std::filesystem::is_directory(s)) { - std::cout << "ERROR: " << target.parent_path() << " is not a directory." << std::endl; + std::cout << "ERROR: " << Files::pathToUnicodeString(target.parent_path()) << " is not a directory." + << std::endl; return 3; } - // Get a stream for the file to extract - Files::IStreamPtr stream = bsa->getFile(archivePath.c_str()); - - bfs::ofstream out(target, std::ios::binary); + std::ofstream out(target, std::ios::binary); // Write the file to disk - std::cout << "Extracting " << info.extractfile << " to " << target << std::endl; + std::cout << "Extracting " << Files::pathToUnicodeString(info.extractfile) << " to " + << Files::pathToUnicodeString(target) << std::endl; out << stream->rdbuf(); out.close(); @@ -274,34 +240,34 @@ int extract(std::unique_ptr& bsa, Arguments& info) return 0; } -int extractAll(std::unique_ptr& bsa, Arguments& info) +template +int extractAll(std::unique_ptr& bsa, Arguments& info) { - for (const auto &file : bsa->getList()) + for (const auto& file : bsa->getList()) { std::string extractPath(file.name()); Misc::StringUtils::replaceAll(extractPath, "\\", "/"); // Get the target path (the path the file will be extracted to) - bfs::path target (info.outdir); - target /= extractPath; + auto target = info.outdir; + target /= Misc::StringUtils::stringToU8String(extractPath); // Create the directory hierarchy - bfs::create_directories(target.parent_path()); + std::filesystem::create_directories(target.parent_path()); - bfs::file_status s = bfs::status(target.parent_path()); - if (!bfs::is_directory(s)) + std::filesystem::file_status s = std::filesystem::status(target.parent_path()); + if (!std::filesystem::is_directory(s)) { std::cout << "ERROR: " << target.parent_path() << " is not a directory." << std::endl; return 3; } // Get a stream for the file to extract - // (inefficient because getFile iter on the list again) - Files::IStreamPtr data = bsa->getFile(file.name()); - bfs::ofstream out(target, std::ios::binary); + Files::IStreamPtr data = bsa->getFile(&file); + std::ofstream out(target, std::ios::binary); // Write the file to disk - std::cout << "Extracting " << target << std::endl; + std::cout << "Extracting " << Files::pathToUnicodeString(target) << std::endl; out << data->rdbuf(); out.close(); } @@ -309,10 +275,77 @@ int extractAll(std::unique_ptr& bsa, Arguments& info) return 0; } -int add(std::unique_ptr& bsa, Arguments& info) +template +int add(std::unique_ptr& bsa, Arguments& info) { - boost::filesystem::fstream stream(info.addfile, std::ios_base::binary | std::ios_base::out | std::ios_base::in); - bsa->addFile(info.addfile, stream); + std::fstream stream(info.addfile, std::ios_base::binary | std::ios_base::out | std::ios_base::in); + bsa->addFile(Files::pathToUnicodeString(info.addfile), stream); return 0; } + +template +int call(Arguments& info) +{ + std::unique_ptr bsa = std::make_unique(); + if (info.mode == "create") + { + bsa->open(info.filename); + return 0; + } + + bsa->open(info.filename); + + if (info.mode == "list") + return list(bsa, info); + else if (info.mode == "extract") + return extract(bsa, info); + else if (info.mode == "extractall") + return extractAll(bsa, info); + else if (info.mode == "add") + return add(bsa, info); + else + { + std::cout << "Unsupported mode. That is not supposed to happen." << std::endl; + return 1; + } +} + +int main(int argc, char** argv) +{ + try + { + Arguments info; + if (!parseOptions(argc, argv, info)) + return 1; + + // Open file + + // TODO: add a version argument for this mode after compressed BSA writing is a thing + if (info.mode == "create") + return call(info); + + Bsa::BsaVersion bsaVersion = Bsa::BSAFile::detectVersion(info.filename); + + switch (bsaVersion) + { + case Bsa::BsaVersion::Unknown: + break; + case Bsa::BsaVersion::Uncompressed: + return call(info); + case Bsa::BsaVersion::Compressed: + return call(info); + case Bsa::BsaVersion::BA2GNRL: + return call(info); + case Bsa::BsaVersion::BA2DX10: + return call(info); + } + + throw std::runtime_error("Unrecognised BSA archive"); + } + catch (std::exception& e) + { + std::cerr << "ERROR reading BSA archive\nDetails:\n" << e.what() << std::endl; + return 2; + } +} diff --git a/apps/bulletobjecttool/CMakeLists.txt b/apps/bulletobjecttool/CMakeLists.txt new file mode 100644 index 00000000000..c29915b1398 --- /dev/null +++ b/apps/bulletobjecttool/CMakeLists.txt @@ -0,0 +1,27 @@ +set(BULLETMESHTOOL + main.cpp +) +source_group(apps\\bulletobjecttool FILES ${BULLETMESHTOOL}) + +openmw_add_executable(openmw-bulletobjecttool ${BULLETMESHTOOL}) + +target_link_libraries(openmw-bulletobjecttool + Boost::program_options + components +) + +if (BUILD_WITH_CODE_COVERAGE) + target_compile_options(openmw-bulletobjecttool PRIVATE --coverage) + target_link_libraries(openmw-bulletobjecttool gcov) +endif() + +if (WIN32) + install(TARGETS openmw-bulletobjecttool RUNTIME DESTINATION ".") +endif() + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw-bulletobjecttool PRIVATE + + + ) +endif() diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp new file mode 100644 index 00000000000..190eb3364d9 --- /dev/null +++ b/apps/bulletobjecttool/main.cpp @@ -0,0 +1,206 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + namespace bpo = boost::program_options; + + using StringsVector = std::vector; + + constexpr std::string_view applicationName = "BulletObjectTool"; + + bpo::options_description makeOptionsDescription() + { + using Fallback::FallbackMap; + + bpo::options_description result; + auto addOption = result.add_options(); + addOption("help", "print help message"); + + addOption("version", "print version information and quit"); + + addOption("data", + bpo::value() + ->default_value(Files::MaybeQuotedPathContainer(), "data") + ->multitoken() + ->composing(), + "set data directories (later directories have higher priority)"); + + addOption("data-local", + bpo::value()->default_value( + Files::MaybeQuotedPathContainer::value_type(), ""), + "set local data directory (highest priority)"); + + addOption("fallback-archive", + bpo::value()->default_value(StringsVector(), "fallback-archive")->multitoken()->composing(), + "set fallback BSA archives (later archives have higher priority)"); + + addOption("content", bpo::value()->default_value(StringsVector(), "")->multitoken()->composing(), + "content file(s): esm/esp, or omwgame/omwaddon/omwscripts"); + + addOption("encoding", bpo::value()->default_value("win1252"), + "Character encoding used in OpenMW game messages:\n" + "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, " + "Croatian, Serbian (Latin script), Romanian and Albanian languages\n" + "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" + "\n\twin1252 - Western European (Latin) alphabet, used by default"); + + addOption("fallback", bpo::value()->default_value(FallbackMap(), "")->multitoken()->composing(), + "fallback values"); + + Files::ConfigurationManager::addCommonOptions(result); + + return result; + } + + struct WriteArray + { + const float (&mValue)[3]; + + friend std::ostream& operator<<(std::ostream& stream, const WriteArray& value) + { + for (std::size_t i = 0; i < 2; ++i) + stream << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue[i] << ", "; + return stream << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue[2]; + } + }; + + int runBulletObjectTool(int argc, char* argv[]) + { + Platform::init(); + + bpo::options_description desc = makeOptionsDescription(); + + bpo::parsed_options options = bpo::command_line_parser(argc, argv).options(desc).allow_unregistered().run(); + bpo::variables_map variables; + + bpo::store(options, variables); + bpo::notify(variables); + + if (variables.find("help") != variables.end()) + { + Debug::getRawStdout() << desc << std::endl; + return 0; + } + + Files::ConfigurationManager config; + config.readConfiguration(variables, desc); + + Debug::setupLogging(config.getLogPath(), applicationName); + + const std::string encoding(variables["encoding"].as()); + Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(encoding)); + + Files::PathContainer dataDirs(asPathContainer(variables["data"].as())); + + auto local = variables["data-local"].as(); + if (!local.empty()) + dataDirs.push_back(std::move(local)); + + config.filterOutNonExistingPaths(dataDirs); + + const auto& resDir = variables["resources"].as(); + Log(Debug::Info) << Version::getOpenmwVersionDescription(); + dataDirs.insert(dataDirs.begin(), resDir / "vfs"); + const Files::Collections fileCollections(dataDirs); + const auto& archives = variables["fallback-archive"].as(); + StringsVector contentFiles{ "builtin.omwscripts" }; + const auto& configContentFiles = variables["content"].as(); + contentFiles.insert(contentFiles.end(), configContentFiles.begin(), configContentFiles.end()); + + Fallback::Map::init(variables["fallback"].as().mMap); + + VFS::Manager vfs; + + VFS::registerArchives(&vfs, fileCollections, archives, true); + + Settings::Manager::load(config); + + ESM::ReadersCache readers; + EsmLoader::Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + const EsmLoader::EsmData esmData + = EsmLoader::loadEsmData(query, contentFiles, fileCollections, readers, &encoder); + + constexpr double expiryDelay = 0; + Resource::ImageManager imageManager(&vfs, expiryDelay); + Resource::NifFileManager nifFileManager(&vfs, &encoder.getStatelessEncoder()); + Resource::BgsmFileManager bgsmFileManager(&vfs, expiryDelay); + Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, &bgsmFileManager, expiryDelay); + Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay); + + Resource::forEachBulletObject( + readers, vfs, bulletShapeManager, esmData, [](const ESM::Cell& cell, const Resource::BulletObject& object) { + Log(Debug::Verbose) << "Found bullet object in " << (cell.isExterior() ? "exterior" : "interior") + << " cell \"" << cell.getDescription() << "\":" + << " fileName=\"" << object.mShape->mFileName << '"' + << " fileHash=" << Misc::StringUtils::toHex(object.mShape->mFileHash) + << " collisionShape=" << std::boolalpha + << (object.mShape->mCollisionShape == nullptr) + << " avoidCollisionShape=" << std::boolalpha + << (object.mShape->mAvoidCollisionShape == nullptr) << " position=(" + << WriteArray{ object.mPosition.pos } << ')' << " rotation=(" + << WriteArray{ object.mPosition.rot } << ')' + << " scale=" << std::setprecision(std::numeric_limits::max_exponent10) + << object.mScale; + }); + + Log(Debug::Info) << "Done"; + + return 0; + } +} + +int main(int argc, char* argv[]) +{ + return Debug::wrapApplication(runBulletObjectTool, argc, argv, applicationName); +} diff --git a/apps/components_tests/CMakeLists.txt b/apps/components_tests/CMakeLists.txt new file mode 100644 index 00000000000..4f260be1898 --- /dev/null +++ b/apps/components_tests/CMakeLists.txt @@ -0,0 +1,139 @@ +include_directories(SYSTEM ${GTEST_INCLUDE_DIRS}) +include_directories(SYSTEM ${GMOCK_INCLUDE_DIRS}) + +file(GLOB UNITTEST_SRC_FILES + main.cpp + + esm/test_fixed_string.cpp + esm/variant.cpp + esm/testrefid.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 + + misc/compression.cpp + misc/progressreporter.cpp + misc/test_endianness.cpp + misc/test_resourcehelpers.cpp + misc/test_stringops.cpp + misc/testmathutil.cpp + + nifloader/testbulletnifloader.cpp + + detournavigator/navigator.cpp + detournavigator/settingsutils.cpp + detournavigator/recastmeshbuilder.cpp + detournavigator/gettilespositions.cpp + detournavigator/recastmeshobject.cpp + detournavigator/navmeshtilescache.cpp + detournavigator/tilecachedrecastmeshmanager.cpp + detournavigator/navmeshdb.cpp + detournavigator/serialization.cpp + detournavigator/asyncnavmeshupdater.cpp + + serialization/binaryreader.cpp + serialization/binarywriter.cpp + serialization/sizeaccumulator.cpp + serialization/integration.cpp + + settings/parser.cpp + settings/shadermanager.cpp + settings/testvalues.cpp + + shader/parsedefines.cpp + shader/parsefors.cpp + shader/parselinks.cpp + shader/shadermanager.cpp + + sqlite3/db.cpp + sqlite3/request.cpp + sqlite3/statement.cpp + sqlite3/transaction.cpp + + esmloader/load.cpp + esmloader/esmdata.cpp + esmloader/record.cpp + + files/hash.cpp + files/conversion_tests.cpp + + toutf8/toutf8.cpp + + esm4/includes.cpp + + fx/lexer.cpp + fx/technique.cpp + + esm3/readerscache.cpp + esm3/testsaveload.cpp + esm3/testesmwriter.cpp + esm3/testinfoorder.cpp + + nifosg/testnifloader.cpp + + esmterrain/testgridsampling.cpp + + resource/testobjectcache.cpp + + vfs/testpathutil.cpp + + sceneutil/osgacontroller.cpp +) + +source_group(apps\\components-tests FILES ${UNITTEST_SRC_FILES}) + +openmw_add_executable(components-tests ${UNITTEST_SRC_FILES}) + +target_link_libraries(components-tests + GTest::GTest + GMock::GMock + components +) + +# Fix for not visible pthreads functions for linker with glibc 2.15 +if (UNIX AND NOT APPLE) + target_link_libraries(components-tests ${CMAKE_THREAD_LIBS_INIT}) +endif() + +if (BUILD_WITH_CODE_COVERAGE) + target_compile_options(components-tests PRIVATE --coverage) + target_link_libraries(components-tests gcov) +endif() + +file(DOWNLOAD + https://gitlab.com/OpenMW/example-suite/-/raw/8966dab24692555eec720c854fb0f73d108070cd/data/template.omwgame + ${CMAKE_CURRENT_BINARY_DIR}/data/template.omwgame + EXPECTED_HASH SHA512=6e38642bcf013c5f496a9cb0bf3ec7c9553b6e86b836e7844824c5a05f556c9391167214469b6318401684b702d7569896bf743c85aee4198612b3315ba778d6 +) + +target_compile_definitions(components-tests + PRIVATE OPENMW_DATA_DIR=u8"${CMAKE_CURRENT_BINARY_DIR}/data" + OPENMW_PROJECT_SOURCE_DIR=u8"${PROJECT_SOURCE_DIR}") + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(components-tests PRIVATE + + + + + + + + + + + + + + ) +endif() diff --git a/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp new file mode 100644 index 00000000000..14c2a3bfe49 --- /dev/null +++ b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp @@ -0,0 +1,602 @@ +#include "settings.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + using namespace DetourNavigator::Tests; + + void addHeightFieldPlane( + TileCachedRecastMeshManager& recastMeshManager, const osg::Vec2i cellPosition = osg::Vec2i(0, 0)) + { + const int cellSize = 8192; + recastMeshManager.addHeightfield(cellPosition, cellSize, HeightfieldPlane{ 0 }, nullptr); + } + + void addObject(const btBoxShape& shape, TileCachedRecastMeshManager& recastMeshManager) + { + const ObjectId id(&shape); + osg::ref_ptr bulletShape(new Resource::BulletShape); + bulletShape->mFileName = "test.nif"; + bulletShape->mFileHash = "test_hash"; + ObjectTransform objectTransform; + std::fill(std::begin(objectTransform.mPosition.pos), std::end(objectTransform.mPosition.pos), 0.1f); + std::fill(std::begin(objectTransform.mPosition.rot), std::end(objectTransform.mPosition.rot), 0.2f); + objectTransform.mScale = 3.14f; + const CollisionShape collisionShape( + osg::ref_ptr(new Resource::BulletShapeInstance(bulletShape)), shape, + objectTransform); + recastMeshManager.addObject(id, collisionShape, btTransform::getIdentity(), AreaType_ground, nullptr); + } + + struct DetourNavigatorAsyncNavMeshUpdaterTest : Test + { + Settings mSettings = makeSettings(); + TileCachedRecastMeshManager mRecastMeshManager{ mSettings.mRecast }; + OffMeshConnectionsManager mOffMeshConnectionsManager{ mSettings.mRecast }; + const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } }; + const TilePosition mPlayerTile{ 0, 0 }; + const ESM::RefId mWorldspace = ESM::RefId::stringRefId("sys::default"); + const btBoxShape mBox{ btVector3(100, 100, 20) }; + Loading::Listener mListener; + }; + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, for_all_jobs_done_when_empty_wait_should_terminate) + { + AsyncNavMeshUpdater updater{ mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr }; + updater.wait(WaitConditionType::allJobsDone, &mListener); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, for_required_tiles_present_when_empty_wait_should_terminate) + { + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + updater.wait(WaitConditionType::requiredTilesPresent, &mListener); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_generate_navmesh_tile) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, repeated_post_should_lead_to_cache_hit) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + { + const auto stats = updater.getStats(); + ASSERT_EQ(stats.mCache.mGetCount, 1); + ASSERT_EQ(stats.mCache.mHitCount, 0); + } + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + { + const auto stats = updater.getStats(); + EXPECT_EQ(stats.mCache.mGetCount, 2); + EXPECT_EQ(stats.mCache.mHitCount, 1); + } + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_for_update_change_type_should_not_update_cache) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::update } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + { + const auto stats = updater.getStats(); + ASSERT_EQ(stats.mCache.mGetCount, 1); + ASSERT_EQ(stats.mCache.mHitCount, 0); + } + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + { + const auto stats = updater.getStats(); + EXPECT_EQ(stats.mCache.mGetCount, 2); + EXPECT_EQ(stats.mCache.mHitCount, 0); + } + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_write_generated_tile_to_db) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + addObject(mBox, mRecastMeshManager); + auto db = std::make_unique(":memory:", std::numeric_limits::max()); + NavMeshDb* const dbPtr = db.get(); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const TilePosition tilePosition{ 0, 0 }; + const std::map changedTiles{ { tilePosition, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + updater.stop(); + const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition); + ASSERT_NE(recastMesh, nullptr); + ShapeId nextShapeId{ 1 }; + const std::vector objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(), + [&](const MeshSource& v) { return resolveMeshSource(*dbPtr, v, nextShapeId); }); + const auto tile = dbPtr->findTile( + mWorldspace, tilePosition, serialize(mSettings.mRecast, mAgentBounds, *recastMesh, objects)); + ASSERT_TRUE(tile.has_value()); + EXPECT_EQ(tile->mTileId, 1); + EXPECT_EQ(tile->mVersion, navMeshFormatVersion); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_when_writing_to_db_disabled_should_not_write_tiles) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + addObject(mBox, mRecastMeshManager); + auto db = std::make_unique(":memory:", std::numeric_limits::max()); + NavMeshDb* const dbPtr = db.get(); + mSettings.mWriteToNavMeshDb = false; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const TilePosition tilePosition{ 0, 0 }; + const std::map changedTiles{ { tilePosition, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + updater.stop(); + const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition); + ASSERT_NE(recastMesh, nullptr); + ShapeId nextShapeId{ 1 }; + const std::vector objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(), + [&](const MeshSource& v) { return resolveMeshSource(*dbPtr, v, nextShapeId); }); + const auto tile = dbPtr->findTile( + mWorldspace, tilePosition, serialize(mSettings.mRecast, mAgentBounds, *recastMesh, objects)); + ASSERT_FALSE(tile.has_value()); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_when_writing_to_db_disabled_should_not_write_shapes) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + addObject(mBox, mRecastMeshManager); + auto db = std::make_unique(":memory:", std::numeric_limits::max()); + NavMeshDb* const dbPtr = db.get(); + mSettings.mWriteToNavMeshDb = false; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const TilePosition tilePosition{ 0, 0 }; + const std::map changedTiles{ { tilePosition, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + updater.stop(); + const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition); + ASSERT_NE(recastMesh, nullptr); + const auto objects = makeDbRefGeometryObjects( + recastMesh->getMeshSources(), [&](const MeshSource& v) { return resolveMeshSource(*dbPtr, v); }); + EXPECT_TRUE(std::holds_alternative(objects)); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_read_from_db_on_cache_miss) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mMaxNavMeshTilesCacheSize = 0; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, + std::make_unique(":memory:", std::numeric_limits::max())); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + { + const auto stats = updater.getStats(); + ASSERT_EQ(stats.mCache.mGetCount, 1); + ASSERT_EQ(stats.mCache.mHitCount, 0); + ASSERT_TRUE(stats.mDb.has_value()); + ASSERT_EQ(stats.mDb->mGetTileCount, 1); + ASSERT_EQ(stats.mDbGetTileHits, 0); + } + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + { + const auto stats = updater.getStats(); + EXPECT_EQ(stats.mCache.mGetCount, 2); + EXPECT_EQ(stats.mCache.mHitCount, 0); + ASSERT_TRUE(stats.mDb.has_value()); + EXPECT_EQ(stats.mDb->mGetTileCount, 2); + EXPECT_EQ(stats.mDbGetTileHits, 1); + } + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, on_changing_player_tile_post_should_remove_tiles_out_of_range) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTilesAdd{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTilesAdd); + updater.wait(WaitConditionType::allJobsDone, &mListener); + ASSERT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + const std::map changedTilesRemove{ { TilePosition{ 0, 0 }, ChangeType::remove } }; + const TilePosition playerTile(100, 100); + updater.post(mAgentBounds, navMeshCacheItem, playerTile, mWorldspace, changedTilesRemove); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_EQ(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_stop_writing_to_db_when_size_limit_is_reached) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + for (int x = -1; x <= 1; ++x) + for (int y = -1; y <= 1; ++y) + addHeightFieldPlane(mRecastMeshManager, osg::Vec2i(x, y)); + addObject(mBox, mRecastMeshManager); + auto db = std::make_unique(":memory:", 4097); + NavMeshDb* const dbPtr = db.get(); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + std::map changedTiles; + for (int x = -5; x <= 5; ++x) + for (int y = -5; y <= 5; ++y) + changedTiles.emplace(TilePosition{ x, y }, ChangeType::add); + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + updater.stop(); + + std::size_t present = 0; + + for (int x = -5; x <= 5; ++x) + { + for (int y = -5; y <= 5; ++y) + { + const TilePosition tilePosition(x, y); + const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition); + ASSERT_NE(recastMesh, nullptr); + const auto objects = makeDbRefGeometryObjects( + recastMesh->getMeshSources(), [&](const MeshSource& v) { return resolveMeshSource(*dbPtr, v); }); + if (std::holds_alternative(objects)) + continue; + present += dbPtr + ->findTile(mWorldspace, tilePosition, + serialize(mSettings.mRecast, mAgentBounds, *recastMesh, + std::get>(objects))) + .has_value(); + } + } + + EXPECT_EQ(present, 11); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, next_tile_id_should_be_updated_on_duplicate) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + addObject(mBox, mRecastMeshManager); + auto db = std::make_unique(":memory:", std::numeric_limits::max()); + NavMeshDb* const dbPtr = db.get(); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)); + + const TileId nextTileId(dbPtr->getMaxTileId() + 1); + ASSERT_EQ(dbPtr->insertTile(nextTileId, mWorldspace, TilePosition{}, TileVersion{ 1 }, {}, {}), 1); + + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const TilePosition tilePosition{ 0, 0 }; + const std::map changedTiles{ { tilePosition, ChangeType::add } }; + + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + + const AgentBounds agentBounds{ CollisionShapeType::Cylinder, { 29, 29, 66 } }; + updater.post(agentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + + updater.stop(); + + const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition); + ASSERT_NE(recastMesh, nullptr); + ShapeId nextShapeId{ 1 }; + const std::vector objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(), + [&](const MeshSource& v) { return resolveMeshSource(*dbPtr, v, nextShapeId); }); + const auto tile = dbPtr->findTile( + mWorldspace, tilePosition, serialize(mSettings.mRecast, agentBounds, *recastMesh, objects)); + ASSERT_TRUE(tile.has_value()); + EXPECT_EQ(tile->mTileId, 2); + EXPECT_EQ(tile->mVersion, navMeshFormatVersion); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, repeated_tile_updates_should_be_delayed) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + + mSettings.mMaxTilesNumber = 9; + mSettings.mMinUpdateInterval = std::chrono::milliseconds(250); + + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + + std::map changedTiles; + + for (int x = -3; x <= 3; ++x) + for (int y = -3; y <= 3; ++y) + changedTiles.emplace(TilePosition{ x, y }, ChangeType::update); + + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + + updater.wait(WaitConditionType::allJobsDone, &mListener); + + { + const AsyncNavMeshUpdaterStats stats = updater.getStats(); + EXPECT_EQ(stats.mJobs, 0); + EXPECT_EQ(stats.mWaiting.mDelayed, 0); + } + + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + + { + const AsyncNavMeshUpdaterStats stats = updater.getStats(); + EXPECT_EQ(stats.mJobs, 49); + EXPECT_EQ(stats.mWaiting.mDelayed, 49); + } + + updater.wait(WaitConditionType::allJobsDone, &mListener); + + { + const AsyncNavMeshUpdaterStats stats = updater.getStats(); + EXPECT_EQ(stats.mJobs, 0); + EXPECT_EQ(stats.mWaiting.mDelayed, 0); + } + } + + struct DetourNavigatorSpatialJobQueueTest : Test + { + const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, osg::Vec3f(1, 1, 1) }; + const std::shared_ptr mNavMeshCacheItemPtr; + const std::weak_ptr mNavMeshCacheItem = mNavMeshCacheItemPtr; + const ESM::RefId mWorldspace = ESM::RefId::stringRefId("worldspace"); + const TilePosition mChangedTile{ 0, 0 }; + const std::chrono::steady_clock::time_point mProcessTime{}; + const TilePosition mPlayerTile{ 0, 0 }; + const int mMaxTiles = 9; + }; + + TEST_F(DetourNavigatorSpatialJobQueueTest, should_store_multiple_jobs_per_tile) + { + std::list jobs; + SpatialJobQueue queue; + + const ESM::RefId worldspace1 = ESM::RefId::stringRefId("worldspace1"); + const ESM::RefId worldspace2 = ESM::RefId::stringRefId("worldspace2"); + + queue.push(jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, worldspace1, mChangedTile, ChangeType::remove, mProcessTime)); + queue.push(jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, worldspace2, mChangedTile, ChangeType::update, mProcessTime)); + + ASSERT_EQ(queue.size(), 2); + + const auto job1 = queue.pop(mChangedTile); + ASSERT_TRUE(job1.has_value()); + EXPECT_EQ((*job1)->mWorldspace, worldspace1); + + const auto job2 = queue.pop(mChangedTile); + ASSERT_TRUE(job2.has_value()); + EXPECT_EQ((*job2)->mWorldspace, worldspace2); + + EXPECT_EQ(queue.size(), 0); + } + + struct DetourNavigatorJobQueueTest : DetourNavigatorSpatialJobQueueTest + { + }; + + TEST_F(DetourNavigatorJobQueueTest, pop_should_return_nullptr_from_empty) + { + JobQueue queue; + ASSERT_FALSE(queue.hasJob()); + ASSERT_FALSE(queue.pop(mPlayerTile).has_value()); + } + + TEST_F(DetourNavigatorJobQueueTest, push_on_change_type_remove_should_add_to_removing) + { + const std::chrono::steady_clock::time_point processTime{}; + + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::remove, processTime); + + JobQueue queue; + queue.push(job); + + EXPECT_EQ(queue.getStats().mRemoving, 1); + } + + TEST_F(DetourNavigatorJobQueueTest, pop_should_return_last_removing) + { + std::list jobs; + JobQueue queue; + + queue.push(jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(0, 0), + ChangeType::remove, mProcessTime)); + queue.push(jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(1, 0), + ChangeType::remove, mProcessTime)); + + ASSERT_TRUE(queue.hasJob()); + const auto job = queue.pop(mPlayerTile); + ASSERT_TRUE(job.has_value()); + EXPECT_EQ((*job)->mChangedTile, TilePosition(1, 0)); + } + + TEST_F(DetourNavigatorJobQueueTest, push_on_change_type_not_remove_should_add_to_updating) + { + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, mProcessTime); + + JobQueue queue; + queue.push(job); + + EXPECT_EQ(queue.getStats().mUpdating, 1); + } + + TEST_F(DetourNavigatorJobQueueTest, pop_should_return_nearest_to_player_tile) + { + std::list jobs; + + JobQueue queue; + queue.push(jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(0, 0), + ChangeType::update, mProcessTime)); + queue.push(jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(1, 0), + ChangeType::update, mProcessTime)); + + ASSERT_TRUE(queue.hasJob()); + const auto job = queue.pop(TilePosition(1, 0)); + ASSERT_TRUE(job.has_value()); + EXPECT_EQ((*job)->mChangedTile, TilePosition(1, 0)); + } + + TEST_F(DetourNavigatorJobQueueTest, push_on_processing_time_more_than_now_should_add_to_delayed) + { + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const std::chrono::steady_clock::time_point processTime = now + std::chrono::seconds(1); + + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, processTime); + + JobQueue queue; + queue.push(job, now); + + EXPECT_EQ(queue.getStats().mDelayed, 1); + } + + TEST_F(DetourNavigatorJobQueueTest, pop_should_return_when_delayed_job_is_ready) + { + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const std::chrono::steady_clock::time_point processTime = now + std::chrono::seconds(1); + + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, processTime); + + JobQueue queue; + queue.push(job, now); + + ASSERT_FALSE(queue.hasJob(now)); + ASSERT_FALSE(queue.pop(mPlayerTile, now).has_value()); + + ASSERT_TRUE(queue.hasJob(processTime)); + EXPECT_TRUE(queue.pop(mPlayerTile, processTime).has_value()); + } + + TEST_F(DetourNavigatorJobQueueTest, update_should_move_ready_delayed_to_updating) + { + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const std::chrono::steady_clock::time_point processTime = now + std::chrono::seconds(1); + + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, processTime); + + JobQueue queue; + queue.push(job, now); + + ASSERT_EQ(queue.getStats().mDelayed, 1); + + queue.update(mPlayerTile, mMaxTiles, processTime); + + EXPECT_EQ(queue.getStats().mDelayed, 0); + EXPECT_EQ(queue.getStats().mUpdating, 1); + } + + TEST_F(DetourNavigatorJobQueueTest, update_should_move_ready_delayed_to_removing_when_out_of_range) + { + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const std::chrono::steady_clock::time_point processTime = now + std::chrono::seconds(1); + + std::list jobs; + const JobIt job = jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, processTime); + + JobQueue queue; + queue.push(job, now); + + ASSERT_EQ(queue.getStats().mDelayed, 1); + + queue.update(TilePosition(10, 10), mMaxTiles, processTime); + + EXPECT_EQ(queue.getStats().mDelayed, 0); + EXPECT_EQ(queue.getStats().mRemoving, 1); + EXPECT_EQ(job->mChangeType, ChangeType::remove); + } + + TEST_F(DetourNavigatorJobQueueTest, update_should_move_updating_to_removing_when_out_of_range) + { + std::list jobs; + + JobQueue queue; + queue.push(jobs.emplace( + jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, mChangedTile, ChangeType::update, mProcessTime)); + queue.push(jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(10, 10), + ChangeType::update, mProcessTime)); + + ASSERT_EQ(queue.getStats().mUpdating, 2); + + queue.update(TilePosition(10, 10), mMaxTiles); + + EXPECT_EQ(queue.getStats().mUpdating, 1); + EXPECT_EQ(queue.getStats().mRemoving, 1); + } + + TEST_F(DetourNavigatorJobQueueTest, clear_should_remove_all) + { + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const std::chrono::steady_clock::time_point processTime = now + std::chrono::seconds(1); + + std::list jobs; + const JobIt removing = jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, + TilePosition(0, 0), ChangeType::remove, mProcessTime); + const JobIt updating = jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, + TilePosition(1, 0), ChangeType::update, mProcessTime); + const JobIt delayed = jobs.emplace(jobs.end(), mAgentBounds, mNavMeshCacheItem, mWorldspace, TilePosition(2, 0), + ChangeType::update, processTime); + + JobQueue queue; + queue.push(removing); + queue.push(updating); + queue.push(delayed, now); + + ASSERT_EQ(queue.getStats().mRemoving, 1); + ASSERT_EQ(queue.getStats().mUpdating, 1); + ASSERT_EQ(queue.getStats().mDelayed, 1); + + queue.clear(); + + EXPECT_EQ(queue.getStats().mRemoving, 0); + EXPECT_EQ(queue.getStats().mUpdating, 0); + EXPECT_EQ(queue.getStats().mDelayed, 0); + } +} diff --git a/apps/components_tests/detournavigator/generate.hpp b/apps/components_tests/detournavigator/generate.hpp new file mode 100644 index 00000000000..94acfc46737 --- /dev/null +++ b/apps/components_tests/detournavigator/generate.hpp @@ -0,0 +1,46 @@ +#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_GENERATE_H +#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_GENERATE_H + +#include +#include +#include +#include + +namespace DetourNavigator +{ + namespace Tests + { + template + inline auto generateValue(T& value, Random& random) -> std::enable_if_t= 2> + { + using Distribution = std::conditional_t, std::uniform_real_distribution, + std::uniform_int_distribution>; + Distribution distribution(std::numeric_limits::min(), std::numeric_limits::max()); + value = distribution(random); + } + + template + inline auto generateValue(T& value, Random& random) -> std::enable_if_t + { + unsigned short v; + generateValue(v, random); + value = static_cast(v % 256); + } + + template + inline void generateValue(unsigned char& value, Random& random) + { + unsigned short v; + generateValue(v, random); + value = static_cast(v % 256); + } + + template + inline void generateRange(I begin, I end, Random& random) + { + std::for_each(begin, end, [&](auto& v) { generateValue(v, random); }); + } + } +} + +#endif diff --git a/apps/components_tests/detournavigator/gettilespositions.cpp b/apps/components_tests/detournavigator/gettilespositions.cpp new file mode 100644 index 00000000000..729d11ddb5d --- /dev/null +++ b/apps/components_tests/detournavigator/gettilespositions.cpp @@ -0,0 +1,165 @@ +#include +#include +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + + struct CollectTilesPositions + { + std::vector& mTilesPositions; + + void operator()(const TilePosition& value) { mTilesPositions.push_back(value); } + }; + + struct DetourNavigatorGetTilesPositionsTest : Test + { + RecastSettings mSettings; + std::vector mTilesPositions; + CollectTilesPositions mCollect{ mTilesPositions }; + + DetourNavigatorGetTilesPositionsTest() + { + mSettings.mBorderSize = 0; + mSettings.mCellSize = 0.5; + mSettings.mRecastScaleFactor = 1; + mSettings.mTileSize = 64; + } + }; + + TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_in_single_tile_should_return_one_tile) + { + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(2, 2), osg::Vec2f(31, 31), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_with_x_bounds_in_two_tiles_should_return_two_tiles) + { + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(32, 31), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0), TilePosition(1, 0))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_with_y_bounds_in_two_tiles_should_return_two_tiles) + { + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(31, 32), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0), TilePosition(0, 1))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, tiling_works_only_for_x_and_y_coordinates) + { + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(31, 31), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, tiling_should_work_with_negative_coordinates) + { + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(-31, -31), osg::Vec2f(31, 31), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, + ElementsAre(TilePosition(-1, -1), TilePosition(-1, 0), TilePosition(0, -1), TilePosition(0, 0))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, border_size_should_extend_tile_bounds) + { + mSettings.mBorderSize = 1; + + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(31.5, 31.5), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, + ElementsAre(TilePosition(-1, -1), TilePosition(-1, 0), TilePosition(-1, 1), TilePosition(0, -1), + TilePosition(0, 0), TilePosition(0, 1), TilePosition(1, -1), TilePosition(1, 0), TilePosition(1, 1))); + } + + TEST_F(DetourNavigatorGetTilesPositionsTest, should_apply_recast_scale_factor) + { + mSettings.mRecastScaleFactor = 0.5; + + getTilesPositions(makeTilesPositionsRange(osg::Vec2f(0, 0), osg::Vec2f(32, 32), mSettings), mCollect); + + EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0))); + } + + struct TilesPositionsRangeParams + { + TilesPositionsRange mA; + TilesPositionsRange mB; + TilesPositionsRange mResult; + }; + + struct DetourNavigatorGetIntersectionTest : TestWithParam + { + }; + + TEST_P(DetourNavigatorGetIntersectionTest, should_return_expected_result) + { + EXPECT_EQ(getIntersection(GetParam().mA, GetParam().mB), GetParam().mResult); + EXPECT_EQ(getIntersection(GetParam().mB, GetParam().mA), GetParam().mResult); + } + + const TilesPositionsRangeParams getIntersectionParams[] = { + { .mA = TilesPositionsRange{}, .mB = TilesPositionsRange{}, .mResult = TilesPositionsRange{} }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 2, 2 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 2, 2 } }, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 2, 2 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{}, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 2, 2 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 1, 1 } }, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 0, 2 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{}, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 2, 0 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{}, + }, + }; + + INSTANTIATE_TEST_SUITE_P( + GetIntersectionParams, DetourNavigatorGetIntersectionTest, ValuesIn(getIntersectionParams)); + + struct DetourNavigatorGetUnionTest : TestWithParam + { + }; + + TEST_P(DetourNavigatorGetUnionTest, should_return_expected_result) + { + EXPECT_EQ(getUnion(GetParam().mA, GetParam().mB), GetParam().mResult); + EXPECT_EQ(getUnion(GetParam().mB, GetParam().mA), GetParam().mResult); + } + + const TilesPositionsRangeParams getUnionParams[] = { + { .mA = TilesPositionsRange{}, .mB = TilesPositionsRange{}, .mResult = TilesPositionsRange{} }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 2, 2 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 3, 3 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 3, 3 } }, + }, + { + .mA = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 1, 1 } }, + .mB = TilesPositionsRange{ .mBegin = TilePosition{ 1, 1 }, .mEnd = TilePosition{ 2, 2 } }, + .mResult = TilesPositionsRange{ .mBegin = TilePosition{ 0, 0 }, .mEnd = TilePosition{ 2, 2 } }, + }, + }; + + INSTANTIATE_TEST_SUITE_P(GetUnionParams, DetourNavigatorGetUnionTest, ValuesIn(getUnionParams)); +} diff --git a/apps/components_tests/detournavigator/navigator.cpp b/apps/components_tests/detournavigator/navigator.cpp new file mode 100644 index 00000000000..29278697825 --- /dev/null +++ b/apps/components_tests/detournavigator/navigator.cpp @@ -0,0 +1,1289 @@ +#include "operators.hpp" +#include "settings.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +MATCHER_P3(Vec3fEq, x, y, z, "") +{ + return std::abs(arg.x() - x) < 1e-3 && std::abs(arg.y() - y) < 1e-3 && std::abs(arg.z() - z) < 1e-3; +} + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + using namespace DetourNavigator::Tests; + + constexpr int heightfieldTileSize = ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1); + + struct DetourNavigatorNavigatorTest : Test + { + Settings mSettings = makeSettings(); + std::unique_ptr mNavigator = std::make_unique( + mSettings, std::make_unique(":memory:", std::numeric_limits::max())); + const osg::Vec3f mPlayerPosition{ 256, 256, 0 }; + const ESM::RefId mWorldspace = ESM::RefId::stringRefId("sys::default"); + const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } }; + osg::Vec3f mStart{ 52, 460, 1 }; + osg::Vec3f mEnd{ 460, 52, 1 }; + std::deque mPath; + std::back_insert_iterator> mOut{ mPath }; + AreaCosts mAreaCosts; + Loading::Listener mListener; + const osg::Vec2i mCellPosition{ 0, 0 }; + const float mEndTolerance = 0; + const btTransform mTransform{ btMatrix3x3::getIdentity(), btVector3(256, 256, 0) }; + const ObjectTransform mObjectTransform{ ESM::Position{ { 256, 256, 0 }, { 0, 0, 0 } }, 0.0f }; + }; + + constexpr std::array defaultHeightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, -25, -25, -25, -25, // row 1 + 0, -25, -100, -100, -100, // row 2 + 0, -25, -100, -100, -100, // row 3 + 0, -25, -100, -100, -100, // row 4 + } }; + + constexpr std::array defaultHeightfieldDataScalar{ { + 0, 0, 0, 0, 0, // row 0 + 0, -25, -25, -25, -25, // row 1 + 0, -25, -100, -100, -100, // row 2 + 0, -25, -100, -100, -100, // row 3 + 0, -25, -100, -100, -100, // row 4 + } }; + + template + std::unique_ptr makeSquareHeightfieldTerrainShape( + const std::array& values, btScalar heightScale = 1, int upAxis = 2, + PHY_ScalarType heightDataType = PHY_FLOAT, bool flipQuadEdges = false) + { + const int width = static_cast(std::sqrt(size)); + const btScalar min = *std::min_element(values.begin(), values.end()); + const btScalar max = *std::max_element(values.begin(), values.end()); + const btScalar greater = std::max(std::abs(min), std::abs(max)); + return std::make_unique( + width, width, values.data(), heightScale, -greater, greater, upAxis, heightDataType, flipQuadEdges); + } + + template + HeightfieldSurface makeSquareHeightfieldSurface(const std::array& values) + { + const auto [min, max] = std::minmax_element(values.begin(), values.end()); + const float greater = std::max(std::abs(*min), std::abs(*max)); + HeightfieldSurface surface; + surface.mHeights = values.data(); + surface.mMinHeight = -greater; + surface.mMaxHeight = greater; + surface.mSize = static_cast(std::sqrt(size)); + return surface; + } + + template + osg::ref_ptr makeBulletShapeInstance(std::unique_ptr&& shape) + { + osg::ref_ptr bulletShape(new Resource::BulletShape); + bulletShape->mCollisionShape.reset(std::move(shape).release()); + return new Resource::BulletShapeInstance(bulletShape); + } + + template + class CollisionShapeInstance + { + public: + CollisionShapeInstance(std::unique_ptr&& shape) + : mInstance(makeBulletShapeInstance(std::move(shape))) + { + } + + T& shape() const { return static_cast(*mInstance->mCollisionShape); } + const osg::ref_ptr& instance() const { return mInstance; } + + private: + osg::ref_ptr mInstance; + }; + + btVector3 getHeightfieldShift(const osg::Vec2i& cellPosition, int cellSize, float minHeight, float maxHeight) + { + return BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.x(), cellSize, minHeight, maxHeight); + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty) + { + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::NavMeshNotFound); + EXPECT_EQ(mPath, std::deque()); + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception) + { + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::StartPolygonNotFound); + } + + TEST_F(DetourNavigatorNavigatorTest, add_agent_should_count_each_agent) + { + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->removeAgent(mAgentBounds); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::StartPolygonNotFound); + } + + TEST_F(DetourNavigatorNavigatorTest, update_then_find_path_should_return_path) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, 1.99999392032623291015625), + Vec3fEq(460, 56.66664886474609375, 1.99999392032623291015625))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_to_the_start_position_should_contain_single_point) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, add_object_should_change_navmesh) + { + mSettings.mWaitUntilMinDistanceToPlayer = 0; + mNavigator.reset(new NavigatorImpl( + mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); + + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + CollisionShapeInstance compound(std::make_unique()); + compound.shape().addChildShape( + btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), new btBoxShape(btVector3(20, 20, 100))); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125))) + << mPath; + + { + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), + mTransform, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + } + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + mPath.clear(); + mOut = std::back_inserter(mPath); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, 1.99999392032623291015625), + Vec3fEq(181.33331298828125, 215.33331298828125, -20.6666717529296875), + Vec3fEq(215.33331298828125, 181.33331298828125, -20.6666717529296875), + Vec3fEq(460, 56.66664886474609375, 1.99999392032623291015625))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, update_changed_object_should_change_navmesh) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + CollisionShapeInstance compound(std::make_unique()); + compound.shape().addChildShape( + btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), new btBoxShape(btVector3(20, 20, 100))); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->addObject( + ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, 1.99999392032623291015625), + Vec3fEq(181.33331298828125, 215.33331298828125, -20.6666717529296875), + Vec3fEq(215.33331298828125, 181.33331298828125, -20.6666717529296875), + Vec3fEq(460, 56.66664886474609375, 1.99999392032623291015625))) + << mPath; + + compound.shape().updateChildTransform(0, btTransform(btMatrix3x3::getIdentity(), btVector3(1000, 0, 0))); + + mNavigator->updateObject( + ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + mPath.clear(); + mOut = std::back_inserter(mPath); + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, 1.99999392032623291015625), + Vec3fEq(460, 56.66664886474609375, 1.99999392032623291015625))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, for_overlapping_heightfields_objects_should_use_higher) + { + CollisionShapeInstance heightfield1(makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar)); + heightfield1.shape().setLocalScaling(btVector3(128, 128, 1)); + + const std::array heightfieldData2{ { + -25, -25, -25, -25, -25, // row 0 + -25, -25, -25, -25, -25, // row 1 + -25, -25, -25, -25, -25, // row 2 + -25, -25, -25, -25, -25, // row 3 + -25, -25, -25, -25, -25, // row 4 + } }; + CollisionShapeInstance heightfield2(makeSquareHeightfieldTerrainShape(heightfieldData2)); + heightfield2.shape().setLocalScaling(btVector3(128, 128, 1)); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addObject(ObjectId(&heightfield1.shape()), ObjectShapes(heightfield1.instance(), mObjectTransform), + mTransform, nullptr); + mNavigator->addObject(ObjectId(&heightfield2.shape()), ObjectShapes(heightfield2.instance(), mObjectTransform), + mTransform, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, 1.99999392032623291015625), + Vec3fEq(460, 56.66664886474609375, 1.99999392032623291015625))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, only_one_heightfield_per_cell_is_allowed) + { + const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize1 = heightfieldTileSize * (surface1.mSize - 1); + + const std::array heightfieldData2{ { + -25, -25, -25, -25, -25, // row 0 + -25, -25, -25, -25, -25, // row 1 + -25, -25, -25, -25, -25, // row 2 + -25, -25, -25, -25, -25, // row 3 + -25, -25, -25, -25, -25, // row 4 + } }; + const HeightfieldSurface surface2 = makeSquareHeightfieldSurface(heightfieldData2); + const int cellSize2 = heightfieldTileSize * (surface2.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize1, surface1, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const Version version = mNavigator->getNavMesh(mAgentBounds)->lockConst()->getVersion(); + + mNavigator->addHeightfield(mCellPosition, cellSize2, surface2, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + EXPECT_EQ(mNavigator->getNavMesh(mAgentBounds)->lockConst()->getVersion(), version); + } + + TEST_F(DetourNavigatorNavigatorTest, path_should_be_around_avoid_shape) + { + osg::ref_ptr bulletShape(new Resource::BulletShape); + + std::unique_ptr shapePtr + = makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar); + shapePtr->setLocalScaling(btVector3(128, 128, 1)); + bulletShape->mCollisionShape.reset(shapePtr.release()); + + std::array heightfieldDataAvoid{ { + -25, -25, -25, -25, -25, // row 0 + -25, -25, -25, -25, -25, // row 1 + -25, -25, -25, -25, -25, // row 2 + -25, -25, -25, -25, -25, // row 3 + -25, -25, -25, -25, -25, // row 4 + } }; + std::unique_ptr shapeAvoidPtr + = makeSquareHeightfieldTerrainShape(heightfieldDataAvoid); + shapeAvoidPtr->setLocalScaling(btVector3(128, 128, 1)); + bulletShape->mAvoidCollisionShape.reset(shapeAvoidPtr.release()); + + osg::ref_ptr instance(new Resource::BulletShapeInstance(bulletShape)); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addObject( + ObjectId(instance->mCollisionShape.get()), ObjectShapes(instance, mObjectTransform), mTransform, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, 1.99999392032623291015625), + Vec3fEq(158.6666412353515625, 249.3332977294921875, -20.6666717529296875), + Vec3fEq(249.3332977294921875, 158.6666412353515625, -20.6666717529296875), + Vec3fEq(460, 56.66664886474609375, 1.99999392032623291015625))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_ground_lower_than_water_with_only_swim_flag) + { + std::array heightfieldData{ { + -50, -50, -50, -50, 0, // row 0 + -50, -100, -150, -100, -50, // row 1 + -50, -150, -200, -150, -100, // row 2 + -50, -100, -150, -100, -100, // row 3 + 0, -50, -100, -100, -100, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addWater(mCellPosition, cellSize, 300, nullptr); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + mStart.x() = 256; + mStart.z() = 300; + mEnd.x() = 256; + mEnd.z() = 300; + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(256, 460, 185.3333282470703125), Vec3fEq(256, 56.66664886474609375, 185.3333282470703125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_when_ground_cross_water_with_swim_and_walk_flags) + { + std::array heightfieldData{ { + 0, 0, 0, 0, 0, 0, 0, // row 0 + 0, -100, -100, -100, -100, -100, 0, // row 1 + 0, -100, -150, -150, -150, -100, 0, // row 2 + 0, -100, -150, -200, -150, -100, 0, // row 3 + 0, -100, -150, -150, -150, -100, 0, // row 4 + 0, -100, -100, -100, -100, -100, 0, // row 5 + 0, 0, 0, 0, 0, 0, 0, // row 6 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addWater(mCellPosition, cellSize, -25, nullptr); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + mStart.x() = 256; + mEnd.x() = 256; + + EXPECT_EQ( + findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(256, 460, -129.4098663330078125), Vec3fEq(256, 56.66664886474609375, -30.0000133514404296875))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, + path_should_be_over_water_when_ground_cross_water_with_max_int_cells_size_and_swim_and_walk_flags) + { + std::array heightfieldData{ { + 0, 0, 0, 0, 0, 0, 0, // row 0 + 0, -100, -100, -100, -100, -100, 0, // row 1 + 0, -100, -150, -150, -150, -100, 0, // row 2 + 0, -100, -150, -200, -150, -100, 0, // row 3 + 0, -100, -150, -150, -150, -100, 0, // row 4 + 0, -100, -100, -100, -100, -100, 0, // row 5 + 0, 0, 0, 0, 0, 0, 0, // row 6 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->addWater(mCellPosition, std::numeric_limits::max(), -25, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + mStart.x() = 256; + mEnd.x() = 256; + + EXPECT_EQ( + findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( + Vec3fEq(256, 460, -129.4098663330078125), Vec3fEq(256, 56.66664886474609375, -30.0000133514404296875))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_ground_when_ground_cross_water_with_only_walk_flag) + { + std::array heightfieldData{ { + 0, 0, 0, 0, 0, 0, 0, // row 0 + 0, -100, -100, -100, -100, -100, 0, // row 1 + 0, -100, -150, -150, -150, -100, 0, // row 2 + 0, -100, -150, -200, -150, -100, 0, // row 3 + 0, -100, -150, -150, -150, -100, 0, // row 4 + 0, -100, -100, -100, -100, -100, 0, // row 5 + 0, 0, 0, 0, 0, 0, 0, // row 6 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addWater(mCellPosition, cellSize, -25, nullptr); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + mStart.x() = 256; + mEnd.x() = 256; + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(256, 460, -129.4098663330078125), Vec3fEq(256, 56.66664886474609375, -30.0000133514404296875))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, update_object_remove_and_update_then_find_path_should_return_path) + { + CollisionShapeInstance heightfield(makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar)); + heightfield.shape().setLocalScaling(btVector3(128, 128, 1)); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance(), mObjectTransform), + mTransform, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + mNavigator->removeObject(ObjectId(&heightfield.shape()), nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance(), mObjectTransform), + mTransform, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, 1.99999392032623291015625), + Vec3fEq(460, 56.66664886474609375, 1.99999392032623291015625))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, update_heightfield_remove_and_update_then_find_path_should_return_path) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + mNavigator->removeHeightfield(mCellPosition, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, 1.99999392032623291015625), + Vec3fEq(460, 56.66664886474609375, 1.99999392032623291015625))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, update_then_find_random_point_around_circle_should_return_position) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, 0, // row 0 + 0, -25, -25, -25, -25, -25, // row 1 + 0, -25, -1000, -1000, -100, -100, // row 2 + 0, -25, -1000, -1000, -100, -100, // row 3 + 0, -25, -100, -100, -100, -100, // row 4 + 0, -25, -100, -100, -100, -100, // row 5 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + Misc::Rng::init(42); + + const auto result = findRandomPointAroundCircle( + *mNavigator, mAgentBounds, mStart, 100.0, Flag_walk, []() { return Misc::Rng::rollClosedProbability(); }); + + ASSERT_THAT(result, Optional(Vec3fEq(70.35845947265625, 335.592041015625, -2.6667339801788330078125))) + << (result ? *result : osg::Vec3f()); + + const auto distance = (*result - mStart).length(); + + EXPECT_FLOAT_EQ(distance, 125.80865478515625) << distance; + } + + TEST_F(DetourNavigatorNavigatorTest, multiple_threads_should_lock_tiles) + { + mSettings.mAsyncNavMeshUpdaterThreads = 2; + mNavigator.reset(new NavigatorImpl( + mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); + + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + const btVector3 shift = getHeightfieldShift(mCellPosition, cellSize, surface.mMinHeight, surface.mMaxHeight); + + std::vector> boxes; + std::generate_n( + std::back_inserter(boxes), 100, [] { return std::make_unique(btVector3(20, 20, 100)); }); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + + for (std::size_t i = 0; i < boxes.size(); ++i) + { + const btTransform transform( + btMatrix3x3::getIdentity(), btVector3(shift.x() + i * 10, shift.y() + i * 10, i * 10)); + mNavigator->addObject( + ObjectId(&boxes[i].shape()), ObjectShapes(boxes[i].instance(), mObjectTransform), transform, nullptr); + } + + std::this_thread::sleep_for(std::chrono::microseconds(1)); + + for (std::size_t i = 0; i < boxes.size(); ++i) + { + const btTransform transform( + btMatrix3x3::getIdentity(), btVector3(shift.x() + i * 10 + 1, shift.y() + i * 10 + 1, i * 10 + 1)); + mNavigator->updateObject( + ObjectId(&boxes[i].shape()), ObjectShapes(boxes[i].instance(), mObjectTransform), transform, nullptr); + } + + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, 1.99999392032623291015625), + Vec3fEq(181.33331298828125, 215.33331298828125, -20.6666717529296875), + Vec3fEq(215.33331298828125, 181.33331298828125, -20.6666717529296875), + Vec3fEq(460, 56.66664886474609375, 1.99999392032623291015625))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, update_changed_multiple_times_object_should_delay_navmesh_change) + { + std::vector> shapes; + std::generate_n( + std::back_inserter(shapes), 100, [] { return std::make_unique(btVector3(64, 64, 64)); }); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + + for (std::size_t i = 0; i < shapes.size(); ++i) + { + const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32, i * 32, i * 32)); + mNavigator->addObject( + ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance(), mObjectTransform), transform, nullptr); + } + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const auto start = std::chrono::steady_clock::now(); + for (std::size_t i = 0; i < shapes.size(); ++i) + { + const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 1, i * 32 + 1, i * 32 + 1)); + mNavigator->updateObject( + ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance(), mObjectTransform), transform, nullptr); + } + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + for (std::size_t i = 0; i < shapes.size(); ++i) + { + const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 2, i * 32 + 2, i * 32 + 2)); + mNavigator->updateObject( + ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance(), mObjectTransform), transform, nullptr); + } + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const auto duration = std::chrono::steady_clock::now() - start; + + EXPECT_GT(duration, mSettings.mMinUpdateInterval) + << std::chrono::duration_cast>(duration).count() << " ms"; + } + + TEST_F(DetourNavigatorNavigatorTest, update_then_raycast_should_return_position) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const osg::Vec3f start(57, 460, 1); + const osg::Vec3f end(460, 57, 1); + const auto result = raycast(*mNavigator, mAgentBounds, start, end, Flag_walk); + + ASSERT_THAT(result, Optional(Vec3fEq(end.x(), end.y(), 1.95257937908172607421875))) + << (result ? *result : osg::Vec3f()); + } + + TEST_F(DetourNavigatorNavigatorTest, + update_for_oscillating_object_that_does_not_change_navmesh_should_not_trigger_navmesh_update) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + CollisionShapeInstance oscillatingBox(std::make_unique(btVector3(20, 20, 20))); + const btVector3 oscillatingBoxShapePosition(288, 288, 400); + CollisionShapeInstance borderBox(std::make_unique(btVector3(50, 50, 50))); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->addObject(ObjectId(&oscillatingBox.shape()), + ObjectShapes(oscillatingBox.instance(), mObjectTransform), + btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition), nullptr); + // add this box to make navmesh bound box independent from oscillatingBoxShape rotations + mNavigator->addObject(ObjectId(&borderBox.shape()), ObjectShapes(borderBox.instance(), mObjectTransform), + btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition + btVector3(0, 0, 200)), nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const Version expectedVersion{ 1, 4 }; + + const auto navMeshes = mNavigator->getNavMeshes(); + ASSERT_EQ(navMeshes.size(), 1); + ASSERT_EQ(navMeshes.begin()->second->lockConst()->getVersion(), expectedVersion); + + for (int n = 0; n < 10; ++n) + { + const btTransform transform( + btQuaternion(btVector3(0, 0, 1), n * 2 * osg::PI / 10), oscillatingBoxShapePosition); + mNavigator->updateObject(ObjectId(&oscillatingBox.shape()), + ObjectShapes(oscillatingBox.instance(), mObjectTransform), transform, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + } + + ASSERT_EQ(navMeshes.size(), 1); + ASSERT_EQ(navMeshes.begin()->second->lockConst()->getVersion(), expectedVersion); + } + + TEST_F(DetourNavigatorNavigatorTest, should_provide_path_over_flat_heightfield) + { + const HeightfieldPlane plane{ 100 }; + const int cellSize = heightfieldTileSize * 4; + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, plane, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, 102), Vec3fEq(460, 56.66664886474609375, 102))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, for_not_reachable_destination_find_path_should_provide_partial_path) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + CollisionShapeInstance compound(std::make_unique()); + compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)), + new btBoxShape(btVector3(200, 200, 1000))); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->addObject( + ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::PartialPath); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, -2.5371119976043701171875), + Vec3fEq(222, 290, -71.33342742919921875))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, end_tolerance_should_extent_available_destinations) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + CollisionShapeInstance compound(std::make_unique()); + compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)), + new btBoxShape(btVector3(100, 100, 1000))); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->addObject( + ObjectId(&compound.shape()), ObjectShapes(compound.instance(), mObjectTransform), mTransform, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const float endTolerance = 1000.0f; + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 460, -2.5371119976043701171875), + Vec3fEq(305.999969482421875, 56.66664886474609375, -2.6667406558990478515625))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, only_one_water_per_cell_is_allowed) + { + const int cellSize1 = 100; + const float level1 = 1; + const int cellSize2 = 200; + const float level2 = 2; + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addWater(mCellPosition, cellSize1, level1, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const Version version = mNavigator->getNavMesh(mAgentBounds)->lockConst()->getVersion(); + + mNavigator->addWater(mCellPosition, cellSize2, level2, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + EXPECT_EQ(mNavigator->getNavMesh(mAgentBounds)->lockConst()->getVersion(), version); + } + + std::pair getMinMax(const RecastMeshTiles& tiles) + { + const auto lessByX = [](const auto& l, const auto& r) { return l.first.x() < r.first.x(); }; + const auto lessByY = [](const auto& l, const auto& r) { return l.first.y() < r.first.y(); }; + + const auto [minX, maxX] = std::ranges::minmax_element(tiles, lessByX); + const auto [minY, maxY] = std::ranges::minmax_element(tiles, lessByY); + + return { TilePosition(minX->first.x(), minY->first.y()), TilePosition(maxX->first.x(), maxY->first.y()) }; + } + + TEST_F(DetourNavigatorNavigatorTest, update_for_very_big_object_should_be_limited) + { + const float size = static_cast((1 << 22) - 1); + CollisionShapeInstance bigBox(std::make_unique(btVector3(size, size, 1))); + const ObjectTransform objectTransform{ + .mPosition = ESM::Position{ .pos = { 0, 0, 0 }, .rot{ 0, 0, 0 } }, + .mScale = 1.0f, + }; + const std::optional cellGridBounds = std::nullopt; + const osg::Vec3f playerPosition(32, 1024, 0); + + mNavigator->updateBounds(mWorldspace, cellGridBounds, playerPosition, nullptr); + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addObject(ObjectId(&bigBox.shape()), ObjectShapes(bigBox.instance(), objectTransform), + btTransform::getIdentity(), nullptr); + + bool updated = false; + std::condition_variable updateFinished; + std::mutex mutex; + + std::thread thread([&] { + auto guard = mNavigator->makeUpdateGuard(); + mNavigator->update(playerPosition, guard.get()); + std::lock_guard lock(mutex); + updated = true; + updateFinished.notify_all(); + }); + + { + std::unique_lock lock(mutex); + updateFinished.wait_for(lock, std::chrono::seconds(10), [&] { return updated; }); + ASSERT_TRUE(updated); + } + + thread.join(); + + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const auto recastMeshTiles = mNavigator->getRecastMeshTiles(); + ASSERT_EQ(recastMeshTiles.size(), 1033); + EXPECT_EQ(getMinMax(recastMeshTiles), std::pair(TilePosition(-18, -17), TilePosition(18, 19))); + + const auto navMesh = mNavigator->getNavMesh(mAgentBounds); + ASSERT_NE(navMesh, nullptr); + + std::size_t usedNavMeshTiles = 0; + navMesh->lockConst()->forEachUsedTile([&](const auto&...) { ++usedNavMeshTiles; }); + EXPECT_EQ(usedNavMeshTiles, 1024); + } + + TEST_F(DetourNavigatorNavigatorTest, update_should_be_limited_by_cell_grid_bounds) + { + const float size = static_cast((1 << 22) - 1); + CollisionShapeInstance bigBox(std::make_unique(btVector3(size, size, 1))); + const ObjectTransform objectTransform{ + .mPosition = ESM::Position{ .pos = { 0, 0, 0 }, .rot{ 0, 0, 0 } }, + .mScale = 1.0f, + }; + const CellGridBounds cellGridBounds{ + .mCenter = osg::Vec2i(0, 0), + .mHalfSize = 1, + }; + const osg::Vec3f playerPosition(32, 1024, 0); + + mNavigator->updateBounds(mWorldspace, cellGridBounds, playerPosition, nullptr); + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addObject(ObjectId(&bigBox.shape()), ObjectShapes(bigBox.instance(), objectTransform), + btTransform::getIdentity(), nullptr); + + bool updated = false; + std::condition_variable updateFinished; + std::mutex mutex; + + std::thread thread([&] { + auto guard = mNavigator->makeUpdateGuard(); + mNavigator->update(playerPosition, guard.get()); + std::lock_guard lock(mutex); + updated = true; + updateFinished.notify_all(); + }); + + { + std::unique_lock lock(mutex); + updateFinished.wait_for(lock, std::chrono::seconds(10), [&] { return updated; }); + ASSERT_TRUE(updated); + } + + thread.join(); + + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const auto recastMeshTiles = mNavigator->getRecastMeshTiles(); + ASSERT_EQ(recastMeshTiles.size(), 854); + EXPECT_EQ(getMinMax(recastMeshTiles), std::pair(TilePosition(-12, -12), TilePosition(18, 19))); + + const auto navMesh = mNavigator->getNavMesh(mAgentBounds); + ASSERT_NE(navMesh, nullptr); + + std::size_t usedNavMeshTiles = 0; + navMesh->lockConst()->forEachUsedTile([&](const auto&...) { ++usedNavMeshTiles; }); + EXPECT_EQ(usedNavMeshTiles, 854); + } + + struct DetourNavigatorNavigatorNotSupportedAgentBoundsTest : TestWithParam + { + }; + + TEST_P(DetourNavigatorNavigatorNotSupportedAgentBoundsTest, on_add_agent) + { + const Settings settings = makeSettings(); + NavigatorImpl navigator(settings, nullptr); + EXPECT_FALSE(navigator.addAgent(GetParam())); + } + + const std::array notSupportedAgentBounds = { + AgentBounds{ .mShapeType = CollisionShapeType::Aabb, .mHalfExtents = osg::Vec3f(0, 0, 0) }, + AgentBounds{ .mShapeType = CollisionShapeType::RotatingBox, .mHalfExtents = osg::Vec3f(0, 0, 0) }, + AgentBounds{ .mShapeType = CollisionShapeType::Cylinder, .mHalfExtents = osg::Vec3f(0, 0, 0) }, + AgentBounds{ .mShapeType = CollisionShapeType::Aabb, .mHalfExtents = osg::Vec3f(0, 0, 11.34f) }, + AgentBounds{ .mShapeType = CollisionShapeType::RotatingBox, .mHalfExtents = osg::Vec3f(0, 11.34f, 11.34f) }, + AgentBounds{ .mShapeType = CollisionShapeType::Cylinder, .mHalfExtents = osg::Vec3f(0, 0, 11.34f) }, + AgentBounds{ .mShapeType = CollisionShapeType::Aabb, .mHalfExtents = osg::Vec3f(1, 1, 0) }, + AgentBounds{ .mShapeType = CollisionShapeType::RotatingBox, .mHalfExtents = osg::Vec3f(1, 1, 0) }, + AgentBounds{ .mShapeType = CollisionShapeType::Cylinder, .mHalfExtents = osg::Vec3f(1, 1, 0) }, + AgentBounds{ .mShapeType = CollisionShapeType::Aabb, .mHalfExtents = osg::Vec3f(1, 1, 11.33f) }, + AgentBounds{ .mShapeType = CollisionShapeType::RotatingBox, .mHalfExtents = osg::Vec3f(1, 1, 11.33f) }, + AgentBounds{ .mShapeType = CollisionShapeType::Cylinder, .mHalfExtents = osg::Vec3f(1, 1, 11.33f) }, + AgentBounds{ .mShapeType = CollisionShapeType::Aabb, .mHalfExtents = osg::Vec3f(2043.54f, 2043.54f, 11.34f) }, + AgentBounds{ .mShapeType = CollisionShapeType::RotatingBox, .mHalfExtents = osg::Vec3f(2890, 1, 11.34f) }, + AgentBounds{ .mShapeType = CollisionShapeType::Cylinder, .mHalfExtents = osg::Vec3f(2890, 2890, 11.34f) }, + }; + + INSTANTIATE_TEST_SUITE_P(NotSupportedAgentBounds, DetourNavigatorNavigatorNotSupportedAgentBoundsTest, + ValuesIn(notSupportedAgentBounds)); + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nav_mesh_position) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 0); + const osg::Vec3f searchAreaHalfExtents(1000, 1000, 1000); + EXPECT_THAT(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_walk), + Optional(Vec3fEq(250, 250, -62.5186))); + } + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nullopt_when_too_far) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 250); + const osg::Vec3f searchAreaHalfExtents(100, 100, 100); + EXPECT_EQ(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_walk), + std::nullopt); + } + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nullopt_when_flags_do_not_match) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 0); + const osg::Vec3f searchAreaHalfExtents(1000, 1000, 1000); + EXPECT_EQ(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_swim), + std::nullopt); + } + + struct DetourNavigatorUpdateTest : TestWithParam> + { + }; + + std::vector getUsedTiles(const NavMeshCacheItem& navMesh) + { + std::vector result; + navMesh.forEachUsedTile([&](const TilePosition& position, const auto&...) { result.push_back(position); }); + return result; + } + + TEST_P(DetourNavigatorUpdateTest, update_should_change_covered_area_when_player_moves) + { + Loading::Listener listener; + Settings settings = makeSettings(); + settings.mMaxTilesNumber = 5; + NavigatorImpl navigator(settings, nullptr); + const AgentBounds agentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } }; + ASSERT_TRUE(navigator.addAgent(agentBounds)); + + GetParam()(navigator); + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(3000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::allJobsDone, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTiles[] = { { 3, 4 }, { 4, 3 }, { 4, 4 }, { 4, 5 }, { 5, 4 } }; + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, UnorderedElementsAreArray(expectedTiles)) << usedTiles; + } + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(4000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::allJobsDone, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTiles[] = { { 4, 4 }, { 5, 3 }, { 5, 4 }, { 5, 5 }, { 6, 4 } }; + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, UnorderedElementsAreArray(expectedTiles)) << usedTiles; + } + } + + TEST_P(DetourNavigatorUpdateTest, update_should_change_covered_area_when_player_moves_without_waiting_for_all) + { + Loading::Listener listener; + Settings settings = makeSettings(); + settings.mMaxTilesNumber = 1; + settings.mWaitUntilMinDistanceToPlayer = 1; + NavigatorImpl navigator(settings, nullptr); + const AgentBounds agentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } }; + ASSERT_TRUE(navigator.addAgent(agentBounds)); + + GetParam()(navigator); + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(3000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::requiredTilesPresent, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTile(4, 4); + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, Contains(expectedTile)) << usedTiles; + } + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(6000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::requiredTilesPresent, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTile(8, 4); + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, Contains(expectedTile)) << usedTiles; + } + } + + TEST_P(DetourNavigatorUpdateTest, update_should_change_covered_area_when_player_moves_with_db) + { + Loading::Listener listener; + Settings settings = makeSettings(); + settings.mMaxTilesNumber = 1; + settings.mWaitUntilMinDistanceToPlayer = 1; + NavigatorImpl navigator(settings, std::make_unique(":memory:", settings.mMaxDbFileSize)); + const AgentBounds agentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } }; + ASSERT_TRUE(navigator.addAgent(agentBounds)); + + GetParam()(navigator); + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(3000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::requiredTilesPresent, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTile(4, 4); + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, Contains(expectedTile)) << usedTiles; + } + + { + auto updateGuard = navigator.makeUpdateGuard(); + navigator.update(osg::Vec3f(6000, 3000, 0), updateGuard.get()); + } + + navigator.wait(WaitConditionType::requiredTilesPresent, &listener); + + { + const auto navMesh = navigator.getNavMesh(agentBounds); + ASSERT_NE(navMesh, nullptr); + + const TilePosition expectedTile(8, 4); + const auto usedTiles = getUsedTiles(*navMesh->lockConst()); + EXPECT_THAT(usedTiles, Contains(expectedTile)) << usedTiles; + } + } + + struct AddHeightfieldSurface + { + static constexpr std::size_t sSize = 65; + static constexpr float sHeights[sSize * sSize]{}; + + void operator()(Navigator& navigator) const + { + const osg::Vec2i cellPosition(0, 0); + const HeightfieldSurface surface{ + .mHeights = sHeights, + .mSize = sSize, + .mMinHeight = -1, + .mMaxHeight = 1, + }; + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + navigator.addHeightfield(cellPosition, cellSize, surface, nullptr); + } + }; + + struct AddHeightfieldPlane + { + void operator()(Navigator& navigator) const + { + const osg::Vec2i cellPosition(0, 0); + const HeightfieldPlane plane{ .mHeight = 0 }; + const int cellSize = 8192; + navigator.addHeightfield(cellPosition, cellSize, plane, nullptr); + } + }; + + struct AddWater + { + void operator()(Navigator& navigator) const + { + const osg::Vec2i cellPosition(0, 0); + const float level = 0; + const int cellSize = 8192; + navigator.addWater(cellPosition, cellSize, level, nullptr); + } + }; + + struct AddObject + { + const float mSize = 8192; + CollisionShapeInstance mBox{ std::make_unique(btVector3(mSize, mSize, 1)) }; + const ObjectTransform mTransform{ + .mPosition = ESM::Position{ .pos = { 0, 0, 0 }, .rot{ 0, 0, 0 } }, + .mScale = 1.0f, + }; + + void operator()(Navigator& navigator) const + { + navigator.addObject(ObjectId(&mBox.shape()), ObjectShapes(mBox.instance(), mTransform), + btTransform::getIdentity(), nullptr); + } + }; + + struct AddAll + { + AddHeightfieldSurface mAddHeightfieldSurface; + AddHeightfieldPlane mAddHeightfieldPlane; + AddWater mAddWater; + AddObject mAddObject; + + void operator()(Navigator& navigator) const + { + mAddHeightfieldSurface(navigator); + mAddHeightfieldPlane(navigator); + mAddWater(navigator); + mAddObject(navigator); + } + }; + + const std::function addNavMeshData[] = { + AddHeightfieldSurface{}, + AddHeightfieldPlane{}, + AddWater{}, + AddObject{}, + AddAll{}, + }; + + INSTANTIATE_TEST_SUITE_P(DifferentNavMeshData, DetourNavigatorUpdateTest, ValuesIn(addNavMeshData)); +} diff --git a/apps/components_tests/detournavigator/navmeshdb.cpp b/apps/components_tests/detournavigator/navmeshdb.cpp new file mode 100644 index 00000000000..cd74983b0ea --- /dev/null +++ b/apps/components_tests/detournavigator/navmeshdb.cpp @@ -0,0 +1,179 @@ +#include "generate.hpp" + +#include + +#include + +#include +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + using namespace DetourNavigator::Tests; + + struct Tile + { + ESM::RefId mWorldspace; + TilePosition mTilePosition; + std::vector mInput; + std::vector mData; + }; + + struct DetourNavigatorNavMeshDbTest : Test + { + NavMeshDb mDb{ ":memory:", std::numeric_limits::max() }; + std::minstd_rand mRandom; + + std::vector generateData() + { + std::vector data(32); + generateRange(data.begin(), data.end(), mRandom); + return data; + } + + Tile insertTile(TileId tileId, TileVersion version) + { + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); + const TilePosition tilePosition{ 3, 4 }; + std::vector input = generateData(); + std::vector data = generateData(); + EXPECT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1); + return { worldspace, tilePosition, std::move(input), std::move(data) }; + } + }; + + TEST_F(DetourNavigatorNavMeshDbTest, get_max_tile_id_for_empty_db_should_return_zero) + { + EXPECT_EQ(mDb.getMaxTileId(), TileId{ 0 }); + } + + TEST_F(DetourNavigatorNavMeshDbTest, inserted_tile_should_be_found_by_key) + { + const TileId tileId{ 146 }; + const TileVersion version{ 1 }; + const auto [worldspace, tilePosition, input, data] = insertTile(tileId, version); + const auto result = mDb.findTile(worldspace, tilePosition, input); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->mTileId, tileId); + EXPECT_EQ(result->mVersion, version); + } + + TEST_F(DetourNavigatorNavMeshDbTest, inserted_tile_should_change_max_tile_id) + { + insertTile(TileId{ 53 }, TileVersion{ 1 }); + EXPECT_EQ(mDb.getMaxTileId(), TileId{ 53 }); + } + + TEST_F(DetourNavigatorNavMeshDbTest, updated_tile_should_change_data) + { + const TileId tileId{ 13 }; + const TileVersion version{ 1 }; + auto [worldspace, tilePosition, input, data] = insertTile(tileId, version); + generateRange(data.begin(), data.end(), mRandom); + ASSERT_EQ(mDb.updateTile(tileId, version, data), 1); + const auto row = mDb.getTileData(worldspace, tilePosition, input); + ASSERT_TRUE(row.has_value()); + EXPECT_EQ(row->mTileId, tileId); + EXPECT_EQ(row->mVersion, version); + ASSERT_FALSE(row->mData.empty()); + EXPECT_EQ(row->mData, data); + } + + TEST_F(DetourNavigatorNavMeshDbTest, on_inserted_duplicate_should_throw_exception) + { + const TileId tileId{ 53 }; + const TileVersion version{ 1 }; + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); + const TilePosition tilePosition{ 3, 4 }; + const std::vector input = generateData(); + const std::vector data = generateData(); + ASSERT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1); + EXPECT_THROW(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), std::runtime_error); + } + + TEST_F(DetourNavigatorNavMeshDbTest, inserted_duplicate_leaves_db_in_correct_state) + { + const TileId tileId{ 53 }; + const TileVersion version{ 1 }; + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); + const TilePosition tilePosition{ 3, 4 }; + const std::vector input = generateData(); + const std::vector data = generateData(); + ASSERT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1); + EXPECT_THROW(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), std::runtime_error); + EXPECT_NO_THROW(insertTile(TileId{ 54 }, version)); + } + + TEST_F(DetourNavigatorNavMeshDbTest, delete_tiles_at_should_remove_all_tiles_with_given_worldspace_and_position) + { + const TileVersion version{ 1 }; + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); + const TilePosition tilePosition{ 3, 4 }; + const std::vector input1 = generateData(); + const std::vector input2 = generateData(); + const std::vector data = generateData(); + ASSERT_EQ(mDb.insertTile(TileId{ 53 }, worldspace, tilePosition, version, input1, data), 1); + ASSERT_EQ(mDb.insertTile(TileId{ 54 }, worldspace, tilePosition, version, input2, data), 1); + ASSERT_EQ(mDb.deleteTilesAt(worldspace, tilePosition), 2); + EXPECT_FALSE(mDb.findTile(worldspace, tilePosition, input1).has_value()); + EXPECT_FALSE(mDb.findTile(worldspace, tilePosition, input2).has_value()); + } + + TEST_F(DetourNavigatorNavMeshDbTest, delete_tiles_at_except_should_leave_tile_with_given_id) + { + const TileId leftTileId{ 53 }; + const TileId removedTileId{ 54 }; + const TileVersion version{ 1 }; + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); + const TilePosition tilePosition{ 3, 4 }; + const std::vector leftInput = generateData(); + const std::vector removedInput = generateData(); + const std::vector data = generateData(); + ASSERT_EQ(mDb.insertTile(leftTileId, worldspace, tilePosition, version, leftInput, data), 1); + ASSERT_EQ(mDb.insertTile(removedTileId, worldspace, tilePosition, version, removedInput, data), 1); + ASSERT_EQ(mDb.deleteTilesAtExcept(worldspace, tilePosition, leftTileId), 1); + const auto left = mDb.findTile(worldspace, tilePosition, leftInput); + ASSERT_TRUE(left.has_value()); + EXPECT_EQ(left->mTileId, leftTileId); + EXPECT_FALSE(mDb.findTile(worldspace, tilePosition, removedInput).has_value()); + } + + TEST_F(DetourNavigatorNavMeshDbTest, delete_tiles_outside_range_should_leave_tiles_inside_given_rectangle) + { + TileId tileId{ 1 }; + const TileVersion version{ 1 }; + const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default"); + const std::vector input = generateData(); + const std::vector data = generateData(); + for (int x = -2; x <= 2; ++x) + { + for (int y = -2; y <= 2; ++y) + { + ASSERT_EQ(mDb.insertTile(tileId, worldspace, TilePosition{ x, y }, version, input, data), 1); + ++tileId; + } + } + const TilesPositionsRange range{ TilePosition{ -1, -1 }, TilePosition{ 2, 2 } }; + ASSERT_EQ(mDb.deleteTilesOutsideRange(worldspace, range), 16); + for (int x = -2; x <= 2; ++x) + for (int y = -2; y <= 2; ++y) + ASSERT_EQ(mDb.findTile(worldspace, TilePosition{ x, y }, input).has_value(), + -1 <= x && x <= 1 && -1 <= y && y <= 1) + << "x=" << x << " y=" << y; + } + + TEST_F(DetourNavigatorNavMeshDbTest, should_support_file_size_limit) + { + mDb = NavMeshDb(":memory:", 4096); + const auto f = [&] { + for (std::int64_t i = 1; i <= 100; ++i) + insertTile(TileId{ i }, TileVersion{ 1 }); + }; + EXPECT_THROW(f(), std::runtime_error); + } +} diff --git a/apps/components_tests/detournavigator/navmeshtilescache.cpp b/apps/components_tests/detournavigator/navmeshtilescache.cpp new file mode 100644 index 00000000000..3bd3deba65f --- /dev/null +++ b/apps/components_tests/detournavigator/navmeshtilescache.cpp @@ -0,0 +1,370 @@ +#include "generate.hpp" + +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + using namespace DetourNavigator::Tests; + + template + void generateRecastArray(T*& values, int size, Random& random) + { + values = static_cast(permRecastAlloc(size * sizeof(T))); + generateRange(values, values + static_cast(size), random); + } + + template + void generate(rcPolyMesh& value, int size, Random& random) + { + value.nverts = size; + value.maxpolys = size; + value.nvp = size; + value.npolys = size; + rcVcopy(value.bmin, osg::Vec3f(-1, -2, -3).ptr()); + rcVcopy(value.bmax, osg::Vec3f(3, 2, 1).ptr()); + generateValue(value.cs, random); + generateValue(value.ch, random); + generateValue(value.borderSize, random); + generateValue(value.maxEdgeError, random); + generateRecastArray(value.verts, getVertsLength(value), random); + generateRecastArray(value.polys, getPolysLength(value), random); + generateRecastArray(value.regs, getRegsLength(value), random); + generateRecastArray(value.flags, getFlagsLength(value), random); + generateRecastArray(value.areas, getAreasLength(value), random); + } + + template + void generate(rcPolyMeshDetail& value, int size, Random& random) + { + value.nmeshes = size; + value.nverts = size; + value.ntris = size; + generateRecastArray(value.meshes, getMeshesLength(value), random); + generateRecastArray(value.verts, getVertsLength(value), random); + generateRecastArray(value.tris, getTrisLength(value), random); + } + + template + void generate(PreparedNavMeshData& value, int size, Random& random) + { + generateValue(value.mUserId, random); + generateValue(value.mCellHeight, random); + generateValue(value.mCellSize, random); + generate(value.mPolyMesh, size, random); + generate(value.mPolyMeshDetail, size, random); + } + + std::unique_ptr makePeparedNavMeshData(int size) + { + std::minstd_rand random; + auto result = std::make_unique(); + generate(*result, size, random); + return result; + } + + std::unique_ptr clone(const PreparedNavMeshData& value) + { + return std::make_unique(value); + } + + Mesh makeMesh() + { + std::vector indices{ { 0, 1, 2 } }; + std::vector vertices{ { 0, 0, 0, 1, 0, 0, 1, 1, 0 } }; + std::vector areaTypes{ 1, AreaType_ground }; + return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes)); + } + + struct DetourNavigatorNavMeshTilesCacheTest : Test + { + const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, { 1, 2, 3 } }; + const TilePosition mTilePosition{ 0, 0 }; + const Version mVersion{ 0, 0 }; + const Mesh mMesh{ makeMesh() }; + const std::vector mWater{}; + const std::vector mHeightfields{}; + const std::vector mFlatHeightfields{}; + const std::vector mSources{}; + const RecastMesh mRecastMesh{ mVersion, mMesh, mWater, mHeightfields, mFlatHeightfields, mSources }; + std::unique_ptr mPreparedNavMeshData{ makePeparedNavMeshData(3) }; + + const std::size_t mRecastMeshSize = sizeof(mRecastMesh) + getSize(mRecastMesh); + const std::size_t mRecastMeshWithWaterSize = mRecastMeshSize + sizeof(CellWater); + const std::size_t mPreparedNavMeshDataSize = sizeof(*mPreparedNavMeshData) + getSize(*mPreparedNavMeshData); + }; + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_empty_cache_should_return_empty_value) + { + const std::size_t maxSize = 0; + NavMeshTilesCache cache(maxSize); + + EXPECT_FALSE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_for_not_enought_cache_size_should_return_empty_value) + { + const std::size_t maxSize = 0; + NavMeshTilesCache cache(maxSize); + + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData))); + EXPECT_NE(mPreparedNavMeshData, nullptr); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_return_cached_value) + { + const std::size_t maxSize = mRecastMeshSize + mPreparedNavMeshDataSize; + NavMeshTilesCache cache(maxSize); + const auto copy = clone(*mPreparedNavMeshData); + ASSERT_EQ(*mPreparedNavMeshData, *copy); + + const auto result = cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + ASSERT_TRUE(result); + EXPECT_EQ(result.get(), *copy); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_existing_element_should_return_cached_element) + { + const std::size_t maxSize = 2 * (mRecastMeshSize + mPreparedNavMeshDataSize); + NavMeshTilesCache cache(maxSize); + auto copy = clone(*mPreparedNavMeshData); + const auto sameCopy = clone(*mPreparedNavMeshData); + + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_EQ(mPreparedNavMeshData, nullptr); + const auto result = cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(copy)); + ASSERT_TRUE(result); + EXPECT_EQ(result.get(), *sameCopy); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_should_return_cached_value) + { + const std::size_t maxSize = mRecastMeshSize + mPreparedNavMeshDataSize; + NavMeshTilesCache cache(maxSize); + const auto copy = clone(*mPreparedNavMeshData); + + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + const auto result = cache.get(mAgentBounds, mTilePosition, mRecastMesh); + ASSERT_TRUE(result); + EXPECT_EQ(result.get(), *copy); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_agent_half_extents_should_return_empty_value) + { + const std::size_t maxSize = 1; + NavMeshTilesCache cache(maxSize); + const AgentBounds absentAgentBounds{ CollisionShapeType::Aabb, { 1, 1, 1 } }; + + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.get(absentAgentBounds, mTilePosition, mRecastMesh)); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_tile_position_should_return_empty_value) + { + const std::size_t maxSize = 1; + NavMeshTilesCache cache(maxSize); + const TilePosition unexistentTilePosition{ 1, 1 }; + + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.get(mAgentBounds, unexistentTilePosition, mRecastMesh)); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_recast_mesh_should_return_empty_value) + { + const std::size_t maxSize = 1; + NavMeshTilesCache cache(maxSize); + const std::vector water(1, CellWater{ osg::Vec2i(), Water{ 1, 0.0f } }); + const RecastMesh unexistentRecastMesh(mVersion, mMesh, water, mHeightfields, mFlatHeightfields, mSources); + + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.get(mAgentBounds, mTilePosition, unexistentRecastMesh)); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_value) + { + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; + NavMeshTilesCache cache(maxSize); + + const std::vector water(1, CellWater{ osg::Vec2i(), Water{ 1, 0.0f } }); + const RecastMesh anotherRecastMesh(mVersion, mMesh, water, mHeightfields, mFlatHeightfields, mSources); + auto anotherPreparedNavMeshData = makePeparedNavMeshData(3); + const auto copy = clone(*anotherPreparedNavMeshData); + + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + const auto result + = cache.set(mAgentBounds, mTilePosition, anotherRecastMesh, std::move(anotherPreparedNavMeshData)); + ASSERT_TRUE(result); + EXPECT_EQ(result.get(), *copy); + EXPECT_FALSE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_used_value) + { + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; + NavMeshTilesCache cache(maxSize); + + const std::vector water(1, CellWater{ osg::Vec2i(), Water{ 1, 0.0f } }); + const RecastMesh anotherRecastMesh(mVersion, mMesh, water, mHeightfields, mFlatHeightfields, mSources); + auto anotherPreparedNavMeshData = makePeparedNavMeshData(3); + + const auto value = cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, anotherRecastMesh, std::move(anotherPreparedNavMeshData))); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_least_recently_set_value) + { + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); + NavMeshTilesCache cache(maxSize); + const auto copy = clone(*mPreparedNavMeshData); + + const std::vector leastRecentlySetWater(1, CellWater{ osg::Vec2i(), Water{ 1, 0.0f } }); + const RecastMesh leastRecentlySetRecastMesh( + mVersion, mMesh, leastRecentlySetWater, mHeightfields, mFlatHeightfields, mSources); + auto leastRecentlySetData = makePeparedNavMeshData(3); + + const std::vector mostRecentlySetWater(1, CellWater{ osg::Vec2i(), Water{ 2, 0.0f } }); + const RecastMesh mostRecentlySetRecastMesh( + mVersion, mMesh, mostRecentlySetWater, mHeightfields, mFlatHeightfields, mSources); + auto mostRecentlySetData = makePeparedNavMeshData(3); + + ASSERT_TRUE( + cache.set(mAgentBounds, mTilePosition, leastRecentlySetRecastMesh, std::move(leastRecentlySetData))); + ASSERT_TRUE(cache.set(mAgentBounds, mTilePosition, mostRecentlySetRecastMesh, std::move(mostRecentlySetData))); + + const auto result = cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_EQ(result.get(), *copy); + + EXPECT_FALSE(cache.get(mAgentBounds, mTilePosition, leastRecentlySetRecastMesh)); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mostRecentlySetRecastMesh)); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_least_recently_used_value) + { + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); + NavMeshTilesCache cache(maxSize); + + const std::vector leastRecentlyUsedWater(1, CellWater{ osg::Vec2i(), Water{ 1, 0.0f } }); + const RecastMesh leastRecentlyUsedRecastMesh( + mVersion, mMesh, leastRecentlyUsedWater, mHeightfields, mFlatHeightfields, mSources); + auto leastRecentlyUsedData = makePeparedNavMeshData(3); + const auto leastRecentlyUsedCopy = clone(*leastRecentlyUsedData); + + const std::vector mostRecentlyUsedWater(1, CellWater{ osg::Vec2i(), Water{ 2, 0.0f } }); + const RecastMesh mostRecentlyUsedRecastMesh( + mVersion, mMesh, mostRecentlyUsedWater, mHeightfields, mFlatHeightfields, mSources); + auto mostRecentlyUsedData = makePeparedNavMeshData(3); + const auto mostRecentlyUsedCopy = clone(*mostRecentlyUsedData); + + cache.set(mAgentBounds, mTilePosition, leastRecentlyUsedRecastMesh, std::move(leastRecentlyUsedData)); + cache.set(mAgentBounds, mTilePosition, mostRecentlyUsedRecastMesh, std::move(mostRecentlyUsedData)); + + { + const auto value = cache.get(mAgentBounds, mTilePosition, leastRecentlyUsedRecastMesh); + ASSERT_TRUE(value); + ASSERT_EQ(value.get(), *leastRecentlyUsedCopy); + } + + { + const auto value = cache.get(mAgentBounds, mTilePosition, mostRecentlyUsedRecastMesh); + ASSERT_TRUE(value); + ASSERT_EQ(value.get(), *mostRecentlyUsedCopy); + } + + const auto copy = clone(*mPreparedNavMeshData); + const auto result = cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_EQ(result.get(), *copy); + + EXPECT_FALSE(cache.get(mAgentBounds, mTilePosition, leastRecentlyUsedRecastMesh)); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mostRecentlyUsedRecastMesh)); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, + set_should_not_replace_unused_least_recently_used_value_when_item_does_not_not_fit_cache_max_size) + { + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); + NavMeshTilesCache cache(maxSize); + + const std::vector water(1, CellWater{ osg::Vec2i(), Water{ 1, 0.0f } }); + const RecastMesh tooLargeRecastMesh(mVersion, mMesh, water, mHeightfields, mFlatHeightfields, mSources); + auto tooLargeData = makePeparedNavMeshData(10); + + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, tooLargeRecastMesh, std::move(tooLargeData))); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, + set_should_not_replace_unused_least_recently_used_value_when_item_does_not_not_fit_size_of_unused_items) + { + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); + NavMeshTilesCache cache(maxSize); + + const std::vector anotherWater(1, CellWater{ osg::Vec2i(), Water{ 1, 0.0f } }); + const RecastMesh anotherRecastMesh(mVersion, mMesh, anotherWater, mHeightfields, mFlatHeightfields, mSources); + auto anotherData = makePeparedNavMeshData(3); + + const std::vector tooLargeWater(1, CellWater{ osg::Vec2i(), Water{ 2, 0.0f } }); + const RecastMesh tooLargeRecastMesh(mVersion, mMesh, tooLargeWater, mHeightfields, mFlatHeightfields, mSources); + auto tooLargeData = makePeparedNavMeshData(10); + + const auto value = cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + ASSERT_TRUE(value); + ASSERT_TRUE(cache.set(mAgentBounds, mTilePosition, anotherRecastMesh, std::move(anotherData))); + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, tooLargeRecastMesh, std::move(tooLargeData))); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, anotherRecastMesh)); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, + release_used_after_set_then_used_by_get_item_should_left_this_item_available) + { + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; + NavMeshTilesCache cache(maxSize); + + const std::vector water(1, CellWater{ osg::Vec2i(), Water{ 1, 0.0f } }); + const RecastMesh anotherRecastMesh(mVersion, mMesh, water, mHeightfields, mFlatHeightfields, mSources); + auto anotherData = makePeparedNavMeshData(3); + + const auto firstCopy = cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + ASSERT_TRUE(firstCopy); + { + const auto secondCopy = cache.get(mAgentBounds, mTilePosition, mRecastMesh); + ASSERT_TRUE(secondCopy); + } + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, anotherRecastMesh, std::move(anotherData))); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); + } + + TEST_F(DetourNavigatorNavMeshTilesCacheTest, release_twice_used_item_should_left_this_item_available) + { + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; + NavMeshTilesCache cache(maxSize); + + const std::vector water(1, CellWater{ osg::Vec2i(), Water{ 1, 0.0f } }); + const RecastMesh anotherRecastMesh(mVersion, mMesh, water, mHeightfields, mFlatHeightfields, mSources); + auto anotherData = makePeparedNavMeshData(3); + + cache.set(mAgentBounds, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + const auto firstCopy = cache.get(mAgentBounds, mTilePosition, mRecastMesh); + ASSERT_TRUE(firstCopy); + { + const auto secondCopy = cache.get(mAgentBounds, mTilePosition, mRecastMesh); + ASSERT_TRUE(secondCopy); + } + EXPECT_FALSE(cache.set(mAgentBounds, mTilePosition, anotherRecastMesh, std::move(anotherData))); + EXPECT_TRUE(cache.get(mAgentBounds, mTilePosition, mRecastMesh)); + } +} diff --git a/apps/components_tests/detournavigator/operators.hpp b/apps/components_tests/detournavigator/operators.hpp new file mode 100644 index 00000000000..4c043027eb6 --- /dev/null +++ b/apps/components_tests/detournavigator/operators.hpp @@ -0,0 +1,106 @@ +#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_OPERATORS_H +#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_OPERATORS_H + +#include +#include + +#include +#include +#include + +#include + +namespace +{ + template + struct Wrapper + { + const T& mValue; + }; + + template + inline testing::Message& writeRange(testing::Message& message, const Range& range, std::size_t newLine) + { + message << "{"; + std::size_t i = 0; + for (const auto& v : range) + { + if (i++ % newLine == 0) + message << "\n"; + message << Wrapper::type>{ v } << ", "; + } + return message << "\n}"; + } +} + +namespace testing +{ + template <> + inline testing::Message& Message::operator<<(const osg::Vec3f& value) + { + return (*this) << std::setprecision(std::numeric_limits::max_exponent10) << "Vec3fEq(" << value.x() + << ", " << value.y() << ", " << value.z() << ')'; + } + + template <> + inline testing::Message& Message::operator<<(const osg::Vec2i& value) + { + return (*this) << "{" << value.x() << ", " << value.y() << '}'; + } + + template <> + inline testing::Message& Message::operator<<(const Wrapper& value) + { + return (*this) << value.mValue; + } + + template <> + inline testing::Message& Message::operator<<(const Wrapper& value) + { + return (*this) << value.mValue; + } + + template <> + inline testing::Message& Message::operator<<(const Wrapper& value) + { + return (*this) << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue; + } + + template <> + inline testing::Message& Message::operator<<(const Wrapper& value) + { + return (*this) << value.mValue; + } + + template <> + inline testing::Message& Message::operator<<(const std::deque& value) + { + return writeRange(*this, value, 1); + } + + template <> + inline testing::Message& Message::operator<<(const std::vector& value) + { + return writeRange(*this, value, 1); + } + + template <> + inline testing::Message& Message::operator<<(const std::vector& value) + { + return writeRange(*this, value, 1); + } + + template <> + inline testing::Message& Message::operator<<(const std::vector& value) + { + return writeRange(*this, value, 3); + } + + template <> + inline testing::Message& Message::operator<<(const std::vector& value) + { + return writeRange(*this, value, 3); + } +} + +#endif diff --git a/apps/components_tests/detournavigator/recastmeshbuilder.cpp b/apps/components_tests/detournavigator/recastmeshbuilder.cpp new file mode 100644 index 00000000000..89a097820d6 --- /dev/null +++ b/apps/components_tests/detournavigator/recastmeshbuilder.cpp @@ -0,0 +1,564 @@ +#include "operators.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +namespace DetourNavigator +{ + static inline bool operator==(const Water& lhs, const Water& rhs) + { + const auto tie = [](const Water& v) { return std::tie(v.mCellSize, v.mLevel); }; + return tie(lhs) == tie(rhs); + } + + static inline bool operator==(const CellWater& lhs, const CellWater& rhs) + { + const auto tie = [](const CellWater& v) { return std::tie(v.mCellPosition, v.mWater); }; + return tie(lhs) == tie(rhs); + } + + static inline bool operator==(const Heightfield& lhs, const Heightfield& rhs) + { + return makeTuple(lhs) == makeTuple(rhs); + } + + static inline bool operator==(const FlatHeightfield& lhs, const FlatHeightfield& rhs) + { + const auto tie = [](const FlatHeightfield& v) { return std::tie(v.mCellPosition, v.mCellSize, v.mHeight); }; + return tie(lhs) == tie(rhs); + } +} + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + + struct DetourNavigatorRecastMeshBuilderTest : Test + { + TileBounds mBounds; + const Version mVersion{ 0, 0 }; + const osg::ref_ptr mSource{ nullptr }; + const ObjectTransform mObjectTransform{ ESM::Position{ { 0, 0, 0 }, { 0, 0, 0 } }, 0.0f }; + + DetourNavigatorRecastMeshBuilderTest() + { + mBounds.mMin = osg::Vec2f(-std::numeric_limits::max() * std::numeric_limits::epsilon(), + -std::numeric_limits::max() * std::numeric_limits::epsilon()); + mBounds.mMax = osg::Vec2f(std::numeric_limits::max() * std::numeric_limits::epsilon(), + std::numeric_limits::max() * std::numeric_limits::epsilon()); + } + }; + + TEST_F(DetourNavigatorRecastMeshBuilderTest, create_for_empty_should_return_empty) + { + RecastMeshBuilder builder(mBounds); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector()); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector()); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector()); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_bhv_triangle_mesh_shape) + { + btTriangleMesh mesh; + mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + btBvhTriangleMeshShape shape(&mesh, true); + + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground, + mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), + std::vector({ + -1, -1, 0, // vertex 0 + -1, 1, 0, // vertex 1 + 1, -1, 0, // vertex 2 + })) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 2, 1, 0 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({ AreaType_ground })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_bhv_triangle_mesh_shape) + { + btTriangleMesh mesh; + mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + btBvhTriangleMeshShape shape(&mesh, true); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), + btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), AreaType_ground, + mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), + std::vector({ + 0, 0, 3, // vertex 0 + 0, 4, 3, // vertex 1 + 2, 0, 3, // vertex 2 + })) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 2, 1, 0 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({ AreaType_ground })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_terrian_shape) + { + const std::array heightfieldData{ { 0, 0, 0, 0 } }; + btHeightfieldTerrainShape shape(2, 2, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground, + mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), + std::vector({ + -0.5, -0.5, 0, // vertex 0 + -0.5, 0.5, 0, // vertex 1 + 0.5, -0.5, 0, // vertex 2 + 0.5, 0.5, 0, // vertex 3 + })) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 0, 1, 2, 2, 1, 3 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({ AreaType_ground, AreaType_ground })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_box_shape_should_produce_12_triangles) + { + btBoxShape shape(btVector3(1, 1, 2)); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground, + mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), + std::vector({ + -1, -1, -2, // vertex 0 + -1, -1, 2, // vertex 1 + -1, 1, -2, // vertex 2 + -1, 1, 2, // vertex 3 + 1, -1, -2, // vertex 4 + 1, -1, 2, // vertex 5 + 1, 1, -2, // vertex 6 + 1, 1, 2, // vertex 7 + })) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), + std::vector({ + 0, 1, 5, // triangle 0 + 0, 2, 3, // triangle 1 + 0, 4, 6, // triangle 2 + 1, 3, 7, // triangle 3 + 2, 6, 7, // triangle 4 + 3, 1, 0, // triangle 5 + 4, 5, 7, // triangle 6 + 5, 4, 0, // triangle 7 + 6, 2, 0, // triangle 8 + 7, 3, 2, // triangle 9 + 7, 5, 1, // triangle 10 + 7, 6, 4, // triangle 11 + })) + << recastMesh->getMesh().getIndices(); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector(12, AreaType_ground)); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_compound_shape) + { + btTriangleMesh mesh1; + mesh1.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + btBvhTriangleMeshShape triangle1(&mesh1, true); + btBoxShape box(btVector3(1, 1, 2)); + btTriangleMesh mesh2; + mesh2.addTriangle(btVector3(1, 1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + btBvhTriangleMeshShape triangle2(&mesh2, true); + btCompoundShape shape; + shape.addChildShape(btTransform::getIdentity(), &triangle1); + shape.addChildShape(btTransform::getIdentity(), &box); + shape.addChildShape(btTransform::getIdentity(), &triangle2); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground, + mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), + std::vector({ + -1, -1, -2, // vertex 0 + -1, -1, 0, // vertex 1 + -1, -1, 2, // vertex 2 + -1, 1, -2, // vertex 3 + -1, 1, 0, // vertex 4 + -1, 1, 2, // vertex 5 + 1, -1, -2, // vertex 6 + 1, -1, 0, // vertex 7 + 1, -1, 2, // vertex 8 + 1, 1, -2, // vertex 9 + 1, 1, 0, // vertex 10 + 1, 1, 2, // vertex 11 + })) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), + std::vector({ + 0, 2, 8, // triangle 0 + 0, 3, 5, // triangle 1 + 0, 6, 9, // triangle 2 + 2, 5, 11, // triangle 3 + 3, 9, 11, // triangle 4 + 5, 2, 0, // triangle 5 + 6, 8, 11, // triangle 6 + 7, 4, 1, // triangle 7 + 7, 4, 10, // triangle 8 + 8, 6, 0, // triangle 9 + 9, 3, 0, // triangle 10 + 11, 5, 3, // triangle 11 + 11, 8, 2, // triangle 12 + 11, 9, 6, // triangle 13 + })) + << recastMesh->getMesh().getIndices(); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector(14, AreaType_ground)); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_compound_shape) + { + btTriangleMesh mesh; + mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + btBvhTriangleMeshShape triangle(&mesh, true); + btCompoundShape shape; + shape.addChildShape(btTransform::getIdentity(), &triangle); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), + btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), AreaType_ground, + mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), + std::vector({ + 0, 0, 3, // vertex 0 + 0, 4, 3, // vertex 1 + 2, 0, 3, // vertex 2 + })) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 2, 1, 0 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({ AreaType_ground })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_compound_shape_with_transformed_bhv_triangle_shape) + { + btTriangleMesh mesh; + mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + btBvhTriangleMeshShape triangle(&mesh, true); + btCompoundShape shape; + shape.addChildShape( + btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), &triangle); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), + btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), AreaType_ground, + mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), + std::vector({ + 1, 2, 12, // vertex 0 + 1, 10, 12, // vertex 1 + 3, 2, 12, // vertex 2 + })) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 2, 1, 0 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({ AreaType_ground })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, without_bounds_add_bhv_triangle_shape_should_not_filter_by_bounds) + { + btTriangleMesh mesh; + mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); + btBvhTriangleMeshShape shape(&mesh, true); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground, + mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), + std::vector({ + -3, -3, 0, // vertex 0 + -3, -2, 0, // vertex 1 + -2, -3, 0, // vertex 2 + -1, -1, 0, // vertex 3 + -1, 1, 0, // vertex 4 + 1, -1, 0, // vertex 5 + })) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 2, 1, 0, 5, 4, 3 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector(2, AreaType_ground)); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_bhv_triangle_shape_should_filter_by_bounds) + { + mBounds.mMin = osg::Vec2f(-3, -3); + mBounds.mMax = osg::Vec2f(-2, -2); + btTriangleMesh mesh; + mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); + btBvhTriangleMeshShape shape(&mesh, true); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground, + mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), + std::vector({ + -3, -3, 0, // vertex 0 + -3, -2, 0, // vertex 1 + -2, -3, 0, // vertex 2 + })) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 2, 1, 0 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({ AreaType_ground })); + } + + TEST_F( + DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_x_bhv_triangle_shape_should_filter_by_bounds) + { + mBounds.mMin = osg::Vec2f(-5, -5); + mBounds.mMax = osg::Vec2f(5, -2); + btTriangleMesh mesh; + mesh.addTriangle(btVector3(0, -1, -1), btVector3(0, -1, -1), btVector3(0, 1, -1)); + mesh.addTriangle(btVector3(0, -3, -3), btVector3(0, -3, -2), btVector3(0, -2, -3)); + btBvhTriangleMeshShape shape(&mesh, true); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), + btTransform(btQuaternion(btVector3(1, 0, 0), static_cast(-osg::PI_4))), AreaType_ground, mSource, + mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_THAT(recastMesh->getMesh().getVertices(), + Pointwise(FloatNear(1e-5f), + std::vector({ + 0, -4.24264049530029296875f, 4.44089209850062616169452667236328125e-16f, // vertex 0 + 0, -3.535533905029296875f, -0.707106769084930419921875f, // vertex 1 + 0, -3.535533905029296875f, 0.707106769084930419921875f, // vertex 2 + }))) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 1, 2, 0 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({ AreaType_ground })); + } + + TEST_F( + DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_y_bhv_triangle_shape_should_filter_by_bounds) + { + mBounds.mMin = osg::Vec2f(-5, -5); + mBounds.mMax = osg::Vec2f(-3, 5); + btTriangleMesh mesh; + mesh.addTriangle(btVector3(-1, 0, -1), btVector3(-1, 0, 1), btVector3(1, 0, -1)); + mesh.addTriangle(btVector3(-3, 0, -3), btVector3(-3, 0, -2), btVector3(-2, 0, -3)); + btBvhTriangleMeshShape shape(&mesh, true); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), + btTransform(btQuaternion(btVector3(0, 1, 0), static_cast(osg::PI_4))), AreaType_ground, mSource, + mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_THAT(recastMesh->getMesh().getVertices(), + Pointwise(FloatNear(1e-5f), + std::vector({ + -4.24264049530029296875f, 0, 4.44089209850062616169452667236328125e-16f, // vertex 0 + -3.535533905029296875f, 0, -0.707106769084930419921875f, // vertex 1 + -3.535533905029296875f, 0, 0.707106769084930419921875f, // vertex 2 + }))) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 1, 2, 0 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({ AreaType_ground })); + } + + TEST_F( + DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_z_bhv_triangle_shape_should_filter_by_bounds) + { + mBounds.mMin = osg::Vec2f(-5, -5); + mBounds.mMax = osg::Vec2f(-1, -1); + btTriangleMesh mesh; + mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); + btBvhTriangleMeshShape shape(&mesh, true); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), + btTransform(btQuaternion(btVector3(0, 0, 1), static_cast(osg::PI_4))), AreaType_ground, mSource, + mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_THAT(recastMesh->getMesh().getVertices(), + Pointwise(FloatNear(1e-5f), + std::vector({ + -1.41421353816986083984375f, -1.1102230246251565404236316680908203125e-16f, 0, // vertex 0 + 1.1102230246251565404236316680908203125e-16f, -1.41421353816986083984375f, 0, // vertex 1 + 1.41421353816986083984375f, 1.1102230246251565404236316680908203125e-16f, 0, // vertex 2 + }))) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 2, 0, 1 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({ AreaType_ground })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, flags_values_should_be_corresponding_to_added_objects) + { + btTriangleMesh mesh1; + mesh1.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + btBvhTriangleMeshShape shape1(&mesh1, true); + btTriangleMesh mesh2; + mesh2.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); + btBvhTriangleMeshShape shape2(&mesh2, true); + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape1), btTransform::getIdentity(), AreaType_ground, + mSource, mObjectTransform); + builder.addObject(static_cast(shape2), btTransform::getIdentity(), AreaType_null, + mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), + std::vector({ + -3, -3, 0, // vertex 0 + -3, -2, 0, // vertex 1 + -2, -3, 0, // vertex 2 + -1, -1, 0, // vertex 3 + -1, 1, 0, // vertex 4 + 1, -1, 0, // vertex 5 + })) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 2, 1, 0, 5, 4, 3 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({ AreaType_null, AreaType_ground })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_water_then_get_water_should_return_it) + { + RecastMeshBuilder builder(mBounds); + builder.addWater(osg::Vec2i(1, 2), Water{ 1000, 300.0f }); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ( + recastMesh->getWater(), std::vector({ CellWater{ osg::Vec2i(1, 2), Water{ 1000, 300.0f } } })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_bhv_triangle_mesh_shape_with_duplicated_vertices) + { + btTriangleMesh mesh; + mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + mesh.addTriangle(btVector3(1, 1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); + btBvhTriangleMeshShape shape(&mesh, true); + + RecastMeshBuilder builder(mBounds); + builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground, + mSource, mObjectTransform); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getMesh().getVertices(), + std::vector({ + -1, -1, 0, // vertex 0 + -1, 1, 0, // vertex 1 + 1, -1, 0, // vertex 2 + 1, 1, 0, // vertex 3 + })) + << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 2, 1, 0, 2, 1, 3 })); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({ AreaType_ground, AreaType_ground })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_flat_heightfield_should_add_intersection) + { + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 1000; + const float height = 10; + mBounds.mMin = osg::Vec2f(100, 100); + RecastMeshBuilder builder(mBounds); + builder.addHeightfield(cellPosition, cellSize, height); + const auto recastMesh = std::move(builder).create(mVersion); + EXPECT_EQ(recastMesh->getFlatHeightfields(), + std::vector({ + FlatHeightfield{ cellPosition, cellSize, height }, + })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_inside_tile) + { + constexpr std::size_t size = 3; + constexpr std::array heights{ { + 0, 1, 2, // row 0 + 3, 4, 5, // row 1 + 6, 7, 8, // row 2 + } }; + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 1000; + const float minHeight = 0; + const float maxHeight = 8; + RecastMeshBuilder builder(mBounds); + builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight); + const auto recastMesh = std::move(builder).create(mVersion); + Heightfield expected; + expected.mCellPosition = cellPosition; + expected.mCellSize = cellSize; + expected.mLength = size; + expected.mMinHeight = minHeight; + expected.mMaxHeight = maxHeight; + expected.mHeights.assign(heights.begin(), heights.end()); + expected.mOriginalSize = 3; + expected.mMinX = 0; + expected.mMinY = 0; + EXPECT_EQ(recastMesh->getHeightfields(), std::vector({ expected })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_to_shifted_cell_inside_tile) + { + constexpr std::size_t size = 3; + constexpr std::array heights{ { + 0, 1, 2, // row 0 + 3, 4, 5, // row 1 + 6, 7, 8, // row 2 + } }; + const osg::Vec2i cellPosition(1, 2); + const int cellSize = 1000; + const float minHeight = 0; + const float maxHeight = 8; + RecastMeshBuilder builder(maxCellTileBounds(cellPosition, cellSize)); + builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight); + const auto recastMesh = std::move(builder).create(mVersion); + Heightfield expected; + expected.mCellPosition = cellPosition; + expected.mCellSize = cellSize; + expected.mLength = size; + expected.mMinHeight = minHeight; + expected.mMaxHeight = maxHeight; + expected.mHeights.assign(heights.begin(), heights.end()); + expected.mOriginalSize = 3; + expected.mMinX = 0; + expected.mMinY = 0; + EXPECT_EQ(recastMesh->getHeightfields(), std::vector({ expected })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_should_add_intersection) + { + constexpr std::size_t size = 3; + constexpr std::array heights{ { + 0, 1, 2, // row 0 + 3, 4, 5, // row 1 + 6, 7, 8, // row 2 + } }; + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 1000; + const float minHeight = 0; + const float maxHeight = 8; + mBounds.mMin = osg::Vec2f(750, 750); + RecastMeshBuilder builder(mBounds); + builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight); + const auto recastMesh = std::move(builder).create(mVersion); + Heightfield expected; + expected.mCellPosition = cellPosition; + expected.mCellSize = cellSize; + expected.mLength = 2; + expected.mMinHeight = 0; + expected.mMaxHeight = 8; + expected.mHeights = { + 4, 5, // row 0 + 7, 8, // row 1 + }; + expected.mOriginalSize = 3; + expected.mMinX = 1; + expected.mMinY = 1; + EXPECT_EQ(recastMesh->getHeightfields(), std::vector({ expected })); + } +} diff --git a/apps/components_tests/detournavigator/recastmeshobject.cpp b/apps/components_tests/detournavigator/recastmeshobject.cpp new file mode 100644 index 00000000000..402b2fe80ca --- /dev/null +++ b/apps/components_tests/detournavigator/recastmeshobject.cpp @@ -0,0 +1,84 @@ + +#include +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + + struct DetourNavigatorRecastMeshObjectTest : Test + { + btBoxShape mBoxShapeImpl{ btVector3(1, 2, 3) }; + const ObjectTransform mObjectTransform{ ESM::Position{ { 1, 2, 3 }, { 1, 2, 3 } }, 0.5f }; + CollisionShape mBoxShape{ nullptr, mBoxShapeImpl, mObjectTransform }; + btCompoundShape mCompoundShapeImpl{ true }; + CollisionShape mCompoundShape{ nullptr, mCompoundShapeImpl, mObjectTransform }; + btTransform mTransform{ Misc::Convert::makeBulletTransform(mObjectTransform.mPosition) }; + + DetourNavigatorRecastMeshObjectTest() + { + mCompoundShapeImpl.addChildShape(mTransform, std::addressof(mBoxShapeImpl)); + } + }; + + TEST_F(DetourNavigatorRecastMeshObjectTest, constructed_object_should_have_shape_and_transform) + { + const RecastMeshObject object(mBoxShape, mTransform, AreaType_ground); + EXPECT_EQ(std::addressof(object.getShape()), std::addressof(mBoxShapeImpl)); + EXPECT_EQ(object.getTransform(), mTransform); + } + + TEST_F(DetourNavigatorRecastMeshObjectTest, update_with_same_transform_for_not_compound_shape_should_return_false) + { + RecastMeshObject object(mBoxShape, mTransform, AreaType_ground); + EXPECT_FALSE(object.update(mTransform, AreaType_ground)); + } + + TEST_F(DetourNavigatorRecastMeshObjectTest, update_with_different_transform_should_return_true) + { + RecastMeshObject object(mBoxShape, mTransform, AreaType_ground); + EXPECT_TRUE(object.update(btTransform::getIdentity(), AreaType_ground)); + } + + TEST_F(DetourNavigatorRecastMeshObjectTest, update_with_different_flags_should_return_true) + { + RecastMeshObject object(mBoxShape, mTransform, AreaType_ground); + EXPECT_TRUE(object.update(mTransform, AreaType_null)); + } + + TEST_F(DetourNavigatorRecastMeshObjectTest, + update_for_compound_shape_with_same_transform_and_not_changed_child_transform_should_return_false) + { + RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground); + EXPECT_FALSE(object.update(mTransform, AreaType_ground)); + } + + TEST_F(DetourNavigatorRecastMeshObjectTest, + update_for_compound_shape_with_same_transform_and_changed_child_transform_should_return_true) + { + RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground); + mCompoundShapeImpl.updateChildTransform(0, btTransform::getIdentity()); + EXPECT_TRUE(object.update(mTransform, AreaType_ground)); + } + + TEST_F(DetourNavigatorRecastMeshObjectTest, repeated_update_for_compound_shape_without_changes_should_return_false) + { + RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground); + mCompoundShapeImpl.updateChildTransform(0, btTransform::getIdentity()); + object.update(mTransform, AreaType_ground); + EXPECT_FALSE(object.update(mTransform, AreaType_ground)); + } + + TEST_F(DetourNavigatorRecastMeshObjectTest, update_for_changed_local_scaling_should_return_true) + { + RecastMeshObject object(mBoxShape, mTransform, AreaType_ground); + mBoxShapeImpl.setLocalScaling(btVector3(2, 2, 2)); + EXPECT_TRUE(object.update(mTransform, AreaType_ground)); + } +} diff --git a/apps/components_tests/detournavigator/settings.hpp b/apps/components_tests/detournavigator/settings.hpp new file mode 100644 index 00000000000..1ebbc5ba7ba --- /dev/null +++ b/apps/components_tests/detournavigator/settings.hpp @@ -0,0 +1,50 @@ +#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_SETTINGS_H +#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_SETTINGS_H + +#include + +#include +#include + +namespace DetourNavigator +{ + namespace Tests + { + inline Settings makeSettings() + { + Settings result; + result.mEnableWriteRecastMeshToFile = false; + result.mEnableWriteNavMeshToFile = false; + result.mEnableRecastMeshFileNameRevision = false; + result.mEnableNavMeshFileNameRevision = false; + result.mRecast.mBorderSize = 16; + result.mRecast.mCellHeight = 0.2f; + result.mRecast.mCellSize = 0.2f; + result.mRecast.mDetailSampleDist = 6; + result.mRecast.mDetailSampleMaxError = 1; + result.mRecast.mMaxClimb = 34; + result.mRecast.mMaxSimplificationError = 1.3f; + result.mRecast.mMaxSlope = 49; + result.mRecast.mRecastScaleFactor = 0.017647058823529415f; + result.mRecast.mSwimHeightScale = 0.89999997615814208984375f; + result.mRecast.mMaxEdgeLen = 12; + result.mDetour.mMaxNavMeshQueryNodes = 2048; + result.mRecast.mMaxVertsPerPoly = 6; + result.mRecast.mRegionMergeArea = 400; + result.mRecast.mRegionMinArea = 64; + result.mRecast.mTileSize = 64; + result.mWaitUntilMinDistanceToPlayer = std::numeric_limits::max(); + result.mAsyncNavMeshUpdaterThreads = 1; + result.mMaxNavMeshTilesCacheSize = 1024 * 1024; + result.mDetour.mMaxPolygonPathSize = 1024; + result.mDetour.mMaxSmoothPathSize = 1024; + result.mDetour.mMaxPolys = 4096; + result.mMaxTilesNumber = 1024; + result.mMinUpdateInterval = std::chrono::milliseconds(50); + result.mWriteToNavMeshDb = true; + return result; + } + } +} + +#endif diff --git a/apps/openmw_test_suite/detournavigator/settingsutils.cpp b/apps/components_tests/detournavigator/settingsutils.cpp similarity index 87% rename from apps/openmw_test_suite/detournavigator/settingsutils.cpp rename to apps/components_tests/detournavigator/settingsutils.cpp index ffed64ab81f..9faa03b075f 100644 --- a/apps/openmw_test_suite/detournavigator/settingsutils.cpp +++ b/apps/components_tests/detournavigator/settingsutils.cpp @@ -1,4 +1,3 @@ -#include "operators.hpp" #include @@ -11,7 +10,7 @@ namespace struct DetourNavigatorGetTilePositionTest : Test { - Settings mSettings; + RecastSettings mSettings; DetourNavigatorGetTilePositionTest() { @@ -47,7 +46,7 @@ namespace struct DetourNavigatorMakeTileBoundsTest : Test { - Settings mSettings; + RecastSettings mSettings; DetourNavigatorMakeTileBoundsTest() { @@ -58,11 +57,12 @@ namespace TEST_F(DetourNavigatorMakeTileBoundsTest, tile_bounds_depend_on_tile_size_and_cell_size) { - EXPECT_EQ(makeTileBounds(mSettings, TilePosition(0, 0)), (TileBounds {osg::Vec2f(0, 0), osg::Vec2f(32, 32)})); + EXPECT_EQ(makeTileBounds(mSettings, TilePosition(0, 0)), (TileBounds{ osg::Vec2f(0, 0), osg::Vec2f(32, 32) })); } TEST_F(DetourNavigatorMakeTileBoundsTest, tile_bounds_are_multiplied_by_tile_position) { - EXPECT_EQ(makeTileBounds(mSettings, TilePosition(1, 2)), (TileBounds {osg::Vec2f(32, 64), osg::Vec2f(64, 96)})); + EXPECT_EQ( + makeTileBounds(mSettings, TilePosition(1, 2)), (TileBounds{ osg::Vec2f(32, 64), osg::Vec2f(64, 96) })); } } diff --git a/apps/components_tests/detournavigator/tilecachedrecastmeshmanager.cpp b/apps/components_tests/detournavigator/tilecachedrecastmeshmanager.cpp new file mode 100644 index 00000000000..e1805e993cd --- /dev/null +++ b/apps/components_tests/detournavigator/tilecachedrecastmeshmanager.cpp @@ -0,0 +1,506 @@ + +#include +#include +#include + +#include + +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + + struct DetourNavigatorTileCachedRecastMeshManagerTest : Test + { + RecastSettings mSettings; + const ObjectTransform mObjectTransform{ ESM::Position{ { 0, 0, 0 }, { 0, 0, 0 } }, 0.0f }; + const osg::ref_ptr mShape = new Resource::BulletShape; + const osg::ref_ptr mInstance = new Resource::BulletShapeInstance(mShape); + const ESM::RefId mWorldspace = ESM::RefId::stringRefId("worldspace"); + + DetourNavigatorTileCachedRecastMeshManagerTest() + { + mSettings.mBorderSize = 16; + mSettings.mCellSize = 0.2f; + mSettings.mRecastScaleFactor = 0.017647058823529415f; + mSettings.mTileSize = 64; + } + }; + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_empty_should_return_nullptr) + { + TileCachedRecastMeshManager manager(mSettings); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_revision_for_empty_should_return_zero) + { + const TileCachedRecastMeshManager manager(mSettings); + EXPECT_EQ(manager.getRevision(), 0); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_for_new_object_should_return_true) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + EXPECT_TRUE(manager.addObject( + ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_for_existing_object_should_return_false) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + EXPECT_FALSE(manager.addObject( + ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_should_add_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + ASSERT_TRUE(manager.addObject( + ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); + for (int x = -1; x < 1; ++x) + for (int y = -1; y < 1; ++y) + ASSERT_NE(manager.getMesh(mWorldspace, TilePosition(x, y)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_should_return_add_changed_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + const TilesPositionsRange range{ + .mBegin = TilePosition(0, 0), + .mEnd = TilePosition(1, 1), + }; + manager.setRange(range, nullptr); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + EXPECT_THAT(manager.takeChangedTiles(nullptr), ElementsAre(std::pair(TilePosition(0, 0), ChangeType::add))); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, update_object_for_changed_object_should_add_changed_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const btTransform transform( + btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + const TilesPositionsRange range{ + .mBegin = TilePosition(-1, -1), + .mEnd = TilePosition(2, 2), + }; + manager.setRange(range, nullptr); + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, nullptr); + manager.takeChangedTiles(nullptr); + EXPECT_TRUE( + manager.updateObject(ObjectId(&boxShape), btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); + EXPECT_THAT(manager.takeChangedTiles(nullptr), + ElementsAre(std::pair(TilePosition(-1, -1), ChangeType::add), + std::pair(TilePosition(-1, 0), ChangeType::add), std::pair(TilePosition(0, -1), ChangeType::update), + std::pair(TilePosition(0, 0), ChangeType::update), std::pair(TilePosition(1, -1), ChangeType::remove), + std::pair(TilePosition(1, 0), ChangeType::remove))); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, + update_object_for_not_changed_object_should_not_add_changed_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + manager.takeChangedTiles(nullptr); + EXPECT_FALSE( + manager.updateObject(ObjectId(&boxShape), btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); + EXPECT_THAT(manager.takeChangedTiles(nullptr), IsEmpty()); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_object_should_return_add_changed_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + const TilesPositionsRange range{ + .mBegin = TilePosition(0, 0), + .mEnd = TilePosition(1, 1), + }; + manager.setRange(range, nullptr); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + manager.takeChangedTiles(nullptr); + manager.removeObject(ObjectId(&boxShape), nullptr); + EXPECT_THAT(manager.takeChangedTiles(nullptr), ElementsAre(std::pair(TilePosition(0, 0), ChangeType::remove))); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, + get_mesh_after_add_object_should_return_recast_mesh_for_each_used_tile) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); + } + + TEST_F( + DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_after_add_object_should_return_nullptr_for_unused_tile) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(1, 0)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, + get_mesh_for_moved_object_should_return_recast_mesh_for_each_used_tile) + { + TileCachedRecastMeshManager manager(mSettings); + const TilesPositionsRange range{ + .mBegin = TilePosition(-1, -1), + .mEnd = TilePosition(2, 2), + }; + manager.setRange(range, nullptr); + manager.setWorldspace(mWorldspace, nullptr); + + const btBoxShape boxShape(btVector3(20, 20, 100)); + const btTransform transform( + btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(1, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(1, -1)), nullptr); + + manager.updateObject(ObjectId(&boxShape), btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); + } + + TEST_F( + DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_moved_object_should_return_nullptr_for_unused_tile) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + + const btBoxShape boxShape(btVector3(20, 20, 100)); + const btTransform transform( + btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); + + manager.updateObject(ObjectId(&boxShape), btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(1, 0)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(1, -1)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, + get_mesh_for_removed_object_should_return_nullptr_for_all_previously_used_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + manager.removeObject(ObjectId(&boxShape), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, + get_mesh_for_not_changed_object_after_update_should_return_recast_mesh_for_same_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); + + manager.updateObject(ObjectId(&boxShape), btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(-1, 0)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, -1)), nullptr); + EXPECT_NE(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, + get_revision_after_add_object_new_should_return_incremented_value) + { + TileCachedRecastMeshManager manager(mSettings); + const auto initialRevision = manager.getRevision(); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + EXPECT_EQ(manager.getRevision(), initialRevision + 1); + } + + TEST_F( + DetourNavigatorTileCachedRecastMeshManagerTest, get_revision_after_add_object_existing_should_return_same_value) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + const auto beforeAddRevision = manager.getRevision(); + EXPECT_FALSE(manager.addObject( + ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); + EXPECT_EQ(manager.getRevision(), beforeAddRevision); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, + get_revision_after_update_moved_object_should_return_incremented_value) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const btTransform transform( + btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, nullptr); + const auto beforeUpdateRevision = manager.getRevision(); + manager.updateObject(ObjectId(&boxShape), btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + EXPECT_EQ(manager.getRevision(), beforeUpdateRevision + 1); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, + get_revision_after_update_not_changed_object_should_return_same_value) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + const auto beforeUpdateRevision = manager.getRevision(); + manager.updateObject(ObjectId(&boxShape), btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + EXPECT_EQ(manager.getRevision(), beforeUpdateRevision); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, + get_revision_after_remove_existing_object_should_return_incremented_value) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + const auto beforeRemoveRevision = manager.getRevision(); + manager.removeObject(ObjectId(&boxShape), nullptr); + EXPECT_EQ(manager.getRevision(), beforeRemoveRevision + 1); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, + get_revision_after_remove_absent_object_should_return_same_value) + { + TileCachedRecastMeshManager manager(mSettings); + const auto beforeRemoveRevision = manager.getRevision(); + manager.removeObject(ObjectId(&manager), nullptr); + EXPECT_EQ(manager.getRevision(), beforeRemoveRevision); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_new_water_should_add_changed_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + manager.addWater(cellPosition, cellSize, 0.0f, nullptr); + const auto changedTiles = manager.takeChangedTiles(nullptr); + EXPECT_EQ(changedTiles.begin()->first, TilePosition(-1, -1)); + EXPECT_EQ(changedTiles.rbegin()->first, TilePosition(11, 11)); + for (const auto& [k, v] : changedTiles) + EXPECT_EQ(v, ChangeType::add) << k; + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_not_max_int_should_add_new_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + manager.addWater(cellPosition, cellSize, 0.0f, nullptr); + for (int x = -1; x < 12; ++x) + for (int y = -1; y < 12; ++y) + ASSERT_NE(manager.getMesh(mWorldspace, TilePosition(x, y)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_max_int_should_not_add_new_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + ASSERT_TRUE(manager.addObject( + ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = std::numeric_limits::max(); + manager.addWater(cellPosition, cellSize, 0.0f, nullptr); + for (int x = -6; x < 6; ++x) + for (int y = -6; y < 6; ++y) + ASSERT_EQ(manager.getMesh(mWorldspace, TilePosition(x, y)) != nullptr, + -1 <= x && x <= 0 && -1 <= y && y <= 0); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_absent_cell_should_not_add_changed_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.removeWater(osg::Vec2i(0, 0), nullptr); + EXPECT_THAT(manager.takeChangedTiles(nullptr), ElementsAre()); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_add_changed_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + manager.addWater(cellPosition, cellSize, 0.0f, nullptr); + manager.takeChangedTiles(nullptr); + manager.removeWater(cellPosition, nullptr); + const auto changedTiles = manager.takeChangedTiles(nullptr); + EXPECT_EQ(changedTiles.begin()->first, TilePosition(-1, -1)); + EXPECT_EQ(changedTiles.rbegin()->first, TilePosition(11, 11)); + for (const auto& [k, v] : changedTiles) + EXPECT_EQ(v, ChangeType::remove) << k; + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_remove_empty_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + manager.addWater(cellPosition, cellSize, 0.0f, nullptr); + manager.removeWater(cellPosition, nullptr); + for (int x = -6; x < 6; ++x) + for (int y = -6; y < 6; ++y) + ASSERT_EQ(manager.getMesh(mWorldspace, TilePosition(x, y)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_leave_not_empty_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + ASSERT_TRUE(manager.addObject( + ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + manager.addWater(cellPosition, cellSize, 0.0f, nullptr); + manager.removeWater(cellPosition, nullptr); + for (int x = -6; x < 6; ++x) + for (int y = -6; y < 6; ++y) + ASSERT_EQ(manager.getMesh(mWorldspace, TilePosition(x, y)) != nullptr, + -1 <= x && x <= 0 && -1 <= y && y <= 0); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_object_should_not_remove_tile_with_water) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + ASSERT_TRUE(manager.addObject( + ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); + manager.addWater(cellPosition, cellSize, 0.0f, nullptr); + manager.removeObject(ObjectId(&boxShape), nullptr); + for (int x = -1; x < 12; ++x) + for (int y = -1; y < 12; ++y) + ASSERT_NE(manager.getMesh(mWorldspace, TilePosition(x, y)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_new_worldspace_should_remove_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + manager.setWorldspace(mWorldspace, nullptr); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(nullptr, boxShape, mObjectTransform); + ASSERT_TRUE(manager.addObject( + ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr)); + const ESM::RefId otherWorldspace(ESM::FormId::fromUint32(0x1)); + manager.setWorldspace(ESM::FormId::fromUint32(0x1), nullptr); + for (int x = -1; x < 1; ++x) + for (int y = -1; y < 1; ++y) + ASSERT_EQ(manager.getMesh(otherWorldspace, TilePosition(x, y)), nullptr); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_range_should_add_changed_tiles) + { + TileCachedRecastMeshManager manager(mSettings); + const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + const TilesPositionsRange range1{ + .mBegin = TilePosition(0, 0), + .mEnd = TilePosition(1, 1), + }; + manager.setRange(range1, nullptr); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + const TilesPositionsRange range2{ + .mBegin = TilePosition(-1, -1), + .mEnd = TilePosition(0, 0), + }; + manager.takeChangedTiles(nullptr); + manager.setRange(range2, nullptr); + EXPECT_THAT(manager.takeChangedTiles(nullptr), + ElementsAre( + std::pair(TilePosition(-1, -1), ChangeType::add), std::pair(TilePosition(0, 0), ChangeType::remove))); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_range_should_remove_cached_recast_meshes_outside_range) + { + TileCachedRecastMeshManager manager(mSettings); + + manager.setWorldspace(mWorldspace, nullptr); + + const btBoxShape boxShape(btVector3(100, 100, 20)); + const CollisionShape shape(mInstance, boxShape, mObjectTransform); + const TilesPositionsRange range1{ + .mBegin = TilePosition(0, 0), + .mEnd = TilePosition(1, 1), + }; + manager.setRange(range1, nullptr); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, nullptr); + + const TilePosition tilePosition(0, 0); + + ASSERT_EQ(manager.getCachedMesh(mWorldspace, tilePosition), nullptr); + ASSERT_NE(manager.getMesh(mWorldspace, tilePosition), nullptr); + ASSERT_NE(manager.getCachedMesh(mWorldspace, tilePosition), nullptr); + + const TilesPositionsRange range2{ + .mBegin = TilePosition(-1, -1), + .mEnd = TilePosition(0, 0), + }; + manager.takeChangedTiles(nullptr); + manager.setRange(range2, nullptr); + + ASSERT_EQ(manager.getCachedMesh(mWorldspace, tilePosition), nullptr); + } +} diff --git a/apps/components_tests/esm/test_fixed_string.cpp b/apps/components_tests/esm/test_fixed_string.cpp new file mode 100644 index 00000000000..76ed346daab --- /dev/null +++ b/apps/components_tests/esm/test_fixed_string.cpp @@ -0,0 +1,188 @@ +#include "components/esm/defs.hpp" +#include "components/esm/esmcommon.hpp" +#include + +namespace +{ + TEST(EsmFixedString, operator__eq_ne) + { + { + SCOPED_TRACE("asdc == asdc"); + constexpr ESM::NAME name("asdc"); + char s[4] = { 'a', 's', 'd', 'c' }; + std::string ss(s, 4); + + EXPECT_TRUE(name == s); + EXPECT_TRUE(name == ss.c_str()); + EXPECT_TRUE(name == ss); + } + { + SCOPED_TRACE("asdc == asdcx"); + constexpr ESM::NAME name("asdc"); + char s[5] = { 'a', 's', 'd', 'c', 'x' }; + std::string ss(s, 5); + + EXPECT_TRUE(name != s); + EXPECT_TRUE(name != ss.c_str()); + EXPECT_TRUE(name != ss); + } + { + SCOPED_TRACE("asdc == asdc[NULL]"); + const ESM::NAME name("asdc"); + char s[5] = { 'a', 's', 'd', 'c', '\0' }; + std::string ss(s, 5); + + EXPECT_TRUE(name == s); + EXPECT_TRUE(name == ss.c_str()); + EXPECT_TRUE(name == ss); + } + } + + TEST(EsmFixedString, operator__eq_ne_const) + { + { + SCOPED_TRACE("asdc == asdc (const)"); + constexpr ESM::NAME name("asdc"); + const char s[4] = { 'a', 's', 'd', 'c' }; + std::string ss(s, 4); + + EXPECT_TRUE(name == s); + EXPECT_TRUE(name == ss.c_str()); + EXPECT_TRUE(name == ss); + } + { + SCOPED_TRACE("asdc == asdcx (const)"); + constexpr ESM::NAME name("asdc"); + const char s[5] = { 'a', 's', 'd', 'c', 'x' }; + std::string ss(s, 5); + + EXPECT_TRUE(name != s); + EXPECT_TRUE(name != ss.c_str()); + EXPECT_TRUE(name != ss); + } + { + SCOPED_TRACE("asdc == asdc[NULL] (const)"); + constexpr ESM::NAME name("asdc"); + const char s[5] = { 'a', 's', 'd', 'c', '\0' }; + std::string ss(s, 5); + + EXPECT_TRUE(name == s); + EXPECT_TRUE(name == ss.c_str()); + EXPECT_TRUE(name == ss); + } + } + + TEST(EsmFixedString, empty_strings) + { + { + SCOPED_TRACE("4 bytes"); + ESM::NAME empty = ESM::NAME(); + EXPECT_TRUE(empty == ""); + EXPECT_TRUE(empty == static_cast(0)); + EXPECT_TRUE(empty != "some string"); + EXPECT_TRUE(empty != static_cast(42)); + } + { + SCOPED_TRACE("32 bytes"); + ESM::NAME32 empty = ESM::NAME32(); + EXPECT_TRUE(empty == ""); + EXPECT_TRUE(empty != "some string"); + } + } + + TEST(EsmFixedString, assign_should_zero_untouched_bytes_for_4) + { + ESM::NAME value; + value = static_cast(0xFFFFFFFFu); + value.assign(std::string(1, 'a')); + EXPECT_EQ(value, static_cast('a')) << value.toInt(); + } + + TEST(EsmFixedString, assign_should_only_truncate_for_4) + { + ESM::NAME value; + value.assign(std::string(5, 'a')); + EXPECT_EQ(value, std::string(4, 'a')); + } + + TEST(EsmFixedString, assign_should_truncate_and_set_last_element_to_zero) + { + ESM::FixedString<17> value; + value.assign(std::string(20, 'a')); + EXPECT_EQ(value, std::string(16, 'a')); + } + + TEST(EsmFixedString, assign_should_truncate_and_set_last_element_to_zero_for_32) + { + ESM::NAME32 value; + value.assign(std::string(33, 'a')); + EXPECT_EQ(value, std::string(31, 'a')); + } + + TEST(EsmFixedString, assign_should_truncate_and_set_last_element_to_zero_for_64) + { + ESM::NAME64 value; + value.assign(std::string(65, 'a')); + EXPECT_EQ(value, std::string(63, 'a')); + } + + TEST(EsmFixedString, assignment_operator_is_supported_for_uint32) + { + ESM::NAME value; + value = static_cast(0xFEDCBA98u); + EXPECT_EQ(value, static_cast(0xFEDCBA98u)) << value.toInt(); + } + + TEST(EsmFixedString, construction_from_uint32_is_supported) + { + constexpr ESM::NAME value(0xFEDCBA98u); + EXPECT_EQ(value, static_cast(0xFEDCBA98u)) << value.toInt(); + } + + TEST(EsmFixedString, construction_from_RecNameInts_is_supported) + { + constexpr ESM::NAME value(ESM::RecNameInts::REC_ACTI); + EXPECT_EQ(value, static_cast(ESM::RecNameInts::REC_ACTI)) << value.toInt(); + } + + TEST(EsmFixedString, equality_operator_for_not_convertible_to_uint32_with_string_literal) + { + const ESM::FixedString<5> value("abcd"); + EXPECT_EQ(value, "abcd"); + } + + TEST(EsmFixedString, equality_operator_for_not_convertible_to_uint32_with_fixed_size_char_array) + { + const ESM::FixedString<5> value("abcd"); + const char other[5] = { 'a', 'b', 'c', 'd', '\0' }; + EXPECT_EQ(value, other); + } + + TEST(EsmFixedString, equality_operator_for_not_convertible_to_uint32_with_const_char_pointer) + { + const ESM::FixedString<5> value("abcd"); + const char other[5] = { 'a', 'b', 'c', 'd', '\0' }; + EXPECT_EQ(value, static_cast(other)); + } + + TEST(EsmFixedString, equality_operator_for_not_convertible_to_uint32_with_string) + { + const ESM::FixedString<5> value("abcd"); + EXPECT_EQ(value, std::string("abcd")); + } + + TEST(EsmFixedString, equality_operator_for_not_convertible_to_uint32_with_string_view) + { + const ESM::FixedString<5> value("abcd"); + const std::string other("abcd"); + EXPECT_EQ(value, std::string_view(other)); + } + + TEST(EsmFixedString, equality_operator_should_not_get_out_of_bounds) + { + ESM::FixedString<5> value; + const char other[5] = { 'a', 'b', 'c', 'd', 'e' }; + std::memcpy(value.mData, other, sizeof(other)); + EXPECT_EQ(value, static_cast(other)); + } +} diff --git a/apps/components_tests/esm/testrefid.cpp b/apps/components_tests/esm/testrefid.cpp new file mode 100644 index 00000000000..1911cd1a5a7 --- /dev/null +++ b/apps/components_tests/esm/testrefid.cpp @@ -0,0 +1,511 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +MATCHER(IsPrint, "") +{ + return std::isprint(arg) != 0; +} + +namespace ESM +{ + namespace + { + using namespace ::testing; + + TEST(ESMRefIdTest, defaultConstructedIsEmpty) + { + const RefId refId; + EXPECT_TRUE(refId.empty()); + } + + TEST(ESMRefIdTest, stringRefIdIsNotEmpty) + { + const RefId refId = RefId::stringRefId("ref_id"); + EXPECT_FALSE(refId.empty()); + } + + TEST(ESMRefIdTest, formIdRefIdIsNotEmpty) + { + const RefId refId = RefId::formIdRefId({ .mIndex = 42, .mContentFile = 0 }); + EXPECT_FALSE(refId.empty()); + } + + TEST(ESMRefIdTest, FormIdRefIdMustHaveContentFile) + { + EXPECT_TRUE(RefId(FormId()).empty()); + EXPECT_ERROR(RefId(FormId{ .mIndex = 1, .mContentFile = -1 }), "RefId can't be a generated FormId"); + } + + TEST(ESMRefIdTest, defaultConstructedIsEqualToItself) + { + const RefId refId; + EXPECT_EQ(refId, refId); + } + + TEST(ESMRefIdTest, defaultConstructedIsEqualToDefaultConstructed) + { + const RefId a; + const RefId b; + EXPECT_EQ(a, b); + } + + TEST(ESMRefIdTest, defaultConstructedIsNotEqualToDebugStringRefId) + { + const RefId a; + const RefId b = RefId::stringRefId("b"); + EXPECT_NE(a, b); + } + + TEST(ESMRefIdTest, defaultConstructedIsNotEqualToFormIdRefId) + { + const RefId a; + const RefId b = RefId::formIdRefId({ .mIndex = 42, .mContentFile = 0 }); + EXPECT_NE(a, b); + } + + TEST(ESMRefIdTest, defaultConstructedIsNotEqualToDebugStringLiteral) + { + const RefId a; + EXPECT_NE(a, "foo"); + } + + TEST(ESMRefIdTest, stringRefIdIsEqualToTheSameStringLiteralValue) + { + const RefId refId = RefId::stringRefId("ref_id"); + EXPECT_EQ(refId, "ref_id"); + } + + TEST(ESMRefIdTest, stringRefIdIsCaseInsensitiveEqualToTheSameStringLiteralValue) + { + const RefId refId = RefId::stringRefId("ref_id"); + EXPECT_EQ(refId, "REF_ID"); + } + + TEST(ESMRefIdTest, stringRefIdIsEqualToTheSameStringRefId) + { + const RefId a = RefId::stringRefId("ref_id"); + const RefId b = RefId::stringRefId("ref_id"); + EXPECT_EQ(a, b); + } + + TEST(ESMRefIdTest, stringRefIdIsCaseInsensitiveEqualToTheSameStringRefId) + { + const RefId lower = RefId::stringRefId("ref_id"); + const RefId upper = RefId::stringRefId("REF_ID"); + EXPECT_EQ(lower, upper); + } + + TEST(ESMRefIdTest, equalityIsDefinedForStringRefIdAndRefId) + { + const StringRefId stringRefId("ref_id"); + const RefId refId = RefId::stringRefId("REF_ID"); + EXPECT_EQ(stringRefId, refId); + } + + TEST(ESMRefIdTest, equalityIsDefinedForFormIdAndRefId) + { + const FormId formId{ .mIndex = 42, .mContentFile = 0 }; + EXPECT_EQ(formId, RefId(formId)); + } + + TEST(ESMRefIdTest, stringRefIdIsEqualToItself) + { + const RefId refId = RefId::stringRefId("ref_id"); + EXPECT_EQ(refId, refId); + } + + TEST(ESMRefIdTest, stringRefIdIsCaseInsensitiveLessByContent) + { + const RefId a = RefId::stringRefId("a"); + const RefId b = RefId::stringRefId("B"); + EXPECT_LT(a, b); + } + + TEST(ESMRefIdTest, stringRefIdDeserializationReturnsEmptyRefIdForNonExistentValues) + { + RefId id = RefId::deserializeText("this stringrefid should not exist"); + EXPECT_TRUE(id.empty()); + } + + TEST(ESMRefIdTest, lessThanIsDefinedForStringRefIdAndRefId) + { + const StringRefId stringRefId("a"); + const RefId refId = RefId::stringRefId("B"); + EXPECT_LT(stringRefId, refId); + } + + TEST(ESMRefIdTest, lessThanIsDefinedForFormRefIdAndRefId) + { + const FormId formId{ .mIndex = 13, .mContentFile = 0 }; + const RefId refId = RefId(FormId{ .mIndex = 42, .mContentFile = 0 }); + EXPECT_LT(formId, refId); + } + + TEST(ESMRefIdTest, stringRefIdHasCaseInsensitiveHash) + { + const RefId lower = RefId::stringRefId("a"); + const RefId upper = RefId::stringRefId("A"); + const std::hash hash; + EXPECT_EQ(hash(lower), hash(upper)); + } + + TEST(ESMRefIdTest, hasCaseInsensitiveEqualityWithStringView) + { + const RefId a = RefId::stringRefId("a"); + const std::string_view b = "A"; + EXPECT_EQ(a, b); + } + + TEST(ESMRefIdTest, hasCaseInsensitiveLessWithStringView) + { + const RefId a = RefId::stringRefId("a"); + const std::string_view b = "B"; + EXPECT_LT(a, b); + } + + TEST(ESMRefIdTest, hasCaseInsensitiveStrongOrderWithStringView) + { + const RefId a = RefId::stringRefId("a"); + const std::string_view b = "B"; + const RefId c = RefId::stringRefId("c"); + EXPECT_LT(a, b); + EXPECT_LT(b, c); + } + + TEST(ESMRefIdTest, stringRefIdHasStrongOrderWithFormId) + { + const RefId stringRefId = RefId::stringRefId("a"); + const RefId formIdRefId = RefId::formIdRefId({ .mIndex = 42, .mContentFile = 0 }); + EXPECT_TRUE(stringRefId < formIdRefId); + EXPECT_FALSE(formIdRefId < stringRefId); + } + + TEST(ESMRefIdTest, formIdRefIdHasStrongOrderWithStringView) + { + const RefId formIdRefId = RefId::formIdRefId({ .mIndex = 42, .mContentFile = 0 }); + const std::string_view stringView = "42"; + EXPECT_TRUE(stringView < formIdRefId); + EXPECT_FALSE(formIdRefId < stringView); + } + + TEST(ESMRefIdTest, canBeUsedAsMapKeyWithLookupByStringView) + { + const std::map> map({ { RefId::stringRefId("a"), 42 } }); + EXPECT_EQ(map.count("A"), 1); + } + + TEST(ESMRefIdTest, canBeUsedAsLookupKeyForMapWithStringKey) + { + const std::map> map({ { "a", 42 } }); + EXPECT_EQ(map.count(RefId::stringRefId("A")), 1); + } + + TEST(ESMRefIdTest, emptyRefId) + { + EXPECT_EQ(RefId(), EmptyRefId()); + EXPECT_EQ(RefId(), RefId::stringRefId("\0")); + EXPECT_EQ(RefId(), RefId::formIdRefId({ .mIndex = 0, .mContentFile = 0 })); + EXPECT_EQ(RefId(), RefId::formIdRefId({ .mIndex = 0, .mContentFile = -1 })); + } + + TEST(ESMRefIdTest, indexRefIdHashDiffersForDistinctValues) + { + const RefId a = RefId::index(static_cast(3), 1); + const RefId b = RefId::index(static_cast(3), 2); + std::hash hash; + EXPECT_NE(hash(a), hash(b)); + } + + TEST(ESMRefIdTest, indexRefIdHashDiffersForDistinctRecords) + { + const RefId a = RefId::index(static_cast(1), 3); + const RefId b = RefId::index(static_cast(2), 3); + std::hash hash; + EXPECT_NE(hash(a), hash(b)); + } + + TEST(ESMRefIdTest, esm3ExteriorCellHasLexicographicalOrder) + { + const RefId a = RefId::esm3ExteriorCell(0, 0); + const RefId b = RefId::esm3ExteriorCell(1, 0); + EXPECT_LT(a, b); + EXPECT_TRUE(!(b < a)); + } + + struct ESMRefIdToStringTest : TestWithParam> + { + }; + + TEST_P(ESMRefIdToStringTest, toString) + { + const RefId& refId = GetParam().first; + const std::string& string = GetParam().second; + EXPECT_EQ(refId.toString(), string); + } + + const std::vector> toStringParams = { + { RefId(), std::string() }, + { RefId::stringRefId("foo"), "foo" }, + { RefId::stringRefId(std::string({ 'a', 0, -1, '\n', '\t' })), { 'a', 0, -1, '\n', '\t' } }, + { RefId::formIdRefId({ .mIndex = 42, .mContentFile = 0 }), "0x2a" }, + { RefId::formIdRefId({ .mIndex = 0xffffff, .mContentFile = std::numeric_limits::max() }), + "0x7fffffffffffff" }, + { RefId::generated(42), "0x2a" }, + { RefId::generated(std::numeric_limits::max()), "0xffffffffffffffff" }, + { RefId::index(REC_ARMO, 42), "ARMO:0x2a" }, + { RefId::esm3ExteriorCell(-13, 42), "#-13 42" }, + { RefId::esm3ExteriorCell(std::numeric_limits::min(), std::numeric_limits::min()), + "#-2147483648 -2147483648" }, + { RefId::esm3ExteriorCell(std::numeric_limits::max(), std::numeric_limits::max()), + "#2147483647 2147483647" }, + }; + + INSTANTIATE_TEST_SUITE_P(ESMRefIdToString, ESMRefIdToStringTest, ValuesIn(toStringParams)); + + struct ESMRefIdToDebugStringTest : TestWithParam> + { + }; + + TEST_P(ESMRefIdToDebugStringTest, toDebugString) + { + const RefId& refId = GetParam().first; + const std::string& debugString = GetParam().second; + EXPECT_EQ(refId.toDebugString(), debugString); + } + + TEST_P(ESMRefIdToDebugStringTest, toStream) + { + const RefId& refId = GetParam().first; + const std::string& debugString = GetParam().second; + std::ostringstream stream; + stream << refId; + EXPECT_EQ(stream.str(), debugString); + } + + const std::vector> toDebugStringParams = { + { RefId(), "Empty{}" }, + { RefId::stringRefId("foo"), "\"foo\"" }, + { RefId::stringRefId("BAR"), "\"BAR\"" }, + { RefId::stringRefId(std::string({ 'a', 0, -1, '\n', '\t' })), "\"a\\x0\\xff\\xa\\x9\"" }, + { RefId::stringRefId("Логово дракона"), "\"Логово дракона\"" }, + { RefId::stringRefId("\xd0\x9b"), "\"Л\"" }, + { RefId::stringRefId("\xff\x9b"), "\"\\xff\\x9b\"" }, + { RefId::stringRefId("\xd0\xd0"), "\"\\xd0\\xd0\"" }, + { RefId::formIdRefId({ .mIndex = 42, .mContentFile = 0 }), "FormId:0x2a" }, + { RefId::formIdRefId({ .mIndex = 0xffffff, .mContentFile = std::numeric_limits::max() }), + "FormId:0x7fffffffffffff" }, + { RefId::generated(42), "Generated:0x2a" }, + { RefId::generated(std::numeric_limits::max()), "Generated:0xffffffffffffffff" }, + { RefId::index(REC_ARMO, 42), "Index:ARMO:0x2a" }, + { RefId::index(REC_ARMO, std::numeric_limits::max()), "Index:ARMO:0xffffffff" }, + { RefId::esm3ExteriorCell(-13, 42), "Esm3ExteriorCell:-13:42" }, + { RefId::esm3ExteriorCell(std::numeric_limits::min(), std::numeric_limits::min()), + "Esm3ExteriorCell:-2147483648:-2147483648" }, + { RefId::esm3ExteriorCell(std::numeric_limits::max(), std::numeric_limits::max()), + "Esm3ExteriorCell:2147483647:2147483647" }, + }; + + INSTANTIATE_TEST_SUITE_P(ESMRefIdToDebugString, ESMRefIdToDebugStringTest, ValuesIn(toDebugStringParams)); + + struct ESMRefIdTextTest : TestWithParam> + { + }; + + TEST_P(ESMRefIdTextTest, serializeTextShouldReturnString) + { + EXPECT_EQ(GetParam().first.serializeText(), GetParam().second); + } + + TEST_P(ESMRefIdTextTest, deserializeTextShouldReturnRefId) + { + EXPECT_EQ(RefId::deserializeText(GetParam().second), GetParam().first); + } + + const std::vector> serializedRefIds = { + { RefId(), "" }, + { RefId::stringRefId("foo"), "foo" }, + { RefId::stringRefId("BAR"), "bar" }, + { RefId::stringRefId(std::string({ 'a', 0, -1, '\n', '\t' })), { 'a', 0, -1, '\n', '\t' } }, + { RefId::formIdRefId({ .mIndex = 1, .mContentFile = 0 }), "FormId:0x1" }, + { RefId::formIdRefId({ .mIndex = 0x1f, .mContentFile = 0 }), "FormId:0x1f" }, + { RefId::formIdRefId({ .mIndex = 0x1f, .mContentFile = 2 }), "FormId:0x200001f" }, + { RefId::formIdRefId({ .mIndex = 0xffffff, .mContentFile = 0x1abc }), "FormId:0x1abcffffff" }, + { RefId::formIdRefId({ .mIndex = 0xffffff, .mContentFile = std::numeric_limits::max() }), + "FormId:0x7fffffffffffff" }, + { RefId::generated(0), "Generated:0x0" }, + { RefId::generated(1), "Generated:0x1" }, + { RefId::generated(0x1f), "Generated:0x1f" }, + { RefId::generated(std::numeric_limits::max()), "Generated:0xffffffffffffffff" }, + { RefId::index(REC_INGR, 0), "Index:INGR:0x0" }, + { RefId::index(REC_INGR, 1), "Index:INGR:0x1" }, + { RefId::index(REC_INGR, 0x1f), "Index:INGR:0x1f" }, + { RefId::index(REC_INGR, std::numeric_limits::max()), "Index:INGR:0xffffffff" }, + { RefId::esm3ExteriorCell(-13, 42), "Esm3ExteriorCell:-13:42" }, + { RefId::esm3ExteriorCell( + std::numeric_limits::min(), std::numeric_limits::min()), + "Esm3ExteriorCell:-2147483648:-2147483648" }, + { RefId::esm3ExteriorCell( + std::numeric_limits::max(), std::numeric_limits::max()), + "Esm3ExteriorCell:2147483647:2147483647" }, + }; + + INSTANTIATE_TEST_SUITE_P(ESMRefIdText, ESMRefIdTextTest, ValuesIn(serializedRefIds)); + + template + [[maybe_unused]] constexpr bool alwaysFalse = false; + + template + struct GenerateRefId + { + static_assert(alwaysFalse, + "There should be specialization for each RefId type. If this assert fails, probably there are no tests " + "for a new RefId type."); + }; + + template <> + struct GenerateRefId + { + static RefId call() { return RefId(); } + }; + + template <> + struct GenerateRefId + { + static RefId call() { return RefId::stringRefId("StringRefId"); } + }; + + template <> + struct GenerateRefId + { + static RefId call() { return FormId{ .mIndex = 42, .mContentFile = 0 }; } + }; + + template <> + struct GenerateRefId + { + static RefId call() { return RefId::generated(13); } + }; + + template <> + struct GenerateRefId + { + static RefId call() { return RefId::index(REC_BOOK, 7); } + }; + + template <> + struct GenerateRefId + { + static RefId call() { return RefId::esm3ExteriorCell(-12, 7); } + }; + + template + struct ESMRefIdTypesTest : Test + { + }; + + TYPED_TEST_SUITE_P(ESMRefIdTypesTest); + + TYPED_TEST_P(ESMRefIdTypesTest, serializeThenDeserializeShouldProduceSameValue) + { + const RefId refId = GenerateRefId::call(); + EXPECT_EQ(RefId::deserialize(refId.serialize()), refId); + } + + TYPED_TEST_P(ESMRefIdTypesTest, serializeTextThenDeserializeTextShouldProduceSameValue) + { + const RefId refId = GenerateRefId::call(); + const std::string text = refId.serializeText(); + EXPECT_EQ(RefId::deserializeText(text), refId); + } + + TYPED_TEST_P(ESMRefIdTypesTest, serializeTextShouldReturnOnlyPrintableCharacters) + { + const RefId refId = GenerateRefId::call(); + EXPECT_THAT(refId.serializeText(), Each(IsPrint())); + } + + TYPED_TEST_P(ESMRefIdTypesTest, toStringShouldReturnOnlyPrintableCharacters) + { + const RefId refId = GenerateRefId::call(); + EXPECT_THAT(refId.toString(), Each(IsPrint())); + } + + TYPED_TEST_P(ESMRefIdTypesTest, toDebugStringShouldReturnOnlyPrintableCharacters) + { + const RefId refId = GenerateRefId::call(); + EXPECT_THAT(refId.toDebugString(), Each(IsPrint())); + } + + TYPED_TEST_P(ESMRefIdTypesTest, shouldBeEqualToItself) + { + const RefId a = GenerateRefId::call(); + const RefId b = GenerateRefId::call(); + EXPECT_EQ(a, b); + } + + TYPED_TEST_P(ESMRefIdTypesTest, shouldNotBeNotEqualToItself) + { + const RefId a = GenerateRefId::call(); + const RefId b = GenerateRefId::call(); + EXPECT_FALSE(a != b) << a; + } + + TYPED_TEST_P(ESMRefIdTypesTest, shouldBeNotLessThanItself) + { + const RefId a = GenerateRefId::call(); + const RefId b = GenerateRefId::call(); + EXPECT_FALSE(a < b) << a; + } + + TYPED_TEST_P(ESMRefIdTypesTest, saveAndLoadShouldNotChange) + { + constexpr NAME fakeRecordId(fourCC("FAKE")); + constexpr NAME subRecordId(fourCC("NAME")); + const RefId expected = GenerateRefId::call(); + auto stream = std::make_unique(); + { + ESMWriter writer; + writer.setFormatVersion(CurrentSaveGameFormatVersion); + writer.save(*stream); + writer.startRecord(fakeRecordId); + writer.writeHNCRefId(subRecordId, expected); + writer.endRecord(fakeRecordId); + } + ESMReader reader; + reader.open(std::move(stream), "stream"); + ASSERT_TRUE(reader.hasMoreRecs()); + ASSERT_EQ(reader.getRecName().toInt(), fakeRecordId); + reader.getRecHeader(); + const RefId actual = reader.getHNRefId(subRecordId); + EXPECT_EQ(actual, expected); + } + + REGISTER_TYPED_TEST_SUITE_P(ESMRefIdTypesTest, serializeThenDeserializeShouldProduceSameValue, + serializeTextThenDeserializeTextShouldProduceSameValue, shouldBeEqualToItself, shouldNotBeNotEqualToItself, + shouldBeNotLessThanItself, serializeTextShouldReturnOnlyPrintableCharacters, + toStringShouldReturnOnlyPrintableCharacters, toDebugStringShouldReturnOnlyPrintableCharacters, + saveAndLoadShouldNotChange); + + template + struct RefIdTypes; + + template + struct RefIdTypes> + { + using Type = Types; + }; + + using RefIdTypeParams = typename RefIdTypes::Type; + + INSTANTIATE_TYPED_TEST_SUITE_P(RefIdTypes, ESMRefIdTypesTest, RefIdTypeParams); + } +} diff --git a/apps/components_tests/esm/variant.cpp b/apps/components_tests/esm/variant.cpp new file mode 100644 index 00000000000..8f3bfd0ada2 --- /dev/null +++ b/apps/components_tests/esm/variant.cpp @@ -0,0 +1,517 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace ESM; + + constexpr std::uint32_t fakeRecordId = fourCC("FAKE"); + + Variant makeVariant(VarType type) + { + Variant v; + v.setType(type); + return v; + } + + Variant makeVariant(VarType type, int value) + { + Variant v; + v.setType(type); + v.setInteger(value); + return v; + } + + TEST(ESMVariantTest, move_constructed_should_have_data) + { + Variant a(int{ 42 }); + const Variant b(std::move(a)); + ASSERT_EQ(b.getInteger(), 42); + } + + TEST(ESMVariantTest, copy_constructed_is_equal_to_source) + { + const Variant a(int{ 42 }); + const Variant b(a); + ASSERT_EQ(a, b); + } + + TEST(ESMVariantTest, copy_constructed_does_not_share_data_with_source) + { + const Variant a(int{ 42 }); + Variant b(a); + b.setInteger(13); + ASSERT_EQ(a.getInteger(), 42); + ASSERT_EQ(b.getInteger(), 13); + } + + TEST(ESMVariantTest, move_assigned_should_have_data) + { + Variant b; + { + Variant a(int{ 42 }); + b = std::move(a); + } + ASSERT_EQ(b.getInteger(), 42); + } + + TEST(ESMVariantTest, copy_assigned_is_equal_to_source) + { + const Variant a(int{ 42 }); + Variant b; + b = a; + ASSERT_EQ(a, b); + } + + TEST(ESMVariantTest, not_equal_is_negation_of_equal) + { + const Variant a(int{ 42 }); + Variant b; + b = a; + ASSERT_TRUE(!(a != b)); + } + + TEST(ESMVariantTest, different_types_are_not_equal) + { + ASSERT_NE(Variant(int{ 42 }), Variant(float{ 2.7f })); + } + + struct ESMVariantWriteToOStreamTest : TestWithParam> + { + }; + + TEST_P(ESMVariantWriteToOStreamTest, should_write) + { + const auto [variant, result] = GetParam(); + std::ostringstream s; + s << variant; + ASSERT_EQ(s.str(), result); + } + + INSTANTIATE_TEST_SUITE_P(VariantAsString, ESMVariantWriteToOStreamTest, + Values(std::make_tuple(Variant(), "variant none"), std::make_tuple(Variant(int{ 42 }), "variant long: 42"), + std::make_tuple(Variant(float{ 2.7f }), "variant float: 2.7"), + std::make_tuple(Variant(std::string("foo")), "variant string: \"foo\""), + std::make_tuple(makeVariant(VT_Unknown), "variant unknown"), + std::make_tuple(makeVariant(VT_Short, 42), "variant short: 42"), + std::make_tuple(makeVariant(VT_Int, 42), "variant int: 42"))); + + struct ESMVariantGetTypeTest : Test + { + }; + + TEST(ESMVariantGetTypeTest, default_constructed_should_return_none) + { + ASSERT_EQ(Variant().getType(), VT_None); + } + + TEST(ESMVariantGetTypeTest, for_constructed_from_int_should_return_long) + { + ASSERT_EQ(Variant(int{}).getType(), VT_Long); + } + + TEST(ESMVariantGetTypeTest, for_constructed_from_float_should_return_float) + { + ASSERT_EQ(Variant(float{}).getType(), VT_Float); + } + + TEST(ESMVariantGetTypeTest, for_constructed_from_lvalue_string_should_return_string) + { + const std::string string; + ASSERT_EQ(Variant(string).getType(), VT_String); + } + + TEST(ESMVariantGetTypeTest, for_constructed_from_rvalue_string_should_return_string) + { + ASSERT_EQ(Variant(std::string{}).getType(), VT_String); + } + + struct ESMVariantGetIntegerTest : Test + { + }; + + TEST(ESMVariantGetIntegerTest, for_default_constructed_should_throw_exception) + { + ASSERT_THROW(Variant().getInteger(), std::runtime_error); + } + + TEST(ESMVariantGetIntegerTest, for_constructed_from_int_should_return_same_value) + { + const Variant variant(int{ 42 }); + ASSERT_EQ(variant.getInteger(), 42); + } + + TEST(ESMVariantGetIntegerTest, for_constructed_from_float_should_return_casted_to_int) + { + const Variant variant(float{ 2.7 }); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantGetIntegerTest, for_constructed_from_string_should_throw_exception) + { + const Variant variant(std::string("foo")); + ASSERT_THROW(variant.getInteger(), std::runtime_error); + } + + TEST(ESMVariantGetFloatTest, for_default_constructed_should_throw_exception) + { + ASSERT_THROW(Variant().getFloat(), std::runtime_error); + } + + TEST(ESMVariantGetFloatTest, for_constructed_from_int_should_return_casted_to_float) + { + const Variant variant(int{ 42 }); + ASSERT_EQ(variant.getFloat(), 42); + } + + TEST(ESMVariantGetFloatTest, for_constructed_from_float_should_return_same_value) + { + const Variant variant(float{ 2.7f }); + ASSERT_EQ(variant.getFloat(), 2.7f); + } + + TEST(ESMVariantGetFloatTest, for_constructed_from_string_should_throw_exception) + { + const Variant variant(std::string("foo")); + ASSERT_THROW(variant.getFloat(), std::runtime_error); + } + + TEST(ESMVariantGetStringTest, for_default_constructed_should_throw_exception) + { + ASSERT_THROW(Variant().getString(), std::bad_variant_access); + } + + TEST(ESMVariantGetStringTest, for_constructed_from_int_should_throw_exception) + { + const Variant variant(int{ 42 }); + ASSERT_THROW(variant.getString(), std::bad_variant_access); + } + + TEST(ESMVariantGetStringTest, for_constructed_from_float_should_throw_exception) + { + const Variant variant(float{ 2.7 }); + ASSERT_THROW(variant.getString(), std::bad_variant_access); + } + + TEST(ESMVariantGetStringTest, for_constructed_from_string_should_return_same_value) + { + const Variant variant(std::string("foo")); + ASSERT_EQ(variant.getString(), "foo"); + } + + TEST(ESMVariantSetTypeTest, for_unknown_should_reset_data) + { + Variant variant(int{ 42 }); + variant.setType(VT_Unknown); + ASSERT_THROW(variant.getInteger(), std::runtime_error); + } + + TEST(ESMVariantSetTypeTest, for_none_should_reset_data) + { + Variant variant(int{ 42 }); + variant.setType(VT_None); + ASSERT_THROW(variant.getInteger(), std::runtime_error); + } + + TEST(ESMVariantSetTypeTest, for_same_type_should_not_change_value) + { + Variant variant(int{ 42 }); + variant.setType(VT_Long); + ASSERT_EQ(variant.getInteger(), 42); + } + + TEST(ESMVariantSetTypeTest, for_float_replaced_by_int_should_cast_float_to_int) + { + Variant variant(float{ 2.7f }); + variant.setType(VT_Int); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetTypeTest, for_string_replaced_by_int_should_set_default_initialized_data) + { + Variant variant(std::string("foo")); + variant.setType(VT_Int); + ASSERT_EQ(variant.getInteger(), 0); + } + + TEST(ESMVariantSetTypeTest, for_default_constructed_replaced_by_float_should_set_default_initialized_value) + { + Variant variant; + variant.setType(VT_Float); + ASSERT_EQ(variant.getInteger(), 0.0f); + } + + TEST(ESMVariantSetTypeTest, for_float_replaced_by_short_should_cast_data_to_int) + { + Variant variant(float{ 2.7f }); + variant.setType(VT_Short); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetTypeTest, for_float_replaced_by_long_should_cast_data_to_int) + { + Variant variant(float{ 2.7f }); + variant.setType(VT_Long); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetTypeTest, for_int_replaced_by_float_should_cast_data_to_float) + { + Variant variant(int{ 42 }); + variant.setType(VT_Float); + ASSERT_EQ(variant.getFloat(), 42.0f); + } + + TEST(ESMVariantSetTypeTest, for_int_replaced_by_string_should_set_default_initialized_data) + { + Variant variant(int{ 42 }); + variant.setType(VT_String); + ASSERT_EQ(variant.getString(), ""); + } + + TEST(ESMVariantSetIntegerTest, for_default_constructed_should_throw_exception) + { + Variant variant; + ASSERT_THROW(variant.setInteger(42), std::runtime_error); + } + + TEST(ESMVariantSetIntegerTest, for_unknown_should_throw_exception) + { + Variant variant; + variant.setType(VT_Unknown); + ASSERT_THROW(variant.setInteger(42), std::runtime_error); + } + + TEST(ESMVariantSetIntegerTest, for_default_int_should_change_value) + { + Variant variant(int{ 13 }); + variant.setInteger(42); + ASSERT_EQ(variant.getInteger(), 42); + } + + TEST(ESMVariantSetIntegerTest, for_int_should_change_value) + { + Variant variant; + variant.setType(VT_Int); + variant.setInteger(42); + ASSERT_EQ(variant.getInteger(), 42); + } + + TEST(ESMVariantSetIntegerTest, for_short_should_change_value) + { + Variant variant; + variant.setType(VT_Short); + variant.setInteger(42); + ASSERT_EQ(variant.getInteger(), 42); + } + + TEST(ESMVariantSetIntegerTest, for_float_should_change_value) + { + Variant variant(float{ 2.7f }); + variant.setInteger(42); + ASSERT_EQ(variant.getFloat(), 42.0f); + } + + TEST(ESMVariantSetIntegerTest, for_string_should_throw_exception) + { + Variant variant(std::string{}); + ASSERT_THROW(variant.setInteger(42), std::runtime_error); + } + + TEST(ESMVariantSetFloatTest, for_default_constructed_should_throw_exception) + { + Variant variant; + ASSERT_THROW(variant.setFloat(2.7f), std::runtime_error); + } + + TEST(ESMVariantSetFloatTest, for_unknown_should_throw_exception) + { + Variant variant; + variant.setType(VT_Unknown); + ASSERT_THROW(variant.setFloat(2.7f), std::runtime_error); + } + + TEST(ESMVariantSetFloatTest, for_default_int_should_change_value) + { + Variant variant(int{ 13 }); + variant.setFloat(2.7f); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetFloatTest, for_int_should_change_value) + { + Variant variant; + variant.setType(VT_Int); + variant.setFloat(2.7f); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetFloatTest, for_short_should_change_value) + { + Variant variant; + variant.setType(VT_Short); + variant.setFloat(2.7f); + ASSERT_EQ(variant.getInteger(), 2); + } + + TEST(ESMVariantSetFloatTest, for_float_should_change_value) + { + Variant variant(float{ 2.7f }); + variant.setFloat(3.14f); + ASSERT_EQ(variant.getFloat(), 3.14f); + } + + TEST(ESMVariantSetFloatTest, for_string_should_throw_exception) + { + Variant variant(std::string{}); + ASSERT_THROW(variant.setFloat(2.7f), std::runtime_error); + } + + TEST(ESMVariantSetStringTest, for_default_constructed_should_throw_exception) + { + Variant variant; + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_unknown_should_throw_exception) + { + Variant variant; + variant.setType(VT_Unknown); + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_default_int_should_throw_exception) + { + Variant variant(int{ 13 }); + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_int_should_throw_exception) + { + Variant variant; + variant.setType(VT_Int); + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_short_should_throw_exception) + { + Variant variant; + variant.setType(VT_Short); + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_float_should_throw_exception) + { + Variant variant(float{ 2.7f }); + ASSERT_THROW(variant.setString("foo"), std::bad_variant_access); + } + + TEST(ESMVariantSetStringTest, for_string_should_change_value) + { + Variant variant(std::string("foo")); + variant.setString("bar"); + ASSERT_EQ(variant.getString(), "bar"); + } + + struct WriteToESMTestCase + { + Variant mVariant; + Variant::Format mFormat; + std::size_t mDataSize{}; + }; + + std::string write(const Variant& variant, const Variant::Format format) + { + std::ostringstream out; + ESMWriter writer; + writer.save(out); + writer.startRecord(fakeRecordId); + variant.write(writer, format); + writer.endRecord(fakeRecordId); + writer.close(); + return out.str(); + } + + void read(const Variant::Format format, const std::string& data, Variant& result) + { + ESMReader reader; + reader.open(std::make_unique(data), "stream"); + ASSERT_TRUE(reader.hasMoreRecs()); + ASSERT_EQ(reader.getRecName().toInt(), fakeRecordId); + reader.getRecHeader(); + result.read(reader, format); + } + + void writeAndRead(const Variant& variant, const Variant::Format format, std::size_t dataSize, Variant& result) + { + const std::string data = write(variant, format); + EXPECT_EQ(data.size(), dataSize); + read(format, data, result); + } + + struct ESMVariantToESMTest : TestWithParam + { + }; + + TEST_P(ESMVariantToESMTest, deserialized_is_equal_to_serialized) + { + const auto param = GetParam(); + ESM::Variant result; + writeAndRead(param.mVariant, param.mFormat, param.mDataSize, result); + ASSERT_EQ(param.mVariant, result); + } + + const std::array deserializedParams = { + WriteToESMTestCase{ Variant(), Variant::Format_Gmst, 340 }, + WriteToESMTestCase{ Variant(int{ 42 }), Variant::Format_Global, 361 }, + WriteToESMTestCase{ Variant(float{ 2.7f }), Variant::Format_Global, 361 }, + WriteToESMTestCase{ Variant(float{ 2.7f }), Variant::Format_Info, 352 }, + WriteToESMTestCase{ Variant(float{ 2.7f }), Variant::Format_Local, 352 }, + WriteToESMTestCase{ makeVariant(VT_Short, 42), Variant::Format_Global, 361 }, + WriteToESMTestCase{ makeVariant(VT_Short, 42), Variant::Format_Local, 350 }, + WriteToESMTestCase{ makeVariant(VT_Int, 42), Variant::Format_Info, 352 }, + WriteToESMTestCase{ makeVariant(VT_Int, 42), Variant::Format_Local, 352 }, + WriteToESMTestCase{ Variant(float{ 2.7f }), Variant::Format_Gmst, 352 }, + WriteToESMTestCase{ Variant(std::string("foo")), Variant::Format_Gmst, 351 }, + WriteToESMTestCase{ makeVariant(VT_Int, 42), Variant::Format_Gmst, 352 }, + }; + + INSTANTIATE_TEST_SUITE_P(VariantAndData, ESMVariantToESMTest, ValuesIn(deserializedParams)); + + struct ESMVariantWriteToESMFailTest : TestWithParam + { + }; + + TEST_P(ESMVariantWriteToESMFailTest, write_is_not_supported) + { + const auto param = GetParam(); + std::ostringstream out; + ESMWriter writer; + writer.save(out); + ASSERT_THROW(param.mVariant.write(writer, param.mFormat), std::runtime_error); + } + + INSTANTIATE_TEST_SUITE_P(VariantAndFormat, ESMVariantWriteToESMFailTest, + Values(WriteToESMTestCase{ Variant(), Variant::Format_Global }, + WriteToESMTestCase{ Variant(), Variant::Format_Info }, + WriteToESMTestCase{ Variant(), Variant::Format_Local }, + WriteToESMTestCase{ Variant(int{ 42 }), Variant::Format_Gmst }, + WriteToESMTestCase{ Variant(int{ 42 }), Variant::Format_Info }, + WriteToESMTestCase{ Variant(int{ 42 }), Variant::Format_Local }, + WriteToESMTestCase{ Variant(std::string("foo")), Variant::Format_Global }, + WriteToESMTestCase{ Variant(std::string("foo")), Variant::Format_Info }, + WriteToESMTestCase{ Variant(std::string("foo")), Variant::Format_Local }, + WriteToESMTestCase{ makeVariant(VT_Unknown), Variant::Format_Global }, + WriteToESMTestCase{ makeVariant(VT_Int, 42), Variant::Format_Global }, + WriteToESMTestCase{ makeVariant(VT_Short, 42), Variant::Format_Gmst }, + WriteToESMTestCase{ makeVariant(VT_Short, 42), Variant::Format_Info })); +} diff --git a/apps/components_tests/esm3/readerscache.cpp b/apps/components_tests/esm3/readerscache.cpp new file mode 100644 index 00000000000..f222a29bf8d --- /dev/null +++ b/apps/components_tests/esm3/readerscache.cpp @@ -0,0 +1,91 @@ +#include +#include +#include + +#include + +#ifndef OPENMW_DATA_DIR +#error "OPENMW_DATA_DIR is not defined" +#endif + +namespace +{ + using namespace testing; + using namespace ESM; + + TEST(ESM3ReadersCache, onAttemptToRequestTheSameReaderTwiceShouldThrowException) + { + ReadersCache readers(1); + const ReadersCache::BusyItem reader = readers.get(0); + EXPECT_THROW(readers.get(0), std::logic_error); + } + + TEST(ESM3ReadersCache, shouldAllowToHaveBusyItemsMoreThanCapacity) + { + ReadersCache readers(1); + const ReadersCache::BusyItem reader0 = readers.get(0); + const ReadersCache::BusyItem reader1 = readers.get(1); + } + + TEST(ESM3ReadersCache, shouldKeepClosedReleasedClosedItem) + { + ReadersCache readers(1); + readers.get(0); + const ReadersCache::BusyItem reader = readers.get(0); + EXPECT_FALSE(reader->isOpen()); + } + + struct ESM3ReadersCacheWithContentFile : Test + { + static constexpr std::size_t sInitialOffset = 324; + static constexpr std::size_t sSkip = 100; + const Files::PathContainer mDataDirs{ { std::filesystem::path{ OPENMW_DATA_DIR } } }; + const Files::Collections mFileCollections{ mDataDirs }; + const std::string mContentFile = "template.omwgame"; + const std::filesystem::path mContentFilePath = mFileCollections.getCollection(".omwgame").getPath(mContentFile); + }; + + TEST_F(ESM3ReadersCacheWithContentFile, shouldKeepOpenReleasedOpenReader) + { + ReadersCache readers(1); + { + const ReadersCache::BusyItem reader = readers.get(0); + reader->open(mContentFilePath); + ASSERT_TRUE(reader->isOpen()); + ASSERT_EQ(reader->getFileOffset(), sInitialOffset); + ASSERT_GT(reader->getFileSize(), sInitialOffset + sSkip); + reader->skip(sSkip); + ASSERT_EQ(reader->getFileOffset(), sInitialOffset + sSkip); + } + { + const ReadersCache::BusyItem reader = readers.get(0); + EXPECT_TRUE(reader->isOpen()); + EXPECT_EQ(reader->getName(), mContentFilePath); + EXPECT_EQ(reader->getFileOffset(), sInitialOffset + sSkip); + } + } + + TEST_F(ESM3ReadersCacheWithContentFile, shouldCloseFreeReaderWhenReachingCapacityLimit) + { + ReadersCache readers(1); + { + const ReadersCache::BusyItem reader = readers.get(0); + reader->open(mContentFilePath); + ASSERT_TRUE(reader->isOpen()); + ASSERT_EQ(reader->getFileOffset(), sInitialOffset); + ASSERT_GT(reader->getFileSize(), sInitialOffset + sSkip); + reader->skip(sSkip); + ASSERT_EQ(reader->getFileOffset(), sInitialOffset + sSkip); + } + { + const ReadersCache::BusyItem reader = readers.get(1); + reader->open(mContentFilePath); + ASSERT_TRUE(reader->isOpen()); + } + { + const ReadersCache::BusyItem reader = readers.get(0); + EXPECT_TRUE(reader->isOpen()); + EXPECT_EQ(reader->getFileOffset(), sInitialOffset); + } + } +} diff --git a/apps/components_tests/esm3/testesmwriter.cpp b/apps/components_tests/esm3/testesmwriter.cpp new file mode 100644 index 00000000000..9e9ae9947e9 --- /dev/null +++ b/apps/components_tests/esm3/testesmwriter.cpp @@ -0,0 +1,87 @@ +#include + +#include +#include + +#include +#include +#include + +namespace ESM +{ + namespace + { + using namespace ::testing; + + struct Esm3EsmWriterTest : public Test + { + std::minstd_rand mRandom; + std::uniform_int_distribution mRefIdDistribution{ 'a', 'z' }; + + std::string generateRandomString(std::size_t size) + { + std::string result; + std::generate_n( + std::back_inserter(result), size, [&] { return static_cast(mRefIdDistribution(mRandom)); }); + return result; + } + }; + + TEST_F(Esm3EsmWriterTest, saveShouldThrowExceptionOnWhenTruncatingHeaderStrings) + { + const std::string author = generateRandomString(33); + const std::string description = generateRandomString(257); + + std::stringstream stream; + + ESMWriter writer; + writer.setAuthor(author); + writer.setDescription(description); + writer.setFormatVersion(MaxLimitedSizeStringsFormatVersion); + EXPECT_THROW(writer.save(stream), std::runtime_error); + } + + TEST_F(Esm3EsmWriterTest, writeFixedStringShouldThrowExceptionOnTruncate) + { + std::stringstream stream; + + ESMWriter writer; + writer.setFormatVersion(MaxLimitedSizeStringsFormatVersion); + writer.save(stream); + EXPECT_THROW(writer.writeMaybeFixedSizeString(generateRandomString(33), 32), std::runtime_error); + } + + struct Esm3EsmWriterRefIdSizeTest : TestWithParam> + { + }; + + // If this test failed probably there is a change in RefId format and CurrentSaveGameFormatVersion should be + // incremented, current version should be handled. + TEST_P(Esm3EsmWriterRefIdSizeTest, writeHRefIdShouldProduceCertainNubmerOfBytes) + { + const auto [refId, size] = GetParam(); + + std::ostringstream stream; + + { + ESMWriter writer; + writer.setFormatVersion(CurrentSaveGameFormatVersion); + writer.save(stream); + writer.writeHRefId(refId); + } + + EXPECT_EQ(stream.str().size(), size); + } + + const std::vector> refIdSizes = { + { RefId(), 57 }, + { RefId::stringRefId(std::string(32, 'a')), 89 }, + { RefId::formIdRefId({ 0x1f, 0 }), 65 }, + { RefId::generated(0x1f), 65 }, + { RefId::index(REC_INGR, 0x1f), 65 }, + { RefId::esm3ExteriorCell(-42, 42), 65 }, + }; + + INSTANTIATE_TEST_SUITE_P(RefIds, Esm3EsmWriterRefIdSizeTest, ValuesIn(refIdSizes)); + } +} diff --git a/apps/components_tests/esm3/testinfoorder.cpp b/apps/components_tests/esm3/testinfoorder.cpp new file mode 100644 index 00000000000..feb48e2d0af --- /dev/null +++ b/apps/components_tests/esm3/testinfoorder.cpp @@ -0,0 +1,27 @@ +#include + +#include + +namespace ESM +{ + namespace + { + struct Value + { + RefId mId; + RefId mPrev; + + Value() = default; + Value(const Value&) = delete; + Value(Value&&) = default; + Value& operator=(const Value&) = delete; + Value& operator=(Value&&) = default; + }; + + TEST(Esm3InfoOrderTest, insertInfoShouldNotCopyValue) + { + InfoOrder order; + order.insertInfo(Value{}, false); + } + } +} diff --git a/apps/components_tests/esm3/testsaveload.cpp b/apps/components_tests/esm3/testsaveload.cpp new file mode 100644 index 00000000000..41a79313ccc --- /dev/null +++ b/apps/components_tests/esm3/testsaveload.cpp @@ -0,0 +1,762 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace ESM +{ + namespace + { + auto tie(const ContItem& value) + { + return std::tie(value.mCount, value.mItem); + } + + auto tie(const ESM::Region::SoundRef& value) + { + return std::tie(value.mSound, value.mChance); + } + + auto tie(const ESM::QuickKeys::QuickKey& value) + { + return std::tie(value.mType, value.mId); + } + } + + inline bool operator==(const ESM::ContItem& lhs, const ESM::ContItem& rhs) + { + return tie(lhs) == tie(rhs); + } + + inline std::ostream& operator<<(std::ostream& stream, const ESM::ContItem& value) + { + return stream << "ESM::ContItem {.mCount = " << value.mCount << ", .mItem = '" << value.mItem << "'}"; + } + + inline bool operator==(const ESM::Region::SoundRef& lhs, const ESM::Region::SoundRef& rhs) + { + return tie(lhs) == tie(rhs); + } + + inline std::ostream& operator<<(std::ostream& stream, const ESM::Region::SoundRef& value) + { + return stream << "ESM::Region::SoundRef {.mSound = '" << value.mSound << "', .mChance = " << value.mChance + << "}"; + } + + inline bool operator==(const ESM::QuickKeys::QuickKey& lhs, const ESM::QuickKeys::QuickKey& rhs) + { + return tie(lhs) == tie(rhs); + } + + inline std::ostream& operator<<(std::ostream& stream, const ESM::QuickKeys::QuickKey& value) + { + return stream << "ESM::QuickKeys::QuickKey {.mType = '" << static_cast(value.mType) + << "', .mId = " << value.mId << "}"; + } + + namespace + { + using namespace ::testing; + + std::vector getFormats() + { + std::vector result({ + CurrentContentFormatVersion, + MaxLimitedSizeStringsFormatVersion, + MaxStringRefIdFormatVersion, + }); + for (ESM::FormatVersion v = result.back() + 1; v <= ESM::CurrentSaveGameFormatVersion; ++v) + result.push_back(v); + return result; + } + + constexpr std::uint32_t fakeRecordId = fourCC("FAKE"); + + template + concept HasSave = requires(T v, ESMWriter& w) + { + v.save(w); + }; + + template + concept NotHasSave = !HasSave; + + template + auto save(const T& record, ESMWriter& writer) + { + record.save(writer); + } + + void save(const CellRef& record, ESMWriter& writer) + { + record.save(writer, true); + } + + template + auto save(const T& record, ESMWriter& writer) + { + writer.writeComposite(record); + } + + template + std::unique_ptr makeEsmStream(const T& record, FormatVersion formatVersion) + { + ESMWriter writer; + auto stream = std::make_unique(); + writer.setFormatVersion(formatVersion); + writer.save(*stream); + writer.startRecord(fakeRecordId); + save(record, writer); + writer.endRecord(fakeRecordId); + return stream; + } + + template + concept HasLoad = requires(T v, ESMReader& r) + { + v.load(r); + }; + + template + concept HasLoadWithDelete = requires(T v, ESMReader& r, bool& d) + { + v.load(r, d); + }; + + template + concept NotHasLoad = !HasLoad && !HasLoadWithDelete; + + template + void load(ESMReader& reader, T& record) + { + record.load(reader); + } + + template + void load(ESMReader& reader, T& record) + { + bool deleted = false; + record.load(reader, deleted); + } + + void load(ESMReader& reader, CellRef& record) + { + bool deleted = false; + record.load(reader, deleted, true); + } + + template + void load(ESMReader& reader, T& record) + { + reader.getComposite(record); + } + + void load(ESMReader& reader, Land& record) + { + bool deleted = false; + record.load(reader, deleted); + if (deleted) + return; + record.mLandData = std::make_unique(); + reader.restoreContext(record.mContext); + loadLandRecordData(record.mDataTypes, reader, *record.mLandData); + } + + template + void saveAndLoadRecord(const T& record, FormatVersion formatVersion, T& result) + { + ESMReader reader; + reader.open(makeEsmStream(record, formatVersion), "stream"); + ASSERT_TRUE(reader.hasMoreRecs()); + ASSERT_EQ(reader.getRecName().toInt(), fakeRecordId); + reader.getRecHeader(); + load(reader, result); + } + + struct Esm3SaveLoadRecordTest : public TestWithParam + { + std::minstd_rand mRandom; + std::uniform_int_distribution mRefIdDistribution{ 'a', 'z' }; + + std::string generateRandomString(std::size_t size) + { + std::string value; + while (value.size() < size) + value.push_back(static_cast(mRefIdDistribution(mRandom))); + return value; + } + + RefId generateRandomRefId(std::size_t size = 33) { return RefId::stringRefId(generateRandomString(size)); } + + template + void generateArray(T (&dst)[n]) + { + for (auto& v : dst) + v = std::uniform_real_distribution{ -1.0f, 1.0f }(mRandom); + } + + void generateBytes(auto iterator, std::size_t count) + { + std::uniform_int_distribution distribution{ 0, + std::numeric_limits::max() }; + std::generate_n(iterator, count, [&] { return static_cast(distribution(mRandom)); }); + } + + void generateStrings(auto iterator, std::size_t count) + { + std::uniform_int_distribution distribution{ 1, 13 }; + std::generate_n(iterator, count, [&] { return generateRandomString(distribution(mRandom)); }); + } + }; + + TEST_F(Esm3SaveLoadRecordTest, headerShouldNotChange) + { + const std::string author = generateRandomString(33); + const std::string description = generateRandomString(257); + + auto stream = std::make_unique(); + + ESMWriter writer; + writer.setAuthor(author); + writer.setDescription(description); + writer.setFormatVersion(CurrentSaveGameFormatVersion); + writer.save(*stream); + writer.close(); + + ESMReader reader; + reader.open(std::move(stream), "stream"); + EXPECT_EQ(reader.getAuthor(), author); + EXPECT_EQ(reader.getDesc(), description); + } + + TEST_F(Esm3SaveLoadRecordTest, containerContItemShouldSupportRefIdLongerThan32) + { + Container record; + record.blank(); + record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 42, .mItem = generateRandomRefId(33) }); + record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 13, .mItem = generateRandomRefId(33) }); + Container result; + saveAndLoadRecord(record, CurrentSaveGameFormatVersion, result); + EXPECT_EQ(result.mInventory.mList, record.mInventory.mList); + } + + TEST_F(Esm3SaveLoadRecordTest, regionSoundRefShouldSupportRefIdLongerThan32) + { + Region record; + record.blank(); + record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(33), .mChance = 42 }); + record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(33), .mChance = 13 }); + Region result; + saveAndLoadRecord(record, CurrentSaveGameFormatVersion, result); + EXPECT_EQ(result.mSoundList, record.mSoundList); + } + + TEST_F(Esm3SaveLoadRecordTest, scriptSoundRefShouldSupportRefIdLongerThan32) + { + Script record; + record.blank(); + record.mId = generateRandomRefId(33); + record.mNumShorts = 42; + Script result; + saveAndLoadRecord(record, CurrentSaveGameFormatVersion, result); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mNumShorts, record.mNumShorts); + } + + TEST_P(Esm3SaveLoadRecordTest, playerShouldNotChange) + { + // Player state is not saved to vanilla ESM format. + if (GetParam() == CurrentContentFormatVersion) + return; + std::minstd_rand random; + Player record{}; + record.mObject.blank(); + record.mBirthsign = generateRandomRefId(); + record.mObject.mRef.mRefID = generateRandomRefId(); + std::generate_n(std::inserter(record.mPreviousItems, record.mPreviousItems.end()), 2, + [&] { return std::make_pair(generateRandomRefId(), generateRandomRefId()); }); + record.mCellId = ESM::RefId::esm3ExteriorCell(0, 0); + generateArray(record.mLastKnownExteriorPosition); + record.mHasMark = true; + record.mMarkedCell = ESM::RefId::esm3ExteriorCell(0, 0); + generateArray(record.mMarkedPosition.pos); + generateArray(record.mMarkedPosition.rot); + record.mCurrentCrimeId = 42; + record.mPaidCrimeId = 13; + Player result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(record.mObject.mRef.mRefID, result.mObject.mRef.mRefID); + EXPECT_EQ(record.mBirthsign, result.mBirthsign); + EXPECT_EQ(record.mPreviousItems, result.mPreviousItems); + EXPECT_EQ(record.mPreviousItems, result.mPreviousItems); + EXPECT_EQ(record.mCellId, result.mCellId); + EXPECT_THAT(record.mLastKnownExteriorPosition, ElementsAreArray(result.mLastKnownExteriorPosition)); + EXPECT_EQ(record.mHasMark, result.mHasMark); + EXPECT_EQ(record.mMarkedCell, result.mMarkedCell); + EXPECT_THAT(record.mMarkedPosition.pos, ElementsAreArray(result.mMarkedPosition.pos)); + EXPECT_THAT(record.mMarkedPosition.rot, ElementsAreArray(result.mMarkedPosition.rot)); + EXPECT_EQ(record.mCurrentCrimeId, result.mCurrentCrimeId); + EXPECT_EQ(record.mPaidCrimeId, result.mPaidCrimeId); + } + + TEST_P(Esm3SaveLoadRecordTest, cellRefShouldNotChange) + { + CellRef record; + record.blank(); + record.mRefNum.mIndex = std::numeric_limits::max(); + record.mRefNum.mContentFile = std::numeric_limits::max(); + record.mRefID = generateRandomRefId(); + record.mScale = 2; + record.mOwner = generateRandomRefId(); + record.mGlobalVariable = generateRandomString(100); + record.mSoul = generateRandomRefId(); + record.mFaction = generateRandomRefId(); + record.mFactionRank = std::numeric_limits::max(); + record.mChargeInt = std::numeric_limits::max(); + record.mEnchantmentCharge = std::numeric_limits::max(); + record.mCount = std::numeric_limits::max(); + record.mTeleport = true; + generateArray(record.mDoorDest.pos); + generateArray(record.mDoorDest.rot); + record.mDestCell = generateRandomString(100); + record.mLockLevel = 0; + record.mIsLocked = true; + record.mKey = generateRandomRefId(); + record.mTrap = generateRandomRefId(); + record.mReferenceBlocked = std::numeric_limits::max(); + generateArray(record.mPos.pos); + generateArray(record.mPos.rot); + CellRef result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(record.mRefNum.mIndex, result.mRefNum.mIndex); + EXPECT_EQ(record.mRefNum.mContentFile, result.mRefNum.mContentFile); + EXPECT_EQ(record.mRefID, result.mRefID); + EXPECT_EQ(record.mScale, result.mScale); + EXPECT_EQ(record.mOwner, result.mOwner); + EXPECT_EQ(record.mGlobalVariable, result.mGlobalVariable); + EXPECT_EQ(record.mSoul, result.mSoul); + EXPECT_EQ(record.mFaction, result.mFaction); + EXPECT_EQ(record.mFactionRank, result.mFactionRank); + EXPECT_EQ(record.mChargeInt, result.mChargeInt); + EXPECT_EQ(record.mEnchantmentCharge, result.mEnchantmentCharge); + EXPECT_EQ(record.mCount, result.mCount); + EXPECT_EQ(record.mTeleport, result.mTeleport); + EXPECT_EQ(record.mDoorDest, result.mDoorDest); + EXPECT_EQ(record.mDestCell, result.mDestCell); + EXPECT_EQ(record.mLockLevel, result.mLockLevel); + EXPECT_EQ(record.mIsLocked, result.mIsLocked); + EXPECT_EQ(record.mKey, result.mKey); + EXPECT_EQ(record.mTrap, result.mTrap); + EXPECT_EQ(record.mReferenceBlocked, result.mReferenceBlocked); + EXPECT_EQ(record.mPos, result.mPos); + } + + TEST_P(Esm3SaveLoadRecordTest, creatureStatsShouldNotChange) + { + CreatureStats record; + record.blank(); + record.mLastHitAttemptObject = generateRandomRefId(); + record.mLastHitObject = generateRandomRefId(); + CreatureStats result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(record.mLastHitAttemptObject, result.mLastHitAttemptObject); + EXPECT_EQ(record.mLastHitObject, result.mLastHitObject); + } + + TEST_P(Esm3SaveLoadRecordTest, containerShouldNotChange) + { + Container record; + record.blank(); + record.mId = generateRandomRefId(); + record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 42, .mItem = generateRandomRefId(32) }); + record.mInventory.mList.push_back(ESM::ContItem{ .mCount = 13, .mItem = generateRandomRefId(32) }); + Container result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mInventory.mList, record.mInventory.mList); + } + + TEST_P(Esm3SaveLoadRecordTest, regionShouldNotChange) + { + Region record; + record.blank(); + record.mId = generateRandomRefId(); + record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(32), .mChance = 42 }); + record.mSoundList.push_back(ESM::Region::SoundRef{ .mSound = generateRandomRefId(32), .mChance = 13 }); + Region result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mSoundList, record.mSoundList); + } + + TEST_P(Esm3SaveLoadRecordTest, scriptShouldNotChange) + { + Script record; + record.blank(); + record.mId = generateRandomRefId(32); + record.mNumShorts = 3; + record.mNumFloats = 4; + record.mNumLongs = 5; + generateStrings( + std::back_inserter(record.mVarNames), record.mNumShorts + record.mNumFloats + record.mNumLongs); + generateBytes(std::back_inserter(record.mScriptData), 13); + record.mScriptText = generateRandomString(17); + + Script result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mNumShorts, record.mNumShorts); + EXPECT_EQ(result.mNumFloats, record.mNumFloats); + EXPECT_EQ(result.mNumShorts, record.mNumShorts); + EXPECT_EQ(result.mVarNames, record.mVarNames); + EXPECT_EQ(result.mScriptData, record.mScriptData); + EXPECT_EQ(result.mScriptText, record.mScriptText); + } + + TEST_P(Esm3SaveLoadRecordTest, quickKeysShouldNotChange) + { + const QuickKeys record { + .mKeys = { + { + .mType = QuickKeys::Type::Magic, + .mId = generateRandomRefId(32), + }, + { + .mType = QuickKeys::Type::MagicItem, + .mId = generateRandomRefId(32), + }, + }, + }; + QuickKeys result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(result.mKeys, record.mKeys); + } + + TEST_P(Esm3SaveLoadRecordTest, dialogueShouldNotChange) + { + Dialogue record; + record.blank(); + record.mStringId = generateRandomString(32); + record.mId = ESM::RefId::stringRefId(record.mStringId); + Dialogue result; + saveAndLoadRecord(record, GetParam(), result); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mStringId, record.mStringId); + } + + TEST_P(Esm3SaveLoadRecordTest, aiSequenceAiWanderShouldNotChange) + { + AiSequence::AiWander record; + record.mData.mDistance = 1; + record.mData.mDuration = 2; + record.mData.mTimeOfDay = 3; + constexpr std::uint8_t idle[8] = { 4, 5, 6, 7, 8, 9, 10, 11 }; + static_assert(std::size(idle) == std::size(record.mData.mIdle)); + std::copy(std::begin(idle), std::end(idle), record.mData.mIdle); + record.mData.mShouldRepeat = 12; + record.mDurationData.mRemainingDuration = 13; + record.mStoredInitialActorPosition = true; + constexpr float initialActorPosition[3] = { 15, 16, 17 }; + static_assert(std::size(initialActorPosition) == std::size(record.mInitialActorPosition.mValues)); + std::copy( + std::begin(initialActorPosition), std::end(initialActorPosition), record.mInitialActorPosition.mValues); + + AiSequence::AiWander result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mDistance, record.mData.mDistance); + EXPECT_EQ(result.mData.mDuration, record.mData.mDuration); + EXPECT_EQ(result.mData.mTimeOfDay, record.mData.mTimeOfDay); + EXPECT_THAT(result.mData.mIdle, ElementsAreArray(record.mData.mIdle)); + EXPECT_EQ(result.mData.mShouldRepeat, record.mData.mShouldRepeat); + EXPECT_EQ(result.mDurationData.mRemainingDuration, record.mDurationData.mRemainingDuration); + EXPECT_EQ(result.mStoredInitialActorPosition, record.mStoredInitialActorPosition); + EXPECT_THAT(result.mInitialActorPosition.mValues, ElementsAreArray(record.mInitialActorPosition.mValues)); + } + + TEST_P(Esm3SaveLoadRecordTest, aiSequenceAiTravelShouldNotChange) + { + AiSequence::AiTravel record; + record.mData.mX = 1; + record.mData.mY = 2; + record.mData.mZ = 3; + record.mHidden = true; + record.mRepeat = true; + + AiSequence::AiTravel result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mX, record.mData.mX); + EXPECT_EQ(result.mData.mY, record.mData.mY); + EXPECT_EQ(result.mData.mZ, record.mData.mZ); + EXPECT_EQ(result.mHidden, record.mHidden); + EXPECT_EQ(result.mRepeat, record.mRepeat); + } + + TEST_P(Esm3SaveLoadRecordTest, aiSequenceAiEscortShouldNotChange) + { + AiSequence::AiEscort record; + record.mData.mX = 1; + record.mData.mY = 2; + record.mData.mZ = 3; + record.mData.mDuration = 4; + record.mTargetActorId = 5; + record.mTargetId = generateRandomRefId(32); + record.mCellId = generateRandomString(257); + record.mRemainingDuration = 6; + record.mRepeat = true; + + AiSequence::AiEscort result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mX, record.mData.mX); + EXPECT_EQ(result.mData.mY, record.mData.mY); + EXPECT_EQ(result.mData.mZ, record.mData.mZ); + if (GetParam() <= MaxOldAiPackageFormatVersion) + EXPECT_EQ(result.mData.mDuration, record.mRemainingDuration); + else + EXPECT_EQ(result.mData.mDuration, record.mData.mDuration); + EXPECT_EQ(result.mTargetActorId, record.mTargetActorId); + EXPECT_EQ(result.mTargetId, record.mTargetId); + EXPECT_EQ(result.mCellId, record.mCellId); + EXPECT_EQ(result.mRemainingDuration, record.mRemainingDuration); + EXPECT_EQ(result.mRepeat, record.mRepeat); + } + + TEST_P(Esm3SaveLoadRecordTest, aiDataShouldNotChange) + { + AIData record = { + .mHello = 1, + .mFight = 2, + .mFlee = 3, + .mAlarm = 4, + .mServices = 5, + }; + + AIData result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mHello, record.mHello); + EXPECT_EQ(result.mFight, record.mFight); + EXPECT_EQ(result.mFlee, record.mFlee); + EXPECT_EQ(result.mAlarm, record.mAlarm); + EXPECT_EQ(result.mServices, record.mServices); + } + + TEST_P(Esm3SaveLoadRecordTest, enamShouldNotChange) + { + EffectList record; + record.mList.emplace_back(IndexedENAMstruct{ { + .mEffectID = 1, + .mSkill = 2, + .mAttribute = 3, + .mRange = 4, + .mArea = 5, + .mDuration = 6, + .mMagnMin = 7, + .mMagnMax = 8, + }, + 0 }); + + EffectList result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mList.size(), record.mList.size()); + EXPECT_EQ(result.mList[0].mData.mEffectID, record.mList[0].mData.mEffectID); + EXPECT_EQ(result.mList[0].mData.mSkill, record.mList[0].mData.mSkill); + EXPECT_EQ(result.mList[0].mData.mAttribute, record.mList[0].mData.mAttribute); + EXPECT_EQ(result.mList[0].mData.mRange, record.mList[0].mData.mRange); + EXPECT_EQ(result.mList[0].mData.mArea, record.mList[0].mData.mArea); + EXPECT_EQ(result.mList[0].mData.mDuration, record.mList[0].mData.mDuration); + EXPECT_EQ(result.mList[0].mData.mMagnMin, record.mList[0].mData.mMagnMin); + EXPECT_EQ(result.mList[0].mData.mMagnMax, record.mList[0].mData.mMagnMax); + } + + TEST_P(Esm3SaveLoadRecordTest, weaponShouldNotChange) + { + Weapon record = { + .mData = { + .mWeight = 0, + .mValue = 1, + .mType = 2, + .mHealth = 3, + .mSpeed = 4, + .mReach = 5, + .mEnchant = 6, + .mChop = { 7, 8 }, + .mSlash = { 9, 10 }, + .mThrust = { 11, 12 }, + .mFlags = 13, + }, + .mRecordFlags = 0, + .mId = generateRandomRefId(32), + .mEnchant = generateRandomRefId(32), + .mScript = generateRandomRefId(32), + .mName = generateRandomString(32), + .mModel = generateRandomString(32), + .mIcon = generateRandomString(32), + }; + + Weapon result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mWeight, record.mData.mWeight); + EXPECT_EQ(result.mData.mValue, record.mData.mValue); + EXPECT_EQ(result.mData.mType, record.mData.mType); + EXPECT_EQ(result.mData.mHealth, record.mData.mHealth); + EXPECT_EQ(result.mData.mSpeed, record.mData.mSpeed); + EXPECT_EQ(result.mData.mReach, record.mData.mReach); + EXPECT_EQ(result.mData.mEnchant, record.mData.mEnchant); + EXPECT_EQ(result.mData.mChop, record.mData.mChop); + EXPECT_EQ(result.mData.mSlash, record.mData.mSlash); + EXPECT_EQ(result.mData.mThrust, record.mData.mThrust); + EXPECT_EQ(result.mData.mFlags, record.mData.mFlags); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mEnchant, record.mEnchant); + EXPECT_EQ(result.mScript, record.mScript); + EXPECT_EQ(result.mName, record.mName); + EXPECT_EQ(result.mModel, record.mModel); + EXPECT_EQ(result.mIcon, record.mIcon); + } + + TEST_P(Esm3SaveLoadRecordTest, infoShouldNotChange) + { + DialInfo record = { + .mData = { + .mType = ESM::Dialogue::Topic, + .mDisposition = 1, + .mRank = 2, + .mGender = ESM::DialInfo::NA, + .mPCrank = 3, + }, + .mSelects = { + ESM::DialogueCondition{ + .mVariable = {}, + .mValue = 42, + .mIndex = 0, + .mFunction = ESM::DialogueCondition::Function_Level, + .mComparison = ESM::DialogueCondition::Comp_Eq + }, + ESM::DialogueCondition{ + .mVariable = generateRandomString(32), + .mValue = 0, + .mIndex = 1, + .mFunction = ESM::DialogueCondition::Function_NotLocal, + .mComparison = ESM::DialogueCondition::Comp_Eq + }, + }, + .mId = generateRandomRefId(32), + .mPrev = generateRandomRefId(32), + .mNext = generateRandomRefId(32), + .mActor = generateRandomRefId(32), + .mRace = generateRandomRefId(32), + .mClass = generateRandomRefId(32), + .mFaction = generateRandomRefId(32), + .mPcFaction = generateRandomRefId(32), + .mCell = generateRandomRefId(32), + .mSound = generateRandomString(32), + .mResponse = generateRandomString(32), + .mResultScript = generateRandomString(32), + .mFactionLess = false, + .mQuestStatus = ESM::DialInfo::QS_None, + }; + + DialInfo result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mData.mType, record.mData.mType); + EXPECT_EQ(result.mData.mDisposition, record.mData.mDisposition); + EXPECT_EQ(result.mData.mRank, record.mData.mRank); + EXPECT_EQ(result.mData.mGender, record.mData.mGender); + EXPECT_EQ(result.mData.mPCrank, record.mData.mPCrank); + EXPECT_EQ(result.mId, record.mId); + EXPECT_EQ(result.mPrev, record.mPrev); + EXPECT_EQ(result.mNext, record.mNext); + EXPECT_EQ(result.mActor, record.mActor); + EXPECT_EQ(result.mRace, record.mRace); + EXPECT_EQ(result.mClass, record.mClass); + EXPECT_EQ(result.mFaction, record.mFaction); + EXPECT_EQ(result.mPcFaction, record.mPcFaction); + EXPECT_EQ(result.mCell, record.mCell); + EXPECT_EQ(result.mSound, record.mSound); + EXPECT_EQ(result.mResponse, record.mResponse); + EXPECT_EQ(result.mResultScript, record.mResultScript); + EXPECT_EQ(result.mFactionLess, record.mFactionLess); + EXPECT_EQ(result.mQuestStatus, record.mQuestStatus); + EXPECT_EQ(result.mSelects.size(), record.mSelects.size()); + for (size_t i = 0; i < result.mSelects.size(); ++i) + { + const auto& resultS = result.mSelects[i]; + const auto& recordS = record.mSelects[i]; + EXPECT_EQ(resultS.mVariable, recordS.mVariable); + EXPECT_EQ(resultS.mValue, recordS.mValue); + EXPECT_EQ(resultS.mIndex, recordS.mIndex); + EXPECT_EQ(resultS.mFunction, recordS.mFunction); + EXPECT_EQ(resultS.mComparison, recordS.mComparison); + } + } + + TEST_P(Esm3SaveLoadRecordTest, landShouldNotChange) + { + LandRecordData data; + std::iota(data.mHeights.begin(), data.mHeights.end(), 1); + 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()); + std::iota(data.mNormals.begin(), data.mNormals.end(), 2); + std::iota(data.mTextures.begin(), data.mTextures.end(), 3); + std::iota(data.mColours.begin(), data.mColours.end(), 4); + data.mDataLoaded = Land::DATA_VNML | Land::DATA_VHGT | Land::DATA_VCLR | Land::DATA_VTEX; + + Land record; + record.mFlags = Land::Flag_HeightsNormals | Land::Flag_Colors | Land::Flag_Textures; + record.mX = 2; + record.mY = 3; + record.mDataTypes = Land::DATA_VNML | Land::DATA_VHGT | Land::DATA_WNAM | Land::DATA_VCLR | Land::DATA_VTEX; + generateWnam(data.mHeights, record.mWnam); + record.mLandData = std::make_unique(data); + + Land result; + saveAndLoadRecord(record, GetParam(), result); + + EXPECT_EQ(result.mFlags, record.mFlags); + EXPECT_EQ(result.mX, record.mX); + EXPECT_EQ(result.mY, record.mY); + EXPECT_EQ(result.mDataTypes, record.mDataTypes); + EXPECT_EQ(result.mWnam, record.mWnam); + EXPECT_EQ(result.mLandData->mHeights, record.mLandData->mHeights); + EXPECT_EQ(result.mLandData->mMinHeight, record.mLandData->mMinHeight); + EXPECT_EQ(result.mLandData->mMaxHeight, record.mLandData->mMaxHeight); + EXPECT_EQ(result.mLandData->mNormals, record.mLandData->mNormals); + EXPECT_EQ(result.mLandData->mTextures, record.mLandData->mTextures); + EXPECT_EQ(result.mLandData->mColours, record.mLandData->mColours); + EXPECT_EQ(result.mLandData->mDataLoaded, record.mLandData->mDataLoaded); + } + + INSTANTIATE_TEST_SUITE_P(FormatVersions, Esm3SaveLoadRecordTest, ValuesIn(getFormats())); + } +} diff --git a/apps/components_tests/esm4/includes.cpp b/apps/components_tests/esm4/includes.cpp new file mode 100644 index 00000000000..f2ea31241e7 --- /dev/null +++ b/apps/components_tests/esm4/includes.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/apps/components_tests/esmloader/esmdata.cpp b/apps/components_tests/esmloader/esmdata.cpp new file mode 100644 index 00000000000..dba53a4a945 --- /dev/null +++ b/apps/components_tests/esmloader/esmdata.cpp @@ -0,0 +1,114 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace EsmLoader; + + struct Params + { + std::string mRefId; + ESM::RecNameInts mType; + std::string mResult; + std::function mPushBack; + }; + + struct EsmLoaderGetModelTest : TestWithParam + { + }; + + TEST_P(EsmLoaderGetModelTest, shouldReturnFoundModelName) + { + EsmData data; + GetParam().mPushBack(data); + EXPECT_EQ(EsmLoader::getModel(data, ESM::RefId::stringRefId(GetParam().mRefId), GetParam().mType), + GetParam().mResult); + } + + void pushBack(ESM::Activator&& value, EsmData& esmData) + { + esmData.mActivators.push_back(std::move(value)); + } + + void pushBack(ESM::Container&& value, EsmData& esmData) + { + esmData.mContainers.push_back(std::move(value)); + } + + void pushBack(ESM::Door&& value, EsmData& esmData) + { + esmData.mDoors.push_back(std::move(value)); + } + + void pushBack(ESM::Static&& value, EsmData& esmData) + { + esmData.mStatics.push_back(std::move(value)); + } + + template + struct PushBack + { + std::string mId; + std::string mModel; + + void operator()(EsmData& esmData) const + { + T value; + value.mId = ESM::RefId::stringRefId(mId); + value.mModel = mModel; + pushBack(std::move(value), esmData); + } + }; + + const std::array params = { + Params{ "acti_ref_id", ESM::REC_ACTI, "acti_model", PushBack{ "acti_ref_id", "acti_model" } }, + Params{ "cont_ref_id", ESM::REC_CONT, "cont_model", PushBack{ "cont_ref_id", "cont_model" } }, + Params{ "door_ref_id", ESM::REC_DOOR, "door_model", PushBack{ "door_ref_id", "door_model" } }, + Params{ + "static_ref_id", ESM::REC_STAT, "static_model", PushBack{ "static_ref_id", "static_model" } }, + Params{ "acti_ref_id_a", ESM::REC_ACTI, "", PushBack{ "acti_ref_id_z", "acti_model" } }, + Params{ "cont_ref_id_a", ESM::REC_CONT, "", PushBack{ "cont_ref_id_z", "cont_model" } }, + Params{ "door_ref_id_a", ESM::REC_DOOR, "", PushBack{ "door_ref_id_z", "door_model" } }, + Params{ "static_ref_id_a", ESM::REC_STAT, "", PushBack{ "static_ref_id_z", "static_model" } }, + Params{ "acti_ref_id_z", ESM::REC_ACTI, "", PushBack{ "acti_ref_id_a", "acti_model" } }, + Params{ "cont_ref_id_z", ESM::REC_CONT, "", PushBack{ "cont_ref_id_a", "cont_model" } }, + Params{ "door_ref_id_z", ESM::REC_DOOR, "", PushBack{ "door_ref_id_a", "door_model" } }, + Params{ "static_ref_id_z", ESM::REC_STAT, "", PushBack{ "static_ref_id_a", "static_model" } }, + Params{ "ref_id", ESM::REC_STAT, "", [](EsmData&) {} }, + Params{ "ref_id", ESM::REC_BOOK, "", [](EsmData&) {} }, + }; + + INSTANTIATE_TEST_SUITE_P(Params, EsmLoaderGetModelTest, ValuesIn(params)); + + TEST(EsmLoaderGetGameSettingTest, shouldReturnFoundValue) + { + std::vector settings; + ESM::GameSetting setting; + setting.mId = ESM::RefId::stringRefId("setting"); + setting.mValue = ESM::Variant(42); + setting.mRecordFlags = 0; + settings.push_back(setting); + EXPECT_EQ(EsmLoader::getGameSetting(settings, "setting"), ESM::Variant(42)); + } + + TEST(EsmLoaderGetGameSettingTest, shouldThrowExceptionWhenNotFound) + { + const std::vector settings; + EXPECT_THROW(EsmLoader::getGameSetting(settings, "setting"), std::runtime_error); + } +} diff --git a/apps/components_tests/esmloader/load.cpp b/apps/components_tests/esmloader/load.cpp new file mode 100644 index 00000000000..20e06507d13 --- /dev/null +++ b/apps/components_tests/esmloader/load.cpp @@ -0,0 +1,136 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifndef OPENMW_DATA_DIR +#error "OPENMW_DATA_DIR is not defined" +#endif + +namespace +{ + using namespace testing; + using namespace EsmLoader; + + struct EsmLoaderTest : Test + { + const Files::PathContainer mDataDirs{ { std::filesystem::path{ OPENMW_DATA_DIR } } }; + const Files::Collections mFileCollections{ mDataDirs }; + const std::vector mContentFiles{ { "template.omwgame" } }; + }; + + TEST_F(EsmLoaderTest, loadEsmDataShouldSupportOmwgame) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + ESM::ReadersCache readers; + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 1); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 1521); + EXPECT_EQ(esmData.mLands.size(), 1); + EXPECT_EQ(esmData.mStatics.size(), 2); + } + + TEST_F(EsmLoaderTest, shouldIgnoreCellsWhenQueryLoadCellsIsFalse) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = false; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + ESM::ReadersCache readers; + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 0); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 1521); + EXPECT_EQ(esmData.mLands.size(), 1); + EXPECT_EQ(esmData.mStatics.size(), 2); + } + + TEST_F(EsmLoaderTest, shouldIgnoreCellsGameSettingsWhenQueryLoadGameSettingsIsFalse) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = false; + query.mLoadLands = true; + query.mLoadStatics = true; + ESM::ReadersCache readers; + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 1); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 0); + EXPECT_EQ(esmData.mLands.size(), 1); + EXPECT_EQ(esmData.mStatics.size(), 2); + } + + TEST_F(EsmLoaderTest, shouldIgnoreAllWithDefaultQuery) + { + const Query query; + ESM::ReadersCache readers; + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 0); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 0); + EXPECT_EQ(esmData.mLands.size(), 0); + EXPECT_EQ(esmData.mStatics.size(), 0); + } + + TEST_F(EsmLoaderTest, loadEsmDataShouldSkipUnsupportedFormats) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + const std::vector contentFiles{ { "script.omwscripts" } }; + ESM::ReadersCache readers; + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, contentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 0); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 0); + EXPECT_EQ(esmData.mLands.size(), 0); + EXPECT_EQ(esmData.mStatics.size(), 0); + } +} diff --git a/apps/components_tests/esmloader/record.cpp b/apps/components_tests/esmloader/record.cpp new file mode 100644 index 00000000000..361c714b7db --- /dev/null +++ b/apps/components_tests/esmloader/record.cpp @@ -0,0 +1,77 @@ +#include + +#include +#include + +#include +#include + +namespace +{ + using namespace testing; + using namespace EsmLoader; + + struct Value + { + int mKey; + int mValue; + }; + + auto tie(const Value& v) + { + return std::tie(v.mKey, v.mValue); + } + + bool operator==(const Value& l, const Value& r) + { + return tie(l) == tie(r); + } + + std::ostream& operator<<(std::ostream& s, const Value& v) + { + return s << "Value {" << v.mKey << ", " << v.mValue << "}"; + } + + Record present(const Value& v) + { + return Record(false, v); + } + + Record deleted(const Value& v) + { + return Record(true, v); + } + + struct Params + { + Records mRecords; + std::vector mResult; + }; + + struct EsmLoaderPrepareRecordTest : TestWithParam + { + }; + + TEST_P(EsmLoaderPrepareRecordTest, prepareRecords) + { + auto records = GetParam().mRecords; + const auto getKey = [&](const Record& v) { return v.mValue.mKey; }; + EXPECT_THAT(prepareRecords(records, getKey), ElementsAreArray(GetParam().mResult)); + } + + const std::array params = { + Params{ {}, {} }, + Params{ { present(Value{ 1, 1 }) }, { Value{ 1, 1 } } }, + Params{ { deleted(Value{ 1, 1 }) }, {} }, + Params{ { present(Value{ 1, 1 }), present(Value{ 2, 2 }) }, { Value{ 1, 1 }, Value{ 2, 2 } } }, + Params{ { present(Value{ 2, 2 }), present(Value{ 1, 1 }) }, { Value{ 1, 1 }, Value{ 2, 2 } } }, + Params{ { present(Value{ 1, 1 }), present(Value{ 1, 2 }) }, { Value{ 1, 2 } } }, + Params{ { present(Value{ 1, 2 }), present(Value{ 1, 1 }) }, { Value{ 1, 1 } } }, + Params{ { present(Value{ 1, 1 }), deleted(Value{ 1, 2 }) }, {} }, + Params{ { deleted(Value{ 1, 1 }), present(Value{ 1, 2 }) }, { Value{ 1, 2 } } }, + Params{ { present(Value{ 1, 2 }), deleted(Value{ 1, 1 }) }, {} }, + Params{ { deleted(Value{ 1, 2 }), present(Value{ 1, 1 }) }, { Value{ 1, 1 } } }, + }; + + INSTANTIATE_TEST_SUITE_P(Params, EsmLoaderPrepareRecordTest, ValuesIn(params)); +} diff --git a/apps/components_tests/esmterrain/testgridsampling.cpp b/apps/components_tests/esmterrain/testgridsampling.cpp new file mode 100644 index 00000000000..5695cf1a54b --- /dev/null +++ b/apps/components_tests/esmterrain/testgridsampling.cpp @@ -0,0 +1,491 @@ +#include + +#include +#include + +namespace ESMTerrain +{ + namespace + { + using namespace testing; + + struct Sample + { + std::size_t mCellX = 0; + std::size_t mCellY = 0; + std::size_t mLocalX = 0; + std::size_t mLocalY = 0; + std::size_t mVertexX = 0; + std::size_t mVertexY = 0; + }; + + auto tie(const Sample& v) + { + return std::tie(v.mCellX, v.mCellY, v.mLocalX, v.mLocalY, v.mVertexX, v.mVertexY); + } + + bool operator==(const Sample& l, const Sample& r) + { + return tie(l) == tie(r); + } + + std::ostream& operator<<(std::ostream& stream, const Sample& v) + { + return stream << "Sample{.mCellX = " << v.mCellX << ", .mCellY = " << v.mCellY + << ", .mLocalX = " << v.mLocalX << ", .mLocalY = " << v.mLocalY + << ", .mVertexX = " << v.mVertexX << ", .mVertexY = " << v.mVertexY << "}"; + } + + struct Collect + { + std::vector& mSamples; + + void operator()(std::size_t cellX, std::size_t cellY, std::size_t localX, std::size_t localY, + std::size_t vertexX, std::size_t vertexY) + { + mSamples.push_back(Sample{ + .mCellX = cellX, + .mCellY = cellY, + .mLocalX = localX, + .mLocalY = localY, + .mVertexX = vertexX, + .mVertexY = vertexY, + }); + } + }; + + TEST(ESMTerrainSampleCellGrid, doesNotSupportCellSizeLessThanTwo) + { + const std::size_t cellSize = 2; + EXPECT_THROW(sampleCellGrid(cellSize, 0, 0, 0, 0, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, doesNotSupportCellSizeMinusOneNotPowerOfTwo) + { + const std::size_t cellSize = 4; + EXPECT_THROW(sampleCellGrid(cellSize, 0, 0, 0, 0, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, doesNotSupportZeroSampleSize) + { + const std::size_t cellSize = 1; + const std::size_t sampleSize = 0; + EXPECT_THROW(sampleCellGrid(cellSize, sampleSize, 0, 0, 0, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, doesNotSupportSampleSizeNotPowerOfTwo) + { + const std::size_t cellSize = 1; + const std::size_t sampleSize = 3; + EXPECT_THROW(sampleCellGrid(cellSize, sampleSize, 0, 0, 0, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, doesNotSupportCountLessThanTwo) + { + const std::size_t cellSize = 1; + const std::size_t sampleSize = 1; + const std::size_t distance = 2; + EXPECT_THROW(sampleCellGrid(cellSize, sampleSize, 0, 0, distance, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, doesNotSupportCountMinusOneNotPowerOfTwo) + { + const std::size_t cellSize = 1; + const std::size_t sampleSize = 1; + const std::size_t distance = 4; + EXPECT_THROW(sampleCellGrid(cellSize, sampleSize, 0, 0, distance, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, sampleSizeOneShouldProduceNumberOfSamplesEqualToCellSize) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 3; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 })); + } + + TEST(ESMTerrainSampleCellGrid, countShouldLimitScope) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 2; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, beginXAndCountShouldLimitScope) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 1; + const std::size_t beginY = 0; + const std::size_t distance = 2; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, beginYAndCountShouldLimitScope) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 0; + const std::size_t beginY = 1; + const std::size_t distance = 2; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 1, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, beginAndCountShouldLimitScope) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 1; + const std::size_t beginY = 1; + const std::size_t distance = 2; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, beginAndCountShouldLimitScopeInTheMiddleOfCell) + { + const std::size_t cellSize = 5; + const std::size_t sampleSize = 1; + const std::size_t beginX = 1; + const std::size_t beginY = 1; + const std::size_t distance = 2; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, beginXWithCountLessThanCellSizeShouldLimitScopeAcrossCellBorder) + { + const std::size_t cellSize = 5; + const std::size_t sampleSize = 1; + const std::size_t beginX = 3; + const std::size_t beginY = 0; + const std::size_t distance = 3; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 3, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 4, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 3, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 4, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 3, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 4, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 })); + } + + TEST(ESMTerrainSampleCellGrid, beginXWithCountEqualToCellSizeShouldLimitScopeAcrossCellBorder) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 1; + const std::size_t beginY = 0; + const std::size_t distance = 3; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 })); + } + + TEST(ESMTerrainSampleCellGrid, beginXWithCountGreaterThanCellSizeShouldLimitScopeAcrossCellBorder) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 1; + const std::size_t beginY = 0; + const std::size_t distance = 5; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 3, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 3, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 3, .mVertexY = 2 }, + Sample{ .mCellX = 2, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 4, .mVertexY = 0 }, + Sample{ .mCellX = 2, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 4, .mVertexY = 1 }, + Sample{ .mCellX = 2, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 4, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 3 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 3 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 1, .mLocalY = 2, .mVertexX = 0, .mVertexY = 4 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 4 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 1, .mLocalY = 1, .mVertexX = 2, .mVertexY = 3 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 2, .mLocalY = 1, .mVertexX = 3, .mVertexY = 3 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 1, .mLocalY = 2, .mVertexX = 2, .mVertexY = 4 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 3, .mVertexY = 4 }, + Sample{ .mCellX = 2, .mCellY = 1, .mLocalX = 1, .mLocalY = 1, .mVertexX = 4, .mVertexY = 3 }, + Sample{ .mCellX = 2, .mCellY = 1, .mLocalX = 1, .mLocalY = 2, .mVertexX = 4, .mVertexY = 4 })); + } + + TEST(ESMTerrainSampleCellGrid, sampleSizeGreaterThanOneShouldSkipPoints) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 2; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 3; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, shouldGroupByCell) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 2; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 5; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 })); + } + + TEST(ESMTerrainSampleCellGrid, sampleSizeGreaterThanCellSizeShouldPickSinglePointPerCell) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 4; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 9; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 3, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 3, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 3, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 3, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 3, .mCellY = 3, .mLocalX = 2, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 })); + } + + TEST(ESMTerrainSampleCellGrid, sampleSizeGreaterThan2CellSizeShouldSkipCells) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 8; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 9; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 3, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 3, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 3, .mCellY = 3, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 })); + } + + auto tie(const CellSample& v) + { + return std::tie(v.mCellX, v.mCellY, v.mSrcRow, v.mSrcCol, v.mDstRow, v.mDstCol); + } + } + + static bool operator==(const CellSample& l, const CellSample& r) + { + return tie(l) == tie(r); + } + + static std::ostream& operator<<(std::ostream& stream, const CellSample& v) + { + return stream << "CellSample{.mCellX = " << v.mCellX << ", .mCellY = " << v.mCellY + << ", .mSrcRow = " << v.mSrcRow << ", .mSrcCol = " << v.mSrcCol << ", .mDstRow = " << v.mDstRow + << ", .mDstCol = " << v.mDstCol << "}"; + } + + namespace + { + struct CollectCellSamples + { + std::vector& mSamples; + + void operator()(const CellSample& value) { mSamples.push_back(value); } + }; + + TEST(ESMTerrainSampleBlendmaps, doesNotSupportNotPositiveSize) + { + const float size = 0; + EXPECT_THROW(sampleBlendmaps(size, 0, 0, 0, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleBlendmaps, doesNotSupportNotPositiveTextureSize) + { + const float size = 1; + const int textureSize = 0; + EXPECT_THROW(sampleBlendmaps(size, 0, 0, textureSize, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleBlendmaps, shouldDecrementBeginRow) + { + const float size = 0.125f; + const float minX = 0.125f; + const float minY = 0.125f; + const int textureSize = 8; + std::vector samples; + sampleBlendmaps(size, minX, minY, textureSize, CollectCellSamples{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 0, .mDstCol = 0 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 1, .mDstCol = 0 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 2, .mDstRow = 0, .mDstCol = 1 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 2, .mDstRow = 1, .mDstCol = 1 })); + } + + TEST(ESMTerrainSampleBlendmaps, shouldDecrementBeginRowOverCellBorder) + { + const float size = 0.125f; + const float minX = 0; + const float minY = 0; + const int textureSize = 8; + std::vector samples; + sampleBlendmaps(size, minX, minY, textureSize, CollectCellSamples{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 7, .mSrcCol = 0, .mDstRow = 0, .mDstCol = 0 }, + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 7, .mSrcCol = 1, .mDstRow = 0, .mDstCol = 1 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 1, .mDstCol = 0 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 1, .mDstCol = 1 })); + } + + TEST(ESMTerrainSampleBlendmaps, shouldSupportNegativeCoordinates) + { + const float size = 0.125f; + const float minX = -0.5f; + const float minY = -0.5f; + const int textureSize = 8; + std::vector samples; + sampleBlendmaps(size, minX, minY, textureSize, CollectCellSamples{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 3, .mSrcCol = 4, .mDstRow = 0, .mDstCol = 0 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 4, .mSrcCol = 4, .mDstRow = 1, .mDstCol = 0 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 3, .mSrcCol = 5, .mDstRow = 0, .mDstCol = 1 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 4, .mSrcCol = 5, .mDstRow = 1, .mDstCol = 1 })); + } + + TEST(ESMTerrainSampleBlendmaps, shouldCoverMultipleCells) + { + const float size = 2; + const float minX = -1.5f; + const float minY = -1.5f; + const int textureSize = 2; + std::vector samples; + sampleBlendmaps(size, minX, minY, textureSize, CollectCellSamples{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + CellSample{ .mCellX = -2, .mCellY = -2, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 0, .mDstCol = 0 }, + CellSample{ .mCellX = -2, .mCellY = -2, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 1, .mDstCol = 0 }, + CellSample{ .mCellX = -1, .mCellY = -2, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 2, .mDstCol = 0 }, + CellSample{ .mCellX = -1, .mCellY = -2, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 3, .mDstCol = 0 }, + CellSample{ .mCellX = 0, .mCellY = -2, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 4, .mDstCol = 0 }, + CellSample{ .mCellX = -2, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 0, .mDstCol = 1 }, + CellSample{ .mCellX = -2, .mCellY = -1, .mSrcRow = 1, .mSrcCol = 0, .mDstRow = 1, .mDstCol = 1 }, + CellSample{ .mCellX = -2, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 0, .mDstCol = 2 }, + CellSample{ .mCellX = -2, .mCellY = -1, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 1, .mDstCol = 2 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 2, .mDstCol = 1 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 1, .mSrcCol = 0, .mDstRow = 3, .mDstCol = 1 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 2, .mDstCol = 2 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 3, .mDstCol = 2 }, + CellSample{ .mCellX = 0, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 4, .mDstCol = 1 }, + CellSample{ .mCellX = 0, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 4, .mDstCol = 2 }, + CellSample{ .mCellX = -2, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 0, .mDstCol = 3 }, + CellSample{ .mCellX = -2, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 0, .mDstRow = 1, .mDstCol = 3 }, + CellSample{ .mCellX = -2, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 0, .mDstCol = 4 }, + CellSample{ .mCellX = -2, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 1, .mDstCol = 4 }, + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 2, .mDstCol = 3 }, + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 0, .mDstRow = 3, .mDstCol = 3 }, + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 2, .mDstCol = 4 }, + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 3, .mDstCol = 4 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 4, .mDstCol = 3 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 4, .mDstCol = 4 })); + } + } +} diff --git a/apps/components_tests/files/conversion_tests.cpp b/apps/components_tests/files/conversion_tests.cpp new file mode 100644 index 00000000000..6c9d801b9ca --- /dev/null +++ b/apps/components_tests/files/conversion_tests.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace Files; + + constexpr auto test_path_u8 = u8"./tmp/ÒĎƎɠˠΏЌԹעڨ/ऊঋਐઊଊ/ஐఋಋഊ/ฎນ༈ႩᄇḮὯ⁂₁₩ℒ/Ⅷ↝∑/☝✌〥ぐズ㌎丕.갔3갛"; + constexpr auto test_path = "./tmp/ÒĎƎɠˠΏЌԹעڨ/ऊঋਐઊଊ/ஐఋಋഊ/ฎນ༈ႩᄇḮὯ⁂₁₩ℒ/Ⅷ↝∑/☝✌〥ぐズ㌎丕.갔3갛"; + + TEST(OpenMWConversion, should_support_unicode_string_to_path) + { + auto p = Files::pathFromUnicodeString(test_path); + EXPECT_EQ(Misc::StringUtils::u8StringToString(p.u8string()), Misc::StringUtils::u8StringToString(test_path_u8)); + } + + TEST(OpenMWConversion, should_support_path_to_unicode_string) + { + std::filesystem::path p{ test_path_u8 }; + EXPECT_EQ(Files::pathToUnicodeString(p), test_path); + } +} diff --git a/apps/components_tests/files/hash.cpp b/apps/components_tests/files/hash.cpp new file mode 100644 index 00000000000..793965112b3 --- /dev/null +++ b/apps/components_tests/files/hash.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace TestingOpenMW; + using namespace Files; + + struct Params + { + std::size_t mSize; + std::array mHash; + }; + + struct FilesGetHash : TestWithParam + { + }; + + TEST(FilesGetHash, shouldClearErrors) + { + const auto fileName = temporaryFilePath("fileName"); + std::string content; + std::fill_n(std::back_inserter(content), 1, 'a'); + std::istringstream stream(content); + stream.exceptions(std::ios::failbit | std::ios::badbit); + EXPECT_THAT(getHash(Files::pathToUnicodeString(fileName), stream), + ElementsAre(9607679276477937801ull, 16624257681780017498ull)); + } + + TEST_P(FilesGetHash, shouldReturnHashForStringStream) + { + const auto fileName = temporaryFilePath("fileName"); + std::string content; + std::fill_n(std::back_inserter(content), GetParam().mSize, 'a'); + std::istringstream stream(content); + EXPECT_EQ(getHash(Files::pathToUnicodeString(fileName), stream), GetParam().mHash); + } + + TEST_P(FilesGetHash, shouldReturnHashForConstrainedFileStream) + { + std::string fileName(UnitTest::GetInstance()->current_test_info()->name()); + std::replace(fileName.begin(), fileName.end(), '/', '_'); + std::string content; + std::fill_n(std::back_inserter(content), GetParam().mSize, 'a'); + const auto file = outputFilePath(fileName); + std::fstream(file, std::ios_base::out | std::ios_base::binary) + .write(content.data(), static_cast(content.size())); + const auto stream = Files::openConstrainedFileStream(file, 0, content.size()); + EXPECT_EQ(getHash(Files::pathToUnicodeString(file), *stream), GetParam().mHash); + } + + INSTANTIATE_TEST_SUITE_P(Params, FilesGetHash, + Values(Params{ 0, { 0, 0 } }, Params{ 1, { 9607679276477937801ull, 16624257681780017498ull } }, + Params{ 128, { 15287858148353394424ull, 16818615825966581310ull } }, + Params{ 1000, { 11018119256083894017ull, 6631144854802791578ull } }, + Params{ 4096, { 11972283295181039100ull, 16027670129106775155ull } }, + Params{ 4097, { 16717956291025443060ull, 12856404199748778153ull } }, + Params{ 5000, { 15775925571142117787ull, 10322955217889622896ull } })); +} diff --git a/apps/components_tests/fx/lexer.cpp b/apps/components_tests/fx/lexer.cpp new file mode 100644 index 00000000000..9c095a1f642 --- /dev/null +++ b/apps/components_tests/fx/lexer.cpp @@ -0,0 +1,259 @@ +#include + +#include + +namespace +{ + using namespace testing; + using namespace fx::Lexer; + + struct LexerTest : Test + { + }; + + struct LexerSingleTokenTest : Test + { + template + void test() + { + const std::string content = std::string(Token::repr); + Lexer lexer(content); + + EXPECT_TRUE(std::holds_alternative(lexer.next())); + } + }; + + TEST_F(LexerSingleTokenTest, single_token_shared) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_technique) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_render_target) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_vertex) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_fragment) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_compute) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_sampler_1d) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_sampler_2d) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_sampler_3d) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_true) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_false) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_vec2) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_vec3) + { + test(); + } + TEST_F(LexerSingleTokenTest, single_token_vec4) + { + test(); + } + + TEST(LexerTest, peek_whitespace_only_content_should_be_eof) + { + Lexer lexer(R"( + + )"); + + EXPECT_TRUE(std::holds_alternative(lexer.peek())); + } + + TEST(LexerTest, float_with_no_prefixed_digits) + { + Lexer lexer(R"( + 0.123; + )"); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + EXPECT_FLOAT_EQ(std::get(token).value, 0.123f); + } + + TEST(LexerTest, float_with_alpha_prefix) + { + Lexer lexer(R"( + abc.123; + )"); + + EXPECT_TRUE(std::holds_alternative(lexer.next())); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + EXPECT_FLOAT_EQ(std::get(token).value, 0.123f); + } + + TEST(LexerTest, float_with_numeric_prefix) + { + Lexer lexer(R"( + 123.123; + )"); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + EXPECT_FLOAT_EQ(std::get(token).value, 123.123f); + } + + TEST(LexerTest, int_should_not_be_float) + { + Lexer lexer(R"( + 123 + )"); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + EXPECT_EQ(std::get(token).value, 123); + } + + TEST(LexerTest, simple_string) + { + Lexer lexer(R"( + "test string" + )"); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + + std::string parsed = std::string(std::get(token).value); + EXPECT_EQ("test string", parsed); + } + + TEST(LexerTest, fail_on_unterminated_double_quotes) + { + Lexer lexer(R"( + "unterminated string' + )"); + + EXPECT_THROW(lexer.next(), LexerException); + } + + TEST(LexerTest, multiline_strings_with_single_quotes) + { + Lexer lexer(R"( + "string that is + on multiple with 'single quotes' + and correctly terminated!" + )"); + + auto token = lexer.next(); + EXPECT_TRUE(std::holds_alternative(token)); + } + + TEST(LexerTest, fail_on_unterminated_double_quotes_with_multiline_strings) + { + Lexer lexer(R"( + "string that is + on multiple with 'single quotes' + and but is unterminated :( + )"); + + EXPECT_THROW(lexer.next(), LexerException); + } + + TEST(LexerTest, jump_with_single_nested_bracket) + { + const std::string content = R"( + #version 120 + + void main() + { + return 0; + }})"; + + const std::string expected = content.substr(0, content.size() - 1); + + Lexer lexer(content); + + auto block = lexer.jump(); + + EXPECT_NE(block, std::nullopt); + EXPECT_EQ(expected, std::string(block.value())); + } + + TEST(LexerTest, jump_with_single_line_comments_and_mismatching_brackets) + { + const std::string content = R"( + #version 120 + + void main() + { + // } + return 0; + }})"; + + const std::string expected = content.substr(0, content.size() - 1); + + Lexer lexer(content); + + auto block = lexer.jump(); + + EXPECT_NE(block, std::nullopt); + EXPECT_EQ(expected, std::string(block.value())); + } + + TEST(LexerTest, jump_with_multi_line_comments_and_mismatching_brackets) + { + const std::string content = R"( + #version 120 + + void main() + { + /* + } + */ + return 0; + }})"; + + const std::string expected = content.substr(0, content.size() - 1); + + Lexer lexer(content); + + auto block = lexer.jump(); + + EXPECT_NE(block, std::nullopt); + EXPECT_EQ(expected, std::string(block.value())); + } + + TEST(LexerTest, immediate_closed_blocks) + { + Lexer lexer(R"(block{})"); + + EXPECT_TRUE(std::holds_alternative(lexer.next())); + EXPECT_TRUE(std::holds_alternative(lexer.next())); + auto block = lexer.jump(); + EXPECT_TRUE(block.has_value()); + EXPECT_TRUE(block.value().empty()); + EXPECT_TRUE(std::holds_alternative(lexer.next())); + } + +} diff --git a/apps/components_tests/fx/technique.cpp b/apps/components_tests/fx/technique.cpp new file mode 100644 index 00000000000..ad57074b18f --- /dev/null +++ b/apps/components_tests/fx/technique.cpp @@ -0,0 +1,204 @@ +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ + constexpr VFS::Path::NormalizedView techniquePropertiesPath("shaders/technique_properties.omwfx"); + + TestingOpenMW::VFSTestFile techniqueProperties(R"( + fragment main {} + vertex main {} + technique { + passes = main; + version = "0.1a"; + description = "description"; + author = "author"; + glsl_version = 330; + glsl_profile = "compatability"; + glsl_extensions = GL_EXT_gpu_shader4, GL_ARB_uniform_buffer_object; + flags = disable_sunglare; + hdr = true; + } +)"); + + constexpr VFS::Path::NormalizedView rendertargetPropertiesPath("shaders/rendertarget_properties.omwfx"); + + TestingOpenMW::VFSTestFile rendertargetProperties{ R"( + render_target rendertarget { + width_ratio = 0.5; + height_ratio = 0.5; + internal_format = r16f; + source_type = float; + source_format = red; + mipmaps = true; + wrap_s = clamp_to_edge; + wrap_t = repeat; + min_filter = linear; + mag_filter = nearest; + } + fragment downsample2x(target=rendertarget) { + + omw_In vec2 omw_TexCoord; + + void main() + { + omw_FragColor.r = omw_GetLastShader(omw_TexCoord).r; + } + } + fragment main { } + technique { passes = downsample2x, main; } +)" }; + + constexpr VFS::Path::NormalizedView uniformPropertiesPath("shaders/uniform_properties.omwfx"); + + TestingOpenMW::VFSTestFile uniformProperties{ R"( + uniform_vec4 uVec4 { + default = vec4(0,0,0,0); + min = vec4(0,1,0,0); + max = vec4(0,0,1,0); + step = 0.5; + header = "header"; + static = true; + description = "description"; + } + fragment main { } + technique { passes = main; } +)" }; + + constexpr VFS::Path::NormalizedView missingSamplerSourcePath("shaders/missing_sampler_source.omwfx"); + + TestingOpenMW::VFSTestFile missingSamplerSource{ R"( + sampler_1d mysampler1d { } + fragment main { } + technique { passes = main; } +)" }; + + constexpr VFS::Path::NormalizedView repeatedSharedBlockPath("shaders/repeated_shared_block.omwfx"); + + TestingOpenMW::VFSTestFile repeatedSharedBlock{ R"( + shared { + float myfloat = 1.0; + } + shared {} + fragment main { } + technique { passes = main; } +)" }; + + using namespace testing; + using namespace fx; + + struct TechniqueTest : Test + { + std::unique_ptr mVFS; + Resource::ImageManager mImageManager; + std::unique_ptr mTechnique; + + TechniqueTest() + : mVFS(TestingOpenMW::createTestVFS({ + { techniquePropertiesPath, &techniqueProperties }, + { rendertargetPropertiesPath, &rendertargetProperties }, + { uniformPropertiesPath, &uniformProperties }, + { missingSamplerSourcePath, &missingSamplerSource }, + { repeatedSharedBlockPath, &repeatedSharedBlock }, + })) + , mImageManager(mVFS.get(), 0) + { + } + + void compile(const std::string& name) + { + mTechnique = std::make_unique(*mVFS.get(), mImageManager, name, 1, 1, true, true); + mTechnique->compile(); + } + }; + + TEST_F(TechniqueTest, technique_properties) + { + std::unordered_set targetExtensions = { "GL_EXT_gpu_shader4", "GL_ARB_uniform_buffer_object" }; + + compile("technique_properties"); + + EXPECT_EQ(mTechnique->getVersion(), "0.1a"); + EXPECT_EQ(mTechnique->getDescription(), "description"); + EXPECT_EQ(mTechnique->getAuthor(), "author"); + EXPECT_EQ(mTechnique->getGLSLVersion(), 330); + EXPECT_EQ(mTechnique->getGLSLProfile(), "compatability"); + EXPECT_EQ(mTechnique->getGLSLExtensions(), targetExtensions); + EXPECT_EQ(mTechnique->getFlags(), Technique::Flag_Disable_SunGlare); + EXPECT_EQ(mTechnique->getHDR(), true); + EXPECT_EQ(mTechnique->getPasses().size(), 1); + EXPECT_EQ(mTechnique->getPasses().front()->getName(), "main"); + } + + TEST_F(TechniqueTest, rendertarget_properties) + { + compile("rendertarget_properties"); + + EXPECT_EQ(mTechnique->getRenderTargetsMap().size(), 1); + + const std::string_view name = mTechnique->getRenderTargetsMap().begin()->first; + auto& rt = mTechnique->getRenderTargetsMap().begin()->second; + auto& texture = rt.mTarget; + + EXPECT_EQ(name, "rendertarget"); + EXPECT_EQ(rt.mMipMap, true); + EXPECT_EQ(rt.mSize.mWidthRatio, 0.5f); + EXPECT_EQ(rt.mSize.mHeightRatio, 0.5f); + EXPECT_EQ(texture->getWrap(osg::Texture::WRAP_S), osg::Texture::CLAMP_TO_EDGE); + EXPECT_EQ(texture->getWrap(osg::Texture::WRAP_T), osg::Texture::REPEAT); + EXPECT_EQ(texture->getFilter(osg::Texture::MIN_FILTER), osg::Texture::LINEAR); + EXPECT_EQ(texture->getFilter(osg::Texture::MAG_FILTER), osg::Texture::NEAREST); + EXPECT_EQ(texture->getSourceType(), static_cast(GL_FLOAT)); + EXPECT_EQ(texture->getSourceFormat(), static_cast(GL_RED)); + EXPECT_EQ(texture->getInternalFormat(), static_cast(GL_R16F)); + + EXPECT_EQ(mTechnique->getPasses().size(), 2); + EXPECT_EQ(mTechnique->getPasses()[0]->getTarget(), "rendertarget"); + } + + TEST_F(TechniqueTest, uniform_properties) + { + compile("uniform_properties"); + + EXPECT_EQ(mTechnique->getUniformMap().size(), 1); + + const auto& uniform = mTechnique->getUniformMap().front(); + + EXPECT_TRUE(uniform->mStatic); + EXPECT_DOUBLE_EQ(uniform->mStep, 0.5); + EXPECT_EQ(uniform->getDefault(), osg::Vec4f(0, 0, 0, 0)); + EXPECT_EQ(uniform->getMin(), osg::Vec4f(0, 1, 0, 0)); + EXPECT_EQ(uniform->getMax(), osg::Vec4f(0, 0, 1, 0)); + EXPECT_EQ(uniform->mHeader, "header"); + EXPECT_EQ(uniform->mDescription, "description"); + EXPECT_EQ(uniform->mName, "uVec4"); + } + + TEST_F(TechniqueTest, fail_with_missing_source_for_sampler) + { + internal::CaptureStdout(); + + compile("missing_sampler_source"); + + std::string output = internal::GetCapturedStdout(); + Log(Debug::Error) << output; + EXPECT_THAT(output, HasSubstr("sampler_1d 'mysampler1d' requires a filename")); + } + + TEST_F(TechniqueTest, fail_with_repeated_shared_block) + { + internal::CaptureStdout(); + + compile("repeated_shared_block"); + + std::string output = internal::GetCapturedStdout(); + Log(Debug::Error) << output; + EXPECT_THAT(output, HasSubstr("repeated 'shared' block")); + } +} diff --git a/apps/components_tests/lua/test_async.cpp b/apps/components_tests/lua/test_async.cpp new file mode 100644 index 00000000000..f1602b8cacd --- /dev/null +++ b/apps/components_tests/lua/test_async.cpp @@ -0,0 +1,58 @@ +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + + struct LuaCoroutineCallbackTest : Test + { + void SetUp() override + { + mLua.protectedCall([&](LuaUtil::LuaView& view) { + view.sol()["callback"] = [](sol::this_state state, sol::protected_function fn) -> LuaUtil::Callback { + sol::table hiddenData(state, sol::create); + hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{}; + return LuaUtil::Callback{ std::move(fn), hiddenData }; + }; + view.sol()["pass"] = [&](LuaUtil::Callback callback) { mCb = callback; }; + }); + } + + LuaUtil::LuaState mLua{ nullptr, nullptr }; + LuaUtil::Callback mCb; + }; + + TEST_F(LuaCoroutineCallbackTest, CoroutineCallbacks) + { + internal::CaptureStdout(); + mLua.protectedCall([&](LuaUtil::LuaView& view) { + view.sol().safe_script(R"X( + local s = 'test' + coroutine.wrap(function() + pass(callback(function(v) print(s) end)) + end)() + )X"); + view.sol().collect_garbage(); + mCb.call(); + }); + EXPECT_THAT(internal::GetCapturedStdout(), "test\n"); + } + + TEST_F(LuaCoroutineCallbackTest, ErrorInCoroutineCallbacks) + { + mLua.protectedCall([&](LuaUtil::LuaView& view) { + view.sol().safe_script(R"X( + coroutine.wrap(function() + pass(callback(function() error('COROUTINE CALLBACK') end)) + end)() + )X"); + view.sol().collect_garbage(); + }); + EXPECT_ERROR(mCb.call(), "COROUTINE CALLBACK"); + } +} diff --git a/apps/components_tests/lua/test_configuration.cpp b/apps/components_tests/lua/test_configuration.cpp new file mode 100644 index 00000000000..2cde0309d66 --- /dev/null +++ b/apps/components_tests/lua/test_configuration.cpp @@ -0,0 +1,259 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + + using testing::ElementsAre; + using testing::Pair; + + std::vector> asVector(const LuaUtil::ScriptIdsWithInitializationData& d) + { + std::vector> res; + for (const auto& [k, v] : d) + res.emplace_back(k, std::string(v)); + return res; + } + + TEST(LuaConfigurationTest, ValidOMWScripts) + { + ESM::LuaScriptsCfg cfg; + LuaUtil::parseOMWScripts(cfg, R"X( + # Lines starting with '#' are comments + GLOBAL: my_mod/#some_global_script.lua + + # Script that will be automatically attached to the player + PLAYER :my_mod/player.lua + CUSTOM : my_mod/some_other_script.lua + NPC , CREATURE PLAYER : my_mod/some_other_script.lua)X"); + LuaUtil::parseOMWScripts(cfg, ":my_mod/player.LUA \r\nCREATURE,CUSTOM: my_mod/creature.lua\r\n"); + + ASSERT_EQ(cfg.mScripts.size(), 6); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[0]), "GLOBAL : my_mod/#some_global_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[1]), "PLAYER : my_mod/player.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[2]), "CUSTOM : my_mod/some_other_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[3]), "PLAYER NPC CREATURE : my_mod/some_other_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[4]), ": my_mod/player.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[5]), "CUSTOM CREATURE : my_mod/creature.lua"); + + LuaUtil::ScriptsConfiguration conf; + conf.init(std::move(cfg)); + ASSERT_EQ(conf.size(), 4); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[0]), "GLOBAL : my_mod/#some_global_script.lua"); + // cfg.mScripts[1] is overridden by cfg.mScripts[4] + // cfg.mScripts[2] is overridden by cfg.mScripts[3] + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "PLAYER NPC CREATURE : my_mod/some_other_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[2]), ": my_mod/player.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[3]), "CUSTOM CREATURE : my_mod/creature.lua"); + + EXPECT_THAT(asVector(conf.getGlobalConf()), ElementsAre(Pair(0, ""))); + EXPECT_THAT(asVector(conf.getPlayerConf()), ElementsAre(Pair(1, ""))); + const ESM::RefId something = ESM::RefId::stringRefId("something"); + + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CONT, something, ESM::RefNum())), ElementsAre()); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_NPC_, something, ESM::RefNum())), ElementsAre(Pair(1, ""))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CREA, something, ESM::RefNum())), + ElementsAre(Pair(1, ""), Pair(3, ""))); + + // Check that initialization cleans old data + cfg = ESM::LuaScriptsCfg(); + conf.init(std::move(cfg)); + EXPECT_EQ(conf.size(), 0); + } + + TEST(LuaConfigurationTest, InvalidOMWScripts) + { + ESM::LuaScriptsCfg cfg; + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL: something"), + "Lua script should have suffix '.lua', got: GLOBAL: something"); + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "something.lua"), "No flags found in: something.lua"); + + cfg.mScripts.clear(); + EXPECT_NO_THROW(LuaUtil::parseOMWScripts(cfg, "GLOBAL, PLAYER: something.lua")); + LuaUtil::ScriptsConfiguration conf; + EXPECT_ERROR(conf.init(std::move(cfg)), "Global script can not have local flags"); + } + + TEST(LuaConfigurationTest, ConfInit) + { + ESM::LuaScriptsCfg cfg; + ESM::LuaScriptCfg& script1 = cfg.mScripts.emplace_back(); + script1.mScriptPath = VFS::Path::Normalized("Script1.lua"); + script1.mInitializationData = "data1"; + script1.mFlags = ESM::LuaScriptCfg::sPlayer; + script1.mTypes.push_back(ESM::REC_CREA); + script1.mRecords.push_back({ true, ESM::RefId::stringRefId("record1"), "dataRecord1" }); + script1.mRefs.push_back({ true, 2, 3, "" }); + script1.mRefs.push_back({ true, 2, 4, "" }); + + ESM::LuaScriptCfg& script2 = cfg.mScripts.emplace_back(); + script2.mScriptPath = VFS::Path::Normalized("Script2.lua"); + script2.mFlags = ESM::LuaScriptCfg::sCustom; + script2.mTypes.push_back(ESM::REC_CONT); + + ESM::LuaScriptCfg& script1Extra = cfg.mScripts.emplace_back(); + script1Extra.mScriptPath = VFS::Path::Normalized("script1.LUA"); + script1Extra.mFlags = ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sMerge; + script1Extra.mTypes.push_back(ESM::REC_NPC_); + script1Extra.mRecords.push_back({ false, ESM::RefId::stringRefId("rat"), "" }); + script1Extra.mRecords.push_back({ true, ESM::RefId::stringRefId("record2"), "" }); + script1Extra.mRefs.push_back({ true, 3, 5, "dataRef35" }); + script1Extra.mRefs.push_back({ false, 2, 3, "" }); + + LuaUtil::ScriptsConfiguration conf; + conf.init(cfg); + ASSERT_EQ(conf.size(), 2); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[0]), + "CUSTOM PLAYER CREATURE NPC : script1.lua ; data 5 bytes ; 3 records ; 4 objects"); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "CUSTOM CONTAINER : script2.lua"); + + EXPECT_THAT(asVector(conf.getPlayerConf()), ElementsAre(Pair(0, "data1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CONT, ESM::RefId::stringRefId("something"), ESM::RefNum())), + ElementsAre(Pair(1, ""))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CREA, ESM::RefId::stringRefId("guar"), ESM::RefNum())), + ElementsAre(Pair(0, "data1"))); + EXPECT_THAT( + asVector(conf.getLocalConf(ESM::REC_CREA, ESM::RefId::stringRefId("rat"), ESM::RefNum())), ElementsAre()); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_DOOR, ESM::RefId::stringRefId("record1"), ESM::RefNum())), + ElementsAre(Pair(0, "dataRecord1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_DOOR, ESM::RefId::stringRefId("record2"), ESM::RefNum())), + ElementsAre(Pair(0, "data1"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_NPC_, ESM::RefId::stringRefId("record3"), { 1, 1 })), + ElementsAre(Pair(0, "data1"))); + EXPECT_THAT( + asVector(conf.getLocalConf(ESM::REC_NPC_, ESM::RefId::stringRefId("record3"), { 2, 3 })), ElementsAre()); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_NPC_, ESM::RefId::stringRefId("record3"), { 3, 5 })), + ElementsAre(Pair(0, "dataRef35"))); + EXPECT_THAT(asVector(conf.getLocalConf(ESM::REC_CONT, ESM::RefId::stringRefId("record4"), { 2, 4 })), + ElementsAre(Pair(0, "data1"), Pair(1, ""))); + + ESM::LuaScriptCfg& script3 = cfg.mScripts.emplace_back(); + script3.mScriptPath = VFS::Path::Normalized("script1.lua"); + script3.mFlags = ESM::LuaScriptCfg::sGlobal; + EXPECT_ERROR(conf.init(cfg), "Flags mismatch for script1.lua"); + } + + TEST(LuaConfigurationTest, Serialization) + { + sol::state lua; + LuaUtil::BasicSerializer serializer; + + ESM::ESMWriter writer; + writer.setAuthor(""); + writer.setDescription(""); + writer.setRecordCount(1); + writer.setFormatVersion(ESM::CurrentContentFormatVersion); + writer.setVersion(); + writer.addMaster("morrowind.esm", 0); + + ESM::LuaScriptsCfg cfg; + std::string luaData; + { + sol::table data(lua, sol::create); + data["number"] = 5; + data["string"] = "some value"; + data["fargoth"] = ESM::RefNum{ 128964, 1 }; + luaData = LuaUtil::serialize(data, &serializer); + } + { + ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back(); + script.mScriptPath = VFS::Path::Normalized("test_global.lua"); + script.mFlags = ESM::LuaScriptCfg::sGlobal; + script.mInitializationData = luaData; + } + { + ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back(); + script.mScriptPath = VFS::Path::Normalized("test_local.lua"); + script.mFlags = ESM::LuaScriptCfg::sMerge; + script.mTypes.push_back(ESM::REC_DOOR); + script.mTypes.push_back(ESM::REC_MISC); + script.mRecords.push_back({ true, ESM::RefId::stringRefId("rat"), luaData }); + script.mRecords.push_back({ false, ESM::RefId::stringRefId("chargendoorjournal"), "" }); + script.mRefs.push_back({ true, 128964, 1, "" }); + script.mRefs.push_back({ true, 128962, 1, luaData }); + } + + std::stringstream stream; + writer.save(stream); + writer.startRecord(ESM::REC_LUAL); + cfg.save(writer); + writer.endRecord(ESM::REC_LUAL); + writer.close(); + std::string serializedOMWAddon = stream.str(); + + { + // Save for manual testing. + std::ofstream f(TestingOpenMW::outputFilePath("lua_conf_test.omwaddon"), std::ios::binary); + f << serializedOMWAddon; + f.close(); + } + + ESM::ESMReader reader; + reader.open(std::make_unique(serializedOMWAddon), "lua_conf_test.omwaddon"); + ASSERT_EQ(reader.getRecordCount(), 1); + ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_LUAL); + reader.getRecHeader(); + ESM::LuaScriptsCfg loadedCfg; + loadedCfg.load(reader); + + ASSERT_EQ(loadedCfg.mScripts.size(), cfg.mScripts.size()); + for (size_t i = 0; i < cfg.mScripts.size(); ++i) + { + EXPECT_EQ(loadedCfg.mScripts[i].mScriptPath, cfg.mScripts[i].mScriptPath); + EXPECT_EQ(loadedCfg.mScripts[i].mFlags, cfg.mScripts[i].mFlags); + EXPECT_EQ(loadedCfg.mScripts[i].mInitializationData, cfg.mScripts[i].mInitializationData); + ASSERT_EQ(loadedCfg.mScripts[i].mTypes.size(), cfg.mScripts[i].mTypes.size()); + for (size_t j = 0; j < cfg.mScripts[i].mTypes.size(); ++j) + EXPECT_EQ(loadedCfg.mScripts[i].mTypes[j], cfg.mScripts[i].mTypes[j]); + ASSERT_EQ(loadedCfg.mScripts[i].mRecords.size(), cfg.mScripts[i].mRecords.size()); + for (size_t j = 0; j < cfg.mScripts[i].mRecords.size(); ++j) + { + EXPECT_EQ(loadedCfg.mScripts[i].mRecords[j].mAttach, cfg.mScripts[i].mRecords[j].mAttach); + EXPECT_EQ(loadedCfg.mScripts[i].mRecords[j].mRecordId, cfg.mScripts[i].mRecords[j].mRecordId); + EXPECT_EQ(loadedCfg.mScripts[i].mRecords[j].mInitializationData, + cfg.mScripts[i].mRecords[j].mInitializationData); + } + ASSERT_EQ(loadedCfg.mScripts[i].mRefs.size(), cfg.mScripts[i].mRefs.size()); + for (size_t j = 0; j < cfg.mScripts[i].mRefs.size(); ++j) + { + EXPECT_EQ(loadedCfg.mScripts[i].mRefs[j].mAttach, cfg.mScripts[i].mRefs[j].mAttach); + EXPECT_EQ(loadedCfg.mScripts[i].mRefs[j].mRefnumIndex, cfg.mScripts[i].mRefs[j].mRefnumIndex); + EXPECT_EQ( + loadedCfg.mScripts[i].mRefs[j].mRefnumContentFile, cfg.mScripts[i].mRefs[j].mRefnumContentFile); + EXPECT_EQ( + loadedCfg.mScripts[i].mRefs[j].mInitializationData, cfg.mScripts[i].mRefs[j].mInitializationData); + } + } + + { + ESM::ReadersCache readers(4); + readers.get(0)->openRaw(std::make_unique("dummyData"), "a.omwaddon"); + readers.get(1)->openRaw(std::make_unique("dummyData"), "b.omwaddon"); + readers.get(2)->openRaw(std::make_unique("dummyData"), "Morrowind.esm"); + readers.get(3)->openRaw(std::make_unique("dummyData"), "c.omwaddon"); + reader.setIndex(3); + reader.resolveParentFileIndices(readers); + } + loadedCfg.adjustRefNums(reader); + EXPECT_EQ(loadedCfg.mScripts[1].mRefs[0].mRefnumIndex, cfg.mScripts[1].mRefs[0].mRefnumIndex); + EXPECT_EQ(loadedCfg.mScripts[1].mRefs[0].mRefnumContentFile, 2); + { + sol::table data = LuaUtil::deserialize( + lua.lua_state(), loadedCfg.mScripts[1].mRefs[1].mInitializationData, &serializer); + ESM::RefNum adjustedRef = data["fargoth"].get(); + EXPECT_EQ(adjustedRef.mIndex, 128964u); + EXPECT_EQ(adjustedRef.mContentFile, 2); + } + } +} diff --git a/apps/components_tests/lua/test_inputactions.cpp b/apps/components_tests/lua/test_inputactions.cpp new file mode 100644 index 00000000000..cad17a5b997 --- /dev/null +++ b/apps/components_tests/lua/test_inputactions.cpp @@ -0,0 +1,64 @@ +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace TestingOpenMW; + + TEST(LuaInputActionsTest, MultiTree) + { + { + LuaUtil::InputAction::MultiTree tree; + auto a = tree.insert(); + auto b = tree.insert(); + auto c = tree.insert(); + auto d = tree.insert(); + EXPECT_TRUE(tree.multiEdge(c, { a, b })); + EXPECT_TRUE(tree.multiEdge(a, { d })); + EXPECT_FALSE(tree.multiEdge(d, { c })); + } + + { + LuaUtil::InputAction::MultiTree tree; + auto a = tree.insert(); + auto b = tree.insert(); + auto c = tree.insert(); + EXPECT_TRUE(tree.multiEdge(b, { a })); + EXPECT_TRUE(tree.multiEdge(c, { a, b })); + } + } + + TEST(LuaInputActionsTest, Registry) + { + sol::state lua; + LuaUtil::InputAction::Registry registry; + LuaUtil::InputAction::Info a({ "a", LuaUtil::InputAction::Type::Boolean, "test", "a_name", "a_description", + sol::make_object(lua, false) }); + registry.insert(a); + LuaUtil::InputAction::Info b({ "b", LuaUtil::InputAction::Type::Boolean, "test", "b_name", "b_description", + sol::make_object(lua, false) }); + registry.insert(b); + LuaUtil::Callback bindA({ lua.load("return function() return true end")(), sol::table(lua, sol::create) }); + LuaUtil::Callback bindBToA( + { lua.load("return function(_, _, aValue) return aValue end")(), sol::table(lua, sol::create) }); + EXPECT_TRUE(registry.bind("a", bindA, {})); + EXPECT_TRUE(registry.bind("b", bindBToA, { "a" })); + registry.update(1.0); + sol::object bValue = registry.valueOfType("b", LuaUtil::InputAction::Type::Boolean); + EXPECT_TRUE(bValue.is()); + LuaUtil::Callback badA( + { lua.load("return function() return 'not_a_bool' end")(), sol::table(lua, sol::create) }); + EXPECT_TRUE(registry.bind("a", badA, {})); + testing::internal::CaptureStderr(); + registry.update(1.0); + sol::object aValue = registry.valueOfType("a", LuaUtil::InputAction::Type::Boolean); + EXPECT_TRUE(aValue.is()); + bValue = registry.valueOfType("b", LuaUtil::InputAction::Type::Boolean); + EXPECT_TRUE(bValue.is() && bValue.as() == aValue.as()); + } +} diff --git a/apps/components_tests/lua/test_l10n.cpp b/apps/components_tests/lua/test_l10n.cpp new file mode 100644 index 00000000000..b48028a7306 --- /dev/null +++ b/apps/components_tests/lua/test_l10n.cpp @@ -0,0 +1,174 @@ +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace TestingOpenMW; + + template + T get(sol::state_view& lua, const std::string& luaCode) + { + return lua.safe_script("return " + luaCode).get(); + } + + constexpr VFS::Path::NormalizedView test1EnPath("l10n/test1/en.yaml"); + constexpr VFS::Path::NormalizedView test1EnUsPath("l10n/test1/en_us.yaml"); + constexpr VFS::Path::NormalizedView test1DePath("l10n/test1/de.yaml"); + 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"); + + VFSTestFile invalidScript("not a script"); + VFSTestFile incorrectScript( + "return { incorrectSection = {}, engineHandlers = { incorrectHandler = function() end } }"); + VFSTestFile emptyScript(""); + + VFSTestFile test1En(R"X( +good_morning: "Good morning." +you_have_arrows: |- + {count, plural, + =0{You have no arrows.} + one{You have one arrow.} + other{You have {count} arrows.} + } +pc_must_come: |- + {PCGender, select, + male {He is} + female {She is} + other {They are} + } coming with us. +quest_completion: "The quest is {done, number, percent} complete." +ordinal: "You came in {num, ordinal} place." +spellout: "There {num, plural, one{is {num, spellout} thing} other{are {num, spellout} things}}." +duration: "It took {num, duration}" +numbers: "{int} and {double, number, integer} are integers, but {double} is a double" +rounding: "{value, number, :: .00}" +)X"); + + VFSTestFile test1De(R"X( +good_morning: "Guten Morgen." +you_have_arrows: |- + {count, plural, + one{Du hast ein Pfeil.} + other{Du hast {count} Pfeile.} + } +"Hello {name}!": "Hallo {name}!" +)X"); + + VFSTestFile test1EnUS(R"X( +currency: "You have {money, number, currency}" +)X"); + + VFSTestFile test2En(R"X( +good_morning: "Morning!" +you_have_arrows: "Arrows count: {count}" +)X"); + + struct LuaL10nTest : Test + { + std::unique_ptr mVFS = createTestVFS({ + { test1EnPath, &test1En }, + { test1EnUsPath, &test1EnUS }, + { test1DePath, &test1De }, + { test2EnPath, &test2En }, + { test3EnPath, &test1En }, + { test3DePath, &test1De }, + }); + + LuaUtil::ScriptsConfiguration mCfg; + }; + + TEST_F(LuaL10nTest, L10n) + { + LuaUtil::LuaState lua{ mVFS.get(), &mCfg }; + lua.protectedCall([&](LuaUtil::LuaView& view) { + sol::state_view& l = view.sol(); + internal::CaptureStdout(); + l10n::Manager l10nManager(mVFS.get()); + l10nManager.setPreferredLocales({ "de", "en" }); + EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: gmst de en\n"); + + l["l10n"] = LuaUtil::initL10nLoader(l, &l10nManager); + + internal::CaptureStdout(); + l.safe_script("t1 = l10n('Test1')"); + EXPECT_THAT(internal::GetCapturedStdout(), + "Language file \"l10n/Test1/de.yaml\" is enabled\n" + "Language file \"l10n/Test1/en.yaml\" is enabled\n"); + + internal::CaptureStdout(); + l.safe_script("t2 = l10n('Test2')"); + { + std::string output = internal::GetCapturedStdout(); + EXPECT_THAT(output, HasSubstr("Language file \"l10n/Test2/en.yaml\" is enabled")); + } + + EXPECT_EQ(get(l, "t1('good_morning')"), "Guten Morgen."); + EXPECT_EQ(get(l, "t1('you_have_arrows', {count=1})"), "Du hast ein Pfeil."); + EXPECT_EQ(get(l, "t1('you_have_arrows', {count=5})"), "Du hast 5 Pfeile."); + EXPECT_EQ(get(l, "t1('Hello {name}!', {name='World'})"), "Hallo World!"); + EXPECT_EQ(get(l, "t2('good_morning')"), "Morning!"); + EXPECT_EQ(get(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3"); + + internal::CaptureStdout(); + l10nManager.setPreferredLocales({ "en", "de" }); + EXPECT_THAT(internal::GetCapturedStdout(), + "Preferred locales: gmst en de\n" + "Language file \"l10n/Test1/en.yaml\" is enabled\n" + "Language file \"l10n/Test1/de.yaml\" is enabled\n" + "Language file \"l10n/Test2/en.yaml\" is enabled\n"); + + EXPECT_EQ(get(l, "t1('good_morning')"), "Good morning."); + EXPECT_EQ(get(l, "t1('you_have_arrows', {count=1})"), "You have one arrow."); + EXPECT_EQ(get(l, "t1('you_have_arrows', {count=5})"), "You have 5 arrows."); + EXPECT_EQ(get(l, "t1('pc_must_come', {PCGender=\"male\"})"), "He is coming with us."); + EXPECT_EQ(get(l, "t1('pc_must_come', {PCGender=\"female\"})"), "She is coming with us."); + EXPECT_EQ(get(l, "t1('pc_must_come', {PCGender=\"blah\"})"), "They are coming with us."); + EXPECT_EQ(get(l, "t1('pc_must_come', {PCGender=\"other\"})"), "They are coming with us."); + EXPECT_EQ(get(l, "t1('quest_completion', {done=0.1})"), "The quest is 10% complete."); + EXPECT_EQ(get(l, "t1('quest_completion', {done=1})"), "The quest is 100% complete."); + EXPECT_EQ(get(l, "t1('ordinal', {num=1})"), "You came in 1st place."); + EXPECT_EQ(get(l, "t1('ordinal', {num=100})"), "You came in 100th place."); + EXPECT_EQ(get(l, "t1('spellout', {num=1})"), "There is one thing."); + EXPECT_EQ(get(l, "t1('spellout', {num=100})"), "There are one hundred things."); + EXPECT_EQ(get(l, "t1('duration', {num=100})"), "It took 1:40"); + EXPECT_EQ(get(l, "t1('numbers', {int=123, double=123.456})"), + "123 and 123 are integers, but 123.456 is a double"); + EXPECT_EQ(get(l, "t1('rounding', {value=123.456789})"), "123.46"); + // Check that failed messages display the key instead of an empty string + EXPECT_EQ(get(l, "t1('{mismatched_braces')"), "{mismatched_braces"); + EXPECT_EQ(get(l, "t1('{unknown_arg}')"), "{unknown_arg}"); + EXPECT_EQ(get(l, "t1('{num, integer}', {num=1})"), "{num, integer}"); + // Doesn't give a valid currency symbol with `en`. Not that openmw is designed for real world currency. + l10nManager.setPreferredLocales({ "en-US", "de" }); + EXPECT_EQ(get(l, "t1('currency', {money=10000.10})"), "You have $10,000.10"); + // Note: Not defined in English localisation file, so we fall back to the German before falling back to the + // key + EXPECT_EQ(get(l, "t1('Hello {name}!', {name='World'})"), "Hallo World!"); + EXPECT_EQ(get(l, "t2('good_morning')"), "Morning!"); + EXPECT_EQ(get(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3"); + + // Test that locales with variants and country codes fall back to more generic locales + internal::CaptureStdout(); + l10nManager.setPreferredLocales({ "en-GB-oed", "de" }); + EXPECT_THAT(internal::GetCapturedStdout(), + "Preferred locales: gmst en_GB_OED de\n" + "Language file \"l10n/Test1/en.yaml\" is enabled\n" + "Language file \"l10n/Test1/de.yaml\" is enabled\n" + "Language file \"l10n/Test2/en.yaml\" is enabled\n"); + EXPECT_EQ(get(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3"); + + // Test setting fallback language + l.safe_script("t3 = l10n('Test3', 'de')"); + l10nManager.setPreferredLocales({ "en" }); + EXPECT_EQ(get(l, "t3('Hello {name}!', {name='World'})"), "Hallo World!"); + }); + } +} diff --git a/apps/components_tests/lua/test_lua.cpp b/apps/components_tests/lua/test_lua.cpp new file mode 100644 index 00000000000..5f1e11c435e --- /dev/null +++ b/apps/components_tests/lua/test_lua.cpp @@ -0,0 +1,265 @@ +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + + constexpr VFS::Path::NormalizedView counterPath("aaa/counter.lua"); + + TestingOpenMW::VFSTestFile counterFile(R"X( +x = 42 +return { + get = function() return x end, + inc = function(v) x = x + v end +} +)X"); + + constexpr VFS::Path::NormalizedView invalidPath("invalid.lua"); + + TestingOpenMW::VFSTestFile invalidScriptFile("Invalid script"); + + constexpr VFS::Path::NormalizedView testsPath("bbb/tests.lua"); + + TestingOpenMW::VFSTestFile testsFile(R"X( +return { + -- should work + sin = function(x) return math.sin(x) end, + requireMathSin = function(x) return require('math').sin(x) end, + useCounter = function() + local counter = require('aaa.counter') + counter.inc(1) + return counter.get() + end, + callRawset = function() + t = {a = 1, b = 2} + rawset(t, 'b', 3) + return t.b + end, + print = print, + + -- should throw an error + incorrectRequire = function() require('counter') end, + modifySystemLib = function() math.sin = 5 end, + modifySystemLib2 = function() math.__index.sin = 5 end, + rawsetSystemLib = function() rawset(math, 'sin', 5) end, + callLoadstring = function() loadstring('print(1)') end, + setSqr = function() require('sqrlib').sqr = math.sin end, + setOmwName = function() require('openmw').name = 'abc' end, + + -- should work if API is registered + sqr = function(x) return require('sqrlib').sqr(x) end, + apiName = function() return require('test.api').name end +} +)X"); + + constexpr VFS::Path::NormalizedView metaIndexErrorPath("metaindexerror.lua"); + + TestingOpenMW::VFSTestFile metaIndexErrorFile( + "return setmetatable({}, { __index = function(t, key) error('meta index error') end })"); + + std::string genBigScript() + { + std::stringstream buf; + buf << "return function()\n"; + buf << " x = {}\n"; + for (int i = 0; i < 1000; ++i) + buf << " x[" << i * 2 << "] = " << i << "\n"; + buf << " return x\n"; + buf << "end\n"; + return buf.str(); + } + + constexpr VFS::Path::NormalizedView bigPath("big.lua"); + + TestingOpenMW::VFSTestFile bigScriptFile(genBigScript()); + + constexpr VFS::Path::NormalizedView requireBigPath("requirebig.lua"); + + TestingOpenMW::VFSTestFile requireBigScriptFile("local x = require('big') ; return {x}"); + + struct LuaStateTest : Test + { + std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ + { counterPath, &counterFile }, + { testsPath, &testsFile }, + { invalidPath, &invalidScriptFile }, + { bigPath, &bigScriptFile }, + { requireBigPath, &requireBigScriptFile }, + { metaIndexErrorPath, &metaIndexErrorFile }, + }); + + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{ mVFS.get(), &mCfg }; + }; + + TEST_F(LuaStateTest, Sandbox) + { + const VFS::Path::Normalized path(counterPath); + sol::table script1 = mLua.runInNewSandbox(path); + + EXPECT_EQ(LuaUtil::call(script1["get"]).get(), 42); + LuaUtil::call(script1["inc"], 3); + EXPECT_EQ(LuaUtil::call(script1["get"]).get(), 45); + + sol::table script2 = mLua.runInNewSandbox(path); + EXPECT_EQ(LuaUtil::call(script2["get"]).get(), 42); + LuaUtil::call(script2["inc"], 1); + EXPECT_EQ(LuaUtil::call(script2["get"]).get(), 43); + + EXPECT_EQ(LuaUtil::call(script1["get"]).get(), 45); + } + + TEST_F(LuaStateTest, ToString) + { + EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.unsafeState(), 3.14)), "3.14"); + EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.unsafeState(), true)), "true"); + EXPECT_EQ(LuaUtil::toString(sol::nil), "nil"); + EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.unsafeState(), "something")), "\"something\""); + } + + TEST_F(LuaStateTest, Cast) + { + EXPECT_EQ(LuaUtil::cast(sol::make_object(mLua.unsafeState(), 3.14)), 3); + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), "3.14")), + "Value \"\"3.14\"\" can not be casted to int"); + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), sol::nil)), + "Value \"nil\" can not be casted to string"); + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), sol::nil)), + "Value \"nil\" can not be casted to string"); + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), sol::nil)), + "Value \"nil\" can not be casted to sol::table"); + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), "3.14")), + "Value \"\"3.14\"\" can not be casted to sol::function"); + EXPECT_ERROR(LuaUtil::cast(sol::make_object(mLua.unsafeState(), "3.14")), + "Value \"\"3.14\"\" can not be casted to sol::function"); + } + + TEST_F(LuaStateTest, ErrorHandling) + { + const VFS::Path::Normalized path("invalid.lua"); + EXPECT_ERROR(mLua.runInNewSandbox(path), "[string \"invalid.lua\"]:1:"); + } + + TEST_F(LuaStateTest, CustomRequire) + { + const VFS::Path::Normalized path("bbb/tests.lua"); + sol::table script = mLua.runInNewSandbox(path); + + EXPECT_FLOAT_EQ( + LuaUtil::call(script["sin"], 1).get(), -LuaUtil::call(script["requireMathSin"], -1).get()); + + EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 43); + EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 44); + { + sol::table script2 = mLua.runInNewSandbox(path); + EXPECT_EQ(LuaUtil::call(script2["useCounter"]).get(), 43); + } + EXPECT_EQ(LuaUtil::call(script["useCounter"]).get(), 45); + + EXPECT_ERROR(LuaUtil::call(script["incorrectRequire"]), "module not found: counter"); + } + + TEST_F(LuaStateTest, ReadOnly) + { + const VFS::Path::Normalized path("bbb/tests.lua"); + sol::table script = mLua.runInNewSandbox(path); + + // rawset itself is allowed + EXPECT_EQ(LuaUtil::call(script["callRawset"]).get(), 3); + + // but read-only object can not be modified even with rawset + EXPECT_ERROR( + LuaUtil::call(script["rawsetSystemLib"]), "bad argument #1 to 'rawset' (table expected, got userdata)"); + EXPECT_ERROR(LuaUtil::call(script["modifySystemLib"]), "a userdata value"); + EXPECT_ERROR(LuaUtil::call(script["modifySystemLib2"]), "a nil value"); + + EXPECT_EQ(LuaUtil::getMutableFromReadOnly(LuaUtil::makeReadOnly(script)), script); + } + + TEST_F(LuaStateTest, Print) + { + const VFS::Path::Normalized path("bbb/tests.lua"); + { + sol::table script = mLua.runInNewSandbox(path); + testing::internal::CaptureStdout(); + LuaUtil::call(script["print"], 1, 2, 3); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(output, "unnamed:\t1\t2\t3\n"); + } + { + sol::table script = mLua.runInNewSandbox(path, "prefix"); + testing::internal::CaptureStdout(); + LuaUtil::call(script["print"]); // print with no arguments + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(output, "prefix:\n"); + } + } + + TEST_F(LuaStateTest, UnsafeFunction) + { + const VFS::Path::Normalized path("bbb/tests.lua"); + sol::table script = mLua.runInNewSandbox(path); + EXPECT_ERROR(LuaUtil::call(script["callLoadstring"]), "a nil value"); + } + + TEST_F(LuaStateTest, ProvideAPI) + { + LuaUtil::LuaState lua(mVFS.get(), &mCfg); + lua.protectedCall([&](LuaUtil::LuaView& view) { + sol::table api1 = LuaUtil::makeReadOnly(view.sol().create_table_with("name", "api1")); + sol::table api2 = LuaUtil::makeReadOnly(view.sol().create_table_with("name", "api2")); + + const VFS::Path::Normalized path("bbb/tests.lua"); + + sol::table script1 = lua.runInNewSandbox(path, "", { { "test.api", api1 } }); + + lua.addCommonPackage("sqrlib", view.sol().create_table_with("sqr", [](int x) { return x * x; })); + + sol::table script2 = lua.runInNewSandbox(path, "", { { "test.api", api2 } }); + + EXPECT_ERROR(LuaUtil::call(script1["sqr"], 3), "module not found: sqrlib"); + EXPECT_EQ(LuaUtil::call(script2["sqr"], 3).get(), 9); + + EXPECT_EQ(LuaUtil::call(script1["apiName"]).get(), "api1"); + EXPECT_EQ(LuaUtil::call(script2["apiName"]).get(), "api2"); + }); + } + + TEST_F(LuaStateTest, GetLuaVersion) + { + EXPECT_THAT(LuaUtil::getLuaVersion(), HasSubstr("Lua")); + } + + TEST_F(LuaStateTest, RemovedScriptsGarbageCollecting) + { + auto getMem = [&] { + for (int i = 0; i < 5; ++i) + lua_gc(mLua.unsafeState(), LUA_GCCOLLECT, 0); + return mLua.getTotalMemoryUsage(); + }; + int64_t memWithScript; + const VFS::Path::Normalized path("requireBig.lua"); + { + sol::object s = mLua.runInNewSandbox(path); + memWithScript = getMem(); + } + for (int i = 0; i < 100; ++i) // run many times to make small memory leaks visible + mLua.runInNewSandbox(path); + int64_t memWithoutScript = getMem(); + // At this moment all instances of the script should be garbage-collected. + EXPECT_LT(memWithoutScript, memWithScript); + } + + TEST_F(LuaStateTest, SafeIndexMetamethod) + { + const VFS::Path::Normalized path("metaIndexError.lua"); + sol::table t = mLua.runInNewSandbox(path); + // without safe get we crash here + EXPECT_ERROR(LuaUtil::safeGet(t, "any key"), "meta index error"); + } +} diff --git a/apps/components_tests/lua/test_scriptscontainer.cpp b/apps/components_tests/lua/test_scriptscontainer.cpp new file mode 100644 index 00000000000..b4f08e9ab63 --- /dev/null +++ b/apps/components_tests/lua/test_scriptscontainer.cpp @@ -0,0 +1,514 @@ +#include +#include + +#include + +#include +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace TestingOpenMW; + + constexpr VFS::Path::NormalizedView invalidPath("invalid.lua"); + + VFSTestFile invalidScript("not a script"); + + constexpr VFS::Path::NormalizedView incorrectPath("incorrect.lua"); + + VFSTestFile incorrectScript( + "return { incorrectSection = {}, engineHandlers = { incorrectHandler = function() end } }"); + + constexpr VFS::Path::NormalizedView emptyPath("empty.lua"); + + VFSTestFile emptyScript(""); + + constexpr VFS::Path::NormalizedView test1Path("test1.lua"); + constexpr VFS::Path::NormalizedView test2Path("test2.lua"); + + VFSTestFile testScript(R"X( +return { + engineHandlers = { + onUpdate = function(dt) print(' update ' .. tostring(dt)) end, + onLoad = function() print('load') end, + }, + eventHandlers = { + Event1 = function(eventData) print(' event1 ' .. tostring(eventData.x)) end, + Event2 = function(eventData) print(' event2 ' .. tostring(eventData.x)) end, + Print = function() print('print') end + } +} +)X"); + + constexpr VFS::Path::NormalizedView stopEventPath("stopevent.lua"); + + VFSTestFile stopEventScript(R"X( +return { + eventHandlers = { + Event1 = function(eventData) + print(' event1 ' .. tostring(eventData.x)) + return eventData.x >= 1 + end + } +} +)X"); + + constexpr VFS::Path::NormalizedView loadSave1Path("loadsave1.lua"); + constexpr VFS::Path::NormalizedView loadSave2Path("loadsave2.lua"); + + VFSTestFile loadSaveScript(R"X( +x = 0 +y = 0 +return { + engineHandlers = { + onSave = function(state) + return {x = x, y = y} + end, + onLoad = function(state) + x, y = state.x, state.y + end + }, + eventHandlers = { + Set = function(eventData) + eventData.n = eventData.n - 1 + if eventData.n == 0 then + x, y = eventData.x, eventData.y + end + end, + Print = function() + print(x, y) + end + } +} +)X"); + + constexpr VFS::Path::NormalizedView testInterfacePath("testinterface.lua"); + + VFSTestFile interfaceScript(R"X( +return { + interfaceName = "TestInterface", + interface = { + fn = function(x) print('FN', x) end, + value = 3.5 + }, +} +)X"); + + constexpr VFS::Path::NormalizedView overrideInterfacePath("overrideinterface.lua"); + + VFSTestFile overrideInterfaceScript(R"X( +local old = nil +local interface = { + fn = function(x) + print('NEW FN', x) + old.fn(x) + end, + value, +} +return { + interfaceName = "TestInterface", + interface = interface, + engineHandlers = { + onInit = function() print('init') end, + onLoad = function() print('load') end, + onInterfaceOverride = function(oldInterface) + print('override') + old = oldInterface + interface.value = oldInterface.value + 1 + end + }, +} +)X"); + + constexpr VFS::Path::NormalizedView useInterfacePath("useinterface.lua"); + + VFSTestFile useInterfaceScript(R"X( +local interfaces = require('openmw.interfaces') +return { + engineHandlers = { + onUpdate = function() + interfaces.TestInterface.fn(interfaces.TestInterface.value) + end, + }, +} +)X"); + + struct LuaScriptsContainerTest : Test + { + std::unique_ptr mVFS = createTestVFS({ + { invalidPath, &invalidScript }, + { incorrectPath, &incorrectScript }, + { emptyPath, &emptyScript }, + { test1Path, &testScript }, + { test2Path, &testScript }, + { stopEventPath, &stopEventScript }, + { loadSave1Path, &loadSaveScript }, + { loadSave2Path, &loadSaveScript }, + { testInterfacePath, &interfaceScript }, + { overrideInterfacePath, &overrideInterfaceScript }, + { useInterfacePath, &useInterfaceScript }, + }); + + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{ mVFS.get(), &mCfg }; + + LuaScriptsContainerTest() + { + ESM::LuaScriptsCfg cfg; + LuaUtil::parseOMWScripts(cfg, R"X( +CUSTOM: invalid.lua +CUSTOM: incorrect.lua +CUSTOM: empty.lua +CUSTOM: test1.lua +CUSTOM: stopEvent.lua +CUSTOM: test2.lua +NPC: loadSave1.lua +CUSTOM, NPC: loadSave2.lua +CUSTOM, PLAYER: testInterface.lua +CUSTOM, PLAYER: overrideInterface.lua +CUSTOM, PLAYER: useInterface.lua +)X"); + mCfg.init(std::move(cfg)); + } + + int getId(VFS::Path::NormalizedView path) const + { + const std::optional id = mCfg.findId(path); + if (!id.has_value()) + throw std::invalid_argument("Script id is not found: " + std::string(path.value())); + return *id; + } + }; + + TEST_F(LuaScriptsContainerTest, addCustomScriptShouldNotStartInvalidScript) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + testing::internal::CaptureStdout(); + EXPECT_FALSE(scripts.addCustomScript(getId(invalidPath))); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, HasSubstr("Can't start Test[invalid.lua]")); + } + + TEST_F(LuaScriptsContainerTest, addCustomScriptShouldNotSuportScriptsWithInvalidHandlerAndSection) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(getId(incorrectPath))); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, HasSubstr("Not supported handler 'incorrectHandler' in Test[incorrect.lua]")); + EXPECT_THAT(output, HasSubstr("Not supported section 'incorrectSection' in Test[incorrect.lua]")); + } + + TEST_F(LuaScriptsContainerTest, addCustomScriptShouldReturnFalseForDuplicates) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + EXPECT_TRUE(scripts.addCustomScript(getId(emptyPath))); + EXPECT_FALSE(scripts.addCustomScript(getId(emptyPath))); + } + + TEST_F(LuaScriptsContainerTest, CallHandler) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(getId(test1Path))); + EXPECT_TRUE(scripts.addCustomScript(getId(stopEventPath))); + EXPECT_TRUE(scripts.addCustomScript(getId(test2Path))); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\t update 1.5\n" + "Test[test2.lua]:\t update 1.5\n"); + } + + TEST_F(LuaScriptsContainerTest, CallEvent) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + + EXPECT_TRUE(scripts.addCustomScript(getId(test1Path))); + EXPECT_TRUE(scripts.addCustomScript(getId(stopEventPath))); + EXPECT_TRUE(scripts.addCustomScript(getId(test2Path))); + + sol::state_view sol = mLua.unsafeState(); + std::string X0 = LuaUtil::serialize(sol.create_table_with("x", 0.5)); + std::string X1 = LuaUtil::serialize(sol.create_table_with("x", 1.5)); + + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("SomeEvent", X1); + EXPECT_EQ(internal::GetCapturedStdout(), ""); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event1", X1); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event1 1.5\n" + "Test[stopevent.lua]:\t event1 1.5\n" + "Test[test1.lua]:\t event1 1.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event2", X1); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event2 1.5\n" + "Test[test1.lua]:\t event2 1.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event1", X0); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event1 0.5\n" + "Test[stopevent.lua]:\t event1 0.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.receiveEvent("Event2", X0); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t event2 0.5\n" + "Test[test1.lua]:\t event2 0.5\n"); + } + } + + TEST_F(LuaScriptsContainerTest, RemoveScript) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + + EXPECT_TRUE(scripts.addCustomScript(getId(test1Path))); + EXPECT_TRUE(scripts.addCustomScript(getId(stopEventPath))); + EXPECT_TRUE(scripts.addCustomScript(getId(test2Path))); + + sol::state_view sol = mLua.unsafeState(); + std::string X = LuaUtil::serialize(sol.create_table_with("x", 0.5)); + + { + testing::internal::CaptureStdout(); + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\t update 1.5\n" + "Test[test2.lua]:\t update 1.5\n" + "Test[test2.lua]:\t event1 0.5\n" + "Test[stopevent.lua]:\t event1 0.5\n"); + } + { + testing::internal::CaptureStdout(); + const int stopEventScriptId = getId(stopEventPath); + EXPECT_TRUE(scripts.hasScript(stopEventScriptId)); + scripts.removeScript(stopEventScriptId); + EXPECT_FALSE(scripts.hasScript(stopEventScriptId)); + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\t update 1.5\n" + "Test[test2.lua]:\t update 1.5\n" + "Test[test2.lua]:\t event1 0.5\n" + "Test[test1.lua]:\t event1 0.5\n"); + } + { + testing::internal::CaptureStdout(); + scripts.removeScript(getId(test1Path)); + scripts.update(1.5f); + scripts.receiveEvent("Event1", X); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test2.lua]:\t update 1.5\n" + "Test[test2.lua]:\t event1 0.5\n"); + } + } + + TEST_F(LuaScriptsContainerTest, AutoStart) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + scripts.setAutoStartConf(mCfg.getPlayerConf()); + testing::internal::CaptureStdout(); + scripts.addAutoStartedScripts(); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideinterface.lua]:\toverride\n" + "Test[overrideinterface.lua]:\tinit\n" + "Test[overrideinterface.lua]:\tNEW FN\t4.5\n" + "Test[testinterface.lua]:\tFN\t4.5\n"); + } + + TEST_F(LuaScriptsContainerTest, Interface) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + scripts.setAutoStartConf(mCfg.getLocalConf(ESM::REC_CREA, ESM::RefId(), ESM::RefNum())); + const int addIfaceId = getId(testInterfacePath); + const int overrideIfaceId = getId(overrideInterfacePath); + const int useIfaceId = getId(useInterfacePath); + + testing::internal::CaptureStdout(); + scripts.addAutoStartedScripts(); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), ""); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(addIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(overrideIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(useIfaceId)); + scripts.update(1.5f); + scripts.removeScript(overrideIfaceId); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideinterface.lua]:\toverride\n" + "Test[overrideinterface.lua]:\tinit\n" + "Test[overrideinterface.lua]:\tNEW FN\t4.5\n" + "Test[testinterface.lua]:\tFN\t4.5\n" + "Test[testinterface.lua]:\tFN\t3.5\n"); + } + + TEST_F(LuaScriptsContainerTest, LoadSave) + { + LuaUtil::ScriptsContainer scripts1(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts2(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts3(&mLua, "Test"); + scripts1.setAutoStartConf(mCfg.getLocalConf(ESM::REC_NPC_, ESM::RefId(), ESM::RefNum())); + scripts2.setAutoStartConf(mCfg.getLocalConf(ESM::REC_NPC_, ESM::RefId(), ESM::RefNum())); + scripts3.setAutoStartConf(mCfg.getPlayerConf()); + + scripts1.addAutoStartedScripts(); + EXPECT_TRUE(scripts1.addCustomScript(getId(test1Path))); + + sol::state_view sol = mLua.unsafeState(); + scripts1.receiveEvent("Set", LuaUtil::serialize(sol.create_table_with("n", 1, "x", 0.5, "y", 3.5))); + scripts1.receiveEvent("Set", LuaUtil::serialize(sol.create_table_with("n", 2, "x", 2.5, "y", 1.5))); + + ESM::LuaScripts data; + scripts1.save(data); + + { + testing::internal::CaptureStdout(); + scripts2.load(data); + scripts2.receiveEvent("Print", ""); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\tload\n" + "Test[loadsave2.lua]:\t0.5\t3.5\n" + "Test[loadsave1.lua]:\t2.5\t1.5\n" + "Test[test1.lua]:\tprint\n"); + EXPECT_FALSE(scripts2.hasScript(getId(testInterfacePath))); + } + { + testing::internal::CaptureStdout(); + scripts3.load(data); + scripts3.receiveEvent("Print", ""); + EXPECT_EQ(internal::GetCapturedStdout(), + "Ignoring Test[loadsave1.lua]; this script is not allowed here\n" + "Test[test1.lua]:\tload\n" + "Test[overrideinterface.lua]:\toverride\n" + "Test[overrideinterface.lua]:\tinit\n" + "Test[loadsave2.lua]:\t0.5\t3.5\n" + "Test[test1.lua]:\tprint\n"); + EXPECT_TRUE(scripts3.hasScript(getId(testInterfacePath))); + } + } + + TEST_F(LuaScriptsContainerTest, Timers) + { + using TimerType = LuaUtil::ScriptsContainer::TimerType; + LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + const int test1Id = getId(test1Path); + const int test2Id = getId(test2Path); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(test1Id)); + EXPECT_TRUE(scripts.addCustomScript(test2Id)); + EXPECT_EQ(internal::GetCapturedStdout(), ""); + + int counter1 = 0, counter2 = 0, counter3 = 0, counter4 = 0; + sol::function fn1 = sol::make_object(mLua.unsafeState(), [&]() { counter1++; }); + sol::function fn2 = sol::make_object(mLua.unsafeState(), [&]() { counter2++; }); + sol::function fn3 = sol::make_object(mLua.unsafeState(), [&](int d) { counter3 += d; }); + sol::function fn4 = sol::make_object(mLua.unsafeState(), [&](int d) { counter4 += d; }); + + scripts.registerTimerCallback(test1Id, "A", fn3); + scripts.registerTimerCallback(test1Id, "B", fn4); + scripts.registerTimerCallback(test2Id, "B", fn3); + scripts.registerTimerCallback(test2Id, "A", fn4); + + scripts.processTimers(1, 2); + + scripts.setupSerializableTimer( + TimerType::SIMULATION_TIME, 10, test1Id, "B", sol::make_object(mLua.unsafeState(), 3)); + scripts.setupSerializableTimer(TimerType::GAME_TIME, 10, test2Id, "B", sol::make_object(mLua.unsafeState(), 4)); + scripts.setupSerializableTimer( + TimerType::SIMULATION_TIME, 5, test1Id, "A", sol::make_object(mLua.unsafeState(), 1)); + scripts.setupSerializableTimer(TimerType::GAME_TIME, 5, test2Id, "A", sol::make_object(mLua.unsafeState(), 2)); + scripts.setupSerializableTimer( + TimerType::SIMULATION_TIME, 15, test1Id, "A", sol::make_object(mLua.unsafeState(), 10)); + scripts.setupSerializableTimer( + TimerType::SIMULATION_TIME, 15, test1Id, "B", sol::make_object(mLua.unsafeState(), 20)); + + scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 10, test2Id, fn2); + scripts.setupUnsavableTimer(TimerType::GAME_TIME, 10, test1Id, fn2); + scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 5, test2Id, fn1); + scripts.setupUnsavableTimer(TimerType::GAME_TIME, 5, test1Id, fn1); + scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 15, test2Id, fn1); + + EXPECT_EQ(counter1, 0); + EXPECT_EQ(counter3, 0); + + scripts.processTimers(6, 4); + + EXPECT_EQ(counter1, 1); + EXPECT_EQ(counter3, 1); + EXPECT_EQ(counter4, 0); + + scripts.processTimers(6, 8); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 0); + EXPECT_EQ(counter3, 1); + EXPECT_EQ(counter4, 2); + + scripts.processTimers(11, 12); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 2); + EXPECT_EQ(counter3, 5); + EXPECT_EQ(counter4, 5); + + testing::internal::CaptureStdout(); + ESM::LuaScripts data; + scripts.save(data); + scripts.load(data); + scripts.registerTimerCallback(test1Id, "B", fn4); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\tload\nTest[test2.lua]:\tload\n"); + + testing::internal::CaptureStdout(); + scripts.processTimers(20, 20); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua] callTimer failed: Callback 'A' doesn't exist\n"); + + EXPECT_EQ(counter1, 2); + EXPECT_EQ(counter2, 2); + EXPECT_EQ(counter3, 5); + EXPECT_EQ(counter4, 25); + } + + TEST_F(LuaScriptsContainerTest, CallbackWrapper) + { + sol::state_view view = mLua.unsafeState(); + LuaUtil::Callback callback{ view["print"], sol::table(view, sol::create) }; + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptDebugNameKey] = "some_script.lua"; + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{ nullptr, 0 }; + + testing::internal::CaptureStdout(); + callback.call(1.5); + EXPECT_EQ(internal::GetCapturedStdout(), "1.5\n"); + + testing::internal::CaptureStdout(); + callback.call(1.5, 2.5); + EXPECT_EQ(internal::GetCapturedStdout(), "1.5\t2.5\n"); + + const Debug::Level level = std::exchange(Log::sMinDebugLevel, Debug::All); + + testing::internal::CaptureStdout(); + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = sol::nil; + callback.call(1.5, 2.5); + EXPECT_EQ(internal::GetCapturedStdout(), "Ignored callback to the removed script some_script.lua\n"); + + Log::sMinDebugLevel = level; + } + +} diff --git a/apps/components_tests/lua/test_serialization.cpp b/apps/components_tests/lua/test_serialization.cpp new file mode 100644 index 00000000000..cff41dde9ab --- /dev/null +++ b/apps/components_tests/lua/test_serialization.cpp @@ -0,0 +1,279 @@ +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + + TEST(LuaSerializationTest, Nil) + { + sol::state lua; + EXPECT_EQ(LuaUtil::serialize(sol::nil), ""); + EXPECT_EQ(LuaUtil::deserialize(lua, ""), sol::nil); + } + + TEST(LuaSerializationTest, Number) + { + sol::state lua; + std::string serialized = LuaUtil::serialize(sol::make_object(lua, 3.14)); + EXPECT_EQ(serialized.size(), 10); // version, type, 8 bytes value + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_DOUBLE_EQ(value.as(), 3.14); + } + + TEST(LuaSerializationTest, Boolean) + { + sol::state lua; + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, true)); + EXPECT_EQ(serialized.size(), 3); // version, type, 1 byte value + sol::object value = LuaUtil::deserialize(lua, serialized); + EXPECT_FALSE(value.is()); + ASSERT_TRUE(value.is()); + EXPECT_TRUE(value.as()); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, false)); + EXPECT_EQ(serialized.size(), 3); // version, type, 1 byte value + sol::object value = LuaUtil::deserialize(lua, serialized); + EXPECT_FALSE(value.is()); + ASSERT_TRUE(value.is()); + EXPECT_FALSE(value.as()); + } + } + + TEST(LuaSerializationTest, String) + { + sol::state lua; + std::string_view emptyString = ""; + std::string_view shortString = "abc"; + std::string_view longString = "It is a string with more than 32 characters..........................."; + + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, emptyString)); + EXPECT_EQ(serialized.size(), 2); // version, type + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), emptyString); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, shortString)); + EXPECT_EQ(serialized.size(), 2 + shortString.size()); // version, type, str data + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), shortString); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, longString)); + EXPECT_EQ(serialized.size(), 6 + longString.size()); // version, type, size, str data + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), longString); + } + } + + TEST(LuaSerializationTest, Vector) + { + sol::state lua; + osg::Vec2f vec2(1, 2); + osg::Vec3f vec3(1, 2, 3); + osg::Vec4f vec4(1, 2, 3, 4); + + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec2)); + EXPECT_EQ(serialized.size(), 18); // version, type, 2x double + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), vec2); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec3)); + EXPECT_EQ(serialized.size(), 26); // version, type, 3x double + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), vec3); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec4)); + EXPECT_EQ(serialized.size(), 34); // version, type, 4x double + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), vec4); + } + } + + TEST(LuaSerializationTest, Color) + { + sol::state lua; + Misc::Color color(1, 1, 1, 1); + + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, color)); + EXPECT_EQ(serialized.size(), 18); // version, type, 4x float + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as(), color); + } + } + + TEST(LuaSerializationTest, Transform) + { + sol::state lua; + osg::Matrixf matrix(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); + LuaUtil::TransformM transM = LuaUtil::asTransform(matrix); + osg::Quat quat(1, 2, 3, 4); + LuaUtil::TransformQ transQ = LuaUtil::asTransform(quat); + + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, transM)); + EXPECT_EQ(serialized.size(), 130); // version, type, 16x double + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as().mM, transM.mM); + } + { + std::string serialized = LuaUtil::serialize(sol::make_object(lua, transQ)); + EXPECT_EQ(serialized.size(), 34); // version, type, 4x double + sol::object value = LuaUtil::deserialize(lua, serialized); + ASSERT_TRUE(value.is()); + EXPECT_EQ(value.as().mQ, transQ.mQ); + } + } + + TEST(LuaSerializationTest, Table) + { + sol::state lua; + sol::table table(lua, sol::create); + table["aa"] = 1; + table["ab"] = true; + table["nested"] = sol::table(lua, sol::create); + table["nested"]["aa"] = 2; + table["nested"]["bb"] = "something"; + table["nested"][5] = -0.5; + table["nested_empty"] = sol::table(lua, sol::create); + table[1] = osg::Vec2f(1, 2); + table[2] = osg::Vec2f(2, 1); + + std::string serialized = LuaUtil::serialize(table); + EXPECT_EQ(serialized.size(), 139); + sol::table res_table = LuaUtil::deserialize(lua, serialized); + sol::table res_readonly_table = LuaUtil::deserialize(lua, serialized, nullptr, true); + + for (auto t : { res_table, res_readonly_table }) + { + EXPECT_EQ(t.get("aa"), 1); + EXPECT_EQ(t.get("ab"), true); + EXPECT_EQ(t.get("nested").get("aa"), 2); + EXPECT_EQ(t.get("nested").get("bb"), "something"); + EXPECT_DOUBLE_EQ(t.get("nested").get(5), -0.5); + EXPECT_EQ(t.get(1), osg::Vec2f(1, 2)); + EXPECT_EQ(t.get(2), osg::Vec2f(2, 1)); + } + + lua["t"] = res_table; + lua["ro_t"] = res_readonly_table; + EXPECT_NO_THROW(lua.safe_script("t.x = 5")); + EXPECT_NO_THROW(lua.safe_script("t.nested.x = 5")); + EXPECT_ERROR(lua.safe_script("ro_t.x = 5"), "userdata value"); + EXPECT_ERROR(lua.safe_script("ro_t.nested.x = 5"), "userdata value"); + } + + struct TestStruct1 + { + double a, b; + }; + struct TestStruct2 + { + int a, b; + }; + + class TestSerializer final : public LuaUtil::UserdataSerializer + { + bool serialize(LuaUtil::BinaryData& out, const sol::userdata& data) const override + { + if (data.is()) + { + TestStruct1 t = data.as(); + t.a = Misc::toLittleEndian(t.a); + t.b = Misc::toLittleEndian(t.b); + append(out, "ts1", &t, sizeof(t)); + return true; + } + if (data.is()) + { + TestStruct2 t = data.as(); + t.a = Misc::toLittleEndian(t.a); + t.b = Misc::toLittleEndian(t.b); + append(out, "test_struct2", &t, sizeof(t)); + return true; + } + return false; + } + + bool deserialize(std::string_view typeName, std::string_view binaryData, lua_State* lua) const override + { + if (typeName == "ts1") + { + if (sizeof(TestStruct1) != binaryData.size()) + throw std::runtime_error( + "Incorrect binaryData.size() for TestStruct1: " + std::to_string(binaryData.size())); + TestStruct1 t; + std::memcpy(&t, binaryData.data(), sizeof(t)); + t.a = Misc::fromLittleEndian(t.a); + t.b = Misc::fromLittleEndian(t.b); + sol::stack::push(lua, t); + return true; + } + if (typeName == "test_struct2") + { + if (sizeof(TestStruct2) != binaryData.size()) + throw std::runtime_error( + "Incorrect binaryData.size() for TestStruct2: " + std::to_string(binaryData.size())); + TestStruct2 t; + std::memcpy(&t, binaryData.data(), sizeof(t)); + t.a = Misc::fromLittleEndian(t.a); + t.b = Misc::fromLittleEndian(t.b); + sol::stack::push(lua, t); + return true; + } + return false; + } + }; + + TEST(LuaSerializationTest, UserdataSerializer) + { + sol::state lua; + sol::table table(lua, sol::create); + table["x"] = TestStruct1{ 1.5, 2.5 }; + table["y"] = TestStruct2{ 4, 3 }; + TestSerializer serializer; + + EXPECT_ERROR(LuaUtil::serialize(table), "Value is not serializable."); + std::string serialized = LuaUtil::serialize(table, &serializer); + EXPECT_ERROR(LuaUtil::deserialize(lua, serialized), "Unknown type in serialized data:"); + sol::table res = LuaUtil::deserialize(lua, serialized, &serializer); + + TestStruct1 rx = res.get("x"); + TestStruct2 ry = res.get("y"); + EXPECT_EQ(rx.a, 1.5); + EXPECT_EQ(rx.b, 2.5); + EXPECT_EQ(ry.a, 4); + EXPECT_EQ(ry.b, 3); + } + +} diff --git a/apps/components_tests/lua/test_storage.cpp b/apps/components_tests/lua/test_storage.cpp new file mode 100644 index 00000000000..cf6db0ca648 --- /dev/null +++ b/apps/components_tests/lua/test_storage.cpp @@ -0,0 +1,128 @@ +#include +#include +#include + +#include +#include + +namespace +{ + using namespace testing; + + template + T get(sol::state_view& lua, std::string luaCode) + { + return lua.safe_script("return " + luaCode).get(); + } + + TEST(LuaUtilStorageTest, Subscribe) + { + // Note: LuaUtil::Callback can be used only if Lua is initialized via LuaUtil::LuaState + LuaUtil::LuaState luaState{ nullptr, nullptr }; + luaState.protectedCall([](LuaUtil::LuaView& view) { + sol::state_view& lua = view.sol(); + LuaUtil::LuaStorage::initLuaBindings(view); + LuaUtil::LuaStorage storage; + storage.setActive(true); + + sol::table callbackHiddenData(lua, sol::create); + callbackHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{}; + LuaUtil::getAsyncPackageInitializer( + lua.lua_state(), []() { return 0.0; }, []() { return 0.0; })(callbackHiddenData); + lua["async"] = LuaUtil::AsyncPackageId{ nullptr, 0, callbackHiddenData }; + + lua["mutable"] = storage.getMutableSection(lua, "test"); + lua["ro"] = storage.getReadOnlySection(lua, "test"); + + lua.safe_script(R"( + callbackCalls = {} + ro:subscribe(async:callback(function(section, key) + table.insert(callbackCalls, section .. '_' .. (key or '*')) + end)) + )"); + + lua.safe_script("mutable:set('x', 5)"); + EXPECT_EQ(get(lua, "mutable:get('x')"), 5); + EXPECT_EQ(get(lua, "ro:get('x')"), 5); + + EXPECT_THROW(lua.safe_script("ro:set('y', 3)"), std::exception); + + lua.safe_script("t1 = mutable:asTable()"); + lua.safe_script("t2 = ro:asTable()"); + EXPECT_EQ(get(lua, "t1.x"), 5); + EXPECT_EQ(get(lua, "t2.x"), 5); + + lua.safe_script("mutable:reset()"); + EXPECT_TRUE(get(lua, "ro:get('x') == nil")); + + lua.safe_script("mutable:reset({x=4, y=7})"); + EXPECT_EQ(get(lua, "ro:get('x')"), 4); + EXPECT_EQ(get(lua, "ro:get('y')"), 7); + + EXPECT_THAT(get(lua, "table.concat(callbackCalls, ', ')"), "test_x, test_*, test_*"); + }); + } + + TEST(LuaUtilStorageTest, Table) + { + LuaUtil::LuaState luaState{ nullptr, nullptr }; + luaState.protectedCall([](LuaUtil::LuaView& view) { + LuaUtil::LuaStorage::initLuaBindings(view); + LuaUtil::LuaStorage storage; + auto& lua = view.sol(); + storage.setActive(true); + lua["mutable"] = storage.getMutableSection(lua, "test"); + lua["ro"] = storage.getReadOnlySection(lua, "test"); + + lua.safe_script("mutable:set('x', { y = 'abc', z = 7 })"); + EXPECT_EQ(get(lua, "mutable:get('x').z"), 7); + EXPECT_THROW(lua.safe_script("mutable:get('x').z = 3"), std::exception); + EXPECT_NO_THROW(lua.safe_script("mutable:getCopy('x').z = 3")); + EXPECT_EQ(get(lua, "mutable:get('x').z"), 7); + EXPECT_EQ(get(lua, "ro:get('x').z"), 7); + EXPECT_EQ(get(lua, "ro:get('x').y"), "abc"); + }); + } + + TEST(LuaUtilStorageTest, Saving) + { + LuaUtil::LuaState luaState{ nullptr, nullptr }; + luaState.protectedCall([](LuaUtil::LuaView& view) { + LuaUtil::LuaStorage::initLuaBindings(view); + LuaUtil::LuaStorage storage; + auto& lua = view.sol(); + storage.setActive(true); + + lua["permanent"] = storage.getMutableSection(lua, "permanent"); + lua["temporary"] = storage.getMutableSection(lua, "temporary"); + lua.safe_script("temporary:removeOnExit()"); + lua.safe_script("permanent:set('x', 1)"); + lua.safe_script("temporary:set('y', 2)"); + + const auto tmpFile = std::filesystem::temp_directory_path() / "test_storage.bin"; + storage.save(lua, tmpFile); + EXPECT_EQ(get(lua, "permanent:get('x')"), 1); + EXPECT_EQ(get(lua, "temporary:get('y')"), 2); + + storage.clearTemporaryAndRemoveCallbacks(); + lua["permanent"] = storage.getMutableSection(lua, "permanent"); + lua["temporary"] = storage.getMutableSection(lua, "temporary"); + EXPECT_EQ(get(lua, "permanent:get('x')"), 1); + EXPECT_TRUE(get(lua, "temporary:get('y') == nil")); + + lua.safe_script("permanent:set('x', 3)"); + lua.safe_script("permanent:set('z', 4)"); + + LuaUtil::LuaStorage storage2; + storage2.setActive(true); + storage2.load(lua, tmpFile); + lua["permanent"] = storage2.getMutableSection(lua, "permanent"); + lua["temporary"] = storage2.getMutableSection(lua, "temporary"); + + EXPECT_EQ(get(lua, "permanent:get('x')"), 1); + EXPECT_TRUE(get(lua, "permanent:get('z') == nil")); + EXPECT_TRUE(get(lua, "temporary:get('y') == nil")); + }); + } + +} diff --git a/apps/components_tests/lua/test_ui_content.cpp b/apps/components_tests/lua/test_ui_content.cpp new file mode 100644 index 00000000000..fcdfd8a1b3b --- /dev/null +++ b/apps/components_tests/lua/test_ui_content.cpp @@ -0,0 +1,146 @@ +#include +#include + +#include +#include + +namespace +{ + using namespace testing; + + struct LuaUiContentTest : Test + { + LuaUtil::LuaState mLuaState{ nullptr, nullptr }; + sol::protected_function mNew; + LuaUiContentTest() + { + mLuaState.addInternalLibSearchPath( + std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "components" / "lua_ui"); + mNew = LuaUi::loadContentConstructor(&mLuaState); + } + + LuaUi::ContentView makeContent(sol::table source) + { + auto result = mNew.call(source); + if (result.get_type() != sol::type::table) + throw std::logic_error("Expected table"); + return LuaUi::ContentView(result.get()); + } + + sol::table makeTable() { return sol::table(mLuaState.unsafeState(), sol::create); } + + sol::table makeTable(std::string name) + { + auto result = makeTable(); + result["name"] = name; + return result; + } + }; + + TEST_F(LuaUiContentTest, ProtectedMetatable) + { + sol::state_view sol = mLuaState.unsafeState(); + sol["makeContent"] = mNew; + sol["M"] = makeContent(makeTable()).getMetatable(); + std::string testScript = R"( + assert(not pcall(function() setmetatable(makeContent{}, {}) end), 'Metatable is not protected') + assert(getmetatable(makeContent{}) == false, 'Metatable is not protected') + )"; + EXPECT_NO_THROW(sol.safe_script(testScript)); + } + + TEST_F(LuaUiContentTest, Create) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table.add(makeTable()); + LuaUi::ContentView content = makeContent(table); + EXPECT_EQ(content.size(), 3); + } + + TEST_F(LuaUiContentTest, Insert) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table.add(makeTable()); + LuaUi::ContentView content = makeContent(table); + content.insert(2, makeTable("inserted")); + EXPECT_EQ(content.size(), 4); + auto inserted = content.at("inserted"); + auto index = content.indexOf(inserted); + EXPECT_TRUE(index.has_value()); + EXPECT_EQ(index.value(), 2); + } + + TEST_F(LuaUiContentTest, MakeHole) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + LuaUi::ContentView content = makeContent(table); + sol::table t = makeTable(); + EXPECT_ANY_THROW(content.assign(3, t)); + } + + TEST_F(LuaUiContentTest, WrongType) + { + auto table = makeTable(); + table.add(makeTable()); + table.add("a"); + table.add(makeTable()); + EXPECT_ANY_THROW(makeContent(table)); + } + + TEST_F(LuaUiContentTest, NameAccess) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable("a")); + LuaUi::ContentView content = makeContent(table); + EXPECT_NO_THROW(content.at("a")); + content.remove("a"); + EXPECT_EQ(content.size(), 1); + content.assign(content.size(), makeTable("b")); + content.assign("b", makeTable()); + EXPECT_ANY_THROW(content.at("b")); + EXPECT_EQ(content.size(), 2); + content.assign(content.size(), makeTable("c")); + content.assign(content.size(), makeTable("c")); + content.remove("c"); + EXPECT_ANY_THROW(content.at("c")); + } + + TEST_F(LuaUiContentTest, IndexOf) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table.add(makeTable()); + LuaUi::ContentView content = makeContent(table); + auto child = makeTable(); + content.assign(2, child); + EXPECT_EQ(content.indexOf(child).value(), 2); + EXPECT_TRUE(!content.indexOf(makeTable()).has_value()); + } + + TEST_F(LuaUiContentTest, BoundsChecks) + { + auto table = makeTable(); + LuaUi::ContentView content = makeContent(table); + EXPECT_ANY_THROW(content.at(0)); + EXPECT_EQ(content.size(), 0); + content.assign(content.size(), makeTable()); + EXPECT_EQ(content.size(), 1); + content.assign(content.size(), makeTable()); + EXPECT_EQ(content.size(), 2); + content.assign(content.size(), makeTable()); + EXPECT_EQ(content.size(), 3); + EXPECT_ANY_THROW(content.at(3)); + EXPECT_ANY_THROW(content.remove(3)); + content.remove(2); + EXPECT_EQ(content.size(), 2); + EXPECT_ANY_THROW(content.at(2)); + } +} diff --git a/apps/components_tests/lua/test_utilpackage.cpp b/apps/components_tests/lua/test_utilpackage.cpp new file mode 100644 index 00000000000..a61c0e03066 --- /dev/null +++ b/apps/components_tests/lua/test_utilpackage.cpp @@ -0,0 +1,211 @@ +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + + template + T get(sol::state& lua, const std::string& luaCode) + { + return lua.safe_script("return " + luaCode).get(); + } + + std::string getAsString(sol::state& lua, std::string luaCode) + { + return LuaUtil::toString(lua.safe_script("return " + luaCode)); + } + + TEST(LuaUtilPackageTest, Vector2) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua.safe_script("v = util.vector2(3, 4)"); + EXPECT_FLOAT_EQ(get(lua, "v.x"), 3); + EXPECT_FLOAT_EQ(get(lua, "v.y"), 4); + EXPECT_EQ(get(lua, "tostring(v)"), "(3, 4)"); + EXPECT_FLOAT_EQ(get(lua, "v:length()"), 5); + EXPECT_FLOAT_EQ(get(lua, "v:length2()"), 25); + EXPECT_FALSE(get(lua, "util.vector2(1, 2) == util.vector2(1, 3)")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) + util.vector2(2, 5) == util.vector2(3, 7)")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) - util.vector2(2, 5) == -util.vector2(1, 3)")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) == util.vector2(2, 4) / 2")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2) * 2 == util.vector2(2, 4)")); + EXPECT_FLOAT_EQ(get(lua, "util.vector2(3, 2) * v"), 17); + EXPECT_FLOAT_EQ(get(lua, "util.vector2(3, 2):dot(v)"), 17); + EXPECT_ERROR(lua.safe_script("v2, len = v.normalize()"), + "value is not a valid userdata"); // checks that it doesn't segfault + lua.safe_script("v2, len = v:normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 5); + EXPECT_TRUE(get(lua, "v2 == util.vector2(3/5, 4/5)")); + lua.safe_script("_, len = util.vector2(0, 0):normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 0); + lua.safe_script("ediv0 = util.vector2(1, 0):ediv(util.vector2(0, 0))"); + EXPECT_TRUE(get(lua, "ediv0.x == math.huge and ediv0.y ~= ediv0.y")); + EXPECT_TRUE(get(lua, "util.vector2(1, 2):emul(util.vector2(3, 4)) == util.vector2(3, 8)")); + EXPECT_TRUE(get(lua, "util.vector2(4, 6):ediv(util.vector2(2, 3)) == util.vector2(2, 2)")); + lua.safe_script("swizzle = util.vector2(1, 2)"); + EXPECT_TRUE(get(lua, "swizzle.xx == util.vector2(1, 1) and swizzle.yy == util.vector2(2, 2)")); + EXPECT_TRUE(get(lua, "swizzle.y0 == util.vector2(2, 0) and swizzle.x1 == util.vector2(1, 1)")); + EXPECT_TRUE(get(lua, "swizzle['01'] == util.vector2(0, 1) and swizzle['0y'] == util.vector2(0, 2)")); + } + + TEST(LuaUtilPackageTest, Vector3) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + 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); + EXPECT_FLOAT_EQ(get(lua, "v.z"), 13); + EXPECT_EQ(get(lua, "tostring(v)"), "(5, 12, 13)"); + EXPECT_EQ(getAsString(lua, "v"), "(5, 12, 13)"); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(4, 0, 3):length()"), 5); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(4, 0, 3):length2()"), 25); + EXPECT_FALSE(get(lua, "util.vector3(1, 2, 3) == util.vector3(1, 3, 2)")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) + util.vector3(2, 5, 1) == util.vector3(3, 7, 4)")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) - util.vector3(2, 5, 1) == -util.vector3(1, 3, -2)")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) == util.vector3(2, 4, 6) / 2")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3) * 2 == util.vector3(2, 4, 6)")); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(3, 2, 1) * v"), 5 * 3 + 12 * 2 + 13 * 1); + EXPECT_FLOAT_EQ(get(lua, "util.vector3(3, 2, 1):dot(v)"), 5 * 3 + 12 * 2 + 13 * 1); + EXPECT_TRUE(get(lua, "util.vector3(1, 0, 0) ^ util.vector3(0, 1, 0) == util.vector3(0, 0, 1)")); + EXPECT_ERROR(lua.safe_script("v2, len = util.vector3(3, 4, 0).normalize()"), "value is not a valid userdata"); + lua.safe_script("v2, len = util.vector3(3, 4, 0):normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 5); + EXPECT_TRUE(get(lua, "v2 == util.vector3(3/5, 4/5, 0)")); + lua.safe_script("_, len = util.vector3(0, 0, 0):normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 0); + lua.safe_script("ediv0 = util.vector3(1, 1, 1):ediv(util.vector3(0, 0, 0))"); + EXPECT_TRUE(get(lua, "ediv0.z == math.huge")); + EXPECT_TRUE(get(lua, "util.vector3(1, 2, 3):emul(util.vector3(3, 4, 5)) == util.vector3(3, 8, 15)")); + EXPECT_TRUE(get(lua, "util.vector3(4, 6, 8):ediv(util.vector3(2, 3, 4)) == util.vector3(2, 2, 2)")); + lua.safe_script("swizzle = util.vector3(1, 2, 3)"); + EXPECT_TRUE(get(lua, "swizzle.xxx == util.vector3(1, 1, 1)")); + EXPECT_TRUE(get(lua, "swizzle.xyz == swizzle.zyx.zyx")); + EXPECT_TRUE(get(lua, "swizzle.xy0 == util.vector3(1, 2, 0) and swizzle.x11 == util.vector3(1, 1, 1)")); + EXPECT_TRUE( + get(lua, "swizzle['001'] == util.vector3(0, 0, 1) and swizzle['0yx'] == util.vector3(0, 2, 1)")); + } + + TEST(LuaUtilPackageTest, Vector4) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + 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); + EXPECT_FLOAT_EQ(get(lua, "v.z"), 13); + EXPECT_FLOAT_EQ(get(lua, "v.w"), 15); + EXPECT_EQ(get(lua, "tostring(v)"), "(5, 12, 13, 15)"); + EXPECT_FLOAT_EQ(get(lua, "util.vector4(4, 0, 0, 3):length()"), 5); + EXPECT_FLOAT_EQ(get(lua, "util.vector4(4, 0, 0, 3):length2()"), 25); + EXPECT_FALSE(get(lua, "util.vector4(1, 2, 3, 4) == util.vector4(1, 3, 2, 4)")); + EXPECT_TRUE(get(lua, "util.vector4(1, 2, 3, 4) + util.vector4(2, 5, 1, 2) == util.vector4(3, 7, 4, 6)")); + EXPECT_TRUE( + get(lua, "util.vector4(1, 2, 3, 4) - util.vector4(2, 5, 1, 7) == -util.vector4(1, 3, -2, 3)")); + EXPECT_TRUE(get(lua, "util.vector4(1, 2, 3, 4) == util.vector4(2, 4, 6, 8) / 2")); + EXPECT_TRUE(get(lua, "util.vector4(1, 2, 3, 4) * 2 == util.vector4(2, 4, 6, 8)")); + EXPECT_FLOAT_EQ(get(lua, "util.vector4(3, 2, 1, 4) * v"), 5 * 3 + 12 * 2 + 13 * 1 + 15 * 4); + EXPECT_FLOAT_EQ(get(lua, "util.vector4(3, 2, 1, 4):dot(v)"), 5 * 3 + 12 * 2 + 13 * 1 + 15 * 4); + lua.safe_script("v2, len = util.vector4(3, 0, 0, 4):normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 5); + EXPECT_TRUE(get(lua, "v2 == util.vector4(3/5, 0, 0, 4/5)")); + lua.safe_script("_, len = util.vector4(0, 0, 0, 0):normalize()"); + EXPECT_FLOAT_EQ(get(lua, "len"), 0); + lua.safe_script("ediv0 = util.vector4(1, 1, 1, -1):ediv(util.vector4(0, 0, 0, 0))"); + EXPECT_TRUE(get(lua, "ediv0.w == -math.huge")); + EXPECT_TRUE( + get(lua, "util.vector4(1, 2, 3, 4):emul(util.vector4(3, 4, 5, 6)) == util.vector4(3, 8, 15, 24)")); + EXPECT_TRUE( + get(lua, "util.vector4(4, 6, 8, 9):ediv(util.vector4(2, 3, 4, 3)) == util.vector4(2, 2, 2, 3)")); + lua.safe_script("swizzle = util.vector4(1, 2, 3, 4)"); + EXPECT_TRUE(get(lua, "swizzle.wwww == util.vector4(4, 4, 4, 4)")); + EXPECT_TRUE(get(lua, "swizzle.xyzw == util.vector4(1, 2, 3, 4)")); + EXPECT_TRUE(get(lua, "swizzle.xyzw == swizzle.wzyx.wzyx")); + EXPECT_TRUE( + get(lua, "swizzle.xyz0 == util.vector4(1, 2, 3, 0) and swizzle.w110 == util.vector4(4, 1, 1, 0)")); + EXPECT_TRUE(get( + lua, "swizzle['0001'] == util.vector4(0, 0, 0, 1) and swizzle['0yx1'] == util.vector4(0, 2, 1, 1)")); + } + + TEST(LuaUtilPackageTest, Color) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + 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)"); + EXPECT_EQ(get(lua, "tostring(blue)"), "(0, 1, 0, 1)"); + lua.safe_script("red = util.color.hex('ff0000')"); + EXPECT_EQ(get(lua, "tostring(red)"), "(1, 0, 0, 1)"); + lua.safe_script("green = util.color.hex('00FF00')"); + EXPECT_EQ(get(lua, "tostring(green)"), "(0, 1, 0, 1)"); + lua.safe_script("darkRed = util.color.hex('a01112')"); + EXPECT_EQ(get(lua, "darkRed:asHex()"), "a01112"); + EXPECT_TRUE(get(lua, "green:asRgba() == util.vector4(0, 1, 0, 1)")); + EXPECT_TRUE(get(lua, "red:asRgb() == util.vector3(1, 0, 0)")); + } + + TEST(LuaUtilPackageTest, Transform) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + lua["T"] = lua["util"]["transform"]; + lua["v"] = lua["util"]["vector3"]; + EXPECT_ERROR(lua.safe_script("T.identity = nil"), "attempt to index"); + EXPECT_EQ(getAsString(lua, "T.identity * v(3, 4, 5)"), "(3, 4, 5)"); + EXPECT_EQ(getAsString(lua, "T.move(1, 2, 3) * v(3, 4, 5)"), "(4, 6, 8)"); + EXPECT_EQ(getAsString(lua, "T.scale(1, -2, 3) * v(3, 4, 5)"), "(3, -8, 15)"); + EXPECT_EQ(getAsString(lua, "T.scale(v(1, 2, 3)) * v(3, 4, 5)"), "(3, 8, 15)"); + lua.safe_script("moveAndScale = T.move(v(1, 2, 3)) * T.scale(0.5, 1, 0.5) * T.move(10, 20, 30)"); + EXPECT_EQ(getAsString(lua, "moveAndScale * v(0, 0, 0)"), "(6, 22, 18)"); + EXPECT_EQ(getAsString(lua, "moveAndScale * v(300, 200, 100)"), "(156, 222, 68)"); + EXPECT_EQ(getAsString(lua, "moveAndScale:apply(v(0, 0, 0))"), "(6, 22, 18)"); + EXPECT_EQ(getAsString(lua, "moveAndScale:apply(v(300, 200, 100))"), "(156, 222, 68)"); + EXPECT_THAT(getAsString(lua, "moveAndScale"), + AllOf(StartsWith("TransformM{ move(6, 22, 18) scale(0.5, 1, 0.5) "), EndsWith(" }"))); + EXPECT_EQ(getAsString(lua, "T.identity"), "TransformQ{ rotation(angle=0, axis=(0, 0, 1)) }"); + lua.safe_script("rx = T.rotateX(-math.pi / 2)"); + lua.safe_script("ry = T.rotateY(-math.pi / 2)"); + lua.safe_script("rz = T.rotateZ(-math.pi / 2)"); + EXPECT_LT(get(lua, "(rx * v(1, 2, 3) - v(1, -3, 2)):length()"), 1e-6); + EXPECT_LT(get(lua, "(ry * v(1, 2, 3) - v(3, 2, -1)):length()"), 1e-6); + EXPECT_LT(get(lua, "(rz * v(1, 2, 3) - v(-2, 1, 3)):length()"), 1e-6); + EXPECT_LT(get(lua, "(rz:apply(v(1, 2, 3)) - v(-2, 1, 3)):length()"), 1e-6); + lua.safe_script("rot = T.rotate(math.pi / 2, v(-1, -1, 0)) * T.rotateZ(math.pi / 4)"); + EXPECT_THAT(getAsString(lua, "rot"), HasSubstr("TransformQ")); + EXPECT_LT(get(lua, "(rot * v(1, 0, 0) - v(0, 0, 1)):length()"), 1e-6); + EXPECT_LT(get(lua, "(rot * rot:inverse() * v(1, 0, 0) - v(1, 0, 0)):length()"), 1e-6); + lua.safe_script("rz_move_rx = rz * T.move(0, 3, 0) * rx"); + EXPECT_LT(get(lua, "(rz_move_rx * v(1, 2, 3) - v(0, 1, 2)):length()"), 1e-6); + EXPECT_LT(get(lua, "(rz_move_rx:inverse() * v(0, 1, 2) - v(1, 2, 3)):length()"), 1e-6); + } + + TEST(LuaUtilPackageTest, UtilityFunctions) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); + lua["util"] = LuaUtil::initUtilPackage(lua); + 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); + EXPECT_FLOAT_EQ(get(lua, "util.normalizeAngle(math.pi * 10 + 0.1)"), 0.1f); + 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); + 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/test_yaml.cpp new file mode 100644 index 00000000000..fa28889440a --- /dev/null +++ b/apps/components_tests/lua/test_yaml.cpp @@ -0,0 +1,357 @@ +#include + +#include +#include +#include + +#include + +#include + +namespace +{ + template + bool checkNumber(sol::state_view& lua, const std::string& inputData, T requiredValue) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + if (result.get_type() != sol::type::number) + return false; + + return result.as() == requiredValue; + } + + bool checkBool(sol::state_view& lua, const std::string& inputData, bool requiredValue) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + if (result.get_type() != sol::type::boolean) + return false; + + return result.as() == requiredValue; + } + + bool checkNil(sol::state_view& lua, const std::string& inputData) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + return result == sol::nil; + } + + bool checkNan(sol::state_view& lua, const std::string& inputData) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + if (result.get_type() != sol::type::number) + return false; + + return std::isnan(result.as()); + } + + bool checkString(sol::state_view& lua, const std::string& inputData, const std::string& requiredValue) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + if (result.get_type() != sol::type::string) + return false; + + return result.as() == requiredValue; + } + + bool checkString(sol::state_view& lua, const std::string& inputData) + { + sol::object result = LuaUtil::loadYaml(inputData, lua); + if (result.get_type() != sol::type::string) + return false; + + return result.as() == inputData; + } + + TEST(LuaUtilYamlLoader, ScalarTypeDeduction) + { + sol::state lua; + + ASSERT_TRUE(checkNil(lua, "null")); + ASSERT_TRUE(checkNil(lua, "Null")); + ASSERT_TRUE(checkNil(lua, "NULL")); + ASSERT_TRUE(checkNil(lua, "~")); + ASSERT_TRUE(checkNil(lua, "")); + ASSERT_FALSE(checkNil(lua, "NUll")); + ASSERT_TRUE(checkString(lua, "NUll")); + ASSERT_TRUE(checkString(lua, "'null'", "null")); + + ASSERT_TRUE(checkNumber(lua, "017", 17)); + ASSERT_TRUE(checkNumber(lua, "-017", -17)); + ASSERT_TRUE(checkNumber(lua, "+017", 17)); + ASSERT_TRUE(checkNumber(lua, "17", 17)); + ASSERT_TRUE(checkNumber(lua, "-17", -17)); + ASSERT_TRUE(checkNumber(lua, "+17", 17)); + ASSERT_TRUE(checkNumber(lua, "0o17", 15)); + ASSERT_TRUE(checkString(lua, "-0o17")); + ASSERT_TRUE(checkString(lua, "+0o17")); + ASSERT_TRUE(checkString(lua, "0b1")); + ASSERT_TRUE(checkString(lua, "1:00")); + ASSERT_TRUE(checkString(lua, "'17'", "17")); + ASSERT_TRUE(checkNumber(lua, "0x17", 23)); + ASSERT_TRUE(checkString(lua, "'-0x17'", "-0x17")); + ASSERT_TRUE(checkString(lua, "'+0x17'", "+0x17")); + + ASSERT_TRUE(checkNumber(lua, "2.1e-05", 2.1e-5)); + ASSERT_TRUE(checkNumber(lua, "-2.1e-05", -2.1e-5)); + ASSERT_TRUE(checkNumber(lua, "+2.1e-05", 2.1e-5)); + ASSERT_TRUE(checkNumber(lua, "2.1e+5", 210000)); + ASSERT_TRUE(checkNumber(lua, "-2.1e+5", -210000)); + ASSERT_TRUE(checkNumber(lua, "+2.1e+5", 210000)); + ASSERT_TRUE(checkNumber(lua, "0.27", 0.27)); + ASSERT_TRUE(checkNumber(lua, "-0.27", -0.27)); + ASSERT_TRUE(checkNumber(lua, "+0.27", 0.27)); + ASSERT_TRUE(checkNumber(lua, "2.7", 2.7)); + ASSERT_TRUE(checkNumber(lua, "-2.7", -2.7)); + ASSERT_TRUE(checkNumber(lua, "+2.7", 2.7)); + ASSERT_TRUE(checkNumber(lua, ".27", 0.27)); + ASSERT_TRUE(checkNumber(lua, "-.27", -0.27)); + ASSERT_TRUE(checkNumber(lua, "+.27", 0.27)); + ASSERT_TRUE(checkNumber(lua, "27.", 27.0)); + ASSERT_TRUE(checkNumber(lua, "-27.", -27.0)); + ASSERT_TRUE(checkNumber(lua, "+27.", 27.0)); + + ASSERT_TRUE(checkNan(lua, ".nan")); + ASSERT_TRUE(checkNan(lua, ".NaN")); + ASSERT_TRUE(checkNan(lua, ".NAN")); + ASSERT_FALSE(checkNan(lua, "nan")); + ASSERT_FALSE(checkNan(lua, ".nAn")); + ASSERT_TRUE(checkString(lua, "'.nan'", ".nan")); + ASSERT_TRUE(checkString(lua, ".nAn")); + + ASSERT_TRUE(checkNumber(lua, "1.7976931348623157E+308", std::numeric_limits::max())); + ASSERT_TRUE(checkNumber(lua, "-1.7976931348623157E+308", std::numeric_limits::lowest())); + ASSERT_TRUE(checkNumber(lua, "2.2250738585072014e-308", std::numeric_limits::min())); + ASSERT_TRUE(checkNumber(lua, ".inf", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "+.inf", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "-.inf", -std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, ".Inf", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "+.Inf", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "-.Inf", -std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, ".INF", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "+.INF", std::numeric_limits::infinity())); + ASSERT_TRUE(checkNumber(lua, "-.INF", -std::numeric_limits::infinity())); + ASSERT_TRUE(checkString(lua, ".INf")); + ASSERT_TRUE(checkString(lua, "-.INf")); + ASSERT_TRUE(checkString(lua, "+.INf")); + + ASSERT_TRUE(checkBool(lua, "true", true)); + ASSERT_TRUE(checkBool(lua, "false", false)); + ASSERT_TRUE(checkBool(lua, "True", true)); + ASSERT_TRUE(checkBool(lua, "False", false)); + ASSERT_TRUE(checkBool(lua, "TRUE", true)); + ASSERT_TRUE(checkBool(lua, "FALSE", false)); + ASSERT_TRUE(checkString(lua, "y")); + ASSERT_TRUE(checkString(lua, "n")); + ASSERT_TRUE(checkString(lua, "On")); + ASSERT_TRUE(checkString(lua, "Off")); + ASSERT_TRUE(checkString(lua, "YES")); + ASSERT_TRUE(checkString(lua, "NO")); + ASSERT_TRUE(checkString(lua, "TrUe")); + ASSERT_TRUE(checkString(lua, "FaLsE")); + ASSERT_TRUE(checkString(lua, "'true'", "true")); + } + + TEST(LuaUtilYamlLoader, DepthLimit) + { + sol::state lua; + + const std::string input = R"( + array1: &array1_alias + [ + <: *array1_alias, + foo + ] + )"; + + bool depthExceptionThrown = false; + try + { + YAML::Node root = YAML::Load(input); + sol::object result = LuaUtil::loadYaml(input, lua); + } + catch (const std::runtime_error& e) + { + ASSERT_EQ(std::string(e.what()), "Maximum layers depth exceeded, probably caused by a circular reference"); + depthExceptionThrown = true; + } + + ASSERT_TRUE(depthExceptionThrown); + } + + TEST(LuaUtilYamlLoader, Collections) + { + sol::state lua; + + sol::object map = LuaUtil::loadYaml("{ x: , y: 2, 4: 5 }", lua); + ASSERT_EQ(map.as()["x"], sol::nil); + ASSERT_EQ(map.as()["y"], 2); + ASSERT_EQ(map.as()[4], 5); + + sol::object array = LuaUtil::loadYaml("[ 3, 4 ]", lua); + ASSERT_EQ(array.as()[1], 3); + + sol::object emptyTable = LuaUtil::loadYaml("{}", lua); + ASSERT_TRUE(emptyTable.as().empty()); + + sol::object emptyArray = LuaUtil::loadYaml("[]", lua); + ASSERT_TRUE(emptyArray.as().empty()); + + ASSERT_THROW(LuaUtil::loadYaml("{ null: 1 }", lua), std::runtime_error); + ASSERT_THROW(LuaUtil::loadYaml("{ .nan: 1 }", lua), std::runtime_error); + + const std::string scalarArrayInput = R"( + - First Scalar + - 1 + - true)"; + + sol::object scalarArray = LuaUtil::loadYaml(scalarArrayInput, lua); + ASSERT_EQ(scalarArray.as()[1], std::string("First Scalar")); + ASSERT_EQ(scalarArray.as()[2], 1); + ASSERT_EQ(scalarArray.as()[3], true); + + const std::string scalarMapWithCommentsInput = R"( + string: 'str' # String value + integer: 65 # Integer value + float: 0.278 # Float value + bool: false # Boolean value)"; + + sol::object scalarMapWithComments = LuaUtil::loadYaml(scalarMapWithCommentsInput, lua); + ASSERT_EQ(scalarMapWithComments.as()["string"], std::string("str")); + ASSERT_EQ(scalarMapWithComments.as()["integer"], 65); + ASSERT_EQ(scalarMapWithComments.as()["float"], 0.278); + ASSERT_EQ(scalarMapWithComments.as()["bool"], false); + + const std::string mapOfArraysInput = R"( + x: + - 2 + - 7 + - true + y: + - aaa + - false + - 1)"; + + sol::object mapOfArrays = LuaUtil::loadYaml(mapOfArraysInput, lua); + ASSERT_EQ(mapOfArrays.as()["x"][3], true); + ASSERT_EQ(mapOfArrays.as()["y"][1], std::string("aaa")); + + const std::string arrayOfMapsInput = R"( + - + name: Name1 + hr: 65 + avg: 0.278 + - + name: Name2 + hr: 63 + avg: 0.288)"; + + sol::object arrayOfMaps = LuaUtil::loadYaml(arrayOfMapsInput, lua); + ASSERT_EQ(arrayOfMaps.as()[1]["avg"], 0.278); + ASSERT_EQ(arrayOfMaps.as()[2]["name"], std::string("Name2")); + + const std::string arrayOfArraysInput = R"( + - [Name1, 65, 0.278] + - [Name2 , 63, 0.288])"; + + sol::object arrayOfArrays = LuaUtil::loadYaml(arrayOfArraysInput, lua); + ASSERT_EQ(arrayOfArrays.as()[1][2], 65); + ASSERT_EQ(arrayOfArrays.as()[2][1], std::string("Name2")); + + const std::string mapOfMapsInput = R"( + Name1: {hr: 65, avg: 0.278} + Name2 : { + hr: 63, + avg: 0.288, + })"; + + sol::object mapOfMaps = LuaUtil::loadYaml(mapOfMapsInput, lua); + ASSERT_EQ(mapOfMaps.as()["Name1"]["hr"], 65); + ASSERT_EQ(mapOfMaps.as()["Name2"]["avg"], 0.288); + } + + TEST(LuaUtilYamlLoader, Structures) + { + sol::state lua; + + const std::string twoDocumentsInput + = "---\n" + " - First Scalar\n" + " - 2\n" + " - true\n" + "\n" + "---\n" + " - Second Scalar\n" + " - 3\n" + " - false"; + + sol::object twoDocuments = LuaUtil::loadYaml(twoDocumentsInput, lua); + ASSERT_EQ(twoDocuments.as()[1][1], std::string("First Scalar")); + ASSERT_EQ(twoDocuments.as()[2][3], false); + + const std::string anchorInput = R"(--- + x: + - Name1 + # Following node labeled as "a" + - &a Value1 + y: + - *a # Subsequent occurrence + - Name2)"; + + sol::object anchor = LuaUtil::loadYaml(anchorInput, lua); + ASSERT_EQ(anchor.as()["y"][1], std::string("Value1")); + + const std::string compoundKeyInput = R"( + ? - String1 + - String2 + : - 1 + + ? [ String3, + String4 ] + : [ 2, 3, 4 ])"; + + ASSERT_THROW(LuaUtil::loadYaml(compoundKeyInput, lua), std::runtime_error); + + const std::string compactNestedMappingInput = R"( + - item : Item1 + quantity: 2 + - item : Item2 + quantity: 4 + - item : Item3 + quantity: 11)"; + + sol::object compactNestedMapping = LuaUtil::loadYaml(compactNestedMappingInput, lua); + ASSERT_EQ(compactNestedMapping.as()[2]["quantity"], 4); + } + + TEST(LuaUtilYamlLoader, Scalars) + { + sol::state lua; + + const std::string literalScalarInput = R"(--- | + a + b + c)"; + + ASSERT_TRUE(checkString(lua, literalScalarInput, "a\nb\nc")); + + const std::string foldedScalarInput = R"(--- > + a + b + c)"; + + ASSERT_TRUE(checkString(lua, foldedScalarInput, "a b c")); + + const std::string multiLinePlanarScalarsInput = R"( + plain: + This unquoted scalar + spans many lines. + + quoted: "So does this + quoted scalar.\n")"; + + sol::object multiLinePlanarScalars = LuaUtil::loadYaml(multiLinePlanarScalarsInput, lua); + ASSERT_TRUE( + multiLinePlanarScalars.as()["plain"] == std::string("This unquoted scalar spans many lines.")); + ASSERT_TRUE(multiLinePlanarScalars.as()["quoted"] == std::string("So does this quoted scalar.\n")); + } +} diff --git a/apps/components_tests/main.cpp b/apps/components_tests/main.cpp new file mode 100644 index 00000000000..dcfb2e9ba91 --- /dev/null +++ b/apps/components_tests/main.cpp @@ -0,0 +1,28 @@ +#include +#include +#include +#include + +#include + +#include + +int main(int argc, char** argv) +{ + Log::sMinDebugLevel = Debug::getDebugLevel(); + + const std::filesystem::path settingsDefaultPath = std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "files" + / Misc::StringUtils::stringToU8String("settings-default.cfg"); + + Settings::SettingsFileParser parser; + parser.loadSettingsFile(settingsDefaultPath, Settings::Manager::mDefaultSettings); + + Settings::StaticValues::initDefaults(); + + Settings::Manager::mUserSettings = Settings::Manager::mDefaultSettings; + + Settings::StaticValues::init(); + + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/apps/components_tests/misc/compression.cpp b/apps/components_tests/misc/compression.cpp new file mode 100644 index 00000000000..e062599f4a9 --- /dev/null +++ b/apps/components_tests/misc/compression.cpp @@ -0,0 +1,31 @@ +#include + +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Misc; + + TEST(MiscCompressionTest, compressShouldAddPrefixWithDataSize) + { + const std::vector data(1234); + const std::vector compressed = compress(data); + int size = 0; + std::memcpy(&size, compressed.data(), sizeof(size)); + EXPECT_EQ(size, data.size()); + } + + TEST(MiscCompressionTest, decompressIsInverseToCompress) + { + const std::vector data(1024); + const std::vector compressed = compress(data); + EXPECT_LT(compressed.size(), data.size()); + const std::vector decompressed = decompress(compressed); + EXPECT_EQ(decompressed, data); + } +} diff --git a/apps/components_tests/misc/progressreporter.cpp b/apps/components_tests/misc/progressreporter.cpp new file mode 100644 index 00000000000..d5f7c649f1a --- /dev/null +++ b/apps/components_tests/misc/progressreporter.cpp @@ -0,0 +1,40 @@ +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace Misc; + + struct ReportMock + { + MOCK_METHOD(void, call, (std::size_t, std::size_t), ()); + }; + + struct Report + { + StrictMock* mImpl; + + void operator()(std::size_t provided, std::size_t expected) { mImpl->call(provided, expected); } + }; + + TEST(MiscProgressReporterTest, shouldCallReportWhenPassedInterval) + { + StrictMock report; + EXPECT_CALL(report, call(13, 42)).WillOnce(Return()); + ProgressReporter reporter(std::chrono::steady_clock::duration(0), Report{ &report }); + reporter(13, 42); + } + + TEST(MiscProgressReporterTest, shouldNotCallReportWhenIntervalIsNotPassed) + { + StrictMock report; + EXPECT_CALL(report, call(13, 42)).Times(0); + ProgressReporter reporter(std::chrono::seconds(1000), Report{ &report }); + reporter(13, 42); + } +} diff --git a/apps/components_tests/misc/test_endianness.cpp b/apps/components_tests/misc/test_endianness.cpp new file mode 100644 index 00000000000..2d0b0bcdbcc --- /dev/null +++ b/apps/components_tests/misc/test_endianness.cpp @@ -0,0 +1,124 @@ +#include "components/misc/endianness.hpp" +#include + +struct EndiannessTest : public ::testing::Test +{ +}; + +TEST_F(EndiannessTest, test_swap_endianness_inplace1) +{ + uint8_t zero = 0x00; + uint8_t ff = 0xFF; + uint8_t fortytwo = 0x42; + uint8_t half = 128; + + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x00); + + Misc::swapEndiannessInplace(ff); + EXPECT_EQ(ff, 0xFF); + + Misc::swapEndiannessInplace(fortytwo); + EXPECT_EQ(fortytwo, 0x42); + + Misc::swapEndiannessInplace(half); + EXPECT_EQ(half, 128); +} + +TEST_F(EndiannessTest, test_swap_endianness_inplace2) +{ + uint16_t zero = 0x0000; + uint16_t ffff = 0xFFFF; + uint16_t n12 = 0x0102; + uint16_t fortytwo = 0x0042; + + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x0000u); + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x0000u); + + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFFu); + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFFu); + + Misc::swapEndiannessInplace(n12); + EXPECT_EQ(n12, 0x0201u); + Misc::swapEndiannessInplace(n12); + EXPECT_EQ(n12, 0x0102u); + + Misc::swapEndiannessInplace(fortytwo); + EXPECT_EQ(fortytwo, 0x4200u); + Misc::swapEndiannessInplace(fortytwo); + EXPECT_EQ(fortytwo, 0x0042u); +} + +TEST_F(EndiannessTest, test_swap_endianness_inplace4) +{ + uint32_t zero = 0x00000000; + uint32_t n1234 = 0x01020304; + uint32_t ffff = 0xFFFFFFFF; + + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x00000000u); + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x00000000u); + + Misc::swapEndiannessInplace(n1234); + EXPECT_EQ(n1234, 0x04030201u); + Misc::swapEndiannessInplace(n1234); + EXPECT_EQ(n1234, 0x01020304u); + + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFFFFFFu); + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFFFFFFu); +} + +TEST_F(EndiannessTest, test_swap_endianness_inplace8) +{ + uint64_t zero = 0x0000'0000'0000'0000; + uint64_t n1234 = 0x0102'0304'0506'0708; + uint64_t ffff = 0xFFFF'FFFF'FFFF'FFFF; + + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x0000'0000'0000'0000u); + Misc::swapEndiannessInplace(zero); + EXPECT_EQ(zero, 0x0000'0000'0000'0000u); + + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFF'FFFF'FFFF'FFFFu); + Misc::swapEndiannessInplace(ffff); + EXPECT_EQ(ffff, 0xFFFF'FFFF'FFFF'FFFFu); + + Misc::swapEndiannessInplace(n1234); + EXPECT_EQ(n1234, 0x0807'0605'0403'0201u); + Misc::swapEndiannessInplace(n1234); + EXPECT_EQ(n1234, 0x0102'0304'0506'0708u); +} + +TEST_F(EndiannessTest, test_swap_endianness_inplace_float) +{ + const uint32_t original = 0x4023d70a; + const uint32_t expected = 0x0ad72340; + + float number; + memcpy(&number, &original, sizeof(original)); + + Misc::swapEndiannessInplace(number); + + EXPECT_TRUE(!memcmp(&number, &expected, sizeof(expected))); +} + +TEST_F(EndiannessTest, test_swap_endianness_inplace_double) +{ + const uint64_t original = 0x040047ae147ae147ul; + const uint64_t expected = 0x47e17a14ae470004ul; + + double number; + memcpy(&number, &original, sizeof(original)); + + Misc::swapEndiannessInplace(number); + + EXPECT_TRUE(!memcmp(&number, &expected, sizeof(expected))); +} diff --git a/apps/components_tests/misc/test_resourcehelpers.cpp b/apps/components_tests/misc/test_resourcehelpers.cpp new file mode 100644 index 00000000000..05079ae8757 --- /dev/null +++ b/apps/components_tests/misc/test_resourcehelpers.cpp @@ -0,0 +1,72 @@ +#include +#include + +#include + +namespace +{ + using namespace Misc::ResourceHelpers; + TEST(CorrectSoundPath, wav_files_not_overridden_with_mp3_in_vfs_are_not_corrected) + { + constexpr VFS::Path::NormalizedView path("sound/bar.wav"); + std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ { path, nullptr } }); + EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/bar.wav"); + } + + TEST(CorrectSoundPath, wav_files_overridden_with_mp3_in_vfs_are_corrected) + { + constexpr VFS::Path::NormalizedView mp3("sound/foo.mp3"); + std::unique_ptr mVFS = TestingOpenMW::createTestVFS({ { mp3, nullptr } }); + constexpr VFS::Path::NormalizedView wav("sound/foo.wav"); + EXPECT_EQ(correctSoundPath(wav, *mVFS), "sound/foo.mp3"); + } + + TEST(CorrectSoundPath, corrected_path_does_not_check_existence_in_vfs) + { + std::unique_ptr mVFS = TestingOpenMW::createTestVFS({}); + constexpr VFS::Path::NormalizedView path("sound/foo.wav"); + EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/foo.mp3"); + } + + namespace + { + std::string checkChangeExtensionToDds(std::string path) + { + changeExtensionToDds(path); + return path; + } + } + + TEST(ChangeExtensionToDds, original_extension_with_same_size_as_dds) + { + EXPECT_EQ(checkChangeExtensionToDds("texture/bar.tga"), "texture/bar.dds"); + } + + TEST(ChangeExtensionToDds, original_extension_greater_than_dds) + { + EXPECT_EQ(checkChangeExtensionToDds("texture/bar.jpeg"), "texture/bar.dds"); + } + + TEST(ChangeExtensionToDds, original_extension_smaller_than_dds) + { + EXPECT_EQ(checkChangeExtensionToDds("texture/bar.xx"), "texture/bar.dds"); + } + + TEST(ChangeExtensionToDds, does_not_change_dds_extension) + { + std::string path = "texture/bar.dds"; + EXPECT_FALSE(changeExtensionToDds(path)); + } + + TEST(ChangeExtensionToDds, does_not_change_when_no_extension) + { + std::string path = "texture/bar"; + EXPECT_FALSE(changeExtensionToDds(path)); + } + + TEST(ChangeExtensionToDds, change_when_there_is_an_extension) + { + std::string path = "texture/bar.jpeg"; + EXPECT_TRUE(changeExtensionToDds(path)); + } +} diff --git a/apps/components_tests/misc/test_stringops.cpp b/apps/components_tests/misc/test_stringops.cpp new file mode 100644 index 00000000000..2cfe53b1dd0 --- /dev/null +++ b/apps/components_tests/misc/test_stringops.cpp @@ -0,0 +1,235 @@ +#include + +#include +#include +#include + +#include + +#include +#include +#include + +struct PartialBinarySearchTest : public ::testing::Test +{ +protected: + std::vector mDataVec; + void SetUp() override + { + const char* data[] = { "Head", "Chest", "Tri Head", "Tri Chest", "Bip01", "Tri Bip01" }; + mDataVec = std::vector(data, data + sizeof(data) / sizeof(data[0])); + std::sort(mDataVec.begin(), mDataVec.end(), Misc::StringUtils::ciLess); + } + + bool matches(const std::string& keyword) + { + return Misc::partialBinarySearch(mDataVec.begin(), mDataVec.end(), keyword) != mDataVec.end(); + } +}; + +TEST_F(PartialBinarySearchTest, partial_binary_search_test) +{ + EXPECT_TRUE(matches("Head 01")); + EXPECT_TRUE(matches("Head")); + EXPECT_TRUE(matches("Tri Head 01")); + EXPECT_TRUE(matches("Tri Head")); + EXPECT_TRUE(matches("tri head")); + EXPECT_TRUE(matches("Tri bip01")); + EXPECT_TRUE(matches("bip01")); + EXPECT_TRUE(matches("bip01 head")); + EXPECT_TRUE(matches("Bip01 L Hand")); + EXPECT_TRUE(matches("BIp01 r Clavicle")); + EXPECT_TRUE(matches("Bip01 SpiNe1")); + + EXPECT_FALSE(matches("")); + EXPECT_FALSE(matches("Node Bip01")); + EXPECT_FALSE(matches("Hea")); + EXPECT_FALSE(matches(" Head")); + EXPECT_FALSE(matches("Tri Head")); +} + +TEST_F(PartialBinarySearchTest, ci_test) +{ + EXPECT_TRUE(Misc::StringUtils::lowerCase("ASD") == "asd"); + + // test to make sure system locale is not used + std::string unicode1 = "\u04151 \u0418"; // CYRILLIC CAPITAL LETTER IE, CYRILLIC CAPITAL LETTER I + EXPECT_TRUE(Misc::StringUtils::lowerCase(unicode1) == unicode1); +} + +namespace +{ + using namespace ::Misc::StringUtils; + using namespace ::testing; + + template + struct MiscStringUtilsCiEqualEmptyTest : Test + { + }; + + TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualEmptyTest); + + TYPED_TEST_P(MiscStringUtilsCiEqualEmptyTest, empty_strings_should_be_equal) + { + const typename TypeParam::first_type a{}; + const typename TypeParam::second_type b{}; + EXPECT_TRUE(ciEqual(a, b)); + } + + REGISTER_TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualEmptyTest, empty_strings_should_be_equal); + + using EmptyStringTypePairsTypes = Types, + std::pair, std::pair, + std::pair, std::pair, + std::pair, std::pair, + std::pair, std::pair>; + + INSTANTIATE_TYPED_TEST_SUITE_P(EmptyStringTypePairs, MiscStringUtilsCiEqualEmptyTest, EmptyStringTypePairsTypes); + + template + struct MiscStringUtilsCiEqualNotEmptyTest : Test + { + }; + + TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualNotEmptyTest); + + using RawValue = const char[4]; + + constexpr RawValue foo = "f0#"; + constexpr RawValue fooUpper = "F0#"; + constexpr RawValue bar = "bar"; + + template + using Value = std::conditional_t, RawValue&, T>; + + TYPED_TEST_P(MiscStringUtilsCiEqualNotEmptyTest, same_strings_should_be_equal) + { + const Value a{ foo }; + const Value b{ foo }; + EXPECT_TRUE(ciEqual(a, b)) << a << "\n" << b; + } + + TYPED_TEST_P(MiscStringUtilsCiEqualNotEmptyTest, same_strings_with_different_case_sensetivity_should_be_equal) + { + const Value a{ foo }; + const Value b{ fooUpper }; + EXPECT_TRUE(ciEqual(a, b)) << a << "\n" << b; + } + + TYPED_TEST_P(MiscStringUtilsCiEqualNotEmptyTest, different_strings_content_should_not_be_equal) + { + const Value a{ foo }; + const Value b{ bar }; + EXPECT_FALSE(ciEqual(a, b)) << a << "\n" << b; + } + + REGISTER_TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualNotEmptyTest, same_strings_should_be_equal, + same_strings_with_different_case_sensetivity_should_be_equal, different_strings_content_should_not_be_equal); + + using NotEmptyStringTypePairsTypes = Types, + std::pair, std::pair, + std::pair, std::pair, + std::pair, std::pair, + std::pair, std::pair>; + + INSTANTIATE_TYPED_TEST_SUITE_P( + NotEmptyStringTypePairs, MiscStringUtilsCiEqualNotEmptyTest, NotEmptyStringTypePairsTypes); + + TEST(MiscStringUtilsCiEqualTest, string_with_different_length_should_not_be_equal) + { + EXPECT_FALSE(ciEqual(std::string("a"), std::string("aa"))); + } + + TEST(MiscStringsCiStartsWith, empty_string_should_start_with_empty_prefix) + { + EXPECT_TRUE(ciStartsWith(std::string_view(), std::string_view())); + } + + TEST(MiscStringsCiStartsWith, string_should_start_with_empty_prefix) + { + EXPECT_TRUE(ciStartsWith("foo", std::string_view())); + } + + TEST(MiscStringsCiStartsWith, string_should_start_with_own_prefix) + { + std::string string = "some string"; + EXPECT_TRUE(ciStartsWith(string, string.substr(0, 4))); + } + + TEST(MiscStringsCiStartsWith, string_should_start_with_the_same_value) + { + EXPECT_TRUE(ciStartsWith("foo", "foo")); + } + + TEST(MiscStringsCiStartsWith, string_should_not_start_with_not_its_prefix) + { + EXPECT_FALSE(ciStartsWith("some string", "foo")); + } + + TEST(MiscStringsCiStartsWith, string_should_not_start_with_longer_string_having_matching_prefix) + { + EXPECT_FALSE(ciStartsWith("foo", "foo bar")); + } + + TEST(MiscStringsCiStartsWith, should_be_case_insensitive) + { + EXPECT_TRUE(ciStartsWith("foo bar", "FOO")); + } + + TEST(MiscStringsFormat, string_format) + { + std::string f = "1%s2"; + EXPECT_EQ(Misc::StringUtils::format(f, ""), "12"); + } + + TEST(MiscStringsFormat, string_format_arg) + { + std::string arg = "12"; + EXPECT_EQ(Misc::StringUtils::format("1%s2", arg), "1122"); + } + + TEST(MiscStringsFormat, string_view_format_arg) + { + std::string f = "1%s2"; + std::string_view view = "12"; + EXPECT_EQ(Misc::StringUtils::format(f, view), "1122"); + EXPECT_EQ(Misc::StringUtils::format(f, view.substr(0, 1)), "112"); + EXPECT_EQ(Misc::StringUtils::format(f, view.substr(1, 1)), "122"); + EXPECT_EQ(Misc::StringUtils::format(f, view.substr(2)), "12"); + } + + TEST(MiscStringsCiFind, should_return_zero_for_2_empty_strings) + { + EXPECT_EQ(ciFind(std::string_view(), std::string_view()), 0); + } + + TEST(MiscStringsCiFind, should_return_zero_when_looking_for_empty_string) + { + EXPECT_EQ(ciFind("foo", std::string_view()), 0); + } + + TEST(MiscStringsCiFind, should_return_npos_for_longer_substring) + { + EXPECT_EQ(ciFind("a", "aa"), std::string_view::npos); + } + + TEST(MiscStringsCiFind, should_return_zero_for_the_same_string) + { + EXPECT_EQ(ciFind("foo", "foo"), 0); + } + + TEST(MiscStringsCiFind, should_return_first_position_of_substring) + { + EXPECT_EQ(ciFind("foobar foobar", "bar"), 3); + } + + TEST(MiscStringsCiFind, should_be_case_insensitive) + { + EXPECT_EQ(ciFind("foobar", "BAR"), 3); + } + + TEST(MiscStringsCiFind, should_return_npos_for_absent_substring) + { + EXPECT_EQ(ciFind("foobar", "baz"), std::string_view::npos); + } +} diff --git a/apps/components_tests/misc/testmathutil.cpp b/apps/components_tests/misc/testmathutil.cpp new file mode 100644 index 00000000000..c4b545c2f41 --- /dev/null +++ b/apps/components_tests/misc/testmathutil.cpp @@ -0,0 +1,194 @@ +#include + +#include + +#include +#include + +#include +#include + +MATCHER_P2(Vec3fEq, other, precision, "") +{ + return std::abs(arg.x() - other.x()) < precision && std::abs(arg.y() - other.y()) < precision + && std::abs(arg.z() - other.z()) < precision; +} + +namespace testing +{ + template <> + inline testing::Message& Message::operator<<(const osg::Vec3f& value) + { + return (*this) << std::setprecision(std::numeric_limits::max_exponent10) << "osg::Vec3f(" << value.x() + << ", " << value.y() << ", " << value.z() << ')'; + } + + template <> + inline testing::Message& Message::operator<<(const osg::Quat& value) + { + return (*this) << std::setprecision(std::numeric_limits::max_exponent10) << "osg::Quat(" << value.x() + << ", " << value.y() << ", " << value.z() << ", " << value.w() << ')'; + } +} + +namespace Misc +{ + namespace + { + using namespace testing; + + struct MiscToEulerAnglesXZQuatTest : TestWithParam> + { + }; + + TEST_P(MiscToEulerAnglesXZQuatTest, shouldReturnValueCloseTo) + { + const osg::Vec3f result = toEulerAnglesXZ(GetParam().first); + EXPECT_THAT(result, Vec3fEq(GetParam().second, 1e-6)) + << "toEulerAnglesXZ(" << GetParam().first << ") = " << result; + } + + const std::pair eulerAnglesXZQuat[] = { + { + osg::Quat(1, 0, 0, 0), + osg::Vec3f(0, 0, osg::PI), + }, + { + osg::Quat(0, 1, 0, 0), + osg::Vec3f(0, 0, 0), + }, + { + osg::Quat(0, 0, 1, 0), + osg::Vec3f(0, 0, osg::PI), + }, + { + osg::Quat(0, 0, 0, 1), + osg::Vec3f(0, 0, 0), + }, + { + osg::Quat(-0.5, -0.5, -0.5, -0.5), + osg::Vec3f(-osg::PI_2f, 0, 0), + }, + { + osg::Quat(0.5, -0.5, -0.5, -0.5), + osg::Vec3f(0, 0, -osg::PI_2f), + }, + { + osg::Quat(0.5, 0.5, -0.5, -0.5), + osg::Vec3f(osg::PI_2f, 0, 0), + }, + { + osg::Quat(0.5, 0.5, 0.5, -0.5), + osg::Vec3f(0, 0, osg::PI_2f), + }, + { + osg::Quat(0.5, 0.5, 0.5, 0.5), + osg::Vec3f(-osg::PI_2f, 0, 0), + }, + { + // normalized osg::Quat(0.1, 0.2, 0.3, 0.4) + osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(-0.72972762584686279296875f, 0, -1.10714876651763916015625f), + }, + { + osg::Quat(-0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(-0.13373161852359771728515625f, 0, -1.2277724742889404296875f), + }, + { + osg::Quat(0.18257418583505536, -0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(0.13373161852359771728515625f, 0, -1.2277724742889404296875f), + }, + { + osg::Quat(0.18257418583505536, 0.36514837167011072, -0.54772255750516607, 0.73029674334022143), + osg::Vec3f(0.13373161852359771728515625f, 0, 1.2277724742889404296875f), + }, + { + osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, -0.73029674334022143), + osg::Vec3f(-0.13373161852359771728515625, 0, 1.2277724742889404296875f), + }, + { + osg::Quat(0.246736, -0.662657, -0.662667, 0.246739), + osg::Vec3f(-osg::PI_2f, 0, 2.5199801921844482421875f), + }, + }; + + INSTANTIATE_TEST_SUITE_P(FromQuat, MiscToEulerAnglesXZQuatTest, ValuesIn(eulerAnglesXZQuat)); + + struct MiscToEulerAnglesZYXQuatTest : TestWithParam> + { + }; + + TEST_P(MiscToEulerAnglesZYXQuatTest, shouldReturnValueCloseTo) + { + const osg::Vec3f result = toEulerAnglesZYX(GetParam().first); + EXPECT_THAT(result, Vec3fEq(GetParam().second, std::numeric_limits::epsilon())) + << "toEulerAnglesZYX(" << GetParam().first << ") = " << result; + } + + const std::pair eulerAnglesZYXQuat[] = { + { + osg::Quat(1, 0, 0, 0), + osg::Vec3f(osg::PI, 0, 0), + }, + { + osg::Quat(0, 1, 0, 0), + osg::Vec3f(osg::PI, 0, osg::PI), + }, + { + osg::Quat(0, 0, 1, 0), + osg::Vec3f(0, 0, osg::PI), + }, + { + osg::Quat(0, 0, 0, 1), + osg::Vec3f(0, 0, 0), + }, + { + osg::Quat(-0.5, -0.5, -0.5, -0.5), + osg::Vec3f(0, -osg::PI_2f, -osg::PI_2f), + }, + { + osg::Quat(0.5, -0.5, -0.5, -0.5), + osg::Vec3f(osg::PI_2f, 0, -osg::PI_2f), + }, + { + osg::Quat(0.5, 0.5, -0.5, -0.5), + osg::Vec3f(0, osg::PI_2f, -osg::PI_2f), + }, + { + osg::Quat(0.5, 0.5, 0.5, -0.5), + osg::Vec3f(osg::PI_2f, 0, osg::PI_2f), + }, + { + osg::Quat(0.5, 0.5, 0.5, 0.5), + osg::Vec3f(0, -osg::PI_2f, -osg::PI_2f), + }, + { + // normalized osg::Quat(0.1, 0.2, 0.3, 0.4) + osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(0.1973955929279327392578125f, -0.8232119083404541015625f, -1.37340080738067626953125f), + }, + { + osg::Quat(-0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(0.78539812564849853515625f, -0.339836895465850830078125f, -1.428899288177490234375f), + }, + { + osg::Quat(0.18257418583505536, -0.36514837167011072, 0.54772255750516607, 0.73029674334022143), + osg::Vec3f(-0.78539812564849853515625f, 0.339836895465850830078125f, -1.428899288177490234375f), + }, + { + osg::Quat(0.18257418583505536, 0.36514837167011072, -0.54772255750516607, 0.73029674334022143), + osg::Vec3f(-0.78539812564849853515625f, -0.339836895465850830078125f, 1.428899288177490234375f), + }, + { + osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, -0.73029674334022143), + osg::Vec3f(0.78539812564849853515625f, 0.339836895465850830078125f, 1.428899288177490234375f), + }, + { + osg::Quat(0.246736, -0.662657, 0.246739, -0.662667), + osg::Vec3f(0.06586204469203948974609375f, -osg::PI_2f, 0.64701664447784423828125f), + }, + }; + + INSTANTIATE_TEST_SUITE_P(FromQuat, MiscToEulerAnglesZYXQuatTest, ValuesIn(eulerAnglesZYXQuat)); + } +} diff --git a/apps/components_tests/nif/node.hpp b/apps/components_tests/nif/node.hpp new file mode 100644 index 00000000000..4e216985013 --- /dev/null +++ b/apps/components_tests/nif/node.hpp @@ -0,0 +1,71 @@ +#ifndef OPENMW_TEST_SUITE_NIF_NODE_H +#define OPENMW_TEST_SUITE_NIF_NODE_H + +#include +#include + +namespace Nif::Testing +{ + inline void init(NiTransform& value) + { + value = NiTransform::getIdentity(); + } + + inline void init(Extra& value) + { + value.mNext = ExtraPtr(nullptr); + } + + inline void init(NiObjectNET& value) + { + value.mExtra = ExtraPtr(nullptr); + value.mExtraList = ExtraList(); + value.mController = NiTimeControllerPtr(nullptr); + } + + inline void init(NiAVObject& value) + { + init(static_cast(value)); + value.mFlags = 0; + init(value.mTransform); + } + + inline void init(NiGeometry& value) + { + init(static_cast(value)); + value.mData = NiGeometryDataPtr(nullptr); + value.mSkin = NiSkinInstancePtr(nullptr); + } + + inline void init(NiTriShape& value) + { + init(static_cast(value)); + value.recType = RC_NiTriShape; + } + + inline void init(NiTriStrips& value) + { + init(static_cast(value)); + value.recType = RC_NiTriStrips; + } + + inline void init(NiSkinInstance& value) + { + value.mData = NiSkinDataPtr(nullptr); + value.mRoot = NiAVObjectPtr(nullptr); + value.mPartitions = NiSkinPartitionPtr(nullptr); + } + + inline void init(NiTimeController& value) + { + value.mNext = NiTimeControllerPtr(nullptr); + value.mFlags = 0; + value.mFrequency = 0; + value.mPhase = 0; + value.mTimeStart = 0; + value.mTimeStop = 0; + value.mTarget = NiObjectNETPtr(nullptr); + } +} + +#endif diff --git a/apps/components_tests/nifloader/testbulletnifloader.cpp b/apps/components_tests/nifloader/testbulletnifloader.cpp new file mode 100644 index 00000000000..f7ee559578f --- /dev/null +++ b/apps/components_tests/nifloader/testbulletnifloader.cpp @@ -0,0 +1,1391 @@ +#include "../nif/node.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace +{ + template + bool compareObjects(const T* lhs, const T* rhs) + { + return (!lhs && !rhs) || (lhs && rhs && *lhs == *rhs); + } + + std::vector getTriangles(const btBvhTriangleMeshShape& shape) + { + std::vector result; + auto callback = BulletHelpers::makeProcessTriangleCallback([&](btVector3* triangle, int, int) { + for (std::size_t i = 0; i < 3; ++i) + result.push_back(triangle[i]); + }); + btVector3 aabbMin; + btVector3 aabbMax; + shape.getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + shape.processAllTriangles(&callback, aabbMin, aabbMax); + return result; + } + + bool isNear(btScalar lhs, btScalar rhs) + { + return std::abs(lhs - rhs) <= 1e-5; + } + + bool isNear(const btVector3& lhs, const btVector3& rhs) + { + return std::equal(static_cast(lhs), static_cast(lhs) + 3, + static_cast(rhs), [](btScalar lhs, btScalar rhs) { return isNear(lhs, rhs); }); + } + + bool isNear(const btMatrix3x3& lhs, const btMatrix3x3& rhs) + { + for (int i = 0; i < 3; ++i) + if (!isNear(lhs[i], rhs[i])) + return false; + return true; + } + + bool isNear(const btTransform& lhs, const btTransform& rhs) + { + return isNear(lhs.getOrigin(), rhs.getOrigin()) && isNear(lhs.getBasis(), rhs.getBasis()); + } + + bool isNear(std::span lhs, std::span rhs) + { + if (lhs.size() != rhs.size()) + return false; + return std::equal( + lhs.begin(), lhs.end(), rhs.begin(), [](const btVector3& l, const btVector3& r) { return isNear(l, r); }); + } + + struct WriteVec3f + { + osg::Vec3f mValue; + + friend std::ostream& operator<<(std::ostream& stream, const WriteVec3f& value) + { + return stream << "osg::Vec3f {" << std::setprecision(std::numeric_limits::max_exponent10) + << value.mValue.x() << ", " << std::setprecision(std::numeric_limits::max_exponent10) + << value.mValue.y() << ", " << std::setprecision(std::numeric_limits::max_exponent10) + << value.mValue.z() << "}"; + } + }; +} + +static std::ostream& operator<<(std::ostream& stream, const btVector3& value) +{ + return stream << "btVector3 {" << std::setprecision(std::numeric_limits::max_exponent10) << value.getX() + << ", " << std::setprecision(std::numeric_limits::max_exponent10) << value.getY() << ", " + << std::setprecision(std::numeric_limits::max_exponent10) << value.getZ() << "}"; +} + +static std::ostream& operator<<(std::ostream& stream, const btMatrix3x3& value) +{ + stream << "btMatrix3x3 {"; + for (int i = 0; i < 3; ++i) + stream << value.getRow(i) << ", "; + return stream << "}"; +} + +static std::ostream& operator<<(std::ostream& stream, const btTransform& value) +{ + return stream << "btTransform {" << value.getBasis() << ", " << value.getOrigin() << "}"; +} + +static std::ostream& operator<<(std::ostream& stream, const btCollisionShape* value); + +static std::ostream& operator<<(std::ostream& stream, const btCompoundShape& value) +{ + stream << "btCompoundShape {" << value.getLocalScaling() << ", "; + stream << "{"; + for (int i = 0; i < value.getNumChildShapes(); ++i) + stream << value.getChildShape(i) << ", "; + stream << "},"; + stream << "{"; + for (int i = 0; i < value.getNumChildShapes(); ++i) + stream << value.getChildTransform(i) << ", "; + stream << "}"; + return stream << "}"; +} + +static std::ostream& operator<<(std::ostream& stream, const btBoxShape& value) +{ + return stream << "btBoxShape {" << value.getLocalScaling() << ", " << value.getHalfExtentsWithoutMargin() << "}"; +} + +namespace Resource +{ + + static std::ostream& operator<<(std::ostream& stream, const TriangleMeshShape& value) + { + stream << "Resource::TriangleMeshShape {" << value.getLocalScaling() << ", " + << value.usesQuantizedAabbCompression() << ", " << value.getOwnsBvh() << ", {"; + auto callback = BulletHelpers::makeProcessTriangleCallback([&](btVector3* triangle, int, int) { + for (std::size_t i = 0; i < 3; ++i) + stream << triangle[i] << ", "; + }); + btVector3 aabbMin; + btVector3 aabbMax; + value.getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + value.processAllTriangles(&callback, aabbMin, aabbMax); + return stream << "}}"; + } + + static std::ostream& operator<<(std::ostream& stream, const ScaledTriangleMeshShape& value) + { + return stream << "Resource::ScaledTriangleMeshShape {" << value.getLocalScaling() << ", " + << value.getChildShape() << "}"; + } + + static bool operator==(const CollisionBox& l, const CollisionBox& r) + { + const auto tie = [](const CollisionBox& v) { return std::tie(v.mExtents, v.mCenter); }; + return tie(l) == tie(r); + } + + static std::ostream& operator<<(std::ostream& stream, const CollisionBox& value) + { + return stream << "CollisionBox {" << WriteVec3f{ value.mExtents } << ", " << WriteVec3f{ value.mCenter } << "}"; + } + +} + +static std::ostream& operator<<(std::ostream& stream, const btCollisionShape& value) +{ + switch (value.getShapeType()) + { + case COMPOUND_SHAPE_PROXYTYPE: + return stream << static_cast(value); + case BOX_SHAPE_PROXYTYPE: + return stream << static_cast(value); + case TRIANGLE_MESH_SHAPE_PROXYTYPE: + if (const auto casted = dynamic_cast(&value)) + return stream << *casted; + break; + case SCALED_TRIANGLE_MESH_SHAPE_PROXYTYPE: + if (const auto casted = dynamic_cast(&value)) + return stream << *casted; + break; + } + return stream << "btCollisionShape {" << value.getShapeType() << "}"; +} + +static std::ostream& operator<<(std::ostream& stream, const btCollisionShape* value) +{ + return value ? stream << "&" << *value : stream << "nullptr"; +} + +namespace std +{ + static std::ostream& operator<<(std::ostream& stream, const map& value) + { + stream << "std::map {"; + for (const auto& v : value) + stream << "{" << v.first << ", " << v.second << "},"; + return stream << "}"; + } +} + +namespace Resource +{ + static bool operator==(const Resource::BulletShape& lhs, const Resource::BulletShape& rhs) + { + return compareObjects(lhs.mCollisionShape.get(), rhs.mCollisionShape.get()) + && compareObjects(lhs.mAvoidCollisionShape.get(), rhs.mAvoidCollisionShape.get()) + && lhs.mCollisionBox == rhs.mCollisionBox && lhs.mVisualCollisionType == rhs.mVisualCollisionType + && lhs.mAnimatedShapes == rhs.mAnimatedShapes; + } + + static std::ostream& operator<<(std::ostream& stream, Resource::VisualCollisionType value) + { + switch (value) + { + case Resource::VisualCollisionType::None: + return stream << "Resource::VisualCollisionType::None"; + case Resource::VisualCollisionType::Default: + return stream << "Resource::VisualCollisionType::Default"; + case Resource::VisualCollisionType::Camera: + return stream << "Resource::VisualCollisionType::Camera"; + } + return stream << static_cast>(value); + } + + static std::ostream& operator<<(std::ostream& stream, const Resource::BulletShape& value) + { + return stream << "Resource::BulletShape {" << value.mCollisionShape.get() << ", " + << value.mAvoidCollisionShape.get() << ", " << value.mCollisionBox << ", " + << value.mAnimatedShapes << ", " << value.mVisualCollisionType << "}"; + } +} + +static bool operator==(const btCollisionShape& lhs, const btCollisionShape& rhs); + +static bool operator==(const btCompoundShape& lhs, const btCompoundShape& rhs) +{ + if (lhs.getNumChildShapes() != rhs.getNumChildShapes() || lhs.getLocalScaling() != rhs.getLocalScaling()) + return false; + for (int i = 0; i < lhs.getNumChildShapes(); ++i) + { + if (!compareObjects(lhs.getChildShape(i), rhs.getChildShape(i)) + || !isNear(lhs.getChildTransform(i), rhs.getChildTransform(i))) + return false; + } + return true; +} + +static bool operator==(const btBoxShape& lhs, const btBoxShape& rhs) +{ + return isNear(lhs.getLocalScaling(), rhs.getLocalScaling()) + && lhs.getHalfExtentsWithoutMargin() == rhs.getHalfExtentsWithoutMargin(); +} + +static bool operator==(const btBvhTriangleMeshShape& lhs, const btBvhTriangleMeshShape& rhs) +{ + return isNear(lhs.getLocalScaling(), rhs.getLocalScaling()) + && lhs.usesQuantizedAabbCompression() == rhs.usesQuantizedAabbCompression() + && lhs.getOwnsBvh() == rhs.getOwnsBvh() && isNear(getTriangles(lhs), getTriangles(rhs)); +} + +static bool operator==(const btScaledBvhTriangleMeshShape& lhs, const btScaledBvhTriangleMeshShape& rhs) +{ + return isNear(lhs.getLocalScaling(), rhs.getLocalScaling()) + && compareObjects(lhs.getChildShape(), rhs.getChildShape()); +} + +static bool operator==(const btCollisionShape& lhs, const btCollisionShape& rhs) +{ + if (lhs.getShapeType() != rhs.getShapeType()) + return false; + switch (lhs.getShapeType()) + { + case COMPOUND_SHAPE_PROXYTYPE: + return static_cast(lhs) == static_cast(rhs); + case BOX_SHAPE_PROXYTYPE: + return static_cast(lhs) == static_cast(rhs); + case TRIANGLE_MESH_SHAPE_PROXYTYPE: + if (const auto lhsCasted = dynamic_cast(&lhs)) + if (const auto rhsCasted = dynamic_cast(&rhs)) + return *lhsCasted == *rhsCasted; + return false; + case SCALED_TRIANGLE_MESH_SHAPE_PROXYTYPE: + if (const auto lhsCasted = dynamic_cast(&lhs)) + if (const auto rhsCasted = dynamic_cast(&rhs)) + return *lhsCasted == *rhsCasted; + return false; + } + return false; +} + +namespace +{ + using namespace testing; + using namespace Nif::Testing; + using NifBullet::BulletNifLoader; + + void copy(const btTransform& src, Nif::NiTransform& dst) + { + dst.mTranslation = osg::Vec3f(src.getOrigin().x(), src.getOrigin().y(), src.getOrigin().z()); + for (int row = 0; row < 3; ++row) + for (int column = 0; column < 3; ++column) + dst.mRotation.mValues[row][column] = src.getBasis().getRow(row)[column]; + } + + struct TestBulletNifLoader : Test + { + BulletNifLoader mLoader; + Nif::NiAVObject mNode; + Nif::NiAVObject mNode2; + Nif::NiNode mNiNode; + Nif::NiNode mNiNode2; + Nif::NiNode mNiNode3; + Nif::NiTriShapeData mNiTriShapeData; + Nif::NiTriShape mNiTriShape; + Nif::NiTriShapeData mNiTriShapeData2; + Nif::NiTriShape mNiTriShape2; + Nif::NiTriStripsData mNiTriStripsData; + Nif::NiTriStrips mNiTriStrips; + Nif::NiSkinInstance mNiSkinInstance; + Nif::NiStringExtraData mNiStringExtraData; + Nif::NiStringExtraData mNiStringExtraData2; + Nif::NiIntegerExtraData mNiIntegerExtraData; + Nif::NiTimeController mController; + btTransform mTransform{ btMatrix3x3(btQuaternion(btVector3(1, 0, 0), 0.5f)), btVector3(1, 2, 3) }; + btTransform mTransformScale2{ btMatrix3x3(btQuaternion(btVector3(1, 0, 0), 0.5f)), btVector3(2, 4, 6) }; + btTransform mTransformScale3{ btMatrix3x3(btQuaternion(btVector3(1, 0, 0), 0.5f)), btVector3(3, 6, 9) }; + btTransform mTransformScale4{ btMatrix3x3(btQuaternion(btVector3(1, 0, 0), 0.5f)), btVector3(4, 8, 12) }; + const std::string mHash = "hash"; + + TestBulletNifLoader() + { + init(mNode); + init(mNode2); + init(mNiNode); + init(mNiNode2); + init(mNiNode3); + init(mNiTriShape); + init(mNiTriShape2); + init(mNiTriStrips); + init(mNiSkinInstance); + init(mNiStringExtraData); + init(mNiStringExtraData2); + init(mController); + + mNiTriShapeData.recType = Nif::RC_NiTriShapeData; + mNiTriShapeData.mVertices = { osg::Vec3f(0, 0, 0), osg::Vec3f(1, 0, 0), osg::Vec3f(1, 1, 0) }; + mNiTriShapeData.mNumTriangles = 1; + mNiTriShapeData.mTriangles = { 0, 1, 2 }; + mNiTriShape.mData = Nif::NiGeometryDataPtr(&mNiTriShapeData); + + mNiTriShapeData2.recType = Nif::RC_NiTriShapeData; + mNiTriShapeData2.mVertices = { osg::Vec3f(0, 0, 1), osg::Vec3f(1, 0, 1), osg::Vec3f(1, 1, 1) }; + mNiTriShapeData2.mNumTriangles = 1; + mNiTriShapeData2.mTriangles = { 0, 1, 2 }; + mNiTriShape2.mData = Nif::NiGeometryDataPtr(&mNiTriShapeData2); + + mNiTriStripsData.recType = Nif::RC_NiTriStripsData; + mNiTriStripsData.mVertices + = { osg::Vec3f(0, 0, 0), osg::Vec3f(1, 0, 0), osg::Vec3f(1, 1, 0), osg::Vec3f(0, 1, 0) }; + mNiTriStripsData.mNumTriangles = 2; + mNiTriStripsData.mStrips = { { 0, 1, 2, 3 } }; + mNiTriStrips.mData = Nif::NiGeometryDataPtr(&mNiTriStripsData); + } + }; + + TEST_F(TestBulletNifLoader, for_zero_num_roots_should_return_default) + { + Nif::NIFFile file("test.nif"); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + EXPECT_EQ(result->mFileName, "test.nif"); + EXPECT_EQ(result->mFileHash, mHash); + } + + TEST_F(TestBulletNifLoader, should_ignore_nullptr_root) + { + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(nullptr); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_default_root_nif_node_should_return_default) + { + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_default_root_collision_node_nif_node_should_return_default) + { + mNode.recType = Nif::RC_RootCollisionNode; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_default_root_nif_node_and_filename_starting_with_x_should_return_default) + { + Nif::NIFFile file("xtest.nif"); + file.mRoots.push_back(&mNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_root_bounding_box_should_return_shape_with_bounding_box_data) + { + mNode.mName = "Bounding Box"; + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_child_bounding_box_should_return_shape_with_bounding_box_data) + { + mNode.mName = "Bounding Box"; + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + mNode.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNode) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_root_with_bounds_and_child_bounding_box_should_use_bounding_box) + { + mNode.mName = "Bounding Box"; + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + mNode.mParents.push_back(&mNiNode); + + mNiNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNiNode.mBounds.mBox.mExtents = osg::Vec3f(4, 5, 6); + mNiNode.mBounds.mBox.mCenter = osg::Vec3f(-4, -5, -6); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNode) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); + + EXPECT_EQ(*result, expected); + } + + TEST_F( + TestBulletNifLoader, for_root_and_two_children_where_both_with_bounds_but_one_is_bounding_box_use_bounding_box) + { + mNode.mName = "Bounding Box"; + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + mNode.mParents.push_back(&mNiNode); + + mNode2.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode2.mBounds.mBox.mExtents = osg::Vec3f(4, 5, 6); + mNode2.mBounds.mBox.mCenter = osg::Vec3f(-4, -5, -6); + mNode2.mParents.push_back(&mNiNode); + + mNiNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNiNode.mBounds.mBox.mExtents = osg::Vec3f(7, 8, 9); + mNiNode.mBounds.mBox.mCenter = osg::Vec3f(-7, -8, -9); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNode), Nif::NiAVObjectPtr(&mNode2) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, + for_root_and_two_children_where_both_with_bounds_but_second_is_bounding_box_use_bounding_box) + { + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + mNode.mParents.push_back(&mNiNode); + + mNode2.mName = "Bounding Box"; + mNode2.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode2.mBounds.mBox.mExtents = osg::Vec3f(4, 5, 6); + mNode2.mBounds.mBox.mCenter = osg::Vec3f(-4, -5, -6); + mNode2.mParents.push_back(&mNiNode); + + mNiNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNiNode.mBounds.mBox.mExtents = osg::Vec3f(7, 8, 9); + mNiNode.mBounds.mBox.mCenter = osg::Vec3f(-7, -8, -9); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNode), Nif::NiAVObjectPtr(&mNode2) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + expected.mCollisionBox.mExtents = osg::Vec3f(4, 5, 6); + expected.mCollisionBox.mCenter = osg::Vec3f(-4, -5, -6); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_root_nif_node_with_bounds_should_return_shape_with_null_collision_shape) + { + mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNode.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNode.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_shape_root_node_should_return_static_shape) + { + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiTriShape); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + std::unique_ptr compound(new btCompoundShape); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_shape_root_node_with_bounds_should_return_static_shape) + { + mNiTriShape.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNiTriShape.mBounds.mBox.mExtents = osg::Vec3f(1, 2, 3); + mNiTriShape.mBounds.mBox.mCenter = osg::Vec3f(-1, -2, -3); + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiTriShape); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + std::unique_ptr compound(new btCompoundShape); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_shape_child_node_should_return_static_shape) + { + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + std::unique_ptr compound(new btCompoundShape); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_nested_tri_shape_child_should_return_static_shape) + { + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiNode2) }; + mNiNode2.mParents.push_back(&mNiNode); + mNiNode2.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiTriShape.mParents.push_back(&mNiNode2); + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + std::unique_ptr compound(new btCompoundShape); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_two_tri_shape_children_should_return_static_shape_with_all_meshes) + { + mNiTriShape.mParents.push_back(&mNiNode); + mNiTriShape2.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape), Nif::NiAVObjectPtr(&mNiTriShape2) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr triangles2(new btTriangleMesh(false)); + triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); + std::unique_ptr compound(new btCompoundShape); + auto triShape = std::make_unique(triangles.release(), true); + auto triShape2 = std::make_unique(triangles2.release(), true); + + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape2.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, + for_tri_shape_child_node_and_filename_starting_with_x_and_not_empty_skin_should_return_static_shape) + { + mNiTriShape.mSkin = Nif::NiSkinInstancePtr(&mNiSkinInstance); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("xtest.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr compound(new btCompoundShape); + auto triShape = std::make_unique(triangles.release(), true); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(triShape.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_shape_root_node_and_filename_starting_with_x_should_return_animated_shape) + { + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 3; + + Nif::NIFFile file("xtest.nif"); + file.mRoots.push_back(&mNiTriShape); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr shape(new btCompoundShape); + shape->addChildShape(mTransform, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(3, 3, 3))); + Resource::BulletShape expected; + expected.mCollisionShape.reset(shape.release()); + expected.mAnimatedShapes = { { -1, 0 } }; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_shape_child_node_and_filename_starting_with_x_should_return_animated_shape) + { + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 3; + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiNode.mTransform.mScale = 4; + + Nif::NIFFile file("xtest.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr shape(new btCompoundShape); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(12, 12, 12))); + Resource::BulletShape expected; + expected.mCollisionShape.reset(shape.release()); + expected.mAnimatedShapes = { { -1, 0 } }; + + EXPECT_EQ(*result, expected); + } + + TEST_F( + TestBulletNifLoader, for_two_tri_shape_children_nodes_and_filename_starting_with_x_should_return_animated_shape) + { + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 3; + mNiTriShape.mParents.push_back(&mNiNode); + + copy(mTransform, mNiTriShape2.mTransform); + mNiTriShape2.mTransform.mScale = 3; + mNiTriShape2.mParents.push_back(&mNiNode); + + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape), Nif::NiAVObjectPtr(&mNiTriShape2) }; + + Nif::NIFFile file("xtest.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + + std::unique_ptr triangles2(new btTriangleMesh(false)); + triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); + std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); + + std::unique_ptr shape(new btCompoundShape); + shape->addChildShape(mTransform, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(3, 3, 3))); + shape->addChildShape(mTransform, new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(3, 3, 3))); + Resource::BulletShape expected; + expected.mCollisionShape.reset(shape.release()); + expected.mAnimatedShapes = { { -1, 0 } }; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_controller_should_return_animated_shape) + { + mController.recType = Nif::RC_NiKeyframeController; + mController.mFlags |= Nif::NiTimeController::Flag_Active; + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 3; + mNiTriShape.mParents.push_back(&mNiNode); + mNiTriShape.mController = Nif::NiTimeControllerPtr(&mController); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiNode.mTransform.mScale = 4; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr shape(new btCompoundShape); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(12, 12, 12))); + Resource::BulletShape expected; + expected.mCollisionShape.reset(shape.release()); + expected.mAnimatedShapes = { { -1, 0 } }; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_two_tri_shape_children_nodes_where_one_with_controller_should_return_animated_shape) + { + mController.recType = Nif::RC_NiKeyframeController; + mController.mFlags |= Nif::NiTimeController::Flag_Active; + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 3; + mNiTriShape.mParents.push_back(&mNiNode); + copy(mTransform, mNiTriShape2.mTransform); + mNiTriShape2.mTransform.mScale = 3; + mNiTriShape2.mParents.push_back(&mNiNode); + mNiTriShape2.mController = Nif::NiTimeControllerPtr(&mController); + mNiNode.mChildren = Nif::NiAVObjectList{ + Nif::NiAVObjectPtr(&mNiTriShape), + Nif::NiAVObjectPtr(&mNiTriShape2), + }; + mNiNode.mTransform.mScale = 4; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + + std::unique_ptr triangles2(new btTriangleMesh(false)); + triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); + std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); + + std::unique_ptr shape(new btCompoundShape); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(12, 12, 12))); + shape->addChildShape( + mTransformScale4, new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(12, 12, 12))); + Resource::BulletShape expected; + expected.mCollisionShape.reset(shape.release()); + expected.mAnimatedShapes = { { -1, 1 } }; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_add_static_mesh_to_existing_compound_mesh) + { + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("xtest.nif"); + file.mRoots.push_back(&mNiNode); + file.mRoots.push_back(&mNiTriShape2); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + + std::unique_ptr triangles2(new btTriangleMesh(false)); + triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); + std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); + + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mAnimatedShapes = { { -1, 0 } }; + + EXPECT_EQ(*result, expected); + } + + TEST_F( + TestBulletNifLoader, for_root_avoid_node_and_tri_shape_child_node_should_return_shape_with_null_collision_shape) + { + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiNode.recType = Nif::RC_AvoidNode; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + Resource::BulletShape expected; + expected.mAvoidCollisionShape.reset(compound.release()); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_empty_data_should_return_shape_with_null_collision_shape) + { + mNiTriShape.mData = Nif::NiGeometryDataPtr(nullptr); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, + for_tri_shape_child_node_with_empty_data_triangles_should_return_shape_with_null_collision_shape) + { + auto data = static_cast(mNiTriShape.mData.getPtr()); + data->mTriangles.clear(); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, + for_root_node_with_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) + { + mNiStringExtraData.mData = "NCC__"; + mNiStringExtraData.recType = Nif::RC_NiStringExtraData; + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + + expected.mVisualCollisionType = Resource::VisualCollisionType::Camera; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, + for_root_node_with_not_first_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision) + { + mNiStringExtraData.mNext = Nif::ExtraPtr(&mNiStringExtraData2); + mNiStringExtraData2.mData = "NCC__"; + mNiStringExtraData2.recType = Nif::RC_NiStringExtraData; + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mVisualCollisionType = Resource::VisualCollisionType::Camera; + + EXPECT_EQ(*result, expected); + } + + TEST_F( + TestBulletNifLoader, for_root_node_with_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) + { + mNiStringExtraData.mData = "NC___"; + mNiStringExtraData.recType = Nif::RC_NiStringExtraData; + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mVisualCollisionType = Resource::VisualCollisionType::Default; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, + for_root_node_with_not_first_extra_data_string_starting_with_nc_should_return_shape_with_nocollision) + { + mNiStringExtraData.mNext = Nif::ExtraPtr(&mNiStringExtraData2); + mNiStringExtraData2.mData = "NC___"; + mNiStringExtraData2.recType = Nif::RC_NiStringExtraData; + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mVisualCollisionType = Resource::VisualCollisionType::Default; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_extra_data_string_should_ignore_extra_data) + { + mNiStringExtraData.mData = "NC___"; + mNiStringExtraData.recType = Nif::RC_NiStringExtraData; + mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_empty_root_collision_node_without_nc_should_return_shape_with_cameraonly_collision) + { + Nif::NiTriShape niTriShape; + Nif::NiNode emptyCollisionNode; + init(niTriShape); + init(emptyCollisionNode); + + niTriShape.mData = Nif::NiGeometryDataPtr(&mNiTriShapeData); + niTriShape.mParents.push_back(&mNiNode); + + emptyCollisionNode.recType = Nif::RC_RootCollisionNode; + emptyCollisionNode.mParents.push_back(&mNiNode); + + mNiNode.mChildren + = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&niTriShape), Nif::NiAVObjectPtr(&emptyCollisionNode) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mVisualCollisionType = Resource::VisualCollisionType::Camera; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, bsx_editor_marker_flag_disables_collision_for_markers) + { + mNiTriShape.mParents.push_back(&mNiNode); + mNiTriShape.mName = "EditorMarker"; + mNiIntegerExtraData.mData = 34; // BSXFlags "has collision" | "editor marker" + mNiIntegerExtraData.recType = Nif::RC_BSXFlags; + mNiNode.mExtraList.push_back(Nif::ExtraPtr(&mNiIntegerExtraData)); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + file.mVersion = Nif::NIFStream::generateVersion(10, 0, 1, 0); + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, mrk_editor_marker_flag_disables_collision_for_markers) + { + mNiTriShape.mParents.push_back(&mNiNode); + mNiTriShape.mName = "Tri EditorMarker"; + mNiStringExtraData.mData = "MRK"; + mNiStringExtraData.recType = Nif::RC_NiStringExtraData; + mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_strips_root_node_should_return_static_shape) + { + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiTriStrips); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + triangles->addTriangle(btVector3(1, 0, 0), btVector3(0, 1, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_strips_data_with_empty_strips) + { + mNiTriStripsData.mStrips.clear(); + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiTriStrips); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_static_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriStripsData.mStrips.front() = { 0, 1 }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiTriStrips); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_avoid_collision_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiNode.recType = Nif::RC_AvoidNode; + mNiTriStripsData.mStrips.front() = { 0, 1 }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiTriStrips); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_animated_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriStripsData.mStrips.front() = { 0, 1 }; + mNiTriStrips.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriStrips) }; + + Nif::NIFFile file("xtest.nif"); + file.mRoots.push_back(&mNiNode); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_not_add_static_mesh_with_no_triangles_to_compound_shape) + { + mNiTriStripsData.mStrips.front() = { 0, 1 }; + mNiTriShape.mParents.push_back(&mNiNode); + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + + Nif::NIFFile file("xtest.nif"); + file.mRoots.push_back(&mNiNode); + file.mRoots.push_back(&mNiTriStrips); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape( + btTransform::getIdentity(), new Resource::ScaledTriangleMeshShape(mesh.release(), btVector3(1, 1, 1))); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mAnimatedShapes = { { -1, 0 } }; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_handle_node_with_multiple_parents) + { + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 4; + mNiTriShape.mParents = { &mNiNode, &mNiNode2 }; + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiNode.mTransform.mScale = 2; + mNiNode2.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) }; + mNiNode2.mTransform.mScale = 3; + + Nif::NIFFile file("xtest.nif"); + file.mRoots.push_back(&mNiNode); + file.mRoots.push_back(&mNiNode2); + file.mHash = mHash; + + const auto result = mLoader.load(file); + + std::unique_ptr triangles1(new btTriangleMesh(false)); + triangles1->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh1(new Resource::TriangleMeshShape(triangles1.release(), true)); + std::unique_ptr triangles2(new btTriangleMesh(false)); + triangles2->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + std::unique_ptr mesh2(new Resource::TriangleMeshShape(triangles2.release(), true)); + std::unique_ptr shape(new btCompoundShape); + shape->addChildShape( + mTransformScale2, new Resource::ScaledTriangleMeshShape(mesh1.release(), btVector3(8, 8, 8))); + shape->addChildShape( + mTransformScale3, new Resource::ScaledTriangleMeshShape(mesh2.release(), btVector3(12, 12, 12))); + Resource::BulletShape expected; + expected.mCollisionShape.reset(shape.release()); + expected.mAnimatedShapes = { { -1, 0 } }; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, dont_assign_invalid_bounding_box_extents) + { + copy(mTransform, mNiTriShape.mTransform); + mNiTriShape.mTransform.mScale = 10; + mNiTriShape.mParents.push_back(&mNiNode); + + mNiTriShape2.mName = "Bounding Box"; + mNiTriShape2.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV; + mNiTriShape2.mBounds.mBox.mExtents = osg::Vec3f(-1, -2, -3); + mNiTriShape2.mParents.push_back(&mNiNode); + + mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape), Nif::NiAVObjectPtr(&mNiTriShape2) }; + + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&mNiNode); + + const auto result = mLoader.load(file); + + const bool extentsUnassigned + = std::ranges::all_of(result->mCollisionBox.mExtents._v, [](float extent) { return extent == 0.f; }); + + EXPECT_EQ(extentsUnassigned, true); + } +} diff --git a/apps/components_tests/nifosg/testnifloader.cpp b/apps/components_tests/nifosg/testnifloader.cpp new file mode 100644 index 00000000000..fa023fff0d5 --- /dev/null +++ b/apps/components_tests/nifosg/testnifloader.cpp @@ -0,0 +1,300 @@ +#include "../nif/node.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace NifOsg; + using namespace Nif::Testing; + + struct BaseNifOsgLoaderTest + { + VFS::Manager mVfs; + Resource::ImageManager mImageManager{ &mVfs, 0 }; + Resource::BgsmFileManager mMaterialManager{ &mVfs, 0 }; + const osgDB::ReaderWriter* mReaderWriter = osgDB::Registry::instance()->getReaderWriterForExtension("osgt"); + osg::ref_ptr mOptions = new osgDB::Options; + + BaseNifOsgLoaderTest() + { + SceneUtil::registerSerializers(); + + if (mReaderWriter == nullptr) + throw std::runtime_error("osgt reader writer is not found"); + + mOptions->setPluginStringData("fileType", "Ascii"); + mOptions->setPluginStringData("WriteImageHint", "UseExternal"); + } + + std::string serialize(const osg::Node& node) const + { + std::stringstream stream; + mReaderWriter->writeNode(node, stream, mOptions); + std::string result; + for (std::string line; std::getline(stream, line);) + { + if (line.starts_with('#')) + continue; + line.erase(line.find_last_not_of(" \t\n\r\f\v") + 1); + result += line; + result += '\n'; + } + return result; + } + }; + + struct NifOsgLoaderTest : Test, BaseNifOsgLoaderTest + { + }; + + TEST_F(NifOsgLoaderTest, shouldLoadFileWithDefaultNode) + { + Nif::NiAVObject node; + init(node); + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&node); + auto result = Loader::load(file, &mImageManager, &mMaterialManager); + EXPECT_EQ(serialize(*result), R"( +osg::Group { + UniqueID 1 + DataVariance STATIC + UserDataContainer TRUE { + osg::DefaultUserDataContainer { + UniqueID 2 + UDC_UserObjects 1 { + osg::StringValueObject { + UniqueID 3 + Name "fileHash" + } + } + } + } + Children 1 { + osg::Group { + UniqueID 4 + DataVariance STATIC + UserDataContainer TRUE { + osg::DefaultUserDataContainer { + UniqueID 5 + UDC_UserObjects 1 { + osg::UIntValueObject { + UniqueID 6 + Name "recIndex" + Value 4294967295 + } + } + } + } + } + } +} +)"); + } + + std::string formatOsgNodeForBSShaderProperty(std::string_view shaderPrefix) + { + std::ostringstream oss; + oss << R"( +osg::Group { + UniqueID 1 + DataVariance STATIC + UserDataContainer TRUE { + osg::DefaultUserDataContainer { + UniqueID 2 + UDC_UserObjects 1 { + osg::StringValueObject { + UniqueID 3 + Name "fileHash" + } + } + } + } + Children 1 { + osg::Group { + UniqueID 4 + DataVariance STATIC + UserDataContainer TRUE { + osg::DefaultUserDataContainer { + UniqueID 5 + UDC_UserObjects 3 { + osg::UIntValueObject { + UniqueID 6 + Name "recIndex" + Value 4294967295 + } + osg::StringValueObject { + UniqueID 7 + Name "shaderPrefix" + Value ")" + << shaderPrefix << R"(" + } + osg::BoolValueObject { + UniqueID 8 + Name "shaderRequired" + Value TRUE + } + } + } + } + StateSet TRUE { + osg::StateSet { + UniqueID 9 + } + } + } + } +} +)"; + return oss.str(); + } + + std::string formatOsgNodeForBSLightingShaderProperty(std::string_view shaderPrefix) + { + std::ostringstream oss; + oss << R"( +osg::Group { + UniqueID 1 + DataVariance STATIC + UserDataContainer TRUE { + osg::DefaultUserDataContainer { + UniqueID 2 + UDC_UserObjects 1 { + osg::StringValueObject { + UniqueID 3 + Name "fileHash" + } + } + } + } + Children 1 { + osg::Group { + UniqueID 4 + DataVariance STATIC + UserDataContainer TRUE { + osg::DefaultUserDataContainer { + UniqueID 5 + UDC_UserObjects 3 { + osg::UIntValueObject { + UniqueID 6 + Name "recIndex" + Value 4294967295 + } + osg::StringValueObject { + UniqueID 7 + Name "shaderPrefix" + Value ")" + << shaderPrefix << R"(" + } + osg::BoolValueObject { + UniqueID 8 + Name "shaderRequired" + Value TRUE + } + } + } + } + StateSet TRUE { + osg::StateSet { + UniqueID 9 + ModeList 1 { + GL_DEPTH_TEST ON + } + AttributeList 1 { + osg::Depth { + UniqueID 10 + } + Value OFF + } + } + } + } + } +} +)"; + return oss.str(); + } + + struct ShaderPrefixParams + { + unsigned int mShaderType; + std::string_view mExpectedShaderPrefix; + }; + + struct NifOsgLoaderBSShaderPrefixTest : TestWithParam, BaseNifOsgLoaderTest + { + static constexpr std::array sParams = { + ShaderPrefixParams{ static_cast(Nif::BSShaderType::ShaderType_Default), "bs/default" }, + ShaderPrefixParams{ static_cast(Nif::BSShaderType::ShaderType_NoLighting), "bs/nolighting" }, + ShaderPrefixParams{ static_cast(Nif::BSShaderType::ShaderType_Tile), "bs/default" }, + ShaderPrefixParams{ std::numeric_limits::max(), "bs/default" }, + }; + }; + + TEST_P(NifOsgLoaderBSShaderPrefixTest, shouldAddShaderPrefix) + { + Nif::NiAVObject node; + init(node); + Nif::BSShaderPPLightingProperty property; + property.recType = Nif::RC_BSShaderPPLightingProperty; + property.mTextureSet = nullptr; + property.mController = nullptr; + property.mType = GetParam().mShaderType; + node.mProperties.push_back(Nif::RecordPtrT(&property)); + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&node); + auto result = Loader::load(file, &mImageManager, &mMaterialManager); + EXPECT_EQ(serialize(*result), formatOsgNodeForBSShaderProperty(GetParam().mExpectedShaderPrefix)); + } + + INSTANTIATE_TEST_SUITE_P(Params, NifOsgLoaderBSShaderPrefixTest, ValuesIn(NifOsgLoaderBSShaderPrefixTest::sParams)); + + struct NifOsgLoaderBSLightingShaderPrefixTest : TestWithParam, BaseNifOsgLoaderTest + { + static constexpr std::array sParams = { + ShaderPrefixParams{ + static_cast(Nif::BSLightingShaderType::ShaderType_Default), "bs/default" }, + ShaderPrefixParams{ static_cast(Nif::BSLightingShaderType::ShaderType_Cloud), "bs/default" }, + ShaderPrefixParams{ std::numeric_limits::max(), "bs/default" }, + }; + }; + + TEST_P(NifOsgLoaderBSLightingShaderPrefixTest, shouldAddShaderPrefix) + { + Nif::NiAVObject node; + init(node); + Nif::BSLightingShaderProperty property; + property.recType = Nif::RC_BSLightingShaderProperty; + property.mTextureSet = nullptr; + property.mController = nullptr; + property.mType = GetParam().mShaderType; + property.mShaderFlags1 |= Nif::BSShaderFlags1::BSSFlag1_DepthTest; + property.mShaderFlags2 |= Nif::BSShaderFlags2::BSSFlag2_DepthWrite; + node.mProperties.push_back(Nif::RecordPtrT(&property)); + Nif::NIFFile file("test.nif"); + file.mRoots.push_back(&node); + auto result = Loader::load(file, &mImageManager, &mMaterialManager); + EXPECT_EQ(serialize(*result), formatOsgNodeForBSLightingShaderProperty(GetParam().mExpectedShaderPrefix)); + } + + INSTANTIATE_TEST_SUITE_P( + Params, NifOsgLoaderBSLightingShaderPrefixTest, ValuesIn(NifOsgLoaderBSLightingShaderPrefixTest::sParams)); +} diff --git a/apps/components_tests/resource/testobjectcache.cpp b/apps/components_tests/resource/testobjectcache.cpp new file mode 100644 index 00000000000..e2f5799edb8 --- /dev/null +++ b/apps/components_tests/resource/testobjectcache.cpp @@ -0,0 +1,377 @@ +#include + +#include +#include + +#include + +namespace Resource +{ + namespace + { + using namespace ::testing; + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheShouldReturnNullptrByDefault) + { + osg::ref_ptr> cache(new GenericObjectCache); + EXPECT_EQ(cache->getRefFromObjectCache(42), nullptr); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheOrNoneShouldReturnNulloptByDefault) + { + osg::ref_ptr> cache(new GenericObjectCache); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(42), std::nullopt); + } + + struct Object : osg::Object + { + Object() = default; + + Object(const Object& other, const osg::CopyOp& copyOp = osg::CopyOp()) + : osg::Object(other, copyOp) + { + } + + META_Object(ResourceTest, Object) + }; + + TEST(ResourceGenericObjectCacheTest, shouldStoreValues) + { + osg::ref_ptr> cache(new GenericObjectCache); + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_EQ(cache->getRefFromObjectCache(key), value); + } + + TEST(ResourceGenericObjectCacheTest, shouldStoreNullptrValues) + { + osg::ref_ptr> cache(new GenericObjectCache); + const int key = 42; + cache->addEntryToObjectCache(key, nullptr); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(nullptr)); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldExtendLifetimeForItemsWithZeroTimestamp) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value, 0); + value = nullptr; + + const double referenceTime = 1000; + const double expiryDelay = 1; + cache->update(referenceTime, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldReplaceExistingItemByKey) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const int key = 42; + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + cache->addEntryToObjectCache(key, value1); + ASSERT_EQ(cache->getRefFromObjectCache(key), value1); + cache->addEntryToObjectCache(key, value2); + EXPECT_EQ(cache->getRefFromObjectCache(key), value2); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldMarkLifetime) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + cache->addEntryToObjectCache(key, nullptr, referenceTime + expiryDelay); + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + 2 * expiryDelay, expiryDelay); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldRemoveExpiredItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 1; + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + value = nullptr; + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + ASSERT_EQ(cache->getStats().mExpired, 0); + + cache->update(referenceTime + expiryDelay, expiryDelay); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + ASSERT_EQ(cache->getStats().mExpired, 1); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldKeepExternallyReferencedItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 1; + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(value)); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldKeepNotExpiredItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + value = nullptr; + + cache->update(referenceTime + expiryDelay, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay / 2, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + } + + TEST(ResourceGenericObjectCacheTest, updateShouldKeepNotExpiredNullptrItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + cache->addEntryToObjectCache(key, nullptr); + + cache->update(referenceTime + expiryDelay, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay / 2, expiryDelay); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheOrNoneShouldNotExtendItemLifetime) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const double referenceTime = 1; + const double expiryDelay = 2; + + const int key = 42; + cache->addEntryToObjectCache(key, nullptr); + + cache->update(referenceTime, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay / 2, expiryDelay); + ASSERT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(_)); + + cache->update(referenceTime + expiryDelay, expiryDelay); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, lowerBoundShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + cache->addEntryToObjectCache("a", nullptr); + cache->addEntryToObjectCache("c", nullptr); + EXPECT_THAT(cache->lowerBound(std::string_view("b")), Optional(Pair("c", _))); + } + + TEST(ResourceGenericObjectCacheTest, shouldSupportRemovingItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + const int key = 42; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + ASSERT_EQ(cache->getRefFromObjectCache(key), value); + cache->removeFromObjectCache(key); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, clearShouldRemoveAllItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + const int key1 = 42; + const int key2 = 13; + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + cache->addEntryToObjectCache(key1, value1); + cache->addEntryToObjectCache(key2, value2); + + ASSERT_EQ(cache->getRefFromObjectCache(key1), value1); + ASSERT_EQ(cache->getRefFromObjectCache(key2), value2); + + cache->clear(); + + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key1), std::nullopt); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key2), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, callShouldIterateOverAllItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + osg::ref_ptr value3(new Object); + cache->addEntryToObjectCache(1, value1); + cache->addEntryToObjectCache(2, value2); + cache->addEntryToObjectCache(3, value3); + + std::vector> actual; + cache->call([&](int key, osg::Object* value) { actual.emplace_back(key, value); }); + + EXPECT_THAT(actual, ElementsAre(Pair(1, value1.get()), Pair(2, value2.get()), Pair(3, value3.get()))); + } + + TEST(ResourceGenericObjectCacheTest, getStatsShouldReturnNumberOrAddedItems) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + cache->addEntryToObjectCache(13, value1); + cache->addEntryToObjectCache(42, value2); + + const CacheStats stats = cache->getStats(); + + EXPECT_EQ(stats.mSize, 2); + } + + TEST(ResourceGenericObjectCacheTest, getStatsShouldReturnNumberOrGetsAndHits) + { + osg::ref_ptr> cache(new GenericObjectCache); + + { + const CacheStats stats = cache->getStats(); + + EXPECT_EQ(stats.mGet, 0); + EXPECT_EQ(stats.mHit, 0); + } + + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(13, value); + cache->getRefFromObjectCache(13); + cache->getRefFromObjectCache(42); + + { + const CacheStats stats = cache->getStats(); + + EXPECT_EQ(stats.mGet, 2); + EXPECT_EQ(stats.mHit, 1); + } + } + + TEST(ResourceGenericObjectCacheTest, lowerBoundShouldReturnFirstNotLessThatGivenKey) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + osg::ref_ptr value3(new Object); + cache->addEntryToObjectCache(1, value1); + cache->addEntryToObjectCache(2, value2); + cache->addEntryToObjectCache(4, value3); + + EXPECT_THAT(cache->lowerBound(3), Optional(Pair(4, value3))); + } + + TEST(ResourceGenericObjectCacheTest, lowerBoundShouldReturnNulloptWhenKeyIsGreaterThanAnyOther) + { + osg::ref_ptr> cache(new GenericObjectCache); + + osg::ref_ptr value1(new Object); + osg::ref_ptr value2(new Object); + osg::ref_ptr value3(new Object); + cache->addEntryToObjectCache(1, value1); + cache->addEntryToObjectCache(2, value2); + cache->addEntryToObjectCache(3, value3); + + EXPECT_EQ(cache->lowerBound(4), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(std::string_view("key"), value); + EXPECT_EQ(cache->getRefFromObjectCache(key), value); + } + + TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldKeyMoving) + { + osg::ref_ptr> cache(new GenericObjectCache); + std::string key(128, 'a'); + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(std::move(key), value); + EXPECT_EQ(key, ""); + EXPECT_EQ(cache->getRefFromObjectCache(std::string(128, 'a')), value); + } + + TEST(ResourceGenericObjectCacheTest, removeFromObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + ASSERT_EQ(cache->getRefFromObjectCache(key), value); + cache->removeFromObjectCache(std::string_view("key")); + EXPECT_EQ(cache->getRefFromObjectCacheOrNone(key), std::nullopt); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_EQ(cache->getRefFromObjectCache(std::string_view("key")), value); + } + + TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheOrNoneShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_THAT(cache->getRefFromObjectCacheOrNone(std::string_view("key")), Optional(value)); + } + + TEST(ResourceGenericObjectCacheTest, checkInObjectCacheShouldSupportHeterogeneousLookup) + { + osg::ref_ptr> cache(new GenericObjectCache); + const std::string key = "key"; + osg::ref_ptr value(new Object); + cache->addEntryToObjectCache(key, value); + EXPECT_TRUE(cache->checkInObjectCache(std::string_view("key"), 0)); + } + } +} diff --git a/apps/components_tests/sceneutil/osgacontroller.cpp b/apps/components_tests/sceneutil/osgacontroller.cpp new file mode 100644 index 00000000000..309de4a878c --- /dev/null +++ b/apps/components_tests/sceneutil/osgacontroller.cpp @@ -0,0 +1,131 @@ +#include + +#include +#include + +#include +#include + +namespace +{ + using namespace SceneUtil; + + static const std::string ROOT_BONE_NAME = "bip01"; + + // Creates a merged anim track with a single root channel with two start/end matrix transforms + osg::ref_ptr createMergedAnimationTrack(std::string name, osg::Matrixf startTransform, + osg::Matrixf endTransform, float startTime = 0.0f, float endTime = 1.0f) + { + osg::ref_ptr mergedAnimationTrack = new Resource::Animation; + mergedAnimationTrack->setName(name); + + osgAnimation::MatrixKeyframeContainer* cbCntr = new osgAnimation::MatrixKeyframeContainer; + cbCntr->push_back(osgAnimation::MatrixKeyframe(startTime, startTransform)); + cbCntr->push_back(osgAnimation::MatrixKeyframe(endTime, endTransform)); + + osg::ref_ptr rootChannel = new osgAnimation::MatrixLinearChannel; + rootChannel->setName("transform"); + rootChannel->setTargetName(ROOT_BONE_NAME); + rootChannel->getOrCreateSampler()->setKeyframeContainer(cbCntr); + mergedAnimationTrack->addChannel(rootChannel); + return mergedAnimationTrack; + } + + TEST(OsgAnimationControllerTest, getTranslationShouldReturnSampledChannelTranslationForBip01) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); // should sample this + emulatedAnimations.push_back({ 1.1f, 2.0f, "test2" }); // should ignore this + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform2 = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + endTransform2.setTrans(2.0f, 2.0f, 2.0f); + controller.addMergedAnimationTrack( + createMergedAnimationTrack("test2", endTransform, endTransform2, 0.1f, 0.9f)); + + // should be halfway between 0,0,0 and 1,1,1 + osg::Vec3f translation = controller.getTranslation(0.5f); + EXPECT_EQ(translation, osg::Vec3f(0.5f, 0.5f, 0.5f)); + } + + TEST(OsgAnimationControllerTest, getTranslationShouldReturnZeroVectorIfNotFound) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + + // Has no emulated animation at time so will return 0,0,0 + osg::Vec3f translation = controller.getTranslation(100.0f); + EXPECT_EQ(translation, osg::Vec3f(0.0f, 0.0f, 0.0f)); + } + + TEST(OsgAnimationControllerTest, getTranslationShouldReturnZeroVectorIfNoMergedTracks) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + // Has no merged tracks so will return 0,0,0 + osg::Vec3f translation = controller.getTranslation(0.5); + EXPECT_EQ(translation, osg::Vec3f(0.0f, 0.0f, 0.0f)); + } + + TEST(OsgAnimationControllerTest, getTransformShouldReturnIdentityIfNotFound) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + + // Has no emulated animation at time so will return identity + EXPECT_EQ(controller.getTransformForNode(100.0f, ROOT_BONE_NAME), osg::Matrixf::identity()); + + // Has no bone animation at time so will return identity + EXPECT_EQ(controller.getTransformForNode(0.5f, "wrongbone"), osg::Matrixf::identity()); + } + + TEST(OsgAnimationControllerTest, getTransformShouldReturnSampledAnimMatrixAtTime) + { + std::vector emulatedAnimations; + emulatedAnimations.push_back({ 0.0f, 1.0f, "test1" }); // should sample this + emulatedAnimations.push_back({ 1.1f, 2.0f, "test2" }); // should ignore this + + OsgAnimationController controller; + controller.setEmulatedAnimations(emulatedAnimations); + + osg::Matrixf startTransform = osg::Matrixf::identity(); + osg::Matrixf endTransform = osg::Matrixf::identity(); + endTransform.setTrans(1.0f, 1.0f, 1.0f); + controller.addMergedAnimationTrack(createMergedAnimationTrack("test1", startTransform, endTransform)); + osg::Matrixf endTransform2 = osg::Matrixf::identity(); + endTransform2.setTrans(2.0f, 2.0f, 2.0f); + controller.addMergedAnimationTrack( + createMergedAnimationTrack("test2", endTransform, endTransform2, 0.1f, 0.9f)); + + EXPECT_EQ(controller.getTransformForNode(0.0f, ROOT_BONE_NAME), startTransform); // start of test1 + EXPECT_EQ(controller.getTransformForNode(1.0f, ROOT_BONE_NAME), endTransform); // end of test1 + EXPECT_EQ(controller.getTransformForNode(1.1f, ROOT_BONE_NAME), endTransform); // start of test2 + EXPECT_EQ(controller.getTransformForNode(2.0f, ROOT_BONE_NAME), endTransform2); // end of test2 + } +} diff --git a/apps/components_tests/serialization/binaryreader.cpp b/apps/components_tests/serialization/binaryreader.cpp new file mode 100644 index 00000000000..e480d5587f9 --- /dev/null +++ b/apps/components_tests/serialization/binaryreader.cpp @@ -0,0 +1,90 @@ +#include "format.hpp" + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Serialization; + using namespace SerializationTesting; + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldReadArithmeticTypeValue) + { + std::uint32_t value = 42; + std::vector data(sizeof(value)); + std::memcpy(data.data(), &value, sizeof(value)); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::uint32_t result = 0; + const TestFormat format; + binaryReader(format, result); + EXPECT_EQ(result, 42u); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldReadArithmeticTypeRangeValue) + { + const std::size_t count = 3; + std::vector data(sizeof(std::size_t) + count * sizeof(std::uint32_t)); + std::memcpy(data.data(), &count, sizeof(count)); + const std::uint32_t value1 = 960900021; + std::memcpy(data.data() + sizeof(count), &value1, sizeof(std::uint32_t)); + const std::uint32_t value2 = 1235496234; + std::memcpy(data.data() + sizeof(count) + sizeof(std::uint32_t), &value2, sizeof(std::uint32_t)); + const std::uint32_t value3 = 2342038092; + std::memcpy(data.data() + sizeof(count) + 2 * sizeof(std::uint32_t), &value3, sizeof(std::uint32_t)); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::size_t resultCount = 0; + const TestFormat format; + binaryReader(format, resultCount); + std::vector result(resultCount); + binaryReader(format, result.data(), result.size()); + EXPECT_THAT(result, ElementsAre(value1, value2, value3)); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, forNotEnoughDataForArithmeticTypeShouldThrowException) + { + std::vector data(3); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::uint32_t result = 0; + const TestFormat format; + EXPECT_THROW(binaryReader(format, result), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, forNotEnoughDataForArithmeticTypeRangeShouldThrowException) + { + std::vector data(7); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::vector values(2); + const TestFormat format; + EXPECT_THROW(binaryReader(format, values.data(), values.size()), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldSetPointerToCurrentBufferPosition) + { + std::vector data(8); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + const std::byte* ptr = nullptr; + const TestFormat format; + binaryReader(format, ptr); + EXPECT_EQ(ptr, data.data()); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldNotAdvanceAfterPointer) + { + std::vector data(8); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + const std::byte* ptr1 = nullptr; + const std::byte* ptr2 = nullptr; + const TestFormat format; + binaryReader(format, ptr1); + binaryReader(format, ptr2); + EXPECT_EQ(ptr1, data.data()); + EXPECT_EQ(ptr2, data.data()); + } +} diff --git a/apps/components_tests/serialization/binarywriter.cpp b/apps/components_tests/serialization/binarywriter.cpp new file mode 100644 index 00000000000..dde14408132 --- /dev/null +++ b/apps/components_tests/serialization/binarywriter.cpp @@ -0,0 +1,63 @@ +#include "format.hpp" + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Serialization; + using namespace SerializationTesting; + + TEST(DetourNavigatorSerializationBinaryWriterTest, shouldWriteArithmeticTypeValue) + { + std::vector result(4); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + const TestFormat format; + binaryWriter(format, std::uint32_t(42)); + EXPECT_THAT(result, ElementsAre(std::byte(42), std::byte(0), std::byte(0), std::byte(0))); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, shouldWriteArithmeticTypeRangeValue) + { + std::vector result(8); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + std::vector values({ 42, 13 }); + const TestFormat format; + binaryWriter(format, values.data(), values.size()); + constexpr std::array expected{ + std::byte(42), + std::byte(0), + std::byte(0), + std::byte(0), + std::byte(13), + std::byte(0), + std::byte(0), + std::byte(0), + }; + EXPECT_THAT(result, ElementsAreArray(expected)); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, forNotEnoughSpaceForArithmeticTypeShouldThrowException) + { + std::vector result(3); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + const TestFormat format; + EXPECT_THROW(binaryWriter(format, std::uint32_t(42)), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, forNotEnoughSpaceForArithmeticTypeRangeShouldThrowException) + { + std::vector result(7); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + std::vector values({ 42, 13 }); + const TestFormat format; + EXPECT_THROW(binaryWriter(format, values.data(), values.size()), std::runtime_error); + } +} diff --git a/apps/components_tests/serialization/format.hpp b/apps/components_tests/serialization/format.hpp new file mode 100644 index 00000000000..680d6c1abc4 --- /dev/null +++ b/apps/components_tests/serialization/format.hpp @@ -0,0 +1,75 @@ +#ifndef OPENMW_TEST_SUITE_SERIALIZATION_FORMAT_H +#define OPENMW_TEST_SUITE_SERIALIZATION_FORMAT_H + +#include + +#include +#include +#include + +namespace SerializationTesting +{ + struct Pod + { + int mInt = 42; + double mDouble = 3.14; + + friend bool operator==(const Pod& l, const Pod& r) + { + const auto tuple = [](const Pod& v) { return std::tuple(v.mInt, v.mDouble); }; + return tuple(l) == tuple(r); + } + }; + + enum Enum : std::int32_t + { + A, + B, + C, + }; + + struct Composite + { + short mFloatArray[3] = { 0 }; + std::vector mIntVector; + std::vector mEnumVector; + std::vector mPodVector; + std::size_t mPodDataSize = 0; + std::vector mPodBuffer; + std::size_t mCharDataSize = 0; + std::vector mCharBuffer; + }; + + template + struct TestFormat : Serialization::Format> + { + using Serialization::Format>::operator(); + + template + auto operator()(Visitor&& visitor, T& value) const -> std::enable_if_t, Pod>> + { + visitor(*this, value.mInt); + visitor(*this, value.mDouble); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, Composite>> + { + visitor(*this, value.mFloatArray); + visitor(*this, value.mIntVector); + visitor(*this, value.mEnumVector); + visitor(*this, value.mPodVector); + visitor(*this, value.mPodDataSize); + if constexpr (mode == Serialization::Mode::Read) + value.mPodBuffer.resize(value.mPodDataSize); + visitor(*this, value.mPodBuffer.data(), value.mPodDataSize); + visitor(*this, value.mCharDataSize); + if constexpr (mode == Serialization::Mode::Read) + value.mCharBuffer.resize(value.mCharDataSize); + visitor(*this, value.mCharBuffer.data(), value.mCharDataSize); + } + }; +} + +#endif diff --git a/apps/components_tests/serialization/integration.cpp b/apps/components_tests/serialization/integration.cpp new file mode 100644 index 00000000000..e703f0f8980 --- /dev/null +++ b/apps/components_tests/serialization/integration.cpp @@ -0,0 +1,56 @@ +#include "format.hpp" + +#include +#include +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace Serialization; + using namespace SerializationTesting; + + struct DetourNavigatorSerializationIntegrationTest : Test + { + Composite mComposite; + + DetourNavigatorSerializationIntegrationTest() + { + mComposite.mIntVector = { 4, 5, 6 }; + mComposite.mEnumVector = { Enum::A, Enum::B, Enum::C }; + mComposite.mPodVector = { Pod{ 4, 23.87 }, Pod{ 5, -31.76 }, Pod{ 6, 65.12 } }; + mComposite.mPodBuffer = { Pod{ 7, 456.123 }, Pod{ 8, -628.346 } }; + mComposite.mPodDataSize = mComposite.mPodBuffer.size(); + std::string charData = "serialization"; + mComposite.mCharBuffer = { charData.begin(), charData.end() }; + mComposite.mCharDataSize = charData.size(); + } + }; + + TEST_F(DetourNavigatorSerializationIntegrationTest, sizeAccumulatorShouldSupportCustomSerializer) + { + SizeAccumulator sizeAccumulator; + TestFormat{}(sizeAccumulator, mComposite); + EXPECT_EQ(sizeAccumulator.value(), 143); + } + + TEST_F(DetourNavigatorSerializationIntegrationTest, binaryReaderShouldDeserializeDataWrittenByBinaryWriter) + { + std::vector data(143); + TestFormat{}(BinaryWriter(data.data(), data.data() + data.size()), mComposite); + Composite result; + TestFormat{}(BinaryReader(data.data(), data.data() + data.size()), result); + EXPECT_EQ(result.mIntVector, mComposite.mIntVector); + EXPECT_EQ(result.mEnumVector, mComposite.mEnumVector); + EXPECT_EQ(result.mPodVector, mComposite.mPodVector); + EXPECT_EQ(result.mPodDataSize, mComposite.mPodDataSize); + EXPECT_EQ(result.mPodBuffer, mComposite.mPodBuffer); + EXPECT_EQ(result.mCharDataSize, mComposite.mCharDataSize); + EXPECT_EQ(result.mCharBuffer, mComposite.mCharBuffer); + } +} diff --git a/apps/components_tests/serialization/sizeaccumulator.cpp b/apps/components_tests/serialization/sizeaccumulator.cpp new file mode 100644 index 00000000000..35ab3be4c48 --- /dev/null +++ b/apps/components_tests/serialization/sizeaccumulator.cpp @@ -0,0 +1,43 @@ +#include "format.hpp" + +#include + +#include + +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Serialization; + using namespace SerializationTesting; + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldProvideSizeForArithmeticType) + { + SizeAccumulator sizeAccumulator; + constexpr std::monostate format; + sizeAccumulator(format, std::uint32_t()); + EXPECT_EQ(sizeAccumulator.value(), 4); + } + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldProvideSizeForArithmeticTypeRange) + { + SizeAccumulator sizeAccumulator; + const std::uint64_t* const data = nullptr; + const std::size_t count = 3; + const std::monostate format; + sizeAccumulator(format, data, count); + EXPECT_EQ(sizeAccumulator.value(), 24); + } + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldSupportCustomSerializer) + { + SizeAccumulator sizeAccumulator; + const TestFormat format; + sizeAccumulator(format, Pod{}); + EXPECT_EQ(sizeAccumulator.value(), 12); + } +} diff --git a/apps/components_tests/settings/parser.cpp b/apps/components_tests/settings/parser.cpp new file mode 100644 index 00000000000..af514dbdd7a --- /dev/null +++ b/apps/components_tests/settings/parser.cpp @@ -0,0 +1,373 @@ +#include +#include + +#include + +#include + +namespace +{ + using namespace testing; + using namespace Settings; + + struct SettingsFileParserTest : Test + { + SettingsFileParser mLoader; + SettingsFileParser mSaver; + + template + void withSettingsFile(const std::string& content, F&& f) + { + auto path = TestingOpenMW::outputFilePath( + std::string(UnitTest::GetInstance()->current_test_info()->name()) + ".cfg"); + + { + std::ofstream stream(path); + stream << content; + stream.close(); + } + + f(path); + } + }; + + TEST_F(SettingsFileParserTest, load_empty_file) + { + const std::string content; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap()); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_empty_section) + { + const std::string content = "[Section]\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap()); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_section_and_key) + { + const std::string content + = "[Section]\n" + "key = value\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ { CategorySetting("Section", "key"), "value" } })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_section_and_key_and_line_comments) + { + const std::string content + = "# foo\n" + "[Section]\n" + "# bar\n" + "key = value\n" + "# baz\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ { CategorySetting("Section", "key"), "value" } })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_section_and_key_file_and_inline_section_comment) + { + const std::string content + = "[Section] # foo\n" + "key = value\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_single_section_and_key_and_inline_key_comment) + { + const std::string content + = "[Section]\n" + "key = value # foo\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ { CategorySetting("Section", "key"), "value # foo" } })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_section_and_key_and_whitespaces) + { + const std::string content + = " [ Section ] \n" + " key = value \n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ { CategorySetting("Section", "key"), "value" } })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_quoted_string_value) + { + const std::string content + = "[Section]\n" + R"(key = "value")"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ { CategorySetting("Section", "key"), R"("value")" } })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_quoted_string_value_and_eol) + { + const std::string content + = "[Section]\n" + R"(key = "value"\n)"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ { CategorySetting("Section", "key"), R"("value"\n)" } })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_empty_value) + { + const std::string content + = "[Section]\n" + "key =\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ { CategorySetting("Section", "key"), "" } })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_empty_key) + { + const std::string content + = "[Section]\n" + "=\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ { CategorySetting("Section", ""), "" } })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_multiple_keys) + { + const std::string content + = "[Section]\n" + "key1 = value1\n" + "key2 = value2\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, + CategorySettingValueMap({ + { CategorySetting("Section", "key1"), "value1" }, + { CategorySetting("Section", "key2"), "value2" }, + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_multiple_sections) + { + const std::string content + = "[Section1]\n" + "key1 = value1\n" + "[Section2]\n" + "key2 = value2\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, + CategorySettingValueMap({ + { CategorySetting("Section1", "key1"), "value1" }, + { CategorySetting("Section2", "key2"), "value2" }, + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_multiple_sections_and_keys) + { + const std::string content + = "[Section1]\n" + "key1 = value1\n" + "key2 = value2\n" + "[Section2]\n" + "key3 = value3\n" + "key4 = value4\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, + CategorySettingValueMap({ + { CategorySetting("Section1", "key1"), "value1" }, + { CategorySetting("Section1", "key2"), "value2" }, + { CategorySetting("Section2", "key3"), "value3" }, + { CategorySetting("Section2", "key4"), "value4" }, + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_repeated_sections) + { + const std::string content + = "[Section]\n" + "key1 = value1\n" + "[Section]\n" + "key2 = value2\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, + CategorySettingValueMap({ + { CategorySetting("Section", "key1"), "value1" }, + { CategorySetting("Section", "key2"), "value2" }, + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_repeated_keys) + { + const std::string content + = "[Section]\n" + "key = value\n" + "key = value\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_with_repeated_keys_in_differrent_sections) + { + const std::string content + = "[Section1]\n" + "key = value\n" + "[Section2]\n" + "key = value\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, + CategorySettingValueMap({ + { CategorySetting("Section1", "key"), "value" }, + { CategorySetting("Section2", "key"), "value" }, + })); + }); + } + + TEST_F(SettingsFileParserTest, file_with_unterminated_section) + { + const std::string content = "[Section"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_with_single_empty_section_name) + { + const std::string content = "[]\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap()); + }); + } + + TEST_F(SettingsFileParserTest, file_with_key_and_without_section) + { + const std::string content = "key = value\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_with_key_in_empty_name_section) + { + const std::string content + = "[]" + "key = value\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_with_unterminated_key) + { + const std::string content + = "[Section]\n" + "key\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + EXPECT_THROW(mLoader.loadSettingsFile(path, map), std::runtime_error); + }); + } + + TEST_F(SettingsFileParserTest, file_with_empty_lines) + { + const std::string content + = "\n" + "[Section]\n" + "\n" + "key = value\n" + "\n"; + + withSettingsFile(content, [this](const auto& path) { + CategorySettingValueMap map; + mLoader.loadSettingsFile(path, map); + + EXPECT_EQ(map, CategorySettingValueMap({ { CategorySetting("Section", "key"), "value" } })); + }); + } +} diff --git a/apps/components_tests/settings/shadermanager.cpp b/apps/components_tests/settings/shadermanager.cpp new file mode 100644 index 00000000000..c3ba5084c29 --- /dev/null +++ b/apps/components_tests/settings/shadermanager.cpp @@ -0,0 +1,69 @@ +#include +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace Settings; + + struct ShaderSettingsTest : Test + { + template + void withSettingsFile(const std::string& content, F&& f) + { + auto path = TestingOpenMW::outputFilePath( + std::string(UnitTest::GetInstance()->current_test_info()->name()) + ".yaml"); + + { + std::ofstream stream; + stream.open(path); + stream << content; + stream.close(); + } + + f(path); + } + }; + + TEST_F(ShaderSettingsTest, fail_to_fetch_then_set_and_succeed) + { + const std::string content = + R"YAML( +config: + shader: + vec3_uniform: [1.0, 2.0] +)YAML"; + + withSettingsFile(content, [](const auto& path) { + EXPECT_TRUE(ShaderManager::get().load(path)); + EXPECT_FALSE(ShaderManager::get().getValue("shader", "vec3_uniform").has_value()); + EXPECT_TRUE(ShaderManager::get().setValue("shader", "vec3_uniform", osg::Vec3f(1, 2, 3))); + EXPECT_TRUE(ShaderManager::get().getValue("shader", "vec3_uniform").has_value()); + EXPECT_EQ(ShaderManager::get().getValue("shader", "vec3_uniform").value(), osg::Vec3f(1, 2, 3)); + EXPECT_TRUE(ShaderManager::get().save()); + }); + } + + TEST_F(ShaderSettingsTest, fail_to_load_file_then_fail_to_set_and_get) + { + const std::string content = + R"YAML( +config: + shader: + uniform: 12.0 + >Defeated by a sideways carrot +)YAML"; + + withSettingsFile(content, [](const auto& path) { + EXPECT_FALSE(ShaderManager::get().load(path)); + EXPECT_FALSE(ShaderManager::get().setValue("shader", "uniform", 12.0)); + EXPECT_FALSE(ShaderManager::get().getValue("shader", "uniform").has_value()); + EXPECT_FALSE(ShaderManager::get().save()); + }); + } +} diff --git a/apps/components_tests/settings/testvalues.cpp b/apps/components_tests/settings/testvalues.cpp new file mode 100644 index 00000000000..236417b559b --- /dev/null +++ b/apps/components_tests/settings/testvalues.cpp @@ -0,0 +1,154 @@ +#include "components/misc/strings/conversion.hpp" +#include "components/settings/parser.hpp" +#include "components/settings/values.hpp" + +#include +#include + +#ifndef OPENMW_PROJECT_SOURCE_DIR +#define OPENMW_PROJECT_SOURCE_DIR "." +#endif + +namespace Settings +{ + namespace + { + using namespace testing; + + struct SettingsValuesTest : Test + { + const std::filesystem::path mSettingsDefaultPath = std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } + / "files" / Misc::StringUtils::stringToU8String("settings-default.cfg"); + + SettingsValuesTest() + { + Manager::mDefaultSettings.clear(); + Manager::mUserSettings.clear(); + Manager::mChangedSettings.clear(); + SettingsFileParser parser; + parser.loadSettingsFile(mSettingsDefaultPath, Manager::mDefaultSettings); + } + }; + + TEST_F(SettingsValuesTest, shouldLoadFromSettingsManager) + { + Index index; + Values values(index); + EXPECT_EQ(values.mCamera.mFieldOfView.get(), 60); + } + + TEST_F(SettingsValuesTest, shouldFillIndexOnLoad) + { + Index index; + Values values(index); + EXPECT_EQ(index.get("Camera", "field of view").get(), 60); + } + + TEST_F(SettingsValuesTest, constructorShouldThrowExceptionOnMissingSetting) + { + Manager::mDefaultSettings.erase({ "Camera", "field of view" }); + Index index; + EXPECT_THROW([&] { Values values(index); }(), std::runtime_error); + } + + TEST_F(SettingsValuesTest, constructorShouldSanitize) + { + Manager::mUserSettings[std::make_pair("Camera", "field of view")] = "-1"; + Index index; + Values values(index); + EXPECT_EQ(values.mCamera.mFieldOfView.get(), 1); + } + + TEST_F(SettingsValuesTest, constructorWithDefaultShouldDoLookup) + { + Manager::mUserSettings[std::make_pair("category", "value")] = "13"; + Index index; + SettingValue value{ index, "category", "value", 42 }; + EXPECT_EQ(value.get(), 13); + value.reset(); + EXPECT_EQ(value.get(), 42); + } + + TEST_F(SettingsValuesTest, constructorWithDefaultShouldSanitize) + { + Manager::mUserSettings[std::make_pair("category", "value")] = "2"; + Index index; + SettingValue value{ index, "category", "value", -1, Settings::makeClampSanitizerInt(0, 1) }; + EXPECT_EQ(value.get(), 1); + value.reset(); + EXPECT_EQ(value.get(), 0); + } + + TEST_F(SettingsValuesTest, constructorWithDefaultShouldFallbackToDefault) + { + Index index; + const SettingValue value{ index, "category", "value", 42 }; + EXPECT_EQ(value.get(), 42); + } + + TEST_F(SettingsValuesTest, moveConstructorShouldSetDefaults) + { + Index index; + Values defaultValues(index); + Manager::mUserSettings.emplace(std::make_pair("Camera", "field of view"), "61"); + Values values(std::move(defaultValues)); + EXPECT_EQ(values.mCamera.mFieldOfView.get(), 61); + values.mCamera.mFieldOfView.reset(); + EXPECT_EQ(values.mCamera.mFieldOfView.get(), 60); + } + + TEST_F(SettingsValuesTest, moveConstructorShouldSanitize) + { + Index index; + Values defaultValues(index); + Manager::mUserSettings[std::make_pair("Camera", "field of view")] = "-1"; + Values values(std::move(defaultValues)); + EXPECT_EQ(values.mCamera.mFieldOfView.get(), 1); + } + + TEST_F(SettingsValuesTest, moveConstructorShouldThrowOnMissingSetting) + { + Index index; + SettingValue defaultValue{ index, "category", "value", 42 }; + EXPECT_THROW([&] { SettingValue value(std::move(defaultValue)); }(), std::runtime_error); + } + + TEST_F(SettingsValuesTest, findShouldThrowExceptionOnTypeMismatch) + { + Index index; + Values values(index); + EXPECT_THROW(index.find("Camera", "field of view"), std::invalid_argument); + } + + TEST_F(SettingsValuesTest, findShouldReturnNullptrForAbsentSetting) + { + Index index; + Values values(index); + EXPECT_EQ(index.find("foo", "bar"), nullptr); + } + + TEST_F(SettingsValuesTest, getShouldThrowExceptionForAbsentSetting) + { + Index index; + Values values(index); + EXPECT_THROW(index.get("foo", "bar").get(), std::invalid_argument); + } + + TEST_F(SettingsValuesTest, setShouldChangeManagerUserSettings) + { + Index index; + Values values(index); + values.mCamera.mFieldOfView.set(42); + EXPECT_EQ(Manager::mUserSettings.at({ "Camera", "field of view" }), "42"); + EXPECT_THAT(Manager::mChangedSettings, ElementsAre(std::make_pair("Camera", "field of view"))); + } + + TEST_F(SettingsValuesTest, setShouldNotChangeManagerChangedSettingsForNoChange) + { + Index index; + Values values(index); + values.mCamera.mFieldOfView.set(values.mCamera.mFieldOfView.get()); + EXPECT_THAT(Manager::mChangedSettings, ElementsAre()); + } + } +} diff --git a/apps/openmw_test_suite/shader/parsedefines.cpp b/apps/components_tests/shader/parsedefines.cpp similarity index 97% rename from apps/openmw_test_suite/shader/parsedefines.cpp rename to apps/components_tests/shader/parsedefines.cpp index 65b4380a718..441dab2bb65 100644 --- a/apps/openmw_test_suite/shader/parsedefines.cpp +++ b/apps/components_tests/shader/parsedefines.cpp @@ -1,7 +1,7 @@ #include -#include #include +#include namespace { @@ -58,7 +58,9 @@ namespace namespace SupportedTerminals { - struct ShaderParseDefinesTest : ::ShaderParseDefinesTest, WithParamInterface {}; + struct ShaderParseDefinesTest : ::ShaderParseDefinesTest, WithParamInterface + { + }; TEST_P(ShaderParseDefinesTest, support_defines_terminated_by) { @@ -69,10 +71,7 @@ namespace } INSTANTIATE_TEST_SUITE_P( - SupportedTerminals, - ShaderParseDefinesTest, - Values(' ', '\n', '\r', '(', ')', '[', ']', '.', ';', ',') - ); + SupportedTerminals, ShaderParseDefinesTest, Values(' ', '\n', '\r', '(', ')', '[', ']', '.', ';', ',')); } TEST_F(ShaderParseDefinesTest, should_not_support_define_ending_with_source) diff --git a/apps/openmw_test_suite/shader/parsefors.cpp b/apps/components_tests/shader/parsefors.cpp similarity index 92% rename from apps/openmw_test_suite/shader/parsefors.cpp rename to apps/components_tests/shader/parsefors.cpp index 330feb172d4..2e1869843d5 100644 --- a/apps/openmw_test_suite/shader/parsefors.cpp +++ b/apps/components_tests/shader/parsefors.cpp @@ -1,7 +1,8 @@ #include -#include #include +#include +#include namespace { @@ -16,6 +17,12 @@ namespace const std::string mName = "shader"; }; + static bool parseFors(std::string& source, const std::string& templateName) + { + std::vector dummy; + return parseDirectives(source, dummy, {}, {}, templateName); + } + TEST_F(ShaderParseForsTest, empty_should_succeed) { ASSERT_TRUE(parseFors(mSource, mName)); diff --git a/apps/components_tests/shader/parselinks.cpp b/apps/components_tests/shader/parselinks.cpp new file mode 100644 index 00000000000..05396b2aebd --- /dev/null +++ b/apps/components_tests/shader/parselinks.cpp @@ -0,0 +1,97 @@ +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Shader; + + using DefineMap = ShaderManager::DefineMap; + + struct ShaderParseLinksTest : Test + { + std::string mSource; + std::vector mLinkTargets; + ShaderManager::DefineMap mDefines; + const std::string mName = "my_shader.glsl"; + + bool parseLinks() { return parseDirectives(mSource, mLinkTargets, mDefines, {}, mName); } + }; + + TEST_F(ShaderParseLinksTest, empty_should_succeed) + { + ASSERT_TRUE(parseLinks()); + EXPECT_EQ(mSource, ""); + EXPECT_TRUE(mLinkTargets.empty()); + } + + TEST_F(ShaderParseLinksTest, should_fail_for_single_escape_symbol) + { + mSource = "$"; + ASSERT_FALSE(parseLinks()); + EXPECT_EQ(mSource, "$"); + EXPECT_TRUE(mLinkTargets.empty()); + } + + TEST_F(ShaderParseLinksTest, should_fail_on_first_found_escaped_not_valid_directive) + { + mSource = "$foo "; + ASSERT_FALSE(parseLinks()); + EXPECT_EQ(mSource, "$foo "); + EXPECT_TRUE(mLinkTargets.empty()); + } + + TEST_F(ShaderParseLinksTest, should_fail_on_absent_link_target) + { + mSource = "$link "; + ASSERT_FALSE(parseLinks()); + EXPECT_EQ(mSource, "$link "); + EXPECT_TRUE(mLinkTargets.empty()); + } + + TEST_F(ShaderParseLinksTest, should_not_require_newline) + { + mSource = "$link \"foo.glsl\""; + ASSERT_TRUE(parseLinks()); + EXPECT_EQ(mSource, ""); + ASSERT_EQ(mLinkTargets.size(), 1); + EXPECT_EQ(mLinkTargets[0], "foo.glsl"); + } + + TEST_F(ShaderParseLinksTest, should_require_quotes) + { + mSource = "$link foo.glsl"; + ASSERT_FALSE(parseLinks()); + EXPECT_EQ(mSource, "$link foo.glsl"); + EXPECT_EQ(mLinkTargets.size(), 0); + } + + TEST_F(ShaderParseLinksTest, should_be_replaced_with_empty_line) + { + mSource = "$link \"foo.glsl\"\nbar"; + ASSERT_TRUE(parseLinks()); + EXPECT_EQ(mSource, "\nbar"); + ASSERT_EQ(mLinkTargets.size(), 1); + EXPECT_EQ(mLinkTargets[0], "foo.glsl"); + } + + TEST_F(ShaderParseLinksTest, should_only_accept_on_true_condition) + { + mSource = + R"glsl( +$link "foo.glsl" if 1 +$link "bar.glsl" if 0 +)glsl"; + ASSERT_TRUE(parseLinks()); + EXPECT_EQ(mSource, + R"glsl( + + +)glsl"); + ASSERT_EQ(mLinkTargets.size(), 1); + EXPECT_EQ(mLinkTargets[0], "foo.glsl"); + } +} diff --git a/apps/components_tests/shader/shadermanager.cpp b/apps/components_tests/shader/shadermanager.cpp new file mode 100644 index 00000000000..5b11d31a44e --- /dev/null +++ b/apps/components_tests/shader/shadermanager.cpp @@ -0,0 +1,234 @@ +#include +#include +#include + +#include + +#include + +namespace +{ + using namespace testing; + using namespace Shader; + + struct ShaderManagerTest : Test + { + ShaderManager mManager; + ShaderManager::DefineMap mDefines; + + ShaderManagerTest() { mManager.setShaderPath("tests_output"); } + + template + void withShaderFile(const std::string& content, F&& f) + { + withShaderFile("", content, std::forward(f)); + } + + template + void withShaderFile(const std::string& suffix, const std::string& content, F&& f) + { + auto subdir = std::filesystem::path("lib") + / std::filesystem::path( + std::string(UnitTest::GetInstance()->current_test_info()->name()) + suffix + ".vert"); + auto path = TestingOpenMW::outputFilePathWithSubDir(subdir); + { + std::ofstream stream(path); + stream << content; + stream.close(); + } + + f(subdir); + } + }; + + TEST_F(ShaderManagerTest, get_shader_with_empty_content_should_succeed) + { + const std::string content; + + withShaderFile(content, [this](const std::filesystem::path& templateName) { + EXPECT_TRUE(mManager.getShader(Files::pathToUnicodeString(templateName))); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_not_change_source_without_template_parameters) + { + const std::string content + = "#version 120\n" + "void main() {}\n"; + + withShaderFile(content, [&](const std::filesystem::path& templateName) { + const auto shader = mManager.getShader(Files::pathToUnicodeString(templateName), mDefines); + ASSERT_TRUE(shader); + EXPECT_EQ(shader->getShaderSource(), content); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_replace_includes_with_content) + { + const std::string content0 = "void foo() {}\n"; + + withShaderFile("_0", content0, [&](const std::filesystem::path& templateName0) { + const std::string content1 = + "#include \"" + Files::pathToUnicodeString(templateName0) + "\"\n" + "void bar() { foo() }\n"; + + withShaderFile("_1", content1, [&](const std::filesystem::path& templateName1) { + const std::string content2 = + "#version 120\n" + "#include \"" + Files::pathToUnicodeString(templateName1) + "\"\n" + "void main() { bar() }\n"; + + withShaderFile(content2, [&](const std::filesystem::path& templateName2) { + const auto shader = mManager.getShader(Files::pathToUnicodeString(templateName2), mDefines); + ASSERT_TRUE(shader); + const std::string expected + = "#version 120\n" + "#line 0 1\n" + "#line 0 2\n" + "void foo() {}\n" + "\n" + "#line 0 0\n" + "\n" + "void bar() { foo() }\n" + "\n" + "#line 1 0\n" + "\n" + "void main() { bar() }\n"; + EXPECT_EQ(shader->getShaderSource(), expected); + }); + }); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_replace_defines) + { + const std::string content + = "#version 120\n" + "#define FLAG @flag\n" + "void main() {}\n"; + + withShaderFile(content, [&](const std::filesystem::path& templateName) { + mDefines["flag"] = "1"; + const auto shader = mManager.getShader(Files::pathToUnicodeString(templateName), mDefines); + ASSERT_TRUE(shader); + const std::string expected + = "#version 120\n" + "#define FLAG 1\n" + "void main() {}\n"; + EXPECT_EQ(shader->getShaderSource(), expected); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_expand_loop) + { + const std::string content + = "#version 120\n" + "@foreach index @list\n" + " varying vec4 foo@index;\n" + "@endforeach\n" + "void main() {}\n"; + + withShaderFile(content, [&](const std::filesystem::path& templateName) { + mDefines["list"] = "1,2,3"; + const auto shader = mManager.getShader(Files::pathToUnicodeString(templateName), mDefines); + ASSERT_TRUE(shader); + const std::string expected + = "#version 120\n" + " varying vec4 foo1;\n" + " varying vec4 foo2;\n" + " varying vec4 foo3;\n" + "\n" + "#line 5\n" + "void main() {}\n"; + EXPECT_EQ(shader->getShaderSource(), expected); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_replace_loops_with_conditions) + { + const std::string content + = "#version 120\n" + "@foreach index @list\n" + " varying vec4 foo@index;\n" + "@endforeach\n" + "void main()\n" + "{\n" + "#ifdef BAR\n" + "@foreach index @list\n" + " foo@index = vec4(1.0);\n" + "@endforeach\n" + "#elif BAZ\n" + "@foreach index @list\n" + " foo@index = vec4(2.0);\n" + "@endforeach\n" + "#else\n" + "@foreach index @list\n" + " foo@index = vec4(3.0);\n" + "@endforeach\n" + "#endif\n" + "}\n"; + + withShaderFile(content, [&](const std::filesystem::path& templateName) { + mDefines["list"] = "1,2,3"; + const auto shader = mManager.getShader(Files::pathToUnicodeString(templateName), mDefines); + ASSERT_TRUE(shader); + const std::string expected + = "#version 120\n" + " varying vec4 foo1;\n" + " varying vec4 foo2;\n" + " varying vec4 foo3;\n" + "\n" + "#line 5\n" + "void main()\n" + "{\n" + "#ifdef BAR\n" + " foo1 = vec4(1.0);\n" + " foo2 = vec4(1.0);\n" + " foo3 = vec4(1.0);\n" + "\n" + "#line 11\n" + "#elif BAZ\n" + "#line 12\n" + " foo1 = vec4(2.0);\n" + " foo2 = vec4(2.0);\n" + " foo3 = vec4(2.0);\n" + "\n" + "#line 15\n" + "#else\n" + "#line 16\n" + " foo1 = vec4(3.0);\n" + " foo2 = vec4(3.0);\n" + " foo3 = vec4(3.0);\n" + "\n" + "#line 19\n" + "#endif\n" + "#line 20\n" + "}\n"; + EXPECT_EQ(shader->getShaderSource(), expected); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_fail_on_absent_template_parameters_in_single_line_comments) + { + const std::string content + = "#version 120\n" + "// #define FLAG @flag\n" + "void main() {}\n"; + + withShaderFile(content, [&](const std::filesystem::path& templateName) { + EXPECT_FALSE(mManager.getShader(Files::pathToUnicodeString(templateName), mDefines)); + }); + } + + TEST_F(ShaderManagerTest, get_shader_should_fail_on_absent_template_parameter_in_multi_line_comments) + { + const std::string content + = "#version 120\n" + "/* #define FLAG @flag */\n" + "void main() {}\n"; + + withShaderFile(content, [&](const std::filesystem::path& templateName) { + EXPECT_FALSE(mManager.getShader(Files::pathToUnicodeString(templateName), mDefines)); + }); + } +} diff --git a/apps/components_tests/sqlite3/db.cpp b/apps/components_tests/sqlite3/db.cpp new file mode 100644 index 00000000000..d2c0cd86749 --- /dev/null +++ b/apps/components_tests/sqlite3/db.cpp @@ -0,0 +1,15 @@ +#include + +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + TEST(Sqlite3DbTest, makeDbShouldCreateInMemoryDbWithSchema) + { + const auto db = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + EXPECT_NE(db, nullptr); + } +} diff --git a/apps/components_tests/sqlite3/request.cpp b/apps/components_tests/sqlite3/request.cpp new file mode 100644 index 00000000000..c299493952b --- /dev/null +++ b/apps/components_tests/sqlite3/request.cpp @@ -0,0 +1,275 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + template + struct InsertInt + { + static std::string_view text() noexcept { return "INSERT INTO ints (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, T value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertReal + { + static std::string_view text() noexcept { return "INSERT INTO reals (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, double value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertText + { + static std::string_view text() noexcept { return "INSERT INTO texts (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, std::string_view value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertBlob + { + static std::string_view text() noexcept { return "INSERT INTO blobs (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, const std::vector& value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetAll + { + std::string mQuery; + + explicit GetAll(const std::string& table) + : mQuery("SELECT value FROM " + table + " ORDER BY value") + { + } + + std::string_view text() noexcept { return mQuery; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + template + struct GetExact + { + std::string mQuery; + + explicit GetExact(const std::string& table) + : mQuery("SELECT value FROM " + table + " WHERE value = :value") + { + } + + std::string_view text() noexcept { return mQuery; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, const T& value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetInt64 + { + static std::string_view text() noexcept { return "SELECT value FROM ints WHERE value = :value"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, std::int64_t value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetNull + { + static std::string_view text() noexcept { return "SELECT NULL"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct Int + { + int mValue = 0; + + Int() = default; + + explicit Int(int value) + : mValue(value) + { + } + + Int& operator=(int value) + { + mValue = value; + return *this; + } + + friend bool operator==(const Int& l, const Int& r) { return l.mValue == r.mValue; } + }; + + constexpr const char schema[] = R"( + CREATE TABLE ints ( value INTEGER ); + CREATE TABLE reals ( value REAL ); + CREATE TABLE texts ( value TEXT ); + CREATE TABLE blobs ( value BLOB ); + )"; + + struct Sqlite3RequestTest : Test + { + const Db mDb = makeDb(":memory:", schema); + }; + + TEST_F(Sqlite3RequestTest, executeShouldSupportInt) + { + Statement insert(*mDb, InsertInt{}); + EXPECT_EQ(execute(*mDb, insert, 13), 1); + EXPECT_EQ(execute(*mDb, insert, 42), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(13), std::tuple(42))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportInt64) + { + Statement insert(*mDb, InsertInt{}); + const std::int64_t value = 1099511627776; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportReal) + { + Statement insert(*mDb, InsertReal{}); + EXPECT_EQ(execute(*mDb, insert, 3.14), 1); + Statement select(*mDb, GetAll("reals")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(3.14))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportText) + { + Statement insert(*mDb, InsertText{}); + const std::string text = "foo"; + EXPECT_EQ(execute(*mDb, insert, text), 1); + Statement select(*mDb, GetAll("texts")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(text))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportBlob) + { + Statement insert(*mDb, InsertBlob{}); + const std::vector blob({ std::byte(42), std::byte(13) }); + EXPECT_EQ(execute(*mDb, insert, blob), 1); + Statement select(*mDb, GetAll("blobs")); + std::vector>> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(blob))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportInt) + { + Statement insert(*mDb, InsertInt{}); + const int value = 42; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportInt64) + { + Statement insert(*mDb, InsertInt{}); + const std::int64_t value = 1099511627776; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportReal) + { + Statement insert(*mDb, InsertReal{}); + const double value = 3.14; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("reals")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportText) + { + Statement insert(*mDb, InsertText{}); + const std::string text = "foo"; + EXPECT_EQ(execute(*mDb, insert, text), 1); + Statement select(*mDb, GetExact("texts")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), text); + EXPECT_THAT(result, ElementsAre(std::tuple(text))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportBlob) + { + Statement insert(*mDb, InsertBlob{}); + const std::vector blob({ std::byte(42), std::byte(13) }); + EXPECT_EQ(execute(*mDb, insert, blob), 1); + Statement select(*mDb, GetExact>("blobs")); + std::vector>> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), blob); + EXPECT_THAT(result, ElementsAre(std::tuple(blob))); + } + + TEST_F(Sqlite3RequestTest, requestResultShouldSupportNull) + { + Statement select(*mDb, GetNull{}); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(nullptr))); + } + + TEST_F(Sqlite3RequestTest, requestResultShouldSupportConstructibleFromInt) + { + Statement insert(*mDb, InsertInt{}); + const int value = 42; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(Int(value)))); + } + + TEST_F(Sqlite3RequestTest, requestShouldLimitOutput) + { + Statement insert(*mDb, InsertInt{}); + EXPECT_EQ(execute(*mDb, insert, 13), 1); + EXPECT_EQ(execute(*mDb, insert, 42), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), 1); + EXPECT_THAT(result, ElementsAre(std::tuple(13))); + } +} diff --git a/apps/components_tests/sqlite3/statement.cpp b/apps/components_tests/sqlite3/statement.cpp new file mode 100644 index 00000000000..6dae43fe174 --- /dev/null +++ b/apps/components_tests/sqlite3/statement.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + struct Query + { + static std::string_view text() noexcept { return "SELECT 1"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + TEST(Sqlite3StatementTest, makeStatementShouldCreateStatementWithPreparedQuery) + { + const auto db = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + const Statement statement(*db, Query{}); + EXPECT_FALSE(statement.mNeedReset); + EXPECT_NE(statement.mHandle, nullptr); + EXPECT_EQ(statement.mQuery.text(), "SELECT 1"); + } +} diff --git a/apps/components_tests/sqlite3/transaction.cpp b/apps/components_tests/sqlite3/transaction.cpp new file mode 100644 index 00000000000..c994273b5e5 --- /dev/null +++ b/apps/components_tests/sqlite3/transaction.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + struct InsertId + { + static std::string_view text() noexcept { return "INSERT INTO test (id) VALUES (42)"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct GetIds + { + static std::string_view text() noexcept { return "SELECT id FROM test"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct Sqlite3TransactionTest : Test + { + const Db mDb = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + + void insertId() const + { + Statement insertId(*mDb, InsertId{}); + EXPECT_EQ(execute(*mDb, insertId), 1); + } + + std::vector> getIds() const + { + Statement getIds(*mDb, GetIds{}); + std::vector> result; + request(*mDb, getIds, std::back_inserter(result), std::numeric_limits::max()); + return result; + } + }; + + TEST_F(Sqlite3TransactionTest, shouldRollbackOnDestruction) + { + { + const Transaction transaction(*mDb); + insertId(); + } + EXPECT_THAT(getIds(), IsEmpty()); + } + + TEST_F(Sqlite3TransactionTest, commitShouldCommitTransaction) + { + { + Transaction transaction(*mDb); + insertId(); + transaction.commit(); + } + EXPECT_THAT(getIds(), ElementsAre(std::tuple(42))); + } +} diff --git a/components/to_utf8/tests/test_data/french-utf8.txt b/apps/components_tests/toutf8/data/french-utf8.txt similarity index 100% rename from components/to_utf8/tests/test_data/french-utf8.txt rename to apps/components_tests/toutf8/data/french-utf8.txt diff --git a/components/to_utf8/tests/test_data/french-win1252.txt b/apps/components_tests/toutf8/data/french-win1252.txt similarity index 100% rename from components/to_utf8/tests/test_data/french-win1252.txt rename to apps/components_tests/toutf8/data/french-win1252.txt diff --git a/components/to_utf8/tests/test_data/russian-utf8.txt b/apps/components_tests/toutf8/data/russian-utf8.txt similarity index 100% rename from components/to_utf8/tests/test_data/russian-utf8.txt rename to apps/components_tests/toutf8/data/russian-utf8.txt diff --git a/components/to_utf8/tests/test_data/russian-win1251.txt b/apps/components_tests/toutf8/data/russian-win1251.txt similarity index 100% rename from components/to_utf8/tests/test_data/russian-win1251.txt rename to apps/components_tests/toutf8/data/russian-win1251.txt diff --git a/apps/components_tests/toutf8/toutf8.cpp b/apps/components_tests/toutf8/toutf8.cpp new file mode 100644 index 00000000000..704ee6742de --- /dev/null +++ b/apps/components_tests/toutf8/toutf8.cpp @@ -0,0 +1,166 @@ +#include +#include + +#include + +#include +#include + +#ifndef OPENMW_PROJECT_SOURCE_DIR +#define OPENMW_PROJECT_SOURCE_DIR "." +#endif + +namespace +{ + using namespace testing; + using namespace ToUTF8; + + struct Params + { + FromType mLegacyEncoding; + std::string mLegacyEncodingFileName; + std::string mUtf8FileName; + }; + + std::string readContent(const std::string& fileName) + { + std::ifstream file; + file.exceptions(std::ios::failbit | std::ios::badbit); + file.open(std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "apps" / "components_tests" / "toutf8" / "data" + / Misc::StringUtils::stringToU8String(fileName)); + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); + } + + struct Utf8EncoderTest : TestWithParam + { + }; + + TEST(Utf8EncoderTest, getUtf8ShouldReturnEmptyAsIs) + { + Utf8Encoder encoder(FromType::CP437); + EXPECT_EQ(encoder.getUtf8(std::string_view()), std::string_view()); + } + + TEST(Utf8EncoderTest, getUtf8ShouldReturnAsciiOnlyAsIs) + { + std::string input; + for (int c = 1; c <= std::numeric_limits::max(); ++c) + input.push_back(static_cast(c)); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getUtf8(input); + EXPECT_EQ(result.data(), input.data()); + EXPECT_EQ(result.size(), input.size()); + } + + TEST(Utf8EncoderTest, getUtf8ShouldLookUpUntilZero) + { + const std::string input("a\0b"); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getUtf8(input); + EXPECT_EQ(result, "a"); + } + + TEST(Utf8EncoderTest, getUtf8ShouldLookUpUntilEndOfInputForAscii) + { + const std::string input("abc"); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getUtf8(std::string_view(input.data(), 2)); + EXPECT_EQ(result, "ab"); + } + + TEST(Utf8EncoderTest, getUtf8ShouldLookUpUntilEndOfInputForNonAscii) + { + const std::string input( + "a\x92" + "b"); + Utf8Encoder encoder(FromType::WINDOWS_1252); + const std::string_view result = encoder.getUtf8(std::string_view(input.data(), 2)); + EXPECT_EQ(result, "a\xE2\x80\x99"); + } + + TEST_P(Utf8EncoderTest, getUtf8ShouldConvertFromLegacyEncodingToUtf8) + { + const std::string input(readContent(GetParam().mLegacyEncodingFileName)); + const std::string expected(readContent(GetParam().mUtf8FileName)); + Utf8Encoder encoder(GetParam().mLegacyEncoding); + const std::string_view result = encoder.getUtf8(input); + EXPECT_EQ(result, expected); + } + + TEST(Utf8EncoderTest, getLegacyEncShouldReturnEmptyAsIs) + { + Utf8Encoder encoder(FromType::CP437); + EXPECT_EQ(encoder.getLegacyEnc(std::string_view()), std::string_view()); + } + + TEST(Utf8EncoderTest, getLegacyEncShouldReturnAsciiOnlyAsIs) + { + std::string input; + for (int c = 1; c <= std::numeric_limits::max(); ++c) + input.push_back(static_cast(c)); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getLegacyEnc(input); + EXPECT_EQ(result.data(), input.data()); + EXPECT_EQ(result.size(), input.size()); + } + + TEST(Utf8EncoderTest, getLegacyEncShouldLookUpUntilZero) + { + const std::string input("a\0b"); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getLegacyEnc(input); + EXPECT_EQ(result, "a"); + } + + TEST(Utf8EncoderTest, getLegacyEncShouldLookUpUntilEndOfInputForAscii) + { + const std::string input("abc"); + Utf8Encoder encoder(FromType::CP437); + const std::string_view result = encoder.getLegacyEnc(std::string_view(input.data(), 2)); + EXPECT_EQ(result, "ab"); + } + + TEST(Utf8EncoderTest, getLegacyEncShouldStripIncompleteCharacters) + { + const std::string input("a\xc3\xa2\xe2\x80\x99"); + Utf8Encoder encoder(FromType::WINDOWS_1252); + const std::string_view result = encoder.getLegacyEnc(std::string_view(input.data(), 5)); + EXPECT_EQ(result, "a\xe2"); + } + + TEST_P(Utf8EncoderTest, getLegacyEncShouldConvertFromUtf8ToLegacyEncoding) + { + const std::string input(readContent(GetParam().mUtf8FileName)); + const std::string expected(readContent(GetParam().mLegacyEncodingFileName)); + Utf8Encoder encoder(GetParam().mLegacyEncoding); + const std::string_view result = encoder.getLegacyEnc(input); + EXPECT_EQ(result, expected); + } + + INSTANTIATE_TEST_SUITE_P(Files, Utf8EncoderTest, + Values(Params{ ToUTF8::WINDOWS_1251, "russian-win1251.txt", "russian-utf8.txt" }, + Params{ ToUTF8::WINDOWS_1252, "french-win1252.txt", "french-utf8.txt" })); + + TEST(StatelessUtf8EncoderTest, shouldCleanupBuffer) + { + std::string buffer; + StatelessUtf8Encoder encoder(FromType::WINDOWS_1252); + encoder.getUtf8(std::string_view("long string\x92"), BufferAllocationPolicy::UseGrowFactor, buffer); + const std::string shortString("short\x92"); + ASSERT_GT(buffer.size(), shortString.size()); + const std::string_view shortUtf8 = encoder.getUtf8(shortString, BufferAllocationPolicy::UseGrowFactor, buffer); + ASSERT_GE(buffer.size(), shortUtf8.size()); + EXPECT_EQ(buffer[shortUtf8.size()], '\0') << buffer; + } + + TEST(StatelessUtf8EncoderTest, withFitToRequiredSizeShouldResizeBuffer) + { + std::string buffer; + StatelessUtf8Encoder encoder(FromType::WINDOWS_1252); + const std::string_view utf8 + = encoder.getUtf8(std::string_view("long string\x92"), BufferAllocationPolicy::FitToRequiredSize, buffer); + EXPECT_EQ(buffer.size(), utf8.size()); + } +} diff --git a/apps/components_tests/vfs/testpathutil.cpp b/apps/components_tests/vfs/testpathutil.cpp new file mode 100644 index 00000000000..3819f9905ad --- /dev/null +++ b/apps/components_tests/vfs/testpathutil.cpp @@ -0,0 +1,201 @@ +#include + +#include + +#include + +namespace VFS::Path +{ + namespace + { + using namespace testing; + + TEST(NormalizedTest, shouldSupportDefaultConstructor) + { + const Normalized value; + EXPECT_EQ(value.value(), ""); + } + + TEST(NormalizedTest, shouldSupportConstructorFromString) + { + const std::string string("Foo\\Bar/baz"); + const Normalized value(string); + EXPECT_EQ(value.value(), "foo/bar/baz"); + } + + TEST(NormalizedTest, shouldSupportConstructorFromConstCharPtr) + { + const char* const ptr = "Foo\\Bar/baz"; + const Normalized value(ptr); + EXPECT_EQ(value.value(), "foo/bar/baz"); + } + + TEST(NormalizedTest, shouldSupportConstructorFromStringView) + { + const std::string_view view = "Foo\\Bar/baz"; + const Normalized value(view); + EXPECT_EQ(value.view(), "foo/bar/baz"); + } + + TEST(NormalizedTest, shouldSupportConstructorFromNormalizedView) + { + const NormalizedView view("foo/bar/baz"); + const Normalized value(view); + EXPECT_EQ(value.view(), "foo/bar/baz"); + } + + TEST(NormalizedTest, supportMovingValueOut) + { + Normalized value("Foo\\Bar/baz"); + EXPECT_EQ(std::move(value).value(), "foo/bar/baz"); + EXPECT_EQ(value.value(), ""); + } + + TEST(NormalizedTest, isNotEqualToNotNormalized) + { + const Normalized value("Foo\\Bar/baz"); + EXPECT_NE(value.value(), "Foo\\Bar/baz"); + } + + TEST(NormalizedTest, shouldSupportOperatorLeftShiftToOStream) + { + const Normalized value("Foo\\Bar/baz"); + std::stringstream stream; + stream << value; + EXPECT_EQ(stream.str(), "foo/bar/baz"); + } + + TEST(NormalizedTest, shouldSupportOperatorDivEqual) + { + Normalized value("foo/bar"); + value /= NormalizedView("baz"); + EXPECT_EQ(value.value(), "foo/bar/baz"); + } + + TEST(NormalizedTest, shouldSupportOperatorDivEqualWithStringView) + { + Normalized value("foo/bar"); + value /= std::string_view("BAZ"); + EXPECT_EQ(value.value(), "foo/bar/baz"); + } + + TEST(NormalizedTest, changeExtensionShouldReplaceAfterLastDot) + { + Normalized value("foo/bar.a"); + ASSERT_TRUE(value.changeExtension("so")); + EXPECT_EQ(value.value(), "foo/bar.so"); + } + + TEST(NormalizedTest, changeExtensionShouldNormalizeExtension) + { + Normalized value("foo/bar.a"); + ASSERT_TRUE(value.changeExtension("SO")); + EXPECT_EQ(value.value(), "foo/bar.so"); + } + + TEST(NormalizedTest, changeExtensionShouldIgnorePathWithoutADot) + { + Normalized value("foo/bar"); + ASSERT_FALSE(value.changeExtension("so")); + EXPECT_EQ(value.value(), "foo/bar"); + } + + TEST(NormalizedTest, changeExtensionShouldIgnorePathWithDotBeforeSeparator) + { + Normalized value("foo.bar/baz"); + ASSERT_FALSE(value.changeExtension("so")); + EXPECT_EQ(value.value(), "foo.bar/baz"); + } + + TEST(NormalizedTest, changeExtensionShouldThrowExceptionOnExtensionWithDot) + { + Normalized value("foo.a"); + EXPECT_THROW(value.changeExtension(".so"), std::invalid_argument); + } + + TEST(NormalizedTest, changeExtensionShouldThrowExceptionOnExtensionWithSeparator) + { + Normalized value("foo.a"); + EXPECT_THROW(value.changeExtension("/so"), std::invalid_argument); + } + + template + struct NormalizedOperatorsTest : Test + { + }; + + TYPED_TEST_SUITE_P(NormalizedOperatorsTest); + + TYPED_TEST_P(NormalizedOperatorsTest, supportsEqual) + { + using Type0 = typename TypeParam::Type0; + using Type1 = typename TypeParam::Type1; + const Type0 normalized{ "a/foo/bar/baz" }; + const Type1 otherEqual{ "a/foo/bar/baz" }; + const Type1 otherNotEqual{ "b/foo/bar/baz" }; + EXPECT_EQ(normalized, otherEqual); + EXPECT_EQ(otherEqual, normalized); + EXPECT_NE(normalized, otherNotEqual); + EXPECT_NE(otherNotEqual, normalized); + } + + TYPED_TEST_P(NormalizedOperatorsTest, supportsLess) + { + using Type0 = typename TypeParam::Type0; + using Type1 = typename TypeParam::Type1; + const Type0 normalized{ "b/foo/bar/baz" }; + const Type1 otherEqual{ "b/foo/bar/baz" }; + const Type1 otherLess{ "a/foo/bar/baz" }; + const Type1 otherGreater{ "c/foo/bar/baz" }; + EXPECT_FALSE(normalized < otherEqual); + EXPECT_FALSE(otherEqual < normalized); + EXPECT_LT(otherLess, normalized); + EXPECT_FALSE(normalized < otherLess); + EXPECT_LT(normalized, otherGreater); + EXPECT_FALSE(otherGreater < normalized); + } + + REGISTER_TYPED_TEST_SUITE_P(NormalizedOperatorsTest, supportsEqual, supportsLess); + + template + struct TypePair + { + using Type0 = T0; + using Type1 = T1; + }; + + using TypePairs = Types, TypePair, + TypePair, TypePair, + TypePair, TypePair, + TypePair, TypePair, + TypePair, TypePair>; + + INSTANTIATE_TYPED_TEST_SUITE_P(Typed, NormalizedOperatorsTest, TypePairs); + + TEST(NormalizedViewTest, shouldSupportConstructorFromNormalized) + { + const Normalized value("Foo\\Bar/baz"); + const NormalizedView view(value); + EXPECT_EQ(view.value(), "foo/bar/baz"); + } + + TEST(NormalizedViewTest, shouldSupportConstexprConstructorFromNormalizedStringLiteral) + { + constexpr NormalizedView view("foo/bar/baz"); + EXPECT_EQ(view.value(), "foo/bar/baz"); + } + + TEST(NormalizedViewTest, constructorShouldThrowExceptionOnNotNormalized) + { + EXPECT_THROW([] { NormalizedView("Foo\\Bar/baz"); }(), std::invalid_argument); + } + + TEST(NormalizedView, shouldSupportOperatorDiv) + { + const NormalizedView a("foo/bar"); + const NormalizedView b("baz"); + const Normalized result = a / b; + EXPECT_EQ(result.value(), "foo/bar/baz"); + } + } +} diff --git a/apps/esmtool/CMakeLists.txt b/apps/esmtool/CMakeLists.txt index 122ca2f3af5..6dd592a4fee 100644 --- a/apps/esmtool/CMakeLists.txt +++ b/apps/esmtool/CMakeLists.txt @@ -1,23 +1,34 @@ set(ESMTOOL - esmtool.cpp - labels.hpp - labels.cpp - record.hpp - record.cpp + esmtool.cpp + labels.hpp + labels.cpp + record.hpp + record.cpp + arguments.hpp + tes4.hpp + tes4.cpp ) source_group(apps\\esmtool FILES ${ESMTOOL}) # Main executable openmw_add_executable(esmtool - ${ESMTOOL} + ${ESMTOOL} ) target_link_libraries(esmtool - ${Boost_PROGRAM_OPTIONS_LIBRARY} - components + Boost::program_options + components ) if (BUILD_WITH_CODE_COVERAGE) - add_definitions (--coverage) - target_link_libraries(esmtool gcov) + target_compile_options(esmtool PRIVATE --coverage) + target_link_libraries(esmtool gcov) +endif() + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(esmtool PRIVATE + + + + ) endif() diff --git a/apps/esmtool/arguments.hpp b/apps/esmtool/arguments.hpp new file mode 100644 index 00000000000..296b6970897 --- /dev/null +++ b/apps/esmtool/arguments.hpp @@ -0,0 +1,29 @@ +#ifndef OPENMW_ESMTOOL_ARGUMENTS_H +#define OPENMW_ESMTOOL_ARGUMENTS_H + +#include +#include +#include + +#include + +namespace EsmTool +{ + struct Arguments + { + std::optional mRawFormat; + bool quiet_given = false; + bool loadcells_given = false; + bool plain_given = false; + + std::string mode; + std::string encoding; + std::filesystem::path filename; + std::filesystem::path outname; + + std::vector types; + std::string name; + }; +} + +#endif diff --git a/apps/esmtool/esmtool.cpp b/apps/esmtool/esmtool.cpp index 60483f98130..0473676f934 100644 --- a/apps/esmtool/esmtool.cpp +++ b/apps/esmtool/esmtool.cpp @@ -1,220 +1,205 @@ -#include -#include +#include #include -#include -#include -#include -#include #include -#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include "arguments.hpp" +#include "labels.hpp" #include "record.hpp" +#include "tes4.hpp" -#define ESMTOOL_VERSION 1.2 - -// Create a local alias for brevity -namespace bpo = boost::program_options; - -struct ESMData -{ - std::string author; - std::string description; - unsigned int version; - std::vector masters; - - std::deque mRecords; - // Value: (Reference, Deleted flag) - std::map > > mCellRefs; - std::map mRecordStats; - - static const std::set sLabeledRec; -}; - -static const int sLabeledRecIds[] = { - ESM::REC_GLOB, ESM::REC_CLAS, ESM::REC_FACT, ESM::REC_RACE, ESM::REC_SOUN, - ESM::REC_REGN, ESM::REC_BSGN, ESM::REC_LTEX, ESM::REC_STAT, ESM::REC_DOOR, - ESM::REC_MISC, ESM::REC_WEAP, ESM::REC_CONT, ESM::REC_SPEL, ESM::REC_CREA, - ESM::REC_BODY, ESM::REC_LIGH, ESM::REC_ENCH, ESM::REC_NPC_, ESM::REC_ARMO, - ESM::REC_CLOT, ESM::REC_REPA, ESM::REC_ACTI, ESM::REC_APPA, ESM::REC_LOCK, - ESM::REC_PROB, ESM::REC_INGR, ESM::REC_BOOK, ESM::REC_ALCH, ESM::REC_LEVI, - ESM::REC_LEVC, ESM::REC_SNDG, ESM::REC_CELL, ESM::REC_DIAL -}; - -const std::set ESMData::sLabeledRec = - std::set(sLabeledRecIds, sLabeledRecIds + 34); - -// Based on the legacy struct -struct Arguments +namespace { - bool raw_given; - bool quiet_given; - bool loadcells_given; - bool plain_given; - std::string mode; - std::string encoding; - std::string filename; - std::string outname; + using namespace EsmTool; - std::vector types; - std::string name; + constexpr unsigned majorVersion = 1; + constexpr unsigned minorVersion = 3; - ESMData data; - ESM::ESMReader reader; - ESM::ESMWriter writer; -}; + // Create a local alias for brevity + namespace bpo = boost::program_options; -bool parseOptions (int argc, char** argv, Arguments &info) -{ - bpo::options_description desc("Inspect and extract from Morrowind ES files (ESM, ESP, ESS)\nSyntax: esmtool [options] mode infile [outfile]\nAllowed modes:\n dump\t Dumps all readable data from the input file.\n clone\t Clones the input file to the output file.\n comp\t Compares the given files.\n\nAllowed options"); - - desc.add_options() - ("help,h", "print help message.") - ("version,v", "print version information and quit.") - ("raw,r", "Show an unformatted list of all records and subrecords.") + struct ESMData + { + ESM::Header mHeader; + std::deque> mRecords; + // Value: (Reference, Deleted flag) + std::map>> mCellRefs; + std::map mRecordStats; + }; + + bool parseOptions(int argc, char** argv, Arguments& info) + { + bpo::options_description desc(R"(Inspect and extract from Morrowind ES files (ESM, ESP, ESS) +Syntax: esmtool [options] mode infile [outfile] +Allowed modes: + dump Dumps all readable data from the input file. + clone Clones the input file to the output file. + comp Compares the given files. + +Allowed options)"); + auto addOption = desc.add_options(); + addOption("help,h", "print help message."); + addOption("version,v", "print version information and quit."); + addOption("raw,r", bpo::value(), + "Show an unformatted list of all records and subrecords of given format:\n" + "\n\tTES3" + "\n\tTES4"); // The intention is that this option would interact better // with other modes including clone, dump, and raw. - ("type,t", bpo::value< std::vector >(), - "Show only records of this type (four character record code). May " - "be specified multiple times. Only affects dump mode.") - ("name,n", bpo::value(), - "Show only the record with this name. Only affects dump mode.") - ("plain,p", "Print contents of dialogs, books and scripts. " - "(skipped by default)" - "Only affects dump mode.") - ("quiet,q", "Suppress all record information. Useful for speed tests.") - ("loadcells,C", "Browse through contents of all cells.") - - ( "encoding,e", bpo::value(&(info.encoding))-> - default_value("win1252"), - "Character encoding used in ESMTool:\n" - "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" - "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" - "\n\twin1252 - Western European (Latin) alphabet, used by default") - ; - - std::string finalText = "\nIf no option is given, the default action is to parse all records in the archive\nand display diagnostic information."; - - // input-file is hidden and used as a positional argument - bpo::options_description hidden("Hidden Options"); - - hidden.add_options() - ( "mode,m", bpo::value(), "esmtool mode") - ( "input-file,i", bpo::value< std::vector >(), "input file") - ; - - bpo::positional_options_description p; - p.add("mode", 1).add("input-file", 2); - - // there might be a better way to do this - bpo::options_description all; - all.add(desc).add(hidden); - bpo::variables_map variables; + addOption("type,t", bpo::value>(), + "Show only records of this type (four character record code). May " + "be specified multiple times. Only affects dump mode."); + addOption("name,n", bpo::value(), "Show only the record with this name. Only affects dump mode."); + addOption("plain,p", + "Print contents of dialogs, books and scripts. " + "(skipped by default) " + "Only affects dump mode."); + addOption("quiet,q", "Suppress all record information. Useful for speed tests."); + addOption("loadcells,C", "Browse through contents of all cells."); + + addOption("encoding,e", bpo::value(&(info.encoding))->default_value("win1252"), + "Character encoding used in ESMTool:\n" + "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, " + "Croatian, Serbian (Latin script), Romanian and Albanian languages\n" + "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" + "\n\twin1252 - Western European (Latin) alphabet, used by default"); + + std::string finalText + = "\nIf no option is given, the default action is to parse all records in the archive\nand display " + "diagnostic information."; + + // input-file is hidden and used as a positional argument + bpo::options_description hidden("Hidden Options"); + auto addHiddenOption = hidden.add_options(); + addHiddenOption("mode,m", bpo::value(), "esmtool mode"); + addHiddenOption("input-file,i", bpo::value(), "input file"); + + bpo::positional_options_description p; + p.add("mode", 1).add("input-file", 2); + + // there might be a better way to do this + bpo::options_description all; + all.add(desc).add(hidden); + bpo::variables_map variables; + + try + { + bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv).options(all).positional(p).run(); - try - { - bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv) - .options(all).positional(p).run(); + bpo::store(valid_opts, variables); + } + catch (std::exception& e) + { + std::cout << "ERROR parsing arguments: " << e.what() << std::endl; + return false; + } - bpo::store(valid_opts, variables); - } - catch(std::exception &e) - { - std::cout << "ERROR parsing arguments: " << e.what() << std::endl; - return false; - } + bpo::notify(variables); - bpo::notify(variables); + if (variables.count("help")) + { + std::cout << desc << finalText << std::endl; + return false; + } + if (variables.count("version")) + { + std::cout << "ESMTool version " << majorVersion << '.' << minorVersion << std::endl; + return false; + } + if (!variables.count("mode")) + { + std::cout << "No mode specified!\n\n" << desc << finalText << std::endl; + return false; + } - if (variables.count ("help")) - { - std::cout << desc << finalText << std::endl; - return false; - } - if (variables.count ("version")) - { - std::cout << "ESMTool version " << ESMTOOL_VERSION << std::endl; - return false; - } - if (!variables.count("mode")) - { - std::cout << "No mode specified!" << std::endl << std::endl - << desc << finalText << std::endl; - return false; - } + if (variables.count("type") > 0) + info.types = variables["type"].as>(); + if (variables.count("name") > 0) + info.name = variables["name"].as(); - if (variables.count("type") > 0) - info.types = variables["type"].as< std::vector >(); - if (variables.count("name") > 0) - info.name = variables["name"].as(); + info.mode = variables["mode"].as(); + if (!(info.mode == "dump" || info.mode == "clone" || info.mode == "comp")) + { + std::cout << "\nERROR: invalid mode \"" << info.mode << "\"\n\n" << desc << finalText << std::endl; + return false; + } - info.mode = variables["mode"].as(); - if (!(info.mode == "dump" || info.mode == "clone" || info.mode == "comp")) - { - std::cout << std::endl << "ERROR: invalid mode \"" << info.mode << "\"" << std::endl << std::endl - << desc << finalText << std::endl; - return false; - } + if (!variables.count("input-file")) + { + std::cout << "\nERROR: missing ES file\n\n"; + std::cout << desc << finalText << std::endl; + return false; + } - if ( !variables.count("input-file") ) - { - std::cout << "\nERROR: missing ES file\n\n"; - std::cout << desc << finalText << std::endl; - return false; - } + // handling gracefully the user adding multiple files + /* if (variables["input-file"].as< std::vector >().size() > 1) + { + std::cout << "\nERROR: more than one ES file specified\n\n"; + std::cout << desc << finalText << std::endl; + return false; + }*/ + + const auto& inputFiles = variables["input-file"].as(); + info.filename = inputFiles[0].u8string(); // This call to u8string is redundant, but required to build on + // MSVC 14.26 due to implementation bugs. + if (inputFiles.size() > 1) + info.outname = inputFiles[1].u8string(); // This call to u8string is redundant, but required to build on + // MSVC 14.26 due to implementation bugs. + + if (const auto it = variables.find("raw"); it != variables.end()) + info.mRawFormat = ESM::parseFormat(it->second.as()); + + info.quiet_given = variables.count("quiet") != 0; + info.loadcells_given = variables.count("loadcells") != 0; + info.plain_given = variables.count("plain") != 0; + + // Font encoding settings + info.encoding = variables["encoding"].as(); + if (info.encoding != "win1250" && info.encoding != "win1251" && info.encoding != "win1252") + { + std::cout << info.encoding << " is not a valid encoding option.\n"; + info.encoding = "win1252"; + } + std::cout << ToUTF8::encodingUsingMessage(info.encoding) << std::endl; - // handling gracefully the user adding multiple files -/* if (variables["input-file"].as< std::vector >().size() > 1) - { - std::cout << "\nERROR: more than one ES file specified\n\n"; - std::cout << desc << finalText << std::endl; - return false; - }*/ - - info.filename = variables["input-file"].as< std::vector >()[0]; - if (variables["input-file"].as< std::vector >().size() > 1) - info.outname = variables["input-file"].as< std::vector >()[1]; - - info.raw_given = variables.count ("raw") != 0; - info.quiet_given = variables.count ("quiet") != 0; - info.loadcells_given = variables.count ("loadcells") != 0; - info.plain_given = variables.count("plain") != 0; - - // Font encoding settings - info.encoding = variables["encoding"].as(); - if(info.encoding != "win1250" && info.encoding != "win1251" && info.encoding != "win1252") - { - std::cout << info.encoding << " is not a valid encoding option." << std::endl; - info.encoding = "win1252"; + return true; } - std::cout << ToUTF8::encodingUsingMessage(info.encoding) << std::endl; - return true; -} + void loadCell(const Arguments& info, ESM::Cell& cell, ESM::ESMReader& esm, ESMData* data); -void printRaw(ESM::ESMReader &esm); -void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info); + int load(const Arguments& info, ESMData* data); + int clone(const Arguments& info); + int comp(const Arguments& info); -int load(Arguments& info); -int clone(Arguments& info); -int comp(Arguments& info); +} -int main(int argc, char**argv) +int main(int argc, char** argv) { try { Arguments info; - if(!parseOptions (argc, argv, info)) + if (!parseOptions(argc, argv, info)) return 1; if (info.mode == "dump") - return load(info); + return load(info, nullptr); else if (info.mode == "clone") return clone(info); else if (info.mode == "comp") @@ -234,340 +219,383 @@ int main(int argc, char**argv) return 0; } -void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info) +namespace { - bool quiet = (info.quiet_given || info.mode == "clone"); - bool save = (info.mode == "clone"); - // Skip back to the beginning of the reference list - // FIXME: Changes to the references backend required to support multiple plugins have - // almost certainly broken this following line. I'll leave it as is for now, so that - // the compiler does not complain. - cell.restore(esm, 0); + void loadCell(const Arguments& info, ESM::Cell& cell, ESM::ESMReader& esm, ESMData* data) + { + bool quiet = (info.quiet_given || info.mode == "clone"); + bool save = (info.mode == "clone"); - // Loop through all the references - ESM::CellRef ref; - if(!quiet) std::cout << " References:\n"; + // Skip back to the beginning of the reference list + // FIXME: Changes to the references backend required to support multiple plugins have + // almost certainly broken this following line. I'll leave it as is for now, so that + // the compiler does not complain. + cell.restore(esm, 0); - bool deleted = false; - while(cell.getNextRef(esm, ref, deleted)) - { - if (save) { - info.data.mCellRefs[&cell].push_back(std::make_pair(ref, deleted)); - } + // Loop through all the references + ESM::CellRef ref; + if (!quiet) + std::cout << " References:\n"; - if(quiet) continue; - - std::cout << " Refnum: " << ref.mRefNum.mIndex << std::endl; - std::cout << " ID: " << ref.mRefID << std::endl; - std::cout << " Position: (" << ref.mPos.pos[0] << ", " << ref.mPos.pos[1] << ", " << ref.mPos.pos[2] << ")" << std::endl; - if (ref.mScale != 1.f) - std::cout << " Scale: " << ref.mScale << std::endl; - if (!ref.mOwner.empty()) - std::cout << " Owner: " << ref.mOwner << std::endl; - if (!ref.mGlobalVariable.empty()) - std::cout << " Global: " << ref.mGlobalVariable << std::endl; - if (!ref.mFaction.empty()) - std::cout << " Faction: " << ref.mFaction << std::endl; - if (!ref.mFaction.empty() || ref.mFactionRank != -2) - std::cout << " Faction rank: " << ref.mFactionRank << std::endl; - std::cout << " Enchantment charge: " << ref.mEnchantmentCharge << std::endl; - std::cout << " Uses/health: " << ref.mChargeInt << std::endl; - std::cout << " Gold value: " << ref.mGoldValue << std::endl; - std::cout << " Blocked: " << static_cast(ref.mReferenceBlocked) << std::endl; - std::cout << " Deleted: " << deleted << std::endl; - if (!ref.mKey.empty()) - std::cout << " Key: " << ref.mKey << std::endl; - std::cout << " Lock level: " << ref.mLockLevel << std::endl; - if (!ref.mTrap.empty()) - std::cout << " Trap: " << ref.mTrap << std::endl; - if (!ref.mSoul.empty()) - std::cout << " Soul: " << ref.mSoul << std::endl; - if (ref.mTeleport) + bool deleted = false; + ESM::MovedCellRef movedCellRef; + bool moved = false; + while (cell.getNextRef(esm, ref, deleted, movedCellRef, moved)) { - std::cout << " Destination position: (" << ref.mDoorDest.pos[0] << ", " - << ref.mDoorDest.pos[1] << ", " << ref.mDoorDest.pos[2] << ")" << std::endl; - if (!ref.mDestCell.empty()) - std::cout << " Destination cell: " << ref.mDestCell << std::endl; + if (data != nullptr && save) + data->mCellRefs[&cell].push_back(std::make_pair(ref, deleted)); + + if (quiet) + continue; + + std::cout << " - Refnum: " << ref.mRefNum.mIndex << '\n'; + std::cout << " ID: " << ref.mRefID << '\n'; + std::cout << " Position: (" << ref.mPos.pos[0] << ", " << ref.mPos.pos[1] << ", " << ref.mPos.pos[2] + << ")\n"; + if (ref.mScale != 1.f) + std::cout << " Scale: " << ref.mScale << '\n'; + if (!ref.mOwner.empty()) + std::cout << " Owner: " << ref.mOwner << '\n'; + if (!ref.mGlobalVariable.empty()) + std::cout << " Global: " << ref.mGlobalVariable << '\n'; + if (!ref.mFaction.empty()) + std::cout << " Faction: " << ref.mFaction << '\n'; + if (!ref.mFaction.empty() || ref.mFactionRank != -2) + std::cout << " Faction rank: " << ref.mFactionRank << '\n'; + std::cout << " Enchantment charge: " << ref.mEnchantmentCharge << '\n'; + std::cout << " Uses/health: " << ref.mChargeInt << '\n'; + std::cout << " Count: " << ref.mCount << '\n'; + std::cout << " Blocked: " << static_cast(ref.mReferenceBlocked) << '\n'; + std::cout << " Deleted: " << deleted << '\n'; + if (!ref.mKey.empty()) + std::cout << " Key: " << ref.mKey << '\n'; + std::cout << " Lock level: " << ref.mLockLevel << '\n'; + if (!ref.mTrap.empty()) + std::cout << " Trap: " << ref.mTrap << '\n'; + if (!ref.mSoul.empty()) + std::cout << " Soul: " << ref.mSoul << '\n'; + if (ref.mTeleport) + { + std::cout << " Destination position: (" << ref.mDoorDest.pos[0] << ", " << ref.mDoorDest.pos[1] + << ", " << ref.mDoorDest.pos[2] << ")\n"; + if (!ref.mDestCell.empty()) + std::cout << " Destination cell: " << ref.mDestCell << '\n'; + } + std::cout << " Moved: " << std::boolalpha << moved << std::noboolalpha << '\n'; + if (moved) + { + std::cout << " Moved refnum: " << movedCellRef.mRefNum.mIndex << '\n'; + std::cout << " Moved content file: " << movedCellRef.mRefNum.mContentFile << '\n'; + std::cout << " Target: " << movedCellRef.mTarget[0] << ", " << movedCellRef.mTarget[1] << '\n'; + } } } -} -void printRaw(ESM::ESMReader &esm) -{ - while(esm.hasMoreRecs()) + void printRawTes3(const std::filesystem::path& path) { - ESM::NAME n = esm.getRecName(); - std::cout << "Record: " << n.toString() << std::endl; - esm.getRecHeader(); - while(esm.hasMoreSubs()) + std::cout << "TES3 RAW file listing: " << Files::pathToUnicodeString(path) << '\n'; + ESM::ESMReader esm; + esm.openRaw(path); + while (esm.hasMoreRecs()) { - size_t offs = esm.getFileOffset(); - esm.getSubName(); - esm.skipHSub(); - n = esm.retSubName(); - std::ios::fmtflags f(std::cout.flags()); - std::cout << " " << n.toString() << " - " << esm.getSubSize() - << " bytes @ 0x" << std::hex << offs << "\n"; - std::cout.flags(f); + ESM::NAME n = esm.getRecName(); + std::cout << "Record: " << n.toStringView() << '\n'; + esm.getRecHeader(); + while (esm.hasMoreSubs()) + { + size_t offs = esm.getFileOffset(); + esm.getSubName(); + esm.skipHSub(); + n = esm.retSubName(); + std::ios::fmtflags f(std::cout.flags()); + std::cout << " " << n.toStringView() << " - " << esm.getSubSize() << " bytes @ 0x" << std::hex + << offs << '\n'; + std::cout.flags(f); + } } } -} - -int load(Arguments& info) -{ - ESM::ESMReader& esm = info.reader; - ToUTF8::Utf8Encoder encoder (ToUTF8::calculateEncoding(info.encoding)); - esm.setEncoder(&encoder); - std::string filename = info.filename; - std::cout << "Loading file: " << filename << std::endl; + int loadTes3(const Arguments& info, std::unique_ptr&& stream, ESMData* data) + { + std::cout << "Loading TES3 file: " << info.filename << '\n'; - std::unordered_set skipped; + ESM::ESMReader esm; + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(info.encoding)); + esm.setEncoder(&encoder); - try { + std::unordered_set skipped; - if(info.raw_given && info.mode == "dump") + try { - std::cout << "RAW file listing:\n"; - - esm.openRaw(filename); - - printRaw(esm); - - return 0; - } - - bool quiet = (info.quiet_given || info.mode == "clone"); - bool loadCells = (info.loadcells_given || info.mode == "clone"); - bool save = (info.mode == "clone"); + bool quiet = (info.quiet_given || info.mode == "clone"); + bool loadCells = (info.loadcells_given || info.mode == "clone"); + bool save = (info.mode == "clone"); - esm.open(filename); + esm.open(std::move(stream), info.filename); - info.data.author = esm.getAuthor(); - info.data.description = esm.getDesc(); - info.data.masters = esm.getGameFiles(); + if (data != nullptr) + data->mHeader = esm.getHeader(); - if (!quiet) - { - std::cout << "Author: " << esm.getAuthor() << std::endl - << "Description: " << esm.getDesc() << std::endl - << "File format version: " << esm.getFVer() << std::endl; - std::vector masterData = esm.getGameFiles(); - if (!masterData.empty()) + if (!quiet) { - std::cout << "Masters:" << std::endl; - for(const auto& master : masterData) - std::cout << " " << master.name << ", " << master.size << " bytes" << std::endl; + std::cout << "Author: " << esm.getAuthor() << '\n' + << "Description: " << esm.getDesc() << '\n' + << "File format version: " << esm.esmVersionF() << '\n'; + std::vector masterData = esm.getGameFiles(); + if (!masterData.empty()) + { + std::cout << "Masters:" << '\n'; + for (const auto& master : masterData) + std::cout << " " << master.name << ", " << master.size << " bytes\n"; + } } - } - - // Loop through all records - while(esm.hasMoreRecs()) - { - const ESM::NAME n = esm.getRecName(); - uint32_t flags; - esm.getRecHeader(flags); - EsmTool::RecordBase *record = EsmTool::RecordBase::create(n); - if (record == nullptr) + // Loop through all records + while (esm.hasMoreRecs()) { - if (skipped.count(n.intval) == 0) + const ESM::NAME n = esm.getRecName(); + uint32_t flags; + esm.getRecHeader(flags); + + auto record = EsmTool::RecordBase::create(n); + if (record == nullptr) { - std::cout << "Skipping " << n.toString() << " records." << std::endl; - skipped.emplace(n.intval); + if (!quiet && skipped.count(n.toInt()) == 0) + { + std::cout << "Skipping " << n.toStringView() << " records.\n"; + skipped.emplace(n.toInt()); + } + + esm.skipRecord(); + if (quiet) + break; + std::cout << " Skipping\n"; + + continue; } - esm.skipRecord(); - if (quiet) break; - std::cout << " Skipping\n"; + record->setFlags(static_cast(flags)); + record->setPrintPlain(info.plain_given); + record->load(esm); - continue; - } + // Is the user interested in this record type? + bool interested = true; + if (!info.types.empty() + && std::find(info.types.begin(), info.types.end(), n.toStringView()) == info.types.end()) + interested = false; - record->setFlags(static_cast(flags)); - record->setPrintPlain(info.plain_given); - record->load(esm); + if (!info.name.empty() && !Misc::StringUtils::ciEqual(info.name, record->getId())) + interested = false; - // Is the user interested in this record type? - bool interested = true; - if (!info.types.empty()) - { - std::vector::iterator match; - match = std::find(info.types.begin(), info.types.end(), n.toString()); - if (match == info.types.end()) interested = false; - } + if (!quiet && interested) + { + std::cout << "\nRecord: " << n.toStringView() << " " << record->getId() << "\n" + << "Record flags: " << recordFlags(record->getFlags()) << '\n'; + record->print(); + } - if (!info.name.empty() && !Misc::StringUtils::ciEqual(info.name, record->getId())) - interested = false; + if (record->getType().toInt() == ESM::REC_CELL && loadCells && interested) + { + loadCell(info, record->cast()->get(), esm, data); + } - if(!quiet && interested) - { - std::cout << "\nRecord: " << n.toString() << " '" << record->getId() << "'\n"; - record->print(); + if (data != nullptr) + { + if (save) + data->mRecords.push_back(std::move(record)); + ++data->mRecordStats[n.toInt()]; + } } + } + catch (const std::exception& e) + { + std::cout << "\nERROR:\n\n " << e.what() << std::endl; + if (data != nullptr) + data->mRecords.clear(); + return 1; + } - if (record->getType().intval == ESM::REC_CELL && loadCells && interested) - { - loadCell(record->cast()->get(), esm, info); - } + return 0; + } - if (save) - { - info.data.mRecords.push_back(record); - } - else + int load(const Arguments& info, ESMData* data) + { + if (info.mRawFormat.has_value() && info.mode == "dump") + { + switch (*info.mRawFormat) { - delete record; + case ESM::Format::Tes3: + printRawTes3(info.filename); + break; + case ESM::Format::Tes4: + std::cout << "Printing raw TES4 file is not supported: " + << Files::pathToUnicodeString(info.filename) << "\n"; + break; } - ++info.data.mRecordStats[n.intval]; + return 0; } - } catch(std::exception &e) { - std::cout << "\nERROR:\n\n " << e.what() << std::endl; + auto stream = Files::openBinaryInputFileStream(info.filename); + if (!stream->is_open()) + { + std::cout << "Failed to open file " << info.filename << ": " << std::generic_category().message(errno) + << '\n'; + return -1; + } - for (const EsmTool::RecordBase* record : info.data.mRecords) - delete record; + const ESM::Format format = ESM::readFormat(*stream); + stream->seekg(0); - info.data.mRecords.clear(); - return 1; - } - - return 0; -} + switch (format) + { + case ESM::Format::Tes3: + return loadTes3(info, std::move(stream), data); + case ESM::Format::Tes4: + if (data != nullptr) + { + std::cout << "Collecting data from esm file is not supported for TES4\n"; + return -1; + } + return loadTes4(info, std::move(stream)); + } -#include + std::cout << "Unsupported ESM format: " << ESM::NAME(format).toStringView() << '\n'; -int clone(Arguments& info) -{ - if (info.outname.empty()) - { - std::cout << "You need to specify an output name" << std::endl; - return 1; + return -1; } - if (load(info) != 0) + int clone(const Arguments& info) { - std::cout << "Failed to load, aborting." << std::endl; - return 1; - } + if (info.outname.empty()) + { + std::cout << "You need to specify an output name" << std::endl; + return 1; + } - size_t recordCount = info.data.mRecords.size(); + ESMData data; + if (load(info, &data) != 0) + { + std::cout << "Failed to load, aborting." << std::endl; + return 1; + } - int digitCount = 1; // For a nicer output - if (recordCount > 0) - digitCount = (int)std::log10(recordCount) + 1; + size_t recordCount = data.mRecords.size(); - std::cout << "Loaded " << recordCount << " records:" << std::endl << std::endl; + int digitCount = 1; // For a nicer output + if (recordCount > 0) + digitCount = (int)std::log10(recordCount) + 1; - int i = 0; - for (std::pair stat : info.data.mRecordStats) - { - ESM::NAME name; - name.intval = stat.first; - int amount = stat.second; - std::cout << std::setw(digitCount) << amount << " " << name.toString() << " "; - if (++i % 3 == 0) - std::cout << std::endl; - } + std::cout << "Loaded " << recordCount << " records:\n\n"; - if (i % 3 != 0) - std::cout << std::endl; + int i = 0; + for (std::pair stat : data.mRecordStats) + { + ESM::NAME name; + name = stat.first; + int amount = stat.second; + std::cout << std::setw(digitCount) << amount << " " << name.toStringView() << " "; + if (++i % 3 == 0) + std::cout << '\n'; + } - std::cout << std::endl << "Saving records to: " << info.outname << "..." << std::endl; + if (i % 3 != 0) + std::cout << '\n'; - ESM::ESMWriter& esm = info.writer; - ToUTF8::Utf8Encoder encoder (ToUTF8::calculateEncoding(info.encoding)); - esm.setEncoder(&encoder); - esm.setAuthor(info.data.author); - esm.setDescription(info.data.description); - esm.setVersion(info.data.version); - esm.setRecordCount (recordCount); + std::cout << "\nSaving records to: " << Files::pathToUnicodeString(info.outname) << "...\n"; - for (const ESM::Header::MasterData &master : info.data.masters) - esm.addMaster(master.name, master.size); + ESM::ESMWriter esm; + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(info.encoding)); + esm.setEncoder(&encoder); + esm.setHeader(data.mHeader); + esm.setVersion(ESM::VER_130); + esm.setRecordCount(recordCount); - std::fstream save(info.outname.c_str(), std::fstream::out | std::fstream::binary); - esm.save(save); + std::fstream save(info.outname, std::fstream::out | std::fstream::binary); + esm.save(save); - int saved = 0; - for (EsmTool::RecordBase* record : info.data.mRecords) - { - if (i <= 0) - break; + int saved = 0; + for (auto& record : data.mRecords) + { + if (i <= 0) + break; - const ESM::NAME& typeName = record->getType(); + const ESM::NAME typeName = record->getType(); - esm.startRecord(typeName.toString(), record->getFlags()); + esm.startRecord(typeName, record->getFlags()); - record->save(esm); - if (typeName.intval == ESM::REC_CELL) { - ESM::Cell *ptr = &record->cast()->get(); - if (!info.data.mCellRefs[ptr].empty()) + record->save(esm); + if (typeName.toInt() == ESM::REC_CELL) { - for (std::pair &ref : info.data.mCellRefs[ptr]) - ref.first.save(esm, ref.second); + ESM::Cell* ptr = &record->cast()->get(); + if (!data.mCellRefs[ptr].empty()) + { + for (std::pair& ref : data.mCellRefs[ptr]) + ref.first.save(esm, ref.second); + } } - } - esm.endRecord(typeName.toString()); + esm.endRecord(typeName); - saved++; - int perc = recordCount == 0 ? 100 : (int)((saved / (float)recordCount)*100); - if (perc % 10 == 0) - { - std::cerr << "\r" << perc << "%"; + saved++; + int perc = recordCount == 0 ? 100 : (int)((saved / (float)recordCount) * 100); + if (perc % 10 == 0) + { + std::cerr << "\r" << perc << "%"; + } } - } - std::cout << "\rDone!" << std::endl; + std::cout << "\rDone!" << std::endl; - esm.close(); - save.close(); + esm.close(); + save.close(); - return 0; -} + return 0; + } -int comp(Arguments& info) -{ - if (info.filename.empty() || info.outname.empty()) + int comp(const Arguments& info) { - std::cout << "You need to specify two input files" << std::endl; - return 1; - } + if (info.filename.empty() || info.outname.empty()) + { + std::cout << "You need to specify two input files" << std::endl; + return 1; + } - Arguments fileOne; - Arguments fileTwo; + Arguments fileOne; + Arguments fileTwo; - fileOne.raw_given = false; - fileTwo.raw_given = false; + fileOne.mode = "clone"; + fileTwo.mode = "clone"; - fileOne.mode = "clone"; - fileTwo.mode = "clone"; + fileOne.encoding = info.encoding; + fileTwo.encoding = info.encoding; - fileOne.encoding = info.encoding; - fileTwo.encoding = info.encoding; + fileOne.filename = info.filename; + fileTwo.filename = info.outname; - fileOne.filename = info.filename; - fileTwo.filename = info.outname; + ESMData dataOne; + if (load(fileOne, &dataOne) != 0) + { + std::cout << "Failed to load " << Files::pathToUnicodeString(info.filename) << ", aborting comparison." + << std::endl; + return 1; + } - if (load(fileOne) != 0) - { - std::cout << "Failed to load " << info.filename << ", aborting comparison." << std::endl; - return 1; - } + ESMData dataTwo; + if (load(fileTwo, &dataTwo) != 0) + { + std::cout << "Failed to load " << Files::pathToUnicodeString(info.outname) << ", aborting comparison." + << std::endl; + return 1; + } - if (load(fileTwo) != 0) - { - std::cout << "Failed to load " << info.outname << ", aborting comparison." << std::endl; - return 1; - } + if (dataOne.mRecords.size() != dataTwo.mRecords.size()) + { + std::cout << "Not equal, different amount of records." << std::endl; + return 1; + } - if (fileOne.data.mRecords.size() != fileTwo.data.mRecords.size()) - { - std::cout << "Not equal, different amount of records." << std::endl; - return 1; + return 0; } - return 0; } diff --git a/apps/esmtool/labels.cpp b/apps/esmtool/labels.cpp index 24e3605eb2a..3d645639233 100644 --- a/apps/esmtool/labels.cpp +++ b/apps/esmtool/labels.cpp @@ -1,25 +1,28 @@ #include "labels.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -std::string bodyPartLabel(int idx) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +std::string_view bodyPartLabel(int idx) { if (idx >= 0 && idx <= 26) { - static const char *bodyPartLabels[] = { + static constexpr std::string_view bodyPartLabels[] = { "Head", "Hair", "Neck", @@ -46,7 +49,7 @@ std::string bodyPartLabel(int idx) "Right Shoulder", "Left Shoulder", "Weapon", - "Tail" + "Tail", }; return bodyPartLabels[idx]; } @@ -54,11 +57,11 @@ std::string bodyPartLabel(int idx) return "Invalid"; } -std::string meshPartLabel(int idx) +std::string_view meshPartLabel(int idx) { if (idx >= 0 && idx <= ESM::BodyPart::MP_Tail) { - static const char *meshPartLabels[] = { + static constexpr std::string_view meshPartLabels[] = { "Head", "Hair", "Neck", @@ -73,7 +76,7 @@ std::string meshPartLabel(int idx) "Knee", "Upper Leg", "Clavicle", - "Tail" + "Tail", }; return meshPartLabels[idx]; } @@ -81,14 +84,14 @@ std::string meshPartLabel(int idx) return "Invalid"; } -std::string meshTypeLabel(int idx) +std::string_view meshTypeLabel(int idx) { if (idx >= 0 && idx <= ESM::BodyPart::MT_Armor) { - static const char *meshTypeLabels[] = { + static constexpr std::string_view meshTypeLabels[] = { "Skin", "Clothing", - "Armor" + "Armor", }; return meshTypeLabels[idx]; } @@ -96,11 +99,11 @@ std::string meshTypeLabel(int idx) return "Invalid"; } -std::string clothingTypeLabel(int idx) +std::string_view clothingTypeLabel(int idx) { if (idx >= 0 && idx <= 9) { - static const char *clothingTypeLabels[] = { + static constexpr std::string_view clothingTypeLabels[] = { "Pants", "Shoes", "Shirt", @@ -110,7 +113,7 @@ std::string clothingTypeLabel(int idx) "Left Glove", "Skirt", "Ring", - "Amulet" + "Amulet", }; return clothingTypeLabels[idx]; } @@ -118,11 +121,11 @@ std::string clothingTypeLabel(int idx) return "Invalid"; } -std::string armorTypeLabel(int idx) +std::string_view armorTypeLabel(int idx) { if (idx >= 0 && idx <= 10) { - static const char *armorTypeLabels[] = { + static constexpr std::string_view armorTypeLabels[] = { "Helmet", "Cuirass", "Left Pauldron", @@ -133,7 +136,7 @@ std::string armorTypeLabel(int idx) "Right Gauntlet", "Shield", "Left Bracer", - "Right Bracer" + "Right Bracer", }; return armorTypeLabels[idx]; } @@ -141,16 +144,16 @@ std::string armorTypeLabel(int idx) return "Invalid"; } -std::string dialogTypeLabel(int idx) +std::string_view dialogTypeLabel(int idx) { if (idx >= 0 && idx <= 4) { - static const char *dialogTypeLabels[] = { + static constexpr std::string_view dialogTypeLabels[] = { "Topic", "Voice", "Greeting", "Persuasion", - "Journal" + "Journal", }; return dialogTypeLabels[idx]; } @@ -160,16 +163,16 @@ std::string dialogTypeLabel(int idx) return "Invalid"; } -std::string questStatusLabel(int idx) +std::string_view questStatusLabel(int idx) { if (idx >= 0 && idx <= 4) { - static const char *questStatusLabels[] = { + static constexpr std::string_view questStatusLabels[] = { "None", "Name", "Finished", "Restart", - "Deleted" + "Deleted", }; return questStatusLabels[idx]; } @@ -177,11 +180,11 @@ std::string questStatusLabel(int idx) return "Invalid"; } -std::string creatureTypeLabel(int idx) +std::string_view creatureTypeLabel(int idx) { if (idx >= 0 && idx <= 3) { - static const char *creatureTypeLabels[] = { + static constexpr std::string_view creatureTypeLabels[] = { "Creature", "Daedra", "Undead", @@ -193,11 +196,11 @@ std::string creatureTypeLabel(int idx) return "Invalid"; } -std::string soundTypeLabel(int idx) +std::string_view soundTypeLabel(int idx) { if (idx >= 0 && idx <= 7) { - static const char *soundTypeLabels[] = { + static constexpr std::string_view soundTypeLabels[] = { "Left Foot", "Right Foot", "Swim Left", @@ -205,7 +208,7 @@ std::string soundTypeLabel(int idx) "Moan", "Roar", "Scream", - "Land" + "Land", }; return soundTypeLabels[idx]; } @@ -213,11 +216,11 @@ std::string soundTypeLabel(int idx) return "Invalid"; } -std::string weaponTypeLabel(int idx) +std::string_view weaponTypeLabel(int idx) { if (idx >= 0 && idx <= 13) { - static const char *weaponTypeLabels[] = { + static constexpr std::string_view weaponTypeLabels[] = { "Short Blade One Hand", "Long Blade One Hand", "Long Blade Two Hand", @@ -231,7 +234,7 @@ std::string weaponTypeLabel(int idx) "Marksman Crossbow", "Marksman Thrown", "Arrow", - "Bolt" + "Bolt", }; return weaponTypeLabels[idx]; } @@ -239,21 +242,29 @@ std::string weaponTypeLabel(int idx) return "Invalid"; } -std::string aiTypeLabel(int type) +std::string_view aiTypeLabel(ESM::AiPackageType type) { - if (type == ESM::AI_Wander) return "Wander"; - else if (type == ESM::AI_Travel) return "Travel"; - else if (type == ESM::AI_Follow) return "Follow"; - else if (type == ESM::AI_Escort) return "Escort"; - else if (type == ESM::AI_Activate) return "Activate"; - else return "Invalid"; + switch (type) + { + case ESM::AI_Wander: + return "Wander"; + case ESM::AI_Travel: + return "Travel"; + case ESM::AI_Follow: + return "Follow"; + case ESM::AI_Escort: + return "Escort"; + case ESM::AI_Activate: + return "Activate"; + } + return "Invalid"; } -std::string magicEffectLabel(int idx) +std::string_view magicEffectLabel(int idx) { if (idx >= 0 && idx <= 142) { - const char* magicEffectLabels [] = { + static constexpr std::string_view magicEffectLabels[] = { "Water Breathing", "Swift Swim", "Water Walking", @@ -396,7 +407,7 @@ std::string magicEffectLabel(int idx) "sEffectSummonCreature02", "sEffectSummonCreature03", "sEffectSummonCreature04", - "sEffectSummonCreature05" + "sEffectSummonCreature05", }; return magicEffectLabels[idx]; } @@ -404,11 +415,11 @@ std::string magicEffectLabel(int idx) return "Invalid"; } -std::string attributeLabel(int idx) +std::string_view attributeLabel(int idx) { if (idx >= 0 && idx <= 7) { - const char* attributeLabels [] = { + static constexpr std::string_view attributeLabels[] = { "Strength", "Intelligence", "Willpower", @@ -416,7 +427,7 @@ std::string attributeLabel(int idx) "Speed", "Endurance", "Personality", - "Luck" + "Luck", }; return attributeLabels[idx]; } @@ -424,17 +435,17 @@ std::string attributeLabel(int idx) return "Invalid"; } -std::string spellTypeLabel(int idx) +std::string_view spellTypeLabel(int idx) { if (idx >= 0 && idx <= 5) { - const char* spellTypeLabels [] = { + static constexpr std::string_view spellTypeLabels[] = { "Spells", "Abilities", "Blight Disease", "Disease", "Curse", - "Powers" + "Powers", }; return spellTypeLabels[idx]; } @@ -442,14 +453,14 @@ std::string spellTypeLabel(int idx) return "Invalid"; } -std::string specializationLabel(int idx) +std::string_view specializationLabel(int idx) { if (idx >= 0 && idx <= 2) { - const char* specializationLabels [] = { + static constexpr std::string_view specializationLabels[] = { "Combat", "Magic", - "Stealth" + "Stealth", }; return specializationLabels[idx]; } @@ -457,11 +468,11 @@ std::string specializationLabel(int idx) return "Invalid"; } -std::string skillLabel(int idx) +std::string_view skillLabel(int idx) { if (idx >= 0 && idx <= 26) { - const char* skillLabels [] = { + static constexpr std::string_view skillLabels[] = { "Block", "Armorer", "Medium Armor", @@ -488,7 +499,7 @@ std::string skillLabel(int idx) "Marksman", "Mercantile", "Speechcraft", - "Hand-to-hand" + "Hand-to-hand", }; return skillLabels[idx]; } @@ -496,11 +507,11 @@ std::string skillLabel(int idx) return "Invalid"; } -std::string apparatusTypeLabel(int idx) +std::string_view apparatusTypeLabel(int idx) { if (idx >= 0 && idx <= 3) { - const char* apparatusTypeLabels [] = { + static constexpr std::string_view apparatusTypeLabels[] = { "Mortar", "Alembic", "Calcinator", @@ -512,14 +523,14 @@ std::string apparatusTypeLabel(int idx) return "Invalid"; } -std::string rangeTypeLabel(int idx) +std::string_view rangeTypeLabel(int idx) { if (idx >= 0 && idx <= 2) { - const char* rangeTypeLabels [] = { + static constexpr std::string_view rangeTypeLabels[] = { "Self", "Touch", - "Target" + "Target", }; return rangeTypeLabels[idx]; } @@ -527,17 +538,17 @@ std::string rangeTypeLabel(int idx) return "Invalid"; } -std::string schoolLabel(int idx) +std::string_view schoolLabel(int idx) { if (idx >= 0 && idx <= 5) { - const char* schoolLabels [] = { + static constexpr std::string_view schoolLabels[] = { "Alteration", "Conjuration", "Destruction", "Illusion", "Mysticism", - "Restoration" + "Restoration", }; return schoolLabels[idx]; } @@ -545,15 +556,15 @@ std::string schoolLabel(int idx) return "Invalid"; } -std::string enchantTypeLabel(int idx) +std::string_view enchantTypeLabel(int idx) { if (idx >= 0 && idx <= 3) { - const char* enchantTypeLabels [] = { + static constexpr std::string_view enchantTypeLabels[] = { "Cast Once", "Cast When Strikes", "Cast When Used", - "Constant Effect" + "Constant Effect", }; return enchantTypeLabels[idx]; } @@ -561,15 +572,16 @@ std::string enchantTypeLabel(int idx) return "Invalid"; } -std::string ruleFunction(int idx) +std::string_view ruleFunction(int idx) { - if (idx >= 0 && idx <= 72) + if (idx >= ESM::DialogueCondition::Function_FacReactionLowest + && idx <= ESM::DialogueCondition::Function_PcWerewolfKills) { - std::string ruleFunctions[] = { - "Reaction Low", - "Reaction High", + static constexpr std::string_view ruleFunctions[] = { + "Lowest Faction Reaction", + "Highest Faction Reaction", "Rank Requirement", - "NPC? Reputation", + "NPC Reputation", "Health Percent", "Player Reputation", "NPC Level", @@ -638,7 +650,8 @@ std::string ruleFunction(int idx) "Alarm", "Flee", "Should Attack", - "Werewolf" + "Werewolf", + "Werewolf Kills", }; return ruleFunctions[idx]; } @@ -653,13 +666,15 @@ std::string ruleFunction(int idx) std::string bodyPartFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; - if (flags & ESM::BodyPart::BPF_Female) properties += "Female "; - if (flags & ESM::BodyPart::BPF_NotPlayable) properties += "NotPlayable "; - int unused = (0xFFFFFFFF ^ - (ESM::BodyPart::BPF_Female| - ESM::BodyPart::BPF_NotPlayable)); - if (flags & unused) properties += "Invalid "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::BodyPart::BPF_Female) + properties += "Female "; + if (flags & ESM::BodyPart::BPF_NotPlayable) + properties += "NotPlayable "; + int unused = (0xFFFFFFFF ^ (ESM::BodyPart::BPF_Female | ESM::BodyPart::BPF_NotPlayable)); + if (flags & unused) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } @@ -667,20 +682,23 @@ std::string bodyPartFlags(int flags) std::string cellFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; - if (flags & ESM::Cell::HasWater) properties += "HasWater "; - if (flags & ESM::Cell::Interior) properties += "Interior "; - if (flags & ESM::Cell::NoSleep) properties += "NoSleep "; - if (flags & ESM::Cell::QuasiEx) properties += "QuasiEx "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::Cell::HasWater) + properties += "HasWater "; + if (flags & ESM::Cell::Interior) + properties += "Interior "; + if (flags & ESM::Cell::NoSleep) + properties += "NoSleep "; + if (flags & ESM::Cell::QuasiEx) + properties += "QuasiEx "; // This used value is not in the ESM component. - if (flags & 0x00000040) properties += "Unknown "; - int unused = (0xFFFFFFFF ^ - (ESM::Cell::HasWater| - ESM::Cell::Interior| - ESM::Cell::NoSleep| - ESM::Cell::QuasiEx| - 0x00000040)); - if (flags & unused) properties += "Invalid "; + if (flags & 0x00000040) + properties += "Unknown "; + int unused = (0xFFFFFFFF + ^ (ESM::Cell::HasWater | ESM::Cell::Interior | ESM::Cell::NoSleep | ESM::Cell::QuasiEx | 0x00000040)); + if (flags & unused) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } @@ -688,15 +706,17 @@ std::string cellFlags(int flags) std::string containerFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; - if (flags & ESM::Container::Unknown) properties += "Unknown "; - if (flags & ESM::Container::Organic) properties += "Organic "; - if (flags & ESM::Container::Respawn) properties += "Respawn "; - int unused = (0xFFFFFFFF ^ - (ESM::Container::Unknown| - ESM::Container::Organic| - ESM::Container::Respawn)); - if (flags & unused) properties += "Invalid "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::Container::Unknown) + properties += "Unknown "; + if (flags & ESM::Container::Organic) + properties += "Organic "; + if (flags & ESM::Container::Respawn) + properties += "Respawn "; + int unused = (0xFFFFFFFF ^ (ESM::Container::Unknown | ESM::Container::Organic | ESM::Container::Respawn)); + if (flags & unused) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } @@ -704,25 +724,29 @@ std::string containerFlags(int flags) std::string creatureFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; - if (flags & ESM::Creature::Base) properties += "Base "; - if (flags & ESM::Creature::Walks) properties += "Walks "; - if (flags & ESM::Creature::Swims) properties += "Swims "; - if (flags & ESM::Creature::Flies) properties += "Flies "; - if (flags & ESM::Creature::Bipedal) properties += "Bipedal "; - if (flags & ESM::Creature::Respawn) properties += "Respawn "; - if (flags & ESM::Creature::Weapon) properties += "Weapon "; - if (flags & ESM::Creature::Essential) properties += "Essential "; - int unused = (0xFFFFFFFF ^ - (ESM::Creature::Base| - ESM::Creature::Walks| - ESM::Creature::Swims| - ESM::Creature::Flies| - ESM::Creature::Bipedal| - ESM::Creature::Respawn| - ESM::Creature::Weapon| - ESM::Creature::Essential)); - if (flags & unused) properties += "Invalid "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::Creature::Base) + properties += "Base "; + if (flags & ESM::Creature::Walks) + properties += "Walks "; + if (flags & ESM::Creature::Swims) + properties += "Swims "; + if (flags & ESM::Creature::Flies) + properties += "Flies "; + if (flags & ESM::Creature::Bipedal) + properties += "Bipedal "; + if (flags & ESM::Creature::Respawn) + properties += "Respawn "; + if (flags & ESM::Creature::Weapon) + properties += "Weapon "; + if (flags & ESM::Creature::Essential) + properties += "Essential "; + int unused = (0xFFFFFFFF + ^ (ESM::Creature::Base | ESM::Creature::Walks | ESM::Creature::Swims | ESM::Creature::Flies + | ESM::Creature::Bipedal | ESM::Creature::Respawn | ESM::Creature::Weapon | ESM::Creature::Essential)); + if (flags & unused) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%02X)", flags); return properties; } @@ -730,24 +754,30 @@ std::string creatureFlags(int flags) std::string enchantmentFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; - if (flags & ESM::Enchantment::Autocalc) properties += "Autocalc "; - if (flags & (0xFFFFFFFF ^ ESM::Enchantment::Autocalc)) properties += "Invalid "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::Enchantment::Autocalc) + properties += "Autocalc "; + if (flags & (0xFFFFFFFF ^ ESM::Enchantment::Autocalc)) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } -std::string landFlags(int flags) +std::string landFlags(std::uint32_t flags) { std::string properties; - // The ESM component says that this first four bits are used, but - // only the first three bits are used as far as I can tell. - // There's also no enumeration of the bit in the ESM component. - if (flags == 0) properties += "[None] "; - if (flags & 0x00000001) properties += "Unknown1 "; - if (flags & 0x00000004) properties += "Unknown3 "; - if (flags & 0x00000002) properties += "Unknown2 "; - if (flags & 0xFFFFFFF8) properties += "Invalid "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::Land::Flag_HeightsNormals) + properties += "HeightsNormals "; + if (flags & ESM::Land::Flag_Colors) + properties += "Colors "; + if (flags & ESM::Land::Flag_Textures) + properties += "Textures "; + int unused = 0xFFFFFFFF ^ (ESM::Land::Flag_HeightsNormals | ESM::Land::Flag_Colors | ESM::Land::Flag_Textures); + if (flags & unused) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } @@ -755,13 +785,15 @@ std::string landFlags(int flags) std::string itemListFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; - if (flags & ESM::ItemLevList::AllLevels) properties += "AllLevels "; - if (flags & ESM::ItemLevList::Each) properties += "Each "; - int unused = (0xFFFFFFFF ^ - (ESM::ItemLevList::AllLevels| - ESM::ItemLevList::Each)); - if (flags & unused) properties += "Invalid "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::ItemLevList::AllLevels) + properties += "AllLevels "; + if (flags & ESM::ItemLevList::Each) + properties += "Each "; + int unused = (0xFFFFFFFF ^ (ESM::ItemLevList::AllLevels | ESM::ItemLevList::Each)); + if (flags & unused) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } @@ -769,10 +801,13 @@ std::string itemListFlags(int flags) std::string creatureListFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; - if (flags & ESM::CreatureLevList::AllLevels) properties += "AllLevels "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::CreatureLevList::AllLevels) + properties += "AllLevels "; int unused = (0xFFFFFFFF ^ ESM::CreatureLevList::AllLevels); - if (flags & unused) properties += "Invalid "; + if (flags & unused) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } @@ -780,27 +815,31 @@ std::string creatureListFlags(int flags) std::string lightFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; - if (flags & ESM::Light::Dynamic) properties += "Dynamic "; - if (flags & ESM::Light::Fire) properties += "Fire "; - if (flags & ESM::Light::Carry) properties += "Carry "; - if (flags & ESM::Light::Flicker) properties += "Flicker "; - if (flags & ESM::Light::FlickerSlow) properties += "FlickerSlow "; - if (flags & ESM::Light::Pulse) properties += "Pulse "; - if (flags & ESM::Light::PulseSlow) properties += "PulseSlow "; - if (flags & ESM::Light::Negative) properties += "Negative "; - if (flags & ESM::Light::OffDefault) properties += "OffDefault "; - int unused = (0xFFFFFFFF ^ - (ESM::Light::Dynamic| - ESM::Light::Fire| - ESM::Light::Carry| - ESM::Light::Flicker| - ESM::Light::FlickerSlow| - ESM::Light::Pulse| - ESM::Light::PulseSlow| - ESM::Light::Negative| - ESM::Light::OffDefault)); - if (flags & unused) properties += "Invalid "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::Light::Dynamic) + properties += "Dynamic "; + if (flags & ESM::Light::Fire) + properties += "Fire "; + if (flags & ESM::Light::Carry) + properties += "Carry "; + if (flags & ESM::Light::Flicker) + properties += "Flicker "; + if (flags & ESM::Light::FlickerSlow) + properties += "FlickerSlow "; + if (flags & ESM::Light::Pulse) + properties += "Pulse "; + if (flags & ESM::Light::PulseSlow) + properties += "PulseSlow "; + if (flags & ESM::Light::Negative) + properties += "Negative "; + if (flags & ESM::Light::OffDefault) + properties += "OffDefault "; + int unused = (0xFFFFFFFF + ^ (ESM::Light::Dynamic | ESM::Light::Fire | ESM::Light::Carry | ESM::Light::Flicker | ESM::Light::FlickerSlow + | ESM::Light::Pulse | ESM::Light::PulseSlow | ESM::Light::Negative | ESM::Light::OffDefault)); + if (flags & unused) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } @@ -808,27 +847,47 @@ std::string lightFlags(int flags) std::string magicEffectFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; - if (flags & ESM::MagicEffect::TargetAttribute) properties += "TargetAttribute "; - if (flags & ESM::MagicEffect::TargetSkill) properties += "TargetSkill "; - if (flags & ESM::MagicEffect::NoDuration) properties += "NoDuration "; - if (flags & ESM::MagicEffect::NoMagnitude) properties += "NoMagnitude "; - if (flags & ESM::MagicEffect::Harmful) properties += "Harmful "; - if (flags & ESM::MagicEffect::ContinuousVfx) properties += "ContinuousVFX "; - if (flags & ESM::MagicEffect::CastSelf) properties += "CastSelf "; - if (flags & ESM::MagicEffect::CastTouch) properties += "CastTouch "; - if (flags & ESM::MagicEffect::CastTarget) properties += "CastTarget "; - if (flags & ESM::MagicEffect::AppliedOnce) properties += "AppliedOnce "; - if (flags & ESM::MagicEffect::Stealth) properties += "Stealth "; - if (flags & ESM::MagicEffect::NonRecastable) properties += "NonRecastable "; - if (flags & ESM::MagicEffect::IllegalDaedra) properties += "IllegalDaedra "; - if (flags & ESM::MagicEffect::Unreflectable) properties += "Unreflectable "; - if (flags & ESM::MagicEffect::CasterLinked) properties += "CasterLinked "; - if (flags & ESM::MagicEffect::AllowSpellmaking) properties += "AllowSpellmaking "; - if (flags & ESM::MagicEffect::AllowEnchanting) properties += "AllowEnchanting "; - if (flags & ESM::MagicEffect::NegativeLight) properties += "NegativeLight "; - - if (flags & 0xFFFC0000) properties += "Invalid "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::MagicEffect::TargetAttribute) + properties += "TargetAttribute "; + if (flags & ESM::MagicEffect::TargetSkill) + properties += "TargetSkill "; + if (flags & ESM::MagicEffect::NoDuration) + properties += "NoDuration "; + if (flags & ESM::MagicEffect::NoMagnitude) + properties += "NoMagnitude "; + if (flags & ESM::MagicEffect::Harmful) + properties += "Harmful "; + if (flags & ESM::MagicEffect::ContinuousVfx) + properties += "ContinuousVFX "; + if (flags & ESM::MagicEffect::CastSelf) + properties += "CastSelf "; + if (flags & ESM::MagicEffect::CastTouch) + properties += "CastTouch "; + if (flags & ESM::MagicEffect::CastTarget) + properties += "CastTarget "; + if (flags & ESM::MagicEffect::AppliedOnce) + properties += "AppliedOnce "; + if (flags & ESM::MagicEffect::Stealth) + properties += "Stealth "; + if (flags & ESM::MagicEffect::NonRecastable) + properties += "NonRecastable "; + if (flags & ESM::MagicEffect::IllegalDaedra) + properties += "IllegalDaedra "; + if (flags & ESM::MagicEffect::Unreflectable) + properties += "Unreflectable "; + if (flags & ESM::MagicEffect::CasterLinked) + properties += "CasterLinked "; + if (flags & ESM::MagicEffect::AllowSpellmaking) + properties += "AllowSpellmaking "; + if (flags & ESM::MagicEffect::AllowEnchanting) + properties += "AllowEnchanting "; + if (flags & ESM::MagicEffect::NegativeLight) + properties += "NegativeLight "; + + if (flags & 0xFFFC0000) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } @@ -836,21 +895,24 @@ std::string magicEffectFlags(int flags) std::string npcFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; - if (flags & ESM::NPC::Base) properties += "Base "; - if (flags & ESM::NPC::Autocalc) properties += "Autocalc "; - if (flags & ESM::NPC::Female) properties += "Female "; - if (flags & ESM::NPC::Respawn) properties += "Respawn "; - if (flags & ESM::NPC::Essential) properties += "Essential "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::NPC::Base) + properties += "Base "; + if (flags & ESM::NPC::Autocalc) + properties += "Autocalc "; + if (flags & ESM::NPC::Female) + properties += "Female "; + if (flags & ESM::NPC::Respawn) + properties += "Respawn "; + if (flags & ESM::NPC::Essential) + properties += "Essential "; // Whether corpses persist is a bit that is unaccounted for, // however relatively few NPCs have this bit set. - int unused = (0xFF ^ - (ESM::NPC::Base| - ESM::NPC::Autocalc| - ESM::NPC::Female| - ESM::NPC::Respawn| - ESM::NPC::Essential)); - if (flags & unused) properties += "Invalid "; + int unused + = (0xFF ^ (ESM::NPC::Base | ESM::NPC::Autocalc | ESM::NPC::Female | ESM::NPC::Respawn | ESM::NPC::Essential)); + if (flags & unused) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%02X)", flags); return properties; } @@ -858,14 +920,16 @@ std::string npcFlags(int flags) std::string raceFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; + if (flags == 0) + properties += "[None] "; // All races have the playable flag set in Bethesda files. - if (flags & ESM::Race::Playable) properties += "Playable "; - if (flags & ESM::Race::Beast) properties += "Beast "; - int unused = (0xFFFFFFFF ^ - (ESM::Race::Playable| - ESM::Race::Beast)); - if (flags & unused) properties += "Invalid "; + if (flags & ESM::Race::Playable) + properties += "Playable "; + if (flags & ESM::Race::Beast) + properties += "Beast "; + int unused = (0xFFFFFFFF ^ (ESM::Race::Playable | ESM::Race::Beast)); + if (flags & unused) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } @@ -873,15 +937,17 @@ std::string raceFlags(int flags) std::string spellFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; - if (flags & ESM::Spell::F_Autocalc) properties += "Autocalc "; - if (flags & ESM::Spell::F_PCStart) properties += "PCStart "; - if (flags & ESM::Spell::F_Always) properties += "Always "; - int unused = (0xFFFFFFFF ^ - (ESM::Spell::F_Autocalc| - ESM::Spell::F_PCStart| - ESM::Spell::F_Always)); - if (flags & unused) properties += "Invalid "; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::Spell::F_Autocalc) + properties += "Autocalc "; + if (flags & ESM::Spell::F_PCStart) + properties += "PCStart "; + if (flags & ESM::Spell::F_Always) + properties += "Always "; + int unused = (0xFFFFFFFF ^ (ESM::Spell::F_Autocalc | ESM::Spell::F_PCStart | ESM::Spell::F_Always)); + if (flags & unused) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } @@ -889,16 +955,51 @@ std::string spellFlags(int flags) std::string weaponFlags(int flags) { std::string properties; - if (flags == 0) properties += "[None] "; + if (flags == 0) + properties += "[None] "; // The interpretation of the flags are still unclear to me. // Apparently you can't be Silver without being Magical? Many of // the "Magical" weapons don't have enchantments of any sort. - if (flags & ESM::Weapon::Magical) properties += "Magical "; - if (flags & ESM::Weapon::Silver) properties += "Silver "; - int unused = (0xFFFFFFFF ^ - (ESM::Weapon::Magical| - ESM::Weapon::Silver)); - if (flags & unused) properties += "Invalid "; + if (flags & ESM::Weapon::Magical) + properties += "Magical "; + if (flags & ESM::Weapon::Silver) + properties += "Silver "; + int unused = (0xFFFFFFFF ^ (ESM::Weapon::Magical | ESM::Weapon::Silver)); + if (flags & unused) + properties += "Invalid "; + properties += Misc::StringUtils::format("(0x%08X)", flags); + return properties; +} + +std::string recordFlags(uint32_t flags) +{ + std::string properties; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::FLAG_Deleted) + properties += "Deleted "; + if (flags & ESM::FLAG_Persistent) + properties += "Persistent "; + if (flags & ESM::FLAG_Ignored) + properties += "Ignored "; + if (flags & ESM::FLAG_Blocked) + properties += "Blocked "; + int unused = ~(ESM::FLAG_Deleted | ESM::FLAG_Persistent | ESM::FLAG_Ignored | ESM::FLAG_Blocked); + if (flags & unused) + properties += "Invalid "; + properties += Misc::StringUtils::format("(0x%08X)", flags); + return properties; +} + +std::string potionFlags(int flags) +{ + std::string properties; + if (flags == 0) + properties += "[None] "; + if (flags & ESM::Potion::Autocalc) + properties += "Autocalc "; + if (flags & (0xFFFFFFFF ^ ESM::Enchantment::Autocalc)) + properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } diff --git a/apps/esmtool/labels.hpp b/apps/esmtool/labels.hpp index b06480a97bb..c3a78141b4c 100644 --- a/apps/esmtool/labels.hpp +++ b/apps/esmtool/labels.hpp @@ -1,27 +1,31 @@ #ifndef OPENMW_ESMTOOL_LABELS_H #define OPENMW_ESMTOOL_LABELS_H +#include #include +#include -std::string bodyPartLabel(int idx); -std::string meshPartLabel(int idx); -std::string meshTypeLabel(int idx); -std::string clothingTypeLabel(int idx); -std::string armorTypeLabel(int idx); -std::string dialogTypeLabel(int idx); -std::string questStatusLabel(int idx); -std::string creatureTypeLabel(int idx); -std::string soundTypeLabel(int idx); -std::string weaponTypeLabel(int idx); +#include + +std::string_view bodyPartLabel(int idx); +std::string_view meshPartLabel(int idx); +std::string_view meshTypeLabel(int idx); +std::string_view clothingTypeLabel(int idx); +std::string_view armorTypeLabel(int idx); +std::string_view dialogTypeLabel(int idx); +std::string_view questStatusLabel(int idx); +std::string_view creatureTypeLabel(int idx); +std::string_view soundTypeLabel(int idx); +std::string_view weaponTypeLabel(int idx); // This function's a bit different because the types are record types, // not consecutive values. -std::string aiTypeLabel(int type); +std::string_view aiTypeLabel(ESM::AiPackageType type); // This one's also a bit different, because it enumerates dialog // select rule functions, not types. Structurally, it still converts // indexes to strings for display. -std::string ruleFunction(int idx); +std::string_view ruleFunction(int idx); // The labels below here can all be loaded from GMSTs, but are not // currently because among other things, that requires loading the @@ -32,15 +36,15 @@ std::string ruleFunction(int idx); // and the indexes for referencing the types in other records in the // database. Then a single label function could work for all types. -std::string magicEffectLabel(int idx); -std::string attributeLabel(int idx); -std::string spellTypeLabel(int idx); -std::string specializationLabel(int idx); -std::string skillLabel(int idx); -std::string apparatusTypeLabel(int idx); -std::string rangeTypeLabel(int idx); -std::string schoolLabel(int idx); -std::string enchantTypeLabel(int idx); +std::string_view magicEffectLabel(int idx); +std::string_view attributeLabel(int idx); +std::string_view spellTypeLabel(int idx); +std::string_view specializationLabel(int idx); +std::string_view skillLabel(int idx); +std::string_view apparatusTypeLabel(int idx); +std::string_view rangeTypeLabel(int idx); +std::string_view schoolLabel(int idx); +std::string_view enchantTypeLabel(int idx); // The are the flag functions that convert a bitmask into a list of // human readble strings representing the set bits. @@ -50,16 +54,19 @@ std::string cellFlags(int flags); std::string containerFlags(int flags); std::string creatureFlags(int flags); std::string enchantmentFlags(int flags); -std::string landFlags(int flags); +std::string landFlags(std::uint32_t flags); std::string creatureListFlags(int flags); std::string itemListFlags(int flags); std::string lightFlags(int flags); std::string magicEffectFlags(int flags); std::string npcFlags(int flags); +std::string potionFlags(int flags); std::string raceFlags(int flags); std::string spellFlags(int flags); std::string weaponFlags(int flags); +std::string recordFlags(uint32_t flags); + // Missing flags functions: // aiServicesFlags, possibly more diff --git a/apps/esmtool/record.cpp b/apps/esmtool/record.cpp index 55170e232ef..b9c64d964ad 100644 --- a/apps/esmtool/record.cpp +++ b/apps/esmtool/record.cpp @@ -2,1355 +2,1366 @@ #include "labels.hpp" #include +#include #include -#include +#include +#include +#include +#include namespace { -void printAIPackage(const ESM::AIPackage& p) -{ - std::cout << " AI Type: " << aiTypeLabel(p.mType) - << " (" << Misc::StringUtils::format("0x%08X", p.mType) << ")" << std::endl; - if (p.mType == ESM::AI_Wander) - { - std::cout << " Distance: " << p.mWander.mDistance << std::endl; - std::cout << " Duration: " << p.mWander.mDuration << std::endl; - std::cout << " Time of Day: " << (int)p.mWander.mTimeOfDay << std::endl; - if (p.mWander.mShouldRepeat != 1) - std::cout << " Should repeat: " << (bool)(p.mWander.mShouldRepeat != 0) << std::endl; - - std::cout << " Idle: "; - for (int i = 0; i != 8; i++) - std::cout << (int)p.mWander.mIdle[i] << " "; - std::cout << std::endl; - } - else if (p.mType == ESM::AI_Travel) - { - std::cout << " Travel Coordinates: (" << p.mTravel.mX << "," - << p.mTravel.mY << "," << p.mTravel.mZ << ")" << std::endl; - std::cout << " Travel Unknown: " << p.mTravel.mUnk << std::endl; - } - else if (p.mType == ESM::AI_Follow || p.mType == ESM::AI_Escort) + void printAIPackage(const ESM::AIPackage& p) { - std::cout << " Follow Coordinates: (" << p.mTarget.mX << "," - << p.mTarget.mY << "," << p.mTarget.mZ << ")" << std::endl; - std::cout << " Duration: " << p.mTarget.mDuration << std::endl; - std::cout << " Target ID: " << p.mTarget.mId.toString() << std::endl; - std::cout << " Unknown: " << p.mTarget.mUnk << std::endl; - } - else if (p.mType == ESM::AI_Activate) - { - std::cout << " Name: " << p.mActivate.mName.toString() << std::endl; - std::cout << " Activate Unknown: " << p.mActivate.mUnk << std::endl; - } - else { - std::cout << " BadPackage: " << Misc::StringUtils::format("0x%08X", p.mType) << std::endl; - } + std::cout << " AI Type: " << aiTypeLabel(p.mType) << " (" << Misc::StringUtils::format("0x%08X", p.mType) + << ")" << std::endl; + if (p.mType == ESM::AI_Wander) + { + std::cout << " Distance: " << p.mWander.mDistance << std::endl; + std::cout << " Duration: " << p.mWander.mDuration << std::endl; + std::cout << " Time of Day: " << (int)p.mWander.mTimeOfDay << std::endl; + if (p.mWander.mShouldRepeat != 1) + std::cout << " Should repeat: " << static_cast(p.mWander.mShouldRepeat != 0) << std::endl; + + std::cout << " Idle: "; + for (int i = 0; i != 8; i++) + std::cout << (int)p.mWander.mIdle[i] << " "; + std::cout << std::endl; + } + else if (p.mType == ESM::AI_Travel) + { + std::cout << " Travel Coordinates: (" << p.mTravel.mX << "," << p.mTravel.mY << "," << p.mTravel.mZ + << ")" << std::endl; + std::cout << " Should repeat: " << static_cast(p.mTravel.mShouldRepeat != 0) << std::endl; + } + else if (p.mType == ESM::AI_Follow || p.mType == ESM::AI_Escort) + { + std::cout << " Follow Coordinates: (" << p.mTarget.mX << "," << p.mTarget.mY << "," << p.mTarget.mZ + << ")" << std::endl; + std::cout << " Duration: " << p.mTarget.mDuration << std::endl; + std::cout << " Target ID: " << p.mTarget.mId.toString() << std::endl; + std::cout << " Should repeat: " << static_cast(p.mTarget.mShouldRepeat != 0) << std::endl; + } + else if (p.mType == ESM::AI_Activate) + { + std::cout << " Name: " << p.mActivate.mName.toString() << std::endl; + std::cout << " Should repeat: " << static_cast(p.mActivate.mShouldRepeat != 0) << std::endl; + } + else + { + std::cout << " BadPackage: " << Misc::StringUtils::format("0x%08X", p.mType) << std::endl; + } - if (!p.mCellName.empty()) - std::cout << " Cell Name: " << p.mCellName << std::endl; -} + if (!p.mCellName.empty()) + std::cout << " Cell Name: " << p.mCellName << std::endl; + } -std::string ruleString(const ESM::DialInfo::SelectStruct& ss) -{ - std::string rule = ss.mSelectRule; + std::string ruleString(const ESM::DialogueCondition& ss) + { + std::string_view type_str = "INVALID"; + std::string_view func_str; - if (rule.length() < 5) - return "INVALID"; + switch (ss.mFunction) + { + case ESM::DialogueCondition::Function_Global: + type_str = "Global"; + func_str = ss.mVariable; + break; + case ESM::DialogueCondition::Function_Local: + type_str = "Local"; + func_str = ss.mVariable; + break; + case ESM::DialogueCondition::Function_Journal: + type_str = "Journal"; + func_str = ss.mVariable; + break; + case ESM::DialogueCondition::Function_Item: + type_str = "Item count"; + func_str = ss.mVariable; + break; + case ESM::DialogueCondition::Function_Dead: + type_str = "Dead"; + func_str = ss.mVariable; + break; + case ESM::DialogueCondition::Function_NotId: + type_str = "Not ID"; + func_str = ss.mVariable; + break; + case ESM::DialogueCondition::Function_NotFaction: + type_str = "Not Faction"; + func_str = ss.mVariable; + break; + case ESM::DialogueCondition::Function_NotClass: + type_str = "Not Class"; + func_str = ss.mVariable; + break; + case ESM::DialogueCondition::Function_NotRace: + type_str = "Not Race"; + func_str = ss.mVariable; + break; + case ESM::DialogueCondition::Function_NotCell: + type_str = "Not Cell"; + func_str = ss.mVariable; + break; + case ESM::DialogueCondition::Function_NotLocal: + type_str = "Not Local"; + func_str = ss.mVariable; + break; + default: + type_str = "Function"; + func_str = ruleFunction(ss.mFunction); + break; + } - char type = rule[1]; - char indicator = rule[2]; + std::string_view oper_str = "??"; + switch (ss.mComparison) + { + case ESM::DialogueCondition::Comp_Eq: + oper_str = "=="; + break; + case ESM::DialogueCondition::Comp_Ne: + oper_str = "!="; + break; + case ESM::DialogueCondition::Comp_Gt: + oper_str = "> "; + break; + case ESM::DialogueCondition::Comp_Ge: + oper_str = ">="; + break; + case ESM::DialogueCondition::Comp_Ls: + oper_str = "< "; + break; + case ESM::DialogueCondition::Comp_Le: + oper_str = "<="; + break; + default: + break; + } - std::string type_str = "INVALID"; - std::string func_str = Misc::StringUtils::format("INVALID=%s", rule.substr(1,3)); - int func; - std::istringstream iss(rule.substr(2,2)); - iss >> func; + std::ostringstream stream; + std::visit([&](auto value) { stream << value; }, ss.mValue); - switch(type) - { - case '1': - type_str = "Function"; - func_str = ruleFunction(func); - break; - case '2': - if (indicator == 's') type_str = "Global short"; - else if (indicator == 'l') type_str = "Global long"; - else if (indicator == 'f') type_str = "Global float"; - break; - case '3': - if (indicator == 's') type_str = "Local short"; - else if (indicator == 'l') type_str = "Local long"; - else if (indicator == 'f') type_str = "Local float"; - break; - case '4': if (indicator == 'J') type_str = "Journal"; break; - case '5': if (indicator == 'I') type_str = "Item type"; break; - case '6': if (indicator == 'D') type_str = "NPC Dead"; break; - case '7': if (indicator == 'X') type_str = "Not ID"; break; - case '8': if (indicator == 'F') type_str = "Not Faction"; break; - case '9': if (indicator == 'C') type_str = "Not Class"; break; - case 'A': if (indicator == 'R') type_str = "Not Race"; break; - case 'B': if (indicator == 'L') type_str = "Not Cell"; break; - case 'C': if (indicator == 's') type_str = "Not Local"; break; - default: break; + std::string result + = Misc::StringUtils::format("%-12s %-32s %2s %s", type_str, func_str, oper_str, stream.str()); + return result; } - // Append the variable name to the function string if any. - if (type != '1') func_str = rule.substr(5); - - // In the previous switch, we assumed that the second char was X - // for all types not qual to one. If this wasn't true, go back to - // the error message. - if (type != '1' && rule[3] != 'X') - func_str = Misc::StringUtils::format("INVALID=%s", rule.substr(1,3)); - - char oper = rule[4]; - std::string oper_str = "??"; - switch (oper) + void printEffectList(const ESM::EffectList& effects) { - case '0': oper_str = "=="; break; - case '1': oper_str = "!="; break; - case '2': oper_str = "> "; break; - case '3': oper_str = ">="; break; - case '4': oper_str = "< "; break; - case '5': oper_str = "<="; break; - default: break; + int i = 0; + for (const ESM::IndexedENAMstruct& effect : effects.mList) + { + std::cout << " Effect[" << i << "]: " << magicEffectLabel(effect.mData.mEffectID) << " (" + << effect.mData.mEffectID << ")" << std::endl; + if (effect.mData.mSkill != -1) + std::cout << " Skill: " << skillLabel(effect.mData.mSkill) << " (" << (int)effect.mData.mSkill << ")" + << std::endl; + if (effect.mData.mAttribute != -1) + std::cout << " Attribute: " << attributeLabel(effect.mData.mAttribute) << " (" + << (int)effect.mData.mAttribute << ")" << std::endl; + std::cout << " Range: " << rangeTypeLabel(effect.mData.mRange) << " (" << effect.mData.mRange << ")" + << std::endl; + // Area is always zero if range type is "Self" + if (effect.mData.mRange != ESM::RT_Self) + std::cout << " Area: " << effect.mData.mArea << std::endl; + std::cout << " Duration: " << effect.mData.mDuration << std::endl; + std::cout << " Magnitude: " << effect.mData.mMagnMin << "-" << effect.mData.mMagnMax << std::endl; + i++; + } } - std::ostringstream stream; - stream << ss.mValue; - - std::string result = Misc::StringUtils::format("%-12s %-32s %2s %s", type_str, func_str, oper_str, stream.str()); - return result; -} - -void printEffectList(const ESM::EffectList& effects) -{ - int i = 0; - for (const ESM::ENAMstruct& effect : effects.mList) + void printTransport(const std::vector& transport) { - std::cout << " Effect[" << i << "]: " << magicEffectLabel(effect.mEffectID) - << " (" << effect.mEffectID << ")" << std::endl; - if (effect.mSkill != -1) - std::cout << " Skill: " << skillLabel(effect.mSkill) - << " (" << (int)effect.mSkill << ")" << std::endl; - if (effect.mAttribute != -1) - std::cout << " Attribute: " << attributeLabel(effect.mAttribute) - << " (" << (int)effect.mAttribute << ")" << std::endl; - std::cout << " Range: " << rangeTypeLabel(effect.mRange) - << " (" << effect.mRange << ")" << std::endl; - // Area is always zero if range type is "Self" - if (effect.mRange != ESM::RT_Self) - std::cout << " Area: " << effect.mArea << std::endl; - std::cout << " Duration: " << effect.mDuration << std::endl; - std::cout << " Magnitude: " << effect.mMagnMin << "-" << effect.mMagnMax << std::endl; - i++; + for (const ESM::Transport::Dest& dest : transport) + { + std::cout << " Destination Position: " << Misc::StringUtils::format("%12.3f", dest.mPos.pos[0]) << "," + << Misc::StringUtils::format("%12.3f", dest.mPos.pos[1]) << "," + << Misc::StringUtils::format("%12.3f", dest.mPos.pos[2]) << ")" << std::endl; + std::cout << " Destination Rotation: " << Misc::StringUtils::format("%9.6f", dest.mPos.rot[0]) << "," + << Misc::StringUtils::format("%9.6f", dest.mPos.rot[1]) << "," + << Misc::StringUtils::format("%9.6f", dest.mPos.rot[2]) << ")" << std::endl; + if (!dest.mCellName.empty()) + std::cout << " Destination Cell: " << dest.mCellName << std::endl; + } } } -void printTransport(const std::vector& transport) +namespace EsmTool { - for (const ESM::Transport::Dest& dest : transport) + void CellState::load(ESM::ESMReader& reader, bool& deleted) { - std::cout << " Destination Position: " - << Misc::StringUtils::format("%12.3f", dest.mPos.pos[0]) << "," - << Misc::StringUtils::format("%12.3f", dest.mPos.pos[1]) << "," - << Misc::StringUtils::format("%12.3f", dest.mPos.pos[2]) << ")" << std::endl; - std::cout << " Destination Rotation: " - << Misc::StringUtils::format("%9.6f", dest.mPos.rot[0]) << "," - << Misc::StringUtils::format("%9.6f", dest.mPos.rot[1]) << "," - << Misc::StringUtils::format("%9.6f", dest.mPos.rot[2]) << ")" << std::endl; - if (!dest.mCellName.empty()) - std::cout << " Destination Cell: " << dest.mCellName << std::endl; + mCellState.mId = reader.getCellId(); + mCellState.load(reader); + if (mCellState.mHasFogOfWar) + mFogState.load(reader); + deleted = false; + reader.skipRecord(); } -} - -} - -namespace EsmTool { - -RecordBase * -RecordBase::create(const ESM::NAME type) -{ - RecordBase *record = nullptr; - switch (type.intval) { - case ESM::REC_ACTI: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_ALCH: + std::unique_ptr RecordBase::create(const ESM::NAME type) { - record = new EsmTool::Record; - break; - } - case ESM::REC_APPA: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_ARMO: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_BODY: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_BOOK: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_BSGN: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_CELL: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_CLAS: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_CLOT: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_CONT: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_CREA: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_DIAL: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_DOOR: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_ENCH: - { - record = new EsmTool::Record; - break; - } - case ESM::REC_FACT: - { - record = new EsmTool::Record; - break; + std::unique_ptr record; + + switch (type.toInt()) + { + case ESM::REC_ACTI: + { + record = std::make_unique>(); + break; + } + case ESM::REC_ALCH: + { + record = std::make_unique>(); + break; + } + case ESM::REC_APPA: + { + record = std::make_unique>(); + break; + } + case ESM::REC_ARMO: + { + record = std::make_unique>(); + break; + } + case ESM::REC_BODY: + { + record = std::make_unique>(); + break; + } + case ESM::REC_BOOK: + { + record = std::make_unique>(); + break; + } + case ESM::REC_BSGN: + { + record = std::make_unique>(); + break; + } + case ESM::REC_CELL: + { + record = std::make_unique>(); + break; + } + case ESM::REC_CLAS: + { + record = std::make_unique>(); + break; + } + case ESM::REC_CLOT: + { + record = std::make_unique>(); + break; + } + case ESM::REC_CONT: + { + record = std::make_unique>(); + break; + } + case ESM::REC_CREA: + { + record = std::make_unique>(); + break; + } + case ESM::REC_DIAL: + { + record = std::make_unique>(); + break; + } + case ESM::REC_DOOR: + { + record = std::make_unique>(); + break; + } + case ESM::REC_ENCH: + { + record = std::make_unique>(); + break; + } + case ESM::REC_FACT: + { + record = std::make_unique>(); + break; + } + case ESM::REC_GLOB: + { + record = std::make_unique>(); + break; + } + case ESM::REC_GMST: + { + record = std::make_unique>(); + break; + } + case ESM::REC_INFO: + { + record = std::make_unique>(); + break; + } + case ESM::REC_INGR: + { + record = std::make_unique>(); + break; + } + case ESM::REC_LAND: + { + record = std::make_unique>(); + break; + } + case ESM::REC_LEVI: + { + record = std::make_unique>(); + break; + } + case ESM::REC_LEVC: + { + record = std::make_unique>(); + break; + } + case ESM::REC_LIGH: + { + record = std::make_unique>(); + break; + } + case ESM::REC_LOCK: + { + record = std::make_unique>(); + break; + } + case ESM::REC_LTEX: + { + record = std::make_unique>(); + break; + } + case ESM::REC_MISC: + { + record = std::make_unique>(); + break; + } + case ESM::REC_MGEF: + { + record = std::make_unique>(); + break; + } + case ESM::REC_NPC_: + { + record = std::make_unique>(); + break; + } + case ESM::REC_PGRD: + { + record = std::make_unique>(); + break; + } + case ESM::REC_PROB: + { + record = std::make_unique>(); + break; + } + case ESM::REC_RACE: + { + record = std::make_unique>(); + break; + } + case ESM::REC_REGN: + { + record = std::make_unique>(); + break; + } + case ESM::REC_REPA: + { + record = std::make_unique>(); + break; + } + case ESM::REC_SCPT: + { + record = std::make_unique>(); + break; + } + case ESM::REC_SKIL: + { + record = std::make_unique>(); + break; + } + case ESM::REC_SNDG: + { + record = std::make_unique>(); + break; + } + case ESM::REC_SOUN: + { + record = std::make_unique>(); + break; + } + case ESM::REC_SPEL: + { + record = std::make_unique>(); + break; + } + case ESM::REC_STAT: + { + record = std::make_unique>(); + break; + } + case ESM::REC_WEAP: + { + record = std::make_unique>(); + break; + } + case ESM::REC_SSCR: + { + record = std::make_unique>(); + break; + } + case ESM::REC_CSTA: + { + record = std::make_unique>(); + break; + } + default: + break; + } + if (record) + { + record->mType = type; + } + return record; } - case ESM::REC_GLOB: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_GMST: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + std::cout << " Flags: " << potionFlags(mData.mData.mFlags) << std::endl; + printEffectList(mData.mEffects); + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_INFO: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + if (!mData.mEnchant.empty()) + std::cout << " Enchantment: " << mData.mEnchant << std::endl; + std::cout << " Type: " << armorTypeLabel(mData.mData.mType) << " (" << mData.mData.mType << ")" << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + std::cout << " Health: " << mData.mData.mHealth << std::endl; + std::cout << " Armor: " << mData.mData.mArmor << std::endl; + std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; + for (const ESM::PartReference& part : mData.mParts.mParts) + { + std::cout << " Body Part: " << bodyPartLabel(part.mPart) << " (" << (int)(part.mPart) << ")" << std::endl; + std::cout << " Male Name: " << part.mMale << std::endl; + if (!part.mFemale.empty()) + std::cout << " Female Name: " << part.mFemale << std::endl; + } + + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_INGR: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " Type: " << apparatusTypeLabel(mData.mData.mType) << " (" << mData.mData.mType << ")" + << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + std::cout << " Quality: " << mData.mData.mQuality << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_LAND: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Race: " << mData.mRace << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + std::cout << " Type: " << meshTypeLabel(mData.mData.mType) << " (" << (int)mData.mData.mType << ")" + << std::endl; + std::cout << " Flags: " << bodyPartFlags(mData.mData.mFlags) << std::endl; + std::cout << " Part: " << meshPartLabel(mData.mData.mPart) << " (" << (int)mData.mData.mPart << ")" + << std::endl; + std::cout << " Vampire: " << (int)mData.mData.mVampire << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_LEVI: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + if (!mData.mEnchant.empty()) + std::cout << " Enchantment: " << mData.mEnchant << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + std::cout << " IsScroll: " << mData.mData.mIsScroll << std::endl; + std::cout << " SkillId: " << mData.mData.mSkillId << std::endl; + std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; + if (mPrintPlain) + { + std::cout << " Text:" << std::endl; + std::cout << "START--------------------------------------" << std::endl; + std::cout << mData.mText << std::endl; + std::cout << "END----------------------------------------" << std::endl; + } + else + { + std::cout << " Text: [skipped]" << std::endl; + } + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_LEVC: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Texture: " << mData.mTexture << std::endl; + std::cout << " Description: " << mData.mDescription << std::endl; + for (const auto& power : mData.mPowers.mList) + std::cout << " Power: " << power << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_LIGH: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + // None of the cells have names... + if (!mData.mName.empty()) + std::cout << " Name: " << mData.mName << std::endl; + if (!mData.mRegion.empty()) + std::cout << " Region: " << mData.mRegion << std::endl; + std::cout << " Flags: " << cellFlags(mData.mData.mFlags) << std::endl; + + std::cout << " Coordinates: " + << " (" << mData.getGridX() << "," << mData.getGridY() << ")" << std::endl; + + if (mData.mData.mFlags & ESM::Cell::Interior && !(mData.mData.mFlags & ESM::Cell::QuasiEx)) + { + if (mData.hasAmbient()) + { + // TODO: see if we can change the integer representation to something more sensible + std::cout << " Ambient Light Color: " << mData.mAmbi.mAmbient << std::endl; + std::cout << " Sunlight Color: " << mData.mAmbi.mSunlight << std::endl; + std::cout << " Fog Color: " << mData.mAmbi.mFog << std::endl; + std::cout << " Fog Density: " << mData.mAmbi.mFogDensity << std::endl; + } + else + { + std::cout << " No Ambient Information" << std::endl; + } + std::cout << " Water Level: " << mData.mWater << std::endl; + } + else + std::cout << " Map Color: " << Misc::StringUtils::format("0x%08X", mData.mMapColor) << std::endl; + std::cout << " RefId counter: " << mData.mRefNumCounter << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_LOCK: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; - } - case ESM::REC_LTEX: + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Description: " << mData.mDescription << std::endl; + std::cout << " Playable: " << mData.mData.mIsPlayable << std::endl; + std::cout << " AI Services: " << Misc::StringUtils::format("0x%08X", mData.mData.mServices) << std::endl; + for (size_t i = 0; i < mData.mData.mAttribute.size(); ++i) + std::cout << " Attribute" << (i + 1) << ": " << attributeLabel(mData.mData.mAttribute[i]) << " (" + << mData.mData.mAttribute[i] << ")" << std::endl; + std::cout << " Specialization: " << specializationLabel(mData.mData.mSpecialization) << " (" + << mData.mData.mSpecialization << ")" << std::endl; + for (const auto& skills : mData.mData.mSkills) + std::cout << " Minor Skill: " << skillLabel(skills[0]) << " (" << skills[0] << ")" << std::endl; + for (const auto& skills : mData.mData.mSkills) + std::cout << " Major Skill: " << skillLabel(skills[1]) << " (" << skills[1] << ")" << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; + } + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + if (!mData.mEnchant.empty()) + std::cout << " Enchantment: " << mData.mEnchant << std::endl; + std::cout << " Type: " << clothingTypeLabel(mData.mData.mType) << " (" << mData.mData.mType << ")" + << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; + for (const ESM::PartReference& part : mData.mParts.mParts) + { + std::cout << " Body Part: " << bodyPartLabel(part.mPart) << " (" << (int)(part.mPart) << ")" << std::endl; + std::cout << " Male Name: " << part.mMale << std::endl; + if (!part.mFemale.empty()) + std::cout << " Female Name: " << part.mFemale << std::endl; + } + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_MISC: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " Flags: " << containerFlags(mData.mFlags) << std::endl; + std::cout << " Weight: " << mData.mWeight << std::endl; + for (const ESM::ContItem& item : mData.mInventory.mList) + std::cout << " Inventory: Count: " << Misc::StringUtils::format("%4d", item.mCount) + << " Item: " << item.mItem << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_MGEF: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; - } - case ESM::REC_NPC_: + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " Flags: " << creatureFlags((int)mData.mFlags) << std::endl; + std::cout << " Blood Type: " << mData.mBloodType + 1 << std::endl; + std::cout << " Original: " << mData.mOriginal << std::endl; + std::cout << " Scale: " << mData.mScale << std::endl; + + std::cout << " Type: " << creatureTypeLabel(mData.mData.mType) << " (" << mData.mData.mType << ")" + << std::endl; + std::cout << " Level: " << mData.mData.mLevel << std::endl; + + std::cout << " Attributes:" << std::endl; + for (size_t i = 0; i < mData.mData.mAttributes.size(); ++i) + std::cout << " " << ESM::Attribute::indexToRefId(i) << ": " << mData.mData.mAttributes[i] << std::endl; + + std::cout << " Health: " << mData.mData.mHealth << std::endl; + std::cout << " Magicka: " << mData.mData.mMana << std::endl; + std::cout << " Fatigue: " << mData.mData.mFatigue << std::endl; + std::cout << " Soul: " << mData.mData.mSoul << std::endl; + std::cout << " Combat: " << mData.mData.mCombat << std::endl; + std::cout << " Magic: " << mData.mData.mMagic << std::endl; + std::cout << " Stealth: " << mData.mData.mStealth << std::endl; + std::cout << " Attack1: " << mData.mData.mAttack[0] << "-" << mData.mData.mAttack[1] << std::endl; + std::cout << " Attack2: " << mData.mData.mAttack[2] << "-" << mData.mData.mAttack[3] << std::endl; + std::cout << " Attack3: " << mData.mData.mAttack[4] << "-" << mData.mData.mAttack[5] << std::endl; + std::cout << " Gold: " << mData.mData.mGold << std::endl; + + for (const ESM::ContItem& item : mData.mInventory.mList) + std::cout << " Inventory: Count: " << Misc::StringUtils::format("%4d", item.mCount) + << " Item: " << item.mItem << std::endl; + + for (const auto& spell : mData.mSpells.mList) + std::cout << " Spell: " << spell << std::endl; + + printTransport(mData.getTransport()); + + std::cout << " Artificial Intelligence: " << std::endl; + std::cout << " AI Hello:" << (int)mData.mAiData.mHello << std::endl; + std::cout << " AI Fight:" << (int)mData.mAiData.mFight << std::endl; + std::cout << " AI Flee:" << (int)mData.mAiData.mFlee << std::endl; + std::cout << " AI Alarm:" << (int)mData.mAiData.mAlarm << std::endl; + std::cout << " AI Services:" << Misc::StringUtils::format("0x%08X", mData.mAiData.mServices) << std::endl; + + for (const ESM::AIPackage& package : mData.mAiPackage.mList) + printAIPackage(package); + std::cout << " Deleted: " << mIsDeleted << std::endl; + } + + template <> + void Record::print() + { + std::cout << " StringId: " << mData.mStringId << std::endl; + std::cout << " Type: " << dialogTypeLabel(mData.mType) << " (" << (int)mData.mType << ")" << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; + // Sadly, there are no DialInfos, because the loader dumps as it + // loads, rather than loading and then dumping. :-( Anyone mind if + // I change this? + for (const ESM::DialInfo& info : mData.mInfo) + std::cout << "INFO!" << info.mId << std::endl; + } + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " OpenSound: " << mData.mOpenSound << std::endl; + std::cout << " CloseSound: " << mData.mCloseSound << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_PGRD: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Type: " << enchantTypeLabel(mData.mData.mType) << " (" << mData.mData.mType << ")" << std::endl; + std::cout << " Cost: " << mData.mData.mCost << std::endl; + std::cout << " Charge: " << mData.mData.mCharge << std::endl; + std::cout << " Flags: " << enchantmentFlags(mData.mData.mFlags) << std::endl; + printEffectList(mData.mEffects); + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_PROB: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Hidden: " << mData.mData.mIsHidden << std::endl; + for (size_t i = 0; i < mData.mData.mAttribute.size(); ++i) + std::cout << " Attribute" << (i + 1) << ": " << attributeLabel(mData.mData.mAttribute[i]) << " (" + << mData.mData.mAttribute[i] << ")" << std::endl; + for (int skill : mData.mData.mSkills) + if (skill != -1) + std::cout << " Skill: " << skillLabel(skill) << " (" << skill << ")" << std::endl; + for (size_t i = 0; i != mData.mData.mRankData.size(); i++) + if (!mData.mRanks[i].empty()) + { + std::cout << " Rank: " << mData.mRanks[i] << std::endl; + std::cout << " Attribute1 Requirement: " << mData.mData.mRankData[i].mAttribute1 << std::endl; + std::cout << " Attribute2 Requirement: " << mData.mData.mRankData[i].mAttribute2 << std::endl; + std::cout << " One Skill at Level: " << mData.mData.mRankData[i].mPrimarySkill << std::endl; + std::cout << " Two Skills at Level: " << mData.mData.mRankData[i].mFavouredSkill << std::endl; + std::cout << " Faction Reaction: " << mData.mData.mRankData[i].mFactReaction << std::endl; + } + for (const auto& reaction : mData.mReactions) + std::cout << " Reaction: " << reaction.second << " = " << reaction.first << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; + } + + template <> + void Record::print() + { + std::cout << " " << mData.mValue << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; + } + + template <> + void Record::print() + { + std::cout << " " << mData.mValue << std::endl; + } + + template <> + void Record::print() + { + std::cout << " Id: " << mData.mId << std::endl; + if (!mData.mPrev.empty()) + std::cout << " Previous ID: " << mData.mPrev << std::endl; + if (!mData.mNext.empty()) + std::cout << " Next ID: " << mData.mNext << std::endl; + std::cout << " Text: " << mData.mResponse << std::endl; + if (!mData.mActor.empty()) + std::cout << " Actor: " << mData.mActor << std::endl; + if (!mData.mRace.empty()) + std::cout << " Race: " << mData.mRace << std::endl; + if (!mData.mClass.empty()) + std::cout << " Class: " << mData.mClass << std::endl; + std::cout << " Factionless: " << mData.mFactionLess << std::endl; + if (!mData.mFaction.empty()) + std::cout << " NPC Faction: " << mData.mFaction << std::endl; + if (mData.mData.mRank != -1) + std::cout << " NPC Rank: " << (int)mData.mData.mRank << std::endl; + if (!mData.mPcFaction.empty()) + std::cout << " PC Faction: " << mData.mPcFaction << std::endl; + // CHANGE? non-standard capitalization mPCrank -> mPCRank (mPcRank?) + if (mData.mData.mPCrank != -1) + std::cout << " PC Rank: " << (int)mData.mData.mPCrank << std::endl; + if (!mData.mCell.empty()) + std::cout << " Cell: " << mData.mCell << std::endl; + if (mData.mData.mDisposition > 0) + std::cout << " Disposition/Journal index: " << mData.mData.mDisposition << std::endl; + if (mData.mData.mGender != ESM::DialInfo::NA) + std::cout << " Gender: " << static_cast(mData.mData.mGender) << std::endl; + if (!mData.mSound.empty()) + std::cout << " Sound File: " << mData.mSound << std::endl; + + std::cout << " Quest Status: " << questStatusLabel(mData.mQuestStatus) << " (" << mData.mQuestStatus << ")" + << std::endl; + std::cout << " Type: " << dialogTypeLabel(mData.mData.mType) << std::endl; + + for (const auto& rule : mData.mSelects) + std::cout << " Select Rule: " << ruleString(rule) << std::endl; + + if (!mData.mResultScript.empty()) + { + if (mPrintPlain) + { + std::cout << " Result Script:" << std::endl; + std::cout << "START--------------------------------------" << std::endl; + std::cout << mData.mResultScript << std::endl; + std::cout << "END----------------------------------------" << std::endl; + } + else + { + std::cout << " Result Script: [skipped]" << std::endl; + } + } + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_RACE: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + for (int i = 0; i != 4; i++) + { + // A value of -1 means no effect + if (mData.mData.mEffectID[i] == -1) + continue; + std::cout << " Effect: " << magicEffectLabel(mData.mData.mEffectID[i]) << " (" << mData.mData.mEffectID[i] + << ")" << std::endl; + std::cout << " Skill: " << skillLabel(mData.mData.mSkills[i]) << " (" << mData.mData.mSkills[i] << ")" + << std::endl; + std::cout << " Attribute: " << attributeLabel(mData.mData.mAttributes[i]) << " (" + << mData.mData.mAttributes[i] << ")" << std::endl; + } + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_REGN: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Coordinates: (" << mData.mX << "," << mData.mY << ")" << std::endl; + std::cout << " Flags: " << landFlags(mData.mFlags) << std::endl; + std::cout << " DataTypes: " << mData.mDataTypes << std::endl; + + if (const ESM::Land::LandData* data = mData.getLandData(mData.mDataTypes)) + { + std::cout << " MinHeight: " << data->mMinHeight << std::endl; + std::cout << " MaxHeight: " << data->mMaxHeight << std::endl; + std::cout << " DataLoaded: " << data->mDataLoaded << std::endl; + } + mData.unloadData(); + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_REPA: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Chance for None: " << (int)mData.mChanceNone << std::endl; + std::cout << " Flags: " << creatureListFlags(mData.mFlags) << std::endl; + std::cout << " Number of items: " << mData.mList.size() << std::endl; + for (const ESM::LevelledListBase::LevelItem& item : mData.mList) + std::cout << " Creature: Level: " << item.mLevel << " Creature: " << item.mId << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_SCPT: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Chance for None: " << (int)mData.mChanceNone << std::endl; + std::cout << " Flags: " << itemListFlags(mData.mFlags) << std::endl; + std::cout << " Number of items: " << mData.mList.size() << std::endl; + for (const ESM::LevelledListBase::LevelItem& item : mData.mList) + std::cout << " Inventory: Level: " << item.mLevel << " Item: " << item.mId << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_SKIL: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + if (!mData.mName.empty()) + std::cout << " Name: " << mData.mName << std::endl; + if (!mData.mModel.empty()) + std::cout << " Model: " << mData.mModel << std::endl; + if (!mData.mIcon.empty()) + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " Flags: " << lightFlags(mData.mData.mFlags) << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + std::cout << " Sound: " << mData.mSound << std::endl; + std::cout << " Duration: " << mData.mData.mTime << std::endl; + std::cout << " Radius: " << mData.mData.mRadius << std::endl; + std::cout << " Color: " << mData.mData.mColor << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_SNDG: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + std::cout << " Quality: " << mData.mData.mQuality << std::endl; + std::cout << " Uses: " << mData.mData.mUses << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_SOUN: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + std::cout << " Quality: " << mData.mData.mQuality << std::endl; + std::cout << " Uses: " << mData.mData.mUses << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_SPEL: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + std::cout << " Quality: " << mData.mData.mQuality << std::endl; + std::cout << " Uses: " << mData.mData.mUses << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_STAT: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; + std::cout << " Id: " << mData.mId << std::endl; + std::cout << " Index: " << mData.mIndex << std::endl; + std::cout << " Texture: " << mData.mTexture << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - case ESM::REC_WEAP: + + template <> + void Record::print() { - record = new EsmTool::Record; - break; - } - case ESM::REC_SSCR: + std::cout << " Index: " << magicEffectLabel(mData.mIndex) << " (" << mData.mIndex << ")" << std::endl; + std::cout << " Description: " << mData.mDescription << std::endl; + std::cout << " Icon: " << mData.mIcon << std::endl; + std::cout << " Flags: " << magicEffectFlags(mData.mData.mFlags) << std::endl; + std::cout << " Particle Texture: " << mData.mParticle << std::endl; + if (!mData.mCasting.empty()) + std::cout << " Casting Static: " << mData.mCasting << std::endl; + if (!mData.mCastSound.empty()) + std::cout << " Casting Sound: " << mData.mCastSound << std::endl; + if (!mData.mBolt.empty()) + std::cout << " Bolt Static: " << mData.mBolt << std::endl; + if (!mData.mBoltSound.empty()) + std::cout << " Bolt Sound: " << mData.mBoltSound << std::endl; + if (!mData.mHit.empty()) + std::cout << " Hit Static: " << mData.mHit << std::endl; + if (!mData.mHitSound.empty()) + std::cout << " Hit Sound: " << mData.mHitSound << std::endl; + if (!mData.mArea.empty()) + std::cout << " Area Static: " << mData.mArea << std::endl; + if (!mData.mAreaSound.empty()) + std::cout << " Area Sound: " << mData.mAreaSound << std::endl; + std::cout << " School: " << schoolLabel(ESM::MagicSchool::skillRefIdToIndex(mData.mData.mSchool)) << " (" + << mData.mData.mSchool << ")" << std::endl; + std::cout << " Base Cost: " << mData.mData.mBaseCost << std::endl; + std::cout << " Unknown 1: " << mData.mData.mUnknown1 << std::endl; + std::cout << " Speed: " << mData.mData.mSpeed << std::endl; + std::cout << " Unknown 2: " << mData.mData.mUnknown2 << std::endl; + std::cout << " RGB Color: " + << "(" << mData.mData.mRed << "," << mData.mData.mGreen << "," << mData.mData.mBlue << ")" + << std::endl; + } + + template <> + void Record::print() { - record = new EsmTool::Record; - break; - } - default: - record = nullptr; - } - if (record) { - record->mType = type; + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + std::cout << " Is Key: " << (mData.mData.mFlags & ESM::Miscellaneous::Key) << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - return record; -} -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Icon: " << mData.mIcon << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " AutoCalc: " << mData.mData.mAutoCalc << std::endl; - printEffectList(mData.mEffects); - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Icon: " << mData.mIcon << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - if (!mData.mEnchant.empty()) - std::cout << " Enchantment: " << mData.mEnchant << std::endl; - std::cout << " Type: " << armorTypeLabel(mData.mData.mType) - << " (" << mData.mData.mType << ")" << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " Health: " << mData.mData.mHealth << std::endl; - std::cout << " Armor: " << mData.mData.mArmor << std::endl; - std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; - for (const ESM::PartReference &part : mData.mParts.mParts) + template <> + void Record::print() { - std::cout << " Body Part: " << bodyPartLabel(part.mPart) - << " (" << (int)(part.mPart) << ")" << std::endl; - std::cout << " Male Name: " << part.mMale << std::endl; - if (!part.mFemale.empty()) - std::cout << " Female Name: " << part.mFemale << std::endl; - } + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Animation: " << mData.mModel << std::endl; + std::cout << " Hair Model: " << mData.mHair << std::endl; + std::cout << " Head Model: " << mData.mHead << std::endl; + std::cout << " Race: " << mData.mRace << std::endl; + std::cout << " Class: " << mData.mClass << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + if (!mData.mFaction.empty()) + std::cout << " Faction: " << mData.mFaction << std::endl; + std::cout << " Flags: " << npcFlags((int)mData.mFlags) << std::endl; + if (mData.mBloodType != 0) + std::cout << " Blood Type: " << mData.mBloodType + 1 << std::endl; + + if (mData.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + { + std::cout << " Level: " << mData.mNpdt.mLevel << std::endl; + std::cout << " Reputation: " << (int)mData.mNpdt.mReputation << std::endl; + std::cout << " Disposition: " << (int)mData.mNpdt.mDisposition << std::endl; + std::cout << " Rank: " << (int)mData.mNpdt.mRank << std::endl; + std::cout << " Gold: " << mData.mNpdt.mGold << std::endl; + } + else + { + std::cout << " Level: " << mData.mNpdt.mLevel << std::endl; + std::cout << " Reputation: " << (int)mData.mNpdt.mReputation << std::endl; + std::cout << " Disposition: " << (int)mData.mNpdt.mDisposition << std::endl; + std::cout << " Rank: " << (int)mData.mNpdt.mRank << std::endl; + + std::cout << " Attributes:" << std::endl; + for (size_t i = 0; i != mData.mNpdt.mAttributes.size(); i++) + std::cout << " " << attributeLabel(i) << ": " << int(mData.mNpdt.mAttributes[i]) << std::endl; + + std::cout << " Skills:" << std::endl; + for (size_t i = 0; i != mData.mNpdt.mSkills.size(); i++) + std::cout << " " << skillLabel(i) << ": " << int(mData.mNpdt.mSkills[i]) << std::endl; + + std::cout << " Health: " << mData.mNpdt.mHealth << std::endl; + std::cout << " Magicka: " << mData.mNpdt.mMana << std::endl; + std::cout << " Fatigue: " << mData.mNpdt.mFatigue << std::endl; + std::cout << " Gold: " << mData.mNpdt.mGold << std::endl; + } - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + for (const ESM::ContItem& item : mData.mInventory.mList) + std::cout << " Inventory: Count: " << Misc::StringUtils::format("%4d", item.mCount) + << " Item: " << item.mItem << std::endl; -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Icon: " << mData.mIcon << std::endl; - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " Type: " << apparatusTypeLabel(mData.mData.mType) - << " (" << mData.mData.mType << ")" << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " Quality: " << mData.mData.mQuality << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + for (const auto& spell : mData.mSpells.mList) + std::cout << " Spell: " << spell << std::endl; -template<> -void Record::print() -{ - std::cout << " Race: " << mData.mRace << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Type: " << meshTypeLabel(mData.mData.mType) - << " (" << (int)mData.mData.mType << ")" << std::endl; - std::cout << " Flags: " << bodyPartFlags(mData.mData.mFlags) << std::endl; - std::cout << " Part: " << meshPartLabel(mData.mData.mPart) - << " (" << (int)mData.mData.mPart << ")" << std::endl; - std::cout << " Vampire: " << (int)mData.mData.mVampire << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + printTransport(mData.getTransport()); -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Icon: " << mData.mIcon << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - if (!mData.mEnchant.empty()) - std::cout << " Enchantment: " << mData.mEnchant << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " IsScroll: " << mData.mData.mIsScroll << std::endl; - std::cout << " SkillId: " << mData.mData.mSkillId << std::endl; - std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; - if (mPrintPlain) - { - std::cout << " Text:" << std::endl; - std::cout << "START--------------------------------------" << std::endl; - std::cout << mData.mText << std::endl; - std::cout << "END----------------------------------------" << std::endl; - } - else - { - std::cout << " Text: [skipped]" << std::endl; - } - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + std::cout << " Artificial Intelligence: " << std::endl; + std::cout << " AI Hello:" << (int)mData.mAiData.mHello << std::endl; + std::cout << " AI Fight:" << (int)mData.mAiData.mFight << std::endl; + std::cout << " AI Flee:" << (int)mData.mAiData.mFlee << std::endl; + std::cout << " AI Alarm:" << (int)mData.mAiData.mAlarm << std::endl; + std::cout << " AI Services:" << Misc::StringUtils::format("0x%08X", mData.mAiData.mServices) << std::endl; -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Texture: " << mData.mTexture << std::endl; - std::cout << " Description: " << mData.mDescription << std::endl; - for (const std::string &power : mData.mPowers.mList) - std::cout << " Power: " << power << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + for (const ESM::AIPackage& package : mData.mAiPackage.mList) + printAIPackage(package); -template<> -void Record::print() -{ - // None of the cells have names... - if (!mData.mName.empty()) - std::cout << " Name: " << mData.mName << std::endl; - if (!mData.mRegion.empty()) - std::cout << " Region: " << mData.mRegion << std::endl; - std::cout << " Flags: " << cellFlags(mData.mData.mFlags) << std::endl; - - std::cout << " Coordinates: " << " (" << mData.getGridX() << "," - << mData.getGridY() << ")" << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; + } - if (mData.mData.mFlags & ESM::Cell::Interior && - !(mData.mData.mFlags & ESM::Cell::QuasiEx)) + template <> + void Record::print() { - if (mData.hasAmbient()) + std::cout << " Cell: " << mData.mCell << std::endl; + std::cout << " Coordinates: (" << mData.mData.mX << "," << mData.mData.mY << ")" << std::endl; + std::cout << " Granularity: " << mData.mData.mGranularity << std::endl; + if (mData.mData.mPoints != mData.mPoints.size()) + std::cout << " Reported Point Count: " << mData.mData.mPoints << std::endl; + std::cout << " Point Count: " << mData.mPoints.size() << std::endl; + std::cout << " Edge Count: " << mData.mEdges.size() << std::endl; + + int i = 0; + for (const ESM::Pathgrid::Point& point : mData.mPoints) { - // TODO: see if we can change the integer representation to something more sensible - std::cout << " Ambient Light Color: " << mData.mAmbi.mAmbient << std::endl; - std::cout << " Sunlight Color: " << mData.mAmbi.mSunlight << std::endl; - std::cout << " Fog Color: " << mData.mAmbi.mFog << std::endl; - std::cout << " Fog Density: " << mData.mAmbi.mFogDensity << std::endl; + std::cout << " Point[" << i << "]:" << std::endl; + std::cout << " Coordinates: (" << point.mX << "," << point.mY << "," << point.mZ << ")" << std::endl; + std::cout << " Auto-Generated: " << (int)point.mAutogenerated << std::endl; + std::cout << " Connections: " << (int)point.mConnectionNum << std::endl; + i++; } - else + + i = 0; + for (const ESM::Pathgrid::Edge& edge : mData.mEdges) { - std::cout << " No Ambient Information" << std::endl; + std::cout << " Edge[" << i << "]: " << edge.mV0 << " -> " << edge.mV1 << std::endl; + if (edge.mV0 >= mData.mData.mPoints || edge.mV1 >= mData.mData.mPoints) + std::cout << " BAD POINT IN EDGE!" << std::endl; + i++; } - std::cout << " Water Level: " << mData.mWater << std::endl; - } - else - std::cout << " Map Color: " << Misc::StringUtils::format("0x%08X", mData.mMapColor) << std::endl; - std::cout << " Water Level Int: " << mData.mWaterInt << std::endl; - std::cout << " RefId counter: " << mData.mRefNumCounter << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Description: " << mData.mDescription << std::endl; - std::cout << " Playable: " << mData.mData.mIsPlayable << std::endl; - std::cout << " AutoCalc: " << mData.mData.mCalc << std::endl; - std::cout << " Attribute1: " << attributeLabel(mData.mData.mAttribute[0]) - << " (" << mData.mData.mAttribute[0] << ")" << std::endl; - std::cout << " Attribute2: " << attributeLabel(mData.mData.mAttribute[1]) - << " (" << mData.mData.mAttribute[1] << ")" << std::endl; - std::cout << " Specialization: " << specializationLabel(mData.mData.mSpecialization) - << " (" << mData.mData.mSpecialization << ")" << std::endl; - for (int i = 0; i != 5; i++) - std::cout << " Minor Skill: " << skillLabel(mData.mData.mSkills[i][0]) - << " (" << mData.mData.mSkills[i][0] << ")" << std::endl; - for (int i = 0; i != 5; i++) - std::cout << " Major Skill: " << skillLabel(mData.mData.mSkills[i][1]) - << " (" << mData.mData.mSkills[i][1] << ")" << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Icon: " << mData.mIcon << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - if (!mData.mEnchant.empty()) - std::cout << " Enchantment: " << mData.mEnchant << std::endl; - std::cout << " Type: " << clothingTypeLabel(mData.mData.mType) - << " (" << mData.mData.mType << ")" << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; - for (const ESM::PartReference &part : mData.mParts.mParts) - { - std::cout << " Body Part: " << bodyPartLabel(part.mPart) - << " (" << (int)(part.mPart) << ")" << std::endl; - std::cout << " Male Name: " << part.mMale << std::endl; - if (!part.mFemale.empty()) - std::cout << " Female Name: " << part.mFemale << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - std::cout << " Deleted: " << mIsDeleted << std::endl; -} -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " Flags: " << containerFlags(mData.mFlags) << std::endl; - std::cout << " Weight: " << mData.mWeight << std::endl; - for (const ESM::ContItem &item : mData.mInventory.mList) - std::cout << " Inventory: Count: " << Misc::StringUtils::format("%4d", item.mCount) - << " Item: " << item.mItem << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + template <> + void Record::print() + { + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Description: " << mData.mDescription << std::endl; + std::cout << " Flags: " << raceFlags(mData.mData.mFlags) << std::endl; -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " Flags: " << creatureFlags((int)mData.mFlags) << std::endl; - std::cout << " Blood Type: " << mData.mBloodType+1 << std::endl; - std::cout << " Original: " << mData.mOriginal << std::endl; - std::cout << " Scale: " << mData.mScale << std::endl; - - std::cout << " Type: " << creatureTypeLabel(mData.mData.mType) - << " (" << mData.mData.mType << ")" << std::endl; - std::cout << " Level: " << mData.mData.mLevel << std::endl; - - std::cout << " Attributes:" << std::endl; - std::cout << " Strength: " << mData.mData.mStrength << std::endl; - std::cout << " Intelligence: " << mData.mData.mIntelligence << std::endl; - std::cout << " Willpower: " << mData.mData.mWillpower << std::endl; - std::cout << " Agility: " << mData.mData.mAgility << std::endl; - std::cout << " Speed: " << mData.mData.mSpeed << std::endl; - std::cout << " Endurance: " << mData.mData.mEndurance << std::endl; - std::cout << " Personality: " << mData.mData.mPersonality << std::endl; - std::cout << " Luck: " << mData.mData.mLuck << std::endl; - - std::cout << " Health: " << mData.mData.mHealth << std::endl; - std::cout << " Magicka: " << mData.mData.mMana << std::endl; - std::cout << " Fatigue: " << mData.mData.mFatigue << std::endl; - std::cout << " Soul: " << mData.mData.mSoul << std::endl; - std::cout << " Combat: " << mData.mData.mCombat << std::endl; - std::cout << " Magic: " << mData.mData.mMagic << std::endl; - std::cout << " Stealth: " << mData.mData.mStealth << std::endl; - std::cout << " Attack1: " << mData.mData.mAttack[0] - << "-" << mData.mData.mAttack[1] << std::endl; - std::cout << " Attack2: " << mData.mData.mAttack[2] - << "-" << mData.mData.mAttack[3] << std::endl; - std::cout << " Attack3: " << mData.mData.mAttack[4] - << "-" << mData.mData.mAttack[5] << std::endl; - std::cout << " Gold: " << mData.mData.mGold << std::endl; - - for (const ESM::ContItem &item : mData.mInventory.mList) - std::cout << " Inventory: Count: " << Misc::StringUtils::format("%4d", item.mCount) - << " Item: " << item.mItem << std::endl; - - for (const std::string &spell : mData.mSpells.mList) - std::cout << " Spell: " << spell << std::endl; - - printTransport(mData.getTransport()); - - std::cout << " Artificial Intelligence: " << std::endl; - std::cout << " AI Hello:" << (int)mData.mAiData.mHello << std::endl; - std::cout << " AI Fight:" << (int)mData.mAiData.mFight << std::endl; - std::cout << " AI Flee:" << (int)mData.mAiData.mFlee << std::endl; - std::cout << " AI Alarm:" << (int)mData.mAiData.mAlarm << std::endl; - std::cout << " AI U1:" << (int)mData.mAiData.mU1 << std::endl; - std::cout << " AI U2:" << (int)mData.mAiData.mU2 << std::endl; - std::cout << " AI U3:" << (int)mData.mAiData.mU3 << std::endl; - std::cout << " AI Services:" << Misc::StringUtils::format("0x%08X", mData.mAiData.mServices) << std::endl; - - for (const ESM::AIPackage &package : mData.mAiPackage.mList) - printAIPackage(package); - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + std::cout << " Male:" << std::endl; + for (int j = 0; j < ESM::Attribute::Length; ++j) + { + ESM::RefId id = ESM::Attribute::indexToRefId(j); + std::cout << " " << id << ": " << mData.mData.getAttribute(id, true) << std::endl; + } + std::cout << " Height: " << mData.mData.mMaleHeight << std::endl; + std::cout << " Weight: " << mData.mData.mMaleWeight << std::endl; -template<> -void Record::print() -{ - std::cout << " Type: " << dialogTypeLabel(mData.mType) - << " (" << (int)mData.mType << ")" << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; - // Sadly, there are no DialInfos, because the loader dumps as it - // loads, rather than loading and then dumping. :-( Anyone mind if - // I change this? - for (const ESM::DialInfo &info : mData.mInfo) - std::cout << "INFO!" << info.mId << std::endl; -} + std::cout << " Female:" << std::endl; + for (int j = 0; j < ESM::Attribute::Length; ++j) + { + ESM::RefId id = ESM::Attribute::indexToRefId(j); + std::cout << " " << id << ": " << mData.mData.getAttribute(id, false) << std::endl; + } + std::cout << " Height: " << mData.mData.mFemaleHeight << std::endl; + std::cout << " Weight: " << mData.mData.mFemaleWeight << std::endl; -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " OpenSound: " << mData.mOpenSound << std::endl; - std::cout << " CloseSound: " << mData.mCloseSound << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + for (const auto& bonus : mData.mData.mBonus) + // Not all races have 7 skills. + if (bonus.mSkill != -1) + std::cout << " Skill: " << skillLabel(bonus.mSkill) << " (" << bonus.mSkill << ") = " << bonus.mBonus + << std::endl; -template<> -void Record::print() -{ - std::cout << " Type: " << enchantTypeLabel(mData.mData.mType) - << " (" << mData.mData.mType << ")" << std::endl; - std::cout << " Cost: " << mData.mData.mCost << std::endl; - std::cout << " Charge: " << mData.mData.mCharge << std::endl; - std::cout << " Flags: " << enchantmentFlags(mData.mData.mFlags) << std::endl; - printEffectList(mData.mEffects); - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + for (const auto& power : mData.mPowers.mList) + std::cout << " Power: " << power << std::endl; -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Hidden: " << mData.mData.mIsHidden << std::endl; - std::cout << " Attribute1: " << attributeLabel(mData.mData.mAttribute[0]) - << " (" << mData.mData.mAttribute[0] << ")" << std::endl; - std::cout << " Attribute2: " << attributeLabel(mData.mData.mAttribute[1]) - << " (" << mData.mData.mAttribute[1] << ")" << std::endl; - for (int skill : mData.mData.mSkills) - if (skill != -1) - std::cout << " Skill: " << skillLabel(skill) << " (" << skill << ")" << std::endl; - for (int i = 0; i != 10; i++) - if (!mData.mRanks[i].empty()) - { - std::cout << " Rank: " << mData.mRanks[i] << std::endl; - std::cout << " Attribute1 Requirement: " - << mData.mData.mRankData[i].mAttribute1 << std::endl; - std::cout << " Attribute2 Requirement: " - << mData.mData.mRankData[i].mAttribute2 << std::endl; - std::cout << " One Skill at Level: " - << mData.mData.mRankData[i].mPrimarySkill << std::endl; - std::cout << " Two Skills at Level: " - << mData.mData.mRankData[i].mFavouredSkill << std::endl; - std::cout << " Faction Reaction: " - << mData.mData.mRankData[i].mFactReaction << std::endl; - } - for (const auto &reaction : mData.mReactions) - std::cout << " Reaction: " << reaction.second << " = " << reaction.first << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + std::cout << " Deleted: " << mIsDeleted << std::endl; + } -template<> -void Record::print() -{ - std::cout << " " << mData.mValue << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + template <> + void Record::print() + { + std::cout << " Name: " << mData.mName << std::endl; -template<> -void Record::print() -{ - std::cout << " " << mData.mValue << std::endl; -} + std::cout << " Weather:" << std::endl; + std::array weathers + = { "Clear", "Cloudy", "Fog", "Overcast", "Rain", "Thunder", "Ash", "Blight", "Snow", "Blizzard" }; + for (size_t i = 0; i < weathers.size(); ++i) + std::cout << " " << weathers[i] << ": " << static_cast(mData.mData.mProbabilities[i]) + << std::endl; + std::cout << " Map Color: " << mData.mMapColor << std::endl; + if (!mData.mSleepList.empty()) + std::cout << " Sleep List: " << mData.mSleepList << std::endl; + for (const ESM::Region::SoundRef& soundref : mData.mSoundList) + std::cout << " Sound: " << (int)soundref.mChance << " = " << soundref.mSound << std::endl; + } -template<> -void Record::print() -{ - std::cout << " Id: " << mData.mId << std::endl; - if (!mData.mPrev.empty()) - std::cout << " Previous ID: " << mData.mPrev << std::endl; - if (!mData.mNext.empty()) - std::cout << " Next ID: " << mData.mNext << std::endl; - std::cout << " Text: " << mData.mResponse << std::endl; - if (!mData.mActor.empty()) - std::cout << " Actor: " << mData.mActor << std::endl; - if (!mData.mRace.empty()) - std::cout << " Race: " << mData.mRace << std::endl; - if (!mData.mClass.empty()) - std::cout << " Class: " << mData.mClass << std::endl; - std::cout << " Factionless: " << mData.mFactionLess << std::endl; - if (!mData.mFaction.empty()) - std::cout << " NPC Faction: " << mData.mFaction << std::endl; - if (mData.mData.mRank != -1) - std::cout << " NPC Rank: " << (int)mData.mData.mRank << std::endl; - if (!mData.mPcFaction.empty()) - std::cout << " PC Faction: " << mData.mPcFaction << std::endl; - // CHANGE? non-standard capitalization mPCrank -> mPCRank (mPcRank?) - if (mData.mData.mPCrank != -1) - std::cout << " PC Rank: " << (int)mData.mData.mPCrank << std::endl; - if (!mData.mCell.empty()) - std::cout << " Cell: " << mData.mCell << std::endl; - if (mData.mData.mDisposition > 0) - std::cout << " Disposition/Journal index: " << mData.mData.mDisposition << std::endl; - if (mData.mData.mGender != ESM::DialInfo::NA) - std::cout << " Gender: " << mData.mData.mGender << std::endl; - if (!mData.mSound.empty()) - std::cout << " Sound File: " << mData.mSound << std::endl; + template <> + void Record::print() + { + std::cout << " Name: " << mData.mId << std::endl; + std::cout << " Num Shorts: " << mData.mNumShorts << std::endl; + std::cout << " Num Longs: " << mData.mNumLongs << std::endl; + std::cout << " Num Floats: " << mData.mNumFloats << std::endl; + std::cout << " Script Data Size: " << mData.mScriptData.size() << std::endl; + std::cout << " Table Size: " << ESM::computeScriptStringTableSize(mData.mVarNames) << std::endl; - std::cout << " Quest Status: " << questStatusLabel(mData.mQuestStatus) - << " (" << mData.mQuestStatus << ")" << std::endl; - std::cout << " Unknown1: " << mData.mData.mUnknown1 << std::endl; - std::cout << " Unknown2: " << (int)mData.mData.mUnknown2 << std::endl; + for (const std::string& variable : mData.mVarNames) + std::cout << " Variable: " << variable << std::endl; - for (const ESM::DialInfo::SelectStruct &rule : mData.mSelects) - std::cout << " Select Rule: " << ruleString(rule) << std::endl; + std::cout << " ByteCode: "; + for (const unsigned char& byte : mData.mScriptData) + std::cout << Misc::StringUtils::format("%02X", (int)(byte)); + std::cout << std::endl; - if (!mData.mResultScript.empty()) - { if (mPrintPlain) { - std::cout << " Result Script:" << std::endl; + std::cout << " Script:" << std::endl; std::cout << "START--------------------------------------" << std::endl; - std::cout << mData.mResultScript << std::endl; + std::cout << mData.mScriptText << std::endl; std::cout << "END----------------------------------------" << std::endl; } else { - std::cout << " Result Script: [skipped]" << std::endl; + std::cout << " Script: [skipped]" << std::endl; } + + std::cout << " Deleted: " << mIsDeleted << std::endl; } - std::cout << " Deleted: " << mIsDeleted << std::endl; -} -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Icon: " << mData.mIcon << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - for (int i = 0; i !=4; i++) + template <> + void Record::print() { - // A value of -1 means no effect - if (mData.mData.mEffectID[i] == -1) continue; - std::cout << " Effect: " << magicEffectLabel(mData.mData.mEffectID[i]) - << " (" << mData.mData.mEffectID[i] << ")" << std::endl; - std::cout << " Skill: " << skillLabel(mData.mData.mSkills[i]) - << " (" << mData.mData.mSkills[i] << ")" << std::endl; - std::cout << " Attribute: " << attributeLabel(mData.mData.mAttributes[i]) - << " (" << mData.mData.mAttributes[i] << ")" << std::endl; + int index = ESM::Skill::refIdToIndex(mData.mId); + std::cout << " ID: " << skillLabel(index) << " (" << index << ")" << std::endl; + std::cout << " Description: " << mData.mDescription << std::endl; + std::cout << " Governing Attribute: " << attributeLabel(mData.mData.mAttribute) << " (" + << mData.mData.mAttribute << ")" << std::endl; + std::cout << " Specialization: " << specializationLabel(mData.mData.mSpecialization) << " (" + << mData.mData.mSpecialization << ")" << std::endl; + for (int i = 0; i != 4; i++) + std::cout << " UseValue[" << i << "]:" << mData.mData.mUseValue[i] << std::endl; } - std::cout << " Deleted: " << mIsDeleted << std::endl; -} -template<> -void Record::print() -{ - std::cout << " Coordinates: (" << mData.mX << "," << mData.mY << ")" << std::endl; - std::cout << " Flags: " << landFlags(mData.mFlags) << std::endl; - std::cout << " DataTypes: " << mData.mDataTypes << std::endl; - - if (const ESM::Land::LandData *data = mData.getLandData (mData.mDataTypes)) + template <> + void Record::print() { - std::cout << " Height Offset: " << data->mHeightOffset << std::endl; - // Lots of missing members. - std::cout << " Unknown1: " << data->mUnk1 << std::endl; - std::cout << " Unknown2: " << data->mUnk2 << std::endl; + if (!mData.mCreature.empty()) + std::cout << " Creature: " << mData.mCreature << std::endl; + std::cout << " Sound: " << mData.mSound << std::endl; + std::cout << " Type: " << soundTypeLabel(mData.mType) << " (" << mData.mType << ")" << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - mData.unloadData(); - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Chance for None: " << (int)mData.mChanceNone << std::endl; - std::cout << " Flags: " << creatureListFlags(mData.mFlags) << std::endl; - std::cout << " Number of items: " << mData.mList.size() << std::endl; - for (const ESM::LevelledListBase::LevelItem &item : mData.mList) - std::cout << " Creature: Level: " << item.mLevel - << " Creature: " << item.mId << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} -template<> -void Record::print() -{ - std::cout << " Chance for None: " << (int)mData.mChanceNone << std::endl; - std::cout << " Flags: " << itemListFlags(mData.mFlags) << std::endl; - std::cout << " Number of items: " << mData.mList.size() << std::endl; - for (const ESM::LevelledListBase::LevelItem &item : mData.mList) - std::cout << " Inventory: Level: " << item.mLevel - << " Item: " << item.mId << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + template <> + void Record::print() + { + std::cout << " Sound: " << mData.mSound << std::endl; + std::cout << " Volume: " << (int)mData.mData.mVolume << std::endl; + if (mData.mData.mMinRange != 0 && mData.mData.mMaxRange != 0) + std::cout << " Range: " << (int)mData.mData.mMinRange << " - " << (int)mData.mData.mMaxRange << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; + } -template<> -void Record::print() -{ - if (!mData.mName.empty()) + template <> + void Record::print() + { std::cout << " Name: " << mData.mName << std::endl; - if (!mData.mModel.empty()) - std::cout << " Model: " << mData.mModel << std::endl; - if (!mData.mIcon.empty()) - std::cout << " Icon: " << mData.mIcon << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " Flags: " << lightFlags(mData.mData.mFlags) << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " Sound: " << mData.mSound << std::endl; - std::cout << " Duration: " << mData.mData.mTime << std::endl; - std::cout << " Radius: " << mData.mData.mRadius << std::endl; - std::cout << " Color: " << mData.mData.mColor << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Icon: " << mData.mIcon << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " Quality: " << mData.mData.mQuality << std::endl; - std::cout << " Uses: " << mData.mData.mUses << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Icon: " << mData.mIcon << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " Quality: " << mData.mData.mQuality << std::endl; - std::cout << " Uses: " << mData.mData.mUses << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Icon: " << mData.mIcon << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " Quality: " << mData.mData.mQuality << std::endl; - std::cout << " Uses: " << mData.mData.mUses << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Id: " << mData.mId << std::endl; - std::cout << " Index: " << mData.mIndex << std::endl; - std::cout << " Texture: " << mData.mTexture << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Index: " << magicEffectLabel(mData.mIndex) - << " (" << mData.mIndex << ")" << std::endl; - std::cout << " Description: " << mData.mDescription << std::endl; - std::cout << " Icon: " << mData.mIcon << std::endl; - std::cout << " Flags: " << magicEffectFlags(mData.mData.mFlags) << std::endl; - std::cout << " Particle Texture: " << mData.mParticle << std::endl; - if (!mData.mCasting.empty()) - std::cout << " Casting Static: " << mData.mCasting << std::endl; - if (!mData.mCastSound.empty()) - std::cout << " Casting Sound: " << mData.mCastSound << std::endl; - if (!mData.mBolt.empty()) - std::cout << " Bolt Static: " << mData.mBolt << std::endl; - if (!mData.mBoltSound.empty()) - std::cout << " Bolt Sound: " << mData.mBoltSound << std::endl; - if (!mData.mHit.empty()) - std::cout << " Hit Static: " << mData.mHit << std::endl; - if (!mData.mHitSound.empty()) - std::cout << " Hit Sound: " << mData.mHitSound << std::endl; - if (!mData.mArea.empty()) - std::cout << " Area Static: " << mData.mArea << std::endl; - if (!mData.mAreaSound.empty()) - std::cout << " Area Sound: " << mData.mAreaSound << std::endl; - std::cout << " School: " << schoolLabel(mData.mData.mSchool) - << " (" << mData.mData.mSchool << ")" << std::endl; - std::cout << " Base Cost: " << mData.mData.mBaseCost << std::endl; - std::cout << " Unknown 1: " << mData.mData.mUnknown1 << std::endl; - std::cout << " Speed: " << mData.mData.mSpeed << std::endl; - std::cout << " Unknown 2: " << mData.mData.mUnknown2 << std::endl; - std::cout << " RGB Color: " << "(" - << mData.mData.mRed << "," - << mData.mData.mGreen << "," - << mData.mData.mBlue << ")" << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - std::cout << " Icon: " << mData.mIcon << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " Is Key: " << mData.mData.mIsKey << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} + std::cout << " Type: " << spellTypeLabel(mData.mData.mType) << " (" << mData.mData.mType << ")" << std::endl; + std::cout << " Flags: " << spellFlags(mData.mData.mFlags) << std::endl; + std::cout << " Cost: " << mData.mData.mCost << std::endl; + printEffectList(mData.mEffects); + std::cout << " Deleted: " << mIsDeleted << std::endl; + } -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Animation: " << mData.mModel << std::endl; - std::cout << " Hair Model: " << mData.mHair << std::endl; - std::cout << " Head Model: " << mData.mHead << std::endl; - std::cout << " Race: " << mData.mRace << std::endl; - std::cout << " Class: " << mData.mClass << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - if (!mData.mFaction.empty()) - std::cout << " Faction: " << mData.mFaction << std::endl; - std::cout << " Flags: " << npcFlags((int)mData.mFlags) << std::endl; - if (mData.mBloodType != 0) - std::cout << " Blood Type: " << mData.mBloodType+1 << std::endl; - - if (mData.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + template <> + void Record::print() { - std::cout << " Level: " << mData.mNpdt.mLevel << std::endl; - std::cout << " Reputation: " << (int)mData.mNpdt.mReputation << std::endl; - std::cout << " Disposition: " << (int)mData.mNpdt.mDisposition << std::endl; - std::cout << " Rank: " << (int)mData.mNpdt.mRank << std::endl; - std::cout << " Gold: " << mData.mNpdt.mGold << std::endl; + std::cout << " Start Script: " << mData.mId << std::endl; + std::cout << " Start Data: " << mData.mData << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; } - else { - std::cout << " Level: " << mData.mNpdt.mLevel << std::endl; - std::cout << " Reputation: " << (int)mData.mNpdt.mReputation << std::endl; - std::cout << " Disposition: " << (int)mData.mNpdt.mDisposition << std::endl; - std::cout << " Rank: " << (int)mData.mNpdt.mRank << std::endl; - std::cout << " Attributes:" << std::endl; - std::cout << " Strength: " << (int)mData.mNpdt.mStrength << std::endl; - std::cout << " Intelligence: " << (int)mData.mNpdt.mIntelligence << std::endl; - std::cout << " Willpower: " << (int)mData.mNpdt.mWillpower << std::endl; - std::cout << " Agility: " << (int)mData.mNpdt.mAgility << std::endl; - std::cout << " Speed: " << (int)mData.mNpdt.mSpeed << std::endl; - std::cout << " Endurance: " << (int)mData.mNpdt.mEndurance << std::endl; - std::cout << " Personality: " << (int)mData.mNpdt.mPersonality << std::endl; - std::cout << " Luck: " << (int)mData.mNpdt.mLuck << std::endl; - - std::cout << " Skills:" << std::endl; - for (int i = 0; i != ESM::Skill::Length; i++) - std::cout << " " << skillLabel(i) << ": " - << (int)(mData.mNpdt.mSkills[i]) << std::endl; - - std::cout << " Health: " << mData.mNpdt.mHealth << std::endl; - std::cout << " Magicka: " << mData.mNpdt.mMana << std::endl; - std::cout << " Fatigue: " << mData.mNpdt.mFatigue << std::endl; - std::cout << " Gold: " << mData.mNpdt.mGold << std::endl; + template <> + void Record::print() + { + std::cout << " Model: " << mData.mModel << std::endl; } - for (const ESM::ContItem &item : mData.mInventory.mList) - std::cout << " Inventory: Count: " << Misc::StringUtils::format("%4d", item.mCount) - << " Item: " << item.mItem << std::endl; - - for (const std::string &spell : mData.mSpells.mList) - std::cout << " Spell: " << spell << std::endl; - - printTransport(mData.getTransport()); - - std::cout << " Artificial Intelligence: " << std::endl; - std::cout << " AI Hello:" << (int)mData.mAiData.mHello << std::endl; - std::cout << " AI Fight:" << (int)mData.mAiData.mFight << std::endl; - std::cout << " AI Flee:" << (int)mData.mAiData.mFlee << std::endl; - std::cout << " AI Alarm:" << (int)mData.mAiData.mAlarm << std::endl; - std::cout << " AI U1:" << (int)mData.mAiData.mU1 << std::endl; - std::cout << " AI U2:" << (int)mData.mAiData.mU2 << std::endl; - std::cout << " AI U3:" << (int)mData.mAiData.mU3 << std::endl; - std::cout << " AI Services:" << Misc::StringUtils::format("0x%08X", mData.mAiData.mServices) << std::endl; - - for (const ESM::AIPackage &package : mData.mAiPackage.mList) - printAIPackage(package); - - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Cell: " << mData.mCell << std::endl; - std::cout << " Coordinates: (" << mData.mData.mX << "," << mData.mData.mY << ")" << std::endl; - std::cout << " Unknown S1: " << mData.mData.mS1 << std::endl; - if ((unsigned int)mData.mData.mS2 != mData.mPoints.size()) - std::cout << " Reported Point Count: " << mData.mData.mS2 << std::endl; - std::cout << " Point Count: " << mData.mPoints.size() << std::endl; - std::cout << " Edge Count: " << mData.mEdges.size() << std::endl; - - int i = 0; - for (const ESM::Pathgrid::Point &point : mData.mPoints) + template <> + void Record::print() { - std::cout << " Point[" << i << "]:" << std::endl; - std::cout << " Coordinates: (" << point.mX << "," - << point.mY << "," << point.mZ << ")" << std::endl; - std::cout << " Auto-Generated: " << (int)point.mAutogenerated << std::endl; - std::cout << " Connections: " << (int)point.mConnectionNum << std::endl; - std::cout << " Unknown: " << point.mUnknown << std::endl; - i++; + // No names on VFX bolts + if (!mData.mName.empty()) + std::cout << " Name: " << mData.mName << std::endl; + std::cout << " Model: " << mData.mModel << std::endl; + // No icons on VFX bolts or magic bolts + if (!mData.mIcon.empty()) + std::cout << " Icon: " << mData.mIcon << std::endl; + if (!mData.mScript.empty()) + std::cout << " Script: " << mData.mScript << std::endl; + if (!mData.mEnchant.empty()) + std::cout << " Enchantment: " << mData.mEnchant << std::endl; + std::cout << " Type: " << weaponTypeLabel(mData.mData.mType) << " (" << mData.mData.mType << ")" << std::endl; + std::cout << " Flags: " << weaponFlags(mData.mData.mFlags) << std::endl; + std::cout << " Weight: " << mData.mData.mWeight << std::endl; + std::cout << " Value: " << mData.mData.mValue << std::endl; + std::cout << " Health: " << mData.mData.mHealth << std::endl; + std::cout << " Speed: " << mData.mData.mSpeed << std::endl; + std::cout << " Reach: " << mData.mData.mReach << std::endl; + std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; + if (mData.mData.mChop[0] != 0 && mData.mData.mChop[1] != 0) + std::cout << " Chop: " << (int)mData.mData.mChop[0] << "-" << (int)mData.mData.mChop[1] << std::endl; + if (mData.mData.mSlash[0] != 0 && mData.mData.mSlash[1] != 0) + std::cout << " Slash: " << (int)mData.mData.mSlash[0] << "-" << (int)mData.mData.mSlash[1] << std::endl; + if (mData.mData.mThrust[0] != 0 && mData.mData.mThrust[1] != 0) + std::cout << " Thrust: " << (int)mData.mData.mThrust[0] << "-" << (int)mData.mData.mThrust[1] << std::endl; + std::cout << " Deleted: " << mIsDeleted << std::endl; + } + + template <> + void Record::print() + { + std::cout << " Cell Id: \"" << mData.mCellState.mId.toString() << "\"" << std::endl; + std::cout << " Water Level: " << mData.mCellState.mWaterLevel << std::endl; + std::cout << " Has Fog Of War: " << mData.mCellState.mHasFogOfWar << std::endl; + std::cout << " Last Respawn:" << std::endl; + std::cout << " Day:" << mData.mCellState.mLastRespawn.mDay << std::endl; + std::cout << " Hour:" << mData.mCellState.mLastRespawn.mHour << std::endl; + if (mData.mCellState.mHasFogOfWar) + { + std::cout << " North Marker Angle: " << mData.mFogState.mNorthMarkerAngle << std::endl; + std::cout << " Bounds:" << std::endl; + std::cout << " Min X: " << mData.mFogState.mBounds.mMinX << std::endl; + std::cout << " Min Y: " << mData.mFogState.mBounds.mMinY << std::endl; + std::cout << " Max X: " << mData.mFogState.mBounds.mMaxX << std::endl; + std::cout << " Max Y: " << mData.mFogState.mBounds.mMaxY << std::endl; + for (const ESM::FogTexture& fogTexture : mData.mFogState.mFogTextures) + { + std::cout << " Fog Texture:" << std::endl; + std::cout << " X: " << fogTexture.mX << std::endl; + std::cout << " Y: " << fogTexture.mY << std::endl; + std::cout << " Image Data: (" << fogTexture.mImageData.size() << ")" << std::endl; + } + } } - i = 0; - for (const ESM::Pathgrid::Edge &edge : mData.mEdges) + template <> + std::string Record::getId() const { - std::cout << " Edge[" << i << "]: " << edge.mV0 << " -> " << edge.mV1 << std::endl; - if (edge.mV0 >= mData.mData.mS2 || edge.mV1 >= mData.mData.mS2) - std::cout << " BAD POINT IN EDGE!" << std::endl; - i++; + return std::string(); // No ID for Cell record } - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - static const char *sAttributeNames[8] = + template <> + std::string Record::getId() const { - "Strength", "Intelligence", "Willpower", "Agility", - "Speed", "Endurance", "Personality", "Luck" - }; - - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Description: " << mData.mDescription << std::endl; - std::cout << " Flags: " << raceFlags(mData.mData.mFlags) << std::endl; + return std::string(); // No ID for Land record + } - for (int i=0; i<2; ++i) + template <> + std::string Record::getId() const { - bool male = i==0; - - std::cout << (male ? " Male:" : " Female:") << std::endl; - - for (int j=0; j<8; ++j) - std::cout << " " << sAttributeNames[j] << ": " - << mData.mData.mAttributeValues[j].getValue (male) << std::endl; - - std::cout << " Height: " << mData.mData.mHeight.getValue (male) << std::endl; - std::cout << " Weight: " << mData.mData.mWeight.getValue (male) << std::endl; + return std::string(); // No ID for MagicEffect record } - for (int i = 0; i != 7; i++) - // Not all races have 7 skills. - if (mData.mData.mBonus[i].mSkill != -1) - std::cout << " Skill: " - << skillLabel(mData.mData.mBonus[i].mSkill) - << " (" << mData.mData.mBonus[i].mSkill << ") = " - << mData.mData.mBonus[i].mBonus << std::endl; - - for (const std::string &power : mData.mPowers.mList) - std::cout << " Power: " << power << std::endl; - - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - - std::cout << " Weather:" << std::endl; - std::cout << " Clear: " << (int)mData.mData.mClear << std::endl; - std::cout << " Cloudy: " << (int)mData.mData.mCloudy << std::endl; - std::cout << " Foggy: " << (int)mData.mData.mFoggy << std::endl; - std::cout << " Overcast: " << (int)mData.mData.mOvercast << std::endl; - std::cout << " Rain: " << (int)mData.mData.mOvercast << std::endl; - std::cout << " Thunder: " << (int)mData.mData.mThunder << std::endl; - std::cout << " Ash: " << (int)mData.mData.mAsh << std::endl; - std::cout << " Blight: " << (int)mData.mData.mBlight << std::endl; - std::cout << " UnknownA: " << (int)mData.mData.mA << std::endl; - std::cout << " UnknownB: " << (int)mData.mData.mB << std::endl; - std::cout << " Map Color: " << mData.mMapColor << std::endl; - if (!mData.mSleepList.empty()) - std::cout << " Sleep List: " << mData.mSleepList << std::endl; - for (const ESM::Region::SoundRef &soundref : mData.mSoundList) - std::cout << " Sound: " << (int)soundref.mChance << " = " << soundref.mSound << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mId << std::endl; - - std::cout << " Num Shorts: " << mData.mData.mNumShorts << std::endl; - std::cout << " Num Longs: " << mData.mData.mNumLongs << std::endl; - std::cout << " Num Floats: " << mData.mData.mNumFloats << std::endl; - std::cout << " Script Data Size: " << mData.mData.mScriptDataSize << std::endl; - std::cout << " Table Size: " << mData.mData.mStringTableSize << std::endl; - - for (const std::string &variable : mData.mVarNames) - std::cout << " Variable: " << variable << std::endl; - - std::cout << " ByteCode: "; - for (const unsigned char &byte : mData.mScriptData) - std::cout << Misc::StringUtils::format("%02X", (int)(byte)); - std::cout << std::endl; - - if (mPrintPlain) + template <> + std::string Record::getId() const { - std::cout << " Script:" << std::endl; - std::cout << "START--------------------------------------" << std::endl; - std::cout << mData.mScriptText << std::endl; - std::cout << "END----------------------------------------" << std::endl; + return std::string(); // No ID for Pathgrid record } - else + + template <> + std::string Record::getId() const { - std::cout << " Script: [skipped]" << std::endl; + return std::string(); // No ID for Skill record } - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " ID: " << skillLabel(mData.mIndex) - << " (" << mData.mIndex << ")" << std::endl; - std::cout << " Description: " << mData.mDescription << std::endl; - std::cout << " Governing Attribute: " << attributeLabel(mData.mData.mAttribute) - << " (" << mData.mData.mAttribute << ")" << std::endl; - std::cout << " Specialization: " << specializationLabel(mData.mData.mSpecialization) - << " (" << mData.mData.mSpecialization << ")" << std::endl; - for (int i = 0; i != 4; i++) - std::cout << " UseValue[" << i << "]:" << mData.mData.mUseValue[i] << std::endl; -} - -template<> -void Record::print() -{ - if (!mData.mCreature.empty()) - std::cout << " Creature: " << mData.mCreature << std::endl; - std::cout << " Sound: " << mData.mSound << std::endl; - std::cout << " Type: " << soundTypeLabel(mData.mType) - << " (" << mData.mType << ")" << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Sound: " << mData.mSound << std::endl; - std::cout << " Volume: " << (int)mData.mData.mVolume << std::endl; - if (mData.mData.mMinRange != 0 && mData.mData.mMaxRange != 0) - std::cout << " Range: " << (int)mData.mData.mMinRange << " - " - << (int)mData.mData.mMaxRange << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Type: " << spellTypeLabel(mData.mData.mType) - << " (" << mData.mData.mType << ")" << std::endl; - std::cout << " Flags: " << spellFlags(mData.mData.mFlags) << std::endl; - std::cout << " Cost: " << mData.mData.mCost << std::endl; - printEffectList(mData.mEffects); - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Start Script: " << mData.mId << std::endl; - std::cout << " Start Data: " << mData.mData << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -void Record::print() -{ - std::cout << " Model: " << mData.mModel << std::endl; -} - -template<> -void Record::print() -{ - // No names on VFX bolts - if (!mData.mName.empty()) - std::cout << " Name: " << mData.mName << std::endl; - std::cout << " Model: " << mData.mModel << std::endl; - // No icons on VFX bolts or magic bolts - if (!mData.mIcon.empty()) - std::cout << " Icon: " << mData.mIcon << std::endl; - if (!mData.mScript.empty()) - std::cout << " Script: " << mData.mScript << std::endl; - if (!mData.mEnchant.empty()) - std::cout << " Enchantment: " << mData.mEnchant << std::endl; - std::cout << " Type: " << weaponTypeLabel(mData.mData.mType) - << " (" << mData.mData.mType << ")" << std::endl; - std::cout << " Flags: " << weaponFlags(mData.mData.mFlags) << std::endl; - std::cout << " Weight: " << mData.mData.mWeight << std::endl; - std::cout << " Value: " << mData.mData.mValue << std::endl; - std::cout << " Health: " << mData.mData.mHealth << std::endl; - std::cout << " Speed: " << mData.mData.mSpeed << std::endl; - std::cout << " Reach: " << mData.mData.mReach << std::endl; - std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; - if (mData.mData.mChop[0] != 0 && mData.mData.mChop[1] != 0) - std::cout << " Chop: " << (int)mData.mData.mChop[0] << "-" - << (int)mData.mData.mChop[1] << std::endl; - if (mData.mData.mSlash[0] != 0 && mData.mData.mSlash[1] != 0) - std::cout << " Slash: " << (int)mData.mData.mSlash[0] << "-" - << (int)mData.mData.mSlash[1] << std::endl; - if (mData.mData.mThrust[0] != 0 && mData.mData.mThrust[1] != 0) - std::cout << " Thrust: " << (int)mData.mData.mThrust[0] << "-" - << (int)mData.mData.mThrust[1] << std::endl; - std::cout << " Deleted: " << mIsDeleted << std::endl; -} - -template<> -std::string Record::getId() const -{ - return mData.mName; -} - -template<> -std::string Record::getId() const -{ - return std::string(); // No ID for Land record -} - -template<> -std::string Record::getId() const -{ - return std::string(); // No ID for MagicEffect record -} - -template<> -std::string Record::getId() const -{ - return std::string(); // No ID for Pathgrid record -} - -template<> -std::string Record::getId() const -{ - return std::string(); // No ID for Skill record -} + template <> + std::string Record::getId() const + { + return std::string(); // No ID for CellState record + } } // end namespace diff --git a/apps/esmtool/record.hpp b/apps/esmtool/record.hpp index bbb3dd09887..eab133b6b34 100644 --- a/apps/esmtool/record.hpp +++ b/apps/esmtool/record.hpp @@ -1,9 +1,12 @@ #ifndef OPENMW_ESMTOOL_RECORD_H #define OPENMW_ESMTOOL_RECORD_H +#include #include #include +#include +#include namespace ESM { @@ -13,7 +16,8 @@ namespace ESM namespace EsmTool { - template class Record; + template + class Record; class RecordBase { @@ -24,45 +28,48 @@ namespace EsmTool bool mPrintPlain; public: - RecordBase () - : mFlags(0) - , mPrintPlain(false) + RecordBase() + : mFlags(0) + , mPrintPlain(false) { } - virtual ~RecordBase() {} + virtual ~RecordBase() = default; virtual std::string getId() const = 0; - uint32_t getFlags() const { - return mFlags; - } + uint32_t getFlags() const { return mFlags; } - void setFlags(uint32_t flags) { - mFlags = flags; - } + void setFlags(uint32_t flags) { mFlags = flags; } - ESM::NAME getType() const { - return mType; - } + ESM::NAME getType() const { return mType; } - void setPrintPlain(bool plain) { - mPrintPlain = plain; - } + void setPrintPlain(bool plain) { mPrintPlain = plain; } - virtual void load(ESM::ESMReader &esm) = 0; - virtual void save(ESM::ESMWriter &esm) = 0; + virtual void load(ESM::ESMReader& esm) = 0; + virtual void save(ESM::ESMWriter& esm) = 0; virtual void print() = 0; - static RecordBase *create(ESM::NAME type); + static std::unique_ptr create(ESM::NAME type); // just make it a bit shorter template - Record *cast() { - return static_cast *>(this); + Record* cast() + { + return static_cast*>(this); } }; + struct CellState + { + ESM::CellState mCellState; + ESM::FogState mFogState; + + void load(ESM::ESMReader& reader, bool& deleted); + + void save(ESM::ESMWriter& /*writer*/, bool /*deleted*/) {} + }; + template class Record : public RecordBase { @@ -72,75 +79,119 @@ namespace EsmTool public: Record() : mIsDeleted(false) - {} - - std::string getId() const override { - return mData.mId; + { } - T &get() { - return mData; - } + std::string getId() const override { return mData.mId.toDebugString(); } - void save(ESM::ESMWriter &esm) override { - mData.save(esm, mIsDeleted); - } + T& get() { return mData; } - void load(ESM::ESMReader &esm) override { - mData.load(esm, mIsDeleted); - } + void save(ESM::ESMWriter& esm) override { mData.save(esm, mIsDeleted); } + + void load(ESM::ESMReader& esm) override { mData.load(esm, mIsDeleted); } void print() override; }; - - template<> std::string Record::getId() const; - template<> std::string Record::getId() const; - template<> std::string Record::getId() const; - template<> std::string Record::getId() const; - template<> std::string Record::getId() const; - - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); - template<> void Record::print(); + + template <> + std::string Record::getId() const; + template <> + std::string Record::getId() const; + template <> + std::string Record::getId() const; + template <> + std::string Record::getId() const; + template <> + std::string Record::getId() const; + template <> + std::string Record::getId() const; + + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); + template <> + void Record::print(); } #endif diff --git a/apps/esmtool/tes4.cpp b/apps/esmtool/tes4.cpp new file mode 100644 index 00000000000..5b657da573c --- /dev/null +++ b/apps/esmtool/tes4.cpp @@ -0,0 +1,593 @@ +#include "tes4.hpp" +#include "arguments.hpp" +#include "labels.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace EsmTool +{ + namespace + { + struct Params + { + const bool mQuite; + + explicit Params(const Arguments& info) + : mQuite(info.quiet_given || info.mode == "clone") + { + } + }; + + std::string toString(ESM4::GroupType type) + { + switch (type) + { + case ESM4::Grp_RecordType: + return "RecordType"; + case ESM4::Grp_WorldChild: + return "WorldChild"; + case ESM4::Grp_InteriorCell: + return "InteriorCell"; + case ESM4::Grp_InteriorSubCell: + return "InteriorSubCell"; + case ESM4::Grp_ExteriorCell: + return "ExteriorCell"; + case ESM4::Grp_ExteriorSubCell: + return "ExteriorSubCell"; + case ESM4::Grp_CellChild: + return "CellChild"; + case ESM4::Grp_TopicChild: + return "TopicChild"; + case ESM4::Grp_CellPersistentChild: + return "CellPersistentChild"; + case ESM4::Grp_CellTemporaryChild: + return "CellTemporaryChild"; + case ESM4::Grp_CellVisibleDistChild: + return "CellVisibleDistChild"; + } + + return "Unknown (" + std::to_string(type) + ")"; + } + + template + struct WriteArray + { + std::string_view mPrefix; + const T& mValue; + + explicit WriteArray(std::string_view prefix, const T& value) + : mPrefix(prefix) + , mValue(value) + { + } + }; + + template + struct WriteData + { + const T& mValue; + + explicit WriteData(const T& value) + : mValue(value) + { + } + }; + + template + std::ostream& operator<<(std::ostream& stream, const WriteArray& write) + { + for (const auto& value : write.mValue) + stream << write.mPrefix << value; + return stream; + } + + template + std::ostream& operator<<(std::ostream& stream, const WriteData& /*write*/) + { + return stream << " ?"; + } + + std::ostream& operator<<(std::ostream& stream, const std::monostate&) + { + return stream << "[none]"; + } + + std::ostream& operator<<(std::ostream& stream, const WriteData& write) + { + std::visit([&](const auto& v) { stream << v; }, write.mValue); + return stream; + } + + struct WriteCellFlags + { + std::uint16_t mValue; + }; + + using CellFlagString = Debug::FlagString; + + constexpr std::array cellFlags{ + CellFlagString{ ESM4::CELL_Interior, "Interior" }, + CellFlagString{ ESM4::CELL_HasWater, "HasWater" }, + CellFlagString{ ESM4::CELL_NoTravel, "NoTravel" }, + CellFlagString{ ESM4::CELL_HideLand, "HideLand" }, + CellFlagString{ ESM4::CELL_Public, "Public" }, + CellFlagString{ ESM4::CELL_HandChgd, "HandChgd" }, + CellFlagString{ ESM4::CELL_QuasiExt, "QuasiExt" }, + CellFlagString{ ESM4::CELL_SkyLight, "SkyLight" }, + }; + + std::ostream& operator<<(std::ostream& stream, const WriteCellFlags& write) + { + return Debug::writeFlags(stream, write.mValue, cellFlags); + } + + template + void readTypedRecord(const Params& params, ESM4::Reader& reader) + { + reader.getRecordData(); + + T value; + value.load(reader); + + if (params.mQuite) + return; + + std::cout << "\n Record: " << ESM::NAME(reader.hdr().record.typeId).toStringView(); + if constexpr (ESM::hasId) + std::cout << "\n Id: " << value.mId; + if constexpr (ESM4::hasFlags) + std::cout << "\n Record flags: " << recordFlags(value.mFlags); + if constexpr (ESM4::hasParent) + std::cout << "\n Parent: " << value.mParent; + if constexpr (ESM4::hasEditorId) + std::cout << "\n EditorId: " << value.mEditorId; + if constexpr (ESM4::hasFullName) + std::cout << "\n FullName: " << value.mFullName; + if constexpr (ESM4::hasCellFlags) + std::cout << "\n CellFlags: " << WriteCellFlags{ value.mCellFlags }; + if constexpr (ESM4::hasX) + std::cout << "\n X: " << value.mX; + if constexpr (ESM4::hasY) + std::cout << "\n Y: " << value.mY; + if constexpr (ESM::hasModel) + std::cout << "\n Model: " << value.mModel; + if constexpr (ESM4::hasNif) + std::cout << "\n Nif:" << WriteArray("\n - ", value.mNif); + if constexpr (ESM4::hasKf) + std::cout << "\n Kf:" << WriteArray("\n - ", value.mKf); + if constexpr (ESM4::hasType) + std::cout << "\n Type: " << value.mType; + if constexpr (ESM4::hasValue) + std::cout << "\n Value: " << value.mValue; + if constexpr (ESM4::hasData) + std::cout << "\n Data: " << WriteData(value.mData); + std::cout << '\n'; + } + + bool readRecord(const Params& params, ESM4::Reader& reader) + { + switch (static_cast(reader.hdr().record.typeId)) + { + case ESM4::REC_AACT: + break; + case ESM4::REC_ACHR: + readTypedRecord(params, reader); + return true; + case ESM4::REC_ACRE: + readTypedRecord(params, reader); + return true; + case ESM4::REC_ACTI: + readTypedRecord(params, reader); + return true; + case ESM4::REC_ADDN: + break; + case ESM4::REC_ALCH: + readTypedRecord(params, reader); + return true; + case ESM4::REC_ALOC: + readTypedRecord(params, reader); + return true; + case ESM4::REC_AMMO: + readTypedRecord(params, reader); + return true; + case ESM4::REC_ANIO: + readTypedRecord(params, reader); + return true; + case ESM4::REC_APPA: + readTypedRecord(params, reader); + return true; + case ESM4::REC_ARMA: + readTypedRecord(params, reader); + return true; + case ESM4::REC_ARMO: + readTypedRecord(params, reader); + return true; + case ESM4::REC_ARTO: + break; + case ESM4::REC_ASPC: + readTypedRecord(params, reader); + return true; + case ESM4::REC_ASTP: + break; + case ESM4::REC_AVIF: + break; + case ESM4::REC_BOOK: + readTypedRecord(params, reader); + return true; + case ESM4::REC_BPTD: + readTypedRecord(params, reader); + return true; + case ESM4::REC_CAMS: + break; + case ESM4::REC_CCRD: + break; + case ESM4::REC_CELL: + readTypedRecord(params, reader); + return true; + case ESM4::REC_CLAS: + readTypedRecord(params, reader); + return true; + case ESM4::REC_CLFM: + readTypedRecord(params, reader); + return true; + case ESM4::REC_CLMT: + break; + case ESM4::REC_CLOT: + readTypedRecord(params, reader); + return true; + case ESM4::REC_CMNY: + break; + case ESM4::REC_COBJ: + break; + case ESM4::REC_COLL: + break; + case ESM4::REC_CONT: + readTypedRecord(params, reader); + return true; + case ESM4::REC_CPTH: + break; + case ESM4::REC_CREA: + readTypedRecord(params, reader); + return true; + case ESM4::REC_CSTY: + break; + case ESM4::REC_DEBR: + break; + case ESM4::REC_DIAL: + readTypedRecord(params, reader); + return true; + case ESM4::REC_DLBR: + break; + case ESM4::REC_DLVW: + break; + case ESM4::REC_DOBJ: + readTypedRecord(params, reader); + return true; + case ESM4::REC_DOOR: + readTypedRecord(params, reader); + return true; + case ESM4::REC_DUAL: + break; + case ESM4::REC_ECZN: + break; + case ESM4::REC_EFSH: + break; + case ESM4::REC_ENCH: + break; + case ESM4::REC_EQUP: + break; + case ESM4::REC_EXPL: + break; + case ESM4::REC_EYES: + readTypedRecord(params, reader); + return true; + case ESM4::REC_FACT: + break; + case ESM4::REC_FLOR: + readTypedRecord(params, reader); + return true; + case ESM4::REC_FLST: + readTypedRecord(params, reader); + return true; + case ESM4::REC_FSTP: + break; + case ESM4::REC_FSTS: + break; + case ESM4::REC_FURN: + readTypedRecord(params, reader); + return true; + case ESM4::REC_GLOB: + readTypedRecord(params, reader); + return true; + case ESM4::REC_GMST: + readTypedRecord(params, reader); + return true; + case ESM4::REC_GRAS: + readTypedRecord(params, reader); + return true; + case ESM4::REC_GRUP: + break; + case ESM4::REC_HAIR: + readTypedRecord(params, reader); + return true; + case ESM4::REC_HAZD: + break; + case ESM4::REC_HDPT: + readTypedRecord(params, reader); + return true; + case ESM4::REC_IDLE: + readTypedRecord(params, reader); + return true; + break; + case ESM4::REC_IDLM: + readTypedRecord(params, reader); + return true; + case ESM4::REC_IMAD: + break; + case ESM4::REC_IMGS: + break; + case ESM4::REC_IMOD: + readTypedRecord(params, reader); + return true; + case ESM4::REC_INFO: + readTypedRecord(params, reader); + return true; + case ESM4::REC_INGR: + readTypedRecord(params, reader); + return true; + case ESM4::REC_IPCT: + break; + case ESM4::REC_IPDS: + break; + case ESM4::REC_KEYM: + readTypedRecord(params, reader); + return true; + case ESM4::REC_KYWD: + break; + case ESM4::REC_LAND: + readTypedRecord(params, reader); + return true; + case ESM4::REC_LCRT: + break; + case ESM4::REC_LCTN: + break; + case ESM4::REC_LGTM: + readTypedRecord(params, reader); + return true; + case ESM4::REC_LIGH: + readTypedRecord(params, reader); + return true; + case ESM4::REC_LSCR: + break; + case ESM4::REC_LTEX: + readTypedRecord(params, reader); + return true; + case ESM4::REC_LVLC: + readTypedRecord(params, reader); + return true; + case ESM4::REC_LVLI: + readTypedRecord(params, reader); + return true; + case ESM4::REC_LVLN: + readTypedRecord(params, reader); + return true; + case ESM4::REC_LVSP: + break; + case ESM4::REC_MATO: + readTypedRecord(params, reader); + return true; + case ESM4::REC_MATT: + break; + case ESM4::REC_MESG: + break; + case ESM4::REC_MGEF: + break; + case ESM4::REC_MISC: + readTypedRecord(params, reader); + return true; + case ESM4::REC_MOVT: + break; + case ESM4::REC_MSET: + readTypedRecord(params, reader); + return true; + case ESM4::REC_MSTT: + readTypedRecord(params, reader); + return true; + case ESM4::REC_MUSC: + readTypedRecord(params, reader); + return true; + case ESM4::REC_MUST: + break; + case ESM4::REC_NAVI: + readTypedRecord(params, reader); + return true; + case ESM4::REC_NAVM: + readTypedRecord(params, reader); + return true; + case ESM4::REC_NOTE: + readTypedRecord(params, reader); + return true; + case ESM4::REC_NPC_: + readTypedRecord(params, reader); + return true; + case ESM4::REC_OTFT: + readTypedRecord(params, reader); + return true; + case ESM4::REC_PACK: + readTypedRecord(params, reader); + return true; + case ESM4::REC_PERK: + break; + case ESM4::REC_PGRD: + readTypedRecord(params, reader); + return true; + case ESM4::REC_PGRE: + readTypedRecord(params, reader); + return true; + case ESM4::REC_PHZD: + break; + case ESM4::REC_PROJ: + break; + case ESM4::REC_PWAT: + readTypedRecord(params, reader); + return true; + case ESM4::REC_QUST: + readTypedRecord(params, reader); + return true; + case ESM4::REC_RACE: + readTypedRecord(params, reader); + return true; + case ESM4::REC_REFR: + readTypedRecord(params, reader); + return true; + case ESM4::REC_REGN: + readTypedRecord(params, reader); + return true; + case ESM4::REC_RELA: + break; + case ESM4::REC_REVB: + break; + case ESM4::REC_RFCT: + break; + case ESM4::REC_ROAD: + readTypedRecord(params, reader); + return true; + case ESM4::REC_SBSP: + readTypedRecord(params, reader); + return true; + case ESM4::REC_SCEN: + break; + case ESM4::REC_SCOL: + readTypedRecord(params, reader); + return true; + case ESM4::REC_SCPT: + readTypedRecord(params, reader); + return true; + case ESM4::REC_SCRL: + readTypedRecord(params, reader); + return true; + case ESM4::REC_SGST: + readTypedRecord(params, reader); + return true; + case ESM4::REC_SHOU: + break; + case ESM4::REC_SLGM: + readTypedRecord(params, reader); + return true; + case ESM4::REC_SMBN: + break; + case ESM4::REC_SMEN: + break; + case ESM4::REC_SMQN: + break; + case ESM4::REC_SNCT: + break; + case ESM4::REC_SNDR: + readTypedRecord(params, reader); + return true; + case ESM4::REC_SOPM: + break; + case ESM4::REC_SOUN: + readTypedRecord(params, reader); + return true; + case ESM4::REC_SPEL: + break; + case ESM4::REC_SPGD: + break; + case ESM4::REC_STAT: + readTypedRecord(params, reader); + return true; + case ESM4::REC_TACT: + readTypedRecord(params, reader); + return true; + case ESM4::REC_TERM: + readTypedRecord(params, reader); + return true; + case ESM4::REC_TES4: + readTypedRecord(params, reader); + return true; + case ESM4::REC_TREE: + readTypedRecord(params, reader); + return true; + case ESM4::REC_TXST: + readTypedRecord(params, reader); + return true; + case ESM4::REC_VTYP: + break; + case ESM4::REC_WATR: + break; + case ESM4::REC_WEAP: + readTypedRecord(params, reader); + return true; + case ESM4::REC_WOOP: + break; + case ESM4::REC_WRLD: + readTypedRecord(params, reader); + return true; + case ESM4::REC_WTHR: + break; + } + + if (!params.mQuite) + std::cout << "\n Unsupported record: " << ESM::NAME(reader.hdr().record.typeId).toStringView() << '\n'; + return false; + } + + } + + int loadTes4(const Arguments& info, std::unique_ptr&& stream) + { + std::cout << "Loading TES4 file: " << info.filename << '\n'; + + try + { + const ToUTF8::StatelessUtf8Encoder encoder(ToUTF8::calculateEncoding(info.encoding)); + ESM4::Reader reader(std::move(stream), info.filename, nullptr, &encoder, true); + const Params params(info); + + if (!params.mQuite) + { + std::cout << "Author: " << reader.getAuthor() << '\n' + << "Description: " << reader.getDesc() << '\n' + << "File format version: " << reader.esmVersionF() << '\n'; + + if (const std::vector& masterData = reader.getGameFiles(); !masterData.empty()) + { + std::cout << "Masters:" << '\n'; + for (const auto& master : masterData) + std::cout << " " << master.name << ", " << master.size << " bytes\n"; + } + } + + auto visitorRec = [¶ms](ESM4::Reader& reader) { return readRecord(params, reader); }; + auto visitorGroup = [¶ms](ESM4::Reader& reader) { + if (params.mQuite) + return; + auto groupType = static_cast(reader.hdr().group.type); + std::cout << "\nGroup: " << toString(groupType) << " " + << ESM::NAME(reader.hdr().group.typeId).toStringView() << '\n'; + }; + ESM4::ReaderUtils::readAll(reader, visitorRec, visitorGroup); + } + catch (const std::exception& e) + { + std::cout << "\nERROR:\n\n " << e.what() << std::endl; + return -1; + } + + return 0; + } +} diff --git a/apps/esmtool/tes4.hpp b/apps/esmtool/tes4.hpp new file mode 100644 index 00000000000..8149b260492 --- /dev/null +++ b/apps/esmtool/tes4.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_ESMTOOL_TES4_H +#define OPENMW_ESMTOOL_TES4_H + +#include +#include +#include + +namespace EsmTool +{ + struct Arguments; + + int loadTes4(const Arguments& info, std::unique_ptr&& stream); +} + +#endif diff --git a/apps/essimporter/CMakeLists.txt b/apps/essimporter/CMakeLists.txt index 0e742ff5487..217d3e7b50f 100644 --- a/apps/essimporter/CMakeLists.txt +++ b/apps/essimporter/CMakeLists.txt @@ -5,7 +5,6 @@ set(ESSIMPORTER_FILES importnpcc.cpp importcrec.cpp importcellref.cpp - importacdt.cpp importinventory.cpp importklst.cpp importcntc.cpp @@ -35,16 +34,25 @@ openmw_add_executable(openmw-essimporter ) target_link_libraries(openmw-essimporter - ${Boost_PROGRAM_OPTIONS_LIBRARY} - ${Boost_FILESYSTEM_LIBRARY} + Boost::program_options components ) if (BUILD_WITH_CODE_COVERAGE) - add_definitions (--coverage) - target_link_libraries(openmw-essimporter gcov) + target_compile_options(openmw-essimporter PRIVATE --coverage) + target_link_libraries(openmw-essimporter gcov) endif() if (WIN32) - INSTALL(TARGETS openmw-essimporter RUNTIME DESTINATION ".") + INSTALL(TARGETS openmw-essimporter RUNTIME DESTINATION ".") endif(WIN32) + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw-essimporter PRIVATE + + + + + + ) +endif() diff --git a/apps/essimporter/convertacdt.cpp b/apps/essimporter/convertacdt.cpp index 5c2bcc40247..a737e0a3a2f 100644 --- a/apps/essimporter/convertacdt.cpp +++ b/apps/essimporter/convertacdt.cpp @@ -1,8 +1,8 @@ -#include #include #include +#include -#include +#include #include "convertacdt.hpp" @@ -18,36 +18,36 @@ namespace ESSImport return mwIndex; } - void convertACDT (const ACDT& acdt, ESM::CreatureStats& cStats) + void convertACDT(const ACDT& acdt, ESM::CreatureStats& cStats) { - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) { int writeIndex = translateDynamicIndex(i); cStats.mDynamic[writeIndex].mBase = acdt.mDynamic[i][1]; - cStats.mDynamic[writeIndex].mMod = acdt.mDynamic[i][1]; + cStats.mDynamic[writeIndex].mMod = 0.f; cStats.mDynamic[writeIndex].mCurrent = acdt.mDynamic[i][0]; } - for (int i=0; i<8; ++i) + for (int i = 0; i < 8; ++i) { - cStats.mAttributes[i].mBase = static_cast(acdt.mAttributes[i][1]); - cStats.mAttributes[i].mMod = static_cast(acdt.mAttributes[i][0]); - cStats.mAttributes[i].mCurrent = static_cast(acdt.mAttributes[i][0]); + cStats.mAttributes[i].mBase = acdt.mAttributes[i][1]; + cStats.mAttributes[i].mMod = 0.f; + cStats.mAttributes[i].mCurrent = acdt.mAttributes[i][0]; } cStats.mGoldPool = acdt.mGoldPool; cStats.mTalkedTo = (acdt.mFlags & TalkedToPlayer) != 0; cStats.mAttacked = (acdt.mFlags & Attacked) != 0; } - void convertACSC (const ACSC& acsc, ESM::CreatureStats& cStats) + void convertACSC(const ACSC& acsc, ESM::CreatureStats& cStats) { cStats.mDead = (acsc.mFlags & Dead) != 0; } - void convertNpcData (const ActorData& actorData, ESM::NpcStats& npcStats) + void convertNpcData(const ActorData& actorData, ESM::NpcStats& npcStats) { - for (int i=0; i -#include -#include -#include +#include +#include +#include +#include #include "importacdt.hpp" @@ -14,13 +14,12 @@ namespace ESSImport // OpenMW uses Health,Magicka,Fatigue, MW uses Health,Fatigue,Magicka int translateDynamicIndex(int mwIndex); + void convertACDT(const ACDT& acdt, ESM::CreatureStats& cStats); + void convertACSC(const ACSC& acsc, ESM::CreatureStats& cStats); - void convertACDT (const ACDT& acdt, ESM::CreatureStats& cStats); - void convertACSC (const ACSC& acsc, ESM::CreatureStats& cStats); + void convertNpcData(const ActorData& actorData, ESM::NpcStats& npcStats); - void convertNpcData (const ActorData& actorData, ESM::NpcStats& npcStats); - - void convertANIS (const ANIS& anis, ESM::AnimationState& state); + void convertANIS(const ANIS& anis, ESM::AnimationState& state); } #endif diff --git a/apps/essimporter/convertcntc.cpp b/apps/essimporter/convertcntc.cpp index 426ef449669..2b461ae3ad0 100644 --- a/apps/essimporter/convertcntc.cpp +++ b/apps/essimporter/convertcntc.cpp @@ -5,7 +5,7 @@ namespace ESSImport { - void convertCNTC(const CNTC &cntc, ESM::ContainerState &state) + void convertCNTC(const CNTC& cntc, ESM::ContainerState& state) { convertInventory(cntc.mInventory, state.mInventory); } diff --git a/apps/essimporter/convertcntc.hpp b/apps/essimporter/convertcntc.hpp index c299d87a1eb..2dc51949b11 100644 --- a/apps/essimporter/convertcntc.hpp +++ b/apps/essimporter/convertcntc.hpp @@ -3,7 +3,7 @@ #include "importcntc.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/convertcrec.cpp b/apps/essimporter/convertcrec.cpp index 34e1c00708d..f8233bcbf5d 100644 --- a/apps/essimporter/convertcrec.cpp +++ b/apps/essimporter/convertcrec.cpp @@ -5,7 +5,7 @@ namespace ESSImport { - void convertCREC(const CREC &crec, ESM::CreatureState &state) + void convertCREC(const CREC& crec, ESM::CreatureState& state) { convertInventory(crec.mInventory, state.mInventory); } diff --git a/apps/essimporter/convertcrec.hpp b/apps/essimporter/convertcrec.hpp index 7d317f03e82..fa2e7e807f9 100644 --- a/apps/essimporter/convertcrec.hpp +++ b/apps/essimporter/convertcrec.hpp @@ -3,7 +3,7 @@ #include "importcrec.hpp" -#include +#include namespace ESSImport { diff --git a/apps/essimporter/converter.cpp b/apps/essimporter/converter.cpp index e0756602ddd..ebb0c9d281e 100644 --- a/apps/essimporter/converter.cpp +++ b/apps/essimporter/converter.cpp @@ -1,17 +1,18 @@ #include "converter.hpp" -#include #include +#include +#include #include -#include -#include +#include +#include #include -#include "convertcrec.hpp" #include "convertcntc.hpp" +#include "convertcrec.hpp" #include "convertscri.hpp" namespace @@ -19,7 +20,7 @@ namespace void convertImage(char* data, int size, int width, int height, GLenum pf, const std::string& out) { - osg::ref_ptr image (new osg::Image); + osg::ref_ptr image(new osg::Image); image->allocateImage(width, height, 1, pf, GL_UNSIGNED_BYTE); memcpy(image->data(), data, size); image->flipVertical(); @@ -27,19 +28,18 @@ namespace osgDB::writeImageFile(*image, out); } - void convertCellRef(const ESSImport::CellRef& cellref, ESM::ObjectState& objstate) { objstate.mEnabled = cellref.mEnabled; objstate.mPosition = cellref.mPos; objstate.mRef.mRefNum = cellref.mRefNum; if (cellref.mDeleted) - objstate.mCount = 0; - convertSCRI(cellref.mSCRI, objstate.mLocals); + objstate.mRef.mCount = 0; + convertSCRI(cellref.mActorData.mSCRI, objstate.mLocals); objstate.mHasLocals = !objstate.mLocals.mVariables.empty(); - if (cellref.mHasANIS) - convertANIS(cellref.mANIS, objstate.mAnimationState); + if (cellref.mActorData.mHasANIS) + convertANIS(cellref.mActorData.mANIS, objstate.mAnimationState); } bool isIndexedRefId(const std::string& indexedRefId) @@ -51,28 +51,28 @@ namespace return false; // entirely numeric refid, this is a reference to // a dynamically created record e.g. player-enchanted weapon - std::string index = indexedRefId.substr(indexedRefId.size()-8); + std::string index = indexedRefId.substr(indexedRefId.size() - 8); return index.find_first_not_of("0123456789ABCDEF") == std::string::npos; } void splitIndexedRefId(const std::string& indexedRefId, int& refIndex, std::string& refId) { std::stringstream stream; - stream << std::hex << indexedRefId.substr(indexedRefId.size()-8,8); + stream << std::hex << indexedRefId.substr(indexedRefId.size() - 8, 8); stream >> refIndex; - refId = indexedRefId.substr(0,indexedRefId.size()-8); + refId = indexedRefId.substr(0, indexedRefId.size() - 8); } int convertActorId(const std::string& indexedRefId, ESSImport::Context& context) { if (isIndexedRefId(indexedRefId)) { - int refIndex; + int refIndex = 0; std::string refId; splitIndexedRefId(indexedRefId, refIndex, refId); - auto it = context.mActorIdMap.find(std::make_pair(refIndex, refId)); + auto it = context.mActorIdMap.find(std::make_pair(refIndex, ESM::RefId::stringRefId(refId))); if (it == context.mActorIdMap.end()) return -1; return it->second; @@ -89,78 +89,77 @@ namespace namespace ESSImport { - struct MAPH { - unsigned int size; - unsigned int value; + uint32_t size; + uint32_t value; }; - void ConvertFMAP::read(ESM::ESMReader &esm) + void ConvertFMAP::read(ESM::ESMReader& esm) { MAPH maph; - esm.getHNT(maph, "MAPH"); + esm.getHNT("MAPH", maph.size, maph.value); std::vector data; esm.getSubNameIs("MAPD"); esm.getSubHeader(); data.resize(esm.getSubSize()); - esm.getExact(&data[0], data.size()); + esm.getExact(data.data(), data.size()); mGlobalMapImage = new osg::Image; mGlobalMapImage->allocateImage(maph.size, maph.size, 1, GL_RGB, GL_UNSIGNED_BYTE); - memcpy(mGlobalMapImage->data(), &data[0], data.size()); + memcpy(mGlobalMapImage->data(), data.data(), data.size()); // to match openmw size // FIXME: filtering? - mGlobalMapImage->scaleImage(maph.size*2, maph.size*2, 1, GL_UNSIGNED_BYTE); + mGlobalMapImage->scaleImage(maph.size * 2, maph.size * 2, 1, GL_UNSIGNED_BYTE); } - void ConvertFMAP::write(ESM::ESMWriter &esm) + void ConvertFMAP::write(ESM::ESMWriter& esm) { int numcells = mGlobalMapImage->s() / 18; // NB truncating, doesn't divide perfectly - // with the 512x512 map the game has by default - int cellSize = mGlobalMapImage->s()/numcells; + // with the 512x512 map the game has by default + int cellSize = mGlobalMapImage->s() / numcells; // Note the upper left corner of the (0,0) cell should be at (width/2, height/2) - mContext->mGlobalMapState.mBounds.mMinX = -numcells/2; - mContext->mGlobalMapState.mBounds.mMaxX = (numcells-1)/2; - mContext->mGlobalMapState.mBounds.mMinY = -(numcells-1)/2; - mContext->mGlobalMapState.mBounds.mMaxY = numcells/2; + mContext->mGlobalMapState.mBounds.mMinX = -numcells / 2; + mContext->mGlobalMapState.mBounds.mMaxX = (numcells - 1) / 2; + mContext->mGlobalMapState.mBounds.mMinY = -(numcells - 1) / 2; + mContext->mGlobalMapState.mBounds.mMaxY = numcells / 2; - osg::ref_ptr image2 (new osg::Image); - int width = cellSize*numcells; - int height = cellSize*numcells; + osg::ref_ptr image2(new osg::Image); + int width = cellSize * numcells; + int height = cellSize * numcells; std::vector data; - data.resize(width*height*4, 0); + data.resize(width * height * 4, 0); image2->allocateImage(width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE); - memcpy(image2->data(), &data[0], data.size()); + memcpy(image2->data(), data.data(), data.size()); - for (const auto & exploredCell : mContext->mExploredCells) + for (const auto& exploredCell : mContext->mExploredCells) { if (exploredCell.first > mContext->mGlobalMapState.mBounds.mMaxX - || exploredCell.first < mContext->mGlobalMapState.mBounds.mMinX - || exploredCell.second > mContext->mGlobalMapState.mBounds.mMaxY - || exploredCell.second < mContext->mGlobalMapState.mBounds.mMinY) + || exploredCell.first < mContext->mGlobalMapState.mBounds.mMinX + || exploredCell.second > mContext->mGlobalMapState.mBounds.mMaxY + || exploredCell.second < mContext->mGlobalMapState.mBounds.mMinY) { // out of bounds, I think this could happen, since the original engine had a fixed-size map continue; } - int imageLeftSrc = mGlobalMapImage->s()/2; - int imageTopSrc = mGlobalMapImage->t()/2; + int imageLeftSrc = mGlobalMapImage->s() / 2; + int imageTopSrc = mGlobalMapImage->t() / 2; imageLeftSrc += exploredCell.first * cellSize; imageTopSrc -= exploredCell.second * cellSize; - int imageLeftDst = width/2; - int imageTopDst = height/2; + int imageLeftDst = width / 2; + int imageTopDst = height / 2; imageLeftDst += exploredCell.first * cellSize; imageTopDst -= exploredCell.second * cellSize; - for (int x=0; xdata(imageLeftSrc+x, imageTopSrc+y, 0); - *(unsigned int*)image2->data(imageLeftDst+x, imageTopDst+y, 0) = col; + unsigned int col = *(unsigned int*)mGlobalMapImage->data(imageLeftSrc + x, imageTopSrc + y, 0); + *(unsigned int*)image2->data(imageLeftDst + x, imageTopDst + y, 0) = col; } } @@ -177,7 +176,8 @@ namespace ESSImport osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*image2, ostream); if (!result.success()) { - std::cerr << "Error: can't write global map image: " << result.message() << " code " << result.status() << std::endl; + std::cerr << "Error: can't write global map image: " << result.message() << " code " << result.status() + << std::endl; return; } @@ -189,7 +189,7 @@ namespace ESSImport esm.endRecord(ESM::REC_GMAP); } - void ConvertCell::read(ESM::ESMReader &esm) + void ConvertCell::read(ESM::ESMReader& esm) { ESM::Cell cell; bool isDeleted = false; @@ -203,9 +203,9 @@ namespace ESSImport } // note if the player is in a nameless exterior cell, we will assign the cellId later based on player position - if (cell.mName == mContext->mPlayerCellName) + if (Misc::StringUtils::ciEqual(cell.mName, mContext->mPlayerCellName)) { - mContext->mPlayer.mCellId = cell.getCellId(); + mContext->mPlayer.mCellId = cell.mId; } Cell newcell; @@ -232,17 +232,17 @@ namespace ESSImport esm.skip(4); } - esm.getExact(nam8, 32); + esm.getT(nam8); - newcell.mFogOfWar.reserve(16*16); - for (int x=0; x<16; ++x) + newcell.mFogOfWar.reserve(16 * 16); + for (int x = 0; x < 16; ++x) { - for (int y=0; y<16; ++y) + for (int y = 0; y < 16; ++y) { - size_t pos = x*16+y; - size_t bytepos = pos/8; - assert(bytepos<32); - int bit = pos%8; + size_t pos = x * 16 + y; + size_t bytepos = pos / 8; + assert(bytepos < 32); + int bit = pos % 8; newcell.mFogOfWar.push_back(((nam8[bytepos] >> bit) & (0x1)) ? 0xffffffff : 0x000000ff); } } @@ -252,7 +252,8 @@ namespace ESSImport std::ostringstream filename; filename << "fog_" << cell.mData.mX << "_" << cell.mData.mY << ".tga"; - convertImage((char*)&newcell.mFogOfWar[0], newcell.mFogOfWar.size()*4, 16, 16, GL_RGBA, filename.str()); + convertImage( + (char*)&newcell.mFogOfWar[0], newcell.mFogOfWar.size() * 4, 16, 16, GL_RGBA, filename.str()); } } @@ -268,17 +269,17 @@ namespace ESSImport } std::vector cellrefs; - while (esm.hasMoreSubs() && esm.isNextSub("FRMR")) + while (esm.hasMoreSubs() && esm.peekNextSub("FRMR")) { CellRef ref; - ref.load (esm); + ref.load(esm); cellrefs.push_back(ref); } while (esm.isNextSub("MPCD")) { float notepos[3]; - esm.getHT(notepos, 3*sizeof(float)); + esm.getHT(notepos); // Markers seem to be arranged in a 32*32 grid, notepos has grid-indices. // This seems to be the reason markers can't be placed everywhere in interior cells, @@ -300,36 +301,38 @@ namespace ESSImport ESM::CustomMarker marker; marker.mWorldX = notepos[0]; marker.mWorldY = notepos[1]; - marker.mNote = note; - marker.mCell = cell.getCellId(); + marker.mNote = std::move(note); + marker.mCell = cell.mId; mMarkers.push_back(marker); } - newcell.mRefs = cellrefs; - + newcell.mRefs = std::move(cellrefs); if (cell.isExterior()) - mExtCells[std::make_pair(cell.mData.mX, cell.mData.mY)] = newcell; + mExtCells[std::make_pair(cell.mData.mX, cell.mData.mY)] = std::move(newcell); else - mIntCells[cell.mName] = newcell; + mIntCells[cell.mName] = std::move(newcell); } - void ConvertCell::writeCell(const Cell &cell, ESM::ESMWriter& esm) + void ConvertCell::writeCell(const Cell& cell, ESM::ESMWriter& esm) { ESM::Cell esmcell = cell.mCell; esm.startRecord(ESM::REC_CSTA); ESM::CellState csta; csta.mHasFogOfWar = 0; - csta.mId = esmcell.getCellId(); - csta.mId.save(esm); + csta.mLastRespawn.mDay = 0; + csta.mLastRespawn.mHour = 0; + csta.mId = esmcell.mId; + csta.mIsInterior = !esmcell.isExterior(); + esm.writeCellId(csta.mId); // TODO csta.mLastRespawn; // shouldn't be needed if we respawn on global schedule like in original MW csta.mWaterLevel = esmcell.mWater; csta.save(esm); - for (const auto & cellref : cell.mRefs) + for (const auto& cellref : cell.mRefs) { - ESM::CellRef out (cellref); + ESM::CellRef out(cellref); // TODO: use mContext->mCreatures/mNpcs @@ -337,88 +340,90 @@ namespace ESSImport { // non-indexed RefNum, i.e. no CREC/NPCC/CNTC record associated with it // this could be any type of object really (even creatures/npcs too) - out.mRefID = cellref.mIndexedRefId; - std::string idLower = Misc::StringUtils::lowerCase(out.mRefID); + out.mRefID = ESM::RefId::stringRefId(cellref.mIndexedRefId); ESM::ObjectState objstate; objstate.blank(); objstate.mRef = out; - objstate.mRef.mRefID = idLower; + objstate.mRef.mRefID = out.mRefID; objstate.mHasCustomState = false; convertCellRef(cellref, objstate); - esm.writeHNT ("OBJE", 0); + esm.writeHNT("OBJE", 0); objstate.save(esm); continue; } else { - int refIndex; - splitIndexedRefId(cellref.mIndexedRefId, refIndex, out.mRefID); - - std::string idLower = Misc::StringUtils::lowerCase(out.mRefID); + int refIndex = 0; + std::string outStringId; + splitIndexedRefId(cellref.mIndexedRefId, refIndex, outStringId); + out.mRefID = ESM::RefId::stringRefId(outStringId); - std::map, NPCC>::const_iterator npccIt = mContext->mNpcChanges.find( - std::make_pair(refIndex, out.mRefID)); + auto npccIt = mContext->mNpcChanges.find(std::make_pair(refIndex, out.mRefID)); if (npccIt != mContext->mNpcChanges.end()) { ESM::NpcState objstate; objstate.blank(); objstate.mRef = out; - objstate.mRef.mRefID = idLower; + objstate.mRef.mRefID = out.mRefID; // TODO: need more micromanagement here so we don't overwrite values // from the ESM with default values - if (cellref.mHasACDT) - convertACDT(cellref.mACDT, objstate.mCreatureStats); - if (cellref.mHasACSC) - convertACSC(cellref.mACSC, objstate.mCreatureStats); - convertNpcData(cellref, objstate.mNpcStats); + if (cellref.mActorData.mHasACDT) + convertACDT(cellref.mActorData.mACDT, objstate.mCreatureStats); + else + objstate.mCreatureStats.mMissingACDT = true; + if (cellref.mActorData.mHasACSC) + convertACSC(cellref.mActorData.mACSC, objstate.mCreatureStats); + convertNpcData(cellref.mActorData, objstate.mNpcStats); convertNPCC(npccIt->second, objstate); convertCellRef(cellref, objstate); objstate.mCreatureStats.mActorId = mContext->generateActorId(); - mContext->mActorIdMap.insert(std::make_pair(std::make_pair(refIndex, out.mRefID), objstate.mCreatureStats.mActorId)); + mContext->mActorIdMap.insert( + std::make_pair(std::make_pair(refIndex, out.mRefID), objstate.mCreatureStats.mActorId)); - esm.writeHNT ("OBJE", ESM::REC_NPC_); + esm.writeHNT("OBJE", ESM::REC_NPC_); objstate.save(esm); continue; } - std::map, CNTC>::const_iterator cntcIt = mContext->mContainerChanges.find( - std::make_pair(refIndex, out.mRefID)); + auto cntcIt = mContext->mContainerChanges.find(std::make_pair(refIndex, out.mRefID)); if (cntcIt != mContext->mContainerChanges.end()) { ESM::ContainerState objstate; objstate.blank(); objstate.mRef = out; - objstate.mRef.mRefID = idLower; + objstate.mRef.mRefID = out.mRefID; convertCNTC(cntcIt->second, objstate); convertCellRef(cellref, objstate); - esm.writeHNT ("OBJE", ESM::REC_CONT); + esm.writeHNT("OBJE", ESM::REC_CONT); objstate.save(esm); continue; } - std::map, CREC>::const_iterator crecIt = mContext->mCreatureChanges.find( - std::make_pair(refIndex, out.mRefID)); + auto crecIt = mContext->mCreatureChanges.find(std::make_pair(refIndex, out.mRefID)); if (crecIt != mContext->mCreatureChanges.end()) { ESM::CreatureState objstate; objstate.blank(); objstate.mRef = out; - objstate.mRef.mRefID = idLower; + objstate.mRef.mRefID = out.mRefID; // TODO: need more micromanagement here so we don't overwrite values // from the ESM with default values - if (cellref.mHasACDT) - convertACDT(cellref.mACDT, objstate.mCreatureStats); - if (cellref.mHasACSC) - convertACSC(cellref.mACSC, objstate.mCreatureStats); + if (cellref.mActorData.mHasACDT) + convertACDT(cellref.mActorData.mACDT, objstate.mCreatureStats); + else + objstate.mCreatureStats.mMissingACDT = true; + if (cellref.mActorData.mHasACSC) + convertACSC(cellref.mActorData.mACSC, objstate.mCreatureStats); convertCREC(crecIt->second, objstate); convertCellRef(cellref, objstate); objstate.mCreatureStats.mActorId = mContext->generateActorId(); - mContext->mActorIdMap.insert(std::make_pair(std::make_pair(refIndex, out.mRefID), objstate.mCreatureStats.mActorId)); + mContext->mActorIdMap.insert( + std::make_pair(std::make_pair(refIndex, out.mRefID), objstate.mCreatureStats.mActorId)); - esm.writeHNT ("OBJE", ESM::REC_CREA); + esm.writeHNT("OBJE", ESM::REC_CREA); objstate.save(esm); continue; } @@ -432,15 +437,15 @@ namespace ESSImport esm.endRecord(ESM::REC_CSTA); } - void ConvertCell::write(ESM::ESMWriter &esm) + void ConvertCell::write(ESM::ESMWriter& esm) { - for (const auto & cell : mIntCells) + for (const auto& cell : mIntCells) writeCell(cell.second, esm); - for (const auto & cell : mExtCells) + for (const auto& cell : mExtCells) writeCell(cell.second, esm); - for (const auto & marker : mMarkers) + for (const auto& marker : mMarkers) { esm.startRecord(ESM::REC_MARK); marker.save(esm); @@ -462,7 +467,7 @@ namespace ESSImport ESM::ProjectileState out; convertBaseState(out, pnam); - out.mBowId = pnam.mBowId.toString(); + out.mBowId = ESM::RefId::stringRefId(pnam.mBowId.toString()); out.mVelocity = pnam.mVelocity; out.mAttackStrength = pnam.mAttackStrength; @@ -476,16 +481,18 @@ namespace ESSImport convertBaseState(out, pnam); auto it = std::find_if(mContext->mActiveSpells.begin(), mContext->mActiveSpells.end(), - [&pnam](const SPLM::ActiveSpell& spell) -> bool { return spell.mIndex == pnam.mSplmIndex; }); + [&pnam](const SPLM::ActiveSpell& spell) -> bool { return spell.mIndex == pnam.mSplmIndex; }); if (it == mContext->mActiveSpells.end()) { - std::cerr << "Warning: Skipped conversion for magic projectile \"" << pnam.mArrowId.toString() << "\" (invalid spell link)" << std::endl; + std::cerr << "Warning: Skipped conversion for magic projectile \"" << pnam.mArrowId.toString() + << "\" (invalid spell link)" << std::endl; continue; } - out.mSpellId = it->mSPDT.mId.toString(); + out.mSpellId = ESM::RefId::stringRefId(it->mSPDT.mId.toString()); out.mSpeed = pnam.mSpeed * 0.001f; // not sure where this factor comes from + out.mItem = ESM::RefNum(); esm.startRecord(ESM::REC_MPRJ); out.save(esm); @@ -496,11 +503,11 @@ namespace ESSImport void ConvertPROJ::convertBaseState(ESM::BaseProjectileState& base, const PROJ::PNAM& pnam) { - base.mId = pnam.mArrowId.toString(); + base.mId = ESM::RefId::stringRefId(pnam.mArrowId.toString()); base.mPosition = pnam.mPosition; osg::Quat orient; - orient.makeRotate(osg::Vec3f(0,1,0), pnam.mVelocity); + orient.makeRotate(osg::Vec3f(0, 1, 0), pnam.mVelocity); base.mOrientation = orient; base.mActorId = convertActorId(pnam.mActorId.toString(), *mContext); diff --git a/apps/essimporter/converter.hpp b/apps/essimporter/converter.hpp index 9a1923c2b63..93b4e2c810e 100644 --- a/apps/essimporter/converter.hpp +++ b/apps/essimporter/converter.hpp @@ -6,619 +6,622 @@ #include #include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include -#include "importcrec.hpp" #include "importcntc.hpp" +#include "importcrec.hpp" -#include "importercontext.hpp" #include "importcellref.hpp" -#include "importklst.hpp" +#include "importdial.hpp" +#include "importercontext.hpp" #include "importgame.hpp" #include "importinfo.hpp" -#include "importdial.hpp" -#include "importques.hpp" #include "importjour.hpp" -#include "importscpt.hpp" +#include "importklst.hpp" #include "importproj.h" +#include "importques.hpp" +#include "importscpt.hpp" #include "importsplm.h" #include "convertacdt.hpp" #include "convertnpcc.hpp" -#include "convertscpt.hpp" #include "convertplayer.hpp" +#include "convertscpt.hpp" +#include namespace ESSImport { -class Converter -{ -public: - /// @return the order for writing this converter's records to the output file, in relation to other converters - virtual int getStage() { return 1; } - - virtual ~Converter() {} - - void setContext(Context& context) { mContext = &context; } - - /// @note The load method of ESM records accept the deleted flag as a parameter. - /// I don't know can the DELE sub-record appear in saved games, so the deleted flag will be ignored. - virtual void read(ESM::ESMReader& esm) + class Converter { - } + public: + /// @return the order for writing this converter's records to the output file, in relation to other converters + virtual int getStage() { return 1; } - /// Called after the input file has been read in completely, which may be necessary - /// if the conversion process relies on information in other records - virtual void write(ESM::ESMWriter& esm) - { + virtual ~Converter() = default; - } + void setContext(Context& context) { mContext = &context; } -protected: - Context* mContext; -}; + /// @note The load method of ESM records accept the deleted flag as a parameter. + /// I don't know can the DELE sub-record appear in saved games, so the deleted flag will be ignored. + virtual void read(ESM::ESMReader& esm) {} -/// Default converter: simply reads the record and writes it unmodified to the output -template -class DefaultConverter : public Converter -{ -public: - int getStage() override { return 0; } + /// Called after the input file has been read in completely, which may be necessary + /// if the conversion process relies on information in other records + virtual void write(ESM::ESMWriter& esm) {} - void read(ESM::ESMReader& esm) override + protected: + Context* mContext; + }; + + /// Default converter: simply reads the record and writes it unmodified to the output + template + class DefaultConverter : public Converter { - T record; - bool isDeleted = false; + public: + int getStage() override { return 0; } - record.load(esm, isDeleted); - mRecords[record.mId] = record; - } + void read(ESM::ESMReader& esm) override + { + T record; + bool isDeleted = false; - void write(ESM::ESMWriter& esm) override - { - for (typename std::map::const_iterator it = mRecords.begin(); it != mRecords.end(); ++it) + record.load(esm, isDeleted); + mRecords[record.mId] = record; + } + + void write(ESM::ESMWriter& esm) override { - esm.startRecord(T::sRecordId); - it->second.save(esm); - esm.endRecord(T::sRecordId); + for (auto it = mRecords.begin(); it != mRecords.end(); ++it) + { + esm.startRecord(T::sRecordId); + it->second.save(esm); + esm.endRecord(T::sRecordId); + } } - } -protected: - std::map mRecords; -}; + protected: + std::map mRecords; + }; -class ConvertNPC : public Converter -{ -public: - void read(ESM::ESMReader &esm) override + class ConvertNPC : public Converter { - ESM::NPC npc; - bool isDeleted = false; - - npc.load(esm, isDeleted); - if (npc.mId != "player") + public: + void read(ESM::ESMReader& esm) override { - // Handles changes to the NPC struct, but since there is no index here - // it will apply to ALL instances of the class. seems to be the reason for the - // "feature" in MW where changing AI settings of one guard will change it for all guards of that refID. - mContext->mNpcs[Misc::StringUtils::lowerCase(npc.mId)] = npc; - } - else - { - mContext->mPlayer.mObject.mCreatureStats.mLevel = npc.mNpdt.mLevel; - mContext->mPlayerBase = npc; - ESM::SpellState::SpellParams empty; - // FIXME: player start spells and birthsign spells aren't listed here, - // need to fix openmw to account for this - for (const auto & spell : npc.mSpells.mList) - mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells[spell] = empty; - - // Clear the list now that we've written it, this prevents issues cropping up with - // ensureCustomData() in OpenMW tripping over no longer existing spells, where an error would be fatal. - mContext->mPlayerBase.mSpells.mList.clear(); - - // Same with inventory. Actually it's strange this would contain something, since there's already an - // inventory list in NPCC. There seems to be a fair amount of redundancy in this format. - mContext->mPlayerBase.mInventory.mList.clear(); + ESM::NPC npc; + bool isDeleted = false; + + npc.load(esm, isDeleted); + if (npc.mId != "player") + { + // Handles changes to the NPC struct, but since there is no index here + // it will apply to ALL instances of the class. seems to be the reason for the + // "feature" in MW where changing AI settings of one guard will change it for all guards of that refID. + mContext->mNpcs[npc.mId] = npc; + } + else + { + mContext->mPlayer.mObject.mCreatureStats.mLevel = npc.mNpdt.mLevel; + mContext->mPlayerBase = npc; + // FIXME: player start spells and birthsign spells aren't listed here, + // need to fix openmw to account for this + mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells = npc.mSpells.mList; + + // Clear the list now that we've written it, this prevents issues cropping up with + // ensureCustomData() in OpenMW tripping over no longer existing spells, where an error would be fatal. + mContext->mPlayerBase.mSpells.mList.clear(); + + // Same with inventory. Actually it's strange this would contain something, since there's already an + // inventory list in NPCC. There seems to be a fair amount of redundancy in this format. + mContext->mPlayerBase.mInventory.mList.clear(); + } } - } -}; + }; -class ConvertCREA : public Converter -{ -public: - void read(ESM::ESMReader &esm) override + class ConvertCREA : public Converter { - // See comment in ConvertNPC - ESM::Creature creature; - bool isDeleted = false; + public: + void read(ESM::ESMReader& esm) override + { + // See comment in ConvertNPC + ESM::Creature creature; + bool isDeleted = false; - creature.load(esm, isDeleted); - mContext->mCreatures[Misc::StringUtils::lowerCase(creature.mId)] = creature; - } -}; + creature.load(esm, isDeleted); + mContext->mCreatures[creature.mId] = creature; + } + }; -// Do we need ConvertCONT? -// I've seen a CONT record in a certain save file, but the container contents in it -// were identical to a corresponding CNTC record. See previous comment about redundancy... + // Do we need ConvertCONT? + // I've seen a CONT record in a certain save file, but the container contents in it + // were identical to a corresponding CNTC record. See previous comment about redundancy... -class ConvertGlobal : public DefaultConverter -{ -public: - void read(ESM::ESMReader &esm) override + class ConvertGlobal : public DefaultConverter { - ESM::Global global; - bool isDeleted = false; - - global.load(esm, isDeleted); - if (Misc::StringUtils::ciEqual(global.mId, "gamehour")) - mContext->mHour = global.mValue.getFloat(); - if (Misc::StringUtils::ciEqual(global.mId, "day")) - mContext->mDay = global.mValue.getInteger(); - if (Misc::StringUtils::ciEqual(global.mId, "month")) - mContext->mMonth = global.mValue.getInteger(); - if (Misc::StringUtils::ciEqual(global.mId, "year")) - mContext->mYear = global.mValue.getInteger(); - mRecords[global.mId] = global; - } -}; - -class ConvertClass : public DefaultConverter -{ -public: - void read(ESM::ESMReader &esm) override + public: + void read(ESM::ESMReader& esm) override + { + ESM::Global global; + bool isDeleted = false; + + global.load(esm, isDeleted); + if (global.mId == "gamehour") + mContext->mHour = global.mValue.getFloat(); + if (global.mId == "day") + mContext->mDay = global.mValue.getInteger(); + if (global.mId == "month") + mContext->mMonth = global.mValue.getInteger(); + if (global.mId == "year") + mContext->mYear = global.mValue.getInteger(); + mRecords[global.mId] = global; + } + }; + + class ConvertClass : public DefaultConverter { - ESM::Class class_; - bool isDeleted = false; + public: + void read(ESM::ESMReader& esm) override + { + ESM::Class class_; + bool isDeleted = false; - class_.load(esm, isDeleted); - if (class_.mId == "NEWCLASSID_CHARGEN") - mContext->mCustomPlayerClassName = class_.mName; + class_.load(esm, isDeleted); + if (class_.mId == "NEWCLASSID_CHARGEN") + mContext->mCustomPlayerClassName = class_.mName; - mRecords[class_.mId] = class_; - } -}; + mRecords[class_.mId] = class_; + } + }; -class ConvertBook : public DefaultConverter -{ -public: - void read(ESM::ESMReader &esm) override + class ConvertBook : public DefaultConverter { - ESM::Book book; - bool isDeleted = false; + public: + void read(ESM::ESMReader& esm) override + { + ESM::Book book; + bool isDeleted = false; - book.load(esm, isDeleted); - if (book.mData.mSkillId == -1) - mContext->mPlayer.mObject.mNpcStats.mUsedIds.push_back(Misc::StringUtils::lowerCase(book.mId)); + book.load(esm, isDeleted); + if (book.mData.mSkillId == -1) + mContext->mPlayer.mObject.mNpcStats.mUsedIds.push_back(book.mId); - mRecords[book.mId] = book; - } -}; + mRecords[book.mId] = book; + } + }; -class ConvertNPCC : public Converter -{ -public: - void read(ESM::ESMReader &esm) override + class ConvertNPCC : public Converter { - std::string id = esm.getHNString("NAME"); - NPCC npcc; - npcc.load(esm); - if (id == "PlayerSaveGame") + public: + void read(ESM::ESMReader& esm) override { - convertNPCC(npcc, mContext->mPlayer.mObject); - } - else - { - int index = npcc.mNPDT.mIndex; - mContext->mNpcChanges.insert(std::make_pair(std::make_pair(index,id), npcc)); + auto id = esm.getHNRefId("NAME"); + NPCC npcc; + npcc.load(esm); + if (id == "PlayerSaveGame") + { + convertNPCC(npcc, mContext->mPlayer.mObject); + } + else + { + int index = npcc.mNPDT.mIndex; + mContext->mNpcChanges.insert(std::make_pair(std::make_pair(index, id), npcc)); + } } - } -}; + }; -class ConvertREFR : public Converter -{ -public: - void read(ESM::ESMReader &esm) override + class ConvertREFR : public Converter { - REFR refr; - refr.load(esm); - assert(refr.mRefID == "PlayerSaveGame"); - mContext->mPlayer.mObject.mPosition = refr.mPos; + public: + void read(ESM::ESMReader& esm) override + { + CellRef refr; + refr.load(esm); + assert(refr.mIndexedRefId == "PlayerSaveGame"); + mContext->mPlayer.mObject.mPosition = refr.mPos; - ESM::CreatureStats& cStats = mContext->mPlayer.mObject.mCreatureStats; - convertACDT(refr.mActorData.mACDT, cStats); + ESM::CreatureStats& cStats = mContext->mPlayer.mObject.mCreatureStats; + convertACDT(refr.mActorData.mACDT, cStats); - ESM::NpcStats& npcStats = mContext->mPlayer.mObject.mNpcStats; - convertNpcData(refr.mActorData, npcStats); + ESM::NpcStats& npcStats = mContext->mPlayer.mObject.mNpcStats; + convertNpcData(refr.mActorData, npcStats); - mSelectedSpell = refr.mActorData.mSelectedSpell; - if (!refr.mActorData.mSelectedEnchantItem.empty()) - { - ESM::InventoryState& invState = mContext->mPlayer.mObject.mInventory; - - for (unsigned int i=0; imPlayer.mObject.mInventory; + + for (size_t i = 0; i < 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 == ESM::RefId::stringRefId(refr.mActorData.mSelectedEnchantItem)) + invState.mSelectedEnchantItem = i; + } } } - } - void write(ESM::ESMWriter& esm) override - { - esm.startRecord(ESM::REC_ASPL); - esm.writeHNString("ID__", mSelectedSpell); - esm.endRecord(ESM::REC_ASPL); - } -private: - std::string mSelectedSpell; -}; - -class ConvertPCDT : public Converter -{ -public: - ConvertPCDT() - : mFirstPersonCam(true), - mTeleportingEnabled(true), - mLevitationEnabled(true) - {} - - void read(ESM::ESMReader &esm) override - { - PCDT pcdt; - pcdt.load(esm); + void write(ESM::ESMWriter& esm) override + { + esm.startRecord(ESM::REC_ASPL); + esm.writeHNString("ID__", mSelectedSpell); + esm.endRecord(ESM::REC_ASPL); + } - convertPCDT(pcdt, mContext->mPlayer, mContext->mDialogueState.mKnownTopics, mFirstPersonCam, mTeleportingEnabled, mLevitationEnabled, mContext->mControlsState); - } - void write(ESM::ESMWriter &esm) override - { - esm.startRecord(ESM::REC_ENAB); - esm.writeHNT("TELE", mTeleportingEnabled); - esm.writeHNT("LEVT", mLevitationEnabled); - esm.endRecord(ESM::REC_ENAB); - - esm.startRecord(ESM::REC_CAM_); - esm.writeHNT("FIRS", mFirstPersonCam); - esm.endRecord(ESM::REC_CAM_); - } -private: - bool mFirstPersonCam; - bool mTeleportingEnabled; - bool mLevitationEnabled; -}; - -class ConvertCNTC : public Converter -{ - void read(ESM::ESMReader &esm) override - { - std::string id = esm.getHNString("NAME"); - CNTC cntc; - cntc.load(esm); - mContext->mContainerChanges.insert(std::make_pair(std::make_pair(cntc.mIndex,id), cntc)); - } -}; - -class ConvertCREC : public Converter -{ -public: - void read(ESM::ESMReader &esm) override + private: + std::string mSelectedSpell; + }; + + class ConvertPCDT : public Converter { - std::string id = esm.getHNString("NAME"); - CREC crec; - crec.load(esm); - mContext->mCreatureChanges.insert(std::make_pair(std::make_pair(crec.mIndex,id), crec)); - } -}; - -class ConvertFMAP : public Converter -{ -public: - void read(ESM::ESMReader &esm) override; - void write(ESM::ESMWriter &esm) override; + public: + ConvertPCDT() + : mFirstPersonCam(true) + , mTeleportingEnabled(true) + , mLevitationEnabled(true) + { + } -private: - osg::ref_ptr mGlobalMapImage; -}; + void read(ESM::ESMReader& esm) override + { + PCDT pcdt; + pcdt.load(esm); -class ConvertCell : public Converter -{ -public: - void read(ESM::ESMReader& esm) override; - void write(ESM::ESMWriter& esm) override; + convertPCDT(pcdt, mContext->mPlayer, mContext->mDialogueState.mKnownTopics, mFirstPersonCam, + mTeleportingEnabled, mLevitationEnabled, mContext->mControlsState); + } + void write(ESM::ESMWriter& esm) override + { + esm.startRecord(ESM::REC_ENAB); + esm.writeHNT("TELE", mTeleportingEnabled); + esm.writeHNT("LEVT", mLevitationEnabled); + esm.endRecord(ESM::REC_ENAB); + + esm.startRecord(ESM::REC_CAM_); + esm.writeHNT("FIRS", mFirstPersonCam); + esm.endRecord(ESM::REC_CAM_); + } -private: - struct Cell - { - ESM::Cell mCell; - std::vector mRefs; - std::vector mFogOfWar; + private: + bool mFirstPersonCam; + bool mTeleportingEnabled; + bool mLevitationEnabled; }; - std::map mIntCells; - std::map, Cell> mExtCells; - - std::vector mMarkers; + class ConvertCNTC : public Converter + { + void read(ESM::ESMReader& esm) override + { + auto id = esm.getHNRefId("NAME"); + CNTC cntc; + cntc.load(esm); + mContext->mContainerChanges.insert(std::make_pair(std::make_pair(cntc.mIndex, id), cntc)); + } + }; - void writeCell(const Cell& cell, ESM::ESMWriter &esm); -}; + class ConvertCREC : public Converter + { + public: + void read(ESM::ESMReader& esm) override + { + auto id = esm.getHNRefId("NAME"); + CREC crec; + crec.load(esm); + mContext->mCreatureChanges.insert(std::make_pair(std::make_pair(crec.mIndex, id), crec)); + } + }; -class ConvertKLST : public Converter -{ -public: - void read(ESM::ESMReader& esm) override + class ConvertFMAP : public Converter { - KLST klst; - klst.load(esm); - mKillCounter = klst.mKillCounter; + public: + void read(ESM::ESMReader& esm) override; + void write(ESM::ESMWriter& esm) override; - mContext->mPlayer.mObject.mNpcStats.mWerewolfKills = klst.mWerewolfKills; - } + private: + osg::ref_ptr mGlobalMapImage; + }; - void write(ESM::ESMWriter &esm) override + class ConvertCell : public Converter { - esm.startRecord(ESM::REC_DCOU); - for (std::map::const_iterator it = mKillCounter.begin(); it != mKillCounter.end(); ++it) + public: + void read(ESM::ESMReader& esm) override; + void write(ESM::ESMWriter& esm) override; + + private: + struct Cell { - esm.writeHNString("ID__", it->first); - esm.writeHNT ("COUN", it->second); - } - esm.endRecord(ESM::REC_DCOU); - } + ESM::Cell mCell; + std::vector mRefs; + std::vector mFogOfWar; + }; -private: - std::map mKillCounter; -}; + std::map mIntCells; + std::map, Cell> mExtCells; -class ConvertFACT : public Converter -{ -public: - void read(ESM::ESMReader& esm) override + std::vector mMarkers; + + void writeCell(const Cell& cell, ESM::ESMWriter& esm); + }; + + class ConvertKLST : public Converter { - ESM::Faction faction; - bool isDeleted = false; + public: + void read(ESM::ESMReader& esm) override + { + KLST klst; + klst.load(esm); + mKillCounter = klst.mKillCounter; - faction.load(esm, isDeleted); - std::string id = Misc::StringUtils::lowerCase(faction.mId); + mContext->mPlayer.mObject.mNpcStats.mWerewolfKills = klst.mWerewolfKills; + } - for (std::map::const_iterator it = faction.mReactions.begin(); it != faction.mReactions.end(); ++it) + void write(ESM::ESMWriter& esm) override { - std::string faction2 = Misc::StringUtils::lowerCase(it->first); - mContext->mDialogueState.mChangedFactionReaction[id].insert(std::make_pair(faction2, it->second)); + esm.startRecord(ESM::REC_DCOU); + for (auto it = mKillCounter.begin(); it != mKillCounter.end(); ++it) + { + esm.writeHNString("ID__", it->first); + esm.writeHNT("COUN", it->second); + } + esm.endRecord(ESM::REC_DCOU); } - } -}; -/// Stolen items -class ConvertSTLN : public Converter -{ -public: - void read(ESM::ESMReader &esm) override - { - std::string itemid = esm.getHNString("NAME"); - Misc::StringUtils::lowerCaseInPlace(itemid); + private: + std::map mKillCounter; + }; - while (esm.isNextSub("FNAM") || esm.isNextSub("ONAM")) + class ConvertFACT : public Converter + { + public: + void read(ESM::ESMReader& esm) override { - if (esm.retSubName().toString() == "FNAM") + ESM::Faction faction; + bool isDeleted = false; + + faction.load(esm, isDeleted); + const auto& id = faction.mId; + + for (auto it = faction.mReactions.begin(); it != faction.mReactions.end(); ++it) { - std::string factionid = esm.getHString(); - mStolenItems[itemid].insert(std::make_pair(Misc::StringUtils::lowerCase(factionid), true)); + const auto& faction2 = it->first; + mContext->mDialogueState.mChangedFactionReaction[id].insert(std::make_pair(faction2, it->second)); } - else + } + }; + + /// Stolen items + class ConvertSTLN : public Converter + { + public: + void read(ESM::ESMReader& esm) override + { + std::string itemid = esm.getHNString("NAME"); + Misc::StringUtils::lowerCaseInPlace(itemid); + + while (esm.isNextSub("FNAM") || esm.isNextSub("ONAM")) { - std::string ownerid = esm.getHString(); - mStolenItems[itemid].insert(std::make_pair(Misc::StringUtils::lowerCase(ownerid), false)); + if (esm.retSubName().toString() == "FNAM") + { + std::string factionid = esm.getHString(); + mStolenItems[itemid].insert(std::make_pair(Misc::StringUtils::lowerCase(factionid), true)); + } + else + { + std::string ownerid = esm.getHString(); + mStolenItems[itemid].insert(std::make_pair(Misc::StringUtils::lowerCase(ownerid), false)); + } } } - } - void write(ESM::ESMWriter &esm) override - { - ESM::StolenItems items; - for (std::map >::const_iterator it = mStolenItems.begin(); it != mStolenItems.end(); ++it) + void write(ESM::ESMWriter& esm) override { - std::map, int> owners; - for (const auto & ownerIt : it->second) + ESM::StolenItems items; + for (auto it = mStolenItems.begin(); it != mStolenItems.end(); ++it) { - owners.insert(std::make_pair(std::make_pair(ownerIt.first, ownerIt.second) - // Since OpenMW doesn't suffer from the owner contamination bug, - // it needs a count argument. But for legacy savegames, we don't know - // this count, so must assume all items of that ID are stolen, - // like vanilla MW did. - ,std::numeric_limits::max())); + std::map, int> owners; + for (const auto& ownerIt : it->second) + { + owners.insert(std::make_pair(std::make_pair(ESM::RefId::stringRefId(ownerIt.first), ownerIt.second) + // Since OpenMW doesn't suffer from the owner contamination bug, + // it needs a count argument. But for legacy savegames, we don't know + // this count, so must assume all items of that ID are stolen, + // like vanilla MW did. + , + std::numeric_limits::max())); + } + + items.mStolenItems.insert(std::make_pair(ESM::RefId::stringRefId(it->first), owners)); } - items.mStolenItems.insert(std::make_pair(it->first, owners)); + esm.startRecord(ESM::REC_STLN); + items.write(esm); + esm.endRecord(ESM::REC_STLN); } - esm.startRecord(ESM::REC_STLN); - items.write(esm); - esm.endRecord(ESM::REC_STLN); - } + private: + typedef std::pair Owner; // -private: - typedef std::pair Owner; // - - std::map > mStolenItems; -}; + std::map> mStolenItems; + }; -/// Seen responses for a dialogue topic? -/// Each DIAL record is followed by a number of INFO records, I believe, just like in ESMs -/// Dialogue conversion problems: -/// - Journal is stored in one continuous HTML markup rather than each entry separately with associated info ID. -/// - Seen dialogue responses only store the INFO id, rather than the fulltext. -/// - Quest stages only store the INFO id, rather than the journal entry fulltext. -class ConvertINFO : public Converter -{ -public: - void read(ESM::ESMReader& esm) override + /// Seen responses for a dialogue topic? + /// Each DIAL record is followed by a number of INFO records, I believe, just like in ESMs + /// Dialogue conversion problems: + /// - Journal is stored in one continuous HTML markup rather than each entry separately with associated info ID. + /// - Seen dialogue responses only store the INFO id, rather than the fulltext. + /// - Quest stages only store the INFO id, rather than the journal entry fulltext. + class ConvertINFO : public Converter { - INFO info; - info.load(esm); - } -}; + public: + void read(ESM::ESMReader& esm) override + { + INFO info; + info.load(esm); + } + }; -class ConvertDIAL : public Converter -{ -public: - void read(ESM::ESMReader& esm) override - { - std::string id = esm.getHNString("NAME"); - DIAL dial; - dial.load(esm); - if (dial.mIndex > 0) - mDials[id] = dial; - } - void write(ESM::ESMWriter &esm) override + class ConvertDIAL : public Converter { - for (std::map::const_iterator it = mDials.begin(); it != mDials.end(); ++it) - { - esm.startRecord(ESM::REC_QUES); - ESM::QuestState state; - state.mFinished = 0; - state.mState = it->second.mIndex; - state.mTopic = Misc::StringUtils::lowerCase(it->first); - state.save(esm); - esm.endRecord(ESM::REC_QUES); + public: + void read(ESM::ESMReader& esm) override + { + std::string id = esm.getHNString("NAME"); + DIAL dial; + dial.load(esm); + if (dial.mIndex > 0) + mDials[id] = dial; + } + void write(ESM::ESMWriter& esm) override + { + for (auto it = mDials.begin(); it != mDials.end(); ++it) + { + esm.startRecord(ESM::REC_QUES); + ESM::QuestState state; + state.mFinished = 0; + state.mState = it->second.mIndex; + state.mTopic = ESM::RefId::stringRefId(it->first); + state.save(esm); + esm.endRecord(ESM::REC_QUES); + } } - } -private: - std::map mDials; -}; -class ConvertQUES : public Converter -{ -public: - void read(ESM::ESMReader& esm) override - { - std::string id = esm.getHNString("NAME"); - QUES quest; - quest.load(esm); - } -}; + private: + std::map mDials; + }; -class ConvertJOUR : public Converter -{ -public: - void read(ESM::ESMReader& esm) override + class ConvertQUES : public Converter { - JOUR journal; - journal.load(esm); - } -}; + public: + void read(ESM::ESMReader& esm) override + { + std::string id = esm.getHNString("NAME"); + QUES quest; + quest.load(esm); + } + }; -class ConvertGAME : public Converter -{ -public: - ConvertGAME() - : mHasGame(false) + class ConvertJOUR : public Converter { - } + public: + void read(ESM::ESMReader& esm) override + { + JOUR journal; + journal.load(esm); + } + }; - void read(ESM::ESMReader &esm) override + class ConvertGAME : public Converter { - mGame.load(esm); - mHasGame = true; - } + public: + ConvertGAME() + : mHasGame(false) + { + } - int validateWeatherID(int weatherID) - { - if(weatherID >= -1 && weatherID < 10) + void read(ESM::ESMReader& esm) override { - return weatherID; + mGame.load(esm); + mHasGame = true; } - else + + int validateWeatherID(int weatherID) { - std::stringstream error; - error << "Invalid weather ID:" << weatherID << std::endl; - throw std::runtime_error(error.str()); + if (weatherID >= -1 && weatherID < 10) + { + return weatherID; + } + else + { + throw std::runtime_error("Invalid weather ID: " + std::to_string(weatherID)); + } } - } - void write(ESM::ESMWriter &esm) override - { - if (!mHasGame) - return; - esm.startRecord(ESM::REC_WTHR); - ESM::WeatherState weather; - weather.mTimePassed = 0.0f; - weather.mFastForward = false; - weather.mWeatherUpdateTime = mGame.mGMDT.mTimeOfNextTransition - mContext->mHour; - weather.mTransitionFactor = 1 - (mGame.mGMDT.mWeatherTransition / 100.0f); - weather.mCurrentWeather = validateWeatherID(mGame.mGMDT.mCurrentWeather); - weather.mNextWeather = validateWeatherID(mGame.mGMDT.mNextWeather); - weather.mQueuedWeather = -1; - // TODO: Determine how ModRegion modifiers are saved in Morrowind. - weather.save(esm); - esm.endRecord(ESM::REC_WTHR); - } - -private: - bool mHasGame; - GAME mGame; -}; - -/// Running global script -class ConvertSCPT : public Converter -{ -public: - void read(ESM::ESMReader &esm) override - { - SCPT script; - script.load(esm); - ESM::GlobalScript out; - convertSCPT(script, out); - mScripts.push_back(out); - } - void write(ESM::ESMWriter &esm) override + void write(ESM::ESMWriter& esm) override + { + if (!mHasGame) + return; + esm.startRecord(ESM::REC_WTHR); + ESM::WeatherState weather; + weather.mTimePassed = 0.0f; + weather.mFastForward = false; + weather.mWeatherUpdateTime = mGame.mGMDT.mTimeOfNextTransition - mContext->mHour; + weather.mTransitionFactor = 1 - (mGame.mGMDT.mWeatherTransition / 100.0f); + weather.mCurrentWeather = validateWeatherID(mGame.mGMDT.mCurrentWeather); + weather.mNextWeather = validateWeatherID(mGame.mGMDT.mNextWeather); + weather.mQueuedWeather = -1; + // TODO: Determine how ModRegion modifiers are saved in Morrowind. + weather.save(esm); + esm.endRecord(ESM::REC_WTHR); + } + + private: + bool mHasGame; + GAME mGame; + }; + + /// Running global script + class ConvertSCPT : public Converter { - for (const auto & script : mScripts) + public: + void read(ESM::ESMReader& esm) override + { + SCPT script; + script.load(esm); + ESM::GlobalScript out; + convertSCPT(script, out); + mScripts.push_back(out); + } + void write(ESM::ESMWriter& esm) override { - esm.startRecord(ESM::REC_GSCR); - script.save(esm); - esm.endRecord(ESM::REC_GSCR); + for (const auto& script : mScripts) + { + esm.startRecord(ESM::REC_GSCR); + script.save(esm); + esm.endRecord(ESM::REC_GSCR); + } } - } -private: - std::vector mScripts; -}; -/// Projectile converter -class ConvertPROJ : public Converter -{ -public: - int getStage() override { return 2; } - void read(ESM::ESMReader& esm) override; - void write(ESM::ESMWriter& esm) override; -private: - void convertBaseState(ESM::BaseProjectileState& base, const PROJ::PNAM& pnam); - PROJ mProj; -}; - -class ConvertSPLM : public Converter -{ -public: - void read(ESM::ESMReader& esm) override; - void write(ESM::ESMWriter& esm) override; -private: - SPLM mSPLM; -}; + private: + std::vector mScripts; + }; + + /// Projectile converter + class ConvertPROJ : public Converter + { + public: + int getStage() override { return 2; } + void read(ESM::ESMReader& esm) override; + void write(ESM::ESMWriter& esm) override; + + private: + void convertBaseState(ESM::BaseProjectileState& base, const PROJ::PNAM& pnam); + PROJ mProj; + }; + + class ConvertSPLM : public Converter + { + public: + void read(ESM::ESMReader& esm) override; + void write(ESM::ESMWriter& esm) override; + + private: + SPLM mSPLM; + }; } diff --git a/apps/essimporter/convertinventory.cpp b/apps/essimporter/convertinventory.cpp index 79e09488c7e..7025b0ae439 100644 --- a/apps/essimporter/convertinventory.cpp +++ b/apps/essimporter/convertinventory.cpp @@ -1,22 +1,22 @@ #include "convertinventory.hpp" -#include +#include + #include namespace ESSImport { - void convertInventory(const Inventory &inventory, ESM::InventoryState &state) + void convertInventory(const Inventory& inventory, ESM::InventoryState& state) { - int index = 0; - for (const auto & item : inventory.mItems) + uint32_t index = 0; + for (const auto& item : inventory.mItems) { ESM::ObjectState objstate; objstate.blank(); objstate.mRef = item; - objstate.mRef.mRefID = Misc::StringUtils::lowerCase(item.mId); - objstate.mCount = std::abs(item.mCount); // restocking items have negative count in the savefile - // openmw handles them differently, so no need to set any flags + objstate.mRef.mRefID = ESM::RefId::stringRefId(item.mId); + objstate.mRef.mCount = item.mCount; state.mItems.push_back(objstate); if (item.mRelativeEquipmentSlot != -1) // Note we should really write the absolute slot here, which we do not know about diff --git a/apps/essimporter/convertinventory.hpp b/apps/essimporter/convertinventory.hpp index 8abe85a44a6..baa10d44146 100644 --- a/apps/essimporter/convertinventory.hpp +++ b/apps/essimporter/convertinventory.hpp @@ -3,12 +3,12 @@ #include "importinventory.hpp" -#include +#include namespace ESSImport { - void convertInventory (const Inventory& inventory, ESM::InventoryState& state); + void convertInventory(const Inventory& inventory, ESM::InventoryState& state); } diff --git a/apps/essimporter/convertnpcc.cpp b/apps/essimporter/convertnpcc.cpp index 48d3d9232e3..f049edd8d74 100644 --- a/apps/essimporter/convertnpcc.cpp +++ b/apps/essimporter/convertnpcc.cpp @@ -5,7 +5,7 @@ namespace ESSImport { - void convertNPCC(const NPCC &npcc, ESM::NpcState &npcState) + void convertNPCC(const NPCC& npcc, ESM::NpcState& npcState) { npcState.mNpcStats.mDisposition = npcc.mNPDT.mDisposition; npcState.mNpcStats.mReputation = npcc.mNPDT.mReputation; diff --git a/apps/essimporter/convertnpcc.hpp b/apps/essimporter/convertnpcc.hpp index eb12d8f3bcb..f0c35b72abe 100644 --- a/apps/essimporter/convertnpcc.hpp +++ b/apps/essimporter/convertnpcc.hpp @@ -3,12 +3,12 @@ #include "importnpcc.hpp" -#include +#include namespace ESSImport { - void convertNPCC (const NPCC& npcc, ESM::NpcState& npcState); + void convertNPCC(const NPCC& npcc, ESM::NpcState& npcState); } diff --git a/apps/essimporter/convertplayer.cpp b/apps/essimporter/convertplayer.cpp index b3ccbca3585..29c49451fd0 100644 --- a/apps/essimporter/convertplayer.cpp +++ b/apps/essimporter/convertplayer.cpp @@ -1,28 +1,35 @@ #include "convertplayer.hpp" +#include + +#include #include -#include +#include namespace ESSImport { - void convertPCDT(const PCDT& pcdt, ESM::Player& out, std::vector& outDialogueTopics, bool& firstPersonCam, bool& teleportingEnabled, bool& levitationEnabled, ESM::ControlsState& controls) + void convertPCDT(const PCDT& pcdt, ESM::Player& out, std::vector& outDialogueTopics, + bool& firstPersonCam, bool& teleportingEnabled, bool& levitationEnabled, ESM::ControlsState& controls) { - out.mBirthsign = pcdt.mBirthsign; + out.mObject.mPosition.rot[0] + = -atan2(pcdt.mPNAM.mVerticalRotation.mData[2][1], pcdt.mPNAM.mVerticalRotation.mData[2][2]); + + out.mBirthsign = ESM::RefId::stringRefId(pcdt.mBirthsign); out.mObject.mNpcStats.mBounty = pcdt.mBounty; - for (const auto & essFaction : pcdt.mFactions) + for (const auto& essFaction : pcdt.mFactions) { ESM::NpcStats::Faction faction; faction.mExpelled = (essFaction.mFlags & 0x2) != 0; faction.mRank = essFaction.mRank; faction.mReputation = essFaction.mReputation; - out.mObject.mNpcStats.mFactions[Misc::StringUtils::lowerCase(essFaction.mFactionName.toString())] = faction; + out.mObject.mNpcStats.mFactions[ESM::RefId::stringRefId(essFaction.mFactionName.toString())] = faction; } - for (int i=0; i<3; ++i) + for (size_t i = 0; i < out.mObject.mNpcStats.mSpecIncreases.size(); ++i) out.mObject.mNpcStats.mSpecIncreases[i] = pcdt.mPNAM.mSpecIncreases[i]; - for (int i=0; i<8; ++i) + for (size_t i = 0; i < out.mObject.mNpcStats.mSkillIncrease.size(); ++i) out.mObject.mNpcStats.mSkillIncrease[i] = pcdt.mPNAM.mSkillIncreases[i]; - for (int i=0; i<27; ++i) + for (size_t i = 0; i < out.mObject.mNpcStats.mSkills.size(); ++i) out.mObject.mNpcStats.mSkills[i].mProgress = pcdt.mPNAM.mSkillProgress[i]; out.mObject.mNpcStats.mLevelProgress = pcdt.mPNAM.mLevelProgress; @@ -35,9 +42,9 @@ namespace ESSImport teleportingEnabled = !(pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_TeleportingDisabled); levitationEnabled = !(pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_LevitationDisabled); - for (const auto & knownDialogueTopic : pcdt.mKnownDialogueTopics) + for (const auto& knownDialogueTopic : pcdt.mKnownDialogueTopics) { - outDialogueTopics.push_back(Misc::StringUtils::lowerCase(knownDialogueTopic)); + outDialogueTopics.push_back(ESM::RefId::stringRefId(knownDialogueTopic)); } controls.mViewSwitchDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_ViewSwitchDisabled; @@ -54,19 +61,9 @@ namespace ESSImport const PCDT::PNAM::MarkLocation& mark = pcdt.mPNAM.mMarkLocation; - ESM::CellId cell; - cell.mWorldspace = ESM::CellId::sDefaultWorldspace; - cell.mPaged = true; - - cell.mIndex.mX = mark.mCellX; - cell.mIndex.mY = mark.mCellY; - // TODO: Figure out a better way to detect interiors. (0, 0) is a valid exterior cell. - if (mark.mCellX == 0 && mark.mCellY == 0) - { - cell.mWorldspace = pcdt.mMNAM; - cell.mPaged = false; - } + bool interior = mark.mCellX == 0 && mark.mCellY == 0; + ESM::RefId cell = ESM::Cell::generateIdForCell(!interior, pcdt.mMNAM, mark.mCellX, mark.mCellY); out.mMarkedCell = cell; out.mMarkedPosition.pos[0] = mark.mX; diff --git a/apps/essimporter/convertplayer.hpp b/apps/essimporter/convertplayer.hpp index 1d2fdc87a6a..ccc0f7d64dd 100644 --- a/apps/essimporter/convertplayer.hpp +++ b/apps/essimporter/convertplayer.hpp @@ -3,13 +3,14 @@ #include "importplayer.hpp" -#include -#include +#include +#include namespace ESSImport { - void convertPCDT(const PCDT& pcdt, ESM::Player& out, std::vector& outDialogueTopics, bool& firstPersonCam, bool& teleportingEnabled, bool& levitationEnabled, ESM::ControlsState& controls); + void convertPCDT(const PCDT& pcdt, ESM::Player& out, std::vector& outDialogueTopics, + bool& firstPersonCam, bool& teleportingEnabled, bool& levitationEnabled, ESM::ControlsState& controls); } diff --git a/apps/essimporter/convertscpt.cpp b/apps/essimporter/convertscpt.cpp index cb7947e4008..dad3ed7b0b8 100644 --- a/apps/essimporter/convertscpt.cpp +++ b/apps/essimporter/convertscpt.cpp @@ -1,17 +1,17 @@ #include "convertscpt.hpp" -#include - #include "convertscri.hpp" +#include + namespace ESSImport { - void convertSCPT(const SCPT &scpt, ESM::GlobalScript &out) + void convertSCPT(const SCPT& scpt, ESM::GlobalScript& out) { - out.mId = Misc::StringUtils::lowerCase(scpt.mSCHD.mName.toString()); + out.mId = ESM::RefId::stringRefId(scpt.mSCHD.mName.toString()); out.mRunning = scpt.mRunning; - out.mTargetRef.unset(); // TODO: convert target reference of global script + out.mTargetRef = ESM::RefNum{}; // TODO: convert target reference of global script convertSCRI(scpt.mSCRI, out.mLocals); } diff --git a/apps/essimporter/convertscpt.hpp b/apps/essimporter/convertscpt.hpp index 3390bd6070d..854cf7c9a0f 100644 --- a/apps/essimporter/convertscpt.hpp +++ b/apps/essimporter/convertscpt.hpp @@ -1,14 +1,14 @@ #ifndef OPENMW_ESSIMPORT_CONVERTSCPT_H #define OPENMW_ESSIMPORT_CONVERTSCPT_H -#include +#include #include "importscpt.hpp" namespace ESSImport { -void convertSCPT(const SCPT& scpt, ESM::GlobalScript& out); + void convertSCPT(const SCPT& scpt, ESM::GlobalScript& out); } diff --git a/apps/essimporter/convertscri.cpp b/apps/essimporter/convertscri.cpp index eba48df77df..1dbe476e2b6 100644 --- a/apps/essimporter/convertscri.cpp +++ b/apps/essimporter/convertscri.cpp @@ -19,12 +19,12 @@ namespace namespace ESSImport { - void convertSCRI(const SCRI &scri, ESM::Locals &locals) + void convertSCRI(const SCRI& scri, ESM::Locals& locals) { // order *is* important, as we do not have variable names available in this format - storeVariables (scri.mShorts, locals, scri.mScript); - storeVariables (scri.mLongs, locals, scri.mScript); - storeVariables (scri.mFloats, locals, scri.mScript); + storeVariables(scri.mShorts, locals, scri.mScript); + storeVariables(scri.mLongs, locals, scri.mScript); + storeVariables(scri.mFloats, locals, scri.mScript); } } diff --git a/apps/essimporter/convertscri.hpp b/apps/essimporter/convertscri.hpp index 2d894566623..4a1026c3994 100644 --- a/apps/essimporter/convertscri.hpp +++ b/apps/essimporter/convertscri.hpp @@ -3,13 +3,13 @@ #include "importscri.hpp" -#include +#include namespace ESSImport { /// Convert script variable assignments - void convertSCRI (const SCRI& scri, ESM::Locals& locals); + void convertSCRI(const SCRI& scri, ESM::Locals& locals); } diff --git a/apps/essimporter/importacdt.cpp b/apps/essimporter/importacdt.cpp deleted file mode 100644 index 0ddd2eb64ce..00000000000 --- a/apps/essimporter/importacdt.cpp +++ /dev/null @@ -1,135 +0,0 @@ -#include "importacdt.hpp" - -#include - -#include - -namespace ESSImport -{ - - void ActorData::load(ESM::ESMReader &esm) - { - if (esm.isNextSub("ACTN")) - { - /* - Activation flags: - ActivationFlag_UseEnabled = 1 - ActivationFlag_OnActivate = 2 - ActivationFlag_OnDeath = 10h - ActivationFlag_OnKnockout = 20h - ActivationFlag_OnMurder = 40h - ActivationFlag_DoorOpening = 100h - ActivationFlag_DoorClosing = 200h - ActivationFlag_DoorJammedOpening = 400h - ActivationFlag_DoorJammedClosing = 800h - */ - esm.skipHSub(); - } - - if (esm.isNextSub("STPR")) - esm.skipHSub(); - - if (esm.isNextSub("MNAM")) - esm.skipHSub(); - - bool isDeleted = false; - ESM::CellRef::loadData(esm, isDeleted); - - mHasACDT = false; - if (esm.isNextSub("ACDT")) - { - mHasACDT = true; - esm.getHT(mACDT); - } - - mHasACSC = false; - if (esm.isNextSub("ACSC")) - { - mHasACSC = true; - esm.getHT(mACSC); - } - - if (esm.isNextSub("ACSL")) - esm.skipHSubSize(112); - - if (esm.isNextSub("CSTN")) - esm.skipHSub(); // "PlayerSaveGame", link to some object? - - if (esm.isNextSub("LSTN")) - esm.skipHSub(); // "PlayerSaveGame", link to some object? - - // unsure at which point between LSTN and TGTN - if (esm.isNextSub("CSHN")) - esm.skipHSub(); // "PlayerSaveGame", link to some object? - - // unsure if before or after CSTN/LSTN - if (esm.isNextSub("LSHN")) - esm.skipHSub(); // "PlayerSaveGame", link to some object? - - while (esm.isNextSub("TGTN")) - esm.skipHSub(); // "PlayerSaveGame", link to some object? - - while (esm.isNextSub("FGTN")) - esm.getHString(); // fight target? - - // unsure at which point between TGTN and CRED - if (esm.isNextSub("AADT")) - { - // occurred when a creature was in the middle of its attack, 44 bytes - esm.skipHSub(); - } - - // unsure at which point between FGTN and CHRD - if (esm.isNextSub("PWPC")) - esm.skipHSub(); - if (esm.isNextSub("PWPS")) - esm.skipHSub(); - - if (esm.isNextSub("WNAM")) - { - std::string id = esm.getHString(); - - if (esm.isNextSub("XNAM")) - mSelectedEnchantItem = esm.getHString(); - else - mSelectedSpell = id; - - if (esm.isNextSub("YNAM")) - esm.skipHSub(); // 4 byte, 0 - } - - while (esm.isNextSub("APUD")) - { - // used power - esm.getSubHeader(); - std::string id = esm.getString(32); - (void)id; - // timestamp can't be used: this is the total hours passed, calculated by - // timestamp = 24 * (365 * year + cumulativeDays[month] + day) - // unfortunately cumulativeDays[month] is not clearly defined, - // in the (non-MCP) vanilla version the first month was missing, but MCP added it. - double timestamp; - esm.getT(timestamp); - } - - // FIXME: not all actors have this, add flag - if (esm.isNextSub("CHRD")) // npc only - esm.getHExact(mSkills, 27*2*sizeof(int)); - - if (esm.isNextSub("CRED")) // creature only - esm.getHExact(mCombatStats, 3*2*sizeof(int)); - - mSCRI.load(esm); - - if (esm.isNextSub("ND3D")) - esm.skipHSub(); - - mHasANIS = false; - if (esm.isNextSub("ANIS")) - { - mHasANIS = true; - esm.getHT(mANIS); - } - } - -} diff --git a/apps/essimporter/importacdt.hpp b/apps/essimporter/importacdt.hpp index 354eca32d89..65519c6a6c4 100644 --- a/apps/essimporter/importacdt.hpp +++ b/apps/essimporter/importacdt.hpp @@ -1,10 +1,9 @@ #ifndef OPENMW_ESSIMPORT_ACDT_H #define OPENMW_ESSIMPORT_ACDT_H +#include #include -#include - #include "importscri.hpp" namespace ESM @@ -27,22 +26,21 @@ namespace ESSImport }; /// Actor data, shared by (at least) REFR and CellRef -#pragma pack(push) -#pragma pack(1) struct ACDT { // Note, not stored at *all*: // - Level changes are lost on reload, except for the player (there it's in the NPC record). unsigned char mUnknown[12]; - unsigned int mFlags; + uint32_t mFlags; float mBreathMeter; // Seconds left before drowning unsigned char mUnknown2[20]; float mDynamic[3][2]; unsigned char mUnknown3[16]; float mAttributes[8][2]; - float mMagicEffects[27]; // Effect attributes: https://wiki.openmw.org/index.php?title=Research:Magic#Effect_attributes + float mMagicEffects[27]; // Effect attributes: + // https://wiki.openmw.org/index.php?title=Research:Magic#Effect_attributes unsigned char mUnknown4[4]; - unsigned int mGoldPool; + uint32_t mGoldPool; unsigned char mCountDown; // seen the same value as in ACSC.mCorpseClearCountdown, maybe // this one is for respawning? unsigned char mUnknown5[3]; @@ -61,9 +59,8 @@ namespace ESSImport unsigned char mUnknown[3]; float mTime; }; -#pragma pack(pop) - struct ActorData : public ESM::CellRef + struct ActorData { bool mHasACDT; ACDT mACDT; @@ -85,10 +82,6 @@ namespace ESSImport bool mHasANIS; ANIS mANIS; // scripted animation state - - virtual void load(ESM::ESMReader& esm); - - virtual ~ActorData() = default; }; } diff --git a/apps/essimporter/importcellref.cpp b/apps/essimporter/importcellref.cpp index 442a7781c74..9e8e9a6948b 100644 --- a/apps/essimporter/importcellref.cpp +++ b/apps/essimporter/importcellref.cpp @@ -1,32 +1,156 @@ #include "importcellref.hpp" -#include +#include +#include + +#include namespace ESSImport { + template T> + void decompose(T&& v, const auto& f) + { + f(v.mUnknown, v.mFlags, v.mBreathMeter, v.mUnknown2, v.mDynamic, v.mUnknown3, v.mAttributes, v.mMagicEffects, + v.mUnknown4, v.mGoldPool, v.mCountDown, v.mUnknown5); + } + + template T> + void decompose(T&& v, const auto& f) + { + f(v.mUnknown1, v.mFlags, v.mUnknown2, v.mCorpseClearCountdown, v.mUnknown3); + } - void CellRef::load(ESM::ESMReader &esm) + template T> + void decompose(T&& v, const auto& f) + { + f(v.mGroupIndex, v.mUnknown, v.mTime); + } + + void CellRef::load(ESM::ESMReader& esm) { blank(); - // (FRMR subrecord name is already read by the loop in ConvertCell) - esm.getHT(mRefNum.mIndex); // FRMR + esm.getHNT(mRefNum.mIndex, "FRMR"); // this is required since openmw supports more than 255 content files int pluginIndex = (mRefNum.mIndex & 0xff000000) >> 24; - mRefNum.mContentFile = pluginIndex-1; + mRefNum.mContentFile = pluginIndex - 1; mRefNum.mIndex &= 0x00ffffff; mIndexedRefId = esm.getHNString("NAME"); - ActorData::load(esm); + if (esm.isNextSub("ACTN")) + { + /* + Activation flags: + ActivationFlag_UseEnabled = 1 + ActivationFlag_OnActivate = 2 + ActivationFlag_OnDeath = 10h + ActivationFlag_OnKnockout = 20h + ActivationFlag_OnMurder = 40h + ActivationFlag_DoorOpening = 100h + ActivationFlag_DoorClosing = 200h + ActivationFlag_DoorJammedOpening = 400h + ActivationFlag_DoorJammedClosing = 800h + */ + esm.skipHSub(); + } + + if (esm.isNextSub("STPR")) + esm.skipHSub(); + + if (esm.isNextSub("MNAM")) + esm.skipHSub(); + + bool isDeleted = false; + ESM::CellRef::loadData(esm, isDeleted); + + mActorData.mHasACDT = esm.getOptionalComposite("ACDT", mActorData.mACDT); + + mActorData.mHasACSC = esm.getOptionalComposite("ACSC", mActorData.mACSC); + + if (esm.isNextSub("ACSL")) + esm.skipHSubSize(112); + + if (esm.isNextSub("CSTN")) + esm.skipHSub(); // "PlayerSaveGame", link to some object? + + if (esm.isNextSub("LSTN")) + esm.skipHSub(); // "PlayerSaveGame", link to some object? + + // unsure at which point between LSTN and TGTN + if (esm.isNextSub("CSHN")) + esm.skipHSub(); // "PlayerSaveGame", link to some object? + + // unsure if before or after CSTN/LSTN + if (esm.isNextSub("LSHN")) + esm.skipHSub(); // "PlayerSaveGame", link to some object? + + while (esm.isNextSub("TGTN")) + esm.skipHSub(); // "PlayerSaveGame", link to some object? + + while (esm.isNextSub("FGTN")) + esm.getHString(); // fight target? + + // unsure at which point between TGTN and CRED + if (esm.isNextSub("AADT")) + { + // occurred when a creature was in the middle of its attack, 44 bytes + esm.skipHSub(); + } + + // unsure at which point between FGTN and CHRD + if (esm.isNextSub("PWPC")) + esm.skipHSub(); + if (esm.isNextSub("PWPS")) + esm.skipHSub(); + + if (esm.isNextSub("WNAM")) + { + std::string id = esm.getHString(); + + if (esm.isNextSub("XNAM")) + mActorData.mSelectedEnchantItem = esm.getHString(); + else + mActorData.mSelectedSpell = std::move(id); + + if (esm.isNextSub("YNAM")) + esm.skipHSub(); // 4 byte, 0 + } + + while (esm.isNextSub("APUD")) + { + // used power + esm.getSubHeader(); + std::string id = esm.getMaybeFixedStringSize(32); + (void)id; + // timestamp can't be used: this is the total hours passed, calculated by + // timestamp = 24 * (365 * year + cumulativeDays[month] + day) + // unfortunately cumulativeDays[month] is not clearly defined, + // in the (non-MCP) vanilla version the first month was missing, but MCP added it. + double timestamp; + esm.getT(timestamp); + } + + // FIXME: not all actors have this, add flag + esm.getHNOT("CHRD", mActorData.mSkills); // npc only + + esm.getHNOT("CRED", mActorData.mCombatStats); // creature only + + mActorData.mSCRI.load(esm); + + if (esm.isNextSub("ND3D")) + esm.skipHSub(); + + mActorData.mHasANIS = esm.getOptionalComposite("ANIS", mActorData.mANIS); + if (esm.isNextSub("LVCR")) { // occurs on levelled creature spawner references // probably some identifier for the creature that has been spawned? unsigned char lvcr; esm.getHT(lvcr); - //std::cout << "LVCR: " << (int)lvcr << std::endl; + // std::cout << "LVCR: " << (int)lvcr << std::endl; } mEnabled = true; @@ -35,13 +159,13 @@ namespace ESSImport // DATA should occur for all references, except levelled creature spawners // I've seen DATA *twice* on a creature record, and with the exact same content too! weird // alarmvoi0000.ess - esm.getHNOT(mPos, "DATA", 24); - esm.getHNOT(mPos, "DATA", 24); + for (int i = 0; i < 2; ++i) + esm.getOptionalComposite("DATA", mPos); mDeleted = 0; if (esm.isNextSub("DELE")) { - unsigned int deleted; + uint32_t deleted; esm.getHT(deleted); mDeleted = ((deleted >> 24) & 0x2) != 0; // the other 3 bytes seem to be uninitialized garbage } diff --git a/apps/essimporter/importcellref.hpp b/apps/essimporter/importcellref.hpp index b115628d5ef..dfc44711e6f 100644 --- a/apps/essimporter/importcellref.hpp +++ b/apps/essimporter/importcellref.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include "importacdt.hpp" @@ -15,7 +15,7 @@ namespace ESM namespace ESSImport { - struct CellRef : public ActorData + struct CellRef : public ESM::CellRef { std::string mIndexedRefId; @@ -25,9 +25,11 @@ namespace ESSImport bool mDeleted; - void load(ESM::ESMReader& esm) override; + ActorData mActorData; - virtual ~CellRef() = default; + void load(ESM::ESMReader& esm); + + ~CellRef() = default; }; } diff --git a/apps/essimporter/importcntc.cpp b/apps/essimporter/importcntc.cpp index a492aef5aa1..34c99babef8 100644 --- a/apps/essimporter/importcntc.cpp +++ b/apps/essimporter/importcntc.cpp @@ -1,11 +1,12 @@ #include "importcntc.hpp" -#include +#include +#include namespace ESSImport { - void CNTC::load(ESM::ESMReader &esm) + void CNTC::load(ESM::ESMReader& esm) { mIndex = 0; esm.getHNT(mIndex, "INDX"); diff --git a/apps/essimporter/importcntc.hpp b/apps/essimporter/importcntc.hpp index 1bc7d94bd5e..6ee843805e2 100644 --- a/apps/essimporter/importcntc.hpp +++ b/apps/essimporter/importcntc.hpp @@ -14,7 +14,7 @@ namespace ESSImport /// Changed container contents struct CNTC { - int mIndex; + int32_t mIndex; Inventory mInventory; diff --git a/apps/essimporter/importcrec.cpp b/apps/essimporter/importcrec.cpp index 64879f2afc1..38ebd724f73 100644 --- a/apps/essimporter/importcrec.cpp +++ b/apps/essimporter/importcrec.cpp @@ -1,11 +1,11 @@ #include "importcrec.hpp" -#include +#include namespace ESSImport { - void CREC::load(ESM::ESMReader &esm) + void CREC::load(ESM::ESMReader& esm) { esm.getHNT(mIndex, "INDX"); @@ -14,9 +14,8 @@ namespace ESSImport float scale; esm.getHNOT(scale, "XSCL"); - while (esm.isNextSub("AI_W") || esm.isNextSub("AI_E") || esm.isNextSub("AI_T") || esm.isNextSub("AI_F") - || esm.isNextSub("AI_A")) + || esm.isNextSub("AI_A")) mAiPackages.add(esm); mInventory.load(esm); diff --git a/apps/essimporter/importcrec.hpp b/apps/essimporter/importcrec.hpp index 5110fbc6896..5217f4edc45 100644 --- a/apps/essimporter/importcrec.hpp +++ b/apps/essimporter/importcrec.hpp @@ -2,7 +2,8 @@ #define OPENMW_ESSIMPORT_CREC_H #include "importinventory.hpp" -#include +#include +#include namespace ESM { @@ -15,7 +16,7 @@ namespace ESSImport /// Creature changes struct CREC { - int mIndex; + int32_t mIndex; Inventory mInventory; ESM::AIPackageList mAiPackages; diff --git a/apps/essimporter/importdial.cpp b/apps/essimporter/importdial.cpp index 5797a708a1b..43905738a1c 100644 --- a/apps/essimporter/importdial.cpp +++ b/apps/essimporter/importdial.cpp @@ -1,18 +1,18 @@ #include "importdial.hpp" -#include +#include namespace ESSImport { - void DIAL::load(ESM::ESMReader &esm) + void DIAL::load(ESM::ESMReader& esm) { // See ESM::Dialogue::Type enum, not sure why we would need this here though - int type = 0; + int32_t type = 0; esm.getHNOT(type, "DATA"); // Deleted dialogue in a savefile. No clue what this means... - int deleted = 0; + int32_t deleted = 0; esm.getHNOT(deleted, "DELE"); mIndex = 0; diff --git a/apps/essimporter/importdial.hpp b/apps/essimporter/importdial.hpp index 9a1e8823320..b8b6fd536a8 100644 --- a/apps/essimporter/importdial.hpp +++ b/apps/essimporter/importdial.hpp @@ -1,5 +1,8 @@ #ifndef OPENMW_ESSIMPORT_IMPORTDIAL_H #define OPENMW_ESSIMPORT_IMPORTDIAL_H + +#include + namespace ESM { class ESMReader; @@ -10,7 +13,7 @@ namespace ESSImport struct DIAL { - int mIndex; // Journal index + int32_t mIndex; // Journal index void load(ESM::ESMReader& esm); }; diff --git a/apps/essimporter/importer.cpp b/apps/essimporter/importer.cpp index 706512263e8..5cc9a8259b4 100644 --- a/apps/essimporter/importer.cpp +++ b/apps/essimporter/importer.cpp @@ -1,27 +1,27 @@ #include "importer.hpp" +#include +#include #include -#include -#include - -#include #include +#include -#include -#include #include +#include +#include -#include -#include +#include +#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include @@ -36,25 +36,25 @@ namespace void writeScreenshot(const ESM::Header& fileHeader, ESM::SavedGame& out) { - if (fileHeader.mSCRS.size() != 128*128*4) + if (fileHeader.mSCRS.size() != 128 * 128 * 4) { std::cerr << "Error: unexpected screenshot size " << std::endl; return; } - osg::ref_ptr image (new osg::Image); + osg::ref_ptr image(new osg::Image); image->allocateImage(128, 128, 1, GL_RGB, GL_UNSIGNED_BYTE); // need to convert pixel format from BGRA to RGB as the jpg readerwriter doesn't support it otherwise auto it = fileHeader.mSCRS.begin(); - for (int y=0; y<128; ++y) + for (int y = 0; y < 128; ++y) { - for (int x=0; x<128; ++x) + for (int x = 0; x < 128; ++x) { - assert(image->data(x,y)); - *(image->data(x,y)+2) = *it++; - *(image->data(x,y)+1) = *it++; - *image->data(x,y) = *it++; + assert(image->data(x, y)); + *(image->data(x, y) + 2) = *it++; + *(image->data(x, y) + 1) = *it++; + *image->data(x, y) = *it++; ++it; // skip alpha } } @@ -73,7 +73,8 @@ namespace osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*image, ostream); if (!result.success()) { - std::cerr << "Error: can't write screenshot: " << result.message() << " code " << result.status() << std::endl; + std::cerr << "Error: can't write screenshot: " << result.message() << " code " << result.status() + << std::endl; return; } @@ -86,12 +87,12 @@ namespace namespace ESSImport { - Importer::Importer(const std::string &essfile, const std::string &outfile, const std::string &encoding) + Importer::Importer( + const std::filesystem::path& essfile, const std::filesystem::path& outfile, const std::string& encoding) : mEssFile(essfile) , mOutFile(outfile) , mEncoding(encoding) { - } struct File @@ -113,7 +114,7 @@ namespace ESSImport std::vector mRecords; }; - void read(const std::string& filename, File& file) + void read(const std::filesystem::path& filename, File& file) { ESM::ESMReader esm; esm.open(filename); @@ -134,7 +135,7 @@ namespace ESSImport sub.mFileOffset = esm.getFileOffset(); sub.mName = esm.retSubName().toString(); sub.mData.resize(esm.getSubSize()); - esm.getExact(&sub.mData[0], sub.mData.size()); + esm.getExact(sub.mData.data(), sub.mData.size()); rec.mSubrecords.push_back(sub); } file.mRecords.push_back(rec); @@ -144,14 +145,14 @@ namespace ESSImport void Importer::compare() { // data that always changes (and/or is already fully decoded) should be blacklisted - std::set > blacklist; + std::set> blacklist; blacklist.insert(std::make_pair("GLOB", "FLTV")); // gamehour blacklist.insert(std::make_pair("REFR", "DATA")); // player position blacklist.insert(std::make_pair("CELL", "NAM8")); // fog of war blacklist.insert(std::make_pair("GAME", "GMDT")); // weather data, current time always changes blacklist.insert(std::make_pair("CELL", "DELE")); // first 3 bytes are uninitialized - // this changes way too often, name suggests some renderer internal data? + // this changes way too often, name suggests some renderer internal data? blacklist.insert(std::make_pair("CELL", "ND3D")); blacklist.insert(std::make_pair("REFR", "ND3D")); @@ -161,7 +162,7 @@ namespace ESSImport read(mOutFile, file2); // todo rename variable // FIXME: use max(size1, size2) - for (unsigned int i=0; i= rec2.mSubrecords.size()) { std::ios::fmtflags f(std::cout.flags()); - std::cout << "Subrecord in file1 not present in file2: (1) 0x" << std::hex << sub.mFileOffset << std::endl; + std::cout << "Subrecord in file1 not present in file2: (1) 0x" << std::hex << sub.mFileOffset + << std::endl; std::cout.flags(f); return; } @@ -201,8 +203,9 @@ namespace ESSImport if (sub.mName != sub2.mName) { std::ios::fmtflags f(std::cout.flags()); - std::cout << "Different subrecord name (" << rec.mName << "." << sub.mName << " vs. " << sub2.mName << ") at (1) 0x" << std::hex << sub.mFileOffset - << " (2) 0x" << sub2.mFileOffset << std::endl; + std::cout << "Different subrecord name (" << rec.mName << "." << sub.mName << " vs. " << sub2.mName + << ") at (1) 0x" << std::hex << sub.mFileOffset << " (2) 0x" << sub2.mFileOffset + << std::endl; std::cout.flags(f); break; // TODO: try to recover } @@ -214,11 +217,11 @@ namespace ESSImport std::ios::fmtflags f(std::cout.flags()); - std::cout << "Different subrecord data for " << rec.mName << "." << sub.mName << " at (1) 0x" << std::hex << sub.mFileOffset - << " (2) 0x" << sub2.mFileOffset << std::endl; + std::cout << "Different subrecord data for " << rec.mName << "." << sub.mName << " at (1) 0x" + << std::hex << sub.mFileOffset << " (2) 0x" << sub2.mFileOffset << std::endl; std::cout << "Data 1:" << std::endl; - for (unsigned int k=0; k= sub2.mData.size() || sub2.mData[k] != sub.mData[k]) @@ -233,7 +236,7 @@ namespace ESSImport std::cout << std::endl; std::cout << "Data 2:" << std::endl; - for (unsigned int k=0; k= sub.mData.size() || sub.mData[k] != sub2.mData[k]) @@ -264,48 +267,48 @@ namespace ESSImport const ESM::Header& header = esm.getHeader(); context.mPlayerCellName = header.mGameData.mCurrentCell.toString(); - const unsigned int recREFR = ESM::FourCC<'R','E','F','R'>::value; - const unsigned int recPCDT = ESM::FourCC<'P','C','D','T'>::value; - const unsigned int recFMAP = ESM::FourCC<'F','M','A','P'>::value; - const unsigned int recKLST = ESM::FourCC<'K','L','S','T'>::value; - const unsigned int recSTLN = ESM::FourCC<'S','T','L','N'>::value; - const unsigned int recGAME = ESM::FourCC<'G','A','M','E'>::value; - const unsigned int recJOUR = ESM::FourCC<'J','O','U','R'>::value; - const unsigned int recSPLM = ESM::FourCC<'S','P','L','M'>::value; - - std::map > converters; - converters[ESM::REC_GLOB] = std::shared_ptr(new ConvertGlobal()); - converters[ESM::REC_BOOK] = std::shared_ptr(new ConvertBook()); - converters[ESM::REC_NPC_] = std::shared_ptr(new ConvertNPC()); - converters[ESM::REC_CREA] = std::shared_ptr(new ConvertCREA()); - converters[ESM::REC_NPCC] = std::shared_ptr(new ConvertNPCC()); - converters[ESM::REC_CREC] = std::shared_ptr(new ConvertCREC()); - converters[recREFR ] = std::shared_ptr(new ConvertREFR()); - converters[recPCDT ] = std::shared_ptr(new ConvertPCDT()); - converters[recFMAP ] = std::shared_ptr(new ConvertFMAP()); - converters[recKLST ] = std::shared_ptr(new ConvertKLST()); - converters[recSTLN ] = std::shared_ptr(new ConvertSTLN()); - converters[recGAME ] = std::shared_ptr(new ConvertGAME()); - converters[ESM::REC_CELL] = std::shared_ptr(new ConvertCell()); - converters[ESM::REC_ALCH] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_CLAS] = std::shared_ptr(new ConvertClass()); - converters[ESM::REC_SPEL] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_ARMO] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_WEAP] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_CLOT] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_ENCH] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_WEAP] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_LEVC] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_LEVI] = std::shared_ptr(new DefaultConverter()); - converters[ESM::REC_CNTC] = std::shared_ptr(new ConvertCNTC()); - converters[ESM::REC_FACT] = std::shared_ptr(new ConvertFACT()); - converters[ESM::REC_INFO] = std::shared_ptr(new ConvertINFO()); - converters[ESM::REC_DIAL] = std::shared_ptr(new ConvertDIAL()); - converters[ESM::REC_QUES] = std::shared_ptr(new ConvertQUES()); - converters[recJOUR ] = std::shared_ptr(new ConvertJOUR()); - converters[ESM::REC_SCPT] = std::shared_ptr(new ConvertSCPT()); - converters[ESM::REC_PROJ] = std::shared_ptr(new ConvertPROJ()); - converters[recSPLM] = std::shared_ptr(new ConvertSPLM()); + const unsigned int recREFR = ESM::fourCC("REFR"); + const unsigned int recPCDT = ESM::fourCC("PCDT"); + const unsigned int recFMAP = ESM::fourCC("FMAP"); + const unsigned int recKLST = ESM::fourCC("KLST"); + const unsigned int recSTLN = ESM::fourCC("STLN"); + const unsigned int recGAME = ESM::fourCC("GAME"); + const unsigned int recJOUR = ESM::fourCC("JOUR"); + const unsigned int recSPLM = ESM::fourCC("SPLM"); + + std::map> converters; + converters[ESM::REC_GLOB] = std::make_unique(); + converters[ESM::REC_BOOK] = std::make_unique(); + converters[ESM::REC_NPC_] = std::make_unique(); + converters[ESM::REC_CREA] = std::make_unique(); + converters[ESM::REC_NPCC] = std::make_unique(); + converters[ESM::REC_CREC] = std::make_unique(); + converters[recREFR] = std::make_unique(); + converters[recPCDT] = std::make_unique(); + converters[recFMAP] = std::make_unique(); + converters[recKLST] = std::make_unique(); + converters[recSTLN] = std::make_unique(); + converters[recGAME] = std::make_unique(); + converters[ESM::REC_CELL] = std::make_unique(); + converters[ESM::REC_ALCH] = std::make_unique>(); + converters[ESM::REC_CLAS] = std::make_unique(); + converters[ESM::REC_SPEL] = std::make_unique>(); + converters[ESM::REC_ARMO] = std::make_unique>(); + converters[ESM::REC_WEAP] = std::make_unique>(); + converters[ESM::REC_CLOT] = std::make_unique>(); + converters[ESM::REC_ENCH] = std::make_unique>(); + converters[ESM::REC_WEAP] = std::make_unique>(); + converters[ESM::REC_LEVC] = std::make_unique>(); + converters[ESM::REC_LEVI] = std::make_unique>(); + converters[ESM::REC_CNTC] = std::make_unique(); + converters[ESM::REC_FACT] = std::make_unique(); + converters[ESM::REC_INFO] = std::make_unique(); + converters[ESM::REC_DIAL] = std::make_unique(); + converters[ESM::REC_QUES] = std::make_unique(); + converters[recJOUR] = std::make_unique(); + converters[ESM::REC_SCPT] = std::make_unique(); + converters[ESM::REC_PROJ] = std::make_unique(); + converters[recSPLM] = std::make_unique(); // TODO: // - REGN (weather in certain regions?) @@ -314,7 +317,7 @@ namespace ESSImport std::set unknownRecords; - for (const auto & converter : converters) + for (const auto& converter : converters) { converter.second->setContext(context); } @@ -324,17 +327,18 @@ namespace ESSImport ESM::NAME n = esm.getRecName(); esm.getRecHeader(); - auto it = converters.find(n.intval); + auto it = converters.find(n.toInt()); if (it != converters.end()) { it->second->read(esm); } else { - if (unknownRecords.insert(n.intval).second) + if (unknownRecords.insert(n.toInt()).second) { std::ios::fmtflags f(std::cerr.flags()); - std::cerr << "Error: unknown record " << n.toString() << " (0x" << std::hex << esm.getFileOffset() << ")" << std::endl; + std::cerr << "Error: unknown record " << n.toString() << " (0x" << std::hex << esm.getFileOffset() + << ")" << std::endl; std::cerr.flags(f); } @@ -344,23 +348,23 @@ namespace ESSImport ESM::ESMWriter writer; - writer.setFormat (ESM::SavedGame::sCurrentFormat); + writer.setFormatVersion(ESM::CurrentSaveGameFormatVersion); - boost::filesystem::ofstream stream(boost::filesystem::path(mOutFile), std::ios::out | std::ios::binary); + std::ofstream stream(mOutFile, std::ios::out | std::ios::binary); // all unused writer.setVersion(0); writer.setType(0); writer.setAuthor(""); writer.setDescription(""); - writer.setRecordCount (0); + writer.setRecordCount(0); - for (const auto & master : header.mMaster) + for (const auto& master : header.mMaster) writer.addMaster(master.name, 0); // not using the size information anyway -> use value of 0 - writer.save (stream); + writer.save(stream); ESM::SavedGame profile; - for (const auto & master : header.mMaster) + for (const auto& master : header.mMaster) { profile.mContentFiles.push_back(master.name); } @@ -369,7 +373,8 @@ namespace ESSImport profile.mInGameTime.mGameHour = context.mHour; profile.mInGameTime.mMonth = context.mMonth; profile.mInGameTime.mYear = context.mYear; - profile.mPlayerCell = header.mGameData.mCurrentCell.toString(); + profile.mTimePlayed = 0; + profile.mPlayerCellName = context.mPlayerCellName; if (context.mPlayerBase.mClass == "NEWCLASSID_CHARGEN") profile.mPlayerClassName = context.mCustomPlayerClassName; else @@ -379,14 +384,13 @@ namespace ESSImport writeScreenshot(header, profile); - writer.startRecord (ESM::REC_SAVE); - profile.save (writer); - writer.endRecord (ESM::REC_SAVE); + writer.startRecord(ESM::REC_SAVE); + profile.save(writer); + writer.endRecord(ESM::REC_SAVE); // Writing order should be Dynamic Store -> Cells -> Player, // so that references to dynamic records can be recognized when loading - for (std::map >::const_iterator it = converters.begin(); - it != converters.end(); ++it) + for (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 0) continue; @@ -394,12 +398,11 @@ namespace ESSImport } writer.startRecord(ESM::REC_NPC_); - context.mPlayerBase.mId = "player"; + context.mPlayerBase.mId = ESM::RefId::stringRefId("Player"); context.mPlayerBase.save(writer); writer.endRecord(ESM::REC_NPC_); - for (std::map >::const_iterator it = converters.begin(); - it != converters.end(); ++it) + for (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 1) continue; @@ -407,14 +410,11 @@ namespace ESSImport } writer.startRecord(ESM::REC_PLAY); - if (context.mPlayer.mCellId.mPaged) - { - // exterior cell -> determine cell coordinates based on position - int cellX = static_cast(std::floor(context.mPlayer.mObject.mPosition.pos[0] / Constants::CellSizeInUnits)); - int cellY = static_cast(std::floor(context.mPlayer.mObject.mPosition.pos[1] / Constants::CellSizeInUnits)); - context.mPlayer.mCellId.mIndex.mX = cellX; - context.mPlayer.mCellId.mIndex.mY = cellY; - } + ESM::CellId cellId = ESM::CellId::extractFromRefId(context.mPlayer.mCellId); + int cellX = static_cast(std::floor(context.mPlayer.mObject.mPosition.pos[0] / Constants::CellSizeInUnits)); + int cellY = static_cast(std::floor(context.mPlayer.mObject.mPosition.pos[1] / Constants::CellSizeInUnits)); + + context.mPlayer.mCellId = ESM::Cell::generateIdForCell(cellId.mPaged, cellId.mWorldspace, cellX, cellY); context.mPlayer.save(writer); writer.endRecord(ESM::REC_PLAY); @@ -423,15 +423,14 @@ namespace ESSImport writer.endRecord(ESM::REC_ACTC); // Stage 2 requires cell references to be written / actors IDs assigned - for (std::map >::const_iterator it = converters.begin(); - it != converters.end(); ++it) + for (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 2) continue; it->second->write(writer); } - writer.startRecord (ESM::REC_DIAS); + writer.startRecord(ESM::REC_DIAS); context.mDialogueState.save(writer); writer.endRecord(ESM::REC_DIAS); @@ -440,5 +439,4 @@ namespace ESSImport writer.endRecord(ESM::REC_INPU); } - } diff --git a/apps/essimporter/importer.hpp b/apps/essimporter/importer.hpp index ccacd7972d1..fba1992808b 100644 --- a/apps/essimporter/importer.hpp +++ b/apps/essimporter/importer.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_ESSIMPORTER_IMPORTER_H #define OPENMW_ESSIMPORTER_IMPORTER_H -#include +#include namespace ESSImport { @@ -9,15 +9,16 @@ namespace ESSImport class Importer { public: - Importer(const std::string& essfile, const std::string& outfile, const std::string& encoding); + Importer( + const std::filesystem::path& essfile, const std::filesystem::path& outfile, const std::string& encoding); void run(); void compare(); private: - std::string mEssFile; - std::string mOutFile; + std::filesystem::path mEssFile; + std::filesystem::path mOutFile; std::string mEncoding; }; diff --git a/apps/essimporter/importercontext.hpp b/apps/essimporter/importercontext.hpp index 179e00f087c..03ea9d0943b 100644 --- a/apps/essimporter/importercontext.hpp +++ b/apps/essimporter/importercontext.hpp @@ -3,22 +3,19 @@ #include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include -#include "importnpcc.hpp" -#include "importcrec.hpp" #include "importcntc.hpp" -#include "importplayer.hpp" +#include "importcrec.hpp" +#include "importnpcc.hpp" #include "importsplm.h" - - namespace ESSImport { @@ -36,7 +33,7 @@ namespace ESSImport ESM::ControlsState mControlsState; // cells which should show an explored overlay on the global map - std::set > mExploredCells; + std::set> mExploredCells; ESM::GlobalMap mGlobalMapState; @@ -44,15 +41,15 @@ namespace ESSImport float mHour; // key - std::map, CREC> mCreatureChanges; - std::map, NPCC> mNpcChanges; - std::map, CNTC> mContainerChanges; + std::map, CREC> mCreatureChanges; + std::map, NPCC> mNpcChanges; + std::map, CNTC> mContainerChanges; - std::map, int> mActorIdMap; + std::map, int> mActorIdMap; int mNextActorId; - std::map mCreatures; - std::map mNpcs; + std::map mCreatures; + std::map mNpcs; std::vector mActiveSpells; @@ -63,20 +60,15 @@ namespace ESSImport , mHour(0.f) , mNextActorId(0) { - ESM::CellId playerCellId; - playerCellId.mPaged = true; - playerCellId.mIndex.mX = playerCellId.mIndex.mY = 0; - mPlayer.mCellId = playerCellId; - mPlayer.mLastKnownExteriorPosition[0] - = mPlayer.mLastKnownExteriorPosition[1] - = mPlayer.mLastKnownExteriorPosition[2] - = 0.0f; + mPlayer.mCellId = ESM::RefId::esm3ExteriorCell(0, 0); + mPlayer.mLastKnownExteriorPosition[0] = mPlayer.mLastKnownExteriorPosition[1] + = mPlayer.mLastKnownExteriorPosition[2] = 0.0f; mPlayer.mHasMark = 0; mPlayer.mCurrentCrimeId = -1; // TODO mPlayer.mPaidCrimeId = -1; mPlayer.mObject.blank(); mPlayer.mObject.mEnabled = true; - mPlayer.mObject.mRef.mRefID = "player"; // REFR.mRefID would be PlayerSaveGame + mPlayer.mObject.mRef.mRefID = ESM::RefId::stringRefId("player"); // REFR.mRefID would be PlayerSaveGame mPlayer.mObject.mCreatureStats.mActorId = generateActorId(); mGlobalMapState.mBounds.mMinX = 0; @@ -87,10 +79,7 @@ namespace ESSImport mPlayerBase.blank(); } - int generateActorId() - { - return mNextActorId++; - } + int generateActorId() { return mNextActorId++; } }; } diff --git a/apps/essimporter/importgame.cpp b/apps/essimporter/importgame.cpp index 1012541b49d..8161a20031a 100644 --- a/apps/essimporter/importgame.cpp +++ b/apps/essimporter/importgame.cpp @@ -1,29 +1,29 @@ #include "importgame.hpp" -#include +#include namespace ESSImport { -void GAME::load(ESM::ESMReader &esm) -{ - esm.getSubNameIs("GMDT"); - esm.getSubHeader(); - if (esm.getSubSize() == 92) - { - esm.getExact(&mGMDT, 92); - mGMDT.mSecundaPhase = 0; - } - else if (esm.getSubSize() == 96) + void GAME::load(ESM::ESMReader& esm) { - esm.getT(mGMDT); - } - else - esm.fail("unexpected subrecord size for GAME.GMDT"); + esm.getSubNameIs("GMDT"); + esm.getSubHeader(); + bool hasSecundaPhase = esm.getSubSize() == 96; + esm.getT(mGMDT.mCellName); + esm.getT(mGMDT.mFogColour); + esm.getT(mGMDT.mFogDensity); + esm.getT(mGMDT.mCurrentWeather); + esm.getT(mGMDT.mNextWeather); + esm.getT(mGMDT.mWeatherTransition); + esm.getT(mGMDT.mTimeOfNextTransition); + esm.getT(mGMDT.mMasserPhase); + if (hasSecundaPhase) + esm.getT(mGMDT.mSecundaPhase); - mGMDT.mWeatherTransition &= (0x000000ff); - mGMDT.mSecundaPhase &= (0x000000ff); - mGMDT.mMasserPhase &= (0x000000ff); -} + mGMDT.mWeatherTransition &= (0x000000ff); + mGMDT.mSecundaPhase &= (0x000000ff); + mGMDT.mMasserPhase &= (0x000000ff); + } } diff --git a/apps/essimporter/importgame.hpp b/apps/essimporter/importgame.hpp index d8051a527a3..276060ae4c1 100644 --- a/apps/essimporter/importgame.hpp +++ b/apps/essimporter/importgame.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_ESSIMPORT_GAME_H #define OPENMW_ESSIMPORT_GAME_H +#include + namespace ESM { class ESMReader; @@ -14,13 +16,13 @@ namespace ESSImport { struct GMDT { - char mCellName[64] {}; - int mFogColour {0}; - float mFogDensity {0.f}; - int mCurrentWeather {0}, mNextWeather {0}; - int mWeatherTransition {0}; // 0-100 transition between weathers, top 3 bytes may be garbage - float mTimeOfNextTransition {0.f}; // weather changes when gamehour == timeOfNextTransition - int mMasserPhase {0}, mSecundaPhase {0}; // top 3 bytes may be garbage + char mCellName[64]{}; + int32_t mFogColour{ 0 }; + float mFogDensity{ 0.f }; + int32_t mCurrentWeather{ 0 }, mNextWeather{ 0 }; + int32_t mWeatherTransition{ 0 }; // 0-100 transition between weathers, top 3 bytes may be garbage + float mTimeOfNextTransition{ 0.f }; // weather changes when gamehour == timeOfNextTransition + int32_t mMasserPhase{ 0 }, mSecundaPhase{ 0 }; // top 3 bytes may be garbage }; GMDT mGMDT; diff --git a/apps/essimporter/importinfo.cpp b/apps/essimporter/importinfo.cpp index 11315537096..66902f6ff43 100644 --- a/apps/essimporter/importinfo.cpp +++ b/apps/essimporter/importinfo.cpp @@ -1,11 +1,11 @@ #include "importinfo.hpp" -#include +#include namespace ESSImport { - void INFO::load(ESM::ESMReader &esm) + void INFO::load(ESM::ESMReader& esm) { mInfo = esm.getHNString("INAM"); mActorRefId = esm.getHNString("ACDT"); diff --git a/apps/essimporter/importinventory.cpp b/apps/essimporter/importinventory.cpp index e91c39452c4..f1db301bd07 100644 --- a/apps/essimporter/importinventory.cpp +++ b/apps/essimporter/importinventory.cpp @@ -2,33 +2,33 @@ #include -#include +#include namespace ESSImport { - void Inventory::load(ESM::ESMReader &esm) + void Inventory::load(ESM::ESMReader& esm) { while (esm.isNextSub("NPCO")) { ContItem contItem; - esm.getHT(contItem); + esm.getHT(contItem.mCount, contItem.mItem.mData); InventoryItem item; item.mId = contItem.mItem.toString(); item.mCount = contItem.mCount; item.mRelativeEquipmentSlot = -1; item.mLockLevel = 0; - item.mRefNum.unset(); + item.mChargeIntRemainder = 0; unsigned int itemCount = std::abs(item.mCount); bool separateStacks = false; - for (unsigned int i=0;i= int(mItems.size())) @@ -68,7 +68,7 @@ namespace ESSImport // appears to be a relative index for only the *possible* slots this item can be equipped in, // i.e. 0 most of the time - int slotIndex; + int32_t slotIndex; esm.getT(slotIndex); mItems[itemIndex].mRelativeEquipmentSlot = slotIndex; diff --git a/apps/essimporter/importinventory.hpp b/apps/essimporter/importinventory.hpp index a1324a69600..7261e64f687 100644 --- a/apps/essimporter/importinventory.hpp +++ b/apps/essimporter/importinventory.hpp @@ -1,11 +1,12 @@ #ifndef OPENMW_ESSIMPORT_IMPORTINVENTORY_H #define OPENMW_ESSIMPORT_IMPORTINVENTORY_H -#include +#include #include +#include -#include #include +#include #include "importscri.hpp" @@ -19,7 +20,7 @@ namespace ESSImport struct ContItem { - int mCount; + int32_t mCount; ESM::NAME32 mItem; }; @@ -28,8 +29,8 @@ namespace ESSImport struct InventoryItem : public ESM::CellRef { std::string mId; - int mCount; - int mRelativeEquipmentSlot; + int32_t mCount; + int32_t mRelativeEquipmentSlot; SCRI mSCRI; }; std::vector mItems; diff --git a/apps/essimporter/importjour.cpp b/apps/essimporter/importjour.cpp index e5d24e113c4..19a2d601c2c 100644 --- a/apps/essimporter/importjour.cpp +++ b/apps/essimporter/importjour.cpp @@ -1,11 +1,11 @@ #include "importjour.hpp" -#include +#include namespace ESSImport { - void JOUR::load(ESM::ESMReader &esm) + void JOUR::load(ESM::ESMReader& esm) { mText = esm.getHNString("NAME"); } diff --git a/apps/essimporter/importklst.cpp b/apps/essimporter/importklst.cpp index daa1ab07745..2d5e09e9137 100644 --- a/apps/essimporter/importklst.cpp +++ b/apps/essimporter/importklst.cpp @@ -1,16 +1,16 @@ #include "importklst.hpp" -#include +#include namespace ESSImport { - void KLST::load(ESM::ESMReader &esm) + void KLST::load(ESM::ESMReader& esm) { while (esm.isNextSub("KNAM")) { std::string refId = esm.getHString(); - int count; + int32_t count; esm.getHNT(count, "CNAM"); mKillCounter[refId] = count; } diff --git a/apps/essimporter/importklst.hpp b/apps/essimporter/importklst.hpp index d07332600f7..9cdb2d701bc 100644 --- a/apps/essimporter/importklst.hpp +++ b/apps/essimporter/importklst.hpp @@ -1,8 +1,9 @@ #ifndef OPENMW_ESSIMPORT_KLST_H #define OPENMW_ESSIMPORT_KLST_H -#include +#include #include +#include namespace ESM { @@ -18,9 +19,9 @@ namespace ESSImport void load(ESM::ESMReader& esm); /// RefId, kill count - std::map mKillCounter; + std::map mKillCounter; - int mWerewolfKills; + int32_t mWerewolfKills; }; } diff --git a/apps/essimporter/importnpcc.cpp b/apps/essimporter/importnpcc.cpp index 3cbd749ce84..c1a53b6cef9 100644 --- a/apps/essimporter/importnpcc.cpp +++ b/apps/essimporter/importnpcc.cpp @@ -1,16 +1,16 @@ #include "importnpcc.hpp" -#include +#include namespace ESSImport { - void NPCC::load(ESM::ESMReader &esm) + void NPCC::load(ESM::ESMReader& esm) { - esm.getHNT(mNPDT, "NPDT"); + esm.getHNT("NPDT", mNPDT.mDisposition, mNPDT.unknown, mNPDT.mReputation, mNPDT.unknown2, mNPDT.mIndex); while (esm.isNextSub("AI_W") || esm.isNextSub("AI_E") || esm.isNextSub("AI_T") || esm.isNextSub("AI_F") - || esm.isNextSub("AI_A")) + || esm.isNextSub("AI_A")) mAiPackages.add(esm); mInventory.load(esm); diff --git a/apps/essimporter/importnpcc.hpp b/apps/essimporter/importnpcc.hpp index a23ab1e50b4..47925226e48 100644 --- a/apps/essimporter/importnpcc.hpp +++ b/apps/essimporter/importnpcc.hpp @@ -1,9 +1,8 @@ #ifndef OPENMW_ESSIMPORT_NPCC_H #define OPENMW_ESSIMPORT_NPCC_H -#include - -#include +#include +#include #include "importinventory.hpp" @@ -23,13 +22,13 @@ namespace ESSImport unsigned char unknown; unsigned char mReputation; unsigned char unknown2; - int mIndex; + int32_t mIndex; } mNPDT; Inventory mInventory; ESM::AIPackageList mAiPackages; - void load(ESM::ESMReader &esm); + void load(ESM::ESMReader& esm); }; } diff --git a/apps/essimporter/importplayer.cpp b/apps/essimporter/importplayer.cpp index 8c275a2868d..f4c280541de 100644 --- a/apps/essimporter/importplayer.cpp +++ b/apps/essimporter/importplayer.cpp @@ -1,22 +1,11 @@ #include "importplayer.hpp" -#include +#include namespace ESSImport { - void REFR::load(ESM::ESMReader &esm) - { - esm.getHNT(mRefNum.mIndex, "FRMR"); - - mRefID = esm.getHNString("NAME"); - - mActorData.load(esm); - - esm.getHNOT(mPos, "DATA", 24); - } - - void PCDT::load(ESM::ESMReader &esm) + void PCDT::load(ESM::ESMReader& esm) { while (esm.isNextSub("DNAM")) { @@ -30,7 +19,12 @@ namespace ESSImport mMNAM = esm.getHString(); } - esm.getHNT(mPNAM, "PNAM"); + esm.getHNT("PNAM", mPNAM.mPlayerFlags, mPNAM.mLevelProgress, mPNAM.mSkillProgress, mPNAM.mSkillIncreases, + mPNAM.mTelekinesisRangeBonus, mPNAM.mVisionBonus, mPNAM.mDetectKeyMagnitude, + mPNAM.mDetectEnchantmentMagnitude, mPNAM.mDetectAnimalMagnitude, mPNAM.mMarkLocation.mX, + mPNAM.mMarkLocation.mY, mPNAM.mMarkLocation.mZ, mPNAM.mMarkLocation.mRotZ, mPNAM.mMarkLocation.mCellX, + mPNAM.mMarkLocation.mCellY, mPNAM.mUnknown3, mPNAM.mVerticalRotation.mData, mPNAM.mSpecIncreases, + mPNAM.mUnknown4); if (esm.isNextSub("SNAM")) esm.skipHSub(); @@ -61,12 +55,7 @@ namespace ESSImport if (esm.isNextSub("NAM3")) esm.skipHSub(); - mHasENAM = false; - if (esm.isNextSub("ENAM")) - { - mHasENAM = true; - esm.getHT(mENAM); - } + mHasENAM = esm.getHNOT("ENAM", mENAM.mCellX, mENAM.mCellY); if (esm.isNextSub("LNAM")) esm.skipHSub(); @@ -74,16 +63,12 @@ namespace ESSImport while (esm.isNextSub("FNAM")) { FNAM fnam; - esm.getHT(fnam); + esm.getHT( + fnam.mRank, fnam.mUnknown1, fnam.mReputation, fnam.mFlags, fnam.mUnknown2, fnam.mFactionName.mData); mFactions.push_back(fnam); } - mHasAADT = false; - if (esm.isNextSub("AADT")) // Attack animation data? - { - mHasAADT = true; - esm.getHT(mAADT); - } + mHasAADT = esm.getHNOT("AADT", mAADT.animGroupIndex, mAADT.mUnknown5); // Attack animation data? if (esm.isNextSub("KNAM")) esm.skipHSub(); // assigned Quick Keys, I think diff --git a/apps/essimporter/importplayer.hpp b/apps/essimporter/importplayer.hpp index 924522383b5..89957bf4b45 100644 --- a/apps/essimporter/importplayer.hpp +++ b/apps/essimporter/importplayer.hpp @@ -1,15 +1,12 @@ #ifndef OPENMW_ESSIMPORT_PLAYER_H #define OPENMW_ESSIMPORT_PLAYER_H -#include +#include #include +#include -#include -#include #include -#include "importacdt.hpp" - namespace ESM { class ESMReader; @@ -18,108 +15,99 @@ namespace ESM namespace ESSImport { -/// Player-agnostic player data -struct REFR -{ - ActorData mActorData; - - std::string mRefID; - ESM::Position mPos; - ESM::RefNum mRefNum; - - void load(ESM::ESMReader& esm); -}; - -/// Other player data -struct PCDT -{ - int mBounty; - std::string mBirthsign; - - std::vector mKnownDialogueTopics; - - enum PlayerFlags + /// Other player data + struct PCDT { - PlayerFlags_ViewSwitchDisabled = 0x1, - PlayerFlags_ControlsDisabled = 0x4, - PlayerFlags_Sleeping = 0x10, - PlayerFlags_Waiting = 0x40, - PlayerFlags_WeaponDrawn = 0x80, - PlayerFlags_SpellDrawn = 0x100, - PlayerFlags_InJail = 0x200, - PlayerFlags_JumpingDisabled = 0x1000, - PlayerFlags_LookingDisabled = 0x2000, - PlayerFlags_VanityModeDisabled = 0x4000, - PlayerFlags_WeaponDrawingDisabled = 0x8000, - PlayerFlags_SpellDrawingDisabled = 0x10000, - PlayerFlags_ThirdPerson = 0x20000, - PlayerFlags_TeleportingDisabled = 0x40000, - PlayerFlags_LevitationDisabled = 0x80000 - }; + int32_t mBounty; + std::string mBirthsign; -#pragma pack(push) -#pragma pack(1) - struct FNAM - { - unsigned char mRank; - unsigned char mUnknown1[3]; - int mReputation; - unsigned char mFlags; // 0x1: unknown, 0x2: expelled - unsigned char mUnknown2[3]; - ESM::NAME32 mFactionName; - }; + std::vector mKnownDialogueTopics; - struct PNAM - { - struct MarkLocation + enum PlayerFlags { - float mX, mY, mZ; // worldspace position - float mRotZ; // Z angle in radians - int mCellX, mCellY; // grid coordinates; for interior cells this is always (0, 0) + PlayerFlags_ViewSwitchDisabled = 0x1, + PlayerFlags_ControlsDisabled = 0x4, + PlayerFlags_Sleeping = 0x10, + PlayerFlags_Waiting = 0x40, + PlayerFlags_WeaponDrawn = 0x80, + PlayerFlags_SpellDrawn = 0x100, + PlayerFlags_InJail = 0x200, + PlayerFlags_JumpingDisabled = 0x1000, + PlayerFlags_LookingDisabled = 0x2000, + PlayerFlags_VanityModeDisabled = 0x4000, + PlayerFlags_WeaponDrawingDisabled = 0x8000, + PlayerFlags_SpellDrawingDisabled = 0x10000, + PlayerFlags_ThirdPerson = 0x20000, + PlayerFlags_TeleportingDisabled = 0x40000, + PlayerFlags_LevitationDisabled = 0x80000 }; - int mPlayerFlags; // controls, camera and draw state - unsigned int mLevelProgress; - float mSkillProgress[27]; // skill progress, non-uniform scaled - unsigned char mSkillIncreases[8]; // number of skill increases for each attribute - int mTelekinesisRangeBonus; // in units; seems redundant - float mVisionBonus; // range: <0.0, 1.0>; affected by light spells and Get/Mod/SetPCVisionBonus - int mDetectKeyMagnitude; // seems redundant - int mDetectEnchantmentMagnitude; // seems redundant - int mDetectAnimalMagnitude; // seems redundant - MarkLocation mMarkLocation; - unsigned char mUnknown3[40]; - unsigned char mSpecIncreases[3]; // number of skill increases for each specialization - unsigned char mUnknown4; - }; + struct FNAM + { + unsigned char mRank; + unsigned char mUnknown1[3]; + int32_t mReputation; + unsigned char mFlags; // 0x1: unknown, 0x2: expelled + unsigned char mUnknown2[3]; + ESM::NAME32 mFactionName; + }; - struct ENAM - { - int mCellX; - int mCellY; - }; + struct PNAM + { + struct MarkLocation + { + float mX, mY, mZ; // worldspace position + float mRotZ; // Z angle in radians + int32_t mCellX, mCellY; // grid coordinates; for interior cells this is always (0, 0) + }; + + struct Rotation + { + float mData[3][3]; + }; + + int32_t mPlayerFlags; // controls, camera and draw state + uint32_t mLevelProgress; + float mSkillProgress[27]; // skill progress, non-uniform scaled + unsigned char mSkillIncreases[8]; // number of skill increases for each attribute + int32_t mTelekinesisRangeBonus; // in units; seems redundant + float mVisionBonus; // range: <0.0, 1.0>; affected by light spells and Get/Mod/SetPCVisionBonus + int32_t mDetectKeyMagnitude; // seems redundant + int32_t mDetectEnchantmentMagnitude; // seems redundant + int32_t mDetectAnimalMagnitude; // seems redundant + MarkLocation mMarkLocation; + unsigned char mUnknown3[4]; + Rotation mVerticalRotation; + unsigned char mSpecIncreases[3]; // number of skill increases for each specialization + unsigned char mUnknown4; + }; - struct AADT // 44 bytes - { - int animGroupIndex; // See convertANIS() for the mapping. - unsigned char mUnknown5[40]; - }; -#pragma pack(pop) + struct ENAM + { + int32_t mCellX; + int32_t mCellY; + }; - std::vector mFactions; - PNAM mPNAM; + struct AADT // 44 bytes + { + int32_t animGroupIndex; // See convertANIS() for the mapping. + unsigned char mUnknown5[40]; + }; + + std::vector mFactions; + PNAM mPNAM; - bool mHasMark; - std::string mMNAM; // mark cell name; can also be sDefaultCellname or region name + bool mHasMark; + std::string mMNAM; // mark cell name; can also be sDefaultCellname or region name - bool mHasENAM; - ENAM mENAM; // last exterior cell + bool mHasENAM; + ENAM mENAM; // last exterior cell - bool mHasAADT; - AADT mAADT; + bool mHasAADT; + AADT mAADT; - void load(ESM::ESMReader& esm); -}; + void load(ESM::ESMReader& esm); + }; } diff --git a/apps/essimporter/importproj.cpp b/apps/essimporter/importproj.cpp index b2dcf4e7da3..a09ade81dde 100644 --- a/apps/essimporter/importproj.cpp +++ b/apps/essimporter/importproj.cpp @@ -1,18 +1,20 @@ #include "importproj.h" -#include +#include namespace ESSImport { -void ESSImport::PROJ::load(ESM::ESMReader& esm) -{ - while (esm.isNextSub("PNAM")) + void ESSImport::PROJ::load(ESM::ESMReader& esm) { - PNAM pnam; - esm.getHT(pnam); - mProjectiles.push_back(pnam); + while (esm.isNextSub("PNAM")) + { + PNAM pnam; + esm.getHT(pnam.mAttackStrength, pnam.mSpeed, pnam.mUnknown, pnam.mFlightTime, pnam.mSplmIndex, + pnam.mUnknown2, pnam.mVelocity.mValues, pnam.mPosition.mValues, pnam.mUnknown3, pnam.mActorId.mData, + pnam.mArrowId.mData, pnam.mBowId.mData); + mProjectiles.push_back(pnam); + } } -} } diff --git a/apps/essimporter/importproj.h b/apps/essimporter/importproj.h index b8abab5fa26..04c7b4003e7 100644 --- a/apps/essimporter/importproj.h +++ b/apps/essimporter/importproj.h @@ -1,9 +1,11 @@ #ifndef OPENMW_ESSIMPORT_IMPORTPROJ_H #define OPENMW_ESSIMPORT_IMPORTPROJ_H -#include #include -#include +#include + +#include +#include namespace ESM { @@ -13,34 +15,31 @@ namespace ESM namespace ESSImport { -struct PROJ -{ - -#pragma pack(push) -#pragma pack(1) - struct PNAM // 184 bytes + struct PROJ { - float mAttackStrength; - float mSpeed; - unsigned char mUnknown[4*2]; - float mFlightTime; - int mSplmIndex; // reference to a SPLM record (0 for ballistic projectiles) - unsigned char mUnknown2[4]; - ESM::Vector3 mVelocity; - ESM::Vector3 mPosition; - unsigned char mUnknown3[4*9]; - ESM::NAME32 mActorId; // indexed refID (with the exception of "PlayerSaveGame") - ESM::NAME32 mArrowId; - ESM::NAME32 mBowId; - - bool isMagic() const { return mSplmIndex != 0; } - }; -#pragma pack(pop) - std::vector mProjectiles; - - void load(ESM::ESMReader& esm); -}; + struct PNAM // 184 bytes + { + float mAttackStrength; + float mSpeed; + unsigned char mUnknown[4 * 2]; + float mFlightTime; + int32_t mSplmIndex; // reference to a SPLM record (0 for ballistic projectiles) + unsigned char mUnknown2[4]; + ESM::Vector3 mVelocity; + ESM::Vector3 mPosition; + unsigned char mUnknown3[4 * 9]; + ESM::NAME32 mActorId; // indexed refID (with the exception of "PlayerSaveGame") + ESM::NAME32 mArrowId; + ESM::NAME32 mBowId; + + bool isMagic() const { return mSplmIndex != 0; } + }; + + std::vector mProjectiles; + + void load(ESM::ESMReader& esm); + }; } diff --git a/apps/essimporter/importques.cpp b/apps/essimporter/importques.cpp index 78b779e4394..46e08cdd9a4 100644 --- a/apps/essimporter/importques.cpp +++ b/apps/essimporter/importques.cpp @@ -1,11 +1,11 @@ #include "importques.hpp" -#include +#include namespace ESSImport { - void QUES::load(ESM::ESMReader &esm) + void QUES::load(ESM::ESMReader& esm) { while (esm.isNextSub("DATA")) mInfo.push_back(esm.getHString()); diff --git a/apps/essimporter/importscpt.cpp b/apps/essimporter/importscpt.cpp index 652383cdaa2..bb62c611033 100644 --- a/apps/essimporter/importscpt.cpp +++ b/apps/essimporter/importscpt.cpp @@ -1,15 +1,14 @@ #include "importscpt.hpp" -#include - - +#include namespace ESSImport { - void SCPT::load(ESM::ESMReader &esm) + void SCPT::load(ESM::ESMReader& esm) { - esm.getHNT(mSCHD, "SCHD"); + esm.getHNT("SCHD", mSCHD.mName.mData, mSCHD.mNumShorts, mSCHD.mNumLongs, mSCHD.mNumFloats, + mSCHD.mScriptDataSize, mSCHD.mStringTableSize); mSCRI.load(esm); diff --git a/apps/essimporter/importscpt.hpp b/apps/essimporter/importscpt.hpp index 6bfd2603a2a..7c728ee97ee 100644 --- a/apps/essimporter/importscpt.hpp +++ b/apps/essimporter/importscpt.hpp @@ -3,7 +3,10 @@ #include "importscri.hpp" -#include +#include + +#include +#include namespace ESM { @@ -15,8 +18,12 @@ namespace ESSImport struct SCHD { - ESM::NAME32 mName; - ESM::Script::SCHDstruct mData; + ESM::NAME32 mName; + std::uint32_t mNumShorts; + std::uint32_t mNumLongs; + std::uint32_t mNumFloats; + std::uint32_t mScriptDataSize; + std::uint32_t mStringTableSize; }; // A running global script @@ -28,7 +35,7 @@ namespace ESSImport SCRI mSCRI; bool mRunning; - int mRefNum; // Targeted reference, -1: no reference + int32_t mRefNum; // Targeted reference, -1: no reference void load(ESM::ESMReader& esm); }; diff --git a/apps/essimporter/importscri.cpp b/apps/essimporter/importscri.cpp index de0b35c86cd..c0425cef322 100644 --- a/apps/essimporter/importscri.cpp +++ b/apps/essimporter/importscri.cpp @@ -1,15 +1,15 @@ #include "importscri.hpp" -#include +#include namespace ESSImport { - void SCRI::load(ESM::ESMReader &esm) + void SCRI::load(ESM::ESMReader& esm) { mScript = esm.getHNOString("SCRI"); - int numShorts = 0, numLongs = 0, numFloats = 0; + int32_t numShorts = 0, numLongs = 0, numFloats = 0; if (esm.isNextSub("SLCS")) { esm.getSubHeader(); @@ -21,9 +21,9 @@ namespace ESSImport if (esm.isNextSub("SLSD")) { esm.getSubHeader(); - for (int i=0; i +#include +#include #include namespace ESM diff --git a/apps/essimporter/importsplm.cpp b/apps/essimporter/importsplm.cpp index 9fdba4ddb51..6019183f833 100644 --- a/apps/essimporter/importsplm.cpp +++ b/apps/essimporter/importsplm.cpp @@ -1,43 +1,45 @@ #include "importsplm.h" -#include +#include namespace ESSImport { -void SPLM::load(ESM::ESMReader& esm) -{ - while (esm.isNextSub("NAME")) + void SPLM::load(ESM::ESMReader& esm) { - ActiveSpell spell; - esm.getHT(spell.mIndex); - esm.getHNT(spell.mSPDT, "SPDT"); - spell.mTarget = esm.getHNOString("TNAM"); - - while (esm.isNextSub("NPDT")) + while (esm.isNextSub("NAME")) { - ActiveEffect effect; - esm.getHT(effect.mNPDT); - - // Effect-specific subrecords can follow: - // - INAM for disintegration and bound effects - // - CNAM for summoning and command effects - // - VNAM for vampirism - // NOTE: There can be multiple INAMs per effect. - // TODO: Needs more research. - - esm.skipHSubUntil("NAM0"); // sentinel - esm.getSubName(); - esm.skipHSub(); - - spell.mActiveEffects.push_back(effect); + ActiveSpell spell; + esm.getHT(spell.mIndex); + esm.getHNT("SPDT", spell.mSPDT.mType, spell.mSPDT.mId.mData, spell.mSPDT.mUnknown, + spell.mSPDT.mCasterId.mData, spell.mSPDT.mSourceId.mData, spell.mSPDT.mUnknown2); + spell.mTarget = esm.getHNOString("TNAM"); + + while (esm.isNextSub("NPDT")) + { + ActiveEffect effect; + esm.getHT(effect.mNPDT.mAffectedActorId.mData, effect.mNPDT.mUnknown, effect.mNPDT.mMagnitude, + effect.mNPDT.mSecondsActive, effect.mNPDT.mUnknown2); + + // Effect-specific subrecords can follow: + // - INAM for disintegration and bound effects + // - CNAM for summoning and command effects + // - VNAM for vampirism + // NOTE: There can be multiple INAMs per effect. + // TODO: Needs more research. + + esm.skipHSubUntil("NAM0"); // sentinel + esm.getSubName(); + esm.skipHSub(); + + spell.mActiveEffects.push_back(effect); + } + + unsigned char xnam; // sentinel + esm.getHNT(xnam, "XNAM"); + + mActiveSpells.push_back(spell); } - - unsigned char xnam; // sentinel - esm.getHNT(xnam, "XNAM"); - - mActiveSpells.push_back(spell); } -} } diff --git a/apps/essimporter/importsplm.h b/apps/essimporter/importsplm.h index 8fd5c2bb528..762e32d9da0 100644 --- a/apps/essimporter/importsplm.h +++ b/apps/essimporter/importsplm.h @@ -1,9 +1,9 @@ #ifndef OPENMW_ESSIMPORT_IMPORTSPLM_H #define OPENMW_ESSIMPORT_IMPORTSPLM_H -#include #include -#include +#include +#include namespace ESM { @@ -13,69 +13,64 @@ namespace ESM namespace ESSImport { -struct SPLM -{ - -#pragma pack(push) -#pragma pack(1) - struct SPDT // 160 bytes + struct SPLM { - int mType; // 1 = spell, 2 = enchantment, 3 = potion - ESM::NAME32 mId; // base ID of a spell/enchantment/potion - unsigned char mUnknown[4*4]; - ESM::NAME32 mCasterId; - ESM::NAME32 mSourceId; // empty for spells - unsigned char mUnknown2[4*11]; - }; - struct NPDT // 56 bytes - { - ESM::NAME32 mAffectedActorId; - unsigned char mUnknown[4*2]; - int mMagnitude; - float mSecondsActive; - unsigned char mUnknown2[4*2]; + struct SPDT // 160 bytes + { + int32_t mType; // 1 = spell, 2 = enchantment, 3 = potion + ESM::NAME32 mId; // base ID of a spell/enchantment/potion + unsigned char mUnknown[4 * 4]; + ESM::NAME32 mCasterId; + ESM::NAME32 mSourceId; // empty for spells + unsigned char mUnknown2[4 * 11]; + }; + + struct NPDT // 56 bytes + { + ESM::NAME32 mAffectedActorId; + unsigned char mUnknown[4 * 2]; + int32_t mMagnitude; + float mSecondsActive; + unsigned char mUnknown2[4 * 2]; + }; + + struct INAM // 40 bytes + { + int32_t mUnknown; + unsigned char mUnknown2; + ESM::FixedString<35> mItemId; // disintegrated item / bound item / item to re-equip after expiration + }; + + struct CNAM // 36 bytes + { + int32_t mUnknown; // seems to always be 0 + ESM::NAME32 mSummonedOrCommandedActor[32]; + }; + + struct VNAM // 4 bytes + { + int32_t mUnknown; + }; + + struct ActiveEffect + { + NPDT mNPDT; + }; + + struct ActiveSpell + { + int32_t mIndex; + SPDT mSPDT; + std::string mTarget; + std::vector mActiveEffects; + }; + + std::vector mActiveSpells; + + void load(ESM::ESMReader& esm); }; - struct INAM // 40 bytes - { - int mUnknown; - unsigned char mUnknown2; - ESM::FIXED_STRING<35> mItemId; // disintegrated item / bound item / item to re-equip after expiration - }; - - struct CNAM // 36 bytes - { - int mUnknown; // seems to always be 0 - ESM::NAME32 mSummonedOrCommandedActor[32]; - }; - - struct VNAM // 4 bytes - { - int mUnknown; - }; - - -#pragma pack(pop) - - struct ActiveEffect - { - NPDT mNPDT; - }; - - struct ActiveSpell - { - int mIndex; - SPDT mSPDT; - std::string mTarget; - std::vector mActiveEffects; - }; - - std::vector mActiveSpells; - - void load(ESM::ESMReader& esm); -}; - } #endif diff --git a/apps/essimporter/main.cpp b/apps/essimporter/main.cpp index 9b969e35af2..f0833e9d81c 100644 --- a/apps/essimporter/main.cpp +++ b/apps/essimporter/main.cpp @@ -1,42 +1,38 @@ +#include #include #include -#include #include #include "importer.hpp" namespace bpo = boost::program_options; -namespace bfs = boost::filesystem; - - int main(int argc, char** argv) { try { - bpo::options_description desc("Syntax: openmw-essimporter infile.ess outfile.omwsave\nAllowed options"); + bpo::options_description desc(R"(Syntax: openmw-essimporter infile.ess outfile.omwsave +Allowed options)"); bpo::positional_options_description p_desc; - desc.add_options() - ("help,h", "produce help message") - ("mwsave,m", bpo::value(), "morrowind .ess save file") - ("output,o", bpo::value(), "output file (.omwsave)") - ("compare,c", "compare two .ess files") - ("encoding", boost::program_options::value()->default_value("win1252"), "encoding of the save file") - ; + auto addOption = desc.add_options(); + addOption("help,h", "produce help message"); + addOption("mwsave,m", bpo::value(), "morrowind .ess save file"); + addOption("output,o", bpo::value(), "output file (.omwsave)"); + addOption("compare,c", "compare two .ess files"); + addOption("encoding", boost::program_options::value()->default_value("win1252"), + "encoding of the save file"); p_desc.add("mwsave", 1).add("output", 1); + Files::ConfigurationManager::addCommonOptions(desc); bpo::variables_map variables; - bpo::parsed_options parsed = bpo::command_line_parser(argc, argv) - .options(desc) - .positional(p_desc) - .run(); - + bpo::parsed_options parsed = bpo::command_line_parser(argc, argv).options(desc).positional(p_desc).run(); bpo::store(parsed, variables); - if(variables.count("help") || !variables.count("mwsave") || !variables.count("output")) { + if (variables.count("help") || !variables.count("mwsave") || !variables.count("output")) + { std::cout << desc; return 0; } @@ -46,8 +42,8 @@ int main(int argc, char** argv) Files::ConfigurationManager cfgManager(true); cfgManager.readConfiguration(variables, desc); - std::string essFile = variables["mwsave"].as(); - std::string outputFile = variables["output"].as(); + const auto& essFile = variables["mwsave"].as(); + const auto& outputFile = variables["output"].as(); std::string encoding = variables["encoding"].as(); ESSImport::Importer importer(essFile, outputFile, encoding); @@ -56,11 +52,13 @@ int main(int argc, char** argv) importer.compare(); else { - const std::string& ext = ".omwsave"; - if (bfs::exists(bfs::path(outputFile)) - && (outputFile.size() < ext.size() || outputFile.substr(outputFile.size()-ext.size()) != ext)) + static constexpr std::u8string_view ext{ u8".omwsave" }; + const auto length = outputFile.native().size(); + if (std::filesystem::exists(outputFile) + && (length < ext.size() || outputFile.u8string().substr(length - ext.size()) != ext)) { - throw std::runtime_error("Output file already exists and does not end in .omwsave. Did you mean to use --compare?"); + throw std::runtime_error( + "Output file already exists and does not end in .omwsave. Did you mean to use --compare?"); } importer.run(); } diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 30182377053..967373de0c6 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -4,10 +4,9 @@ set(LAUNCHER sdlinit.cpp main.cpp maindialog.cpp - playpage.cpp textslotmsgbox.cpp + importpage.cpp settingspage.cpp - advancedpage.cpp utils/cellnameloader.cpp utils/profilescombobox.cpp @@ -23,10 +22,9 @@ set(LAUNCHER_HEADER graphicspage.hpp sdlinit.hpp maindialog.hpp - playpage.hpp textslotmsgbox.hpp + importpage.hpp settingspage.hpp - advancedpage.hpp utils/cellnameloader.hpp utils/profilescombobox.hpp @@ -36,46 +34,30 @@ set(LAUNCHER_HEADER ) # Headers that must be pre-processed -set(LAUNCHER_HEADER_MOC - datafilespage.hpp - graphicspage.hpp - maindialog.hpp - playpage.hpp - textslotmsgbox.hpp - settingspage.hpp - advancedpage.hpp - - utils/cellnameloader.hpp - utils/textinputdialog.hpp - utils/profilescombobox.hpp - utils/lineedit.hpp - utils/openalutil.hpp - -) - set(LAUNCHER_UI - ${CMAKE_SOURCE_DIR}/files/ui/datafilespage.ui - ${CMAKE_SOURCE_DIR}/files/ui/graphicspage.ui - ${CMAKE_SOURCE_DIR}/files/ui/mainwindow.ui - ${CMAKE_SOURCE_DIR}/files/ui/playpage.ui - ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui - ${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui - ${CMAKE_SOURCE_DIR}/files/ui/advancedpage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/datafilespage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/graphicspage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/mainwindow.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/importpage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/settingspage.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/directorypicker.ui ) source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER}) set(QT_USE_QTGUI 1) +set (LAUNCHER_RES ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc) + # Set some platform specific settings if(WIN32) + set(LAUNCHER_RES ${LAUNCHER_RES} ${CMAKE_SOURCE_DIR}/files/windows/QWindowsVistaDark/dark.qrc) set(GUI_TYPE WIN32) set(QT_USE_QTMAIN TRUE) endif(WIN32) -QT5_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc) -QT5_WRAP_CPP(MOC_SRCS ${LAUNCHER_HEADER_MOC}) -QT5_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) +QT_ADD_RESOURCES(RCC_SRCS ${LAUNCHER_RES}) +QT_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) if(NOT WIN32) @@ -92,21 +74,35 @@ openmw_add_executable(openmw-launcher ${UI_HDRS} ) +add_dependencies(openmw-launcher qm-files) + if (WIN32) INSTALL(TARGETS openmw-launcher RUNTIME DESTINATION ".") endif (WIN32) target_link_libraries(openmw-launcher - ${SDL2_LIBRARY_ONLY} + SDL2::SDL2 ${OPENAL_LIBRARY} - components + components_qt ) -target_link_libraries(openmw-launcher Qt5::Widgets Qt5::Core) +target_link_libraries(openmw-launcher Qt::Widgets Qt::Core Qt::Svg) if (BUILD_WITH_CODE_COVERAGE) - add_definitions (--coverage) - target_link_libraries(openmw-launcher gcov) + target_compile_options(openmw-launcher PRIVATE --coverage) + target_link_libraries(openmw-launcher gcov) endif() +if(USE_QT) + set_property(TARGET openmw-launcher PROPERTY AUTOMOC ON) +endif(USE_QT) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw-launcher PRIVATE + + + + + + ) +endif() diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp deleted file mode 100644 index ed36634b62b..00000000000 --- a/apps/launcher/advancedpage.cpp +++ /dev/null @@ -1,428 +0,0 @@ -#include "advancedpage.hpp" - -#include - -#include -#include -#include -#include -#include -#include - -#include - -#include "utils/openalutil.hpp" - -Launcher::AdvancedPage::AdvancedPage(Config::GameSettings &gameSettings, QWidget *parent) - : QWidget(parent) - , mGameSettings(gameSettings) -{ - setObjectName ("AdvancedPage"); - setupUi(this); - - for(const char * name : Launcher::enumerateOpenALDevices()) - { - audioDeviceSelectorComboBox->addItem(QString::fromUtf8(name), QString::fromUtf8(name)); - } - for(const char * name : Launcher::enumerateOpenALDevicesHrtf()) - { - hrtfProfileSelectorComboBox->addItem(QString::fromUtf8(name), QString::fromUtf8(name)); - } - - loadSettings(); - - mCellNameCompleter.setModel(&mCellNameCompleterModel); - startDefaultCharacterAtField->setCompleter(&mCellNameCompleter); -} - -void Launcher::AdvancedPage::loadCellsForAutocomplete(QStringList cellNames) { - // Update the list of suggestions for the "Start default character at" field - mCellNameCompleterModel.setStringList(cellNames); - mCellNameCompleter.setCompletionMode(QCompleter::PopupCompletion); - mCellNameCompleter.setCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); -} - -void Launcher::AdvancedPage::on_skipMenuCheckBox_stateChanged(int state) { - startDefaultCharacterAtLabel->setEnabled(state == Qt::Checked); - startDefaultCharacterAtField->setEnabled(state == Qt::Checked); -} - -void Launcher::AdvancedPage::on_runScriptAfterStartupBrowseButton_clicked() -{ - QString scriptFile = QFileDialog::getOpenFileName( - this, - QObject::tr("Select script file"), - QDir::currentPath(), - QString(tr("Text file (*.txt)"))); - - if (scriptFile.isEmpty()) - return; - - QFileInfo info(scriptFile); - - if (!info.exists() || !info.isReadable()) - return; - - const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); - runScriptAfterStartupField->setText(path); -} - -namespace -{ - constexpr double CellSizeInUnits = 8192; - - double convertToCells(double unitRadius) - { - return std::round((unitRadius + 1024) / CellSizeInUnits); - } - - double convertToUnits(double CellGridRadius) - { - return CellSizeInUnits * CellGridRadius - 1024; - } -} - -bool Launcher::AdvancedPage::loadSettings() -{ - // Game mechanics - { - loadSettingBool(toggleSneakCheckBox, "toggle sneak", "Input"); - loadSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game"); - loadSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game"); - loadSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game"); - loadSettingBool(enchantedWeaponsMagicalCheckBox, "enchanted weapons are magical", "Game"); - loadSettingBool(permanentBarterDispositionChangeCheckBox, "barter disposition change is permanent", "Game"); - loadSettingBool(classicReflectedAbsorbSpellsCheckBox, "classic reflected absorb spells behavior", "Game"); - loadSettingBool(requireAppropriateAmmunitionCheckBox, "only appropriate ammunition bypasses resistance", "Game"); - loadSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game"); - loadSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); - loadSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); - loadSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game"); - int unarmedFactorsStrengthIndex = Settings::Manager::getInt("strength influences hand to hand", "Game"); - if (unarmedFactorsStrengthIndex >= 0 && unarmedFactorsStrengthIndex <= 2) - unarmedFactorsStrengthComboBox->setCurrentIndex(unarmedFactorsStrengthIndex); - loadSettingBool(stealingFromKnockedOutCheckBox, "always allow stealing from knocked out actors", "Game"); - loadSettingBool(enableNavigatorCheckBox, "enable", "Navigator"); - int numPhysicsThreads = Settings::Manager::getInt("async num threads", "Physics"); - if (numPhysicsThreads >= 0) - physicsThreadsSpinBox->setValue(numPhysicsThreads); - loadSettingBool(allowNPCToFollowOverWaterSurfaceCheckBox, "allow actors to follow over water surface", "Game"); - } - - // Visuals - { - loadSettingBool(autoUseObjectNormalMapsCheckBox, "auto use object normal maps", "Shaders"); - loadSettingBool(autoUseObjectSpecularMapsCheckBox, "auto use object specular maps", "Shaders"); - loadSettingBool(autoUseTerrainNormalMapsCheckBox, "auto use terrain normal maps", "Shaders"); - loadSettingBool(autoUseTerrainSpecularMapsCheckBox, "auto use terrain specular maps", "Shaders"); - loadSettingBool(bumpMapLocalLightingCheckBox, "apply lighting to environment maps", "Shaders"); - loadSettingBool(radialFogCheckBox, "radial fog", "Shaders"); - loadSettingBool(magicItemAnimationsCheckBox, "use magic item animations", "Game"); - connect(animSourcesCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotAnimSourcesToggled(bool))); - loadSettingBool(animSourcesCheckBox, "use additional anim sources", "Game"); - if (animSourcesCheckBox->checkState() != Qt::Unchecked) - { - loadSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game"); - loadSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game"); - } - loadSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); - loadSettingBool(smoothMovementCheckBox, "smooth movement", "Game"); - - const bool distantTerrain = Settings::Manager::getBool("distant terrain", "Terrain"); - const bool objectPaging = Settings::Manager::getBool("object paging", "Terrain"); - if (distantTerrain && objectPaging) { - distantLandCheckBox->setCheckState(Qt::Checked); - } - - loadSettingBool(activeGridObjectPagingCheckBox, "object paging active grid", "Terrain"); - viewingDistanceComboBox->setValue(convertToCells(Settings::Manager::getInt("viewing distance", "Camera"))); - } - - // Audio - { - std::string selectedAudioDevice = Settings::Manager::getString("device", "Sound"); - if (selectedAudioDevice.empty() == false) - { - int audioDeviceIndex = audioDeviceSelectorComboBox->findData(QString::fromStdString(selectedAudioDevice)); - if (audioDeviceIndex != -1) - { - audioDeviceSelectorComboBox->setCurrentIndex(audioDeviceIndex); - } - } - int hrtfEnabledIndex = Settings::Manager::getInt("hrtf enable", "Sound"); - if (hrtfEnabledIndex >= -1 && hrtfEnabledIndex <= 1) - { - enableHRTFComboBox->setCurrentIndex(hrtfEnabledIndex + 1); - } - std::string selectedHRTFProfile = Settings::Manager::getString("hrtf", "Sound"); - if (selectedHRTFProfile.empty() == false) - { - int hrtfProfileIndex = hrtfProfileSelectorComboBox->findData(QString::fromStdString(selectedHRTFProfile)); - if (hrtfProfileIndex != -1) - { - hrtfProfileSelectorComboBox->setCurrentIndex(hrtfProfileIndex); - } - } - } - - - // Camera - { - loadSettingBool(viewOverShoulderCheckBox, "view over shoulder", "Camera"); - connect(viewOverShoulderCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotViewOverShoulderToggled(bool))); - viewOverShoulderVerticalLayout->setEnabled(viewOverShoulderCheckBox->checkState()); - loadSettingBool(autoSwitchShoulderCheckBox, "auto switch shoulder", "Camera"); - loadSettingBool(previewIfStandStillCheckBox, "preview if stand still", "Camera"); - loadSettingBool(deferredPreviewRotationCheckBox, "deferred preview rotation", "Camera"); - loadSettingBool(headBobbingCheckBox, "head bobbing", "Camera"); - defaultShoulderComboBox->setCurrentIndex( - Settings::Manager::getVector2("view over shoulder offset", "Camera").x() >= 0 ? 0 : 1); - } - - // Interface Changes - { - loadSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); - loadSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); - loadSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); - loadSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); - loadSettingBool(changeDialogTopicsCheckBox, "color topic enable", "GUI"); - int showOwnedIndex = Settings::Manager::getInt("show owned", "Game"); - // Match the index with the option (only 0, 1, 2, or 3 are valid). Will default to 0 if invalid. - if (showOwnedIndex >= 0 && showOwnedIndex <= 3) - showOwnedComboBox->setCurrentIndex(showOwnedIndex); - loadSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); - loadSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); - scalingSpinBox->setValue(Settings::Manager::getFloat("scaling factor", "GUI")); - } - - // Bug fixes - { - loadSettingBool(preventMerchantEquippingCheckBox, "prevent merchant equipping", "Game"); - loadSettingBool(trainersTrainingSkillsBasedOnBaseSkillCheckBox, "trainers training skills based on base skill", "Game"); - } - - // Miscellaneous - { - // Saves - loadSettingBool(timePlayedCheckbox, "timeplayed", "Saves"); - maximumQuicksavesComboBox->setValue(Settings::Manager::getInt("max quicksaves", "Saves")); - - // Other Settings - QString screenshotFormatString = QString::fromStdString(Settings::Manager::getString("screenshot format", "General")).toUpper(); - if (screenshotFormatComboBox->findText(screenshotFormatString) == -1) - screenshotFormatComboBox->addItem(screenshotFormatString); - screenshotFormatComboBox->setCurrentIndex(screenshotFormatComboBox->findText(screenshotFormatString)); - } - - // Testing - { - loadSettingBool(grabCursorCheckBox, "grab cursor", "Input"); - - bool skipMenu = mGameSettings.value("skip-menu").toInt() == 1; - if (skipMenu) - { - skipMenuCheckBox->setCheckState(Qt::Checked); - } - startDefaultCharacterAtLabel->setEnabled(skipMenu); - startDefaultCharacterAtField->setEnabled(skipMenu); - - startDefaultCharacterAtField->setText(mGameSettings.value("start")); - runScriptAfterStartupField->setText(mGameSettings.value("script-run")); - } - return true; -} - -void Launcher::AdvancedPage::saveSettings() -{ - // Game mechanics - { - saveSettingBool(toggleSneakCheckBox, "toggle sneak", "Input"); - saveSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game"); - saveSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game"); - saveSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game"); - saveSettingBool(enchantedWeaponsMagicalCheckBox, "enchanted weapons are magical", "Game"); - saveSettingBool(permanentBarterDispositionChangeCheckBox, "barter disposition change is permanent", "Game"); - saveSettingBool(classicReflectedAbsorbSpellsCheckBox, "classic reflected absorb spells behavior", "Game"); - saveSettingBool(requireAppropriateAmmunitionCheckBox, "only appropriate ammunition bypasses resistance", "Game"); - saveSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game"); - saveSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); - saveSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); - saveSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game"); - int unarmedFactorsStrengthIndex = unarmedFactorsStrengthComboBox->currentIndex(); - if (unarmedFactorsStrengthIndex != Settings::Manager::getInt("strength influences hand to hand", "Game")) - Settings::Manager::setInt("strength influences hand to hand", "Game", unarmedFactorsStrengthIndex); - saveSettingBool(stealingFromKnockedOutCheckBox, "always allow stealing from knocked out actors", "Game"); - saveSettingBool(enableNavigatorCheckBox, "enable", "Navigator"); - int numPhysicsThreads = physicsThreadsSpinBox->value(); - if (numPhysicsThreads != Settings::Manager::getInt("async num threads", "Physics")) - Settings::Manager::setInt("async num threads", "Physics", numPhysicsThreads); - } - - // Visuals - { - saveSettingBool(autoUseObjectNormalMapsCheckBox, "auto use object normal maps", "Shaders"); - saveSettingBool(autoUseObjectSpecularMapsCheckBox, "auto use object specular maps", "Shaders"); - saveSettingBool(autoUseTerrainNormalMapsCheckBox, "auto use terrain normal maps", "Shaders"); - saveSettingBool(autoUseTerrainSpecularMapsCheckBox, "auto use terrain specular maps", "Shaders"); - saveSettingBool(bumpMapLocalLightingCheckBox, "apply lighting to environment maps", "Shaders"); - saveSettingBool(radialFogCheckBox, "radial fog", "Shaders"); - saveSettingBool(magicItemAnimationsCheckBox, "use magic item animations", "Game"); - saveSettingBool(animSourcesCheckBox, "use additional anim sources", "Game"); - saveSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game"); - saveSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game"); - saveSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); - saveSettingBool(smoothMovementCheckBox, "smooth movement", "Game"); - - const bool distantTerrain = Settings::Manager::getBool("distant terrain", "Terrain"); - const bool objectPaging = Settings::Manager::getBool("object paging", "Terrain"); - const bool wantDistantLand = distantLandCheckBox->checkState(); - if (wantDistantLand != (distantTerrain && objectPaging)) { - Settings::Manager::setBool("distant terrain", "Terrain", wantDistantLand); - Settings::Manager::setBool("object paging", "Terrain", wantDistantLand); - } - - saveSettingBool(activeGridObjectPagingCheckBox, "object paging active grid", "Terrain"); - double viewingDistance = viewingDistanceComboBox->value(); - if (viewingDistance != convertToCells(Settings::Manager::getInt("viewing distance", "Camera"))) - { - Settings::Manager::setInt("viewing distance", "Camera", convertToUnits(viewingDistance)); - } - } - - // Audio - { - int audioDeviceIndex = audioDeviceSelectorComboBox->currentIndex(); - if (audioDeviceIndex != 0) - { - Settings::Manager::setString("device", "Sound", audioDeviceSelectorComboBox->currentText().toUtf8().constData()); - } - else - { - Settings::Manager::setString("device", "Sound", ""); - } - int hrtfEnabledIndex = enableHRTFComboBox->currentIndex() - 1; - if (hrtfEnabledIndex != Settings::Manager::getInt("hrtf enable", "Sound")) - { - Settings::Manager::setInt("hrtf enable", "Sound", hrtfEnabledIndex); - } - int selectedHRTFProfileIndex = hrtfProfileSelectorComboBox->currentIndex(); - if (selectedHRTFProfileIndex != 0) - { - Settings::Manager::setString("hrtf", "Sound", hrtfProfileSelectorComboBox->currentText().toUtf8().constData()); - } - else - { - Settings::Manager::setString("hrtf", "Sound", ""); - } - } - - // Camera - { - saveSettingBool(viewOverShoulderCheckBox, "view over shoulder", "Camera"); - saveSettingBool(autoSwitchShoulderCheckBox, "auto switch shoulder", "Camera"); - saveSettingBool(previewIfStandStillCheckBox, "preview if stand still", "Camera"); - saveSettingBool(deferredPreviewRotationCheckBox, "deferred preview rotation", "Camera"); - saveSettingBool(headBobbingCheckBox, "head bobbing", "Camera"); - - osg::Vec2f shoulderOffset = Settings::Manager::getVector2("view over shoulder offset", "Camera"); - if (defaultShoulderComboBox->currentIndex() != (shoulderOffset.x() >= 0 ? 0 : 1)) - { - if (defaultShoulderComboBox->currentIndex() == 0) - shoulderOffset.x() = std::abs(shoulderOffset.x()); - else - shoulderOffset.x() = -std::abs(shoulderOffset.x()); - Settings::Manager::setVector2("view over shoulder offset", "Camera", shoulderOffset); - } - } - - // Interface Changes - { - saveSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); - saveSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); - saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); - saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); - saveSettingBool(changeDialogTopicsCheckBox, "color topic enable", "GUI"); - int showOwnedCurrentIndex = showOwnedComboBox->currentIndex(); - if (showOwnedCurrentIndex != Settings::Manager::getInt("show owned", "Game")) - Settings::Manager::setInt("show owned", "Game", showOwnedCurrentIndex); - saveSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); - saveSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); - float uiScalingFactor = scalingSpinBox->value(); - if (uiScalingFactor != Settings::Manager::getFloat("scaling factor", "GUI")) - Settings::Manager::setFloat("scaling factor", "GUI", uiScalingFactor); - } - - // Bug fixes - { - saveSettingBool(preventMerchantEquippingCheckBox, "prevent merchant equipping", "Game"); - saveSettingBool(trainersTrainingSkillsBasedOnBaseSkillCheckBox, "trainers training skills based on base skill", "Game"); - } - - // Miscellaneous - { - // Saves Settings - saveSettingBool(timePlayedCheckbox, "timeplayed", "Saves"); - int maximumQuicksaves = maximumQuicksavesComboBox->value(); - if (maximumQuicksaves != Settings::Manager::getInt("max quicksaves", "Saves")) - { - Settings::Manager::setInt("max quicksaves", "Saves", maximumQuicksaves); - } - - // Other Settings - std::string screenshotFormatString = screenshotFormatComboBox->currentText().toLower().toStdString(); - if (screenshotFormatString != Settings::Manager::getString("screenshot format", "General")) - Settings::Manager::setString("screenshot format", "General", screenshotFormatString); - } - - // Testing - { - saveSettingBool(grabCursorCheckBox, "grab cursor", "Input"); - - int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked; - if (skipMenu != mGameSettings.value("skip-menu").toInt()) - mGameSettings.setValue("skip-menu", QString::number(skipMenu)); - - QString startCell = startDefaultCharacterAtField->text(); - if (startCell != mGameSettings.value("start")) - { - mGameSettings.setValue("start", startCell); - } - QString scriptRun = runScriptAfterStartupField->text(); - if (scriptRun != mGameSettings.value("script-run")) - mGameSettings.setValue("script-run", scriptRun); - } -} - -void Launcher::AdvancedPage::loadSettingBool(QCheckBox *checkbox, const std::string &setting, const std::string &group) -{ - if (Settings::Manager::getBool(setting, group)) - checkbox->setCheckState(Qt::Checked); -} - -void Launcher::AdvancedPage::saveSettingBool(QCheckBox *checkbox, const std::string &setting, const std::string &group) -{ - bool cValue = checkbox->checkState(); - if (cValue != Settings::Manager::getBool(setting, group)) - Settings::Manager::setBool(setting, group, cValue); -} - -void Launcher::AdvancedPage::slotLoadedCellsChanged(QStringList cellNames) -{ - loadCellsForAutocomplete(cellNames); -} - -void Launcher::AdvancedPage::slotAnimSourcesToggled(bool checked) -{ - weaponSheathingCheckBox->setEnabled(checked); - shieldSheathingCheckBox->setEnabled(checked); - if (!checked) - { - weaponSheathingCheckBox->setCheckState(Qt::Unchecked); - shieldSheathingCheckBox->setCheckState(Qt::Unchecked); - } -} - -void Launcher::AdvancedPage::slotViewOverShoulderToggled(bool checked) -{ - viewOverShoulderVerticalLayout->setEnabled(viewOverShoulderCheckBox->checkState()); -} diff --git a/apps/launcher/advancedpage.hpp b/apps/launcher/advancedpage.hpp deleted file mode 100644 index 9685dcefebf..00000000000 --- a/apps/launcher/advancedpage.hpp +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef ADVANCEDPAGE_H -#define ADVANCEDPAGE_H - -#include -#include - -#include "ui_advancedpage.h" - -#include - -namespace Config { class GameSettings; } - -namespace Launcher -{ - class AdvancedPage : public QWidget, private Ui::AdvancedPage - { - Q_OBJECT - - public: - explicit AdvancedPage(Config::GameSettings &gameSettings, QWidget *parent = nullptr); - - bool loadSettings(); - void saveSettings(); - - public slots: - void slotLoadedCellsChanged(QStringList cellNames); - - private slots: - void on_skipMenuCheckBox_stateChanged(int state); - void on_runScriptAfterStartupBrowseButton_clicked(); - void slotAnimSourcesToggled(bool checked); - void slotViewOverShoulderToggled(bool checked); - - private: - Config::GameSettings &mGameSettings; - QCompleter mCellNameCompleter; - QStringListModel mCellNameCompleterModel; - - /** - * Load the cells associated with the given content files for use in autocomplete - * @param filePaths the file paths of the content files to be examined - */ - void loadCellsForAutocomplete(QStringList filePaths); - void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); - void saveSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); - }; -} -#endif diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 956483a3f20..356a15de81f 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -1,15 +1,20 @@ #include "datafilespage.hpp" +#include "maindialog.hpp" #include - -#include +#include +#include #include -#include -#include -#include +#include +#include + +#include #include +#include +#include #include + #include #include @@ -17,182 +22,505 @@ #include #include -#include -#include "utils/textinputdialog.hpp" +#include +#include +#include +#include +#include +#include +#include + #include "utils/profilescombobox.hpp" +#include "utils/textinputdialog.hpp" + +#include "ui_directorypicker.h" + +const char* Launcher::DataFilesPage::mDefaultContentListName = "Default"; + +namespace +{ + void contentSubdirs(const QString& path, QStringList& dirs) + { + static const QStringList fileFilter{ + "*.esm", + "*.esp", + "*.bsa", + "*.ba2", + "*.omwgame", + "*.omwaddon", + "*.omwscripts", + }; + + static const QStringList dirFilter{ + "animations", + "bookart", + "fonts", + "icons", + "interface", + "l10n", + "meshes", + "music", + "mygui", + "scripts", + "shaders", + "sound", + "splash", + "strings", + "textures", + "trees", + "video", + }; + + QDir currentDir(path); + if (!currentDir.entryInfoList(fileFilter, QDir::Files).empty() + || !currentDir.entryInfoList(dirFilter, QDir::Dirs | QDir::NoDotAndDotDot).empty()) + { + dirs.push_back(currentDir.canonicalPath()); + return; + } + + for (const auto& subdir : currentDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) + contentSubdirs(subdir.canonicalFilePath(), dirs); + } + QList> sortedSelectedItems(QListWidget* list, bool reverse = false) + { + QList> sortedItems; + for (QListWidgetItem* item : list->selectedItems()) + sortedItems.append(qMakePair(list->row(item), item)); + + if (reverse) + std::sort(sortedItems.begin(), sortedItems.end(), [](auto a, auto b) { return a.first > b.first; }); + else + std::sort(sortedItems.begin(), sortedItems.end(), [](auto a, auto b) { return a.first < b.first; }); -const char *Launcher::DataFilesPage::mDefaultContentListName = "Default"; + return sortedItems; + } +} -Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, QWidget *parent) +namespace Launcher +{ + namespace + { + struct HandleNavMeshToolMessage + { + int mCellsCount; + int mExpectedMaxProgress; + int mMaxProgress; + int mProgress; + + HandleNavMeshToolMessage operator()(NavMeshTool::ExpectedCells&& message) const + { + return HandleNavMeshToolMessage{ static_cast(message.mCount), mExpectedMaxProgress, + static_cast(message.mCount) * 100, mProgress }; + } + + HandleNavMeshToolMessage operator()(NavMeshTool::ProcessedCells&& message) const + { + return HandleNavMeshToolMessage{ mCellsCount, mExpectedMaxProgress, mMaxProgress, + std::max(mProgress, static_cast(message.mCount)) }; + } + + HandleNavMeshToolMessage operator()(NavMeshTool::ExpectedTiles&& message) const + { + const int expectedMaxProgress = mCellsCount + static_cast(message.mCount); + return HandleNavMeshToolMessage{ mCellsCount, expectedMaxProgress, + std::max(mMaxProgress, expectedMaxProgress), mProgress }; + } + + HandleNavMeshToolMessage operator()(NavMeshTool::GeneratedTiles&& message) const + { + int progress = mCellsCount + static_cast(message.mCount); + if (mExpectedMaxProgress < mMaxProgress) + progress += static_cast(std::round((mMaxProgress - mExpectedMaxProgress) + * (static_cast(progress) / static_cast(mExpectedMaxProgress)))); + return HandleNavMeshToolMessage{ mCellsCount, mExpectedMaxProgress, mMaxProgress, + std::max(mProgress, progress) }; + } + }; + + int getMaxNavMeshDbFileSizeMiB() + { + return Settings::navigator().mMaxNavmeshdbFileSize / (1024 * 1024); + } + } +} + +Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, Config::GameSettings& gameSettings, + Config::LauncherSettings& launcherSettings, MainDialog* parent) : QWidget(parent) + , mMainDialog(parent) , mCfgMgr(cfg) , mGameSettings(gameSettings) , mLauncherSettings(launcherSettings) + , mNavMeshToolInvoker(new Process::ProcessInvoker(this)) { - ui.setupUi (this); - setObjectName ("DataFilesPage"); - mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); - const QString encoding = mGameSettings.value("encoding", "win1252"); + ui.setupUi(this); + setObjectName("DataFilesPage"); + mSelector = new ContentSelectorView::ContentSelector(ui.contentSelectorWidget, /*showOMWScripts=*/true); + const QString encoding = mGameSettings.value("encoding", { "win1252" }).value; mSelector->setEncoding(encoding); + QVector> languages = { { "English", tr("English") }, { "French", tr("French") }, + { "German", tr("German") }, { "Italian", tr("Italian") }, { "Polish", tr("Polish") }, + { "Russian", tr("Russian") }, { "Spanish", tr("Spanish") } }; + + for (auto lang : languages) + { + mSelector->languageBox()->addItem(lang.second, lang.first); + } + mNewProfileDialog = new TextInputDialog(tr("New Content List"), tr("Content List name:"), this); mCloneProfileDialog = new TextInputDialog(tr("Clone Content List"), tr("Content List name:"), this); - connect(mNewProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), - this, SLOT(updateNewProfileOkButton(QString))); - connect(mCloneProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), - this, SLOT(updateCloneProfileOkButton(QString))); + connect(mNewProfileDialog->lineEdit(), &LineEdit::textChanged, this, &DataFilesPage::updateNewProfileOkButton); + connect(mCloneProfileDialog->lineEdit(), &LineEdit::textChanged, this, &DataFilesPage::updateCloneProfileOkButton); + connect(ui.directoryAddSubdirsButton, &QPushButton::released, this, [this]() { this->addSubdirectories(true); }); + connect(ui.directoryInsertButton, &QPushButton::released, this, [this]() { this->addSubdirectories(false); }); + connect(ui.directoryUpButton, &QPushButton::released, this, + [this]() { this->moveSources(ui.directoryListWidget, -1); }); + connect(ui.directoryDownButton, &QPushButton::released, this, + [this]() { this->moveSources(ui.directoryListWidget, 1); }); + connect(ui.directoryRemoveButton, &QPushButton::released, this, &DataFilesPage::removeDirectory); + connect( + ui.archiveUpButton, &QPushButton::released, this, [this]() { this->moveSources(ui.archiveListWidget, -1); }); + connect( + ui.archiveDownButton, &QPushButton::released, this, [this]() { this->moveSources(ui.archiveListWidget, 1); }); + connect(ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortDirectories); + connect(ui.archiveListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortArchives); buildView(); loadSettings(); // Connect signal and slot after the settings have been loaded. We only care about the user changing // the addons and don't want to get signals of the system doing it during startup. - connect(mSelector, SIGNAL(signalAddonDataChanged(QModelIndex,QModelIndex)), - this, SLOT(slotAddonDataChanged())); + connect(mSelector, &ContentSelectorView::ContentSelector::signalAddonDataChanged, this, + &DataFilesPage::slotAddonDataChanged); // Call manually to indicate all changes to addon data during startup. slotAddonDataChanged(); } void Launcher::DataFilesPage::buildView() { - ui.verticalLayout->insertWidget (0, mSelector->uiWidget()); - - QToolButton * refreshButton = mSelector->refreshButton(); + QToolButton* refreshButton = mSelector->refreshButton(); - //tool buttons - ui.newProfileButton->setToolTip ("Create a new Content List"); - ui.cloneProfileButton->setToolTip ("Clone the current Content List"); - ui.deleteProfileButton->setToolTip ("Delete an existing Content List"); - refreshButton->setToolTip("Refresh Data Files"); + // tool buttons + ui.newProfileButton->setToolTip("Create a new Content List"); + ui.cloneProfileButton->setToolTip("Clone the current Content List"); + ui.deleteProfileButton->setToolTip("Delete an existing Content List"); - //combo box + // combo box ui.profilesComboBox->addItem(mDefaultContentListName); - ui.profilesComboBox->setPlaceholderText (QString("Select a Content List...")); + ui.profilesComboBox->setPlaceholderText(QString("Select a Content List...")); ui.profilesComboBox->setCurrentIndex(ui.profilesComboBox->findText(QLatin1String(mDefaultContentListName))); // Add the actions to the toolbuttons - ui.newProfileButton->setDefaultAction (ui.newProfileAction); - ui.cloneProfileButton->setDefaultAction (ui.cloneProfileAction); - ui.deleteProfileButton->setDefaultAction (ui.deleteProfileAction); + ui.newProfileButton->setDefaultAction(ui.newProfileAction); + ui.cloneProfileButton->setDefaultAction(ui.cloneProfileAction); + ui.deleteProfileButton->setDefaultAction(ui.deleteProfileAction); refreshButton->setDefaultAction(ui.refreshDataFilesAction); - //establish connections - connect (ui.profilesComboBox, SIGNAL (currentIndexChanged(int)), - this, SLOT (slotProfileChanged(int))); + // establish connections + connect(ui.profilesComboBox, qOverload(&::ProfilesComboBox::currentIndexChanged), this, + &DataFilesPage::slotProfileChanged); + + connect(ui.profilesComboBox, &::ProfilesComboBox::profileRenamed, this, &DataFilesPage::slotProfileRenamed); + + connect(ui.profilesComboBox, qOverload(&::ProfilesComboBox::signalProfileChanged), + this, &DataFilesPage::slotProfileChangedByUser); - connect (ui.profilesComboBox, SIGNAL (profileRenamed(QString, QString)), - this, SLOT (slotProfileRenamed(QString, QString))); + connect(ui.refreshDataFilesAction, &QAction::triggered, this, &DataFilesPage::slotRefreshButtonClicked); - connect (ui.profilesComboBox, SIGNAL (signalProfileChanged(QString, QString)), - this, SLOT (slotProfileChangedByUser(QString, QString))); + connect(ui.updateNavMeshButton, &QPushButton::clicked, this, &DataFilesPage::startNavMeshTool); + connect(ui.cancelNavMeshButton, &QPushButton::clicked, this, &DataFilesPage::killNavMeshTool); - connect(ui.refreshDataFilesAction, SIGNAL(triggered()),this, SLOT(slotRefreshButtonClicked())); + connect(mNavMeshToolInvoker->getProcess(), &QProcess::readyReadStandardOutput, this, + &DataFilesPage::readNavMeshToolStdout); + connect(mNavMeshToolInvoker->getProcess(), &QProcess::readyReadStandardError, this, + &DataFilesPage::readNavMeshToolStderr); + connect(mNavMeshToolInvoker->getProcess(), qOverload(&QProcess::finished), this, + &DataFilesPage::navMeshToolFinished); + + buildArchiveContextMenu(); +} + +void Launcher::DataFilesPage::buildArchiveContextMenu() +{ + connect(ui.archiveListWidget, &QListWidget::customContextMenuRequested, this, + &DataFilesPage::slotShowArchiveContextMenu); + + mArchiveContextMenu = new QMenu(ui.archiveListWidget); + mArchiveContextMenu->addAction(tr("&Check Selected"), this, SLOT(slotCheckMultiSelectedItems())); + mArchiveContextMenu->addAction(tr("&Uncheck Selected"), this, SLOT(slotUncheckMultiSelectedItems())); } bool Launcher::DataFilesPage::loadSettings() { + ui.navMeshMaxSizeSpinBox->setValue(getMaxNavMeshDbFileSizeMiB()); + QStringList profiles = mLauncherSettings.getContentLists(); QString currentProfile = mLauncherSettings.getCurrentContentListName(); qDebug() << "The current profile is: " << currentProfile; - for (const QString &item : profiles) - addProfile (item, false); + for (const QString& item : profiles) + addProfile(item, false); // Hack: also add the current profile if (!currentProfile.isEmpty()) addProfile(currentProfile, true); + auto language = mLauncherSettings.getLanguage(); + + for (int i = 0; i < mSelector->languageBox()->count(); ++i) + { + QString languageItem = mSelector->languageBox()->itemData(i).toString(); + if (language == languageItem) + { + mSelector->languageBox()->setCurrentIndex(i); + break; + } + } + return true; } void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) { - QStringList paths = mGameSettings.getDataDirs(); + mSelector->clearFiles(); + ui.archiveListWidget->clear(); + ui.directoryListWidget->clear(); - mDataLocal = mGameSettings.getDataLocal(); + QList directories = mGameSettings.getDataDirs(); + QStringList contentModelDirectories = mLauncherSettings.getDataDirectoryList(contentModelName); + if (!contentModelDirectories.isEmpty()) + { + directories.erase(std::remove_if(directories.begin(), directories.end(), + [&](const Config::SettingValue& dir) { return mGameSettings.isUserSetting(dir); }), + directories.end()); + for (const auto& dir : contentModelDirectories) + directories.push_back({ dir }); + } + mDataLocal = mGameSettings.getDataLocal(); if (!mDataLocal.isEmpty()) - paths.insert(0, mDataLocal); + directories.insert(0, { mDataLocal }); - mSelector->clearFiles(); + const auto& resourcesVfs = mGameSettings.getResourcesVfs(); + if (!resourcesVfs.isEmpty()) + directories.insert(0, { resourcesVfs }); - for (const QString &path : paths) - mSelector->addFiles(path); + std::unordered_set visitedDirectories; + for (const Config::SettingValue& currentDir : directories) + { + if (!visitedDirectories.insert(currentDir.value).second) + continue; - PathIterator pathIterator(paths); + // add new achives files presents in current directory + addArchivesFromDir(currentDir.value); - mSelector->setProfileContent(filesInProfile(contentModelName, pathIterator)); -} + QStringList tooltip; -QStringList Launcher::DataFilesPage::filesInProfile(const QString& profileName, PathIterator& pathIterator) -{ - QStringList files = mLauncherSettings.getContentListFiles(profileName); - QStringList filepaths; + // add content files presents in current directory + mSelector->addFiles(currentDir.value, mNewDataDirs.contains(currentDir.value)); + + // add current directory to list + ui.directoryListWidget->addItem(currentDir.originalRepresentation); + auto row = ui.directoryListWidget->count() - 1; + auto* item = ui.directoryListWidget->item(row); + item->setData(Qt::UserRole, QVariant::fromValue(currentDir)); + + if (currentDir.value != currentDir.originalRepresentation) + tooltip << tr("Resolved as %1").arg(currentDir.value); - for (const QString& file : files) + // Display new content with custom formatting + if (mNewDataDirs.contains(currentDir.value)) + { + tooltip << tr("Will be added to the current profile"); + QFont font = item->font(); + font.setBold(true); + font.setItalic(true); + item->setFont(font); + } + + // deactivate data-local and resources/vfs: they are always included + // same for ones from non-user config files + if (currentDir.value == mDataLocal || currentDir.value == resourcesVfs + || !mGameSettings.isUserSetting(currentDir)) + { + auto flags = item->flags(); + item->setFlags(flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled)); + if (currentDir.value == mDataLocal) + tooltip << tr("This is the data-local directory and cannot be disabled"); + else if (currentDir.value == resourcesVfs) + tooltip << tr("This directory is part of OpenMW and cannot be disabled"); + else + tooltip << tr("This directory is enabled in an openmw.cfg other than the user one"); + } + + // Add a "data file" icon if the directory contains a content file + if (mSelector->containsDataFiles(currentDir.value)) + { + item->setIcon(QIcon(":/images/openmw-plugin.png")); + + tooltip << tr("Contains content file(s)"); + } + else + { + // Pad to correct vertical alignment + QPixmap pixmap(QSize(200, 200)); // Arbitrary big number, will be scaled down to widget size + pixmap.fill(QColor(0, 0, 0, 0)); + auto emptyIcon = QIcon(pixmap); + item->setIcon(emptyIcon); + } + item->setToolTip(tooltip.join('\n')); + } + mSelector->sortFiles(); + + QList selectedArchives = mGameSettings.getArchiveList(); + QStringList contentModelSelectedArchives = mLauncherSettings.getArchiveList(contentModelName); + if (!contentModelSelectedArchives.isEmpty()) { - QString filepath = pathIterator.findFirstPath(file); + selectedArchives.erase(std::remove_if(selectedArchives.begin(), selectedArchives.end(), + [&](const Config::SettingValue& dir) { return mGameSettings.isUserSetting(dir); }), + selectedArchives.end()); + for (const auto& dir : contentModelSelectedArchives) + selectedArchives.push_back({ dir }); + } - if (!filepath.isEmpty()) - filepaths << filepath; + // sort and tick BSA according to profile + int row = 0; + for (const auto& archive : selectedArchives) + { + const auto match = ui.archiveListWidget->findItems(archive.value, Qt::MatchExactly); + if (match.isEmpty()) + continue; + const auto name = match[0]->text(); + const auto oldrow = ui.archiveListWidget->row(match[0]); + ui.archiveListWidget->takeItem(oldrow); + ui.archiveListWidget->insertItem(row, name); + ui.archiveListWidget->item(row)->setCheckState(Qt::Checked); + ui.archiveListWidget->item(row)->setData(Qt::UserRole, QVariant::fromValue(archive)); + if (!mGameSettings.isUserSetting(archive)) + { + auto flags = ui.archiveListWidget->item(row)->flags(); + ui.archiveListWidget->item(row)->setFlags( + flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled)); + ui.archiveListWidget->item(row)->setToolTip( + tr("This archive is enabled in an openmw.cfg other than the user one")); + } + row++; } - return filepaths; + QStringList nonUserContent; + for (const auto& content : mGameSettings.getContentList()) + { + if (!mGameSettings.isUserSetting(content)) + nonUserContent.push_back(content.value); + } + mSelector->setNonUserContent(nonUserContent); + mSelector->setProfileContent(mLauncherSettings.getContentListFiles(contentModelName)); } -void Launcher::DataFilesPage::saveSettings(const QString &profile) +void Launcher::DataFilesPage::saveSettings(const QString& profile) { - QString profileName = profile; + Settings::navigator().mMaxNavmeshdbFileSize.set( + static_cast(std::max(0, ui.navMeshMaxSizeSpinBox->value())) * 1024 * 1024); - if (profileName.isEmpty()) - profileName = ui.profilesComboBox->currentText(); + QString profileName = profile; - //retrieve the files selected for the profile - ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); + if (profileName.isEmpty()) + profileName = ui.profilesComboBox->currentText(); - //set the value of the current profile (not necessarily the profile being saved!) + // retrieve the data paths + auto dirList = selectedDirectoriesPaths(); + + // retrieve the files selected for the profile + ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); + + // set the value of the current profile (not necessarily the profile being saved!) mLauncherSettings.setCurrentContentListName(ui.profilesComboBox->currentText()); QStringList fileNames; - for (const ContentSelectorModel::EsmFile *item : items) + for (const ContentSelectorModel::EsmFile* item : items) { fileNames.append(item->fileName()); } - mLauncherSettings.setContentList(profileName, fileNames); - mGameSettings.setContentList(fileNames); + QStringList dirNames; + for (const auto& dir : dirList) + { + if (mGameSettings.isUserSetting(dir)) + dirNames.push_back(dir.originalRepresentation); + } + QStringList archiveNames; + for (const auto& archive : selectedArchivePaths()) + { + if (mGameSettings.isUserSetting(archive)) + archiveNames.push_back(archive.originalRepresentation); + } + mLauncherSettings.setContentList(profileName, dirNames, archiveNames, fileNames); + mGameSettings.setContentList(dirList, selectedArchivePaths(), fileNames); + + QString language(mSelector->languageBox()->currentData().toString()); + + mLauncherSettings.setLanguage(language); + + if (language == QLatin1String("Polish")) + { + mGameSettings.setValue(QLatin1String("encoding"), { "win1250" }); + } + else if (language == QLatin1String("Russian")) + { + mGameSettings.setValue(QLatin1String("encoding"), { "win1251" }); + } + else + { + mGameSettings.setValue(QLatin1String("encoding"), { "win1252" }); + } +} + +QList Launcher::DataFilesPage::selectedDirectoriesPaths() const +{ + QList dirList; + for (int i = 0; i < ui.directoryListWidget->count(); ++i) + { + const QListWidgetItem* item = ui.directoryListWidget->item(i); + if (item->flags() & Qt::ItemIsEnabled) + dirList.append(qvariant_cast(item->data(Qt::UserRole))); + } + return dirList; +} + +QList Launcher::DataFilesPage::selectedArchivePaths() const +{ + QList archiveList; + for (int i = 0; i < ui.archiveListWidget->count(); ++i) + { + const QListWidgetItem* item = ui.archiveListWidget->item(i); + if (item->checkState() == Qt::Checked) + archiveList.append(qvariant_cast(item->data(Qt::UserRole))); + } + return archiveList; } -QStringList Launcher::DataFilesPage::selectedFilePaths() +QStringList Launcher::DataFilesPage::selectedFilePaths() const { - //retrieve the files selected for the profile + // retrieve the files selected for the profile ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); QStringList filePaths; - for (const ContentSelectorModel::EsmFile *item : items) - { - QFile file(item->filePath()); - - if(file.exists()) - { + for (const ContentSelectorModel::EsmFile* item : items) + if (QFile::exists(item->filePath())) filePaths.append(item->filePath()); - } - else - { - slotRefreshButtonClicked(); - } - } return filePaths; } -void Launcher::DataFilesPage::removeProfile(const QString &profile) +void Launcher::DataFilesPage::removeProfile(const QString& profile) { mLauncherSettings.removeContentList(profile); } -QAbstractItemModel *Launcher::DataFilesPage::profilesModel() const +QAbstractItemModel* Launcher::DataFilesPage::profilesModel() const { return ui.profilesComboBox->model(); } @@ -211,50 +539,59 @@ void Launcher::DataFilesPage::setProfile(int index, bool savePrevious) mPreviousProfile = current; - setProfile (previous, current, savePrevious); + setProfile(previous, current, savePrevious); } } -void Launcher::DataFilesPage::setProfile (const QString &previous, const QString ¤t, bool savePrevious) +void Launcher::DataFilesPage::setProfile(const QString& previous, const QString& current, bool savePrevious) { - //abort if no change (poss. duplicate signal) + // abort if no change (poss. duplicate signal) if (previous == current) - return; + return; if (!previous.isEmpty() && savePrevious) - saveSettings (previous); + saveSettings(previous); - ui.profilesComboBox->setCurrentProfile (ui.profilesComboBox->findText (current)); + ui.profilesComboBox->setCurrentProfile(ui.profilesComboBox->findText(current)); + mNewDataDirs.clear(); + mKnownArchives.clear(); populateFileViews(current); + // save list of "old" bsa to be able to display "new" bsa in a different colour + for (int i = 0; i < ui.archiveListWidget->count(); ++i) + { + auto* item = ui.archiveListWidget->item(i); + mKnownArchives.push_back(item->text()); + } + checkForDefaultProfile(); } -void Launcher::DataFilesPage::slotProfileDeleted (const QString &item) +void Launcher::DataFilesPage::slotProfileDeleted(const QString& item) { - removeProfile (item); + removeProfile(item); } -void Launcher::DataFilesPage:: refreshDataFilesView () +void Launcher::DataFilesPage::refreshDataFilesView() { QString currentProfile = ui.profilesComboBox->currentText(); saveSettings(currentProfile); populateFileViews(currentProfile); } -void Launcher::DataFilesPage::slotRefreshButtonClicked () +void Launcher::DataFilesPage::slotRefreshButtonClicked() { refreshDataFilesView(); } -void Launcher::DataFilesPage::slotProfileChangedByUser(const QString &previous, const QString ¤t) +void Launcher::DataFilesPage::slotProfileChangedByUser(const QString& previous, const QString& current) { setProfile(previous, current, true); - emit signalProfileChanged (ui.profilesComboBox->findText(current)); + emit signalProfileChanged(ui.profilesComboBox->findText(current)); } -void Launcher::DataFilesPage::slotProfileRenamed(const QString &previous, const QString ¤t) +void Launcher::DataFilesPage::slotProfileRenamed(const QString& previous, const QString& current) { if (previous.isEmpty()) return; @@ -263,7 +600,7 @@ void Launcher::DataFilesPage::slotProfileRenamed(const QString &previous, const saveSettings(); // Remove the old one - removeProfile (previous); + removeProfile(previous); loadSettings(); } @@ -274,7 +611,7 @@ void Launcher::DataFilesPage::slotProfileChanged(int index) if (ui.profilesComboBox->currentIndex() != index) ui.profilesComboBox->setCurrentIndex(index); - setProfile (index, true); + setProfile(index, true); } void Launcher::DataFilesPage::on_newProfileAction_triggered() @@ -294,16 +631,16 @@ void Launcher::DataFilesPage::on_newProfileAction_triggered() addProfile(profile, true); } -void Launcher::DataFilesPage::addProfile (const QString &profile, bool setAsCurrent) +void Launcher::DataFilesPage::addProfile(const QString& profile, bool setAsCurrent) { if (profile.isEmpty()) return; - if (ui.profilesComboBox->findText (profile) == -1) - ui.profilesComboBox->addItem (profile); + if (ui.profilesComboBox->findText(profile) == -1) + ui.profilesComboBox->addItem(profile); if (setAsCurrent) - setProfile (ui.profilesComboBox->findText (profile), false); + setProfile(ui.profilesComboBox->findText(profile), false); } void Launcher::DataFilesPage::on_cloneProfileAction_triggered() @@ -316,7 +653,20 @@ void Launcher::DataFilesPage::on_cloneProfileAction_triggered() if (profile.isEmpty()) return; - mLauncherSettings.setContentList(profile, selectedFilePaths()); + const auto& dirList = selectedDirectoriesPaths(); + QStringList dirNames; + for (const auto& dir : dirList) + { + if (mGameSettings.isUserSetting(dir)) + dirNames.push_back(dir.originalRepresentation); + } + QStringList archiveNames; + for (const auto& archive : selectedArchivePaths()) + { + if (mGameSettings.isUserSetting(archive)) + archiveNames.push_back(archive.originalRepresentation); + } + mLauncherSettings.setContentList(profile, dirNames, archiveNames, selectedFilePaths()); addProfile(profile, true); } @@ -327,11 +677,11 @@ void Launcher::DataFilesPage::on_deleteProfileAction_triggered() if (profile.isEmpty()) return; - if (!showDeleteMessageBox (profile)) + if (!showDeleteMessageBox(profile)) return; // this should work since the Default profile can't be deleted and is always index 0 - int next = ui.profilesComboBox->currentIndex()-1; + int next = ui.profilesComboBox->currentIndex() - 1; // changing the profile forces a reload of plugin file views. ui.profilesComboBox->setCurrentIndex(next); @@ -342,28 +692,230 @@ void Launcher::DataFilesPage::on_deleteProfileAction_triggered() checkForDefaultProfile(); } -void Launcher::DataFilesPage::updateNewProfileOkButton(const QString &text) +void Launcher::DataFilesPage::updateNewProfileOkButton(const QString& text) { // We do this here because we need the profiles combobox text mNewProfileDialog->setOkButtonEnabled(!text.isEmpty() && ui.profilesComboBox->findText(text) == -1); } -void Launcher::DataFilesPage::updateCloneProfileOkButton(const QString &text) +void Launcher::DataFilesPage::updateCloneProfileOkButton(const QString& text) { // We do this here because we need the profiles combobox text mCloneProfileDialog->setOkButtonEnabled(!text.isEmpty() && ui.profilesComboBox->findText(text) == -1); } +void Launcher::DataFilesPage::addSubdirectories(bool append) +{ + int selectedRow = -1; + if (append) + { + selectedRow = ui.directoryListWidget->count(); + } + else + { + const QList> sortedItems = sortedSelectedItems(ui.directoryListWidget); + if (!sortedItems.isEmpty()) + selectedRow = sortedItems.first().first; + } + + if (selectedRow == -1) + return; + + QString rootPath = QFileDialog::getExistingDirectory( + this, tr("Select Directory"), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::Option::ReadOnly); + + if (rootPath.isEmpty()) + return; + + const QDir rootDir(rootPath); + rootPath = rootDir.canonicalPath(); + + QStringList subdirs; + contentSubdirs(rootPath, subdirs); + + // Always offer to append the root directory just in case + if (subdirs.isEmpty() || subdirs[0] != rootPath) + subdirs.prepend(rootPath); + else if (subdirs.size() == 1) + { + // We didn't find anything else that looks like a content directory + // Automatically add the directory selected by user + if (!ui.directoryListWidget->findItems(rootPath, Qt::MatchFixedString).isEmpty()) + return; + ui.directoryListWidget->insertItem(selectedRow, rootPath); + auto* item = ui.directoryListWidget->item(selectedRow); + item->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ rootPath })); + mNewDataDirs.push_back(rootPath); + refreshDataFilesView(); + return; + } + + QDialog dialog; + Ui::SelectSubdirs select; + + select.setupUi(&dialog); + + 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); + } + + dialog.show(); + + if (dialog.exec() == QDialog::Rejected) + return; + + for (int i = 0; i < select.dirListWidget->count(); ++i) + { + const auto* dir = select.dirListWidget->item(i); + if (dir->checkState() == Qt::Checked) + { + ui.directoryListWidget->insertItem(selectedRow, dir->text()); + auto* item = ui.directoryListWidget->item(selectedRow); + item->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ dir->text() })); + mNewDataDirs.push_back(dir->text()); + ++selectedRow; + } + } + + refreshDataFilesView(); +} + +void Launcher::DataFilesPage::sortDirectories() +{ + // Ensure disabled entries (aka default directories) are always at the top. + for (auto i = 1; i < ui.directoryListWidget->count(); ++i) + { + if (!(ui.directoryListWidget->item(i)->flags() & Qt::ItemIsEnabled) + && (ui.directoryListWidget->item(i - 1)->flags() & Qt::ItemIsEnabled)) + { + const auto item = ui.directoryListWidget->takeItem(i); + ui.directoryListWidget->insertItem(i - 1, item); + ui.directoryListWidget->setCurrentRow(i); + } + } +} + +void Launcher::DataFilesPage::sortArchives() +{ + // Ensure disabled entries (aka ones from non-user config files) are always at the top. + for (auto i = 1; i < ui.archiveListWidget->count(); ++i) + { + if (!(ui.archiveListWidget->item(i)->flags() & Qt::ItemIsEnabled) + && (ui.archiveListWidget->item(i - 1)->flags() & Qt::ItemIsEnabled)) + { + const auto item = ui.archiveListWidget->takeItem(i); + ui.archiveListWidget->insertItem(i - 1, item); + ui.archiveListWidget->setCurrentRow(i); + } + } +} + +void Launcher::DataFilesPage::removeDirectory() +{ + for (const auto& path : ui.directoryListWidget->selectedItems()) + ui.directoryListWidget->takeItem(ui.directoryListWidget->row(path)); + refreshDataFilesView(); +} + +void Launcher::DataFilesPage::slotShowArchiveContextMenu(const QPoint& pos) +{ + QPoint globalPos = ui.archiveListWidget->viewport()->mapToGlobal(pos); + mArchiveContextMenu->exec(globalPos); +} + +void Launcher::DataFilesPage::setCheckStateForMultiSelectedItems(bool checked) +{ + Qt::CheckState checkState = checked ? Qt::Checked : Qt::Unchecked; + + for (QListWidgetItem* selectedItem : ui.archiveListWidget->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); + for (const auto& i : sortedItems) + { + int selectedRow = sourceList->row(i.second); + int newRow = selectedRow + step; + if (selectedRow == -1 || newRow < 0 || newRow > sourceList->count() - 1) + break; + + if (!(sourceList->item(newRow)->flags() & Qt::ItemIsEnabled)) + break; + + const auto item = sourceList->takeItem(selectedRow); + sourceList->insertItem(newRow, item); + sourceList->setCurrentRow(newRow); + } +} + +void Launcher::DataFilesPage::addArchive(const QString& name, Qt::CheckState selected, int row) +{ + if (row == -1) + row = ui.archiveListWidget->count(); + ui.archiveListWidget->insertItem(row, name); + ui.archiveListWidget->item(row)->setCheckState(selected); + ui.archiveListWidget->item(row)->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ name })); + if (mKnownArchives.filter(name).isEmpty()) // XXX why contains doesn't work here ??? + { + auto item = ui.archiveListWidget->item(row); + QFont font = item->font(); + font.setBold(true); + font.setItalic(true); + item->setFont(font); + } +} + +void Launcher::DataFilesPage::addArchivesFromDir(const QString& path) +{ + QStringList archiveFilter{ "*.bsa", "*.ba2" }; + QDir dir(path); + + std::unordered_set archives; + for (int i = 0; i < ui.archiveListWidget->count(); ++i) + archives.insert(ui.archiveListWidget->item(i)->text()); + + for (const auto& fileinfo : dir.entryInfoList(archiveFilter)) + { + const auto absPath = fileinfo.absoluteFilePath(); + if (Bsa::BSAFile::detectVersion(Files::pathFromQString(absPath)) == Bsa::BsaVersion::Unknown) + continue; + + const auto fileName = fileinfo.fileName(); + + if (archives.insert(fileName).second) + addArchive(fileName, Qt::Unchecked); + } +} + void Launcher::DataFilesPage::checkForDefaultProfile() { - //don't allow deleting "Default" profile + // don't allow deleting "Default" profile bool success = (ui.profilesComboBox->currentText() != mDefaultContentListName); - ui.deleteProfileAction->setEnabled (success); - ui.profilesComboBox->setEditEnabled (success); + ui.deleteProfileAction->setEnabled(success); + ui.profilesComboBox->setEditEnabled(success); } -bool Launcher::DataFilesPage::showDeleteMessageBox (const QString &text) +bool Launcher::DataFilesPage::showDeleteMessageBox(const QString& text) { QMessageBox msgBox(this); msgBox.setWindowTitle(tr("Delete Content List")); @@ -371,8 +923,7 @@ bool Launcher::DataFilesPage::showDeleteMessageBox (const QString &text) msgBox.setStandardButtons(QMessageBox::Cancel); msgBox.setText(tr("Are you sure you want to delete %1?").arg(text)); - QAbstractButton *deleteButton = - msgBox.addButton(tr("Delete"), QMessageBox::ActionRole); + QAbstractButton* deleteButton = msgBox.addButton(tr("Delete"), QMessageBox::ActionRole); msgBox.exec(); @@ -382,7 +933,8 @@ bool Launcher::DataFilesPage::showDeleteMessageBox (const QString &text) void Launcher::DataFilesPage::slotAddonDataChanged() { QStringList selectedFiles = selectedFilePaths(); - if (previousSelectedFiles != selectedFiles) { + if (previousSelectedFiles != selectedFiles) + { previousSelectedFiles = selectedFiles; // Loading cells for core Morrowind + Expansions takes about 0.2 seconds, which is enough to cause a // barely perceptible UI lag. Splitting into its own thread to alleviate that. @@ -392,22 +944,120 @@ void Launcher::DataFilesPage::slotAddonDataChanged() } // Mutex lock to run reloadCells synchronously. -std::mutex _reloadCellsMutex; +static std::mutex reloadCellsMutex; void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles) { // Use a mutex lock so that we can prevent two threads from executing the rest of this code at the same time // Based on https://stackoverflow.com/a/5429695/531762 - std::unique_lock lock(_reloadCellsMutex); + std::unique_lock lock(reloadCellsMutex); // The following code will run only if there is not another thread currently running it CellNameLoader cellNameLoader; -#if QT_VERSION >= QT_VERSION_CHECK(5,14,0) QSet set = cellNameLoader.getCellNames(selectedFiles); QStringList cellNamesList(set.begin(), set.end()); -#else - QStringList cellNamesList = QStringList::fromSet(cellNameLoader.getCellNames(selectedFiles)); -#endif std::sort(cellNamesList.begin(), cellNamesList.end()); emit signalLoadedCellsChanged(cellNamesList); } + +void Launcher::DataFilesPage::startNavMeshTool() +{ + mMainDialog->writeSettings(); + + ui.navMeshLogPlainTextEdit->clear(); + ui.navMeshProgressBar->setValue(0); + ui.navMeshProgressBar->setMaximum(1); + ui.navMeshProgressBar->resetFormat(); + + mNavMeshToolProgress = NavMeshToolProgress{}; + + QStringList arguments({ "--write-binary-log" }); + if (ui.navMeshRemoveUnusedTilesCheckBox->checkState() == Qt::Checked) + arguments.append("--remove-unused-tiles"); + + if (!mNavMeshToolInvoker->startProcess(QLatin1String("openmw-navmeshtool"), arguments)) + return; + + ui.cancelNavMeshButton->setEnabled(true); + ui.navMeshProgressBar->setEnabled(true); +} + +void Launcher::DataFilesPage::killNavMeshTool() +{ + mNavMeshToolInvoker->killProcess(); +} + +void Launcher::DataFilesPage::readNavMeshToolStderr() +{ + updateNavMeshProgress(4096); +} + +void Launcher::DataFilesPage::updateNavMeshProgress(int minDataSize) +{ + if (!mNavMeshToolProgress.mEnabled) + return; + QProcess& process = *mNavMeshToolInvoker->getProcess(); + mNavMeshToolProgress.mMessagesData.append(process.readAllStandardError()); + if (mNavMeshToolProgress.mMessagesData.size() < minDataSize) + return; + const std::byte* const begin = reinterpret_cast(mNavMeshToolProgress.mMessagesData.constData()); + const std::byte* const end = begin + mNavMeshToolProgress.mMessagesData.size(); + const std::byte* position = begin; + HandleNavMeshToolMessage handle{ + mNavMeshToolProgress.mCellsCount, + mNavMeshToolProgress.mExpectedMaxProgress, + ui.navMeshProgressBar->maximum(), + ui.navMeshProgressBar->value(), + }; + try + { + while (true) + { + NavMeshTool::Message message; + const std::byte* const nextPosition = NavMeshTool::deserialize(position, end, message); + if (nextPosition == position) + break; + position = nextPosition; + handle = std::visit(handle, NavMeshTool::decode(message)); + } + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Failed to deserialize navmeshtool message: " << e.what(); + mNavMeshToolProgress.mEnabled = false; + ui.navMeshProgressBar->setFormat("Failed to update progress: " + QString(e.what())); + } + if (position != begin) + mNavMeshToolProgress.mMessagesData = mNavMeshToolProgress.mMessagesData.mid(position - begin); + mNavMeshToolProgress.mCellsCount = handle.mCellsCount; + mNavMeshToolProgress.mExpectedMaxProgress = handle.mExpectedMaxProgress; + ui.navMeshProgressBar->setMaximum(handle.mMaxProgress); + ui.navMeshProgressBar->setValue(handle.mProgress); +} + +void Launcher::DataFilesPage::readNavMeshToolStdout() +{ + QProcess& process = *mNavMeshToolInvoker->getProcess(); + QByteArray& logData = mNavMeshToolProgress.mLogData; + logData.append(process.readAllStandardOutput()); + const int lineEnd = logData.lastIndexOf('\n'); + if (lineEnd == -1) + return; + const int size = logData.size() >= lineEnd && logData[lineEnd - 1] == '\r' ? lineEnd - 1 : lineEnd; + ui.navMeshLogPlainTextEdit->appendPlainText(QString::fromUtf8(logData.data(), size)); + logData = logData.mid(lineEnd + 1); +} + +void Launcher::DataFilesPage::navMeshToolFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + updateNavMeshProgress(0); + ui.navMeshLogPlainTextEdit->appendPlainText( + QString::fromUtf8(mNavMeshToolInvoker->getProcess()->readAllStandardOutput())); + if (exitCode == 0 && exitStatus == QProcess::ExitStatus::NormalExit) + { + ui.navMeshProgressBar->setValue(ui.navMeshProgressBar->maximum()); + ui.navMeshProgressBar->resetFormat(); + } + ui.cancelNavMeshButton->setEnabled(false); + ui.navMeshProgressBar->setEnabled(false); +} diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 5a7a6dc6e6e..7e347c08443 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -2,23 +2,36 @@ #define DATAFILESPAGE_H #include "ui_datafilespage.h" -#include +#include #include +#include #include +#include class QSortFilterProxyModel; class QAbstractItemModel; class QMenu; -namespace Files { struct ConfigurationManager; } -namespace ContentSelectorView { class ContentSelector; } -namespace Config { class GameSettings; - class LauncherSettings; } +namespace Files +{ + struct ConfigurationManager; +} +namespace ContentSelectorView +{ + class ContentSelector; +} +namespace Config +{ + class GameSettings; + struct SettingValue; + class LauncherSettings; +} namespace Launcher { + class MainDialog; class TextInputDialog; class ProfilesComboBox; @@ -26,131 +39,115 @@ namespace Launcher { Q_OBJECT - ContentSelectorView::ContentSelector *mSelector; + ContentSelectorView::ContentSelector* mSelector; Ui::DataFilesPage ui; + QMenu* mArchiveContextMenu; public: - explicit DataFilesPage (Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, - Config::LauncherSettings &launcherSettings, QWidget *parent = nullptr); + explicit DataFilesPage(const Files::ConfigurationManager& cfg, Config::GameSettings& gameSettings, + Config::LauncherSettings& launcherSettings, MainDialog* parent = nullptr); QAbstractItemModel* profilesModel() const; int profilesIndex() const; - //void writeConfig(QString profile = QString()); - void saveSettings(const QString &profile = ""); + // void writeConfig(QString profile = QString()); + void saveSettings(const QString& profile = ""); bool loadSettings(); - /** - * Returns the file paths of all selected content files - * @return the file paths of all selected content files - */ - QStringList selectedFilePaths(); - signals: - void signalProfileChanged (int index); + void signalProfileChanged(int index); void signalLoadedCellsChanged(QStringList selectedFiles); public slots: - void slotProfileChanged (int index); + void slotProfileChanged(int index); private slots: - void slotProfileChangedByUser(const QString &previous, const QString ¤t); - void slotProfileRenamed(const QString &previous, const QString ¤t); - void slotProfileDeleted(const QString &item); - void slotAddonDataChanged (); - void slotRefreshButtonClicked (); + void slotProfileChangedByUser(const QString& previous, const QString& current); + void slotProfileRenamed(const QString& previous, const QString& current); + void slotProfileDeleted(const QString& item); + void slotAddonDataChanged(); + void slotRefreshButtonClicked(); - void updateNewProfileOkButton(const QString &text); - void updateCloneProfileOkButton(const QString &text); + void updateNewProfileOkButton(const QString& text); + void updateCloneProfileOkButton(const QString& text); + void addSubdirectories(bool append); + void sortDirectories(); + void sortArchives(); + void removeDirectory(); + void moveSources(QListWidget* sourceList, int step); + + void slotShowArchiveContextMenu(const QPoint& pos); + void slotCheckMultiSelectedItems(); + void slotUncheckMultiSelectedItems(); void on_newProfileAction_triggered(); void on_cloneProfileAction_triggered(); void on_deleteProfileAction_triggered(); + void startNavMeshTool(); + void killNavMeshTool(); + void readNavMeshToolStdout(); + void readNavMeshToolStderr(); + void navMeshToolFinished(int exitCode, QProcess::ExitStatus exitStatus); + public: /// Content List that is always present - const static char *mDefaultContentListName; + const static char* mDefaultContentListName; private: + struct NavMeshToolProgress + { + bool mEnabled = true; + QByteArray mLogData; + QByteArray mMessagesData; + std::map mWorldspaces; + int mCellsCount = 0; + int mExpectedMaxProgress = 0; + }; - TextInputDialog *mNewProfileDialog; - TextInputDialog *mCloneProfileDialog; + MainDialog* mMainDialog; + TextInputDialog* mNewProfileDialog; + TextInputDialog* mCloneProfileDialog; - Files::ConfigurationManager &mCfgMgr; + const Files::ConfigurationManager& mCfgMgr; - Config::GameSettings &mGameSettings; - Config::LauncherSettings &mLauncherSettings; + Config::GameSettings& mGameSettings; + Config::LauncherSettings& mLauncherSettings; QString mPreviousProfile; QStringList previousSelectedFiles; QString mDataLocal; + QStringList mKnownArchives; + QStringList mNewDataDirs; + Process::ProcessInvoker* mNavMeshToolInvoker; + NavMeshToolProgress mNavMeshToolProgress; + + void addArchive(const QString& name, Qt::CheckState selected, int row = -1); + void addArchivesFromDir(const QString& dir); void buildView(); - void setProfile (int index, bool savePrevious); - void setProfile (const QString &previous, const QString ¤t, bool savePrevious); - void removeProfile (const QString &profile); - bool showDeleteMessageBox (const QString &text); - void addProfile (const QString &profile, bool setAsCurrent); + void buildArchiveContextMenu(); + void setCheckStateForMultiSelectedItems(bool checked); + void setProfile(int index, bool savePrevious); + void setProfile(const QString& previous, const QString& current, bool savePrevious); + void removeProfile(const QString& profile); + bool showDeleteMessageBox(const QString& text); + void addProfile(const QString& profile, bool setAsCurrent); void checkForDefaultProfile(); void populateFileViews(const QString& contentModelName); void reloadCells(QStringList selectedFiles); - void refreshDataFilesView (); - - class PathIterator - { - QStringList::ConstIterator mCitEnd; - QStringList::ConstIterator mCitCurrent; - QStringList::ConstIterator mCitBegin; - QString mFile; - QString mFilePath; - - public: - PathIterator (const QStringList &list) - { - mCitBegin = list.constBegin(); - mCitCurrent = mCitBegin; - mCitEnd = list.constEnd(); - } - - QString findFirstPath (const QString &file) - { - mCitCurrent = mCitBegin; - mFile = file; - return path(); - } - - QString findNextPath () { return path(); } - - private: - - QString path () - { - bool success = false; - QDir dir; - QFileInfo file; - - while (!success) - { - if (mCitCurrent == mCitEnd) - break; - - dir.setPath (*(mCitCurrent++)); - file.setFile (dir.absoluteFilePath (mFile)); - - success = file.exists(); - } - - if (success) - return file.absoluteFilePath(); - - return ""; - } + void refreshDataFilesView(); + void updateNavMeshProgress(int minDataSize); - }; - - QStringList filesInProfile(const QString& profileName, PathIterator& pathIterator); + /** + * Returns the file paths of all selected content files + * @return the file paths of all selected content files + */ + QStringList selectedFilePaths() const; + QList selectedArchivePaths() const; + QList selectedDirectoriesPaths() const; }; } #endif diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index ebb031e9eff..735bcf1df1c 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -1,8 +1,11 @@ #include "graphicspage.hpp" -#include +#include "sdlinit.hpp" + +#include +#include + #include -#include #include #ifdef MAC_OS_X_VERSION_MIN_REQUIRED @@ -13,29 +16,12 @@ #include -#include - -#include - -QString getAspect(int x, int y) -{ - int gcd = std::gcd (x, y); - if (gcd == 0) - return QString(); - - int xaspect = x / gcd; - int yaspect = y / gcd; - // special case: 8 : 5 is usually referred to as 16:10 - if (xaspect == 8 && yaspect == 5) - return QString("16:10"); - - return QString(QString::number(xaspect) + ":" + QString::number(yaspect)); -} +#include -Launcher::GraphicsPage::GraphicsPage(QWidget *parent) +Launcher::GraphicsPage::GraphicsPage(QWidget* parent) : QWidget(parent) { - setObjectName ("GraphicsPage"); + setObjectName("GraphicsPage"); setupUi(this); // Set the maximum res we can set in windowed mode @@ -43,12 +29,11 @@ Launcher::GraphicsPage::GraphicsPage(QWidget *parent) customWidthSpinBox->setMaximum(res.width()); customHeightSpinBox->setMaximum(res.height()); - connect(fullScreenCheckBox, SIGNAL(stateChanged(int)), this, SLOT(slotFullScreenChanged(int))); - connect(standardRadioButton, SIGNAL(toggled(bool)), this, SLOT(slotStandardToggled(bool))); - connect(screenComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(screenChanged(int))); - connect(framerateLimitCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotFramerateLimitToggled(bool))); - connect(shadowDistanceCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotShadowDistLimitToggled(bool))); - + connect(windowModeComboBox, qOverload(&QComboBox::currentIndexChanged), this, + &GraphicsPage::slotFullScreenChanged); + connect(standardRadioButton, &QRadioButton::toggled, this, &GraphicsPage::slotStandardToggled); + connect(screenComboBox, qOverload(&QComboBox::currentIndexChanged), this, &GraphicsPage::screenChanged); + connect(framerateLimitCheckBox, &QCheckBox::toggled, this, &GraphicsPage::slotFramerateLimitToggled); } bool Launcher::GraphicsPage::setupSDL() @@ -67,7 +52,8 @@ bool Launcher::GraphicsPage::setupSDL() msgBox.setWindowTitle(tr("Error receiving number of screens")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
SDL_GetNumVideoDisplays failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); + msgBox.setText( + tr("
SDL_GetNumVideoDisplays failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); msgBox.exec(); return false; } @@ -93,85 +79,52 @@ bool Launcher::GraphicsPage::loadSettings() return false; // Visuals - if (Settings::Manager::getBool("vsync", "Video")) - vSyncCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("fullscreen", "Video")) - fullScreenCheckBox->setCheckState(Qt::Checked); + const int vsync = Settings::video().mVsyncMode; + + vSyncComboBox->setCurrentIndex(vsync); + + const Settings::WindowMode windowMode = Settings::video().mWindowMode; + + windowModeComboBox->setCurrentIndex(static_cast(windowMode)); + handleWindowModeChange(windowMode); - if (Settings::Manager::getBool("window border", "Video")) + if (Settings::video().mWindowBorder) windowBorderCheckBox->setCheckState(Qt::Checked); // aaValue is the actual value (0, 1, 2, 4, 8, 16) - int aaValue = Settings::Manager::getInt("antialiasing", "Video"); + const int aaValue = Settings::video().mAntialiasing; // aaIndex is the index into the allowed values in the pull down. - int aaIndex = antiAliasingComboBox->findText(QString::number(aaValue)); + const int aaIndex = antiAliasingComboBox->findText(QString::number(aaValue)); if (aaIndex != -1) antiAliasingComboBox->setCurrentIndex(aaIndex); - int width = Settings::Manager::getInt("resolution x", "Video"); - int height = Settings::Manager::getInt("resolution y", "Video"); - QString resolution = QString::number(width) + QString(" x ") + QString::number(height); - screenComboBox->setCurrentIndex(Settings::Manager::getInt("screen", "Video")); + const int width = Settings::video().mResolutionX; + const int height = Settings::video().mResolutionY; + QString resolution = QString::number(width) + QString(" × ") + QString::number(height); + screenComboBox->setCurrentIndex(Settings::video().mScreen); int resIndex = resolutionComboBox->findText(resolution, Qt::MatchStartsWith); - if (resIndex != -1) { + if (resIndex != -1) + { standardRadioButton->toggle(); resolutionComboBox->setCurrentIndex(resIndex); - } else { + } + else + { customRadioButton->toggle(); customWidthSpinBox->setValue(width); customHeightSpinBox->setValue(height); } - float fpsLimit = Settings::Manager::getFloat("framerate limit", "Video"); + const float fpsLimit = Settings::video().mFramerateLimit; if (fpsLimit != 0) { framerateLimitCheckBox->setCheckState(Qt::Checked); framerateLimitSpinBox->setValue(fpsLimit); } - // Lighting - int lightingMethod = 1; - if (Settings::Manager::getString("lighting method", "Shaders") == "legacy") - lightingMethod = 0; - else if (Settings::Manager::getString("lighting method", "Shaders") == "shaders") - lightingMethod = 2; - lightingMethodComboBox->setCurrentIndex(lightingMethod); - - // Shadows - if (Settings::Manager::getBool("actor shadows", "Shadows")) - actorShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("player shadows", "Shadows")) - playerShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("terrain shadows", "Shadows")) - terrainShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("object shadows", "Shadows")) - objectShadowsCheckBox->setCheckState(Qt::Checked); - if (Settings::Manager::getBool("enable indoor shadows", "Shadows")) - indoorShadowsCheckBox->setCheckState(Qt::Checked); - - shadowComputeSceneBoundsComboBox->setCurrentIndex( - shadowComputeSceneBoundsComboBox->findText( - QString(tr(Settings::Manager::getString("compute scene bounds", "Shadows").c_str())))); - - int shadowDistLimit = Settings::Manager::getInt("maximum shadow map distance", "Shadows"); - if (shadowDistLimit > 0) - { - shadowDistanceCheckBox->setCheckState(Qt::Checked); - shadowDistanceSpinBox->setValue(shadowDistLimit); - } - - float shadowFadeStart = Settings::Manager::getFloat("shadow fade start", "Shadows"); - if (shadowFadeStart != 0) - fadeStartSpinBox->setValue(shadowFadeStart); - - int shadowRes = Settings::Manager::getInt("shadow map resolution", "Shadows"); - int shadowResIndex = shadowResolutionComboBox->findText(QString::number(shadowRes)); - if (shadowResIndex != -1) - shadowResolutionComboBox->setCurrentIndex(shadowResIndex); - return true; } @@ -179,112 +132,41 @@ void Launcher::GraphicsPage::saveSettings() { // Visuals - // Ensure we only set the new settings if they changed. This is to avoid cluttering the - // user settings file (which by definition should only contain settings the user has touched) - bool cVSync = vSyncCheckBox->checkState(); - if (cVSync != Settings::Manager::getBool("vsync", "Video")) - Settings::Manager::setBool("vsync", "Video", cVSync); - - bool cFullScreen = fullScreenCheckBox->checkState(); - if (cFullScreen != Settings::Manager::getBool("fullscreen", "Video")) - Settings::Manager::setBool("fullscreen", "Video", cFullScreen); - - bool cWindowBorder = windowBorderCheckBox->checkState(); - if (cWindowBorder != Settings::Manager::getBool("window border", "Video")) - Settings::Manager::setBool("window border", "Video", cWindowBorder); - - int cAAValue = antiAliasingComboBox->currentText().toInt(); - if (cAAValue != Settings::Manager::getInt("antialiasing", "Video")) - Settings::Manager::setInt("antialiasing", "Video", cAAValue); + Settings::video().mVsyncMode.set(static_cast(vSyncComboBox->currentIndex())); + Settings::video().mWindowMode.set(static_cast(windowModeComboBox->currentIndex())); + Settings::video().mWindowBorder.set(windowBorderCheckBox->checkState() == Qt::Checked); + Settings::video().mAntialiasing.set(antiAliasingComboBox->currentText().toInt()); int cWidth = 0; int cHeight = 0; - if (standardRadioButton->isChecked()) { - QRegExp resolutionRe(QString("(\\d+) x (\\d+).*")); - if (resolutionRe.exactMatch(resolutionComboBox->currentText().simplified())) { - cWidth = resolutionRe.cap(1).toInt(); - cHeight = resolutionRe.cap(2).toInt(); + if (standardRadioButton->isChecked()) + { + QRegularExpression resolutionRe("^(\\d+) × (\\d+)"); + QRegularExpressionMatch match = resolutionRe.match(resolutionComboBox->currentText().simplified()); + if (match.hasMatch()) + { + cWidth = match.captured(1).toInt(); + cHeight = match.captured(2).toInt(); } - } else { + } + else + { cWidth = customWidthSpinBox->value(); cHeight = customHeightSpinBox->value(); } - if (cWidth != Settings::Manager::getInt("resolution x", "Video")) - Settings::Manager::setInt("resolution x", "Video", cWidth); - - if (cHeight != Settings::Manager::getInt("resolution y", "Video")) - Settings::Manager::setInt("resolution y", "Video", cHeight); - - int cScreen = screenComboBox->currentIndex(); - if (cScreen != Settings::Manager::getInt("screen", "Video")) - Settings::Manager::setInt("screen", "Video", cScreen); + Settings::video().mResolutionX.set(cWidth); + Settings::video().mResolutionY.set(cHeight); + Settings::video().mScreen.set(screenComboBox->currentIndex()); if (framerateLimitCheckBox->checkState() != Qt::Unchecked) { - float cFpsLimit = framerateLimitSpinBox->value(); - if (cFpsLimit != Settings::Manager::getFloat("framerate limit", "Video")) - Settings::Manager::setFloat("framerate limit", "Video", cFpsLimit); - } - else if (Settings::Manager::getFloat("framerate limit", "Video") != 0) - { - Settings::Manager::setFloat("framerate limit", "Video", 0); - } - - // Lighting - static std::array lightingMethodMap = {"legacy", "shaders compatibility", "shaders"}; - Settings::Manager::setString("lighting method", "Shaders", lightingMethodMap[lightingMethodComboBox->currentIndex()]); - - // Shadows - int cShadowDist = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0; - if (Settings::Manager::getInt("maximum shadow map distance", "Shadows") != cShadowDist) - Settings::Manager::setInt("maximum shadow map distance", "Shadows", cShadowDist); - float cFadeStart = fadeStartSpinBox->value(); - if (cShadowDist > 0 && Settings::Manager::getFloat("shadow fade start", "Shadows") != cFadeStart) - Settings::Manager::setFloat("shadow fade start", "Shadows", cFadeStart); - - bool cActorShadows = actorShadowsCheckBox->checkState(); - bool cObjectShadows = objectShadowsCheckBox->checkState(); - bool cTerrainShadows = terrainShadowsCheckBox->checkState(); - bool cPlayerShadows = playerShadowsCheckBox->checkState(); - if (cActorShadows || cObjectShadows || cTerrainShadows || cPlayerShadows) - { - if (!Settings::Manager::getBool("enable shadows", "Shadows")) - Settings::Manager::setBool("enable shadows", "Shadows", true); - if (Settings::Manager::getBool("actor shadows", "Shadows") != cActorShadows) - Settings::Manager::setBool("actor shadows", "Shadows", cActorShadows); - if (Settings::Manager::getBool("player shadows", "Shadows") != cPlayerShadows) - Settings::Manager::setBool("player shadows", "Shadows", cPlayerShadows); - if (Settings::Manager::getBool("object shadows", "Shadows") != cObjectShadows) - Settings::Manager::setBool("object shadows", "Shadows", cObjectShadows); - if (Settings::Manager::getBool("terrain shadows", "Shadows") != cTerrainShadows) - Settings::Manager::setBool("terrain shadows", "Shadows", cTerrainShadows); + Settings::video().mFramerateLimit.set(framerateLimitSpinBox->value()); } - else + else if (Settings::video().mFramerateLimit != 0) { - if (Settings::Manager::getBool("enable shadows", "Shadows")) - Settings::Manager::setBool("enable shadows", "Shadows", false); - if (Settings::Manager::getBool("actor shadows", "Shadows")) - Settings::Manager::setBool("actor shadows", "Shadows", false); - if (Settings::Manager::getBool("player shadows", "Shadows")) - Settings::Manager::setBool("player shadows", "Shadows", false); - if (Settings::Manager::getBool("object shadows", "Shadows")) - Settings::Manager::setBool("object shadows", "Shadows", false); - if (Settings::Manager::getBool("terrain shadows", "Shadows")) - Settings::Manager::setBool("terrain shadows", "Shadows", false); + Settings::video().mFramerateLimit.set(0); } - - bool cIndoorShadows = indoorShadowsCheckBox->checkState(); - if (Settings::Manager::getBool("enable indoor shadows", "Shadows") != cIndoorShadows) - Settings::Manager::setBool("enable indoor shadows", "Shadows", cIndoorShadows); - - int cShadowRes = shadowResolutionComboBox->currentText().toInt(); - if (cShadowRes != Settings::Manager::getInt("shadow map resolution", "Shadows")) - Settings::Manager::setInt("shadow map resolution", "Shadows", cShadowRes); - - auto cComputeSceneBounds = shadowComputeSceneBoundsComboBox->currentText().toStdString(); - if (cComputeSceneBounds != Settings::Manager::getString("compute scene bounds", "Shadows")) - Settings::Manager::setString("compute scene bounds", "Shadows", cComputeSceneBounds); } QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) @@ -299,7 +181,8 @@ QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) msgBox.setWindowTitle(tr("Error receiving resolutions")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
SDL_GetNumDisplayModes failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); + msgBox.setText( + tr("
SDL_GetNumDisplayModes failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); msgBox.exec(); return result; } @@ -312,22 +195,14 @@ QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) msgBox.setWindowTitle(tr("Error receiving resolutions")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
SDL_GetDisplayMode failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); + msgBox.setText( + tr("
SDL_GetDisplayMode failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); msgBox.exec(); return result; } - QString resolution = QString::number(mode.w) + QString(" x ") + QString::number(mode.h); - - QString aspect = getAspect(mode.w, mode.h); - if (aspect == QLatin1String("16:9") || aspect == QLatin1String("16:10")) { - resolution.append(tr("\t(Wide ") + aspect + ")"); - - } else if (aspect == QLatin1String("4:3")) { - resolution.append(tr("\t(Standard 4:3)")); - } - - result.append(resolution); + auto str = Misc::getResolutionText(mode.w, mode.h, "%i × %i (%i:%i)"); + result.append(QString(str.c_str())); } result.removeDuplicates(); @@ -351,21 +226,30 @@ QRect Launcher::GraphicsPage::getMaximumResolution() void Launcher::GraphicsPage::screenChanged(int screen) { - if (screen >= 0) { + if (screen >= 0) + { resolutionComboBox->clear(); resolutionComboBox->addItems(mResolutionsPerScreen[screen]); } } -void Launcher::GraphicsPage::slotFullScreenChanged(int state) +void Launcher::GraphicsPage::slotFullScreenChanged(int mode) { - if (state == Qt::Checked) { + handleWindowModeChange(static_cast(mode)); +} + +void Launcher::GraphicsPage::handleWindowModeChange(Settings::WindowMode mode) +{ + if (mode == Settings::WindowMode::Fullscreen || mode == Settings::WindowMode::WindowedFullscreen) + { standardRadioButton->toggle(); customRadioButton->setEnabled(false); customWidthSpinBox->setEnabled(false); customHeightSpinBox->setEnabled(false); windowBorderCheckBox->setEnabled(false); - } else { + } + else + { customRadioButton->setEnabled(true); customWidthSpinBox->setEnabled(true); customHeightSpinBox->setEnabled(true); @@ -375,11 +259,14 @@ void Launcher::GraphicsPage::slotFullScreenChanged(int state) void Launcher::GraphicsPage::slotStandardToggled(bool checked) { - if (checked) { + if (checked) + { resolutionComboBox->setEnabled(true); customWidthSpinBox->setEnabled(false); customHeightSpinBox->setEnabled(false); - } else { + } + else + { resolutionComboBox->setEnabled(false); customWidthSpinBox->setEnabled(true); customHeightSpinBox->setEnabled(true); @@ -390,9 +277,3 @@ void Launcher::GraphicsPage::slotFramerateLimitToggled(bool checked) { framerateLimitSpinBox->setEnabled(checked); } - -void Launcher::GraphicsPage::slotShadowDistLimitToggled(bool checked) -{ - shadowDistanceSpinBox->setEnabled(checked); - fadeStartSpinBox->setEnabled(checked); -} diff --git a/apps/launcher/graphicspage.hpp b/apps/launcher/graphicspage.hpp index a6754ccb04f..928ec9f1a28 100644 --- a/apps/launcher/graphicspage.hpp +++ b/apps/launcher/graphicspage.hpp @@ -3,11 +3,12 @@ #include "ui_graphicspage.h" -#include +#include -#include "sdlinit.hpp" - -namespace Files { struct ConfigurationManager; } +namespace Files +{ + struct ConfigurationManager; +} namespace Launcher { @@ -18,7 +19,7 @@ namespace Launcher Q_OBJECT public: - explicit GraphicsPage(QWidget *parent = nullptr); + explicit GraphicsPage(QWidget* parent = nullptr); void saveSettings(); bool loadSettings(); @@ -30,7 +31,6 @@ namespace Launcher void slotFullScreenChanged(int state); void slotStandardToggled(bool checked); void slotFramerateLimitToggled(bool checked); - void slotShadowDistLimitToggled(bool checked); private: QVector mResolutionsPerScreen; @@ -39,6 +39,7 @@ namespace Launcher static QRect getMaximumResolution(); bool setupSDL(); + void handleWindowModeChange(Settings::WindowMode state); }; } #endif diff --git a/apps/launcher/importpage.cpp b/apps/launcher/importpage.cpp new file mode 100644 index 00000000000..47075db1bcb --- /dev/null +++ b/apps/launcher/importpage.cpp @@ -0,0 +1,228 @@ +#include "importpage.hpp" + +#include +#include +#include +#include + +#include +#include + +using namespace Process; + +Launcher::ImportPage::ImportPage(const Files::ConfigurationManager& cfg, Config::GameSettings& gameSettings, + Config::LauncherSettings& launcherSettings, MainDialog* parent) + : QWidget(parent) + , mCfgMgr(cfg) + , mGameSettings(gameSettings) + , mLauncherSettings(launcherSettings) + , mMain(parent) +{ + setupUi(this); + + mWizardInvoker = new ProcessInvoker(); + mImporterInvoker = new ProcessInvoker(); + resetProgressBar(); + + connect(mWizardInvoker->getProcess(), &QProcess::started, this, &ImportPage::wizardStarted); + + connect(mWizardInvoker->getProcess(), qOverload(&QProcess::finished), this, + &ImportPage::wizardFinished); + + connect(mImporterInvoker->getProcess(), &QProcess::started, this, &ImportPage::importerStarted); + + connect(mImporterInvoker->getProcess(), qOverload(&QProcess::finished), this, + &ImportPage::importerFinished); + + // Detect Morrowind configuration files + QStringList iniPaths; + + for (const auto& path : mGameSettings.getDataDirs()) + { + QDir dir(path.value); + dir.setPath(dir.canonicalPath()); // Resolve symlinks + + if (dir.exists(QString("Morrowind.ini"))) + iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); + else + { + if (!dir.cdUp()) + continue; // Cannot move from Data Files + + if (dir.exists(QString("Morrowind.ini"))) + iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); + } + } + + if (!iniPaths.isEmpty()) + { + settingsComboBox->addItems(iniPaths); + importerButton->setEnabled(true); + } + else + { + importerButton->setEnabled(false); + } + + loadSettings(); +} + +Launcher::ImportPage::~ImportPage() +{ + delete mWizardInvoker; + delete mImporterInvoker; +} + +void Launcher::ImportPage::on_wizardButton_clicked() +{ + mMain->writeSettings(); + + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) + return; +} + +void Launcher::ImportPage::on_importerButton_clicked() +{ + mMain->writeSettings(); + + // Create the file if it doesn't already exist, else the importer will fail + auto path = mCfgMgr.getUserConfigPath(); + path /= "openmw.cfg"; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QFile file(path); +#else + QFile file(Files::pathToQString(path)); +#endif + + if (!file.exists()) + { + if (!file.open(QIODevice::ReadWrite)) + { + // File cannot be created + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText( + tr("

Could not open or create %1 for writing

" + "

Please make sure you have the right permissions " + "and try again.

") + .arg(file.fileName())); + msgBox.exec(); + return; + } + + file.close(); + } + + // Construct the arguments to run the importer + QStringList arguments; + + if (addonsCheckBox->isChecked()) + arguments.append(QString("--game-files")); + + if (fontsCheckBox->isChecked()) + arguments.append(QString("--fonts")); + + arguments.append(QString("--encoding")); + arguments.append(mGameSettings.value(QString("encoding"), { "win1252" }).value); + arguments.append(QString("--ini")); + arguments.append(settingsComboBox->currentText()); + arguments.append(QString("--cfg")); + arguments.append(Files::pathToQString(path)); + + qDebug() << "arguments " << arguments; + + // start the progress bar as a "bouncing ball" + progressBar->setMaximum(0); + progressBar->setValue(0); + if (!mImporterInvoker->startProcess(QLatin1String("openmw-iniimporter"), arguments, false)) + { + resetProgressBar(); + } +} + +void Launcher::ImportPage::on_browseButton_clicked() +{ + QString iniFile = QFileDialog::getOpenFileName(this, QObject::tr("Select configuration file"), QDir::currentPath(), + QString(tr("Morrowind configuration file (*.ini)"))); + + if (iniFile.isEmpty()) + return; + + QFileInfo info(iniFile); + + if (!info.exists() || !info.isReadable()) + return; + + const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); + + if (settingsComboBox->findText(path) == -1) + { + settingsComboBox->addItem(path); + settingsComboBox->setCurrentIndex(settingsComboBox->findText(path)); + importerButton->setEnabled(true); + } +} + +void Launcher::ImportPage::wizardStarted() +{ + mMain->hide(); // Hide the launcher + + wizardButton->setEnabled(false); +} + +void Launcher::ImportPage::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return qApp->quit(); + + mMain->reloadSettings(); + wizardButton->setEnabled(true); + + mMain->show(); // Show the launcher again +} + +void Launcher::ImportPage::importerStarted() +{ + importerButton->setEnabled(false); +} + +void Launcher::ImportPage::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + { + resetProgressBar(); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Importer finished")); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setText(tr("Failed to import settings from INI file.")); + msgBox.exec(); + } + else + { + // indicate progress finished + progressBar->setMaximum(1); + progressBar->setValue(1); + + // Importer may have changed settings, so refresh + mMain->reloadSettings(); + } + + importerButton->setEnabled(true); +} + +void Launcher::ImportPage::resetProgressBar() +{ + // set progress bar to 0 % + progressBar->reset(); +} + +void Launcher::ImportPage::saveSettings() {} + +bool Launcher::ImportPage::loadSettings() +{ + return true; +} diff --git a/apps/launcher/importpage.hpp b/apps/launcher/importpage.hpp new file mode 100644 index 00000000000..f3e16e42beb --- /dev/null +++ b/apps/launcher/importpage.hpp @@ -0,0 +1,64 @@ +#ifndef IMPORTSPAGE_HPP +#define IMPORTSPAGE_HPP + +#include + +#include "ui_importpage.h" + +#include "maindialog.hpp" + +namespace Files +{ + struct ConfigurationManager; +} +namespace Config +{ + class GameSettings; + class LauncherSettings; +} + +namespace Launcher +{ + class TextInputDialog; + + class ImportPage : public QWidget, private Ui::ImportPage + { + Q_OBJECT + + public: + explicit ImportPage(const Files::ConfigurationManager& cfg, Config::GameSettings& gameSettings, + Config::LauncherSettings& launcherSettings, MainDialog* parent = nullptr); + ~ImportPage() override; + + void saveSettings(); + bool loadSettings(); + + /// set progress bar on page to 0% + void resetProgressBar(); + + private slots: + + void on_wizardButton_clicked(); + void on_importerButton_clicked(); + void on_browseButton_clicked(); + + void wizardStarted(); + void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); + + void importerStarted(); + void importerFinished(int exitCode, QProcess::ExitStatus exitStatus); + + private: + Process::ProcessInvoker* mWizardInvoker; + Process::ProcessInvoker* mImporterInvoker; + + const Files::ConfigurationManager& mCfgMgr; + + Config::GameSettings& mGameSettings; + Config::LauncherSettings& mLauncherSettings; + + MainDialog* mMain; + }; +} + +#endif // IMPORTSPAGE_HPP diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index 9c9acb4a174..2ea152305f6 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -1,9 +1,17 @@ #include -#include -#include #include +#include +#include + +#include +#include +#include +#include +#include +#include + #ifdef MAC_OS_X_VERSION_MIN_REQUIRED #undef MAC_OS_X_VERSION_MIN_REQUIRED // We need to do this because of Qt: https://bugreports.qt-project.org/browse/QTBUG-22154 @@ -12,25 +20,31 @@ #include "maindialog.hpp" -int main(int argc, char *argv[]) +int runLauncher(int argc, char* argv[]) { - try - { - QApplication app(argc, argv); + Platform::init(); + + boost::program_options::variables_map variables; + boost::program_options::options_description description; + Files::ConfigurationManager configurationManager; + configurationManager.addCommonOptions(description); + configurationManager.readConfiguration(variables, description, true); - // Internationalization - QString locale = QLocale::system().name().section('_', 0, 0); + Debug::setupLogging(configurationManager.getLogPath(), "Launcher"); - QTranslator appTranslator; - appTranslator.load(":/translations/" + locale + ".qm"); - app.installTranslator(&appTranslator); + try + { + Platform::Application app(argc, argv); - // Now we make sure the current dir is set to application path - QDir dir(QCoreApplication::applicationDirPath()); + QString resourcesPath("."); + if (!variables["resources"].empty()) + { + resourcesPath = Files::pathToQString(variables["resources"].as().u8string()); + } - QDir::setCurrent(dir.absolutePath()); + l10n::installQtTranslations(app, "launcher", resourcesPath); - Launcher::MainDialog mainWin; + Launcher::MainDialog mainWin(configurationManager); Launcher::FirstRunDialogResult result = mainWin.showFirstRunDialog(); if (result == Launcher::FirstRunDialogResultFailure) @@ -43,9 +57,14 @@ int main(int argc, char *argv[]) return exitCode; } - catch (std::exception& e) + catch (const std::exception& e) { - std::cerr << "ERROR: " << e.what() << std::endl; + Log(Debug::Error) << "Unexpected exception: " << e.what(); return 0; } } + +int main(int argc, char* argv[]) +{ + return Debug::wrapApplication(runLauncher, argc, argv, "Launcher"); +} diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index d41cd529d31..07face085f3 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -1,25 +1,36 @@ #include "maindialog.hpp" -#include -#include - -#include -#include -#include -#include -#include #include -#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -#include "playpage.hpp" -#include "graphicspage.hpp" #include "datafilespage.hpp" +#include "graphicspage.hpp" +#include "importpage.hpp" #include "settingspage.hpp" -#include "advancedpage.hpp" + +namespace +{ + constexpr const char* toolBarStyle = "QToolBar { border: 0px; } QToolButton { min-width: 70px }"; +} using namespace Process; -void cfgError(const QString& title, const QString& msg) { +void cfgError(const QString& title, const QString& msg) +{ QMessageBox msgBox; msgBox.setWindowTitle(title); msgBox.setIcon(QMessageBox::Critical); @@ -28,44 +39,48 @@ void cfgError(const QString& title, const QString& msg) { msgBox.exec(); } -Launcher::MainDialog::MainDialog(QWidget *parent) - : QMainWindow(parent), mGameSettings (mCfgMgr) +Launcher::MainDialog::MainDialog(const Files::ConfigurationManager& configurationManager, QWidget* parent) + : QMainWindow(parent) + , mCfgMgr(configurationManager) + , mGameSettings(mCfgMgr) { setupUi(this); mGameInvoker = new ProcessInvoker(); mWizardInvoker = new ProcessInvoker(); - connect(mWizardInvoker->getProcess(), SIGNAL(started()), - this, SLOT(wizardStarted())); + connect(mWizardInvoker->getProcess(), &QProcess::started, this, &MainDialog::wizardStarted); - connect(mWizardInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), - this, SLOT(wizardFinished(int,QProcess::ExitStatus))); + connect(mWizardInvoker->getProcess(), qOverload(&QProcess::finished), this, + &MainDialog::wizardFinished); - iconWidget->setViewMode(QListView::IconMode); - iconWidget->setWrapping(false); - iconWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Just to be sure - iconWidget->setIconSize(QSize(48, 48)); - iconWidget->setMovement(QListView::Static); + buttonBox->button(QDialogButtonBox::Close)->setText(tr("Close")); + buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Launch OpenMW")); + buttonBox->button(QDialogButtonBox::Help)->setText(tr("Help")); - iconWidget->setSpacing(4); - iconWidget->setCurrentRow(0); - iconWidget->setFlow(QListView::LeftToRight); + buttonBox->button(QDialogButtonBox::Ok)->setMinimumWidth(160); - QPushButton *helpButton = new QPushButton(tr("Help")); - QPushButton *playButton = new QPushButton(tr("Play")); - buttonBox->button(QDialogButtonBox::Close)->setText(tr("Close")); - buttonBox->addButton(helpButton, QDialogButtonBox::HelpRole); - buttonBox->addButton(playButton, QDialogButtonBox::AcceptRole); + // Order of buttons can be different on different setups, + // so make sure that the Play button has a focus by default. + buttonBox->button(QDialogButtonBox::Ok)->setFocus(); - connect(buttonBox, SIGNAL(rejected()), this, SLOT(close())); - connect(buttonBox, SIGNAL(accepted()), this, SLOT(play())); - connect(buttonBox, SIGNAL(helpRequested()), this, SLOT(help())); + connect(buttonBox, &QDialogButtonBox::rejected, this, &MainDialog::close); + connect(buttonBox, &QDialogButtonBox::accepted, this, &MainDialog::play); + connect(buttonBox, &QDialogButtonBox::helpRequested, this, &MainDialog::help); // Remove what's this? button setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); createIcons(); + + QWidget* spacer = new QWidget(); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + toolBar->addWidget(spacer); + + QLabel* logo = new QLabel(this); + logo->setPixmap(QIcon(":/images/openmw-header.png").pixmap(QSize(294, 64))); + toolBar->addWidget(logo); + toolBar->setStyleSheet(toolBarStyle); } Launcher::MainDialog::~MainDialog() @@ -74,45 +89,27 @@ Launcher::MainDialog::~MainDialog() delete mWizardInvoker; } +bool Launcher::MainDialog::event(QEvent* event) +{ + // Apply style sheet again if style was changed + if (event->type() == QEvent::PaletteChange) + { + if (toolBar != nullptr) + toolBar->setStyleSheet(toolBarStyle); + } + + return QMainWindow::event(event); +} + void Launcher::MainDialog::createIcons() { if (!QIcon::hasThemeIcon("document-new")) - QIcon::setThemeName("tango"); - - QListWidgetItem *playButton = new QListWidgetItem(iconWidget); - playButton->setIcon(QIcon(":/images/openmw.png")); - playButton->setText(tr("Play")); - playButton->setTextAlignment(Qt::AlignCenter); - playButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - - QListWidgetItem *dataFilesButton = new QListWidgetItem(iconWidget); - dataFilesButton->setIcon(QIcon(":/images/openmw-plugin.png")); - dataFilesButton->setText(tr("Data Files")); - dataFilesButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); - dataFilesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - - QListWidgetItem *graphicsButton = new QListWidgetItem(iconWidget); - graphicsButton->setIcon(QIcon(":/images/preferences-video.png")); - graphicsButton->setText(tr("Graphics")); - graphicsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom | Qt::AlignAbsolute); - graphicsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - - QListWidgetItem *settingsButton = new QListWidgetItem(iconWidget); - settingsButton->setIcon(QIcon(":/images/preferences.png")); - settingsButton->setText(tr("Settings")); - settingsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); - settingsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - - QListWidgetItem *advancedButton = new QListWidgetItem(iconWidget); - advancedButton->setIcon(QIcon(":/images/preferences-advanced.png")); - advancedButton->setText(tr("Advanced")); - advancedButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); - advancedButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - - connect(iconWidget, - SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), - this, SLOT(changePage(QListWidgetItem*,QListWidgetItem*))); + QIcon::setThemeName("fallback"); + connect(dataAction, &QAction::triggered, this, &MainDialog::enableDataPage); + connect(graphicsAction, &QAction::triggered, this, &MainDialog::enableGraphicsPage); + connect(settingsAction, &QAction::triggered, this, &MainDialog::enableSettingsPage); + connect(importAction, &QAction::triggered, this, &MainDialog::enableImportPage); } void Launcher::MainDialog::createPages() @@ -121,33 +118,23 @@ void Launcher::MainDialog::createPages() if (pagesWidget->count() != 0) return; - mPlayPage = new PlayPage(this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mGraphicsPage = new GraphicsPage(this); - mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this); - mAdvancedPage = new AdvancedPage(mGameSettings, this); - - // Set the combobox of the play page to imitate the combobox on the datafilespage - mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); - mPlayPage->setProfilesIndex(mDataFilesPage->profilesIndex()); + mImportPage = new ImportPage(mCfgMgr, mGameSettings, mLauncherSettings, this); + mSettingsPage = new SettingsPage(mGameSettings, this); // Add the pages to the stacked widget - pagesWidget->addWidget(mPlayPage); pagesWidget->addWidget(mDataFilesPage); pagesWidget->addWidget(mGraphicsPage); pagesWidget->addWidget(mSettingsPage); - pagesWidget->addWidget(mAdvancedPage); + pagesWidget->addWidget(mImportPage); // Select the first page - iconWidget->setCurrentItem(iconWidget->item(0), QItemSelectionModel::Select); + dataAction->setChecked(true); - connect(mPlayPage, SIGNAL(playButtonClicked()), this, SLOT(play())); - - connect(mPlayPage, SIGNAL(signalProfileChanged(int)), mDataFilesPage, SLOT(slotProfileChanged(int))); - connect(mDataFilesPage, SIGNAL(signalProfileChanged(int)), mPlayPage, SLOT(setProfilesIndex(int))); // Using Qt::QueuedConnection because signal is emitted in a subthread and slot is in the main thread - connect(mDataFilesPage, SIGNAL(signalLoadedCellsChanged(QStringList)), mAdvancedPage, SLOT(slotLoadedCellsChanged(QStringList)), Qt::QueuedConnection); - + connect(mDataFilesPage, &DataFilesPage::signalLoadedCellsChanged, mSettingsPage, + &SettingsPage::slotLoadedCellsChanged, Qt::QueuedConnection); } Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() @@ -155,21 +142,37 @@ Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() if (!setupLauncherSettings()) return FirstRunDialogResultFailure; - if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) + // Dialog wizard and setup will fail if the config directory does not already exist + const auto& userConfigDir = mCfgMgr.getUserConfigPath(); + if (!exists(userConfigDir)) + { + std::error_code ec; + if (!create_directories(userConfigDir, ec)) + { + cfgError(tr("Error creating OpenMW configuration directory: code %0").arg(ec.value()), + tr("
Could not create directory %0

" + "%1
") + .arg(Files::pathToQString(userConfigDir)) + .arg(QString(ec.message().c_str()))); + return FirstRunDialogResultFailure; + } + } + + if (mLauncherSettings.isFirstRun()) { QMessageBox msgBox; msgBox.setWindowTitle(tr("First run")); msgBox.setIcon(QMessageBox::Question); msgBox.setStandardButtons(QMessageBox::NoButton); - msgBox.setText(tr("

Welcome to OpenMW!

\ -

It is recommended to run the Installation Wizard.

\ -

The Wizard will let you select an existing Morrowind installation, \ - or install Morrowind for OpenMW to use.

")); + msgBox.setText( + tr("

Welcome to OpenMW!

" + "

It is recommended to run the Installation Wizard.

" + "

The Wizard will let you select an existing Morrowind installation, " + "or install Morrowind for OpenMW to use.

")); - QAbstractButton *wizardButton = - msgBox.addButton(tr("Run &Installation Wizard"), QMessageBox::AcceptRole); // ActionRole doesn't work?! - QAbstractButton *skipButton = - msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); + QAbstractButton* wizardButton + = msgBox.addButton(tr("Run &Installation Wizard"), QMessageBox::AcceptRole); // ActionRole doesn't work?! + QAbstractButton* skipButton = msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); msgBox.exec(); @@ -187,7 +190,8 @@ Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() return FirstRunDialogResultFailure; } - if (!setup() || !setupGameData()) { + if (!setup() || !setupGameData()) + { return FirstRunDialogResultFailure; } return FirstRunDialogResultContinue; @@ -196,22 +200,22 @@ Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() void Launcher::MainDialog::setVersionLabel() { // Add version information to bottom of the window - Version::Version v = Version::getOpenmwVersion(mGameSettings.value("resources").toUtf8().constData()); - - QString revision(QString::fromUtf8(v.mCommitHash.c_str())); - QString tag(QString::fromUtf8(v.mTagHash.c_str())); + QString revision(QString::fromUtf8(Version::getCommitHash().data(), Version::getCommitHash().size())); + QString tag(QString::fromUtf8(Version::getTagHash().data(), Version::getTagHash().size())); versionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - if (!v.mVersion.empty() && (revision.isEmpty() || revision == tag)) - versionLabel->setText(tr("OpenMW %1 release").arg(QString::fromUtf8(v.mVersion.c_str()))); + if (!Version::getVersion().empty() && (revision.isEmpty() || revision == tag)) + versionLabel->setText( + tr("OpenMW %1 release").arg(QString::fromUtf8(Version::getVersion().data(), Version::getVersion().size()))); else versionLabel->setText(tr("OpenMW development (%1)").arg(revision.left(10))); // Add the compile date and time auto compileDate = QLocale(QLocale::C).toDate(QString(__DATE__).simplified(), QLatin1String("MMM d yyyy")); auto compileTime = QLocale(QLocale::C).toTime(QString(__TIME__).simplified(), QLatin1String("hh:mm:ss")); - versionLabel->setToolTip(tr("Compiled on %1 %2").arg(QLocale::system().toString(compileDate, QLocale::LongFormat), - QLocale::system().toString(compileTime, QLocale::ShortFormat))); + versionLabel->setToolTip(tr("Compiled on %1 %2") + .arg(QLocale::system().toString(compileDate, QLocale::LongFormat), + QLocale::system().toString(compileTime, QLocale::ShortFormat))); } bool Launcher::MainDialog::setup() @@ -251,7 +255,7 @@ bool Launcher::MainDialog::reloadSettings() if (!setupGraphicsSettings()) return false; - if (!mSettingsPage->loadSettings()) + if (!mImportPage->loadSettings()) return false; if (!mDataFilesPage->loadSettings()) @@ -260,54 +264,81 @@ bool Launcher::MainDialog::reloadSettings() if (!mGraphicsPage->loadSettings()) return false; - if (!mAdvancedPage->loadSettings()) + if (!mSettingsPage->loadSettings()) return false; return true; } -void Launcher::MainDialog::changePage(QListWidgetItem *current, QListWidgetItem *previous) +void Launcher::MainDialog::enableDataPage() { - if (!current) - current = previous; + pagesWidget->setCurrentIndex(0); + mImportPage->resetProgressBar(); + dataAction->setChecked(true); + graphicsAction->setChecked(false); + importAction->setChecked(false); + settingsAction->setChecked(false); +} - int currentIndex = iconWidget->row(current); - pagesWidget->setCurrentIndex(currentIndex); - mSettingsPage->resetProgressBar(); +void Launcher::MainDialog::enableGraphicsPage() +{ + pagesWidget->setCurrentIndex(1); + mImportPage->resetProgressBar(); + dataAction->setChecked(false); + graphicsAction->setChecked(true); + settingsAction->setChecked(false); + importAction->setChecked(false); +} + +void Launcher::MainDialog::enableSettingsPage() +{ + pagesWidget->setCurrentIndex(2); + mImportPage->resetProgressBar(); + dataAction->setChecked(false); + graphicsAction->setChecked(false); + settingsAction->setChecked(true); + importAction->setChecked(false); +} + +void Launcher::MainDialog::enableImportPage() +{ + pagesWidget->setCurrentIndex(3); + mImportPage->resetProgressBar(); + dataAction->setChecked(false); + graphicsAction->setChecked(false); + settingsAction->setChecked(false); + importAction->setChecked(true); } bool Launcher::MainDialog::setupLauncherSettings() { mLauncherSettings.clear(); - mLauncherSettings.setMultiValueEnabled(true); + const QString path + = Files::pathToQString(mCfgMgr.getUserConfigPath() / Config::LauncherSettings::sLauncherConfigFileName); - QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); + if (!QFile::exists(path)) + return true; - QStringList paths; - paths.append(QString(Config::LauncherSettings::sLauncherConfigFileName)); - paths.append(userPath + QString(Config::LauncherSettings::sLauncherConfigFileName)); + Log(Debug::Info) << "Loading config file: " << path.toUtf8().constData(); - for (const QString &path : paths) + QFile file(path); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qDebug() << "Loading config file:" << path.toUtf8().constData(); - QFile file(path); - if (file.exists()) { - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - cfgError(tr("Error opening OpenMW configuration file"), - tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - return false; - } - QTextStream stream(&file); - stream.setCodec(QTextCodec::codecForName("UTF-8")); - - mLauncherSettings.readFile(stream); - } - file.close(); + cfgError(tr("Error opening OpenMW configuration file"), + tr("
Could not open %0 for reading:

%1

" + "Please make sure you have the right permissions " + "and try again.
") + .arg(file.fileName()) + .arg(file.errorString())); + return false; } + QTextStream stream(&file); + Misc::ensureUtf8Encoding(stream); + + mLauncherSettings.readFile(stream); + return true; } @@ -315,57 +346,42 @@ bool Launcher::MainDialog::setupGameSettings() { mGameSettings.clear(); - QString localPath = QString::fromUtf8(mCfgMgr.getLocalPath().string().c_str()); - QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); - QString globalPath = QString::fromUtf8(mCfgMgr.getGlobalPath().string().c_str()); + QFile file; - // Load the user config file first, separately - // So we can write it properly, uncontaminated - QString path = userPath + QLatin1String("openmw.cfg"); - QFile file(path); - - qDebug() << "Loading config file:" << path.toUtf8().constData(); - - if (file.exists()) { - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - cfgError(tr("Error opening OpenMW configuration file"), - tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - return false; - } - QTextStream stream(&file); - stream.setCodec(QTextCodec::codecForName("UTF-8")); - - mGameSettings.readUserFile(stream); - file.close(); - } - - // Now the rest - priority: user > local > global - QStringList paths; - paths.append(globalPath + QString("openmw.cfg")); - paths.append(localPath + QString("openmw.cfg")); - paths.append(userPath + QString("openmw.cfg")); - - for (const QString &path2 : paths) - { - qDebug() << "Loading config file:" << path2.toUtf8().constData(); - - file.setFileName(path2); - if (file.exists()) { - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + auto loadFile = [&](const QString& path, bool (Config::GameSettings::*reader)(QTextStream&, const QString&, bool), + bool ignoreContent = false) -> std::optional { + file.setFileName(path); + if (file.exists()) + { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { cfgError(tr("Error opening OpenMW configuration file"), - tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - return false; + tr("
Could not open %0 for reading

" + "Please make sure you have the right permissions " + "and try again.
") + .arg(file.fileName())); + return {}; } QTextStream stream(&file); - stream.setCodec(QTextCodec::codecForName("UTF-8")); + Misc::ensureUtf8Encoding(stream); - mGameSettings.readFile(stream); + (mGameSettings.*reader)(stream, QFileInfo(path).dir().path(), ignoreContent); file.close(); + return true; } + return false; + }; + + // Load the user config file first, separately + // So we can write it properly, uncontaminated + if (!loadFile(Files::getUserConfigPathQString(mCfgMgr), &Config::GameSettings::readUserFile)) + return false; + + for (const auto& path : Files::getActiveConfigPathsQString(mCfgMgr)) + { + Log(Debug::Info) << "Loading config file: " << path.toUtf8().constData(); + if (!loadFile(path, &Config::GameSettings::readFile)) + return false; } return true; @@ -373,32 +389,37 @@ bool Launcher::MainDialog::setupGameSettings() bool Launcher::MainDialog::setupGameData() { - QStringList dataDirs; + bool foundData = false; // Check if the paths actually contain data files - for (const QString& path3 : mGameSettings.getDataDirs()) + for (const auto& path3 : mGameSettings.getDataDirs()) { - QDir dir(path3); + QDir dir(path3.value); QStringList filters; - filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; + filters << "*.esp" + << "*.esm" + << "*.omwgame" + << "*.omwaddon"; if (!dir.entryList(filters).isEmpty()) - dataDirs.append(path3); + { + foundData = true; + break; + } } - if (dataDirs.isEmpty()) + if (!foundData) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Error detecting Morrowind installation")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::NoButton); - msgBox.setText(tr("
Could not find the Data Files location

\ - The directory containing the data files was not found.")); + msgBox.setText( + tr("
Could not find the Data Files location

" + "The directory containing the data files was not found.")); - QAbstractButton *wizardButton = - msgBox.addButton(tr("Run &Installation Wizard..."), QMessageBox::ActionRole); - QAbstractButton *skipButton = - msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); + QAbstractButton* wizardButton = msgBox.addButton(tr("Run &Installation Wizard..."), QMessageBox::ActionRole); + QAbstractButton* skipButton = msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); Q_UNUSED(skipButton); // Suppress compiler unused warning @@ -416,87 +437,38 @@ bool Launcher::MainDialog::setupGameData() bool Launcher::MainDialog::setupGraphicsSettings() { - // This method is almost a copy of OMW::Engine::loadSettings(). They should definitely - // remain consistent, and possibly be merged into a shared component. At the very least - // the filenames should be in the CfgMgr component. - - // Ensure to clear previous settings in case we had already loaded settings. - mEngineSettings.clear(); - - // Create the settings manager and load default settings file - const std::string localDefault = (mCfgMgr.getLocalPath() / "defaults.bin").string(); - const std::string globalDefault = (mCfgMgr.getGlobalPath() / "defaults.bin").string(); - std::string defaultPath; - - // Prefer the defaults.bin in the current directory. - if (boost::filesystem::exists(localDefault)) - defaultPath = localDefault; - else if (boost::filesystem::exists(globalDefault)) - defaultPath = globalDefault; - // Something's very wrong if we can't find the file at all. - else { - cfgError(tr("Error reading OpenMW configuration file"), - tr("
Could not find defaults.bin

\ - The problem may be due to an incomplete installation of OpenMW.
\ - Reinstalling OpenMW may resolve the problem.")); - return false; - } - - // Load the default settings, report any parsing errors. - try { - mEngineSettings.loadDefault(defaultPath); - } - catch (std::exception& e) { - std::string msg = std::string("
Error reading defaults.bin

") + e.what(); - cfgError(tr("Error reading OpenMW configuration file"), tr(msg.c_str())); - return false; - } - - // Load user settings if they exist - const std::string userPath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); - // User settings are not required to exist, so if they don't we're done. - if (!boost::filesystem::exists(userPath)) return true; - - try { - mEngineSettings.loadUser(userPath); + Settings::Manager::clear(); // Ensure to clear previous settings in case we had already loaded settings. + try + { + Settings::Manager::load(mCfgMgr); + return true; } - catch (std::exception& e) { - std::string msg = std::string("
Error reading settings.cfg

") + e.what(); - cfgError(tr("Error reading OpenMW configuration file"), tr(msg.c_str())); + catch (std::exception& e) + { + cfgError(tr("Error reading OpenMW configuration files"), + tr("
The problem may be due to an incomplete installation of OpenMW.
" + "Reinstalling OpenMW may resolve the problem.
") + + e.what()); return false; } - - return true; } void Launcher::MainDialog::loadSettings() { - int width = mLauncherSettings.value(QString("General/MainWindow/width")).toInt(); - int height = mLauncherSettings.value(QString("General/MainWindow/height")).toInt(); - - int posX = mLauncherSettings.value(QString("General/MainWindow/posx")).toInt(); - int posY = mLauncherSettings.value(QString("General/MainWindow/posy")).toInt(); - - resize(width, height); - move(posX, posY); + const auto& mainWindow = mLauncherSettings.getMainWindow(); + resize(mainWindow.mWidth, mainWindow.mHeight); + move(mainWindow.mPosX, mainWindow.mPosY); } void Launcher::MainDialog::saveSettings() { - QString width = QString::number(this->width()); - QString height = QString::number(this->height()); - - mLauncherSettings.setValue(QString("General/MainWindow/width"), width); - mLauncherSettings.setValue(QString("General/MainWindow/height"), height); - - QString posX = QString::number(this->pos().x()); - QString posY = QString::number(this->pos().y()); - - mLauncherSettings.setValue(QString("General/MainWindow/posx"), posX); - mLauncherSettings.setValue(QString("General/MainWindow/posy"), posY); - - mLauncherSettings.setValue(QString("General/firstrun"), QString("false")); - + mLauncherSettings.setMainWindow(Config::LauncherSettings::MainWindow{ + .mWidth = width(), + .mHeight = height(), + .mPosX = pos().x(), + .mPosY = pos().y(), + }); + mLauncherSettings.resetFirstRun(); } bool Launcher::MainDialog::writeSettings() @@ -505,65 +477,77 @@ bool Launcher::MainDialog::writeSettings() saveSettings(); mDataFilesPage->saveSettings(); mGraphicsPage->saveSettings(); + mImportPage->saveSettings(); mSettingsPage->saveSettings(); - mAdvancedPage->saveSettings(); - QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); - QDir dir(userPath); + const auto& userPath = mCfgMgr.getUserConfigPath(); - if (!dir.exists()) { - if (!dir.mkpath(userPath)) { - cfgError(tr("Error creating OpenMW configuration directory"), - tr("
Could not create %0

\ - Please make sure you have the right permissions \ - and try again.
").arg(userPath)); + if (!exists(userPath)) + { + std::error_code ec; + if (!create_directories(userPath, ec)) + { + cfgError(tr("Error creating OpenMW configuration directory: code %0").arg(ec.value()), + tr("
Could not create directory %0

" + "%1
") + .arg(Files::pathToQString(userPath)) + .arg(QString(ec.message().c_str()))); return false; } } // Game settings - QFile file(userPath + QString("openmw.cfg")); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QFile file(userPath / Files::openmwCfgFile); +#else + QFile file(Files::getUserConfigPathQString(mCfgMgr)); +#endif - if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { + if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) + { // File cannot be opened or created cfgError(tr("Error writing OpenMW configuration file"), - tr("
Could not open or create %0 for writing

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); + tr("
Could not open or create %0 for writing

" + "Please make sure you have the right permissions " + "and try again.
") + .arg(file.fileName())); return false; } - mGameSettings.writeFileWithComments(file); file.close(); // Graphics settings - const std::string settingsPath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); - try { - mEngineSettings.saveUser(settingsPath); + const auto settingsPath = mCfgMgr.getUserConfigPath() / "settings.cfg"; + try + { + Settings::Manager::saveUser(settingsPath); } - catch (std::exception& e) { - std::string msg = "
Error writing settings.cfg

" + - settingsPath + "

" + e.what(); + catch (std::exception& e) + { + std::string msg = "
Error writing settings.cfg

" + Files::pathToUnicodeString(settingsPath) + + "

" + e.what(); cfgError(tr("Error writing user settings file"), tr(msg.c_str())); return false; } // Launcher settings - file.setFileName(userPath + QString(Config::LauncherSettings::sLauncherConfigFileName)); + file.setFileName(Files::pathToQString(userPath / Config::LauncherSettings::sLauncherConfigFileName)); - if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { + if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) + { // File cannot be opened or created cfgError(tr("Error writing Launcher configuration file"), - tr("
Could not open or create %0 for writing

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); + tr("
Could not open or create %0 for writing

" + "Please make sure you have the right permissions " + "and try again.
") + .arg(file.fileName())); return false; } QTextStream stream(&file); stream.setDevice(&file); - stream.setCodec(QTextCodec::codecForName("UTF-8")); + Misc::ensureUtf8Encoding(stream); mLauncherSettings.writeFile(stream); file.close(); @@ -571,7 +555,7 @@ bool Launcher::MainDialog::writeSettings() return true; } -void Launcher::MainDialog::closeEvent(QCloseEvent *event) +void Launcher::MainDialog::closeEvent(QCloseEvent* event) { writeSettings(); event->accept(); @@ -599,14 +583,16 @@ void Launcher::MainDialog::play() if (!writeSettings()) return qApp->quit(); - if (!mGameSettings.hasMaster()) { + if (!mGameSettings.hasMaster()) + { QMessageBox msgBox; msgBox.setWindowTitle(tr("No game file selected")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
You do not have a game file selected.

\ - OpenMW will not start without a game file selected.
")); - msgBox.exec(); + msgBox.setText( + tr("
You do not have a game file selected.

" + "OpenMW will not start without a game file selected.
")); + msgBox.exec(); return; } diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index 80e014e2852..5ceee966bae 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -1,34 +1,32 @@ #ifndef MAINDIALOG_H #define MAINDIALOG_H - #ifndef Q_MOC_RUN -#include - - #include #include #include -#include #endif #include "ui_mainwindow.h" class QListWidgetItem; class QStackedWidget; -class QStringList; class QStringListModel; class QString; +namespace Files +{ + struct ConfigurationManager; +} + namespace Launcher { - class PlayPage; class GraphicsPage; class DataFilesPage; class UnshieldThread; + class ImportPage; class SettingsPage; - class AdvancedPage; enum FirstRunDialogResult { @@ -46,8 +44,8 @@ namespace Launcher Q_OBJECT public: - explicit MainDialog(QWidget *parent = nullptr); - ~MainDialog(); + explicit MainDialog(const Files::ConfigurationManager& configurationManager, QWidget* parent = nullptr); + ~MainDialog() override; FirstRunDialogResult showFirstRunDialog(); @@ -55,10 +53,16 @@ namespace Launcher bool writeSettings(); public slots: - void changePage(QListWidgetItem *current, QListWidgetItem *previous); + void enableDataPage(); + void enableGraphicsPage(); + void enableSettingsPage(); + void enableImportPage(); void play(); void help(); + protected: + bool event(QEvent* event) override; + private slots: void wizardStarted(); void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); @@ -79,26 +83,26 @@ namespace Launcher void loadSettings(); void saveSettings(); - inline bool startProgram(const QString &name, bool detached = false) { return startProgram(name, QStringList(), detached); } - bool startProgram(const QString &name, const QStringList &arguments, bool detached = false); + inline bool startProgram(const QString& name, bool detached = false) + { + return startProgram(name, QStringList(), detached); + } + bool startProgram(const QString& name, const QStringList& arguments, bool detached = false); - void closeEvent(QCloseEvent *event) override; + void closeEvent(QCloseEvent* event) override; - PlayPage *mPlayPage; - GraphicsPage *mGraphicsPage; - DataFilesPage *mDataFilesPage; - SettingsPage *mSettingsPage; - AdvancedPage *mAdvancedPage; + GraphicsPage* mGraphicsPage; + DataFilesPage* mDataFilesPage; + ImportPage* mImportPage; + SettingsPage* mSettingsPage; - Process::ProcessInvoker *mGameInvoker; - Process::ProcessInvoker *mWizardInvoker; + Process::ProcessInvoker* mGameInvoker; + Process::ProcessInvoker* mWizardInvoker; - Files::ConfigurationManager mCfgMgr; + const Files::ConfigurationManager& mCfgMgr; Config::GameSettings mGameSettings; - Settings::Manager mEngineSettings; Config::LauncherSettings mLauncherSettings; - }; } #endif diff --git a/apps/launcher/playpage.cpp b/apps/launcher/playpage.cpp deleted file mode 100644 index 99b74fdd39d..00000000000 --- a/apps/launcher/playpage.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "playpage.hpp" - -#include - -Launcher::PlayPage::PlayPage(QWidget *parent) : QWidget(parent) -{ - setObjectName ("PlayPage"); - setupUi(this); - - profilesComboBox->setView(new QListView()); - - connect(profilesComboBox, SIGNAL(activated(int)), this, SIGNAL (signalProfileChanged(int))); - connect(playButton, SIGNAL(clicked()), this, SLOT(slotPlayClicked())); - -} - -void Launcher::PlayPage::setProfilesModel(QAbstractItemModel *model) -{ - profilesComboBox->setModel(model); -} - -void Launcher::PlayPage::setProfilesIndex(int index) -{ - profilesComboBox->setCurrentIndex(index); -} - -void Launcher::PlayPage::slotPlayClicked() -{ - emit playButtonClicked(); -} diff --git a/apps/launcher/playpage.hpp b/apps/launcher/playpage.hpp deleted file mode 100644 index 8f414dc6aa6..00000000000 --- a/apps/launcher/playpage.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef PLAYPAGE_H -#define PLAYPAGE_H - -#include "ui_playpage.h" - -class QComboBox; -class QPushButton; -class QAbstractItemModel; - -namespace Launcher -{ - class PlayPage : public QWidget, private Ui::PlayPage - { - Q_OBJECT - - public: - PlayPage(QWidget *parent = nullptr); - void setProfilesModel(QAbstractItemModel *model); - - signals: - void signalProfileChanged(int index); - void playButtonClicked(); - - public slots: - void setProfilesIndex(int index); - - private slots: - void slotPlayClicked(); - - - - }; -} -#endif diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index ca7fd028a75..dfddc45bc54 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -1,275 +1,581 @@ #include "settingspage.hpp" -#include -#include -#include +#include +#include +#include -#include +#include +#include +#include #include -#include -#include "utils/textinputdialog.hpp" -#include "datafilespage.hpp" +#include -using namespace Process; +#include "utils/openalutil.hpp" -Launcher::SettingsPage::SettingsPage(Files::ConfigurationManager &cfg, - Config::GameSettings &gameSettings, - Config::LauncherSettings &launcherSettings, MainDialog *parent) - : QWidget(parent) - , mCfgMgr(cfg) - , mGameSettings(gameSettings) - , mLauncherSettings(launcherSettings) - , mMain(parent) +namespace { - setupUi(this); - - QStringList languages; - languages << tr("English") - << tr("French") - << tr("German") - << tr("Italian") - << tr("Polish") - << tr("Russian") - << tr("Spanish"); - - languageComboBox->addItems(languages); - - mWizardInvoker = new ProcessInvoker(); - mImporterInvoker = new ProcessInvoker(); - resetProgressBar(); - - connect(mWizardInvoker->getProcess(), SIGNAL(started()), - this, SLOT(wizardStarted())); + void loadSettingBool(const Settings::SettingValue& value, QCheckBox& checkbox) + { + checkbox.setCheckState(value ? Qt::Checked : Qt::Unchecked); + } - connect(mWizardInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), - this, SLOT(wizardFinished(int,QProcess::ExitStatus))); + void saveSettingBool(const QCheckBox& checkbox, Settings::SettingValue& value) + { + value.set(checkbox.checkState() == Qt::Checked); + } - connect(mImporterInvoker->getProcess(), SIGNAL(started()), - this, SLOT(importerStarted())); + void loadSettingInt(const Settings::SettingValue& value, QComboBox& comboBox) + { + comboBox.setCurrentIndex(value); + } - connect(mImporterInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), - this, SLOT(importerFinished(int,QProcess::ExitStatus))); + void loadSettingInt(const Settings::SettingValue& value, QComboBox& comboBox) + { + comboBox.setCurrentIndex(static_cast(value.get())); + } - mProfileDialog = new TextInputDialog(tr("New Content List"), tr("Content List name:"), this); + void saveSettingInt(const QComboBox& comboBox, Settings::SettingValue& value) + { + value.set(comboBox.currentIndex()); + } - connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), - this, SLOT(updateOkButton(QString))); + void saveSettingInt(const QComboBox& comboBox, Settings::SettingValue& value) + { + value.set(static_cast(comboBox.currentIndex())); + } - // Detect Morrowind configuration files - QStringList iniPaths; + void loadSettingInt(const Settings::SettingValue& value, QSpinBox& spinBox) + { + spinBox.setValue(value); + } - for (const QString &path : mGameSettings.getDataDirs()) + void saveSettingInt(const QSpinBox& spinBox, Settings::SettingValue& value) { - QDir dir(path); - dir.setPath(dir.canonicalPath()); // Resolve symlinks + value.set(spinBox.value()); + } - if (dir.exists(QString("Morrowind.ini"))) - iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); - else + int toIndex(Settings::HrtfMode value) + { + switch (value) { - if (!dir.cdUp()) - continue; // Cannot move from Data Files - - if (dir.exists(QString("Morrowind.ini"))) - iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); + case Settings::HrtfMode::Auto: + return 0; + case Settings::HrtfMode::Disable: + return 1; + case Settings::HrtfMode::Enable: + return 2; } + return 0; } +} - if (!iniPaths.isEmpty()) { - settingsComboBox->addItems(iniPaths); - importerButton->setEnabled(true); - } else { - importerButton->setEnabled(false); +Launcher::SettingsPage::SettingsPage(Config::GameSettings& gameSettings, QWidget* parent) + : QWidget(parent) + , mGameSettings(gameSettings) +{ + setObjectName("SettingsPage"); + setupUi(this); + + for (const std::string& name : Launcher::enumerateOpenALDevices()) + { + audioDeviceSelectorComboBox->addItem(QString::fromStdString(name), QString::fromStdString(name)); + } + for (const std::string& name : Launcher::enumerateOpenALDevicesHrtf()) + { + hrtfProfileSelectorComboBox->addItem(QString::fromStdString(name), QString::fromStdString(name)); } loadSettings(); + + mCellNameCompleter.setModel(&mCellNameCompleterModel); + startDefaultCharacterAtField->setCompleter(&mCellNameCompleter); } -Launcher::SettingsPage::~SettingsPage() +void Launcher::SettingsPage::loadCellsForAutocomplete(QStringList cellNames) { - delete mWizardInvoker; - delete mImporterInvoker; + // Update the list of suggestions for the "Start default character at" field + mCellNameCompleterModel.setStringList(cellNames); + mCellNameCompleter.setCompletionMode(QCompleter::PopupCompletion); + mCellNameCompleter.setCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); } -void Launcher::SettingsPage::on_wizardButton_clicked() +void Launcher::SettingsPage::on_skipMenuCheckBox_stateChanged(int state) { - mMain->writeSettings(); - - if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) - return; + startDefaultCharacterAtLabel->setEnabled(state == Qt::Checked); + startDefaultCharacterAtField->setEnabled(state == Qt::Checked); } -void Launcher::SettingsPage::on_importerButton_clicked() +void Launcher::SettingsPage::on_runScriptAfterStartupBrowseButton_clicked() { - mMain->writeSettings(); - - // Create the file if it doesn't already exist, else the importer will fail - QString path(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); - path.append(QLatin1String("openmw.cfg")); - QFile file(path); - - if (!file.exists()) { - if (!file.open(QIODevice::ReadWrite)) { - // File cannot be created - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("

Could not open or create %1 for writing

\ -

Please make sure you have the right permissions \ - and try again.

").arg(file.fileName())); - msgBox.exec(); - return; - } + QString scriptFile = QFileDialog::getOpenFileName( + this, QObject::tr("Select script file"), QDir::currentPath(), QString(tr("Text file (*.txt)"))); - file.close(); - } + if (scriptFile.isEmpty()) + return; + + QFileInfo info(scriptFile); - // Construct the arguments to run the importer - QStringList arguments; + if (!info.exists() || !info.isReadable()) + return; - if (addonsCheckBox->isChecked()) - arguments.append(QString("--game-files")); + const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); + runScriptAfterStartupField->setText(path); +} - arguments.append(QString("--encoding")); - arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); - arguments.append(QString("--ini")); - arguments.append(settingsComboBox->currentText()); - arguments.append(QString("--cfg")); - arguments.append(path); +namespace +{ + constexpr double CellSizeInUnits = 8192; - qDebug() << "arguments " << arguments; + double convertToCells(double unitRadius) + { + return unitRadius / CellSizeInUnits; + } - // start the progress bar as a "bouncing ball" - progressBar->setMaximum(0); - progressBar->setValue(0); - if (!mImporterInvoker->startProcess(QLatin1String("openmw-iniimporter"), arguments, false)) + int convertToUnits(double CellGridRadius) { - resetProgressBar(); + return static_cast(CellSizeInUnits * CellGridRadius); } } -void Launcher::SettingsPage::on_browseButton_clicked() +bool Launcher::SettingsPage::loadSettings() { - QString iniFile = QFileDialog::getOpenFileName( - this, - QObject::tr("Select configuration file"), - QDir::currentPath(), - QString(tr("Morrowind configuration file (*.ini)"))); + // Game mechanics + { + loadSettingBool(Settings::game().mCanLootDuringDeathAnimation, *canLootDuringDeathAnimationCheckBox); + loadSettingBool(Settings::game().mFollowersAttackOnSight, *followersAttackOnSightCheckBox); + loadSettingBool(Settings::game().mRebalanceSoulGemValues, *rebalanceSoulGemValuesCheckBox); + loadSettingBool(Settings::game().mEnchantedWeaponsAreMagical, *enchantedWeaponsMagicalCheckBox); + loadSettingBool( + Settings::game().mBarterDispositionChangeIsPermanent, *permanentBarterDispositionChangeCheckBox); + loadSettingBool(Settings::game().mClassicReflectedAbsorbSpellsBehavior, *classicReflectedAbsorbSpellsCheckBox); + loadSettingBool(Settings::game().mClassicCalmSpellsBehavior, *classicCalmSpellsCheckBox); + loadSettingBool( + Settings::game().mOnlyAppropriateAmmunitionBypassesResistance, *requireAppropriateAmmunitionCheckBox); + loadSettingBool(Settings::game().mUncappedDamageFatigue, *uncappedDamageFatigueCheckBox); + loadSettingBool(Settings::game().mNormaliseRaceSpeed, *normaliseRaceSpeedCheckBox); + loadSettingBool(Settings::game().mSwimUpwardCorrection, *swimUpwardCorrectionCheckBox); + loadSettingBool(Settings::game().mNPCsAvoidCollisions, *avoidCollisionsCheckBox); + loadSettingInt(Settings::game().mStrengthInfluencesHandToHand, *unarmedFactorsStrengthComboBox); + loadSettingBool(Settings::game().mAlwaysAllowStealingFromKnockedOutActors, *stealingFromKnockedOutCheckBox); + loadSettingBool(Settings::navigator().mEnable, *enableNavigatorCheckBox); + loadSettingInt(Settings::physics().mAsyncNumThreads, *physicsThreadsSpinBox); + loadSettingBool( + Settings::game().mAllowActorsToFollowOverWaterSurface, *allowNPCToFollowOverWaterSurfaceCheckBox); + loadSettingBool( + Settings::game().mUnarmedCreatureAttacksDamageArmor, *unarmedCreatureAttacksDamageArmorCheckBox); + loadSettingInt(Settings::game().mActorCollisionShapeType, *actorCollisonShapeTypeComboBox); + } + // Visuals + { + loadSettingBool(Settings::shaders().mAutoUseObjectNormalMaps, *autoUseObjectNormalMapsCheckBox); + loadSettingBool(Settings::shaders().mAutoUseObjectSpecularMaps, *autoUseObjectSpecularMapsCheckBox); + loadSettingBool(Settings::shaders().mAutoUseTerrainNormalMaps, *autoUseTerrainNormalMapsCheckBox); + loadSettingBool(Settings::shaders().mAutoUseTerrainSpecularMaps, *autoUseTerrainSpecularMapsCheckBox); + loadSettingBool(Settings::shaders().mApplyLightingToEnvironmentMaps, *bumpMapLocalLightingCheckBox); + loadSettingBool(Settings::shaders().mSoftParticles, *softParticlesCheckBox); + loadSettingBool(Settings::shaders().mAntialiasAlphaTest, *antialiasAlphaTestCheckBox); + if (Settings::shaders().mAntialiasAlphaTest == 0) + antialiasAlphaTestCheckBox->setCheckState(Qt::Unchecked); + loadSettingBool(Settings::shaders().mAdjustCoverageForAlphaTest, *adjustCoverageForAlphaTestCheckBox); + loadSettingBool(Settings::shaders().mWeatherParticleOcclusion, *weatherParticleOcclusionCheckBox); + loadSettingBool(Settings::game().mUseMagicItemAnimations, *magicItemAnimationsCheckBox); + connect(animSourcesCheckBox, &QCheckBox::toggled, this, &SettingsPage::slotAnimSourcesToggled); + loadSettingBool(Settings::game().mUseAdditionalAnimSources, *animSourcesCheckBox); + if (animSourcesCheckBox->checkState() != Qt::Unchecked) + { + loadSettingBool(Settings::game().mWeaponSheathing, *weaponSheathingCheckBox); + loadSettingBool(Settings::game().mShieldSheathing, *shieldSheathingCheckBox); + } + loadSettingBool(Settings::game().mSmoothAnimTransitions, *smoothAnimTransitionsCheckBox); + loadSettingBool(Settings::game().mTurnToMovementDirection, *turnToMovementDirectionCheckBox); + loadSettingBool(Settings::game().mSmoothMovement, *smoothMovementCheckBox); + loadSettingBool(Settings::game().mPlayerMovementIgnoresAnimation, *playerMovementIgnoresAnimationCheckBox); + + connect(distantLandCheckBox, &QCheckBox::toggled, this, &SettingsPage::slotDistantLandToggled); + bool distantLandEnabled = Settings::terrain().mDistantTerrain && Settings::terrain().mObjectPaging; + distantLandCheckBox->setCheckState(distantLandEnabled ? Qt::Checked : Qt::Unchecked); + slotDistantLandToggled(distantLandEnabled); + + loadSettingBool(Settings::terrain().mObjectPagingActiveGrid, *activeGridObjectPagingCheckBox); + viewingDistanceComboBox->setValue(convertToCells(Settings::camera().mViewingDistance)); + objectPagingMinSizeComboBox->setValue(Settings::terrain().mObjectPagingMinSize); + + loadSettingBool(Settings::game().mDayNightSwitches, *nightDaySwitchesCheckBox); + + connect(postprocessEnabledCheckBox, &QCheckBox::toggled, this, &SettingsPage::slotPostProcessToggled); + loadSettingBool(Settings::postProcessing().mEnabled, *postprocessEnabledCheckBox); + loadSettingBool(Settings::postProcessing().mTransparentPostpass, *postprocessTransparentPostpassCheckBox); + postprocessHDRTimeComboBox->setValue(Settings::postProcessing().mAutoExposureSpeed); + + connect(skyBlendingCheckBox, &QCheckBox::toggled, this, &SettingsPage::slotSkyBlendingToggled); + loadSettingBool(Settings::fog().mRadialFog, *radialFogCheckBox); + loadSettingBool(Settings::fog().mExponentialFog, *exponentialFogCheckBox); + loadSettingBool(Settings::fog().mSkyBlending, *skyBlendingCheckBox); + skyBlendingStartComboBox->setValue(Settings::fog().mSkyBlendingStart); + + loadSettingBool(Settings::shadows().mActorShadows, *actorShadowsCheckBox); + loadSettingBool(Settings::shadows().mPlayerShadows, *playerShadowsCheckBox); + loadSettingBool(Settings::shadows().mTerrainShadows, *terrainShadowsCheckBox); + loadSettingBool(Settings::shadows().mObjectShadows, *objectShadowsCheckBox); + loadSettingBool(Settings::shadows().mEnableIndoorShadows, *indoorShadowsCheckBox); + + const auto& boundMethod = Settings::shadows().mComputeSceneBounds.get(); + if (boundMethod == "bounds") + shadowComputeSceneBoundsComboBox->setCurrentIndex(0); + else if (boundMethod == "primitives") + shadowComputeSceneBoundsComboBox->setCurrentIndex(1); + else + shadowComputeSceneBoundsComboBox->setCurrentIndex(2); - if (iniFile.isEmpty()) - return; + const int shadowDistLimit = Settings::shadows().mMaximumShadowMapDistance; + if (shadowDistLimit > 0) + { + shadowDistanceCheckBox->setCheckState(Qt::Checked); + shadowDistanceSpinBox->setValue(shadowDistLimit); + shadowDistanceSpinBox->setEnabled(true); + fadeStartSpinBox->setEnabled(true); + } - QFileInfo info(iniFile); + const float shadowFadeStart = Settings::shadows().mShadowFadeStart; + if (shadowFadeStart != 0) + fadeStartSpinBox->setValue(shadowFadeStart); - if (!info.exists() || !info.isReadable()) - return; + const int shadowRes = Settings::shadows().mShadowMapResolution; + int shadowResIndex = shadowResolutionComboBox->findText(QString::number(shadowRes)); + if (shadowResIndex != -1) + shadowResolutionComboBox->setCurrentIndex(shadowResIndex); + else + { + shadowResolutionComboBox->addItem(QString::number(shadowRes)); + shadowResolutionComboBox->setCurrentIndex(shadowResolutionComboBox->count() - 1); + } - const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); + connect(shadowDistanceCheckBox, &QCheckBox::toggled, this, &SettingsPage::slotShadowDistLimitToggled); - if (settingsComboBox->findText(path) == -1) { - settingsComboBox->addItem(path); - settingsComboBox->setCurrentIndex(settingsComboBox->findText(path)); - importerButton->setEnabled(true); + int lightingMethod = 1; + switch (Settings::shaders().mLightingMethod) + { + case SceneUtil::LightingMethod::FFP: + lightingMethod = 0; + break; + case SceneUtil::LightingMethod::PerObjectUniform: + lightingMethod = 1; + break; + case SceneUtil::LightingMethod::SingleUBO: + lightingMethod = 2; + break; + } + lightingMethodComboBox->setCurrentIndex(lightingMethod); } -} -void Launcher::SettingsPage::wizardStarted() -{ - mMain->hide(); // Hide the launcher + // Audio + { + const std::string& selectedAudioDevice = Settings::sound().mDevice; + if (selectedAudioDevice.empty() == false) + { + int audioDeviceIndex = audioDeviceSelectorComboBox->findData(QString::fromStdString(selectedAudioDevice)); + if (audioDeviceIndex != -1) + { + audioDeviceSelectorComboBox->setCurrentIndex(audioDeviceIndex); + } + } + enableHRTFComboBox->setCurrentIndex(toIndex(Settings::sound().mHrtfEnable)); + const std::string& selectedHRTFProfile = Settings::sound().mHrtf; + if (selectedHRTFProfile.empty() == false) + { + int hrtfProfileIndex = hrtfProfileSelectorComboBox->findData(QString::fromStdString(selectedHRTFProfile)); + if (hrtfProfileIndex != -1) + { + hrtfProfileSelectorComboBox->setCurrentIndex(hrtfProfileIndex); + } + } + loadSettingBool(Settings::sound().mCameraListener, *cameraListenerCheckBox); + } - wizardButton->setEnabled(false); -} + // Interface Changes + { + loadSettingBool(Settings::game().mShowEffectDuration, *showEffectDurationCheckBox); + loadSettingBool(Settings::game().mShowEnchantChance, *showEnchantChanceCheckBox); + loadSettingBool(Settings::game().mShowMeleeInfo, *showMeleeInfoCheckBox); + loadSettingBool(Settings::game().mShowProjectileDamage, *showProjectileDamageCheckBox); + loadSettingBool(Settings::gui().mColorTopicEnable, *changeDialogTopicsCheckBox); + showOwnedComboBox->setCurrentIndex(Settings::game().mShowOwned); + loadSettingBool(Settings::gui().mStretchMenuBackground, *stretchBackgroundCheckBox); + loadSettingBool(Settings::map().mAllowZooming, *useZoomOnMapCheckBox); + loadSettingBool(Settings::game().mGraphicHerbalism, *graphicHerbalismCheckBox); + scalingSpinBox->setValue(Settings::gui().mScalingFactor); + fontSizeSpinBox->setValue(Settings::gui().mFontSize); + } -void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) -{ - if (exitCode != 0 || exitStatus == QProcess::CrashExit) - return qApp->quit(); + // Bug fixes + { + loadSettingBool(Settings::game().mPreventMerchantEquipping, *preventMerchantEquippingCheckBox); + loadSettingBool( + Settings::game().mTrainersTrainingSkillsBasedOnBaseSkill, *trainersTrainingSkillsBasedOnBaseSkillCheckBox); + } - mMain->reloadSettings(); - wizardButton->setEnabled(true); + // Miscellaneous + { + // Saves + loadSettingInt(Settings::saves().mMaxQuicksaves, *maximumQuicksavesComboBox); - mMain->show(); // Show the launcher again -} + // Other Settings + QString screenshotFormatString = QString::fromStdString(Settings::general().mScreenshotFormat).toUpper(); + if (screenshotFormatComboBox->findText(screenshotFormatString) == -1) + screenshotFormatComboBox->addItem(screenshotFormatString); + screenshotFormatComboBox->setCurrentIndex(screenshotFormatComboBox->findText(screenshotFormatString)); -void Launcher::SettingsPage::importerStarted() -{ - importerButton->setEnabled(false); + loadSettingBool(Settings::general().mNotifyOnSavedScreenshot, *notifyOnSavedScreenshotCheckBox); + } + + // Testing + { + loadSettingBool(Settings::input().mGrabCursor, *grabCursorCheckBox); + + bool skipMenu = mGameSettings.value("skip-menu").value.toInt() == 1; + if (skipMenu) + { + skipMenuCheckBox->setCheckState(Qt::Checked); + } + startDefaultCharacterAtLabel->setEnabled(skipMenu); + startDefaultCharacterAtField->setEnabled(skipMenu); + + startDefaultCharacterAtField->setText(mGameSettings.value("start").value); + runScriptAfterStartupField->setText(mGameSettings.value("script-run").value); + } + return true; } -void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) +void Launcher::SettingsPage::saveSettings() { - if (exitCode != 0 || exitStatus == QProcess::CrashExit) + // Game mechanics { - resetProgressBar(); - - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Importer finished")); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setText(tr("Failed to import settings from INI file.")); - msgBox.exec(); + saveSettingBool(*canLootDuringDeathAnimationCheckBox, Settings::game().mCanLootDuringDeathAnimation); + saveSettingBool(*followersAttackOnSightCheckBox, Settings::game().mFollowersAttackOnSight); + saveSettingBool(*rebalanceSoulGemValuesCheckBox, Settings::game().mRebalanceSoulGemValues); + saveSettingBool(*enchantedWeaponsMagicalCheckBox, Settings::game().mEnchantedWeaponsAreMagical); + saveSettingBool( + *permanentBarterDispositionChangeCheckBox, Settings::game().mBarterDispositionChangeIsPermanent); + saveSettingBool(*classicReflectedAbsorbSpellsCheckBox, Settings::game().mClassicReflectedAbsorbSpellsBehavior); + saveSettingBool(*classicCalmSpellsCheckBox, Settings::game().mClassicCalmSpellsBehavior); + saveSettingBool( + *requireAppropriateAmmunitionCheckBox, Settings::game().mOnlyAppropriateAmmunitionBypassesResistance); + saveSettingBool(*uncappedDamageFatigueCheckBox, Settings::game().mUncappedDamageFatigue); + saveSettingBool(*normaliseRaceSpeedCheckBox, Settings::game().mNormaliseRaceSpeed); + saveSettingBool(*swimUpwardCorrectionCheckBox, Settings::game().mSwimUpwardCorrection); + saveSettingBool(*avoidCollisionsCheckBox, Settings::game().mNPCsAvoidCollisions); + saveSettingInt(*unarmedFactorsStrengthComboBox, Settings::game().mStrengthInfluencesHandToHand); + saveSettingBool(*stealingFromKnockedOutCheckBox, Settings::game().mAlwaysAllowStealingFromKnockedOutActors); + saveSettingBool(*enableNavigatorCheckBox, Settings::navigator().mEnable); + saveSettingInt(*physicsThreadsSpinBox, Settings::physics().mAsyncNumThreads); + saveSettingBool( + *allowNPCToFollowOverWaterSurfaceCheckBox, Settings::game().mAllowActorsToFollowOverWaterSurface); + saveSettingBool( + *unarmedCreatureAttacksDamageArmorCheckBox, Settings::game().mUnarmedCreatureAttacksDamageArmor); + saveSettingInt(*actorCollisonShapeTypeComboBox, Settings::game().mActorCollisionShapeType); } - else + + // Visuals { - // indicate progress finished - progressBar->setMaximum(1); - progressBar->setValue(1); + saveSettingBool(*autoUseObjectNormalMapsCheckBox, Settings::shaders().mAutoUseObjectNormalMaps); + saveSettingBool(*autoUseObjectSpecularMapsCheckBox, Settings::shaders().mAutoUseObjectSpecularMaps); + saveSettingBool(*autoUseTerrainNormalMapsCheckBox, Settings::shaders().mAutoUseTerrainNormalMaps); + saveSettingBool(*autoUseTerrainSpecularMapsCheckBox, Settings::shaders().mAutoUseTerrainSpecularMaps); + saveSettingBool(*bumpMapLocalLightingCheckBox, Settings::shaders().mApplyLightingToEnvironmentMaps); + saveSettingBool(*radialFogCheckBox, Settings::fog().mRadialFog); + saveSettingBool(*softParticlesCheckBox, Settings::shaders().mSoftParticles); + saveSettingBool(*antialiasAlphaTestCheckBox, Settings::shaders().mAntialiasAlphaTest); + saveSettingBool(*adjustCoverageForAlphaTestCheckBox, Settings::shaders().mAdjustCoverageForAlphaTest); + saveSettingBool(*weatherParticleOcclusionCheckBox, Settings::shaders().mWeatherParticleOcclusion); + saveSettingBool(*magicItemAnimationsCheckBox, Settings::game().mUseMagicItemAnimations); + saveSettingBool(*animSourcesCheckBox, Settings::game().mUseAdditionalAnimSources); + saveSettingBool(*weaponSheathingCheckBox, Settings::game().mWeaponSheathing); + saveSettingBool(*shieldSheathingCheckBox, Settings::game().mShieldSheathing); + saveSettingBool(*turnToMovementDirectionCheckBox, Settings::game().mTurnToMovementDirection); + saveSettingBool(*smoothAnimTransitionsCheckBox, Settings::game().mSmoothAnimTransitions); + saveSettingBool(*smoothMovementCheckBox, Settings::game().mSmoothMovement); + saveSettingBool(*playerMovementIgnoresAnimationCheckBox, Settings::game().mPlayerMovementIgnoresAnimation); + + const bool wantDistantLand = distantLandCheckBox->checkState() == Qt::Checked; + if (wantDistantLand != (Settings::terrain().mDistantTerrain && Settings::terrain().mObjectPaging)) + { + Settings::terrain().mDistantTerrain.set(wantDistantLand); + Settings::terrain().mObjectPaging.set(wantDistantLand); + } - // Importer may have changed settings, so refresh - mMain->reloadSettings(); + saveSettingBool(*activeGridObjectPagingCheckBox, Settings::terrain().mObjectPagingActiveGrid); + Settings::camera().mViewingDistance.set(convertToUnits(viewingDistanceComboBox->value())); + Settings::terrain().mObjectPagingMinSize.set(objectPagingMinSizeComboBox->value()); + saveSettingBool(*nightDaySwitchesCheckBox, Settings::game().mDayNightSwitches); + saveSettingBool(*postprocessEnabledCheckBox, Settings::postProcessing().mEnabled); + saveSettingBool(*postprocessTransparentPostpassCheckBox, Settings::postProcessing().mTransparentPostpass); + Settings::postProcessing().mAutoExposureSpeed.set(postprocessHDRTimeComboBox->value()); + saveSettingBool(*radialFogCheckBox, Settings::fog().mRadialFog); + saveSettingBool(*exponentialFogCheckBox, Settings::fog().mExponentialFog); + saveSettingBool(*skyBlendingCheckBox, Settings::fog().mSkyBlending); + Settings::fog().mSkyBlendingStart.set(skyBlendingStartComboBox->value()); + + static constexpr std::array lightingMethodMap = { + SceneUtil::LightingMethod::FFP, + SceneUtil::LightingMethod::PerObjectUniform, + SceneUtil::LightingMethod::SingleUBO, + }; + Settings::shaders().mLightingMethod.set(lightingMethodMap[lightingMethodComboBox->currentIndex()]); + + const int cShadowDist + = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0; + Settings::shadows().mMaximumShadowMapDistance.set(cShadowDist); + const float cFadeStart = fadeStartSpinBox->value(); + if (cShadowDist > 0) + Settings::shadows().mShadowFadeStart.set(cFadeStart); + + const bool cActorShadows = actorShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cObjectShadows = objectShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cTerrainShadows = terrainShadowsCheckBox->checkState() != Qt::Unchecked; + const bool cPlayerShadows = playerShadowsCheckBox->checkState() != Qt::Unchecked; + if (cActorShadows || cObjectShadows || cTerrainShadows || cPlayerShadows) + { + Settings::shadows().mEnableShadows.set(true); + Settings::shadows().mActorShadows.set(cActorShadows); + Settings::shadows().mPlayerShadows.set(cPlayerShadows); + Settings::shadows().mObjectShadows.set(cObjectShadows); + Settings::shadows().mTerrainShadows.set(cTerrainShadows); + } + else + { + Settings::shadows().mEnableShadows.set(false); + Settings::shadows().mActorShadows.set(false); + Settings::shadows().mPlayerShadows.set(false); + Settings::shadows().mObjectShadows.set(false); + Settings::shadows().mTerrainShadows.set(false); + } + + Settings::shadows().mEnableIndoorShadows.set(indoorShadowsCheckBox->checkState() != Qt::Unchecked); + Settings::shadows().mShadowMapResolution.set(shadowResolutionComboBox->currentText().toInt()); + + auto index = shadowComputeSceneBoundsComboBox->currentIndex(); + if (index == 0) + Settings::shadows().mComputeSceneBounds.set("bounds"); + else if (index == 1) + Settings::shadows().mComputeSceneBounds.set("primitives"); + else + Settings::shadows().mComputeSceneBounds.set("none"); } - importerButton->setEnabled(true); -} + // Audio + { + if (audioDeviceSelectorComboBox->currentIndex() != 0) + Settings::sound().mDevice.set(audioDeviceSelectorComboBox->currentText().toStdString()); + else + Settings::sound().mDevice.set({}); -void Launcher::SettingsPage::resetProgressBar() -{ - // set progress bar to 0 % - progressBar->reset(); -} + static constexpr std::array hrtfModes{ + Settings::HrtfMode::Auto, + Settings::HrtfMode::Disable, + Settings::HrtfMode::Enable, + }; + Settings::sound().mHrtfEnable.set(hrtfModes[enableHRTFComboBox->currentIndex()]); -void Launcher::SettingsPage::updateOkButton(const QString &text) -{ - // We do this here because we need to access the profiles - if (text.isEmpty()) { - mProfileDialog->setOkButtonEnabled(false); - return; + if (hrtfProfileSelectorComboBox->currentIndex() != 0) + Settings::sound().mHrtf.set(hrtfProfileSelectorComboBox->currentText().toStdString()); + else + Settings::sound().mHrtf.set({}); + + const bool cCameraListener = cameraListenerCheckBox->checkState() != Qt::Unchecked; + Settings::sound().mCameraListener.set(cCameraListener); + } + + // Interface Changes + { + saveSettingBool(*showEffectDurationCheckBox, Settings::game().mShowEffectDuration); + saveSettingBool(*showEnchantChanceCheckBox, Settings::game().mShowEnchantChance); + saveSettingBool(*showMeleeInfoCheckBox, Settings::game().mShowMeleeInfo); + saveSettingBool(*showProjectileDamageCheckBox, Settings::game().mShowProjectileDamage); + saveSettingBool(*changeDialogTopicsCheckBox, Settings::gui().mColorTopicEnable); + saveSettingInt(*showOwnedComboBox, Settings::game().mShowOwned); + saveSettingBool(*stretchBackgroundCheckBox, Settings::gui().mStretchMenuBackground); + saveSettingBool(*useZoomOnMapCheckBox, Settings::map().mAllowZooming); + saveSettingBool(*graphicHerbalismCheckBox, Settings::game().mGraphicHerbalism); + Settings::gui().mScalingFactor.set(scalingSpinBox->value()); + Settings::gui().mFontSize.set(fontSizeSpinBox->value()); } - const QStringList profiles(mLauncherSettings.getContentLists()); + // Bug fixes + { + saveSettingBool(*preventMerchantEquippingCheckBox, Settings::game().mPreventMerchantEquipping); + saveSettingBool( + *trainersTrainingSkillsBasedOnBaseSkillCheckBox, Settings::game().mTrainersTrainingSkillsBasedOnBaseSkill); + } + + // Miscellaneous + { + // Saves Settings + saveSettingInt(*maximumQuicksavesComboBox, Settings::saves().mMaxQuicksaves); + + // Other Settings + Settings::general().mScreenshotFormat.set(screenshotFormatComboBox->currentText().toLower().toStdString()); + saveSettingBool(*notifyOnSavedScreenshotCheckBox, Settings::general().mNotifyOnSavedScreenshot); + } + + // Testing + { + saveSettingBool(*grabCursorCheckBox, Settings::input().mGrabCursor); - (profiles.contains(text)) - ? mProfileDialog->setOkButtonEnabled(false) - : mProfileDialog->setOkButtonEnabled(true); + int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked; + if (skipMenu != mGameSettings.value("skip-menu").value.toInt()) + mGameSettings.setValue("skip-menu", { QString::number(skipMenu) }); + + QString startCell = startDefaultCharacterAtField->text(); + if (startCell != mGameSettings.value("start").value) + { + mGameSettings.setValue("start", { startCell }); + } + QString scriptRun = runScriptAfterStartupField->text(); + if (scriptRun != mGameSettings.value("script-run").value) + mGameSettings.setValue("script-run", { scriptRun }); + } } -void Launcher::SettingsPage::saveSettings() +void Launcher::SettingsPage::slotLoadedCellsChanged(QStringList cellNames) { - QString language(languageComboBox->currentText()); - - mLauncherSettings.setValue(QLatin1String("Settings/language"), language); + loadCellsForAutocomplete(std::move(cellNames)); +} - if (language == QLatin1String("Polish")) { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250")); - } else if (language == QLatin1String("Russian")) { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251")); - } else { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); +void Launcher::SettingsPage::slotAnimSourcesToggled(bool checked) +{ + weaponSheathingCheckBox->setEnabled(checked); + shieldSheathingCheckBox->setEnabled(checked); + if (!checked) + { + weaponSheathingCheckBox->setCheckState(Qt::Unchecked); + shieldSheathingCheckBox->setCheckState(Qt::Unchecked); } } -bool Launcher::SettingsPage::loadSettings() +void Launcher::SettingsPage::slotPostProcessToggled(bool checked) { - QString language(mLauncherSettings.value(QLatin1String("Settings/language"))); + postprocessTransparentPostpassCheckBox->setEnabled(checked); + postprocessHDRTimeComboBox->setEnabled(checked); + postprocessHDRTimeLabel->setEnabled(checked); +} - int index = languageComboBox->findText(language); +void Launcher::SettingsPage::slotSkyBlendingToggled(bool checked) +{ + skyBlendingStartComboBox->setEnabled(checked); + skyBlendingStartLabel->setEnabled(checked); +} - if (index != -1) - languageComboBox->setCurrentIndex(index); +void Launcher::SettingsPage::slotShadowDistLimitToggled(bool checked) +{ + shadowDistanceSpinBox->setEnabled(checked); + fadeStartSpinBox->setEnabled(checked); +} - return true; +void Launcher::SettingsPage::slotDistantLandToggled(bool checked) +{ + activeGridObjectPagingCheckBox->setEnabled(checked); + objectPagingMinSizeComboBox->setEnabled(checked); } diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index df7c0e8eb50..d2bb80d86a7 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -1,63 +1,50 @@ -#ifndef SETTINGSPAGE_HPP -#define SETTINGSPAGE_HPP +#ifndef SETTINGSPAGE_H +#define SETTINGSPAGE_H -#include +#include +#include #include "ui_settingspage.h" -#include "maindialog.hpp" - -namespace Files { struct ConfigurationManager; } -namespace Config { class GameSettings; - class LauncherSettings; } +namespace Config +{ + class GameSettings; +} namespace Launcher { - class TextInputDialog; - class SettingsPage : public QWidget, private Ui::SettingsPage { Q_OBJECT public: - SettingsPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, - Config::LauncherSettings &launcherSettings, MainDialog *parent = nullptr); - ~SettingsPage(); + explicit SettingsPage(Config::GameSettings& gameSettings, QWidget* parent = nullptr); - void saveSettings(); bool loadSettings(); - - /// set progress bar on page to 0% - void resetProgressBar(); - - private slots: - - void on_wizardButton_clicked(); - void on_importerButton_clicked(); - void on_browseButton_clicked(); - - void wizardStarted(); - void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); + void saveSettings(); - void importerStarted(); - void importerFinished(int exitCode, QProcess::ExitStatus exitStatus); + public slots: + void slotLoadedCellsChanged(QStringList cellNames); - void updateOkButton(const QString &text); + private slots: + void on_skipMenuCheckBox_stateChanged(int state); + void on_runScriptAfterStartupBrowseButton_clicked(); + void slotAnimSourcesToggled(bool checked); + void slotPostProcessToggled(bool checked); + void slotSkyBlendingToggled(bool checked); + void slotShadowDistLimitToggled(bool checked); + void slotDistantLandToggled(bool checked); private: - - Process::ProcessInvoker *mWizardInvoker; - Process::ProcessInvoker *mImporterInvoker; - - Files::ConfigurationManager &mCfgMgr; - - Config::GameSettings &mGameSettings; - Config::LauncherSettings &mLauncherSettings; - - MainDialog *mMain; - TextInputDialog *mProfileDialog; - + Config::GameSettings& mGameSettings; + QCompleter mCellNameCompleter; + QStringListModel mCellNameCompleterModel; + + /** + * Load the cells associated with the given content files for use in autocomplete + * @param filePaths the file paths of the content files to be examined + */ + void loadCellsForAutocomplete(QStringList filePaths); }; } - -#endif // SETTINGSPAGE_HPP +#endif diff --git a/apps/launcher/textslotmsgbox.hpp b/apps/launcher/textslotmsgbox.hpp index a0fefaa2535..1eac6cf9181 100644 --- a/apps/launcher/textslotmsgbox.hpp +++ b/apps/launcher/textslotmsgbox.hpp @@ -7,9 +7,9 @@ namespace Launcher { class TextSlotMsgBox : public QMessageBox { - Q_OBJECT - public slots: - void setTextSlot(const QString& string); + Q_OBJECT + public slots: + void setTextSlot(const QString& string); }; } #endif diff --git a/apps/launcher/ui/datafilespage.ui b/apps/launcher/ui/datafilespage.ui new file mode 100644 index 00000000000..1f537b1cc84 --- /dev/null +++ b/apps/launcher/ui/datafilespage.ui @@ -0,0 +1,537 @@ + + + DataFilesPage + + + + 0 + 0 + 573 + 557 + + + + Qt::DefaultContextMenu + + + + + + 0 + + + + Content Files + + + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Note: content files that are not part of current Content List are <span style=" font-style:italic;font-weight: bold">highlighted</span></p></body></html> + + + + + + + + Data Directories + + + + + + true + + + QAbstractItemView::InternalMove + + + QAbstractItemView::ExtendedSelection + + + + + + + + + + 0 + 33 + + + + Scan directories for likely data directories and append them at the end of the list. + + + Append + + + + + + + + 0 + 33 + + + + Scan directories for likely data directories and insert them above the selected position + + + Insert Above + + + + + + + + 0 + 33 + + + + Move selected directory one position up + + + Move Up + + + + + + + + 0 + 33 + + + + Move selected directory one position down + + + Move Down + + + + + + + + 0 + 33 + + + + Remove selected directory + + + Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Note: directories that are not part of current Content List are <span style=" font-style:italic;font-weight: bold">highlighted</span></p></body></html> + + + + + + + + Archive Files + + + + + + true + + + Qt::CustomContextMenu + + + QAbstractItemView::InternalMove + + + Qt::CopyAction + + + QAbstractItemView::ExtendedSelection + + + + + + + + + + 0 + 33 + + + + + 0 + 33 + + + + Move selected archive one position up + + + Move Up + + + + + + + + 0 + 33 + + + + + 0 + 33 + + + + Move selected archive one position down + + + Move Down + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + <html><head/><body><p>Note: archives that are not part of current Content List are <span style=" font-style:italic;font-weight: bold">highlighted</span></p></body></html> + + + + + + + + Navigation Mesh Cache + + + + + + + + Qt::TabFocus + + + Generate navigation mesh cache for all content. Will be used by the engine to make cell loading faster. + + + Update + + + + + + + false + + + 0 + + + + + + + false + + + Cancel navigation mesh generation. Already processed data will be saved. + + + Cancel + + + + + + + + + Remove Unused Tiles + + + true + + + + + + + + + Max Size + + + + + + + MiB + + + 2147483647 + + + 2048 + + + + + + + + + true + + + QPlainTextEdit::NoWrap + + + true + + + + + + + + + + + Qt::NoFocus + + + Content List + + + false + + + + 6 + + + 3 + + + 6 + + + 0 + + + 6 + + + + + true + + + + 0 + 0 + + + + Select a content list + + + + + + + New Content List + + + &New Content List + + + true + + + + + + + Clone Content List + + + Clone Content List + + + true + + + + + + + Delete Content List + + + Delete Content List + + + true + + + + + + + + + + + .. + + + New Content List + + + New Content List + + + Ctrl+N + + + + + + .. + + + Clone Content List + + + Clone Content List + + + Ctrl+G + + + + + false + + + + .. + + + Delete Content List + + + Delete Content List + + + Ctrl+D + + + + + true + + + Check Selection + + + + + Uncheck Selection + + + + + + .. + + + Refresh Data Files + + + Refresh Data Files + + + Ctrl+R + + + + + + ProfilesComboBox + QComboBox +
apps/launcher/utils/profilescombobox.hpp
+
+
+ + +
diff --git a/apps/launcher/ui/directorypicker.ui b/apps/launcher/ui/directorypicker.ui new file mode 100644 index 00000000000..6350bcd2d3d --- /dev/null +++ b/apps/launcher/ui/directorypicker.ui @@ -0,0 +1,47 @@ + + + SelectSubdirs + + + + 0 + 0 + 800 + 500 + + + + Select directories you wish to add + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + confirmButton + accepted() + SelectSubdirs + accept() + + + confirmButton + rejected() + SelectSubdirs + reject() + + + diff --git a/apps/launcher/ui/graphicspage.ui b/apps/launcher/ui/graphicspage.ui new file mode 100644 index 00000000000..b3e2b15e39f --- /dev/null +++ b/apps/launcher/ui/graphicspage.ui @@ -0,0 +1,243 @@ + + + GraphicsPage + + + + 0 + 0 + 650 + 358 + + + + + + + + + + + + Screen + + + + + + + Window Mode + + + + + + + + + + + 800 + + + + + + + × + + + + + + + 600 + + + + + + + + + Custom: + + + + + + + Standard: + + + true + + + + + + + + + + + + + 0 + + + + + 2 + + + + + 4 + + + + + 8 + + + + + 16 + + + + + + + + Framerate Limit + + + + + + + Window Border + + + + + + + + + + 0 + + + + Disabled + + + + + Enabled + + + + + Adaptive + + + + + + + + 0 + + + + Fullscreen + + + + + Windowed Fullscreen + + + + + Windowed + + + + + + + + Resolution + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + false + + + FPS + + + 1 + + + 1.000000000000000 + + + 1000.000000000000000 + + + 15.000000000000000 + + + 300.000000000000000 + + + + + + + Anti-Aliasing + + + + + + + Vertical Synchronization + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + diff --git a/apps/launcher/ui/importpage.ui b/apps/launcher/ui/importpage.ui new file mode 100644 index 00000000000..0b5d014afad --- /dev/null +++ b/apps/launcher/ui/importpage.ui @@ -0,0 +1,152 @@ + + + ImportPage + + + + 0 + 0 + 515 + 397 + + + + Form + + + + + + Morrowind Installation Wizard + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Run &Installation Wizard + + + + + + + + + + Morrowind Settings Importer + + + + + + + + File to Import Settings From: + + + + + + + + + + Browse... + + + + + + + + + Import Add-on and Plugin Selection + + + true + + + + + + + Fonts shipped with the original engine are blurry with UI scaling and support only a small amount of characters, +so OpenMW provides another set of fonts to avoid these issues. These fonts use TrueType technology and are quite similar +to default Morrowind fonts. Check this box if you still prefer original fonts over OpenMW ones or if you use custom bitmap fonts. + + + Import Bitmap Fonts + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Run &Settings Importer + + + + + + + + + 4 + + + false + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + + diff --git a/apps/launcher/ui/mainwindow.ui b/apps/launcher/ui/mainwindow.ui new file mode 100644 index 00000000000..b5ee65d17c2 --- /dev/null +++ b/apps/launcher/ui/mainwindow.ui @@ -0,0 +1,175 @@ + + + MainWindow + + + + 0 + 0 + 775 + 635 + + + + + 775 + 635 + + + + OpenMW Launcher + + + + :/images/openmw.png:/images/openmw.png + + + + + + + + + + Qt::Horizontal + + + + + + + + + OpenMW version + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + QDialogButtonBox::Close|QDialogButtonBox::Help|QDialogButtonBox::Ok + + + + + + + + + + toolBar + + + Qt::LeftToRight + + + false + + + + 48 + 48 + + + + Qt::ToolButtonTextUnderIcon + + + false + + + TopToolBarArea + + + false + + + + + + + + + true + + + + :/images/openmw-plugin.png:/images/openmw-plugin.png + + + Data Files + + + Allows to setup data files and directories + + + + + + + + + + + true + + + + :/images/preferences-video.png:/images/preferences-video.png + + + Display + + + Allows to change display settings + + + + + true + + + + :/images/preferences.png:/images/preferences.png + + + Settings + + + Allows to tweak engine settings + + + + + true + + + + :/images/preferences-advanced.png:/images/preferences-advanced.png + + + Import + + + Allows to import data from original engine + + + + + + + + diff --git a/apps/launcher/ui/settingspage.ui b/apps/launcher/ui/settingspage.ui new file mode 100644 index 00000000000..e5ff1d5c7f8 --- /dev/null +++ b/apps/launcher/ui/settingspage.ui @@ -0,0 +1,1640 @@ + + + SettingsPage + + + + 0 + 0 + 741 + 503 + + + + + + + 0 + + + + Gameplay + + + + + + + + <html><head/><body><p>Make Damage Fatigue magic effect uncapped like Drain Fatigue effect.</p><p>This means that unlike Morrowind you will be able to knock down actors using this effect.</p></body></html> + + + Uncapped Damage Fatigue + + + + + + + <html><head/><body><p>Give actors an ability to swim over the water surface when they follow other actor independently from their ability to swim. Has effect only when nav mesh building is enabled.</p></body></html> + + + Always Allow Actors to Follow over Water + + + + + + + <html><head/><body><p>Make disposition change of merchants caused by trading permanent.</p></body></html> + + + Permanent Barter Disposition Changes + + + + + + + <html><head/><body><p>Don't use race weight in NPC movement speed calculations.</p></body></html> + + + Racial Variation in Speed Fix + + + + + + + <html><head/><body><p>Stops combat with NPCs affected by Calm spells every frame -- like in Morrowind without the MCP.</p></body></html> + + + Classic Calm Spells Behavior + + + + + + + <html><head/><body><p>If enabled NPCs apply evasion maneuver to avoid collisions with others.</p></body></html> + + + NPCs Avoid Collisions + + + + + + + <html><head/><body><p>Make the value of filled soul gems dependent only on soul magnitude.</p></body></html> + + + Soulgem Values Rebalance + + + + + + + <html><head/><body><p>If this setting is true, supporting models will make use of day night switch nodes.</p></body></html> + + + Day Night Switch Nodes + + + + + + + <html><head/><body><p>Make player followers and escorters start combat with enemies who have started combat with them or the player. Otherwise they wait for the enemies or the player to do an attack first.</p></body></html> + + + Followers Defend Immediately + + + + + + + <html><head/><body><p><a name="docs-internal-guid-f375b85a-7fff-02ff-a5af-c5cff63923c0"/>When enabled, a navigation mesh is built in the background for world geometry to be used for pathfinding. When disabled only the path grid is used to build paths. Single-core CPU systems may have a big performance impact on existing interior location and moving across the exterior world. May slightly affect performance on multi-core CPU systems. Multi-core CPU systems may have different latency for nav mesh update depending on other settings and system performance. Moving across external world, entering/exiting location produce nav mesh update. NPC and creatures may not be able to find path before nav mesh is built around them. Try to disable this if you want to have old fashioned AI which doesn't know where to go when you stand behind that stone and cast a firebolt.</p></body></html> + + + Use Navigation Mesh for Pathfinding + + + + + + + <html><head/><body><p>If enabled, a magical ammunition is required to bypass normal weapon resistance or weakness. If disabled, a magical ranged weapon or a magical ammunition is required.</p></body></html> + + + Only Magical Ammo Bypass Resistance + + + + + + + <html><head/><body><p>If this setting is true, containers supporting graphic herbalism will do so instead of opening the menu.</p></body></html> + + + Graphic Herbalism + + + + + + + <html><head/><body><p>Makes player swim a bit upward from the line of sight. Applies only in third person mode. Intended to make simpler swimming without diving.</p></body></html> + + + Swim Upward Correction + + + + + + + <html><head/><body><p>Make enchanted weapons without Magical flag bypass normal weapons resistance, like in Morrowind.</p></body></html> + + + Enchanted Weapons Are Magical + + + + + + + <html><head/><body><p>Prevents merchants from equipping items that are sold to them.</p></body></html> + + + Merchant Equipping Fix + + + + + + + <html><head/><body><p>Trainers now only choose which skills to train using their base skill points, allowing mercantile improving effects to be used without making mercantile an offered skill.</p></body></html> + + + Trainers Choose Offered Skills by Base Value + + + + + + + <html><head/><body><p>If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor's script instantly.</p><p>If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.</p></body></html> + + + Can Loot During Death Animation + + + + + + + <html><head/><body><p>Make stealing items from NPCs that were knocked down possible during combat.</p></body></html> + + + Steal from Knocked out Actors in Combat + + + + + + + <html><head/><body><p>Effects of reflected Absorb spells are not mirrored - like in Morrowind.</p></body></html> + + + Classic Reflected Absorb Spells Behavior + + + + + + + <html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html> + + + Unarmed Creature Attacks Damage Armor + + + + + + + + + + + Factor Strength into Hand-to-Hand Combat + + + + + + + 0 + + + + Off + + + + + Affect Werewolves + + + + + Do Not Affect Werewolves + + + + + + + + <html><head/><body><p>How many threads will be spawned to compute physics update in the background. A value of 0 means that the update will be performed in the main thread.</p><p>A value greater than 1 requires the Bullet library be compiled with multithreading support.</p></body></html> + + + Background Physics Threads + + + + + + + + + + Actor Collision Shape Type + + + + + + + Collision is used for both physics simulation and navigation mesh generation for pathfinding. Cylinder gives the best consistency between available navigation paths and ability to move by them. Changing this value affects navigation mesh generation therefore navigation mesh disk cache generated for one value will not be useful with another. + + + Axis-Aligned Bounding Box + + + + Axis-Aligned Bounding Box + + + + + Rotating Box + + + + + Cylinder + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Visuals + + + + + + + + 0 + + + + Animations + + + + + + + + <html><head/><body><p>Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.</p></body></html> + + + Smooth Movement + + + + + + + <html><head/><body><p>Load per-group KF-files and skeleton files from Animations folder</p></body></html> + + + Use Additional Animation Sources + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>Affects side and diagonal movement. Enabling this setting makes movement more realistic.</p><p>If disabled then the whole character's body is pointed to the direction of view. Diagonal movement has no special animation and causes sliding.</p><p>If enabled then the character turns lower body to the direction of movement. Upper body is turned partially. Head is always pointed to the direction of view. In combat mode it works only for diagonal movement. In non-combat mode it changes straight right and straight left movement as well. Also turns the whole body up or down when swimming according to the movement direction.</p></body></html> + + + Turn to Movement Direction + + + + + + + false + + + <html><head/><body><p>Render holstered weapons (with quivers and scabbards), requires modded assets.</p></body></html> + + + Weapon Sheathing + + + + + + + false + + + <html><head/><body><p>Render holstered shield, requires modded assets.</p></body></html> + + + Shield Sheathing + + + + + + + <html><head/><body><p>In third person, the camera will sway along with the movement animations of the player. Enabling this option disables this swaying by having the player character move independently of its animation. This was the default behavior of OpenMW 0.48 and earlier.</p></body></html> + + + Player Movement Ignores Animation + + + + + + + <html><head/><body><p>Use casting animations for magic items, just as for spells.</p></body></html> + + + Use Magic Item Animation + + + + + + + <html><head/><body><p>If enabled - makes transitions between different animations/poses much smoother. Also allows to load animation blending config YAML files that can be bundled with animations in order to customise blending styles.</p></body></html> + + + Smooth Animation Transitions + + + + + + + + + + Shaders + + + + + + + + + + <html><head/><body><p>If this option is enabled, normal maps are automatically recognized and used if they are named appropriately + (see 'normal map pattern', e.g. for a base texture foo.dds, the normal map texture would have to be named foo_n.dds). + If this option is disabled, normal maps are only used if they are explicitly listed within the mesh file (.nif or .osg file). Affects objects.</p></body></html> + + + Auto Use Object Normal Maps + + + + + + + <html><head/><body><p>Enables soft particles for particle effects. This technique softens the intersection between individual particles and other opaque geometry by blending between them.</p></body></html> + + + Soft Particles + + + + + + + <html><head/><body><p>If this option is enabled, specular maps are automatically recognized and used if they are named appropriately + (see 'specular map pattern', e.g. for a base texture foo.dds, + the specular map texture would have to be named foo_spec.dds). + If this option is disabled, normal maps are only used if they are explicitly listed within the mesh file + (.osg file, not supported in .nif files). Affects objects.</p></body></html> + + + Auto Use Object Specular Maps + + + + + + + <html><head/><body><p>See 'auto use object normal maps'. Affects terrain.</p></body></html> + + + Auto Use Terrain Normal Maps + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>If a file with pattern 'terrain specular map pattern' exists, use that file as a 'diffuse specular' map. The texture must contain the layer colour in the RGB channel (as usual), and a specular multiplier in the alpha channel.</p></body></html> + + + Auto Use Terrain Specular Maps + + + + + + + <html><head/><body><p>Simulate coverage-preserving mipmaps to prevent alpha-tested meshes shrinking as they get further away. Will cause meshes whose textures have coverage-preserving mipmaps to grow, though, so refer to mod installation instructions for how to set this.</p></body></html> + + + Adjust Coverage for Alpha Test + + + + + + + <html><head/><body><p>Allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation. Can negatively impact performance.</p></body></html> + + + Use Anti-Aliased Alpha Testing + + + + + + + <html><head/><body><p>Normally environment map reflections aren't affected by lighting, which makes environment-mapped (and thus bump-mapped objects) glow in the dark. + Morrowind Code Patch includes an option to remedy that by doing environment-mapping before applying lighting, this is the equivalent of that option. + Affected objects will use shaders. + </p></body></html> + + + Bump/Reflect Map Local Lighting + + + + + + + <html><head/><body><p>EXPERIMENTAL: Stop rain and snow from falling through overhangs and roofs.</p></body></html> + + + Weather Particle Occlusion + + + + + + + + + + + + Fog + + + + + + + + <html><head/><body><p>Use exponential fog formula. By default, linear fog is used.</p></body></html> + + + Exponential Fog + + + + + + + false + + + 3 + + + 0.000000000000000 + + + 1.000000000000000 + + + 0.005000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>By default, the fog becomes thicker proportionally to your distance from the clipping plane set at the clipping distance, which causes distortion at the edges of the screen. + This setting makes the fog use the actual eye point distance (or so called Euclidean distance) to calculate the fog, which makes the fog look less artificial, especially if you have a wide FOV.</p></body></html> + + + Radial Fog + + + + + + + false + + + <html><head/><body><p>The fraction of the maximum distance at which blending with the sky starts.</p></body></html> + + + Sky Blending Start + + + + + + + <html><head/><body><p>Reduce visibility of clipping plane by blending objects with the sky.</p></body></html> + + + Sky Blending + + + + + + + + + + Terrain + + + + + + + + <html><head/><body><p>Controls how large an object must be to be visible in the scene. The object’s size is divided by its distance to the camera and the result of the division is compared with this value. The smaller this value is, the more objects you will see in the scene.</p></body></html> + + + Object Paging Min Size + + + + + + + 3 + + + 0.000000000000000 + + + 0.250000000000000 + + + 0.005000000000000 + + + + + + + Viewing Distance + + + + + + + cells + + + 3 + + + 0.000000000000000 + + + 0.125000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>If true, use paging and LOD algorithms to display the entire terrain. If false, only display terrain of the loaded cells.</p></body></html> + + + Distant Land + + + + + + + <html><head/><body><p>Use object paging for active cells grid.</p></body></html> + + + Active Grid Object Paging + + + + + + + + + + Post Processing + + + + + + + + false + + + 3 + + + 0.010000000000000 + + + 10.000000000000000 + + + 0.001000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + false + + + <html><head/><body><p>Re-render transparent objects with forced alpha clipping.</p></body></html> + + + Transparent Postpass + + + + + + + false + + + <html><head/><body><p>Controls how much eye adaptation can change from frame to frame. Smaller values makes for slower transitions.</p></body></html> + + + Auto Exposure Speed + + + + + + + <html><head/><body><p>If this setting is true, post processing will be enabled.</p></body></html> + + + Enable Post Processing + + + + + + + + + + Shadows + + + + + + + + + Bounds + + + + + Primitives + + + + + None + + + + + + + + <html><head/><body><p>Type of "compute scene bounds" computation method to be used. Bounds (default) for good balance between performance and shadow quality, primitives for better looking shadows or none for no computation.</p></body></html> + + + Shadow Planes Computation Method + + + + + + + false + + + <html><head/><body><p>64 game units is 1 real life yard or about 0.9 m</p></body></html> + + + unit(s) + + + 512 + + + 81920 + + + 128 + + + 8192 + + + + + + + <html><head/><body><p>Enable shadows for NPCs and creatures besides the player character. May have a minor performance impact.</p></body></html> + + + Enable Actor Shadows + + + + + + + + 512 + + + + + 1024 + + + + + 2048 + + + + + 4096 + + + + + + + + <html><head/><body><p>The fraction of the limit above at which shadows begin to gradually fade away.</p></body></html> + + + Fade Start Multiplier + + + + + + + <html><head/><body><p>Enable shadows exclusively for the player character. May have a very minor performance impact.</p></body></html> + + + Enable Player Shadows + + + + + + + <html><head/><body><p>The resolution of each individual shadow map. Increasing it significantly improves shadow quality but may have a minor performance impact.</p></body></html> + + + Shadow Map Resolution + + + + + + + <html><head/><body><p>The distance from the camera at which shadows completely disappear.</p></body></html> + + + Shadow Distance Limit: + + + + + + + <html><head/><body><p>Enable shadows for primarily inanimate objects. May have a significant performance impact.</p></body></html> + + + Enable Object Shadows + + + + + + + false + + + 2 + + + 0.000000000000000 + + + 1.000000000000000 + + + 0.010000000000000 + + + 0.900000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>Due to limitations with Morrowind's data, only actors can cast shadows indoors, which some might feel is distracting.</p><p>Has no effect if actor/player shadows are not enabled.</p></body></html> + + + Enable Indoor Shadows + + + + + + + <html><head/><body><p>Enable shadows for the terrain including distant terrain. May have a significant performance and shadow quality impact.</p></body></html> + + + Enable Terrain Shadows + + + + + + + + + + Lighting + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>Set the internal handling of light sources.</p> +<p> "Legacy" always uses 8 lights per object. It provides results most similar to Morrowind's lighting.</p> +<p>"Shaders (compatibility)" removes the 8 light limit. This mode also enables lighting on groundcover. It is recommended to use this with older hardware and a light limit closer to 8.</p> +<p> "Shaders" carries all of the benefits that "Shaders (compatibility)" does, but uses a modern approach that allows for a higher max lights count with little to no performance penalties on modern hardware.</p></body></html> + + + Lighting Method + + + + + + + + Legacy + + + + + Shaders (compatibility) + + + + + Shaders + + + + + + + + + + + + + + + + + Audio + + + + + + + + Select your preferred audio device. + + + Audio Device + + + + + + + + 0 + 0 + + + + + 283 + 0 + + + + 0 + + + + Default + + + + + + + + + + + + This setting controls HRTF, which simulates 3D sound on stereo systems. + + + HRTF + + + + + + + + 0 + 0 + + + + + 283 + 0 + + + + 0 + + + + Automatic + + + + + Off + + + + + On + + + + + + + + + + + + Select your preferred HRTF profile. + + + HRTF Profile + + + + + + + + 0 + 0 + + + + + 283 + 0 + + + + 0 + + + + Default + + + + + + + + + + In third-person view, use the camera as the sound listener instead of the player character. + + + Use the Camera as the Sound Listener + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + Interface + + + + + + + + 1 + + + + Off + + + + + Tooltip + + + + + Crosshair + + + + + Tooltip and Crosshair + + + + + + + + 2 + + + 0.500000000000000 + + + 8.000000000000000 + + + 0.250000000000000 + + + 1.000000000000000 + + + + + + + 12 + + + 18 + + + 1 + + + 16 + + + + + + + <html><head/><body><p>This setting scales GUI windows. A value of 1.0 results in the normal scale.</p></body></html> + + + GUI Scaling Factor + + + + + + + <html><head/><body><p>Show the remaining duration of magic effects and lights if this setting is true. The remaining duration is displayed in the tooltip by hovering over the magical effect. </p><p>The default value is false.</p></body></html> + + + Show Effect Duration + + + + + + + <html><head/><body><p>If this setting is true, dialogue topics will have a different color if the topic is specific to the NPC you're talking to or the topic was previously seen. Color can be changed in settings.cfg.</p><p>The default value is false.</p></body></html> + + + Change Dialogue Topic Color + + + + + + + Size of characters in game texts. + + + Font Size + + + + + + + <html><head/><body><p>Enable zooming on local and global maps.</p></body></html> + + + Can Zoom on Maps + + + + + + + <html><head/><body><p>If this setting is true, damage bonus of arrows and bolts will be shown on item tooltip.</p><p>The default value is false.</p></body></html> + + + Show Projectile Damage + + + + + + + <html><head/><body><p>If this setting is true, melee weapons reach and speed will be shown on item tooltip.</p><p>The default value is false.</p></body></html> + + + Show Melee Info + + + + + + + <html><head/><body><p>Stretch menus, load screens, etc. to the window aspect ratio.</p></body></html> + + + Stretch Menu Background + + + + + + + Show Owned Objects + + + + + + + <html><head/><body><p>Whether or not the chance of success will be displayed in the enchanting menu.</p><p>The default value is false.</p></body></html> + + + Show Enchant Chance + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Miscellaneous + + + + + + Saves + + + + + + + + Maximum Quicksaves + + + + + + + 1 + + + + + + + + + + + + Screenshots + + + + + + + + Screenshot Format + + + + + + + + JPG + + + + + PNG + + + + + TGA + + + + + + + + + + Notify on Saved Screenshot + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + Testing + + + + + + These settings are intended for testing mods and will cause issues if used for normal gameplay. + + + true + + + + + + + Qt::Horizontal + + + + + + + <html><head/><body><p>OpenMW will capture control of the cursor if this setting is true.</p><p>In “look mode”, OpenMW will center the cursor regardless of the value of this setting (since the cursor/crosshair is always centered in the OpenMW window). However, in GUI mode, this setting determines the behavior when the cursor is moved outside the OpenMW window. If true, the cursor movement stops at the edge of the window preventing access to other applications. If false, the cursor is allowed to move freely on the desktop.</p><p>This setting does not apply to the screen where escape has been pressed, where the cursor is never captured. Regardless of this setting “Alt-Tab” or some other operating system dependent key sequence can be used to allow the operating system to regain control of the mouse cursor. This setting interacts with the minimize on focus loss setting by affecting what counts as a focus loss. Specifically on a two-screen configuration it may be more convenient to access the second screen with setting disabled.</p><p>Note for developers: it’s desirable to have this setting disabled when running the game in a debugger, to prevent the mouse cursor from becoming unusable when the game pauses on a breakpoint.</p></body></html> + + + Grab Cursor + + + + + + + Skip Menu and Generate Default Character + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 0 + 0 + + + + + + + + Start Default Character at + + + + + + + Default Cell + + + + + + + + + Run Script After Startup: + + + + + + + + + + + + Browse… + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + + + diff --git a/apps/launcher/utils/cellnameloader.cpp b/apps/launcher/utils/cellnameloader.cpp index e7f6e83e74e..8cc1191d766 100644 --- a/apps/launcher/utils/cellnameloader.cpp +++ b/apps/launcher/utils/cellnameloader.cpp @@ -1,44 +1,71 @@ #include "cellnameloader.hpp" -#include -#include +#include -QSet CellNameLoader::getCellNames(QStringList &contentPaths) +#include +#include +#include +#include +#include + +QSet CellNameLoader::getCellNames(const QStringList& contentPaths) { QSet cellNames; ESM::ESMReader esmReader; // Loop through all content files - for (auto &contentPath : contentPaths) { - esmReader.open(contentPath.toStdString()); - - // Loop through all records - while(esmReader.hasMoreRecs()) + for (const QString& contentPath : contentPaths) + { + if (contentPath.endsWith(".omwscripts", Qt::CaseInsensitive)) + continue; + try { - ESM::NAME recordName = esmReader.getRecName(); - esmReader.getRecHeader(); + std::filesystem::path filepath = Files::pathFromQString(contentPath); + auto stream = Files::openBinaryInputFileStream(filepath); + if (!stream->is_open()) + continue; + + const ESM::Format format = ESM::readFormat(*stream); + if (format != ESM::Format::Tes3) + continue; + + stream->seekg(0); + esmReader.open(std::move(stream), filepath); + + // Loop through all records + while (esmReader.hasMoreRecs()) + { + ESM::NAME recordName = esmReader.getRecName(); + esmReader.getRecHeader(); - if (isCellRecord(recordName)) { - QString cellName = getCellName(esmReader); - if (!cellName.isEmpty()) { - cellNames.insert(cellName); + if (isCellRecord(recordName)) + { + QString cellName = getCellName(esmReader); + if (!cellName.isEmpty()) + { + cellNames.insert(cellName); + } } - } - // Stop loading content for this record and continue to the next - esmReader.skipRecord(); + // Stop loading content for this record and continue to the next + esmReader.skipRecord(); + } + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Failed to get cell names from " << contentPath.toStdString() << ": " << e.what(); } } return cellNames; } -bool CellNameLoader::isCellRecord(ESM::NAME &recordName) +bool CellNameLoader::isCellRecord(ESM::NAME& recordName) { - return recordName.intval == ESM::REC_CELL; + return recordName.toInt() == ESM::REC_CELL; } -QString CellNameLoader::getCellName(ESM::ESMReader &esmReader) +QString CellNameLoader::getCellName(ESM::ESMReader& esmReader) { ESM::Cell cell; bool isDeleted = false; @@ -46,4 +73,3 @@ QString CellNameLoader::getCellName(ESM::ESMReader &esmReader) return QString::fromStdString(cell.mName); } - diff --git a/apps/launcher/utils/cellnameloader.hpp b/apps/launcher/utils/cellnameloader.hpp index 899ff75adba..6c7993dc5ab 100644 --- a/apps/launcher/utils/cellnameloader.hpp +++ b/apps/launcher/utils/cellnameloader.hpp @@ -4,21 +4,28 @@ #include #include -#include +#include -namespace ESM {class ESMReader; struct Cell;} -namespace ContentSelectorView {class ContentSelector;} +namespace ESM +{ + class ESMReader; + struct Cell; +} +namespace ContentSelectorView +{ + class ContentSelector; +} -class CellNameLoader { +class CellNameLoader +{ public: - /** * Returns the names of all cells contained within the given content files * @param contentPaths the file paths of each content file to be examined * @return the names of all cells */ - QSet getCellNames(QStringList &contentPaths); + QSet getCellNames(const QStringList& contentPaths); private: /** @@ -26,15 +33,14 @@ class CellNameLoader { * @param name The name associated with the record * @return whether or not the given record is of type "Cell" */ - bool isCellRecord(ESM::NAME &name); + bool isCellRecord(ESM::NAME& name); /** * Returns the name of the cell * @param esmReader the reader currently pointed to a loaded cell * @return the name of the cell */ - QString getCellName(ESM::ESMReader &esmReader); + QString getCellName(ESM::ESMReader& esmReader); }; - -#endif //OPENMW_CELLNAMELOADER_H +#endif // OPENMW_CELLNAMELOADER_H diff --git a/apps/launcher/utils/lineedit.cpp b/apps/launcher/utils/lineedit.cpp index 3487075808c..ad3f82ec306 100644 --- a/apps/launcher/utils/lineedit.cpp +++ b/apps/launcher/utils/lineedit.cpp @@ -1,6 +1,6 @@ #include "lineedit.hpp" -LineEdit::LineEdit(QWidget *parent) +LineEdit::LineEdit(QWidget* parent) : QLineEdit(parent) { setupClearButton(); @@ -9,22 +9,19 @@ LineEdit::LineEdit(QWidget *parent) void LineEdit::setupClearButton() { mClearButton = new QToolButton(this); - QPixmap pixmap(":images/clear.png"); - mClearButton->setIcon(QIcon(pixmap)); - mClearButton->setIconSize(pixmap.size()); + mClearButton->setIcon(QIcon::fromTheme("edit-clear")); mClearButton->setCursor(Qt::ArrowCursor); - mClearButton->setStyleSheet("QToolButton { border: none; padding: 0px; }"); + mClearButton->setAutoRaise(true); mClearButton->hide(); - connect(mClearButton, SIGNAL(clicked()), this, SLOT(clear())); - connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(updateClearButton(const QString&))); + connect(mClearButton, &QToolButton::clicked, this, &LineEdit::clear); + connect(this, &LineEdit::textChanged, this, &LineEdit::updateClearButton); } -void LineEdit::resizeEvent(QResizeEvent *) +void LineEdit::resizeEvent(QResizeEvent*) { QSize sz = mClearButton->sizeHint(); int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); - mClearButton->move(rect().right() - frameWidth - sz.width(), - (rect().bottom() + 1 - sz.height())/2); + mClearButton->move(rect().right() - frameWidth - sz.width(), (rect().bottom() + 1 - sz.height()) / 2); } void LineEdit::updateClearButton(const QString& text) diff --git a/apps/launcher/utils/lineedit.hpp b/apps/launcher/utils/lineedit.hpp index 89de39588a3..1a06c3ee063 100644 --- a/apps/launcher/utils/lineedit.hpp +++ b/apps/launcher/utils/lineedit.hpp @@ -23,19 +23,18 @@ class LineEdit : public QLineEdit QString mPlaceholderText; public: - LineEdit(QWidget *parent = nullptr); + LineEdit(QWidget* parent = nullptr); protected: - void resizeEvent(QResizeEvent *) override; + void resizeEvent(QResizeEvent*) override; private slots: - void updateClearButton(const QString &text); + void updateClearButton(const QString& text); protected: - QToolButton *mClearButton; + QToolButton* mClearButton; void setupClearButton(); }; #endif // LIENEDIT_H - diff --git a/apps/launcher/utils/openalutil.cpp b/apps/launcher/utils/openalutil.cpp index 52ad20894d3..9a9ae9981b3 100644 --- a/apps/launcher/utils/openalutil.cpp +++ b/apps/launcher/utils/openalutil.cpp @@ -1,8 +1,7 @@ #include #include -#include -#include +#include "apps/openmw/mwsound/alext.h" #include "openalutil.hpp" @@ -10,12 +9,12 @@ #define ALC_ALL_DEVICES_SPECIFIER 0x1013 #endif -std::vector Launcher::enumerateOpenALDevices() +std::vector Launcher::enumerateOpenALDevices() { - std::vector devlist; - const ALCchar *devnames; + std::vector devlist; + const ALCchar* devnames; - if(alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) + if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) { devnames = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); } @@ -23,23 +22,23 @@ std::vector Launcher::enumerateOpenALDevices() { devnames = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); } - - while(devnames && *devnames) + + while (devnames && *devnames) { devlist.emplace_back(devnames); - devnames += strlen(devnames)+1; + devnames += strlen(devnames) + 1; } return devlist; } -std::vector Launcher::enumerateOpenALDevicesHrtf() +std::vector Launcher::enumerateOpenALDevicesHrtf() { - std::vector ret; + std::vector ret; - ALCdevice *device = alcOpenDevice(nullptr); - if(device) + ALCdevice* device = alcOpenDevice(nullptr); + if (device) { - if(alcIsExtensionPresent(device, "ALC_SOFT_HRTF")) + if (alcIsExtensionPresent(device, "ALC_SOFT_HRTF")) { LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; void* funcPtr = alcGetProcAddress(device, "alcGetStringiSOFT"); @@ -47,10 +46,10 @@ std::vector Launcher::enumerateOpenALDevicesHrtf() ALCint num_hrtf; alcGetIntegerv(device, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); ret.reserve(num_hrtf); - for(ALCint i = 0;i < num_hrtf;++i) + for (ALCint i = 0; i < num_hrtf; ++i) { - const ALCchar *entry = alcGetStringiSOFT(device, ALC_HRTF_SPECIFIER_SOFT, i); - if(strcmp(entry, "") == 0) + const ALCchar* entry = alcGetStringiSOFT(device, ALC_HRTF_SPECIFIER_SOFT, i); + if (strcmp(entry, "") == 0) break; ret.emplace_back(entry); } diff --git a/apps/launcher/utils/openalutil.hpp b/apps/launcher/utils/openalutil.hpp index 4a84fbae7dd..f0add753903 100644 --- a/apps/launcher/utils/openalutil.hpp +++ b/apps/launcher/utils/openalutil.hpp @@ -1,7 +1,8 @@ +#include #include namespace Launcher { - std::vector enumerateOpenALDevices(); - std::vector enumerateOpenALDevicesHrtf(); -} \ No newline at end of file + std::vector enumerateOpenALDevices(); + std::vector enumerateOpenALDevicesHrtf(); +} diff --git a/apps/launcher/utils/profilescombobox.cpp b/apps/launcher/utils/profilescombobox.cpp index af349ddfff4..193dd8e4cfa 100644 --- a/apps/launcher/utils/profilescombobox.cpp +++ b/apps/launcher/utils/profilescombobox.cpp @@ -1,15 +1,12 @@ -#include -#include #include -#include +#include #include "profilescombobox.hpp" -ProfilesComboBox::ProfilesComboBox(QWidget *parent) : - ContentSelectorView::ComboBox(parent) +ProfilesComboBox::ProfilesComboBox(QWidget* parent) + : ContentSelectorView::ComboBox(parent) { - connect(this, SIGNAL(activated(int)), this, - SLOT(slotIndexChangedByUser(int))); + connect(this, qOverload(&ProfilesComboBox::activated), this, &ProfilesComboBox::slotIndexChangedByUser); setInsertPolicy(QComboBox::NoInsert); } @@ -19,9 +16,10 @@ void ProfilesComboBox::setEditEnabled(bool editable) if (isEditable() == editable) return; - if (!editable) { - disconnect(lineEdit(), SIGNAL(editingFinished()), this, SLOT(slotEditingFinished())); - disconnect(lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(slotTextChanged(QString))); + if (!editable) + { + disconnect(lineEdit(), &QLineEdit::editingFinished, this, &ProfilesComboBox::slotEditingFinished); + disconnect(lineEdit(), &QLineEdit::textChanged, this, &ProfilesComboBox::slotTextChanged); return setEditable(false); } @@ -29,31 +27,31 @@ void ProfilesComboBox::setEditEnabled(bool editable) setEditable(true); setValidator(mValidator); - ComboBoxLineEdit *edit = new ComboBoxLineEdit(this); + auto* edit = new ComboBoxLineEdit(this); setLineEdit(edit); setCompleter(nullptr); - connect(lineEdit(), SIGNAL(editingFinished()), this, - SLOT(slotEditingFinished())); + connect(lineEdit(), &QLineEdit::editingFinished, this, &ProfilesComboBox::slotEditingFinished); - connect(lineEdit(), SIGNAL(textChanged(QString)), this, - SLOT(slotTextChanged(QString))); + connect(lineEdit(), &QLineEdit::textChanged, this, &ProfilesComboBox::slotTextChanged); - connect (lineEdit(), SIGNAL(textChanged(QString)), this, - SIGNAL (signalProfileTextChanged (QString))); + connect(lineEdit(), &QLineEdit::textChanged, this, &ProfilesComboBox::signalProfileTextChanged); } -void ProfilesComboBox::slotTextChanged(const QString &text) +void ProfilesComboBox::slotTextChanged(const QString& text) { QPalette palette; - palette.setColor(QPalette::Text,Qt::red); + palette.setColor(QPalette::Text, Qt::red); int index = findText(text); - if (text.isEmpty() || (index != -1 && index != currentIndex())) { + if (text.isEmpty() || (index != -1 && index != currentIndex())) + { lineEdit()->setPalette(palette); - } else { + } + else + { lineEdit()->setPalette(QApplication::palette()); } } @@ -76,7 +74,7 @@ void ProfilesComboBox::slotEditingFinished() return; setItemText(currentIndex(), current); - emit(profileRenamed(previous, current)); + emit profileRenamed(previous, current); } void ProfilesComboBox::slotIndexChangedByUser(int index) @@ -84,15 +82,16 @@ void ProfilesComboBox::slotIndexChangedByUser(int index) if (index == -1) return; - emit (signalProfileChanged(mOldProfile, currentText())); + emit signalProfileChanged(mOldProfile, currentText()); mOldProfile = currentText(); } -ProfilesComboBox::ComboBoxLineEdit::ComboBoxLineEdit (QWidget *parent) - : LineEdit (parent) +ProfilesComboBox::ComboBoxLineEdit::ComboBoxLineEdit(QWidget* parent) + : LineEdit(parent) { int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); setObjectName(QString("ComboBoxLineEdit")); - setStyleSheet(QString("ComboBoxLineEdit { background-color: transparent; padding-right: %1px; } ").arg(mClearButton->sizeHint().width() + frameWidth + 1)); + setStyleSheet(QString("ComboBoxLineEdit { background-color: transparent; padding-right: %1px; } ") + .arg(mClearButton->sizeHint().width() + frameWidth + 1)); } diff --git a/apps/launcher/utils/profilescombobox.hpp b/apps/launcher/utils/profilescombobox.hpp index 065682f3d1c..91f4f9ef9c3 100644 --- a/apps/launcher/utils/profilescombobox.hpp +++ b/apps/launcher/utils/profilescombobox.hpp @@ -4,7 +4,7 @@ #include "components/contentselector/view/combobox.hpp" #include "lineedit.hpp" -#include +#include class QString; @@ -16,12 +16,11 @@ class ProfilesComboBox : public ContentSelectorView::ComboBox class ComboBoxLineEdit : public LineEdit { public: - explicit ComboBoxLineEdit (QWidget *parent = nullptr); + explicit ComboBoxLineEdit(QWidget* parent = nullptr); }; public: - - explicit ProfilesComboBox(QWidget *parent = nullptr); + explicit ProfilesComboBox(QWidget* parent = nullptr); void setEditEnabled(bool editable); void setCurrentProfile(int index) { @@ -30,16 +29,16 @@ class ProfilesComboBox : public ContentSelectorView::ComboBox } signals: - void signalProfileTextChanged(const QString &item); - void signalProfileChanged(const QString &previous, const QString ¤t); + void signalProfileTextChanged(const QString& item); + void signalProfileChanged(const QString& previous, const QString& current); void signalProfileChanged(int index); - void profileRenamed(const QString &oldName, const QString &newName); + void profileRenamed(const QString& oldName, const QString& newName); private slots: void slotEditingFinished(); void slotIndexChangedByUser(int index); - void slotTextChanged(const QString &text); + void slotTextChanged(const QString& text); private: QString mOldProfile; diff --git a/apps/launcher/utils/textinputdialog.cpp b/apps/launcher/utils/textinputdialog.cpp index 70b827596e0..9e06e6a6cd9 100644 --- a/apps/launcher/utils/textinputdialog.cpp +++ b/apps/launcher/utils/textinputdialog.cpp @@ -1,31 +1,31 @@ #include "textinputdialog.hpp" -#include #include +#include +#include #include #include #include -#include -Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString &text, QWidget *parent) : - QDialog(parent) +Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString& text, QWidget* parent) + : QDialog(parent) { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); mButtonBox = new QDialogButtonBox(this); mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); - mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); + mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - QLabel *label = new QLabel(this); + auto* label = new QLabel(this); label->setText(text); // Line edit - QValidator *validator = new QRegExpValidator(QRegExp("^[a-zA-Z0-9_]*$"), this); // Alpha-numeric + underscore + QValidator* validator = new QRegularExpressionValidator(QRegularExpression("^[a-zA-Z0-9_]*$"), this); mLineEdit = new LineEdit(this); mLineEdit->setValidator(validator); mLineEdit->setCompleter(nullptr); - QVBoxLayout *dialogLayout = new QVBoxLayout(this); + auto* dialogLayout = new QVBoxLayout(this); dialogLayout->addWidget(label); dialogLayout->addWidget(mLineEdit); dialogLayout->addWidget(mButtonBox); @@ -39,12 +39,8 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & setModal(true); - connect(mButtonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(mButtonBox, SIGNAL(rejected()), this, SLOT(reject())); -} - -Launcher::TextInputDialog::~TextInputDialog() -{ + connect(mButtonBox, &QDialogButtonBox::accepted, this, &TextInputDialog::accept); + connect(mButtonBox, &QDialogButtonBox::rejected, this, &TextInputDialog::reject); } int Launcher::TextInputDialog::exec() @@ -56,15 +52,18 @@ int Launcher::TextInputDialog::exec() void Launcher::TextInputDialog::setOkButtonEnabled(bool enabled) { - QPushButton *okButton = mButtonBox->button(QDialogButtonBox::Ok); + QPushButton* okButton = mButtonBox->button(QDialogButtonBox::Ok); okButton->setEnabled(enabled); QPalette palette; palette.setColor(QPalette::Text, Qt::red); - if (enabled) { + if (enabled) + { mLineEdit->setPalette(QApplication::palette()); - } else { + } + else + { // Existing profile name, make the text red mLineEdit->setPalette(palette); } diff --git a/apps/launcher/utils/textinputdialog.hpp b/apps/launcher/utils/textinputdialog.hpp index de065996394..333f4b65794 100644 --- a/apps/launcher/utils/textinputdialog.hpp +++ b/apps/launcher/utils/textinputdialog.hpp @@ -14,20 +14,17 @@ namespace Launcher Q_OBJECT public: + explicit TextInputDialog(const QString& title, const QString& text, QWidget* parent = nullptr); + ~TextInputDialog() override = default; - explicit TextInputDialog(const QString& title, const QString &text, QWidget *parent = nullptr); - ~TextInputDialog (); - - inline LineEdit *lineEdit() { return mLineEdit; } + inline LineEdit* lineEdit() { return mLineEdit; } void setOkButtonEnabled(bool enabled); int exec() override; private: - - QDialogButtonBox *mButtonBox; - LineEdit *mLineEdit; - + QDialogButtonBox* mButtonBox; + LineEdit* mLineEdit; }; } diff --git a/apps/mwiniimporter/CMakeLists.txt b/apps/mwiniimporter/CMakeLists.txt index e83656708b7..ed88ac0bc4d 100644 --- a/apps/mwiniimporter/CMakeLists.txt +++ b/apps/mwiniimporter/CMakeLists.txt @@ -14,22 +14,26 @@ openmw_add_executable(openmw-iniimporter ) target_link_libraries(openmw-iniimporter - ${Boost_PROGRAM_OPTIONS_LIBRARY} - ${Boost_FILESYSTEM_LIBRARY} + Boost::program_options components ) if (WIN32) - target_link_libraries(openmw-iniimporter - ${Boost_LOCALE_LIBRARY}) INSTALL(TARGETS openmw-iniimporter RUNTIME DESTINATION ".") endif(WIN32) if (MINGW) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -municode") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -municode") endif() if (BUILD_WITH_CODE_COVERAGE) - add_definitions (--coverage) - target_link_libraries(openmw-iniimporter gcov) + target_compile_options(openmw-iniimporter PRIVATE --coverage) + target_link_libraries(openmw-iniimporter gcov) +endif() + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw-iniimporter PRIVATE + + + ) endif() diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index 2763d8ad959..8c7c238b4ab 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -1,638 +1,254 @@ #include "importer.hpp" +#include +#include +#include +#include +#include +#include +#include #include -#include -#include -#include -#include -#include -#include - -namespace bfs = boost::filesystem; +namespace sfs = std::filesystem; MwIniImporter::MwIniImporter() : mVerbose(false) , mEncoding(ToUTF8::WINDOWS_1250) { - const char *map[][2] = - { - { "no-sound", "General:Disable Audio" }, - { 0, 0 } - }; - const char *fallback[] = { + const char* map[][2] = { { "no-sound", "General:Disable Audio" }, { 0, 0 } }; + const char* fallback[] = { // light - "LightAttenuation:UseConstant", - "LightAttenuation:ConstantValue", - "LightAttenuation:UseLinear", - "LightAttenuation:LinearMethod", - "LightAttenuation:LinearValue", - "LightAttenuation:LinearRadiusMult", - "LightAttenuation:UseQuadratic", - "LightAttenuation:QuadraticMethod", - "LightAttenuation:QuadraticValue", - "LightAttenuation:QuadraticRadiusMult", - "LightAttenuation:OutQuadInLin", + "LightAttenuation:UseConstant", "LightAttenuation:ConstantValue", "LightAttenuation:UseLinear", + "LightAttenuation:LinearMethod", "LightAttenuation:LinearValue", "LightAttenuation:LinearRadiusMult", + "LightAttenuation:UseQuadratic", "LightAttenuation:QuadraticMethod", "LightAttenuation:QuadraticValue", + "LightAttenuation:QuadraticRadiusMult", "LightAttenuation:OutQuadInLin", // inventory - "Inventory:DirectionalDiffuseR", - "Inventory:DirectionalDiffuseG", - "Inventory:DirectionalDiffuseB", - "Inventory:DirectionalAmbientR", - "Inventory:DirectionalAmbientG", - "Inventory:DirectionalAmbientB", - "Inventory:DirectionalRotationX", - "Inventory:DirectionalRotationY", - "Inventory:UniformScaling", + "Inventory:DirectionalDiffuseR", "Inventory:DirectionalDiffuseG", "Inventory:DirectionalDiffuseB", + "Inventory:DirectionalAmbientR", "Inventory:DirectionalAmbientG", "Inventory:DirectionalAmbientB", + "Inventory:DirectionalRotationX", "Inventory:DirectionalRotationY", "Inventory:UniformScaling", // map - "Map:Travel Siltstrider Red", - "Map:Travel Siltstrider Green", - "Map:Travel Siltstrider Blue", - "Map:Travel Boat Red", - "Map:Travel Boat Green", - "Map:Travel Boat Blue", - "Map:Travel Magic Red", - "Map:Travel Magic Green", - "Map:Travel Magic Blue", - "Map:Show Travel Lines", + "Map:Travel Siltstrider Red", "Map:Travel Siltstrider Green", "Map:Travel Siltstrider Blue", + "Map:Travel Boat Red", "Map:Travel Boat Green", "Map:Travel Boat Blue", "Map:Travel Magic Red", + "Map:Travel Magic Green", "Map:Travel Magic Blue", "Map:Show Travel Lines", // water - "Water:Map Alpha", - "Water:World Alpha", - "Water:SurfaceTextureSize", - "Water:SurfaceTileCount", - "Water:SurfaceFPS", - "Water:SurfaceTexture", - "Water:SurfaceFrameCount", - "Water:TileTextureDivisor", - "Water:RippleTexture", - "Water:RippleFrameCount", - "Water:RippleLifetime", - "Water:MaxNumberRipples", - "Water:RippleScale", - "Water:RippleRotSpeed", - "Water:RippleAlphas", - "Water:PSWaterReflectTerrain", - "Water:PSWaterReflectUpdate", - "Water:NearWaterRadius", - "Water:NearWaterPoints", - "Water:NearWaterUnderwaterFreq", - "Water:NearWaterUnderwaterVolume", - "Water:NearWaterIndoorTolerance", - "Water:NearWaterOutdoorTolerance", - "Water:NearWaterIndoorID", - "Water:NearWaterOutdoorID", - "Water:UnderwaterSunriseFog", - "Water:UnderwaterDayFog", - "Water:UnderwaterSunsetFog", - "Water:UnderwaterNightFog", - "Water:UnderwaterIndoorFog", - "Water:UnderwaterColor", + "Water:Map Alpha", "Water:World Alpha", "Water:SurfaceTextureSize", "Water:SurfaceTileCount", + "Water:SurfaceFPS", "Water:SurfaceTexture", "Water:SurfaceFrameCount", "Water:TileTextureDivisor", + "Water:RippleTexture", "Water:RippleFrameCount", "Water:RippleLifetime", "Water:MaxNumberRipples", + "Water:RippleScale", "Water:RippleRotSpeed", "Water:RippleAlphas", "Water:PSWaterReflectTerrain", + "Water:PSWaterReflectUpdate", "Water:NearWaterRadius", "Water:NearWaterPoints", "Water:NearWaterUnderwaterFreq", + "Water:NearWaterUnderwaterVolume", "Water:NearWaterIndoorTolerance", "Water:NearWaterOutdoorTolerance", + "Water:NearWaterIndoorID", "Water:NearWaterOutdoorID", "Water:UnderwaterSunriseFog", "Water:UnderwaterDayFog", + "Water:UnderwaterSunsetFog", "Water:UnderwaterNightFog", "Water:UnderwaterIndoorFog", "Water:UnderwaterColor", "Water:UnderwaterColorWeight", // pixelwater - "PixelWater:SurfaceFPS", - "PixelWater:TileCount", - "PixelWater:Resolution", + "PixelWater:SurfaceFPS", "PixelWater:TileCount", "PixelWater:Resolution", // fonts - "Fonts:Font 0", - "Fonts:Font 1", - "Fonts:Font 2", + "Fonts:Font 0", "Fonts:Font 1", "Fonts:Font 2", // UI colors - "FontColor:color_normal", - "FontColor:color_normal_over", - "FontColor:color_normal_pressed", - "FontColor:color_active", - "FontColor:color_active_over", - "FontColor:color_active_pressed", - "FontColor:color_disabled", - "FontColor:color_disabled_over", - "FontColor:color_disabled_pressed", - "FontColor:color_link", - "FontColor:color_link_over", - "FontColor:color_link_pressed", - "FontColor:color_journal_link", - "FontColor:color_journal_link_over", - "FontColor:color_journal_link_pressed", - "FontColor:color_journal_topic", - "FontColor:color_journal_topic_over", - "FontColor:color_journal_topic_pressed", - "FontColor:color_answer", - "FontColor:color_answer_over", - "FontColor:color_answer_pressed", - "FontColor:color_header", - "FontColor:color_notify", - "FontColor:color_big_normal", - "FontColor:color_big_normal_over", - "FontColor:color_big_normal_pressed", - "FontColor:color_big_link", - "FontColor:color_big_link_over", - "FontColor:color_big_link_pressed", - "FontColor:color_big_answer", - "FontColor:color_big_answer_over", - "FontColor:color_big_answer_pressed", - "FontColor:color_big_header", - "FontColor:color_big_notify", - "FontColor:color_background", - "FontColor:color_focus", - "FontColor:color_health", - "FontColor:color_magic", - "FontColor:color_fatigue", - "FontColor:color_misc", - "FontColor:color_weapon_fill", - "FontColor:color_magic_fill", - "FontColor:color_positive", - "FontColor:color_negative", - "FontColor:color_count", + "FontColor:color_normal", "FontColor:color_normal_over", "FontColor:color_normal_pressed", + "FontColor:color_active", "FontColor:color_active_over", "FontColor:color_active_pressed", + "FontColor:color_disabled", "FontColor:color_disabled_over", "FontColor:color_disabled_pressed", + "FontColor:color_link", "FontColor:color_link_over", "FontColor:color_link_pressed", + "FontColor:color_journal_link", "FontColor:color_journal_link_over", "FontColor:color_journal_link_pressed", + "FontColor:color_journal_topic", "FontColor:color_journal_topic_over", "FontColor:color_journal_topic_pressed", + "FontColor:color_answer", "FontColor:color_answer_over", "FontColor:color_answer_pressed", + "FontColor:color_header", "FontColor:color_notify", "FontColor:color_big_normal", + "FontColor:color_big_normal_over", "FontColor:color_big_normal_pressed", "FontColor:color_big_link", + "FontColor:color_big_link_over", "FontColor:color_big_link_pressed", "FontColor:color_big_answer", + "FontColor:color_big_answer_over", "FontColor:color_big_answer_pressed", "FontColor:color_big_header", + "FontColor:color_big_notify", "FontColor:color_background", "FontColor:color_focus", "FontColor:color_health", + "FontColor:color_magic", "FontColor:color_fatigue", "FontColor:color_misc", "FontColor:color_weapon_fill", + "FontColor:color_magic_fill", "FontColor:color_positive", "FontColor:color_negative", "FontColor:color_count", // level up messages - "Level Up:Level2", - "Level Up:Level3", - "Level Up:Level4", - "Level Up:Level5", - "Level Up:Level6", - "Level Up:Level7", - "Level Up:Level8", - "Level Up:Level9", - "Level Up:Level10", - "Level Up:Level11", - "Level Up:Level12", - "Level Up:Level13", - "Level Up:Level14", - "Level Up:Level15", - "Level Up:Level16", - "Level Up:Level17", - "Level Up:Level18", - "Level Up:Level19", - "Level Up:Level20", - "Level Up:Default", + "Level Up:Level2", "Level Up:Level3", "Level Up:Level4", "Level Up:Level5", "Level Up:Level6", + "Level Up:Level7", "Level Up:Level8", "Level Up:Level9", "Level Up:Level10", "Level Up:Level11", + "Level Up:Level12", "Level Up:Level13", "Level Up:Level14", "Level Up:Level15", "Level Up:Level16", + "Level Up:Level17", "Level Up:Level18", "Level Up:Level19", "Level Up:Level20", "Level Up:Default", // character creation multiple choice test - "Question 1:Question", - "Question 1:AnswerOne", - "Question 1:AnswerTwo", - "Question 1:AnswerThree", - "Question 1:Sound", - "Question 2:Question", - "Question 2:AnswerOne", - "Question 2:AnswerTwo", - "Question 2:AnswerThree", - "Question 2:Sound", - "Question 3:Question", - "Question 3:AnswerOne", - "Question 3:AnswerTwo", - "Question 3:AnswerThree", - "Question 3:Sound", - "Question 4:Question", - "Question 4:AnswerOne", - "Question 4:AnswerTwo", - "Question 4:AnswerThree", - "Question 4:Sound", - "Question 5:Question", - "Question 5:AnswerOne", - "Question 5:AnswerTwo", - "Question 5:AnswerThree", - "Question 5:Sound", - "Question 6:Question", - "Question 6:AnswerOne", - "Question 6:AnswerTwo", - "Question 6:AnswerThree", - "Question 6:Sound", - "Question 7:Question", - "Question 7:AnswerOne", - "Question 7:AnswerTwo", - "Question 7:AnswerThree", - "Question 7:Sound", - "Question 8:Question", - "Question 8:AnswerOne", - "Question 8:AnswerTwo", - "Question 8:AnswerThree", - "Question 8:Sound", - "Question 9:Question", - "Question 9:AnswerOne", - "Question 9:AnswerTwo", - "Question 9:AnswerThree", - "Question 9:Sound", - "Question 10:Question", - "Question 10:AnswerOne", - "Question 10:AnswerTwo", - "Question 10:AnswerThree", - "Question 10:Sound", + "Question 1:Question", "Question 1:AnswerOne", "Question 1:AnswerTwo", "Question 1:AnswerThree", + "Question 1:Sound", "Question 2:Question", "Question 2:AnswerOne", "Question 2:AnswerTwo", + "Question 2:AnswerThree", "Question 2:Sound", "Question 3:Question", "Question 3:AnswerOne", + "Question 3:AnswerTwo", "Question 3:AnswerThree", "Question 3:Sound", "Question 4:Question", + "Question 4:AnswerOne", "Question 4:AnswerTwo", "Question 4:AnswerThree", "Question 4:Sound", + "Question 5:Question", "Question 5:AnswerOne", "Question 5:AnswerTwo", "Question 5:AnswerThree", + "Question 5:Sound", "Question 6:Question", "Question 6:AnswerOne", "Question 6:AnswerTwo", + "Question 6:AnswerThree", "Question 6:Sound", "Question 7:Question", "Question 7:AnswerOne", + "Question 7:AnswerTwo", "Question 7:AnswerThree", "Question 7:Sound", "Question 8:Question", + "Question 8:AnswerOne", "Question 8:AnswerTwo", "Question 8:AnswerThree", "Question 8:Sound", + "Question 9:Question", "Question 9:AnswerOne", "Question 9:AnswerTwo", "Question 9:AnswerThree", + "Question 9:Sound", "Question 10:Question", "Question 10:AnswerOne", "Question 10:AnswerTwo", + "Question 10:AnswerThree", "Question 10:Sound", // blood textures and models - "Blood:Model 0", - "Blood:Model 1", - "Blood:Model 2", - "Blood:Texture 0", - "Blood:Texture 1", - "Blood:Texture 2", - "Blood:Texture 3", - "Blood:Texture 4", - "Blood:Texture 5", - "Blood:Texture 6", - "Blood:Texture 7", - "Blood:Texture Name 0", - "Blood:Texture Name 1", - "Blood:Texture Name 2", - "Blood:Texture Name 3", - "Blood:Texture Name 4", - "Blood:Texture Name 5", - "Blood:Texture Name 6", - "Blood:Texture Name 7", + "Blood:Model 0", "Blood:Model 1", "Blood:Model 2", "Blood:Texture 0", "Blood:Texture 1", "Blood:Texture 2", + "Blood:Texture 3", "Blood:Texture 4", "Blood:Texture 5", "Blood:Texture 6", "Blood:Texture 7", + "Blood:Texture Name 0", "Blood:Texture Name 1", "Blood:Texture Name 2", "Blood:Texture Name 3", + "Blood:Texture Name 4", "Blood:Texture Name 5", "Blood:Texture Name 6", "Blood:Texture Name 7", // movies - "Movies:Company Logo", - "Movies:Morrowind Logo", - "Movies:New Game", - "Movies:Loading", - "Movies:Options Menu", + "Movies:Company Logo", "Movies:Morrowind Logo", "Movies:New Game", "Movies:Loading", "Movies:Options Menu", // weather related values - "Weather Thunderstorm:Thunder Sound ID 0", - "Weather Thunderstorm:Thunder Sound ID 1", - "Weather Thunderstorm:Thunder Sound ID 2", - "Weather Thunderstorm:Thunder Sound ID 3", - "Weather:Sunrise Time", - "Weather:Sunset Time", - "Weather:Sunrise Duration", - "Weather:Sunset Duration", + "Weather Thunderstorm:Thunder Sound ID 0", "Weather Thunderstorm:Thunder Sound ID 1", + "Weather Thunderstorm:Thunder Sound ID 2", "Weather Thunderstorm:Thunder Sound ID 3", "Weather:Sunrise Time", + "Weather:Sunset Time", "Weather:Sunrise Duration", "Weather:Sunset Duration", "Weather:Hours Between Weather Changes", // AKA weather update time - "Weather Thunderstorm:Thunder Frequency", - "Weather Thunderstorm:Thunder Threshold", - - "Weather:EnvReduceColor", - "Weather:LerpCloseColor", - "Weather:BumpFadeColor", - "Weather:AlphaReduce", - "Weather:Minimum Time Between Environmental Sounds", - "Weather:Maximum Time Between Environmental Sounds", - "Weather:Sun Glare Fader Max", - "Weather:Sun Glare Fader Angle Max", - "Weather:Sun Glare Fader Color", - "Weather:Timescale Clouds", - "Weather:Precip Gravity", - "Weather:Rain Ripples", - "Weather:Rain Ripple Radius", - "Weather:Rain Ripples Per Drop", - "Weather:Rain Ripple Scale", - "Weather:Rain Ripple Speed", - "Weather:Fog Depth Change Speed", - "Weather:Sky Pre-Sunrise Time", - "Weather:Sky Post-Sunrise Time", - "Weather:Sky Pre-Sunset Time", - "Weather:Sky Post-Sunset Time", - "Weather:Ambient Pre-Sunrise Time", - "Weather:Ambient Post-Sunrise Time", - "Weather:Ambient Pre-Sunset Time", - "Weather:Ambient Post-Sunset Time", - "Weather:Fog Pre-Sunrise Time", - "Weather:Fog Post-Sunrise Time", - "Weather:Fog Pre-Sunset Time", - "Weather:Fog Post-Sunset Time", - "Weather:Sun Pre-Sunrise Time", - "Weather:Sun Post-Sunrise Time", - "Weather:Sun Pre-Sunset Time", - "Weather:Sun Post-Sunset Time", - "Weather:Stars Post-Sunset Start", - "Weather:Stars Pre-Sunrise Finish", - "Weather:Stars Fading Duration", - "Weather:Snow Ripples", - "Weather:Snow Ripple Radius", - "Weather:Snow Ripples Per Flake", - "Weather:Snow Ripple Scale", - "Weather:Snow Ripple Speed", - "Weather:Snow Gravity Scale", - "Weather:Snow High Kill", - "Weather:Snow Low Kill", - - "Weather Clear:Cloud Texture", - "Weather Clear:Clouds Maximum Percent", - "Weather Clear:Transition Delta", - "Weather Clear:Sky Sunrise Color", - "Weather Clear:Sky Day Color", - "Weather Clear:Sky Sunset Color", - "Weather Clear:Sky Night Color", - "Weather Clear:Fog Sunrise Color", - "Weather Clear:Fog Day Color", - "Weather Clear:Fog Sunset Color", - "Weather Clear:Fog Night Color", - "Weather Clear:Ambient Sunrise Color", - "Weather Clear:Ambient Day Color", - "Weather Clear:Ambient Sunset Color", - "Weather Clear:Ambient Night Color", - "Weather Clear:Sun Sunrise Color", - "Weather Clear:Sun Day Color", - "Weather Clear:Sun Sunset Color", - "Weather Clear:Sun Night Color", - "Weather Clear:Sun Disc Sunset Color", - "Weather Clear:Land Fog Day Depth", - "Weather Clear:Land Fog Night Depth", - "Weather Clear:Wind Speed", - "Weather Clear:Cloud Speed", - "Weather Clear:Glare View", - "Weather Clear:Ambient Loop Sound ID", - - "Weather Cloudy:Cloud Texture", - "Weather Cloudy:Clouds Maximum Percent", - "Weather Cloudy:Transition Delta", - "Weather Cloudy:Sky Sunrise Color", - "Weather Cloudy:Sky Day Color", - "Weather Cloudy:Sky Sunset Color", - "Weather Cloudy:Sky Night Color", - "Weather Cloudy:Fog Sunrise Color", - "Weather Cloudy:Fog Day Color", - "Weather Cloudy:Fog Sunset Color", - "Weather Cloudy:Fog Night Color", - "Weather Cloudy:Ambient Sunrise Color", - "Weather Cloudy:Ambient Day Color", - "Weather Cloudy:Ambient Sunset Color", - "Weather Cloudy:Ambient Night Color", - "Weather Cloudy:Sun Sunrise Color", - "Weather Cloudy:Sun Day Color", - "Weather Cloudy:Sun Sunset Color", - "Weather Cloudy:Sun Night Color", - "Weather Cloudy:Sun Disc Sunset Color", - "Weather Cloudy:Land Fog Day Depth", - "Weather Cloudy:Land Fog Night Depth", - "Weather Cloudy:Wind Speed", - "Weather Cloudy:Cloud Speed", - "Weather Cloudy:Glare View", - "Weather Cloudy:Ambient Loop Sound ID", - - "Weather Foggy:Cloud Texture", - "Weather Foggy:Clouds Maximum Percent", - "Weather Foggy:Transition Delta", - "Weather Foggy:Sky Sunrise Color", - "Weather Foggy:Sky Day Color", - "Weather Foggy:Sky Sunset Color", - "Weather Foggy:Sky Night Color", - "Weather Foggy:Fog Sunrise Color", - "Weather Foggy:Fog Day Color", - "Weather Foggy:Fog Sunset Color", - "Weather Foggy:Fog Night Color", - "Weather Foggy:Ambient Sunrise Color", - "Weather Foggy:Ambient Day Color", - "Weather Foggy:Ambient Sunset Color", - "Weather Foggy:Ambient Night Color", - "Weather Foggy:Sun Sunrise Color", - "Weather Foggy:Sun Day Color", - "Weather Foggy:Sun Sunset Color", - "Weather Foggy:Sun Night Color", - "Weather Foggy:Sun Disc Sunset Color", - "Weather Foggy:Land Fog Day Depth", - "Weather Foggy:Land Fog Night Depth", - "Weather Foggy:Wind Speed", - "Weather Foggy:Cloud Speed", - "Weather Foggy:Glare View", - "Weather Foggy:Ambient Loop Sound ID", - - "Weather Thunderstorm:Cloud Texture", - "Weather Thunderstorm:Clouds Maximum Percent", - "Weather Thunderstorm:Transition Delta", - "Weather Thunderstorm:Sky Sunrise Color", - "Weather Thunderstorm:Sky Day Color", - "Weather Thunderstorm:Sky Sunset Color", - "Weather Thunderstorm:Sky Night Color", - "Weather Thunderstorm:Fog Sunrise Color", - "Weather Thunderstorm:Fog Day Color", - "Weather Thunderstorm:Fog Sunset Color", - "Weather Thunderstorm:Fog Night Color", - "Weather Thunderstorm:Ambient Sunrise Color", - "Weather Thunderstorm:Ambient Day Color", - "Weather Thunderstorm:Ambient Sunset Color", - "Weather Thunderstorm:Ambient Night Color", - "Weather Thunderstorm:Sun Sunrise Color", - "Weather Thunderstorm:Sun Day Color", - "Weather Thunderstorm:Sun Sunset Color", - "Weather Thunderstorm:Sun Night Color", - "Weather Thunderstorm:Sun Disc Sunset Color", - "Weather Thunderstorm:Land Fog Day Depth", - "Weather Thunderstorm:Land Fog Night Depth", - "Weather Thunderstorm:Wind Speed", - "Weather Thunderstorm:Cloud Speed", - "Weather Thunderstorm:Glare View", - "Weather Thunderstorm:Rain Loop Sound ID", - "Weather Thunderstorm:Using Precip", - "Weather Thunderstorm:Rain Diameter", - "Weather Thunderstorm:Rain Height Min", - "Weather Thunderstorm:Rain Height Max", - "Weather Thunderstorm:Rain Threshold", - "Weather Thunderstorm:Max Raindrops", - "Weather Thunderstorm:Rain Entrance Speed", - "Weather Thunderstorm:Ambient Loop Sound ID", - "Weather Thunderstorm:Flash Decrement", - - "Weather Rain:Cloud Texture", - "Weather Rain:Clouds Maximum Percent", - "Weather Rain:Transition Delta", - "Weather Rain:Sky Sunrise Color", - "Weather Rain:Sky Day Color", - "Weather Rain:Sky Sunset Color", - "Weather Rain:Sky Night Color", - "Weather Rain:Fog Sunrise Color", - "Weather Rain:Fog Day Color", - "Weather Rain:Fog Sunset Color", - "Weather Rain:Fog Night Color", - "Weather Rain:Ambient Sunrise Color", - "Weather Rain:Ambient Day Color", - "Weather Rain:Ambient Sunset Color", - "Weather Rain:Ambient Night Color", - "Weather Rain:Sun Sunrise Color", - "Weather Rain:Sun Day Color", - "Weather Rain:Sun Sunset Color", - "Weather Rain:Sun Night Color", - "Weather Rain:Sun Disc Sunset Color", - "Weather Rain:Land Fog Day Depth", - "Weather Rain:Land Fog Night Depth", - "Weather Rain:Wind Speed", - "Weather Rain:Cloud Speed", - "Weather Rain:Glare View", - "Weather Rain:Rain Loop Sound ID", - "Weather Rain:Using Precip", - "Weather Rain:Rain Diameter", - "Weather Rain:Rain Height Min", - "Weather Rain:Rain Height Max", - "Weather Rain:Rain Threshold", - "Weather Rain:Rain Entrance Speed", - "Weather Rain:Ambient Loop Sound ID", + "Weather Thunderstorm:Thunder Frequency", "Weather Thunderstorm:Thunder Threshold", + + "Weather:EnvReduceColor", "Weather:LerpCloseColor", "Weather:BumpFadeColor", "Weather:AlphaReduce", + "Weather:Minimum Time Between Environmental Sounds", "Weather:Maximum Time Between Environmental Sounds", + "Weather:Sun Glare Fader Max", "Weather:Sun Glare Fader Angle Max", "Weather:Sun Glare Fader Color", + "Weather:Timescale Clouds", "Weather:Precip Gravity", "Weather:Rain Ripples", "Weather:Rain Ripple Radius", + "Weather:Rain Ripples Per Drop", "Weather:Rain Ripple Scale", "Weather:Rain Ripple Speed", + "Weather:Fog Depth Change Speed", "Weather:Sky Pre-Sunrise Time", "Weather:Sky Post-Sunrise Time", + "Weather:Sky Pre-Sunset Time", "Weather:Sky Post-Sunset Time", "Weather:Ambient Pre-Sunrise Time", + "Weather:Ambient Post-Sunrise Time", "Weather:Ambient Pre-Sunset Time", "Weather:Ambient Post-Sunset Time", + "Weather:Fog Pre-Sunrise Time", "Weather:Fog Post-Sunrise Time", "Weather:Fog Pre-Sunset Time", + "Weather:Fog Post-Sunset Time", "Weather:Sun Pre-Sunrise Time", "Weather:Sun Post-Sunrise Time", + "Weather:Sun Pre-Sunset Time", "Weather:Sun Post-Sunset Time", "Weather:Stars Post-Sunset Start", + "Weather:Stars Pre-Sunrise Finish", "Weather:Stars Fading Duration", "Weather:Snow Ripples", + "Weather:Snow Ripple Radius", "Weather:Snow Ripples Per Flake", "Weather:Snow Ripple Scale", + "Weather:Snow Ripple Speed", "Weather:Snow Gravity Scale", "Weather:Snow High Kill", "Weather:Snow Low Kill", + + "Weather Clear:Cloud Texture", "Weather Clear:Clouds Maximum Percent", "Weather Clear:Transition Delta", + "Weather Clear:Sky Sunrise Color", "Weather Clear:Sky Day Color", "Weather Clear:Sky Sunset Color", + "Weather Clear:Sky Night Color", "Weather Clear:Fog Sunrise Color", "Weather Clear:Fog Day Color", + "Weather Clear:Fog Sunset Color", "Weather Clear:Fog Night Color", "Weather Clear:Ambient Sunrise Color", + "Weather Clear:Ambient Day Color", "Weather Clear:Ambient Sunset Color", "Weather Clear:Ambient Night Color", + "Weather Clear:Sun Sunrise Color", "Weather Clear:Sun Day Color", "Weather Clear:Sun Sunset Color", + "Weather Clear:Sun Night Color", "Weather Clear:Sun Disc Sunset Color", "Weather Clear:Land Fog Day Depth", + "Weather Clear:Land Fog Night Depth", "Weather Clear:Wind Speed", "Weather Clear:Cloud Speed", + "Weather Clear:Glare View", "Weather Clear:Ambient Loop Sound ID", + + "Weather Cloudy:Cloud Texture", "Weather Cloudy:Clouds Maximum Percent", "Weather Cloudy:Transition Delta", + "Weather Cloudy:Sky Sunrise Color", "Weather Cloudy:Sky Day Color", "Weather Cloudy:Sky Sunset Color", + "Weather Cloudy:Sky Night Color", "Weather Cloudy:Fog Sunrise Color", "Weather Cloudy:Fog Day Color", + "Weather Cloudy:Fog Sunset Color", "Weather Cloudy:Fog Night Color", "Weather Cloudy:Ambient Sunrise Color", + "Weather Cloudy:Ambient Day Color", "Weather Cloudy:Ambient Sunset Color", "Weather Cloudy:Ambient Night Color", + "Weather Cloudy:Sun Sunrise Color", "Weather Cloudy:Sun Day Color", "Weather Cloudy:Sun Sunset Color", + "Weather Cloudy:Sun Night Color", "Weather Cloudy:Sun Disc Sunset Color", "Weather Cloudy:Land Fog Day Depth", + "Weather Cloudy:Land Fog Night Depth", "Weather Cloudy:Wind Speed", "Weather Cloudy:Cloud Speed", + "Weather Cloudy:Glare View", "Weather Cloudy:Ambient Loop Sound ID", + + "Weather Foggy:Cloud Texture", "Weather Foggy:Clouds Maximum Percent", "Weather Foggy:Transition Delta", + "Weather Foggy:Sky Sunrise Color", "Weather Foggy:Sky Day Color", "Weather Foggy:Sky Sunset Color", + "Weather Foggy:Sky Night Color", "Weather Foggy:Fog Sunrise Color", "Weather Foggy:Fog Day Color", + "Weather Foggy:Fog Sunset Color", "Weather Foggy:Fog Night Color", "Weather Foggy:Ambient Sunrise Color", + "Weather Foggy:Ambient Day Color", "Weather Foggy:Ambient Sunset Color", "Weather Foggy:Ambient Night Color", + "Weather Foggy:Sun Sunrise Color", "Weather Foggy:Sun Day Color", "Weather Foggy:Sun Sunset Color", + "Weather Foggy:Sun Night Color", "Weather Foggy:Sun Disc Sunset Color", "Weather Foggy:Land Fog Day Depth", + "Weather Foggy:Land Fog Night Depth", "Weather Foggy:Wind Speed", "Weather Foggy:Cloud Speed", + "Weather Foggy:Glare View", "Weather Foggy:Ambient Loop Sound ID", + + "Weather Thunderstorm:Cloud Texture", "Weather Thunderstorm:Clouds Maximum Percent", + "Weather Thunderstorm:Transition Delta", "Weather Thunderstorm:Sky Sunrise Color", + "Weather Thunderstorm:Sky Day Color", "Weather Thunderstorm:Sky Sunset Color", + "Weather Thunderstorm:Sky Night Color", "Weather Thunderstorm:Fog Sunrise Color", + "Weather Thunderstorm:Fog Day Color", "Weather Thunderstorm:Fog Sunset Color", + "Weather Thunderstorm:Fog Night Color", "Weather Thunderstorm:Ambient Sunrise Color", + "Weather Thunderstorm:Ambient Day Color", "Weather Thunderstorm:Ambient Sunset Color", + "Weather Thunderstorm:Ambient Night Color", "Weather Thunderstorm:Sun Sunrise Color", + "Weather Thunderstorm:Sun Day Color", "Weather Thunderstorm:Sun Sunset Color", + "Weather Thunderstorm:Sun Night Color", "Weather Thunderstorm:Sun Disc Sunset Color", + "Weather Thunderstorm:Land Fog Day Depth", "Weather Thunderstorm:Land Fog Night Depth", + "Weather Thunderstorm:Wind Speed", "Weather Thunderstorm:Cloud Speed", "Weather Thunderstorm:Glare View", + "Weather Thunderstorm:Rain Loop Sound ID", "Weather Thunderstorm:Using Precip", + "Weather Thunderstorm:Rain Diameter", "Weather Thunderstorm:Rain Height Min", + "Weather Thunderstorm:Rain Height Max", "Weather Thunderstorm:Rain Threshold", + "Weather Thunderstorm:Max Raindrops", "Weather Thunderstorm:Rain Entrance Speed", + "Weather Thunderstorm:Ambient Loop Sound ID", "Weather Thunderstorm:Flash Decrement", + + "Weather Rain:Cloud Texture", "Weather Rain:Clouds Maximum Percent", "Weather Rain:Transition Delta", + "Weather Rain:Sky Sunrise Color", "Weather Rain:Sky Day Color", "Weather Rain:Sky Sunset Color", + "Weather Rain:Sky Night Color", "Weather Rain:Fog Sunrise Color", "Weather Rain:Fog Day Color", + "Weather Rain:Fog Sunset Color", "Weather Rain:Fog Night Color", "Weather Rain:Ambient Sunrise Color", + "Weather Rain:Ambient Day Color", "Weather Rain:Ambient Sunset Color", "Weather Rain:Ambient Night Color", + "Weather Rain:Sun Sunrise Color", "Weather Rain:Sun Day Color", "Weather Rain:Sun Sunset Color", + "Weather Rain:Sun Night Color", "Weather Rain:Sun Disc Sunset Color", "Weather Rain:Land Fog Day Depth", + "Weather Rain:Land Fog Night Depth", "Weather Rain:Wind Speed", "Weather Rain:Cloud Speed", + "Weather Rain:Glare View", "Weather Rain:Rain Loop Sound ID", "Weather Rain:Using Precip", + "Weather Rain:Rain Diameter", "Weather Rain:Rain Height Min", "Weather Rain:Rain Height Max", + "Weather Rain:Rain Threshold", "Weather Rain:Rain Entrance Speed", "Weather Rain:Ambient Loop Sound ID", "Weather Rain:Max Raindrops", - "Weather Overcast:Cloud Texture", - "Weather Overcast:Clouds Maximum Percent", - "Weather Overcast:Transition Delta", - "Weather Overcast:Sky Sunrise Color", - "Weather Overcast:Sky Day Color", - "Weather Overcast:Sky Sunset Color", - "Weather Overcast:Sky Night Color", - "Weather Overcast:Fog Sunrise Color", - "Weather Overcast:Fog Day Color", - "Weather Overcast:Fog Sunset Color", - "Weather Overcast:Fog Night Color", - "Weather Overcast:Ambient Sunrise Color", - "Weather Overcast:Ambient Day Color", - "Weather Overcast:Ambient Sunset Color", - "Weather Overcast:Ambient Night Color", - "Weather Overcast:Sun Sunrise Color", - "Weather Overcast:Sun Day Color", - "Weather Overcast:Sun Sunset Color", - "Weather Overcast:Sun Night Color", - "Weather Overcast:Sun Disc Sunset Color", - "Weather Overcast:Land Fog Day Depth", - "Weather Overcast:Land Fog Night Depth", - "Weather Overcast:Wind Speed", - "Weather Overcast:Cloud Speed", - "Weather Overcast:Glare View", - "Weather Overcast:Ambient Loop Sound ID", - - "Weather Ashstorm:Cloud Texture", - "Weather Ashstorm:Clouds Maximum Percent", - "Weather Ashstorm:Transition Delta", - "Weather Ashstorm:Sky Sunrise Color", - "Weather Ashstorm:Sky Day Color", - "Weather Ashstorm:Sky Sunset Color", - "Weather Ashstorm:Sky Night Color", - "Weather Ashstorm:Fog Sunrise Color", - "Weather Ashstorm:Fog Day Color", - "Weather Ashstorm:Fog Sunset Color", - "Weather Ashstorm:Fog Night Color", - "Weather Ashstorm:Ambient Sunrise Color", - "Weather Ashstorm:Ambient Day Color", - "Weather Ashstorm:Ambient Sunset Color", - "Weather Ashstorm:Ambient Night Color", - "Weather Ashstorm:Sun Sunrise Color", - "Weather Ashstorm:Sun Day Color", - "Weather Ashstorm:Sun Sunset Color", - "Weather Ashstorm:Sun Night Color", - "Weather Ashstorm:Sun Disc Sunset Color", - "Weather Ashstorm:Land Fog Day Depth", - "Weather Ashstorm:Land Fog Night Depth", - "Weather Ashstorm:Wind Speed", - "Weather Ashstorm:Cloud Speed", - "Weather Ashstorm:Glare View", - "Weather Ashstorm:Ambient Loop Sound ID", + "Weather Overcast:Cloud Texture", "Weather Overcast:Clouds Maximum Percent", + "Weather Overcast:Transition Delta", "Weather Overcast:Sky Sunrise Color", "Weather Overcast:Sky Day Color", + "Weather Overcast:Sky Sunset Color", "Weather Overcast:Sky Night Color", "Weather Overcast:Fog Sunrise Color", + "Weather Overcast:Fog Day Color", "Weather Overcast:Fog Sunset Color", "Weather Overcast:Fog Night Color", + "Weather Overcast:Ambient Sunrise Color", "Weather Overcast:Ambient Day Color", + "Weather Overcast:Ambient Sunset Color", "Weather Overcast:Ambient Night Color", + "Weather Overcast:Sun Sunrise Color", "Weather Overcast:Sun Day Color", "Weather Overcast:Sun Sunset Color", + "Weather Overcast:Sun Night Color", "Weather Overcast:Sun Disc Sunset Color", + "Weather Overcast:Land Fog Day Depth", "Weather Overcast:Land Fog Night Depth", "Weather Overcast:Wind Speed", + "Weather Overcast:Cloud Speed", "Weather Overcast:Glare View", "Weather Overcast:Ambient Loop Sound ID", + + "Weather Ashstorm:Cloud Texture", "Weather Ashstorm:Clouds Maximum Percent", + "Weather Ashstorm:Transition Delta", "Weather Ashstorm:Sky Sunrise Color", "Weather Ashstorm:Sky Day Color", + "Weather Ashstorm:Sky Sunset Color", "Weather Ashstorm:Sky Night Color", "Weather Ashstorm:Fog Sunrise Color", + "Weather Ashstorm:Fog Day Color", "Weather Ashstorm:Fog Sunset Color", "Weather Ashstorm:Fog Night Color", + "Weather Ashstorm:Ambient Sunrise Color", "Weather Ashstorm:Ambient Day Color", + "Weather Ashstorm:Ambient Sunset Color", "Weather Ashstorm:Ambient Night Color", + "Weather Ashstorm:Sun Sunrise Color", "Weather Ashstorm:Sun Day Color", "Weather Ashstorm:Sun Sunset Color", + "Weather Ashstorm:Sun Night Color", "Weather Ashstorm:Sun Disc Sunset Color", + "Weather Ashstorm:Land Fog Day Depth", "Weather Ashstorm:Land Fog Night Depth", "Weather Ashstorm:Wind Speed", + "Weather Ashstorm:Cloud Speed", "Weather Ashstorm:Glare View", "Weather Ashstorm:Ambient Loop Sound ID", "Weather Ashstorm:Storm Threshold", - "Weather Blight:Cloud Texture", - "Weather Blight:Clouds Maximum Percent", - "Weather Blight:Transition Delta", - "Weather Blight:Sky Sunrise Color", - "Weather Blight:Sky Day Color", - "Weather Blight:Sky Sunset Color", - "Weather Blight:Sky Night Color", - "Weather Blight:Fog Sunrise Color", - "Weather Blight:Fog Day Color", - "Weather Blight:Fog Sunset Color", - "Weather Blight:Fog Night Color", - "Weather Blight:Ambient Sunrise Color", - "Weather Blight:Ambient Day Color", - "Weather Blight:Ambient Sunset Color", - "Weather Blight:Ambient Night Color", - "Weather Blight:Sun Sunrise Color", - "Weather Blight:Sun Day Color", - "Weather Blight:Sun Sunset Color", - "Weather Blight:Sun Night Color", - "Weather Blight:Sun Disc Sunset Color", - "Weather Blight:Land Fog Day Depth", - "Weather Blight:Land Fog Night Depth", - "Weather Blight:Wind Speed", - "Weather Blight:Cloud Speed", - "Weather Blight:Glare View", - "Weather Blight:Ambient Loop Sound ID", - "Weather Blight:Storm Threshold", + "Weather Blight:Cloud Texture", "Weather Blight:Clouds Maximum Percent", "Weather Blight:Transition Delta", + "Weather Blight:Sky Sunrise Color", "Weather Blight:Sky Day Color", "Weather Blight:Sky Sunset Color", + "Weather Blight:Sky Night Color", "Weather Blight:Fog Sunrise Color", "Weather Blight:Fog Day Color", + "Weather Blight:Fog Sunset Color", "Weather Blight:Fog Night Color", "Weather Blight:Ambient Sunrise Color", + "Weather Blight:Ambient Day Color", "Weather Blight:Ambient Sunset Color", "Weather Blight:Ambient Night Color", + "Weather Blight:Sun Sunrise Color", "Weather Blight:Sun Day Color", "Weather Blight:Sun Sunset Color", + "Weather Blight:Sun Night Color", "Weather Blight:Sun Disc Sunset Color", "Weather Blight:Land Fog Day Depth", + "Weather Blight:Land Fog Night Depth", "Weather Blight:Wind Speed", "Weather Blight:Cloud Speed", + "Weather Blight:Glare View", "Weather Blight:Ambient Loop Sound ID", "Weather Blight:Storm Threshold", "Weather Blight:Disease Chance", // for Bloodmoon - "Weather Snow:Cloud Texture", - "Weather Snow:Clouds Maximum Percent", - "Weather Snow:Transition Delta", - "Weather Snow:Sky Sunrise Color", - "Weather Snow:Sky Day Color", - "Weather Snow:Sky Sunset Color", - "Weather Snow:Sky Night Color", - "Weather Snow:Fog Sunrise Color", - "Weather Snow:Fog Day Color", - "Weather Snow:Fog Sunset Color", - "Weather Snow:Fog Night Color", - "Weather Snow:Ambient Sunrise Color", - "Weather Snow:Ambient Day Color", - "Weather Snow:Ambient Sunset Color", - "Weather Snow:Ambient Night Color", - "Weather Snow:Sun Sunrise Color", - "Weather Snow:Sun Day Color", - "Weather Snow:Sun Sunset Color", - "Weather Snow:Sun Night Color", - "Weather Snow:Sun Disc Sunset Color", - "Weather Snow:Land Fog Day Depth", - "Weather Snow:Land Fog Night Depth", - "Weather Snow:Wind Speed", - "Weather Snow:Cloud Speed", - "Weather Snow:Glare View", - "Weather Snow:Snow Diameter", - "Weather Snow:Snow Height Min", - "Weather Snow:Snow Height Max", - "Weather Snow:Snow Entrance Speed", - "Weather Snow:Max Snowflakes", - "Weather Snow:Ambient Loop Sound ID", - "Weather Snow:Snow Threshold", + "Weather Snow:Cloud Texture", "Weather Snow:Clouds Maximum Percent", "Weather Snow:Transition Delta", + "Weather Snow:Sky Sunrise Color", "Weather Snow:Sky Day Color", "Weather Snow:Sky Sunset Color", + "Weather Snow:Sky Night Color", "Weather Snow:Fog Sunrise Color", "Weather Snow:Fog Day Color", + "Weather Snow:Fog Sunset Color", "Weather Snow:Fog Night Color", "Weather Snow:Ambient Sunrise Color", + "Weather Snow:Ambient Day Color", "Weather Snow:Ambient Sunset Color", "Weather Snow:Ambient Night Color", + "Weather Snow:Sun Sunrise Color", "Weather Snow:Sun Day Color", "Weather Snow:Sun Sunset Color", + "Weather Snow:Sun Night Color", "Weather Snow:Sun Disc Sunset Color", "Weather Snow:Land Fog Day Depth", + "Weather Snow:Land Fog Night Depth", "Weather Snow:Wind Speed", "Weather Snow:Cloud Speed", + "Weather Snow:Glare View", "Weather Snow:Snow Diameter", "Weather Snow:Snow Height Min", + "Weather Snow:Snow Height Max", "Weather Snow:Snow Entrance Speed", "Weather Snow:Max Snowflakes", + "Weather Snow:Ambient Loop Sound ID", "Weather Snow:Snow Threshold", // for Bloodmoon - "Weather Blizzard:Cloud Texture", - "Weather Blizzard:Clouds Maximum Percent", - "Weather Blizzard:Transition Delta", - "Weather Blizzard:Sky Sunrise Color", - "Weather Blizzard:Sky Day Color", - "Weather Blizzard:Sky Sunset Color", - "Weather Blizzard:Sky Night Color", - "Weather Blizzard:Fog Sunrise Color", - "Weather Blizzard:Fog Day Color", - "Weather Blizzard:Fog Sunset Color", - "Weather Blizzard:Fog Night Color", - "Weather Blizzard:Ambient Sunrise Color", - "Weather Blizzard:Ambient Day Color", - "Weather Blizzard:Ambient Sunset Color", - "Weather Blizzard:Ambient Night Color", - "Weather Blizzard:Sun Sunrise Color", - "Weather Blizzard:Sun Day Color", - "Weather Blizzard:Sun Sunset Color", - "Weather Blizzard:Sun Night Color", - "Weather Blizzard:Sun Disc Sunset Color", - "Weather Blizzard:Land Fog Day Depth", - "Weather Blizzard:Land Fog Night Depth", - "Weather Blizzard:Wind Speed", - "Weather Blizzard:Cloud Speed", - "Weather Blizzard:Glare View", - "Weather Blizzard:Ambient Loop Sound ID", + "Weather Blizzard:Cloud Texture", "Weather Blizzard:Clouds Maximum Percent", + "Weather Blizzard:Transition Delta", "Weather Blizzard:Sky Sunrise Color", "Weather Blizzard:Sky Day Color", + "Weather Blizzard:Sky Sunset Color", "Weather Blizzard:Sky Night Color", "Weather Blizzard:Fog Sunrise Color", + "Weather Blizzard:Fog Day Color", "Weather Blizzard:Fog Sunset Color", "Weather Blizzard:Fog Night Color", + "Weather Blizzard:Ambient Sunrise Color", "Weather Blizzard:Ambient Day Color", + "Weather Blizzard:Ambient Sunset Color", "Weather Blizzard:Ambient Night Color", + "Weather Blizzard:Sun Sunrise Color", "Weather Blizzard:Sun Day Color", "Weather Blizzard:Sun Sunset Color", + "Weather Blizzard:Sun Night Color", "Weather Blizzard:Sun Disc Sunset Color", + "Weather Blizzard:Land Fog Day Depth", "Weather Blizzard:Land Fog Night Depth", "Weather Blizzard:Wind Speed", + "Weather Blizzard:Cloud Speed", "Weather Blizzard:Glare View", "Weather Blizzard:Ambient Loop Sound ID", "Weather Blizzard:Storm Threshold", // moons - "Moons:Secunda Size", - "Moons:Secunda Axis Offset", - "Moons:Secunda Speed", - "Moons:Secunda Daily Increment", - "Moons:Secunda Moon Shadow Early Fade Angle", - "Moons:Secunda Fade Start Angle", - "Moons:Secunda Fade End Angle", - "Moons:Secunda Fade In Start", - "Moons:Secunda Fade In Finish", - "Moons:Secunda Fade Out Start", - "Moons:Secunda Fade Out Finish", - "Moons:Masser Size", - "Moons:Masser Axis Offset", - "Moons:Masser Speed", - "Moons:Masser Daily Increment", - "Moons:Masser Moon Shadow Early Fade Angle", - "Moons:Masser Fade Start Angle", - "Moons:Masser Fade End Angle", - "Moons:Masser Fade In Start", - "Moons:Masser Fade In Finish", - "Moons:Masser Fade Out Start", - "Moons:Masser Fade Out Finish", - "Moons:Script Color", + "Moons:Secunda Size", "Moons:Secunda Axis Offset", "Moons:Secunda Speed", "Moons:Secunda Daily Increment", + "Moons:Secunda Moon Shadow Early Fade Angle", "Moons:Secunda Fade Start Angle", "Moons:Secunda Fade End Angle", + "Moons:Secunda Fade In Start", "Moons:Secunda Fade In Finish", "Moons:Secunda Fade Out Start", + "Moons:Secunda Fade Out Finish", "Moons:Masser Size", "Moons:Masser Axis Offset", "Moons:Masser Speed", + "Moons:Masser Daily Increment", "Moons:Masser Moon Shadow Early Fade Angle", "Moons:Masser Fade Start Angle", + "Moons:Masser Fade End Angle", "Moons:Masser Fade In Start", "Moons:Masser Fade In Finish", + "Moons:Masser Fade Out Start", "Moons:Masser Fade Out Finish", "Moons:Script Color", // werewolf (Bloodmoon) "General:Werewolf FOV", @@ -640,107 +256,127 @@ MwIniImporter::MwIniImporter() 0 }; - for(int i=0; map[i][0]; i++) { + for (int i = 0; map[i][0]; i++) + { mMergeMap.insert(std::make_pair(map[i][0], map[i][1])); } - for(int i=0; fallback[i]; i++) { + for (int i = 0; fallback[i]; i++) + { mMergeFallback.emplace_back(fallback[i]); } } -void MwIniImporter::setVerbose(bool verbose) { +void MwIniImporter::setVerbose(bool verbose) +{ mVerbose = verbose; } -MwIniImporter::multistrmap MwIniImporter::loadIniFile(const boost::filesystem::path& filename) const { - std::cout << "load ini file: " << filename << std::endl; +MwIniImporter::multistrmap MwIniImporter::loadIniFile(const std::filesystem::path& filename) const +{ + std::cout << "load ini file: " << Files::pathToUnicodeString(filename) << std::endl; std::string section(""); MwIniImporter::multistrmap map; - bfs::ifstream file((bfs::path(filename))); + std::ifstream file(filename); ToUTF8::Utf8Encoder encoder(mEncoding); std::string line; - while (std::getline(file, line)) { + while (std::getline(file, line)) + { - line = encoder.getUtf8(line); + std::string_view utf8 = encoder.getUtf8(line); // unify Unix-style and Windows file ending - if (!(line.empty()) && (line[line.length()-1]) == '\r') { - line = line.substr(0, line.length()-1); + if (!(utf8.empty()) && (utf8[utf8.length() - 1]) == '\r') + { + utf8 = utf8.substr(0, utf8.length() - 1); } - if(line.empty()) { + if (utf8.empty()) + { continue; } - if(line[0] == '[') { - int pos = static_cast(line.find(']')); - if(pos < 2) { - std::cout << "Warning: ini file wrongly formatted (" << line << "). Line ignored." << std::endl; + if (utf8[0] == '[') + { + int pos = static_cast(utf8.find(']')); + if (pos < 2) + { + std::cout << "Warning: ini file wrongly formatted (" << utf8 << "). Line ignored." << std::endl; continue; } - section = line.substr(1, line.find(']')-1); + section = utf8.substr(1, utf8.find(']') - 1); continue; } - int comment_pos = static_cast(line.find(';')); - if(comment_pos > 0) { - line = line.substr(0,comment_pos); + int comment_pos = static_cast(utf8.find(';')); + if (comment_pos > 0) + { + utf8 = utf8.substr(0, comment_pos); } - int pos = static_cast(line.find('=')); - if(pos < 1) { + int pos = static_cast(utf8.find('=')); + if (pos < 1) + { continue; } - std::string key(section + ":" + line.substr(0,pos)); - std::string value(line.substr(pos+1)); - if(value.empty()) { + std::string key(section + ":" + std::string(utf8.substr(0, pos))); + const std::string_view value(utf8.substr(pos + 1)); + if (value.empty()) + { std::cout << "Warning: ignored empty value for key '" << key << "'." << std::endl; continue; } - if(map.find(key) == map.end()) { - map.insert( std::make_pair (key, std::vector() ) ); - } - map[key].push_back(value); + auto it = map.find(key); + + if (it == map.end()) + it = map.emplace_hint(it, std::move(key), std::vector()); + + it->second.push_back(std::string(value)); } return map; } -MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const boost::filesystem::path& filename) { - std::cout << "load cfg file: " << filename << std::endl; +MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const std::filesystem::path& filename) +{ + std::cout << "load cfg file: " << Files::pathToUnicodeString(filename) << std::endl; MwIniImporter::multistrmap map; - bfs::ifstream file((bfs::path(filename))); + std::ifstream file(filename); std::string line; - while (std::getline(file, 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) { - line = line.substr(0,comment_pos); + if (comment_pos > 0) + { + line = line.substr(0, comment_pos); } - if(line.empty()) { + if (line.empty()) + { continue; } int pos = static_cast(line.find('=')); - if(pos < 1) { + if (pos < 1) + { continue; } - std::string key(line.substr(0,pos)); - std::string value(line.substr(pos+1)); + std::string key(line.substr(0, pos)); + std::string value(line.substr(pos + 1)); - if(map.find(key) == map.end()) { - map.insert( std::make_pair (key, std::vector() ) ); + if (map.find(key) == map.end()) + { + map.insert(std::make_pair(key, std::vector())); } map[key].push_back(value); } @@ -748,11 +384,15 @@ MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const boost::filesystem::p return map; } -void MwIniImporter::merge(multistrmap &cfg, const multistrmap &ini) const { +void MwIniImporter::merge(multistrmap& cfg, const multistrmap& ini) const +{ multistrmap::const_iterator iniIt; - for(strmap::const_iterator it=mMergeMap.begin(); it!=mMergeMap.end(); ++it) { - if((iniIt = ini.find(it->second)) != ini.end()) { - for(std::vector::const_iterator vc = iniIt->second.begin(); vc != iniIt->second.end(); ++vc) { + for (strmap::const_iterator it = mMergeMap.begin(); it != mMergeMap.end(); ++it) + { + if ((iniIt = ini.find(it->second)) != ini.end()) + { + for (std::vector::const_iterator vc = iniIt->second.begin(); vc != iniIt->second.end(); ++vc) + { cfg.erase(it->first); insertMultistrmap(cfg, it->first, *vc); } @@ -760,74 +400,79 @@ void MwIniImporter::merge(multistrmap &cfg, const multistrmap &ini) const { } } -void MwIniImporter::mergeFallback(multistrmap &cfg, const multistrmap &ini) const { +void MwIniImporter::mergeFallback(multistrmap& cfg, const multistrmap& ini) const +{ cfg.erase("fallback"); multistrmap::const_iterator iniIt; - for(std::vector::const_iterator it=mMergeFallback.begin(); it!=mMergeFallback.end(); ++it) { - if((iniIt = ini.find(*it)) != ini.end()) { - for(std::vector::const_iterator vc = iniIt->second.begin(); vc != iniIt->second.end(); ++vc) { + for (std::vector::const_iterator it = mMergeFallback.begin(); it != mMergeFallback.end(); ++it) + { + if ((iniIt = ini.find(*it)) != ini.end()) + { + for (std::vector::const_iterator vc = iniIt->second.begin(); vc != iniIt->second.end(); ++vc) + { std::string value(*it); - std::replace( value.begin(), value.end(), ' ', '_' ); - std::replace( value.begin(), value.end(), ':', '_' ); - value.append(",").append(vc->substr(0,vc->length())); + std::replace(value.begin(), value.end(), ' ', '_'); + std::replace(value.begin(), value.end(), ':', '_'); + value.append(",").append(vc->substr(0, vc->length())); insertMultistrmap(cfg, "fallback", value); } } } } -void MwIniImporter::insertMultistrmap(multistrmap &cfg, const std::string& key, const std::string& value) { - const multistrmap::const_iterator it = cfg.find(key); - if(it == cfg.end()) { - cfg.insert(std::make_pair (key, std::vector() )); +void MwIniImporter::insertMultistrmap(multistrmap& cfg, const std::string& key, const std::string& value) +{ + const auto it = cfg.find(key); + if (it == cfg.end()) + { + cfg.insert(std::make_pair(key, std::vector())); } cfg[key].push_back(value); } -void MwIniImporter::importArchives(multistrmap &cfg, const multistrmap &ini) const { +void MwIniImporter::importArchives(multistrmap& cfg, const multistrmap& ini) const +{ std::vector archives; - std::string baseArchive("Archives:Archive "); - std::string archive; // Search archives listed in ini file - multistrmap::const_iterator it = ini.begin(); - for(int i=0; it != ini.end(); i++) { - archive = baseArchive; - archive.append(std::to_string(i)); + auto it = ini.begin(); + for (int i = 0; it != ini.end(); i++) + { + std::string archive("Archives:Archive " + std::to_string(i)); it = ini.find(archive); - if(it == ini.end()) { + if (it == ini.end()) + { break; } - for(std::vector::const_iterator entry = it->second.begin(); entry!=it->second.end(); ++entry) { + for (std::vector::const_iterator entry = it->second.begin(); entry != it->second.end(); ++entry) + { archives.push_back(*entry); } } cfg.erase("fallback-archive"); - cfg.insert( std::make_pair > ("fallback-archive", std::vector())); + cfg.insert(std::make_pair>("fallback-archive", std::vector())); // Add Morrowind.bsa by default, since Vanilla loads this archive even if it // does not appears in the ini file cfg["fallback-archive"].push_back("Morrowind.bsa"); - for(std::vector::const_iterator iter=archives.begin(); iter!=archives.end(); ++iter) { + for (auto iter = archives.begin(); iter != archives.end(); ++iter) + { cfg["fallback-archive"].push_back(*iter); } } -void MwIniImporter::dependencySortStep(std::string& element, MwIniImporter::dependencyList& source, std::vector& result) +void MwIniImporter::dependencySortStep( + std::string& element, MwIniImporter::dependencyList& source, std::vector& result) { auto iter = std::find_if( - source.begin(), - source.end(), - [&element](std::pair< std::string, std::vector >& sourceElement) - { + source.begin(), source.end(), [&element](std::pair>& sourceElement) { return sourceElement.first == element; - } - ); + }); if (iter != source.end()) { auto foundElement = std::move(*iter); @@ -850,15 +495,15 @@ std::vector MwIniImporter::dependencySort(MwIniImporter::dependency return result; } -std::vector::iterator MwIniImporter::findString(std::vector& source, const std::string& string) +std::vector::iterator MwIniImporter::findString( + std::vector& source, const std::string& string) { - return std::find_if(source.begin(), source.end(), [&string](const std::string& sourceString) - { - return Misc::StringUtils::ciEqual(sourceString, string); - }); + return std::find_if(source.begin(), source.end(), + [&string](const std::string& sourceString) { return Misc::StringUtils::ciEqual(sourceString, string); }); } -void MwIniImporter::addPaths(std::vector& output, std::vector input) { +void MwIniImporter::addPaths(std::vector& output, std::vector input) +{ for (auto& path : input) { if (path.front() == '"') @@ -866,18 +511,18 @@ void MwIniImporter::addPaths(std::vector& output, std:: // Drop first and last characters - quotation marks path = path.substr(1, path.size() - 2); } - output.emplace_back(path); + output.emplace_back(Files::pathFromUnicodeString(path)); } } -void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, const boost::filesystem::path& iniFilename) const +void MwIniImporter::importGameFiles( + multistrmap& cfg, const multistrmap& ini, const std::filesystem::path& iniFilename) const { - std::vector> contentFiles; - std::string baseGameFile("Game Files:GameFile"); + std::vector> contentFiles; std::time_t defaultTime = 0; ToUTF8::Utf8Encoder encoder(mEncoding); - std::vector dataPaths; + std::vector dataPaths; if (cfg.count("data")) addPaths(dataPaths, cfg["data"]); @@ -886,27 +531,23 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co dataPaths.push_back(iniFilename.parent_path() /= "Data Files"); - multistrmap::const_iterator it = ini.begin(); - for (int i=0; it != ini.end(); i++) + auto it = ini.begin(); + for (int i = 0; it != ini.end(); i++) { - std::string gameFile = baseGameFile; - gameFile.append(std::to_string(i)); + std::string gameFile("Game Files:GameFile" + std::to_string(i)); it = ini.find(gameFile); - if(it == ini.end()) + if (it == ini.end()) break; - for(std::vector::const_iterator entry = it->second.begin(); entry!=it->second.end(); ++entry) + for (const std::string& file : it->second) { - std::string filetype(entry->substr(entry->length()-3)); - Misc::StringUtils::lowerCaseInPlace(filetype); - - if(filetype.compare("esm") == 0 || filetype.compare("esp") == 0) + if (Misc::StringUtils::ciEndsWith(file, "esm") || Misc::StringUtils::ciEndsWith(file, "esp")) { bool found = false; - for (auto & dataPath : dataPaths) + for (auto& dataPath : dataPaths) { - boost::filesystem::path path = dataPath / *entry; + std::filesystem::path path = dataPath / file; std::time_t time = lastWriteTime(path, defaultTime); if (time != defaultTime) { @@ -916,13 +557,13 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co } } if (!found) - std::cout << "Warning: " << *entry << " not found, ignoring" << std::endl; + std::cout << "Warning: " << file << " not found, ignoring" << std::endl; } } } cfg.erase("content"); - cfg.insert( std::make_pair("content", std::vector() ) ); + cfg.insert(std::make_pair("content", std::vector())); // sort by timestamp sort(contentFiles.begin(), contentFiles.end()); @@ -933,28 +574,28 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co reader.setEncoder(&encoder); for (auto& file : contentFiles) { - reader.open(file.second.string()); + reader.open(file.second); std::vector dependencies; for (auto& gameFile : reader.getGameFiles()) { dependencies.push_back(gameFile.name); } - unsortedFiles.emplace_back(boost::filesystem::path(reader.getName()).filename().string(), dependencies); + unsortedFiles.emplace_back(Files::pathToUnicodeString(reader.getName().filename()), dependencies); reader.close(); } - auto sortedFiles = dependencySort(unsortedFiles); + auto sortedFiles = dependencySort(std::move(unsortedFiles)); // hard-coded dependency Morrowind - Tribunal - Bloodmoon - if(findString(sortedFiles, "Morrowind.esm") != sortedFiles.end()) + if (findString(sortedFiles, "Morrowind.esm") != sortedFiles.end()) { - auto tribunalIter = findString(sortedFiles, "Tribunal.esm"); + auto tribunalIter = findString(sortedFiles, "Tribunal.esm"); auto bloodmoonIter = findString(sortedFiles, "Bloodmoon.esm"); if (bloodmoonIter != sortedFiles.end() && tribunalIter != sortedFiles.end()) { size_t bloodmoonIndex = std::distance(sortedFiles.begin(), bloodmoonIter); - size_t tribunalIndex = std::distance(sortedFiles.begin(), tribunalIter); + size_t tribunalIndex = std::distance(sortedFiles.begin(), tribunalIter); if (bloodmoonIndex < tribunalIndex) tribunalIndex++; sortedFiles.insert(bloodmoonIter, *tribunalIter); @@ -966,34 +607,36 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co cfg["content"].push_back(file); } -void MwIniImporter::writeToFile(std::ostream &out, const multistrmap &cfg) { +void MwIniImporter::writeToFile(std::ostream& out, const multistrmap& cfg) +{ - for(multistrmap::const_iterator it=cfg.begin(); it != cfg.end(); ++it) { - for(std::vector::const_iterator entry=it->second.begin(); entry != it->second.end(); ++entry) { + for (multistrmap::const_iterator it = cfg.begin(); it != cfg.end(); ++it) + { + for (auto entry = it->second.begin(); entry != it->second.end(); ++entry) + { out << (it->first) << "=" << (*entry) << std::endl; } } } -void MwIniImporter::setInputEncoding(const ToUTF8::FromType &encoding) +void MwIniImporter::setInputEncoding(const ToUTF8::FromType& encoding) { - mEncoding = encoding; + mEncoding = encoding; } -std::time_t MwIniImporter::lastWriteTime(const boost::filesystem::path& filename, std::time_t defaultTime) +std::time_t MwIniImporter::lastWriteTime(const std::filesystem::path& filename, std::time_t defaultTime) { std::time_t writeTime(defaultTime); - if (boost::filesystem::exists(filename)) + if (std::filesystem::exists(filename)) { - boost::filesystem::path resolved = boost::filesystem::canonical(filename); - writeTime = boost::filesystem::last_write_time(resolved); + std::filesystem::path resolved = std::filesystem::canonical(filename); + const auto time = std::filesystem::last_write_time(resolved); + writeTime = Misc::toTimeT(time); // print timestamp - const int size=1024; - char timeStrBuffer[size]; - if (std::strftime(timeStrBuffer, size, "%x %X", localtime(&writeTime)) > 0) - std::cout << "content file: " << resolved << " timestamp = (" << writeTime << - ") " << timeStrBuffer << std::endl; + const auto str = Misc::fileTimeToString(time, "%x %X"); + if (!str.empty()) + std::cout << "content file: " << resolved << " timestamp = (" << writeTime << ") " << str << std::endl; } return writeTime; } diff --git a/apps/mwiniimporter/importer.hpp b/apps/mwiniimporter/importer.hpp index 7b710a4a40a..4c42caf5a3d 100644 --- a/apps/mwiniimporter/importer.hpp +++ b/apps/mwiniimporter/importer.hpp @@ -1,44 +1,45 @@ #ifndef MWINIIMPORTER_IMPORTER #define MWINIIMPORTER_IMPORTER 1 -#include -#include -#include #include +#include #include -#include +#include +#include +#include #include -class MwIniImporter { - public: +class MwIniImporter +{ +public: typedef std::map strmap; - typedef std::map > multistrmap; - typedef std::vector< std::pair< std::string, std::vector > > dependencyList; + typedef std::map> multistrmap; + typedef std::vector>> dependencyList; MwIniImporter(); - void setInputEncoding(const ToUTF8::FromType& encoding); - void setVerbose(bool verbose); - multistrmap loadIniFile(const boost::filesystem::path& filename) const; - static multistrmap loadCfgFile(const boost::filesystem::path& filename); - void merge(multistrmap &cfg, const multistrmap &ini) const; - void mergeFallback(multistrmap &cfg, const multistrmap &ini) const; - void importGameFiles(multistrmap &cfg, const multistrmap &ini, - const boost::filesystem::path& iniFilename) const; - void importArchives(multistrmap &cfg, const multistrmap &ini) const; - static void writeToFile(std::ostream &out, const multistrmap &cfg); + void setInputEncoding(const ToUTF8::FromType& encoding); + void setVerbose(bool verbose); + multistrmap loadIniFile(const std::filesystem::path& filename) const; + static multistrmap loadCfgFile(const std::filesystem::path& filename); + void merge(multistrmap& cfg, const multistrmap& ini) const; + void mergeFallback(multistrmap& cfg, const multistrmap& ini) const; + void importGameFiles(multistrmap& cfg, const multistrmap& ini, const std::filesystem::path& iniFilename) const; + void importArchives(multistrmap& cfg, const multistrmap& ini) const; + static void writeToFile(std::ostream& out, const multistrmap& cfg); static std::vector dependencySort(MwIniImporter::dependencyList source); - private: - static void dependencySortStep(std::string& element, MwIniImporter::dependencyList& source, std::vector& result); +private: + static void dependencySortStep( + std::string& element, MwIniImporter::dependencyList& source, std::vector& result); static std::vector::iterator findString(std::vector& source, const std::string& string); - static void insertMultistrmap(multistrmap &cfg, const std::string& key, const std::string& value); - static void addPaths(std::vector& output, std::vector input); + static void insertMultistrmap(multistrmap& cfg, const std::string& key, const std::string& value); + static void addPaths(std::vector& output, std::vector input); /// \return file's "last modified time", used in original MW to determine plug-in load order - static std::time_t lastWriteTime(const boost::filesystem::path& filename, std::time_t defaultTime); + static std::time_t lastWriteTime(const std::filesystem::path& filename, std::time_t defaultTime); bool mVerbose; strmap mMergeMap; diff --git a/apps/mwiniimporter/main.cpp b/apps/mwiniimporter/main.cpp index 8a2aeb603ba..0beb2b38cc6 100644 --- a/apps/mwiniimporter/main.cpp +++ b/apps/mwiniimporter/main.cpp @@ -1,44 +1,51 @@ #include "importer.hpp" +#include +#include #include #include -#include -#include + +#include +#include namespace bpo = boost::program_options; -namespace bfs = boost::filesystem; +namespace sfs = std::filesystem; #ifndef _WIN32 -int main(int argc, char *argv[]) { +int main(int argc, char* argv[]) +{ #else // Include on Windows only -#include +#include +#include class utf8argv { public: - - utf8argv(int argc, wchar_t *wargv[]) + utf8argv(int argc, wchar_t* wargv[]) { args.reserve(argc); - argv = new const char *[argc]; + argv = new const char*[argc]; - for (int i = 0; i < argc; ++i) { - args.push_back(boost::locale::conv::utf_to_utf(wargv[i])); + std::wstring_convert> converter; + + for (int i = 0; i < argc; ++i) + { + args.push_back(converter.to_bytes(wargv[i])); argv[i] = args.back().c_str(); } } ~utf8argv() { delete[] argv; } - char **get() const { return const_cast(argv); } + char** get() const { return const_cast(argv); } private: utf8argv(const utf8argv&); utf8argv& operator=(const utf8argv&); - const char **argv; + const char** argv; std::vector args; }; @@ -46,65 +53,74 @@ class utf8argv characters interface which presents UTF-16 encoding. The rest of OpenMW application stack assumes UTF-8 encoding, therefore this conversion. - - For boost::filesystem::path::imbue see components/files/windowspath.cpp */ -int wmain(int argc, wchar_t *wargv[]) { +int wmain(int argc, wchar_t* wargv[]) +{ utf8argv converter(argc, wargv); - char **argv = converter.get(); - boost::filesystem::path::imbue(boost::locale::generator().generate("")); + char** argv = converter.get(); #endif try { bpo::options_description desc("Syntax: openmw-iniimporter inifile configfile\nAllowed options"); bpo::positional_options_description p_desc; - desc.add_options() - ("help,h", "produce help message") - ("verbose,v", "verbose output") - ("ini,i", bpo::value(), "morrowind.ini file") - ("cfg,c", bpo::value(), "openmw.cfg file") - ("output,o", bpo::value()->default_value(""), "openmw.cfg file") - ("game-files,g", "import esm and esp files") - ("no-archives,A", "disable bsa archives import") - ("encoding,e", bpo::value()-> default_value("win1252"), - "Character encoding used in OpenMW game messages:\n" - "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" - "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" - "\n\twin1252 - Western European (Latin) alphabet, used by default") - ; + auto addOption = desc.add_options(); + addOption("help,h", "produce help message"); + addOption("verbose,v", "verbose output"); + addOption("ini,i", bpo::value(), "morrowind.ini file"); + addOption("cfg,c", bpo::value(), "openmw.cfg file"); + addOption("output,o", bpo::value()->default_value({}), "openmw.cfg file"); + addOption("game-files,g", "import esm and esp files"); + addOption("fonts,f", "import bitmap fonts"); + addOption("no-archives,A", "disable bsa archives import"); + addOption("encoding,e", bpo::value()->default_value("win1252"), + "Character encoding used in OpenMW game messages:\n" + "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, " + "Croatian, Serbian (Latin script), Romanian and Albanian languages\n" + "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" + "\n\twin1252 - Western European (Latin) alphabet, used by default"); + ; p_desc.add("ini", 1).add("cfg", 1); bpo::variables_map vm; - bpo::parsed_options parsed = bpo::command_line_parser(argc, argv) - .options(desc) - .positional(p_desc) - .run(); - + bpo::parsed_options parsed = bpo::command_line_parser(argc, argv).options(desc).positional(p_desc).run(); bpo::store(parsed, vm); - if(vm.count("help") || !vm.count("ini") || !vm.count("cfg")) { + if (vm.count("help") || !vm.count("ini") || !vm.count("cfg")) + { std::cout << desc; return 0; } bpo::notify(vm); - boost::filesystem::path iniFile(vm["ini"].as()); - boost::filesystem::path cfgFile(vm["cfg"].as()); + std::filesystem::path iniFile( + vm["ini"].as().u8string()); // This call to u8string is redundant, but required to + // build on MSVC 14.26 due to implementation bugs. + std::filesystem::path cfgFile( + vm["cfg"].as().u8string()); // This call to u8string is redundant, but required to + // build on MSVC 14.26 due to implementation bugs. // if no output is given, write back to cfg file - std::string outputFile(vm["output"].as()); - if(vm["output"].defaulted()) { - outputFile = vm["cfg"].as(); + std::filesystem::path outputFile = vm["output"] + .as() + .u8string(); // This call to u8string is redundant, but required to build + // on MSVC 14.26 due to implementation bugs. + if (vm["output"].defaulted()) + { + outputFile = vm["cfg"] + .as() + .u8string(); // This call to u8string is redundant, but required to build on MSVC 14.26 due + // to implementation bugs. } - if(!boost::filesystem::exists(iniFile)) { + if (!std::filesystem::exists(iniFile)) + { std::cerr << "ini file does not exist" << std::endl; return -3; } - if(!boost::filesystem::exists(cfgFile)) + if (!std::filesystem::exists(cfgFile)) std::cerr << "cfg file does not exist" << std::endl; MwIniImporter importer; @@ -117,19 +133,28 @@ int wmain(int argc, wchar_t *wargv[]) { MwIniImporter::multistrmap ini = importer.loadIniFile(iniFile); MwIniImporter::multistrmap cfg = importer.loadCfgFile(cfgFile); + if (!vm.count("fonts")) + { + ini.erase("Fonts:Font 0"); + ini.erase("Fonts:Font 1"); + ini.erase("Fonts:Font 2"); + } + importer.merge(cfg, ini); importer.mergeFallback(cfg, ini); - if(vm.count("game-files")) { + if (vm.count("game-files")) + { importer.importGameFiles(cfg, ini, iniFile); } - if(!vm.count("no-archives")) { + if (!vm.count("no-archives")) + { importer.importArchives(cfg, ini); } - std::cout << "write to: " << outputFile << std::endl; - bfs::ofstream file((bfs::path(outputFile))); + std::cout << "write to: " << Files::pathToUnicodeString(outputFile) << std::endl; + std::ofstream file(outputFile); importer.writeToFile(file, cfg); } catch (std::exception& e) diff --git a/apps/navmeshtool/CMakeLists.txt b/apps/navmeshtool/CMakeLists.txt new file mode 100644 index 00000000000..090fc00f36a --- /dev/null +++ b/apps/navmeshtool/CMakeLists.txt @@ -0,0 +1,43 @@ +set(NAVMESHTOOL + worldspacedata.cpp + navmesh.cpp + main.cpp +) +source_group(apps\\navmeshtool FILES ${NAVMESHTOOL}) + +add_library(openmw-navmeshtool-lib STATIC + ${NAVMESHTOOL} +) + +if (ANDROID) + add_library(openmw-navmeshtool SHARED + main.cpp + ) +else() + openmw_add_executable(openmw-navmeshtool ${NAVMESHTOOL}) +endif() + +target_link_libraries(openmw-navmeshtool openmw-navmeshtool-lib) + +target_link_libraries(openmw-navmeshtool-lib + Boost::program_options + components +) + +if (BUILD_WITH_CODE_COVERAGE) + target_compile_options(openmw-navmeshtool PRIVATE --coverage) + target_link_libraries(openmw-navmeshtool gcov) +endif() + +if (WIN32) + install(TARGETS openmw-navmeshtool RUNTIME DESTINATION ".") +endif() + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw-navmeshtool PRIVATE + + + + + ) +endif() diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp new file mode 100644 index 00000000000..27f84104acf --- /dev/null +++ b/apps/navmeshtool/main.cpp @@ -0,0 +1,268 @@ +#include "navmesh.hpp" +#include "worldspacedata.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include +#include +#endif + +namespace NavMeshTool +{ + namespace + { + namespace bpo = boost::program_options; + + using StringsVector = std::vector; + + constexpr std::string_view applicationName = "NavMeshTool"; + + bpo::options_description makeOptionsDescription() + { + using Fallback::FallbackMap; + + bpo::options_description result; + auto addOption = result.add_options(); + addOption("help", "print help message"); + + addOption("version", "print version information and quit"); + + addOption("data", + bpo::value() + ->default_value(Files::MaybeQuotedPathContainer(), "data") + ->multitoken() + ->composing(), + "set data directories (later directories have higher priority)"); + + addOption("data-local", + bpo::value()->default_value( + Files::MaybeQuotedPathContainer::value_type(), ""), + "set local data directory (highest priority)"); + + addOption("fallback-archive", + bpo::value() + ->default_value(StringsVector(), "fallback-archive") + ->multitoken() + ->composing(), + "set fallback BSA archives (later archives have higher priority)"); + + addOption("content", + bpo::value()->default_value(StringsVector(), "")->multitoken()->composing(), + "content file(s): esm/esp, or omwgame/omwaddon/omwscripts"); + + addOption("encoding", bpo::value()->default_value("win1252"), + "Character encoding used in OpenMW game messages:\n" + "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, " + "Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" + "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" + "\n\twin1252 - Western European (Latin) alphabet, used by default"); + + addOption("fallback", + bpo::value() + ->default_value(Fallback::FallbackMap(), "") + ->multitoken() + ->composing(), + "fallback values"); + + addOption("threads", + bpo::value()->default_value( + std::max(std::thread::hardware_concurrency() - 1, 1)), + "number of threads for parallel processing"); + + addOption("process-interior-cells", bpo::value()->implicit_value(true)->default_value(false), + "build navmesh for interior cells"); + + addOption("remove-unused-tiles", bpo::value()->implicit_value(true)->default_value(false), + "remove tiles from cache that will not be used with current content profile"); + + addOption("write-binary-log", bpo::value()->implicit_value(true)->default_value(false), + "write progress in binary messages to be consumed by the launcher"); + + Files::ConfigurationManager::addCommonOptions(result); + + return result; + } + + int runNavMeshTool(int argc, char* argv[]) + { + Platform::init(); + + bpo::options_description desc = makeOptionsDescription(); + + bpo::parsed_options options = bpo::command_line_parser(argc, argv).options(desc).allow_unregistered().run(); + bpo::variables_map variables; + + bpo::store(options, variables); + bpo::notify(variables); + + if (variables.find("help") != variables.end()) + { + Debug::getRawStdout() << desc << std::endl; + return 0; + } + + Files::ConfigurationManager config; + config.readConfiguration(variables, desc); + + Debug::setupLogging(config.getLogPath(), applicationName); + + const std::string encoding(variables["encoding"].as()); + Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(encoding)); + + Files::PathContainer dataDirs(asPathContainer(variables["data"].as())); + + auto local = variables["data-local"].as(); + if (!local.empty()) + dataDirs.push_back(std::move(local)); + + config.filterOutNonExistingPaths(dataDirs); + + const auto& resDir = variables["resources"].as(); + Log(Debug::Info) << Version::getOpenmwVersionDescription(); + dataDirs.insert(dataDirs.begin(), resDir / "vfs"); + const Files::Collections fileCollections(dataDirs); + const auto& archives = variables["fallback-archive"].as(); + StringsVector contentFiles{ "builtin.omwscripts" }; + const auto& configContentFiles = variables["content"].as(); + contentFiles.insert(contentFiles.end(), configContentFiles.begin(), configContentFiles.end()); + const std::size_t threadsNumber = variables["threads"].as(); + + if (threadsNumber < 1) + { + std::cerr << "Invalid threads number: " << threadsNumber << ", expected >= 1"; + return -1; + } + + const bool processInteriorCells = variables["process-interior-cells"].as(); + const bool removeUnusedTiles = variables["remove-unused-tiles"].as(); + const bool writeBinaryLog = variables["write-binary-log"].as(); + +#ifdef WIN32 + if (writeBinaryLog) + _setmode(_fileno(stderr), _O_BINARY); +#endif + + Fallback::Map::init(variables["fallback"].as().mMap); + + VFS::Manager vfs; + + VFS::registerArchives(&vfs, fileCollections, archives, true); + + Settings::Manager::load(config); + + const DetourNavigator::AgentBounds agentBounds{ + Settings::game().mActorCollisionShapeType, + Settings::game().mDefaultActorPathfindHalfExtents, + }; + const std::uint64_t maxDbFileSize = Settings::navigator().mMaxNavmeshdbFileSize; + const auto dbPath = Files::pathToUnicodeString(config.getUserDataPath() / "navmesh.db"); + + Log(Debug::Info) << "Using navmeshdb at " << dbPath; + + DetourNavigator::NavMeshDb db(dbPath, maxDbFileSize); + + ESM::ReadersCache readers; + EsmLoader::Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + const EsmLoader::EsmData esmData + = EsmLoader::loadEsmData(query, contentFiles, fileCollections, readers, &encoder); + + constexpr double expiryDelay = 0; + + Resource::ImageManager imageManager(&vfs, expiryDelay); + Resource::NifFileManager nifFileManager(&vfs, &encoder.getStatelessEncoder()); + Resource::BgsmFileManager bgsmFileManager(&vfs, expiryDelay); + Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, &bgsmFileManager, expiryDelay); + Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay); + DetourNavigator::RecastGlobalAllocator::init(); + DetourNavigator::Settings navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager(); + navigatorSettings.mRecast.mSwimHeightScale + = EsmLoader::getGameSetting(esmData.mGameSettings, "fSwimHeightScale").getFloat(); + + WorldspaceData cellsData = gatherWorldspaceData( + navigatorSettings, readers, vfs, bulletShapeManager, esmData, processInteriorCells, writeBinaryLog); + + const Status status = generateAllNavMeshTiles(agentBounds, navigatorSettings, threadsNumber, + removeUnusedTiles, writeBinaryLog, cellsData, std::move(db)); + + switch (status) + { + case Status::Ok: + Log(Debug::Info) << "Done"; + break; + case Status::Cancelled: + Log(Debug::Warning) << "Cancelled"; + break; + case Status::NotEnoughSpace: + Log(Debug::Warning) + << "Navmesh generation is cancelled due to running out of disk space or limits " + << "for navmesh db. Check disk space at the db location \"" << dbPath + << "\". If there is enough space, adjust \"max navmeshdb file size\" setting (see " + << "https://openmw.readthedocs.io/en/latest/reference/modding/settings/" + "navigator.html?highlight=navmesh#max-navmeshdb-file-size)."; + break; + } + + return 0; + } + } +} + +#ifdef ANDROID +extern "C" int SDL_main(int argc, char* argv[]) +#else +int main(int argc, char* argv[]) +#endif +{ + return Debug::wrapApplication(NavMeshTool::runNavMeshTool, argc, argv, NavMeshTool::applicationName); +} diff --git a/apps/navmeshtool/navmesh.cpp b/apps/navmeshtool/navmesh.cpp new file mode 100644 index 00000000000..6a4a708ef97 --- /dev/null +++ b/apps/navmeshtool/navmesh.cpp @@ -0,0 +1,315 @@ +#include "navmesh.hpp" + +#include "worldspacedata.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace NavMeshTool +{ + namespace + { + using DetourNavigator::AgentBounds; + using DetourNavigator::GenerateNavMeshTile; + using DetourNavigator::MeshSource; + using DetourNavigator::NavMeshDb; + using DetourNavigator::NavMeshTileInfo; + using DetourNavigator::PreparedNavMeshData; + using DetourNavigator::RecastMeshProvider; + using DetourNavigator::Settings; + using DetourNavigator::ShapeId; + using DetourNavigator::TileId; + using DetourNavigator::TilePosition; + using DetourNavigator::TilesPositionsRange; + using DetourNavigator::TileVersion; + using Sqlite3::Transaction; + + void logGeneratedTiles(std::size_t provided, std::size_t expected) + { + Log(Debug::Info) << provided << "/" << expected << " (" + << (static_cast(provided) / static_cast(expected) * 100) + << "%) navmesh tiles are generated"; + } + + template + void serializeToStderr(const T& value) + { + const std::vector data = serialize(value); + Debug::getLockedRawStderr()->write( + reinterpret_cast(data.data()), static_cast(data.size())); + } + + void logGeneratedTilesMessage(std::size_t number) + { + serializeToStderr(GeneratedTiles{ static_cast(number) }); + } + + struct LogGeneratedTiles + { + void operator()(std::size_t provided, std::size_t expected) const { logGeneratedTiles(provided, expected); } + }; + + class NavMeshTileConsumer final : public DetourNavigator::NavMeshTileConsumer + { + public: + std::atomic_size_t mExpected{ 0 }; + + explicit NavMeshTileConsumer(NavMeshDb&& db, bool removeUnusedTiles, bool writeBinaryLog) + : mDb(std::move(db)) + , mRemoveUnusedTiles(removeUnusedTiles) + , mWriteBinaryLog(writeBinaryLog) + , mTransaction(mDb.startTransaction(Sqlite3::TransactionMode::Immediate)) + , mNextTileId(mDb.getMaxTileId() + 1) + , mNextShapeId(mDb.getMaxShapeId() + 1) + { + } + + std::size_t getProvided() const { return mProvided.load(); } + + std::size_t getInserted() const { return mInserted.load(); } + + std::size_t getUpdated() const { return mUpdated.load(); } + + std::size_t getDeleted() const + { + const std::lock_guard lock(mMutex); + return mDeleted; + } + + std::int64_t resolveMeshSource(const MeshSource& source) override + { + const std::lock_guard lock(mMutex); + return DetourNavigator::resolveMeshSource(mDb, source, mNextShapeId); + } + + std::optional find( + ESM::RefId worldspace, const TilePosition& tilePosition, const std::vector& input) override + { + std::optional result; + std::lock_guard lock(mMutex); + if (const auto tile = mDb.findTile(worldspace, tilePosition, input)) + { + NavMeshTileInfo info; + info.mTileId = tile->mTileId; + info.mVersion = tile->mVersion; + result.emplace(info); + } + return result; + } + + void ignore(ESM::RefId worldspace, const TilePosition& tilePosition) override + { + if (mRemoveUnusedTiles) + { + std::lock_guard lock(mMutex); + mDeleted += static_cast(mDb.deleteTilesAt(worldspace, tilePosition)); + } + report(); + } + + void identity(ESM::RefId worldspace, const TilePosition& tilePosition, std::int64_t tileId) override + { + if (mRemoveUnusedTiles) + { + std::lock_guard lock(mMutex); + mDeleted += static_cast( + mDb.deleteTilesAtExcept(worldspace, tilePosition, TileId{ tileId })); + } + report(); + } + + void insert(ESM::RefId worldspace, const TilePosition& tilePosition, std::int64_t version, + const std::vector& input, PreparedNavMeshData& data) override + { + { + std::lock_guard lock(mMutex); + if (mRemoveUnusedTiles) + mDeleted += static_cast(mDb.deleteTilesAt(worldspace, tilePosition)); + data.mUserId = static_cast(mNextTileId); + mDb.insertTile( + mNextTileId, worldspace, tilePosition, TileVersion{ version }, input, serialize(data)); + ++mNextTileId; + } + ++mInserted; + report(); + } + + void update(ESM::RefId worldspace, const TilePosition& tilePosition, std::int64_t tileId, + std::int64_t version, PreparedNavMeshData& data) override + { + data.mUserId = static_cast(tileId); + { + std::lock_guard lock(mMutex); + if (mRemoveUnusedTiles) + mDeleted += static_cast( + mDb.deleteTilesAtExcept(worldspace, tilePosition, TileId{ tileId })); + mDb.updateTile(TileId{ tileId }, TileVersion{ version }, serialize(data)); + } + ++mUpdated; + report(); + } + + void cancel(std::string_view reason) override + { + std::unique_lock lock(mMutex); + if (reason.find("database or disk is full") != std::string_view::npos) + mStatus = Status::NotEnoughSpace; + else + mStatus = Status::Cancelled; + mHasTile.notify_one(); + } + + Status wait() + { + constexpr std::chrono::seconds transactionInterval(1); + std::unique_lock lock(mMutex); + auto start = std::chrono::steady_clock::now(); + while (mProvided < mExpected && mStatus == Status::Ok) + { + mHasTile.wait(lock); + const auto now = std::chrono::steady_clock::now(); + if (now - start > transactionInterval) + { + mTransaction.commit(); + mTransaction = mDb.startTransaction(Sqlite3::TransactionMode::Immediate); + start = now; + } + } + logGeneratedTiles(mProvided, mExpected); + if (mWriteBinaryLog) + logGeneratedTilesMessage(mProvided); + return mStatus; + } + + void commit() + { + const std::lock_guard lock(mMutex); + mTransaction.commit(); + } + + void vacuum() + { + const std::lock_guard lock(mMutex); + mDb.vacuum(); + } + + void removeTilesOutsideRange(ESM::RefId worldspace, const TilesPositionsRange& range) + { + const std::lock_guard lock(mMutex); + mTransaction.commit(); + Log(Debug::Info) << "Removing tiles outside processed range for worldspace \"" << worldspace << "\"..."; + mDeleted += static_cast(mDb.deleteTilesOutsideRange(worldspace, range)); + mTransaction = mDb.startTransaction(Sqlite3::TransactionMode::Immediate); + } + + private: + std::atomic_size_t mProvided{ 0 }; + std::atomic_size_t mInserted{ 0 }; + std::atomic_size_t mUpdated{ 0 }; + std::size_t mDeleted = 0; + Status mStatus = Status::Ok; + mutable std::mutex mMutex; + NavMeshDb mDb; + const bool mRemoveUnusedTiles; + const bool mWriteBinaryLog; + Transaction mTransaction; + TileId mNextTileId; + std::condition_variable mHasTile; + Misc::ProgressReporter mReporter; + ShapeId mNextShapeId; + std::mutex mReportMutex; + + void report() + { + const std::size_t provided = mProvided.fetch_add(1, std::memory_order_relaxed) + 1; + mReporter(provided, mExpected); + mHasTile.notify_one(); + if (mWriteBinaryLog) + logGeneratedTilesMessage(provided); + } + }; + } + + Status generateAllNavMeshTiles(const AgentBounds& agentBounds, const Settings& settings, std::size_t threadsNumber, + bool removeUnusedTiles, bool writeBinaryLog, WorldspaceData& data, NavMeshDb&& db) + { + Log(Debug::Info) << "Generating navmesh tiles by " << threadsNumber << " parallel workers..."; + + SceneUtil::WorkQueue workQueue(threadsNumber); + auto navMeshTileConsumer + = std::make_shared(std::move(db), removeUnusedTiles, writeBinaryLog); + std::size_t tiles = 0; + std::mt19937_64 random; + + for (const std::unique_ptr& input : data.mNavMeshInputs) + { + const auto range = DetourNavigator::makeTilesPositionsRange(Misc::Convert::toOsgXY(input->mAabb.m_min), + Misc::Convert::toOsgXY(input->mAabb.m_max), settings.mRecast); + + if (removeUnusedTiles) + navMeshTileConsumer->removeTilesOutsideRange(input->mWorldspace, range); + + std::vector worldspaceTiles; + + DetourNavigator::getTilesPositions( + range, [&](const TilePosition& tilePosition) { worldspaceTiles.push_back(tilePosition); }); + + tiles += worldspaceTiles.size(); + + if (writeBinaryLog) + serializeToStderr(ExpectedTiles{ static_cast(tiles) }); + + navMeshTileConsumer->mExpected = tiles; + + std::shuffle(worldspaceTiles.begin(), worldspaceTiles.end(), random); + + for (const TilePosition& tilePosition : worldspaceTiles) + workQueue.addWorkItem(new GenerateNavMeshTile(input->mWorldspace, tilePosition, + RecastMeshProvider(input->mTileCachedRecastMeshManager), agentBounds, settings, + navMeshTileConsumer)); + } + + const Status status = navMeshTileConsumer->wait(); + if (status == Status::Ok) + navMeshTileConsumer->commit(); + + const auto inserted = navMeshTileConsumer->getInserted(); + const auto updated = navMeshTileConsumer->getUpdated(); + const auto deleted = navMeshTileConsumer->getDeleted(); + + Log(Debug::Info) << "Generated navmesh for " << navMeshTileConsumer->getProvided() << " tiles, " << inserted + << " are inserted, " << updated << " updated and " << deleted << " deleted"; + + if (inserted + updated + deleted > 0) + { + Log(Debug::Info) << "Vacuuming the database..."; + navMeshTileConsumer->vacuum(); + } + + return status; + } +} diff --git a/apps/navmeshtool/navmesh.hpp b/apps/navmeshtool/navmesh.hpp new file mode 100644 index 00000000000..4e607f4f2b0 --- /dev/null +++ b/apps/navmeshtool/navmesh.hpp @@ -0,0 +1,29 @@ +#ifndef OPENMW_NAVMESHTOOL_NAVMESH_H +#define OPENMW_NAVMESHTOOL_NAVMESH_H + +#include + +namespace DetourNavigator +{ + class NavMeshDb; + struct Settings; + struct AgentBounds; +} + +namespace NavMeshTool +{ + struct WorldspaceData; + + enum class Status + { + Ok, + Cancelled, + NotEnoughSpace, + }; + + Status generateAllNavMeshTiles(const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Settings& settings, std::size_t threadsNumber, bool removeUnusedTiles, + bool writeBinaryLog, WorldspaceData& cellsData, DetourNavigator::NavMeshDb&& db); +} + +#endif diff --git a/apps/navmeshtool/worldspacedata.cpp b/apps/navmeshtool/worldspacedata.cpp new file mode 100644 index 00000000000..3a4ff633295 --- /dev/null +++ b/apps/navmeshtool/worldspacedata.cpp @@ -0,0 +1,370 @@ +#include "worldspacedata.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NavMeshTool +{ + namespace + { + using DetourNavigator::CollisionShape; + using DetourNavigator::HeightfieldPlane; + using DetourNavigator::HeightfieldShape; + using DetourNavigator::HeightfieldSurface; + using DetourNavigator::ObjectId; + using DetourNavigator::ObjectTransform; + + struct CellRef + { + ESM::RecNameInts mType; + ESM::RefNum mRefNum; + ESM::RefId mRefId; + float mScale; + ESM::Position mPos; + + CellRef( + ESM::RecNameInts type, ESM::RefNum refNum, ESM::RefId&& refId, float scale, const ESM::Position& pos) + : mType(type) + , mRefNum(refNum) + , mRefId(std::move(refId)) + , mScale(scale) + , mPos(pos) + { + } + }; + + ESM::RecNameInts getType(const EsmLoader::EsmData& esmData, const ESM::RefId& refId) + { + const auto it = std::lower_bound( + esmData.mRefIdTypes.begin(), esmData.mRefIdTypes.end(), refId, EsmLoader::LessById{}); + if (it == esmData.mRefIdTypes.end() || it->mId != refId) + return {}; + return it->mType; + } + + std::vector loadCellRefs( + const ESM::Cell& cell, const EsmLoader::EsmData& esmData, ESM::ReadersCache& readers) + { + std::vector> cellRefs; + + for (std::size_t i = 0; i < cell.mContextList.size(); i++) + { + ESM::ReadersCache::BusyItem reader = readers.get(static_cast(cell.mContextList[i].index)); + cell.restore(*reader, static_cast(i)); + ESM::CellRef cellRef; + bool deleted = false; + while (ESM::Cell::getNextRef(*reader, cellRef, deleted)) + { + const ESM::RecNameInts type = getType(esmData, cellRef.mRefID); + if (type == ESM::RecNameInts{}) + continue; + cellRefs.emplace_back( + deleted, type, cellRef.mRefNum, std::move(cellRef.mRefID), cellRef.mScale, cellRef.mPos); + } + } + + Log(Debug::Debug) << "Loaded " << cellRefs.size() << " cell refs"; + + const auto getKey = [](const EsmLoader::Record& v) -> ESM::RefNum { return v.mValue.mRefNum; }; + std::vector result = prepareRecords(cellRefs, getKey); + + Log(Debug::Debug) << "Prepared " << result.size() << " unique cell refs"; + + return result; + } + + template + void forEachObject(const ESM::Cell& cell, const EsmLoader::EsmData& esmData, const VFS::Manager& vfs, + Resource::BulletShapeManager& bulletShapeManager, ESM::ReadersCache& readers, F&& f) + { + std::vector cellRefs = loadCellRefs(cell, esmData, readers); + + Log(Debug::Debug) << "Prepared " << cellRefs.size() << " unique cell refs"; + + for (CellRef& cellRef : cellRefs) + { + std::string model(getModel(esmData, cellRef.mRefId, cellRef.mType)); + if (model.empty()) + continue; + + if (cellRef.mType != ESM::REC_STAT) + model = Misc::ResourceHelpers::correctActorModelPath(model, &vfs); + + osg::ref_ptr shape = [&] { + try + { + return bulletShapeManager.getShape(Misc::ResourceHelpers::correctMeshPath(model)); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to load cell ref \"" << cellRef.mRefId << "\" model \"" << model + << "\": " << e.what(); + return osg::ref_ptr(); + } + }(); + + if (shape == nullptr || shape->mCollisionShape == nullptr) + continue; + + osg::ref_ptr shapeInstance( + new Resource::BulletShapeInstance(std::move(shape))); + + switch (cellRef.mType) + { + case ESM::REC_ACTI: + case ESM::REC_CONT: + case ESM::REC_DOOR: + case ESM::REC_STAT: + f(BulletObject(std::move(shapeInstance), cellRef.mPos, cellRef.mScale)); + break; + default: + break; + } + } + } + + struct GetXY + { + osg::Vec2i operator()(const ESM::Land& value) const { return osg::Vec2i(value.mX, value.mY); } + }; + + struct LessByXY + { + bool operator()(const ESM::Land& lhs, const ESM::Land& rhs) const { return GetXY{}(lhs) < GetXY{}(rhs); } + + bool operator()(const ESM::Land& lhs, const osg::Vec2i& rhs) const { return GetXY{}(lhs) < rhs; } + + bool operator()(const osg::Vec2i& lhs, const ESM::Land& rhs) const { return lhs < GetXY{}(rhs); } + }; + + btAABB getAabb(const osg::Vec2i& cellPosition, btScalar minHeight, btScalar maxHeight) + { + btAABB aabb; + aabb.m_min = btVector3(static_cast(cellPosition.x() * ESM::Land::REAL_SIZE), + static_cast(cellPosition.y() * ESM::Land::REAL_SIZE), minHeight); + aabb.m_max = btVector3(static_cast((cellPosition.x() + 1) * ESM::Land::REAL_SIZE), + static_cast((cellPosition.y() + 1) * ESM::Land::REAL_SIZE), maxHeight); + return aabb; + } + + void mergeOrAssign(const btAABB& aabb, btAABB& target, bool& initialized) + { + if (initialized) + return target.merge(aabb); + + target.m_min = aabb.m_min; + target.m_max = aabb.m_max; + initialized = true; + } + + std::tuple makeHeightfieldShape(const std::optional& land, + const osg::Vec2i& cellPosition, std::vector>& heightfields, + std::vector>& landDatas) + { + 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 }; + + ESM::Land::LandData& landData = *landDatas.emplace_back(std::make_unique()); + land->loadData(ESM::Land::DATA_VHGT, landData); + heightfields.push_back(std::vector(std::begin(landData.mHeights), std::end(landData.mHeights))); + HeightfieldSurface surface; + surface.mHeights = heightfields.back().data(); + surface.mMinHeight = landData.mMinHeight; + surface.mMaxHeight = landData.mMaxHeight; + surface.mSize = static_cast(ESM::Land::LAND_SIZE); + return { surface, landData.mMinHeight, landData.mMaxHeight }; + } + + template + void serializeToStderr(const T& value) + { + const std::vector data = serialize(value); + Debug::getRawStderr().write( + reinterpret_cast(data.data()), static_cast(data.size())); + } + + std::string makeAddObjectErrorMessage( + ObjectId objectId, DetourNavigator::AreaType areaType, const CollisionShape& shape) + { + std::ostringstream stream; + stream << "Failed to add object to recast mesh objectId=" << objectId.value() << " areaType=" << areaType + << " fileName=" << shape.getInstance()->mFileName + << " fileHash=" << Misc::StringUtils::toHex(shape.getInstance()->mFileHash); + return stream.str(); + } + } + + WorldspaceNavMeshInput::WorldspaceNavMeshInput( + ESM::RefId worldspace, const DetourNavigator::RecastSettings& settings) + : mWorldspace(worldspace) + , mTileCachedRecastMeshManager(settings) + { + mAabb.m_min = btVector3(0, 0, 0); + mAabb.m_max = btVector3(0, 0, 0); + } + + WorldspaceData gatherWorldspaceData(const DetourNavigator::Settings& settings, ESM::ReadersCache& readers, + const VFS::Manager& vfs, Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData, + bool processInteriorCells, bool writeBinaryLog) + { + Log(Debug::Info) << "Processing " << esmData.mCells.size() << " cells..."; + + std::unordered_map> navMeshInputs; + WorldspaceData data; + + std::size_t objectsCounter = 0; + + if (writeBinaryLog) + serializeToStderr(ExpectedCells{ static_cast(esmData.mCells.size()) }); + + for (std::size_t i = 0; i < esmData.mCells.size(); ++i) + { + const ESM::Cell& cell = esmData.mCells[i]; + const bool exterior = cell.isExterior(); + + if (!exterior && !processInteriorCells) + { + if (writeBinaryLog) + serializeToStderr(ProcessedCells{ static_cast(i + 1) }); + Log(Debug::Info) << "Skipped interior" + << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") \"" + << cell.getDescription() << "\""; + continue; + } + + Log(Debug::Debug) << "Processing " << (exterior ? "exterior" : "interior") << " cell (" << (i + 1) << "/" + << esmData.mCells.size() << ") \"" << cell.getDescription() << "\""; + + const osg::Vec2i cellPosition(cell.mData.mX, cell.mData.mY); + const std::size_t cellObjectsBegin = data.mObjects.size(); + const ESM::RefId cellWorldspace = cell.isExterior() ? ESM::Cell::sDefaultWorldspaceId : cell.mId; + WorldspaceNavMeshInput& navMeshInput = [&]() -> WorldspaceNavMeshInput& { + auto it = navMeshInputs.find(cellWorldspace); + if (it == navMeshInputs.end()) + { + it = navMeshInputs + .emplace(cellWorldspace, + std::make_unique(cellWorldspace, settings.mRecast)) + .first; + it->second->mTileCachedRecastMeshManager.setWorldspace(cellWorldspace, nullptr); + } + return *it->second; + }(); + + const auto guard = navMeshInput.mTileCachedRecastMeshManager.makeUpdateGuard(); + + if (exterior) + { + const auto it + = std::lower_bound(esmData.mLands.begin(), esmData.mLands.end(), cellPosition, LessByXY{}); + const auto [heightfieldShape, minHeight, maxHeight] + = makeHeightfieldShape(it == esmData.mLands.end() ? std::optional() : *it, cellPosition, + data.mHeightfields, data.mLandData); + + mergeOrAssign( + getAabb(cellPosition, minHeight, maxHeight), navMeshInput.mAabb, navMeshInput.mAabbInitialized); + + navMeshInput.mTileCachedRecastMeshManager.addHeightfield( + cellPosition, ESM::Land::REAL_SIZE, heightfieldShape, guard.get()); + + navMeshInput.mTileCachedRecastMeshManager.addWater(cellPosition, ESM::Land::REAL_SIZE, -1, guard.get()); + } + else + { + if ((cell.mData.mFlags & ESM::Cell::HasWater) != 0) + navMeshInput.mTileCachedRecastMeshManager.addWater( + cellPosition, std::numeric_limits::max(), cell.mWater, guard.get()); + } + + forEachObject(cell, esmData, vfs, bulletShapeManager, readers, [&](BulletObject object) { + if (object.getShapeInstance()->mVisualCollisionType != Resource::VisualCollisionType::None) + return; + + const btTransform& transform = object.getCollisionObject().getWorldTransform(); + const btAABB aabb = BulletHelpers::getAabb(*object.getCollisionObject().getCollisionShape(), transform); + mergeOrAssign(aabb, navMeshInput.mAabb, navMeshInput.mAabbInitialized); + if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get()) + navMeshInput.mAabb.merge(BulletHelpers::getAabb(*avoid, transform)); + + const ObjectId objectId(++objectsCounter); + const CollisionShape shape(object.getShapeInstance(), *object.getCollisionObject().getCollisionShape(), + object.getObjectTransform()); + + if (!navMeshInput.mTileCachedRecastMeshManager.addObject( + objectId, shape, transform, DetourNavigator::AreaType_ground, guard.get())) + throw std::logic_error( + makeAddObjectErrorMessage(objectId, DetourNavigator::AreaType_ground, shape)); + + if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get()) + { + const ObjectId avoidObjectId(++objectsCounter); + const CollisionShape avoidShape(object.getShapeInstance(), *avoid, object.getObjectTransform()); + if (!navMeshInput.mTileCachedRecastMeshManager.addObject( + avoidObjectId, avoidShape, transform, DetourNavigator::AreaType_null, guard.get())) + throw std::logic_error( + makeAddObjectErrorMessage(avoidObjectId, DetourNavigator::AreaType_null, avoidShape)); + } + + data.mObjects.emplace_back(std::move(object)); + }); + + const auto cellDescription = cell.getDescription(); + + if (writeBinaryLog) + serializeToStderr(ProcessedCells{ static_cast(i + 1) }); + + Log(Debug::Info) << "Processed " << (exterior ? "exterior" : "interior") << " cell (" << (i + 1) << "/" + << esmData.mCells.size() << ") " << cellDescription << " with " + << (data.mObjects.size() - cellObjectsBegin) << " objects"; + } + + data.mNavMeshInputs.reserve(navMeshInputs.size()); + std::transform(navMeshInputs.begin(), navMeshInputs.end(), std::back_inserter(data.mNavMeshInputs), + [](auto& v) { return std::move(v.second); }); + + Log(Debug::Info) << "Processed " << esmData.mCells.size() << " cells, added " << data.mObjects.size() + << " objects and " << data.mHeightfields.size() << " height fields"; + + return data; + } +} diff --git a/apps/navmeshtool/worldspacedata.hpp b/apps/navmeshtool/worldspacedata.hpp new file mode 100644 index 00000000000..7096cf95ed9 --- /dev/null +++ b/apps/navmeshtool/worldspacedata.hpp @@ -0,0 +1,96 @@ +#ifndef OPENMW_NAVMESHTOOL_WORLDSPACEDATA_H +#define OPENMW_NAVMESHTOOL_WORLDSPACEDATA_H + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace ESM +{ + class ESMReader; + class ReadersCache; +} + +namespace VFS +{ + class Manager; +} + +namespace Resource +{ + class BulletShapeManager; +} + +namespace EsmLoader +{ + struct EsmData; +} + +namespace DetourNavigator +{ + struct Settings; +} + +namespace NavMeshTool +{ + using DetourNavigator::ObjectTransform; + using DetourNavigator::TileCachedRecastMeshManager; + + struct WorldspaceNavMeshInput + { + ESM::RefId mWorldspace; + TileCachedRecastMeshManager mTileCachedRecastMeshManager; + btAABB mAabb; + bool mAabbInitialized = false; + + explicit WorldspaceNavMeshInput(ESM::RefId worldspace, const DetourNavigator::RecastSettings& settings); + }; + + class BulletObject + { + public: + BulletObject(osg::ref_ptr&& shapeInstance, const ESM::Position& position, + float localScaling) + : mShapeInstance(std::move(shapeInstance)) + , mObjectTransform{ position, localScaling } + , mCollisionObject(BulletHelpers::makeCollisionObject(mShapeInstance->mCollisionShape.get(), + Misc::Convert::toBullet(position.asVec3()), + Misc::Convert::toBullet(Misc::Convert::makeOsgQuat(position)))) + { + mShapeInstance->setLocalScaling(btVector3(localScaling, localScaling, localScaling)); + } + + const osg::ref_ptr& getShapeInstance() const noexcept { return mShapeInstance; } + const DetourNavigator::ObjectTransform& getObjectTransform() const noexcept { return mObjectTransform; } + btCollisionObject& getCollisionObject() const noexcept { return *mCollisionObject; } + + private: + osg::ref_ptr mShapeInstance; + DetourNavigator::ObjectTransform mObjectTransform; + std::unique_ptr mCollisionObject; + }; + + struct WorldspaceData + { + std::vector> mNavMeshInputs; + std::vector mObjects; + std::vector> mLandData; + std::vector> mHeightfields; + }; + + WorldspaceData gatherWorldspaceData(const DetourNavigator::Settings& settings, ESM::ReadersCache& readers, + const VFS::Manager& vfs, Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData, + bool processInteriorCells, bool writeBinaryLog); +} + +#endif diff --git a/apps/niftest/CMakeLists.txt b/apps/niftest/CMakeLists.txt index 3cbee2b7e80..cf37162f6e7 100644 --- a/apps/niftest/CMakeLists.txt +++ b/apps/niftest/CMakeLists.txt @@ -9,11 +9,14 @@ openmw_add_executable(niftest ) target_link_libraries(niftest - ${Boost_FILESYSTEM_LIBRARY} - components + components ) if (BUILD_WITH_CODE_COVERAGE) - add_definitions (--coverage) - target_link_libraries(niftest gcov) + target_compile_options(niftest PRIVATE --coverage) + target_link_libraries(niftest gcov) +endif() + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(niftest PRIVATE ) endif() diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index e403562d327..012245937fd 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -1,123 +1,261 @@ -///Program to test .nif files both on the FileSystem and in BSA archives. +/// Program to test .nif files both on the FileSystem and in BSA archives. +#include +#include #include -#include -#include +#include +#include +#include +#include -#include +#include +#include #include -#include +#include +#include +#include +#include #include #include +#include +#include #include -#include // Create local aliases for brevity namespace bpo = boost::program_options; -namespace bfs = boost::filesystem; -///See if the file has the named extension -bool hasExtension(std::string filename, std::string extensionToFind) +enum class FileType { - std::string extension = filename.substr(filename.find_last_of('.')+1); + BSA, + BA2, + BGEM, + BGSM, + NIF, + KF, + BTO, + BTR, + RDT, + PSA, + Unknown, +}; - //Convert strings to lower case for comparison - std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); - std::transform(extensionToFind.begin(), extensionToFind.end(), extensionToFind.begin(), ::tolower); +enum class FileClass +{ + Archive, + Material, + NIF, + Unknown, +}; - if(extension == extensionToFind) - return true; - else - return false; +std::pair classifyFile(const std::filesystem::path& filename) +{ + const std::string extension = Misc::StringUtils::lowerCase(Files::pathToUnicodeString(filename.extension())); + if (extension == ".bsa") + return { FileType::BSA, FileClass::Archive }; + if (extension == ".ba2") + return { FileType::BA2, FileClass::Archive }; + if (extension == ".bgem") + return { FileType::BGEM, FileClass::Material }; + if (extension == ".bgsm") + return { FileType::BGSM, FileClass::Material }; + if (extension == ".nif") + return { FileType::NIF, FileClass::NIF }; + if (extension == ".kf") + return { FileType::KF, FileClass::NIF }; + if (extension == ".bto") + return { FileType::BTO, FileClass::NIF }; + if (extension == ".btr") + return { FileType::BTR, FileClass::NIF }; + if (extension == ".rdt") + return { FileType::RDT, FileClass::NIF }; + if (extension == ".psa") + return { FileType::PSA, FileClass::NIF }; + + return { FileType::Unknown, FileClass::Unknown }; +} + +std::string getFileTypeName(FileType fileType) +{ + switch (fileType) + { + case FileType::BSA: + return "BSA"; + case FileType::BA2: + return "BA2"; + case FileType::BGEM: + return "BGEM"; + case FileType::BGSM: + return "BGSM"; + case FileType::NIF: + return "NIF"; + case FileType::KF: + return "KF"; + case FileType::BTO: + return "BTO"; + case FileType::BTR: + return "BTR"; + case FileType::RDT: + return "RDT"; + case FileType::PSA: + return "PSA"; + case FileType::Unknown: + default: + return {}; + } +} + +bool isBSA(const std::filesystem::path& path) +{ + return classifyFile(path).second == FileClass::Archive; } -///See if the file has the "nif" extension. -bool isNIF(const std::string & filename) +std::unique_ptr makeArchive(const std::filesystem::path& path) { - return hasExtension(filename,"nif"); + if (isBSA(path)) + return VFS::makeBsaArchive(path); + if (std::filesystem::is_directory(path)) + return std::make_unique(path); + return nullptr; } -///See if the file has the "bsa" extension. -bool isBSA(const std::string & filename) + +bool readFile( + const std::filesystem::path& source, const std::filesystem::path& path, const VFS::Manager* vfs, bool quiet) { - return hasExtension(filename,"bsa"); + const auto [fileType, fileClass] = classifyFile(path); + if (fileClass != FileClass::NIF && fileClass != FileClass::Material) + return false; + + const std::string pathStr = Files::pathToUnicodeString(path); + if (!quiet) + { + std::cout << "Reading " << getFileTypeName(fileType) << " file '" << pathStr << "'"; + if (!source.empty()) + std::cout << " from '" << Files::pathToUnicodeString(isBSA(source) ? source.filename() : source) << "'"; + std::cout << std::endl; + } + const std::filesystem::path fullPath = !source.empty() ? source / path : path; + try + { + switch (fileClass) + { + case FileClass::NIF: + { + Nif::NIFFile file(Files::pathToUnicodeString(fullPath)); + Nif::Reader reader(file, nullptr); + if (vfs != nullptr) + reader.parse(vfs->get(pathStr)); + else + reader.parse(Files::openConstrainedFileStream(fullPath)); + break; + } + case FileClass::Material: + { + if (vfs != nullptr) + Bgsm::parse(vfs->get(pathStr)); + else + Bgsm::parse(Files::openConstrainedFileStream(fullPath)); + break; + } + default: + break; + } + } + catch (std::exception& e) + { + std::cerr << "Failed to read '" << pathStr << "':" << std::endl << e.what() << std::endl; + } + return true; } /// Check all the nif files in a given VFS::Archive -/// \note Takes ownership! /// \note Can not read a bsa file inside of a bsa file. -void readVFS(VFS::Archive* anArchive,std::string archivePath = "") +void readVFS(std::unique_ptr&& archive, const std::filesystem::path& archivePath, bool quiet) { - VFS::Manager myManager(true); - myManager.addArchive(anArchive); - myManager.buildIndex(); + if (archive == nullptr) + return; - std::map files=myManager.getIndex(); - for(std::map::const_iterator it=files.begin(); it!=files.end(); ++it) + if (!quiet) + std::cout << "Reading data source '" << Files::pathToUnicodeString(archivePath) << "'" << std::endl; + + VFS::Manager vfs; + vfs.addArchive(std::move(archive)); + vfs.buildIndex(); + + for (const auto& name : vfs.getRecursiveDirectoryIterator()) { - std::string name = it->first; + readFile(archivePath, name.value(), &vfs, quiet); + } - try{ - if(isNIF(name)) - { - // std::cout << "Decoding: " << name << std::endl; - Nif::NIFFile temp_nif(myManager.get(name),archivePath+name); - } - else if(isBSA(name)) + if (!archivePath.empty() && !isBSA(archivePath)) + { + const Files::Collections fileCollections({ archivePath }); + const Files::MultiDirCollection& bsaCol = fileCollections.getCollection(".bsa"); + const Files::MultiDirCollection& ba2Col = fileCollections.getCollection(".ba2"); + for (const Files::MultiDirCollection& collection : { bsaCol, ba2Col }) + { + for (auto& file : collection) { - if(!archivePath.empty() && !isBSA(archivePath)) + try + { + readVFS(VFS::makeBsaArchive(file.second), file.second, quiet); + } + catch (const std::exception& e) { -// std::cout << "Reading BSA File: " << name << std::endl; - readVFS(new VFS::BsaArchive(archivePath+name),archivePath+name+"/"); -// std::cout << "Done with BSA File: " << name << std::endl; + std::cerr << "Failed to read archive file '" << Files::pathToUnicodeString(file.second) + << "': " << e.what() << std::endl; } } } - catch (std::exception& e) - { - std::cerr << "ERROR, an exception has occurred: " << e.what() << std::endl; - } } } -bool parseOptions (int argc, char** argv, std::vector& files) +bool parseOptions(int argc, char** argv, Files::PathContainer& files, Files::PathContainer& archives, + bool& writeDebugLog, bool& quiet) { - bpo::options_description desc("Ensure that OpenMW can use the provided NIF and BSA files\n\n" - "Usages:\n" - " niftool \n" - " Scan the file or directories for nif errors.\n\n" - "Allowed options"); - desc.add_options() - ("help,h", "print help message.") - ("input-file", bpo::value< std::vector >(), "input file") - ; - - //Default option if none provided + bpo::options_description desc( + R"(Ensure that OpenMW can use the provided NIF, KF, BTO/BTR, RDT, PSA, BGEM/BGSM and BSA/BA2 files + +Usages: + niftest + Scan the file or directories for NIF errors. + +Allowed options)"); + auto addOption = desc.add_options(); + addOption("help,h", "print help message."); + addOption("write-debug-log,v", "write debug log for unsupported nif files"); + addOption("quiet,q", "do not log read archives/files"); + addOption("archives", bpo::value(), "path to archive files to provide files"); + addOption("input-file", bpo::value(), "input file"); + + // Default option if none provided bpo::positional_options_description p; p.add("input-file", -1); bpo::variables_map variables; try { - bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv). - options(desc).positional(p).run(); + bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv).options(desc).positional(p).run(); bpo::store(valid_opts, variables); bpo::notify(variables); - if (variables.count ("help")) + if (variables.count("help")) { std::cout << desc << std::endl; return false; } + writeDebugLog = variables.count("write-debug-log") > 0; + quiet = variables.count("quiet") > 0; if (variables.count("input-file")) { - files = variables["input-file"].as< std::vector >(); + files = asPathContainer(variables["input-file"].as()); + if (const auto it = variables.find("archives"); it != variables.end()) + archives = asPathContainer(it->second.as()); return true; } } - catch(std::exception &e) + catch (std::exception& e) { - std::cout << "ERROR parsing arguments: " << e.what() << "\n\n" - << desc << std::endl; + std::cout << "Error parsing arguments: " << e.what() << "\n\n" << desc << std::endl; return false; } @@ -126,44 +264,66 @@ bool parseOptions (int argc, char** argv, std::vector& files) return false; } -int main(int argc, char **argv) +int main(int argc, char** argv) { - std::vector files; - if(!parseOptions (argc, argv, files)) + Files::PathContainer files, sources; + bool writeDebugLog = false; + bool quiet = false; + if (!parseOptions(argc, argv, files, sources, writeDebugLog, quiet)) return 1; - Nif::NIFFile::setLoadUnsupportedFiles(true); -// std::cout << "Reading Files" << std::endl; - for(std::vector::const_iterator it=files.begin(); it!=files.end(); ++it) + Nif::Reader::setLoadUnsupportedFiles(true); + Nif::Reader::setWriteNifDebugLog(writeDebugLog); + + std::unique_ptr vfs; + if (!sources.empty()) { - std::string name = *it; + vfs = std::make_unique(); + for (const std::filesystem::path& path : sources) + { + const std::string pathStr = Files::pathToUnicodeString(path); + if (!quiet) + std::cout << "Adding data source '" << pathStr << "'" << std::endl; + + try + { + if (auto archive = makeArchive(path)) + vfs->addArchive(std::move(archive)); + else + std::cerr << "Error: '" << pathStr << "' is not an archive or directory" << std::endl; + } + catch (std::exception& e) + { + std::cerr << "Failed to add data source '" << pathStr << "': " << e.what() << std::endl; + } + } + + vfs->buildIndex(); + } + for (const auto& path : files) + { + const std::string pathStr = Files::pathToUnicodeString(path); try { - if(isNIF(name)) + const bool isFile = readFile({}, path, vfs.get(), quiet); + if (!isFile) { - //std::cout << "Decoding: " << name << std::endl; - Nif::NIFFile temp_nif(Files::openConstrainedFileStream(name.c_str()),name); - } - else if(isBSA(name)) - { -// std::cout << "Reading BSA File: " << name << std::endl; - readVFS(new VFS::BsaArchive(name)); - } - else if(bfs::is_directory(bfs::path(name))) - { -// std::cout << "Reading All Files in: " << name << std::endl; - readVFS(new VFS::FileSystemArchive(name),name); - } - else - { - std::cerr << "ERROR: \"" << name << "\" is not a nif file, bsa file, or directory!" << std::endl; - } + if (auto archive = makeArchive(path)) + { + readVFS(std::move(archive), path, quiet); + } + else + { + std::cerr << "Error: '" << pathStr << "' is not a NIF file, material file, archive, or directory" + << std::endl; + } + } } catch (std::exception& e) { - std::cerr << "ERROR, an exception has occurred: " << e.what() << std::endl; + std::cerr << "Failed to read '" << pathStr << "': " << e.what() << std::endl; } - } - return 0; + } + return 0; } diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 19c32df609d..87b1821cba6 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -1,5 +1,4 @@ -set (OPENCS_SRC main.cpp - ${CMAKE_SOURCE_DIR}/files/windows/opencs.rc +set (OPENCS_SRC ) opencs_units (. editor) @@ -8,30 +7,30 @@ opencs_units (model/doc document operation saving documentmanager loader runner operationholder ) -opencs_units_noqt (model/doc - stage savingstate savingstages blacklist messages +opencs_units (model/doc + savingstate savingstages blacklist messages ) -opencs_hdrs_noqt (model/doc +opencs_hdrs (model/doc state ) opencs_units (model/world idtable idtableproxymodel regionmap data commanddispatcher idtablebase resourcetable nestedtableproxymodel idtree infotableproxymodel landtexturetableproxymodel - actoradapter + actoradapter idcollection ) -opencs_units_noqt (model/world +opencs_units (model/world universalid record commands columnbase columnimp scriptcontext cell refidcollection - refidadapter refiddata refidadapterimp ref collectionbase refcollection columns infocollection tablemimedata cellcoordinates cellselection resources resourcesmanager scope - pathgrid landtexture land nestedtablewrapper nestedcollection nestedcoladapterimp nestedinfocollection + refiddata refidadapterimp ref collectionbase refcollection columns infocollection tablemimedata cellcoordinates cellselection resources resourcesmanager scope + pathgrid land nestedtablewrapper nestedcollection nestedcoladapterimp nestedinfocollection idcompletionmanager metadata defaultgmsts infoselectwrapper commandmacro ) -opencs_hdrs_noqt (model/world - columnimp idcollection collection info subcellcollection +opencs_hdrs (model/world + columnimp disabletag idcollection collection info subcellcollection ) @@ -39,14 +38,14 @@ opencs_units (model/tools tools reportmodel mergeoperation ) -opencs_units_noqt (model/tools +opencs_units (model/tools mandatoryid skillcheck classcheck factioncheck racecheck soundcheck regioncheck birthsigncheck spellcheck referencecheck referenceablecheck scriptcheck bodypartcheck startscriptcheck search searchoperation searchstage pathgridcheck soundgencheck magiceffectcheck - mergestages gmstcheck topicinfocheck journalcheck enchantmentcheck + mergestages gmstcheck topicinfocheck journalcheck enchantmentcheck effectlistcheck ) -opencs_hdrs_noqt (model/tools +opencs_hdrs (model/tools mergestate ) @@ -57,11 +56,11 @@ opencs_units (view/doc ) -opencs_units_noqt (view/doc +opencs_units (view/doc subviewfactory ) -opencs_hdrs_noqt (view/doc +opencs_hdrs (view/doc subviewfactoryimp ) @@ -71,10 +70,10 @@ opencs_units (view/world cellcreator pathgridcreator referenceablecreator startscriptcreator referencecreator scenesubview infocreator scriptedit dialoguesubview previewsubview regionmap dragrecordtable nestedtable dialoguespinbox recordbuttonbar tableeditidaction scripterrortable extendedcommandconfigurator - bodypartcreator landtexturecreator landcreator + bodypartcreator landcreator tableheadermouseeventhandler ) -opencs_units_noqt (view/world +opencs_units (view/world subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate scripthighlighter idvalidator dialoguecreator idcompletiondelegate colordelegate dragdroputils @@ -92,12 +91,12 @@ opencs_units (view/render cellwater terraintexturemode actor terrainselection terrainshapemode brushdraw commands ) -opencs_units_noqt (view/render +opencs_units (view/render lighting lightingday lightingnight lightingbright object cell terrainstorage tagbase cellarrow cellmarker cellborder pathgrid ) -opencs_hdrs_noqt (view/render +opencs_hdrs (view/render mask ) @@ -106,7 +105,7 @@ opencs_units (view/tools reportsubview reporttable searchsubview searchbox merge ) -opencs_units_noqt (view/tools +opencs_units (view/tools subviews ) @@ -116,15 +115,15 @@ opencs_units (view/prefs opencs_units (model/prefs state setting intsetting doublesetting boolsetting enumsetting coloursetting shortcut - shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting + shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting subcategory ) -opencs_units_noqt (model/prefs +opencs_units (model/prefs category ) -opencs_units_noqt (model/filter - node unarynode narynode leafnode booleannode parser andnode ornode notnode textnode valuenode +opencs_units (model/filter + unarynode narynode leafnode booleannode parser andnode ornode notnode textnode valuenode ) opencs_units (view/filter @@ -139,26 +138,32 @@ set (OPENCS_RES ${CMAKE_SOURCE_DIR}/files/opencs/resources.qrc ) set (OPENCS_UI - ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui - ${CMAKE_SOURCE_DIR}/files/ui/filedialog.ui + ${CMAKE_CURRENT_SOURCE_DIR}/ui/filedialog.ui ) -source_group (openmw-cs FILES ${OPENCS_SRC} ${OPENCS_HDR}) +source_group (openmw-cs FILES main.cpp ${OPENCS_SRC} ${OPENCS_HDR}) if(WIN32) + set(OPENCS_RES ${OPENCS_RES} ${CMAKE_SOURCE_DIR}/files/windows/QWindowsVistaDark/dark.qrc) set(QT_USE_QTMAIN TRUE) + set(OPENCS_RC_FILE ${CMAKE_SOURCE_DIR}/files/windows/opencs.rc) +else(WIN32) + set(OPENCS_RC_FILE "") endif(WIN32) -qt5_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) -qt5_wrap_cpp(OPENCS_MOC_SRC ${OPENCS_HDR_QT}) -qt5_add_resources(OPENCS_RES_SRC ${OPENCS_RES}) +if (QT_VERSION_MAJOR VERSION_EQUAL 5) + qt5_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) +else () + qt6_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) +endif() +qt_add_resources(OPENCS_RES_SRC ${OPENCS_RES}) # for compiled .ui files include_directories(${CMAKE_CURRENT_BINARY_DIR}) if(APPLE) set (OPENCS_MAC_ICON "${CMAKE_SOURCE_DIR}/files/mac/openmw-cs.icns") - set (OPENCS_CFG "${OpenMW_BINARY_DIR}/openmw-cs.cfg") + set (OPENCS_CFG "${OpenMW_BINARY_DIR}/defaults-cs.bin") set (OPENCS_DEFAULT_FILTERS_FILE "${OpenMW_BINARY_DIR}/resources/defaultfilters") set (OPENCS_OPENMW_CFG "${OpenMW_BINARY_DIR}/openmw.cfg") else() @@ -168,24 +173,37 @@ else() set (OPENCS_OPENMW_CFG "") endif(APPLE) -openmw_add_executable(openmw-cs - MACOSX_BUNDLE +add_library(openmw-cs-lib STATIC ${OPENCS_SRC} ${OPENCS_UI_HDR} ${OPENCS_MOC_SRC} ${OPENCS_RES_SRC} - ${OPENCS_MAC_ICON} - ${OPENCS_CFG} - ${OPENCS_DEFAULT_FILTERS_FILE} - ${OPENCS_OPENMW_CFG} ) -if(APPLE) +if(BUILD_OPENCS) + openmw_add_executable(openmw-cs + MACOSX_BUNDLE + ${OPENCS_MAC_ICON} + ${OPENCS_CFG} + ${OPENCS_DEFAULT_FILTERS_FILE} + ${OPENCS_OPENMW_CFG} + ${OPENCS_RC_FILE} + main.cpp + ) + + target_link_libraries(openmw-cs openmw-cs-lib) + + if (BUILD_WITH_CODE_COVERAGE) + target_compile_options(openmw-cs PRIVATE --coverage) + target_link_libraries(openmw-cs gcov) + endif() +endif() + +if(APPLE AND BUILD_OPENCS) set(OPENCS_BUNDLE_NAME "OpenMW-CS") set(OPENCS_BUNDLE_RESOURCES_DIR "${OpenMW_BINARY_DIR}/${OPENCS_BUNDLE_NAME}.app/Contents/Resources") - set(OPENMW_MYGUI_FILES_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR}) - set(OPENMW_SHADERS_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR}) + set(OPENMW_RESOURCES_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR}) add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files) @@ -212,9 +230,9 @@ if(APPLE) add_custom_command(TARGET openmw-cs POST_BUILD COMMAND cp "${OpenMW_BINARY_DIR}/resources/version" "${OPENCS_BUNDLE_RESOURCES_DIR}/resources") -endif(APPLE) +endif() -target_link_libraries(openmw-cs +target_link_libraries(openmw-cs-lib # CMake's built-in OSG finder does not use pkgconfig, so we have to # manually ensure the order is correct for inter-library dependencies. # This only makes a difference with `-DOPENMW_USE_SYSTEM_OSG=ON -DOSG_STATIC=ON`. @@ -226,41 +244,21 @@ target_link_libraries(openmw-cs ${OSGTEXT_LIBRARIES} ${OSG_LIBRARIES} ${EXTERN_OSGQT_LIBRARY} - ${Boost_SYSTEM_LIBRARY} - ${Boost_FILESYSTEM_LIBRARY} - ${Boost_PROGRAM_OPTIONS_LIBRARY} - components + Boost::program_options + components_qt ) -if(OSG_STATIC) - unset(_osg_plugins_static_files) - add_library(openmw_cs_osg_plugins INTERFACE) - foreach(_plugin ${USED_OSG_PLUGINS}) - string(TOUPPER ${_plugin} _plugin_uc) - if(OPENMW_USE_SYSTEM_OSG) - list(APPEND _osg_plugins_static_files ${${_plugin_uc}_LIBRARY}) - else() - list(APPEND _osg_plugins_static_files $) - target_link_libraries(openmw_cs_osg_plugins INTERFACE $) - add_dependencies(openmw_cs_osg_plugins ${${_plugin_uc}_LIBRARY}) - endif() - endforeach() - # We use --whole-archive because OSG plugins use registration. - get_whole_archive_options(_opts ${_osg_plugins_static_files}) - target_link_options(openmw_cs_osg_plugins INTERFACE ${_opts}) - target_link_libraries(openmw-cs openmw_cs_osg_plugins) - - if(OPENMW_USE_SYSTEM_OSG) - # OSG plugin pkgconfig files are missing these dependencies. - # https://github.com/openscenegraph/OpenSceneGraph/issues/1052 - target_link_libraries(openmw freetype jpeg png) - endif() -endif(OSG_STATIC) - -target_link_libraries(openmw-cs Qt5::Widgets Qt5::Core Qt5::Network Qt5::OpenGL) +if (QT_VERSION_MAJOR VERSION_EQUAL 6) + target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::OpenGLWidgets Qt::Svg) +else() + target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::Svg) +endif() if (WIN32) - target_link_libraries(openmw-cs ${Boost_LOCALE_LIBRARY}) + target_sources(openmw-cs PRIVATE ${CMAKE_SOURCE_DIR}/files/windows/openmw-cs.exe.manifest) +endif() + +if (WIN32 AND BUILD_OPENCS) INSTALL(TARGETS openmw-cs RUNTIME DESTINATION ".") get_generator_is_multi_config(multi_config) @@ -270,7 +268,7 @@ if (WIN32) SET(INSTALL_SOURCE "${OpenMW_BINARY_DIR}") endif () - INSTALL(FILES "${INSTALL_SOURCE}/openmw-cs.cfg" DESTINATION ".") + INSTALL(FILES "${INSTALL_SOURCE}/defaults-cs.bin" DESTINATION ".") endif() if (MSVC) @@ -280,7 +278,29 @@ if (MSVC) endif (CMAKE_CL_64) endif (MSVC) - -if(APPLE) +if(APPLE AND BUILD_OPENCS) INSTALL(TARGETS openmw-cs BUNDLE DESTINATION "." COMPONENT Bundle) endif() + +if(USE_QT) + set_property(TARGET openmw-cs-lib PROPERTY AUTOMOC ON) +endif(USE_QT) + +if (BUILD_WITH_CODE_COVERAGE) + target_compile_options(openmw-cs-lib PRIVATE --coverage) + target_link_libraries(openmw-cs-lib gcov) +endif() + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw-cs-lib PRIVATE + + + + + + + + + + ) +endif() diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 3f53a523f42..24957b0015c 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -1,30 +1,57 @@ #include "editor.hpp" #include +#include #include #include #include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +#include #include +#include +#include +#include #include +#include #include #include +#include +#include -#include "model/doc/document.hpp" -#include "model/world/data.hpp" - -#ifdef _WIN32 -#include -#endif +#include "view/doc/viewmanager.hpp" using namespace Fallback; -CS::Editor::Editor (int argc, char **argv) -: mSettingsState (mCfgMgr), mDocumentManager (mCfgMgr), - mPid(""), mLock(), mMerge (mDocumentManager), - mIpcServerName ("org.openmw.OpenCS"), mServer(nullptr), mClientSocket(nullptr) +CS::Editor::Editor(int argc, char** argv) + : mConfigVariables(readConfiguration()) + , mSettingsState(mCfgMgr) + , mDocumentManager(mCfgMgr) + , mPid(std::filesystem::temp_directory_path() / "openmw-cs.pid") + , mLockFile(QFileInfo(Files::pathToQString(mPid)).absoluteFilePath() + ".lock") + , mMerge(mDocumentManager) + , mIpcServerName("org.openmw.OpenCS") + , mServer(nullptr) + , mClientSocket(nullptr) { - std::pair > config = readConfig(); + std::pair> config = readConfig(); mViewManager = new CSVDoc::ViewManager(mDocumentManager); if (argc > 1) @@ -35,127 +62,148 @@ CS::Editor::Editor (int argc, char **argv) NifOsg::Loader::setShowMarkers(true); - mDocumentManager.setFileData(mFsStrict, config.first, config.second); - - mNewGame.setLocalData (mLocal); - mFileDialog.setLocalData (mLocal); - mMerge.setLocalData (mLocal); + mDocumentManager.setFileData(config.first, config.second); - connect (&mDocumentManager, SIGNAL (documentAdded (CSMDoc::Document *)), - this, SLOT (documentAdded (CSMDoc::Document *))); - connect (&mDocumentManager, SIGNAL (documentAboutToBeRemoved (CSMDoc::Document *)), - this, SLOT (documentAboutToBeRemoved (CSMDoc::Document *))); - connect (&mDocumentManager, SIGNAL (lastDocumentDeleted()), - this, SLOT (lastDocumentDeleted())); + mNewGame.setLocalData(mLocal); + mFileDialog.setLocalData(mLocal); + mMerge.setLocalData(mLocal); - connect (mViewManager, SIGNAL (newGameRequest ()), this, SLOT (createGame ())); - connect (mViewManager, SIGNAL (newAddonRequest ()), this, SLOT (createAddon ())); - connect (mViewManager, SIGNAL (loadDocumentRequest ()), this, SLOT (loadDocument ())); - connect (mViewManager, SIGNAL (editSettingsRequest()), this, SLOT (showSettings ())); - connect (mViewManager, SIGNAL (mergeDocument (CSMDoc::Document *)), this, SLOT (mergeDocument (CSMDoc::Document *))); + connect(&mDocumentManager, &CSMDoc::DocumentManager::documentAdded, this, &Editor::documentAdded); + connect( + &mDocumentManager, &CSMDoc::DocumentManager::documentAboutToBeRemoved, this, &Editor::documentAboutToBeRemoved); + connect(&mDocumentManager, &CSMDoc::DocumentManager::lastDocumentDeleted, this, &Editor::lastDocumentDeleted); - connect (&mStartup, SIGNAL (createGame()), this, SLOT (createGame ())); - connect (&mStartup, SIGNAL (createAddon()), this, SLOT (createAddon ())); - connect (&mStartup, SIGNAL (loadDocument()), this, SLOT (loadDocument ())); - connect (&mStartup, SIGNAL (editConfig()), this, SLOT (showSettings ())); + connect(mViewManager, &CSVDoc::ViewManager::newGameRequest, this, &Editor::createGame); + connect(mViewManager, &CSVDoc::ViewManager::newAddonRequest, this, &Editor::createAddon); + connect(mViewManager, &CSVDoc::ViewManager::loadDocumentRequest, this, &Editor::loadDocument); + connect(mViewManager, &CSVDoc::ViewManager::editSettingsRequest, this, &Editor::showSettings); + connect(mViewManager, &CSVDoc::ViewManager::mergeDocument, this, &Editor::mergeDocument); - connect (&mFileDialog, SIGNAL(signalOpenFiles (const boost::filesystem::path&)), - this, SLOT(openFiles (const boost::filesystem::path&))); + connect(&mStartup, &CSVDoc::StartupDialogue::createGame, this, &Editor::createGame); + connect(&mStartup, &CSVDoc::StartupDialogue::createAddon, this, &Editor::createAddon); + connect(&mStartup, &CSVDoc::StartupDialogue::loadDocument, this, &Editor::loadDocument); + connect(&mStartup, &CSVDoc::StartupDialogue::editConfig, this, &Editor::showSettings); - connect (&mFileDialog, SIGNAL(signalCreateNewFile (const boost::filesystem::path&)), - this, SLOT(createNewFile (const boost::filesystem::path&))); - connect (&mFileDialog, SIGNAL (rejected()), this, SLOT (cancelFileDialog ())); + connect(&mFileDialog, &CSVDoc::FileDialog::signalOpenFiles, this, + [this](const std::filesystem::path& savePath) { this->openFiles(savePath); }); + connect(&mFileDialog, &CSVDoc::FileDialog::signalCreateNewFile, this, &Editor::createNewFile); + connect(&mFileDialog, &CSVDoc::FileDialog::rejected, this, &Editor::cancelFileDialog); - connect (&mNewGame, SIGNAL (createRequest (const boost::filesystem::path&)), - this, SLOT (createNewGame (const boost::filesystem::path&))); - connect (&mNewGame, SIGNAL (cancelCreateGame()), this, SLOT (cancelCreateGame ())); + connect(&mNewGame, &CSVDoc::NewGameDialogue::createRequest, this, &Editor::createNewGame); + connect(&mNewGame, &CSVDoc::NewGameDialogue::cancelCreateGame, this, &Editor::cancelCreateGame); } -CS::Editor::~Editor () +CS::Editor::~Editor() { delete mViewManager; + mLockFile.unlock(); mPidFile.close(); - if(mServer && boost::filesystem::exists(mPid)) - static_cast ( // silence coverity warning - remove(mPid.string().c_str())); // ignore any error + if (mServer && std::filesystem::exists(mPid)) + std::filesystem::remove(mPid); } -std::pair > CS::Editor::readConfig(bool quiet) +boost::program_options::variables_map CS::Editor::readConfiguration() { boost::program_options::variables_map variables; boost::program_options::options_description desc("Syntax: openmw-cs \nAllowed options"); - desc.add_options() - ("data", boost::program_options::value()->default_value(Files::EscapePathContainer(), "data")->multitoken()->composing()) - ("data-local", boost::program_options::value()->default_value(Files::EscapePath(), "")) - ("fs-strict", boost::program_options::value()->implicit_value(true)->default_value(false)) - ("encoding", boost::program_options::value()->default_value("win1252")) - ("resources", boost::program_options::value()->default_value(Files::EscapePath(), "resources")) - ("fallback-archive", boost::program_options::value()-> - default_value(Files::EscapeStringVector(), "fallback-archive")->multitoken()) - ("fallback", boost::program_options::value()->default_value(FallbackMap(), "") - ->multitoken()->composing(), "fallback values") - ("script-blacklist", boost::program_options::value()->default_value(Files::EscapeStringVector(), "") - ->multitoken(), "exclude specified script from the verifier (if the use of the blacklist is enabled)") - ("script-blacklist-use", boost::program_options::value()->implicit_value(true) - ->default_value(true), "enable script blacklisting"); + auto addOption = desc.add_options(); + addOption("data", + boost::program_options::value() + ->default_value(Files::MaybeQuotedPathContainer(), "data") + ->multitoken() + ->composing()); + addOption("data-local", + boost::program_options::value()->default_value( + Files::MaybeQuotedPathContainer::value_type(), "")); + addOption("encoding", boost::program_options::value()->default_value("win1252")); + addOption("fallback-archive", + boost::program_options::value>() + ->default_value(std::vector(), "fallback-archive") + ->multitoken()); + addOption("fallback", + boost::program_options::value()->default_value(FallbackMap(), "")->multitoken()->composing(), + "fallback values"); + addOption("script-blacklist", + boost::program_options::value>() + ->default_value(std::vector(), "") + ->multitoken(), + "exclude specified script from the verifier (if the use of the blacklist is enabled)"); + addOption("script-blacklist-use", boost::program_options::value()->implicit_value(true)->default_value(true), + "enable script blacklisting"); + Files::ConfigurationManager::addCommonOptions(desc); boost::program_options::notify(variables); mCfgMgr.readConfiguration(variables, desc, false); + Settings::Manager::load(mCfgMgr, true); + Debug::setupLogging(mCfgMgr.getLogPath(), "OpenMW-CS"); + + return variables; +} + +std::pair> CS::Editor::readConfig(bool quiet) +{ + boost::program_options::variables_map& variables = mConfigVariables; Fallback::Map::init(variables["fallback"].as().mMap); - mEncodingName = variables["encoding"].as().toStdString(); + mEncodingName = variables["encoding"].as(); mDocumentManager.setEncoding(ToUTF8::calculateEncoding(mEncodingName)); - mFileDialog.setEncoding (QString::fromUtf8(mEncodingName.c_str())); + mFileDialog.setEncoding(QString::fromUtf8(mEncodingName.c_str())); - mDocumentManager.setResourceDir (mResources = variables["resources"].as().mPath); + mDocumentManager.setResourceDir(mResources = variables["resources"] + .as() + .u8string()); // This call to u8string is redundant, but required + // to build on MSVC 14.26 due to implementation bugs. if (variables["script-blacklist-use"].as()) - mDocumentManager.setBlacklistedScripts ( - variables["script-blacklist"].as().toStdStringVector()); - - mFsStrict = variables["fs-strict"].as(); + mDocumentManager.setBlacklistedScripts(variables["script-blacklist"].as>()); Files::PathContainer dataDirs, dataLocal; - if (!variables["data"].empty()) { - dataDirs = Files::PathContainer(Files::EscapePath::toPathContainer(variables["data"].as())); + if (!variables["data"].empty()) + { + dataDirs = asPathContainer(variables["data"].as()); } - Files::PathContainer::value_type local(variables["data-local"].as().mPath); + Files::PathContainer::value_type local(variables["data-local"] + .as() + .u8string()); // This call to u8string is redundant, but required to + // build on MSVC 14.26 due to implementation bugs. if (!local.empty()) + { + std::filesystem::create_directories(local); dataLocal.push_back(local); - - mCfgMgr.processPaths (dataDirs); - mCfgMgr.processPaths (dataLocal, true); + } + mCfgMgr.filterOutNonExistingPaths(dataDirs); + mCfgMgr.filterOutNonExistingPaths(dataLocal); if (!dataLocal.empty()) mLocal = dataLocal[0]; else { QMessageBox messageBox; - messageBox.setWindowTitle (tr ("No local data path available")); - messageBox.setIcon (QMessageBox::Critical); - messageBox.setStandardButtons (QMessageBox::Ok); - messageBox.setText(tr("
OpenCS is unable to access the local data directory. This may indicate a faulty configuration or a broken install.")); + messageBox.setWindowTitle(tr("No local data path available")); + messageBox.setIcon(QMessageBox::Critical); + messageBox.setStandardButtons(QMessageBox::Ok); + messageBox.setText( + tr("
OpenCS is unable to access the local data directory. This may indicate a faulty configuration " + "or a broken install.")); messageBox.exec(); - QApplication::exit (1); + QApplication::exit(1); } - dataDirs.insert (dataDirs.end(), dataLocal.begin(), dataLocal.end()); + dataDirs.insert(dataDirs.end(), dataLocal.begin(), dataLocal.end()); - //iterate the data directories and add them to the file dialog for loading - for (Files::PathContainer::const_reverse_iterator iter = dataDirs.rbegin(); iter != dataDirs.rend(); ++iter) - { - QString path = QString::fromUtf8 (iter->string().c_str()); - mFileDialog.addFiles(path); - } + dataDirs.insert(dataDirs.begin(), mResources / "vfs"); + + // iterate the data directories and add them to the file dialog for loading + mFileDialog.addFiles(dataDirs); - return std::make_pair (dataDirs, variables["fallback-archive"].as().toStdStringVector()); + return std::make_pair(dataDirs, variables["fallback-archive"].as>()); } void CS::Editor::createGame() @@ -188,9 +236,9 @@ void CS::Editor::createAddon() mStartup.hide(); mFileDialog.clearFiles(); - readConfig(/*quiet*/true); + readConfig(/*quiet*/ true); - mFileDialog.showDialog (CSVDoc::ContentAction_New); + mFileDialog.showDialog(CSVDoc::ContentAction_New); } void CS::Editor::cancelFileDialog() @@ -212,59 +260,63 @@ void CS::Editor::loadDocument() mStartup.hide(); mFileDialog.clearFiles(); - readConfig(/*quiet*/true); + readConfig(/*quiet*/ true); - mFileDialog.showDialog (CSVDoc::ContentAction_Edit); + mFileDialog.showDialog(CSVDoc::ContentAction_Edit); } -void CS::Editor::openFiles (const boost::filesystem::path &savePath, const std::vector &discoveredFiles) +void CS::Editor::openFiles( + const std::filesystem::path& savePath, const std::vector& discoveredFiles) { - std::vector files; + std::vector files; - if(discoveredFiles.empty()) + if (discoveredFiles.empty()) { - for (const QString &path : mFileDialog.selectedFilePaths()) - files.emplace_back(path.toUtf8().constData()); + for (const QString& path : mFileDialog.selectedFilePaths()) + { + files.emplace_back(Files::pathFromQString(path)); + } } else { files = discoveredFiles; } - mDocumentManager.addDocument (files, savePath, false); + mDocumentManager.addDocument(files, savePath, false); mFileDialog.hide(); } -void CS::Editor::createNewFile (const boost::filesystem::path &savePath) +void CS::Editor::createNewFile(const std::filesystem::path& savePath) { - std::vector files; + std::vector files; - for (const QString &path : mFileDialog.selectedFilePaths()) { - files.emplace_back(path.toUtf8().constData()); + for (const QString& path : mFileDialog.selectedFilePaths()) + { + files.emplace_back(Files::pathFromQString(path)); } - files.push_back (savePath); + files.push_back(savePath); - mDocumentManager.addDocument (files, savePath, true); + mDocumentManager.addDocument(files, savePath, true); mFileDialog.hide(); } -void CS::Editor::createNewGame (const boost::filesystem::path& file) +void CS::Editor::createNewGame(const std::filesystem::path& file) { - std::vector files; + std::vector files; - files.push_back (file); + files.push_back(file); - mDocumentManager.addDocument (files, file, true); + mDocumentManager.addDocument(files, file, true); mNewGame.hide(); } void CS::Editor::showStartup() { - if(mStartup.isHidden()) + if (mStartup.isHidden()) mStartup.show(); mStartup.raise(); mStartup.activateWindow(); @@ -275,7 +327,7 @@ void CS::Editor::showSettings() if (mSettings.isHidden()) mSettings.show(); - mSettings.move (QCursor::pos()); + mSettings.move(QCursor::pos()); mSettings.raise(); mSettings.activateWindow(); } @@ -284,14 +336,11 @@ bool CS::Editor::makeIPCServer() { try { - mPid = boost::filesystem::temp_directory_path(); - mPid /= "openmw-cs.pid"; - bool pidExists = boost::filesystem::exists(mPid); + bool pidExists = std::filesystem::exists(mPid); mPidFile.open(mPid); - mLock = boost::interprocess::file_lock(mPid.string().c_str()); - if(!mLock.try_lock()) + if (!mLockFile.tryLock()) { Log(Debug::Error) << "Error: OpenMW-CS is already running."; return false; @@ -305,34 +354,35 @@ bool CS::Editor::makeIPCServer() mServer = new QLocalServer(this); - if(pidExists) + if (pidExists) { // hack to get the temp directory path mServer->listen("dummy"); QString fullPath = mServer->fullServerName(); mServer->close(); - fullPath.remove(QRegExp("dummy$")); + fullPath.remove(QRegularExpression("dummy$")); fullPath += mIpcServerName; - if(boost::filesystem::exists(fullPath.toUtf8().constData())) + const auto path = Files::pathFromQString(fullPath); + if (exists(path)) { // TODO: compare pid of the current process with that in the file Log(Debug::Info) << "Detected unclean shutdown."; // delete the stale file - if(remove(fullPath.toUtf8().constData())) + if (remove(path)) Log(Debug::Error) << "Error: can not remove stale connection file."; } } } - catch(const std::exception& e) + catch (const std::exception& e) { Log(Debug::Error) << "Error: " << e.what(); return false; } - if(mServer->listen(mIpcServerName)) + if (mServer->listen(mIpcServerName)) { - connect(mServer, SIGNAL(newConnection()), this, SLOT(showStartup())); + connect(mServer, &QLocalServer::newConnection, this, &Editor::showStartup); return true; } @@ -364,29 +414,26 @@ int CS::Editor::run() else { ESM::ESMReader fileReader; - ToUTF8::Utf8Encoder encoder = ToUTF8::calculateEncoding(mEncodingName); + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(mEncodingName)); fileReader.setEncoder(&encoder); - fileReader.open(mFileToLoad.string()); + fileReader.open(mFileToLoad); - std::vector discoveredFiles; + std::vector discoveredFiles; - for (std::vector::const_iterator itemIter = fileReader.getGameFiles().begin(); - itemIter != fileReader.getGameFiles().end(); ++itemIter) + for (const auto& item : fileReader.getGameFiles()) { - for (Files::PathContainer::const_iterator pathIter = mDataDirs.begin(); - pathIter != mDataDirs.end(); ++pathIter) + for (const auto& path : mDataDirs) { - const boost::filesystem::path masterPath = *pathIter / itemIter->name; - if (boost::filesystem::exists(masterPath)) + if (auto masterPath = path / item.name; std::filesystem::exists(masterPath)) { - discoveredFiles.push_back(masterPath); + discoveredFiles.emplace_back(std::move(masterPath)); break; } } } discoveredFiles.push_back(mFileToLoad); - QString extension = QString::fromStdString(mFileToLoad.extension().string()).toLower(); + const auto extension = Files::pathToQString(mFileToLoad.extension()).toLower(); if (extension == ".esm") { mFileToLoad.replace_extension(".omwgame"); @@ -406,14 +453,14 @@ int CS::Editor::run() return QApplication::exec(); } -void CS::Editor::documentAdded (CSMDoc::Document *document) +void CS::Editor::documentAdded(CSMDoc::Document* document) { - mViewManager->addView (document); + mViewManager->addView(document); } -void CS::Editor::documentAboutToBeRemoved (CSMDoc::Document *document) +void CS::Editor::documentAboutToBeRemoved(CSMDoc::Document* document) { - if (mMerge.getDocument()==document) + if (mMerge.getDocument() == document) mMerge.cancel(); } @@ -422,9 +469,9 @@ void CS::Editor::lastDocumentDeleted() QApplication::quit(); } -void CS::Editor::mergeDocument (CSMDoc::Document *document) +void CS::Editor::mergeDocument(CSMDoc::Document* document) { - mMerge.configure (document); + mMerge.configure(document); mMerge.show(); mMerge.raise(); mMerge.activateWindow(); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index 1c93427613e..79c658c04d4 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -1,13 +1,17 @@ #ifndef CS_EDITOR_H #define CS_EDITOR_H -#include -#include +#include +#include #include #include -#include -#include + +#include +#include +#include +#include +#include #ifndef Q_MOC_RUN #include @@ -16,95 +20,101 @@ #include #include "model/doc/documentmanager.hpp" - #include "model/prefs/state.hpp" -#include "view/doc/viewmanager.hpp" -#include "view/doc/startup.hpp" #include "view/doc/filedialog.hpp" #include "view/doc/newgame.hpp" - +#include "view/doc/startup.hpp" #include "view/prefs/dialogue.hpp" - #include "view/tools/merge.hpp" +class QLocalServer; +class QLocalSocket; + namespace CSMDoc { class Document; } +namespace CSVDoc +{ + class ViewManager; +} + namespace CS { class Editor : public QObject { - Q_OBJECT - - Files::ConfigurationManager mCfgMgr; - CSMPrefs::State mSettingsState; - CSMDoc::DocumentManager mDocumentManager; - CSVDoc::StartupDialogue mStartup; - CSVDoc::NewGameDialogue mNewGame; - CSVPrefs::Dialogue mSettings; - CSVDoc::FileDialog mFileDialog; - boost::filesystem::path mLocal; - boost::filesystem::path mResources; - boost::filesystem::path mPid; - boost::interprocess::file_lock mLock; - boost::filesystem::ofstream mPidFile; - bool mFsStrict; - CSVTools::Merge mMerge; - CSVDoc::ViewManager* mViewManager; - boost::filesystem::path mFileToLoad; - Files::PathContainer mDataDirs; - std::string mEncodingName; - - std::pair > readConfig(bool quiet=false); - ///< \return data paths - - // not implemented - Editor (const Editor&); - Editor& operator= (const Editor&); - - public: - - Editor (int argc, char **argv); - ~Editor (); - - bool makeIPCServer(); - void connectToIPCServer(); - - int run(); - ///< \return error status - - private slots: - - void createGame(); - void createAddon(); - void cancelCreateGame(); - void cancelFileDialog(); - - void loadDocument(); - void openFiles (const boost::filesystem::path &path, const std::vector &discoveredFiles = std::vector()); - void createNewFile (const boost::filesystem::path& path); - void createNewGame (const boost::filesystem::path& file); - - void showStartup(); - - void showSettings(); - - void documentAdded (CSMDoc::Document *document); - - void documentAboutToBeRemoved (CSMDoc::Document *document); - - void lastDocumentDeleted(); - - void mergeDocument (CSMDoc::Document *document); - - private: - - QString mIpcServerName; - QLocalServer *mServer; - QLocalSocket *mClientSocket; + Q_OBJECT + + Files::ConfigurationManager mCfgMgr; + boost::program_options::variables_map mConfigVariables; + CSMPrefs::State mSettingsState; + CSMDoc::DocumentManager mDocumentManager; + CSVDoc::StartupDialogue mStartup; + CSVDoc::NewGameDialogue mNewGame; + CSVPrefs::Dialogue mSettings; + CSVDoc::FileDialog mFileDialog; + std::filesystem::path mLocal; + std::filesystem::path mResources; + std::filesystem::path mPid; + QLockFile mLockFile; + std::ofstream mPidFile; + CSVTools::Merge mMerge; + CSVDoc::ViewManager* mViewManager; + std::filesystem::path mFileToLoad; + Files::PathContainer mDataDirs; + std::string mEncodingName; + + boost::program_options::variables_map readConfiguration(); + ///< Calls mCfgMgr.readConfiguration; should be used before initialization of mSettingsState as it depends on + ///< the configuration. + std::pair> readConfig(bool quiet = false); + ///< \return data paths + + // not implemented + Editor(const Editor&); + Editor& operator=(const Editor&); + + public: + Editor(int argc, char** argv); + ~Editor(); + + bool makeIPCServer(); + void connectToIPCServer(); + + int run(); + ///< \return error status + + private slots: + + void createGame(); + void createAddon(); + void cancelCreateGame(); + void cancelFileDialog(); + + void loadDocument(); + void openFiles( + const std::filesystem::path& path, const std::vector& discoveredFiles = {}); + void createNewFile(const std::filesystem::path& path); + void createNewGame(const std::filesystem::path& file); + + void showStartup(); + + void showSettings(); + + void documentAdded(CSMDoc::Document* document); + + void documentAboutToBeRemoved(CSMDoc::Document* document); + + void lastDocumentDeleted(); + + void mergeDocument(CSMDoc::Document* document); + + private: + QString mIpcServerName; + QLocalServer* mServer; + QLocalSocket* mClientSocket; }; } diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index c7d57a256ef..f2e21568652 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -3,71 +3,73 @@ #include #include -#include #include -#include +#include + +#include #include +#include +#include #include "model/doc/messages.hpp" +#include "model/world/disabletag.hpp" #include "model/world/universalid.hpp" #ifdef Q_OS_MAC #include #endif -Q_DECLARE_METATYPE (std::string) - -class Application : public QApplication -{ - private: - - bool notify (QObject *receiver, QEvent *event) override - { - try - { - return QApplication::notify (receiver, event); - } - catch (const std::exception& exception) - { - Log(Debug::Error) << "An exception has been caught: " << exception.what(); - } +Q_DECLARE_METATYPE(std::string) - return false; - } +class QEvent; +class QObject; - public: - - Application (int& argc, char *argv[]) : QApplication (argc, argv) {} -}; +void setQSurfaceFormat() +{ + osg::DisplaySettings* ds = osg::DisplaySettings::instance().get(); + QSurfaceFormat format = QSurfaceFormat::defaultFormat(); + format.setVersion(2, 1); + format.setRenderableType(QSurfaceFormat::OpenGL); + format.setDepthBufferSize(24); + format.setSamples(ds->getMultiSamples()); + format.setStencilBufferSize(ds->getMinimumNumStencilBits()); + format.setSwapBehavior(QSurfaceFormat::DoubleBuffer); + QSurfaceFormat::setDefaultFormat(format); +} -int runApplication(int argc, char *argv[]) +int runApplication(int argc, char* argv[]) { + Platform::init(); + #ifdef Q_OS_MAC setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif - Q_INIT_RESOURCE (resources); + Q_INIT_RESOURCE(resources); - qRegisterMetaType ("std::string"); - qRegisterMetaType ("CSMWorld::UniversalId"); - qRegisterMetaType ("CSMDoc::Message"); +#ifdef WIN32 + Q_INIT_RESOURCE(dark); +#endif - Application application (argc, argv); + qRegisterMetaType("std::string"); + qRegisterMetaType("CSMWorld::UniversalId"); + qRegisterMetaType("CSMWorld::DisableTag"); + qRegisterMetaType("CSMDoc::Message"); -#ifdef Q_OS_MAC - QDir dir(QCoreApplication::applicationDirPath()); - QDir::setCurrent(dir.absolutePath()); -#endif + setQSurfaceFormat(); + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + + Platform::Application application(argc, argv); - application.setWindowIcon (QIcon (":./openmw-cs.png")); + application.setWindowIcon(QIcon(":openmw-cs")); CS::Editor editor(argc, argv); #ifdef __linux__ - setlocale(LC_NUMERIC,"C"); + setlocale(LC_NUMERIC, "C"); #endif - if(!editor.makeIPCServer()) + if (!editor.makeIPCServer()) { editor.connectToIPCServer(); return 0; @@ -76,8 +78,7 @@ int runApplication(int argc, char *argv[]) return editor.run(); } - -int main(int argc, char *argv[]) +int main(int argc, char* argv[]) { - return wrapApplication(&runApplication, argc, argv, "OpenMW-CS"); + return Debug::wrapApplication(&runApplication, argc, argv, "OpenMW-CS"); } diff --git a/apps/opencs/model/doc/blacklist.cpp b/apps/opencs/model/doc/blacklist.cpp index 690d79983ae..9b422cb7516 100644 --- a/apps/opencs/model/doc/blacklist.cpp +++ b/apps/opencs/model/doc/blacklist.cpp @@ -1,30 +1,32 @@ #include "blacklist.hpp" #include +#include +#include -#include +#include -bool CSMDoc::Blacklist::isBlacklisted (const CSMWorld::UniversalId& id) const +#include + +bool CSMDoc::Blacklist::isBlacklisted(const CSMWorld::UniversalId& id) const { - std::map >::const_iterator iter = - mIds.find (id.getType()); + std::map>::const_iterator iter = mIds.find(id.getType()); - if (iter==mIds.end()) + if (iter == mIds.end()) return false; - return std::binary_search (iter->second.begin(), iter->second.end(), - Misc::StringUtils::lowerCase (id.getId())); + return std::binary_search(iter->second.begin(), iter->second.end(), Misc::StringUtils::lowerCase(id.getId())); } -void CSMDoc::Blacklist::add (CSMWorld::UniversalId::Type type, - const std::vector& ids) +void CSMDoc::Blacklist::add(CSMWorld::UniversalId::Type type, const std::vector& ids) { std::vector& list = mIds[type]; size_t size = list.size(); - list.resize (size+ids.size()); + list.resize(size + ids.size()); - std::transform (ids.begin(), ids.end(), list.begin()+size, Misc::StringUtils::lowerCase); - std::sort (list.begin(), list.end()); + std::transform(ids.begin(), ids.end(), list.begin() + size, + [](const std::string& s) { return Misc::StringUtils::lowerCase(s); }); + std::sort(list.begin(), list.end()); } diff --git a/apps/opencs/model/doc/blacklist.hpp b/apps/opencs/model/doc/blacklist.hpp index 9bf7f1d86fc..e565aa252b5 100644 --- a/apps/opencs/model/doc/blacklist.hpp +++ b/apps/opencs/model/doc/blacklist.hpp @@ -2,8 +2,8 @@ #define CSM_DOC_BLACKLIST_H #include -#include #include +#include #include "../world/universalid.hpp" @@ -12,13 +12,12 @@ namespace CSMDoc /// \brief ID blacklist sorted by UniversalId type class Blacklist { - std::map > mIds; - - public: + std::map> mIds; - bool isBlacklisted (const CSMWorld::UniversalId& id) const; + public: + bool isBlacklisted(const CSMWorld::UniversalId& id) const; - void add (CSMWorld::UniversalId::Type type, const std::vector& ids); + void add(CSMWorld::UniversalId::Type type, const std::vector& ids); }; } diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index 3a20555d1ec..f604608c7b1 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -1,9 +1,34 @@ #include "document.hpp" -#include +#include "state.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include #include "../world/defaultgmsts.hpp" @@ -11,143 +36,146 @@ #include #endif -#include +namespace CSMWorld +{ + class IdCompletionManager; +} void CSMDoc::Document::addGmsts() { - for (size_t i=0; i < CSMWorld::DefaultGmsts::FloatCount; ++i) + for (size_t i = 0; i < CSMWorld::DefaultGmsts::FloatCount; ++i) { ESM::GameSetting gmst; - gmst.mId = CSMWorld::DefaultGmsts::Floats[i]; - gmst.mValue.setType (ESM::VT_Float); - gmst.mValue.setFloat (CSMWorld::DefaultGmsts::FloatsDefaultValues[i]); - getData().getGmsts().add (gmst); + gmst.mId = ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::Floats[i]); + gmst.mValue.setType(ESM::VT_Float); + gmst.mRecordFlags = 0; + gmst.mValue.setFloat(CSMWorld::DefaultGmsts::FloatsDefaultValues[i]); + getData().getGmsts().add(gmst); } - for (size_t i=0; i < CSMWorld::DefaultGmsts::IntCount; ++i) + for (size_t i = 0; i < CSMWorld::DefaultGmsts::IntCount; ++i) { ESM::GameSetting gmst; - gmst.mId = CSMWorld::DefaultGmsts::Ints[i]; - gmst.mValue.setType (ESM::VT_Int); - gmst.mValue.setInteger (CSMWorld::DefaultGmsts::IntsDefaultValues[i]); - getData().getGmsts().add (gmst); + gmst.mId = ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::Ints[i]); + gmst.mValue.setType(ESM::VT_Int); + gmst.mRecordFlags = 0; + gmst.mValue.setInteger(CSMWorld::DefaultGmsts::IntsDefaultValues[i]); + getData().getGmsts().add(gmst); } - for (size_t i=0; i < CSMWorld::DefaultGmsts::StringCount; ++i) + for (size_t i = 0; i < CSMWorld::DefaultGmsts::StringCount; ++i) { ESM::GameSetting gmst; - gmst.mId = CSMWorld::DefaultGmsts::Strings[i]; - gmst.mValue.setType (ESM::VT_String); - gmst.mValue.setString (""); - getData().getGmsts().add (gmst); + gmst.mId = ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::Strings[i]); + gmst.mValue.setType(ESM::VT_String); + gmst.mRecordFlags = 0; + gmst.mValue.setString(""); + getData().getGmsts().add(gmst); } } void CSMDoc::Document::addOptionalGmsts() { - for (size_t i=0; i < CSMWorld::DefaultGmsts::OptionalFloatCount; ++i) + for (size_t i = 0; i < CSMWorld::DefaultGmsts::OptionalFloatCount; ++i) { ESM::GameSetting gmst; - gmst.mId = CSMWorld::DefaultGmsts::OptionalFloats[i]; + gmst.mId = ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::OptionalFloats[i]); gmst.blank(); - gmst.mValue.setType (ESM::VT_Float); - addOptionalGmst (gmst); + gmst.mValue.setType(ESM::VT_Float); + addOptionalGmst(gmst); } - for (size_t i=0; i < CSMWorld::DefaultGmsts::OptionalIntCount; ++i) + for (size_t i = 0; i < CSMWorld::DefaultGmsts::OptionalIntCount; ++i) { ESM::GameSetting gmst; - gmst.mId = CSMWorld::DefaultGmsts::OptionalInts[i]; + gmst.mId = ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::OptionalInts[i]); gmst.blank(); - gmst.mValue.setType (ESM::VT_Int); - addOptionalGmst (gmst); + gmst.mValue.setType(ESM::VT_Int); + addOptionalGmst(gmst); } - for (size_t i=0; i < CSMWorld::DefaultGmsts::OptionalStringCount; ++i) + for (size_t i = 0; i < CSMWorld::DefaultGmsts::OptionalStringCount; ++i) { ESM::GameSetting gmst; - gmst.mId = CSMWorld::DefaultGmsts::OptionalStrings[i]; + gmst.mId = ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::OptionalStrings[i]); gmst.blank(); - gmst.mValue.setType (ESM::VT_String); - gmst.mValue.setString (""); - addOptionalGmst (gmst); + gmst.mValue.setType(ESM::VT_String); + gmst.mValue.setString(""); + addOptionalGmst(gmst); } } void CSMDoc::Document::addOptionalGlobals() { - static const char *sGlobals[] = - { + static constexpr std::string_view globals[] = { "DaysPassed", "PCWerewolf", "PCYear", - 0 }; - for (int i=0; sGlobals[i]; ++i) + for (std::size_t i = 0; i < std::size(globals); ++i) { ESM::Global global; - global.mId = sGlobals[i]; + global.mId = ESM::RefId::stringRefId(globals[i]); global.blank(); - global.mValue.setType (ESM::VT_Long); + global.mValue.setType(ESM::VT_Long); - if (i==0) - global.mValue.setInteger (1); // dayspassed starts counting at 1 + if (i == 0) + global.mValue.setInteger(1); // dayspassed starts counting at 1 - addOptionalGlobal (global); + addOptionalGlobal(global); } } void CSMDoc::Document::addOptionalMagicEffects() { - for (int i=ESM::MagicEffect::SummonFabricant; i<=ESM::MagicEffect::SummonCreature05; ++i) + for (int i = ESM::MagicEffect::SummonFabricant; i <= ESM::MagicEffect::SummonCreature05; ++i) { ESM::MagicEffect effect; effect.mIndex = i; - effect.mId = ESM::MagicEffect::indexToId (i); + effect.mId = ESM::MagicEffect::indexToRefId(i); effect.blank(); - addOptionalMagicEffect (effect); + addOptionalMagicEffect(effect); } } -void CSMDoc::Document::addOptionalGmst (const ESM::GameSetting& gmst) +void CSMDoc::Document::addOptionalGmst(const ESM::GameSetting& gmst) { - if (getData().getGmsts().searchId (gmst.mId)==-1) + if (getData().getGmsts().searchId(gmst.mId) == -1) { - CSMWorld::Record record; - record.mBase = gmst; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - getData().getGmsts().appendRecord (record); + auto record = std::make_unique>(); + record->mBase = gmst; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + getData().getGmsts().appendRecord(std::move(record)); } } -void CSMDoc::Document::addOptionalGlobal (const ESM::Global& global) +void CSMDoc::Document::addOptionalGlobal(const ESM::Global& global) { - if (getData().getGlobals().searchId (global.mId)==-1) + if (getData().getGlobals().searchId(global.mId) == -1) { - CSMWorld::Record record; - record.mBase = global; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - getData().getGlobals().appendRecord (record); + auto record = std::make_unique>(); + record->mBase = global; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + getData().getGlobals().appendRecord(std::move(record)); } } -void CSMDoc::Document::addOptionalMagicEffect (const ESM::MagicEffect& magicEffect) +void CSMDoc::Document::addOptionalMagicEffect(const ESM::MagicEffect& magicEffect) { - if (getData().getMagicEffects().searchId (magicEffect.mId)==-1) + if (getData().getMagicEffects().searchId(magicEffect.mId) == -1) { - CSMWorld::Record record; - record.mBase = magicEffect; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - getData().getMagicEffects().appendRecord (record); + auto record = std::make_unique>(); + record->mBase = magicEffect; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + getData().getMagicEffects().appendRecord(std::move(record)); } } void CSMDoc::Document::createBase() { - static const char *sGlobals[] = - { + static constexpr std::string_view globals[] = { "Day", "DaysPassed", "GameHour", @@ -156,35 +184,33 @@ void CSMDoc::Document::createBase() "PCVampire", "PCWerewolf", "PCYear", - 0 }; - for (int i=0; sGlobals[i]; ++i) + for (std::size_t i = 0; i < std::size(globals); ++i) { ESM::Global record; - record.mId = sGlobals[i]; - record.mValue.setType (i==2 ? ESM::VT_Float : ESM::VT_Long); + record.mId = ESM::RefId::stringRefId(globals[i]); + record.mRecordFlags = 0; + record.mValue.setType(i == 2 ? ESM::VT_Float : ESM::VT_Long); - if (i==0 || i==1) - record.mValue.setInteger (1); + if (i == 0 || i == 1) + record.mValue.setInteger(1); - getData().getGlobals().add (record); + getData().getGlobals().add(record); } addGmsts(); - for (int i=0; i<27; ++i) + for (int i = 0; i < ESM::Skill::Length; ++i) { ESM::Skill record; - record.mIndex = i; - record.mId = ESM::Skill::indexToId (record.mIndex); + record.mId = *ESM::Skill::indexToRefId(i).getIf(); record.blank(); - getData().getSkills().add (record); + getData().getSkills().add(record); } - static const char *sVoice[] = - { + static constexpr std::string_view voices[] = { "Intruder", "Attack", "Hello", @@ -193,21 +219,20 @@ void CSMDoc::Document::createBase() "Idle", "Flee", "Hit", - 0 }; - for (int i=0; sVoice[i]; ++i) + for (const std::string_view voice : voices) { ESM::Dialogue record; - record.mId = sVoice[i]; + record.mId = ESM::RefId::stringRefId(voice); + record.mStringId = voice; record.mType = ESM::Dialogue::Voice; record.blank(); - getData().getTopics().add (record); + getData().getTopics().add(record); } - static const char *sGreetings[] = - { + static constexpr std::string_view greetings[] = { "Greeting 0", "Greeting 1", "Greeting 2", @@ -218,21 +243,20 @@ void CSMDoc::Document::createBase() "Greeting 7", "Greeting 8", "Greeting 9", - 0 }; - for (int i=0; sGreetings[i]; ++i) + for (const std::string_view greeting : greetings) { ESM::Dialogue record; - record.mId = sGreetings[i]; + record.mId = ESM::RefId::stringRefId(greeting); + record.mStringId = greeting; record.mType = ESM::Dialogue::Greeting; record.blank(); - getData().getTopics().add (record); + getData().getTopics().add(record); } - static const char *sPersuasion[] = - { + static constexpr std::string_view persuasions[] = { "Intimidate Success", "Intimidate Fail", "Service Refusal", @@ -243,64 +267,67 @@ void CSMDoc::Document::createBase() "Admire Fail", "Taunt Fail", "Bribe Fail", - 0 }; - for (int i=0; sPersuasion[i]; ++i) + for (const std::string_view persuasion : persuasions) { ESM::Dialogue record; - record.mId = sPersuasion[i]; + record.mId = ESM::RefId::stringRefId(persuasion); + record.mStringId = persuasion; record.mType = ESM::Dialogue::Persuasion; record.blank(); - getData().getTopics().add (record); + getData().getTopics().add(record); } - for (int i=0; i& files,bool new_, - const boost::filesystem::path& savePath, const boost::filesystem::path& resDir, - ToUTF8::FromType encoding, const std::vector& blacklistedScripts, - bool fsStrict, const Files::PathContainer& dataPaths, const std::vector& archives) -: mSavePath (savePath), mContentFiles (files), mNew (new_), mData (encoding, fsStrict, dataPaths, archives, resDir), - mTools (*this, encoding), - mProjectPath ((configuration.getUserDataPath() / "projects") / - (savePath.filename().string() + ".project")), - mSavingOperation (*this, mProjectPath, encoding), - mSaving (&mSavingOperation), - mResDir(resDir), mRunner (mProjectPath), - mDirty (false), mIdCompletionManager(mData) +CSMDoc::Document::Document(const Files::ConfigurationManager& configuration, std::vector files, + bool new_, const std::filesystem::path& savePath, const std::filesystem::path& resDir, ToUTF8::FromType encoding, + const std::vector& blacklistedScripts, const Files::PathContainer& dataPaths, + const std::vector& archives) + : mSavePath(savePath) + , mContentFiles(std::move(files)) + , mNew(new_) + , mData(encoding, dataPaths, archives, resDir) + , mTools(*this, encoding) + , mProjectPath((configuration.getUserDataPath() / "projects") / (savePath.filename().u8string() + u8".project")) + , mSavingOperation(*this, mProjectPath, encoding) + , mSaving(&mSavingOperation) + , mResDir(resDir) + , mRunner(mProjectPath) + , mDirty(false) + , mIdCompletionManager(mData) { if (mContentFiles.empty()) - throw std::runtime_error ("Empty content file sequence"); + throw std::runtime_error("Empty content file sequence"); - if (mNew || !boost::filesystem::exists (mProjectPath)) + if (mNew || !std::filesystem::exists(mProjectPath)) { - boost::filesystem::path filtersPath (configuration.getUserDataPath() / "defaultfilters"); + auto filtersPath = configuration.getUserDataPath() / "defaultfilters"; - boost::filesystem::ofstream destination(mProjectPath, std::ios::out | std::ios::binary); + std::ofstream destination(mProjectPath, std::ios::out | std::ios::binary); if (!destination.is_open()) - throw std::runtime_error("Can not create project file: " + mProjectPath.string()); + throw std::runtime_error("Can not create project file: " + Files::pathToUnicodeString(mProjectPath)); destination.exceptions(std::ios::failbit | std::ios::badbit); - if (!boost::filesystem::exists (filtersPath)) + if (!std::filesystem::exists(filtersPath)) filtersPath = mResDir / "defaultfilters"; - boost::filesystem::ifstream source(filtersPath, std::ios::in | std::ios::binary); + std::ifstream source(filtersPath, std::ios::in | std::ios::binary); if (!source.is_open()) - throw std::runtime_error("Can not read filters file: " + filtersPath.string()); + throw std::runtime_error("Can not read filters file: " + Files::pathToUnicodeString(filtersPath)); source.exceptions(std::ios::failbit | std::ios::badbit); destination << source.rdbuf(); @@ -308,36 +335,29 @@ CSMDoc::Document::Document (const Files::ConfigurationManager& configuration, if (mNew) { - if (mContentFiles.size()==1) + if (mContentFiles.size() == 1) createBase(); } - mBlacklist.add (CSMWorld::UniversalId::Type_Script, blacklistedScripts); + mBlacklist.add(CSMWorld::UniversalId::Type_Script, blacklistedScripts); addOptionalGmsts(); addOptionalGlobals(); addOptionalMagicEffects(); - connect (&mUndoStack, SIGNAL (cleanChanged (bool)), this, SLOT (modificationStateChanged (bool))); + connect(&mUndoStack, &QUndoStack::cleanChanged, this, &Document::modificationStateChanged); - connect (&mTools, SIGNAL (progress (int, int, int)), this, SLOT (progress (int, int, int))); - connect (&mTools, SIGNAL (done (int, bool)), this, SIGNAL (operationDone (int, bool))); - connect (&mTools, SIGNAL (done (int, bool)), this, SLOT (operationDone2 (int, bool))); - connect (&mTools, SIGNAL (mergeDone (CSMDoc::Document*)), - this, SIGNAL (mergeDone (CSMDoc::Document*))); + connect(&mTools, &CSMTools::Tools::progress, this, qOverload(&Document::progress)); + connect(&mTools, &CSMTools::Tools::done, this, &Document::operationDone); + connect(&mTools, &CSMTools::Tools::done, this, &Document::operationDone2); + connect(&mTools, &CSMTools::Tools::mergeDone, this, &Document::mergeDone); - connect (&mSaving, SIGNAL (progress (int, int, int)), this, SLOT (progress (int, int, int))); - connect (&mSaving, SIGNAL (done (int, bool)), this, SLOT (operationDone2 (int, bool))); + connect(&mSaving, &OperationHolder::progress, this, qOverload(&Document::progress)); + connect(&mSaving, &OperationHolder::done, this, &Document::operationDone2); - connect ( - &mSaving, SIGNAL (reportMessage (const CSMDoc::Message&, int)), - this, SLOT (reportMessage (const CSMDoc::Message&, int))); - - connect (&mRunner, SIGNAL (runStateChanged()), this, SLOT (runStateChanged())); -} + connect(&mSaving, &OperationHolder::reportMessage, this, &Document::reportMessage); -CSMDoc::Document::~Document() -{ + connect(&mRunner, &Runner::runStateChanged, this, &Document::runStateChanged); } QUndoStack& CSMDoc::Document::getUndoStack() @@ -364,22 +384,22 @@ int CSMDoc::Document::getState() const return state; } -const boost::filesystem::path& CSMDoc::Document::getResourceDir() const +const std::filesystem::path& CSMDoc::Document::getResourceDir() const { return mResDir; } -const boost::filesystem::path& CSMDoc::Document::getSavePath() const +const std::filesystem::path& CSMDoc::Document::getSavePath() const { return mSavePath; } -const boost::filesystem::path& CSMDoc::Document::getProjectPath() const +const std::filesystem::path& CSMDoc::Document::getProjectPath() const { return mProjectPath; } -const std::vector& CSMDoc::Document::getContentFiles() const +const std::vector& CSMDoc::Document::getContentFiles() const { return mContentFiles; } @@ -392,64 +412,62 @@ bool CSMDoc::Document::isNew() const void CSMDoc::Document::save() { if (mSaving.isRunning()) - throw std::logic_error ( - "Failed to initiate save, because a save operation is already running."); + throw std::logic_error("Failed to initiate save, because a save operation is already running."); mSaving.start(); - emit stateChanged (getState(), this); + emit stateChanged(getState(), this); } -CSMWorld::UniversalId CSMDoc::Document::verify (const CSMWorld::UniversalId& reportId) +CSMWorld::UniversalId CSMDoc::Document::verify(const CSMWorld::UniversalId& reportId) { - CSMWorld::UniversalId id = mTools.runVerifier (reportId); - emit stateChanged (getState(), this); + CSMWorld::UniversalId id = mTools.runVerifier(reportId); + emit stateChanged(getState(), this); return id; } - CSMWorld::UniversalId CSMDoc::Document::newSearch() { return mTools.newSearch(); } -void CSMDoc::Document::runSearch (const CSMWorld::UniversalId& searchId, const CSMTools::Search& search) +void CSMDoc::Document::runSearch(const CSMWorld::UniversalId& searchId, const CSMTools::Search& search) { - mTools.runSearch (searchId, search); - emit stateChanged (getState(), this); + mTools.runSearch(searchId, search); + emit stateChanged(getState(), this); } -void CSMDoc::Document::runMerge (std::unique_ptr target) +void CSMDoc::Document::runMerge(std::unique_ptr target) { - mTools.runMerge (std::move(target)); - emit stateChanged (getState(), this); + mTools.runMerge(std::move(target)); + emit stateChanged(getState(), this); } -void CSMDoc::Document::abortOperation (int type) +void CSMDoc::Document::abortOperation(int type) { - if (type==State_Saving) + if (type == State_Saving) mSaving.abort(); else - mTools.abortOperation (type); + mTools.abortOperation(type); } -void CSMDoc::Document::modificationStateChanged (bool clean) +void CSMDoc::Document::modificationStateChanged(bool clean) { - emit stateChanged (getState(), this); + emit stateChanged(getState(), this); } -void CSMDoc::Document::reportMessage (const CSMDoc::Message& message, int type) +void CSMDoc::Document::reportMessage(const CSMDoc::Message& message, int type) { /// \todo find a better way to get these messages to the user. Log(Debug::Info) << message.mMessage; } -void CSMDoc::Document::operationDone2 (int type, bool failed) +void CSMDoc::Document::operationDone2(int type, bool failed) { - if (type==CSMDoc::State_Saving && !failed) + if (type == CSMDoc::State_Saving && !failed) mDirty = false; - emit stateChanged (getState(), this); + emit stateChanged(getState(), this); } const CSMWorld::Data& CSMDoc::Document::getData() const @@ -462,27 +480,26 @@ CSMWorld::Data& CSMDoc::Document::getData() return mData; } -CSMTools::ReportModel *CSMDoc::Document::getReport (const CSMWorld::UniversalId& id) +CSMTools::ReportModel* CSMDoc::Document::getReport(const CSMWorld::UniversalId& id) { - return mTools.getReport (id); + return mTools.getReport(id); } -bool CSMDoc::Document::isBlacklisted (const CSMWorld::UniversalId& id) - const +bool CSMDoc::Document::isBlacklisted(const CSMWorld::UniversalId& id) const { - return mBlacklist.isBlacklisted (id); + return mBlacklist.isBlacklisted(id); } -void CSMDoc::Document::startRunning (const std::string& profile, - const std::string& startupInstruction) +void CSMDoc::Document::startRunning(const std::string& profile, const std::string& startupInstruction) { - std::vector contentFiles; + std::vector contentFiles; - for (std::vector::const_iterator iter (mContentFiles.begin()); - iter!=mContentFiles.end(); ++iter) - contentFiles.push_back (iter->filename().string()); + for (const auto& mContentFile : mContentFiles) + { + contentFiles.emplace_back(mContentFile.filename()); + } - mRunner.configure (getData().getDebugProfiles().getRecord (profile).get(), contentFiles, + mRunner.configure(getData().getDebugProfiles().getRecord(ESM::RefId::stringRefId(profile)).get(), contentFiles, startupInstruction); int state = getState(); @@ -490,9 +507,9 @@ void CSMDoc::Document::startRunning (const std::string& profile, if (state & State_Modified) { // need to save first - mRunner.start (true); + mRunner.start(true); - new SaveWatcher (&mRunner, &mSaving); // no, that is not a memory leak. Qt is weird. + new SaveWatcher(&mRunner, &mSaving); // no, that is not a memory leak. Qt is weird. if (!(state & State_Saving)) save(); @@ -506,22 +523,22 @@ void CSMDoc::Document::stopRunning() mRunner.stop(); } -QTextDocument *CSMDoc::Document::getRunLog() +QTextDocument* CSMDoc::Document::getRunLog() { return mRunner.getLog(); } void CSMDoc::Document::runStateChanged() { - emit stateChanged (getState(), this); + emit stateChanged(getState(), this); } -void CSMDoc::Document::progress (int current, int max, int type) +void CSMDoc::Document::progress(int current, int max, int type) { - emit progress (current, max, type, 1, this); + emit progress(current, max, type, 1, this); } -CSMWorld::IdCompletionManager &CSMDoc::Document::getIdCompletionManager() +CSMWorld::IdCompletionManager& CSMDoc::Document::getIdCompletionManager() { return mIdCompletionManager; } diff --git a/apps/opencs/model/doc/document.hpp b/apps/opencs/model/doc/document.hpp index 0332cb43a13..4acdfafa411 100644 --- a/apps/opencs/model/doc/document.hpp +++ b/apps/opencs/model/doc/document.hpp @@ -1,13 +1,15 @@ #ifndef CSM_DOC_DOCUMENT_H #define CSM_DOC_DOCUMENT_H +#include +#include + +#include +#include #include +#include -#include - -#include -#include -#include +#include #include #include @@ -17,22 +19,17 @@ #include "../tools/tools.hpp" -#include "state.hpp" -#include "saving.hpp" #include "blacklist.hpp" -#include "runner.hpp" #include "operationholder.hpp" +#include "runner.hpp" +#include "saving.hpp" -class QAbstractItemModel; - -namespace Fallback -{ - class Map; -} +class QTextDocument; -namespace VFS +namespace CSMTools { - class Manager; + class ReportModel; + class Search; } namespace ESM @@ -47,142 +44,135 @@ namespace Files struct ConfigurationManager; } -namespace CSMWorld -{ - class ResourcesManager; -} - namespace CSMDoc { + struct Message; class Document : public QObject { - Q_OBJECT - - private: - - boost::filesystem::path mSavePath; - std::vector mContentFiles; - bool mNew; - CSMWorld::Data mData; - CSMTools::Tools mTools; - boost::filesystem::path mProjectPath; - Saving mSavingOperation; - OperationHolder mSaving; - boost::filesystem::path mResDir; - Blacklist mBlacklist; - Runner mRunner; - bool mDirty; + Q_OBJECT - CSMWorld::IdCompletionManager mIdCompletionManager; + private: + std::filesystem::path mSavePath; + std::vector mContentFiles; + bool mNew; + CSMWorld::Data mData; + CSMTools::Tools mTools; + std::filesystem::path mProjectPath; + Saving mSavingOperation; + OperationHolder mSaving; + std::filesystem::path mResDir; + Blacklist mBlacklist; + Runner mRunner; + bool mDirty; - // It is important that the undo stack is declared last, because on desctruction it fires a signal, that is connected to a slot, that is - // using other member variables. Unfortunately this connection is cut only in the QObject destructor, which is way too late. - QUndoStack mUndoStack; + CSMWorld::IdCompletionManager mIdCompletionManager; - // not implemented - Document (const Document&); - Document& operator= (const Document&); + // It is important that the undo stack is declared last, because on desctruction it fires a signal, that is + // connected to a slot, that is using other member variables. Unfortunately this connection is cut only in the + // QObject destructor, which is way too late. + QUndoStack mUndoStack; - void createBase(); + // not implemented + Document(const Document&); + Document& operator=(const Document&); - void addGmsts(); + void createBase(); - void addOptionalGmsts(); + void addGmsts(); - void addOptionalGlobals(); + void addOptionalGmsts(); - void addOptionalMagicEffects(); + void addOptionalGlobals(); - void addOptionalGmst (const ESM::GameSetting& gmst); + void addOptionalMagicEffects(); - void addOptionalGlobal (const ESM::Global& global); + void addOptionalGmst(const ESM::GameSetting& gmst); - void addOptionalMagicEffect (const ESM::MagicEffect& effect); + void addOptionalGlobal(const ESM::Global& global); - public: + void addOptionalMagicEffect(const ESM::MagicEffect& effect); - Document (const Files::ConfigurationManager& configuration, - const std::vector< boost::filesystem::path >& files, bool new_, - const boost::filesystem::path& savePath, const boost::filesystem::path& resDir, - ToUTF8::FromType encoding, const std::vector& blacklistedScripts, - bool fsStrict, const Files::PathContainer& dataPaths, const std::vector& archives); + public: + Document(const Files::ConfigurationManager& configuration, std::vector files, bool new_, + const std::filesystem::path& savePath, const std::filesystem::path& resDir, ToUTF8::FromType encoding, + const std::vector& blacklistedScripts, const Files::PathContainer& dataPaths, + const std::vector& archives); - ~Document(); + ~Document() override = default; - QUndoStack& getUndoStack(); + QUndoStack& getUndoStack(); - int getState() const; + int getState() const; - const boost::filesystem::path& getResourceDir() const; + const std::filesystem::path& getResourceDir() const; - const boost::filesystem::path& getSavePath() const; + const std::filesystem::path& getSavePath() const; - const boost::filesystem::path& getProjectPath() const; + const std::filesystem::path& getProjectPath() const; - const std::vector& getContentFiles() const; - ///< \attention The last element in this collection is the file that is being edited, - /// but with its original path instead of the save path. + const std::vector& getContentFiles() const; + ///< \attention The last element in this collection is the file that is being edited, + /// but with its original path instead of the save path. - bool isNew() const; - ///< Is this a newly created content file? + bool isNew() const; + ///< Is this a newly created content file? - void save(); + void save(); - CSMWorld::UniversalId verify (const CSMWorld::UniversalId& reportId = CSMWorld::UniversalId()); + CSMWorld::UniversalId verify(const CSMWorld::UniversalId& reportId = CSMWorld::UniversalId()); - CSMWorld::UniversalId newSearch(); + CSMWorld::UniversalId newSearch(); - void runSearch (const CSMWorld::UniversalId& searchId, const CSMTools::Search& search); + void runSearch(const CSMWorld::UniversalId& searchId, const CSMTools::Search& search); - void runMerge (std::unique_ptr target); + void runMerge(std::unique_ptr target); - void abortOperation (int type); + void abortOperation(int type); - const CSMWorld::Data& getData() const; + const CSMWorld::Data& getData() const; - CSMWorld::Data& getData(); + CSMWorld::Data& getData(); - CSMTools::ReportModel *getReport (const CSMWorld::UniversalId& id); - ///< The ownership of the returned report is not transferred. + CSMTools::ReportModel* getReport(const CSMWorld::UniversalId& id); + ///< The ownership of the returned report is not transferred. - bool isBlacklisted (const CSMWorld::UniversalId& id) const; + bool isBlacklisted(const CSMWorld::UniversalId& id) const; - void startRunning (const std::string& profile, - const std::string& startupInstruction = ""); + void startRunning(const std::string& profile, const std::string& startupInstruction = ""); - void stopRunning(); + void stopRunning(); - QTextDocument *getRunLog(); + QTextDocument* getRunLog(); - CSMWorld::IdCompletionManager &getIdCompletionManager(); + CSMWorld::IdCompletionManager& getIdCompletionManager(); - void flagAsDirty(); + void flagAsDirty(); - signals: + signals: - void stateChanged (int state, CSMDoc::Document *document); + void stateChanged(int state, CSMDoc::Document* document); - void progress (int current, int max, int type, int threads, CSMDoc::Document *document); + void progress(int current, int max, int type, int threads, CSMDoc::Document* document); - /// \attention When this signal is emitted, *this hands over the ownership of the - /// document. This signal must be handled to avoid a leak. - void mergeDone (CSMDoc::Document *document); + /// \attention When this signal is emitted, *this hands over the ownership of the + /// document. This signal must be handled to avoid a leak. + void mergeDone(CSMDoc::Document* document); - void operationDone (int type, bool failed); + void operationDone(int type, bool failed); - private slots: + private slots: - void modificationStateChanged (bool clean); + void modificationStateChanged(bool clean); - void reportMessage (const CSMDoc::Message& message, int type); + void reportMessage(const CSMDoc::Message& message, int type); - void operationDone2 (int type, bool failed); + void operationDone2(int type, bool failed); - void runStateChanged(); + void runStateChanged(); - public slots: + public slots: - void progress (int current, int max, int type); + void progress(int current, int max, int type); }; } diff --git a/apps/opencs/model/doc/documentmanager.cpp b/apps/opencs/model/doc/documentmanager.cpp index d70301ac542..4052c8a789e 100644 --- a/apps/opencs/model/doc/documentmanager.cpp +++ b/apps/opencs/model/doc/documentmanager.cpp @@ -1,6 +1,12 @@ #include "documentmanager.hpp" -#include +#include + +#include +#include +#include + +#include #ifndef Q_MOC_RUN #include @@ -8,31 +14,25 @@ #include "document.hpp" -CSMDoc::DocumentManager::DocumentManager (const Files::ConfigurationManager& configuration) -: mConfiguration (configuration), mEncoding (ToUTF8::WINDOWS_1252), mFsStrict(false) +CSMDoc::DocumentManager::DocumentManager(const Files::ConfigurationManager& configuration) + : mConfiguration(configuration) + , mEncoding(ToUTF8::WINDOWS_1252) { - boost::filesystem::path projectPath = configuration.getUserDataPath() / "projects"; + std::filesystem::path projectPath = configuration.getUserDataPath() / "projects"; - if (!boost::filesystem::is_directory (projectPath)) - boost::filesystem::create_directories (projectPath); + if (!std::filesystem::is_directory(projectPath)) + std::filesystem::create_directories(projectPath); - mLoader.moveToThread (&mLoaderThread); + mLoader.moveToThread(&mLoaderThread); mLoaderThread.start(); - connect (&mLoader, SIGNAL (documentLoaded (Document *)), - this, SLOT (documentLoaded (Document *))); - connect (&mLoader, SIGNAL (documentNotLoaded (Document *, const std::string&)), - this, SLOT (documentNotLoaded (Document *, const std::string&))); - connect (this, SIGNAL (loadRequest (CSMDoc::Document *)), - &mLoader, SLOT (loadDocument (CSMDoc::Document *))); - connect (&mLoader, SIGNAL (nextStage (CSMDoc::Document *, const std::string&, int)), - this, SIGNAL (nextStage (CSMDoc::Document *, const std::string&, int))); - connect (&mLoader, SIGNAL (nextRecord (CSMDoc::Document *, int)), - this, SIGNAL (nextRecord (CSMDoc::Document *, int))); - connect (this, SIGNAL (cancelLoading (CSMDoc::Document *)), - &mLoader, SLOT (abortLoading (CSMDoc::Document *))); - connect (&mLoader, SIGNAL (loadMessage (CSMDoc::Document *, const std::string&)), - this, SIGNAL (loadMessage (CSMDoc::Document *, const std::string&))); + connect(&mLoader, &Loader::documentLoaded, this, &DocumentManager::documentLoaded); + connect(&mLoader, &Loader::documentNotLoaded, this, &DocumentManager::documentNotLoaded); + connect(this, &DocumentManager::loadRequest, &mLoader, &Loader::loadDocument); + connect(&mLoader, &Loader::nextStage, this, &DocumentManager::nextStage); + connect(&mLoader, &Loader::nextRecord, this, &DocumentManager::nextRecord); + connect(this, &DocumentManager::cancelLoading, &mLoader, &Loader::abortLoading); + connect(&mLoader, &Loader::loadMessage, this, &DocumentManager::loadMessage); } CSMDoc::DocumentManager::~DocumentManager() @@ -42,7 +42,7 @@ CSMDoc::DocumentManager::~DocumentManager() mLoader.hasThingsToDo().wakeAll(); mLoaderThread.wait(); - for (std::vector::iterator iter (mDocuments.begin()); iter!=mDocuments.end(); ++iter) + for (std::vector::iterator iter(mDocuments.begin()); iter != mDocuments.end(); ++iter) delete *iter; } @@ -51,80 +51,79 @@ bool CSMDoc::DocumentManager::isEmpty() return mDocuments.empty(); } -void CSMDoc::DocumentManager::addDocument (const std::vector& files, const boost::filesystem::path& savePath, - bool new_) +void CSMDoc::DocumentManager::addDocument( + const std::vector& files, const std::filesystem::path& savePath, bool new_) { - Document *document = makeDocument (files, savePath, new_); - insertDocument (document); + Document* document = makeDocument(files, savePath, new_); + insertDocument(document); } -CSMDoc::Document *CSMDoc::DocumentManager::makeDocument ( - const std::vector< boost::filesystem::path >& files, - const boost::filesystem::path& savePath, bool new_) +CSMDoc::Document* CSMDoc::DocumentManager::makeDocument( + const std::vector& files, const std::filesystem::path& savePath, bool new_) { - return new Document (mConfiguration, files, new_, savePath, mResDir, mEncoding, mBlacklistedScripts, mFsStrict, mDataPaths, mArchives); + return new Document( + mConfiguration, files, new_, savePath, mResDir, mEncoding, mBlacklistedScripts, mDataPaths, mArchives); } -void CSMDoc::DocumentManager::insertDocument (CSMDoc::Document *document) +void CSMDoc::DocumentManager::insertDocument(CSMDoc::Document* document) { - mDocuments.push_back (document); + mDocuments.push_back(document); - connect (document, SIGNAL (mergeDone (CSMDoc::Document*)), - this, SLOT (insertDocument (CSMDoc::Document*))); + connect(document, SIGNAL(mergeDone(CSMDoc::Document*)), this, SLOT(insertDocument(CSMDoc::Document*))); - emit loadRequest (document); + emit loadRequest(document); mLoader.hasThingsToDo().wakeAll(); } -void CSMDoc::DocumentManager::removeDocument (CSMDoc::Document *document) +void CSMDoc::DocumentManager::removeDocument(CSMDoc::Document* document) { - std::vector::iterator iter = std::find (mDocuments.begin(), mDocuments.end(), document); + std::vector::iterator iter = std::find(mDocuments.begin(), mDocuments.end(), document); - if (iter==mDocuments.end()) - throw std::runtime_error ("removing invalid document"); + if (iter == mDocuments.end()) + throw std::runtime_error("removing invalid document"); - emit documentAboutToBeRemoved (document); + emit documentAboutToBeRemoved(document); - mDocuments.erase (iter); + mDocuments.erase(iter); document->deleteLater(); if (mDocuments.empty()) emit lastDocumentDeleted(); } -void CSMDoc::DocumentManager::setResourceDir (const boost::filesystem::path& parResDir) +void CSMDoc::DocumentManager::setResourceDir(const std::filesystem::path& parResDir) { - mResDir = boost::filesystem::system_complete(parResDir); + mResDir = std::filesystem::absolute(parResDir); } -void CSMDoc::DocumentManager::setEncoding (ToUTF8::FromType encoding) +void CSMDoc::DocumentManager::setEncoding(ToUTF8::FromType encoding) { mEncoding = encoding; } -void CSMDoc::DocumentManager::setBlacklistedScripts (const std::vector& scriptIds) +void CSMDoc::DocumentManager::setBlacklistedScripts(const std::vector& scriptIds) { mBlacklistedScripts = scriptIds; } -void CSMDoc::DocumentManager::documentLoaded (Document *document) +void CSMDoc::DocumentManager::documentLoaded(Document* document) { - emit documentAdded (document); - emit loadingStopped (document, true, ""); + emit documentAdded(document); + emit loadingStopped(document, true, ""); } -void CSMDoc::DocumentManager::documentNotLoaded (Document *document, const std::string& error) +void CSMDoc::DocumentManager::documentNotLoaded(Document* document, const std::string& error) { - emit loadingStopped (document, false, error); + emit loadingStopped(document, false, error); if (error.empty()) // do not remove the document yet, if we have an error - removeDocument (document); + removeDocument(document); } -void CSMDoc::DocumentManager::setFileData(bool strict, const Files::PathContainer& dataPaths, const std::vector& archives) +void CSMDoc::DocumentManager::setFileData( + const Files::PathContainer& dataPaths, const std::vector& archives) { - mFsStrict = strict; mDataPaths = dataPaths; mArchives = archives; } diff --git a/apps/opencs/model/doc/documentmanager.hpp b/apps/opencs/model/doc/documentmanager.hpp index ecb2a1103cc..25f7d1d4f02 100644 --- a/apps/opencs/model/doc/documentmanager.hpp +++ b/apps/opencs/model/doc/documentmanager.hpp @@ -1,25 +1,18 @@ #ifndef CSM_DOC_DOCUMENTMGR_H #define CSM_DOC_DOCUMENTMGR_H -#include -#include - -#include - #include #include -#include -#include +#include +#include +#include + #include +#include #include "loader.hpp" -namespace VFS -{ - class Manager; -} - namespace Files { struct ConfigurationManager; @@ -31,94 +24,90 @@ namespace CSMDoc class DocumentManager : public QObject { - Q_OBJECT - - std::vector mDocuments; - const Files::ConfigurationManager& mConfiguration; - QThread mLoaderThread; - Loader mLoader; - ToUTF8::FromType mEncoding; - std::vector mBlacklistedScripts; + Q_OBJECT - boost::filesystem::path mResDir; + std::vector mDocuments; + const Files::ConfigurationManager& mConfiguration; + QThread mLoaderThread; + Loader mLoader; + ToUTF8::FromType mEncoding; + std::vector mBlacklistedScripts; - bool mFsStrict; - Files::PathContainer mDataPaths; - std::vector mArchives; + std::filesystem::path mResDir; - DocumentManager (const DocumentManager&); - DocumentManager& operator= (const DocumentManager&); + Files::PathContainer mDataPaths; + std::vector mArchives; - public: + DocumentManager(const DocumentManager&); + DocumentManager& operator=(const DocumentManager&); - DocumentManager (const Files::ConfigurationManager& configuration); + public: + DocumentManager(const Files::ConfigurationManager& configuration); - ~DocumentManager(); + ~DocumentManager(); - void addDocument (const std::vector< boost::filesystem::path >& files, - const boost::filesystem::path& savePath, bool new_); - ///< \param new_ Do not load the last content file in \a files and instead create in an - /// appropriate way. + void addDocument( + const std::vector& files, const std::filesystem::path& savePath, bool new_); + ///< \param new_ Do not load the last content file in \a files and instead create in an + /// appropriate way. - /// Create a new document. The ownership of the created document is transferred to - /// the calling function. The DocumentManager does not manage it. Loading has not - /// taken place at the point when the document is returned. - /// - /// \param new_ Do not load the last content file in \a files and instead create in an - /// appropriate way. - Document *makeDocument (const std::vector< boost::filesystem::path >& files, - const boost::filesystem::path& savePath, bool new_); + /// Create a new document. The ownership of the created document is transferred to + /// the calling function. The DocumentManager does not manage it. Loading has not + /// taken place at the point when the document is returned. + /// + /// \param new_ Do not load the last content file in \a files and instead create in an + /// appropriate way. + Document* makeDocument( + const std::vector& files, const std::filesystem::path& savePath, bool new_); - void setResourceDir (const boost::filesystem::path& parResDir); + void setResourceDir(const std::filesystem::path& parResDir); - void setEncoding (ToUTF8::FromType encoding); + void setEncoding(ToUTF8::FromType encoding); - void setBlacklistedScripts (const std::vector& scriptIds); + void setBlacklistedScripts(const std::vector& scriptIds); - /// Sets the file data that gets passed to newly created documents. - void setFileData(bool strict, const Files::PathContainer& dataPaths, const std::vector& archives); + /// Sets the file data that gets passed to newly created documents. + void setFileData(const Files::PathContainer& dataPaths, const std::vector& archives); - bool isEmpty(); + bool isEmpty(); - private slots: + private slots: - void documentLoaded (Document *document); - ///< The ownership of \a document is not transferred. + void documentLoaded(Document* document); + ///< The ownership of \a document is not transferred. - void documentNotLoaded (Document *document, const std::string& error); - ///< Document load has been interrupted either because of a call to abortLoading - /// or a problem during loading). In the former case error will be an empty string. + void documentNotLoaded(Document* document, const std::string& error); + ///< Document load has been interrupted either because of a call to abortLoading + /// or a problem during loading). In the former case error will be an empty string. - public slots: + public slots: - void removeDocument (CSMDoc::Document *document); - ///< Emits the lastDocumentDeleted signal, if applicable. + void removeDocument(CSMDoc::Document* document); + ///< Emits the lastDocumentDeleted signal, if applicable. - /// Hand over document to *this. The ownership is transferred. The DocumentManager - /// will initiate the load procedure, if necessary - void insertDocument (CSMDoc::Document *document); + /// Hand over document to *this. The ownership is transferred. The DocumentManager + /// will initiate the load procedure, if necessary + void insertDocument(CSMDoc::Document* document); - signals: + signals: - void documentAdded (CSMDoc::Document *document); + void documentAdded(CSMDoc::Document* document); - void documentAboutToBeRemoved (CSMDoc::Document *document); + void documentAboutToBeRemoved(CSMDoc::Document* document); - void loadRequest (CSMDoc::Document *document); + void loadRequest(CSMDoc::Document* document); - void lastDocumentDeleted(); + void lastDocumentDeleted(); - void loadingStopped (CSMDoc::Document *document, bool completed, - const std::string& error); + void loadingStopped(CSMDoc::Document* document, bool completed, const std::string& error); - void nextStage (CSMDoc::Document *document, const std::string& name, - int totalRecords); + void nextStage(CSMDoc::Document* document, const std::string& name, int totalRecords); - void nextRecord (CSMDoc::Document *document, int records); + void nextRecord(CSMDoc::Document* document, int records); - void cancelLoading (CSMDoc::Document *document); + void cancelLoading(CSMDoc::Document* document); - void loadMessage (CSMDoc::Document *document, const std::string& message); + void loadMessage(CSMDoc::Document* document, const std::string& message); }; } diff --git a/apps/opencs/model/doc/loader.cpp b/apps/opencs/model/doc/loader.cpp index 1c5a7348c82..5be733e0d1a 100644 --- a/apps/opencs/model/doc/loader.cpp +++ b/apps/opencs/model/doc/loader.cpp @@ -1,20 +1,28 @@ #include "loader.hpp" -#include +#include +#include +#include -#include "../tools/reportmodel.hpp" +#include +#include +#include -#include "document.hpp" +#include +#include + +#include -CSMDoc::Loader::Stage::Stage() : mFile (0), mRecordsLoaded (0), mRecordsLeft (false) {} +#include "../tools/reportmodel.hpp" +#include "document.hpp" CSMDoc::Loader::Loader() : mShouldStop(false) { - mTimer = new QTimer (this); + mTimer = new QTimer(this); - connect (mTimer, SIGNAL (timeout()), this, SLOT (load())); + connect(mTimer, &QTimer::timeout, this, &Loader::load); mTimer->start(); } @@ -33,7 +41,7 @@ void CSMDoc::Loader::load() if (mDocuments.empty()) { mMutex.lock(); - mThingsToDo.wait (&mMutex); + mThingsToDo.wait(&mMutex); mMutex.unlock(); if (mShouldStop) @@ -42,12 +50,15 @@ void CSMDoc::Loader::load() return; } - std::vector >::iterator iter = mDocuments.begin(); + if (!mStart.has_value()) + mStart = std::chrono::steady_clock::now(); + + std::vector>::iterator iter = mDocuments.begin(); - Document *document = iter->first; + Document* document = iter->first; - int size = static_cast (document->getContentFiles().size()); - int editedIndex = size-1; // index of the file to be edited/created + int size = static_cast(document->getContentFiles().size()); + int editedIndex = size - 1; // index of the file to be edited/created if (document->isNew()) --size; @@ -58,10 +69,10 @@ void CSMDoc::Loader::load() { if (iter->second.mRecordsLeft) { - Messages messages (Message::Severity_Error); + Messages messages(Message::Severity_Error); const int batchingSize = 50; - for (int i=0; igetData().continueLoading (messages)) + for (int i = 0; i < batchingSize; ++i) // do not flood the system with update signals + if (document->getData().continueLoading(messages)) { iter->second.mRecordsLeft = false; break; @@ -69,42 +80,43 @@ void CSMDoc::Loader::load() else ++(iter->second.mRecordsLoaded); - CSMWorld::UniversalId log (CSMWorld::UniversalId::Type_LoadErrorLog, 0); + CSMWorld::UniversalId log(CSMWorld::UniversalId::Type_LoadErrorLog, 0); { // silence a g++ warning - for (CSMDoc::Messages::Iterator messageIter (messages.begin()); - messageIter!=messages.end(); ++messageIter) - { - document->getReport (log)->add (*messageIter); - emit loadMessage (document, messageIter->mMessage); - } + for (CSMDoc::Messages::Iterator messageIter(messages.begin()); messageIter != messages.end(); + ++messageIter) + { + document->getReport(log)->add(*messageIter); + emit loadMessage(document, messageIter->mMessage); + } } - emit nextRecord (document, iter->second.mRecordsLoaded); + emit nextRecord(document, iter->second.mRecordsLoaded); return; } - if (iter->second.mFilesecond.mFile < size) // start loading the files { - boost::filesystem::path path = document->getContentFiles()[iter->second.mFile]; + const std::filesystem::path& path = document->getContentFiles()[iter->second.mFile]; - int steps = document->getData().startLoading (path, iter->second.mFile!=editedIndex, false); + int steps = document->getData().startLoading(path, iter->second.mFile != editedIndex, /*project*/ false); iter->second.mRecordsLeft = true; iter->second.mRecordsLoaded = 0; - emit nextStage (document, path.filename().string(), steps); + emit nextStage(document, Files::pathToUnicodeString(path.filename()), steps); } - else if (iter->second.mFile==size) + else if (iter->second.mFile == size) // start loading the last (project) file { - int steps = document->getData().startLoading (document->getProjectPath(), false, true); + int steps = document->getData().startLoading(document->getProjectPath(), /*base*/ false, true); iter->second.mRecordsLeft = true; iter->second.mRecordsLoaded = 0; - emit nextStage (document, "Project File", steps); + emit nextStage(document, "Project File", steps); } else { + document->getData().finishLoading(); done = true; } @@ -112,32 +124,39 @@ void CSMDoc::Loader::load() } catch (const std::exception& e) { - mDocuments.erase (iter); - emit documentNotLoaded (document, e.what()); + mDocuments.erase(iter); + emit documentNotLoaded(document, e.what()); return; } if (done) { - mDocuments.erase (iter); - emit documentLoaded (document); + if (mStart.has_value()) + { + const auto duration = std::chrono::steady_clock::now() - *mStart; + Log(Debug::Verbose) << "Loaded content files in " + << std::chrono::duration_cast>(duration).count() << 's'; + mStart.reset(); + } + + mDocuments.erase(iter); + emit documentLoaded(document); } } -void CSMDoc::Loader::loadDocument (CSMDoc::Document *document) +void CSMDoc::Loader::loadDocument(CSMDoc::Document* document) { - mDocuments.emplace_back (document, Stage()); + mDocuments.emplace_back(document, Stage()); } -void CSMDoc::Loader::abortLoading (CSMDoc::Document *document) +void CSMDoc::Loader::abortLoading(CSMDoc::Document* document) { - for (std::vector >::iterator iter = mDocuments.begin(); - iter!=mDocuments.end(); ++iter) + for (std::vector>::iterator iter = mDocuments.begin(); iter != mDocuments.end(); ++iter) { - if (iter->first==document) + if (iter->first == document) { - mDocuments.erase (iter); - emit documentNotLoaded (document, ""); + mDocuments.erase(iter); + emit documentNotLoaded(document, ""); break; } } diff --git a/apps/opencs/model/doc/loader.hpp b/apps/opencs/model/doc/loader.hpp index ce5bc5848ab..79b4524f4fd 100644 --- a/apps/opencs/model/doc/loader.hpp +++ b/apps/opencs/model/doc/loader.hpp @@ -1,78 +1,81 @@ #ifndef CSM_DOC_LOADER_H #define CSM_DOC_LOADER_H +#include +#include +#include +#include #include -#include #include -#include +#include #include +class QTimer; + namespace CSMDoc { class Document; class Loader : public QObject { - Q_OBJECT - - struct Stage - { - int mFile; - int mRecordsLoaded; - bool mRecordsLeft; + Q_OBJECT - Stage(); - }; + struct Stage + { + int mFile = 0; + int mRecordsLoaded = 0; + bool mRecordsLeft = false; + }; - QMutex mMutex; - QWaitCondition mThingsToDo; - std::vector > mDocuments; + QMutex mMutex; + QWaitCondition mThingsToDo; + std::vector> mDocuments; - QTimer* mTimer; - bool mShouldStop; + QTimer* mTimer; + bool mShouldStop; - public: + std::optional mStart; - Loader(); + public: + Loader(); - QWaitCondition& hasThingsToDo(); + QWaitCondition& hasThingsToDo(); - void stop(); + void stop(); - private slots: + private slots: - void load(); + void load(); - public slots: + public slots: - void loadDocument (CSMDoc::Document *document); - ///< The ownership of \a document is not transferred. + void loadDocument(CSMDoc::Document* document); + ///< The ownership of \a document is not transferred. - void abortLoading (CSMDoc::Document *document); - ///< Abort loading \a docuemnt (ignored if \a document has already finished being - /// loaded). Will result in a documentNotLoaded signal, once the Loader has finished - /// cleaning up. + void abortLoading(CSMDoc::Document* document); + ///< Abort loading \a docuemnt (ignored if \a document has already finished being + /// loaded). Will result in a documentNotLoaded signal, once the Loader has finished + /// cleaning up. - signals: + signals: - void documentLoaded (Document *document); - ///< The ownership of \a document is not transferred. + void documentLoaded(Document* document); + ///< The ownership of \a document is not transferred. - void documentNotLoaded (Document *document, const std::string& error); - ///< Document load has been interrupted either because of a call to abortLoading - /// or a problem during loading). In the former case error will be an empty string. + void documentNotLoaded(Document* document, const std::string& error); + ///< Document load has been interrupted either because of a call to abortLoading + /// or a problem during loading). In the former case error will be an empty string. - void nextStage (CSMDoc::Document *document, const std::string& name, - int totalRecords); + void nextStage(CSMDoc::Document* document, const std::string& name, int totalRecords); - void nextRecord (CSMDoc::Document *document, int records); - ///< \note This signal is only given once per group of records. The group size is - /// approximately the total number of records divided by the steps value of the - /// previous nextStage signal. + void nextRecord(CSMDoc::Document* document, int records); + ///< \note This signal is only given once per group of records. The group size is + /// approximately the total number of records divided by the steps value of the + /// previous nextStage signal. - void loadMessage (CSMDoc::Document *document, const std::string& message); - ///< Non-critical load error or warning + void loadMessage(CSMDoc::Document* document, const std::string& message); + ///< Non-critical load error or warning }; } diff --git a/apps/opencs/model/doc/messages.cpp b/apps/opencs/model/doc/messages.cpp index b70d44eda36..bf28ac1c9f3 100644 --- a/apps/opencs/model/doc/messages.cpp +++ b/apps/opencs/model/doc/messages.cpp @@ -1,38 +1,52 @@ #include "messages.hpp" -CSMDoc::Message::Message() : mSeverity(Severity_Default){} +#include -CSMDoc::Message::Message (const CSMWorld::UniversalId& id, const std::string& message, - const std::string& hint, Severity severity) -: mId (id), mMessage (message), mHint (hint), mSeverity (severity) -{} +CSMDoc::Message::Message() + : mSeverity(Severity_Default) +{ +} + +CSMDoc::Message::Message( + const CSMWorld::UniversalId& id, const std::string& message, const std::string& hint, Severity severity) + : mId(id) + , mMessage(message) + , mHint(hint) + , mSeverity(severity) +{ +} -std::string CSMDoc::Message::toString (Severity severity) +std::string CSMDoc::Message::toString(Severity severity) { switch (severity) { - case CSMDoc::Message::Severity_Info: return "Information"; - case CSMDoc::Message::Severity_Warning: return "Warning"; - case CSMDoc::Message::Severity_Error: return "Error"; - case CSMDoc::Message::Severity_SeriousError: return "Serious Error"; - case CSMDoc::Message::Severity_Default: break; + case CSMDoc::Message::Severity_Info: + return "Information"; + case CSMDoc::Message::Severity_Warning: + return "Warning"; + case CSMDoc::Message::Severity_Error: + return "Error"; + case CSMDoc::Message::Severity_SeriousError: + return "Serious Error"; + case CSMDoc::Message::Severity_Default: + break; } return ""; } +CSMDoc::Messages::Messages(Message::Severity default_) + : mDefault(default_) +{ +} -CSMDoc::Messages::Messages (Message::Severity default_) -: mDefault (default_) -{} - -void CSMDoc::Messages::add (const CSMWorld::UniversalId& id, const std::string& message, - const std::string& hint, Message::Severity severity) +void CSMDoc::Messages::add( + const CSMWorld::UniversalId& id, const std::string& message, const std::string& hint, Message::Severity severity) { - if (severity==Message::Severity_Default) + if (severity == Message::Severity_Default) severity = mDefault; - mMessages.push_back (Message (id, message, hint, severity)); + mMessages.push_back(Message(id, message, hint, severity)); } CSMDoc::Messages::Iterator CSMDoc::Messages::begin() const diff --git a/apps/opencs/model/doc/messages.hpp b/apps/opencs/model/doc/messages.hpp index 671ded82a00..7715a89b333 100644 --- a/apps/opencs/model/doc/messages.hpp +++ b/apps/opencs/model/doc/messages.hpp @@ -1,11 +1,12 @@ #ifndef CSM_DOC_MESSAGES_H #define CSM_DOC_MESSAGES_H +#include + +#include #include #include -#include - #include "../world/universalid.hpp" namespace CSMDoc @@ -14,9 +15,9 @@ namespace CSMDoc { enum Severity { - Severity_Info = 0, // no problem - Severity_Warning = 1, // a potential problem, but we are probably fine - Severity_Error = 2, // an error; we are not fine + Severity_Info = 0, // no problem + Severity_Warning = 1, // a potential problem, but we are probably fine + Severity_Error = 2, // an error; we are not fine Severity_SeriousError = 3, // an error so bad we can't even be sure if we are // reporting it correctly Severity_Default = 4 @@ -29,39 +30,35 @@ namespace CSMDoc Message(); - Message (const CSMWorld::UniversalId& id, const std::string& message, - const std::string& hint, Severity severity); + Message( + const CSMWorld::UniversalId& id, const std::string& message, const std::string& hint, Severity severity); - static std::string toString (Severity severity); + static std::string toString(Severity severity); }; class Messages { - public: - - typedef std::vector Collection; - - typedef Collection::const_iterator Iterator; - - private: + public: + typedef std::vector Collection; - Collection mMessages; - Message::Severity mDefault; + typedef Collection::const_iterator Iterator; - public: + private: + Collection mMessages; + Message::Severity mDefault; - Messages (Message::Severity default_); + public: + Messages(Message::Severity default_); - void add (const CSMWorld::UniversalId& id, const std::string& message, - const std::string& hint = "", - Message::Severity severity = Message::Severity_Default); + void add(const CSMWorld::UniversalId& id, const std::string& message, const std::string& hint = "", + Message::Severity severity = Message::Severity_Default); - Iterator begin() const; + Iterator begin() const; - Iterator end() const; + Iterator end() const; }; } -Q_DECLARE_METATYPE (CSMDoc::Message) +Q_DECLARE_METATYPE(CSMDoc::Message) #endif diff --git a/apps/opencs/model/doc/operation.cpp b/apps/opencs/model/doc/operation.cpp index 369c6bb105a..4ba9f8ef845 100644 --- a/apps/opencs/model/doc/operation.cpp +++ b/apps/opencs/model/doc/operation.cpp @@ -1,14 +1,45 @@ #include "operation.hpp" -#include +#include +#include #include #include +#include + +#include + #include "../world/universalid.hpp" #include "stage.hpp" +namespace CSMDoc +{ + namespace + { + std::string_view operationToString(State value) + { + switch (value) + { + case State_Saving: + return "Saving"; + case State_Merging: + return "Merging"; + case State_Verifying: + return "Verifying"; + case State_Searching: + return "Searching"; + case State_Loading: + return "Loading"; + default: + break; + } + return "Unknown"; + } + } +} + void CSMDoc::Operation::prepareStages() { mCurrentStage = mStages.begin(); @@ -17,25 +48,33 @@ void CSMDoc::Operation::prepareStages() mTotalSteps = 0; mError = false; - for (std::vector >::iterator iter (mStages.begin()); iter!=mStages.end(); ++iter) + for (std::vector>::iterator iter(mStages.begin()); iter != mStages.end(); ++iter) { iter->second = iter->first->setup(); mTotalSteps += iter->second; } } -CSMDoc::Operation::Operation (int type, bool ordered, bool finalAlways) -: mType (type), mStages(std::vector >()), mCurrentStage(mStages.begin()), - mCurrentStep(0), mCurrentStepTotal(0), mTotalSteps(0), mOrdered (ordered), - mFinalAlways (finalAlways), mError(false), mConnected (false), mPrepared (false), - mDefaultSeverity (Message::Severity_Error) +CSMDoc::Operation::Operation(State type, bool ordered, bool finalAlways) + : mType(type) + , mStages(std::vector>()) + , mCurrentStage(mStages.begin()) + , mCurrentStep(0) + , mCurrentStepTotal(0) + , mTotalSteps(0) + , mOrdered(ordered) + , mFinalAlways(finalAlways) + , mError(false) + , mConnected(false) + , mPrepared(false) + , mDefaultSeverity(Message::Severity_Error) { - mTimer = new QTimer (this); + mTimer = new QTimer(this); } CSMDoc::Operation::~Operation() { - for (std::vector >::iterator iter (mStages.begin()); iter!=mStages.end(); ++iter) + for (std::vector>::iterator iter(mStages.begin()); iter != mStages.end(); ++iter) delete iter->first; } @@ -45,21 +84,22 @@ void CSMDoc::Operation::run() if (!mConnected) { - connect (mTimer, SIGNAL (timeout()), this, SLOT (executeStage())); + connect(mTimer, &QTimer::timeout, this, &Operation::executeStage); mConnected = true; } mPrepared = false; + mStart = std::chrono::steady_clock::now(); - mTimer->start (0); + mTimer->start(0); } -void CSMDoc::Operation::appendStage (Stage *stage) +void CSMDoc::Operation::appendStage(Stage* stage) { - mStages.emplace_back (stage, 0); + mStages.emplace_back(stage, 0); } -void CSMDoc::Operation::setDefaultSeverity (Message::Severity severity) +void CSMDoc::Operation::setDefaultSeverity(Message::Severity severity) { mDefaultSeverity = severity; } @@ -78,7 +118,7 @@ void CSMDoc::Operation::abort() if (mFinalAlways) { - if (mStages.begin()!=mStages.end() && mCurrentStage!=--mStages.end()) + if (mStages.begin() != mStages.end() && mCurrentStage != --mStages.end()) { mCurrentStep = 0; mCurrentStage = --mStages.end(); @@ -96,11 +136,11 @@ void CSMDoc::Operation::executeStage() mPrepared = true; } - Messages messages (mDefaultSeverity); + Messages messages(mDefaultSeverity); - while (mCurrentStage!=mStages.end()) + while (mCurrentStage != mStages.end()) { - if (mCurrentStep>=mCurrentStage->second) + if (mCurrentStep >= mCurrentStage->second) { mCurrentStep = 0; ++mCurrentStage; @@ -109,11 +149,12 @@ void CSMDoc::Operation::executeStage() { try { - mCurrentStage->first->perform (mCurrentStep++, messages); + mCurrentStage->first->perform(mCurrentStep++, messages); } catch (const std::exception& e) { - emit reportMessage (Message (CSMWorld::UniversalId(), e.what(), "", Message::Severity_SeriousError), mType); + emit reportMessage( + Message(CSMWorld::UniversalId(), e.what(), "", Message::Severity_SeriousError), mType); abort(); } @@ -122,17 +163,27 @@ void CSMDoc::Operation::executeStage() } } - emit progress (mCurrentStepTotal, mTotalSteps ? mTotalSteps : 1, mType); + emit progress(mCurrentStepTotal, mTotalSteps ? mTotalSteps : 1, mType); + + for (Messages::Iterator iter(messages.begin()); iter != messages.end(); ++iter) + emit reportMessage(*iter, mType); - for (Messages::Iterator iter (messages.begin()); iter!=messages.end(); ++iter) - emit reportMessage (*iter, mType); + if (mCurrentStage == mStages.end()) + { + if (mStart.has_value()) + { + const auto duration = std::chrono::steady_clock::now() - *mStart; + Log(Debug::Verbose) << operationToString(mType) << " operation is completed in " + << std::chrono::duration_cast>(duration).count() << 's'; + mStart.reset(); + } - if (mCurrentStage==mStages.end()) operationDone(); + } } void CSMDoc::Operation::operationDone() { mTimer->stop(); - emit done (mType, mError); + emit done(mType, mError); } diff --git a/apps/opencs/model/doc/operation.hpp b/apps/opencs/model/doc/operation.hpp index ff396fa3ce9..f569ffa2518 100644 --- a/apps/opencs/model/doc/operation.hpp +++ b/apps/opencs/model/doc/operation.hpp @@ -1,19 +1,17 @@ #ifndef CSM_DOC_OPERATION_H #define CSM_DOC_OPERATION_H +#include +#include +#include #include -#include #include -#include -#include #include "messages.hpp" +#include "state.hpp" -namespace CSMWorld -{ - class UniversalId; -} +class QTimer; namespace CSMDoc { @@ -21,63 +19,63 @@ namespace CSMDoc class Operation : public QObject { - Q_OBJECT - - int mType; - std::vector > mStages; // stage, number of steps - std::vector >::iterator mCurrentStage; - int mCurrentStep; - int mCurrentStepTotal; - int mTotalSteps; - int mOrdered; - bool mFinalAlways; - bool mError; - bool mConnected; - QTimer *mTimer; - bool mPrepared; - Message::Severity mDefaultSeverity; + Q_OBJECT - void prepareStages(); + State mType; + std::vector> mStages; // stage, number of steps + std::vector>::iterator mCurrentStage; + int mCurrentStep; + int mCurrentStepTotal; + int mTotalSteps; + int mOrdered; + bool mFinalAlways; + bool mError; + bool mConnected; + QTimer* mTimer; + bool mPrepared; + Message::Severity mDefaultSeverity; + std::optional mStart; - public: + void prepareStages(); - Operation (int type, bool ordered, bool finalAlways = false); - ///< \param ordered Stages must be executed in the given order. - /// \param finalAlways Execute last stage even if an error occurred during earlier stages. + public: + Operation(State type, bool ordered, bool finalAlways = false); + ///< \param ordered Stages must be executed in the given order. + /// \param finalAlways Execute last stage even if an error occurred during earlier stages. - virtual ~Operation(); + virtual ~Operation(); - void appendStage (Stage *stage); - ///< The ownership of \a stage is transferred to *this. - /// - /// \attention Do no call this function while this Operation is running. + void appendStage(Stage* stage); + ///< The ownership of \a stage is transferred to *this. + /// + /// \attention Do no call this function while this Operation is running. - /// \attention Do no call this function while this Operation is running. - void setDefaultSeverity (Message::Severity severity); + /// \attention Do no call this function while this Operation is running. + void setDefaultSeverity(Message::Severity severity); - bool hasError() const; + bool hasError() const; - signals: + signals: - void progress (int current, int max, int type); + void progress(int current, int max, int type); - void reportMessage (const CSMDoc::Message& message, int type); + void reportMessage(const CSMDoc::Message& message, int type); - void done (int type, bool failed); + void done(int type, bool failed); - public slots: + public slots: - void abort(); + void abort(); - void run(); + void run(); - private slots: + private slots: - void executeStage(); + void executeStage(); - protected slots: + protected slots: - virtual void operationDone(); + virtual void operationDone(); }; } diff --git a/apps/opencs/model/doc/operationholder.cpp b/apps/opencs/model/doc/operationholder.cpp index 0fd2bef9578..6beab1fd99a 100644 --- a/apps/opencs/model/doc/operationholder.cpp +++ b/apps/opencs/model/doc/operationholder.cpp @@ -2,34 +2,28 @@ #include "operation.hpp" -CSMDoc::OperationHolder::OperationHolder (Operation *operation) +CSMDoc::OperationHolder::OperationHolder(Operation* operation) : mOperation(nullptr) - , mRunning (false) + , mRunning(false) { if (operation) - setOperation (operation); + setOperation(operation); } -void CSMDoc::OperationHolder::setOperation (Operation *operation) +void CSMDoc::OperationHolder::setOperation(Operation* operation) { mOperation = operation; - mOperation->moveToThread (&mThread); + mOperation->moveToThread(&mThread); - connect ( - mOperation, SIGNAL (progress (int, int, int)), - this, SIGNAL (progress (int, int, int))); + connect(mOperation, &Operation::progress, this, &OperationHolder::progress); - connect ( - mOperation, SIGNAL (reportMessage (const CSMDoc::Message&, int)), - this, SIGNAL (reportMessage (const CSMDoc::Message&, int))); + connect(mOperation, &Operation::reportMessage, this, &OperationHolder::reportMessage); - connect ( - mOperation, SIGNAL (done (int, bool)), - this, SLOT (doneSlot (int, bool))); + connect(mOperation, &Operation::done, this, &OperationHolder::doneSlot); - connect (this, SIGNAL (abortSignal()), mOperation, SLOT (abort())); + connect(this, &OperationHolder::abortSignal, mOperation, &Operation::abort); - connect (&mThread, SIGNAL (started()), mOperation, SLOT (run())); + connect(&mThread, &QThread::started, mOperation, &Operation::run); } bool CSMDoc::OperationHolder::isRunning() const @@ -58,9 +52,9 @@ void CSMDoc::OperationHolder::abortAndWait() } } -void CSMDoc::OperationHolder::doneSlot (int type, bool failed) +void CSMDoc::OperationHolder::doneSlot(int type, bool failed) { mRunning = false; mThread.quit(); - emit done (type, failed); + emit done(type, failed); } diff --git a/apps/opencs/model/doc/operationholder.hpp b/apps/opencs/model/doc/operationholder.hpp index 69af6ed66cd..8056850b96c 100644 --- a/apps/opencs/model/doc/operationholder.hpp +++ b/apps/opencs/model/doc/operationholder.hpp @@ -4,53 +4,46 @@ #include #include -#include "messages.hpp" - -namespace CSMWorld -{ - class UniversalId; -} - namespace CSMDoc { class Operation; + struct Message; class OperationHolder : public QObject { - Q_OBJECT - - QThread mThread; - Operation *mOperation; - bool mRunning; + Q_OBJECT + + QThread mThread; + Operation* mOperation; + bool mRunning; - public: + public: + OperationHolder(Operation* operation = nullptr); - OperationHolder (Operation *operation = nullptr); + void setOperation(Operation* operation); - void setOperation (Operation *operation); + bool isRunning() const; - bool isRunning() const; + void start(); - void start(); + void abort(); - void abort(); + // Abort and wait until thread has finished. + void abortAndWait(); - // Abort and wait until thread has finished. - void abortAndWait(); + private slots: - private slots: + void doneSlot(int type, bool failed); - void doneSlot (int type, bool failed); - - signals: + signals: - void progress (int current, int max, int type); + void progress(int current, int max, int type); - void reportMessage (const CSMDoc::Message& message, int type); + void reportMessage(const CSMDoc::Message& message, int type); - void done (int type, bool failed); + void done(int type, bool failed); - void abortSignal(); + void abortSignal(); }; } diff --git a/apps/opencs/model/doc/runner.cpp b/apps/opencs/model/doc/runner.cpp index ccdff1444fc..d647d6b4982 100644 --- a/apps/opencs/model/doc/runner.cpp +++ b/apps/opencs/model/doc/runner.cpp @@ -1,22 +1,32 @@ #include "runner.hpp" -#include +#include + +#if defined(Q_OS_MAC) +#include #include +#endif + +#include +#include +#include #include #include +#include + #include "operationholder.hpp" -CSMDoc::Runner::Runner (const boost::filesystem::path& projectPath) -: mRunning (false), mStartup (nullptr), mProjectPath (projectPath) +CSMDoc::Runner::Runner(std::filesystem::path projectPath) + : mRunning(false) + , mStartup(nullptr) + , mProjectPath(std::move(projectPath)) { - connect (&mProcess, SIGNAL (finished (int, QProcess::ExitStatus)), - this, SLOT (finished (int, QProcess::ExitStatus))); + connect(&mProcess, qOverload(&QProcess::finished), this, &Runner::finished); - connect (&mProcess, SIGNAL (readyReadStandardOutput()), - this, SLOT (readyReadStandardOutput())); + connect(&mProcess, &QProcess::readyReadStandardOutput, this, &Runner::readyReadStandardOutput); - mProcess.setProcessChannelMode (QProcess::MergedChannels); + mProcess.setProcessChannelMode(QProcess::MergedChannels); mProfile.blank(); } @@ -25,13 +35,13 @@ CSMDoc::Runner::~Runner() { if (mRunning) { - disconnect (&mProcess, nullptr, this, nullptr); + disconnect(&mProcess, nullptr, this, nullptr); mProcess.kill(); mProcess.waitForFinished(); } } -void CSMDoc::Runner::start (bool delayed) +void CSMDoc::Runner::start(bool delayed) { if (mStartup) { @@ -56,16 +66,16 @@ void CSMDoc::Runner::start (bool delayed) path.prepend(QString("./")); #endif - mStartup = new QTemporaryFile (this); + mStartup = new QTemporaryFile(this); mStartup->open(); { - QTextStream stream (mStartup); + QTextStream stream(mStartup); if (!mStartupInstruction.empty()) - stream << QString::fromUtf8 (mStartupInstruction.c_str()) << '\n'; + stream << QString::fromUtf8(mStartupInstruction.c_str()) << '\n'; - stream << QString::fromUtf8 (mProfile.mScriptText.c_str()); + stream << QString::fromUtf8(mProfile.mScriptText.c_str()); } mStartup->close(); @@ -78,21 +88,20 @@ void CSMDoc::Runner::start (bool delayed) else arguments << "--new-game=1"; - arguments << ("--script-run="+mStartup->fileName());; + arguments << ("--script-run=" + mStartup->fileName()); + + arguments << "--data=\"" + Files::pathToQString(mProjectPath.parent_path()) + "\""; - arguments << - QString::fromUtf8 (("--data=\""+mProjectPath.parent_path().string()+"\"").c_str()); + arguments << "--replace=content"; - for (std::vector::const_iterator iter (mContentFiles.begin()); - iter!=mContentFiles.end(); ++iter) + for (const auto& mContentFile : mContentFiles) { - arguments << QString::fromUtf8 (("--content="+*iter).c_str()); + arguments << "--content=" + Files::pathToQString(mContentFile); } - arguments - << QString::fromUtf8 (("--content="+mProjectPath.filename().string()).c_str()); + arguments << "--content=" + Files::pathToQString(mProjectPath.filename()); - mProcess.start (path, arguments); + mProcess.start(path, arguments); } mRunning = true; @@ -104,7 +113,7 @@ void CSMDoc::Runner::stop() delete mStartup; mStartup = nullptr; - if (mProcess.state()==QProcess::NotRunning) + if (mProcess.state() == QProcess::NotRunning) { mRunning = false; emit runStateChanged(); @@ -118,39 +127,38 @@ bool CSMDoc::Runner::isRunning() const return mRunning; } -void CSMDoc::Runner::configure (const ESM::DebugProfile& profile, - const std::vector& contentFiles, const std::string& startupInstruction) +void CSMDoc::Runner::configure(const ESM::DebugProfile& profile, const std::vector& contentFiles, + const std::string& startupInstruction) { mProfile = profile; mContentFiles = contentFiles; mStartupInstruction = startupInstruction; } -void CSMDoc::Runner::finished (int exitCode, QProcess::ExitStatus exitStatus) +void CSMDoc::Runner::finished(int exitCode, QProcess::ExitStatus exitStatus) { mRunning = false; emit runStateChanged(); } -QTextDocument *CSMDoc::Runner::getLog() +QTextDocument* CSMDoc::Runner::getLog() { return &mLog; } void CSMDoc::Runner::readyReadStandardOutput() { - mLog.setPlainText ( - mLog.toPlainText() + QString::fromUtf8 (mProcess.readAllStandardOutput())); + mLog.setPlainText(mLog.toPlainText() + QString::fromUtf8(mProcess.readAllStandardOutput())); } - -CSMDoc::SaveWatcher::SaveWatcher (Runner *runner, OperationHolder *operation) -: QObject (runner), mRunner (runner) +CSMDoc::SaveWatcher::SaveWatcher(Runner* runner, OperationHolder* operation) + : QObject(runner) + , mRunner(runner) { - connect (operation, SIGNAL (done (int, bool)), this, SLOT (saveDone (int, bool))); + connect(operation, &OperationHolder::done, this, &SaveWatcher::saveDone); } -void CSMDoc::SaveWatcher::saveDone (int type, bool failed) +void CSMDoc::SaveWatcher::saveDone(int type, bool failed) { if (failed) mRunner->stop(); diff --git a/apps/opencs/model/doc/runner.hpp b/apps/opencs/model/doc/runner.hpp index 517122492a2..b94caca3869 100644 --- a/apps/opencs/model/doc/runner.hpp +++ b/apps/opencs/model/doc/runner.hpp @@ -1,86 +1,81 @@ #ifndef CSM_DOC_RUNNER_H #define CSM_DOC_RUNNER_H -#include #include - -#include +#include #include #include #include -#include +#include + +#include class QTemporaryFile; namespace CSMDoc { class OperationHolder; - + class Runner : public QObject { - Q_OBJECT - - QProcess mProcess; - bool mRunning; - ESM::DebugProfile mProfile; - std::vector mContentFiles; - std::string mStartupInstruction; - QTemporaryFile *mStartup; - QTextDocument mLog; - boost::filesystem::path mProjectPath; + Q_OBJECT - public: + QProcess mProcess; + bool mRunning; + ESM::DebugProfile mProfile; + std::vector mContentFiles; + std::string mStartupInstruction; + QTemporaryFile* mStartup; + QTextDocument mLog; + std::filesystem::path mProjectPath; - Runner (const boost::filesystem::path& projectPath); + public: + Runner(std::filesystem::path projectPath); - ~Runner(); + ~Runner(); - /// \param delayed Flag as running but do not start the OpenMW process yet (the - /// process must be started by another call of start with delayed==false) - void start (bool delayed = false); + /// \param delayed Flag as running but do not start the OpenMW process yet (the + /// process must be started by another call of start with delayed==false) + void start(bool delayed = false); - void stop(); + void stop(); - /// \note Running state is entered when the start function is called. This - /// is not necessarily identical to the moment the child process is started. - bool isRunning() const; + /// \note Running state is entered when the start function is called. This + /// is not necessarily identical to the moment the child process is started. + bool isRunning() const; - void configure (const ESM::DebugProfile& profile, - const std::vector& contentFiles, - const std::string& startupInstruction); + void configure(const ESM::DebugProfile& profile, const std::vector& contentFiles, + const std::string& startupInstruction); - QTextDocument *getLog(); + QTextDocument* getLog(); - signals: + signals: - void runStateChanged(); + void runStateChanged(); - private slots: + private slots: - void finished (int exitCode, QProcess::ExitStatus exitStatus); + void finished(int exitCode, QProcess::ExitStatus exitStatus); - void readyReadStandardOutput(); + void readyReadStandardOutput(); }; - class Operation; - /// \brief Watch for end of save operation and restart or stop runner class SaveWatcher : public QObject { - Q_OBJECT - - Runner *mRunner; + Q_OBJECT - public: + Runner* mRunner; - /// *this attaches itself to runner - SaveWatcher (Runner *runner, OperationHolder *operation); + public: + /// *this attaches itself to runner + SaveWatcher(Runner* runner, OperationHolder* operation); - private slots: + private slots: - void saveDone (int type, bool failed); + void saveDone(int type, bool failed); }; } diff --git a/apps/opencs/model/doc/saving.cpp b/apps/opencs/model/doc/saving.cpp index 95a2feaf24e..ed785c38fee 100644 --- a/apps/opencs/model/doc/saving.cpp +++ b/apps/opencs/model/doc/saving.cpp @@ -1,106 +1,132 @@ #include "saving.hpp" +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "../world/data.hpp" #include "../world/idcollection.hpp" -#include "state.hpp" -#include "savingstages.hpp" #include "document.hpp" +#include "savingstages.hpp" +#include "state.hpp" -CSMDoc::Saving::Saving (Document& document, const boost::filesystem::path& projectPath, - ToUTF8::FromType encoding) -: Operation (State_Saving, true, true), mDocument (document), mState (*this, projectPath, encoding) +CSMDoc::Saving::Saving(Document& document, const std::filesystem::path& projectPath, ToUTF8::FromType encoding) + : Operation(State_Saving, true, true) + , mDocument(document) + , mState(*this, projectPath, encoding) { // save project file - appendStage (new OpenSaveStage (mDocument, mState, true)); + appendStage(new OpenSaveStage(mDocument, mState, true)); - appendStage (new WriteHeaderStage (mDocument, mState, true)); + appendStage(new WriteHeaderStage(mDocument, mState, true)); - appendStage (new WriteCollectionStage > ( + appendStage(new WriteCollectionStage>( mDocument.getData().getFilters(), mState, CSMWorld::Scope_Project)); - appendStage (new WriteCollectionStage > ( + appendStage(new WriteCollectionStage>( mDocument.getData().getDebugProfiles(), mState, CSMWorld::Scope_Project)); - appendStage (new WriteCollectionStage > ( + appendStage(new WriteCollectionStage>( mDocument.getData().getScripts(), mState, CSMWorld::Scope_Project)); - appendStage (new CloseSaveStage (mState)); + appendStage(new WriteCollectionStage>( + mDocument.getData().getSelectionGroups(), mState, CSMWorld::Scope_Project)); + + appendStage(new CloseSaveStage(mState)); // save content file - appendStage (new OpenSaveStage (mDocument, mState, false)); + appendStage(new OpenSaveStage(mDocument, mState, false)); - appendStage (new WriteHeaderStage (mDocument, mState, false)); + appendStage(new WriteHeaderStage(mDocument, mState, false)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getGlobals(), mState)); + appendStage( + new WriteCollectionStage>(mDocument.getData().getGlobals(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getGmsts(), mState)); + appendStage( + new WriteCollectionStage>(mDocument.getData().getGmsts(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getSkills(), mState)); + appendStage(new WriteCollectionStage>(mDocument.getData().getSkills(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getClasses(), mState)); + appendStage(new WriteCollectionStage>(mDocument.getData().getClasses(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getFactions(), mState)); + appendStage( + new WriteCollectionStage>(mDocument.getData().getFactions(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getRaces(), mState)); + appendStage(new WriteCollectionStage>(mDocument.getData().getRaces(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getSounds(), mState)); + appendStage(new WriteCollectionStage>(mDocument.getData().getSounds(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getScripts(), mState)); + appendStage( + new WriteCollectionStage>(mDocument.getData().getScripts(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getRegions(), mState)); + appendStage( + new WriteCollectionStage>(mDocument.getData().getRegions(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getBirthsigns(), mState)); + appendStage( + new WriteCollectionStage>(mDocument.getData().getBirthsigns(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getSpells(), mState)); + appendStage(new WriteCollectionStage>(mDocument.getData().getSpells(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getEnchantments(), mState)); + appendStage(new WriteCollectionStage>( + mDocument.getData().getEnchantments(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getBodyParts(), mState)); + appendStage( + new WriteCollectionStage>(mDocument.getData().getBodyParts(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getSoundGens(), mState)); + appendStage(new WriteCollectionStage>( + mDocument.getData().getSoundGens(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getMagicEffects(), mState)); + appendStage(new WriteCollectionStage>( + mDocument.getData().getMagicEffects(), mState)); - appendStage (new WriteCollectionStage > - (mDocument.getData().getStartScripts(), mState)); + appendStage(new WriteCollectionStage>( + mDocument.getData().getStartScripts(), mState)); - appendStage (new WriteRefIdCollectionStage (mDocument, mState)); + appendStage(new WriteRefIdCollectionStage(mDocument, mState)); - appendStage (new CollectionReferencesStage (mDocument, mState)); + appendStage(new CollectionReferencesStage(mDocument, mState)); - appendStage (new WriteCellCollectionStage (mDocument, mState)); + appendStage(new WriteCellCollectionStage(mDocument, mState)); // Dialogue can reference objects and cells so must be written after these records for vanilla-compatible files - appendStage (new WriteDialogueCollectionStage (mDocument, mState, false)); + appendStage(new WriteDialogueCollectionStage(mDocument, mState, false)); - appendStage (new WriteDialogueCollectionStage (mDocument, mState, true)); + appendStage(new WriteDialogueCollectionStage(mDocument, mState, true)); - appendStage (new WritePathgridCollectionStage (mDocument, mState)); + appendStage(new WritePathgridCollectionStage(mDocument, mState)); - appendStage (new WriteLandTextureCollectionStage (mDocument, mState)); + appendStage(new WriteLandTextureCollectionStage(mDocument, mState)); // references Land Textures - appendStage (new WriteLandCollectionStage (mDocument, mState)); + appendStage(new WriteLandCollectionStage(mDocument, mState)); // close file and clean up - appendStage (new CloseSaveStage (mState)); + appendStage(new CloseSaveStage(mState)); - appendStage (new FinalSavingStage (mDocument, mState)); + appendStage(new FinalSavingStage(mDocument, mState)); } diff --git a/apps/opencs/model/doc/saving.hpp b/apps/opencs/model/doc/saving.hpp index 44239b21b5f..5dcdbb68032 100644 --- a/apps/opencs/model/doc/saving.hpp +++ b/apps/opencs/model/doc/saving.hpp @@ -1,29 +1,28 @@ #ifndef CSM_DOC_SAVING_H #define CSM_DOC_SAVING_H -#include +#include #include #include "operation.hpp" #include "savingstate.hpp" +#include + namespace CSMDoc { class Document; class Saving : public Operation { - Q_OBJECT - - Document& mDocument; - SavingState mState; - - public: + Q_OBJECT - Saving (Document& document, const boost::filesystem::path& projectPath, - ToUTF8::FromType encoding); + Document& mDocument; + SavingState mState; + public: + Saving(Document& document, const std::filesystem::path& projectPath, ToUTF8::FromType encoding); }; } diff --git a/apps/opencs/model/doc/savingstages.cpp b/apps/opencs/model/doc/savingstages.cpp index 44698cd2e33..12fa6e811f1 100644 --- a/apps/opencs/model/doc/savingstages.cpp +++ b/apps/opencs/model/doc/savingstages.cpp @@ -1,48 +1,80 @@ #include "savingstages.hpp" -#include - #include -#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include -#include "../world/infocollection.hpp" #include "../world/cellcoordinates.hpp" #include "document.hpp" -CSMDoc::OpenSaveStage::OpenSaveStage (Document& document, SavingState& state, bool projectFile) -: mDocument (document), mState (state), mProjectFile (projectFile) -{} +CSMDoc::OpenSaveStage::OpenSaveStage(Document& document, SavingState& state, bool projectFile) + : mDocument(document) + , mState(state) + , mProjectFile(projectFile) +{ +} int CSMDoc::OpenSaveStage::setup() { return 1; } -void CSMDoc::OpenSaveStage::perform (int stage, Messages& messages) +void CSMDoc::OpenSaveStage::perform(int stage, Messages& messages) { - mState.start (mDocument, mProjectFile); + mState.start(mDocument, mProjectFile); - mState.getStream().open ( - mProjectFile ? mState.getPath() : mState.getTmpPath(), - std::ios::binary); + mState.getStream().open(mProjectFile ? mState.getPath() : mState.getTmpPath(), std::ios::binary); if (!mState.getStream().is_open()) - throw std::runtime_error ("failed to open stream for saving"); + throw std::runtime_error("failed to open stream for saving"); } - -CSMDoc::WriteHeaderStage::WriteHeaderStage (Document& document, SavingState& state, bool simple) -: mDocument (document), mState (state), mSimple (simple) -{} +CSMDoc::WriteHeaderStage::WriteHeaderStage(Document& document, SavingState& state, bool simple) + : mDocument(document) + , mState(state) + , mSimple(simple) +{ +} int CSMDoc::WriteHeaderStage::setup() { return 1; } -void CSMDoc::WriteHeaderStage::perform (int stage, Messages& messages) +void CSMDoc::WriteHeaderStage::perform(int stage, Messages& messages) { mState.getWriter().setVersion(); @@ -50,58 +82,59 @@ void CSMDoc::WriteHeaderStage::perform (int stage, Messages& messages) if (mSimple) { - mState.getWriter().setAuthor (""); - mState.getWriter().setDescription (""); - mState.getWriter().setRecordCount (0); - mState.getWriter().setFormat (ESM::Header::CurrentFormat); + mState.getWriter().setAuthor(""); + mState.getWriter().setDescription(""); + mState.getWriter().setRecordCount(0); + + // ESM::Header::CurrentFormat is `1` but since new records are not yet used in opencs + // we use the format `0` for compatibility with old versions. + mState.getWriter().setFormatVersion(ESM::DefaultFormatVersion); } else { - mDocument.getData().getMetaData().save (mState.getWriter()); - mState.getWriter().setRecordCount ( - mDocument.getData().count (CSMWorld::RecordBase::State_Modified) + - mDocument.getData().count (CSMWorld::RecordBase::State_ModifiedOnly) + - mDocument.getData().count (CSMWorld::RecordBase::State_Deleted)); + mDocument.getData().getMetaData().save(mState.getWriter()); + mState.getWriter().setRecordCount(mDocument.getData().count(CSMWorld::RecordBase::State_Modified) + + mDocument.getData().count(CSMWorld::RecordBase::State_ModifiedOnly) + + mDocument.getData().count(CSMWorld::RecordBase::State_Deleted)); /// \todo refine dependency list (at least remove redundant dependencies) - std::vector dependencies = mDocument.getContentFiles(); - std::vector::const_iterator end (--dependencies.end()); + std::vector dependencies = mDocument.getContentFiles(); + std::vector::const_iterator end(--dependencies.end()); - for (std::vector::const_iterator iter (dependencies.begin()); - iter!=end; ++iter) + for (std::vector::const_iterator iter(dependencies.begin()); iter != end; ++iter) { - std::string name = iter->filename().string(); - uint64_t size = boost::filesystem::file_size (*iter); + auto name = Files::pathToUnicodeString(iter->filename()); + auto size = std::filesystem::file_size(*iter); - mState.getWriter().addMaster (name, size); + mState.getWriter().addMaster(name, size); } } - mState.getWriter().save (mState.getStream()); + mState.getWriter().save(mState.getStream()); } - -CSMDoc::WriteDialogueCollectionStage::WriteDialogueCollectionStage (Document& document, - SavingState& state, bool journal) -: mState (state), - mTopics (journal ? document.getData().getJournals() : document.getData().getTopics()), - mInfos (journal ? document.getData().getJournalInfos() : document.getData().getTopicInfos()) -{} +CSMDoc::WriteDialogueCollectionStage::WriteDialogueCollectionStage(Document& document, SavingState& state, bool journal) + : mState(state) + , mTopics(journal ? document.getData().getJournals() : document.getData().getTopics()) + , mInfos(journal ? document.getData().getJournalInfos() : document.getData().getTopicInfos()) +{ +} int CSMDoc::WriteDialogueCollectionStage::setup() { + mInfosByTopic = mInfos.getInfosByTopic(); return mTopics.getSize(); } -void CSMDoc::WriteDialogueCollectionStage::perform (int stage, Messages& messages) +void CSMDoc::WriteDialogueCollectionStage::perform(int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); - const CSMWorld::Record& topic = mTopics.getRecord (stage); + const CSMWorld::Record& topic = mTopics.getRecord(stage); if (topic.mState == CSMWorld::RecordBase::State_Deleted) { // if the topic is deleted, we do not need to bother with INFO records. - ESM::Dialogue dialogue = topic.get(); + const ESM::Dialogue& dialogue = topic.get(); writer.startRecord(dialogue.sRecordId); dialogue.save(writer, true); writer.endRecord(dialogue.sRecordId); @@ -110,191 +143,261 @@ void CSMDoc::WriteDialogueCollectionStage::perform (int stage, Messages& message // Test, if we need to save anything associated info records. bool infoModified = false; - CSMWorld::InfoCollection::Range range = mInfos.getTopicRange (topic.get().mId); + const auto topicInfos = mInfosByTopic.find(topic.get().mId); - for (CSMWorld::InfoCollection::RecordConstIterator iter (range.first); iter!=range.second; ++iter) + if (topicInfos != mInfosByTopic.end()) { - if (iter->isModified() || iter->mState == CSMWorld::RecordBase::State_Deleted) + for (const auto& record : topicInfos->second) { - infoModified = true; - break; + if (record->isModified() || record->mState == CSMWorld::RecordBase::State_Deleted) + { + infoModified = true; + break; + } } } if (topic.isModified() || infoModified) { if (infoModified && topic.mState != CSMWorld::RecordBase::State_Modified - && topic.mState != CSMWorld::RecordBase::State_ModifiedOnly) + && topic.mState != CSMWorld::RecordBase::State_ModifiedOnly) { - mState.getWriter().startRecord (topic.mBase.sRecordId); - topic.mBase.save (mState.getWriter(), topic.mState == CSMWorld::RecordBase::State_Deleted); - mState.getWriter().endRecord (topic.mBase.sRecordId); + mState.getWriter().startRecord(topic.mBase.sRecordId); + topic.mBase.save(mState.getWriter(), topic.mState == CSMWorld::RecordBase::State_Deleted); + mState.getWriter().endRecord(topic.mBase.sRecordId); } else { - mState.getWriter().startRecord (topic.mModified.sRecordId); - topic.mModified.save (mState.getWriter(), topic.mState == CSMWorld::RecordBase::State_Deleted); - mState.getWriter().endRecord (topic.mModified.sRecordId); + mState.getWriter().startRecord(topic.mModified.sRecordId); + topic.mModified.save(mState.getWriter(), topic.mState == CSMWorld::RecordBase::State_Deleted); + mState.getWriter().endRecord(topic.mModified.sRecordId); } // write modified selected info records - for (CSMWorld::InfoCollection::RecordConstIterator iter (range.first); iter!=range.second; ++iter) + if (topicInfos != mInfosByTopic.end()) { - if (iter->isModified() || iter->mState == CSMWorld::RecordBase::State_Deleted) + const std::vector*>& infos = topicInfos->second; + + for (auto iter = infos.begin(); iter != infos.end(); ++iter) { - ESM::DialInfo info = iter->get(); - info.mId = info.mId.substr (info.mId.find_last_of ('#')+1); + const CSMWorld::Record& record = **iter; - info.mPrev = ""; - if (iter!=range.first) + if (record.isModified() || record.mState == CSMWorld::RecordBase::State_Deleted) { - CSMWorld::InfoCollection::RecordConstIterator prev = iter; - --prev; + ESM::DialInfo info = record.get(); + info.mId = record.get().mOriginalId; + info.mData.mType = topic.get().mType; - info.mPrev = prev->get().mId.substr (prev->get().mId.find_last_of ('#')+1); - } + if (iter == infos.begin()) + info.mPrev = ESM::RefId(); + else + info.mPrev = (*std::prev(iter))->get().mOriginalId; - CSMWorld::InfoCollection::RecordConstIterator next = iter; - ++next; + const auto next = std::next(iter); - info.mNext = ""; - if (next!=range.second) - { - info.mNext = next->get().mId.substr (next->get().mId.find_last_of ('#')+1); - } + if (next == infos.end()) + info.mNext = ESM::RefId(); + else + info.mNext = (*next)->get().mOriginalId; - writer.startRecord (info.sRecordId); - info.save (writer, iter->mState == CSMWorld::RecordBase::State_Deleted); - writer.endRecord (info.sRecordId); + writer.startRecord(info.sRecordId); + info.save(writer, record.mState == CSMWorld::RecordBase::State_Deleted); + writer.endRecord(info.sRecordId); + } } } } } - -CSMDoc::WriteRefIdCollectionStage::WriteRefIdCollectionStage (Document& document, SavingState& state) -: mDocument (document), mState (state) -{} +CSMDoc::WriteRefIdCollectionStage::WriteRefIdCollectionStage(Document& document, SavingState& state) + : mDocument(document) + , mState(state) +{ +} int CSMDoc::WriteRefIdCollectionStage::setup() { return mDocument.getData().getReferenceables().getSize(); } -void CSMDoc::WriteRefIdCollectionStage::perform (int stage, Messages& messages) +void CSMDoc::WriteRefIdCollectionStage::perform(int stage, Messages& messages) { - mDocument.getData().getReferenceables().save (stage, mState.getWriter()); + mDocument.getData().getReferenceables().save(stage, mState.getWriter()); } - -CSMDoc::CollectionReferencesStage::CollectionReferencesStage (Document& document, - SavingState& state) -: mDocument (document), mState (state) -{} +CSMDoc::CollectionReferencesStage::CollectionReferencesStage(Document& document, SavingState& state) + : mDocument(document) + , mState(state) +{ +} int CSMDoc::CollectionReferencesStage::setup() { - mState.getSubRecords().clear(); + mState.clearSubRecords(); int size = mDocument.getData().getReferences().getSize(); - int steps = size/100; - if (size%100) ++steps; + int steps = size / 100; + if (size % 100) + ++steps; return steps; } -void CSMDoc::CollectionReferencesStage::perform (int stage, Messages& messages) +void CSMDoc::CollectionReferencesStage::perform(int stage, Messages& messages) { int size = mDocument.getData().getReferences().getSize(); - for (int i=stage*100; i& record = - mDocument.getData().getReferences().getRecord (i); + const CSMWorld::Record& record = mDocument.getData().getReferences().getRecord(i); if (record.isModified() || record.mState == CSMWorld::RecordBase::State_Deleted) { - std::string cellId = record.get().mOriginalCell.empty() ? - record.get().mCell : record.get().mOriginalCell; + ESM::RefId cellId = record.get().mOriginalCell.empty() ? record.get().mCell : record.get().mOriginalCell; - std::deque& indices = - mState.getSubRecords()[Misc::StringUtils::lowerCase (cellId)]; + std::deque& indices = mState.getOrInsertSubRecord(cellId); // collect moved references at the end of the container - bool interior = cellId.substr (0, 1)!="#"; + + const bool interior = !cellId.startsWith("#"); std::ostringstream stream; if (!interior) { // recalculate the ref's cell location std::pair index = record.get().getCellIndex(); - stream << "#" << index.first << " " << index.second; + cellId = ESM::RefId::stringRefId(ESM::RefId::esm3ExteriorCell(index.first, index.second).toString()); } // An empty mOriginalCell is meant to indicate that it is the same as // the current cell. It is possible that a moved ref is moved again. - if ((record.get().mOriginalCell.empty() ? - record.get().mCell : record.get().mOriginalCell) != stream.str() && !interior && record.mState!=CSMWorld::RecordBase::State_ModifiedOnly && !record.get().mNew) - indices.push_back (i); + if ((record.get().mOriginalCell.empty() ? record.get().mCell : record.get().mOriginalCell) != cellId + && !interior && record.mState != CSMWorld::RecordBase::State_ModifiedOnly && !record.get().mNew) + indices.push_back(i); else - indices.push_front (i); + indices.push_front(i); } } } - -CSMDoc::WriteCellCollectionStage::WriteCellCollectionStage (Document& document, - SavingState& state) -: mDocument (document), mState (state) -{} +CSMDoc::WriteCellCollectionStage::WriteCellCollectionStage(Document& document, SavingState& state) + : mDocument(document) + , mState(state) +{ +} int CSMDoc::WriteCellCollectionStage::setup() { return mDocument.getData().getCells().getSize(); } -void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages) +void CSMDoc::WriteCellCollectionStage::writeReferences( + const std::deque& references, bool interior, unsigned int& newRefNum) +{ + ESM::ESMWriter& writer = mState.getWriter(); + + for (std::deque::const_iterator iter(references.begin()); iter != references.end(); ++iter) + { + const CSMWorld::Record& ref = mDocument.getData().getReferences().getRecord(*iter); + + if (ref.isModified() || ref.mState == CSMWorld::RecordBase::State_Deleted) + { + CSMWorld::CellRef refRecord = ref.get(); + + // -1 is the current file, saved indices are 1-based + refRecord.mRefNum.mContentFile++; + + // recalculate the ref's cell location + std::ostringstream stream; + if (!interior) + { + std::pair index = refRecord.getCellIndex(); + stream << "#" << index.first << " " << index.second; + } + + ESM::RefId streamId = ESM::RefId::stringRefId(stream.str()); + if (refRecord.mNew || refRecord.mRefNum.mIndex == 0 + || (!interior && ref.mState == CSMWorld::RecordBase::State_ModifiedOnly && refRecord.mCell != streamId)) + { + refRecord.mRefNum.mIndex = newRefNum++; + } + else if ((refRecord.mOriginalCell.empty() ? refRecord.mCell : refRecord.mOriginalCell) != streamId + && !interior) + { + // An empty mOriginalCell is meant to indicate that it is the same as + // the current cell. It is possible that a moved ref is moved again. + + ESM::MovedCellRef moved; + moved.mRefNum = refRecord.mRefNum; + + // Need to fill mTarget with the ref's new position. + std::istringstream istream(stream.str().c_str()); + + char ignore; + istream >> ignore >> moved.mTarget[0] >> moved.mTarget[1]; + + writer.writeFormId(refRecord.mRefNum, false, "MVRF"); + writer.writeHNT("CNDT", moved.mTarget); + } + + refRecord.save(writer, false, false, ref.mState == CSMWorld::RecordBase::State_Deleted); + } + } +} + +void CSMDoc::WriteCellCollectionStage::perform(int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); - const CSMWorld::Record& cell = mDocument.getData().getCells().getRecord (stage); + const CSMWorld::Record& cell = mDocument.getData().getCells().getRecord(stage); + const CSMWorld::RefIdCollection& referenceables = mDocument.getData().getReferenceables(); + const CSMWorld::RefIdData& refIdData = referenceables.getDataSet(); + + std::deque tempRefs; + std::deque persistentRefs; - std::map >::const_iterator references = - mState.getSubRecords().find (Misc::StringUtils::lowerCase (cell.get().mId)); + const std::deque* references = mState.findSubRecord(cell.get().mId); - if (cell.isModified() || - cell.mState == CSMWorld::RecordBase::State_Deleted || - references!=mState.getSubRecords().end()) + if (cell.isModified() || cell.mState == CSMWorld::RecordBase::State_Deleted || references != nullptr) { CSMWorld::Cell cellRecord = cell.get(); - bool interior = cellRecord.mId.substr (0, 1)!="#"; + const bool interior = !cellRecord.mId.startsWith("#"); // count new references and adjust RefNumCount accordingsly unsigned int newRefNum = cellRecord.mRefNumCounter; - if (references!=mState.getSubRecords().end()) + if (references != nullptr) { - for (std::deque::const_iterator iter (references->second.begin()); - iter!=references->second.end(); ++iter) + for (std::deque::const_iterator iter(references->begin()); iter != references->end(); ++iter) { - const CSMWorld::Record& ref = - mDocument.getData().getReferences().getRecord (*iter); + const CSMWorld::Record& ref = mDocument.getData().getReferences().getRecord(*iter); CSMWorld::CellRef refRecord = ref.get(); - if (refRecord.mNew || - (!interior && ref.mState==CSMWorld::RecordBase::State_ModifiedOnly && - /// \todo consider worldspace - CSMWorld::CellCoordinates (refRecord.getCellIndex()).getId("") != refRecord.mCell)) + CSMWorld::RefIdData::LocalIndex localIndex = refIdData.searchId(refRecord.mRefID); + unsigned int recordFlags = refIdData.getRecordFlags(refRecord.mRefID); + bool isPersistent = ((recordFlags & ESM::FLAG_Persistent) != 0) || refRecord.mTeleport + || localIndex.second == CSMWorld::UniversalId::Type_Creature + || localIndex.second == CSMWorld::UniversalId::Type_Npc; + + if (isPersistent) + persistentRefs.push_back(*iter); + else + tempRefs.push_back(*iter); + + if (refRecord.mNew + || (!interior && ref.mState == CSMWorld::RecordBase::State_ModifiedOnly && + /// \todo consider worldspace + ESM::RefId::stringRefId(CSMWorld::CellCoordinates(refRecord.getCellIndex()).getId("")) + != refRecord.mCell)) ++cellRecord.mRefNumCounter; if (refRecord.mRefNum.mIndex >= newRefNum) newRefNum = refRecord.mRefNum.mIndex + 1; - } } // write cell data - writer.startRecord (cellRecord.sRecordId); + writer.startRecord(cellRecord.sRecordId); if (interior) cellRecord.mData.mFlags |= ESM::Cell::Interior; @@ -302,204 +405,154 @@ void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages) { cellRecord.mData.mFlags &= ~ESM::Cell::Interior; - std::istringstream stream (cellRecord.mId.c_str()); + std::istringstream stream(cellRecord.mId.getRefIdString().c_str()); char ignore; stream >> ignore >> cellRecord.mData.mX >> cellRecord.mData.mY; } - cellRecord.save (writer, cell.mState == CSMWorld::RecordBase::State_Deleted); + cellRecord.save(writer, cell.mState == CSMWorld::RecordBase::State_Deleted); // write references - if (references!=mState.getSubRecords().end()) + if (references != nullptr) { - for (std::deque::const_iterator iter (references->second.begin()); - iter!=references->second.end(); ++iter) - { - const CSMWorld::Record& ref = - mDocument.getData().getReferences().getRecord (*iter); - - if (ref.isModified() || ref.mState == CSMWorld::RecordBase::State_Deleted) - { - CSMWorld::CellRef refRecord = ref.get(); - - // Check for uninitialized content file - if (!refRecord.mRefNum.hasContentFile()) - refRecord.mRefNum.mContentFile = 0; - - // recalculate the ref's cell location - std::ostringstream stream; - if (!interior) - { - std::pair index = refRecord.getCellIndex(); - stream << "#" << index.first << " " << index.second; - } - - if (refRecord.mNew || refRecord.mRefNum.mIndex == 0 || - (!interior && ref.mState==CSMWorld::RecordBase::State_ModifiedOnly && - refRecord.mCell!=stream.str())) - { - refRecord.mRefNum.mIndex = newRefNum++; - } - else if ((refRecord.mOriginalCell.empty() ? refRecord.mCell : refRecord.mOriginalCell) - != stream.str() && !interior) - { - // An empty mOriginalCell is meant to indicate that it is the same as - // the current cell. It is possible that a moved ref is moved again. - - ESM::MovedCellRef moved; - moved.mRefNum = refRecord.mRefNum; - - // Need to fill mTarget with the ref's new position. - std::istringstream istream (stream.str().c_str()); - - char ignore; - istream >> ignore >> moved.mTarget[0] >> moved.mTarget[1]; - - refRecord.mRefNum.save (writer, false, "MVRF"); - writer.writeHNT ("CNDT", moved.mTarget); - } - - refRecord.save (writer, false, false, ref.mState == CSMWorld::RecordBase::State_Deleted); - } - } + writeReferences(persistentRefs, interior, newRefNum); + cellRecord.saveTempMarker(writer, static_cast(references->size()) - persistentRefs.size()); + writeReferences(tempRefs, interior, newRefNum); } - writer.endRecord (cellRecord.sRecordId); + writer.endRecord(cellRecord.sRecordId); } } - -CSMDoc::WritePathgridCollectionStage::WritePathgridCollectionStage (Document& document, - SavingState& state) -: mDocument (document), mState (state) -{} +CSMDoc::WritePathgridCollectionStage::WritePathgridCollectionStage(Document& document, SavingState& state) + : mDocument(document) + , mState(state) +{ +} int CSMDoc::WritePathgridCollectionStage::setup() { return mDocument.getData().getPathgrids().getSize(); } -void CSMDoc::WritePathgridCollectionStage::perform (int stage, Messages& messages) +void CSMDoc::WritePathgridCollectionStage::perform(int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); - const CSMWorld::Record& pathgrid = - mDocument.getData().getPathgrids().getRecord (stage); + const CSMWorld::Record& pathgrid = mDocument.getData().getPathgrids().getRecord(stage); if (pathgrid.isModified() || pathgrid.mState == CSMWorld::RecordBase::State_Deleted) { CSMWorld::Pathgrid record = pathgrid.get(); - - if (record.mId.substr (0, 1)=="#") + if (record.mId.startsWith("#")) { - std::istringstream stream (record.mId.c_str()); + std::istringstream stream(record.mId.getRefIdString()); char ignore; stream >> ignore >> record.mData.mX >> record.mData.mY; } else record.mCell = record.mId; - writer.startRecord (record.sRecordId); - record.save (writer, pathgrid.mState == CSMWorld::RecordBase::State_Deleted); - writer.endRecord (record.sRecordId); + writer.startRecord(record.sRecordId); + record.save(writer, pathgrid.mState == CSMWorld::RecordBase::State_Deleted); + writer.endRecord(record.sRecordId); } } - -CSMDoc::WriteLandCollectionStage::WriteLandCollectionStage (Document& document, - SavingState& state) -: mDocument (document), mState (state) -{} +CSMDoc::WriteLandCollectionStage::WriteLandCollectionStage(Document& document, SavingState& state) + : mDocument(document) + , mState(state) +{ +} int CSMDoc::WriteLandCollectionStage::setup() { return mDocument.getData().getLand().getSize(); } -void CSMDoc::WriteLandCollectionStage::perform (int stage, Messages& messages) +void CSMDoc::WriteLandCollectionStage::perform(int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); - const CSMWorld::Record& land = - mDocument.getData().getLand().getRecord (stage); + const CSMWorld::Record& land = mDocument.getData().getLand().getRecord(stage); if (land.isModified() || land.mState == CSMWorld::RecordBase::State_Deleted) { CSMWorld::Land record = land.get(); - writer.startRecord (record.sRecordId); - record.save (writer, land.mState == CSMWorld::RecordBase::State_Deleted); - writer.endRecord (record.sRecordId); + writer.startRecord(record.sRecordId); + record.save(writer, land.mState == CSMWorld::RecordBase::State_Deleted); + writer.endRecord(record.sRecordId); } } - -CSMDoc::WriteLandTextureCollectionStage::WriteLandTextureCollectionStage (Document& document, - SavingState& state) -: mDocument (document), mState (state) -{} +CSMDoc::WriteLandTextureCollectionStage::WriteLandTextureCollectionStage(Document& document, SavingState& state) + : mDocument(document) + , mState(state) +{ +} int CSMDoc::WriteLandTextureCollectionStage::setup() { return mDocument.getData().getLandTextures().getSize(); } -void CSMDoc::WriteLandTextureCollectionStage::perform (int stage, Messages& messages) +void CSMDoc::WriteLandTextureCollectionStage::perform(int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); - const CSMWorld::Record& landTexture = - mDocument.getData().getLandTextures().getRecord (stage); + const CSMWorld::Record& landTexture = mDocument.getData().getLandTextures().getRecord(stage); if (landTexture.isModified() || landTexture.mState == CSMWorld::RecordBase::State_Deleted) { - CSMWorld::LandTexture record = landTexture.get(); - writer.startRecord (record.sRecordId); - record.save (writer, landTexture.mState == CSMWorld::RecordBase::State_Deleted); - writer.endRecord (record.sRecordId); + ESM::LandTexture record = landTexture.get(); + writer.startRecord(record.sRecordId); + record.save(writer, landTexture.mState == CSMWorld::RecordBase::State_Deleted); + writer.endRecord(record.sRecordId); } } - -CSMDoc::CloseSaveStage::CloseSaveStage (SavingState& state) -: mState (state) -{} +CSMDoc::CloseSaveStage::CloseSaveStage(SavingState& state) + : mState(state) +{ +} int CSMDoc::CloseSaveStage::setup() { return 1; } -void CSMDoc::CloseSaveStage::perform (int stage, Messages& messages) +void CSMDoc::CloseSaveStage::perform(int stage, Messages& messages) { mState.getStream().close(); if (!mState.getStream()) - throw std::runtime_error ("saving failed"); + throw std::runtime_error("saving failed"); } - -CSMDoc::FinalSavingStage::FinalSavingStage (Document& document, SavingState& state) -: mDocument (document), mState (state) -{} +CSMDoc::FinalSavingStage::FinalSavingStage(Document& document, SavingState& state) + : mDocument(document) + , mState(state) +{ +} int CSMDoc::FinalSavingStage::setup() { return 1; } -void CSMDoc::FinalSavingStage::perform (int stage, Messages& messages) +void CSMDoc::FinalSavingStage::perform(int stage, Messages& messages) { if (mState.hasError()) { mState.getWriter().close(); mState.getStream().close(); - if (boost::filesystem::exists (mState.getTmpPath())) - boost::filesystem::remove (mState.getTmpPath()); + if (std::filesystem::exists(mState.getTmpPath())) + std::filesystem::remove(mState.getTmpPath()); } else if (!mState.isProjectFile()) { - if (boost::filesystem::exists (mState.getPath())) - boost::filesystem::remove (mState.getPath()); + if (std::filesystem::exists(mState.getPath())) + std::filesystem::remove(mState.getPath()); - boost::filesystem::rename (mState.getTmpPath(), mState.getPath()); + std::filesystem::rename(mState.getTmpPath(), mState.getPath()); mDocument.getUndoStack().setClean(); } diff --git a/apps/opencs/model/doc/savingstages.hpp b/apps/opencs/model/doc/savingstages.hpp index d3a0cc9b316..5423b8f5049 100644 --- a/apps/opencs/model/doc/savingstages.hpp +++ b/apps/opencs/model/doc/savingstages.hpp @@ -3,17 +3,20 @@ #include "stage.hpp" -#include "../world/record.hpp" +#include +#include + #include "../world/idcollection.hpp" +#include "../world/infocollection.hpp" +#include "../world/record.hpp" #include "../world/scope.hpp" -#include - #include "savingstate.hpp" namespace ESM { struct Dialogue; + class ESMWriter; } namespace CSMWorld @@ -24,244 +27,230 @@ namespace CSMWorld namespace CSMDoc { class Document; - class SavingState; + class Messages; class OpenSaveStage : public Stage { - Document& mDocument; - SavingState& mState; - bool mProjectFile; - - public: + Document& mDocument; + SavingState& mState; + bool mProjectFile; - OpenSaveStage (Document& document, SavingState& state, bool projectFile); - ///< \param projectFile Saving the project file instead of the content file. + public: + OpenSaveStage(Document& document, SavingState& state, bool projectFile); + ///< \param projectFile Saving the project file instead of the content file. - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; class WriteHeaderStage : public Stage { - Document& mDocument; - SavingState& mState; - bool mSimple; - - public: + Document& mDocument; + SavingState& mState; + bool mSimple; - WriteHeaderStage (Document& document, SavingState& state, bool simple); - ///< \param simple Simplified header (used for project files). + public: + WriteHeaderStage(Document& document, SavingState& state, bool simple); + ///< \param simple Simplified header (used for project files). - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; - - template + template class WriteCollectionStage : public Stage { - const CollectionT& mCollection; - SavingState& mState; - CSMWorld::Scope mScope; - - public: + const CollectionT& mCollection; + SavingState& mState; + CSMWorld::Scope mScope; - WriteCollectionStage (const CollectionT& collection, SavingState& state, - CSMWorld::Scope scope = CSMWorld::Scope_Content); + public: + WriteCollectionStage( + const CollectionT& collection, SavingState& state, CSMWorld::Scope scope = CSMWorld::Scope_Content); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; - template - WriteCollectionStage::WriteCollectionStage (const CollectionT& collection, - SavingState& state, CSMWorld::Scope scope) - : mCollection (collection), mState (state), mScope (scope) - {} + template + WriteCollectionStage::WriteCollectionStage( + const CollectionT& collection, SavingState& state, CSMWorld::Scope scope) + : mCollection(collection) + , mState(state) + , mScope(scope) + { + } - template + template int WriteCollectionStage::setup() { return mCollection.getSize(); } - template - void WriteCollectionStage::perform (int stage, Messages& messages) + template + void WriteCollectionStage::perform(int stage, Messages& messages) { - if (CSMWorld::getScopeFromId (mCollection.getRecord (stage).get().mId)!=mScope) + if (CSMWorld::getScopeFromId(mCollection.getRecord(stage).get().mId) != mScope) return; ESM::ESMWriter& writer = mState.getWriter(); - CSMWorld::RecordBase::State state = mCollection.getRecord (stage).mState; - typename CollectionT::ESXRecord record = mCollection.getRecord (stage).get(); + CSMWorld::RecordBase::State state = mCollection.getRecord(stage).mState; + typename CollectionT::ESXRecord record = mCollection.getRecord(stage).get(); - if (state == CSMWorld::RecordBase::State_Modified || - state == CSMWorld::RecordBase::State_ModifiedOnly || - state == CSMWorld::RecordBase::State_Deleted) + if (state == CSMWorld::RecordBase::State_Modified || state == CSMWorld::RecordBase::State_ModifiedOnly + || state == CSMWorld::RecordBase::State_Deleted) { - writer.startRecord (record.sRecordId); - record.save (writer, state == CSMWorld::RecordBase::State_Deleted); - writer.endRecord (record.sRecordId); + writer.startRecord(record.sRecordId, record.mRecordFlags); + record.save(writer, state == CSMWorld::RecordBase::State_Deleted); + writer.endRecord(record.sRecordId); } } - class WriteDialogueCollectionStage : public Stage { - SavingState& mState; - const CSMWorld::IdCollection& mTopics; - CSMWorld::InfoCollection& mInfos; - - public: + SavingState& mState; + const CSMWorld::IdCollection& mTopics; + CSMWorld::InfoCollection& mInfos; + CSMWorld::InfosRecordPtrByTopic mInfosByTopic; - WriteDialogueCollectionStage (Document& document, SavingState& state, bool journal); + public: + WriteDialogueCollectionStage(Document& document, SavingState& state, bool journal); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; - class WriteRefIdCollectionStage : public Stage { - Document& mDocument; - SavingState& mState; - - public: + Document& mDocument; + SavingState& mState; - WriteRefIdCollectionStage (Document& document, SavingState& state); + public: + WriteRefIdCollectionStage(Document& document, SavingState& state); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; - class CollectionReferencesStage : public Stage { - Document& mDocument; - SavingState& mState; + Document& mDocument; + SavingState& mState; - public: + public: + CollectionReferencesStage(Document& document, SavingState& state); - CollectionReferencesStage (Document& document, SavingState& state); + int setup() override; + ///< \return number of steps - int setup() override; - ///< \return number of steps - - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; class WriteCellCollectionStage : public Stage { - Document& mDocument; - SavingState& mState; + Document& mDocument; + SavingState& mState; - public: + void writeReferences(const std::deque& references, bool interior, unsigned int& newRefNum); - WriteCellCollectionStage (Document& document, SavingState& state); + public: + WriteCellCollectionStage(Document& document, SavingState& state); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; - class WritePathgridCollectionStage : public Stage { - Document& mDocument; - SavingState& mState; - - public: + Document& mDocument; + SavingState& mState; - WritePathgridCollectionStage (Document& document, SavingState& state); + public: + WritePathgridCollectionStage(Document& document, SavingState& state); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; - class WriteLandCollectionStage : public Stage { - Document& mDocument; - SavingState& mState; - - public: + Document& mDocument; + SavingState& mState; - WriteLandCollectionStage (Document& document, SavingState& state); + public: + WriteLandCollectionStage(Document& document, SavingState& state); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; - class WriteLandTextureCollectionStage : public Stage { - Document& mDocument; - SavingState& mState; + Document& mDocument; + SavingState& mState; - public: + public: + WriteLandTextureCollectionStage(Document& document, SavingState& state); - WriteLandTextureCollectionStage (Document& document, SavingState& state); + int setup() override; + ///< \return number of steps - int setup() override; - ///< \return number of steps - - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; class CloseSaveStage : public Stage { - SavingState& mState; - - public: + SavingState& mState; - CloseSaveStage (SavingState& state); + public: + CloseSaveStage(SavingState& state); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; class FinalSavingStage : public Stage { - Document& mDocument; - SavingState& mState; - - public: + Document& mDocument; + SavingState& mState; - FinalSavingStage (Document& document, SavingState& state); + public: + FinalSavingStage(Document& document, SavingState& state); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; } diff --git a/apps/opencs/model/doc/savingstate.cpp b/apps/opencs/model/doc/savingstate.cpp index 7214735f073..aec2d0fd9b9 100644 --- a/apps/opencs/model/doc/savingstate.cpp +++ b/apps/opencs/model/doc/savingstate.cpp @@ -1,15 +1,18 @@ #include "savingstate.hpp" -#include +#include +#include -#include "operation.hpp" #include "document.hpp" +#include "operation.hpp" -CSMDoc::SavingState::SavingState (Operation& operation, const boost::filesystem::path& projectPath, - ToUTF8::FromType encoding) -: mOperation (operation), mEncoder (encoding), mProjectPath (projectPath), mProjectFile (false) +CSMDoc::SavingState::SavingState(Operation& operation, std::filesystem::path projectPath, ToUTF8::FromType encoding) + : mOperation(operation) + , mEncoder(encoding) + , mProjectPath(std::move(projectPath)) + , mProjectFile(false) { - mWriter.setEncoder (&mEncoder); + mWriter.setEncoder(&mEncoder); } bool CSMDoc::SavingState::hasError() const @@ -17,7 +20,7 @@ bool CSMDoc::SavingState::hasError() const return mOperation.hasError(); } -void CSMDoc::SavingState::start (Document& document, bool project) +void CSMDoc::SavingState::start(Document& document, bool project) { mProjectFile = project; @@ -33,24 +36,24 @@ void CSMDoc::SavingState::start (Document& document, bool project) else mPath = document.getSavePath(); - boost::filesystem::path file (mPath.filename().string() + ".tmp"); + std::filesystem::path file(mPath.filename().u8string() + u8".tmp"); mTmpPath = mPath.parent_path(); mTmpPath /= file; } -const boost::filesystem::path& CSMDoc::SavingState::getPath() const +const std::filesystem::path& CSMDoc::SavingState::getPath() const { return mPath; } -const boost::filesystem::path& CSMDoc::SavingState::getTmpPath() const +const std::filesystem::path& CSMDoc::SavingState::getTmpPath() const { return mTmpPath; } -boost::filesystem::ofstream& CSMDoc::SavingState::getStream() +std::ofstream& CSMDoc::SavingState::getStream() { return mStream; } @@ -65,7 +68,20 @@ bool CSMDoc::SavingState::isProjectFile() const return mProjectFile; } -std::map >& CSMDoc::SavingState::getSubRecords() +const std::deque* CSMDoc::SavingState::findSubRecord(const ESM::RefId& refId) const +{ + const auto it = mSubRecords.find(refId); + if (it == mSubRecords.end()) + return nullptr; + return &it->second; +} + +std::deque& CSMDoc::SavingState::getOrInsertSubRecord(const ESM::RefId& refId) { - return mSubRecords; + return mSubRecords[refId]; +} + +void CSMDoc::SavingState::clearSubRecords() +{ + mSubRecords.clear(); } diff --git a/apps/opencs/model/doc/savingstate.hpp b/apps/opencs/model/doc/savingstate.hpp index e6c8c545a7d..c42dff0366e 100644 --- a/apps/opencs/model/doc/savingstate.hpp +++ b/apps/opencs/model/doc/savingstate.hpp @@ -1,15 +1,14 @@ #ifndef CSM_DOC_SAVINGSTATE_H #define CSM_DOC_SAVINGSTATE_H +#include +#include #include #include -#include - -#include -#include - -#include +#include +#include +#include #include namespace CSMDoc @@ -19,40 +18,41 @@ namespace CSMDoc class SavingState { - Operation& mOperation; - boost::filesystem::path mPath; - boost::filesystem::path mTmpPath; - ToUTF8::Utf8Encoder mEncoder; - boost::filesystem::ofstream mStream; - ESM::ESMWriter mWriter; - boost::filesystem::path mProjectPath; - bool mProjectFile; - std::map > mSubRecords; // record ID, list of subrecords + Operation& mOperation; + std::filesystem::path mPath; + std::filesystem::path mTmpPath; + ToUTF8::Utf8Encoder mEncoder; + std::ofstream mStream; + ESM::ESMWriter mWriter; + std::filesystem::path mProjectPath; + bool mProjectFile; + std::map> mSubRecords; // record ID, list of subrecords - public: + public: + SavingState(Operation& operation, std::filesystem::path projectPath, ToUTF8::FromType encoding); - SavingState (Operation& operation, const boost::filesystem::path& projectPath, - ToUTF8::FromType encoding); + bool hasError() const; - bool hasError() const; + void start(Document& document, bool project); + ///< \param project Save project file instead of content file. - void start (Document& document, bool project); - ///< \param project Save project file instead of content file. + const std::filesystem::path& getPath() const; - const boost::filesystem::path& getPath() const; + const std::filesystem::path& getTmpPath() const; - const boost::filesystem::path& getTmpPath() const; + std::ofstream& getStream(); - boost::filesystem::ofstream& getStream(); + ESM::ESMWriter& getWriter(); - ESM::ESMWriter& getWriter(); + bool isProjectFile() const; + ///< Currently saving project file? (instead of content file) - bool isProjectFile() const; - ///< Currently saving project file? (instead of content file) + const std::deque* findSubRecord(const ESM::RefId& refId) const; - std::map >& getSubRecords(); - }; + std::deque& getOrInsertSubRecord(const ESM::RefId& refId); + void clearSubRecords(); + }; } diff --git a/apps/opencs/model/doc/stage.cpp b/apps/opencs/model/doc/stage.cpp deleted file mode 100644 index 3c8c107ba00..00000000000 --- a/apps/opencs/model/doc/stage.cpp +++ /dev/null @@ -1,3 +0,0 @@ -#include "stage.hpp" - -CSMDoc::Stage::~Stage() {} diff --git a/apps/opencs/model/doc/stage.hpp b/apps/opencs/model/doc/stage.hpp index 1eae27a982d..2f1d0fdbd55 100644 --- a/apps/opencs/model/doc/stage.hpp +++ b/apps/opencs/model/doc/stage.hpp @@ -1,28 +1,19 @@ #ifndef CSM_DOC_STAGE_H #define CSM_DOC_STAGE_H -#include -#include - -#include "../world/universalid.hpp" - -#include "messages.hpp" - -class QString; - namespace CSMDoc { + class Messages; class Stage { - public: - - virtual ~Stage(); + public: + virtual ~Stage() = default; - virtual int setup() = 0; - ///< \return number of steps + virtual int setup() = 0; + ///< \return number of steps - virtual void perform (int stage, Messages& messages) = 0; - ///< Messages resulting from this stage will be appended to \a messages. + virtual void perform(int stage, Messages& messages) = 0; + ///< Messages resulting from this stage will be appended to \a messages. }; } diff --git a/apps/opencs/model/doc/state.hpp b/apps/opencs/model/doc/state.hpp index 7352b4b9995..9d53fa816b7 100644 --- a/apps/opencs/model/doc/state.hpp +++ b/apps/opencs/model/doc/state.hpp @@ -14,7 +14,7 @@ namespace CSMDoc State_Verifying = 32, State_Merging = 64, State_Searching = 128, - State_Loading = 256 // pseudo-state; can not be encountered in a loaded document + State_Loading = 256 // pseudo-state; can not be encountered in a loaded document }; } diff --git a/apps/opencs/model/filter/andnode.cpp b/apps/opencs/model/filter/andnode.cpp index 75786571788..1fbf1743507 100644 --- a/apps/opencs/model/filter/andnode.cpp +++ b/apps/opencs/model/filter/andnode.cpp @@ -1,18 +1,19 @@ #include "andnode.hpp" -#include +#include +#include -CSMFilter::AndNode::AndNode (const std::vector >& nodes) -: NAryNode (nodes, "and") -{} +CSMFilter::AndNode::AndNode(const std::vector>& nodes) + : NAryNode(nodes, "and") +{ +} -bool CSMFilter::AndNode::test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const +bool CSMFilter::AndNode::test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { int size = getSize(); - for (int i=0; i +#include +#include + +namespace CSMWorld +{ + class IdTableBase; +} + namespace CSMFilter { + class Node; class AndNode : public NAryNode { - public: - - AndNode (const std::vector >& nodes); + public: + AndNode(const std::vector>& nodes); - bool test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const override; - ///< \return Can the specified table row pass through to filter? - /// \param columns column ID to column index mapping + bool test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping }; } diff --git a/apps/opencs/model/filter/booleannode.cpp b/apps/opencs/model/filter/booleannode.cpp index ee7ddc1c056..793709b8f55 100644 --- a/apps/opencs/model/filter/booleannode.cpp +++ b/apps/opencs/model/filter/booleannode.cpp @@ -1,14 +1,21 @@ #include "booleannode.hpp" -CSMFilter::BooleanNode::BooleanNode (bool true_) : mTrue (true_) {} +namespace CSMWorld +{ + class IdTableBase; +} + +CSMFilter::BooleanNode::BooleanNode(bool true_) + : mTrue(true_) +{ +} -bool CSMFilter::BooleanNode::test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const +bool CSMFilter::BooleanNode::test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { return mTrue; } -std::string CSMFilter::BooleanNode::toString (bool numericColumns) const +std::string CSMFilter::BooleanNode::toString(bool numericColumns) const { return mTrue ? "true" : "false"; } diff --git a/apps/opencs/model/filter/booleannode.hpp b/apps/opencs/model/filter/booleannode.hpp index bed6cbeb0cd..331899553e9 100644 --- a/apps/opencs/model/filter/booleannode.hpp +++ b/apps/opencs/model/filter/booleannode.hpp @@ -1,28 +1,33 @@ #ifndef CSM_FILTER_BOOLEANNODE_H #define CSM_FILTER_BOOLEANNODE_H +#include +#include + #include "leafnode.hpp" +namespace CSMWorld +{ + class IdTableBase; +} + namespace CSMFilter { class BooleanNode : public LeafNode { - bool mTrue; - - public: - - BooleanNode (bool true_); + bool mTrue; - bool test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const override; - ///< \return Can the specified table row pass through to filter? - /// \param columns column ID to column index mapping + public: + BooleanNode(bool true_); - std::string toString (bool numericColumns) const override; - ///< Return a string that represents this node. - /// - /// \param numericColumns Use numeric IDs instead of string to represent columns. + bool test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + std::string toString(bool numericColumns) const override; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } diff --git a/apps/opencs/model/filter/leafnode.cpp b/apps/opencs/model/filter/leafnode.cpp index 6745e165ecc..fd37c6cb64a 100644 --- a/apps/opencs/model/filter/leafnode.cpp +++ b/apps/opencs/model/filter/leafnode.cpp @@ -4,4 +4,3 @@ std::vector CSMFilter::LeafNode::getReferencedColumns() const { return std::vector(); } - diff --git a/apps/opencs/model/filter/leafnode.hpp b/apps/opencs/model/filter/leafnode.hpp index d27bdcfe844..bf90da18b0e 100644 --- a/apps/opencs/model/filter/leafnode.hpp +++ b/apps/opencs/model/filter/leafnode.hpp @@ -1,7 +1,7 @@ #ifndef CSM_FILTER_LEAFNODE_H #define CSM_FILTER_LEAFNODE_H -#include +#include #include "node.hpp" @@ -9,11 +9,10 @@ namespace CSMFilter { class LeafNode : public Node { - public: - - std::vector getReferencedColumns() const override; - ///< Return a list of the IDs of the columns referenced by this node. The column mapping - /// passed into test as columns must contain all columns listed here. + public: + std::vector getReferencedColumns() const override; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. }; } diff --git a/apps/opencs/model/filter/narynode.cpp b/apps/opencs/model/filter/narynode.cpp index 9415f1daff1..baa5eb788c1 100644 --- a/apps/opencs/model/filter/narynode.cpp +++ b/apps/opencs/model/filter/narynode.cpp @@ -1,38 +1,41 @@ #include "narynode.hpp" +#include #include -CSMFilter::NAryNode::NAryNode (const std::vector >& nodes, - const std::string& name) -: mNodes (nodes), mName (name) -{} +#include + +CSMFilter::NAryNode::NAryNode(const std::vector>& nodes, const std::string& name) + : mNodes(nodes) + , mName(name) +{ +} int CSMFilter::NAryNode::getSize() const { return static_cast(mNodes.size()); } -const CSMFilter::Node& CSMFilter::NAryNode::operator[] (int index) const +const CSMFilter::Node& CSMFilter::NAryNode::operator[](int index) const { - return *mNodes.at (index); + return *mNodes.at(index); } std::vector CSMFilter::NAryNode::getReferencedColumns() const { std::vector columns; - for (std::vector >::const_iterator iter (mNodes.begin()); - iter!=mNodes.end(); ++iter) + for (std::vector>::const_iterator iter(mNodes.begin()); iter != mNodes.end(); ++iter) { std::vector columns2 = (*iter)->getReferencedColumns(); - columns.insert (columns.end(), columns2.begin(), columns2.end()); + columns.insert(columns.end(), columns2.begin(), columns2.end()); } return columns; } -std::string CSMFilter::NAryNode::toString (bool numericColumns) const +std::string CSMFilter::NAryNode::toString(bool numericColumns) const { std::ostringstream stream; @@ -41,19 +44,17 @@ std::string CSMFilter::NAryNode::toString (bool numericColumns) const bool first = true; int size = getSize(); - for (int i=0; i +#include #include +#include #include "node.hpp" @@ -10,25 +11,24 @@ namespace CSMFilter { class NAryNode : public Node { - std::vector > mNodes; - std::string mName; - - public: + std::vector> mNodes; + std::string mName; - NAryNode (const std::vector >& nodes, const std::string& name); + public: + NAryNode(const std::vector>& nodes, const std::string& name); - int getSize() const; + int getSize() const; - const Node& operator[] (int index) const; + const Node& operator[](int index) const; - std::vector getReferencedColumns() const override; - ///< Return a list of the IDs of the columns referenced by this node. The column mapping - /// passed into test as columns must contain all columns listed here. + std::vector getReferencedColumns() const override; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. - std::string toString (bool numericColumns) const override; - ///< Return a string that represents this node. - /// - /// \param numericColumns Use numeric IDs instead of string to represent columns. + std::string toString(bool numericColumns) const override; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } diff --git a/apps/opencs/model/filter/node.cpp b/apps/opencs/model/filter/node.cpp deleted file mode 100644 index e25610675ec..00000000000 --- a/apps/opencs/model/filter/node.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "node.hpp" - -CSMFilter::Node::Node() {} - -CSMFilter::Node::~Node() {} diff --git a/apps/opencs/model/filter/node.hpp b/apps/opencs/model/filter/node.hpp index 7295b9018a7..b143d238d2c 100644 --- a/apps/opencs/model/filter/node.hpp +++ b/apps/opencs/model/filter/node.hpp @@ -1,9 +1,9 @@ #ifndef CSM_FILTER_NODE_H #define CSM_FILTER_NODE_H -#include #include #include +#include #include #include @@ -21,32 +21,27 @@ namespace CSMFilter /// interpreted as "the node and all its children". class Node { - // not implemented - Node (const Node&); - Node& operator= (const Node&); - - public: - - Node(); - - virtual ~Node(); - - virtual bool test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const = 0; - ///< \return Can the specified table row pass through to filter? - /// \param columns column ID to column index mapping - - virtual std::vector getReferencedColumns() const = 0; - ///< Return a list of the IDs of the columns referenced by this node. The column mapping - /// passed into test as columns must contain all columns listed here. - - virtual std::string toString (bool numericColumns) const = 0; - ///< Return a string that represents this node. - /// - /// \param numericColumns Use numeric IDs instead of string to represent columns. + public: + Node() = default; + Node(const Node&) = delete; + Node& operator=(const Node&) = delete; + virtual ~Node() = default; + + virtual bool test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const = 0; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + + virtual std::vector getReferencedColumns() const = 0; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual std::string toString(bool numericColumns) const = 0; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } -Q_DECLARE_METATYPE (std::shared_ptr) +Q_DECLARE_METATYPE(std::shared_ptr) #endif diff --git a/apps/opencs/model/filter/notnode.cpp b/apps/opencs/model/filter/notnode.cpp index 81588c75472..ad86fff80cd 100644 --- a/apps/opencs/model/filter/notnode.cpp +++ b/apps/opencs/model/filter/notnode.cpp @@ -1,9 +1,19 @@ #include "notnode.hpp" -CSMFilter::NotNode::NotNode (std::shared_ptr child) : UnaryNode (child, "not") {} +#include +#include -bool CSMFilter::NotNode::test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const +namespace CSMWorld { - return !getChild().test (table, row, columns); + class IdTableBase; +} + +CSMFilter::NotNode::NotNode(std::shared_ptr child) + : UnaryNode(std::move(child), "not") +{ +} + +bool CSMFilter::NotNode::test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const +{ + return !getChild().test(table, row, columns); } diff --git a/apps/opencs/model/filter/notnode.hpp b/apps/opencs/model/filter/notnode.hpp index 5b15163d97f..955a19f7bc1 100644 --- a/apps/opencs/model/filter/notnode.hpp +++ b/apps/opencs/model/filter/notnode.hpp @@ -1,20 +1,27 @@ #ifndef CSM_FILTER_NOTNODE_H #define CSM_FILTER_NOTNODE_H +#include +#include + #include "unarynode.hpp" +namespace CSMWorld +{ + class IdTableBase; +} + namespace CSMFilter { + class Node; class NotNode : public UnaryNode { - public: - - NotNode (std::shared_ptr child); + public: + NotNode(std::shared_ptr child); - bool test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const override; - ///< \return Can the specified table row pass through to filter? - /// \param columns column ID to column index mapping + bool test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping }; } diff --git a/apps/opencs/model/filter/ornode.cpp b/apps/opencs/model/filter/ornode.cpp index 9e6d8b2c46b..ed17b88a56c 100644 --- a/apps/opencs/model/filter/ornode.cpp +++ b/apps/opencs/model/filter/ornode.cpp @@ -1,18 +1,24 @@ #include "ornode.hpp" -#include +#include +#include -CSMFilter::OrNode::OrNode (const std::vector >& nodes) -: NAryNode (nodes, "or") -{} +namespace CSMWorld +{ + class IdTableBase; +} + +CSMFilter::OrNode::OrNode(const std::vector>& nodes) + : NAryNode(nodes, "or") +{ +} -bool CSMFilter::OrNode::test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const +bool CSMFilter::OrNode::test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { int size = getSize(); - for (int i=0; i +#include +#include + +namespace CSMWorld +{ + class IdTableBase; +} + namespace CSMFilter { + class Node; class OrNode : public NAryNode { - public: - - OrNode (const std::vector >& nodes); + public: + OrNode(const std::vector>& nodes); - bool test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const override; - ///< \return Can the specified table row pass through to filter? - /// \param columns column ID to column index mapping + bool test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping }; } diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index d363b4849db..6248b03bb41 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -1,22 +1,40 @@ #include "parser.hpp" +#include #include -#include #include +#include +#include + +#include +#include -#include +#include +#include #include "../world/columns.hpp" #include "../world/data.hpp" -#include "../world/idcollection.hpp" -#include "booleannode.hpp" -#include "ornode.hpp" #include "andnode.hpp" +#include "booleannode.hpp" #include "notnode.hpp" +#include "ornode.hpp" #include "textnode.hpp" #include "valuenode.hpp" +namespace +{ + bool isAlpha(char c) + { + return std::isalpha(static_cast(c)); + } + + bool isDigit(char c) + { + return std::isdigit(static_cast(c)); + } +} + namespace CSMFilter { struct Token @@ -46,49 +64,70 @@ namespace CSMFilter std::string mString; double mNumber; - Token (Type type = Type_None); + Token(Type type = Type_None); - Token (Type type, const std::string& string); + Token(Type type, const std::string& string); ///< Non-string type that can also be interpreted as a string. - Token (const std::string& string); + Token(const std::string& string); - Token (double number); + Token(double number); operator bool() const; bool isString() const; }; - Token::Token (Type type) : mType (type), mNumber(0.0) {} + Token::Token(Type type) + : mType(type) + , mNumber(0.0) + { + } - Token::Token (Type type, const std::string& string) : mType (type), mString (string), mNumber(0.0) {} + Token::Token(Type type, const std::string& string) + : mType(type) + , mString(string) + , mNumber(0.0) + { + } - Token::Token (const std::string& string) : mType (Type_String), mString (string), mNumber(0.0) {} + Token::Token(const std::string& string) + : mType(Type_String) + , mString(string) + , mNumber(0.0) + { + } - Token::Token (double number) : mType (Type_Number), mNumber (number) {} + Token::Token(double number) + : mType(Type_Number) + , mNumber(number) + { + } bool Token::isString() const { - return mType==Type_String || mType>=Type_Keyword_True; + return mType == Type_String || mType >= Type_Keyword_True; } Token::operator bool() const { - return mType!=Type_None; + return mType != Type_None; } - bool operator== (const Token& left, const Token& right) + bool operator==(const Token& left, const Token& right) { - if (left.mType!=right.mType) + if (left.mType != right.mType) return false; switch (left.mType) { - case Token::Type_String: return left.mString==right.mString; - case Token::Type_Number: return left.mNumber==right.mNumber; + case Token::Type_String: + return left.mString == right.mString; + case Token::Type_Number: + return left.mNumber == right.mNumber; - default: return true; + default: + return true; } } } @@ -97,19 +136,19 @@ CSMFilter::Token CSMFilter::Parser::getStringToken() { std::string string; - int size = static_cast (mInput.size()); + int size = static_cast(mInput.size()); - for (; mIndex1) + if (c == '"' && string.size() > 1) { ++mIndex; break; @@ -118,49 +157,49 @@ CSMFilter::Token CSMFilter::Parser::getStringToken() if (!string.empty()) { - if (string[0]=='"' && (string[string.size()-1]!='"' || string.size()<2) ) + if (string[0] == '"' && (string[string.size() - 1] != '"' || string.size() < 2)) { error(); - return Token (Token::Type_None); + return Token(Token::Type_None); } - if (string[0]!='"' && string[string.size()-1]=='"') + if (string[0] != '"' && string[string.size() - 1] == '"') { error(); - return Token (Token::Type_None); + return Token(Token::Type_None); } - if (string[0]=='"') - return string.substr (1, string.size()-2); + if (string[0] == '"') + return string.substr(1, string.size() - 2); } - return checkKeywords (string); + return checkKeywords(string); } CSMFilter::Token CSMFilter::Parser::getNumberToken() { std::string string; - int size = static_cast (mInput.size()); + int size = static_cast(mInput.size()); bool hasDecimalPoint = false; bool hasDigit = false; - for (; mIndex> value; return value; } -CSMFilter::Token CSMFilter::Parser::checkKeywords (const Token& token) +CSMFilter::Token CSMFilter::Parser::checkKeywords(const Token& token) { - static const char *sKeywords[] = - { - "true", "false", - "and", "or", "not", - "string", "value", - 0 + static const char* sKeywords[] = { + "true", + "false", + "and", + "or", + "not", + "string", + "value", + nullptr, }; - std::string string = Misc::StringUtils::lowerCase (token.mString); + std::string string = Misc::StringUtils::lowerCase(token.mString); - for (int i=0; sKeywords[i]; ++i) - if (sKeywords[i]==string || (string.size()==1 && sKeywords[i][0]==string[0])) - return Token (static_cast (i+Token::Type_Keyword_True), token.mString); + for (int i = 0; sKeywords[i]; ++i) + if (sKeywords[i] == string || (string.size() == 1 && sKeywords[i][0] == string[0])) + return Token(static_cast(i + Token::Type_Keyword_True), token.mString); return token; } CSMFilter::Token CSMFilter::Parser::getNextToken() { - int size = static_cast (mInput.size()); + int size = static_cast(mInput.size()); char c = 0; - for (; mIndex=size) - return Token (Token::Type_EOS); + if (mIndex >= size) + return Token(Token::Type_EOS); switch (c) { - case '(': ++mIndex; return Token (Token::Type_Open); - case ')': ++mIndex; return Token (Token::Type_Close); - case '[': ++mIndex; return Token (Token::Type_OpenSquare); - case ']': ++mIndex; return Token (Token::Type_CloseSquare); - case ',': ++mIndex; return Token (Token::Type_Comma); - case '!': ++mIndex; return Token (Token::Type_OneShot); + case '(': + ++mIndex; + return Token(Token::Type_Open); + case ')': + ++mIndex; + return Token(Token::Type_Close); + case '[': + ++mIndex; + return Token(Token::Type_OpenSquare); + case ']': + ++mIndex; + return Token(Token::Type_CloseSquare); + case ',': + ++mIndex; + return Token(Token::Type_Comma); + case '!': + ++mIndex; + return Token(Token::Type_OneShot); } - if (c=='"' || c=='_' || std::isalpha (c) || c==':') + if (c == '"' || c == '_' || isAlpha(c) || c == ':') return getStringToken(); - if (c=='-' || c=='.' || std::isdigit (c)) + if (c == '-' || c == '.' || isDigit(c)) return getNumberToken(); error(); - return Token (Token::Type_None); + return Token(Token::Type_None); } -std::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty, bool ignoreOneShot) +std::shared_ptr CSMFilter::Parser::parseImp(bool allowEmpty, bool ignoreOneShot) { if (Token token = getNextToken()) { - if (token==Token (Token::Type_OneShot)) + if (token == Token(Token::Type_OneShot)) token = getNextToken(); if (token) @@ -247,16 +301,16 @@ std::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty, b { case Token::Type_Keyword_True: - return std::shared_ptr (new BooleanNode (true)); + return std::make_shared(true); case Token::Type_Keyword_False: - return std::shared_ptr (new BooleanNode (false)); + return std::make_shared(false); case Token::Type_Keyword_And: case Token::Type_Keyword_Or: - return parseNAry (token); + return parseNAry(token); case Token::Type_Keyword_Not: { @@ -265,7 +319,7 @@ std::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty, b if (mError) return std::shared_ptr(); - return std::shared_ptr (new NotNode (node)); + return std::make_shared(node); } case Token::Type_Keyword_Text: @@ -292,13 +346,13 @@ std::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty, b return std::shared_ptr(); } -std::shared_ptr CSMFilter::Parser::parseNAry (const Token& keyword) +std::shared_ptr CSMFilter::Parser::parseNAry(const Token& keyword) { - std::vector > nodes; + std::vector> nodes; Token token = getNextToken(); - if (token.mType!=Token::Type_Open) + if (token.mType != Token::Type_Open) { error(); return std::shared_ptr(); @@ -311,25 +365,29 @@ std::shared_ptr CSMFilter::Parser::parseNAry (const Token& keyw if (mError) return std::shared_ptr(); - nodes.push_back (node); + nodes.push_back(node); token = getNextToken(); - if (!token || (token.mType!=Token::Type_Close && token.mType!=Token::Type_Comma)) + if (!token || (token.mType != Token::Type_Close && token.mType != Token::Type_Comma)) { error(); return std::shared_ptr(); } - if (token.mType==Token::Type_Close) + if (token.mType == Token::Type_Close) break; } switch (keyword.mType) { - case Token::Type_Keyword_And: return std::shared_ptr (new AndNode (nodes)); - case Token::Type_Keyword_Or: return std::shared_ptr (new OrNode (nodes)); - default: error(); return std::shared_ptr(); + case Token::Type_Keyword_And: + return std::make_shared(nodes); + case Token::Type_Keyword_Or: + return std::make_shared(nodes); + default: + error(); + return std::shared_ptr(); } } @@ -337,7 +395,7 @@ std::shared_ptr CSMFilter::Parser::parseText() { Token token = getNextToken(); - if (token.mType!=Token::Type_Open) + if (token.mType != Token::Type_Open) { error(); return std::shared_ptr(); @@ -351,17 +409,17 @@ std::shared_ptr CSMFilter::Parser::parseText() // parse column ID int columnId = -1; - if (token.mType==Token::Type_Number) + if (token.mType == Token::Type_Number) { - if (static_cast (token.mNumber)==token.mNumber) - columnId = static_cast (token.mNumber); + if (static_cast(token.mNumber) == token.mNumber) + columnId = static_cast(token.mNumber); } else if (token.isString()) { - columnId = CSMWorld::Columns::getId (token.mString); + columnId = CSMWorld::Columns::getId(token.mString); } - if (columnId<0) + if (columnId < 0) { error(); return std::shared_ptr(); @@ -369,7 +427,7 @@ std::shared_ptr CSMFilter::Parser::parseText() token = getNextToken(); - if (token.mType!=Token::Type_Comma) + if (token.mType != Token::Type_Comma) { error(); return std::shared_ptr(); @@ -388,20 +446,23 @@ std::shared_ptr CSMFilter::Parser::parseText() token = getNextToken(); - if (token.mType!=Token::Type_Close) + if (token.mType != Token::Type_Close) { error(); return std::shared_ptr(); } - return std::shared_ptr (new TextNode (columnId, text)); + auto node = std::make_shared(columnId, text); + if (!node->isValid()) + error(); + return node; } std::shared_ptr CSMFilter::Parser::parseValue() { Token token = getNextToken(); - if (token.mType!=Token::Type_Open) + if (token.mType != Token::Type_Open) { error(); return std::shared_ptr(); @@ -415,17 +476,17 @@ std::shared_ptr CSMFilter::Parser::parseValue() // parse column ID int columnId = -1; - if (token.mType==Token::Type_Number) + if (token.mType == Token::Type_Number) { - if (static_cast (token.mNumber)==token.mNumber) - columnId = static_cast (token.mNumber); + if (static_cast(token.mNumber) == token.mNumber) + columnId = static_cast(token.mNumber); } else if (token.isString()) { - columnId = CSMWorld::Columns::getId (token.mString); + columnId = CSMWorld::Columns::getId(token.mString); } - if (columnId<0) + if (columnId < 0) { error(); return std::shared_ptr(); @@ -433,7 +494,7 @@ std::shared_ptr CSMFilter::Parser::parseValue() token = getNextToken(); - if (token.mType!=Token::Type_Comma) + if (token.mType != Token::Type_Comma) { error(); return std::shared_ptr(); @@ -447,7 +508,7 @@ std::shared_ptr CSMFilter::Parser::parseValue() token = getNextToken(); - if (token.mType==Token::Type_Number) + if (token.mType == Token::Type_Number) { // single value lower = upper = token.mNumber; @@ -456,9 +517,9 @@ std::shared_ptr CSMFilter::Parser::parseValue() else { // interval - if (token.mType==Token::Type_OpenSquare) + if (token.mType == Token::Type_OpenSquare) lowerType = ValueNode::Type_Closed; - else if (token.mType!=Token::Type_CloseSquare && token.mType!=Token::Type_Open) + else if (token.mType != Token::Type_CloseSquare && token.mType != Token::Type_Open) { error(); return std::shared_ptr(); @@ -466,19 +527,19 @@ std::shared_ptr CSMFilter::Parser::parseValue() token = getNextToken(); - if (token.mType==Token::Type_Number) + if (token.mType == Token::Type_Number) { lower = token.mNumber; token = getNextToken(); - if (token.mType!=Token::Type_Comma) + if (token.mType != Token::Type_Comma) { error(); return std::shared_ptr(); } } - else if (token.mType==Token::Type_Comma) + else if (token.mType == Token::Type_Comma) { lowerType = ValueNode::Type_Infinite; } @@ -490,7 +551,7 @@ std::shared_ptr CSMFilter::Parser::parseValue() token = getNextToken(); - if (token.mType==Token::Type_Number) + if (token.mType == Token::Type_Number) { upper = token.mNumber; @@ -499,12 +560,12 @@ std::shared_ptr CSMFilter::Parser::parseValue() else upperType = ValueNode::Type_Infinite; - if (token.mType==Token::Type_CloseSquare) + if (token.mType == Token::Type_CloseSquare) { - if (upperType!=ValueNode::Type_Infinite) + if (upperType != ValueNode::Type_Infinite) upperType = ValueNode::Type_Closed; } - else if (token.mType!=Token::Type_OpenSquare && token.mType!=Token::Type_Close) + else if (token.mType != Token::Type_OpenSquare && token.mType != Token::Type_Close) { error(); return std::shared_ptr(); @@ -513,13 +574,13 @@ std::shared_ptr CSMFilter::Parser::parseValue() token = getNextToken(); - if (token.mType!=Token::Type_Close) + if (token.mType != Token::Type_Close) { error(); return std::shared_ptr(); } - return std::shared_ptr (new ValueNode (columnId, lowerType, upperType, lower, upper)); + return std::make_shared(columnId, lowerType, upperType, lower, upper); } void CSMFilter::Parser::error() @@ -527,10 +588,14 @@ void CSMFilter::Parser::error() mError = true; } -CSMFilter::Parser::Parser (const CSMWorld::Data& data) -: mIndex (0), mError (false), mData (data) {} +CSMFilter::Parser::Parser(const CSMWorld::Data& data) + : mIndex(0) + , mError(false) + , mData(data) +{ +} -bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) +bool CSMFilter::Parser::parse(const std::string& filter, bool allowPredefined) { // reset mFilter.reset(); @@ -543,53 +608,53 @@ bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) if (allowPredefined) token = getNextToken(); - if (allowPredefined && token==Token (Token::Type_EOS)) + if (allowPredefined && token == Token(Token::Type_EOS)) { mFilter.reset(); return true; } - else if (!allowPredefined || token==Token (Token::Type_OneShot)) + else if (!allowPredefined || token == Token(Token::Type_OneShot)) { - std::shared_ptr node = parseImp (true, token!=Token (Token::Type_OneShot)); + std::shared_ptr node = parseImp(true, token != Token(Token::Type_OneShot)); if (mError) return false; - if (getNextToken()!=Token (Token::Type_EOS)) + if (getNextToken() != Token(Token::Type_EOS)) { error(); return false; } if (node) - mFilter = node; + mFilter = std::move(node); else { // Empty filter string equals to filter "true". - mFilter.reset (new BooleanNode (true)); + mFilter = std::make_shared(true); } return true; } // We do not use isString() here, because there could be a pre-defined filter with an ID that is // equal a filter keyword. - else if (token.mType==Token::Type_String) + else if (token.mType == Token::Type_String) { - if (getNextToken()!=Token (Token::Type_EOS)) + if (getNextToken() != Token(Token::Type_EOS)) { error(); return false; } - int index = mData.getFilters().searchId (token.mString); + const int index = mData.getFilters().searchId(ESM::RefId::stringRefId(token.mString)); - if (index==-1) + if (index == -1) { error(); return false; } - const CSMWorld::Record& record = mData.getFilters().getRecord (index); + const CSMWorld::Record& record = mData.getFilters().getRecord(index); if (record.isDeleted()) { @@ -597,7 +662,7 @@ bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) return false; } - return parse (record.get().mFilter, false); + return parse(record.get().mFilter, false); } else { @@ -609,7 +674,7 @@ bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) std::shared_ptr CSMFilter::Parser::getFilter() const { if (mError) - throw std::logic_error ("No filter available"); + throw std::logic_error("No filter available"); return mFilter; } diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp index 344c552eff4..d5e3f3aed39 100644 --- a/apps/opencs/model/filter/parser.hpp +++ b/apps/opencs/model/filter/parser.hpp @@ -1,7 +1,13 @@ #ifndef CSM_FILTER_PARSER_H #define CSM_FILTER_PARSER_H -#include "node.hpp" +#include +#include + +namespace CSMFilter +{ + class Node; +} namespace CSMWorld { @@ -14,43 +20,42 @@ namespace CSMFilter class Parser { - std::shared_ptr mFilter; - std::string mInput; - int mIndex; - bool mError; - const CSMWorld::Data& mData; - - Token getStringToken(); + std::shared_ptr mFilter; + std::string mInput; + int mIndex; + bool mError; + const CSMWorld::Data& mData; - Token getNumberToken(); + Token getStringToken(); - Token getNextToken(); + Token getNumberToken(); - Token checkKeywords (const Token& token); - ///< Turn string token into keyword token, if possible. + Token getNextToken(); - std::shared_ptr parseImp (bool allowEmpty = false, bool ignoreOneShot = false); - ///< Will return a null-pointer, if there is nothing more to parse. + Token checkKeywords(const Token& token); + ///< Turn string token into keyword token, if possible. - std::shared_ptr parseNAry (const Token& keyword); + std::shared_ptr parseImp(bool allowEmpty = false, bool ignoreOneShot = false); + ///< Will return a null-pointer, if there is nothing more to parse. - std::shared_ptr parseText(); + std::shared_ptr parseNAry(const Token& keyword); - std::shared_ptr parseValue(); + std::shared_ptr parseText(); - void error(); + std::shared_ptr parseValue(); - public: + void error(); - Parser (const CSMWorld::Data& data); + public: + Parser(const CSMWorld::Data& data); - bool parse (const std::string& filter, bool allowPredefined = true); - ///< Discards any previous calls to parse - /// - /// \return Success? + bool parse(const std::string& filter, bool allowPredefined = true); + ///< Discards any previous calls to parse + /// + /// \return Success? - std::shared_ptr getFilter() const; - ///< Throws an exception if the last call to parse did not return true. + std::shared_ptr getFilter() const; + ///< Throws an exception if the last call to parse did not return true. }; } diff --git a/apps/opencs/model/filter/textnode.cpp b/apps/opencs/model/filter/textnode.cpp index 808090dc378..7d837f9e545 100644 --- a/apps/opencs/model/filter/textnode.cpp +++ b/apps/opencs/model/filter/textnode.cpp @@ -1,50 +1,55 @@ #include "textnode.hpp" +#include #include #include +#include -#include +#include #include "../world/columns.hpp" #include "../world/idtablebase.hpp" -CSMFilter::TextNode::TextNode (int columnId, const std::string& text) -: mColumnId (columnId), mText (text) -{} +CSMFilter::TextNode::TextNode(int columnId, const std::string& text) + : mColumnId(columnId) + , mText(text) + , mRegExp(QRegularExpression::anchoredPattern(QString::fromUtf8(mText.c_str())), + QRegularExpression::CaseInsensitiveOption) +{ +} -bool CSMFilter::TextNode::test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const +bool CSMFilter::TextNode::test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { - const std::map::const_iterator iter = columns.find (mColumnId); + const std::map::const_iterator iter = columns.find(mColumnId); - if (iter==columns.end()) - throw std::logic_error ("invalid column in text node test"); + if (iter == columns.end()) + throw std::logic_error("invalid column in text node test"); - if (iter->second==-1) + if (iter->second == -1) return true; - QModelIndex index = table.index (row, iter->second); + QModelIndex index = table.index(row, iter->second); - QVariant data = table.data (index); + QVariant data = table.data(index); QString string; - if (data.type()==QVariant::String) + if (data.type() == QVariant::String) { string = data.toString(); } - else if ((data.type()==QVariant::Int || data.type()==QVariant::UInt) && - CSMWorld::Columns::hasEnums (static_cast (mColumnId))) + else if ((data.type() == QVariant::Int || data.type() == QVariant::UInt) + && CSMWorld::Columns::hasEnums(static_cast(mColumnId))) { int value = data.toInt(); - std::vector> enums = - CSMWorld::Columns::getEnums (static_cast (mColumnId)); + std::vector> enums + = CSMWorld::Columns::getEnums(static_cast(mColumnId)); - if (value>=0 && value (enums.size())) - string = QString::fromUtf8 (enums[value].second.c_str()); + if (value >= 0 && value < static_cast(enums.size())) + string = QString::fromUtf8(enums[value].second.c_str()); } - else if (data.type()==QVariant::Bool) + else if (data.type() == QVariant::Bool) { string = data.toBool() ? "true" : "false"; } @@ -54,17 +59,17 @@ bool CSMFilter::TextNode::test (const CSMWorld::IdTableBase& table, int row, return false; /// \todo make pattern syntax configurable - QRegExp regExp (QString::fromUtf8 (mText.c_str()), Qt::CaseInsensitive); + QRegularExpressionMatch match = mRegExp.match(string); - return regExp.exactMatch (string); + return match.hasMatch(); } std::vector CSMFilter::TextNode::getReferencedColumns() const { - return std::vector (1, mColumnId); + return std::vector(1, mColumnId); } -std::string CSMFilter::TextNode::toString (bool numericColumns) const +std::string CSMFilter::TextNode::toString(bool numericColumns) const { std::ostringstream stream; @@ -73,10 +78,7 @@ std::string CSMFilter::TextNode::toString (bool numericColumns) const if (numericColumns) stream << mColumnId; else - stream - << "\"" - << CSMWorld::Columns::getName (static_cast (mColumnId)) - << "\""; + stream << "\"" << CSMWorld::Columns::getName(static_cast(mColumnId)) << "\""; stream << ", \"" << mText << "\")"; diff --git a/apps/opencs/model/filter/textnode.hpp b/apps/opencs/model/filter/textnode.hpp index 0e7a0e4f5e4..d629cbe3368 100644 --- a/apps/opencs/model/filter/textnode.hpp +++ b/apps/opencs/model/filter/textnode.hpp @@ -1,32 +1,41 @@ #ifndef CSM_FILTER_TEXTNODE_H #define CSM_FILTER_TEXTNODE_H +#include +#include +#include + +#include + +#include + #include "leafnode.hpp" namespace CSMFilter { class TextNode : public LeafNode { - int mColumnId; - std::string mText; + int mColumnId; + std::string mText; + QRegularExpression mRegExp; - public: + public: + TextNode(int columnId, const std::string& text); - TextNode (int columnId, const std::string& text); + bool test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping - bool test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const override; - ///< \return Can the specified table row pass through to filter? - /// \param columns column ID to column index mapping + std::vector getReferencedColumns() const override; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. - std::vector getReferencedColumns() const override; - ///< Return a list of the IDs of the columns referenced by this node. The column mapping - /// passed into test as columns must contain all columns listed here. + std::string toString(bool numericColumns) const override; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. - std::string toString (bool numericColumns) const override; - ///< Return a string that represents this node. - /// - /// \param numericColumns Use numeric IDs instead of string to represent columns. + bool isValid() { return mRegExp.isValid(); } }; } diff --git a/apps/opencs/model/filter/unarynode.cpp b/apps/opencs/model/filter/unarynode.cpp index 7211f78b0a8..0fefab19925 100644 --- a/apps/opencs/model/filter/unarynode.cpp +++ b/apps/opencs/model/filter/unarynode.cpp @@ -1,8 +1,12 @@ #include "unarynode.hpp" -CSMFilter::UnaryNode::UnaryNode (std::shared_ptr child, const std::string& name) -: mChild (child), mName (name) -{} +#include + +CSMFilter::UnaryNode::UnaryNode(std::shared_ptr child, const std::string& name) + : mChild(std::move(child)) + , mName(name) +{ +} const CSMFilter::Node& CSMFilter::UnaryNode::getChild() const { @@ -19,7 +23,7 @@ std::vector CSMFilter::UnaryNode::getReferencedColumns() const return mChild->getReferencedColumns(); } -std::string CSMFilter::UnaryNode::toString (bool numericColumns) const +std::string CSMFilter::UnaryNode::toString(bool numericColumns) const { - return mName + " " + mChild->toString (numericColumns); + return mName + " " + mChild->toString(numericColumns); } diff --git a/apps/opencs/model/filter/unarynode.hpp b/apps/opencs/model/filter/unarynode.hpp index 39326d16729..04422d3c0ca 100644 --- a/apps/opencs/model/filter/unarynode.hpp +++ b/apps/opencs/model/filter/unarynode.hpp @@ -1,31 +1,34 @@ #ifndef CSM_FILTER_UNARYNODE_H #define CSM_FILTER_UNARYNODE_H +#include +#include +#include + #include "node.hpp" namespace CSMFilter { class UnaryNode : public Node { - std::shared_ptr mChild; - std::string mName; - - public: + std::shared_ptr mChild; + std::string mName; - UnaryNode (std::shared_ptr child, const std::string& name); + public: + UnaryNode(std::shared_ptr child, const std::string& name); - const Node& getChild() const; + const Node& getChild() const; - Node& getChild(); + Node& getChild(); - std::vector getReferencedColumns() const override; - ///< Return a list of the IDs of the columns referenced by this node. The column mapping - /// passed into test as columns must contain all columns listed here. + std::vector getReferencedColumns() const override; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. - std::string toString (bool numericColumns) const override; - ///< Return a string that represents this node. - /// - /// \param numericColumns Use numeric IDs instead of string to represent columns. + std::string toString(bool numericColumns) const override; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } diff --git a/apps/opencs/model/filter/valuenode.cpp b/apps/opencs/model/filter/valuenode.cpp index 85c5a8e27f2..264967f2401 100644 --- a/apps/opencs/model/filter/valuenode.cpp +++ b/apps/opencs/model/filter/valuenode.cpp @@ -2,47 +2,65 @@ #include #include +#include -#include "../world/columns.hpp" #include "../world/idtablebase.hpp" -CSMFilter::ValueNode::ValueNode (int columnId, Type lowerType, Type upperType, - double lower, double upper) -: mColumnId (columnId), mLower (lower), mUpper (upper), mLowerType (lowerType), mUpperType (upperType){} +CSMFilter::ValueNode::ValueNode(int columnId, Type lowerType, Type upperType, double lower, double upper) + : mColumnId(columnId) + , mLower(lower) + , mUpper(upper) + , mLowerType(lowerType) + , mUpperType(upperType) +{ +} -bool CSMFilter::ValueNode::test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const +bool CSMFilter::ValueNode::test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { - const std::map::const_iterator iter = columns.find (mColumnId); + const std::map::const_iterator iter = columns.find(mColumnId); - if (iter==columns.end()) - throw std::logic_error ("invalid column in value node test"); + if (iter == columns.end()) + throw std::logic_error("invalid column in value node test"); - if (iter->second==-1) + if (iter->second == -1) return true; - QModelIndex index = table.index (row, iter->second); + QModelIndex index = table.index(row, iter->second); - QVariant data = table.data (index); + QVariant data = table.data(index); - if (data.type()!=QVariant::Double && data.type()!=QVariant::Bool && data.type()!=QVariant::Int && - data.type()!=QVariant::UInt && data.type()!=static_cast (QMetaType::Float)) + if (data.type() != QVariant::Double && data.type() != QVariant::Bool && data.type() != QVariant::Int + && data.type() != QVariant::UInt && data.type() != static_cast(QMetaType::Float)) return false; double value = data.toDouble(); switch (mLowerType) { - case Type_Closed: if (valuemUpper) return false; break; - case Type_Open: if (value>=mUpper) return false; break; - case Type_Infinite: break; + case Type_Closed: + if (value > mUpper) + return false; + break; + case Type_Open: + if (value >= mUpper) + return false; + break; + case Type_Infinite: + break; } return true; @@ -50,10 +68,10 @@ bool CSMFilter::ValueNode::test (const CSMWorld::IdTableBase& table, int row, std::vector CSMFilter::ValueNode::getReferencedColumns() const { - return std::vector (1, mColumnId); + return std::vector(1, mColumnId); } -std::string CSMFilter::ValueNode::toString (bool numericColumns) const +std::string CSMFilter::ValueNode::toString(bool numericColumns) const { std::ostringstream stream; @@ -62,31 +80,40 @@ std::string CSMFilter::ValueNode::toString (bool numericColumns) const if (numericColumns) stream << mColumnId; else - stream - << "\"" - << CSMWorld::Columns::getName (static_cast (mColumnId)) - << "\""; + stream << "\"" << CSMWorld::Columns::getName(static_cast(mColumnId)) << "\""; stream << ", "; - if (mLower==mUpper && mLowerType!=Type_Infinite && mUpperType!=Type_Infinite) + if (mLower == mUpper && mLowerType != Type_Infinite && mUpperType != Type_Infinite) stream << mLower; else { switch (mLowerType) { - case Type_Closed: stream << "[" << mLower; break; - case Type_Open: stream << "(" << mLower; break; - case Type_Infinite: stream << "("; break; + case Type_Closed: + stream << "[" << mLower; + break; + case Type_Open: + stream << "(" << mLower; + break; + case Type_Infinite: + stream << "("; + break; } stream << ", "; switch (mUpperType) { - case Type_Closed: stream << mUpper << "]"; break; - case Type_Open: stream << mUpper << ")"; break; - case Type_Infinite: stream << ")"; break; + case Type_Closed: + stream << mUpper << "]"; + break; + case Type_Open: + stream << mUpper << ")"; + break; + case Type_Infinite: + stream << ")"; + break; } } diff --git a/apps/opencs/model/filter/valuenode.hpp b/apps/opencs/model/filter/valuenode.hpp index 339e4948a6e..caf4f186207 100644 --- a/apps/opencs/model/filter/valuenode.hpp +++ b/apps/opencs/model/filter/valuenode.hpp @@ -3,43 +3,47 @@ #include "leafnode.hpp" +#include + +#include +#include +#include + namespace CSMFilter { class ValueNode : public LeafNode { - public: - - enum Type - { - Type_Closed, Type_Open, Type_Infinite - }; - - private: - - int mColumnId; - std::string mText; - double mLower; - double mUpper; - Type mLowerType; - Type mUpperType; - - public: - - ValueNode (int columnId, Type lowerType, Type upperType, double lower, double upper); - - bool test (const CSMWorld::IdTableBase& table, int row, - const std::map& columns) const override; - ///< \return Can the specified table row pass through to filter? - /// \param columns column ID to column index mapping - - std::vector getReferencedColumns() const override; - ///< Return a list of the IDs of the columns referenced by this node. The column mapping - /// passed into test as columns must contain all columns listed here. - - std::string toString (bool numericColumns) const override; - ///< Return a string that represents this node. - /// - /// \param numericColumns Use numeric IDs instead of string to represent columns. + public: + enum Type + { + Type_Closed, + Type_Open, + Type_Infinite + }; + + private: + int mColumnId; + std::string mText; + double mLower; + double mUpper; + Type mLowerType; + Type mUpperType; + + public: + ValueNode(int columnId, Type lowerType, Type upperType, double lower, double upper); + + bool test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const override; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + + std::vector getReferencedColumns() const override; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + std::string toString(bool numericColumns) const override; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } diff --git a/apps/opencs/model/prefs/boolsetting.cpp b/apps/opencs/model/prefs/boolsetting.cpp index 0058753490d..44262e20126 100644 --- a/apps/opencs/model/prefs/boolsetting.cpp +++ b/apps/opencs/model/prefs/boolsetting.cpp @@ -1,4 +1,3 @@ - #include "boolsetting.hpp" #include @@ -6,52 +5,50 @@ #include +#include + #include "category.hpp" #include "state.hpp" -CSMPrefs::BoolSetting::BoolSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, bool default_) -: Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(nullptr) -{} +CSMPrefs::BoolSetting::BoolSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) + , mWidget(nullptr) +{ +} -CSMPrefs::BoolSetting& CSMPrefs::BoolSetting::setTooltip (const std::string& tooltip) +CSMPrefs::BoolSetting& CSMPrefs::BoolSetting::setTooltip(const std::string& tooltip) { mTooltip = tooltip; return *this; } -std::pair CSMPrefs::BoolSetting::makeWidgets (QWidget *parent) +CSMPrefs::SettingWidgets CSMPrefs::BoolSetting::makeWidgets(QWidget* parent) { - mWidget = new QCheckBox (QString::fromUtf8 (getLabel().c_str()), parent); - mWidget->setCheckState (mDefault ? Qt::Checked : Qt::Unchecked); + mWidget = new QCheckBox(getLabel(), parent); + mWidget->setCheckState(getValue() ? Qt::Checked : Qt::Unchecked); if (!mTooltip.empty()) { - QString tooltip = QString::fromUtf8 (mTooltip.c_str()); - mWidget->setToolTip (tooltip); + QString tooltip = QString::fromUtf8(mTooltip.c_str()); + mWidget->setToolTip(tooltip); } - connect (mWidget, SIGNAL (stateChanged (int)), this, SLOT (valueChanged (int))); + connect(mWidget, &QCheckBox::stateChanged, this, &BoolSetting::valueChanged); - return std::make_pair (static_cast (nullptr), mWidget); + return SettingWidgets{ .mLabel = nullptr, .mInput = mWidget }; } void CSMPrefs::BoolSetting::updateWidget() { if (mWidget) { - mWidget->setCheckState(getValues().getBool(getKey(), getParent()->getKey()) - ? Qt::Checked - : Qt::Unchecked); + mWidget->setCheckState(getValue() ? Qt::Checked : Qt::Unchecked); } } -void CSMPrefs::BoolSetting::valueChanged (int value) +void CSMPrefs::BoolSetting::valueChanged(int value) { - { - QMutexLocker lock (getMutex()); - getValues().setBool (getKey(), getParent()->getKey(), value); - } - - getParent()->getState()->update (*this); + setValue(value != Qt::Unchecked); + getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/boolsetting.hpp b/apps/opencs/model/prefs/boolsetting.hpp index 941cb503721..edabf85058b 100644 --- a/apps/opencs/model/prefs/boolsetting.hpp +++ b/apps/opencs/model/prefs/boolsetting.hpp @@ -3,33 +3,36 @@ #include "setting.hpp" +#include +#include + class QCheckBox; namespace CSMPrefs { - class BoolSetting : public Setting - { - Q_OBJECT + class Category; - std::string mTooltip; - bool mDefault; - QCheckBox* mWidget; + class BoolSetting final : public TypedSetting + { + Q_OBJECT - public: + std::string mTooltip; + QCheckBox* mWidget; - BoolSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, bool default_); + public: + explicit BoolSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - BoolSetting& setTooltip (const std::string& tooltip); + BoolSetting& setTooltip(const std::string& tooltip); - /// Return label, input widget. - std::pair makeWidgets (QWidget *parent) override; + /// Return label, input widget. + SettingWidgets makeWidgets(QWidget* parent) override; - void updateWidget() override; + void updateWidget() override; - private slots: + private slots: - void valueChanged (int value); + void valueChanged(int value); }; } diff --git a/apps/opencs/model/prefs/category.cpp b/apps/opencs/model/prefs/category.cpp index 6af0ac645b7..3ae48269531 100644 --- a/apps/opencs/model/prefs/category.cpp +++ b/apps/opencs/model/prefs/category.cpp @@ -5,24 +5,35 @@ #include "setting.hpp" #include "state.hpp" +#include "subcategory.hpp" -CSMPrefs::Category::Category (State *parent, const std::string& key) -: mParent (parent), mKey (key) -{} +CSMPrefs::Category::Category(State* parent, const std::string& key) + : mParent(parent) + , mKey(key) +{ +} const std::string& CSMPrefs::Category::getKey() const { return mKey; } -CSMPrefs::State *CSMPrefs::Category::getState() const +CSMPrefs::State* CSMPrefs::Category::getState() const { return mParent; } -void CSMPrefs::Category::addSetting (Setting *setting) +void CSMPrefs::Category::addSetting(Setting* setting) { - mSettings.push_back (setting); + if (!mIndex.emplace(setting->getKey(), setting).second) + throw std::logic_error("Category " + mKey + " already has setting: " + setting->getKey()); + + mSettings.push_back(setting); +} + +void CSMPrefs::Category::addSubcategory(Subcategory* setting) +{ + mSettings.push_back(setting); } CSMPrefs::Category::Iterator CSMPrefs::Category::begin() @@ -35,17 +46,18 @@ CSMPrefs::Category::Iterator CSMPrefs::Category::end() return mSettings.end(); } -CSMPrefs::Setting& CSMPrefs::Category::operator[] (const std::string& key) +CSMPrefs::Setting& CSMPrefs::Category::operator[](const std::string& key) { - for (Iterator iter = mSettings.begin(); iter!=mSettings.end(); ++iter) - if ((*iter)->getKey()==key) - return **iter; + const auto it = mIndex.find(key); + + if (it != mIndex.end()) + return *it->second; - throw std::logic_error ("Invalid user setting: " + key); + throw std::logic_error("Invalid user setting in " + mKey + " category: " + key); } void CSMPrefs::Category::update() { - for (Iterator iter = mSettings.begin(); iter!=mSettings.end(); ++iter) - mParent->update (**iter); + for (Iterator iter = mSettings.begin(); iter != mSettings.end(); ++iter) + mParent->update(**iter); } diff --git a/apps/opencs/model/prefs/category.hpp b/apps/opencs/model/prefs/category.hpp index b70716aa0e7..ef67c82138c 100644 --- a/apps/opencs/model/prefs/category.hpp +++ b/apps/opencs/model/prefs/category.hpp @@ -1,44 +1,47 @@ #ifndef CSM_PREFS_CATEGORY_H #define CSM_PREFS_CATEGORY_H +#include #include +#include #include namespace CSMPrefs { class State; class Setting; + class Subcategory; class Category { - public: + public: + typedef std::vector Container; + typedef Container::iterator Iterator; - typedef std::vector Container; - typedef Container::iterator Iterator; + private: + State* mParent; + std::string mKey; + Container mSettings; + std::unordered_map mIndex; - private: + public: + Category(State* parent, const std::string& key); - State *mParent; - std::string mKey; - Container mSettings; + const std::string& getKey() const; - public: + State* getState() const; - Category (State *parent, const std::string& key); + void addSetting(Setting* setting); - const std::string& getKey() const; + void addSubcategory(Subcategory* setting); - State *getState() const; + Iterator begin(); - void addSetting (Setting *setting); + Iterator end(); - Iterator begin(); + Setting& operator[](const std::string& key); - Iterator end(); - - Setting& operator[] (const std::string& key); - - void update(); + void update(); }; } diff --git a/apps/opencs/model/prefs/coloursetting.cpp b/apps/opencs/model/prefs/coloursetting.cpp index e676ad91c34..10ca9d7f681 100644 --- a/apps/opencs/model/prefs/coloursetting.cpp +++ b/apps/opencs/model/prefs/coloursetting.cpp @@ -1,4 +1,3 @@ - #include "coloursetting.hpp" #include @@ -7,56 +6,53 @@ #include +#include + #include "../../view/widget/coloreditor.hpp" #include "category.hpp" #include "state.hpp" -CSMPrefs::ColourSetting::ColourSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, QColor default_) -: Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(nullptr) -{} +CSMPrefs::ColourSetting::ColourSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) + , mWidget(nullptr) +{ +} -CSMPrefs::ColourSetting& CSMPrefs::ColourSetting::setTooltip (const std::string& tooltip) +CSMPrefs::ColourSetting& CSMPrefs::ColourSetting::setTooltip(const std::string& tooltip) { mTooltip = tooltip; return *this; } -std::pair CSMPrefs::ColourSetting::makeWidgets (QWidget *parent) +CSMPrefs::SettingWidgets CSMPrefs::ColourSetting::makeWidgets(QWidget* parent) { - QLabel *label = new QLabel (QString::fromUtf8 (getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); - mWidget = new CSVWidget::ColorEditor (mDefault, parent); + mWidget = new CSVWidget::ColorEditor(toColor(), parent); if (!mTooltip.empty()) { - QString tooltip = QString::fromUtf8 (mTooltip.c_str()); - label->setToolTip (tooltip); - mWidget->setToolTip (tooltip); + QString tooltip = QString::fromUtf8(mTooltip.c_str()); + label->setToolTip(tooltip); + mWidget->setToolTip(tooltip); } - connect (mWidget, SIGNAL (pickingFinished()), this, SLOT (valueChanged())); + connect(mWidget, &CSVWidget::ColorEditor::pickingFinished, this, &ColourSetting::valueChanged); - return std::make_pair (label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::ColourSetting::updateWidget() { if (mWidget) - { - mWidget->setColor(QString::fromStdString - (getValues().getString(getKey(), getParent()->getKey()))); - } + mWidget->setColor(toColor()); } void CSMPrefs::ColourSetting::valueChanged() { - CSVWidget::ColorEditor& widget = dynamic_cast (*sender()); - { - QMutexLocker lock (getMutex()); - getValues().setString (getKey(), getParent()->getKey(), widget.color().name().toUtf8().data()); - } - - getParent()->getState()->update (*this); + CSVWidget::ColorEditor& widget = dynamic_cast(*sender()); + setValue(widget.color().name().toStdString()); + getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/coloursetting.hpp b/apps/opencs/model/prefs/coloursetting.hpp index 4a814c0e2ea..85e43f28bda 100644 --- a/apps/opencs/model/prefs/coloursetting.hpp +++ b/apps/opencs/model/prefs/coloursetting.hpp @@ -5,6 +5,14 @@ #include +#include +#include +#include + +class QMutex; +class QObject; +class QWidget; + namespace CSVWidget { class ColorEditor; @@ -12,30 +20,29 @@ namespace CSVWidget namespace CSMPrefs { - class ColourSetting : public Setting - { - Q_OBJECT + class Category; - std::string mTooltip; - QColor mDefault; - CSVWidget::ColorEditor* mWidget; + class ColourSetting final : public TypedSetting + { + Q_OBJECT - public: + std::string mTooltip; + CSVWidget::ColorEditor* mWidget; - ColourSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, - QColor default_); + public: + explicit ColourSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - ColourSetting& setTooltip (const std::string& tooltip); + ColourSetting& setTooltip(const std::string& tooltip); - /// Return label, input widget. - std::pair makeWidgets (QWidget *parent) override; + /// Return label, input widget. + SettingWidgets makeWidgets(QWidget* parent) override; - void updateWidget() override; + void updateWidget() override; - private slots: + private slots: - void valueChanged(); + void valueChanged(); }; } diff --git a/apps/opencs/model/prefs/doublesetting.cpp b/apps/opencs/model/prefs/doublesetting.cpp index 757b67389af..bbe573f8004 100644 --- a/apps/opencs/model/prefs/doublesetting.cpp +++ b/apps/opencs/model/prefs/doublesetting.cpp @@ -3,21 +3,26 @@ #include -#include #include +#include #include #include +#include + #include "category.hpp" #include "state.hpp" -CSMPrefs::DoubleSetting::DoubleSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, double default_) -: Setting (parent, values, mutex, key, label), - mPrecision(2), mMin (0), mMax (std::numeric_limits::max()), - mDefault (default_), mWidget(nullptr) -{} +CSMPrefs::DoubleSetting::DoubleSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) + , mPrecision(2) + , mMin(0) + , mMax(std::numeric_limits::max()) + , mWidget(nullptr) +{ +} CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setPrecision(int precision) { @@ -25,66 +30,60 @@ CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setPrecision(int precision) return *this; } -CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setRange (double min, double max) +CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setRange(double min, double max) { mMin = min; mMax = max; return *this; } -CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setMin (double min) +CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setMin(double min) { mMin = min; return *this; } -CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setMax (double max) +CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setMax(double max) { mMax = max; return *this; } -CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setTooltip (const std::string& tooltip) +CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setTooltip(const std::string& tooltip) { mTooltip = tooltip; return *this; } -std::pair CSMPrefs::DoubleSetting::makeWidgets (QWidget *parent) +CSMPrefs::SettingWidgets CSMPrefs::DoubleSetting::makeWidgets(QWidget* parent) { - QLabel *label = new QLabel (QString::fromUtf8 (getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); - mWidget = new QDoubleSpinBox (parent); + mWidget = new QDoubleSpinBox(parent); mWidget->setDecimals(mPrecision); - mWidget->setRange (mMin, mMax); - mWidget->setValue (mDefault); + mWidget->setRange(mMin, mMax); + mWidget->setValue(getValue()); if (!mTooltip.empty()) { - QString tooltip = QString::fromUtf8 (mTooltip.c_str()); - label->setToolTip (tooltip); - mWidget->setToolTip (tooltip); + QString tooltip = QString::fromUtf8(mTooltip.c_str()); + label->setToolTip(tooltip); + mWidget->setToolTip(tooltip); } - connect (mWidget, SIGNAL (valueChanged (double)), this, SLOT (valueChanged (double))); + connect(mWidget, qOverload(&QDoubleSpinBox::valueChanged), this, &DoubleSetting::valueChanged); - return std::make_pair (label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::DoubleSetting::updateWidget() { if (mWidget) - { - mWidget->setValue(getValues().getFloat(getKey(), getParent()->getKey())); - } + mWidget->setValue(getValue()); } -void CSMPrefs::DoubleSetting::valueChanged (double value) +void CSMPrefs::DoubleSetting::valueChanged(double value) { - { - QMutexLocker lock (getMutex()); - getValues().setFloat (getKey(), getParent()->getKey(), value); - } - - getParent()->getState()->update (*this); + setValue(value); + getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/doublesetting.hpp b/apps/opencs/model/prefs/doublesetting.hpp index 47886e446fd..856cebcb464 100644 --- a/apps/opencs/model/prefs/doublesetting.hpp +++ b/apps/opencs/model/prefs/doublesetting.hpp @@ -7,42 +7,41 @@ class QDoubleSpinBox; namespace CSMPrefs { - class DoubleSetting : public Setting - { - Q_OBJECT + class Category; - int mPrecision; - double mMin; - double mMax; - std::string mTooltip; - double mDefault; - QDoubleSpinBox* mWidget; + class DoubleSetting final : public TypedSetting + { + Q_OBJECT - public: + int mPrecision; + double mMin; + double mMax; + std::string mTooltip; + QDoubleSpinBox* mWidget; - DoubleSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, - double default_); + public: + explicit DoubleSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - DoubleSetting& setPrecision (int precision); + DoubleSetting& setPrecision(int precision); - // defaults to [0, std::numeric_limits::max()] - DoubleSetting& setRange (double min, double max); + // defaults to [0, std::numeric_limits::max()] + DoubleSetting& setRange(double min, double max); - DoubleSetting& setMin (double min); + DoubleSetting& setMin(double min); - DoubleSetting& setMax (double max); + DoubleSetting& setMax(double max); - DoubleSetting& setTooltip (const std::string& tooltip); + DoubleSetting& setTooltip(const std::string& tooltip); - /// Return label, input widget. - std::pair makeWidgets (QWidget *parent) override; + /// Return label, input widget. + SettingWidgets makeWidgets(QWidget* parent) override; - void updateWidget() override; + void updateWidget() override; - private slots: + private slots: - void valueChanged (double value); + void valueChanged(double value); }; } diff --git a/apps/opencs/model/prefs/enumsetting.cpp b/apps/opencs/model/prefs/enumsetting.cpp index ec3fca328ff..aaa4c28c61d 100644 --- a/apps/opencs/model/prefs/enumsetting.cpp +++ b/apps/opencs/model/prefs/enumsetting.cpp @@ -1,124 +1,80 @@ - #include "enumsetting.hpp" -#include #include +#include #include #include +#include +#include + +#include + #include #include "category.hpp" #include "state.hpp" - -CSMPrefs::EnumValue::EnumValue (const std::string& value, const std::string& tooltip) -: mValue (value), mTooltip (tooltip) -{} - -CSMPrefs::EnumValue::EnumValue (const char *value) -: mValue (value) -{} - - -CSMPrefs::EnumValues& CSMPrefs::EnumValues::add (const EnumValues& values) -{ - mValues.insert (mValues.end(), values.mValues.begin(), values.mValues.end()); - return *this; -} - -CSMPrefs::EnumValues& CSMPrefs::EnumValues::add (const EnumValue& value) -{ - mValues.push_back (value); - return *this; -} - -CSMPrefs::EnumValues& CSMPrefs::EnumValues::add (const std::string& value, const std::string& tooltip) +CSMPrefs::EnumSetting::EnumSetting(Category* parent, QMutex* mutex, std::string_view key, const QString& label, + std::span values, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) + , mValues(values) + , mWidget(nullptr) { - mValues.emplace_back(value, tooltip); - return *this; } - -CSMPrefs::EnumSetting::EnumSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, const EnumValue& default_) -: Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(nullptr) -{} - -CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::setTooltip (const std::string& tooltip) +CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::setTooltip(const std::string& tooltip) { mTooltip = tooltip; return *this; } -CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValues (const EnumValues& values) +CSMPrefs::SettingWidgets CSMPrefs::EnumSetting::makeWidgets(QWidget* parent) { - mValues.add (values); - return *this; -} - -CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValue (const EnumValue& value) -{ - mValues.add (value); - return *this; -} + QLabel* label = new QLabel(getLabel(), parent); -CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::addValue (const std::string& value, const std::string& tooltip) -{ - mValues.add (value, tooltip); - return *this; -} + mWidget = new QComboBox(parent); -std::pair CSMPrefs::EnumSetting::makeWidgets (QWidget *parent) -{ - QLabel *label = new QLabel (QString::fromUtf8 (getLabel().c_str()), parent); - - mWidget = new QComboBox (parent); - - int index = 0; - - for (int i=0; i (mValues.mValues.size()); ++i) + for (std::size_t i = 0; i < mValues.size(); ++i) { - if (mDefault.mValue==mValues.mValues[i].mValue) - index = i; + const EnumValueView& v = mValues[i]; - mWidget->addItem (QString::fromUtf8 (mValues.mValues[i].mValue.c_str())); + mWidget->addItem(QString::fromUtf8(v.mValue.data(), static_cast(v.mValue.size()))); - if (!mValues.mValues[i].mTooltip.empty()) - mWidget->setItemData (i, QString::fromUtf8 (mValues.mValues[i].mTooltip.c_str()), - Qt::ToolTipRole); + if (!v.mTooltip.empty()) + mWidget->setItemData(static_cast(i), + QString::fromUtf8(v.mTooltip.data(), static_cast(v.mTooltip.size())), Qt::ToolTipRole); } - mWidget->setCurrentIndex (index); + const std::string value = getValue(); + const std::size_t index = std::find_if(mValues.begin(), mValues.end(), [&](const EnumValueView& v) { + return v.mValue == value; + }) - mValues.begin(); + + mWidget->setCurrentIndex(static_cast(index)); if (!mTooltip.empty()) { - QString tooltip = QString::fromUtf8 (mTooltip.c_str()); - label->setToolTip (tooltip); + QString tooltip = QString::fromUtf8(mTooltip.c_str()); + label->setToolTip(tooltip); } - connect (mWidget, SIGNAL (currentIndexChanged (int)), this, SLOT (valueChanged (int))); + connect(mWidget, qOverload(&QComboBox::currentIndexChanged), this, &EnumSetting::valueChanged); - return std::make_pair (label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::EnumSetting::updateWidget() { if (mWidget) - { - int index = mWidget->findText(QString::fromStdString - (getValues().getString(getKey(), getParent()->getKey()))); - - mWidget->setCurrentIndex(index); - } + mWidget->setCurrentIndex(mWidget->findText(QString::fromStdString(getValue()))); } -void CSMPrefs::EnumSetting::valueChanged (int value) +void CSMPrefs::EnumSetting::valueChanged(int value) { - { - QMutexLocker lock (getMutex()); - getValues().setString (getKey(), getParent()->getKey(), mValues.mValues.at (value).mValue); - } + if (value < 0 || static_cast(value) >= mValues.size()) + throw std::logic_error("Invalid enum setting \"" + getKey() + "\" value index: " + std::to_string(value)); - getParent()->getState()->update (*this); + setValue(std::string(mValues[value].mValue)); + getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/enumsetting.hpp b/apps/opencs/model/prefs/enumsetting.hpp index 235f6adc3a7..00953f914ed 100644 --- a/apps/opencs/model/prefs/enumsetting.hpp +++ b/apps/opencs/model/prefs/enumsetting.hpp @@ -1,66 +1,43 @@ #ifndef CSM_PREFS_ENUMSETTING_H #define CSM_PREFS_ENUMSETTING_H +#include +#include +#include +#include #include +#include "enumvalueview.hpp" #include "setting.hpp" class QComboBox; namespace CSMPrefs { - struct EnumValue - { - std::string mValue; - std::string mTooltip; - - EnumValue (const std::string& value, const std::string& tooltip = ""); - - EnumValue (const char *value); - }; - - struct EnumValues - { - std::vector mValues; - - EnumValues& add (const EnumValues& values); + class Category; - EnumValues& add (const EnumValue& value); - - EnumValues& add (const std::string& value, const std::string& tooltip); - }; - - class EnumSetting : public Setting + class EnumSetting final : public TypedSetting { - Q_OBJECT - - std::string mTooltip; - EnumValue mDefault; - EnumValues mValues; - QComboBox* mWidget; + Q_OBJECT - public: - - EnumSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, - const EnumValue& default_); - - EnumSetting& setTooltip (const std::string& tooltip); - - EnumSetting& addValues (const EnumValues& values); + std::string mTooltip; + std::span mValues; + QComboBox* mWidget; - EnumSetting& addValue (const EnumValue& value); + public: + explicit EnumSetting(Category* parent, QMutex* mutex, std::string_view key, const QString& label, + std::span values, Settings::Index& index); - EnumSetting& addValue (const std::string& value, const std::string& tooltip); + EnumSetting& setTooltip(const std::string& tooltip); - /// Return label, input widget. - std::pair makeWidgets (QWidget *parent) override; + /// Return label, input widget. + SettingWidgets makeWidgets(QWidget* parent) override; - void updateWidget() override; + void updateWidget() override; - private slots: + private slots: - void valueChanged (int value); + void valueChanged(int value); }; } diff --git a/apps/opencs/model/prefs/enumvalueview.hpp b/apps/opencs/model/prefs/enumvalueview.hpp new file mode 100644 index 00000000000..f46d250a812 --- /dev/null +++ b/apps/opencs/model/prefs/enumvalueview.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_APPS_OPENCS_MODEL_PREFS_ENUMVALUEVIEW_H +#define OPENMW_APPS_OPENCS_MODEL_PREFS_ENUMVALUEVIEW_H + +#include + +namespace CSMPrefs +{ + struct EnumValueView + { + std::string_view mValue; + std::string_view mTooltip; + }; +} + +#endif diff --git a/apps/opencs/model/prefs/intsetting.cpp b/apps/opencs/model/prefs/intsetting.cpp index 407ed11f05c..a593b6f688b 100644 --- a/apps/opencs/model/prefs/intsetting.cpp +++ b/apps/opencs/model/prefs/intsetting.cpp @@ -4,79 +4,78 @@ #include #include -#include #include +#include #include +#include + #include "category.hpp" #include "state.hpp" -CSMPrefs::IntSetting::IntSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, int default_) -: Setting (parent, values, mutex, key, label), mMin (0), mMax (std::numeric_limits::max()), - mDefault (default_), mWidget(nullptr) -{} +CSMPrefs::IntSetting::IntSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) + , mMin(0) + , mMax(std::numeric_limits::max()) + , mWidget(nullptr) +{ +} -CSMPrefs::IntSetting& CSMPrefs::IntSetting::setRange (int min, int max) +CSMPrefs::IntSetting& CSMPrefs::IntSetting::setRange(int min, int max) { mMin = min; mMax = max; return *this; } -CSMPrefs::IntSetting& CSMPrefs::IntSetting::setMin (int min) +CSMPrefs::IntSetting& CSMPrefs::IntSetting::setMin(int min) { mMin = min; return *this; } -CSMPrefs::IntSetting& CSMPrefs::IntSetting::setMax (int max) +CSMPrefs::IntSetting& CSMPrefs::IntSetting::setMax(int max) { mMax = max; return *this; } -CSMPrefs::IntSetting& CSMPrefs::IntSetting::setTooltip (const std::string& tooltip) +CSMPrefs::IntSetting& CSMPrefs::IntSetting::setTooltip(const std::string& tooltip) { mTooltip = tooltip; return *this; } -std::pair CSMPrefs::IntSetting::makeWidgets (QWidget *parent) +CSMPrefs::SettingWidgets CSMPrefs::IntSetting::makeWidgets(QWidget* parent) { - QLabel *label = new QLabel (QString::fromUtf8 (getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); - mWidget = new QSpinBox (parent); - mWidget->setRange (mMin, mMax); - mWidget->setValue (mDefault); + mWidget = new QSpinBox(parent); + mWidget->setRange(mMin, mMax); + mWidget->setValue(getValue()); if (!mTooltip.empty()) { - QString tooltip = QString::fromUtf8 (mTooltip.c_str()); - label->setToolTip (tooltip); - mWidget->setToolTip (tooltip); + QString tooltip = QString::fromUtf8(mTooltip.c_str()); + label->setToolTip(tooltip); + mWidget->setToolTip(tooltip); } - connect (mWidget, SIGNAL (valueChanged (int)), this, SLOT (valueChanged (int))); + connect(mWidget, qOverload(&QSpinBox::valueChanged), this, &IntSetting::valueChanged); - return std::make_pair (label, mWidget); + return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::IntSetting::updateWidget() { if (mWidget) - { - mWidget->setValue(getValues().getInt(getKey(), getParent()->getKey())); - } + mWidget->setValue(getValue()); } -void CSMPrefs::IntSetting::valueChanged (int value) +void CSMPrefs::IntSetting::valueChanged(int value) { - { - QMutexLocker lock (getMutex()); - getValues().setInt (getKey(), getParent()->getKey(), value); - } - - getParent()->getState()->update (*this); + setValue(value); + getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/intsetting.hpp b/apps/opencs/model/prefs/intsetting.hpp index f18213b77b2..e2926456aa0 100644 --- a/apps/opencs/model/prefs/intsetting.hpp +++ b/apps/opencs/model/prefs/intsetting.hpp @@ -4,41 +4,44 @@ #include "setting.hpp" class QSpinBox; +class QMutex; +class QObject; +class QWidget; namespace CSMPrefs { - class IntSetting : public Setting - { - Q_OBJECT + class Category; - int mMin; - int mMax; - std::string mTooltip; - int mDefault; - QSpinBox* mWidget; + class IntSetting final : public TypedSetting + { + Q_OBJECT - public: + int mMin; + int mMax; + std::string mTooltip; + QSpinBox* mWidget; - IntSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, int default_); + public: + explicit IntSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - // defaults to [0, std::numeric_limits::max()] - IntSetting& setRange (int min, int max); + // defaults to [0, std::numeric_limits::max()] + IntSetting& setRange(int min, int max); - IntSetting& setMin (int min); + IntSetting& setMin(int min); - IntSetting& setMax (int max); + IntSetting& setMax(int max); - IntSetting& setTooltip (const std::string& tooltip); + IntSetting& setTooltip(const std::string& tooltip); - /// Return label, input widget. - std::pair makeWidgets (QWidget *parent) override; + /// Return label, input widget. + SettingWidgets makeWidgets(QWidget* parent) override; - void updateWidget() override; + void updateWidget() override; - private slots: + private slots: - void valueChanged (int value); + void valueChanged(int value); }; } diff --git a/apps/opencs/model/prefs/modifiersetting.cpp b/apps/opencs/model/prefs/modifiersetting.cpp index 288926d00b3..4bb7d64e605 100644 --- a/apps/opencs/model/prefs/modifiersetting.cpp +++ b/apps/opencs/model/prefs/modifiersetting.cpp @@ -5,29 +5,36 @@ #include #include #include -#include -#include "state.hpp" +#include + +#include +#include + #include "shortcutmanager.hpp" +#include "state.hpp" + +class QObject; +class QWidget; namespace CSMPrefs { - ModifierSetting::ModifierSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, - const std::string& label) - : Setting(parent, values, mutex, key, label) + ModifierSetting::ModifierSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mButton(nullptr) , mEditorActive(false) { } - std::pair ModifierSetting::makeWidgets(QWidget* parent) + SettingWidgets ModifierSetting::makeWidgets(QWidget* parent) { int modifier = 0; State::get().getShortcutManager().getModifier(getKey(), modifier); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(modifier).c_str()); - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); QPushButton* widget = new QPushButton(text, parent); widget->setCheckable(true); @@ -38,16 +45,16 @@ namespace CSMPrefs mButton = widget; - connect(widget, SIGNAL(toggled(bool)), this, SLOT(buttonToggled(bool))); + connect(widget, &QPushButton::toggled, this, &ModifierSetting::buttonToggled); - return std::make_pair(label, widget); + return SettingWidgets{ .mLabel = label, .mInput = widget }; } void ModifierSetting::updateWidget() { if (mButton) { - std::string shortcut = getValues().getString(getKey(), getParent()->getKey()); + const std::string& shortcut = getValue(); int modifier; State::get().getShortcutManager().convertFromString(shortcut, modifier); @@ -88,10 +95,7 @@ namespace CSMPrefs bool ModifierSetting::handleEvent(QObject* target, int mod, int value) { // For potential future exceptions - const int Blacklist[] = - { - 0 - }; + const int Blacklist[] = { 0 }; const size_t BlacklistSize = sizeof(Blacklist) / sizeof(int); @@ -116,7 +120,6 @@ namespace CSMPrefs return true; } - // Update modifier int modifier = value; storeValue(modifier); @@ -129,15 +132,7 @@ namespace CSMPrefs void ModifierSetting::storeValue(int modifier) { State::get().getShortcutManager().setModifier(getKey(), modifier); - - // Convert to string and assign - std::string value = State::get().getShortcutManager().convertToString(modifier); - - { - QMutexLocker lock(getMutex()); - getValues().setString(getKey(), getParent()->getKey(), value); - } - + setValue(State::get().getShortcutManager().convertToString(modifier)); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/modifiersetting.hpp b/apps/opencs/model/prefs/modifiersetting.hpp index 977badb8dfa..76a3a82e719 100644 --- a/apps/opencs/model/prefs/modifiersetting.hpp +++ b/apps/opencs/model/prefs/modifiersetting.hpp @@ -1,45 +1,48 @@ #ifndef CSM_PREFS_MODIFIERSETTING_H #define CSM_PREFS_MODIFIERSETTING_H -#include - #include "setting.hpp" +#include +#include + +class QMutex; +class QObject; +class QWidget; class QEvent; class QPushButton; namespace CSMPrefs { - class ModifierSetting : public Setting - { - Q_OBJECT + class Category; - public: - - ModifierSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, - const std::string& label); - - std::pair makeWidgets(QWidget* parent) override; + class ModifierSetting final : public TypedSetting + { + Q_OBJECT - void updateWidget() override; + public: + explicit ModifierSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - protected: + SettingWidgets makeWidgets(QWidget* parent) override; - bool eventFilter(QObject* target, QEvent* event) override; + void updateWidget() override; - private: + protected: + bool eventFilter(QObject* target, QEvent* event) override; - bool handleEvent(QObject* target, int mod, int value); + private: + bool handleEvent(QObject* target, int mod, int value); - void storeValue(int modifier); - void resetState(); + void storeValue(int modifier); + void resetState(); - QPushButton* mButton; - bool mEditorActive; + QPushButton* mButton; + bool mEditorActive; - private slots: + private slots: - void buttonToggled(bool checked); + void buttonToggled(bool checked); }; } diff --git a/apps/opencs/model/prefs/setting.cpp b/apps/opencs/model/prefs/setting.cpp index 165062232ab..3c2ac65c941 100644 --- a/apps/opencs/model/prefs/setting.cpp +++ b/apps/opencs/model/prefs/setting.cpp @@ -4,37 +4,29 @@ #include #include +#include +#include + #include "category.hpp" #include "state.hpp" -Settings::Manager& CSMPrefs::Setting::getValues() -{ - return *mValues; -} - -QMutex *CSMPrefs::Setting::getMutex() +QMutex* CSMPrefs::Setting::getMutex() { return mMutex; } -CSMPrefs::Setting::Setting (Category *parent, Settings::Manager *values, QMutex *mutex, - const std::string& key, const std::string& label) -: QObject (parent->getState()), mParent (parent), mValues (values), mMutex (mutex), mKey (key), - mLabel (label) -{} - -CSMPrefs::Setting:: ~Setting() {} - -std::pair CSMPrefs::Setting::makeWidgets (QWidget *parent) -{ - return std::pair (0, 0); -} - -void CSMPrefs::Setting::updateWidget() +CSMPrefs::Setting::Setting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : QObject(parent->getState()) + , mParent(parent) + , mMutex(mutex) + , mKey(key) + , mLabel(label) + , mIndex(index) { } -const CSMPrefs::Category *CSMPrefs::Setting::getParent() const +const CSMPrefs::Category* CSMPrefs::Setting::getParent() const { return mParent; } @@ -44,58 +36,29 @@ const std::string& CSMPrefs::Setting::getKey() const return mKey; } -const std::string& CSMPrefs::Setting::getLabel() const -{ - return mLabel; -} - -int CSMPrefs::Setting::toInt() const -{ - QMutexLocker lock (mMutex); - return mValues->getInt (mKey, mParent->getKey()); -} - -double CSMPrefs::Setting::toDouble() const -{ - QMutexLocker lock (mMutex); - return mValues->getFloat (mKey, mParent->getKey()); -} - -std::string CSMPrefs::Setting::toString() const -{ - QMutexLocker lock (mMutex); - return mValues->getString (mKey, mParent->getKey()); -} - -bool CSMPrefs::Setting::isTrue() const -{ - QMutexLocker lock (mMutex); - return mValues->getBool (mKey, mParent->getKey()); -} - QColor CSMPrefs::Setting::toColor() const { // toString() handles lock - return QColor (QString::fromUtf8 (toString().c_str())); + return QColor(QString::fromUtf8(toString().c_str())); } -bool CSMPrefs::operator== (const Setting& setting, const std::string& key) +bool CSMPrefs::operator==(const Setting& setting, const std::string& key) { std::string fullKey = setting.getParent()->getKey() + "/" + setting.getKey(); - return fullKey==key; + return fullKey == key; } -bool CSMPrefs::operator== (const std::string& key, const Setting& setting) +bool CSMPrefs::operator==(const std::string& key, const Setting& setting) { - return setting==key; + return setting == key; } -bool CSMPrefs::operator!= (const Setting& setting, const std::string& key) +bool CSMPrefs::operator!=(const Setting& setting, const std::string& key) { - return !(setting==key); + return !(setting == key); } -bool CSMPrefs::operator!= (const std::string& key, const Setting& setting) +bool CSMPrefs::operator!=(const std::string& key, const Setting& setting) { - return !(key==setting); + return !(key == setting); } diff --git a/apps/opencs/model/prefs/setting.hpp b/apps/opencs/model/prefs/setting.hpp index 7cb2d7acf60..faadbcadd1c 100644 --- a/apps/opencs/model/prefs/setting.hpp +++ b/apps/opencs/model/prefs/setting.hpp @@ -4,76 +4,115 @@ #include #include +#include #include +#include + +#include "category.hpp" + class QWidget; class QColor; class QMutex; - -namespace Settings -{ - class Manager; -} +class QGridLayout; +class QLabel; namespace CSMPrefs { - class Category; + struct SettingWidgets + { + QLabel* mLabel; + QWidget* mInput; + }; class Setting : public QObject { - Q_OBJECT + Q_OBJECT - Category *mParent; - Settings::Manager *mValues; - QMutex *mMutex; - std::string mKey; - std::string mLabel; + Category* mParent; + QMutex* mMutex; + std::string mKey; + QString mLabel; + Settings::Index& mIndex; - protected: + protected: + QMutex* getMutex(); - Settings::Manager& getValues(); + template + void resetValueImpl() + { + QMutexLocker lock(mMutex); + return mIndex.get(mParent->getKey(), mKey).reset(); + } - QMutex *getMutex(); + template + T getValueImpl() const + { + QMutexLocker lock(mMutex); + return mIndex.get(mParent->getKey(), mKey).get(); + } - public: + template + void setValueImpl(const T& value) + { + QMutexLocker lock(mMutex); + return mIndex.get(mParent->getKey(), mKey).set(value); + } - Setting (Category *parent, Settings::Manager *values, QMutex *mutex, const std::string& key, const std::string& label); + public: + explicit Setting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - virtual ~Setting(); + ~Setting() override = default; - /// Return label, input widget. - /// - /// \note first can be a 0-pointer, which means that the label is part of the input - /// widget. - virtual std::pair makeWidgets (QWidget *parent); + virtual SettingWidgets makeWidgets(QWidget* parent) = 0; - /// Updates the widget returned by makeWidgets() to the current setting. - /// - /// \note If make_widgets() has not been called yet then nothing happens. - virtual void updateWidget(); + /// Updates the widget returned by makeWidgets() to the current setting. + /// + /// \note If make_widgets() has not been called yet then nothing happens. + virtual void updateWidget() = 0; - const Category *getParent() const; + virtual void reset() = 0; - const std::string& getKey() const; + const Category* getParent() const; - const std::string& getLabel() const; + const std::string& getKey() const; - int toInt() const; + const QString& getLabel() const { return mLabel; } - double toDouble() const; + int toInt() const { return getValueImpl(); } + + double toDouble() const { return getValueImpl(); } + + std::string toString() const { return getValueImpl(); } + + bool isTrue() const { return getValueImpl(); } + + QColor toColor() const; + }; + + template + class TypedSetting : public Setting + { + public: + using Setting::Setting; - std::string toString() const; + void reset() final + { + resetValueImpl(); + updateWidget(); + } - bool isTrue() const; + T getValue() const { return getValueImpl(); } - QColor toColor() const; + void setValue(const T& value) { return setValueImpl(value); } }; // note: fullKeys have the format categoryKey/settingKey - bool operator== (const Setting& setting, const std::string& fullKey); - bool operator== (const std::string& fullKey, const Setting& setting); - bool operator!= (const Setting& setting, const std::string& fullKey); - bool operator!= (const std::string& fullKey, const Setting& setting); + bool operator==(const Setting& setting, const std::string& fullKey); + bool operator==(const std::string& fullKey, const Setting& setting); + bool operator!=(const Setting& setting, const std::string& fullKey); + bool operator!=(const std::string& fullKey, const Setting& setting); } #endif diff --git a/apps/opencs/model/prefs/shortcut.cpp b/apps/opencs/model/prefs/shortcut.cpp index ff7b949a4ab..673627e672a 100644 --- a/apps/opencs/model/prefs/shortcut.cpp +++ b/apps/opencs/model/prefs/shortcut.cpp @@ -1,14 +1,15 @@ #include "shortcut.hpp" #include +#include #include #include #include -#include "state.hpp" #include "shortcutmanager.hpp" +#include "state.hpp" namespace CSMPrefs { @@ -25,7 +26,7 @@ namespace CSMPrefs , mModifierStatus(false) , mAction(nullptr) { - assert (parent); + assert(parent); State::get().getShortcutManager().addShortcut(this); State::get().getShortcutManager().getSequence(name, mSequence); @@ -44,7 +45,7 @@ namespace CSMPrefs , mModifierStatus(false) , mAction(nullptr) { - assert (parent); + assert(parent); State::get().getShortcutManager().addShortcut(this); State::get().getShortcutManager().getSequence(name, mSequence); @@ -64,7 +65,7 @@ namespace CSMPrefs , mModifierStatus(false) , mAction(nullptr) { - assert (parent); + assert(parent); State::get().getShortcutManager().addShortcut(this); State::get().getShortcutManager().getSequence(name, mSequence); @@ -77,7 +78,7 @@ namespace CSMPrefs { State::get().getShortcutManager().removeShortcut(this); } - catch(const std::exception& e) + catch (const std::exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } @@ -176,8 +177,8 @@ namespace CSMPrefs { mAction->setText(mActionText); - disconnect(this, SIGNAL(activated()), mAction, SLOT(trigger())); - disconnect(mAction, SIGNAL(destroyed()), this, SLOT(actionDeleted())); + disconnect(this, qOverload<>(&Shortcut::activated), mAction, &QAction::trigger); + disconnect(mAction, &QAction::destroyed, this, &Shortcut::actionDeleted); } mAction = action; @@ -187,8 +188,8 @@ namespace CSMPrefs mActionText = mAction->text(); mAction->setText(mActionText + "\t" + State::get().getShortcutManager().convertToString(mSequence).data()); - connect(this, SIGNAL(activated()), mAction, SLOT(trigger())); - connect(mAction, SIGNAL(destroyed()), this, SLOT(actionDeleted())); + connect(this, qOverload<>(&Shortcut::activated), mAction, &QAction::trigger); + connect(mAction, &QAction::destroyed, this, &Shortcut::actionDeleted); } } diff --git a/apps/opencs/model/prefs/shortcut.hpp b/apps/opencs/model/prefs/shortcut.hpp index 4fa6f8a1a19..b2a17b8cb27 100644 --- a/apps/opencs/model/prefs/shortcut.hpp +++ b/apps/opencs/model/prefs/shortcut.hpp @@ -15,107 +15,105 @@ namespace CSMPrefs /// A class similar in purpose to QShortcut, but with the ability to use mouse buttons class Shortcut : public QObject { - Q_OBJECT + Q_OBJECT - public: + public: + enum ActivationStatus + { + AS_Regular, + AS_Secondary, + AS_Inactive + }; - enum ActivationStatus - { - AS_Regular, - AS_Secondary, - AS_Inactive - }; + enum SecondaryMode + { + SM_Replace, ///< The secondary signal replaces the regular signal when the modifier is active + SM_Detach, ///< The secondary signal is emitted independent of the regular signal, even if not active + SM_Ignore ///< The secondary signal will not ever be emitted + }; - enum SecondaryMode - { - SM_Replace, ///< The secondary signal replaces the regular signal when the modifier is active - SM_Detach, ///< The secondary signal is emitted independent of the regular signal, even if not active - SM_Ignore ///< The secondary signal will not ever be emitted - }; + Shortcut(const std::string& name, QWidget* parent); + Shortcut(const std::string& name, const std::string& modName, QWidget* parent); + Shortcut(const std::string& name, const std::string& modName, SecondaryMode secMode, QWidget* parent); - Shortcut(const std::string& name, QWidget* parent); - Shortcut(const std::string& name, const std::string& modName, QWidget* parent); - Shortcut(const std::string& name, const std::string& modName, SecondaryMode secMode, QWidget* parent); + ~Shortcut(); - ~Shortcut(); + bool isEnabled() const; - bool isEnabled() const; + const std::string& getName() const; + const std::string& getModifierName() const; - const std::string& getName() const; - const std::string& getModifierName() const; + SecondaryMode getSecondaryMode() const; - SecondaryMode getSecondaryMode() const; + const QKeySequence& getSequence() const; + int getModifier() const; - const QKeySequence& getSequence() const; - int getModifier() const; + /// The position in the sequence + int getPosition() const; + /// The position in the sequence + int getLastPosition() const; - /// The position in the sequence - int getPosition() const; - /// The position in the sequence - int getLastPosition() const; + ActivationStatus getActivationStatus() const; + bool getModifierStatus() const; - ActivationStatus getActivationStatus() const; - bool getModifierStatus() const; + void enable(bool state); - void enable(bool state); + void setSequence(const QKeySequence& sequence); + void setModifier(int modifier); - void setSequence(const QKeySequence& sequence); - void setModifier(int modifier); + /// The position in the sequence + void setPosition(int pos); - /// The position in the sequence - void setPosition(int pos); + void setActivationStatus(ActivationStatus status); + void setModifierStatus(bool status); - void setActivationStatus(ActivationStatus status); - void setModifierStatus(bool status); + /// Appends the sequence to the QAction text, also keeps it up to date + void associateAction(QAction* action); - /// Appends the sequence to the QAction text, also keeps it up to date - void associateAction(QAction* action); + // Workaround for Qt4 signals being "protected" + void signalActivated(bool state); + void signalActivated(); - // Workaround for Qt4 signals being "protected" - void signalActivated(bool state); - void signalActivated(); + void signalSecondary(bool state); + void signalSecondary(); - void signalSecondary(bool state); - void signalSecondary(); + QString toString() const; - QString toString() const; + private: + bool mEnabled; - private: + std::string mName; + std::string mModName; + SecondaryMode mSecondaryMode; + QKeySequence mSequence; + int mModifier; - bool mEnabled; + int mCurrentPos; + int mLastPos; - std::string mName; - std::string mModName; - SecondaryMode mSecondaryMode; - QKeySequence mSequence; - int mModifier; + ActivationStatus mActivationStatus; + bool mModifierStatus; - int mCurrentPos; - int mLastPos; + QAction* mAction; + QString mActionText; - ActivationStatus mActivationStatus; - bool mModifierStatus; + private slots: - QAction* mAction; - QString mActionText; + void actionDeleted(); - private slots: + signals: - void actionDeleted(); + /// Triggered when the shortcut is activated or deactivated; can be determined from \p state + void activated(bool state); - signals: + /// Convenience signal. + void activated(); - /// Triggered when the shortcut is activated or deactivated; can be determined from \p state - void activated(bool state); + /// Triggered depending on SecondaryMode + void secondary(bool state); - /// Convenience signal. - void activated(); - - /// Triggered depending on SecondaryMode - void secondary(bool state); - - /// Convenience signal. - void secondary(); + /// Convenience signal. + void secondary(); }; } diff --git a/apps/opencs/model/prefs/shortcuteventhandler.cpp b/apps/opencs/model/prefs/shortcuteventhandler.cpp index a4102e1db9b..4bbda2996fb 100644 --- a/apps/opencs/model/prefs/shortcuteventhandler.cpp +++ b/apps/opencs/model/prefs/shortcuteventhandler.cpp @@ -34,7 +34,7 @@ namespace CSMPrefs // Intercept widget events widget->installEventFilter(this); - connect(widget, SIGNAL(destroyed()), this, SLOT(widgetDestroyed())); + connect(widget, &QWidget::destroyed, this, &ShortcutEventHandler::widgetDestroyed); } // Add to list @@ -49,7 +49,9 @@ namespace CSMPrefs ShortcutMap::iterator shortcutListIt = mWidgetShortcuts.find(widget); if (shortcutListIt != mWidgetShortcuts.end()) { - shortcutListIt->second.erase(std::remove(shortcutListIt->second.begin(), shortcutListIt->second.end(), shortcut), shortcutListIt->second.end()); + shortcutListIt->second.erase( + std::remove(shortcutListIt->second.begin(), shortcutListIt->second.end(), shortcut), + shortcutListIt->second.end()); } } @@ -60,8 +62,8 @@ namespace CSMPrefs { QWidget* widget = static_cast(watched); QKeyEvent* keyEvent = static_cast(event); - unsigned int mod = (unsigned int) keyEvent->modifiers(); - unsigned int key = (unsigned int) keyEvent->key(); + unsigned int mod = (unsigned int)keyEvent->modifiers(); + unsigned int key = (unsigned int)keyEvent->key(); if (!keyEvent->isAutoRepeat()) return activate(widget, mod, key); @@ -70,8 +72,8 @@ namespace CSMPrefs { QWidget* widget = static_cast(watched); QKeyEvent* keyEvent = static_cast(event); - unsigned int mod = (unsigned int) keyEvent->modifiers(); - unsigned int key = (unsigned int) keyEvent->key(); + unsigned int mod = (unsigned int)keyEvent->modifiers(); + unsigned int key = (unsigned int)keyEvent->key(); if (!keyEvent->isAutoRepeat()) return deactivate(widget, mod, key); @@ -80,8 +82,8 @@ namespace CSMPrefs { QWidget* widget = static_cast(watched); QMouseEvent* mouseEvent = static_cast(event); - unsigned int mod = (unsigned int) mouseEvent->modifiers(); - unsigned int button = (unsigned int) mouseEvent->button(); + unsigned int mod = (unsigned int)mouseEvent->modifiers(); + unsigned int button = (unsigned int)mouseEvent->button(); return activate(widget, mod, button); } @@ -89,8 +91,8 @@ namespace CSMPrefs { QWidget* widget = static_cast(watched); QMouseEvent* mouseEvent = static_cast(event); - unsigned int mod = (unsigned int) mouseEvent->modifiers(); - unsigned int button = (unsigned int) mouseEvent->button(); + unsigned int mod = (unsigned int)mouseEvent->modifiers(); + unsigned int button = (unsigned int)mouseEvent->button(); return deactivate(widget, mod, button); } @@ -98,6 +100,7 @@ namespace CSMPrefs { QWidget* widget = static_cast(watched); ShortcutMap::iterator shortcutListIt = mWidgetShortcuts.find(widget); + assert(shortcutListIt != mWidgetShortcuts.end()); // Deactivate in case events are missed for (ShortcutList::iterator it = shortcutListIt->second.begin(); it != shortcutListIt->second.end(); ++it) @@ -148,7 +151,7 @@ namespace CSMPrefs bool ShortcutEventHandler::activate(QWidget* widget, unsigned int mod, unsigned int button) { - std::vector > potentials; + std::vector> potentials; bool used = false; while (widget) @@ -178,7 +181,7 @@ namespace CSMPrefs { if (pos < lastPos && (result == Matches_WithMod || pos > 0)) { - shortcut->setPosition(pos+1); + shortcut->setPosition(pos + 1); } else if (pos == lastPos) { @@ -267,8 +270,8 @@ namespace CSMPrefs bool ShortcutEventHandler::checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate) { - if (!shortcut->isEnabled() || !shortcut->getModifier() || shortcut->getSecondaryMode() == Shortcut::SM_Ignore || - shortcut->getModifierStatus() == activate) + if (!shortcut->isEnabled() || !shortcut->getModifier() || shortcut->getSecondaryMode() == Shortcut::SM_Ignore + || shortcut->getModifierStatus() == activate) return false; MatchResult result = match(mod, button, shortcut->getModifier()); @@ -302,8 +305,8 @@ namespace CSMPrefs return used; } - ShortcutEventHandler::MatchResult ShortcutEventHandler::match(unsigned int mod, unsigned int button, - unsigned int value) + ShortcutEventHandler::MatchResult ShortcutEventHandler::match( + unsigned int mod, unsigned int button, unsigned int value) { if ((mod | button) == value) { @@ -319,8 +322,8 @@ namespace CSMPrefs } } - bool ShortcutEventHandler::sort(const std::pair& left, - const std::pair& right) + bool ShortcutEventHandler::sort( + const std::pair& left, const std::pair& right) { if (left.first == Matches_WithMod && right.first == Matches_NoMod) return true; diff --git a/apps/opencs/model/prefs/shortcuteventhandler.hpp b/apps/opencs/model/prefs/shortcuteventhandler.hpp index 700b977fd37..2093e259e90 100644 --- a/apps/opencs/model/prefs/shortcuteventhandler.hpp +++ b/apps/opencs/model/prefs/shortcuteventhandler.hpp @@ -16,53 +16,49 @@ namespace CSMPrefs /// Users of this class should install it as an event handler class ShortcutEventHandler : public QObject { - Q_OBJECT + Q_OBJECT - public: + public: + ShortcutEventHandler(QObject* parent); - ShortcutEventHandler(QObject* parent); + void addShortcut(Shortcut* shortcut); + void removeShortcut(Shortcut* shortcut); - void addShortcut(Shortcut* shortcut); - void removeShortcut(Shortcut* shortcut); + protected: + bool eventFilter(QObject* watched, QEvent* event) override; - protected: + private: + typedef std::vector ShortcutList; + // Child, Parent + typedef std::map WidgetMap; + typedef std::map ShortcutMap; - bool eventFilter(QObject* watched, QEvent* event) override; + enum MatchResult + { + Matches_WithMod, + Matches_NoMod, + Matches_Not + }; - private: + void updateParent(QWidget* widget); - typedef std::vector ShortcutList; - // Child, Parent - typedef std::map WidgetMap; - typedef std::map ShortcutMap; + bool activate(QWidget* widget, unsigned int mod, unsigned int button); - enum MatchResult - { - Matches_WithMod, - Matches_NoMod, - Matches_Not - }; + bool deactivate(QWidget* widget, unsigned int mod, unsigned int button); - void updateParent(QWidget* widget); + bool checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate); - bool activate(QWidget* widget, unsigned int mod, unsigned int button); + MatchResult match(unsigned int mod, unsigned int button, unsigned int value); - bool deactivate(QWidget* widget, unsigned int mod, unsigned int button); + // Prefers Matches_WithMod and a larger number of buttons + static bool sort(const std::pair& left, const std::pair& right); - bool checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate); + WidgetMap mChildParentRelations; + ShortcutMap mWidgetShortcuts; - MatchResult match(unsigned int mod, unsigned int button, unsigned int value); + private slots: - // Prefers Matches_WithMod and a larger number of buttons - static bool sort(const std::pair& left, - const std::pair& right); - - WidgetMap mChildParentRelations; - ShortcutMap mWidgetShortcuts; - - private slots: - - void widgetDestroyed(); + void widgetDestroyed(); }; } diff --git a/apps/opencs/model/prefs/shortcutmanager.cpp b/apps/opencs/model/prefs/shortcutmanager.cpp index 4c5f77900c8..d6686d31d9c 100644 --- a/apps/opencs/model/prefs/shortcutmanager.cpp +++ b/apps/opencs/model/prefs/shortcutmanager.cpp @@ -1,8 +1,9 @@ #include "shortcutmanager.hpp" #include +#include +#include -#include #include #include "shortcut.hpp" @@ -42,7 +43,7 @@ namespace CSMPrefs mEventHandler->removeShortcut(shortcut); } - bool ShortcutManager::getSequence(const std::string& name, QKeySequence& sequence) const + bool ShortcutManager::getSequence(std::string_view name, QKeySequence& sequence) const { SequenceMap::const_iterator item = mSequences.find(name); if (item != mSequences.end()) @@ -55,7 +56,7 @@ namespace CSMPrefs return false; } - void ShortcutManager::setSequence(const std::string& name, const QKeySequence& sequence) + void ShortcutManager::setSequence(std::string_view name, const QKeySequence& sequence) { // Add to map/modify SequenceMap::iterator item = mSequences.find(name); @@ -90,7 +91,7 @@ namespace CSMPrefs return false; } - void ShortcutManager::setModifier(const std::string& name, int modifier) + void ShortcutManager::setModifier(std::string_view name, int modifier) { // Add to map/modify ModifierMap::iterator item = mModifiers.find(name); @@ -342,446 +343,445 @@ namespace CSMPrefs return substrings.join(""); } - const std::pair ShortcutManager::QtKeys[] = - { - std::make_pair((int)Qt::Key_Space , "Space"), - std::make_pair((int)Qt::Key_Exclam , "Exclam"), - std::make_pair((int)Qt::Key_QuoteDbl , "QuoteDbl"), - std::make_pair((int)Qt::Key_NumberSign , "NumberSign"), - std::make_pair((int)Qt::Key_Dollar , "Dollar"), - std::make_pair((int)Qt::Key_Percent , "Percent"), - std::make_pair((int)Qt::Key_Ampersand , "Ampersand"), - std::make_pair((int)Qt::Key_Apostrophe , "Apostrophe"), - std::make_pair((int)Qt::Key_ParenLeft , "ParenLeft"), - std::make_pair((int)Qt::Key_ParenRight , "ParenRight"), - std::make_pair((int)Qt::Key_Asterisk , "Asterisk"), - std::make_pair((int)Qt::Key_Plus , "Plus"), - std::make_pair((int)Qt::Key_Comma , "Comma"), - std::make_pair((int)Qt::Key_Minus , "Minus"), - std::make_pair((int)Qt::Key_Period , "Period"), - std::make_pair((int)Qt::Key_Slash , "Slash"), - std::make_pair((int)Qt::Key_0 , "0"), - std::make_pair((int)Qt::Key_1 , "1"), - std::make_pair((int)Qt::Key_2 , "2"), - std::make_pair((int)Qt::Key_3 , "3"), - std::make_pair((int)Qt::Key_4 , "4"), - std::make_pair((int)Qt::Key_5 , "5"), - std::make_pair((int)Qt::Key_6 , "6"), - std::make_pair((int)Qt::Key_7 , "7"), - std::make_pair((int)Qt::Key_8 , "8"), - std::make_pair((int)Qt::Key_9 , "9"), - std::make_pair((int)Qt::Key_Colon , "Colon"), - std::make_pair((int)Qt::Key_Semicolon , "Semicolon"), - std::make_pair((int)Qt::Key_Less , "Less"), - std::make_pair((int)Qt::Key_Equal , "Equal"), - std::make_pair((int)Qt::Key_Greater , "Greater"), - std::make_pair((int)Qt::Key_Question , "Question"), - std::make_pair((int)Qt::Key_At , "At"), - std::make_pair((int)Qt::Key_A , "A"), - std::make_pair((int)Qt::Key_B , "B"), - std::make_pair((int)Qt::Key_C , "C"), - std::make_pair((int)Qt::Key_D , "D"), - std::make_pair((int)Qt::Key_E , "E"), - std::make_pair((int)Qt::Key_F , "F"), - std::make_pair((int)Qt::Key_G , "G"), - std::make_pair((int)Qt::Key_H , "H"), - std::make_pair((int)Qt::Key_I , "I"), - std::make_pair((int)Qt::Key_J , "J"), - std::make_pair((int)Qt::Key_K , "K"), - std::make_pair((int)Qt::Key_L , "L"), - std::make_pair((int)Qt::Key_M , "M"), - std::make_pair((int)Qt::Key_N , "N"), - std::make_pair((int)Qt::Key_O , "O"), - std::make_pair((int)Qt::Key_P , "P"), - std::make_pair((int)Qt::Key_Q , "Q"), - std::make_pair((int)Qt::Key_R , "R"), - std::make_pair((int)Qt::Key_S , "S"), - std::make_pair((int)Qt::Key_T , "T"), - std::make_pair((int)Qt::Key_U , "U"), - std::make_pair((int)Qt::Key_V , "V"), - std::make_pair((int)Qt::Key_W , "W"), - std::make_pair((int)Qt::Key_X , "X"), - std::make_pair((int)Qt::Key_Y , "Y"), - std::make_pair((int)Qt::Key_Z , "Z"), - std::make_pair((int)Qt::Key_BracketLeft , "BracketLeft"), - std::make_pair((int)Qt::Key_Backslash , "Backslash"), - std::make_pair((int)Qt::Key_BracketRight , "BracketRight"), - std::make_pair((int)Qt::Key_AsciiCircum , "AsciiCircum"), - std::make_pair((int)Qt::Key_Underscore , "Underscore"), - std::make_pair((int)Qt::Key_QuoteLeft , "QuoteLeft"), - std::make_pair((int)Qt::Key_BraceLeft , "BraceLeft"), - std::make_pair((int)Qt::Key_Bar , "Bar"), - std::make_pair((int)Qt::Key_BraceRight , "BraceRight"), - std::make_pair((int)Qt::Key_AsciiTilde , "AsciiTilde"), - std::make_pair((int)Qt::Key_nobreakspace , "nobreakspace"), - std::make_pair((int)Qt::Key_exclamdown , "exclamdown"), - std::make_pair((int)Qt::Key_cent , "cent"), - std::make_pair((int)Qt::Key_sterling , "sterling"), - std::make_pair((int)Qt::Key_currency , "currency"), - std::make_pair((int)Qt::Key_yen , "yen"), - std::make_pair((int)Qt::Key_brokenbar , "brokenbar"), - std::make_pair((int)Qt::Key_section , "section"), - std::make_pair((int)Qt::Key_diaeresis , "diaeresis"), - std::make_pair((int)Qt::Key_copyright , "copyright"), - std::make_pair((int)Qt::Key_ordfeminine , "ordfeminine"), - std::make_pair((int)Qt::Key_guillemotleft , "guillemotleft"), - std::make_pair((int)Qt::Key_notsign , "notsign"), - std::make_pair((int)Qt::Key_hyphen , "hyphen"), - std::make_pair((int)Qt::Key_registered , "registered"), - std::make_pair((int)Qt::Key_macron , "macron"), - std::make_pair((int)Qt::Key_degree , "degree"), - std::make_pair((int)Qt::Key_plusminus , "plusminus"), - std::make_pair((int)Qt::Key_twosuperior , "twosuperior"), - std::make_pair((int)Qt::Key_threesuperior , "threesuperior"), - std::make_pair((int)Qt::Key_acute , "acute"), - std::make_pair((int)Qt::Key_mu , "mu"), - std::make_pair((int)Qt::Key_paragraph , "paragraph"), - std::make_pair((int)Qt::Key_periodcentered , "periodcentered"), - std::make_pair((int)Qt::Key_cedilla , "cedilla"), - std::make_pair((int)Qt::Key_onesuperior , "onesuperior"), - std::make_pair((int)Qt::Key_masculine , "masculine"), - std::make_pair((int)Qt::Key_guillemotright , "guillemotright"), - std::make_pair((int)Qt::Key_onequarter , "onequarter"), - std::make_pair((int)Qt::Key_onehalf , "onehalf"), - std::make_pair((int)Qt::Key_threequarters , "threequarters"), - std::make_pair((int)Qt::Key_questiondown , "questiondown"), - std::make_pair((int)Qt::Key_Agrave , "Agrave"), - std::make_pair((int)Qt::Key_Aacute , "Aacute"), - std::make_pair((int)Qt::Key_Acircumflex , "Acircumflex"), - std::make_pair((int)Qt::Key_Atilde , "Atilde"), - std::make_pair((int)Qt::Key_Adiaeresis , "Adiaeresis"), - std::make_pair((int)Qt::Key_Aring , "Aring"), - std::make_pair((int)Qt::Key_AE , "AE"), - std::make_pair((int)Qt::Key_Ccedilla , "Ccedilla"), - std::make_pair((int)Qt::Key_Egrave , "Egrave"), - std::make_pair((int)Qt::Key_Eacute , "Eacute"), - std::make_pair((int)Qt::Key_Ecircumflex , "Ecircumflex"), - std::make_pair((int)Qt::Key_Ediaeresis , "Ediaeresis"), - std::make_pair((int)Qt::Key_Igrave , "Igrave"), - std::make_pair((int)Qt::Key_Iacute , "Iacute"), - std::make_pair((int)Qt::Key_Icircumflex , "Icircumflex"), - std::make_pair((int)Qt::Key_Idiaeresis , "Idiaeresis"), - std::make_pair((int)Qt::Key_ETH , "ETH"), - std::make_pair((int)Qt::Key_Ntilde , "Ntilde"), - std::make_pair((int)Qt::Key_Ograve , "Ograve"), - std::make_pair((int)Qt::Key_Oacute , "Oacute"), - std::make_pair((int)Qt::Key_Ocircumflex , "Ocircumflex"), - std::make_pair((int)Qt::Key_Otilde , "Otilde"), - std::make_pair((int)Qt::Key_Odiaeresis , "Odiaeresis"), - std::make_pair((int)Qt::Key_multiply , "multiply"), - std::make_pair((int)Qt::Key_Ooblique , "Ooblique"), - std::make_pair((int)Qt::Key_Ugrave , "Ugrave"), - std::make_pair((int)Qt::Key_Uacute , "Uacute"), - std::make_pair((int)Qt::Key_Ucircumflex , "Ucircumflex"), - std::make_pair((int)Qt::Key_Udiaeresis , "Udiaeresis"), - std::make_pair((int)Qt::Key_Yacute , "Yacute"), - std::make_pair((int)Qt::Key_THORN , "THORN"), - std::make_pair((int)Qt::Key_ssharp , "ssharp"), - std::make_pair((int)Qt::Key_division , "division"), - std::make_pair((int)Qt::Key_ydiaeresis , "ydiaeresis"), - std::make_pair((int)Qt::Key_Escape , "Escape"), - std::make_pair((int)Qt::Key_Tab , "Tab"), - std::make_pair((int)Qt::Key_Backtab , "Backtab"), - std::make_pair((int)Qt::Key_Backspace , "Backspace"), - std::make_pair((int)Qt::Key_Return , "Return"), - std::make_pair((int)Qt::Key_Enter , "Enter"), - std::make_pair((int)Qt::Key_Insert , "Insert"), - std::make_pair((int)Qt::Key_Delete , "Delete"), - std::make_pair((int)Qt::Key_Pause , "Pause"), - std::make_pair((int)Qt::Key_Print , "Print"), - std::make_pair((int)Qt::Key_SysReq , "SysReq"), - std::make_pair((int)Qt::Key_Clear , "Clear"), - std::make_pair((int)Qt::Key_Home , "Home"), - std::make_pair((int)Qt::Key_End , "End"), - std::make_pair((int)Qt::Key_Left , "Left"), - std::make_pair((int)Qt::Key_Up , "Up"), - std::make_pair((int)Qt::Key_Right , "Right"), - std::make_pair((int)Qt::Key_Down , "Down"), - std::make_pair((int)Qt::Key_PageUp , "PageUp"), - std::make_pair((int)Qt::Key_PageDown , "PageDown"), - std::make_pair((int)Qt::Key_Shift , "Shift"), - std::make_pair((int)Qt::Key_Control , "Control"), - std::make_pair((int)Qt::Key_Meta , "Meta"), - std::make_pair((int)Qt::Key_Alt , "Alt"), - std::make_pair((int)Qt::Key_CapsLock , "CapsLock"), - std::make_pair((int)Qt::Key_NumLock , "NumLock"), - std::make_pair((int)Qt::Key_ScrollLock , "ScrollLock"), - std::make_pair((int)Qt::Key_F1 , "F1"), - std::make_pair((int)Qt::Key_F2 , "F2"), - std::make_pair((int)Qt::Key_F3 , "F3"), - std::make_pair((int)Qt::Key_F4 , "F4"), - std::make_pair((int)Qt::Key_F5 , "F5"), - std::make_pair((int)Qt::Key_F6 , "F6"), - std::make_pair((int)Qt::Key_F7 , "F7"), - std::make_pair((int)Qt::Key_F8 , "F8"), - std::make_pair((int)Qt::Key_F9 , "F9"), - std::make_pair((int)Qt::Key_F10 , "F10"), - std::make_pair((int)Qt::Key_F11 , "F11"), - std::make_pair((int)Qt::Key_F12 , "F12"), - std::make_pair((int)Qt::Key_F13 , "F13"), - std::make_pair((int)Qt::Key_F14 , "F14"), - std::make_pair((int)Qt::Key_F15 , "F15"), - std::make_pair((int)Qt::Key_F16 , "F16"), - std::make_pair((int)Qt::Key_F17 , "F17"), - std::make_pair((int)Qt::Key_F18 , "F18"), - std::make_pair((int)Qt::Key_F19 , "F19"), - std::make_pair((int)Qt::Key_F20 , "F20"), - std::make_pair((int)Qt::Key_F21 , "F21"), - std::make_pair((int)Qt::Key_F22 , "F22"), - std::make_pair((int)Qt::Key_F23 , "F23"), - std::make_pair((int)Qt::Key_F24 , "F24"), - std::make_pair((int)Qt::Key_F25 , "F25"), - std::make_pair((int)Qt::Key_F26 , "F26"), - std::make_pair((int)Qt::Key_F27 , "F27"), - std::make_pair((int)Qt::Key_F28 , "F28"), - std::make_pair((int)Qt::Key_F29 , "F29"), - std::make_pair((int)Qt::Key_F30 , "F30"), - std::make_pair((int)Qt::Key_F31 , "F31"), - std::make_pair((int)Qt::Key_F32 , "F32"), - std::make_pair((int)Qt::Key_F33 , "F33"), - std::make_pair((int)Qt::Key_F34 , "F34"), - std::make_pair((int)Qt::Key_F35 , "F35"), - std::make_pair((int)Qt::Key_Super_L , "Super_L"), - std::make_pair((int)Qt::Key_Super_R , "Super_R"), - std::make_pair((int)Qt::Key_Menu , "Menu"), - std::make_pair((int)Qt::Key_Hyper_L , "Hyper_L"), - std::make_pair((int)Qt::Key_Hyper_R , "Hyper_R"), - std::make_pair((int)Qt::Key_Help , "Help"), - std::make_pair((int)Qt::Key_Direction_L , "Direction_L"), - std::make_pair((int)Qt::Key_Direction_R , "Direction_R"), - std::make_pair((int)Qt::Key_Back , "Back"), - std::make_pair((int)Qt::Key_Forward , "Forward"), - std::make_pair((int)Qt::Key_Stop , "Stop"), - std::make_pair((int)Qt::Key_Refresh , "Refresh"), - std::make_pair((int)Qt::Key_VolumeDown , "VolumeDown"), - std::make_pair((int)Qt::Key_VolumeMute , "VolumeMute"), - std::make_pair((int)Qt::Key_VolumeUp , "VolumeUp"), - std::make_pair((int)Qt::Key_BassBoost , "BassBoost"), - std::make_pair((int)Qt::Key_BassUp , "BassUp"), - std::make_pair((int)Qt::Key_BassDown , "BassDown"), - std::make_pair((int)Qt::Key_TrebleUp , "TrebleUp"), - std::make_pair((int)Qt::Key_TrebleDown , "TrebleDown"), - std::make_pair((int)Qt::Key_MediaPlay , "MediaPlay"), - std::make_pair((int)Qt::Key_MediaStop , "MediaStop"), - std::make_pair((int)Qt::Key_MediaPrevious , "MediaPrevious"), - std::make_pair((int)Qt::Key_MediaNext , "MediaNext"), - std::make_pair((int)Qt::Key_MediaRecord , "MediaRecord"), - std::make_pair((int)Qt::Key_MediaPause , "MediaPause"), - std::make_pair((int)Qt::Key_MediaTogglePlayPause , "MediaTogglePlayPause"), - std::make_pair((int)Qt::Key_HomePage , "HomePage"), - std::make_pair((int)Qt::Key_Favorites , "Favorites"), - std::make_pair((int)Qt::Key_Search , "Search"), - std::make_pair((int)Qt::Key_Standby , "Standby"), - std::make_pair((int)Qt::Key_OpenUrl , "OpenUrl"), - std::make_pair((int)Qt::Key_LaunchMail , "LaunchMail"), - std::make_pair((int)Qt::Key_LaunchMedia , "LaunchMedia"), - std::make_pair((int)Qt::Key_Launch0 , "Launch0"), - std::make_pair((int)Qt::Key_Launch1 , "Launch1"), - std::make_pair((int)Qt::Key_Launch2 , "Launch2"), - std::make_pair((int)Qt::Key_Launch3 , "Launch3"), - std::make_pair((int)Qt::Key_Launch4 , "Launch4"), - std::make_pair((int)Qt::Key_Launch5 , "Launch5"), - std::make_pair((int)Qt::Key_Launch6 , "Launch6"), - std::make_pair((int)Qt::Key_Launch7 , "Launch7"), - std::make_pair((int)Qt::Key_Launch8 , "Launch8"), - std::make_pair((int)Qt::Key_Launch9 , "Launch9"), - std::make_pair((int)Qt::Key_LaunchA , "LaunchA"), - std::make_pair((int)Qt::Key_LaunchB , "LaunchB"), - std::make_pair((int)Qt::Key_LaunchC , "LaunchC"), - std::make_pair((int)Qt::Key_LaunchD , "LaunchD"), - std::make_pair((int)Qt::Key_LaunchE , "LaunchE"), - std::make_pair((int)Qt::Key_LaunchF , "LaunchF"), - std::make_pair((int)Qt::Key_MonBrightnessUp , "MonBrightnessUp"), - std::make_pair((int)Qt::Key_MonBrightnessDown , "MonBrightnessDown"), - std::make_pair((int)Qt::Key_KeyboardLightOnOff , "KeyboardLightOnOff"), - std::make_pair((int)Qt::Key_KeyboardBrightnessUp , "KeyboardBrightnessUp"), - std::make_pair((int)Qt::Key_KeyboardBrightnessDown , "KeyboardBrightnessDown"), - std::make_pair((int)Qt::Key_PowerOff , "PowerOff"), - std::make_pair((int)Qt::Key_WakeUp , "WakeUp"), - std::make_pair((int)Qt::Key_Eject , "Eject"), - std::make_pair((int)Qt::Key_ScreenSaver , "ScreenSaver"), - std::make_pair((int)Qt::Key_WWW , "WWW"), - std::make_pair((int)Qt::Key_Memo , "Memo"), - std::make_pair((int)Qt::Key_LightBulb , "LightBulb"), - std::make_pair((int)Qt::Key_Shop , "Shop"), - std::make_pair((int)Qt::Key_History , "History"), - std::make_pair((int)Qt::Key_AddFavorite , "AddFavorite"), - std::make_pair((int)Qt::Key_HotLinks , "HotLinks"), - std::make_pair((int)Qt::Key_BrightnessAdjust , "BrightnessAdjust"), - std::make_pair((int)Qt::Key_Finance , "Finance"), - std::make_pair((int)Qt::Key_Community , "Community"), - std::make_pair((int)Qt::Key_AudioRewind , "AudioRewind"), - std::make_pair((int)Qt::Key_BackForward , "BackForward"), - std::make_pair((int)Qt::Key_ApplicationLeft , "ApplicationLeft"), - std::make_pair((int)Qt::Key_ApplicationRight , "ApplicationRight"), - std::make_pair((int)Qt::Key_Book , "Book"), - std::make_pair((int)Qt::Key_CD , "CD"), - std::make_pair((int)Qt::Key_Calculator , "Calculator"), - std::make_pair((int)Qt::Key_ToDoList , "ToDoList"), - std::make_pair((int)Qt::Key_ClearGrab , "ClearGrab"), - std::make_pair((int)Qt::Key_Close , "Close"), - std::make_pair((int)Qt::Key_Copy , "Copy"), - std::make_pair((int)Qt::Key_Cut , "Cut"), - std::make_pair((int)Qt::Key_Display , "Display"), - std::make_pair((int)Qt::Key_DOS , "DOS"), - std::make_pair((int)Qt::Key_Documents , "Documents"), - std::make_pair((int)Qt::Key_Excel , "Excel"), - std::make_pair((int)Qt::Key_Explorer , "Explorer"), - std::make_pair((int)Qt::Key_Game , "Game"), - std::make_pair((int)Qt::Key_Go , "Go"), - std::make_pair((int)Qt::Key_iTouch , "iTouch"), - std::make_pair((int)Qt::Key_LogOff , "LogOff"), - std::make_pair((int)Qt::Key_Market , "Market"), - std::make_pair((int)Qt::Key_Meeting , "Meeting"), - std::make_pair((int)Qt::Key_MenuKB , "MenuKB"), - std::make_pair((int)Qt::Key_MenuPB , "MenuPB"), - std::make_pair((int)Qt::Key_MySites , "MySites"), - std::make_pair((int)Qt::Key_News , "News"), - std::make_pair((int)Qt::Key_OfficeHome , "OfficeHome"), - std::make_pair((int)Qt::Key_Option , "Option"), - std::make_pair((int)Qt::Key_Paste , "Paste"), - std::make_pair((int)Qt::Key_Phone , "Phone"), - std::make_pair((int)Qt::Key_Calendar , "Calendar"), - std::make_pair((int)Qt::Key_Reply , "Reply"), - std::make_pair((int)Qt::Key_Reload , "Reload"), - std::make_pair((int)Qt::Key_RotateWindows , "RotateWindows"), - std::make_pair((int)Qt::Key_RotationPB , "RotationPB"), - std::make_pair((int)Qt::Key_RotationKB , "RotationKB"), - std::make_pair((int)Qt::Key_Save , "Save"), - std::make_pair((int)Qt::Key_Send , "Send"), - std::make_pair((int)Qt::Key_Spell , "Spell"), - std::make_pair((int)Qt::Key_SplitScreen , "SplitScreen"), - std::make_pair((int)Qt::Key_Support , "Support"), - std::make_pair((int)Qt::Key_TaskPane , "TaskPane"), - std::make_pair((int)Qt::Key_Terminal , "Terminal"), - std::make_pair((int)Qt::Key_Tools , "Tools"), - std::make_pair((int)Qt::Key_Travel , "Travel"), - std::make_pair((int)Qt::Key_Video , "Video"), - std::make_pair((int)Qt::Key_Word , "Word"), - std::make_pair((int)Qt::Key_Xfer , "Xfer"), - std::make_pair((int)Qt::Key_ZoomIn , "ZoomIn"), - std::make_pair((int)Qt::Key_ZoomOut , "ZoomOut"), - std::make_pair((int)Qt::Key_Away , "Away"), - std::make_pair((int)Qt::Key_Messenger , "Messenger"), - std::make_pair((int)Qt::Key_WebCam , "WebCam"), - std::make_pair((int)Qt::Key_MailForward , "MailForward"), - std::make_pair((int)Qt::Key_Pictures , "Pictures"), - std::make_pair((int)Qt::Key_Music , "Music"), - std::make_pair((int)Qt::Key_Battery , "Battery"), - std::make_pair((int)Qt::Key_Bluetooth , "Bluetooth"), - std::make_pair((int)Qt::Key_WLAN , "WLAN"), - std::make_pair((int)Qt::Key_UWB , "UWB"), - std::make_pair((int)Qt::Key_AudioForward , "AudioForward"), - std::make_pair((int)Qt::Key_AudioRepeat , "AudioRepeat"), - std::make_pair((int)Qt::Key_AudioRandomPlay , "AudioRandomPlay"), - std::make_pair((int)Qt::Key_Subtitle , "Subtitle"), - std::make_pair((int)Qt::Key_AudioCycleTrack , "AudioCycleTrack"), - std::make_pair((int)Qt::Key_Time , "Time"), - std::make_pair((int)Qt::Key_Hibernate , "Hibernate"), - std::make_pair((int)Qt::Key_View , "View"), - std::make_pair((int)Qt::Key_TopMenu , "TopMenu"), - std::make_pair((int)Qt::Key_PowerDown , "PowerDown"), - std::make_pair((int)Qt::Key_Suspend , "Suspend"), - std::make_pair((int)Qt::Key_ContrastAdjust , "ContrastAdjust"), - std::make_pair((int)Qt::Key_LaunchG , "LaunchG"), - std::make_pair((int)Qt::Key_LaunchH , "LaunchH"), - std::make_pair((int)Qt::Key_TouchpadToggle , "TouchpadToggle"), - std::make_pair((int)Qt::Key_TouchpadOn , "TouchpadOn"), - std::make_pair((int)Qt::Key_TouchpadOff , "TouchpadOff"), - std::make_pair((int)Qt::Key_MicMute , "MicMute"), - std::make_pair((int)Qt::Key_Red , "Red"), - std::make_pair((int)Qt::Key_Green , "Green"), - std::make_pair((int)Qt::Key_Yellow , "Yellow"), - std::make_pair((int)Qt::Key_Blue , "Blue"), - std::make_pair((int)Qt::Key_ChannelUp , "ChannelUp"), - std::make_pair((int)Qt::Key_ChannelDown , "ChannelDown"), - std::make_pair((int)Qt::Key_Guide , "Guide"), - std::make_pair((int)Qt::Key_Info , "Info"), - std::make_pair((int)Qt::Key_Settings , "Settings"), - std::make_pair((int)Qt::Key_MicVolumeUp , "MicVolumeUp"), - std::make_pair((int)Qt::Key_MicVolumeDown , "MicVolumeDown"), - std::make_pair((int)Qt::Key_New , "New"), - std::make_pair((int)Qt::Key_Open , "Open"), - std::make_pair((int)Qt::Key_Find , "Find"), - std::make_pair((int)Qt::Key_Undo , "Undo"), - std::make_pair((int)Qt::Key_Redo , "Redo"), - std::make_pair((int)Qt::Key_AltGr , "AltGr"), - std::make_pair((int)Qt::Key_Multi_key , "Multi_key"), - std::make_pair((int)Qt::Key_Kanji , "Kanji"), - std::make_pair((int)Qt::Key_Muhenkan , "Muhenkan"), - std::make_pair((int)Qt::Key_Henkan , "Henkan"), - std::make_pair((int)Qt::Key_Romaji , "Romaji"), - std::make_pair((int)Qt::Key_Hiragana , "Hiragana"), - std::make_pair((int)Qt::Key_Katakana , "Katakana"), - std::make_pair((int)Qt::Key_Hiragana_Katakana , "Hiragana_Katakana"), - std::make_pair((int)Qt::Key_Zenkaku , "Zenkaku"), - std::make_pair((int)Qt::Key_Hankaku , "Hankaku"), - std::make_pair((int)Qt::Key_Zenkaku_Hankaku , "Zenkaku_Hankaku"), - std::make_pair((int)Qt::Key_Touroku , "Touroku"), - std::make_pair((int)Qt::Key_Massyo , "Massyo"), - std::make_pair((int)Qt::Key_Kana_Lock , "Kana_Lock"), - std::make_pair((int)Qt::Key_Kana_Shift , "Kana_Shift"), - std::make_pair((int)Qt::Key_Eisu_Shift , "Eisu_Shift"), - std::make_pair((int)Qt::Key_Eisu_toggle , "Eisu_toggle"), - std::make_pair((int)Qt::Key_Hangul , "Hangul"), - std::make_pair((int)Qt::Key_Hangul_Start , "Hangul_Start"), - std::make_pair((int)Qt::Key_Hangul_End , "Hangul_End"), - std::make_pair((int)Qt::Key_Hangul_Hanja , "Hangul_Hanja"), - std::make_pair((int)Qt::Key_Hangul_Jamo , "Hangul_Jamo"), - std::make_pair((int)Qt::Key_Hangul_Romaja , "Hangul_Romaja"), - std::make_pair((int)Qt::Key_Codeinput , "Codeinput"), - std::make_pair((int)Qt::Key_Hangul_Jeonja , "Hangul_Jeonja"), - std::make_pair((int)Qt::Key_Hangul_Banja , "Hangul_Banja"), - std::make_pair((int)Qt::Key_Hangul_PreHanja , "Hangul_PreHanja"), - std::make_pair((int)Qt::Key_Hangul_PostHanja , "Hangul_PostHanja"), - std::make_pair((int)Qt::Key_SingleCandidate , "SingleCandidate"), - std::make_pair((int)Qt::Key_MultipleCandidate , "MultipleCandidate"), - std::make_pair((int)Qt::Key_PreviousCandidate , "PreviousCandidate"), - std::make_pair((int)Qt::Key_Hangul_Special , "Hangul_Special"), - std::make_pair((int)Qt::Key_Mode_switch , "Mode_switch"), - std::make_pair((int)Qt::Key_Dead_Grave , "Dead_Grave"), - std::make_pair((int)Qt::Key_Dead_Acute , "Dead_Acute"), - std::make_pair((int)Qt::Key_Dead_Circumflex , "Dead_Circumflex"), - std::make_pair((int)Qt::Key_Dead_Tilde , "Dead_Tilde"), - std::make_pair((int)Qt::Key_Dead_Macron , "Dead_Macron"), - std::make_pair((int)Qt::Key_Dead_Breve , "Dead_Breve"), - std::make_pair((int)Qt::Key_Dead_Abovedot , "Dead_Abovedot"), - std::make_pair((int)Qt::Key_Dead_Diaeresis , "Dead_Diaeresis"), - std::make_pair((int)Qt::Key_Dead_Abovering , "Dead_Abovering"), - std::make_pair((int)Qt::Key_Dead_Doubleacute , "Dead_Doubleacute"), - std::make_pair((int)Qt::Key_Dead_Caron , "Dead_Caron"), - std::make_pair((int)Qt::Key_Dead_Cedilla , "Dead_Cedilla"), - std::make_pair((int)Qt::Key_Dead_Ogonek , "Dead_Ogonek"), - std::make_pair((int)Qt::Key_Dead_Iota , "Dead_Iota"), - std::make_pair((int)Qt::Key_Dead_Voiced_Sound , "Dead_Voiced_Sound"), - std::make_pair((int)Qt::Key_Dead_Semivoiced_Sound , "Dead_Semivoiced_Sound"), - std::make_pair((int)Qt::Key_Dead_Belowdot , "Dead_Belowdot"), - std::make_pair((int)Qt::Key_Dead_Hook , "Dead_Hook"), - std::make_pair((int)Qt::Key_Dead_Horn , "Dead_Horn"), - std::make_pair((int)Qt::Key_MediaLast , "MediaLast"), - std::make_pair((int)Qt::Key_Select , "Select"), - std::make_pair((int)Qt::Key_Yes , "Yes"), - std::make_pair((int)Qt::Key_No , "No"), - std::make_pair((int)Qt::Key_Cancel , "Cancel"), - std::make_pair((int)Qt::Key_Printer , "Printer"), - std::make_pair((int)Qt::Key_Execute , "Execute"), - std::make_pair((int)Qt::Key_Sleep , "Sleep"), - std::make_pair((int)Qt::Key_Play , "Play"), - std::make_pair((int)Qt::Key_Zoom , "Zoom"), - std::make_pair((int)Qt::Key_Exit , "Exit"), - std::make_pair((int)Qt::Key_Context1 , "Context1"), - std::make_pair((int)Qt::Key_Context2 , "Context2"), - std::make_pair((int)Qt::Key_Context3 , "Context3"), - std::make_pair((int)Qt::Key_Context4 , "Context4"), - std::make_pair((int)Qt::Key_Call , "Call"), - std::make_pair((int)Qt::Key_Hangup , "Hangup"), - std::make_pair((int)Qt::Key_Flip , "Flip"), - std::make_pair((int)Qt::Key_ToggleCallHangup , "ToggleCallHangup"), - std::make_pair((int)Qt::Key_VoiceDial , "VoiceDial"), - std::make_pair((int)Qt::Key_LastNumberRedial , "LastNumberRedial"), - std::make_pair((int)Qt::Key_Camera , "Camera"), - std::make_pair((int)Qt::Key_CameraFocus , "CameraFocus"), - std::make_pair(0 , (const char*) nullptr) + const std::pair ShortcutManager::QtKeys[] = { + std::make_pair((int)Qt::Key_Space, "Space"), + std::make_pair((int)Qt::Key_Exclam, "Exclam"), + std::make_pair((int)Qt::Key_QuoteDbl, "QuoteDbl"), + std::make_pair((int)Qt::Key_NumberSign, "NumberSign"), + std::make_pair((int)Qt::Key_Dollar, "Dollar"), + std::make_pair((int)Qt::Key_Percent, "Percent"), + std::make_pair((int)Qt::Key_Ampersand, "Ampersand"), + std::make_pair((int)Qt::Key_Apostrophe, "Apostrophe"), + std::make_pair((int)Qt::Key_ParenLeft, "ParenLeft"), + std::make_pair((int)Qt::Key_ParenRight, "ParenRight"), + std::make_pair((int)Qt::Key_Asterisk, "Asterisk"), + std::make_pair((int)Qt::Key_Plus, "Plus"), + std::make_pair((int)Qt::Key_Comma, "Comma"), + std::make_pair((int)Qt::Key_Minus, "Minus"), + std::make_pair((int)Qt::Key_Period, "Period"), + std::make_pair((int)Qt::Key_Slash, "Slash"), + std::make_pair((int)Qt::Key_0, "0"), + std::make_pair((int)Qt::Key_1, "1"), + std::make_pair((int)Qt::Key_2, "2"), + std::make_pair((int)Qt::Key_3, "3"), + std::make_pair((int)Qt::Key_4, "4"), + std::make_pair((int)Qt::Key_5, "5"), + std::make_pair((int)Qt::Key_6, "6"), + std::make_pair((int)Qt::Key_7, "7"), + std::make_pair((int)Qt::Key_8, "8"), + std::make_pair((int)Qt::Key_9, "9"), + std::make_pair((int)Qt::Key_Colon, "Colon"), + std::make_pair((int)Qt::Key_Semicolon, "Semicolon"), + std::make_pair((int)Qt::Key_Less, "Less"), + std::make_pair((int)Qt::Key_Equal, "Equal"), + std::make_pair((int)Qt::Key_Greater, "Greater"), + std::make_pair((int)Qt::Key_Question, "Question"), + std::make_pair((int)Qt::Key_At, "At"), + std::make_pair((int)Qt::Key_A, "A"), + std::make_pair((int)Qt::Key_B, "B"), + std::make_pair((int)Qt::Key_C, "C"), + std::make_pair((int)Qt::Key_D, "D"), + std::make_pair((int)Qt::Key_E, "E"), + std::make_pair((int)Qt::Key_F, "F"), + std::make_pair((int)Qt::Key_G, "G"), + std::make_pair((int)Qt::Key_H, "H"), + std::make_pair((int)Qt::Key_I, "I"), + std::make_pair((int)Qt::Key_J, "J"), + std::make_pair((int)Qt::Key_K, "K"), + std::make_pair((int)Qt::Key_L, "L"), + std::make_pair((int)Qt::Key_M, "M"), + std::make_pair((int)Qt::Key_N, "N"), + std::make_pair((int)Qt::Key_O, "O"), + std::make_pair((int)Qt::Key_P, "P"), + std::make_pair((int)Qt::Key_Q, "Q"), + std::make_pair((int)Qt::Key_R, "R"), + std::make_pair((int)Qt::Key_S, "S"), + std::make_pair((int)Qt::Key_T, "T"), + std::make_pair((int)Qt::Key_U, "U"), + std::make_pair((int)Qt::Key_V, "V"), + std::make_pair((int)Qt::Key_W, "W"), + std::make_pair((int)Qt::Key_X, "X"), + std::make_pair((int)Qt::Key_Y, "Y"), + std::make_pair((int)Qt::Key_Z, "Z"), + std::make_pair((int)Qt::Key_BracketLeft, "BracketLeft"), + std::make_pair((int)Qt::Key_Backslash, "Backslash"), + std::make_pair((int)Qt::Key_BracketRight, "BracketRight"), + std::make_pair((int)Qt::Key_AsciiCircum, "AsciiCircum"), + std::make_pair((int)Qt::Key_Underscore, "Underscore"), + std::make_pair((int)Qt::Key_QuoteLeft, "QuoteLeft"), + std::make_pair((int)Qt::Key_BraceLeft, "BraceLeft"), + std::make_pair((int)Qt::Key_Bar, "Bar"), + std::make_pair((int)Qt::Key_BraceRight, "BraceRight"), + std::make_pair((int)Qt::Key_AsciiTilde, "AsciiTilde"), + std::make_pair((int)Qt::Key_nobreakspace, "nobreakspace"), + std::make_pair((int)Qt::Key_exclamdown, "exclamdown"), + std::make_pair((int)Qt::Key_cent, "cent"), + std::make_pair((int)Qt::Key_sterling, "sterling"), + std::make_pair((int)Qt::Key_currency, "currency"), + std::make_pair((int)Qt::Key_yen, "yen"), + std::make_pair((int)Qt::Key_brokenbar, "brokenbar"), + std::make_pair((int)Qt::Key_section, "section"), + std::make_pair((int)Qt::Key_diaeresis, "diaeresis"), + std::make_pair((int)Qt::Key_copyright, "copyright"), + std::make_pair((int)Qt::Key_ordfeminine, "ordfeminine"), + std::make_pair((int)Qt::Key_guillemotleft, "guillemotleft"), + std::make_pair((int)Qt::Key_notsign, "notsign"), + std::make_pair((int)Qt::Key_hyphen, "hyphen"), + std::make_pair((int)Qt::Key_registered, "registered"), + std::make_pair((int)Qt::Key_macron, "macron"), + std::make_pair((int)Qt::Key_degree, "degree"), + std::make_pair((int)Qt::Key_plusminus, "plusminus"), + std::make_pair((int)Qt::Key_twosuperior, "twosuperior"), + std::make_pair((int)Qt::Key_threesuperior, "threesuperior"), + std::make_pair((int)Qt::Key_acute, "acute"), + std::make_pair((int)Qt::Key_mu, "mu"), + std::make_pair((int)Qt::Key_paragraph, "paragraph"), + std::make_pair((int)Qt::Key_periodcentered, "periodcentered"), + std::make_pair((int)Qt::Key_cedilla, "cedilla"), + std::make_pair((int)Qt::Key_onesuperior, "onesuperior"), + std::make_pair((int)Qt::Key_masculine, "masculine"), + std::make_pair((int)Qt::Key_guillemotright, "guillemotright"), + std::make_pair((int)Qt::Key_onequarter, "onequarter"), + std::make_pair((int)Qt::Key_onehalf, "onehalf"), + std::make_pair((int)Qt::Key_threequarters, "threequarters"), + std::make_pair((int)Qt::Key_questiondown, "questiondown"), + std::make_pair((int)Qt::Key_Agrave, "Agrave"), + std::make_pair((int)Qt::Key_Aacute, "Aacute"), + std::make_pair((int)Qt::Key_Acircumflex, "Acircumflex"), + std::make_pair((int)Qt::Key_Atilde, "Atilde"), + std::make_pair((int)Qt::Key_Adiaeresis, "Adiaeresis"), + std::make_pair((int)Qt::Key_Aring, "Aring"), + std::make_pair((int)Qt::Key_AE, "AE"), + std::make_pair((int)Qt::Key_Ccedilla, "Ccedilla"), + std::make_pair((int)Qt::Key_Egrave, "Egrave"), + std::make_pair((int)Qt::Key_Eacute, "Eacute"), + std::make_pair((int)Qt::Key_Ecircumflex, "Ecircumflex"), + std::make_pair((int)Qt::Key_Ediaeresis, "Ediaeresis"), + std::make_pair((int)Qt::Key_Igrave, "Igrave"), + std::make_pair((int)Qt::Key_Iacute, "Iacute"), + std::make_pair((int)Qt::Key_Icircumflex, "Icircumflex"), + std::make_pair((int)Qt::Key_Idiaeresis, "Idiaeresis"), + std::make_pair((int)Qt::Key_ETH, "ETH"), + std::make_pair((int)Qt::Key_Ntilde, "Ntilde"), + std::make_pair((int)Qt::Key_Ograve, "Ograve"), + std::make_pair((int)Qt::Key_Oacute, "Oacute"), + std::make_pair((int)Qt::Key_Ocircumflex, "Ocircumflex"), + std::make_pair((int)Qt::Key_Otilde, "Otilde"), + std::make_pair((int)Qt::Key_Odiaeresis, "Odiaeresis"), + std::make_pair((int)Qt::Key_multiply, "multiply"), + std::make_pair((int)Qt::Key_Ooblique, "Ooblique"), + std::make_pair((int)Qt::Key_Ugrave, "Ugrave"), + std::make_pair((int)Qt::Key_Uacute, "Uacute"), + std::make_pair((int)Qt::Key_Ucircumflex, "Ucircumflex"), + std::make_pair((int)Qt::Key_Udiaeresis, "Udiaeresis"), + std::make_pair((int)Qt::Key_Yacute, "Yacute"), + std::make_pair((int)Qt::Key_THORN, "THORN"), + std::make_pair((int)Qt::Key_ssharp, "ssharp"), + std::make_pair((int)Qt::Key_division, "division"), + std::make_pair((int)Qt::Key_ydiaeresis, "ydiaeresis"), + std::make_pair((int)Qt::Key_Escape, "Escape"), + std::make_pair((int)Qt::Key_Tab, "Tab"), + std::make_pair((int)Qt::Key_Backtab, "Backtab"), + std::make_pair((int)Qt::Key_Backspace, "Backspace"), + std::make_pair((int)Qt::Key_Return, "Return"), + std::make_pair((int)Qt::Key_Enter, "Enter"), + std::make_pair((int)Qt::Key_Insert, "Insert"), + std::make_pair((int)Qt::Key_Delete, "Delete"), + std::make_pair((int)Qt::Key_Pause, "Pause"), + std::make_pair((int)Qt::Key_Print, "Print"), + std::make_pair((int)Qt::Key_SysReq, "SysReq"), + std::make_pair((int)Qt::Key_Clear, "Clear"), + std::make_pair((int)Qt::Key_Home, "Home"), + std::make_pair((int)Qt::Key_End, "End"), + std::make_pair((int)Qt::Key_Left, "Left"), + std::make_pair((int)Qt::Key_Up, "Up"), + std::make_pair((int)Qt::Key_Right, "Right"), + std::make_pair((int)Qt::Key_Down, "Down"), + std::make_pair((int)Qt::Key_PageUp, "PageUp"), + std::make_pair((int)Qt::Key_PageDown, "PageDown"), + std::make_pair((int)Qt::Key_Shift, "Shift"), + std::make_pair((int)Qt::Key_Control, "Control"), + std::make_pair((int)Qt::Key_Meta, "Meta"), + std::make_pair((int)Qt::Key_Alt, "Alt"), + std::make_pair((int)Qt::Key_CapsLock, "CapsLock"), + std::make_pair((int)Qt::Key_NumLock, "NumLock"), + std::make_pair((int)Qt::Key_ScrollLock, "ScrollLock"), + std::make_pair((int)Qt::Key_F1, "F1"), + std::make_pair((int)Qt::Key_F2, "F2"), + std::make_pair((int)Qt::Key_F3, "F3"), + std::make_pair((int)Qt::Key_F4, "F4"), + std::make_pair((int)Qt::Key_F5, "F5"), + std::make_pair((int)Qt::Key_F6, "F6"), + std::make_pair((int)Qt::Key_F7, "F7"), + std::make_pair((int)Qt::Key_F8, "F8"), + std::make_pair((int)Qt::Key_F9, "F9"), + std::make_pair((int)Qt::Key_F10, "F10"), + std::make_pair((int)Qt::Key_F11, "F11"), + std::make_pair((int)Qt::Key_F12, "F12"), + std::make_pair((int)Qt::Key_F13, "F13"), + std::make_pair((int)Qt::Key_F14, "F14"), + std::make_pair((int)Qt::Key_F15, "F15"), + std::make_pair((int)Qt::Key_F16, "F16"), + std::make_pair((int)Qt::Key_F17, "F17"), + std::make_pair((int)Qt::Key_F18, "F18"), + std::make_pair((int)Qt::Key_F19, "F19"), + std::make_pair((int)Qt::Key_F20, "F20"), + std::make_pair((int)Qt::Key_F21, "F21"), + std::make_pair((int)Qt::Key_F22, "F22"), + std::make_pair((int)Qt::Key_F23, "F23"), + std::make_pair((int)Qt::Key_F24, "F24"), + std::make_pair((int)Qt::Key_F25, "F25"), + std::make_pair((int)Qt::Key_F26, "F26"), + std::make_pair((int)Qt::Key_F27, "F27"), + std::make_pair((int)Qt::Key_F28, "F28"), + std::make_pair((int)Qt::Key_F29, "F29"), + std::make_pair((int)Qt::Key_F30, "F30"), + std::make_pair((int)Qt::Key_F31, "F31"), + std::make_pair((int)Qt::Key_F32, "F32"), + std::make_pair((int)Qt::Key_F33, "F33"), + std::make_pair((int)Qt::Key_F34, "F34"), + std::make_pair((int)Qt::Key_F35, "F35"), + std::make_pair((int)Qt::Key_Super_L, "Super_L"), + std::make_pair((int)Qt::Key_Super_R, "Super_R"), + std::make_pair((int)Qt::Key_Menu, "Menu"), + std::make_pair((int)Qt::Key_Hyper_L, "Hyper_L"), + std::make_pair((int)Qt::Key_Hyper_R, "Hyper_R"), + std::make_pair((int)Qt::Key_Help, "Help"), + std::make_pair((int)Qt::Key_Direction_L, "Direction_L"), + std::make_pair((int)Qt::Key_Direction_R, "Direction_R"), + std::make_pair((int)Qt::Key_Back, "Back"), + std::make_pair((int)Qt::Key_Forward, "Forward"), + std::make_pair((int)Qt::Key_Stop, "Stop"), + std::make_pair((int)Qt::Key_Refresh, "Refresh"), + std::make_pair((int)Qt::Key_VolumeDown, "VolumeDown"), + std::make_pair((int)Qt::Key_VolumeMute, "VolumeMute"), + std::make_pair((int)Qt::Key_VolumeUp, "VolumeUp"), + std::make_pair((int)Qt::Key_BassBoost, "BassBoost"), + std::make_pair((int)Qt::Key_BassUp, "BassUp"), + std::make_pair((int)Qt::Key_BassDown, "BassDown"), + std::make_pair((int)Qt::Key_TrebleUp, "TrebleUp"), + std::make_pair((int)Qt::Key_TrebleDown, "TrebleDown"), + std::make_pair((int)Qt::Key_MediaPlay, "MediaPlay"), + std::make_pair((int)Qt::Key_MediaStop, "MediaStop"), + std::make_pair((int)Qt::Key_MediaPrevious, "MediaPrevious"), + std::make_pair((int)Qt::Key_MediaNext, "MediaNext"), + std::make_pair((int)Qt::Key_MediaRecord, "MediaRecord"), + std::make_pair((int)Qt::Key_MediaPause, "MediaPause"), + std::make_pair((int)Qt::Key_MediaTogglePlayPause, "MediaTogglePlayPause"), + std::make_pair((int)Qt::Key_HomePage, "HomePage"), + std::make_pair((int)Qt::Key_Favorites, "Favorites"), + std::make_pair((int)Qt::Key_Search, "Search"), + std::make_pair((int)Qt::Key_Standby, "Standby"), + std::make_pair((int)Qt::Key_OpenUrl, "OpenUrl"), + std::make_pair((int)Qt::Key_LaunchMail, "LaunchMail"), + std::make_pair((int)Qt::Key_LaunchMedia, "LaunchMedia"), + std::make_pair((int)Qt::Key_Launch0, "Launch0"), + std::make_pair((int)Qt::Key_Launch1, "Launch1"), + std::make_pair((int)Qt::Key_Launch2, "Launch2"), + std::make_pair((int)Qt::Key_Launch3, "Launch3"), + std::make_pair((int)Qt::Key_Launch4, "Launch4"), + std::make_pair((int)Qt::Key_Launch5, "Launch5"), + std::make_pair((int)Qt::Key_Launch6, "Launch6"), + std::make_pair((int)Qt::Key_Launch7, "Launch7"), + std::make_pair((int)Qt::Key_Launch8, "Launch8"), + std::make_pair((int)Qt::Key_Launch9, "Launch9"), + std::make_pair((int)Qt::Key_LaunchA, "LaunchA"), + std::make_pair((int)Qt::Key_LaunchB, "LaunchB"), + std::make_pair((int)Qt::Key_LaunchC, "LaunchC"), + std::make_pair((int)Qt::Key_LaunchD, "LaunchD"), + std::make_pair((int)Qt::Key_LaunchE, "LaunchE"), + std::make_pair((int)Qt::Key_LaunchF, "LaunchF"), + std::make_pair((int)Qt::Key_MonBrightnessUp, "MonBrightnessUp"), + std::make_pair((int)Qt::Key_MonBrightnessDown, "MonBrightnessDown"), + std::make_pair((int)Qt::Key_KeyboardLightOnOff, "KeyboardLightOnOff"), + std::make_pair((int)Qt::Key_KeyboardBrightnessUp, "KeyboardBrightnessUp"), + std::make_pair((int)Qt::Key_KeyboardBrightnessDown, "KeyboardBrightnessDown"), + std::make_pair((int)Qt::Key_PowerOff, "PowerOff"), + std::make_pair((int)Qt::Key_WakeUp, "WakeUp"), + std::make_pair((int)Qt::Key_Eject, "Eject"), + std::make_pair((int)Qt::Key_ScreenSaver, "ScreenSaver"), + std::make_pair((int)Qt::Key_WWW, "WWW"), + std::make_pair((int)Qt::Key_Memo, "Memo"), + std::make_pair((int)Qt::Key_LightBulb, "LightBulb"), + std::make_pair((int)Qt::Key_Shop, "Shop"), + std::make_pair((int)Qt::Key_History, "History"), + std::make_pair((int)Qt::Key_AddFavorite, "AddFavorite"), + std::make_pair((int)Qt::Key_HotLinks, "HotLinks"), + std::make_pair((int)Qt::Key_BrightnessAdjust, "BrightnessAdjust"), + std::make_pair((int)Qt::Key_Finance, "Finance"), + std::make_pair((int)Qt::Key_Community, "Community"), + std::make_pair((int)Qt::Key_AudioRewind, "AudioRewind"), + std::make_pair((int)Qt::Key_BackForward, "BackForward"), + std::make_pair((int)Qt::Key_ApplicationLeft, "ApplicationLeft"), + std::make_pair((int)Qt::Key_ApplicationRight, "ApplicationRight"), + std::make_pair((int)Qt::Key_Book, "Book"), + std::make_pair((int)Qt::Key_CD, "CD"), + std::make_pair((int)Qt::Key_Calculator, "Calculator"), + std::make_pair((int)Qt::Key_ToDoList, "ToDoList"), + std::make_pair((int)Qt::Key_ClearGrab, "ClearGrab"), + std::make_pair((int)Qt::Key_Close, "Close"), + std::make_pair((int)Qt::Key_Copy, "Copy"), + std::make_pair((int)Qt::Key_Cut, "Cut"), + std::make_pair((int)Qt::Key_Display, "Display"), + std::make_pair((int)Qt::Key_DOS, "DOS"), + std::make_pair((int)Qt::Key_Documents, "Documents"), + std::make_pair((int)Qt::Key_Excel, "Excel"), + std::make_pair((int)Qt::Key_Explorer, "Explorer"), + std::make_pair((int)Qt::Key_Game, "Game"), + std::make_pair((int)Qt::Key_Go, "Go"), + std::make_pair((int)Qt::Key_iTouch, "iTouch"), + std::make_pair((int)Qt::Key_LogOff, "LogOff"), + std::make_pair((int)Qt::Key_Market, "Market"), + std::make_pair((int)Qt::Key_Meeting, "Meeting"), + std::make_pair((int)Qt::Key_MenuKB, "MenuKB"), + std::make_pair((int)Qt::Key_MenuPB, "MenuPB"), + std::make_pair((int)Qt::Key_MySites, "MySites"), + std::make_pair((int)Qt::Key_News, "News"), + std::make_pair((int)Qt::Key_OfficeHome, "OfficeHome"), + std::make_pair((int)Qt::Key_Option, "Option"), + std::make_pair((int)Qt::Key_Paste, "Paste"), + std::make_pair((int)Qt::Key_Phone, "Phone"), + std::make_pair((int)Qt::Key_Calendar, "Calendar"), + std::make_pair((int)Qt::Key_Reply, "Reply"), + std::make_pair((int)Qt::Key_Reload, "Reload"), + std::make_pair((int)Qt::Key_RotateWindows, "RotateWindows"), + std::make_pair((int)Qt::Key_RotationPB, "RotationPB"), + std::make_pair((int)Qt::Key_RotationKB, "RotationKB"), + std::make_pair((int)Qt::Key_Save, "Save"), + std::make_pair((int)Qt::Key_Send, "Send"), + std::make_pair((int)Qt::Key_Spell, "Spell"), + std::make_pair((int)Qt::Key_SplitScreen, "SplitScreen"), + std::make_pair((int)Qt::Key_Support, "Support"), + std::make_pair((int)Qt::Key_TaskPane, "TaskPane"), + std::make_pair((int)Qt::Key_Terminal, "Terminal"), + std::make_pair((int)Qt::Key_Tools, "Tools"), + std::make_pair((int)Qt::Key_Travel, "Travel"), + std::make_pair((int)Qt::Key_Video, "Video"), + std::make_pair((int)Qt::Key_Word, "Word"), + std::make_pair((int)Qt::Key_Xfer, "Xfer"), + std::make_pair((int)Qt::Key_ZoomIn, "ZoomIn"), + std::make_pair((int)Qt::Key_ZoomOut, "ZoomOut"), + std::make_pair((int)Qt::Key_Away, "Away"), + std::make_pair((int)Qt::Key_Messenger, "Messenger"), + std::make_pair((int)Qt::Key_WebCam, "WebCam"), + std::make_pair((int)Qt::Key_MailForward, "MailForward"), + std::make_pair((int)Qt::Key_Pictures, "Pictures"), + std::make_pair((int)Qt::Key_Music, "Music"), + std::make_pair((int)Qt::Key_Battery, "Battery"), + std::make_pair((int)Qt::Key_Bluetooth, "Bluetooth"), + std::make_pair((int)Qt::Key_WLAN, "WLAN"), + std::make_pair((int)Qt::Key_UWB, "UWB"), + std::make_pair((int)Qt::Key_AudioForward, "AudioForward"), + std::make_pair((int)Qt::Key_AudioRepeat, "AudioRepeat"), + std::make_pair((int)Qt::Key_AudioRandomPlay, "AudioRandomPlay"), + std::make_pair((int)Qt::Key_Subtitle, "Subtitle"), + std::make_pair((int)Qt::Key_AudioCycleTrack, "AudioCycleTrack"), + std::make_pair((int)Qt::Key_Time, "Time"), + std::make_pair((int)Qt::Key_Hibernate, "Hibernate"), + std::make_pair((int)Qt::Key_View, "View"), + std::make_pair((int)Qt::Key_TopMenu, "TopMenu"), + std::make_pair((int)Qt::Key_PowerDown, "PowerDown"), + std::make_pair((int)Qt::Key_Suspend, "Suspend"), + std::make_pair((int)Qt::Key_ContrastAdjust, "ContrastAdjust"), + std::make_pair((int)Qt::Key_LaunchG, "LaunchG"), + std::make_pair((int)Qt::Key_LaunchH, "LaunchH"), + std::make_pair((int)Qt::Key_TouchpadToggle, "TouchpadToggle"), + std::make_pair((int)Qt::Key_TouchpadOn, "TouchpadOn"), + std::make_pair((int)Qt::Key_TouchpadOff, "TouchpadOff"), + std::make_pair((int)Qt::Key_MicMute, "MicMute"), + std::make_pair((int)Qt::Key_Red, "Red"), + std::make_pair((int)Qt::Key_Green, "Green"), + std::make_pair((int)Qt::Key_Yellow, "Yellow"), + std::make_pair((int)Qt::Key_Blue, "Blue"), + std::make_pair((int)Qt::Key_ChannelUp, "ChannelUp"), + std::make_pair((int)Qt::Key_ChannelDown, "ChannelDown"), + std::make_pair((int)Qt::Key_Guide, "Guide"), + std::make_pair((int)Qt::Key_Info, "Info"), + std::make_pair((int)Qt::Key_Settings, "Settings"), + std::make_pair((int)Qt::Key_MicVolumeUp, "MicVolumeUp"), + std::make_pair((int)Qt::Key_MicVolumeDown, "MicVolumeDown"), + std::make_pair((int)Qt::Key_New, "New"), + std::make_pair((int)Qt::Key_Open, "Open"), + std::make_pair((int)Qt::Key_Find, "Find"), + std::make_pair((int)Qt::Key_Undo, "Undo"), + std::make_pair((int)Qt::Key_Redo, "Redo"), + std::make_pair((int)Qt::Key_AltGr, "AltGr"), + std::make_pair((int)Qt::Key_Multi_key, "Multi_key"), + std::make_pair((int)Qt::Key_Kanji, "Kanji"), + std::make_pair((int)Qt::Key_Muhenkan, "Muhenkan"), + std::make_pair((int)Qt::Key_Henkan, "Henkan"), + std::make_pair((int)Qt::Key_Romaji, "Romaji"), + std::make_pair((int)Qt::Key_Hiragana, "Hiragana"), + std::make_pair((int)Qt::Key_Katakana, "Katakana"), + std::make_pair((int)Qt::Key_Hiragana_Katakana, "Hiragana_Katakana"), + std::make_pair((int)Qt::Key_Zenkaku, "Zenkaku"), + std::make_pair((int)Qt::Key_Hankaku, "Hankaku"), + std::make_pair((int)Qt::Key_Zenkaku_Hankaku, "Zenkaku_Hankaku"), + std::make_pair((int)Qt::Key_Touroku, "Touroku"), + std::make_pair((int)Qt::Key_Massyo, "Massyo"), + std::make_pair((int)Qt::Key_Kana_Lock, "Kana_Lock"), + std::make_pair((int)Qt::Key_Kana_Shift, "Kana_Shift"), + std::make_pair((int)Qt::Key_Eisu_Shift, "Eisu_Shift"), + std::make_pair((int)Qt::Key_Eisu_toggle, "Eisu_toggle"), + std::make_pair((int)Qt::Key_Hangul, "Hangul"), + std::make_pair((int)Qt::Key_Hangul_Start, "Hangul_Start"), + std::make_pair((int)Qt::Key_Hangul_End, "Hangul_End"), + std::make_pair((int)Qt::Key_Hangul_Hanja, "Hangul_Hanja"), + std::make_pair((int)Qt::Key_Hangul_Jamo, "Hangul_Jamo"), + std::make_pair((int)Qt::Key_Hangul_Romaja, "Hangul_Romaja"), + std::make_pair((int)Qt::Key_Codeinput, "Codeinput"), + std::make_pair((int)Qt::Key_Hangul_Jeonja, "Hangul_Jeonja"), + std::make_pair((int)Qt::Key_Hangul_Banja, "Hangul_Banja"), + std::make_pair((int)Qt::Key_Hangul_PreHanja, "Hangul_PreHanja"), + std::make_pair((int)Qt::Key_Hangul_PostHanja, "Hangul_PostHanja"), + std::make_pair((int)Qt::Key_SingleCandidate, "SingleCandidate"), + std::make_pair((int)Qt::Key_MultipleCandidate, "MultipleCandidate"), + std::make_pair((int)Qt::Key_PreviousCandidate, "PreviousCandidate"), + std::make_pair((int)Qt::Key_Hangul_Special, "Hangul_Special"), + std::make_pair((int)Qt::Key_Mode_switch, "Mode_switch"), + std::make_pair((int)Qt::Key_Dead_Grave, "Dead_Grave"), + std::make_pair((int)Qt::Key_Dead_Acute, "Dead_Acute"), + std::make_pair((int)Qt::Key_Dead_Circumflex, "Dead_Circumflex"), + std::make_pair((int)Qt::Key_Dead_Tilde, "Dead_Tilde"), + std::make_pair((int)Qt::Key_Dead_Macron, "Dead_Macron"), + std::make_pair((int)Qt::Key_Dead_Breve, "Dead_Breve"), + std::make_pair((int)Qt::Key_Dead_Abovedot, "Dead_Abovedot"), + std::make_pair((int)Qt::Key_Dead_Diaeresis, "Dead_Diaeresis"), + std::make_pair((int)Qt::Key_Dead_Abovering, "Dead_Abovering"), + std::make_pair((int)Qt::Key_Dead_Doubleacute, "Dead_Doubleacute"), + std::make_pair((int)Qt::Key_Dead_Caron, "Dead_Caron"), + std::make_pair((int)Qt::Key_Dead_Cedilla, "Dead_Cedilla"), + std::make_pair((int)Qt::Key_Dead_Ogonek, "Dead_Ogonek"), + std::make_pair((int)Qt::Key_Dead_Iota, "Dead_Iota"), + std::make_pair((int)Qt::Key_Dead_Voiced_Sound, "Dead_Voiced_Sound"), + std::make_pair((int)Qt::Key_Dead_Semivoiced_Sound, "Dead_Semivoiced_Sound"), + std::make_pair((int)Qt::Key_Dead_Belowdot, "Dead_Belowdot"), + std::make_pair((int)Qt::Key_Dead_Hook, "Dead_Hook"), + std::make_pair((int)Qt::Key_Dead_Horn, "Dead_Horn"), + std::make_pair((int)Qt::Key_MediaLast, "MediaLast"), + std::make_pair((int)Qt::Key_Select, "Select"), + std::make_pair((int)Qt::Key_Yes, "Yes"), + std::make_pair((int)Qt::Key_No, "No"), + std::make_pair((int)Qt::Key_Cancel, "Cancel"), + std::make_pair((int)Qt::Key_Printer, "Printer"), + std::make_pair((int)Qt::Key_Execute, "Execute"), + std::make_pair((int)Qt::Key_Sleep, "Sleep"), + std::make_pair((int)Qt::Key_Play, "Play"), + std::make_pair((int)Qt::Key_Zoom, "Zoom"), + std::make_pair((int)Qt::Key_Exit, "Exit"), + std::make_pair((int)Qt::Key_Context1, "Context1"), + std::make_pair((int)Qt::Key_Context2, "Context2"), + std::make_pair((int)Qt::Key_Context3, "Context3"), + std::make_pair((int)Qt::Key_Context4, "Context4"), + std::make_pair((int)Qt::Key_Call, "Call"), + std::make_pair((int)Qt::Key_Hangup, "Hangup"), + std::make_pair((int)Qt::Key_Flip, "Flip"), + std::make_pair((int)Qt::Key_ToggleCallHangup, "ToggleCallHangup"), + std::make_pair((int)Qt::Key_VoiceDial, "VoiceDial"), + std::make_pair((int)Qt::Key_LastNumberRedial, "LastNumberRedial"), + std::make_pair((int)Qt::Key_Camera, "Camera"), + std::make_pair((int)Qt::Key_CameraFocus, "CameraFocus"), + std::make_pair(0, static_cast(nullptr)), }; } diff --git a/apps/opencs/model/prefs/shortcutmanager.hpp b/apps/opencs/model/prefs/shortcutmanager.hpp index 99f01a5dffd..0cfe3ad86a7 100644 --- a/apps/opencs/model/prefs/shortcutmanager.hpp +++ b/apps/opencs/model/prefs/shortcutmanager.hpp @@ -2,6 +2,8 @@ #define CSM_PREFS_SHORTCUTMANAGER_H #include +#include +#include #include #include @@ -15,58 +17,56 @@ namespace CSMPrefs /// Class used to track and update shortcuts/sequences class ShortcutManager : public QObject { - Q_OBJECT + Q_OBJECT - public: + public: + ShortcutManager(); - ShortcutManager(); + /// The shortcut class will do this automatically + void addShortcut(Shortcut* shortcut); - /// The shortcut class will do this automatically - void addShortcut(Shortcut* shortcut); + /// The shortcut class will do this automatically + void removeShortcut(Shortcut* shortcut); - /// The shortcut class will do this automatically - void removeShortcut(Shortcut* shortcut); + bool getSequence(std::string_view name, QKeySequence& sequence) const; + void setSequence(std::string_view name, const QKeySequence& sequence); - bool getSequence(const std::string& name, QKeySequence& sequence) const; - void setSequence(const std::string& name, const QKeySequence& sequence); + bool getModifier(const std::string& name, int& modifier) const; + void setModifier(std::string_view name, int modifier); - bool getModifier(const std::string& name, int& modifier) const; - void setModifier(const std::string& name, int modifier); + std::string convertToString(const QKeySequence& sequence) const; + std::string convertToString(int modifier) const; - std::string convertToString(const QKeySequence& sequence) const; - std::string convertToString(int modifier) const; + std::string convertToString(const QKeySequence& sequence, int modifier) const; - 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(const std::string& data, QKeySequence& sequence) const; - void convertFromString(const std::string& data, int& modifier) const; + void convertFromString(const std::string& data, QKeySequence& sequence, int& modifier) const; - void convertFromString(const std::string& data, QKeySequence& sequence, int& modifier) const; + /// Replaces "{sequence-name}" or "{modifier-name}" with the appropriate text + QString processToolTip(const QString& toolTip) const; - /// Replaces "{sequence-name}" or "{modifier-name}" with the appropriate text - QString processToolTip(const QString& toolTip) const; + private: + // Need a multimap in case multiple shortcuts share the same name + typedef std::multimap> ShortcutMap; + typedef std::map> SequenceMap; + typedef std::map> ModifierMap; + typedef std::map NameMap; + typedef std::map KeyMap; - private: + ShortcutMap mShortcuts; + SequenceMap mSequences; + ModifierMap mModifiers; - // Need a multimap in case multiple shortcuts share the same name - typedef std::multimap ShortcutMap; - typedef std::map SequenceMap; - typedef std::map ModifierMap; - typedef std::map NameMap; - typedef std::map KeyMap; + NameMap mNames; + KeyMap mKeys; - ShortcutMap mShortcuts; - SequenceMap mSequences; - ModifierMap mModifiers; + ShortcutEventHandler* mEventHandler; - NameMap mNames; - KeyMap mKeys; + void createLookupTables(); - ShortcutEventHandler* mEventHandler; - - void createLookupTables(); - - static const std::pair QtKeys[]; + static const std::pair QtKeys[]; }; } diff --git a/apps/opencs/model/prefs/shortcutsetting.cpp b/apps/opencs/model/prefs/shortcutsetting.cpp index e0cd5bb07b7..bdaf3a0fdad 100644 --- a/apps/opencs/model/prefs/shortcutsetting.cpp +++ b/apps/opencs/model/prefs/shortcutsetting.cpp @@ -5,17 +5,22 @@ #include #include #include -#include #include +#include + +#include + +#include +#include -#include "state.hpp" #include "shortcutmanager.hpp" +#include "state.hpp" namespace CSMPrefs { - ShortcutSetting::ShortcutSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, - const std::string& label) - : Setting(parent, values, mutex, key, label) + ShortcutSetting::ShortcutSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) , mButton(nullptr) , mEditorActive(false) , mEditorPos(0) @@ -26,14 +31,14 @@ namespace CSMPrefs } } - std::pair ShortcutSetting::makeWidgets(QWidget* parent) + SettingWidgets ShortcutSetting::makeWidgets(QWidget* parent) { QKeySequence sequence; State::get().getShortcutManager().getSequence(getKey(), sequence); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(sequence).c_str()); - QLabel* label = new QLabel(QString::fromUtf8(getLabel().c_str()), parent); + QLabel* label = new QLabel(getLabel(), parent); QPushButton* widget = new QPushButton(text, parent); widget->setCheckable(true); @@ -44,16 +49,16 @@ namespace CSMPrefs mButton = widget; - connect(widget, SIGNAL(toggled(bool)), this, SLOT(buttonToggled(bool))); + connect(widget, &QPushButton::toggled, this, &ShortcutSetting::buttonToggled); - return std::make_pair(label, widget); + return SettingWidgets{ .mLabel = label, .mInput = widget }; } void ShortcutSetting::updateWidget() { if (mButton) { - std::string shortcut = getValues().getString(getKey(), getParent()->getKey()); + const std::string shortcut = getValue(); QKeySequence sequence; State::get().getShortcutManager().convertFromString(shortcut, sequence); @@ -113,14 +118,7 @@ namespace CSMPrefs bool ShortcutSetting::handleEvent(QObject* target, int mod, int value, bool active) { // Modifiers are handled differently - const int Blacklist[] = - { - Qt::Key_Shift, - Qt::Key_Control, - Qt::Key_Meta, - Qt::Key_Alt, - Qt::Key_AltGr - }; + const int Blacklist[] = { Qt::Key_Shift, Qt::Key_Control, Qt::Key_Meta, Qt::Key_Alt, Qt::Key_AltGr }; const size_t BlacklistSize = sizeof(Blacklist) / sizeof(int); @@ -173,15 +171,7 @@ namespace CSMPrefs void ShortcutSetting::storeValue(const QKeySequence& sequence) { State::get().getShortcutManager().setSequence(getKey(), sequence); - - // Convert to string and assign - std::string value = State::get().getShortcutManager().convertToString(sequence); - - { - QMutexLocker lock(getMutex()); - getValues().setString(getKey(), getParent()->getKey(), value); - } - + setValue(State::get().getShortcutManager().convertToString(sequence)); getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/shortcutsetting.hpp b/apps/opencs/model/prefs/shortcutsetting.hpp index 52298232e7a..bcb7b89488d 100644 --- a/apps/opencs/model/prefs/shortcutsetting.hpp +++ b/apps/opencs/model/prefs/shortcutsetting.hpp @@ -1,50 +1,56 @@ #ifndef CSM_PREFS_SHORTCUTSETTING_H #define CSM_PREFS_SHORTCUTSETTING_H +#include +#include +#include + #include #include "setting.hpp" class QEvent; +class QMutex; +class QObject; class QPushButton; +class QWidget; namespace CSMPrefs { - class ShortcutSetting : public Setting - { - Q_OBJECT - - public: + class Category; - ShortcutSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, - const std::string& label); - - std::pair makeWidgets(QWidget* parent) override; + class ShortcutSetting final : public TypedSetting + { + Q_OBJECT - void updateWidget() override; + public: + explicit ShortcutSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - protected: + SettingWidgets makeWidgets(QWidget* parent) override; - bool eventFilter(QObject* target, QEvent* event) override; + void updateWidget() override; - private: + protected: + bool eventFilter(QObject* target, QEvent* event) override; - bool handleEvent(QObject* target, int mod, int value, bool active); + private: + bool handleEvent(QObject* target, int mod, int value, bool active); - void storeValue(const QKeySequence& sequence); - void resetState(); + void storeValue(const QKeySequence& sequence); + void resetState(); - static constexpr int MaxKeys = 4; + static constexpr int MaxKeys = 4; - QPushButton* mButton; + QPushButton* mButton; - bool mEditorActive; - int mEditorPos; - int mEditorKeys[MaxKeys]; + bool mEditorActive; + int mEditorPos; + int mEditorKeys[MaxKeys]; - private slots: + private slots: - void buttonToggled(bool checked); + void buttonToggled(bool checked); }; } diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 0958fa8d4df..7b3b827583c 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -1,655 +1,575 @@ - #include "state.hpp" -#include -#include +#include +#include + +#include #include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include -#include "intsetting.hpp" -#include "doublesetting.hpp" #include "boolsetting.hpp" #include "coloursetting.hpp" -#include "shortcutsetting.hpp" +#include "doublesetting.hpp" +#include "intsetting.hpp" #include "modifiersetting.hpp" +#include "shortcutsetting.hpp" +#include "stringsetting.hpp" +#include "values.hpp" -CSMPrefs::State *CSMPrefs::State::sThis = nullptr; - -void CSMPrefs::State::load() -{ - // default settings file - boost::filesystem::path local = mConfigurationManager.getLocalPath() / mConfigFile; - boost::filesystem::path global = mConfigurationManager.getGlobalPath() / mConfigFile; - - if (boost::filesystem::exists (local)) - mSettings.loadDefault (local.string()); - else if (boost::filesystem::exists (global)) - mSettings.loadDefault (global.string()); - else - throw std::runtime_error ("No default settings file found! Make sure the file \"openmw-cs.cfg\" was properly installed."); - - // user settings file - boost::filesystem::path user = mConfigurationManager.getUserConfigPath() / mConfigFile; - - if (boost::filesystem::exists (user)) - mSettings.loadUser (user.string()); -} +CSMPrefs::State* CSMPrefs::State::sThis = nullptr; void CSMPrefs::State::declare() { - declareCategory ("Windows"); - declareInt ("default-width", "Default window width", 800). - setTooltip ("Newly opened top-level windows will open with this width."). - setMin (80); - declareInt ("default-height", "Default window height", 600). - setTooltip ("Newly opened top-level windows will open with this height."). - setMin (80); - declareBool ("show-statusbar", "Show Status Bar", true). - setTooltip ("If a newly open top level window is showing status bars or not. " - " Note that this does not affect existing windows."); - declareSeparator(); - declareBool ("reuse", "Reuse Subviews", true). - setTooltip ("When a new subview is requested and a matching subview already " - " exist, do not open a new subview and use the existing one instead."); - declareInt ("max-subviews", "Maximum number of subviews per top-level window", 256). - setTooltip ("If the maximum number is reached and a new subview is opened " - "it will be placed into a new top-level window."). - setRange (1, 256); - declareBool ("hide-subview", "Hide single subview", false). - setTooltip ("When a view contains only a single subview, hide the subview title " - "bar and if this subview is closed also close the view (unless it is the last " - "view for this document)"); - declareInt ("minimum-width", "Minimum subview width", 325). - setTooltip ("Minimum width of subviews."). - setRange (50, 10000); - declareSeparator(); - EnumValue scrollbarOnly ("Scrollbar Only", "Simple addition of scrollbars, the view window " - "does not grow automatically."); - declareEnum ("mainwindow-scrollbar", "Horizontal scrollbar mode for main window.", scrollbarOnly). - addValue (scrollbarOnly). - addValue ("Grow Only", "The view window grows as subviews are added. No scrollbars."). - addValue ("Grow then Scroll", "The view window grows. The scrollbar appears once it cannot grow any further."); - declareBool ("grow-limit", "Grow Limit Screen", false). - setTooltip ("When \"Grow then Scroll\" option is selected, the window size grows to" - " the width of the virtual desktop. \nIf this option is selected the the window growth" - "is limited to the current screen."); - - declareCategory ("Records"); - EnumValue iconAndText ("Icon and Text"); - EnumValues recordValues; - recordValues.add (iconAndText).add ("Icon Only").add ("Text Only"); - declareEnum ("status-format", "Modification status display format", iconAndText). - addValues (recordValues); - declareEnum ("type-format", "ID type display format", iconAndText). - addValues (recordValues); - - declareCategory ("ID Tables"); - EnumValue inPlaceEdit ("Edit in Place", "Edit the clicked cell"); - EnumValue editRecord ("Edit Record", "Open a dialogue subview for the clicked record"); - EnumValue view ("View", "Open a scene subview for the clicked record (not available everywhere)"); - EnumValue editRecordAndClose ("Edit Record and Close"); - EnumValues doubleClickValues; - doubleClickValues.add (inPlaceEdit).add (editRecord).add (view).add ("Revert"). - add ("Delete").add (editRecordAndClose). - add ("View and Close", "Open a scene subview for the clicked record and close the table subview"); - declareEnum ("double", "Double Click", inPlaceEdit).addValues (doubleClickValues); - declareEnum ("double-s", "Shift Double Click", editRecord).addValues (doubleClickValues); - declareEnum ("double-c", "Control Double Click", view).addValues (doubleClickValues); - declareEnum ("double-sc", "Shift Control Double Click", editRecordAndClose).addValues (doubleClickValues); - declareSeparator(); - EnumValue jumpAndSelect ("Jump and Select", "Scroll new record into view and make it the selection"); - declareEnum ("jump-to-added", "Action on adding or cloning a record", jumpAndSelect). - addValue (jumpAndSelect). - addValue ("Jump Only", "Scroll new record into view"). - addValue ("No Jump", "No special action"); - declareBool ("extended-config", - "Manually specify affected record types for an extended delete/revert", false). - setTooltip ("Delete and revert commands have an extended form that also affects " - "associated records.\n\n" - "If this option is enabled, types of affected records are selected " - "manually before a command execution.\nOtherwise, all associated " - "records are deleted/reverted immediately."); - - declareCategory ("ID Dialogues"); - declareBool ("toolbar", "Show toolbar", true); - - declareCategory ("Reports"); - EnumValue actionNone ("None"); - EnumValue actionEdit ("Edit", "Open a table or dialogue suitable for addressing the listed report"); - EnumValue actionRemove ("Remove", "Remove the report from the report table"); - EnumValue actionEditAndRemove ("Edit And Remove", "Open a table or dialogue suitable for addressing the listed report, then remove the report from the report table"); - EnumValues reportValues; - reportValues.add (actionNone).add (actionEdit).add (actionRemove).add (actionEditAndRemove); - declareEnum ("double", "Double Click", actionEdit).addValues (reportValues); - declareEnum ("double-s", "Shift Double Click", actionRemove).addValues (reportValues); - declareEnum ("double-c", "Control Double Click", actionEditAndRemove).addValues (reportValues); - declareEnum ("double-sc", "Shift Control Double Click", actionNone).addValues (reportValues); - declareBool("ignore-base-records", "Ignore base records in verifier", false); - - declareCategory ("Search & Replace"); - declareInt ("char-before", "Characters before search string", 10). - setTooltip ("Maximum number of character to display in search result before the searched text"); - declareInt ("char-after", "Characters after search string", 10). - setTooltip ("Maximum number of character to display in search result after the searched text"); - declareBool ("auto-delete", "Delete row from result table after a successful replace", true); - - declareCategory ("Scripts"); - declareBool ("show-linenum", "Show Line Numbers", true). - setTooltip ("Show line numbers to the left of the script editor window." - "The current row and column numbers of the text cursor are shown at the bottom."); - declareBool ("wrap-lines", "Wrap Lines", false). - setTooltip ("Wrap lines longer than width of script editor."); - declareBool ("mono-font", "Use monospace font", true); - declareInt ("tab-width", "Tab Width", 4). - setTooltip ("Number of characters for tab width"). - setRange (1, 10); - EnumValue warningsNormal ("Normal", "Report warnings as warning"); - declareEnum ("warnings", "Warning Mode", warningsNormal). - addValue ("Ignore", "Do not report warning"). - addValue (warningsNormal). - addValue ("Strict", "Promote warning to an error"); - declareBool ("toolbar", "Show toolbar", true); - declareInt ("compile-delay", "Delay between updating of source errors", 100). - setTooltip ("Delay in milliseconds"). - setRange (0, 10000); - declareInt ("error-height", "Initial height of the error panel", 100). - setRange (100, 10000); - declareBool ("highlight-occurrences", "Highlight other occurrences of selected names", true); - declareColour ("colour-highlight", "Colour of highlighted occurrences", QColor("lightcyan")); - declareSeparator(); - declareColour ("colour-int", "Highlight Colour: Integer Literals", QColor ("darkmagenta")); - declareColour ("colour-float", "Highlight Colour: Float Literals", QColor ("magenta")); - declareColour ("colour-name", "Highlight Colour: Names", QColor ("grey")); - declareColour ("colour-keyword", "Highlight Colour: Keywords", QColor ("red")); - declareColour ("colour-special", "Highlight Colour: Special Characters", QColor ("darkorange")); - declareColour ("colour-comment", "Highlight Colour: Comments", QColor ("green")); - declareColour ("colour-id", "Highlight Colour: IDs", QColor ("blue")); - - declareCategory ("General Input"); - declareBool ("cycle", "Cyclic next/previous", false). - setTooltip ("When using next/previous functions at the last/first item of a " - "list go to the first/last item"); - - declareCategory ("3D Scene Input"); - - declareDouble ("navi-wheel-factor", "Camera Zoom Sensitivity", 8).setRange(-100.0, 100.0); - declareDouble ("s-navi-sensitivity", "Secondary Camera Movement Sensitivity", 50.0).setRange(-1000.0, 1000.0); - declareSeparator(); - - declareDouble ("p-navi-free-sensitivity", "Free Camera Sensitivity", 1/650.).setPrecision(5).setRange(0.0, 1.0); - declareBool ("p-navi-free-invert", "Invert Free Camera Mouse Input", false); - declareDouble ("navi-free-lin-speed", "Free Camera Linear Speed", 1000.0).setRange(1.0, 10000.0); - declareDouble ("navi-free-rot-speed", "Free Camera Rotational Speed", 3.14 / 2).setRange(0.001, 6.28); - declareDouble ("navi-free-speed-mult", "Free Camera Speed Multiplier (from Modifier)", 8).setRange(0.001, 1000.0); - declareSeparator(); - - declareDouble ("p-navi-orbit-sensitivity", "Orbit Camera Sensitivity", 1/650.).setPrecision(5).setRange(0.0, 1.0); - declareBool ("p-navi-orbit-invert", "Invert Orbit Camera Mouse Input", false); - declareDouble ("navi-orbit-rot-speed", "Orbital Camera Rotational Speed", 3.14 / 4).setRange(0.001, 6.28); - declareDouble ("navi-orbit-speed-mult", "Orbital Camera Speed Multiplier (from Modifier)", 4).setRange(0.001, 1000.0); - declareBool ("navi-orbit-const-roll", "Keep camera roll constant for orbital camera", true); - declareSeparator(); - - declareBool ("context-select", "Context Sensitive Selection", false); - declareDouble ("drag-factor", "Mouse sensitivity during drag operations", 1.0). - setRange (0.001, 100.0); - declareDouble ("drag-wheel-factor", "Mouse wheel sensitivity during drag operations", 1.0). - setRange (0.001, 100.0); - declareDouble ("drag-shift-factor", - "Shift-acceleration factor during drag operations", 4.0). - setTooltip ("Acceleration factor during drag operations while holding down shift"). - setRange (0.001, 100.0); - declareDouble ("rotate-factor", "Free rotation factor", 0.007).setPrecision(4).setRange(0.0001, 0.1); - - declareCategory ("Rendering"); - declareInt ("framerate-limit", "FPS limit", 60). - setTooltip("Framerate limit in 3D preview windows. Zero value means \"unlimited\"."). - setRange(0, 10000); - declareInt ("camera-fov", "Camera FOV", 90).setRange(10, 170); - declareBool ("camera-ortho", "Orthographic projection for camera", false); - declareInt ("camera-ortho-size", "Orthographic projection size parameter", 100). - setTooltip("Size of the orthographic frustum, greater value will allow the camera to see more of the world."). - setRange(10, 10000); - declareDouble ("object-marker-alpha", "Object Marker Transparency", 0.5).setPrecision(2).setRange(0,1); - declareBool("scene-use-gradient", "Use Gradient Background", true); - declareColour ("scene-day-background-colour", "Day Background Colour", QColor (110, 120, 128, 255)); - declareColour ("scene-day-gradient-colour", "Day Gradient Colour", QColor (47, 51, 51, 255)). - setTooltip("Sets the gradient color to use in conjunction with the day background color. Ignored if " - "the gradient option is disabled."); - declareColour ("scene-bright-background-colour", "Scene Bright Background Colour", QColor (79, 87, 92, 255)); - declareColour ("scene-bright-gradient-colour", "Scene Bright Gradient Colour", QColor (47, 51, 51, 255)). - setTooltip("Sets the gradient color to use in conjunction with the bright background color. Ignored if " + declareCategory("Windows"); + declareInt(mValues->mWindows.mDefaultWidth, "Default window width") + .setTooltip("Newly opened top-level windows will open with this width.") + .setMin(80); + declareInt(mValues->mWindows.mDefaultHeight, "Default window height") + .setTooltip("Newly opened top-level windows will open with this height.") + .setMin(80); + declareBool(mValues->mWindows.mShowStatusbar, "Show Status Bar") + .setTooltip( + "If a newly open top level window is showing status bars or not. " + " Note that this does not affect existing windows."); + declareBool(mValues->mWindows.mReuse, "Reuse Subviews") + .setTooltip( + "When a new subview is requested and a matching subview already " + " exist, do not open a new subview and use the existing one instead."); + declareInt(mValues->mWindows.mMaxSubviews, "Maximum number of subviews per top-level window") + .setTooltip( + "If the maximum number is reached and a new subview is opened " + "it will be placed into a new top-level window.") + .setRange(1, 256); + declareBool(mValues->mWindows.mHideSubview, "Hide single subview") + .setTooltip( + "When a view contains only a single subview, hide the subview title " + "bar and if this subview is closed also close the view (unless it is the last " + "view for this document)"); + declareInt(mValues->mWindows.mMinimumWidth, "Minimum subview width") + .setTooltip("Minimum width of subviews.") + .setRange(50, 10000); + declareEnum(mValues->mWindows.mMainwindowScrollbar, "Horizontal scrollbar mode for main window."); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + declareBool(mValues->mWindows.mGrowLimit, "Grow Limit Screen") + .setTooltip( + "When \"Grow then Scroll\" option is selected, the window size grows to" + " the width of the virtual desktop. \nIf this option is selected the the window growth" + "is limited to the current screen."); +#endif + + declareCategory("Records"); + declareEnum(mValues->mRecords.mStatusFormat, "Modification status display format"); + declareEnum(mValues->mRecords.mTypeFormat, "ID type display format"); + + declareCategory("ID Tables"); + declareEnum(mValues->mIdTables.mDouble, "Double Click"); + declareEnum(mValues->mIdTables.mDoubleS, "Shift Double Click"); + declareEnum(mValues->mIdTables.mDoubleC, "Control Double Click"); + declareEnum(mValues->mIdTables.mDoubleSc, "Shift Control Double Click"); + declareEnum(mValues->mIdTables.mJumpToAdded, "Action on adding or cloning a record"); + declareBool( + mValues->mIdTables.mExtendedConfig, "Manually specify affected record types for an extended delete/revert") + .setTooltip( + "Delete and revert commands have an extended form that also affects " + "associated records.\n\n" + "If this option is enabled, types of affected records are selected " + "manually before a command execution.\nOtherwise, all associated " + "records are deleted/reverted immediately."); + declareBool(mValues->mIdTables.mSubviewNewWindow, "Open Record in new window") + .setTooltip( + "When editing a record, open the view in a new window," + " rather than docked in the main view."); + declareInt(mValues->mIdTables.mFilterDelay, "Delay before applying a filter (in miliseconds)"); + + declareCategory("ID Dialogues"); + declareBool(mValues->mIdDialogues.mToolbar, "Show toolbar"); + + declareCategory("Reports"); + declareEnum(mValues->mReports.mDouble, "Double Click"); + declareEnum(mValues->mReports.mDoubleS, "Shift Double Click"); + declareEnum(mValues->mReports.mDoubleC, "Control Double Click"); + declareEnum(mValues->mReports.mDoubleSc, "Shift Control Double Click"); + declareBool(mValues->mReports.mIgnoreBaseRecords, "Ignore base records in verifier"); + + declareCategory("Search & Replace"); + declareInt(mValues->mSearchAndReplace.mCharBefore, "Characters before search string") + .setTooltip("Maximum number of character to display in search result before the searched text"); + declareInt(mValues->mSearchAndReplace.mCharAfter, "Characters after search string") + .setTooltip("Maximum number of character to display in search result after the searched text"); + declareBool(mValues->mSearchAndReplace.mAutoDelete, "Delete row from result table after a successful replace"); + + declareCategory("Scripts"); + declareBool(mValues->mScripts.mShowLinenum, "Show Line Numbers") + .setTooltip( + "Show line numbers to the left of the script editor window." + "The current row and column numbers of the text cursor are shown at the bottom."); + declareBool(mValues->mScripts.mWrapLines, "Wrap Lines") + .setTooltip("Wrap lines longer than width of script editor."); + declareBool(mValues->mScripts.mMonoFont, "Use monospace font"); + declareInt(mValues->mScripts.mTabWidth, "Tab Width") + .setTooltip("Number of characters for tab width") + .setRange(1, 10); + declareEnum(mValues->mScripts.mWarnings, "Warning Mode"); + declareBool(mValues->mScripts.mToolbar, "Show toolbar"); + declareInt(mValues->mScripts.mCompileDelay, "Delay between updating of source errors") + .setTooltip("Delay in milliseconds") + .setRange(0, 10000); + declareInt(mValues->mScripts.mErrorHeight, "Initial height of the error panel").setRange(100, 10000); + declareBool(mValues->mScripts.mHighlightOccurrences, "Highlight other occurrences of selected names"); + declareColour(mValues->mScripts.mColourHighlight, "Colour of highlighted occurrences"); + declareColour(mValues->mScripts.mColourInt, "Highlight Colour: Integer Literals"); + declareColour(mValues->mScripts.mColourFloat, "Highlight Colour: Float Literals"); + declareColour(mValues->mScripts.mColourName, "Highlight Colour: Names"); + declareColour(mValues->mScripts.mColourKeyword, "Highlight Colour: Keywords"); + declareColour(mValues->mScripts.mColourSpecial, "Highlight Colour: Special Characters"); + declareColour(mValues->mScripts.mColourComment, "Highlight Colour: Comments"); + declareColour(mValues->mScripts.mColourId, "Highlight Colour: IDs"); + + declareCategory("General Input"); + declareBool(mValues->mGeneralInput.mCycle, "Cyclic next/previous") + .setTooltip( + "When using next/previous functions at the last/first item of a " + "list go to the first/last item"); + + declareCategory("3D Scene Input"); + + declareDouble(mValues->mSceneInput.mNaviWheelFactor, "Camera Zoom Sensitivity").setRange(-100.0, 100.0); + declareDouble(mValues->mSceneInput.mSNaviSensitivity, "Secondary Camera Movement Sensitivity") + .setRange(-1000.0, 1000.0); + + declareDouble(mValues->mSceneInput.mPNaviFreeSensitivity, "Free Camera Sensitivity") + .setPrecision(5) + .setRange(0.0, 1.0); + declareBool(mValues->mSceneInput.mPNaviFreeInvert, "Invert Free Camera Mouse Input"); + declareDouble(mValues->mSceneInput.mNaviFreeLinSpeed, "Free Camera Linear Speed").setRange(1.0, 10000.0); + declareDouble(mValues->mSceneInput.mNaviFreeRotSpeed, "Free Camera Rotational Speed").setRange(0.001, 6.28); + declareDouble(mValues->mSceneInput.mNaviFreeSpeedMult, "Free Camera Speed Multiplier (from Modifier)") + .setRange(0.001, 1000.0); + + declareDouble(mValues->mSceneInput.mPNaviOrbitSensitivity, "Orbit Camera Sensitivity") + .setPrecision(5) + .setRange(0.0, 1.0); + declareBool(mValues->mSceneInput.mPNaviOrbitInvert, "Invert Orbit Camera Mouse Input"); + declareDouble(mValues->mSceneInput.mNaviOrbitRotSpeed, "Orbital Camera Rotational Speed").setRange(0.001, 6.28); + declareDouble(mValues->mSceneInput.mNaviOrbitSpeedMult, "Orbital Camera Speed Multiplier (from Modifier)") + .setRange(0.001, 1000.0); + declareBool(mValues->mSceneInput.mNaviOrbitConstRoll, "Keep camera roll constant for orbital camera"); + + declareBool(mValues->mSceneInput.mContextSelect, "Context Sensitive Selection"); + declareDouble(mValues->mSceneInput.mDragFactor, "Mouse sensitivity during drag operations").setRange(0.001, 100.0); + declareDouble(mValues->mSceneInput.mDragWheelFactor, "Mouse wheel sensitivity during drag operations") + .setRange(0.001, 100.0); + declareDouble(mValues->mSceneInput.mDragShiftFactor, "Shift-acceleration factor during drag operations") + .setTooltip("Acceleration factor during drag operations while holding down shift") + .setRange(0.001, 100.0); + declareDouble(mValues->mSceneInput.mRotateFactor, "Free rotation factor").setPrecision(4).setRange(0.0001, 0.1); + + declareCategory("Rendering"); + declareInt(mValues->mRendering.mFramerateLimit, "FPS limit") + .setTooltip("Framerate limit in 3D preview windows. Zero value means \"unlimited\".") + .setRange(0, 10000); + declareInt(mValues->mRendering.mCameraFov, "Camera FOV").setRange(10, 170); + declareBool(mValues->mRendering.mCameraOrtho, "Orthographic projection for camera"); + 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); + declareBool(mValues->mRendering.mSceneUseGradient, "Use Gradient Background"); + declareColour(mValues->mRendering.mSceneDayBackgroundColour, "Day Background Colour"); + declareColour(mValues->mRendering.mSceneDayGradientColour, "Day Gradient Colour") + .setTooltip( + "Sets the gradient color to use in conjunction with the day background color. Ignored if " "the gradient option is disabled."); - declareColour ("scene-night-background-colour", "Scene Night Background Colour", QColor (64, 77, 79, 255)); - declareColour ("scene-night-gradient-colour", "Scene Night Gradient Colour", QColor (47, 51, 51, 255)). - setTooltip("Sets the gradient color to use in conjunction with the night background color. Ignored if " + declareColour(mValues->mRendering.mSceneBrightBackgroundColour, "Scene Bright Background Colour"); + declareColour(mValues->mRendering.mSceneBrightGradientColour, "Scene Bright Gradient Colour") + .setTooltip( + "Sets the gradient color to use in conjunction with the bright background color. Ignored if " "the gradient option is disabled."); - - declareCategory ("Tooltips"); - declareBool ("scene", "Show Tooltips in 3D scenes", true); - declareBool ("scene-hide-basic", "Hide basic 3D scenes tooltips", false); - declareInt ("scene-delay", "Tooltip delay in milliseconds", 500). - setMin (1); - - EnumValue createAndInsert ("Create cell and insert"); - EnumValue showAndInsert ("Show cell and insert"); - EnumValue dontInsert ("Discard"); - EnumValue insertAnyway ("Insert anyway"); - EnumValues insertOutsideCell; - insertOutsideCell.add (createAndInsert).add (dontInsert).add (insertAnyway); - EnumValues insertOutsideVisibleCell; - insertOutsideVisibleCell.add (showAndInsert).add (dontInsert).add (insertAnyway); - - EnumValue createAndLandEdit ("Create cell and land, then edit"); - EnumValue showAndLandEdit ("Show cell and edit"); - EnumValue dontLandEdit ("Discard"); - EnumValues landeditOutsideCell; - landeditOutsideCell.add (createAndLandEdit).add (dontLandEdit); - EnumValues landeditOutsideVisibleCell; - landeditOutsideVisibleCell.add (showAndLandEdit).add (dontLandEdit); - - EnumValue SelectOnly ("Select only"); - EnumValue SelectAdd ("Add to selection"); - EnumValue SelectRemove ("Remove from selection"); - EnumValue selectInvert ("Invert selection"); - EnumValues primarySelectAction; - primarySelectAction.add (SelectOnly).add (SelectAdd).add (SelectRemove).add (selectInvert); - EnumValues secondarySelectAction; - secondarySelectAction.add (SelectOnly).add (SelectAdd).add (SelectRemove).add (selectInvert); - - declareCategory ("3D Scene Editing"); - declareInt ("distance", "Drop Distance", 50). - setTooltip ("If an instance drop can not be placed against another object at the " + declareColour(mValues->mRendering.mSceneNightBackgroundColour, "Scene Night Background Colour"); + declareColour(mValues->mRendering.mSceneNightGradientColour, "Scene Night Gradient Colour") + .setTooltip( + "Sets the gradient color to use in conjunction with the night background color. Ignored if " + "the gradient option is disabled."); + declareBool(mValues->mRendering.mSceneDayNightSwitchNodes, "Use Day/Night Switch Nodes"); + + declareCategory("Tooltips"); + declareBool(mValues->mTooltips.mScene, "Show Tooltips in 3D scenes"); + declareBool(mValues->mTooltips.mSceneHideBasic, "Hide basic 3D scenes tooltips"); + declareInt(mValues->mTooltips.mSceneDelay, "Tooltip delay in milliseconds").setMin(1); + + declareCategory("3D Scene Editing"); + declareDouble(mValues->mSceneEditing.mGridsnapMovement, "Grid snap size"); + declareDouble(mValues->mSceneEditing.mGridsnapRotation, "Angle snap size"); + declareDouble(mValues->mSceneEditing.mGridsnapScale, "Scale snap size"); + declareInt(mValues->mSceneEditing.mDistance, "Drop Distance") + .setTooltip( + "If an instance drop can not be placed against another object at the " "insert point, it will be placed by this distance from the insert point instead"); - declareEnum ("outside-drop", "Handling drops outside of cells", createAndInsert). - addValues (insertOutsideCell); - declareEnum ("outside-visible-drop", "Handling drops outside of visible cells", showAndInsert). - addValues (insertOutsideVisibleCell); - declareEnum ("outside-landedit", "Handling terrain edit outside of cells", createAndLandEdit). - setTooltip("Behavior of terrain editing, if land editing brush reaches an area without cell record."). - addValues (landeditOutsideCell); - declareEnum ("outside-visible-landedit", "Handling terrain edit outside of visible cells", showAndLandEdit). - setTooltip("Behavior of terrain editing, if land editing brush reaches an area that is not currently visible."). - addValues (landeditOutsideVisibleCell); - declareInt ("texturebrush-maximumsize", "Maximum texture brush size", 50). - setMin (1); - declareInt ("shapebrush-maximumsize", "Maximum height edit brush size", 100). - setTooltip("Setting for the slider range of brush size in terrain height editing."). - setMin (1); - declareBool ("landedit-post-smoothpainting", "Smooth land after painting height", false). - setTooltip("Raise and lower tools will leave bumpy finish without this option"); - declareDouble ("landedit-post-smoothstrength", "Smoothing strength (post-edit)", 0.25). - setTooltip("If smoothing land after painting height is used, this is the percentage of smooth applied afterwards. " - "Negative values may be used to roughen instead of smooth."). - setMin (-1). - setMax (1); - declareBool ("open-list-view", "Open displays list view", false). - setTooltip ("When opening a reference from the scene view, it will open the" - " instance list view instead of the individual instance record view."); - declareEnum ("primary-select-action", "Action for primary select", SelectOnly). - setTooltip("Selection can be chosen between select only, add to selection, remove from selection and invert selection."). - addValues (primarySelectAction); - declareEnum ("secondary-select-action", "Action for secondary select", SelectAdd). - setTooltip("Selection can be chosen between select only, add to selection, remove from selection and invert selection."). - addValues (secondarySelectAction); - - declareCategory ("Key Bindings"); - - declareSubcategory ("Document"); - declareShortcut ("document-file-newgame", "New Game", QKeySequence(Qt::ControlModifier | Qt::Key_N)); - declareShortcut ("document-file-newaddon", "New Addon", QKeySequence()); - declareShortcut ("document-file-open", "Open", QKeySequence(Qt::ControlModifier | Qt::Key_O)); - declareShortcut ("document-file-save", "Save", QKeySequence(Qt::ControlModifier | Qt::Key_S)); - declareShortcut ("document-help-help", "Help", QKeySequence(Qt::Key_F1)); - declareShortcut ("document-help-tutorial", "Tutorial", QKeySequence()); - declareShortcut ("document-file-verify", "Verify", QKeySequence()); - declareShortcut ("document-file-merge", "Merge", QKeySequence()); - declareShortcut ("document-file-errorlog", "Open Load Error Log", QKeySequence()); - declareShortcut ("document-file-metadata", "Meta Data", QKeySequence()); - declareShortcut ("document-file-close", "Close Document", QKeySequence(Qt::ControlModifier | Qt::Key_W)); - declareShortcut ("document-file-exit", "Exit Application", QKeySequence(Qt::ControlModifier | Qt::Key_Q)); - declareShortcut ("document-edit-undo", "Undo", QKeySequence(Qt::ControlModifier | Qt::Key_Z)); - declareShortcut ("document-edit-redo", "Redo", QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_Z)); - declareShortcut ("document-edit-preferences", "Open Preferences", QKeySequence()); - declareShortcut ("document-edit-search", "Search", QKeySequence(Qt::ControlModifier | Qt::Key_F)); - declareShortcut ("document-view-newview", "New View", QKeySequence()); - declareShortcut ("document-view-statusbar", "Toggle Status Bar", QKeySequence()); - declareShortcut ("document-view-filters", "Open Filter List", QKeySequence()); - declareShortcut ("document-world-regions", "Open Region List", QKeySequence()); - declareShortcut ("document-world-cells", "Open Cell List", QKeySequence()); - declareShortcut ("document-world-referencables", "Open Object List", QKeySequence()); - declareShortcut ("document-world-references", "Open Instance List", QKeySequence()); - declareShortcut ("document-world-lands", "Open Lands List", QKeySequence()); - declareShortcut ("document-world-landtextures", "Open Land Textures List", QKeySequence()); - declareShortcut ("document-world-pathgrid", "Open Pathgrid List", QKeySequence()); - declareShortcut ("document-world-regionmap", "Open Region Map", QKeySequence()); - declareShortcut ("document-mechanics-globals", "Open Global List", QKeySequence()); - declareShortcut ("document-mechanics-gamesettings", "Open Game Settings", QKeySequence()); - declareShortcut ("document-mechanics-scripts", "Open Script List", QKeySequence()); - declareShortcut ("document-mechanics-spells", "Open Spell List", QKeySequence()); - declareShortcut ("document-mechanics-enchantments", "Open Enchantment List", QKeySequence()); - declareShortcut ("document-mechanics-magiceffects", "Open Magic Effect List", QKeySequence()); - declareShortcut ("document-mechanics-startscripts", "Open Start Script List", QKeySequence()); - declareShortcut ("document-character-skills", "Open Skill List", QKeySequence()); - declareShortcut ("document-character-classes", "Open Class List", QKeySequence()); - declareShortcut ("document-character-factions", "Open Faction List", QKeySequence()); - declareShortcut ("document-character-races", "Open Race List", QKeySequence()); - declareShortcut ("document-character-birthsigns", "Open Birthsign List", QKeySequence()); - declareShortcut ("document-character-topics", "Open Topic List", QKeySequence()); - declareShortcut ("document-character-journals", "Open Journal List", QKeySequence()); - declareShortcut ("document-character-topicinfos", "Open Topic Info List", QKeySequence()); - declareShortcut ("document-character-journalinfos", "Open Journal Info List", QKeySequence()); - declareShortcut ("document-character-bodyparts", "Open Body Part List", QKeySequence()); - declareShortcut ("document-assets-reload", "Reload Assets", QKeySequence(Qt::Key_F5)); - declareShortcut ("document-assets-sounds", "Open Sound Asset List", QKeySequence()); - declareShortcut ("document-assets-soundgens", "Open Sound Generator List", QKeySequence()); - declareShortcut ("document-assets-meshes", "Open Mesh Asset List", QKeySequence()); - declareShortcut ("document-assets-icons", "Open Icon Asset List", QKeySequence()); - declareShortcut ("document-assets-music", "Open Music Asset List", QKeySequence()); - declareShortcut ("document-assets-soundres", "Open Sound File List", QKeySequence()); - declareShortcut ("document-assets-textures", "Open Texture Asset List", QKeySequence()); - declareShortcut ("document-assets-videos", "Open Video Asset List", QKeySequence()); - declareShortcut ("document-debug-run", "Run Debug", QKeySequence()); - declareShortcut ("document-debug-shutdown", "Stop Debug", QKeySequence()); - declareShortcut ("document-debug-profiles", "Debug Profiles", QKeySequence()); - declareShortcut ("document-debug-runlog", "Open Run Log", QKeySequence()); - - declareSubcategory ("Table"); - declareShortcut ("table-edit", "Edit Record", QKeySequence()); - declareShortcut ("table-add", "Add Row/Record", QKeySequence(Qt::ShiftModifier | Qt::Key_A)); - declareShortcut ("table-clone", "Clone Record", QKeySequence(Qt::ShiftModifier | Qt::Key_D)); - declareShortcut ("touch-record", "Touch Record", QKeySequence()); - declareShortcut ("table-revert", "Revert Record", QKeySequence()); - declareShortcut ("table-remove", "Remove Row/Record", QKeySequence(Qt::Key_Delete)); - declareShortcut ("table-moveup", "Move Record Up", QKeySequence()); - declareShortcut ("table-movedown", "Move Record Down", QKeySequence()); - declareShortcut ("table-view", "View Record", QKeySequence(Qt::ShiftModifier | Qt::Key_C)); - declareShortcut ("table-preview", "Preview Record", QKeySequence(Qt::ShiftModifier | Qt::Key_V)); - declareShortcut ("table-extendeddelete", "Extended Record Deletion", QKeySequence()); - declareShortcut ("table-extendedrevert", "Extended Record Revertion", QKeySequence()); - - declareSubcategory ("Report Table"); - declareShortcut ("reporttable-show", "Show Report", QKeySequence()); - declareShortcut ("reporttable-remove", "Remove Report", QKeySequence(Qt::Key_Delete)); - declareShortcut ("reporttable-replace", "Replace Report", QKeySequence()); - declareShortcut ("reporttable-refresh", "Refresh Report", QKeySequence()); - - declareSubcategory ("Scene"); - declareShortcut ("scene-navi-primary", "Camera Rotation From Mouse Movement", QKeySequence(Qt::LeftButton)); - declareShortcut ("scene-navi-secondary", "Camera Translation From Mouse Movement", - QKeySequence(Qt::ControlModifier | (int)Qt::LeftButton)); - declareShortcut ("scene-open-primary", "Primary Open", QKeySequence(Qt::ShiftModifier | (int)Qt::LeftButton)); - declareShortcut ("scene-edit-primary", "Primary Edit", QKeySequence(Qt::RightButton)); - declareShortcut ("scene-edit-secondary", "Secondary Edit", - QKeySequence(Qt::ControlModifier | (int)Qt::RightButton)); - declareShortcut ("scene-select-primary", "Primary Select", QKeySequence(Qt::MiddleButton)); - declareShortcut ("scene-select-secondary", "Secondary Select", - QKeySequence(Qt::ControlModifier | (int)Qt::MiddleButton)); - declareModifier ("scene-speed-modifier", "Speed Modifier", Qt::Key_Shift); - declareShortcut ("scene-delete", "Delete Instance", QKeySequence(Qt::Key_Delete)); - declareShortcut ("scene-instance-drop-terrain", "Drop to terrain level", QKeySequence(Qt::Key_G)); - declareShortcut ("scene-instance-drop-collision", "Drop to collision", QKeySequence(Qt::Key_H)); - declareShortcut ("scene-instance-drop-terrain-separately", "Drop to terrain level separately", QKeySequence()); - declareShortcut ("scene-instance-drop-collision-separately", "Drop to collision separately", QKeySequence()); - declareShortcut ("scene-load-cam-cell", "Load Camera Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_5)); - declareShortcut ("scene-load-cam-eastcell", "Load East Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_6)); - declareShortcut ("scene-load-cam-northcell", "Load North Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_8)); - declareShortcut ("scene-load-cam-westcell", "Load West Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_4)); - declareShortcut ("scene-load-cam-southcell", "Load South Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_2)); - declareShortcut ("scene-edit-abort", "Abort", QKeySequence(Qt::Key_Escape)); - declareShortcut ("scene-focus-toolbar", "Toggle Toolbar Focus", QKeySequence(Qt::Key_T)); - declareShortcut ("scene-render-stats", "Debug Rendering Stats", QKeySequence(Qt::Key_F3)); - - declareSubcategory ("1st/Free Camera"); - declareShortcut ("free-forward", "Forward", QKeySequence(Qt::Key_W)); - declareShortcut ("free-backward", "Backward", QKeySequence(Qt::Key_S)); - declareShortcut ("free-left", "Left", QKeySequence(Qt::Key_A)); - declareShortcut ("free-right", "Right", QKeySequence(Qt::Key_D)); - declareShortcut ("free-roll-left", "Roll Left", QKeySequence(Qt::Key_Q)); - declareShortcut ("free-roll-right", "Roll Right", QKeySequence(Qt::Key_E)); - declareShortcut ("free-speed-mode", "Toggle Speed Mode", QKeySequence(Qt::Key_F)); - - declareSubcategory ("Orbit Camera"); - declareShortcut ("orbit-up", "Up", QKeySequence(Qt::Key_W)); - declareShortcut ("orbit-down", "Down", QKeySequence(Qt::Key_S)); - declareShortcut ("orbit-left", "Left", QKeySequence(Qt::Key_A)); - declareShortcut ("orbit-right", "Right", QKeySequence(Qt::Key_D)); - declareShortcut ("orbit-roll-left", "Roll Left", QKeySequence(Qt::Key_Q)); - declareShortcut ("orbit-roll-right", "Roll Right", QKeySequence(Qt::Key_E)); - declareShortcut ("orbit-speed-mode", "Toggle Speed Mode", QKeySequence(Qt::Key_F)); - declareShortcut ("orbit-center-selection", "Center On Selected", QKeySequence(Qt::Key_C)); - - declareSubcategory ("Script Editor"); - declareShortcut ("script-editor-comment", "Comment Selection", QKeySequence()); - declareShortcut ("script-editor-uncomment", "Uncomment Selection", QKeySequence()); - - declareCategory ("Models"); - declareString ("baseanim", "base animations", "meshes/base_anim.nif"). - setTooltip("3rd person base model with textkeys-data"); - declareString ("baseanimkna", "base animations, kna", "meshes/base_animkna.nif"). - setTooltip("3rd person beast race base model with textkeys-data"); - declareString ("baseanimfemale", "base animations, female", "meshes/base_anim_female.nif"). - setTooltip("3rd person female base model with textkeys-data"); - declareString ("wolfskin", "base animations, wolf", "meshes/wolf/skin.nif"). - setTooltip("3rd person werewolf skin"); + declareEnum(mValues->mSceneEditing.mOutsideDrop, "Handling drops outside of cells"); + declareEnum(mValues->mSceneEditing.mOutsideVisibleDrop, "Handling drops outside of visible cells"); + declareEnum(mValues->mSceneEditing.mOutsideLandedit, "Handling terrain edit outside of cells") + .setTooltip("Behavior of terrain editing, if land editing brush reaches an area without cell record."); + declareEnum(mValues->mSceneEditing.mOutsideVisibleLandedit, "Handling terrain edit outside of visible cells") + .setTooltip( + "Behavior of terrain editing, if land editing brush reaches an area that is not currently visible."); + declareInt(mValues->mSceneEditing.mTexturebrushMaximumsize, "Maximum texture brush size").setMin(1); + declareInt(mValues->mSceneEditing.mShapebrushMaximumsize, "Maximum height edit brush size") + .setTooltip("Setting for the slider range of brush size in terrain height editing.") + .setMin(1); + declareBool(mValues->mSceneEditing.mLandeditPostSmoothpainting, "Smooth land after painting height") + .setTooltip("Raise and lower tools will leave bumpy finish without this option"); + declareDouble(mValues->mSceneEditing.mLandeditPostSmoothstrength, "Smoothing strength (post-edit)") + .setTooltip( + "If smoothing land after painting height is used, this is the percentage of smooth applied afterwards. " + "Negative values may be used to roughen instead of smooth.") + .setMin(-1) + .setMax(1); + declareBool(mValues->mSceneEditing.mOpenListView, "Open displays list view") + .setTooltip( + "When opening a reference from the scene view, it will open the" + " instance list view instead of the individual instance record view."); + declareEnum(mValues->mSceneEditing.mPrimarySelectAction, "Action for primary select") + .setTooltip( + "Selection can be chosen between select only, add to selection, remove from selection and invert " + "selection."); + declareEnum(mValues->mSceneEditing.mSecondarySelectAction, "Action for secondary select") + .setTooltip( + "Selection can be chosen between select only, add to selection, remove from selection and invert " + "selection."); + + declareCategory("Key Bindings"); + + declareSubcategory("Document"); + declareShortcut(mValues->mKeyBindings.mDocumentFileNewgame, "New Game"); + declareShortcut(mValues->mKeyBindings.mDocumentFileNewaddon, "New Addon"); + declareShortcut(mValues->mKeyBindings.mDocumentFileOpen, "Open"); + declareShortcut(mValues->mKeyBindings.mDocumentFileSave, "Save"); + declareShortcut(mValues->mKeyBindings.mDocumentHelpHelp, "Help"); + declareShortcut(mValues->mKeyBindings.mDocumentHelpTutorial, "Tutorial"); + declareShortcut(mValues->mKeyBindings.mDocumentFileVerify, "Verify"); + declareShortcut(mValues->mKeyBindings.mDocumentFileMerge, "Merge"); + declareShortcut(mValues->mKeyBindings.mDocumentFileErrorlog, "Open Load Error Log"); + declareShortcut(mValues->mKeyBindings.mDocumentFileMetadata, "Meta Data"); + declareShortcut(mValues->mKeyBindings.mDocumentFileClose, "Close Document"); + declareShortcut(mValues->mKeyBindings.mDocumentFileExit, "Exit Application"); + declareShortcut(mValues->mKeyBindings.mDocumentEditUndo, "Undo"); + declareShortcut(mValues->mKeyBindings.mDocumentEditRedo, "Redo"); + declareShortcut(mValues->mKeyBindings.mDocumentEditPreferences, "Open Preferences"); + declareShortcut(mValues->mKeyBindings.mDocumentEditSearch, "Search"); + declareShortcut(mValues->mKeyBindings.mDocumentViewNewview, "New View"); + declareShortcut(mValues->mKeyBindings.mDocumentViewStatusbar, "Toggle Status Bar"); + declareShortcut(mValues->mKeyBindings.mDocumentViewFilters, "Open Filter List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldRegions, "Open Region List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldCells, "Open Cell List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldReferencables, "Open Object List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldReferences, "Open Instance List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldLands, "Open Lands List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldLandtextures, "Open Land Textures List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldPathgrid, "Open Pathgrid List"); + declareShortcut(mValues->mKeyBindings.mDocumentWorldRegionmap, "Open Region Map"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsGlobals, "Open Global List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsGamesettings, "Open Game Settings"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsScripts, "Open Script List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsSpells, "Open Spell List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsEnchantments, "Open Enchantment List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsMagiceffects, "Open Magic Effect List"); + declareShortcut(mValues->mKeyBindings.mDocumentMechanicsStartscripts, "Open Start Script List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterSkills, "Open Skill List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterClasses, "Open Class List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterFactions, "Open Faction List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterRaces, "Open Race List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterBirthsigns, "Open Birthsign List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterTopics, "Open Topic List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterJournals, "Open Journal List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterTopicinfos, "Open Topic Info List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterJournalinfos, "Open Journal Info List"); + declareShortcut(mValues->mKeyBindings.mDocumentCharacterBodyparts, "Open Body Part List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsReload, "Reload Assets"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsSounds, "Open Sound Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsSoundgens, "Open Sound Generator List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsMeshes, "Open Mesh Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsIcons, "Open Icon Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsMusic, "Open Music Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsSoundres, "Open Sound File List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsTextures, "Open Texture Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentAssetsVideos, "Open Video Asset List"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugRun, "Run Debug"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugShutdown, "Stop Debug"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugProfiles, "Debug Profiles"); + declareShortcut(mValues->mKeyBindings.mDocumentDebugRunlog, "Open Run Log"); + + declareSubcategory("Table"); + declareShortcut(mValues->mKeyBindings.mTableEdit, "Edit Record"); + declareShortcut(mValues->mKeyBindings.mTableAdd, "Add Row/Record"); + declareShortcut(mValues->mKeyBindings.mTableClone, "Clone Record"); + declareShortcut(mValues->mKeyBindings.mTouchRecord, "Touch Record"); + declareShortcut(mValues->mKeyBindings.mTableRevert, "Revert Record"); + declareShortcut(mValues->mKeyBindings.mTableRemove, "Remove Row/Record"); + declareShortcut(mValues->mKeyBindings.mTableMoveup, "Move Record Up"); + declareShortcut(mValues->mKeyBindings.mTableMovedown, "Move Record Down"); + declareShortcut(mValues->mKeyBindings.mTableView, "View Record"); + declareShortcut(mValues->mKeyBindings.mTablePreview, "Preview Record"); + declareShortcut(mValues->mKeyBindings.mTableExtendeddelete, "Extended Record Deletion"); + declareShortcut(mValues->mKeyBindings.mTableExtendedrevert, "Extended Record Revertion"); + + declareSubcategory("Report Table"); + declareShortcut(mValues->mKeyBindings.mReporttableShow, "Show Report"); + declareShortcut(mValues->mKeyBindings.mReporttableRemove, "Remove Report"); + declareShortcut(mValues->mKeyBindings.mReporttableReplace, "Replace Report"); + declareShortcut(mValues->mKeyBindings.mReporttableRefresh, "Refresh Report"); + + declareSubcategory("Scene"); + declareShortcut(mValues->mKeyBindings.mSceneNaviPrimary, "Camera Rotation From Mouse Movement"); + declareShortcut(mValues->mKeyBindings.mSceneNaviSecondary, "Camera Translation From Mouse Movement"); + declareShortcut(mValues->mKeyBindings.mSceneOpenPrimary, "Primary Open"); + declareShortcut(mValues->mKeyBindings.mSceneEditPrimary, "Primary Edit"); + declareShortcut(mValues->mKeyBindings.mSceneEditSecondary, "Secondary Edit"); + declareShortcut(mValues->mKeyBindings.mSceneSelectPrimary, "Primary Select"); + declareShortcut(mValues->mKeyBindings.mSceneSelectSecondary, "Secondary Select"); + declareShortcut(mValues->mKeyBindings.mSceneSelectTertiary, "Tertiary Select"); + declareModifier(mValues->mKeyBindings.mSceneSpeedModifier, "Speed Modifier"); + declareShortcut(mValues->mKeyBindings.mSceneDelete, "Delete Instance"); + declareShortcut(mValues->mKeyBindings.mSceneInstanceDropTerrain, "Drop to terrain level"); + declareShortcut(mValues->mKeyBindings.mSceneInstanceDropCollision, "Drop to collision"); + declareShortcut(mValues->mKeyBindings.mSceneInstanceDropTerrainSeparately, "Drop to terrain level separately"); + declareShortcut(mValues->mKeyBindings.mSceneInstanceDropCollisionSeparately, "Drop to collision separately"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamCell, "Load Camera Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamEastcell, "Load East Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamNorthcell, "Load North Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamWestcell, "Load West Cell"); + declareShortcut(mValues->mKeyBindings.mSceneLoadCamSouthcell, "Load South Cell"); + declareShortcut(mValues->mKeyBindings.mSceneEditAbort, "Abort"); + declareShortcut(mValues->mKeyBindings.mSceneFocusToolbar, "Toggle Toolbar Focus"); + declareShortcut(mValues->mKeyBindings.mSceneRenderStats, "Debug Rendering Stats"); + declareShortcut(mValues->mKeyBindings.mSceneDuplicate, "Duplicate Instance"); + declareShortcut(mValues->mKeyBindings.mSceneClearSelection, "Clear Selection"); + declareShortcut(mValues->mKeyBindings.mSceneUnhideAll, "Unhide All Objects"); + declareShortcut(mValues->mKeyBindings.mSceneToggleVisibility, "Toggle Selection Visibility"); + declareShortcut(mValues->mKeyBindings.mSceneGroup0, "Selection Group 0"); + declareShortcut(mValues->mKeyBindings.mSceneSave0, "Save Group 0"); + declareShortcut(mValues->mKeyBindings.mSceneGroup1, "Select Group 1"); + declareShortcut(mValues->mKeyBindings.mSceneSave1, "Save Group 1"); + declareShortcut(mValues->mKeyBindings.mSceneGroup2, "Select Group 2"); + declareShortcut(mValues->mKeyBindings.mSceneSave2, "Save Group 2"); + declareShortcut(mValues->mKeyBindings.mSceneGroup3, "Select Group 3"); + declareShortcut(mValues->mKeyBindings.mSceneSave3, "Save Group 3"); + declareShortcut(mValues->mKeyBindings.mSceneGroup4, "Select Group 4"); + declareShortcut(mValues->mKeyBindings.mSceneSave4, "Save Group 4"); + declareShortcut(mValues->mKeyBindings.mSceneGroup5, "Selection Group 5"); + declareShortcut(mValues->mKeyBindings.mSceneSave5, "Save Group 5"); + declareShortcut(mValues->mKeyBindings.mSceneGroup6, "Selection Group 6"); + declareShortcut(mValues->mKeyBindings.mSceneSave6, "Save Group 6"); + declareShortcut(mValues->mKeyBindings.mSceneGroup7, "Selection Group 7"); + declareShortcut(mValues->mKeyBindings.mSceneSave7, "Save Group 7"); + declareShortcut(mValues->mKeyBindings.mSceneGroup8, "Selection Group 8"); + declareShortcut(mValues->mKeyBindings.mSceneSave8, "Save Group 8"); + declareShortcut(mValues->mKeyBindings.mSceneGroup9, "Selection Group 9"); + declareShortcut(mValues->mKeyBindings.mSceneSave9, "Save Group 9"); + declareShortcut(mValues->mKeyBindings.mSceneAxisX, "X Axis Movement Lock"); + declareShortcut(mValues->mKeyBindings.mSceneAxisY, "Y Axis Movement Lock"); + declareShortcut(mValues->mKeyBindings.mSceneAxisZ, "Z Axis Movement Lock"); + declareShortcut(mValues->mKeyBindings.mSceneMoveSubmode, "Move Object Submode"); + declareShortcut(mValues->mKeyBindings.mSceneScaleSubmode, "Scale Object Submode"); + declareShortcut(mValues->mKeyBindings.mSceneRotateSubmode, "Rotate Object Submode"); + declareShortcut(mValues->mKeyBindings.mSceneCameraCycle, "Cycle Camera Mode"); + + declareSubcategory("1st/Free Camera"); + declareShortcut(mValues->mKeyBindings.mFreeForward, "Forward"); + declareShortcut(mValues->mKeyBindings.mFreeBackward, "Backward"); + declareShortcut(mValues->mKeyBindings.mFreeLeft, "Left"); + declareShortcut(mValues->mKeyBindings.mFreeRight, "Right"); + declareShortcut(mValues->mKeyBindings.mFreeRollLeft, "Roll Left"); + declareShortcut(mValues->mKeyBindings.mFreeRollRight, "Roll Right"); + declareShortcut(mValues->mKeyBindings.mFreeSpeedMode, "Toggle Speed Mode"); + + declareSubcategory("Orbit Camera"); + declareShortcut(mValues->mKeyBindings.mOrbitUp, "Up"); + declareShortcut(mValues->mKeyBindings.mOrbitDown, "Down"); + declareShortcut(mValues->mKeyBindings.mOrbitLeft, "Left"); + declareShortcut(mValues->mKeyBindings.mOrbitRight, "Right"); + declareShortcut(mValues->mKeyBindings.mOrbitRollLeft, "Roll Left"); + declareShortcut(mValues->mKeyBindings.mOrbitRollRight, "Roll Right"); + declareShortcut(mValues->mKeyBindings.mOrbitSpeedMode, "Toggle Speed Mode"); + declareShortcut(mValues->mKeyBindings.mOrbitCenterSelection, "Center On Selected"); + + declareSubcategory("Script Editor"); + declareShortcut(mValues->mKeyBindings.mScriptEditorComment, "Comment Selection"); + declareShortcut(mValues->mKeyBindings.mScriptEditorUncomment, "Uncomment Selection"); + + declareCategory("Models"); + declareString(mValues->mModels.mBaseanim, "base animations").setTooltip("3rd person base model with textkeys-data"); + declareString(mValues->mModels.mBaseanimkna, "base animations, kna") + .setTooltip("3rd person beast race base model with textkeys-data"); + declareString(mValues->mModels.mBaseanimfemale, "base animations, female") + .setTooltip("3rd person female base model with textkeys-data"); + declareString(mValues->mModels.mWolfskin, "base animations, wolf").setTooltip("3rd person werewolf skin"); } -void CSMPrefs::State::declareCategory (const std::string& key) +void CSMPrefs::State::declareCategory(const std::string& key) { - std::map::iterator iter = mCategories.find (key); + std::map::iterator iter = mCategories.find(key); - if (iter!=mCategories.end()) + if (iter != mCategories.end()) { mCurrentCategory = iter; } else { - mCurrentCategory = - mCategories.insert (std::make_pair (key, Category (this, key))).first; + mCurrentCategory = mCategories.insert(std::make_pair(key, Category(this, key))).first; } } -CSMPrefs::IntSetting& CSMPrefs::State::declareInt (const std::string& key, - const std::string& label, int default_) +CSMPrefs::IntSetting& CSMPrefs::State::declareInt(Settings::SettingValue& value, const QString& label) { - if (mCurrentCategory==mCategories.end()) - throw std::logic_error ("no category for setting"); + if (mCurrentCategory == mCategories.end()) + throw std::logic_error("no category for setting"); - setDefault(key, std::to_string(default_)); + CSMPrefs::IntSetting* setting + = new CSMPrefs::IntSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); - default_ = mSettings.getInt (key, mCurrentCategory->second.getKey()); - - CSMPrefs::IntSetting *setting = - new CSMPrefs::IntSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, - default_); - - mCurrentCategory->second.addSetting (setting); + mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::DoubleSetting& CSMPrefs::State::declareDouble (const std::string& key, - const std::string& label, double default_) +CSMPrefs::DoubleSetting& CSMPrefs::State::declareDouble(Settings::SettingValue& value, const QString& label) { - if (mCurrentCategory==mCategories.end()) - throw std::logic_error ("no category for setting"); - - std::ostringstream stream; - stream << default_; - setDefault(key, stream.str()); + if (mCurrentCategory == mCategories.end()) + throw std::logic_error("no category for setting"); - default_ = mSettings.getFloat (key, mCurrentCategory->second.getKey()); + CSMPrefs::DoubleSetting* setting + = new CSMPrefs::DoubleSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); - CSMPrefs::DoubleSetting *setting = - new CSMPrefs::DoubleSetting (&mCurrentCategory->second, &mSettings, &mMutex, - key, label, default_); - - mCurrentCategory->second.addSetting (setting); + mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::BoolSetting& CSMPrefs::State::declareBool (const std::string& key, - const std::string& label, bool default_) +CSMPrefs::BoolSetting& CSMPrefs::State::declareBool(Settings::SettingValue& value, const QString& label) { - if (mCurrentCategory==mCategories.end()) - throw std::logic_error ("no category for setting"); - - setDefault (key, default_ ? "true" : "false"); - - default_ = mSettings.getBool (key, mCurrentCategory->second.getKey()); + if (mCurrentCategory == mCategories.end()) + throw std::logic_error("no category for setting"); - CSMPrefs::BoolSetting *setting = - new CSMPrefs::BoolSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, - default_); + CSMPrefs::BoolSetting* setting + = new CSMPrefs::BoolSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); - mCurrentCategory->second.addSetting (setting); + mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::EnumSetting& CSMPrefs::State::declareEnum (const std::string& key, - const std::string& label, EnumValue default_) +CSMPrefs::EnumSetting& CSMPrefs::State::declareEnum(EnumSettingValue& value, const QString& label) { - if (mCurrentCategory==mCategories.end()) - throw std::logic_error ("no category for setting"); + if (mCurrentCategory == mCategories.end()) + throw std::logic_error("no category for setting"); - setDefault (key, default_.mValue); + CSMPrefs::EnumSetting* setting = new CSMPrefs::EnumSetting( + &mCurrentCategory->second, &mMutex, value.getValue().mName, label, value.getEnumValues(), *mIndex); - default_.mValue = mSettings.getString (key, mCurrentCategory->second.getKey()); - - CSMPrefs::EnumSetting *setting = - new CSMPrefs::EnumSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, - default_); - - mCurrentCategory->second.addSetting (setting); + mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::ColourSetting& CSMPrefs::State::declareColour (const std::string& key, - const std::string& label, QColor default_) +CSMPrefs::ColourSetting& CSMPrefs::State::declareColour( + Settings::SettingValue& value, const QString& label) { - if (mCurrentCategory==mCategories.end()) - throw std::logic_error ("no category for setting"); - - setDefault (key, default_.name().toUtf8().data()); - - default_.setNamedColor (QString::fromUtf8 (mSettings.getString (key, mCurrentCategory->second.getKey()).c_str())); + if (mCurrentCategory == mCategories.end()) + throw std::logic_error("no category for setting"); - CSMPrefs::ColourSetting *setting = - new CSMPrefs::ColourSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, - default_); + CSMPrefs::ColourSetting* setting + = new CSMPrefs::ColourSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); - mCurrentCategory->second.addSetting (setting); + mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::ShortcutSetting& CSMPrefs::State::declareShortcut (const std::string& key, const std::string& label, - const QKeySequence& default_) +CSMPrefs::ShortcutSetting& CSMPrefs::State::declareShortcut( + Settings::SettingValue& value, const QString& label) { - if (mCurrentCategory==mCategories.end()) - throw std::logic_error ("no category for setting"); - - std::string seqStr = getShortcutManager().convertToString(default_); - setDefault (key, seqStr); + if (mCurrentCategory == mCategories.end()) + throw std::logic_error("no category for setting"); // Setup with actual data QKeySequence sequence; - getShortcutManager().convertFromString(mSettings.getString(key, mCurrentCategory->second.getKey()), sequence); - getShortcutManager().setSequence(key, sequence); + getShortcutManager().convertFromString(value, sequence); + getShortcutManager().setSequence(value.mName, sequence); - CSMPrefs::ShortcutSetting *setting = new CSMPrefs::ShortcutSetting (&mCurrentCategory->second, &mSettings, &mMutex, - key, label); - mCurrentCategory->second.addSetting (setting); + CSMPrefs::ShortcutSetting* setting + = new CSMPrefs::ShortcutSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); + mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::StringSetting& CSMPrefs::State::declareString (const std::string& key, const std::string& label, std::string default_) +CSMPrefs::StringSetting& CSMPrefs::State::declareString( + Settings::SettingValue& value, const QString& label) { - if (mCurrentCategory==mCategories.end()) - throw std::logic_error ("no category for setting"); - - setDefault (key, default_); - - default_ = mSettings.getString (key, mCurrentCategory->second.getKey()); + if (mCurrentCategory == mCategories.end()) + throw std::logic_error("no category for setting"); - CSMPrefs::StringSetting *setting = - new CSMPrefs::StringSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, - default_); + CSMPrefs::StringSetting* setting + = new CSMPrefs::StringSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); - mCurrentCategory->second.addSetting (setting); + mCurrentCategory->second.addSetting(setting); return *setting; } -CSMPrefs::ModifierSetting& CSMPrefs::State::declareModifier(const std::string& key, const std::string& label, - int default_) +CSMPrefs::ModifierSetting& CSMPrefs::State::declareModifier( + Settings::SettingValue& value, const QString& label) { - if (mCurrentCategory==mCategories.end()) - throw std::logic_error ("no category for setting"); - - std::string modStr = getShortcutManager().convertToString(default_); - setDefault (key, modStr); + if (mCurrentCategory == mCategories.end()) + throw std::logic_error("no category for setting"); // Setup with actual data int modifier; - getShortcutManager().convertFromString(mSettings.getString(key, mCurrentCategory->second.getKey()), modifier); - getShortcutManager().setModifier(key, modifier); + getShortcutManager().convertFromString(value.get(), modifier); + getShortcutManager().setModifier(value.mName, modifier); - CSMPrefs::ModifierSetting *setting = new CSMPrefs::ModifierSetting (&mCurrentCategory->second, &mSettings, &mMutex, - key, label); - mCurrentCategory->second.addSetting (setting); + CSMPrefs::ModifierSetting* setting + = new CSMPrefs::ModifierSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); + mCurrentCategory->second.addSetting(setting); return *setting; } -void CSMPrefs::State::declareSeparator() +void CSMPrefs::State::declareSubcategory(const QString& label) { - if (mCurrentCategory==mCategories.end()) - throw std::logic_error ("no category for setting"); - - CSMPrefs::Setting *setting = - new CSMPrefs::Setting (&mCurrentCategory->second, &mSettings, &mMutex, "", ""); + if (mCurrentCategory == mCategories.end()) + throw std::logic_error("no category for setting"); - mCurrentCategory->second.addSetting (setting); + mCurrentCategory->second.addSubcategory( + new CSMPrefs::Subcategory(&mCurrentCategory->second, &mMutex, label, *mIndex)); } -void CSMPrefs::State::declareSubcategory(const std::string& label) -{ - if (mCurrentCategory==mCategories.end()) - throw std::logic_error ("no category for setting"); - - CSMPrefs::Setting *setting = - new CSMPrefs::Setting (&mCurrentCategory->second, &mSettings, &mMutex, "", label); - - mCurrentCategory->second.addSetting (setting); -} - -void CSMPrefs::State::setDefault (const std::string& key, const std::string& default_) -{ - Settings::CategorySetting fullKey (mCurrentCategory->second.getKey(), key); - - Settings::CategorySettingValueMap::iterator iter = - mSettings.mDefaultSettings.find (fullKey); - - if (iter==mSettings.mDefaultSettings.end()) - mSettings.mDefaultSettings.insert (std::make_pair (fullKey, default_)); -} - -CSMPrefs::State::State (const Files::ConfigurationManager& configurationManager) -: mConfigFile ("openmw-cs.cfg"), mConfigurationManager (configurationManager), - mCurrentCategory (mCategories.end()) +CSMPrefs::State::State(const Files::ConfigurationManager& configurationManager) + : mConfigFile("openmw-cs.cfg") + , mDefaultConfigFile("defaults-cs.bin") + , mConfigurationManager(configurationManager) + , mCurrentCategory(mCategories.end()) + , mIndex(std::make_unique()) + , mValues(std::make_unique(*mIndex)) { if (sThis) - throw std::logic_error ("An instance of CSMPRefs::State already exists"); + throw std::logic_error("An instance of CSMPRefs::State already exists"); sThis = this; - load(); declare(); } @@ -660,8 +580,7 @@ CSMPrefs::State::~State() void CSMPrefs::State::save() { - boost::filesystem::path user = mConfigurationManager.getUserConfigPath() / mConfigFile; - mSettings.saveUser (user.string()); + Settings::Manager::saveUser(mConfigurationManager.getUserConfigPath() / mConfigFile); } CSMPrefs::State::Iterator CSMPrefs::State::begin() @@ -679,52 +598,38 @@ CSMPrefs::ShortcutManager& CSMPrefs::State::getShortcutManager() return mShortcutManager; } -CSMPrefs::Category& CSMPrefs::State::operator[] (const std::string& key) +CSMPrefs::Category& CSMPrefs::State::operator[](const std::string& key) { - Iterator iter = mCategories.find (key); + Iterator iter = mCategories.find(key); - if (iter==mCategories.end()) - throw std::logic_error ("Invalid user settings category: " + key); + if (iter == mCategories.end()) + throw std::logic_error("Invalid user settings category: " + key); return iter->second; } -void CSMPrefs::State::update (const Setting& setting) +void CSMPrefs::State::update(const Setting& setting) { - emit (settingChanged (&setting)); + emit settingChanged(&setting); } CSMPrefs::State& CSMPrefs::State::get() { if (!sThis) - throw std::logic_error ("No instance of CSMPrefs::State"); + throw std::logic_error("No instance of CSMPrefs::State"); return *sThis; } void CSMPrefs::State::resetCategory(const std::string& category) { - for (Settings::CategorySettingValueMap::iterator i = mSettings.mUserSettings.begin(); - i != mSettings.mUserSettings.end(); ++i) - { - // if the category matches - if (i->first.first == category) - { - // mark the setting as changed - mSettings.mChangedSettings.insert(std::make_pair(i->first.first, i->first.second)); - // reset the value to the default - i->second = mSettings.mDefaultSettings[i->first]; - } - } - Collection::iterator container = mCategories.find(category); if (container != mCategories.end()) { - Category settings = container->second; - for (Category::Iterator i = settings.begin(); i != settings.end(); ++i) + for (Setting* setting : container->second) { - (*i)->updateWidget(); - update(**i); + setting->reset(); + update(*setting); } } } @@ -737,7 +642,6 @@ void CSMPrefs::State::resetAll() } } - CSMPrefs::State& CSMPrefs::get() { return State::get(); diff --git a/apps/opencs/model/prefs/state.hpp b/apps/opencs/model/prefs/state.hpp index aa63de595eb..821322d586c 100644 --- a/apps/opencs/model/prefs/state.hpp +++ b/apps/opencs/model/prefs/state.hpp @@ -4,23 +4,24 @@ #include #include -#include #include +#include #ifndef Q_MOC_RUN #include #endif -#include - #include "category.hpp" -#include "setting.hpp" #include "enumsetting.hpp" -#include "stringsetting.hpp" #include "shortcutmanager.hpp" class QColor; +namespace Settings +{ + class Index; +} + namespace CSMPrefs { class IntSetting; @@ -29,6 +30,10 @@ namespace CSMPrefs class ColourSetting; class ShortcutSetting; class ModifierSetting; + class Setting; + class StringSetting; + class EnumSettingValue; + struct Values; /// \brief User settings state /// @@ -36,86 +41,77 @@ namespace CSMPrefs /// been completed. class State : public QObject { - Q_OBJECT - - static State *sThis; - - public: - - typedef std::map Collection; - typedef Collection::iterator Iterator; - - private: - - const std::string mConfigFile; - const Files::ConfigurationManager& mConfigurationManager; - ShortcutManager mShortcutManager; - Settings::Manager mSettings; - Collection mCategories; - Iterator mCurrentCategory; - QMutex mMutex; + Q_OBJECT - // not implemented - State (const State&); - State& operator= (const State&); + static State* sThis; - private: + public: + typedef std::map Collection; + typedef Collection::iterator Iterator; - void load(); + private: + const std::string mConfigFile; + const std::string mDefaultConfigFile; + const Files::ConfigurationManager& mConfigurationManager; + ShortcutManager mShortcutManager; + Collection mCategories; + Iterator mCurrentCategory; + QMutex mMutex; + std::unique_ptr mIndex; + std::unique_ptr mValues; - void declare(); + void declare(); - void declareCategory (const std::string& key); + void declareCategory(const std::string& key); - IntSetting& declareInt (const std::string& key, const std::string& label, int default_); - DoubleSetting& declareDouble (const std::string& key, const std::string& label, double default_); + IntSetting& declareInt(Settings::SettingValue& value, const QString& label); - BoolSetting& declareBool (const std::string& key, const std::string& label, bool default_); + DoubleSetting& declareDouble(Settings::SettingValue& value, const QString& label); - EnumSetting& declareEnum (const std::string& key, const std::string& label, EnumValue default_); + BoolSetting& declareBool(Settings::SettingValue& value, const QString& label); - ColourSetting& declareColour (const std::string& key, const std::string& label, QColor default_); + EnumSetting& declareEnum(EnumSettingValue& value, const QString& label); - ShortcutSetting& declareShortcut (const std::string& key, const std::string& label, - const QKeySequence& default_); + ColourSetting& declareColour(Settings::SettingValue& value, const QString& label); - StringSetting& declareString (const std::string& key, const std::string& label, std::string default_); + ShortcutSetting& declareShortcut(Settings::SettingValue& value, const QString& label); - ModifierSetting& declareModifier(const std::string& key, const std::string& label, int modifier_); + StringSetting& declareString(Settings::SettingValue& value, const QString& label); - void declareSeparator(); + ModifierSetting& declareModifier(Settings::SettingValue& value, const QString& label); - void declareSubcategory(const std::string& label); + void declareSubcategory(const QString& label); - void setDefault (const std::string& key, const std::string& default_); + public: + State(const Files::ConfigurationManager& configurationManager); - public: + State(const State&) = delete; - State (const Files::ConfigurationManager& configurationManager); + ~State(); - ~State(); + State& operator=(const State&) = delete; - void save(); + void save(); - Iterator begin(); + Iterator begin(); - Iterator end(); + Iterator end(); - ShortcutManager& getShortcutManager(); + ShortcutManager& getShortcutManager(); - Category& operator[](const std::string& key); + Category& operator[](const std::string& key); - void update (const Setting& setting); + void update(const Setting& setting); - static State& get(); + static State& get(); - void resetCategory(const std::string& category); + void resetCategory(const std::string& category); - void resetAll(); + void resetAll(); - signals: + signals: - void settingChanged (const CSMPrefs::Setting *setting); + void settingChanged(const CSMPrefs::Setting* setting); }; // convenience function diff --git a/apps/opencs/model/prefs/stringsetting.cpp b/apps/opencs/model/prefs/stringsetting.cpp index 27290b6c720..10bd8cb5589 100644 --- a/apps/opencs/model/prefs/stringsetting.cpp +++ b/apps/opencs/model/prefs/stringsetting.cpp @@ -6,49 +6,47 @@ #include +#include + #include "category.hpp" #include "state.hpp" -CSMPrefs::StringSetting::StringSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, std::string default_) -: Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(nullptr) -{} +CSMPrefs::StringSetting::StringSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index) + : TypedSetting(parent, mutex, key, label, index) + , mWidget(nullptr) +{ +} -CSMPrefs::StringSetting& CSMPrefs::StringSetting::setTooltip (const std::string& tooltip) +CSMPrefs::StringSetting& CSMPrefs::StringSetting::setTooltip(const std::string& tooltip) { mTooltip = tooltip; return *this; } -std::pair CSMPrefs::StringSetting::makeWidgets (QWidget *parent) +CSMPrefs::SettingWidgets CSMPrefs::StringSetting::makeWidgets(QWidget* parent) { - mWidget = new QLineEdit (QString::fromUtf8 (mDefault.c_str()), parent); + mWidget = new QLineEdit(QString::fromStdString(getValue()), parent); if (!mTooltip.empty()) { - QString tooltip = QString::fromUtf8 (mTooltip.c_str()); - mWidget->setToolTip (tooltip); + QString tooltip = QString::fromUtf8(mTooltip.c_str()); + mWidget->setToolTip(tooltip); } - connect (mWidget, SIGNAL (textChanged (QString)), this, SLOT (textChanged (QString))); + connect(mWidget, &QLineEdit::textChanged, this, &StringSetting::textChanged); - return std::make_pair (static_cast (nullptr), mWidget); + return SettingWidgets{ .mLabel = nullptr, .mInput = mWidget }; } void CSMPrefs::StringSetting::updateWidget() { if (mWidget) - { - mWidget->setText(QString::fromStdString(getValues().getString(getKey(), getParent()->getKey()))); - } + mWidget->setText(QString::fromStdString(getValue())); } -void CSMPrefs::StringSetting::textChanged (const QString& text) +void CSMPrefs::StringSetting::textChanged(const QString& text) { - { - QMutexLocker lock (getMutex()); - getValues().setString (getKey(), getParent()->getKey(), text.toStdString()); - } - - getParent()->getState()->update (*this); + setValue(text.toStdString()); + getParent()->getState()->update(*this); } diff --git a/apps/opencs/model/prefs/stringsetting.hpp b/apps/opencs/model/prefs/stringsetting.hpp index 36d020f2960..0a7d2a49356 100644 --- a/apps/opencs/model/prefs/stringsetting.hpp +++ b/apps/opencs/model/prefs/stringsetting.hpp @@ -3,33 +3,39 @@ #include "setting.hpp" +#include +#include + class QLineEdit; +class QMutex; +class QObject; +class QWidget; namespace CSMPrefs { - class StringSetting : public Setting - { - Q_OBJECT + class Category; - std::string mTooltip; - std::string mDefault; - QLineEdit* mWidget; + class StringSetting final : public TypedSetting + { + Q_OBJECT - public: + std::string mTooltip; + QLineEdit* mWidget; - StringSetting (Category *parent, Settings::Manager *values, - QMutex *mutex, const std::string& key, const std::string& label, std::string default_); + public: + explicit StringSetting( + Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); - StringSetting& setTooltip (const std::string& tooltip); + StringSetting& setTooltip(const std::string& tooltip); - /// Return label, input widget. - std::pair makeWidgets (QWidget *parent) override; + /// Return label, input widget. + SettingWidgets makeWidgets(QWidget* parent) override; - void updateWidget() override; + void updateWidget() override; - private slots: + private slots: - void textChanged (const QString& text); + void textChanged(const QString& text); }; } diff --git a/apps/opencs/model/prefs/subcategory.cpp b/apps/opencs/model/prefs/subcategory.cpp new file mode 100644 index 00000000000..815025daec2 --- /dev/null +++ b/apps/opencs/model/prefs/subcategory.cpp @@ -0,0 +1,18 @@ +#include "subcategory.hpp" + +#include + +namespace CSMPrefs +{ + class Category; + + Subcategory::Subcategory(Category* parent, QMutex* mutex, const QString& label, Settings::Index& index) + : Setting(parent, mutex, "", label, index) + { + } + + SettingWidgets Subcategory::makeWidgets(QWidget* /*parent*/) + { + return SettingWidgets{ .mLabel = nullptr, .mInput = nullptr }; + } +} diff --git a/apps/opencs/model/prefs/subcategory.hpp b/apps/opencs/model/prefs/subcategory.hpp new file mode 100644 index 00000000000..4c661ad0faa --- /dev/null +++ b/apps/opencs/model/prefs/subcategory.hpp @@ -0,0 +1,28 @@ +#ifndef OPENMW_APPS_OPENCS_MODEL_PREFS_SUBCATEGORY_H +#define OPENMW_APPS_OPENCS_MODEL_PREFS_SUBCATEGORY_H + +#include "setting.hpp" + +#include +#include + +namespace CSMPrefs +{ + class Category; + + class Subcategory final : public Setting + { + Q_OBJECT + + public: + explicit Subcategory(Category* parent, QMutex* mutex, const QString& label, Settings::Index& index); + + SettingWidgets makeWidgets(QWidget* parent) override; + + void updateWidget() override {} + + void reset() override {} + }; +} + +#endif diff --git a/apps/opencs/model/prefs/values.hpp b/apps/opencs/model/prefs/values.hpp new file mode 100644 index 00000000000..c4b7010aaf9 --- /dev/null +++ b/apps/opencs/model/prefs/values.hpp @@ -0,0 +1,555 @@ +#ifndef OPENMW_APPS_OPENCS_MODEL_PREFS_VALUES_H +#define OPENMW_APPS_OPENCS_MODEL_PREFS_VALUES_H + +#include "enumvalueview.hpp" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace CSMPrefs +{ + class EnumSanitizer final : public Settings::Sanitizer + { + public: + explicit EnumSanitizer(std::span values) + : mValues(values) + { + } + + std::string apply(const std::string& value) const override + { + const auto hasValue = [&](const EnumValueView& v) { return v.mValue == value; }; + if (std::find_if(mValues.begin(), mValues.end(), hasValue) == mValues.end()) + { + std::ostringstream message; + message << "Invalid enum value: " << value; + throw std::runtime_error(message.str()); + } + return value; + } + + private: + std::span mValues; + }; + + inline std::unique_ptr> makeEnumSanitizerString( + std::span values) + { + return std::make_unique(values); + } + + class EnumSettingValue + { + public: + explicit EnumSettingValue(Settings::Index& index, std::string_view category, std::string_view name, + std::span values, std::size_t defaultValueIndex) + : mValue( + index, category, name, std::string(values[defaultValueIndex].mValue), makeEnumSanitizerString(values)) + , mEnumValues(values) + { + } + + Settings::SettingValue& getValue() { return mValue; } + + std::span getEnumValues() const { return mEnumValues; } + + private: + Settings::SettingValue mValue; + std::span mEnumValues; + }; + + struct WindowsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Windows"; + + static constexpr std::array sMainwindowScrollbarValues{ + EnumValueView{ + "Scrollbar Only", "Simple addition of scrollbars, the view window does not grow automatically." }, + EnumValueView{ "Grow Only", "The view window grows as subviews are added. No scrollbars." }, + EnumValueView{ + "Grow then Scroll", "The view window grows. The scrollbar appears once it cannot grow any further." }, + }; + + Settings::SettingValue mDefaultWidth{ mIndex, sName, "default-width", 800 }; + Settings::SettingValue mDefaultHeight{ mIndex, sName, "default-height", 600 }; + Settings::SettingValue mShowStatusbar{ mIndex, sName, "show-statusbar", true }; + Settings::SettingValue mReuse{ mIndex, sName, "reuse", true }; + Settings::SettingValue mMaxSubviews{ mIndex, sName, "max-subviews", 256 }; + Settings::SettingValue mHideSubview{ mIndex, sName, "hide-subview", false }; + Settings::SettingValue mMinimumWidth{ mIndex, sName, "minimum-width", 325 }; + EnumSettingValue mMainwindowScrollbar{ mIndex, sName, "mainwindow-scrollbar", sMainwindowScrollbarValues, 0 }; + Settings::SettingValue mGrowLimit{ mIndex, sName, "grow-limit", false }; + }; + + struct RecordsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Records"; + + static constexpr std::array sRecordValues{ + EnumValueView{ "Icon and Text", "" }, + EnumValueView{ "Icon Only", "" }, + EnumValueView{ "Text Only", "" }, + }; + + EnumSettingValue mStatusFormat{ mIndex, sName, "status-format", sRecordValues, 0 }; + EnumSettingValue mTypeFormat{ mIndex, sName, "type-format", sRecordValues, 0 }; + }; + + struct IdTablesCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "ID Tables"; + + static constexpr std::array sDoubleClickValues{ + EnumValueView{ "Edit in Place", "Edit the clicked cell" }, + EnumValueView{ "Edit Record", "Open a dialogue subview for the clicked record" }, + EnumValueView{ "View", "Open a scene subview for the clicked record (not available everywhere)" }, + EnumValueView{ "Revert", "" }, + EnumValueView{ "Delete", "" }, + EnumValueView{ "Edit Record and Close", "" }, + EnumValueView{ + "View and Close", "Open a scene subview for the clicked record and close the table subview" }, + }; + + static constexpr std::array sJumpAndSelectValues{ + EnumValueView{ "Jump and Select", "Scroll new record into view and make it the selection" }, + EnumValueView{ "Jump Only", "Scroll new record into view" }, + EnumValueView{ "No Jump", "No special action" }, + }; + + EnumSettingValue mDouble{ mIndex, sName, "double", sDoubleClickValues, 0 }; + EnumSettingValue mDoubleS{ mIndex, sName, "double-s", sDoubleClickValues, 1 }; + EnumSettingValue mDoubleC{ mIndex, sName, "double-c", sDoubleClickValues, 2 }; + EnumSettingValue mDoubleSc{ mIndex, sName, "double-sc", sDoubleClickValues, 5 }; + EnumSettingValue mJumpToAdded{ mIndex, sName, "jump-to-added", sJumpAndSelectValues, 0 }; + Settings::SettingValue mExtendedConfig{ mIndex, sName, "extended-config", false }; + Settings::SettingValue mSubviewNewWindow{ mIndex, sName, "subview-new-window", false }; + Settings::SettingValue mFilterDelay{ mIndex, sName, "filter-delay", 500 }; + }; + + struct IdDialoguesCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "ID Dialogues"; + + Settings::SettingValue mToolbar{ mIndex, sName, "toolbar", true }; + }; + + struct ReportsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Reports"; + + static constexpr std::array sReportValues{ + EnumValueView{ "None", "" }, + EnumValueView{ "Edit", "Open a table or dialogue suitable for addressing the listed report" }, + EnumValueView{ "Remove", "Remove the report from the report table" }, + EnumValueView{ "Edit And Remove", + "Open a table or dialogue suitable for addressing the listed report, then remove the report from the " + "report table" }, + }; + + EnumSettingValue mDouble{ mIndex, sName, "double", sReportValues, 1 }; + EnumSettingValue mDoubleS{ mIndex, sName, "double-s", sReportValues, 2 }; + EnumSettingValue mDoubleC{ mIndex, sName, "double-c", sReportValues, 3 }; + EnumSettingValue mDoubleSc{ mIndex, sName, "double-sc", sReportValues, 0 }; + Settings::SettingValue mIgnoreBaseRecords{ mIndex, sName, "ignore-base-records", false }; + }; + + struct SearchAndReplaceCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Search & Replace"; + + Settings::SettingValue mCharBefore{ mIndex, sName, "char-before", 10 }; + Settings::SettingValue mCharAfter{ mIndex, sName, "char-after", 10 }; + Settings::SettingValue mAutoDelete{ mIndex, sName, "auto-delete", true }; + }; + + struct ScriptsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Scripts"; + + static constexpr std::array sWarningValues{ + EnumValueView{ "Ignore", "Do not report warning" }, + EnumValueView{ "Normal", "Report warnings as warning" }, + EnumValueView{ "Strict", "Promote warning to an error" }, + }; + + Settings::SettingValue mShowLinenum{ mIndex, sName, "show-linenum", true }; + Settings::SettingValue mWrapLines{ mIndex, sName, "wrap-lines", false }; + Settings::SettingValue mMonoFont{ mIndex, sName, "mono-font", true }; + Settings::SettingValue mTabWidth{ mIndex, sName, "tab-width", 4 }; + EnumSettingValue mWarnings{ mIndex, sName, "warnings", sWarningValues, 1 }; + Settings::SettingValue mToolbar{ mIndex, sName, "toolbar", true }; + Settings::SettingValue mCompileDelay{ mIndex, sName, "compile-delay", 100 }; + Settings::SettingValue mErrorHeight{ mIndex, sName, "error-height", 100 }; + Settings::SettingValue mHighlightOccurrences{ mIndex, sName, "highlight-occurrences", true }; + Settings::SettingValue mColourHighlight{ mIndex, sName, "colour-highlight", "lightcyan" }; + Settings::SettingValue mColourInt{ mIndex, sName, "colour-int", "#aa55ff" }; + Settings::SettingValue mColourFloat{ mIndex, sName, "colour-float", "magenta" }; + Settings::SettingValue mColourName{ mIndex, sName, "colour-name", "grey" }; + Settings::SettingValue mColourKeyword{ mIndex, sName, "colour-keyword", "red" }; + Settings::SettingValue mColourSpecial{ mIndex, sName, "colour-special", "darkorange" }; + Settings::SettingValue mColourComment{ mIndex, sName, "colour-comment", "green" }; + Settings::SettingValue mColourId{ mIndex, sName, "colour-id", "#0055ff" }; + }; + + struct GeneralInputCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "General Input"; + + Settings::SettingValue mCycle{ mIndex, sName, "cycle", false }; + }; + + struct SceneInputCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "3D Scene Input"; + + Settings::SettingValue mNaviWheelFactor{ mIndex, sName, "navi-wheel-factor", 8 }; + Settings::SettingValue mSNaviSensitivity{ mIndex, sName, "s-navi-sensitivity", 50 }; + Settings::SettingValue mPNaviFreeSensitivity{ mIndex, sName, "p-navi-free-sensitivity", 1 / 650.0 }; + Settings::SettingValue mPNaviFreeInvert{ mIndex, sName, "p-navi-free-invert", false }; + Settings::SettingValue mNaviFreeLinSpeed{ mIndex, sName, "navi-free-lin-speed", 1000 }; + Settings::SettingValue mNaviFreeRotSpeed{ mIndex, sName, "navi-free-rot-speed", 3.14 / 2 }; + Settings::SettingValue mNaviFreeSpeedMult{ mIndex, sName, "navi-free-speed-mult", 8 }; + Settings::SettingValue mPNaviOrbitSensitivity{ mIndex, sName, "p-navi-orbit-sensitivity", 1 / 650.0 }; + Settings::SettingValue mPNaviOrbitInvert{ mIndex, sName, "p-navi-orbit-invert", false }; + Settings::SettingValue mNaviOrbitRotSpeed{ mIndex, sName, "navi-orbit-rot-speed", 3.14 / 4 }; + Settings::SettingValue mNaviOrbitSpeedMult{ mIndex, sName, "navi-orbit-speed-mult", 4 }; + Settings::SettingValue mNaviOrbitConstRoll{ mIndex, sName, "navi-orbit-const-roll", true }; + Settings::SettingValue mContextSelect{ mIndex, sName, "context-select", false }; + Settings::SettingValue mDragFactor{ mIndex, sName, "drag-factor", 1 }; + Settings::SettingValue mDragWheelFactor{ mIndex, sName, "drag-wheel-factor", 1 }; + Settings::SettingValue mDragShiftFactor{ mIndex, sName, "drag-shift-factor", 4 }; + Settings::SettingValue mRotateFactor{ mIndex, sName, "rotate-factor", 0.007 }; + }; + + struct RenderingCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Rendering"; + + Settings::SettingValue mFramerateLimit{ mIndex, sName, "framerate-limit", 60 }; + 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 mSceneUseGradient{ mIndex, sName, "scene-use-gradient", true }; + Settings::SettingValue mSceneDayBackgroundColour{ mIndex, sName, "scene-day-background-colour", + "#6e7880" }; + Settings::SettingValue mSceneDayGradientColour{ mIndex, sName, "scene-day-gradient-colour", + "#2f3333" }; + Settings::SettingValue mSceneBrightBackgroundColour{ mIndex, sName, + "scene-bright-background-colour", "#4f575c" }; + Settings::SettingValue mSceneBrightGradientColour{ mIndex, sName, "scene-bright-gradient-colour", + "#2f3333" }; + Settings::SettingValue mSceneNightBackgroundColour{ mIndex, sName, "scene-night-background-colour", + "#404d4f" }; + Settings::SettingValue mSceneNightGradientColour{ mIndex, sName, "scene-night-gradient-colour", + "#2f3333" }; + Settings::SettingValue mSceneDayNightSwitchNodes{ mIndex, sName, "scene-day-night-switch-nodes", true }; + }; + + struct TooltipsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Tooltips"; + + Settings::SettingValue mScene{ mIndex, sName, "scene", true }; + Settings::SettingValue mSceneHideBasic{ mIndex, sName, "scene-hide-basic", false }; + Settings::SettingValue mSceneDelay{ mIndex, sName, "scene-delay", 500 }; + }; + + struct SceneEditingCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "3D Scene Editing"; + + static constexpr std::array sInsertOutsideCellValues{ + EnumValueView{ "Create cell and insert", "" }, + EnumValueView{ "Discard", "" }, + EnumValueView{ "Insert anyway", "" }, + }; + + static constexpr std::array sInsertOutsideVisibleCellValues{ + EnumValueView{ "Show cell and insert", "" }, + EnumValueView{ "Discard", "" }, + EnumValueView{ "Insert anyway", "" }, + }; + + static constexpr std::array sLandEditOutsideCellValues{ + EnumValueView{ "Create cell and land, then edit", "" }, + EnumValueView{ "Discard", "" }, + }; + + static constexpr std::array sLandEditOutsideVisibleCellValues{ + EnumValueView{ "Show cell and edit", "" }, + EnumValueView{ "Discard", "" }, + }; + + static constexpr std::array sSelectAction{ + EnumValueView{ "Select only", "" }, + EnumValueView{ "Add to selection", "" }, + EnumValueView{ "Remove from selection", "" }, + EnumValueView{ "Invert selection", "" }, + }; + + Settings::SettingValue mGridsnapMovement{ mIndex, sName, "gridsnap-movement", 16 }; + Settings::SettingValue mGridsnapRotation{ mIndex, sName, "gridsnap-rotation", 15 }; + Settings::SettingValue mGridsnapScale{ mIndex, sName, "gridsnap-scale", 0.25 }; + Settings::SettingValue mDistance{ mIndex, sName, "distance", 50 }; + EnumSettingValue mOutsideDrop{ mIndex, sName, "outside-drop", sInsertOutsideCellValues, 0 }; + EnumSettingValue mOutsideVisibleDrop{ mIndex, sName, "outside-visible-drop", sInsertOutsideVisibleCellValues, + 0 }; + EnumSettingValue mOutsideLandedit{ mIndex, sName, "outside-landedit", sLandEditOutsideCellValues, 0 }; + EnumSettingValue mOutsideVisibleLandedit{ mIndex, sName, "outside-visible-landedit", + sLandEditOutsideVisibleCellValues, 0 }; + Settings::SettingValue mTexturebrushMaximumsize{ mIndex, sName, "texturebrush-maximumsize", 50 }; + Settings::SettingValue mShapebrushMaximumsize{ mIndex, sName, "shapebrush-maximumsize", 100 }; + Settings::SettingValue mLandeditPostSmoothpainting{ mIndex, sName, "landedit-post-smoothpainting", + false }; + Settings::SettingValue mLandeditPostSmoothstrength{ mIndex, sName, "landedit-post-smoothstrength", + 0.25 }; + Settings::SettingValue mOpenListView{ mIndex, sName, "open-list-view", false }; + EnumSettingValue mPrimarySelectAction{ mIndex, sName, "primary-select-action", sSelectAction, 0 }; + EnumSettingValue mSecondarySelectAction{ mIndex, sName, "secondary-select-action", sSelectAction, 1 }; + }; + + struct KeyBindingsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Key Bindings"; + + Settings::SettingValue mDocumentFileNewgame{ mIndex, sName, "document-file-newgame", "Ctrl+N" }; + Settings::SettingValue mDocumentFileNewaddon{ mIndex, sName, "document-file-newaddon", "" }; + Settings::SettingValue mDocumentFileOpen{ mIndex, sName, "document-file-open", "Ctrl+O" }; + Settings::SettingValue mDocumentFileSave{ mIndex, sName, "document-file-save", "Ctrl+S" }; + Settings::SettingValue mDocumentHelpHelp{ mIndex, sName, "document-help-help", "F1" }; + Settings::SettingValue mDocumentHelpTutorial{ mIndex, sName, "document-help-tutorial", "" }; + Settings::SettingValue mDocumentFileVerify{ mIndex, sName, "document-file-verify", "" }; + Settings::SettingValue mDocumentFileMerge{ mIndex, sName, "document-file-merge", "" }; + Settings::SettingValue mDocumentFileErrorlog{ mIndex, sName, "document-file-errorlog", "" }; + Settings::SettingValue mDocumentFileMetadata{ mIndex, sName, "document-file-metadata", "" }; + Settings::SettingValue mDocumentFileClose{ mIndex, sName, "document-file-close", "Ctrl+W" }; + Settings::SettingValue mDocumentFileExit{ mIndex, sName, "document-file-exit", "Ctrl+Q" }; + Settings::SettingValue mDocumentEditUndo{ mIndex, sName, "document-edit-undo", "Ctrl+Z" }; + Settings::SettingValue mDocumentEditRedo{ mIndex, sName, "document-edit-redo", "Ctrl+Shift+Z" }; + Settings::SettingValue mDocumentEditPreferences{ mIndex, sName, "document-edit-preferences", "" }; + Settings::SettingValue mDocumentEditSearch{ mIndex, sName, "document-edit-search", "Ctrl+F" }; + Settings::SettingValue mDocumentViewNewview{ mIndex, sName, "document-view-newview", "" }; + Settings::SettingValue mDocumentViewStatusbar{ mIndex, sName, "document-view-statusbar", "" }; + Settings::SettingValue mDocumentViewFilters{ mIndex, sName, "document-view-filters", "" }; + Settings::SettingValue mDocumentWorldRegions{ mIndex, sName, "document-world-regions", "" }; + Settings::SettingValue mDocumentWorldCells{ mIndex, sName, "document-world-cells", "" }; + Settings::SettingValue mDocumentWorldReferencables{ mIndex, sName, "document-world-referencables", + "" }; + Settings::SettingValue mDocumentWorldReferences{ mIndex, sName, "document-world-references", "" }; + Settings::SettingValue mDocumentWorldLands{ mIndex, sName, "document-world-lands", "" }; + Settings::SettingValue mDocumentWorldLandtextures{ mIndex, sName, "document-world-landtextures", + "" }; + Settings::SettingValue mDocumentWorldPathgrid{ mIndex, sName, "document-world-pathgrid", "" }; + Settings::SettingValue mDocumentWorldRegionmap{ mIndex, sName, "document-world-regionmap", "" }; + Settings::SettingValue mDocumentMechanicsGlobals{ mIndex, sName, "document-mechanics-globals", + "" }; + Settings::SettingValue mDocumentMechanicsGamesettings{ mIndex, sName, + "document-mechanics-gamesettings", "" }; + Settings::SettingValue mDocumentMechanicsScripts{ mIndex, sName, "document-mechanics-scripts", + "" }; + Settings::SettingValue mDocumentMechanicsSpells{ mIndex, sName, "document-mechanics-spells", "" }; + Settings::SettingValue mDocumentMechanicsEnchantments{ mIndex, sName, + "document-mechanics-enchantments", "" }; + Settings::SettingValue mDocumentMechanicsMagiceffects{ mIndex, sName, + "document-mechanics-magiceffects", "" }; + Settings::SettingValue mDocumentMechanicsStartscripts{ mIndex, sName, + "document-mechanics-startscripts", "" }; + Settings::SettingValue mDocumentCharacterSkills{ mIndex, sName, "document-character-skills", "" }; + Settings::SettingValue mDocumentCharacterClasses{ mIndex, sName, "document-character-classes", + "" }; + Settings::SettingValue mDocumentCharacterFactions{ mIndex, sName, "document-character-factions", + "" }; + Settings::SettingValue mDocumentCharacterRaces{ mIndex, sName, "document-character-races", "" }; + Settings::SettingValue mDocumentCharacterBirthsigns{ mIndex, sName, + "document-character-birthsigns", "" }; + Settings::SettingValue mDocumentCharacterTopics{ mIndex, sName, "document-character-topics", "" }; + Settings::SettingValue mDocumentCharacterJournals{ mIndex, sName, "document-character-journals", + "" }; + Settings::SettingValue mDocumentCharacterTopicinfos{ mIndex, sName, + "document-character-topicinfos", "" }; + Settings::SettingValue mDocumentCharacterJournalinfos{ mIndex, sName, + "document-character-journalinfos", "" }; + Settings::SettingValue mDocumentCharacterBodyparts{ mIndex, sName, "document-character-bodyparts", + "" }; + Settings::SettingValue mDocumentAssetsReload{ mIndex, sName, "document-assets-reload", "F5" }; + Settings::SettingValue mDocumentAssetsSounds{ mIndex, sName, "document-assets-sounds", "" }; + Settings::SettingValue mDocumentAssetsSoundgens{ mIndex, sName, "document-assets-soundgens", "" }; + Settings::SettingValue mDocumentAssetsMeshes{ mIndex, sName, "document-assets-meshes", "" }; + Settings::SettingValue mDocumentAssetsIcons{ mIndex, sName, "document-assets-icons", "" }; + Settings::SettingValue mDocumentAssetsMusic{ mIndex, sName, "document-assets-music", "" }; + Settings::SettingValue mDocumentAssetsSoundres{ mIndex, sName, "document-assets-soundres", "" }; + Settings::SettingValue mDocumentAssetsTextures{ mIndex, sName, "document-assets-textures", "" }; + Settings::SettingValue mDocumentAssetsVideos{ mIndex, sName, "document-assets-videos", "" }; + Settings::SettingValue mDocumentDebugRun{ mIndex, sName, "document-debug-run", "" }; + Settings::SettingValue mDocumentDebugShutdown{ mIndex, sName, "document-debug-shutdown", "" }; + Settings::SettingValue mDocumentDebugProfiles{ mIndex, sName, "document-debug-profiles", "" }; + Settings::SettingValue mDocumentDebugRunlog{ mIndex, sName, "document-debug-runlog", "" }; + Settings::SettingValue mTableEdit{ mIndex, sName, "table-edit", "" }; + Settings::SettingValue mTableAdd{ mIndex, sName, "table-add", "Shift+A" }; + Settings::SettingValue mTableClone{ mIndex, sName, "table-clone", "Shift+D" }; + Settings::SettingValue mTouchRecord{ mIndex, sName, "touch-record", "" }; + Settings::SettingValue mTableRevert{ mIndex, sName, "table-revert", "" }; + Settings::SettingValue mTableRemove{ mIndex, sName, "table-remove", "Delete" }; + Settings::SettingValue mTableMoveup{ mIndex, sName, "table-moveup", "" }; + Settings::SettingValue mTableMovedown{ mIndex, sName, "table-movedown", "" }; + Settings::SettingValue mTableView{ mIndex, sName, "table-view", "Shift+C" }; + Settings::SettingValue mTablePreview{ mIndex, sName, "table-preview", "Shift+V" }; + Settings::SettingValue mTableExtendeddelete{ mIndex, sName, "table-extendeddelete", "" }; + Settings::SettingValue mTableExtendedrevert{ mIndex, sName, "table-extendedrevert", "" }; + Settings::SettingValue mReporttableShow{ mIndex, sName, "reporttable-show", "" }; + Settings::SettingValue mReporttableRemove{ mIndex, sName, "reporttable-remove", "Delete" }; + Settings::SettingValue mReporttableReplace{ mIndex, sName, "reporttable-replace", "" }; + Settings::SettingValue mReporttableRefresh{ mIndex, sName, "reporttable-refresh", "" }; + Settings::SettingValue mSceneNaviPrimary{ mIndex, sName, "scene-navi-primary", "MMB" }; + Settings::SettingValue mSceneNaviSecondary{ mIndex, sName, "scene-navi-secondary", "Ctrl+MMB" }; + Settings::SettingValue mSceneOpenPrimary{ mIndex, sName, "scene-open-primary", "Shift+RMB" }; + Settings::SettingValue mSceneEditPrimary{ mIndex, sName, "scene-edit-primary", "RMB" }; + Settings::SettingValue mSceneEditSecondary{ mIndex, sName, "scene-edit-secondary", "Ctrl+RMB" }; + Settings::SettingValue mSceneSelectPrimary{ mIndex, sName, "scene-select-primary", "LMB" }; + Settings::SettingValue mSceneSelectSecondary{ mIndex, sName, "scene-select-secondary", + "Ctrl+LMB" }; + Settings::SettingValue mSceneSelectTertiary{ mIndex, sName, "scene-select-tertiary", "Shift+LMB" }; + Settings::SettingValue mSceneSpeedModifier{ mIndex, sName, "scene-speed-modifier", "Shift" }; + Settings::SettingValue mSceneDelete{ mIndex, sName, "scene-delete", "Delete" }; + Settings::SettingValue mSceneInstanceDropTerrain{ mIndex, sName, "scene-instance-drop-terrain", + "B" }; + Settings::SettingValue mSceneInstanceDropCollision{ mIndex, sName, "scene-instance-drop-collision", + "H" }; + Settings::SettingValue mSceneInstanceDropTerrainSeparately{ mIndex, sName, + "scene-instance-drop-terrain-separately", "" }; + Settings::SettingValue mSceneInstanceDropCollisionSeparately{ mIndex, sName, + "scene-instance-drop-collision-separately", "" }; + Settings::SettingValue mSceneDuplicate{ mIndex, sName, "scene-duplicate", "Shift+C" }; + Settings::SettingValue mSceneLoadCamCell{ mIndex, sName, "scene-load-cam-cell", "Keypad+5" }; + Settings::SettingValue mSceneLoadCamEastcell{ mIndex, sName, "scene-load-cam-eastcell", + "Keypad+6" }; + Settings::SettingValue mSceneLoadCamNorthcell{ mIndex, sName, "scene-load-cam-northcell", + "Keypad+8" }; + Settings::SettingValue mSceneLoadCamWestcell{ mIndex, sName, "scene-load-cam-westcell", + "Keypad+4" }; + Settings::SettingValue mSceneLoadCamSouthcell{ mIndex, sName, "scene-load-cam-southcell", + "Keypad+2" }; + Settings::SettingValue mSceneEditAbort{ mIndex, sName, "scene-edit-abort", "Escape" }; + Settings::SettingValue mSceneFocusToolbar{ mIndex, sName, "scene-focus-toolbar", "T" }; + Settings::SettingValue mSceneRenderStats{ mIndex, sName, "scene-render-stats", "F3" }; + Settings::SettingValue mSceneClearSelection{ mIndex, sName, "scene-clear-selection", "Space" }; + Settings::SettingValue mSceneUnhideAll{ mIndex, sName, "scene-unhide-all", "Alt+H" }; + Settings::SettingValue mSceneToggleVisibility{ mIndex, sName, "scene-toggle-visibility", "H" }; + Settings::SettingValue mSceneGroup0{ mIndex, sName, "scene-group-0", "0" }; + Settings::SettingValue mSceneSave0{ mIndex, sName, "scene-save-0", "Ctrl+0" }; + Settings::SettingValue mSceneGroup1{ mIndex, sName, "scene-group-1", "1" }; + Settings::SettingValue mSceneSave1{ mIndex, sName, "scene-save-1", "Ctrl+1" }; + Settings::SettingValue mSceneGroup2{ mIndex, sName, "scene-group-2", "2" }; + Settings::SettingValue mSceneSave2{ mIndex, sName, "scene-save-2", "Ctrl+2" }; + Settings::SettingValue mSceneGroup3{ mIndex, sName, "scene-group-3", "3" }; + Settings::SettingValue mSceneSave3{ mIndex, sName, "scene-save-3", "Ctrl+3" }; + Settings::SettingValue mSceneGroup4{ mIndex, sName, "scene-group-4", "4" }; + Settings::SettingValue mSceneSave4{ mIndex, sName, "scene-save-4", "Ctrl+4" }; + Settings::SettingValue mSceneGroup5{ mIndex, sName, "scene-group-5", "5" }; + Settings::SettingValue mSceneSave5{ mIndex, sName, "scene-save-5", "Ctrl+5" }; + Settings::SettingValue mSceneGroup6{ mIndex, sName, "scene-group-6", "6" }; + Settings::SettingValue mSceneSave6{ mIndex, sName, "scene-save-6", "Ctrl+6" }; + Settings::SettingValue mSceneGroup7{ mIndex, sName, "scene-group-7", "7" }; + Settings::SettingValue mSceneSave7{ mIndex, sName, "scene-save-7", "Ctrl+7" }; + Settings::SettingValue mSceneGroup8{ mIndex, sName, "scene-group-8", "8" }; + Settings::SettingValue mSceneSave8{ mIndex, sName, "scene-save-8", "Ctrl+8" }; + Settings::SettingValue mSceneGroup9{ mIndex, sName, "scene-group-9", "9" }; + Settings::SettingValue mSceneSave9{ mIndex, sName, "scene-save-9", "Ctrl+9" }; + Settings::SettingValue mSceneAxisX{ mIndex, sName, "scene-axis-x", "X" }; + Settings::SettingValue mSceneAxisY{ mIndex, sName, "scene-axis-y", "Y" }; + Settings::SettingValue mSceneAxisZ{ mIndex, sName, "scene-axis-z", "Z" }; + Settings::SettingValue mSceneMoveSubmode{ mIndex, sName, "scene-submode-move", "G" }; + 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 mFreeForward{ mIndex, sName, "free-forward", "W" }; + Settings::SettingValue mFreeBackward{ mIndex, sName, "free-backward", "S" }; + Settings::SettingValue mFreeLeft{ mIndex, sName, "free-left", "A" }; + Settings::SettingValue mFreeRight{ mIndex, sName, "free-right", "D" }; + Settings::SettingValue mFreeRollLeft{ mIndex, sName, "free-roll-left", "Q" }; + Settings::SettingValue mFreeRollRight{ mIndex, sName, "free-roll-right", "E" }; + Settings::SettingValue mFreeSpeedMode{ mIndex, sName, "free-speed-mode", "F" }; + Settings::SettingValue mOrbitUp{ mIndex, sName, "orbit-up", "W" }; + Settings::SettingValue mOrbitDown{ mIndex, sName, "orbit-down", "S" }; + Settings::SettingValue mOrbitLeft{ mIndex, sName, "orbit-left", "A" }; + Settings::SettingValue mOrbitRight{ mIndex, sName, "orbit-right", "D" }; + Settings::SettingValue mOrbitRollLeft{ mIndex, sName, "orbit-roll-left", "Q" }; + Settings::SettingValue mOrbitRollRight{ mIndex, sName, "orbit-roll-right", "E" }; + Settings::SettingValue mOrbitSpeedMode{ mIndex, sName, "orbit-speed-mode", "F" }; + Settings::SettingValue mOrbitCenterSelection{ mIndex, sName, "orbit-center-selection", "C" }; + Settings::SettingValue mScriptEditorComment{ mIndex, sName, "script-editor-comment", "" }; + Settings::SettingValue mScriptEditorUncomment{ mIndex, sName, "script-editor-uncomment", "" }; + }; + + struct ModelsCategory : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + static constexpr std::string_view sName = "Models"; + + Settings::SettingValue mBaseanim{ mIndex, sName, "baseanim", "meshes/base_anim.nif" }; + Settings::SettingValue mBaseanimkna{ mIndex, sName, "baseanimkna", "meshes/base_animkna.nif" }; + Settings::SettingValue mBaseanimfemale{ mIndex, sName, "baseanimfemale", + "meshes/base_anim_female.nif" }; + Settings::SettingValue mWolfskin{ mIndex, sName, "wolfskin", "meshes/wolf/skin.nif" }; + }; + + struct Values : Settings::WithIndex + { + using Settings::WithIndex::WithIndex; + + WindowsCategory mWindows{ mIndex }; + RecordsCategory mRecords{ mIndex }; + IdTablesCategory mIdTables{ mIndex }; + IdDialoguesCategory mIdDialogues{ mIndex }; + ReportsCategory mReports{ mIndex }; + SearchAndReplaceCategory mSearchAndReplace{ mIndex }; + ScriptsCategory mScripts{ mIndex }; + GeneralInputCategory mGeneralInput{ mIndex }; + SceneInputCategory mSceneInput{ mIndex }; + RenderingCategory mRendering{ mIndex }; + TooltipsCategory mTooltips{ mIndex }; + SceneEditingCategory mSceneEditing{ mIndex }; + KeyBindingsCategory mKeyBindings{ mIndex }; + ModelsCategory mModels{ mIndex }; + }; +} + +#endif diff --git a/apps/opencs/model/tools/birthsigncheck.cpp b/apps/opencs/model/tools/birthsigncheck.cpp index f91fc22f605..f642ab3a197 100644 --- a/apps/opencs/model/tools/birthsigncheck.cpp +++ b/apps/opencs/model/tools/birthsigncheck.cpp @@ -1,15 +1,25 @@ #include "birthsigncheck.hpp" +#include + +#include +#include +#include +#include +#include +#include + +#include #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" -CSMTools::BirthsignCheckStage::BirthsignCheckStage (const CSMWorld::IdCollection& birthsigns, - const CSMWorld::Resources &textures) -: mBirthsigns(birthsigns), - mTextures(textures) +CSMTools::BirthsignCheckStage::BirthsignCheckStage( + const CSMWorld::IdCollection& birthsigns, const CSMWorld::Resources& textures) + : mBirthsigns(birthsigns) + , mTextures(textures) { mIgnoreBaseRecords = false; } @@ -21,9 +31,9 @@ int CSMTools::BirthsignCheckStage::setup() return mBirthsigns.getSize(); } -void CSMTools::BirthsignCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::BirthsignCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mBirthsigns.getRecord (stage); + const CSMWorld::Record& record = mBirthsigns.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) @@ -31,7 +41,7 @@ void CSMTools::BirthsignCheckStage::perform (int stage, CSMDoc::Messages& messag const ESM::BirthSign& birthsign = record.get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Birthsign, birthsign.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Birthsign, birthsign.mId); if (birthsign.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); @@ -45,7 +55,7 @@ void CSMTools::BirthsignCheckStage::perform (int stage, CSMDoc::Messages& messag { std::string ddsTexture = birthsign.mTexture; if (!(Misc::ResourceHelpers::changeExtensionToDds(ddsTexture) && mTextures.searchId(ddsTexture) != -1)) - messages.add(id, "Image '" + birthsign.mTexture + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Image '" + birthsign.mTexture + "' does not exist", "", CSMDoc::Message::Severity_Error); } /// \todo check data members that can't be edited in the table view diff --git a/apps/opencs/model/tools/birthsigncheck.hpp b/apps/opencs/model/tools/birthsigncheck.hpp index 498894f882c..d01beb02459 100644 --- a/apps/opencs/model/tools/birthsigncheck.hpp +++ b/apps/opencs/model/tools/birthsigncheck.hpp @@ -1,32 +1,43 @@ #ifndef CSM_TOOLS_BIRTHSIGNCHECK_H #define CSM_TOOLS_BIRTHSIGNCHECK_H -#include - #include "../world/idcollection.hpp" -#include "../world/resources.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace CSMWorld +{ + class Resources; +} + +namespace ESM +{ + struct BirthSign; +} + namespace CSMTools { /// \brief VerifyStage: make sure that birthsign records are internally consistent class BirthsignCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection &mBirthsigns; - const CSMWorld::Resources &mTextures; - bool mIgnoreBaseRecords; - - public: + const CSMWorld::IdCollection& mBirthsigns; + const CSMWorld::Resources& mTextures; + bool mIgnoreBaseRecords; - BirthsignCheckStage (const CSMWorld::IdCollection &birthsigns, - const CSMWorld::Resources &textures); + public: + BirthsignCheckStage( + const CSMWorld::IdCollection& birthsigns, const CSMWorld::Resources& textures); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/bodypartcheck.cpp b/apps/opencs/model/tools/bodypartcheck.cpp index 1490a81039f..4fc9f5cf83d 100644 --- a/apps/opencs/model/tools/bodypartcheck.cpp +++ b/apps/opencs/model/tools/bodypartcheck.cpp @@ -1,14 +1,24 @@ #include "bodypartcheck.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + #include "../prefs/state.hpp" -CSMTools::BodyPartCheckStage::BodyPartCheckStage( - const CSMWorld::IdCollection &bodyParts, - const CSMWorld::Resources &meshes, - const CSMWorld::IdCollection &races ) : - mBodyParts(bodyParts), - mMeshes(meshes), - mRaces(races) +CSMTools::BodyPartCheckStage::BodyPartCheckStage(const CSMWorld::IdCollection& bodyParts, + const CSMWorld::Resources& meshes, const CSMWorld::IdCollection& races) + : mBodyParts(bodyParts) + , mMeshes(meshes) + , mRaces(races) { mIgnoreBaseRecords = false; } @@ -20,37 +30,38 @@ int CSMTools::BodyPartCheckStage::setup() return mBodyParts.getSize(); } -void CSMTools::BodyPartCheckStage::perform (int stage, CSMDoc::Messages &messages) +void CSMTools::BodyPartCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record &record = mBodyParts.getRecord(stage); + const CSMWorld::Record& record = mBodyParts.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; - const ESM::BodyPart &bodyPart = record.get(); + const ESM::BodyPart& bodyPart = record.get(); - CSMWorld::UniversalId id( CSMWorld::UniversalId::Type_BodyPart, bodyPart.mId ); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_BodyPart, bodyPart.mId); // Check BYDT - if (bodyPart.mData.mPart >= ESM::BodyPart::MP_Count ) + if (bodyPart.mData.mPart >= ESM::BodyPart::MP_Count) messages.add(id, "Invalid part", "", CSMDoc::Message::Severity_Error); - if (bodyPart.mData.mType > ESM::BodyPart::MT_Armor ) + if (bodyPart.mData.mType > ESM::BodyPart::MT_Armor) messages.add(id, "Invalid type", "", CSMDoc::Message::Severity_Error); // Check MODL - if ( bodyPart.mModel.empty() ) + if (bodyPart.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); - else if ( mMeshes.searchId( bodyPart.mModel ) == -1 ) + else if (mMeshes.searchId(bodyPart.mModel) == -1) messages.add(id, "Model '" + bodyPart.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); // Check FNAM for skin body parts (for non-skin body parts it's meaningless) - if ( bodyPart.mData.mType == ESM::BodyPart::MT_Skin ) + if (bodyPart.mData.mType == ESM::BodyPart::MT_Skin) { - if ( bodyPart.mRace.empty() ) + if (bodyPart.mRace.empty()) messages.add(id, "Race is missing", "", CSMDoc::Message::Severity_Error); - else if ( mRaces.searchId( bodyPart.mRace ) == -1 ) - messages.add(id, "Race '" + bodyPart.mRace + "' does not exist", "", CSMDoc::Message::Severity_Error); + else if (mRaces.searchId(bodyPart.mRace) == -1) + messages.add(id, "Race '" + bodyPart.mRace.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); } } diff --git a/apps/opencs/model/tools/bodypartcheck.hpp b/apps/opencs/model/tools/bodypartcheck.hpp index 2c379bd0782..b945668e5eb 100644 --- a/apps/opencs/model/tools/bodypartcheck.hpp +++ b/apps/opencs/model/tools/bodypartcheck.hpp @@ -1,34 +1,46 @@ #ifndef CSM_TOOLS_BODYPARTCHECK_H #define CSM_TOOLS_BODYPARTCHECK_H -#include -#include +#include -#include "../world/resources.hpp" #include "../world/idcollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace CSMWorld +{ + class Resources; +} + +namespace ESM +{ + struct BodyPart; + struct Race; +} + namespace CSMTools { /// \brief VerifyStage: make sure that body part records are internally consistent class BodyPartCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection &mBodyParts; - const CSMWorld::Resources &mMeshes; - const CSMWorld::IdCollection &mRaces; - bool mIgnoreBaseRecords; + const CSMWorld::IdCollection& mBodyParts; + const CSMWorld::Resources& mMeshes; + const CSMWorld::IdCollection& mRaces; + bool mIgnoreBaseRecords; public: - BodyPartCheckStage( - const CSMWorld::IdCollection &bodyParts, - const CSMWorld::Resources &meshes, - const CSMWorld::IdCollection &races ); + BodyPartCheckStage(const CSMWorld::IdCollection& bodyParts, const CSMWorld::Resources& meshes, + const CSMWorld::IdCollection& races); int setup() override; ///< \return number of steps - void perform(int stage, CSMDoc::Messages &messages) override; + void perform(int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/classcheck.cpp b/apps/opencs/model/tools/classcheck.cpp index a82121597cb..0cbf5562fc1 100644 --- a/apps/opencs/model/tools/classcheck.cpp +++ b/apps/opencs/model/tools/classcheck.cpp @@ -1,16 +1,23 @@ #include "classcheck.hpp" #include +#include +#include -#include -#include +#include +#include +#include +#include +#include +#include -#include "../prefs/state.hpp" +#include +#include -#include "../world/universalid.hpp" +#include "../prefs/state.hpp" -CSMTools::ClassCheckStage::ClassCheckStage (const CSMWorld::IdCollection& classes) -: mClasses (classes) +CSMTools::ClassCheckStage::ClassCheckStage(const CSMWorld::IdCollection& classes) + : mClasses(classes) { mIgnoreBaseRecords = false; } @@ -22,9 +29,9 @@ int CSMTools::ClassCheckStage::setup() return mClasses.getSize(); } -void CSMTools::ClassCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::ClassCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mClasses.getRecord (stage); + const CSMWorld::Record& record = mClasses.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) @@ -32,7 +39,7 @@ void CSMTools::ClassCheckStage::perform (int stage, CSMDoc::Messages& messages) const ESM::Class& class_ = record.get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Class, class_.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Class, class_.mId); // A class should have a name if (class_.mName.empty()) @@ -43,27 +50,37 @@ void CSMTools::ClassCheckStage::perform (int stage, CSMDoc::Messages& messages) messages.add(id, "Description of a playable class is missing", "", CSMDoc::Message::Severity_Warning); // test for invalid attributes - for (int i=0; i<2; ++i) - if (class_.mData.mAttribute[i]==-1) + std::map attributeCount; + for (size_t i = 0; i < class_.mData.mAttribute.size(); ++i) + { + int attribute = class_.mData.mAttribute[i]; + if (attribute == -1) + messages.add(id, "Attribute #" + std::to_string(i) + " is not set", {}, CSMDoc::Message::Severity_Error); + else { - messages.add(id, "Attribute #" + std::to_string(i) + " is not set", "", CSMDoc::Message::Severity_Error); + auto it = attributeCount.find(attribute); + if (it == attributeCount.end()) + attributeCount.emplace(attribute, 1); + else + { + if (it->second == 1) + messages.add(id, "Same attribute is listed twice", {}, CSMDoc::Message::Severity_Error); + ++it->second; + } } - - if (class_.mData.mAttribute[0]==class_.mData.mAttribute[1] && class_.mData.mAttribute[0]!=-1) - { - messages.add(id, "Same attribute is listed twice", "", CSMDoc::Message::Severity_Error); } // test for non-unique skill std::map skills; // ID, number of occurrences - for (int i=0; i<5; ++i) - for (int i2=0; i2<2; ++i2) - ++skills[class_.mData.mSkills[i][i2]]; + for (const auto& s : class_.mData.mSkills) + for (int skill : s) + ++skills[skill]; - for (auto &skill : skills) - if (skill.second>1) + for (auto& skill : skills) + if (skill.second > 1) { - messages.add(id, "Skill " + ESM::Skill::indexToId (skill.first) + " is listed more than once", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Skill " + ESM::Skill::indexToRefId(skill.first).toString() + " is listed more than once", + "", CSMDoc::Message::Severity_Error); } } diff --git a/apps/opencs/model/tools/classcheck.hpp b/apps/opencs/model/tools/classcheck.hpp index a78c2eb9756..9830a2b9d93 100644 --- a/apps/opencs/model/tools/classcheck.hpp +++ b/apps/opencs/model/tools/classcheck.hpp @@ -1,29 +1,36 @@ #ifndef CSM_TOOLS_CLASSCHECK_H #define CSM_TOOLS_CLASSCHECK_H -#include - #include "../world/idcollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace ESM +{ + struct Class; +} + namespace CSMTools { /// \brief VerifyStage: make sure that class records are internally consistent class ClassCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection& mClasses; - bool mIgnoreBaseRecords; - - public: + const CSMWorld::IdCollection& mClasses; + bool mIgnoreBaseRecords; - ClassCheckStage (const CSMWorld::IdCollection& classes); + public: + ClassCheckStage(const CSMWorld::IdCollection& classes); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/effectlistcheck.cpp b/apps/opencs/model/tools/effectlistcheck.cpp new file mode 100644 index 00000000000..b8695bc4195 --- /dev/null +++ b/apps/opencs/model/tools/effectlistcheck.cpp @@ -0,0 +1,88 @@ +#include "effectlistcheck.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace CSMTools +{ + void effectListCheck( + const std::vector& list, CSMDoc::Messages& messages, const CSMWorld::UniversalId& id) + { + if (list.empty()) + { + messages.add(id, "No magic effects", "", CSMDoc::Message::Severity_Warning); + return; + } + + size_t i = 1; + for (const ESM::IndexedENAMstruct& effect : list) + { + const std::string number = std::to_string(i); + + // At the time of writing this effects, attributes and skills are mostly hardcoded + if (effect.mData.mEffectID < 0 || effect.mData.mEffectID >= ESM::MagicEffect::Length) + messages.add(id, "Effect #" + number + ": invalid effect ID", "", CSMDoc::Message::Severity_Error); + if (effect.mData.mSkill < -1 || effect.mData.mSkill >= ESM::Skill::Length) + messages.add(id, "Effect #" + number + ": invalid skill", "", CSMDoc::Message::Severity_Error); + if (effect.mData.mAttribute < -1 || effect.mData.mAttribute >= ESM::Attribute::Length) + messages.add(id, "Effect #" + number + ": invalid attribute", "", CSMDoc::Message::Severity_Error); + + if (effect.mData.mRange < ESM::RT_Self || effect.mData.mRange > ESM::RT_Target) + messages.add(id, "Effect #" + number + ": invalid range", "", CSMDoc::Message::Severity_Error); + + if (effect.mData.mArea < 0) + messages.add(id, "Effect #" + number + ": negative area", "", CSMDoc::Message::Severity_Error); + + if (effect.mData.mDuration < 0) + messages.add(id, "Effect #" + number + ": negative duration", "", CSMDoc::Message::Severity_Error); + + if (effect.mData.mMagnMin < 0) + messages.add( + id, "Effect #" + number + ": negative minimum magnitude", "", CSMDoc::Message::Severity_Error); + + if (effect.mData.mMagnMax < 0) + messages.add( + id, "Effect #" + number + ": negative maximum magnitude", "", CSMDoc::Message::Severity_Error); + else if (effect.mData.mMagnMax == 0) + messages.add( + id, "Effect #" + number + ": zero maximum magnitude", "", CSMDoc::Message::Severity_Warning); + + if (effect.mData.mMagnMin > effect.mData.mMagnMax) + messages.add(id, "Effect #" + number + ": minimum magnitude is higher than maximum magnitude", "", + CSMDoc::Message::Severity_Error); + + ++i; + } + } + + void ingredientEffectListCheck( + const ESM::Ingredient& ingredient, CSMDoc::Messages& messages, const CSMWorld::UniversalId& id) + { + bool hasEffects = false; + + for (size_t i = 0; i < 4; i++) + { + if (ingredient.mData.mEffectID[i] == -1) + continue; + + hasEffects = true; + + const std::string number = std::to_string(i + 1); + if (ingredient.mData.mEffectID[i] < -1 || ingredient.mData.mEffectID[i] >= ESM::MagicEffect::Length) + messages.add(id, "Effect #" + number + ": invalid effect ID", "", CSMDoc::Message::Severity_Error); + if (ingredient.mData.mSkills[i] < -1 || ingredient.mData.mSkills[i] >= ESM::Skill::Length) + messages.add(id, "Effect #" + number + ": invalid skill", "", CSMDoc::Message::Severity_Error); + if (ingredient.mData.mAttributes[i] < -1 || ingredient.mData.mAttributes[i] >= ESM::Attribute::Length) + messages.add(id, "Effect #" + number + ": invalid attribute", "", CSMDoc::Message::Severity_Error); + } + + if (!hasEffects) + messages.add(id, "No magic effects", "", CSMDoc::Message::Severity_Warning); + } +} diff --git a/apps/opencs/model/tools/effectlistcheck.hpp b/apps/opencs/model/tools/effectlistcheck.hpp new file mode 100644 index 00000000000..832f3650eba --- /dev/null +++ b/apps/opencs/model/tools/effectlistcheck.hpp @@ -0,0 +1,30 @@ +#ifndef CSM_TOOLS_EFFECTLISTCHECK_H +#define CSM_TOOLS_EFFECTLISTCHECK_H + +#include + +namespace ESM +{ + struct IndexedENAMstruct; + struct Ingredient; +} + +namespace CSMDoc +{ + class Messages; +} + +namespace CSMWorld +{ + class UniversalId; +} + +namespace CSMTools +{ + void effectListCheck( + const std::vector& list, CSMDoc::Messages& messages, const CSMWorld::UniversalId& id); + void ingredientEffectListCheck( + const ESM::Ingredient& ingredient, CSMDoc::Messages& messages, const CSMWorld::UniversalId& id); +} + +#endif diff --git a/apps/opencs/model/tools/enchantmentcheck.cpp b/apps/opencs/model/tools/enchantmentcheck.cpp index 28f2b32cb95..8bd7d6cc4c6 100644 --- a/apps/opencs/model/tools/enchantmentcheck.cpp +++ b/apps/opencs/model/tools/enchantmentcheck.cpp @@ -1,11 +1,25 @@ #include "enchantmentcheck.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + #include "../prefs/state.hpp" #include "../world/universalid.hpp" -CSMTools::EnchantmentCheckStage::EnchantmentCheckStage (const CSMWorld::IdCollection& enchantments) - : mEnchantments (enchantments) +#include "effectlistcheck.hpp" + +CSMTools::EnchantmentCheckStage::EnchantmentCheckStage(const CSMWorld::IdCollection& enchantments) + : mEnchantments(enchantments) { mIgnoreBaseRecords = false; } @@ -17,9 +31,9 @@ int CSMTools::EnchantmentCheckStage::setup() return mEnchantments.getSize(); } -void CSMTools::EnchantmentCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::EnchantmentCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mEnchantments.getRecord (stage); + const CSMWorld::Record& record = mEnchantments.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) @@ -27,7 +41,7 @@ void CSMTools::EnchantmentCheckStage::perform (int stage, CSMDoc::Messages& mess const ESM::Enchantment& enchantment = record.get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Enchantment, enchantment.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Enchantment, enchantment.mId); if (enchantment.mData.mType < 0 || enchantment.mData.mType > 3) messages.add(id, "Invalid type", "", CSMDoc::Message::Severity_Error); @@ -41,42 +55,5 @@ void CSMTools::EnchantmentCheckStage::perform (int stage, CSMDoc::Messages& mess if (enchantment.mData.mCost > enchantment.mData.mCharge) messages.add(id, "Cost is higher than charge", "", CSMDoc::Message::Severity_Error); - if (enchantment.mEffects.mList.empty()) - { - messages.add(id, "Enchantment doesn't have any magic effects", "", CSMDoc::Message::Severity_Warning); - } - else - { - std::vector::const_iterator effect = enchantment.mEffects.mList.begin(); - - for (size_t i = 1; i <= enchantment.mEffects.mList.size(); i++) - { - const std::string number = std::to_string(i); - // At the time of writing this effects, attributes and skills are hardcoded - if (effect->mEffectID < 0 || effect->mEffectID > 142) - { - messages.add(id, "Effect #" + number + " is invalid", "", CSMDoc::Message::Severity_Error); - ++effect; - continue; - } - - if (effect->mSkill < -1 || effect->mSkill > 26) - messages.add(id, "Effect #" + number + " affected skill is invalid", "", CSMDoc::Message::Severity_Error); - if (effect->mAttribute < -1 || effect->mAttribute > 7) - messages.add(id, "Effect #" + number + " affected attribute is invalid", "", CSMDoc::Message::Severity_Error); - if (effect->mRange < 0 || effect->mRange > 2) - messages.add(id, "Effect #" + number + " range is invalid", "", CSMDoc::Message::Severity_Error); - if (effect->mArea < 0) - messages.add(id, "Effect #" + number + " area is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mDuration < 0) - messages.add(id, "Effect #" + number + " duration is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mMagnMin < 0) - messages.add(id, "Effect #" + number + " minimum magnitude is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mMagnMax < 0) - messages.add(id, "Effect #" + number + " maximum magnitude is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mMagnMin > effect->mMagnMax) - messages.add(id, "Effect #" + number + " minimum magnitude is higher than maximum magnitude", "", CSMDoc::Message::Severity_Error); - ++effect; - } - } + effectListCheck(enchantment.mEffects.mList, messages, id); } diff --git a/apps/opencs/model/tools/enchantmentcheck.hpp b/apps/opencs/model/tools/enchantmentcheck.hpp index e9c8b9eece9..3fab99c2a56 100644 --- a/apps/opencs/model/tools/enchantmentcheck.hpp +++ b/apps/opencs/model/tools/enchantmentcheck.hpp @@ -1,30 +1,36 @@ #ifndef CSM_TOOLS_ENCHANTMENTCHECK_H #define CSM_TOOLS_ENCHANTMENTCHECK_H -#include - #include "../world/idcollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace ESM +{ + struct Enchantment; +} + namespace CSMTools { /// \brief Make sure that enchantment records are correct class EnchantmentCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection& mEnchantments; - bool mIgnoreBaseRecords; - - public: - - EnchantmentCheckStage (const CSMWorld::IdCollection& enchantments); + const CSMWorld::IdCollection& mEnchantments; + bool mIgnoreBaseRecords; - int setup() override; - ///< \return number of steps + public: + EnchantmentCheckStage(const CSMWorld::IdCollection& enchantments); - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + int setup() override; + ///< \return number of steps + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/factioncheck.cpp b/apps/opencs/model/tools/factioncheck.cpp index 8a198e9535a..69b790e6593 100644 --- a/apps/opencs/model/tools/factioncheck.cpp +++ b/apps/opencs/model/tools/factioncheck.cpp @@ -1,15 +1,23 @@ #include "factioncheck.hpp" #include +#include +#include -#include +#include +#include +#include +#include +#include +#include -#include "../prefs/state.hpp" +#include +#include -#include "../world/universalid.hpp" +#include "../prefs/state.hpp" -CSMTools::FactionCheckStage::FactionCheckStage (const CSMWorld::IdCollection& factions) -: mFactions (factions) +CSMTools::FactionCheckStage::FactionCheckStage(const CSMWorld::IdCollection& factions) + : mFactions(factions) { mIgnoreBaseRecords = false; } @@ -21,9 +29,9 @@ int CSMTools::FactionCheckStage::setup() return mFactions.getSize(); } -void CSMTools::FactionCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::FactionCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mFactions.getRecord (stage); + const CSMWorld::Record& record = mFactions.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) @@ -31,29 +39,43 @@ void CSMTools::FactionCheckStage::perform (int stage, CSMDoc::Messages& messages const ESM::Faction& faction = record.get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Faction, faction.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Faction, faction.mId); // test for empty name if (faction.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); // test for invalid attributes - if (faction.mData.mAttribute[0]==faction.mData.mAttribute[1] && faction.mData.mAttribute[0]!=-1) + std::map attributeCount; + for (size_t i = 0; i < faction.mData.mAttribute.size(); ++i) { - messages.add(id, "Same attribute is listed twice", "", CSMDoc::Message::Severity_Error); + int attribute = faction.mData.mAttribute[i]; + if (attribute != -1) + { + auto it = attributeCount.find(attribute); + if (it == attributeCount.end()) + attributeCount.emplace(attribute, 1); + else + { + if (it->second == 1) + messages.add(id, "Same attribute is listed twice", {}, CSMDoc::Message::Severity_Error); + ++it->second; + } + } } // test for non-unique skill std::map skills; // ID, number of occurrences - for (int i=0; i<7; ++i) - if (faction.mData.mSkills[i]!=-1) - ++skills[faction.mData.mSkills[i]]; + for (int skill : faction.mData.mSkills) + if (skill != -1) + ++skills[skill]; - for (auto &skill : skills) - if (skill.second>1) + for (auto& skill : skills) + if (skill.second > 1) { - messages.add(id, "Skill " + ESM::Skill::indexToId (skill.first) + " is listed more than once", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Skill " + ESM::Skill::indexToRefId(skill.first).toString() + " is listed more than once", + "", CSMDoc::Message::Severity_Error); } /// \todo check data members that can't be edited in the table view diff --git a/apps/opencs/model/tools/factioncheck.hpp b/apps/opencs/model/tools/factioncheck.hpp index d281c1b416e..4339b3a6b15 100644 --- a/apps/opencs/model/tools/factioncheck.hpp +++ b/apps/opencs/model/tools/factioncheck.hpp @@ -1,29 +1,36 @@ #ifndef CSM_TOOLS_FACTIONCHECK_H #define CSM_TOOLS_FACTIONCHECK_H -#include - #include "../world/idcollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace ESM +{ + struct Faction; +} + namespace CSMTools { /// \brief VerifyStage: make sure that faction records are internally consistent class FactionCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection& mFactions; - bool mIgnoreBaseRecords; - - public: + const CSMWorld::IdCollection& mFactions; + bool mIgnoreBaseRecords; - FactionCheckStage (const CSMWorld::IdCollection& factions); + public: + FactionCheckStage(const CSMWorld::IdCollection& factions); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/gmstcheck.cpp b/apps/opencs/model/tools/gmstcheck.cpp index 7cd13e5c234..858bc2c3eca 100644 --- a/apps/opencs/model/tools/gmstcheck.cpp +++ b/apps/opencs/model/tools/gmstcheck.cpp @@ -1,6 +1,16 @@ #include "gmstcheck.hpp" #include +#include + +#include +#include +#include +#include +#include +#include + +#include #include "../prefs/state.hpp" @@ -21,93 +31,93 @@ int CSMTools::GmstCheckStage::setup() void CSMTools::GmstCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mGameSettings.getRecord (stage); - + const CSMWorld::Record& record = mGameSettings.getRecord(stage); + // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; - + const ESM::GameSetting& gmst = record.get(); - - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Gmst, gmst.mId); - + + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Gmst, gmst.mId); + const std::string& gmstIdString = gmst.mId.getRefIdString(); // Test for empty string if (gmst.mValue.getType() == ESM::VT_String && gmst.mValue.getString().empty()) - messages.add(id, gmst.mId + " is an empty string", "", CSMDoc::Message::Severity_Warning); - + messages.add(id, gmstIdString + " is an empty string", "", CSMDoc::Message::Severity_Warning); + // Checking type and limits // optimization - compare it to lists based on naming convention (f-float,i-int,s-string) - if (gmst.mId[0] == 'f') + if (gmst.mId.startsWith("f")) { for (size_t i = 0; i < CSMWorld::DefaultGmsts::FloatCount; ++i) { - if (gmst.mId == CSMWorld::DefaultGmsts::Floats[i]) + if (gmst.mId == ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::Floats[i])) { if (gmst.mValue.getType() != ESM::VT_Float) { std::ostringstream stream; stream << "Expected float type for " << gmst.mId << " but found " - << varTypeToString(gmst.mValue.getType()) << " type"; - + << varTypeToString(gmst.mValue.getType()) << " type"; + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); } - - if (gmst.mValue.getFloat() < CSMWorld::DefaultGmsts::FloatLimits[i*2]) - messages.add(id, gmst.mId + " is less than the suggested range", "", - CSMDoc::Message::Severity_Warning); - - if (gmst.mValue.getFloat() > CSMWorld::DefaultGmsts::FloatLimits[i*2+1]) - messages.add(id, gmst.mId + " is more than the suggested range", "", - CSMDoc::Message::Severity_Warning); - + + if (gmst.mValue.getFloat() < CSMWorld::DefaultGmsts::FloatLimits[i * 2]) + messages.add( + id, gmstIdString + " is less than the suggested range", "", CSMDoc::Message::Severity_Warning); + + if (gmst.mValue.getFloat() > CSMWorld::DefaultGmsts::FloatLimits[i * 2 + 1]) + messages.add( + id, gmstIdString + " is more than the suggested range", "", CSMDoc::Message::Severity_Warning); + break; // for loop } } } - else if (gmst.mId[0] == 'i') + else if (gmst.mId.startsWith("i")) { for (size_t i = 0; i < CSMWorld::DefaultGmsts::IntCount; ++i) - { - if (gmst.mId == CSMWorld::DefaultGmsts::Ints[i]) + { + if (gmst.mId == ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::Ints[i])) { if (gmst.mValue.getType() != ESM::VT_Int) { std::ostringstream stream; stream << "Expected int type for " << gmst.mId << " but found " - << varTypeToString(gmst.mValue.getType()) << " type"; - + << varTypeToString(gmst.mValue.getType()) << " type"; + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); } - - if (gmst.mValue.getInteger() < CSMWorld::DefaultGmsts::IntLimits[i*2]) - messages.add(id, gmst.mId + " is less than the suggested range", "", - CSMDoc::Message::Severity_Warning); - - if (gmst.mValue.getInteger() > CSMWorld::DefaultGmsts::IntLimits[i*2+1]) - messages.add(id, gmst.mId + " is more than the suggested range", "", - CSMDoc::Message::Severity_Warning); - + + if (gmst.mValue.getInteger() < CSMWorld::DefaultGmsts::IntLimits[i * 2]) + messages.add( + id, gmstIdString + " is less than the suggested range", "", CSMDoc::Message::Severity_Warning); + + if (gmst.mValue.getInteger() > CSMWorld::DefaultGmsts::IntLimits[i * 2 + 1]) + messages.add( + id, gmstIdString + " is more than the suggested range", "", CSMDoc::Message::Severity_Warning); + break; // for loop } } } - else if (gmst.mId[0] == 's') + else if (gmst.mId.startsWith("s")) { for (size_t i = 0; i < CSMWorld::DefaultGmsts::StringCount; ++i) { - if (gmst.mId == CSMWorld::DefaultGmsts::Strings[i]) + if (gmst.mId == ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::Strings[i])) { ESM::VarType type = gmst.mValue.getType(); - + if (type != ESM::VT_String && type != ESM::VT_None) { std::ostringstream stream; - stream << "Expected string or none type for " << gmst.mId << " but found " - << varTypeToString(gmst.mValue.getType()) << " type"; - + stream << "Expected string or none type for " << gmstIdString << " but found " + << varTypeToString(gmst.mValue.getType()) << " type"; + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); } - + break; // for loop } } @@ -118,13 +128,21 @@ std::string CSMTools::GmstCheckStage::varTypeToString(ESM::VarType type) { switch (type) { - case ESM::VT_Unknown: return "unknown"; - case ESM::VT_None: return "none"; - case ESM::VT_Short: return "short"; - case ESM::VT_Int: return "int"; - case ESM::VT_Long: return "long"; - case ESM::VT_Float: return "float"; - case ESM::VT_String: return "string"; - default: return "unhandled"; + case ESM::VT_Unknown: + return "unknown"; + case ESM::VT_None: + return "none"; + case ESM::VT_Short: + return "short"; + case ESM::VT_Int: + return "int"; + case ESM::VT_Long: + return "long"; + case ESM::VT_Float: + return "float"; + case ESM::VT_String: + return "string"; + default: + return "unhandled"; } } diff --git a/apps/opencs/model/tools/gmstcheck.hpp b/apps/opencs/model/tools/gmstcheck.hpp index 2c12a8607a3..c4c258c8f38 100644 --- a/apps/opencs/model/tools/gmstcheck.hpp +++ b/apps/opencs/model/tools/gmstcheck.hpp @@ -1,34 +1,43 @@ #ifndef CSM_TOOLS_GMSTCHECK_H #define CSM_TOOLS_GMSTCHECK_H -#include +#include + +#include #include "../world/idcollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace ESM +{ + struct GameSetting; +} + namespace CSMTools { /// \brief VerifyStage: make sure that GMSTs are alright class GmstCheckStage : public CSMDoc::Stage { public: - GmstCheckStage(const CSMWorld::IdCollection& gameSettings); - + int setup() override; ///< \return number of steps void perform(int stage, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages - + private: - const CSMWorld::IdCollection& mGameSettings; bool mIgnoreBaseRecords; - + std::string varTypeToString(ESM::VarType); - }; } diff --git a/apps/opencs/model/tools/journalcheck.cpp b/apps/opencs/model/tools/journalcheck.cpp index ae83abfa01a..1f34073a4a3 100644 --- a/apps/opencs/model/tools/journalcheck.cpp +++ b/apps/opencs/model/tools/journalcheck.cpp @@ -1,12 +1,30 @@ #include "journalcheck.hpp" +#include #include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include #include "../prefs/state.hpp" -CSMTools::JournalCheckStage::JournalCheckStage(const CSMWorld::IdCollection &journals, - const CSMWorld::InfoCollection& journalInfos) - : mJournals(journals), mJournalInfos(journalInfos) +CSMTools::JournalCheckStage::JournalCheckStage( + const CSMWorld::IdCollection& journals, const CSMWorld::InfoCollection& journalInfos) + : mJournals(journals) + , mJournalInfos(journalInfos) { mIgnoreBaseRecords = false; } @@ -14,58 +32,57 @@ CSMTools::JournalCheckStage::JournalCheckStage(const CSMWorld::IdCollection &journalRecord = mJournals.getRecord(stage); + const CSMWorld::Record& journalRecord = mJournals.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && journalRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || journalRecord.isDeleted()) + if ((mIgnoreBaseRecords && journalRecord.mState == CSMWorld::RecordBase::State_BaseOnly) + || journalRecord.isDeleted()) return; - const ESM::Dialogue &journal = journalRecord.get(); + const ESM::Dialogue& journal = journalRecord.get(); int statusNamedCount = 0; int totalInfoCount = 0; std::set questIndices; - CSMWorld::InfoCollection::Range range = mJournalInfos.getTopicRange(journal.mId); - - for (CSMWorld::InfoCollection::RecordConstIterator it = range.first; it != range.second; ++it) + if (const auto infos = mInfosByTopic.find(journal.mId); infos != mInfosByTopic.end()) { - const CSMWorld::Record infoRecord = (*it); - - if (infoRecord.isDeleted()) - continue; - - const CSMWorld::Info& journalInfo = infoRecord.get(); - - totalInfoCount += 1; - - if (journalInfo.mQuestStatus == ESM::DialInfo::QS_Name) - { - statusNamedCount += 1; - } - - // Skip "Base" records (setting!) - if (mIgnoreBaseRecords && infoRecord.mState == CSMWorld::RecordBase::State_BaseOnly) - continue; - - if (journalInfo.mResponse.empty()) - { - CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_JournalInfo, journalInfo.mId); - messages.add(id, "Missing journal entry text", "", CSMDoc::Message::Severity_Warning); - } - - std::pair::iterator, bool> result = questIndices.insert(journalInfo.mData.mJournalIndex); - - // Duplicate index - if (!result.second) + for (const CSMWorld::Record* record : infos->second) { - CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_JournalInfo, journalInfo.mId); - messages.add(id, "Duplicated quest index " + std::to_string(journalInfo.mData.mJournalIndex), "", CSMDoc::Message::Severity_Error); + if (record->isDeleted()) + continue; + + const CSMWorld::Info& journalInfo = record->get(); + + totalInfoCount += 1; + + if (journalInfo.mQuestStatus == ESM::DialInfo::QS_Name) + { + statusNamedCount += 1; + } + + // Skip "Base" records (setting!) + if (mIgnoreBaseRecords && record->mState == CSMWorld::RecordBase::State_BaseOnly) + continue; + + if (journalInfo.mResponse.empty()) + { + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_JournalInfo, journalInfo.mId); + messages.add(id, "Missing journal entry text", "", CSMDoc::Message::Severity_Warning); + } + + // Duplicate index + if (!questIndices.insert(journalInfo.mData.mJournalIndex).second) + { + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_JournalInfo, journalInfo.mId); + messages.add(id, "Duplicated quest index " + std::to_string(journalInfo.mData.mJournalIndex), "", + CSMDoc::Message::Severity_Error); + } } } diff --git a/apps/opencs/model/tools/journalcheck.hpp b/apps/opencs/model/tools/journalcheck.hpp index b63127b5225..a3bee1492a1 100644 --- a/apps/opencs/model/tools/journalcheck.hpp +++ b/apps/opencs/model/tools/journalcheck.hpp @@ -1,22 +1,34 @@ #ifndef CSM_TOOLS_JOURNALCHECK_H #define CSM_TOOLS_JOURNALCHECK_H -#include - #include "../world/idcollection.hpp" #include "../world/infocollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace CSMWorld +{ + class InfoCollection; +} + +namespace ESM +{ + struct Dialogue; +} + namespace CSMTools { /// \brief VerifyStage: make sure that journal infos are good class JournalCheckStage : public CSMDoc::Stage { public: - - JournalCheckStage(const CSMWorld::IdCollection& journals, - const CSMWorld::InfoCollection& journalInfos); + JournalCheckStage( + const CSMWorld::IdCollection& journals, const CSMWorld::InfoCollection& journalInfos); int setup() override; ///< \return number of steps @@ -25,11 +37,10 @@ namespace CSMTools ///< Messages resulting from this stage will be appended to \a messages private: - const CSMWorld::IdCollection& mJournals; const CSMWorld::InfoCollection& mJournalInfos; bool mIgnoreBaseRecords; - + CSMWorld::InfosRecordPtrByTopic mInfosByTopic; }; } diff --git a/apps/opencs/model/tools/magiceffectcheck.cpp b/apps/opencs/model/tools/magiceffectcheck.cpp index f55fb14eea0..212b343e00f 100644 --- a/apps/opencs/model/tools/magiceffectcheck.cpp +++ b/apps/opencs/model/tools/magiceffectcheck.cpp @@ -1,31 +1,43 @@ #include "magiceffectcheck.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +#include #include #include "../prefs/state.hpp" -std::string CSMTools::MagicEffectCheckStage::checkObject(const std::string &id, - const CSMWorld::UniversalId &type, - const std::string &column) const +namespace ESM +{ + struct Sound; +} + +std::string CSMTools::MagicEffectCheckStage::checkObject( + const ESM::RefId& id, const CSMWorld::UniversalId& type, const std::string& column) const { CSMWorld::RefIdData::LocalIndex index = mObjects.getDataSet().searchId(id); - if (index.first == -1) - return (column + " '" + id + "' does not exist"); - else if (index.second != type.getType()) - return (column + " '" + id + "' does not have " + type.getTypeName() + " type"); + if (index.first == -1) + return (column + " '" + id.getRefIdString() + "' does not exist"); + else if (index.second != type.getType()) + return (column + " '" + id.getRefIdString() + "' does not have " + type.getTypeName() + " type"); return std::string(); } -CSMTools::MagicEffectCheckStage::MagicEffectCheckStage(const CSMWorld::IdCollection &effects, - const CSMWorld::IdCollection &sounds, - const CSMWorld::RefIdCollection &objects, - const CSMWorld::Resources &icons, - const CSMWorld::Resources &textures) - : mMagicEffects(effects), - mSounds(sounds), - mObjects(objects), - mIcons(icons), - mTextures(textures) +CSMTools::MagicEffectCheckStage::MagicEffectCheckStage(const CSMWorld::IdCollection& effects, + const CSMWorld::IdCollection& sounds, const CSMWorld::RefIdCollection& objects, + const CSMWorld::Resources& icons, const CSMWorld::Resources& textures) + : mMagicEffects(effects) + , mSounds(sounds) + , mObjects(objects) + , mIcons(icons) + , mTextures(textures) { mIgnoreBaseRecords = false; } @@ -37,16 +49,21 @@ int CSMTools::MagicEffectCheckStage::setup() return mMagicEffects.getSize(); } -void CSMTools::MagicEffectCheckStage::perform(int stage, CSMDoc::Messages &messages) +void CSMTools::MagicEffectCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record &record = mMagicEffects.getRecord(stage); + const CSMWorld::Record& record = mMagicEffects.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; ESM::MagicEffect effect = record.get(); - CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_MagicEffect, effect.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_MagicEffect, CSMWorld::getRecordId(effect)); + + if (effect.mData.mSpeed <= 0.0f) + { + messages.add(id, "Speed is less than or equal to zero", "", CSMDoc::Message::Severity_Error); + } if (effect.mDescription.empty()) { @@ -78,7 +95,8 @@ void CSMTools::MagicEffectCheckStage::perform(int stage, CSMDoc::Messages &messa { std::string ddsParticle = effect.mParticle; if (!(Misc::ResourceHelpers::changeExtensionToDds(ddsParticle) && mTextures.searchId(ddsParticle) != -1)) - messages.add(id, "Particle texture '" + effect.mParticle + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Particle texture '" + effect.mParticle + "' does not exist", "", + CSMDoc::Message::Severity_Error); } } @@ -111,11 +129,15 @@ void CSMTools::MagicEffectCheckStage::perform(int stage, CSMDoc::Messages &messa } if (!effect.mCastSound.empty() && mSounds.searchId(effect.mCastSound) == -1) - messages.add(id, "Casting sound '" + effect.mCastSound + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Casting sound '" + effect.mCastSound.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); if (!effect.mHitSound.empty() && mSounds.searchId(effect.mHitSound) == -1) - messages.add(id, "Hit sound '" + effect.mHitSound + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Hit sound '" + effect.mHitSound.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); if (!effect.mAreaSound.empty() && mSounds.searchId(effect.mAreaSound) == -1) - messages.add(id, "Area sound '" + effect.mAreaSound + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Area sound '" + effect.mAreaSound.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); if (!effect.mBoltSound.empty() && mSounds.searchId(effect.mBoltSound) == -1) - messages.add(id, "Bolt sound '" + effect.mBoltSound + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Bolt sound '" + effect.mBoltSound.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); } diff --git a/apps/opencs/model/tools/magiceffectcheck.hpp b/apps/opencs/model/tools/magiceffectcheck.hpp index 4b2c24cc7c5..3ebb2621f84 100644 --- a/apps/opencs/model/tools/magiceffectcheck.hpp +++ b/apps/opencs/model/tools/magiceffectcheck.hpp @@ -1,41 +1,58 @@ #ifndef CSM_TOOLS_MAGICEFFECTCHECK_HPP #define CSM_TOOLS_MAGICEFFECTCHECK_HPP -#include -#include +#include + +#include + +#include #include "../world/idcollection.hpp" -#include "../world/refidcollection.hpp" -#include "../world/resources.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace CSMWorld +{ + class RefIdCollection; + class Resources; +} + +namespace ESM +{ + struct MagicEffect; + struct Sound; +} + namespace CSMTools { /// \brief VerifyStage: make sure that magic effect records are internally consistent class MagicEffectCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection &mMagicEffects; - const CSMWorld::IdCollection &mSounds; - const CSMWorld::RefIdCollection &mObjects; - const CSMWorld::Resources &mIcons; - const CSMWorld::Resources &mTextures; - bool mIgnoreBaseRecords; - - private: - std::string checkObject(const std::string &id, const CSMWorld::UniversalId &type, const std::string &column) const; - - public: - MagicEffectCheckStage(const CSMWorld::IdCollection &effects, - const CSMWorld::IdCollection &sounds, - const CSMWorld::RefIdCollection &objects, - const CSMWorld::Resources &icons, - const CSMWorld::Resources &textures); - - int setup() override; - ///< \return number of steps - void perform (int stage, CSMDoc::Messages &messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + const CSMWorld::IdCollection& mMagicEffects; + const CSMWorld::IdCollection& mSounds; + const CSMWorld::RefIdCollection& mObjects; + const CSMWorld::Resources& mIcons; + const CSMWorld::Resources& mTextures; + bool mIgnoreBaseRecords; + + private: + std::string checkObject( + const ESM::RefId& id, const CSMWorld::UniversalId& type, const std::string& column) const; + + public: + MagicEffectCheckStage(const CSMWorld::IdCollection& effects, + const CSMWorld::IdCollection& sounds, const CSMWorld::RefIdCollection& objects, + const CSMWorld::Resources& icons, const CSMWorld::Resources& textures); + + int setup() override; + ///< \return number of steps + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/mandatoryid.cpp b/apps/opencs/model/tools/mandatoryid.cpp index d0d9cc0b9dd..b6515539e22 100644 --- a/apps/opencs/model/tools/mandatoryid.cpp +++ b/apps/opencs/model/tools/mandatoryid.cpp @@ -1,22 +1,29 @@ #include "mandatoryid.hpp" +#include + #include "../world/collectionbase.hpp" #include "../world/record.hpp" -CSMTools::MandatoryIdStage::MandatoryIdStage (const CSMWorld::CollectionBase& idCollection, - const CSMWorld::UniversalId& collectionId, const std::vector& ids) -: mIdCollection (idCollection), mCollectionId (collectionId), mIds (ids) -{} +#include +#include + +CSMTools::MandatoryIdStage::MandatoryIdStage(const CSMWorld::CollectionBase& idCollection, + const CSMWorld::UniversalId& collectionId, const std::vector& ids) + : mIdCollection(idCollection) + , mCollectionId(collectionId) + , mIds(ids) +{ +} int CSMTools::MandatoryIdStage::setup() { return static_cast(mIds.size()); } -void CSMTools::MandatoryIdStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::MandatoryIdStage::perform(int stage, CSMDoc::Messages& messages) { - if (mIdCollection.searchId (mIds.at (stage))==-1 || - mIdCollection.getRecord (mIds.at (stage)).isDeleted()) - messages.add (mCollectionId, "Missing mandatory record: " + mIds.at (stage)); + if (mIdCollection.searchId(mIds.at(stage)) == -1 || mIdCollection.getRecord(mIds.at(stage)).isDeleted()) + messages.add(mCollectionId, "Missing mandatory record: " + mIds.at(stage).toDebugString()); } diff --git a/apps/opencs/model/tools/mandatoryid.hpp b/apps/opencs/model/tools/mandatoryid.hpp index 9d069a2da8c..24e295539bd 100644 --- a/apps/opencs/model/tools/mandatoryid.hpp +++ b/apps/opencs/model/tools/mandatoryid.hpp @@ -4,9 +4,14 @@ #include #include +#include "../doc/stage.hpp" #include "../world/universalid.hpp" +#include -#include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} namespace CSMWorld { @@ -18,20 +23,19 @@ namespace CSMTools /// \brief Verify stage: make sure that records with specific IDs exist. class MandatoryIdStage : public CSMDoc::Stage { - const CSMWorld::CollectionBase& mIdCollection; - CSMWorld::UniversalId mCollectionId; - std::vector mIds; - - public: + const CSMWorld::CollectionBase& mIdCollection; + CSMWorld::UniversalId mCollectionId; + std::vector mIds; - MandatoryIdStage (const CSMWorld::CollectionBase& idCollection, const CSMWorld::UniversalId& collectionId, - const std::vector& ids); + public: + MandatoryIdStage(const CSMWorld::CollectionBase& idCollection, const CSMWorld::UniversalId& collectionId, + const std::vector& ids); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/mergeoperation.cpp b/apps/opencs/model/tools/mergeoperation.cpp index b15b4b83f1f..2c7a8bbc4e3 100644 --- a/apps/opencs/model/tools/mergeoperation.cpp +++ b/apps/opencs/model/tools/mergeoperation.cpp @@ -1,52 +1,88 @@ - #include "mergeoperation.hpp" -#include "../doc/state.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "../doc/document.hpp" +#include "../doc/state.hpp" #include "mergestages.hpp" -CSMTools::MergeOperation::MergeOperation (CSMDoc::Document& document, ToUTF8::FromType encoding) -: CSMDoc::Operation (CSMDoc::State_Merging, true), mState (document) +CSMTools::MergeOperation::MergeOperation(CSMDoc::Document& document, ToUTF8::FromType encoding) + : CSMDoc::Operation(CSMDoc::State_Merging, true) + , mState(document) { - appendStage (new StartMergeStage (mState)); - - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getGlobals)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getGmsts)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getSkills)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getClasses)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getFactions)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getRaces)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getSounds)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getScripts)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getRegions)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getBirthsigns)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getSpells)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getTopics)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getJournals)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getCells)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getFilters)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getEnchantments)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getBodyParts)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getDebugProfiles)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getSoundGens)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getMagicEffects)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getStartScripts)); - appendStage (new MergeIdCollectionStage > (mState, &CSMWorld::Data::getPathgrids)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getTopicInfos)); - appendStage (new MergeIdCollectionStage (mState, &CSMWorld::Data::getJournalInfos)); - appendStage (new MergeRefIdsStage (mState)); - appendStage (new MergeReferencesStage (mState)); - appendStage (new MergeReferencesStage (mState)); - appendStage (new PopulateLandTexturesMergeStage (mState)); - appendStage (new MergeLandStage (mState)); - appendStage (new FixLandsAndLandTexturesMergeStage (mState)); - appendStage (new CleanupLandTexturesMergeStage (mState)); - - appendStage (new FinishMergedDocumentStage (mState, encoding)); + appendStage(new StartMergeStage(mState)); + + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getGlobals)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getGmsts)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getSkills)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getClasses)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getFactions)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getRaces)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getSounds)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getScripts)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getRegions)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getBirthsigns)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getSpells)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getTopics)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getJournals)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getCells)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getFilters)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getEnchantments)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getBodyParts)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getDebugProfiles)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getSoundGens)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getMagicEffects)); + appendStage(new MergeIdCollectionStage(mState, &CSMWorld::Data::getStartScripts)); + appendStage(new MergeIdCollectionStage>( + mState, &CSMWorld::Data::getPathgrids)); + appendStage( + new MergeIdCollectionStage(mState, &CSMWorld::Data::getTopicInfos)); + appendStage( + new MergeIdCollectionStage(mState, &CSMWorld::Data::getJournalInfos)); + appendStage(new MergeRefIdsStage(mState)); + appendStage(new MergeReferencesStage(mState)); + appendStage(new MergeReferencesStage(mState)); + appendStage(new PopulateLandTexturesMergeStage(mState)); + appendStage(new MergeLandStage(mState)); + appendStage(new FixLandsAndLandTexturesMergeStage(mState)); + appendStage(new CleanupLandTexturesMergeStage(mState)); + + appendStage(new FinishMergedDocumentStage(mState, encoding)); } -void CSMTools::MergeOperation::setTarget (std::unique_ptr document) +void CSMTools::MergeOperation::setTarget(std::unique_ptr document) { mState.mTarget = std::move(document); } @@ -56,5 +92,5 @@ void CSMTools::MergeOperation::operationDone() CSMDoc::Operation::operationDone(); if (mState.mCompleted) - emit mergeDone (mState.mTarget.release()); + emit mergeDone(mState.mTarget.release()); } diff --git a/apps/opencs/model/tools/mergeoperation.hpp b/apps/opencs/model/tools/mergeoperation.hpp index 427967190c3..2cce2bec0dd 100644 --- a/apps/opencs/model/tools/mergeoperation.hpp +++ b/apps/opencs/model/tools/mergeoperation.hpp @@ -9,6 +9,8 @@ #include "mergestate.hpp" +class QObject; + namespace CSMDoc { class Document; @@ -18,27 +20,25 @@ namespace CSMTools { class MergeOperation : public CSMDoc::Operation { - Q_OBJECT - - MergeState mState; - - public: + Q_OBJECT - MergeOperation (CSMDoc::Document& document, ToUTF8::FromType encoding); + MergeState mState; - /// \attention Do not call this function while a merge is running. - void setTarget (std::unique_ptr document); + public: + MergeOperation(CSMDoc::Document& document, ToUTF8::FromType encoding); - protected slots: + /// \attention Do not call this function while a merge is running. + void setTarget(std::unique_ptr document); - void operationDone() override; + protected slots: - signals: + void operationDone() override; - /// \attention When this signal is emitted, *this hands over the ownership of the - /// document. This signal must be handled to avoid a leak. - void mergeDone (CSMDoc::Document *document); + signals: + /// \attention When this signal is emitted, *this hands over the ownership of the + /// document. This signal must be handled to avoid a leak. + void mergeDone(CSMDoc::Document* document); }; } diff --git a/apps/opencs/model/tools/mergestages.cpp b/apps/opencs/model/tools/mergestages.cpp index 016e2da3921..88cad267cb0 100644 --- a/apps/opencs/model/tools/mergestages.cpp +++ b/apps/opencs/model/tools/mergestages.cpp @@ -1,9 +1,19 @@ - #include "mergestages.hpp" -#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include -#include +#include +#include #include "mergestate.hpp" @@ -12,75 +22,82 @@ #include "../world/data.hpp" #include "../world/idtable.hpp" +namespace CSMDoc +{ + class Messages; +} -CSMTools::StartMergeStage::StartMergeStage (MergeState& state) -: mState (state) -{} +CSMTools::StartMergeStage::StartMergeStage(MergeState& state) + : mState(state) +{ +} int CSMTools::StartMergeStage::setup() { return 1; } -void CSMTools::StartMergeStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::StartMergeStage::perform(int stage, CSMDoc::Messages& messages) { mState.mCompleted = false; mState.mTextureIndices.clear(); } - -CSMTools::FinishMergedDocumentStage::FinishMergedDocumentStage (MergeState& state, ToUTF8::FromType encoding) -: mState (state), mEncoder (encoding) -{} +CSMTools::FinishMergedDocumentStage::FinishMergedDocumentStage(MergeState& state, ToUTF8::FromType encoding) + : mState(state) + , mEncoder(encoding) +{ +} int CSMTools::FinishMergedDocumentStage::setup() { return 1; } -void CSMTools::FinishMergedDocumentStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::FinishMergedDocumentStage::perform(int stage, CSMDoc::Messages& messages) { // We know that the content file list contains at least two entries and that the first one // does exist on disc (otherwise it would have been impossible to initiate a merge on that // document). - boost::filesystem::path path = mState.mSource.getContentFiles()[0]; + std::filesystem::path path = mState.mSource.getContentFiles()[0]; ESM::ESMReader reader; - reader.setEncoder (&mEncoder); - reader.open (path.string()); + reader.setEncoder(&mEncoder); + reader.open(path); CSMWorld::MetaData source; - source.mId = "sys::meta"; - source.load (reader); + source.mId = ESM::RefId::stringRefId("sys::meta"); + source.load(reader); CSMWorld::MetaData target = mState.mTarget->getData().getMetaData(); target.mAuthor = source.mAuthor; target.mDescription = source.mDescription; - mState.mTarget->getData().setMetaData (target); + mState.mTarget->getData().setMetaData(target); mState.mCompleted = true; } - -CSMTools::MergeRefIdsStage::MergeRefIdsStage (MergeState& state) : mState (state) {} +CSMTools::MergeRefIdsStage::MergeRefIdsStage(MergeState& state) + : mState(state) +{ +} int CSMTools::MergeRefIdsStage::setup() { return mState.mSource.getData().getReferenceables().getSize(); } -void CSMTools::MergeRefIdsStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::MergeRefIdsStage::perform(int stage, CSMDoc::Messages& messages) { - mState.mSource.getData().getReferenceables().copyTo ( - stage, mState.mTarget->getData().getReferenceables()); + mState.mSource.getData().getReferenceables().copyTo(stage, mState.mTarget->getData().getReferenceables()); } - -CSMTools::MergeReferencesStage::MergeReferencesStage (MergeState& state) -: mState (state) -{} +CSMTools::MergeReferencesStage::MergeReferencesStage(MergeState& state) + : mState(state) +{ +} int CSMTools::MergeReferencesStage::setup() { @@ -88,10 +105,9 @@ int CSMTools::MergeReferencesStage::setup() return mState.mSource.getData().getReferences().getSize(); } -void CSMTools::MergeReferencesStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::MergeReferencesStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = - mState.mSource.getData().getReferences().getRecord (stage); + const CSMWorld::Record& record = mState.mSource.getData().getReferences().getRecord(stage); if (!record.isDeleted()) { @@ -99,20 +115,17 @@ void CSMTools::MergeReferencesStage::perform (int stage, CSMDoc::Messages& messa ref.mOriginalCell = ref.mCell; - ref.mRefNum.mIndex = mIndex[Misc::StringUtils::lowerCase (ref.mCell)]++; - ref.mRefNum.mContentFile = 0; + ref.mRefNum.mIndex = mIndex[ref.mCell]++; + ref.mRefNum.mContentFile = -1; ref.mNew = false; - CSMWorld::Record newRecord ( - CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &ref); - - mState.mTarget->getData().getReferences().appendRecord (newRecord); + mState.mTarget->getData().getReferences().appendRecord(std::make_unique>( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &ref))); } } - -CSMTools::PopulateLandTexturesMergeStage::PopulateLandTexturesMergeStage (MergeState& state) - : mState (state) +CSMTools::PopulateLandTexturesMergeStage::PopulateLandTexturesMergeStage(MergeState& state) + : mState(state) { } @@ -121,20 +134,19 @@ int CSMTools::PopulateLandTexturesMergeStage::setup() return mState.mSource.getData().getLandTextures().getSize(); } -void CSMTools::PopulateLandTexturesMergeStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::PopulateLandTexturesMergeStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = - mState.mSource.getData().getLandTextures().getRecord (stage); + const CSMWorld::Record& record = mState.mSource.getData().getLandTextures().getRecord(stage); if (!record.isDeleted()) { - mState.mTarget->getData().getLandTextures().appendRecord(record); + mState.mTarget->getData().getLandTextures().appendRecord(std::make_unique>( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } } - -CSMTools::MergeLandStage::MergeLandStage (MergeState& state) - : mState (state) +CSMTools::MergeLandStage::MergeLandStage(MergeState& state) + : mState(state) { } @@ -143,20 +155,19 @@ int CSMTools::MergeLandStage::setup() return mState.mSource.getData().getLand().getSize(); } -void CSMTools::MergeLandStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::MergeLandStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = - mState.mSource.getData().getLand().getRecord (stage); + const CSMWorld::Record& record = mState.mSource.getData().getLand().getRecord(stage); if (!record.isDeleted()) { - mState.mTarget->getData().getLand().appendRecord (record); + mState.mTarget->getData().getLand().appendRecord(std::make_unique>( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } } - -CSMTools::FixLandsAndLandTexturesMergeStage::FixLandsAndLandTexturesMergeStage (MergeState& state) - : mState (state) +CSMTools::FixLandsAndLandTexturesMergeStage::FixLandsAndLandTexturesMergeStage(MergeState& state) + : mState(state) { } @@ -166,7 +177,7 @@ int CSMTools::FixLandsAndLandTexturesMergeStage::setup() return mState.mSource.getData().getLand().getSize(); } -void CSMTools::FixLandsAndLandTexturesMergeStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::FixLandsAndLandTexturesMergeStage::perform(int stage, CSMDoc::Messages& messages) { if (stage < mState.mTarget->getData().getLand().getSize()) { @@ -176,24 +187,22 @@ void CSMTools::FixLandsAndLandTexturesMergeStage::perform (int stage, CSMDoc::Me CSMWorld::IdTable& ltexTable = dynamic_cast( *mState.mTarget->getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); - std::string id = mState.mTarget->getData().getLand().getId(stage); + const auto& id = mState.mTarget->getData().getLand().getId(stage); - CSMWorld::TouchLandCommand cmd(landTable, ltexTable, id); + CSMWorld::TouchLandCommand cmd(landTable, ltexTable, id.getRefIdString()); cmd.redo(); // Get rid of base data - const CSMWorld::Record& oldRecord = - mState.mTarget->getData().getLand().getRecord (stage); - - CSMWorld::Record newRecord(CSMWorld::RecordBase::State_ModifiedOnly, - nullptr, &oldRecord.get()); + const CSMWorld::Record& oldRecord = mState.mTarget->getData().getLand().getRecord(stage); - mState.mTarget->getData().getLand().setRecord(stage, newRecord); + mState.mTarget->getData().getLand().setRecord(stage, + std::make_unique>( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &oldRecord.get()))); } } -CSMTools::CleanupLandTexturesMergeStage::CleanupLandTexturesMergeStage (MergeState& state) - : mState (state) +CSMTools::CleanupLandTexturesMergeStage::CleanupLandTexturesMergeStage(MergeState& state) + : mState(state) { } @@ -202,10 +211,10 @@ int CSMTools::CleanupLandTexturesMergeStage::setup() return 1; } -void CSMTools::CleanupLandTexturesMergeStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::CleanupLandTexturesMergeStage::perform(int stage, CSMDoc::Messages& messages) { auto& landTextures = mState.mTarget->getData().getLandTextures(); - for (int i = 0; i < landTextures.getSize(); ) + for (int i = 0; i < landTextures.getSize();) { if (!landTextures.getRecord(i).isModified()) landTextures.removeRows(i, 1); diff --git a/apps/opencs/model/tools/mergestages.hpp b/apps/opencs/model/tools/mergestages.hpp index a6b6de58f86..42f06858b17 100644 --- a/apps/opencs/model/tools/mergestages.hpp +++ b/apps/opencs/model/tools/mergestages.hpp @@ -1,8 +1,13 @@ #ifndef CSM_TOOLS_MERGESTAGES_H #define CSM_TOOLS_MERGESTAGES_H -#include #include +#include +#include + +#include +#include +#include #include @@ -12,173 +17,173 @@ #include "mergestate.hpp" +namespace CSMDoc +{ + class Messages; +} + namespace CSMTools { class StartMergeStage : public CSMDoc::Stage { - MergeState& mState; + MergeState& mState; - public: + public: + StartMergeStage(MergeState& state); - StartMergeStage (MergeState& state); + int setup() override; + ///< \return number of steps - int setup() override; - ///< \return number of steps - - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; class FinishMergedDocumentStage : public CSMDoc::Stage { - MergeState& mState; - ToUTF8::Utf8Encoder mEncoder; - - public: + MergeState& mState; + ToUTF8::Utf8Encoder mEncoder; - FinishMergedDocumentStage (MergeState& state, ToUTF8::FromType encoding); + public: + FinishMergedDocumentStage(MergeState& state, ToUTF8::FromType encoding); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; - template > + template > class MergeIdCollectionStage : public CSMDoc::Stage { - MergeState& mState; - Collection& (CSMWorld::Data::*mAccessor)(); + MergeState& mState; + Collection& (CSMWorld::Data::*mAccessor)(); - public: + public: + MergeIdCollectionStage(MergeState& state, Collection& (CSMWorld::Data::*accessor)()); - MergeIdCollectionStage (MergeState& state, Collection& (CSMWorld::Data::*accessor)()); + int setup() override; + ///< \return number of steps - int setup() override; - ///< \return number of steps - - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; - template - MergeIdCollectionStage::MergeIdCollectionStage (MergeState& state, Collection& (CSMWorld::Data::*accessor)()) - : mState (state), mAccessor (accessor) - {} + template + MergeIdCollectionStage::MergeIdCollectionStage( + MergeState& state, Collection& (CSMWorld::Data::*accessor)()) + : mState(state) + , mAccessor(accessor) + { + } - template + template int MergeIdCollectionStage::setup() { return (mState.mSource.getData().*mAccessor)().getSize(); } - template - void MergeIdCollectionStage::perform (int stage, CSMDoc::Messages& messages) + template + void MergeIdCollectionStage::perform(int stage, CSMDoc::Messages& messages) { const Collection& source = (mState.mSource.getData().*mAccessor)(); Collection& target = (mState.mTarget->getData().*mAccessor)(); - const CSMWorld::Record& record = source.getRecord (stage); + const CSMWorld::Record& record = source.getRecord(stage); if (!record.isDeleted()) - target.appendRecord (CSMWorld::Record (CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get())); + target.appendRecord(std::make_unique>( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } class MergeRefIdsStage : public CSMDoc::Stage { - MergeState& mState; - - public: + MergeState& mState; - MergeRefIdsStage (MergeState& state); + public: + MergeRefIdsStage(MergeState& state); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; class MergeReferencesStage : public CSMDoc::Stage { - MergeState& mState; - std::map mIndex; + MergeState& mState; + std::map mIndex; - public: + public: + MergeReferencesStage(MergeState& state); - MergeReferencesStage (MergeState& state); + int setup() override; + ///< \return number of steps - int setup() override; - ///< \return number of steps - - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; /// Adds all land texture records that could potentially be referenced when merging class PopulateLandTexturesMergeStage : public CSMDoc::Stage { - MergeState& mState; - - public: + MergeState& mState; - PopulateLandTexturesMergeStage (MergeState& state); + public: + PopulateLandTexturesMergeStage(MergeState& state); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; class MergeLandStage : public CSMDoc::Stage { - MergeState& mState; + MergeState& mState; - public: + public: + MergeLandStage(MergeState& state); - MergeLandStage (MergeState& state); + int setup() override; + ///< \return number of steps - int setup() override; - ///< \return number of steps - - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; /// During this stage, the complex process of combining LandTextures from /// potentially multiple plugins is undertaken. class FixLandsAndLandTexturesMergeStage : public CSMDoc::Stage { - MergeState& mState; - - public: + MergeState& mState; - FixLandsAndLandTexturesMergeStage (MergeState& state); + public: + FixLandsAndLandTexturesMergeStage(MergeState& state); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; /// Removes base LandTexture records. This gets rid of the base records previously /// needed in FixLandsAndLandTexturesMergeStage. class CleanupLandTexturesMergeStage : public CSMDoc::Stage { - MergeState& mState; - - public: + MergeState& mState; - CleanupLandTexturesMergeStage (MergeState& state); + public: + CleanupLandTexturesMergeStage(MergeState& state); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/mergestate.hpp b/apps/opencs/model/tools/mergestate.hpp index 96e6752e20f..9174854be0d 100644 --- a/apps/opencs/model/tools/mergestate.hpp +++ b/apps/opencs/model/tools/mergestate.hpp @@ -1,10 +1,10 @@ #ifndef CSM_TOOLS_MERGESTATE_H #define CSM_TOOLS_MERGESTATE_H -#include +#include -#include #include +#include #include "../doc/document.hpp" @@ -17,7 +17,11 @@ namespace CSMTools bool mCompleted; std::map, int> mTextureIndices; // (texture, content file) -> new texture - MergeState (CSMDoc::Document& source) : mSource (source), mCompleted (false) {} + MergeState(CSMDoc::Document& source) + : mSource(source) + , mCompleted(false) + { + } }; } diff --git a/apps/opencs/model/tools/pathgridcheck.cpp b/apps/opencs/model/tools/pathgridcheck.cpp index febb79c6419..f03b896321b 100644 --- a/apps/opencs/model/tools/pathgridcheck.cpp +++ b/apps/opencs/model/tools/pathgridcheck.cpp @@ -1,17 +1,25 @@ #include "pathgridcheck.hpp" -#include #include +#include +#include + +#include +#include +#include +#include + +#include #include "../prefs/state.hpp" -#include "../world/universalid.hpp" #include "../world/idcollection.hpp" -#include "../world/subcellcollection.hpp" #include "../world/pathgrid.hpp" +#include "../world/subcellcollection.hpp" +#include "../world/universalid.hpp" -CSMTools::PathgridCheckStage::PathgridCheckStage (const CSMWorld::SubCellCollection& pathgrids) -: mPathgrids (pathgrids) +CSMTools::PathgridCheckStage::PathgridCheckStage(const CSMWorld::SubCellCollection& pathgrids) + : mPathgrids(pathgrids) { mIgnoreBaseRecords = false; } @@ -23,9 +31,9 @@ int CSMTools::PathgridCheckStage::setup() return mPathgrids.getSize(); } -void CSMTools::PathgridCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::PathgridCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mPathgrids.getRecord (stage); + const CSMWorld::Record& record = mPathgrids.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) @@ -33,56 +41,57 @@ void CSMTools::PathgridCheckStage::perform (int stage, CSMDoc::Messages& message const CSMWorld::Pathgrid& pathgrid = record.get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Pathgrid, pathgrid.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Pathgrid, pathgrid.mId); // check the number of pathgrid points - if (pathgrid.mData.mS2 < static_cast(pathgrid.mPoints.size())) - messages.add (id, "Less points than expected", "", CSMDoc::Message::Severity_Error); - else if (pathgrid.mData.mS2 > static_cast(pathgrid.mPoints.size())) - messages.add (id, "More points than expected", "", CSMDoc::Message::Severity_Error); + if (pathgrid.mData.mPoints < pathgrid.mPoints.size()) + messages.add(id, "Less points than expected", "", CSMDoc::Message::Severity_Error); + else if (pathgrid.mData.mPoints > pathgrid.mPoints.size()) + messages.add(id, "More points than expected", "", CSMDoc::Message::Severity_Error); std::vector pointList(pathgrid.mPoints.size()); - std::vector duplList; + std::vector duplList; - for (unsigned int i = 0; i < pathgrid.mEdges.size(); ++i) + for (const auto& edge : pathgrid.mEdges) { - if (pathgrid.mEdges[i].mV0 < static_cast(pathgrid.mPoints.size()) && pathgrid.mEdges[i].mV0 >= 0) + if (edge.mV0 < pathgrid.mPoints.size()) { - pointList[pathgrid.mEdges[i].mV0].mConnectionNum++; + auto& point = pointList[edge.mV0]; + point.mConnectionNum++; // first check for duplicate edges - unsigned int j = 0; - for (; j < pointList[pathgrid.mEdges[i].mV0].mOtherIndex.size(); ++j) + size_t j = 0; + for (; j < point.mOtherIndex.size(); ++j) { - if (pointList[pathgrid.mEdges[i].mV0].mOtherIndex[j] == pathgrid.mEdges[i].mV1) + if (point.mOtherIndex[j] == edge.mV1) { std::ostringstream ss; - ss << "Duplicate edge between points #" << pathgrid.mEdges[i].mV0 << " and #" << pathgrid.mEdges[i].mV1; - messages.add (id, ss.str(), "", CSMDoc::Message::Severity_Error); + ss << "Duplicate edge between points #" << edge.mV0 << " and #" << edge.mV1; + messages.add(id, ss.str(), {}, CSMDoc::Message::Severity_Error); break; } } // only add if not a duplicate - if (j == pointList[pathgrid.mEdges[i].mV0].mOtherIndex.size()) - pointList[pathgrid.mEdges[i].mV0].mOtherIndex.push_back(pathgrid.mEdges[i].mV1); + if (j == point.mOtherIndex.size()) + point.mOtherIndex.push_back(edge.mV1); } else { std::ostringstream ss; - ss << "An edge is connected to a non-existent point #" << pathgrid.mEdges[i].mV0; - messages.add (id, ss.str(), "", CSMDoc::Message::Severity_Error); + ss << "An edge is connected to a non-existent point #" << edge.mV0; + messages.add(id, ss.str(), {}, CSMDoc::Message::Severity_Error); } } - for (unsigned int i = 0; i < pathgrid.mPoints.size(); ++i) + for (size_t i = 0; i < pathgrid.mPoints.size(); ++i) { // check that edges are bidirectional bool foundReverse = false; - for (unsigned int j = 0; j < pointList[i].mOtherIndex.size(); ++j) + for (const auto& otherIndex : pointList[i].mOtherIndex) { - for (unsigned int k = 0; k < pointList[pointList[i].mOtherIndex[j]].mOtherIndex.size(); ++k) + for (const auto& other : pointList[otherIndex].mOtherIndex) { - if (pointList[pointList[i].mOtherIndex[j]].mOtherIndex[k] == static_cast(i)) + if (other == i) { foundReverse = true; break; @@ -92,26 +101,25 @@ void CSMTools::PathgridCheckStage::perform (int stage, CSMDoc::Messages& message if (!foundReverse) { std::ostringstream ss; - ss << "Missing edge between points #" << i << " and #" << pointList[i].mOtherIndex[j]; - messages.add (id, ss.str(), "", CSMDoc::Message::Severity_Error); + ss << "Missing edge between points #" << i << " and #" << otherIndex; + messages.add(id, ss.str(), {}, CSMDoc::Message::Severity_Error); } } // check duplicate points // FIXME: how to do this efficiently? - for (unsigned int j = 0; j != i; ++j) + for (size_t j = 0; j != i; ++j) { - if (pathgrid.mPoints[i].mX == pathgrid.mPoints[j].mX && - pathgrid.mPoints[i].mY == pathgrid.mPoints[j].mY && - pathgrid.mPoints[i].mZ == pathgrid.mPoints[j].mZ) + if (pathgrid.mPoints[i].mX == pathgrid.mPoints[j].mX && pathgrid.mPoints[i].mY == pathgrid.mPoints[j].mY + && pathgrid.mPoints[i].mZ == pathgrid.mPoints[j].mZ) { - std::vector::const_iterator it = find(duplList.begin(), duplList.end(), static_cast(i)); + auto it = std::find(duplList.begin(), duplList.end(), i); if (it == duplList.end()) { std::ostringstream ss; - ss << "Point #" << i << " duplicates point #" << j - << " (" << pathgrid.mPoints[i].mX << ", " << pathgrid.mPoints[i].mY << ", " << pathgrid.mPoints[i].mZ << ")"; - messages.add (id, ss.str(), "", CSMDoc::Message::Severity_Warning); + ss << "Point #" << i << " duplicates point #" << j << " (" << pathgrid.mPoints[i].mX << ", " + << pathgrid.mPoints[i].mY << ", " << pathgrid.mPoints[i].mZ << ")"; + messages.add(id, ss.str(), {}, CSMDoc::Message::Severity_Warning); duplList.push_back(i); break; @@ -121,16 +129,14 @@ void CSMTools::PathgridCheckStage::perform (int stage, CSMDoc::Messages& message } // check pathgrid points that are not connected to anything - for (unsigned int i = 0; i < pointList.size(); ++i) + for (size_t i = 0; i < pointList.size(); ++i) { if (pointList[i].mConnectionNum == 0) { std::ostringstream ss; - ss << "Point #" << i << " (" - << pathgrid.mPoints[i].mX << ", " - << pathgrid.mPoints[i].mY << ", " - << pathgrid.mPoints[i].mZ << ") is disconnected from other points"; - messages.add (id, ss.str(), "", CSMDoc::Message::Severity_Warning); + ss << "Point #" << i << " (" << pathgrid.mPoints[i].mX << ", " << pathgrid.mPoints[i].mY << ", " + << pathgrid.mPoints[i].mZ << ") is disconnected from other points"; + messages.add(id, ss.str(), {}, CSMDoc::Message::Severity_Warning); } } diff --git a/apps/opencs/model/tools/pathgridcheck.hpp b/apps/opencs/model/tools/pathgridcheck.hpp index 212637fd422..29c705f206f 100644 --- a/apps/opencs/model/tools/pathgridcheck.hpp +++ b/apps/opencs/model/tools/pathgridcheck.hpp @@ -1,14 +1,20 @@ #ifndef CSM_TOOLS_PATHGRIDCHECK_H #define CSM_TOOLS_PATHGRIDCHECK_H -#include "../world/collection.hpp" +#include +#include #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + namespace CSMWorld { struct Pathgrid; - template + template class SubCellCollection; } @@ -17,24 +23,25 @@ namespace CSMTools struct Point { unsigned char mConnectionNum; - std::vector mOtherIndex; - Point() : mConnectionNum(0), mOtherIndex(0) {} + std::vector mOtherIndex; + Point() + : mConnectionNum(0) + , mOtherIndex(0) + { + } }; class PathgridCheckStage : public CSMDoc::Stage { - const CSMWorld::SubCellCollection >& mPathgrids; + const CSMWorld::SubCellCollection& mPathgrids; bool mIgnoreBaseRecords; public: - - PathgridCheckStage (const CSMWorld::SubCellCollection >& pathgrids); + explicit PathgridCheckStage(const CSMWorld::SubCellCollection& pathgrids); int setup() override; - void perform (int stage, CSMDoc::Messages& messages) override; + void perform(int stage, CSMDoc::Messages& messages) override; }; } diff --git a/apps/opencs/model/tools/racecheck.cpp b/apps/opencs/model/tools/racecheck.cpp index 6585a31ccbb..8f0df823c3f 100644 --- a/apps/opencs/model/tools/racecheck.cpp +++ b/apps/opencs/model/tools/racecheck.cpp @@ -1,12 +1,21 @@ #include "racecheck.hpp" +#include + #include "../prefs/state.hpp" #include "../world/universalid.hpp" -void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& messages) +#include +#include +#include +#include +#include +#include + +void CSMTools::RaceCheckStage::performPerRecord(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mRaces.getRecord (stage); + const CSMWorld::Record& record = mRaces.getRecord(stage); if (record.isDeleted()) return; @@ -21,42 +30,44 @@ void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& me if (mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) return; - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Race, race.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Race, race.mId); // test for empty name and description if (race.mName.empty()) - messages.add(id, "Name is missing", "", (race.mData.mFlags & 0x1) ? CSMDoc::Message::Severity_Error : CSMDoc::Message::Severity_Warning); + messages.add(id, "Name is missing", "", + (race.mData.mFlags & 0x1) ? CSMDoc::Message::Severity_Error : CSMDoc::Message::Severity_Warning); if (race.mDescription.empty()) messages.add(id, "Description is missing", "", CSMDoc::Message::Severity_Warning); // test for positive height - if (race.mData.mHeight.mMale<=0) + if (race.mData.mMaleHeight <= 0) messages.add(id, "Male height is non-positive", "", CSMDoc::Message::Severity_Error); - if (race.mData.mHeight.mFemale<=0) + if (race.mData.mFemaleHeight <= 0) messages.add(id, "Female height is non-positive", "", CSMDoc::Message::Severity_Error); // test for non-negative weight - if (race.mData.mWeight.mMale<0) + if (race.mData.mMaleWeight < 0) messages.add(id, "Male weight is negative", "", CSMDoc::Message::Severity_Error); - if (race.mData.mWeight.mFemale<0) + if (race.mData.mFemaleWeight < 0) messages.add(id, "Female weight is negative", "", CSMDoc::Message::Severity_Error); /// \todo check data members that can't be edited in the table view } -void CSMTools::RaceCheckStage::performFinal (CSMDoc::Messages& messages) +void CSMTools::RaceCheckStage::performFinal(CSMDoc::Messages& messages) { - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Races); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Races); if (!mPlayable) messages.add(id, "No playable race", "", CSMDoc::Message::Severity_SeriousError); } -CSMTools::RaceCheckStage::RaceCheckStage (const CSMWorld::IdCollection& races) -: mRaces (races), mPlayable (false) +CSMTools::RaceCheckStage::RaceCheckStage(const CSMWorld::IdCollection& races) + : mRaces(races) + , mPlayable(false) { mIgnoreBaseRecords = false; } @@ -66,13 +77,13 @@ int CSMTools::RaceCheckStage::setup() mPlayable = false; mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); - return mRaces.getSize()+1; + return mRaces.getSize() + 1; } -void CSMTools::RaceCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::RaceCheckStage::perform(int stage, CSMDoc::Messages& messages) { - if (stage==mRaces.getSize()) - performFinal (messages); + if (stage == mRaces.getSize()) + performFinal(messages); else - performPerRecord (stage, messages); + performPerRecord(stage, messages); } diff --git a/apps/opencs/model/tools/racecheck.hpp b/apps/opencs/model/tools/racecheck.hpp index 7c70f13b00d..600e0d7fc5f 100644 --- a/apps/opencs/model/tools/racecheck.hpp +++ b/apps/opencs/model/tools/racecheck.hpp @@ -1,34 +1,41 @@ #ifndef CSM_TOOLS_RACECHECK_H #define CSM_TOOLS_RACECHECK_H -#include - #include "../world/idcollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace ESM +{ + struct Race; +} + namespace CSMTools { /// \brief VerifyStage: make sure that race records are internally consistent class RaceCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection& mRaces; - bool mPlayable; - bool mIgnoreBaseRecords; - - void performPerRecord (int stage, CSMDoc::Messages& messages); + const CSMWorld::IdCollection& mRaces; + bool mPlayable; + bool mIgnoreBaseRecords; - void performFinal (CSMDoc::Messages& messages); + void performPerRecord(int stage, CSMDoc::Messages& messages); - public: + void performFinal(CSMDoc::Messages& messages); - RaceCheckStage (const CSMWorld::IdCollection& races); + public: + RaceCheckStage(const CSMWorld::IdCollection& races); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/referenceablecheck.cpp b/apps/opencs/model/tools/referenceablecheck.cpp index 981e8c19578..a00c3acd1c9 100644 --- a/apps/opencs/model/tools/referenceablecheck.cpp +++ b/apps/opencs/model/tools/referenceablecheck.cpp @@ -1,37 +1,75 @@ #include "referenceablecheck.hpp" -#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include "../prefs/state.hpp" #include "../world/record.hpp" #include "../world/universalid.hpp" -CSMTools::ReferenceableCheckStage::ReferenceableCheckStage( - const CSMWorld::RefIdData& referenceable, const CSMWorld::IdCollection& races, - const CSMWorld::IdCollection& classes, - const CSMWorld::IdCollection& faction, - const CSMWorld::IdCollection& scripts, - const CSMWorld::Resources& models, - const CSMWorld::Resources& icons, +#include "effectlistcheck.hpp" + +namespace ESM +{ + class Script; + struct BodyPart; + struct Class; + struct Faction; + struct Race; +} + +CSMTools::ReferenceableCheckStage::ReferenceableCheckStage(const CSMWorld::RefIdData& referenceable, + const CSMWorld::IdCollection& races, const CSMWorld::IdCollection& classes, + const CSMWorld::IdCollection& faction, const CSMWorld::IdCollection& scripts, + const CSMWorld::Resources& models, const CSMWorld::Resources& icons, const CSMWorld::IdCollection& bodyparts) - :mReferencables(referenceable), - mRaces(races), - mClasses(classes), - mFactions(faction), - mScripts(scripts), - mModels(models), - mIcons(icons), - mBodyParts(bodyparts), - mPlayerPresent(false) + : mReferencables(referenceable) + , mRaces(races) + , mClasses(classes) + , mFactions(faction) + , mScripts(scripts) + , mModels(models) + , mIcons(icons) + , mBodyParts(bodyparts) + , mPlayerPresent(false) { mIgnoreBaseRecords = false; } -void CSMTools::ReferenceableCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::ReferenceableCheckStage::perform(int stage, CSMDoc::Messages& messages) { - //Checks for books, than, when stage is above mBooksSize goes to other checks, with (stage - PrevSum) as stage. + // Checks for books, than, when stage is above mBooksSize goes to other checks, with (stage - PrevSum) as stage. const int bookSize(mReferencables.getBooks().getSize()); if (stage < bookSize) @@ -229,7 +267,7 @@ void CSMTools::ReferenceableCheckStage::perform (int stage, CSMDoc::Messages& me creatureCheck(stage, mReferencables.getCreatures(), messages); return; } -// if we come that far, we are about to perform our last, final check. + // if we come that far, we are about to perform our last, final check. finalCheck(messages); return; } @@ -243,9 +281,7 @@ int CSMTools::ReferenceableCheckStage::setup() } void CSMTools::ReferenceableCheckStage::bookCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::Book >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -253,7 +289,7 @@ void CSMTools::ReferenceableCheckStage::bookCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Book& book = (dynamic_cast& >(baseRecord)).get(); + const ESM::Book& book = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Book, book.mId); inventoryItemCheck(book, messages, id.toString(), true); @@ -263,9 +299,7 @@ void CSMTools::ReferenceableCheckStage::bookCheck( } void CSMTools::ReferenceableCheckStage::activatorCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::Activator >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -273,7 +307,7 @@ void CSMTools::ReferenceableCheckStage::activatorCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Activator& activator = (dynamic_cast& >(baseRecord)).get(); + const ESM::Activator& activator = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Activator, activator.mId); if (activator.mModel.empty()) @@ -286,9 +320,7 @@ void CSMTools::ReferenceableCheckStage::activatorCheck( } void CSMTools::ReferenceableCheckStage::potionCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::Potion >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -296,21 +328,19 @@ void CSMTools::ReferenceableCheckStage::potionCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Potion& potion = (dynamic_cast& >(baseRecord)).get(); + const ESM::Potion& potion = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Potion, potion.mId); inventoryItemCheck(potion, messages, id.toString()); - /// \todo Check magic effects for validity + + effectListCheck(potion.mEffects.mList, messages, id); // Check that mentioned scripts exist scriptCheck(potion, messages, id.toString()); } - void CSMTools::ReferenceableCheckStage::apparatusCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::Apparatus >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -318,7 +348,7 @@ void CSMTools::ReferenceableCheckStage::apparatusCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Apparatus& apparatus = (dynamic_cast& >(baseRecord)).get(); + const ESM::Apparatus& apparatus = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Apparatus, apparatus.mId); inventoryItemCheck(apparatus, messages, id.toString()); @@ -330,9 +360,7 @@ void CSMTools::ReferenceableCheckStage::apparatusCheck( } void CSMTools::ReferenceableCheckStage::armorCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::Armor >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -340,7 +368,7 @@ void CSMTools::ReferenceableCheckStage::armorCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Armor& armor = (dynamic_cast& >(baseRecord)).get(); + const ESM::Armor& armor = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Armor, armor.mId); inventoryItemCheck(armor, messages, id.toString(), true); @@ -358,9 +386,7 @@ void CSMTools::ReferenceableCheckStage::armorCheck( } void CSMTools::ReferenceableCheckStage::clothingCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::Clothing >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -368,7 +394,7 @@ void CSMTools::ReferenceableCheckStage::clothingCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Clothing& clothing = (dynamic_cast& >(baseRecord)).get(); + const ESM::Clothing& clothing = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Clothing, clothing.mId); inventoryItemCheck(clothing, messages, id.toString(), true); @@ -377,9 +403,7 @@ void CSMTools::ReferenceableCheckStage::clothingCheck( } void CSMTools::ReferenceableCheckStage::containerCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::Container >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -387,33 +411,32 @@ void CSMTools::ReferenceableCheckStage::containerCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Container& container = (dynamic_cast& >(baseRecord)).get(); + const ESM::Container& container = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Container, container.mId); - //checking for name + // checking for name if (container.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); - //Checking for model + // Checking for model if (container.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(container.mModel) == -1) messages.add(id, "Model '" + container.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); - //Checking for capacity (weight) - if (container.mWeight < 0) //0 is allowed + // Checking for capacity (weight) + if (container.mWeight < 0) // 0 is allowed messages.add(id, "Capacity is negative", "", CSMDoc::Message::Severity_Error); - - //checking contained items + + // checking contained items inventoryListCheck(container.mInventory.mList, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(container, messages, id.toString()); } -void CSMTools::ReferenceableCheckStage::creatureCheck ( - int stage, const CSMWorld::RefIdDataContainer< ESM::Creature >& records, - CSMDoc::Messages& messages) +void CSMTools::ReferenceableCheckStage::creatureCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -432,26 +455,16 @@ void CSMTools::ReferenceableCheckStage::creatureCheck ( else if (mModels.searchId(creature.mModel) == -1) messages.add(id, "Model '" + creature.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); - //stats checks + // stats checks if (creature.mData.mLevel <= 0) messages.add(id, "Level is non-positive", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mStrength < 0) - messages.add(id, "Strength is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mIntelligence < 0) - messages.add(id, "Intelligence is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mWillpower < 0) - messages.add(id, "Willpower is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mAgility < 0) - messages.add(id, "Agility is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mSpeed < 0) - messages.add(id, "Speed is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mEndurance < 0) - messages.add(id, "Endurance is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mPersonality < 0) - messages.add(id, "Personality is negative", "", CSMDoc::Message::Severity_Warning); - if (creature.mData.mLuck < 0) - messages.add(id, "Luck is negative", "", CSMDoc::Message::Severity_Warning); + for (size_t i = 0; i < creature.mData.mAttributes.size(); ++i) + { + if (creature.mData.mAttributes[i] < 0) + messages.add(id, ESM::Attribute::indexToRefId(i).toDebugString() + " is negative", {}, + CSMDoc::Message::Severity_Warning); + } if (creature.mData.mCombat < 0) messages.add(id, "Combat is negative", "", CSMDoc::Message::Severity_Warning); @@ -480,9 +493,13 @@ void CSMTools::ReferenceableCheckStage::creatureCheck ( for (int i = 0; i < 6; ++i) { if (creature.mData.mAttack[i] < 0) - messages.add(id, "Attack " + std::to_string(i/2 + 1) + " has negative" + (i % 2 == 0 ? " minimum " : " maximum ") + "damage", "", CSMDoc::Message::Severity_Error); - if (i % 2 == 0 && creature.mData.mAttack[i] > creature.mData.mAttack[i+1]) - messages.add(id, "Attack " + std::to_string(i/2 + 1) + " has minimum damage higher than maximum damage", "", CSMDoc::Message::Severity_Error); + messages.add(id, + "Attack " + std::to_string(i / 2 + 1) + " has negative" + (i % 2 == 0 ? " minimum " : " maximum ") + + "damage", + "", CSMDoc::Message::Severity_Error); + if (i % 2 == 0 && creature.mData.mAttack[i] > creature.mData.mAttack[i + 1]) + messages.add(id, "Attack " + std::to_string(i / 2 + 1) + " has minimum damage higher than maximum damage", + "", CSMDoc::Message::Severity_Error); } if (creature.mData.mGold < 0) @@ -495,22 +512,23 @@ void CSMTools::ReferenceableCheckStage::creatureCheck ( { CSMWorld::RefIdData::LocalIndex index = mReferencables.searchId(creature.mOriginal); if (index.first == -1) - messages.add(id, "Parent creature '" + creature.mOriginal + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Parent creature '" + creature.mOriginal.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); else if (index.second != CSMWorld::UniversalId::Type_Creature) - messages.add(id, "'" + creature.mOriginal + "' is not a creature", "", CSMDoc::Message::Severity_Error); + messages.add(id, "'" + creature.mOriginal.getRefIdString() + "' is not a creature", "", + CSMDoc::Message::Severity_Error); } // Check inventory inventoryListCheck(creature.mInventory.mList, messages, id.toString()); - + // Check that mentioned scripts exist scriptCheck(creature, messages, id.toString()); /// \todo Check spells, teleport table, AI data and AI packages for validity } void CSMTools::ReferenceableCheckStage::doorCheck( - int stage, const CSMWorld::RefIdDataContainer< ESM::Door >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -521,7 +539,7 @@ void CSMTools::ReferenceableCheckStage::doorCheck( const ESM::Door& door = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Door, door.mId); - //usual, name or model + // usual, name or model if (door.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); @@ -535,9 +553,7 @@ void CSMTools::ReferenceableCheckStage::doorCheck( } void CSMTools::ReferenceableCheckStage::ingredientCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::Ingredient >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -545,19 +561,19 @@ void CSMTools::ReferenceableCheckStage::ingredientCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Ingredient& ingredient = (dynamic_cast& >(baseRecord)).get(); + const ESM::Ingredient& ingredient = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Ingredient, ingredient.mId); inventoryItemCheck(ingredient, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(ingredient, messages, id.toString()); + + ingredientEffectListCheck(ingredient, messages, id); } void CSMTools::ReferenceableCheckStage::creaturesLevListCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::CreatureLevList >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -565,16 +581,16 @@ void CSMTools::ReferenceableCheckStage::creaturesLevListCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::CreatureLevList& CreatureLevList = (dynamic_cast& >(baseRecord)).get(); - CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_CreatureLevelledList, CreatureLevList.mId); //CreatureLevList but Type_CreatureLevelledList :/ + const ESM::CreatureLevList& CreatureLevList + = (dynamic_cast&>(baseRecord)).get(); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_CreatureLevelledList, + CreatureLevList.mId); // CreatureLevList but Type_CreatureLevelledList :/ listCheck(CreatureLevList, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::itemLevelledListCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::ItemLevList >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -582,15 +598,14 @@ void CSMTools::ReferenceableCheckStage::itemLevelledListCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::ItemLevList& ItemLevList = (dynamic_cast& >(baseRecord)).get(); + const ESM::ItemLevList& ItemLevList = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_ItemLevelledList, ItemLevList.mId); listCheck(ItemLevList, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::lightCheck( - int stage, const CSMWorld::RefIdDataContainer< ESM::Light >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -598,7 +613,7 @@ void CSMTools::ReferenceableCheckStage::lightCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Light& light = (dynamic_cast& >(baseRecord)).get(); + const ESM::Light& light = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Light, light.mId); if (light.mData.mRadius < 0) @@ -612,9 +627,7 @@ void CSMTools::ReferenceableCheckStage::lightCheck( } void CSMTools::ReferenceableCheckStage::lockpickCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::Lockpick >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -622,7 +635,7 @@ void CSMTools::ReferenceableCheckStage::lockpickCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Lockpick& lockpick = (dynamic_cast& >(baseRecord)).get(); + const ESM::Lockpick& lockpick = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Lockpick, lockpick.mId); inventoryItemCheck(lockpick, messages, id.toString()); @@ -634,9 +647,7 @@ void CSMTools::ReferenceableCheckStage::lockpickCheck( } void CSMTools::ReferenceableCheckStage::miscCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::Miscellaneous >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -644,7 +655,8 @@ void CSMTools::ReferenceableCheckStage::miscCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Miscellaneous& miscellaneous = (dynamic_cast& >(baseRecord)).get(); + const ESM::Miscellaneous& miscellaneous + = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Miscellaneous, miscellaneous.mId); inventoryItemCheck(miscellaneous, messages, id.toString()); @@ -653,20 +665,19 @@ void CSMTools::ReferenceableCheckStage::miscCheck( scriptCheck(miscellaneous, messages, id.toString()); } -void CSMTools::ReferenceableCheckStage::npcCheck ( - int stage, const CSMWorld::RefIdDataContainer< ESM::NPC >& records, - CSMDoc::Messages& messages) +void CSMTools::ReferenceableCheckStage::npcCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); if (baseRecord.isDeleted()) return; - const ESM::NPC& npc = (dynamic_cast& >(baseRecord)).get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Npc, npc.mId); + const ESM::NPC& npc = (dynamic_cast&>(baseRecord)).get(); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Npc, npc.mId); - //Detect if player is present - if (Misc::StringUtils::ciEqual(npc.mId, "player")) //Happy now, scrawl? + // Detect if player is present + if (npc.mId == "Player") // Happy now, scrawl? mPlayerPresent = true; // Skip "Base" records (setting!) @@ -676,33 +687,15 @@ void CSMTools::ReferenceableCheckStage::npcCheck ( short level(npc.mNpdt.mLevel); int gold(npc.mNpdt.mGold); - if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) //12 = autocalculated + if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) // 12 = autocalculated { - if ((npc.mFlags & ESM::NPC::Autocalc) == 0) //0x0010 = autocalculated flag + if ((npc.mFlags & ESM::NPC::Autocalc) == 0) // 0x0010 = autocalculated flag { - messages.add(id, "NPC with autocalculated stats doesn't have autocalc flag turned on", "", CSMDoc::Message::Severity_Error); //should not happen? + messages.add(id, "NPC with autocalculated stats doesn't have autocalc flag turned on", "", + CSMDoc::Message::Severity_Error); // should not happen? return; } } - else - { - if (npc.mNpdt.mStrength == 0) - messages.add(id, "Strength is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mIntelligence == 0) - messages.add(id, "Intelligence is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mWillpower == 0) - messages.add(id, "Willpower is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mAgility == 0) - messages.add(id, "Agility is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mSpeed == 0) - messages.add(id, "Speed is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mEndurance == 0) - messages.add(id, "Endurance is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mPersonality == 0) - messages.add(id, "Personality is equal to zero", "", CSMDoc::Message::Severity_Warning); - if (npc.mNpdt.mLuck == 0) - messages.add(id, "Luck is equal to zero", "", CSMDoc::Message::Severity_Warning); - } if (level <= 0) messages.add(id, "Level is non-positive", "", CSMDoc::Message::Severity_Warning); @@ -722,23 +715,27 @@ void CSMTools::ReferenceableCheckStage::npcCheck ( if (npc.mClass.empty()) messages.add(id, "Class is missing", "", CSMDoc::Message::Severity_Error); - else if (mClasses.searchId (npc.mClass) == -1) - messages.add(id, "Class '" + npc.mClass + "' does not exist", "", CSMDoc::Message::Severity_Error); + else if (mClasses.searchId(npc.mClass) == -1) + messages.add( + id, "Class '" + npc.mClass.getRefIdString() + "' does not exist", "", CSMDoc::Message::Severity_Error); if (npc.mRace.empty()) messages.add(id, "Race is missing", "", CSMDoc::Message::Severity_Error); - else if (mRaces.searchId (npc.mRace) == -1) - messages.add(id, "Race '" + npc.mRace + "' does not exist", "", CSMDoc::Message::Severity_Error); + else if (mRaces.searchId(npc.mRace) == -1) + messages.add( + id, "Race '" + npc.mRace.getRefIdString() + "' does not exist", "", CSMDoc::Message::Severity_Error); if (!npc.mFaction.empty() && mFactions.searchId(npc.mFaction) == -1) - messages.add(id, "Faction '" + npc.mFaction + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add( + id, "Faction '" + npc.mFaction.getRefIdString() + "' does not exist", "", CSMDoc::Message::Severity_Error); if (npc.mHead.empty()) messages.add(id, "Head is missing", "", CSMDoc::Message::Severity_Error); else { if (mBodyParts.searchId(npc.mHead) == -1) - messages.add(id, "Head body part '" + npc.mHead + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Head body part '" + npc.mHead.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); /// \todo Check gender, race and other body parts stuff validity for the specific NPC } @@ -747,66 +744,50 @@ void CSMTools::ReferenceableCheckStage::npcCheck ( else { if (mBodyParts.searchId(npc.mHair) == -1) - messages.add(id, "Hair body part '" + npc.mHair + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Hair body part '" + npc.mHair.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); /// \todo Check gender, race and other body part stuff validity for the specific NPC } // Check inventory inventoryListCheck(npc.mInventory.mList, messages, id.toString()); - + // Check that mentioned scripts exist scriptCheck(npc, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::weaponCheck( - int stage, const CSMWorld::RefIdDataContainer< ESM::Weapon >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { - const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); + const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Weapon& weapon = (dynamic_cast& >(baseRecord)).get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Weapon, weapon.mId); - - //TODO, It seems that this stuff for spellcasting is obligatory and In fact We should check if records are present - if - ( //THOSE ARE HARDCODED! - !(weapon.mId == "VFX_Hands" || - weapon.mId == "VFX_Absorb" || - weapon.mId == "VFX_Reflect" || - weapon.mId == "VFX_DefaultBolt" || - //TODO I don't know how to get full list of effects :/ - //DANGER!, ACHTUNG! FIXME! The following is the list of the magical bolts, valid for Morrowind.esm. However those are not hardcoded. - weapon.mId == "magic_bolt" || - weapon.mId == "shock_bolt" || - weapon.mId == "shield_bolt" || - weapon.mId == "VFX_DestructBolt" || - weapon.mId == "VFX_PoisonBolt" || - weapon.mId == "VFX_RestoreBolt" || - weapon.mId == "VFX_AlterationBolt" || - weapon.mId == "VFX_ConjureBolt" || - weapon.mId == "VFX_FrostBolt" || - weapon.mId == "VFX_MysticismBolt" || - weapon.mId == "VFX_IllusionBolt" || - weapon.mId == "VFX_Multiple2" || - weapon.mId == "VFX_Multiple3" || - weapon.mId == "VFX_Multiple4" || - weapon.mId == "VFX_Multiple5" || - weapon.mId == "VFX_Multiple6" || - weapon.mId == "VFX_Multiple7" || - weapon.mId == "VFX_Multiple8" || - weapon.mId == "VFX_Multiple9")) + const ESM::Weapon& weapon = (dynamic_cast&>(baseRecord)).get(); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Weapon, weapon.mId); + + // TODO, It seems that this stuff for spellcasting is obligatory and In fact We should check if records are present + if ( // THOSE ARE HARDCODED! + !(weapon.mId == "VFX_Hands" || weapon.mId == "VFX_Absorb" || weapon.mId == "VFX_Reflect" + || weapon.mId == "VFX_DefaultBolt" || + // TODO I don't know how to get full list of effects :/ + // DANGER!, ACHTUNG! FIXME! The following is the list of the magical bolts, valid for Morrowind.esm. However + // those are not hardcoded. + weapon.mId == "magic_bolt" || weapon.mId == "shock_bolt" || weapon.mId == "shield_bolt" + || weapon.mId == "VFX_DestructBolt" || weapon.mId == "VFX_PoisonBolt" || weapon.mId == "VFX_RestoreBolt" + || weapon.mId == "VFX_AlterationBolt" || weapon.mId == "VFX_ConjureBolt" || weapon.mId == "VFX_FrostBolt" + || weapon.mId == "VFX_MysticismBolt" || weapon.mId == "VFX_IllusionBolt" || weapon.mId == "VFX_Multiple2" + || weapon.mId == "VFX_Multiple3" || weapon.mId == "VFX_Multiple4" || weapon.mId == "VFX_Multiple5" + || weapon.mId == "VFX_Multiple6" || weapon.mId == "VFX_Multiple7" || weapon.mId == "VFX_Multiple8" + || weapon.mId == "VFX_Multiple9")) { inventoryItemCheck(weapon, messages, id.toString(), true); - if (!(weapon.mData.mType == ESM::Weapon::MarksmanBow || - weapon.mData.mType == ESM::Weapon::MarksmanCrossbow || - weapon.mData.mType == ESM::Weapon::MarksmanThrown || - weapon.mData.mType == ESM::Weapon::Arrow || - weapon.mData.mType == ESM::Weapon::Bolt)) + if (!(weapon.mData.mType == ESM::Weapon::MarksmanBow || weapon.mData.mType == ESM::Weapon::MarksmanCrossbow + || weapon.mData.mType == ESM::Weapon::MarksmanThrown || weapon.mData.mType == ESM::Weapon::Arrow + || weapon.mData.mType == ESM::Weapon::Bolt)) { if (weapon.mData.mSlash[0] > weapon.mData.mSlash[1]) messages.add(id, "Minimum slash damage higher than maximum", "", CSMDoc::Message::Severity_Warning); @@ -818,11 +799,10 @@ void CSMTools::ReferenceableCheckStage::weaponCheck( if (weapon.mData.mChop[0] > weapon.mData.mChop[1]) messages.add(id, "Minimum chop damage higher than maximum", "", CSMDoc::Message::Severity_Warning); - if (!(weapon.mData.mType == ESM::Weapon::Arrow || - weapon.mData.mType == ESM::Weapon::Bolt || - weapon.mData.mType == ESM::Weapon::MarksmanThrown)) + if (!(weapon.mData.mType == ESM::Weapon::Arrow || weapon.mData.mType == ESM::Weapon::Bolt + || weapon.mData.mType == ESM::Weapon::MarksmanThrown)) { - //checking of health + // checking of health if (weapon.mData.mHealth == 0) messages.add(id, "Durability is equal to zero", "", CSMDoc::Message::Severity_Warning); @@ -836,9 +816,7 @@ void CSMTools::ReferenceableCheckStage::weaponCheck( } void CSMTools::ReferenceableCheckStage::probeCheck( - int stage, - const CSMWorld::RefIdDataContainer< ESM::Probe >& records, - CSMDoc::Messages& messages) + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -846,7 +824,7 @@ void CSMTools::ReferenceableCheckStage::probeCheck( if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Probe& probe = (dynamic_cast& >(baseRecord)).get(); + const ESM::Probe& probe = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Probe, probe.mId); inventoryItemCheck(probe, messages, id.toString()); @@ -856,38 +834,36 @@ void CSMTools::ReferenceableCheckStage::probeCheck( scriptCheck(probe, messages, id.toString()); } -void CSMTools::ReferenceableCheckStage::repairCheck ( - int stage, const CSMWorld::RefIdDataContainer< ESM::Repair >& records, - CSMDoc::Messages& messages) +void CSMTools::ReferenceableCheckStage::repairCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { - const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); + const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Repair& repair = (dynamic_cast& >(baseRecord)).get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Repair, repair.mId); + const ESM::Repair& repair = (dynamic_cast&>(baseRecord)).get(); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Repair, repair.mId); - inventoryItemCheck (repair, messages, id.toString()); - toolCheck (repair, messages, id.toString(), true); + inventoryItemCheck(repair, messages, id.toString()); + toolCheck(repair, messages, id.toString(), true); // Check that mentioned scripts exist scriptCheck(repair, messages, id.toString()); } -void CSMTools::ReferenceableCheckStage::staticCheck ( - int stage, const CSMWorld::RefIdDataContainer< ESM::Static >& records, - CSMDoc::Messages& messages) +void CSMTools::ReferenceableCheckStage::staticCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { - const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); + const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; - const ESM::Static& staticElement = (dynamic_cast& >(baseRecord)).get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Static, staticElement.mId); + const ESM::Static& staticElement = (dynamic_cast&>(baseRecord)).get(); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Static, staticElement.mId); if (staticElement.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); @@ -895,23 +871,23 @@ void CSMTools::ReferenceableCheckStage::staticCheck ( messages.add(id, "Model '" + staticElement.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); } -//final check +// final check -void CSMTools::ReferenceableCheckStage::finalCheck (CSMDoc::Messages& messages) +void CSMTools::ReferenceableCheckStage::finalCheck(CSMDoc::Messages& messages) { if (!mPlayerPresent) - messages.add(CSMWorld::UniversalId::Type_Referenceables, "Player record is missing", "", CSMDoc::Message::Severity_SeriousError); + messages.add(CSMWorld::UniversalId::Type_Referenceables, "Player record is missing", "", + CSMDoc::Message::Severity_SeriousError); } void CSMTools::ReferenceableCheckStage::inventoryListCheck( - const std::vector& itemList, - CSMDoc::Messages& messages, - const std::string& id) + const std::vector& itemList, CSMDoc::Messages& messages, const std::string& id) { for (size_t i = 0; i < itemList.size(); ++i) { - std::string itemName = itemList[i].mItem; - CSMWorld::RefIdData::LocalIndex localIndex = mReferencables.searchId(itemName); + const ESM::RefId& item = itemList[i].mItem; + const auto& itemName = item.getRefIdString(); + CSMWorld::RefIdData::LocalIndex localIndex = mReferencables.searchId(item); if (localIndex.first == -1) messages.add(id, "Item '" + itemName + "' does not exist", "", CSMDoc::Message::Severity_Error); @@ -920,50 +896,51 @@ void CSMTools::ReferenceableCheckStage::inventoryListCheck( // Needs to accommodate containers, creatures, and NPCs switch (localIndex.second) { - case CSMWorld::UniversalId::Type_Potion: - case CSMWorld::UniversalId::Type_Apparatus: - case CSMWorld::UniversalId::Type_Armor: - case CSMWorld::UniversalId::Type_Book: - case CSMWorld::UniversalId::Type_Clothing: - case CSMWorld::UniversalId::Type_Ingredient: - case CSMWorld::UniversalId::Type_Light: - case CSMWorld::UniversalId::Type_Lockpick: - case CSMWorld::UniversalId::Type_Miscellaneous: - case CSMWorld::UniversalId::Type_Probe: - case CSMWorld::UniversalId::Type_Repair: - case CSMWorld::UniversalId::Type_Weapon: - case CSMWorld::UniversalId::Type_ItemLevelledList: - break; - default: - messages.add(id, "'" + itemName + "' is not an item", "", CSMDoc::Message::Severity_Error); + case CSMWorld::UniversalId::Type_Potion: + case CSMWorld::UniversalId::Type_Apparatus: + case CSMWorld::UniversalId::Type_Armor: + case CSMWorld::UniversalId::Type_Book: + case CSMWorld::UniversalId::Type_Clothing: + case CSMWorld::UniversalId::Type_Ingredient: + case CSMWorld::UniversalId::Type_Light: + case CSMWorld::UniversalId::Type_Lockpick: + case CSMWorld::UniversalId::Type_Miscellaneous: + case CSMWorld::UniversalId::Type_Probe: + case CSMWorld::UniversalId::Type_Repair: + case CSMWorld::UniversalId::Type_Weapon: + case CSMWorld::UniversalId::Type_ItemLevelledList: + break; + default: + messages.add(id, "'" + itemName + "' is not an item", "", CSMDoc::Message::Severity_Error); } } } } -//Templates begins here +// Templates begins here -template void CSMTools::ReferenceableCheckStage::inventoryItemCheck ( +template +void CSMTools::ReferenceableCheckStage::inventoryItemCheck( const Item& someItem, CSMDoc::Messages& messages, const std::string& someID, bool enchantable) { if (someItem.mName.empty()) messages.add(someID, "Name is missing", "", CSMDoc::Message::Severity_Error); - //Checking for weight + // Checking for weight if (someItem.mData.mWeight < 0) messages.add(someID, "Weight is negative", "", CSMDoc::Message::Severity_Error); - //Checking for value + // Checking for value if (someItem.mData.mValue < 0) messages.add(someID, "Value is negative", "", CSMDoc::Message::Severity_Error); - //checking for model + // checking for model if (someItem.mModel.empty()) messages.add(someID, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(someItem.mModel) == -1) messages.add(someID, "Model '" + someItem.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); - //checking for icon + // checking for icon if (someItem.mIcon.empty()) messages.add(someID, "Icon is missing", "", CSMDoc::Message::Severity_Error); else if (mIcons.searchId(someItem.mIcon) == -1) @@ -977,27 +954,28 @@ template void CSMTools::ReferenceableCheckStage::inventoryItemChe messages.add(someID, "Enchantment points number is negative", "", CSMDoc::Message::Severity_Error); } -template void CSMTools::ReferenceableCheckStage::inventoryItemCheck ( +template +void CSMTools::ReferenceableCheckStage::inventoryItemCheck( const Item& someItem, CSMDoc::Messages& messages, const std::string& someID) { if (someItem.mName.empty()) messages.add(someID, "Name is missing", "", CSMDoc::Message::Severity_Error); - //Checking for weight + // Checking for weight if (someItem.mData.mWeight < 0) messages.add(someID, "Weight is negative", "", CSMDoc::Message::Severity_Error); - //Checking for value + // Checking for value if (someItem.mData.mValue < 0) messages.add(someID, "Value is negative", "", CSMDoc::Message::Severity_Error); - //checking for model + // checking for model if (someItem.mModel.empty()) messages.add(someID, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(someItem.mModel) == -1) messages.add(someID, "Model '" + someItem.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); - //checking for icon + // checking for icon if (someItem.mIcon.empty()) messages.add(someID, "Icon is missing", "", CSMDoc::Message::Severity_Error); else if (mIcons.searchId(someItem.mIcon) == -1) @@ -1008,47 +986,55 @@ template void CSMTools::ReferenceableCheckStage::inventoryItemChe } } -template void CSMTools::ReferenceableCheckStage::toolCheck ( +template +void CSMTools::ReferenceableCheckStage::toolCheck( const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID, bool canBeBroken) { if (someTool.mData.mQuality <= 0) messages.add(someID, "Quality is non-positive", "", CSMDoc::Message::Severity_Error); - if (canBeBroken && someTool.mData.mUses<=0) + if (canBeBroken && someTool.mData.mUses <= 0) messages.add(someID, "Number of uses is non-positive", "", CSMDoc::Message::Severity_Error); } -template void CSMTools::ReferenceableCheckStage::toolCheck ( +template +void CSMTools::ReferenceableCheckStage::toolCheck( const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID) { if (someTool.mData.mQuality <= 0) messages.add(someID, "Quality is non-positive", "", CSMDoc::Message::Severity_Error); } -template void CSMTools::ReferenceableCheckStage::listCheck ( +template +void CSMTools::ReferenceableCheckStage::listCheck( const List& someList, CSMDoc::Messages& messages, const std::string& someID) { if (someList.mChanceNone > 100) { - messages.add(someID, "Chance that no object is used is over 100 percent", "", CSMDoc::Message::Severity_Warning); + messages.add( + someID, "Chance that no object is used is over 100 percent", "", CSMDoc::Message::Severity_Warning); } - for (unsigned i = 0; i < someList.mList.size(); ++i) + for (const auto& element : someList.mList) { - if (mReferencables.searchId(someList.mList[i].mId).first == -1) - messages.add(someID, "Object '" + someList.mList[i].mId + "' does not exist", "", CSMDoc::Message::Severity_Error); + if (mReferencables.searchId(element.mId).first == -1) + messages.add(someID, "Object '" + element.mId.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); - if (someList.mList[i].mLevel < 1) - messages.add(someID, "Level of item '" + someList.mList[i].mId + "' is non-positive", "", CSMDoc::Message::Severity_Error); + if (element.mLevel < 1) + messages.add(someID, "Level of item '" + element.mId.getRefIdString() + "' is non-positive", "", + CSMDoc::Message::Severity_Error); } } -template void CSMTools::ReferenceableCheckStage::scriptCheck ( +template +void CSMTools::ReferenceableCheckStage::scriptCheck( const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID) { if (!someTool.mScript.empty()) { if (mScripts.searchId(someTool.mScript) == -1) - messages.add(someID, "Script '" + someTool.mScript + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(someID, "Script '" + someTool.mScript.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); } } diff --git a/apps/opencs/model/tools/referenceablecheck.hpp b/apps/opencs/model/tools/referenceablecheck.hpp index 83c8f123257..1c578ecedd0 100644 --- a/apps/opencs/model/tools/referenceablecheck.hpp +++ b/apps/opencs/model/tools/referenceablecheck.hpp @@ -1,95 +1,122 @@ #ifndef REFERENCEABLECHECKSTAGE_H #define REFERENCEABLECHECKSTAGE_H -#include "../world/universalid.hpp" +#include +#include + #include "../doc/stage.hpp" -#include "../world/data.hpp" + +#include "../world/idcollection.hpp" #include "../world/refiddata.hpp" -#include "../world/resources.hpp" + +#include +#include +#include +#include +#include + +namespace CSMWorld +{ + class Resources; +} + +namespace CSMDoc +{ + class Messages; +} namespace CSMTools { class ReferenceableCheckStage : public CSMDoc::Stage { - public: - - ReferenceableCheckStage (const CSMWorld::RefIdData& referenceable, - const CSMWorld::IdCollection& races, - const CSMWorld::IdCollection& classes, - const CSMWorld::IdCollection& factions, - const CSMWorld::IdCollection& scripts, - const CSMWorld::Resources& models, - const CSMWorld::Resources& icons, - const CSMWorld::IdCollection& bodyparts); - - void perform(int stage, CSMDoc::Messages& messages) override; - int setup() override; - - private: - //CONCRETE CHECKS - void bookCheck(int stage, const CSMWorld::RefIdDataContainer< ESM::Book >& records, CSMDoc::Messages& messages); - void activatorCheck(int stage, const CSMWorld::RefIdDataContainer< ESM::Activator >& records, CSMDoc::Messages& messages); - void potionCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void apparatusCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void armorCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void clothingCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void containerCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void creatureCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void doorCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void ingredientCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void creaturesLevListCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void itemLevelledListCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void lightCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void lockpickCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void miscCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void npcCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void weaponCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void probeCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void repairCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - void staticCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); - - //FINAL CHECK - void finalCheck (CSMDoc::Messages& messages); - - //Convenience functions - void inventoryListCheck(const std::vector& itemList, CSMDoc::Messages& messages, const std::string& id); - - template void inventoryItemCheck(const ITEM& someItem, - CSMDoc::Messages& messages, - const std::string& someID, - bool enchantable); //for all enchantable items. - - template void inventoryItemCheck(const ITEM& someItem, - CSMDoc::Messages& messages, - const std::string& someID); //for non-enchantable items. - - template void toolCheck(const TOOL& someTool, - CSMDoc::Messages& messages, - const std::string& someID, - bool canbebroken); //for tools with uses. - - template void toolCheck(const TOOL& someTool, - CSMDoc::Messages& messages, - const std::string& someID); //for tools without uses. - - template void listCheck(const LIST& someList, - CSMDoc::Messages& messages, - const std::string& someID); - - template void scriptCheck(const TOOL& someTool, - CSMDoc::Messages& messages, - const std::string& someID); - - const CSMWorld::RefIdData& mReferencables; - const CSMWorld::IdCollection& mRaces; - const CSMWorld::IdCollection& mClasses; - const CSMWorld::IdCollection& mFactions; - const CSMWorld::IdCollection& mScripts; - const CSMWorld::Resources& mModels; - const CSMWorld::Resources& mIcons; - const CSMWorld::IdCollection& mBodyParts; - bool mPlayerPresent; - bool mIgnoreBaseRecords; + public: + ReferenceableCheckStage(const CSMWorld::RefIdData& referenceable, + const CSMWorld::IdCollection& races, const CSMWorld::IdCollection& classes, + const CSMWorld::IdCollection& factions, const CSMWorld::IdCollection& scripts, + const CSMWorld::Resources& models, const CSMWorld::Resources& icons, + const CSMWorld::IdCollection& bodyparts); + + void perform(int stage, CSMDoc::Messages& messages) override; + int setup() override; + + private: + // CONCRETE CHECKS + void bookCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void activatorCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void potionCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void apparatusCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void armorCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void clothingCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void containerCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void creatureCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void doorCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void ingredientCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void creaturesLevListCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void itemLevelledListCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void lightCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void lockpickCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void miscCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void npcCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void weaponCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void probeCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void repairCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void staticCheck( + int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + + // FINAL CHECK + void finalCheck(CSMDoc::Messages& messages); + + // Convenience functions + void inventoryListCheck( + const std::vector& itemList, CSMDoc::Messages& messages, const std::string& id); + + /// for all enchantable items. + template + inline void inventoryItemCheck( + const Item& someItem, CSMDoc::Messages& messages, const std::string& someID, bool enchantable); + + /// for non-enchantable items. + template + inline void inventoryItemCheck(const Item& someItem, CSMDoc::Messages& messages, const std::string& someID); + + /// for tools with uses. + template + inline void toolCheck( + const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID, bool canbebroken); + + /// for tools without uses. + template + inline void toolCheck(const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID); + + template + inline void listCheck(const List& someList, CSMDoc::Messages& messages, const std::string& someID); + + template + inline void scriptCheck(const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID); + + const CSMWorld::RefIdData& mReferencables; + const CSMWorld::IdCollection& mRaces; + const CSMWorld::IdCollection& mClasses; + const CSMWorld::IdCollection& mFactions; + const CSMWorld::IdCollection& mScripts; + const CSMWorld::Resources& mModels; + const CSMWorld::Resources& mIcons; + const CSMWorld::IdCollection& mBodyParts; + bool mPlayerPresent; + bool mIgnoreBaseRecords; }; } #endif // REFERENCEABLECHECKSTAGE_H diff --git a/apps/opencs/model/tools/referencecheck.cpp b/apps/opencs/model/tools/referencecheck.cpp index d8ff9f20efb..306094f2f5e 100644 --- a/apps/opencs/model/tools/referencecheck.cpp +++ b/apps/opencs/model/tools/referencecheck.cpp @@ -1,23 +1,40 @@ #include "referencecheck.hpp" +#include + #include "../prefs/state.hpp" -CSMTools::ReferenceCheckStage::ReferenceCheckStage( - const CSMWorld::RefCollection& references, - const CSMWorld::RefIdCollection& referencables, - const CSMWorld::IdCollection& cells, - const CSMWorld::IdCollection& factions) - : - mReferences(references), - mObjects(referencables), - mDataSet(referencables.getDataSet()), - mCells(cells), - mFactions(factions) +#include "../../model/world/cell.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +CSMTools::ReferenceCheckStage::ReferenceCheckStage(const CSMWorld::RefCollection& references, + const CSMWorld::RefIdCollection& referencables, const CSMWorld::IdCollection& cells, + const CSMWorld::IdCollection& factions, const CSMWorld::IdCollection& bodyparts) + : mReferences(references) + , mObjects(referencables) + , mDataSet(referencables.getDataSet()) + , mCells(cells) + , mFactions(factions) + , mBodyParts(bodyparts) { mIgnoreBaseRecords = false; } -void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &messages) +void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mReferences.getRecord(stage); @@ -28,15 +45,28 @@ void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &message const CSMWorld::CellRef& cellRef = record.get(); const CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Reference, cellRef.mId); + // Check RefNum is unique per content file, otherwise can cause load issues + const auto refNum = cellRef.mRefNum; + const auto insertResult = mUsedReferenceIDs.emplace(refNum, cellRef.mId); + if (!insertResult.second) + messages.add(id, + "Duplicate RefNum: " + std::to_string(refNum.mContentFile) + std::string("-") + + std::to_string(refNum.mIndex) + " shared with cell reference " + + insertResult.first->second.toString(), + "", CSMDoc::Message::Severity_Error); + // Check reference id if (cellRef.mRefID.empty()) messages.add(id, "Instance is not based on an object", "", CSMDoc::Message::Severity_Error); - else + else { // Check for non existing referenced object - if (mObjects.searchId(cellRef.mRefID) == -1) - messages.add(id, "Instance of a non-existent object '" + cellRef.mRefID + "'", "", CSMDoc::Message::Severity_Error); - else + if (mObjects.searchId(cellRef.mRefID) == -1 && mBodyParts.searchId(cellRef.mRefID) == -1) + { + messages.add(id, "Instance of a non-existent object '" + cellRef.mRefID.getRefIdString() + "'", "", + CSMDoc::Message::Severity_Error); + } + else { // Check if reference charge is valid for it's proper referenced type CSMWorld::RefIdData::LocalIndex localIndex = mDataSet.searchId(cellRef.mRefID); @@ -48,12 +78,14 @@ void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &message // If object have owner, check if that owner reference is valid if (!cellRef.mOwner.empty() && mObjects.searchId(cellRef.mOwner) == -1) - messages.add(id, "Owner object '" + cellRef.mOwner + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Owner object '" + cellRef.mOwner.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); // If object have creature soul trapped, check if that creature reference is valid if (!cellRef.mSoul.empty()) if (mObjects.searchId(cellRef.mSoul) == -1) - messages.add(id, "Trapped soul object '" + cellRef.mSoul + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Trapped soul object '" + cellRef.mSoul.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); if (cellRef.mFaction.empty()) { @@ -63,13 +95,15 @@ void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &message else { if (mFactions.searchId(cellRef.mFaction) == -1) - messages.add(id, "Faction '" + cellRef.mFaction + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Faction '" + cellRef.mFaction.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); else if (cellRef.mFactionRank < -1) messages.add(id, "Invalid faction rank", "", CSMDoc::Message::Severity_Error); } - if (!cellRef.mDestCell.empty() && mCells.searchId(cellRef.mDestCell) == -1) - messages.add(id, "Destination cell '" + cellRef.mDestCell + "' does not exist", "", CSMDoc::Message::Severity_Error); + if (!cellRef.mDestCell.empty() && mCells.searchId(ESM::RefId::stringRefId(cellRef.mDestCell)) == -1) + messages.add( + id, "Destination cell '" + cellRef.mDestCell + "' does not exist", "", CSMDoc::Message::Severity_Error); if (cellRef.mScale < 0) messages.add(id, "Negative scale", "", CSMDoc::Message::Severity_Error); @@ -78,14 +112,14 @@ void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &message if (cellRef.mEnchantmentCharge < -1) messages.add(id, "Negative number of enchantment points", "", CSMDoc::Message::Severity_Error); - // Check if gold value isn't negative - if (cellRef.mGoldValue < 0) - messages.add(id, "Negative gold value", "", CSMDoc::Message::Severity_Error); + if (cellRef.mCount < 1) + messages.add(id, "Reference without count", {}, CSMDoc::Message::Severity_Error); } int CSMTools::ReferenceCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mUsedReferenceIDs.clear(); return mReferences.getSize(); } diff --git a/apps/opencs/model/tools/referencecheck.hpp b/apps/opencs/model/tools/referencecheck.hpp index 2da139869c8..58fb4e4170c 100644 --- a/apps/opencs/model/tools/referencecheck.hpp +++ b/apps/opencs/model/tools/referencecheck.hpp @@ -1,29 +1,50 @@ #ifndef CSM_TOOLS_REFERENCECHECK_H #define CSM_TOOLS_REFERENCECHECK_H -#include "../doc/state.hpp" -#include "../doc/document.hpp" +#include "../world/idcollection.hpp" +#include "../world/refcollection.hpp" + +#include "../doc/stage.hpp" + +namespace ESM +{ + struct BodyPart; + struct Faction; +} + +namespace CSMDoc +{ + class Messages; +} + +namespace CSMWorld +{ + class RefIdCollection; + class RefIdData; + struct Cell; +} namespace CSMTools { class ReferenceCheckStage : public CSMDoc::Stage { - public: - ReferenceCheckStage (const CSMWorld::RefCollection& references, - const CSMWorld::RefIdCollection& referencables, - const CSMWorld::IdCollection& cells, - const CSMWorld::IdCollection& factions); - - void perform(int stage, CSMDoc::Messages& messages) override; - int setup() override; - - private: - const CSMWorld::RefCollection& mReferences; - const CSMWorld::RefIdCollection& mObjects; - const CSMWorld::RefIdData& mDataSet; - const CSMWorld::IdCollection& mCells; - const CSMWorld::IdCollection& mFactions; - bool mIgnoreBaseRecords; + public: + ReferenceCheckStage(const CSMWorld::RefCollection& references, const CSMWorld::RefIdCollection& referencables, + const CSMWorld::IdCollection& cells, const CSMWorld::IdCollection& factions, + const CSMWorld::IdCollection& bodyparts); + + void perform(int stage, CSMDoc::Messages& messages) override; + int setup() override; + + private: + const CSMWorld::RefCollection& mReferences; + const CSMWorld::RefIdCollection& mObjects; + const CSMWorld::RefIdData& mDataSet; + const CSMWorld::IdCollection& mCells; + const CSMWorld::IdCollection& mFactions; + const CSMWorld::IdCollection& mBodyParts; + std::unordered_map mUsedReferenceIDs; + bool mIgnoreBaseRecords; }; } diff --git a/apps/opencs/model/tools/regioncheck.cpp b/apps/opencs/model/tools/regioncheck.cpp index 27a73be93d4..08e8a17714b 100644 --- a/apps/opencs/model/tools/regioncheck.cpp +++ b/apps/opencs/model/tools/regioncheck.cpp @@ -1,11 +1,22 @@ #include "regioncheck.hpp" +#include +#include + #include "../prefs/state.hpp" #include "../world/universalid.hpp" -CSMTools::RegionCheckStage::RegionCheckStage (const CSMWorld::IdCollection& regions) -: mRegions (regions) +#include +#include +#include +#include +#include + +#include + +CSMTools::RegionCheckStage::RegionCheckStage(const CSMWorld::IdCollection& regions) + : mRegions(regions) { mIgnoreBaseRecords = false; } @@ -17,9 +28,9 @@ int CSMTools::RegionCheckStage::setup() return mRegions.getSize(); } -void CSMTools::RegionCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::RegionCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mRegions.getRecord (stage); + const CSMWorld::Record& record = mRegions.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) @@ -27,7 +38,7 @@ void CSMTools::RegionCheckStage::perform (int stage, CSMDoc::Messages& messages) const ESM::Region& region = record.get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Region, region.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Region, region.mId); // test for empty name if (region.mName.empty()) @@ -36,16 +47,15 @@ void CSMTools::RegionCheckStage::perform (int stage, CSMDoc::Messages& messages) /// \todo test that the ID in mSleeplist exists // test that chances add up to 100 - int chances = region.mData.mClear + region.mData.mCloudy + region.mData.mFoggy + region.mData.mOvercast + - region.mData.mRain + region.mData.mThunder + region.mData.mAsh + region.mData.mBlight + - region.mData.mA + region.mData.mB; + auto chances = std::accumulate(region.mData.mProbabilities.begin(), region.mData.mProbabilities.end(), 0u); if (chances != 100) messages.add(id, "Weather chances do not add up to 100", "", CSMDoc::Message::Severity_Error); for (const ESM::Region::SoundRef& sound : region.mSoundList) { if (sound.mChance > 100) - messages.add(id, "Chance of '" + sound.mSound + "' sound to play is over 100 percent", "", CSMDoc::Message::Severity_Warning); + messages.add(id, "Chance of '" + sound.mSound.getRefIdString() + "' sound to play is over 100 percent", "", + CSMDoc::Message::Severity_Warning); } /// \todo check data members that can't be edited in the table view diff --git a/apps/opencs/model/tools/regioncheck.hpp b/apps/opencs/model/tools/regioncheck.hpp index e7ddb0bcab9..9ac6e4e92e0 100644 --- a/apps/opencs/model/tools/regioncheck.hpp +++ b/apps/opencs/model/tools/regioncheck.hpp @@ -1,29 +1,36 @@ #ifndef CSM_TOOLS_REGIONCHECK_H #define CSM_TOOLS_REGIONCHECK_H -#include - #include "../world/idcollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace ESM +{ + struct Region; +} + namespace CSMTools { /// \brief VerifyStage: make sure that region records are internally consistent class RegionCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection& mRegions; - bool mIgnoreBaseRecords; - - public: + const CSMWorld::IdCollection& mRegions; + bool mIgnoreBaseRecords; - RegionCheckStage (const CSMWorld::IdCollection& regions); + public: + RegionCheckStage(const CSMWorld::IdCollection& regions); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/reportmodel.cpp b/apps/opencs/model/tools/reportmodel.cpp index a2901a66301..f9251acdabd 100644 --- a/apps/opencs/model/tools/reportmodel.cpp +++ b/apps/opencs/model/tools/reportmodel.cpp @@ -1,12 +1,18 @@ #include "reportmodel.hpp" -#include +#include +#include #include +#include + +#include +#include #include "../world/columns.hpp" -CSMTools::ReportModel::ReportModel (bool fieldColumn, bool severityColumn) -: mColumnField (-1), mColumnSeverity (-1) +CSMTools::ReportModel::ReportModel(bool fieldColumn, bool severityColumn) + : mColumnField(-1) + , mColumnSeverity(-1) { int index = 3; @@ -19,7 +25,7 @@ CSMTools::ReportModel::ReportModel (bool fieldColumn, bool severityColumn) mColumnDescription = index; } -int CSMTools::ReportModel::rowCount (const QModelIndex & parent) const +int CSMTools::ReportModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; @@ -27,110 +33,118 @@ int CSMTools::ReportModel::rowCount (const QModelIndex & parent) const return static_cast(mRows.size()); } -int CSMTools::ReportModel::columnCount (const QModelIndex & parent) const +int CSMTools::ReportModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; - return mColumnDescription+1; + return mColumnDescription + 1; } -QVariant CSMTools::ReportModel::data (const QModelIndex & index, int role) const +QVariant CSMTools::ReportModel::data(const QModelIndex& index, int role) const { - if (role!=Qt::DisplayRole && role!=Qt::UserRole) + if (role != Qt::DisplayRole && role != Qt::UserRole) return QVariant(); switch (index.column()) { case Column_Type: - if(role == Qt::UserRole) - return QString::fromUtf8 ( - mRows.at (index.row()).mId.getTypeName().c_str()); + if (role == Qt::UserRole) + return QString::fromUtf8(mRows.at(index.row()).mId.getTypeName().c_str()); else - return static_cast (mRows.at (index.row()).mId.getType()); + return static_cast(mRows.at(index.row()).mId.getType()); case Column_Id: { - CSMWorld::UniversalId id = mRows.at (index.row()).mId; - - if (id.getArgumentType()==CSMWorld::UniversalId::ArgumentType_Id) - return QString::fromUtf8 (id.getId().c_str()); - - return QString ("-"); + CSMWorld::UniversalId id = mRows.at(index.row()).mId; + + switch (id.getArgumentType()) + { + case CSMWorld::UniversalId::ArgumentType_None: + return QString("-"); + case CSMWorld::UniversalId::ArgumentType_Index: + return QString::number(id.getIndex()); + case CSMWorld::UniversalId::ArgumentType_Id: + return QString::fromStdString(id.getId()); + case CSMWorld::UniversalId::ArgumentType_RefId: + return QString::fromStdString(id.getRefId().toString()); + } + + return QString("unsupported"); } case Column_Hint: - return QString::fromUtf8 (mRows.at (index.row()).mHint.c_str()); + return QString::fromUtf8(mRows.at(index.row()).mHint.c_str()); } - if (index.column()==mColumnDescription) - return QString::fromUtf8 (mRows.at (index.row()).mMessage.c_str()); + if (index.column() == mColumnDescription) + return QString::fromUtf8(mRows.at(index.row()).mMessage.c_str()); - if (index.column()==mColumnField) + if (index.column() == mColumnField) { std::string field; - std::istringstream stream (mRows.at (index.row()).mHint); + std::istringstream stream(mRows.at(index.row()).mHint); char type, ignore; int fieldIndex; - if ((stream >> type >> ignore >> fieldIndex) && (type=='r' || type=='R')) + if ((stream >> type >> ignore >> fieldIndex) && (type == 'r' || type == 'R')) { - field = CSMWorld::Columns::getName ( - static_cast (fieldIndex)); + field = CSMWorld::Columns::getName(static_cast(fieldIndex)); } - return QString::fromUtf8 (field.c_str()); + return QString::fromUtf8(field.c_str()); } - if (index.column()==mColumnSeverity) + if (index.column() == mColumnSeverity) { - return QString::fromUtf8 ( - CSMDoc::Message::toString (mRows.at (index.row()).mSeverity).c_str()); + return QString::fromUtf8(CSMDoc::Message::toString(mRows.at(index.row()).mSeverity).c_str()); } return QVariant(); } -QVariant CSMTools::ReportModel::headerData (int section, Qt::Orientation orientation, int role) const +QVariant CSMTools::ReportModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (role!=Qt::DisplayRole) + if (role != Qt::DisplayRole) return QVariant(); - if (orientation==Qt::Vertical) + if (orientation == Qt::Vertical) return QVariant(); switch (section) { - case Column_Type: return "Type"; - case Column_Id: return "ID"; + case Column_Type: + return "Type"; + case Column_Id: + return "ID"; } - if (section==mColumnDescription) + if (section == mColumnDescription) return "Description"; - if (section==mColumnField) + if (section == mColumnField) return "Field"; - if (section==mColumnSeverity) + if (section == mColumnSeverity) return "Severity"; return "-"; } -bool CSMTools::ReportModel::removeRows (int row, int count, const QModelIndex& parent) +bool CSMTools::ReportModel::removeRows(int row, int count, const QModelIndex& parent) { if (parent.isValid()) return false; - if (count>0) + if (count > 0) { - beginRemoveRows (parent, row, row+count-1); + beginRemoveRows(parent, row, row + count - 1); - mRows.erase (mRows.begin()+row, mRows.begin()+row+count); + mRows.erase(mRows.begin() + row, mRows.begin() + row + count); endRemoveRows(); } @@ -138,45 +152,45 @@ bool CSMTools::ReportModel::removeRows (int row, int count, const QModelIndex& p return true; } -void CSMTools::ReportModel::add (const CSMDoc::Message& message) +void CSMTools::ReportModel::add(const CSMDoc::Message& message) { - beginInsertRows (QModelIndex(), static_cast(mRows.size()), static_cast(mRows.size())); + beginInsertRows(QModelIndex(), static_cast(mRows.size()), static_cast(mRows.size())); - mRows.push_back (message); + mRows.push_back(message); endInsertRows(); } -void CSMTools::ReportModel::flagAsReplaced (int index) +void CSMTools::ReportModel::flagAsReplaced(int index) { - CSMDoc::Message& line = mRows.at (index); + CSMDoc::Message& line = mRows.at(index); std::string hint = line.mHint; - if (hint.empty() || hint[0]!='R') - throw std::logic_error ("trying to flag message as replaced that is not replaceable"); + if (hint.empty() || hint[0] != 'R') + throw std::logic_error("trying to flag message as replaced that is not replaceable"); hint[0] = 'r'; - line.mHint = hint; + line.mHint = std::move(hint); - emit dataChanged (this->index (index, 0), this->index (index, columnCount())); + emit dataChanged(this->index(index, 0), this->index(index, columnCount())); } -const CSMWorld::UniversalId& CSMTools::ReportModel::getUniversalId (int row) const +const CSMWorld::UniversalId& CSMTools::ReportModel::getUniversalId(int row) const { - return mRows.at (row).mId; + return mRows.at(row).mId; } -std::string CSMTools::ReportModel::getHint (int row) const +std::string CSMTools::ReportModel::getHint(int row) const { - return mRows.at (row).mHint; + return mRows.at(row).mHint; } void CSMTools::ReportModel::clear() { if (!mRows.empty()) { - beginRemoveRows (QModelIndex(), 0, static_cast(mRows.size())-1); + beginRemoveRows(QModelIndex(), 0, static_cast(mRows.size()) - 1); mRows.clear(); endRemoveRows(); } @@ -186,10 +200,9 @@ int CSMTools::ReportModel::countErrors() const { int count = 0; - for (std::vector::const_iterator iter (mRows.begin()); - iter!=mRows.end(); ++iter) - if (iter->mSeverity==CSMDoc::Message::Severity_Error || - iter->mSeverity==CSMDoc::Message::Severity_SeriousError) + for (std::vector::const_iterator iter(mRows.begin()); iter != mRows.end(); ++iter) + if (iter->mSeverity == CSMDoc::Message::Severity_Error + || iter->mSeverity == CSMDoc::Message::Severity_SeriousError) ++count; return count; diff --git a/apps/opencs/model/tools/reportmodel.hpp b/apps/opencs/model/tools/reportmodel.hpp index 809dfcc3e81..449105cea00 100644 --- a/apps/opencs/model/tools/reportmodel.hpp +++ b/apps/opencs/model/tools/reportmodel.hpp @@ -1,60 +1,66 @@ #ifndef CSM_TOOLS_REPORTMODEL_H #define CSM_TOOLS_REPORTMODEL_H -#include #include +#include #include +#include +#include #include "../doc/messages.hpp" -#include "../world/universalid.hpp" +namespace CSMWorld +{ + class UniversalId; +} namespace CSMTools { class ReportModel : public QAbstractTableModel { - Q_OBJECT + Q_OBJECT + + std::vector mRows; + + // Fixed columns + enum Columns + { + Column_Type = 0, + Column_Id = 1, + Column_Hint = 2 + }; - std::vector mRows; + // Configurable columns + int mColumnDescription; + int mColumnField; + int mColumnSeverity; - // Fixed columns - enum Columns - { - Column_Type = 0, Column_Id = 1, Column_Hint = 2 - }; + public: + ReportModel(bool fieldColumn = false, bool severityColumn = true); - // Configurable columns - int mColumnDescription; - int mColumnField; - int mColumnSeverity; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; - public: + int columnCount(const QModelIndex& parent = QModelIndex()) const override; - ReportModel (bool fieldColumn = false, bool severityColumn = true); - - int rowCount (const QModelIndex & parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - int columnCount (const QModelIndex & parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const override; + bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; - QVariant headerData (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + void add(const CSMDoc::Message& message); - bool removeRows (int row, int count, const QModelIndex& parent = QModelIndex()) override; - - void add (const CSMDoc::Message& message); + void flagAsReplaced(int index); - void flagAsReplaced (int index); - - const CSMWorld::UniversalId& getUniversalId (int row) const; + const CSMWorld::UniversalId& getUniversalId(int row) const; - std::string getHint (int row) const; + std::string getHint(int row) const; - void clear(); + void clear(); - // Return number of messages with Error or SeriousError severity. - int countErrors() const; + // Return number of messages with Error or SeriousError severity. + int countErrors() const; }; } diff --git a/apps/opencs/model/tools/scriptcheck.cpp b/apps/opencs/model/tools/scriptcheck.cpp index 46a74362b03..7a76efa4835 100644 --- a/apps/opencs/model/tools/scriptcheck.cpp +++ b/apps/opencs/model/tools/scriptcheck.cpp @@ -1,10 +1,23 @@ #include "scriptcheck.hpp" -#include -#include -#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + #include #include +#include +#include +#include +#include #include "../doc/document.hpp" @@ -12,51 +25,56 @@ #include "../prefs/state.hpp" -CSMDoc::Message::Severity CSMTools::ScriptCheckStage::getSeverity (Type type) +CSMDoc::Message::Severity CSMTools::ScriptCheckStage::getSeverity(Type type) { switch (type) { - case WarningMessage: return CSMDoc::Message::Severity_Warning; - case ErrorMessage: return CSMDoc::Message::Severity_Error; + case WarningMessage: + return CSMDoc::Message::Severity_Warning; + case ErrorMessage: + return CSMDoc::Message::Severity_Error; } return CSMDoc::Message::Severity_SeriousError; } -void CSMTools::ScriptCheckStage::report (const std::string& message, const Compiler::TokenLoc& loc, - Type type) +void CSMTools::ScriptCheckStage::report(const std::string& message, const Compiler::TokenLoc& loc, Type type) { std::ostringstream stream; - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Script, mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Script, mId); - stream << message << " (" << loc.mLiteral << ")" << " @ line " << loc.mLine+1 << ", column " << loc.mColumn; + stream << message << " (" << loc.mLiteral << ")" + << " @ line " << loc.mLine + 1 << ", column " << loc.mColumn; std::ostringstream hintStream; - hintStream << "l:" << loc.mLine+1 << " " << loc.mColumn; + hintStream << "l:" << loc.mLine + 1 << " " << loc.mColumn; - mMessages->add (id, stream.str(), hintStream.str(), getSeverity (type)); + mMessages->add(id, stream.str(), hintStream.str(), getSeverity(type)); } -void CSMTools::ScriptCheckStage::report (const std::string& message, Type type) +void CSMTools::ScriptCheckStage::report(const std::string& message, Type type) { - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Script, mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Script, mId); std::ostringstream stream; stream << message; - mMessages->add (id, stream.str(), "", getSeverity (type)); + mMessages->add(id, stream.str(), "", getSeverity(type)); } -CSMTools::ScriptCheckStage::ScriptCheckStage (const CSMDoc::Document& document) -: mDocument (document), mContext (document.getData()), mMessages (nullptr), mWarningMode (Mode_Ignore) +CSMTools::ScriptCheckStage::ScriptCheckStage(const CSMDoc::Document& document) + : mDocument(document) + , mContext(document.getData()) + , mMessages(nullptr) + , mWarningMode(Mode_Ignore) { /// \todo add an option to configure warning mode - setWarningsMode (0); + setWarningsMode(0); - Compiler::registerExtensions (mExtensions); - mContext.setExtensions (&mExtensions); + Compiler::registerExtensions(mExtensions); + mContext.setExtensions(&mExtensions); mIgnoreBaseRecords = false; } @@ -65,16 +83,16 @@ int CSMTools::ScriptCheckStage::setup() { std::string warnings = CSMPrefs::get()["Scripts"]["warnings"].toString(); - if (warnings=="Ignore") + if (warnings == "Ignore") mWarningMode = Mode_Ignore; - else if (warnings=="Normal") + else if (warnings == "Normal") mWarningMode = Mode_Normal; - else if (warnings=="Strict") + else if (warnings == "Strict") mWarningMode = Mode_Strict; mContext.clear(); mMessages = nullptr; - mId.clear(); + mId = ESM::RefId(); Compiler::ErrorHandler::reset(); mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); @@ -82,14 +100,13 @@ int CSMTools::ScriptCheckStage::setup() return mDocument.getData().getScripts().getSize(); } -void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::ScriptCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record &record = mDocument.getData().getScripts().getRecord(stage); + const CSMWorld::Record& record = mDocument.getData().getScripts().getRecord(stage); - mId = mDocument.getData().getScripts().getId (stage); + mId = mDocument.getData().getScripts().getId(stage); - if (mDocument.isBlacklisted ( - CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Script, mId))) + if (mDocument.isBlacklisted(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Script, mId.getRefIdString()))) return; // Skip "Base" records (setting!) and "Deleted" records @@ -100,21 +117,27 @@ void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) switch (mWarningMode) { - case Mode_Ignore: setWarningsMode (0); break; - case Mode_Normal: setWarningsMode (1); break; - case Mode_Strict: setWarningsMode (2); break; + case Mode_Ignore: + setWarningsMode(0); + break; + case Mode_Normal: + setWarningsMode(1); + break; + case Mode_Strict: + setWarningsMode(2); + break; } try { - mFile = record.get().mId; - std::istringstream input (record.get().mScriptText); + mFile = record.get().mId.getRefIdString(); + std::istringstream input(record.get().mScriptText); - Compiler::Scanner scanner (*this, input, mContext.getExtensions()); + Compiler::Scanner scanner(*this, input, mContext.getExtensions()); - Compiler::FileParser parser (*this, mContext); + Compiler::FileParser parser(*this, mContext); - scanner.scan (parser); + scanner.scan(parser); } catch (const Compiler::SourceException&) { @@ -122,12 +145,12 @@ void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) } catch (const std::exception& error) { - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Script, mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Script, mId); std::ostringstream stream; stream << error.what(); - messages.add (id, stream.str(), "", CSMDoc::Message::Severity_SeriousError); + messages.add(id, stream.str(), "", CSMDoc::Message::Severity_SeriousError); } mMessages = nullptr; diff --git a/apps/opencs/model/tools/scriptcheck.hpp b/apps/opencs/model/tools/scriptcheck.hpp index d975e02da59..31b3f32915e 100644 --- a/apps/opencs/model/tools/scriptcheck.hpp +++ b/apps/opencs/model/tools/scriptcheck.hpp @@ -1,9 +1,13 @@ #ifndef CSM_TOOLS_SCRIPTCHECK_H #define CSM_TOOLS_SCRIPTCHECK_H +#include + #include #include +#include + #include "../doc/stage.hpp" #include "../world/scriptcontext.hpp" @@ -13,44 +17,48 @@ namespace CSMDoc class Document; } +namespace Compiler +{ + struct TokenLoc; +} + namespace CSMTools { /// \brief VerifyStage: make sure that scripts compile class ScriptCheckStage : public CSMDoc::Stage, private Compiler::ErrorHandler { - enum WarningMode - { - Mode_Ignore, - Mode_Normal, - Mode_Strict - }; - - const CSMDoc::Document& mDocument; - Compiler::Extensions mExtensions; - CSMWorld::ScriptContext mContext; - std::string mId; - std::string mFile; - CSMDoc::Messages *mMessages; - WarningMode mWarningMode; - bool mIgnoreBaseRecords; + enum WarningMode + { + Mode_Ignore, + Mode_Normal, + Mode_Strict + }; - CSMDoc::Message::Severity getSeverity (Type type); + const CSMDoc::Document& mDocument; + Compiler::Extensions mExtensions; + CSMWorld::ScriptContext mContext; + ESM::RefId mId; + std::string mFile; + CSMDoc::Messages* mMessages; + WarningMode mWarningMode; + bool mIgnoreBaseRecords; - void report (const std::string& message, const Compiler::TokenLoc& loc, Type type) override; - ///< Report error to the user. + CSMDoc::Message::Severity getSeverity(Type type); - void report (const std::string& message, Type type) override; - ///< Report a file related error + void report(const std::string& message, const Compiler::TokenLoc& loc, Type type) override; + ///< Report error to the user. - public: + void report(const std::string& message, Type type) override; + ///< Report a file related error - ScriptCheckStage (const CSMDoc::Document& document); + public: + ScriptCheckStage(const CSMDoc::Document& document); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/search.cpp b/apps/opencs/model/tools/search.cpp index 7005480d114..624cfef82d9 100644 --- a/apps/opencs/model/tools/search.cpp +++ b/apps/opencs/model/tools/search.cpp @@ -1,157 +1,185 @@ #include "search.hpp" -#include +#include +#include +#include + +#include #include +#include +#include +#include + +#include -#include "../doc/messages.hpp" #include "../doc/document.hpp" +#include "../doc/messages.hpp" -#include "../world/idtablebase.hpp" #include "../world/columnbase.hpp" -#include "../world/universalid.hpp" #include "../world/commands.hpp" +#include "../world/idtablebase.hpp" +#include "../world/universalid.hpp" -void CSMTools::Search::searchTextCell (const CSMWorld::IdTableBase *model, - const QModelIndex& index, const CSMWorld::UniversalId& id, bool writable, - CSMDoc::Messages& messages) const +void CSMTools::Search::searchTextCell(const CSMWorld::IdTableBase* model, const QModelIndex& index, + const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const { // using QString here for easier handling of case folding. - - QString search = QString::fromUtf8 (mText.c_str()); - QString text = model->data (index).toString(); + + QString search = QString::fromUtf8(mText.c_str()); + QString text = model->data(index).toString(); int pos = 0; Qt::CaseSensitivity caseSensitivity = mCase ? Qt::CaseSensitive : Qt::CaseInsensitive; - while ((pos = text.indexOf (search, pos, caseSensitivity))!=-1) + while ((pos = text.indexOf(search, pos, caseSensitivity)) != -1) { std::ostringstream hint; - hint - << (writable ? 'R' : 'r') - <<": " - << model->getColumnId (index.column()) - << " " << pos - << " " << search.length(); - - messages.add (id, formatDescription (text, pos, search.length()).toUtf8().data(), hint.str()); + hint << (writable ? 'R' : 'r') << ": " << model->getColumnId(index.column()) << " " << pos << " " + << search.length(); + + messages.add(id, formatDescription(text, pos, search.length()).toUtf8().data(), hint.str()); pos += search.length(); } } -void CSMTools::Search::searchRegExCell (const CSMWorld::IdTableBase *model, - const QModelIndex& index, const CSMWorld::UniversalId& id, bool writable, - CSMDoc::Messages& messages) const +void CSMTools::Search::searchRegExCell(const CSMWorld::IdTableBase* model, const QModelIndex& index, + const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const { - QString text = model->data (index).toString(); + // TODO: verify regular expression before starting a search + if (!mRegExp.isValid()) + return; - int pos = 0; + QString text = model->data(index).toString(); - while ((pos = mRegExp.indexIn (text, pos))!=-1) + QRegularExpressionMatchIterator i = mRegExp.globalMatch(text); + while (i.hasNext()) { - int length = mRegExp.matchedLength(); - + QRegularExpressionMatch match = i.next(); + + int pos = match.capturedStart(); + int length = match.capturedLength(); + std::ostringstream hint; - hint - << (writable ? 'R' : 'r') - <<": " - << model->getColumnId (index.column()) - << " " << pos - << " " << length; - - messages.add (id, formatDescription (text, pos, length).toUtf8().data(), hint.str()); - - pos += length; + hint << (writable ? 'R' : 'r') << ": " << model->getColumnId(index.column()) << " " << pos << " " << length; + + messages.add(id, formatDescription(text, pos, length).toUtf8().data(), hint.str()); } } -void CSMTools::Search::searchRecordStateCell (const CSMWorld::IdTableBase *model, - const QModelIndex& index, const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const +void CSMTools::Search::searchRecordStateCell(const CSMWorld::IdTableBase* model, const QModelIndex& index, + const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const { if (writable) - throw std::logic_error ("Record state can not be modified by search and replace"); - - int data = model->data (index).toInt(); + throw std::logic_error("Record state can not be modified by search and replace"); + + int data = model->data(index).toInt(); - if (data==mValue) + if (data == mValue) { - std::vector> states = - CSMWorld::Columns::getEnums (CSMWorld::Columns::ColumnId_Modification); + std::vector> states + = CSMWorld::Columns::getEnums(CSMWorld::Columns::ColumnId_Modification); const std::string hint = "r: " + std::to_string(model->getColumnId(index.column())); - messages.add (id, states.at(data).second, hint); + messages.add(id, states.at(data).second, hint); } } -QString CSMTools::Search::formatDescription (const QString& description, int pos, int length) const +QString CSMTools::Search::formatDescription(const QString& description, int pos, int length) const { - QString text (description); + QString text(description); // split - QString highlight = flatten (text.mid (pos, length)); - QString before = flatten (mPaddingBefore>=pos ? - text.mid (0, pos) : text.mid (pos-mPaddingBefore, mPaddingBefore)); - QString after = flatten (text.mid (pos+length, mPaddingAfter)); + QString highlight = flatten(text.mid(pos, length)); + QString before = flatten(mPaddingBefore >= pos ? text.mid(0, pos) : text.mid(pos - mPaddingBefore, mPaddingBefore)); + QString after = flatten(text.mid(pos + length, mPaddingAfter)); // compensate for Windows nonsense - text.remove ('\r'); + text.remove('\r'); // join text = before + "" + highlight + "" + after; // improve layout for single line display - text.replace ("\n", "<CR>"); - text.replace ('\t', ' '); - - return text; + text.replace("\n", "<CR>"); + text.replace('\t', ' '); + + return text; } -QString CSMTools::Search::flatten (const QString& text) const +QString CSMTools::Search::flatten(const QString& text) const { - QString flat (text); + QString flat(text); - flat.replace ("&", "&"); - flat.replace ("<", "<"); + flat.replace("&", "&"); + flat.replace("<", "<"); return flat; } -CSMTools::Search::Search() : mType (Type_None), mValue (0), mCase (false), mIdColumn (0), mTypeColumn (0), - mPaddingBefore (10), mPaddingAfter (10) {} +CSMTools::Search::Search() + : mType(Type_None) + , mValue(0) + , mCase(false) + , mIdColumn(0) + , mTypeColumn(0) + , mPaddingBefore(10) + , mPaddingAfter(10) +{ +} -CSMTools::Search::Search (Type type, bool caseSensitive, const std::string& value) -: mType (type), mText (value), mValue (0), mCase (caseSensitive), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) +CSMTools::Search::Search(Type type, bool caseSensitive, const std::string& value) + : mType(type) + , mText(value) + , mValue(0) + , mCase(caseSensitive) + , mIdColumn(0) + , mTypeColumn(0) + , mPaddingBefore(10) + , mPaddingAfter(10) { - if (type!=Type_Text && type!=Type_Id) - throw std::logic_error ("Invalid search parameter (string)"); + if (type != Type_Text && type != Type_Id) + throw std::logic_error("Invalid search parameter (string)"); } -CSMTools::Search::Search (Type type, bool caseSensitive, const QRegExp& value) -: mType (type), mRegExp (value), mValue (0), mCase (caseSensitive), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) +CSMTools::Search::Search(Type type, bool caseSensitive, const QRegularExpression& value) + : mType(type) + , mRegExp(value) + , mValue(0) + , mCase(caseSensitive) + , mIdColumn(0) + , mTypeColumn(0) + , mPaddingBefore(10) + , mPaddingAfter(10) { - mRegExp.setCaseSensitivity(mCase ? Qt::CaseSensitive : Qt::CaseInsensitive); - if (type!=Type_TextRegEx && type!=Type_IdRegEx) - throw std::logic_error ("Invalid search parameter (RegExp)"); + mRegExp.setPatternOptions(mCase ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption); + if (type != Type_TextRegEx && type != Type_IdRegEx) + throw std::logic_error("Invalid search parameter (RegExp)"); } -CSMTools::Search::Search (Type type, bool caseSensitive, int value) -: mType (type), mValue (value), mCase (caseSensitive), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) +CSMTools::Search::Search(Type type, bool caseSensitive, int value) + : mType(type) + , mValue(value) + , mCase(caseSensitive) + , mIdColumn(0) + , mTypeColumn(0) + , mPaddingBefore(10) + , mPaddingAfter(10) { - if (type!=Type_RecordState) - throw std::logic_error ("invalid search parameter (int)"); + if (type != Type_RecordState) + throw std::logic_error("invalid search parameter (int)"); } -void CSMTools::Search::configure (const CSMWorld::IdTableBase *model) +void CSMTools::Search::configure(const CSMWorld::IdTableBase* model) { mColumns.clear(); int columns = model->columnCount(); - for (int i=0; i ( - model->headerData ( - i, Qt::Horizontal, static_cast (CSMWorld::ColumnBase::Role_Display)).toInt()); + CSMWorld::ColumnBase::Display display = static_cast( + model->headerData(i, Qt::Horizontal, static_cast(CSMWorld::ColumnBase::Role_Display)).toInt()); bool consider = false; @@ -160,28 +188,26 @@ void CSMTools::Search::configure (const CSMWorld::IdTableBase *model) case Type_Text: case Type_TextRegEx: - if (CSMWorld::ColumnBase::isText (display) || - CSMWorld::ColumnBase::isScript (display)) + if (CSMWorld::ColumnBase::isText(display) || CSMWorld::ColumnBase::isScript(display)) { consider = true; } break; - + case Type_Id: case Type_IdRegEx: - if (CSMWorld::ColumnBase::isId (display) || - CSMWorld::ColumnBase::isScript (display)) + if (CSMWorld::ColumnBase::isId(display) || CSMWorld::ColumnBase::isScript(display)) { consider = true; } break; - + case Type_RecordState: - if (display==CSMWorld::ColumnBase::Display_RecordState) + if (display == CSMWorld::ColumnBase::Display_RecordState) consider = true; break; @@ -192,45 +218,43 @@ void CSMTools::Search::configure (const CSMWorld::IdTableBase *model) } if (consider) - mColumns.insert (i); + mColumns.insert(i); } - mIdColumn = model->findColumnIndex (CSMWorld::Columns::ColumnId_Id); - mTypeColumn = model->findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); + mIdColumn = model->findColumnIndex(CSMWorld::Columns::ColumnId_Id); + mTypeColumn = model->findColumnIndex(CSMWorld::Columns::ColumnId_RecordType); } -void CSMTools::Search::searchRow (const CSMWorld::IdTableBase *model, int row, - CSMDoc::Messages& messages) const +void CSMTools::Search::searchRow(const CSMWorld::IdTableBase* model, int row, CSMDoc::Messages& messages) const { - for (std::set::const_iterator iter (mColumns.begin()); iter!=mColumns.end(); ++iter) + for (std::set::const_iterator iter(mColumns.begin()); iter != mColumns.end(); ++iter) { - QModelIndex index = model->index (row, *iter); + QModelIndex index = model->index(row, *iter); - CSMWorld::UniversalId::Type type = static_cast ( - model->data (model->index (row, mTypeColumn)).toInt()); - - CSMWorld::UniversalId id ( - type, model->data (model->index (row, mIdColumn)).toString().toUtf8().data()); + CSMWorld::UniversalId::Type type + = static_cast(model->data(model->index(row, mTypeColumn)).toInt()); + + CSMWorld::UniversalId id(type, model->data(model->index(row, mIdColumn)).toString().toUtf8().data()); + + bool writable = model->flags(index) & Qt::ItemIsEditable; - bool writable = model->flags (index) & Qt::ItemIsEditable; - switch (mType) { case Type_Text: case Type_Id: - searchTextCell (model, index, id, writable, messages); + searchTextCell(model, index, id, writable, messages); break; - + case Type_TextRegEx: case Type_IdRegEx: - searchRegExCell (model, index, id, writable, messages); + searchRegExCell(model, index, id, writable, messages); break; - + case Type_RecordState: - searchRecordStateCell (model, index, id, writable, messages); + searchRecordStateCell(model, index, id, writable, messages); break; case Type_None: @@ -240,54 +264,49 @@ void CSMTools::Search::searchRow (const CSMWorld::IdTableBase *model, int row, } } -void CSMTools::Search::setPadding (int before, int after) +void CSMTools::Search::setPadding(int before, int after) { mPaddingBefore = before; mPaddingAfter = after; } -void CSMTools::Search::replace (CSMDoc::Document& document, CSMWorld::IdTableBase *model, - const CSMWorld::UniversalId& id, const std::string& messageHint, - const std::string& replaceText) const +void CSMTools::Search::replace(CSMDoc::Document& document, CSMWorld::IdTableBase* model, + const CSMWorld::UniversalId& id, const std::string& messageHint, const std::string& replaceText) const { - std::istringstream stream (messageHint.c_str()); + std::istringstream stream(messageHint.c_str()); char hint, ignore; int columnId, pos, length; if (stream >> hint >> ignore >> columnId >> pos >> length) { - int column = - model->findColumnIndex (static_cast (columnId)); - - QModelIndex index = model->getModelIndex (id.getId(), column); + int column = model->findColumnIndex(static_cast(columnId)); - std::string text = model->data (index).toString().toUtf8().constData(); + QModelIndex index = model->getModelIndex(id.getId(), column); - std::string before = text.substr (0, pos); - std::string after = text.substr (pos+length); + std::string text = model->data(index).toString().toUtf8().constData(); + + std::string before = text.substr(0, pos); + std::string after = text.substr(pos + length); std::string newText = before + replaceText + after; - - document.getUndoStack().push ( - new CSMWorld::ModifyCommand (*model, index, QString::fromUtf8 (newText.c_str()))); + + document.getUndoStack().push(new CSMWorld::ModifyCommand(*model, index, QString::fromUtf8(newText.c_str()))); } } -bool CSMTools::Search::verify (CSMDoc::Document& document, CSMWorld::IdTableBase *model, - const CSMWorld::UniversalId& id, const std::string& messageHint) const +bool CSMTools::Search::verify(CSMDoc::Document& document, CSMWorld::IdTableBase* model, const CSMWorld::UniversalId& id, + const std::string& messageHint) const { - CSMDoc::Messages messages (CSMDoc::Message::Severity_Info); + CSMDoc::Messages messages(CSMDoc::Message::Severity_Info); + + int row = model->getModelIndex(id.getId(), model->findColumnIndex(CSMWorld::Columns::ColumnId_Id)).row(); - int row = model->getModelIndex (id.getId(), - model->findColumnIndex (CSMWorld::Columns::ColumnId_Id)).row(); - - searchRow (model, row, messages); + searchRow(model, row, messages); - for (CSMDoc::Messages::Iterator iter (messages.begin()); iter!=messages.end(); ++iter) - if (iter->mHint==messageHint) + for (CSMDoc::Messages::Iterator iter(messages.begin()); iter != messages.end(); ++iter) + if (iter->mHint == messageHint) return true; return false; } - diff --git a/apps/opencs/model/tools/search.hpp b/apps/opencs/model/tools/search.hpp index 35cfa640255..6e1b72cd682 100644 --- a/apps/opencs/model/tools/search.hpp +++ b/apps/opencs/model/tools/search.hpp @@ -1,11 +1,11 @@ #ifndef CSM_TOOLS_SEARCH_H #define CSM_TOOLS_SEARCH_H -#include #include +#include -#include #include +#include class QModelIndex; @@ -25,77 +25,71 @@ namespace CSMTools { class Search { - public: - - enum Type - { - Type_Text = 0, - Type_TextRegEx = 1, - Type_Id = 2, - Type_IdRegEx = 3, - Type_RecordState = 4, - Type_None - }; - - private: - - Type mType; - std::string mText; - QRegExp mRegExp; - int mValue; - bool mCase; - std::set mColumns; - int mIdColumn; - int mTypeColumn; - int mPaddingBefore; - int mPaddingAfter; - - void searchTextCell (const CSMWorld::IdTableBase *model, const QModelIndex& index, - const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const; - - void searchRegExCell (const CSMWorld::IdTableBase *model, const QModelIndex& index, - const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const; - - void searchRecordStateCell (const CSMWorld::IdTableBase *model, - const QModelIndex& index, const CSMWorld::UniversalId& id, bool writable, - CSMDoc::Messages& messages) const; - - QString formatDescription (const QString& description, int pos, int length) const; - - QString flatten (const QString& text) const; - - public: - - Search(); - - Search (Type type, bool caseSensitive, const std::string& value); - - Search (Type type, bool caseSensitive, const QRegExp& value); - - Search (Type type, bool caseSensitive, int value); - - // Configure search for the specified model. - void configure (const CSMWorld::IdTableBase *model); - - // Search row in \a model and store results in \a messages. - // - // \attention *this needs to be configured for \a model. - void searchRow (const CSMWorld::IdTableBase *model, int row, - CSMDoc::Messages& messages) const; - - void setPadding (int before, int after); - - // Configuring *this for the model is not necessary when calling this function. - void replace (CSMDoc::Document& document, CSMWorld::IdTableBase *model, - const CSMWorld::UniversalId& id, const std::string& messageHint, - const std::string& replaceText) const; - - // Check if model still matches search results. - bool verify (CSMDoc::Document& document, CSMWorld::IdTableBase *model, - const CSMWorld::UniversalId& id, const std::string& messageHint) const; + public: + enum Type + { + Type_Text = 0, + Type_TextRegEx = 1, + Type_Id = 2, + Type_IdRegEx = 3, + Type_RecordState = 4, + Type_None + }; + + private: + Type mType; + std::string mText; + QRegularExpression mRegExp; + int mValue; + bool mCase; + std::set mColumns; + int mIdColumn; + int mTypeColumn; + int mPaddingBefore; + int mPaddingAfter; + + void searchTextCell(const CSMWorld::IdTableBase* model, const QModelIndex& index, + const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const; + + void searchRegExCell(const CSMWorld::IdTableBase* model, const QModelIndex& index, + const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const; + + void searchRecordStateCell(const CSMWorld::IdTableBase* model, const QModelIndex& index, + const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const; + + QString formatDescription(const QString& description, int pos, int length) const; + + QString flatten(const QString& text) const; + + public: + Search(); + + Search(Type type, bool caseSensitive, const std::string& value); + + Search(Type type, bool caseSensitive, const QRegularExpression& value); + + Search(Type type, bool caseSensitive, int value); + + // Configure search for the specified model. + void configure(const CSMWorld::IdTableBase* model); + + // Search row in \a model and store results in \a messages. + // + // \attention *this needs to be configured for \a model. + void searchRow(const CSMWorld::IdTableBase* model, int row, CSMDoc::Messages& messages) const; + + void setPadding(int before, int after); + + // Configuring *this for the model is not necessary when calling this function. + void replace(CSMDoc::Document& document, CSMWorld::IdTableBase* model, const CSMWorld::UniversalId& id, + const std::string& messageHint, const std::string& replaceText) const; + + // Check if model still matches search results. + bool verify(CSMDoc::Document& document, CSMWorld::IdTableBase* model, const CSMWorld::UniversalId& id, + const std::string& messageHint) const; }; } -Q_DECLARE_METATYPE (CSMTools::Search) +Q_DECLARE_METATYPE(CSMTools::Search) #endif diff --git a/apps/opencs/model/tools/searchoperation.cpp b/apps/opencs/model/tools/searchoperation.cpp index 8fba1cc1efd..d7e5dd419da 100644 --- a/apps/opencs/model/tools/searchoperation.cpp +++ b/apps/opencs/model/tools/searchoperation.cpp @@ -1,38 +1,41 @@ #include "searchoperation.hpp" -#include "../doc/state.hpp" #include "../doc/document.hpp" +#include "../doc/state.hpp" #include "../world/data.hpp" #include "../world/idtablebase.hpp" +#include + +#include +#include +#include +#include + #include "searchstage.hpp" -CSMTools::SearchOperation::SearchOperation (CSMDoc::Document& document) -: CSMDoc::Operation (CSMDoc::State_Searching, false) +CSMTools::SearchOperation::SearchOperation(CSMDoc::Document& document) + : CSMDoc::Operation(CSMDoc::State_Searching, false) { - std::vector types = CSMWorld::UniversalId::listTypes ( - CSMWorld::UniversalId::Class_RecordList | - CSMWorld::UniversalId::Class_ResourceList - ); + std::vector types = CSMWorld::UniversalId::listTypes( + CSMWorld::UniversalId::Class_RecordList | CSMWorld::UniversalId::Class_ResourceList); - for (std::vector::const_iterator iter (types.begin()); - iter!=types.end(); ++iter) - appendStage (new SearchStage (&dynamic_cast ( - *document.getData().getTableModel (*iter)))); + for (std::vector::const_iterator iter(types.begin()); iter != types.end(); ++iter) + appendStage(new SearchStage(&dynamic_cast(*document.getData().getTableModel(*iter)))); - setDefaultSeverity (CSMDoc::Message::Severity_Info); + setDefaultSeverity(CSMDoc::Message::Severity_Info); } -void CSMTools::SearchOperation::configure (const Search& search) +void CSMTools::SearchOperation::configure(const Search& search) { mSearch = search; } -void CSMTools::SearchOperation::appendStage (SearchStage *stage) +void CSMTools::SearchOperation::appendStage(SearchStage* stage) { - CSMDoc::Operation::appendStage (stage); - stage->setOperation (this); + CSMDoc::Operation::appendStage(stage); + stage->setOperation(this); } const CSMTools::Search& CSMTools::SearchOperation::getSearch() const diff --git a/apps/opencs/model/tools/searchoperation.hpp b/apps/opencs/model/tools/searchoperation.hpp index fbbb3889813..87a046d09ea 100644 --- a/apps/opencs/model/tools/searchoperation.hpp +++ b/apps/opencs/model/tools/searchoperation.hpp @@ -13,25 +13,23 @@ namespace CSMDoc namespace CSMTools { class SearchStage; - + class SearchOperation : public CSMDoc::Operation { - Search mSearch; - - public: + Search mSearch; - SearchOperation (CSMDoc::Document& document); + public: + SearchOperation(CSMDoc::Document& document); - /// \attention Do not call this function while a search is running. - void configure (const Search& search); + /// \attention Do not call this function while a search is running. + void configure(const Search& search); - void appendStage (SearchStage *stage); - ///< The ownership of \a stage is transferred to *this. - /// - /// \attention Do no call this function while this Operation is running. + void appendStage(SearchStage* stage); + ///< The ownership of \a stage is transferred to *this. + /// + /// \attention Do no call this function while this Operation is running. - const Search& getSearch() const; - + const Search& getSearch() const; }; } diff --git a/apps/opencs/model/tools/searchstage.cpp b/apps/opencs/model/tools/searchstage.cpp index 7cd3f492433..c52eed39e4c 100644 --- a/apps/opencs/model/tools/searchstage.cpp +++ b/apps/opencs/model/tools/searchstage.cpp @@ -2,28 +2,37 @@ #include "../world/idtablebase.hpp" +#include + #include "searchoperation.hpp" -CSMTools::SearchStage::SearchStage (const CSMWorld::IdTableBase *model) -: mModel (model), mOperation (nullptr) -{} +namespace CSMDoc +{ + class Messages; +} + +CSMTools::SearchStage::SearchStage(const CSMWorld::IdTableBase* model) + : mModel(model) + , mOperation(nullptr) +{ +} int CSMTools::SearchStage::setup() { if (mOperation) mSearch = mOperation->getSearch(); - mSearch.configure (mModel); - + mSearch.configure(mModel); + return mModel->rowCount(); } -void CSMTools::SearchStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::SearchStage::perform(int stage, CSMDoc::Messages& messages) { - mSearch.searchRow (mModel, stage, messages); + mSearch.searchRow(mModel, stage, messages); } -void CSMTools::SearchStage::setOperation (const SearchOperation *operation) +void CSMTools::SearchStage::setOperation(const SearchOperation* operation) { mOperation = operation; } diff --git a/apps/opencs/model/tools/searchstage.hpp b/apps/opencs/model/tools/searchstage.hpp index 86abf31a349..69a5eed4649 100644 --- a/apps/opencs/model/tools/searchstage.hpp +++ b/apps/opencs/model/tools/searchstage.hpp @@ -5,6 +5,11 @@ #include "search.hpp" +namespace CSMDoc +{ + class Messages; +} + namespace CSMWorld { class IdTableBase; @@ -13,24 +18,23 @@ namespace CSMWorld namespace CSMTools { class SearchOperation; - + class SearchStage : public CSMDoc::Stage { - const CSMWorld::IdTableBase *mModel; - Search mSearch; - const SearchOperation *mOperation; - - public: + const CSMWorld::IdTableBase* mModel; + Search mSearch; + const SearchOperation* mOperation; - SearchStage (const CSMWorld::IdTableBase *model); + public: + SearchStage(const CSMWorld::IdTableBase* model); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. - void setOperation (const SearchOperation *operation); + void setOperation(const SearchOperation* operation); }; } diff --git a/apps/opencs/model/tools/skillcheck.cpp b/apps/opencs/model/tools/skillcheck.cpp index c5b38dc1e02..b3300c4fae3 100644 --- a/apps/opencs/model/tools/skillcheck.cpp +++ b/apps/opencs/model/tools/skillcheck.cpp @@ -1,11 +1,20 @@ #include "skillcheck.hpp" +#include + #include "../prefs/state.hpp" #include "../world/universalid.hpp" -CSMTools::SkillCheckStage::SkillCheckStage (const CSMWorld::IdCollection& skills) -: mSkills (skills) +#include +#include +#include +#include +#include +#include + +CSMTools::SkillCheckStage::SkillCheckStage(const CSMWorld::IdCollection& skills) + : mSkills(skills) { mIgnoreBaseRecords = false; } @@ -17,9 +26,9 @@ int CSMTools::SkillCheckStage::setup() return mSkills.getSize(); } -void CSMTools::SkillCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::SkillCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mSkills.getRecord (stage); + const CSMWorld::Record& record = mSkills.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) @@ -27,13 +36,13 @@ void CSMTools::SkillCheckStage::perform (int stage, CSMDoc::Messages& messages) const ESM::Skill& skill = record.get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Skill, skill.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Skill, skill.mId); if (skill.mDescription.empty()) messages.add(id, "Description is missing", "", CSMDoc::Message::Severity_Warning); - for (int i=0; i<4; ++i) - if (skill.mData.mUseValue[i]<0) + for (int i = 0; i < 4; ++i) + if (skill.mData.mUseValue[i] < 0) { messages.add(id, "Use value #" + std::to_string(i) + " is negative", "", CSMDoc::Message::Severity_Error); } diff --git a/apps/opencs/model/tools/skillcheck.hpp b/apps/opencs/model/tools/skillcheck.hpp index b1af887f6c8..517dfc807d3 100644 --- a/apps/opencs/model/tools/skillcheck.hpp +++ b/apps/opencs/model/tools/skillcheck.hpp @@ -1,29 +1,36 @@ #ifndef CSM_TOOLS_SKILLCHECK_H #define CSM_TOOLS_SKILLCHECK_H -#include - #include "../world/idcollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace ESM +{ + struct Skill; +} + namespace CSMTools { /// \brief VerifyStage: make sure that skill records are internally consistent class SkillCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection& mSkills; - bool mIgnoreBaseRecords; - - public: + const CSMWorld::IdCollection& mSkills; + bool mIgnoreBaseRecords; - SkillCheckStage (const CSMWorld::IdCollection& skills); + public: + SkillCheckStage(const CSMWorld::IdCollection& skills); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/soundcheck.cpp b/apps/opencs/model/tools/soundcheck.cpp index c0d893f1a12..b14222523b7 100644 --- a/apps/opencs/model/tools/soundcheck.cpp +++ b/apps/opencs/model/tools/soundcheck.cpp @@ -1,13 +1,24 @@ #include "soundcheck.hpp" +#include + #include "../prefs/state.hpp" #include "../world/universalid.hpp" -CSMTools::SoundCheckStage::SoundCheckStage (const CSMWorld::IdCollection &sounds, - const CSMWorld::Resources &soundfiles) - : mSounds (sounds), - mSoundFiles (soundfiles) +#include +#include +#include +#include +#include +#include + +#include + +CSMTools::SoundCheckStage::SoundCheckStage( + const CSMWorld::IdCollection& sounds, const CSMWorld::Resources& soundfiles) + : mSounds(sounds) + , mSoundFiles(soundfiles) { mIgnoreBaseRecords = false; } @@ -19,9 +30,9 @@ int CSMTools::SoundCheckStage::setup() return mSounds.getSize(); } -void CSMTools::SoundCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::SoundCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mSounds.getRecord (stage); + const CSMWorld::Record& record = mSounds.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) @@ -29,9 +40,9 @@ void CSMTools::SoundCheckStage::perform (int stage, CSMDoc::Messages& messages) const ESM::Sound& sound = record.get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Sound, sound.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Sound, sound.mId); - if (sound.mData.mMinRange>sound.mData.mMaxRange) + if (sound.mData.mMinRange > sound.mData.mMaxRange) { messages.add(id, "Minimum range is larger than maximum range", "", CSMDoc::Message::Severity_Warning); } diff --git a/apps/opencs/model/tools/soundcheck.hpp b/apps/opencs/model/tools/soundcheck.hpp index 80eb9e7f29e..4f540a6fafb 100644 --- a/apps/opencs/model/tools/soundcheck.hpp +++ b/apps/opencs/model/tools/soundcheck.hpp @@ -1,32 +1,42 @@ #ifndef CSM_TOOLS_SOUNDCHECK_H #define CSM_TOOLS_SOUNDCHECK_H -#include - -#include "../world/resources.hpp" #include "../world/idcollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace CSMWorld +{ + class Resources; +} + +namespace ESM +{ + struct Sound; +} + namespace CSMTools { /// \brief VerifyStage: make sure that sound records are internally consistent class SoundCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection& mSounds; - const CSMWorld::Resources &mSoundFiles; - bool mIgnoreBaseRecords; - - public: + const CSMWorld::IdCollection& mSounds; + const CSMWorld::Resources& mSoundFiles; + bool mIgnoreBaseRecords; - SoundCheckStage (const CSMWorld::IdCollection& sounds, - const CSMWorld::Resources &soundfiles); + public: + SoundCheckStage(const CSMWorld::IdCollection& sounds, const CSMWorld::Resources& soundfiles); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/soundgencheck.cpp b/apps/opencs/model/tools/soundgencheck.cpp index ec29e23fef8..9caf007a479 100644 --- a/apps/opencs/model/tools/soundgencheck.cpp +++ b/apps/opencs/model/tools/soundgencheck.cpp @@ -1,16 +1,27 @@ #include "soundgencheck.hpp" +#include + #include "../prefs/state.hpp" #include "../world/refiddata.hpp" #include "../world/universalid.hpp" -CSMTools::SoundGenCheckStage::SoundGenCheckStage(const CSMWorld::IdCollection &soundGens, - const CSMWorld::IdCollection &sounds, - const CSMWorld::RefIdCollection &objects) - : mSoundGens(soundGens), - mSounds(sounds), - mObjects(objects) +#include +#include +#include +#include +#include +#include + +#include +#include + +CSMTools::SoundGenCheckStage::SoundGenCheckStage(const CSMWorld::IdCollection& soundGens, + const CSMWorld::IdCollection& sounds, const CSMWorld::RefIdCollection& objects) + : mSoundGens(soundGens) + , mSounds(sounds) + , mObjects(objects) { mIgnoreBaseRecords = false; } @@ -22,10 +33,10 @@ int CSMTools::SoundGenCheckStage::setup() return mSoundGens.getSize(); } -void CSMTools::SoundGenCheckStage::perform(int stage, CSMDoc::Messages &messages) +void CSMTools::SoundGenCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record &record = mSoundGens.getRecord(stage); - + const CSMWorld::Record& record = mSoundGens.getRecord(stage); + // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; @@ -38,11 +49,13 @@ void CSMTools::SoundGenCheckStage::perform(int stage, CSMDoc::Messages &messages CSMWorld::RefIdData::LocalIndex creatureIndex = mObjects.getDataSet().searchId(soundGen.mCreature); if (creatureIndex.first == -1) { - messages.add(id, "Creature '" + soundGen.mCreature + "' doesn't exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Creature '" + soundGen.mCreature.getRefIdString() + "' doesn't exist", "", + CSMDoc::Message::Severity_Error); } else if (creatureIndex.second != CSMWorld::UniversalId::Type_Creature) { - messages.add(id, "'" + soundGen.mCreature + "' is not a creature", "", CSMDoc::Message::Severity_Error); + messages.add(id, "'" + soundGen.mCreature.getRefIdString() + "' is not a creature", "", + CSMDoc::Message::Severity_Error); } } @@ -52,6 +65,7 @@ void CSMTools::SoundGenCheckStage::perform(int stage, CSMDoc::Messages &messages } else if (mSounds.searchId(soundGen.mSound) == -1) { - messages.add(id, "Sound '" + soundGen.mSound + "' doesn't exist", "", CSMDoc::Message::Severity_Error); + messages.add( + id, "Sound '" + soundGen.mSound.getRefIdString() + "' doesn't exist", "", CSMDoc::Message::Severity_Error); } } diff --git a/apps/opencs/model/tools/soundgencheck.hpp b/apps/opencs/model/tools/soundgencheck.hpp index 306d35ded96..0a23107a6d0 100644 --- a/apps/opencs/model/tools/soundgencheck.hpp +++ b/apps/opencs/model/tools/soundgencheck.hpp @@ -1,30 +1,45 @@ #ifndef CSM_TOOLS_SOUNDGENCHECK_HPP #define CSM_TOOLS_SOUNDGENCHECK_HPP -#include "../world/data.hpp" +#include "../world/idcollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace CSMWorld +{ + class RefIdCollection; +} + +namespace ESM +{ + struct SoundGenerator; + struct Sound; +} + namespace CSMTools { /// \brief VerifyStage: make sure that sound gen records are internally consistent class SoundGenCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection &mSoundGens; - const CSMWorld::IdCollection &mSounds; - const CSMWorld::RefIdCollection &mObjects; - bool mIgnoreBaseRecords; + const CSMWorld::IdCollection& mSoundGens; + const CSMWorld::IdCollection& mSounds; + const CSMWorld::RefIdCollection& mObjects; + bool mIgnoreBaseRecords; - public: - SoundGenCheckStage(const CSMWorld::IdCollection &soundGens, - const CSMWorld::IdCollection &sounds, - const CSMWorld::RefIdCollection &objects); + public: + SoundGenCheckStage(const CSMWorld::IdCollection& soundGens, + const CSMWorld::IdCollection& sounds, const CSMWorld::RefIdCollection& objects); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform(int stage, CSMDoc::Messages &messages) override; - ///< Messages resulting from this stage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this stage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/spellcheck.cpp b/apps/opencs/model/tools/spellcheck.cpp index dc9ce65c0a9..07973bf08bf 100644 --- a/apps/opencs/model/tools/spellcheck.cpp +++ b/apps/opencs/model/tools/spellcheck.cpp @@ -1,16 +1,22 @@ #include "spellcheck.hpp" -#include -#include +#include -#include +#include +#include +#include +#include +#include +#include + +#include #include "../prefs/state.hpp" -#include "../world/universalid.hpp" +#include "effectlistcheck.hpp" -CSMTools::SpellCheckStage::SpellCheckStage (const CSMWorld::IdCollection& spells) -: mSpells (spells) +CSMTools::SpellCheckStage::SpellCheckStage(const CSMWorld::IdCollection& spells) + : mSpells(spells) { mIgnoreBaseRecords = false; } @@ -22,9 +28,9 @@ int CSMTools::SpellCheckStage::setup() return mSpells.getSize(); } -void CSMTools::SpellCheckStage::perform (int stage, CSMDoc::Messages& messages) +void CSMTools::SpellCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mSpells.getRecord (stage); + const CSMWorld::Record& record = mSpells.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) @@ -32,15 +38,15 @@ void CSMTools::SpellCheckStage::perform (int stage, CSMDoc::Messages& messages) const ESM::Spell& spell = record.get(); - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Spell, spell.mId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Spell, spell.mId); // test for empty name if (spell.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); // test for invalid cost values - if (spell.mData.mCost<0) + if (spell.mData.mCost < 0) messages.add(id, "Spell cost is negative", "", CSMDoc::Message::Severity_Error); - /// \todo check data members that can't be edited in the table view + effectListCheck(spell.mEffects.mList, messages, id); } diff --git a/apps/opencs/model/tools/spellcheck.hpp b/apps/opencs/model/tools/spellcheck.hpp index bfc96281071..6d1278689db 100644 --- a/apps/opencs/model/tools/spellcheck.hpp +++ b/apps/opencs/model/tools/spellcheck.hpp @@ -1,29 +1,36 @@ #ifndef CSM_TOOLS_SPELLCHECK_H #define CSM_TOOLS_SPELLCHECK_H -#include - #include "../world/idcollection.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace ESM +{ + struct Spell; +} + namespace CSMTools { /// \brief VerifyStage: make sure that spell records are internally consistent class SpellCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection& mSpells; - bool mIgnoreBaseRecords; - - public: + const CSMWorld::IdCollection& mSpells; + bool mIgnoreBaseRecords; - SpellCheckStage (const CSMWorld::IdCollection& spells); + public: + SpellCheckStage(const CSMWorld::IdCollection& spells); - int setup() override; - ///< \return number of steps + int setup() override; + ///< \return number of steps - void perform (int stage, CSMDoc::Messages& messages) override; - ///< Messages resulting from this tage will be appended to \a messages. + void perform(int stage, CSMDoc::Messages& messages) override; + ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/startscriptcheck.cpp b/apps/opencs/model/tools/startscriptcheck.cpp index deb7d384fca..bf301333813 100644 --- a/apps/opencs/model/tools/startscriptcheck.cpp +++ b/apps/opencs/model/tools/startscriptcheck.cpp @@ -1,31 +1,47 @@ #include "startscriptcheck.hpp" +#include + #include "../prefs/state.hpp" -#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ESM +{ + class Script; +} -CSMTools::StartScriptCheckStage::StartScriptCheckStage ( - const CSMWorld::IdCollection& startScripts, - const CSMWorld::IdCollection& scripts) -: mStartScripts (startScripts), mScripts (scripts) +CSMTools::StartScriptCheckStage::StartScriptCheckStage( + const CSMWorld::IdCollection& startScripts, const CSMWorld::IdCollection& scripts) + : mStartScripts(startScripts) + , mScripts(scripts) { mIgnoreBaseRecords = false; } void CSMTools::StartScriptCheckStage::perform(int stage, CSMDoc::Messages& messages) { - const CSMWorld::Record& record = mStartScripts.getRecord (stage); + const CSMWorld::Record& record = mStartScripts.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; - std::string scriptId = record.get().mId; + const auto& scriptId = record.get().mId; - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_StartScript, scriptId); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_StartScript, scriptId); - if (mScripts.searchId (Misc::StringUtils::lowerCase (scriptId))==-1) - messages.add(id, "Start script " + scriptId + " does not exist", "", CSMDoc::Message::Severity_Error); + if (mScripts.searchId(scriptId) == -1) + messages.add( + id, "Start script " + scriptId.getRefIdString() + " does not exist", "", CSMDoc::Message::Severity_Error); } int CSMTools::StartScriptCheckStage::setup() diff --git a/apps/opencs/model/tools/startscriptcheck.hpp b/apps/opencs/model/tools/startscriptcheck.hpp index a45d3c94379..31e5e451dfd 100644 --- a/apps/opencs/model/tools/startscriptcheck.hpp +++ b/apps/opencs/model/tools/startscriptcheck.hpp @@ -1,28 +1,37 @@ #ifndef CSM_TOOLS_STARTSCRIPTCHECK_H #define CSM_TOOLS_STARTSCRIPTCHECK_H -#include -#include +#include #include "../doc/stage.hpp" #include "../world/idcollection.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace ESM +{ + class Script; + struct StartScript; +} + namespace CSMTools { class StartScriptCheckStage : public CSMDoc::Stage { - const CSMWorld::IdCollection& mStartScripts; - const CSMWorld::IdCollection& mScripts; - bool mIgnoreBaseRecords; - - public: + const CSMWorld::IdCollection& mStartScripts; + const CSMWorld::IdCollection& mScripts; + bool mIgnoreBaseRecords; - StartScriptCheckStage (const CSMWorld::IdCollection& startScripts, - const CSMWorld::IdCollection& scripts); + public: + StartScriptCheckStage(const CSMWorld::IdCollection& startScripts, + const CSMWorld::IdCollection& scripts); - void perform(int stage, CSMDoc::Messages& messages) override; - int setup() override; + void perform(int stage, CSMDoc::Messages& messages) override; + int setup() override; }; } diff --git a/apps/opencs/model/tools/tools.cpp b/apps/opencs/model/tools/tools.cpp index a3d7db497f5..04548ca4cfa 100644 --- a/apps/opencs/model/tools/tools.cpp +++ b/apps/opencs/model/tools/tools.cpp @@ -1,157 +1,165 @@ #include "tools.hpp" -#include +#include +#include +#include +#include +#include -#include "../doc/state.hpp" -#include "../doc/operation.hpp" #include "../doc/document.hpp" -#include "../world/data.hpp" -#include "../world/universalid.hpp" - -#include "reportmodel.hpp" -#include "mandatoryid.hpp" -#include "skillcheck.hpp" +#include "birthsigncheck.hpp" +#include "bodypartcheck.hpp" #include "classcheck.hpp" +#include "enchantmentcheck.hpp" #include "factioncheck.hpp" +#include "gmstcheck.hpp" +#include "journalcheck.hpp" +#include "magiceffectcheck.hpp" +#include "mandatoryid.hpp" +#include "mergeoperation.hpp" +#include "pathgridcheck.hpp" #include "racecheck.hpp" -#include "soundcheck.hpp" -#include "regioncheck.hpp" -#include "birthsigncheck.hpp" -#include "spellcheck.hpp" #include "referenceablecheck.hpp" -#include "scriptcheck.hpp" -#include "bodypartcheck.hpp" #include "referencecheck.hpp" -#include "startscriptcheck.hpp" +#include "regioncheck.hpp" +#include "reportmodel.hpp" +#include "scriptcheck.hpp" #include "searchoperation.hpp" -#include "pathgridcheck.hpp" +#include "skillcheck.hpp" +#include "soundcheck.hpp" #include "soundgencheck.hpp" -#include "magiceffectcheck.hpp" -#include "mergeoperation.hpp" -#include "gmstcheck.hpp" +#include "spellcheck.hpp" +#include "startscriptcheck.hpp" #include "topicinfocheck.hpp" -#include "journalcheck.hpp" -#include "enchantmentcheck.hpp" -CSMDoc::OperationHolder *CSMTools::Tools::get (int type) +#include +#include +#include + +namespace CSMDoc +{ + struct Message; +} + +CSMDoc::OperationHolder* CSMTools::Tools::get(int type) { switch (type) { - case CSMDoc::State_Verifying: return &mVerifier; - case CSMDoc::State_Searching: return &mSearch; - case CSMDoc::State_Merging: return &mMerge; + case CSMDoc::State_Verifying: + return &mVerifier; + case CSMDoc::State_Searching: + return &mSearch; + case CSMDoc::State_Merging: + return &mMerge; } return nullptr; } -const CSMDoc::OperationHolder *CSMTools::Tools::get (int type) const +const CSMDoc::OperationHolder* CSMTools::Tools::get(int type) const { - return const_cast (this)->get (type); + return const_cast(this)->get(type); } -CSMDoc::OperationHolder *CSMTools::Tools::getVerifier() +CSMDoc::OperationHolder* CSMTools::Tools::getVerifier() { if (!mVerifierOperation) { - mVerifierOperation = new CSMDoc::Operation (CSMDoc::State_Verifying, false); + mVerifierOperation = new CSMDoc::Operation(CSMDoc::State_Verifying, false); - connect (&mVerifier, SIGNAL (progress (int, int, int)), this, SIGNAL (progress (int, int, int))); - connect (&mVerifier, SIGNAL (done (int, bool)), this, SIGNAL (done (int, bool))); - connect (&mVerifier, SIGNAL (reportMessage (const CSMDoc::Message&, int)), - this, SLOT (verifierMessage (const CSMDoc::Message&, int))); + connect(&mVerifier, &CSMDoc::OperationHolder::progress, this, &Tools::progress); + connect(&mVerifier, &CSMDoc::OperationHolder::done, this, &Tools::done); + connect(&mVerifier, &CSMDoc::OperationHolder::reportMessage, this, &Tools::verifierMessage); - std::vector mandatoryIds {"Day", "DaysPassed", "GameHour", "Month", "PCRace"}; + std::vector mandatoryRefIds; + { + auto mandatoryIds = { "Day", "DaysPassed", "GameHour", "Month", "PCRace" }; + for (auto& id : mandatoryIds) + mandatoryRefIds.push_back(ESM::RefId::stringRefId(id)); + } - mVerifierOperation->appendStage (new MandatoryIdStage (mData.getGlobals(), - CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Globals), mandatoryIds)); + mVerifierOperation->appendStage(new MandatoryIdStage( + mData.getGlobals(), CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Globals), mandatoryRefIds)); - mVerifierOperation->appendStage (new SkillCheckStage (mData.getSkills())); + mVerifierOperation->appendStage(new SkillCheckStage(mData.getSkills())); - mVerifierOperation->appendStage (new ClassCheckStage (mData.getClasses())); + mVerifierOperation->appendStage(new ClassCheckStage(mData.getClasses())); - mVerifierOperation->appendStage (new FactionCheckStage (mData.getFactions())); + mVerifierOperation->appendStage(new FactionCheckStage(mData.getFactions())); - mVerifierOperation->appendStage (new RaceCheckStage (mData.getRaces())); + mVerifierOperation->appendStage(new RaceCheckStage(mData.getRaces())); - mVerifierOperation->appendStage (new SoundCheckStage (mData.getSounds(), mData.getResources (CSMWorld::UniversalId::Type_SoundsRes))); + mVerifierOperation->appendStage( + new SoundCheckStage(mData.getSounds(), mData.getResources(CSMWorld::UniversalId::Type_SoundsRes))); + + mVerifierOperation->appendStage(new RegionCheckStage(mData.getRegions())); + + mVerifierOperation->appendStage( + new BirthsignCheckStage(mData.getBirthsigns(), mData.getResources(CSMWorld::UniversalId::Type_Textures))); - mVerifierOperation->appendStage (new RegionCheckStage (mData.getRegions())); + mVerifierOperation->appendStage(new SpellCheckStage(mData.getSpells())); - mVerifierOperation->appendStage (new BirthsignCheckStage (mData.getBirthsigns(), mData.getResources (CSMWorld::UniversalId::Type_Textures))); + mVerifierOperation->appendStage( + new ReferenceableCheckStage(mData.getReferenceables().getDataSet(), mData.getRaces(), mData.getClasses(), + mData.getFactions(), mData.getScripts(), mData.getResources(CSMWorld::UniversalId::Type_Meshes), + mData.getResources(CSMWorld::UniversalId::Type_Icons), mData.getBodyParts())); - mVerifierOperation->appendStage (new SpellCheckStage (mData.getSpells())); + mVerifierOperation->appendStage(new ReferenceCheckStage(mData.getReferences(), mData.getReferenceables(), + mData.getCells(), mData.getFactions(), mData.getBodyParts())); - mVerifierOperation->appendStage (new ReferenceableCheckStage (mData.getReferenceables().getDataSet(), mData.getRaces(), mData.getClasses(), mData.getFactions(), mData.getScripts(), - mData.getResources (CSMWorld::UniversalId::Type_Meshes), mData.getResources (CSMWorld::UniversalId::Type_Icons), - mData.getBodyParts())); + mVerifierOperation->appendStage(new ScriptCheckStage(mDocument)); - mVerifierOperation->appendStage (new ReferenceCheckStage(mData.getReferences(), mData.getReferenceables(), mData.getCells(), mData.getFactions())); + mVerifierOperation->appendStage(new StartScriptCheckStage(mData.getStartScripts(), mData.getScripts())); - mVerifierOperation->appendStage (new ScriptCheckStage (mDocument)); + mVerifierOperation->appendStage(new BodyPartCheckStage(mData.getBodyParts(), + mData.getResources(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Meshes)), mData.getRaces())); - mVerifierOperation->appendStage (new StartScriptCheckStage (mData.getStartScripts(), mData.getScripts())); + mVerifierOperation->appendStage(new PathgridCheckStage(mData.getPathgrids())); mVerifierOperation->appendStage( - new BodyPartCheckStage( - mData.getBodyParts(), - mData.getResources( - CSMWorld::UniversalId( CSMWorld::UniversalId::Type_Meshes )), - mData.getRaces() )); - - mVerifierOperation->appendStage (new PathgridCheckStage (mData.getPathgrids())); - - mVerifierOperation->appendStage (new SoundGenCheckStage (mData.getSoundGens(), - mData.getSounds(), - mData.getReferenceables())); - - mVerifierOperation->appendStage (new MagicEffectCheckStage (mData.getMagicEffects(), - mData.getSounds(), - mData.getReferenceables(), - mData.getResources (CSMWorld::UniversalId::Type_Icons), - mData.getResources (CSMWorld::UniversalId::Type_Textures))); - - mVerifierOperation->appendStage (new GmstCheckStage (mData.getGmsts())); - - mVerifierOperation->appendStage (new TopicInfoCheckStage (mData.getTopicInfos(), - mData.getCells(), - mData.getClasses(), - mData.getFactions(), - mData.getGmsts(), - mData.getGlobals(), - mData.getJournals(), - mData.getRaces(), - mData.getRegions(), - mData.getTopics(), - mData.getReferenceables().getDataSet(), - mData.getResources (CSMWorld::UniversalId::Type_SoundsRes))); - - mVerifierOperation->appendStage (new JournalCheckStage(mData.getJournals(), mData.getJournalInfos())); - - mVerifierOperation->appendStage (new EnchantmentCheckStage(mData.getEnchantments())); - - mVerifier.setOperation (mVerifierOperation); + new SoundGenCheckStage(mData.getSoundGens(), mData.getSounds(), mData.getReferenceables())); + + mVerifierOperation->appendStage(new MagicEffectCheckStage(mData.getMagicEffects(), mData.getSounds(), + mData.getReferenceables(), mData.getResources(CSMWorld::UniversalId::Type_Icons), + mData.getResources(CSMWorld::UniversalId::Type_Textures))); + + mVerifierOperation->appendStage(new GmstCheckStage(mData.getGmsts())); + + mVerifierOperation->appendStage(new TopicInfoCheckStage(mData.getTopicInfos(), mData.getCells(), + mData.getClasses(), mData.getFactions(), mData.getGmsts(), mData.getGlobals(), mData.getJournals(), + mData.getRaces(), mData.getRegions(), mData.getTopics(), mData.getReferenceables().getDataSet(), + mData.getResources(CSMWorld::UniversalId::Type_SoundsRes))); + + mVerifierOperation->appendStage(new JournalCheckStage(mData.getJournals(), mData.getJournalInfos())); + + mVerifierOperation->appendStage(new EnchantmentCheckStage(mData.getEnchantments())); + + mVerifier.setOperation(mVerifierOperation); } return &mVerifier; } -CSMTools::Tools::Tools (CSMDoc::Document& document, ToUTF8::FromType encoding) -: mDocument (document), mData (document.getData()), mVerifierOperation (nullptr), - mSearchOperation (nullptr), mMergeOperation (nullptr), mNextReportNumber (0), mEncoding (encoding) +CSMTools::Tools::Tools(CSMDoc::Document& document, ToUTF8::FromType encoding) + : mDocument(document) + , mData(document.getData()) + , mVerifierOperation(nullptr) + , mSearchOperation(nullptr) + , mMergeOperation(nullptr) + , mNextReportNumber(0) + , mEncoding(encoding) { // index 0: load error log - mReports.insert (std::make_pair (mNextReportNumber++, new ReportModel)); - mActiveReports.insert (std::make_pair (CSMDoc::State_Loading, 0)); + mReports.insert(std::make_pair(mNextReportNumber++, new ReportModel)); + mActiveReports.insert(std::make_pair(CSMDoc::State_Loading, 0)); - connect (&mSearch, SIGNAL (progress (int, int, int)), this, SIGNAL (progress (int, int, int))); - connect (&mSearch, SIGNAL (done (int, bool)), this, SIGNAL (done (int, bool))); - connect (&mSearch, SIGNAL (reportMessage (const CSMDoc::Message&, int)), - this, SLOT (verifierMessage (const CSMDoc::Message&, int))); + connect(&mSearch, &CSMDoc::OperationHolder::progress, this, &Tools::progress); + connect(&mSearch, &CSMDoc::OperationHolder::done, this, &Tools::done); + connect(&mSearch, &CSMDoc::OperationHolder::reportMessage, this, &Tools::verifierMessage); - connect (&mMerge, SIGNAL (progress (int, int, int)), this, SIGNAL (progress (int, int, int))); - connect (&mMerge, SIGNAL (done (int, bool)), this, SIGNAL (done (int, bool))); + connect(&mMerge, &CSMDoc::OperationHolder::progress, this, &Tools::progress); + connect(&mMerge, &CSMDoc::OperationHolder::done, this, &Tools::done); // don't need to connect report message, since there are no messages for merge } @@ -175,106 +183,104 @@ CSMTools::Tools::~Tools() delete mMergeOperation; } - for (std::map::iterator iter (mReports.begin()); iter!=mReports.end(); ++iter) + for (std::map::iterator iter(mReports.begin()); iter != mReports.end(); ++iter) delete iter->second; } -CSMWorld::UniversalId CSMTools::Tools::runVerifier (const CSMWorld::UniversalId& reportId) +CSMWorld::UniversalId CSMTools::Tools::runVerifier(const CSMWorld::UniversalId& reportId) { - int reportNumber = reportId.getType()==CSMWorld::UniversalId::Type_VerificationResults ? - reportId.getIndex() : mNextReportNumber++; + int reportNumber = reportId.getType() == CSMWorld::UniversalId::Type_VerificationResults ? reportId.getIndex() + : mNextReportNumber++; - if (mReports.find (reportNumber)==mReports.end()) - mReports.insert (std::make_pair (reportNumber, new ReportModel)); + if (mReports.find(reportNumber) == mReports.end()) + mReports.insert(std::make_pair(reportNumber, new ReportModel)); mActiveReports[CSMDoc::State_Verifying] = reportNumber; getVerifier()->start(); - return CSMWorld::UniversalId (CSMWorld::UniversalId::Type_VerificationResults, reportNumber); + return CSMWorld::UniversalId(CSMWorld::UniversalId::Type_VerificationResults, reportNumber); } CSMWorld::UniversalId CSMTools::Tools::newSearch() { - mReports.insert (std::make_pair (mNextReportNumber++, new ReportModel (true, false))); + mReports.insert(std::make_pair(mNextReportNumber++, new ReportModel(true, false))); - return CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Search, mNextReportNumber-1); + return CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Search, mNextReportNumber - 1); } -void CSMTools::Tools::runSearch (const CSMWorld::UniversalId& searchId, const Search& search) +void CSMTools::Tools::runSearch(const CSMWorld::UniversalId& searchId, const Search& search) { mActiveReports[CSMDoc::State_Searching] = searchId.getIndex(); if (!mSearchOperation) { - mSearchOperation = new SearchOperation (mDocument); - mSearch.setOperation (mSearchOperation); + mSearchOperation = new SearchOperation(mDocument); + mSearch.setOperation(mSearchOperation); } - mSearchOperation->configure (search); + mSearchOperation->configure(search); mSearch.start(); } -void CSMTools::Tools::runMerge (std::unique_ptr target) +void CSMTools::Tools::runMerge(std::unique_ptr target) { // not setting an active report, because merge does not produce messages if (!mMergeOperation) { - mMergeOperation = new MergeOperation (mDocument, mEncoding); - mMerge.setOperation (mMergeOperation); - connect (mMergeOperation, SIGNAL (mergeDone (CSMDoc::Document*)), - this, SIGNAL (mergeDone (CSMDoc::Document*))); + mMergeOperation = new MergeOperation(mDocument, mEncoding); + mMerge.setOperation(mMergeOperation); + connect(mMergeOperation, &MergeOperation::mergeDone, this, &Tools::mergeDone); } target->flagAsDirty(); - mMergeOperation->setTarget (std::move(target)); + mMergeOperation->setTarget(std::move(target)); mMerge.start(); } -void CSMTools::Tools::abortOperation (int type) +void CSMTools::Tools::abortOperation(int type) { - if (CSMDoc::OperationHolder *operation = get (type)) + if (CSMDoc::OperationHolder* operation = get(type)) operation->abort(); } int CSMTools::Tools::getRunningOperations() const { - static const int sOperations[] = - { - CSMDoc::State_Verifying, - CSMDoc::State_Searching, - CSMDoc::State_Merging, - -1 + static const int sOperations[] = { + CSMDoc::State_Verifying, + CSMDoc::State_Searching, + CSMDoc::State_Merging, + -1, }; int result = 0; - for (int i=0; sOperations[i]!=-1; ++i) - if (const CSMDoc::OperationHolder *operation = get (sOperations[i])) + for (int i = 0; sOperations[i] != -1; ++i) + if (const CSMDoc::OperationHolder* operation = get(sOperations[i])) if (operation->isRunning()) result |= sOperations[i]; return result; } -CSMTools::ReportModel *CSMTools::Tools::getReport (const CSMWorld::UniversalId& id) +CSMTools::ReportModel* CSMTools::Tools::getReport(const CSMWorld::UniversalId& id) { - if (id.getType()!=CSMWorld::UniversalId::Type_VerificationResults && - id.getType()!=CSMWorld::UniversalId::Type_LoadErrorLog && - id.getType()!=CSMWorld::UniversalId::Type_Search) - throw std::logic_error ("invalid request for report model: " + id.toString()); + if (id.getType() != CSMWorld::UniversalId::Type_VerificationResults + && id.getType() != CSMWorld::UniversalId::Type_LoadErrorLog + && id.getType() != CSMWorld::UniversalId::Type_Search) + throw std::logic_error("invalid request for report model: " + id.toString()); - return mReports.at (id.getIndex()); + return mReports.at(id.getIndex()); } -void CSMTools::Tools::verifierMessage (const CSMDoc::Message& message, int type) +void CSMTools::Tools::verifierMessage(const CSMDoc::Message& message, int type) { - std::map::iterator iter = mActiveReports.find (type); + std::map::iterator iter = mActiveReports.find(type); - if (iter!=mActiveReports.end()) - mReports[iter->second]->add (message); + if (iter != mActiveReports.end()) + mReports[iter->second]->add(message); } diff --git a/apps/opencs/model/tools/tools.hpp b/apps/opencs/model/tools/tools.hpp index f544c831273..c9e8937c90c 100644 --- a/apps/opencs/model/tools/tools.hpp +++ b/apps/opencs/model/tools/tools.hpp @@ -1,27 +1,27 @@ #ifndef CSM_TOOLS_TOOLS_H #define CSM_TOOLS_TOOLS_H -#include #include +#include + +#include #include #include -#include - #include "../doc/operationholder.hpp" namespace CSMWorld { class Data; - class UniversalId; } namespace CSMDoc { class Operation; class Document; + struct Message; } namespace CSMTools @@ -33,73 +33,72 @@ namespace CSMTools class Tools : public QObject { - Q_OBJECT - - CSMDoc::Document& mDocument; - CSMWorld::Data& mData; - CSMDoc::Operation *mVerifierOperation; - CSMDoc::OperationHolder mVerifier; - SearchOperation *mSearchOperation; - CSMDoc::OperationHolder mSearch; - MergeOperation *mMergeOperation; - CSMDoc::OperationHolder mMerge; - std::map mReports; - int mNextReportNumber; - std::map mActiveReports; // type, report number - ToUTF8::FromType mEncoding; + Q_OBJECT - // not implemented - Tools (const Tools&); - Tools& operator= (const Tools&); + CSMDoc::Document& mDocument; + CSMWorld::Data& mData; + CSMDoc::Operation* mVerifierOperation; + CSMDoc::OperationHolder mVerifier; + SearchOperation* mSearchOperation; + CSMDoc::OperationHolder mSearch; + MergeOperation* mMergeOperation; + CSMDoc::OperationHolder mMerge; + std::map mReports; + int mNextReportNumber; + std::map mActiveReports; // type, report number + ToUTF8::FromType mEncoding; - CSMDoc::OperationHolder *getVerifier(); + // not implemented + Tools(const Tools&); + Tools& operator=(const Tools&); - CSMDoc::OperationHolder *get (int type); - ///< Returns a 0-pointer, if operation hasn't been used yet. + CSMDoc::OperationHolder* getVerifier(); - const CSMDoc::OperationHolder *get (int type) const; - ///< Returns a 0-pointer, if operation hasn't been used yet. + CSMDoc::OperationHolder* get(int type); + ///< Returns a 0-pointer, if operation hasn't been used yet. - public: + const CSMDoc::OperationHolder* get(int type) const; + ///< Returns a 0-pointer, if operation hasn't been used yet. - Tools (CSMDoc::Document& document, ToUTF8::FromType encoding); + public: + Tools(CSMDoc::Document& document, ToUTF8::FromType encoding); - virtual ~Tools(); + virtual ~Tools(); - /// \param reportId If a valid VerificationResults ID, run verifier for the - /// specified report instead of creating a new one. - /// - /// \return ID of the report for this verification run - CSMWorld::UniversalId runVerifier (const CSMWorld::UniversalId& reportId = CSMWorld::UniversalId()); + /// \param reportId If a valid VerificationResults ID, run verifier for the + /// specified report instead of creating a new one. + /// + /// \return ID of the report for this verification run + CSMWorld::UniversalId runVerifier(const CSMWorld::UniversalId& reportId = CSMWorld::UniversalId()); - /// Return ID of the report for this search. - CSMWorld::UniversalId newSearch(); + /// Return ID of the report for this search. + CSMWorld::UniversalId newSearch(); - void runSearch (const CSMWorld::UniversalId& searchId, const Search& search); + void runSearch(const CSMWorld::UniversalId& searchId, const Search& search); - void runMerge (std::unique_ptr target); + void runMerge(std::unique_ptr target); - void abortOperation (int type); - ///< \attention The operation is not aborted immediately. + void abortOperation(int type); + ///< \attention The operation is not aborted immediately. - int getRunningOperations() const; + int getRunningOperations() const; - ReportModel *getReport (const CSMWorld::UniversalId& id); - ///< The ownership of the returned report is not transferred. + ReportModel* getReport(const CSMWorld::UniversalId& id); + ///< The ownership of the returned report is not transferred. - private slots: + private slots: - void verifierMessage (const CSMDoc::Message& message, int type); + void verifierMessage(const CSMDoc::Message& message, int type); - signals: + signals: - void progress (int current, int max, int type); + void progress(int current, int max, int type); - void done (int type, bool failed); + void done(int type, bool failed); - /// \attention When this signal is emitted, *this hands over the ownership of the - /// document. This signal must be handled to avoid a leak. - void mergeDone (CSMDoc::Document *document); + /// \attention When this signal is emitted, *this hands over the ownership of the + /// document. This signal must be handled to avoid a leak. + void mergeDone(CSMDoc::Document* document); }; } diff --git a/apps/opencs/model/tools/topicinfocheck.cpp b/apps/opencs/model/tools/topicinfocheck.cpp index fe9bc991d41..fab90d951ac 100644 --- a/apps/opencs/model/tools/topicinfocheck.cpp +++ b/apps/opencs/model/tools/topicinfocheck.cpp @@ -1,36 +1,54 @@ #include "topicinfocheck.hpp" #include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "../prefs/state.hpp" #include "../world/infoselectwrapper.hpp" -CSMTools::TopicInfoCheckStage::TopicInfoCheckStage( - const CSMWorld::InfoCollection& topicInfos, - const CSMWorld::IdCollection& cells, - const CSMWorld::IdCollection& classes, - const CSMWorld::IdCollection& factions, - const CSMWorld::IdCollection& gmsts, - const CSMWorld::IdCollection& globals, - const CSMWorld::IdCollection& journals, - const CSMWorld::IdCollection& races, - const CSMWorld::IdCollection& regions, - const CSMWorld::IdCollection &topics, - const CSMWorld::RefIdData& referencables, +CSMTools::TopicInfoCheckStage::TopicInfoCheckStage(const CSMWorld::InfoCollection& topicInfos, + const CSMWorld::IdCollection& cells, const CSMWorld::IdCollection& classes, + const CSMWorld::IdCollection& factions, const CSMWorld::IdCollection& gmsts, + const CSMWorld::IdCollection& globals, const CSMWorld::IdCollection& journals, + const CSMWorld::IdCollection& races, const CSMWorld::IdCollection& regions, + const CSMWorld::IdCollection& topics, const CSMWorld::RefIdData& referencables, const CSMWorld::Resources& soundFiles) - : mTopicInfos(topicInfos), - mCells(cells), - mClasses(classes), - mFactions(factions), - mGameSettings(gmsts), - mGlobals(globals), - mJournals(journals), - mRaces(races), - mRegions(regions), - mTopics(topics), - mReferencables(referencables), - mSoundFiles(soundFiles) + : mTopicInfos(topicInfos) + , mCells(cells) + , mClasses(classes) + , mFactions(factions) + , mGameSettings(gmsts) + , mGlobals(globals) + , mJournals(journals) + , mRaces(races) + , mRegions(regions) + , mTopics(topics) + , mReferencables(referencables) + , mSoundFiles(soundFiles) { mIgnoreBaseRecords = false; } @@ -60,7 +78,7 @@ int CSMTools::TopicInfoCheckStage::setup() mCellNames.insert(regionRecord.get().mName); } // Default cell name - int index = mGameSettings.searchId("sDefaultCellname"); + const int index = mGameSettings.searchId(ESM::RefId::stringRefId("sDefaultCellname")); if (index != -1) { const CSMWorld::Record& gmstRecord = mGameSettings.getRecord(index); @@ -112,7 +130,7 @@ void CSMTools::TopicInfoCheckStage::perform(int stage, CSMDoc::Messages& message if (!topicInfo.mCell.empty()) { - verifyCell(topicInfo.mCell, id, messages); + verifyCell(topicInfo.mCell.getRefIdString(), id, messages); } if (!topicInfo.mFaction.empty() && !topicInfo.mFactionLess) @@ -153,35 +171,37 @@ void CSMTools::TopicInfoCheckStage::perform(int stage, CSMDoc::Messages& message // Check info conditions - for (std::vector::const_iterator it = topicInfo.mSelects.begin(); - it != topicInfo.mSelects.end(); ++it) + for (const auto& select : topicInfo.mSelects) { - verifySelectStruct((*it), id, messages); + verifySelectStruct(select, id, messages); } } // Verification functions -bool CSMTools::TopicInfoCheckStage::verifyActor(const std::string& actor, const CSMWorld::UniversalId& id, - CSMDoc::Messages& messages) +bool CSMTools::TopicInfoCheckStage::verifyActor( + const ESM::RefId& actor, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { + const std::string& actorString = actor.getRefIdString(); CSMWorld::RefIdData::LocalIndex index = mReferencables.searchId(actor); if (index.first == -1) { - messages.add(id, "Actor '" + actor + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Actor '" + actorString + "' does not exist", "", CSMDoc::Message::Severity_Error); return false; } else if (mReferencables.getRecord(index).isDeleted()) { - messages.add(id, "Deleted actor '" + actor + "' is being referenced", "", CSMDoc::Message::Severity_Error); + messages.add( + id, "Deleted actor '" + actorString + "' is being referenced", "", CSMDoc::Message::Severity_Error); return false; } else if (index.second != CSMWorld::UniversalId::Type_Npc && index.second != CSMWorld::UniversalId::Type_Creature) { CSMWorld::UniversalId tempId(index.second, actor); std::ostringstream stream; - stream << "Object '" << actor << "' has invalid type " << tempId.getTypeName() << " (an actor must be an NPC or a creature)"; + stream << "Object '" << actor << "' has invalid type " << tempId.getTypeName() + << " (an actor must be an NPC or a creature)"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); return false; } @@ -189,8 +209,8 @@ bool CSMTools::TopicInfoCheckStage::verifyActor(const std::string& actor, const return true; } -bool CSMTools::TopicInfoCheckStage::verifyCell(const std::string& cell, const CSMWorld::UniversalId& id, - CSMDoc::Messages& messages) +bool CSMTools::TopicInfoCheckStage::verifyCell( + const std::string& cell, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { if (mCellNames.find(cell) == mCellNames.end()) { @@ -201,8 +221,8 @@ bool CSMTools::TopicInfoCheckStage::verifyCell(const std::string& cell, const CS return true; } -bool CSMTools::TopicInfoCheckStage::verifyFactionRank(const std::string& factionName, int rank, const CSMWorld::UniversalId& id, - CSMDoc::Messages& messages) +bool CSMTools::TopicInfoCheckStage::verifyFactionRank( + const ESM::RefId& factionName, int rank, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { if (rank < -1) { @@ -214,7 +234,7 @@ bool CSMTools::TopicInfoCheckStage::verifyFactionRank(const std::string& faction int index = mFactions.searchId(factionName); - const ESM::Faction &faction = mFactions.getRecord(index).get(); + const ESM::Faction& faction = mFactions.getRecord(index).get(); int limit = 0; for (; limit < 10; ++limit) @@ -236,19 +256,20 @@ bool CSMTools::TopicInfoCheckStage::verifyFactionRank(const std::string& faction return true; } -bool CSMTools::TopicInfoCheckStage::verifyItem(const std::string& item, const CSMWorld::UniversalId& id, - CSMDoc::Messages& messages) +bool CSMTools::TopicInfoCheckStage::verifyItem( + const ESM::RefId& item, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { + const std::string& idString = item.getRefIdString(); CSMWorld::RefIdData::LocalIndex index = mReferencables.searchId(item); if (index.first == -1) { - messages.add(id, ("Item '" + item + "' does not exist"), "", CSMDoc::Message::Severity_Error); + messages.add(id, ("Item '" + idString + "' does not exist"), "", CSMDoc::Message::Severity_Error); return false; } else if (mReferencables.getRecord(index).isDeleted()) { - messages.add(id, ("Deleted item '" + item + "' is being referenced"), "", CSMDoc::Message::Severity_Error); + messages.add(id, ("Deleted item '" + idString + "' is being referenced"), "", CSMDoc::Message::Severity_Error); return false; } else @@ -274,7 +295,8 @@ bool CSMTools::TopicInfoCheckStage::verifyItem(const std::string& item, const CS { CSMWorld::UniversalId tempId(index.second, item); std::ostringstream stream; - stream << "Object '" << item << "' has invalid type " << tempId.getTypeName() << " (an item can be a potion, an armor piece, a book and so on)"; + stream << "Object '" << item << "' has invalid type " << tempId.getTypeName() + << " (an item can be a potion, an armor piece, a book and so on)"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); return false; } @@ -284,90 +306,72 @@ bool CSMTools::TopicInfoCheckStage::verifyItem(const std::string& item, const CS return true; } -bool CSMTools::TopicInfoCheckStage::verifySelectStruct(const ESM::DialInfo::SelectStruct& select, - const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) +bool CSMTools::TopicInfoCheckStage::verifySelectStruct( + const ESM::DialogueCondition& select, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { CSMWorld::ConstInfoSelectWrapper infoCondition(select); - if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_None) + if (select.mFunction == ESM::DialogueCondition::Function_None) { messages.add(id, "Invalid condition '" + infoCondition.toString() + "'", "", CSMDoc::Message::Severity_Error); return false; } - else if (!infoCondition.variantTypeIsValid()) - { - std::ostringstream stream; - stream << "Value of condition '" << infoCondition.toString() << "' has invalid "; - - switch (select.mValue.getType()) - { - case ESM::VT_None: stream << "None"; break; - case ESM::VT_Short: stream << "Short"; break; - case ESM::VT_Int: stream << "Int"; break; - case ESM::VT_Long: stream << "Long"; break; - case ESM::VT_Float: stream << "Float"; break; - case ESM::VT_String: stream << "String"; break; - default: stream << "unknown"; break; - } - stream << " type"; - - messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); - return false; - } else if (infoCondition.conditionIsAlwaysTrue()) { - messages.add(id, "Condition '" + infoCondition.toString() + "' is always true", "", CSMDoc::Message::Severity_Warning); + messages.add( + id, "Condition '" + infoCondition.toString() + "' is always true", "", CSMDoc::Message::Severity_Warning); return false; } else if (infoCondition.conditionIsNeverTrue()) { - messages.add(id, "Condition '" + infoCondition.toString() + "' is never true", "", CSMDoc::Message::Severity_Warning); + messages.add( + id, "Condition '" + infoCondition.toString() + "' is never true", "", CSMDoc::Message::Severity_Warning); return false; } // Id checks - if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Global && - !verifyId(infoCondition.getVariableName(), mGlobals, id, messages)) + if (select.mFunction == ESM::DialogueCondition::Function_Global + && !verifyId(ESM::RefId::stringRefId(select.mVariable), mGlobals, id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Journal && - !verifyId(infoCondition.getVariableName(), mJournals, id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_Journal + && !verifyId(ESM::RefId::stringRefId(select.mVariable), mJournals, id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Item && - !verifyItem(infoCondition.getVariableName(), id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_Item + && !verifyItem(ESM::RefId::stringRefId(select.mVariable), id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_Dead && - !verifyActor(infoCondition.getVariableName(), id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_Dead + && !verifyActor(ESM::RefId::stringRefId(select.mVariable), id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotId && - !verifyActor(infoCondition.getVariableName(), id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_NotId + && !verifyActor(ESM::RefId::stringRefId(select.mVariable), id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotFaction && - !verifyId(infoCondition.getVariableName(), mFactions, id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_NotFaction + && !verifyId(ESM::RefId::stringRefId(select.mVariable), mFactions, id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotClass && - !verifyId(infoCondition.getVariableName(), mClasses, id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_NotClass + && !verifyId(ESM::RefId::stringRefId(select.mVariable), mClasses, id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotRace && - !verifyId(infoCondition.getVariableName(), mRaces, id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_NotRace + && !verifyId(ESM::RefId::stringRefId(select.mVariable), mRaces, id, messages)) { return false; } - else if (infoCondition.getFunctionName() == CSMWorld::ConstInfoSelectWrapper::Function_NotCell && - !verifyCell(infoCondition.getVariableName(), id, messages)) + else if (select.mFunction == ESM::DialogueCondition::Function_NotCell + && !verifyCell(select.mVariable, id, messages)) { return false; } @@ -375,8 +379,8 @@ bool CSMTools::TopicInfoCheckStage::verifySelectStruct(const ESM::DialInfo::Sele return true; } -bool CSMTools::TopicInfoCheckStage::verifySound(const std::string& sound, const CSMWorld::UniversalId& id, - CSMDoc::Messages& messages) +bool CSMTools::TopicInfoCheckStage::verifySound( + const std::string& sound, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { if (mSoundFiles.searchId(sound) == -1) { @@ -388,19 +392,23 @@ bool CSMTools::TopicInfoCheckStage::verifySound(const std::string& sound, const } template -bool CSMTools::TopicInfoCheckStage::verifyId(const std::string& name, const CSMWorld::IdCollection& collection, +bool CSMTools::TopicInfoCheckStage::verifyId(const ESM::RefId& name, const CSMWorld::IdCollection& collection, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { int index = collection.searchId(name); if (index == -1) { - messages.add(id, T::getRecordType() + " '" + name + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, std::string(T::getRecordType()) + " '" + name.getRefIdString() + "' does not exist", "", + CSMDoc::Message::Severity_Error); return false; } else if (collection.getRecord(index).isDeleted()) { - messages.add(id, "Deleted " + T::getRecordType() + " record '" + name + "' is being referenced", "", CSMDoc::Message::Severity_Error); + messages.add(id, + "Deleted " + std::string(T::getRecordType()) + " record '" + name.getRefIdString() + + "' is being referenced", + "", CSMDoc::Message::Severity_Error); return false; } diff --git a/apps/opencs/model/tools/topicinfocheck.hpp b/apps/opencs/model/tools/topicinfocheck.hpp index b9dbdc15362..3069fbc0fff 100644 --- a/apps/opencs/model/tools/topicinfocheck.hpp +++ b/apps/opencs/model/tools/topicinfocheck.hpp @@ -2,42 +2,53 @@ #define CSM_TOOLS_TOPICINFOCHECK_HPP #include +#include -#include -#include -#include -#include -#include -#include -#include +#include + +#include +#include -#include "../world/cell.hpp" #include "../world/idcollection.hpp" -#include "../world/infocollection.hpp" -#include "../world/refiddata.hpp" -#include "../world/resources.hpp" #include "../doc/stage.hpp" +namespace CSMDoc +{ + class Messages; +} + +namespace CSMWorld +{ + class InfoCollection; + class RefIdData; + class Resources; + struct Cell; +} + +namespace ESM +{ + struct Class; + struct Dialogue; + struct Faction; + struct GameSetting; + struct Global; + struct Race; + struct Region; +} + namespace CSMTools { /// \brief VerifyStage: check topics class TopicInfoCheckStage : public CSMDoc::Stage { public: - - TopicInfoCheckStage( - const CSMWorld::InfoCollection& topicInfos, - const CSMWorld::IdCollection& cells, - const CSMWorld::IdCollection& classes, - const CSMWorld::IdCollection& factions, - const CSMWorld::IdCollection& gmsts, - const CSMWorld::IdCollection& globals, - const CSMWorld::IdCollection& journals, - const CSMWorld::IdCollection& races, - const CSMWorld::IdCollection& regions, - const CSMWorld::IdCollection& topics, - const CSMWorld::RefIdData& referencables, + TopicInfoCheckStage(const CSMWorld::InfoCollection& topicInfos, + const CSMWorld::IdCollection& cells, const CSMWorld::IdCollection& classes, + const CSMWorld::IdCollection& factions, const CSMWorld::IdCollection& gmsts, + const CSMWorld::IdCollection& globals, const CSMWorld::IdCollection& journals, + const CSMWorld::IdCollection& races, const CSMWorld::IdCollection& regions, + const CSMWorld::IdCollection& topics, const CSMWorld::RefIdData& referencables, const CSMWorld::Resources& soundFiles); int setup() override; @@ -47,7 +58,6 @@ namespace CSMTools ///< Messages resulting from this stage will be appended to \a messages private: - const CSMWorld::InfoCollection& mTopicInfos; const CSMWorld::IdCollection& mCells; @@ -63,22 +73,22 @@ namespace CSMTools const CSMWorld::RefIdData& mReferencables; const CSMWorld::Resources& mSoundFiles; - std::set mCellNames; + std::set mCellNames; bool mIgnoreBaseRecords; // These return false when not successful and write an error - bool verifyActor(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); - bool verifyCell(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); - bool verifyFactionRank(const std::string& name, int rank, const CSMWorld::UniversalId& id, - CSMDoc::Messages& messages); - bool verifyItem(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); - bool verifySelectStruct(const ESM::DialInfo::SelectStruct& select, const CSMWorld::UniversalId& id, - CSMDoc::Messages& messages); + bool verifyActor(const ESM::RefId& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + bool verifyCell(const std::string& cell, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + bool verifyFactionRank( + const ESM::RefId& name, int rank, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + bool verifyItem(const ESM::RefId& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); + bool verifySelectStruct( + const ESM::DialogueCondition& select, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); bool verifySound(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); template - bool verifyId(const std::string& name, const CSMWorld::IdCollection& collection, + bool verifyId(const ESM::RefId& name, const CSMWorld::IdCollection& collection, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); }; } diff --git a/apps/opencs/model/world/actoradapter.cpp b/apps/opencs/model/world/actoradapter.cpp index 8558aa9bc9c..acbe6b5d387 100644 --- a/apps/opencs/model/world/actoradapter.cpp +++ b/apps/opencs/model/world/actoradapter.cpp @@ -1,17 +1,33 @@ #include "actoradapter.hpp" -#include -#include -#include -#include -#include -#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include #include "data.hpp" namespace CSMWorld { - const std::string& ActorAdapter::RaceData::getId() const + const ESM::RefId& ActorAdapter::RaceData::getId() const { return mId; } @@ -41,60 +57,66 @@ namespace CSMWorld } } - const std::string& ActorAdapter::RaceData::getFemalePart(ESM::PartReferenceType index) const + const ESM::RefId& ActorAdapter::RaceData::getFemalePart(ESM::PartReferenceType index) const { return mFemaleParts[ESM::getMeshPart(index)]; } - const std::string& ActorAdapter::RaceData::getMalePart(ESM::PartReferenceType index) const + const ESM::RefId& ActorAdapter::RaceData::getMalePart(ESM::PartReferenceType index) const { return mMaleParts[ESM::getMeshPart(index)]; } - bool ActorAdapter::RaceData::hasDependency(const std::string& id) const + const osg::Vec2f& ActorAdapter::RaceData::getGenderWeightHeight(bool isFemale) + { + return isFemale ? mWeightsHeights.mFemaleWeightHeight : mWeightsHeights.mMaleWeightHeight; + } + + bool ActorAdapter::RaceData::hasDependency(const ESM::RefId& id) const { return mDependencies.find(id) != mDependencies.end(); } - void ActorAdapter::RaceData::setFemalePart(ESM::BodyPart::MeshPart index, const std::string& partId) + void ActorAdapter::RaceData::setFemalePart(ESM::BodyPart::MeshPart index, const ESM::RefId& partId) { mFemaleParts[index] = partId; addOtherDependency(partId); } - void ActorAdapter::RaceData::setMalePart(ESM::BodyPart::MeshPart index, const std::string& partId) + void ActorAdapter::RaceData::setMalePart(ESM::BodyPart::MeshPart index, const ESM::RefId& partId) { mMaleParts[index] = partId; addOtherDependency(partId); } - void ActorAdapter::RaceData::addOtherDependency(const std::string& id) + void ActorAdapter::RaceData::addOtherDependency(const ESM::RefId& id) { - if (!id.empty()) mDependencies.emplace(id); + if (!id.empty()) + mDependencies.emplace(id); } - void ActorAdapter::RaceData::reset_data(const std::string& id, bool isBeast) + void ActorAdapter::RaceData::reset_data(const ESM::RefId& id, const WeightsHeights& raceStats, bool isBeast) { mId = id; mIsBeast = isBeast; + mWeightsHeights = raceStats; for (auto& str : mFemaleParts) - str.clear(); + str = ESM::RefId(); for (auto& str : mMaleParts) - str.clear(); + str = ESM::RefId(); mDependencies.clear(); // Mark self as a dependency addOtherDependency(id); } - ActorAdapter::ActorData::ActorData() { mCreature = false; mFemale = false; } - const std::string& ActorAdapter::ActorData::getId() const + const ESM::RefId& ActorAdapter::ActorData::getId() const { return mId; } @@ -114,14 +136,17 @@ namespace CSMWorld if (mCreature || !mSkeletonOverride.empty()) return "meshes\\" + mSkeletonOverride; - bool firstPerson = false; bool beast = mRaceData ? mRaceData->isBeast() : false; - bool werewolf = false; - return SceneUtil::getActorSkeleton(firstPerson, mFemale, beast, werewolf); + if (beast) + return CSMPrefs::get()["Models"]["baseanimkna"].toString(); + else if (mFemale) + return CSMPrefs::get()["Models"]["baseanimfemale"].toString(); + else + return CSMPrefs::get()["Models"]["baseanim"].toString(); } - const std::string ActorAdapter::ActorData::getPart(ESM::PartReferenceType index) const + ESM::RefId ActorAdapter::ActorData::getPart(ESM::PartReferenceType index) const { auto it = mParts.find(index); if (it == mParts.end()) @@ -131,27 +156,30 @@ namespace CSMWorld if (mFemale) { // Note: we should use male parts for females as fallback - const std::string femalePart = mRaceData->getFemalePart(index); - if (!femalePart.empty()) + if (const ESM::RefId femalePart = mRaceData->getFemalePart(index); !femalePart.empty()) return femalePart; } return mRaceData->getMalePart(index); } - return ""; + return {}; } - const std::string& partName = it->second.first; - return partName; + return it->second.first; } - bool ActorAdapter::ActorData::hasDependency(const std::string& id) const + const osg::Vec2f& ActorAdapter::ActorData::getRaceWeightHeight() const + { + return mRaceData->getGenderWeightHeight(isFemale()); + } + + bool ActorAdapter::ActorData::hasDependency(const ESM::RefId& id) const { return mDependencies.find(id) != mDependencies.end(); } - void ActorAdapter::ActorData::setPart(ESM::PartReferenceType index, const std::string& partId, int priority) + void ActorAdapter::ActorData::setPart(ESM::PartReferenceType index, const ESM::RefId& partId, int priority) { auto it = mParts.find(index); if (it != mParts.end()) @@ -164,12 +192,14 @@ namespace CSMWorld addOtherDependency(partId); } - void ActorAdapter::ActorData::addOtherDependency(const std::string& id) + void ActorAdapter::ActorData::addOtherDependency(const ESM::RefId& id) { - if (!id.empty()) mDependencies.emplace(id); + if (!id.empty()) + mDependencies.emplace(id); } - void ActorAdapter::ActorData::reset_data(const std::string& id, const std::string& skeleton, bool isCreature, bool isFemale, RaceDataPtr raceData) + void ActorAdapter::ActorData::reset_data( + const ESM::RefId& id, const std::string& skeleton, bool isCreature, bool isFemale, RaceDataPtr raceData) { mId = id; mCreature = isCreature; @@ -181,10 +211,10 @@ namespace CSMWorld // Mark self and race as a dependency addOtherDependency(id); - if (raceData) addOtherDependency(raceData->getId()); + if (raceData) + addOtherDependency(raceData->getId()); } - ActorAdapter::ActorAdapter(Data& data) : mReferenceables(data.getReferenceables()) , mRaces(data.getRaces()) @@ -192,31 +222,24 @@ namespace CSMWorld { // Setup qt slots and signals QAbstractItemModel* refModel = data.getTableModel(UniversalId::Type_Referenceable); - connect(refModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)), - this, SLOT(handleReferenceablesInserted(const QModelIndex&, int, int))); - connect(refModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), - this, SLOT(handleReferenceableChanged(const QModelIndex&, const QModelIndex&))); - connect(refModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)), - this, SLOT(handleReferenceablesAboutToBeRemoved(const QModelIndex&, int, int))); + connect(refModel, &QAbstractItemModel::rowsInserted, this, &ActorAdapter::handleReferenceablesInserted); + connect(refModel, &QAbstractItemModel::dataChanged, this, &ActorAdapter::handleReferenceableChanged); + connect(refModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, + &ActorAdapter::handleReferenceablesAboutToBeRemoved); QAbstractItemModel* raceModel = data.getTableModel(UniversalId::Type_Race); - connect(raceModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)), - this, SLOT(handleRacesAboutToBeRemoved(const QModelIndex&, int, int))); - connect(raceModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), - this, SLOT(handleRaceChanged(const QModelIndex&, const QModelIndex&))); - connect(raceModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)), - this, SLOT(handleRacesAboutToBeRemoved(const QModelIndex&, int, int))); + connect(raceModel, &QAbstractItemModel::rowsInserted, this, &ActorAdapter::handleRacesAboutToBeRemoved); + connect(raceModel, &QAbstractItemModel::dataChanged, this, &ActorAdapter::handleRaceChanged); + connect(raceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ActorAdapter::handleRacesAboutToBeRemoved); QAbstractItemModel* partModel = data.getTableModel(UniversalId::Type_BodyPart); - connect(partModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)), - this, SLOT(handleBodyPartsInserted(const QModelIndex&, int, int))); - connect(partModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), - this, SLOT(handleBodyPartChanged(const QModelIndex&, const QModelIndex&))); - connect(partModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)), - this, SLOT(handleBodyPartsAboutToBeRemoved(const QModelIndex&, int, int))); + connect(partModel, &QAbstractItemModel::rowsInserted, this, &ActorAdapter::handleBodyPartsInserted); + connect(partModel, &QAbstractItemModel::dataChanged, this, &ActorAdapter::handleBodyPartChanged); + connect( + partModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ActorAdapter::handleBodyPartsAboutToBeRemoved); } - ActorAdapter::ActorDataPtr ActorAdapter::getActorData(const std::string& id) + ActorAdapter::ActorDataPtr ActorAdapter::getActorData(const ESM::RefId& id) { // Return cached actor data if it exists ActorDataPtr data = mCachedActors.get(id); @@ -226,7 +249,7 @@ namespace CSMWorld } // Create the actor data - data.reset(new ActorData()); + data = std::make_shared(); setupActor(id, data); mCachedActors.insert(id, data); return data; @@ -239,7 +262,7 @@ namespace CSMWorld { for (int row = start; row <= end; ++row) { - std::string refId = mReferenceables.getId(row); + auto refId = mReferenceables.getId(row); markDirtyDependency(refId); } } @@ -260,7 +283,7 @@ namespace CSMWorld // Handle each record for (int row = start; row <= end; ++row) { - std::string refId = mReferenceables.getId(row); + auto refId = mReferenceables.getId(row); markDirtyDependency(refId); } @@ -275,7 +298,7 @@ namespace CSMWorld { for (int row = start; row <= end; ++row) { - std::string refId = mReferenceables.getId(row); + auto refId = mReferenceables.getId(row); markDirtyDependency(refId); } } @@ -294,7 +317,7 @@ namespace CSMWorld { for (int row = start; row <= end; ++row) { - std::string raceId = mReferenceables.getId(row); + auto raceId = mReferenceables.getId(row); markDirtyDependency(raceId); } } @@ -314,7 +337,7 @@ namespace CSMWorld for (int row = start; row <= end; ++row) { - std::string raceId = mRaces.getId(row); + auto raceId = mRaces.getId(row); markDirtyDependency(raceId); } @@ -329,7 +352,7 @@ namespace CSMWorld { for (int row = start; row <= end; ++row) { - std::string raceId = mRaces.getId(row); + auto raceId = mRaces.getId(row); markDirtyDependency(raceId); } } @@ -355,7 +378,7 @@ namespace CSMWorld markDirtyDependency(record.get().mRace); } - std::string partId = mBodyParts.getId(row); + auto partId = mBodyParts.getId(row); markDirtyDependency(partId); } } @@ -383,7 +406,7 @@ namespace CSMWorld } // Update entries with a tracked dependency - std::string partId = mBodyParts.getId(row); + auto partId = mBodyParts.getId(row); markDirtyDependency(partId); } @@ -398,7 +421,7 @@ namespace CSMWorld { for (int row = start; row <= end; ++row) { - std::string partId = mBodyParts.getId(row); + auto partId = mBodyParts.getId(row); markDirtyDependency(partId); } } @@ -417,25 +440,21 @@ namespace CSMWorld return index; } - bool ActorAdapter::is1stPersonPart(const std::string& name) const - { - return name.size() >= 4 && name.find(".1st", name.size() - 4) != std::string::npos; - } - - ActorAdapter::RaceDataPtr ActorAdapter::getRaceData(const std::string& id) + ActorAdapter::RaceDataPtr ActorAdapter::getRaceData(const ESM::RefId& id) { // Return cached race data if it exists RaceDataPtr data = mCachedRaces.get(id); - if (data) return data; + if (data) + return data; // Create the race data - data.reset(new RaceData()); + data = std::make_shared(); setupRace(id, data); mCachedRaces.insert(id, data); return data; } - void ActorAdapter::setupActor(const std::string& id, ActorDataPtr data) + void ActorAdapter::setupActor(const ESM::RefId& id, ActorDataPtr data) { int index = mReferenceables.searchId(id); if (index == -1) @@ -460,13 +479,13 @@ namespace CSMWorld if (type == UniversalId::Type_Creature) { // Valid creature record - setupCreature(id, data); + setupCreature(id, std::move(data)); emit actorChanged(id); } else if (type == UniversalId::Type_Npc) { // Valid npc record - setupNpc(id, data); + setupNpc(id, std::move(data)); emit actorChanged(id); } else @@ -477,7 +496,7 @@ namespace CSMWorld } } - void ActorAdapter::setupRace(const std::string& id, RaceDataPtr data) + void ActorAdapter::setupRace(const ESM::RefId& id, RaceDataPtr data) { int index = mRaces.searchId(id); if (index == -1) @@ -496,12 +515,15 @@ namespace CSMWorld } auto& race = raceRecord.get(); - data->reset_data(id, race.mData.mFlags & ESM::Race::Beast); + + WeightsHeights scaleStats = { osg::Vec2f(race.mData.mMaleWeight, race.mData.mMaleHeight), + osg::Vec2f(race.mData.mFemaleWeight, race.mData.mFemaleHeight) }; + + data->reset_data(id, scaleStats, race.mData.mFlags & ESM::Race::Beast); // Setup body parts for (int i = 0; i < mBodyParts.getSize(); ++i) { - std::string partId = mBodyParts.getId(i); auto& partRecord = mBodyParts.getRecord(i); if (partRecord.isDeleted()) @@ -511,24 +533,26 @@ namespace CSMWorld } auto& part = partRecord.get(); - if (part.mRace == id && part.mData.mType == ESM::BodyPart::MT_Skin && !is1stPersonPart(part.mId)) + if (part.mRace == id && part.mData.mType == ESM::BodyPart::MT_Skin && !ESM::isFirstPersonBodyPart(part)) { - auto type = (ESM::BodyPart::MeshPart) part.mData.mPart; + auto type = (ESM::BodyPart::MeshPart)part.mData.mPart; bool female = part.mData.mFlags & ESM::BodyPart::BPF_Female; - if (female) data->setFemalePart(type, part.mId); - else data->setMalePart(type, part.mId); + if (female) + data->setFemalePart(type, part.mId); + else + data->setMalePart(type, part.mId); } } } - void ActorAdapter::setupNpc(const std::string& id, ActorDataPtr data) + void ActorAdapter::setupNpc(const ESM::RefId& id, ActorDataPtr data) { // Common setup, record is known to exist and is not deleted int index = mReferenceables.searchId(id); auto& npc = dynamic_cast&>(mReferenceables.getRecord(index)).get(); RaceDataPtr raceData = getRaceData(npc.mRace); - data->reset_data(id, "", false, !npc.isMale(), raceData); + data->reset_data(id, "", false, !npc.isMale(), std::move(raceData)); // Add head and hair data->setPart(ESM::PRT_Head, npc.mHead, 0); @@ -537,13 +561,14 @@ namespace CSMWorld // Add inventory items for (auto& item : npc.mInventory.mList) { - if (item.mCount <= 0) continue; - std::string itemId = item.mItem; + if (item.mCount <= 0) + continue; + auto itemId = item.mItem; addNpcItem(itemId, data); } } - void ActorAdapter::addNpcItem(const std::string& itemId, ActorDataPtr data) + void ActorAdapter::addNpcItem(const ESM::RefId& itemId, ActorDataPtr data) { int index = mReferenceables.searchId(itemId); if (index == -1) @@ -565,8 +590,8 @@ namespace CSMWorld auto addParts = [&](const std::vector& list, int priority) { for (auto& part : list) { - std::string partId; - auto partType = (ESM::PartReferenceType) part.mPart; + ESM::RefId partId; + auto partType = (ESM::PartReferenceType)part.mPart; if (data->isFemale()) partId = part.mFemale; @@ -577,7 +602,7 @@ namespace CSMWorld // An another vanilla quirk: hide hairs if an item replaces Head part if (partType == ESM::PRT_Head) - data->setPart(ESM::PRT_Hair, "", priority); + data->setPart(ESM::PRT_Hair, ESM::RefId(), priority); } }; @@ -598,15 +623,13 @@ namespace CSMWorld std::vector parts; if (clothing.mData.mType == ESM::Clothing::Robe) { - parts = { - ESM::PRT_Groin, ESM::PRT_Skirt, ESM::PRT_RLeg, ESM::PRT_LLeg, - ESM::PRT_RUpperarm, ESM::PRT_LUpperarm, ESM::PRT_RKnee, ESM::PRT_LKnee, - ESM::PRT_RForearm, ESM::PRT_LForearm, ESM::PRT_Cuirass - }; + parts = { ESM::PRT_Groin, ESM::PRT_Skirt, ESM::PRT_RLeg, ESM::PRT_LLeg, ESM::PRT_RUpperarm, + ESM::PRT_LUpperarm, ESM::PRT_RKnee, ESM::PRT_LKnee, ESM::PRT_RForearm, ESM::PRT_LForearm, + ESM::PRT_Cuirass }; } else if (clothing.mData.mType == ESM::Clothing::Skirt) { - parts = {ESM::PRT_Groin, ESM::PRT_RLeg, ESM::PRT_LLeg}; + parts = { ESM::PRT_Groin, ESM::PRT_RLeg, ESM::PRT_LLeg }; } std::vector reservedList; @@ -626,7 +649,7 @@ namespace CSMWorld } } - void ActorAdapter::setupCreature(const std::string& id, ActorDataPtr data) + void ActorAdapter::setupCreature(const ESM::RefId& id, ActorDataPtr data) { // Record is known to exist and is not deleted int index = mReferenceables.searchId(id); @@ -635,7 +658,7 @@ namespace CSMWorld data->reset_data(id, creature.mModel, true); } - void ActorAdapter::markDirtyDependency(const std::string& dep) + void ActorAdapter::markDirtyDependency(const ESM::RefId& dep) { for (auto raceIt : mCachedRaces) { @@ -657,7 +680,7 @@ namespace CSMWorld RaceDataPtr data = mCachedRaces.get(race); if (data) { - setupRace(race, data); + setupRace(race, std::move(data)); // Race was changed. Need to mark actor dependencies as dirty. // Cannot use markDirtyDependency because that would invalidate // the current iterator. @@ -675,7 +698,7 @@ namespace CSMWorld ActorDataPtr data = mCachedActors.get(actor); if (data) { - setupActor(actor, data); + setupActor(actor, std::move(data)); } } mDirtyActors.clear(); diff --git a/apps/opencs/model/world/actoradapter.hpp b/apps/opencs/model/world/actoradapter.hpp index 912a6bcb387..d9a64cda317 100644 --- a/apps/opencs/model/world/actoradapter.hpp +++ b/apps/opencs/model/world/actoradapter.hpp @@ -3,16 +3,19 @@ #include #include +#include +#include +#include #include +#include -#include #include +#include -#include -#include +#include +#include #include -#include "refidcollection.hpp" #include "idcollection.hpp" namespace ESM @@ -23,6 +26,7 @@ namespace ESM namespace CSMWorld { class Data; + class RefIdCollection; /// Adapts multiple collections to provide the data needed to render /// an npc or creature. @@ -30,14 +34,18 @@ namespace CSMWorld { Q_OBJECT public: - /// A list indexed by ESM::PartReferenceType - using ActorPartList = std::map>; + using ActorPartList = std::map>; /// A list indexed by ESM::BodyPart::MeshPart - using RacePartList = std::array; + using RacePartList = std::array; /// Tracks unique strings - using StringSet = std::unordered_set; + using RefIdSet = std::unordered_set; + struct WeightsHeights + { + osg::Vec2f mMaleWeightHeight; + osg::Vec2f mFemaleWeightHeight; + }; /// Contains base race data shared between actors class RaceData @@ -46,34 +54,38 @@ namespace CSMWorld RaceData(); /// Retrieves the id of the race represented - const std::string& getId() const; + const ESM::RefId& getId() const; /// Checks if it's a beast race bool isBeast() const; /// Checks if a part could exist for the given type bool handlesPart(ESM::PartReferenceType type) const; /// Retrieves the associated body part - const std::string& getFemalePart(ESM::PartReferenceType index) const; + const ESM::RefId& getFemalePart(ESM::PartReferenceType index) const; /// Retrieves the associated body part - const std::string& getMalePart(ESM::PartReferenceType index) const; + const ESM::RefId& getMalePart(ESM::PartReferenceType index) const; + + const osg::Vec2f& getGenderWeightHeight(bool isFemale); /// Checks if the race has a data dependency - bool hasDependency(const std::string& id) const; + bool hasDependency(const ESM::RefId& id) const; /// Sets the associated part if it's empty and marks a dependency - void setFemalePart(ESM::BodyPart::MeshPart partIndex, const std::string& partId); + void setFemalePart(ESM::BodyPart::MeshPart partIndex, const ESM::RefId& partId); /// Sets the associated part if it's empty and marks a dependency - void setMalePart(ESM::BodyPart::MeshPart partIndex, const std::string& partId); + void setMalePart(ESM::BodyPart::MeshPart partIndex, const ESM::RefId& partId); /// Marks an additional dependency - void addOtherDependency(const std::string& id); + void addOtherDependency(const ESM::RefId& id); /// Clears parts and dependencies - void reset_data(const std::string& raceId, bool isBeast=false); + void reset_data(const ESM::RefId& raceId, const WeightsHeights& raceStats = { { 1.f, 1.f }, { 1.f, 1.f } }, + bool isBeast = false); private: bool handles(ESM::PartReferenceType type) const; - std::string mId; + ESM::RefId mId; bool mIsBeast; RacePartList mFemaleParts; RacePartList mMaleParts; - StringSet mDependencies; + WeightsHeights mWeightsHeights; + RefIdSet mDependencies; }; using RaceDataPtr = std::shared_ptr; @@ -85,7 +97,7 @@ namespace CSMWorld ActorData(); /// Retrieves the id of the actor represented - const std::string& getId() const; + const ESM::RefId& getId() const; /// Checks if the actor is a creature bool isCreature() const; /// Checks if the actor is female @@ -93,37 +105,39 @@ namespace CSMWorld /// Returns the skeleton the actor should use for attaching parts to std::string getSkeleton() const; /// Retrieves the associated actor part - const std::string getPart(ESM::PartReferenceType index) const; + ESM::RefId getPart(ESM::PartReferenceType index) const; + + const osg::Vec2f& getRaceWeightHeight() const; /// Checks if the actor has a data dependency - bool hasDependency(const std::string& id) const; + bool hasDependency(const ESM::RefId& id) const; /// Sets the actor part used and marks a dependency - void setPart(ESM::PartReferenceType partIndex, const std::string& partId, int priority); + void setPart(ESM::PartReferenceType partIndex, const ESM::RefId& partId, int priority); /// Marks an additional dependency for the actor - void addOtherDependency(const std::string& id); + void addOtherDependency(const ESM::RefId& id); /// Clears race, parts, and dependencies - void reset_data(const std::string& actorId, const std::string& skeleton="", bool isCreature=false, bool female=true, RaceDataPtr raceData=nullptr); + void reset_data(const ESM::RefId& actorId, const std::string& skeleton = "", bool isCreature = false, + bool female = true, RaceDataPtr raceData = nullptr); private: - std::string mId; + ESM::RefId mId; bool mCreature; bool mFemale; std::string mSkeletonOverride; RaceDataPtr mRaceData; ActorPartList mParts; - StringSet mDependencies; + RefIdSet mDependencies; }; using ActorDataPtr = std::shared_ptr; - ActorAdapter(Data& data); /// Obtains the shared data for a given actor - ActorDataPtr getActorData(const std::string& refId); + ActorDataPtr getActorData(const ESM::RefId& refId); signals: - void actorChanged(const std::string& refId); + void actorChanged(const ESM::RefId& refId); public slots: @@ -143,35 +157,33 @@ namespace CSMWorld void handleBodyPartsRemoved(const QModelIndex&, int, int); private: - ActorAdapter(const ActorAdapter&) = delete; ActorAdapter& operator=(const ActorAdapter&) = delete; QModelIndex getHighestIndex(QModelIndex) const; - bool is1stPersonPart(const std::string& id) const; - RaceDataPtr getRaceData(const std::string& raceId); + RaceDataPtr getRaceData(const ESM::RefId& raceId); - void setupActor(const std::string& id, ActorDataPtr data); - void setupRace(const std::string& id, RaceDataPtr data); + void setupActor(const ESM::RefId& id, ActorDataPtr data); + void setupRace(const ESM::RefId& id, RaceDataPtr data); - void setupNpc(const std::string& id, ActorDataPtr data); - void addNpcItem(const std::string& itemId, ActorDataPtr data); + void setupNpc(const ESM::RefId& id, ActorDataPtr data); + void addNpcItem(const ESM::RefId& itemId, ActorDataPtr data); - void setupCreature(const std::string& id, ActorDataPtr data); + void setupCreature(const ESM::RefId& id, ActorDataPtr data); - void markDirtyDependency(const std::string& dependency); + void markDirtyDependency(const ESM::RefId& dependency); void updateDirty(); RefIdCollection& mReferenceables; IdCollection& mRaces; IdCollection& mBodyParts; - Misc::WeakCache mCachedActors; // Key: referenceable id - Misc::WeakCache mCachedRaces; // Key: race id + Misc::WeakCache mCachedActors; // Key: referenceable id + Misc::WeakCache mCachedRaces; // Key: race id - StringSet mDirtyActors; // Actors that need updating - StringSet mDirtyRaces; // Races that need updating + RefIdSet mDirtyActors; // Actors that need updating + RefIdSet mDirtyRaces; // Races that need updating }; } diff --git a/apps/opencs/model/world/cell.cpp b/apps/opencs/model/world/cell.cpp index 93f3c500d75..edf3b0d40ce 100644 --- a/apps/opencs/model/world/cell.cpp +++ b/apps/opencs/model/world/cell.cpp @@ -2,15 +2,9 @@ #include -void CSMWorld::Cell::load (ESM::ESMReader &esm, bool &isDeleted) +void CSMWorld::Cell::load(ESM::ESMReader& esm, bool& isDeleted) { - ESM::Cell::load (esm, isDeleted, false); + ESM::Cell::load(esm, isDeleted, false); - mId = mName; - if (isExterior()) - { - std::ostringstream stream; - stream << "#" << mData.mX << " " << mData.mY; - mId = stream.str(); - } + mId = ESM::RefId::stringRefId(ESM::Cell::mId.toString()); } diff --git a/apps/opencs/model/world/cell.hpp b/apps/opencs/model/world/cell.hpp index 160610874c2..5d4aefbda20 100644 --- a/apps/opencs/model/world/cell.hpp +++ b/apps/opencs/model/world/cell.hpp @@ -1,10 +1,14 @@ #ifndef CSM_WOLRD_CELL_H #define CSM_WOLRD_CELL_H -#include #include -#include +#include + +namespace ESM +{ + class ESMReader; +} namespace CSMWorld { @@ -14,10 +18,9 @@ namespace CSMWorld /// Exterior cell coordinates are encoded in the cell ID. struct Cell : public ESM::Cell { - std::string mId; - - void load (ESM::ESMReader &esm, bool &isDeleted); + ESM::RefId mId; + void load(ESM::ESMReader& esm, bool& isDeleted); }; } diff --git a/apps/opencs/model/world/cellcoordinates.cpp b/apps/opencs/model/world/cellcoordinates.cpp index af8c26d70a2..bb5c73fc547 100644 --- a/apps/opencs/model/world/cellcoordinates.cpp +++ b/apps/opencs/model/world/cellcoordinates.cpp @@ -1,19 +1,32 @@ #include "cellcoordinates.hpp" #include - -#include #include +#include + +#include -#include +#include +#include #include -CSMWorld::CellCoordinates::CellCoordinates() : mX (0), mY (0) {} +CSMWorld::CellCoordinates::CellCoordinates() + : mX(0) + , mY(0) +{ +} -CSMWorld::CellCoordinates::CellCoordinates (int x, int y) : mX (x), mY (y) {} +CSMWorld::CellCoordinates::CellCoordinates(int x, int y) + : mX(x) + , mY(y) +{ +} -CSMWorld::CellCoordinates::CellCoordinates (const std::pair& coordinates) -: mX (coordinates.first), mY (coordinates.second) {} +CSMWorld::CellCoordinates::CellCoordinates(const std::pair& coordinates) + : mX(coordinates.first) + , mY(coordinates.second) +{ +} int CSMWorld::CellCoordinates::getX() const { @@ -25,30 +38,34 @@ int CSMWorld::CellCoordinates::getY() const return mY; } -CSMWorld::CellCoordinates CSMWorld::CellCoordinates::move (int x, int y) const +CSMWorld::CellCoordinates CSMWorld::CellCoordinates::move(int x, int y) const { - return CellCoordinates (mX + x, mY + y); + return CellCoordinates(mX + x, mY + y); } -std::string CSMWorld::CellCoordinates::getId (const std::string& worldspace) const +std::string CSMWorld::CellCoordinates::getId(const std::string& worldspace) const { // we ignore the worldspace for now, since there is only one (will change in 1.1) return generateId(mX, mY); } -std::string CSMWorld::CellCoordinates::generateId (int x, int y) +std::string CSMWorld::CellCoordinates::generateId(int x, int y) { std::string cellId = "#" + std::to_string(x) + " " + std::to_string(y); return cellId; } -bool CSMWorld::CellCoordinates::isExteriorCell (const std::string& id) +bool CSMWorld::CellCoordinates::isExteriorCell(const std::string& id) +{ + return (!id.empty() && id[0] == '#'); +} + +bool CSMWorld::CellCoordinates::isExteriorCell(const ESM::RefId& id) { - return (!id.empty() && id[0]=='#'); + return id.startsWith("#"); } -std::pair CSMWorld::CellCoordinates::fromId ( - const std::string& id) +std::pair CSMWorld::CellCoordinates::fromId(const std::string& id) { // no worldspace for now, needs to be changed for 1.1 if (isExteriorCell(id)) @@ -56,17 +73,17 @@ std::pair CSMWorld::CellCoordinates::fromId ( int x, y; char ignore; - std::istringstream stream (id); + std::istringstream stream(id); if (stream >> ignore >> x >> y) - return std::make_pair (CellCoordinates (x, y), true); + return std::make_pair(CellCoordinates(x, y), true); } - return std::make_pair (CellCoordinates(), false); + return std::make_pair(CellCoordinates(), false); } -std::pair CSMWorld::CellCoordinates::coordinatesToCellIndex (float x, float y) +std::pair CSMWorld::CellCoordinates::coordinatesToCellIndex(float x, float y) { - return std::make_pair (std::floor (x / Constants::CellSizeInUnits), std::floor (y / Constants::CellSizeInUnits)); + return std::make_pair(std::floor(x / Constants::CellSizeInUnits), std::floor(y / Constants::CellSizeInUnits)); } std::pair CSMWorld::CellCoordinates::toTextureCoords(const osg::Vec3d& worldPos) @@ -108,7 +125,8 @@ float CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(int vertexGlobal) int CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(int vertexGlobal) { - return static_cast(vertexGlobal - std::floor(static_cast(vertexGlobal) / (ESM::Land::LAND_SIZE - 1)) * (ESM::Land::LAND_SIZE - 1)); + return static_cast(vertexGlobal + - std::floor(static_cast(vertexGlobal) / (ESM::Land::LAND_SIZE - 1)) * (ESM::Land::LAND_SIZE - 1)); } std::string CSMWorld::CellCoordinates::textureGlobalToCellId(const std::pair& textureGlobal) @@ -125,28 +143,28 @@ std::string CSMWorld::CellCoordinates::vertexGlobalToCellId(const std::pairright.getX()) + if (left.getX() > right.getX()) return false; - return left.getY() -#include +namespace osg +{ + class Vec3d; +} + +namespace ESM +{ + class RefId; +} namespace CSMWorld { class CellCoordinates { - int mX; - int mY; + int mX; + int mY; - public: + public: + CellCoordinates(); - CellCoordinates(); + CellCoordinates(int x, int y); - CellCoordinates (int x, int y); + CellCoordinates(const std::pair& coordinates); - CellCoordinates (const std::pair& coordinates); + int getX() const; - int getX() const; + int getY() const; - int getY() const; + CellCoordinates move(int x, int y) const; + ///< Return a copy of *this, moved by the given offset. - CellCoordinates move (int x, int y) const; - ///< Return a copy of *this, moved by the given offset. + /// Generate cell id string from x and y coordinates + static std::string generateId(int x, int y); - ///Generate cell id string from x and y coordinates - static std::string generateId (int x, int y); + std::string getId(const std::string& worldspace) const; + ///< Return the ID for the cell at these coordinates. - std::string getId (const std::string& worldspace) const; - ///< Return the ID for the cell at these coordinates. + static bool isExteriorCell(const std::string& id); - static bool isExteriorCell (const std::string& id); + static bool isExteriorCell(const ESM::RefId& id); - /// \return first: CellCoordinates (or 0, 0 if cell does not have coordinates), - /// second: is cell paged? - /// - /// \note The worldspace part of \a id is ignored - static std::pair fromId (const std::string& id); + /// \return first: CellCoordinates (or 0, 0 if cell does not have coordinates), + /// second: is cell paged? + /// + /// \note The worldspace part of \a id is ignored + static std::pair fromId(const std::string& id); - /// \return cell coordinates such that given world coordinates are in it. - static std::pair coordinatesToCellIndex (float x, float y); + /// \return cell coordinates such that given world coordinates are in it. + static std::pair coordinatesToCellIndex(float x, float y); - ///Converts worldspace coordinates to global texture selection, taking in account the texture offset. - static std::pair toTextureCoords(const osg::Vec3d& worldPos); + /// Converts worldspace coordinates to global texture selection, taking in account the texture offset. + static std::pair toTextureCoords(const osg::Vec3d& worldPos); - ///Converts worldspace coordinates to global vertex selection. - static std::pair toVertexCoords(const osg::Vec3d& worldPos); + /// Converts worldspace coordinates to global vertex selection. + static std::pair toVertexCoords(const osg::Vec3d& worldPos); - ///Converts global texture coordinate X to worldspace coordinate, offset by 0.25f. - static float textureGlobalXToWorldCoords(int textureGlobal); + /// Converts global texture coordinate X to worldspace coordinate, offset by 0.25f. + static float textureGlobalXToWorldCoords(int textureGlobal); - ///Converts global texture coordinate Y to worldspace coordinate, offset by 0.25f. - static float textureGlobalYToWorldCoords(int textureGlobal); + /// Converts global texture coordinate Y to worldspace coordinate, offset by 0.25f. + static float textureGlobalYToWorldCoords(int textureGlobal); - ///Converts global vertex coordinate to worldspace coordinate - static float vertexGlobalToWorldCoords(int vertexGlobal); + /// Converts global vertex coordinate to worldspace coordinate + static float vertexGlobalToWorldCoords(int vertexGlobal); - ///Converts global vertex coordinate to local cell's heightmap coordinates - static int vertexGlobalToInCellCoords(int vertexGlobal); + /// Converts global vertex coordinate to local cell's heightmap coordinates + static int vertexGlobalToInCellCoords(int vertexGlobal); - ///Converts global texture coordinates to cell id - static std::string textureGlobalToCellId(const std::pair& textureGlobal); + /// Converts global texture coordinates to cell id + static std::string textureGlobalToCellId(const std::pair& textureGlobal); - ///Converts global vertex coordinates to cell id - static std::string vertexGlobalToCellId(const std::pair& vertexGlobal); + /// Converts global vertex coordinates to cell id + static std::string vertexGlobalToCellId(const std::pair& vertexGlobal); }; - bool operator== (const CellCoordinates& left, const CellCoordinates& right); - bool operator!= (const CellCoordinates& left, const CellCoordinates& right); - bool operator< (const CellCoordinates& left, const CellCoordinates& right); + bool operator==(const CellCoordinates& left, const CellCoordinates& right); + bool operator!=(const CellCoordinates& left, const CellCoordinates& right); + bool operator<(const CellCoordinates& left, const CellCoordinates& right); - std::ostream& operator<< (std::ostream& stream, const CellCoordinates& coordiantes); + std::ostream& operator<<(std::ostream& stream, const CellCoordinates& coordiantes); } -Q_DECLARE_METATYPE (CSMWorld::CellCoordinates) +Q_DECLARE_METATYPE(CSMWorld::CellCoordinates) #endif diff --git a/apps/opencs/model/world/cellselection.cpp b/apps/opencs/model/world/cellselection.cpp index c6988e48800..e6b346be987 100644 --- a/apps/opencs/model/world/cellselection.cpp +++ b/apps/opencs/model/world/cellselection.cpp @@ -1,8 +1,11 @@ #include "cellselection.hpp" +#include "cellcoordinates.hpp" + #include -#include #include +#include +#include CSMWorld::CellSelection::Iterator CSMWorld::CellSelection::begin() const { @@ -14,19 +17,19 @@ CSMWorld::CellSelection::Iterator CSMWorld::CellSelection::end() const return mCells.end(); } -bool CSMWorld::CellSelection::add (const CellCoordinates& coordinates) +bool CSMWorld::CellSelection::add(const CellCoordinates& coordinates) { - return mCells.insert (coordinates).second; + return mCells.insert(coordinates).second; } -void CSMWorld::CellSelection::remove (const CellCoordinates& coordinates) +void CSMWorld::CellSelection::remove(const CellCoordinates& coordinates) { - mCells.erase (coordinates); + mCells.erase(coordinates); } -bool CSMWorld::CellSelection::has (const CellCoordinates& coordinates) const +bool CSMWorld::CellSelection::has(const CellCoordinates& coordinates) const { - return mCells.find (coordinates)!=end(); + return mCells.find(coordinates) != end(); } int CSMWorld::CellSelection::getSize() const @@ -37,12 +40,12 @@ int CSMWorld::CellSelection::getSize() const CSMWorld::CellCoordinates CSMWorld::CellSelection::getCentre() const { if (mCells.empty()) - throw std::logic_error ("call of getCentre on empty cell selection"); + throw std::logic_error("call of getCentre on empty cell selection"); double x = 0; double y = 0; - for (Iterator iter = begin(); iter!=end(); ++iter) + for (Iterator iter = begin(); iter != end(); ++iter) { x += iter->getX(); y += iter->getY(); @@ -54,14 +57,14 @@ CSMWorld::CellCoordinates CSMWorld::CellSelection::getCentre() const Iterator closest = begin(); double distance = std::numeric_limits::max(); - for (Iterator iter (begin()); iter!=end(); ++iter) + for (Iterator iter(begin()); iter != end(); ++iter) { double deltaX = x - iter->getX(); double deltaY = y - iter->getY(); - double delta = std::sqrt (deltaX * deltaX + deltaY * deltaY); + double delta = std::sqrt(deltaX * deltaX + deltaY * deltaY); - if (deltamove (x, y)); + for (Iterator iter = begin(); iter != end(); ++iter) + moved.insert(iter->move(x, y)); - mCells.swap (moved); + mCells.swap(moved); } diff --git a/apps/opencs/model/world/cellselection.hpp b/apps/opencs/model/world/cellselection.hpp index 042416a33f9..5a948aab98c 100644 --- a/apps/opencs/model/world/cellselection.hpp +++ b/apps/opencs/model/world/cellselection.hpp @@ -5,7 +5,7 @@ #include -#include "cellcoordinates.hpp" +#include namespace CSMWorld { @@ -14,44 +14,41 @@ namespace CSMWorld /// \note The CellSelection does not specify the worldspace it applies to. class CellSelection { - public: + public: + typedef std::set Container; + typedef Container::const_iterator Iterator; - typedef std::set Container; - typedef Container::const_iterator Iterator; + private: + Container mCells; - private: + public: + Iterator begin() const; - Container mCells; + Iterator end() const; - public: + bool add(const CellCoordinates& coordinates); + ///< Ignored if the cell specified by \a coordinates is already part of the selection. + /// + /// \return Was a cell added to the collection? - Iterator begin() const; + void remove(const CellCoordinates& coordinates); + ///< ignored if the cell specified by \a coordinates is not part of the selection. - Iterator end() const; + bool has(const CellCoordinates& coordinates) const; + ///< \return Is the cell specified by \a coordinates part of the selection? - bool add (const CellCoordinates& coordinates); - ///< Ignored if the cell specified by \a coordinates is already part of the selection. - /// - /// \return Was a cell added to the collection? + int getSize() const; + ///< Return number of cells. - void remove (const CellCoordinates& coordinates); - ///< ignored if the cell specified by \a coordinates is not part of the selection. + CellCoordinates getCentre() const; + ///< Return the selected cell that is closest to the geometric centre of the selection. + /// + /// \attention This function must not be called on selections that are empty. - bool has (const CellCoordinates& coordinates) const; - ///< \return Is the cell specified by \a coordinates part of the selection? - - int getSize() const; - ///< Return number of cells. - - CellCoordinates getCentre() const; - ///< Return the selected cell that is closest to the geometric centre of the selection. - /// - /// \attention This function must not be called on selections that are empty. - - void move (int x, int y); + void move(int x, int y); }; } -Q_DECLARE_METATYPE (CSMWorld::CellSelection) +Q_DECLARE_METATYPE(CSMWorld::CellSelection) #endif diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 451ef9d0e19..fa42ee0f096 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -1,285 +1,306 @@ #ifndef CSM_WOLRD_COLLECTION_H #define CSM_WOLRD_COLLECTION_H -#include -#include #include #include +#include +#include +#include #include #include -#include +#include +#include +#include #include -#include +#include +#include +#include +#include -#include "columnbase.hpp" #include "collectionbase.hpp" +#include "columnbase.hpp" +#include "columnimp.hpp" +#include "info.hpp" #include "land.hpp" -#include "landtexture.hpp" +#include "record.hpp" #include "ref.hpp" namespace CSMWorld { - /// \brief Access to ID field in records - template - struct IdAccessor + inline std::pair parseInfoRefId(const ESM::RefId& infoId) { - void setId(ESXRecordT& record, const std::string& id) const; - const std::string getId (const ESXRecordT& record) const; - }; + const auto separator = infoId.getRefIdString().find('#'); + if (separator == std::string::npos) + throw std::runtime_error("Invalid info id: " + infoId.getRefIdString()); + const std::string_view view(infoId.getRefIdString()); + return { view.substr(0, separator), view.substr(separator + 1) }; + } + + template + void setRecordId(const decltype(T::mId)& id, T& record) + { + record.mId = id; + } - template - void IdAccessor::setId(ESXRecordT& record, const std::string& id) const + inline void setRecordId(const ESM::RefId& id, Info& record) { record.mId = id; + const auto [topicId, originalId] = parseInfoRefId(id); + record.mTopicId = ESM::RefId::stringRefId(topicId); + record.mOriginalId = ESM::RefId::stringRefId(originalId); } - template - const std::string IdAccessor::getId (const ESXRecordT& record) const + template + auto getRecordId(const T& record) { return record.mId; } - template<> - inline void IdAccessor::setId (Land& record, const std::string& id) const + inline void setRecordId(const ESM::RefId& id, Land& record) { - int x=0, y=0; + int x = 0; + int y = 0; - Land::parseUniqueRecordId(id, x, y); + Land::parseUniqueRecordId(id.getRefIdString(), x, y); record.mX = x; record.mY = y; } - template<> - inline void IdAccessor::setId (LandTexture& record, const std::string& id) const + inline ESM::RefId getRecordId(const Land& record) { - int plugin = 0; - int index = 0; + return ESM::RefId::stringRefId(Land::createUniqueRecordId(record.mX, record.mY)); + } - LandTexture::parseUniqueRecordId(id, plugin, index); - record.mPluginIndex = plugin; - record.mIndex = index; + inline ESM::RefId getRecordId(const ESM::MagicEffect& record) + { + return ESM::RefId::stringRefId(CSMWorld::getStringId(record.mId)); } - template<> - inline const std::string IdAccessor::getId (const Land& record) const + inline void setRecordId(const ESM::RefId& id, ESM::MagicEffect& record) { - return Land::createUniqueRecordId(record.mX, record.mY); + int index = ESM::MagicEffect::indexNameToIndex(id.getRefIdString()); + record.mId = ESM::RefId::index(ESM::REC_MGEF, static_cast(index)); } - template<> - inline const std::string IdAccessor::getId (const LandTexture& record) const + inline void setRecordId(const ESM::RefId& id, ESM::Skill& record) { - return LandTexture::createUniqueRecordId(record.mPluginIndex, record.mIndex); + if (const auto* skillId = id.getIf()) + record.mId = *skillId; + throw std::runtime_error("Invalid skill id: " + id.toDebugString()); } /// \brief Single-type record collection - template > + template class Collection : public CollectionBase { - public: - - typedef ESXRecordT ESXRecord; + public: + typedef ESXRecordT ESXRecord; - private: + private: + std::vector>> mRecords; + std::map mIndex; + std::vector*> mColumns; - std::vector > mRecords; - std::map mIndex; - std::vector *> mColumns; + protected: + const std::vector>>& getRecords() const; - // not implemented - Collection (const Collection&); - Collection& operator= (const Collection&); + void reorderRowsImp(const std::vector& indexOrder); - protected: + bool reorderRowsImp(int baseIndex, const std::vector& newOrder); + ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices + /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). + /// + /// \return Success? - const std::map& getIdMap() const; + int cloneRecordImp(const ESM::RefId& origin, const ESM::RefId& dest, UniversalId::Type type); + ///< Returns the index of the clone. - const std::vector >& getRecords() const; + int touchRecordImp(const ESM::RefId& id); + ///< Returns the index of the record on success, -1 on failure. - bool reorderRowsImp (int baseIndex, const std::vector& newOrder); - ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices - /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). - /// - /// \return Success? + public: + Collection() = default; + Collection(const Collection&) = delete; + Collection& operator=(const Collection&) = delete; - int cloneRecordImp (const std::string& origin, const std::string& dest, - UniversalId::Type type); - ///< Returns the index of the clone. + ~Collection() override; - int touchRecordImp (const std::string& id); - ///< Returns the index of the record on success, -1 on failure. + void add(const ESXRecordT& record); + ///< Add a new record (modified) - public: + int getSize() const override; - Collection(); + ESM::RefId getId(int index) const override; - virtual ~Collection(); + int getIndex(const ESM::RefId& id) const override; - void add (const ESXRecordT& record); - ///< Add a new record (modified) + int getColumns() const override; - int getSize() const override; + QVariant getData(int index, int column) const override; - std::string getId (int index) const override; + void setData(int index, int column, const QVariant& data) override; - int getIndex (const std::string& id) const override; + const ColumnBase& getColumn(int column) const override; - int getColumns() const override; + void merge(); + ///< Merge modified into base. - QVariant getData (int index, int column) const override; + void purge(); + ///< Remove records that are flagged as erased. - void setData (int index, int column, const QVariant& data) override; + void removeRows(int index, int count) override; - const ColumnBase& getColumn (int column) const override; + void appendBlankRecord(const ESM::RefId& id, UniversalId::Type type = UniversalId::Type_None) override; + ///< \param type Will be ignored, unless the collection supports multiple record types - virtual void merge(); - ///< Merge modified into base. + void cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) override; - virtual void purge(); - ///< Remove records that are flagged as erased. + bool touchRecord(const ESM::RefId& id) override; + ///< Change the state of a record from base to modified, if it is not already. + /// \return True if the record was changed. - void removeRows (int index, int count) override; + int searchId(const ESM::RefId& id) const override; + ////< Search record with \a id. + /// \return index of record (if found) or -1 (not found) - void appendBlankRecord (const std::string& id, - UniversalId::Type type = UniversalId::Type_None) override; - ///< \param type Will be ignored, unless the collection supports multiple record types + void replace(int index, std::unique_ptr record) override; + ///< If the record type does not match, an exception is thrown. + /// + /// \attention \a record must not change the ID. - void cloneRecord(const std::string& origin, - const std::string& destination, - const UniversalId::Type type) override; + void appendRecord(std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None) override; + ///< If the record type does not match, an exception is thrown. + ///< \param type Will be ignored, unless the collection supports multiple record types - bool touchRecord(const std::string& id) override; - ///< Change the state of a record from base to modified, if it is not already. - /// \return True if the record was changed. + const Record& getRecord(const ESM::RefId& id) const override; - int searchId (const std::string& id) const override; - ////< Search record with \a id. - /// \return index of record (if found) or -1 (not found) + const Record& getRecord(int index) const override; - void replace (int index, const RecordBase& record) override; - ///< If the record type does not match, an exception is thrown. - /// - /// \attention \a record must not change the ID. + int getAppendIndex(const ESM::RefId& id, UniversalId::Type type = UniversalId::Type_None) const override; + ///< \param type Will be ignored, unless the collection supports multiple record types - void appendRecord (const RecordBase& record, - UniversalId::Type type = UniversalId::Type_None) override; - ///< If the record type does not match, an exception is thrown. - ///< \param type Will be ignored, unless the collection supports multiple record types + std::vector getIds(bool listDeleted = true) const override; + ///< Return a sorted collection of all IDs + /// + /// \param listDeleted include deleted record in the list - const Record& getRecord (const std::string& id) const override; + virtual void insertRecord( + std::unique_ptr record, int index, UniversalId::Type type = UniversalId::Type_None); + ///< Insert record before index. + /// + /// If the record type does not match, an exception is thrown. + /// + /// If the index is invalid either generally (by being out of range) or for the particular + /// record, an exception is thrown. - const Record& getRecord (int index) const override; + bool reorderRows(int baseIndex, const std::vector& newOrder) override; + ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices + /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). + /// + /// \return Success? - int getAppendIndex (const std::string& id, - UniversalId::Type type = UniversalId::Type_None) const override; - ///< \param type Will be ignored, unless the collection supports multiple record types + void addColumn(Column* column); - std::vector getIds (bool listDeleted = true) const override; - ///< Return a sorted collection of all IDs - /// - /// \param listDeleted include deleted record in the list + void setRecord(int index, std::unique_ptr> record); + ///< \attention This function must not change the ID. - virtual void insertRecord (const RecordBase& record, int index, - UniversalId::Type type = UniversalId::Type_None); - ///< Insert record before index. - /// - /// If the record type does not match, an exception is thrown. - /// - /// If the index is invalid either generally (by being out of range) or for the particular - /// record, an exception is thrown. - - bool reorderRows (int baseIndex, const std::vector& newOrder) override; - ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices - /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). - /// - /// \return Success? - - void addColumn (Column *column); - - void setRecord (int index, const Record& record); - ///< \attention This function must not change the ID. - - NestableColumn *getNestableColumn (int column) const; + NestableColumn* getNestableColumn(int column) const; }; - template - const std::map& Collection::getIdMap() const + template + const std::vector>>& Collection::getRecords() const { - return mIndex; + return mRecords; } - template - const std::vector >& Collection::getRecords() const + template + void Collection::reorderRowsImp(const std::vector& indexOrder) { - return mRecords; + assert(indexOrder.size() == mRecords.size()); + assert(std::unordered_set(indexOrder.begin(), indexOrder.end()).size() == indexOrder.size()); + std::vector>> orderedRecords; + for (const int index : indexOrder) + { + mIndex.at(mRecords[index]->get().mId) = static_cast(orderedRecords.size()); + orderedRecords.push_back(std::move(mRecords[index])); + } + mRecords = std::move(orderedRecords); } - template - bool Collection::reorderRowsImp (int baseIndex, - const std::vector& newOrder) + template + bool Collection::reorderRowsImp(int baseIndex, const std::vector& newOrder) { if (!newOrder.empty()) { - int size = static_cast (newOrder.size()); + int size = static_cast(newOrder.size()); // check that all indices are present - std::vector test (newOrder); - std::sort (test.begin(), test.end()); - if (*test.begin()!=0 || *--test.end()!=size-1) + std::vector test(newOrder); + std::sort(test.begin(), test.end()); + if (*test.begin() != 0 || *--test.end() != size - 1) return false; // reorder records - std::vector > buffer (size); + std::vector>> buffer(size); - for (int i=0; isetModified(buffer[newOrder[i]]->get()); } - std::copy (buffer.begin(), buffer.end(), mRecords.begin()+baseIndex); + std::move(buffer.begin(), buffer.end(), mRecords.begin() + baseIndex); // adjust index - for (std::map::iterator iter (mIndex.begin()); iter!=mIndex.end(); - ++iter) - if (iter->second>=baseIndex && iter->secondsecond = newOrder.at (iter->second-baseIndex)+baseIndex; + for (auto& [id, index] : mIndex) + if (index >= baseIndex && index < baseIndex + size) + index = newOrder.at(index - baseIndex) + baseIndex; } return true; } - template - int Collection::cloneRecordImp(const std::string& origin, - const std::string& destination, UniversalId::Type type) + template + int Collection::cloneRecordImp( + const ESM::RefId& origin, const ESM::RefId& destination, UniversalId::Type type) { - Record copy; - copy.mModified = getRecord(origin).get(); - copy.mState = RecordBase::State_ModifiedOnly; - IdAccessorT().setId(copy.get(), destination); + auto copy = std::make_unique>(); + copy->mModified = getRecord(origin).get(); + copy->mState = RecordBase::State_ModifiedOnly; + setRecordId(destination, copy->get()); - if (type == UniversalId::Type_Reference) { - CSMWorld::CellRef* ptr = (CSMWorld::CellRef*) ©.mModified; - ptr->mRefNum.mIndex = 0; + if constexpr (std::is_same_v) + { + if (type == UniversalId::Type_Reference) + { + CSMWorld::CellRef* ptr = (CSMWorld::CellRef*)©->mModified; + ptr->mRefNum.mIndex = 0; + } } - int index = getAppendIndex(destination, type); - insertRecord(copy, getAppendIndex(destination, type)); + if constexpr (std::is_same_v) + { + copy->mModified.mStringId = copy->mModified.mId.getRefIdString(); + } + + const int index = getAppendIndex(destination, type); + insertRecord(std::move(copy), getAppendIndex(destination, type)); return index; } - template - int Collection::touchRecordImp(const std::string& id) + template + int Collection::touchRecordImp(const ESM::RefId& id) { - int index = getIndex(id); - Record& record = mRecords.at(index); + const int index = getIndex(id); + Record& record = *mRecords.at(index); if (record.isDeleted()) - { - throw std::runtime_error("attempt to touch deleted record"); - } + throw std::runtime_error("attempt to touch deleted record from collection of " + + std::string(ESXRecordT::getRecordType()) + ": " + id.toDebugString()); if (!record.isModified()) { @@ -290,176 +311,175 @@ namespace CSMWorld return -1; } - template - void Collection::cloneRecord(const std::string& origin, - const std::string& destination, const UniversalId::Type type) + template + void Collection::cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) { cloneRecordImp(origin, destination, type); } - template<> - inline void Collection >::cloneRecord(const std::string& origin, - const std::string& destination, const UniversalId::Type type) + template <> + inline void Collection::cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) { - int index = cloneRecordImp(origin, destination, type); - mRecords.at(index).get().mPlugin = 0; + const int index = cloneRecordImp(origin, destination, type); + mRecords.at(index)->get().setPlugin(-1); } - template - bool Collection::touchRecord(const std::string& id) + template + bool Collection::touchRecord(const ESM::RefId& id) { return touchRecordImp(id) != -1; } - template<> - inline bool Collection >::touchRecord(const std::string& id) + template <> + inline bool Collection::touchRecord(const ESM::RefId& id) { - int index = touchRecordImp(id); + const int index = touchRecordImp(id); if (index >= 0) { - mRecords.at(index).get().mPlugin = 0; + mRecords.at(index)->get().setPlugin(-1); return true; } return false; } - template - Collection::Collection() - {} - - template - Collection::~Collection() + template + Collection::~Collection() { - for (typename std::vector *>::iterator iter (mColumns.begin()); iter!=mColumns.end(); ++iter) + for (typename std::vector*>::iterator iter(mColumns.begin()); iter != mColumns.end(); ++iter) delete *iter; } - template - void Collection::add (const ESXRecordT& record) + template + void Collection::add(const ESXRecordT& record) { - std::string id = Misc::StringUtils::lowerCase (IdAccessorT().getId (record)); + const ESM::RefId id = getRecordId(record); - std::map::iterator iter = mIndex.find (id); + auto iter = mIndex.find(id); - if (iter==mIndex.end()) + if (iter == mIndex.end()) { - Record record2; - record2.mState = Record::State_ModifiedOnly; - record2.mModified = record; + auto record2 = std::make_unique>(); + record2->mState = Record::State_ModifiedOnly; + record2->mModified = record; - insertRecord (record2, getAppendIndex (id)); + insertRecord(std::move(record2), getAppendIndex(id)); } else { - mRecords[iter->second].setModified (record); + mRecords[iter->second]->setModified(record); } } - template - int Collection::getSize() const + template + int Collection::getSize() const { return mRecords.size(); } - template - std::string Collection::getId (int index) const + template + ESM::RefId Collection::getId(int index) const { - return IdAccessorT().getId (mRecords.at (index).get()); + return getRecordId(mRecords.at(index)->get()); } - template - int Collection::getIndex (const std::string& id) const + template + int Collection::getIndex(const ESM::RefId& id) const { - int index = searchId (id); + int index = searchId(id); - if (index==-1) - throw std::runtime_error ("invalid ID: " + id); + if (index == -1) + throw std::runtime_error("ID is not found in collection of " + std::string(ESXRecordT::getRecordType()) + + " records: " + id.getRefIdString()); return index; } - template - int Collection::getColumns() const + template + int Collection::getColumns() const { return mColumns.size(); } - template - QVariant Collection::getData (int index, int column) const + template + QVariant Collection::getData(int index, int column) const { - return mColumns.at (column)->get (mRecords.at (index)); + return mColumns.at(column)->get(*mRecords.at(index)); } - template - void Collection::setData (int index, int column, const QVariant& data) + template + void Collection::setData(int index, int column, const QVariant& data) { - return mColumns.at (column)->set (mRecords.at (index), data); + return mColumns.at(column)->set(*mRecords.at(index), data); } - template - const ColumnBase& Collection::getColumn (int column) const + template + const ColumnBase& Collection::getColumn(int column) const { - return *mColumns.at (column); + return *mColumns.at(column); } - template - NestableColumn *Collection::getNestableColumn (int column) const + template + NestableColumn* Collection::getNestableColumn(int column) const { if (column < 0 || column >= static_cast(mColumns.size())) - throw std::runtime_error("column index out of range"); + throw std::runtime_error( + "column index out of range [0, " + std::to_string(mColumns.size()) + "): " + std::to_string(column)); - return mColumns.at (column); + return mColumns.at(column); } - template - void Collection::addColumn (Column *column) + template + void Collection::addColumn(Column* column) { - mColumns.push_back (column); + mColumns.push_back(column); } - template - void Collection::merge() + template + void Collection::merge() { - for (typename std::vector >::iterator iter (mRecords.begin()); iter!=mRecords.end(); ++iter) - iter->merge(); + for (typename std::vector>>::iterator iter(mRecords.begin()); + iter != mRecords.end(); ++iter) + (*iter)->merge(); purge(); } - template - void Collection::purge() + template + void Collection::purge() { int i = 0; - while (i (mRecords.size())) + while (i < static_cast(mRecords.size())) { - if (mRecords[i].isErased()) - removeRows (i, 1); + if (mRecords[i]->isErased()) + removeRows(i, 1); else ++i; } } - template - void Collection::removeRows (int index, int count) + template + void Collection::removeRows(int index, int count) { - mRecords.erase (mRecords.begin()+index, mRecords.begin()+index+count); + mRecords.erase(mRecords.begin() + index, mRecords.begin() + index + count); - typename std::map::iterator iter = mIndex.begin(); + auto iter = mIndex.begin(); - while (iter!=mIndex.end()) + while (iter != mIndex.end()) { - if (iter->second>=index) + if (iter->second >= index) { - if (iter->second>=index+count) + if (iter->second >= index + count) { iter->second -= count; ++iter; } else { - mIndex.erase (iter++); + iter = mIndex.erase(iter); } } else @@ -467,119 +487,121 @@ namespace CSMWorld } } - template - void Collection::appendBlankRecord (const std::string& id, - UniversalId::Type type) + template + void Collection::appendBlankRecord(const ESM::RefId& id, UniversalId::Type type) { ESXRecordT record; - IdAccessorT().setId(record, id); + setRecordId(id, record); record.blank(); - Record record2; - record2.mState = Record::State_ModifiedOnly; - record2.mModified = record; + if constexpr (std::is_same_v) + { + record.mStringId = record.mId.getRefIdString(); + } + + auto record2 = std::make_unique>(); + record2->mState = Record::State_ModifiedOnly; + record2->mModified = std::move(record); - insertRecord (record2, getAppendIndex (id, type), type); + insertRecord(std::move(record2), getAppendIndex(id, type), type); } - template - int Collection::searchId (const std::string& id) const + template + int Collection::searchId(const ESM::RefId& id) const { - std::string id2 = Misc::StringUtils::lowerCase(id); + const auto iter = mIndex.find(id); - std::map::const_iterator iter = mIndex.find (id2); - - if (iter==mIndex.end()) + if (iter == mIndex.end()) return -1; return iter->second; } - template - void Collection::replace (int index, const RecordBase& record) + template + void Collection::replace(int index, std::unique_ptr record) { - mRecords.at (index) = dynamic_cast&> (record); + std::unique_ptr> tmp(static_cast*>(record.release())); + mRecords.at(index) = std::move(tmp); } - template - void Collection::appendRecord (const RecordBase& record, - UniversalId::Type type) + template + void Collection::appendRecord(std::unique_ptr record, UniversalId::Type type) { - insertRecord (record, - getAppendIndex (IdAccessorT().getId ( - dynamic_cast&> (record).get()), type), type); + int index = getAppendIndex(getRecordId(static_cast*>(record.get())->get()), type); + insertRecord(std::move(record), index, type); } - template - int Collection::getAppendIndex (const std::string& id, - UniversalId::Type type) const + template + int Collection::getAppendIndex(const ESM::RefId& id, UniversalId::Type type) const { - return static_cast (mRecords.size()); + return static_cast(mRecords.size()); } - template - std::vector Collection::getIds (bool listDeleted) const + template + std::vector Collection::getIds(bool listDeleted) const { - std::vector ids; + std::vector ids; - for (typename std::map::const_iterator iter = mIndex.begin(); - iter!=mIndex.end(); ++iter) + for (auto iter = mIndex.begin(); iter != mIndex.end(); ++iter) { - if (listDeleted || !mRecords[iter->second].isDeleted()) - ids.push_back (IdAccessorT().getId (mRecords[iter->second].get())); + if (listDeleted || !mRecords[iter->second]->isDeleted()) + ids.push_back(getRecordId(mRecords[iter->second]->get())); } return ids; } - template - const Record& Collection::getRecord (const std::string& id) const + template + const Record& Collection::getRecord(const ESM::RefId& id) const { - int index = getIndex (id); - return mRecords.at (index); + int index = getIndex(id); + return *mRecords.at(index); } - template - const Record& Collection::getRecord (int index) const + template + const Record& Collection::getRecord(int index) const { - return mRecords.at (index); + return *mRecords.at(index); } - template - void Collection::insertRecord (const RecordBase& record, int index, - UniversalId::Type type) + template + void Collection::insertRecord(std::unique_ptr record, int index, UniversalId::Type type) { - if (index<0 || index>static_cast (mRecords.size())) - throw std::runtime_error ("index out of range"); + int size = static_cast(mRecords.size()); + if (index < 0 || index > size) + throw std::runtime_error("index out of range"); - const Record& record2 = dynamic_cast&> (record); + std::unique_ptr> record2(static_cast*>(record.release())); + ESM::RefId id = getRecordId(record2->get()); - mRecords.insert (mRecords.begin()+index, record2); + if (index == size) + mRecords.push_back(std::move(record2)); + else + mRecords.insert(mRecords.begin() + index, std::move(record2)); - if (index (mRecords.size())-1) + if (index < size - 1) { - for (std::map::iterator iter (mIndex.begin()); iter!=mIndex.end(); - ++iter) - if (iter->second>=index) - ++(iter->second); + for (auto& [key, value] : mIndex) + { + if (value >= index) + ++value; + } } - mIndex.insert (std::make_pair (Misc::StringUtils::lowerCase (IdAccessorT().getId ( - record2.get())), index)); + mIndex.insert(std::make_pair(id, index)); } - template - void Collection::setRecord (int index, const Record& record) + template + void Collection::setRecord(int index, std::unique_ptr> record) { - if (Misc::StringUtils::lowerCase (IdAccessorT().getId (mRecords.at (index).get()))!= - Misc::StringUtils::lowerCase (IdAccessorT().getId (record.get()))) - throw std::runtime_error ("attempt to change the ID of a record"); + if (getRecordId(mRecords.at(index)->get()) != getRecordId(record->get())) + throw std::runtime_error("attempt to change the ID of a record"); - mRecords.at (index) = record; + mRecords.at(index) = std::move(record); } - template - bool Collection::reorderRows (int baseIndex, const std::vector& newOrder) + template + bool Collection::reorderRows(int baseIndex, const std::vector& newOrder) { return false; } diff --git a/apps/opencs/model/world/collectionbase.cpp b/apps/opencs/model/world/collectionbase.cpp index 6134dc17271..c71bb0afc31 100644 --- a/apps/opencs/model/world/collectionbase.cpp +++ b/apps/opencs/model/world/collectionbase.cpp @@ -2,29 +2,33 @@ #include -#include "columnbase.hpp" +#include +#include -CSMWorld::CollectionBase::CollectionBase() {} +#include "columnbase.hpp" -CSMWorld::CollectionBase::~CollectionBase() {} +int CSMWorld::CollectionBase::getInsertIndex(const ESM::RefId& id, UniversalId::Type type, RecordBase* record) const +{ + return getAppendIndex(id, type); +} -int CSMWorld::CollectionBase::searchColumnIndex (Columns::ColumnId id) const +int CSMWorld::CollectionBase::searchColumnIndex(Columns::ColumnId id) const { int columns = getColumns(); - for (int i=0; i #include +#include #include -#include "universalid.hpp" #include "columns.hpp" +#include "universalid.hpp" class QVariant; +namespace ESM +{ + class RefId; +} + namespace CSMWorld { struct ColumnBase; @@ -22,89 +29,87 @@ namespace CSMWorld /// manually. class CollectionBase { - // not implemented - CollectionBase (const CollectionBase&); - CollectionBase& operator= (const CollectionBase&); - - public: - - CollectionBase(); + public: + CollectionBase() = default; + CollectionBase(const CollectionBase&) = delete; + CollectionBase& operator=(const CollectionBase&) = delete; + virtual ~CollectionBase() = default; - virtual ~CollectionBase(); + virtual int getSize() const = 0; - virtual int getSize() const = 0; + virtual ESM::RefId getId(int index) const = 0; - virtual std::string getId (int index) const = 0; + virtual int getIndex(const ESM::RefId& id) const = 0; - virtual int getIndex (const std::string& id) const = 0; + virtual int getColumns() const = 0; - virtual int getColumns() const = 0; + virtual const ColumnBase& getColumn(int column) const = 0; - virtual const ColumnBase& getColumn (int column) const = 0; + virtual QVariant getData(int index, int column) const = 0; - virtual QVariant getData (int index, int column) const = 0; + virtual void setData(int index, int column, const QVariant& data) = 0; - virtual void setData (int index, int column, const QVariant& data) = 0; + // Not in use. Temporarily removed so that the implementation of RefIdCollection can continue without + // these functions for now. + // virtual void merge() = 0; + ///< Merge modified into base. -// Not in use. Temporarily removed so that the implementation of RefIdCollection can continue without -// these functions for now. -// virtual void merge() = 0; - ///< Merge modified into base. + // virtual void purge() = 0; + ///< Remove records that are flagged as erased. -// virtual void purge() = 0; - ///< Remove records that are flagged as erased. + virtual void removeRows(int index, int count) = 0; - virtual void removeRows (int index, int count) = 0; + virtual void appendBlankRecord(const ESM::RefId& id, UniversalId::Type type = UniversalId::Type_None) = 0; + ///< \param type Will be ignored, unless the collection supports multiple record types - virtual void appendBlankRecord (const std::string& id, - UniversalId::Type type = UniversalId::Type_None) = 0; - ///< \param type Will be ignored, unless the collection supports multiple record types + virtual int searchId(const ESM::RefId& id) const = 0; + ////< Search record with \a id. + /// \return index of record (if found) or -1 (not found) - virtual int searchId (const std::string& id) const = 0; - ////< Search record with \a id. - /// \return index of record (if found) or -1 (not found) + virtual void replace(int index, std::unique_ptr record) = 0; + ///< If the record type does not match, an exception is thrown. + /// + /// \attention \a record must not change the ID. + ///< \param type Will be ignored, unless the collection supports multiple record types - virtual void replace (int index, const RecordBase& record) = 0; - ///< If the record type does not match, an exception is thrown. - /// - /// \attention \a record must not change the ID. - ///< \param type Will be ignored, unless the collection supports multiple record types + virtual void appendRecord(std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None) + = 0; + ///< If the record type does not match, an exception is thrown. - virtual void appendRecord (const RecordBase& record, - UniversalId::Type type = UniversalId::Type_None) = 0; - ///< If the record type does not match, an exception is thrown. + virtual void cloneRecord(const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) + = 0; - virtual void cloneRecord(const std::string& origin, - const std::string& destination, - const UniversalId::Type type) = 0; + virtual bool touchRecord(const ESM::RefId& id) = 0; - virtual bool touchRecord(const std::string& id) = 0; + virtual const RecordBase& getRecord(const ESM::RefId& id) const = 0; - virtual const RecordBase& getRecord (const std::string& id) const = 0; + virtual const RecordBase& getRecord(int index) const = 0; - virtual const RecordBase& getRecord (int index) const = 0; + virtual int getAppendIndex(const ESM::RefId& id, UniversalId::Type type = UniversalId::Type_None) const = 0; + ///< \param type Will be ignored, unless the collection supports multiple record types - virtual int getAppendIndex (const std::string& id, - UniversalId::Type type = UniversalId::Type_None) const = 0; - ///< \param type Will be ignored, unless the collection supports multiple record types + virtual std::vector getIds(bool listDeleted = true) const = 0; + ///< Return a sorted collection of all IDs + /// + /// \param listDeleted include deleted record in the list - virtual std::vector getIds (bool listDeleted = true) const = 0; - ///< Return a sorted collection of all IDs - /// - /// \param listDeleted include deleted record in the list + virtual bool reorderRows(int baseIndex, const std::vector& newOrder) = 0; + ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices + /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). + /// + /// \return Success? - virtual bool reorderRows (int baseIndex, const std::vector& newOrder) = 0; - ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices - /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). - /// - /// \return Success? + virtual int getInsertIndex( + const ESM::RefId& id, UniversalId::Type type = UniversalId::Type_None, RecordBase* record = nullptr) const; + ///< Works like getAppendIndex unless an overloaded method uses the record pointer + /// to get additional info about the record that results in an alternative index. - int searchColumnIndex (Columns::ColumnId id) const; - ///< Return index of column with the given \a id. If no such column exists, -1 is returned. + int searchColumnIndex(Columns::ColumnId id) const; + ///< Return index of column with the given \a id. If no such column exists, -1 is returned. - int findColumnIndex (Columns::ColumnId id) const; - ///< Return index of column with the given \a id. If no such column exists, an exception is - /// thrown. + int findColumnIndex(Columns::ColumnId id) const; + ///< Return index of column with the given \a id. If no such column exists, an exception is + /// thrown. }; } diff --git a/apps/opencs/model/world/columnbase.cpp b/apps/opencs/model/world/columnbase.cpp index cf333c1b1aa..1f963b0e4a4 100644 --- a/apps/opencs/model/world/columnbase.cpp +++ b/apps/opencs/model/world/columnbase.cpp @@ -1,12 +1,15 @@ #include "columnbase.hpp" -#include "columns.hpp" +#include -CSMWorld::ColumnBase::ColumnBase (int columnId, Display displayType, int flags) - : mColumnId (columnId), mFlags (flags), mDisplayType (displayType) -{} +#include "columns.hpp" -CSMWorld::ColumnBase::~ColumnBase() {} +CSMWorld::ColumnBase::ColumnBase(int columnId, Display displayType, int flags) + : mColumnId(columnId) + , mFlags(flags) + , mDisplayType(displayType) +{ +} bool CSMWorld::ColumnBase::isUserEditable() const { @@ -15,18 +18,17 @@ bool CSMWorld::ColumnBase::isUserEditable() const std::string CSMWorld::ColumnBase::getTitle() const { - return Columns::getName (static_cast (mColumnId)); + return Columns::getName(static_cast(mColumnId)); } -int CSMWorld::ColumnBase::getId() const +int CSMWorld::ColumnBase::getId() const { return mColumnId; } -bool CSMWorld::ColumnBase::isId (Display display) +bool CSMWorld::ColumnBase::isId(Display display) { - static const Display ids[] = - { + static const Display ids[] = { Display_Skill, Display_Class, Display_Faction, @@ -91,28 +93,28 @@ bool CSMWorld::ColumnBase::isId (Display display) Display_EffectAttribute, Display_IngredEffectId, - Display_None + Display_None, }; - for (int i=0; ids[i]!=Display_None; ++i) - if (ids[i]==display) + for (int i = 0; ids[i] != Display_None; ++i) + if (ids[i] == display) return true; return false; } -bool CSMWorld::ColumnBase::isText (Display display) +bool CSMWorld::ColumnBase::isText(Display display) { - return display==Display_String || display==Display_LongString || - display==Display_String32 || display==Display_LongString256; + return display == Display_String || display == Display_LongString || display == Display_String32 + || display == Display_String64 || display == Display_LongString256; } -bool CSMWorld::ColumnBase::isScript (Display display) +bool CSMWorld::ColumnBase::isScript(Display display) { - return display==Display_ScriptFile || display==Display_ScriptLines; + return display == Display_ScriptFile || display == Display_ScriptLines; } -void CSMWorld::NestableColumn::addColumn(CSMWorld::NestableColumn *column) +void CSMWorld::NestableColumn::addColumn(CSMWorld::NestableColumn* column) { mNestedColumns.push_back(column); } @@ -125,16 +127,16 @@ const CSMWorld::ColumnBase& CSMWorld::NestableColumn::nestedColumn(int subColumn return *mNestedColumns.at(subColumn); } -CSMWorld::NestableColumn::NestableColumn(int columnId, CSMWorld::ColumnBase::Display displayType, - int flag) +CSMWorld::NestableColumn::NestableColumn(int columnId, CSMWorld::ColumnBase::Display displayType, int flag) : CSMWorld::ColumnBase(columnId, displayType, flag) -{} +{ +} CSMWorld::NestableColumn::~NestableColumn() { - for (unsigned int i = 0; i < mNestedColumns.size(); ++i) + for (auto* col : mNestedColumns) { - delete mNestedColumns[i]; + delete col; } } @@ -143,12 +145,14 @@ bool CSMWorld::NestableColumn::hasChildren() const return !mNestedColumns.empty(); } -CSMWorld::NestedChildColumn::NestedChildColumn (int id, - CSMWorld::ColumnBase::Display display, int flags, bool isEditable) - : NestableColumn (id, display, flags) , mIsEditable(isEditable) -{} +CSMWorld::NestedChildColumn::NestedChildColumn( + int id, CSMWorld::ColumnBase::Display display, int flags, bool isEditable) + : NestableColumn(id, display, flags) + , mIsEditable(isEditable) +{ +} -bool CSMWorld::NestedChildColumn::isEditable () const +bool CSMWorld::NestedChildColumn::isEditable() const { return mIsEditable; } diff --git a/apps/opencs/model/world/columnbase.hpp b/apps/opencs/model/world/columnbase.hpp index 6dc58bd6345..e1576bba97c 100644 --- a/apps/opencs/model/world/columnbase.hpp +++ b/apps/opencs/model/world/columnbase.hpp @@ -1,31 +1,31 @@ #ifndef CSM_WOLRD_COLUMNBASE_H #define CSM_WOLRD_COLUMNBASE_H +#include #include #include -#include -#include #include -#include "record.hpp" - namespace CSMWorld { + template + struct Record; + struct ColumnBase { enum TableEditModes { - TableEdit_None, // no editing - TableEdit_Full, // edit cells and add/remove rows - TableEdit_FixedRows // edit cells only + TableEdit_None, // no editing + TableEdit_Full, // edit cells and add/remove rows + TableEdit_FixedRows // edit cells only }; enum Roles { Role_Flags = Qt::UserRole, - Role_Display = Qt::UserRole+1, - Role_ColumnId = Qt::UserRole+2 + Role_Display = Qt::UserRole + 1, + Role_ColumnId = Qt::UserRole + 2 }; enum Flags @@ -38,11 +38,11 @@ namespace CSMWorld enum Display { - Display_None, //Do not use + Display_None, // Do not use Display_String, Display_LongString, - //CONCRETE TYPES STARTS HERE (for drag and drop) + // CONCRETE TYPES STARTS HERE (for drag and drop) Display_Skill, Display_Class, Display_Faction, @@ -84,7 +84,7 @@ namespace CSMWorld Display_GlobalVariable, Display_BodyPart, Display_Enchantment, - //CONCRETE TYPES ENDS HERE + // CONCRETE TYPES ENDS HERE Display_SignedInteger8, Display_SignedInteger16, @@ -135,17 +135,18 @@ namespace CSMWorld Display_InfoCondVar, Display_InfoCondComp, Display_String32, + Display_String64, Display_LongString256, Display_BookType, Display_BloodType, Display_EmitterType, - Display_EffectSkill, // must display at least one, unlike Display_Skill + Display_EffectSkill, // must display at least one, unlike Display_Skill Display_EffectAttribute, // must display at least one, unlike Display_Attribute - Display_IngredEffectId, // display none allowed, unlike Display_EffectId - Display_GenderNpc, // must display at least one, unlike Display_Gender + Display_IngredEffectId, // display none allowed, unlike Display_EffectId + Display_GenderNpc, // must display at least one, unlike Display_Gender - //top level columns that nest other columns + // top level columns that nest other columns Display_NestedHeader }; @@ -153,9 +154,9 @@ namespace CSMWorld int mFlags; Display mDisplayType; - ColumnBase (int columnId, Display displayType, int flag); + ColumnBase(int columnId, Display displayType, int flag); - virtual ~ColumnBase(); + virtual ~ColumnBase() = default; virtual bool isEditable() const = 0; @@ -166,58 +167,61 @@ namespace CSMWorld virtual int getId() const; - static bool isId (Display display); + static bool isId(Display display); - static bool isText (Display display); + static bool isText(Display display); - static bool isScript (Display display); + static bool isScript(Display display); }; class NestableColumn : public ColumnBase { - std::vector mNestedColumns; + std::vector mNestedColumns; public: - NestableColumn(int columnId, Display displayType, int flag); ~NestableColumn(); - void addColumn(CSMWorld::NestableColumn *column); + void addColumn(CSMWorld::NestableColumn* column); const ColumnBase& nestedColumn(int subColumn) const; bool hasChildren() const; }; - template + template struct Column : public NestableColumn { - Column (int columnId, Display displayType, int flags = Flag_Table | Flag_Dialogue) - : NestableColumn (columnId, displayType, flags) {} + Column(int columnId, Display displayType, int flags = Flag_Table | Flag_Dialogue) + : NestableColumn(columnId, displayType, flags) + { + } - virtual QVariant get (const Record& record) const = 0; + virtual QVariant get(const Record& record) const = 0; - virtual void set (Record& record, const QVariant& data) + virtual void set(Record& record, const QVariant& data) { - throw std::logic_error ("Column " + getTitle() + " is not editable"); + throw std::logic_error("Column " + getTitle() + " is not editable"); } }; - template + template struct NestedParentColumn : public Column { - NestedParentColumn (int id, int flags = ColumnBase::Flag_Dialogue, bool fixedRows = false) - : Column (id, ColumnBase::Display_NestedHeader, flags), mFixedRows(fixedRows) - {} + NestedParentColumn(int id, int flags = ColumnBase::Flag_Dialogue, bool fixedRows = false) + : Column(id, ColumnBase::Display_NestedHeader, flags) + , mFixedRows(fixedRows) + { + } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { // There is nothing to do here. // This prevents exceptions from parent's implementation } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { // by default editable; also see IdTree::hasChildren() if (mFixedRows) @@ -226,10 +230,7 @@ namespace CSMWorld return QVariant::fromValue(ColumnBase::TableEdit_Full); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } private: bool mFixedRows; @@ -237,8 +238,7 @@ namespace CSMWorld struct NestedChildColumn : public NestableColumn { - NestedChildColumn (int id, - Display display, int flags = ColumnBase::Flag_Dialogue, bool isEditable = true); + NestedChildColumn(int id, Display display, int flags = ColumnBase::Flag_Dialogue, bool isEditable = true); bool isEditable() const override; diff --git a/apps/opencs/model/world/columnimp.cpp b/apps/opencs/model/world/columnimp.cpp index bec5008d35d..c31f9c01d07 100644 --- a/apps/opencs/model/world/columnimp.cpp +++ b/apps/opencs/model/world/columnimp.cpp @@ -1,40 +1,58 @@ #include "columnimp.hpp" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include #include -#include namespace CSMWorld { - /* LandTextureNicknameColumn */ - LandTextureNicknameColumn::LandTextureNicknameColumn() - : Column(Columns::ColumnId_TextureNickname, ColumnBase::Display_String) + namespace { - } + struct GetStringId + { + std::string operator()(ESM::EmptyRefId /*value*/) const { return std::string(); } - QVariant LandTextureNicknameColumn::get(const Record& record) const - { - return QString::fromUtf8(record.get().mId.c_str()); - } + std::string operator()(ESM::StringRefId value) const { return value.getValue(); } - void LandTextureNicknameColumn::set(Record& record, const QVariant& data) - { - LandTexture copy = record.get(); - copy.mId = data.toString().toUtf8().constData(); - record.setModified(copy); - } + std::string operator()(ESM::FormId value) const { return value.toString("FormId:"); } - bool LandTextureNicknameColumn::isEditable() const - { - return true; + std::string operator()(ESM::IndexRefId value) const + { + switch (value.getRecordType()) + { + case ESM::REC_MGEF: + return std::string(ESM::MagicEffect::sIndexNames[value.getValue()]); + default: + break; + } + + return value.toDebugString(); + } + + template + std::string operator()(const T& value) const + { + return value.toDebugString(); + } + }; } /* LandTextureIndexColumn */ LandTextureIndexColumn::LandTextureIndexColumn() - : Column(Columns::ColumnId_TextureIndex, ColumnBase::Display_Integer) + : Column(Columns::ColumnId_TextureIndex, ColumnBase::Display_Integer) { } - QVariant LandTextureIndexColumn::get(const Record& record) const + QVariant LandTextureIndexColumn::get(const Record& record) const { return record.get().mIndex; } @@ -52,7 +70,7 @@ namespace CSMWorld QVariant LandPluginIndexColumn::get(const Record& record) const { - return record.get().mPlugin; + return record.get().getPlugin(); } bool LandPluginIndexColumn::isEditable() const @@ -60,22 +78,6 @@ namespace CSMWorld return false; } - /* LandTexturePluginIndexColumn */ - LandTexturePluginIndexColumn::LandTexturePluginIndexColumn() - : Column(Columns::ColumnId_PluginIndex, ColumnBase::Display_Integer, 0) - { - } - - QVariant LandTexturePluginIndexColumn::get(const Record& record) const - { - return record.get().mPluginIndex; - } - - bool LandTexturePluginIndexColumn::isEditable() const - { - return false; - } - /* LandNormalsColumn */ LandNormalsColumn::LandNormalsColumn() : Column(Columns::ColumnId_LandNormalsIndex, ColumnBase::Display_String, 0) @@ -162,6 +164,8 @@ namespace CSMWorld copy.getLandData()->mHeights[i] = values[i]; } + copy.mFlags |= Land::Flag_HeightsNormals; + record.setModified(copy); } @@ -209,6 +213,8 @@ namespace CSMWorld copy.getLandData()->mColours[i] = values[i]; } + copy.mFlags |= Land::Flag_Colors; + record.setModified(copy); } @@ -256,6 +262,8 @@ namespace CSMWorld copy.getLandData()->mTextures[i] = values[i]; } + copy.mFlags |= Land::Flag_Textures; + record.setModified(copy); } @@ -265,24 +273,26 @@ namespace CSMWorld } /* BodyPartRaceColumn */ - BodyPartRaceColumn::BodyPartRaceColumn(const MeshTypeColumn *meshType) + BodyPartRaceColumn::BodyPartRaceColumn(const MeshTypeColumn* meshType) : mMeshType(meshType) - {} + { + } - QVariant BodyPartRaceColumn::get(const Record &record) const + QVariant BodyPartRaceColumn::get(const Record& record) const { if (mMeshType != nullptr && mMeshType->get(record) == ESM::BodyPart::MT_Skin) { - return QString::fromUtf8(record.get().mRace.c_str()); + return QString::fromUtf8(record.get().mRace.getRefIdString().c_str()); } - return QVariant(QVariant::UserType); + + return DisableTag::getVariant(); } - void BodyPartRaceColumn::set(Record &record, const QVariant &data) + void BodyPartRaceColumn::set(Record& record, const QVariant& data) { ESM::BodyPart record2 = record.get(); - record2.mRace = data.toString().toUtf8().constData(); + record2.mRace = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } @@ -291,4 +301,48 @@ namespace CSMWorld { return true; } + + SelectionGroupColumn::SelectionGroupColumn() + : Column(Columns::ColumnId_SelectionGroupObjects, ColumnBase::Display_None) + { + } + + QVariant SelectionGroupColumn::get(const Record& record) const + { + QVariant data; + QStringList selectionInfo; + const std::vector& instances = record.get().selectedInstances; + + for (const std::string& instance : instances) + selectionInfo << QString::fromStdString(instance); + data.setValue(selectionInfo); + + return data; + } + + void SelectionGroupColumn::set(Record& record, const QVariant& data) + { + ESM::SelectionGroup record2 = record.get(); + for (const auto& item : data.toStringList()) + record2.selectedInstances.push_back(item.toStdString()); + record.setModified(record2); + } + + bool SelectionGroupColumn::isEditable() const + { + return false; + } + + std::optional getSkillIndex(std::string_view value) + { + int index = ESM::Skill::refIdToIndex(ESM::RefId::stringRefId(value)); + if (index < 0) + return std::nullopt; + return static_cast(index); + } + + std::string getStringId(ESM::RefId value) + { + return visit(GetStringId{}, value); + } } diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index 17518937c24..192f27ed46f 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -5,171 +5,168 @@ #include #include #include +#include -#include -#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -#include -#include -#include +#include + +#include +#include +#include #include "columnbase.hpp" #include "columns.hpp" #include "info.hpp" - #include "land.hpp" -#include "landtexture.hpp" +#include "record.hpp" namespace CSMWorld { + std::optional getSkillIndex(std::string_view value); + + std::string getStringId(ESM::RefId value); + /// \note Shares ID with VarValueColumn. A table can not have both. - template + template struct FloatValueColumn : public Column { - FloatValueColumn() : Column (Columns::ColumnId_Value, ColumnBase::Display_Float) {} - - QVariant get (const Record& record) const override + FloatValueColumn() + : Column(Columns::ColumnId_Value, ColumnBase::Display_Float) { - return record.get().mValue.getFloat(); } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mValue.getFloat(); } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mValue.setFloat (data.toFloat()); - record.setModified (record2); + record2.mValue.setFloat(data.toFloat()); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct StringIdColumn : public Column { - StringIdColumn (bool hidden = false) - : Column (Columns::ColumnId_Id, ColumnBase::Display_Id, - hidden ? 0 : ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue) - {} - - QVariant get (const Record& record) const override + StringIdColumn(bool hidden = false) + : Column(Columns::ColumnId_Id, ColumnBase::Display_Id, + hidden ? 0 : ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue) { - return QString::fromUtf8 (record.get().mId.c_str()); } - bool isEditable() const override + QVariant get(const Record& record) const override { - return false; + return QString::fromStdString(getStringId(record.get().mId)); } + + bool isEditable() const override { return false; } }; - template<> + template <> inline QVariant StringIdColumn::get(const Record& record) const { const Land& land = record.get(); return QString::fromUtf8(Land::createUniqueRecordId(land.mX, land.mY).c_str()); } - template<> - inline QVariant StringIdColumn::get(const Record& record) const - { - const LandTexture& ltex = record.get(); - return QString::fromUtf8(LandTexture::createUniqueRecordId(ltex.mPluginIndex, ltex.mIndex).c_str()); - } - - template + template struct RecordStateColumn : public Column { RecordStateColumn() - : Column (Columns::ColumnId_Modification, ColumnBase::Display_RecordState) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_Modification, ColumnBase::Display_RecordState) { - if (record.mState==Record::State_Erased) - return static_cast (Record::State_Deleted); - - return static_cast (record.mState); } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { - record.mState = static_cast (data.toInt()); - } + if (record.mState == Record::State_Erased) + return static_cast(Record::State_Deleted); - bool isEditable() const override - { - return true; + return static_cast(record.mState); } - bool isUserEditable() const override + void set(Record& record, const QVariant& data) override { - return false; + record.mState = static_cast(data.toInt()); } + + bool isEditable() const override { return true; } + + bool isUserEditable() const override { return false; } }; - template + template struct FixedRecordTypeColumn : public Column { int mType; - FixedRecordTypeColumn (int type) - : Column (Columns::ColumnId_RecordType, ColumnBase::Display_Integer, 0), - mType (type) - {} - - QVariant get (const Record& record) const override + FixedRecordTypeColumn(int type) + : Column(Columns::ColumnId_RecordType, ColumnBase::Display_Integer, 0) + , mType(type) { - return mType; } - bool isEditable() const override - { - return false; - } + QVariant get(const Record& record) const override { return mType; } + + bool isEditable() const override { return false; } }; /// \attention A var type column must be immediately followed by a suitable value column. - template + template struct VarTypeColumn : public Column { - VarTypeColumn (ColumnBase::Display display) - : Column (Columns::ColumnId_ValueType, display, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh) - {} - - QVariant get (const Record& record) const override + VarTypeColumn(ColumnBase::Display display) + : Column(Columns::ColumnId_ValueType, display, + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh) { - return static_cast (record.get().mValue.getType()); } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { - ESXRecordT record2 = record.get(); - record2.mValue.setType (static_cast (data.toInt())); - record.setModified (record2); + return static_cast(record.get().mValue.getType()); } - bool isEditable() const override + void set(Record& record, const QVariant& data) override { - return true; + ESXRecordT record2 = record.get(); + record2.mValue.setType(static_cast(data.toInt())); + record.setModified(record2); } + + bool isEditable() const override { return true; } }; /// \note Shares ID with FloatValueColumn. A table can not have both. - template + template struct VarValueColumn : public Column { - VarValueColumn() : Column (Columns::ColumnId_Value, ColumnBase::Display_Var, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh) {} + VarValueColumn() + : Column(Columns::ColumnId_Value, ColumnBase::Display_Var, + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { switch (record.get().mValue.getType()) { case ESM::VT_String: - return QString::fromUtf8 (record.get().mValue.getString().c_str()); + return QString::fromUtf8(record.get().mValue.getString().c_str()); case ESM::VT_Int: case ESM::VT_Short: @@ -181,11 +178,12 @@ namespace CSMWorld return record.get().mValue.getFloat(); - default: return QVariant(); + default: + return QVariant(); } } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); @@ -193,310 +191,298 @@ namespace CSMWorld { case ESM::VT_String: - record2.mValue.setString (data.toString().toUtf8().constData()); + record2.mValue.setString(data.toString().toUtf8().constData()); break; case ESM::VT_Int: case ESM::VT_Short: case ESM::VT_Long: - record2.mValue.setInteger (data.toInt()); + record2.mValue.setInteger(data.toInt()); break; case ESM::VT_Float: - record2.mValue.setFloat (data.toFloat()); + record2.mValue.setFloat(data.toFloat()); break; - default: break; + default: + break; } - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct DescriptionColumn : public Column { DescriptionColumn() - : Column (Columns::ColumnId_Description, ColumnBase::Display_LongString) - {} + : Column(Columns::ColumnId_Description, ColumnBase::Display_LongString) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mDescription.c_str()); + return QString::fromUtf8(record.get().mDescription.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mDescription = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct SpecialisationColumn : public Column { SpecialisationColumn() - : Column (Columns::ColumnId_Specialisation, ColumnBase::Display_Specialisation) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_Specialisation, ColumnBase::Display_Specialisation) { - return record.get().mData.mSpecialization; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mSpecialization; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mSpecialization = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct UseValueColumn : public Column { int mIndex; - UseValueColumn (int index) - : Column (Columns::ColumnId_UseValue1 + index, ColumnBase::Display_Float), - mIndex (index) - {} - - QVariant get (const Record& record) const override + UseValueColumn(int index) + : Column(Columns::ColumnId_UseValue1 + index, ColumnBase::Display_Float) + , mIndex(index) { - return record.get().mData.mUseValue[mIndex]; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mUseValue[mIndex]; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mUseValue[mIndex] = data.toFloat(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct AttributeColumn : public Column { AttributeColumn() - : Column (Columns::ColumnId_Attribute, ColumnBase::Display_Attribute) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_Attribute, ColumnBase::Display_Attribute) { - return record.get().mData.mAttribute; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mAttribute; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mAttribute = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct NameColumn : public Column { - NameColumn() : Column (Columns::ColumnId_Name, ColumnBase::Display_String) {} + NameColumn(ColumnBase::Display display = ColumnBase::Display_String) + : Column(Columns::ColumnId_Name, display) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mName.c_str()); + return QString::fromUtf8(record.get().mName.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mName = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); + } + + bool isEditable() const override { return true; } + }; + + template <> + struct NameColumn : public Column + { + NameColumn(ColumnBase::Display display = ColumnBase::Display_String) + : Column(Columns::ColumnId_Name, display) + { + } + + QVariant get(const Record& record) const override + { + return QString::fromUtf8(record.get().mName.c_str()); } - bool isEditable() const override + void set(Record& record, const QVariant& data) override { - return true; + CSMWorld::Cell record2 = record.get(); + + record2.mName = data.toString().toUtf8().constData(); + + record.setModified(record2); } + + bool isEditable() const override { return true; } }; - template + template struct AttributesColumn : public Column { int mIndex; - AttributesColumn (int index) - : Column (Columns::ColumnId_Attribute1 + index, ColumnBase::Display_Attribute), - mIndex (index) - {} - - QVariant get (const Record& record) const override + AttributesColumn(int index) + : Column(Columns::ColumnId_Attribute1 + index, ColumnBase::Display_Attribute) + , mIndex(index) { - return record.get().mData.mAttribute[mIndex]; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mAttribute[mIndex]; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mAttribute[mIndex] = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct SkillsColumn : public Column { int mIndex; bool mMajor; - SkillsColumn (int index, bool typePrefix = false, bool major = false) - : Column ((typePrefix ? ( - major ? Columns::ColumnId_MajorSkill1 : Columns::ColumnId_MinorSkill1) : - Columns::ColumnId_Skill1) + index, ColumnBase::Display_Skill), - mIndex (index), mMajor (major) - {} - - QVariant get (const Record& record) const override + SkillsColumn(int index, bool typePrefix = false, bool major = false) + : Column((typePrefix ? (major ? Columns::ColumnId_MajorSkill1 : Columns::ColumnId_MinorSkill1) + : Columns::ColumnId_Skill1) + + index, + ColumnBase::Display_Skill) + , mIndex(index) + , mMajor(major) { - int skill = record.get().mData.getSkill (mIndex, mMajor); - - return QString::fromUtf8 (ESM::Skill::indexToId (skill).c_str()); } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { - std::istringstream stream (data.toString().toUtf8().constData()); - - int index = -1; - char c; - - stream >> c >> index; + return QString::fromStdString( + ESM::Skill::indexToRefId(record.get().mData.getSkill(mIndex, mMajor)).getRefIdString()); + } - if (index!=-1) + void set(Record& record, const QVariant& data) override + { + if (const auto index = getSkillIndex(data.toString().toStdString())) { ESXRecordT record2 = record.get(); - record2.mData.getSkill (mIndex, mMajor) = index; + record2.mData.getSkill(mIndex, mMajor) = static_cast(*index); - record.setModified (record2); + record.setModified(record2); } } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct PlayableColumn : public Column { - PlayableColumn() : Column (Columns::ColumnId_Playable, ColumnBase::Display_Boolean) - {} - - QVariant get (const Record& record) const override + PlayableColumn() + : Column(Columns::ColumnId_Playable, ColumnBase::Display_Boolean) { - return record.get().mData.mIsPlayable!=0; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mIsPlayable != 0; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mIsPlayable = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct HiddenColumn : public Column { - HiddenColumn() : Column (Columns::ColumnId_Hidden, ColumnBase::Display_Boolean) {} - - QVariant get (const Record& record) const override + HiddenColumn() + : Column(Columns::ColumnId_Hidden, ColumnBase::Display_Boolean) { - return record.get().mData.mIsHidden!=0; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mIsHidden != 0; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mIsHidden = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct FlagColumn : public Column { int mMask; bool mInverted; - FlagColumn (int columnId, int mask, - int flags = ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, bool inverted = false) - : Column (columnId, ColumnBase::Display_Boolean, flags), mMask (mask), - mInverted (inverted) - {} + FlagColumn(int columnId, int mask, int flags = ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, + bool inverted = false) + : Column(columnId, ColumnBase::Display_Boolean, flags) + , mMask(mask) + , mInverted(inverted) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - bool flag = (record.get().mData.mFlags & mMask)!=0; + bool flag = (record.get().mData.mFlags & mMask) != 0; if (mInverted) flag = !flag; @@ -504,40 +490,39 @@ namespace CSMWorld return flag; } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); int flags = record2.mData.mFlags & ~mMask; - if ((data.toInt()!=0)!=mInverted) + if ((data.toInt() != 0) != mInverted) flags |= mMask; record2.mData.mFlags = flags; - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct FlagColumn2 : public Column { int mMask; bool mInverted; - FlagColumn2 (int columnId, int mask, bool inverted = false) - : Column (columnId, ColumnBase::Display_Boolean), mMask (mask), - mInverted (inverted) - {} + FlagColumn2(int columnId, int mask, bool inverted = false) + : Column(columnId, ColumnBase::Display_Boolean) + , mMask(mask) + , mInverted(inverted) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - bool flag = (record.get().mFlags & mMask)!=0; + bool flag = (record.get().mFlags & mMask) != 0; if (mInverted) flag = !flag; @@ -545,67 +530,78 @@ namespace CSMWorld return flag; } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); int flags = record2.mFlags & ~mMask; - if ((data.toInt()!=0)!=mInverted) + if ((data.toInt() != 0) != mInverted) flags |= mMask; record2.mFlags = flags; - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct WeightHeightColumn : public Column { bool mMale; bool mWeight; - WeightHeightColumn (bool male, bool weight) - : Column (male ? - (weight ? Columns::ColumnId_MaleWeight : Columns::ColumnId_MaleHeight) : - (weight ? Columns::ColumnId_FemaleWeight : Columns::ColumnId_FemaleHeight), - ColumnBase::Display_Float), - mMale (male), mWeight (weight) - {} - - QVariant get (const Record& record) const override + WeightHeightColumn(bool male, bool weight) + : Column(male ? (weight ? Columns::ColumnId_MaleWeight : Columns::ColumnId_MaleHeight) + : (weight ? Columns::ColumnId_FemaleWeight : Columns::ColumnId_FemaleHeight), + ColumnBase::Display_Float) + , mMale(male) + , mWeight(weight) { - const ESM::Race::MaleFemaleF& value = - mWeight ? record.get().mData.mWeight : record.get().mData.mHeight; + } - return mMale ? value.mMale : value.mFemale; + QVariant get(const Record& record) const override + { + if (mWeight) + { + if (mMale) + return record.get().mData.mMaleWeight; + return record.get().mData.mFemaleWeight; + } + if (mMale) + return record.get().mData.mMaleHeight; + return record.get().mData.mFemaleHeight; } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - ESM::Race::MaleFemaleF& value = - mWeight ? record2.mData.mWeight : record2.mData.mHeight; - - (mMale ? value.mMale : value.mFemale) = data.toFloat(); + float bodyAttr = std::clamp(data.toFloat(), 0.5f, 2.0f); - record.setModified (record2); + if (mWeight) + { + if (mMale) + record2.mData.mMaleWeight = bodyAttr; + else + record2.mData.mFemaleWeight = bodyAttr; + } + else + { + if (mMale) + record2.mData.mMaleHeight = bodyAttr; + else + record2.mData.mFemaleHeight = bodyAttr; + } + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct SoundParamColumn : public Column { enum Type @@ -617,209 +613,203 @@ namespace CSMWorld Type mType; - SoundParamColumn (Type type) - : Column (type==Type_Volume ? Columns::ColumnId_Volume : - (type==Type_MinRange ? Columns::ColumnId_MinRange : Columns::ColumnId_MaxRange), - ColumnBase::Display_Integer), - mType (type) - {} + SoundParamColumn(Type type) + : Column(type == Type_Volume + ? Columns::ColumnId_Volume + : (type == Type_MinRange ? Columns::ColumnId_MinRange : Columns::ColumnId_MaxRange), + ColumnBase::Display_Integer) + , mType(type) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { int value = 0; switch (mType) { - case Type_Volume: value = record.get().mData.mVolume; break; - case Type_MinRange: value = record.get().mData.mMinRange; break; - case Type_MaxRange: value = record.get().mData.mMaxRange; break; + case Type_Volume: + value = record.get().mData.mVolume; + break; + case Type_MinRange: + value = record.get().mData.mMinRange; + break; + case Type_MaxRange: + value = record.get().mData.mMaxRange; + break; } return value; } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { int value = data.toInt(); - if (value<0) + if (value < 0) value = 0; - else if (value>255) + else if (value > 255) value = 255; ESXRecordT record2 = record.get(); switch (mType) { - case Type_Volume: record2.mData.mVolume = static_cast (value); break; - case Type_MinRange: record2.mData.mMinRange = static_cast (value); break; - case Type_MaxRange: record2.mData.mMaxRange = static_cast (value); break; + case Type_Volume: + record2.mData.mVolume = static_cast(value); + break; + case Type_MinRange: + record2.mData.mMinRange = static_cast(value); + break; + case Type_MaxRange: + record2.mData.mMaxRange = static_cast(value); + break; } - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct SoundFileColumn : public Column { SoundFileColumn() - : Column (Columns::ColumnId_SoundFile, ColumnBase::Display_SoundRes) - {} + : Column(Columns::ColumnId_SoundFile, ColumnBase::Display_SoundRes) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mSound.c_str()); + return QString::fromUtf8(record.get().mSound.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mSound = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct MapColourColumn : public Column { MapColourColumn() - : Column (Columns::ColumnId_MapColour, ColumnBase::Display_Colour) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_MapColour, ColumnBase::Display_Colour) { - return record.get().mMapColor; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mMapColor; } + + void set(Record& record, const QVariant& data) override { ESXRecordT copy = record.get(); copy.mMapColor = data.toInt(); - record.setModified (copy); + record.setModified(copy); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct SleepListColumn : public Column { SleepListColumn() - : Column (Columns::ColumnId_SleepEncounter, ColumnBase::Display_CreatureLevelledList) - {} + : Column(Columns::ColumnId_SleepEncounter, ColumnBase::Display_CreatureLevelledList) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mSleepList.c_str()); + return QString::fromUtf8(record.get().mSleepList.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mSleepList = data.toString().toUtf8().constData(); + record2.mSleepList = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct TextureColumn : public Column { - TextureColumn() : Column (Columns::ColumnId_Texture, ColumnBase::Display_Texture) {} + TextureColumn() + : Column(Columns::ColumnId_Texture, ColumnBase::Display_Texture) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mTexture.c_str()); + return QString::fromUtf8(record.get().mTexture.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mTexture = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct SpellTypeColumn : public Column { SpellTypeColumn() - : Column (Columns::ColumnId_SpellType, ColumnBase::Display_SpellType) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_SpellType, ColumnBase::Display_SpellType) { - return record.get().mData.mType; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mType; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mType = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct CostColumn : public Column { - CostColumn() : Column (Columns::ColumnId_Cost, ColumnBase::Display_Integer) {} - - QVariant get (const Record& record) const override + CostColumn() + : Column(Columns::ColumnId_Cost, ColumnBase::Display_Integer) { - return record.get().mData.mCost; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mCost; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mCost = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct ScriptColumn : public Column { enum Type @@ -829,523 +819,511 @@ namespace CSMWorld Type_Info // dialogue context (not implemented yet) }; - ScriptColumn (Type type) - : Column (Columns::ColumnId_ScriptText, - type==Type_File ? ColumnBase::Display_ScriptFile : ColumnBase::Display_ScriptLines, - type==Type_File ? 0 : ColumnBase::Flag_Dialogue) - {} + ScriptColumn(Type type) + : Column(Columns::ColumnId_ScriptText, + type == Type_File ? ColumnBase::Display_ScriptFile : ColumnBase::Display_ScriptLines, + type == Type_File ? 0 : ColumnBase::Flag_Dialogue) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mScriptText.c_str()); + return QString::fromUtf8(record.get().mScriptText.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mScriptText = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct RegionColumn : public Column { - RegionColumn() : Column (Columns::ColumnId_Region, ColumnBase::Display_Region) {} + RegionColumn() + : Column(Columns::ColumnId_Region, ColumnBase::Display_Region) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mRegion.c_str()); + return QString::fromUtf8(record.get().mRegion.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mRegion = data.toString().toUtf8().constData(); + record2.mRegion = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct CellColumn : public Column { bool mBlocked; /// \param blocked Do not allow user-modification - CellColumn (bool blocked = false) - : Column (Columns::ColumnId_Cell, ColumnBase::Display_Cell), - mBlocked (blocked) - {} + CellColumn(bool blocked = false) + : Column(Columns::ColumnId_Cell, ColumnBase::Display_Cell) + , mBlocked(blocked) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mCell.c_str()); + return QString::fromUtf8(record.get().mCell.toString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mCell = data.toString().toUtf8().constData(); + record2.mCell = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } - bool isUserEditable() const override - { - return !mBlocked; - } + bool isUserEditable() const override { return !mBlocked; } }; - template + template struct OriginalCellColumn : public Column { OriginalCellColumn() - : Column (Columns::ColumnId_OriginalCell, ColumnBase::Display_Cell) - {} + : Column(Columns::ColumnId_OriginalCell, ColumnBase::Display_Cell) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mOriginalCell.c_str()); + return QString::fromUtf8(record.get().mOriginalCell.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mOriginalCell = data.toString().toUtf8().constData(); + record2.mOriginalCell = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } - bool isUserEditable() const override - { - return false; - } + bool isUserEditable() const override { return false; } }; - template + template struct IdColumn : public Column { - IdColumn() : Column (Columns::ColumnId_ReferenceableId, - ColumnBase::Display_Referenceable) {} + IdColumn() + : Column(Columns::ColumnId_ReferenceableId, ColumnBase::Display_Referenceable) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mRefID.c_str()); + return QString::fromUtf8(record.get().mRefID.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mRefID = data.toString().toUtf8().constData(); + record2.mRefID = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct ScaleColumn : public Column { - ScaleColumn() : Column (Columns::ColumnId_Scale, ColumnBase::Display_Float) {} - - QVariant get (const Record& record) const override + ScaleColumn() + : Column(Columns::ColumnId_Scale, ColumnBase::Display_Float) { - return record.get().mScale; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mScale; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mScale = data.toFloat(); - record.setModified (record2); + record2.mScale = std::clamp(data.toFloat(), 0.5f, 2.0f); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct OwnerColumn : public Column { - OwnerColumn() : Column (Columns::ColumnId_Owner, ColumnBase::Display_Npc) {} + OwnerColumn() + : Column(Columns::ColumnId_Owner, ColumnBase::Display_Npc) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mOwner.c_str()); + return QString::fromUtf8(record.get().mOwner.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mOwner = data.toString().toUtf8().constData(); + record2.mOwner = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct SoulColumn : public Column { - SoulColumn() : Column (Columns::ColumnId_Soul, ColumnBase::Display_Creature) {} + SoulColumn() + : Column(Columns::ColumnId_Soul, ColumnBase::Display_Creature) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mSoul.c_str()); + return QString::fromUtf8(record.get().mSoul.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mSoul = data.toString().toUtf8().constData(); + record2.mSoul = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct FactionColumn : public Column { - FactionColumn() : Column (Columns::ColumnId_Faction, ColumnBase::Display_Faction) {} + FactionColumn() + : Column(Columns::ColumnId_Faction, ColumnBase::Display_Faction) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mFaction.c_str()); + return QString::fromUtf8(record.get().mFaction.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mFaction = data.toString().toUtf8().constData(); + record2.mFaction = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct FactionIndexColumn : public Column { FactionIndexColumn() - : Column (Columns::ColumnId_FactionIndex, ColumnBase::Display_Integer) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_FactionIndex, ColumnBase::Display_Integer) { - return record.get().mFactionRank; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mFactionRank; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mFactionRank = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct ChargesColumn : public Column { - ChargesColumn() : Column (Columns::ColumnId_Charges, ColumnBase::Display_Integer) {} - - QVariant get (const Record& record) const override + ChargesColumn() + : Column(Columns::ColumnId_Charges, ColumnBase::Display_Integer) { - return record.get().mChargeInt; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mChargeInt; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mChargeInt = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct EnchantmentChargesColumn : public Column { EnchantmentChargesColumn() - : Column (Columns::ColumnId_Enchantment, ColumnBase::Display_Float) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_Enchantment, ColumnBase::Display_Float) { - return record.get().mEnchantmentCharge; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mEnchantmentCharge; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mEnchantmentCharge = data.toFloat(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template - struct GoldValueColumn : public Column + template + struct StackSizeColumn : public Column { - GoldValueColumn() - : Column (Columns::ColumnId_CoinValue, ColumnBase::Display_Integer) {} - - QVariant get (const Record& record) const override + StackSizeColumn() + : Column(Columns::ColumnId_StackCount, ColumnBase::Display_Integer) { - return record.get().mGoldValue; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mCount; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mGoldValue = data.toInt(); - record.setModified (record2); + record2.mCount = data.toInt(); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct TeleportColumn : public Column { - TeleportColumn() - : Column (Columns::ColumnId_Teleport, ColumnBase::Display_Boolean) - {} - - QVariant get (const Record& record) const override + TeleportColumn(int flags) + : Column(Columns::ColumnId_Teleport, ColumnBase::Display_Boolean, flags) { - return record.get().mTeleport; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mTeleport; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mTeleport = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct TeleportCellColumn : public Column { TeleportCellColumn() - : Column (Columns::ColumnId_TeleportCell, ColumnBase::Display_Cell) - {} + : Column(Columns::ColumnId_TeleportCell, ColumnBase::Display_Cell) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mDestCell.c_str()); + if (!record.get().mTeleport) + return QVariant(); + return QString::fromUtf8(record.get().mDestCell.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mDestCell = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override + bool isEditable() const override { return true; } + + bool isUserEditable() const override { return true; } + }; + + template + struct IsLockedColumn : public Column + { + IsLockedColumn(int flags) + : Column(Columns::ColumnId_IsLocked, ColumnBase::Display_Boolean, flags) { - return true; } - bool isUserEditable() const override + QVariant get(const Record& record) const override { return record.get().mIsLocked; } + + void set(Record& record, const QVariant& data) override { - return true; + ESXRecordT record2 = record.get(); + record2.mIsLocked = data.toBool(); + record.setModified(record2); } + + bool isEditable() const override { return true; } }; - template + template struct LockLevelColumn : public Column { LockLevelColumn() - : Column (Columns::ColumnId_LockLevel, ColumnBase::Display_Integer) - {} + : Column(Columns::ColumnId_LockLevel, ColumnBase::Display_Integer) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return record.get().mLockLevel; + if (record.get().mIsLocked) + return record.get().mLockLevel; + return QVariant(); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mLockLevel = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct KeyColumn : public Column { - KeyColumn() : Column (Columns::ColumnId_Key, ColumnBase::Display_Miscellaneous) {} + KeyColumn() + : Column(Columns::ColumnId_Key, ColumnBase::Display_Miscellaneous) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mKey.c_str()); + if (record.get().mIsLocked) + return QString::fromUtf8(record.get().mKey.getRefIdString().c_str()); + return QVariant(); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mKey = data.toString().toUtf8().constData(); + record2.mKey = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct TrapColumn : public Column { - TrapColumn() : Column (Columns::ColumnId_Trap, ColumnBase::Display_Spell) {} + TrapColumn() + : Column(Columns::ColumnId_Trap, ColumnBase::Display_Spell) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mTrap.c_str()); + return QString::fromUtf8(record.get().mTrap.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mTrap = data.toString().toUtf8().constData(); + record2.mTrap = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct FilterColumn : public Column { - FilterColumn() : Column (Columns::ColumnId_Filter, ColumnBase::Display_Filter) {} + FilterColumn() + : Column(Columns::ColumnId_Filter, ColumnBase::Display_Filter) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mFilter.c_str()); + return QString::fromUtf8(record.get().mFilter.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mFilter = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct PosColumn : public Column { - ESM::Position ESXRecordT::* mPosition; + ESM::Position ESXRecordT::*mPosition; int mIndex; + bool mIsDoor; - PosColumn (ESM::Position ESXRecordT::* position, int index, bool door) - : Column ( - (door ? Columns::ColumnId_DoorPositionXPos : Columns::ColumnId_PositionXPos)+index, - ColumnBase::Display_Float), mPosition (position), mIndex (index) {} + PosColumn(ESM::Position ESXRecordT::*position, int index, bool door) + : Column((door ? Columns::ColumnId_DoorPositionXPos : Columns::ColumnId_PositionXPos) + index, + ColumnBase::Display_Float) + , mPosition(position) + , mIndex(index) + , mIsDoor(door) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { + if (!record.get().mTeleport && mIsDoor) + return QVariant(); const ESM::Position& position = record.get().*mPosition; return position.pos[mIndex]; } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); @@ -1353,33 +1331,37 @@ namespace CSMWorld position.pos[mIndex] = data.toFloat(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct RotColumn : public Column { - ESM::Position ESXRecordT::* mPosition; + ESM::Position ESXRecordT::*mPosition; int mIndex; + bool mIsDoor; - RotColumn (ESM::Position ESXRecordT::* position, int index, bool door) - : Column ( - (door ? Columns::ColumnId_DoorPositionXRot : Columns::ColumnId_PositionXRot)+index, - ColumnBase::Display_Double), mPosition (position), mIndex (index) {} + RotColumn(ESM::Position ESXRecordT::*position, int index, bool door) + : Column((door ? Columns::ColumnId_DoorPositionXRot : Columns::ColumnId_PositionXRot) + index, + ColumnBase::Display_Double) + , mPosition(position) + , mIndex(index) + , mIsDoor(door) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { + if (!record.get().mTeleport && mIsDoor) + return QVariant(); const ESM::Position& position = record.get().*mPosition; return osg::RadiansToDegrees(position.rot[mIndex]); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); @@ -1387,391 +1369,358 @@ namespace CSMWorld position.rot[mIndex] = osg::DegreesToRadians(data.toFloat()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct DialogueTypeColumn : public Column { - DialogueTypeColumn (bool hidden = false) - : Column (Columns::ColumnId_DialogueType, ColumnBase::Display_DialogueType, - hidden ? 0 : ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue) - {} - - QVariant get (const Record& record) const override + DialogueTypeColumn(bool hidden = false) + : Column(Columns::ColumnId_DialogueType, ColumnBase::Display_DialogueType, + hidden ? 0 : ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue) { - return static_cast (record.get().mType); } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return static_cast(record.get().mType); } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mType = data.toInt(); + record2.mType = static_cast(data.toInt()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } - bool isUserEditable() const override - { - return false; - } + bool isUserEditable() const override { return false; } }; - template + template struct QuestStatusTypeColumn : public Column { QuestStatusTypeColumn() - : Column (Columns::ColumnId_QuestStatusType, ColumnBase::Display_QuestStatusType) - {} + : Column(Columns::ColumnId_QuestStatusType, ColumnBase::Display_QuestStatusType) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return static_cast (record.get().mQuestStatus); + return static_cast(record.get().mQuestStatus); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mQuestStatus = static_cast (data.toInt()); + record2.mQuestStatus = static_cast(data.toInt()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct QuestDescriptionColumn : public Column { - QuestDescriptionColumn() : Column (Columns::ColumnId_QuestDescription, ColumnBase::Display_LongString) {} + QuestDescriptionColumn() + : Column(Columns::ColumnId_QuestDescription, ColumnBase::Display_LongString) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mResponse.c_str()); + return QString::fromUtf8(record.get().mResponse.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mResponse = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct QuestIndexColumn : public Column { QuestIndexColumn() - : Column (Columns::ColumnId_QuestIndex, ColumnBase::Display_Integer) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_QuestIndex, ColumnBase::Display_Integer) { - return record.get().mData.mDisposition; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mDisposition; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mDisposition = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct TopicColumn : public Column { - TopicColumn (bool journal) - : Column (journal ? Columns::ColumnId_Journal : Columns::ColumnId_Topic, - journal ? ColumnBase::Display_Journal : ColumnBase::Display_Topic) - {} + TopicColumn(bool journal) + : Column(journal ? Columns::ColumnId_Journal : Columns::ColumnId_Topic, + journal ? ColumnBase::Display_Journal : ColumnBase::Display_Topic) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mTopicId.c_str()); + return QString::fromUtf8(record.get().mTopicId.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mTopicId = data.toString().toUtf8().constData(); + record2.mTopicId = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } - bool isUserEditable() const override - { - return false; - } + bool isUserEditable() const override { return false; } }; - template + template struct ActorColumn : public Column { - ActorColumn() : Column (Columns::ColumnId_Actor, ColumnBase::Display_Npc) {} + ActorColumn() + : Column(Columns::ColumnId_Actor, ColumnBase::Display_Npc) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mActor.c_str()); + return QString::fromUtf8(record.get().mActor.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mActor = data.toString().toUtf8().constData(); + record2.mActor = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct RaceColumn : public Column { - RaceColumn() : Column (Columns::ColumnId_Race, ColumnBase::Display_Race) {} + RaceColumn() + : Column(Columns::ColumnId_Race, ColumnBase::Display_Race) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mRace.c_str()); + return QString::fromUtf8(record.get().mRace.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mRace = data.toString().toUtf8().constData(); + record2.mRace = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct ClassColumn : public Column { - ClassColumn() : Column (Columns::ColumnId_Class, ColumnBase::Display_Class) {} + ClassColumn() + : Column(Columns::ColumnId_Class, ColumnBase::Display_Class) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mClass.c_str()); + return QString::fromUtf8(record.get().mClass.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mClass = data.toString().toUtf8().constData(); + record2.mClass = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct PcFactionColumn : public Column { - PcFactionColumn() : Column (Columns::ColumnId_PcFaction, ColumnBase::Display_Faction) {} + PcFactionColumn() + : Column(Columns::ColumnId_PcFaction, ColumnBase::Display_Faction) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mPcFaction.c_str()); + return QString::fromUtf8(record.get().mPcFaction.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mPcFaction = data.toString().toUtf8().constData(); + record2.mPcFaction = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct ResponseColumn : public Column { - ResponseColumn() : Column (Columns::ColumnId_Response, ColumnBase::Display_LongString) {} + ResponseColumn() + : Column(Columns::ColumnId_Response, ColumnBase::Display_LongString) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mResponse.c_str()); + return QString::fromUtf8(record.get().mResponse.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mResponse = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct DispositionColumn : public Column { DispositionColumn() - : Column (Columns::ColumnId_Disposition, ColumnBase::Display_Integer) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_Disposition, ColumnBase::Display_Integer) { - return record.get().mData.mDisposition; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mDisposition; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mDisposition = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct RankColumn : public Column { RankColumn() - : Column (Columns::ColumnId_Rank, ColumnBase::Display_Integer) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_Rank, ColumnBase::Display_Integer) { - return static_cast (record.get().mData.mRank); } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { - ESXRecordT record2 = record.get(); - record2.mData.mRank = static_cast (data.toInt()); - record.setModified (record2); + return static_cast(record.get().mData.mRank); } - bool isEditable() const override + void set(Record& record, const QVariant& data) override { - return true; + ESXRecordT record2 = record.get(); + record2.mData.mRank = static_cast(data.toInt()); + record.setModified(record2); } + + bool isEditable() const override { return true; } }; - template + template struct PcRankColumn : public Column { PcRankColumn() - : Column (Columns::ColumnId_PcRank, ColumnBase::Display_Integer) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_PcRank, ColumnBase::Display_Integer) { - return static_cast (record.get().mData.mPCrank); } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { - ESXRecordT record2 = record.get(); - record2.mData.mPCrank = static_cast (data.toInt()); - record.setModified (record2); + return static_cast(record.get().mData.mPCrank); } - bool isEditable() const override + void set(Record& record, const QVariant& data) override { - return true; + ESXRecordT record2 = record.get(); + record2.mData.mPCrank = static_cast(data.toInt()); + record.setModified(record2); } + + bool isEditable() const override { return true; } }; - template + template struct GenderColumn : public Column { GenderColumn() - : Column (Columns::ColumnId_Gender, ColumnBase::Display_Gender) - {} + : Column(Columns::ColumnId_Gender, ColumnBase::Display_Gender) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return static_cast (record.get().mData.mGender); + return static_cast(record.get().mData.mGender); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mGender = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct GenderNpcColumn : public Column { GenderNpcColumn() : Column(Columns::ColumnId_Gender, ColumnBase::Display_GenderNpc) - {} + { + } QVariant get(const Record& record) const override { @@ -1795,653 +1744,625 @@ namespace CSMWorld record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct EnchantmentTypeColumn : public Column { EnchantmentTypeColumn() - : Column (Columns::ColumnId_EnchantmentType, ColumnBase::Display_EnchantmentType) - {} + : Column(Columns::ColumnId_EnchantmentType, ColumnBase::Display_EnchantmentType) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return static_cast (record.get().mData.mType); + return static_cast(record.get().mData.mType); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mType = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct ChargesColumn2 : public Column { - ChargesColumn2() : Column (Columns::ColumnId_Charges, ColumnBase::Display_Integer) {} - - QVariant get (const Record& record) const override + ChargesColumn2() + : Column(Columns::ColumnId_Charges, ColumnBase::Display_Integer) { - return record.get().mData.mCharge; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mCharge; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mCharge = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct AutoCalcColumn : public Column { - AutoCalcColumn() : Column (Columns::ColumnId_AutoCalc, ColumnBase::Display_Boolean) - {} - - QVariant get (const Record& record) const override + AutoCalcColumn() + : Column(Columns::ColumnId_AutoCalc, ColumnBase::Display_Boolean) { - return record.get().mData.mAutocalc!=0; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mAutocalc != 0; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mAutocalc = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct ModelColumn : public Column { - ModelColumn() : Column (Columns::ColumnId_Model, ColumnBase::Display_Mesh) {} + ModelColumn() + : Column(Columns::ColumnId_Model, ColumnBase::Display_Mesh) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mModel.c_str()); + return QString::fromUtf8(record.get().mModel.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mModel = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct VampireColumn : public Column { - VampireColumn() : Column (Columns::ColumnId_Vampire, ColumnBase::Display_Boolean) - {} - - QVariant get (const Record& record) const override + VampireColumn() + : Column(Columns::ColumnId_Vampire, ColumnBase::Display_Boolean) { - return record.get().mData.mVampire!=0; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mVampire != 0; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mVampire = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct BodyPartTypeColumn : public Column { BodyPartTypeColumn() - : Column (Columns::ColumnId_BodyPartType, ColumnBase::Display_BodyPartType) - {} + : Column(Columns::ColumnId_BodyPartType, ColumnBase::Display_BodyPartType) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return static_cast (record.get().mData.mPart); + return static_cast(record.get().mData.mPart); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mPart = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct MeshTypeColumn : public Column { MeshTypeColumn(int flags = ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue) - : Column (Columns::ColumnId_MeshType, ColumnBase::Display_MeshType, flags) - {} + : Column(Columns::ColumnId_MeshType, ColumnBase::Display_MeshType, flags) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return static_cast (record.get().mData.mType); + return static_cast(record.get().mData.mType); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mType = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct OwnerGlobalColumn : public Column { OwnerGlobalColumn() - : Column (Columns::ColumnId_OwnerGlobal, ColumnBase::Display_GlobalVariable) - {} + : Column(Columns::ColumnId_OwnerGlobal, ColumnBase::Display_GlobalVariable) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mGlobalVariable.c_str()); + return QString::fromUtf8(record.get().mGlobalVariable.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mGlobalVariable = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct RefNumCounterColumn : public Column { RefNumCounterColumn() - : Column (Columns::ColumnId_RefNumCounter, ColumnBase::Display_Integer, 0) - {} + : Column(Columns::ColumnId_RefNumCounter, ColumnBase::Display_Integer, 0) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return static_cast (record.get().mRefNumCounter); + return static_cast(record.get().mRefNumCounter); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRefNumCounter = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } - bool isUserEditable() const override - { - return false; - } + bool isUserEditable() const override { return false; } }; - template + template struct RefNumColumn : public Column { RefNumColumn() - : Column (Columns::ColumnId_RefNum, ColumnBase::Display_Integer, 0) - {} + : Column(Columns::ColumnId_RefNum, ColumnBase::Display_Integer, 0) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return static_cast (record.get().mRefNum.mIndex); + return static_cast(record.get().mRefNum.mIndex); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRefNum.mIndex = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } - bool isUserEditable() const override - { - return false; - } + bool isUserEditable() const override { return false; } }; - template + template struct SoundColumn : public Column { SoundColumn() - : Column (Columns::ColumnId_Sound, ColumnBase::Display_Sound) - {} + : Column(Columns::ColumnId_Sound, ColumnBase::Display_Sound) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mSound.c_str()); + return QString::fromUtf8(record.get().mSound.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mSound = data.toString().toUtf8().constData(); + record2.mSound = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct CreatureColumn : public Column { CreatureColumn() - : Column (Columns::ColumnId_Creature, ColumnBase::Display_Creature) - {} + : Column(Columns::ColumnId_Creature, ColumnBase::Display_Creature) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mCreature.c_str()); + return QString::fromUtf8(record.get().mCreature.getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mCreature = data.toString().toUtf8().constData(); + record2.mCreature = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct SoundGeneratorTypeColumn : public Column { SoundGeneratorTypeColumn() - : Column (Columns::ColumnId_SoundGeneratorType, ColumnBase::Display_SoundGeneratorType) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_SoundGeneratorType, ColumnBase::Display_SoundGeneratorType) { - return static_cast (record.get().mType); } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return static_cast(record.get().mType); } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mType = data.toInt(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct BaseCostColumn : public Column { - BaseCostColumn() : Column (Columns::ColumnId_BaseCost, ColumnBase::Display_Float) {} - - QVariant get (const Record& record) const override + BaseCostColumn() + : Column(Columns::ColumnId_BaseCost, ColumnBase::Display_Float) { - return record.get().mData.mBaseCost; } - void set (Record& record, const QVariant& data) override + QVariant get(const Record& record) const override { return record.get().mData.mBaseCost; } + + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mBaseCost = data.toFloat(); - record.setModified (record2); + record.setModified(record2); + } + + bool isEditable() const override { return true; } + }; + + template + struct ProjectileSpeedColumn : public Column + { + ProjectileSpeedColumn() + : Column(Columns::ColumnId_ProjectileSpeed, ColumnBase::Display_Float) + { } - bool isEditable() const override + QVariant get(const Record& record) const override { return record.get().mData.mSpeed; } + + void set(Record& record, const QVariant& data) override { - return true; + ESXRecordT record2 = record.get(); + record2.mData.mSpeed = data.toFloat(); + record.setModified(record2); } + + bool isEditable() const override { return true; } }; - template + template struct SchoolColumn : public Column { SchoolColumn() - : Column (Columns::ColumnId_School, ColumnBase::Display_School) - {} + : Column(Columns::ColumnId_School, ColumnBase::Display_School) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return record.get().mData.mSchool; + return ESM::MagicSchool::skillRefIdToIndex(record.get().mData.mSchool); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - record2.mData.mSchool = data.toInt(); + record2.mData.mSchool = ESM::MagicSchool::indexToSkillRefId(data.toInt()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct EffectTextureColumn : public Column { - EffectTextureColumn (Columns::ColumnId columnId) - : Column (columnId, - columnId == Columns::ColumnId_Particle ? ColumnBase::Display_Texture - : ColumnBase::Display_Icon) + EffectTextureColumn(Columns::ColumnId columnId) + : Column(columnId, + columnId == Columns::ColumnId_Particle ? ColumnBase::Display_Texture : ColumnBase::Display_Icon) { - assert (this->mColumnId==Columns::ColumnId_Icon || - this->mColumnId==Columns::ColumnId_Particle); + assert(this->mColumnId == Columns::ColumnId_Icon || this->mColumnId == Columns::ColumnId_Particle); } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 ( - (this->mColumnId==Columns::ColumnId_Icon ? - record.get().mIcon : record.get().mParticle).c_str()); + return QString::fromUtf8( + (this->mColumnId == Columns::ColumnId_Icon ? record.get().mIcon : record.get().mParticle).c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); - (this->mColumnId==Columns::ColumnId_Icon ? - record2.mIcon : record2.mParticle) + (this->mColumnId == Columns::ColumnId_Icon ? record2.mIcon : record2.mParticle) = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct EffectObjectColumn : public Column { - EffectObjectColumn (Columns::ColumnId columnId) - : Column (columnId, columnId==Columns::ColumnId_BoltObject ? ColumnBase::Display_Weapon : ColumnBase::Display_Static) + EffectObjectColumn(Columns::ColumnId columnId) + : Column(columnId, + columnId == Columns::ColumnId_BoltObject ? ColumnBase::Display_Weapon : ColumnBase::Display_Static) { - assert (this->mColumnId==Columns::ColumnId_CastingObject || - this->mColumnId==Columns::ColumnId_HitObject || - this->mColumnId==Columns::ColumnId_AreaObject || - this->mColumnId==Columns::ColumnId_BoltObject); + assert(this->mColumnId == Columns::ColumnId_CastingObject || this->mColumnId == Columns::ColumnId_HitObject + || this->mColumnId == Columns::ColumnId_AreaObject || this->mColumnId == Columns::ColumnId_BoltObject); } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - const std::string *string = nullptr; + const ESM::RefId* string = nullptr; switch (this->mColumnId) { - case Columns::ColumnId_CastingObject: string = &record.get().mCasting; break; - case Columns::ColumnId_HitObject: string = &record.get().mHit; break; - case Columns::ColumnId_AreaObject: string = &record.get().mArea; break; - case Columns::ColumnId_BoltObject: string = &record.get().mBolt; break; + case Columns::ColumnId_CastingObject: + string = &record.get().mCasting; + break; + case Columns::ColumnId_HitObject: + string = &record.get().mHit; + break; + case Columns::ColumnId_AreaObject: + string = &record.get().mArea; + break; + case Columns::ColumnId_BoltObject: + string = &record.get().mBolt; + break; } if (!string) - throw std::logic_error ("Unsupported column ID"); + throw std::logic_error("Unsupported column ID"); - return QString::fromUtf8 (string->c_str()); + return QString::fromUtf8(string->getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { - std::string *string = nullptr; + ESM::RefId* id = nullptr; ESXRecordT record2 = record.get(); switch (this->mColumnId) { - case Columns::ColumnId_CastingObject: string = &record2.mCasting; break; - case Columns::ColumnId_HitObject: string = &record2.mHit; break; - case Columns::ColumnId_AreaObject: string = &record2.mArea; break; - case Columns::ColumnId_BoltObject: string = &record2.mBolt; break; + case Columns::ColumnId_CastingObject: + id = &record2.mCasting; + break; + case Columns::ColumnId_HitObject: + id = &record2.mHit; + break; + case Columns::ColumnId_AreaObject: + id = &record2.mArea; + break; + case Columns::ColumnId_BoltObject: + id = &record2.mBolt; + break; } - if (!string) - throw std::logic_error ("Unsupported column ID"); + if (!id) + throw std::logic_error("Unsupported column ID"); - *string = data.toString().toUtf8().constData(); + *id = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct EffectSoundColumn : public Column { - EffectSoundColumn (Columns::ColumnId columnId) - : Column (columnId, ColumnBase::Display_Sound) + EffectSoundColumn(Columns::ColumnId columnId) + : Column(columnId, ColumnBase::Display_Sound) { - assert (this->mColumnId==Columns::ColumnId_CastingSound || - this->mColumnId==Columns::ColumnId_HitSound || - this->mColumnId==Columns::ColumnId_AreaSound || - this->mColumnId==Columns::ColumnId_BoltSound); + assert(this->mColumnId == Columns::ColumnId_CastingSound || this->mColumnId == Columns::ColumnId_HitSound + || this->mColumnId == Columns::ColumnId_AreaSound || this->mColumnId == Columns::ColumnId_BoltSound); } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - const std::string *string = nullptr; + const ESM::RefId* id = nullptr; switch (this->mColumnId) { - case Columns::ColumnId_CastingSound: string = &record.get().mCastSound; break; - case Columns::ColumnId_HitSound: string = &record.get().mHitSound; break; - case Columns::ColumnId_AreaSound: string = &record.get().mAreaSound; break; - case Columns::ColumnId_BoltSound: string = &record.get().mBoltSound; break; + case Columns::ColumnId_CastingSound: + id = &record.get().mCastSound; + break; + case Columns::ColumnId_HitSound: + id = &record.get().mHitSound; + break; + case Columns::ColumnId_AreaSound: + id = &record.get().mAreaSound; + break; + case Columns::ColumnId_BoltSound: + id = &record.get().mBoltSound; + break; } - if (!string) - throw std::logic_error ("Unsupported column ID"); + if (!id) + throw std::logic_error("Unsupported column ID"); - return QString::fromUtf8 (string->c_str()); + return QString::fromUtf8(id->getRefIdString().c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { - std::string *string = nullptr; + ESM::RefId* id = nullptr; ESXRecordT record2 = record.get(); switch (this->mColumnId) { - case Columns::ColumnId_CastingSound: string = &record2.mCastSound; break; - case Columns::ColumnId_HitSound: string = &record2.mHitSound; break; - case Columns::ColumnId_AreaSound: string = &record2.mAreaSound; break; - case Columns::ColumnId_BoltSound: string = &record2.mBoltSound; break; + case Columns::ColumnId_CastingSound: + id = &record2.mCastSound; + break; + case Columns::ColumnId_HitSound: + id = &record2.mHitSound; + break; + case Columns::ColumnId_AreaSound: + id = &record2.mAreaSound; + break; + case Columns::ColumnId_BoltSound: + id = &record2.mBoltSound; + break; } - if (!string) - throw std::logic_error ("Unsupported column ID"); + if (!id) + throw std::logic_error("Unsupported column ID"); - *string = data.toString().toUtf8().constData(); + *id = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct FormatColumn : public Column { FormatColumn() - : Column (Columns::ColumnId_FileFormat, ColumnBase::Display_Integer) - {} - - QVariant get (const Record& record) const override + : Column(Columns::ColumnId_FileFormat, ColumnBase::Display_Integer) { - return record.get().mFormat; } - bool isEditable() const override - { - return false; - } + QVariant get(const Record& record) const override { return record.get().mFormatVersion; } + + bool isEditable() const override { return false; } }; - template + template struct AuthorColumn : public Column { AuthorColumn() - : Column (Columns::ColumnId_Author, ColumnBase::Display_String32) - {} + : Column(Columns::ColumnId_Author, ColumnBase::Display_String32) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mAuthor.c_str()); + return QString::fromUtf8(record.get().mAuthor.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mAuthor = data.toString().toUtf8().constData(); - record.setModified (record2); + record.setModified(record2); } - bool isEditable() const override - { - return true; - } + bool isEditable() const override { return true; } }; - template + template struct FileDescriptionColumn : public Column { FileDescriptionColumn() - : Column (Columns::ColumnId_FileDescription, ColumnBase::Display_LongString256) - {} + : Column(Columns::ColumnId_FileDescription, ColumnBase::Display_LongString256) + { + } - QVariant get (const Record& record) const override + QVariant get(const Record& record) const override { - return QString::fromUtf8 (record.get().mDescription.c_str()); + return QString::fromUtf8(record.get().mDescription.c_str()); } - void set (Record& record, const QVariant& data) override + void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mDescription = data.toString().toUtf8().constData(); - record.setModified (record2); - } - - bool isEditable() const override - { - return true; + record.setModified(record2); } - }; - - struct LandTextureNicknameColumn : public Column - { - LandTextureNicknameColumn(); - QVariant get(const Record& record) const override; - void set(Record& record, const QVariant& data) override; - bool isEditable() const override; + bool isEditable() const override { return true; } }; - struct LandTextureIndexColumn : public Column + struct LandTextureIndexColumn : public Column { LandTextureIndexColumn(); - QVariant get(const Record& record) const override; + QVariant get(const Record& record) const override; bool isEditable() const override; }; @@ -2453,14 +2374,6 @@ namespace CSMWorld bool isEditable() const override; }; - struct LandTexturePluginIndexColumn : public Column - { - LandTexturePluginIndexColumn(); - - QVariant get(const Record& record) const override; - bool isEditable() const override; - }; - struct LandNormalsColumn : public Column { using DataType = QVector; @@ -2507,12 +2420,23 @@ namespace CSMWorld struct BodyPartRaceColumn : public RaceColumn { - const MeshTypeColumn *mMeshType; + const MeshTypeColumn* mMeshType; + + BodyPartRaceColumn(const MeshTypeColumn* meshType); + + QVariant get(const Record& record) const override; + void set(Record& record, const QVariant& data) override; + bool isEditable() const override; + }; + + struct SelectionGroupColumn : public Column + { + SelectionGroupColumn(); + + QVariant get(const Record& record) const override; - BodyPartRaceColumn(const MeshTypeColumn *meshType); + void set(Record& record, const QVariant& data) override; - QVariant get(const Record &record) const override; - void set(Record &record, const QVariant &data) override; bool isEditable() const override; }; } diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index eaea66c2f9f..d4c35c5cec7 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -1,10 +1,13 @@ #include "columns.hpp" +#include +#include + #include -#include +#include -#include "universalid.hpp" #include "infoselectwrapper.hpp" +#include "universalid.hpp" namespace CSMWorld { @@ -13,11 +16,10 @@ namespace CSMWorld struct ColumnDesc { int mId; - const char *mName; + const char* mName; }; - const ColumnDesc sNames[] = - { + const ColumnDesc sNames[] = { { ColumnId_Value, "Value" }, { ColumnId_Id, "ID" }, { ColumnId_Modification, "Modified" }, @@ -54,9 +56,11 @@ namespace CSMWorld { ColumnId_FactionIndex, "Faction Index" }, { ColumnId_Charges, "Charges" }, { ColumnId_Enchantment, "Enchantment" }, - { ColumnId_CoinValue, "Coin Value" }, + { ColumnId_StackCount, "Count" }, + { ColumnId_GoldValue, "Value" }, { ColumnId_Teleport, "Teleport" }, { ColumnId_TeleportCell, "Teleport Cell" }, + { ColumnId_IsLocked, "Locked" }, { ColumnId_LockLevel, "Lock Level" }, { ColumnId_Key, "Key" }, { ColumnId_Trap, "Trap" }, @@ -175,7 +179,7 @@ namespace CSMWorld { ColumnId_ContainerContent, "Content" }, { ColumnId_ItemCount, "Count" }, - { ColumnId_InventoryItemId, "Item ID"}, + { ColumnId_InventoryItemId, "Item ID" }, { ColumnId_CombatState, "Combat" }, { ColumnId_MagicState, "Magic" }, @@ -187,16 +191,16 @@ namespace CSMWorld { ColumnId_ActorInventory, "Inventory" }, { ColumnId_SpellList, "Spells" }, - { ColumnId_SpellId, "Spell ID"}, + { ColumnId_SpellId, "Spell ID" }, { ColumnId_NpcDestinations, "Destinations" }, - { ColumnId_DestinationCell, "Dest Cell"}, - { ColumnId_PosX, "Dest X"}, - { ColumnId_PosY, "Dest Y"}, - { ColumnId_PosZ, "Dest Z"}, - { ColumnId_RotX, "Rotation X"}, - { ColumnId_RotY, "Rotation Y"}, - { ColumnId_RotZ, "Rotation Z"}, + { ColumnId_DestinationCell, "Dest Cell" }, + { ColumnId_PosX, "Dest X" }, + { ColumnId_PosY, "Dest Y" }, + { ColumnId_PosZ, "Dest Z" }, + { ColumnId_RotX, "Rotation X" }, + { ColumnId_RotY, "Rotation Y" }, + { ColumnId_RotZ, "Rotation Z" }, { ColumnId_OwnerGlobal, "Owner Global" }, { ColumnId_DefaultProfile, "Default Profile" }, @@ -233,6 +237,7 @@ namespace CSMWorld { ColumnId_RegionSounds, "Sounds" }, { ColumnId_SoundName, "Sound Name" }, { ColumnId_SoundChance, "Chance" }, + { ColumnId_SoundProbability, "Probability" }, { ColumnId_FactionReactions, "Reactions" }, { ColumnId_FactionRanks, "Ranks" }, @@ -255,7 +260,7 @@ namespace CSMWorld { ColumnId_AiWanderDist, "Wander Dist" }, { ColumnId_AiDuration, "Ai Duration" }, { ColumnId_AiWanderToD, "Wander ToD" }, - { ColumnId_AiWanderRepeat, "Wander Repeat" }, + { ColumnId_AiWanderRepeat, "Ai Repeat" }, { ColumnId_AiActivateName, "Activate" }, { ColumnId_AiTargetId, "Target ID" }, { ColumnId_AiTargetCell, "Target Cell" }, @@ -265,11 +270,11 @@ namespace CSMWorld { ColumnId_PartRefMale, "Male Part" }, { ColumnId_PartRefFemale, "Female Part" }, - { ColumnId_LevelledList,"Levelled List" }, - { ColumnId_LevelledItemId,"Levelled Item" }, - { ColumnId_LevelledItemLevel,"Item Level" }, + { ColumnId_LevelledList, "Levelled List" }, + { ColumnId_LevelledItemId, "Levelled Item" }, + { ColumnId_LevelledItemLevel, "PC Level" }, { ColumnId_LevelledItemType, "Calculate all levels <= player" }, - { ColumnId_LevelledItemTypeEach, "Select a new item each instance" }, + { ColumnId_LevelledItemTypeEach, "Select a new item for each instance" }, { ColumnId_LevelledItemChanceNone, "Chance None" }, { ColumnId_PowerList, "Powers" }, @@ -294,7 +299,6 @@ namespace CSMWorld { ColumnId_NpcReputation, "Reputation" }, { ColumnId_NpcRank, "NPC Rank" }, { ColumnId_Gold, "Gold" }, - { ColumnId_NpcPersistence, "Persistent" }, { ColumnId_RaceAttributes, "Race Attributes" }, { ColumnId_Male, "Male" }, @@ -320,7 +324,6 @@ namespace CSMWorld { ColumnId_MaxAttack, "Max Attack" }, { ColumnId_CreatureMisc, "Creature Misc" }, - { ColumnId_Idle1, "Idle 1" }, { ColumnId_Idle2, "Idle 2" }, { ColumnId_Idle3, "Idle 3" }, { ColumnId_Idle4, "Idle 4" }, @@ -328,6 +331,7 @@ namespace CSMWorld { ColumnId_Idle6, "Idle 6" }, { ColumnId_Idle7, "Idle 7" }, { ColumnId_Idle8, "Idle 8" }, + { ColumnId_Idle9, "Idle 9" }, { ColumnId_RegionWeather, "Weather" }, { ColumnId_WeatherName, "Type" }, @@ -371,26 +375,31 @@ namespace CSMWorld { ColumnId_Skill6, "Skill 6" }, { ColumnId_Skill7, "Skill 7" }, - { -1, 0 } // end marker + { ColumnId_Persistent, "Persistent" }, + { ColumnId_Blocked, "Blocked" }, + + { ColumnId_LevelledCreatureId, "Levelled Creature" }, + { ColumnId_ProjectileSpeed, "Projectile Speed" }, + + // end marker + { -1, 0 }, }; } } -std::string CSMWorld::Columns::getName (ColumnId column) +std::string CSMWorld::Columns::getName(ColumnId column) { - for (int i=0; sNames[i].mName; ++i) - if (column==sNames[i].mId) + for (int i = 0; sNames[i].mName; ++i) + if (column == sNames[i].mId) return sNames[i].mName; return ""; } -int CSMWorld::Columns::getId (const std::string& name) +int CSMWorld::Columns::getId(const std::string& name) { - std::string name2 = Misc::StringUtils::lowerCase (name); - - for (int i=0; sNames[i].mName; ++i) - if (Misc::StringUtils::ciEqual(sNames[i].mName, name2)) + for (int i = 0; sNames[i].mName; ++i) + if (Misc::StringUtils::ciEqual(std::string_view(sNames[i].mName), name)) return sNames[i].mId; return -1; @@ -398,246 +407,191 @@ int CSMWorld::Columns::getId (const std::string& name) namespace { - static const char *sSpecialisations[] = - { - "Combat", "Magic", "Stealth", 0 - }; + static const char* sSpecialisations[] = { "Combat", "Magic", "Stealth", 0 }; - // see ESM::Attribute::AttributeID in - static const char *sAttributes[] = - { - "Strength", "Intelligence", "Willpower", "Agility", "Speed", "Endurance", "Personality", - "Luck", 0 - }; + // see ESM::Attribute::AttributeID in + static const char* sAttributes[] + = { "Strength", "Intelligence", "Willpower", "Agility", "Speed", "Endurance", "Personality", "Luck", 0 }; - static const char *sSpellTypes[] = - { - "Spell", "Ability", "Blight", "Disease", "Curse", "Power", 0 - }; + static const char* sSpellTypes[] = { "Spell", "Ability", "Blight", "Disease", "Curse", "Power", 0 }; - static const char *sApparatusTypes[] = - { - "Mortar & Pestle", "Alembic", "Calcinator", "Retort", 0 - }; + static const char* sApparatusTypes[] = { "Mortar & Pestle", "Alembic", "Calcinator", "Retort", 0 }; - static const char *sArmorTypes[] = - { - "Helmet", "Cuirass", "Left Pauldron", "Right Pauldron", "Greaves", "Boots", "Left Gauntlet", - "Right Gauntlet", "Shield", "Left Bracer", "Right Bracer", 0 - }; + static const char* sArmorTypes[] = { "Helmet", "Cuirass", "Left Pauldron", "Right Pauldron", "Greaves", "Boots", + "Left Gauntlet", "Right Gauntlet", "Shield", "Left Bracer", "Right Bracer", 0 }; - static const char *sClothingTypes[] = - { - "Pants", "Shoes", "Shirt", "Belt", "Robe", "Right Glove", "Left Glove", "Skirt", "Ring", - "Amulet", 0 - }; + static const char* sClothingTypes[] + = { "Pants", "Shoes", "Shirt", "Belt", "Robe", "Right Glove", "Left Glove", "Skirt", "Ring", "Amulet", 0 }; - static const char *sCreatureTypes[] = - { - "Creature", "Daedra", "Undead", "Humanoid", 0 - }; + static const char* sCreatureTypes[] = { "Creature", "Daedra", "Undead", "Humanoid", 0 }; - static const char *sWeaponTypes[] = - { - "Short Blade 1H", "Long Blade 1H", "Long Blade 2H", "Blunt 1H", "Blunt 2H Close", - "Blunt 2H Wide", "Spear 2H", "Axe 1H", "Axe 2H", "Bow", "Crossbow", "Thrown", "Arrow", - "Bolt", 0 - }; + static const char* sWeaponTypes[] + = { "Short Blade 1H", "Long Blade 1H", "Long Blade 2H", "Blunt 1H", "Blunt 2H Close", "Blunt 2H Wide", + "Spear 2H", "Axe 1H", "Axe 2H", "Bow", "Crossbow", "Thrown", "Arrow", "Bolt", 0 }; - static const char *sModificationEnums[] = - { - "Base", "Modified", "Added", "Deleted", "Deleted", 0 - }; + static const char* sModificationEnums[] = { "Base", "Modified", "Added", "Deleted", "Deleted", 0 }; - static const char *sVarTypeEnums[] = - { - "unknown", "none", "short", "integer", "long", "float", "string", 0 - }; + static const char* sVarTypeEnums[] = { "unknown", "none", "short", "integer", "long", "float", "string", 0 }; - static const char *sDialogueTypeEnums[] = - { - "Topic", "Voice", "Greeting", "Persuasion", 0 - }; + static const char* sDialogueTypeEnums[] = { "Topic", "Voice", "Greeting", "Persuasion", 0 }; - static const char *sQuestStatusTypes[] = - { - "None", "Name", "Finished", "Restart", 0 - }; + static const char* sQuestStatusTypes[] = { "None", "Name", "Finished", "Restart", 0 }; - static const char *sGenderEnums[] = - { - "Male", "Female", 0 - }; + static const char* sGenderEnums[] = { "Male", "Female", 0 }; - static const char *sEnchantmentTypes[] = - { - "Cast Once", "When Strikes", "When Used", "Constant Effect", 0 - }; + static const char* sEnchantmentTypes[] = { "Cast Once", "When Strikes", "When Used", "Constant Effect", 0 }; - static const char *sBodyPartTypes[] = - { - "Head", "Hair", "Neck", "Chest", "Groin", "Hand", "Wrist", "Forearm", "Upper Arm", - "Foot", "Ankle", "Knee", "Upper Leg", "Clavicle", "Tail", 0 - }; + static const char* sBodyPartTypes[] = { "Head", "Hair", "Neck", "Chest", "Groin", "Hand", "Wrist", "Forearm", + "Upper Arm", "Foot", "Ankle", "Knee", "Upper Leg", "Clavicle", "Tail", 0 }; - static const char *sMeshTypes[] = - { - "Skin", "Clothing", "Armour", 0 - }; + static const char* sMeshTypes[] = { "Skin", "Clothing", "Armour", 0 }; - static const char *sSoundGeneratorType[] = - { - "Left Foot", "Right Foot", "Swim Left", "Swim Right", "Moan", "Roar", "Scream", - "Land", 0 - }; + static const char* sSoundGeneratorType[] + = { "Left Foot", "Right Foot", "Swim Left", "Swim Right", "Moan", "Roar", "Scream", "Land", 0 }; - static const char *sSchools[] = - { - "Alteration", "Conjuration", "Destruction", "Illusion", "Mysticism", "Restoration", 0 - }; + static const char* sSchools[] + = { "Alteration", "Conjuration", "Destruction", "Illusion", "Mysticism", "Restoration", 0 }; - // impact from magic effects, see ESM::Skill::SkillEnum in - static const char *sSkills[] = - { - "Block", "Armorer", "MediumArmor", "HeavyArmor", "BluntWeapon", - "LongBlade", "Axe", "Spear", "Athletics", "Enchant", - "Destruction", "Alteration", "Illusion", "Conjuration", "Mysticism", - "Restoration", "Alchemy", "Unarmored", "Security", "Sneak", - "Acrobatics", "LightArmor", "ShortBlade", "Marksman", "Mercantile", - "Speechcraft", "HandToHand", 0 - }; - - // range of magic effects, see ESM::RangeType in - static const char *sEffectRange[] = - { - "Self", "Touch", "Target", 0 - }; + // impact from magic effects, see ESM::Skill::SkillEnum in + static const char* sSkills[] = { "Block", "Armorer", "MediumArmor", "HeavyArmor", "BluntWeapon", "LongBlade", "Axe", + "Spear", "Athletics", "Enchant", "Destruction", "Alteration", "Illusion", "Conjuration", "Mysticism", + "Restoration", "Alchemy", "Unarmored", "Security", "Sneak", "Acrobatics", "LightArmor", "ShortBlade", + "Marksman", "Mercantile", "Speechcraft", "HandToHand", 0 }; - // magic effect names, see ESM::MagicEffect::Effects in - static const char *sEffectId[] = - { - "WaterBreathing", "SwiftSwim", "WaterWalking", "Shield", "FireShield", - "LightningShield", "FrostShield", "Burden", "Feather", "Jump", - "Levitate", "SlowFall", "Lock", "Open", "FireDamage", - "ShockDamage", "FrostDamage", "DrainAttribute", "DrainHealth", "DrainMagicka", - "DrainFatigue", "DrainSkill", "DamageAttribute", "DamageHealth", "DamageMagicka", - "DamageFatigue", "DamageSkill", "Poison", "WeaknessToFire", "WeaknessToFrost", - "WeaknessToShock", "WeaknessToMagicka", "WeaknessToCommonDisease", "WeaknessToBlightDisease", "WeaknessToCorprusDisease", - "WeaknessToPoison", "WeaknessToNormalWeapons", "DisintegrateWeapon", "DisintegrateArmor", "Invisibility", - "Chameleon", "Light", "Sanctuary", "NightEye", "Charm", - "Paralyze", "Silence", "Blind", "Sound", "CalmHumanoid", - "CalmCreature", "FrenzyHumanoid", "FrenzyCreature", "DemoralizeHumanoid", "DemoralizeCreature", - "RallyHumanoid", "RallyCreature", "Dispel", "Soultrap", "Telekinesis", - "Mark", "Recall", "DivineIntervention", "AlmsiviIntervention", "DetectAnimal", - "DetectEnchantment", "DetectKey", "SpellAbsorption", "Reflect", "CureCommonDisease", - "CureBlightDisease", "CureCorprusDisease", "CurePoison", "CureParalyzation", "RestoreAttribute", - "RestoreHealth", "RestoreMagicka", "RestoreFatigue", "RestoreSkill", "FortifyAttribute", - "FortifyHealth", "FortifyMagicka", "FortifyFatigue", "FortifySkill", "FortifyMaximumMagicka", - "AbsorbAttribute", "AbsorbHealth", "AbsorbMagicka", "AbsorbFatigue", "AbsorbSkill", - "ResistFire", "ResistFrost", "ResistShock", "ResistMagicka", "ResistCommonDisease", + // range of magic effects, see ESM::RangeType in + static const char* sEffectRange[] = { "Self", "Touch", "Target", 0 }; + + // magic effect names, see ESM::MagicEffect::Effects in + static const char* sEffectId[] = { "WaterBreathing", "SwiftSwim", "WaterWalking", "Shield", "FireShield", + "LightningShield", "FrostShield", "Burden", "Feather", "Jump", "Levitate", "SlowFall", "Lock", "Open", + "FireDamage", "ShockDamage", "FrostDamage", "DrainAttribute", "DrainHealth", "DrainMagicka", "DrainFatigue", + "DrainSkill", "DamageAttribute", "DamageHealth", "DamageMagicka", "DamageFatigue", "DamageSkill", "Poison", + "WeaknessToFire", "WeaknessToFrost", "WeaknessToShock", "WeaknessToMagicka", "WeaknessToCommonDisease", + "WeaknessToBlightDisease", "WeaknessToCorprusDisease", "WeaknessToPoison", "WeaknessToNormalWeapons", + "DisintegrateWeapon", "DisintegrateArmor", "Invisibility", "Chameleon", "Light", "Sanctuary", "NightEye", + "Charm", "Paralyze", "Silence", "Blind", "Sound", "CalmHumanoid", "CalmCreature", "FrenzyHumanoid", + "FrenzyCreature", "DemoralizeHumanoid", "DemoralizeCreature", "RallyHumanoid", "RallyCreature", "Dispel", + "Soultrap", "Telekinesis", "Mark", "Recall", "DivineIntervention", "AlmsiviIntervention", "DetectAnimal", + "DetectEnchantment", "DetectKey", "SpellAbsorption", "Reflect", "CureCommonDisease", "CureBlightDisease", + "CureCorprusDisease", "CurePoison", "CureParalyzation", "RestoreAttribute", "RestoreHealth", "RestoreMagicka", + "RestoreFatigue", "RestoreSkill", "FortifyAttribute", "FortifyHealth", "FortifyMagicka", "FortifyFatigue", + "FortifySkill", "FortifyMaximumMagicka", "AbsorbAttribute", "AbsorbHealth", "AbsorbMagicka", "AbsorbFatigue", + "AbsorbSkill", "ResistFire", "ResistFrost", "ResistShock", "ResistMagicka", "ResistCommonDisease", "ResistBlightDisease", "ResistCorprusDisease", "ResistPoison", "ResistNormalWeapons", "ResistParalysis", - "RemoveCurse", "TurnUndead", "SummonScamp", "SummonClannfear", "SummonDaedroth", - "SummonDremora", "SummonAncestralGhost", "SummonSkeletalMinion", "SummonBonewalker", "SummonGreaterBonewalker", - "SummonBonelord", "SummonWingedTwilight", "SummonHunger", "SummonGoldenSaint", "SummonFlameAtronach", - "SummonFrostAtronach", "SummonStormAtronach", "FortifyAttack", "CommandCreature", "CommandHumanoid", - "BoundDagger", "BoundLongsword", "BoundMace", "BoundBattleAxe", "BoundSpear", - "BoundLongbow", "ExtraSpell", "BoundCuirass", "BoundHelm", "BoundBoots", - "BoundShield", "BoundGloves", "Corprus", "Vampirism", "SummonCenturionSphere", - "SunDamage", "StuntedMagicka", "SummonFabricant", "SummonWolf", "SummonBear", - "SummonBonewolf", "SummonCreature04", "SummonCreature05", 0 - }; - - // see ESM::PartReferenceType in - static const char *sPartRefType[] = - { - "Head", "Hair", "Neck", "Cuirass", "Groin", - "Skirt", "Right Hand", "Left Hand", "Right Wrist", "Left Wrist", - "Shield", "Right Forearm", "Left Forearm", "Right Upperarm", "Left Upperarm", - "Right Foot", "Left Foot", "Right Ankle", "Left Ankle", "Right Knee", - "Left Knee", "Right Leg", "Left Leg", "Right Pauldron", "Left Pauldron", - "Weapon", "Tail", 0 - }; - - // see the enums in - static const char *sAiPackageType[] = - { - "AI Wander", "AI Travel", "AI Follow", "AI Escort", "AI Activate", 0 - }; + "RemoveCurse", "TurnUndead", "SummonScamp", "SummonClannfear", "SummonDaedroth", "SummonDremora", + "SummonAncestralGhost", "SummonSkeletalMinion", "SummonBonewalker", "SummonGreaterBonewalker", "SummonBonelord", + "SummonWingedTwilight", "SummonHunger", "SummonGoldenSaint", "SummonFlameAtronach", "SummonFrostAtronach", + "SummonStormAtronach", "FortifyAttack", "CommandCreature", "CommandHumanoid", "BoundDagger", "BoundLongsword", + "BoundMace", "BoundBattleAxe", "BoundSpear", "BoundLongbow", "ExtraSpell", "BoundCuirass", "BoundHelm", + "BoundBoots", "BoundShield", "BoundGloves", "Corprus", "Vampirism", "SummonCenturionSphere", "SunDamage", + "StuntedMagicka", "SummonFabricant", "SummonWolf", "SummonBear", "SummonBonewolf", "SummonCreature04", + "SummonCreature05", 0 }; - static const char *sBookType[] = - { - "Book", "Scroll", 0 - }; + // see ESM::PartReferenceType in + static const char* sPartRefType[] = { "Head", "Hair", "Neck", "Cuirass", "Groin", "Skirt", "Right Hand", + "Left Hand", "Right Wrist", "Left Wrist", "Shield", "Right Forearm", "Left Forearm", "Right Upperarm", + "Left Upperarm", "Right Foot", "Left Foot", "Right Ankle", "Left Ankle", "Right Knee", "Left Knee", "Right Leg", + "Left Leg", "Right Pauldron", "Left Pauldron", "Weapon", "Tail", 0 }; - static const char *sEmitterType[] = - { - "", "Flickering", "Flickering (Slow)", "Pulsing", "Pulsing (Slow)", 0 - }; + // see the enums in + static const char* sAiPackageType[] = { "AI Wander", "AI Travel", "AI Follow", "AI Escort", "AI Activate", 0 }; + + static const char* sBookType[] = { "Book", "Scroll", 0 }; + + static const char* sEmitterType[] = { "", "Flickering", "Flickering (Slow)", "Pulsing", "Pulsing (Slow)", 0 }; - const char **getEnumNames (CSMWorld::Columns::ColumnId column) + const char** getEnumNames(CSMWorld::Columns::ColumnId column) { switch (column) { - case CSMWorld::Columns::ColumnId_Specialisation: return sSpecialisations; - case CSMWorld::Columns::ColumnId_Attribute: return sAttributes; - case CSMWorld::Columns::ColumnId_SpellType: return sSpellTypes; - case CSMWorld::Columns::ColumnId_ApparatusType: return sApparatusTypes; - case CSMWorld::Columns::ColumnId_ArmorType: return sArmorTypes; - case CSMWorld::Columns::ColumnId_ClothingType: return sClothingTypes; - case CSMWorld::Columns::ColumnId_CreatureType: return sCreatureTypes; - case CSMWorld::Columns::ColumnId_WeaponType: return sWeaponTypes; - case CSMWorld::Columns::ColumnId_Modification: return sModificationEnums; - case CSMWorld::Columns::ColumnId_ValueType: return sVarTypeEnums; - case CSMWorld::Columns::ColumnId_DialogueType: return sDialogueTypeEnums; - case CSMWorld::Columns::ColumnId_QuestStatusType: return sQuestStatusTypes; - case CSMWorld::Columns::ColumnId_Gender: return sGenderEnums; - case CSMWorld::Columns::ColumnId_EnchantmentType: return sEnchantmentTypes; - case CSMWorld::Columns::ColumnId_BodyPartType: return sBodyPartTypes; - case CSMWorld::Columns::ColumnId_MeshType: return sMeshTypes; - case CSMWorld::Columns::ColumnId_SoundGeneratorType: return sSoundGeneratorType; - case CSMWorld::Columns::ColumnId_School: return sSchools; - case CSMWorld::Columns::ColumnId_Skill: return sSkills; - case CSMWorld::Columns::ColumnId_EffectRange: return sEffectRange; - case CSMWorld::Columns::ColumnId_EffectId: return sEffectId; - case CSMWorld::Columns::ColumnId_PartRefType: return sPartRefType; - case CSMWorld::Columns::ColumnId_AiPackageType: return sAiPackageType; - case CSMWorld::Columns::ColumnId_InfoCondFunc: return CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings; - case CSMWorld::Columns::ColumnId_InfoCondComp: return CSMWorld::ConstInfoSelectWrapper::RelationEnumStrings; - case CSMWorld::Columns::ColumnId_BookType: return sBookType; - case CSMWorld::Columns::ColumnId_EmitterType: return sEmitterType; - - default: return 0; + case CSMWorld::Columns::ColumnId_Specialisation: + return sSpecialisations; + case CSMWorld::Columns::ColumnId_Attribute: + return sAttributes; + case CSMWorld::Columns::ColumnId_SpellType: + return sSpellTypes; + case CSMWorld::Columns::ColumnId_ApparatusType: + return sApparatusTypes; + case CSMWorld::Columns::ColumnId_ArmorType: + return sArmorTypes; + case CSMWorld::Columns::ColumnId_ClothingType: + return sClothingTypes; + case CSMWorld::Columns::ColumnId_CreatureType: + return sCreatureTypes; + case CSMWorld::Columns::ColumnId_WeaponType: + return sWeaponTypes; + case CSMWorld::Columns::ColumnId_Modification: + return sModificationEnums; + case CSMWorld::Columns::ColumnId_ValueType: + return sVarTypeEnums; + case CSMWorld::Columns::ColumnId_DialogueType: + return sDialogueTypeEnums; + case CSMWorld::Columns::ColumnId_QuestStatusType: + return sQuestStatusTypes; + case CSMWorld::Columns::ColumnId_Gender: + return sGenderEnums; + case CSMWorld::Columns::ColumnId_EnchantmentType: + return sEnchantmentTypes; + case CSMWorld::Columns::ColumnId_BodyPartType: + return sBodyPartTypes; + case CSMWorld::Columns::ColumnId_MeshType: + return sMeshTypes; + case CSMWorld::Columns::ColumnId_SoundGeneratorType: + return sSoundGeneratorType; + case CSMWorld::Columns::ColumnId_School: + return sSchools; + case CSMWorld::Columns::ColumnId_Skill: + return sSkills; + case CSMWorld::Columns::ColumnId_EffectRange: + return sEffectRange; + case CSMWorld::Columns::ColumnId_EffectId: + return sEffectId; + case CSMWorld::Columns::ColumnId_PartRefType: + return sPartRefType; + case CSMWorld::Columns::ColumnId_AiPackageType: + return sAiPackageType; + case CSMWorld::Columns::ColumnId_InfoCondFunc: + return CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings; + case CSMWorld::Columns::ColumnId_InfoCondComp: + return CSMWorld::ConstInfoSelectWrapper::RelationEnumStrings; + case CSMWorld::Columns::ColumnId_BookType: + return sBookType; + case CSMWorld::Columns::ColumnId_EmitterType: + return sEmitterType; + + default: + return 0; } } } -bool CSMWorld::Columns::hasEnums (ColumnId column) +bool CSMWorld::Columns::hasEnums(ColumnId column) { - return getEnumNames (column)!=0 || column==ColumnId_RecordType; + return getEnumNames(column) != 0 || column == ColumnId_RecordType; } -std::vector>CSMWorld::Columns::getEnums (ColumnId column) +std::vector> CSMWorld::Columns::getEnums(ColumnId column) { - std::vector> enums; + std::vector> enums; - if (const char **table = getEnumNames (column)) - for (int i=0; table[i]; ++i) + if (const char** table = getEnumNames(column)) + for (int i = 0; table[i]; ++i) enums.emplace_back(i, table[i]); - else if (column==ColumnId_BloodType) + else if (column == ColumnId_BloodType) { - for (int i=0; i<8; i++) + for (int i = 0; i < 8; i++) { - const std::string& bloodName = Fallback::Map::getString("Blood_Texture_Name_" + std::to_string(i)); + std::string_view bloodName = Fallback::Map::getString("Blood_Texture_Name_" + std::to_string(i)); if (!bloodName.empty()) enums.emplace_back(i, bloodName); } } - else if (column==ColumnId_RecordType) + else if (column == ColumnId_RecordType) { enums.emplace_back(UniversalId::Type_None, ""); // none - for (int i=UniversalId::Type_None+1; i (i)).getTypeName()); + for (int i = UniversalId::Type_None + 1; i < UniversalId::NumberOfTypes; ++i) + enums.emplace_back(i, UniversalId(static_cast(i)).getTypeName()); } return enums; diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index c85eaac5f16..469c1eee33e 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -2,10 +2,9 @@ #define CSM_WOLRD_COLUMNS_H #include +#include #include -#include "columnbase.hpp" - namespace CSMWorld { namespace Columns @@ -46,7 +45,7 @@ namespace CSMWorld ColumnId_FactionIndex = 31, ColumnId_Charges = 32, ColumnId_Enchantment = 33, - ColumnId_CoinValue = 34, + ColumnId_StackCount = 34, ColumnId_Teleport = 35, ColumnId_TeleportCell = 36, ColumnId_LockLevel = 37, @@ -280,7 +279,7 @@ namespace CSMWorld ColumnId_NpcReputation = 258, ColumnId_NpcRank = 259, ColumnId_Gold = 260, - ColumnId_NpcPersistence = 261, + // unused ColumnId_RaceAttributes = 262, ColumnId_Male = 263, @@ -311,14 +310,14 @@ namespace CSMWorld ColumnId_MaxAttack = 284, ColumnId_CreatureMisc = 285, - ColumnId_Idle1 = 286, - ColumnId_Idle2 = 287, - ColumnId_Idle3 = 288, - ColumnId_Idle4 = 289, - ColumnId_Idle5 = 290, - ColumnId_Idle6 = 291, - ColumnId_Idle7 = 292, - ColumnId_Idle8 = 293, + ColumnId_Idle2 = 286, + ColumnId_Idle3 = 287, + ColumnId_Idle4 = 288, + ColumnId_Idle5 = 289, + ColumnId_Idle6 = 290, + ColumnId_Idle7 = 291, + ColumnId_Idle8 = 292, + ColumnId_Idle9 = 293, ColumnId_RegionWeather = 294, ColumnId_WeatherName = 295, @@ -343,6 +342,21 @@ namespace CSMWorld ColumnId_FactionAttrib1 = 311, ColumnId_FactionAttrib2 = 312, + ColumnId_Persistent = 313, + ColumnId_Blocked = 314, + + ColumnId_LevelledCreatureId = 315, + + ColumnId_SelectionGroupObjects = 316, + + ColumnId_SoundProbability = 317, + + ColumnId_IsLocked = 318, + + ColumnId_ProjectileSpeed = 319, + + ColumnId_GoldValue = 320, + // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of use values. ColumnId_UseValue1 = 0x10000, @@ -380,14 +394,14 @@ namespace CSMWorld ColumnId_Skill7 = 0x50006 }; - std::string getName (ColumnId column); + std::string getName(ColumnId column); - int getId (const std::string& name); + int getId(const std::string& name); ///< Will return -1 for an invalid name. - bool hasEnums (ColumnId column); + bool hasEnums(ColumnId column); - std::vector> getEnums (ColumnId column); + std::vector> getEnums(ColumnId column); ///< Returns an empty vector, if \a column isn't an enum type column. } } diff --git a/apps/opencs/model/world/commanddispatcher.cpp b/apps/opencs/model/world/commanddispatcher.cpp index 36b3ba2e002..b211477cafb 100644 --- a/apps/opencs/model/world/commanddispatcher.cpp +++ b/apps/opencs/model/world/commanddispatcher.cpp @@ -1,51 +1,66 @@ #include "commanddispatcher.hpp" #include +#include #include - -#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include #include +#include #include "../doc/document.hpp" -#include "idtable.hpp" -#include "record.hpp" +#include "commandmacro.hpp" #include "commands.hpp" +#include "idtable.hpp" #include "idtableproxymodel.hpp" -#include "commandmacro.hpp" +#include "record.hpp" std::vector CSMWorld::CommandDispatcher::getDeletableRecords() const { std::vector result; - IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); + IdTable& model = dynamic_cast(*mDocument.getData().getTableModel(mId)); - int stateColumnIndex = model.findColumnIndex (Columns::ColumnId_Modification); + int stateColumnIndex = model.findColumnIndex(Columns::ColumnId_Modification); - for (std::vector::const_iterator iter (mSelection.begin()); - iter!=mSelection.end(); ++iter) + for (std::vector::const_iterator iter(mSelection.begin()); iter != mSelection.end(); ++iter) { - int row = model.getModelIndex (*iter, 0).row(); + int row = model.getModelIndex(*iter, 0).row(); // check record state - RecordBase::State state = static_cast ( - model.data (model.index (row, stateColumnIndex)).toInt()); + RecordBase::State state + = static_cast(model.data(model.index(row, stateColumnIndex)).toInt()); - if (state==RecordBase::State_Deleted) + if (state == RecordBase::State_Deleted) continue; // check other columns (only relevant for a subset of the tables) - int dialogueTypeIndex = model.searchColumnIndex (Columns::ColumnId_DialogueType); + int dialogueTypeIndex = model.searchColumnIndex(Columns::ColumnId_DialogueType); - if (dialogueTypeIndex!=-1) + if (dialogueTypeIndex != -1) { - int type = model.data (model.index (row, dialogueTypeIndex)).toInt(); + int type = model.data(model.index(row, dialogueTypeIndex)).toInt(); - if (type!=ESM::Dialogue::Topic && type!=ESM::Dialogue::Journal) + if (type != ESM::Dialogue::Topic && type != ESM::Dialogue::Journal) continue; } - result.push_back (*iter); + result.push_back(*iter); } return result; @@ -55,51 +70,53 @@ std::vector CSMWorld::CommandDispatcher::getRevertableRecords() con { std::vector result; - IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); + IdTable& model = dynamic_cast(*mDocument.getData().getTableModel(mId)); /// \todo Reverting temporarily disabled on tables that support reordering, because /// revert logic currently can not handle reordering. if (model.getFeatures() & IdTable::Feature_ReorderWithinTopic) return result; - int stateColumnIndex = model.findColumnIndex (Columns::ColumnId_Modification); + int stateColumnIndex = model.findColumnIndex(Columns::ColumnId_Modification); - for (std::vector::const_iterator iter (mSelection.begin()); - iter!=mSelection.end(); ++iter) + for (std::vector::const_iterator iter(mSelection.begin()); iter != mSelection.end(); ++iter) { - int row = model.getModelIndex (*iter, 0).row(); + int row = model.getModelIndex(*iter, 0).row(); // check record state - RecordBase::State state = static_cast ( - model.data (model.index (row, stateColumnIndex)).toInt()); + RecordBase::State state + = static_cast(model.data(model.index(row, stateColumnIndex)).toInt()); - if (state==RecordBase::State_BaseOnly) + if (state == RecordBase::State_BaseOnly) continue; - result.push_back (*iter); + result.push_back(*iter); } return result; } -CSMWorld::CommandDispatcher::CommandDispatcher (CSMDoc::Document& document, - const CSMWorld::UniversalId& id, QObject *parent) -: QObject (parent), mLocked (false), mDocument (document), mId (id) -{} +CSMWorld::CommandDispatcher::CommandDispatcher( + CSMDoc::Document& document, const CSMWorld::UniversalId& id, QObject* parent) + : QObject(parent) + , mLocked(false) + , mDocument(document) + , mId(id) +{ +} -void CSMWorld::CommandDispatcher::setEditLock (bool locked) +void CSMWorld::CommandDispatcher::setEditLock(bool locked) { mLocked = locked; } -void CSMWorld::CommandDispatcher::setSelection (const std::vector& selection) +void CSMWorld::CommandDispatcher::setSelection(const std::vector& selection) { mSelection = selection; - std::for_each (mSelection.begin(), mSelection.end(), Misc::StringUtils::lowerCaseInPlace); - std::sort (mSelection.begin(), mSelection.end()); + std::sort(mSelection.begin(), mSelection.end(), Misc::StringUtils::CiComp{}); } -void CSMWorld::CommandDispatcher::setExtendedTypes (const std::vector& types) +void CSMWorld::CommandDispatcher::setExtendedTypes(const std::vector& types) { mExtendedTypes = types; } @@ -109,7 +126,7 @@ bool CSMWorld::CommandDispatcher::canDelete() const if (mLocked) return false; - return getDeletableRecords().size()!=0; + return getDeletableRecords().size() != 0; } bool CSMWorld::CommandDispatcher::canRevert() const @@ -117,16 +134,16 @@ bool CSMWorld::CommandDispatcher::canRevert() const if (mLocked) return false; - return getRevertableRecords().size()!=0; + return getRevertableRecords().size() != 0; } std::vector CSMWorld::CommandDispatcher::getExtendedTypes() const { std::vector tables; - if (mId==UniversalId::Type_Cells) + if (mId == UniversalId::Type_Cells) { - tables.push_back (mId); + tables.push_back(mId); tables.emplace_back(UniversalId::Type_References); /// \todo add other cell-specific types } @@ -134,68 +151,59 @@ std::vector CSMWorld::CommandDispatcher::getExtendedTypes return tables; } -void CSMWorld::CommandDispatcher::executeModify (QAbstractItemModel *model, const QModelIndex& index, const QVariant& new_) +void CSMWorld::CommandDispatcher::executeModify( + QAbstractItemModel* model, const QModelIndex& index, const QVariant& new_) { if (mLocked) return; std::unique_ptr modifyCell; - std::unique_ptr modifyDataRefNum; + int columnId = model->data(index, ColumnBase::Role_ColumnId).toInt(); - int columnId = model->data (index, ColumnBase::Role_ColumnId).toInt(); - - if (columnId==Columns::ColumnId_PositionXPos || columnId==Columns::ColumnId_PositionYPos) + if (columnId == Columns::ColumnId_PositionXPos || columnId == Columns::ColumnId_PositionYPos) { - const float oldPosition = model->data (index).toFloat(); + const float oldPosition = model->data(index).toFloat(); // Modulate by cell size, update cell id if reference has been moved to a new cell if (std::abs(std::fmod(oldPosition, Constants::CellSizeInUnits)) - - std::abs(std::fmod(new_.toFloat(), Constants::CellSizeInUnits)) >= 0.5f) + - std::abs(std::fmod(new_.toFloat(), Constants::CellSizeInUnits)) + >= 0.5f) { - IdTableProxyModel *proxy = dynamic_cast (model); + IdTableProxyModel* proxy = dynamic_cast(model); - int row = proxy ? proxy->mapToSource (index).row() : index.row(); + int row = proxy ? proxy->mapToSource(index).row() : index.row(); // This is not guaranteed to be the same as \a model, since a proxy could be used. - IdTable& model2 = dynamic_cast (*mDocument.getData().getTableModel (mId)); + IdTable& model2 = dynamic_cast(*mDocument.getData().getTableModel(mId)); - int cellColumn = model2.searchColumnIndex (Columns::ColumnId_Cell); + int cellColumn = model2.searchColumnIndex(Columns::ColumnId_Cell); - if (cellColumn!=-1) + if (cellColumn != -1) { - QModelIndex cellIndex = model2.index (row, cellColumn); + QModelIndex cellIndex = model2.index(row, cellColumn); - std::string cellId = model2.data (cellIndex).toString().toUtf8().data(); + std::string cellId = model2.data(cellIndex).toString().toUtf8().data(); - if (cellId.find ('#')!=std::string::npos) + if (cellId.find('#') != std::string::npos) { - // Need to recalculate the cell and (if necessary) clear the instance's refNum - modifyCell.reset (new UpdateCellCommand (model2, row)); - - // Not sure which model this should be applied to - int refNumColumn = model2.searchColumnIndex (Columns::ColumnId_RefNum); - - if (refNumColumn!=-1) - modifyDataRefNum.reset (new ModifyCommand(*model, model->index(row, refNumColumn), 0)); + // Need to recalculate the cell + modifyCell = std::make_unique(model2, row); } } } } - std::unique_ptr modifyData ( - new CSMWorld::ModifyCommand (*model, index, new_)); + auto modifyData = std::make_unique(*model, index, new_); if (modifyCell.get()) { - CommandMacro macro (mDocument.getUndoStack()); - macro.push (modifyData.release()); - macro.push (modifyCell.release()); - if (modifyDataRefNum.get()) - macro.push (modifyDataRefNum.release()); + CommandMacro macro(mDocument.getUndoStack()); + macro.push(modifyData.release()); + macro.push(modifyCell.release()); } else - mDocument.getUndoStack().push (modifyData.release()); + mDocument.getUndoStack().push(modifyData.release()); } void CSMWorld::CommandDispatcher::executeDelete() @@ -208,25 +216,26 @@ void CSMWorld::CommandDispatcher::executeDelete() if (rows.empty()) return; - IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); + IdTable& model = dynamic_cast(*mDocument.getData().getTableModel(mId)); - int columnIndex = model.findColumnIndex (Columns::ColumnId_Id); + int columnIndex = model.findColumnIndex(Columns::ColumnId_Id); - CommandMacro macro (mDocument.getUndoStack(), rows.size()>1 ? "Delete multiple records" : ""); - for (std::vector::const_iterator iter (rows.begin()); iter!=rows.end(); ++iter) + CommandMacro macro(mDocument.getUndoStack(), rows.size() > 1 ? "Delete multiple records" : ""); + for (std::vector::const_iterator iter(rows.begin()); iter != rows.end(); ++iter) { - std::string id = model.data (model.getModelIndex (*iter, columnIndex)). - toString().toUtf8().constData(); + std::string id = model.data(model.getModelIndex(*iter, columnIndex)).toString().toUtf8().constData(); if (mId.getType() == UniversalId::Type_Referenceables) { - macro.push (new CSMWorld::DeleteCommand (model, id, - static_cast(model.data (model.index ( - model.getModelIndex (id, columnIndex).row(), - model.findColumnIndex (CSMWorld::Columns::ColumnId_RecordType))).toInt()))); + macro.push(new CSMWorld::DeleteCommand(model, id, + static_cast( + model + .data(model.index(model.getModelIndex(id, columnIndex).row(), + model.findColumnIndex(CSMWorld::Columns::ColumnId_RecordType))) + .toInt()))); } else - mDocument.getUndoStack().push (new CSMWorld::DeleteCommand (model, id)); + mDocument.getUndoStack().push(new CSMWorld::DeleteCommand(model, id)); } } @@ -240,50 +249,47 @@ void CSMWorld::CommandDispatcher::executeRevert() if (rows.empty()) return; - IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); + IdTable& model = dynamic_cast(*mDocument.getData().getTableModel(mId)); - int columnIndex = model.findColumnIndex (Columns::ColumnId_Id); + int columnIndex = model.findColumnIndex(Columns::ColumnId_Id); - CommandMacro macro (mDocument.getUndoStack(), rows.size()>1 ? "Revert multiple records" : ""); - for (std::vector::const_iterator iter (rows.begin()); iter!=rows.end(); ++iter) + CommandMacro macro(mDocument.getUndoStack(), rows.size() > 1 ? "Revert multiple records" : ""); + for (std::vector::const_iterator iter(rows.begin()); iter != rows.end(); ++iter) { - std::string id = model.data (model.getModelIndex (*iter, columnIndex)). - toString().toUtf8().constData(); + std::string id = model.data(model.getModelIndex(*iter, columnIndex)).toString().toUtf8().constData(); - macro.push (new CSMWorld::RevertCommand (model, id)); + macro.push(new CSMWorld::RevertCommand(model, id)); } } void CSMWorld::CommandDispatcher::executeExtendedDelete() { - CommandMacro macro (mDocument.getUndoStack(), mExtendedTypes.size()>1 ? tr ("Extended delete of multiple records") : ""); + CommandMacro macro( + mDocument.getUndoStack(), mExtendedTypes.size() > 1 ? tr("Extended delete of multiple records") : ""); - for (std::vector::const_iterator iter (mExtendedTypes.begin()); - iter!=mExtendedTypes.end(); ++iter) + for (std::vector::const_iterator iter(mExtendedTypes.begin()); iter != mExtendedTypes.end(); ++iter) { - if (*iter==mId) + if (*iter == mId) executeDelete(); - else if (*iter==UniversalId::Type_References) + else if (*iter == UniversalId::Type_References) { - IdTable& model = dynamic_cast ( - *mDocument.getData().getTableModel (*iter)); + IdTable& model = dynamic_cast(*mDocument.getData().getTableModel(*iter)); const RefCollection& collection = mDocument.getData().getReferences(); int size = collection.getSize(); - for (int i=size-1; i>=0; --i) + for (int i = size - 1; i >= 0; --i) { - const Record& record = collection.getRecord (i); + const Record& record = collection.getRecord(i); - if (record.mState==RecordBase::State_Deleted) + if (record.mState == RecordBase::State_Deleted) continue; - if (!std::binary_search (mSelection.begin(), mSelection.end(), - Misc::StringUtils::lowerCase (record.get().mCell))) + if (!std::binary_search(mSelection.begin(), mSelection.end(), record.get().mCell)) continue; - macro.push (new CSMWorld::DeleteCommand (model, record.get().mId)); + macro.push(new CSMWorld::DeleteCommand(model, record.get().mId.getRefIdString())); } } } @@ -291,31 +297,29 @@ void CSMWorld::CommandDispatcher::executeExtendedDelete() void CSMWorld::CommandDispatcher::executeExtendedRevert() { - CommandMacro macro (mDocument.getUndoStack(), mExtendedTypes.size()>1 ? tr ("Extended revert of multiple records") : ""); + CommandMacro macro( + mDocument.getUndoStack(), mExtendedTypes.size() > 1 ? tr("Extended revert of multiple records") : ""); - for (std::vector::const_iterator iter (mExtendedTypes.begin()); - iter!=mExtendedTypes.end(); ++iter) + for (std::vector::const_iterator iter(mExtendedTypes.begin()); iter != mExtendedTypes.end(); ++iter) { - if (*iter==mId) + if (*iter == mId) executeRevert(); - else if (*iter==UniversalId::Type_References) + else if (*iter == UniversalId::Type_References) { - IdTable& model = dynamic_cast ( - *mDocument.getData().getTableModel (*iter)); + IdTable& model = dynamic_cast(*mDocument.getData().getTableModel(*iter)); const RefCollection& collection = mDocument.getData().getReferences(); int size = collection.getSize(); - for (int i=size-1; i>=0; --i) + for (int i = size - 1; i >= 0; --i) { - const Record& record = collection.getRecord (i); + const Record& record = collection.getRecord(i); - if (!std::binary_search (mSelection.begin(), mSelection.end(), - Misc::StringUtils::lowerCase (record.get().mCell))) + if (!std::binary_search(mSelection.begin(), mSelection.end(), record.get().mCell)) continue; - macro.push (new CSMWorld::RevertCommand (model, record.get().mId)); + macro.push(new CSMWorld::RevertCommand(model, record.get().mId.getRefIdString())); } } } diff --git a/apps/opencs/model/world/commanddispatcher.hpp b/apps/opencs/model/world/commanddispatcher.hpp index 538fd7f1884..46faa4524f8 100644 --- a/apps/opencs/model/world/commanddispatcher.hpp +++ b/apps/opencs/model/world/commanddispatcher.hpp @@ -1,9 +1,13 @@ #ifndef CSM_WOLRD_COMMANDDISPATCHER_H #define CSM_WOLRD_COMMANDDISPATCHER_H +#include #include +#include +#include #include +#include #include "universalid.hpp" @@ -19,59 +23,56 @@ namespace CSMWorld { class CommandDispatcher : public QObject { - Q_OBJECT + Q_OBJECT - bool mLocked; - CSMDoc::Document& mDocument; - UniversalId mId; - std::vector mSelection; - std::vector mExtendedTypes; + bool mLocked; + CSMDoc::Document& mDocument; + UniversalId mId; + std::vector mSelection; + std::vector mExtendedTypes; - std::vector getDeletableRecords() const; + std::vector getDeletableRecords() const; - std::vector getRevertableRecords() const; + std::vector getRevertableRecords() const; - public: + public: + CommandDispatcher(CSMDoc::Document& document, const CSMWorld::UniversalId& id, QObject* parent = nullptr); + ///< \param id ID of the table the commands should operate on primarily. - CommandDispatcher (CSMDoc::Document& document, const CSMWorld::UniversalId& id, - QObject *parent = nullptr); - ///< \param id ID of the table the commands should operate on primarily. + void setEditLock(bool locked); - void setEditLock (bool locked); + void setSelection(const std::vector& selection); - void setSelection (const std::vector& selection); + void setExtendedTypes(const std::vector& types); + ///< Set record lists selected by the user for extended operations. - void setExtendedTypes (const std::vector& types); - ///< Set record lists selected by the user for extended operations. + bool canDelete() const; - bool canDelete() const; + bool canRevert() const; - bool canRevert() const; + /// Return IDs of the record collection that can also be affected when + /// operating on the record collection this dispatcher is used for. + /// + /// \note The returned collection contains the ID of the record collection this + /// dispatcher is used for. However if that record collection does not support + /// the extended mode, the returned vector will be empty instead. + std::vector getExtendedTypes() const; - /// Return IDs of the record collection that can also be affected when - /// operating on the record collection this dispatcher is used for. - /// - /// \note The returned collection contains the ID of the record collection this - /// dispatcher is used for. However if that record collection does not support - /// the extended mode, the returned vector will be empty instead. - std::vector getExtendedTypes() const; + /// Add a modify command to the undo stack. + /// + /// \attention model must either be a model for the table operated on by this + /// dispatcher or a proxy of it. + void executeModify(QAbstractItemModel* model, const QModelIndex& index, const QVariant& new_); - /// Add a modify command to the undo stack. - /// - /// \attention model must either be a model for the table operated on by this - /// dispatcher or a proxy of it. - void executeModify (QAbstractItemModel *model, const QModelIndex& index, const QVariant& new_); + public slots: - public slots: + void executeDelete(); - void executeDelete(); + void executeRevert(); - void executeRevert(); - - void executeExtendedDelete(); - - void executeExtendedRevert(); + void executeExtendedDelete(); + void executeExtendedRevert(); }; } diff --git a/apps/opencs/model/world/commandmacro.cpp b/apps/opencs/model/world/commandmacro.cpp index 0bd74cbe211..2c3b7e1726d 100644 --- a/apps/opencs/model/world/commandmacro.cpp +++ b/apps/opencs/model/world/commandmacro.cpp @@ -1,12 +1,15 @@ #include "commandmacro.hpp" -#include #include +#include -CSMWorld::CommandMacro::CommandMacro (QUndoStack& undoStack, const QString& description) -: mUndoStack (undoStack), mDescription (description), mStarted (false) -{} +CSMWorld::CommandMacro::CommandMacro(QUndoStack& undoStack, const QString& description) + : mUndoStack(undoStack) + , mDescription(description) + , mStarted(false) +{ +} CSMWorld::CommandMacro::~CommandMacro() { @@ -14,13 +17,13 @@ CSMWorld::CommandMacro::~CommandMacro() mUndoStack.endMacro(); } -void CSMWorld::CommandMacro::push (QUndoCommand *command) +void CSMWorld::CommandMacro::push(QUndoCommand* command) { if (!mStarted) { - mUndoStack.beginMacro (mDescription.isEmpty() ? command->text() : mDescription); + mUndoStack.beginMacro(mDescription.isEmpty() ? command->text() : mDescription); mStarted = true; } - mUndoStack.push (command); + mUndoStack.push(command); } diff --git a/apps/opencs/model/world/commandmacro.hpp b/apps/opencs/model/world/commandmacro.hpp index b1f6301d9ae..bc21e935c0b 100644 --- a/apps/opencs/model/world/commandmacro.hpp +++ b/apps/opencs/model/world/commandmacro.hpp @@ -10,24 +10,23 @@ namespace CSMWorld { class CommandMacro { - QUndoStack& mUndoStack; - QString mDescription; - bool mStarted; + QUndoStack& mUndoStack; + QString mDescription; + bool mStarted; - /// not implemented - CommandMacro (const CommandMacro&); + /// not implemented + CommandMacro(const CommandMacro&); - /// not implemented - CommandMacro& operator= (const CommandMacro&); + /// not implemented + CommandMacro& operator=(const CommandMacro&); - public: + public: + /// If \a description is empty, the description of the first command is used. + CommandMacro(QUndoStack& undoStack, const QString& description = ""); - /// If \a description is empty, the description of the first command is used. - CommandMacro (QUndoStack& undoStack, const QString& description = ""); + ~CommandMacro(); - ~CommandMacro(); - - void push (QUndoCommand *command); + void push(QUndoCommand* command); }; } diff --git a/apps/opencs/model/world/commands.cpp b/apps/opencs/model/world/commands.cpp index 5ec7401dc9e..168049641a3 100644 --- a/apps/opencs/model/world/commands.cpp +++ b/apps/opencs/model/world/commands.cpp @@ -1,16 +1,23 @@ #include "commands.hpp" +#include #include #include #include -#include +#include +#include +#include +#include + +#include +#include +#include #include #include #include "cellcoordinates.hpp" -#include "idcollection.hpp" #include "idtable.hpp" #include "idtree.hpp" #include "nestedtablewrapper.hpp" @@ -24,11 +31,11 @@ CSMWorld::TouchCommand::TouchCommand(IdTable& table, const std::string& id, QUnd , mChanged(false) { setText(("Touch " + mId).c_str()); - mOld.reset(mTable.getRecord(mId).clone()); } void CSMWorld::TouchCommand::redo() { + mOld = mTable.getRecord(mId).clone(); mChanged = mTable.touchRecord(mId); } @@ -36,13 +43,13 @@ void CSMWorld::TouchCommand::undo() { if (mChanged) { - mTable.setRecord(mId, *mOld); + mTable.setRecord(mId, std::move(mOld)); mChanged = false; } } -CSMWorld::ImportLandTexturesCommand::ImportLandTexturesCommand(IdTable& landTable, - IdTable& ltexTable, QUndoCommand* parent) +CSMWorld::ImportLandTexturesCommand::ImportLandTexturesCommand( + IdTable& landTable, IdTable& ltexTable, QUndoCommand* parent) : QUndoCommand(parent) , mLands(landTable) , mLtexs(ltexTable) @@ -53,11 +60,11 @@ CSMWorld::ImportLandTexturesCommand::ImportLandTexturesCommand(IdTable& landTabl void CSMWorld::ImportLandTexturesCommand::redo() { - int pluginColumn = mLands.findColumnIndex(Columns::ColumnId_PluginIndex); - int oldPlugin = mLands.data(mLands.getModelIndex(getOriginId(), pluginColumn)).toInt(); + const int pluginColumn = mLands.findColumnIndex(Columns::ColumnId_PluginIndex); + const int oldPlugin = mLands.data(mLands.getModelIndex(getOriginId(), pluginColumn)).toInt(); // Original data - int textureColumn = mLands.findColumnIndex(Columns::ColumnId_LandTexturesIndex); + const int textureColumn = mLands.findColumnIndex(Columns::ColumnId_LandTexturesIndex); mOld = mLands.data(mLands.getModelIndex(getOriginId(), textureColumn)).value(); // Need to make a copy so the old values can be looked up @@ -66,44 +73,37 @@ void CSMWorld::ImportLandTexturesCommand::redo() // Perform touch/copy/etc... onRedo(); - // Find all indices used - std::unordered_set texIndices; - for (int i = 0; i < mOld.size(); ++i) + std::unordered_map indexMapping; + for (uint16_t index : mOld) { // All indices are offset by 1 for a default texture - if (mOld[i] > 0) - texIndices.insert(mOld[i] - 1); - } - - std::vector oldTextures; - oldTextures.reserve(texIndices.size()); - for (int index : texIndices) - { - oldTextures.push_back(LandTexture::createUniqueRecordId(oldPlugin, index)); - } - - // Import the textures, replace old values - LandTextureIdTable::ImportResults results = dynamic_cast(mLtexs).importTextures(oldTextures); - mCreatedTextures = std::move(results.createdRecords); - for (const auto& it : results.recordMapping) - { - int plugin = 0, newIndex = 0, oldIndex = 0; - LandTexture::parseUniqueRecordId(it.first, plugin, oldIndex); - LandTexture::parseUniqueRecordId(it.second, plugin, newIndex); - - if (newIndex != oldIndex) + if (index == 0) + continue; + if (indexMapping.contains(index)) + continue; + const CSMWorld::Record* record + = static_cast(mLtexs).searchRecord(index - 1, oldPlugin); + if (!record || record->isDeleted()) + { + indexMapping.emplace(index, 0); + continue; + } + if (!record->isModified()) { - for (int i = 0; i < Land::LAND_NUM_TEXTURES; ++i) - { - // All indices are offset by 1 for a default texture - if (mOld[i] == oldIndex + 1) - copy[i] = newIndex + 1; - } + mTouchedTextures.emplace_back(record->clone()); + mLtexs.touchRecord(record->get().mId.getRefIdString()); } + indexMapping.emplace(index, record->get().mIndex + 1); + } + for (int i = 0; i < Land::LAND_NUM_TEXTURES; ++i) + { + uint16_t oldIndex = mOld[i]; + uint16_t newIndex = indexMapping[oldIndex]; + copy[i] = newIndex; } // Apply modification - int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification); + const int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification); mOldState = mLands.data(mLands.getModelIndex(getDestinationId(), stateColumn)).toInt(); QVariant variant; @@ -125,16 +125,16 @@ void CSMWorld::ImportLandTexturesCommand::undo() // Undo copy/touch/etc... onUndo(); - for (const std::string& id : mCreatedTextures) + for (auto& ltex : mTouchedTextures) { - int row = mLtexs.getModelIndex(id, 0).row(); - mLtexs.removeRows(row, 1); + ESM::RefId id = static_cast*>(ltex.get())->get().mId; + mLtexs.setRecord(id.getRefIdString(), std::move(ltex)); } - mCreatedTextures.clear(); + mTouchedTextures.clear(); } -CSMWorld::CopyLandTexturesCommand::CopyLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, - const std::string& origin, const std::string& dest, QUndoCommand* parent) +CSMWorld::CopyLandTexturesCommand::CopyLandTexturesCommand( + IdTable& landTable, IdTable& ltexTable, const std::string& origin, const std::string& dest, QUndoCommand* parent) : ImportLandTexturesCommand(landTable, ltexTable, parent) , mOriginId(origin) , mDestId(dest) @@ -151,15 +151,14 @@ const std::string& CSMWorld::CopyLandTexturesCommand::getDestinationId() const return mDestId; } -CSMWorld::TouchLandCommand::TouchLandCommand(IdTable& landTable, IdTable& ltexTable, - const std::string& id, QUndoCommand* parent) +CSMWorld::TouchLandCommand::TouchLandCommand( + IdTable& landTable, IdTable& ltexTable, const std::string& id, QUndoCommand* parent) : ImportLandTexturesCommand(landTable, ltexTable, parent) , mId(id) , mOld(nullptr) , mChanged(false) { setText(("Touch " + mId).c_str()); - mOld.reset(mLands.getRecord(mId).clone()); } const std::string& CSMWorld::TouchLandCommand::getOriginId() const @@ -174,6 +173,7 @@ const std::string& CSMWorld::TouchLandCommand::getDestinationId() const void CSMWorld::TouchLandCommand::onRedo() { + mOld = mLands.getRecord(mId).clone(); mChanged = mLands.touchRecord(mId); } @@ -181,35 +181,44 @@ void CSMWorld::TouchLandCommand::onUndo() { if (mChanged) { - mLands.setRecord(mId, *mOld); + mLands.setRecord(mId, std::move(mOld)); mChanged = false; } } -CSMWorld::ModifyCommand::ModifyCommand (QAbstractItemModel& model, const QModelIndex& index, - const QVariant& new_, QUndoCommand* parent) - : QUndoCommand (parent), mModel (&model), mIndex (index), mNew (new_), mHasRecordState(false), mOldRecordState(CSMWorld::RecordBase::State_BaseOnly) +CSMWorld::ModifyCommand::ModifyCommand( + QAbstractItemModel& model, const QModelIndex& index, const QVariant& new_, QUndoCommand* parent) + : QUndoCommand(parent) + , mModel(&model) + , mIndex(index) + , mNew(new_) + , mHasRecordState(false) + , mOldRecordState(CSMWorld::RecordBase::State_BaseOnly) { - if (QAbstractProxyModel *proxy = dynamic_cast (&model)) + if (QAbstractProxyModel* proxy = dynamic_cast(mModel)) { // Replace proxy with actual model - mIndex = proxy->mapToSource (index); + mIndex = proxy->mapToSource(mIndex); mModel = proxy->sourceModel(); } +} +void CSMWorld::ModifyCommand::redo() +{ if (mIndex.parent().isValid()) { CSMWorld::IdTree* tree = &dynamic_cast(*mModel); - setText ("Modify " + tree->nestedHeaderData ( - mIndex.parent().column(), mIndex.column(), Qt::Horizontal, Qt::DisplayRole).toString()); + setText("Modify " + + tree->nestedHeaderData(mIndex.parent().column(), mIndex.column(), Qt::Horizontal, Qt::DisplayRole) + .toString()); } else { - setText ("Modify " + mModel->headerData (mIndex.column(), Qt::Horizontal, Qt::DisplayRole).toString()); + setText("Modify " + mModel->headerData(mIndex.column(), Qt::Horizontal, Qt::DisplayRole).toString()); } // Remember record state before the modification - if (CSMWorld::IdTable *table = dynamic_cast(mModel)) + if (CSMWorld::IdTable* table = dynamic_cast(mModel)) { mHasRecordState = true; int stateColumnIndex = table->findColumnIndex(Columns::ColumnId_Modification); @@ -223,190 +232,183 @@ CSMWorld::ModifyCommand::ModifyCommand (QAbstractItemModel& model, const QModelI mRecordStateIndex = table->index(rowIndex, stateColumnIndex); mOldRecordState = static_cast(table->data(mRecordStateIndex).toInt()); } -} -void CSMWorld::ModifyCommand::redo() -{ - mOld = mModel->data (mIndex, Qt::EditRole); - mModel->setData (mIndex, mNew); + mOld = mModel->data(mIndex, Qt::EditRole); + mModel->setData(mIndex, mNew); } void CSMWorld::ModifyCommand::undo() { - mModel->setData (mIndex, mOld); + mModel->setData(mIndex, mOld); if (mHasRecordState) { mModel->setData(mRecordStateIndex, mOldRecordState); } } - void CSMWorld::CreateCommand::applyModifications() { if (!mNestedValues.empty()) { CSMWorld::IdTree* tree = &dynamic_cast(mModel); - std::map >::const_iterator current = mNestedValues.begin(); - std::map >::const_iterator end = mNestedValues.end(); + std::map>::const_iterator current = mNestedValues.begin(); + std::map>::const_iterator end = mNestedValues.end(); for (; current != end; ++current) { - QModelIndex index = tree->index(0, - current->second.first, - tree->getNestedModelIndex(mId, current->first)); + QModelIndex index = tree->index(0, current->second.first, tree->getNestedModelIndex(mId, current->first)); tree->setData(index, current->second.second); } } } -CSMWorld::CreateCommand::CreateCommand (IdTable& model, const std::string& id, QUndoCommand* parent) -: QUndoCommand (parent), mModel (model), mId (id), mType (UniversalId::Type_None) +CSMWorld::CreateCommand::CreateCommand(IdTable& model, const std::string& id, QUndoCommand* parent) + : QUndoCommand(parent) + , mModel(model) + , mId(id) + , mType(UniversalId::Type_None) { - setText (("Create record " + id).c_str()); + setText(("Create record " + id).c_str()); } -void CSMWorld::CreateCommand::addValue (int column, const QVariant& value) +void CSMWorld::CreateCommand::addValue(int column, const QVariant& value) { mValues[column] = value; } -void CSMWorld::CreateCommand::addNestedValue(int parentColumn, int nestedColumn, const QVariant &value) +void CSMWorld::CreateCommand::addNestedValue(int parentColumn, int nestedColumn, const QVariant& value) { mNestedValues[parentColumn] = std::make_pair(nestedColumn, value); } -void CSMWorld::CreateCommand::setType (UniversalId::Type type) +void CSMWorld::CreateCommand::setType(UniversalId::Type type) { mType = type; } void CSMWorld::CreateCommand::redo() { - mModel.addRecordWithData (mId, mValues, mType); + mModel.addRecordWithData(mId, mValues, mType); applyModifications(); } void CSMWorld::CreateCommand::undo() { - mModel.removeRow (mModel.getModelIndex (mId, 0).row()); -} - -CSMWorld::RevertCommand::RevertCommand (IdTable& model, const std::string& id, QUndoCommand* parent) -: QUndoCommand (parent), mModel (model), mId (id), mOld (nullptr) -{ - setText (("Revert record " + id).c_str()); - - mOld = model.getRecord (id).clone(); + mModel.removeRow(mModel.getModelIndex(mId, 0).row()); } -CSMWorld::RevertCommand::~RevertCommand() +CSMWorld::RevertCommand::RevertCommand(IdTable& model, const std::string& id, QUndoCommand* parent) + : QUndoCommand(parent) + , mModel(model) + , mId(id) + , mOld(nullptr) { - delete mOld; + setText(("Revert record " + id).c_str()); } void CSMWorld::RevertCommand::redo() { - int column = mModel.findColumnIndex (Columns::ColumnId_Modification); + mOld = mModel.getRecord(mId).clone(); + + int column = mModel.findColumnIndex(Columns::ColumnId_Modification); - QModelIndex index = mModel.getModelIndex (mId, column); - RecordBase::State state = static_cast (mModel.data (index).toInt()); + QModelIndex index = mModel.getModelIndex(mId, column); + RecordBase::State state = static_cast(mModel.data(index).toInt()); - if (state==RecordBase::State_ModifiedOnly) + if (state == RecordBase::State_ModifiedOnly) { - mModel.removeRows (index.row(), 1); + mModel.removeRows(index.row(), 1); } else { - mModel.setData (index, static_cast (RecordBase::State_BaseOnly)); + mModel.setData(index, static_cast(RecordBase::State_BaseOnly)); } } void CSMWorld::RevertCommand::undo() { - mModel.setRecord (mId, *mOld); -} - -CSMWorld::DeleteCommand::DeleteCommand (IdTable& model, - const std::string& id, CSMWorld::UniversalId::Type type, QUndoCommand* parent) -: QUndoCommand (parent), mModel (model), mId (id), mOld (nullptr), mType(type) -{ - setText (("Delete record " + id).c_str()); - - mOld = model.getRecord (id).clone(); + mModel.setRecord(mId, std::move(mOld)); } -CSMWorld::DeleteCommand::~DeleteCommand() +CSMWorld::DeleteCommand::DeleteCommand( + IdTable& model, const std::string& id, CSMWorld::UniversalId::Type type, QUndoCommand* parent) + : QUndoCommand(parent) + , mModel(model) + , mId(id) + , mOld(nullptr) + , mType(type) { - delete mOld; + setText(("Delete record " + id).c_str()); } void CSMWorld::DeleteCommand::redo() { - int column = mModel.findColumnIndex (Columns::ColumnId_Modification); + mOld = mModel.getRecord(mId).clone(); - QModelIndex index = mModel.getModelIndex (mId, column); - RecordBase::State state = static_cast (mModel.data (index).toInt()); + int column = mModel.findColumnIndex(Columns::ColumnId_Modification); - if (state==RecordBase::State_ModifiedOnly) + QModelIndex index = mModel.getModelIndex(mId, column); + RecordBase::State state = static_cast(mModel.data(index).toInt()); + + if (state == RecordBase::State_ModifiedOnly) { - mModel.removeRows (index.row(), 1); + mModel.removeRows(index.row(), 1); } else { - mModel.setData (index, static_cast (RecordBase::State_Deleted)); + mModel.setData(index, static_cast(RecordBase::State_Deleted)); } } void CSMWorld::DeleteCommand::undo() { - mModel.setRecord (mId, *mOld, mType); + mModel.setRecord(mId, std::move(mOld), mType); } - -CSMWorld::ReorderRowsCommand::ReorderRowsCommand (IdTable& model, int baseIndex, - const std::vector& newOrder) -: mModel (model), mBaseIndex (baseIndex), mNewOrder (newOrder) -{} +CSMWorld::ReorderRowsCommand::ReorderRowsCommand(IdTable& model, int baseIndex, const std::vector& newOrder) + : mModel(model) + , mBaseIndex(baseIndex) + , mNewOrder(newOrder) +{ +} void CSMWorld::ReorderRowsCommand::redo() { - mModel.reorderRows (mBaseIndex, mNewOrder); + mModel.reorderRows(mBaseIndex, mNewOrder); } void CSMWorld::ReorderRowsCommand::undo() { - int size = static_cast (mNewOrder.size()); - std::vector reverse (size); + int size = static_cast(mNewOrder.size()); + std::vector reverse(size); - for (int i=0; i< size; ++i) - reverse.at (mNewOrder[i]) = i; + for (int i = 0; i < size; ++i) + reverse.at(mNewOrder[i]) = i; - mModel.reorderRows (mBaseIndex, reverse); + mModel.reorderRows(mBaseIndex, reverse); } -CSMWorld::CloneCommand::CloneCommand (CSMWorld::IdTable& model, - const std::string& idOrigin, - const std::string& idDestination, - const CSMWorld::UniversalId::Type type, - QUndoCommand* parent) -: CreateCommand (model, idDestination, parent), mIdOrigin (idOrigin) +CSMWorld::CloneCommand::CloneCommand(CSMWorld::IdTable& model, const std::string& idOrigin, + const std::string& idDestination, const CSMWorld::UniversalId::Type type, QUndoCommand* parent) + : CreateCommand(model, idDestination, parent) + , mIdOrigin(idOrigin) { - setType (type); - setText ( ("Clone record " + idOrigin + " to the " + idDestination).c_str()); + setType(type); + setText(("Clone record " + idOrigin + " to the " + idDestination).c_str()); } void CSMWorld::CloneCommand::redo() { - mModel.cloneRecord (mIdOrigin, mId, mType); + mModel.cloneRecord(ESM::RefId::stringRefId(mIdOrigin), ESM::RefId::stringRefId(mId), mType); applyModifications(); for (auto& value : mOverrideValues) { - mModel.setData(mModel.getModelIndex (mId, value.first), value.second); + mModel.setData(mModel.getModelIndex(mId, value.first), value.second); } } void CSMWorld::CloneCommand::undo() { - mModel.removeRow (mModel.getModelIndex (mId, 0).row()); + mModel.removeRow(mModel.getModelIndex(mId, 0).row()); } void CSMWorld::CloneCommand::setOverrideValue(int column, QVariant value) @@ -414,7 +416,7 @@ void CSMWorld::CloneCommand::setOverrideValue(int column, QVariant value) mOverrideValues.emplace_back(std::make_pair(column, value)); } -CSMWorld::CreatePathgridCommand::CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand *parent) +CSMWorld::CreatePathgridCommand::CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand* parent) : CreateCommand(model, id, parent) { setType(UniversalId::Type_Pathgrid); @@ -424,73 +426,69 @@ void CSMWorld::CreatePathgridCommand::redo() { CreateCommand::redo(); - Record record = static_cast& >(mModel.getRecord(mId)); - record.get().blank(); - record.get().mCell = mId; + std::unique_ptr> record + = std::make_unique>(static_cast&>(mModel.getRecord(mId))); + record->get().blank(); + record->get().mCell = ESM::RefId::stringRefId(mId); std::pair coords = CellCoordinates::fromId(mId); if (coords.second) { - record.get().mData.mX = coords.first.getX(); - record.get().mData.mY = coords.first.getY(); + record->get().mData.mX = coords.first.getX(); + record->get().mData.mY = coords.first.getY(); } - mModel.setRecord(mId, record, mType); + mModel.setRecord(mId, std::move(record), mType); } -CSMWorld::UpdateCellCommand::UpdateCellCommand (IdTable& model, int row, QUndoCommand *parent) -: QUndoCommand (parent), mModel (model), mRow (row) +CSMWorld::UpdateCellCommand::UpdateCellCommand(IdTable& model, int row, QUndoCommand* parent) + : QUndoCommand(parent) + , mModel(model) + , mRow(row) { - setText ("Update cell ID"); + setText("Update cell ID"); } void CSMWorld::UpdateCellCommand::redo() { if (!mNew.isValid()) { - int cellColumn = mModel.searchColumnIndex (Columns::ColumnId_Cell); - mIndex = mModel.index (mRow, cellColumn); + int cellColumn = mModel.searchColumnIndex(Columns::ColumnId_Cell); + mIndex = mModel.index(mRow, cellColumn); - QModelIndex xIndex = mModel.index ( - mRow, mModel.findColumnIndex (Columns::ColumnId_PositionXPos)); + QModelIndex xIndex = mModel.index(mRow, mModel.findColumnIndex(Columns::ColumnId_PositionXPos)); - QModelIndex yIndex = mModel.index ( - mRow, mModel.findColumnIndex (Columns::ColumnId_PositionYPos)); + QModelIndex yIndex = mModel.index(mRow, mModel.findColumnIndex(Columns::ColumnId_PositionYPos)); - int x = std::floor (mModel.data (xIndex).toFloat() / Constants::CellSizeInUnits); - int y = std::floor (mModel.data (yIndex).toFloat() / Constants::CellSizeInUnits); + int x = std::floor(mModel.data(xIndex).toFloat() / Constants::CellSizeInUnits); + int y = std::floor(mModel.data(yIndex).toFloat() / Constants::CellSizeInUnits); std::ostringstream stream; stream << "#" << x << " " << y; - mNew = QString::fromUtf8 (stream.str().c_str()); + mNew = QString::fromUtf8(stream.str().c_str()); } - mModel.setData (mIndex, mNew); + mModel.setData(mIndex, mNew); } void CSMWorld::UpdateCellCommand::undo() { - mModel.setData (mIndex, mOld); + mModel.setData(mIndex, mOld); } - -CSMWorld::DeleteNestedCommand::DeleteNestedCommand (IdTree& model, - const std::string& id, - int nestedRow, - int parentColumn, - QUndoCommand* parent) : - QUndoCommand(parent), - NestedTableStoring(model, id, parentColumn), - mModel(model), - mId(id), - mParentColumn(parentColumn), - mNestedRow(nestedRow) +CSMWorld::DeleteNestedCommand::DeleteNestedCommand( + IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent) + : QUndoCommand(parent) + , NestedTableStoring(model, id, parentColumn) + , mModel(model) + , mId(id) + , mParentColumn(parentColumn) + , mNestedRow(nestedRow) { - std::string title = - model.headerData(parentColumn, Qt::Horizontal, Qt::DisplayRole).toString().toUtf8().constData(); - setText (("Delete row in " + title + " sub-table of " + mId).c_str()); + std::string title = model.headerData(parentColumn, Qt::Horizontal, Qt::DisplayRole).toString().toUtf8().constData(); + setText(("Delete row in " + title + " sub-table of " + mId).c_str()); QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModifyParentCommand = new ModifyCommand(mModel, parentIndex, parentIndex.data(Qt::EditRole), this); @@ -499,11 +497,10 @@ CSMWorld::DeleteNestedCommand::DeleteNestedCommand (IdTree& model, void CSMWorld::DeleteNestedCommand::redo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); - mModel.removeRows (mNestedRow, 1, parentIndex); mModifyParentCommand->redo(); + mModel.removeRows(mNestedRow, 1, parentIndex); } - void CSMWorld::DeleteNestedCommand::undo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); @@ -511,17 +508,17 @@ void CSMWorld::DeleteNestedCommand::undo() mModifyParentCommand->undo(); } -CSMWorld::AddNestedCommand::AddNestedCommand(IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent) - : QUndoCommand(parent), - NestedTableStoring(model, id, parentColumn), - mModel(model), - mId(id), - mNewRow(nestedRow), - mParentColumn(parentColumn) +CSMWorld::AddNestedCommand::AddNestedCommand( + IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent) + : QUndoCommand(parent) + , NestedTableStoring(model, id, parentColumn) + , mModel(model) + , mId(id) + , mNewRow(nestedRow) + , mParentColumn(parentColumn) { - std::string title = - model.headerData(parentColumn, Qt::Horizontal, Qt::DisplayRole).toString().toUtf8().constData(); - setText (("Add row in " + title + " sub-table of " + mId).c_str()); + std::string title = model.headerData(parentColumn, Qt::Horizontal, Qt::DisplayRole).toString().toUtf8().constData(); + setText(("Add row in " + title + " sub-table of " + mId).c_str()); QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModifyParentCommand = new ModifyCommand(mModel, parentIndex, parentIndex.data(Qt::EditRole), this); @@ -530,8 +527,8 @@ CSMWorld::AddNestedCommand::AddNestedCommand(IdTree& model, const std::string& i void CSMWorld::AddNestedCommand::redo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); - mModel.addNestedRow (parentIndex, mNewRow); mModifyParentCommand->redo(); + mModel.addNestedRow(parentIndex, mNewRow); } void CSMWorld::AddNestedCommand::undo() @@ -542,7 +539,9 @@ void CSMWorld::AddNestedCommand::undo() } CSMWorld::NestedTableStoring::NestedTableStoring(const IdTree& model, const std::string& id, int parentColumn) - : mOld(model.nestedTable(model.getModelIndex(id, parentColumn))) {} + : mOld(model.nestedTable(model.getModelIndex(id, parentColumn))) +{ +} CSMWorld::NestedTableStoring::~NestedTableStoring() { diff --git a/apps/opencs/model/world/commands.hpp b/apps/opencs/model/world/commands.hpp index 33608304f4e..f6b0caeb1ae 100644 --- a/apps/opencs/model/world/commands.hpp +++ b/apps/opencs/model/world/commands.hpp @@ -3,45 +3,40 @@ #include "record.hpp" -#include #include #include +#include +#include #include -#include -#include +#include #include +#include +#include #include "columnimp.hpp" #include "universalid.hpp" -#include "nestedtablewrapper.hpp" - -class QModelIndex; -class QAbstractItemModel; namespace CSMWorld { class IdTable; class IdTree; - struct RecordBase; struct NestedTableWrapperBase; class TouchCommand : public QUndoCommand { - public: - - TouchCommand(IdTable& model, const std::string& id, QUndoCommand* parent=nullptr); - - void redo() override; - void undo() override; + public: + TouchCommand(IdTable& model, const std::string& id, QUndoCommand* parent = nullptr); - private: + void redo() override; + void undo() override; - IdTable& mTable; - std::string mId; - std::unique_ptr mOld; + private: + IdTable& mTable; + std::string mId; + std::unique_ptr mOld; - bool mChanged; + bool mChanged; }; /// \brief Adds LandTexture records and modifies texture indices as needed. @@ -55,29 +50,26 @@ namespace CSMWorld /// LandRecord and within the Land record. class ImportLandTexturesCommand : public QUndoCommand { - public: - - ImportLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, - QUndoCommand* parent); - - void redo() override; - void undo() override; + public: + ImportLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, QUndoCommand* parent); - protected: + void redo() override; + void undo() override; - using DataType = LandTexturesColumn::DataType; + protected: + using DataType = LandTexturesColumn::DataType; - virtual const std::string& getOriginId() const = 0; - virtual const std::string& getDestinationId() const = 0; + virtual const std::string& getOriginId() const = 0; + virtual const std::string& getDestinationId() const = 0; - virtual void onRedo() = 0; - virtual void onUndo() = 0; + virtual void onRedo() = 0; + virtual void onUndo() = 0; - IdTable& mLands; - IdTable& mLtexs; - DataType mOld; - int mOldState; - std::vector mCreatedTextures; + IdTable& mLands; + IdTable& mLtexs; + DataType mOld; + int mOldState; + std::vector> mTouchedTextures; }; /// \brief This command is used to fix LandTexture records and texture @@ -85,21 +77,19 @@ namespace CSMWorld /// details. class CopyLandTexturesCommand : public ImportLandTexturesCommand { - public: - - CopyLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, const std::string& origin, - const std::string& dest, QUndoCommand* parent = nullptr); - - private: + public: + CopyLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, const std::string& origin, + const std::string& dest, QUndoCommand* parent = nullptr); - const std::string& getOriginId() const override; - const std::string& getDestinationId() const override; + private: + const std::string& getOriginId() const override; + const std::string& getDestinationId() const override; - void onRedo() override {} - void onUndo() override {} + void onRedo() override {} + void onUndo() override {} - std::string mOriginId; - std::string mDestId; + std::string mOriginId; + std::string mDestId; }; /// \brief This command brings a land record into the current plugin, adding @@ -107,164 +97,150 @@ namespace CSMWorld /// \note See ImportLandTextures for more details. class TouchLandCommand : public ImportLandTexturesCommand { - public: - - TouchLandCommand(IdTable& landTable, IdTable& ltexTable, - const std::string& id, QUndoCommand* parent = nullptr); - - private: + public: + TouchLandCommand(IdTable& landTable, IdTable& ltexTable, const std::string& id, QUndoCommand* parent = nullptr); - const std::string& getOriginId() const override; - const std::string& getDestinationId() const override; + private: + const std::string& getOriginId() const override; + const std::string& getDestinationId() const override; - void onRedo() override; - void onUndo() override; + void onRedo() override; + void onUndo() override; - std::string mId; - std::unique_ptr mOld; + std::string mId; + std::unique_ptr mOld; - bool mChanged; + bool mChanged; }; class ModifyCommand : public QUndoCommand { - QAbstractItemModel *mModel; - QModelIndex mIndex; - QVariant mNew; - QVariant mOld; - - bool mHasRecordState; - QModelIndex mRecordStateIndex; - CSMWorld::RecordBase::State mOldRecordState; + QAbstractItemModel* mModel; + QModelIndex mIndex; + QVariant mNew; + QVariant mOld; - public: + bool mHasRecordState; + QModelIndex mRecordStateIndex; + CSMWorld::RecordBase::State mOldRecordState; - ModifyCommand (QAbstractItemModel& model, const QModelIndex& index, const QVariant& new_, - QUndoCommand *parent = nullptr); + public: + ModifyCommand( + QAbstractItemModel& model, const QModelIndex& index, const QVariant& new_, QUndoCommand* parent = nullptr); - void redo() override; + void redo() override; - void undo() override; + void undo() override; }; class CreateCommand : public QUndoCommand { - std::map mValues; - std::map > mNestedValues; - ///< Parameter order: a parent column, a nested column, a data. - ///< A nested row has index of 0. - - protected: + std::map mValues; + std::map> mNestedValues; + ///< Parameter order: a parent column, a nested column, a data. + ///< A nested row has index of 0. - IdTable& mModel; - std::string mId; - UniversalId::Type mType; - - protected: - - /// Apply modifications set via addValue. - void applyModifications(); + protected: + IdTable& mModel; + std::string mId; + UniversalId::Type mType; - public: + protected: + /// Apply modifications set via addValue. + void applyModifications(); - CreateCommand (IdTable& model, const std::string& id, QUndoCommand *parent = nullptr); + public: + CreateCommand(IdTable& model, const std::string& id, QUndoCommand* parent = nullptr); - void setType (UniversalId::Type type); + void setType(UniversalId::Type type); - void addValue (int column, const QVariant& value); + void addValue(int column, const QVariant& value); - void addNestedValue(int parentColumn, int nestedColumn, const QVariant &value); + void addNestedValue(int parentColumn, int nestedColumn, const QVariant& value); - void redo() override; + void redo() override; - void undo() override; + void undo() override; }; class CloneCommand : public CreateCommand { - std::string mIdOrigin; - std::vector> mOverrideValues; - - public: + std::string mIdOrigin; + std::vector> mOverrideValues; - CloneCommand (IdTable& model, const std::string& idOrigin, - const std::string& IdDestination, - const UniversalId::Type type, - QUndoCommand* parent = nullptr); + public: + CloneCommand(IdTable& model, const std::string& idOrigin, const std::string& IdDestination, + const UniversalId::Type type, QUndoCommand* parent = nullptr); - void redo() override; + void redo() override; - void undo() override; + void undo() override; - void setOverrideValue(int column, QVariant value); + void setOverrideValue(int column, QVariant value); }; class RevertCommand : public QUndoCommand { - IdTable& mModel; - std::string mId; - RecordBase *mOld; - - // not implemented - RevertCommand (const RevertCommand&); - RevertCommand& operator= (const RevertCommand&); + IdTable& mModel; + std::string mId; + std::unique_ptr mOld; - public: + // not implemented + RevertCommand(const RevertCommand&); + RevertCommand& operator=(const RevertCommand&); - RevertCommand (IdTable& model, const std::string& id, QUndoCommand *parent = nullptr); + public: + RevertCommand(IdTable& model, const std::string& id, QUndoCommand* parent = nullptr); - virtual ~RevertCommand(); + ~RevertCommand() override = default; - void redo() override; + void redo() override; - void undo() override; + void undo() override; }; class DeleteCommand : public QUndoCommand { - IdTable& mModel; - std::string mId; - RecordBase *mOld; - UniversalId::Type mType; + IdTable& mModel; + std::string mId; + std::unique_ptr mOld; + UniversalId::Type mType; - // not implemented - DeleteCommand (const DeleteCommand&); - DeleteCommand& operator= (const DeleteCommand&); + // not implemented + DeleteCommand(const DeleteCommand&); + DeleteCommand& operator=(const DeleteCommand&); - public: - - DeleteCommand (IdTable& model, const std::string& id, - UniversalId::Type type = UniversalId::Type_None, QUndoCommand *parent = nullptr); + public: + DeleteCommand(IdTable& model, const std::string& id, UniversalId::Type type = UniversalId::Type_None, + QUndoCommand* parent = nullptr); - virtual ~DeleteCommand(); + ~DeleteCommand() override = default; - void redo() override; + void redo() override; - void undo() override; + void undo() override; }; class ReorderRowsCommand : public QUndoCommand { - IdTable& mModel; - int mBaseIndex; - std::vector mNewOrder; - - public: + IdTable& mModel; + int mBaseIndex; + std::vector mNewOrder; - ReorderRowsCommand (IdTable& model, int baseIndex, const std::vector& newOrder); + public: + ReorderRowsCommand(IdTable& model, int baseIndex, const std::vector& newOrder); - void redo() override; + void redo() override; - void undo() override; + void undo() override; }; class CreatePathgridCommand : public CreateCommand { - public: - - CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand *parent = nullptr); + public: + CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand* parent = nullptr); - void redo() override; + void redo() override; }; /// \brief Update cell ID according to x/y-coordinates @@ -274,22 +250,20 @@ namespace CSMWorld /// in a macro. class UpdateCellCommand : public QUndoCommand { - IdTable& mModel; - int mRow; - QModelIndex mIndex; - QVariant mNew; // invalid, if new cell ID has not been calculated yet - QVariant mOld; + IdTable& mModel; + int mRow; + QModelIndex mIndex; + QVariant mNew; // invalid, if new cell ID has not been calculated yet + QVariant mOld; - public: - - UpdateCellCommand (IdTable& model, int row, QUndoCommand *parent = nullptr); + public: + UpdateCellCommand(IdTable& model, int row, QUndoCommand* parent = nullptr); - void redo() override; + void redo() override; - void undo() override; + void undo() override; }; - class NestedTableStoring { NestedTableWrapperBase* mOld; @@ -300,52 +274,51 @@ namespace CSMWorld ~NestedTableStoring(); protected: - const NestedTableWrapperBase& getOld() const; }; class DeleteNestedCommand : public QUndoCommand, private NestedTableStoring { - IdTree& mModel; - - std::string mId; + IdTree& mModel; - int mParentColumn; + std::string mId; - int mNestedRow; + int mParentColumn; - // The command to redo/undo the Modified status of a record - ModifyCommand *mModifyParentCommand; + int mNestedRow; - public: + // The command to redo/undo the Modified status of a record + ModifyCommand* mModifyParentCommand; - DeleteNestedCommand (IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent = nullptr); + public: + DeleteNestedCommand( + IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent = nullptr); - void redo() override; + void redo() override; - void undo() override; + void undo() override; }; class AddNestedCommand : public QUndoCommand, private NestedTableStoring { - IdTree& mModel; + IdTree& mModel; - std::string mId; + std::string mId; - int mNewRow; + int mNewRow; - int mParentColumn; + int mParentColumn; - // The command to redo/undo the Modified status of a record - ModifyCommand *mModifyParentCommand; + // The command to redo/undo the Modified status of a record + ModifyCommand* mModifyParentCommand; - public: - - AddNestedCommand(IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent = nullptr); + public: + AddNestedCommand( + IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent = nullptr); - void redo() override; + void redo() override; - void undo() override; + void undo() override; }; } diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 4ccd2a06dbc..5637099d0c5 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -1,617 +1,690 @@ #include "data.hpp" -#include #include +#include +#include +#include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include #include -#include -#include - +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include #include #include +#include "../doc/messages.hpp" + +#include "columnimp.hpp" +#include "columns.hpp" #include "idtable.hpp" #include "idtree.hpp" -#include "columnimp.hpp" +#include "nestedcoladapterimp.hpp" #include "regionmap.hpp" -#include "columns.hpp" #include "resourcesmanager.hpp" #include "resourcetable.hpp" -#include "nestedcoladapterimp.hpp" -void CSMWorld::Data::addModel (QAbstractItemModel *model, UniversalId::Type type, bool update) +namespace CSMWorld { - mModels.push_back (model); - mModelIndex.insert (std::make_pair (type, model)); + namespace + { + void removeDialogueInfos( + const ESM::RefId& dialogueId, InfoOrderByTopic& infoOrders, InfoCollection& infoCollection) + { + const auto topicInfoOrder = infoOrders.find(dialogueId); - UniversalId::Type type2 = UniversalId::getParentType (type); + if (topicInfoOrder == infoOrders.end()) + return; - if (type2!=UniversalId::Type_None) - mModelIndex.insert (std::make_pair (type2, model)); + std::vector erasedRecords; + + for (const OrderedInfo& info : topicInfoOrder->second.getOrderedInfo()) + { + const ESM::RefId id = makeCompositeInfoRefId(dialogueId, info.mId); + const Record& record = infoCollection.getRecord(id); + + if (record.mState == RecordBase::State_ModifiedOnly) + { + erasedRecords.push_back(infoCollection.searchId(id)); + continue; + } + + auto deletedRecord = std::make_unique>(record); + deletedRecord->mState = RecordBase::State_Deleted; + infoCollection.setRecord(infoCollection.searchId(id), std::move(deletedRecord)); + } + + while (!erasedRecords.empty()) + { + infoCollection.removeRows(erasedRecords.back(), 1); + erasedRecords.pop_back(); + } + } + } +} + +void CSMWorld::Data::addModel(QAbstractItemModel* model, UniversalId::Type type, bool update) +{ + mModels.push_back(model); + mModelIndex.insert(std::make_pair(type, model)); + + UniversalId::Type type2 = UniversalId::getParentType(type); + + if (type2 != UniversalId::Type_None) + mModelIndex.insert(std::make_pair(type2, model)); if (update) { - connect (model, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (dataChanged (const QModelIndex&, const QModelIndex&))); - connect (model, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (rowsChanged (const QModelIndex&, int, int))); - connect (model, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), - this, SLOT (rowsChanged (const QModelIndex&, int, int))); + connect(model, &QAbstractItemModel::dataChanged, this, &Data::dataChanged); + connect(model, &QAbstractItemModel::rowsInserted, this, &Data::rowsChanged); + connect(model, &QAbstractItemModel::rowsRemoved, this, &Data::rowsChanged); } } -void CSMWorld::Data::appendIds (std::vector& ids, const CollectionBase& collection, - bool listDeleted) +void CSMWorld::Data::appendIds(std::vector& ids, const CollectionBase& collection, bool listDeleted) { - std::vector ids2 = collection.getIds (listDeleted); + std::vector ids2 = collection.getIds(listDeleted); - ids.insert (ids.end(), ids2.begin(), ids2.end()); + ids.insert(ids.end(), ids2.begin(), ids2.end()); } -int CSMWorld::Data::count (RecordBase::State state, const CollectionBase& collection) +int CSMWorld::Data::count(RecordBase::State state, const CollectionBase& collection) { int number = 0; - for (int i=0; i& archives, const boost::filesystem::path& resDir) -: mEncoder (encoding), mPathgrids (mCells), mRefs (mCells), - mReader (nullptr), mDialogue (nullptr), mReaderIndex(1), - mFsStrict(fsStrict), mDataPaths(dataPaths), mArchives(archives) +CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& dataPaths, + const std::vector& archives, const std::filesystem::path& resDir) + : mEncoder(encoding) + , mPathgrids(mCells) + , mRefs(mCells) + , mDialogue(nullptr) + , mReaderIndex(0) + , mDataPaths(dataPaths) + , mArchives(archives) + , mVFS(std::make_unique()) { - mVFS.reset(new VFS::Manager(mFsStrict)); - VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths, !mFsStrict), mArchives, true); + VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths), mArchives, true); mResourcesManager.setVFS(mVFS.get()); - mResourceSystem.reset(new Resource::ResourceSystem(mVFS.get())); - Shader::ShaderManager::DefineMap defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); + constexpr double expiryDelay = 0; + mResourceSystem + = std::make_unique(mVFS.get(), expiryDelay, &mEncoder.getStatelessEncoder()); + + Shader::ShaderManager::DefineMap defines + = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); Shader::ShaderManager::DefineMap shadowDefines = SceneUtil::ShadowManager::getShadowsDisabledDefines(); defines["forcePPL"] = "0"; // Don't force per-pixel lighting defines["clamp"] = "1"; // Clamp lighting defines["preLightEnv"] = "0"; // Apply environment maps after lighting like Morrowind defines["radialFog"] = "0"; defines["lightingModel"] = "0"; + defines["reverseZ"] = "0"; + defines["waterRefraction"] = "0"; for (const auto& define : shadowDefines) defines[define.first] = define.second; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); - mResourceSystem->getSceneManager()->setShaderPath((resDir / "shaders").string()); + mResourceSystem->getSceneManager()->setShaderPath(resDir / "shaders"); int index = 0; - mGlobals.addColumn (new StringIdColumn); - mGlobals.addColumn (new RecordStateColumn); - mGlobals.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Global)); - mGlobals.addColumn (new VarTypeColumn (ColumnBase::Display_GlobalVarType)); - mGlobals.addColumn (new VarValueColumn); - - mGmsts.addColumn (new StringIdColumn); - mGmsts.addColumn (new RecordStateColumn); - mGmsts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Gmst)); - mGmsts.addColumn (new VarTypeColumn (ColumnBase::Display_GmstVarType)); - mGmsts.addColumn (new VarValueColumn); - - mSkills.addColumn (new StringIdColumn); - mSkills.addColumn (new RecordStateColumn); - mSkills.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Skill)); - mSkills.addColumn (new AttributeColumn); - mSkills.addColumn (new SpecialisationColumn); - for (int i=0; i<4; ++i) - mSkills.addColumn (new UseValueColumn (i)); - mSkills.addColumn (new DescriptionColumn); - - mClasses.addColumn (new StringIdColumn); - mClasses.addColumn (new RecordStateColumn); - mClasses.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Class)); - mClasses.addColumn (new NameColumn); - mClasses.addColumn (new AttributesColumn (0)); - mClasses.addColumn (new AttributesColumn (1)); - mClasses.addColumn (new SpecialisationColumn); - for (int i=0; i<5; ++i) - mClasses.addColumn (new SkillsColumn (i, true, true)); - for (int i=0; i<5; ++i) - mClasses.addColumn (new SkillsColumn (i, true, false)); - mClasses.addColumn (new PlayableColumn); - mClasses.addColumn (new DescriptionColumn); - - mFactions.addColumn (new StringIdColumn); - mFactions.addColumn (new RecordStateColumn); - mFactions.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Faction)); - mFactions.addColumn (new NameColumn); - mFactions.addColumn (new AttributesColumn (0)); - mFactions.addColumn (new AttributesColumn (1)); - mFactions.addColumn (new HiddenColumn); - for (int i=0; i<7; ++i) - mFactions.addColumn (new SkillsColumn (i)); + mGlobals.addColumn(new StringIdColumn); + mGlobals.addColumn(new RecordStateColumn); + mGlobals.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Global)); + mGlobals.addColumn(new VarTypeColumn(ColumnBase::Display_GlobalVarType)); + mGlobals.addColumn(new VarValueColumn); + + mGmsts.addColumn(new StringIdColumn); + mGmsts.addColumn(new RecordStateColumn); + mGmsts.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Gmst)); + mGmsts.addColumn(new VarTypeColumn(ColumnBase::Display_GmstVarType)); + mGmsts.addColumn(new VarValueColumn); + + mSkills.addColumn(new StringIdColumn); + mSkills.addColumn(new RecordStateColumn); + mSkills.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Skill)); + mSkills.addColumn(new AttributeColumn); + mSkills.addColumn(new SpecialisationColumn); + for (int i = 0; i < 4; ++i) + mSkills.addColumn(new UseValueColumn(i)); + mSkills.addColumn(new DescriptionColumn); + + mClasses.addColumn(new StringIdColumn); + mClasses.addColumn(new RecordStateColumn); + mClasses.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Class)); + mClasses.addColumn(new NameColumn); + mClasses.addColumn(new AttributesColumn(0)); + mClasses.addColumn(new AttributesColumn(1)); + mClasses.addColumn(new SpecialisationColumn); + for (int i = 0; i < 5; ++i) + mClasses.addColumn(new SkillsColumn(i, true, true)); + for (int i = 0; i < 5; ++i) + mClasses.addColumn(new SkillsColumn(i, true, false)); + mClasses.addColumn(new PlayableColumn); + mClasses.addColumn(new DescriptionColumn); + + mFactions.addColumn(new StringIdColumn); + mFactions.addColumn(new RecordStateColumn); + mFactions.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Faction)); + mFactions.addColumn(new NameColumn(ColumnBase::Display_String32)); + mFactions.addColumn(new AttributesColumn(0)); + mFactions.addColumn(new AttributesColumn(1)); + mFactions.addColumn(new HiddenColumn); + for (int i = 0; i < 7; ++i) + mFactions.addColumn(new SkillsColumn(i)); // Faction Reactions - mFactions.addColumn (new NestedParentColumn (Columns::ColumnId_FactionReactions)); - index = mFactions.getColumns()-1; - mFactions.addAdapter (std::make_pair(&mFactions.getColumn(index), new FactionReactionsAdapter ())); + mFactions.addColumn(new NestedParentColumn(Columns::ColumnId_FactionReactions)); + index = mFactions.getColumns() - 1; + mFactions.addAdapter(std::make_pair(&mFactions.getColumn(index), new FactionReactionsAdapter())); mFactions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Faction, ColumnBase::Display_Faction)); + new NestedChildColumn(Columns::ColumnId_Faction, ColumnBase::Display_Faction)); mFactions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_FactionReaction, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_FactionReaction, ColumnBase::Display_Integer)); // Faction Ranks - mFactions.addColumn (new NestedParentColumn (Columns::ColumnId_FactionRanks)); - index = mFactions.getColumns()-1; - mFactions.addAdapter (std::make_pair(&mFactions.getColumn(index), new FactionRanksAdapter ())); + mFactions.addColumn(new NestedParentColumn(Columns::ColumnId_FactionRanks)); + index = mFactions.getColumns() - 1; + mFactions.addAdapter(std::make_pair(&mFactions.getColumn(index), new FactionRanksAdapter())); mFactions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_RankName, ColumnBase::Display_Rank)); + new NestedChildColumn(Columns::ColumnId_RankName, ColumnBase::Display_Rank)); mFactions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_FactionAttrib1, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_FactionAttrib1, ColumnBase::Display_Integer)); mFactions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_FactionAttrib2, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_FactionAttrib2, ColumnBase::Display_Integer)); mFactions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_FactionPrimSkill, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_FactionPrimSkill, ColumnBase::Display_Integer)); mFactions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_FactionFavSkill, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_FactionFavSkill, ColumnBase::Display_Integer)); mFactions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_FactionRep, ColumnBase::Display_Integer)); - - mRaces.addColumn (new StringIdColumn); - mRaces.addColumn (new RecordStateColumn); - mRaces.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Race)); - mRaces.addColumn (new NameColumn); - mRaces.addColumn (new DescriptionColumn); - mRaces.addColumn (new FlagColumn (Columns::ColumnId_Playable, 0x1)); - mRaces.addColumn (new FlagColumn (Columns::ColumnId_BeastRace, 0x2)); - mRaces.addColumn (new WeightHeightColumn (true, true)); - mRaces.addColumn (new WeightHeightColumn (true, false)); - mRaces.addColumn (new WeightHeightColumn (false, true)); - mRaces.addColumn (new WeightHeightColumn (false, false)); + new NestedChildColumn(Columns::ColumnId_FactionRep, ColumnBase::Display_Integer)); + + mRaces.addColumn(new StringIdColumn); + mRaces.addColumn(new RecordStateColumn); + mRaces.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Race)); + mRaces.addColumn(new NameColumn); + mRaces.addColumn(new DescriptionColumn); + mRaces.addColumn(new FlagColumn(Columns::ColumnId_Playable, 0x1)); + mRaces.addColumn(new FlagColumn(Columns::ColumnId_BeastRace, 0x2)); + mRaces.addColumn(new WeightHeightColumn(true, true)); + mRaces.addColumn(new WeightHeightColumn(true, false)); + mRaces.addColumn(new WeightHeightColumn(false, true)); + mRaces.addColumn(new WeightHeightColumn(false, false)); // Race spells - mRaces.addColumn (new NestedParentColumn (Columns::ColumnId_PowerList)); - index = mRaces.getColumns()-1; - mRaces.addAdapter (std::make_pair(&mRaces.getColumn(index), new SpellListAdapter ())); + mRaces.addColumn(new NestedParentColumn(Columns::ColumnId_PowerList)); + index = mRaces.getColumns() - 1; + mRaces.addAdapter(std::make_pair(&mRaces.getColumn(index), new SpellListAdapter())); mRaces.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_SpellId, ColumnBase::Display_Spell)); + new NestedChildColumn(Columns::ColumnId_SpellId, ColumnBase::Display_Spell)); // Race attributes - mRaces.addColumn (new NestedParentColumn (Columns::ColumnId_RaceAttributes, - ColumnBase::Flag_Dialogue, true)); // fixed rows table - index = mRaces.getColumns()-1; - mRaces.addAdapter (std::make_pair(&mRaces.getColumn(index), new RaceAttributeAdapter())); - mRaces.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Attribute, ColumnBase::Display_Attribute, - ColumnBase::Flag_Dialogue, false)); + mRaces.addColumn(new NestedParentColumn( + Columns::ColumnId_RaceAttributes, ColumnBase::Flag_Dialogue, true)); // fixed rows table + index = mRaces.getColumns() - 1; + mRaces.addAdapter(std::make_pair(&mRaces.getColumn(index), new RaceAttributeAdapter())); + mRaces.getNestableColumn(index)->addColumn(new NestedChildColumn( + Columns::ColumnId_Attribute, ColumnBase::Display_Attribute, ColumnBase::Flag_Dialogue, false)); mRaces.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Male, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_Male, ColumnBase::Display_Integer)); mRaces.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Female, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_Female, ColumnBase::Display_Integer)); // Race skill bonus - mRaces.addColumn (new NestedParentColumn (Columns::ColumnId_RaceSkillBonus, - ColumnBase::Flag_Dialogue, true)); // fixed rows table - index = mRaces.getColumns()-1; - mRaces.addAdapter (std::make_pair(&mRaces.getColumn(index), new RaceSkillsBonusAdapter())); + mRaces.addColumn(new NestedParentColumn( + Columns::ColumnId_RaceSkillBonus, ColumnBase::Flag_Dialogue, true)); // fixed rows table + index = mRaces.getColumns() - 1; + mRaces.addAdapter(std::make_pair(&mRaces.getColumn(index), new RaceSkillsBonusAdapter())); mRaces.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Skill, ColumnBase::Display_SkillId)); + new NestedChildColumn(Columns::ColumnId_Skill, ColumnBase::Display_SkillId)); mRaces.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_RaceBonus, ColumnBase::Display_Integer)); - - mSounds.addColumn (new StringIdColumn); - mSounds.addColumn (new RecordStateColumn); - mSounds.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Sound)); - mSounds.addColumn (new SoundParamColumn (SoundParamColumn::Type_Volume)); - mSounds.addColumn (new SoundParamColumn (SoundParamColumn::Type_MinRange)); - mSounds.addColumn (new SoundParamColumn (SoundParamColumn::Type_MaxRange)); - mSounds.addColumn (new SoundFileColumn); - - mScripts.addColumn (new StringIdColumn); - mScripts.addColumn (new RecordStateColumn); - mScripts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Script)); - mScripts.addColumn (new ScriptColumn (ScriptColumn::Type_File)); - - mRegions.addColumn (new StringIdColumn); - mRegions.addColumn (new RecordStateColumn); - mRegions.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Region)); - mRegions.addColumn (new NameColumn); - mRegions.addColumn (new MapColourColumn); - mRegions.addColumn (new SleepListColumn); + new NestedChildColumn(Columns::ColumnId_RaceBonus, ColumnBase::Display_Integer)); + + mSounds.addColumn(new StringIdColumn); + mSounds.addColumn(new RecordStateColumn); + mSounds.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Sound)); + mSounds.addColumn(new SoundParamColumn(SoundParamColumn::Type_Volume)); + mSounds.addColumn(new SoundParamColumn(SoundParamColumn::Type_MinRange)); + mSounds.addColumn(new SoundParamColumn(SoundParamColumn::Type_MaxRange)); + mSounds.addColumn(new SoundFileColumn); + + mScripts.addColumn(new StringIdColumn); + mScripts.addColumn(new RecordStateColumn); + mScripts.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Script)); + mScripts.addColumn(new ScriptColumn(ScriptColumn::Type_File)); + + mRegions.addColumn(new StringIdColumn); + mRegions.addColumn(new RecordStateColumn); + mRegions.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Region)); + mRegions.addColumn(new NameColumn); + mRegions.addColumn(new MapColourColumn); + mRegions.addColumn(new SleepListColumn); // Region Weather - mRegions.addColumn (new NestedParentColumn (Columns::ColumnId_RegionWeather)); - index = mRegions.getColumns()-1; - mRegions.addAdapter (std::make_pair(&mRegions.getColumn(index), new RegionWeatherAdapter ())); + mRegions.addColumn(new NestedParentColumn(Columns::ColumnId_RegionWeather)); + index = mRegions.getColumns() - 1; + mRegions.addAdapter(std::make_pair(&mRegions.getColumn(index), new RegionWeatherAdapter())); + mRegions.getNestableColumn(index)->addColumn(new NestedChildColumn( + Columns::ColumnId_WeatherName, ColumnBase::Display_String, ColumnBase::Flag_Dialogue, false)); mRegions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_WeatherName, ColumnBase::Display_String, false)); - mRegions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_WeatherChance, ColumnBase::Display_UnsignedInteger8)); + new NestedChildColumn(Columns::ColumnId_WeatherChance, ColumnBase::Display_UnsignedInteger8)); // Region Sounds - mRegions.addColumn (new NestedParentColumn (Columns::ColumnId_RegionSounds)); - index = mRegions.getColumns()-1; - mRegions.addAdapter (std::make_pair(&mRegions.getColumn(index), new RegionSoundListAdapter ())); + mRegions.addColumn(new NestedParentColumn(Columns::ColumnId_RegionSounds)); + index = mRegions.getColumns() - 1; + mRegions.addAdapter(std::make_pair(&mRegions.getColumn(index), new RegionSoundListAdapter())); mRegions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_SoundName, ColumnBase::Display_Sound)); + new NestedChildColumn(Columns::ColumnId_SoundName, ColumnBase::Display_Sound)); mRegions.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_SoundChance, ColumnBase::Display_UnsignedInteger8)); - - mBirthsigns.addColumn (new StringIdColumn); - mBirthsigns.addColumn (new RecordStateColumn); - mBirthsigns.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Birthsign)); - mBirthsigns.addColumn (new NameColumn); - mBirthsigns.addColumn (new TextureColumn); - mBirthsigns.addColumn (new DescriptionColumn); + new NestedChildColumn(Columns::ColumnId_SoundChance, ColumnBase::Display_UnsignedInteger8)); + mRegions.getNestableColumn(index)->addColumn(new NestedChildColumn( + Columns::ColumnId_SoundProbability, ColumnBase::Display_String, ColumnBase::Flag_Dialogue, false)); + + mBirthsigns.addColumn(new StringIdColumn); + mBirthsigns.addColumn(new RecordStateColumn); + mBirthsigns.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Birthsign)); + mBirthsigns.addColumn(new NameColumn); + mBirthsigns.addColumn(new TextureColumn); + mBirthsigns.addColumn(new DescriptionColumn); // Birthsign spells - mBirthsigns.addColumn (new NestedParentColumn (Columns::ColumnId_PowerList)); - index = mBirthsigns.getColumns()-1; - mBirthsigns.addAdapter (std::make_pair(&mBirthsigns.getColumn(index), - new SpellListAdapter ())); + mBirthsigns.addColumn(new NestedParentColumn(Columns::ColumnId_PowerList)); + index = mBirthsigns.getColumns() - 1; + mBirthsigns.addAdapter(std::make_pair(&mBirthsigns.getColumn(index), new SpellListAdapter())); mBirthsigns.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_SpellId, ColumnBase::Display_Spell)); - - mSpells.addColumn (new StringIdColumn); - mSpells.addColumn (new RecordStateColumn); - mSpells.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Spell)); - mSpells.addColumn (new NameColumn); - mSpells.addColumn (new SpellTypeColumn); - mSpells.addColumn (new CostColumn); - mSpells.addColumn (new FlagColumn (Columns::ColumnId_AutoCalc, 0x1)); - mSpells.addColumn (new FlagColumn (Columns::ColumnId_StarterSpell, 0x2)); - mSpells.addColumn (new FlagColumn (Columns::ColumnId_AlwaysSucceeds, 0x4)); + new NestedChildColumn(Columns::ColumnId_SpellId, ColumnBase::Display_Spell)); + + mSpells.addColumn(new StringIdColumn); + mSpells.addColumn(new RecordStateColumn); + mSpells.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Spell)); + mSpells.addColumn(new NameColumn); + mSpells.addColumn(new SpellTypeColumn); + mSpells.addColumn(new CostColumn); + mSpells.addColumn(new FlagColumn(Columns::ColumnId_AutoCalc, 0x1)); + mSpells.addColumn(new FlagColumn(Columns::ColumnId_StarterSpell, 0x2)); + mSpells.addColumn(new FlagColumn(Columns::ColumnId_AlwaysSucceeds, 0x4)); // Spell effects - mSpells.addColumn (new NestedParentColumn (Columns::ColumnId_EffectList)); - index = mSpells.getColumns()-1; - mSpells.addAdapter (std::make_pair(&mSpells.getColumn(index), new EffectsListAdapter ())); + mSpells.addColumn(new NestedParentColumn(Columns::ColumnId_EffectList)); + index = mSpells.getColumns() - 1; + mSpells.addAdapter(std::make_pair(&mSpells.getColumn(index), new EffectsListAdapter())); mSpells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_EffectId, ColumnBase::Display_EffectId)); + new NestedChildColumn(Columns::ColumnId_EffectId, ColumnBase::Display_EffectId)); mSpells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); + new NestedChildColumn(Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); mSpells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); + new NestedChildColumn(Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); mSpells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_EffectRange, ColumnBase::Display_EffectRange)); + new NestedChildColumn(Columns::ColumnId_EffectRange, ColumnBase::Display_EffectRange)); mSpells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_EffectArea, ColumnBase::Display_String)); + new NestedChildColumn(Columns::ColumnId_EffectArea, ColumnBase::Display_String)); mSpells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Duration, ColumnBase::Display_Integer)); // reuse from light + new NestedChildColumn(Columns::ColumnId_Duration, ColumnBase::Display_Integer)); // reuse from light mSpells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_MinMagnitude, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_MinMagnitude, ColumnBase::Display_Integer)); mSpells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_MaxMagnitude, ColumnBase::Display_Integer)); - - mTopics.addColumn (new StringIdColumn); - mTopics.addColumn (new RecordStateColumn); - mTopics.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Topic)); - mTopics.addColumn (new DialogueTypeColumn); - - mJournals.addColumn (new StringIdColumn); - mJournals.addColumn (new RecordStateColumn); - mJournals.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Journal)); - mJournals.addColumn (new DialogueTypeColumn (true)); - - mTopicInfos.addColumn (new StringIdColumn (true)); - mTopicInfos.addColumn (new RecordStateColumn); - mTopicInfos.addColumn (new FixedRecordTypeColumn (UniversalId::Type_TopicInfo)); - mTopicInfos.addColumn (new TopicColumn (false)); - mTopicInfos.addColumn (new ActorColumn); - mTopicInfos.addColumn (new RaceColumn); - mTopicInfos.addColumn (new ClassColumn); - mTopicInfos.addColumn (new FactionColumn); - mTopicInfos.addColumn (new CellColumn); - mTopicInfos.addColumn (new DispositionColumn); - mTopicInfos.addColumn (new RankColumn); - mTopicInfos.addColumn (new GenderColumn); - mTopicInfos.addColumn (new PcFactionColumn); - mTopicInfos.addColumn (new PcRankColumn); - mTopicInfos.addColumn (new SoundFileColumn); - mTopicInfos.addColumn (new ResponseColumn); + new NestedChildColumn(Columns::ColumnId_MaxMagnitude, ColumnBase::Display_Integer)); + + mTopics.addColumn(new StringIdColumn); + mTopics.addColumn(new RecordStateColumn); + mTopics.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Topic)); + mTopics.addColumn(new DialogueTypeColumn); + + mJournals.addColumn(new StringIdColumn); + mJournals.addColumn(new RecordStateColumn); + mJournals.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Journal)); + mJournals.addColumn(new DialogueTypeColumn(true)); + + mTopicInfos.addColumn(new StringIdColumn(true)); + mTopicInfos.addColumn(new RecordStateColumn); + mTopicInfos.addColumn(new FixedRecordTypeColumn(UniversalId::Type_TopicInfo)); + mTopicInfos.addColumn(new TopicColumn(false)); + mTopicInfos.addColumn(new ResponseColumn); + mTopicInfos.addColumn(new ActorColumn); + mTopicInfos.addColumn(new RaceColumn); + mTopicInfos.addColumn(new ClassColumn); + mTopicInfos.addColumn(new FactionColumn); + mTopicInfos.addColumn(new CellColumn); + mTopicInfos.addColumn(new DispositionColumn); + mTopicInfos.addColumn(new RankColumn); + mTopicInfos.addColumn(new GenderColumn); + mTopicInfos.addColumn(new PcFactionColumn); + mTopicInfos.addColumn(new PcRankColumn); + mTopicInfos.addColumn(new SoundFileColumn); // Result script - mTopicInfos.addColumn (new NestedParentColumn (Columns::ColumnId_InfoList, - ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List)); - index = mTopicInfos.getColumns()-1; - mTopicInfos.addAdapter (std::make_pair(&mTopicInfos.getColumn(index), new InfoListAdapter ())); + mTopicInfos.addColumn(new NestedParentColumn( + Columns::ColumnId_InfoList, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List)); + index = mTopicInfos.getColumns() - 1; + mTopicInfos.addAdapter(std::make_pair(&mTopicInfos.getColumn(index), new InfoListAdapter())); mTopicInfos.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_ScriptText, ColumnBase::Display_ScriptLines)); + new NestedChildColumn(Columns::ColumnId_ScriptText, ColumnBase::Display_ScriptLines)); // Special conditions - mTopicInfos.addColumn (new NestedParentColumn (Columns::ColumnId_InfoCondition)); - index = mTopicInfos.getColumns()-1; - mTopicInfos.addAdapter (std::make_pair(&mTopicInfos.getColumn(index), new InfoConditionAdapter ())); + mTopicInfos.addColumn(new NestedParentColumn(Columns::ColumnId_InfoCondition)); + index = mTopicInfos.getColumns() - 1; + mTopicInfos.addAdapter(std::make_pair(&mTopicInfos.getColumn(index), new InfoConditionAdapter())); mTopicInfos.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_InfoCondFunc, ColumnBase::Display_InfoCondFunc)); + new NestedChildColumn(Columns::ColumnId_InfoCondFunc, ColumnBase::Display_InfoCondFunc)); // FIXME: don't have dynamic value enum delegate, use Display_String for now mTopicInfos.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_InfoCondVar, ColumnBase::Display_InfoCondVar)); + new NestedChildColumn(Columns::ColumnId_InfoCondVar, ColumnBase::Display_InfoCondVar)); mTopicInfos.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_InfoCondComp, ColumnBase::Display_InfoCondComp)); + new NestedChildColumn(Columns::ColumnId_InfoCondComp, ColumnBase::Display_InfoCondComp)); mTopicInfos.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Value, ColumnBase::Display_Var)); - - mJournalInfos.addColumn (new StringIdColumn (true)); - mJournalInfos.addColumn (new RecordStateColumn); - mJournalInfos.addColumn (new FixedRecordTypeColumn (UniversalId::Type_JournalInfo)); - mJournalInfos.addColumn (new TopicColumn (true)); - mJournalInfos.addColumn (new QuestStatusTypeColumn); - mJournalInfos.addColumn (new QuestIndexColumn); - mJournalInfos.addColumn (new QuestDescriptionColumn); - - mCells.addColumn (new StringIdColumn); - mCells.addColumn (new RecordStateColumn); - mCells.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Cell)); - mCells.addColumn (new NameColumn); - mCells.addColumn (new FlagColumn (Columns::ColumnId_SleepForbidden, ESM::Cell::NoSleep)); - mCells.addColumn (new FlagColumn (Columns::ColumnId_InteriorWater, ESM::Cell::HasWater, + new NestedChildColumn(Columns::ColumnId_Value, ColumnBase::Display_Var)); + + mJournalInfos.addColumn(new StringIdColumn(true)); + mJournalInfos.addColumn(new RecordStateColumn); + mJournalInfos.addColumn(new FixedRecordTypeColumn(UniversalId::Type_JournalInfo)); + mJournalInfos.addColumn(new TopicColumn(true)); + mJournalInfos.addColumn(new QuestStatusTypeColumn); + mJournalInfos.addColumn(new QuestIndexColumn); + mJournalInfos.addColumn(new QuestDescriptionColumn); + + mCells.addColumn(new StringIdColumn); + mCells.addColumn(new RecordStateColumn); + mCells.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Cell)); + mCells.addColumn(new NameColumn(ColumnBase::Display_String64)); + mCells.addColumn(new FlagColumn(Columns::ColumnId_SleepForbidden, ESM::Cell::NoSleep)); + mCells.addColumn(new FlagColumn(Columns::ColumnId_InteriorWater, ESM::Cell::HasWater, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); - mCells.addColumn (new FlagColumn (Columns::ColumnId_InteriorSky, ESM::Cell::QuasiEx, + mCells.addColumn(new FlagColumn(Columns::ColumnId_InteriorSky, ESM::Cell::QuasiEx, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); - mCells.addColumn (new RegionColumn); - mCells.addColumn (new RefNumCounterColumn); + mCells.addColumn(new RegionColumn); + mCells.addColumn(new RefNumCounterColumn); // Misc Cell data - mCells.addColumn (new NestedParentColumn (Columns::ColumnId_Cell, - ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List)); - index = mCells.getColumns()-1; - mCells.addAdapter (std::make_pair(&mCells.getColumn(index), new CellListAdapter ())); + mCells.addColumn(new NestedParentColumn( + Columns::ColumnId_Cell, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List)); + index = mCells.getColumns() - 1; + mCells.addAdapter(std::make_pair(&mCells.getColumn(index), new CellListAdapter())); mCells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Interior, ColumnBase::Display_Boolean, - ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); + new NestedChildColumn(Columns::ColumnId_Interior, ColumnBase::Display_Boolean, + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); mCells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Ambient, ColumnBase::Display_Colour)); + new NestedChildColumn(Columns::ColumnId_Ambient, ColumnBase::Display_Colour)); mCells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Sunlight, ColumnBase::Display_Colour)); + new NestedChildColumn(Columns::ColumnId_Sunlight, ColumnBase::Display_Colour)); mCells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Fog, ColumnBase::Display_Colour)); + new NestedChildColumn(Columns::ColumnId_Fog, ColumnBase::Display_Colour)); mCells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_FogDensity, ColumnBase::Display_Float)); + new NestedChildColumn(Columns::ColumnId_FogDensity, ColumnBase::Display_Float)); mCells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_WaterLevel, ColumnBase::Display_Float)); + new NestedChildColumn(Columns::ColumnId_WaterLevel, ColumnBase::Display_Float)); mCells.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_MapColor, ColumnBase::Display_Colour)); - - mEnchantments.addColumn (new StringIdColumn); - mEnchantments.addColumn (new RecordStateColumn); - mEnchantments.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Enchantment)); - mEnchantments.addColumn (new EnchantmentTypeColumn); - mEnchantments.addColumn (new CostColumn); - mEnchantments.addColumn (new ChargesColumn2); - mEnchantments.addColumn (new FlagColumn (Columns::ColumnId_AutoCalc, ESM::Enchantment::Autocalc)); + new NestedChildColumn(Columns::ColumnId_MapColor, ColumnBase::Display_Colour)); + + mEnchantments.addColumn(new StringIdColumn); + mEnchantments.addColumn(new RecordStateColumn); + mEnchantments.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Enchantment)); + mEnchantments.addColumn(new EnchantmentTypeColumn); + mEnchantments.addColumn(new CostColumn); + mEnchantments.addColumn(new ChargesColumn2); + mEnchantments.addColumn(new FlagColumn(Columns::ColumnId_AutoCalc, ESM::Enchantment::Autocalc)); // Enchantment effects - mEnchantments.addColumn (new NestedParentColumn (Columns::ColumnId_EffectList)); - index = mEnchantments.getColumns()-1; - mEnchantments.addAdapter (std::make_pair(&mEnchantments.getColumn(index), - new EffectsListAdapter ())); + mEnchantments.addColumn(new NestedParentColumn(Columns::ColumnId_EffectList)); + index = mEnchantments.getColumns() - 1; + mEnchantments.addAdapter( + std::make_pair(&mEnchantments.getColumn(index), new EffectsListAdapter())); mEnchantments.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_EffectId, ColumnBase::Display_EffectId)); + new NestedChildColumn(Columns::ColumnId_EffectId, ColumnBase::Display_EffectId)); mEnchantments.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); + new NestedChildColumn(Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); mEnchantments.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); + new NestedChildColumn(Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); mEnchantments.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_EffectRange, ColumnBase::Display_EffectRange)); + new NestedChildColumn(Columns::ColumnId_EffectRange, ColumnBase::Display_EffectRange)); mEnchantments.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_EffectArea, ColumnBase::Display_String)); + new NestedChildColumn(Columns::ColumnId_EffectArea, ColumnBase::Display_String)); mEnchantments.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_Duration, ColumnBase::Display_Integer)); // reuse from light + new NestedChildColumn(Columns::ColumnId_Duration, ColumnBase::Display_Integer)); // reuse from light mEnchantments.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_MinMagnitude, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_MinMagnitude, ColumnBase::Display_Integer)); mEnchantments.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_MaxMagnitude, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_MaxMagnitude, ColumnBase::Display_Integer)); - mBodyParts.addColumn (new StringIdColumn); - mBodyParts.addColumn (new RecordStateColumn); - mBodyParts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_BodyPart)); - mBodyParts.addColumn (new BodyPartTypeColumn); - mBodyParts.addColumn (new VampireColumn); + mBodyParts.addColumn(new StringIdColumn); + mBodyParts.addColumn(new RecordStateColumn); + mBodyParts.addColumn(new FixedRecordTypeColumn(UniversalId::Type_BodyPart)); + mBodyParts.addColumn(new BodyPartTypeColumn); + mBodyParts.addColumn(new VampireColumn); mBodyParts.addColumn(new GenderNpcColumn); - mBodyParts.addColumn (new FlagColumn (Columns::ColumnId_Playable, - ESM::BodyPart::BPF_NotPlayable, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, true)); + mBodyParts.addColumn(new FlagColumn(Columns::ColumnId_Playable, ESM::BodyPart::BPF_NotPlayable, + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, true)); int meshTypeFlags = ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh; - MeshTypeColumn *meshTypeColumn = new MeshTypeColumn(meshTypeFlags); - mBodyParts.addColumn (meshTypeColumn); - mBodyParts.addColumn (new ModelColumn); - mBodyParts.addColumn (new BodyPartRaceColumn(meshTypeColumn)); - - mSoundGens.addColumn (new StringIdColumn); - mSoundGens.addColumn (new RecordStateColumn); - mSoundGens.addColumn (new FixedRecordTypeColumn (UniversalId::Type_SoundGen)); - mSoundGens.addColumn (new CreatureColumn); - mSoundGens.addColumn (new SoundColumn); - mSoundGens.addColumn (new SoundGeneratorTypeColumn); - - mMagicEffects.addColumn (new StringIdColumn); - mMagicEffects.addColumn (new RecordStateColumn); - mMagicEffects.addColumn (new FixedRecordTypeColumn (UniversalId::Type_MagicEffect)); - mMagicEffects.addColumn (new SchoolColumn); - mMagicEffects.addColumn (new BaseCostColumn); - mMagicEffects.addColumn (new EffectTextureColumn (Columns::ColumnId_Icon)); - mMagicEffects.addColumn (new EffectTextureColumn (Columns::ColumnId_Particle)); - mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_CastingObject)); - mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_HitObject)); - mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_AreaObject)); - mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_BoltObject)); - mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_CastingSound)); - mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_HitSound)); - mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_AreaSound)); - mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_BoltSound)); - mMagicEffects.addColumn (new FlagColumn ( - Columns::ColumnId_AllowSpellmaking, ESM::MagicEffect::AllowSpellmaking)); - mMagicEffects.addColumn (new FlagColumn ( - Columns::ColumnId_AllowEnchanting, ESM::MagicEffect::AllowEnchanting)); - mMagicEffects.addColumn (new FlagColumn ( - Columns::ColumnId_NegativeLight, ESM::MagicEffect::NegativeLight)); - mMagicEffects.addColumn (new DescriptionColumn); - - mLand.addColumn (new StringIdColumn); - mLand.addColumn (new RecordStateColumn); - mLand.addColumn (new FixedRecordTypeColumn(UniversalId::Type_Land)); - mLand.addColumn (new LandPluginIndexColumn); - mLand.addColumn (new LandNormalsColumn); - mLand.addColumn (new LandHeightsColumn); - mLand.addColumn (new LandColoursColumn); - mLand.addColumn (new LandTexturesColumn); - - mLandTextures.addColumn (new StringIdColumn(true)); - mLandTextures.addColumn (new RecordStateColumn); - mLandTextures.addColumn (new FixedRecordTypeColumn(UniversalId::Type_LandTexture)); - mLandTextures.addColumn (new LandTextureNicknameColumn); - mLandTextures.addColumn (new LandTexturePluginIndexColumn); - mLandTextures.addColumn (new LandTextureIndexColumn); - mLandTextures.addColumn (new TextureColumn); - - mPathgrids.addColumn (new StringIdColumn); - mPathgrids.addColumn (new RecordStateColumn); - mPathgrids.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Pathgrid)); + MeshTypeColumn* meshTypeColumn = new MeshTypeColumn(meshTypeFlags); + mBodyParts.addColumn(meshTypeColumn); + mBodyParts.addColumn(new ModelColumn); + mBodyParts.addColumn(new BodyPartRaceColumn(meshTypeColumn)); + + mSoundGens.addColumn(new StringIdColumn); + mSoundGens.addColumn(new RecordStateColumn); + mSoundGens.addColumn(new FixedRecordTypeColumn(UniversalId::Type_SoundGen)); + mSoundGens.addColumn(new CreatureColumn); + mSoundGens.addColumn(new SoundColumn); + mSoundGens.addColumn(new SoundGeneratorTypeColumn); + + mMagicEffects.addColumn(new StringIdColumn); + mMagicEffects.addColumn(new RecordStateColumn); + mMagicEffects.addColumn(new FixedRecordTypeColumn(UniversalId::Type_MagicEffect)); + mMagicEffects.addColumn(new SchoolColumn); + mMagicEffects.addColumn(new BaseCostColumn); + mMagicEffects.addColumn(new ProjectileSpeedColumn); + mMagicEffects.addColumn(new EffectTextureColumn(Columns::ColumnId_Icon)); + mMagicEffects.addColumn(new EffectTextureColumn(Columns::ColumnId_Particle)); + mMagicEffects.addColumn(new EffectObjectColumn(Columns::ColumnId_CastingObject)); + mMagicEffects.addColumn(new EffectObjectColumn(Columns::ColumnId_HitObject)); + mMagicEffects.addColumn(new EffectObjectColumn(Columns::ColumnId_AreaObject)); + mMagicEffects.addColumn(new EffectObjectColumn(Columns::ColumnId_BoltObject)); + mMagicEffects.addColumn(new EffectSoundColumn(Columns::ColumnId_CastingSound)); + mMagicEffects.addColumn(new EffectSoundColumn(Columns::ColumnId_HitSound)); + mMagicEffects.addColumn(new EffectSoundColumn(Columns::ColumnId_AreaSound)); + mMagicEffects.addColumn(new EffectSoundColumn(Columns::ColumnId_BoltSound)); + + mMagicEffects.addColumn( + new FlagColumn(Columns::ColumnId_AllowSpellmaking, ESM::MagicEffect::AllowSpellmaking)); + mMagicEffects.addColumn( + new FlagColumn(Columns::ColumnId_AllowEnchanting, ESM::MagicEffect::AllowEnchanting)); + mMagicEffects.addColumn( + new FlagColumn(Columns::ColumnId_NegativeLight, ESM::MagicEffect::NegativeLight)); + mMagicEffects.addColumn(new DescriptionColumn); + + mLand.addColumn(new StringIdColumn); + mLand.addColumn(new RecordStateColumn); + mLand.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Land)); + mLand.addColumn(new LandPluginIndexColumn); + mLand.addColumn(new LandNormalsColumn); + mLand.addColumn(new LandHeightsColumn); + mLand.addColumn(new LandColoursColumn); + mLand.addColumn(new LandTexturesColumn); + + mLandTextures.addColumn(new StringIdColumn); + mLandTextures.addColumn(new RecordStateColumn); + mLandTextures.addColumn(new FixedRecordTypeColumn(UniversalId::Type_LandTexture)); + mLandTextures.addColumn(new LandTextureIndexColumn); + mLandTextures.addColumn(new TextureColumn); + + mPathgrids.addColumn(new StringIdColumn); + mPathgrids.addColumn(new RecordStateColumn); + mPathgrids.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Pathgrid)); // new object deleted in dtor of Collection - mPathgrids.addColumn (new NestedParentColumn (Columns::ColumnId_PathgridPoints)); - index = mPathgrids.getColumns()-1; + mPathgrids.addColumn(new NestedParentColumn(Columns::ColumnId_PathgridPoints)); + index = mPathgrids.getColumns() - 1; // new object deleted in dtor of NestedCollection - mPathgrids.addAdapter (std::make_pair(&mPathgrids.getColumn(index), new PathgridPointListAdapter ())); + mPathgrids.addAdapter(std::make_pair(&mPathgrids.getColumn(index), new PathgridPointListAdapter())); // new objects deleted in dtor of NestableColumn // WARNING: The order of the columns below are assumed in PathgridPointListAdapter + mPathgrids.getNestableColumn(index)->addColumn(new NestedChildColumn( + Columns::ColumnId_PathgridIndex, ColumnBase::Display_Integer, ColumnBase::Flag_Dialogue, false)); mPathgrids.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_PathgridIndex, ColumnBase::Display_Integer, - ColumnBase::Flag_Dialogue, false)); + new NestedChildColumn(Columns::ColumnId_PathgridPosX, ColumnBase::Display_Integer)); mPathgrids.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_PathgridPosX, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_PathgridPosY, ColumnBase::Display_Integer)); mPathgrids.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_PathgridPosY, ColumnBase::Display_Integer)); - mPathgrids.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_PathgridPosZ, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_PathgridPosZ, ColumnBase::Display_Integer)); - mPathgrids.addColumn (new NestedParentColumn (Columns::ColumnId_PathgridEdges)); - index = mPathgrids.getColumns()-1; - mPathgrids.addAdapter (std::make_pair(&mPathgrids.getColumn(index), new PathgridEdgeListAdapter ())); - mPathgrids.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_PathgridEdgeIndex, ColumnBase::Display_Integer, - ColumnBase::Flag_Dialogue, false)); + mPathgrids.addColumn(new NestedParentColumn(Columns::ColumnId_PathgridEdges)); + index = mPathgrids.getColumns() - 1; + mPathgrids.addAdapter(std::make_pair(&mPathgrids.getColumn(index), new PathgridEdgeListAdapter())); + mPathgrids.getNestableColumn(index)->addColumn(new NestedChildColumn( + Columns::ColumnId_PathgridEdgeIndex, ColumnBase::Display_Integer, ColumnBase::Flag_Dialogue, false)); mPathgrids.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_PathgridEdge0, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_PathgridEdge0, ColumnBase::Display_Integer)); mPathgrids.getNestableColumn(index)->addColumn( - new NestedChildColumn (Columns::ColumnId_PathgridEdge1, ColumnBase::Display_Integer)); - - mStartScripts.addColumn (new StringIdColumn); - mStartScripts.addColumn (new RecordStateColumn); - mStartScripts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_StartScript)); - - mRefs.addColumn (new StringIdColumn (true)); - mRefs.addColumn (new RecordStateColumn); - mRefs.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Reference)); - mRefs.addColumn (new CellColumn (true)); - mRefs.addColumn (new OriginalCellColumn); - mRefs.addColumn (new IdColumn); - mRefs.addColumn (new PosColumn (&CellRef::mPos, 0, false)); - mRefs.addColumn (new PosColumn (&CellRef::mPos, 1, false)); - mRefs.addColumn (new PosColumn (&CellRef::mPos, 2, false)); - mRefs.addColumn (new RotColumn (&CellRef::mPos, 0, false)); - mRefs.addColumn (new RotColumn (&CellRef::mPos, 1, false)); - mRefs.addColumn (new RotColumn (&CellRef::mPos, 2, false)); - mRefs.addColumn (new ScaleColumn); - mRefs.addColumn (new OwnerColumn); - mRefs.addColumn (new SoulColumn); - mRefs.addColumn (new FactionColumn); - mRefs.addColumn (new FactionIndexColumn); - mRefs.addColumn (new ChargesColumn); - mRefs.addColumn (new EnchantmentChargesColumn); - mRefs.addColumn (new GoldValueColumn); - mRefs.addColumn (new TeleportColumn); - mRefs.addColumn (new TeleportCellColumn); - mRefs.addColumn (new PosColumn (&CellRef::mDoorDest, 0, true)); - mRefs.addColumn (new PosColumn (&CellRef::mDoorDest, 1, true)); - mRefs.addColumn (new PosColumn (&CellRef::mDoorDest, 2, true)); - mRefs.addColumn (new RotColumn (&CellRef::mDoorDest, 0, true)); - mRefs.addColumn (new RotColumn (&CellRef::mDoorDest, 1, true)); - mRefs.addColumn (new RotColumn (&CellRef::mDoorDest, 2, true)); - mRefs.addColumn (new LockLevelColumn); - mRefs.addColumn (new KeyColumn); - mRefs.addColumn (new TrapColumn); - mRefs.addColumn (new OwnerGlobalColumn); - mRefs.addColumn (new RefNumColumn); - - mFilters.addColumn (new StringIdColumn); - mFilters.addColumn (new RecordStateColumn); - mFilters.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Filter)); - mFilters.addColumn (new FilterColumn); - mFilters.addColumn (new DescriptionColumn); - - mDebugProfiles.addColumn (new StringIdColumn); - mDebugProfiles.addColumn (new RecordStateColumn); - mDebugProfiles.addColumn (new FixedRecordTypeColumn (UniversalId::Type_DebugProfile)); - mDebugProfiles.addColumn (new FlagColumn2 ( - Columns::ColumnId_DefaultProfile, ESM::DebugProfile::Flag_Default)); - mDebugProfiles.addColumn (new FlagColumn2 ( - Columns::ColumnId_BypassNewGame, ESM::DebugProfile::Flag_BypassNewGame)); - mDebugProfiles.addColumn (new FlagColumn2 ( - Columns::ColumnId_GlobalProfile, ESM::DebugProfile::Flag_Global)); - mDebugProfiles.addColumn (new DescriptionColumn); - mDebugProfiles.addColumn (new ScriptColumn ( - ScriptColumn::Type_Lines)); - - mMetaData.appendBlankRecord ("sys::meta"); - - mMetaData.addColumn (new StringIdColumn (true)); - mMetaData.addColumn (new RecordStateColumn); - mMetaData.addColumn (new FixedRecordTypeColumn (UniversalId::Type_MetaData)); - mMetaData.addColumn (new FormatColumn); - mMetaData.addColumn (new AuthorColumn); - mMetaData.addColumn (new FileDescriptionColumn); - - addModel (new IdTable (&mGlobals), UniversalId::Type_Global); - addModel (new IdTable (&mGmsts), UniversalId::Type_Gmst); - addModel (new IdTable (&mSkills), UniversalId::Type_Skill); - addModel (new IdTable (&mClasses), UniversalId::Type_Class); - addModel (new IdTree (&mFactions, &mFactions), UniversalId::Type_Faction); - addModel (new IdTree (&mRaces, &mRaces), UniversalId::Type_Race); - addModel (new IdTable (&mSounds), UniversalId::Type_Sound); - addModel (new IdTable (&mScripts), UniversalId::Type_Script); - addModel (new IdTree (&mRegions, &mRegions), UniversalId::Type_Region); - addModel (new IdTree (&mBirthsigns, &mBirthsigns), UniversalId::Type_Birthsign); - addModel (new IdTree (&mSpells, &mSpells), UniversalId::Type_Spell); - addModel (new IdTable (&mTopics), UniversalId::Type_Topic); - addModel (new IdTable (&mJournals), UniversalId::Type_Journal); - addModel (new IdTree (&mTopicInfos, &mTopicInfos, IdTable::Feature_ReorderWithinTopic), - UniversalId::Type_TopicInfo); - addModel (new IdTable (&mJournalInfos, IdTable::Feature_ReorderWithinTopic), UniversalId::Type_JournalInfo); - addModel (new IdTree (&mCells, &mCells, IdTable::Feature_ViewId), UniversalId::Type_Cell); - addModel (new IdTree (&mEnchantments, &mEnchantments), UniversalId::Type_Enchantment); - addModel (new IdTable (&mBodyParts), UniversalId::Type_BodyPart); - addModel (new IdTable (&mSoundGens), UniversalId::Type_SoundGen); - addModel (new IdTable (&mMagicEffects), UniversalId::Type_MagicEffect); - addModel (new IdTable (&mLand, IdTable::Feature_AllowTouch), UniversalId::Type_Land); - addModel (new LandTextureIdTable (&mLandTextures), UniversalId::Type_LandTexture); - addModel (new IdTree (&mPathgrids, &mPathgrids), UniversalId::Type_Pathgrid); - addModel (new IdTable (&mStartScripts), UniversalId::Type_StartScript); - addModel (new IdTree (&mReferenceables, &mReferenceables, IdTable::Feature_Preview), - UniversalId::Type_Referenceable); - addModel (new IdTable (&mRefs, IdTable::Feature_ViewCell | IdTable::Feature_Preview), UniversalId::Type_Reference); - addModel (new IdTable (&mFilters), UniversalId::Type_Filter); - addModel (new IdTable (&mDebugProfiles), UniversalId::Type_DebugProfile); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Meshes)), - UniversalId::Type_Mesh); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Icons)), - UniversalId::Type_Icon); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Musics)), - UniversalId::Type_Music); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_SoundsRes)), - UniversalId::Type_SoundRes); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Textures)), - UniversalId::Type_Texture); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Videos)), - UniversalId::Type_Video); - addModel (new IdTable (&mMetaData), UniversalId::Type_MetaData); - - mActorAdapter.reset(new ActorAdapter(*this)); + new NestedChildColumn(Columns::ColumnId_PathgridEdge1, ColumnBase::Display_Integer)); + + mStartScripts.addColumn(new StringIdColumn); + mStartScripts.addColumn(new RecordStateColumn); + mStartScripts.addColumn(new FixedRecordTypeColumn(UniversalId::Type_StartScript)); + + mRefs.addColumn(new StringIdColumn(true)); + mRefs.addColumn(new RecordStateColumn); + mRefs.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Reference)); + mRefs.addColumn(new CellColumn(true)); + mRefs.addColumn(new OriginalCellColumn); + mRefs.addColumn(new IdColumn); + mRefs.addColumn(new PosColumn(&CellRef::mPos, 0, false)); + mRefs.addColumn(new PosColumn(&CellRef::mPos, 1, false)); + mRefs.addColumn(new PosColumn(&CellRef::mPos, 2, false)); + mRefs.addColumn(new RotColumn(&CellRef::mPos, 0, false)); + mRefs.addColumn(new RotColumn(&CellRef::mPos, 1, false)); + mRefs.addColumn(new RotColumn(&CellRef::mPos, 2, false)); + mRefs.addColumn(new ScaleColumn); + mRefs.addColumn(new OwnerColumn); + mRefs.addColumn(new SoulColumn); + mRefs.addColumn(new FactionColumn); + mRefs.addColumn(new FactionIndexColumn); + mRefs.addColumn(new ChargesColumn); + mRefs.addColumn(new EnchantmentChargesColumn); + mRefs.addColumn(new StackSizeColumn); + mRefs.addColumn(new TeleportColumn( + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); + mRefs.addColumn(new TeleportCellColumn); + mRefs.addColumn(new PosColumn(&CellRef::mDoorDest, 0, true)); + mRefs.addColumn(new PosColumn(&CellRef::mDoorDest, 1, true)); + mRefs.addColumn(new PosColumn(&CellRef::mDoorDest, 2, true)); + mRefs.addColumn(new RotColumn(&CellRef::mDoorDest, 0, true)); + mRefs.addColumn(new RotColumn(&CellRef::mDoorDest, 1, true)); + mRefs.addColumn(new RotColumn(&CellRef::mDoorDest, 2, true)); + mRefs.addColumn(new IsLockedColumn( + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); + mRefs.addColumn(new LockLevelColumn); + mRefs.addColumn(new KeyColumn); + mRefs.addColumn(new TrapColumn); + mRefs.addColumn(new OwnerGlobalColumn); + mRefs.addColumn(new RefNumColumn); + + mFilters.addColumn(new StringIdColumn); + mFilters.addColumn(new RecordStateColumn); + mFilters.addColumn(new FixedRecordTypeColumn(UniversalId::Type_Filter)); + mFilters.addColumn(new FilterColumn); + mFilters.addColumn(new DescriptionColumn); + + mDebugProfiles.addColumn(new StringIdColumn); + mDebugProfiles.addColumn(new RecordStateColumn); + mDebugProfiles.addColumn(new FixedRecordTypeColumn(UniversalId::Type_DebugProfile)); + mDebugProfiles.addColumn( + new FlagColumn2(Columns::ColumnId_DefaultProfile, ESM::DebugProfile::Flag_Default)); + mDebugProfiles.addColumn( + new FlagColumn2(Columns::ColumnId_BypassNewGame, ESM::DebugProfile::Flag_BypassNewGame)); + mDebugProfiles.addColumn( + new FlagColumn2(Columns::ColumnId_GlobalProfile, ESM::DebugProfile::Flag_Global)); + mDebugProfiles.addColumn(new DescriptionColumn); + mDebugProfiles.addColumn(new ScriptColumn(ScriptColumn::Type_Lines)); + + mSelectionGroups.addColumn(new StringIdColumn); + mSelectionGroups.addColumn(new RecordStateColumn); + mSelectionGroups.addColumn(new FixedRecordTypeColumn(UniversalId::Type_SelectionGroup)); + mSelectionGroups.addColumn(new SelectionGroupColumn); + + mMetaData.appendBlankRecord(ESM::RefId::stringRefId("sys::meta")); + + mMetaData.addColumn(new StringIdColumn(true)); + mMetaData.addColumn(new RecordStateColumn); + mMetaData.addColumn(new FixedRecordTypeColumn(UniversalId::Type_MetaData)); + mMetaData.addColumn(new FormatColumn); + mMetaData.addColumn(new AuthorColumn); + mMetaData.addColumn(new FileDescriptionColumn); + + addModel(new IdTable(&mGlobals), UniversalId::Type_Global); + addModel(new IdTable(&mGmsts), UniversalId::Type_Gmst); + addModel(new IdTable(&mSkills), UniversalId::Type_Skill); + addModel(new IdTable(&mClasses), UniversalId::Type_Class); + addModel(new IdTree(&mFactions, &mFactions), UniversalId::Type_Faction); + addModel(new IdTree(&mRaces, &mRaces), UniversalId::Type_Race); + addModel(new IdTable(&mSounds), UniversalId::Type_Sound); + addModel(new IdTable(&mScripts), UniversalId::Type_Script); + addModel(new IdTree(&mRegions, &mRegions), UniversalId::Type_Region); + addModel(new IdTree(&mBirthsigns, &mBirthsigns), UniversalId::Type_Birthsign); + addModel(new IdTree(&mSpells, &mSpells), UniversalId::Type_Spell); + addModel(new IdTable(&mTopics), UniversalId::Type_Topic); + addModel(new IdTable(&mJournals), UniversalId::Type_Journal); + addModel(new IdTree(&mTopicInfos, &mTopicInfos, IdTable::Feature_ReorderWithinTopic), UniversalId::Type_TopicInfo); + addModel(new IdTable(&mJournalInfos, IdTable::Feature_ReorderWithinTopic), UniversalId::Type_JournalInfo); + addModel(new IdTree(&mCells, &mCells, IdTable::Feature_ViewId), UniversalId::Type_Cell); + addModel(new IdTree(&mEnchantments, &mEnchantments), UniversalId::Type_Enchantment); + addModel(new IdTable(&mBodyParts), UniversalId::Type_BodyPart); + addModel(new IdTable(&mSoundGens), UniversalId::Type_SoundGen); + addModel(new IdTable(&mMagicEffects), UniversalId::Type_MagicEffect); + addModel(new IdTable(&mLand, IdTable::Feature_AllowTouch), UniversalId::Type_Land); + addModel(new LandTextureIdTable(&mLandTextures), UniversalId::Type_LandTexture); + addModel(new IdTree(&mPathgrids, &mPathgrids), UniversalId::Type_Pathgrid); + addModel(new IdTable(&mStartScripts), UniversalId::Type_StartScript); + addModel(new IdTree(&mReferenceables, &mReferenceables, IdTable::Feature_Preview), UniversalId::Type_Referenceable); + addModel(new IdTable(&mRefs, IdTable::Feature_ViewCell | IdTable::Feature_Preview), UniversalId::Type_Reference); + addModel(new IdTable(&mFilters), UniversalId::Type_Filter); + addModel(new IdTable(&mDebugProfiles), UniversalId::Type_DebugProfile); + addModel(new ResourceTable(&mResourcesManager.get(UniversalId::Type_Meshes)), UniversalId::Type_Mesh); + addModel(new ResourceTable(&mResourcesManager.get(UniversalId::Type_Icons)), UniversalId::Type_Icon); + addModel(new ResourceTable(&mResourcesManager.get(UniversalId::Type_Musics)), UniversalId::Type_Music); + addModel(new ResourceTable(&mResourcesManager.get(UniversalId::Type_SoundsRes)), UniversalId::Type_SoundRes); + addModel(new ResourceTable(&mResourcesManager.get(UniversalId::Type_Textures)), UniversalId::Type_Texture); + addModel(new ResourceTable(&mResourcesManager.get(UniversalId::Type_Videos)), UniversalId::Type_Video); + addModel(new IdTable(&mMetaData), UniversalId::Type_MetaData); + addModel(new IdTable(&mSelectionGroups), UniversalId::Type_SelectionGroup); + + mActorAdapter = std::make_unique(*this); mRefLoadCache.clear(); // clear here rather than startLoading() and continueLoading() for multiple content files } CSMWorld::Data::~Data() { - for (std::vector::iterator iter (mModels.begin()); iter!=mModels.end(); ++iter) + for (std::vector::iterator iter(mModels.begin()); iter != mModels.end(); ++iter) delete *iter; - - delete mReader; } std::shared_ptr CSMWorld::Data::getResourceSystem() @@ -734,7 +807,6 @@ CSMWorld::IdCollection& CSMWorld::Data::getSpells() return mSpells; } - const CSMWorld::IdCollection& CSMWorld::Data::getTopics() const { return mTopics; @@ -845,6 +917,16 @@ CSMWorld::IdCollection& CSMWorld::Data::getDebugProfiles() return mDebugProfiles; } +CSMWorld::IdCollection& CSMWorld::Data::getSelectionGroups() +{ + return mSelectionGroups; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getSelectionGroups() const +{ + return mSelectionGroups; +} + const CSMWorld::IdCollection& CSMWorld::Data::getLand() const { return mLand; @@ -855,12 +937,12 @@ CSMWorld::IdCollection& CSMWorld::Data::getLand() return mLand; } -const CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() const +const CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() const { return mLandTextures; } -CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() +CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() { return mLandTextures; } @@ -905,39 +987,39 @@ CSMWorld::IdCollection& CSMWorld::Data::getStartScripts() return mStartScripts; } -const CSMWorld::Resources& CSMWorld::Data::getResources (const UniversalId& id) const +const CSMWorld::Resources& CSMWorld::Data::getResources(const UniversalId& id) const { - return mResourcesManager.get (id.getType()); + return mResourcesManager.get(id.getType()); } const CSMWorld::MetaData& CSMWorld::Data::getMetaData() const { - return mMetaData.getRecord (0).get(); + return mMetaData.getRecord(0).get(); } -void CSMWorld::Data::setMetaData (const MetaData& metaData) +void CSMWorld::Data::setMetaData(const MetaData& metaData) { - Record record (RecordBase::State_ModifiedOnly, nullptr, &metaData); - mMetaData.setRecord (0, record); + mMetaData.setRecord( + 0, std::make_unique>(Record(RecordBase::State_ModifiedOnly, nullptr, &metaData))); } -QAbstractItemModel *CSMWorld::Data::getTableModel (const CSMWorld::UniversalId& id) +QAbstractItemModel* CSMWorld::Data::getTableModel(const CSMWorld::UniversalId& id) { - std::map::iterator iter = mModelIndex.find (id.getType()); + std::map::iterator iter = mModelIndex.find(id.getType()); - if (iter==mModelIndex.end()) + if (iter == mModelIndex.end()) { // try creating missing (secondary) tables on the fly // // Note: We create these tables here so we don't have to deal with them during load/initial // construction of the ESX data where no update signals are available. - if (id.getType()==UniversalId::Type_RegionMap) + if (id.getType() == UniversalId::Type_RegionMap) { - RegionMap *table = nullptr; - addModel (table = new RegionMap (*this), UniversalId::Type_RegionMap, false); + RegionMap* table = nullptr; + addModel(table = new RegionMap(*this), UniversalId::Type_RegionMap, false); return table; } - throw std::logic_error ("No table model available for " + id.toString()); + throw std::logic_error("No table model available for " + id.toString()); } return iter->second; @@ -958,21 +1040,34 @@ void CSMWorld::Data::merge() mGlobals.merge(); } -int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base, bool project) +int CSMWorld::Data::getTotalRecords(const std::vector& files) { - // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand loading - std::shared_ptr ptr(mReader); - mReaders.push_back(ptr); - mReader = nullptr; + int records = 0; - mDialogue = nullptr; + std::unique_ptr reader = std::make_unique(); - mReader = new ESM::ESMReader; - mReader->setEncoder (&mEncoder); - mReader->setIndex((project || !base) ? 0 : mReaderIndex++); - mReader->open (path.string()); + for (const auto& file : files) + { + if (!std::filesystem::exists(file)) + continue; - mContentFileNames.insert(std::make_pair(path.filename().string(), mReader->getIndex())); + reader->open(file); + records += reader->getRecordCount(); + reader->close(); + } + + return records; +} + +int CSMWorld::Data::startLoading(const std::filesystem::path& path, bool base, bool project) +{ + Log(Debug::Info) << "Loading content file " << path; + + mDialogue = nullptr; + + ESM::ReadersCache::BusyItem reader = mReaders.get(mReaderIndex++); + reader->setEncoder(&mEncoder); + reader->open(path); mBase = base; mProject = project; @@ -980,79 +1075,71 @@ int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base if (!mProject && !mBase) { MetaData metaData; - metaData.mId = "sys::meta"; - metaData.load (*mReader); + metaData.mId = ESM::RefId::stringRefId("sys::meta"); + metaData.load(*reader); - mMetaData.setRecord (0, Record (RecordBase::State_ModifiedOnly, nullptr, &metaData)); + mMetaData.setRecord(0, + std::make_unique>(Record(RecordBase::State_ModifiedOnly, nullptr, &metaData))); } - return mReader->getRecordCount(); + return reader->getRecordCount(); } void CSMWorld::Data::loadFallbackEntries() { // Load default marker definitions, if game files do not have them for some reason - std::pair staticMarkers[] = { - std::make_pair("DivineMarker", "marker_divine.nif"), - std::make_pair("DoorMarker", "marker_arrow.nif"), - std::make_pair("NorthMarker", "marker_north.nif"), - std::make_pair("TempleMarker", "marker_temple.nif"), - std::make_pair("TravelMarker", "marker_travel.nif") - }; - - std::pair doorMarkers[] = { - std::make_pair("PrisonMarker", "marker_prison.nif") - }; - - for (const auto &marker : staticMarkers) + std::pair staticMarkers[] = { std::make_pair("DivineMarker", "marker_divine.nif"), + std::make_pair("DoorMarker", "marker_arrow.nif"), std::make_pair("NorthMarker", "marker_north.nif"), + std::make_pair("TempleMarker", "marker_temple.nif"), std::make_pair("TravelMarker", "marker_travel.nif") }; + + std::pair doorMarkers[] = { std::make_pair("PrisonMarker", "marker_prison.nif") }; + + for (const auto& [id, model] : staticMarkers) { - if (mReferenceables.searchId (marker.first)==-1) + const ESM::RefId refId = ESM::RefId::stringRefId(id); + if (mReferenceables.searchId(refId) == -1) { ESM::Static newMarker; - newMarker.mId = marker.first; - newMarker.mModel = marker.second; - CSMWorld::Record record; - record.mBase = newMarker; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - mReferenceables.appendRecord (record, CSMWorld::UniversalId::Type_Static); + newMarker.mId = refId; + newMarker.mModel = model; + newMarker.mRecordFlags = 0; + auto record = std::make_unique>(); + record->mBase = std::move(newMarker); + record->mState = CSMWorld::RecordBase::State_BaseOnly; + mReferenceables.appendRecord(std::move(record), CSMWorld::UniversalId::Type_Static); } } - for (const auto &marker : doorMarkers) + for (const auto& [id, model] : doorMarkers) { - if (mReferenceables.searchId (marker.first)==-1) + const ESM::RefId refId = ESM::RefId::stringRefId(id); + if (mReferenceables.searchId(refId) == -1) { ESM::Door newMarker; - newMarker.mId = marker.first; - newMarker.mModel = marker.second; - CSMWorld::Record record; - record.mBase = newMarker; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - mReferenceables.appendRecord (record, CSMWorld::UniversalId::Type_Door); + newMarker.mId = refId; + newMarker.mModel = model; + newMarker.mRecordFlags = 0; + auto record = std::make_unique>(); + record->mBase = std::move(newMarker); + record->mState = CSMWorld::RecordBase::State_BaseOnly; + mReferenceables.appendRecord(std::move(record), CSMWorld::UniversalId::Type_Door); } } } -bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) +bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) { - if (!mReader) - throw std::logic_error ("can't continue loading, because no load has been started"); + if (mReaderIndex == 0) + throw std::logic_error("can't continue loading, because no load has been started"); + ESM::ReadersCache::BusyItem reader = mReaders.get(mReaderIndex - 1); + if (!reader->isOpen()) + throw std::logic_error("can't continue loading, because no load has been started"); + reader->setEncoder(&mEncoder); + reader->setIndex(static_cast(mReaderIndex - 1)); + reader->resolveParentFileIndices(mReaders); - if (!mReader->hasMoreRecs()) + if (!reader->hasMoreRecs()) { - if (mBase) - { - // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand loading. - // We don't store non-base reader, because everything going into modified will be - // fully loaded during the initial loading process. - std::shared_ptr ptr(mReader); - mReaders.push_back(ptr); - } - else - delete mReader; - - mReader = nullptr; - mDialogue = nullptr; loadFallbackEntries(); @@ -1060,112 +1147,187 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) return true; } - ESM::NAME n = mReader->getRecName(); - mReader->getRecHeader(); + ESM::NAME n = reader->getRecName(); + reader->getRecHeader(); bool unhandledRecord = false; - switch (n.intval) + switch (n.toInt()) { - case ESM::REC_GLOB: mGlobals.load (*mReader, mBase); break; - case ESM::REC_GMST: mGmsts.load (*mReader, mBase); break; - case ESM::REC_SKIL: mSkills.load (*mReader, mBase); break; - case ESM::REC_CLAS: mClasses.load (*mReader, mBase); break; - case ESM::REC_FACT: mFactions.load (*mReader, mBase); break; - case ESM::REC_RACE: mRaces.load (*mReader, mBase); break; - case ESM::REC_SOUN: mSounds.load (*mReader, mBase); break; - case ESM::REC_SCPT: mScripts.load (*mReader, mBase); break; - case ESM::REC_REGN: mRegions.load (*mReader, mBase); break; - case ESM::REC_BSGN: mBirthsigns.load (*mReader, mBase); break; - case ESM::REC_SPEL: mSpells.load (*mReader, mBase); break; - case ESM::REC_ENCH: mEnchantments.load (*mReader, mBase); break; - case ESM::REC_BODY: mBodyParts.load (*mReader, mBase); break; - case ESM::REC_SNDG: mSoundGens.load (*mReader, mBase); break; - case ESM::REC_MGEF: mMagicEffects.load (*mReader, mBase); break; - case ESM::REC_PGRD: mPathgrids.load (*mReader, mBase); break; - case ESM::REC_SSCR: mStartScripts.load (*mReader, mBase); break; - - case ESM::REC_LTEX: mLandTextures.load (*mReader, mBase); break; - - case ESM::REC_LAND: mLand.load(*mReader, mBase); break; + case ESM::REC_GLOB: + mGlobals.load(*reader, mBase); + break; + case ESM::REC_GMST: + mGmsts.load(*reader, mBase); + break; + case ESM::REC_SKIL: + mSkills.load(*reader, mBase); + break; + case ESM::REC_CLAS: + mClasses.load(*reader, mBase); + break; + case ESM::REC_FACT: + mFactions.load(*reader, mBase); + break; + case ESM::REC_RACE: + mRaces.load(*reader, mBase); + break; + case ESM::REC_SOUN: + mSounds.load(*reader, mBase); + break; + case ESM::REC_SCPT: + mScripts.load(*reader, mBase); + break; + case ESM::REC_REGN: + mRegions.load(*reader, mBase); + break; + case ESM::REC_BSGN: + mBirthsigns.load(*reader, mBase); + break; + case ESM::REC_SPEL: + mSpells.load(*reader, mBase); + break; + case ESM::REC_ENCH: + mEnchantments.load(*reader, mBase); + break; + case ESM::REC_BODY: + mBodyParts.load(*reader, mBase); + break; + case ESM::REC_SNDG: + mSoundGens.load(*reader, mBase); + break; + case ESM::REC_MGEF: + mMagicEffects.load(*reader, mBase); + break; + case ESM::REC_PGRD: + mPathgrids.load(*reader, mBase); + break; + case ESM::REC_SSCR: + mStartScripts.load(*reader, mBase); + break; + + case ESM::REC_LTEX: + mLandTextures.load(*reader, mBase); + break; + + case ESM::REC_LAND: + mLand.load(*reader, mBase); + break; case ESM::REC_CELL: { - int index = mCells.load (*mReader, mBase); + int index = mCells.load(*reader, mBase); if (index < 0 || index >= mCells.getSize()) { // log an error and continue loading the refs to the last loaded cell - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_None); - messages.add (id, "Logic error: cell index out of bounds", "", CSMDoc::Message::Severity_Error); - index = mCells.getSize()-1; + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_None); + messages.add(id, "Logic error: cell index out of bounds", "", CSMDoc::Message::Severity_Error); + index = mCells.getSize() - 1; } - std::string cellId = Misc::StringUtils::lowerCase (mCells.getId (index)); - mRefs.load (*mReader, index, mBase, mRefLoadCache[cellId], messages); + + mRefs.load(*reader, index, mBase, mRefLoadCache[mCells.getId(index)], messages); break; } - case ESM::REC_ACTI: mReferenceables.load (*mReader, mBase, UniversalId::Type_Activator); break; - case ESM::REC_ALCH: mReferenceables.load (*mReader, mBase, UniversalId::Type_Potion); break; - case ESM::REC_APPA: mReferenceables.load (*mReader, mBase, UniversalId::Type_Apparatus); break; - case ESM::REC_ARMO: mReferenceables.load (*mReader, mBase, UniversalId::Type_Armor); break; - case ESM::REC_BOOK: mReferenceables.load (*mReader, mBase, UniversalId::Type_Book); break; - case ESM::REC_CLOT: mReferenceables.load (*mReader, mBase, UniversalId::Type_Clothing); break; - case ESM::REC_CONT: mReferenceables.load (*mReader, mBase, UniversalId::Type_Container); break; - case ESM::REC_CREA: mReferenceables.load (*mReader, mBase, UniversalId::Type_Creature); break; - case ESM::REC_DOOR: mReferenceables.load (*mReader, mBase, UniversalId::Type_Door); break; - case ESM::REC_INGR: mReferenceables.load (*mReader, mBase, UniversalId::Type_Ingredient); break; + case ESM::REC_ACTI: + mReferenceables.load(*reader, mBase, UniversalId::Type_Activator); + break; + case ESM::REC_ALCH: + mReferenceables.load(*reader, mBase, UniversalId::Type_Potion); + break; + case ESM::REC_APPA: + mReferenceables.load(*reader, mBase, UniversalId::Type_Apparatus); + break; + case ESM::REC_ARMO: + mReferenceables.load(*reader, mBase, UniversalId::Type_Armor); + break; + case ESM::REC_BOOK: + mReferenceables.load(*reader, mBase, UniversalId::Type_Book); + break; + case ESM::REC_CLOT: + mReferenceables.load(*reader, mBase, UniversalId::Type_Clothing); + break; + case ESM::REC_CONT: + mReferenceables.load(*reader, mBase, UniversalId::Type_Container); + break; + case ESM::REC_CREA: + mReferenceables.load(*reader, mBase, UniversalId::Type_Creature); + break; + case ESM::REC_DOOR: + mReferenceables.load(*reader, mBase, UniversalId::Type_Door); + break; + case ESM::REC_INGR: + mReferenceables.load(*reader, mBase, UniversalId::Type_Ingredient); + break; case ESM::REC_LEVC: - mReferenceables.load (*mReader, mBase, UniversalId::Type_CreatureLevelledList); break; + mReferenceables.load(*reader, mBase, UniversalId::Type_CreatureLevelledList); + break; case ESM::REC_LEVI: - mReferenceables.load (*mReader, mBase, UniversalId::Type_ItemLevelledList); break; - case ESM::REC_LIGH: mReferenceables.load (*mReader, mBase, UniversalId::Type_Light); break; - case ESM::REC_LOCK: mReferenceables.load (*mReader, mBase, UniversalId::Type_Lockpick); break; + mReferenceables.load(*reader, mBase, UniversalId::Type_ItemLevelledList); + break; + case ESM::REC_LIGH: + mReferenceables.load(*reader, mBase, UniversalId::Type_Light); + break; + case ESM::REC_LOCK: + mReferenceables.load(*reader, mBase, UniversalId::Type_Lockpick); + break; case ESM::REC_MISC: - mReferenceables.load (*mReader, mBase, UniversalId::Type_Miscellaneous); break; - case ESM::REC_NPC_: mReferenceables.load (*mReader, mBase, UniversalId::Type_Npc); break; - case ESM::REC_PROB: mReferenceables.load (*mReader, mBase, UniversalId::Type_Probe); break; - case ESM::REC_REPA: mReferenceables.load (*mReader, mBase, UniversalId::Type_Repair); break; - case ESM::REC_STAT: mReferenceables.load (*mReader, mBase, UniversalId::Type_Static); break; - case ESM::REC_WEAP: mReferenceables.load (*mReader, mBase, UniversalId::Type_Weapon); break; + mReferenceables.load(*reader, mBase, UniversalId::Type_Miscellaneous); + break; + case ESM::REC_NPC_: + mReferenceables.load(*reader, mBase, UniversalId::Type_Npc); + break; + case ESM::REC_PROB: + mReferenceables.load(*reader, mBase, UniversalId::Type_Probe); + break; + case ESM::REC_REPA: + mReferenceables.load(*reader, mBase, UniversalId::Type_Repair); + break; + case ESM::REC_STAT: + mReferenceables.load(*reader, mBase, UniversalId::Type_Static); + break; + case ESM::REC_WEAP: + mReferenceables.load(*reader, mBase, UniversalId::Type_Weapon); + break; case ESM::REC_DIAL: { ESM::Dialogue record; bool isDeleted = false; - record.load (*mReader, isDeleted); + record.load(*reader, isDeleted); if (isDeleted) { // record vector can be shuffled around which would make pointer to record invalid mDialogue = nullptr; - if (mJournals.tryDelete (record.mId)) + if (mJournals.tryDelete(record.mId)) { - mJournalInfos.removeDialogueInfos(record.mId); + removeDialogueInfos(record.mId, mJournalInfoOrder, mJournalInfos); } - else if (mTopics.tryDelete (record.mId)) + else if (mTopics.tryDelete(record.mId)) { - mTopicInfos.removeDialogueInfos(record.mId); + removeDialogueInfos(record.mId, mTopicInfoOrder, mTopicInfos); } else { - messages.add (UniversalId::Type_None, - "Trying to delete dialogue record " + record.mId + " which does not exist", - "", CSMDoc::Message::Severity_Warning); + messages.add(UniversalId::Type_None, + "Trying to delete dialogue record " + record.mId.getRefIdString() + " which does not exist", "", + CSMDoc::Message::Severity_Warning); } } else { if (record.mType == ESM::Dialogue::Journal) { - mJournals.load (record, mBase); - mDialogue = &mJournals.getRecord (record.mId).get(); + mJournals.load(record, mBase); + mDialogue = &mJournals.getRecord(record.mId).get(); } else { - mTopics.load (record, mBase); - mDialogue = &mTopics.getRecord (record.mId).get(); + mTopics.load(record, mBase); + mDialogue = &mTopics.getRecord(record.mId).get(); } } @@ -1176,17 +1338,17 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) { if (!mDialogue) { - messages.add (UniversalId::Type_None, - "Found info record not following a dialogue record", "", CSMDoc::Message::Severity_Error); + messages.add(UniversalId::Type_None, "Found info record not following a dialogue record", "", + CSMDoc::Message::Severity_Error); - mReader->skipRecord(); + reader->skipRecord(); break; } - if (mDialogue->mType==ESM::Dialogue::Journal) - mJournalInfos.load (*mReader, mBase, *mDialogue); + if (mDialogue->mType == ESM::Dialogue::Journal) + mJournalInfos.load(*reader, mBase, *mDialogue, mJournalInfoOrder); else - mTopicInfos.load (*mReader, mBase, *mDialogue); + mTopicInfos.load(*reader, mBase, *mDialogue, mTopicInfoOrder); break; } @@ -1199,7 +1361,7 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) break; } - mFilters.load (*mReader, mBase); + mFilters.load(*reader, mBase); break; case ESM::REC_DBGP: @@ -1210,7 +1372,18 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) break; } - mDebugProfiles.load (*mReader, mBase); + mDebugProfiles.load(*reader, mBase); + break; + + case ESM::REC_SELG: + + if (!mProject) + { + unhandledRecord = true; + break; + } + + mSelectionGroups.load(*reader, mBase); break; default: @@ -1220,88 +1393,70 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) if (unhandledRecord) { - messages.add (UniversalId::Type_None, "Unsupported record type: " + n.toString(), "", - CSMDoc::Message::Severity_Error); + messages.add( + UniversalId::Type_None, "Unsupported record type: " + n.toString(), "", CSMDoc::Message::Severity_Error); - mReader->skipRecord(); + reader->skipRecord(); } return false; } -bool CSMWorld::Data::hasId (const std::string& id) const -{ - return - getGlobals().searchId (id)!=-1 || - getGmsts().searchId (id)!=-1 || - getSkills().searchId (id)!=-1 || - getClasses().searchId (id)!=-1 || - getFactions().searchId (id)!=-1 || - getRaces().searchId (id)!=-1 || - getSounds().searchId (id)!=-1 || - getScripts().searchId (id)!=-1 || - getRegions().searchId (id)!=-1 || - getBirthsigns().searchId (id)!=-1 || - getSpells().searchId (id)!=-1 || - getTopics().searchId (id)!=-1 || - getJournals().searchId (id)!=-1 || - getCells().searchId (id)!=-1 || - getEnchantments().searchId (id)!=-1 || - getBodyParts().searchId (id)!=-1 || - getSoundGens().searchId (id)!=-1 || - getMagicEffects().searchId (id)!=-1 || - getReferenceables().searchId (id)!=-1; -} - -int CSMWorld::Data::count (RecordBase::State state) const -{ - return - count (state, mGlobals) + - count (state, mGmsts) + - count (state, mSkills) + - count (state, mClasses) + - count (state, mFactions) + - count (state, mRaces) + - count (state, mSounds) + - count (state, mScripts) + - count (state, mRegions) + - count (state, mBirthsigns) + - count (state, mSpells) + - count (state, mCells) + - count (state, mEnchantments) + - count (state, mBodyParts) + - count (state, mLand) + - count (state, mLandTextures) + - count (state, mSoundGens) + - count (state, mMagicEffects) + - count (state, mReferenceables) + - count (state, mPathgrids); -} - -std::vector CSMWorld::Data::getIds (bool listDeleted) const -{ - std::vector ids; - - appendIds (ids, mGlobals, listDeleted); - appendIds (ids, mGmsts, listDeleted); - appendIds (ids, mClasses, listDeleted); - appendIds (ids, mFactions, listDeleted); - appendIds (ids, mRaces, listDeleted); - appendIds (ids, mSounds, listDeleted); - appendIds (ids, mScripts, listDeleted); - appendIds (ids, mRegions, listDeleted); - appendIds (ids, mBirthsigns, listDeleted); - appendIds (ids, mSpells, listDeleted); - appendIds (ids, mTopics, listDeleted); - appendIds (ids, mJournals, listDeleted); - appendIds (ids, mCells, listDeleted); - appendIds (ids, mEnchantments, listDeleted); - appendIds (ids, mBodyParts, listDeleted); - appendIds (ids, mSoundGens, listDeleted); - appendIds (ids, mMagicEffects, listDeleted); - appendIds (ids, mReferenceables, listDeleted); - - std::sort (ids.begin(), ids.end()); +void CSMWorld::Data::finishLoading() +{ + mTopicInfos.sort(mTopicInfoOrder); + mJournalInfos.sort(mJournalInfoOrder); + // Release file locks so we can actually write to the file we're editing + mReaders.clear(); +} + +bool CSMWorld::Data::hasId(const std::string& id) const +{ + const ESM::RefId refId = ESM::RefId::stringRefId(id); + return getGlobals().searchId(refId) != -1 || getGmsts().searchId(refId) != -1 || getSkills().searchId(refId) != -1 + || getClasses().searchId(refId) != -1 || getFactions().searchId(refId) != -1 || getRaces().searchId(refId) != -1 + || getSounds().searchId(refId) != -1 || getScripts().searchId(refId) != -1 || getRegions().searchId(refId) != -1 + || getBirthsigns().searchId(refId) != -1 || getSpells().searchId(refId) != -1 + || getTopics().searchId(refId) != -1 || getJournals().searchId(refId) != -1 || getCells().searchId(refId) != -1 + || getEnchantments().searchId(refId) != -1 || getBodyParts().searchId(refId) != -1 + || getSoundGens().searchId(refId) != -1 || getMagicEffects().searchId(refId) != -1 + || getReferenceables().searchId(refId) != -1; +} + +int CSMWorld::Data::count(RecordBase::State state) const +{ + return count(state, mGlobals) + count(state, mGmsts) + count(state, mSkills) + count(state, mClasses) + + count(state, mFactions) + count(state, mRaces) + count(state, mSounds) + count(state, mScripts) + + count(state, mRegions) + count(state, mBirthsigns) + count(state, mSpells) + count(state, mCells) + + count(state, mEnchantments) + count(state, mBodyParts) + count(state, mLand) + count(state, mLandTextures) + + count(state, mSoundGens) + count(state, mMagicEffects) + count(state, mReferenceables) + + count(state, mPathgrids); +} + +std::vector CSMWorld::Data::getIds(bool listDeleted) const +{ + std::vector ids; + + appendIds(ids, mGlobals, listDeleted); + appendIds(ids, mGmsts, listDeleted); + appendIds(ids, mClasses, listDeleted); + appendIds(ids, mFactions, listDeleted); + appendIds(ids, mRaces, listDeleted); + appendIds(ids, mSounds, listDeleted); + appendIds(ids, mScripts, listDeleted); + appendIds(ids, mRegions, listDeleted); + appendIds(ids, mBirthsigns, listDeleted); + appendIds(ids, mSpells, listDeleted); + appendIds(ids, mTopics, listDeleted); + appendIds(ids, mJournals, listDeleted); + appendIds(ids, mCells, listDeleted); + appendIds(ids, mEnchantments, listDeleted); + appendIds(ids, mBodyParts, listDeleted); + appendIds(ids, mSoundGens, listDeleted); + appendIds(ids, mMagicEffects, listDeleted); + appendIds(ids, mReferenceables, listDeleted); + + std::sort(ids.begin(), ids.end()); return ids; } @@ -1309,16 +1464,10 @@ std::vector CSMWorld::Data::getIds (bool listDeleted) const void CSMWorld::Data::assetsChanged() { mVFS.get()->reset(); - VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths, !mFsStrict), mArchives, true); + VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths), mArchives, true); - const UniversalId assetTableIds[] = { - UniversalId::Type_Meshes, - UniversalId::Type_Icons, - UniversalId::Type_Musics, - UniversalId::Type_SoundsRes, - UniversalId::Type_Textures, - UniversalId::Type_Videos - }; + const UniversalId assetTableIds[] = { UniversalId::Type_Meshes, UniversalId::Type_Icons, UniversalId::Type_Musics, + UniversalId::Type_SoundsRes, UniversalId::Type_Textures, UniversalId::Type_Videos }; size_t numAssetTables = sizeof(assetTableIds) / sizeof(UniversalId); @@ -1343,13 +1492,13 @@ void CSMWorld::Data::assetsChanged() emit assetTablesChanged(); } -void CSMWorld::Data::dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +void CSMWorld::Data::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { - if (topLeft.column()<=0) + if (topLeft.column() <= 0) emit idListChanged(); } -void CSMWorld::Data::rowsChanged (const QModelIndex& parent, int start, int end) +void CSMWorld::Data::rowsChanged(const QModelIndex& parent, int start, int end) { emit idListChanged(); } diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index 51f92116258..237b7467460 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -1,330 +1,343 @@ #ifndef CSM_WOLRD_DATA_H #define CSM_WOLRD_DATA_H +#include + +#include #include +#include +#include +#include #include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include -#include "../doc/stage.hpp" - -#include "actoradapter.hpp" -#include "idcollection.hpp" -#include "nestedidcollection.hpp" -#include "universalid.hpp" #include "cell.hpp" -#include "land.hpp" -#include "landtexture.hpp" -#include "refidcollection.hpp" -#include "refcollection.hpp" +#include "idcollection.hpp" #include "infocollection.hpp" +#include "land.hpp" +#include "metadata.hpp" +#include "nestedidcollection.hpp" #include "nestedinfocollection.hpp" #include "pathgrid.hpp" +#include "refcollection.hpp" +#include "refidcollection.hpp" #include "resourcesmanager.hpp" -#include "metadata.hpp" +#include "universalid.hpp" #ifndef Q_MOC_RUN #include "subcellcollection.hpp" #endif class QAbstractItemModel; +class QModelIndex; -namespace VFS +namespace Resource { - class Manager; + class ResourceSystem; } -namespace Fallback +namespace VFS { - class Map; + class Manager; } namespace ESM { class ESMReader; - struct Dialogue; +} + +namespace CSMDoc +{ + class Messages; } namespace CSMWorld { - class ResourcesManager; + class ActorAdapter; + class CollectionBase; class Resources; class Data : public QObject { - Q_OBJECT + Q_OBJECT + + ToUTF8::Utf8Encoder mEncoder; + IdCollection mGlobals; + IdCollection mGmsts; + IdCollection mSkills; + IdCollection mClasses; + NestedIdCollection mFactions; + NestedIdCollection mRaces; + IdCollection mSounds; + IdCollection mScripts; + NestedIdCollection mRegions; + NestedIdCollection mBirthsigns; + NestedIdCollection mSpells; + IdCollection mTopics; + IdCollection mJournals; + NestedIdCollection mEnchantments; + IdCollection mBodyParts; + IdCollection mMagicEffects; + IdCollection mDebugProfiles; + IdCollection mSelectionGroups; + IdCollection mSoundGens; + IdCollection mStartScripts; + NestedInfoCollection mTopicInfos; + InfoCollection mJournalInfos; + NestedIdCollection mCells; + SubCellCollection mPathgrids; + IdCollection mLandTextures; + IdCollection mLand; + RefIdCollection mReferenceables; + RefCollection mRefs; + IdCollection mFilters; + Collection mMetaData; + std::unique_ptr mActorAdapter; + std::vector mModels; + std::map mModelIndex; + ESM::ReadersCache mReaders; + const ESM::Dialogue* mDialogue; // last loaded dialogue + bool mBase; + bool mProject; + std::map> mRefLoadCache; + std::size_t mReaderIndex; + + Files::PathContainer mDataPaths; + std::vector mArchives; + std::unique_ptr mVFS; + ResourcesManager mResourcesManager; + std::shared_ptr mResourceSystem; + + InfoOrderByTopic mJournalInfoOrder; + InfoOrderByTopic mTopicInfoOrder; - ToUTF8::Utf8Encoder mEncoder; - IdCollection mGlobals; - IdCollection mGmsts; - IdCollection mSkills; - IdCollection mClasses; - NestedIdCollection mFactions; - NestedIdCollection mRaces; - IdCollection mSounds; - IdCollection mScripts; - NestedIdCollection mRegions; - NestedIdCollection mBirthsigns; - NestedIdCollection mSpells; - IdCollection mTopics; - IdCollection mJournals; - NestedIdCollection mEnchantments; - IdCollection mBodyParts; - IdCollection mMagicEffects; - SubCellCollection mPathgrids; - IdCollection mDebugProfiles; - IdCollection mSoundGens; - IdCollection mStartScripts; - NestedInfoCollection mTopicInfos; - InfoCollection mJournalInfos; - NestedIdCollection mCells; - IdCollection mLandTextures; - IdCollection mLand; - RefIdCollection mReferenceables; - RefCollection mRefs; - IdCollection mFilters; - Collection mMetaData; - std::unique_ptr mActorAdapter; - std::vector mModels; - std::map mModelIndex; - ESM::ESMReader *mReader; - const ESM::Dialogue *mDialogue; // last loaded dialogue - bool mBase; - bool mProject; - std::map > mRefLoadCache; - int mReaderIndex; + Data(const Data&) = delete; + Data& operator=(const Data&) = delete; - bool mFsStrict; - Files::PathContainer mDataPaths; - std::vector mArchives; - std::unique_ptr mVFS; - ResourcesManager mResourcesManager; - std::shared_ptr mResourceSystem; + void addModel(QAbstractItemModel* model, UniversalId::Type type, bool update = true); - std::vector > mReaders; + static void appendIds(std::vector& ids, const CollectionBase& collection, bool listDeleted); + ///< Append all IDs from collection to \a ids. - std::map mContentFileNames; + static int count(RecordBase::State state, const CollectionBase& collection); - // not implemented - Data (const Data&); - Data& operator= (const Data&); + void loadFallbackEntries(); - void addModel (QAbstractItemModel *model, UniversalId::Type type, - bool update = true); + public: + Data(ToUTF8::FromType encoding, const Files::PathContainer& dataPaths, const std::vector& archives, + const std::filesystem::path& resDir); - static void appendIds (std::vector& ids, const CollectionBase& collection, - bool listDeleted); - ///< Append all IDs from collection to \a ids. + ~Data() override; - static int count (RecordBase::State state, const CollectionBase& collection); + const VFS::Manager* getVFS() const; - void loadFallbackEntries(); + std::shared_ptr getResourceSystem(); - public: + std::shared_ptr getResourceSystem() const; - Data (ToUTF8::FromType encoding, bool fsStrict, const Files::PathContainer& dataPaths, - const std::vector& archives, const boost::filesystem::path& resDir); + const IdCollection& getGlobals() const; - virtual ~Data(); + IdCollection& getGlobals(); - const VFS::Manager* getVFS() const; + const IdCollection& getGmsts() const; - std::shared_ptr getResourceSystem(); + IdCollection& getGmsts(); - std::shared_ptr getResourceSystem() const; + const IdCollection& getSkills() const; - const IdCollection& getGlobals() const; + IdCollection& getSkills(); - IdCollection& getGlobals(); + const IdCollection& getClasses() const; - const IdCollection& getGmsts() const; + IdCollection& getClasses(); - IdCollection& getGmsts(); + const IdCollection& getFactions() const; - const IdCollection& getSkills() const; + IdCollection& getFactions(); - IdCollection& getSkills(); + const IdCollection& getRaces() const; - const IdCollection& getClasses() const; + IdCollection& getRaces(); - IdCollection& getClasses(); + const IdCollection& getSounds() const; - const IdCollection& getFactions() const; + IdCollection& getSounds(); - IdCollection& getFactions(); + const IdCollection& getScripts() const; - const IdCollection& getRaces() const; + IdCollection& getScripts(); - IdCollection& getRaces(); + const IdCollection& getRegions() const; - const IdCollection& getSounds() const; + IdCollection& getRegions(); - IdCollection& getSounds(); + const IdCollection& getBirthsigns() const; - const IdCollection& getScripts() const; + IdCollection& getBirthsigns(); - IdCollection& getScripts(); + const IdCollection& getSpells() const; - const IdCollection& getRegions() const; + IdCollection& getSpells(); - IdCollection& getRegions(); + const IdCollection& getTopics() const; - const IdCollection& getBirthsigns() const; + IdCollection& getTopics(); - IdCollection& getBirthsigns(); + const IdCollection& getJournals() const; - const IdCollection& getSpells() const; + IdCollection& getJournals(); - IdCollection& getSpells(); + const InfoCollection& getTopicInfos() const; - const IdCollection& getTopics() const; + InfoCollection& getTopicInfos(); - IdCollection& getTopics(); + const InfoCollection& getJournalInfos() const; - const IdCollection& getJournals() const; + InfoCollection& getJournalInfos(); - IdCollection& getJournals(); + const IdCollection& getCells() const; - const InfoCollection& getTopicInfos() const; + IdCollection& getCells(); - InfoCollection& getTopicInfos(); + const RefIdCollection& getReferenceables() const; - const InfoCollection& getJournalInfos() const; + RefIdCollection& getReferenceables(); - InfoCollection& getJournalInfos(); + const RefCollection& getReferences() const; - const IdCollection& getCells() const; + RefCollection& getReferences(); - IdCollection& getCells(); + const IdCollection& getFilters() const; - const RefIdCollection& getReferenceables() const; + IdCollection& getFilters(); - RefIdCollection& getReferenceables(); + const IdCollection& getEnchantments() const; - const RefCollection& getReferences() const; + IdCollection& getEnchantments(); - RefCollection& getReferences(); + const IdCollection& getBodyParts() const; - const IdCollection& getFilters() const; + IdCollection& getBodyParts(); - IdCollection& getFilters(); + const IdCollection& getDebugProfiles() const; - const IdCollection& getEnchantments() const; + IdCollection& getDebugProfiles(); - IdCollection& getEnchantments(); + const IdCollection& getSelectionGroups() const; - const IdCollection& getBodyParts() const; + IdCollection& getSelectionGroups(); - IdCollection& getBodyParts(); + const IdCollection& getLand() const; - const IdCollection& getDebugProfiles() const; + IdCollection& getLand(); - IdCollection& getDebugProfiles(); + const IdCollection& getLandTextures() const; - const IdCollection& getLand() const; + IdCollection& getLandTextures(); - IdCollection& getLand(); + const IdCollection& getSoundGens() const; - const IdCollection& getLandTextures() const; + IdCollection& getSoundGens(); - IdCollection& getLandTextures(); + const IdCollection& getMagicEffects() const; - const IdCollection& getSoundGens() const; + IdCollection& getMagicEffects(); - IdCollection& getSoundGens(); + const SubCellCollection& getPathgrids() const; - const IdCollection& getMagicEffects() const; + SubCellCollection& getPathgrids(); - IdCollection& getMagicEffects(); + const IdCollection& getStartScripts() const; - const SubCellCollection& getPathgrids() const; + IdCollection& getStartScripts(); - SubCellCollection& getPathgrids(); + /// Throws an exception, if \a id does not match a resources list. + const Resources& getResources(const UniversalId& id) const; - const IdCollection& getStartScripts() const; + const MetaData& getMetaData() const; - IdCollection& getStartScripts(); + void setMetaData(const MetaData& metaData); - /// Throws an exception, if \a id does not match a resources list. - const Resources& getResources (const UniversalId& id) const; + QAbstractItemModel* getTableModel(const UniversalId& id); + ///< If no table model is available for \a id, an exception is thrown. + /// + /// \note The returned table may either be the model for the ID itself or the model that + /// contains the record specified by the ID. - const MetaData& getMetaData() const; + const ActorAdapter* getActorAdapter() const; - void setMetaData (const MetaData& metaData); + ActorAdapter* getActorAdapter(); - QAbstractItemModel *getTableModel (const UniversalId& id); - ///< If no table model is available for \a id, an exception is thrown. - /// - /// \note The returned table may either be the model for the ID itself or the model that - /// contains the record specified by the ID. + void merge(); + ///< Merge modified into base. - const ActorAdapter* getActorAdapter() const; + int getTotalRecords(const std::vector& files); // for better loading bar - ActorAdapter* getActorAdapter(); + int startLoading(const std::filesystem::path& path, bool base, bool project); + ///< Begin merging content of a file into base or modified. + /// + /// \param project load project file instead of content file + /// + ///< \return estimated number of records - void merge(); - ///< Merge modified into base. + bool continueLoading(CSMDoc::Messages& messages); + ///< \return Finished? - int startLoading (const boost::filesystem::path& path, bool base, bool project); - ///< Begin merging content of a file into base or modified. - /// - /// \param project load project file instead of content file - /// - ///< \return estimated number of records + void finishLoading(); - bool continueLoading (CSMDoc::Messages& messages); - ///< \return Finished? + bool hasId(const std::string& id) const; - bool hasId (const std::string& id) const; + std::vector getIds(bool listDeleted = true) const; + ///< Return a sorted collection of all IDs that are not internal to the editor. + /// + /// \param listDeleted include deleted record in the list - std::vector getIds (bool listDeleted = true) const; - ///< Return a sorted collection of all IDs that are not internal to the editor. - /// - /// \param listDeleted include deleted record in the list + int count(RecordBase::State state) const; + ///< Return number of top-level records with the given \a state. - int count (RecordBase::State state) const; - ///< Return number of top-level records with the given \a state. + signals: - signals: + void idListChanged(); - void idListChanged(); + void assetTablesChanged(); - void assetTablesChanged(); + public slots: - private slots: + void assetsChanged(); - void assetsChanged(); + private slots: - void dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - void rowsChanged (const QModelIndex& parent, int start, int end); + void rowsChanged(const QModelIndex& parent, int start, int end); }; } diff --git a/apps/opencs/model/world/defaultgmsts.cpp b/apps/opencs/model/world/defaultgmsts.cpp index da83a86be23..2c9a8a66e0d 100644 --- a/apps/opencs/model/world/defaultgmsts.cpp +++ b/apps/opencs/model/world/defaultgmsts.cpp @@ -8,8 +8,7 @@ const float FEps = std::numeric_limits::epsilon(); const int IMax = std::numeric_limits::max(); const int IMin = std::numeric_limits::min(); -const char* CSMWorld::DefaultGmsts::Floats[CSMWorld::DefaultGmsts::FloatCount] = -{ +const char* CSMWorld::DefaultGmsts::Floats[CSMWorld::DefaultGmsts::FloatCount] = { "fAIFleeFleeMult", "fAIFleeHealthMult", "fAIMagicSpellMult", @@ -267,11 +266,10 @@ const char* CSMWorld::DefaultGmsts::Floats[CSMWorld::DefaultGmsts::FloatCount] = "fWereWolfStrength", "fWereWolfUnarmored", "fWereWolfWillPower", - "fWortChanceValue" + "fWortChanceValue", }; -const char * CSMWorld::DefaultGmsts::Ints[CSMWorld::DefaultGmsts::IntCount] = -{ +const char* CSMWorld::DefaultGmsts::Ints[CSMWorld::DefaultGmsts::IntCount] = { "i1stPersonSneakDelta", "iAlarmAttack", "iAlarmKilling", @@ -360,11 +358,10 @@ const char * CSMWorld::DefaultGmsts::Ints[CSMWorld::DefaultGmsts::IntCount] = "iWereWolfBounty", "iWereWolfFightMod", "iWereWolfFleeMod", - "iWereWolfLevelToAttack" + "iWereWolfLevelToAttack", }; -const char * CSMWorld::DefaultGmsts::Strings[CSMWorld::DefaultGmsts::StringCount] = -{ +const char* CSMWorld::DefaultGmsts::Strings[CSMWorld::DefaultGmsts::StringCount] = { "s3dAudio", "s3dHardware", "s3dSoftware", @@ -1538,11 +1535,10 @@ const char * CSMWorld::DefaultGmsts::Strings[CSMWorld::DefaultGmsts::StringCount "sXTimes", "sXTimesINT", "sYes", - "sYourGold" + "sYourGold", }; -const char * CSMWorld::DefaultGmsts::OptionalFloats[CSMWorld::DefaultGmsts::OptionalFloatCount] = -{ +const char* CSMWorld::DefaultGmsts::OptionalFloats[CSMWorld::DefaultGmsts::OptionalFloatCount] = { "fCombatDistanceWerewolfMod", "fFleeDistance", "fWereWolfAcrobatics", @@ -1584,19 +1580,17 @@ const char * CSMWorld::DefaultGmsts::OptionalFloats[CSMWorld::DefaultGmsts::Opti "fWereWolfSpeed", "fWereWolfStrength", "fWereWolfUnarmored", - "fWereWolfWillPower" + "fWereWolfWillPower", }; -const char * CSMWorld::DefaultGmsts::OptionalInts[CSMWorld::DefaultGmsts::OptionalIntCount] = -{ +const char* CSMWorld::DefaultGmsts::OptionalInts[CSMWorld::DefaultGmsts::OptionalIntCount] = { "iWereWolfBounty", "iWereWolfFightMod", "iWereWolfFleeMod", - "iWereWolfLevelToAttack" + "iWereWolfLevelToAttack", }; -const char * CSMWorld::DefaultGmsts::OptionalStrings[CSMWorld::DefaultGmsts::OptionalStringCount] = -{ +const char* CSMWorld::DefaultGmsts::OptionalStrings[CSMWorld::DefaultGmsts::OptionalStringCount] = { "sCompanionShare", "sCompanionWarningButtonOne", "sCompanionWarningButtonTwo", @@ -1622,715 +1616,711 @@ const char * CSMWorld::DefaultGmsts::OptionalStrings[CSMWorld::DefaultGmsts::Opt "sWerewolfAlarmMessage", "sWerewolfPopup", "sWerewolfRefusal", - "sWerewolfRestMessage" + "sWerewolfRestMessage", }; -const float CSMWorld::DefaultGmsts::FloatsDefaultValues[CSMWorld::DefaultGmsts::FloatCount] = -{ - 0.3f, // fAIFleeFleeMult - 7.0f, // fAIFleeHealthMult - 3.0f, // fAIMagicSpellMult - 1.0f, // fAIMeleeArmorMult - 1.0f, // fAIMeleeSummWeaponMult - 2.0f, // fAIMeleeWeaponMult - 5.0f, // fAIRangeMagicSpellMult - 5.0f, // fAIRangeMeleeWeaponMult +const float CSMWorld::DefaultGmsts::FloatsDefaultValues[CSMWorld::DefaultGmsts::FloatCount] = { + 0.3f, // fAIFleeFleeMult + 7.0f, // fAIFleeHealthMult + 3.0f, // fAIMagicSpellMult + 1.0f, // fAIMeleeArmorMult + 1.0f, // fAIMeleeSummWeaponMult + 2.0f, // fAIMeleeWeaponMult + 5.0f, // fAIRangeMagicSpellMult + 5.0f, // fAIRangeMeleeWeaponMult 2000.0f, // fAlarmRadius - 1.0f, // fAthleticsRunBonus - 40.0f, // fAudioDefaultMaxDistance - 5.0f, // fAudioDefaultMinDistance - 50.0f, // fAudioMaxDistanceMult - 20.0f, // fAudioMinDistanceMult - 60.0f, // fAudioVoiceDefaultMaxDistance - 10.0f, // fAudioVoiceDefaultMinDistance - 50.0f, // fAutoPCSpellChance - 80.0f, // fAutoSpellChance - 50.0f, // fBargainOfferBase - -4.0f, // fBargainOfferMulti - 24.0f, // fBarterGoldResetDelay - 1.75f, // fBaseRunMultiplier - 1.25f, // fBlockStillBonus - 150.0f, // fBribe1000Mod - 75.0f, // fBribe100Mod - 35.0f, // fBribe10Mod - 60.0f, // fCombatAngleXY - 60.0f, // fCombatAngleZ - 0.25f, // fCombatArmorMinMult - -90.0f, // fCombatBlockLeftAngle - 30.0f, // fCombatBlockRightAngle - 4.0f, // fCombatCriticalStrikeMult - 0.1f, // fCombatDelayCreature - 0.1f, // fCombatDelayNPC - 128.0f, // fCombatDistance - 0.3f, // fCombatDistanceWerewolfMod - 30.0f, // fCombatForceSideAngle - 0.2f, // fCombatInvisoMult - 1.5f, // fCombatKODamageMult - 45.0f, // fCombatTorsoSideAngle - 0.3f, // fCombatTorsoStartPercent - 0.8f, // fCombatTorsoStopPercent - 15.0f, // fConstantEffectMult - 72.0f, // fCorpseClearDelay - 72.0f, // fCorpseRespawnDelay - 0.5f, // fCrimeGoldDiscountMult - 0.9f, // fCrimeGoldTurnInMult - 1.0f, // fCrimeStealing - 0.5f, // fDamageStrengthBase - 0.1f, // fDamageStrengthMult - 5.0f, // fDifficultyMult - 2.5f, // fDiseaseXferChance - -10.0f, // fDispAttacking - -1.0f, // fDispBargainFailMod - 1.0f, // fDispBargainSuccessMod - 0.0f, // fDispCrimeMod - -10.0f, // fDispDiseaseMod - 3.0f, // fDispFactionMod - 1.0f, // fDispFactionRankBase - 0.5f, // fDispFactionRankMult - 1.0f, // fDispositionMod - 50.0f, // fDispPersonalityBase - 0.5f, // fDispPersonalityMult - -25.0f, // fDispPickPocketMod - 5.0f, // fDispRaceMod - -0.5f, // fDispStealing - -5.0f, // fDispWeaponDrawn - 0.5f, // fEffectCostMult - 0.1f, // fElementalShieldMult - 3.0f, // fEnchantmentChanceMult - 0.5f, // fEnchantmentConstantChanceMult - 100.0f, // fEnchantmentConstantDurationMult - 0.1f, // fEnchantmentMult + 1.0f, // fAthleticsRunBonus + 40.0f, // fAudioDefaultMaxDistance + 5.0f, // fAudioDefaultMinDistance + 50.0f, // fAudioMaxDistanceMult + 20.0f, // fAudioMinDistanceMult + 60.0f, // fAudioVoiceDefaultMaxDistance + 10.0f, // fAudioVoiceDefaultMinDistance + 50.0f, // fAutoPCSpellChance + 80.0f, // fAutoSpellChance + 50.0f, // fBargainOfferBase + -4.0f, // fBargainOfferMulti + 24.0f, // fBarterGoldResetDelay + 1.75f, // fBaseRunMultiplier + 1.25f, // fBlockStillBonus + 150.0f, // fBribe1000Mod + 75.0f, // fBribe100Mod + 35.0f, // fBribe10Mod + 60.0f, // fCombatAngleXY + 60.0f, // fCombatAngleZ + 0.25f, // fCombatArmorMinMult + -90.0f, // fCombatBlockLeftAngle + 30.0f, // fCombatBlockRightAngle + 4.0f, // fCombatCriticalStrikeMult + 0.1f, // fCombatDelayCreature + 0.1f, // fCombatDelayNPC + 128.0f, // fCombatDistance + 0.3f, // fCombatDistanceWerewolfMod + 30.0f, // fCombatForceSideAngle + 0.2f, // fCombatInvisoMult + 1.5f, // fCombatKODamageMult + 45.0f, // fCombatTorsoSideAngle + 0.3f, // fCombatTorsoStartPercent + 0.8f, // fCombatTorsoStopPercent + 15.0f, // fConstantEffectMult + 72.0f, // fCorpseClearDelay + 72.0f, // fCorpseRespawnDelay + 0.5f, // fCrimeGoldDiscountMult + 0.9f, // fCrimeGoldTurnInMult + 1.0f, // fCrimeStealing + 0.5f, // fDamageStrengthBase + 0.1f, // fDamageStrengthMult + 5.0f, // fDifficultyMult + 2.5f, // fDiseaseXferChance + -10.0f, // fDispAttacking + -1.0f, // fDispBargainFailMod + 1.0f, // fDispBargainSuccessMod + 0.0f, // fDispCrimeMod + -10.0f, // fDispDiseaseMod + 3.0f, // fDispFactionMod + 1.0f, // fDispFactionRankBase + 0.5f, // fDispFactionRankMult + 1.0f, // fDispositionMod + 50.0f, // fDispPersonalityBase + 0.5f, // fDispPersonalityMult + -25.0f, // fDispPickPocketMod + 5.0f, // fDispRaceMod + -0.5f, // fDispStealing + -5.0f, // fDispWeaponDrawn + 0.5f, // fEffectCostMult + 0.1f, // fElementalShieldMult + 3.0f, // fEnchantmentChanceMult + 0.5f, // fEnchantmentConstantChanceMult + 100.0f, // fEnchantmentConstantDurationMult + 0.1f, // fEnchantmentMult 1000.0f, // fEnchantmentValueMult - 0.3f, // fEncumberedMoveEffect - 5.0f, // fEncumbranceStrMult - 0.04f, // fEndFatigueMult - 0.25f, // fFallAcroBase - 0.01f, // fFallAcroMult - 400.0f, // fFallDamageDistanceMin - 0.0f, // fFallDistanceBase - 0.07f, // fFallDistanceMult - 2.0f, // fFatigueAttackBase - 0.0f, // fFatigueAttackMult - 1.25f, // fFatigueBase - 4.0f, // fFatigueBlockBase - 0.0f, // fFatigueBlockMult - 5.0f, // fFatigueJumpBase - 0.0f, // fFatigueJumpMult - 0.5f, // fFatigueMult - 2.5f, // fFatigueReturnBase - 0.02f, // fFatigueReturnMult - 5.0f, // fFatigueRunBase - 2.0f, // fFatigueRunMult - 1.5f, // fFatigueSneakBase - 1.5f, // fFatigueSneakMult - 0.0f, // fFatigueSpellBase - 0.0f, // fFatigueSpellCostMult - 0.0f, // fFatigueSpellMult - 7.0f, // fFatigueSwimRunBase - 0.0f, // fFatigueSwimRunMult - 2.5f, // fFatigueSwimWalkBase - 0.0f, // fFatigueSwimWalkMult - 0.2f, // fFightDispMult - 0.005f, // fFightDistanceMultiplier - 50.0f, // fFightStealing + 0.3f, // fEncumberedMoveEffect + 5.0f, // fEncumbranceStrMult + 0.04f, // fEndFatigueMult + 0.25f, // fFallAcroBase + 0.01f, // fFallAcroMult + 400.0f, // fFallDamageDistanceMin + 0.0f, // fFallDistanceBase + 0.07f, // fFallDistanceMult + 2.0f, // fFatigueAttackBase + 0.0f, // fFatigueAttackMult + 1.25f, // fFatigueBase + 4.0f, // fFatigueBlockBase + 0.0f, // fFatigueBlockMult + 5.0f, // fFatigueJumpBase + 0.0f, // fFatigueJumpMult + 0.5f, // fFatigueMult + 2.5f, // fFatigueReturnBase + 0.02f, // fFatigueReturnMult + 5.0f, // fFatigueRunBase + 2.0f, // fFatigueRunMult + 1.5f, // fFatigueSneakBase + 1.5f, // fFatigueSneakMult + 0.0f, // fFatigueSpellBase + 0.0f, // fFatigueSpellCostMult + 0.0f, // fFatigueSpellMult + 7.0f, // fFatigueSwimRunBase + 0.0f, // fFatigueSwimRunMult + 2.5f, // fFatigueSwimWalkBase + 0.0f, // fFatigueSwimWalkMult + 0.2f, // fFightDispMult + 0.005f, // fFightDistanceMultiplier + 50.0f, // fFightStealing 3000.0f, // fFleeDistance - 512.0f, // fGreetDistanceReset - 0.1f, // fHandtoHandHealthPer - 1.0f, // fHandToHandReach - 0.5f, // fHoldBreathEndMult - 20.0f, // fHoldBreathTime - 0.75f, // fIdleChanceMultiplier - 1.0f, // fIngredientMult - 0.5f, // fInteriorHeadTrackMult - 128.0f, // fJumpAcrobaticsBase - 4.0f, // fJumpAcroMultiplier - 0.5f, // fJumpEncumbranceBase - 1.0f, // fJumpEncumbranceMultiplier - 0.5f, // fJumpMoveBase - 0.5f, // fJumpMoveMult - 1.0f, // fJumpRunMultiplier - 0.5f, // fKnockDownMult - 5.0f, // fLevelMod - 0.1f, // fLevelUpHealthEndMult - 0.6f, // fLightMaxMod - 10.0f, // fLuckMod - 10.0f, // fMagesGuildTravel - 1.5f, // fMagicCreatureCastDelay + 512.0f, // fGreetDistanceReset + 0.1f, // fHandtoHandHealthPer + 1.0f, // fHandToHandReach + 0.5f, // fHoldBreathEndMult + 20.0f, // fHoldBreathTime + 0.75f, // fIdleChanceMultiplier + 1.0f, // fIngredientMult + 0.5f, // fInteriorHeadTrackMult + 128.0f, // fJumpAcrobaticsBase + 4.0f, // fJumpAcroMultiplier + 0.5f, // fJumpEncumbranceBase + 1.0f, // fJumpEncumbranceMultiplier + 0.5f, // fJumpMoveBase + 0.5f, // fJumpMoveMult + 1.0f, // fJumpRunMultiplier + 0.5f, // fKnockDownMult + 5.0f, // fLevelMod + 0.1f, // fLevelUpHealthEndMult + 0.6f, // fLightMaxMod + 10.0f, // fLuckMod + 10.0f, // fMagesGuildTravel + 1.5f, // fMagicCreatureCastDelay 0.0167f, // fMagicDetectRefreshRate - 1.0f, // fMagicItemConstantMult - 1.0f, // fMagicItemCostMult - 1.0f, // fMagicItemOnceMult - 1.0f, // fMagicItemPriceMult - 0.05f, // fMagicItemRechargePerSecond - 1.0f, // fMagicItemStrikeMult - 1.0f, // fMagicItemUsedMult - 3.0f, // fMagicStartIconBlink - 0.5f, // fMagicSunBlockedMult - 0.75f, // fMajorSkillBonus - 300.0f, // fMaxFlySpeed - 0.5f, // fMaxHandToHandMult - 400.0f, // fMaxHeadTrackDistance - 200.0f, // fMaxWalkSpeed - 300.0f, // fMaxWalkSpeedCreature - 0.9f, // fMedMaxMod - 0.1f, // fMessageTimePerChar - 5.0f, // fMinFlySpeed - 0.1f, // fMinHandToHandMult - 1.0f, // fMinorSkillBonus - 100.0f, // fMinWalkSpeed - 5.0f, // fMinWalkSpeedCreature - 1.25f, // fMiscSkillBonus - 2.0f, // fNPCbaseMagickaMult - 0.5f, // fNPCHealthBarFade - 3.0f, // fNPCHealthBarTime - 1.0f, // fPCbaseMagickaMult - 0.3f, // fPerDieRollMult - 5.0f, // fPersonalityMod - 1.0f, // fPerTempMult - -1.0f, // fPickLockMult - 0.3f, // fPickPocketMod - 20.0f, // fPotionMinUsefulDuration - 0.5f, // fPotionStrengthMult - 0.5f, // fPotionT1DurMult - 1.5f, // fPotionT1MagMult - 20.0f, // fPotionT4BaseStrengthMult - 12.0f, // fPotionT4EquipStrengthMult + 1.0f, // fMagicItemConstantMult + 1.0f, // fMagicItemCostMult + 1.0f, // fMagicItemOnceMult + 1.0f, // fMagicItemPriceMult + 0.05f, // fMagicItemRechargePerSecond + 1.0f, // fMagicItemStrikeMult + 1.0f, // fMagicItemUsedMult + 3.0f, // fMagicStartIconBlink + 0.5f, // fMagicSunBlockedMult + 0.75f, // fMajorSkillBonus + 300.0f, // fMaxFlySpeed + 0.5f, // fMaxHandToHandMult + 400.0f, // fMaxHeadTrackDistance + 200.0f, // fMaxWalkSpeed + 300.0f, // fMaxWalkSpeedCreature + 0.9f, // fMedMaxMod + 0.1f, // fMessageTimePerChar + 5.0f, // fMinFlySpeed + 0.1f, // fMinHandToHandMult + 1.0f, // fMinorSkillBonus + 100.0f, // fMinWalkSpeed + 5.0f, // fMinWalkSpeedCreature + 1.25f, // fMiscSkillBonus + 2.0f, // fNPCbaseMagickaMult + 0.5f, // fNPCHealthBarFade + 3.0f, // fNPCHealthBarTime + 1.0f, // fPCbaseMagickaMult + 0.3f, // fPerDieRollMult + 5.0f, // fPersonalityMod + 1.0f, // fPerTempMult + -1.0f, // fPickLockMult + 0.3f, // fPickPocketMod + 20.0f, // fPotionMinUsefulDuration + 0.5f, // fPotionStrengthMult + 0.5f, // fPotionT1DurMult + 1.5f, // fPotionT1MagMult + 20.0f, // fPotionT4BaseStrengthMult + 12.0f, // fPotionT4EquipStrengthMult 3000.0f, // fProjectileMaxSpeed - 400.0f, // fProjectileMinSpeed - 25.0f, // fProjectileThrownStoreChance - 3.0f, // fRepairAmountMult - 1.0f, // fRepairMult - 1.0f, // fReputationMod - 0.15f, // fRestMagicMult - 0.0f, // fSeriousWoundMult - 0.25f, // fSleepRandMod - 0.3f, // fSleepRestMod - -1.0f, // fSneakBootMult - 0.5f, // fSneakDistanceBase - 0.002f, // fSneakDistanceMultiplier - 0.5f, // fSneakNoViewMult - 1.0f, // fSneakSkillMult - 0.75f, // fSneakSpeedMultiplier - 1.0f, // fSneakUseDelay - 500.0f, // fSneakUseDist - 1.5f, // fSneakViewMult - 3.0f, // fSoulGemMult - 0.8f, // fSpecialSkillBonus - 7.0f, // fSpellMakingValueMult - 2.0f, // fSpellPriceMult - 10.0f, // fSpellValueMult - 0.25f, // fStromWalkMult - 0.7f, // fStromWindSpeed - 3.0f, // fSuffocationDamage - 0.9f, // fSwimHeightScale - 0.1f, // fSwimRunAthleticsMult - 0.5f, // fSwimRunBase - 0.02f, // fSwimWalkAthleticsMult - 0.5f, // fSwimWalkBase - 1.0f, // fSwingBlockBase - 1.0f, // fSwingBlockMult + 400.0f, // fProjectileMinSpeed + 25.0f, // fProjectileThrownStoreChance + 3.0f, // fRepairAmountMult + 1.0f, // fRepairMult + 1.0f, // fReputationMod + 0.15f, // fRestMagicMult + 0.0f, // fSeriousWoundMult + 0.25f, // fSleepRandMod + 0.3f, // fSleepRestMod + -1.0f, // fSneakBootMult + 0.5f, // fSneakDistanceBase + 0.002f, // fSneakDistanceMultiplier + 0.5f, // fSneakNoViewMult + 1.0f, // fSneakSkillMult + 0.75f, // fSneakSpeedMultiplier + 1.0f, // fSneakUseDelay + 500.0f, // fSneakUseDist + 1.5f, // fSneakViewMult + 3.0f, // fSoulGemMult + 0.8f, // fSpecialSkillBonus + 7.0f, // fSpellMakingValueMult + 2.0f, // fSpellPriceMult + 10.0f, // fSpellValueMult + 0.25f, // fStromWalkMult + 0.7f, // fStromWindSpeed + 3.0f, // fSuffocationDamage + 0.9f, // fSwimHeightScale + 0.1f, // fSwimRunAthleticsMult + 0.5f, // fSwimRunBase + 0.02f, // fSwimWalkAthleticsMult + 0.5f, // fSwimWalkBase + 1.0f, // fSwingBlockBase + 1.0f, // fSwingBlockMult 1000.0f, // fTargetSpellMaxSpeed 1000.0f, // fThrownWeaponMaxSpeed - 300.0f, // fThrownWeaponMinSpeed - 0.0f, // fTrapCostMult + 300.0f, // fThrownWeaponMinSpeed + 0.0f, // fTrapCostMult 4000.0f, // fTravelMult - 16000.0f,// fTravelTimeMult - 0.1f, // fUnarmoredBase1 - 0.065f, // fUnarmoredBase2 - 30.0f, // fVanityDelay - 10.0f, // fVoiceIdleOdds - 0.0f, // fWaterReflectUpdateAlways - 10.0f, // fWaterReflectUpdateSeldom - 0.1f, // fWeaponDamageMult - 1.0f, // fWeaponFatigueBlockMult - 0.25f, // fWeaponFatigueMult - 150.0f, // fWereWolfAcrobatics - 150.0f, // fWereWolfAgility - 1.0f, // fWereWolfAlchemy - 1.0f, // fWereWolfAlteration - 1.0f, // fWereWolfArmorer - 150.0f, // fWereWolfAthletics - 1.0f, // fWereWolfAxe - 1.0f, // fWereWolfBlock - 1.0f, // fWereWolfBluntWeapon - 1.0f, // fWereWolfConjuration - 1.0f, // fWereWolfDestruction - 1.0f, // fWereWolfEnchant - 150.0f, // fWereWolfEndurance - 400.0f, // fWereWolfFatigue - 100.0f, // fWereWolfHandtoHand - 2.0f, // fWereWolfHealth - 1.0f, // fWereWolfHeavyArmor - 1.0f, // fWereWolfIllusion - 1.0f, // fWereWolfIntellegence - 1.0f, // fWereWolfLightArmor - 1.0f, // fWereWolfLongBlade - 1.0f, // fWereWolfLuck - 100.0f, // fWereWolfMagicka - 1.0f, // fWereWolfMarksman - 1.0f, // fWereWolfMediumArmor - 1.0f, // fWereWolfMerchantile - 1.0f, // fWereWolfMysticism - 1.0f, // fWereWolfPersonality - 1.0f, // fWereWolfRestoration - 1.5f, // fWereWolfRunMult - 1.0f, // fWereWolfSecurity - 1.0f, // fWereWolfShortBlade - 1.5f, // fWereWolfSilverWeaponDamageMult - 1.0f, // fWereWolfSneak - 1.0f, // fWereWolfSpear - 1.0f, // fWereWolfSpeechcraft - 150.0f, // fWereWolfSpeed - 150.0f, // fWereWolfStrength - 100.0f, // fWereWolfUnarmored - 1.0f, // fWereWolfWillPower - 15.0f // fWortChanceValue + 16000.0f, // fTravelTimeMult + 0.1f, // fUnarmoredBase1 + 0.065f, // fUnarmoredBase2 + 30.0f, // fVanityDelay + 10.0f, // fVoiceIdleOdds + 0.0f, // fWaterReflectUpdateAlways + 10.0f, // fWaterReflectUpdateSeldom + 0.1f, // fWeaponDamageMult + 1.0f, // fWeaponFatigueBlockMult + 0.25f, // fWeaponFatigueMult + 150.0f, // fWereWolfAcrobatics + 150.0f, // fWereWolfAgility + 1.0f, // fWereWolfAlchemy + 1.0f, // fWereWolfAlteration + 1.0f, // fWereWolfArmorer + 150.0f, // fWereWolfAthletics + 1.0f, // fWereWolfAxe + 1.0f, // fWereWolfBlock + 1.0f, // fWereWolfBluntWeapon + 1.0f, // fWereWolfConjuration + 1.0f, // fWereWolfDestruction + 1.0f, // fWereWolfEnchant + 150.0f, // fWereWolfEndurance + 400.0f, // fWereWolfFatigue + 100.0f, // fWereWolfHandtoHand + 2.0f, // fWereWolfHealth + 1.0f, // fWereWolfHeavyArmor + 1.0f, // fWereWolfIllusion + 1.0f, // fWereWolfIntellegence + 1.0f, // fWereWolfLightArmor + 1.0f, // fWereWolfLongBlade + 1.0f, // fWereWolfLuck + 100.0f, // fWereWolfMagicka + 1.0f, // fWereWolfMarksman + 1.0f, // fWereWolfMediumArmor + 1.0f, // fWereWolfMerchantile + 1.0f, // fWereWolfMysticism + 1.0f, // fWereWolfPersonality + 1.0f, // fWereWolfRestoration + 1.5f, // fWereWolfRunMult + 1.0f, // fWereWolfSecurity + 1.0f, // fWereWolfShortBlade + 1.5f, // fWereWolfSilverWeaponDamageMult + 1.0f, // fWereWolfSneak + 1.0f, // fWereWolfSpear + 1.0f, // fWereWolfSpeechcraft + 150.0f, // fWereWolfSpeed + 150.0f, // fWereWolfStrength + 100.0f, // fWereWolfUnarmored + 1.0f, // fWereWolfWillPower + 15.0f, // fWortChanceValue }; -const int CSMWorld::DefaultGmsts::IntsDefaultValues[CSMWorld::DefaultGmsts::IntCount] = -{ - 10, // i1stPersonSneakDelta - 50, // iAlarmAttack - 90, // iAlarmKilling - 20, // iAlarmPickPocket - 1, // iAlarmStealing - 5, // iAlarmTresspass - 2, // iAlchemyMod - 100, // iAutoPCSpellMax - 2, // iAutoRepFacMod - 0, // iAutoRepLevMod - 5, // iAutoSpellAlterationMax - 70, // iAutoSpellAttSkillMin - 2, // iAutoSpellConjurationMax - 5, // iAutoSpellDestructionMax - 5, // iAutoSpellIllusionMax - 5, // iAutoSpellMysticismMax - 5, // iAutoSpellRestorationMax - 3, // iAutoSpellTimesCanCast - -1, // iBarterFailDisposition - 1, // iBarterSuccessDisposition - 30, // iBaseArmorSkill - 50, // iBlockMaxChance - 10, // iBlockMinChance - 20, // iBootsWeight - 40, // iCrimeAttack - 1000, // iCrimeKilling - 25, // iCrimePickPocket - 1000, // iCrimeThreshold - 10, // iCrimeThresholdMultiplier - 5, // iCrimeTresspass - 30, // iCuirassWeight - 100, // iDaysinPrisonMod - -50, // iDispAttackMod - -50, // iDispKilling - -20, // iDispTresspass - 1, // iFightAlarmMult - 100, // iFightAttack - 50, // iFightAttacking - 20, // iFightDistanceBase - 50, // iFightKilling - 25, // iFightPickpocket - 25, // iFightTrespass - 0, // iFlee - 5, // iGauntletWeight - 15, // iGreavesWeight - 6, // iGreetDistanceMultiplier - 4, // iGreetDuration - 5, // iHelmWeight - 50, // iKnockDownOddsBase - 50, // iKnockDownOddsMult - 2, // iLevelUp01Mult - 2, // iLevelUp02Mult - 2, // iLevelUp03Mult - 2, // iLevelUp04Mult - 3, // iLevelUp05Mult - 3, // iLevelUp06Mult - 3, // iLevelUp07Mult - 4, // iLevelUp08Mult - 4, // iLevelUp09Mult - 5, // iLevelUp10Mult - 1, // iLevelupMajorMult - 1, // iLevelupMajorMultAttribute - 1, // iLevelupMinorMult - 1, // iLevelupMinorMultAttribute - 1, // iLevelupMiscMultAttriubte - 1, // iLevelupSpecialization - 10, // iLevelupTotal - 10, // iMagicItemChargeConst - 1, // iMagicItemChargeOnce - 10, // iMagicItemChargeStrike - 5, // iMagicItemChargeUse - 192, // iMaxActivateDist - 192, // iMaxInfoDist - 4, // iMonthsToRespawn - 1, // iNumberCreatures - 10, // iPauldronWeight - 5, // iPerMinChance - 10, // iPerMinChange - 75, // iPickMaxChance - 5, // iPickMinChance - 15, // iShieldWeight - 400, // iSoulAmountForConstantEffect - 10, // iTrainingMod - 10, // iVoiceAttackOdds - 30, // iVoiceHitOdds - 10000, // iWereWolfBounty - 100, // iWereWolfFightMod - 100, // iWereWolfFleeMod - 20 // iWereWolfLevelToAttack +const int CSMWorld::DefaultGmsts::IntsDefaultValues[CSMWorld::DefaultGmsts::IntCount] = { + 10, // i1stPersonSneakDelta + 50, // iAlarmAttack + 90, // iAlarmKilling + 20, // iAlarmPickPocket + 1, // iAlarmStealing + 5, // iAlarmTresspass + 2, // iAlchemyMod + 100, // iAutoPCSpellMax + 2, // iAutoRepFacMod + 0, // iAutoRepLevMod + 5, // iAutoSpellAlterationMax + 70, // iAutoSpellAttSkillMin + 2, // iAutoSpellConjurationMax + 5, // iAutoSpellDestructionMax + 5, // iAutoSpellIllusionMax + 5, // iAutoSpellMysticismMax + 5, // iAutoSpellRestorationMax + 3, // iAutoSpellTimesCanCast + -1, // iBarterFailDisposition + 1, // iBarterSuccessDisposition + 30, // iBaseArmorSkill + 50, // iBlockMaxChance + 10, // iBlockMinChance + 20, // iBootsWeight + 40, // iCrimeAttack + 1000, // iCrimeKilling + 25, // iCrimePickPocket + 1000, // iCrimeThreshold + 10, // iCrimeThresholdMultiplier + 5, // iCrimeTresspass + 30, // iCuirassWeight + 100, // iDaysinPrisonMod + -50, // iDispAttackMod + -50, // iDispKilling + -20, // iDispTresspass + 1, // iFightAlarmMult + 100, // iFightAttack + 50, // iFightAttacking + 20, // iFightDistanceBase + 50, // iFightKilling + 25, // iFightPickpocket + 25, // iFightTrespass + 0, // iFlee + 5, // iGauntletWeight + 15, // iGreavesWeight + 6, // iGreetDistanceMultiplier + 4, // iGreetDuration + 5, // iHelmWeight + 50, // iKnockDownOddsBase + 50, // iKnockDownOddsMult + 2, // iLevelUp01Mult + 2, // iLevelUp02Mult + 2, // iLevelUp03Mult + 2, // iLevelUp04Mult + 3, // iLevelUp05Mult + 3, // iLevelUp06Mult + 3, // iLevelUp07Mult + 4, // iLevelUp08Mult + 4, // iLevelUp09Mult + 5, // iLevelUp10Mult + 1, // iLevelupMajorMult + 1, // iLevelupMajorMultAttribute + 1, // iLevelupMinorMult + 1, // iLevelupMinorMultAttribute + 1, // iLevelupMiscMultAttriubte + 1, // iLevelupSpecialization + 10, // iLevelupTotal + 10, // iMagicItemChargeConst + 1, // iMagicItemChargeOnce + 10, // iMagicItemChargeStrike + 5, // iMagicItemChargeUse + 192, // iMaxActivateDist + 192, // iMaxInfoDist + 4, // iMonthsToRespawn + 1, // iNumberCreatures + 10, // iPauldronWeight + 5, // iPerMinChance + 10, // iPerMinChange + 75, // iPickMaxChance + 5, // iPickMinChance + 15, // iShieldWeight + 400, // iSoulAmountForConstantEffect + 10, // iTrainingMod + 10, // iVoiceAttackOdds + 30, // iVoiceHitOdds + 10000, // iWereWolfBounty + 100, // iWereWolfFightMod + 100, // iWereWolfFleeMod + 20, // iWereWolfLevelToAttack }; -const float CSMWorld::DefaultGmsts::FloatLimits[CSMWorld::DefaultGmsts::FloatCount * 2] = -{ - -FInf, FInf, // fAIFleeFleeMult - -FInf, FInf, // fAIFleeHealthMult - -FInf, FInf, // fAIMagicSpellMult - -FInf, FInf, // fAIMeleeArmorMult - -FInf, FInf, // fAIMeleeSummWeaponMult - -FInf, FInf, // fAIMeleeWeaponMult - -FInf, FInf, // fAIRangeMagicSpellMult - -FInf, FInf, // fAIRangeMeleeWeaponMult - 0, FInf, // fAlarmRadius - -FInf, FInf, // fAthleticsRunBonus - 0, FInf, // fAudioDefaultMaxDistance - 0, FInf, // fAudioDefaultMinDistance - 0, FInf, // fAudioMaxDistanceMult - 0, FInf, // fAudioMinDistanceMult - 0, FInf, // fAudioVoiceDefaultMaxDistance - 0, FInf, // fAudioVoiceDefaultMinDistance - 0, FInf, // fAutoPCSpellChance - 0, FInf, // fAutoSpellChance - -FInf, FInf, // fBargainOfferBase - -FInf, 0, // fBargainOfferMulti - -FInf, FInf, // fBarterGoldResetDelay - 0, FInf, // fBaseRunMultiplier - -FInf, FInf, // fBlockStillBonus - 0, FInf, // fBribe1000Mod - 0, FInf, // fBribe100Mod - 0, FInf, // fBribe10Mod - 0, FInf, // fCombatAngleXY - 0, FInf, // fCombatAngleZ - 0, 1, // fCombatArmorMinMult - -180, 0, // fCombatBlockLeftAngle - 0, 180, // fCombatBlockRightAngle - 0, FInf, // fCombatCriticalStrikeMult - 0, FInf, // fCombatDelayCreature - 0, FInf, // fCombatDelayNPC - 0, FInf, // fCombatDistance - -FInf, FInf, // fCombatDistanceWerewolfMod - -FInf, FInf, // fCombatForceSideAngle - 0, FInf, // fCombatInvisoMult - 0, FInf, // fCombatKODamageMult - -FInf, FInf, // fCombatTorsoSideAngle - -FInf, FInf, // fCombatTorsoStartPercent - -FInf, FInf, // fCombatTorsoStopPercent - -FInf, FInf, // fConstantEffectMult - -FInf, FInf, // fCorpseClearDelay - -FInf, FInf, // fCorpseRespawnDelay - 0, 1, // fCrimeGoldDiscountMult - 0, FInf, // fCrimeGoldTurnInMult - 0, FInf, // fCrimeStealing - 0, FInf, // fDamageStrengthBase - 0, FInf, // fDamageStrengthMult - -FInf, FInf, // fDifficultyMult - 0, FInf, // fDiseaseXferChance - -FInf, 0, // fDispAttacking - -FInf, FInf, // fDispBargainFailMod - -FInf, FInf, // fDispBargainSuccessMod - -FInf, 0, // fDispCrimeMod - -FInf, 0, // fDispDiseaseMod - 0, FInf, // fDispFactionMod - 0, FInf, // fDispFactionRankBase - 0, FInf, // fDispFactionRankMult - 0, FInf, // fDispositionMod - 0, FInf, // fDispPersonalityBase - 0, FInf, // fDispPersonalityMult - -FInf, 0, // fDispPickPocketMod - 0, FInf, // fDispRaceMod - -FInf, 0, // fDispStealing - -FInf, 0, // fDispWeaponDrawn - 0, FInf, // fEffectCostMult - 0, FInf, // fElementalShieldMult - FEps, FInf, // fEnchantmentChanceMult - 0, FInf, // fEnchantmentConstantChanceMult - 0, FInf, // fEnchantmentConstantDurationMult - 0, FInf, // fEnchantmentMult - 0, FInf, // fEnchantmentValueMult - 0, FInf, // fEncumberedMoveEffect - 0, FInf, // fEncumbranceStrMult - 0, FInf, // fEndFatigueMult - -FInf, FInf, // fFallAcroBase - 0, FInf, // fFallAcroMult - 0, FInf, // fFallDamageDistanceMin - -FInf, FInf, // fFallDistanceBase - 0, FInf, // fFallDistanceMult - -FInf, FInf, // fFatigueAttackBase - 0, FInf, // fFatigueAttackMult - 0, FInf, // fFatigueBase - 0, FInf, // fFatigueBlockBase - 0, FInf, // fFatigueBlockMult - 0, FInf, // fFatigueJumpBase - 0, FInf, // fFatigueJumpMult - 0, FInf, // fFatigueMult - -FInf, FInf, // fFatigueReturnBase - 0, FInf, // fFatigueReturnMult - -FInf, FInf, // fFatigueRunBase - 0, FInf, // fFatigueRunMult - -FInf, FInf, // fFatigueSneakBase - 0, FInf, // fFatigueSneakMult - -FInf, FInf, // fFatigueSpellBase - -FInf, FInf, // fFatigueSpellCostMult - 0, FInf, // fFatigueSpellMult - -FInf, FInf, // fFatigueSwimRunBase - 0, FInf, // fFatigueSwimRunMult - -FInf, FInf, // fFatigueSwimWalkBase - 0, FInf, // fFatigueSwimWalkMult - -FInf, FInf, // fFightDispMult - -FInf, FInf, // fFightDistanceMultiplier - -FInf, FInf, // fFightStealing - -FInf, FInf, // fFleeDistance - -FInf, FInf, // fGreetDistanceReset - 0, FInf, // fHandtoHandHealthPer - 0, FInf, // fHandToHandReach - -FInf, FInf, // fHoldBreathEndMult - 0, FInf, // fHoldBreathTime - 0, FInf, // fIdleChanceMultiplier - -FInf, FInf, // fIngredientMult - 0, FInf, // fInteriorHeadTrackMult - -FInf, FInf, // fJumpAcrobaticsBase - 0, FInf, // fJumpAcroMultiplier - -FInf, FInf, // fJumpEncumbranceBase - 0, FInf, // fJumpEncumbranceMultiplier - -FInf, FInf, // fJumpMoveBase - 0, FInf, // fJumpMoveMult - 0, FInf, // fJumpRunMultiplier - -FInf, FInf, // fKnockDownMult - 0, FInf, // fLevelMod - 0, FInf, // fLevelUpHealthEndMult - 0, FInf, // fLightMaxMod - 0, FInf, // fLuckMod - 0, FInf, // fMagesGuildTravel - -FInf, FInf, // fMagicCreatureCastDelay - -FInf, FInf, // fMagicDetectRefreshRate - -FInf, FInf, // fMagicItemConstantMult - -FInf, FInf, // fMagicItemCostMult - -FInf, FInf, // fMagicItemOnceMult - -FInf, FInf, // fMagicItemPriceMult - 0, FInf, // fMagicItemRechargePerSecond - -FInf, FInf, // fMagicItemStrikeMult - -FInf, FInf, // fMagicItemUsedMult - 0, FInf, // fMagicStartIconBlink - 0, FInf, // fMagicSunBlockedMult - FEps, FInf, // fMajorSkillBonus - 0, FInf, // fMaxFlySpeed - 0, FInf, // fMaxHandToHandMult - 0, FInf, // fMaxHeadTrackDistance - 0, FInf, // fMaxWalkSpeed - 0, FInf, // fMaxWalkSpeedCreature - 0, FInf, // fMedMaxMod - 0, FInf, // fMessageTimePerChar - 0, FInf, // fMinFlySpeed - 0, FInf, // fMinHandToHandMult - FEps, FInf, // fMinorSkillBonus - 0, FInf, // fMinWalkSpeed - 0, FInf, // fMinWalkSpeedCreature - FEps, FInf, // fMiscSkillBonus - 0, FInf, // fNPCbaseMagickaMult - 0, FInf, // fNPCHealthBarFade - 0, FInf, // fNPCHealthBarTime - 0, FInf, // fPCbaseMagickaMult - 0, FInf, // fPerDieRollMult - 0, FInf, // fPersonalityMod - 0, FInf, // fPerTempMult - -FInf, 0, // fPickLockMult - 0, FInf, // fPickPocketMod - -FInf, FInf, // fPotionMinUsefulDuration - 0, FInf, // fPotionStrengthMult - FEps, FInf, // fPotionT1DurMult - FEps, FInf, // fPotionT1MagMult - -FInf, FInf, // fPotionT4BaseStrengthMult - -FInf, FInf, // fPotionT4EquipStrengthMult - 0, FInf, // fProjectileMaxSpeed - 0, FInf, // fProjectileMinSpeed - 0, FInf, // fProjectileThrownStoreChance - 0, FInf, // fRepairAmountMult - 0, FInf, // fRepairMult - 0, FInf, // fReputationMod - 0, FInf, // fRestMagicMult - -FInf, FInf, // fSeriousWoundMult - 0, FInf, // fSleepRandMod - 0, FInf, // fSleepRestMod - -FInf, 0, // fSneakBootMult - -FInf, FInf, // fSneakDistanceBase - 0, FInf, // fSneakDistanceMultiplier - 0, FInf, // fSneakNoViewMult - 0, FInf, // fSneakSkillMult - 0, FInf, // fSneakSpeedMultiplier - 0, FInf, // fSneakUseDelay - 0, FInf, // fSneakUseDist - 0, FInf, // fSneakViewMult - 0, FInf, // fSoulGemMult - 0, FInf, // fSpecialSkillBonus - 0, FInf, // fSpellMakingValueMult - -FInf, FInf, // fSpellPriceMult - 0, FInf, // fSpellValueMult - 0, FInf, // fStromWalkMult - 0, FInf, // fStromWindSpeed - 0, FInf, // fSuffocationDamage - 0, FInf, // fSwimHeightScale - 0, FInf, // fSwimRunAthleticsMult - 0, FInf, // fSwimRunBase - -FInf, FInf, // fSwimWalkAthleticsMult - -FInf, FInf, // fSwimWalkBase - 0, FInf, // fSwingBlockBase - 0, FInf, // fSwingBlockMult - 0, FInf, // fTargetSpellMaxSpeed - 0, FInf, // fThrownWeaponMaxSpeed - 0, FInf, // fThrownWeaponMinSpeed - 0, FInf, // fTrapCostMult - 0, FInf, // fTravelMult - 0, FInf, // fTravelTimeMult - 0, FInf, // fUnarmoredBase1 - 0, FInf, // fUnarmoredBase2 - 0, FInf, // fVanityDelay - 0, FInf, // fVoiceIdleOdds - -FInf, FInf, // fWaterReflectUpdateAlways - -FInf, FInf, // fWaterReflectUpdateSeldom - 0, FInf, // fWeaponDamageMult - 0, FInf, // fWeaponFatigueBlockMult - 0, FInf, // fWeaponFatigueMult - 0, FInf, // fWereWolfAcrobatics - -FInf, FInf, // fWereWolfAgility - -FInf, FInf, // fWereWolfAlchemy - -FInf, FInf, // fWereWolfAlteration - -FInf, FInf, // fWereWolfArmorer - -FInf, FInf, // fWereWolfAthletics - -FInf, FInf, // fWereWolfAxe - -FInf, FInf, // fWereWolfBlock - -FInf, FInf, // fWereWolfBluntWeapon - -FInf, FInf, // fWereWolfConjuration - -FInf, FInf, // fWereWolfDestruction - -FInf, FInf, // fWereWolfEnchant - -FInf, FInf, // fWereWolfEndurance - -FInf, FInf, // fWereWolfFatigue - -FInf, FInf, // fWereWolfHandtoHand - -FInf, FInf, // fWereWolfHealth - -FInf, FInf, // fWereWolfHeavyArmor - -FInf, FInf, // fWereWolfIllusion - -FInf, FInf, // fWereWolfIntellegence - -FInf, FInf, // fWereWolfLightArmor - -FInf, FInf, // fWereWolfLongBlade - -FInf, FInf, // fWereWolfLuck - -FInf, FInf, // fWereWolfMagicka - -FInf, FInf, // fWereWolfMarksman - -FInf, FInf, // fWereWolfMediumArmor - -FInf, FInf, // fWereWolfMerchantile - -FInf, FInf, // fWereWolfMysticism - -FInf, FInf, // fWereWolfPersonality - -FInf, FInf, // fWereWolfRestoration - 0, FInf, // fWereWolfRunMult - -FInf, FInf, // fWereWolfSecurity - -FInf, FInf, // fWereWolfShortBlade - -FInf, FInf, // fWereWolfSilverWeaponDamageMult - -FInf, FInf, // fWereWolfSneak - -FInf, FInf, // fWereWolfSpear - -FInf, FInf, // fWereWolfSpeechcraft - -FInf, FInf, // fWereWolfSpeed - -FInf, FInf, // fWereWolfStrength - -FInf, FInf, // fWereWolfUnarmored - -FInf, FInf, // fWereWolfWillPower - 0, FInf // fWortChanceValue +const float CSMWorld::DefaultGmsts::FloatLimits[CSMWorld::DefaultGmsts::FloatCount * 2] = { + -FInf, FInf, // fAIFleeFleeMult + -FInf, FInf, // fAIFleeHealthMult + -FInf, FInf, // fAIMagicSpellMult + -FInf, FInf, // fAIMeleeArmorMult + -FInf, FInf, // fAIMeleeSummWeaponMult + -FInf, FInf, // fAIMeleeWeaponMult + -FInf, FInf, // fAIRangeMagicSpellMult + -FInf, FInf, // fAIRangeMeleeWeaponMult + 0, FInf, // fAlarmRadius + -FInf, FInf, // fAthleticsRunBonus + 0, FInf, // fAudioDefaultMaxDistance + 0, FInf, // fAudioDefaultMinDistance + 0, FInf, // fAudioMaxDistanceMult + 0, FInf, // fAudioMinDistanceMult + 0, FInf, // fAudioVoiceDefaultMaxDistance + 0, FInf, // fAudioVoiceDefaultMinDistance + 0, FInf, // fAutoPCSpellChance + 0, FInf, // fAutoSpellChance + -FInf, FInf, // fBargainOfferBase + -FInf, 0, // fBargainOfferMulti + -FInf, FInf, // fBarterGoldResetDelay + 0, FInf, // fBaseRunMultiplier + -FInf, FInf, // fBlockStillBonus + 0, FInf, // fBribe1000Mod + 0, FInf, // fBribe100Mod + 0, FInf, // fBribe10Mod + 0, FInf, // fCombatAngleXY + 0, FInf, // fCombatAngleZ + 0, 1, // fCombatArmorMinMult + -180, 0, // fCombatBlockLeftAngle + 0, 180, // fCombatBlockRightAngle + 0, FInf, // fCombatCriticalStrikeMult + 0, FInf, // fCombatDelayCreature + 0, FInf, // fCombatDelayNPC + 0, FInf, // fCombatDistance + -FInf, FInf, // fCombatDistanceWerewolfMod + -FInf, FInf, // fCombatForceSideAngle + 0, FInf, // fCombatInvisoMult + 0, FInf, // fCombatKODamageMult + -FInf, FInf, // fCombatTorsoSideAngle + -FInf, FInf, // fCombatTorsoStartPercent + -FInf, FInf, // fCombatTorsoStopPercent + -FInf, FInf, // fConstantEffectMult + -FInf, FInf, // fCorpseClearDelay + -FInf, FInf, // fCorpseRespawnDelay + 0, 1, // fCrimeGoldDiscountMult + 0, FInf, // fCrimeGoldTurnInMult + 0, FInf, // fCrimeStealing + 0, FInf, // fDamageStrengthBase + 0, FInf, // fDamageStrengthMult + -FInf, FInf, // fDifficultyMult + 0, FInf, // fDiseaseXferChance + -FInf, 0, // fDispAttacking + -FInf, FInf, // fDispBargainFailMod + -FInf, FInf, // fDispBargainSuccessMod + -FInf, 0, // fDispCrimeMod + -FInf, 0, // fDispDiseaseMod + 0, FInf, // fDispFactionMod + 0, FInf, // fDispFactionRankBase + 0, FInf, // fDispFactionRankMult + 0, FInf, // fDispositionMod + 0, FInf, // fDispPersonalityBase + 0, FInf, // fDispPersonalityMult + -FInf, 0, // fDispPickPocketMod + 0, FInf, // fDispRaceMod + -FInf, 0, // fDispStealing + -FInf, 0, // fDispWeaponDrawn + 0, FInf, // fEffectCostMult + 0, FInf, // fElementalShieldMult + FEps, FInf, // fEnchantmentChanceMult + 0, FInf, // fEnchantmentConstantChanceMult + 0, FInf, // fEnchantmentConstantDurationMult + 0, FInf, // fEnchantmentMult + 0, FInf, // fEnchantmentValueMult + 0, FInf, // fEncumberedMoveEffect + 0, FInf, // fEncumbranceStrMult + 0, FInf, // fEndFatigueMult + -FInf, FInf, // fFallAcroBase + 0, FInf, // fFallAcroMult + 0, FInf, // fFallDamageDistanceMin + -FInf, FInf, // fFallDistanceBase + 0, FInf, // fFallDistanceMult + -FInf, FInf, // fFatigueAttackBase + 0, FInf, // fFatigueAttackMult + 0, FInf, // fFatigueBase + 0, FInf, // fFatigueBlockBase + 0, FInf, // fFatigueBlockMult + 0, FInf, // fFatigueJumpBase + 0, FInf, // fFatigueJumpMult + 0, FInf, // fFatigueMult + -FInf, FInf, // fFatigueReturnBase + 0, FInf, // fFatigueReturnMult + -FInf, FInf, // fFatigueRunBase + 0, FInf, // fFatigueRunMult + -FInf, FInf, // fFatigueSneakBase + 0, FInf, // fFatigueSneakMult + -FInf, FInf, // fFatigueSpellBase + -FInf, FInf, // fFatigueSpellCostMult + 0, FInf, // fFatigueSpellMult + -FInf, FInf, // fFatigueSwimRunBase + 0, FInf, // fFatigueSwimRunMult + -FInf, FInf, // fFatigueSwimWalkBase + 0, FInf, // fFatigueSwimWalkMult + -FInf, FInf, // fFightDispMult + -FInf, FInf, // fFightDistanceMultiplier + -FInf, FInf, // fFightStealing + -FInf, FInf, // fFleeDistance + -FInf, FInf, // fGreetDistanceReset + 0, FInf, // fHandtoHandHealthPer + 0, FInf, // fHandToHandReach + -FInf, FInf, // fHoldBreathEndMult + 0, FInf, // fHoldBreathTime + 0, FInf, // fIdleChanceMultiplier + -FInf, FInf, // fIngredientMult + 0, FInf, // fInteriorHeadTrackMult + -FInf, FInf, // fJumpAcrobaticsBase + 0, FInf, // fJumpAcroMultiplier + -FInf, FInf, // fJumpEncumbranceBase + 0, FInf, // fJumpEncumbranceMultiplier + -FInf, FInf, // fJumpMoveBase + 0, FInf, // fJumpMoveMult + 0, FInf, // fJumpRunMultiplier + -FInf, FInf, // fKnockDownMult + 0, FInf, // fLevelMod + 0, FInf, // fLevelUpHealthEndMult + 0, FInf, // fLightMaxMod + 0, FInf, // fLuckMod + 0, FInf, // fMagesGuildTravel + -FInf, FInf, // fMagicCreatureCastDelay + -FInf, FInf, // fMagicDetectRefreshRate + -FInf, FInf, // fMagicItemConstantMult + -FInf, FInf, // fMagicItemCostMult + -FInf, FInf, // fMagicItemOnceMult + -FInf, FInf, // fMagicItemPriceMult + 0, FInf, // fMagicItemRechargePerSecond + -FInf, FInf, // fMagicItemStrikeMult + -FInf, FInf, // fMagicItemUsedMult + 0, FInf, // fMagicStartIconBlink + 0, FInf, // fMagicSunBlockedMult + FEps, FInf, // fMajorSkillBonus + 0, FInf, // fMaxFlySpeed + 0, FInf, // fMaxHandToHandMult + 0, FInf, // fMaxHeadTrackDistance + 0, FInf, // fMaxWalkSpeed + 0, FInf, // fMaxWalkSpeedCreature + 0, FInf, // fMedMaxMod + 0, FInf, // fMessageTimePerChar + 0, FInf, // fMinFlySpeed + 0, FInf, // fMinHandToHandMult + FEps, FInf, // fMinorSkillBonus + 0, FInf, // fMinWalkSpeed + 0, FInf, // fMinWalkSpeedCreature + FEps, FInf, // fMiscSkillBonus + 0, FInf, // fNPCbaseMagickaMult + 0, FInf, // fNPCHealthBarFade + 0, FInf, // fNPCHealthBarTime + 0, FInf, // fPCbaseMagickaMult + 0, FInf, // fPerDieRollMult + 0, FInf, // fPersonalityMod + 0, FInf, // fPerTempMult + -FInf, 0, // fPickLockMult + 0, FInf, // fPickPocketMod + -FInf, FInf, // fPotionMinUsefulDuration + 0, FInf, // fPotionStrengthMult + FEps, FInf, // fPotionT1DurMult + FEps, FInf, // fPotionT1MagMult + -FInf, FInf, // fPotionT4BaseStrengthMult + -FInf, FInf, // fPotionT4EquipStrengthMult + 0, FInf, // fProjectileMaxSpeed + 0, FInf, // fProjectileMinSpeed + 0, FInf, // fProjectileThrownStoreChance + 0, FInf, // fRepairAmountMult + 0, FInf, // fRepairMult + 0, FInf, // fReputationMod + 0, FInf, // fRestMagicMult + -FInf, FInf, // fSeriousWoundMult + 0, FInf, // fSleepRandMod + 0, FInf, // fSleepRestMod + -FInf, 0, // fSneakBootMult + -FInf, FInf, // fSneakDistanceBase + 0, FInf, // fSneakDistanceMultiplier + 0, FInf, // fSneakNoViewMult + 0, FInf, // fSneakSkillMult + 0, FInf, // fSneakSpeedMultiplier + 0, FInf, // fSneakUseDelay + 0, FInf, // fSneakUseDist + 0, FInf, // fSneakViewMult + 0, FInf, // fSoulGemMult + 0, FInf, // fSpecialSkillBonus + 0, FInf, // fSpellMakingValueMult + -FInf, FInf, // fSpellPriceMult + 0, FInf, // fSpellValueMult + 0, FInf, // fStromWalkMult + 0, FInf, // fStromWindSpeed + 0, FInf, // fSuffocationDamage + 0, FInf, // fSwimHeightScale + 0, FInf, // fSwimRunAthleticsMult + 0, FInf, // fSwimRunBase + -FInf, FInf, // fSwimWalkAthleticsMult + -FInf, FInf, // fSwimWalkBase + 0, FInf, // fSwingBlockBase + 0, FInf, // fSwingBlockMult + 0, FInf, // fTargetSpellMaxSpeed + 0, FInf, // fThrownWeaponMaxSpeed + 0, FInf, // fThrownWeaponMinSpeed + 0, FInf, // fTrapCostMult + 0, FInf, // fTravelMult + 0, FInf, // fTravelTimeMult + 0, FInf, // fUnarmoredBase1 + 0, FInf, // fUnarmoredBase2 + 0, FInf, // fVanityDelay + 0, FInf, // fVoiceIdleOdds + -FInf, FInf, // fWaterReflectUpdateAlways + -FInf, FInf, // fWaterReflectUpdateSeldom + 0, FInf, // fWeaponDamageMult + 0, FInf, // fWeaponFatigueBlockMult + 0, FInf, // fWeaponFatigueMult + 0, FInf, // fWereWolfAcrobatics + -FInf, FInf, // fWereWolfAgility + -FInf, FInf, // fWereWolfAlchemy + -FInf, FInf, // fWereWolfAlteration + -FInf, FInf, // fWereWolfArmorer + -FInf, FInf, // fWereWolfAthletics + -FInf, FInf, // fWereWolfAxe + -FInf, FInf, // fWereWolfBlock + -FInf, FInf, // fWereWolfBluntWeapon + -FInf, FInf, // fWereWolfConjuration + -FInf, FInf, // fWereWolfDestruction + -FInf, FInf, // fWereWolfEnchant + -FInf, FInf, // fWereWolfEndurance + -FInf, FInf, // fWereWolfFatigue + -FInf, FInf, // fWereWolfHandtoHand + -FInf, FInf, // fWereWolfHealth + -FInf, FInf, // fWereWolfHeavyArmor + -FInf, FInf, // fWereWolfIllusion + -FInf, FInf, // fWereWolfIntellegence + -FInf, FInf, // fWereWolfLightArmor + -FInf, FInf, // fWereWolfLongBlade + -FInf, FInf, // fWereWolfLuck + -FInf, FInf, // fWereWolfMagicka + -FInf, FInf, // fWereWolfMarksman + -FInf, FInf, // fWereWolfMediumArmor + -FInf, FInf, // fWereWolfMerchantile + -FInf, FInf, // fWereWolfMysticism + -FInf, FInf, // fWereWolfPersonality + -FInf, FInf, // fWereWolfRestoration + 0, FInf, // fWereWolfRunMult + -FInf, FInf, // fWereWolfSecurity + -FInf, FInf, // fWereWolfShortBlade + -FInf, FInf, // fWereWolfSilverWeaponDamageMult + -FInf, FInf, // fWereWolfSneak + -FInf, FInf, // fWereWolfSpear + -FInf, FInf, // fWereWolfSpeechcraft + -FInf, FInf, // fWereWolfSpeed + -FInf, FInf, // fWereWolfStrength + -FInf, FInf, // fWereWolfUnarmored + -FInf, FInf, // fWereWolfWillPower + 0, FInf, // fWortChanceValue }; -const int CSMWorld::DefaultGmsts::IntLimits[CSMWorld::DefaultGmsts::IntCount * 2] = -{ - IMin, IMax, // i1stPersonSneakDelta - IMin, IMax, // iAlarmAttack - IMin, IMax, // iAlarmKilling - IMin, IMax, // iAlarmPickPocket - IMin, IMax, // iAlarmStealing - IMin, IMax, // iAlarmTresspass - IMin, IMax, // iAlchemyMod - 0, IMax, // iAutoPCSpellMax - IMin, IMax, // iAutoRepFacMod - IMin, IMax, // iAutoRepLevMod - IMin, IMax, // iAutoSpellAlterationMax - 0, IMax, // iAutoSpellAttSkillMin - IMin, IMax, // iAutoSpellConjurationMax - IMin, IMax, // iAutoSpellDestructionMax - IMin, IMax, // iAutoSpellIllusionMax - IMin, IMax, // iAutoSpellMysticismMax - IMin, IMax, // iAutoSpellRestorationMax - 0, IMax, // iAutoSpellTimesCanCast - IMin, 0, // iBarterFailDisposition - 0, IMax, // iBarterSuccessDisposition - 1, IMax, // iBaseArmorSkill - 0, IMax, // iBlockMaxChance - 0, IMax, // iBlockMinChance - 0, IMax, // iBootsWeight - IMin, IMax, // iCrimeAttack - IMin, IMax, // iCrimeKilling - IMin, IMax, // iCrimePickPocket - 0, IMax, // iCrimeThreshold - 0, IMax, // iCrimeThresholdMultiplier - IMin, IMax, // iCrimeTresspass - 0, IMax, // iCuirassWeight - 1, IMax, // iDaysinPrisonMod - IMin, 0, // iDispAttackMod - IMin, 0, // iDispKilling - IMin, 0, // iDispTresspass - IMin, IMax, // iFightAlarmMult - IMin, IMax, // iFightAttack - IMin, IMax, // iFightAttacking - 0, IMax, // iFightDistanceBase - IMin, IMax, // iFightKilling - IMin, IMax, // iFightPickpocket - IMin, IMax, // iFightTrespass - IMin, IMax, // iFlee - 0, IMax, // iGauntletWeight - 0, IMax, // iGreavesWeight - 0, IMax, // iGreetDistanceMultiplier - 0, IMax, // iGreetDuration - 0, IMax, // iHelmWeight - IMin, IMax, // iKnockDownOddsBase - IMin, IMax, // iKnockDownOddsMult - IMin, IMax, // iLevelUp01Mult - IMin, IMax, // iLevelUp02Mult - IMin, IMax, // iLevelUp03Mult - IMin, IMax, // iLevelUp04Mult - IMin, IMax, // iLevelUp05Mult - IMin, IMax, // iLevelUp06Mult - IMin, IMax, // iLevelUp07Mult - IMin, IMax, // iLevelUp08Mult - IMin, IMax, // iLevelUp09Mult - IMin, IMax, // iLevelUp10Mult - IMin, IMax, // iLevelupMajorMult - IMin, IMax, // iLevelupMajorMultAttribute - IMin, IMax, // iLevelupMinorMult - IMin, IMax, // iLevelupMinorMultAttribute - IMin, IMax, // iLevelupMiscMultAttriubte - IMin, IMax, // iLevelupSpecialization - IMin, IMax, // iLevelupTotal - IMin, IMax, // iMagicItemChargeConst - IMin, IMax, // iMagicItemChargeOnce - IMin, IMax, // iMagicItemChargeStrike - IMin, IMax, // iMagicItemChargeUse - IMin, IMax, // iMaxActivateDist - IMin, IMax, // iMaxInfoDist - 0, IMax, // iMonthsToRespawn - 0, IMax, // iNumberCreatures - 0, IMax, // iPauldronWeight - 0, IMax, // iPerMinChance - 0, IMax, // iPerMinChange - 0, IMax, // iPickMaxChance - 0, IMax, // iPickMinChance - 0, IMax, // iShieldWeight - 0, IMax, // iSoulAmountForConstantEffect - 0, IMax, // iTrainingMod - 0, IMax, // iVoiceAttackOdds - 0, IMax, // iVoiceHitOdds - IMin, IMax, // iWereWolfBounty - IMin, IMax, // iWereWolfFightMod - IMin, IMax, // iWereWolfFleeMod - IMin, IMax // iWereWolfLevelToAttack +const int CSMWorld::DefaultGmsts::IntLimits[CSMWorld::DefaultGmsts::IntCount * 2] = { + IMin, IMax, // i1stPersonSneakDelta + IMin, IMax, // iAlarmAttack + IMin, IMax, // iAlarmKilling + IMin, IMax, // iAlarmPickPocket + IMin, IMax, // iAlarmStealing + IMin, IMax, // iAlarmTresspass + IMin, IMax, // iAlchemyMod + 0, IMax, // iAutoPCSpellMax + IMin, IMax, // iAutoRepFacMod + IMin, IMax, // iAutoRepLevMod + IMin, IMax, // iAutoSpellAlterationMax + 0, IMax, // iAutoSpellAttSkillMin + IMin, IMax, // iAutoSpellConjurationMax + IMin, IMax, // iAutoSpellDestructionMax + IMin, IMax, // iAutoSpellIllusionMax + IMin, IMax, // iAutoSpellMysticismMax + IMin, IMax, // iAutoSpellRestorationMax + 0, IMax, // iAutoSpellTimesCanCast + IMin, 0, // iBarterFailDisposition + 0, IMax, // iBarterSuccessDisposition + 1, IMax, // iBaseArmorSkill + 0, IMax, // iBlockMaxChance + 0, IMax, // iBlockMinChance + 0, IMax, // iBootsWeight + IMin, IMax, // iCrimeAttack + IMin, IMax, // iCrimeKilling + IMin, IMax, // iCrimePickPocket + 0, IMax, // iCrimeThreshold + 0, IMax, // iCrimeThresholdMultiplier + IMin, IMax, // iCrimeTresspass + 0, IMax, // iCuirassWeight + 1, IMax, // iDaysinPrisonMod + IMin, 0, // iDispAttackMod + IMin, 0, // iDispKilling + IMin, 0, // iDispTresspass + IMin, IMax, // iFightAlarmMult + IMin, IMax, // iFightAttack + IMin, IMax, // iFightAttacking + 0, IMax, // iFightDistanceBase + IMin, IMax, // iFightKilling + IMin, IMax, // iFightPickpocket + IMin, IMax, // iFightTrespass + IMin, IMax, // iFlee + 0, IMax, // iGauntletWeight + 0, IMax, // iGreavesWeight + 0, IMax, // iGreetDistanceMultiplier + 0, IMax, // iGreetDuration + 0, IMax, // iHelmWeight + IMin, IMax, // iKnockDownOddsBase + IMin, IMax, // iKnockDownOddsMult + IMin, IMax, // iLevelUp01Mult + IMin, IMax, // iLevelUp02Mult + IMin, IMax, // iLevelUp03Mult + IMin, IMax, // iLevelUp04Mult + IMin, IMax, // iLevelUp05Mult + IMin, IMax, // iLevelUp06Mult + IMin, IMax, // iLevelUp07Mult + IMin, IMax, // iLevelUp08Mult + IMin, IMax, // iLevelUp09Mult + IMin, IMax, // iLevelUp10Mult + IMin, IMax, // iLevelupMajorMult + IMin, IMax, // iLevelupMajorMultAttribute + IMin, IMax, // iLevelupMinorMult + IMin, IMax, // iLevelupMinorMultAttribute + IMin, IMax, // iLevelupMiscMultAttriubte + IMin, IMax, // iLevelupSpecialization + IMin, IMax, // iLevelupTotal + IMin, IMax, // iMagicItemChargeConst + IMin, IMax, // iMagicItemChargeOnce + IMin, IMax, // iMagicItemChargeStrike + IMin, IMax, // iMagicItemChargeUse + IMin, IMax, // iMaxActivateDist + IMin, IMax, // iMaxInfoDist + 0, IMax, // iMonthsToRespawn + 0, IMax, // iNumberCreatures + 0, IMax, // iPauldronWeight + 0, IMax, // iPerMinChance + 0, IMax, // iPerMinChange + 0, IMax, // iPickMaxChance + 0, IMax, // iPickMinChance + 0, IMax, // iShieldWeight + 0, IMax, // iSoulAmountForConstantEffect + 0, IMax, // iTrainingMod + 0, IMax, // iVoiceAttackOdds + 0, IMax, // iVoiceHitOdds + IMin, IMax, // iWereWolfBounty + IMin, IMax, // iWereWolfFightMod + IMin, IMax, // iWereWolfFleeMod + IMin, IMax, // iWereWolfLevelToAttack }; diff --git a/apps/opencs/model/world/defaultgmsts.hpp b/apps/opencs/model/world/defaultgmsts.hpp index a2888ed6acb..baee7d66a9b 100644 --- a/apps/opencs/model/world/defaultgmsts.hpp +++ b/apps/opencs/model/world/defaultgmsts.hpp @@ -3,24 +3,26 @@ #include -namespace CSMWorld { - namespace DefaultGmsts { - +namespace CSMWorld +{ + namespace DefaultGmsts + { + const size_t FloatCount = 258; const size_t IntCount = 89; const size_t StringCount = 1174; - + const size_t OptionalFloatCount = 42; const size_t OptionalIntCount = 4; const size_t OptionalStringCount = 26; - + extern const char* Floats[]; - extern const char * Ints[]; - extern const char * Strings[]; - - extern const char * OptionalFloats[]; - extern const char * OptionalInts[]; - extern const char * OptionalStrings[]; + extern const char* Ints[]; + extern const char* Strings[]; + + extern const char* OptionalFloats[]; + extern const char* OptionalInts[]; + extern const char* OptionalStrings[]; extern const float FloatsDefaultValues[]; extern const int IntsDefaultValues[]; diff --git a/apps/opencs/model/world/disabletag.hpp b/apps/opencs/model/world/disabletag.hpp new file mode 100644 index 00000000000..1aee1ce6fcb --- /dev/null +++ b/apps/opencs/model/world/disabletag.hpp @@ -0,0 +1,22 @@ +#ifndef CSM_WOLRD_DISABLETAG_H +#define CSM_WOLRD_DISABLETAG_H + +#include + +namespace CSMWorld +{ + class DisableTag + { + public: + static QVariant getVariant() { return QVariant::fromValue(CSMWorld::DisableTag()); } + + static bool isDisableTag(const QVariant& variant) + { + return strcmp(variant.typeName(), "CSMWorld::DisableTag") == 0; + } + }; +} + +Q_DECLARE_METATYPE(CSMWorld::DisableTag) + +#endif diff --git a/apps/opencs/model/world/idcollection.cpp b/apps/opencs/model/world/idcollection.cpp new file mode 100644 index 00000000000..ca56eceb694 --- /dev/null +++ b/apps/opencs/model/world/idcollection.cpp @@ -0,0 +1,170 @@ +#include "idcollection.hpp" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +namespace ESM +{ + class ESMReader; +} + +namespace CSMWorld +{ + template <> + int BaseIdCollection::load(ESM::ESMReader& reader, bool base) + { + Pathgrid record; + bool isDeleted = false; + + loadRecord(record, reader, isDeleted, base); + + const ESM::RefId id = getRecordId(record); + int index = this->searchId(id); + + if (record.mPoints.empty() || record.mEdges.empty()) + isDeleted = true; + + if (isDeleted) + { + if (index == -1) + { + // deleting a record that does not exist + // ignore it for now + /// \todo report the problem to the user + return -1; + } + + if (base) + { + this->removeRows(index, 1); + return -1; + } + + auto baseRecord = std::make_unique>(this->getRecord(index)); + baseRecord->mState = RecordBase::State_Deleted; + this->setRecord(index, std::move(baseRecord)); + return index; + } + + return load(record, base, index); + } + + const Record* IdCollection::searchRecord(std::uint16_t index, int plugin) const + { + auto found = mIndices.find({ plugin, index }); + if (found != mIndices.end()) + { + int index = searchId(found->second); + if (index != -1) + return &getRecord(index); + } + return nullptr; + } + + const std::string* IdCollection::getLandTexture(std::uint16_t index, int plugin) const + { + const Record* record = searchRecord(index, plugin); + if (record && !record->isDeleted()) + return &record->get().mTexture; + return nullptr; + } + + void IdCollection::loadRecord( + ESM::LandTexture& record, ESM::ESMReader& reader, bool& isDeleted, bool base) + { + record.load(reader, isDeleted); + int plugin = base ? reader.getIndex() : -1; + mIndices.emplace(std::make_pair(plugin, record.mIndex), record.mId); + } + + std::uint16_t IdCollection::assignNewIndex(ESM::RefId id) + { + std::uint16_t index = 0; + if (!mIndices.empty()) + { + auto end = mIndices.lower_bound({ -1, std::numeric_limits::max() }); + if (end != mIndices.begin()) + end = std::prev(end); + if (end->first.first == -1) + { + constexpr std::uint16_t maxIndex = std::numeric_limits::max() - 1; + if (end->first.second < maxIndex) + index = end->first.second + 1; + else + { + std::uint16_t prevIndex = 0; + for (auto it = mIndices.lower_bound({ -1, 0 }); it != end; ++it) + { + if (prevIndex != it->first.second) + { + index = prevIndex; + break; + } + ++prevIndex; + } + } + } + } + mIndices.emplace(std::make_pair(-1, index), id); + return index; + } + + bool IdCollection::touchRecord(const ESM::RefId& id) + { + int row = BaseIdCollection::touchRecordImp(id); + if (row != -1) + { + const_cast(getRecord(row).get()).mIndex = assignNewIndex(id); + return true; + } + return false; + } + + void IdCollection::cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) + { + int row = cloneRecordImp(origin, destination, type); + const_cast(getRecord(row).get()).mIndex = assignNewIndex(destination); + } + + void IdCollection::appendBlankRecord(const ESM::RefId& id, UniversalId::Type type) + { + ESM::LandTexture record; + record.blank(); + record.mId = id; + record.mIndex = assignNewIndex(id); + + auto record2 = std::make_unique>(); + record2->mState = Record::State_ModifiedOnly; + record2->mModified = std::move(record); + + insertRecord(std::move(record2), getAppendIndex(id, type), type); + } + + void IdCollection::removeRows(int index, int count) + { + for (int row = index; row < index + count; ++row) + { + const auto& record = getRecord(row); + if (record.isModified()) + mIndices.erase({ -1, record.get().mIndex }); + } + BaseIdCollection::removeRows(index, count); + } + + void IdCollection::replace(int index, std::unique_ptr record) + { + const auto& current = getRecord(index); + if (current.isModified() && !record->isModified()) + mIndices.erase({ -1, current.get().mIndex }); + BaseIdCollection::replace(index, std::move(record)); + } +} diff --git a/apps/opencs/model/world/idcollection.hpp b/apps/opencs/model/world/idcollection.hpp index 7849aab9262..d4aa82ffc99 100644 --- a/apps/opencs/model/world/idcollection.hpp +++ b/apps/opencs/model/world/idcollection.hpp @@ -1,75 +1,120 @@ #ifndef CSM_WOLRD_IDCOLLECTION_H #define CSM_WOLRD_IDCOLLECTION_H -#include +#include +#include +#include + +#include + +#include +#include #include "collection.hpp" #include "land.hpp" +#include "pathgrid.hpp" + +namespace ESM +{ + class ESMReader; + struct LandTexture; +} namespace CSMWorld { + struct Pathgrid; + /// \brief Single type collection of top level records - template > - class IdCollection : public Collection + template + class BaseIdCollection : public Collection + { + virtual void loadRecord(ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted, bool base); + + public: + /// \return Index of loaded record (-1 if no record was loaded) + int load(ESM::ESMReader& reader, bool base); + + /// \param index Index at which the record can be found. + /// Special values: -2 index unknown, -1 record does not exist yet and therefore + /// does not have an index + /// + /// \return index + int load(const ESXRecordT& record, bool base, int index = -2); + + bool tryDelete(const ESM::RefId& id); + ///< Try deleting \a id. If the id does not exist or can't be deleted the call is ignored. + /// + /// \return Has the ID been deleted? + }; + + template + class IdCollection : public BaseIdCollection { - virtual void loadRecord (ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted); + }; + + template <> + class IdCollection : public BaseIdCollection + { + std::map, ESM::RefId> mIndices; + + void loadRecord(ESM::LandTexture& record, ESM::ESMReader& reader, bool& isDeleted, bool base) override; + + std::uint16_t assignNewIndex(ESM::RefId id); - public: + public: + const Record* searchRecord(std::uint16_t index, int plugin) const; - /// \return Index of loaded record (-1 if no record was loaded) - int load (ESM::ESMReader& reader, bool base); + const std::string* getLandTexture(std::uint16_t index, int plugin) const; - /// \param index Index at which the record can be found. - /// Special values: -2 index unknown, -1 record does not exist yet and therefore - /// does not have an index - /// - /// \return index - int load (const ESXRecordT& record, bool base, int index = -2); + bool touchRecord(const ESM::RefId& id) override; - bool tryDelete (const std::string& id); - ///< Try deleting \a id. If the id does not exist or can't be deleted the call is ignored. - /// - /// \return Has the ID been deleted? + void cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) override; + + void appendBlankRecord(const ESM::RefId& id, UniversalId::Type type) override; + + void removeRows(int index, int count) override; + + void replace(int index, std::unique_ptr record) override; }; - template - void IdCollection::loadRecord (ESXRecordT& record, - ESM::ESMReader& reader, - bool& isDeleted) + template + void BaseIdCollection::loadRecord( + ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted, bool base) { - record.load (reader, isDeleted); + record.load(reader, isDeleted); } - template<> - inline void IdCollection >::loadRecord (Land& record, - ESM::ESMReader& reader, bool& isDeleted) + template <> + inline void BaseIdCollection::loadRecord(Land& record, ESM::ESMReader& reader, bool& isDeleted, bool base) { - record.load (reader, isDeleted); + record.load(reader, isDeleted); // Load all land data for now. A future optimisation may only load non-base data // if a suitable mechanism for avoiding race conditions can be established. - int flags = ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | - ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX; - record.loadData (flags); + int flags = ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX; + record.loadData(flags); // Prevent data from being reloaded. record.mContext.filename.clear(); + if (!base) + record.setPlugin(-1); } - template - int IdCollection::load (ESM::ESMReader& reader, bool base) + template + int BaseIdCollection::load(ESM::ESMReader& reader, bool base) { ESXRecordT record; bool isDeleted = false; - loadRecord (record, reader, isDeleted); + loadRecord(record, reader, isDeleted, base); - std::string id = IdAccessorT().getId (record); - int index = this->searchId (id); + ESM::RefId id = getRecordId(record); + int index = this->searchId(id); if (isDeleted) { - if (index==-1) + if (index == -1) { // deleting a record that does not exist // ignore it for now @@ -79,77 +124,80 @@ namespace CSMWorld if (base) { - this->removeRows (index, 1); + this->removeRows(index, 1); return -1; } - Record baseRecord = this->getRecord (index); - baseRecord.mState = RecordBase::State_Deleted; - this->setRecord (index, baseRecord); + auto baseRecord = std::make_unique>(this->getRecord(index)); + baseRecord->mState = RecordBase::State_Deleted; + this->setRecord(index, std::move(baseRecord)); return index; } - return load (record, base, index); + return load(record, base, index); } - template - int IdCollection::load (const ESXRecordT& record, bool base, - int index) + template + int BaseIdCollection::load(const ESXRecordT& record, bool base, int index) { - if (index==-2) - index = this->searchId (IdAccessorT().getId (record)); + if (index == -2) // index unknown + index = this->searchId(getRecordId(record)); - if (index==-1) + if (index == -1) { // new record - Record record2; - record2.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - (base ? record2.mBase : record2.mModified) = record; + auto record2 = std::make_unique>(); + record2->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record2->mBase : record2->mModified) = record; index = this->getSize(); - this->appendRecord (record2); + this->appendRecord(std::move(record2)); } else { // old record - Record record2 = Collection::getRecord (index); + auto record2 = std::make_unique>(Collection::getRecord(index)); if (base) - record2.mBase = record; + record2->mBase = record; else - record2.setModified (record); + record2->setModified(record); - this->setRecord (index, record2); + this->setRecord(index, std::move(record2)); } return index; } - template - bool IdCollection::tryDelete (const std::string& id) + template + bool BaseIdCollection::tryDelete(const ESM::RefId& id) { - int index = this->searchId (id); + int index = this->searchId(id); - if (index==-1) + if (index == -1) return false; - Record record = Collection::getRecord (index); + const Record& record = Collection::getRecord(index); if (record.isDeleted()) return false; - if (record.mState==RecordBase::State_ModifiedOnly) + if (record.mState == RecordBase::State_ModifiedOnly) { - Collection::removeRows (index, 1); + Collection::removeRows(index, 1); } else { - record.mState = RecordBase::State_Deleted; - this->setRecord (index, record); + auto record2 = std::make_unique>(Collection::getRecord(index)); + record2->mState = RecordBase::State_Deleted; + this->setRecord(index, std::move(record2)); } return true; } + + template <> + int BaseIdCollection::load(ESM::ESMReader& reader, bool base); } #endif diff --git a/apps/opencs/model/world/idcompletionmanager.cpp b/apps/opencs/model/world/idcompletionmanager.cpp index 649a9603876..a4fdb4776d3 100644 --- a/apps/opencs/model/world/idcompletionmanager.cpp +++ b/apps/opencs/model/world/idcompletionmanager.cpp @@ -1,49 +1,59 @@ #include "idcompletionmanager.hpp" +#include #include +#include +#include +#include + +#include +#include +#include + #include "../../view/widget/completerpopup.hpp" #include "data.hpp" #include "idtablebase.hpp" +class QAbstractItemView; + namespace { std::map generateModelTypes() { std::map types; - types[CSMWorld::ColumnBase::Display_BodyPart ] = CSMWorld::UniversalId::Type_BodyPart; - types[CSMWorld::ColumnBase::Display_Cell ] = CSMWorld::UniversalId::Type_Cell; - types[CSMWorld::ColumnBase::Display_Class ] = CSMWorld::UniversalId::Type_Class; + types[CSMWorld::ColumnBase::Display_BodyPart] = CSMWorld::UniversalId::Type_BodyPart; + types[CSMWorld::ColumnBase::Display_Cell] = CSMWorld::UniversalId::Type_Cell; + types[CSMWorld::ColumnBase::Display_Class] = CSMWorld::UniversalId::Type_Class; types[CSMWorld::ColumnBase::Display_CreatureLevelledList] = CSMWorld::UniversalId::Type_Referenceable; - types[CSMWorld::ColumnBase::Display_Creature ] = CSMWorld::UniversalId::Type_Referenceable; - types[CSMWorld::ColumnBase::Display_Enchantment ] = CSMWorld::UniversalId::Type_Enchantment; - types[CSMWorld::ColumnBase::Display_Faction ] = CSMWorld::UniversalId::Type_Faction; - types[CSMWorld::ColumnBase::Display_GlobalVariable ] = CSMWorld::UniversalId::Type_Global; - types[CSMWorld::ColumnBase::Display_Icon ] = CSMWorld::UniversalId::Type_Icon; - types[CSMWorld::ColumnBase::Display_Journal ] = CSMWorld::UniversalId::Type_Journal; - types[CSMWorld::ColumnBase::Display_Mesh ] = CSMWorld::UniversalId::Type_Mesh; - types[CSMWorld::ColumnBase::Display_Miscellaneous ] = CSMWorld::UniversalId::Type_Referenceable; - types[CSMWorld::ColumnBase::Display_Npc ] = CSMWorld::UniversalId::Type_Referenceable; - types[CSMWorld::ColumnBase::Display_Race ] = CSMWorld::UniversalId::Type_Race; - types[CSMWorld::ColumnBase::Display_Region ] = CSMWorld::UniversalId::Type_Region; - types[CSMWorld::ColumnBase::Display_Referenceable ] = CSMWorld::UniversalId::Type_Referenceable; - types[CSMWorld::ColumnBase::Display_Script ] = CSMWorld::UniversalId::Type_Script; - types[CSMWorld::ColumnBase::Display_Skill ] = CSMWorld::UniversalId::Type_Skill; - types[CSMWorld::ColumnBase::Display_Sound ] = CSMWorld::UniversalId::Type_Sound; - types[CSMWorld::ColumnBase::Display_SoundRes ] = CSMWorld::UniversalId::Type_SoundRes; - types[CSMWorld::ColumnBase::Display_Spell ] = CSMWorld::UniversalId::Type_Spell; - types[CSMWorld::ColumnBase::Display_Static ] = CSMWorld::UniversalId::Type_Referenceable; - types[CSMWorld::ColumnBase::Display_Texture ] = CSMWorld::UniversalId::Type_Texture; - types[CSMWorld::ColumnBase::Display_Topic ] = CSMWorld::UniversalId::Type_Topic; - types[CSMWorld::ColumnBase::Display_Weapon ] = CSMWorld::UniversalId::Type_Referenceable; + types[CSMWorld::ColumnBase::Display_Creature] = CSMWorld::UniversalId::Type_Referenceable; + types[CSMWorld::ColumnBase::Display_Enchantment] = CSMWorld::UniversalId::Type_Enchantment; + types[CSMWorld::ColumnBase::Display_Faction] = CSMWorld::UniversalId::Type_Faction; + types[CSMWorld::ColumnBase::Display_GlobalVariable] = CSMWorld::UniversalId::Type_Global; + types[CSMWorld::ColumnBase::Display_Icon] = CSMWorld::UniversalId::Type_Icon; + types[CSMWorld::ColumnBase::Display_Journal] = CSMWorld::UniversalId::Type_Journal; + types[CSMWorld::ColumnBase::Display_Mesh] = CSMWorld::UniversalId::Type_Mesh; + types[CSMWorld::ColumnBase::Display_Miscellaneous] = CSMWorld::UniversalId::Type_Referenceable; + types[CSMWorld::ColumnBase::Display_Npc] = CSMWorld::UniversalId::Type_Referenceable; + types[CSMWorld::ColumnBase::Display_Race] = CSMWorld::UniversalId::Type_Race; + types[CSMWorld::ColumnBase::Display_Region] = CSMWorld::UniversalId::Type_Region; + types[CSMWorld::ColumnBase::Display_Referenceable] = CSMWorld::UniversalId::Type_Referenceable; + types[CSMWorld::ColumnBase::Display_Script] = CSMWorld::UniversalId::Type_Script; + types[CSMWorld::ColumnBase::Display_Skill] = CSMWorld::UniversalId::Type_Skill; + types[CSMWorld::ColumnBase::Display_Sound] = CSMWorld::UniversalId::Type_Sound; + types[CSMWorld::ColumnBase::Display_SoundRes] = CSMWorld::UniversalId::Type_SoundRes; + types[CSMWorld::ColumnBase::Display_Spell] = CSMWorld::UniversalId::Type_Spell; + types[CSMWorld::ColumnBase::Display_Static] = CSMWorld::UniversalId::Type_Referenceable; + types[CSMWorld::ColumnBase::Display_Texture] = CSMWorld::UniversalId::Type_Texture; + types[CSMWorld::ColumnBase::Display_Topic] = CSMWorld::UniversalId::Type_Topic; + types[CSMWorld::ColumnBase::Display_Weapon] = CSMWorld::UniversalId::Type_Referenceable; return types; } - typedef std::map::const_iterator ModelTypeConstIterator; + typedef std::map::const_iterator ModelTypeConstIterator; } const std::map @@ -65,7 +75,7 @@ std::vector CSMWorld::IdCompletionManager::getDis return types; } -CSMWorld::IdCompletionManager::IdCompletionManager(CSMWorld::Data &data) +CSMWorld::IdCompletionManager::IdCompletionManager(CSMWorld::Data& data) { generateCompleters(data); } @@ -84,14 +94,14 @@ std::shared_ptr CSMWorld::IdCompletionManager::getCompleter(CSMWorld return mCompleters[display]; } -void CSMWorld::IdCompletionManager::generateCompleters(CSMWorld::Data &data) +void CSMWorld::IdCompletionManager::generateCompleters(CSMWorld::Data& data) { ModelTypeConstIterator current = sCompleterModelTypes.begin(); ModelTypeConstIterator end = sCompleterModelTypes.end(); for (; current != end; ++current) { - QAbstractItemModel *model = data.getTableModel(current->second); - CSMWorld::IdTableBase *table = dynamic_cast(model); + QAbstractItemModel* model = data.getTableModel(current->second); + CSMWorld::IdTableBase* table = dynamic_cast(model); if (table != nullptr) { int idColumn = table->searchColumnIndex(CSMWorld::Columns::ColumnId_Id); @@ -103,11 +113,11 @@ void CSMWorld::IdCompletionManager::generateCompleters(CSMWorld::Data &data) completer->setCompletionRole(Qt::DisplayRole); completer->setCaseSensitivity(Qt::CaseInsensitive); - QAbstractItemView *popup = new CSVWidget::CompleterPopup(); + QAbstractItemView* popup = new CSVWidget::CompleterPopup(); completer->setPopup(popup); // The completer takes ownership of the popup completer->setMaxVisibleItems(10); - mCompleters[current->first] = completer; + mCompleters[current->first] = std::move(completer); } } } diff --git a/apps/opencs/model/world/idcompletionmanager.hpp b/apps/opencs/model/world/idcompletionmanager.hpp index e48360432aa..b1e9f732fd6 100644 --- a/apps/opencs/model/world/idcompletionmanager.hpp +++ b/apps/opencs/model/world/idcompletionmanager.hpp @@ -1,9 +1,9 @@ #ifndef CSM_WORLD_IDCOMPLETIONMANAGER_HPP #define CSM_WORLD_IDCOMPLETIONMANAGER_HPP -#include #include #include +#include #include "columnbase.hpp" #include "universalid.hpp" @@ -17,23 +17,23 @@ namespace CSMWorld /// \brief Creates and stores all ID completers class IdCompletionManager { - static const std::map sCompleterModelTypes; + static const std::map sCompleterModelTypes; - std::map > mCompleters; + std::map> mCompleters; - // Don't allow copying - IdCompletionManager(const IdCompletionManager &); - IdCompletionManager &operator = (const IdCompletionManager &); + // Don't allow copying + IdCompletionManager(const IdCompletionManager&); + IdCompletionManager& operator=(const IdCompletionManager&); - void generateCompleters(Data &data); + void generateCompleters(Data& data); - public: - static std::vector getDisplayTypes(); + public: + static std::vector getDisplayTypes(); - IdCompletionManager(Data &data); + IdCompletionManager(Data& data); - bool hasCompleterFor(ColumnBase::Display display) const; - std::shared_ptr getCompleter(ColumnBase::Display display); + bool hasCompleterFor(ColumnBase::Display display) const; + std::shared_ptr getCompleter(ColumnBase::Display display); }; } diff --git a/apps/opencs/model/world/idtable.cpp b/apps/opencs/model/world/idtable.cpp index 30fe6f639ac..5ed0c6f0e76 100644 --- a/apps/opencs/model/world/idtable.cpp +++ b/apps/opencs/model/world/idtable.cpp @@ -1,26 +1,35 @@ #include "idtable.hpp" +#include +#include + #include -#include #include #include #include +#include #include +#include + +#include +#include +#include +#include +#include -#include +#include +#include #include "collectionbase.hpp" #include "columnbase.hpp" -#include "landtexture.hpp" -CSMWorld::IdTable::IdTable (CollectionBase *idCollection, unsigned int features) -: IdTableBase (features), mIdCollection (idCollection) -{} - -CSMWorld::IdTable::~IdTable() -{} +CSMWorld::IdTable::IdTable(CollectionBase* idCollection, unsigned int features) + : IdTableBase(features) + , mIdCollection(idCollection) +{ +} -int CSMWorld::IdTable::rowCount (const QModelIndex & parent) const +int CSMWorld::IdTable::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; @@ -28,7 +37,7 @@ int CSMWorld::IdTable::rowCount (const QModelIndex & parent) const return mIdCollection->getSize(); } -int CSMWorld::IdTable::columnCount (const QModelIndex & parent) const +int CSMWorld::IdTable::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; @@ -36,54 +45,54 @@ int CSMWorld::IdTable::columnCount (const QModelIndex & parent) const return mIdCollection->getColumns(); } -QVariant CSMWorld::IdTable::data (const QModelIndex & index, int role) const +QVariant CSMWorld::IdTable::data(const QModelIndex& index, int role) const { if (index.row() < 0 || index.column() < 0) return QVariant(); - if (role==ColumnBase::Role_Display) + if (role == ColumnBase::Role_Display) return QVariant(mIdCollection->getColumn(index.column()).mDisplayType); - if (role==ColumnBase::Role_ColumnId) - return QVariant (getColumnId (index.column())); + if (role == ColumnBase::Role_ColumnId) + return QVariant(getColumnId(index.column())); - if ((role!=Qt::DisplayRole && role!=Qt::EditRole)) + if ((role != Qt::DisplayRole && role != Qt::EditRole)) return QVariant(); - if (role==Qt::EditRole && !mIdCollection->getColumn (index.column()).isEditable()) + if (role == Qt::EditRole && !mIdCollection->getColumn(index.column()).isEditable()) return QVariant(); - return mIdCollection->getData (index.row(), index.column()); + return mIdCollection->getData(index.row(), index.column()); } -QVariant CSMWorld::IdTable::headerData (int section, Qt::Orientation orientation, int role) const +QVariant CSMWorld::IdTable::headerData(int section, Qt::Orientation orientation, int role) const { - if (orientation==Qt::Vertical) + if (orientation == Qt::Vertical) return QVariant(); if (orientation != Qt::Horizontal) throw std::logic_error("Unknown header orientation specified"); - if (role==Qt::DisplayRole) - return tr (mIdCollection->getColumn (section).getTitle().c_str()); + if (role == Qt::DisplayRole) + return tr(mIdCollection->getColumn(section).getTitle().c_str()); - if (role==ColumnBase::Role_Flags) - return mIdCollection->getColumn (section).mFlags; + if (role == ColumnBase::Role_Flags) + return mIdCollection->getColumn(section).mFlags; - if (role==ColumnBase::Role_Display) - return mIdCollection->getColumn (section).mDisplayType; + if (role == ColumnBase::Role_Display) + return mIdCollection->getColumn(section).mDisplayType; - if (role==ColumnBase::Role_ColumnId) - return getColumnId (section); + if (role == ColumnBase::Role_ColumnId) + return getColumnId(section); return QVariant(); } -bool CSMWorld::IdTable::setData (const QModelIndex &index, const QVariant &value, int role) +bool CSMWorld::IdTable::setData(const QModelIndex& index, const QVariant& value, int role) { - if (mIdCollection->getColumn (index.column()).isEditable() && role==Qt::EditRole) + if (mIdCollection->getColumn(index.column()).isEditable() && role == Qt::EditRole) { - mIdCollection->setData (index.row(), index.column(), value); + mIdCollection->setData(index.row(), index.column(), value); int stateColumn = searchColumnIndex(Columns::ColumnId_Modification); if (stateColumn != -1) @@ -93,10 +102,10 @@ bool CSMWorld::IdTable::setData (const QModelIndex &index, const QVariant &value // modifying the state column can modify other values. we need to tell // views that the whole row has changed. - emit dataChanged(this->index(index.row(), 0), - this->index(index.row(), columnCount(index.parent()) - 1)); - - } else + emit dataChanged( + this->index(index.row(), 0), this->index(index.row(), columnCount(index.parent()) - 1)); + } + else { emit dataChanged(index, index); @@ -104,7 +113,8 @@ bool CSMWorld::IdTable::setData (const QModelIndex &index, const QVariant &value QModelIndex stateIndex = this->index(index.row(), stateColumn); emit dataChanged(stateIndex, stateIndex); } - } else + } + else emit dataChanged(index, index); return true; @@ -113,73 +123,83 @@ bool CSMWorld::IdTable::setData (const QModelIndex &index, const QVariant &value return false; } -Qt::ItemFlags CSMWorld::IdTable::flags (const QModelIndex & index) const +Qt::ItemFlags CSMWorld::IdTable::flags(const QModelIndex& index) const { if (!index.isValid()) return Qt::ItemFlags(); Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; - if (mIdCollection->getColumn (index.column()).isUserEditable()) + if (mIdCollection->getColumn(index.column()).isUserEditable()) flags |= Qt::ItemIsEditable; + int blockedColumn = searchColumnIndex(Columns::ColumnId_Blocked); + if (blockedColumn != -1 && blockedColumn != index.column()) + { + bool isBlocked = mIdCollection->getData(index.row(), blockedColumn).toInt(); + if (isBlocked) + flags = Qt::ItemIsSelectable; // not enabled (to grey out) + } + return flags; } -bool CSMWorld::IdTable::removeRows (int row, int count, const QModelIndex& parent) +bool CSMWorld::IdTable::removeRows(int row, int count, const QModelIndex& parent) { if (parent.isValid()) return false; - beginRemoveRows (parent, row, row+count-1); + beginRemoveRows(parent, row, row + count - 1); - mIdCollection->removeRows (row, count); + mIdCollection->removeRows(row, count); endRemoveRows(); return true; } -QModelIndex CSMWorld::IdTable::index (int row, int column, const QModelIndex& parent) const +QModelIndex CSMWorld::IdTable::index(int row, int column, const QModelIndex& parent) const { if (parent.isValid()) return QModelIndex(); - if (row<0 || row>=mIdCollection->getSize()) + if (row < 0 || row >= mIdCollection->getSize()) return QModelIndex(); - if (column<0 || column>=mIdCollection->getColumns()) + if (column < 0 || column >= mIdCollection->getColumns()) return QModelIndex(); - return createIndex (row, column); + return createIndex(row, column); } -QModelIndex CSMWorld::IdTable::parent (const QModelIndex& index) const +QModelIndex CSMWorld::IdTable::parent(const QModelIndex& index) const { return QModelIndex(); } -void CSMWorld::IdTable::addRecord (const std::string& id, UniversalId::Type type) +void CSMWorld::IdTable::addRecord(const std::string& id, UniversalId::Type type) { - int index = mIdCollection->getAppendIndex (id, type); + ESM::RefId refId = ESM::RefId::stringRefId(id); + int index = mIdCollection->getAppendIndex(refId, type); - beginInsertRows (QModelIndex(), index, index); + beginInsertRows(QModelIndex(), index, index); - mIdCollection->appendBlankRecord (id, type); + mIdCollection->appendBlankRecord(refId, type); endInsertRows(); } -void CSMWorld::IdTable::addRecordWithData (const std::string& id, - const std::map& data, UniversalId::Type type) +void CSMWorld::IdTable::addRecordWithData( + const std::string& id, const std::map& data, UniversalId::Type type) { - int index = mIdCollection->getAppendIndex (id, type); + ESM::RefId refId = ESM::RefId::stringRefId(id); + int index = mIdCollection->getAppendIndex(refId, type); - beginInsertRows (QModelIndex(), index, index); + beginInsertRows(QModelIndex(), index, index); - mIdCollection->appendBlankRecord (id, type); + mIdCollection->appendBlankRecord(refId, type); - for (std::map::const_iterator iter (data.begin()); iter!=data.end(); ++iter) + for (std::map::const_iterator iter(data.begin()); iter != data.end(); ++iter) { mIdCollection->setData(index, iter->first, iter->second); } @@ -187,22 +207,22 @@ void CSMWorld::IdTable::addRecordWithData (const std::string& id, endInsertRows(); } -void CSMWorld::IdTable::cloneRecord(const std::string& origin, - const std::string& destination, - CSMWorld::UniversalId::Type type) +void CSMWorld::IdTable::cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, CSMWorld::UniversalId::Type type) { - int index = mIdCollection->getAppendIndex (destination); + int index = mIdCollection->getAppendIndex(destination, type); - beginInsertRows (QModelIndex(), index, index); + beginInsertRows(QModelIndex(), index, index); mIdCollection->cloneRecord(origin, destination, type); endInsertRows(); } bool CSMWorld::IdTable::touchRecord(const std::string& id) { - bool changed = mIdCollection->touchRecord(id); + ESM::RefId refId = ESM::RefId::stringRefId(id); + bool changed = mIdCollection->touchRecord(refId); - int row = mIdCollection->getIndex(id); + int row = mIdCollection->getIndex(refId); int column = mIdCollection->searchColumnIndex(Columns::ColumnId_RecordType); if (changed && column != -1) { @@ -215,104 +235,115 @@ bool CSMWorld::IdTable::touchRecord(const std::string& id) std::string CSMWorld::IdTable::getId(int row) const { - return mIdCollection->getId(row); + return mIdCollection->getId(row).getRefIdString(); } -///This method can return only indexes to the top level table cells -QModelIndex CSMWorld::IdTable::getModelIndex (const std::string& id, int column) const +/// This method can return only indexes to the top level table cells +QModelIndex CSMWorld::IdTable::getModelIndex(const std::string& id, int column) const { - int row = mIdCollection->searchId (id); + const int row = mIdCollection->searchId(ESM::RefId::stringRefId(id)); + if (row != -1) return index(row, column); return QModelIndex(); } -void CSMWorld::IdTable::setRecord (const std::string& id, const RecordBase& record, CSMWorld::UniversalId::Type type) +void CSMWorld::IdTable::setRecord( + const std::string& id, std::unique_ptr record, CSMWorld::UniversalId::Type type) { - int index = mIdCollection->searchId (id); + const ESM::RefId refId = ESM::RefId::stringRefId(id); + const int index = mIdCollection->searchId(refId); - if (index==-1) + if (index == -1) { - index = mIdCollection->getAppendIndex (id, type); + // For info records, appendRecord may use a different index than the one returned by + // getAppendIndex (because of prev/next links). This can result in the display not + // updating correctly after an undo + // + // Use an alternative method to get the correct index. For non-Info records the + // record pointer is ignored and internally calls getAppendIndex. + const int index2 = mIdCollection->getInsertIndex(refId, type, record.get()); - beginInsertRows (QModelIndex(), index, index); + beginInsertRows(QModelIndex(), index2, index2); - mIdCollection->appendRecord (record, type); + mIdCollection->appendRecord(std::move(record), type); endInsertRows(); } else { - mIdCollection->replace (index, record); - emit dataChanged (CSMWorld::IdTable::index (index, 0), - CSMWorld::IdTable::index (index, mIdCollection->getColumns()-1)); + mIdCollection->replace(index, std::move(record)); + emit dataChanged( + CSMWorld::IdTable::index(index, 0), CSMWorld::IdTable::index(index, mIdCollection->getColumns() - 1)); } } -const CSMWorld::RecordBase& CSMWorld::IdTable::getRecord (const std::string& id) const +const CSMWorld::RecordBase& CSMWorld::IdTable::getRecord(const std::string& id) const { - return mIdCollection->getRecord (id); + return mIdCollection->getRecord(ESM::RefId::stringRefId(id)); } -int CSMWorld::IdTable::searchColumnIndex (Columns::ColumnId id) const +int CSMWorld::IdTable::searchColumnIndex(Columns::ColumnId id) const { - return mIdCollection->searchColumnIndex (id); + return mIdCollection->searchColumnIndex(id); } -int CSMWorld::IdTable::findColumnIndex (Columns::ColumnId id) const +int CSMWorld::IdTable::findColumnIndex(Columns::ColumnId id) const { - return mIdCollection->findColumnIndex (id); + return mIdCollection->findColumnIndex(id); } -void CSMWorld::IdTable::reorderRows (int baseIndex, const std::vector& newOrder) +void CSMWorld::IdTable::reorderRows(int baseIndex, const std::vector& newOrder) { - if (!newOrder.empty()) - if (mIdCollection->reorderRows (baseIndex, newOrder)) - emit dataChanged (index (baseIndex, 0), - index (baseIndex+static_cast(newOrder.size())-1, mIdCollection->getColumns()-1)); + if (newOrder.empty()) + return; + if (!mIdCollection->reorderRows(baseIndex, newOrder)) + return; + emit dataChanged( + index(baseIndex, 0), index(baseIndex + static_cast(newOrder.size()) - 1, mIdCollection->getColumns() - 1)); } -std::pair CSMWorld::IdTable::view (int row) const +std::pair CSMWorld::IdTable::view(int row) const { std::string id; std::string hint; if (getFeatures() & Feature_ViewCell) { - int cellColumn = mIdCollection->searchColumnIndex (Columns::ColumnId_Cell); - int idColumn = mIdCollection->searchColumnIndex (Columns::ColumnId_Id); + int cellColumn = mIdCollection->searchColumnIndex(Columns::ColumnId_Cell); + int idColumn = mIdCollection->searchColumnIndex(Columns::ColumnId_Id); - if (cellColumn!=-1 && idColumn!=-1) + if (cellColumn != -1 && idColumn != -1) { - id = mIdCollection->getData (row, cellColumn).toString().toUtf8().constData(); - hint = "r:" + std::string (mIdCollection->getData (row, idColumn).toString().toUtf8().constData()); + id = mIdCollection->getData(row, cellColumn).toString().toUtf8().constData(); + hint = "r:" + std::string(mIdCollection->getData(row, idColumn).toString().toUtf8().constData()); } } else if (getFeatures() & Feature_ViewId) { - int column = mIdCollection->searchColumnIndex (Columns::ColumnId_Id); + int column = mIdCollection->searchColumnIndex(Columns::ColumnId_Id); - if (column!=-1) + if (column != -1) { - id = mIdCollection->getData (row, column).toString().toUtf8().constData(); + id = mIdCollection->getData(row, column).toString().toUtf8().constData(); hint = "c:" + id; } } if (id.empty()) - return std::make_pair (UniversalId::Type_None, ""); + return std::make_pair(UniversalId::Type_None, ""); - if (id[0]=='#') - id = ESM::CellId::sDefaultWorldspace; + if (id[0] == '#') + id = ESM::Cell::sDefaultWorldspaceId.getValue(); - return std::make_pair (UniversalId (UniversalId::Type_Scene, id), hint); + return std::make_pair(UniversalId(UniversalId::Type_Scene, id), hint); } -///For top level data/columns -bool CSMWorld::IdTable::isDeleted (const std::string& id) const +/// For top level data/columns +bool CSMWorld::IdTable::isDeleted(const std::string& id) const { - return getRecord (id).isDeleted(); + return getRecord(id).isDeleted(); } int CSMWorld::IdTable::getColumnId(int column) const @@ -320,7 +351,7 @@ int CSMWorld::IdTable::getColumnId(int column) const return mIdCollection->getColumn(column).getId(); } -CSMWorld::CollectionBase *CSMWorld::IdTable::idCollection() const +CSMWorld::CollectionBase* CSMWorld::IdTable::idCollection() const { return mIdCollection; } @@ -330,66 +361,8 @@ CSMWorld::LandTextureIdTable::LandTextureIdTable(CollectionBase* idCollection, u { } -CSMWorld::LandTextureIdTable::ImportResults CSMWorld::LandTextureIdTable::importTextures(const std::vector& ids) +const CSMWorld::Record* CSMWorld::LandTextureIdTable::searchRecord( + std::uint16_t index, int plugin) const { - ImportResults results; - - // Map existing textures to ids - std::map reverseLookupMap; - for (int i = 0; i < idCollection()->getSize(); ++i) - { - auto& record = static_cast&>(idCollection()->getRecord(i)); - std::string texture = record.get().mTexture; - std::transform(texture.begin(), texture.end(), texture.begin(), tolower); - if (record.isModified()) - reverseLookupMap.emplace(texture, idCollection()->getId(i)); - } - - for (const std::string& id : ids) - { - int plugin, index; - - LandTexture::parseUniqueRecordId(id, plugin, index); - int oldRow = idCollection()->searchId(id); - - // If it does not exist or it is in the current plugin, it can be skipped. - if (oldRow < 0 || plugin == 0) - { - results.recordMapping.emplace_back(id, id); - continue; - } - - // Look for a pre-existing record - auto& record = static_cast&>(idCollection()->getRecord(oldRow)); - std::string texture = record.get().mTexture; - std::transform(texture.begin(), texture.end(), texture.begin(), tolower); - auto searchIt = reverseLookupMap.find(texture); - if (searchIt != reverseLookupMap.end()) - { - results.recordMapping.emplace_back(id, searchIt->second); - continue; - } - - // Iterate until an unused index or found, or the index has completely wrapped around. - int startIndex = index; - do { - std::string newId = LandTexture::createUniqueRecordId(0, index); - int newRow = idCollection()->searchId(newId); - - if (newRow < 0) - { - // Id not taken, clone it - cloneRecord(id, newId, UniversalId::Type_LandTexture); - results.createdRecords.push_back(newId); - results.recordMapping.emplace_back(id, newId); - reverseLookupMap.emplace(texture, newId); - break; - } - - const size_t MaxIndex = std::numeric_limits::max() - 1; - index = (index + 1) % MaxIndex; - } while (index != startIndex); - } - - return results; + return static_cast*>(idCollection())->searchRecord(index, plugin); } diff --git a/apps/opencs/model/world/idtable.hpp b/apps/opencs/model/world/idtable.hpp index 6b7b8d3182e..9247c6c1e83 100644 --- a/apps/opencs/model/world/idtable.hpp +++ b/apps/opencs/model/world/idtable.hpp @@ -1,124 +1,121 @@ #ifndef CSM_WOLRD_IDTABLE_H #define CSM_WOLRD_IDTABLE_H +#include +#include + +#include +#include +#include +#include #include +#include "columns.hpp" #include "idtablebase.hpp" #include "universalid.hpp" -#include "columns.hpp" + +class QObject; + +namespace ESM +{ + struct LandTexture; +} namespace CSMWorld { class CollectionBase; struct RecordBase; + template + struct Record; class IdTable : public IdTableBase { - Q_OBJECT + Q_OBJECT - private: + private: + CollectionBase* mIdCollection; - CollectionBase *mIdCollection; + // not implemented + IdTable(const IdTable&); + IdTable& operator=(const IdTable&); - // not implemented - IdTable (const IdTable&); - IdTable& operator= (const IdTable&); + public: + IdTable(CollectionBase* idCollection, unsigned int features = 0); + ///< The ownership of \a idCollection is not transferred. - public: + virtual ~IdTable() = default; - IdTable (CollectionBase *idCollection, unsigned int features = 0); - ///< The ownership of \a idCollection is not transferred. + int rowCount(const QModelIndex& parent = QModelIndex()) const override; - virtual ~IdTable(); + int columnCount(const QModelIndex& parent = QModelIndex()) const override; - int rowCount (const QModelIndex & parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - int columnCount (const QModelIndex & parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; - QVariant headerData (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; - bool setData ( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; - Qt::ItemFlags flags (const QModelIndex & index) const override; + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; - bool removeRows (int row, int count, const QModelIndex& parent = QModelIndex()) override; + QModelIndex parent(const QModelIndex& index) const override; - QModelIndex index (int row, int column, const QModelIndex& parent = QModelIndex()) const override; + void addRecord(const std::string& id, UniversalId::Type type = UniversalId::Type_None); + ///< \param type Will be ignored, unless the collection supports multiple record types - QModelIndex parent (const QModelIndex& index) const override; + void addRecordWithData(const std::string& id, const std::map& data, + UniversalId::Type type = UniversalId::Type_None); + ///< \param type Will be ignored, unless the collection supports multiple record types - void addRecord (const std::string& id, UniversalId::Type type = UniversalId::Type_None); - ///< \param type Will be ignored, unless the collection supports multiple record types + void cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, UniversalId::Type type = UniversalId::Type_None); - void addRecordWithData (const std::string& id, const std::map& data, - UniversalId::Type type = UniversalId::Type_None); - ///< \param type Will be ignored, unless the collection supports multiple record types + bool touchRecord(const std::string& id); + ///< Will change the record state to modified, if it is not already. - void cloneRecord(const std::string& origin, - const std::string& destination, - UniversalId::Type type = UniversalId::Type_None); + std::string getId(int row) const; - bool touchRecord(const std::string& id); - ///< Will change the record state to modified, if it is not already. + QModelIndex getModelIndex(const std::string& id, int column) const override; - std::string getId(int row) const; + void setRecord( + const std::string& id, std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None); + ///< Add record or overwrite existing record. - QModelIndex getModelIndex (const std::string& id, int column) const override; + const RecordBase& getRecord(const std::string& id) const; - void setRecord (const std::string& id, const RecordBase& record, - UniversalId::Type type = UniversalId::Type_None); - ///< Add record or overwrite existing record. + int searchColumnIndex(Columns::ColumnId id) const override; + ///< Return index of column with the given \a id. If no such column exists, -1 is returned. - const RecordBase& getRecord (const std::string& id) const; + int findColumnIndex(Columns::ColumnId id) const override; + ///< Return index of column with the given \a id. If no such column exists, an exception is + /// thrown. - int searchColumnIndex (Columns::ColumnId id) const override; - ///< Return index of column with the given \a id. If no such column exists, -1 is returned. + void reorderRows(int baseIndex, const std::vector& newOrder); + ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices + /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). - int findColumnIndex (Columns::ColumnId id) const override; - ///< Return index of column with the given \a id. If no such column exists, an exception is - /// thrown. + std::pair view(int row) const override; + ///< Return the UniversalId and the hint for viewing \a row. If viewing is not + /// supported by this table, return (UniversalId::Type_None, ""). - void reorderRows (int baseIndex, const std::vector& newOrder); - ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices - /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). + /// Is \a id flagged as deleted? + bool isDeleted(const std::string& id) const override; - std::pair view (int row) const override; - ///< Return the UniversalId and the hint for viewing \a row. If viewing is not - /// supported by this table, return (UniversalId::Type_None, ""). + int getColumnId(int column) const override; - /// Is \a id flagged as deleted? - bool isDeleted (const std::string& id) const override; - - int getColumnId(int column) const override; - - protected: - - virtual CollectionBase *idCollection() const; + protected: + virtual CollectionBase* idCollection() const; }; - /// An IdTable customized to handle the more unique needs of LandTextureId's which behave - /// differently from other records. The major difference is that base records cannot be - /// modified. class LandTextureIdTable : public IdTable { - public: - - struct ImportResults - { - using StringPair = std::pair; - - /// The newly added records - std::vector createdRecords; - /// The 1st string is the original id, the 2nd is the mapped id - std::vector recordMapping; - }; - - LandTextureIdTable(CollectionBase* idCollection, unsigned int features=0); + public: + LandTextureIdTable(CollectionBase* idCollection, unsigned int features = 0); - /// Finds and maps/recreates the specified ids. - ImportResults importTextures(const std::vector& ids); + const CSMWorld::Record* searchRecord(std::uint16_t index, int plugin) const; }; } diff --git a/apps/opencs/model/world/idtablebase.cpp b/apps/opencs/model/world/idtablebase.cpp index 274446b796b..fd523d1ec78 100644 --- a/apps/opencs/model/world/idtablebase.cpp +++ b/apps/opencs/model/world/idtablebase.cpp @@ -1,6 +1,9 @@ #include "idtablebase.hpp" -CSMWorld::IdTableBase::IdTableBase (unsigned int features) : mFeatures (features) {} +CSMWorld::IdTableBase::IdTableBase(unsigned int features) + : mFeatures(features) +{ +} unsigned int CSMWorld::IdTableBase::getFeatures() const { diff --git a/apps/opencs/model/world/idtablebase.hpp b/apps/opencs/model/world/idtablebase.hpp index 8b82f984b9d..af84ff480d4 100644 --- a/apps/opencs/model/world/idtablebase.hpp +++ b/apps/opencs/model/world/idtablebase.hpp @@ -2,6 +2,10 @@ #define CSM_WOLRD_IDTABLEBASE_H #include +#include + +#include +#include #include "columns.hpp" @@ -11,60 +15,57 @@ namespace CSMWorld class IdTableBase : public QAbstractItemModel { - Q_OBJECT - - public: - - enum Features - { - Feature_ReorderWithinTopic = 1, - - /// Use ID column to generate view request (ID is transformed into - /// worldspace and original ID is passed as hint with c: prefix). - Feature_ViewId = 2, + Q_OBJECT - /// Use cell column to generate view request (cell ID is transformed - /// into worldspace and record ID is passed as hint with r: prefix). - Feature_ViewCell = 4, + public: + enum Features + { + Feature_ReorderWithinTopic = 1, - Feature_View = Feature_ViewId | Feature_ViewCell, + /// Use ID column to generate view request (ID is transformed into + /// worldspace and original ID is passed as hint with c: prefix). + Feature_ViewId = 2, - Feature_Preview = 8, + /// Use cell column to generate view request (cell ID is transformed + /// into worldspace and record ID is passed as hint with r: prefix). + Feature_ViewCell = 4, - /// Table can not be modified through ordinary means. - Feature_Constant = 16, + Feature_View = Feature_ViewId | Feature_ViewCell, - Feature_AllowTouch = 32 - }; + Feature_Preview = 8, - private: + /// Table can not be modified through ordinary means. + Feature_Constant = 16, - unsigned int mFeatures; + Feature_AllowTouch = 32 + }; - public: + private: + unsigned int mFeatures; - IdTableBase (unsigned int features); + public: + IdTableBase(unsigned int features); - virtual QModelIndex getModelIndex (const std::string& id, int column) const = 0; + virtual QModelIndex getModelIndex(const std::string& id, int column) const = 0; - /// Return index of column with the given \a id. If no such column exists, -1 is - /// returned. - virtual int searchColumnIndex (Columns::ColumnId id) const = 0; + /// Return index of column with the given \a id. If no such column exists, -1 is + /// returned. + virtual int searchColumnIndex(Columns::ColumnId id) const = 0; - /// Return index of column with the given \a id. If no such column exists, an - /// exception is thrown. - virtual int findColumnIndex (Columns::ColumnId id) const = 0; + /// Return index of column with the given \a id. If no such column exists, an + /// exception is thrown. + virtual int findColumnIndex(Columns::ColumnId id) const = 0; - /// Return the UniversalId and the hint for viewing \a row. If viewing is not - /// supported by this table, return (UniversalId::Type_None, ""). - virtual std::pair view (int row) const = 0; + /// Return the UniversalId and the hint for viewing \a row. If viewing is not + /// supported by this table, return (UniversalId::Type_None, ""). + virtual std::pair view(int row) const = 0; - /// Is \a id flagged as deleted? - virtual bool isDeleted (const std::string& id) const = 0; + /// Is \a id flagged as deleted? + virtual bool isDeleted(const std::string& id) const = 0; - virtual int getColumnId (int column) const = 0; + virtual int getColumnId(int column) const = 0; - unsigned int getFeatures() const; + unsigned int getFeatures() const; }; } diff --git a/apps/opencs/model/world/idtableproxymodel.cpp b/apps/opencs/model/world/idtableproxymodel.cpp index 3e24f8d12e1..c03b4ea30a5 100644 --- a/apps/opencs/model/world/idtableproxymodel.cpp +++ b/apps/opencs/model/world/idtableproxymodel.cpp @@ -1,12 +1,25 @@ #include "idtableproxymodel.hpp" +#include +#include +#include +#include + +#include +#include #include +#include +#include + +#include "columnbase.hpp" #include "idtablebase.hpp" +class QObject; + namespace { - std::string getEnumValue(const std::vector> &values, int index) + std::string getEnumValue(const std::vector>& values, int index) { if (index < 0 || index >= static_cast(values.size())) { @@ -24,14 +37,13 @@ void CSMWorld::IdTableProxyModel::updateColumnMap() if (mFilter) { std::vector columns = mFilter->getReferencedColumns(); - for (std::vector::const_iterator iter (columns.begin()); iter!=columns.end(); ++iter) - mColumnMap.insert (std::make_pair (*iter, - mSourceModel->searchColumnIndex (static_cast (*iter)))); + for (std::vector::const_iterator iter(columns.begin()); iter != columns.end(); ++iter) + mColumnMap.insert(std::make_pair( + *iter, mSourceModel->searchColumnIndex(static_cast(*iter)))); } } -bool CSMWorld::IdTableProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) - const +bool CSMWorld::IdTableProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { Q_ASSERT(mSourceModel != nullptr); @@ -46,51 +58,49 @@ bool CSMWorld::IdTableProxyModel::filterAcceptsRow (int sourceRow, const QModelI if (!mFilter) return true; - return mFilter->test (*mSourceModel, sourceRow, mColumnMap); + return mFilter->test(*mSourceModel, sourceRow, mColumnMap); } -CSMWorld::IdTableProxyModel::IdTableProxyModel (QObject *parent) - : QSortFilterProxyModel (parent), - mSourceModel(nullptr) +CSMWorld::IdTableProxyModel::IdTableProxyModel(QObject* parent) + : QSortFilterProxyModel(parent) + , mFilterTimer{ new QTimer(this) } + , mSourceModel(nullptr) { - setSortCaseSensitivity (Qt::CaseInsensitive); + setSortCaseSensitivity(Qt::CaseInsensitive); + + mFilterTimer->setSingleShot(true); + int intervalSetting = CSMPrefs::State::get()["ID Tables"]["filter-delay"].toInt(); + mFilterTimer->setInterval(intervalSetting); + + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, + [this](const CSMPrefs::Setting* setting) { this->settingChanged(setting); }); + connect(mFilterTimer.get(), &QTimer::timeout, this, [this]() { this->timerTimeout(); }); } -QModelIndex CSMWorld::IdTableProxyModel::getModelIndex (const std::string& id, int column) const +QModelIndex CSMWorld::IdTableProxyModel::getModelIndex(const std::string& id, int column) const { Q_ASSERT(mSourceModel != nullptr); - return mapFromSource(mSourceModel->getModelIndex (id, column)); + return mapFromSource(mSourceModel->getModelIndex(id, column)); } -void CSMWorld::IdTableProxyModel::setSourceModel(QAbstractItemModel *model) +void CSMWorld::IdTableProxyModel::setSourceModel(QAbstractItemModel* model) { QSortFilterProxyModel::setSourceModel(model); - mSourceModel = dynamic_cast(sourceModel()); - connect(mSourceModel, - SIGNAL(rowsInserted(const QModelIndex &, int, int)), - this, - SLOT(sourceRowsInserted(const QModelIndex &, int, int))); - connect(mSourceModel, - SIGNAL(rowsRemoved(const QModelIndex &, int, int)), - this, - SLOT(sourceRowsRemoved(const QModelIndex &, int, int))); - connect(mSourceModel, - SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &)), - this, - SLOT(sourceDataChanged(const QModelIndex &, const QModelIndex &))); + mSourceModel = dynamic_cast(sourceModel()); + connect(mSourceModel, &IdTableBase::rowsInserted, this, &IdTableProxyModel::sourceRowsInserted); + connect(mSourceModel, &IdTableBase::rowsRemoved, this, &IdTableProxyModel::sourceRowsRemoved); + connect(mSourceModel, &IdTableBase::dataChanged, this, &IdTableProxyModel::sourceDataChanged); } -void CSMWorld::IdTableProxyModel::setFilter (const std::shared_ptr& filter) +void CSMWorld::IdTableProxyModel::setFilter(const std::shared_ptr& filter) { - beginResetModel(); - mFilter = filter; - updateColumnMap(); - endResetModel(); + mAwaitingFilter = filter; + mFilterTimer->start(); } -bool CSMWorld::IdTableProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +bool CSMWorld::IdTableProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { Columns::ColumnId id = static_cast(left.data(ColumnBase::Role_ColumnId).toInt()); EnumColumnCache::const_iterator valuesIt = mEnumColumnCache.find(id); @@ -121,11 +131,34 @@ QString CSMWorld::IdTableProxyModel::getRecordId(int sourceRow) const void CSMWorld::IdTableProxyModel::refreshFilter() { - updateColumnMap(); - invalidateFilter(); + if (mFilter) + { + updateColumnMap(); + invalidateFilter(); + } +} + +void CSMWorld::IdTableProxyModel::timerTimeout() +{ + if (mAwaitingFilter) + { + beginResetModel(); + mFilter = mAwaitingFilter; + updateColumnMap(); + endResetModel(); + mAwaitingFilter.reset(); + } +} + +void CSMWorld::IdTableProxyModel::settingChanged(const CSMPrefs::Setting* setting) +{ + if (*setting == "ID Tables/filter-delay") + { + mFilterTimer->setInterval(setting->toInt()); + } } -void CSMWorld::IdTableProxyModel::sourceRowsInserted(const QModelIndex &parent, int /*start*/, int end) +void CSMWorld::IdTableProxyModel::sourceRowsInserted(const QModelIndex& parent, int /*start*/, int end) { refreshFilter(); if (!parent.isValid()) @@ -134,12 +167,12 @@ void CSMWorld::IdTableProxyModel::sourceRowsInserted(const QModelIndex &parent, } } -void CSMWorld::IdTableProxyModel::sourceRowsRemoved(const QModelIndex &/*parent*/, int /*start*/, int /*end*/) +void CSMWorld::IdTableProxyModel::sourceRowsRemoved(const QModelIndex& /*parent*/, int /*start*/, int /*end*/) { refreshFilter(); } -void CSMWorld::IdTableProxyModel::sourceDataChanged(const QModelIndex &/*topLeft*/, const QModelIndex &/*bottomRight*/) +void CSMWorld::IdTableProxyModel::sourceDataChanged(const QModelIndex& /*topLeft*/, const QModelIndex& /*bottomRight*/) { refreshFilter(); } diff --git a/apps/opencs/model/world/idtableproxymodel.hpp b/apps/opencs/model/world/idtableproxymodel.hpp index 7e0563834a9..b7d38f5a2d0 100644 --- a/apps/opencs/model/world/idtableproxymodel.hpp +++ b/apps/opencs/model/world/idtableproxymodel.hpp @@ -1,69 +1,85 @@ #ifndef CSM_WOLRD_IDTABLEPROXYMODEL_H #define CSM_WOLRD_IDTABLEPROXYMODEL_H -#include - #include +#include +#include +#include +#include +#include #include +#include +#include -#include "../filter/node.hpp" +#include "../prefs/state.hpp" #include "columns.hpp" +class QObject; + +namespace CSMFilter +{ + class Node; +} + namespace CSMWorld { + class IdTableBase; + class IdTableProxyModel : public QSortFilterProxyModel { - Q_OBJECT - - std::shared_ptr mFilter; - std::map mColumnMap; // column ID, column index in this model (or -1) - - // Cache of enum values for enum columns (e.g. Modified, Record Type). - // Used to speed up comparisons during the sort by such columns. - typedef std::map> > EnumColumnCache; - mutable EnumColumnCache mEnumColumnCache; + Q_OBJECT - protected: + std::shared_ptr mFilter; + std::unique_ptr mFilterTimer; + std::shared_ptr mAwaitingFilter; + std::map mColumnMap; // column ID, column index in this model (or -1) - IdTableBase *mSourceModel; + // Cache of enum values for enum columns (e.g. Modified, Record Type). + // Used to speed up comparisons during the sort by such columns. + typedef std::map>> EnumColumnCache; + mutable EnumColumnCache mEnumColumnCache; - private: + protected: + IdTableBase* mSourceModel; - void updateColumnMap(); + private: + void updateColumnMap(); - public: + public: + IdTableProxyModel(QObject* parent = nullptr); - IdTableProxyModel (QObject *parent = nullptr); + virtual QModelIndex getModelIndex(const std::string& id, int column) const; - virtual QModelIndex getModelIndex (const std::string& id, int column) const; + void setSourceModel(QAbstractItemModel* model) override; - void setSourceModel(QAbstractItemModel *model) override; + void setFilter(const std::shared_ptr& filter); - void setFilter (const std::shared_ptr& filter); + void refreshFilter(); - void refreshFilter(); + protected: + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; - protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; - bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + QString getRecordId(int sourceRow) const; - bool filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const override; + protected slots: - QString getRecordId(int sourceRow) const; + virtual void sourceRowsInserted(const QModelIndex& parent, int start, int end); - protected slots: + virtual void sourceRowsRemoved(const QModelIndex& parent, int start, int end); - virtual void sourceRowsInserted(const QModelIndex &parent, int start, int end); + virtual void sourceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - virtual void sourceRowsRemoved(const QModelIndex &parent, int start, int end); + void timerTimeout(); - virtual void sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void settingChanged(const CSMPrefs::Setting* setting); - signals: + signals: - void rowAdded(const std::string &id); + void rowAdded(const std::string& id); }; } diff --git a/apps/opencs/model/world/idtree.cpp b/apps/opencs/model/world/idtree.cpp index 1e3398bbb28..da5c415ced6 100644 --- a/apps/opencs/model/world/idtree.cpp +++ b/apps/opencs/model/world/idtree.cpp @@ -3,18 +3,22 @@ #include "nestedtablewrapper.hpp" #include "collectionbase.hpp" -#include "nestedcollection.hpp" #include "columnbase.hpp" +#include "nestedcollection.hpp" -// NOTE: parent class still needs idCollection -CSMWorld::IdTree::IdTree (NestedCollection *nestedCollection, CollectionBase *idCollection, unsigned int features) -: IdTable (idCollection, features), mNestedCollection (nestedCollection) -{} +#include +#include -CSMWorld::IdTree::~IdTree() -{} +#include -int CSMWorld::IdTree::rowCount (const QModelIndex & parent) const +// NOTE: parent class still needs idCollection +CSMWorld::IdTree::IdTree(NestedCollection* nestedCollection, CollectionBase* idCollection, unsigned int features) + : IdTable(idCollection, features) + , mNestedCollection(nestedCollection) +{ +} + +int CSMWorld::IdTree::rowCount(const QModelIndex& parent) const { if (hasChildren(parent)) return mNestedCollection->getNestedRowsCount(parent.row(), parent.column()); @@ -22,7 +26,7 @@ int CSMWorld::IdTree::rowCount (const QModelIndex & parent) const return IdTable::rowCount(parent); } -int CSMWorld::IdTree::columnCount (const QModelIndex & parent) const +int CSMWorld::IdTree::columnCount(const QModelIndex& parent) const { if (hasChildren(parent)) return mNestedCollection->getNestedColumnsCount(parent.row(), parent.column()); @@ -30,15 +34,15 @@ int CSMWorld::IdTree::columnCount (const QModelIndex & parent) const return IdTable::columnCount(parent); } -QVariant CSMWorld::IdTree::data (const QModelIndex & index, int role) const +QVariant CSMWorld::IdTree::data(const QModelIndex& index, int role) const { - if (!index.isValid()) - return QVariant(); + if (!index.isValid()) + return QVariant(); if (index.internalId() != 0) { std::pair parentAddress(unfoldIndexAddress(index.internalId())); - const NestableColumn *parentColumn = mNestedCollection->getNestableColumn(parentAddress.second); + const NestableColumn* parentColumn = mNestedCollection->getNestableColumn(parentAddress.second); if (role == ColumnBase::Role_Display) return parentColumn->nestedColumn(index.column()).mDisplayType; @@ -52,8 +56,7 @@ QVariant CSMWorld::IdTree::data (const QModelIndex & index, int role) const if (role != Qt::DisplayRole && role != Qt::EditRole) return QVariant(); - return mNestedCollection->getNestedData(parentAddress.first, - parentAddress.second, index.row(), index.column()); + return mNestedCollection->getNestedData(parentAddress.first, parentAddress.second, index.row(), index.column()); } else { @@ -66,35 +69,36 @@ QVariant CSMWorld::IdTree::nestedHeaderData(int section, int subSection, Qt::Ori if (section < 0 || section >= idCollection()->getColumns()) return QVariant(); - const NestableColumn *parentColumn = mNestedCollection->getNestableColumn(section); + const NestableColumn* parentColumn = mNestedCollection->getNestableColumn(section); - if (orientation==Qt::Vertical) + if (orientation == Qt::Vertical) return QVariant(); - if (role==Qt::DisplayRole) + if (role == Qt::DisplayRole) return tr(parentColumn->nestedColumn(subSection).getTitle().c_str()); - if (role==ColumnBase::Role_Flags) + if (role == ColumnBase::Role_Flags) return parentColumn->nestedColumn(subSection).mFlags; - if (role==ColumnBase::Role_Display) + if (role == ColumnBase::Role_Display) return parentColumn->nestedColumn(subSection).mDisplayType; - if (role==ColumnBase::Role_ColumnId) + if (role == ColumnBase::Role_ColumnId) return parentColumn->nestedColumn(subSection).mColumnId; return QVariant(); } -bool CSMWorld::IdTree::setData (const QModelIndex &index, const QVariant &value, int role) +bool CSMWorld::IdTree::setData(const QModelIndex& index, const QVariant& value, int role) { if (index.internalId() != 0) { - if (idCollection()->getColumn(parent(index).column()).isEditable() && role==Qt::EditRole) + if (idCollection()->getColumn(parent(index).column()).isEditable() && role == Qt::EditRole) { const std::pair& parentAddress(unfoldIndexAddress(index.internalId())); - mNestedCollection->setNestedData(parentAddress.first, parentAddress.second, value, index.row(), index.column()); + mNestedCollection->setNestedData( + parentAddress.first, parentAddress.second, value, index.row(), index.column()); emit dataChanged(index, index); // Modifying a value can also change the Modified status of a record. @@ -113,7 +117,7 @@ bool CSMWorld::IdTree::setData (const QModelIndex &index, const QVariant &value, return IdTable::setData(index, value, role); } -Qt::ItemFlags CSMWorld::IdTree::flags (const QModelIndex & index) const +Qt::ItemFlags CSMWorld::IdTree::flags(const QModelIndex& index) const { if (!index.isValid()) return Qt::ItemFlags(); @@ -133,21 +137,21 @@ Qt::ItemFlags CSMWorld::IdTree::flags (const QModelIndex & index) const return IdTable::flags(index); } -bool CSMWorld::IdTree::removeRows (int row, int count, const QModelIndex& parent) +bool CSMWorld::IdTree::removeRows(int row, int count, const QModelIndex& parent) { if (parent.isValid()) { - beginRemoveRows (parent, row, row+count-1); + beginRemoveRows(parent, row, row + count - 1); for (int i = 0; i < count; ++i) { - mNestedCollection->removeNestedRows(parent.row(), parent.column(), row+i); + mNestedCollection->removeNestedRows(parent.row(), parent.column(), row + i); } endRemoveRows(); - emit dataChanged (CSMWorld::IdTree::index (parent.row(), 0), - CSMWorld::IdTree::index (parent.row(), idCollection()->getColumns()-1)); + emit dataChanged(CSMWorld::IdTree::index(parent.row(), 0), + CSMWorld::IdTree::index(parent.row(), idCollection()->getColumns() - 1)); return true; } @@ -166,11 +170,10 @@ void CSMWorld::IdTree::addNestedRow(const QModelIndex& parent, int position) mNestedCollection->addNestedRow(row, parent.column(), position); endInsertRows(); - emit dataChanged (CSMWorld::IdTree::index (row, 0), - CSMWorld::IdTree::index (row, idCollection()->getColumns()-1)); + emit dataChanged(CSMWorld::IdTree::index(row, 0), CSMWorld::IdTree::index(row, idCollection()->getColumns() - 1)); } -QModelIndex CSMWorld::IdTree::index (int row, int column, const QModelIndex& parent) const +QModelIndex CSMWorld::IdTree::index(int row, int column, const QModelIndex& parent) const { unsigned int encodedId = 0; if (parent.isValid()) @@ -187,12 +190,12 @@ QModelIndex CSMWorld::IdTree::index (int row, int column, const QModelIndex& par return createIndex(row, column, encodedId); // store internal id } -QModelIndex CSMWorld::IdTree::getNestedModelIndex (const std::string& id, int column) const +QModelIndex CSMWorld::IdTree::getNestedModelIndex(const std::string& id, int column) const { - return CSMWorld::IdTable::index(idCollection()->getIndex (id), column); + return CSMWorld::IdTable::index(idCollection()->getIndex(ESM::RefId::stringRefId(id)), column); } -QModelIndex CSMWorld::IdTree::parent (const QModelIndex& index) const +QModelIndex CSMWorld::IdTree::parent(const QModelIndex& index) const { if (index.internalId() == 0) // 0 is used for indexs with invalid parent (top level data) return QModelIndex(); @@ -206,14 +209,14 @@ QModelIndex CSMWorld::IdTree::parent (const QModelIndex& index) const return createIndex(address.first, address.second); } -unsigned int CSMWorld::IdTree::foldIndexAddress (const QModelIndex& index) const +unsigned int CSMWorld::IdTree::foldIndexAddress(const QModelIndex& index) const { unsigned int out = index.row() * this->columnCount(); out += index.column(); return ++out; } -std::pair< int, int > CSMWorld::IdTree::unfoldIndexAddress (unsigned int id) const +std::pair CSMWorld::IdTree::unfoldIndexAddress(unsigned int id) const { if (id == 0) throw std::runtime_error("Attempt to unfold index id of the top level data cell"); @@ -221,7 +224,7 @@ std::pair< int, int > CSMWorld::IdTree::unfoldIndexAddress (unsigned int id) con --id; int row = id / this->columnCount(); int column = id - row * this->columnCount(); - return std::make_pair (row, column); + return std::make_pair(row, column); } // FIXME: Not sure why this check is also needed? @@ -232,10 +235,8 @@ std::pair< int, int > CSMWorld::IdTree::unfoldIndexAddress (unsigned int id) con // Also see comments in refidadapter.hpp and refidadapterimp.hpp. bool CSMWorld::IdTree::hasChildren(const QModelIndex& index) const { - return (index.isValid() && - index.internalId() == 0 && - mNestedCollection->getNestableColumn(index.column())->hasChildren() && - index.data().isValid()); + return (index.isValid() && index.internalId() == 0 + && mNestedCollection->getNestableColumn(index.column())->hasChildren() && index.data().isValid()); } void CSMWorld::IdTree::setNestedTable(const QModelIndex& index, const CSMWorld::NestedTableWrapperBase& nestedTable) @@ -252,8 +253,8 @@ void CSMWorld::IdTree::setNestedTable(const QModelIndex& index, const CSMWorld:: mNestedCollection->setNestedTable(index.row(), index.column(), nestedTable); - emit dataChanged (CSMWorld::IdTree::index (index.row(), 0), - CSMWorld::IdTree::index (index.row(), idCollection()->getColumns()-1)); + emit dataChanged(CSMWorld::IdTree::index(index.row(), 0), + CSMWorld::IdTree::index(index.row(), idCollection()->getColumns() - 1)); if (removeRowsMode) { diff --git a/apps/opencs/model/world/idtree.hpp b/apps/opencs/model/world/idtree.hpp index c525a60b885..64e16e3ddb2 100644 --- a/apps/opencs/model/world/idtree.hpp +++ b/apps/opencs/model/world/idtree.hpp @@ -1,9 +1,15 @@ #ifndef CSM_WOLRD_IDTREE_H #define CSM_WOLRD_IDTREE_H -#include "idtable.hpp" -#include "universalid.hpp" #include "columns.hpp" +#include "idtable.hpp" + +#include +#include +#include + +#include +#include /*! \brief * Class for holding the model. Uses typical qt table abstraction/interface for granting access @@ -18,65 +24,61 @@ namespace CSMWorld { + class CollectionBase; class NestedCollection; - struct RecordBase; struct NestedTableWrapperBase; class IdTree : public IdTable { - Q_OBJECT - - private: - - NestedCollection *mNestedCollection; - - // not implemented - IdTree (const IdTree&); - IdTree& operator= (const IdTree&); - - unsigned int foldIndexAddress(const QModelIndex& index) const; - std::pair unfoldIndexAddress(unsigned int id) const; + Q_OBJECT - public: + private: + NestedCollection* mNestedCollection; - IdTree (NestedCollection *nestedCollection, CollectionBase *idCollection, unsigned int features = 0); - ///< The ownerships of \a nestedCollecton and \a idCollection are not transferred. + unsigned int foldIndexAddress(const QModelIndex& index) const; + std::pair unfoldIndexAddress(unsigned int id) const; - virtual ~IdTree(); + public: + IdTree(NestedCollection* nestedCollection, CollectionBase* idCollection, unsigned int features = 0); + ///< The ownerships of \a nestedCollecton and \a idCollection are not transferred. + IdTree(const IdTree&) = delete; + IdTree& operator=(const IdTree&) = delete; + ~IdTree() override = default; - int rowCount (const QModelIndex & parent = QModelIndex()) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; - int columnCount (const QModelIndex & parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - bool setData ( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; - Qt::ItemFlags flags (const QModelIndex & index) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; - bool removeRows (int row, int count, const QModelIndex& parent = QModelIndex()) override; + bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; - QModelIndex index (int row, int column, const QModelIndex& parent = QModelIndex()) const override; + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; - QModelIndex parent (const QModelIndex& index) const override; + QModelIndex parent(const QModelIndex& index) const override; - QModelIndex getNestedModelIndex (const std::string& id, int column) const; + QModelIndex getNestedModelIndex(const std::string& id, int column) const; - QVariant nestedHeaderData(int section, int subSection, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + QVariant nestedHeaderData( + int section, int subSection, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - NestedTableWrapperBase* nestedTable(const QModelIndex &index) const; + NestedTableWrapperBase* nestedTable(const QModelIndex& index) const; - void setNestedTable(const QModelIndex &index, const NestedTableWrapperBase& nestedTable); + void setNestedTable(const QModelIndex& index, const NestedTableWrapperBase& nestedTable); - void addNestedRow (const QModelIndex& parent, int position); + void addNestedRow(const QModelIndex& parent, int position); - bool hasChildren (const QModelIndex& index) const override; + bool hasChildren(const QModelIndex& index) const override; - virtual int searchNestedColumnIndex(int parentColumn, Columns::ColumnId id); - ///< \return the column index or -1 if the requested column wasn't found. + virtual int searchNestedColumnIndex(int parentColumn, Columns::ColumnId id); + ///< \return the column index or -1 if the requested column wasn't found. - virtual int findNestedColumnIndex(int parentColumn, Columns::ColumnId id); - ///< \return the column index or throws an exception if the requested column wasn't found. + virtual int findNestedColumnIndex(int parentColumn, Columns::ColumnId id); + ///< \return the column index or throws an exception if the requested column wasn't found. signals: diff --git a/apps/opencs/model/world/info.hpp b/apps/opencs/model/world/info.hpp index 1bcb2dc2d0c..dd3741b2667 100644 --- a/apps/opencs/model/world/info.hpp +++ b/apps/opencs/model/world/info.hpp @@ -1,13 +1,14 @@ #ifndef CSM_WOLRD_INFO_H #define CSM_WOLRD_INFO_H -#include +#include namespace CSMWorld { struct Info : public ESM::DialInfo { - std::string mTopicId; + ESM::RefId mTopicId; + ESM::RefId mOriginalId; }; } diff --git a/apps/opencs/model/world/infocollection.cpp b/apps/opencs/model/world/infocollection.cpp index be73aece6f5..7fc8a4e45d1 100644 --- a/apps/opencs/model/world/infocollection.cpp +++ b/apps/opencs/model/world/infocollection.cpp @@ -1,225 +1,141 @@ #include "infocollection.hpp" +#include +#include #include -#include +#include +#include -#include -#include +#include "components/debug/debuglog.hpp" +#include "components/esm3/infoorder.hpp" +#include "components/esm3/loaddial.hpp" +#include "components/esm3/loadinfo.hpp" -#include +#include "collection.hpp" +#include "info.hpp" -void CSMWorld::InfoCollection::load (const Info& record, bool base) +namespace CSMWorld { - int index = searchId (record.mId); - - if (index==-1) + namespace { - // new record - Record record2; - record2.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - (base ? record2.mBase : record2.mModified) = record; - - std::string topic = Misc::StringUtils::lowerCase (record2.get().mTopicId); - - if (!record2.get().mPrev.empty()) + std::string_view getInfoTopicId(const ESM::RefId& infoId) { - index = getInfoIndex (record2.get().mPrev, topic); - - if (index!=-1) - ++index; + return parseInfoRefId(infoId).first; } + } - if (index==-1 && !record2.get().mNext.empty()) - { - index = getInfoIndex (record2.get().mNext, topic); - } + ESM::RefId makeCompositeInfoRefId(const ESM::RefId& topicId, const ESM::RefId& infoId) + { + return ESM::RefId::stringRefId(topicId.getRefIdString() + '#' + infoId.getRefIdString()); + } +} - if (index==-1) - { - Range range = getTopicRange (topic); +void CSMWorld::InfoCollection::load(const Info& value, bool base) +{ + const int index = searchId(value.mId); - index = std::distance (getRecords().begin(), range.second); - } + if (index == -1) + { + // new record + auto record = std::make_unique>(); + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record->mBase : record->mModified) = value; - insertRecord (record2, index); + insertRecord(std::move(record), getSize()); } else { // old record - Record record2 = getRecord (index); + auto record = std::make_unique>(getRecord(index)); if (base) - record2.mBase = record; + record->mBase = value; else - record2.setModified (record); + record->setModified(value); - setRecord (index, record2); + setRecord(index, std::move(record)); } } -int CSMWorld::InfoCollection::getInfoIndex (const std::string& id, const std::string& topic) const -{ - std::string fullId = Misc::StringUtils::lowerCase (topic) + "#" + id; - - std::pair range = getTopicRange (topic); - - for (; range.first!=range.second; ++range.first) - if (Misc::StringUtils::ciEqual(range.first->get().mId, fullId)) - return std::distance (getRecords().begin(), range.first); - - return -1; -} - -int CSMWorld::InfoCollection::getAppendIndex (const std::string& id, UniversalId::Type type) const -{ - std::string::size_type separator = id.find_last_of ('#'); - - if (separator==std::string::npos) - throw std::runtime_error ("invalid info ID: " + id); - - std::pair range = getTopicRange (id.substr (0, separator)); - - if (range.first==range.second) - return Collection >::getAppendIndex (id, type); - - return std::distance (getRecords().begin(), range.second); -} - -bool CSMWorld::InfoCollection::reorderRows (int baseIndex, const std::vector& newOrder) -{ - // check if the range is valid - int lastIndex = baseIndex + newOrder.size() -1; - - if (lastIndex>=getSize()) - return false; - - // Check that topics match - if (!Misc::StringUtils::ciEqual(getRecord(baseIndex).get().mTopicId, - getRecord(lastIndex).get().mTopicId)) - return false; - - // reorder - return reorderRowsImp (baseIndex, newOrder); -} - -void CSMWorld::InfoCollection::load (ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue) +void CSMWorld::InfoCollection::load( + ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue, InfoOrderByTopic& infoOrders) { Info info; bool isDeleted = false; - info.load (reader, isDeleted); - std::string id = Misc::StringUtils::lowerCase (dialogue.mId) + "#" + info.mId; + info.load(reader, isDeleted); + + const ESM::RefId id = makeCompositeInfoRefId(dialogue.mId, info.mId); if (isDeleted) { - int index = searchId (id); + const int index = searchId(id); - if (index==-1) + if (index == -1) { - // deleting a record that does not exist - // ignore it for now - /// \todo report the problem to the user + Log(Debug::Warning) << "Trying to delete absent info \"" << info.mId << "\" from topic \"" << dialogue.mId + << "\""; + return; } - else if (base) - { - removeRows (index, 1); - } - else + + if (base) { - Record record = getRecord (index); - record.mState = RecordBase::State_Deleted; - setRecord (index, record); + infoOrders.at(dialogue.mId).removeInfo(info.mId); + removeRows(index, 1); + return; } - } - else - { - info.mTopicId = dialogue.mId; - info.mId = id; - load (info, base); - } -} -CSMWorld::InfoCollection::Range CSMWorld::InfoCollection::getTopicRange (const std::string& topic) - const -{ - std::string topic2 = Misc::StringUtils::lowerCase (topic); - - std::map::const_iterator iter = getIdMap().lower_bound (topic2); - - // Skip invalid records: The beginning of a topic string could be identical to another topic - // string. - for (; iter!=getIdMap().end(); ++iter) - { - std::string testTopicId = - Misc::StringUtils::lowerCase (getRecord (iter->second).get().mTopicId); - - if (testTopicId==topic2) - break; + auto record = std::make_unique>(getRecord(index)); + record->mState = RecordBase::State_Deleted; + setRecord(index, std::move(record)); - std::size_t size = topic2.size(); - - if (testTopicId.size()second; + load(info, base); - while (begin != getRecords().begin()) - { - if (!Misc::StringUtils::ciEqual(begin->get().mTopicId, topic2)) - { - // we've gone one too far, go back - ++begin; - break; - } - --begin; - } + infoOrders[dialogue.mId].insertInfo(OrderedInfo(info), isDeleted); +} - // Find end - RecordConstIterator end = begin; +void CSMWorld::InfoCollection::sort(const InfoOrderByTopic& infoOrders) +{ + std::vector order; + order.reserve(getSize()); + for (const auto& [topicId, infoOrder] : infoOrders) + for (const OrderedInfo& info : infoOrder.getOrderedInfo()) + order.push_back(getIndex(makeCompositeInfoRefId(topicId, info.mId))); + reorderRowsImp(order); +} - for (; end!=getRecords().end(); ++end) - if (!Misc::StringUtils::ciEqual(end->get().mTopicId, topic2)) - break; +CSMWorld::InfosRecordPtrByTopic CSMWorld::InfoCollection::getInfosByTopic() const +{ + InfosRecordPtrByTopic result; + for (const std::unique_ptr>& record : getRecords()) + result[record->get().mTopicId].push_back(record.get()); + return result; +} - return Range (begin, end); +int CSMWorld::InfoCollection::getAppendIndex(const ESM::RefId& id, UniversalId::Type /*type*/) const +{ + const auto lessByTopicId + = [](std::string_view lhs, const std::unique_ptr>& rhs) { return lhs < rhs->get().mTopicId; }; + const auto it = std::upper_bound(getRecords().begin(), getRecords().end(), getInfoTopicId(id), lessByTopicId); + return static_cast(it - getRecords().begin()); } -void CSMWorld::InfoCollection::removeDialogueInfos(const std::string& dialogueId) +bool CSMWorld::InfoCollection::reorderRows(int baseIndex, const std::vector& newOrder) { - std::string id = Misc::StringUtils::lowerCase(dialogueId); - std::vector erasedRecords; + const int lastIndex = baseIndex + static_cast(newOrder.size()) - 1; - std::map::const_iterator current = getIdMap().lower_bound(id); - std::map::const_iterator end = getIdMap().end(); - for (; current != end; ++current) - { - Record record = getRecord(current->second); + if (lastIndex >= getSize()) + return false; - if (Misc::StringUtils::ciEqual(dialogueId, record.get().mTopicId)) - { - if (record.mState == RecordBase::State_ModifiedOnly) - { - erasedRecords.push_back(current->second); - } - else - { - record.mState = RecordBase::State_Deleted; - setRecord(current->second, record); - } - } - else - { - break; - } - } + if (getRecord(baseIndex).get().mTopicId != getRecord(lastIndex).get().mTopicId) + return false; - while (!erasedRecords.empty()) - { - removeRows(erasedRecords.back(), 1); - erasedRecords.pop_back(); - } + return reorderRowsImp(baseIndex, newOrder); } diff --git a/apps/opencs/model/world/infocollection.hpp b/apps/opencs/model/world/infocollection.hpp index 8f5aea6012b..a26bd50dfd0 100644 --- a/apps/opencs/model/world/infocollection.hpp +++ b/apps/opencs/model/world/infocollection.hpp @@ -1,52 +1,62 @@ #ifndef CSM_WOLRD_INFOCOLLECTION_H #define CSM_WOLRD_INFOCOLLECTION_H +#include +#include +#include +#include + #include "collection.hpp" #include "info.hpp" namespace ESM { struct Dialogue; + class ESMReader; + + template + class InfoOrder; } namespace CSMWorld { - class InfoCollection : public Collection > - { - public: - - typedef std::vector >::const_iterator RecordConstIterator; - typedef std::pair Range; - - private: + using InfosRecordPtrByTopic = std::unordered_map*>>; - void load (const Info& record, bool base); + struct OrderedInfo + { + ESM::RefId mId; + ESM::RefId mNext; + ESM::RefId mPrev; + + explicit OrderedInfo(const Info& info) + : mId(info.mOriginalId) + , mNext(info.mNext) + , mPrev(info.mPrev) + { + } + }; - int getInfoIndex (const std::string& id, const std::string& topic) const; - ///< Return index for record \a id or -1 (if not present; deleted records are considered) - /// - /// \param id info ID without topic prefix + using InfoOrder = ESM::InfoOrder; + using InfoOrderByTopic = std::map>; - public: + class InfoCollection : public Collection + { + private: + void load(const Info& value, bool base); - int getAppendIndex (const std::string& id, - UniversalId::Type type = UniversalId::Type_None) const override; - ///< \param type Will be ignored, unless the collection supports multiple record types + public: + void load(ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue, InfoOrderByTopic& infoOrder); - bool reorderRows (int baseIndex, const std::vector& newOrder) override; - ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices - /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). - /// - /// \return Success? + void sort(const InfoOrderByTopic& infoOrders); - void load (ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue); + InfosRecordPtrByTopic getInfosByTopic() const; - Range getTopicRange (const std::string& topic) const; - ///< Return iterators that point to the beginning and past the end of the range for - /// the given topic. + int getAppendIndex(const ESM::RefId& id, UniversalId::Type type = UniversalId::Type_None) const override; - void removeDialogueInfos(const std::string& dialogueId); + bool reorderRows(int baseIndex, const std::vector& newOrder) override; }; + + ESM::RefId makeCompositeInfoRefId(const ESM::RefId& topicId, const ESM::RefId& infoId); } #endif diff --git a/apps/opencs/model/world/infoselectwrapper.cpp b/apps/opencs/model/world/infoselectwrapper.cpp index 3200d39fccc..2e9ed6e150c 100644 --- a/apps/opencs/model/world/infoselectwrapper.cpp +++ b/apps/opencs/model/world/infoselectwrapper.cpp @@ -4,17 +4,11 @@ #include #include -const size_t CSMWorld::ConstInfoSelectWrapper::RuleMinSize = 5; +#include -const size_t CSMWorld::ConstInfoSelectWrapper::FunctionPrefixOffset = 1; -const size_t CSMWorld::ConstInfoSelectWrapper::FunctionIndexOffset = 2; -const size_t CSMWorld::ConstInfoSelectWrapper::RelationIndexOffset = 4; -const size_t CSMWorld::ConstInfoSelectWrapper::VarNameOffset = 5; - -const char* CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings[] = -{ - "Rank Low", - "Rank High", +const char* CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings[] = { + "Faction Reaction Low", + "Faction Reaction High", "Rank Requirement", "Reputation", "Health Percent", @@ -71,7 +65,7 @@ const char* CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings[] = "PC Endurance", "PC Personality", "PC Luck", - "PC Corpus", + "PC Corprus", "Weather", "PC Vampire", "Level", @@ -98,70 +92,53 @@ const char* CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings[] = "Not Race", "Not Cell", "Not Local", - 0 + nullptr, }; -const char* CSMWorld::ConstInfoSelectWrapper::RelationEnumStrings[] = -{ +const char* CSMWorld::ConstInfoSelectWrapper::RelationEnumStrings[] = { "=", "!=", ">", ">=", "<", "<=", - 0 -}; - -const char* CSMWorld::ConstInfoSelectWrapper::ComparisonEnumStrings[] = -{ - "Boolean", - "Integer", - "Numeric", - 0 + nullptr, }; -// static functions - -std::string CSMWorld::ConstInfoSelectWrapper::convertToString(FunctionName name) +namespace { - if (name < Function_None) - return FunctionEnumStrings[name]; - else + std::string_view convertToString(ESM::DialogueCondition::Function name) + { + if (name < ESM::DialogueCondition::Function_None) + return CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings[name]; return "(Invalid Data: Function)"; -} + } -std::string CSMWorld::ConstInfoSelectWrapper::convertToString(RelationType type) -{ - if (type < Relation_None) - return RelationEnumStrings[type]; - else + std::string_view convertToString(ESM::DialogueCondition::Comparison type) + { + if (type != ESM::DialogueCondition::Comp_None) + return CSMWorld::ConstInfoSelectWrapper::RelationEnumStrings[type - ESM::DialogueCondition::Comp_Eq]; return "(Invalid Data: Relation)"; -} - -std::string CSMWorld::ConstInfoSelectWrapper::convertToString(ComparisonType type) -{ - if (type < Comparison_None) - return ComparisonEnumStrings[type]; - else - return "(Invalid Data: Comparison)"; + } } // ConstInfoSelectWrapper -CSMWorld::ConstInfoSelectWrapper::ConstInfoSelectWrapper(const ESM::DialInfo::SelectStruct& select) +CSMWorld::ConstInfoSelectWrapper::ConstInfoSelectWrapper(const ESM::DialogueCondition& select) : mConstSelect(select) { - readRule(); + updateHasVariable(); + updateComparisonType(); } -CSMWorld::ConstInfoSelectWrapper::FunctionName CSMWorld::ConstInfoSelectWrapper::getFunctionName() const +ESM::DialogueCondition::Function CSMWorld::ConstInfoSelectWrapper::getFunctionName() const { - return mFunctionName; + return mConstSelect.mFunction; } -CSMWorld::ConstInfoSelectWrapper::RelationType CSMWorld::ConstInfoSelectWrapper::getRelationType() const +ESM::DialogueCondition::Comparison CSMWorld::ConstInfoSelectWrapper::getRelationType() const { - return mRelationType; + return mConstSelect.mComparison; } CSMWorld::ConstInfoSelectWrapper::ComparisonType CSMWorld::ConstInfoSelectWrapper::getComparisonType() const @@ -176,24 +153,21 @@ bool CSMWorld::ConstInfoSelectWrapper::hasVariable() const const std::string& CSMWorld::ConstInfoSelectWrapper::getVariableName() const { - return mVariableName; + return mConstSelect.mVariable; } bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue() const { - if (!variantTypeIsValid()) - return false; - if (mComparisonType == Comparison_Boolean || mComparisonType == Comparison_Integer) { - if (mConstSelect.mValue.getType() == ESM::VT_Float) + if (std::holds_alternative(mConstSelect.mValue)) return conditionIsAlwaysTrue(getConditionFloatRange(), getValidIntRange()); else return conditionIsAlwaysTrue(getConditionIntRange(), getValidIntRange()); } else if (mComparisonType == Comparison_Numeric) { - if (mConstSelect.mValue.getType() == ESM::VT_Float) + if (std::holds_alternative(mConstSelect.mValue)) return conditionIsAlwaysTrue(getConditionFloatRange(), getValidFloatRange()); else return conditionIsAlwaysTrue(getConditionIntRange(), getValidFloatRange()); @@ -204,19 +178,16 @@ bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue() const bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue() const { - if (!variantTypeIsValid()) - return false; - if (mComparisonType == Comparison_Boolean || mComparisonType == Comparison_Integer) { - if (mConstSelect.mValue.getType() == ESM::VT_Float) + if (std::holds_alternative(mConstSelect.mValue)) return conditionIsNeverTrue(getConditionFloatRange(), getValidIntRange()); else return conditionIsNeverTrue(getConditionIntRange(), getValidIntRange()); } else if (mComparisonType == Comparison_Numeric) { - if (mConstSelect.mValue.getType() == ESM::VT_Float) + if (std::holds_alternative(mConstSelect.mValue)) return conditionIsNeverTrue(getConditionFloatRange(), getValidFloatRange()); else return conditionIsNeverTrue(getConditionIntRange(), getValidFloatRange()); @@ -225,133 +196,36 @@ bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue() const return false; } -bool CSMWorld::ConstInfoSelectWrapper::variantTypeIsValid() const -{ - return (mConstSelect.mValue.getType() == ESM::VT_Int || mConstSelect.mValue.getType() == ESM::VT_Float); -} - -const ESM::Variant& CSMWorld::ConstInfoSelectWrapper::getVariant() const -{ - return mConstSelect.mValue; -} - std::string CSMWorld::ConstInfoSelectWrapper::toString() const { std::ostringstream stream; - stream << convertToString(mFunctionName) << " "; + stream << convertToString(getFunctionName()) << " "; if (mHasVariable) - stream << mVariableName << " "; - - stream << convertToString(mRelationType) << " "; - - switch (mConstSelect.mValue.getType()) - { - case ESM::VT_Int: - stream << mConstSelect.mValue.getInteger(); - break; + stream << getVariableName() << " "; - case ESM::VT_Float: - stream << mConstSelect.mValue.getFloat(); - break; + stream << convertToString(getRelationType()) << " "; - default: - stream << "(Invalid value type)"; - break; - } + std::visit([&](auto value) { stream << value; }, mConstSelect.mValue); return stream.str(); } -void CSMWorld::ConstInfoSelectWrapper::readRule() -{ - if (mConstSelect.mSelectRule.size() < RuleMinSize) - throw std::runtime_error("InfoSelectWrapper: rule is to small"); - - readFunctionName(); - readRelationType(); - readVariableName(); - updateHasVariable(); - updateComparisonType(); -} - -void CSMWorld::ConstInfoSelectWrapper::readFunctionName() -{ - char functionPrefix = mConstSelect.mSelectRule[FunctionPrefixOffset]; - std::string functionIndex = mConstSelect.mSelectRule.substr(FunctionIndexOffset, 2); - int convertedIndex = -1; - - // Read in function index, form ## from 00 .. 73, skip leading zero - if (functionIndex[0] == '0') - functionIndex = functionIndex[1]; - - std::stringstream stream; - stream << functionIndex; - stream >> convertedIndex; - - switch (functionPrefix) - { - case '1': - if (convertedIndex >= 0 && convertedIndex <= 73) - mFunctionName = static_cast(convertedIndex); - else - mFunctionName = Function_None; - break; - - case '2': mFunctionName = Function_Global; break; - case '3': mFunctionName = Function_Local; break; - case '4': mFunctionName = Function_Journal; break; - case '5': mFunctionName = Function_Item; break; - case '6': mFunctionName = Function_Dead; break; - case '7': mFunctionName = Function_NotId; break; - case '8': mFunctionName = Function_NotFaction; break; - case '9': mFunctionName = Function_NotClass; break; - case 'A': mFunctionName = Function_NotRace; break; - case 'B': mFunctionName = Function_NotCell; break; - case 'C': mFunctionName = Function_NotLocal; break; - default: mFunctionName = Function_None; break; - } -} - -void CSMWorld::ConstInfoSelectWrapper::readRelationType() -{ - char relationIndex = mConstSelect.mSelectRule[RelationIndexOffset]; - - switch (relationIndex) - { - case '0': mRelationType = Relation_Equal; break; - case '1': mRelationType = Relation_NotEqual; break; - case '2': mRelationType = Relation_Greater; break; - case '3': mRelationType = Relation_GreaterOrEqual; break; - case '4': mRelationType = Relation_Less; break; - case '5': mRelationType = Relation_LessOrEqual; break; - default: mRelationType = Relation_None; - } -} - -void CSMWorld::ConstInfoSelectWrapper::readVariableName() -{ - if (mConstSelect.mSelectRule.size() >= VarNameOffset) - mVariableName = mConstSelect.mSelectRule.substr(VarNameOffset); - else - mVariableName.clear(); -} - void CSMWorld::ConstInfoSelectWrapper::updateHasVariable() { - switch (mFunctionName) + switch (getFunctionName()) { - case Function_Global: - case Function_Local: - case Function_Journal: - case Function_Item: - case Function_Dead: - case Function_NotId: - case Function_NotFaction: - case Function_NotClass: - case Function_NotRace: - case Function_NotCell: - case Function_NotLocal: + case ESM::DialogueCondition::Function_Global: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_Journal: + case ESM::DialogueCondition::Function_Item: + case ESM::DialogueCondition::Function_Dead: + case ESM::DialogueCondition::Function_NotId: + case ESM::DialogueCondition::Function_NotFaction: + case ESM::DialogueCondition::Function_NotClass: + case ESM::DialogueCondition::Function_NotRace: + case ESM::DialogueCondition::Function_NotCell: + case ESM::DialogueCondition::Function_NotLocal: mHasVariable = true; break; @@ -363,103 +237,103 @@ void CSMWorld::ConstInfoSelectWrapper::updateHasVariable() void CSMWorld::ConstInfoSelectWrapper::updateComparisonType() { - switch (mFunctionName) + switch (getFunctionName()) { // Boolean - case Function_NotId: - case Function_NotFaction: - case Function_NotClass: - case Function_NotRace: - case Function_NotCell: - case Function_PcExpelled: - case Function_PcCommonDisease: - case Function_PcBlightDisease: - case Function_SameSex: - case Function_SameRace: - case Function_SameFaction: - case Function_Detected: - case Function_Alarmed: - case Function_PcCorpus: - case Function_PcVampire: - case Function_Attacked: - case Function_TalkedToPc: - case Function_ShouldAttack: - case Function_Werewolf: + case ESM::DialogueCondition::Function_NotId: + case ESM::DialogueCondition::Function_NotFaction: + case ESM::DialogueCondition::Function_NotClass: + case ESM::DialogueCondition::Function_NotRace: + case ESM::DialogueCondition::Function_NotCell: + case ESM::DialogueCondition::Function_PcExpelled: + case ESM::DialogueCondition::Function_PcCommonDisease: + case ESM::DialogueCondition::Function_PcBlightDisease: + case ESM::DialogueCondition::Function_SameSex: + case ESM::DialogueCondition::Function_SameRace: + case ESM::DialogueCondition::Function_SameFaction: + case ESM::DialogueCondition::Function_Detected: + case ESM::DialogueCondition::Function_Alarmed: + case ESM::DialogueCondition::Function_PcCorprus: + case ESM::DialogueCondition::Function_PcVampire: + case ESM::DialogueCondition::Function_Attacked: + case ESM::DialogueCondition::Function_TalkedToPc: + case ESM::DialogueCondition::Function_ShouldAttack: + case ESM::DialogueCondition::Function_Werewolf: mComparisonType = Comparison_Boolean; break; // Integer - case Function_Journal: - case Function_Item: - case Function_Dead: - case Function_RankLow: - case Function_RankHigh: - case Function_RankRequirement: - case Function_Reputation: - case Function_PcReputation: - case Function_PcLevel: - case Function_PcStrength: - case Function_PcBlock: - case Function_PcArmorer: - case Function_PcMediumArmor: - case Function_PcHeavyArmor: - case Function_PcBluntWeapon: - case Function_PcLongBlade: - case Function_PcAxe: - case Function_PcSpear: - case Function_PcAthletics: - case Function_PcEnchant: - case Function_PcDestruction: - case Function_PcAlteration: - case Function_PcIllusion: - case Function_PcConjuration: - case Function_PcMysticism: - case Function_PcRestoration: - case Function_PcAlchemy: - case Function_PcUnarmored: - case Function_PcSecurity: - case Function_PcSneak: - case Function_PcAcrobatics: - case Function_PcLightArmor: - case Function_PcShortBlade: - case Function_PcMarksman: - case Function_PcMerchantile: - case Function_PcSpeechcraft: - case Function_PcHandToHand: - case Function_PcGender: - case Function_PcClothingModifier: - case Function_PcCrimeLevel: - case Function_FactionRankDifference: - case Function_Choice: - case Function_PcIntelligence: - case Function_PcWillpower: - case Function_PcAgility: - case Function_PcSpeed: - case Function_PcEndurance: - case Function_PcPersonality: - case Function_PcLuck: - case Function_Weather: - case Function_Level: - case Function_CreatureTarget: - case Function_FriendHit: - case Function_Fight: - case Function_Hello: - case Function_Alarm: - case Function_Flee: - case Function_PcWerewolfKills: + case ESM::DialogueCondition::Function_Journal: + case ESM::DialogueCondition::Function_Item: + case ESM::DialogueCondition::Function_Dead: + case ESM::DialogueCondition::Function_FacReactionLowest: + case ESM::DialogueCondition::Function_FacReactionHighest: + case ESM::DialogueCondition::Function_RankRequirement: + case ESM::DialogueCondition::Function_Reputation: + case ESM::DialogueCondition::Function_PcReputation: + case ESM::DialogueCondition::Function_PcLevel: + case ESM::DialogueCondition::Function_PcStrength: + case ESM::DialogueCondition::Function_PcBlock: + case ESM::DialogueCondition::Function_PcArmorer: + case ESM::DialogueCondition::Function_PcMediumArmor: + case ESM::DialogueCondition::Function_PcHeavyArmor: + case ESM::DialogueCondition::Function_PcBluntWeapon: + case ESM::DialogueCondition::Function_PcLongBlade: + case ESM::DialogueCondition::Function_PcAxe: + case ESM::DialogueCondition::Function_PcSpear: + case ESM::DialogueCondition::Function_PcAthletics: + case ESM::DialogueCondition::Function_PcEnchant: + case ESM::DialogueCondition::Function_PcDestruction: + case ESM::DialogueCondition::Function_PcAlteration: + case ESM::DialogueCondition::Function_PcIllusion: + case ESM::DialogueCondition::Function_PcConjuration: + case ESM::DialogueCondition::Function_PcMysticism: + case ESM::DialogueCondition::Function_PcRestoration: + case ESM::DialogueCondition::Function_PcAlchemy: + case ESM::DialogueCondition::Function_PcUnarmored: + case ESM::DialogueCondition::Function_PcSecurity: + case ESM::DialogueCondition::Function_PcSneak: + case ESM::DialogueCondition::Function_PcAcrobatics: + case ESM::DialogueCondition::Function_PcLightArmor: + case ESM::DialogueCondition::Function_PcShortBlade: + case ESM::DialogueCondition::Function_PcMarksman: + case ESM::DialogueCondition::Function_PcMerchantile: + case ESM::DialogueCondition::Function_PcSpeechcraft: + case ESM::DialogueCondition::Function_PcHandToHand: + case ESM::DialogueCondition::Function_PcGender: + case ESM::DialogueCondition::Function_PcClothingModifier: + case ESM::DialogueCondition::Function_PcCrimeLevel: + case ESM::DialogueCondition::Function_FactionRankDifference: + case ESM::DialogueCondition::Function_Choice: + case ESM::DialogueCondition::Function_PcIntelligence: + case ESM::DialogueCondition::Function_PcWillpower: + case ESM::DialogueCondition::Function_PcAgility: + case ESM::DialogueCondition::Function_PcSpeed: + case ESM::DialogueCondition::Function_PcEndurance: + case ESM::DialogueCondition::Function_PcPersonality: + case ESM::DialogueCondition::Function_PcLuck: + case ESM::DialogueCondition::Function_Weather: + case ESM::DialogueCondition::Function_Level: + case ESM::DialogueCondition::Function_CreatureTarget: + case ESM::DialogueCondition::Function_FriendHit: + case ESM::DialogueCondition::Function_Fight: + case ESM::DialogueCondition::Function_Hello: + case ESM::DialogueCondition::Function_Alarm: + case ESM::DialogueCondition::Function_Flee: + case ESM::DialogueCondition::Function_PcWerewolfKills: mComparisonType = Comparison_Integer; break; // Numeric - case Function_Global: - case Function_Local: - case Function_NotLocal: - - case Function_Health_Percent: - case Function_PcHealthPercent: - case Function_PcMagicka: - case Function_PcFatigue: - case Function_PcHealth: + case ESM::DialogueCondition::Function_Global: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_NotLocal: + + case ESM::DialogueCondition::Function_Health_Percent: + case ESM::DialogueCondition::Function_PcHealthPercent: + case ESM::DialogueCondition::Function_PcMagicka: + case ESM::DialogueCondition::Function_PcFatigue: + case ESM::DialogueCondition::Function_PcHealth: mComparisonType = Comparison_Numeric; break; @@ -475,15 +349,15 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getConditionIntRange() con const int IntMin = std::numeric_limits::min(); const std::pair InvalidRange(IntMax, IntMin); - int value = mConstSelect.mValue.getInteger(); + int value = std::get(mConstSelect.mValue); - switch (mRelationType) + switch (getRelationType()) { - case Relation_Equal: - case Relation_NotEqual: + case ESM::DialogueCondition::Comp_Eq: + case ESM::DialogueCondition::Comp_Ne: return std::pair(value, value); - case Relation_Greater: + case ESM::DialogueCondition::Comp_Gt: if (value == IntMax) { return InvalidRange; @@ -494,10 +368,10 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getConditionIntRange() con } break; - case Relation_GreaterOrEqual: + case ESM::DialogueCondition::Comp_Ge: return std::pair(value, IntMax); - case Relation_Less: + case ESM::DialogueCondition::Comp_Ls: if (value == IntMin) { return InvalidRange; @@ -507,7 +381,7 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getConditionIntRange() con return std::pair(IntMin, value - 1); } - case Relation_LessOrEqual: + case ESM::DialogueCondition::Comp_Le: return std::pair(IntMin, value); default: @@ -521,24 +395,24 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getConditionFloatRange const float FloatMin = -std::numeric_limits::infinity(); const float Epsilon = std::numeric_limits::epsilon(); - float value = mConstSelect.mValue.getFloat(); + float value = std::get(mConstSelect.mValue); - switch (mRelationType) + switch (getRelationType()) { - case Relation_Equal: - case Relation_NotEqual: + case ESM::DialogueCondition::Comp_Eq: + case ESM::DialogueCondition::Comp_Ne: return std::pair(value, value); - case Relation_Greater: + case ESM::DialogueCondition::Comp_Gt: return std::pair(value + Epsilon, FloatMax); - case Relation_GreaterOrEqual: + case ESM::DialogueCondition::Comp_Ge: return std::pair(value, FloatMax); - case Relation_Less: + case ESM::DialogueCondition::Comp_Ls: return std::pair(FloatMin, value - Epsilon); - case Relation_LessOrEqual: + case ESM::DialogueCondition::Comp_Le: return std::pair(FloatMin, value); default: @@ -551,120 +425,120 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getValidIntRange() const const int IntMax = std::numeric_limits::max(); const int IntMin = std::numeric_limits::min(); - switch (mFunctionName) + switch (getFunctionName()) { // Boolean - case Function_NotId: - case Function_NotFaction: - case Function_NotClass: - case Function_NotRace: - case Function_NotCell: - case Function_PcExpelled: - case Function_PcCommonDisease: - case Function_PcBlightDisease: - case Function_SameSex: - case Function_SameRace: - case Function_SameFaction: - case Function_Detected: - case Function_Alarmed: - case Function_PcCorpus: - case Function_PcVampire: - case Function_Attacked: - case Function_TalkedToPc: - case Function_ShouldAttack: - case Function_Werewolf: + case ESM::DialogueCondition::Function_NotId: + case ESM::DialogueCondition::Function_NotFaction: + case ESM::DialogueCondition::Function_NotClass: + case ESM::DialogueCondition::Function_NotRace: + case ESM::DialogueCondition::Function_NotCell: + case ESM::DialogueCondition::Function_PcExpelled: + case ESM::DialogueCondition::Function_PcCommonDisease: + case ESM::DialogueCondition::Function_PcBlightDisease: + case ESM::DialogueCondition::Function_SameSex: + case ESM::DialogueCondition::Function_SameRace: + case ESM::DialogueCondition::Function_SameFaction: + case ESM::DialogueCondition::Function_Detected: + case ESM::DialogueCondition::Function_Alarmed: + case ESM::DialogueCondition::Function_PcCorprus: + case ESM::DialogueCondition::Function_PcVampire: + case ESM::DialogueCondition::Function_Attacked: + case ESM::DialogueCondition::Function_TalkedToPc: + case ESM::DialogueCondition::Function_ShouldAttack: + case ESM::DialogueCondition::Function_Werewolf: return std::pair(0, 1); // Integer - case Function_RankLow: - case Function_RankHigh: - case Function_Reputation: - case Function_PcReputation: - case Function_Journal: + case ESM::DialogueCondition::Function_FacReactionLowest: + case ESM::DialogueCondition::Function_FacReactionHighest: + case ESM::DialogueCondition::Function_Reputation: + case ESM::DialogueCondition::Function_PcReputation: + case ESM::DialogueCondition::Function_Journal: return std::pair(IntMin, IntMax); - case Function_Item: - case Function_Dead: - case Function_PcLevel: - case Function_PcStrength: - case Function_PcBlock: - case Function_PcArmorer: - case Function_PcMediumArmor: - case Function_PcHeavyArmor: - case Function_PcBluntWeapon: - case Function_PcLongBlade: - case Function_PcAxe: - case Function_PcSpear: - case Function_PcAthletics: - case Function_PcEnchant: - case Function_PcDestruction: - case Function_PcAlteration: - case Function_PcIllusion: - case Function_PcConjuration: - case Function_PcMysticism: - case Function_PcRestoration: - case Function_PcAlchemy: - case Function_PcUnarmored: - case Function_PcSecurity: - case Function_PcSneak: - case Function_PcAcrobatics: - case Function_PcLightArmor: - case Function_PcShortBlade: - case Function_PcMarksman: - case Function_PcMerchantile: - case Function_PcSpeechcraft: - case Function_PcHandToHand: - case Function_PcClothingModifier: - case Function_PcCrimeLevel: - case Function_Choice: - case Function_PcIntelligence: - case Function_PcWillpower: - case Function_PcAgility: - case Function_PcSpeed: - case Function_PcEndurance: - case Function_PcPersonality: - case Function_PcLuck: - case Function_Level: - case Function_PcWerewolfKills: + case ESM::DialogueCondition::Function_Item: + case ESM::DialogueCondition::Function_Dead: + case ESM::DialogueCondition::Function_PcLevel: + case ESM::DialogueCondition::Function_PcStrength: + case ESM::DialogueCondition::Function_PcBlock: + case ESM::DialogueCondition::Function_PcArmorer: + case ESM::DialogueCondition::Function_PcMediumArmor: + case ESM::DialogueCondition::Function_PcHeavyArmor: + case ESM::DialogueCondition::Function_PcBluntWeapon: + case ESM::DialogueCondition::Function_PcLongBlade: + case ESM::DialogueCondition::Function_PcAxe: + case ESM::DialogueCondition::Function_PcSpear: + case ESM::DialogueCondition::Function_PcAthletics: + case ESM::DialogueCondition::Function_PcEnchant: + case ESM::DialogueCondition::Function_PcDestruction: + case ESM::DialogueCondition::Function_PcAlteration: + case ESM::DialogueCondition::Function_PcIllusion: + case ESM::DialogueCondition::Function_PcConjuration: + case ESM::DialogueCondition::Function_PcMysticism: + case ESM::DialogueCondition::Function_PcRestoration: + case ESM::DialogueCondition::Function_PcAlchemy: + case ESM::DialogueCondition::Function_PcUnarmored: + case ESM::DialogueCondition::Function_PcSecurity: + case ESM::DialogueCondition::Function_PcSneak: + case ESM::DialogueCondition::Function_PcAcrobatics: + case ESM::DialogueCondition::Function_PcLightArmor: + case ESM::DialogueCondition::Function_PcShortBlade: + case ESM::DialogueCondition::Function_PcMarksman: + case ESM::DialogueCondition::Function_PcMerchantile: + case ESM::DialogueCondition::Function_PcSpeechcraft: + case ESM::DialogueCondition::Function_PcHandToHand: + case ESM::DialogueCondition::Function_PcClothingModifier: + case ESM::DialogueCondition::Function_PcCrimeLevel: + case ESM::DialogueCondition::Function_Choice: + case ESM::DialogueCondition::Function_PcIntelligence: + case ESM::DialogueCondition::Function_PcWillpower: + case ESM::DialogueCondition::Function_PcAgility: + case ESM::DialogueCondition::Function_PcSpeed: + case ESM::DialogueCondition::Function_PcEndurance: + case ESM::DialogueCondition::Function_PcPersonality: + case ESM::DialogueCondition::Function_PcLuck: + case ESM::DialogueCondition::Function_Level: + case ESM::DialogueCondition::Function_PcWerewolfKills: return std::pair(0, IntMax); - case Function_Fight: - case Function_Hello: - case Function_Alarm: - case Function_Flee: + case ESM::DialogueCondition::Function_Fight: + case ESM::DialogueCondition::Function_Hello: + case ESM::DialogueCondition::Function_Alarm: + case ESM::DialogueCondition::Function_Flee: return std::pair(0, 100); - case Function_Weather: + case ESM::DialogueCondition::Function_Weather: return std::pair(0, 9); - case Function_FriendHit: + case ESM::DialogueCondition::Function_FriendHit: return std::pair(0, 4); - case Function_RankRequirement: + case ESM::DialogueCondition::Function_RankRequirement: return std::pair(0, 3); - case Function_CreatureTarget: + case ESM::DialogueCondition::Function_CreatureTarget: return std::pair(0, 2); - case Function_PcGender: + case ESM::DialogueCondition::Function_PcGender: return std::pair(0, 1); - case Function_FactionRankDifference: + case ESM::DialogueCondition::Function_FactionRankDifference: return std::pair(-9, 9); // Numeric - case Function_Global: - case Function_Local: - case Function_NotLocal: + case ESM::DialogueCondition::Function_Global: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_NotLocal: return std::pair(IntMin, IntMax); - case Function_PcMagicka: - case Function_PcFatigue: - case Function_PcHealth: + case ESM::DialogueCondition::Function_PcMagicka: + case ESM::DialogueCondition::Function_PcFatigue: + case ESM::DialogueCondition::Function_PcHealth: return std::pair(0, IntMax); - case Function_Health_Percent: - case Function_PcHealthPercent: + case ESM::DialogueCondition::Function_Health_Percent: + case ESM::DialogueCondition::Function_PcHealthPercent: return std::pair(0, 100); default: @@ -677,21 +551,21 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getValidFloatRange() c const float FloatMax = std::numeric_limits::infinity(); const float FloatMin = -std::numeric_limits::infinity(); - switch (mFunctionName) + switch (getFunctionName()) { // Numeric - case Function_Global: - case Function_Local: - case Function_NotLocal: + case ESM::DialogueCondition::Function_Global: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_NotLocal: return std::pair(FloatMin, FloatMax); - case Function_PcMagicka: - case Function_PcFatigue: - case Function_PcHealth: + case ESM::DialogueCondition::Function_PcMagicka: + case ESM::DialogueCondition::Function_PcFatigue: + case ESM::DialogueCondition::Function_PcHealth: return std::pair(0, FloatMax); - case Function_Health_Percent: - case Function_PcHealthPercent: + case ESM::DialogueCondition::Function_Health_Percent: + case ESM::DialogueCondition::Function_PcHealthPercent: return std::pair(0, 100); default: @@ -700,52 +574,51 @@ std::pair CSMWorld::ConstInfoSelectWrapper::getValidFloatRange() c } template -bool CSMWorld::ConstInfoSelectWrapper::rangeContains(T1 value, std::pair range) const +bool CSMWorld::ConstInfoSelectWrapper::rangeContains(T1 value, std::pair range) const { return (value >= range.first && value <= range.second); } template -bool CSMWorld::ConstInfoSelectWrapper::rangeFullyContains(std::pair containingRange, - std::pair testRange) const +bool CSMWorld::ConstInfoSelectWrapper::rangeFullyContains( + std::pair containingRange, std::pair testRange) const { return (containingRange.first <= testRange.first) && (testRange.second <= containingRange.second); } template -bool CSMWorld::ConstInfoSelectWrapper::rangesOverlap(std::pair range1, std::pair range2) const +bool CSMWorld::ConstInfoSelectWrapper::rangesOverlap(std::pair range1, std::pair range2) const { // One of the bounds of either range should fall within the other range - return - (range1.first <= range2.first && range2.first <= range1.second) || - (range1.first <= range2.second && range2.second <= range1.second) || - (range2.first <= range1.first && range1.first <= range2.second) || - (range2.first <= range1.second && range1.second <= range2.second); + return (range1.first <= range2.first && range2.first <= range1.second) + || (range1.first <= range2.second && range2.second <= range1.second) + || (range2.first <= range1.first && range1.first <= range2.second) + || (range2.first <= range1.second && range1.second <= range2.second); } template -bool CSMWorld::ConstInfoSelectWrapper::rangesMatch(std::pair range1, std::pair range2) const +bool CSMWorld::ConstInfoSelectWrapper::rangesMatch(std::pair range1, std::pair range2) const { - return (range1.first == range2.first && range1.second == range2.second); + return (range1.first == range2.first && range1.second == range2.second); } template -bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue(std::pair conditionRange, - std::pair validRange) const +bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue( + std::pair conditionRange, std::pair validRange) const { - switch (mRelationType) + switch (getRelationType()) { - case Relation_Equal: + case ESM::DialogueCondition::Comp_Eq: return false; - case Relation_NotEqual: + case ESM::DialogueCondition::Comp_Ne: // If value is not within range, it will always be true return !rangeContains(conditionRange.first, validRange); - case Relation_Greater: - case Relation_GreaterOrEqual: - case Relation_Less: - case Relation_LessOrEqual: + case ESM::DialogueCondition::Comp_Gt: + case ESM::DialogueCondition::Comp_Ge: + case ESM::DialogueCondition::Comp_Ls: + case ESM::DialogueCondition::Comp_Le: // If the valid range is completely within the condition range, it will always be true return rangeFullyContains(conditionRange, validRange); @@ -757,21 +630,21 @@ bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue(std::pair co } template -bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue(std::pair conditionRange, - std::pair validRange) const +bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue( + std::pair conditionRange, std::pair validRange) const { - switch (mRelationType) + switch (getRelationType()) { - case Relation_Equal: + case ESM::DialogueCondition::Comp_Eq: return !rangeContains(conditionRange.first, validRange); - case Relation_NotEqual: + case ESM::DialogueCondition::Comp_Ne: return false; - case Relation_Greater: - case Relation_GreaterOrEqual: - case Relation_Less: - case Relation_LessOrEqual: + case ESM::DialogueCondition::Comp_Gt: + case ESM::DialogueCondition::Comp_Ge: + case ESM::DialogueCondition::Comp_Ls: + case ESM::DialogueCondition::Comp_Le: // If ranges do not overlap, it will never be true return !rangesOverlap(conditionRange, validRange); @@ -782,111 +655,47 @@ bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue(std::pair con return false; } +QVariant CSMWorld::ConstInfoSelectWrapper::getValue() const +{ + return std::visit([](auto value) { return QVariant(value); }, mConstSelect.mValue); +} + // InfoSelectWrapper -CSMWorld::InfoSelectWrapper::InfoSelectWrapper(ESM::DialInfo::SelectStruct& select) - : CSMWorld::ConstInfoSelectWrapper(select), mSelect(select) +CSMWorld::InfoSelectWrapper::InfoSelectWrapper(ESM::DialogueCondition& select) + : CSMWorld::ConstInfoSelectWrapper(select) + , mSelect(select) { } -void CSMWorld::InfoSelectWrapper::setFunctionName(FunctionName name) +void CSMWorld::InfoSelectWrapper::setFunctionName(ESM::DialogueCondition::Function name) { - mFunctionName = name; + mSelect.mFunction = name; updateHasVariable(); updateComparisonType(); + if (getComparisonType() != ConstInfoSelectWrapper::Comparison_Numeric + && std::holds_alternative(mSelect.mValue)) + { + mSelect.mValue = std::visit([](auto value) { return static_cast(value); }, mSelect.mValue); + } } -void CSMWorld::InfoSelectWrapper::setRelationType(RelationType type) +void CSMWorld::InfoSelectWrapper::setRelationType(ESM::DialogueCondition::Comparison type) { - mRelationType = type; + mSelect.mComparison = type; } void CSMWorld::InfoSelectWrapper::setVariableName(const std::string& name) { - mVariableName = name; -} - -void CSMWorld::InfoSelectWrapper::setDefaults() -{ - if (!variantTypeIsValid()) - mSelect.mValue.setType(ESM::VT_Int); - - switch (mComparisonType) - { - case Comparison_Boolean: - setRelationType(Relation_Equal); - mSelect.mValue.setInteger(1); - break; - - case Comparison_Integer: - case Comparison_Numeric: - setRelationType(Relation_Greater); - mSelect.mValue.setInteger(0); - break; - - default: - // Do nothing - break; - } - - update(); + mSelect.mVariable = name; } -void CSMWorld::InfoSelectWrapper::update() +void CSMWorld::InfoSelectWrapper::setValue(int value) { - std::ostringstream stream; - - // Leading 0 - stream << '0'; - - // Write Function - - bool writeIndex = false; - size_t functionIndex = static_cast(mFunctionName); - - switch (mFunctionName) - { - case Function_None: stream << '0'; break; - case Function_Global: stream << '2'; break; - case Function_Local: stream << '3'; break; - case Function_Journal: stream << '4'; break; - case Function_Item: stream << '5'; break; - case Function_Dead: stream << '6'; break; - case Function_NotId: stream << '7'; break; - case Function_NotFaction: stream << '8'; break; - case Function_NotClass: stream << '9'; break; - case Function_NotRace: stream << 'A'; break; - case Function_NotCell: stream << 'B'; break; - case Function_NotLocal: stream << 'C'; break; - default: stream << '1'; writeIndex = true; break; - } - - if (writeIndex && functionIndex < 10) // leading 0 - stream << '0' << functionIndex; - else if (writeIndex) - stream << functionIndex; - else - stream << "00"; - - // Write Relation - switch (mRelationType) - { - case Relation_Equal: stream << '0'; break; - case Relation_NotEqual: stream << '1'; break; - case Relation_Greater: stream << '2'; break; - case Relation_GreaterOrEqual: stream << '3'; break; - case Relation_Less: stream << '4'; break; - case Relation_LessOrEqual: stream << '5'; break; - default: stream << '0'; break; - } - - if (mHasVariable) - stream << mVariableName; - - mSelect.mSelectRule = stream.str(); + mSelect.mValue = value; } -ESM::Variant& CSMWorld::InfoSelectWrapper::getVariant() +void CSMWorld::InfoSelectWrapper::setValue(float value) { - return mSelect.mValue; + mSelect.mValue = value; } diff --git a/apps/opencs/model/world/infoselectwrapper.hpp b/apps/opencs/model/world/infoselectwrapper.hpp index ce26a46dc7f..b3b5abe462e 100644 --- a/apps/opencs/model/world/infoselectwrapper.hpp +++ b/apps/opencs/model/world/infoselectwrapper.hpp @@ -1,131 +1,19 @@ #ifndef CSM_WORLD_INFOSELECTWRAPPER_H #define CSM_WORLD_INFOSELECTWRAPPER_H -#include +#include +#include +#include + +#include + +#include namespace CSMWorld { - // ESM::DialInfo::SelectStruct.mSelectRule - // 012345... - // ^^^ ^^ - // ||| || - // ||| |+------------- condition variable string - // ||| +-------------- comparison type, ['0'..'5']; e.g. !=, <, >=, etc - // ||+---------------- function index (encoded, where function == '1') - // |+----------------- function, ['1'..'C']; e.g. Global, Local, Not ID, etc - // +------------------ unknown - // - - // Wrapper for DialInfo::SelectStruct class ConstInfoSelectWrapper { public: - - // Order matters - enum FunctionName - { - Function_RankLow=0, - Function_RankHigh, - Function_RankRequirement, - Function_Reputation, - Function_Health_Percent, - Function_PcReputation, - Function_PcLevel, - Function_PcHealthPercent, - Function_PcMagicka, - Function_PcFatigue, - Function_PcStrength, - Function_PcBlock, - Function_PcArmorer, - Function_PcMediumArmor, - Function_PcHeavyArmor, - Function_PcBluntWeapon, - Function_PcLongBlade, - Function_PcAxe, - Function_PcSpear, - Function_PcAthletics, - Function_PcEnchant, - Function_PcDestruction, - Function_PcAlteration, - Function_PcIllusion, - Function_PcConjuration, - Function_PcMysticism, - Function_PcRestoration, - Function_PcAlchemy, - Function_PcUnarmored, - Function_PcSecurity, - Function_PcSneak, - Function_PcAcrobatics, - Function_PcLightArmor, - Function_PcShortBlade, - Function_PcMarksman, - Function_PcMerchantile, - Function_PcSpeechcraft, - Function_PcHandToHand, - Function_PcGender, - Function_PcExpelled, - Function_PcCommonDisease, - Function_PcBlightDisease, - Function_PcClothingModifier, - Function_PcCrimeLevel, - Function_SameSex, - Function_SameRace, - Function_SameFaction, - Function_FactionRankDifference, - Function_Detected, - Function_Alarmed, - Function_Choice, - Function_PcIntelligence, - Function_PcWillpower, - Function_PcAgility, - Function_PcSpeed, - Function_PcEndurance, - Function_PcPersonality, - Function_PcLuck, - Function_PcCorpus, - Function_Weather, - Function_PcVampire, - Function_Level, - Function_Attacked, - Function_TalkedToPc, - Function_PcHealth, - Function_CreatureTarget, - Function_FriendHit, - Function_Fight, - Function_Hello, - Function_Alarm, - Function_Flee, - Function_ShouldAttack, - Function_Werewolf, - Function_PcWerewolfKills=73, - - Function_Global, - Function_Local, - Function_Journal, - Function_Item, - Function_Dead, - Function_NotId, - Function_NotFaction, - Function_NotClass, - Function_NotRace, - Function_NotCell, - Function_NotLocal, - - Function_None - }; - - enum RelationType - { - Relation_Equal, - Relation_NotEqual, - Relation_Greater, - Relation_GreaterOrEqual, - Relation_Less, - Relation_LessOrEqual, - - Relation_None - }; - enum ComparisonType { Comparison_Boolean, @@ -135,25 +23,13 @@ namespace CSMWorld Comparison_None }; - static const size_t RuleMinSize; - - static const size_t FunctionPrefixOffset; - static const size_t FunctionIndexOffset; - static const size_t RelationIndexOffset; - static const size_t VarNameOffset; - static const char* FunctionEnumStrings[]; static const char* RelationEnumStrings[]; - static const char* ComparisonEnumStrings[]; - - static std::string convertToString(FunctionName name); - static std::string convertToString(RelationType type); - static std::string convertToString(ComparisonType type); - ConstInfoSelectWrapper(const ESM::DialInfo::SelectStruct& select); + ConstInfoSelectWrapper(const ESM::DialogueCondition& select); - FunctionName getFunctionName() const; - RelationType getRelationType() const; + ESM::DialogueCondition::Function getFunctionName() const; + ESM::DialogueCondition::Comparison getRelationType() const; ComparisonType getComparisonType() const; bool hasVariable() const; @@ -161,18 +37,12 @@ namespace CSMWorld bool conditionIsAlwaysTrue() const; bool conditionIsNeverTrue() const; - bool variantTypeIsValid() const; - const ESM::Variant& getVariant() const; + QVariant getValue() const; std::string toString() const; protected: - - void readRule(); - void readFunctionName(); - void readRelationType(); - void readVariableName(); void updateHasVariable(); void updateComparisonType(); @@ -183,58 +53,46 @@ namespace CSMWorld std::pair getValidFloatRange() const; template - bool rangeContains(Type1 value, std::pair range) const; + bool rangeContains(Type1 value, std::pair range) const; template - bool rangesOverlap(std::pair range1, std::pair range2) const; + bool rangesOverlap(std::pair range1, std::pair range2) const; template - bool rangeFullyContains(std::pair containing, std::pair test) const; + bool rangeFullyContains(std::pair containing, std::pair test) const; template - bool rangesMatch(std::pair range1, std::pair range2) const; + bool rangesMatch(std::pair range1, std::pair range2) const; template - bool conditionIsAlwaysTrue(std::pair conditionRange, std::pair validRange) const; + bool conditionIsAlwaysTrue(std::pair conditionRange, std::pair validRange) const; template - bool conditionIsNeverTrue(std::pair conditionRange, std::pair validRange) const; + bool conditionIsNeverTrue(std::pair conditionRange, std::pair validRange) const; - FunctionName mFunctionName; - RelationType mRelationType; ComparisonType mComparisonType; bool mHasVariable; - std::string mVariableName; private: - - const ESM::DialInfo::SelectStruct& mConstSelect; + const ESM::DialogueCondition& mConstSelect; }; - // Wrapper for DialInfo::SelectStruct that can modify the wrapped select struct + // Wrapper for DialogueCondition that can modify the wrapped select struct class InfoSelectWrapper : public ConstInfoSelectWrapper { public: - - InfoSelectWrapper(ESM::DialInfo::SelectStruct& select); + InfoSelectWrapper(ESM::DialogueCondition& select); // Wrapped SelectStruct will not be modified until update() is called - void setFunctionName(FunctionName name); - void setRelationType(RelationType type); + void setFunctionName(ESM::DialogueCondition::Function name); + void setRelationType(ESM::DialogueCondition::Comparison type); void setVariableName(const std::string& name); - - // Modified wrapped SelectStruct - void update(); - - // This sets properties based on the function name to its defaults and updates the wrapped object - void setDefaults(); - - ESM::Variant& getVariant(); + void setValue(int value); + void setValue(float value); private: - - ESM::DialInfo::SelectStruct& mSelect; + ESM::DialogueCondition& mSelect; void writeRule(); }; diff --git a/apps/opencs/model/world/infotableproxymodel.cpp b/apps/opencs/model/world/infotableproxymodel.cpp index 7c6a9c323e5..171fbd9ffd9 100644 --- a/apps/opencs/model/world/infotableproxymodel.cpp +++ b/apps/opencs/model/world/infotableproxymodel.cpp @@ -1,30 +1,38 @@ #include "infotableproxymodel.hpp" -#include +#include +#include +#include + +#include +#include + +#include + +#include -#include "idtablebase.hpp" #include "columns.hpp" +#include "idtablebase.hpp" namespace { - QString toLower(const QString &str) + QString toLower(const QString& str) { return QString::fromUtf8(Misc::StringUtils::lowerCase(str.toUtf8().constData()).c_str()); } } -CSMWorld::InfoTableProxyModel::InfoTableProxyModel(CSMWorld::UniversalId::Type type, QObject *parent) - : IdTableProxyModel(parent), - mType(type), - mInfoColumnId(type == UniversalId::Type_TopicInfos ? Columns::ColumnId_Topic : - Columns::ColumnId_Journal), - mInfoColumnIndex(-1), - mLastAddedSourceRow(-1) +CSMWorld::InfoTableProxyModel::InfoTableProxyModel(CSMWorld::UniversalId::Type type, QObject* parent) + : IdTableProxyModel(parent) + , mType(type) + , mInfoColumnId(type == UniversalId::Type_TopicInfos ? Columns::ColumnId_Topic : Columns::ColumnId_Journal) + , mInfoColumnIndex(-1) + , mLastAddedSourceRow(-1) { Q_ASSERT(type == UniversalId::Type_TopicInfos || type == UniversalId::Type_JournalInfos); } -void CSMWorld::InfoTableProxyModel::setSourceModel(QAbstractItemModel *sourceModel) +void CSMWorld::InfoTableProxyModel::setSourceModel(QAbstractItemModel* sourceModel) { IdTableProxyModel::setSourceModel(sourceModel); @@ -35,7 +43,7 @@ void CSMWorld::InfoTableProxyModel::setSourceModel(QAbstractItemModel *sourceMod } } -bool CSMWorld::InfoTableProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +bool CSMWorld::InfoTableProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { Q_ASSERT(mSourceModel != nullptr); @@ -63,21 +71,21 @@ int CSMWorld::InfoTableProxyModel::getFirstInfoRow(int currentRow) const return mFirstRowCache[info]; } - while (--row >= 0 && - toLower(mSourceModel->data(mSourceModel->index(row, column)).toString()) == info); + while (--row >= 0 && toLower(mSourceModel->data(mSourceModel->index(row, column)).toString()) == info) + ; ++row; mFirstRowCache[info] = row; return row; } -void CSMWorld::InfoTableProxyModel::sourceRowsRemoved(const QModelIndex &/*parent*/, int /*start*/, int /*end*/) +void CSMWorld::InfoTableProxyModel::sourceRowsRemoved(const QModelIndex& /*parent*/, int /*start*/, int /*end*/) { refreshFilter(); mFirstRowCache.clear(); } -void CSMWorld::InfoTableProxyModel::sourceRowsInserted(const QModelIndex &parent, int /*start*/, int end) +void CSMWorld::InfoTableProxyModel::sourceRowsInserted(const QModelIndex& parent, int /*start*/, int end) { refreshFilter(); @@ -90,19 +98,18 @@ void CSMWorld::InfoTableProxyModel::sourceRowsInserted(const QModelIndex &parent } } -void CSMWorld::InfoTableProxyModel::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +void CSMWorld::InfoTableProxyModel::sourceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { refreshFilter(); - if (mLastAddedSourceRow != -1 && - topLeft.row() <= mLastAddedSourceRow && bottomRight.row() >= mLastAddedSourceRow) + if (mLastAddedSourceRow != -1 && topLeft.row() <= mLastAddedSourceRow && bottomRight.row() >= mLastAddedSourceRow) { - // Now the topic of the last added row is set, + // Now the topic of the last added row is set, // so we can re-sort the model to ensure the corrent position of this row int column = sortColumn(); Qt::SortOrder order = sortOrder(); sort(mInfoColumnIndex); // Restore the correct position of an added row - sort(column, order); // Restore the original sort order + sort(column, order); // Restore the original sort order emit rowAdded(getRecordId(mLastAddedSourceRow).toUtf8().constData()); // Make sure that we perform a re-sorting only in the first dataChanged() after a row insertion diff --git a/apps/opencs/model/world/infotableproxymodel.hpp b/apps/opencs/model/world/infotableproxymodel.hpp index 92afdabdc57..63f67e85910 100644 --- a/apps/opencs/model/world/infotableproxymodel.hpp +++ b/apps/opencs/model/world/infotableproxymodel.hpp @@ -3,42 +3,44 @@ #include -#include "idtableproxymodel.hpp" #include "columns.hpp" +#include "idtableproxymodel.hpp" #include "universalid.hpp" +class QAbstractItemModel; +class QModelIndex; +class QObject; + namespace CSMWorld { - class IdTableBase; - class InfoTableProxyModel : public IdTableProxyModel { - Q_OBJECT + Q_OBJECT - UniversalId::Type mType; - Columns::ColumnId mInfoColumnId; - ///< Contains ID for Topic or Journal ID - int mInfoColumnIndex; - int mLastAddedSourceRow; + UniversalId::Type mType; + Columns::ColumnId mInfoColumnId; + ///< Contains ID for Topic or Journal ID + int mInfoColumnIndex; + int mLastAddedSourceRow; - mutable QHash mFirstRowCache; + mutable QHash mFirstRowCache; - int getFirstInfoRow(int currentRow) const; - ///< Finds the first row with the same topic (journal entry) as in \a currentRow - ///< \a currentRow is a row of the source model. + int getFirstInfoRow(int currentRow) const; + ///< Finds the first row with the same topic (journal entry) as in \a currentRow + ///< \a currentRow is a row of the source model. - public: - InfoTableProxyModel(UniversalId::Type type, QObject *parent = nullptr); + public: + InfoTableProxyModel(UniversalId::Type type, QObject* parent = nullptr); - void setSourceModel(QAbstractItemModel *sourceModel) override; + void setSourceModel(QAbstractItemModel* sourceModel) override; - protected: - bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + protected: + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; - protected slots: - void sourceRowsInserted(const QModelIndex &parent, int start, int end) override; - void sourceRowsRemoved(const QModelIndex &parent, int start, int end) override; - void sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) override; + protected slots: + void sourceRowsInserted(const QModelIndex& parent, int start, int end) override; + void sourceRowsRemoved(const QModelIndex& parent, int start, int end) override; + void sourceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) override; }; } diff --git a/apps/opencs/model/world/land.cpp b/apps/opencs/model/world/land.cpp index bfa927444e4..0a8a4a2aa06 100644 --- a/apps/opencs/model/world/land.cpp +++ b/apps/opencs/model/world/land.cpp @@ -3,9 +3,16 @@ #include #include +#include + +namespace ESM +{ + class ESMReader; +} + namespace CSMWorld { - void Land::load(ESM::ESMReader &esm, bool &isDeleted) + void Land::load(ESM::ESMReader& esm, bool& isDeleted) { ESM::Land::load(esm, isDeleted); } @@ -24,7 +31,7 @@ namespace CSMWorld if (mid == std::string::npos || id[0] != '#') throw std::runtime_error("Invalid Land ID"); - x = std::stoi(id.substr(1, mid - 1)); - y = std::stoi(id.substr(mid + 1)); + x = Misc::StringUtils::toNumeric(id.substr(1, mid - 1), 0); + y = Misc::StringUtils::toNumeric(id.substr(mid + 1), 0); } } diff --git a/apps/opencs/model/world/land.hpp b/apps/opencs/model/world/land.hpp index e604f131194..69a3e1b12fb 100644 --- a/apps/opencs/model/world/land.hpp +++ b/apps/opencs/model/world/land.hpp @@ -3,7 +3,12 @@ #include -#include +#include + +namespace ESM +{ + class ESMReader; +} namespace CSMWorld { @@ -13,7 +18,7 @@ namespace CSMWorld struct Land : public ESM::Land { /// Loads the metadata and ID - void load (ESM::ESMReader &esm, bool &isDeleted); + void load(ESM::ESMReader& esm, bool& isDeleted); static std::string createUniqueRecordId(int x, int y); static void parseUniqueRecordId(const std::string& id, int& x, int& y); diff --git a/apps/opencs/model/world/landtexture.cpp b/apps/opencs/model/world/landtexture.cpp deleted file mode 100644 index 43deb64a47a..00000000000 --- a/apps/opencs/model/world/landtexture.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "landtexture.hpp" - -#include -#include - -#include - -namespace CSMWorld -{ - void LandTexture::load(ESM::ESMReader &esm, bool &isDeleted) - { - ESM::LandTexture::load(esm, isDeleted); - - mPluginIndex = esm.getIndex(); - } - - std::string LandTexture::createUniqueRecordId(int plugin, int index) - { - std::stringstream ss; - ss << 'L' << plugin << '#' << index; - return ss.str(); - } - - void LandTexture::parseUniqueRecordId(const std::string& id, int& plugin, int& index) - { - size_t middle = id.find('#'); - - if (middle == std::string::npos || id[0] != 'L') - throw std::runtime_error("Invalid LandTexture ID"); - - plugin = std::stoi(id.substr(1,middle-1)); - index = std::stoi(id.substr(middle+1)); - } -} diff --git a/apps/opencs/model/world/landtexture.hpp b/apps/opencs/model/world/landtexture.hpp deleted file mode 100644 index a7376438c18..00000000000 --- a/apps/opencs/model/world/landtexture.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef CSM_WORLD_LANDTEXTURE_H -#define CSM_WORLD_LANDTEXTURE_H - -#include - -#include - -namespace CSMWorld -{ - /// \brief Wrapper for LandTexture record, providing info which plugin the LandTexture was loaded from. - struct LandTexture : public ESM::LandTexture - { - int mPluginIndex; - - void load (ESM::ESMReader &esm, bool &isDeleted); - - /// Returns a string identifier that will be unique to any LandTexture. - static std::string createUniqueRecordId(int plugin, int index); - /// Deconstructs a unique string identifier into plugin and index. - static void parseUniqueRecordId(const std::string& id, int& plugin, int& index); - }; -} - -#endif diff --git a/apps/opencs/model/world/landtexturetableproxymodel.cpp b/apps/opencs/model/world/landtexturetableproxymodel.cpp index e064bbe8aac..438dba4678b 100644 --- a/apps/opencs/model/world/landtexturetableproxymodel.cpp +++ b/apps/opencs/model/world/landtexturetableproxymodel.cpp @@ -1,6 +1,6 @@ #include "landtexturetableproxymodel.hpp" -#include "idtable.hpp" +#include namespace CSMWorld { @@ -9,7 +9,7 @@ namespace CSMWorld { } - bool LandTextureTableProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const + bool LandTextureTableProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { return IdTableProxyModel::filterAcceptsRow(sourceRow, sourceParent); } diff --git a/apps/opencs/model/world/landtexturetableproxymodel.hpp b/apps/opencs/model/world/landtexturetableproxymodel.hpp index bbedecb5381..b53c35c235c 100644 --- a/apps/opencs/model/world/landtexturetableproxymodel.hpp +++ b/apps/opencs/model/world/landtexturetableproxymodel.hpp @@ -3,19 +3,20 @@ #include "idtableproxymodel.hpp" +class QModelIndex; +class QObject; + namespace CSMWorld { /// \brief Removes base records from filtered results. class LandTextureTableProxyModel : public IdTableProxyModel { - Q_OBJECT - public: - - LandTextureTableProxyModel(QObject* parent = nullptr); - - protected: + Q_OBJECT + public: + LandTextureTableProxyModel(QObject* parent = nullptr); - bool filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const override; + protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; }; } diff --git a/apps/opencs/model/world/metadata.cpp b/apps/opencs/model/world/metadata.cpp index b2fa3487cd7..c1c6325e3df 100644 --- a/apps/opencs/model/world/metadata.cpp +++ b/apps/opencs/model/world/metadata.cpp @@ -1,26 +1,28 @@ #include "metadata.hpp" -#include -#include -#include +#include +#include +#include void CSMWorld::MetaData::blank() { - mFormat = ESM::Header::CurrentFormat; + // ESM::Header::CurrentFormat is `1` but since new records are not yet used in opencs + // we use the format `0` for compatibility with old versions. + mFormatVersion = ESM::DefaultFormatVersion; mAuthor.clear(); mDescription.clear(); } -void CSMWorld::MetaData::load (ESM::ESMReader& esm) +void CSMWorld::MetaData::load(ESM::ESMReader& esm) { - mFormat = esm.getHeader().mFormat; + mFormatVersion = esm.getHeader().mFormatVersion; mAuthor = esm.getHeader().mData.author; mDescription = esm.getHeader().mData.desc; } -void CSMWorld::MetaData::save (ESM::ESMWriter& esm) const +void CSMWorld::MetaData::save(ESM::ESMWriter& esm) const { - esm.setFormat (mFormat); - esm.setAuthor (mAuthor); - esm.setDescription (mDescription); + esm.setFormatVersion(mFormatVersion); + esm.setAuthor(mAuthor); + esm.setDescription(mDescription); } diff --git a/apps/opencs/model/world/metadata.hpp b/apps/opencs/model/world/metadata.hpp index f8df2690ec0..963ca6453dc 100644 --- a/apps/opencs/model/world/metadata.hpp +++ b/apps/opencs/model/world/metadata.hpp @@ -1,6 +1,9 @@ #ifndef CSM_WOLRD_METADATA_H #define CSM_WOLRD_METADATA_H +#include +#include + #include namespace ESM @@ -13,16 +16,18 @@ namespace CSMWorld { struct MetaData { - std::string mId; + static constexpr std::string_view getRecordType() { return "MetaData"; } + + ESM::RefId mId; - int mFormat; + ESM::FormatVersion mFormatVersion; std::string mAuthor; std::string mDescription; void blank(); - void load (ESM::ESMReader& esm); - void save (ESM::ESMWriter& esm) const; + void load(ESM::ESMReader& esm); + void save(ESM::ESMWriter& esm) const; }; } diff --git a/apps/opencs/model/world/nestedcoladapterimp.cpp b/apps/opencs/model/world/nestedcoladapterimp.cpp index e8b4102d7cc..26c52ce50a2 100644 --- a/apps/opencs/model/world/nestedcoladapterimp.cpp +++ b/apps/opencs/model/world/nestedcoladapterimp.cpp @@ -1,17 +1,31 @@ #include "nestedcoladapterimp.hpp" -#include -#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include -#include "idcollection.hpp" -#include "pathgrid.hpp" #include "info.hpp" #include "infoselectwrapper.hpp" +#include "pathgrid.hpp" namespace CSMWorld { - PathgridPointListAdapter::PathgridPointListAdapter () {} - void PathgridPointListAdapter::addRow(Record& record, int position) const { Pathgrid pathgrid = record.get(); @@ -25,12 +39,11 @@ namespace CSMWorld point.mZ = 0; point.mAutogenerated = 0; point.mConnectionNum = 0; - point.mUnknown = 0; - points.insert(points.begin()+position, point); - pathgrid.mData.mS2 = pathgrid.mPoints.size(); + points.insert(points.begin() + position, point); + pathgrid.mData.mPoints = pathgrid.mPoints.size(); - record.setModified (pathgrid); + record.setModified(pathgrid); } void PathgridPointListAdapter::removeRow(Record& record, int rowToRemove) const @@ -39,25 +52,24 @@ namespace CSMWorld ESM::Pathgrid::PointList& points = pathgrid.mPoints; - if (rowToRemove < 0 || rowToRemove >= static_cast (points.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(points.size())) + throw std::runtime_error("index out of range"); // Do not remove dangling edges, does not work with current undo mechanism // Do not automatically adjust indices, what would be done with dangling edges? - points.erase(points.begin()+rowToRemove); - pathgrid.mData.mS2 = pathgrid.mPoints.size(); + points.erase(points.begin() + rowToRemove); + pathgrid.mData.mPoints = pathgrid.mPoints.size(); - record.setModified (pathgrid); + record.setModified(pathgrid); } - void PathgridPointListAdapter::setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const + void PathgridPointListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { Pathgrid pathgrid = record.get(); - pathgrid.mPoints = static_cast &>(nestedTable).mNestedTable; - pathgrid.mData.mS2 = pathgrid.mPoints.size(); + pathgrid.mPoints = static_cast&>(nestedTable).mNestedTable; + pathgrid.mData.mPoints = pathgrid.mPoints.size(); - record.setModified (pathgrid); + record.setModified(pathgrid); } NestedTableWrapperBase* PathgridPointListAdapter::table(const Record& record) const @@ -66,37 +78,49 @@ namespace CSMWorld return new NestedTableWrapper(record.get().mPoints); } - QVariant PathgridPointListAdapter::getData(const Record& record, - int subRowIndex, int subColIndex) const + QVariant PathgridPointListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Pathgrid::Point point = record.get().mPoints[subRowIndex]; switch (subColIndex) { - case 0: return subRowIndex; - case 1: return point.mX; - case 2: return point.mY; - case 3: return point.mZ; - default: throw std::runtime_error("Pathgrid point subcolumn index out of range"); + case 0: + return subRowIndex; + case 1: + return point.mX; + case 2: + return point.mY; + case 3: + return point.mZ; + default: + throw std::runtime_error("Pathgrid point subcolumn index out of range"); } } - void PathgridPointListAdapter::setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const + void PathgridPointListAdapter::setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { Pathgrid pathgrid = record.get(); ESM::Pathgrid::Point point = pathgrid.mPoints[subRowIndex]; switch (subColIndex) { - case 0: return; // return without saving - case 1: point.mX = value.toInt(); break; - case 2: point.mY = value.toInt(); break; - case 3: point.mZ = value.toInt(); break; - default: throw std::runtime_error("Pathgrid point subcolumn index out of range"); + case 0: + return; // return without saving + case 1: + point.mX = value.toInt(); + break; + case 2: + point.mY = value.toInt(); + break; + case 3: + point.mZ = value.toInt(); + break; + default: + throw std::runtime_error("Pathgrid point subcolumn index out of range"); } pathgrid.mPoints[subRowIndex] = point; - record.setModified (pathgrid); + record.setModified(pathgrid); } int PathgridPointListAdapter::getColumnsCount(const Record& record) const @@ -109,8 +133,6 @@ namespace CSMWorld return static_cast(record.get().mPoints.size()); } - PathgridEdgeListAdapter::PathgridEdgeListAdapter () {} - void PathgridEdgeListAdapter::addRow(Record& record, int position) const { Pathgrid pathgrid = record.get(); @@ -127,9 +149,9 @@ namespace CSMWorld // // Currently the code assumes that the end user to know what he/she is doing. // e.g. Edges come in pairs, from points a->b and b->a - edges.insert(edges.begin()+position, edge); + edges.insert(edges.begin() + position, edge); - record.setModified (pathgrid); + record.setModified(pathgrid); } void PathgridEdgeListAdapter::removeRow(Record& record, int rowToRemove) const @@ -138,23 +160,21 @@ namespace CSMWorld ESM::Pathgrid::EdgeList& edges = pathgrid.mEdges; - if (rowToRemove < 0 || rowToRemove >= static_cast (edges.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(edges.size())) + throw std::runtime_error("index out of range"); - edges.erase(edges.begin()+rowToRemove); + edges.erase(edges.begin() + rowToRemove); - record.setModified (pathgrid); + record.setModified(pathgrid); } - void PathgridEdgeListAdapter::setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const + void PathgridEdgeListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { Pathgrid pathgrid = record.get(); - pathgrid.mEdges = - static_cast &>(nestedTable).mNestedTable; + pathgrid.mEdges = static_cast&>(nestedTable).mNestedTable; - record.setModified (pathgrid); + record.setModified(pathgrid); } NestedTableWrapperBase* PathgridEdgeListAdapter::table(const Record& record) const @@ -163,44 +183,53 @@ namespace CSMWorld return new NestedTableWrapper(record.get().mEdges); } - QVariant PathgridEdgeListAdapter::getData(const Record& record, - int subRowIndex, int subColIndex) const + QVariant PathgridEdgeListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { Pathgrid pathgrid = record.get(); - if (subRowIndex < 0 || subRowIndex >= static_cast (pathgrid.mEdges.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(pathgrid.mEdges.size())) + throw std::runtime_error("index out of range"); ESM::Pathgrid::Edge edge = pathgrid.mEdges[subRowIndex]; switch (subColIndex) { - case 0: return subRowIndex; - case 1: return edge.mV0; - case 2: return edge.mV1; - default: throw std::runtime_error("Pathgrid edge subcolumn index out of range"); + case 0: + return subRowIndex; + case 1: + return static_cast(edge.mV0); + case 2: + return static_cast(edge.mV1); + default: + throw std::runtime_error("Pathgrid edge subcolumn index out of range"); } } - void PathgridEdgeListAdapter::setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const + void PathgridEdgeListAdapter::setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { Pathgrid pathgrid = record.get(); - if (subRowIndex < 0 || subRowIndex >= static_cast (pathgrid.mEdges.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(pathgrid.mEdges.size())) + throw std::runtime_error("index out of range"); ESM::Pathgrid::Edge edge = pathgrid.mEdges[subRowIndex]; switch (subColIndex) { - case 0: return; // return without saving - case 1: edge.mV0 = value.toInt(); break; - case 2: edge.mV1 = value.toInt(); break; - default: throw std::runtime_error("Pathgrid edge subcolumn index out of range"); + case 0: + return; // return without saving + case 1: + edge.mV0 = value.toInt(); + break; + case 2: + edge.mV1 = value.toInt(); + break; + default: + throw std::runtime_error("Pathgrid edge subcolumn index out of range"); } pathgrid.mEdges[subRowIndex] = edge; - record.setModified (pathgrid); + record.setModified(pathgrid); } int PathgridEdgeListAdapter::getColumnsCount(const Record& record) const @@ -213,96 +242,97 @@ namespace CSMWorld return static_cast(record.get().mEdges.size()); } - FactionReactionsAdapter::FactionReactionsAdapter () {} - void FactionReactionsAdapter::addRow(Record& record, int position) const { ESM::Faction faction = record.get(); - std::map& reactions = faction.mReactions; + std::map& reactions = faction.mReactions; // blank row - reactions.insert(std::make_pair("", 0)); + reactions.insert(std::make_pair(ESM::RefId(), 0)); - record.setModified (faction); + record.setModified(faction); } void FactionReactionsAdapter::removeRow(Record& record, int rowToRemove) const { ESM::Faction faction = record.get(); - std::map& reactions = faction.mReactions; + std::map& reactions = faction.mReactions; - if (rowToRemove < 0 || rowToRemove >= static_cast (reactions.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(reactions.size())) + throw std::runtime_error("index out of range"); // FIXME: how to ensure that the map entries correspond to table indicies? // WARNING: Assumed that the table view has the same order as std::map - std::map::iterator iter = reactions.begin(); - for(int i = 0; i < rowToRemove; ++i) + auto iter = reactions.begin(); + for (int i = 0; i < rowToRemove; ++i) ++iter; reactions.erase(iter); - record.setModified (faction); + record.setModified(faction); } - void FactionReactionsAdapter::setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const + void FactionReactionsAdapter::setTable( + Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Faction faction = record.get(); - faction.mReactions = - static_cast >&>(nestedTable).mNestedTable; + faction.mReactions + = static_cast>&>(nestedTable).mNestedTable; - record.setModified (faction); + record.setModified(faction); } NestedTableWrapperBase* FactionReactionsAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(record.get().mReactions); + return new NestedTableWrapper>(record.get().mReactions); } - QVariant FactionReactionsAdapter::getData(const Record& record, - int subRowIndex, int subColIndex) const + QVariant FactionReactionsAdapter::getData( + const Record& record, int subRowIndex, int subColIndex) const { ESM::Faction faction = record.get(); - std::map& reactions = faction.mReactions; + std::map& reactions = faction.mReactions; - if (subRowIndex < 0 || subRowIndex >= static_cast (reactions.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(reactions.size())) + throw std::runtime_error("index out of range"); // FIXME: how to ensure that the map entries correspond to table indicies? // WARNING: Assumed that the table view has the same order as std::map - std::map::const_iterator iter = reactions.begin(); - for(int i = 0; i < subRowIndex; ++i) + auto iter = reactions.begin(); + for (int i = 0; i < subRowIndex; ++i) ++iter; switch (subColIndex) { - case 0: return QString((*iter).first.c_str()); - case 1: return (*iter).second; - default: throw std::runtime_error("Faction reactions subcolumn index out of range"); + case 0: + return QString((*iter).first.getRefIdString().c_str()); + case 1: + return (*iter).second; + default: + throw std::runtime_error("Faction reactions subcolumn index out of range"); } } - void FactionReactionsAdapter::setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const + void FactionReactionsAdapter::setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Faction faction = record.get(); - std::map& reactions = faction.mReactions; + std::map& reactions = faction.mReactions; - if (subRowIndex < 0 || subRowIndex >= static_cast (reactions.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(reactions.size())) + throw std::runtime_error("index out of range"); // FIXME: how to ensure that the map entries correspond to table indicies? // WARNING: Assumed that the table view has the same order as std::map - std::map::iterator iter = reactions.begin(); - for(int i = 0; i < subRowIndex; ++i) + auto iter = reactions.begin(); + for (int i = 0; i < subRowIndex; ++i) ++iter; - std::string factionId = (*iter).first; + ESM::RefId factionId = (*iter).first; int reaction = (*iter).second; switch (subColIndex) @@ -310,7 +340,8 @@ namespace CSMWorld case 0: { reactions.erase(iter); - reactions.insert(std::make_pair(value.toString().toUtf8().constData(), reaction)); + reactions.insert( + std::make_pair(ESM::RefId::stringRefId(value.toString().toUtf8().constData()), reaction)); break; } case 1: @@ -318,10 +349,11 @@ namespace CSMWorld reactions[factionId] = value.toInt(); break; } - default: throw std::runtime_error("Faction reactions subcolumn index out of range"); + default: + throw std::runtime_error("Faction reactions subcolumn index out of range"); } - record.setModified (faction); + record.setModified(faction); } int FactionReactionsAdapter::getColumnsCount(const Record& record) const @@ -334,8 +366,6 @@ namespace CSMWorld return static_cast(record.get().mReactions.size()); } - RegionSoundListAdapter::RegionSoundListAdapter () {} - void RegionSoundListAdapter::addRow(Record& record, int position) const { ESM::Region region = record.get(); @@ -344,12 +374,12 @@ namespace CSMWorld // blank row ESM::Region::SoundRef soundRef; - soundRef.mSound.assign(""); + soundRef.mSound = ESM::RefId(); soundRef.mChance = 0; - soundList.insert(soundList.begin()+position, soundRef); + soundList.insert(soundList.begin() + position, soundRef); - record.setModified (region); + record.setModified(region); } void RegionSoundListAdapter::removeRow(Record& record, int rowToRemove) const @@ -358,76 +388,94 @@ namespace CSMWorld std::vector& soundList = region.mSoundList; - if (rowToRemove < 0 || rowToRemove >= static_cast (soundList.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(soundList.size())) + throw std::runtime_error("index out of range"); - soundList.erase(soundList.begin()+rowToRemove); + soundList.erase(soundList.begin() + rowToRemove); - record.setModified (region); + record.setModified(region); } - void RegionSoundListAdapter::setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const + void RegionSoundListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Region region = record.get(); - region.mSoundList = - static_cast >&>(nestedTable).mNestedTable; + region.mSoundList + = static_cast>&>(nestedTable).mNestedTable; - record.setModified (region); + record.setModified(region); } NestedTableWrapperBase* RegionSoundListAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(record.get().mSoundList); + return new NestedTableWrapper>(record.get().mSoundList); } - QVariant RegionSoundListAdapter::getData(const Record& record, - int subRowIndex, int subColIndex) const + QVariant RegionSoundListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { - ESM::Region region = record.get(); + const ESM::Region& region = record.get(); - std::vector& soundList = region.mSoundList; + const std::vector& soundList = region.mSoundList; - if (subRowIndex < 0 || subRowIndex >= static_cast (soundList.size())) - throw std::runtime_error ("index out of range"); + const size_t index = static_cast(subRowIndex); + if (subRowIndex < 0 || index >= soundList.size()) + throw std::runtime_error("index out of range"); - ESM::Region::SoundRef soundRef = soundList[subRowIndex]; + const ESM::Region::SoundRef& soundRef = soundList[subRowIndex]; switch (subColIndex) { - case 0: return QString(soundRef.mSound.c_str()); - case 1: return soundRef.mChance; - default: throw std::runtime_error("Region sounds subcolumn index out of range"); + case 0: + return QString(soundRef.mSound.getRefIdString().c_str()); + case 1: + return soundRef.mChance; + case 2: + { + float probability = 1.f; + for (size_t i = 0; i < index; ++i) + { + const float p = std::min(soundList[i].mChance / 100.f, 1.f); + probability *= 1.f - p; + } + probability *= std::min(soundRef.mChance / 100.f, 1.f) * 100.f; + return QString("%1%").arg(probability, 0, 'f', 2); + } + default: + throw std::runtime_error("Region sounds subcolumn index out of range"); } } - void RegionSoundListAdapter::setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const + void RegionSoundListAdapter::setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Region region = record.get(); std::vector& soundList = region.mSoundList; - if (subRowIndex < 0 || subRowIndex >= static_cast (soundList.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(soundList.size())) + throw std::runtime_error("index out of range"); ESM::Region::SoundRef soundRef = soundList[subRowIndex]; switch (subColIndex) { - case 0: soundRef.mSound.assign(value.toString().toUtf8().constData()); break; - case 1: soundRef.mChance = static_cast(value.toInt()); break; - default: throw std::runtime_error("Region sounds subcolumn index out of range"); + case 0: + soundRef.mSound = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); + break; + case 1: + soundRef.mChance = static_cast(value.toInt()); + break; + default: + throw std::runtime_error("Region sounds subcolumn index out of range"); } region.mSoundList[subRowIndex] = soundRef; - record.setModified (region); + record.setModified(region); } int RegionSoundListAdapter::getColumnsCount(const Record& record) const { - return 2; + return 3; } int RegionSoundListAdapter::getRowsCount(const Record& record) const @@ -435,31 +483,27 @@ namespace CSMWorld return static_cast(record.get().mSoundList.size()); } - InfoListAdapter::InfoListAdapter () {} - void InfoListAdapter::addRow(Record& record, int position) const { - throw std::logic_error ("cannot add a row to a fixed table"); + throw std::logic_error("cannot add a row to a fixed table"); } void InfoListAdapter::removeRow(Record& record, int rowToRemove) const { - throw std::logic_error ("cannot remove a row to a fixed table"); + throw std::logic_error("cannot remove a row to a fixed table"); } - void InfoListAdapter::setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const + void InfoListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } NestedTableWrapperBase* InfoListAdapter::table(const Record& record) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } - QVariant InfoListAdapter::getData(const Record& record, - int subRowIndex, int subColIndex) const + QVariant InfoListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { Info info = record.get(); @@ -469,8 +513,7 @@ namespace CSMWorld throw std::runtime_error("Trying to access non-existing column in the nested table!"); } - void InfoListAdapter::setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const + void InfoListAdapter::setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { Info info = record.get(); @@ -479,7 +522,7 @@ namespace CSMWorld else throw std::runtime_error("Trying to access non-existing column in the nested table!"); - record.setModified (info); + record.setModified(info); } int InfoListAdapter::getColumnsCount(const Record& record) const @@ -492,65 +535,59 @@ namespace CSMWorld return 1; // fixed at size 1 } - InfoConditionAdapter::InfoConditionAdapter () {} - void InfoConditionAdapter::addRow(Record& record, int position) const { Info info = record.get(); - std::vector& conditions = info.mSelects; + auto& conditions = info.mSelects; // default row - ESM::DialInfo::SelectStruct condStruct; - condStruct.mSelectRule = "01000"; - condStruct.mValue = ESM::Variant(); - condStruct.mValue.setType(ESM::VT_Int); + ESM::DialogueCondition condStruct; + condStruct.mIndex = conditions.size(); - conditions.insert(conditions.begin()+position, condStruct); + conditions.insert(conditions.begin() + position, condStruct); - record.setModified (info); + record.setModified(info); } void InfoConditionAdapter::removeRow(Record& record, int rowToRemove) const { Info info = record.get(); - std::vector& conditions = info.mSelects; + auto& conditions = info.mSelects; - if (rowToRemove < 0 || rowToRemove >= static_cast (conditions.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(conditions.size())) + throw std::runtime_error("index out of range"); - conditions.erase(conditions.begin()+rowToRemove); + conditions.erase(conditions.begin() + rowToRemove); - record.setModified (info); + record.setModified(info); } - void InfoConditionAdapter::setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const + void InfoConditionAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { Info info = record.get(); - info.mSelects = - static_cast >&>(nestedTable).mNestedTable; + info.mSelects + = static_cast>&>(nestedTable).mNestedTable; - record.setModified (info); + record.setModified(info); } NestedTableWrapperBase* InfoConditionAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(record.get().mSelects); + return new NestedTableWrapper>(record.get().mSelects); } - QVariant InfoConditionAdapter::getData(const Record& record, - int subRowIndex, int subColIndex) const + QVariant InfoConditionAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { Info info = record.get(); - std::vector& conditions = info.mSelects; + auto& conditions = info.mSelects; - if (subRowIndex < 0 || subRowIndex >= static_cast (conditions.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(conditions.size())) + throw std::runtime_error("index out of range"); ConstInfoSelectWrapper infoSelectWrapper(conditions[subRowIndex]); @@ -569,36 +606,26 @@ namespace CSMWorld } case 2: { - return infoSelectWrapper.getRelationType(); + return infoSelectWrapper.getRelationType() - ESM::DialogueCondition::Comp_Eq; } case 3: { - switch (infoSelectWrapper.getVariant().getType()) - { - case ESM::VT_Int: - { - return infoSelectWrapper.getVariant().getInteger(); - } - case ESM::VT_Float: - { - return infoSelectWrapper.getVariant().getFloat(); - } - default: return QVariant(); - } + return infoSelectWrapper.getValue(); } - default: throw std::runtime_error("Info condition subcolumn index out of range"); + default: + throw std::runtime_error("Info condition subcolumn index out of range"); } } - void InfoConditionAdapter::setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const + void InfoConditionAdapter::setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { Info info = record.get(); - std::vector& conditions = info.mSelects; + auto& conditions = info.mSelects; - if (subRowIndex < 0 || subRowIndex >= static_cast (conditions.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(conditions.size())) + throw std::runtime_error("index out of range"); InfoSelectWrapper infoSelectWrapper(conditions[subRowIndex]); bool conversionResult = false; @@ -607,27 +634,18 @@ namespace CSMWorld { case 0: // Function { - infoSelectWrapper.setFunctionName(static_cast(value.toInt())); - - if (infoSelectWrapper.getComparisonType() != ConstInfoSelectWrapper::Comparison_Numeric && - infoSelectWrapper.getVariant().getType() != ESM::VT_Int) - { - infoSelectWrapper.getVariant().setType(ESM::VT_Int); - } - - infoSelectWrapper.update(); + infoSelectWrapper.setFunctionName(static_cast(value.toInt())); break; } case 1: // Variable { infoSelectWrapper.setVariableName(value.toString().toUtf8().constData()); - infoSelectWrapper.update(); break; } case 2: // Relation { - infoSelectWrapper.setRelationType(static_cast(value.toInt())); - infoSelectWrapper.update(); + infoSelectWrapper.setRelationType( + static_cast(value.toInt() + ESM::DialogueCondition::Comp_Eq)); break; } case 3: // Value @@ -639,13 +657,11 @@ namespace CSMWorld // QVariant seems to have issues converting 0 if ((value.toInt(&conversionResult) && conversionResult) || value.toString().compare("0") == 0) { - infoSelectWrapper.getVariant().setType(ESM::VT_Int); - infoSelectWrapper.getVariant().setInteger(value.toInt()); + infoSelectWrapper.setValue(value.toInt()); } else if (value.toFloat(&conversionResult) && conversionResult) { - infoSelectWrapper.getVariant().setType(ESM::VT_Float); - infoSelectWrapper.getVariant().setFloat(value.toFloat()); + infoSelectWrapper.setValue(value.toFloat()); } break; } @@ -654,19 +670,20 @@ namespace CSMWorld { if ((value.toInt(&conversionResult) && conversionResult) || value.toString().compare("0") == 0) { - infoSelectWrapper.getVariant().setType(ESM::VT_Int); - infoSelectWrapper.getVariant().setInteger(value.toInt()); + infoSelectWrapper.setValue(value.toInt()); } break; } - default: break; + default: + break; } break; } - default: throw std::runtime_error("Info condition subcolumn index out of range"); + default: + throw std::runtime_error("Info condition subcolumn index out of range"); } - record.setModified (info); + record.setModified(info); } int InfoConditionAdapter::getColumnsCount(const Record& record) const @@ -679,8 +696,6 @@ namespace CSMWorld return static_cast(record.get().mSelects.size()); } - RaceAttributeAdapter::RaceAttributeAdapter () {} - void RaceAttributeAdapter::addRow(Record& record, int position) const { // Do nothing, this table cannot be changed by the user @@ -691,15 +706,14 @@ namespace CSMWorld // Do nothing, this table cannot be changed by the user } - void RaceAttributeAdapter::setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const + void RaceAttributeAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Race race = record.get(); - race.mData = - static_cast >&>(nestedTable).mNestedTable.at(0); + race.mData = static_cast>&>(nestedTable) + .mNestedTable.at(0); - record.setModified (race); + record.setModified(race); } NestedTableWrapperBase* RaceAttributeAdapter::table(const Record& record) const @@ -707,43 +721,52 @@ namespace CSMWorld std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(wrap); + return new NestedTableWrapper>(wrap); } - QVariant RaceAttributeAdapter::getData(const Record& record, - int subRowIndex, int subColIndex) const + QVariant RaceAttributeAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); - - if (subRowIndex < 0 || subRowIndex >= ESM::Attribute::Length) - throw std::runtime_error ("index out of range"); + ESM::RefId attribute = ESM::Attribute::indexToRefId(subRowIndex); + if (attribute.empty()) + throw std::runtime_error("index out of range"); switch (subColIndex) { - case 0: return subRowIndex; - case 1: return race.mData.mAttributeValues[subRowIndex].mMale; - case 2: return race.mData.mAttributeValues[subRowIndex].mFemale; - default: throw std::runtime_error("Race Attribute subcolumn index out of range"); + case 0: + return subRowIndex; + case 1: + return race.mData.getAttribute(attribute, true); + case 2: + return race.mData.getAttribute(attribute, false); + default: + throw std::runtime_error("Race Attribute subcolumn index out of range"); } } - void RaceAttributeAdapter::setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const + void RaceAttributeAdapter::setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); - - if (subRowIndex < 0 || subRowIndex >= ESM::Attribute::Length) - throw std::runtime_error ("index out of range"); + ESM::RefId attribute = ESM::Attribute::indexToRefId(subRowIndex); + if (attribute.empty()) + throw std::runtime_error("index out of range"); switch (subColIndex) { - case 0: return; // throw an exception here? - case 1: race.mData.mAttributeValues[subRowIndex].mMale = value.toInt(); break; - case 2: race.mData.mAttributeValues[subRowIndex].mFemale = value.toInt(); break; - default: throw std::runtime_error("Race Attribute subcolumn index out of range"); + case 0: + return; // throw an exception here? + case 1: + race.mData.setAttribute(attribute, true, value.toInt()); + break; + case 2: + race.mData.setAttribute(attribute, false, value.toInt()); + break; + default: + throw std::runtime_error("Race Attribute subcolumn index out of range"); } - record.setModified (race); + record.setModified(race); } int RaceAttributeAdapter::getColumnsCount(const Record& record) const @@ -756,8 +779,6 @@ namespace CSMWorld return ESM::Attribute::Length; // there are 8 attributes } - RaceSkillsBonusAdapter::RaceSkillsBonusAdapter () {} - void RaceSkillsBonusAdapter::addRow(Record& record, int position) const { // Do nothing, this table cannot be changed by the user @@ -768,15 +789,14 @@ namespace CSMWorld // Do nothing, this table cannot be changed by the user } - void RaceSkillsBonusAdapter::setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const + void RaceSkillsBonusAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Race race = record.get(); - race.mData = - static_cast >&>(nestedTable).mNestedTable.at(0); + race.mData = static_cast>&>(nestedTable) + .mNestedTable.at(0); - record.setModified (race); + record.setModified(race); } NestedTableWrapperBase* RaceSkillsBonusAdapter::table(const Record& record) const @@ -784,41 +804,48 @@ namespace CSMWorld std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(wrap); + return new NestedTableWrapper>(wrap); } - QVariant RaceSkillsBonusAdapter::getData(const Record& record, - int subRowIndex, int subColIndex) const + QVariant RaceSkillsBonusAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); - if (subRowIndex < 0 || subRowIndex >= static_cast(sizeof(race.mData.mBonus)/sizeof(race.mData.mBonus[0]))) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || static_cast(subRowIndex) >= race.mData.mBonus.size()) + throw std::runtime_error("index out of range"); switch (subColIndex) { - case 0: return race.mData.mBonus[subRowIndex].mSkill; // can be -1 - case 1: return race.mData.mBonus[subRowIndex].mBonus; - default: throw std::runtime_error("Race skill bonus subcolumn index out of range"); + case 0: + return race.mData.mBonus[subRowIndex].mSkill; // can be -1 + case 1: + return race.mData.mBonus[subRowIndex].mBonus; + default: + throw std::runtime_error("Race skill bonus subcolumn index out of range"); } } - void RaceSkillsBonusAdapter::setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const + void RaceSkillsBonusAdapter::setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); - if (subRowIndex < 0 || subRowIndex >= static_cast(sizeof(race.mData.mBonus)/sizeof(race.mData.mBonus[0]))) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || static_cast(subRowIndex) >= race.mData.mBonus.size()) + throw std::runtime_error("index out of range"); switch (subColIndex) { - case 0: race.mData.mBonus[subRowIndex].mSkill = value.toInt(); break; // can be -1 - case 1: race.mData.mBonus[subRowIndex].mBonus = value.toInt(); break; - default: throw std::runtime_error("Race skill bonus subcolumn index out of range"); + case 0: + race.mData.mBonus[subRowIndex].mSkill = value.toInt(); + break; // can be -1 + case 1: + race.mData.mBonus[subRowIndex].mBonus = value.toInt(); + break; + default: + throw std::runtime_error("Race skill bonus subcolumn index out of range"); } - record.setModified (race); + record.setModified(race); } int RaceSkillsBonusAdapter::getColumnsCount(const Record& record) const @@ -828,35 +855,30 @@ namespace CSMWorld int RaceSkillsBonusAdapter::getRowsCount(const Record& record) const { - // there are 7 skill bonuses - return static_cast(sizeof(record.get().mData.mBonus)/sizeof(record.get().mData.mBonus[0])); + return record.get().mData.mBonus.size(); } - CellListAdapter::CellListAdapter () {} - void CellListAdapter::addRow(Record& record, int position) const { - throw std::logic_error ("cannot add a row to a fixed table"); + throw std::logic_error("cannot add a row to a fixed table"); } void CellListAdapter::removeRow(Record& record, int rowToRemove) const { - throw std::logic_error ("cannot remove a row to a fixed table"); + throw std::logic_error("cannot remove a row to a fixed table"); } - void CellListAdapter::setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const + void CellListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } NestedTableWrapperBase* CellListAdapter::table(const Record& record) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } - QVariant CellListAdapter::getData(const Record& record, - int subRowIndex, int subColIndex) const + QVariant CellListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { CSMWorld::Cell cell = record.get(); @@ -866,34 +888,36 @@ namespace CSMWorld switch (subColIndex) { - case 0: return isInterior; + case 0: + return isInterior; // While the ambient information is not necessarily valid if the subrecord wasn't loaded, // the user should still be allowed to edit it - case 1: return (isInterior && !behaveLikeExterior) ? - cell.mAmbi.mAmbient : QVariant(QVariant::UserType); - case 2: return (isInterior && !behaveLikeExterior) ? - cell.mAmbi.mSunlight : QVariant(QVariant::UserType); - case 3: return (isInterior && !behaveLikeExterior) ? - cell.mAmbi.mFog : QVariant(QVariant::UserType); - case 4: return (isInterior && !behaveLikeExterior) ? - cell.mAmbi.mFogDensity : QVariant(QVariant::UserType); + case 1: + return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mAmbient : DisableTag::getVariant(); + case 2: + return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mSunlight : DisableTag::getVariant(); + case 3: + return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mFog : DisableTag::getVariant(); + case 4: + return (isInterior && !behaveLikeExterior) ? cell.mAmbi.mFogDensity : DisableTag::getVariant(); case 5: { if (isInterior && interiorWater) return cell.mWater; else - return QVariant(QVariant::UserType); + return DisableTag::getVariant(); } - case 6: return isInterior ? - QVariant(QVariant::UserType) : cell.mMapColor; // TODO: how to select? - //case 7: return isInterior ? - //behaveLikeExterior : QVariant(QVariant::UserType); - default: throw std::runtime_error("Cell subcolumn index out of range"); + case 6: + return isInterior ? DisableTag::getVariant() : cell.mMapColor; // TODO: how to select? + // case 7: return isInterior ? + // behaveLikeExterior : DisableTag::getVariant(); + default: + throw std::runtime_error("Cell subcolumn index out of range"); } } - void CellListAdapter::setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const + void CellListAdapter::setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { CSMWorld::Cell cell = record.get(); @@ -958,7 +982,10 @@ namespace CSMWorld case 5: { if (isInterior && interiorWater) + { cell.mWater = value.toFloat(); + cell.setHasWaterHeightSub(true); + } else return; // return without saving break; @@ -988,10 +1015,11 @@ namespace CSMWorld break; } #endif - default: throw std::runtime_error("Cell subcolumn index out of range"); + default: + throw std::runtime_error("Cell subcolumn index out of range"); } - record.setModified (cell); + record.setModified(cell); } int CellListAdapter::getColumnsCount(const Record& record) const @@ -1004,42 +1032,30 @@ namespace CSMWorld return 1; // fixed at size 1 } - RegionWeatherAdapter::RegionWeatherAdapter () {} - void RegionWeatherAdapter::addRow(Record& record, int position) const { - throw std::logic_error ("cannot add a row to a fixed table"); + throw std::logic_error("cannot add a row to a fixed table"); } void RegionWeatherAdapter::removeRow(Record& record, int rowToRemove) const { - throw std::logic_error ("cannot remove a row from a fixed table"); + throw std::logic_error("cannot remove a row from a fixed table"); } void RegionWeatherAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } NestedTableWrapperBase* RegionWeatherAdapter::table(const Record& record) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } QVariant RegionWeatherAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { - const char* WeatherNames[] = { - "Clear", - "Cloudy", - "Fog", - "Overcast", - "Rain", - "Thunder", - "Ash", - "Blight", - "Snow", - "Blizzard" - }; + const char* WeatherNames[] + = { "Clear", "Cloudy", "Fog", "Overcast", "Rain", "Thunder", "Ash", "Blight", "Snow", "Blizzard" }; const ESM::Region& region = record.get(); @@ -1049,49 +1065,24 @@ namespace CSMWorld } else if (subColIndex == 1) { - switch (subRowIndex) - { - case 0: return region.mData.mClear; - case 1: return region.mData.mCloudy; - case 2: return region.mData.mFoggy; - case 3: return region.mData.mOvercast; - case 4: return region.mData.mRain; - case 5: return region.mData.mThunder; - case 6: return region.mData.mAsh; - case 7: return region.mData.mBlight; - case 8: return region.mData.mA; // Snow - case 9: return region.mData.mB; // Blizzard - default: break; - } + if (subRowIndex >= 0 && static_cast(subRowIndex) < region.mData.mProbabilities.size()) + return region.mData.mProbabilities[subRowIndex]; } throw std::runtime_error("index out of range"); } - void RegionWeatherAdapter::setData(Record& record, const QVariant& value, int subRowIndex, - int subColIndex) const + void RegionWeatherAdapter::setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Region region = record.get(); - unsigned char chance = static_cast(value.toInt()); + uint8_t chance = static_cast(value.toInt()); if (subColIndex == 1) { - switch (subRowIndex) - { - case 0: region.mData.mClear = chance; break; - case 1: region.mData.mCloudy = chance; break; - case 2: region.mData.mFoggy = chance; break; - case 3: region.mData.mOvercast = chance; break; - case 4: region.mData.mRain = chance; break; - case 5: region.mData.mThunder = chance; break; - case 6: region.mData.mAsh = chance; break; - case 7: region.mData.mBlight = chance; break; - case 8: region.mData.mA = chance; break; - case 9: region.mData.mB = chance; break; - default: throw std::runtime_error("index out of range"); - } + region.mData.mProbabilities.at(subRowIndex) = chance; - record.setModified (region); + record.setModified(region); } } @@ -1105,73 +1096,83 @@ namespace CSMWorld return 10; } - FactionRanksAdapter::FactionRanksAdapter () {} - void FactionRanksAdapter::addRow(Record& record, int position) const { - throw std::logic_error ("cannot add a row to a fixed table"); + throw std::logic_error("cannot add a row to a fixed table"); } void FactionRanksAdapter::removeRow(Record& record, int rowToRemove) const { - throw std::logic_error ("cannot remove a row from a fixed table"); + throw std::logic_error("cannot remove a row from a fixed table"); } - void FactionRanksAdapter::setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const + void FactionRanksAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } NestedTableWrapperBase* FactionRanksAdapter::table(const Record& record) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } - QVariant FactionRanksAdapter::getData(const Record& record, - int subRowIndex, int subColIndex) const + QVariant FactionRanksAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { - ESM::Faction faction = record.get(); + const ESM::Faction& faction = record.get(); - if (subRowIndex < 0 || subRowIndex >= static_cast(sizeof(faction.mData.mRankData)/sizeof(faction.mData.mRankData[0]))) - throw std::runtime_error ("index out of range"); - - auto& rankData = faction.mData.mRankData[subRowIndex]; + const auto& rankData = faction.mData.mRankData.at(subRowIndex); switch (subColIndex) { - case 0: return QString(faction.mRanks[subRowIndex].c_str()); - case 1: return rankData.mAttribute1; - case 2: return rankData.mAttribute2; - case 3: return rankData.mPrimarySkill; - case 4: return rankData.mFavouredSkill; - case 5: return rankData.mFactReaction; - default: throw std::runtime_error("Rank subcolumn index out of range"); + case 0: + return QString(faction.mRanks[subRowIndex].c_str()); + case 1: + return rankData.mAttribute1; + case 2: + return rankData.mAttribute2; + case 3: + return rankData.mPrimarySkill; + case 4: + return rankData.mFavouredSkill; + case 5: + return rankData.mFactReaction; + default: + throw std::runtime_error("Rank subcolumn index out of range"); } } - void FactionRanksAdapter::setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const + void FactionRanksAdapter::setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Faction faction = record.get(); - if (subRowIndex < 0 || subRowIndex >= static_cast(sizeof(faction.mData.mRankData)/sizeof(faction.mData.mRankData[0]))) - throw std::runtime_error ("index out of range"); - - auto& rankData = faction.mData.mRankData[subRowIndex]; + auto& rankData = faction.mData.mRankData.at(subRowIndex); switch (subColIndex) { - case 0: faction.mRanks[subRowIndex] = value.toString().toUtf8().constData(); break; - case 1: rankData.mAttribute1 = value.toInt(); break; - case 2: rankData.mAttribute2 = value.toInt(); break; - case 3: rankData.mPrimarySkill = value.toInt(); break; - case 4: rankData.mFavouredSkill = value.toInt(); break; - case 5: rankData.mFactReaction = value.toInt(); break; - default: throw std::runtime_error("Rank index out of range"); + case 0: + faction.mRanks[subRowIndex] = value.toString().toUtf8().constData(); + break; + case 1: + rankData.mAttribute1 = value.toInt(); + break; + case 2: + rankData.mAttribute2 = value.toInt(); + break; + case 3: + rankData.mPrimarySkill = value.toInt(); + break; + case 4: + rankData.mFavouredSkill = value.toInt(); + break; + case 5: + rankData.mFactReaction = value.toInt(); + break; + default: + throw std::runtime_error("Rank index out of range"); } - record.setModified (faction); + record.setModified(faction); } int FactionRanksAdapter::getColumnsCount(const Record& record) const diff --git a/apps/opencs/model/world/nestedcoladapterimp.hpp b/apps/opencs/model/world/nestedcoladapterimp.hpp index 54780d290e1..f5c5501889c 100644 --- a/apps/opencs/model/world/nestedcoladapterimp.hpp +++ b/apps/opencs/model/world/nestedcoladapterimp.hpp @@ -1,49 +1,52 @@ #ifndef CSM_WOLRD_NESTEDCOLADAPTERIMP_H #define CSM_WOLRD_NESTEDCOLADAPTERIMP_H +#include #include -#include -#include -#include // for converting magic effect id to string & back -#include // for converting skill names -#include // for converting attributes -#include +#include +#include +#include +#include + +#include +#include // for converting magic effect id to string & back #include "nestedcolumnadapter.hpp" #include "nestedtablewrapper.hpp" -#include "cell.hpp" namespace ESM { struct Faction; struct Region; + struct Race; } namespace CSMWorld { struct Pathgrid; struct Info; + struct Cell; + + template + struct Record; class PathgridPointListAdapter : public NestedColumnAdapter { public: - PathgridPointListAdapter (); + PathgridPointListAdapter() = default; void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; - void setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const override; + void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; - QVariant getData(const Record& record, - int subRowIndex, int subColIndex) const override; + QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; - void setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const override; + void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; @@ -53,22 +56,19 @@ namespace CSMWorld class PathgridEdgeListAdapter : public NestedColumnAdapter { public: - PathgridEdgeListAdapter (); + PathgridEdgeListAdapter() = default; void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; - void setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const override; + void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; - QVariant getData(const Record& record, - int subRowIndex, int subColIndex) const override; + QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; - void setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const override; + void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; @@ -78,22 +78,20 @@ namespace CSMWorld class FactionReactionsAdapter : public NestedColumnAdapter { public: - FactionReactionsAdapter (); + FactionReactionsAdapter() = default; void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; - void setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const override; + void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; - QVariant getData(const Record& record, - int subRowIndex, int subColIndex) const override; + QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; - void setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const override; + void setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; @@ -103,22 +101,20 @@ namespace CSMWorld class FactionRanksAdapter : public NestedColumnAdapter { public: - FactionRanksAdapter (); + FactionRanksAdapter() = default; void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; - void setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const override; + void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; - QVariant getData(const Record& record, - int subRowIndex, int subColIndex) const override; + QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; - void setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const override; + void setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; @@ -128,121 +124,120 @@ namespace CSMWorld class RegionSoundListAdapter : public NestedColumnAdapter { public: - RegionSoundListAdapter (); + RegionSoundListAdapter() = default; void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; - void setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const override; + void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; - QVariant getData(const Record& record, - int subRowIndex, int subColIndex) const override; + QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; - void setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const override; + void setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; - template + template class SpellListAdapter : public NestedColumnAdapter { public: - SpellListAdapter () {} + SpellListAdapter() = default; void addRow(Record& record, int position) const override { ESXRecordT raceOrBthSgn = record.get(); - std::vector& spells = raceOrBthSgn.mPowers.mList; + std::vector& spells = raceOrBthSgn.mPowers.mList; // blank row - std::string spell = ""; + ESM::RefId spell; - spells.insert(spells.begin()+position, spell); + spells.insert(spells.begin() + position, spell); - record.setModified (raceOrBthSgn); + record.setModified(raceOrBthSgn); } void removeRow(Record& record, int rowToRemove) const override { ESXRecordT raceOrBthSgn = record.get(); - std::vector& spells = raceOrBthSgn.mPowers.mList; + std::vector& spells = raceOrBthSgn.mPowers.mList; - if (rowToRemove < 0 || rowToRemove >= static_cast (spells.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(spells.size())) + throw std::runtime_error("index out of range"); - spells.erase(spells.begin()+rowToRemove); + spells.erase(spells.begin() + rowToRemove); - record.setModified (raceOrBthSgn); + record.setModified(raceOrBthSgn); } void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override { ESXRecordT raceOrBthSgn = record.get(); - raceOrBthSgn.mPowers.mList = - static_cast >&>(nestedTable).mNestedTable; + raceOrBthSgn.mPowers.mList + = static_cast>&>(nestedTable).mNestedTable; - record.setModified (raceOrBthSgn); + record.setModified(raceOrBthSgn); } NestedTableWrapperBase* table(const Record& record) const override { // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(record.get().mPowers.mList); + return new NestedTableWrapper>(record.get().mPowers.mList); } QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override { ESXRecordT raceOrBthSgn = record.get(); - std::vector& spells = raceOrBthSgn.mPowers.mList; + std::vector& spells = raceOrBthSgn.mPowers.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (spells.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(spells.size())) + throw std::runtime_error("index out of range"); - std::string spell = spells[subRowIndex]; + ESM::RefId spell = spells[subRowIndex]; switch (subColIndex) { - case 0: return QString(spell.c_str()); - default: throw std::runtime_error("Spells subcolumn index out of range"); + case 0: + return QString(spell.getRefIdString().c_str()); + default: + throw std::runtime_error("Spells subcolumn index out of range"); } } - void setData(Record& record, const QVariant& value, - int subRowIndex, int subColIndex) const override + void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override { ESXRecordT raceOrBthSgn = record.get(); - std::vector& spells = raceOrBthSgn.mPowers.mList; + std::vector& spells = raceOrBthSgn.mPowers.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (spells.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(spells.size())) + throw std::runtime_error("index out of range"); - std::string spell = spells[subRowIndex]; + ESM::RefId spell = spells[subRowIndex]; switch (subColIndex) { - case 0: spell = value.toString().toUtf8().constData(); break; - default: throw std::runtime_error("Spells subcolumn index out of range"); + case 0: + spell = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); + break; + default: + throw std::runtime_error("Spells subcolumn index out of range"); } raceOrBthSgn.mPowers.mList[subRowIndex] = spell; - record.setModified (raceOrBthSgn); + record.setModified(raceOrBthSgn); } - int getColumnsCount(const Record& record) const override - { - return 1; - } + int getColumnsCount(const Record& record) const override { return 1; } int getRowsCount(const Record& record) const override { @@ -250,83 +245,81 @@ namespace CSMWorld } }; - template + template class EffectsListAdapter : public NestedColumnAdapter { public: - EffectsListAdapter () {} + EffectsListAdapter() = default; void addRow(Record& record, int position) const override { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; // blank row - ESM::ENAMstruct effect; - effect.mEffectID = 0; - effect.mSkill = -1; - effect.mAttribute = -1; - effect.mRange = 0; - effect.mArea = 0; - effect.mDuration = 0; - effect.mMagnMin = 0; - effect.mMagnMax = 0; - - effectsList.insert(effectsList.begin()+position, effect); - - record.setModified (magic); + ESM::IndexedENAMstruct effect; + effect.mIndex = position; + effect.mData.mEffectID = 0; + effect.mData.mSkill = -1; + effect.mData.mAttribute = -1; + effect.mData.mRange = 0; + effect.mData.mArea = 0; + effect.mData.mDuration = 0; + effect.mData.mMagnMin = 0; + effect.mData.mMagnMax = 0; + + effectsList.insert(effectsList.begin() + position, effect); + magic.mEffects.updateIndexes(); + + record.setModified(magic); } void removeRow(Record& record, int rowToRemove) const override { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; - if (rowToRemove < 0 || rowToRemove >= static_cast (effectsList.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(effectsList.size())) + throw std::runtime_error("index out of range"); - effectsList.erase(effectsList.begin()+rowToRemove); + effectsList.erase(effectsList.begin() + rowToRemove); + magic.mEffects.updateIndexes(); - record.setModified (magic); + record.setModified(magic); } void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override { ESXRecordT magic = record.get(); - magic.mEffects.mList = - static_cast >&>(nestedTable).mNestedTable; + magic.mEffects.mList + = static_cast>&>(nestedTable).mNestedTable; - record.setModified (magic); + record.setModified(magic); } NestedTableWrapperBase* table(const Record& record) const override { // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(record.get().mEffects.mList); + return new NestedTableWrapper>(record.get().mEffects.mList); } QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (effectsList.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(effectsList.size())) + throw std::runtime_error("index out of range"); - ESM::ENAMstruct effect = effectsList[subRowIndex]; + ESM::ENAMstruct effect = effectsList[subRowIndex].mData; switch (subColIndex) { case 0: - { - if (effect.mEffectID >=0 && effect.mEffectID < ESM::MagicEffect::Length) - return effect.mEffectID; - else - throw std::runtime_error("Magic effects ID unexpected value"); - } + return effect.mEffectID; case 1: { switch (effect.mEffectID) @@ -336,7 +329,7 @@ namespace CSMWorld case ESM::MagicEffect::RestoreSkill: case ESM::MagicEffect::FortifySkill: case ESM::MagicEffect::AbsorbSkill: - return effect.mSkill; + return effect.mSkill; default: return QVariant(); } @@ -350,42 +343,61 @@ namespace CSMWorld case ESM::MagicEffect::RestoreAttribute: case ESM::MagicEffect::FortifyAttribute: case ESM::MagicEffect::AbsorbAttribute: - return effect.mAttribute; + return effect.mAttribute; default: return QVariant(); } } case 3: - { - if (effect.mRange >=0 && effect.mRange <=2) - return effect.mRange; - else - throw std::runtime_error("Magic effects range unexpected value"); - } - case 4: return effect.mArea; - case 5: return effect.mDuration; - case 6: return effect.mMagnMin; - case 7: return effect.mMagnMax; - default: throw std::runtime_error("Magic Effects subcolumn index out of range"); + return effect.mRange; + case 4: + return effect.mArea; + case 5: + return effect.mDuration; + case 6: + return effect.mMagnMin; + case 7: + return effect.mMagnMax; + default: + throw std::runtime_error("Magic Effects subcolumn index out of range"); } } - void setData(Record& record, const QVariant& value, - int subRowIndex, int subColIndex) const override + void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (effectsList.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(effectsList.size())) + throw std::runtime_error("index out of range"); - ESM::ENAMstruct effect = effectsList[subRowIndex]; + ESM::ENAMstruct effect = effectsList[subRowIndex].mData; switch (subColIndex) { case 0: { effect.mEffectID = static_cast(value.toInt()); + switch (effect.mEffectID) + { + case ESM::MagicEffect::DrainSkill: + case ESM::MagicEffect::DamageSkill: + case ESM::MagicEffect::RestoreSkill: + case ESM::MagicEffect::FortifySkill: + case ESM::MagicEffect::AbsorbSkill: + effect.mAttribute = -1; + break; + case ESM::MagicEffect::DrainAttribute: + case ESM::MagicEffect::DamageAttribute: + case ESM::MagicEffect::RestoreAttribute: + case ESM::MagicEffect::FortifyAttribute: + case ESM::MagicEffect::AbsorbAttribute: + effect.mSkill = -1; + break; + default: + effect.mSkill = -1; + effect.mAttribute = -1; + } break; } case 1: @@ -403,22 +415,28 @@ namespace CSMWorld effect.mRange = value.toInt(); break; } - case 4: effect.mArea = value.toInt(); break; - case 5: effect.mDuration = value.toInt(); break; - case 6: effect.mMagnMin = value.toInt(); break; - case 7: effect.mMagnMax = value.toInt(); break; - default: throw std::runtime_error("Magic Effects subcolumn index out of range"); + case 4: + effect.mArea = value.toInt(); + break; + case 5: + effect.mDuration = value.toInt(); + break; + case 6: + effect.mMagnMin = value.toInt(); + break; + case 7: + effect.mMagnMax = value.toInt(); + break; + default: + throw std::runtime_error("Magic Effects subcolumn index out of range"); } - magic.mEffects.mList[subRowIndex] = effect; + magic.mEffects.mList[subRowIndex].mData = effect; - record.setModified (magic); + record.setModified(magic); } - int getColumnsCount(const Record& record) const override - { - return 8; - } + int getColumnsCount(const Record& record) const override { return 8; } int getRowsCount(const Record& record) const override { @@ -429,22 +447,19 @@ namespace CSMWorld class InfoListAdapter : public NestedColumnAdapter { public: - InfoListAdapter (); + InfoListAdapter() = default; void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; - void setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const override; + void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; - QVariant getData(const Record& record, - int subRowIndex, int subColIndex) const override; + QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; - void setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const override; + void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; @@ -454,22 +469,19 @@ namespace CSMWorld class InfoConditionAdapter : public NestedColumnAdapter { public: - InfoConditionAdapter (); + InfoConditionAdapter() = default; void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; - void setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const override; + void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; - QVariant getData(const Record& record, - int subRowIndex, int subColIndex) const override; + QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; - void setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const override; + void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; @@ -479,22 +491,19 @@ namespace CSMWorld class RaceAttributeAdapter : public NestedColumnAdapter { public: - RaceAttributeAdapter (); + RaceAttributeAdapter() = default; void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; - void setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const override; + void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; - QVariant getData(const Record& record, - int subRowIndex, int subColIndex) const override; + QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; - void setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const override; + void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; @@ -504,22 +513,19 @@ namespace CSMWorld class RaceSkillsBonusAdapter : public NestedColumnAdapter { public: - RaceSkillsBonusAdapter (); + RaceSkillsBonusAdapter() = default; void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; - void setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const override; + void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; - QVariant getData(const Record& record, - int subRowIndex, int subColIndex) const override; + QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; - void setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const override; + void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; @@ -529,22 +535,20 @@ namespace CSMWorld class CellListAdapter : public NestedColumnAdapter { public: - CellListAdapter (); + CellListAdapter() = default; void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; - void setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const override; + void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; - QVariant getData(const Record& record, - int subRowIndex, int subColIndex) const override; + QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; - void setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const override; + void setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; @@ -554,22 +558,20 @@ namespace CSMWorld class RegionWeatherAdapter : public NestedColumnAdapter { public: - RegionWeatherAdapter (); + RegionWeatherAdapter() = default; void addRow(Record& record, int position) const override; void removeRow(Record& record, int rowToRemove) const override; - void setTable(Record& record, - const NestedTableWrapperBase& nestedTable) const override; + void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override; NestedTableWrapperBase* table(const Record& record) const override; - QVariant getData(const Record& record, - int subRowIndex, int subColIndex) const override; + QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override; - void setData(Record& record, - const QVariant& value, int subRowIndex, int subColIndex) const override; + void setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const override; int getColumnsCount(const Record& record) const override; diff --git a/apps/opencs/model/world/nestedcollection.cpp b/apps/opencs/model/world/nestedcollection.cpp index 850d8c3859d..14fe4b78a88 100644 --- a/apps/opencs/model/world/nestedcollection.cpp +++ b/apps/opencs/model/world/nestedcollection.cpp @@ -1,10 +1,10 @@ #include "nestedcollection.hpp" -CSMWorld::NestedCollection::NestedCollection() -{} +#include -CSMWorld::NestedCollection::~NestedCollection() -{} +#include + +#include "columnbase.hpp" int CSMWorld::NestedCollection::getNestedRowsCount(int row, int column) const { @@ -19,7 +19,7 @@ int CSMWorld::NestedCollection::getNestedColumnsCount(int row, int column) const int CSMWorld::NestedCollection::searchNestedColumnIndex(int parentColumn, Columns::ColumnId id) { // Assumed that the parentColumn is always a valid index - const NestableColumn *parent = getNestableColumn(parentColumn); + const NestableColumn* parent = getNestableColumn(parentColumn); int nestedColumnCount = getNestedColumnsCount(0, parentColumn); for (int i = 0; i < nestedColumnCount; ++i) { diff --git a/apps/opencs/model/world/nestedcollection.hpp b/apps/opencs/model/world/nestedcollection.hpp index 4548cfb2b93..08d1c7c5ea9 100644 --- a/apps/opencs/model/world/nestedcollection.hpp +++ b/apps/opencs/model/world/nestedcollection.hpp @@ -14,9 +14,8 @@ namespace CSMWorld { public: - - NestedCollection(); - virtual ~NestedCollection(); + NestedCollection() = default; + virtual ~NestedCollection() = default; virtual void addNestedRow(int row, int col, int position) = 0; @@ -34,7 +33,7 @@ namespace CSMWorld virtual int getNestedColumnsCount(int row, int column) const; - virtual NestableColumn *getNestableColumn(int column) = 0; + virtual NestableColumn* getNestableColumn(int column) = 0; virtual int searchNestedColumnIndex(int parentColumn, Columns::ColumnId id); ///< \return the column index or -1 if the requested column wasn't found. diff --git a/apps/opencs/model/world/nestedcolumnadapter.hpp b/apps/opencs/model/world/nestedcolumnadapter.hpp index 462b5a5ab14..ebe77baef6a 100644 --- a/apps/opencs/model/world/nestedcolumnadapter.hpp +++ b/apps/opencs/model/world/nestedcolumnadapter.hpp @@ -10,14 +10,13 @@ namespace CSMWorld template struct Record; - template + template class NestedColumnAdapter { public: + NestedColumnAdapter() = default; - NestedColumnAdapter() {} - - virtual ~NestedColumnAdapter() {} + virtual ~NestedColumnAdapter() = default; virtual void addRow(Record& record, int position) const = 0; @@ -29,7 +28,8 @@ namespace CSMWorld virtual QVariant getData(const Record& record, int subRowIndex, int subColIndex) const = 0; - virtual void setData(Record& record, const QVariant& value, int subRowIndex, int subColIndex) const = 0; + virtual void setData( + Record& record, const QVariant& value, int subRowIndex, int subColIndex) const = 0; virtual int getColumnsCount(const Record& record) const = 0; diff --git a/apps/opencs/model/world/nestedidcollection.hpp b/apps/opencs/model/world/nestedidcollection.hpp index a699d4bd668..2e15f9b5831 100644 --- a/apps/opencs/model/world/nestedidcollection.hpp +++ b/apps/opencs/model/world/nestedidcollection.hpp @@ -2,10 +2,12 @@ #define CSM_WOLRD_NESTEDIDCOLLECTION_H #include +#include #include +#include "collection.hpp" #include "nestedcollection.hpp" -#include "nestedcoladapterimp.hpp" +#include "nestedcolumnadapter.hpp" namespace ESM { @@ -16,156 +18,155 @@ namespace CSMWorld { struct NestedTableWrapperBase; struct Cell; + struct ColumnBase; - template + template class IdCollection; - template > - class NestedIdCollection : public IdCollection, public NestedCollection - { - std::map* > mAdapters; + template + class NestedColumnAdapter; - const NestedColumnAdapter& getAdapter(const ColumnBase &column) const; + template + class NestedIdCollection : public IdCollection, public NestedCollection + { + std::map*> mAdapters; - public: + const NestedColumnAdapter& getAdapter(const ColumnBase& column) const; - NestedIdCollection (); - ~NestedIdCollection(); + public: + NestedIdCollection(); + ~NestedIdCollection() override; - void addNestedRow(int row, int column, int position) override; + void addNestedRow(int row, int column, int position) override; - void removeNestedRows(int row, int column, int subRow) override; + void removeNestedRows(int row, int column, int subRow) override; - QVariant getNestedData(int row, int column, int subRow, int subColumn) const override; + QVariant getNestedData(int row, int column, int subRow, int subColumn) const override; - void setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) override; + void setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) override; - NestedTableWrapperBase* nestedTable(int row, int column) const override; + NestedTableWrapperBase* nestedTable(int row, int column) const override; - void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; + void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; - int getNestedRowsCount(int row, int column) const override; + int getNestedRowsCount(int row, int column) const override; - int getNestedColumnsCount(int row, int column) const override; + int getNestedColumnsCount(int row, int column) const override; - // this method is inherited from NestedCollection, not from Collection - NestableColumn *getNestableColumn(int column) override; + // this method is inherited from NestedCollection, not from Collection + NestableColumn* getNestableColumn(int column) override; - void addAdapter(std::pair* > adapter); + void addAdapter(std::pair*> adapter); }; - template - NestedIdCollection::NestedIdCollection () - {} + template + NestedIdCollection::NestedIdCollection() + { + } - template - NestedIdCollection::~NestedIdCollection() + template + NestedIdCollection::~NestedIdCollection() { - for (typename std::map* >::iterator - iter (mAdapters.begin()); iter!=mAdapters.end(); ++iter) + for (typename std::map*>::iterator iter(mAdapters.begin()); + iter != mAdapters.end(); ++iter) { delete (*iter).second; } } - template - void NestedIdCollection::addAdapter(std::pair* > adapter) + template + void NestedIdCollection::addAdapter( + std::pair*> adapter) { mAdapters.insert(adapter); } - template - const NestedColumnAdapter& NestedIdCollection::getAdapter(const ColumnBase &column) const + template + const NestedColumnAdapter& NestedIdCollection::getAdapter(const ColumnBase& column) const { - typename std::map* >::const_iterator iter = - mAdapters.find (&column); + typename std::map*>::const_iterator iter + = mAdapters.find(&column); - if (iter==mAdapters.end()) + if (iter == mAdapters.end()) throw std::logic_error("No such column in the nestedidadapter"); return *iter->second; } - template - void NestedIdCollection::addNestedRow(int row, int column, int position) + template + void NestedIdCollection::addNestedRow(int row, int column, int position) { - Record record; - record.assign(Collection::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); - getAdapter(Collection::getColumn(column)).addRow(record, position); + getAdapter(Collection::getColumn(column)).addRow(*record, position); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } - template - void NestedIdCollection::removeNestedRows(int row, int column, int subRow) + template + void NestedIdCollection::removeNestedRows(int row, int column, int subRow) { - Record record; - record.assign(Collection::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); - getAdapter(Collection::getColumn(column)).removeRow(record, subRow); + getAdapter(Collection::getColumn(column)).removeRow(*record, subRow); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } - template - QVariant NestedIdCollection::getNestedData (int row, - int column, int subRow, int subColumn) const + template + QVariant NestedIdCollection::getNestedData(int row, int column, int subRow, int subColumn) const { - return getAdapter(Collection::getColumn(column)).getData( - Collection::getRecord(row), subRow, subColumn); + return getAdapter(Collection::getColumn(column)) + .getData(Collection::getRecord(row), subRow, subColumn); } - template - void NestedIdCollection::setNestedData(int row, - int column, const QVariant& data, int subRow, int subColumn) + template + void NestedIdCollection::setNestedData( + int row, int column, const QVariant& data, int subRow, int subColumn) { - Record record; - record.assign(Collection::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); - getAdapter(Collection::getColumn(column)).setData( - record, data, subRow, subColumn); + getAdapter(Collection::getColumn(column)).setData(*record, data, subRow, subColumn); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } - template - CSMWorld::NestedTableWrapperBase* NestedIdCollection::nestedTable(int row, - int column) const + template + CSMWorld::NestedTableWrapperBase* NestedIdCollection::nestedTable(int row, int column) const { - return getAdapter(Collection::getColumn(column)).table( - Collection::getRecord(row)); + return getAdapter(Collection::getColumn(column)).table(Collection::getRecord(row)); } - template - void NestedIdCollection::setNestedTable(int row, - int column, const CSMWorld::NestedTableWrapperBase& nestedTable) + template + void NestedIdCollection::setNestedTable( + int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { - Record record; - record.assign(Collection::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); - getAdapter(Collection::getColumn(column)).setTable( - record, nestedTable); + getAdapter(Collection::getColumn(column)).setTable(*record, nestedTable); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } - template - int NestedIdCollection::getNestedRowsCount(int row, int column) const + template + int NestedIdCollection::getNestedRowsCount(int row, int column) const { - return getAdapter(Collection::getColumn(column)).getRowsCount( - Collection::getRecord(row)); + return getAdapter(Collection::getColumn(column)) + .getRowsCount(Collection::getRecord(row)); } - template - int NestedIdCollection::getNestedColumnsCount(int row, int column) const + template + int NestedIdCollection::getNestedColumnsCount(int row, int column) const { - const ColumnBase &nestedColumn = Collection::getColumn(column); - int numRecords = Collection::getSize(); + const ColumnBase& nestedColumn = Collection::getColumn(column); + int numRecords = Collection::getSize(); if (row >= 0 && row < numRecords) { - const Record& record = Collection::getRecord(row); + const Record& record = Collection::getRecord(row); return getAdapter(nestedColumn).getColumnsCount(record); } else @@ -176,10 +177,10 @@ namespace CSMWorld } } - template - CSMWorld::NestableColumn *NestedIdCollection::getNestableColumn(int column) + template + CSMWorld::NestableColumn* NestedIdCollection::getNestableColumn(int column) { - return Collection::getNestableColumn(column); + return Collection::getNestableColumn(column); } } diff --git a/apps/opencs/model/world/nestedinfocollection.cpp b/apps/opencs/model/world/nestedinfocollection.cpp index 4abaaf9c027..74a51a9c142 100644 --- a/apps/opencs/model/world/nestedinfocollection.cpp +++ b/apps/opencs/model/world/nestedinfocollection.cpp @@ -1,33 +1,41 @@ #include "nestedinfocollection.hpp" -#include "nestedcoladapterimp.hpp" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include namespace CSMWorld { - NestedInfoCollection::NestedInfoCollection () - {} - NestedInfoCollection::~NestedInfoCollection() { - for (std::map* >::iterator - iter (mAdapters.begin()); iter!=mAdapters.end(); ++iter) + for (std::map*>::iterator iter(mAdapters.begin()); + iter != mAdapters.end(); ++iter) { delete (*iter).second; } } - void NestedInfoCollection::addAdapter(std::pair* > adapter) + void NestedInfoCollection::addAdapter(std::pair*> adapter) { mAdapters.insert(adapter); } - const NestedColumnAdapter& NestedInfoCollection::getAdapter(const ColumnBase &column) const + const NestedColumnAdapter& NestedInfoCollection::getAdapter(const ColumnBase& column) const { - std::map* >::const_iterator iter = - mAdapters.find (&column); + std::map*>::const_iterator iter = mAdapters.find(&column); - if (iter==mAdapters.end()) + if (iter == mAdapters.end()) throw std::logic_error("No such column in the nestedidadapter"); return *iter->second; @@ -35,76 +43,67 @@ namespace CSMWorld void NestedInfoCollection::addNestedRow(int row, int column, int position) { - Record record; - record.assign(Collection >::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); - getAdapter(Collection >::getColumn(column)).addRow(record, position); + getAdapter(Collection::getColumn(column)).addRow(*record, position); - Collection >::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } void NestedInfoCollection::removeNestedRows(int row, int column, int subRow) { - Record record; - record.assign(Collection >::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); - getAdapter(Collection >::getColumn(column)).removeRow(record, subRow); + getAdapter(Collection::getColumn(column)).removeRow(*record, subRow); - Collection >::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } - QVariant NestedInfoCollection::getNestedData (int row, - int column, int subRow, int subColumn) const + QVariant NestedInfoCollection::getNestedData(int row, int column, int subRow, int subColumn) const { - return getAdapter(Collection >::getColumn(column)).getData( - Collection >::getRecord(row), subRow, subColumn); + return getAdapter(Collection::getColumn(column)) + .getData(Collection::getRecord(row), subRow, subColumn); } - void NestedInfoCollection::setNestedData(int row, - int column, const QVariant& data, int subRow, int subColumn) + void NestedInfoCollection::setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) { - Record record; - record.assign(Collection >::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); - getAdapter(Collection >::getColumn(column)).setData( - record, data, subRow, subColumn); + getAdapter(Collection::getColumn(column)).setData(*record, data, subRow, subColumn); - Collection >::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } - CSMWorld::NestedTableWrapperBase* NestedInfoCollection::nestedTable(int row, - int column) const + CSMWorld::NestedTableWrapperBase* NestedInfoCollection::nestedTable(int row, int column) const { - return getAdapter(Collection >::getColumn(column)).table( - Collection >::getRecord(row)); + return getAdapter(Collection::getColumn(column)).table(Collection::getRecord(row)); } - void NestedInfoCollection::setNestedTable(int row, - int column, const CSMWorld::NestedTableWrapperBase& nestedTable) + void NestedInfoCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { - Record record; - record.assign(Collection >::getRecord(row)); + auto record = std::make_unique>(); + record->assign(Collection::getRecord(row)); - getAdapter(Collection >::getColumn(column)).setTable( - record, nestedTable); + getAdapter(Collection::getColumn(column)).setTable(*record, nestedTable); - Collection >::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } int NestedInfoCollection::getNestedRowsCount(int row, int column) const { - return getAdapter(Collection >::getColumn(column)).getRowsCount( - Collection >::getRecord(row)); + return getAdapter(Collection::getColumn(column)).getRowsCount(Collection::getRecord(row)); } int NestedInfoCollection::getNestedColumnsCount(int row, int column) const { - return getAdapter(Collection >::getColumn(column)).getColumnsCount( - Collection >::getRecord(row)); + return getAdapter(Collection::getColumn(column)).getColumnsCount(Collection::getRecord(row)); } - CSMWorld::NestableColumn *NestedInfoCollection::getNestableColumn(int column) + CSMWorld::NestableColumn* NestedInfoCollection::getNestableColumn(int column) { - return Collection >::getNestableColumn(column); + return Collection::getNestableColumn(column); } } diff --git a/apps/opencs/model/world/nestedinfocollection.hpp b/apps/opencs/model/world/nestedinfocollection.hpp index fe2cd43faa4..a401317aa5f 100644 --- a/apps/opencs/model/world/nestedinfocollection.hpp +++ b/apps/opencs/model/world/nestedinfocollection.hpp @@ -1,7 +1,10 @@ #ifndef CSM_WOLRD_NESTEDINFOCOLLECTION_H #define CSM_WOLRD_NESTEDINFOCOLLECTION_H +#include + #include +#include #include "infocollection.hpp" #include "nestedcollection.hpp" @@ -9,41 +12,43 @@ namespace CSMWorld { struct NestedTableWrapperBase; + class NestableColumn; + struct ColumnBase; + struct Info; - template + template class NestedColumnAdapter; class NestedInfoCollection : public InfoCollection, public NestedCollection { - std::map* > mAdapters; - - const NestedColumnAdapter& getAdapter(const ColumnBase &column) const; + std::map*> mAdapters; - public: + const NestedColumnAdapter& getAdapter(const ColumnBase& column) const; - NestedInfoCollection (); - ~NestedInfoCollection(); + public: + NestedInfoCollection() = default; + ~NestedInfoCollection() override; - void addNestedRow(int row, int column, int position) override; + void addNestedRow(int row, int column, int position) override; - void removeNestedRows(int row, int column, int subRow) override; + void removeNestedRows(int row, int column, int subRow) override; - QVariant getNestedData(int row, int column, int subRow, int subColumn) const override; + QVariant getNestedData(int row, int column, int subRow, int subColumn) const override; - void setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) override; + void setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) override; - NestedTableWrapperBase* nestedTable(int row, int column) const override; + NestedTableWrapperBase* nestedTable(int row, int column) const override; - void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; + void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; - int getNestedRowsCount(int row, int column) const override; + int getNestedRowsCount(int row, int column) const override; - int getNestedColumnsCount(int row, int column) const override; + int getNestedColumnsCount(int row, int column) const override; - // this method is inherited from NestedCollection, not from Collection > - NestableColumn *getNestableColumn(int column) override; + // this method is inherited from NestedCollection, not from Collection + NestableColumn* getNestableColumn(int column) override; - void addAdapter(std::pair* > adapter); + void addAdapter(std::pair*> adapter); }; } diff --git a/apps/opencs/model/world/nestedtableproxymodel.cpp b/apps/opencs/model/world/nestedtableproxymodel.cpp index edcc7a07061..f542ce4defa 100644 --- a/apps/opencs/model/world/nestedtableproxymodel.cpp +++ b/apps/opencs/model/world/nestedtableproxymodel.cpp @@ -1,13 +1,15 @@ #include "nestedtableproxymodel.hpp" -#include #include "idtree.hpp" -CSMWorld::NestedTableProxyModel::NestedTableProxyModel(const QModelIndex& parent, - ColumnBase::Display columnId, - CSMWorld::IdTree* parentModel) - : mParentColumn(parent.column()), - mMainModel(parentModel) +#include + +#include + +CSMWorld::NestedTableProxyModel::NestedTableProxyModel( + const QModelIndex& parent, ColumnBase::Display columnId, CSMWorld::IdTree* parentModel) + : mParentColumn(parent.column()) + , mMainModel(parentModel) { const int parentRow = parent.row(); @@ -15,32 +17,25 @@ CSMWorld::NestedTableProxyModel::NestedTableProxyModel(const QModelIndex& parent QAbstractProxyModel::setSourceModel(parentModel); - connect(mMainModel, SIGNAL(rowsAboutToBeInserted(const QModelIndex &, int, int)), - this, SLOT(forwardRowsAboutToInserted(const QModelIndex &, int, int))); + connect(mMainModel, &IdTree::rowsAboutToBeInserted, this, &NestedTableProxyModel::forwardRowsAboutToInserted); - connect(mMainModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), - this, SLOT(forwardRowsInserted(const QModelIndex &, int, int))); + connect(mMainModel, &IdTree::rowsInserted, this, &NestedTableProxyModel::forwardRowsInserted); - connect(mMainModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex &, int, int)), - this, SLOT(forwardRowsAboutToRemoved(const QModelIndex &, int, int))); + connect(mMainModel, &IdTree::rowsAboutToBeRemoved, this, &NestedTableProxyModel::forwardRowsAboutToRemoved); - connect(mMainModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), - this, SLOT(forwardRowsRemoved(const QModelIndex &, int, int))); + connect(mMainModel, &IdTree::rowsRemoved, this, &NestedTableProxyModel::forwardRowsRemoved); - connect(mMainModel, SIGNAL(resetStart(const QString&)), - this, SLOT(forwardResetStart(const QString&))); + connect(mMainModel, &IdTree::resetStart, this, &NestedTableProxyModel::forwardResetStart); - connect(mMainModel, SIGNAL(resetEnd(const QString&)), - this, SLOT(forwardResetEnd(const QString&))); + connect(mMainModel, &IdTree::resetEnd, this, &NestedTableProxyModel::forwardResetEnd); - connect(mMainModel, SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &)), - this, SLOT(forwardDataChanged(const QModelIndex &, const QModelIndex &))); + connect(mMainModel, &IdTree::dataChanged, this, &NestedTableProxyModel::forwardDataChanged); } QModelIndex CSMWorld::NestedTableProxyModel::mapFromSource(const QModelIndex& sourceIndex) const { const QModelIndex& testedParent = mMainModel->parent(sourceIndex); - const QModelIndex& parent = mMainModel->getNestedModelIndex (mId, mParentColumn); + const QModelIndex& parent = mMainModel->getNestedModelIndex(mId, mParentColumn); if (testedParent == parent) { return createIndex(sourceIndex.row(), sourceIndex.column()); @@ -53,27 +48,27 @@ QModelIndex CSMWorld::NestedTableProxyModel::mapFromSource(const QModelIndex& so QModelIndex CSMWorld::NestedTableProxyModel::mapToSource(const QModelIndex& proxyIndex) const { - const QModelIndex& parent = mMainModel->getNestedModelIndex (mId, mParentColumn); + const QModelIndex& parent = mMainModel->getNestedModelIndex(mId, mParentColumn); return mMainModel->index(proxyIndex.row(), proxyIndex.column(), parent); } int CSMWorld::NestedTableProxyModel::rowCount(const QModelIndex& index) const { - assert (!index.isValid()); + assert(!index.isValid()); return mMainModel->rowCount(mMainModel->getModelIndex(mId, mParentColumn)); } int CSMWorld::NestedTableProxyModel::columnCount(const QModelIndex& parent) const { - assert (!parent.isValid()); + assert(!parent.isValid()); return mMainModel->columnCount(mMainModel->getModelIndex(mId, mParentColumn)); } QModelIndex CSMWorld::NestedTableProxyModel::index(int row, int column, const QModelIndex& parent) const { - assert (!parent.isValid()); + assert(!parent.isValid()); int numRows = rowCount(parent); int numColumns = columnCount(parent); @@ -89,9 +84,7 @@ QModelIndex CSMWorld::NestedTableProxyModel::parent(const QModelIndex& index) co return QModelIndex(); } -QVariant CSMWorld::NestedTableProxyModel::headerData(int section, - Qt::Orientation orientation, - int role) const +QVariant CSMWorld::NestedTableProxyModel::headerData(int section, Qt::Orientation orientation, int role) const { return mMainModel->nestedHeaderData(mParentColumn, section, orientation, role); } @@ -104,7 +97,7 @@ QVariant CSMWorld::NestedTableProxyModel::data(const QModelIndex& index, int rol // NOTE: Due to mapToSouce(index) the dataChanged() signal resulting from setData() will have the // source model's index values. The indicies need to be converted to the proxy space values. // See forwardDataChanged() -bool CSMWorld::NestedTableProxyModel::setData (const QModelIndex & index, const QVariant & value, int role) +bool CSMWorld::NestedTableProxyModel::setData(const QModelIndex& index, const QVariant& value, int role) { return mMainModel->setData(mapToSource(index), value, role); } @@ -129,8 +122,7 @@ CSMWorld::IdTree* CSMWorld::NestedTableProxyModel::model() const return mMainModel; } -void CSMWorld::NestedTableProxyModel::forwardRowsAboutToInserted(const QModelIndex& parent, - int first, int last) +void CSMWorld::NestedTableProxyModel::forwardRowsAboutToInserted(const QModelIndex& parent, int first, int last) { if (indexIsParent(parent)) { @@ -148,13 +140,11 @@ void CSMWorld::NestedTableProxyModel::forwardRowsInserted(const QModelIndex& par bool CSMWorld::NestedTableProxyModel::indexIsParent(const QModelIndex& index) { - return (index.isValid() && - index.column() == mParentColumn && - mMainModel->data(mMainModel->index(index.row(), 0)).toString().toUtf8().constData() == mId); + return (index.isValid() && index.column() == mParentColumn + && mMainModel->data(mMainModel->index(index.row(), 0)).toString().toUtf8().constData() == mId); } -void CSMWorld::NestedTableProxyModel::forwardRowsAboutToRemoved(const QModelIndex& parent, - int first, int last) +void CSMWorld::NestedTableProxyModel::forwardRowsAboutToRemoved(const QModelIndex& parent, int first, int last) { if (indexIsParent(parent)) { @@ -182,15 +172,13 @@ void CSMWorld::NestedTableProxyModel::forwardResetEnd(const QString& id) endResetModel(); } -void CSMWorld::NestedTableProxyModel::forwardDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSMWorld::NestedTableProxyModel::forwardDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { - const QModelIndex& parent = mMainModel->getNestedModelIndex (mId, mParentColumn); + const QModelIndex& parent = mMainModel->getNestedModelIndex(mId, mParentColumn); if (topLeft.column() <= parent.column() && bottomRight.column() >= parent.column()) { - emit dataChanged(index(0,0), - index(mMainModel->rowCount(parent)-1, mMainModel->columnCount(parent)-1)); + emit dataChanged(index(0, 0), index(mMainModel->rowCount(parent) - 1, mMainModel->columnCount(parent) - 1)); } else if (topLeft.parent() == parent && bottomRight.parent() == parent) { diff --git a/apps/opencs/model/world/nestedtableproxymodel.hpp b/apps/opencs/model/world/nestedtableproxymodel.hpp index b10f8a3a36f..46737537e77 100644 --- a/apps/opencs/model/world/nestedtableproxymodel.hpp +++ b/apps/opencs/model/world/nestedtableproxymodel.hpp @@ -1,12 +1,13 @@ #ifndef CSM_WOLRD_NESTEDTABLEPROXYMODEL_H #define CSM_WOLRD_NESTEDTABLEPROXYMODEL_H -#include +#include #include +#include +#include +#include -#include "universalid.hpp" -#include "columns.hpp" #include "columnbase.hpp" /*! \brief @@ -15,8 +16,6 @@ namespace CSMWorld { - class CollectionBase; - struct RecordBase; class IdTree; class NestedTableProxyModel : public QAbstractProxyModel @@ -28,10 +27,8 @@ namespace CSMWorld std::string mId; public: - NestedTableProxyModel(const QModelIndex& parent, - ColumnBase::Display displayType, - IdTree* parentModel); - //parent is the parent of columns to work with. Columnid provides information about the column + NestedTableProxyModel(const QModelIndex& parent, ColumnBase::Display displayType, IdTree* parentModel); + // parent is the parent of columns to work with. Columnid provides information about the column std::string getParentId() const; @@ -51,11 +48,11 @@ namespace CSMWorld QModelIndex parent(const QModelIndex& index) const override; - QVariant headerData (int section, Qt::Orientation orientation, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - bool setData (const QModelIndex & index, const QVariant & value, int role = Qt::EditRole) override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; Qt::ItemFlags flags(const QModelIndex& index) const override; @@ -65,19 +62,19 @@ namespace CSMWorld bool indexIsParent(const QModelIndex& index); private slots: - void forwardRowsAboutToInserted(const QModelIndex & parent, int first, int last); + void forwardRowsAboutToInserted(const QModelIndex& parent, int first, int last); - void forwardRowsInserted(const QModelIndex & parent, int first, int last); + void forwardRowsInserted(const QModelIndex& parent, int first, int last); - void forwardRowsAboutToRemoved(const QModelIndex & parent, int first, int last); + void forwardRowsAboutToRemoved(const QModelIndex& parent, int first, int last); - void forwardRowsRemoved(const QModelIndex & parent, int first, int last); + void forwardRowsRemoved(const QModelIndex& parent, int first, int last); void forwardResetStart(const QString& id); void forwardResetEnd(const QString& id); - void forwardDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void forwardDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); }; } diff --git a/apps/opencs/model/world/nestedtablewrapper.cpp b/apps/opencs/model/world/nestedtablewrapper.cpp index 3966dbc575a..00602762010 100644 --- a/apps/opencs/model/world/nestedtablewrapper.cpp +++ b/apps/opencs/model/world/nestedtablewrapper.cpp @@ -1,11 +1,5 @@ #include "nestedtablewrapper.hpp" -CSMWorld::NestedTableWrapperBase::NestedTableWrapperBase() -{} - -CSMWorld::NestedTableWrapperBase::~NestedTableWrapperBase() -{} - int CSMWorld::NestedTableWrapperBase::size() const { return -5; diff --git a/apps/opencs/model/world/nestedtablewrapper.hpp b/apps/opencs/model/world/nestedtablewrapper.hpp index 7d46dff8bfa..4756409c96b 100644 --- a/apps/opencs/model/world/nestedtablewrapper.hpp +++ b/apps/opencs/model/world/nestedtablewrapper.hpp @@ -5,26 +5,28 @@ namespace CSMWorld { struct NestedTableWrapperBase { - virtual ~NestedTableWrapperBase(); - + virtual ~NestedTableWrapperBase() = default; + virtual int size() const; - - NestedTableWrapperBase(); + + NestedTableWrapperBase() = default; }; - - template + + template struct NestedTableWrapper : public NestedTableWrapperBase { NestedTable mNestedTable; NestedTableWrapper(const NestedTable& nestedTable) - : mNestedTable(nestedTable) {} + : mNestedTable(nestedTable) + { + } - virtual ~NestedTableWrapper() {} + ~NestedTableWrapper() override = default; int size() const override { - return mNestedTable.size(); //i hope that this will be enough + return mNestedTable.size(); // i hope that this will be enough } }; } diff --git a/apps/opencs/model/world/pathgrid.cpp b/apps/opencs/model/world/pathgrid.cpp index c995bd8f097..27d2b29600a 100644 --- a/apps/opencs/model/world/pathgrid.cpp +++ b/apps/opencs/model/world/pathgrid.cpp @@ -1,31 +1,31 @@ +#include "pathgrid.hpp" #include "cell.hpp" #include "idcollection.hpp" -#include "pathgrid.hpp" #include -void CSMWorld::Pathgrid::load (ESM::ESMReader &esm, bool &isDeleted, const IdCollection& cells) +void CSMWorld::Pathgrid::load(ESM::ESMReader& esm, bool& isDeleted, const IdCollection& cells) { - load (esm, isDeleted); + load(esm, isDeleted); // correct ID - if (!mId.empty() && mId[0]!='#' && cells.searchId (mId)==-1) + if (!mId.empty() && !mId.startsWith("#") && cells.searchId(mId) == -1) { std::ostringstream stream; stream << "#" << mData.mX << " " << mData.mY; - mId = stream.str(); + mId = ESM::RefId::stringRefId(stream.str()); } } -void CSMWorld::Pathgrid::load (ESM::ESMReader &esm, bool &isDeleted) +void CSMWorld::Pathgrid::load(ESM::ESMReader& esm, bool& isDeleted) { - ESM::Pathgrid::load (esm, isDeleted); + ESM::Pathgrid::load(esm, isDeleted); mId = mCell; if (mCell.empty()) { std::ostringstream stream; stream << "#" << mData.mX << " " << mData.mY; - mId = stream.str(); + mId = ESM::RefId::stringRefId(stream.str()); } } diff --git a/apps/opencs/model/world/pathgrid.hpp b/apps/opencs/model/world/pathgrid.hpp index 22d01b07106..a727e571a36 100644 --- a/apps/opencs/model/world/pathgrid.hpp +++ b/apps/opencs/model/world/pathgrid.hpp @@ -1,15 +1,20 @@ #ifndef CSM_WOLRD_PATHGRID_H #define CSM_WOLRD_PATHGRID_H -#include #include -#include +#include +#include + +namespace ESM +{ + class ESMReader; +} namespace CSMWorld { struct Cell; - template + template class IdCollection; /// \brief Wrapper for Pathgrid record @@ -18,10 +23,10 @@ namespace CSMWorld /// Exterior cell coordinates are encoded in the pathgrid ID. struct Pathgrid : public ESM::Pathgrid { - std::string mId; + ESM::RefId mId; - void load (ESM::ESMReader &esm, bool &isDeleted, const IdCollection& cells); - void load (ESM::ESMReader &esm, bool &isDeleted); + void load(ESM::ESMReader& esm, bool& isDeleted, const IdCollection& cells); + void load(ESM::ESMReader& esm, bool& isDeleted); }; } diff --git a/apps/opencs/model/world/record.cpp b/apps/opencs/model/world/record.cpp index da1651f2b57..ddd90508280 100644 --- a/apps/opencs/model/world/record.cpp +++ b/apps/opencs/model/world/record.cpp @@ -1,20 +1,16 @@ #include "record.hpp" -CSMWorld::RecordBase::~RecordBase() {} - bool CSMWorld::RecordBase::isDeleted() const { - return mState==State_Deleted || mState==State_Erased; + return mState == State_Deleted || mState == State_Erased; } - bool CSMWorld::RecordBase::isErased() const { - return mState==State_Erased; + return mState == State_Erased; } - bool CSMWorld::RecordBase::isModified() const { - return mState==State_Modified || mState==State_ModifiedOnly; + return mState == State_Modified || mState == State_ModifiedOnly; } \ No newline at end of file diff --git a/apps/opencs/model/world/record.hpp b/apps/opencs/model/world/record.hpp index 5f67a93b12d..35e4c82a352 100644 --- a/apps/opencs/model/world/record.hpp +++ b/apps/opencs/model/world/record.hpp @@ -1,6 +1,7 @@ #ifndef CSM_WOLRD_RECORD_H #define CSM_WOLRD_RECORD_H +#include #include namespace CSMWorld @@ -18,13 +19,18 @@ namespace CSMWorld State mState; - virtual ~RecordBase(); + explicit RecordBase(State state) + : mState(state) + { + } - virtual RecordBase *clone() const = 0; + virtual ~RecordBase() = default; - virtual RecordBase *modifiedCopy() const = 0; + virtual std::unique_ptr clone() const = 0; - virtual void assign (const RecordBase& record) = 0; + virtual std::unique_ptr modifiedCopy() const = 0; + + virtual void assign(const RecordBase& record) = 0; ///< Will throw an exception if the types don't match. bool isDeleted() const; @@ -42,14 +48,13 @@ namespace CSMWorld Record(); - Record(State state, - const ESXRecordT *base = 0, const ESXRecordT *modified = 0); + Record(State state, const ESXRecordT* base = 0, const ESXRecordT* modified = 0); - RecordBase *clone() const override; + std::unique_ptr clone() const override; - RecordBase *modifiedCopy() const override; + std::unique_ptr modifiedCopy() const override; - void assign (const RecordBase& record) override; + void assign(const RecordBase& record) override; const ESXRecordT& get() const; ///< Throws an exception, if the record is deleted. @@ -60,7 +65,7 @@ namespace CSMWorld const ESXRecordT& getBase() const; ///< Throws an exception, if the record is deleted. Returns modified, if there is no base. - void setModified (const ESXRecordT& modified); + void setModified(const ESXRecordT& modified); ///< Throws an exception, if the record is deleted. void merge(); @@ -69,75 +74,74 @@ namespace CSMWorld template Record::Record() - : mBase(), mModified() - { } + : RecordBase(State_BaseOnly) + , mBase() + , mModified() + { + } template - Record::Record(State state, const ESXRecordT *base, const ESXRecordT *modified) + Record::Record(State state, const ESXRecordT* base, const ESXRecordT* modified) + : RecordBase(state) + , mBase(base == nullptr ? ESXRecordT{} : *base) + , mModified(modified == nullptr ? ESXRecordT{} : *modified) { - if(base) - mBase = *base; - - if(modified) - mModified = *modified; - - this->mState = state; } template - RecordBase *Record::modifiedCopy() const + std::unique_ptr Record::modifiedCopy() const { - return new Record (State_ModifiedOnly, nullptr, &(this->get())); + return std::make_unique>(Record(State_ModifiedOnly, nullptr, &(this->get()))); } template - RecordBase *Record::clone() const + std::unique_ptr Record::clone() const { - return new Record (*this); + return std::make_unique>(Record(*this)); } template - void Record::assign (const RecordBase& record) + void Record::assign(const RecordBase& record) { - *this = dynamic_cast& > (record); + *this = dynamic_cast&>(record); } template const ESXRecordT& Record::get() const { - if (mState==State_Erased) - throw std::logic_error ("attempt to access a deleted record"); + if (mState == State_Erased) + throw std::logic_error("attempt to access a deleted record"); - return mState==State_BaseOnly || mState==State_Deleted ? mBase : mModified; + return mState == State_BaseOnly || mState == State_Deleted ? mBase : mModified; } template ESXRecordT& Record::get() { - if (mState==State_Erased) - throw std::logic_error ("attempt to access a deleted record"); + if (mState == State_Erased) + throw std::logic_error("attempt to access a deleted record"); - return mState==State_BaseOnly || mState==State_Deleted ? mBase : mModified; + return mState == State_BaseOnly || mState == State_Deleted ? mBase : mModified; } template const ESXRecordT& Record::getBase() const { - if (mState==State_Erased) - throw std::logic_error ("attempt to access a deleted record"); + if (mState == State_Erased) + throw std::logic_error("attempt to access a deleted record"); - return mState==State_ModifiedOnly ? mModified : mBase; + return mState == State_ModifiedOnly ? mModified : mBase; } template - void Record::setModified (const ESXRecordT& modified) + void Record::setModified(const ESXRecordT& modified) { - if (mState==State_Erased) - throw std::logic_error ("attempt to modify a deleted record"); + if (mState == State_Erased) + throw std::logic_error("attempt to modify a deleted record"); mModified = modified; - if (mState!=State_ModifiedOnly) + if (mState != State_ModifiedOnly) mState = State_Modified; } @@ -149,7 +153,7 @@ namespace CSMWorld mBase = mModified; mState = State_BaseOnly; } - else if (mState==State_Deleted) + else if (mState == State_Deleted) { mState = State_Erased; } diff --git a/apps/opencs/model/world/ref.cpp b/apps/opencs/model/world/ref.cpp index b336235909b..e28ef64fe7d 100644 --- a/apps/opencs/model/world/ref.cpp +++ b/apps/opencs/model/world/ref.cpp @@ -1,14 +1,16 @@ #include "ref.hpp" +#include + #include "cellcoordinates.hpp" -CSMWorld::CellRef::CellRef() : mNew (true) +CSMWorld::CellRef::CellRef() + : mNew(true) + , mIdNum(0) { - mRefNum.mIndex = 0; - mRefNum.mContentFile = 0; } std::pair CSMWorld::CellRef::getCellIndex() const { - return CellCoordinates::coordinatesToCellIndex (mPos.pos[0], mPos.pos[1]); + return CellCoordinates::coordinatesToCellIndex(mPos.pos[0], mPos.pos[1]); } diff --git a/apps/opencs/model/world/ref.hpp b/apps/opencs/model/world/ref.hpp index 5d10a3a1b3e..8e634d8106f 100644 --- a/apps/opencs/model/world/ref.hpp +++ b/apps/opencs/model/world/ref.hpp @@ -1,19 +1,21 @@ #ifndef CSM_WOLRD_REF_H #define CSM_WOLRD_REF_H +#include #include -#include +#include namespace CSMWorld { /// \brief Wrapper for CellRef sub record struct CellRef : public ESM::CellRef { - std::string mId; - std::string mCell; - std::string mOriginalCell; + ESM::RefId mId; + ESM::RefId mCell; + ESM::RefId mOriginalCell; bool mNew; // new reference, not counted yet, ref num not assigned yet + unsigned int mIdNum; CellRef(); diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index dfdb8e73bf1..124e697de82 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -1,40 +1,85 @@ #include "refcollection.hpp" -#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include -#include "ref.hpp" #include "cell.hpp" -#include "universalid.hpp" #include "record.hpp" +#include "ref.hpp" +#include "universalid.hpp" + +#include "../doc/messages.hpp" + +namespace CSMWorld +{ + template <> + void Collection::removeRows(int index, int count) + { + mRecords.erase(mRecords.begin() + index, mRecords.begin() + index + count); + + // index map is updated in RefCollection::removeRows() + } -void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool base, - std::map& cache, CSMDoc::Messages& messages) + template <> + void Collection::insertRecord(std::unique_ptr record, int index, UniversalId::Type type) + { + int size = static_cast(mRecords.size()); + if (index < 0 || index > size) + throw std::runtime_error("index out of range"); + + std::unique_ptr> record2(static_cast*>(record.release())); + + if (index == size) + mRecords.push_back(std::move(record2)); + else + mRecords.insert(mRecords.begin() + index, std::move(record2)); + + // index map is updated in RefCollection::insertRecord() + } +} + +void CSMWorld::RefCollection::load(ESM::ESMReader& reader, int cellIndex, bool base, + std::map& cache, CSMDoc::Messages& messages) { - Record cell = mCells.getRecord (cellIndex); + Record cell = mCells.getRecord(cellIndex); Cell& cell2 = base ? cell.mBase : cell.mModified; - CellRef ref; - ref.mNew = false; ESM::MovedCellRef mref; - mref.mRefNum.mIndex = 0; bool isDeleted = false; + bool isMoved = false; - while (ESM::Cell::getNextRef(reader, ref, isDeleted, true, &mref)) + while (true) { + CellRef ref; + ref.mNew = false; + + if (!ESM::Cell::getNextRef(reader, ref, isDeleted, mref, isMoved)) + break; + if (!base && reader.getIndex() == ref.mRefNum.mContentFile) + ref.mRefNum.mContentFile = -1; // Keep mOriginalCell empty when in modified (as an indicator that the // original cell will always be equal the current cell). - ref.mOriginalCell = base ? cell2.mId : ""; + ref.mOriginalCell = base ? cell2.mId : ESM::RefId(); if (cell.get().isExterior()) { // Autocalculate the cell index from coordinates first std::pair index = ref.getCellIndex(); - ref.mCell = "#" + std::to_string(index.first) + " " + std::to_string(index.second); + ref.mCell = ESM::RefId::stringRefId(ESM::RefId::esm3ExteriorCell(index.first, index.second).toString()); // Handle non-base moved references - if (!base && mref.mRefNum.mIndex != 0) + if (!base && isMoved) { // Moved references must have a link back to their original cell // See discussion: https://forum.openmw.org/viewtopic.php?f=6&t=577&start=30 @@ -46,12 +91,13 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool // Log a warning if the record target cell is different if (index.first != mref.mTarget[0] || index.second != mref.mTarget[1]) { - std::string indexCell = ref.mCell; - ref.mCell = "#" + std::to_string(mref.mTarget[0]) + " " + std::to_string(mref.mTarget[1]); + ESM::RefId indexCell = ref.mCell; + ref.mCell = ESM::RefId::stringRefId( + ESM::RefId::esm3ExteriorCell(mref.mTarget[0], mref.mTarget[1]).toString()); - CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId (cellIndex)); - messages.add(id, "The position of the moved reference " + ref.mRefID + " (cell " + indexCell + ")" - " does not match the target cell (" + ref.mCell + ")", + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); + messages.add(id, "The position of the moved reference " + ref.mRefID.toDebugString() + " (cell " + indexCell.toDebugString() + ")" + " does not match the target cell (" + ref.mCell.toDebugString() + ")", std::string(), CSMDoc::Message::Severity_Warning); } } @@ -59,75 +105,114 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool else ref.mCell = cell2.mId; - mref.mRefNum.mIndex = 0; + auto iter = cache.find(ref.mRefNum); - // ignore content file number - std::map::iterator iter = cache.begin(); - unsigned int thisIndex = ref.mRefNum.mIndex & 0x00ffffff; - if (ref.mRefNum.mContentFile != -1 && !base) ref.mRefNum.mContentFile = ref.mRefNum.mIndex >> 24; - - for (; iter != cache.end(); ++iter) + if (isMoved) { - if (thisIndex == iter->first.mIndex) - break; + if (iter == cache.end()) + { + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); + + messages.add(id, + "Attempt to move a non-existent reference - RefNum index " + std::to_string(ref.mRefNum.mIndex) + + ", refID " + ref.mRefID.toDebugString() + ", content file index " + + std::to_string(ref.mRefNum.mContentFile), + /*hint*/ "", CSMDoc::Message::Severity_Warning); + continue; + } + + int index = getIntIndex(iter->second); + + // ensure we have the same record id for setRecord() + ref.mId = getRecord(index).get().mId; + ref.mIdNum = extractIdNum(ref.mId.getRefIdString()); + + auto record = std::make_unique>(); + // TODO: check whether a base record be moved + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record->mBase : record->mModified) = std::move(ref); + + // overwrite original record + setRecord(index, std::move(record)); + + continue; // NOTE: assumed moved references are not deleted at the same time } if (isDeleted) { - if (iter==cache.end()) + if (iter == cache.end()) { - CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Cell, - mCells.getId (cellIndex)); + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); - messages.add (id, "Attempt to delete a non-existent reference"); + messages.add(id, + "Attempt to delete a non-existent reference - RefNum index " + std::to_string(ref.mRefNum.mIndex) + + ", refID " + ref.mRefID.getRefIdString() + ", content file index " + + std::to_string(ref.mRefNum.mContentFile), + /*hint*/ "", CSMDoc::Message::Severity_Warning); continue; } - int index = getIndex (iter->second); - - Record record = getRecord (index); + int index = getIntIndex(iter->second); if (base) { - removeRows (index, 1); - cache.erase (iter); + removeRows(index, 1); + cache.erase(iter); } else { - record.mState = RecordBase::State_Deleted; - setRecord (index, record); + auto record = std::make_unique>(getRecord(index)); + record->mState = RecordBase::State_Deleted; + setRecord(index, std::move(record)); } continue; } - if (iter==cache.end()) + if (iter == cache.end()) { // new reference - ref.mId = getNewId(); + ref.mIdNum = mNextId; // FIXME: fragile + ref.mId = ESM::RefId::stringRefId(getNewId()); - Record record; - record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - const ESM::RefNum refNum = ref.mRefNum; - std::string refId = ref.mId; - (base ? record.mBase : record.mModified) = std::move(ref); + cache.emplace(ref.mRefNum, ref.mIdNum); - appendRecord (record); + auto record = std::make_unique>(); + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record->mBase : record->mModified) = std::move(ref); - cache.emplace(refNum, std::move(refId)); + appendRecord(std::move(record)); } else { // old reference -> merge - ref.mId = iter->second; + int index = getIntIndex(iter->second); +#if 0 + // ref.mRefNum.mIndex : the key + // iter->second : previously cached idNum for the key + // index : position of the record for that idNum + // getRecord(index).get() : record in the index position + assert(iter->second != getRecord(index).get().mIdNum); // sanity check - int index = getIndex (ref.mId); + // check if the plugin used the same RefNum index for a different record + if (ref.mRefID != getRecord(index).get().mRefID) + { + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); + messages.add(id, + "RefNum renamed from RefID \"" + getRecord(index).get().mRefID + "\" to \"" + + ref.mRefID + "\" (RefNum index " + std::to_string(ref.mRefNum.mIndex) + ")", + /*hint*/"", + CSMDoc::Message::Severity_Info); + } +#endif + ref.mId = getRecord(index).get().mId; + ref.mIdNum = extractIdNum(ref.mId.getRefIdString()); - Record record = getRecord (index); - record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_Modified; - (base ? record.mBase : record.mModified) = std::move(ref); + auto record = std::make_unique>(getRecord(index)); + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_Modified; + (base ? record->mBase : record->mModified) = std::move(ref); - setRecord (index, record); + setRecord(index, std::move(record)); } } } @@ -136,3 +221,125 @@ std::string CSMWorld::RefCollection::getNewId() { return "ref#" + std::to_string(mNextId++); } + +unsigned int CSMWorld::RefCollection::extractIdNum(std::string_view id) const +{ + std::string::size_type separator = id.find_last_of('#'); + + if (separator == std::string::npos) + throw std::runtime_error("invalid ref ID: " + std::string(id)); + + return Misc::StringUtils::toNumeric(id.substr(separator + 1), 0); +} + +int CSMWorld::RefCollection::getIntIndex(unsigned int id) const +{ + int index = searchId(id); + + if (index == -1) + throw std::runtime_error("invalid RefNum: " + std::to_string(id)); + + return index; +} + +int CSMWorld::RefCollection::searchId(unsigned int id) const +{ + std::map::const_iterator iter = mRefIndex.find(id); + + if (iter == mRefIndex.end()) + return -1; + + return iter->second; +} + +void CSMWorld::RefCollection::removeRows(int index, int count) +{ + Collection::removeRows(index, count); // erase records only + + std::map::iterator iter = mRefIndex.begin(); + while (iter != mRefIndex.end()) + { + if (iter->second >= index) + { + if (iter->second >= index + count) + { + iter->second -= count; + ++iter; + } + else + mRefIndex.erase(iter++); + } + else + ++iter; + } +} + +void CSMWorld::RefCollection::appendBlankRecord(const ESM::RefId& id, UniversalId::Type type) +{ + auto record = std::make_unique>(); + + record->mState = Record::State_ModifiedOnly; + record->mModified.blank(); + + record->get().mId = id; + record->get().mIdNum = extractIdNum(id.getRefIdString()); + + Collection::appendRecord(std::move(record)); +} + +void CSMWorld::RefCollection::cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) +{ + auto copy = std::make_unique>(); + int index = getAppendIndex(ESM::RefId(), type); + + copy->mModified = getRecord(origin).get(); + copy->mState = RecordBase::State_ModifiedOnly; + + copy->get().mId = destination; + copy->get().mIdNum = extractIdNum(destination.getRefIdString()); + + if (copy->get().mRefNum.hasContentFile()) + { + mRefIndex.insert(std::make_pair(static_cast*>(copy.get())->get().mIdNum, index)); + copy->get().mRefNum.mContentFile = -1; + copy->get().mRefNum.mIndex = index; + } + else + copy->get().mRefNum.mIndex = copy->get().mIdNum; + + insertRecord(std::move(copy), getAppendIndex(destination, type)); // call RefCollection::insertRecord() +} + +int CSMWorld::RefCollection::searchId(const ESM::RefId& id) const +{ + return searchId(extractIdNum(id.getRefIdString())); +} + +void CSMWorld::RefCollection::appendRecord(std::unique_ptr record, UniversalId::Type type) +{ + int index = getAppendIndex(/*id*/ ESM::RefId(), type); // for CellRef records id is ignored + + mRefIndex.insert(std::make_pair(static_cast*>(record.get())->get().mIdNum, index)); + + Collection::insertRecord(std::move(record), index, type); // add records only +} + +void CSMWorld::RefCollection::insertRecord(std::unique_ptr record, int index, UniversalId::Type type) +{ + int size = getAppendIndex(/*id*/ ESM::RefId(), type); // for CellRef records id is ignored + unsigned int idNum = static_cast*>(record.get())->get().mIdNum; + + Collection::insertRecord(std::move(record), index, type); // add records only + + if (index < size - 1) + { + for (std::map::iterator iter(mRefIndex.begin()); iter != mRefIndex.end(); ++iter) + { + if (iter->second >= index) + ++(iter->second); + } + } + + mRefIndex.insert(std::make_pair(idNum, index)); +} diff --git a/apps/opencs/model/world/refcollection.hpp b/apps/opencs/model/world/refcollection.hpp index d031398d3f8..d3d200e6c21 100644 --- a/apps/opencs/model/world/refcollection.hpp +++ b/apps/opencs/model/world/refcollection.hpp @@ -2,35 +2,78 @@ #define CSM_WOLRD_REFCOLLECTION_H #include +#include +#include +#include +#include -#include "../doc/stage.hpp" +#include #include "collection.hpp" -#include "ref.hpp" #include "record.hpp" +#include "ref.hpp" + +namespace ESM +{ + class ESMReader; +} + +namespace CSMDoc +{ + class Messages; +} namespace CSMWorld { struct Cell; - class UniversalId; + + template <> + void Collection::removeRows(int index, int count); + + template <> + void Collection::insertRecord(std::unique_ptr record, int index, UniversalId::Type type); /// \brief References in cells - class RefCollection : public Collection + class RefCollection final : public Collection { - Collection& mCells; - int mNextId; + Collection& mCells; + std::map mRefIndex; // CellRef index keyed by CSMWorld::CellRef::mIdNum + + int mNextId; + + unsigned int extractIdNum(std::string_view id) const; + + int getIntIndex(unsigned int id) const; + + int searchId(unsigned int id) const; + + public: + // MSVC needs the constructor for a class inheriting a template to be defined in header + RefCollection(Collection& cells) + : mCells(cells) + , mNextId(0) + { + } + + void load(ESM::ESMReader& reader, int cellIndex, bool base, std::map& cache, + CSMDoc::Messages& messages); + ///< Load a sequence of references. + + std::string getNewId(); + + void removeRows(int index, int count) override; + + void appendBlankRecord(const ESM::RefId& id, UniversalId::Type type = UniversalId::Type_None) override; + + void cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) override; - public: - // MSVC needs the constructor for a class inheriting a template to be defined in header - RefCollection (Collection& cells) - : mCells (cells), mNextId (0) - {} + int searchId(const ESM::RefId& id) const override; - void load (ESM::ESMReader& reader, int cellIndex, bool base, - std::map& cache, CSMDoc::Messages& messages); - ///< Load a sequence of references. + void appendRecord(std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None) override; - std::string getNewId(); + void insertRecord( + std::unique_ptr record, int index, UniversalId::Type type = UniversalId::Type_None) override; }; } diff --git a/apps/opencs/model/world/refidadapter.cpp b/apps/opencs/model/world/refidadapter.cpp deleted file mode 100644 index 37cb67bca3e..00000000000 --- a/apps/opencs/model/world/refidadapter.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "refidadapter.hpp" - -CSMWorld::RefIdAdapter::RefIdAdapter() {} - -CSMWorld::RefIdAdapter::~RefIdAdapter() {} - -CSMWorld::NestedRefIdAdapterBase::NestedRefIdAdapterBase() {} - -CSMWorld::NestedRefIdAdapterBase::~NestedRefIdAdapterBase() {} diff --git a/apps/opencs/model/world/refidadapter.hpp b/apps/opencs/model/world/refidadapter.hpp index 116adb69a73..11ed82d0ffe 100644 --- a/apps/opencs/model/world/refidadapter.hpp +++ b/apps/opencs/model/world/refidadapter.hpp @@ -1,14 +1,17 @@ #ifndef CSM_WOLRD_REFIDADAPTER_H #define CSM_WOLRD_REFIDADAPTER_H +#include #include #include /*! \brief - * Adapters acts as indirection layer, abstracting details of the record types (in the wrappers) from the higher levels of model. - * Please notice that nested adaptor uses helper classes for actually performing any actions. Different record types require different helpers (needs to be created in the subclass and then fetched via member function). + * Adapters acts as indirection layer, abstracting details of the record types (in the wrappers) from the higher levels + * of model. Please notice that nested adaptor uses helper classes for actually performing any actions. Different record + * types require different helpers (needs to be created in the subclass and then fetched via member function). * - * Important point: don't forget to make sure that getData on the nestedColumn returns true (otherwise code will not treat the index pointing to the column as having children! + * Important point: don't forget to make sure that getData on the nestedColumn returns true (otherwise code will not + * treat the index pointing to the column as having children! */ class QVariant; @@ -19,61 +22,52 @@ namespace CSMWorld class RefIdData; struct RecordBase; struct NestedTableWrapperBase; - class HelperBase; class RefIdAdapter { - // not implemented - RefIdAdapter (const RefIdAdapter&); - RefIdAdapter& operator= (const RefIdAdapter&); + public: + RefIdAdapter() = default; + RefIdAdapter(const RefIdAdapter&) = delete; + RefIdAdapter& operator=(const RefIdAdapter&) = delete; + virtual ~RefIdAdapter() = default; - public: + virtual QVariant getData(const RefIdColumn* column, const RefIdData& data, int idnex) const = 0; + ///< If called on the nest column, should return QVariant(true). - RefIdAdapter(); + virtual void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const = 0; + ///< If the data type does not match an exception is thrown. - virtual ~RefIdAdapter(); + virtual ESM::RefId getId(const RecordBase& record) const = 0; - virtual QVariant getData (const RefIdColumn *column, const RefIdData& data, int idnex) - const = 0; - ///< If called on the nest column, should return QVariant(true). - - virtual void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const = 0; - ///< If the data type does not match an exception is thrown. - - virtual std::string getId (const RecordBase& record) const = 0; - - virtual void setId(RecordBase& record, const std::string& id) = 0; // used by RefIdCollection::cloneRecord() + virtual void setId(RecordBase& record, const std::string& id) = 0; // used by RefIdCollection::cloneRecord() }; class NestedRefIdAdapterBase { - public: - NestedRefIdAdapterBase(); + public: + NestedRefIdAdapterBase() = default; - virtual ~NestedRefIdAdapterBase(); + virtual ~NestedRefIdAdapterBase() = default; - virtual void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const = 0; + virtual void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, + int subRowIndex, int subColIndex) const = 0; - virtual QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const = 0; + virtual QVariant getNestedData( + const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const = 0; - virtual int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const = 0; + virtual int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const = 0; - virtual int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const = 0; + virtual int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const = 0; - virtual void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const = 0; + virtual void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const = 0; - virtual void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const = 0; + virtual void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const = 0; - virtual void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const = 0; + virtual void setNestedTable( + const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const = 0; - virtual NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const = 0; + virtual NestedTableWrapperBase* nestedTable( + const RefIdColumn* column, const RefIdData& data, int index) const = 0; }; } diff --git a/apps/opencs/model/world/refidadapterimp.cpp b/apps/opencs/model/world/refidadapterimp.cpp index 644092f1647..e7d6eb0c1c5 100644 --- a/apps/opencs/model/world/refidadapterimp.cpp +++ b/apps/opencs/model/world/refidadapterimp.cpp @@ -1,52 +1,61 @@ #include "refidadapterimp.hpp" -#include #include -#include -#include -#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include #include "nestedtablewrapper.hpp" -CSMWorld::PotionColumns::PotionColumns (const InventoryColumns& columns) -: InventoryColumns (columns), mEffects(nullptr) {} +CSMWorld::PotionColumns::PotionColumns(const InventoryColumns& columns) + : InventoryColumns(columns) + , mEffects(nullptr) +{ +} -CSMWorld::PotionRefIdAdapter::PotionRefIdAdapter (const PotionColumns& columns, - const RefIdColumn *autoCalc) -: InventoryRefIdAdapter (UniversalId::Type_Potion, columns), - mColumns(columns), mAutoCalc (autoCalc) -{} +CSMWorld::PotionRefIdAdapter::PotionRefIdAdapter(const PotionColumns& columns, const RefIdColumn* autoCalc) + : InventoryRefIdAdapter(UniversalId::Type_Potion, columns) + , mColumns(columns) + , mAutoCalc(autoCalc) +{ +} -QVariant CSMWorld::PotionRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const +QVariant CSMWorld::PotionRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Potion))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Potion))); - if (column==mAutoCalc) - return record.get().mData.mAutoCalc!=0; + if (column == mAutoCalc) + return record.get().mData.mFlags & ESM::Potion::Autocalc; // to show nested tables in dialogue subview, see IdTree::hasChildren() - if (column==mColumns.mEffects) + if (column == mColumns.mEffects) return QVariant::fromValue(ColumnBase::TableEdit_Full); - return InventoryRefIdAdapter::getData (column, data, index); + return InventoryRefIdAdapter::getData(column, data, index); } -void CSMWorld::PotionRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::PotionRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Potion))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Potion))); ESM::Potion potion = record.get(); - if (column==mAutoCalc) - potion.mData.mAutoCalc = value.toInt(); + if (column == mAutoCalc) + potion.mData.mFlags = value.toBool(); else { - InventoryRefIdAdapter::setData (column, data, index, value); + InventoryRefIdAdapter::setData(column, data, index, value); return; } @@ -54,93 +63,91 @@ void CSMWorld::PotionRefIdAdapter::setData (const RefIdColumn *column, RefIdData record.setModified(potion); } +CSMWorld::IngredientColumns::IngredientColumns(const InventoryColumns& columns) + : InventoryColumns(columns) + , mEffects(nullptr) +{ +} -CSMWorld::IngredientColumns::IngredientColumns (const InventoryColumns& columns) -: InventoryColumns (columns) -, mEffects(nullptr) -{} - -CSMWorld::IngredientRefIdAdapter::IngredientRefIdAdapter (const IngredientColumns& columns) -: InventoryRefIdAdapter (UniversalId::Type_Ingredient, columns), - mColumns(columns) -{} +CSMWorld::IngredientRefIdAdapter::IngredientRefIdAdapter(const IngredientColumns& columns) + : InventoryRefIdAdapter(UniversalId::Type_Ingredient, columns) + , mColumns(columns) +{ +} -QVariant CSMWorld::IngredientRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const +QVariant CSMWorld::IngredientRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - if (column==mColumns.mEffects) + if (column == mColumns.mEffects) return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); - return InventoryRefIdAdapter::getData (column, data, index); + return InventoryRefIdAdapter::getData(column, data, index); } -void CSMWorld::IngredientRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::IngredientRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - InventoryRefIdAdapter::setData (column, data, index, value); + InventoryRefIdAdapter::setData(column, data, index, value); return; } - CSMWorld::IngredEffectRefIdAdapter::IngredEffectRefIdAdapter() -: mType(UniversalId::Type_Ingredient) -{} - -CSMWorld::IngredEffectRefIdAdapter::~IngredEffectRefIdAdapter() -{} + : mType(UniversalId::Type_Ingredient) +{ +} -void CSMWorld::IngredEffectRefIdAdapter::addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const +void CSMWorld::IngredEffectRefIdAdapter::addNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int position) const { // Do nothing, this table cannot be changed by the user } -void CSMWorld::IngredEffectRefIdAdapter::removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const +void CSMWorld::IngredEffectRefIdAdapter::removeNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } -void CSMWorld::IngredEffectRefIdAdapter::setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const +void CSMWorld::IngredEffectRefIdAdapter::setNestedTable( + const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESM::Ingredient ingredient = record.get(); - ingredient.mData = - static_cast >&>(nestedTable).mNestedTable.at(0); + ingredient.mData = static_cast>&>(nestedTable) + .mNestedTable.at(0); - record.setModified (ingredient); + record.setModified(ingredient); } -CSMWorld::NestedTableWrapperBase* CSMWorld::IngredEffectRefIdAdapter::nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const +CSMWorld::NestedTableWrapperBase* CSMWorld::IngredEffectRefIdAdapter::nestedTable( + const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(wrap); + return new NestedTableWrapper>(wrap); } -QVariant CSMWorld::IngredEffectRefIdAdapter::getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const +QVariant CSMWorld::IngredEffectRefIdAdapter::getNestedData( + const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); if (subRowIndex < 0 || subRowIndex >= 4) - throw std::runtime_error ("index out of range"); + throw std::runtime_error("index out of range"); switch (subColIndex) { - case 0: return record.get().mData.mEffectID[subRowIndex]; + case 0: + return record.get().mData.mEffectID[subRowIndex]; case 1: { switch (record.get().mData.mEffectID[subRowIndex]) @@ -174,127 +181,155 @@ QVariant CSMWorld::IngredEffectRefIdAdapter::getNestedData (const RefIdColumn *c } } -void CSMWorld::IngredEffectRefIdAdapter::setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const +void CSMWorld::IngredEffectRefIdAdapter::setNestedData( + const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, mType))); ESM::Ingredient ingredient = record.get(); if (subRowIndex < 0 || subRowIndex >= 4) - throw std::runtime_error ("index out of range"); + throw std::runtime_error("index out of range"); - switch(subColIndex) + switch (subColIndex) { - case 0: ingredient.mData.mEffectID[subRowIndex] = value.toInt(); break; - case 1: ingredient.mData.mSkills[subRowIndex] = value.toInt(); break; - case 2: ingredient.mData.mAttributes[subRowIndex] = value.toInt(); break; + case 0: + ingredient.mData.mEffectID[subRowIndex] = value.toInt(); + switch (ingredient.mData.mEffectID[subRowIndex]) + { + case ESM::MagicEffect::DrainSkill: + case ESM::MagicEffect::DamageSkill: + case ESM::MagicEffect::RestoreSkill: + case ESM::MagicEffect::FortifySkill: + case ESM::MagicEffect::AbsorbSkill: + ingredient.mData.mAttributes[subRowIndex] = -1; + break; + case ESM::MagicEffect::DrainAttribute: + case ESM::MagicEffect::DamageAttribute: + case ESM::MagicEffect::RestoreAttribute: + case ESM::MagicEffect::FortifyAttribute: + case ESM::MagicEffect::AbsorbAttribute: + ingredient.mData.mSkills[subRowIndex] = -1; + break; + default: + ingredient.mData.mSkills[subRowIndex] = -1; + ingredient.mData.mAttributes[subRowIndex] = -1; + } + break; + case 1: + ingredient.mData.mSkills[subRowIndex] = value.toInt(); + break; + case 2: + ingredient.mData.mAttributes[subRowIndex] = value.toInt(); + break; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } - record.setModified (ingredient); + record.setModified(ingredient); } -int CSMWorld::IngredEffectRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const +int CSMWorld::IngredEffectRefIdAdapter::getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const { return 3; // effect, skill, attribute } -int CSMWorld::IngredEffectRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const +int CSMWorld::IngredEffectRefIdAdapter::getNestedRowsCount( + const RefIdColumn* column, const RefIdData& data, int index) const { return 4; // up to 4 effects } +CSMWorld::ApparatusRefIdAdapter::ApparatusRefIdAdapter( + const InventoryColumns& columns, const RefIdColumn* type, const RefIdColumn* quality) + : InventoryRefIdAdapter(UniversalId::Type_Apparatus, columns) + , mType(type) + , mQuality(quality) +{ +} -CSMWorld::ApparatusRefIdAdapter::ApparatusRefIdAdapter (const InventoryColumns& columns, - const RefIdColumn *type, const RefIdColumn *quality) -: InventoryRefIdAdapter (UniversalId::Type_Apparatus, columns), - mType (type), mQuality (quality) -{} - -QVariant CSMWorld::ApparatusRefIdAdapter::getData (const RefIdColumn *column, - const RefIdData& data, int index) const +QVariant CSMWorld::ApparatusRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Apparatus))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Apparatus))); - if (column==mType) + if (column == mType) return record.get().mData.mType; - if (column==mQuality) + if (column == mQuality) return record.get().mData.mQuality; - return InventoryRefIdAdapter::getData (column, data, index); + return InventoryRefIdAdapter::getData(column, data, index); } -void CSMWorld::ApparatusRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::ApparatusRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Apparatus))); + Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Apparatus))); ESM::Apparatus apparatus = record.get(); - if (column==mType) + if (column == mType) apparatus.mData.mType = value.toInt(); - else if (column==mQuality) + else if (column == mQuality) apparatus.mData.mQuality = value.toFloat(); else { - InventoryRefIdAdapter::setData (column, data, index, value); + InventoryRefIdAdapter::setData(column, data, index, value); return; } record.setModified(apparatus); } +CSMWorld::ArmorRefIdAdapter::ArmorRefIdAdapter(const EnchantableColumns& columns, const RefIdColumn* type, + const RefIdColumn* health, const RefIdColumn* armor, const RefIdColumn* partRef) + : EnchantableRefIdAdapter(UniversalId::Type_Armor, columns) + , mType(type) + , mHealth(health) + , mArmor(armor) + , mPartRef(partRef) +{ +} -CSMWorld::ArmorRefIdAdapter::ArmorRefIdAdapter (const EnchantableColumns& columns, - const RefIdColumn *type, const RefIdColumn *health, const RefIdColumn *armor, - const RefIdColumn *partRef) -: EnchantableRefIdAdapter (UniversalId::Type_Armor, columns), - mType (type), mHealth (health), mArmor (armor), mPartRef(partRef) -{} - -QVariant CSMWorld::ArmorRefIdAdapter::getData (const RefIdColumn *column, - const RefIdData& data, int index) const +QVariant CSMWorld::ArmorRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Armor))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Armor))); - if (column==mType) + if (column == mType) return record.get().mData.mType; - if (column==mHealth) + if (column == mHealth) return record.get().mData.mHealth; - if (column==mArmor) + if (column == mArmor) return record.get().mData.mArmor; - if (column==mPartRef) + if (column == mPartRef) return QVariant::fromValue(ColumnBase::TableEdit_Full); - return EnchantableRefIdAdapter::getData (column, data, index); + return EnchantableRefIdAdapter::getData(column, data, index); } -void CSMWorld::ArmorRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::ArmorRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Armor))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Armor))); ESM::Armor armor = record.get(); - if (column==mType) + if (column == mType) armor.mData.mType = value.toInt(); - else if (column==mHealth) + else if (column == mHealth) armor.mData.mHealth = value.toInt(); - else if (column==mArmor) + else if (column == mArmor) armor.mData.mArmor = value.toInt(); else { - EnchantableRefIdAdapter::setData (column, data, index, value); + EnchantableRefIdAdapter::setData(column, data, index, value); return; } @@ -302,47 +337,49 @@ void CSMWorld::ArmorRefIdAdapter::setData (const RefIdColumn *column, RefIdData& record.setModified(armor); } -CSMWorld::BookRefIdAdapter::BookRefIdAdapter (const EnchantableColumns& columns, - const RefIdColumn *bookType, const RefIdColumn *skill, const RefIdColumn *text) -: EnchantableRefIdAdapter (UniversalId::Type_Book, columns), - mBookType (bookType), mSkill (skill), mText (text) -{} +CSMWorld::BookRefIdAdapter::BookRefIdAdapter( + const EnchantableColumns& columns, const RefIdColumn* bookType, const RefIdColumn* skill, const RefIdColumn* text) + : EnchantableRefIdAdapter(UniversalId::Type_Book, columns) + , mBookType(bookType) + , mSkill(skill) + , mText(text) +{ +} -QVariant CSMWorld::BookRefIdAdapter::getData (const RefIdColumn *column, - const RefIdData& data, int index) const +QVariant CSMWorld::BookRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Book))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Book))); - if (column==mBookType) + if (column == mBookType) return record.get().mData.mIsScroll; - if (column==mSkill) + if (column == mSkill) return record.get().mData.mSkillId; - if (column==mText) - return QString::fromUtf8 (record.get().mText.c_str()); + if (column == mText) + return QString::fromUtf8(record.get().mText.c_str()); - return EnchantableRefIdAdapter::getData (column, data, index); + return EnchantableRefIdAdapter::getData(column, data, index); } -void CSMWorld::BookRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::BookRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Book))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Book))); ESM::Book book = record.get(); - if (column==mBookType) + if (column == mBookType) book.mData.mIsScroll = value.toInt(); - else if (column==mSkill) + else if (column == mSkill) book.mData.mSkillId = value.toInt(); - else if (column==mText) + else if (column == mText) book.mText = value.toString().toUtf8().data(); else { - EnchantableRefIdAdapter::setData (column, data, index, value); + EnchantableRefIdAdapter::setData(column, data, index, value); return; } @@ -350,40 +387,41 @@ void CSMWorld::BookRefIdAdapter::setData (const RefIdColumn *column, RefIdData& record.setModified(book); } -CSMWorld::ClothingRefIdAdapter::ClothingRefIdAdapter (const EnchantableColumns& columns, - const RefIdColumn *type, const RefIdColumn *partRef) -: EnchantableRefIdAdapter (UniversalId::Type_Clothing, columns), mType (type), - mPartRef(partRef) -{} +CSMWorld::ClothingRefIdAdapter::ClothingRefIdAdapter( + const EnchantableColumns& columns, const RefIdColumn* type, const RefIdColumn* partRef) + : EnchantableRefIdAdapter(UniversalId::Type_Clothing, columns) + , mType(type) + , mPartRef(partRef) +{ +} -QVariant CSMWorld::ClothingRefIdAdapter::getData (const RefIdColumn *column, - const RefIdData& data, int index) const +QVariant CSMWorld::ClothingRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Clothing))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Clothing))); - if (column==mType) + if (column == mType) return record.get().mData.mType; - if (column==mPartRef) + if (column == mPartRef) return QVariant::fromValue(ColumnBase::TableEdit_Full); - return EnchantableRefIdAdapter::getData (column, data, index); + return EnchantableRefIdAdapter::getData(column, data, index); } -void CSMWorld::ClothingRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::ClothingRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Clothing))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Clothing))); ESM::Clothing clothing = record.get(); - if (column==mType) + if (column == mType) clothing.mData.mType = value.toInt(); else { - EnchantableRefIdAdapter::setData (column, data, index, value); + EnchantableRefIdAdapter::setData(column, data, index, value); return; } @@ -391,52 +429,54 @@ void CSMWorld::ClothingRefIdAdapter::setData (const RefIdColumn *column, RefIdDa record.setModified(clothing); } -CSMWorld::ContainerRefIdAdapter::ContainerRefIdAdapter (const NameColumns& columns, - const RefIdColumn *weight, const RefIdColumn *organic, const RefIdColumn *respawn, const RefIdColumn *content) -: NameRefIdAdapter (UniversalId::Type_Container, columns), mWeight (weight), - mOrganic (organic), mRespawn (respawn), mContent(content) -{} +CSMWorld::ContainerRefIdAdapter::ContainerRefIdAdapter(const NameColumns& columns, const RefIdColumn* weight, + const RefIdColumn* organic, const RefIdColumn* respawn, const RefIdColumn* content) + : NameRefIdAdapter(UniversalId::Type_Container, columns) + , mWeight(weight) + , mOrganic(organic) + , mRespawn(respawn) + , mContent(content) +{ +} -QVariant CSMWorld::ContainerRefIdAdapter::getData (const RefIdColumn *column, - const RefIdData& data, - int index) const +QVariant CSMWorld::ContainerRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Container))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Container))); - if (column==mWeight) + if (column == mWeight) return record.get().mWeight; - if (column==mOrganic) - return (record.get().mFlags & ESM::Container::Organic)!=0; + if (column == mOrganic) + return (record.get().mFlags & ESM::Container::Organic) != 0; - if (column==mRespawn) - return (record.get().mFlags & ESM::Container::Respawn)!=0; + if (column == mRespawn) + return (record.get().mFlags & ESM::Container::Respawn) != 0; - if (column==mContent) + if (column == mContent) return QVariant::fromValue(ColumnBase::TableEdit_Full); - return NameRefIdAdapter::getData (column, data, index); + return NameRefIdAdapter::getData(column, data, index); } -void CSMWorld::ContainerRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::ContainerRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Container))); + Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Container))); ESM::Container container = record.get(); - if (column==mWeight) + if (column == mWeight) container.mWeight = value.toFloat(); - else if (column==mOrganic) + else if (column == mOrganic) { if (value.toInt()) container.mFlags |= ESM::Container::Organic; else container.mFlags &= ~ESM::Container::Organic; } - else if (column==mRespawn) + else if (column == mRespawn) { if (value.toInt()) container.mFlags |= ESM::Container::Respawn; @@ -445,7 +485,7 @@ void CSMWorld::ContainerRefIdAdapter::setData (const RefIdColumn *column, RefIdD } else { - NameRefIdAdapter::setData (column, data, index, value); + NameRefIdAdapter::setData(column, data, index, value); return; } @@ -453,88 +493,96 @@ void CSMWorld::ContainerRefIdAdapter::setData (const RefIdColumn *column, RefIdD record.setModified(container); } -CSMWorld::CreatureColumns::CreatureColumns (const ActorColumns& actorColumns) -: ActorColumns (actorColumns), - mType(nullptr), - mScale(nullptr), - mOriginal(nullptr), - mAttributes(nullptr), - mAttacks(nullptr), - mMisc(nullptr), - mBloodType(nullptr) -{} +CSMWorld::CreatureColumns::CreatureColumns(const ActorColumns& actorColumns) + : ActorColumns(actorColumns) + , mType(nullptr) + , mScale(nullptr) + , mOriginal(nullptr) + , mAttributes(nullptr) + , mAttacks(nullptr) + , mMisc(nullptr) + , mBloodType(nullptr) +{ +} -CSMWorld::CreatureRefIdAdapter::CreatureRefIdAdapter (const CreatureColumns& columns) -: ActorRefIdAdapter (UniversalId::Type_Creature, columns), mColumns (columns) -{} +CSMWorld::CreatureRefIdAdapter::CreatureRefIdAdapter(const CreatureColumns& columns) + : ActorRefIdAdapter(UniversalId::Type_Creature, columns) + , mColumns(columns) +{ +} -QVariant CSMWorld::CreatureRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const +QVariant CSMWorld::CreatureRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Creature))); - if (column==mColumns.mType) + if (column == mColumns.mType) return record.get().mData.mType; - if (column==mColumns.mScale) + if (column == mColumns.mScale) return record.get().mScale; - if (column==mColumns.mOriginal) - return QString::fromUtf8 (record.get().mOriginal.c_str()); + if (column == mColumns.mOriginal) + return QString::fromUtf8(record.get().mOriginal.getRefIdString().c_str()); - if (column==mColumns.mAttributes) + if (column == mColumns.mAttributes) return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); - if (column==mColumns.mAttacks) + if (column == mColumns.mAttacks) return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); - if (column==mColumns.mMisc) + if (column == mColumns.mMisc) return QVariant::fromValue(ColumnBase::TableEdit_Full); if (column == mColumns.mBloodType) return record.get().mBloodType; - std::map::const_iterator iter = - mColumns.mFlags.find (column); + { + std::map::const_iterator iter = mColumns.mFlags.find(column); - if (iter!=mColumns.mFlags.end()) - return (record.get().mFlags & iter->second)!=0; + if (iter != mColumns.mFlags.end()) + return (record.get().mFlags & iter->second) != 0; + } - return ActorRefIdAdapter::getData (column, data, index); + { + std::map::const_iterator iter = mColumns.mServices.find(column); + if (iter != mColumns.mServices.end() && iter->second == ESM::NPC::Training) + return QVariant(); + } + + return ActorRefIdAdapter::getData(column, data, index); } -void CSMWorld::CreatureRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::CreatureRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); - if (column==mColumns.mType) + if (column == mColumns.mType) creature.mData.mType = value.toInt(); - else if (column==mColumns.mScale) + else if (column == mColumns.mScale) creature.mScale = value.toFloat(); - else if (column==mColumns.mOriginal) - creature.mOriginal = value.toString().toUtf8().constData(); + else if (column == mColumns.mOriginal) + creature.mOriginal = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); else if (column == mColumns.mBloodType) creature.mBloodType = value.toInt(); else { - std::map::const_iterator iter = - mColumns.mFlags.find (column); + std::map::const_iterator iter = mColumns.mFlags.find(column); - if (iter!=mColumns.mFlags.end()) + if (iter != mColumns.mFlags.end()) { - if (value.toInt()!=0) + if (value.toInt() != 0) creature.mFlags |= iter->second; else creature.mFlags &= ~iter->second; } else { - ActorRefIdAdapter::setData (column, data, index, value); + ActorRefIdAdapter::setData(column, data, index, value); return; } @@ -543,42 +591,43 @@ void CSMWorld::CreatureRefIdAdapter::setData (const RefIdColumn *column, RefIdDa record.setModified(creature); } -CSMWorld::DoorRefIdAdapter::DoorRefIdAdapter (const NameColumns& columns, - const RefIdColumn *openSound, const RefIdColumn *closeSound) -: NameRefIdAdapter (UniversalId::Type_Door, columns), mOpenSound (openSound), - mCloseSound (closeSound) -{} +CSMWorld::DoorRefIdAdapter::DoorRefIdAdapter( + const NameColumns& columns, const RefIdColumn* openSound, const RefIdColumn* closeSound) + : NameRefIdAdapter(UniversalId::Type_Door, columns) + , mOpenSound(openSound) + , mCloseSound(closeSound) +{ +} -QVariant CSMWorld::DoorRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const +QVariant CSMWorld::DoorRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Door))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Door))); - if (column==mOpenSound) - return QString::fromUtf8 (record.get().mOpenSound.c_str()); + if (column == mOpenSound) + return QString::fromUtf8(record.get().mOpenSound.getRefIdString().c_str()); - if (column==mCloseSound) - return QString::fromUtf8 (record.get().mCloseSound.c_str()); + if (column == mCloseSound) + return QString::fromUtf8(record.get().mCloseSound.getRefIdString().c_str()); - return NameRefIdAdapter::getData (column, data, index); + return NameRefIdAdapter::getData(column, data, index); } -void CSMWorld::DoorRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::DoorRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Door))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Door))); ESM::Door door = record.get(); - if (column==mOpenSound) - door.mOpenSound = value.toString().toUtf8().constData(); - else if (column==mCloseSound) - door.mCloseSound = value.toString().toUtf8().constData(); + if (column == mOpenSound) + door.mOpenSound = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); + else if (column == mCloseSound) + door.mCloseSound = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); else { - NameRefIdAdapter::setData (column, data, index, value); + NameRefIdAdapter::setData(column, data, index, value); return; } @@ -586,36 +635,38 @@ void CSMWorld::DoorRefIdAdapter::setData (const RefIdColumn *column, RefIdData& record.setModified(door); } -CSMWorld::LightColumns::LightColumns (const InventoryColumns& columns) -: InventoryColumns (columns) -, mTime(nullptr) -, mRadius(nullptr) -, mColor(nullptr) -, mSound(nullptr) -, mEmitterType(nullptr) -{} +CSMWorld::LightColumns::LightColumns(const InventoryColumns& columns) + : InventoryColumns(columns) + , mTime(nullptr) + , mRadius(nullptr) + , mColor(nullptr) + , mSound(nullptr) + , mEmitterType(nullptr) +{ +} -CSMWorld::LightRefIdAdapter::LightRefIdAdapter (const LightColumns& columns) -: InventoryRefIdAdapter (UniversalId::Type_Light, columns), mColumns (columns) -{} +CSMWorld::LightRefIdAdapter::LightRefIdAdapter(const LightColumns& columns) + : InventoryRefIdAdapter(UniversalId::Type_Light, columns) + , mColumns(columns) +{ +} -QVariant CSMWorld::LightRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const +QVariant CSMWorld::LightRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Light))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Light))); - if (column==mColumns.mTime) + if (column == mColumns.mTime) return record.get().mData.mTime; - if (column==mColumns.mRadius) + if (column == mColumns.mRadius) return record.get().mData.mRadius; - if (column==mColumns.mColor) + if (column == mColumns.mColor) return record.get().mData.mColor; - if (column==mColumns.mSound) - return QString::fromUtf8 (record.get().mSound.c_str()); + if (column == mColumns.mSound) + return QString::fromUtf8(record.get().mSound.getRefIdString().c_str()); if (column == mColumns.mEmitterType) { @@ -636,31 +687,30 @@ QVariant CSMWorld::LightRefIdAdapter::getData (const RefIdColumn *column, const return 0; } - std::map::const_iterator iter = - mColumns.mFlags.find (column); + std::map::const_iterator iter = mColumns.mFlags.find(column); - if (iter!=mColumns.mFlags.end()) - return (record.get().mData.mFlags & iter->second)!=0; + if (iter != mColumns.mFlags.end()) + return (record.get().mData.mFlags & iter->second) != 0; - return InventoryRefIdAdapter::getData (column, data, index); + return InventoryRefIdAdapter::getData(column, data, index); } -void CSMWorld::LightRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::LightRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Light))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Light))); ESM::Light light = record.get(); - if (column==mColumns.mTime) + if (column == mColumns.mTime) light.mData.mTime = value.toInt(); - else if (column==mColumns.mRadius) + else if (column == mColumns.mRadius) light.mData.mRadius = value.toInt(); - else if (column==mColumns.mColor) + else if (column == mColumns.mColor) light.mData.mColor = value.toInt(); - else if (column==mColumns.mSound) - light.mSound = value.toString().toUtf8().constData(); + else if (column == mColumns.mSound) + light.mSound = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); else if (column == mColumns.mEmitterType) { int mask = ~(ESM::Light::Flicker | ESM::Light::FlickerSlow | ESM::Light::Pulse | ESM::Light::PulseSlow); @@ -678,56 +728,56 @@ void CSMWorld::LightRefIdAdapter::setData (const RefIdColumn *column, RefIdData& } else { - std::map::const_iterator iter = - mColumns.mFlags.find (column); + std::map::const_iterator iter = mColumns.mFlags.find(column); - if (iter!=mColumns.mFlags.end()) + if (iter != mColumns.mFlags.end()) { - if (value.toInt()!=0) + if (value.toInt() != 0) light.mData.mFlags |= iter->second; else light.mData.mFlags &= ~iter->second; } else { - InventoryRefIdAdapter::setData (column, data, index, value); + InventoryRefIdAdapter::setData(column, data, index, value); return; } } - record.setModified (light); + record.setModified(light); } -CSMWorld::MiscRefIdAdapter::MiscRefIdAdapter (const InventoryColumns& columns, const RefIdColumn *key) -: InventoryRefIdAdapter (UniversalId::Type_Miscellaneous, columns), mKey (key) -{} +CSMWorld::MiscRefIdAdapter::MiscRefIdAdapter(const InventoryColumns& columns, const RefIdColumn* key) + : InventoryRefIdAdapter(UniversalId::Type_Miscellaneous, columns) + , mKey(key) +{ +} -QVariant CSMWorld::MiscRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const +QVariant CSMWorld::MiscRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Miscellaneous))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Miscellaneous))); - if (column==mKey) - return record.get().mData.mIsKey!=0; + if (column == mKey) + return bool(record.get().mData.mFlags & ESM::Miscellaneous::Key); - return InventoryRefIdAdapter::getData (column, data, index); + return InventoryRefIdAdapter::getData(column, data, index); } -void CSMWorld::MiscRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::MiscRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Miscellaneous))); + Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Miscellaneous))); ESM::Miscellaneous misc = record.get(); - if (column==mKey) - misc.mData.mIsKey = value.toInt(); + if (column == mKey) + misc.mData.mFlags = value.toInt(); else { - InventoryRefIdAdapter::setData (column, data, index, value); + InventoryRefIdAdapter::setData(column, data, index, value); return; } @@ -735,46 +785,48 @@ void CSMWorld::MiscRefIdAdapter::setData (const RefIdColumn *column, RefIdData& record.setModified(misc); } -CSMWorld::NpcColumns::NpcColumns (const ActorColumns& actorColumns) -: ActorColumns (actorColumns), - mRace(nullptr), - mClass(nullptr), - mFaction(nullptr), - mHair(nullptr), - mHead(nullptr), - mAttributes(nullptr), - mSkills(nullptr), - mMisc(nullptr), - mBloodType(nullptr), - mGender(nullptr) -{} +CSMWorld::NpcColumns::NpcColumns(const ActorColumns& actorColumns) + : ActorColumns(actorColumns) + , mRace(nullptr) + , mClass(nullptr) + , mFaction(nullptr) + , mHair(nullptr) + , mHead(nullptr) + , mAttributes(nullptr) + , mSkills(nullptr) + , mMisc(nullptr) + , mBloodType(nullptr) + , mGender(nullptr) +{ +} -CSMWorld::NpcRefIdAdapter::NpcRefIdAdapter (const NpcColumns& columns) -: ActorRefIdAdapter (UniversalId::Type_Npc, columns), mColumns (columns) -{} +CSMWorld::NpcRefIdAdapter::NpcRefIdAdapter(const NpcColumns& columns) + : ActorRefIdAdapter(UniversalId::Type_Npc, columns) + , mColumns(columns) +{ +} -QVariant CSMWorld::NpcRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) - const +QVariant CSMWorld::NpcRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Npc))); - if (column==mColumns.mRace) - return QString::fromUtf8 (record.get().mRace.c_str()); + if (column == mColumns.mRace) + return QString::fromUtf8(record.get().mRace.getRefIdString().c_str()); - if (column==mColumns.mClass) - return QString::fromUtf8 (record.get().mClass.c_str()); + if (column == mColumns.mClass) + return QString::fromUtf8(record.get().mClass.getRefIdString().c_str()); - if (column==mColumns.mFaction) - return QString::fromUtf8 (record.get().mFaction.c_str()); + if (column == mColumns.mFaction) + return QString::fromUtf8(record.get().mFaction.getRefIdString().c_str()); - if (column==mColumns.mHair) - return QString::fromUtf8 (record.get().mHair.c_str()); + if (column == mColumns.mHair) + return QString::fromUtf8(record.get().mHair.getRefIdString().c_str()); - if (column==mColumns.mHead) - return QString::fromUtf8 (record.get().mHead.c_str()); + if (column == mColumns.mHead) + return QString::fromUtf8(record.get().mHead.getRefIdString().c_str()); - if (column==mColumns.mAttributes || column==mColumns.mSkills) + if (column == mColumns.mAttributes || column == mColumns.mSkills) { if ((record.get().mFlags & ESM::NPC::Autocalc) != 0) return QVariant::fromValue(ColumnBase::TableEdit_None); @@ -782,7 +834,7 @@ QVariant CSMWorld::NpcRefIdAdapter::getData (const RefIdColumn *column, const Re return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); } - if (column==mColumns.mMisc) + if (column == mColumns.mMisc) return QVariant::fromValue(ColumnBase::TableEdit_Full); if (column == mColumns.mBloodType) @@ -797,33 +849,32 @@ QVariant CSMWorld::NpcRefIdAdapter::getData (const RefIdColumn *column, const Re return 0; } - std::map::const_iterator iter = - mColumns.mFlags.find (column); + std::map::const_iterator iter = mColumns.mFlags.find(column); - if (iter!=mColumns.mFlags.end()) - return (record.get().mFlags & iter->second)!=0; + if (iter != mColumns.mFlags.end()) + return (record.get().mFlags & iter->second) != 0; - return ActorRefIdAdapter::getData (column, data, index); + return ActorRefIdAdapter::getData(column, data, index); } -void CSMWorld::NpcRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::NpcRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); - if (column==mColumns.mRace) - npc.mRace = value.toString().toUtf8().constData(); - else if (column==mColumns.mClass) - npc.mClass = value.toString().toUtf8().constData(); - else if (column==mColumns.mFaction) - npc.mFaction = value.toString().toUtf8().constData(); - else if (column==mColumns.mHair) - npc.mHair = value.toString().toUtf8().constData(); - else if (column==mColumns.mHead) - npc.mHead = value.toString().toUtf8().constData(); + if (column == mColumns.mRace) + npc.mRace = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); + else if (column == mColumns.mClass) + npc.mClass = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); + else if (column == mColumns.mFaction) + npc.mFaction = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); + else if (column == mColumns.mHair) + npc.mHair = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); + else if (column == mColumns.mHead) + npc.mHead = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); else if (column == mColumns.mBloodType) npc.mBloodType = value.toInt(); else if (column == mColumns.mGender) @@ -836,190 +887,159 @@ void CSMWorld::NpcRefIdAdapter::setData (const RefIdColumn *column, RefIdData& d } else { - std::map::const_iterator iter = - mColumns.mFlags.find (column); + std::map::const_iterator iter = mColumns.mFlags.find(column); - if (iter!=mColumns.mFlags.end()) + if (iter != mColumns.mFlags.end()) { - if (value.toInt()!=0) + if (value.toInt() != 0) npc.mFlags |= iter->second; else npc.mFlags &= ~iter->second; if (iter->second == ESM::NPC::Autocalc) - npc.mNpdtType = (value.toInt() != 0) ? ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS - : ESM::NPC::NPC_DEFAULT; + npc.mNpdtType = (value.toInt() != 0) ? ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS : ESM::NPC::NPC_DEFAULT; } else { - ActorRefIdAdapter::setData (column, data, index, value); + ActorRefIdAdapter::setData(column, data, index, value); return; } } - record.setModified (npc); + record.setModified(npc); } -CSMWorld::NpcAttributesRefIdAdapter::NpcAttributesRefIdAdapter () -{} - -void CSMWorld::NpcAttributesRefIdAdapter::addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const +void CSMWorld::NpcAttributesRefIdAdapter::addNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int position) const { // Do nothing, this table cannot be changed by the user } -void CSMWorld::NpcAttributesRefIdAdapter::removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const +void CSMWorld::NpcAttributesRefIdAdapter::removeNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } -void CSMWorld::NpcAttributesRefIdAdapter::setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const +void CSMWorld::NpcAttributesRefIdAdapter::setNestedTable( + const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); // store the whole struct - npc.mNpdt = - static_cast > &>(nestedTable).mNestedTable.at(0); + npc.mNpdt + = static_cast>&>(nestedTable).mNestedTable.at(0); - record.setModified (npc); + record.setModified(npc); } -CSMWorld::NestedTableWrapperBase* CSMWorld::NpcAttributesRefIdAdapter::nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const +CSMWorld::NestedTableWrapperBase* CSMWorld::NpcAttributesRefIdAdapter::nestedTable( + const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Npc))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mNpdt); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(wrap); + return new NestedTableWrapper>(wrap); } -QVariant CSMWorld::NpcAttributesRefIdAdapter::getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const +QVariant CSMWorld::NpcAttributesRefIdAdapter::getNestedData( + const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Npc))); const ESM::NPC::NPDTstruct52& npcStruct = record.get().mNpdt; if (subColIndex == 0) return subRowIndex; - else if (subColIndex == 1) - switch (subRowIndex) - { - case 0: return static_cast(npcStruct.mStrength); - case 1: return static_cast(npcStruct.mIntelligence); - case 2: return static_cast(npcStruct.mWillpower); - case 3: return static_cast(npcStruct.mAgility); - case 4: return static_cast(npcStruct.mSpeed); - case 5: return static_cast(npcStruct.mEndurance); - case 6: return static_cast(npcStruct.mPersonality); - case 7: return static_cast(npcStruct.mLuck); - default: return QVariant(); // throw an exception here? - } - else - return QVariant(); // throw an exception here? + else if (subColIndex == 1 && subRowIndex >= 0 && subRowIndex < ESM::Attribute::Length) + return static_cast(npcStruct.mAttributes[subRowIndex]); + return QVariant(); // throw an exception here? } -void CSMWorld::NpcAttributesRefIdAdapter::setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const +void CSMWorld::NpcAttributesRefIdAdapter::setNestedData( + const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Npc))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); ESM::NPC::NPDTstruct52& npcStruct = npc.mNpdt; - if (subColIndex == 1) - switch(subRowIndex) - { - case 0: npcStruct.mStrength = static_cast(value.toInt()); break; - case 1: npcStruct.mIntelligence = static_cast(value.toInt()); break; - case 2: npcStruct.mWillpower = static_cast(value.toInt()); break; - case 3: npcStruct.mAgility = static_cast(value.toInt()); break; - case 4: npcStruct.mSpeed = static_cast(value.toInt()); break; - case 5: npcStruct.mEndurance = static_cast(value.toInt()); break; - case 6: npcStruct.mPersonality = static_cast(value.toInt()); break; - case 7: npcStruct.mLuck = static_cast(value.toInt()); break; - default: return; // throw an exception here? - } + if (subColIndex == 1 && subRowIndex >= 0 && subRowIndex < ESM::Attribute::Length) + npcStruct.mAttributes[subRowIndex] = static_cast(value.toInt()); else return; // throw an exception here? - record.setModified (npc); + record.setModified(npc); } -int CSMWorld::NpcAttributesRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const +int CSMWorld::NpcAttributesRefIdAdapter::getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const { return 2; } -int CSMWorld::NpcAttributesRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const +int CSMWorld::NpcAttributesRefIdAdapter::getNestedRowsCount( + const RefIdColumn* column, const RefIdData& data, int index) const { - // There are 8 attributes - return 8; + return ESM::Attribute::Length; } -CSMWorld::NpcSkillsRefIdAdapter::NpcSkillsRefIdAdapter () -{} - -void CSMWorld::NpcSkillsRefIdAdapter::addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const +void CSMWorld::NpcSkillsRefIdAdapter::addNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int position) const { // Do nothing, this table cannot be changed by the user } -void CSMWorld::NpcSkillsRefIdAdapter::removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const +void CSMWorld::NpcSkillsRefIdAdapter::removeNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } -void CSMWorld::NpcSkillsRefIdAdapter::setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const +void CSMWorld::NpcSkillsRefIdAdapter::setNestedTable( + const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); // store the whole struct - npc.mNpdt = - static_cast > &>(nestedTable).mNestedTable.at(0); + npc.mNpdt + = static_cast>&>(nestedTable).mNestedTable.at(0); - record.setModified (npc); + record.setModified(npc); } -CSMWorld::NestedTableWrapperBase* CSMWorld::NpcSkillsRefIdAdapter::nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const +CSMWorld::NestedTableWrapperBase* CSMWorld::NpcSkillsRefIdAdapter::nestedTable( + const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Npc))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mNpdt); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(wrap); + return new NestedTableWrapper>(wrap); } -QVariant CSMWorld::NpcSkillsRefIdAdapter::getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const +QVariant CSMWorld::NpcSkillsRefIdAdapter::getNestedData( + const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Npc))); const ESM::NPC::NPDTstruct52& npcStruct = record.get().mNpdt; if (subRowIndex < 0 || subRowIndex >= ESM::Skill::Length) - throw std::runtime_error ("index out of range"); + throw std::runtime_error("index out of range"); if (subColIndex == 0) return subRowIndex; @@ -1029,535 +1049,572 @@ QVariant CSMWorld::NpcSkillsRefIdAdapter::getNestedData (const RefIdColumn *colu return QVariant(); // throw an exception here? } -void CSMWorld::NpcSkillsRefIdAdapter::setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const +void CSMWorld::NpcSkillsRefIdAdapter::setNestedData( + const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Npc))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); ESM::NPC::NPDTstruct52& npcStruct = npc.mNpdt; if (subRowIndex < 0 || subRowIndex >= ESM::Skill::Length) - throw std::runtime_error ("index out of range"); + throw std::runtime_error("index out of range"); if (subColIndex == 1) npcStruct.mSkills[subRowIndex] = static_cast(value.toInt()); else return; // throw an exception here? - record.setModified (npc); + record.setModified(npc); } -int CSMWorld::NpcSkillsRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const +int CSMWorld::NpcSkillsRefIdAdapter::getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const { return 2; } -int CSMWorld::NpcSkillsRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const +int CSMWorld::NpcSkillsRefIdAdapter::getNestedRowsCount( + const RefIdColumn* column, const RefIdData& data, int index) const { // There are 27 skills return ESM::Skill::Length; } -CSMWorld::NpcMiscRefIdAdapter::NpcMiscRefIdAdapter () -{} - -CSMWorld::NpcMiscRefIdAdapter::~NpcMiscRefIdAdapter() -{} - -void CSMWorld::NpcMiscRefIdAdapter::addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const +void CSMWorld::NpcMiscRefIdAdapter::addNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int position) const { - throw std::logic_error ("cannot add a row to a fixed table"); + throw std::logic_error("cannot add a row to a fixed table"); } -void CSMWorld::NpcMiscRefIdAdapter::removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const +void CSMWorld::NpcMiscRefIdAdapter::removeNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const { - throw std::logic_error ("cannot remove a row to a fixed table"); + throw std::logic_error("cannot remove a row to a fixed table"); } -void CSMWorld::NpcMiscRefIdAdapter::setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const +void CSMWorld::NpcMiscRefIdAdapter::setNestedTable( + const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } -CSMWorld::NestedTableWrapperBase* CSMWorld::NpcMiscRefIdAdapter::nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const +CSMWorld::NestedTableWrapperBase* CSMWorld::NpcMiscRefIdAdapter::nestedTable( + const RefIdColumn* column, const RefIdData& data, int index) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } -QVariant CSMWorld::NpcMiscRefIdAdapter::getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const +QVariant CSMWorld::NpcMiscRefIdAdapter::getNestedData( + const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Npc))); bool autoCalc = (record.get().mFlags & ESM::NPC::Autocalc) != 0; if (autoCalc) switch (subColIndex) { - case 0: return static_cast(record.get().mNpdt.mLevel); - case 1: return QVariant(QVariant::UserType); - case 2: return QVariant(QVariant::UserType); - case 3: return QVariant(QVariant::UserType); - case 4: return static_cast(record.get().mNpdt.mDisposition); - case 5: return static_cast(record.get().mNpdt.mReputation); - case 6: return static_cast(record.get().mNpdt.mRank); - case 7: return record.get().mNpdt.mGold; - case 8: return record.get().mPersistent == true; - default: return QVariant(); // throw an exception here? + case 0: + return static_cast(record.get().mNpdt.mLevel); + case 1: + return CSMWorld::DisableTag::getVariant(); + case 2: + return CSMWorld::DisableTag::getVariant(); + case 3: + return CSMWorld::DisableTag::getVariant(); + case 4: + return static_cast(record.get().mNpdt.mDisposition); + case 5: + return static_cast(record.get().mNpdt.mReputation); + case 6: + return static_cast(record.get().mNpdt.mRank); + case 7: + return record.get().mNpdt.mGold; + default: + return QVariant(); // throw an exception here? } else switch (subColIndex) { - case 0: return static_cast(record.get().mNpdt.mLevel); - case 1: return static_cast(record.get().mNpdt.mHealth); - case 2: return static_cast(record.get().mNpdt.mMana); - case 3: return static_cast(record.get().mNpdt.mFatigue); - case 4: return static_cast(record.get().mNpdt.mDisposition); - case 5: return static_cast(record.get().mNpdt.mReputation); - case 6: return static_cast(record.get().mNpdt.mRank); - case 7: return record.get().mNpdt.mGold; - case 8: return record.get().mPersistent == true; - default: return QVariant(); // throw an exception here? + case 0: + return static_cast(record.get().mNpdt.mLevel); + case 1: + return static_cast(record.get().mNpdt.mHealth); + case 2: + return static_cast(record.get().mNpdt.mMana); + case 3: + return static_cast(record.get().mNpdt.mFatigue); + case 4: + return static_cast(record.get().mNpdt.mDisposition); + case 5: + return static_cast(record.get().mNpdt.mReputation); + case 6: + return static_cast(record.get().mNpdt.mRank); + case 7: + return record.get().mNpdt.mGold; + default: + return QVariant(); // throw an exception here? } } -void CSMWorld::NpcMiscRefIdAdapter::setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const +void CSMWorld::NpcMiscRefIdAdapter::setNestedData( + const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Npc))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, UniversalId::Type_Npc))); ESM::NPC npc = record.get(); bool autoCalc = (record.get().mFlags & ESM::NPC::Autocalc) != 0; if (autoCalc) - switch(subColIndex) + switch (subColIndex) { - case 0: npc.mNpdt.mLevel = static_cast(value.toInt()); break; - case 1: return; - case 2: return; - case 3: return; - case 4: npc.mNpdt.mDisposition = static_cast(value.toInt()); break; - case 5: npc.mNpdt.mReputation = static_cast(value.toInt()); break; - case 6: npc.mNpdt.mRank = static_cast(value.toInt()); break; - case 7: npc.mNpdt.mGold = value.toInt(); break; - case 8: npc.mPersistent = value.toBool(); break; - default: return; // throw an exception here? + case 0: + npc.mNpdt.mLevel = static_cast(value.toInt()); + break; + case 1: + return; + case 2: + return; + case 3: + return; + case 4: + npc.mNpdt.mDisposition = static_cast(value.toInt()); + break; + case 5: + npc.mNpdt.mReputation = static_cast(value.toInt()); + break; + case 6: + npc.mNpdt.mRank = static_cast(value.toInt()); + break; + case 7: + npc.mNpdt.mGold = value.toInt(); + break; + default: + return; // throw an exception here? } else - switch(subColIndex) + switch (subColIndex) { - case 0: npc.mNpdt.mLevel = static_cast(value.toInt()); break; - case 1: npc.mNpdt.mHealth = static_cast(value.toInt()); break; - case 2: npc.mNpdt.mMana = static_cast(value.toInt()); break; - case 3: npc.mNpdt.mFatigue = static_cast(value.toInt()); break; - case 4: npc.mNpdt.mDisposition = static_cast(value.toInt()); break; - case 5: npc.mNpdt.mReputation = static_cast(value.toInt()); break; - case 6: npc.mNpdt.mRank = static_cast(value.toInt()); break; - case 7: npc.mNpdt.mGold = value.toInt(); break; - case 8: npc.mPersistent = value.toBool(); break; - default: return; // throw an exception here? + case 0: + npc.mNpdt.mLevel = static_cast(value.toInt()); + break; + case 1: + npc.mNpdt.mHealth = static_cast(value.toInt()); + break; + case 2: + npc.mNpdt.mMana = static_cast(value.toInt()); + break; + case 3: + npc.mNpdt.mFatigue = static_cast(value.toInt()); + break; + case 4: + npc.mNpdt.mDisposition = static_cast(value.toInt()); + break; + case 5: + npc.mNpdt.mReputation = static_cast(value.toInt()); + break; + case 6: + npc.mNpdt.mRank = static_cast(value.toInt()); + break; + case 7: + npc.mNpdt.mGold = value.toInt(); + break; + default: + return; // throw an exception here? } - record.setModified (npc); + record.setModified(npc); } -int CSMWorld::NpcMiscRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const +int CSMWorld::NpcMiscRefIdAdapter::getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const { - return 9; // Level, Health, Mana, Fatigue, Disposition, Reputation, Rank, Gold, Persist + return 8; // Level, Health, Mana, Fatigue, Disposition, Reputation, Rank, Gold } -int CSMWorld::NpcMiscRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const +int CSMWorld::NpcMiscRefIdAdapter::getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const { return 1; // fixed at size 1 } -CSMWorld::CreatureAttributesRefIdAdapter::CreatureAttributesRefIdAdapter() -{} - -void CSMWorld::CreatureAttributesRefIdAdapter::addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const +void CSMWorld::CreatureAttributesRefIdAdapter::addNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int position) const { // Do nothing, this table cannot be changed by the user } -void CSMWorld::CreatureAttributesRefIdAdapter::removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const +void CSMWorld::CreatureAttributesRefIdAdapter::removeNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } -void CSMWorld::CreatureAttributesRefIdAdapter::setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const +void CSMWorld::CreatureAttributesRefIdAdapter::setNestedTable( + const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); // store the whole struct - creature.mData = - static_cast > &>(nestedTable).mNestedTable.at(0); + creature.mData = static_cast>&>(nestedTable) + .mNestedTable.at(0); - record.setModified (creature); + record.setModified(creature); } -CSMWorld::NestedTableWrapperBase* CSMWorld::CreatureAttributesRefIdAdapter::nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const +CSMWorld::NestedTableWrapperBase* CSMWorld::CreatureAttributesRefIdAdapter::nestedTable( + const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Creature))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(wrap); + return new NestedTableWrapper>(wrap); } -QVariant CSMWorld::CreatureAttributesRefIdAdapter::getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const +QVariant CSMWorld::CreatureAttributesRefIdAdapter::getNestedData( + const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Creature))); const ESM::Creature& creature = record.get(); if (subColIndex == 0) return subRowIndex; - else if (subColIndex == 1) - switch (subRowIndex) - { - case 0: return creature.mData.mStrength; - case 1: return creature.mData.mIntelligence; - case 2: return creature.mData.mWillpower; - case 3: return creature.mData.mAgility; - case 4: return creature.mData.mSpeed; - case 5: return creature.mData.mEndurance; - case 6: return creature.mData.mPersonality; - case 7: return creature.mData.mLuck; - default: return QVariant(); // throw an exception here? - } - else - return QVariant(); // throw an exception here? + else if (subColIndex == 1 && subRowIndex > 0 && subRowIndex < ESM::Attribute::Length) + return creature.mData.mAttributes[subRowIndex]; + return QVariant(); // throw an exception here? } -void CSMWorld::CreatureAttributesRefIdAdapter::setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const +void CSMWorld::CreatureAttributesRefIdAdapter::setNestedData( + const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Creature))); - ESM::Creature creature = record.get(); - - if (subColIndex == 1) - switch(subRowIndex) - { - case 0: creature.mData.mStrength = value.toInt(); break; - case 1: creature.mData.mIntelligence = value.toInt(); break; - case 2: creature.mData.mWillpower = value.toInt(); break; - case 3: creature.mData.mAgility = value.toInt(); break; - case 4: creature.mData.mSpeed = value.toInt(); break; - case 5: creature.mData.mEndurance = value.toInt(); break; - case 6: creature.mData.mPersonality = value.toInt(); break; - case 7: creature.mData.mLuck = value.toInt(); break; - default: return; // throw an exception here? - } - else - return; // throw an exception here? + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, UniversalId::Type_Creature))); - record.setModified (creature); + if (subColIndex == 1 && subRowIndex > 0 && subRowIndex < ESM::Attribute::Length) + { + ESM::Creature creature = record.get(); + creature.mData.mAttributes[subRowIndex] = value.toInt(); + record.setModified(creature); + } + // throw an exception here? } -int CSMWorld::CreatureAttributesRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const +int CSMWorld::CreatureAttributesRefIdAdapter::getNestedColumnsCount( + const RefIdColumn* column, const RefIdData& data) const { return 2; } -int CSMWorld::CreatureAttributesRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const +int CSMWorld::CreatureAttributesRefIdAdapter::getNestedRowsCount( + const RefIdColumn* column, const RefIdData& data, int index) const { - // There are 8 attributes - return 8; + return ESM::Attribute::Length; } -CSMWorld::CreatureAttackRefIdAdapter::CreatureAttackRefIdAdapter() -{} - -void CSMWorld::CreatureAttackRefIdAdapter::addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const +void CSMWorld::CreatureAttackRefIdAdapter::addNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int position) const { // Do nothing, this table cannot be changed by the user } -void CSMWorld::CreatureAttackRefIdAdapter::removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const +void CSMWorld::CreatureAttackRefIdAdapter::removeNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } -void CSMWorld::CreatureAttackRefIdAdapter::setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const +void CSMWorld::CreatureAttackRefIdAdapter::setNestedTable( + const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); // store the whole struct - creature.mData = - static_cast > &>(nestedTable).mNestedTable.at(0); + creature.mData = static_cast>&>(nestedTable) + .mNestedTable.at(0); - record.setModified (creature); + record.setModified(creature); } -CSMWorld::NestedTableWrapperBase* CSMWorld::CreatureAttackRefIdAdapter::nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const +CSMWorld::NestedTableWrapperBase* CSMWorld::CreatureAttackRefIdAdapter::nestedTable( + const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Creature))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(wrap); + return new NestedTableWrapper>(wrap); } -QVariant CSMWorld::CreatureAttackRefIdAdapter::getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const +QVariant CSMWorld::CreatureAttackRefIdAdapter::getNestedData( + const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Creature))); const ESM::Creature& creature = record.get(); if (subRowIndex < 0 || subRowIndex > 2) - throw std::runtime_error ("index out of range"); + throw std::runtime_error("index out of range"); if (subColIndex == 0) return subRowIndex + 1; else if (subColIndex == 1 || subColIndex == 2) return creature.mData.mAttack[(subRowIndex * 2) + (subColIndex - 1)]; else - throw std::runtime_error ("index out of range"); + throw std::runtime_error("index out of range"); } -void CSMWorld::CreatureAttackRefIdAdapter::setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const +void CSMWorld::CreatureAttackRefIdAdapter::setNestedData( + const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Creature))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); if (subRowIndex < 0 || subRowIndex > 2) - throw std::runtime_error ("index out of range"); + throw std::runtime_error("index out of range"); if (subColIndex == 1 || subColIndex == 2) creature.mData.mAttack[(subRowIndex * 2) + (subColIndex - 1)] = value.toInt(); else return; // throw an exception here? - record.setModified (creature); + record.setModified(creature); } -int CSMWorld::CreatureAttackRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const +int CSMWorld::CreatureAttackRefIdAdapter::getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const { return 3; } -int CSMWorld::CreatureAttackRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const +int CSMWorld::CreatureAttackRefIdAdapter::getNestedRowsCount( + const RefIdColumn* column, const RefIdData& data, int index) const { // There are 3 attacks return 3; } -CSMWorld::CreatureMiscRefIdAdapter::CreatureMiscRefIdAdapter() -{} - -CSMWorld::CreatureMiscRefIdAdapter::~CreatureMiscRefIdAdapter() -{} - -void CSMWorld::CreatureMiscRefIdAdapter::addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const +void CSMWorld::CreatureMiscRefIdAdapter::addNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int position) const { - throw std::logic_error ("cannot add a row to a fixed table"); + throw std::logic_error("cannot add a row to a fixed table"); } -void CSMWorld::CreatureMiscRefIdAdapter::removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const +void CSMWorld::CreatureMiscRefIdAdapter::removeNestedRow( + const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const { - throw std::logic_error ("cannot remove a row to a fixed table"); + throw std::logic_error("cannot remove a row to a fixed table"); } -void CSMWorld::CreatureMiscRefIdAdapter::setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const +void CSMWorld::CreatureMiscRefIdAdapter::setNestedTable( + const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } -CSMWorld::NestedTableWrapperBase* CSMWorld::CreatureMiscRefIdAdapter::nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const +CSMWorld::NestedTableWrapperBase* CSMWorld::CreatureMiscRefIdAdapter::nestedTable( + const RefIdColumn* column, const RefIdData& data, int index) const { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } -QVariant CSMWorld::CreatureMiscRefIdAdapter::getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const +QVariant CSMWorld::CreatureMiscRefIdAdapter::getNestedData( + const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, int subColIndex) const { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Creature))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Creature))); const ESM::Creature& creature = record.get(); switch (subColIndex) { - case 0: return creature.mData.mLevel; - case 1: return creature.mData.mHealth; - case 2: return creature.mData.mMana; - case 3: return creature.mData.mFatigue; - case 4: return creature.mData.mSoul; - case 5: return creature.mData.mCombat; - case 6: return creature.mData.mMagic; - case 7: return creature.mData.mStealth; - case 8: return creature.mData.mGold; - default: return QVariant(); // throw an exception here? + case 0: + return creature.mData.mLevel; + case 1: + return creature.mData.mHealth; + case 2: + return creature.mData.mMana; + case 3: + return creature.mData.mFatigue; + case 4: + return creature.mData.mSoul; + case 5: + return creature.mData.mCombat; + case 6: + return creature.mData.mMagic; + case 7: + return creature.mData.mStealth; + case 8: + return creature.mData.mGold; + default: + return QVariant(); // throw an exception here? } } -void CSMWorld::CreatureMiscRefIdAdapter::setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const +void CSMWorld::CreatureMiscRefIdAdapter::setNestedData( + const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, UniversalId::Type_Creature))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); - switch(subColIndex) + switch (subColIndex) { - case 0: creature.mData.mLevel = value.toInt(); break; - case 1: creature.mData.mHealth = value.toInt(); break; - case 2: creature.mData.mMana = value.toInt(); break; - case 3: creature.mData.mFatigue = value.toInt(); break; - case 4: creature.mData.mSoul = value.toInt(); break; - case 5: creature.mData.mCombat = value.toInt(); break; - case 6: creature.mData.mMagic = value.toInt(); break; - case 7: creature.mData.mStealth = value.toInt(); break; - case 8: creature.mData.mGold = value.toInt(); break; - default: return; // throw an exception here? + case 0: + creature.mData.mLevel = value.toInt(); + break; + case 1: + creature.mData.mHealth = value.toInt(); + break; + case 2: + creature.mData.mMana = value.toInt(); + break; + case 3: + creature.mData.mFatigue = value.toInt(); + break; + case 4: + creature.mData.mSoul = value.toInt(); + break; + case 5: + creature.mData.mCombat = value.toInt(); + break; + case 6: + creature.mData.mMagic = value.toInt(); + break; + case 7: + creature.mData.mStealth = value.toInt(); + break; + case 8: + creature.mData.mGold = value.toInt(); + break; + default: + return; // throw an exception here? } - record.setModified (creature); + record.setModified(creature); } -int CSMWorld::CreatureMiscRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const +int CSMWorld::CreatureMiscRefIdAdapter::getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const { return 9; // Level, Health, Mana, Fatigue, Soul, Combat, Magic, Steath, Gold } -int CSMWorld::CreatureMiscRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const +int CSMWorld::CreatureMiscRefIdAdapter::getNestedRowsCount( + const RefIdColumn* column, const RefIdData& data, int index) const { return 1; // fixed at size 1 } -CSMWorld::WeaponColumns::WeaponColumns (const EnchantableColumns& columns) -: EnchantableColumns (columns) -, mType(nullptr) -, mHealth(nullptr) -, mSpeed(nullptr) -, mReach(nullptr) -, mChop{nullptr} -, mSlash{nullptr} -, mThrust{nullptr} -{} +CSMWorld::WeaponColumns::WeaponColumns(const EnchantableColumns& columns) + : EnchantableColumns(columns) + , mType(nullptr) + , mHealth(nullptr) + , mSpeed(nullptr) + , mReach(nullptr) + , mChop{ nullptr } + , mSlash{ nullptr } + , mThrust{ nullptr } +{ +} -CSMWorld::WeaponRefIdAdapter::WeaponRefIdAdapter (const WeaponColumns& columns) -: EnchantableRefIdAdapter (UniversalId::Type_Weapon, columns), mColumns (columns) -{} +CSMWorld::WeaponRefIdAdapter::WeaponRefIdAdapter(const WeaponColumns& columns) + : EnchantableRefIdAdapter(UniversalId::Type_Weapon, columns) + , mColumns(columns) +{ +} -QVariant CSMWorld::WeaponRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const +QVariant CSMWorld::WeaponRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Weapon))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Weapon))); - if (column==mColumns.mType) + if (column == mColumns.mType) return record.get().mData.mType; - if (column==mColumns.mHealth) + if (column == mColumns.mHealth) return record.get().mData.mHealth; - if (column==mColumns.mSpeed) + if (column == mColumns.mSpeed) return record.get().mData.mSpeed; - if (column==mColumns.mReach) + if (column == mColumns.mReach) return record.get().mData.mReach; - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { - if (column==mColumns.mChop[i]) + if (column == mColumns.mChop[i]) return record.get().mData.mChop[i]; - if (column==mColumns.mSlash[i]) + if (column == mColumns.mSlash[i]) return record.get().mData.mSlash[i]; - if (column==mColumns.mThrust[i]) + if (column == mColumns.mThrust[i]) return record.get().mData.mThrust[i]; } - std::map::const_iterator iter = - mColumns.mFlags.find (column); + std::map::const_iterator iter = mColumns.mFlags.find(column); - if (iter!=mColumns.mFlags.end()) - return (record.get().mData.mFlags & iter->second)!=0; + if (iter != mColumns.mFlags.end()) + return (record.get().mData.mFlags & iter->second) != 0; - return EnchantableRefIdAdapter::getData (column, data, index); + return EnchantableRefIdAdapter::getData(column, data, index); } -void CSMWorld::WeaponRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const +void CSMWorld::WeaponRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Weapon))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Weapon))); ESM::Weapon weapon = record.get(); - if (column==mColumns.mType) + if (column == mColumns.mType) weapon.mData.mType = value.toInt(); - else if (column==mColumns.mHealth) + else if (column == mColumns.mHealth) weapon.mData.mHealth = value.toInt(); - else if (column==mColumns.mSpeed) + else if (column == mColumns.mSpeed) weapon.mData.mSpeed = value.toFloat(); - else if (column==mColumns.mReach) + else if (column == mColumns.mReach) weapon.mData.mReach = value.toFloat(); - else if (column==mColumns.mChop[0]) + else if (column == mColumns.mChop[0]) weapon.mData.mChop[0] = value.toInt(); - else if (column==mColumns.mChop[1]) + else if (column == mColumns.mChop[1]) weapon.mData.mChop[1] = value.toInt(); - else if (column==mColumns.mSlash[0]) + else if (column == mColumns.mSlash[0]) weapon.mData.mSlash[0] = value.toInt(); - else if (column==mColumns.mSlash[1]) + else if (column == mColumns.mSlash[1]) weapon.mData.mSlash[1] = value.toInt(); - else if (column==mColumns.mThrust[0]) + else if (column == mColumns.mThrust[0]) weapon.mData.mThrust[0] = value.toInt(); - else if (column==mColumns.mThrust[1]) + else if (column == mColumns.mThrust[1]) weapon.mData.mThrust[1] = value.toInt(); else { - std::map::const_iterator iter = - mColumns.mFlags.find (column); + std::map::const_iterator iter = mColumns.mFlags.find(column); - if (iter!=mColumns.mFlags.end()) + if (iter != mColumns.mFlags.end()) { - if (value.toInt()!=0) + if (value.toInt() != 0) weapon.mData.mFlags |= iter->second; else weapon.mData.mFlags &= ~iter->second; } else { - EnchantableRefIdAdapter::setData (column, data, index, value); + EnchantableRefIdAdapter::setData(column, data, index, value); return; // Don't overwrite changes made by base class } } diff --git a/apps/opencs/model/world/refidadapterimp.hpp b/apps/opencs/model/world/refidadapterimp.hpp index 95d1a09a269..5c54b9a0cfe 100644 --- a/apps/opencs/model/world/refidadapterimp.hpp +++ b/apps/opencs/model/world/refidadapterimp.hpp @@ -1,172 +1,236 @@ #ifndef CSM_WOLRD_REFIDADAPTERIMP_H #define CSM_WOLRD_REFIDADAPTERIMP_H +#include #include +#include +#include +#include +#include +#include +#include #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "columnbase.hpp" +#include "nestedtablewrapper.hpp" #include "record.hpp" +#include "refidadapter.hpp" #include "refiddata.hpp" #include "universalid.hpp" -#include "refidadapter.hpp" -#include "nestedtablewrapper.hpp" namespace CSMWorld { + class RefIdColumn; struct BaseColumns { - const RefIdColumn *mId; - const RefIdColumn *mModified; - const RefIdColumn *mType; + const RefIdColumn* mId; + const RefIdColumn* mModified; + const RefIdColumn* mType; + const RefIdColumn* mBlocked; + + BaseColumns() + : mId(nullptr) + , mModified(nullptr) + , mType(nullptr) + , mBlocked(nullptr) + { + } }; /// \brief Base adapter for all refereceable record types /// Adapters that can handle nested tables, needs to return valid qvariant for parent columns - template + template class BaseRefIdAdapter : public RefIdAdapter { - UniversalId::Type mType; - BaseColumns mBase; - - public: + UniversalId::Type mType; + BaseColumns mBase; - BaseRefIdAdapter (UniversalId::Type type, const BaseColumns& base); + public: + BaseRefIdAdapter(UniversalId::Type type, const BaseColumns& base); - std::string getId (const RecordBase& record) const override; + ESM::RefId getId(const RecordBase& record) const override; - void setId (RecordBase& record, const std::string& id) override; + void setId(RecordBase& record, const std::string& id) override; - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. - UniversalId::Type getType() const; + UniversalId::Type getType() const; }; - template - BaseRefIdAdapter::BaseRefIdAdapter (UniversalId::Type type, const BaseColumns& base) - : mType (type), mBase (base) - {} + template + BaseRefIdAdapter::BaseRefIdAdapter(UniversalId::Type type, const BaseColumns& base) + : mType(type) + , mBase(base) + { + } - template - void BaseRefIdAdapter::setId (RecordBase& record, const std::string& id) + template + void BaseRefIdAdapter::setId(RecordBase& record, const std::string& id) { - (dynamic_cast&> (record).get().mId) = id; + (dynamic_cast&>(record).get().mId) = ESM::RefId::stringRefId(id); } - template - std::string BaseRefIdAdapter::getId (const RecordBase& record) const + template + ESM::RefId BaseRefIdAdapter::getId(const RecordBase& record) const { - return dynamic_cast&> (record).get().mId; + return dynamic_cast&>(record).get().mId; } - template - QVariant BaseRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const + template + QVariant BaseRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); - if (column==mBase.mId) - return QString::fromUtf8 (record.get().mId.c_str()); + if (column == mBase.mId) + return QString::fromStdString(record.get().mId.toString()); - if (column==mBase.mModified) + if (column == mBase.mModified) { - if (record.mState==Record::State_Erased) - return static_cast (Record::State_Deleted); + if (record.mState == Record::State_Erased) + return static_cast(Record::State_Deleted); - return static_cast (record.mState); + return static_cast(record.mState); } - if (column==mBase.mType) - return static_cast (mType); + if (column == mBase.mType) + return static_cast(mType); + + if (column == mBase.mBlocked) + return (record.get().mRecordFlags & ESM::FLAG_Blocked) != 0; return QVariant(); } - template - void BaseRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const + template + void BaseRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); - if (column==mBase.mModified) - record.mState = static_cast (value.toInt()); + if (column == mBase.mModified) + record.mState = static_cast(value.toInt()); + else if (column == mBase.mBlocked) + { + RecordT record2 = record.get(); + + if (value.toInt() != 0) + record2.mRecordFlags |= ESM::FLAG_Blocked; + else + record2.mRecordFlags &= ~ESM::FLAG_Blocked; + + record.setModified(record2); + } } - template + template UniversalId::Type BaseRefIdAdapter::getType() const { return mType; } + // NOTE: Body Part should not have persistence (but BodyPart is not listed in the Objects + // table at the moment). + // + // Spellmaking - not persistent - currently not part of objects table + // Enchanting - not persistent - currently not part of objects table + // + // Leveled Creature - no model, so not persistent + // Leveled Item - no model, so not persistent struct ModelColumns : public BaseColumns { - const RefIdColumn *mModel; + const RefIdColumn* mModel; + const RefIdColumn* mPersistence; - ModelColumns (const BaseColumns& base) : BaseColumns (base), mModel(nullptr) {} + ModelColumns(const BaseColumns& base) + : BaseColumns(base) + , mModel(nullptr) + , mPersistence(nullptr) + { + } }; /// \brief Adapter for IDs with models (all but levelled lists) - template + template class ModelRefIdAdapter : public BaseRefIdAdapter { - ModelColumns mModel; - - public: + ModelColumns mModel; - ModelRefIdAdapter (UniversalId::Type type, const ModelColumns& columns); + public: + ModelRefIdAdapter(UniversalId::Type type, const ModelColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; - template - ModelRefIdAdapter::ModelRefIdAdapter (UniversalId::Type type, const ModelColumns& columns) - : BaseRefIdAdapter (type, columns), mModel (columns) - {} + template + ModelRefIdAdapter::ModelRefIdAdapter(UniversalId::Type type, const ModelColumns& columns) + : BaseRefIdAdapter(type, columns) + , mModel(columns) + { + } - template - QVariant ModelRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const + template + QVariant ModelRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); - if (column==mModel.mModel) - return QString::fromUtf8 (record.get().mModel.c_str()); + if (column == mModel.mModel) + return QString::fromUtf8(record.get().mModel.c_str()); - return BaseRefIdAdapter::getData (column, data, index); + if (column == mModel.mPersistence) + return (record.get().mRecordFlags & ESM::FLAG_Persistent) != 0; + + return BaseRefIdAdapter::getData(column, data, index); } - template - void ModelRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const + template + void ModelRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); - if (column==mModel.mModel) + if (column == mModel.mModel) record2.mModel = value.toString().toUtf8().constData(); + else if (column == mModel.mPersistence) + { + if (value.toInt() != 0) + record2.mRecordFlags |= ESM::FLAG_Persistent; + else + record2.mRecordFlags &= ~ESM::FLAG_Persistent; + } else { - BaseRefIdAdapter::setData (column, data, index, value); + BaseRefIdAdapter::setData(column, data, index, value); return; } @@ -175,70 +239,69 @@ namespace CSMWorld struct NameColumns : public ModelColumns { - const RefIdColumn *mName; - const RefIdColumn *mScript; + const RefIdColumn* mName; + const RefIdColumn* mScript; - NameColumns (const ModelColumns& base) - : ModelColumns (base) - , mName(nullptr) - , mScript(nullptr) - {} + NameColumns(const ModelColumns& base) + : ModelColumns(base) + , mName(nullptr) + , mScript(nullptr) + { + } }; /// \brief Adapter for IDs with names (all but levelled lists and statics) - template + template class NameRefIdAdapter : public ModelRefIdAdapter { - NameColumns mName; - - public: + NameColumns mName; - NameRefIdAdapter (UniversalId::Type type, const NameColumns& columns); + public: + NameRefIdAdapter(UniversalId::Type type, const NameColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; - template - NameRefIdAdapter::NameRefIdAdapter (UniversalId::Type type, const NameColumns& columns) - : ModelRefIdAdapter (type, columns), mName (columns) - {} + template + NameRefIdAdapter::NameRefIdAdapter(UniversalId::Type type, const NameColumns& columns) + : ModelRefIdAdapter(type, columns) + , mName(columns) + { + } - template - QVariant NameRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const + template + QVariant NameRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); - if (column==mName.mName) - return QString::fromUtf8 (record.get().mName.c_str()); + if (column == mName.mName) + return QString::fromUtf8(record.get().mName.c_str()); - if (column==mName.mScript) - return QString::fromUtf8 (record.get().mScript.c_str()); + if (column == mName.mScript) + return QString::fromUtf8(record.get().mScript.getRefIdString().c_str()); - return ModelRefIdAdapter::getData (column, data, index); + return ModelRefIdAdapter::getData(column, data, index); } - template - void NameRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const + template + void NameRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); - if (column==mName.mName) + if (column == mName.mName) record2.mName = value.toString().toUtf8().constData(); - else if (column==mName.mScript) - record2.mScript = value.toString().toUtf8().constData(); + else if (column == mName.mScript) + record2.mScript = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); else { - ModelRefIdAdapter::setData (column, data, index, value); + ModelRefIdAdapter::setData(column, data, index, value); return; } @@ -247,78 +310,76 @@ namespace CSMWorld struct InventoryColumns : public NameColumns { - const RefIdColumn *mIcon; - const RefIdColumn *mWeight; - const RefIdColumn *mValue; + const RefIdColumn* mIcon; + const RefIdColumn* mWeight; + const RefIdColumn* mValue; - InventoryColumns (const NameColumns& base) - : NameColumns (base) - , mIcon(nullptr) - , mWeight(nullptr) - , mValue(nullptr) - {} + InventoryColumns(const NameColumns& base) + : NameColumns(base) + , mIcon(nullptr) + , mWeight(nullptr) + , mValue(nullptr) + { + } }; /// \brief Adapter for IDs that can go into an inventory - template + template class InventoryRefIdAdapter : public NameRefIdAdapter { - InventoryColumns mInventory; + InventoryColumns mInventory; - public: - - InventoryRefIdAdapter (UniversalId::Type type, const InventoryColumns& columns); + public: + InventoryRefIdAdapter(UniversalId::Type type, const InventoryColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; - template - InventoryRefIdAdapter::InventoryRefIdAdapter (UniversalId::Type type, - const InventoryColumns& columns) - : NameRefIdAdapter (type, columns), mInventory (columns) - {} + template + InventoryRefIdAdapter::InventoryRefIdAdapter(UniversalId::Type type, const InventoryColumns& columns) + : NameRefIdAdapter(type, columns) + , mInventory(columns) + { + } - template - QVariant InventoryRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const + template + QVariant InventoryRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); - if (column==mInventory.mIcon) - return QString::fromUtf8 (record.get().mIcon.c_str()); + if (column == mInventory.mIcon) + return QString::fromUtf8(record.get().mIcon.c_str()); - if (column==mInventory.mWeight) + if (column == mInventory.mWeight) return record.get().mData.mWeight; - if (column==mInventory.mValue) + if (column == mInventory.mValue) return record.get().mData.mValue; - return NameRefIdAdapter::getData (column, data, index); + return NameRefIdAdapter::getData(column, data, index); } - template - void InventoryRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const + template + void InventoryRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); - if (column==mInventory.mIcon) + if (column == mInventory.mIcon) record2.mIcon = value.toString().toUtf8().constData(); - else if (column==mInventory.mWeight) + else if (column == mInventory.mWeight) record2.mData.mWeight = value.toFloat(); - else if (column==mInventory.mValue) + else if (column == mInventory.mValue) record2.mData.mValue = value.toInt(); else { - NameRefIdAdapter::setData (column, data, index, value); + NameRefIdAdapter::setData(column, data, index, value); return; } @@ -327,155 +388,141 @@ namespace CSMWorld struct PotionColumns : public InventoryColumns { - const RefIdColumn *mEffects; + const RefIdColumn* mEffects; - PotionColumns (const InventoryColumns& columns); + PotionColumns(const InventoryColumns& columns); }; class PotionRefIdAdapter : public InventoryRefIdAdapter { - PotionColumns mColumns; - const RefIdColumn *mAutoCalc; - - public: + PotionColumns mColumns; + const RefIdColumn* mAutoCalc; - PotionRefIdAdapter (const PotionColumns& columns, const RefIdColumn *autoCalc); + public: + PotionRefIdAdapter(const PotionColumns& columns, const RefIdColumn* autoCalc); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; struct IngredientColumns : public InventoryColumns { - const RefIdColumn *mEffects; + const RefIdColumn* mEffects; - IngredientColumns (const InventoryColumns& columns); + IngredientColumns(const InventoryColumns& columns); }; class IngredientRefIdAdapter : public InventoryRefIdAdapter { - IngredientColumns mColumns; - - public: + IngredientColumns mColumns; - IngredientRefIdAdapter (const IngredientColumns& columns); + public: + IngredientRefIdAdapter(const IngredientColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; class IngredEffectRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; - // not implemented - IngredEffectRefIdAdapter (const IngredEffectRefIdAdapter&); - IngredEffectRefIdAdapter& operator= (const IngredEffectRefIdAdapter&); - public: - IngredEffectRefIdAdapter(); + IngredEffectRefIdAdapter(const IngredEffectRefIdAdapter&) = delete; + IngredEffectRefIdAdapter& operator=(const IngredEffectRefIdAdapter&) = delete; + ~IngredEffectRefIdAdapter() override = default; - virtual ~IngredEffectRefIdAdapter(); - - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override; + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override; - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override; + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override; - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override; - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override; + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override; - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override; - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override; - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; struct EnchantableColumns : public InventoryColumns { - const RefIdColumn *mEnchantment; - const RefIdColumn *mEnchantmentPoints; + const RefIdColumn* mEnchantment; + const RefIdColumn* mEnchantmentPoints; - EnchantableColumns (const InventoryColumns& base) - : InventoryColumns (base) - , mEnchantment(nullptr) - , mEnchantmentPoints(nullptr) - {} + EnchantableColumns(const InventoryColumns& base) + : InventoryColumns(base) + , mEnchantment(nullptr) + , mEnchantmentPoints(nullptr) + { + } }; /// \brief Adapter for enchantable IDs - template + template class EnchantableRefIdAdapter : public InventoryRefIdAdapter { - EnchantableColumns mEnchantable; - - public: + EnchantableColumns mEnchantable; - EnchantableRefIdAdapter (UniversalId::Type type, const EnchantableColumns& columns); + public: + EnchantableRefIdAdapter(UniversalId::Type type, const EnchantableColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; - template - EnchantableRefIdAdapter::EnchantableRefIdAdapter (UniversalId::Type type, - const EnchantableColumns& columns) - : InventoryRefIdAdapter (type, columns), mEnchantable (columns) - {} + template + EnchantableRefIdAdapter::EnchantableRefIdAdapter(UniversalId::Type type, const EnchantableColumns& columns) + : InventoryRefIdAdapter(type, columns) + , mEnchantable(columns) + { + } - template - QVariant EnchantableRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const + template + QVariant EnchantableRefIdAdapter::getData( + const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); - if (column==mEnchantable.mEnchantment) - return QString::fromUtf8 (record.get().mEnchant.c_str()); + if (column == mEnchantable.mEnchantment) + return QString::fromUtf8(record.get().mEnchant.getRefIdString().c_str()); - if (column==mEnchantable.mEnchantmentPoints) - return static_cast (record.get().mData.mEnchant); + if (column == mEnchantable.mEnchantmentPoints) + return static_cast(record.get().mData.mEnchant); - return InventoryRefIdAdapter::getData (column, data, index); + return InventoryRefIdAdapter::getData(column, data, index); } - template - void EnchantableRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, - int index, const QVariant& value) const + template + void EnchantableRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); - if (column==mEnchantable.mEnchantment) - record2.mEnchant = value.toString().toUtf8().constData(); - else if (column==mEnchantable.mEnchantmentPoints) + if (column == mEnchantable.mEnchantment) + record2.mEnchant = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); + else if (column == mEnchantable.mEnchantmentPoints) record2.mData.mEnchant = value.toInt(); else { - InventoryRefIdAdapter::setData (column, data, index, value); + InventoryRefIdAdapter::setData(column, data, index, value); return; } @@ -484,70 +531,69 @@ namespace CSMWorld struct ToolColumns : public InventoryColumns { - const RefIdColumn *mQuality; - const RefIdColumn *mUses; + const RefIdColumn* mQuality; + const RefIdColumn* mUses; - ToolColumns (const InventoryColumns& base) - : InventoryColumns (base) - , mQuality(nullptr) - , mUses(nullptr) - {} + ToolColumns(const InventoryColumns& base) + : InventoryColumns(base) + , mQuality(nullptr) + , mUses(nullptr) + { + } }; /// \brief Adapter for tools with limited uses IDs (lockpick, repair, probes) - template + template class ToolRefIdAdapter : public InventoryRefIdAdapter { - ToolColumns mTools; + ToolColumns mTools; - public: - - ToolRefIdAdapter (UniversalId::Type type, const ToolColumns& columns); + public: + ToolRefIdAdapter(UniversalId::Type type, const ToolColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; - template - ToolRefIdAdapter::ToolRefIdAdapter (UniversalId::Type type, const ToolColumns& columns) - : InventoryRefIdAdapter (type, columns), mTools (columns) - {} + template + ToolRefIdAdapter::ToolRefIdAdapter(UniversalId::Type type, const ToolColumns& columns) + : InventoryRefIdAdapter(type, columns) + , mTools(columns) + { + } - template - QVariant ToolRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const + template + QVariant ToolRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); - if (column==mTools.mQuality) + if (column == mTools.mQuality) return record.get().mData.mQuality; - if (column==mTools.mUses) + if (column == mTools.mUses) return record.get().mData.mUses; - return InventoryRefIdAdapter::getData (column, data, index); + return InventoryRefIdAdapter::getData(column, data, index); } - template - void ToolRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, - int index, const QVariant& value) const + template + void ToolRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); - if (column==mTools.mQuality) + if (column == mTools.mQuality) record2.mData.mQuality = value.toFloat(); - else if (column==mTools.mUses) + else if (column == mTools.mUses) record2.mData.mUses = value.toInt(); else { - InventoryRefIdAdapter::setData (column, data, index, value); + InventoryRefIdAdapter::setData(column, data, index, value); return; } @@ -556,18 +602,18 @@ namespace CSMWorld struct ActorColumns : public NameColumns { - const RefIdColumn *mHello; - const RefIdColumn *mFlee; - const RefIdColumn *mFight; - const RefIdColumn *mAlarm; - const RefIdColumn *mInventory; - const RefIdColumn *mSpells; - const RefIdColumn *mDestinations; - const RefIdColumn *mAiPackages; - std::map mServices; - - ActorColumns (const NameColumns& base) - : NameColumns (base) + const RefIdColumn* mHello; + const RefIdColumn* mFlee; + const RefIdColumn* mFight; + const RefIdColumn* mAlarm; + const RefIdColumn* mInventory; + const RefIdColumn* mSpells; + const RefIdColumn* mDestinations; + const RefIdColumn* mAiPackages; + std::map mServices; + + ActorColumns(const NameColumns& base) + : NameColumns(base) , mHello(nullptr) , mFlee(nullptr) , mFight(nullptr) @@ -576,103 +622,99 @@ namespace CSMWorld , mSpells(nullptr) , mDestinations(nullptr) , mAiPackages(nullptr) - {} + { + } }; /// \brief Adapter for actor IDs (handles common AI functionality) - template + template class ActorRefIdAdapter : public NameRefIdAdapter { - ActorColumns mActors; - - public: + ActorColumns mActors; - ActorRefIdAdapter (UniversalId::Type type, const ActorColumns& columns); + public: + ActorRefIdAdapter(UniversalId::Type type, const ActorColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; - template - ActorRefIdAdapter::ActorRefIdAdapter (UniversalId::Type type, - const ActorColumns& columns) - : NameRefIdAdapter (type, columns), mActors (columns) - {} + template + ActorRefIdAdapter::ActorRefIdAdapter(UniversalId::Type type, const ActorColumns& columns) + : NameRefIdAdapter(type, columns) + , mActors(columns) + { + } - template - QVariant ActorRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const + template + QVariant ActorRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { - const Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + const Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); - if (column==mActors.mHello) + if (column == mActors.mHello) return record.get().mAiData.mHello; - if (column==mActors.mFlee) + if (column == mActors.mFlee) return record.get().mAiData.mFlee; - if (column==mActors.mFight) + if (column == mActors.mFight) return record.get().mAiData.mFight; - if (column==mActors.mAlarm) + if (column == mActors.mAlarm) return record.get().mAiData.mAlarm; - if (column==mActors.mInventory) + if (column == mActors.mInventory) return QVariant::fromValue(ColumnBase::TableEdit_Full); - if (column==mActors.mSpells) + if (column == mActors.mSpells) return QVariant::fromValue(ColumnBase::TableEdit_Full); - if (column==mActors.mDestinations) + if (column == mActors.mDestinations) return QVariant::fromValue(ColumnBase::TableEdit_Full); - if (column==mActors.mAiPackages) + if (column == mActors.mAiPackages) return QVariant::fromValue(ColumnBase::TableEdit_Full); - std::map::const_iterator iter = - mActors.mServices.find (column); + std::map::const_iterator iter = mActors.mServices.find(column); - if (iter!=mActors.mServices.end()) - return (record.get().mAiData.mServices & iter->second)!=0; + if (iter != mActors.mServices.end()) + return (record.get().mAiData.mServices & iter->second) != 0; - return NameRefIdAdapter::getData (column, data, index); + return NameRefIdAdapter::getData(column, data, index); } - template - void ActorRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const + template + void ActorRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - Record& record = static_cast&> ( - data.getRecord (RefIdData::LocalIndex (index, BaseRefIdAdapter::getType()))); + Record& record = static_cast&>( + data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); - if (column==mActors.mHello) + if (column == mActors.mHello) record2.mAiData.mHello = value.toInt(); - else if (column==mActors.mFlee) // Flee, Fight and Alarm ratings are probabilities. + else if (column == mActors.mFlee) // Flee, Fight and Alarm ratings are probabilities. record2.mAiData.mFlee = std::min(100, value.toInt()); - else if (column==mActors.mFight) + else if (column == mActors.mFight) record2.mAiData.mFight = std::min(100, value.toInt()); - else if (column==mActors.mAlarm) + else if (column == mActors.mAlarm) record2.mAiData.mAlarm = std::min(100, value.toInt()); else { - typename std::map::const_iterator iter = - mActors.mServices.find (column); - if (iter!=mActors.mServices.end()) + typename std::map::const_iterator iter = mActors.mServices.find(column); + if (iter != mActors.mServices.end()) { - if (value.toInt()!=0) + if (value.toInt() != 0) record2.mAiData.mServices |= iter->second; else record2.mAiData.mServices &= ~iter->second; } else { - NameRefIdAdapter::setData (column, data, index, value); + NameRefIdAdapter::setData(column, data, index, value); return; } } @@ -682,514 +724,449 @@ namespace CSMWorld class ApparatusRefIdAdapter : public InventoryRefIdAdapter { - const RefIdColumn *mType; - const RefIdColumn *mQuality; - - public: + const RefIdColumn* mType; + const RefIdColumn* mQuality; - ApparatusRefIdAdapter (const InventoryColumns& columns, const RefIdColumn *type, - const RefIdColumn *quality); + public: + ApparatusRefIdAdapter(const InventoryColumns& columns, const RefIdColumn* type, const RefIdColumn* quality); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; class ArmorRefIdAdapter : public EnchantableRefIdAdapter { - const RefIdColumn *mType; - const RefIdColumn *mHealth; - const RefIdColumn *mArmor; - const RefIdColumn *mPartRef; + const RefIdColumn* mType; + const RefIdColumn* mHealth; + const RefIdColumn* mArmor; + const RefIdColumn* mPartRef; - public: - - ArmorRefIdAdapter (const EnchantableColumns& columns, const RefIdColumn *type, - const RefIdColumn *health, const RefIdColumn *armor, const RefIdColumn *partRef); + public: + ArmorRefIdAdapter(const EnchantableColumns& columns, const RefIdColumn* type, const RefIdColumn* health, + const RefIdColumn* armor, const RefIdColumn* partRef); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; class BookRefIdAdapter : public EnchantableRefIdAdapter { - const RefIdColumn *mBookType; - const RefIdColumn *mSkill; - const RefIdColumn *mText; + const RefIdColumn* mBookType; + const RefIdColumn* mSkill; + const RefIdColumn* mText; - public: - - BookRefIdAdapter (const EnchantableColumns& columns, const RefIdColumn *bookType, - const RefIdColumn *skill, const RefIdColumn *text); + public: + BookRefIdAdapter(const EnchantableColumns& columns, const RefIdColumn* bookType, const RefIdColumn* skill, + const RefIdColumn* text); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; class ClothingRefIdAdapter : public EnchantableRefIdAdapter { - const RefIdColumn *mType; - const RefIdColumn *mPartRef; + const RefIdColumn* mType; + const RefIdColumn* mPartRef; - public: - - ClothingRefIdAdapter (const EnchantableColumns& columns, - const RefIdColumn *type, const RefIdColumn *partRef); + public: + ClothingRefIdAdapter(const EnchantableColumns& columns, const RefIdColumn* type, const RefIdColumn* partRef); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; class ContainerRefIdAdapter : public NameRefIdAdapter { - const RefIdColumn *mWeight; - const RefIdColumn *mOrganic; - const RefIdColumn *mRespawn; - const RefIdColumn *mContent; - - public: + const RefIdColumn* mWeight; + const RefIdColumn* mOrganic; + const RefIdColumn* mRespawn; + const RefIdColumn* mContent; - ContainerRefIdAdapter (const NameColumns& columns, const RefIdColumn *weight, - const RefIdColumn *organic, const RefIdColumn *respawn, const RefIdColumn *content); + public: + ContainerRefIdAdapter(const NameColumns& columns, const RefIdColumn* weight, const RefIdColumn* organic, + const RefIdColumn* respawn, const RefIdColumn* content); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; struct CreatureColumns : public ActorColumns { - std::map mFlags; - const RefIdColumn *mType; - const RefIdColumn *mScale; - const RefIdColumn *mOriginal; - const RefIdColumn *mAttributes; - const RefIdColumn *mAttacks; - const RefIdColumn *mMisc; - const RefIdColumn *mBloodType; + std::map mFlags; + const RefIdColumn* mType; + const RefIdColumn* mScale; + const RefIdColumn* mOriginal; + const RefIdColumn* mAttributes; + const RefIdColumn* mAttacks; + const RefIdColumn* mMisc; + const RefIdColumn* mBloodType; - CreatureColumns (const ActorColumns& actorColumns); + CreatureColumns(const ActorColumns& actorColumns); }; class CreatureRefIdAdapter : public ActorRefIdAdapter { - CreatureColumns mColumns; - - public: + CreatureColumns mColumns; - CreatureRefIdAdapter (const CreatureColumns& columns); + public: + CreatureRefIdAdapter(const CreatureColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; class DoorRefIdAdapter : public NameRefIdAdapter { - const RefIdColumn *mOpenSound; - const RefIdColumn *mCloseSound; + const RefIdColumn* mOpenSound; + const RefIdColumn* mCloseSound; - public: - - DoorRefIdAdapter (const NameColumns& columns, const RefIdColumn *openSound, - const RefIdColumn *closeSound); + public: + DoorRefIdAdapter(const NameColumns& columns, const RefIdColumn* openSound, const RefIdColumn* closeSound); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; struct LightColumns : public InventoryColumns { - const RefIdColumn *mTime; - const RefIdColumn *mRadius; - const RefIdColumn *mColor; - const RefIdColumn *mSound; - const RefIdColumn *mEmitterType; - std::map mFlags; + const RefIdColumn* mTime; + const RefIdColumn* mRadius; + const RefIdColumn* mColor; + const RefIdColumn* mSound; + const RefIdColumn* mEmitterType; + std::map mFlags; - LightColumns (const InventoryColumns& columns); + LightColumns(const InventoryColumns& columns); }; class LightRefIdAdapter : public InventoryRefIdAdapter { - LightColumns mColumns; - - public: + LightColumns mColumns; - LightRefIdAdapter (const LightColumns& columns); + public: + LightRefIdAdapter(const LightColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; class MiscRefIdAdapter : public InventoryRefIdAdapter { - const RefIdColumn *mKey; - - public: + const RefIdColumn* mKey; - MiscRefIdAdapter (const InventoryColumns& columns, const RefIdColumn *key); + public: + MiscRefIdAdapter(const InventoryColumns& columns, const RefIdColumn* key); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; struct NpcColumns : public ActorColumns { - std::map mFlags; - const RefIdColumn *mRace; - const RefIdColumn *mClass; - const RefIdColumn *mFaction; - const RefIdColumn *mHair; - const RefIdColumn *mHead; - const RefIdColumn *mAttributes; // depends on npc type - const RefIdColumn *mSkills; // depends on npc type - const RefIdColumn *mMisc; // may depend on npc type, e.g. FactionID - const RefIdColumn *mBloodType; - const RefIdColumn *mGender; - - NpcColumns (const ActorColumns& actorColumns); + std::map mFlags; + const RefIdColumn* mRace; + const RefIdColumn* mClass; + const RefIdColumn* mFaction; + const RefIdColumn* mHair; + const RefIdColumn* mHead; + const RefIdColumn* mAttributes; // depends on npc type + const RefIdColumn* mSkills; // depends on npc type + const RefIdColumn* mMisc; // may depend on npc type, e.g. FactionID + const RefIdColumn* mBloodType; + const RefIdColumn* mGender; + + NpcColumns(const ActorColumns& actorColumns); }; class NpcRefIdAdapter : public ActorRefIdAdapter { - NpcColumns mColumns; - - public: + NpcColumns mColumns; - NpcRefIdAdapter (const NpcColumns& columns); + public: + NpcRefIdAdapter(const NpcColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; struct WeaponColumns : public EnchantableColumns { - const RefIdColumn *mType; - const RefIdColumn *mHealth; - const RefIdColumn *mSpeed; - const RefIdColumn *mReach; - const RefIdColumn *mChop[2]; - const RefIdColumn *mSlash[2]; - const RefIdColumn *mThrust[2]; - std::map mFlags; + const RefIdColumn* mType; + const RefIdColumn* mHealth; + const RefIdColumn* mSpeed; + const RefIdColumn* mReach; + const RefIdColumn* mChop[2]; + const RefIdColumn* mSlash[2]; + const RefIdColumn* mThrust[2]; + std::map mFlags; - WeaponColumns (const EnchantableColumns& columns); + WeaponColumns(const EnchantableColumns& columns); }; class WeaponRefIdAdapter : public EnchantableRefIdAdapter { - WeaponColumns mColumns; - - public: + WeaponColumns mColumns; - WeaponRefIdAdapter (const WeaponColumns& columns); + public: + WeaponRefIdAdapter(const WeaponColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; - - class NestedRefIdAdapterBase; - class NpcAttributesRefIdAdapter : public NestedRefIdAdapterBase { public: + NpcAttributesRefIdAdapter() = default; - NpcAttributesRefIdAdapter (); - - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override; + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override; - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override; + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override; - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override; - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override; + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override; - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override; - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override; - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; class NpcSkillsRefIdAdapter : public NestedRefIdAdapterBase { public: + NpcSkillsRefIdAdapter() = default; - NpcSkillsRefIdAdapter (); + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override; - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override; + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override; - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override; + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override; - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override; - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override; + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override; - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override; - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; - - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; class NpcMiscRefIdAdapter : public NestedRefIdAdapterBase { - NpcMiscRefIdAdapter (const NpcMiscRefIdAdapter&); - NpcMiscRefIdAdapter& operator= (const NpcMiscRefIdAdapter&); - public: + NpcMiscRefIdAdapter() = default; + NpcMiscRefIdAdapter(const NpcMiscRefIdAdapter&) = delete; + NpcMiscRefIdAdapter& operator=(const NpcMiscRefIdAdapter&) = delete; + ~NpcMiscRefIdAdapter() override = default; - NpcMiscRefIdAdapter (); - virtual ~NpcMiscRefIdAdapter(); - - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override; + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override; - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override; + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override; - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override; - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override; + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override; - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override; - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override; - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; class CreatureAttributesRefIdAdapter : public NestedRefIdAdapterBase { public: + CreatureAttributesRefIdAdapter() = default; - CreatureAttributesRefIdAdapter (); + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override; - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override; + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override; - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override; + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override; - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override; - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override; + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override; - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override; - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; - - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; class CreatureAttackRefIdAdapter : public NestedRefIdAdapterBase { public: + CreatureAttackRefIdAdapter() = default; - CreatureAttackRefIdAdapter (); - - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override; + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override; - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override; + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override; - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override; - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override; + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override; - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override; - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override; - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; class CreatureMiscRefIdAdapter : public NestedRefIdAdapterBase { - CreatureMiscRefIdAdapter (const CreatureMiscRefIdAdapter&); - CreatureMiscRefIdAdapter& operator= (const CreatureMiscRefIdAdapter&); - public: + CreatureMiscRefIdAdapter() = default; + CreatureMiscRefIdAdapter(const CreatureMiscRefIdAdapter&) = delete; + CreatureMiscRefIdAdapter& operator=(const CreatureMiscRefIdAdapter&) = delete; + ~CreatureMiscRefIdAdapter() override = default; - CreatureMiscRefIdAdapter (); - virtual ~CreatureMiscRefIdAdapter(); - - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override; + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override; - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override; + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override; - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override; + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override; - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override; + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override; - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override; + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override; - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override; + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override; - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override; + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override; + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; - template + template class EffectsListAdapter; - template + template class EffectsRefIdAdapter : public EffectsListAdapter, public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented - EffectsRefIdAdapter (const EffectsRefIdAdapter&); - EffectsRefIdAdapter& operator= (const EffectsRefIdAdapter&); + EffectsRefIdAdapter(const EffectsRefIdAdapter&); + EffectsRefIdAdapter& operator=(const EffectsRefIdAdapter&); public: + EffectsRefIdAdapter(UniversalId::Type type) + : mType(type) + { + } - EffectsRefIdAdapter(UniversalId::Type type) :mType(type) {} - - virtual ~EffectsRefIdAdapter() {} + virtual ~EffectsRefIdAdapter() = default; - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); EffectsListAdapter::addRow(record, position); } - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); EffectsListAdapter::removeRow(record, rowToRemove); } - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); EffectsListAdapter::setTable(record, nestedTable); } - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return EffectsListAdapter::table(record); } - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return EffectsListAdapter::getData(record, subRowIndex, subColIndex); } - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, mType))); EffectsListAdapter::setData(record, value, subRowIndex, subColIndex); } - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { const Record record; // not used, just a dummy return EffectsListAdapter::getColumnsCount(record); } - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return EffectsListAdapter::getRowsCount(record); } }; @@ -1200,20 +1177,21 @@ namespace CSMWorld UniversalId::Type mType; // not implemented - NestedInventoryRefIdAdapter (const NestedInventoryRefIdAdapter&); - NestedInventoryRefIdAdapter& operator= (const NestedInventoryRefIdAdapter&); + NestedInventoryRefIdAdapter(const NestedInventoryRefIdAdapter&); + NestedInventoryRefIdAdapter& operator=(const NestedInventoryRefIdAdapter&); public: + NestedInventoryRefIdAdapter(UniversalId::Type type) + : mType(type) + { + } - NestedInventoryRefIdAdapter(UniversalId::Type type) :mType(type) {} - - virtual ~NestedInventoryRefIdAdapter() {} + virtual ~NestedInventoryRefIdAdapter() = default; - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT container = record.get(); std::vector& list = container.mInventory.mList; @@ -1223,88 +1201,88 @@ namespace CSMWorld if (position >= (int)list.size()) list.push_back(newRow); else - list.insert(list.begin()+position, newRow); + list.insert(list.begin() + position, newRow); - record.setModified (container); + record.setModified(container); } - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT container = record.get(); std::vector& list = container.mInventory.mList; - if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(list.size())) + throw std::runtime_error("index out of range"); - list.erase (list.begin () + rowToRemove); + list.erase(list.begin() + rowToRemove); - record.setModified (container); + record.setModified(container); } - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT container = record.get(); - container.mInventory.mList = - static_cast >&>(nestedTable).mNestedTable; + container.mInventory.mList + = static_cast>&>(nestedTable).mNestedTable; - record.setModified (container); + record.setModified(container); } - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(record.get().mInventory.mList); + return new NestedTableWrapper>(record.get().mInventory.mList); } - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); const std::vector& list = record.get().mInventory.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); const ESM::ContItem& content = list.at(subRowIndex); switch (subColIndex) { - case 0: return QString::fromUtf8(content.mItem.c_str()); - case 1: return content.mCount; + case 0: + return QString::fromUtf8(content.mItem.getRefIdString().c_str()); + case 1: + return content.mCount; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } } - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, mType))); ESXRecordT container = record.get(); std::vector& list = container.mInventory.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); - switch(subColIndex) + switch (subColIndex) { case 0: - list.at(subRowIndex).mItem.assign(std::string(value.toString().toUtf8().constData())); + list.at(subRowIndex).mItem = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); break; case 1: @@ -1315,18 +1293,15 @@ namespace CSMWorld throw std::runtime_error("Trying to access non-existing column in the nested table!"); } - record.setModified (container); + record.setModified(container); } - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override - { - return 2; - } + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 2; } - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return static_cast(record.get().mInventory.mList.size()); } @@ -1338,121 +1313,117 @@ namespace CSMWorld UniversalId::Type mType; // not implemented - NestedSpellRefIdAdapter (const NestedSpellRefIdAdapter&); - NestedSpellRefIdAdapter& operator= (const NestedSpellRefIdAdapter&); + NestedSpellRefIdAdapter(const NestedSpellRefIdAdapter&); + NestedSpellRefIdAdapter& operator=(const NestedSpellRefIdAdapter&); public: + NestedSpellRefIdAdapter(UniversalId::Type type) + : mType(type) + { + } - NestedSpellRefIdAdapter(UniversalId::Type type) :mType(type) {} - - virtual ~NestedSpellRefIdAdapter() {} + virtual ~NestedSpellRefIdAdapter() = default; - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT caster = record.get(); - std::vector& list = caster.mSpells.mList; + std::vector& list = caster.mSpells.mList; - std::string newString; + ESM::RefId newString; if (position >= (int)list.size()) list.push_back(newString); else - list.insert(list.begin()+position, newString); + list.insert(list.begin() + position, newString); - record.setModified (caster); + record.setModified(caster); } - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT caster = record.get(); - std::vector& list = caster.mSpells.mList; + std::vector& list = caster.mSpells.mList; - if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(list.size())) + throw std::runtime_error("index out of range"); - list.erase (list.begin () + rowToRemove); + list.erase(list.begin() + rowToRemove); - record.setModified (caster); + record.setModified(caster); } - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT caster = record.get(); - caster.mSpells.mList = - static_cast >&>(nestedTable).mNestedTable; + caster.mSpells.mList + = static_cast>&>(nestedTable).mNestedTable; - record.setModified (caster); + record.setModified(caster); } - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(record.get().mSpells.mList); + return new NestedTableWrapper>(record.get().mSpells.mList); } - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); - const std::vector& list = record.get().mSpells.mList; + const std::vector& list = record.get().mSpells.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); - const std::string& content = list.at(subRowIndex); + const ESM::RefId& content = list.at(subRowIndex); if (subColIndex == 0) - return QString::fromUtf8(content.c_str()); + return QString::fromUtf8(content.getRefIdString().c_str()); else throw std::runtime_error("Trying to access non-existing column in the nested table!"); } - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, mType))); ESXRecordT caster = record.get(); - std::vector& list = caster.mSpells.mList; + std::vector& list = caster.mSpells.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); if (subColIndex == 0) - list.at(subRowIndex) = std::string(value.toString().toUtf8()); + list.at(subRowIndex) = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); else throw std::runtime_error("Trying to access non-existing column in the nested table!"); - record.setModified (caster); + record.setModified(caster); } - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override - { - return 1; - } + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 1; } - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return static_cast(record.get().mSpells.mList.size()); } @@ -1464,20 +1435,21 @@ namespace CSMWorld UniversalId::Type mType; // not implemented - NestedTravelRefIdAdapter (const NestedTravelRefIdAdapter&); - NestedTravelRefIdAdapter& operator= (const NestedTravelRefIdAdapter&); + NestedTravelRefIdAdapter(const NestedTravelRefIdAdapter&); + NestedTravelRefIdAdapter& operator=(const NestedTravelRefIdAdapter&); public: + NestedTravelRefIdAdapter(UniversalId::Type type) + : mType(type) + { + } - NestedTravelRefIdAdapter(UniversalId::Type type) :mType(type) {} - - virtual ~NestedTravelRefIdAdapter() {} + virtual ~NestedTravelRefIdAdapter() = default; - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT traveller = record.get(); std::vector& list = traveller.mTransport.mList; @@ -1491,119 +1463,136 @@ namespace CSMWorld ESM::Transport::Dest newRow; newRow.mPos = newPos; - newRow.mCellName = ""; + newRow.mCellName.clear(); if (position >= (int)list.size()) list.push_back(newRow); else - list.insert(list.begin()+position, newRow); + list.insert(list.begin() + position, newRow); - record.setModified (traveller); + record.setModified(traveller); } - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT traveller = record.get(); std::vector& list = traveller.mTransport.mList; - if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(list.size())) + throw std::runtime_error("index out of range"); - list.erase (list.begin () + rowToRemove); + list.erase(list.begin() + rowToRemove); - record.setModified (traveller); + record.setModified(traveller); } - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT traveller = record.get(); - traveller.mTransport.mList = - static_cast >&>(nestedTable).mNestedTable; + traveller.mTransport.mList + = static_cast>&>(nestedTable) + .mNestedTable; - record.setModified (traveller); + record.setModified(traveller); } - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(record.get().mTransport.mList); + return new NestedTableWrapper>(record.get().mTransport.mList); } - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); const std::vector& list = record.get().mTransport.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); const ESM::Transport::Dest& content = list.at(subRowIndex); switch (subColIndex) { - case 0: return QString::fromUtf8(content.mCellName.c_str()); - case 1: return content.mPos.pos[0]; - case 2: return content.mPos.pos[1]; - case 3: return content.mPos.pos[2]; - case 4: return content.mPos.rot[0]; - case 5: return content.mPos.rot[1]; - case 6: return content.mPos.rot[2]; + case 0: + return QString::fromUtf8(content.mCellName.c_str()); + case 1: + return content.mPos.pos[0]; + case 2: + return content.mPos.pos[1]; + case 3: + return content.mPos.pos[2]; + case 4: + return content.mPos.rot[0]; + case 5: + return content.mPos.rot[1]; + case 6: + return content.mPos.rot[2]; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } } - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, mType))); ESXRecordT traveller = record.get(); std::vector& list = traveller.mTransport.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); - switch(subColIndex) + switch (subColIndex) { - case 0: list.at(subRowIndex).mCellName = std::string(value.toString().toUtf8().constData()); break; - case 1: list.at(subRowIndex).mPos.pos[0] = value.toFloat(); break; - case 2: list.at(subRowIndex).mPos.pos[1] = value.toFloat(); break; - case 3: list.at(subRowIndex).mPos.pos[2] = value.toFloat(); break; - case 4: list.at(subRowIndex).mPos.rot[0] = value.toFloat(); break; - case 5: list.at(subRowIndex).mPos.rot[1] = value.toFloat(); break; - case 6: list.at(subRowIndex).mPos.rot[2] = value.toFloat(); break; + case 0: + list.at(subRowIndex).mCellName = value.toString().toUtf8().constData(); + break; + case 1: + list.at(subRowIndex).mPos.pos[0] = value.toFloat(); + break; + case 2: + list.at(subRowIndex).mPos.pos[1] = value.toFloat(); + break; + case 3: + list.at(subRowIndex).mPos.pos[2] = value.toFloat(); + break; + case 4: + list.at(subRowIndex).mPos.rot[0] = value.toFloat(); + break; + case 5: + list.at(subRowIndex).mPos.rot[1] = value.toFloat(); + break; + case 6: + list.at(subRowIndex).mPos.rot[2] = value.toFloat(); + break; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } - record.setModified (traveller); + record.setModified(traveller); } - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override - { - return 7; - } + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 7; } - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return static_cast(record.get().mTransport.mList.size()); } @@ -1615,22 +1604,23 @@ namespace CSMWorld UniversalId::Type mType; // not implemented - ActorAiRefIdAdapter (const ActorAiRefIdAdapter&); - ActorAiRefIdAdapter& operator= (const ActorAiRefIdAdapter&); + ActorAiRefIdAdapter(const ActorAiRefIdAdapter&); + ActorAiRefIdAdapter& operator=(const ActorAiRefIdAdapter&); public: + ActorAiRefIdAdapter(UniversalId::Type type) + : mType(type) + { + } - ActorAiRefIdAdapter(UniversalId::Type type) :mType(type) {} - - virtual ~ActorAiRefIdAdapter() {} + virtual ~ActorAiRefIdAdapter() = default; // FIXME: should check if the AI package type is already in the list and use a default // that wasn't used already (in extreme case do not add anything at all? - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT actor = record.get(); std::vector& list = actor.mAiPackage.mList; @@ -1642,67 +1632,66 @@ namespace CSMWorld newRow.mWander.mTimeOfDay = 0; for (int i = 0; i < 8; ++i) newRow.mWander.mIdle[i] = 0; - newRow.mWander.mShouldRepeat = 0; - newRow.mCellName = ""; + newRow.mWander.mShouldRepeat = 1; + newRow.mCellName.clear(); if (position >= (int)list.size()) list.push_back(newRow); else - list.insert(list.begin()+position, newRow); + list.insert(list.begin() + position, newRow); - record.setModified (actor); + record.setModified(actor); } - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT actor = record.get(); std::vector& list = actor.mAiPackage.mList; - if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(list.size())) + throw std::runtime_error("index out of range"); - list.erase (list.begin () + rowToRemove); + list.erase(list.begin() + rowToRemove); - record.setModified (actor); + record.setModified(actor); } - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT actor = record.get(); - actor.mAiPackage.mList = - static_cast >&>(nestedTable).mNestedTable; + actor.mAiPackage.mList + = static_cast>&>(nestedTable) + .mNestedTable; - record.setModified (actor); + record.setModified(actor); } - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(record.get().mAiPackage.mList); + return new NestedTableWrapper>(record.get().mAiPackage.mList); } - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); const std::vector& list = record.get().mAiPackage.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); const ESM::AIPackage& content = list.at(subRowIndex); @@ -1712,23 +1701,29 @@ namespace CSMWorld // FIXME: should more than one AI package type be allowed? Check vanilla switch (content.mType) { - case ESM::AI_Wander: return 0; - case ESM::AI_Travel: return 1; - case ESM::AI_Follow: return 2; - case ESM::AI_Escort: return 3; - case ESM::AI_Activate: return 4; - case ESM::AI_CNDT: - default: return QVariant(); + case ESM::AI_Wander: + return 0; + case ESM::AI_Travel: + return 1; + case ESM::AI_Follow: + return 2; + case ESM::AI_Escort: + return 3; + case ESM::AI_Activate: + return 4; + default: + return QVariant(); } case 1: // wander dist if (content.mType == ESM::AI_Wander) return content.mWander.mDistance; else return QVariant(); - case 2: // wander dur - if (content.mType == ESM::AI_Wander || - content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) + case 2: // wander/follow dur + if (content.mType == ESM::AI_Wander) return content.mWander.mDuration; + else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) + return content.mTarget.mDuration; else return QVariant(); case 3: // wander ToD @@ -1745,12 +1740,18 @@ namespace CSMWorld case 10: case 11: if (content.mType == ESM::AI_Wander) - return static_cast(content.mWander.mIdle[subColIndex-4]); + return static_cast(content.mWander.mIdle[subColIndex - 4]); else return QVariant(); - case 12: // wander repeat + case 12: // repeat if (content.mType == ESM::AI_Wander) return content.mWander.mShouldRepeat != 0; + else if (content.mType == ESM::AI_Travel) + return content.mTravel.mShouldRepeat != 0; + else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) + return content.mTarget.mShouldRepeat != 0; + else if (content.mType == ESM::AI_Activate) + return content.mActivate.mShouldRepeat != 0; else return QVariant(); case 13: // activate name @@ -1794,30 +1795,41 @@ namespace CSMWorld } } - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, mType))); ESXRecordT actor = record.get(); std::vector& list = actor.mAiPackage.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); ESM::AIPackage& content = list.at(subRowIndex); - switch(subColIndex) + switch (subColIndex) { case 0: // ai package type switch (value.toInt()) { - case 0: content.mType = ESM::AI_Wander; break; - case 1: content.mType = ESM::AI_Travel; break; - case 2: content.mType = ESM::AI_Follow; break; - case 3: content.mType = ESM::AI_Escort; break; - case 4: content.mType = ESM::AI_Activate; break; - default: return; // return without saving + case 0: + content.mType = ESM::AI_Wander; + break; + case 1: + content.mType = ESM::AI_Travel; + break; + case 2: + content.mType = ESM::AI_Follow; + break; + case 3: + content.mType = ESM::AI_Escort; + break; + case 4: + content.mType = ESM::AI_Activate; + break; + default: + return; // return without saving } break; // always save @@ -1829,11 +1841,13 @@ namespace CSMWorld break; // always save case 2: - if (content.mType == ESM::AI_Wander || - content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) + if (content.mType == ESM::AI_Wander) content.mWander.mDuration = static_cast(value.toInt()); + else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) + content.mTarget.mDuration = static_cast(value.toInt()); else return; // return without saving + break; case 3: if (content.mType == ESM::AI_Wander) content.mWander.mTimeOfDay = static_cast(value.toInt()); @@ -1850,7 +1864,7 @@ namespace CSMWorld case 10: case 11: if (content.mType == ESM::AI_Wander) - content.mWander.mIdle[subColIndex-4] = static_cast(value.toInt()); + content.mWander.mIdle[subColIndex - 4] = static_cast(value.toInt()); else return; // return without saving @@ -1858,20 +1872,32 @@ namespace CSMWorld case 12: if (content.mType == ESM::AI_Wander) content.mWander.mShouldRepeat = static_cast(value.toInt()); + else if (content.mType == ESM::AI_Travel) + content.mTravel.mShouldRepeat = static_cast(value.toInt()); + else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) + content.mTarget.mShouldRepeat = static_cast(value.toInt()); + else if (content.mType == ESM::AI_Activate) + content.mActivate.mShouldRepeat = static_cast(value.toInt()); else return; // return without saving break; // always save case 13: // NAME32 if (content.mType == ESM::AI_Activate) - content.mActivate.mName.assign(value.toString().toUtf8().constData()); + { + const QByteArray name = value.toString().toUtf8(); + content.mActivate.mName.assign(std::string_view(name.constData(), name.size())); + } else return; // return without saving break; // always save case 14: // NAME32 if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) - content.mTarget.mId.assign(value.toString().toUtf8().constData()); + { + const QByteArray id = value.toString().toUtf8(); + content.mTarget.mId.assign(std::string_view(id.constData(), id.size())); + } else return; // return without saving @@ -1914,111 +1940,107 @@ namespace CSMWorld throw std::runtime_error("Trying to access non-existing column in the nested table!"); } - record.setModified (actor); + record.setModified(actor); } - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override - { - return 19; - } + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 19; } - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return static_cast(record.get().mAiPackage.mList.size()); } }; - template class BodyPartRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented - BodyPartRefIdAdapter (const BodyPartRefIdAdapter&); - BodyPartRefIdAdapter& operator= (const BodyPartRefIdAdapter&); + BodyPartRefIdAdapter(const BodyPartRefIdAdapter&); + BodyPartRefIdAdapter& operator=(const BodyPartRefIdAdapter&); public: + BodyPartRefIdAdapter(UniversalId::Type type) + : mType(type) + { + } - BodyPartRefIdAdapter(UniversalId::Type type) :mType(type) {} - - virtual ~BodyPartRefIdAdapter() {} + virtual ~BodyPartRefIdAdapter() = default; - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT apparel = record.get(); std::vector& list = apparel.mParts.mParts; ESM::PartReference newPart; newPart.mPart = 0; // 0 == head - newPart.mMale = ""; - newPart.mFemale = ""; + newPart.mMale = ESM::RefId(); + newPart.mFemale = ESM::RefId(); if (position >= (int)list.size()) list.push_back(newPart); else - list.insert(list.begin()+position, newPart); + list.insert(list.begin() + position, newPart); - record.setModified (apparel); + record.setModified(apparel); } - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT apparel = record.get(); std::vector& list = apparel.mParts.mParts; - if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(list.size())) + throw std::runtime_error("index out of range"); - list.erase (list.begin () + rowToRemove); + list.erase(list.begin() + rowToRemove); - record.setModified (apparel); + record.setModified(apparel); } - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT apparel = record.get(); - apparel.mParts.mParts = - static_cast >&>(nestedTable).mNestedTable; + apparel.mParts.mParts + = static_cast>&>(nestedTable) + .mNestedTable; - record.setModified (apparel); + record.setModified(apparel); } - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(record.get().mParts.mParts); + return new NestedTableWrapper>(record.get().mParts.mParts); } - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); const std::vector& list = record.get().mParts.mParts; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); const ESM::PartReference& content = list.at(subRowIndex); @@ -2031,105 +2053,107 @@ namespace CSMWorld else throw std::runtime_error("Part Reference Type unexpected value"); } - case 1: return QString(content.mMale.c_str()); - case 2: return QString(content.mFemale.c_str()); + case 1: + return QString(content.mMale.getRefIdString().c_str()); + case 2: + return QString(content.mFemale.getRefIdString().c_str()); default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } } - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, mType))); ESXRecordT apparel = record.get(); std::vector& list = apparel.mParts.mParts; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); - switch(subColIndex) + switch (subColIndex) { - case 0: list.at(subRowIndex).mPart = static_cast(value.toInt()); break; - case 1: list.at(subRowIndex).mMale = value.toString().toStdString(); break; - case 2: list.at(subRowIndex).mFemale = value.toString().toStdString(); break; + case 0: + list.at(subRowIndex).mPart = static_cast(value.toInt()); + break; + case 1: + list.at(subRowIndex).mMale = ESM::RefId::stringRefId(value.toString().toStdString()); + break; + case 2: + list.at(subRowIndex).mFemale = ESM::RefId::stringRefId(value.toString().toStdString()); + break; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } - record.setModified (apparel); + record.setModified(apparel); } - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override - { - return 3; - } + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 3; } - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return static_cast(record.get().mParts.mParts.size()); } }; - struct LevListColumns : public BaseColumns { - const RefIdColumn *mLevList; - const RefIdColumn *mNestedListLevList; + const RefIdColumn* mLevList; + const RefIdColumn* mNestedListLevList; - LevListColumns (const BaseColumns& base) - : BaseColumns (base) - , mLevList(nullptr) - , mNestedListLevList(nullptr) - {} + LevListColumns(const BaseColumns& base) + : BaseColumns(base) + , mLevList(nullptr) + , mNestedListLevList(nullptr) + { + } }; - template + template class LevelledListRefIdAdapter : public BaseRefIdAdapter { - LevListColumns mLevList; - - public: + LevListColumns mLevList; - LevelledListRefIdAdapter (UniversalId::Type type, const LevListColumns &columns); + public: + LevelledListRefIdAdapter(UniversalId::Type type, const LevListColumns& columns); - QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) - const override; + QVariant getData(const RefIdColumn* column, const RefIdData& data, int index) const override; - void setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const override; - ///< If the data type does not match an exception is thrown. + void setData(const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const override; + ///< If the data type does not match an exception is thrown. }; - template - LevelledListRefIdAdapter::LevelledListRefIdAdapter (UniversalId::Type type, - const LevListColumns &columns) - : BaseRefIdAdapter (type, columns), mLevList (columns) - {} + template + LevelledListRefIdAdapter::LevelledListRefIdAdapter(UniversalId::Type type, const LevListColumns& columns) + : BaseRefIdAdapter(type, columns) + , mLevList(columns) + { + } - template - QVariant LevelledListRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, - int index) const + template + QVariant LevelledListRefIdAdapter::getData( + const RefIdColumn* column, const RefIdData& data, int index) const { - if (column==mLevList.mLevList || column == mLevList.mNestedListLevList) + if (column == mLevList.mLevList || column == mLevList.mNestedListLevList) return QVariant::fromValue(ColumnBase::TableEdit_Full); - return BaseRefIdAdapter::getData (column, data, index); + return BaseRefIdAdapter::getData(column, data, index); } - template - void LevelledListRefIdAdapter::setData (const RefIdColumn *column, RefIdData& data, int index, - const QVariant& value) const + template + void LevelledListRefIdAdapter::setData( + const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { - BaseRefIdAdapter::setData (column, data, index, value); + BaseRefIdAdapter::setData(column, data, index, value); return; } - // for non-tables template class NestedListLevListRefIdAdapter : public NestedRefIdAdapterBase @@ -2137,53 +2161,54 @@ namespace CSMWorld UniversalId::Type mType; // not implemented - NestedListLevListRefIdAdapter (const NestedListLevListRefIdAdapter&); - NestedListLevListRefIdAdapter& operator= (const NestedListLevListRefIdAdapter&); + NestedListLevListRefIdAdapter(const NestedListLevListRefIdAdapter&); + NestedListLevListRefIdAdapter& operator=(const NestedListLevListRefIdAdapter&); public: - NestedListLevListRefIdAdapter(UniversalId::Type type) - :mType(type) {} + : mType(type) + { + } - virtual ~NestedListLevListRefIdAdapter() {} + virtual ~NestedListLevListRefIdAdapter() = default; - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { - throw std::logic_error ("cannot add a row to a fixed table"); + throw std::logic_error("cannot add a row to a fixed table"); } - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { - throw std::logic_error ("cannot remove a row to a fixed table"); + throw std::logic_error("cannot remove a row to a fixed table"); } - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { - throw std::logic_error ("table operation not supported"); + throw std::logic_error("table operation not supported"); } - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); if (mType == UniversalId::Type_CreatureLevelledList) { switch (subColIndex) { - case 0: return QVariant(); // disable the checkbox editor - case 1: return record.get().mFlags & ESM::CreatureLevList::AllLevels; - case 2: return static_cast (record.get().mChanceNone); + case 0: + return QVariant(); // disable the checkbox editor + case 1: + return record.get().mFlags & ESM::CreatureLevList::AllLevels; + case 2: + return static_cast(record.get().mChanceNone); default: throw std::runtime_error("Trying to access non-existing column in levelled creatues!"); } @@ -2192,30 +2217,34 @@ namespace CSMWorld { switch (subColIndex) { - case 0: return record.get().mFlags & ESM::ItemLevList::Each; - case 1: return record.get().mFlags & ESM::ItemLevList::AllLevels; - case 2: return static_cast (record.get().mChanceNone); + case 0: + return record.get().mFlags & ESM::ItemLevList::Each; + case 1: + return record.get().mFlags & ESM::ItemLevList::AllLevels; + case 2: + return static_cast(record.get().mChanceNone); default: throw std::runtime_error("Trying to access non-existing column in levelled items!"); } } } - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, mType))); ESXRecordT leveled = record.get(); if (mType == UniversalId::Type_CreatureLevelledList) { - switch(subColIndex) + switch (subColIndex) { - case 0: return; // return without saving + case 0: + return; // return without saving case 1: { - if(value.toBool()) + if (value.toBool()) { leveled.mFlags |= ESM::CreatureLevList::AllLevels; break; @@ -2226,18 +2255,20 @@ namespace CSMWorld break; } } - case 2: leveled.mChanceNone = static_cast(value.toInt()); break; + case 2: + leveled.mChanceNone = static_cast(value.toInt()); + break; default: throw std::runtime_error("Trying to set non-existing column in levelled creatures!"); } } else { - switch(subColIndex) + switch (subColIndex) { case 0: { - if(value.toBool()) + if (value.toBool()) { leveled.mFlags |= ESM::ItemLevList::Each; break; @@ -2250,7 +2281,7 @@ namespace CSMWorld } case 1: { - if(value.toBool()) + if (value.toBool()) { leveled.mFlags |= ESM::ItemLevList::AllLevels; break; @@ -2261,21 +2292,20 @@ namespace CSMWorld break; } } - case 2: leveled.mChanceNone = static_cast(value.toInt()); break; + case 2: + leveled.mChanceNone = static_cast(value.toInt()); + break; default: throw std::runtime_error("Trying to set non-existing column in levelled items!"); } } - record.setModified (leveled); + record.setModified(leveled); } - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override - { - return 3; - } + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 3; } - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { return 1; // fixed at size 1 } @@ -2288,129 +2318,133 @@ namespace CSMWorld UniversalId::Type mType; // not implemented - NestedLevListRefIdAdapter (const NestedLevListRefIdAdapter&); - NestedLevListRefIdAdapter& operator= (const NestedLevListRefIdAdapter&); + NestedLevListRefIdAdapter(const NestedLevListRefIdAdapter&); + NestedLevListRefIdAdapter& operator=(const NestedLevListRefIdAdapter&); public: + NestedLevListRefIdAdapter(UniversalId::Type type) + : mType(type) + { + } - NestedLevListRefIdAdapter(UniversalId::Type type) :mType(type) {} - - virtual ~NestedLevListRefIdAdapter() {} + virtual ~NestedLevListRefIdAdapter() = default; - void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const override + void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT leveled = record.get(); std::vector& list = leveled.mList; ESM::LevelledListBase::LevelItem newItem; - newItem.mId = ""; + newItem.mId = ESM::RefId(); newItem.mLevel = 0; if (position >= (int)list.size()) list.push_back(newItem); else - list.insert(list.begin()+position, newItem); + list.insert(list.begin() + position, newItem); - record.setModified (leveled); + record.setModified(leveled); } - void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const override + void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT leveled = record.get(); std::vector& list = leveled.mList; - if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (rowToRemove < 0 || rowToRemove >= static_cast(list.size())) + throw std::runtime_error("index out of range"); - list.erase (list.begin () + rowToRemove); + list.erase(list.begin() + rowToRemove); - record.setModified (leveled); + record.setModified(leveled); } - void setNestedTable (const RefIdColumn* column, - RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override + void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, + const NestedTableWrapperBase& nestedTable) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT leveled = record.get(); - leveled.mList = - static_cast >&>(nestedTable).mNestedTable; + leveled.mList + = static_cast>&>( + nestedTable) + .mNestedTable; - record.setModified (leveled); + record.setModified(leveled); } - NestedTableWrapperBase* nestedTable (const RefIdColumn* column, - const RefIdData& data, int index) const override + NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring - return new NestedTableWrapper >(record.get().mList); + return new NestedTableWrapper>(record.get().mList); } - QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const override + QVariant getNestedData(const RefIdColumn* column, const RefIdData& data, int index, int subRowIndex, + int subColIndex) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); const std::vector& list = record.get().mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); const ESM::LevelledListBase::LevelItem& content = list.at(subRowIndex); switch (subColIndex) { - case 0: return QString(content.mId.c_str()); - case 1: return content.mLevel; + case 0: + return QString(content.mId.getRefIdString().c_str()); + case 1: + return content.mLevel; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } } - void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const override + void setNestedData(const RefIdColumn* column, RefIdData& data, int row, const QVariant& value, int subRowIndex, + int subColIndex) const override { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); + Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, mType))); ESXRecordT leveled = record.get(); std::vector& list = leveled.mList; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) + throw std::runtime_error("index out of range"); - switch(subColIndex) + switch (subColIndex) { - case 0: list.at(subRowIndex).mId = value.toString().toStdString(); break; - case 1: list.at(subRowIndex).mLevel = static_cast(value.toInt()); break; + case 0: + list.at(subRowIndex).mId = ESM::RefId::stringRefId(value.toString().toStdString()); + break; + case 1: + list.at(subRowIndex).mLevel = static_cast(value.toInt()); + break; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } - record.setModified (leveled); + record.setModified(leveled); } - int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const override - { - return 2; - } + int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 2; } - int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const override + int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + const Record& record + = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return static_cast(record.get().mList.size()); } diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index cd2dd89dff0..e0d57997269 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -1,20 +1,42 @@ #include "refidcollection.hpp" -#include +#include #include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -#include - -#include "refidadapter.hpp" -#include "refidadapterimp.hpp" #include "columns.hpp" -#include "nestedtablewrapper.hpp" #include "nestedcoladapterimp.hpp" +#include "nestedtablewrapper.hpp" +#include "refidadapter.hpp" +#include "refidadapterimp.hpp" -CSMWorld::RefIdColumn::RefIdColumn (int columnId, Display displayType, int flag, - bool editable, bool userEditable) - : NestableColumn (columnId, displayType, flag), mEditable (editable), mUserEditable (userEditable) -{} +CSMWorld::RefIdColumn::RefIdColumn(int columnId, Display displayType, int flag, bool editable, bool userEditable) + : NestableColumn(columnId, displayType, flag) + , mEditable(editable) + , mUserEditable(userEditable) +{ +} bool CSMWorld::RefIdColumn::isEditable() const { @@ -26,12 +48,12 @@ bool CSMWorld::RefIdColumn::isUserEditable() const return mUserEditable; } -const CSMWorld::RefIdAdapter& CSMWorld::RefIdCollection::findAdapter (UniversalId::Type type) const +const CSMWorld::RefIdAdapter& CSMWorld::RefIdCollection::findAdapter(UniversalId::Type type) const { - std::map::const_iterator iter = mAdapters.find (type); + std::map::const_iterator iter = mAdapters.find(type); - if (iter==mAdapters.end()) - throw std::logic_error ("unsupported type in RefIdCollection"); + if (iter == mAdapters.end()) + throw std::logic_error("unsupported type in RefIdCollection"); return *iter->second; } @@ -40,8 +62,8 @@ CSMWorld::RefIdCollection::RefIdCollection() { BaseColumns baseColumns; - mColumns.emplace_back(Columns::ColumnId_Id, ColumnBase::Display_Id, - ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false); + mColumns.emplace_back( + Columns::ColumnId_Id, ColumnBase::Display_Id, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false); baseColumns.mId = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Modification, ColumnBase::Display_RecordState, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, true, false); @@ -49,84 +71,78 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.emplace_back(Columns::ColumnId_RecordType, ColumnBase::Display_RefRecordType, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false); baseColumns.mType = &mColumns.back(); + mColumns.emplace_back(Columns::ColumnId_Blocked, ColumnBase::Display_Boolean, + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh); + baseColumns.mBlocked = &mColumns.back(); - ModelColumns modelColumns (baseColumns); + ModelColumns modelColumns(baseColumns); + mColumns.emplace_back(Columns::ColumnId_Persistent, ColumnBase::Display_Boolean); + modelColumns.mPersistence = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Model, ColumnBase::Display_Mesh); modelColumns.mModel = &mColumns.back(); - NameColumns nameColumns (modelColumns); + NameColumns nameColumns(modelColumns); - mColumns.emplace_back(Columns::ColumnId_Name, ColumnBase::Display_String); + // Only items that can be placed in a container have the 32 character limit, but enforce + // that for all referenceable types for now. + mColumns.emplace_back(Columns::ColumnId_Name, ColumnBase::Display_String32); nameColumns.mName = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Script, ColumnBase::Display_Script); nameColumns.mScript = &mColumns.back(); - InventoryColumns inventoryColumns (nameColumns); + InventoryColumns inventoryColumns(nameColumns); mColumns.emplace_back(Columns::ColumnId_Icon, ColumnBase::Display_Icon); inventoryColumns.mIcon = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Weight, ColumnBase::Display_Float); inventoryColumns.mWeight = &mColumns.back(); - mColumns.emplace_back(Columns::ColumnId_CoinValue, ColumnBase::Display_Integer); + mColumns.emplace_back(Columns::ColumnId_GoldValue, ColumnBase::Display_Integer); inventoryColumns.mValue = &mColumns.back(); - IngredientColumns ingredientColumns (inventoryColumns); - mColumns.emplace_back(Columns::ColumnId_EffectList, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + IngredientColumns ingredientColumns(inventoryColumns); + mColumns.emplace_back(Columns::ColumnId_EffectList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); ingredientColumns.mEffects = &mColumns.back(); std::map ingredientEffectsMap; - ingredientEffectsMap.insert(std::make_pair(UniversalId::Type_Ingredient, - new IngredEffectRefIdAdapter ())); + ingredientEffectsMap.insert(std::make_pair(UniversalId::Type_Ingredient, new IngredEffectRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), ingredientEffectsMap); - mColumns.back().addColumn( - new NestedChildColumn (Columns::ColumnId_EffectId, ColumnBase::Display_IngredEffectId)); - mColumns.back().addColumn( - new NestedChildColumn (Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); - mColumns.back().addColumn( - new NestedChildColumn (Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); + mColumns.back().addColumn(new NestedChildColumn(Columns::ColumnId_EffectId, ColumnBase::Display_IngredEffectId)); + mColumns.back().addColumn(new NestedChildColumn(Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); + mColumns.back().addColumn(new NestedChildColumn(Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); // nested table - PotionColumns potionColumns (inventoryColumns); - mColumns.emplace_back(Columns::ColumnId_EffectList, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + PotionColumns potionColumns(inventoryColumns); + mColumns.emplace_back(Columns::ColumnId_EffectList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); potionColumns.mEffects = &mColumns.back(); // see refidadapterimp.hpp std::map effectsMap; - effectsMap.insert(std::make_pair(UniversalId::Type_Potion, - new EffectsRefIdAdapter (UniversalId::Type_Potion))); + effectsMap.insert( + std::make_pair(UniversalId::Type_Potion, new EffectsRefIdAdapter(UniversalId::Type_Potion))); mNestedAdapters.emplace_back(&mColumns.back(), effectsMap); + mColumns.back().addColumn(new NestedChildColumn(Columns::ColumnId_EffectId, ColumnBase::Display_EffectId)); + mColumns.back().addColumn(new NestedChildColumn(Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); + mColumns.back().addColumn(new NestedChildColumn(Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); + mColumns.back().addColumn(new NestedChildColumn(Columns::ColumnId_EffectRange, ColumnBase::Display_EffectRange)); + mColumns.back().addColumn(new NestedChildColumn(Columns::ColumnId_EffectArea, ColumnBase::Display_String)); mColumns.back().addColumn( - new NestedChildColumn (Columns::ColumnId_EffectId, ColumnBase::Display_EffectId)); - mColumns.back().addColumn( - new NestedChildColumn (Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); - mColumns.back().addColumn( - new NestedChildColumn (Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); - mColumns.back().addColumn( - new NestedChildColumn (Columns::ColumnId_EffectRange, ColumnBase::Display_EffectRange)); - mColumns.back().addColumn( - new NestedChildColumn (Columns::ColumnId_EffectArea, ColumnBase::Display_String)); - mColumns.back().addColumn( - new NestedChildColumn (Columns::ColumnId_Duration, ColumnBase::Display_Integer)); // reuse from light - mColumns.back().addColumn( - new NestedChildColumn (Columns::ColumnId_MinMagnitude, ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new NestedChildColumn (Columns::ColumnId_MaxMagnitude, ColumnBase::Display_Integer)); + new NestedChildColumn(Columns::ColumnId_Duration, ColumnBase::Display_Integer)); // reuse from light + mColumns.back().addColumn(new NestedChildColumn(Columns::ColumnId_MinMagnitude, ColumnBase::Display_Integer)); + mColumns.back().addColumn(new NestedChildColumn(Columns::ColumnId_MaxMagnitude, ColumnBase::Display_Integer)); - EnchantableColumns enchantableColumns (inventoryColumns); + EnchantableColumns enchantableColumns(inventoryColumns); mColumns.emplace_back(Columns::ColumnId_Enchantment, ColumnBase::Display_Enchantment); enchantableColumns.mEnchantment = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_EnchantmentPoints, ColumnBase::Display_Integer); enchantableColumns.mEnchantmentPoints = &mColumns.back(); - ToolColumns toolsColumns (inventoryColumns); + ToolColumns toolsColumns(inventoryColumns); mColumns.emplace_back(Columns::ColumnId_Quality, ColumnBase::Display_Float); toolsColumns.mQuality = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Charges, ColumnBase::Display_Integer); toolsColumns.mUses = &mColumns.back(); - ActorColumns actorsColumns (nameColumns); + ActorColumns actorsColumns(nameColumns); mColumns.emplace_back(Columns::ColumnId_AiHello, ColumnBase::Display_UnsignedInteger16); actorsColumns.mHello = &mColumns.back(); @@ -138,194 +154,152 @@ CSMWorld::RefIdCollection::RefIdCollection() actorsColumns.mAlarm = &mColumns.back(); // Nested table - mColumns.emplace_back(Columns::ColumnId_ActorInventory, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + mColumns.emplace_back( + Columns::ColumnId_ActorInventory, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); actorsColumns.mInventory = &mColumns.back(); std::map inventoryMap; - inventoryMap.insert(std::make_pair(UniversalId::Type_Npc, - new NestedInventoryRefIdAdapter (UniversalId::Type_Npc))); - inventoryMap.insert(std::make_pair(UniversalId::Type_Creature, - new NestedInventoryRefIdAdapter (UniversalId::Type_Creature))); + inventoryMap.insert( + std::make_pair(UniversalId::Type_Npc, new NestedInventoryRefIdAdapter(UniversalId::Type_Npc))); + inventoryMap.insert(std::make_pair( + UniversalId::Type_Creature, new NestedInventoryRefIdAdapter(UniversalId::Type_Creature))); mNestedAdapters.emplace_back(&mColumns.back(), inventoryMap); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_InventoryItemId, CSMWorld::ColumnBase::Display_Referenceable)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_ItemCount, CSMWorld::ColumnBase::Display_Integer)); + new RefIdColumn(Columns::ColumnId_InventoryItemId, CSMWorld::ColumnBase::Display_Referenceable)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_ItemCount, CSMWorld::ColumnBase::Display_Integer)); // Nested table - mColumns.emplace_back(Columns::ColumnId_SpellList, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + mColumns.emplace_back(Columns::ColumnId_SpellList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); actorsColumns.mSpells = &mColumns.back(); std::map spellsMap; - spellsMap.insert(std::make_pair(UniversalId::Type_Npc, - new NestedSpellRefIdAdapter (UniversalId::Type_Npc))); - spellsMap.insert(std::make_pair(UniversalId::Type_Creature, - new NestedSpellRefIdAdapter (UniversalId::Type_Creature))); + spellsMap.insert( + std::make_pair(UniversalId::Type_Npc, new NestedSpellRefIdAdapter(UniversalId::Type_Npc))); + spellsMap.insert(std::make_pair( + UniversalId::Type_Creature, new NestedSpellRefIdAdapter(UniversalId::Type_Creature))); mNestedAdapters.emplace_back(&mColumns.back(), spellsMap); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_SpellId, CSMWorld::ColumnBase::Display_Spell)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_SpellId, CSMWorld::ColumnBase::Display_Spell)); // Nested table - mColumns.emplace_back(Columns::ColumnId_NpcDestinations, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + mColumns.emplace_back( + Columns::ColumnId_NpcDestinations, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); actorsColumns.mDestinations = &mColumns.back(); std::map destMap; - destMap.insert(std::make_pair(UniversalId::Type_Npc, - new NestedTravelRefIdAdapter (UniversalId::Type_Npc))); - destMap.insert(std::make_pair(UniversalId::Type_Creature, - new NestedTravelRefIdAdapter (UniversalId::Type_Creature))); + destMap.insert( + std::make_pair(UniversalId::Type_Npc, new NestedTravelRefIdAdapter(UniversalId::Type_Npc))); + destMap.insert(std::make_pair( + UniversalId::Type_Creature, new NestedTravelRefIdAdapter(UniversalId::Type_Creature))); mNestedAdapters.emplace_back(&mColumns.back(), destMap); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_DestinationCell, CSMWorld::ColumnBase::Display_Cell)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_PosX, CSMWorld::ColumnBase::Display_Float)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_PosY, CSMWorld::ColumnBase::Display_Float)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_PosZ, CSMWorld::ColumnBase::Display_Float)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_RotX, CSMWorld::ColumnBase::Display_Double)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_RotY, CSMWorld::ColumnBase::Display_Double)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_RotZ, CSMWorld::ColumnBase::Display_Double)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_DestinationCell, CSMWorld::ColumnBase::Display_Cell)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_PosX, CSMWorld::ColumnBase::Display_Float)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_PosY, CSMWorld::ColumnBase::Display_Float)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_PosZ, CSMWorld::ColumnBase::Display_Float)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_RotX, CSMWorld::ColumnBase::Display_Double)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_RotY, CSMWorld::ColumnBase::Display_Double)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_RotZ, CSMWorld::ColumnBase::Display_Double)); // Nested table - mColumns.emplace_back(Columns::ColumnId_AiPackageList, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + mColumns.emplace_back(Columns::ColumnId_AiPackageList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); actorsColumns.mAiPackages = &mColumns.back(); std::map aiMap; - aiMap.insert(std::make_pair(UniversalId::Type_Npc, - new ActorAiRefIdAdapter (UniversalId::Type_Npc))); - aiMap.insert(std::make_pair(UniversalId::Type_Creature, - new ActorAiRefIdAdapter (UniversalId::Type_Creature))); + aiMap.insert(std::make_pair(UniversalId::Type_Npc, new ActorAiRefIdAdapter(UniversalId::Type_Npc))); + aiMap.insert( + std::make_pair(UniversalId::Type_Creature, new ActorAiRefIdAdapter(UniversalId::Type_Creature))); mNestedAdapters.emplace_back(&mColumns.back(), aiMap); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiPackageType, CSMWorld::ColumnBase::Display_AiPackageType)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiWanderDist, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiDuration, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiWanderToD, CSMWorld::ColumnBase::Display_Integer)); - - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Idle1, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Idle2, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Idle3, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Idle4, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Idle5, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Idle6, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Idle7, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Idle8, CSMWorld::ColumnBase::Display_Integer)); - - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiWanderRepeat, CSMWorld::ColumnBase::Display_Boolean)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiActivateName, CSMWorld::ColumnBase::Display_String)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiTargetId, CSMWorld::ColumnBase::Display_String)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiTargetCell, CSMWorld::ColumnBase::Display_String)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_PosX, CSMWorld::ColumnBase::Display_Float)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_PosY, CSMWorld::ColumnBase::Display_Float)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_PosZ, CSMWorld::ColumnBase::Display_Float)); + new RefIdColumn(Columns::ColumnId_AiPackageType, CSMWorld::ColumnBase::Display_AiPackageType)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiWanderDist, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiDuration, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiWanderToD, CSMWorld::ColumnBase::Display_Integer)); + + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle2, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle3, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle4, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle5, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle6, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle7, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle8, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Idle9, CSMWorld::ColumnBase::Display_Integer)); + + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiWanderRepeat, CSMWorld::ColumnBase::Display_Boolean)); + mColumns.back().addColumn( + new RefIdColumn(Columns::ColumnId_AiActivateName, CSMWorld::ColumnBase::Display_String32)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiTargetId, CSMWorld::ColumnBase::Display_String32)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AiTargetCell, CSMWorld::ColumnBase::Display_String)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_PosX, CSMWorld::ColumnBase::Display_Float)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_PosY, CSMWorld::ColumnBase::Display_Float)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_PosZ, CSMWorld::ColumnBase::Display_Float)); static const struct { int mName; unsigned int mFlag; - } sServiceTable[] = - { - { Columns::ColumnId_BuysWeapons, ESM::NPC::Weapon}, - { Columns::ColumnId_BuysArmor, ESM::NPC::Armor}, - { Columns::ColumnId_BuysClothing, ESM::NPC::Clothing}, - { Columns::ColumnId_BuysBooks, ESM::NPC::Books}, - { Columns::ColumnId_BuysIngredients, ESM::NPC::Ingredients}, - { Columns::ColumnId_BuysLockpicks, ESM::NPC::Picks}, - { Columns::ColumnId_BuysProbes, ESM::NPC::Probes}, - { Columns::ColumnId_BuysLights, ESM::NPC::Lights}, - { Columns::ColumnId_BuysApparati, ESM::NPC::Apparatus}, - { Columns::ColumnId_BuysRepairItems, ESM::NPC::RepairItem}, - { Columns::ColumnId_BuysMiscItems, ESM::NPC::Misc}, - { Columns::ColumnId_BuysPotions, ESM::NPC::Potions}, - { Columns::ColumnId_BuysMagicItems, ESM::NPC::MagicItems}, - { Columns::ColumnId_SellsSpells, ESM::NPC::Spells}, - { Columns::ColumnId_Trainer, ESM::NPC::Training}, - { Columns::ColumnId_Spellmaking, ESM::NPC::Spellmaking}, - { Columns::ColumnId_EnchantingService, ESM::NPC::Enchanting}, - { Columns::ColumnId_RepairService, ESM::NPC::Repair}, - { -1, 0 } - }; - - for (int i=0; sServiceTable[i].mName!=-1; ++i) + } sServiceTable[] = { { Columns::ColumnId_BuysWeapons, ESM::NPC::Weapon }, + { Columns::ColumnId_BuysArmor, ESM::NPC::Armor }, { Columns::ColumnId_BuysClothing, ESM::NPC::Clothing }, + { Columns::ColumnId_BuysBooks, ESM::NPC::Books }, { Columns::ColumnId_BuysIngredients, ESM::NPC::Ingredients }, + { Columns::ColumnId_BuysLockpicks, ESM::NPC::Picks }, { Columns::ColumnId_BuysProbes, ESM::NPC::Probes }, + { Columns::ColumnId_BuysLights, ESM::NPC::Lights }, { Columns::ColumnId_BuysApparati, ESM::NPC::Apparatus }, + { Columns::ColumnId_BuysRepairItems, ESM::NPC::RepairItem }, + { Columns::ColumnId_BuysMiscItems, ESM::NPC::Misc }, { Columns::ColumnId_BuysPotions, ESM::NPC::Potions }, + { Columns::ColumnId_BuysMagicItems, ESM::NPC::MagicItems }, { Columns::ColumnId_SellsSpells, ESM::NPC::Spells }, + { Columns::ColumnId_Trainer, ESM::NPC::Training }, { Columns::ColumnId_Spellmaking, ESM::NPC::Spellmaking }, + { Columns::ColumnId_EnchantingService, ESM::NPC::Enchanting }, + { Columns::ColumnId_RepairService, ESM::NPC::Repair }, { -1, 0 } }; + + for (int i = 0; sServiceTable[i].mName != -1; ++i) { mColumns.emplace_back(sServiceTable[i].mName, ColumnBase::Display_Boolean); - actorsColumns.mServices.insert (std::make_pair (&mColumns.back(), sServiceTable[i].mFlag)); + actorsColumns.mServices.insert(std::make_pair(&mColumns.back(), sServiceTable[i].mFlag)); } mColumns.emplace_back(Columns::ColumnId_AutoCalc, ColumnBase::Display_Boolean, - ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh); - const RefIdColumn *autoCalc = &mColumns.back(); + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh); + const RefIdColumn* autoCalc = &mColumns.back(); - mColumns.emplace_back(Columns::ColumnId_ApparatusType, - ColumnBase::Display_ApparatusType); - const RefIdColumn *apparatusType = &mColumns.back(); + mColumns.emplace_back(Columns::ColumnId_ApparatusType, ColumnBase::Display_ApparatusType); + const RefIdColumn* apparatusType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_ArmorType, ColumnBase::Display_ArmorType); - const RefIdColumn *armorType = &mColumns.back(); + const RefIdColumn* armorType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Health, ColumnBase::Display_Integer); - const RefIdColumn *health = &mColumns.back(); + const RefIdColumn* health = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_ArmorValue, ColumnBase::Display_Integer); - const RefIdColumn *armor = &mColumns.back(); + const RefIdColumn* armor = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_BookType, ColumnBase::Display_BookType); - const RefIdColumn *bookType = &mColumns.back(); + const RefIdColumn* bookType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Skill, ColumnBase::Display_SkillId); - const RefIdColumn *skill = &mColumns.back(); + const RefIdColumn* skill = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Text, ColumnBase::Display_LongString); - const RefIdColumn *text = &mColumns.back(); + const RefIdColumn* text = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_ClothingType, ColumnBase::Display_ClothingType); - const RefIdColumn *clothingType = &mColumns.back(); + const RefIdColumn* clothingType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_WeightCapacity, ColumnBase::Display_Float); - const RefIdColumn *weightCapacity = &mColumns.back(); + const RefIdColumn* weightCapacity = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_OrganicContainer, ColumnBase::Display_Boolean); - const RefIdColumn *organic = &mColumns.back(); + const RefIdColumn* organic = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Respawn, ColumnBase::Display_Boolean); - const RefIdColumn *respawn = &mColumns.back(); + const RefIdColumn* respawn = &mColumns.back(); // Nested table - mColumns.emplace_back(Columns::ColumnId_ContainerContent, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); - const RefIdColumn *content = &mColumns.back(); + mColumns.emplace_back( + Columns::ColumnId_ContainerContent, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + const RefIdColumn* content = &mColumns.back(); std::map contMap; - contMap.insert(std::make_pair(UniversalId::Type_Container, - new NestedInventoryRefIdAdapter (UniversalId::Type_Container))); + contMap.insert(std::make_pair( + UniversalId::Type_Container, new NestedInventoryRefIdAdapter(UniversalId::Type_Container))); mNestedAdapters.emplace_back(&mColumns.back(), contMap); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_InventoryItemId, CSMWorld::ColumnBase::Display_Referenceable)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_ItemCount, CSMWorld::ColumnBase::Display_Integer)); + new RefIdColumn(Columns::ColumnId_InventoryItemId, CSMWorld::ColumnBase::Display_Referenceable)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_ItemCount, CSMWorld::ColumnBase::Display_Integer)); - CreatureColumns creatureColumns (actorsColumns); + CreatureColumns creatureColumns(actorsColumns); mColumns.emplace_back(Columns::ColumnId_CreatureType, ColumnBase::Display_CreatureType); creatureColumns.mType = &mColumns.back(); @@ -338,98 +312,82 @@ CSMWorld::RefIdCollection::RefIdCollection() { int mName; unsigned int mFlag; - } sCreatureFlagTable[] = - { - { Columns::ColumnId_Biped, ESM::Creature::Bipedal }, - { Columns::ColumnId_HasWeapon, ESM::Creature::Weapon }, - { Columns::ColumnId_Swims, ESM::Creature::Swims }, - { Columns::ColumnId_Flies, ESM::Creature::Flies }, - { Columns::ColumnId_Walks, ESM::Creature::Walks }, - { Columns::ColumnId_Essential, ESM::Creature::Essential }, - { -1, 0 } - }; + } sCreatureFlagTable[] = { { Columns::ColumnId_Biped, ESM::Creature::Bipedal }, + { Columns::ColumnId_HasWeapon, ESM::Creature::Weapon }, { Columns::ColumnId_Swims, ESM::Creature::Swims }, + { Columns::ColumnId_Flies, ESM::Creature::Flies }, { Columns::ColumnId_Walks, ESM::Creature::Walks }, + { Columns::ColumnId_Essential, ESM::Creature::Essential }, { -1, 0 } }; // for re-use in NPC records - const RefIdColumn *essential = nullptr; + const RefIdColumn* essential = nullptr; - for (int i=0; sCreatureFlagTable[i].mName!=-1; ++i) + for (int i = 0; sCreatureFlagTable[i].mName != -1; ++i) { mColumns.emplace_back(sCreatureFlagTable[i].mName, ColumnBase::Display_Boolean); - creatureColumns.mFlags.insert (std::make_pair (&mColumns.back(), sCreatureFlagTable[i].mFlag)); + creatureColumns.mFlags.insert(std::make_pair(&mColumns.back(), sCreatureFlagTable[i].mFlag)); switch (sCreatureFlagTable[i].mFlag) { - case ESM::Creature::Essential: essential = &mColumns.back(); break; + case ESM::Creature::Essential: + essential = &mColumns.back(); + break; } } mColumns.emplace_back(Columns::ColumnId_BloodType, ColumnBase::Display_BloodType); // For re-use in NPC records. - const RefIdColumn *bloodType = &mColumns.back(); + const RefIdColumn* bloodType = &mColumns.back(); creatureColumns.mBloodType = bloodType; - creatureColumns.mFlags.insert (std::make_pair (respawn, ESM::Creature::Respawn)); + creatureColumns.mFlags.insert(std::make_pair(respawn, ESM::Creature::Respawn)); // Nested table - mColumns.emplace_back(Columns::ColumnId_CreatureAttributes, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + mColumns.emplace_back( + Columns::ColumnId_CreatureAttributes, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); creatureColumns.mAttributes = &mColumns.back(); std::map creaAttrMap; creaAttrMap.insert(std::make_pair(UniversalId::Type_Creature, new CreatureAttributesRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), creaAttrMap); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Attribute, CSMWorld::ColumnBase::Display_Attribute, false, false)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AttributeValue, CSMWorld::ColumnBase::Display_Integer)); + new RefIdColumn(Columns::ColumnId_Attribute, CSMWorld::ColumnBase::Display_Attribute, false, false)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_AttributeValue, CSMWorld::ColumnBase::Display_Integer)); // Nested table - mColumns.emplace_back(Columns::ColumnId_CreatureAttack, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + mColumns.emplace_back( + Columns::ColumnId_CreatureAttack, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); creatureColumns.mAttacks = &mColumns.back(); std::map attackMap; attackMap.insert(std::make_pair(UniversalId::Type_Creature, new CreatureAttackRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), attackMap); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_CreatureAttack, CSMWorld::ColumnBase::Display_Integer, false, false)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_MinAttack, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_MaxAttack, CSMWorld::ColumnBase::Display_Integer)); + new RefIdColumn(Columns::ColumnId_CreatureAttack, CSMWorld::ColumnBase::Display_Integer, false, false)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_MinAttack, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_MaxAttack, CSMWorld::ColumnBase::Display_Integer)); // Nested list - mColumns.emplace_back(Columns::ColumnId_CreatureMisc, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List); + mColumns.emplace_back(Columns::ColumnId_CreatureMisc, ColumnBase::Display_NestedHeader, + ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List); creatureColumns.mMisc = &mColumns.back(); std::map creaMiscMap; creaMiscMap.insert(std::make_pair(UniversalId::Type_Creature, new CreatureMiscRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), creaMiscMap); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Level, CSMWorld::ColumnBase::Display_Integer, - ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Health, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Mana, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Fatigue, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_SoulPoints, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_CombatState, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_MagicState, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_StealthState, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Gold, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Level, CSMWorld::ColumnBase::Display_Integer, + ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Health, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Mana, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Fatigue, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_SoulPoints, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_CombatState, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_MagicState, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_StealthState, CSMWorld::ColumnBase::Display_Integer)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Gold, CSMWorld::ColumnBase::Display_Integer)); mColumns.emplace_back(Columns::ColumnId_OpenSound, ColumnBase::Display_Sound); - const RefIdColumn *openSound = &mColumns.back(); + const RefIdColumn* openSound = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_CloseSound, ColumnBase::Display_Sound); - const RefIdColumn *closeSound = &mColumns.back(); + const RefIdColumn* closeSound = &mColumns.back(); - LightColumns lightColumns (inventoryColumns); + LightColumns lightColumns(inventoryColumns); mColumns.emplace_back(Columns::ColumnId_Duration, ColumnBase::Display_Integer); lightColumns.mTime = &mColumns.back(); @@ -450,26 +408,21 @@ CSMWorld::RefIdCollection::RefIdCollection() { int mName; unsigned int mFlag; - } sLightFlagTable[] = - { - { Columns::ColumnId_Dynamic, ESM::Light::Dynamic }, - { Columns::ColumnId_Portable, ESM::Light::Carry }, - { Columns::ColumnId_NegativeLight, ESM::Light::Negative }, - { Columns::ColumnId_Fire, ESM::Light::Fire }, - { Columns::ColumnId_OffByDefault, ESM::Light::OffDefault }, - { -1, 0 } - }; - - for (int i=0; sLightFlagTable[i].mName!=-1; ++i) + } sLightFlagTable[] + = { { Columns::ColumnId_Dynamic, ESM::Light::Dynamic }, { Columns::ColumnId_Portable, ESM::Light::Carry }, + { Columns::ColumnId_NegativeLight, ESM::Light::Negative }, { Columns::ColumnId_Fire, ESM::Light::Fire }, + { Columns::ColumnId_OffByDefault, ESM::Light::OffDefault }, { -1, 0 } }; + + for (int i = 0; sLightFlagTable[i].mName != -1; ++i) { mColumns.emplace_back(sLightFlagTable[i].mName, ColumnBase::Display_Boolean); - lightColumns.mFlags.insert (std::make_pair (&mColumns.back(), sLightFlagTable[i].mFlag)); + lightColumns.mFlags.insert(std::make_pair(&mColumns.back(), sLightFlagTable[i].mFlag)); } mColumns.emplace_back(Columns::ColumnId_IsKey, ColumnBase::Display_Boolean); - const RefIdColumn *key = &mColumns.back(); + const RefIdColumn* key = &mColumns.back(); - NpcColumns npcColumns (actorsColumns); + NpcColumns npcColumns(actorsColumns); mColumns.emplace_back(Columns::ColumnId_Race, ColumnBase::Display_Race); npcColumns.mRace = &mColumns.back(); @@ -477,6 +430,7 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.emplace_back(Columns::ColumnId_Class, ColumnBase::Display_Class); npcColumns.mClass = &mColumns.back(); + // NAME32 enforced in IdCompletionDelegate::createEditor() mColumns.emplace_back(Columns::ColumnId_Faction, ColumnBase::Display_Faction); npcColumns.mFaction = &mColumns.back(); @@ -489,11 +443,11 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.emplace_back(Columns::ColumnId_Gender, ColumnBase::Display_GenderNpc); npcColumns.mGender = &mColumns.back(); - npcColumns.mFlags.insert (std::make_pair (essential, ESM::NPC::Essential)); + npcColumns.mFlags.insert(std::make_pair(essential, ESM::NPC::Essential)); - npcColumns.mFlags.insert (std::make_pair (respawn, ESM::NPC::Respawn)); + npcColumns.mFlags.insert(std::make_pair(respawn, ESM::NPC::Respawn)); - npcColumns.mFlags.insert (std::make_pair (autoCalc, ESM::NPC::Autocalc)); + npcColumns.mFlags.insert(std::make_pair(autoCalc, ESM::NPC::Autocalc)); // Re-used from Creature records. npcColumns.mBloodType = bloodType; @@ -503,56 +457,47 @@ CSMWorld::RefIdCollection::RefIdCollection() // These needs to be driven from the autocalculated setting. // Nested table - mColumns.emplace_back(Columns::ColumnId_NpcAttributes, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + mColumns.emplace_back(Columns::ColumnId_NpcAttributes, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); npcColumns.mAttributes = &mColumns.back(); std::map attrMap; attrMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcAttributesRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), attrMap); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Attribute, CSMWorld::ColumnBase::Display_Attribute, false, false)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_UChar, CSMWorld::ColumnBase::Display_UnsignedInteger8)); + new RefIdColumn(Columns::ColumnId_Attribute, CSMWorld::ColumnBase::Display_Attribute, false, false)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_UChar, CSMWorld::ColumnBase::Display_UnsignedInteger8)); // Nested table - mColumns.emplace_back(Columns::ColumnId_NpcSkills, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + mColumns.emplace_back(Columns::ColumnId_NpcSkills, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); npcColumns.mSkills = &mColumns.back(); std::map skillsMap; skillsMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcSkillsRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), skillsMap); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Skill, CSMWorld::ColumnBase::Display_SkillId, false, false)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_UChar, CSMWorld::ColumnBase::Display_UnsignedInteger8)); + new RefIdColumn(Columns::ColumnId_Skill, CSMWorld::ColumnBase::Display_SkillId, false, false)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_UChar, CSMWorld::ColumnBase::Display_UnsignedInteger8)); // Nested list - mColumns.emplace_back(Columns::ColumnId_NpcMisc, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List); + mColumns.emplace_back(Columns::ColumnId_NpcMisc, ColumnBase::Display_NestedHeader, + ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List); npcColumns.mMisc = &mColumns.back(); std::map miscMap; miscMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcMiscRefIdAdapter())); mNestedAdapters.emplace_back(&mColumns.back(), miscMap); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Level, CSMWorld::ColumnBase::Display_SignedInteger16)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Level, CSMWorld::ColumnBase::Display_SignedInteger16)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Health, CSMWorld::ColumnBase::Display_UnsignedInteger16)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Mana, CSMWorld::ColumnBase::Display_UnsignedInteger16)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Fatigue, CSMWorld::ColumnBase::Display_UnsignedInteger16)); + new RefIdColumn(Columns::ColumnId_Health, CSMWorld::ColumnBase::Display_UnsignedInteger16)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Mana, CSMWorld::ColumnBase::Display_UnsignedInteger16)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_NpcDisposition, CSMWorld::ColumnBase::Display_UnsignedInteger8)); + new RefIdColumn(Columns::ColumnId_Fatigue, CSMWorld::ColumnBase::Display_UnsignedInteger16)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_NpcReputation, CSMWorld::ColumnBase::Display_UnsignedInteger8)); + new RefIdColumn(Columns::ColumnId_NpcDisposition, CSMWorld::ColumnBase::Display_UnsignedInteger8)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_NpcRank, CSMWorld::ColumnBase::Display_UnsignedInteger8)); + new RefIdColumn(Columns::ColumnId_NpcReputation, CSMWorld::ColumnBase::Display_UnsignedInteger8)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_Gold, CSMWorld::ColumnBase::Display_Integer)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_NpcPersistence, CSMWorld::ColumnBase::Display_Boolean)); + new RefIdColumn(Columns::ColumnId_NpcRank, CSMWorld::ColumnBase::Display_UnsignedInteger8)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Gold, CSMWorld::ColumnBase::Display_Integer)); - WeaponColumns weaponColumns (enchantableColumns); + WeaponColumns weaponColumns(enchantableColumns); mColumns.emplace_back(Columns::ColumnId_WeaponType, ColumnBase::Display_WeaponType); weaponColumns.mType = &mColumns.back(); @@ -565,14 +510,14 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.emplace_back(Columns::ColumnId_WeaponReach, ColumnBase::Display_Float); weaponColumns.mReach = &mColumns.back(); - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) { - const RefIdColumn **column = - i==0 ? weaponColumns.mChop : (i==1 ? weaponColumns.mSlash : weaponColumns.mThrust); + const RefIdColumn** column + = i == 0 ? weaponColumns.mChop : (i == 1 ? weaponColumns.mSlash : weaponColumns.mThrust); - for (int j=0; j<2; ++j) + for (int j = 0; j < 2; ++j) { - mColumns.emplace_back(Columns::ColumnId_MinChop+i*2+j, ColumnBase::Display_Integer); + mColumns.emplace_back(Columns::ColumnId_MinChop + i * 2 + j, ColumnBase::Display_Integer); column[j] = &mColumns.back(); } } @@ -581,123 +526,119 @@ CSMWorld::RefIdCollection::RefIdCollection() { int mName; unsigned int mFlag; - } sWeaponFlagTable[] = - { - { Columns::ColumnId_Magical, ESM::Weapon::Magical }, - { Columns::ColumnId_Silver, ESM::Weapon::Silver }, - { -1, 0 } - }; + } sWeaponFlagTable[] = { { Columns::ColumnId_Magical, ESM::Weapon::Magical }, + { Columns::ColumnId_Silver, ESM::Weapon::Silver }, { -1, 0 } }; - for (int i=0; sWeaponFlagTable[i].mName!=-1; ++i) + for (int i = 0; sWeaponFlagTable[i].mName != -1; ++i) { mColumns.emplace_back(sWeaponFlagTable[i].mName, ColumnBase::Display_Boolean); - weaponColumns.mFlags.insert (std::make_pair (&mColumns.back(), sWeaponFlagTable[i].mFlag)); + weaponColumns.mFlags.insert(std::make_pair(&mColumns.back(), sWeaponFlagTable[i].mFlag)); } // Nested table mColumns.emplace_back(Columns::ColumnId_PartRefList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); - const RefIdColumn *partRef = &mColumns.back(); + const RefIdColumn* partRef = &mColumns.back(); std::map partMap; - partMap.insert(std::make_pair(UniversalId::Type_Armor, - new BodyPartRefIdAdapter (UniversalId::Type_Armor))); - partMap.insert(std::make_pair(UniversalId::Type_Clothing, - new BodyPartRefIdAdapter (UniversalId::Type_Clothing))); + partMap.insert( + std::make_pair(UniversalId::Type_Armor, new BodyPartRefIdAdapter(UniversalId::Type_Armor))); + partMap.insert(std::make_pair( + UniversalId::Type_Clothing, new BodyPartRefIdAdapter(UniversalId::Type_Clothing))); mNestedAdapters.emplace_back(&mColumns.back(), partMap); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_PartRefType, CSMWorld::ColumnBase::Display_PartRefType)); + new RefIdColumn(Columns::ColumnId_PartRefType, CSMWorld::ColumnBase::Display_PartRefType)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_PartRefMale, CSMWorld::ColumnBase::Display_BodyPart)); + mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_PartRefFemale, CSMWorld::ColumnBase::Display_BodyPart)); + + LevListColumns creatureLevListColumns(baseColumns); + LevListColumns itemLevListColumns(baseColumns); + std::map creatureLevListMap, itemLevListMap; + creatureLevListMap.insert(std::make_pair(UniversalId::Type_CreatureLevelledList, + new NestedLevListRefIdAdapter(UniversalId::Type_CreatureLevelledList))); + itemLevListMap.insert(std::make_pair(UniversalId::Type_ItemLevelledList, + new NestedLevListRefIdAdapter(UniversalId::Type_ItemLevelledList))); + + // Levelled creature nested table + mColumns.emplace_back(Columns::ColumnId_LevelledList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + creatureLevListColumns.mLevList = &mColumns.back(); + mNestedAdapters.emplace_back(&mColumns.back(), creatureLevListMap); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_PartRefMale, CSMWorld::ColumnBase::Display_BodyPart)); + new RefIdColumn(Columns::ColumnId_LevelledCreatureId, CSMWorld::ColumnBase::Display_Referenceable)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_PartRefFemale, CSMWorld::ColumnBase::Display_BodyPart)); - - LevListColumns levListColumns (baseColumns); + new RefIdColumn(Columns::ColumnId_LevelledItemLevel, CSMWorld::ColumnBase::Display_Integer)); - // Nested table - mColumns.emplace_back(Columns::ColumnId_LevelledList, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); - levListColumns.mLevList = &mColumns.back(); - std::map levListMap; - levListMap.insert(std::make_pair(UniversalId::Type_CreatureLevelledList, - new NestedLevListRefIdAdapter (UniversalId::Type_CreatureLevelledList))); - levListMap.insert(std::make_pair(UniversalId::Type_ItemLevelledList, - new NestedLevListRefIdAdapter (UniversalId::Type_ItemLevelledList))); - mNestedAdapters.emplace_back(&mColumns.back(), levListMap); + // Levelled item nested table + mColumns.emplace_back(Columns::ColumnId_LevelledList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); + itemLevListColumns.mLevList = &mColumns.back(); + mNestedAdapters.emplace_back(&mColumns.back(), itemLevListMap); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_LevelledItemId, CSMWorld::ColumnBase::Display_Referenceable)); + new RefIdColumn(Columns::ColumnId_LevelledItemId, CSMWorld::ColumnBase::Display_Referenceable)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_LevelledItemLevel, CSMWorld::ColumnBase::Display_Integer)); + new RefIdColumn(Columns::ColumnId_LevelledItemLevel, CSMWorld::ColumnBase::Display_Integer)); - // Nested list - mColumns.emplace_back(Columns::ColumnId_LevelledList, - ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List); - levListColumns.mNestedListLevList = &mColumns.back(); + // Shared levelled list nested list + mColumns.emplace_back(Columns::ColumnId_LevelledList, ColumnBase::Display_NestedHeader, + ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List); + creatureLevListColumns.mNestedListLevList = &mColumns.back(); + itemLevListColumns.mNestedListLevList = &mColumns.back(); std::map nestedListLevListMap; nestedListLevListMap.insert(std::make_pair(UniversalId::Type_CreatureLevelledList, - new NestedListLevListRefIdAdapter (UniversalId::Type_CreatureLevelledList))); + new NestedListLevListRefIdAdapter(UniversalId::Type_CreatureLevelledList))); nestedListLevListMap.insert(std::make_pair(UniversalId::Type_ItemLevelledList, - new NestedListLevListRefIdAdapter (UniversalId::Type_ItemLevelledList))); + new NestedListLevListRefIdAdapter(UniversalId::Type_ItemLevelledList))); mNestedAdapters.emplace_back(&mColumns.back(), nestedListLevListMap); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_LevelledItemTypeEach, CSMWorld::ColumnBase::Display_Boolean)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_LevelledItemType, CSMWorld::ColumnBase::Display_Boolean)); - mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_LevelledItemChanceNone, CSMWorld::ColumnBase::Display_UnsignedInteger8)); - - mAdapters.insert (std::make_pair (UniversalId::Type_Activator, - new NameRefIdAdapter (UniversalId::Type_Activator, nameColumns))); - mAdapters.insert (std::make_pair (UniversalId::Type_Potion, - new PotionRefIdAdapter (potionColumns, autoCalc))); - mAdapters.insert (std::make_pair (UniversalId::Type_Apparatus, - new ApparatusRefIdAdapter (inventoryColumns, apparatusType, toolsColumns.mQuality))); - mAdapters.insert (std::make_pair (UniversalId::Type_Armor, - new ArmorRefIdAdapter (enchantableColumns, armorType, health, armor, partRef))); - mAdapters.insert (std::make_pair (UniversalId::Type_Book, - new BookRefIdAdapter (enchantableColumns, bookType, skill, text))); - mAdapters.insert (std::make_pair (UniversalId::Type_Clothing, - new ClothingRefIdAdapter (enchantableColumns, clothingType, partRef))); - mAdapters.insert (std::make_pair (UniversalId::Type_Container, - new ContainerRefIdAdapter (nameColumns, weightCapacity, organic, respawn, content))); - mAdapters.insert (std::make_pair (UniversalId::Type_Creature, - new CreatureRefIdAdapter (creatureColumns))); - mAdapters.insert (std::make_pair (UniversalId::Type_Door, - new DoorRefIdAdapter (nameColumns, openSound, closeSound))); - mAdapters.insert (std::make_pair (UniversalId::Type_Ingredient, - new IngredientRefIdAdapter (ingredientColumns))); - mAdapters.insert (std::make_pair (UniversalId::Type_CreatureLevelledList, - new LevelledListRefIdAdapter ( - UniversalId::Type_CreatureLevelledList, levListColumns))); - mAdapters.insert (std::make_pair (UniversalId::Type_ItemLevelledList, - new LevelledListRefIdAdapter (UniversalId::Type_ItemLevelledList, levListColumns))); - mAdapters.insert (std::make_pair (UniversalId::Type_Light, - new LightRefIdAdapter (lightColumns))); - mAdapters.insert (std::make_pair (UniversalId::Type_Lockpick, - new ToolRefIdAdapter (UniversalId::Type_Lockpick, toolsColumns))); - mAdapters.insert (std::make_pair (UniversalId::Type_Miscellaneous, - new MiscRefIdAdapter (inventoryColumns, key))); - mAdapters.insert (std::make_pair (UniversalId::Type_Npc, - new NpcRefIdAdapter (npcColumns))); - mAdapters.insert (std::make_pair (UniversalId::Type_Probe, - new ToolRefIdAdapter (UniversalId::Type_Probe, toolsColumns))); - mAdapters.insert (std::make_pair (UniversalId::Type_Repair, - new ToolRefIdAdapter (UniversalId::Type_Repair, toolsColumns))); - mAdapters.insert (std::make_pair (UniversalId::Type_Static, - new ModelRefIdAdapter (UniversalId::Type_Static, modelColumns))); - mAdapters.insert (std::make_pair (UniversalId::Type_Weapon, - new WeaponRefIdAdapter (weaponColumns))); + new RefIdColumn(Columns::ColumnId_LevelledItemTypeEach, CSMWorld::ColumnBase::Display_Boolean)); + mColumns.back().addColumn( + new RefIdColumn(Columns::ColumnId_LevelledItemType, CSMWorld::ColumnBase::Display_Boolean)); + mColumns.back().addColumn( + new RefIdColumn(Columns::ColumnId_LevelledItemChanceNone, CSMWorld::ColumnBase::Display_UnsignedInteger8)); + + mAdapters.insert(std::make_pair( + UniversalId::Type_Activator, new NameRefIdAdapter(UniversalId::Type_Activator, nameColumns))); + mAdapters.insert(std::make_pair(UniversalId::Type_Potion, new PotionRefIdAdapter(potionColumns, autoCalc))); + mAdapters.insert(std::make_pair(UniversalId::Type_Apparatus, + new ApparatusRefIdAdapter(inventoryColumns, apparatusType, toolsColumns.mQuality))); + mAdapters.insert(std::make_pair( + UniversalId::Type_Armor, new ArmorRefIdAdapter(enchantableColumns, armorType, health, armor, partRef))); + mAdapters.insert( + std::make_pair(UniversalId::Type_Book, new BookRefIdAdapter(enchantableColumns, bookType, skill, text))); + mAdapters.insert(std::make_pair( + UniversalId::Type_Clothing, new ClothingRefIdAdapter(enchantableColumns, clothingType, partRef))); + mAdapters.insert(std::make_pair(UniversalId::Type_Container, + new ContainerRefIdAdapter(nameColumns, weightCapacity, organic, respawn, content))); + mAdapters.insert(std::make_pair(UniversalId::Type_Creature, new CreatureRefIdAdapter(creatureColumns))); + mAdapters.insert(std::make_pair(UniversalId::Type_Door, new DoorRefIdAdapter(nameColumns, openSound, closeSound))); + mAdapters.insert(std::make_pair(UniversalId::Type_Ingredient, new IngredientRefIdAdapter(ingredientColumns))); + mAdapters.insert(std::make_pair(UniversalId::Type_CreatureLevelledList, + new LevelledListRefIdAdapter( + UniversalId::Type_CreatureLevelledList, creatureLevListColumns))); + mAdapters.insert(std::make_pair(UniversalId::Type_ItemLevelledList, + new LevelledListRefIdAdapter(UniversalId::Type_ItemLevelledList, itemLevListColumns))); + mAdapters.insert(std::make_pair(UniversalId::Type_Light, new LightRefIdAdapter(lightColumns))); + mAdapters.insert(std::make_pair( + UniversalId::Type_Lockpick, new ToolRefIdAdapter(UniversalId::Type_Lockpick, toolsColumns))); + mAdapters.insert(std::make_pair(UniversalId::Type_Miscellaneous, new MiscRefIdAdapter(inventoryColumns, key))); + mAdapters.insert(std::make_pair(UniversalId::Type_Npc, new NpcRefIdAdapter(npcColumns))); + mAdapters.insert(std::make_pair( + UniversalId::Type_Probe, new ToolRefIdAdapter(UniversalId::Type_Probe, toolsColumns))); + mAdapters.insert(std::make_pair( + UniversalId::Type_Repair, new ToolRefIdAdapter(UniversalId::Type_Repair, toolsColumns))); + mAdapters.insert(std::make_pair( + UniversalId::Type_Static, new ModelRefIdAdapter(UniversalId::Type_Static, modelColumns))); + mAdapters.insert(std::make_pair(UniversalId::Type_Weapon, new WeaponRefIdAdapter(weaponColumns))); } CSMWorld::RefIdCollection::~RefIdCollection() { - for (std::map::iterator iter (mAdapters.begin()); - iter!=mAdapters.end(); ++iter) - delete iter->second; + for (std::map::iterator iter(mAdapters.begin()); iter != mAdapters.end(); ++iter) + delete iter->second; - for (std::vector > >::iterator iter (mNestedAdapters.begin()); - iter!=mNestedAdapters.end(); ++iter) + for (std::vector>>::iterator iter( + mNestedAdapters.begin()); + iter != mNestedAdapters.end(); ++iter) { - for (std::map::iterator it ((iter->second).begin()); - it!=(iter->second).end(); ++it) + for (std::map::iterator it((iter->second).begin()); + it != (iter->second).end(); ++it) delete it->second; } } @@ -707,17 +648,17 @@ int CSMWorld::RefIdCollection::getSize() const return mData.getSize(); } -std::string CSMWorld::RefIdCollection::getId (int index) const +ESM::RefId CSMWorld::RefIdCollection::getId(int index) const { - return getData (index, 0).toString().toUtf8().constData(); + return ESM::RefId::stringRefId(getData(index, 0).toString().toUtf8().constData()); } -int CSMWorld::RefIdCollection::getIndex (const std::string& id) const +int CSMWorld::RefIdCollection::getIndex(const ESM::RefId& id) const { - int index = searchId (id); + int index = searchId(id); - if (index==-1) - throw std::runtime_error ("invalid ID: " + id); + if (index == -1) + throw std::runtime_error("ID is not found in RefId collection: " + id.toDebugString()); return index; } @@ -727,140 +668,138 @@ int CSMWorld::RefIdCollection::getColumns() const return mColumns.size(); } -const CSMWorld::ColumnBase& CSMWorld::RefIdCollection::getColumn (int column) const +const CSMWorld::ColumnBase& CSMWorld::RefIdCollection::getColumn(int column) const { - return mColumns.at (column); + return mColumns.at(column); } -QVariant CSMWorld::RefIdCollection::getData (int index, int column) const +QVariant CSMWorld::RefIdCollection::getData(int index, int column) const { - RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (index); + RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(index); - const RefIdAdapter& adaptor = findAdapter (localIndex.second); + const RefIdAdapter& adaptor = findAdapter(localIndex.second); - return adaptor.getData (&mColumns.at (column), mData, localIndex.first); + return adaptor.getData(&mColumns.at(column), mData, localIndex.first); } -QVariant CSMWorld::RefIdCollection::getNestedData (int row, int column, int subRow, int subColumn) const +QVariant CSMWorld::RefIdCollection::getNestedData(int row, int column, int subRow, int subColumn) const { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); - return nestedAdapter.getNestedData(&mColumns.at (column), mData, localIndex.first, subRow, subColumn); + return nestedAdapter.getNestedData(&mColumns.at(column), mData, localIndex.first, subRow, subColumn); } -void CSMWorld::RefIdCollection::setData (int index, int column, const QVariant& data) +void CSMWorld::RefIdCollection::setData(int index, int column, const QVariant& data) { - RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (index); + RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(index); - const RefIdAdapter& adaptor = findAdapter (localIndex.second); + const RefIdAdapter& adaptor = findAdapter(localIndex.second); - adaptor.setData (&mColumns.at (column), mData, localIndex.first, data); + adaptor.setData(&mColumns.at(column), mData, localIndex.first, data); } void CSMWorld::RefIdCollection::setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) { - RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); + RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); - nestedAdapter.setNestedData(&mColumns.at (column), mData, localIndex.first, data, subRow, subColumn); - return; + nestedAdapter.setNestedData(&mColumns.at(column), mData, localIndex.first, data, subRow, subColumn); } -void CSMWorld::RefIdCollection::removeRows (int index, int count) +void CSMWorld::RefIdCollection::removeRows(int index, int count) { - mData.erase (index, count); + mData.erase(index, count); } void CSMWorld::RefIdCollection::removeNestedRows(int row, int column, int subRow) { - RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); + RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); - nestedAdapter.removeNestedRow(&mColumns.at (column), mData, localIndex.first, subRow); - return; + nestedAdapter.removeNestedRow(&mColumns.at(column), mData, localIndex.first, subRow); } -void CSMWorld::RefIdCollection::appendBlankRecord (const std::string& id, UniversalId::Type type) +void CSMWorld::RefIdCollection::appendBlankRecord(const ESM::RefId& id, UniversalId::Type type) { - mData.appendRecord (type, id, false); + mData.appendRecord(type, id, false); } -int CSMWorld::RefIdCollection::searchId (const std::string& id) const +int CSMWorld::RefIdCollection::searchId(const ESM::RefId& id) const { - RefIdData::LocalIndex localIndex = mData.searchId (id); + const RefIdData::LocalIndex localIndex = mData.searchId(id); - if (localIndex.first==-1) + if (localIndex.first == -1) return -1; - return mData.localToGlobalIndex (localIndex); + return mData.localToGlobalIndex(localIndex); } -void CSMWorld::RefIdCollection::replace (int index, const RecordBase& record) +void CSMWorld::RefIdCollection::replace(int index, std::unique_ptr record) { - mData.getRecord (mData.globalToLocalIndex (index)).assign (record); + mData.getRecord(mData.globalToLocalIndex(index)).assign(*record.release()); } -void CSMWorld::RefIdCollection::cloneRecord(const std::string& origin, - const std::string& destination, - const CSMWorld::UniversalId::Type type) +void CSMWorld::RefIdCollection::cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, const CSMWorld::UniversalId::Type type) { - std::unique_ptr newRecord(mData.getRecord(mData.searchId(origin)).modifiedCopy()); - mAdapters.find(type)->second->setId(*newRecord, destination); - mData.insertRecord(*newRecord, type, destination); + std::unique_ptr newRecord = mData.getRecord(mData.searchId(origin)).modifiedCopy(); + auto adapter = mAdapters.find(type); + assert(adapter != mAdapters.end()); + adapter->second->setId(*newRecord, destination.getRefIdString()); + mData.insertRecord(std::move(newRecord), type, destination); } -bool CSMWorld::RefIdCollection::touchRecord(const std::string& id) +bool CSMWorld::RefIdCollection::touchRecord(const ESM::RefId& id) { throw std::runtime_error("RefIdCollection::touchRecord is unimplemented"); return false; } -void CSMWorld::RefIdCollection::appendRecord (const RecordBase& record, - UniversalId::Type type) +void CSMWorld::RefIdCollection::appendRecord(std::unique_ptr record, UniversalId::Type type) { - std::string id = findAdapter (type).getId (record); + auto id = findAdapter(type).getId(*record.get()); - int index = mData.getAppendIndex (type); + int index = mData.getAppendIndex(type); - mData.appendRecord (type, id, false); + mData.appendRecord(type, id, false); - mData.getRecord (mData.globalToLocalIndex (index)).assign (record); + mData.getRecord(mData.globalToLocalIndex(index)).assign(*record.release()); } -const CSMWorld::RecordBase& CSMWorld::RefIdCollection::getRecord (const std::string& id) const +const CSMWorld::RecordBase& CSMWorld::RefIdCollection::getRecord(const ESM::RefId& id) const { - return mData.getRecord (mData.searchId (id)); + return mData.getRecord(mData.searchId(id)); } -const CSMWorld::RecordBase& CSMWorld::RefIdCollection::getRecord (int index) const +const CSMWorld::RecordBase& CSMWorld::RefIdCollection::getRecord(int index) const { - return mData.getRecord (mData.globalToLocalIndex (index)); + return mData.getRecord(mData.globalToLocalIndex(index)); } -void CSMWorld::RefIdCollection::load (ESM::ESMReader& reader, bool base, UniversalId::Type type) +void CSMWorld::RefIdCollection::load(ESM::ESMReader& reader, bool base, UniversalId::Type type) { mData.load(reader, base, type); } -int CSMWorld::RefIdCollection::getAppendIndex (const std::string& id, UniversalId::Type type) const +int CSMWorld::RefIdCollection::getAppendIndex(const ESM::RefId& id, UniversalId::Type type) const { - return mData.getAppendIndex (type); + return mData.getAppendIndex(type); } -std::vector CSMWorld::RefIdCollection::getIds (bool listDeleted) const +std::vector CSMWorld::RefIdCollection::getIds(bool listDeleted) const { - return mData.getIds (listDeleted); + return mData.getIds(listDeleted); } -bool CSMWorld::RefIdCollection::reorderRows (int baseIndex, const std::vector& newOrder) +bool CSMWorld::RefIdCollection::reorderRows(int baseIndex, const std::vector& newOrder) { return false; } -void CSMWorld::RefIdCollection::save (int index, ESM::ESMWriter& writer) const +void CSMWorld::RefIdCollection::save(int index, ESM::ESMWriter& writer) const { - mData.save (index, writer); + mData.save(index, writer); } const CSMWorld::RefIdData& CSMWorld::RefIdCollection::getDataSet() const @@ -870,7 +809,7 @@ const CSMWorld::RefIdData& CSMWorld::RefIdCollection::getDataSet() const int CSMWorld::RefIdCollection::getNestedRowsCount(int row, int column) const { - RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); + RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); return nestedAdapter.getNestedRowsCount(&mColumns.at(column), mData, localIndex.first); @@ -878,52 +817,51 @@ int CSMWorld::RefIdCollection::getNestedRowsCount(int row, int column) const int CSMWorld::RefIdCollection::getNestedColumnsCount(int row, int column) const { - RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); + RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); return nestedAdapter.getNestedColumnsCount(&mColumns.at(column), mData); } -CSMWorld::NestableColumn *CSMWorld::RefIdCollection::getNestableColumn(int column) +CSMWorld::NestableColumn* CSMWorld::RefIdCollection::getNestableColumn(int column) { return &mColumns.at(column); } void CSMWorld::RefIdCollection::addNestedRow(int row, int col, int position) { - RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); + RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(col), localIndex.second); nestedAdapter.addNestedRow(&mColumns.at(col), mData, localIndex.first, position); - return; } void CSMWorld::RefIdCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { - RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); + RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); nestedAdapter.setNestedTable(&mColumns.at(column), mData, localIndex.first, nestedTable); - return; } CSMWorld::NestedTableWrapperBase* CSMWorld::RefIdCollection::nestedTable(int row, int column) const { - RefIdData::LocalIndex localIndex = mData.globalToLocalIndex (row); + RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(row); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); return nestedAdapter.nestedTable(&mColumns.at(column), mData, localIndex.first); } -const CSMWorld::NestedRefIdAdapterBase& CSMWorld::RefIdCollection::getNestedAdapter(const CSMWorld::ColumnBase &column, UniversalId::Type type) const +const CSMWorld::NestedRefIdAdapterBase& CSMWorld::RefIdCollection::getNestedAdapter( + const CSMWorld::ColumnBase& column, UniversalId::Type type) const { - for (std::vector > >::const_iterator iter (mNestedAdapters.begin()); - iter!=mNestedAdapters.end(); ++iter) + for (std::vector>>::const_iterator + iter(mNestedAdapters.begin()); + iter != mNestedAdapters.end(); ++iter) { if ((iter->first) == &column) { - std::map::const_iterator it = - (iter->second).find(type); + std::map::const_iterator it = (iter->second).find(type); if (it == (iter->second).end()) throw std::runtime_error("No such type in the nestedadapters"); @@ -934,7 +872,7 @@ const CSMWorld::NestedRefIdAdapterBase& CSMWorld::RefIdCollection::getNestedAdap throw std::runtime_error("No such column in the nestedadapters"); } -void CSMWorld::RefIdCollection::copyTo (int index, RefIdCollection& target) const +void CSMWorld::RefIdCollection::copyTo(int index, RefIdCollection& target) const { - mData.copyTo (index, target.mData); + mData.copyTo(index, target.mData); } diff --git a/apps/opencs/model/world/refidcollection.hpp b/apps/opencs/model/world/refidcollection.hpp index e85263ac13c..10892535cf8 100644 --- a/apps/opencs/model/world/refidcollection.hpp +++ b/apps/opencs/model/world/refidcollection.hpp @@ -1,146 +1,150 @@ #ifndef CSM_WOLRD_REFIDCOLLECTION_H #define CSM_WOLRD_REFIDCOLLECTION_H -#include -#include +#include + #include +#include +#include +#include +#include +#include +#include + +#include -#include "columnbase.hpp" #include "collectionbase.hpp" +#include "columnbase.hpp" #include "nestedcollection.hpp" #include "refiddata.hpp" namespace ESM { + class ESMReader; class ESMWriter; } namespace CSMWorld { class RefIdAdapter; + struct RecordBase; struct NestedTableWrapperBase; class NestedRefIdAdapterBase; class RefIdColumn : public NestableColumn { - bool mEditable; - bool mUserEditable; - - public: + bool mEditable; + bool mUserEditable; - RefIdColumn (int columnId, Display displayType, - int flag = Flag_Table | Flag_Dialogue, bool editable = true, - bool userEditable = true); + public: + RefIdColumn(int columnId, Display displayType, int flag = Flag_Table | Flag_Dialogue, bool editable = true, + bool userEditable = true); - bool isEditable() const override; + bool isEditable() const override; - bool isUserEditable() const override; + bool isUserEditable() const override; }; class RefIdCollection : public CollectionBase, public NestedCollection { - private: - - RefIdData mData; - std::deque mColumns; - std::map mAdapters; - - std::vector > > mNestedAdapters; - - private: + private: + RefIdData mData; + std::deque mColumns; + std::map mAdapters; - const RefIdAdapter& findAdapter (UniversalId::Type) const; - ///< Throws an exception if no adaptor for \a Type can be found. + std::vector>> mNestedAdapters; - const NestedRefIdAdapterBase& getNestedAdapter(const ColumnBase &column, UniversalId::Type type) const; + private: + const RefIdAdapter& findAdapter(UniversalId::Type) const; + ///< Throws an exception if no adaptor for \a Type can be found. - public: + const NestedRefIdAdapterBase& getNestedAdapter(const ColumnBase& column, UniversalId::Type type) const; - RefIdCollection(); + public: + RefIdCollection(); - virtual ~RefIdCollection(); + ~RefIdCollection() override; - int getSize() const override; + int getSize() const override; - std::string getId (int index) const override; + ESM::RefId getId(int index) const override; - int getIndex (const std::string& id) const override; + int getIndex(const ESM::RefId& id) const override; - int getColumns() const override; + int getColumns() const override; - const ColumnBase& getColumn (int column) const override; + const ColumnBase& getColumn(int column) const override; - QVariant getData (int index, int column) const override; + QVariant getData(int index, int column) const override; - void setData (int index, int column, const QVariant& data) override; + void setData(int index, int column, const QVariant& data) override; - void removeRows (int index, int count) override; + void removeRows(int index, int count) override; - void cloneRecord(const std::string& origin, - const std::string& destination, - const UniversalId::Type type) override; + void cloneRecord( + const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) override; - bool touchRecord(const std::string& id) override; + bool touchRecord(const ESM::RefId& id) override; - void appendBlankRecord (const std::string& id, UniversalId::Type type) override; - ///< \param type Will be ignored, unless the collection supports multiple record types + void appendBlankRecord(const ESM::RefId& id, UniversalId::Type type) override; + ///< \param type Will be ignored, unless the collection supports multiple record types - int searchId (const std::string& id) const override; - ////< Search record with \a id. - /// \return index of record (if found) or -1 (not found) + int searchId(const ESM::RefId& id) const override; + ////< Search record with \a id. + /// \return index of record (if found) or -1 (not found) - void replace (int index, const RecordBase& record) override; - ///< If the record type does not match, an exception is thrown. - /// - /// \attention \a record must not change the ID. + void replace(int index, std::unique_ptr record) override; + ///< If the record type does not match, an exception is thrown. + /// + /// \attention \a record must not change the ID. - void appendRecord (const RecordBase& record, UniversalId::Type type) override; - ///< If the record type does not match, an exception is thrown. - /// - ///< \param type Will be ignored, unless the collection supports multiple record types + void appendRecord(std::unique_ptr record, UniversalId::Type type) override; + ///< If the record type does not match, an exception is thrown. + /// + ///< \param type Will be ignored, unless the collection supports multiple record types - const RecordBase& getRecord (const std::string& id) const override; + const RecordBase& getRecord(const ESM::RefId& id) const override; - const RecordBase& getRecord (int index) const override; + const RecordBase& getRecord(int index) const override; - void load (ESM::ESMReader& reader, bool base, UniversalId::Type type); + void load(ESM::ESMReader& reader, bool base, UniversalId::Type type); - int getAppendIndex (const std::string& id, UniversalId::Type type) const override; - ///< \param type Will be ignored, unless the collection supports multiple record types + int getAppendIndex(const ESM::RefId& id, UniversalId::Type type) const override; + ///< \param type Will be ignored, unless the collection supports multiple record types - std::vector getIds (bool listDeleted) const override; - ///< Return a sorted collection of all IDs - /// - /// \param listDeleted include deleted record in the list + std::vector getIds(bool listDeleted) const override; + ///< Return a sorted collection of all IDs + /// + /// \param listDeleted include deleted record in the list - bool reorderRows (int baseIndex, const std::vector& newOrder) override; - ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices - /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). - /// - /// \return Success? + bool reorderRows(int baseIndex, const std::vector& newOrder) override; + ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices + /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). + /// + /// \return Success? - QVariant getNestedData(int row, int column, int subRow, int subColumn) const override; + QVariant getNestedData(int row, int column, int subRow, int subColumn) const override; - NestedTableWrapperBase* nestedTable(int row, int column) const override; + NestedTableWrapperBase* nestedTable(int row, int column) const override; - void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; + void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; - int getNestedRowsCount(int row, int column) const override; + int getNestedRowsCount(int row, int column) const override; - int getNestedColumnsCount(int row, int column) const override; + int getNestedColumnsCount(int row, int column) const override; - NestableColumn *getNestableColumn(int column) override; + NestableColumn* getNestableColumn(int column) override; - void setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) override; + void setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) override; - void removeNestedRows(int row, int column, int subRow) override; + void removeNestedRows(int row, int column, int subRow) override; - void addNestedRow(int row, int col, int position) override; + void addNestedRow(int row, int col, int position) override; - void save (int index, ESM::ESMWriter& writer) const; + void save(int index, ESM::ESMWriter& writer) const; - const RefIdData& getDataSet() const; //I can't figure out a better name for this one :( - void copyTo (int index, RefIdCollection& target) const; + const RefIdData& getDataSet() const; // I can't figure out a better name for this one :( + void copyTo(int index, RefIdCollection& target) const; }; } diff --git a/apps/opencs/model/world/refiddata.cpp b/apps/opencs/model/world/refiddata.cpp index e2ffbcca6b4..bab9ae38d62 100644 --- a/apps/opencs/model/world/refiddata.cpp +++ b/apps/opencs/model/world/refiddata.cpp @@ -1,183 +1,232 @@ #include "refiddata.hpp" -#include -#include +#include +#include + +#include -CSMWorld::RefIdDataContainerBase::~RefIdDataContainerBase() {} +#include +#include +#include +namespace ESM +{ + class ESMWriter; +} -std::string CSMWorld::RefIdData::getRecordId(const CSMWorld::RefIdData::LocalIndex &index) const +ESM::RefId CSMWorld::RefIdData::getRecordId(const CSMWorld::RefIdData::LocalIndex& index) const { - std::map::const_iterator found = - mRecordContainers.find (index.second); + std::map::const_iterator found = mRecordContainers.find(index.second); if (found == mRecordContainers.end()) - throw std::logic_error ("invalid local index type"); + throw std::logic_error("invalid local index type"); return found->second->getId(index.first); } CSMWorld::RefIdData::RefIdData() { - mRecordContainers.insert (std::make_pair (UniversalId::Type_Activator, &mActivators)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Potion, &mPotions)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Apparatus, &mApparati)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Armor, &mArmors)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Book, &mBooks)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Clothing, &mClothing)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Container, &mContainers)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Creature, &mCreatures)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Door, &mDoors)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Ingredient, &mIngredients)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_CreatureLevelledList, - &mCreatureLevelledLists)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_ItemLevelledList, &mItemLevelledLists)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Light, &mLights)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Lockpick, &mLockpicks)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Miscellaneous, &mMiscellaneous)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Npc, &mNpcs)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Probe, &mProbes)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Repair, &mRepairs)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Static, &mStatics)); - mRecordContainers.insert (std::make_pair (UniversalId::Type_Weapon, &mWeapons)); -} - -CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::globalToLocalIndex (int index) const -{ - for (std::map::const_iterator iter ( - mRecordContainers.begin()); iter!=mRecordContainers.end(); ++iter) + mRecordContainers.insert(std::make_pair(UniversalId::Type_Activator, &mActivators)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Potion, &mPotions)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Apparatus, &mApparati)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Armor, &mArmors)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Book, &mBooks)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Clothing, &mClothing)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Container, &mContainers)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Creature, &mCreatures)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Door, &mDoors)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Ingredient, &mIngredients)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_CreatureLevelledList, &mCreatureLevelledLists)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_ItemLevelledList, &mItemLevelledLists)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Light, &mLights)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Lockpick, &mLockpicks)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Miscellaneous, &mMiscellaneous)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Npc, &mNpcs)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Probe, &mProbes)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Repair, &mRepairs)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Static, &mStatics)); + mRecordContainers.insert(std::make_pair(UniversalId::Type_Weapon, &mWeapons)); +} + +CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::globalToLocalIndex(int index) const +{ + for (std::map::const_iterator iter(mRecordContainers.begin()); + iter != mRecordContainers.end(); ++iter) { - if (indexsecond->getSize()) - return LocalIndex (index, iter->first); + if (index < iter->second->getSize()) + return LocalIndex(index, iter->first); index -= iter->second->getSize(); } - throw std::runtime_error ("RefIdData index out of range"); + throw std::runtime_error("RefIdData index out of range"); } -int CSMWorld::RefIdData::localToGlobalIndex (const LocalIndex& index) - const +int CSMWorld::RefIdData::localToGlobalIndex(const LocalIndex& index) const { - std::map::const_iterator end = - mRecordContainers.find (index.second); + std::map::const_iterator end = mRecordContainers.find(index.second); - if (end==mRecordContainers.end()) - throw std::logic_error ("invalid local index type"); + if (end == mRecordContainers.end()) + throw std::logic_error("invalid local index type"); int globalIndex = index.first; - for (std::map::const_iterator iter ( - mRecordContainers.begin()); iter!=end; ++iter) + for (std::map::const_iterator iter(mRecordContainers.begin()); + iter != end; ++iter) globalIndex += iter->second->getSize(); return globalIndex; } -CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::searchId ( - const std::string& id) const +CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::searchId(const ESM::RefId& id) const { - std::string id2 = Misc::StringUtils::lowerCase (id); - - std::map >::const_iterator iter = mIndex.find (id2); + auto iter = mIndex.find(id); - if (iter==mIndex.end()) - return std::make_pair (-1, CSMWorld::UniversalId::Type_None); + if (iter == mIndex.end()) + return std::make_pair(-1, CSMWorld::UniversalId::Type_None); return iter->second; } -void CSMWorld::RefIdData::erase (int index, int count) +unsigned int CSMWorld::RefIdData::getRecordFlags(const ESM::RefId& id) const +{ + LocalIndex localIndex = searchId(id); + + switch (localIndex.second) + { + case UniversalId::Type_Activator: + return mActivators.getRecordFlags(localIndex.first); + case UniversalId::Type_Potion: + return mPotions.getRecordFlags(localIndex.first); + case UniversalId::Type_Apparatus: + return mApparati.getRecordFlags(localIndex.first); + case UniversalId::Type_Armor: + return mArmors.getRecordFlags(localIndex.first); + case UniversalId::Type_Book: + return mBooks.getRecordFlags(localIndex.first); + case UniversalId::Type_Clothing: + return mClothing.getRecordFlags(localIndex.first); + case UniversalId::Type_Container: + return mContainers.getRecordFlags(localIndex.first); + case UniversalId::Type_Creature: + return mCreatures.getRecordFlags(localIndex.first); + case UniversalId::Type_Door: + return mDoors.getRecordFlags(localIndex.first); + case UniversalId::Type_Ingredient: + return mIngredients.getRecordFlags(localIndex.first); + case UniversalId::Type_CreatureLevelledList: + return mCreatureLevelledLists.getRecordFlags(localIndex.first); + case UniversalId::Type_ItemLevelledList: + return mItemLevelledLists.getRecordFlags(localIndex.first); + case UniversalId::Type_Light: + return mLights.getRecordFlags(localIndex.first); + case UniversalId::Type_Lockpick: + return mLockpicks.getRecordFlags(localIndex.first); + case UniversalId::Type_Miscellaneous: + return mMiscellaneous.getRecordFlags(localIndex.first); + case UniversalId::Type_Npc: + return mNpcs.getRecordFlags(localIndex.first); + case UniversalId::Type_Probe: + return mProbes.getRecordFlags(localIndex.first); + case UniversalId::Type_Repair: + return mRepairs.getRecordFlags(localIndex.first); + case UniversalId::Type_Static: + return mStatics.getRecordFlags(localIndex.first); + case UniversalId::Type_Weapon: + return mWeapons.getRecordFlags(localIndex.first); + default: + break; + } + + return 0; +} + +void CSMWorld::RefIdData::erase(int index, int count) { - LocalIndex localIndex = globalToLocalIndex (index); + LocalIndex localIndex = globalToLocalIndex(index); - std::map::const_iterator iter = - mRecordContainers.find (localIndex.second); + std::map::const_iterator iter + = mRecordContainers.find(localIndex.second); - while (count>0 && iter!=mRecordContainers.end()) + while (count > 0 && iter != mRecordContainers.end()) { int size = iter->second->getSize(); - if (localIndex.first+count>size) + if (localIndex.first + count > size) { - erase (localIndex, size-localIndex.first); - count -= size-localIndex.first; + erase(localIndex, size - localIndex.first); + count -= size - localIndex.first; ++iter; - if (iter==mRecordContainers.end()) - throw std::runtime_error ("invalid count value for erase operation"); + if (iter == mRecordContainers.end()) + throw std::runtime_error("invalid count value for erase operation"); localIndex.first = 0; localIndex.second = iter->first; } else { - erase (localIndex, count); + erase(localIndex, count); count = 0; } } } -const CSMWorld::RecordBase& CSMWorld::RefIdData::getRecord (const LocalIndex& index) const +const CSMWorld::RecordBase& CSMWorld::RefIdData::getRecord(const LocalIndex& index) const { - std::map::const_iterator iter = - mRecordContainers.find (index.second); + std::map::const_iterator iter = mRecordContainers.find(index.second); - if (iter==mRecordContainers.end()) - throw std::logic_error ("invalid local index type"); + if (iter == mRecordContainers.end()) + throw std::logic_error("invalid local index type"); - return iter->second->getRecord (index.first); + return iter->second->getRecord(index.first); } -CSMWorld::RecordBase& CSMWorld::RefIdData::getRecord (const LocalIndex& index) +CSMWorld::RecordBase& CSMWorld::RefIdData::getRecord(const LocalIndex& index) { - std::map::iterator iter = - mRecordContainers.find (index.second); + std::map::iterator iter = mRecordContainers.find(index.second); - if (iter==mRecordContainers.end()) - throw std::logic_error ("invalid local index type"); + if (iter == mRecordContainers.end()) + throw std::logic_error("invalid local index type"); - return iter->second->getRecord (index.first); + return iter->second->getRecord(index.first); } -void CSMWorld::RefIdData::appendRecord (UniversalId::Type type, const std::string& id, bool base) +void CSMWorld::RefIdData::appendRecord(UniversalId::Type type, const ESM::RefId& id, bool base) { - std::map::iterator iter = - mRecordContainers.find (type); + std::map::iterator iter = mRecordContainers.find(type); - if (iter==mRecordContainers.end()) - throw std::logic_error ("invalid local index type"); + if (iter == mRecordContainers.end()) + throw std::logic_error("invalid local index type"); - iter->second->appendRecord (id, base); + iter->second->appendRecord(id, base); - mIndex.insert (std::make_pair (Misc::StringUtils::lowerCase (id), - LocalIndex (iter->second->getSize()-1, type))); + mIndex.insert(std::make_pair(id, LocalIndex(iter->second->getSize() - 1, type))); } -int CSMWorld::RefIdData::getAppendIndex (UniversalId::Type type) const +int CSMWorld::RefIdData::getAppendIndex(UniversalId::Type type) const { int index = 0; - for (std::map::const_iterator iter ( - mRecordContainers.begin()); iter!=mRecordContainers.end(); ++iter) + for (std::map::const_iterator iter(mRecordContainers.begin()); + iter != mRecordContainers.end(); ++iter) { index += iter->second->getSize(); - if (type==iter->first) + if (type == iter->first) break; } return index; } -void CSMWorld::RefIdData::load (ESM::ESMReader& reader, bool base, CSMWorld::UniversalId::Type type) +void CSMWorld::RefIdData::load(ESM::ESMReader& reader, bool base, CSMWorld::UniversalId::Type type) { - std::map::iterator found = - mRecordContainers.find (type); + std::map::iterator found = mRecordContainers.find(type); if (found == mRecordContainers.end()) - throw std::logic_error ("Invalid Referenceable ID type"); + throw std::logic_error("Invalid Referenceable ID type"); int index = found->second->load(reader, base); if (index != -1) @@ -189,25 +238,23 @@ void CSMWorld::RefIdData::load (ESM::ESMReader& reader, bool base, CSMWorld::Uni } else { - mIndex[Misc::StringUtils::lowerCase(getRecordId(localIndex))] = localIndex; + mIndex[getRecordId(localIndex)] = localIndex; } } } -void CSMWorld::RefIdData::erase (const LocalIndex& index, int count) +void CSMWorld::RefIdData::erase(const LocalIndex& index, int count) { - std::map::iterator iter = - mRecordContainers.find (index.second); - if (iter==mRecordContainers.end()) - throw std::logic_error ("invalid local index type"); + std::map::iterator iter = mRecordContainers.find(index.second); + if (iter == mRecordContainers.end()) + throw std::logic_error("invalid local index type"); - for (int i=index.first; i::iterator result = - mIndex.find (Misc::StringUtils::lowerCase (iter->second->getId (i))); + auto result = mIndex.find(iter->second->getId(i)); - if (result!=mIndex.end()) - mIndex.erase (result); + if (result != mIndex.end()) + mIndex.erase(result); } // Adjust the local indexes to avoid gaps between them after removal of records @@ -215,8 +262,7 @@ void CSMWorld::RefIdData::erase (const LocalIndex& index, int count) int recordCount = iter->second->getSize(); while (recordIndex < recordCount) { - std::map::iterator recordIndexFound = - mIndex.find(Misc::StringUtils::lowerCase(iter->second->getId(recordIndex))); + auto recordIndexFound = mIndex.find(iter->second->getId(recordIndex)); if (recordIndexFound != mIndex.end()) { recordIndexFound->second.first -= count; @@ -224,7 +270,7 @@ void CSMWorld::RefIdData::erase (const LocalIndex& index, int count) ++recordIndex; } - iter->second->erase (index.first, count); + iter->second->erase(index.first, count); } int CSMWorld::RefIdData::getSize() const @@ -232,164 +278,161 @@ int CSMWorld::RefIdData::getSize() const return mIndex.size(); } -std::vector CSMWorld::RefIdData::getIds (bool listDeleted) const +std::vector CSMWorld::RefIdData::getIds(bool listDeleted) const { - std::vector ids; + std::vector ids; - for (std::map::const_iterator iter (mIndex.begin()); iter!=mIndex.end(); - ++iter) + for (auto iter(mIndex.begin()); iter != mIndex.end(); ++iter) { - if (listDeleted || !getRecord (iter->second).isDeleted()) + if (listDeleted || !getRecord(iter->second).isDeleted()) { - std::map::const_iterator container = - mRecordContainers.find (iter->second.second); + std::map::const_iterator container + = mRecordContainers.find(iter->second.second); - if (container==mRecordContainers.end()) - throw std::logic_error ("Invalid referenceable ID type"); + if (container == mRecordContainers.end()) + throw std::logic_error("Invalid referenceable ID type"); - ids.push_back (container->second->getId (iter->second.first)); + ids.push_back(container->second->getId(iter->second.first)); } } return ids; } -void CSMWorld::RefIdData::save (int index, ESM::ESMWriter& writer) const +void CSMWorld::RefIdData::save(int index, ESM::ESMWriter& writer) const { - LocalIndex localIndex = globalToLocalIndex (index); + LocalIndex localIndex = globalToLocalIndex(index); - std::map::const_iterator iter = - mRecordContainers.find (localIndex.second); + std::map::const_iterator iter + = mRecordContainers.find(localIndex.second); - if (iter==mRecordContainers.end()) - throw std::logic_error ("invalid local index type"); + if (iter == mRecordContainers.end()) + throw std::logic_error("invalid local index type"); - iter->second->save (localIndex.first, writer); + iter->second->save(localIndex.first, writer); } -const CSMWorld::RefIdDataContainer< ESM::Book >& CSMWorld::RefIdData::getBooks() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getBooks() const { return mBooks; } -const CSMWorld::RefIdDataContainer< ESM::Activator >& CSMWorld::RefIdData::getActivators() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getActivators() const { return mActivators; } -const CSMWorld::RefIdDataContainer< ESM::Potion >& CSMWorld::RefIdData::getPotions() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getPotions() const { return mPotions; } -const CSMWorld::RefIdDataContainer< ESM::Apparatus >& CSMWorld::RefIdData::getApparati() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getApparati() const { return mApparati; } -const CSMWorld::RefIdDataContainer< ESM::Armor >& CSMWorld::RefIdData::getArmors() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getArmors() const { return mArmors; } -const CSMWorld::RefIdDataContainer< ESM::Clothing >& CSMWorld::RefIdData::getClothing() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getClothing() const { return mClothing; } -const CSMWorld::RefIdDataContainer< ESM::Container >& CSMWorld::RefIdData::getContainers() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getContainers() const { return mContainers; } -const CSMWorld::RefIdDataContainer< ESM::Creature >& CSMWorld::RefIdData::getCreatures() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getCreatures() const { return mCreatures; } -const CSMWorld::RefIdDataContainer< ESM::Door >& CSMWorld::RefIdData::getDoors() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getDoors() const { return mDoors; } -const CSMWorld::RefIdDataContainer< ESM::Ingredient >& CSMWorld::RefIdData::getIngredients() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getIngredients() const { return mIngredients; } -const CSMWorld::RefIdDataContainer< ESM::CreatureLevList >& CSMWorld::RefIdData::getCreatureLevelledLists() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getCreatureLevelledLists() const { return mCreatureLevelledLists; } -const CSMWorld::RefIdDataContainer< ESM::ItemLevList >& CSMWorld::RefIdData::getItemLevelledList() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getItemLevelledList() const { return mItemLevelledLists; } -const CSMWorld::RefIdDataContainer< ESM::Light >& CSMWorld::RefIdData::getLights() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getLights() const { return mLights; } -const CSMWorld::RefIdDataContainer< ESM::Lockpick >& CSMWorld::RefIdData::getLocpicks() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getLocpicks() const { return mLockpicks; } -const CSMWorld::RefIdDataContainer< ESM::Miscellaneous >& CSMWorld::RefIdData::getMiscellaneous() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getMiscellaneous() const { return mMiscellaneous; } -const CSMWorld::RefIdDataContainer< ESM::NPC >& CSMWorld::RefIdData::getNPCs() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getNPCs() const { return mNpcs; } -const CSMWorld::RefIdDataContainer< ESM::Weapon >& CSMWorld::RefIdData::getWeapons() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getWeapons() const { return mWeapons; } -const CSMWorld::RefIdDataContainer< ESM::Probe >& CSMWorld::RefIdData::getProbes() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getProbes() const { return mProbes; } -const CSMWorld::RefIdDataContainer< ESM::Repair >& CSMWorld::RefIdData::getRepairs() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getRepairs() const { return mRepairs; } -const CSMWorld::RefIdDataContainer< ESM::Static >& CSMWorld::RefIdData::getStatics() const +const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getStatics() const { return mStatics; } -void CSMWorld::RefIdData::insertRecord (CSMWorld::RecordBase& record, CSMWorld::UniversalId::Type type, const std::string& id) +void CSMWorld::RefIdData::insertRecord( + std::unique_ptr record, CSMWorld::UniversalId::Type type, const ESM::RefId& id) { - std::map::iterator iter = - mRecordContainers.find (type); + std::map::iterator iter = mRecordContainers.find(type); - if (iter==mRecordContainers.end()) - throw std::logic_error ("invalid local index type"); + if (iter == mRecordContainers.end()) + throw std::logic_error("invalid local index type"); - iter->second->insertRecord(record); + iter->second->insertRecord(std::move(record)); - mIndex.insert (std::make_pair (Misc::StringUtils::lowerCase (id), - LocalIndex (iter->second->getSize()-1, type))); + mIndex.insert(std::make_pair(id, LocalIndex(iter->second->getSize() - 1, type))); } -void CSMWorld::RefIdData::copyTo (int index, RefIdData& target) const +void CSMWorld::RefIdData::copyTo(int index, RefIdData& target) const { - LocalIndex localIndex = globalToLocalIndex (index); - - RefIdDataContainerBase *source = mRecordContainers.find (localIndex.second)->second; - - std::string id = source->getId (localIndex.first); + LocalIndex localIndex = globalToLocalIndex(index); - std::unique_ptr newRecord (source->getRecord (localIndex.first).modifiedCopy()); + auto foundIndex = mRecordContainers.find(localIndex.second); + assert(foundIndex != mRecordContainers.end()); + RefIdDataContainerBase* source = foundIndex->second; - target.insertRecord (*newRecord, localIndex.second, id); + target.insertRecord( + source->getRecord(localIndex.first).modifiedCopy(), localIndex.second, source->getId(localIndex.first)); } diff --git a/apps/opencs/model/world/refiddata.hpp b/apps/opencs/model/world/refiddata.hpp index 1480bb71d92..4870e8672aa 100644 --- a/apps/opencs/model/world/refiddata.hpp +++ b/apps/opencs/model/world/refiddata.hpp @@ -1,31 +1,38 @@ #ifndef CSM_WOLRD_REFIDDATA_H #define CSM_WOLRD_REFIDDATA_H -#include +#include +#include #include +#include +#include +#include +#include +#include +#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include #include "record.hpp" #include "universalid.hpp" @@ -39,94 +46,107 @@ namespace CSMWorld { struct RefIdDataContainerBase { - virtual ~RefIdDataContainerBase(); + virtual ~RefIdDataContainerBase() = default; virtual int getSize() const = 0; - virtual const RecordBase& getRecord (int index) const = 0; + virtual const RecordBase& getRecord(int index) const = 0; + + virtual RecordBase& getRecord(int index) = 0; - virtual RecordBase& getRecord (int index)= 0; + virtual unsigned int getRecordFlags(int index) const = 0; - virtual void appendRecord (const std::string& id, bool base) = 0; + virtual void appendRecord(const ESM::RefId& id, bool base) = 0; - virtual void insertRecord (RecordBase& record) = 0; + virtual void insertRecord(std::unique_ptr record) = 0; - virtual int load (ESM::ESMReader& reader, bool base) = 0; + virtual int load(ESM::ESMReader& reader, bool base) = 0; ///< \return index of a loaded record or -1 if no record was loaded - virtual void erase (int index, int count) = 0; + virtual void erase(int index, int count) = 0; - virtual std::string getId (int index) const = 0; + virtual ESM::RefId getId(int index) const = 0; - virtual void save (int index, ESM::ESMWriter& writer) const = 0; + virtual void save(int index, ESM::ESMWriter& writer) const = 0; }; - template + template struct RefIdDataContainer : public RefIdDataContainerBase { - std::vector > mContainer; + std::vector>> mContainer; int getSize() const override; - const RecordBase& getRecord (int index) const override; + const RecordBase& getRecord(int index) const override; - RecordBase& getRecord (int index) override; + RecordBase& getRecord(int index) override; - void appendRecord (const std::string& id, bool base) override; + unsigned int getRecordFlags(int index) const override; - void insertRecord (RecordBase& record) override; + void appendRecord(const ESM::RefId& id, bool base) override; - int load (ESM::ESMReader& reader, bool base) override; + void insertRecord(std::unique_ptr record) override; + + int load(ESM::ESMReader& reader, bool base) override; ///< \return index of a loaded record or -1 if no record was loaded - void erase (int index, int count) override; + void erase(int index, int count) override; - std::string getId (int index) const override; + ESM::RefId getId(int index) const override; - void save (int index, ESM::ESMWriter& writer) const override; + void save(int index, ESM::ESMWriter& writer) const override; }; - template - void RefIdDataContainer::insertRecord(RecordBase& record) + template + void RefIdDataContainer::insertRecord(std::unique_ptr record) { - Record& newRecord = dynamic_cast& >(record); - mContainer.push_back(newRecord); + assert(record != nullptr); + // convert base pointer to record type pointer + std::unique_ptr> typedRecord(&dynamic_cast&>(*record)); + record.release(); + mContainer.push_back(std::move(typedRecord)); } - template + template int RefIdDataContainer::getSize() const { - return static_cast (mContainer.size()); + return static_cast(mContainer.size()); + } + + template + const RecordBase& RefIdDataContainer::getRecord(int index) const + { + return *mContainer.at(index); } - template - const RecordBase& RefIdDataContainer::getRecord (int index) const + template + RecordBase& RefIdDataContainer::getRecord(int index) { - return mContainer.at (index); + return *mContainer.at(index); } - template - RecordBase& RefIdDataContainer::getRecord (int index) + template + unsigned int RefIdDataContainer::getRecordFlags(int index) const { - return mContainer.at (index); + return mContainer.at(index)->get().mRecordFlags; } - template - void RefIdDataContainer::appendRecord (const std::string& id, bool base) + template + void RefIdDataContainer::appendRecord(const ESM::RefId& id, bool base) { - Record record; + auto record = std::make_unique>(); - record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - record.mBase.mId = id; - record.mModified.mId = id; - (base ? record.mBase : record.mModified).blank(); + record->mBase.mId = id; + record->mModified.mId = id; + (base ? record->mBase : record->mModified).blank(); - mContainer.push_back (record); + mContainer.push_back(std::move(record)); } - template - int RefIdDataContainer::load (ESM::ESMReader& reader, bool base) + template + int RefIdDataContainer::load(ESM::ESMReader& reader, bool base) { RecordT record; bool isDeleted = false; @@ -137,7 +157,7 @@ namespace CSMWorld int numRecords = static_cast(mContainer.size()); for (; index < numRecords; ++index) { - if (Misc::StringUtils::ciEqual(mContainer[index].get().mId, record.mId)) + if ((mContainer[index]->get().mId == record.mId)) { break; } @@ -155,7 +175,7 @@ namespace CSMWorld // Flag the record as Deleted even for a base content file. // RefIdData is responsible for its erasure. - mContainer[index].mState = RecordBase::State_Deleted; + mContainer[index]->mState = RecordBase::State_Deleted; } else { @@ -164,153 +184,150 @@ namespace CSMWorld appendRecord(record.mId, base); if (base) { - mContainer.back().mBase = record; + mContainer.back()->mBase = record; } else { - mContainer.back().mModified = record; + mContainer.back()->mModified = record; } } else if (!base) { - mContainer[index].setModified(record); + mContainer[index]->setModified(record); } else { // Overwrite - mContainer[index].setModified(record); - mContainer[index].merge(); + mContainer[index]->setModified(record); + mContainer[index]->merge(); } } return index; } - template - void RefIdDataContainer::erase (int index, int count) + template + void RefIdDataContainer::erase(int index, int count) { - if (index<0 || index+count>getSize()) - throw std::runtime_error ("invalid RefIdDataContainer index"); + if (index < 0 || index + count > getSize()) + throw std::runtime_error("invalid RefIdDataContainer index"); - mContainer.erase (mContainer.begin()+index, mContainer.begin()+index+count); + mContainer.erase(mContainer.begin() + index, mContainer.begin() + index + count); } - template - std::string RefIdDataContainer::getId (int index) const + template + ESM::RefId RefIdDataContainer::getId(int index) const { - return mContainer.at (index).get().mId; + return mContainer.at(index)->get().mId; } - template - void RefIdDataContainer::save (int index, ESM::ESMWriter& writer) const + template + void RefIdDataContainer::save(int index, ESM::ESMWriter& writer) const { - Record record = mContainer.at(index); + const Record& record = *mContainer.at(index); if (record.isModified() || record.mState == RecordBase::State_Deleted) { RecordT esmRecord = record.get(); - writer.startRecord(esmRecord.sRecordId); + writer.startRecord(esmRecord.sRecordId, esmRecord.mRecordFlags); esmRecord.save(writer, record.mState == RecordBase::State_Deleted); writer.endRecord(esmRecord.sRecordId); } } - class RefIdData { - public: - - typedef std::pair LocalIndex; - - private: + public: + typedef std::pair LocalIndex; - RefIdDataContainer mActivators; - RefIdDataContainer mPotions; - RefIdDataContainer mApparati; - RefIdDataContainer mArmors; - RefIdDataContainer mBooks; - RefIdDataContainer mClothing; - RefIdDataContainer mContainers; - RefIdDataContainer mCreatures; - RefIdDataContainer mDoors; - RefIdDataContainer mIngredients; - RefIdDataContainer mCreatureLevelledLists; - RefIdDataContainer mItemLevelledLists; - RefIdDataContainer mLights; - RefIdDataContainer mLockpicks; - RefIdDataContainer mMiscellaneous; - RefIdDataContainer mNpcs; - RefIdDataContainer mProbes; - RefIdDataContainer mRepairs; - RefIdDataContainer mStatics; - RefIdDataContainer mWeapons; + private: + RefIdDataContainer mActivators; + RefIdDataContainer mPotions; + RefIdDataContainer mApparati; + RefIdDataContainer mArmors; + RefIdDataContainer mBooks; + RefIdDataContainer mClothing; + RefIdDataContainer mContainers; + RefIdDataContainer mCreatures; + RefIdDataContainer mDoors; + RefIdDataContainer mIngredients; + RefIdDataContainer mCreatureLevelledLists; + RefIdDataContainer mItemLevelledLists; + RefIdDataContainer mLights; + RefIdDataContainer mLockpicks; + RefIdDataContainer mMiscellaneous; + RefIdDataContainer mNpcs; + RefIdDataContainer mProbes; + RefIdDataContainer mRepairs; + RefIdDataContainer mStatics; + RefIdDataContainer mWeapons; - std::map mIndex; + std::map mIndex; - std::map mRecordContainers; + std::map mRecordContainers; - void erase (const LocalIndex& index, int count); - ///< Must not spill over into another type. + void erase(const LocalIndex& index, int count); + ///< Must not spill over into another type. - std::string getRecordId(const LocalIndex &index) const; + ESM::RefId getRecordId(const LocalIndex& index) const; - public: + public: + RefIdData(); - RefIdData(); + LocalIndex globalToLocalIndex(int index) const; - LocalIndex globalToLocalIndex (int index) const; + int localToGlobalIndex(const LocalIndex& index) const; - int localToGlobalIndex (const LocalIndex& index) const; + LocalIndex searchId(const ESM::RefId& id) const; - LocalIndex searchId (const std::string& id) const; + void erase(int index, int count); - void erase (int index, int count); + void insertRecord(std::unique_ptr record, CSMWorld::UniversalId::Type type, const ESM::RefId& id); - void insertRecord (CSMWorld::RecordBase& record, CSMWorld::UniversalId::Type type, - const std::string& id); + const RecordBase& getRecord(const LocalIndex& index) const; - const RecordBase& getRecord (const LocalIndex& index) const; + RecordBase& getRecord(const LocalIndex& index); - RecordBase& getRecord (const LocalIndex& index); + unsigned int getRecordFlags(const ESM::RefId& id) const; - void appendRecord (UniversalId::Type type, const std::string& id, bool base); + void appendRecord(UniversalId::Type type, const ESM::RefId& id, bool base); - int getAppendIndex (UniversalId::Type type) const; + int getAppendIndex(UniversalId::Type type) const; - void load (ESM::ESMReader& reader, bool base, UniversalId::Type type); + void load(ESM::ESMReader& reader, bool base, UniversalId::Type type); - int getSize() const; + int getSize() const; - std::vector getIds (bool listDeleted = true) const; - ///< Return a sorted collection of all IDs - /// - /// \param listDeleted include deleted record in the list + std::vector getIds(bool listDeleted = true) const; + ///< Return a sorted collection of all IDs + /// + /// \param listDeleted include deleted record in the list - void save (int index, ESM::ESMWriter& writer) const; + void save(int index, ESM::ESMWriter& writer) const; - //RECORD CONTAINERS ACCESS METHODS - const RefIdDataContainer& getBooks() const; - const RefIdDataContainer& getActivators() const; - const RefIdDataContainer& getPotions() const; - const RefIdDataContainer& getApparati() const; - const RefIdDataContainer& getArmors() const; - const RefIdDataContainer& getClothing() const; - const RefIdDataContainer& getContainers() const; - const RefIdDataContainer& getCreatures() const; - const RefIdDataContainer& getDoors() const; - const RefIdDataContainer& getIngredients() const; - const RefIdDataContainer& getCreatureLevelledLists() const; - const RefIdDataContainer& getItemLevelledList() const; - const RefIdDataContainer& getLights() const; - const RefIdDataContainer& getLocpicks() const; - const RefIdDataContainer& getMiscellaneous() const; - const RefIdDataContainer& getNPCs() const; - const RefIdDataContainer& getWeapons() const; - const RefIdDataContainer& getProbes() const; - const RefIdDataContainer& getRepairs() const; - const RefIdDataContainer& getStatics() const; + // RECORD CONTAINERS ACCESS METHODS + const RefIdDataContainer& getBooks() const; + const RefIdDataContainer& getActivators() const; + const RefIdDataContainer& getPotions() const; + const RefIdDataContainer& getApparati() const; + const RefIdDataContainer& getArmors() const; + const RefIdDataContainer& getClothing() const; + const RefIdDataContainer& getContainers() const; + const RefIdDataContainer& getCreatures() const; + const RefIdDataContainer& getDoors() const; + const RefIdDataContainer& getIngredients() const; + const RefIdDataContainer& getCreatureLevelledLists() const; + const RefIdDataContainer& getItemLevelledList() const; + const RefIdDataContainer& getLights() const; + const RefIdDataContainer& getLocpicks() const; + const RefIdDataContainer& getMiscellaneous() const; + const RefIdDataContainer& getNPCs() const; + const RefIdDataContainer& getWeapons() const; + const RefIdDataContainer& getProbes() const; + const RefIdDataContainer& getRepairs() const; + const RefIdDataContainer& getStatics() const; - void copyTo (int index, RefIdData& target) const; + void copyTo(int index, RefIdData& target) const; }; } diff --git a/apps/opencs/model/world/regionmap.cpp b/apps/opencs/model/world/regionmap.cpp index 6dbbac97fbc..79a0d5474db 100644 --- a/apps/opencs/model/world/regionmap.cpp +++ b/apps/opencs/model/world/regionmap.cpp @@ -1,52 +1,81 @@ #include "regionmap.hpp" -#include +#include +#include +#include +#include +#include +#include + #include +#include +#include +#include -#include +#include +#include +#include +#include -#include +#include +#include #include "data.hpp" #include "universalid.hpp" -CSMWorld::RegionMap::CellDescription::CellDescription() : mDeleted (false) {} +namespace CSMWorld +{ + float getLandHeight(const CSMWorld::Cell& cell, CSMWorld::Data& data) + { + const IdCollection& lands = data.getLand(); + int landIndex = lands.searchId(cell.mId); + if (landIndex == -1) + return 0.0f; + + // If any part of land is above water, returns > 0 - otherwise returns < 0 + const Land& land = lands.getRecord(landIndex).get(); + if (land.getLandData()) + return land.getLandData()->mMaxHeight - cell.mWater; -CSMWorld::RegionMap::CellDescription::CellDescription (const Record& cell) + return 0.0f; + } +} + +CSMWorld::RegionMap::CellDescription::CellDescription(const Record& cell, float landHeight) { const Cell& cell2 = cell.get(); if (!cell2.isExterior()) - throw std::logic_error ("Interior cell in region map"); + throw std::logic_error("Interior cell in region map"); + mMaxLandHeight = landHeight; mDeleted = cell.isDeleted(); - mRegion = cell2.mRegion; mName = cell2.mName; } -CSMWorld::CellCoordinates CSMWorld::RegionMap::getIndex (const QModelIndex& index) const +CSMWorld::CellCoordinates CSMWorld::RegionMap::getIndex(const QModelIndex& index) const { - return mMin.move (index.column(), mMax.getY()-mMin.getY() - index.row()-1); + return mMin.move(index.column(), mMax.getY() - mMin.getY() - index.row() - 1); } -QModelIndex CSMWorld::RegionMap::getIndex (const CellCoordinates& index) const +QModelIndex CSMWorld::RegionMap::getIndex(const CellCoordinates& index) const { // I hate you, Qt API naming scheme! - return QAbstractTableModel::index (mMax.getY()-mMin.getY() - (index.getY()-mMin.getY())-1, - index.getX()-mMin.getX()); + return QAbstractTableModel::index( + mMax.getY() - mMin.getY() - (index.getY() - mMin.getY()) - 1, index.getX() - mMin.getX()); } -CSMWorld::CellCoordinates CSMWorld::RegionMap::getIndex (const Cell& cell) const +CSMWorld::CellCoordinates CSMWorld::RegionMap::getIndex(const Cell& cell) const { - std::istringstream stream (cell.mId); + std::istringstream stream(cell.mId.getRefIdString()); char ignore; int x = 0; int y = 0; stream >> ignore >> x >> y; - return CellCoordinates (x, y); + return CellCoordinates(x, y); } void CSMWorld::RegionMap::buildRegions() @@ -55,13 +84,12 @@ void CSMWorld::RegionMap::buildRegions() int size = regions.getSize(); - for (int i=0; i& region = regions.getRecord (i); + const Record& region = regions.getRecord(i); if (!region.isDeleted()) - mColours.insert (std::make_pair (Misc::StringUtils::lowerCase (region.get().mId), - region.get().mMapColor)); + mColours.insert(std::make_pair(region.get().mId, region.get().mMapColor)); } } @@ -71,19 +99,19 @@ void CSMWorld::RegionMap::buildMap() int size = cells.getSize(); - for (int i=0; i& cell = cells.getRecord (i); + const Record& cell = cells.getRecord(i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) { - CellDescription description (cell); + CellDescription description(cell, getLandHeight(cell2, mData)); - CellCoordinates index = getIndex (cell2); + CellCoordinates index = getIndex(cell2); - mMap.insert (std::make_pair (index, description)); + mMap.insert(std::make_pair(index, description)); } } @@ -93,11 +121,11 @@ void CSMWorld::RegionMap::buildMap() mMax = mapSize.second; } -void CSMWorld::RegionMap::addCell (const CellCoordinates& index, const CellDescription& description) +void CSMWorld::RegionMap::addCell(const CellCoordinates& index, const CellDescription& description) { - std::map::iterator cell = mMap.find (index); + std::map::iterator cell = mMap.find(index); - if (cell!=mMap.end()) + if (cell != mMap.end()) { cell->second = description; } @@ -105,82 +133,78 @@ void CSMWorld::RegionMap::addCell (const CellCoordinates& index, const CellDescr { updateSize(); - mMap.insert (std::make_pair (index, description)); + mMap.insert(std::make_pair(index, description)); } - QModelIndex index2 = getIndex (index); + QModelIndex index2 = getIndex(index); - dataChanged (index2, index2); + dataChanged(index2, index2); } -void CSMWorld::RegionMap::addCells (int start, int end) +void CSMWorld::RegionMap::addCells(int start, int end) { const IdCollection& cells = mData.getCells(); - for (int i=start; i<=end; ++i) + for (int i = start; i <= end; ++i) { - const Record& cell = cells.getRecord (i); + const Record& cell = cells.getRecord(i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) { - CellCoordinates index = getIndex (cell2); + CellCoordinates index = getIndex(cell2); - CellDescription description (cell); + CellDescription description(cell, getLandHeight(cell.get(), mData)); - addCell (index, description); + addCell(index, description); } } } -void CSMWorld::RegionMap::removeCell (const CellCoordinates& index) +void CSMWorld::RegionMap::removeCell(const CellCoordinates& index) { - std::map::iterator cell = mMap.find (index); + std::map::iterator cell = mMap.find(index); - if (cell!=mMap.end()) + if (cell != mMap.end()) { - mMap.erase (cell); + mMap.erase(cell); - QModelIndex index2 = getIndex (index); + QModelIndex index2 = getIndex(index); - dataChanged (index2, index2); + dataChanged(index2, index2); updateSize(); } } -void CSMWorld::RegionMap::addRegion (const std::string& region, unsigned int colour) +void CSMWorld::RegionMap::addRegion(const ESM::RefId& region, unsigned int colour) { - mColours[Misc::StringUtils::lowerCase (region)] = colour; + mColours[region] = colour; } -void CSMWorld::RegionMap::removeRegion (const std::string& region) +void CSMWorld::RegionMap::removeRegion(const ESM::RefId& region) { - std::map::iterator iter ( - mColours.find (Misc::StringUtils::lowerCase (region))); + auto iter(mColours.find(region)); - if (iter!=mColours.end()) - mColours.erase (iter); + if (iter != mColours.end()) + mColours.erase(iter); } -void CSMWorld::RegionMap::updateRegions (const std::vector& regions) +void CSMWorld::RegionMap::updateRegions(const std::vector& regions) { - std::vector regions2 (regions); + std::vector regions2(regions); - std::for_each (regions2.begin(), regions2.end(), Misc::StringUtils::lowerCaseInPlace); - std::sort (regions2.begin(), regions2.end()); + std::sort(regions2.begin(), regions2.end()); - for (std::map::const_iterator iter (mMap.begin()); - iter!=mMap.end(); ++iter) + for (std::map::const_iterator iter(mMap.begin()); iter != mMap.end(); ++iter) { - if (!iter->second.mRegion.empty() && - std::find (regions2.begin(), regions2.end(), - Misc::StringUtils::lowerCase (iter->second.mRegion))!=regions2.end()) + if (!iter->second.mRegion.empty() + && std::find(regions2.begin(), regions2.end(), iter->second.mRegion) != regions2.end()) { - QModelIndex index = getIndex (iter->first); + QModelIndex index = getIndex(iter->first); - dataChanged (index, index); + dataChanged(index, index); } } } @@ -191,15 +215,15 @@ void CSMWorld::RegionMap::updateSize() if (int diff = size.first.getX() - mMin.getX()) { - beginInsertColumns (QModelIndex(), 0, std::abs (diff)-1); - mMin = CellCoordinates (size.first.getX(), mMin.getY()); + beginInsertColumns(QModelIndex(), 0, std::abs(diff) - 1); + mMin = CellCoordinates(size.first.getX(), mMin.getY()); endInsertColumns(); } if (int diff = size.first.getY() - mMin.getY()) { - beginInsertRows (QModelIndex(), 0, std::abs (diff)-1); - mMin = CellCoordinates (mMin.getX(), size.first.getY()); + beginInsertRows(QModelIndex(), 0, std::abs(diff) - 1); + mMin = CellCoordinates(mMin.getX(), size.first.getY()); endInsertRows(); } @@ -207,12 +231,12 @@ void CSMWorld::RegionMap::updateSize() { int columns = columnCount(); - if (diff>0) - beginInsertColumns (QModelIndex(), columns, columns+diff-1); + if (diff > 0) + beginInsertColumns(QModelIndex(), columns, columns + diff - 1); else - beginRemoveColumns (QModelIndex(), columns+diff, columns-1); + beginRemoveColumns(QModelIndex(), columns + diff, columns - 1); - mMax = CellCoordinates (size.second.getX(), mMax.getY()); + mMax = CellCoordinates(size.second.getX(), mMax.getY()); endInsertColumns(); } @@ -220,12 +244,12 @@ void CSMWorld::RegionMap::updateSize() { int rows = rowCount(); - if (diff>0) - beginInsertRows (QModelIndex(), rows, rows+diff-1); + if (diff > 0) + beginInsertRows(QModelIndex(), rows, rows + diff - 1); else - beginRemoveRows (QModelIndex(), rows+diff, rows-1); + beginRemoveRows(QModelIndex(), rows + diff, rows - 1); - mMax = CellCoordinates (mMax.getX(), size.second.getY()); + mMax = CellCoordinates(mMax.getX(), size.second.getY()); endInsertRows(); } } @@ -236,128 +260,119 @@ std::pair CSMWorld::Region int size = cells.getSize(); - CellCoordinates min (0, 0); - CellCoordinates max (0, 0); + CellCoordinates min(0, 0); + CellCoordinates max(0, 0); - for (int i=0; i& cell = cells.getRecord (i); + const Record& cell = cells.getRecord(i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) { - CellCoordinates index = getIndex (cell2); + CellCoordinates index = getIndex(cell2); - if (min==max) + if (min == max) { min = index; - max = min.move (1, 1); + max = min.move(1, 1); } else { - if (index.getX()=max.getX()) - max = CellCoordinates (index.getX()+1, max.getY()); - - if (index.getY()=max.getY()) - max = CellCoordinates (max.getX(), index.getY() + 1); + if (index.getX() < min.getX()) + min = CellCoordinates(index.getX(), min.getY()); + else if (index.getX() >= max.getX()) + max = CellCoordinates(index.getX() + 1, max.getY()); + + if (index.getY() < min.getY()) + min = CellCoordinates(min.getX(), index.getY()); + else if (index.getY() >= max.getY()) + max = CellCoordinates(max.getX(), index.getY() + 1); } } } - return std::make_pair (min, max); + return std::make_pair(min, max); } -CSMWorld::RegionMap::RegionMap (Data& data) : mData (data) +CSMWorld::RegionMap::RegionMap(Data& data) + : mData(data) { buildRegions(); buildMap(); - QAbstractItemModel *regions = data.getTableModel (UniversalId (UniversalId::Type_Regions)); + QAbstractItemModel* regions = data.getTableModel(UniversalId(UniversalId::Type_Regions)); - connect (regions, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (regionsAboutToBeRemoved (const QModelIndex&, int, int))); - connect (regions, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (regionsInserted (const QModelIndex&, int, int))); - connect (regions, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (regionsChanged (const QModelIndex&, const QModelIndex&))); + connect(regions, &QAbstractItemModel::rowsAboutToBeRemoved, this, &RegionMap::regionsAboutToBeRemoved); + connect(regions, &QAbstractItemModel::rowsInserted, this, &RegionMap::regionsInserted); + connect(regions, &QAbstractItemModel::dataChanged, this, &RegionMap::regionsChanged); - QAbstractItemModel *cells = data.getTableModel (UniversalId (UniversalId::Type_Cells)); + QAbstractItemModel* cells = data.getTableModel(UniversalId(UniversalId::Type_Cells)); - connect (cells, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (cellsAboutToBeRemoved (const QModelIndex&, int, int))); - connect (cells, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (cellsInserted (const QModelIndex&, int, int))); - connect (cells, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (cellsChanged (const QModelIndex&, const QModelIndex&))); + connect(cells, &QAbstractItemModel::rowsAboutToBeRemoved, this, &RegionMap::cellsAboutToBeRemoved); + connect(cells, &QAbstractItemModel::rowsInserted, this, &RegionMap::cellsInserted); + connect(cells, &QAbstractItemModel::dataChanged, this, &RegionMap::cellsChanged); } -int CSMWorld::RegionMap::rowCount (const QModelIndex& parent) const +int CSMWorld::RegionMap::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; - return mMax.getY()-mMin.getY(); + return mMax.getY() - mMin.getY(); } -int CSMWorld::RegionMap::columnCount (const QModelIndex& parent) const +int CSMWorld::RegionMap::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; - return mMax.getX()-mMin.getX(); + return mMax.getX() - mMin.getX(); } -QVariant CSMWorld::RegionMap::data (const QModelIndex& index, int role) const +QVariant CSMWorld::RegionMap::data(const QModelIndex& index, int role) const { - if (role==Qt::SizeHintRole) - return QSize (16, 16); + if (role == Qt::SizeHintRole) + return QSize(16, 16); - if (role==Qt::BackgroundRole) + if (role == Qt::BackgroundRole) { /// \todo GUI class in non-GUI code. Needs to be addressed eventually. - std::map::const_iterator cell = - mMap.find (getIndex (index)); + std::map::const_iterator cell = mMap.find(getIndex(index)); - if (cell!=mMap.end()) + if (cell != mMap.end()) { if (cell->second.mDeleted) - return QBrush (Qt::red, Qt::DiagCrossPattern); + return QBrush(Qt::red, Qt::DiagCrossPattern); - std::map::const_iterator iter = - mColours.find (Misc::StringUtils::lowerCase (cell->second.mRegion)); + auto iter = mColours.find(cell->second.mRegion); - if (iter!=mColours.end()) - return QBrush (QColor (iter->second & 0xff, - (iter->second >> 8) & 0xff, - (iter->second >> 16) & 0xff)); + if (iter != mColours.end()) + return QBrush(QColor(iter->second & 0xff, (iter->second >> 8) & 0xff, (iter->second >> 16) & 0xff), + cell->second.mMaxLandHeight > 0 ? Qt::SolidPattern : Qt::CrossPattern); - if (cell->second.mRegion.empty()) - return QBrush (Qt::Dense6Pattern); // no region + if (cell->second.mRegion.empty()) // no region + return QBrush(cell->second.mMaxLandHeight > 0 ? Qt::Dense3Pattern : Qt::Dense6Pattern); - return QBrush (Qt::red, Qt::Dense6Pattern); // invalid region + return QBrush(Qt::red, Qt::Dense6Pattern); // invalid region } - return QBrush (Qt::DiagCrossPattern); + return QBrush(Qt::DiagCrossPattern); } - if (role==Qt::ToolTipRole) + if (role == Qt::ToolTipRole) { - CellCoordinates cellIndex = getIndex (index); + CellCoordinates cellIndex = getIndex(index); std::ostringstream stream; stream << cellIndex; - std::map::const_iterator cell = - mMap.find (cellIndex); + std::map::const_iterator cell = mMap.find(cellIndex); - if (cell!=mMap.end()) + if (cell != mMap.end()) { if (!cell->second.mName.empty()) stream << " " << cell->second.mName; @@ -369,10 +384,9 @@ QVariant CSMWorld::RegionMap::data (const QModelIndex& index, int role) const { stream << "
"; - std::map::const_iterator iter = - mColours.find (Misc::StringUtils::lowerCase (cell->second.mRegion)); + auto iter = mColours.find(cell->second.mRegion); - if (iter!=mColours.end()) + if (iter != mColours.end()) stream << cell->second.mRegion; else stream << "" << cell->second.mRegion << ""; @@ -381,125 +395,124 @@ QVariant CSMWorld::RegionMap::data (const QModelIndex& index, int role) const else stream << " (no cell)"; - return QString::fromUtf8 (stream.str().c_str()); + return QString::fromUtf8(stream.str().c_str()); } - if (role==Role_Region) + if (role == Role_Region) { - CellCoordinates cellIndex = getIndex (index); + CellCoordinates cellIndex = getIndex(index); - std::map::const_iterator cell = - mMap.find (cellIndex); + std::map::const_iterator cell = mMap.find(cellIndex); - if (cell!=mMap.end() && !cell->second.mRegion.empty()) - return QString::fromUtf8 (Misc::StringUtils::lowerCase (cell->second.mRegion).c_str()); + if (cell != mMap.end() && !cell->second.mRegion.empty()) + return QString::fromUtf8(cell->second.mRegion.getRefIdString().c_str()); } - if (role==Role_CellId) + if (role == Role_CellId) { - CellCoordinates cellIndex = getIndex (index); + CellCoordinates cellIndex = getIndex(index); std::ostringstream stream; stream << "#" << cellIndex.getX() << " " << cellIndex.getY(); - return QString::fromUtf8 (stream.str().c_str()); + return QString::fromUtf8(stream.str().c_str()); } return QVariant(); } -Qt::ItemFlags CSMWorld::RegionMap::flags (const QModelIndex& index) const +Qt::ItemFlags CSMWorld::RegionMap::flags(const QModelIndex& index) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } -void CSMWorld::RegionMap::regionsAboutToBeRemoved (const QModelIndex& parent, int start, int end) +void CSMWorld::RegionMap::regionsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { - std::vector update; + std::vector update; const IdCollection& regions = mData.getRegions(); - for (int i=start; i<=end; ++i) + for (int i = start; i <= end; ++i) { - const Record& region = regions.getRecord (i); + const Record& region = regions.getRecord(i); - update.push_back (region.get().mId); + update.push_back(region.get().mId); - removeRegion (region.get().mId); + removeRegion(region.get().mId); } - updateRegions (update); + updateRegions(update); } -void CSMWorld::RegionMap::regionsInserted (const QModelIndex& parent, int start, int end) +void CSMWorld::RegionMap::regionsInserted(const QModelIndex& parent, int start, int end) { - std::vector update; + std::vector update; const IdCollection& regions = mData.getRegions(); - for (int i=start; i<=end; ++i) + for (int i = start; i <= end; ++i) { - const Record& region = regions.getRecord (i); + const Record& region = regions.getRecord(i); if (!region.isDeleted()) { - update.push_back (region.get().mId); + update.push_back(region.get().mId); - addRegion (region.get().mId, region.get().mMapColor); + addRegion(region.get().mId, region.get().mMapColor); } } - updateRegions (update); + updateRegions(update); } -void CSMWorld::RegionMap::regionsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +void CSMWorld::RegionMap::regionsChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { // Note: At this point an additional check could be inserted to see if there is any change to the // columns we are interested in. If not we can exit the function here and avoid all updating. - std::vector update; + std::vector update; const IdCollection& regions = mData.getRegions(); - for (int i=topLeft.row(); i<=bottomRight.column(); ++i) + for (int i = topLeft.row(); i <= bottomRight.column(); ++i) { - const Record& region = regions.getRecord (i); + const Record& region = regions.getRecord(i); - update.push_back (region.get().mId); + update.push_back(region.get().mId); if (!region.isDeleted()) - addRegion (region.get().mId, region.get().mMapColor); + addRegion(region.get().mId, region.get().mMapColor); else - removeRegion (region.get().mId); + removeRegion(region.get().mId); } - updateRegions (update); + updateRegions(update); } -void CSMWorld::RegionMap::cellsAboutToBeRemoved (const QModelIndex& parent, int start, int end) +void CSMWorld::RegionMap::cellsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { const IdCollection& cells = mData.getCells(); - for (int i=start; i<=end; ++i) + for (int i = start; i <= end; ++i) { - const Record& cell = cells.getRecord (i); + const Record& cell = cells.getRecord(i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) - removeCell (getIndex (cell2)); + removeCell(getIndex(cell2)); } } -void CSMWorld::RegionMap::cellsInserted (const QModelIndex& parent, int start, int end) +void CSMWorld::RegionMap::cellsInserted(const QModelIndex& parent, int start, int end) { - addCells (start, end); + addCells(start, end); } -void CSMWorld::RegionMap::cellsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +void CSMWorld::RegionMap::cellsChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { // Note: At this point an additional check could be inserted to see if there is any change to the // columns we are interested in. If not we can exit the function here and avoid all updating. - addCells (topLeft.row(), bottomRight.row()); + addCells(topLeft.row(), bottomRight.row()); } diff --git a/apps/opencs/model/world/regionmap.hpp b/apps/opencs/model/world/regionmap.hpp index 1de7f1cdb9a..96281ba49c9 100644 --- a/apps/opencs/model/world/regionmap.hpp +++ b/apps/opencs/model/world/regionmap.hpp @@ -3,117 +3,121 @@ #include #include +#include #include #include +#include +#include -#include "record.hpp" -#include "cell.hpp" #include "cellcoordinates.hpp" +#include + +class QObject; namespace CSMWorld { class Data; + struct Cell; + + template + struct Record; /// \brief Model for the region map /// /// This class does not holds any record data (other than for the purpose of buffering). class RegionMap : public QAbstractTableModel { - Q_OBJECT - - public: - - enum Role - { - Role_Region = Qt::UserRole, - Role_CellId = Qt::UserRole+1 - }; - - private: - - struct CellDescription - { - bool mDeleted; - std::string mRegion; - std::string mName; + Q_OBJECT - CellDescription(); + public: + enum Role + { + Role_Region = Qt::UserRole, + Role_CellId = Qt::UserRole + 1 + }; - CellDescription (const Record& cell); - }; + private: + struct CellDescription + { + float mMaxLandHeight; + bool mDeleted; + ESM::RefId mRegion; + std::string mName; - Data& mData; - std::map mMap; - CellCoordinates mMin; ///< inclusive - CellCoordinates mMax; ///< exclusive - std::map mColours; ///< region ID, colour (RGBA) + CellDescription(const Record& cell, float landHeight); + }; - CellCoordinates getIndex (const QModelIndex& index) const; - ///< Translates a Qt model index into a cell index (which can contain negative components) + Data& mData; + std::map mMap; + CellCoordinates mMin; ///< inclusive + CellCoordinates mMax; ///< exclusive + std::map mColours; ///< region ID, colour (RGBA) - QModelIndex getIndex (const CellCoordinates& index) const; + CellCoordinates getIndex(const QModelIndex& index) const; + ///< Translates a Qt model index into a cell index (which can contain negative components) - CellCoordinates getIndex (const Cell& cell) const; + QModelIndex getIndex(const CellCoordinates& index) const; - void buildRegions(); + CellCoordinates getIndex(const Cell& cell) const; - void buildMap(); + void buildRegions(); - void addCell (const CellCoordinates& index, const CellDescription& description); - ///< May be called on a cell that is already in the map (in which case an update is - // performed) + void buildMap(); - void addCells (int start, int end); + void addCell(const CellCoordinates& index, const CellDescription& description); + ///< May be called on a cell that is already in the map (in which case an update is + // performed) - void removeCell (const CellCoordinates& index); - ///< May be called on a cell that is not in the map (in which case the call is ignored) + void addCells(int start, int end); - void addRegion (const std::string& region, unsigned int colour); - ///< May be called on a region that is already listed (in which case an update is - /// performed) - /// - /// \note This function does not update the region map. + void removeCell(const CellCoordinates& index); + ///< May be called on a cell that is not in the map (in which case the call is ignored) - void removeRegion (const std::string& region); - ///< May be called on a region that is not listed (in which case the call is ignored) - /// - /// \note This function does not update the region map. + void addRegion(const ESM::RefId& region, unsigned int colour); + ///< May be called on a region that is already listed (in which case an update is + /// performed) + /// + /// \note This function does not update the region map. - void updateRegions (const std::vector& regions); - ///< Update cells affected by the listed regions + void removeRegion(const ESM::RefId& region); + ///< May be called on a region that is not listed (in which case the call is ignored) + /// + /// \note This function does not update the region map. - void updateSize(); + void updateRegions(const std::vector& regions); + ///< Update cells affected by the listed regions - std::pair getSize() const; + void updateSize(); - public: + std::pair getSize() const; - RegionMap (Data& data); + public: + RegionMap(Data& data); - int rowCount (const QModelIndex& parent = QModelIndex()) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; - int columnCount (const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data (const QModelIndex& index, int role = Qt::DisplayRole) const override; - ///< \note Calling this function with role==Role_CellId may return the ID of a cell - /// that does not exist. + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + ///< \note Calling this function with role==Role_CellId may return the ID of a cell + /// that does not exist. - Qt::ItemFlags flags (const QModelIndex& index) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; - private slots: + private slots: - void regionsAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void regionsAboutToBeRemoved(const QModelIndex& parent, int start, int end); - void regionsInserted (const QModelIndex& parent, int start, int end); + void regionsInserted(const QModelIndex& parent, int start, int end); - void regionsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void regionsChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - void cellsAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void cellsAboutToBeRemoved(const QModelIndex& parent, int start, int end); - void cellsInserted (const QModelIndex& parent, int start, int end); + void cellsInserted(const QModelIndex& parent, int start, int end); - void cellsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void cellsChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); }; } diff --git a/apps/opencs/model/world/resources.cpp b/apps/opencs/model/world/resources.cpp index c3eb9762e70..7082575c644 100644 --- a/apps/opencs/model/world/resources.cpp +++ b/apps/opencs/model/world/resources.cpp @@ -1,60 +1,61 @@ #include "resources.hpp" +#include #include +#include #include -#include +#include +#include -#include +#include -#include +#include +#include +#include -CSMWorld::Resources::Resources (const VFS::Manager* vfs, const std::string& baseDirectory, UniversalId::Type type, - const char * const *extensions) -: mBaseDirectory (baseDirectory), mType (type) +CSMWorld::Resources::Resources( + const VFS::Manager* vfs, const std::string& baseDirectory, UniversalId::Type type, const char* const* extensions) + : mBaseDirectory(baseDirectory) + , mType(type) { recreate(vfs, extensions); } -void CSMWorld::Resources::recreate(const VFS::Manager* vfs, const char * const *extensions) +void CSMWorld::Resources::recreate(const VFS::Manager* vfs, const char* const* extensions) { mFiles.clear(); mIndex.clear(); size_t baseSize = mBaseDirectory.size(); - const std::map& index = vfs->getIndex(); - for (std::map::const_iterator it = index.begin(); it != index.end(); ++it) + for (const auto& filepath : vfs->getRecursiveDirectoryIterator()) { - std::string filepath = it->first; - if (filepath.size() (mFiles.size())-1)); + std::string file(view.substr(baseSize + 1)); + mFiles.push_back(file); + mIndex.emplace(std::move(file), static_cast(mFiles.size()) - 1); } } @@ -63,35 +64,35 @@ int CSMWorld::Resources::getSize() const return static_cast(mFiles.size()); } -std::string CSMWorld::Resources::getId (int index) const +std::string CSMWorld::Resources::getId(int index) const { - return mFiles.at (index); + return mFiles.at(index); } -int CSMWorld::Resources::getIndex (const std::string& id) const +int CSMWorld::Resources::getIndex(const std::string& id) const { - int index = searchId (id); + int index = searchId(id); - if (index==-1) + if (index == -1) { std::ostringstream stream; stream << "Invalid resource: " << mBaseDirectory << '/' << id; - throw std::runtime_error (stream.str()); + throw std::runtime_error(stream.str()); } return index; } -int CSMWorld::Resources::searchId (const std::string& id) const +int CSMWorld::Resources::searchId(std::string_view id) const { - std::string id2 = Misc::StringUtils::lowerCase (id); + std::string id2 = Misc::StringUtils::lowerCase(id); - std::replace (id2.begin(), id2.end(), '\\', '/'); + std::replace(id2.begin(), id2.end(), '\\', '/'); - std::map::const_iterator iter = mIndex.find (id2); + std::map::const_iterator iter = mIndex.find(id2); - if (iter==mIndex.end()) + if (iter == mIndex.end()) return -1; return iter->second; diff --git a/apps/opencs/model/world/resources.hpp b/apps/opencs/model/world/resources.hpp index c217b793d37..9a3152daa15 100644 --- a/apps/opencs/model/world/resources.hpp +++ b/apps/opencs/model/world/resources.hpp @@ -1,8 +1,9 @@ #ifndef CSM_WOLRD_RESOURCES_H #define CSM_WOLRD_RESOURCES_H -#include #include +#include +#include #include #include "universalid.hpp" @@ -16,28 +17,27 @@ namespace CSMWorld { class Resources { - std::map mIndex; - std::vector mFiles; - std::string mBaseDirectory; - UniversalId::Type mType; - - public: + std::map mIndex; + std::vector mFiles; + std::string mBaseDirectory; + UniversalId::Type mType; - /// \param type Type of resources in this table. - Resources (const VFS::Manager* vfs, const std::string& baseDirectory, UniversalId::Type type, - const char * const *extensions = nullptr); + public: + /// \param type Type of resources in this table. + Resources(const VFS::Manager* vfs, const std::string& baseDirectory, UniversalId::Type type, + const char* const* extensions = nullptr); - void recreate(const VFS::Manager* vfs, const char * const *extensions = nullptr); + void recreate(const VFS::Manager* vfs, const char* const* extensions = nullptr); - int getSize() const; + int getSize() const; - std::string getId (int index) const; + std::string getId(int index) const; - int getIndex (const std::string& id) const; + int getIndex(const std::string& id) const; - int searchId (const std::string& id) const; + int searchId(std::string_view id) const; - UniversalId::Type getType() const; + UniversalId::Type getType() const; }; } diff --git a/apps/opencs/model/world/resourcesmanager.cpp b/apps/opencs/model/world/resourcesmanager.cpp index 378ba7c6bbe..2b6ed2f8f3d 100644 --- a/apps/opencs/model/world/resourcesmanager.cpp +++ b/apps/opencs/model/world/resourcesmanager.cpp @@ -1,37 +1,44 @@ #include "resourcesmanager.hpp" +#include "resources.hpp" + +#include + #include +#include +#include CSMWorld::ResourcesManager::ResourcesManager() : mVFS(nullptr) { } -void CSMWorld::ResourcesManager::addResources (const Resources& resources) +CSMWorld::ResourcesManager::~ResourcesManager() = default; + +void CSMWorld::ResourcesManager::addResources(const Resources& resources) { - mResources.insert (std::make_pair (resources.getType(), resources)); - mResources.insert (std::make_pair (UniversalId::getParentType (resources.getType()), - resources)); + mResources.insert(std::make_pair(resources.getType(), resources)); + mResources.insert(std::make_pair(UniversalId::getParentType(resources.getType()), resources)); } -const char * const * CSMWorld::ResourcesManager::getMeshExtensions() +const char* const* CSMWorld::ResourcesManager::getMeshExtensions() { // maybe we could go over the osgDB::Registry to list all supported node formats - static const char * const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae", 0 }; + static const char* const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae", 0 }; return sMeshTypes; } -void CSMWorld::ResourcesManager::setVFS(const VFS::Manager *vfs) +void CSMWorld::ResourcesManager::setVFS(const VFS::Manager* vfs) { mVFS = vfs; mResources.clear(); - addResources (Resources (vfs, "meshes", UniversalId::Type_Mesh, getMeshExtensions())); - addResources (Resources (vfs, "icons", UniversalId::Type_Icon)); - addResources (Resources (vfs, "music", UniversalId::Type_Music)); - addResources (Resources (vfs, "sound", UniversalId::Type_SoundRes)); - addResources (Resources (vfs, "textures", UniversalId::Type_Texture)); - addResources (Resources (vfs, "video", UniversalId::Type_Video)); + addResources(Resources(vfs, "meshes", UniversalId::Type_Mesh, getMeshExtensions())); + addResources(Resources(vfs, "icons", UniversalId::Type_Icon)); + addResources(Resources(vfs, "music", UniversalId::Type_Music)); + addResources(Resources(vfs, "sound", UniversalId::Type_SoundRes)); + addResources(Resources(vfs, "textures", UniversalId::Type_Texture)); + addResources(Resources(vfs, "video", UniversalId::Type_Video)); } const VFS::Manager* CSMWorld::ResourcesManager::getVFS() const @@ -42,7 +49,7 @@ const VFS::Manager* CSMWorld::ResourcesManager::getVFS() const void CSMWorld::ResourcesManager::recreateResources() { std::map::iterator it = mResources.begin(); - for ( ; it != mResources.end(); ++it) + for (; it != mResources.end(); ++it) { if (it->first == UniversalId::Type_Mesh) it->second.recreate(mVFS, getMeshExtensions()); @@ -51,12 +58,12 @@ void CSMWorld::ResourcesManager::recreateResources() } } -const CSMWorld::Resources& CSMWorld::ResourcesManager::get (UniversalId::Type type) const +const CSMWorld::Resources& CSMWorld::ResourcesManager::get(UniversalId::Type type) const { - std::map::const_iterator iter = mResources.find (type); + std::map::const_iterator iter = mResources.find(type); - if (iter==mResources.end()) - throw std::logic_error ("Unknown resource type"); + if (iter == mResources.end()) + throw std::logic_error("Unknown resource type"); return iter->second; } diff --git a/apps/opencs/model/world/resourcesmanager.hpp b/apps/opencs/model/world/resourcesmanager.hpp index 0e838530063..0bc1c86410e 100644 --- a/apps/opencs/model/world/resourcesmanager.hpp +++ b/apps/opencs/model/world/resourcesmanager.hpp @@ -3,8 +3,9 @@ #include +#include + #include "universalid.hpp" -#include "resources.hpp" namespace VFS { @@ -15,26 +16,25 @@ namespace CSMWorld { class ResourcesManager { - std::map mResources; - const VFS::Manager* mVFS; - - private: - - void addResources (const Resources& resources); + std::map mResources; + const VFS::Manager* mVFS; - const char * const * getMeshExtensions(); + private: + void addResources(const Resources& resources); - public: + const char* const* getMeshExtensions(); - ResourcesManager(); + public: + ResourcesManager(); + ~ResourcesManager(); - const VFS::Manager* getVFS() const; + const VFS::Manager* getVFS() const; - void setVFS(const VFS::Manager* vfs); + void setVFS(const VFS::Manager* vfs); - void recreateResources(); + void recreateResources(); - const Resources& get (UniversalId::Type type) const; + const Resources& get(UniversalId::Type type) const; }; } diff --git a/apps/opencs/model/world/resourcetable.cpp b/apps/opencs/model/world/resourcetable.cpp index 0e7864e48ef..5c15ee90ef8 100644 --- a/apps/opencs/model/world/resourcetable.cpp +++ b/apps/opencs/model/world/resourcetable.cpp @@ -1,18 +1,21 @@ #include "resourcetable.hpp" #include +#include + +#include -#include "resources.hpp" #include "columnbase.hpp" +#include "resources.hpp" #include "universalid.hpp" -CSMWorld::ResourceTable::ResourceTable (const Resources *resources, unsigned int features) -: IdTableBase (features | Feature_Constant), mResources (resources) -{} - -CSMWorld::ResourceTable::~ResourceTable() {} +CSMWorld::ResourceTable::ResourceTable(const Resources* resources, unsigned int features) + : IdTableBase(features | Feature_Constant) + , mResources(resources) +{ +} -int CSMWorld::ResourceTable::rowCount (const QModelIndex & parent) const +int CSMWorld::ResourceTable::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; @@ -20,7 +23,7 @@ int CSMWorld::ResourceTable::rowCount (const QModelIndex & parent) const return mResources->getSize(); } -int CSMWorld::ResourceTable::columnCount (const QModelIndex & parent) const +int CSMWorld::ResourceTable::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; @@ -28,47 +31,46 @@ int CSMWorld::ResourceTable::columnCount (const QModelIndex & parent) const return 2; // ID, type } -QVariant CSMWorld::ResourceTable::data (const QModelIndex & index, int role) const +QVariant CSMWorld::ResourceTable::data(const QModelIndex& index, int role) const { - if (role!=Qt::DisplayRole) + if (role != Qt::DisplayRole) return QVariant(); - if (index.column()==0) - return QString::fromUtf8 (mResources->getId (index.row()).c_str()); + if (index.column() == 0) + return QString::fromUtf8(mResources->getId(index.row()).c_str()); - if (index.column()==1) - return static_cast (mResources->getType()); + if (index.column() == 1) + return static_cast(mResources->getType()); - throw std::logic_error ("Invalid column in resource table"); + throw std::logic_error("Invalid column in resource table"); } -QVariant CSMWorld::ResourceTable::headerData (int section, Qt::Orientation orientation, - int role ) const +QVariant CSMWorld::ResourceTable::headerData(int section, Qt::Orientation orientation, int role) const { - if (orientation==Qt::Vertical) + if (orientation == Qt::Vertical) return QVariant(); - if (role==ColumnBase::Role_Flags) - return section==0 ? ColumnBase::Flag_Table : 0; + if (role == ColumnBase::Role_Flags) + return section == 0 ? ColumnBase::Flag_Table : 0; switch (section) { case 0: - if (role==Qt::DisplayRole) - return Columns::getName (Columns::ColumnId_Id).c_str(); + if (role == Qt::DisplayRole) + return Columns::getName(Columns::ColumnId_Id).c_str(); - if (role==ColumnBase::Role_Display) + if (role == ColumnBase::Role_Display) return ColumnBase::Display_Id; break; case 1: - if (role==Qt::DisplayRole) - return Columns::getName (Columns::ColumnId_RecordType).c_str(); + if (role == Qt::DisplayRole) + return Columns::getName(Columns::ColumnId_RecordType).c_str(); - if (role==ColumnBase::Role_Display) + if (role == ColumnBase::Role_Display) return ColumnBase::Display_Integer; break; @@ -77,83 +79,83 @@ QVariant CSMWorld::ResourceTable::headerData (int section, Qt::Orientation orien return QVariant(); } -bool CSMWorld::ResourceTable::setData ( const QModelIndex &index, const QVariant &value, - int role) +bool CSMWorld::ResourceTable::setData(const QModelIndex& index, const QVariant& value, int role) { return false; } -Qt::ItemFlags CSMWorld::ResourceTable::flags (const QModelIndex & index) const +Qt::ItemFlags CSMWorld::ResourceTable::flags(const QModelIndex& index) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } -QModelIndex CSMWorld::ResourceTable::index (int row, int column, const QModelIndex& parent) - const +QModelIndex CSMWorld::ResourceTable::index(int row, int column, const QModelIndex& parent) const { if (parent.isValid()) return QModelIndex(); - if (row<0 || row>=mResources->getSize()) + if (row < 0 || row >= mResources->getSize()) return QModelIndex(); - if (column<0 || column>1) + if (column < 0 || column > 1) return QModelIndex(); - return createIndex (row, column); + return createIndex(row, column); } -QModelIndex CSMWorld::ResourceTable::parent (const QModelIndex& index) const +QModelIndex CSMWorld::ResourceTable::parent(const QModelIndex& index) const { return QModelIndex(); } -QModelIndex CSMWorld::ResourceTable::getModelIndex (const std::string& id, int column) const +QModelIndex CSMWorld::ResourceTable::getModelIndex(const std::string& id, int column) const { int row = mResources->searchId(id); if (row != -1) - return index (row, column); + return index(row, column); return QModelIndex(); } -int CSMWorld::ResourceTable::searchColumnIndex (Columns::ColumnId id) const +int CSMWorld::ResourceTable::searchColumnIndex(Columns::ColumnId id) const { - if (id==Columns::ColumnId_Id) + if (id == Columns::ColumnId_Id) return 0; - if (id==Columns::ColumnId_RecordType) + if (id == Columns::ColumnId_RecordType) return 1; return -1; } -int CSMWorld::ResourceTable::findColumnIndex (Columns::ColumnId id) const +int CSMWorld::ResourceTable::findColumnIndex(Columns::ColumnId id) const { - int index = searchColumnIndex (id); + int index = searchColumnIndex(id); - if (index==-1) - throw std::logic_error ("invalid column index"); + if (index == -1) + throw std::logic_error("invalid column index"); return index; } -std::pair CSMWorld::ResourceTable::view (int row) const +std::pair CSMWorld::ResourceTable::view(int row) const { - return std::make_pair (UniversalId::Type_None, ""); + return std::make_pair(UniversalId::Type_None, ""); } -bool CSMWorld::ResourceTable::isDeleted (const std::string& id) const +bool CSMWorld::ResourceTable::isDeleted(const std::string& id) const { return false; } -int CSMWorld::ResourceTable::getColumnId (int column) const +int CSMWorld::ResourceTable::getColumnId(int column) const { switch (column) { - case 0: return Columns::ColumnId_Id; - case 1: return Columns::ColumnId_RecordType; + case 0: + return Columns::ColumnId_Id; + case 1: + return Columns::ColumnId_RecordType; } return -1; diff --git a/apps/opencs/model/world/resourcetable.hpp b/apps/opencs/model/world/resourcetable.hpp index 8a3fab8a21d..be3ff79f8c6 100644 --- a/apps/opencs/model/world/resourcetable.hpp +++ b/apps/opencs/model/world/resourcetable.hpp @@ -3,60 +3,68 @@ #include "idtablebase.hpp" +#include +#include + +#include +#include + +#include + namespace CSMWorld { class Resources; + class UniversalId; class ResourceTable : public IdTableBase { - const Resources *mResources; - - public: + const Resources* mResources; - /// \note The feature Feature_Constant will be added implicitly. - ResourceTable (const Resources *resources, unsigned int features = 0); + public: + /// \note The feature Feature_Constant will be added implicitly. + ResourceTable(const Resources* resources, unsigned int features = 0); - virtual ~ResourceTable(); + ~ResourceTable() override = default; - int rowCount (const QModelIndex & parent = QModelIndex()) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; - int columnCount (const QModelIndex & parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QVariant headerData (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - bool setData ( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; - Qt::ItemFlags flags (const QModelIndex & index) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; - QModelIndex index (int row, int column, const QModelIndex& parent = QModelIndex()) const override; + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; - QModelIndex parent (const QModelIndex& index) const override; + QModelIndex parent(const QModelIndex& index) const override; - QModelIndex getModelIndex (const std::string& id, int column) const override; + QModelIndex getModelIndex(const std::string& id, int column) const override; - /// Return index of column with the given \a id. If no such column exists, -1 is - /// returned. - int searchColumnIndex (Columns::ColumnId id) const override; + /// Return index of column with the given \a id. If no such column exists, -1 is + /// returned. + int searchColumnIndex(Columns::ColumnId id) const override; - /// Return index of column with the given \a id. If no such column exists, an - /// exception is thrown. - int findColumnIndex (Columns::ColumnId id) const override; + /// Return index of column with the given \a id. If no such column exists, an + /// exception is thrown. + int findColumnIndex(Columns::ColumnId id) const override; - /// Return the UniversalId and the hint for viewing \a row. If viewing is not - /// supported by this table, return (UniversalId::Type_None, ""). - std::pair view (int row) const override; + /// Return the UniversalId and the hint for viewing \a row. If viewing is not + /// supported by this table, return (UniversalId::Type_None, ""). + std::pair view(int row) const override; - /// Is \a id flagged as deleted? - bool isDeleted (const std::string& id) const override; + /// Is \a id flagged as deleted? + bool isDeleted(const std::string& id) const override; - int getColumnId (int column) const override; + int getColumnId(int column) const override; - /// Signal Qt that the data is about to change. - void beginReset(); - /// Signal Qt that the data has been changed. - void endReset(); + /// Signal Qt that the data is about to change. + void beginReset(); + /// Signal Qt that the data has been changed. + void endReset(); }; } diff --git a/apps/opencs/model/world/scope.cpp b/apps/opencs/model/world/scope.cpp index b026c34c322..175074e49ac 100644 --- a/apps/opencs/model/world/scope.cpp +++ b/apps/opencs/model/world/scope.cpp @@ -1,24 +1,44 @@ #include "scope.hpp" -#include +#include -#include +#include +#include -CSMWorld::Scope CSMWorld::getScopeFromId (const std::string& id) +namespace CSMWorld { - // get root namespace - std::string namespace_; - - std::string::size_type i = id.find ("::"); - - if (i!=std::string::npos) - namespace_ = Misc::StringUtils::lowerCase (id.substr (0, i)); - - if (namespace_=="project") - return Scope_Project; - - if (namespace_=="session") - return Scope_Session; + namespace + { + struct GetScope + { + Scope operator()(ESM::StringRefId v) const + { + std::string_view namespace_; + + const std::string::size_type i = v.getValue().find("::"); + + if (i != std::string::npos) + namespace_ = std::string_view(v.getValue()).substr(0, i); + + if (Misc::StringUtils::ciEqual(namespace_, "project")) + return Scope_Project; + + if (Misc::StringUtils::ciEqual(namespace_, "session")) + return Scope_Session; + + return Scope_Content; + } + + template + Scope operator()(const T& /*v*/) const + { + return Scope_Content; + } + }; + } +} - return Scope_Content; +CSMWorld::Scope CSMWorld::getScopeFromId(ESM::RefId id) +{ + return visit(GetScope{}, id); } diff --git a/apps/opencs/model/world/scope.hpp b/apps/opencs/model/world/scope.hpp index 3983d91f503..c679a78de8e 100644 --- a/apps/opencs/model/world/scope.hpp +++ b/apps/opencs/model/world/scope.hpp @@ -1,7 +1,10 @@ #ifndef CSM_WOLRD_SCOPE_H #define CSM_WOLRD_SCOPE_H -#include +namespace ESM +{ + class RefId; +} namespace CSMWorld { @@ -17,7 +20,7 @@ namespace CSMWorld Scope_Session = 4 }; - Scope getScopeFromId (const std::string& id); + Scope getScopeFromId(ESM::RefId id); } #endif diff --git a/apps/opencs/model/world/scriptcontext.cpp b/apps/opencs/model/world/scriptcontext.cpp index 344ae322e99..ba70127ad3e 100644 --- a/apps/opencs/model/world/scriptcontext.cpp +++ b/apps/opencs/model/world/scriptcontext.cpp @@ -1,110 +1,120 @@ #include "scriptcontext.hpp" #include +#include +#include -#include +#include +#include +#include +#include -#include #include +#include #include +#include +#include +#include +#include #include "data.hpp" -CSMWorld::ScriptContext::ScriptContext (const Data& data) : mData (data), mIdsUpdated (false) {} +CSMWorld::ScriptContext::ScriptContext(const Data& data) + : mData(data) + , mIdsUpdated(false) +{ +} bool CSMWorld::ScriptContext::canDeclareLocals() const { return true; } -char CSMWorld::ScriptContext::getGlobalType (const std::string& name) const +char CSMWorld::ScriptContext::getGlobalType(const std::string& name) const { - int index = mData.getGlobals().searchId (name); + const int index = mData.getGlobals().searchId(ESM::RefId::stringRefId(name)); - if (index!=-1) + if (index != -1) { - switch (mData.getGlobals().getRecord (index).get().mValue.getType()) + switch (mData.getGlobals().getRecord(index).get().mValue.getType()) { - case ESM::VT_Short: return 's'; - case ESM::VT_Long: return 'l'; - case ESM::VT_Float: return 'f'; - - default: return ' '; + case ESM::VT_Short: + return 's'; + case ESM::VT_Long: + return 'l'; + case ESM::VT_Float: + return 'f'; + + default: + return ' '; } } return ' '; } -std::pair CSMWorld::ScriptContext::getMemberType (const std::string& name, - const std::string& id) const +std::pair CSMWorld::ScriptContext::getMemberType(const std::string& name, const ESM::RefId& id) const { - std::string id2 = Misc::StringUtils::lowerCase (id); + ESM::RefId id2 = id; - int index = mData.getScripts().searchId (id2); + int index = mData.getScripts().searchId(id2); bool reference = false; - if (index==-1) + if (index == -1) { // ID is not a script ID. Search for a matching referenceable instead. - index = mData.getReferenceables().searchId (id2); + index = mData.getReferenceables().searchId(id2); - if (index!=-1) + if (index != -1) { // Referenceable found. - int columnIndex = mData.getReferenceables().findColumnIndex (Columns::ColumnId_Script); + int columnIndex = mData.getReferenceables().findColumnIndex(Columns::ColumnId_Script); - id2 = Misc::StringUtils::lowerCase (mData.getReferenceables(). - getData (index, columnIndex).toString().toUtf8().constData()); + id2 = ESM::RefId::stringRefId( + mData.getReferenceables().getData(index, columnIndex).toString().toUtf8().constData()); if (!id2.empty()) { // Referenceable has a script -> use it. - index = mData.getScripts().searchId (id2); + index = mData.getScripts().searchId(id2); reference = true; } } } - if (index==-1) - return std::make_pair (' ', false); + if (index == -1) + return std::make_pair(' ', false); - std::map::iterator iter = mLocals.find (id2); + auto iter = mLocals.find(id2); - if (iter==mLocals.end()) + if (iter == mLocals.end()) { Compiler::Locals locals; Compiler::NullErrorHandler errorHandler; - std::istringstream stream (mData.getScripts().getRecord (index).get().mScriptText); - Compiler::QuickFileParser parser (errorHandler, *this, locals); - Compiler::Scanner scanner (errorHandler, stream, getExtensions()); - scanner.scan (parser); + std::istringstream stream(mData.getScripts().getRecord(index).get().mScriptText); + Compiler::QuickFileParser parser(errorHandler, *this, locals); + Compiler::Scanner scanner(errorHandler, stream, getExtensions()); + scanner.scan(parser); - iter = mLocals.insert (std::make_pair (id2, locals)).first; + iter = mLocals.emplace(id2, std::move(locals)).first; } - return std::make_pair (iter->second.getType (Misc::StringUtils::lowerCase (name)), reference); + return std::make_pair(iter->second.getType(Misc::StringUtils::lowerCase(name)), reference); } -bool CSMWorld::ScriptContext::isId (const std::string& name) const +bool CSMWorld::ScriptContext::isId(const ESM::RefId& name) const { if (!mIdsUpdated) { mIds = mData.getIds(); - std::for_each (mIds.begin(), mIds.end(), &Misc::StringUtils::lowerCaseInPlace); - std::sort (mIds.begin(), mIds.end()); + std::sort(mIds.begin(), mIds.end()); mIdsUpdated = true; } - return std::binary_search (mIds.begin(), mIds.end(), Misc::StringUtils::lowerCase (name)); -} - -bool CSMWorld::ScriptContext::isJournalId (const std::string& name) const -{ - return mData.getJournals().searchId (name)!=-1; + return std::binary_search(mIds.begin(), mIds.end(), name); } void CSMWorld::ScriptContext::invalidateIds() @@ -119,14 +129,13 @@ void CSMWorld::ScriptContext::clear() mLocals.clear(); } -bool CSMWorld::ScriptContext::clearLocals (const std::string& script) +bool CSMWorld::ScriptContext::clearLocals(const std::string& script) { - std::map::iterator iter = - mLocals.find (Misc::StringUtils::lowerCase (script)); + const auto iter = mLocals.find(script); - if (iter!=mLocals.end()) + if (iter != mLocals.end()) { - mLocals.erase (iter); + mLocals.erase(iter); mIdsUpdated = false; return true; } diff --git a/apps/opencs/model/world/scriptcontext.hpp b/apps/opencs/model/world/scriptcontext.hpp index 8e1a5e57b86..c2dbadfc6f7 100644 --- a/apps/opencs/model/world/scriptcontext.hpp +++ b/apps/opencs/model/world/scriptcontext.hpp @@ -1,12 +1,14 @@ #ifndef CSM_WORLD_SCRIPTCONTEXT_H #define CSM_WORLD_SCRIPTCONTEXT_H +#include #include +#include #include -#include #include #include +#include namespace CSMWorld { @@ -14,41 +16,36 @@ namespace CSMWorld class ScriptContext : public Compiler::Context { - const Data& mData; - mutable std::vector mIds; - mutable bool mIdsUpdated; - mutable std::map mLocals; - - public: - - ScriptContext (const Data& data); + const Data& mData; + mutable std::vector mIds; + mutable bool mIdsUpdated; + mutable std::map> mLocals; - bool canDeclareLocals() const override; - ///< Is the compiler allowed to declare local variables? + public: + ScriptContext(const Data& data); - char getGlobalType (const std::string& name) const override; - ///< 'l: long, 's': short, 'f': float, ' ': does not exist. + bool canDeclareLocals() const override; + ///< Is the compiler allowed to declare local variables? - std::pair getMemberType (const std::string& name, - const std::string& id) const override; - ///< Return type of member variable \a name in script \a id or in script of reference of - /// \a id - /// \return first: 'l: long, 's': short, 'f': float, ' ': does not exist. - /// second: true: script of reference + char getGlobalType(const std::string& name) const override; + ///< 'l: long, 's': short, 'f': float, ' ': does not exist. - bool isId (const std::string& name) const override; - ///< Does \a name match an ID, that can be referenced? + std::pair getMemberType(const std::string& name, const ESM::RefId& id) const override; + ///< Return type of member variable \a name in script \a id or in script of reference of + /// \a id + /// \return first: 'l: long, 's': short, 'f': float, ' ': does not exist. + /// second: true: script of reference - bool isJournalId (const std::string& name) const override; - ///< Does \a name match a journal ID? + bool isId(const ESM::RefId& name) const override; + ///< Does \a name match an ID, that can be referenced? - void invalidateIds(); + void invalidateIds(); - void clear(); - ///< Remove all cached data. + void clear(); + ///< Remove all cached data. - /// \return Were there any locals that needed clearing? - bool clearLocals (const std::string& script); + /// \return Were there any locals that needed clearing? + bool clearLocals(const std::string& script); }; } diff --git a/apps/opencs/model/world/subcellcollection.hpp b/apps/opencs/model/world/subcellcollection.hpp index a60929680b2..a025d407c7c 100644 --- a/apps/opencs/model/world/subcellcollection.hpp +++ b/apps/opencs/model/world/subcellcollection.hpp @@ -11,35 +11,31 @@ namespace ESM namespace CSMWorld { struct Cell; - template - class IdCollection; /// \brief Single type collection of top level records that are associated with cells - template > - class SubCellCollection : public NestedIdCollection + template + class SubCellCollection final : public NestedIdCollection { - const IdCollection& mCells; + const IdCollection& mCells; - void loadRecord (ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted) override; + void loadRecord(ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted, bool base) override; - public: - - SubCellCollection (const IdCollection& cells); + public: + SubCellCollection(const IdCollection& cells); }; - template - void SubCellCollection::loadRecord (ESXRecordT& record, - ESM::ESMReader& reader, - bool& isDeleted) + template + void SubCellCollection::loadRecord( + ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted, bool base) { - record.load (reader, isDeleted, mCells); + record.load(reader, isDeleted, mCells); } - template - SubCellCollection::SubCellCollection ( - const IdCollection& cells) - : mCells (cells) - {} + template + SubCellCollection::SubCellCollection(const IdCollection& cells) + : mCells(cells) + { + } } #endif diff --git a/apps/opencs/model/world/tablemimedata.cpp b/apps/opencs/model/world/tablemimedata.cpp index 1268a7389de..a40698bc27e 100644 --- a/apps/opencs/model/world/tablemimedata.cpp +++ b/apps/opencs/model/world/tablemimedata.cpp @@ -1,25 +1,32 @@ #include "tablemimedata.hpp" +#include +#include +#include #include #include +#include -#include "universalid.hpp" #include "columnbase.hpp" +#include "universalid.hpp" -CSMWorld::TableMimeData::TableMimeData (UniversalId id, const CSMDoc::Document& document) : -mDocument(document) +CSMWorld::TableMimeData::TableMimeData(UniversalId id, const CSMDoc::Document& document) + : mDocument(document) + , mTableOfDragStart(nullptr) { - mUniversalId.push_back (id); - mObjectsFormats << QString::fromUtf8 (("tabledata/" + id.getTypeName()).c_str()); + mUniversalId.push_back(id); + mObjectsFormats << QString::fromUtf8(("tabledata/" + id.getTypeName()).c_str()); } -CSMWorld::TableMimeData::TableMimeData (const std::vector< CSMWorld::UniversalId >& id, const CSMDoc::Document& document) : - mUniversalId (id), mDocument(document) +CSMWorld::TableMimeData::TableMimeData(const std::vector& id, const CSMDoc::Document& document) + : mUniversalId(id) + , mDocument(document) + , mTableOfDragStart(nullptr) { - for (std::vector::iterator it (mUniversalId.begin()); it != mUniversalId.end(); ++it) + for (std::vector::iterator it(mUniversalId.begin()); it != mUniversalId.end(); ++it) { - mObjectsFormats << QString::fromUtf8 (("tabledata/" + it->getTypeName()).c_str()); + mObjectsFormats << QString::fromUtf8(("tabledata/" + it->getTypeName()).c_str()); } } @@ -28,94 +35,73 @@ QStringList CSMWorld::TableMimeData::formats() const return mObjectsFormats; } -CSMWorld::TableMimeData::~TableMimeData() -{ -} - std::string CSMWorld::TableMimeData::getIcon() const { if (mUniversalId.empty()) { - qDebug()<<"TableMimeData object does not hold any records!"; //because throwing in the event loop tends to be problematic - throw std::runtime_error ("TableMimeData object does not hold any records!"); + qDebug() << "TableMimeData object does not hold any records!"; // because throwing in the event loop tends to be + // problematic + throw std::runtime_error("TableMimeData object does not hold any records!"); } std::string tmpIcon; bool firstIteration = true; - for (unsigned i = 0; i < mUniversalId.size(); ++i) + for (const auto& id : mUniversalId) { if (firstIteration) { firstIteration = false; - tmpIcon = mUniversalId[i].getIcon(); + tmpIcon = id.getIcon(); continue; } - if (tmpIcon != mUniversalId[i].getIcon()) + if (tmpIcon != id.getIcon()) { - return ":/multitype.png"; //icon stolen from gnome TODO: get new icon + return ":multitype"; } - tmpIcon = mUniversalId[i].getIcon(); + tmpIcon = id.getIcon(); } - return mUniversalId.begin()->getIcon(); //All objects are of the same type; + return mUniversalId.begin()->getIcon(); // All objects are of the same type; } -std::vector< CSMWorld::UniversalId > CSMWorld::TableMimeData::getData() const +std::vector CSMWorld::TableMimeData::getData() const { return mUniversalId; } bool CSMWorld::TableMimeData::isReferencable(CSMWorld::ColumnBase::Display type) const { -return ( type == CSMWorld::ColumnBase::Display_Activator - || type == CSMWorld::ColumnBase::Display_Potion - || type == CSMWorld::ColumnBase::Display_Apparatus - || type == CSMWorld::ColumnBase::Display_Armor - || type == CSMWorld::ColumnBase::Display_Book - || type == CSMWorld::ColumnBase::Display_Clothing - || type == CSMWorld::ColumnBase::Display_Container - || type == CSMWorld::ColumnBase::Display_Creature - || type == CSMWorld::ColumnBase::Display_Door - || type == CSMWorld::ColumnBase::Display_Ingredient - || type == CSMWorld::ColumnBase::Display_CreatureLevelledList - || type == CSMWorld::ColumnBase::Display_ItemLevelledList - || type == CSMWorld::ColumnBase::Display_Light - || type == CSMWorld::ColumnBase::Display_Lockpick - || type == CSMWorld::ColumnBase::Display_Miscellaneous - || type == CSMWorld::ColumnBase::Display_Npc - || type == CSMWorld::ColumnBase::Display_Probe - || type == CSMWorld::ColumnBase::Display_Repair - || type == CSMWorld::ColumnBase::Display_Static - || type == CSMWorld::ColumnBase::Display_Weapon); + return (type == CSMWorld::ColumnBase::Display_Activator || type == CSMWorld::ColumnBase::Display_Potion + || type == CSMWorld::ColumnBase::Display_Apparatus || type == CSMWorld::ColumnBase::Display_Armor + || type == CSMWorld::ColumnBase::Display_Book || type == CSMWorld::ColumnBase::Display_Clothing + || type == CSMWorld::ColumnBase::Display_Container || type == CSMWorld::ColumnBase::Display_Creature + || type == CSMWorld::ColumnBase::Display_Door || type == CSMWorld::ColumnBase::Display_Ingredient + || type == CSMWorld::ColumnBase::Display_CreatureLevelledList + || type == CSMWorld::ColumnBase::Display_ItemLevelledList || type == CSMWorld::ColumnBase::Display_Light + || type == CSMWorld::ColumnBase::Display_Lockpick || type == CSMWorld::ColumnBase::Display_Miscellaneous + || type == CSMWorld::ColumnBase::Display_Npc || type == CSMWorld::ColumnBase::Display_Probe + || type == CSMWorld::ColumnBase::Display_Repair || type == CSMWorld::ColumnBase::Display_Static + || type == CSMWorld::ColumnBase::Display_Weapon); } bool CSMWorld::TableMimeData::isReferencable(CSMWorld::UniversalId::Type type) { - return ( type == CSMWorld::UniversalId::Type_Activator - || type == CSMWorld::UniversalId::Type_Potion - || type == CSMWorld::UniversalId::Type_Apparatus - || type == CSMWorld::UniversalId::Type_Armor - || type == CSMWorld::UniversalId::Type_Book - || type == CSMWorld::UniversalId::Type_Clothing - || type == CSMWorld::UniversalId::Type_Container - || type == CSMWorld::UniversalId::Type_Creature - || type == CSMWorld::UniversalId::Type_Door - || type == CSMWorld::UniversalId::Type_Ingredient - || type == CSMWorld::UniversalId::Type_CreatureLevelledList - || type == CSMWorld::UniversalId::Type_ItemLevelledList - || type == CSMWorld::UniversalId::Type_Light - || type == CSMWorld::UniversalId::Type_Lockpick - || type == CSMWorld::UniversalId::Type_Miscellaneous - || type == CSMWorld::UniversalId::Type_Npc - || type == CSMWorld::UniversalId::Type_Probe - || type == CSMWorld::UniversalId::Type_Repair - || type == CSMWorld::UniversalId::Type_Static - || type == CSMWorld::UniversalId::Type_Weapon); + return (type == CSMWorld::UniversalId::Type_Activator || type == CSMWorld::UniversalId::Type_Potion + || type == CSMWorld::UniversalId::Type_Apparatus || type == CSMWorld::UniversalId::Type_Armor + || type == CSMWorld::UniversalId::Type_Book || type == CSMWorld::UniversalId::Type_Clothing + || type == CSMWorld::UniversalId::Type_Container || type == CSMWorld::UniversalId::Type_Creature + || type == CSMWorld::UniversalId::Type_Door || type == CSMWorld::UniversalId::Type_Ingredient + || type == CSMWorld::UniversalId::Type_CreatureLevelledList + || type == CSMWorld::UniversalId::Type_ItemLevelledList || type == CSMWorld::UniversalId::Type_Light + || type == CSMWorld::UniversalId::Type_Lockpick || type == CSMWorld::UniversalId::Type_Miscellaneous + || type == CSMWorld::UniversalId::Type_Npc || type == CSMWorld::UniversalId::Type_Probe + || type == CSMWorld::UniversalId::Type_Repair || type == CSMWorld::UniversalId::Type_Static + || type == CSMWorld::UniversalId::Type_Weapon); } -bool CSMWorld::TableMimeData::holdsType (CSMWorld::UniversalId::Type type) const +bool CSMWorld::TableMimeData::holdsType(CSMWorld::UniversalId::Type type) const { bool referencable = (type == CSMWorld::UniversalId::Type_Referenceable); for (std::vector::const_iterator it = mUniversalId.begin(); it != mUniversalId.end(); ++it) @@ -126,8 +112,10 @@ bool CSMWorld::TableMimeData::holdsType (CSMWorld::UniversalId::Type type) const { return true; } - } else { - if (it->getType() == type) + } + else + { + if (it->getType() == type) { return true; } @@ -137,7 +125,7 @@ bool CSMWorld::TableMimeData::holdsType (CSMWorld::UniversalId::Type type) const return false; } -bool CSMWorld::TableMimeData::holdsType (CSMWorld::ColumnBase::Display type) const +bool CSMWorld::TableMimeData::holdsType(CSMWorld::ColumnBase::Display type) const { bool referencable = (type == CSMWorld::ColumnBase::Display_Referenceable); for (std::vector::const_iterator it = mUniversalId.begin(); it != mUniversalId.end(); ++it) @@ -148,8 +136,10 @@ bool CSMWorld::TableMimeData::holdsType (CSMWorld::ColumnBase::Display type) con { return true; } - } else { - if (it->getType() == convertEnums (type)) + } + else + { + if (it->getType() == convertEnums(type)) { return true; } @@ -159,7 +149,7 @@ bool CSMWorld::TableMimeData::holdsType (CSMWorld::ColumnBase::Display type) con return false; } -CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching (CSMWorld::UniversalId::Type type) const +CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching(CSMWorld::UniversalId::Type type) const { bool referencable = (type == CSMWorld::UniversalId::Type_Referenceable); for (std::vector::const_iterator it = mUniversalId.begin(); it != mUniversalId.end(); ++it) @@ -170,7 +160,8 @@ CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching (CSMWorld::Univers { return *it; } - } else + } + else { if (it->getType() == type) { @@ -179,10 +170,10 @@ CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching (CSMWorld::Univers } } - throw std::runtime_error ("TableMimeData object does not hold object of the sought type"); + throw std::runtime_error("TableMimeData object does not hold object of the sought type"); } -CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching (CSMWorld::ColumnBase::Display type) const +CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching(CSMWorld::ColumnBase::Display type) const { bool referencable = (type == CSMWorld::ColumnBase::Display_Referenceable); for (std::vector::const_iterator it = mUniversalId.begin(); it != mUniversalId.end(); ++it) @@ -193,18 +184,20 @@ CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching (CSMWorld::ColumnB { return *it; } - } else { - if (it->getType() == convertEnums (type)) + } + else + { + if (it->getType() == convertEnums(type)) { return *it; } } } - throw std::runtime_error ("TableMimeData object does not hold object of the sought type"); + throw std::runtime_error("TableMimeData object does not hold object of the sought type"); } -bool CSMWorld::TableMimeData::fromDocument (const CSMDoc::Document& document) const +bool CSMWorld::TableMimeData::fromDocument(const CSMDoc::Document& document) const { return &document == &mDocument; } @@ -217,8 +210,7 @@ namespace CSMWorld::ColumnBase::Display mDisplayType; }; - const Mapping mapping[] = - { + const Mapping mapping[] = { { CSMWorld::UniversalId::Type_Race, CSMWorld::ColumnBase::Display_Race }, { CSMWorld::UniversalId::Type_Skill, CSMWorld::ColumnBase::Display_Skill }, { CSMWorld::UniversalId::Type_Class, CSMWorld::ColumnBase::Display_Class }, @@ -271,19 +263,19 @@ namespace }; } -CSMWorld::UniversalId::Type CSMWorld::TableMimeData::convertEnums (ColumnBase::Display type) +CSMWorld::UniversalId::Type CSMWorld::TableMimeData::convertEnums(ColumnBase::Display type) { - for (int i=0; mapping[i].mUniversalIdType!=CSMWorld::UniversalId::Type_None; ++i) - if (mapping[i].mDisplayType==type) + for (int i = 0; mapping[i].mUniversalIdType != CSMWorld::UniversalId::Type_None; ++i) + if (mapping[i].mDisplayType == type) return mapping[i].mUniversalIdType; return CSMWorld::UniversalId::Type_None; } -CSMWorld::ColumnBase::Display CSMWorld::TableMimeData::convertEnums (UniversalId::Type type) +CSMWorld::ColumnBase::Display CSMWorld::TableMimeData::convertEnums(UniversalId::Type type) { - for (int i=0; mapping[i].mUniversalIdType!=CSMWorld::UniversalId::Type_None; ++i) - if (mapping[i].mUniversalIdType==type) + for (int i = 0; mapping[i].mUniversalIdType != CSMWorld::UniversalId::Type_None; ++i) + if (mapping[i].mUniversalIdType == type) return mapping[i].mDisplayType; return CSMWorld::ColumnBase::Display_None; diff --git a/apps/opencs/model/world/tablemimedata.hpp b/apps/opencs/model/world/tablemimedata.hpp index 23452491229..bcee8c0f100 100644 --- a/apps/opencs/model/world/tablemimedata.hpp +++ b/apps/opencs/model/world/tablemimedata.hpp @@ -1,67 +1,85 @@ #ifndef TABLEMIMEDATA_H #define TABLEMIMEDATA_H +#include #include -#include +#include #include +#include -#include "universalid.hpp" #include "columnbase.hpp" +#include "universalid.hpp" namespace CSMDoc { class Document; } +namespace CSVWorld +{ + class DragRecordTable; +} + namespace CSMWorld { -/// \brief Subclass of QmimeData, augmented to contain and transport UniversalIds. -/// -/// This class provides way to construct mimedata object holding the universalid copy -/// Universalid is used in the majority of the tables to store type, id, argument types. -/// This way universalid grants a way to retrieve record from the concrete table. -/// Please note, that tablemimedata object can hold multiple universalIds in the vector. + /// \brief Subclass of QmimeData, augmented to contain and transport UniversalIds. + /// + /// This class provides way to construct mimedata object holding the universalid copy + /// Universalid is used in the majority of the tables to store type, id, argument types. + /// This way universalid grants a way to retrieve record from the concrete table. + /// Please note, that tablemimedata object can hold multiple universalIds in the vector. class TableMimeData : public QMimeData { - std::vector mUniversalId; - QStringList mObjectsFormats; - const CSMDoc::Document& mDocument; - public: - TableMimeData(UniversalId id, const CSMDoc::Document& document); + std::vector mUniversalId; + QStringList mObjectsFormats; + const CSMDoc::Document& mDocument; + const CSVWorld::DragRecordTable* mTableOfDragStart; + QModelIndex mIndexAtDragStart; + + public: + TableMimeData(UniversalId id, const CSMDoc::Document& document); + + TableMimeData(const std::vector& id, const CSMDoc::Document& document); + + ~TableMimeData() override = default; + + QStringList formats() const override; + + std::string getIcon() const; - TableMimeData(const std::vector& id, const CSMDoc::Document& document); + std::vector getData() const; - ~TableMimeData(); + bool holdsType(UniversalId::Type type) const; - QStringList formats() const override; + bool holdsType(CSMWorld::ColumnBase::Display type) const; - std::string getIcon() const; + bool fromDocument(const CSMDoc::Document& document) const; - std::vector getData() const; + UniversalId returnMatching(UniversalId::Type type) const; - bool holdsType(UniversalId::Type type) const; + const CSMDoc::Document* getDocumentPtr() const; - bool holdsType(CSMWorld::ColumnBase::Display type) const; + UniversalId returnMatching(CSMWorld::ColumnBase::Display type) const; - bool fromDocument(const CSMDoc::Document& document) const; + void setIndexAtDragStart(const QModelIndex& index) { mIndexAtDragStart = index; } - UniversalId returnMatching(UniversalId::Type type) const; + void setTableOfDragStart(const CSVWorld::DragRecordTable* const table) { mTableOfDragStart = table; } - const CSMDoc::Document* getDocumentPtr() const; + const QModelIndex getIndexAtDragStart() const { return mIndexAtDragStart; } - UniversalId returnMatching(CSMWorld::ColumnBase::Display type) const; + const CSVWorld::DragRecordTable* getTableOfDragStart() const { return mTableOfDragStart; } - static CSMWorld::UniversalId::Type convertEnums(CSMWorld::ColumnBase::Display type); + static CSMWorld::UniversalId::Type convertEnums(CSMWorld::ColumnBase::Display type); - static CSMWorld::ColumnBase::Display convertEnums(CSMWorld::UniversalId::Type type); + static CSMWorld::ColumnBase::Display convertEnums(CSMWorld::UniversalId::Type type); - static bool isReferencable(CSMWorld::UniversalId::Type type); - private: - bool isReferencable(CSMWorld::ColumnBase::Display type) const; + static bool isReferencable(CSMWorld::UniversalId::Type type); + private: + bool isReferencable(CSMWorld::ColumnBase::Display type) const; }; } #endif // TABLEMIMEDATA_H diff --git a/apps/opencs/model/world/universalid.cpp b/apps/opencs/model/world/universalid.cpp index 486f3770aa2..0ebccd6253c 100644 --- a/apps/opencs/model/world/universalid.cpp +++ b/apps/opencs/model/world/universalid.cpp @@ -1,239 +1,349 @@ #include "universalid.hpp" -#include -#include -#include +#include +#include #include +#include +#include +#include +#include +#include +#include namespace { struct TypeData { - CSMWorld::UniversalId::Class mClass; - CSMWorld::UniversalId::Type mType; - const char *mName; - const char *mIcon; + CSMWorld::UniversalId::Class mClass; + CSMWorld::UniversalId::Type mType; + std::string_view mName; + std::string_view mIcon; }; - static const TypeData sNoArg[] = - { - { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, "-", 0 }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Globals, "Global Variables", ":./global-variable.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Gmsts, "Game Settings", ":./gmst.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Skills, "Skills", ":./skill.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Classes, "Classes", ":./class.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Factions, "Factions", ":./faction.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Races, "Races", ":./race.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Sounds, "Sounds", ":./sound.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Scripts, "Scripts", ":./script.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Regions, "Regions", ":./region.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Birthsigns, "Birthsigns", ":./birthsign.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Spells, "Spells", ":./spell.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Topics, "Topics", ":./dialogue-topics.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Journals, "Journals", ":./journal-topics.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_TopicInfos, "Topic Infos", ":./dialogue-topic-infos.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_JournalInfos, "Journal Infos", ":./journal-topic-infos.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Cells, "Cells", ":./cell.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Enchantments, "Enchantments", ":./enchantment.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_BodyParts, "Body Parts", ":./body-part.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Referenceables, "Objects", ":./object.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_References, "Instances", ":./instance.png" }, - { CSMWorld::UniversalId::Class_NonRecord, CSMWorld::UniversalId::Type_RegionMap, "Region Map", ":./region-map.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Filters, "Filters", ":./filter.png" }, - { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Meshes, "Meshes", ":./resources-mesh" }, - { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Icons, "Icons", ":./resources-icon" }, - { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Musics, "Music Files", ":./resources-music" }, - { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_SoundsRes, "Sound Files", ":resources-sound" }, - { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Textures, "Textures", ":./resources-texture" }, - { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Videos, "Videos", ":./resources-video" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_DebugProfiles, "Debug Profiles", ":./debug-profile.png" }, - { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_RunLog, "Run Log", ":./run-log.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_SoundGens, "Sound Generators", ":./sound-generator.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MagicEffects, "Magic Effects", ":./magic-effect.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Lands, "Lands", ":./land-heightmap.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_LandTextures, "Land Textures", ":./land-texture.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Pathgrids, "Pathgrids", ":./pathgrid.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_StartScripts, "Start Scripts", ":./start-script.png" }, - { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MetaDatas, "Metadata", ":./metadata.png" }, - { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker + constexpr TypeData sNoArg[] = { + { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, "-", ":placeholder" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Globals, "Global Variables", + ":global-variable" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Gmsts, "Game Settings", ":gmst" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Skills, "Skills", ":skill" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Classes, "Classes", ":class" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Factions, "Factions", ":faction" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Races, "Races", ":race" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Sounds, "Sounds", ":sound" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Scripts, "Scripts", ":script" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Regions, "Regions", ":region" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Birthsigns, "Birthsigns", ":birthsign" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Spells, "Spells", ":spell" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Topics, "Topics", ":dialogue-topics" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Journals, "Journals", + ":journal-topics" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_TopicInfos, "Topic Infos", + ":dialogue-info" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_JournalInfos, "Journal Infos", + ":journal-topic-infos" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Cells, "Cells", ":cell" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Enchantments, "Enchantments", + ":enchantment" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_BodyParts, "Body Parts", ":body-part" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Referenceables, "Objects", ":object" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_References, "Instances", ":instance" }, + { CSMWorld::UniversalId::Class_NonRecord, CSMWorld::UniversalId::Type_RegionMap, "Region Map", ":region-map" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Filters, "Filters", ":filter" }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Meshes, "Meshes", ":resources-mesh" }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Icons, "Icons", ":resources-icon" }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Musics, "Music Files", + ":resources-music" }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_SoundsRes, "Sound Files", + ":resources-sound" }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Textures, "Textures", + ":resources-texture" }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Videos, "Videos", ":resources-video" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_DebugProfiles, "Debug Profiles", + ":debug-profile" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_SelectionGroup, "Selection Groups", "" }, + { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_RunLog, "Run Log", ":run-log" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_SoundGens, "Sound Generators", + ":sound-generator" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MagicEffects, "Magic Effects", + ":magic-effect" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Lands, "Lands", ":land-heightmap" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_LandTextures, "Land Textures", + ":land-texture" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Pathgrids, "Pathgrids", ":pathgrid" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_StartScripts, "Start Scripts", + ":start-script" }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MetaDatas, "Metadata", ":metadata" }, }; - static const TypeData sIdArg[] = - { - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Global, "Global Variable", ":./global-variable.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Gmst, "Game Setting", ":./gmst.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Skill, "Skill", ":./skill.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Class, "Class", ":./class.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Faction, "Faction", ":./faction.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Race, "Race", ":./race.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Sound, "Sound", ":./sound.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Script, "Script", ":./script.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Region, "Region", ":./region.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Birthsign, "Birthsign", ":./birthsign.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Spell, "Spell", ":./spell.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Topic, "Topic", ":./dialogue-topics.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Journal, "Journal", ":./journal-topics.png" }, - { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_TopicInfo, "TopicInfo", ":./dialogue-topic-infos.png" }, - { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_JournalInfo, "JournalInfo", ":./journal-topic-infos.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell, "Cell", ":./cell.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell_Missing, "Cell", ":./cell.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Referenceable, "Object", ":./object.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Activator, "Activator", ":./activator.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Potion, "Potion", ":./potion.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Apparatus, "Apparatus", ":./apparatus.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Armor, "Armor", ":./armor.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Book, "Book", ":./book.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Clothing, "Clothing", ":./clothing.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Container, "Container", ":./container.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Creature, "Creature", ":./creature.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Door, "Door", ":./door.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Ingredient, "Ingredient", ":./ingredient.png" }, + constexpr TypeData sIdArg[] = { + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Global, "Global Variable", + ":global-variable" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Gmst, "Game Setting", ":gmst" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Skill, "Skill", ":skill" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Class, "Class", ":class" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Faction, "Faction", ":faction" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Race, "Race", ":race" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Sound, "Sound", ":sound" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Script, "Script", ":script" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Region, "Region", ":region" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Birthsign, "Birthsign", ":birthsign" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Spell, "Spell", ":spell" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Topic, "Topic", ":dialogue-topics" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Journal, "Journal", ":journal-topics" }, + { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_TopicInfo, "TopicInfo", + ":dialogue-info" }, + { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_JournalInfo, "JournalInfo", + ":journal-topic-infos" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell, "Cell", ":cell" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell_Missing, "Cell", ":cell" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Referenceable, "Object", ":object" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Activator, "Activator", ":activator" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Potion, "Potion", ":potion" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Apparatus, "Apparatus", ":apparatus" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Armor, "Armor", ":armor" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Book, "Book", ":book" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Clothing, "Clothing", ":clothing" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Container, "Container", ":container" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Creature, "Creature", ":creature" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Door, "Door", ":door" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Ingredient, "Ingredient", ":ingredient" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_CreatureLevelledList, - "Creature Levelled List", ":./levelled-creature.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_ItemLevelledList, - "Item Levelled List", ":./levelled-item.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Light, "Light", ":./light.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Lockpick, "Lockpick", ":./lockpick.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Miscellaneous, - "Miscellaneous", ":./miscellaneous.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Npc, "NPC", ":./npc.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Probe, "Probe", ":./probe.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Repair, "Repair", ":./repair.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Static, "Static", ":./static.png" }, - { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Weapon, "Weapon", ":./weapon.png" }, - { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_Reference, "Instance", ":./instance.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Filter, "Filter", ":./filter.png" }, - { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Scene, "Scene", ":./scene.png" }, - { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Preview, "Preview", ":./record-preview.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Enchantment, "Enchantment", ":./enchantment.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_BodyPart, "Body Part", ":./body-part.png" }, - { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Mesh, "Mesh", ":./resources-mesh"}, - { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Icon, "Icon", ":./resources-icon"}, - { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Music, "Music", ":./resources-music" }, - { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_SoundRes, "Sound File", ":./resources-sound" }, - { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Texture, "Texture", ":./resources-texture" }, - { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Video, "Video", ":./resources-video" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_DebugProfile, "Debug Profile", ":./debug-profile.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_SoundGen, "Sound Generator", ":./sound-generator.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MagicEffect, "Magic Effect", ":./magic-effect.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Land, "Land", ":./land-heightmap.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_LandTexture, "Land Texture", ":./land-texture.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Pathgrid, "Pathgrid", ":./pathgrid.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_StartScript, "Start Script", ":./start-script.png" }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MetaData, "Metadata", ":./metadata.png" }, - - { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker + "Creature Levelled List", ":levelled-creature" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_ItemLevelledList, "Item Levelled List", + ":levelled-item" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Light, "Light", ":light" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Lockpick, "Lockpick", ":lockpick" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Miscellaneous, "Miscellaneous", + ":miscellaneous" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Npc, "NPC", ":npc" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Probe, "Probe", ":probe" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Repair, "Repair", ":repair" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Static, "Static", ":static" }, + { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Weapon, "Weapon", ":weapon" }, + { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_Reference, "Instance", ":instance" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Filter, "Filter", ":filter" }, + { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Scene, "Scene", ":scene" }, + { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Preview, "Preview", ":edit-preview" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Enchantment, "Enchantment", ":enchantment" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_BodyPart, "Body Part", ":body-part" }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Mesh, "Mesh", ":resources-mesh" }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Icon, "Icon", ":resources-icon" }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Music, "Music", ":resources-music" }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_SoundRes, "Sound File", + ":resources-sound" }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Texture, "Texture", ":resources-texture" }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Video, "Video", ":resources-video" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_DebugProfile, "Debug Profile", + ":debug-profile" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_SoundGen, "Sound Generator", + ":sound-generator" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MagicEffect, "Magic Effect", + ":magic-effect" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Land, "Land", ":land-heightmap" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_LandTexture, "Land Texture", + ":land-texture" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Pathgrid, "Pathgrid", ":pathgrid" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_StartScript, "Start Script", + ":start-script" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MetaData, "Metadata", ":metadata" }, + }; + + constexpr TypeData sIndexArg[] = { + { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_VerificationResults, + "Verification Results", ":menu-verify" }, + { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_LoadErrorLog, "Load Error Log", + ":error-log" }, + { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_Search, "Global Search", ":menu-search" }, }; - static const TypeData sIndexArg[] = + struct WriteToStream { - { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_VerificationResults, "Verification Results", ":./menu-verify.png" }, - { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_LoadErrorLog, "Load Error Log", ":./error-log.png" }, - { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_Search, "Global Search", ":./menu-search.png" }, - { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker + std::ostream& mStream; + + void operator()(std::monostate /*value*/) const {} + + template + void operator()(const T& value) const + { + mStream << ": " << value; + } + + void operator()(const ESM::RefId& value) const { mStream << ": " << value.toString(); } }; + + struct GetTypeData + { + std::span operator()(std::monostate /*value*/) const { return sNoArg; } + + std::span operator()(int /*value*/) const { return sIndexArg; } + + template + std::span operator()(const T& /*value*/) const + { + return sIdArg; + } + }; + + std::string toString(CSMWorld::UniversalId::ArgumentType value) + { + switch (value) + { + case CSMWorld::UniversalId::ArgumentType_None: + return "None"; + case CSMWorld::UniversalId::ArgumentType_Id: + return "Id"; + case CSMWorld::UniversalId::ArgumentType_Index: + return "Index"; + case CSMWorld::UniversalId::ArgumentType_RefId: + return "RefId"; + } + + return std::to_string(value); + } + + CSMWorld::UniversalId::Class getClassByType(CSMWorld::UniversalId::Type type) + { + if (const auto it + = std::find_if(std::begin(sIdArg), std::end(sIdArg), [&](const TypeData& v) { return v.mType == type; }); + it != std::end(sIdArg)) + return it->mClass; + if (const auto it = std::find_if( + std::begin(sIndexArg), std::end(sIndexArg), [&](const TypeData& v) { return v.mType == type; }); + it != std::end(sIndexArg)) + return it->mClass; + if (const auto it + = std::find_if(std::begin(sNoArg), std::end(sNoArg), [&](const TypeData& v) { return v.mType == type; }); + it != std::end(sNoArg)) + return it->mClass; + throw std::logic_error("invalid UniversalId type: " + std::to_string(type)); + } } -CSMWorld::UniversalId::UniversalId (const std::string& universalId) -: mIndex(0) +CSMWorld::UniversalId::UniversalId(const std::string& universalId) + : mValue(std::monostate{}) { - std::string::size_type index = universalId.find (':'); + std::string::size_type index = universalId.find(':'); - if (index!=std::string::npos) + if (index != std::string::npos) { - std::string type = universalId.substr (0, index); + std::string type = universalId.substr(0, index); - for (int i=0; sIdArg[i].mName; ++i) - if (type==sIdArg[i].mName) + for (const TypeData& value : sIdArg) + if (type == value.mName) { - mArgumentType = ArgumentType_Id; - mType = sIdArg[i].mType; - mClass = sIdArg[i].mClass; - mId = universalId.substr (index+2); + mType = value.mType; + mClass = value.mClass; + mValue = universalId.substr(index + 2); return; } - for (int i=0; sIndexArg[i].mName; ++i) - if (type==sIndexArg[i].mName) + for (const TypeData& value : sIndexArg) + if (type == value.mName) { - mArgumentType = ArgumentType_Index; - mType = sIndexArg[i].mType; - mClass = sIndexArg[i].mClass; + mType = value.mType; + mClass = value.mClass; - std::istringstream stream (universalId.substr (index+2)); + std::istringstream stream(universalId.substr(index + 2)); - if (stream >> mIndex) + int index = 0; + if (stream >> index) + { + mValue = index; return; + } break; } } else { - for (int i=0; sNoArg[i].mName; ++i) - if (universalId==sNoArg[i].mName) + for (const TypeData& value : sIndexArg) + if (universalId == value.mName) { - mArgumentType = ArgumentType_None; - mType = sNoArg[i].mType; - mClass = sNoArg[i].mClass; + mType = value.mType; + mClass = value.mClass; return; } } - throw std::runtime_error ("invalid UniversalId: " + universalId); + throw std::runtime_error("invalid UniversalId: " + universalId); } -CSMWorld::UniversalId::UniversalId (Type type) : mArgumentType (ArgumentType_None), mType (type), mIndex (0) +CSMWorld::UniversalId::UniversalId(Type type) + : mType(type) + , mValue(std::monostate{}) { - for (int i=0; sNoArg[i].mName; ++i) - if (type==sNoArg[i].mType) + for (const TypeData& value : sNoArg) + if (type == value.mType) { - mClass = sNoArg[i].mClass; + mClass = value.mClass; return; } - for (int i=0; sIdArg[i].mName; ++i) - if (type==sIdArg[i].mType) + for (const TypeData& value : sIdArg) + if (type == value.mType) { - mArgumentType = ArgumentType_Id; - mClass = sIdArg[i].mClass; + mValue = std::string(); + mClass = value.mClass; return; } - for (int i=0; sIndexArg[i].mName; ++i) - if (type==sIndexArg[i].mType) + for (const TypeData& value : sIndexArg) + if (type == value.mType) { - mArgumentType = ArgumentType_Index; - mClass = sIndexArg[i].mClass; + mValue = int{}; + mClass = value.mClass; return; } - throw std::logic_error ("invalid argument-less UniversalId type"); + throw std::logic_error("invalid argument-less UniversalId type"); } -CSMWorld::UniversalId::UniversalId (Type type, const std::string& id) -: mArgumentType (ArgumentType_Id), mType (type), mId (id), mIndex (0) +CSMWorld::UniversalId::UniversalId(Type type, const std::string& id) + : mType(type) + , mValue(id) { - for (int i=0; sIdArg[i].mName; ++i) - if (type==sIdArg[i].mType) + for (const TypeData& value : sIdArg) + if (type == value.mType) { - mClass = sIdArg[i].mClass; + mClass = value.mClass; return; } - throw std::logic_error ("invalid ID argument UniversalId type"); + throw std::logic_error("invalid ID argument UniversalId type: " + std::to_string(type)); } -CSMWorld::UniversalId::UniversalId (Type type, int index) -: mArgumentType (ArgumentType_Index), mType (type), mIndex (index) +CSMWorld::UniversalId::UniversalId(Type type, ESM::RefId id) + : mType(type) + , mValue(id) { - for (int i=0; sIndexArg[i].mName; ++i) - if (type==sIndexArg[i].mType) + for (const TypeData& value : sIdArg) + if (type == value.mType) { - mClass = sIndexArg[i].mClass; + mClass = value.mClass; return; } + throw std::logic_error("invalid RefId argument UniversalId type: " + std::to_string(type)); +} - throw std::logic_error ("invalid index argument UniversalId type"); +CSMWorld::UniversalId::UniversalId(Type type, const UniversalId& id) + : mClass(getClassByType(type)) + , mType(type) + , mValue(id.mValue) +{ +} + +CSMWorld::UniversalId::UniversalId(Type type, int index) + : mType(type) + , mValue(index) +{ + for (const TypeData& value : sIndexArg) + if (type == value.mType) + { + mClass = value.mClass; + return; + } + + throw std::logic_error("invalid index argument UniversalId type: " + std::to_string(type)); } CSMWorld::UniversalId::Class CSMWorld::UniversalId::getClass() const @@ -243,7 +353,7 @@ CSMWorld::UniversalId::Class CSMWorld::UniversalId::getClass() const CSMWorld::UniversalId::ArgumentType CSMWorld::UniversalId::getArgumentType() const { - return mArgumentType; + return static_cast(mValue.index()); } CSMWorld::UniversalId::Type CSMWorld::UniversalId::getType() const @@ -253,61 +363,41 @@ CSMWorld::UniversalId::Type CSMWorld::UniversalId::getType() const const std::string& CSMWorld::UniversalId::getId() const { - if (mArgumentType!=ArgumentType_Id) - throw std::logic_error ("invalid access to ID of non-ID UniversalId"); + if (const std::string* result = std::get_if(&mValue)) + return *result; + + if (const ESM::RefId* refId = std::get_if(&mValue)) + if (const ESM::StringRefId* result = refId->getIf()) + return result->getValue(); - return mId; + throw std::logic_error("invalid access to ID of " + ::toString(getArgumentType()) + " UniversalId"); } int CSMWorld::UniversalId::getIndex() const { - if (mArgumentType!=ArgumentType_Index) - throw std::logic_error ("invalid access to index of non-index UniversalId"); + if (const int* result = std::get_if(&mValue)) + return *result; - return mIndex; + throw std::logic_error("invalid access to index of " + ::toString(getArgumentType()) + " UniversalId"); } -bool CSMWorld::UniversalId::isEqual (const UniversalId& universalId) const +ESM::RefId CSMWorld::UniversalId::getRefId() const { - if (mClass!=universalId.mClass || mArgumentType!=universalId.mArgumentType || mType!=universalId.mType) - return false; - - switch (mArgumentType) - { - case ArgumentType_Id: return mId==universalId.mId; - case ArgumentType_Index: return mIndex==universalId.mIndex; + if (const ESM::RefId* result = std::get_if(&mValue)) + return *result; - default: return true; - } -} - -bool CSMWorld::UniversalId::isLess (const UniversalId& universalId) const -{ - if (mTypeuniversalId.mType) - return false; - - switch (mArgumentType) - { - case ArgumentType_Id: return mId typeData = std::visit(GetTypeData{}, mValue); - for (int i=0; typeData[i].mName; ++i) - if (typeData[i].mType==mType) - return typeData[i].mName; + for (const TypeData& value : typeData) + if (value.mType == mType) + return std::string(value.mName); - throw std::logic_error ("failed to retrieve UniversalId type name"); + throw std::logic_error("failed to retrieve UniversalId type name"); } std::string CSMWorld::UniversalId::toString() const @@ -316,73 +406,66 @@ std::string CSMWorld::UniversalId::toString() const stream << getTypeName(); - switch (mArgumentType) - { - case ArgumentType_None: break; - case ArgumentType_Id: stream << ": " << mId; break; - case ArgumentType_Index: stream << ": " << mIndex; break; - } + std::visit(WriteToStream{ stream }, mValue); return stream.str(); } std::string CSMWorld::UniversalId::getIcon() const { - const TypeData *typeData = mArgumentType==ArgumentType_None ? sNoArg : - (mArgumentType==ArgumentType_Id ? sIdArg : sIndexArg); + const std::span typeData = std::visit(GetTypeData{}, mValue); - for (int i=0; typeData[i].mName; ++i) - if (typeData[i].mType==mType) - return typeData[i].mIcon ? typeData[i].mIcon : ":placeholder"; + for (const TypeData& value : typeData) + if (value.mType == mType) + return std::string(value.mIcon); - throw std::logic_error ("failed to retrieve UniversalId type icon"); + throw std::logic_error("failed to retrieve UniversalId type icon"); } std::vector CSMWorld::UniversalId::listReferenceableTypes() { std::vector list; - for (int i=0; sIdArg[i].mName; ++i) - if (sIdArg[i].mClass==Class_RefRecord) - list.push_back (sIdArg[i].mType); + for (const TypeData& value : sIdArg) + if (value.mClass == Class_RefRecord) + list.push_back(value.mType); return list; } -std::vector CSMWorld::UniversalId::listTypes (int classes) +std::vector CSMWorld::UniversalId::listTypes(int classes) { std::vector list; - for (int i=0; sNoArg[i].mName; ++i) - if (sNoArg[i].mClass & classes) - list.push_back (sNoArg[i].mType); + for (const TypeData& value : sNoArg) + if (value.mClass & classes) + list.push_back(value.mType); - for (int i=0; sIdArg[i].mName; ++i) - if (sIdArg[i].mClass & classes) - list.push_back (sIdArg[i].mType); + for (const TypeData& value : sIdArg) + if (value.mClass & classes) + list.push_back(value.mType); - for (int i=0; sIndexArg[i].mName; ++i) - if (sIndexArg[i].mClass & classes) - list.push_back (sIndexArg[i].mType); + for (const TypeData& value : sIndexArg) + if (value.mClass & classes) + list.push_back(value.mType); return list; } -CSMWorld::UniversalId::Type CSMWorld::UniversalId::getParentType (Type type) +CSMWorld::UniversalId::Type CSMWorld::UniversalId::getParentType(Type type) { - for (int i=0; sIdArg[i].mType; ++i) - if (type==sIdArg[i].mType) + for (const TypeData& value : sIdArg) + if (type == value.mType) { - if (sIdArg[i].mClass==Class_RefRecord) + if (value.mClass == Class_RefRecord) return Type_Referenceables; - if (sIdArg[i].mClass==Class_SubRecord || sIdArg[i].mClass==Class_Record || - sIdArg[i].mClass==Class_Resource) + if (value.mClass == Class_SubRecord || value.mClass == Class_Record || value.mClass == Class_Resource) { - if (type==Type_Cell_Missing) + if (type == Type_Cell_Missing) return Type_Cells; - return static_cast (type-1); + return static_cast(type - 1); } break; @@ -391,22 +474,12 @@ CSMWorld::UniversalId::Type CSMWorld::UniversalId::getParentType (Type type) return Type_None; } -bool CSMWorld::operator== (const CSMWorld::UniversalId& left, const CSMWorld::UniversalId& right) -{ - return left.isEqual (right); -} - -bool CSMWorld::operator!= (const CSMWorld::UniversalId& left, const CSMWorld::UniversalId& right) -{ - return !left.isEqual (right); -} - -bool CSMWorld::operator< (const UniversalId& left, const UniversalId& right) +bool CSMWorld::operator==(const CSMWorld::UniversalId& left, const CSMWorld::UniversalId& right) { - return left.isLess (right); + return std::tie(left.mClass, left.mType, left.mValue) == std::tie(right.mClass, right.mType, right.mValue); } -std::ostream& CSMWorld::operator< (std::ostream& stream, const CSMWorld::UniversalId& universalId) +bool CSMWorld::operator<(const UniversalId& left, const UniversalId& right) { - return stream << universalId.toString(); + return std::tie(left.mClass, left.mType, left.mValue) < std::tie(right.mClass, right.mType, right.mValue); } diff --git a/apps/opencs/model/world/universalid.hpp b/apps/opencs/model/world/universalid.hpp index accd1b78da0..34ef480fa53 100644 --- a/apps/opencs/model/world/universalid.hpp +++ b/apps/opencs/model/world/universalid.hpp @@ -2,207 +2,210 @@ #define CSM_WOLRD_UNIVERSALID_H #include -#include +#include #include #include +#include namespace CSMWorld { class UniversalId { - public: - - enum Class - { - Class_None = 0, - Class_Record = 1, - Class_RefRecord = 2, // referenceable record - Class_SubRecord = 4, - Class_RecordList = 8, - Class_Collection = 16, // multiple types of records combined - Class_Transient = 32, // not part of the world data or the project data - Class_NonRecord = 64, // record like data that is not part of the world - Class_Resource = 128, ///< \attention Resource IDs are unique only within the - /// respective collection - Class_ResourceList = 256 - }; - - enum ArgumentType - { - ArgumentType_None, - ArgumentType_Id, - ArgumentType_Index - }; - - /// \note A record list type must always be immediately followed by the matching - /// record type, if this type is of class SubRecord or Record. - enum Type - { - Type_None = 0, - Type_Globals, - Type_Global, - Type_VerificationResults, - Type_Gmsts, - Type_Gmst, - Type_Skills, - Type_Skill, - Type_Classes, - Type_Class, - Type_Factions, - Type_Faction, - Type_Races, - Type_Race, - Type_Sounds, - Type_Sound, - Type_Scripts, - Type_Script, - Type_Regions, - Type_Region, - Type_Birthsigns, - Type_Birthsign, - Type_Spells, - Type_Spell, - Type_Cells, - Type_Cell, - Type_Cell_Missing, //For cells that does not exist yet. - Type_Referenceables, - Type_Referenceable, - Type_Activator, - Type_Potion, - Type_Apparatus, - Type_Armor, - Type_Book, - Type_Clothing, - Type_Container, - Type_Creature, - Type_Door, - Type_Ingredient, - Type_CreatureLevelledList, - Type_ItemLevelledList, - Type_Light, - Type_Lockpick, - Type_Miscellaneous, - Type_Npc, - Type_Probe, - Type_Repair, - Type_Static, - Type_Weapon, - Type_References, - Type_Reference, - Type_RegionMap, - Type_Filters, - Type_Filter, - Type_Topics, - Type_Topic, - Type_Journals, - Type_Journal, - Type_TopicInfos, - Type_TopicInfo, - Type_JournalInfos, - Type_JournalInfo, - Type_Scene, - Type_Preview, - Type_LoadErrorLog, - Type_Enchantments, - Type_Enchantment, - Type_BodyParts, - Type_BodyPart, - Type_Meshes, - Type_Mesh, - Type_Icons, - Type_Icon, - Type_Musics, - Type_Music, - Type_SoundsRes, - Type_SoundRes, - Type_Textures, - Type_Texture, - Type_Videos, - Type_Video, - Type_DebugProfiles, - Type_DebugProfile, - Type_SoundGens, - Type_SoundGen, - Type_MagicEffects, - Type_MagicEffect, - Type_Lands, - Type_Land, - Type_LandTextures, - Type_LandTexture, - Type_Pathgrids, - Type_Pathgrid, - Type_StartScripts, - Type_StartScript, - Type_Search, - Type_MetaDatas, - Type_MetaData, - Type_RunLog - }; - - enum { NumberOfTypes = Type_RunLog+1 }; - - private: - - Class mClass; - ArgumentType mArgumentType; - Type mType; - std::string mId; - int mIndex; - - public: - - UniversalId (const std::string& universalId); - - UniversalId (Type type = Type_None); - - UniversalId (Type type, const std::string& id); - ///< Using a type for a non-ID-argument UniversalId will throw an exception. - - UniversalId (Type type, int index); - ///< Using a type for a non-index-argument UniversalId will throw an exception. - - Class getClass() const; - - ArgumentType getArgumentType() const; - - Type getType() const; - - const std::string& getId() const; - ///< Calling this function for a non-ID type will throw an exception. - - int getIndex() const; - ///< Calling this function for a non-index type will throw an exception. - - bool isEqual (const UniversalId& universalId) const; - - bool isLess (const UniversalId& universalId) const; - - std::string getTypeName() const; - - std::string toString() const; - - std::string getIcon() const; - ///< Will return an empty string, if no icon is available. - - static std::vector listReferenceableTypes(); - - static std::vector listTypes (int classes); - - /// If \a type is a SubRecord, RefRecord or Record type return the type of the table - /// that contains records of type \a type. - /// Otherwise return Type_None. - static Type getParentType (Type type); + public: + enum Class + { + Class_None = 0, + Class_Record = 1, + Class_RefRecord = 2, // referenceable record + Class_SubRecord = 4, + Class_RecordList = 8, + Class_Collection = 16, // multiple types of records combined + Class_Transient = 32, // not part of the world data or the project data + Class_NonRecord = 64, // record like data that is not part of the world + Class_Resource = 128, ///< \attention Resource IDs are unique only within the + /// respective collection + Class_ResourceList = 256 + }; + + enum ArgumentType + { + ArgumentType_None, + ArgumentType_Id, + ArgumentType_Index, + ArgumentType_RefId, + }; + + /// \note A record list type must always be immediately followed by the matching + /// record type, if this type is of class SubRecord or Record. + enum Type + { + Type_None = 0, + Type_Globals, + Type_Global, + Type_VerificationResults, + Type_Gmsts, + Type_Gmst, + Type_Skills, + Type_Skill, + Type_Classes, + Type_Class, + Type_Factions, + Type_Faction, + Type_Races, + Type_Race, + Type_Sounds, + Type_Sound, + Type_Scripts, + Type_Script, + Type_Regions, + Type_Region, + Type_Birthsigns, + Type_Birthsign, + Type_Spells, + Type_Spell, + Type_Cells, + Type_Cell, + Type_Cell_Missing, // For cells that does not exist yet. + Type_Referenceables, + Type_Referenceable, + Type_Activator, + Type_Potion, + Type_Apparatus, + Type_Armor, + Type_Book, + Type_Clothing, + Type_Container, + Type_Creature, + Type_Door, + Type_Ingredient, + Type_CreatureLevelledList, + Type_ItemLevelledList, + Type_Light, + Type_Lockpick, + Type_Miscellaneous, + Type_Npc, + Type_Probe, + Type_Repair, + Type_Static, + Type_Weapon, + Type_References, + Type_Reference, + Type_RegionMap, + Type_Filters, + Type_Filter, + Type_Topics, + Type_Topic, + Type_Journals, + Type_Journal, + Type_TopicInfos, + Type_TopicInfo, + Type_JournalInfos, + Type_JournalInfo, + Type_Scene, + Type_Preview, + Type_LoadErrorLog, + Type_Enchantments, + Type_Enchantment, + Type_BodyParts, + Type_BodyPart, + Type_Meshes, + Type_Mesh, + Type_Icons, + Type_Icon, + Type_Musics, + Type_Music, + Type_SoundsRes, + Type_SoundRes, + Type_Textures, + Type_Texture, + Type_Videos, + Type_Video, + Type_DebugProfiles, + Type_DebugProfile, + Type_SoundGens, + Type_SoundGen, + Type_MagicEffects, + Type_MagicEffect, + Type_Lands, + Type_Land, + Type_LandTextures, + Type_LandTexture, + Type_Pathgrids, + Type_Pathgrid, + Type_SelectionGroup, + Type_StartScripts, + Type_StartScript, + Type_Search, + Type_MetaDatas, + Type_MetaData, + Type_RunLog + }; + + enum + { + NumberOfTypes = Type_RunLog + 1 + }; + + UniversalId(const std::string& universalId); + + UniversalId(Type type = Type_None); + + UniversalId(Type type, const std::string& id); + ///< Using a type for a non-ID-argument UniversalId will throw an exception. + + UniversalId(Type type, ESM::RefId id); + + UniversalId(Type type, int index); + ///< Using a type for a non-index-argument UniversalId will throw an exception. + + UniversalId(Type type, const UniversalId& id); + + Class getClass() const; + + ArgumentType getArgumentType() const; + + Type getType() const; + + const std::string& getId() const; + ///< Calling this function for a non-ID type will throw an exception. + + int getIndex() const; + ///< Calling this function for a non-index type will throw an exception. + + ESM::RefId getRefId() const; + + std::string getTypeName() const; + + std::string toString() const; + + std::string getIcon() const; + ///< Will return an empty string, if no icon is available. + + static std::vector listReferenceableTypes(); + + static std::vector listTypes(int classes); + + /// If \a type is a SubRecord, RefRecord or Record type return the type of the table + /// that contains records of type \a type. + /// Otherwise return Type_None. + static Type getParentType(Type type); + + private: + Class mClass; + Type mType; + std::variant mValue; + + friend bool operator==(const UniversalId& left, const UniversalId& right); + + friend bool operator<(const UniversalId& left, const UniversalId& right); }; - bool operator== (const UniversalId& left, const UniversalId& right); - bool operator!= (const UniversalId& left, const UniversalId& right); + bool operator==(const UniversalId& left, const UniversalId& right); - bool operator< (const UniversalId& left, const UniversalId& right); - - std::ostream& operator< (std::ostream& stream, const UniversalId& universalId); + bool operator<(const UniversalId& left, const UniversalId& right); } -Q_DECLARE_METATYPE (CSMWorld::UniversalId) +Q_DECLARE_METATYPE(CSMWorld::UniversalId) #endif diff --git a/files/ui/filedialog.ui b/apps/opencs/ui/filedialog.ui similarity index 100% rename from files/ui/filedialog.ui rename to apps/opencs/ui/filedialog.ui diff --git a/apps/opencs/view/doc/adjusterwidget.cpp b/apps/opencs/view/doc/adjusterwidget.cpp index 509e656c33a..a282ebcaff4 100644 --- a/apps/opencs/view/doc/adjusterwidget.cpp +++ b/apps/opencs/view/doc/adjusterwidget.cpp @@ -1,47 +1,52 @@ #include "adjusterwidget.hpp" -#include - -#include - #include #include #include -CSVDoc::AdjusterWidget::AdjusterWidget (QWidget *parent) - : QWidget (parent), mValid (false), mAction (ContentAction_Undefined) +#include +#include +#include + +#include +#include + +CSVDoc::AdjusterWidget::AdjusterWidget(QWidget* parent) + : QWidget(parent) + , mValid(false) + , mAction(ContentAction_Undefined) { - QHBoxLayout *layout = new QHBoxLayout (this); + QHBoxLayout* layout = new QHBoxLayout(this); - mIcon = new QLabel (this); + mIcon = new QLabel(this); - layout->addWidget (mIcon, 0); + layout->addWidget(mIcon, 0); - mMessage = new QLabel (this); - mMessage->setWordWrap (true); - mMessage->setSizePolicy (QSizePolicy (QSizePolicy::Minimum, QSizePolicy::Minimum)); + mMessage = new QLabel(this); + mMessage->setWordWrap(true); + mMessage->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); - layout->addWidget (mMessage, 1); + layout->addWidget(mMessage, 1); - setName ("", false); + setName("", false); - setLayout (layout); + setLayout(layout); } -void CSVDoc::AdjusterWidget::setAction (ContentAction action) +void CSVDoc::AdjusterWidget::setAction(ContentAction action) { mAction = action; } -void CSVDoc::AdjusterWidget::setLocalData (const boost::filesystem::path& localData) +void CSVDoc::AdjusterWidget::setLocalData(const std::filesystem::path& localData) { mLocalData = localData; } -boost::filesystem::path CSVDoc::AdjusterWidget::getPath() const +std::filesystem::path CSVDoc::AdjusterWidget::getPath() const { if (!mValid) - throw std::logic_error ("invalid content file path"); + throw std::logic_error("invalid content file path"); return mResultPath; } @@ -51,12 +56,12 @@ bool CSVDoc::AdjusterWidget::isValid() const return mValid; } -void CSVDoc::AdjusterWidget::setFilenameCheck (bool doCheck) +void CSVDoc::AdjusterWidget::setFilenameCheck(bool doCheck) { mDoFilenameCheck = doCheck; } -void CSVDoc::AdjusterWidget::setName (const QString& name, bool addon) +void CSVDoc::AdjusterWidget::setName(const QString& name, bool addon) { QString message; @@ -69,37 +74,36 @@ void CSVDoc::AdjusterWidget::setName (const QString& name, bool addon) } else { - boost::filesystem::path path (name.toUtf8().data()); + auto path = Files::pathFromQString(name); - std::string extension = Misc::StringUtils::lowerCase(path.extension().string()); + const auto extension = Misc::StringUtils::lowerCase(path.extension().u8string()); - bool isLegacyPath = (extension == ".esm" || - extension == ".esp"); + bool isLegacyPath = (extension == u8".esm" || extension == u8".esp"); - bool isFilePathChanged = (path.parent_path().string() != mLocalData.string()); + bool isFilePathChanged = (path.parent_path() != mLocalData); if (isLegacyPath) - path.replace_extension (addon ? ".omwaddon" : ".omwgame"); + path.replace_extension(addon ? ".omwaddon" : ".omwgame"); - //if the file came from data-local and is not a legacy file to be converted, - //don't worry about doing a file check. + // if the file came from data-local and is not a legacy file to be converted, + // don't worry about doing a file check. if (!isFilePathChanged && !isLegacyPath) { // path already points to the local data directory - message = QString::fromUtf8 (("Will be saved as: " + path.string()).c_str()); - mResultPath = path; + message = "Will be saved as: " + Files::pathToQString(path); + mResultPath = std::move(path); } - //in all other cases, ensure the path points to data-local and do an existing file check + // in all other cases, ensure the path points to data-local and do an existing file check else { // path points somewhere else or is a leaf name. if (isFilePathChanged) path = mLocalData / path.filename(); - message = QString::fromUtf8 (("Will be saved as: " + path.string()).c_str()); + message = "Will be saved as: " + Files::pathToQString(path); mResultPath = path; - if (boost::filesystem::exists (path)) + if (std::filesystem::exists(path)) { /// \todo add an user setting to make this an error. message += "

A file with the same name already exists. If you continue, it will be overwritten."; @@ -108,10 +112,12 @@ void CSVDoc::AdjusterWidget::setName (const QString& name, bool addon) } } - mMessage->setText (message); - mIcon->setPixmap (style()->standardIcon ( - mValid ? (warning ? QStyle::SP_MessageBoxWarning : QStyle::SP_MessageBoxInformation) : QStyle::SP_MessageBoxCritical). - pixmap (QSize (16, 16))); + mMessage->setText(message); + mIcon->setPixmap( + style() + ->standardIcon(mValid ? (warning ? QStyle::SP_MessageBoxWarning : QStyle::SP_MessageBoxInformation) + : QStyle::SP_MessageBoxCritical) + .pixmap(QSize(16, 16))); - emit stateChanged (mValid); + emit stateChanged(mValid); } diff --git a/apps/opencs/view/doc/adjusterwidget.hpp b/apps/opencs/view/doc/adjusterwidget.hpp index cec9ca22916..53192d8414a 100644 --- a/apps/opencs/view/doc/adjusterwidget.hpp +++ b/apps/opencs/view/doc/adjusterwidget.hpp @@ -1,10 +1,10 @@ #ifndef CSV_DOC_ADJUSTERWIDGET_H #define CSV_DOC_ADJUSTERWIDGET_H -#include - #include +#include + class QLabel; namespace CSVDoc @@ -18,38 +18,36 @@ namespace CSVDoc class AdjusterWidget : public QWidget { - Q_OBJECT - - public: - - boost::filesystem::path mLocalData; - QLabel *mMessage; - QLabel *mIcon; - bool mValid; - boost::filesystem::path mResultPath; - ContentAction mAction; - bool mDoFilenameCheck; + Q_OBJECT - public: + public: + std::filesystem::path mLocalData; + QLabel* mMessage; + QLabel* mIcon; + bool mValid; + std::filesystem::path mResultPath; + ContentAction mAction; + bool mDoFilenameCheck; - AdjusterWidget (QWidget *parent = nullptr); + public: + AdjusterWidget(QWidget* parent = nullptr); - void setLocalData (const boost::filesystem::path& localData); - void setAction (ContentAction action); + void setLocalData(const std::filesystem::path& localData); + void setAction(ContentAction action); - void setFilenameCheck (bool doCheck); - bool isValid() const; + void setFilenameCheck(bool doCheck); + bool isValid() const; - boost::filesystem::path getPath() const; - ///< This function must not be called if there is no valid path. + std::filesystem::path getPath() const; + ///< This function must not be called if there is no valid path. - public slots: + public slots: - void setName (const QString& name, bool addon); + void setName(const QString& name, bool addon); - signals: + signals: - void stateChanged (bool valid); + void stateChanged(bool valid); }; } diff --git a/apps/opencs/view/doc/filedialog.cpp b/apps/opencs/view/doc/filedialog.cpp index 69490edca25..1b502c9d6ce 100644 --- a/apps/opencs/view/doc/filedialog.cpp +++ b/apps/opencs/view/doc/filedialog.cpp @@ -1,39 +1,45 @@ #include "filedialog.hpp" -#include -#include #include -#include -#include -#include -#include #include -#include -#include -#include "components/contentselector/model/esmfile.hpp" -#include "components/contentselector/view/contentselector.hpp" +#include +#include +#include +#include + +#include -#include "filewidget.hpp" #include "adjusterwidget.hpp" +#include "filewidget.hpp" -CSVDoc::FileDialog::FileDialog(QWidget *parent) : - QDialog(parent), mSelector (nullptr), mAction(ContentAction_Undefined), mFileWidget (nullptr), mAdjusterWidget (nullptr), mDialogBuilt(false) +CSVDoc::FileDialog::FileDialog(QWidget* parent) + : QDialog(parent) + , mSelector(nullptr) + , mAction(ContentAction_Undefined) + , mFileWidget(nullptr) + , mAdjusterWidget(nullptr) + , mDialogBuilt(false) { - ui.setupUi (this); + ui.setupUi(this); resize(400, 400); - setObjectName ("FileDialog"); - mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); - mAdjusterWidget = new AdjusterWidget (this); + setObjectName("FileDialog"); + mSelector = new ContentSelectorView::ContentSelector(ui.contentSelectorWidget, /*showOMWScripts=*/false); + mAdjusterWidget = new AdjusterWidget(this); } -void CSVDoc::FileDialog::addFiles(const QString &path) +void CSVDoc::FileDialog::addFiles(const std::vector& dataDirs) { - mSelector->addFiles(path); + for (auto iter = dataDirs.rbegin(); iter != dataDirs.rend(); ++iter) + { + QString path = Files::pathToQString(*iter); + mSelector->addFiles(path); + } + mSelector->sortFiles(); } -void CSVDoc::FileDialog::setEncoding(const QString &encoding) +void CSVDoc::FileDialog::setEncoding(const QString& encoding) { mSelector->setEncoding(encoding); } @@ -47,46 +53,46 @@ QStringList CSVDoc::FileDialog::selectedFilePaths() { QStringList filePaths; - for (ContentSelectorModel::EsmFile *file : mSelector->selectedFiles() ) + for (ContentSelectorModel::EsmFile* file : mSelector->selectedFiles()) filePaths.append(file->filePath()); return filePaths; } -void CSVDoc::FileDialog::setLocalData (const boost::filesystem::path& localData) +void CSVDoc::FileDialog::setLocalData(const std::filesystem::path& localData) { - mAdjusterWidget->setLocalData (localData); + mAdjusterWidget->setLocalData(localData); } -void CSVDoc::FileDialog::showDialog (ContentAction action) +void CSVDoc::FileDialog::showDialog(ContentAction action) { mAction = action; - ui.projectGroupBoxLayout->insertWidget (0, mAdjusterWidget); + ui.projectGroupBoxLayout->insertWidget(0, mAdjusterWidget); switch (mAction) { - case ContentAction_New: - buildNewFileView(); - break; + case ContentAction_New: + buildNewFileView(); + break; - case ContentAction_Edit: - buildOpenFileView(); - break; + case ContentAction_Edit: + buildOpenFileView(); + break; - default: - break; + default: + break; } - mAdjusterWidget->setFilenameCheck (mAction == ContentAction_New); + mAdjusterWidget->setFilenameCheck(mAction == ContentAction_New); - if(!mDialogBuilt) + if (!mDialogBuilt) { - //connections common to both dialog view flavors - connect (mSelector, SIGNAL (signalCurrentGamefileIndexChanged (int)), - this, SLOT (slotUpdateAcceptButton (int))); + // connections common to both dialog view flavors + connect(mSelector, &ContentSelectorView::ContentSelector::signalCurrentGamefileIndexChanged, this, + qOverload(&FileDialog::slotUpdateAcceptButton)); - connect (ui.projectButtonBox, SIGNAL (rejected()), this, SLOT (slotRejected())); + connect(ui.projectButtonBox, &QDialogButtonBox::rejected, this, &FileDialog::slotRejected); mDialogBuilt = true; } @@ -99,47 +105,47 @@ void CSVDoc::FileDialog::buildNewFileView() { setWindowTitle(tr("Create a new addon")); - QPushButton* createButton = ui.projectButtonBox->button (QDialogButtonBox::Ok); - createButton->setText ("Create"); - createButton->setEnabled (false); + QPushButton* createButton = ui.projectButtonBox->button(QDialogButtonBox::Ok); + createButton->setText("Create"); + createButton->setEnabled(false); - if(!mFileWidget) + if (!mFileWidget) { - mFileWidget = new FileWidget (this); + mFileWidget = new FileWidget(this); - mFileWidget->setType (true); + mFileWidget->setType(true); mFileWidget->extensionLabelIsVisible(true); - connect (mFileWidget, SIGNAL (nameChanged (const QString&, bool)), - mAdjusterWidget, SLOT (setName (const QString&, bool))); + connect(mFileWidget, &FileWidget::nameChanged, mAdjusterWidget, &AdjusterWidget::setName); - connect (mFileWidget, SIGNAL (nameChanged(const QString &, bool)), - this, SLOT (slotUpdateAcceptButton(const QString &, bool))); + connect(mFileWidget, &FileWidget::nameChanged, this, + qOverload(&FileDialog::slotUpdateAcceptButton)); } - ui.projectGroupBoxLayout->insertWidget (0, mFileWidget); + ui.projectGroupBoxLayout->insertWidget(0, mFileWidget); - connect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); + connect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotNewFile); } void CSVDoc::FileDialog::buildOpenFileView() { setWindowTitle(tr("Open")); - ui.projectGroupBox->setTitle (QString("")); - ui.projectButtonBox->button(QDialogButtonBox::Ok)->setText ("Open"); - if(mSelector->isGamefileSelected()) - ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled (true); + ui.projectGroupBox->setTitle(QString("")); + ui.projectButtonBox->button(QDialogButtonBox::Ok)->setText("Open"); + if (mSelector->isGamefileSelected()) + ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled(true); else - ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); + ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - if(!mDialogBuilt) + if (!mDialogBuilt) { - connect (mSelector, SIGNAL (signalAddonDataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (slotAddonDataChanged(const QModelIndex&, const QModelIndex&))); + connect(mSelector, &ContentSelectorView::ContentSelector::signalAddonDataChanged, this, + &FileDialog::slotAddonDataChanged); } - connect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); + connect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); } -void CSVDoc::FileDialog::slotAddonDataChanged(const QModelIndex &topleft, const QModelIndex &bottomright) +void CSVDoc::FileDialog::slotAddonDataChanged(const QModelIndex& topleft, const QModelIndex& bottomright) { slotUpdateAcceptButton(0); } @@ -151,26 +157,26 @@ void CSVDoc::FileDialog::slotUpdateAcceptButton(int) if (mFileWidget && mAction == ContentAction_New) name = mFileWidget->getName(); - slotUpdateAcceptButton (name, true); + slotUpdateAcceptButton(name, true); } -void CSVDoc::FileDialog::slotUpdateAcceptButton(const QString &name, bool) +void CSVDoc::FileDialog::slotUpdateAcceptButton(const QString& name, bool) { bool success = !mSelector->selectedFiles().empty(); bool isNew = (mAction == ContentAction_New); if (isNew) - success = success && !(name.isEmpty()); + success = !name.isEmpty(); else if (success) { - ContentSelectorModel::EsmFile *file = mSelector->selectedFiles().back(); - mAdjusterWidget->setName (file->filePath(), !file->isGameFile()); + ContentSelectorModel::EsmFile* file = mSelector->selectedFiles().back(); + mAdjusterWidget->setName(file->filePath(), !file->isGameFile()); } else - mAdjusterWidget->setName ("", true); + mAdjusterWidget->setName("", true); - ui.projectButtonBox->button (QDialogButtonBox::Ok)->setEnabled (success); + ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled(success); } QString CSVDoc::FileDialog::filename() const @@ -184,9 +190,9 @@ QString CSVDoc::FileDialog::filename() const void CSVDoc::FileDialog::slotRejected() { emit rejected(); - disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); - disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); - if(mFileWidget) + disconnect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotNewFile); + disconnect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); + if (mFileWidget) { delete mFileWidget; mFileWidget = nullptr; @@ -196,23 +202,23 @@ void CSVDoc::FileDialog::slotRejected() void CSVDoc::FileDialog::slotNewFile() { - emit signalCreateNewFile (mAdjusterWidget->getPath()); - if(mFileWidget) + emit signalCreateNewFile(mAdjusterWidget->getPath()); + if (mFileWidget) { delete mFileWidget; mFileWidget = nullptr; } - disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); + disconnect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotNewFile); close(); } void CSVDoc::FileDialog::slotOpenFile() { - ContentSelectorModel::EsmFile *file = mSelector->selectedFiles().back(); + ContentSelectorModel::EsmFile* file = mSelector->selectedFiles().back(); - mAdjusterWidget->setName (file->filePath(), !file->isGameFile()); + mAdjusterWidget->setName(file->filePath(), !file->isGameFile()); - emit signalOpenFiles (mAdjusterWidget->getPath()); - disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); + emit signalOpenFiles(mAdjusterWidget->getPath()); + disconnect(ui.projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); close(); } diff --git a/apps/opencs/view/doc/filedialog.hpp b/apps/opencs/view/doc/filedialog.hpp index 6c48fa78b96..d660f22f755 100644 --- a/apps/opencs/view/doc/filedialog.hpp +++ b/apps/opencs/view/doc/filedialog.hpp @@ -2,22 +2,25 @@ #define FILEDIALOG_HPP #include -#include #ifndef Q_MOC_RUN -#include #include "adjusterwidget.hpp" -#ifndef CS_QT_BOOST_FILESYSTEM_PATH_DECLARED -#define CS_QT_BOOST_FILESYSTEM_PATH_DECLARED -Q_DECLARE_METATYPE (boost::filesystem::path) +#ifndef CS_QT_STD_FILESYSTEM_PATH_DECLARED +#define CS_QT_STD_FILESYSTEM_PATH_DECLARED +Q_DECLARE_METATYPE(std::filesystem::path) #endif #endif #include "ui_filedialog.h" +#include +#include + +class QModelIndex; + namespace ContentSelectorView { class ContentSelector; @@ -32,46 +35,43 @@ namespace CSVDoc Q_OBJECT private: - - ContentSelectorView::ContentSelector *mSelector; + ContentSelectorView::ContentSelector* mSelector; Ui::FileDialog ui; ContentAction mAction; - FileWidget *mFileWidget; - AdjusterWidget *mAdjusterWidget; + FileWidget* mFileWidget; + AdjusterWidget* mAdjusterWidget; bool mDialogBuilt; public: + explicit FileDialog(QWidget* parent = nullptr); + void showDialog(ContentAction action); - explicit FileDialog(QWidget *parent = nullptr); - void showDialog (ContentAction action); - - void addFiles (const QString &path); - void setEncoding (const QString &encoding); - void clearFiles (); + void addFiles(const std::vector& dataDirs); + void setEncoding(const QString& encoding); + void clearFiles(); QString filename() const; QStringList selectedFilePaths(); - void setLocalData (const boost::filesystem::path& localData); + void setLocalData(const std::filesystem::path& localData); private: - void buildNewFileView(); void buildOpenFileView(); signals: - void signalOpenFiles (const boost::filesystem::path &path); - void signalCreateNewFile (const boost::filesystem::path &path); + void signalOpenFiles(const std::filesystem::path& path); + void signalCreateNewFile(const std::filesystem::path& path); - void signalUpdateAcceptButton (bool, int); + void signalUpdateAcceptButton(bool, int); private slots: void slotNewFile(); void slotOpenFile(); - void slotUpdateAcceptButton (int); - void slotUpdateAcceptButton (const QString &, bool); + void slotUpdateAcceptButton(int); + void slotUpdateAcceptButton(const QString&, bool); void slotRejected(); void slotAddonDataChanged(const QModelIndex& topleft, const QModelIndex& bottomright); }; diff --git a/apps/opencs/view/doc/filewidget.cpp b/apps/opencs/view/doc/filewidget.cpp index 9e9acdfbe6e..26cdef011fc 100644 --- a/apps/opencs/view/doc/filewidget.cpp +++ b/apps/opencs/view/doc/filewidget.cpp @@ -1,38 +1,38 @@ #include "filewidget.hpp" #include -#include #include -#include -#include +#include QString CSVDoc::FileWidget::getExtension() const { return mAddon ? ".omwaddon" : ".omwgame"; } -CSVDoc::FileWidget::FileWidget (QWidget *parent) : QWidget (parent), mAddon (false) +CSVDoc::FileWidget::FileWidget(QWidget* parent) + : QWidget(parent) + , mAddon(false) { - QHBoxLayout *layout = new QHBoxLayout (this); + QHBoxLayout* layout = new QHBoxLayout(this); - mInput = new QLineEdit (this); + mInput = new QLineEdit(this); - layout->addWidget (mInput, 1); + layout->addWidget(mInput, 1); - mType = new QLabel (this); + mType = new QLabel(this); - layout ->addWidget (mType); + layout->addWidget(mType); - connect (mInput, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); + connect(mInput, &QLineEdit::textChanged, this, &FileWidget::textChanged); - setLayout (layout); + setLayout(layout); } -void CSVDoc::FileWidget::setType (bool addon) +void CSVDoc::FileWidget::setType(bool addon) { mAddon = addon; - mType->setText (getExtension()); + mType->setText(getExtension()); } QString CSVDoc::FileWidget::getName() const @@ -45,9 +45,9 @@ QString CSVDoc::FileWidget::getName() const return text + getExtension(); } -void CSVDoc::FileWidget::textChanged (const QString& text) +void CSVDoc::FileWidget::textChanged(const QString& text) { - emit nameChanged (getName(), mAddon); + emit nameChanged(getName(), mAddon); } void CSVDoc::FileWidget::extensionLabelIsVisible(bool visible) @@ -55,10 +55,10 @@ void CSVDoc::FileWidget::extensionLabelIsVisible(bool visible) mType->setVisible(visible); } -void CSVDoc::FileWidget::setName (const std::string& text) +void CSVDoc::FileWidget::setName(const std::string& text) { - QString text2 = QString::fromUtf8 (text.c_str()); + QString text2 = QString::fromUtf8(text.c_str()); - mInput->setText (text2); - textChanged (text2); + mInput->setText(text2); + textChanged(text2); } diff --git a/apps/opencs/view/doc/filewidget.hpp b/apps/opencs/view/doc/filewidget.hpp index 626b8d77d8b..1d90afd3628 100644 --- a/apps/opencs/view/doc/filewidget.hpp +++ b/apps/opencs/view/doc/filewidget.hpp @@ -1,45 +1,44 @@ #ifndef CSV_DOC_FILEWIDGET_H #define CSV_DOC_FILEWIDGET_H +#include #include #include class QLabel; -class QString; class QLineEdit; namespace CSVDoc { class FileWidget : public QWidget { - Q_OBJECT + Q_OBJECT - bool mAddon; - QLineEdit *mInput; - QLabel *mType; + bool mAddon; + QLineEdit* mInput; + QLabel* mType; - QString getExtension() const; + QString getExtension() const; - public: + public: + FileWidget(QWidget* parent = nullptr); - FileWidget (QWidget *parent = nullptr); + void setType(bool addon); - void setType (bool addon); + QString getName() const; - QString getName() const; + void extensionLabelIsVisible(bool visible); - void extensionLabelIsVisible(bool visible); + void setName(const std::string& text); - void setName (const std::string& text); + private slots: - private slots: + void textChanged(const QString& text); - void textChanged (const QString& text); + signals: - signals: - - void nameChanged (const QString& file, bool addon); + void nameChanged(const QString& file, bool addon); }; } diff --git a/apps/opencs/view/doc/globaldebugprofilemenu.cpp b/apps/opencs/view/doc/globaldebugprofilemenu.cpp index c898b819c2b..87b148c5ecf 100644 --- a/apps/opencs/view/doc/globaldebugprofilemenu.cpp +++ b/apps/opencs/view/doc/globaldebugprofilemenu.cpp @@ -1,13 +1,20 @@ #include "globaldebugprofilemenu.hpp" -#include #include +#include +#include #include +#include +#include + +#include #include "../../model/world/idtable.hpp" #include "../../model/world/record.hpp" +class QWidget; + void CSVDoc::GlobalDebugProfileMenu::rebuild() { clear(); @@ -15,78 +22,72 @@ void CSVDoc::GlobalDebugProfileMenu::rebuild() delete mActions; mActions = nullptr; - int idColumn = mDebugProfiles->findColumnIndex (CSMWorld::Columns::ColumnId_Id); - int stateColumn = mDebugProfiles->findColumnIndex (CSMWorld::Columns::ColumnId_Modification); - int globalColumn = mDebugProfiles->findColumnIndex ( - CSMWorld::Columns::ColumnId_GlobalProfile); + int idColumn = mDebugProfiles->findColumnIndex(CSMWorld::Columns::ColumnId_Id); + int stateColumn = mDebugProfiles->findColumnIndex(CSMWorld::Columns::ColumnId_Modification); + int globalColumn = mDebugProfiles->findColumnIndex(CSMWorld::Columns::ColumnId_GlobalProfile); int size = mDebugProfiles->rowCount(); std::vector ids; - for (int i=0; idata (mDebugProfiles->index (i, stateColumn)).toInt(); + int state = mDebugProfiles->data(mDebugProfiles->index(i, stateColumn)).toInt(); - bool global = mDebugProfiles->data (mDebugProfiles->index (i, globalColumn)).toInt(); + bool global = mDebugProfiles->data(mDebugProfiles->index(i, globalColumn)).toInt(); - if (state!=CSMWorld::RecordBase::State_Deleted && global) - ids.push_back ( - mDebugProfiles->data (mDebugProfiles->index (i, idColumn)).toString()); + if (state != CSMWorld::RecordBase::State_Deleted && global) + ids.push_back(mDebugProfiles->data(mDebugProfiles->index(i, idColumn)).toString()); } - mActions = new QActionGroup (this); - connect (mActions, SIGNAL (triggered (QAction *)), this, SLOT (actionTriggered (QAction *))); + mActions = new QActionGroup(this); + connect(mActions, &QActionGroup::triggered, this, &GlobalDebugProfileMenu::actionTriggered); - std::sort (ids.begin(), ids.end()); + std::sort(ids.begin(), ids.end()); - for (std::vector::const_iterator iter (ids.begin()); iter!=ids.end(); ++iter) + for (std::vector::const_iterator iter(ids.begin()); iter != ids.end(); ++iter) { - mActions->addAction (addAction (*iter)); + mActions->addAction(addAction(*iter)); } } -CSVDoc::GlobalDebugProfileMenu::GlobalDebugProfileMenu (CSMWorld::IdTable *debugProfiles, - QWidget *parent) -: QMenu (parent), mDebugProfiles (debugProfiles), mActions (nullptr) +CSVDoc::GlobalDebugProfileMenu::GlobalDebugProfileMenu(CSMWorld::IdTable* debugProfiles, QWidget* parent) + : QMenu(parent) + , mDebugProfiles(debugProfiles) + , mActions(nullptr) { rebuild(); - connect (mDebugProfiles, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (profileAboutToBeRemoved (const QModelIndex&, int, int))); + connect(mDebugProfiles, &CSMWorld::IdTable::rowsAboutToBeRemoved, this, + &GlobalDebugProfileMenu::profileAboutToBeRemoved); - connect (mDebugProfiles, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (profileInserted (const QModelIndex&, int, int))); + connect(mDebugProfiles, &CSMWorld::IdTable::rowsInserted, this, &GlobalDebugProfileMenu::profileInserted); - connect (mDebugProfiles, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (profileChanged (const QModelIndex&, const QModelIndex&))); + connect(mDebugProfiles, &CSMWorld::IdTable::dataChanged, this, &GlobalDebugProfileMenu::profileChanged); } -void CSVDoc::GlobalDebugProfileMenu::updateActions (bool running) +void CSVDoc::GlobalDebugProfileMenu::updateActions(bool running) { if (mActions) - mActions->setEnabled (!running); + mActions->setEnabled(!running); } -void CSVDoc::GlobalDebugProfileMenu::profileAboutToBeRemoved (const QModelIndex& parent, - int start, int end) +void CSVDoc::GlobalDebugProfileMenu::profileAboutToBeRemoved(const QModelIndex& parent, int start, int end) { rebuild(); } -void CSVDoc::GlobalDebugProfileMenu::profileInserted (const QModelIndex& parent, int start, - int end) +void CSVDoc::GlobalDebugProfileMenu::profileInserted(const QModelIndex& parent, int start, int end) { rebuild(); } -void CSVDoc::GlobalDebugProfileMenu::profileChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSVDoc::GlobalDebugProfileMenu::profileChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { rebuild(); } -void CSVDoc::GlobalDebugProfileMenu::actionTriggered (QAction *action) +void CSVDoc::GlobalDebugProfileMenu::actionTriggered(QAction* action) { - emit triggered (std::string (action->text().toUtf8().constData())); + emit triggered(std::string(action->text().toUtf8().constData())); } diff --git a/apps/opencs/view/doc/globaldebugprofilemenu.hpp b/apps/opencs/view/doc/globaldebugprofilemenu.hpp index e12ee306a1a..797fda3e883 100644 --- a/apps/opencs/view/doc/globaldebugprofilemenu.hpp +++ b/apps/opencs/view/doc/globaldebugprofilemenu.hpp @@ -3,8 +3,13 @@ #include -class QModelIndex; +#include + +class QAction; class QActionGroup; +class QModelIndex; +class QObject; +class QWidget; namespace CSMWorld { @@ -15,34 +20,32 @@ namespace CSVDoc { class GlobalDebugProfileMenu : public QMenu { - Q_OBJECT - - CSMWorld::IdTable *mDebugProfiles; - QActionGroup *mActions; - - private: + Q_OBJECT - void rebuild(); + CSMWorld::IdTable* mDebugProfiles; + QActionGroup* mActions; - public: + private: + void rebuild(); - GlobalDebugProfileMenu (CSMWorld::IdTable *debugProfiles, QWidget *parent = nullptr); + public: + GlobalDebugProfileMenu(CSMWorld::IdTable* debugProfiles, QWidget* parent = nullptr); - void updateActions (bool running); + void updateActions(bool running); - private slots: + private slots: - void profileAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void profileAboutToBeRemoved(const QModelIndex& parent, int start, int end); - void profileInserted (const QModelIndex& parent, int start, int end); + void profileInserted(const QModelIndex& parent, int start, int end); - void profileChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void profileChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - void actionTriggered (QAction *action); + void actionTriggered(QAction* action); - signals: + signals: - void triggered (const std::string& profile); + void triggered(const std::string& profile); }; } diff --git a/apps/opencs/view/doc/loader.cpp b/apps/opencs/view/doc/loader.cpp index 1cdfc017330..9d98635ef4f 100644 --- a/apps/opencs/view/doc/loader.cpp +++ b/apps/opencs/view/doc/loader.cpp @@ -1,201 +1,214 @@ #include "loader.hpp" -#include -#include -#include +#include #include #include -#include +#include #include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include #include "../../model/doc/document.hpp" -void CSVDoc::LoadingDocument::closeEvent (QCloseEvent *event) +void CSVDoc::LoadingDocument::closeEvent(QCloseEvent* event) { event->ignore(); cancel(); } -CSVDoc::LoadingDocument::LoadingDocument (CSMDoc::Document *document) -: mDocument (document), mAborted (false), mMessages (nullptr), mTotalRecords (0) +CSVDoc::LoadingDocument::LoadingDocument(CSMDoc::Document* document) + : mDocument(document) + , mTotalRecordsLabel(0) + , mRecordsLabel(0) + , mAborted(false) + , mMessages(nullptr) + , mRecords(0) { - setWindowTitle (QString::fromUtf8((std::string("Opening ") + document->getSavePath().filename().string()).c_str())); + setWindowTitle("Opening " + Files::pathToQString(document->getSavePath().filename())); - setMinimumWidth (400); + setMinimumWidth(400); - mLayout = new QVBoxLayout (this); + mLayout = new QVBoxLayout(this); - // file progress - mFile = new QLabel (this); + // total progress + mTotalRecordsLabel = new QLabel(this); - mLayout->addWidget (mFile); + mLayout->addWidget(mTotalRecordsLabel); - mFileProgress = new QProgressBar (this); + mTotalProgress = new QProgressBar(this); - mLayout->addWidget (mFileProgress); + mLayout->addWidget(mTotalProgress); - int size = static_cast (document->getContentFiles().size())+1; - if (document->isNew()) - --size; + mTotalProgress->setMinimum(0); + mTotalProgress->setMaximum(document->getData().getTotalRecords(document->getContentFiles())); + mTotalProgress->setTextVisible(true); + mTotalProgress->setValue(0); + mTotalRecords = 0; - mFileProgress->setMinimum (0); - mFileProgress->setMaximum (size); - mFileProgress->setTextVisible (true); - mFileProgress->setValue (0); + mFilesLoaded = 0; // record progress - mLayout->addWidget (mRecords = new QLabel ("Records", this)); + mLayout->addWidget(mRecordsLabel = new QLabel("Records", this)); - mRecordProgress = new QProgressBar (this); + mRecordProgress = new QProgressBar(this); - mLayout->addWidget (mRecordProgress); + mLayout->addWidget(mRecordProgress); - mRecordProgress->setMinimum (0); - mRecordProgress->setTextVisible (true); - mRecordProgress->setValue (0); + mRecordProgress->setMinimum(0); + mRecordProgress->setTextVisible(true); + mRecordProgress->setValue(0); // error message - mError = new QLabel (this); - mError->setWordWrap (true); + mError = new QLabel(this); + mError->setWordWrap(true); + mError->setTextInteractionFlags(Qt::TextSelectableByMouse); - mLayout->addWidget (mError); + mLayout->addWidget(mError); // buttons - mButtons = new QDialogButtonBox (QDialogButtonBox::Cancel, Qt::Horizontal, this); + mButtons = new QDialogButtonBox(QDialogButtonBox::Cancel, Qt::Horizontal, this); - mLayout->addWidget (mButtons); + mLayout->addWidget(mButtons); - setLayout (mLayout); + setLayout(mLayout); - move (QCursor::pos()); + move(QCursor::pos()); show(); - connect (mButtons, SIGNAL (rejected()), this, SLOT (cancel())); + connect(mButtons, &QDialogButtonBox::rejected, this, qOverload<>(&LoadingDocument::cancel)); } -void CSVDoc::LoadingDocument::nextStage (const std::string& name, int totalRecords) +void CSVDoc::LoadingDocument::nextStage(const std::string& name, int fileRecords) { - mFile->setText (QString::fromUtf8 (("Loading: " + name).c_str())); + ++mFilesLoaded; + size_t numFiles = mDocument->getContentFiles().size(); + + mTotalRecordsLabel->setText(QString::fromUtf8( + ("Loading: " + name + " (" + std::to_string(mFilesLoaded) + " of " + std::to_string((numFiles)) + ")") + .c_str())); - mFileProgress->setValue (mFileProgress->value()+1); + mTotalRecords = mTotalProgress->value(); - mRecordProgress->setValue (0); - mRecordProgress->setMaximum (totalRecords>0 ? totalRecords : 1); + mRecordProgress->setValue(0); + mRecordProgress->setMaximum(fileRecords > 0 ? fileRecords : 1); - mTotalRecords = totalRecords; + mRecords = fileRecords; } -void CSVDoc::LoadingDocument::nextRecord (int records) +void CSVDoc::LoadingDocument::nextRecord(int records) { - if (records<=mTotalRecords) + if (records <= mRecords) { - mRecordProgress->setValue (records); + mTotalProgress->setValue(mTotalRecords + records); - std::ostringstream stream; + mRecordProgress->setValue(records); - stream << "Records: " << records << " of " << mTotalRecords; - - mRecords->setText (QString::fromUtf8 (stream.str().c_str())); + mRecordsLabel->setText("Records: " + QString::number(records) + " of " + QString::number(mRecords)); } } -void CSVDoc::LoadingDocument::abort (const std::string& error) +void CSVDoc::LoadingDocument::abort(const std::string& error) { mAborted = true; - mError->setText (QString::fromUtf8 (("Loading failed: " + error + "").c_str())); - mButtons->setStandardButtons (QDialogButtonBox::Close); + mError->setText(QString::fromUtf8(("Loading failed: " + error + "").c_str())); + Log(Debug::Error) << "Loading failed: " << error; + mButtons->setStandardButtons(QDialogButtonBox::Close); } -void CSVDoc::LoadingDocument::addMessage (const std::string& message) +void CSVDoc::LoadingDocument::addMessage(const std::string& message) { if (!mMessages) { - mMessages = new QListWidget (this); - mLayout->insertWidget (4, mMessages); + mMessages = new QListWidget(this); + mLayout->insertWidget(4, mMessages); } - new QListWidgetItem (QString::fromUtf8 (message.c_str()), mMessages); + new QListWidgetItem(QString::fromUtf8(message.c_str()), mMessages); } void CSVDoc::LoadingDocument::cancel() { if (!mAborted) - emit cancel (mDocument); + emit cancel(mDocument); else { - emit close (mDocument); + emit close(mDocument); deleteLater(); } } - -CSVDoc::Loader::Loader() {} - CSVDoc::Loader::~Loader() { - for (std::map::iterator iter (mDocuments.begin()); - iter!=mDocuments.end(); ++iter) + for (std::map::iterator iter(mDocuments.begin()); iter != mDocuments.end(); + ++iter) delete iter->second; } -void CSVDoc::Loader::add (CSMDoc::Document *document) +void CSVDoc::Loader::add(CSMDoc::Document* document) { - LoadingDocument *loading = new LoadingDocument (document); - mDocuments.insert (std::make_pair (document, loading)); + LoadingDocument* loading = new LoadingDocument(document); + mDocuments.insert(std::make_pair(document, loading)); - connect (loading, SIGNAL (cancel (CSMDoc::Document *)), - this, SIGNAL (cancel (CSMDoc::Document *))); - connect (loading, SIGNAL (close (CSMDoc::Document *)), - this, SIGNAL (close (CSMDoc::Document *))); + connect(loading, qOverload(&LoadingDocument::cancel), this, &Loader::cancel); + connect(loading, &LoadingDocument::close, this, &Loader::close); } -void CSVDoc::Loader::loadingStopped (CSMDoc::Document *document, bool completed, - const std::string& error) +void CSVDoc::Loader::loadingStopped(CSMDoc::Document* document, bool completed, const std::string& error) { - std::map::iterator iter = mDocuments.begin(); + std::map::iterator iter = mDocuments.begin(); - for (; iter!=mDocuments.end(); ++iter) - if (iter->first==document) + for (; iter != mDocuments.end(); ++iter) + if (iter->first == document) break; - if (iter==mDocuments.end()) + if (iter == mDocuments.end()) return; if (completed || error.empty()) { delete iter->second; - mDocuments.erase (iter); + mDocuments.erase(iter); } else { - iter->second->abort (error); + iter->second->abort(error); // Leave the window open for now (wait for the user to close it) - mDocuments.erase (iter); + mDocuments.erase(iter); } } -void CSVDoc::Loader::nextStage (CSMDoc::Document *document, const std::string& name, - int totalRecords) +void CSVDoc::Loader::nextStage(CSMDoc::Document* document, const std::string& name, int fileRecords) { - std::map::iterator iter = mDocuments.find (document); + std::map::iterator iter = mDocuments.find(document); - if (iter!=mDocuments.end()) - iter->second->nextStage (name, totalRecords); + if (iter != mDocuments.end()) + iter->second->nextStage(name, fileRecords); } -void CSVDoc::Loader::nextRecord (CSMDoc::Document *document, int records) +void CSVDoc::Loader::nextRecord(CSMDoc::Document* document, int records) { - std::map::iterator iter = mDocuments.find (document); + std::map::iterator iter = mDocuments.find(document); - if (iter!=mDocuments.end()) - iter->second->nextRecord (records); + if (iter != mDocuments.end()) + iter->second->nextRecord(records); } -void CSVDoc::Loader::loadMessage (CSMDoc::Document *document, const std::string& message) +void CSVDoc::Loader::loadMessage(CSMDoc::Document* document, const std::string& message) { - std::map::iterator iter = mDocuments.find (document); + std::map::iterator iter = mDocuments.find(document); - if (iter!=mDocuments.end()) - iter->second->addMessage (message); + if (iter != mDocuments.end()) + iter->second->addMessage(message); } diff --git a/apps/opencs/view/doc/loader.hpp b/apps/opencs/view/doc/loader.hpp index 24cbee78843..223d77ce3cc 100644 --- a/apps/opencs/view/doc/loader.hpp +++ b/apps/opencs/view/doc/loader.hpp @@ -2,11 +2,12 @@ #define CSV_DOC_LOADER_H #include +#include #include #include -#include +class QCloseEvent; class QLabel; class QProgressBar; class QDialogButtonBox; @@ -22,79 +23,77 @@ namespace CSVDoc { class LoadingDocument : public QWidget { - Q_OBJECT + Q_OBJECT - CSMDoc::Document *mDocument; - QLabel *mFile; - QLabel *mRecords; - QProgressBar *mFileProgress; - QProgressBar *mRecordProgress; - bool mAborted; - QDialogButtonBox *mButtons; - QLabel *mError; - QListWidget *mMessages; - QVBoxLayout *mLayout; - int mTotalRecords; + CSMDoc::Document* mDocument; + QLabel* mTotalRecordsLabel; + QLabel* mRecordsLabel; + QProgressBar* mTotalProgress; + QProgressBar* mRecordProgress; + bool mAborted; + QDialogButtonBox* mButtons; + QLabel* mError; + QListWidget* mMessages; + QVBoxLayout* mLayout; + int mRecords; + int mTotalRecords; + int mFilesLoaded; - private: + private: + void closeEvent(QCloseEvent* event) override; - void closeEvent (QCloseEvent *event) override; + public: + LoadingDocument(CSMDoc::Document* document); - public: + void nextStage(const std::string& name, int totalRecords); - LoadingDocument (CSMDoc::Document *document); + void nextRecord(int records); - void nextStage (const std::string& name, int totalRecords); + void abort(const std::string& error); - void nextRecord (int records); + void addMessage(const std::string& message); - void abort (const std::string& error); + private slots: - void addMessage (const std::string& message); + void cancel(); - private slots: + signals: - void cancel(); + void cancel(CSMDoc::Document* document); + ///< Stop loading process. - signals: - - void cancel (CSMDoc::Document *document); - ///< Stop loading process. - - void close (CSMDoc::Document *document); - ///< Close stopped loading process. + void close(CSMDoc::Document* document); + ///< Close stopped loading process. }; class Loader : public QObject { - Q_OBJECT - - std::map mDocuments; + Q_OBJECT - public: + std::map mDocuments; - Loader(); + public: + Loader() = default; - virtual ~Loader(); + ~Loader() override; - signals: + signals: - void cancel (CSMDoc::Document *document); + void cancel(CSMDoc::Document* document); - void close (CSMDoc::Document *document); + void close(CSMDoc::Document* document); - public slots: + public slots: - void add (CSMDoc::Document *document); + void add(CSMDoc::Document* document); - void loadingStopped (CSMDoc::Document *document, bool completed, - const std::string& error); + void loadingStopped(CSMDoc::Document* document, bool completed, const std::string& error); - void nextStage (CSMDoc::Document *document, const std::string& name, int totalRecords); + void nextStage(CSMDoc::Document* document, const std::string& name, int totalRecords); - void nextRecord (CSMDoc::Document *document, int records); + void nextRecord(CSMDoc::Document* document, int records); - void loadMessage (CSMDoc::Document *document, const std::string& message); + void loadMessage(CSMDoc::Document* document, const std::string& message); }; } diff --git a/apps/opencs/view/doc/newgame.cpp b/apps/opencs/view/doc/newgame.cpp index 7b247652ed5..99ee189e7d1 100644 --- a/apps/opencs/view/doc/newgame.cpp +++ b/apps/opencs/view/doc/newgame.cpp @@ -1,74 +1,72 @@ #include "newgame.hpp" -#include -#include -#include #include +#include #include #include +#include -#include "filewidget.hpp" #include "adjusterwidget.hpp" +#include "filewidget.hpp" CSVDoc::NewGameDialogue::NewGameDialogue() { - setWindowTitle ("Create New Game"); + setWindowTitle("Create New Game"); - QVBoxLayout *layout = new QVBoxLayout (this); + QVBoxLayout* layout = new QVBoxLayout(this); - mFileWidget = new FileWidget (this); - mFileWidget->setType (false); + mFileWidget = new FileWidget(this); + mFileWidget->setType(false); - layout->addWidget (mFileWidget, 1); + layout->addWidget(mFileWidget, 1); - mAdjusterWidget = new AdjusterWidget (this); + mAdjusterWidget = new AdjusterWidget(this); - layout->addWidget (mAdjusterWidget, 1); + layout->addWidget(mAdjusterWidget, 1); - QDialogButtonBox *buttons = new QDialogButtonBox (this); + QDialogButtonBox* buttons = new QDialogButtonBox(this); - mCreate = new QPushButton ("Create", this); - mCreate->setDefault (true); - mCreate->setEnabled (false); + mCreate = new QPushButton("Create", this); + mCreate->setDefault(true); + mCreate->setEnabled(false); - buttons->addButton (mCreate, QDialogButtonBox::AcceptRole); + buttons->addButton(mCreate, QDialogButtonBox::AcceptRole); - QPushButton *cancel = new QPushButton ("Cancel", this); + QPushButton* cancel = new QPushButton("Cancel", this); - buttons->addButton (cancel, QDialogButtonBox::RejectRole); + buttons->addButton(cancel, QDialogButtonBox::RejectRole); - layout->addWidget (buttons); + layout->addWidget(buttons); - setLayout (layout); + setLayout(layout); - connect (mAdjusterWidget, SIGNAL (stateChanged (bool)), this, SLOT (stateChanged (bool))); - connect (mCreate, SIGNAL (clicked()), this, SLOT (create())); - connect (cancel, SIGNAL (clicked()), this, SLOT (reject())); - connect (mFileWidget, SIGNAL (nameChanged (const QString&, bool)), - mAdjusterWidget, SLOT (setName (const QString&, bool))); + connect(mAdjusterWidget, &AdjusterWidget::stateChanged, this, &NewGameDialogue::stateChanged); + connect(mCreate, &QPushButton::clicked, this, &NewGameDialogue::create); + connect(cancel, &QPushButton::clicked, this, &NewGameDialogue::reject); + connect(mFileWidget, &FileWidget::nameChanged, mAdjusterWidget, &AdjusterWidget::setName); QRect scr = QGuiApplication::primaryScreen()->geometry(); QRect rect = geometry(); - move (scr.center().x() - rect.center().x(), scr.center().y() - rect.center().y()); + move(scr.center().x() - rect.center().x(), scr.center().y() - rect.center().y()); } -void CSVDoc::NewGameDialogue::setLocalData (const boost::filesystem::path& localData) +void CSVDoc::NewGameDialogue::setLocalData(const std::filesystem::path& localData) { - mAdjusterWidget->setLocalData (localData); + mAdjusterWidget->setLocalData(localData); } -void CSVDoc::NewGameDialogue::stateChanged (bool valid) +void CSVDoc::NewGameDialogue::stateChanged(bool valid) { - mCreate->setEnabled (valid); + mCreate->setEnabled(valid); } void CSVDoc::NewGameDialogue::create() { - emit createRequest (mAdjusterWidget->getPath()); + emit createRequest(mAdjusterWidget->getPath()); } void CSVDoc::NewGameDialogue::reject() { - emit cancelCreateGame (); + emit cancelCreateGame(); QDialog::reject(); } diff --git a/apps/opencs/view/doc/newgame.hpp b/apps/opencs/view/doc/newgame.hpp index e3c6f53ca54..4e8154e22b5 100644 --- a/apps/opencs/view/doc/newgame.hpp +++ b/apps/opencs/view/doc/newgame.hpp @@ -1,14 +1,14 @@ #ifndef CSV_DOC_NEWGAME_H #define CSV_DOC_NEWGAME_H -#include - #include #include -#ifndef CS_QT_BOOST_FILESYSTEM_PATH_DECLARED -#define CS_QT_BOOST_FILESYSTEM_PATH_DECLARED -Q_DECLARE_METATYPE (boost::filesystem::path) +#include + +#ifndef CS_QT_STD_FILESYSTEM_PATH_DECLARED +#define CS_QT_STD_FILESYSTEM_PATH_DECLARED +Q_DECLARE_METATYPE(std::filesystem::path) #endif class QPushButton; @@ -20,31 +20,30 @@ namespace CSVDoc class NewGameDialogue : public QDialog { - Q_OBJECT - - QPushButton *mCreate; - FileWidget *mFileWidget; - AdjusterWidget *mAdjusterWidget; + Q_OBJECT - public: + QPushButton* mCreate; + FileWidget* mFileWidget; + AdjusterWidget* mAdjusterWidget; - NewGameDialogue(); + public: + NewGameDialogue(); - void setLocalData (const boost::filesystem::path& localData); + void setLocalData(const std::filesystem::path& localData); - signals: + signals: - void createRequest (const boost::filesystem::path& file); + void createRequest(const std::filesystem::path& file); - void cancelCreateGame (); + void cancelCreateGame(); - private slots: + private slots: - void stateChanged (bool valid); + void stateChanged(bool valid); - void create(); + void create(); - void reject() override; + void reject() override; }; } diff --git a/apps/opencs/view/doc/operation.cpp b/apps/opencs/view/doc/operation.cpp index e5c1e7b89ed..571413af1b8 100644 --- a/apps/opencs/view/doc/operation.cpp +++ b/apps/opencs/view/doc/operation.cpp @@ -1,30 +1,39 @@ #include "operation.hpp" #include +#include +#include #include #include -#include -#include "../../model/doc/document.hpp" +#include "../../model/doc/state.hpp" -void CSVDoc::Operation::updateLabel (int threads) +void CSVDoc::Operation::updateLabel(int threads) { - if (threads==-1 || ((threads==0)!=mStalling)) + if (threads == -1 || ((threads == 0) != mStalling)) { - std::string name ("unknown operation"); + std::string name("Unknown operation"); switch (mType) { - case CSMDoc::State_Saving: name = "saving"; break; - case CSMDoc::State_Verifying: name = "verifying"; break; - case CSMDoc::State_Searching: name = "searching"; break; - case CSMDoc::State_Merging: name = "merging"; break; + case CSMDoc::State_Saving: + name = "Saving"; + break; + case CSMDoc::State_Verifying: + name = "Verifying"; + break; + case CSMDoc::State_Searching: + name = "Searching"; + break; + case CSMDoc::State_Merging: + name = "Merging"; + break; } std::ostringstream stream; - if ((mStalling = (threads<=0))) + if ((mStalling = (threads <= 0))) { stream << name << " (waiting for a free worker thread)"; } @@ -33,15 +42,17 @@ void CSVDoc::Operation::updateLabel (int threads) stream << name << " (%p%)"; } - mProgressBar->setFormat (stream.str().c_str()); + mProgressBar->setFormat(stream.str().c_str()); } } -CSVDoc::Operation::Operation (int type, QWidget* parent) : mType (type), mStalling (false) +CSVDoc::Operation::Operation(int type, QWidget* parent) + : mType(type) + , mStalling(false) { /// \todo Add a cancel button or a pop up menu with a cancel item initWidgets(); - setBarColor( type); + setBarColor(type); updateLabel(); /// \todo assign different progress bar colours to allow the user to distinguish easily between operation types @@ -56,21 +67,22 @@ CSVDoc::Operation::~Operation() void CSVDoc::Operation::initWidgets() { - mProgressBar = new QProgressBar (); + mProgressBar = new QProgressBar(); mAbortButton = new QPushButton("Abort"); mLayout = new QHBoxLayout(); + mLayout->setContentsMargins(8, 4, 8, 4); - mLayout->addWidget (mProgressBar); - mLayout->addWidget (mAbortButton); + mLayout->addWidget(mProgressBar); + mLayout->addWidget(mAbortButton); - connect (mAbortButton, SIGNAL (clicked()), this, SLOT (abortOperation())); + connect(mAbortButton, &QPushButton::clicked, this, qOverload<>(&Operation::abortOperation)); } -void CSVDoc::Operation::setProgress (int current, int max, int threads) +void CSVDoc::Operation::setProgress(int current, int max, int threads) { - updateLabel (threads); - mProgressBar->setRange (0, max); - mProgressBar->setValue (current); + updateLabel(threads); + mProgressBar->setRange(0, max); + mProgressBar->setValue(current); } int CSVDoc::Operation::getType() const @@ -78,76 +90,71 @@ int CSVDoc::Operation::getType() const return mType; } -void CSVDoc::Operation::setBarColor (int type) +void CSVDoc::Operation::setBarColor(int type) { - QString style ="QProgressBar {" - "text-align: center;" - "}" + QString style + = "QProgressBar {" + "text-align: center;" + "border: 1px solid #4e4e4e;" + "}" "QProgressBar::chunk {" - "background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 %1, stop:.50 %2 stop: .51 %3 stop:1 %4);" - "text-align: center;" - "margin: 2px 1px 1p 2px;" + "background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 %1, stop:.50 %2 stop: .51 %3 stop:1 %4);" + "text-align: center;" + "margin: 2px;" "}"; - QString topColor = "#F2F6F8"; - QString bottomColor = "#E0EFF9"; - QString midTopColor = "#D8E1E7"; - QString midBottomColor = "#B5C6D0"; // default gray gloss + QString topColor = "#9e9e9e"; + QString bottomColor = "#919191"; + QString midTopColor = "#848484"; + QString midBottomColor = "#717171"; // default gray // colors inspired by samples from: // http://www.colorzilla.com/gradient-editor/ switch (type) { - case CSMDoc::State_Saving: + case CSMDoc::State_Saving: - topColor = "#FECCB1"; - midTopColor = "#F17432"; - midBottomColor = "#EA5507"; - bottomColor = "#FB955E"; // red gloss #2 - break; + topColor = "#f27d6e"; + midTopColor = "#ee6954"; + midBottomColor = "#f05536"; + bottomColor = "#de511e"; // red + break; - case CSMDoc::State_Searching: + case CSMDoc::State_Searching: - topColor = "#EBF1F6"; - midTopColor = "#ABD3EE"; - midBottomColor = "#89C3EB"; - bottomColor = "#D5EBFB"; //blue gloss #4 - break; + topColor = "#6db3f2"; + midTopColor = "#54a3ee"; + midBottomColor = "#3690f0"; + bottomColor = "#1e69de"; // blue + break; - case CSMDoc::State_Verifying: + case CSMDoc::State_Verifying: - topColor = "#BFD255"; - midTopColor = "#8EB92A"; - midBottomColor = "#72AA00"; - bottomColor = "#9ECB2D"; //green gloss - break; + topColor = "#bfd255"; + midTopColor = "#8eb92a"; + midBottomColor = "#72aa00"; + bottomColor = "#9ecb2d"; // green + break; - case CSMDoc::State_Merging: + case CSMDoc::State_Merging: - topColor = "#F3E2C7"; - midTopColor = "#C19E67"; - midBottomColor = "#B68D4C"; - bottomColor = "#E9D4B3"; //l Brown 3D - break; - - default: - - topColor = "#F2F6F8"; - bottomColor = "#E0EFF9"; - midTopColor = "#D8E1E7"; - midBottomColor = "#B5C6D0"; // gray gloss for undefined ops + topColor = "#d89188"; + midTopColor = "#d07f72"; + midBottomColor = "#cc6d5a"; + bottomColor = "#b86344"; // brown + break; } mProgressBar->setStyleSheet(style.arg(topColor).arg(midTopColor).arg(midBottomColor).arg(bottomColor)); } -QHBoxLayout *CSVDoc::Operation::getLayout() const +QHBoxLayout* CSVDoc::Operation::getLayout() const { return mLayout; } void CSVDoc::Operation::abortOperation() { - emit abortOperation (mType); + emit abortOperation(mType); } diff --git a/apps/opencs/view/doc/operation.hpp b/apps/opencs/view/doc/operation.hpp index 48839fada4c..b5083306035 100644 --- a/apps/opencs/view/doc/operation.hpp +++ b/apps/opencs/view/doc/operation.hpp @@ -6,47 +6,46 @@ class QHBoxLayout; class QPushButton; class QProgressBar; +class QWidget; namespace CSVDoc { class Operation : public QObject { - Q_OBJECT + Q_OBJECT - int mType; - bool mStalling; - QProgressBar *mProgressBar; - QPushButton *mAbortButton; - QHBoxLayout *mLayout; + int mType; + bool mStalling; + QProgressBar* mProgressBar; + QPushButton* mAbortButton; + QHBoxLayout* mLayout; - // not implemented - Operation (const Operation&); - Operation& operator= (const Operation&); + // not implemented + Operation(const Operation&); + Operation& operator=(const Operation&); - void updateLabel (int threads = -1); + void updateLabel(int threads = -1); - public: + public: + Operation(int type, QWidget* parent); + ~Operation() override; - Operation (int type, QWidget *parent); - ~Operation(); + void setProgress(int current, int max, int threads); - void setProgress (int current, int max, int threads); + int getType() const; + QHBoxLayout* getLayout() const; - int getType() const; - QHBoxLayout *getLayout() const; + private: + void setBarColor(int type); + void initWidgets(); - private: + signals: - void setBarColor (int type); - void initWidgets(); + void abortOperation(int type); - signals: + private slots: - void abortOperation (int type); - - private slots: - - void abortOperation(); + void abortOperation(); }; } diff --git a/apps/opencs/view/doc/operations.cpp b/apps/opencs/view/doc/operations.cpp index 9ed77ff9c51..97f47d621ba 100644 --- a/apps/opencs/view/doc/operations.cpp +++ b/apps/opencs/view/doc/operations.cpp @@ -1,68 +1,71 @@ #include "operations.hpp" +#include + +#include #include -#include +#include #include "operation.hpp" -CSVDoc::Operations::Operations() +namespace { - /// \todo make widget height fixed (exactly the height required to display all operations) + constexpr int operationLineHeight = 36; +} - setFeatures (QDockWidget::NoDockWidgetFeatures); +CSVDoc::Operations::Operations() +{ + setFeatures(QDockWidget::NoDockWidgetFeatures); - QWidget *widgetContainer = new QWidget (this); + QWidget* widgetContainer = new QWidget(this); mLayout = new QVBoxLayout; - - widgetContainer->setLayout (mLayout); - setWidget (widgetContainer); - setVisible (false); - setFixedHeight (widgetContainer->height()); - setTitleBarWidget (new QWidget (this)); + mLayout->setContentsMargins(0, 0, 0, 0); + + widgetContainer->setContentsMargins(0, 0, 0, 0); + widgetContainer->setLayout(mLayout); + setWidget(widgetContainer); + setVisible(false); + setFixedHeight(operationLineHeight); + setTitleBarWidget(new QWidget(this)); } -void CSVDoc::Operations::setProgress (int current, int max, int type, int threads) +void CSVDoc::Operations::setProgress(int current, int max, int type, int threads) { - for (std::vector::iterator iter (mOperations.begin()); iter!=mOperations.end(); ++iter) - if ((*iter)->getType()==type) + for (std::vector::iterator iter(mOperations.begin()); iter != mOperations.end(); ++iter) + if ((*iter)->getType() == type) { - (*iter)->setProgress (current, max, threads); + (*iter)->setProgress(current, max, threads); return; } - int oldCount = static_cast(mOperations.size()); - int newCount = oldCount + 1; + Operation* operation = new Operation(type, this); + connect(operation, qOverload(&Operation::abortOperation), this, &Operations::abortOperation); - Operation *operation = new Operation (type, this); - connect (operation, SIGNAL (abortOperation (int)), this, SIGNAL (abortOperation (int))); + mLayout->addLayout(operation->getLayout()); + mOperations.push_back(operation); + operation->setProgress(current, max, threads); - mLayout->addLayout (operation->getLayout()); - mOperations.push_back (operation); - operation->setProgress (current, max, threads); + int newCount = static_cast(mOperations.size()); + setFixedHeight(operationLineHeight * newCount); - if ( oldCount > 0) - setFixedHeight (height()/oldCount * newCount); - - setVisible (true); + setVisible(true); } -void CSVDoc::Operations::quitOperation (int type) +void CSVDoc::Operations::quitOperation(int type) { - for (std::vector::iterator iter (mOperations.begin()); iter!=mOperations.end(); ++iter) - if ((*iter)->getType()==type) + for (std::vector::iterator iter(mOperations.begin()); iter != mOperations.end(); ++iter) + if ((*iter)->getType() == type) { - int oldCount = static_cast(mOperations.size()); - int newCount = oldCount - 1; - - mLayout->removeItem ((*iter)->getLayout()); + mLayout->removeItem((*iter)->getLayout()); (*iter)->deleteLater(); - mOperations.erase (iter); + mOperations.erase(iter); - if (oldCount > 1) - setFixedHeight (height() / oldCount * newCount); + int newCount = static_cast(mOperations.size()); + if (newCount > 0) + setFixedHeight(operationLineHeight * newCount); else - setVisible (false); + setVisible(false); break; } diff --git a/apps/opencs/view/doc/operations.hpp b/apps/opencs/view/doc/operations.hpp index 71c595f66b1..110ed3f7283 100644 --- a/apps/opencs/view/doc/operations.hpp +++ b/apps/opencs/view/doc/operations.hpp @@ -13,28 +13,27 @@ namespace CSVDoc class Operations : public QDockWidget { - Q_OBJECT + Q_OBJECT - QVBoxLayout *mLayout; - std::vector mOperations; + QVBoxLayout* mLayout; + std::vector mOperations; - // not implemented - Operations (const Operations&); - Operations& operator= (const Operations&); + // not implemented + Operations(const Operations&); + Operations& operator=(const Operations&); - public: + public: + Operations(); - Operations(); + void setProgress(int current, int max, int type, int threads); + ///< Implicitly starts the operation, if it is not running already. - void setProgress (int current, int max, int type, int threads); - ///< Implicitly starts the operation, if it is not running already. + void quitOperation(int type); + ///< Calling this function for an operation that is not running is a no-op. - void quitOperation (int type); - ///< Calling this function for an operation that is not running is a no-op. + signals: - signals: - - void abortOperation (int type); + void abortOperation(int type); }; } diff --git a/apps/opencs/view/doc/runlogsubview.cpp b/apps/opencs/view/doc/runlogsubview.cpp index 2b7182228b4..7affd73810e 100644 --- a/apps/opencs/view/doc/runlogsubview.cpp +++ b/apps/opencs/view/doc/runlogsubview.cpp @@ -1,19 +1,22 @@ #include "runlogsubview.hpp" +#include "../../model/doc/document.hpp" + +#include + #include -CSVDoc::RunLogSubView::RunLogSubView (const CSMWorld::UniversalId& id, - CSMDoc::Document& document) -: SubView (id) +CSVDoc::RunLogSubView::RunLogSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) + : SubView(id) { - QTextEdit *edit = new QTextEdit (this); - edit->setDocument (document.getRunLog()); - edit->setReadOnly (true); + QTextEdit* edit = new QTextEdit(this); + edit->setDocument(document.getRunLog()); + edit->setReadOnly(true); - setWidget (edit); + setWidget(edit); } -void CSVDoc::RunLogSubView::setEditLock (bool locked) +void CSVDoc::RunLogSubView::setEditLock(bool locked) { // ignored since this SubView does not have editing } diff --git a/apps/opencs/view/doc/runlogsubview.hpp b/apps/opencs/view/doc/runlogsubview.hpp index e7b490fff0e..8ff9cd7a5ad 100644 --- a/apps/opencs/view/doc/runlogsubview.hpp +++ b/apps/opencs/view/doc/runlogsubview.hpp @@ -3,17 +3,23 @@ #include "subview.hpp" +#include + +namespace CSMDoc +{ + class Document; +} + namespace CSVDoc { class RunLogSubView : public SubView { - Q_OBJECT - - public: + Q_OBJECT - RunLogSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); + public: + RunLogSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); - void setEditLock (bool locked) override; + void setEditLock(bool locked) override; }; } diff --git a/apps/opencs/view/doc/sizehint.cpp b/apps/opencs/view/doc/sizehint.cpp index 038bd9e4d10..8b907ead097 100644 --- a/apps/opencs/view/doc/sizehint.cpp +++ b/apps/opencs/view/doc/sizehint.cpp @@ -1,17 +1,16 @@ #include "sizehint.hpp" -CSVDoc::SizeHintWidget::SizeHintWidget(QWidget *parent) : QWidget(parent) -{} - -CSVDoc::SizeHintWidget::~SizeHintWidget() -{} +CSVDoc::SizeHintWidget::SizeHintWidget(QWidget* parent) + : QWidget(parent) +{ +} QSize CSVDoc::SizeHintWidget::sizeHint() const { return mSize; } -void CSVDoc::SizeHintWidget::setSizeHint(const QSize &size) +void CSVDoc::SizeHintWidget::setSizeHint(const QSize& size) { mSize = size; } diff --git a/apps/opencs/view/doc/sizehint.hpp b/apps/opencs/view/doc/sizehint.hpp index 14ec7b1865e..a0296a51ec9 100644 --- a/apps/opencs/view/doc/sizehint.hpp +++ b/apps/opencs/view/doc/sizehint.hpp @@ -1,21 +1,21 @@ #ifndef CSV_DOC_SIZEHINT_H #define CSV_DOC_SIZEHINT_H -#include #include +#include namespace CSVDoc { class SizeHintWidget : public QWidget { - QSize mSize; + QSize mSize; - public: - SizeHintWidget(QWidget *parent = nullptr); - ~SizeHintWidget(); + public: + SizeHintWidget(QWidget* parent = nullptr); + ~SizeHintWidget() override = default; - QSize sizeHint() const override; - void setSizeHint(const QSize &size); + QSize sizeHint() const override; + void setSizeHint(const QSize& size); }; } diff --git a/apps/opencs/view/doc/startup.cpp b/apps/opencs/view/doc/startup.cpp index 3a1950a6e33..c6e109355f6 100644 --- a/apps/opencs/view/doc/startup.cpp +++ b/apps/opencs/view/doc/startup.cpp @@ -1,126 +1,131 @@ #include "startup.hpp" -#include -#include -#include -#include -#include +#include + #include -#include +#include +#include #include +#include #include +#include #include +#include -QPushButton *CSVDoc::StartupDialogue::addButton (const QString& label, const QIcon& icon) +QPushButton* CSVDoc::StartupDialogue::addButton(const QString& label, const QString& icon) { int column = mColumn--; - QPushButton *button = new QPushButton (this); + QPushButton* button = new QPushButton(this); - button->setIcon (QIcon (icon)); + button->setIcon(Misc::ScalableIcon::load(icon)); - button->setSizePolicy (QSizePolicy (QSizePolicy::Preferred, QSizePolicy::Preferred)); + button->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred)); - mLayout->addWidget (button, 0, column); + mLayout->addWidget(button, 0, column); - mLayout->addWidget (new QLabel (label, this), 1, column, Qt::AlignCenter); + mLayout->addWidget(new QLabel(label, this), 1, column, Qt::AlignCenter); - int width = mLayout->itemAtPosition (1, column)->widget()->sizeHint().width(); + int width = mLayout->itemAtPosition(1, column)->widget()->sizeHint().width(); - if (width>mWidth) + if (width > mWidth) mWidth = width; return button; } - -QWidget *CSVDoc::StartupDialogue::createButtons() +QWidget* CSVDoc::StartupDialogue::createButtons() { - QWidget *widget = new QWidget (this); + QWidget* widget = new QWidget(this); - mLayout = new QGridLayout (widget); + mLayout = new QGridLayout(widget); /// \todo add icons - QPushButton *loadDocument = addButton ("Edit A Content File", QIcon (":startup/edit-content")); - connect (loadDocument, SIGNAL (clicked()), this, SIGNAL (loadDocument())); + QPushButton* loadDocument = addButton("Edit A Content File", ":startup/edit-content"); + connect(loadDocument, &QPushButton::clicked, this, &StartupDialogue::loadDocument); - QPushButton *createAddon = addButton ("Create A New Addon", QIcon (":startup/create-addon")); - connect (createAddon, SIGNAL (clicked()), this, SIGNAL (createAddon())); + QPushButton* createAddon = addButton("Create A New Addon", ":startup/create-addon"); + connect(createAddon, &QPushButton::clicked, this, &StartupDialogue::createAddon); - QPushButton *createGame = addButton ("Create A New Game", QIcon (":startup/create-game")); - connect (createGame, SIGNAL (clicked()), this, SIGNAL (createGame())); + QPushButton* createGame = addButton("Create A New Game", ":startup/create-game"); + connect(createGame, &QPushButton::clicked, this, &StartupDialogue::createGame); - for (int i=0; i<3; ++i) - mLayout->setColumnMinimumWidth (i, mWidth); + for (int i = 0; i < 3; ++i) + mLayout->setColumnMinimumWidth(i, mWidth); - mLayout->setRowMinimumHeight (0, mWidth); + mLayout->setRowMinimumHeight(0, mWidth); - mLayout->setSizeConstraint (QLayout::SetMinimumSize); - mLayout->setHorizontalSpacing (32); + mLayout->setSizeConstraint(QLayout::SetMinimumSize); + mLayout->setHorizontalSpacing(32); - mLayout->setContentsMargins (16, 16, 16, 8); + mLayout->setContentsMargins(16, 16, 16, 8); - loadDocument->setIconSize (QSize (mWidth, mWidth)); - createGame->setIconSize (QSize (mWidth, mWidth)); - createAddon->setIconSize (QSize (mWidth, mWidth)); + loadDocument->setIconSize(QSize(mWidth, mWidth)); + createGame->setIconSize(QSize(mWidth, mWidth)); + createAddon->setIconSize(QSize(mWidth, mWidth)); - widget->setLayout (mLayout); + widget->setLayout(mLayout); return widget; } -QWidget *CSVDoc::StartupDialogue::createTools() +QWidget* CSVDoc::StartupDialogue::createTools() { - QWidget *widget = new QWidget (this); + QWidget* widget = new QWidget(this); - QHBoxLayout *layout = new QHBoxLayout (widget); - layout->setDirection (QBoxLayout::RightToLeft); - layout->setContentsMargins (4, 4, 4, 4); + QHBoxLayout* layout = new QHBoxLayout(widget); + layout->setDirection(QBoxLayout::RightToLeft); + layout->setContentsMargins(4, 4, 4, 4); - QPushButton *config = new QPushButton (widget); + QPushButton* config = new QPushButton(widget); - config->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); - config->setIcon (QIcon (":startup/configure")); - config->setToolTip ("Open user settings"); + config->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + config->setIcon(Misc::ScalableIcon::load(":startup/configure")); + config->setToolTip("Open user settings"); - layout->addWidget (config); + layout->addWidget(config); - layout->addWidget (new QWidget, 1); // dummy widget; stops buttons from taking all the space + layout->addWidget(new QWidget, 1); // dummy widget; stops buttons from taking all the space - widget->setLayout (layout); + widget->setLayout(layout); - connect (config, SIGNAL (clicked()), this, SIGNAL (editConfig())); + connect(config, &QPushButton::clicked, this, &StartupDialogue::editConfig); return widget; } -CSVDoc::StartupDialogue::StartupDialogue() : mWidth (0), mColumn (2) +CSVDoc::StartupDialogue::StartupDialogue() + : mWidth(0) + , mColumn(2) { - setWindowTitle ("OpenMW-CS"); + setWindowTitle("OpenMW-CS"); - QVBoxLayout *layout = new QVBoxLayout (this); + QVBoxLayout* layout = new QVBoxLayout(this); - layout->setContentsMargins (0, 0, 0, 0); + layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget (createButtons()); - layout->addWidget (createTools()); + layout->addWidget(createButtons()); + layout->addWidget(createTools()); /// \todo remove this label once we are feature complete and convinced that this thing is /// working properly. - QLabel *warning = new QLabel ("WARNING: OpenMW-CS is in alpha stage.

The editor is not feature complete and not sufficiently tested.
In theory your data should be safe. But we strongly advise to make backups regularly if you are working with live data.
"); + QLabel* warning = new QLabel( + "WARNING: OpenMW-CS is in alpha stage.

The editor is not feature complete and not " + "sufficiently tested.
In theory your data should be safe. But we strongly advise to make backups regularly " + "if you are working with live data.
"); QFont font; - font.setPointSize (12); - font.setBold (true); + font.setPointSize(12); + font.setBold(true); - warning->setFont (font); - warning->setWordWrap (true); + warning->setFont(font); + warning->setWordWrap(true); - layout->addWidget (warning, 1); + layout->addWidget(warning, 1); - setLayout (layout); + setLayout(layout); QRect scr = QGuiApplication::primaryScreen()->geometry(); QRect rect = geometry(); - move (scr.center().x() - rect.center().x(), scr.center().y() - rect.center().y()); + move(scr.center().x() - rect.center().x(), scr.center().y() - rect.center().y()); } diff --git a/apps/opencs/view/doc/startup.hpp b/apps/opencs/view/doc/startup.hpp index f059a44e5ca..f2cccfcd384 100644 --- a/apps/opencs/view/doc/startup.hpp +++ b/apps/opencs/view/doc/startup.hpp @@ -15,31 +15,29 @@ namespace CSVDoc { Q_OBJECT - private: + private: + int mWidth; + int mColumn; + QGridLayout* mLayout; - int mWidth; - int mColumn; - QGridLayout *mLayout; + QPushButton* addButton(const QString& label, const QString& icon); - QPushButton *addButton (const QString& label, const QIcon& icon); + QWidget* createButtons(); - QWidget *createButtons(); + QWidget* createTools(); - QWidget *createTools(); + public: + StartupDialogue(); - public: + signals: - StartupDialogue(); + void createGame(); - signals: + void createAddon(); - void createGame(); + void loadDocument(); - void createAddon(); - - void loadDocument(); - - void editConfig(); + void editConfig(); }; } diff --git a/apps/opencs/view/doc/subview.cpp b/apps/opencs/view/doc/subview.cpp index 82cbe835e70..c6193029136 100644 --- a/apps/opencs/view/doc/subview.cpp +++ b/apps/opencs/view/doc/subview.cpp @@ -1,31 +1,30 @@ #include "subview.hpp" -#include "view.hpp" - -#include #include #include -bool CSVDoc::SubView::event (QEvent *event) +#include + +bool CSVDoc::SubView::event(QEvent* event) { - if (event->type()==QEvent::ShortcutOverride) + if (event->type() == QEvent::ShortcutOverride) { - QKeyEvent *keyEvent = static_cast (event); + QKeyEvent* keyEvent = static_cast(event); - if (keyEvent->key()==Qt::Key_W && keyEvent->modifiers()==(Qt::ShiftModifier | Qt::ControlModifier)) + if (keyEvent->key() == Qt::Key_W && keyEvent->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) emit closeRequest(); - return true; + return true; } - return QDockWidget::event (event); + return QDockWidget::event(event); } -CSVDoc::SubView::SubView (const CSMWorld::UniversalId& id) - : mUniversalId (id) +CSVDoc::SubView::SubView(const CSMWorld::UniversalId& id) + : mUniversalId(id) { /// \todo add a button to the title bar that clones this sub view - setWindowTitle (QString::fromUtf8 (mUniversalId.toString().c_str())); + setWindowTitle(QString::fromUtf8(mUniversalId.toString().c_str())); setAttribute(Qt::WA_DeleteOnClose); } @@ -34,20 +33,20 @@ CSMWorld::UniversalId CSVDoc::SubView::getUniversalId() const return mUniversalId; } -void CSVDoc::SubView::setStatusBar (bool show) {} +void CSVDoc::SubView::setStatusBar(bool show) {} -void CSVDoc::SubView::useHint (const std::string& hint) {} +void CSVDoc::SubView::useHint(const std::string& hint) {} -void CSVDoc::SubView::setUniversalId (const CSMWorld::UniversalId& id) +void CSVDoc::SubView::setUniversalId(const CSMWorld::UniversalId& id) { mUniversalId = id; - setWindowTitle (QString::fromUtf8(mUniversalId.toString().c_str())); - emit universalIdChanged (mUniversalId); + setWindowTitle(QString::fromUtf8(mUniversalId.toString().c_str())); + emit universalIdChanged(mUniversalId); } -void CSVDoc::SubView::closeEvent (QCloseEvent *event) +void CSVDoc::SubView::closeEvent(QCloseEvent* event) { - emit updateSubViewIndices (this); + emit updateSubViewIndices(this); } std::string CSVDoc::SubView::getTitle() const @@ -57,5 +56,5 @@ std::string CSVDoc::SubView::getTitle() const void CSVDoc::SubView::closeRequest() { - emit closeRequest (this); + emit closeRequest(this); } diff --git a/apps/opencs/view/doc/subview.hpp b/apps/opencs/view/doc/subview.hpp index ca9ca82252e..d8b30cce6ef 100644 --- a/apps/opencs/view/doc/subview.hpp +++ b/apps/opencs/view/doc/subview.hpp @@ -1,76 +1,66 @@ #ifndef CSV_DOC_SUBVIEW_H #define CSV_DOC_SUBVIEW_H -#include "../../model/doc/document.hpp" - #include "../../model/world/universalid.hpp" -#include "subviewfactory.hpp" - #include -class QUndoStack; +#include -namespace CSMWorld -{ - class Data; -} +class QCloseEvent; +class QEvent; +class QObject; namespace CSVDoc { - class View; - class SubView : public QDockWidget { - Q_OBJECT - - CSMWorld::UniversalId mUniversalId; - - // not implemented - SubView (const SubView&); - SubView& operator= (SubView&); - - protected: + Q_OBJECT - void setUniversalId(const CSMWorld::UniversalId& id); + CSMWorld::UniversalId mUniversalId; - bool event (QEvent *event) override; + // not implemented + SubView(const SubView&); + SubView& operator=(SubView&); - public: + protected: + void setUniversalId(const CSMWorld::UniversalId& id); - SubView (const CSMWorld::UniversalId& id); + bool event(QEvent* event) override; - CSMWorld::UniversalId getUniversalId() const; + public: + SubView(const CSMWorld::UniversalId& id); - virtual void setEditLock (bool locked) = 0; + CSMWorld::UniversalId getUniversalId() const; - virtual void setStatusBar (bool show); - ///< Default implementation: ignored + virtual void setEditLock(bool locked) = 0; - virtual void useHint (const std::string& hint); - ///< Default implementation: ignored + virtual void setStatusBar(bool show); + ///< Default implementation: ignored - virtual std::string getTitle() const; + virtual void useHint(const std::string& hint); + ///< Default implementation: ignored - private: + virtual std::string getTitle() const; - void closeEvent (QCloseEvent *event) override; + private: + void closeEvent(QCloseEvent* event) override; - signals: + signals: - void focusId (const CSMWorld::UniversalId& universalId, const std::string& hint); + void focusId(const CSMWorld::UniversalId& universalId, const std::string& hint); - void closeRequest (SubView *subView); + void closeRequest(SubView* subView); - void updateTitle(); + void updateTitle(); - void updateSubViewIndices (SubView *view = nullptr); + void updateSubViewIndices(SubView* view = nullptr); - void universalIdChanged (const CSMWorld::UniversalId& universalId); + void universalIdChanged(const CSMWorld::UniversalId& universalId); - protected slots: + protected slots: - void closeRequest(); + void closeRequest(); }; } diff --git a/apps/opencs/view/doc/subviewfactory.cpp b/apps/opencs/view/doc/subviewfactory.cpp index 82a8aeb0540..8330a2c8696 100644 --- a/apps/opencs/view/doc/subviewfactory.cpp +++ b/apps/opencs/view/doc/subviewfactory.cpp @@ -1,37 +1,33 @@ #include "subviewfactory.hpp" #include - #include +#include +#include +#include -CSVDoc::SubViewFactoryBase::SubViewFactoryBase() {} - -CSVDoc::SubViewFactoryBase::~SubViewFactoryBase() {} - - -CSVDoc::SubViewFactoryManager::SubViewFactoryManager() {} +#include CSVDoc::SubViewFactoryManager::~SubViewFactoryManager() { - for (std::map::iterator iter (mSubViewFactories.begin()); - iter!=mSubViewFactories.end(); ++iter) + for (std::map::iterator iter(mSubViewFactories.begin()); + iter != mSubViewFactories.end(); ++iter) delete iter->second; } -void CSVDoc::SubViewFactoryManager::add (const CSMWorld::UniversalId::Type& id, SubViewFactoryBase *factory) +void CSVDoc::SubViewFactoryManager::add(const CSMWorld::UniversalId::Type& id, SubViewFactoryBase* factory) { - assert (mSubViewFactories.find (id)==mSubViewFactories.end()); + assert(mSubViewFactories.find(id) == mSubViewFactories.end()); - mSubViewFactories.insert (std::make_pair (id, factory)); + mSubViewFactories.insert(std::make_pair(id, factory)); } -CSVDoc::SubView *CSVDoc::SubViewFactoryManager::makeSubView (const CSMWorld::UniversalId& id, - CSMDoc::Document& document) +CSVDoc::SubView* CSVDoc::SubViewFactoryManager::makeSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) { - std::map::iterator iter = mSubViewFactories.find (id.getType()); + std::map::iterator iter = mSubViewFactories.find(id.getType()); - if (iter==mSubViewFactories.end()) - throw std::runtime_error ("Failed to create a sub view for: " + id.toString()); + if (iter == mSubViewFactories.end()) + throw std::runtime_error("Failed to create a sub view for: " + id.toString()); - return iter->second->makeSubView (id, document); + return iter->second->makeSubView(id, document); } diff --git a/apps/opencs/view/doc/subviewfactory.hpp b/apps/opencs/view/doc/subviewfactory.hpp index 1f7c154806b..d3630c5e96e 100644 --- a/apps/opencs/view/doc/subviewfactory.hpp +++ b/apps/opencs/view/doc/subviewfactory.hpp @@ -16,39 +16,31 @@ namespace CSVDoc class SubViewFactoryBase { - // not implemented - SubViewFactoryBase (const SubViewFactoryBase&); - SubViewFactoryBase& operator= (const SubViewFactoryBase&); - - public: - - SubViewFactoryBase(); - - virtual ~SubViewFactoryBase(); - - virtual SubView *makeSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) = 0; - ///< The ownership of the returned sub view is not transferred. + public: + SubViewFactoryBase() = default; + SubViewFactoryBase(const SubViewFactoryBase&) = delete; + SubViewFactoryBase& operator=(const SubViewFactoryBase&) = delete; + virtual ~SubViewFactoryBase() = default; + + virtual SubView* makeSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) = 0; + ///< The ownership of the returned sub view is not transferred. }; class SubViewFactoryManager { - std::map mSubViewFactories; - - // not implemented - SubViewFactoryManager (const SubViewFactoryManager&); - SubViewFactoryManager& operator= (const SubViewFactoryManager&); - - public: - - SubViewFactoryManager(); + std::map mSubViewFactories; - ~SubViewFactoryManager(); + public: + SubViewFactoryManager() = default; + SubViewFactoryManager(const SubViewFactoryManager&) = delete; + SubViewFactoryManager& operator=(const SubViewFactoryManager&) = delete; + ~SubViewFactoryManager(); - void add (const CSMWorld::UniversalId::Type& id, SubViewFactoryBase *factory); - ///< The ownership of \a factory is transferred to this. + void add(const CSMWorld::UniversalId::Type& id, SubViewFactoryBase* factory); + ///< The ownership of \a factory is transferred to this. - SubView *makeSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); - ///< The ownership of the returned sub view is not transferred. + SubView* makeSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); + ///< The ownership of the returned sub view is not transferred. }; } diff --git a/apps/opencs/view/doc/subviewfactoryimp.hpp b/apps/opencs/view/doc/subviewfactoryimp.hpp index 152b443a343..40990779a4d 100644 --- a/apps/opencs/view/doc/subviewfactoryimp.hpp +++ b/apps/opencs/view/doc/subviewfactoryimp.hpp @@ -7,44 +7,41 @@ namespace CSVDoc { - template + template class SubViewFactory : public SubViewFactoryBase { - public: - - CSVDoc::SubView *makeSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) override; + public: + CSVDoc::SubView* makeSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) override; }; - template - CSVDoc::SubView *SubViewFactory::makeSubView (const CSMWorld::UniversalId& id, - CSMDoc::Document& document) + template + CSVDoc::SubView* SubViewFactory::makeSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) { - return new SubViewT (id, document); + return new SubViewT(id, document); } - - template + template class SubViewFactoryWithCreator : public SubViewFactoryBase { - bool mSorting; - - public: + bool mSorting; - SubViewFactoryWithCreator (bool sorting = true); + public: + SubViewFactoryWithCreator(bool sorting = true); - CSVDoc::SubView *makeSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) override; + CSVDoc::SubView* makeSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) override; }; - template - SubViewFactoryWithCreator::SubViewFactoryWithCreator (bool sorting) - : mSorting (sorting) - {} + template + SubViewFactoryWithCreator::SubViewFactoryWithCreator(bool sorting) + : mSorting(sorting) + { + } - template - CSVDoc::SubView *SubViewFactoryWithCreator::makeSubView ( + template + CSVDoc::SubView* SubViewFactoryWithCreator::makeSubView( const CSMWorld::UniversalId& id, CSMDoc::Document& document) { - return new SubViewT (id, document, CreatorFactoryT(), mSorting); + return new SubViewT(id, document, CreatorFactoryT(), mSorting); } } diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 4514cfb585f..f6ef02ad6d1 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -1,46 +1,65 @@ #include "view.hpp" -#include -#include - +#include #include #include #include -#include -#include -#include -#include +#include #include -#include -#include #include -#include + +#include +#include +#include #include "../../model/doc/document.hpp" -#include "../../model/prefs/state.hpp" +#include "../../model/doc/state.hpp" + #include "../../model/prefs/shortcut.hpp" +#include "../../model/prefs/state.hpp" #include "../../model/world/idtable.hpp" -#include "../world/subviews.hpp" +#include "../world/dialoguesubview.hpp" #include "../world/scenesubview.hpp" +#include "../world/scriptsubview.hpp" +#include "../world/subviews.hpp" #include "../world/tablesubview.hpp" #include "../tools/subviews.hpp" +#include +#include +#include +#include +#include + +#include #include +#include +#include #include -#include "viewmanager.hpp" -#include "operations.hpp" -#include "subview.hpp" #include "globaldebugprofilemenu.hpp" +#include "operations.hpp" #include "runlogsubview.hpp" +#include "subview.hpp" #include "subviewfactoryimp.hpp" +#include "viewmanager.hpp" -void CSVDoc::View::closeEvent (QCloseEvent *event) +QRect desktopRect() { - if (!mViewManager.closeRequest (this)) + QRegion virtualGeometry; + for (auto screen : QGuiApplication::screens()) + { + virtualGeometry += screen->geometry(); + } + return virtualGeometry.boundingRect(); +} + +void CSVDoc::View::closeEvent(QCloseEvent* event) +{ + if (!mViewManager.closeRequest(this)) event->ignore(); else { @@ -51,48 +70,52 @@ void CSVDoc::View::closeEvent (QCloseEvent *event) void CSVDoc::View::setupFileMenu() { - QMenu *file = menuBar()->addMenu (tr ("File")); + QMenu* file = menuBar()->addMenu(tr("File")); - QAction* newGame = createMenuEntry("New Game", ":./menu-new-game.png", file, "document-file-newgame"); - connect (newGame, SIGNAL (triggered()), this, SIGNAL (newGameRequest())); + QAction* newGame = createMenuEntry("New Game", ":menu-new-game", file, "document-file-newgame"); + connect(newGame, &QAction::triggered, this, &View::newGameRequest); - QAction* newAddon = createMenuEntry("New Addon", ":./menu-new-addon.png", file, "document-file-newaddon"); - connect (newAddon, SIGNAL (triggered()), this, SIGNAL (newAddonRequest())); + QAction* newAddon = createMenuEntry("New Addon", ":menu-new-addon", file, "document-file-newaddon"); + connect(newAddon, &QAction::triggered, this, &View::newAddonRequest); - QAction* open = createMenuEntry("Open", ":./menu-open.png", file, "document-file-open"); - connect (open, SIGNAL (triggered()), this, SIGNAL (loadDocumentRequest())); + QAction* open = createMenuEntry("Open", ":menu-open", file, "document-file-open"); + connect(open, &QAction::triggered, this, &View::loadDocumentRequest); - QAction* save = createMenuEntry("Save", ":./menu-save.png", file, "document-file-save"); - connect (save, SIGNAL (triggered()), this, SLOT (save())); + QAction* save = createMenuEntry("Save", ":menu-save", file, "document-file-save"); + connect(save, &QAction::triggered, this, &View::save); mSave = save; - QAction* verify = createMenuEntry("Verify", ":./menu-verify.png", file, "document-file-verify"); - connect (verify, SIGNAL (triggered()), this, SLOT (verify())); + file->addSeparator(); + + QAction* verify = createMenuEntry("Verify", ":menu-verify", file, "document-file-verify"); + connect(verify, &QAction::triggered, this, &View::verify); mVerify = verify; - QAction* merge = createMenuEntry("Merge", ":./menu-merge.png", file, "document-file-merge"); - connect (merge, SIGNAL (triggered()), this, SLOT (merge())); + QAction* merge = createMenuEntry("Merge", ":menu-merge", file, "document-file-merge"); + connect(merge, &QAction::triggered, this, &View::merge); mMerge = merge; - QAction* loadErrors = createMenuEntry("Error Log", ":./error-log.png", file, "document-file-errorlog"); - connect (loadErrors, SIGNAL (triggered()), this, SLOT (loadErrorLog())); + QAction* loadErrors = createMenuEntry("Error Log", ":error-log", file, "document-file-errorlog"); + connect(loadErrors, &QAction::triggered, this, &View::loadErrorLog); QAction* meta = createMenuEntry(CSMWorld::UniversalId::Type_MetaDatas, file, "document-file-metadata"); - connect (meta, SIGNAL (triggered()), this, SLOT (addMetaDataSubView())); + connect(meta, &QAction::triggered, this, &View::addMetaDataSubView); + + file->addSeparator(); - QAction* close = createMenuEntry("Close", ":./menu-close.png", file, "document-file-close"); - connect (close, SIGNAL (triggered()), this, SLOT (close())); + QAction* close = createMenuEntry("Close", ":menu-close", file, "document-file-close"); + connect(close, &QAction::triggered, this, &View::close); - QAction* exit = createMenuEntry("Exit", ":./menu-exit.png", file, "document-file-exit"); - connect (exit, SIGNAL (triggered()), this, SLOT (exit())); + QAction* exit = createMenuEntry("Exit", ":menu-exit", file, "document-file-exit"); + connect(exit, &QAction::triggered, this, &View::exit); - connect (this, SIGNAL(exitApplicationRequest(CSVDoc::View *)), &mViewManager, SLOT(exitApplication(CSVDoc::View *))); + connect(this, &View::exitApplicationRequest, &mViewManager, &ViewManager::exitApplication); } namespace { - void updateUndoRedoAction(QAction *action, const std::string &settingsKey) + void updateUndoRedoAction(QAction* action, const std::string& settingsKey) { QKeySequence seq; CSMPrefs::State::get().getShortcutManager().getSequence(settingsKey, seq); @@ -113,241 +136,261 @@ void CSVDoc::View::redoActionChanged() void CSVDoc::View::setupEditMenu() { - QMenu *edit = menuBar()->addMenu (tr ("Edit")); + QMenu* edit = menuBar()->addMenu(tr("Edit")); - mUndo = mDocument->getUndoStack().createUndoAction (this, tr("Undo")); + mUndo = mDocument->getUndoStack().createUndoAction(this, tr("Undo")); setupShortcut("document-edit-undo", mUndo); - connect(mUndo, SIGNAL (changed ()), this, SLOT (undoActionChanged ())); - mUndo->setIcon(QIcon(QString::fromStdString(":./menu-undo.png"))); - edit->addAction (mUndo); + connect(mUndo, &QAction::changed, this, &View::undoActionChanged); + mUndo->setIcon(Misc::ScalableIcon::load(":menu-undo")); + edit->addAction(mUndo); - mRedo = mDocument->getUndoStack().createRedoAction (this, tr("Redo")); - connect(mRedo, SIGNAL (changed ()), this, SLOT (redoActionChanged ())); + mRedo = mDocument->getUndoStack().createRedoAction(this, tr("Redo")); + connect(mRedo, &QAction::changed, this, &View::redoActionChanged); setupShortcut("document-edit-redo", mRedo); - mRedo->setIcon(QIcon(QString::fromStdString(":./menu-redo.png"))); - edit->addAction (mRedo); + mRedo->setIcon(Misc::ScalableIcon::load(":menu-redo")); + edit->addAction(mRedo); - QAction* userSettings = createMenuEntry("Preferences", ":./menu-preferences.png", edit, "document-edit-preferences"); - connect (userSettings, SIGNAL (triggered()), this, SIGNAL (editSettingsRequest())); + QAction* userSettings = createMenuEntry("Preferences", ":menu-preferences", edit, "document-edit-preferences"); + connect(userSettings, &QAction::triggered, this, &View::editSettingsRequest); QAction* search = createMenuEntry(CSMWorld::UniversalId::Type_Search, edit, "document-edit-search"); - connect (search, SIGNAL (triggered()), this, SLOT (addSearchSubView())); + connect(search, &QAction::triggered, this, &View::addSearchSubView); } void CSVDoc::View::setupViewMenu() { - QMenu *view = menuBar()->addMenu (tr ("View")); + QMenu* view = menuBar()->addMenu(tr("View")); - QAction *newWindow = createMenuEntry("New View", ":./menu-new-window.png", view, "document-view-newview"); - connect (newWindow, SIGNAL (triggered()), this, SLOT (newView())); + QAction* newWindow = createMenuEntry("New View", ":menu-new-window", view, "document-view-newview"); + connect(newWindow, &QAction::triggered, this, &View::newView); - mShowStatusBar = createMenuEntry("Toggle Status Bar", ":./menu-status-bar.png", view, "document-view-statusbar"); - connect (mShowStatusBar, SIGNAL (toggled (bool)), this, SLOT (toggleShowStatusBar (bool))); - mShowStatusBar->setCheckable (true); - mShowStatusBar->setChecked (CSMPrefs::get()["Windows"]["show-statusbar"].isTrue()); + mShowStatusBar = createMenuEntry("Toggle Status Bar", ":menu-status-bar", view, "document-view-statusbar"); + connect(mShowStatusBar, &QAction::toggled, this, &View::toggleShowStatusBar); + mShowStatusBar->setCheckable(true); + mShowStatusBar->setChecked(CSMPrefs::get()["Windows"]["show-statusbar"].isTrue()); - view->addAction (mShowStatusBar); + view->addAction(mShowStatusBar); - QAction *filters = createMenuEntry(CSMWorld::UniversalId::Type_Filters, view, "document-mechanics-filters"); - connect (filters, SIGNAL (triggered()), this, SLOT (addFiltersSubView())); + QAction* filters = createMenuEntry(CSMWorld::UniversalId::Type_Filters, view, "document-mechanics-filters"); + connect(filters, &QAction::triggered, this, &View::addFiltersSubView); } void CSVDoc::View::setupWorldMenu() { - QMenu *world = menuBar()->addMenu (tr ("World")); + QMenu* world = menuBar()->addMenu(tr("World")); - QAction* regions = createMenuEntry(CSMWorld::UniversalId::Type_Regions, world, "document-world-regions"); - connect (regions, SIGNAL (triggered()), this, SLOT (addRegionsSubView())); + QAction* referenceables + = createMenuEntry(CSMWorld::UniversalId::Type_Referenceables, world, "document-world-referencables"); + connect(referenceables, &QAction::triggered, this, &View::addReferenceablesSubView); - QAction* cells = createMenuEntry(CSMWorld::UniversalId::Type_Cells, world, "document-world-cells"); - connect (cells, SIGNAL (triggered()), this, SLOT (addCellsSubView())); + QAction* references = createMenuEntry(CSMWorld::UniversalId::Type_References, world, "document-world-references"); + connect(references, &QAction::triggered, this, &View::addReferencesSubView); - QAction* referenceables = createMenuEntry(CSMWorld::UniversalId::Type_Referenceables, world, "document-world-referencables"); - connect (referenceables, SIGNAL (triggered()), this, SLOT (addReferenceablesSubView())); + world->addSeparator(); - QAction* references = createMenuEntry(CSMWorld::UniversalId::Type_References, world, "document-world-references"); - connect (references, SIGNAL (triggered()), this, SLOT (addReferencesSubView())); + QAction* cells = createMenuEntry(CSMWorld::UniversalId::Type_Cells, world, "document-world-cells"); + connect(cells, &QAction::triggered, this, &View::addCellsSubView); + + QAction* lands = createMenuEntry(CSMWorld::UniversalId::Type_Lands, world, "document-world-lands"); + connect(lands, &QAction::triggered, this, &View::addLandsSubView); - QAction *lands = createMenuEntry(CSMWorld::UniversalId::Type_Lands, world, "document-world-lands"); - connect (lands, SIGNAL (triggered()), this, SLOT (addLandsSubView())); + QAction* landTextures + = createMenuEntry(CSMWorld::UniversalId::Type_LandTextures, world, "document-world-landtextures"); + connect(landTextures, &QAction::triggered, this, &View::addLandTexturesSubView); - QAction *landTextures = createMenuEntry(CSMWorld::UniversalId::Type_LandTextures, world, "document-world-landtextures"); - connect (landTextures, SIGNAL (triggered()), this, SLOT (addLandTexturesSubView())); + QAction* grid = createMenuEntry(CSMWorld::UniversalId::Type_Pathgrids, world, "document-world-pathgrid"); + connect(grid, &QAction::triggered, this, &View::addPathgridSubView); - QAction *grid = createMenuEntry(CSMWorld::UniversalId::Type_Pathgrids, world, "document-world-pathgrid"); - connect (grid, SIGNAL (triggered()), this, SLOT (addPathgridSubView())); + world->addSeparator(); - world->addSeparator(); // items that don't represent single record lists follow here + QAction* regions = createMenuEntry(CSMWorld::UniversalId::Type_Regions, world, "document-world-regions"); + connect(regions, &QAction::triggered, this, &View::addRegionsSubView); - QAction *regionMap = createMenuEntry(CSMWorld::UniversalId::Type_RegionMap, world, "document-world-regionmap"); - connect (regionMap, SIGNAL (triggered()), this, SLOT (addRegionMapSubView())); + QAction* regionMap = createMenuEntry(CSMWorld::UniversalId::Type_RegionMap, world, "document-world-regionmap"); + connect(regionMap, &QAction::triggered, this, &View::addRegionMapSubView); } void CSVDoc::View::setupMechanicsMenu() { - QMenu *mechanics = menuBar()->addMenu (tr ("Mechanics")); + QMenu* mechanics = menuBar()->addMenu(tr("Mechanics")); + + QAction* scripts = createMenuEntry(CSMWorld::UniversalId::Type_Scripts, mechanics, "document-mechanics-scripts"); + connect(scripts, &QAction::triggered, this, &View::addScriptsSubView); + + QAction* startScripts + = createMenuEntry(CSMWorld::UniversalId::Type_StartScripts, mechanics, "document-mechanics-startscripts"); + connect(startScripts, &QAction::triggered, this, &View::addStartScriptsSubView); QAction* globals = createMenuEntry(CSMWorld::UniversalId::Type_Globals, mechanics, "document-mechanics-globals"); - connect (globals, SIGNAL (triggered()), this, SLOT (addGlobalsSubView())); + connect(globals, &QAction::triggered, this, &View::addGlobalsSubView); QAction* gmsts = createMenuEntry(CSMWorld::UniversalId::Type_Gmsts, mechanics, "document-mechanics-gamesettings"); - connect (gmsts, SIGNAL (triggered()), this, SLOT (addGmstsSubView())); + connect(gmsts, &QAction::triggered, this, &View::addGmstsSubView); - QAction* scripts = createMenuEntry(CSMWorld::UniversalId::Type_Scripts, mechanics, "document-mechanics-scripts"); - connect (scripts, SIGNAL (triggered()), this, SLOT (addScriptsSubView())); + mechanics->addSeparator(); QAction* spells = createMenuEntry(CSMWorld::UniversalId::Type_Spells, mechanics, "document-mechanics-spells"); - connect (spells, SIGNAL (triggered()), this, SLOT (addSpellsSubView())); + connect(spells, &QAction::triggered, this, &View::addSpellsSubView); - QAction* enchantments = createMenuEntry(CSMWorld::UniversalId::Type_Enchantments, mechanics, "document-mechanics-enchantments"); - connect (enchantments, SIGNAL (triggered()), this, SLOT (addEnchantmentsSubView())); + QAction* enchantments + = createMenuEntry(CSMWorld::UniversalId::Type_Enchantments, mechanics, "document-mechanics-enchantments"); + connect(enchantments, &QAction::triggered, this, &View::addEnchantmentsSubView); - QAction* magicEffects = createMenuEntry(CSMWorld::UniversalId::Type_MagicEffects, mechanics, "document-mechanics-magiceffects"); - connect (magicEffects, SIGNAL (triggered()), this, SLOT (addMagicEffectsSubView())); - - QAction* startScripts = createMenuEntry(CSMWorld::UniversalId::Type_StartScripts, mechanics, "document-mechanics-startscripts"); - connect (startScripts, SIGNAL (triggered()), this, SLOT (addStartScriptsSubView())); + QAction* magicEffects + = createMenuEntry(CSMWorld::UniversalId::Type_MagicEffects, mechanics, "document-mechanics-magiceffects"); + connect(magicEffects, &QAction::triggered, this, &View::addMagicEffectsSubView); } void CSVDoc::View::setupCharacterMenu() { - QMenu *characters = menuBar()->addMenu (tr ("Characters")); + QMenu* characters = menuBar()->addMenu(tr("Characters")); QAction* skills = createMenuEntry(CSMWorld::UniversalId::Type_Skills, characters, "document-character-skills"); - connect (skills, SIGNAL (triggered()), this, SLOT (addSkillsSubView())); + connect(skills, &QAction::triggered, this, &View::addSkillsSubView); QAction* classes = createMenuEntry(CSMWorld::UniversalId::Type_Classes, characters, "document-character-classes"); - connect (classes, SIGNAL (triggered()), this, SLOT (addClassesSubView())); + connect(classes, &QAction::triggered, this, &View::addClassesSubView); QAction* factions = createMenuEntry(CSMWorld::UniversalId::Type_Faction, characters, "document-character-factions"); - connect (factions, SIGNAL (triggered()), this, SLOT (addFactionsSubView())); + connect(factions, &QAction::triggered, this, &View::addFactionsSubView); QAction* races = createMenuEntry(CSMWorld::UniversalId::Type_Races, characters, "document-character-races"); - connect (races, SIGNAL (triggered()), this, SLOT (addRacesSubView())); + connect(races, &QAction::triggered, this, &View::addRacesSubView); + + QAction* birthsigns + = createMenuEntry(CSMWorld::UniversalId::Type_Birthsigns, characters, "document-character-birthsigns"); + connect(birthsigns, &QAction::triggered, this, &View::addBirthsignsSubView); + + QAction* bodyParts + = createMenuEntry(CSMWorld::UniversalId::Type_BodyParts, characters, "document-character-bodyparts"); + connect(bodyParts, &QAction::triggered, this, &View::addBodyPartsSubView); - QAction* birthsigns = createMenuEntry(CSMWorld::UniversalId::Type_Birthsigns, characters, "document-character-birthsigns"); - connect (birthsigns, SIGNAL (triggered()), this, SLOT (addBirthsignsSubView())); + characters->addSeparator(); QAction* topics = createMenuEntry(CSMWorld::UniversalId::Type_Topics, characters, "document-character-topics"); - connect (topics, SIGNAL (triggered()), this, SLOT (addTopicsSubView())); + connect(topics, &QAction::triggered, this, &View::addTopicsSubView); - QAction* journals = createMenuEntry(CSMWorld::UniversalId::Type_Journals, characters, "document-character-journals"); - connect (journals, SIGNAL (triggered()), this, SLOT (addJournalsSubView())); + QAction* topicInfos + = createMenuEntry(CSMWorld::UniversalId::Type_TopicInfos, characters, "document-character-topicinfos"); + connect(topicInfos, &QAction::triggered, this, &View::addTopicInfosSubView); - QAction* topicInfos = createMenuEntry(CSMWorld::UniversalId::Type_TopicInfos, characters, "document-character-topicinfos"); - connect (topicInfos, SIGNAL (triggered()), this, SLOT (addTopicInfosSubView())); + characters->addSeparator(); - QAction* journalInfos = createMenuEntry(CSMWorld::UniversalId::Type_JournalInfos, characters, "document-character-journalinfos"); - connect (journalInfos, SIGNAL (triggered()), this, SLOT (addJournalInfosSubView())); + QAction* journals + = createMenuEntry(CSMWorld::UniversalId::Type_Journals, characters, "document-character-journals"); + connect(journals, &QAction::triggered, this, &View::addJournalsSubView); - QAction* bodyParts = createMenuEntry(CSMWorld::UniversalId::Type_BodyParts, characters, "document-character-bodyparts"); - connect (bodyParts, SIGNAL (triggered()), this, SLOT (addBodyPartsSubView())); + QAction* journalInfos + = createMenuEntry(CSMWorld::UniversalId::Type_JournalInfos, characters, "document-character-journalinfos"); + connect(journalInfos, &QAction::triggered, this, &View::addJournalInfosSubView); } void CSVDoc::View::setupAssetsMenu() { - QMenu *assets = menuBar()->addMenu (tr ("Assets")); + QMenu* assets = menuBar()->addMenu(tr("Assets")); - QAction* reload = createMenuEntry("Reload", ":./menu-reload.png", assets, "document-assets-reload"); - connect (reload, SIGNAL (triggered()), &mDocument->getData(), SLOT (assetsChanged())); + QAction* reload = createMenuEntry("Reload", ":menu-reload", assets, "document-assets-reload"); + connect(reload, &QAction::triggered, &mDocument->getData(), &CSMWorld::Data::assetsChanged); assets->addSeparator(); QAction* sounds = createMenuEntry(CSMWorld::UniversalId::Type_Sounds, assets, "document-assets-sounds"); - connect (sounds, SIGNAL (triggered()), this, SLOT (addSoundsSubView())); + connect(sounds, &QAction::triggered, this, &View::addSoundsSubView); QAction* soundGens = createMenuEntry(CSMWorld::UniversalId::Type_SoundGens, assets, "document-assets-soundgens"); - connect (soundGens, SIGNAL (triggered()), this, SLOT (addSoundGensSubView())); + connect(soundGens, &QAction::triggered, this, &View::addSoundGensSubView); assets->addSeparator(); // resources follow here QAction* meshes = createMenuEntry(CSMWorld::UniversalId::Type_Meshes, assets, "document-assets-meshes"); - connect (meshes, SIGNAL (triggered()), this, SLOT (addMeshesSubView())); + connect(meshes, &QAction::triggered, this, &View::addMeshesSubView); QAction* icons = createMenuEntry(CSMWorld::UniversalId::Type_Icons, assets, "document-assets-icons"); - connect (icons, SIGNAL (triggered()), this, SLOT (addIconsSubView())); + connect(icons, &QAction::triggered, this, &View::addIconsSubView); QAction* musics = createMenuEntry(CSMWorld::UniversalId::Type_Musics, assets, "document-assets-musics"); - connect (musics, SIGNAL (triggered()), this, SLOT (addMusicsSubView())); + connect(musics, &QAction::triggered, this, &View::addMusicsSubView); QAction* soundFiles = createMenuEntry(CSMWorld::UniversalId::Type_SoundsRes, assets, "document-assets-soundres"); - connect (soundFiles, SIGNAL (triggered()), this, SLOT (addSoundsResSubView())); + connect(soundFiles, &QAction::triggered, this, &View::addSoundsResSubView); QAction* textures = createMenuEntry(CSMWorld::UniversalId::Type_Textures, assets, "document-assets-textures"); - connect (textures, SIGNAL (triggered()), this, SLOT (addTexturesSubView())); + connect(textures, &QAction::triggered, this, &View::addTexturesSubView); QAction* videos = createMenuEntry(CSMWorld::UniversalId::Type_Videos, assets, "document-assets-videos"); - connect (videos, SIGNAL (triggered()), this, SLOT (addVideosSubView())); + connect(videos, &QAction::triggered, this, &View::addVideosSubView); } void CSVDoc::View::setupDebugMenu() { - QMenu *debug = menuBar()->addMenu (tr ("Debug")); + QMenu* debug = menuBar()->addMenu(tr("Debug")); QAction* profiles = createMenuEntry(CSMWorld::UniversalId::Type_DebugProfiles, debug, "document-debug-profiles"); - connect (profiles, SIGNAL (triggered()), this, SLOT (addDebugProfilesSubView())); + connect(profiles, &QAction::triggered, this, &View::addDebugProfilesSubView); debug->addSeparator(); - mGlobalDebugProfileMenu = new GlobalDebugProfileMenu ( - &dynamic_cast (*mDocument->getData().getTableModel ( - CSMWorld::UniversalId::Type_DebugProfiles)), this); + mGlobalDebugProfileMenu = new GlobalDebugProfileMenu( + &dynamic_cast( + *mDocument->getData().getTableModel(CSMWorld::UniversalId::Type_DebugProfiles)), + this); - connect (mGlobalDebugProfileMenu, SIGNAL (triggered (const std::string&)), - this, SLOT (run (const std::string&))); + connect(mGlobalDebugProfileMenu, &GlobalDebugProfileMenu::triggered, this, + [this](const std::string& profile) { this->run(profile, ""); }); - QAction *runDebug = debug->addMenu (mGlobalDebugProfileMenu); - runDebug->setText (tr ("Run OpenMW")); + QAction* runDebug = debug->addMenu(mGlobalDebugProfileMenu); + runDebug->setText(tr("Run OpenMW")); setupShortcut("document-debug-run", runDebug); - runDebug->setIcon(QIcon(QString::fromStdString(":./run-openmw.png"))); + runDebug->setIcon(Misc::ScalableIcon::load(":run-openmw")); - QAction* stopDebug = createMenuEntry("Stop OpenMW", ":./stop-openmw.png", debug, "document-debug-shutdown"); - connect (stopDebug, SIGNAL (triggered()), this, SLOT (stop())); + QAction* stopDebug = createMenuEntry("Stop OpenMW", ":stop-openmw", debug, "document-debug-shutdown"); + connect(stopDebug, &QAction::triggered, this, &View::stop); mStopDebug = stopDebug; QAction* runLog = createMenuEntry(CSMWorld::UniversalId::Type_RunLog, debug, "document-debug-runlog"); - connect (runLog, SIGNAL (triggered()), this, SLOT (addRunLogSubView())); + connect(runLog, &QAction::triggered, this, &View::addRunLogSubView); } void CSVDoc::View::setupHelpMenu() { - QMenu *help = menuBar()->addMenu (tr ("Help")); + QMenu* help = menuBar()->addMenu(tr("Help")); - QAction* helpInfo = createMenuEntry("Help", ":/info.png", help, "document-help-help"); - connect (helpInfo, SIGNAL (triggered()), this, SLOT (openHelp())); + QAction* helpInfo = createMenuEntry("Help", ":info", help, "document-help-help"); + connect(helpInfo, &QAction::triggered, this, &View::openHelp); - QAction* tutorial = createMenuEntry("Tutorial", ":/info.png", help, "document-help-tutorial"); - connect (tutorial, SIGNAL (triggered()), this, SLOT (tutorial())); + QAction* tutorial = createMenuEntry("Tutorial", ":info", help, "document-help-tutorial"); + connect(tutorial, &QAction::triggered, this, &View::tutorial); - QAction* about = createMenuEntry("About OpenMW-CS", ":./info.png", help, "document-help-about"); - connect (about, SIGNAL (triggered()), this, SLOT (infoAbout())); + QAction* about = createMenuEntry("About OpenMW-CS", ":info", help, "document-help-about"); + connect(about, &QAction::triggered, this, &View::infoAbout); - QAction* aboutQt = createMenuEntry("About Qt", ":./qt.png", help, "document-help-qt"); - connect (aboutQt, SIGNAL (triggered()), this, SLOT (infoAboutQt())); + QAction* aboutQt = createMenuEntry("About Qt", ":qt", help, "document-help-qt"); + connect(aboutQt, &QAction::triggered, this, &View::infoAboutQt); } QAction* CSVDoc::View::createMenuEntry(CSMWorld::UniversalId::Type type, QMenu* menu, const char* shortcutName) { - const std::string title = CSMWorld::UniversalId (type).getTypeName(); - QAction *entry = new QAction(QString::fromStdString(title), this); + const std::string title = CSMWorld::UniversalId(type).getTypeName(); + QAction* entry = new QAction(QString::fromStdString(title), this); setupShortcut(shortcutName, entry); - const std::string iconName = CSMWorld::UniversalId (type).getIcon(); + const std::string iconName = CSMWorld::UniversalId(type).getIcon(); if (!iconName.empty() && iconName != ":placeholder") - entry->setIcon(QIcon(QString::fromStdString(iconName))); + entry->setIcon(Misc::ScalableIcon::load(QString::fromStdString(iconName))); - menu->addAction (entry); + menu->addAction(entry); return entry; } -QAction* CSVDoc::View::createMenuEntry(const std::string& title, const std::string& iconName, QMenu* menu, const char* shortcutName) +QAction* CSVDoc::View::createMenuEntry( + const std::string& title, const std::string& iconName, QMenu* menu, const char* shortcutName) { - QAction *entry = new QAction(QString::fromStdString(title), this); + QAction* entry = new QAction(QString::fromStdString(title), this); setupShortcut(shortcutName, entry); if (!iconName.empty() && iconName != ":placeholder") - entry->setIcon(QIcon(QString::fromStdString(iconName))); + entry->setIcon(Misc::ScalableIcon::load(QString::fromStdString(iconName))); - menu->addAction (entry); + menu->addAction(entry); return entry; } @@ -375,30 +418,29 @@ void CSVDoc::View::updateTitle() { std::ostringstream stream; - stream << mDocument->getSavePath().filename().string(); + stream << Files::pathToUnicodeString(mDocument->getSavePath().filename()); if (mDocument->getState() & CSMDoc::State_Modified) - stream << " *"; + stream << " *"; - if (mViewTotal>1) - stream << " [" << (mViewIndex+1) << "/" << mViewTotal << "]"; + if (mViewTotal > 1) + stream << " [" << (mViewIndex + 1) << "/" << mViewTotal << "]"; CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; - bool hideTitle = windows["hide-subview"].isTrue() && - mSubViews.size()==1 && !mSubViews.at (0)->isFloating(); + bool hideTitle = windows["hide-subview"].isTrue() && mSubViews.size() == 1 && !mSubViews.at(0)->isFloating(); if (hideTitle) - stream << " - " << mSubViews.at (0)->getTitle(); + stream << " - " << mSubViews.at(0)->getTitle(); - setWindowTitle (QString::fromUtf8(stream.str().c_str())); + setWindowTitle(QString::fromUtf8(stream.str().c_str())); } -void CSVDoc::View::updateSubViewIndices(SubView *view) +void CSVDoc::View::updateSubViewIndices(SubView* view) { CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; - if(view && mSubViews.contains(view)) + if (view && mSubViews.contains(view)) { mSubViews.removeOne(view); @@ -407,24 +449,23 @@ void CSVDoc::View::updateSubViewIndices(SubView *view) updateScrollbar(); } - bool hideTitle = windows["hide-subview"].isTrue() && - mSubViews.size()==1 && !mSubViews.at (0)->isFloating(); + bool hideTitle = windows["hide-subview"].isTrue() && mSubViews.size() == 1 && !mSubViews.at(0)->isFloating(); updateTitle(); - for (SubView *subView : mSubViews) + for (SubView* subView : mSubViews) { if (!subView->isFloating()) { if (hideTitle) { - subView->setTitleBarWidget (new QWidget (this)); - subView->setWindowTitle (QString::fromUtf8 (subView->getTitle().c_str())); + subView->setTitleBarWidget(new QWidget(this)); + subView->setWindowTitle(QString::fromUtf8(subView->getTitle().c_str())); } else { delete subView->titleBarWidget(); - subView->setTitleBarWidget (nullptr); + subView->setTitleBarWidget(nullptr); } } } @@ -435,38 +476,41 @@ void CSVDoc::View::updateActions() bool editing = !(mDocument->getState() & CSMDoc::State_Locked); bool running = mDocument->getState() & CSMDoc::State_Running; - for (std::vector::iterator iter (mEditingActions.begin()); iter!=mEditingActions.end(); ++iter) - (*iter)->setEnabled (editing); + for (std::vector::iterator iter(mEditingActions.begin()); iter != mEditingActions.end(); ++iter) + (*iter)->setEnabled(editing); - mUndo->setEnabled (editing && mDocument->getUndoStack().canUndo()); - mRedo->setEnabled (editing && mDocument->getUndoStack().canRedo()); + mUndo->setEnabled(editing && mDocument->getUndoStack().canUndo()); + mRedo->setEnabled(editing && mDocument->getUndoStack().canRedo()); - mSave->setEnabled (!(mDocument->getState() & CSMDoc::State_Saving) && !running); - mVerify->setEnabled (!(mDocument->getState() & CSMDoc::State_Verifying)); + mSave->setEnabled(!(mDocument->getState() & CSMDoc::State_Saving) && !running); + mVerify->setEnabled(!(mDocument->getState() & CSMDoc::State_Verifying)); - mGlobalDebugProfileMenu->updateActions (running); - mStopDebug->setEnabled (running); + mGlobalDebugProfileMenu->updateActions(running); + mStopDebug->setEnabled(running); - mMerge->setEnabled (mDocument->getContentFiles().size()>1 && - !(mDocument->getState() & CSMDoc::State_Merging)); + mMerge->setEnabled(mDocument->getContentFiles().size() > 1 && !(mDocument->getState() & CSMDoc::State_Merging)); } -CSVDoc::View::View (ViewManager& viewManager, CSMDoc::Document *document, int totalViews) - : mViewManager (viewManager), mDocument (document), mViewIndex (totalViews-1), - mViewTotal (totalViews), mScroll(nullptr), mScrollbarOnly(false) +CSVDoc::View::View(ViewManager& viewManager, CSMDoc::Document* document, int totalViews) + : mViewManager(viewManager) + , mDocument(document) + , mViewIndex(totalViews - 1) + , mViewTotal(totalViews) + , mScroll(nullptr) + , mScrollbarOnly(false) { CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; - int width = std::max (windows["default-width"].toInt(), 300); - int height = std::max (windows["default-height"].toInt(), 300); + int width = std::max(windows["default-width"].toInt(), 300); + int height = std::max(windows["default-height"].toInt(), 300); - resize (width, height); + resize(width, height); - mSubViewWindow.setDockOptions (QMainWindow::AllowNestedDocks); + mSubViewWindow.setDockOptions(QMainWindow::AllowNestedDocks); if (windows["mainwindow-scrollbar"].toString() == "Grow Only") { - setCentralWidget (&mSubViewWindow); + setCentralWidget(&mSubViewWindow); } else { @@ -474,7 +518,7 @@ CSVDoc::View::View (ViewManager& viewManager, CSMDoc::Document *document, int to } mOperations = new Operations; - addDockWidget (Qt::BottomDockWidgetArea, mOperations); + addDockWidget(Qt::BottomDockWidgetArea, mOperations); setContextMenuPolicy(Qt::NoContextMenu); @@ -484,32 +528,27 @@ CSVDoc::View::View (ViewManager& viewManager, CSMDoc::Document *document, int to updateActions(); - CSVWorld::addSubViewFactories (mSubViewFactory); - CSVTools::addSubViewFactories (mSubViewFactory); + CSVWorld::addSubViewFactories(mSubViewFactory); + CSVTools::addSubViewFactories(mSubViewFactory); - mSubViewFactory.add (CSMWorld::UniversalId::Type_RunLog, new SubViewFactory); + mSubViewFactory.add(CSMWorld::UniversalId::Type_RunLog, new SubViewFactory); - connect (mOperations, SIGNAL (abortOperation (int)), this, SLOT (abortOperation (int))); + connect(mOperations, &Operations::abortOperation, this, &View::abortOperation); - connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), - this, SLOT (settingChanged (const CSMPrefs::Setting *))); + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &View::settingChanged); } -CSVDoc::View::~View() +const CSMDoc::Document* CSVDoc::View::getDocument() const { + return mDocument; } -const CSMDoc::Document *CSVDoc::View::getDocument() const +CSMDoc::Document* CSVDoc::View::getDocument() { - return mDocument; + return mDocument; } -CSMDoc::Document *CSVDoc::View::getDocument() -{ - return mDocument; -} - -void CSVDoc::View::setIndex (int viewIndex, int totalViews) +void CSVDoc::View::setIndex(int viewIndex, int totalViews) { mViewIndex = viewIndex; mViewTotal = totalViews; @@ -521,31 +560,33 @@ void CSVDoc::View::updateDocumentState() updateTitle(); updateActions(); - static const int operations[] = - { - CSMDoc::State_Saving, CSMDoc::State_Verifying, CSMDoc::State_Searching, + static const int operations[] = { + CSMDoc::State_Saving, + CSMDoc::State_Verifying, + CSMDoc::State_Searching, CSMDoc::State_Merging, - -1 // end marker + // end marker + -1, }; - int state = mDocument->getState() ; + int state = mDocument->getState(); - for (int i=0; operations[i]!=-1; ++i) + for (int i = 0; operations[i] != -1; ++i) if (!(state & operations[i])) - mOperations->quitOperation (operations[i]); + mOperations->quitOperation(operations[i]); - QList subViews = findChildren(); + QList subViews = findChildren(); - for (QList::iterator iter (subViews.begin()); iter!=subViews.end(); ++iter) - (*iter)->setEditLock (state & CSMDoc::State_Locked); + for (QList::iterator iter(subViews.begin()); iter != subViews.end(); ++iter) + (*iter)->setEditLock(state & CSMDoc::State_Locked); } -void CSVDoc::View::updateProgress (int current, int max, int type, int threads) +void CSVDoc::View::updateProgress(int current, int max, int type, int threads) { - mOperations->setProgress (current, max, type, threads); + mOperations->setProgress(current, max, type, threads); } -void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::string& hint) +void CSVDoc::View::addSubView(const CSMWorld::UniversalId& id, const std::string& hint) { CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; @@ -554,57 +595,55 @@ void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::strin // User setting to reuse sub views (on a per top level view basis) if (windows["reuse"].isTrue()) { - for (SubView *sb : mSubViews) + for (SubView* sb : mSubViews) { - bool isSubViewReferenceable = - sb->getUniversalId().getType() == CSMWorld::UniversalId::Type_Referenceable; + bool isSubViewReferenceable = sb->getUniversalId().getType() == CSMWorld::UniversalId::Type_Referenceable; - if((isReferenceable && isSubViewReferenceable && id.getId() == sb->getUniversalId().getId()) - || - (!isReferenceable && id == sb->getUniversalId())) + if ((isReferenceable && isSubViewReferenceable && id.getId() == sb->getUniversalId().getId()) + || (!isReferenceable && id == sb->getUniversalId())) { sb->setFocus(); if (!hint.empty()) - sb->useHint (hint); + sb->useHint(hint); return; } } } if (mScroll) - QObject::connect(mScroll->horizontalScrollBar(), - SIGNAL(rangeChanged(int,int)), this, SLOT(moveScrollBarToEnd(int,int))); + QObject::connect(mScroll->horizontalScrollBar(), &QScrollBar::rangeChanged, this, &View::moveScrollBarToEnd); // User setting for limiting the number of sub views per top level view. // Automatically open a new top level view if this number is exceeded // // If the sub view limit setting is one, the sub view title bar is hidden and the // text in the main title bar is adjusted accordingly - if(mSubViews.size() >= windows["max-subviews"].toInt()) // create a new top level view + if (mSubViews.size() >= windows["max-subviews"].toInt()) // create a new top level view { mViewManager.addView(mDocument, id, hint); return; } - SubView *view = nullptr; - if(isReferenceable) + SubView* view = nullptr; + if (isReferenceable) { - view = mSubViewFactory.makeSubView (CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id.getId()), *mDocument); + view = mSubViewFactory.makeSubView( + CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id), *mDocument); } else { - view = mSubViewFactory.makeSubView (id, *mDocument); + view = mSubViewFactory.makeSubView(id, *mDocument); } assert(view); view->setParent(this); - view->setEditLock (mDocument->getState() & CSMDoc::State_Locked); + view->setEditLock(mDocument->getState() & CSMDoc::State_Locked); mSubViews.append(view); // only after assert int minWidth = windows["minimum-width"].toInt(); - view->setMinimumWidth (minWidth); + view->setMinimumWidth(minWidth); - view->setStatusBar (mShowStatusBar->isChecked()); + view->setStatusBar(mShowStatusBar->isChecked()); // Work out how to deal with additional subviews // @@ -619,40 +658,51 @@ void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::strin // mScrollbarOnly = windows["mainwindow-scrollbar"].toString() == "Scrollbar Only"; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) updateWidth(windows["grow-limit"].isTrue(), minWidth); +#else + updateWidth(true, minWidth); +#endif - mSubViewWindow.addDockWidget (Qt::TopDockWidgetArea, view); + mSubViewWindow.addDockWidget(Qt::TopDockWidgetArea, view); updateSubViewIndices(); - connect (view, SIGNAL (focusId (const CSMWorld::UniversalId&, const std::string&)), this, - SLOT (addSubView (const CSMWorld::UniversalId&, const std::string&))); + connect(view, &SubView::focusId, this, &View::addSubView); - connect (view, SIGNAL (closeRequest (SubView *)), this, SLOT (closeRequest (SubView *))); + connect(view, qOverload(&SubView::closeRequest), this, &View::closeRequest); - connect (view, SIGNAL (updateTitle()), this, SLOT (updateTitle())); + connect(view, &SubView::updateTitle, this, &View::updateTitle); - connect (view, SIGNAL (updateSubViewIndices (SubView *)), - this, SLOT (updateSubViewIndices (SubView *))); + connect(view, &SubView::updateSubViewIndices, this, &View::updateSubViewIndices); CSVWorld::TableSubView* tableView = dynamic_cast(view); if (tableView) { - connect (this, SIGNAL (requestFocus (const std::string&)), - tableView, SLOT (requestFocus (const std::string&))); + connect(this, &View::requestFocus, tableView, &CSVWorld::TableSubView::requestFocus); } CSVWorld::SceneSubView* sceneView = dynamic_cast(view); if (sceneView) { - connect(sceneView, SIGNAL(requestFocus(const std::string&)), - this, SLOT(onRequestFocus(const std::string&))); + connect(sceneView, &CSVWorld::SceneSubView::requestFocus, this, &View::onRequestFocus); + } + + if (CSMPrefs::State::get()["ID Tables"]["subview-new-window"].isTrue()) + { + CSVWorld::DialogueSubView* dialogueView = dynamic_cast(view); + if (dialogueView) + dialogueView->setFloating(true); + + CSVWorld::ScriptSubView* scriptView = dynamic_cast(view); + if (scriptView) + scriptView->setFloating(true); } view->show(); if (!hint.empty()) - view->useHint (hint); + view->useHint(hint); } void CSVDoc::View::moveScrollBarToEnd(int min, int max) @@ -661,22 +711,21 @@ void CSVDoc::View::moveScrollBarToEnd(int min, int max) { mScroll->horizontalScrollBar()->setValue(max); - QObject::disconnect(mScroll->horizontalScrollBar(), - SIGNAL(rangeChanged(int,int)), this, SLOT(moveScrollBarToEnd(int,int))); + QObject::disconnect(mScroll->horizontalScrollBar(), &QScrollBar::rangeChanged, this, &View::moveScrollBarToEnd); } } -void CSVDoc::View::settingChanged (const CSMPrefs::Setting *setting) +void CSVDoc::View::settingChanged(const CSMPrefs::Setting* setting) { - if (*setting=="Windows/hide-subview") - updateSubViewIndices (nullptr); - else if (*setting=="Windows/mainwindow-scrollbar") + if (*setting == "Windows/hide-subview") + updateSubViewIndices(nullptr); + else if (*setting == "Windows/mainwindow-scrollbar") { - if (setting->toString()!="Grow Only") + if (setting->toString() != "Grow Only") { if (mScroll) { - if (setting->toString()=="Scrollbar Only") + if (setting->toString() == "Scrollbar Only") { mScrollbarOnly = true; mSubViewWindow.setMinimumWidth(0); @@ -692,10 +741,10 @@ void CSVDoc::View::settingChanged (const CSMPrefs::Setting *setting) createScrollArea(); } } - else if (mScroll) + else if (mScroll) { mScroll->takeWidget(); - setCentralWidget (&mSubViewWindow); + setCentralWidget(&mSubViewWindow); mScroll->deleteLater(); mScroll = nullptr; } @@ -704,7 +753,7 @@ void CSVDoc::View::settingChanged (const CSMPrefs::Setting *setting) void CSVDoc::View::newView() { - mViewManager.addView (mDocument); + mViewManager.addView(mDocument); } void CSVDoc::View::save() @@ -725,40 +774,37 @@ void CSVDoc::View::tutorial() void CSVDoc::View::infoAbout() { // Get current OpenMW version - QString versionInfo = (Version::getOpenmwVersionDescription(mDocument->getResourceDir().string())+ + QString versionInfo = (Version::getOpenmwVersionDescription() + #if defined(__x86_64__) || defined(_M_X64) - " (64-bit)").c_str(); + " (64-bit)") + .c_str(); #else - " (32-bit)").c_str(); + " (32-bit)") + .c_str(); #endif // Get current year - time_t now = time(nullptr); - struct tm tstruct; - char copyrightInfo[40]; - tstruct = *localtime(&now); - strftime(copyrightInfo, sizeof(copyrightInfo), "Copyright © 2008-%Y OpenMW Team", &tstruct); + const auto copyrightInfo = Misc::timeToString(std::chrono::system_clock::now(), "Copyright © 2008-%Y OpenMW Team"); QString aboutText = QString( - "

" - "

OpenMW Construction Set

" - "%1\n\n" - "%2\n\n" - "%3\n\n" - "" - "" - "" - "" - "" - "
%4https://openmw.org
%5https://forum.openmw.org
%6https://gitlab.com/OpenMW/openmw/issues
%7ircs://irc.libera.chat/#openmw
" - "

") - .arg(versionInfo - , tr("OpenMW-CS is a content file editor for OpenMW, a modern, free and open source game engine.") - , tr(copyrightInfo) - , tr("Home Page:") - , tr("Forum:") - , tr("Bug Tracker:") - , tr("IRC:")); + "

" + "

OpenMW Construction Set

" + "%1\n\n" + "%2\n\n" + "%3\n\n" + "" + "" + "" + "" + "" + "
%4https://openmw.org
%5https://forum.openmw.org
%6https://gitlab.com/OpenMW/openmw/issues
%7ircs://irc.libera.chat/#openmw
" + "

") + .arg(versionInfo, + tr("OpenMW-CS is a content file editor for OpenMW, a modern, free and open source game " + "engine."), + tr(copyrightInfo.c_str()), tr("Home Page:"), tr("Forum:"), tr("Bug Tracker:"), + tr("IRC:")); QMessageBox::about(this, "About OpenMW-CS", aboutText); } @@ -770,233 +816,233 @@ void CSVDoc::View::infoAboutQt() void CSVDoc::View::verify() { - addSubView (mDocument->verify()); + addSubView(mDocument->verify()); } void CSVDoc::View::addGlobalsSubView() { - addSubView (CSMWorld::UniversalId::Type_Globals); + addSubView(CSMWorld::UniversalId::Type_Globals); } void CSVDoc::View::addGmstsSubView() { - addSubView (CSMWorld::UniversalId::Type_Gmsts); + addSubView(CSMWorld::UniversalId::Type_Gmsts); } void CSVDoc::View::addSkillsSubView() { - addSubView (CSMWorld::UniversalId::Type_Skills); + addSubView(CSMWorld::UniversalId::Type_Skills); } void CSVDoc::View::addClassesSubView() { - addSubView (CSMWorld::UniversalId::Type_Classes); + addSubView(CSMWorld::UniversalId::Type_Classes); } void CSVDoc::View::addFactionsSubView() { - addSubView (CSMWorld::UniversalId::Type_Factions); + addSubView(CSMWorld::UniversalId::Type_Factions); } void CSVDoc::View::addRacesSubView() { - addSubView (CSMWorld::UniversalId::Type_Races); + addSubView(CSMWorld::UniversalId::Type_Races); } void CSVDoc::View::addSoundsSubView() { - addSubView (CSMWorld::UniversalId::Type_Sounds); + addSubView(CSMWorld::UniversalId::Type_Sounds); } void CSVDoc::View::addScriptsSubView() { - addSubView (CSMWorld::UniversalId::Type_Scripts); + addSubView(CSMWorld::UniversalId::Type_Scripts); } void CSVDoc::View::addRegionsSubView() { - addSubView (CSMWorld::UniversalId::Type_Regions); + addSubView(CSMWorld::UniversalId::Type_Regions); } void CSVDoc::View::addBirthsignsSubView() { - addSubView (CSMWorld::UniversalId::Type_Birthsigns); + addSubView(CSMWorld::UniversalId::Type_Birthsigns); } void CSVDoc::View::addSpellsSubView() { - addSubView (CSMWorld::UniversalId::Type_Spells); + addSubView(CSMWorld::UniversalId::Type_Spells); } void CSVDoc::View::addCellsSubView() { - addSubView (CSMWorld::UniversalId::Type_Cells); + addSubView(CSMWorld::UniversalId::Type_Cells); } void CSVDoc::View::addReferenceablesSubView() { - addSubView (CSMWorld::UniversalId::Type_Referenceables); + addSubView(CSMWorld::UniversalId::Type_Referenceables); } void CSVDoc::View::addReferencesSubView() { - addSubView (CSMWorld::UniversalId::Type_References); + addSubView(CSMWorld::UniversalId::Type_References); } void CSVDoc::View::addRegionMapSubView() { - addSubView (CSMWorld::UniversalId::Type_RegionMap); + addSubView(CSMWorld::UniversalId::Type_RegionMap); } void CSVDoc::View::addFiltersSubView() { - addSubView (CSMWorld::UniversalId::Type_Filters); + addSubView(CSMWorld::UniversalId::Type_Filters); } void CSVDoc::View::addTopicsSubView() { - addSubView (CSMWorld::UniversalId::Type_Topics); + addSubView(CSMWorld::UniversalId::Type_Topics); } void CSVDoc::View::addJournalsSubView() { - addSubView (CSMWorld::UniversalId::Type_Journals); + addSubView(CSMWorld::UniversalId::Type_Journals); } void CSVDoc::View::addTopicInfosSubView() { - addSubView (CSMWorld::UniversalId::Type_TopicInfos); + addSubView(CSMWorld::UniversalId::Type_TopicInfos); } void CSVDoc::View::addJournalInfosSubView() { - addSubView (CSMWorld::UniversalId::Type_JournalInfos); + addSubView(CSMWorld::UniversalId::Type_JournalInfos); } void CSVDoc::View::addEnchantmentsSubView() { - addSubView (CSMWorld::UniversalId::Type_Enchantments); + addSubView(CSMWorld::UniversalId::Type_Enchantments); } void CSVDoc::View::addBodyPartsSubView() { - addSubView (CSMWorld::UniversalId::Type_BodyParts); + addSubView(CSMWorld::UniversalId::Type_BodyParts); } void CSVDoc::View::addSoundGensSubView() { - addSubView (CSMWorld::UniversalId::Type_SoundGens); + addSubView(CSMWorld::UniversalId::Type_SoundGens); } void CSVDoc::View::addMeshesSubView() { - addSubView (CSMWorld::UniversalId::Type_Meshes); + addSubView(CSMWorld::UniversalId::Type_Meshes); } void CSVDoc::View::addIconsSubView() { - addSubView (CSMWorld::UniversalId::Type_Icons); + addSubView(CSMWorld::UniversalId::Type_Icons); } void CSVDoc::View::addMusicsSubView() { - addSubView (CSMWorld::UniversalId::Type_Musics); + addSubView(CSMWorld::UniversalId::Type_Musics); } void CSVDoc::View::addSoundsResSubView() { - addSubView (CSMWorld::UniversalId::Type_SoundsRes); + addSubView(CSMWorld::UniversalId::Type_SoundsRes); } void CSVDoc::View::addMagicEffectsSubView() { - addSubView (CSMWorld::UniversalId::Type_MagicEffects); + addSubView(CSMWorld::UniversalId::Type_MagicEffects); } void CSVDoc::View::addTexturesSubView() { - addSubView (CSMWorld::UniversalId::Type_Textures); + addSubView(CSMWorld::UniversalId::Type_Textures); } void CSVDoc::View::addVideosSubView() { - addSubView (CSMWorld::UniversalId::Type_Videos); + addSubView(CSMWorld::UniversalId::Type_Videos); } void CSVDoc::View::addDebugProfilesSubView() { - addSubView (CSMWorld::UniversalId::Type_DebugProfiles); + addSubView(CSMWorld::UniversalId::Type_DebugProfiles); } void CSVDoc::View::addRunLogSubView() { - addSubView (CSMWorld::UniversalId::Type_RunLog); + addSubView(CSMWorld::UniversalId::Type_RunLog); } void CSVDoc::View::addLandsSubView() { - addSubView (CSMWorld::UniversalId::Type_Lands); + addSubView(CSMWorld::UniversalId::Type_Lands); } void CSVDoc::View::addLandTexturesSubView() { - addSubView (CSMWorld::UniversalId::Type_LandTextures); + addSubView(CSMWorld::UniversalId::Type_LandTextures); } void CSVDoc::View::addPathgridSubView() { - addSubView (CSMWorld::UniversalId::Type_Pathgrids); + addSubView(CSMWorld::UniversalId::Type_Pathgrids); } void CSVDoc::View::addStartScriptsSubView() { - addSubView (CSMWorld::UniversalId::Type_StartScripts); + addSubView(CSMWorld::UniversalId::Type_StartScripts); } void CSVDoc::View::addSearchSubView() { - addSubView (mDocument->newSearch()); + addSubView(mDocument->newSearch()); } void CSVDoc::View::addMetaDataSubView() { - addSubView (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_MetaData, "sys::meta")); + addSubView(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_MetaData, "sys::meta")); } -void CSVDoc::View::abortOperation (int type) +void CSVDoc::View::abortOperation(int type) { - mDocument->abortOperation (type); + mDocument->abortOperation(type); updateActions(); } -CSVDoc::Operations *CSVDoc::View::getOperations() const +CSVDoc::Operations* CSVDoc::View::getOperations() const { return mOperations; } void CSVDoc::View::exit() { - emit exitApplicationRequest (this); + emit exitApplicationRequest(this); } -void CSVDoc::View::resizeViewWidth (int width) +void CSVDoc::View::resizeViewWidth(int width) { if (width >= 0) - resize (width, geometry().height()); + resize(width, geometry().height()); } -void CSVDoc::View::resizeViewHeight (int height) +void CSVDoc::View::resizeViewHeight(int height) { if (height >= 0) - resize (geometry().width(), height); + resize(geometry().width(), height); } -void CSVDoc::View::toggleShowStatusBar (bool show) +void CSVDoc::View::toggleShowStatusBar(bool show) { - for (QObject *view : mSubViewWindow.children()) + for (QObject* view : mSubViewWindow.children()) { - if (CSVDoc::SubView *subView = dynamic_cast (view)) - subView->setStatusBar (show); + if (CSVDoc::SubView* subView = dynamic_cast(view)) + subView->setStatusBar(show); } } @@ -1007,12 +1053,12 @@ void CSVDoc::View::toggleStatusBar(bool checked) void CSVDoc::View::loadErrorLog() { - addSubView (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_LoadErrorLog, 0)); + addSubView(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_LoadErrorLog, 0)); } -void CSVDoc::View::run (const std::string& profile, const std::string& startupInstruction) +void CSVDoc::View::run(const std::string& profile, const std::string& startupInstruction) { - mDocument->startRunning (profile, startupInstruction); + mDocument->startRunning(profile, startupInstruction); } void CSVDoc::View::stop() @@ -1020,23 +1066,23 @@ void CSVDoc::View::stop() mDocument->stopRunning(); } -void CSVDoc::View::closeRequest (SubView *subView) +void CSVDoc::View::closeRequest(SubView* subView) { CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; - if (mSubViews.size()>1 || mViewTotal<=1 || !windows["hide-subview"].isTrue()) + if (mSubViews.size() > 1 || mViewTotal <= 1 || !windows["hide-subview"].isTrue()) { subView->deleteLater(); - mSubViews.removeOne (subView); + mSubViews.removeOne(subView); } - else if (mViewManager.closeRequest (this)) - mViewManager.removeDocAndView (mDocument); + else if (mViewManager.closeRequest(this)) + mViewManager.removeDocAndView(mDocument); } void CSVDoc::View::updateScrollbar() { QRect rect; - QWidget *topLevel = QApplication::topLevelAt(pos()); + QWidget* topLevel = QApplication::topLevelAt(pos()); if (topLevel) rect = topLevel->rect(); else @@ -1050,7 +1096,7 @@ void CSVDoc::View::updateScrollbar() int frameWidth = frameGeometry().width() - width(); - if ((newWidth+frameWidth) >= rect.width()) + if ((newWidth + frameWidth) >= rect.width()) mSubViewWindow.setMinimumWidth(newWidth); else mSubViewWindow.setMinimumWidth(0); @@ -1058,34 +1104,42 @@ void CSVDoc::View::updateScrollbar() void CSVDoc::View::merge() { - emit mergeDocument (mDocument); + emit mergeDocument(mDocument); } void CSVDoc::View::updateWidth(bool isGrowLimit, int minSubViewWidth) { - QDesktopWidget *dw = QApplication::desktop(); QRect rect; if (isGrowLimit) - rect = dw->screenGeometry(this); + { + // Widget position can be negative, we should clamp it. + QPoint position = pos(); + if (position.x() <= 0) + position.setX(0); + if (position.y() <= 0) + position.setY(0); + + rect = QApplication::screenAt(position)->geometry(); + } else - rect = QGuiApplication::screens().at(dw->screenNumber(this))->geometry(); + rect = desktopRect(); if (!mScrollbarOnly && mScroll && mSubViews.size() > 1) { - int newWidth = width()+minSubViewWidth; + int newWidth = width() + minSubViewWidth; int frameWidth = frameGeometry().width() - width(); - if (newWidth+frameWidth <= rect.width()) + if (newWidth + frameWidth <= rect.width()) { resize(newWidth, height()); // WARNING: below code assumes that new subviews are added to the right - if (x() > rect.width()-(newWidth+frameWidth)) - move(rect.width()-(newWidth+frameWidth), y()); // shift left to stay within the screen + if (x() > rect.width() - (newWidth + frameWidth)) + move(rect.width() - (newWidth + frameWidth), y()); // shift left to stay within the screen } else { // full width - resize(rect.width()-frameWidth, height()); - mSubViewWindow.setMinimumWidth(mSubViewWindow.width()+minSubViewWidth); + resize(rect.width() - frameWidth, height()); + mSubViewWindow.setMinimumWidth(mSubViewWindow.width() + minSubViewWidth); move(0, y()); } } @@ -1099,15 +1153,15 @@ void CSVDoc::View::createScrollArea() setCentralWidget(mScroll); } -void CSVDoc::View::onRequestFocus (const std::string& id) +void CSVDoc::View::onRequestFocus(const std::string& id) { - if(CSMPrefs::get()["3D Scene Editing"]["open-list-view"].isTrue()) + if (CSMPrefs::get()["3D Scene Editing"]["open-list-view"].isTrue()) { addReferencesSubView(); emit requestFocus(id); } else { - addSubView(CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Reference, id)); + addSubView(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Reference, id)); } } diff --git a/apps/opencs/view/doc/view.hpp b/apps/opencs/view/doc/view.hpp index 322bcdfb7f4..165cb0da8e3 100644 --- a/apps/opencs/view/doc/view.hpp +++ b/apps/opencs/view/doc/view.hpp @@ -1,15 +1,20 @@ #ifndef CSV_DOC_VIEW_H #define CSV_DOC_VIEW_H +#include #include -#include +#include #include +#include + #include "subviewfactory.hpp" class QAction; -class QDockWidget; +class QCloseEvent; +class QMenu; +class QObject; class QScrollArea; namespace CSMDoc @@ -17,11 +22,6 @@ namespace CSMDoc class Document; } -namespace CSMWorld -{ - class UniversalId; -} - namespace CSMPrefs { class Setting; @@ -30,246 +30,242 @@ namespace CSMPrefs namespace CSVDoc { class ViewManager; + class SubView; class Operations; class GlobalDebugProfileMenu; class View : public QMainWindow { - Q_OBJECT - - ViewManager& mViewManager; - CSMDoc::Document *mDocument; - int mViewIndex; - int mViewTotal; - QList mSubViews; - QAction *mUndo; - QAction *mRedo; - QAction *mSave; - QAction *mVerify; - QAction *mShowStatusBar; - QAction *mStopDebug; - QAction *mMerge; - std::vector mEditingActions; - Operations *mOperations; - SubViewFactoryManager mSubViewFactory; - QMainWindow mSubViewWindow; - GlobalDebugProfileMenu *mGlobalDebugProfileMenu; - QScrollArea *mScroll; - bool mScrollbarOnly; - - - // not implemented - View (const View&); - View& operator= (const View&); - - private: - - void closeEvent (QCloseEvent *event) override; + Q_OBJECT - QAction* createMenuEntry(CSMWorld::UniversalId::Type type, QMenu* menu, const char* shortcutName); - QAction* createMenuEntry(const std::string& title, const std::string& iconName, QMenu* menu, const char* shortcutName); + ViewManager& mViewManager; + CSMDoc::Document* mDocument; + int mViewIndex; + int mViewTotal; + QList mSubViews; + QAction* mUndo; + QAction* mRedo; + QAction* mSave; + QAction* mVerify; + QAction* mShowStatusBar; + QAction* mStopDebug; + QAction* mMerge; + std::vector mEditingActions; + Operations* mOperations; + SubViewFactoryManager mSubViewFactory; + QMainWindow mSubViewWindow; + GlobalDebugProfileMenu* mGlobalDebugProfileMenu; + QScrollArea* mScroll; + bool mScrollbarOnly; - void setupFileMenu(); + private: + void closeEvent(QCloseEvent* event) override; - void setupEditMenu(); + QAction* createMenuEntry(CSMWorld::UniversalId::Type type, QMenu* menu, const char* shortcutName); + QAction* createMenuEntry( + const std::string& title, const std::string& iconName, QMenu* menu, const char* shortcutName); - void setupViewMenu(); + void setupFileMenu(); - void setupWorldMenu(); + void setupEditMenu(); - void setupMechanicsMenu(); + void setupViewMenu(); - void setupCharacterMenu(); + void setupWorldMenu(); - void setupAssetsMenu(); + void setupMechanicsMenu(); - void setupDebugMenu(); + void setupCharacterMenu(); - void setupHelpMenu(); + void setupAssetsMenu(); - void setupUi(); + void setupDebugMenu(); - void setupShortcut(const char* name, QAction* action); + void setupHelpMenu(); - void updateActions(); + void setupUi(); - void exitApplication(); + void setupShortcut(const char* name, QAction* action); - /// User preference function - void resizeViewWidth (int width); + void updateActions(); - /// User preference function - void resizeViewHeight (int height); + void exitApplication(); - void updateScrollbar(); - void updateWidth(bool isGrowLimit, int minSubViewWidth); - void createScrollArea(); - public: + /// User preference function + void resizeViewWidth(int width); - View (ViewManager& viewManager, CSMDoc::Document *document, int totalViews); + /// User preference function + void resizeViewHeight(int height); - ///< The ownership of \a document is not transferred to *this. + void updateScrollbar(); + void updateWidth(bool isGrowLimit, int minSubViewWidth); + void createScrollArea(); - virtual ~View(); + public: + View(ViewManager& viewManager, CSMDoc::Document* document, int totalViews); + ///< The ownership of \a document is not transferred to *this. + View(const View&) = delete; + View& operator=(const View&) = delete; + ~View() override = default; - const CSMDoc::Document *getDocument() const; + const CSMDoc::Document* getDocument() const; - CSMDoc::Document *getDocument(); + CSMDoc::Document* getDocument(); - void setIndex (int viewIndex, int totalViews); + void setIndex(int viewIndex, int totalViews); - void updateDocumentState(); + void updateDocumentState(); - void updateProgress (int current, int max, int type, int threads); + void updateProgress(int current, int max, int type, int threads); - void toggleStatusBar(bool checked); + void toggleStatusBar(bool checked); - Operations *getOperations() const; + Operations* getOperations() const; - signals: + signals: - void newGameRequest(); + void newGameRequest(); - void newAddonRequest(); + void newAddonRequest(); - void loadDocumentRequest(); + void loadDocumentRequest(); - void exitApplicationRequest (CSVDoc::View *view); + void exitApplicationRequest(CSVDoc::View* view); - void editSettingsRequest(); + void editSettingsRequest(); - void mergeDocument (CSMDoc::Document *document); + void mergeDocument(CSMDoc::Document* document); - void requestFocus (const std::string& id); + void requestFocus(const std::string& id); - public slots: + public slots: - void addSubView (const CSMWorld::UniversalId& id, const std::string& hint = ""); - ///< \param hint Suggested view point (e.g. coordinates in a 3D scene or a line number - /// in a script). + void addSubView(const CSMWorld::UniversalId& id, const std::string& hint = ""); + ///< \param hint Suggested view point (e.g. coordinates in a 3D scene or a line number + /// in a script). - void abortOperation (int type); + void abortOperation(int type); - void updateTitle(); + void updateTitle(); - // called when subviews are added or removed - void updateSubViewIndices (SubView *view = nullptr); + // called when subviews are added or removed + void updateSubViewIndices(SubView* view = nullptr); - private slots: + private slots: - void settingChanged (const CSMPrefs::Setting *setting); + void settingChanged(const CSMPrefs::Setting* setting); - void undoActionChanged(); + void undoActionChanged(); - void redoActionChanged(); + void redoActionChanged(); - void newView(); + void newView(); - void save(); + void save(); - void exit(); + void exit(); - static void openHelp(); + static void openHelp(); - static void tutorial(); + static void tutorial(); - void infoAbout(); + void infoAbout(); - void infoAboutQt(); + void infoAboutQt(); - void verify(); + void verify(); - void addGlobalsSubView(); + void addGlobalsSubView(); - void addGmstsSubView(); + void addGmstsSubView(); - void addSkillsSubView(); + void addSkillsSubView(); - void addClassesSubView(); + void addClassesSubView(); - void addFactionsSubView(); + void addFactionsSubView(); - void addRacesSubView(); + void addRacesSubView(); - void addSoundsSubView(); + void addSoundsSubView(); - void addScriptsSubView(); + void addScriptsSubView(); - void addRegionsSubView(); + void addRegionsSubView(); - void addBirthsignsSubView(); + void addBirthsignsSubView(); - void addSpellsSubView(); + void addSpellsSubView(); - void addCellsSubView(); + void addCellsSubView(); - void addReferenceablesSubView(); + void addReferenceablesSubView(); - void addReferencesSubView(); + void addReferencesSubView(); - void addRegionMapSubView(); + void addRegionMapSubView(); - void addFiltersSubView(); + void addFiltersSubView(); - void addTopicsSubView(); + void addTopicsSubView(); - void addJournalsSubView(); + void addJournalsSubView(); - void addTopicInfosSubView(); + void addTopicInfosSubView(); - void addJournalInfosSubView(); + void addJournalInfosSubView(); - void addEnchantmentsSubView(); + void addEnchantmentsSubView(); - void addBodyPartsSubView(); + void addBodyPartsSubView(); - void addSoundGensSubView(); + void addSoundGensSubView(); - void addMagicEffectsSubView(); + void addMagicEffectsSubView(); - void addMeshesSubView(); + void addMeshesSubView(); - void addIconsSubView(); + void addIconsSubView(); - void addMusicsSubView(); + void addMusicsSubView(); - void addSoundsResSubView(); + void addSoundsResSubView(); - void addTexturesSubView(); + void addTexturesSubView(); - void addVideosSubView(); + void addVideosSubView(); - void addDebugProfilesSubView(); + void addDebugProfilesSubView(); - void addRunLogSubView(); + void addRunLogSubView(); - void addLandsSubView(); + void addLandsSubView(); - void addLandTexturesSubView(); + void addLandTexturesSubView(); - void addPathgridSubView(); + void addPathgridSubView(); - void addStartScriptsSubView(); + void addStartScriptsSubView(); - void addSearchSubView(); + void addSearchSubView(); - void addMetaDataSubView(); + void addMetaDataSubView(); - void toggleShowStatusBar (bool show); + void toggleShowStatusBar(bool show); - void loadErrorLog(); + void loadErrorLog(); - void run (const std::string& profile, const std::string& startupInstruction = ""); + void run(const std::string& profile, const std::string& startupInstruction = ""); - void stop(); + void stop(); - void closeRequest (SubView *subView); + void closeRequest(SubView* subView); - void moveScrollBarToEnd(int min, int max); + void moveScrollBarToEnd(int min, int max); - void merge(); + void merge(); - void onRequestFocus (const std::string& id); + void onRequestFocus(const std::string& id); }; } diff --git a/apps/opencs/view/doc/viewmanager.cpp b/apps/opencs/view/doc/viewmanager.cpp index 95f9f606da5..812a1bd5342 100644 --- a/apps/opencs/view/doc/viewmanager.cpp +++ b/apps/opencs/view/doc/viewmanager.cpp @@ -1,72 +1,80 @@ #include "viewmanager.hpp" -#include +#include +#include +#include #include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include #include -#include #include #include -#include "../../model/doc/documentmanager.hpp" #include "../../model/doc/document.hpp" -#include "../../model/world/columns.hpp" -#include "../../model/world/idcompletionmanager.hpp" +#include "../../model/doc/documentmanager.hpp" #include "../../model/prefs/state.hpp" -#include "../world/util.hpp" +#include "../world/colordelegate.hpp" #include "../world/enumdelegate.hpp" -#include "../world/vartypedelegate.hpp" -#include "../world/recordstatusdelegate.hpp" -#include "../world/idtypedelegate.hpp" #include "../world/idcompletiondelegate.hpp" -#include "../world/colordelegate.hpp" +#include "../world/idtypedelegate.hpp" +#include "../world/recordstatusdelegate.hpp" +#include "../world/vartypedelegate.hpp" #include "view.hpp" void CSVDoc::ViewManager::updateIndices() { - std::map > documents; + std::map> documents; - for (std::vector::const_iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) + for (std::vector::const_iterator iter(mViews.begin()); iter != mViews.end(); ++iter) { - std::map >::iterator document = documents.find ((*iter)->getDocument()); + std::map>::iterator document = documents.find((*iter)->getDocument()); - if (document==documents.end()) - document = - documents.insert ( - std::make_pair ((*iter)->getDocument(), std::make_pair (0, countViews ((*iter)->getDocument())))). - first; + if (document == documents.end()) + document = documents + .insert(std::make_pair( + (*iter)->getDocument(), std::make_pair(0, countViews((*iter)->getDocument())))) + .first; - (*iter)->setIndex (document->second.first++, document->second.second); + (*iter)->setIndex(document->second.first++, document->second.second); } } -CSVDoc::ViewManager::ViewManager (CSMDoc::DocumentManager& documentManager) - : mDocumentManager (documentManager), mExitOnSaveStateChange(false), mUserWarned(false) +CSVDoc::ViewManager::ViewManager(CSMDoc::DocumentManager& documentManager) + : mDocumentManager(documentManager) + , mExitOnSaveStateChange(false) + , mUserWarned(false) { mDelegateFactories = new CSVWorld::CommandDelegateFactoryCollection; - mDelegateFactories->add (CSMWorld::ColumnBase::Display_GmstVarType, - new CSVWorld::VarTypeDelegateFactory (ESM::VT_None, ESM::VT_String, ESM::VT_Int, ESM::VT_Float)); + mDelegateFactories->add(CSMWorld::ColumnBase::Display_GmstVarType, + new CSVWorld::VarTypeDelegateFactory(ESM::VT_None, ESM::VT_String, ESM::VT_Int, ESM::VT_Float)); - mDelegateFactories->add (CSMWorld::ColumnBase::Display_GlobalVarType, - new CSVWorld::VarTypeDelegateFactory (ESM::VT_Short, ESM::VT_Long, ESM::VT_Float)); + mDelegateFactories->add(CSMWorld::ColumnBase::Display_GlobalVarType, + new CSVWorld::VarTypeDelegateFactory(ESM::VT_Short, ESM::VT_Long, ESM::VT_Float)); - mDelegateFactories->add (CSMWorld::ColumnBase::Display_RecordState, - new CSVWorld::RecordStatusDelegateFactory()); + mDelegateFactories->add(CSMWorld::ColumnBase::Display_RecordState, new CSVWorld::RecordStatusDelegateFactory()); - mDelegateFactories->add (CSMWorld::ColumnBase::Display_RefRecordType, - new CSVWorld::IdTypeDelegateFactory()); + mDelegateFactories->add(CSMWorld::ColumnBase::Display_RefRecordType, new CSVWorld::IdTypeDelegateFactory()); - mDelegateFactories->add (CSMWorld::ColumnBase::Display_Colour, - new CSVWorld::ColorDelegateFactory()); + mDelegateFactories->add(CSMWorld::ColumnBase::Display_Colour, new CSVWorld::ColorDelegateFactory()); std::vector idCompletionColumns = CSMWorld::IdCompletionManager::getDisplayTypes(); for (std::vector::const_iterator current = idCompletionColumns.begin(); - current != idCompletionColumns.end(); - ++current) + current != idCompletionColumns.end(); ++current) { mDelegateFactories->add(*current, new CSVWorld::IdCompletionDelegateFactory()); } @@ -78,8 +86,7 @@ CSVDoc::ViewManager::ViewManager (CSMDoc::DocumentManager& documentManager) bool mAllowNone; }; - static const Mapping sMapping[] = - { + static const Mapping sMapping[] = { { CSMWorld::ColumnBase::Display_Specialisation, CSMWorld::Columns::ColumnId_Specialisation, false }, { CSMWorld::ColumnBase::Display_Attribute, CSMWorld::Columns::ColumnId_Attribute, true }, { CSMWorld::ColumnBase::Display_SpellType, CSMWorld::Columns::ColumnId_SpellType, false }, @@ -109,80 +116,68 @@ CSVDoc::ViewManager::ViewManager (CSMDoc::DocumentManager& documentManager) { CSMWorld::ColumnBase::Display_BookType, CSMWorld::Columns::ColumnId_BookType, false }, { CSMWorld::ColumnBase::Display_BloodType, CSMWorld::Columns::ColumnId_BloodType, false }, { CSMWorld::ColumnBase::Display_EmitterType, CSMWorld::Columns::ColumnId_EmitterType, false }, - { CSMWorld::ColumnBase::Display_GenderNpc, CSMWorld::Columns::ColumnId_Gender, false } + { CSMWorld::ColumnBase::Display_GenderNpc, CSMWorld::Columns::ColumnId_Gender, false }, }; - for (std::size_t i=0; iadd (sMapping[i].mDisplay, new CSVWorld::EnumDelegateFactory ( - CSMWorld::Columns::getEnums (sMapping[i].mColumnId), sMapping[i].mAllowNone)); + for (std::size_t i = 0; i < sizeof(sMapping) / sizeof(Mapping); ++i) + mDelegateFactories->add(sMapping[i].mDisplay, + new CSVWorld::EnumDelegateFactory( + CSMWorld::Columns::getEnums(sMapping[i].mColumnId), sMapping[i].mAllowNone)); - connect (&mDocumentManager, SIGNAL (loadRequest (CSMDoc::Document *)), - &mLoader, SLOT (add (CSMDoc::Document *))); + connect(&mDocumentManager, &CSMDoc::DocumentManager::loadRequest, &mLoader, &Loader::add); - connect ( - &mDocumentManager, SIGNAL (loadingStopped (CSMDoc::Document *, bool, const std::string&)), - &mLoader, SLOT (loadingStopped (CSMDoc::Document *, bool, const std::string&))); + connect(&mDocumentManager, &CSMDoc::DocumentManager::loadingStopped, &mLoader, &Loader::loadingStopped); - connect ( - &mDocumentManager, SIGNAL (nextStage (CSMDoc::Document *, const std::string&, int)), - &mLoader, SLOT (nextStage (CSMDoc::Document *, const std::string&, int))); + connect(&mDocumentManager, &CSMDoc::DocumentManager::nextStage, &mLoader, &Loader::nextStage); - connect ( - &mDocumentManager, SIGNAL (nextRecord (CSMDoc::Document *, int)), - &mLoader, SLOT (nextRecord (CSMDoc::Document *, int))); + connect(&mDocumentManager, &CSMDoc::DocumentManager::nextRecord, &mLoader, &Loader::nextRecord); - connect ( - &mDocumentManager, SIGNAL (loadMessage (CSMDoc::Document *, const std::string&)), - &mLoader, SLOT (loadMessage (CSMDoc::Document *, const std::string&))); + connect(&mDocumentManager, &CSMDoc::DocumentManager::loadMessage, &mLoader, &Loader::loadMessage); - connect ( - &mLoader, SIGNAL (cancel (CSMDoc::Document *)), - &mDocumentManager, SIGNAL (cancelLoading (CSMDoc::Document *))); + connect(&mLoader, &Loader::cancel, &mDocumentManager, &CSMDoc::DocumentManager::cancelLoading); - connect ( - &mLoader, SIGNAL (close (CSMDoc::Document *)), - &mDocumentManager, SLOT (removeDocument (CSMDoc::Document *))); + connect(&mLoader, &Loader::close, &mDocumentManager, &CSMDoc::DocumentManager::removeDocument); } CSVDoc::ViewManager::~ViewManager() { delete mDelegateFactories; - for (std::vector::iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) + for (std::vector::iterator iter(mViews.begin()); iter != mViews.end(); ++iter) delete *iter; } -CSVDoc::View *CSVDoc::ViewManager::addView (CSMDoc::Document *document) +CSVDoc::View* CSVDoc::ViewManager::addView(CSMDoc::Document* document) { - if (countViews (document)==0) + if (countViews(document) == 0) { // new document - connect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), - this, SLOT (documentStateChanged (int, CSMDoc::Document *))); + connect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::documentStateChanged); - connect (document, SIGNAL (progress (int, int, int, int, CSMDoc::Document *)), - this, SLOT (progress (int, int, int, int, CSMDoc::Document *))); + connect(document, qOverload(&CSMDoc::Document::progress), this, + &ViewManager::progress); } - View *view = new View (*this, document, countViews (document)+1); + View* view = new View(*this, document, countViews(document) + 1); - mViews.push_back (view); + mViews.push_back(view); - view->toggleStatusBar (CSMPrefs::get()["Windows"]["show-statusbar"].isTrue()); + view->toggleStatusBar(CSMPrefs::get()["Windows"]["show-statusbar"].isTrue()); view->show(); - connect (view, SIGNAL (newGameRequest ()), this, SIGNAL (newGameRequest())); - connect (view, SIGNAL (newAddonRequest ()), this, SIGNAL (newAddonRequest())); - connect (view, SIGNAL (loadDocumentRequest ()), this, SIGNAL (loadDocumentRequest())); - connect (view, SIGNAL (editSettingsRequest()), this, SIGNAL (editSettingsRequest())); - connect (view, SIGNAL (mergeDocument (CSMDoc::Document *)), this, SIGNAL (mergeDocument (CSMDoc::Document *))); + connect(view, &View::newGameRequest, this, &ViewManager::newGameRequest); + connect(view, &View::newAddonRequest, this, &ViewManager::newAddonRequest); + connect(view, &View::loadDocumentRequest, this, &ViewManager::loadDocumentRequest); + connect(view, &View::editSettingsRequest, this, &ViewManager::editSettingsRequest); + connect(view, &View::mergeDocument, this, &ViewManager::mergeDocument); updateIndices(); return view; } -CSVDoc::View *CSVDoc::ViewManager::addView (CSMDoc::Document *document, const CSMWorld::UniversalId& id, const std::string& hint) +CSVDoc::View* CSVDoc::ViewManager::addView( + CSMDoc::Document* document, const CSMWorld::UniversalId& id, const std::string& hint) { View* view = addView(document); view->addSubView(id, hint); @@ -190,33 +185,33 @@ CSVDoc::View *CSVDoc::ViewManager::addView (CSMDoc::Document *document, const CS return view; } -int CSVDoc::ViewManager::countViews (const CSMDoc::Document *document) const +int CSVDoc::ViewManager::countViews(const CSMDoc::Document* document) const { int count = 0; - for (std::vector::const_iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) - if ((*iter)->getDocument()==document) + for (std::vector::const_iterator iter(mViews.begin()); iter != mViews.end(); ++iter) + if ((*iter)->getDocument() == document) ++count; return count; } -bool CSVDoc::ViewManager::closeRequest (View *view) +bool CSVDoc::ViewManager::closeRequest(View* view) { - std::vector::iterator iter = std::find (mViews.begin(), mViews.end(), view); + std::vector::iterator iter = std::find(mViews.begin(), mViews.end(), view); bool continueWithClose = false; - if (iter!=mViews.end()) + if (iter != mViews.end()) { - bool last = countViews (view->getDocument())<=1; + bool last = countViews(view->getDocument()) <= 1; if (last) - continueWithClose = notifySaveOnClose (view); + continueWithClose = notifySaveOnClose(view); else { (*iter)->deleteLater(); - mViews.erase (iter); + mViews.erase(iter); updateIndices(); } @@ -226,16 +221,16 @@ bool CSVDoc::ViewManager::closeRequest (View *view) } // NOTE: This method assumes that it is called only if the last document -void CSVDoc::ViewManager::removeDocAndView (CSMDoc::Document *document) +void CSVDoc::ViewManager::removeDocAndView(CSMDoc::Document* document) { - for (std::vector::iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) + for (std::vector::iterator iter(mViews.begin()); iter != mViews.end(); ++iter) { // the first match should also be the only match - if((*iter)->getDocument() == document) + if ((*iter)->getDocument() == document) { mDocumentManager.removeDocument(document); (*iter)->deleteLater(); - mViews.erase (iter); + mViews.erase(iter); updateIndices(); return; @@ -243,43 +238,43 @@ void CSVDoc::ViewManager::removeDocAndView (CSMDoc::Document *document) } } -bool CSVDoc::ViewManager::notifySaveOnClose (CSVDoc::View *view) +bool CSVDoc::ViewManager::notifySaveOnClose(CSVDoc::View* view) { bool result = true; - CSMDoc::Document *document = view->getDocument(); + CSMDoc::Document* document = view->getDocument(); - //notify user of saving in progress - if ( (document->getState() & CSMDoc::State_Saving) ) - result = showSaveInProgressMessageBox (view); + // notify user of saving in progress + if ((document->getState() & CSMDoc::State_Saving)) + result = showSaveInProgressMessageBox(view); - //notify user of unsaved changes and process response - else if ( document->getState() & CSMDoc::State_Modified) - result = showModifiedDocumentMessageBox (view); + // notify user of unsaved changes and process response + else if (document->getState() & CSMDoc::State_Modified) + result = showModifiedDocumentMessageBox(view); return result; } -bool CSVDoc::ViewManager::showModifiedDocumentMessageBox (CSVDoc::View *view) +bool CSVDoc::ViewManager::showModifiedDocumentMessageBox(CSVDoc::View* view) { emit closeMessageBox(); QMessageBox messageBox(view); - CSMDoc::Document *document = view->getDocument(); - - messageBox.setWindowTitle (QString::fromUtf8(document->getSavePath().filename().string().c_str())); - messageBox.setText ("The document has been modified."); - messageBox.setInformativeText ("Do you want to save your changes?"); - messageBox.setStandardButtons (QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); - messageBox.setDefaultButton (QMessageBox::Save); - messageBox.setWindowModality (Qt::NonModal); + CSMDoc::Document* document = view->getDocument(); + + messageBox.setWindowTitle(Files::pathToQString(document->getSavePath().filename())); + messageBox.setText("The document has been modified."); + messageBox.setInformativeText("Do you want to save your changes?"); + messageBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + messageBox.setDefaultButton(QMessageBox::Save); + messageBox.setWindowModality(Qt::NonModal); messageBox.hide(); messageBox.show(); bool retVal = true; - connect (this, SIGNAL (closeMessageBox()), &messageBox, SLOT (close())); + connect(this, &ViewManager::closeMessageBox, &messageBox, &QMessageBox::close); - connect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); + connect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::onExitWarningHandler); mUserWarned = true; int response = messageBox.exec(); @@ -292,65 +287,64 @@ bool CSVDoc::ViewManager::showModifiedDocumentMessageBox (CSVDoc::View *view) document->save(); mExitOnSaveStateChange = true; retVal = false; - break; + break; case QMessageBox::Discard: - disconnect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); - break; + disconnect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::onExitWarningHandler); + break; case QMessageBox::Cancel: - //disconnect to prevent unintended view closures - disconnect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); + // disconnect to prevent unintended view closures + disconnect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::onExitWarningHandler); retVal = false; - break; + break; default: - break; - + break; } return retVal; } -bool CSVDoc::ViewManager::showSaveInProgressMessageBox (CSVDoc::View *view) +bool CSVDoc::ViewManager::showSaveInProgressMessageBox(CSVDoc::View* view) { QMessageBox messageBox; - CSMDoc::Document *document = view->getDocument(); + CSMDoc::Document* document = view->getDocument(); - messageBox.setText ("The document is currently being saved."); + messageBox.setText("The document is currently being saved."); messageBox.setInformativeText("Do you want to close now and abort saving, or wait until saving has completed?"); - QPushButton* waitButton = messageBox.addButton (tr("Wait"), QMessageBox::YesRole); - QPushButton* closeButton = messageBox.addButton (tr("Close Now"), QMessageBox::RejectRole); - QPushButton* cancelButton = messageBox.addButton (tr("Cancel"), QMessageBox::NoRole); + QPushButton* waitButton = messageBox.addButton(tr("Wait"), QMessageBox::YesRole); + QPushButton* closeButton = messageBox.addButton(tr("Close Now"), QMessageBox::RejectRole); + QPushButton* cancelButton = messageBox.addButton(tr("Cancel"), QMessageBox::NoRole); - messageBox.setDefaultButton (waitButton); + messageBox.setDefaultButton(waitButton); bool retVal = true; - //Connections shut down message box if operation ends before user makes a decision. - connect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); - connect (this, SIGNAL (closeMessageBox()), &messageBox, SLOT (close())); + // Connections shut down message box if operation ends before user makes a decision. + connect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::onExitWarningHandler); + connect(this, &ViewManager::closeMessageBox, &messageBox, &QMessageBox::close); - //set / clear the user warned flag to indicate whether or not the message box is currently active. + // set / clear the user warned flag to indicate whether or not the message box is currently active. mUserWarned = true; messageBox.exec(); mUserWarned = false; - //if closed by the warning handler, defaults to the RejectRole button (closeButton) + // if closed by the warning handler, defaults to the RejectRole button (closeButton) if (messageBox.clickedButton() == waitButton) { - //save the View iterator for shutdown after the save operation ends + // save the View iterator for shutdown after the save operation ends mExitOnSaveStateChange = true; retVal = false; } else if (messageBox.clickedButton() == closeButton) { - //disconnect to avoid segmentation fault - disconnect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); + // disconnect to avoid segmentation fault + disconnect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::onExitWarningHandler); view->abortOperation(CSMDoc::State_Saving); mExitOnSaveStateChange = true; @@ -358,79 +352,79 @@ bool CSVDoc::ViewManager::showSaveInProgressMessageBox (CSVDoc::View *view) else if (messageBox.clickedButton() == cancelButton) { - //abort shutdown, allow save to complete - //disconnection to prevent unintended view closures + // abort shutdown, allow save to complete + // disconnection to prevent unintended view closures mExitOnSaveStateChange = false; - disconnect (document, SIGNAL (stateChanged (int, CSMDoc::Document *)), this, SLOT (onExitWarningHandler(int, CSMDoc::Document *))); + disconnect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::onExitWarningHandler); retVal = false; } return retVal; } -void CSVDoc::ViewManager::documentStateChanged (int state, CSMDoc::Document *document) +void CSVDoc::ViewManager::documentStateChanged(int state, CSMDoc::Document* document) { - for (std::vector::const_iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) - if ((*iter)->getDocument()==document) - (*iter)->updateDocumentState(); + for (std::vector::const_iterator iter(mViews.begin()); iter != mViews.end(); ++iter) + if ((*iter)->getDocument() == document) + (*iter)->updateDocumentState(); } -void CSVDoc::ViewManager::progress (int current, int max, int type, int threads, CSMDoc::Document *document) +void CSVDoc::ViewManager::progress(int current, int max, int type, int threads, CSMDoc::Document* document) { - for (std::vector::const_iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) - if ((*iter)->getDocument()==document) - (*iter)->updateProgress (current, max, type, threads); + for (std::vector::const_iterator iter(mViews.begin()); iter != mViews.end(); ++iter) + if ((*iter)->getDocument() == document) + (*iter)->updateProgress(current, max, type, threads); } -void CSVDoc::ViewManager::onExitWarningHandler (int state, CSMDoc::Document *document) +void CSVDoc::ViewManager::onExitWarningHandler(int state, CSMDoc::Document* document) { - if ( !(state & CSMDoc::State_Saving) ) + if (!(state & CSMDoc::State_Saving)) { - //if the user is being warned (message box is active), shut down the message box, - //as there is no save operation currently running - if ( mUserWarned ) + // if the user is being warned (message box is active), shut down the message box, + // as there is no save operation currently running + if (mUserWarned) emit closeMessageBox(); - //otherwise, the user has closed the message box before the save operation ended. - //exit the application + // otherwise, the user has closed the message box before the save operation ended. + // exit the application else if (mExitOnSaveStateChange) QApplication::instance()->exit(); } } -bool CSVDoc::ViewManager::removeDocument (CSVDoc::View *view) +bool CSVDoc::ViewManager::removeDocument(CSVDoc::View* view) { - if(!notifySaveOnClose(view)) + if (!notifySaveOnClose(view)) return false; else { // don't bother closing views or updating indicies, but remove from mViews - CSMDoc::Document * document = view->getDocument(); - std::vector remainingViews; - std::vector::const_iterator iter = mViews.begin(); - for (; iter!=mViews.end(); ++iter) + CSMDoc::Document* document = view->getDocument(); + std::vector remainingViews; + std::vector::const_iterator iter = mViews.begin(); + for (; iter != mViews.end(); ++iter) { - if(document == (*iter)->getDocument()) + if (document == (*iter)->getDocument()) (*iter)->setVisible(false); else remainingViews.push_back(*iter); } mDocumentManager.removeDocument(document); - mViews = remainingViews; + mViews = std::move(remainingViews); } return true; } -void CSVDoc::ViewManager::exitApplication (CSVDoc::View *view) +void CSVDoc::ViewManager::exitApplication(CSVDoc::View* view) { - if(!removeDocument(view)) // close the current document first + if (!removeDocument(view)) // close the current document first return; - while(!mViews.empty()) // attempt to close all other documents + while (!mViews.empty()) // attempt to close all other documents { mViews.back()->activateWindow(); mViews.back()->raise(); // raise the window to alert the user - if(!removeDocument(mViews.back())) + if (!removeDocument(mViews.back())) return; } // Editor exits (via a signal) when the last document is deleted diff --git a/apps/opencs/view/doc/viewmanager.hpp b/apps/opencs/view/doc/viewmanager.hpp index 8424be6f895..779d059ba73 100644 --- a/apps/opencs/view/doc/viewmanager.hpp +++ b/apps/opencs/view/doc/viewmanager.hpp @@ -1,6 +1,7 @@ #ifndef CSV_DOC_VIEWMANAGER_H #define CSV_DOC_VIEWMANAGER_H +#include #include #include @@ -29,67 +30,66 @@ namespace CSVDoc class ViewManager : public QObject { - Q_OBJECT + Q_OBJECT - CSMDoc::DocumentManager& mDocumentManager; - std::vector mViews; - CSVWorld::CommandDelegateFactoryCollection *mDelegateFactories; - bool mExitOnSaveStateChange; - bool mUserWarned; - Loader mLoader; + CSMDoc::DocumentManager& mDocumentManager; + std::vector mViews; + CSVWorld::CommandDelegateFactoryCollection* mDelegateFactories; + bool mExitOnSaveStateChange; + bool mUserWarned; + Loader mLoader; - // not implemented - ViewManager (const ViewManager&); - ViewManager& operator= (const ViewManager&); + // not implemented + ViewManager(const ViewManager&); + ViewManager& operator=(const ViewManager&); - void updateIndices(); - bool notifySaveOnClose (View *view = nullptr); - bool showModifiedDocumentMessageBox (View *view); - bool showSaveInProgressMessageBox (View *view); - bool removeDocument(View *view); + void updateIndices(); + bool notifySaveOnClose(View* view = nullptr); + bool showModifiedDocumentMessageBox(View* view); + bool showSaveInProgressMessageBox(View* view); + bool removeDocument(View* view); - public: + public: + ViewManager(CSMDoc::DocumentManager& documentManager); - ViewManager (CSMDoc::DocumentManager& documentManager); + ~ViewManager() override; - virtual ~ViewManager(); + View* addView(CSMDoc::Document* document); + ///< The ownership of the returned view is not transferred. - View *addView (CSMDoc::Document *document); - ///< The ownership of the returned view is not transferred. + View* addView(CSMDoc::Document* document, const CSMWorld::UniversalId& id, const std::string& hint); - View *addView (CSMDoc::Document *document, const CSMWorld::UniversalId& id, const std::string& hint); + int countViews(const CSMDoc::Document* document) const; + ///< Return number of views for \a document. - int countViews (const CSMDoc::Document *document) const; - ///< Return number of views for \a document. + bool closeRequest(View* view); + void removeDocAndView(CSMDoc::Document* document); - bool closeRequest (View *view); - void removeDocAndView (CSMDoc::Document *document); + signals: - signals: + void newGameRequest(); - void newGameRequest(); + void newAddonRequest(); - void newAddonRequest(); + void loadDocumentRequest(); - void loadDocumentRequest(); + void closeMessageBox(); - void closeMessageBox(); + void editSettingsRequest(); - void editSettingsRequest(); + void mergeDocument(CSMDoc::Document* document); - void mergeDocument (CSMDoc::Document *document); + public slots: - public slots: + void exitApplication(CSVDoc::View* view); - void exitApplication (CSVDoc::View *view); + private slots: - private slots: + void documentStateChanged(int state, CSMDoc::Document* document); - void documentStateChanged (int state, CSMDoc::Document *document); + void progress(int current, int max, int type, int threads, CSMDoc::Document* document); - void progress (int current, int max, int type, int threads, CSMDoc::Document *document); - - void onExitWarningHandler(int state, CSMDoc::Document* document); + void onExitWarningHandler(int state, CSMDoc::Document* document); }; } diff --git a/apps/opencs/view/filter/editwidget.cpp b/apps/opencs/view/filter/editwidget.cpp index 6b585591fa9..8076e99e63b 100644 --- a/apps/opencs/view/filter/editwidget.cpp +++ b/apps/opencs/view/filter/editwidget.cpp @@ -1,106 +1,129 @@ #include "editwidget.hpp" -#include +#include +#include + #include +#include #include #include +#include #include -#include +#include +#include "filterdata.hpp" + +#include +#include + +#include #include +#include +#include "../../model/prefs/shortcut.hpp" +#include "../../model/world/columns.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtablebase.hpp" -#include "../../model/world/columns.hpp" -#include "../../model/prefs/shortcut.hpp" -CSVFilter::EditWidget::EditWidget (CSMWorld::Data& data, QWidget *parent) -: QLineEdit (parent), mParser (data), mIsEmpty(true) +CSVFilter::EditWidget::EditWidget(CSMWorld::Data& data, QWidget* parent) + : QLineEdit(parent) + , mParser(data) + , mIsEmpty(true) { mPalette = palette(); - connect (this, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); - - const CSMWorld::IdTableBase *model = - static_cast (data.getTableModel (CSMWorld::UniversalId::Type_Filters)); - - connect (model, SIGNAL (dataChanged (const QModelIndex &, const QModelIndex&)), - this, SLOT (filterDataChanged (const QModelIndex &, const QModelIndex&)), - Qt::QueuedConnection); - connect (model, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), - this, SLOT (filterRowsRemoved (const QModelIndex&, int, int)), - Qt::QueuedConnection); - connect (model, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (filterRowsInserted (const QModelIndex&, int, int)), - Qt::QueuedConnection); - - mStateColumnIndex = model->findColumnIndex(CSMWorld::Columns::ColumnId_Modification); - mDescColumnIndex = model->findColumnIndex(CSMWorld::Columns::ColumnId_Description); - - mHelpAction = new QAction (tr ("Help"), this); - connect (mHelpAction, SIGNAL (triggered()), this, SLOT (openHelp())); - mHelpAction->setIcon(QIcon(":/info.png")); - addAction (mHelpAction); + connect(this, &QLineEdit::textChanged, this, &EditWidget::textChanged); + + const CSMWorld::IdTableBase* model + = static_cast(data.getTableModel(CSMWorld::UniversalId::Type_Filters)); + + connect(model, &CSMWorld::IdTableBase::dataChanged, this, &EditWidget::filterDataChanged, Qt::QueuedConnection); + connect(model, &CSMWorld::IdTableBase::rowsRemoved, this, &EditWidget::filterRowsRemoved, Qt::QueuedConnection); + connect(model, &CSMWorld::IdTableBase::rowsInserted, this, &EditWidget::filterRowsInserted, Qt::QueuedConnection); + + mStateColumnIndex = model->findColumnIndex(CSMWorld::Columns::ColumnId_Modification); + mDescColumnIndex = model->findColumnIndex(CSMWorld::Columns::ColumnId_Description); + + mHelpAction = new QAction(tr("Help"), this); + connect(mHelpAction, &QAction::triggered, this, &EditWidget::openHelp); + mHelpAction->setIcon(Misc::ScalableIcon::load(":info")); + addAction(mHelpAction); auto* openHelpShortcut = new CSMPrefs::Shortcut("help", this); openHelpShortcut->associateAction(mHelpAction); + + setText("!string(\"ID\", \".*\")"); } -void CSVFilter::EditWidget::textChanged (const QString& text) +void CSVFilter::EditWidget::textChanged(const QString& text) { - //no need to parse and apply filter if it was empty and now is empty too. - //e.g. - we modifiing content of filter with already opened some other (big) tables. - if (text.length() == 0){ + // no need to parse and apply filter if it was empty and now is empty too. + // e.g. - we modifiing content of filter with already opened some other (big) tables. + if (text.length() == 0) + { if (mIsEmpty) return; else mIsEmpty = true; - }else + } + else mIsEmpty = false; - if (mParser.parse (text.toUtf8().constData())) + if (mParser.parse(text.toUtf8().constData())) { - setPalette (mPalette); - emit filterChanged (mParser.getFilter()); + setPalette(mPalette); + emit filterChanged(mParser.getFilter()); } else { - QPalette palette (mPalette); - palette.setColor (QPalette::Text, Qt::red); - setPalette (palette); + QPalette palette(mPalette); + palette.setColor(QPalette::Text, Qt::red); + setPalette(palette); /// \todo improve error reporting; mark only the faulty part } } -void CSVFilter::EditWidget::filterDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSVFilter::EditWidget::filterDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { for (int i = topLeft.column(); i <= bottomRight.column(); ++i) if (i != mStateColumnIndex && i != mDescColumnIndex) - textChanged (text()); + textChanged(text()); } -void CSVFilter::EditWidget::filterRowsRemoved (const QModelIndex& parent, int start, int end) +void CSVFilter::EditWidget::filterRowsRemoved(const QModelIndex& parent, int start, int end) { - textChanged (text()); + textChanged(text()); } -void CSVFilter::EditWidget::filterRowsInserted (const QModelIndex& parent, int start, int end) +void CSVFilter::EditWidget::filterRowsInserted(const QModelIndex& parent, int start, int end) { - textChanged (text()); + textChanged(text()); } -void CSVFilter::EditWidget::createFilterRequest (std::vector< std::pair< std::string, std::vector< std::string > > >& filterSource, - Qt::DropAction action) +void CSVFilter::EditWidget::createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action) { - const unsigned count = filterSource.size(); + FilterType filterType = FilterType::String; + std::vector newFilter; + + for (auto filterData : sourceFilter) + { + FilterData newFilterData; + std::pair pair = std::visit(FilterVisitor(), filterData.searchData); + std::string searchString = pair.first; + filterType = pair.second; + newFilterData.searchData = searchString; + newFilterData.columns = filterData.columns; + newFilter.emplace_back(newFilterData); + } + + const unsigned count = newFilter.size(); bool multipleElements = false; - switch (count) //setting multipleElements; + switch (count) // setting multipleElements; { - case 0: //empty - return; //nothing to do here + case 0: // empty + return; // nothing to do here - case 1: //only single + case 1: // only single multipleElements = false; break; @@ -110,12 +133,12 @@ void CSVFilter::EditWidget::createFilterRequest (std::vector< std::pair< std::st } Qt::KeyboardModifiers key = QApplication::keyboardModifiers(); - QString oldContent (text()); + QString oldContent(text()); bool replaceMode = false; std::string orAnd; - switch (key) //setting replaceMode and string used to glue expressions + switch (key) // setting replaceMode and string used to glue expressions { case Qt::ShiftModifier: orAnd = "!or("; @@ -132,14 +155,17 @@ void CSVFilter::EditWidget::createFilterRequest (std::vector< std::pair< std::st break; } - if (oldContent.isEmpty() || !oldContent.contains (QRegExp ("^!.*$", Qt::CaseInsensitive))) //if line edit is empty or it does not contain one shot filter go into replace mode + if (oldContent.isEmpty() + || !oldContent.contains(QRegularExpression("^!.*$", + QRegularExpression::CaseInsensitiveOption))) // if line edit is empty or it does not contain one shot filter + // go into replace mode { replaceMode = true; } if (!replaceMode) { - oldContent.remove ('!'); + oldContent.remove('!'); } std::stringstream ss; @@ -148,86 +174,106 @@ void CSVFilter::EditWidget::createFilterRequest (std::vector< std::pair< std::st { if (replaceMode) { - ss<<"!or("; - } else { + ss << "!or("; + } + else + { ss << orAnd << oldContent.toUtf8().constData() << ','; } for (unsigned i = 0; i < count; ++i) { - ss<4) + if (ss.str().length() > 4) { clear(); - insert (QString::fromUtf8(ss.str().c_str())); + insert(QString::fromUtf8(ss.str().c_str())); } } -std::string CSVFilter::EditWidget::generateFilter (std::pair< std::string, std::vector< std::string > >& seekedString) const +std::string CSVFilter::EditWidget::generateFilter(const FilterData& filterData, FilterType filterType) const { - const unsigned columns = seekedString.second.size(); + const unsigned columns = filterData.columns.size(); bool multipleColumns = false; switch (columns) { - case 0: //empty - return ""; //no column to filter + case 0: // empty + return ""; // no column to filter + + case 1: // one column to look for + multipleColumns = false; + break; - case 1: //one column to look for - multipleColumns = false; - break; + default: + multipleColumns = true; + break; + } - default: - multipleColumns = true; - break; + std::string quotesResolved; + if (std::get_if(&filterData.searchData)) + quotesResolved = std::get(filterData.searchData); + else + { + Log(Debug::Warning) << "Generating record filter failed."; + return ""; } + if (filterType == FilterType::String) + quotesResolved = '"' + quotesResolved + '"'; std::stringstream ss; + if (multipleColumns) { - ss<<"or("; + ss << "or("; for (unsigned i = 0; i < columns; ++i) { - ss<<"string("<<'"'<addAction(mHelpAction); menu->exec(event->globalPos()); delete menu; @@ -237,4 +283,3 @@ void CSVFilter::EditWidget::openHelp() { Misc::HelpViewer::openHelp("manuals/openmw-cs/record-filters.html"); } - diff --git a/apps/opencs/view/filter/editwidget.hpp b/apps/opencs/view/filter/editwidget.hpp index f933ec92eb8..f2ecd5f6426 100644 --- a/apps/opencs/view/filter/editwidget.hpp +++ b/apps/opencs/view/filter/editwidget.hpp @@ -3,12 +3,28 @@ #include #include -#include +#include + +#include +#include +#include +#include +#include + +#include "filterdata.hpp" #include "../../model/filter/parser.hpp" -#include "../../model/filter/node.hpp" class QModelIndex; +class QAction; +class QContextMenuEvent; +class QObject; +class QWidget; + +namespace CSMFilter +{ + class Node; +} namespace CSMWorld { @@ -17,45 +33,80 @@ namespace CSMWorld namespace CSVFilter { - class EditWidget : public QLineEdit + enum class FilterType { - Q_OBJECT + String, + Value + }; + + struct FilterVisitor + { + std::pair operator()(const std::string& stringData) + { + FilterType filterType = FilterType::String; + return std::make_pair(stringData, filterType); + } + + std::pair operator()(const QVariant& variantData) + { + FilterType filterType = FilterType::String; + QMetaType::Type dataType = static_cast(variantData.type()); + if (dataType == QMetaType::QString || dataType == QMetaType::Bool || dataType == QMetaType::Int) + filterType = FilterType::String; + if (dataType == QMetaType::Int || dataType == QMetaType::Float) + filterType = FilterType::Value; + return std::make_pair(variantData.toString().toStdString(), filterType); + } + }; - CSMFilter::Parser mParser; - QPalette mPalette; - bool mIsEmpty; - int mStateColumnIndex; - int mDescColumnIndex; - QAction *mHelpAction; + class EditWidget : public QLineEdit + { + Q_OBJECT - public: + CSMFilter::Parser mParser; + QPalette mPalette; + bool mIsEmpty; + int mStateColumnIndex; + int mDescColumnIndex; + QAction* mHelpAction; - EditWidget (CSMWorld::Data& data, QWidget *parent = nullptr); + public: + EditWidget(CSMWorld::Data& data, QWidget* parent = nullptr); - void createFilterRequest(std::vector > >& filterSource, - Qt::DropAction action); + void createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action); - signals: + signals: - void filterChanged (std::shared_ptr filter); + void filterChanged(std::shared_ptr filter); private: - std::string generateFilter(std::pair >& seekedString) const; - void contextMenuEvent (QContextMenuEvent *event) override; + std::string generateFilter(const FilterData& filterData, FilterType filterType) const; - private slots: + void contextMenuEvent(QContextMenuEvent* event) override; - void textChanged (const QString& text); + constexpr std::string_view filterTypeName(const FilterType& type) const + { + switch (type) + { + case FilterType::String: + return "string"; + case FilterType::Value: + return "value"; + } + return "unknown type"; + } - void filterDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + private slots: - void filterRowsRemoved (const QModelIndex& parent, int start, int end); + void textChanged(const QString& text); - void filterRowsInserted (const QModelIndex& parent, int start, int end); + void filterDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - static void openHelp(); + void filterRowsRemoved(const QModelIndex& parent, int start, int end); + void filterRowsInserted(const QModelIndex& parent, int start, int end); + static void openHelp(); }; } diff --git a/apps/opencs/view/filter/filterbox.cpp b/apps/opencs/view/filter/filterbox.cpp index 461b131dbfd..e79b83ed51e 100644 --- a/apps/opencs/view/filter/filterbox.cpp +++ b/apps/opencs/view/filter/filterbox.cpp @@ -1,60 +1,76 @@ #include "filterbox.hpp" -#include +#include +#include +#include +#include + #include +#include +#include +#include "filterdata.hpp" #include "recordfilterbox.hpp" #include +#include +#include -CSVFilter::FilterBox::FilterBox (CSMWorld::Data& data, QWidget *parent) -: QWidget (parent) +CSVFilter::FilterBox::FilterBox(CSMWorld::Data& data, QWidget* parent) + : QWidget(parent) { - QHBoxLayout *layout = new QHBoxLayout (this); + QHBoxLayout* layout = new QHBoxLayout(this); - layout->setContentsMargins (0, 0, 0, 0); + layout->setContentsMargins(0, 0, 0, 0); - mRecordFilterBox = new RecordFilterBox (data, this); + mRecordFilterBox = new RecordFilterBox(data, this); - layout->addWidget (mRecordFilterBox); + layout->addWidget(mRecordFilterBox); - setLayout (layout); + setLayout(layout); - connect (mRecordFilterBox, - SIGNAL (filterChanged (std::shared_ptr)), - this, SIGNAL (recordFilterChanged (std::shared_ptr))); + connect(mRecordFilterBox, &RecordFilterBox::filterChanged, this, &FilterBox::recordFilterChanged); setAcceptDrops(true); } -void CSVFilter::FilterBox::setRecordFilter (const std::string& filter) +void CSVFilter::FilterBox::setRecordFilter(const std::string& filter) { - mRecordFilterBox->setFilter (filter); + mRecordFilterBox->setFilter(filter); } -void CSVFilter::FilterBox::dropEvent (QDropEvent* event) +void CSVFilter::FilterBox::dropEvent(QDropEvent* event) { - const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped return; std::vector universalIdData = mime->getData(); - - emit recordDropped(universalIdData, event->proposedAction()); + QModelIndex index = mime->getIndexAtDragStart(); + const CSVWorld::DragRecordTable* dragTable = mime->getTableOfDragStart(); + + QVariant qData; + std::string searchColumn; + if (index.isValid() && dragTable) + { + qData = dragTable->model()->data(index); + searchColumn = dragTable->model()->headerData(index.column(), Qt::Horizontal).toString().toStdString(); + } + + emit recordDropped(universalIdData, std::make_pair(qData, searchColumn), event->proposedAction()); } -void CSVFilter::FilterBox::dragEnterEvent (QDragEnterEvent* event) +void CSVFilter::FilterBox::dragEnterEvent(QDragEnterEvent* event) { event->acceptProposedAction(); } -void CSVFilter::FilterBox::dragMoveEvent (QDragMoveEvent* event) +void CSVFilter::FilterBox::dragMoveEvent(QDragMoveEvent* event) { event->accept(); } -void CSVFilter::FilterBox::createFilterRequest (std::vector< std::pair< std::string, std::vector< std::string > > >& filterSource, - Qt::DropAction action) +void CSVFilter::FilterBox::createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action) { - mRecordFilterBox->createFilterRequest(filterSource, action); + mRecordFilterBox->createFilterRequest(sourceFilter, action); } diff --git a/apps/opencs/view/filter/filterbox.hpp b/apps/opencs/view/filter/filterbox.hpp index 94b5fced30c..bb53b945580 100644 --- a/apps/opencs/view/filter/filterbox.hpp +++ b/apps/opencs/view/filter/filterbox.hpp @@ -1,17 +1,30 @@ #ifndef CSV_FILTER_FILTERBOX_H #define CSV_FILTER_FILTERBOX_H +#include +#include +#include +#include #include +#include #include -#include -#include "../../model/filter/node.hpp" -#include "../../model/world/universalid.hpp" +#include "filterdata.hpp" +class QDragEnterEvent; +class QDragMoveEvent; +class QDropEvent; +class QObject; + +namespace CSMFilter +{ + class Node; +} namespace CSMWorld { class Data; + class UniversalId; } namespace CSVFilter @@ -20,32 +33,30 @@ namespace CSVFilter class FilterBox : public QWidget { - Q_OBJECT + Q_OBJECT - RecordFilterBox *mRecordFilterBox; + RecordFilterBox* mRecordFilterBox; - public: - FilterBox (CSMWorld::Data& data, QWidget *parent = nullptr); + public: + FilterBox(CSMWorld::Data& data, QWidget* parent = nullptr); - void setRecordFilter (const std::string& filter); + void setRecordFilter(const std::string& filter); - void createFilterRequest(std::vector > >& filterSource, - Qt::DropAction action); + void createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action); + private: + void dragEnterEvent(QDragEnterEvent* event) override; - private: - void dragEnterEvent (QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; - void dropEvent (QDropEvent* event) override; + void dragMoveEvent(QDragMoveEvent* event) override; - void dragMoveEvent(QDragMoveEvent *event) override; - - signals: - void recordFilterChanged (std::shared_ptr filter); - void recordDropped (std::vector& types, Qt::DropAction action); + signals: + void recordFilterChanged(std::shared_ptr filter); + void recordDropped(std::vector& types, + const std::pair& columnSearchData, Qt::DropAction action); }; } #endif - diff --git a/apps/opencs/view/filter/filterdata.hpp b/apps/opencs/view/filter/filterdata.hpp new file mode 100644 index 00000000000..b99363712de --- /dev/null +++ b/apps/opencs/view/filter/filterdata.hpp @@ -0,0 +1,19 @@ +#ifndef CSV_FILTER_FILTERDATA_H +#define CSV_FILTER_FILTERDATA_H + +#include +#include +#include + +#include + +namespace CSVFilter +{ + struct FilterData + { + std::variant searchData; + std::vector columns; + }; +} + +#endif diff --git a/apps/opencs/view/filter/recordfilterbox.cpp b/apps/opencs/view/filter/recordfilterbox.cpp index d244658971b..deed76bfb8b 100644 --- a/apps/opencs/view/filter/recordfilterbox.cpp +++ b/apps/opencs/view/filter/recordfilterbox.cpp @@ -1,40 +1,41 @@ #include "recordfilterbox.hpp" +#include + #include #include +#include #include "editwidget.hpp" +#include "filterdata.hpp" -CSVFilter::RecordFilterBox::RecordFilterBox (CSMWorld::Data& data, QWidget *parent) -: QWidget (parent) +CSVFilter::RecordFilterBox::RecordFilterBox(CSMWorld::Data& data, QWidget* parent) + : QWidget(parent) { - QHBoxLayout *layout = new QHBoxLayout (this); + QHBoxLayout* layout = new QHBoxLayout(this); - layout->setContentsMargins (0, 6, 5, 0); + layout->setContentsMargins(0, 6, 5, 0); - QLabel *label = new QLabel("Record Filter", this); + QLabel* label = new QLabel("Record Filter", this); label->setIndent(2); - layout->addWidget (label); + layout->addWidget(label); - mEdit = new EditWidget (data, this); + mEdit = new EditWidget(data, this); - layout->addWidget (mEdit); + layout->addWidget(mEdit); - setLayout (layout); + setLayout(layout); - connect ( - mEdit, SIGNAL (filterChanged (std::shared_ptr)), - this, SIGNAL (filterChanged (std::shared_ptr))); + connect(mEdit, &EditWidget::filterChanged, this, &RecordFilterBox::filterChanged); } -void CSVFilter::RecordFilterBox::setFilter (const std::string& filter) +void CSVFilter::RecordFilterBox::setFilter(const std::string& filter) { mEdit->clear(); - mEdit->setText (QString::fromUtf8 (filter.c_str())); + mEdit->setText(QString::fromUtf8(filter.c_str())); } -void CSVFilter::RecordFilterBox::createFilterRequest (std::vector< std::pair< std::string, std::vector< std::string > > >& filterSource, - Qt::DropAction action) +void CSVFilter::RecordFilterBox::createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action) { - mEdit->createFilterRequest(filterSource, action); + mEdit->createFilterRequest(sourceFilter, action); } diff --git a/apps/opencs/view/filter/recordfilterbox.hpp b/apps/opencs/view/filter/recordfilterbox.hpp index 3bcd7c57b4d..d7fdb5cbaca 100644 --- a/apps/opencs/view/filter/recordfilterbox.hpp +++ b/apps/opencs/view/filter/recordfilterbox.hpp @@ -1,12 +1,21 @@ #ifndef CSV_FILTER_RECORDFILTERBOX_H #define CSV_FILTER_RECORDFILTERBOX_H +#include +#include +#include +#include +#include + +#include #include -#include -#include +#include "filterdata.hpp" -#include "../../model/filter/node.hpp" +namespace CSMFilter +{ + class Node; +} namespace CSMWorld { @@ -19,24 +28,22 @@ namespace CSVFilter class RecordFilterBox : public QWidget { - Q_OBJECT - - EditWidget *mEdit; + Q_OBJECT - public: + EditWidget* mEdit; - RecordFilterBox (CSMWorld::Data& data, QWidget *parent = nullptr); + public: + RecordFilterBox(CSMWorld::Data& data, QWidget* parent = nullptr); - void setFilter (const std::string& filter); + void setFilter(const std::string& filter); - void useFilterRequest(const std::string& idOfFilter); + void useFilterRequest(const std::string& idOfFilter); - void createFilterRequest(std::vector > >& filterSource, - Qt::DropAction action); + void createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action); - signals: + signals: - void filterChanged (std::shared_ptr filter); + void filterChanged(std::shared_ptr filter); }; } diff --git a/apps/opencs/view/prefs/contextmenulist.cpp b/apps/opencs/view/prefs/contextmenulist.cpp index 8115c3ecc57..bf7a5f289f9 100644 --- a/apps/opencs/view/prefs/contextmenulist.cpp +++ b/apps/opencs/view/prefs/contextmenulist.cpp @@ -1,13 +1,13 @@ #include "contextmenulist.hpp" -#include #include +#include #include #include "../../model/prefs/state.hpp" CSVPrefs::ContextMenuList::ContextMenuList(QWidget* parent) - :QListWidget(parent) + : QListWidget(parent) { } diff --git a/apps/opencs/view/prefs/contextmenulist.hpp b/apps/opencs/view/prefs/contextmenulist.hpp index f527057d2f1..42063cea189 100644 --- a/apps/opencs/view/prefs/contextmenulist.hpp +++ b/apps/opencs/view/prefs/contextmenulist.hpp @@ -10,24 +10,22 @@ namespace CSVPrefs { class ContextMenuList : public QListWidget { - Q_OBJECT - - public: - - ContextMenuList(QWidget* parent = nullptr); - - protected: - - void contextMenuEvent(QContextMenuEvent* e) override; - - void mousePressEvent(QMouseEvent* e) override; - - private slots: - - void resetCategory(); - - void resetAll(); - }; + Q_OBJECT + + public: + ContextMenuList(QWidget* parent = nullptr); + + protected: + void contextMenuEvent(QContextMenuEvent* e) override; + + void mousePressEvent(QMouseEvent* e) override; + + private slots: + + void resetCategory(); + + void resetAll(); + }; } #endif diff --git a/apps/opencs/view/prefs/dialogue.cpp b/apps/opencs/view/prefs/dialogue.cpp index 7e41fcf8220..26cb89478b6 100644 --- a/apps/opencs/view/prefs/dialogue.cpp +++ b/apps/opencs/view/prefs/dialogue.cpp @@ -1,81 +1,86 @@ #include "dialogue.hpp" +#include +#include +#include + #include -#include -#include -#include -#include #include #include +#include +#include #include +#include +#include + #include "../../model/prefs/state.hpp" -#include "page.hpp" -#include "keybindingpage.hpp" #include "contextmenulist.hpp" +#include "keybindingpage.hpp" +#include "page.hpp" -void CSVPrefs::Dialogue::buildCategorySelector (QSplitter *main) +void CSVPrefs::Dialogue::buildCategorySelector(QSplitter* main) { - CSVPrefs::ContextMenuList* list = new CSVPrefs::ContextMenuList (main); - list->setMinimumWidth (50); - list->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Expanding); + CSVPrefs::ContextMenuList* list = new CSVPrefs::ContextMenuList(main); + list->setMinimumWidth(50); + list->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - list->setSelectionBehavior (QAbstractItemView::SelectItems); + list->setSelectionBehavior(QAbstractItemView::SelectItems); - main->addWidget (list); + main->addWidget(list); - QFontMetrics metrics (QApplication::font(list)); + QFontMetrics metrics(QApplication::font(list)); int maxWidth = 1; - for (CSMPrefs::State::Iterator iter = CSMPrefs::get().begin(); iter!=CSMPrefs::get().end(); - ++iter) + for (CSMPrefs::State::Iterator iter = CSMPrefs::get().begin(); iter != CSMPrefs::get().end(); ++iter) { - QString label = QString::fromUtf8 (iter->second.getKey().c_str()); + QString label = QString::fromUtf8(iter->second.getKey().c_str()); - maxWidth = std::max (maxWidth, metrics.horizontalAdvance (label)); + maxWidth = std::max(maxWidth, metrics.horizontalAdvance(label)); - list->addItem (label); + list->addItem(label); } - list->setMaximumWidth (maxWidth + 10); + list->setMaximumWidth(maxWidth + 50); - connect (list, SIGNAL (currentItemChanged (QListWidgetItem *, QListWidgetItem *)), - this, SLOT (selectionChanged (QListWidgetItem *, QListWidgetItem *))); + connect(list, &ContextMenuList::currentItemChanged, this, &Dialogue::selectionChanged); } -void CSVPrefs::Dialogue::buildContentArea (QSplitter *main) +void CSVPrefs::Dialogue::buildContentArea(QSplitter* main) { - mContent = new QStackedWidget (main); - mContent->setSizePolicy (QSizePolicy::Preferred, QSizePolicy::Expanding); + mContent = new QStackedWidget(main); + mContent->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - main->addWidget (mContent); + main->addWidget(mContent); } -CSVPrefs::PageBase *CSVPrefs::Dialogue::makePage (const std::string& key) +CSVPrefs::PageBase* CSVPrefs::Dialogue::makePage(const std::string& key) { // special case page code goes here if (key == "Key Bindings") return new KeyBindingPage(CSMPrefs::get()[key], mContent); else - return new Page (CSMPrefs::get()[key], mContent); + return new Page(CSMPrefs::get()[key], mContent); } CSVPrefs::Dialogue::Dialogue() { - setWindowTitle ("User Settings"); + setWindowTitle("User Settings"); + + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); - setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + setMinimumSize(600, 400); - setMinimumSize (600, 400); + resize(810, 680); - QSplitter *main = new QSplitter (this); + QSplitter* main = new QSplitter(this); - setCentralWidget (main); - buildCategorySelector (main); - buildContentArea (main); + setCentralWidget(main); + buildCategorySelector(main); + buildContentArea(main); } CSVPrefs::Dialogue::~Dialogue() @@ -85,26 +90,26 @@ CSVPrefs::Dialogue::~Dialogue() if (isVisible()) CSMPrefs::State::get().save(); } - catch(const std::exception& e) + catch (const std::exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } -void CSVPrefs::Dialogue::closeEvent (QCloseEvent *event) +void CSVPrefs::Dialogue::closeEvent(QCloseEvent* event) { - QMainWindow::closeEvent (event); + QMainWindow::closeEvent(event); CSMPrefs::State::get().save(); } void CSVPrefs::Dialogue::show() { - if (QWidget *active = QApplication::activeWindow()) + if (QWidget* active = QApplication::activeWindow()) { // place at the centre of the window with focus QSize size = active->size(); - move (active->geometry().x()+(size.width() - frameGeometry().width())/2, - active->geometry().y()+(size.height() - frameGeometry().height())/2); + move(active->geometry().x() + (size.width() - frameGeometry().width()) / 2, + active->geometry().y() + (size.height() - frameGeometry().height()) / 2); } else { @@ -113,30 +118,30 @@ void CSVPrefs::Dialogue::show() // otherwise place at the centre of the screen QPoint screenCenter = scr.center(); - move (screenCenter - QPoint(frameGeometry().width()/2, frameGeometry().height()/2)); + move(screenCenter - QPoint(frameGeometry().width() / 2, frameGeometry().height() / 2)); } QWidget::show(); } -void CSVPrefs::Dialogue::selectionChanged (QListWidgetItem *current, QListWidgetItem *previous) +void CSVPrefs::Dialogue::selectionChanged(QListWidgetItem* current, QListWidgetItem* previous) { if (current) { std::string key = current->text().toUtf8().data(); - for (int i=0; icount(); ++i) + for (int i = 0; i < mContent->count(); ++i) { - PageBase& page = dynamic_cast (*mContent->widget (i)); + PageBase& page = dynamic_cast(*mContent->widget(i)); - if (page.getCategory().getKey()==key) + if (page.getCategory().getKey() == key) { - mContent->setCurrentIndex (i); + mContent->setCurrentIndex(i); return; } } - PageBase *page = makePage (key); - mContent->setCurrentIndex (mContent->addWidget (page)); + PageBase* page = makePage(key); + mContent->setCurrentIndex(mContent->addWidget(page)); } } diff --git a/apps/opencs/view/prefs/dialogue.hpp b/apps/opencs/view/prefs/dialogue.hpp index 2e09756496b..dab25d303f8 100644 --- a/apps/opencs/view/prefs/dialogue.hpp +++ b/apps/opencs/view/prefs/dialogue.hpp @@ -4,7 +4,6 @@ #include class QSplitter; -class QListWidget; class QStackedWidget; class QListWidgetItem; @@ -14,35 +13,32 @@ namespace CSVPrefs class Dialogue : public QMainWindow { - Q_OBJECT + Q_OBJECT - QStackedWidget *mContent; + QStackedWidget* mContent; - private: + private: + void buildCategorySelector(QSplitter* main); - void buildCategorySelector (QSplitter *main); + void buildContentArea(QSplitter* main); - void buildContentArea (QSplitter *main); + PageBase* makePage(const std::string& key); - PageBase *makePage (const std::string& key); + public: + Dialogue(); - public: + ~Dialogue() override; - Dialogue(); + protected: + void closeEvent(QCloseEvent* event) override; - virtual ~Dialogue(); + public slots: - protected: + void show(); - void closeEvent (QCloseEvent *event) override; + private slots: - public slots: - - void show(); - - private slots: - - void selectionChanged (QListWidgetItem *current, QListWidgetItem *previous); + void selectionChanged(QListWidgetItem* current, QListWidgetItem* previous); }; } diff --git a/apps/opencs/view/prefs/keybindingpage.cpp b/apps/opencs/view/prefs/keybindingpage.cpp index 39c9f78ec17..f292fa4cf51 100644 --- a/apps/opencs/view/prefs/keybindingpage.cpp +++ b/apps/opencs/view/prefs/keybindingpage.cpp @@ -1,15 +1,21 @@ #include "keybindingpage.hpp" #include +#include +#include +#include #include #include +#include #include #include #include -#include "../../model/prefs/setting.hpp" +#include + #include "../../model/prefs/category.hpp" +#include "../../model/prefs/setting.hpp" #include "../../model/prefs/state.hpp" namespace CSVPrefs @@ -29,15 +35,16 @@ namespace CSVPrefs mStackedLayout = new QStackedLayout(stackedWidget); mPageSelector = new QComboBox(); - connect(mPageSelector, SIGNAL(currentIndexChanged(int)), mStackedLayout, SLOT(setCurrentIndex(int))); + connect(mPageSelector, qOverload(&QComboBox::currentIndexChanged), mStackedLayout, + &QStackedLayout::setCurrentIndex); QFrame* lineSeparator = new QFrame(topWidget); lineSeparator->setFrameShape(QFrame::HLine); lineSeparator->setFrameShadow(QFrame::Sunken); // Reset key bindings button - QPushButton* resetButton = new QPushButton ("Reset to Defaults", topWidget); - connect(resetButton, SIGNAL(clicked()), this, SLOT(resetKeyBindings())); + QPushButton* resetButton = new QPushButton("Reset to Defaults", topWidget); + connect(resetButton, &QPushButton::clicked, this, &KeyBindingPage::resetKeyBindings); topLayout->addWidget(mPageSelector); topLayout->addWidget(stackedWidget); @@ -46,55 +53,44 @@ namespace CSVPrefs topLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); // Add each option - for (CSMPrefs::Category::Iterator iter = category.begin(); iter!=category.end(); ++iter) - addSetting (*iter); + for (CSMPrefs::Category::Iterator iter = category.begin(); iter != category.end(); ++iter) + addSetting(*iter); setWidgetResizable(true); setWidget(topWidget); } - void KeyBindingPage::addSetting(CSMPrefs::Setting *setting) + void KeyBindingPage::addSetting(CSMPrefs::Setting* setting) { - std::pair widgets = setting->makeWidgets (this); + const CSMPrefs::SettingWidgets widgets = setting->makeWidgets(this); - if (widgets.first) + if (widgets.mLabel != nullptr && widgets.mInput != nullptr) { // Label, Option widgets assert(mPageLayout); int next = mPageLayout->rowCount(); - mPageLayout->addWidget(widgets.first, next, 0); - mPageLayout->addWidget(widgets.second, next, 1); + mPageLayout->addWidget(widgets.mLabel, next, 0); + mPageLayout->addWidget(widgets.mInput, next, 1); } - else if (widgets.second) + else if (widgets.mInput != nullptr) { // Wide single widget assert(mPageLayout); int next = mPageLayout->rowCount(); - mPageLayout->addWidget(widgets.second, next, 0, 1, 2); + mPageLayout->addWidget(widgets.mInput, next, 0, 1, 2); } else { - if (setting->getLabel().empty()) - { - // Insert empty space - assert(mPageLayout); - - int next = mPageLayout->rowCount(); - mPageLayout->addWidget(new QWidget(), next, 0); - } - else - { - // Create new page - QWidget* pageWidget = new QWidget(); - mPageLayout = new QGridLayout(pageWidget); - mPageLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); - - mStackedLayout->addWidget(pageWidget); - - mPageSelector->addItem(QString::fromUtf8(setting->getLabel().c_str())); - } + // Create new page + QWidget* pageWidget = new QWidget(); + mPageLayout = new QGridLayout(pageWidget); + mPageLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); + + mStackedLayout->addWidget(pageWidget); + + mPageSelector->addItem(setting->getLabel()); } } diff --git a/apps/opencs/view/prefs/keybindingpage.hpp b/apps/opencs/view/prefs/keybindingpage.hpp index 05c4b22dbcd..d674e965bb4 100644 --- a/apps/opencs/view/prefs/keybindingpage.hpp +++ b/apps/opencs/view/prefs/keybindingpage.hpp @@ -6,33 +6,33 @@ class QComboBox; class QGridLayout; class QStackedLayout; +class QWidget; namespace CSMPrefs { class Setting; + class Category; } namespace CSVPrefs { class KeyBindingPage : public PageBase { - Q_OBJECT + Q_OBJECT - public: + public: + KeyBindingPage(CSMPrefs::Category& category, QWidget* parent); - KeyBindingPage(CSMPrefs::Category& category, QWidget* parent); + void addSetting(CSMPrefs::Setting* setting); - void addSetting(CSMPrefs::Setting* setting); + private: + QStackedLayout* mStackedLayout; + QGridLayout* mPageLayout; + QComboBox* mPageSelector; - private: + private slots: - QStackedLayout* mStackedLayout; - QGridLayout* mPageLayout; - QComboBox* mPageSelector; - - private slots: - - void resetKeyBindings(); + void resetKeyBindings(); }; } diff --git a/apps/opencs/view/prefs/page.cpp b/apps/opencs/view/prefs/page.cpp index c23e9f64fe9..cc74122782d 100644 --- a/apps/opencs/view/prefs/page.cpp +++ b/apps/opencs/view/prefs/page.cpp @@ -1,40 +1,41 @@ - #include "page.hpp" +#include +#include + +#include + #include +#include -#include "../../model/prefs/setting.hpp" #include "../../model/prefs/category.hpp" +#include "../../model/prefs/setting.hpp" -CSVPrefs::Page::Page (CSMPrefs::Category& category, QWidget *parent) -: PageBase (category, parent) +CSVPrefs::Page::Page(CSMPrefs::Category& category, QWidget* parent) + : PageBase(category, parent) { - QWidget *widget = new QWidget (parent); - mGrid = new QGridLayout (widget); + QWidget* widget = new QWidget(parent); + mGrid = new QGridLayout(widget); - for (CSMPrefs::Category::Iterator iter = category.begin(); iter!=category.end(); ++iter) - addSetting (*iter); + for (CSMPrefs::Category::Iterator iter = category.begin(); iter != category.end(); ++iter) + addSetting(*iter); - setWidget (widget); + setWidget(widget); } -void CSVPrefs::Page::addSetting (CSMPrefs::Setting *setting) +void CSVPrefs::Page::addSetting(CSMPrefs::Setting* setting) { - std::pair widgets = setting->makeWidgets (this); + const CSMPrefs::SettingWidgets widgets = setting->makeWidgets(this); int next = mGrid->rowCount(); - if (widgets.first) - { - mGrid->addWidget (widgets.first, next, 0); - mGrid->addWidget (widgets.second, next, 1); - } - else if (widgets.second) + if (widgets.mLabel != nullptr && widgets.mInput != nullptr) { - mGrid->addWidget (widgets.second, next, 0, 1, 2); + mGrid->addWidget(widgets.mLabel, next, 0); + mGrid->addWidget(widgets.mInput, next, 1); } - else + else if (widgets.mInput != nullptr) { - mGrid->addWidget (new QWidget (this), next, 0); + mGrid->addWidget(widgets.mInput, next, 0, 1, 2); } } diff --git a/apps/opencs/view/prefs/page.hpp b/apps/opencs/view/prefs/page.hpp index ce13e5d9b90..8968b3d5995 100644 --- a/apps/opencs/view/prefs/page.hpp +++ b/apps/opencs/view/prefs/page.hpp @@ -4,9 +4,12 @@ #include "pagebase.hpp" class QGridLayout; +class QWidget; +class QObject; namespace CSMPrefs { + class Category; class Setting; } @@ -14,15 +17,14 @@ namespace CSVPrefs { class Page : public PageBase { - Q_OBJECT + Q_OBJECT - QGridLayout *mGrid; + QGridLayout* mGrid; - public: + public: + Page(CSMPrefs::Category& category, QWidget* parent); - Page (CSMPrefs::Category& category, QWidget *parent); - - void addSetting (CSMPrefs::Setting *setting); + void addSetting(CSMPrefs::Setting* setting); }; } diff --git a/apps/opencs/view/prefs/pagebase.cpp b/apps/opencs/view/prefs/pagebase.cpp index 15535b785f6..ea9af3256de 100644 --- a/apps/opencs/view/prefs/pagebase.cpp +++ b/apps/opencs/view/prefs/pagebase.cpp @@ -1,15 +1,17 @@ #include "pagebase.hpp" -#include #include +#include #include "../../model/prefs/category.hpp" #include "../../model/prefs/state.hpp" -CSVPrefs::PageBase::PageBase (CSMPrefs::Category& category, QWidget *parent) -: QScrollArea (parent), mCategory (category) -{} +CSVPrefs::PageBase::PageBase(CSMPrefs::Category& category, QWidget* parent) + : QScrollArea(parent) + , mCategory(category) +{ +} CSMPrefs::Category& CSVPrefs::PageBase::getCategory() { diff --git a/apps/opencs/view/prefs/pagebase.hpp b/apps/opencs/view/prefs/pagebase.hpp index ce5b378b35b..b5e0836dde1 100644 --- a/apps/opencs/view/prefs/pagebase.hpp +++ b/apps/opencs/view/prefs/pagebase.hpp @@ -14,25 +14,23 @@ namespace CSVPrefs { class PageBase : public QScrollArea { - Q_OBJECT + Q_OBJECT - CSMPrefs::Category& mCategory; + CSMPrefs::Category& mCategory; - public: + public: + PageBase(CSMPrefs::Category& category, QWidget* parent); - PageBase (CSMPrefs::Category& category, QWidget *parent); + CSMPrefs::Category& getCategory(); - CSMPrefs::Category& getCategory(); + protected: + void contextMenuEvent(QContextMenuEvent*) override; - protected: + private slots: - void contextMenuEvent(QContextMenuEvent*) override; + void resetCategory(); - private slots: - - void resetCategory(); - - void resetAll(); + void resetAll(); }; } diff --git a/apps/opencs/view/render/actor.cpp b/apps/opencs/view/render/actor.cpp index d6077a65a50..1b1e4dfcaa8 100644 --- a/apps/opencs/view/render/actor.cpp +++ b/apps/opencs/view/render/actor.cpp @@ -1,11 +1,22 @@ #include "actor.hpp" +#include +#include +#include + #include +#include #include +#include + +#include +#include +#include -#include +#include +#include #include -#include +#include #include #include #include @@ -16,15 +27,14 @@ namespace CSVRender { const std::string Actor::MeshPrefix = "meshes\\"; - Actor::Actor(const std::string& id, CSMWorld::Data& data) + Actor::Actor(const ESM::RefId& id, CSMWorld::Data& data) : mId(id) , mData(data) - , mBaseNode(new osg::Group()) + , mBaseNode(new osg::PositionAttitudeTransform()) , mSkeleton(nullptr) { mActorData = mData.getActorAdapter()->getActorData(mId); - connect(mData.getActorAdapter(), SIGNAL(actorChanged(const std::string&)), - this, SLOT(handleActorChanged(const std::string&))); + connect(mData.getActorAdapter(), &CSMWorld::ActorAdapter::actorChanged, this, &Actor::handleActorChanged); } osg::Group* Actor::getBaseNode() @@ -38,7 +48,8 @@ namespace CSVRender // Load skeleton std::string skeletonModel = mActorData->getSkeleton(); - skeletonModel = Misc::ResourceHelpers::correctActorModelPath(skeletonModel, mData.getResourceSystem()->getVFS()); + skeletonModel + = Misc::ResourceHelpers::correctActorModelPath(skeletonModel, mData.getResourceSystem()->getVFS()); loadSkeleton(skeletonModel); if (!mActorData->isCreature()) @@ -50,6 +61,10 @@ namespace CSVRender // Attach parts to skeleton loadBodyParts(); + + const osg::Vec2f& attributes = mActorData->getRaceWeightHeight(); + + mBaseNode->setScale(osg::Vec3d(attributes.x(), attributes.x(), attributes.y())); } else { @@ -63,7 +78,7 @@ namespace CSVRender mSkeleton->setActive(SceneUtil::Skeleton::Active); } - void Actor::handleActorChanged(const std::string& refId) + void Actor::handleActorChanged(const ESM::RefId& refId) { if (mId == refId) { @@ -75,7 +90,7 @@ namespace CSVRender { auto sceneMgr = mData.getResourceSystem()->getSceneManager(); - osg::ref_ptr temp = sceneMgr->getInstance(model); + osg::ref_ptr temp = sceneMgr->getInstance(VFS::Path::toNormalized(model)); mSkeleton = dynamic_cast(temp.get()); if (!mSkeleton) { @@ -88,16 +103,14 @@ namespace CSVRender mNodeMap.clear(); SceneUtil::NodeMapVisitor nmVisitor(mNodeMap); mSkeleton->accept(nmVisitor); - } void Actor::loadBodyParts() { for (int i = 0; i < ESM::PRT_Count; ++i) { - auto type = (ESM::PartReferenceType) i; - std::string partId = mActorData->getPart(type); - attachBodyPart(type, getBodyPartMesh(partId)); + const auto type = static_cast(i); + attachBodyPart(type, getBodyPartMesh(mActorData->getPart(type))); } } @@ -110,16 +123,16 @@ namespace CSVRender auto node = mNodeMap.find(boneName); if (!mesh.empty() && node != mNodeMap.end()) { - auto instance = sceneMgr->getInstance(mesh); - SceneUtil::attach(instance, mSkeleton, boneName, node->second); + auto instance = sceneMgr->getInstance(VFS::Path::toNormalized(mesh)); + SceneUtil::attach(instance, mSkeleton, boneName, node->second, sceneMgr); } } - std::string Actor::getBodyPartMesh(const std::string& bodyPartId) + std::string Actor::getBodyPartMesh(const ESM::RefId& bodyPartId) { const auto& bodyParts = mData.getBodyParts(); - int index = bodyParts.searchId(bodyPartId); + const int index = bodyParts.searchId(bodyPartId); if (index != -1 && !bodyParts.getRecord(index).isDeleted()) return MeshPrefix + bodyParts.getRecord(index).get().mModel; else diff --git a/apps/opencs/view/render/actor.hpp b/apps/opencs/view/render/actor.hpp index 2f19454f780..09d896e7e79 100644 --- a/apps/opencs/view/render/actor.hpp +++ b/apps/opencs/view/render/actor.hpp @@ -2,21 +2,19 @@ #define OPENCS_VIEW_RENDER_ACTOR_H #include +#include +#include +#include #include #include -#include +#include #include #include "../../model/world/actoradapter.hpp" -namespace osg -{ - class Group; -} - namespace CSMWorld { class Data; @@ -38,7 +36,7 @@ namespace CSVRender /// \param id The referenceable id /// \param type The record type /// \param data The data store - Actor(const std::string& id, CSMWorld::Data& data); + Actor(const ESM::RefId& id, CSMWorld::Data& data); /// Retrieves the base node that meshes are attached to osg::Group* getBaseNode(); @@ -47,24 +45,24 @@ namespace CSVRender void update(); private slots: - void handleActorChanged(const std::string& refId); + void handleActorChanged(const ESM::RefId& refId); private: void loadSkeleton(const std::string& model); void loadBodyParts(); void attachBodyPart(ESM::PartReferenceType, const std::string& mesh); - std::string getBodyPartMesh(const std::string& bodyPartId); + std::string getBodyPartMesh(const ESM::RefId& bodyPartId); static const std::string MeshPrefix; - std::string mId; + ESM::RefId mId; CSMWorld::Data& mData; CSMWorld::ActorAdapter::ActorDataPtr mActorData; - osg::ref_ptr mBaseNode; + osg::ref_ptr mBaseNode; SceneUtil::Skeleton* mSkeleton; - SceneUtil::NodeMapVisitor::NodeMap mNodeMap; + SceneUtil::NodeMap mNodeMap; }; } diff --git a/apps/opencs/view/render/brushdraw.cpp b/apps/opencs/view/render/brushdraw.cpp index 255a13a12eb..3f33ae4d1b9 100644 --- a/apps/opencs/view/render/brushdraw.cpp +++ b/apps/opencs/view/render/brushdraw.cpp @@ -1,23 +1,37 @@ #include "brushdraw.hpp" #include +#include +#include -#include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include + #include "../../model/world/cellcoordinates.hpp" #include "../widget/brushshapes.hpp" #include "mask.hpp" -CSVRender::BrushDraw::BrushDraw(osg::ref_ptr parentNode, bool textureMode) : - mParentNode(parentNode), mTextureMode(textureMode) +CSVRender::BrushDraw::BrushDraw(osg::ref_ptr parentNode, bool textureMode) + : mParentNode(std::move(parentNode)) + , mTextureMode(textureMode) { mBrushDrawNode = new osg::Group(); mGeometry = new osg::Geometry(); mBrushDrawNode->addChild(mGeometry); + mBrushDrawNode->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + mBrushDrawNode->getOrCreateStateSet()->setRenderBinDetails(11, "RenderBin"); mParentNode->addChild(mBrushDrawNode); if (mTextureMode) mLandSizeFactor = static_cast(ESM::Land::REAL_SIZE) / static_cast(ESM::Land::LAND_TEXTURE_SIZE); @@ -31,7 +45,7 @@ CSVRender::BrushDraw::~BrushDraw() mParentNode->removeChild(mBrushDrawNode); } -float CSVRender::BrushDraw::getIntersectionHeight (const osg::Vec3d& point) +float CSVRender::BrushDraw::getIntersectionHeight(const osg::Vec3d& point) { osg::Vec3d start = point; osg::Vec3d end = point; @@ -40,8 +54,8 @@ float CSVRender::BrushDraw::getIntersectionHeight (const osg::Vec3d& point) osg::Vec3d direction = end - start; // Get intersection - osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector( - osgUtil::Intersector::MODEL, start, end) ); + osg::ref_ptr intersector( + new osgUtil::LineSegmentIntersector(osgUtil::Intersector::MODEL, start, end)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); osgUtil::IntersectionVisitor visitor(intersector); @@ -67,44 +81,28 @@ float CSVRender::BrushDraw::getIntersectionHeight (const osg::Vec3d& point) void CSVRender::BrushDraw::buildPointGeometry(const osg::Vec3d& point) { - osg::ref_ptr geom (new osg::Geometry()); - osg::ref_ptr vertices (new osg::Vec3Array()); - osg::ref_ptr colors (new osg::Vec4Array()); - const float brushOutlineHeight (1.0f); - const float crossHeadSize (8.0f); + osg::ref_ptr geom(new osg::Geometry()); + osg::ref_ptr vertices(new osg::Vec3Array()); + osg::ref_ptr colors(new osg::Vec4Array()); + const float brushOutlineHeight(1.0f); + const float crossHeadSize(8.0f); osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); - vertices->push_back(osg::Vec3d( - point.x() - crossHeadSize, - point.y() - crossHeadSize, - getIntersectionHeight(osg::Vec3d( - point.x() - crossHeadSize, - point.y() - crossHeadSize, - point.z()) ) + brushOutlineHeight)); + vertices->push_back(osg::Vec3d(point.x() - crossHeadSize, point.y() - crossHeadSize, + getIntersectionHeight(osg::Vec3d(point.x() - crossHeadSize, point.y() - crossHeadSize, point.z())) + + brushOutlineHeight)); colors->push_back(lineColor); - vertices->push_back(osg::Vec3d( - point.x() + crossHeadSize, - point.y() + crossHeadSize, - getIntersectionHeight(osg::Vec3d( - point.x() + crossHeadSize, - point.y() + crossHeadSize, - point.z()) ) + brushOutlineHeight)); + vertices->push_back(osg::Vec3d(point.x() + crossHeadSize, point.y() + crossHeadSize, + getIntersectionHeight(osg::Vec3d(point.x() + crossHeadSize, point.y() + crossHeadSize, point.z())) + + brushOutlineHeight)); colors->push_back(lineColor); - vertices->push_back(osg::Vec3d( - point.x() + crossHeadSize, - point.y() - crossHeadSize, - getIntersectionHeight(osg::Vec3d( - point.x() + crossHeadSize, - point.y() - crossHeadSize, - point.z()) ) + brushOutlineHeight)); + vertices->push_back(osg::Vec3d(point.x() + crossHeadSize, point.y() - crossHeadSize, + getIntersectionHeight(osg::Vec3d(point.x() + crossHeadSize, point.y() - crossHeadSize, point.z())) + + brushOutlineHeight)); colors->push_back(lineColor); - vertices->push_back(osg::Vec3d( - point.x() - crossHeadSize, - point.y() + crossHeadSize, - getIntersectionHeight(osg::Vec3d( - point.x() - crossHeadSize, - point.y() + crossHeadSize, - point.z()) ) + brushOutlineHeight)); + vertices->push_back(osg::Vec3d(point.x() - crossHeadSize, point.y() + crossHeadSize, + getIntersectionHeight(osg::Vec3d(point.x() - crossHeadSize, point.y() + crossHeadSize, point.z())) + + brushOutlineHeight)); colors->push_back(lineColor); geom->setVertexArray(vertices); @@ -115,14 +113,21 @@ void CSVRender::BrushDraw::buildPointGeometry(const osg::Vec3d& point) void CSVRender::BrushDraw::buildSquareGeometry(const float& radius, const osg::Vec3d& point) { - osg::ref_ptr geom (new osg::Geometry()); - osg::ref_ptr vertices (new osg::Vec3Array()); - osg::ref_ptr colors (new osg::Vec4Array()); + osg::ref_ptr geom(new osg::Geometry()); + osg::ref_ptr vertices(new osg::Vec3Array()); + osg::ref_ptr colors(new osg::Vec4Array()); - const float brushOutlineHeight (1.0f); + const float brushOutlineHeight(1.0f); float diameter = radius * 2; - int resolution = static_cast(2.f * diameter / mLandSizeFactor); //half a vertex resolution - float resAdjustedLandSizeFactor = mLandSizeFactor / 2; + int resolution = static_cast(2.f * diameter / mLandSizeFactor); // half a vertex resolution + float resAdjustedLandSizeFactor = mLandSizeFactor / 2; // 128 + + if (resolution > 128) // limit accuracy for performance + { + resolution = 128; + resAdjustedLandSizeFactor = diameter / resolution; + } + osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); for (int i = 0; i < resolution; i++) @@ -130,62 +135,30 @@ void CSVRender::BrushDraw::buildSquareGeometry(const float& radius, const osg::V int step = i * resAdjustedLandSizeFactor; int step2 = (i + 1) * resAdjustedLandSizeFactor; - osg::Vec3d upHorizontalLinePoint1( - point.x() - radius + step, - point.y() - radius, - getIntersectionHeight(osg::Vec3d( - point.x() - radius + step, - point.y() - radius, - point.z())) + brushOutlineHeight); - osg::Vec3d upHorizontalLinePoint2( - point.x() - radius + step2, - point.y() - radius, - getIntersectionHeight(osg::Vec3d( - point.x() - radius + step2, - point.y() - radius, - point.z())) + brushOutlineHeight); - osg::Vec3d upVerticalLinePoint1( - point.x() - radius, - point.y() - radius + step, - getIntersectionHeight(osg::Vec3d( - point.x() - radius, - point.y() - radius + step, - point.z())) + brushOutlineHeight); - osg::Vec3d upVerticalLinePoint2( - point.x() - radius, - point.y() - radius + step2, - getIntersectionHeight(osg::Vec3d( - point.x() - radius, - point.y() - radius + step2, - point.z())) + brushOutlineHeight); - osg::Vec3d downHorizontalLinePoint1( - point.x() + radius - step, - point.y() + radius, - getIntersectionHeight(osg::Vec3d( - point.x() + radius - step, - point.y() + radius, - point.z())) + brushOutlineHeight); - osg::Vec3d downHorizontalLinePoint2( - point.x() + radius - step2, - point.y() + radius, - getIntersectionHeight(osg::Vec3d( - point.x() + radius - step2, - point.y() + radius, - point.z())) + brushOutlineHeight); - osg::Vec3d downVerticalLinePoint1( - point.x() + radius, - point.y() + radius - step, - getIntersectionHeight(osg::Vec3d( - point.x() + radius, - point.y() + radius - step, - point.z())) + brushOutlineHeight); - osg::Vec3d downVerticalLinePoint2( - point.x() + radius, - point.y() + radius - step2, - getIntersectionHeight(osg::Vec3d( - point.x() + radius, - point.y() + radius - step2, - point.z())) + brushOutlineHeight); + osg::Vec3d upHorizontalLinePoint1(point.x() - radius + step, point.y() - radius, + getIntersectionHeight(osg::Vec3d(point.x() - radius + step, point.y() - radius, point.z())) + + brushOutlineHeight); + osg::Vec3d upHorizontalLinePoint2(point.x() - radius + step2, point.y() - radius, + getIntersectionHeight(osg::Vec3d(point.x() - radius + step2, point.y() - radius, point.z())) + + brushOutlineHeight); + osg::Vec3d upVerticalLinePoint1(point.x() - radius, point.y() - radius + step, + getIntersectionHeight(osg::Vec3d(point.x() - radius, point.y() - radius + step, point.z())) + + brushOutlineHeight); + osg::Vec3d upVerticalLinePoint2(point.x() - radius, point.y() - radius + step2, + getIntersectionHeight(osg::Vec3d(point.x() - radius, point.y() - radius + step2, point.z())) + + brushOutlineHeight); + osg::Vec3d downHorizontalLinePoint1(point.x() + radius - step, point.y() + radius, + getIntersectionHeight(osg::Vec3d(point.x() + radius - step, point.y() + radius, point.z())) + + brushOutlineHeight); + osg::Vec3d downHorizontalLinePoint2(point.x() + radius - step2, point.y() + radius, + getIntersectionHeight(osg::Vec3d(point.x() + radius - step2, point.y() + radius, point.z())) + + brushOutlineHeight); + osg::Vec3d downVerticalLinePoint1(point.x() + radius, point.y() + radius - step, + getIntersectionHeight(osg::Vec3d(point.x() + radius, point.y() + radius - step, point.z())) + + brushOutlineHeight); + osg::Vec3d downVerticalLinePoint2(point.x() + radius, point.y() + radius - step2, + getIntersectionHeight(osg::Vec3d(point.x() + radius, point.y() + radius - step2, point.z())) + + brushOutlineHeight); vertices->push_back(upHorizontalLinePoint1); colors->push_back(lineColor); vertices->push_back(upHorizontalLinePoint2); @@ -212,33 +185,28 @@ void CSVRender::BrushDraw::buildSquareGeometry(const float& radius, const osg::V void CSVRender::BrushDraw::buildCircleGeometry(const float& radius, const osg::Vec3d& point) { - osg::ref_ptr geom (new osg::Geometry()); - osg::ref_ptr vertices (new osg::Vec3Array()); - osg::ref_ptr colors (new osg::Vec4Array()); - const int amountOfPoints = (osg::PI * 2.0f) * radius / 20; - const float step ((osg::PI * 2.0f) / static_cast(amountOfPoints)); - const float brushOutlineHeight (1.0f); + osg::ref_ptr geom(new osg::Geometry()); + osg::ref_ptr vertices(new osg::Vec3Array()); + osg::ref_ptr colors(new osg::Vec4Array()); + + const int amountOfPoints = 128; + const float step((osg::PI * 2.0f) / static_cast(amountOfPoints)); + const float brushOutlineHeight(1.0f); osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); for (int i = 0; i < amountOfPoints + 2; i++) { - float angle (i * step); - vertices->push_back(osg::Vec3d( - point.x() + radius * cosf(angle), - point.y() + radius * sinf(angle), - getIntersectionHeight(osg::Vec3d( - point.x() + radius * cosf(angle), - point.y() + radius * sinf(angle), - point.z()) ) + brushOutlineHeight)); + float angle(i * step); + vertices->push_back(osg::Vec3d(point.x() + radius * cosf(angle), point.y() + radius * sinf(angle), + getIntersectionHeight( + osg::Vec3d(point.x() + radius * cosf(angle), point.y() + radius * sinf(angle), point.z())) + + brushOutlineHeight)); colors->push_back(lineColor); angle = static_cast(i + 1) * step; - vertices->push_back(osg::Vec3d( - point.x() + radius * cosf(angle), - point.y() + radius * sinf(angle), - getIntersectionHeight(osg::Vec3d( - point.x() + radius * cosf(angle), - point.y() + radius * sinf(angle), - point.z()) ) + brushOutlineHeight)); + vertices->push_back(osg::Vec3d(point.x() + radius * cosf(angle), point.y() + radius * sinf(angle), + getIntersectionHeight( + osg::Vec3d(point.x() + radius * cosf(angle), point.y() + radius * sinf(angle), point.z())) + + brushOutlineHeight)); colors->push_back(lineColor); } @@ -262,36 +230,32 @@ void CSVRender::BrushDraw::update(osg::Vec3d point, int brushSize, CSVWidget::Br if (mTextureMode) { std::pair snapToGridXY = CSMWorld::CellCoordinates::toTextureCoords(point); - float offsetToMiddle = mLandSizeFactor * 0.5f; + float offsetToMiddle = mLandSizeFactor * 0.5f; snapToGridPoint = osg::Vec3d( CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(snapToGridXY.first) + offsetToMiddle, - CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(snapToGridXY.second) + offsetToMiddle, - point.z()); + CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(snapToGridXY.second) + offsetToMiddle, point.z()); } else { std::pair snapToGridXY = CSMWorld::CellCoordinates::toVertexCoords(point); - snapToGridPoint = osg::Vec3d( - CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(snapToGridXY.first), - CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(snapToGridXY.second), - point.z()); + snapToGridPoint = osg::Vec3d(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(snapToGridXY.first), + CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(snapToGridXY.second), point.z()); } - switch (toolShape) { - case (CSVWidget::BrushShape_Point) : + case (CSVWidget::BrushShape_Point): buildPointGeometry(snapToGridPoint); break; - case (CSVWidget::BrushShape_Square) : + case (CSVWidget::BrushShape_Square): buildSquareGeometry(radius, snapToGridPoint); break; - case (CSVWidget::BrushShape_Circle) : + case (CSVWidget::BrushShape_Circle): buildCircleGeometry(radius, snapToGridPoint); break; - case (CSVWidget::BrushShape_Custom) : + case (CSVWidget::BrushShape_Custom): buildSquareGeometry(1, snapToGridPoint); - //buildCustomGeometry + // buildCustomGeometry break; } diff --git a/apps/opencs/view/render/brushdraw.hpp b/apps/opencs/view/render/brushdraw.hpp index 0551631cd91..27a276c7cc6 100644 --- a/apps/opencs/view/render/brushdraw.hpp +++ b/apps/opencs/view/render/brushdraw.hpp @@ -1,35 +1,40 @@ #ifndef CSV_RENDER_BRUSHDRAW_H #define CSV_RENDER_BRUSHDRAW_H -#include -#include +#include +#include -#include #include "../widget/brushshapes.hpp" +namespace osg +{ + class Geometry; + class Group; +} + namespace CSVRender { class BrushDraw { - public: - BrushDraw(osg::ref_ptr parentNode, bool textureMode = false); - ~BrushDraw(); - - void update(osg::Vec3d point, int brushSize, CSVWidget::BrushShape toolShape); - void hide(); - - private: - void buildPointGeometry(const osg::Vec3d& point); - void buildSquareGeometry(const float& radius, const osg::Vec3d& point); - void buildCircleGeometry(const float& radius, const osg::Vec3d& point); - void buildCustomGeometry(const float& radius, const osg::Vec3d& point); - float getIntersectionHeight (const osg::Vec3d& point); - - osg::ref_ptr mParentNode; - osg::ref_ptr mBrushDrawNode; - osg::ref_ptr mGeometry; - bool mTextureMode; - float mLandSizeFactor; + public: + BrushDraw(osg::ref_ptr parentNode, bool textureMode = false); + ~BrushDraw(); + + void update(osg::Vec3d point, int brushSize, CSVWidget::BrushShape toolShape); + void hide(); + + private: + void buildPointGeometry(const osg::Vec3d& point); + void buildSquareGeometry(const float& radius, const osg::Vec3d& point); + void buildCircleGeometry(const float& radius, const osg::Vec3d& point); + void buildCustomGeometry(const float& radius, const osg::Vec3d& point); + float getIntersectionHeight(const osg::Vec3d& point); + + osg::ref_ptr mParentNode; + osg::ref_ptr mBrushDrawNode; + osg::ref_ptr mGeometry; + bool mTextureMode; + float mLandSizeFactor; }; } diff --git a/apps/opencs/view/render/cameracontroller.cpp b/apps/opencs/view/render/cameracontroller.cpp index f21224d73de..10033cb51fb 100644 --- a/apps/opencs/view/render/cameracontroller.cpp +++ b/apps/opencs/view/render/cameracontroller.cpp @@ -1,23 +1,26 @@ #include "cameracontroller.hpp" +#include #include +#include #include #include #include #include -#include #include +#include #include #include +#include +#include +#include #include #include "../../model/prefs/shortcut.hpp" -#include "scenewidget.hpp" - namespace CSVRender { @@ -35,17 +38,13 @@ namespace CSVRender : QObject(parent) , mActive(false) , mInverted(false) - , mCameraSensitivity(1/650.f) + , mCameraSensitivity(1 / 650.f) , mSecondaryMoveMult(50) , mWheelMoveMult(8) , mCamera(nullptr) { } - CameraController::~CameraController() - { - } - bool CameraController::isActive() const { return mActive; @@ -179,57 +178,63 @@ namespace CSVRender { CSMPrefs::Shortcut* naviPrimaryShortcut = new CSMPrefs::Shortcut("scene-navi-primary", widget); naviPrimaryShortcut->enable(false); - connect(naviPrimaryShortcut, SIGNAL(activated(bool)), this, SLOT(naviPrimary(bool))); + connect(naviPrimaryShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, + &FreeCameraController::naviPrimary); addShortcut(naviPrimaryShortcut); CSMPrefs::Shortcut* naviSecondaryShortcut = new CSMPrefs::Shortcut("scene-navi-secondary", widget); naviSecondaryShortcut->enable(false); - connect(naviSecondaryShortcut, SIGNAL(activated(bool)), this, SLOT(naviSecondary(bool))); + connect(naviSecondaryShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, + &FreeCameraController::naviSecondary); addShortcut(naviSecondaryShortcut); - CSMPrefs::Shortcut* forwardShortcut = new CSMPrefs::Shortcut("free-forward", "scene-speed-modifier", - CSMPrefs::Shortcut::SM_Detach, widget); + CSMPrefs::Shortcut* forwardShortcut + = new CSMPrefs::Shortcut("free-forward", "scene-speed-modifier", CSMPrefs::Shortcut::SM_Detach, widget); forwardShortcut->enable(false); - connect(forwardShortcut, SIGNAL(activated(bool)), this, SLOT(forward(bool))); - connect(forwardShortcut, SIGNAL(secondary(bool)), this, SLOT(alternateFast(bool))); + connect(forwardShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &FreeCameraController::forward); + connect(forwardShortcut, qOverload(&CSMPrefs::Shortcut::secondary), this, + &FreeCameraController::alternateFast); addShortcut(forwardShortcut); CSMPrefs::Shortcut* leftShortcut = new CSMPrefs::Shortcut("free-left", widget); leftShortcut->enable(false); - connect(leftShortcut, SIGNAL(activated(bool)), this, SLOT(left(bool))); + connect(leftShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &FreeCameraController::left); addShortcut(leftShortcut); CSMPrefs::Shortcut* backShortcut = new CSMPrefs::Shortcut("free-backward", widget); backShortcut->enable(false); - connect(backShortcut, SIGNAL(activated(bool)), this, SLOT(backward(bool))); + connect(backShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &FreeCameraController::backward); addShortcut(backShortcut); CSMPrefs::Shortcut* rightShortcut = new CSMPrefs::Shortcut("free-right", widget); rightShortcut->enable(false); - connect(rightShortcut, SIGNAL(activated(bool)), this, SLOT(right(bool))); + connect(rightShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &FreeCameraController::right); addShortcut(rightShortcut); CSMPrefs::Shortcut* rollLeftShortcut = new CSMPrefs::Shortcut("free-roll-left", widget); rollLeftShortcut->enable(false); - connect(rollLeftShortcut, SIGNAL(activated(bool)), this, SLOT(rollLeft(bool))); + connect( + rollLeftShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &FreeCameraController::rollLeft); addShortcut(rollLeftShortcut); CSMPrefs::Shortcut* rollRightShortcut = new CSMPrefs::Shortcut("free-roll-right", widget); rollRightShortcut->enable(false); - connect(rollRightShortcut, SIGNAL(activated(bool)), this, SLOT(rollRight(bool))); + connect( + rollRightShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &FreeCameraController::rollRight); addShortcut(rollRightShortcut); CSMPrefs::Shortcut* speedModeShortcut = new CSMPrefs::Shortcut("free-speed-mode", widget); speedModeShortcut->enable(false); - connect(speedModeShortcut, SIGNAL(activated()), this, SLOT(swapSpeedMode())); + connect( + speedModeShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &FreeCameraController::swapSpeedMode); addShortcut(speedModeShortcut); } @@ -332,7 +337,7 @@ namespace CSVRender if (mRollRight) roll(rotDist); } - else if(mModified) + else if (mModified) { stabilize(); mModified = false; @@ -459,7 +464,7 @@ namespace CSVRender , mRollLeft(false) , mRollRight(false) , mPickingMask(~0u) - , mCenter(0,0,0) + , mCenter(0, 0, 0) , mDistance(0) , mOrbitSpeed(osg::PI / 4) , mOrbitSpeedMult(4) @@ -467,57 +472,63 @@ namespace CSVRender { CSMPrefs::Shortcut* naviPrimaryShortcut = new CSMPrefs::Shortcut("scene-navi-primary", widget); naviPrimaryShortcut->enable(false); - connect(naviPrimaryShortcut, SIGNAL(activated(bool)), this, SLOT(naviPrimary(bool))); + connect(naviPrimaryShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, + &OrbitCameraController::naviPrimary); addShortcut(naviPrimaryShortcut); CSMPrefs::Shortcut* naviSecondaryShortcut = new CSMPrefs::Shortcut("scene-navi-secondary", widget); naviSecondaryShortcut->enable(false); - connect(naviSecondaryShortcut, SIGNAL(activated(bool)), this, SLOT(naviSecondary(bool))); + connect(naviSecondaryShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, + &OrbitCameraController::naviSecondary); addShortcut(naviSecondaryShortcut); - CSMPrefs::Shortcut* upShortcut = new CSMPrefs::Shortcut("orbit-up", "scene-speed-modifier", - CSMPrefs::Shortcut::SM_Detach, widget); + CSMPrefs::Shortcut* upShortcut + = new CSMPrefs::Shortcut("orbit-up", "scene-speed-modifier", CSMPrefs::Shortcut::SM_Detach, widget); upShortcut->enable(false); - connect(upShortcut, SIGNAL(activated(bool)), this, SLOT(up(bool))); - connect(upShortcut, SIGNAL(secondary(bool)), this, SLOT(alternateFast(bool))); + connect(upShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &OrbitCameraController::up); + connect( + upShortcut, qOverload(&CSMPrefs::Shortcut::secondary), this, &OrbitCameraController::alternateFast); addShortcut(upShortcut); CSMPrefs::Shortcut* leftShortcut = new CSMPrefs::Shortcut("orbit-left", widget); leftShortcut->enable(false); - connect(leftShortcut, SIGNAL(activated(bool)), this, SLOT(left(bool))); + connect(leftShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &OrbitCameraController::left); addShortcut(leftShortcut); CSMPrefs::Shortcut* downShortcut = new CSMPrefs::Shortcut("orbit-down", widget); downShortcut->enable(false); - connect(downShortcut, SIGNAL(activated(bool)), this, SLOT(down(bool))); + connect(downShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &OrbitCameraController::down); addShortcut(downShortcut); CSMPrefs::Shortcut* rightShortcut = new CSMPrefs::Shortcut("orbit-right", widget); rightShortcut->enable(false); - connect(rightShortcut, SIGNAL(activated(bool)), this, SLOT(right(bool))); + connect(rightShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &OrbitCameraController::right); addShortcut(rightShortcut); CSMPrefs::Shortcut* rollLeftShortcut = new CSMPrefs::Shortcut("orbit-roll-left", widget); rollLeftShortcut->enable(false); - connect(rollLeftShortcut, SIGNAL(activated(bool)), this, SLOT(rollLeft(bool))); + connect( + rollLeftShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &OrbitCameraController::rollLeft); addShortcut(rollLeftShortcut); CSMPrefs::Shortcut* rollRightShortcut = new CSMPrefs::Shortcut("orbit-roll-right", widget); rollRightShortcut->enable(false); - connect(rollRightShortcut, SIGNAL(activated(bool)), this, SLOT(rollRight(bool))); + connect(rollRightShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, + &OrbitCameraController::rollRight); addShortcut(rollRightShortcut); CSMPrefs::Shortcut* speedModeShortcut = new CSMPrefs::Shortcut("orbit-speed-mode", widget); speedModeShortcut->enable(false); - connect(speedModeShortcut, SIGNAL(activated()), this, SLOT(swapSpeedMode())); + connect(speedModeShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, + &OrbitCameraController::swapSpeedMode); addShortcut(speedModeShortcut); } @@ -643,8 +654,8 @@ namespace CSVRender static const int DefaultStartDistance = 10000.f; // Try to intelligently pick focus object - osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector( - osgUtil::Intersector::PROJECTION, osg::Vec3d(0, 0, 0), LocalForward)); + osg::ref_ptr intersector( + new osgUtil::LineSegmentIntersector(osgUtil::Intersector::PROJECTION, osg::Vec3d(0, 0, 0), LocalForward)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST); osgUtil::IntersectionVisitor visitor(intersector); @@ -669,7 +680,7 @@ namespace CSVRender mInitialized = true; } - + void OrbitCameraController::setConstRoll(bool enabled) { mConstRoll = enabled; @@ -679,7 +690,7 @@ namespace CSVRender { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); - osg::Vec3d absoluteUp = osg::Vec3(0,0,1); + osg::Vec3d absoluteUp = osg::Vec3(0, 0, 1); osg::Quat rotation = osg::Quat(value, mConstRoll ? absoluteUp : up); osg::Vec3d oldOffset = eye - mCenter; @@ -699,10 +710,10 @@ namespace CSVRender osg::Vec3d forward = center - eye; osg::Vec3d axis = up ^ forward; - osg::Quat rotation = osg::Quat(value,axis); + osg::Quat rotation = osg::Quat(value, axis); osg::Vec3d oldOffset = eye - mCenter; osg::Vec3d newOffset = rotation * oldOffset; - + if (mConstRoll) up = rotation * up; diff --git a/apps/opencs/view/render/cameracontroller.hpp b/apps/opencs/view/render/cameracontroller.hpp index dff0f212e99..a026087f616 100644 --- a/apps/opencs/view/render/cameracontroller.hpp +++ b/apps/opencs/view/render/cameracontroller.hpp @@ -1,12 +1,10 @@ #ifndef OPENCS_VIEW_CAMERACONTROLLER_H #define OPENCS_VIEW_CAMERACONTROLLER_H -#include #include #include -#include #include namespace osg @@ -22,181 +20,172 @@ namespace CSMPrefs namespace CSVRender { - class SceneWidget; - class CameraController : public QObject { - Q_OBJECT - - public: - - static const osg::Vec3d WorldUp; - - static const osg::Vec3d LocalUp; - static const osg::Vec3d LocalLeft; - static const osg::Vec3d LocalForward; + Q_OBJECT - CameraController(QObject* parent); - virtual ~CameraController(); + public: + static const osg::Vec3d WorldUp; - bool isActive() const; + static const osg::Vec3d LocalUp; + static const osg::Vec3d LocalLeft; + static const osg::Vec3d LocalForward; - osg::Camera* getCamera() const; - double getCameraSensitivity() const; - bool getInverted() const; - double getSecondaryMovementMultiplier() const; - double getWheelMovementMultiplier() const; + CameraController(QObject* parent); + ~CameraController() override = default; - void setCamera(osg::Camera*); - void setCameraSensitivity(double value); - void setInverted(bool value); - void setSecondaryMovementMultiplier(double value); - void setWheelMovementMultiplier(double value); + bool isActive() const; - // moves the camera to an intelligent position - void setup(osg::Group* root, unsigned int mask, const osg::Vec3d& up); + osg::Camera* getCamera() const; + double getCameraSensitivity() const; + bool getInverted() const; + double getSecondaryMovementMultiplier() const; + double getWheelMovementMultiplier() const; - virtual void handleMouseMoveEvent(int x, int y) = 0; - virtual void handleMouseScrollEvent(int x) = 0; + void setCamera(osg::Camera*); + void setCameraSensitivity(double value); + void setInverted(bool value); + void setSecondaryMovementMultiplier(double value); + void setWheelMovementMultiplier(double value); - virtual void update(double dt) = 0; + // moves the camera to an intelligent position + void setup(osg::Group* root, unsigned int mask, const osg::Vec3d& up); - protected: + virtual void handleMouseMoveEvent(int x, int y) = 0; + virtual void handleMouseScrollEvent(int x) = 0; - void addShortcut(CSMPrefs::Shortcut* shortcut); + virtual void update(double dt) = 0; - private: + protected: + void addShortcut(CSMPrefs::Shortcut* shortcut); - bool mActive, mInverted; - double mCameraSensitivity; - double mSecondaryMoveMult; - double mWheelMoveMult; + private: + bool mActive, mInverted; + double mCameraSensitivity; + double mSecondaryMoveMult; + double mWheelMoveMult; - osg::Camera* mCamera; + osg::Camera* mCamera; - std::vector mShortcuts; + std::vector mShortcuts; }; class FreeCameraController : public CameraController { - Q_OBJECT + Q_OBJECT - public: + public: + FreeCameraController(QWidget* parent); - FreeCameraController(QWidget* parent); + double getLinearSpeed() const; + double getRotationalSpeed() const; + double getSpeedMultiplier() const; - double getLinearSpeed() const; - double getRotationalSpeed() const; - double getSpeedMultiplier() const; + void setLinearSpeed(double value); + void setRotationalSpeed(double value); + void setSpeedMultiplier(double value); - void setLinearSpeed(double value); - void setRotationalSpeed(double value); - void setSpeedMultiplier(double value); + void fixUpAxis(const osg::Vec3d& up); + void unfixUpAxis(); - void fixUpAxis(const osg::Vec3d& up); - void unfixUpAxis(); + void handleMouseMoveEvent(int x, int y) override; + void handleMouseScrollEvent(int x) override; - void handleMouseMoveEvent(int x, int y) override; - void handleMouseScrollEvent(int x) override; + void update(double dt) override; - void update(double dt) override; + private: + void yaw(double value); + void pitch(double value); + void roll(double value); + void translate(const osg::Vec3d& offset); - private: + void stabilize(); - void yaw(double value); - void pitch(double value); - void roll(double value); - void translate(const osg::Vec3d& offset); + bool mLockUpright, mModified; + bool mNaviPrimary, mNaviSecondary; + bool mFast, mFastAlternate; + bool mLeft, mRight, mForward, mBackward, mRollLeft, mRollRight; + osg::Vec3d mUp; - void stabilize(); + double mLinSpeed; + double mRotSpeed; + double mSpeedMult; - bool mLockUpright, mModified; - bool mNaviPrimary, mNaviSecondary; - bool mFast, mFastAlternate; - bool mLeft, mRight, mForward, mBackward, mRollLeft, mRollRight; - osg::Vec3d mUp; + private slots: - double mLinSpeed; - double mRotSpeed; - double mSpeedMult; - - private slots: - - void naviPrimary(bool active); - void naviSecondary(bool active); - void forward(bool active); - void left(bool active); - void backward(bool active); - void right(bool active); - void rollLeft(bool active); - void rollRight(bool active); - void alternateFast(bool active); - void swapSpeedMode(); + void naviPrimary(bool active); + void naviSecondary(bool active); + void forward(bool active); + void left(bool active); + void backward(bool active); + void right(bool active); + void rollLeft(bool active); + void rollRight(bool active); + void alternateFast(bool active); + void swapSpeedMode(); }; class OrbitCameraController : public CameraController { - Q_OBJECT - - public: - - OrbitCameraController(QWidget* parent); + Q_OBJECT - osg::Vec3d getCenter() const; - double getOrbitSpeed() const; - double getOrbitSpeedMultiplier() const; - unsigned int getPickingMask() const; + public: + OrbitCameraController(QWidget* parent); - void setCenter(const osg::Vec3d& center); - void setOrbitSpeed(double value); - void setOrbitSpeedMultiplier(double value); - void setPickingMask(unsigned int value); + osg::Vec3d getCenter() const; + double getOrbitSpeed() const; + double getOrbitSpeedMultiplier() const; + unsigned int getPickingMask() const; - void handleMouseMoveEvent(int x, int y) override; - void handleMouseScrollEvent(int x) override; + void setCenter(const osg::Vec3d& center); + void setOrbitSpeed(double value); + void setOrbitSpeedMultiplier(double value); + void setPickingMask(unsigned int value); - void update(double dt) override; + void handleMouseMoveEvent(int x, int y) override; + void handleMouseScrollEvent(int x) override; - /// \brief Flag controller to be re-initialized. - void reset(); + void update(double dt) override; - void setConstRoll(bool enable); + /// \brief Flag controller to be re-initialized. + void reset(); - private: + void setConstRoll(bool enable); - void initialize(); + private: + void initialize(); - void rotateHorizontal(double value); - void rotateVertical(double value); - void roll(double value); - void translate(const osg::Vec3d& offset); - void zoom(double value); + void rotateHorizontal(double value); + void rotateVertical(double value); + void roll(double value); + void translate(const osg::Vec3d& offset); + void zoom(double value); - bool mInitialized; - bool mNaviPrimary, mNaviSecondary; - bool mFast, mFastAlternate; - bool mLeft, mRight, mUp, mDown, mRollLeft, mRollRight; - unsigned int mPickingMask; - osg::Vec3d mCenter; - double mDistance; + bool mInitialized; + bool mNaviPrimary, mNaviSecondary; + bool mFast, mFastAlternate; + bool mLeft, mRight, mUp, mDown, mRollLeft, mRollRight; + unsigned int mPickingMask; + osg::Vec3d mCenter; + double mDistance; - double mOrbitSpeed; - double mOrbitSpeedMult; + double mOrbitSpeed; + double mOrbitSpeedMult; - bool mConstRoll; + bool mConstRoll; - private slots: + private slots: - void naviPrimary(bool active); - void naviSecondary(bool active); - void up(bool active); - void left(bool active); - void down(bool active); - void right(bool active); - void rollLeft(bool active); - void rollRight(bool active); - void alternateFast(bool active); - void swapSpeedMode(); + void naviPrimary(bool active); + void naviSecondary(bool active); + void up(bool active); + void left(bool active); + void down(bool active); + void right(bool active); + void rollLeft(bool active); + void rollRight(bool active); + void alternateFast(bool active); + void swapSpeedMode(); }; } diff --git a/apps/opencs/view/render/cell.cpp b/apps/opencs/view/render/cell.cpp index 2502dc1fd06..3d3b82acf8c 100644 --- a/apps/opencs/view/render/cell.cpp +++ b/apps/opencs/view/render/cell.cpp @@ -1,104 +1,116 @@ #include "cell.hpp" -#include +#include +#include +#include -#include -#include -#include +#include #include - -#include -#include -#include -#include +#include +#include +#include + +#include +#include +#include +#include #include +#include "../../model/doc/document.hpp" #include "../../model/world/idtable.hpp" -#include "../../model/world/columns.hpp" -#include "../../model/world/data.hpp" -#include "../../model/world/refcollection.hpp" -#include "../../model/world/cellcoordinates.hpp" -#include "cellwater.hpp" -#include "cellborder.hpp" #include "cellarrow.hpp" +#include "cellborder.hpp" #include "cellmarker.hpp" +#include "cellwater.hpp" +#include "instancedragmodes.hpp" #include "mask.hpp" +#include "object.hpp" #include "pathgrid.hpp" #include "terrainstorage.hpp" -#include "object.hpp" -#include "instancedragmodes.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace CSVRender { class CellNodeContainer : public osg::Referenced { - public: - - CellNodeContainer(Cell* cell) : mCell(cell) {} - - Cell* getCell(){ return mCell; } + public: + CellNodeContainer(Cell* cell) + : mCell(cell) + { + } - private: + Cell* getCell() { return mCell; } - Cell* mCell; + private: + Cell* mCell; }; class CellNodeCallback : public osg::NodeCallback { - public: - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override - { - traverse(node, nv); - CellNodeContainer* container = static_cast(node->getUserData()); - container->getCell()->updateLand(); - } + public: + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + traverse(node, nv); + CellNodeContainer* container = static_cast(node->getUserData()); + container->getCell()->updateLand(); + } }; } -bool CSVRender::Cell::removeObject (const std::string& id) +bool CSVRender::Cell::removeObject(const std::string& id) { - std::map::iterator iter = - mObjects.find (Misc::StringUtils::lowerCase (id)); + std::map::iterator iter = mObjects.find(Misc::StringUtils::lowerCase(id)); - if (iter==mObjects.end()) + if (iter == mObjects.end()) return false; - removeObject (iter); + removeObject(iter); return true; } -std::map::iterator CSVRender::Cell::removeObject ( - std::map::iterator iter) +std::map::iterator CSVRender::Cell::removeObject( + std::map::iterator iter) { delete iter->second; - mObjects.erase (iter++); + mObjects.erase(iter++); return iter; } -bool CSVRender::Cell::addObjects (int start, int end) +bool CSVRender::Cell::addObjects(int start, int end) { bool modified = false; const CSMWorld::RefCollection& collection = mData.getReferences(); - for (int i=start; i<=end; ++i) + for (int i = start; i <= end; ++i) { - std::string cell = Misc::StringUtils::lowerCase (collection.getRecord (i).get().mCell); + const auto& cellId = ESM::RefId::stringRefId(collection.getRecord(i).get().mCell.toString()); - CSMWorld::RecordBase::State state = collection.getRecord (i).mState; + CSMWorld::RecordBase::State state = collection.getRecord(i).mState; - if (cell==mId && state!=CSMWorld::RecordBase::State_Deleted) + if (cellId == mId && state != CSMWorld::RecordBase::State_Deleted) { - std::string id = Misc::StringUtils::lowerCase (collection.getRecord (i).get().mId); + const std::string& id = collection.getRecord(i).get().mId.getRefIdString(); - std::unique_ptr object (new Object (mData, mCellNode, id, false)); + auto object = std::make_unique(mData, mCellNode, id, false); if (mSubModeElementMask & Mask_Reference) - object->setSubMode (mSubMode); + object->setSubMode(mSubMode); - mObjects.insert (std::make_pair (id, object.release())); + mObjects.insert(std::make_pair(id, object.release())); modified = true; } } @@ -120,42 +132,34 @@ void CSVRender::Cell::updateLand() return; } - // Setup land if available const CSMWorld::IdCollection& land = mData.getLand(); - int landIndex = land.searchId(mId); - if (landIndex != -1 && !land.getRecord(mId).isDeleted()) - { - const ESM::Land& esmLand = land.getRecord(mId).get(); - if (esmLand.getLandData (ESM::Land::DATA_VHGT)) - { - if (mTerrain) - { - mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); - mTerrain->clearAssociatedCaches(); - } - else - { - mTerrain.reset(new Terrain::TerrainGrid(mCellNode, mCellNode, - mData.getResourceSystem().get(), mTerrainStorage, Mask_Terrain)); - } + if (land.getRecord(mId).isDeleted()) + return; - mTerrain->loadCell(esmLand.mX, esmLand.mY); + const ESM::Land& esmLand = land.getRecord(mId).get(); - if (!mCellBorder) - mCellBorder.reset(new CellBorder(mCellNode, mCoordinates)); + if (mTerrain) + { + mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); + mTerrain->clearAssociatedCaches(); + } + else + { + constexpr double expiryDelay = 0; + mTerrain = std::make_unique(mCellNode, mCellNode, mData.getResourceSystem().get(), + mTerrainStorage, Mask_Terrain, ESM::Cell::sDefaultWorldspaceId, expiryDelay); + } - mCellBorder->buildShape(esmLand); + mTerrain->loadCell(esmLand.mX, esmLand.mY); - return; - } - } + if (!mCellBorder) + mCellBorder = std::make_unique(mCellNode, mCoordinates); - // No land data - unloadLand(); + mCellBorder->buildShape(esmLand); } -void CSVRender::Cell::unloadLand() +void CSVRender::Cell::unloadLand() { if (mTerrain) mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); @@ -164,12 +168,17 @@ void CSVRender::Cell::unloadLand() mCellBorder.reset(); } -CSVRender::Cell::Cell (CSMWorld::Data& data, osg::Group* rootNode, const std::string& id, - bool deleted) -: mData (data), mId (Misc::StringUtils::lowerCase (id)), mDeleted (deleted), mSubMode (0), - mSubModeElementMask (0), mUpdateLand(true), mLandDeleted(false) +CSVRender::Cell::Cell( + CSMDoc::Document& document, osg::Group* rootNode, const std::string& id, bool deleted, bool isExterior) + : mData(document.getData()) + , mId(ESM::RefId::stringRefId(id)) + , mDeleted(deleted) + , mSubMode(0) + , mSubModeElementMask(0) + , mUpdateLand(isExterior) + , mLandDeleted(false) { - std::pair result = CSMWorld::CellCoordinates::fromId (id); + std::pair result = CSMWorld::CellCoordinates::fromId(id); mTerrainStorage = new TerrainStorage(mData); @@ -185,24 +194,33 @@ CSVRender::Cell::Cell (CSMWorld::Data& data, osg::Group* rootNode, const std::st if (!mDeleted) { - CSMWorld::IdTable& references = dynamic_cast ( - *mData.getTableModel (CSMWorld::UniversalId::Type_References)); + CSMWorld::IdTable& references + = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_References)); int rows = references.rowCount(); - addObjects (0, rows-1); + addObjects(0, rows - 1); - updateLand(); + if (mUpdateLand) + { + int landIndex = document.getData().getLand().searchId(mId); + if (landIndex == -1) + { + CSMWorld::IdTable& landTable + = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Land)); + document.getUndoStack().push(new CSMWorld::CreateCommand(landTable, mId.getRefIdString())); + } + updateLand(); + } - mPathgrid.reset(new Pathgrid(mData, mCellNode, mId, mCoordinates)); - mCellWater.reset(new CellWater(mData, mCellNode, mId, mCoordinates)); + mPathgrid = std::make_unique(mData, mCellNode, mId.getRefIdString(), mCoordinates); + mCellWater = std::make_unique(mData, mCellNode, mId.getRefIdString(), mCoordinates); } } CSVRender::Cell::~Cell() { - for (std::map::iterator iter (mObjects.begin()); - iter!=mObjects.end(); ++iter) + for (std::map::iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) delete iter->second; mCellNode->getParent(0)->removeChild(mCellNode); @@ -213,86 +231,81 @@ CSVRender::Pathgrid* CSVRender::Cell::getPathgrid() const return mPathgrid.get(); } -bool CSVRender::Cell::referenceableDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +bool CSVRender::Cell::referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { bool modified = false; - for (std::map::iterator iter (mObjects.begin()); - iter!=mObjects.end(); ++iter) - if (iter->second->referenceableDataChanged (topLeft, bottomRight)) + for (std::map::iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) + if (iter->second->referenceableDataChanged(topLeft, bottomRight)) modified = true; return modified; } -bool CSVRender::Cell::referenceableAboutToBeRemoved (const QModelIndex& parent, int start, - int end) +bool CSVRender::Cell::referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end) { if (parent.isValid()) return false; bool modified = false; - for (std::map::iterator iter (mObjects.begin()); - iter!=mObjects.end(); ++iter) - if (iter->second->referenceableAboutToBeRemoved (parent, start, end)) + for (std::map::iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) + if (iter->second->referenceableAboutToBeRemoved(parent, start, end)) modified = true; return modified; } -bool CSVRender::Cell::referenceDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +bool CSVRender::Cell::referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mDeleted) return false; - CSMWorld::IdTable& references = dynamic_cast ( - *mData.getTableModel (CSMWorld::UniversalId::Type_References)); + CSMWorld::IdTable& references + = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_References)); - int idColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Id); - int cellColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); - int stateColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); + int idColumn = references.findColumnIndex(CSMWorld::Columns::ColumnId_Id); + int cellColumn = references.findColumnIndex(CSMWorld::Columns::ColumnId_Cell); + int stateColumn = references.findColumnIndex(CSMWorld::Columns::ColumnId_Modification); // list IDs in cell std::map ids; // id, deleted state - for (int i=topLeft.row(); i<=bottomRight.row(); ++i) + for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { - std::string cell = Misc::StringUtils::lowerCase (references.data ( - references.index (i, cellColumn)).toString().toUtf8().constData()); + auto cell + = ESM::RefId::stringRefId(references.data(references.index(i, cellColumn)).toString().toUtf8().constData()); - if (cell==mId) + if (cell == mId) { - std::string id = Misc::StringUtils::lowerCase (references.data ( - references.index (i, idColumn)).toString().toUtf8().constData()); + std::string id = Misc::StringUtils::lowerCase( + references.data(references.index(i, idColumn)).toString().toUtf8().constData()); - int state = references.data (references.index (i, stateColumn)).toInt(); + int state = references.data(references.index(i, stateColumn)).toInt(); - ids.insert (std::make_pair (id, state==CSMWorld::RecordBase::State_Deleted)); + ids.insert(std::make_pair(id, state == CSMWorld::RecordBase::State_Deleted)); } } // perform update and remove where needed bool modified = false; - std::map::iterator iter = mObjects.begin(); - while (iter!=mObjects.end()) + std::map::iterator iter = mObjects.begin(); + while (iter != mObjects.end()) { - if (iter->second->referenceDataChanged (topLeft, bottomRight)) + if (iter->second->referenceDataChanged(topLeft, bottomRight)) modified = true; - std::map::iterator iter2 = ids.find (iter->first); + std::map::iterator iter2 = ids.find(iter->first); - if (iter2!=ids.end()) + if (iter2 != ids.end()) { bool deleted = iter2->second; - ids.erase (iter2); + ids.erase(iter2); if (deleted) { - iter = removeObject (iter); + iter = removeObject(iter); modified = true; continue; } @@ -302,12 +315,11 @@ bool CSVRender::Cell::referenceDataChanged (const QModelIndex& topLeft, } // add new objects - for (std::map::iterator mapIter (ids.begin()); mapIter!=ids.end(); ++mapIter) + for (std::map::iterator mapIter(ids.begin()); mapIter != ids.end(); ++mapIter) { if (!mapIter->second) { - mObjects.insert (std::make_pair ( - mapIter->first, new Object (mData, mCellNode, mapIter->first, false))); + mObjects.insert(std::make_pair(mapIter->first, new Object(mData, mCellNode, mapIter->first, false))); modified = true; } @@ -316,8 +328,7 @@ bool CSVRender::Cell::referenceDataChanged (const QModelIndex& topLeft, return modified; } -bool CSVRender::Cell::referenceAboutToBeRemoved (const QModelIndex& parent, int start, - int end) +bool CSVRender::Cell::referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end) { if (parent.isValid()) return false; @@ -325,22 +336,21 @@ bool CSVRender::Cell::referenceAboutToBeRemoved (const QModelIndex& parent, int if (mDeleted) return false; - CSMWorld::IdTable& references = dynamic_cast ( - *mData.getTableModel (CSMWorld::UniversalId::Type_References)); + CSMWorld::IdTable& references + = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_References)); - int idColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int idColumn = references.findColumnIndex(CSMWorld::Columns::ColumnId_Id); bool modified = false; - for (int row = start; row<=end; ++row) - if (removeObject (references.data ( - references.index (row, idColumn)).toString().toUtf8().constData())) + for (int row = start; row <= end; ++row) + if (removeObject(references.data(references.index(row, idColumn)).toString().toUtf8().constData())) modified = true; return modified; } -bool CSVRender::Cell::referenceAdded (const QModelIndex& parent, int start, int end) +bool CSVRender::Cell::referenceAdded(const QModelIndex& parent, int start, int end) { if (parent.isValid()) return false; @@ -348,7 +358,7 @@ bool CSVRender::Cell::referenceAdded (const QModelIndex& parent, int start, int if (mDeleted) return false; - return addObjects (start, end); + return addObjects(start, end); } void CSVRender::Cell::setAlteredHeight(int inCellX, int inCellY, float height) @@ -385,42 +395,41 @@ void CSVRender::Cell::pathgridRemoved() mPathgrid->removeGeometry(); } -void CSVRender::Cell::landDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +void CSVRender::Cell::landDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { mUpdateLand = true; } -void CSVRender::Cell::landAboutToBeRemoved (const QModelIndex& parent, int start, int end) +void CSVRender::Cell::landAboutToBeRemoved(const QModelIndex& parent, int start, int end) { mLandDeleted = true; unloadLand(); } -void CSVRender::Cell::landAdded (const QModelIndex& parent, int start, int end) +void CSVRender::Cell::landAdded(const QModelIndex& parent, int start, int end) { mUpdateLand = true; mLandDeleted = false; } -void CSVRender::Cell::landTextureChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +void CSVRender::Cell::landTextureChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { mUpdateLand = true; } -void CSVRender::Cell::landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end) +void CSVRender::Cell::landTextureAboutToBeRemoved(const QModelIndex& parent, int start, int end) { mUpdateLand = true; } -void CSVRender::Cell::landTextureAdded (const QModelIndex& parent, int start, int end) +void CSVRender::Cell::landTextureAdded(const QModelIndex& parent, int start, int end) { mUpdateLand = true; } void CSVRender::Cell::reloadAssets() { - for (std::map::const_iterator iter (mObjects.begin()); - iter != mObjects.end(); ++iter) + for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) { iter->second->reloadAssets(); } @@ -435,23 +444,28 @@ void CSVRender::Cell::reloadAssets() mCellWater->reloadAssets(); } -void CSVRender::Cell::setSelection (int elementMask, Selection mode) +void CSVRender::Cell::setSelection(int elementMask, Selection mode) { if (elementMask & Mask_Reference) { - for (std::map::const_iterator iter (mObjects.begin()); - iter!=mObjects.end(); ++iter) + for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) { bool selected = false; switch (mode) { - case Selection_Clear: selected = false; break; - case Selection_All: selected = true; break; - case Selection_Invert: selected = !iter->second->getSelected(); break; + case Selection_Clear: + selected = false; + break; + case Selection_All: + selected = true; + break; + case Selection_Invert: + selected = !iter->second->getSelected(); + break; } - iter->second->setSelected (selected); + iter->second->setSelected(selected); } } if (mPathgrid && elementMask & Mask_Pathgrid) @@ -477,24 +491,21 @@ void CSVRender::Cell::setSelection (int elementMask, Selection mode) } } -void CSVRender::Cell::selectAllWithSameParentId (int elementMask) +void CSVRender::Cell::selectAllWithSameParentId(int elementMask) { std::set ids; - for (std::map::const_iterator iter (mObjects.begin()); - iter!=mObjects.end(); ++iter) + for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) { if (iter->second->getSelected()) - ids.insert (iter->second->getReferenceableId()); + ids.insert(iter->second->getReferenceableId()); } - for (std::map::const_iterator iter (mObjects.begin()); - iter!=mObjects.end(); ++iter) + for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) { - if (!iter->second->getSelected() && - ids.find (iter->second->getReferenceableId())!=ids.end()) + if (!iter->second->getSelected() && ids.find(iter->second->getReferenceableId()) != ids.end()) { - iter->second->setSelected (true); + iter->second->setSelected(true); } } } @@ -508,26 +519,27 @@ void CSVRender::Cell::handleSelectDrag(Object* object, DragMode dragMode) object->setSelected(false); else if (dragMode == DragMode_Select_Invert) - object->setSelected (!object->getSelected()); + object->setSelected(!object->getSelected()); } void CSVRender::Cell::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) { for (auto& object : mObjects) { - if (dragMode == DragMode_Select_Only) object.second->setSelected (false); + if (dragMode == DragMode_Select_Only) + object.second->setSelected(false); - if ( ( object.second->getPosition().pos[0] > pointA[0] && object.second->getPosition().pos[0] < pointB[0] ) || - ( object.second->getPosition().pos[0] > pointB[0] && object.second->getPosition().pos[0] < pointA[0] )) + if ((object.second->getPosition().pos[0] > pointA[0] && object.second->getPosition().pos[0] < pointB[0]) + || (object.second->getPosition().pos[0] > pointB[0] && object.second->getPosition().pos[0] < pointA[0])) { - if ( ( object.second->getPosition().pos[1] > pointA[1] && object.second->getPosition().pos[1] < pointB[1] ) || - ( object.second->getPosition().pos[1] > pointB[1] && object.second->getPosition().pos[1] < pointA[1] )) + if ((object.second->getPosition().pos[1] > pointA[1] && object.second->getPosition().pos[1] < pointB[1]) + || (object.second->getPosition().pos[1] > pointB[1] && object.second->getPosition().pos[1] < pointA[1])) { - if ( ( object.second->getPosition().pos[2] > pointA[2] && object.second->getPosition().pos[2] < pointB[2] ) || - ( object.second->getPosition().pos[2] > pointB[2] && object.second->getPosition().pos[2] < pointA[2] )) + if ((object.second->getPosition().pos[2] > pointA[2] && object.second->getPosition().pos[2] < pointB[2]) + || (object.second->getPosition().pos[2] > pointB[2] + && object.second->getPosition().pos[2] < pointA[2])) handleSelectDrag(object.second, dragMode); } - } } } @@ -536,27 +548,29 @@ void CSVRender::Cell::selectWithinDistance(const osg::Vec3d& point, float distan { for (auto& object : mObjects) { - if (dragMode == DragMode_Select_Only) object.second->setSelected (false); + if (dragMode == DragMode_Select_Only) + object.second->setSelected(false); float distanceFromObject = (point - object.second->getPosition().asVec3()).length(); - if (distanceFromObject < distance) handleSelectDrag(object.second, dragMode); + if (distanceFromObject < distance) + handleSelectDrag(object.second, dragMode); } } -void CSVRender::Cell::setCellArrows (int mask) +void CSVRender::Cell::setCellArrows(int mask) { - for (int i=0; i<4; ++i) + for (int i = 0; i < 4; ++i) { - CellArrow::Direction direction = static_cast (1<(1 << i); bool enable = mask & direction; - if (enable!=(mCellArrows[i].get()!=nullptr)) + if (enable != (mCellArrows[i].get() != nullptr)) { if (enable) - mCellArrows[i].reset (new CellArrow (mCellNode, direction, mCoordinates)); + mCellArrows[i] = std::make_unique(mCellNode, direction, mCoordinates); else - mCellArrows[i].reset (nullptr); + mCellArrows[i].reset(nullptr); } } } @@ -574,8 +588,9 @@ void CSVRender::Cell::setCellMarker() isInteriorCell = cellRecord.get().mData.mFlags & ESM::Cell::Interior; } - if (!isInteriorCell) { - mCellMarker.reset(new CellMarker(mCellNode, mCoordinates, cellExists)); + if (!isInteriorCell) + { + mCellMarker = std::make_unique(mCellNode, mCoordinates, cellExists); } } @@ -589,15 +604,50 @@ bool CSVRender::Cell::isDeleted() const return mDeleted; } -std::vector > CSVRender::Cell::getSelection (unsigned int elementMask) const +osg::ref_ptr CSVRender::Cell::getSnapTarget(unsigned int elementMask) const +{ + osg::ref_ptr result; + + if (elementMask & Mask_Reference) + for (auto& obj : mObjects) + if (obj.second->getSnapTarget()) + return obj.second->getTag(); + + return result; +} + +void CSVRender::Cell::selectFromGroup(const std::vector& group) +{ + for (const auto& [_, object] : mObjects) + { + for (const auto& objectName : group) + { + if (objectName == object->getReferenceId()) + { + object->setSelected(true, osg::Vec4f(1, 0, 1, 1)); + } + } + } +} + +void CSVRender::Cell::unhideAll() +{ + for (const auto& [_, object] : mObjects) + { + osg::ref_ptr rootNode = object->getRootNode(); + if (rootNode->getNodeMask() == Mask_Hidden) + rootNode->setNodeMask(Mask_Reference); + } +} + +std::vector> CSVRender::Cell::getSelection(unsigned int elementMask) const { - std::vector > result; + std::vector> result; if (elementMask & Mask_Reference) - for (std::map::const_iterator iter (mObjects.begin()); - iter!=mObjects.end(); ++iter) + for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) if (iter->second->getSelected()) - result.push_back (iter->second->getTag()); + result.push_back(iter->second->getTag()); if (mPathgrid && elementMask & Mask_Pathgrid) if (mPathgrid->isSelected()) result.emplace_back(mPathgrid->getTag()); @@ -605,35 +655,32 @@ std::vector > CSVRender::Cell::getSelection (un return result; } -std::vector > CSVRender::Cell::getEdited (unsigned int elementMask) const +std::vector> CSVRender::Cell::getEdited(unsigned int elementMask) const { - std::vector > result; + std::vector> result; if (elementMask & Mask_Reference) - for (std::map::const_iterator iter (mObjects.begin()); - iter!=mObjects.end(); ++iter) + for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) if (iter->second->isEdited()) - result.push_back (iter->second->getTag()); + result.push_back(iter->second->getTag()); return result; } -void CSVRender::Cell::setSubMode (int subMode, unsigned int elementMask) +void CSVRender::Cell::setSubMode(int subMode, unsigned int elementMask) { mSubMode = subMode; mSubModeElementMask = elementMask; if (elementMask & Mask_Reference) - for (std::map::const_iterator iter (mObjects.begin()); - iter!=mObjects.end(); ++iter) - iter->second->setSubMode (subMode); + for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) + iter->second->setSubMode(subMode); } -void CSVRender::Cell::reset (unsigned int elementMask) +void CSVRender::Cell::reset(unsigned int elementMask) { if (elementMask & Mask_Reference) - for (std::map::const_iterator iter (mObjects.begin()); - iter!=mObjects.end(); ++iter) + for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) iter->second->reset(); if (mPathgrid && elementMask & Mask_Pathgrid) mPathgrid->resetIndicators(); diff --git a/apps/opencs/view/render/cell.hpp b/apps/opencs/view/render/cell.hpp index 5998a4ee62b..5ec8d87c33d 100644 --- a/apps/opencs/view/render/cell.hpp +++ b/apps/opencs/view/render/cell.hpp @@ -1,24 +1,25 @@ #ifndef OPENCS_VIEW_CELL_H #define OPENCS_VIEW_CELL_H -#include #include #include +#include #include +#include #include +#include "../../model/doc/document.hpp" #include "../../model/world/cellcoordinates.hpp" -#include "terrainstorage.hpp" #include "instancedragmodes.hpp" +#include +#include class QModelIndex; namespace osg { class Group; - class Geometry; - class Geode; } namespace CSMWorld @@ -36,150 +37,152 @@ namespace CSVRender class CellWater; class Pathgrid; class TagBase; + class TerrainStorage; class Object; class CellArrow; class CellBorder; class CellMarker; - class CellWater; class Cell { - CSMWorld::Data& mData; - std::string mId; - osg::ref_ptr mCellNode; - std::map mObjects; - std::unique_ptr mTerrain; - CSMWorld::CellCoordinates mCoordinates; - std::unique_ptr mCellArrows[4]; - std::unique_ptr mCellMarker; - std::unique_ptr mCellBorder; - std::unique_ptr mCellWater; - std::unique_ptr mPathgrid; - bool mDeleted; - int mSubMode; - unsigned int mSubModeElementMask; - bool mUpdateLand, mLandDeleted; - TerrainStorage *mTerrainStorage; + CSMWorld::Data& mData; + ESM::RefId mId; + osg::ref_ptr mCellNode; + std::map mObjects; + std::unique_ptr mTerrain; + CSMWorld::CellCoordinates mCoordinates; + std::unique_ptr mCellArrows[4]; + std::unique_ptr mCellMarker; + std::unique_ptr mCellBorder; + std::unique_ptr mCellWater; + std::unique_ptr mPathgrid; + bool mDeleted; + int mSubMode; + unsigned int mSubModeElementMask; + bool mUpdateLand, mLandDeleted; + TerrainStorage* mTerrainStorage; + + /// Ignored if cell does not have an object with the given ID. + /// + /// \return Was the object deleted? + bool removeObject(const std::string& id); - /// Ignored if cell does not have an object with the given ID. - /// - /// \return Was the object deleted? - bool removeObject (const std::string& id); + // Remove object and return iterator to next object. + std::map::iterator removeObject(std::map::iterator iter); - // Remove object and return iterator to next object. - std::map::iterator removeObject ( - std::map::iterator iter); + /// Add objects from reference table that are within this cell. + /// + /// \return Have any objects been added? + bool addObjects(int start, int end); - /// Add objects from reference table that are within this cell. - /// - /// \return Have any objects been added? - bool addObjects (int start, int end); + void updateLand(); + void unloadLand(); - void updateLand(); - void unloadLand(); + public: + enum Selection + { + Selection_Clear, + Selection_All, + Selection_Invert + }; - public: + 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); - enum Selection - { - Selection_Clear, - Selection_All, - Selection_Invert - }; + ~Cell(); - public: + /// \note Returns the pathgrid representation which will exist as long as the cell exists + Pathgrid* getPathgrid() const; - /// \note Deleted covers both cells that are deleted and cells that don't exist in - /// the first place. - Cell (CSMWorld::Data& data, osg::Group* rootNode, const std::string& id, - bool deleted = false); + /// \return Did this call result in a modification of the visual representation of + /// this cell? + bool referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - ~Cell(); + /// \return Did this call result in a modification of the visual representation of + /// this cell? + bool referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end); - /// \note Returns the pathgrid representation which will exist as long as the cell exists - Pathgrid* getPathgrid() const; + /// \return Did this call result in a modification of the visual representation of + /// this cell? + bool referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - /// \return Did this call result in a modification of the visual representation of - /// this cell? - bool referenceableDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight); + /// \return Did this call result in a modification of the visual representation of + /// this cell? + bool referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end); - /// \return Did this call result in a modification of the visual representation of - /// this cell? - bool referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); + /// \return Did this call result in a modification of the visual representation of + /// this cell? + bool referenceAdded(const QModelIndex& parent, int start, int end); - /// \return Did this call result in a modification of the visual representation of - /// this cell? - bool referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void setAlteredHeight(int inCellX, int inCellY, float height); - /// \return Did this call result in a modification of the visual representation of - /// this cell? - bool referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end); + float getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY); - /// \return Did this call result in a modification of the visual representation of - /// this cell? - bool referenceAdded (const QModelIndex& parent, int start, int end); + float* getAlteredHeight(int inCellX, int inCellY); - void setAlteredHeight(int inCellX, int inCellY, float height); + void resetAlteredHeights(); - float getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY); + void pathgridModified(); - float* getAlteredHeight(int inCellX, int inCellY); + void pathgridRemoved(); - void resetAlteredHeights(); + void landDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - void pathgridModified(); + void landAboutToBeRemoved(const QModelIndex& parent, int start, int end); - void pathgridRemoved(); + void landAdded(const QModelIndex& parent, int start, int end); - void landDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void landTextureChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - void landAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void landTextureAboutToBeRemoved(const QModelIndex& parent, int start, int end); - void landAdded (const QModelIndex& parent, int start, int end); + void landTextureAdded(const QModelIndex& parent, int start, int end); - void landTextureChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void reloadAssets(); - void landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void setSelection(int elementMask, Selection mode); - void landTextureAdded (const QModelIndex& parent, int start, int end); + // Select everything that references the same ID as at least one of the elements + // already selected + void selectAllWithSameParentId(int elementMask); - void reloadAssets(); + void selectFromGroup(const std::vector& group); - void setSelection (int elementMask, Selection mode); + void unhideAll(); - // Select everything that references the same ID as at least one of the elements - // already selected - void selectAllWithSameParentId (int elementMask); + void handleSelectDrag(Object* object, DragMode dragMode); - void handleSelectDrag(Object* object, DragMode dragMode); + void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode); - void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode); + void selectWithinDistance(const osg::Vec3d& pointA, float distance, DragMode dragMode); - void selectWithinDistance(const osg::Vec3d& pointA, float distance, DragMode dragMode); + void setCellArrows(int mask); - void setCellArrows (int mask); + /// \brief Set marker for this cell. + void setCellMarker(); - /// \brief Set marker for this cell. - void setCellMarker(); + /// Returns 0, 0 in case of an unpaged cell. + CSMWorld::CellCoordinates getCoordinates() const; - /// Returns 0, 0 in case of an unpaged cell. - CSMWorld::CellCoordinates getCoordinates() const; + bool isDeleted() const; - bool isDeleted() const; + osg::ref_ptr getSnapTarget(unsigned int elementMask) const; - std::vector > getSelection (unsigned int elementMask) const; + std::vector> getSelection(unsigned int elementMask) const; - std::vector > getEdited (unsigned int elementMask) const; + std::vector> getEdited(unsigned int elementMask) const; - void setSubMode (int subMode, unsigned int elementMask); + void setSubMode(int subMode, unsigned int elementMask); - /// Erase all overrides and restore the visual representation of the cell to its - /// true state. - void reset (unsigned int elementMask); + /// Erase all overrides and restore the visual representation of the cell to its + /// true state. + void reset(unsigned int elementMask); - friend class CellNodeCallback; + friend class CellNodeCallback; }; } diff --git a/apps/opencs/view/render/cellarrow.cpp b/apps/opencs/view/render/cellarrow.cpp index 4d615512368..d31df99a114 100644 --- a/apps/opencs/view/render/cellarrow.cpp +++ b/apps/opencs/view/render/cellarrow.cpp @@ -1,38 +1,62 @@ - #include "cellarrow.hpp" +#include +#include +#include #include +#include #include -#include -#include #include +#include +#include +#include +#include +#include #include "../../model/prefs/state.hpp" -#include "../../model/prefs/shortcutmanager.hpp" + +#include +#include +#include #include #include "mask.hpp" -CSVRender::CellArrowTag::CellArrowTag (CellArrow *arrow) -: TagBase (Mask_CellArrow), mArrow (arrow) -{} +namespace CSVRender +{ + struct WorldspaceHitResult; +} -CSVRender::CellArrow *CSVRender::CellArrowTag::getCellArrow() const +CSVRender::CellArrowTag::CellArrowTag(CellArrow* arrow) + : TagBase(Mask_CellArrow) + , mArrow(arrow) +{ +} + +CSVRender::CellArrow* CSVRender::CellArrowTag::getCellArrow() const { return mArrow; } -QString CSVRender::CellArrowTag::getToolTip (bool hideBasics) const +QString CSVRender::CellArrowTag::getToolTip(bool hideBasics, const WorldspaceHitResult& /*hit*/) const { - QString text ("Direction: "); + QString text("Direction: "); switch (mArrow->getDirection()) { - case CellArrow::Direction_North: text += "North"; break; - case CellArrow::Direction_West: text += "West"; break; - case CellArrow::Direction_South: text += "South"; break; - case CellArrow::Direction_East: text += "East"; break; + case CellArrow::Direction_North: + text += "North"; + break; + case CellArrow::Direction_West: + text += "West"; + break; + case CellArrow::Direction_South: + text += "South"; + break; + case CellArrow::Direction_East: + text += "East"; + break; } if (!hideBasics) @@ -55,127 +79,138 @@ QString CSVRender::CellArrowTag::getToolTip (bool hideBasics) const return CSMPrefs::State::get().getShortcutManager().processToolTip(text); } - void CSVRender::CellArrow::adjustTransform() { // position const int cellSize = Constants::CellSizeInUnits; - const int offset = cellSize / 2 + 800; + const int offset = cellSize / 2 + 600; - int x = mCoordinates.getX()*cellSize + cellSize/2; - int y = mCoordinates.getY()*cellSize + cellSize/2; + int x = mCoordinates.getX() * cellSize + cellSize / 2; + int y = mCoordinates.getY() * cellSize + cellSize / 2; float xr = 0; float yr = 0; float zr = 0; - float angle = osg::DegreesToRadians (90.0f); + float angle = osg::DegreesToRadians(90.0f); switch (mDirection) { - case Direction_North: y += offset; xr = -angle; zr = angle; break; - case Direction_West: x -= offset; yr = -angle; break; - case Direction_South: y -= offset; xr = angle; zr = angle; break; - case Direction_East: x += offset; yr = angle; break; + case Direction_North: + y += offset; + xr = -angle; + zr = angle; + break; + case Direction_West: + x -= offset; + yr = -angle; + break; + case Direction_South: + y -= offset; + xr = angle; + zr = angle; + break; + case Direction_East: + x += offset; + yr = angle; + break; }; - mBaseNode->setPosition (osg::Vec3f (x, y, 0)); + mBaseNode->setPosition(osg::Vec3f(x, y, 0)); // orientation - osg::Quat xr2 (xr, osg::Vec3f (1,0,0)); - osg::Quat yr2 (yr, osg::Vec3f (0,1,0)); - osg::Quat zr2 (zr, osg::Vec3f (0,0,1)); - mBaseNode->setAttitude (zr2*yr2*xr2); + osg::Quat xr2(xr, osg::Vec3f(1, 0, 0)); + osg::Quat yr2(yr, osg::Vec3f(0, 1, 0)); + osg::Quat zr2(zr, osg::Vec3f(0, 0, 1)); + mBaseNode->setAttitude(zr2 * yr2 * xr2); } void CSVRender::CellArrow::buildShape() { - osg::ref_ptr geometry (new osg::Geometry); + osg::ref_ptr geometry(new osg::Geometry); - const int arrowWidth = 4000; - const int arrowLength = 1500; - const int arrowHeight = 500; + const int arrowWidth = 2700; + const int arrowLength = 1350; + const int arrowHeight = 300; - osg::Vec3Array *vertices = new osg::Vec3Array; - for (int i2=0; i2<2; ++i2) - for (int i=0; i<2; ++i) + osg::Vec3Array* vertices = new osg::Vec3Array; + for (int i2 = 0; i2 < 2; ++i2) + for (int i = 0; i < 2; ++i) { - float height = i ? -arrowHeight/2 : arrowHeight/2; - vertices->push_back (osg::Vec3f (height, -arrowWidth/2, 0)); - vertices->push_back (osg::Vec3f (height, arrowWidth/2, 0)); - vertices->push_back (osg::Vec3f (height, 0, arrowLength)); + float height = i ? -arrowHeight / 2 : arrowHeight / 2; + vertices->push_back(osg::Vec3f(height, -arrowWidth / 2, 0)); + vertices->push_back(osg::Vec3f(height, arrowWidth / 2, 0)); + vertices->push_back(osg::Vec3f(height, 0, arrowLength)); } - geometry->setVertexArray (vertices); + geometry->setVertexArray(vertices); - osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); + osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, 0); // top - primitives->push_back (0); - primitives->push_back (1); - primitives->push_back (2); + primitives->push_back(0); + primitives->push_back(1); + primitives->push_back(2); // bottom - primitives->push_back (5); - primitives->push_back (4); - primitives->push_back (3); + primitives->push_back(5); + primitives->push_back(4); + primitives->push_back(3); // back - primitives->push_back (3+6); - primitives->push_back (4+6); - primitives->push_back (1+6); + primitives->push_back(3 + 6); + primitives->push_back(4 + 6); + primitives->push_back(1 + 6); - primitives->push_back (3+6); - primitives->push_back (1+6); - primitives->push_back (0+6); + primitives->push_back(3 + 6); + primitives->push_back(1 + 6); + primitives->push_back(0 + 6); // sides - primitives->push_back (0+6); - primitives->push_back (2+6); - primitives->push_back (5+6); - - primitives->push_back (0+6); - primitives->push_back (5+6); - primitives->push_back (3+6); + primitives->push_back(0 + 6); + primitives->push_back(2 + 6); + primitives->push_back(5 + 6); - primitives->push_back (4+6); - primitives->push_back (5+6); - primitives->push_back (2+6); + primitives->push_back(0 + 6); + primitives->push_back(5 + 6); + primitives->push_back(3 + 6); - primitives->push_back (4+6); - primitives->push_back (2+6); - primitives->push_back (1+6); + primitives->push_back(4 + 6); + primitives->push_back(5 + 6); + primitives->push_back(2 + 6); - geometry->addPrimitiveSet (primitives); + primitives->push_back(4 + 6); + primitives->push_back(2 + 6); + primitives->push_back(1 + 6); - osg::Vec4Array *colours = new osg::Vec4Array; + geometry->addPrimitiveSet(primitives); - for (int i=0; i<6; ++i) - colours->push_back (osg::Vec4f (0.11f, 0.6f, 0.95f, 1.0f)); - for (int i=0; i<6; ++i) - colours->push_back (osg::Vec4f (0.08f, 0.44f, 0.7f, 1.0f)); + osg::Vec4Array* colours = new osg::Vec4Array; - geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); + for (int i = 0; i < 6; ++i) + colours->push_back(osg::Vec4f(0.11f, 0.6f, 0.95f, 1.0f)); + for (int i = 0; i < 6; ++i) + colours->push_back(osg::Vec4f(0.08f, 0.44f, 0.7f, 1.0f)); - geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); + geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); - osg::ref_ptr geode (new osg::Geode); - geode->addDrawable (geometry); + geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - mBaseNode->addChild (geode); + mBaseNode->addChild(geometry); } -CSVRender::CellArrow::CellArrow (osg::Group *cellNode, Direction direction, - const CSMWorld::CellCoordinates& coordinates) -: mDirection (direction), mParentNode (cellNode), mCoordinates (coordinates) +CSVRender::CellArrow::CellArrow(osg::Group* cellNode, Direction direction, const CSMWorld::CellCoordinates& coordinates) + : mDirection(direction) + , mParentNode(cellNode) + , mCoordinates(coordinates) { mBaseNode = new osg::PositionAttitudeTransform; - mBaseNode->setUserData (new CellArrowTag (this)); + mBaseNode->setUserData(new CellArrowTag(this)); - mParentNode->addChild (mBaseNode); + mParentNode->addChild(mBaseNode); - mBaseNode->setNodeMask (Mask_CellArrow); + mBaseNode->setNodeMask(Mask_CellArrow); adjustTransform(); buildShape(); @@ -183,7 +218,7 @@ CSVRender::CellArrow::CellArrow (osg::Group *cellNode, Direction direction, CSVRender::CellArrow::~CellArrow() { - mParentNode->removeChild (mBaseNode); + mParentNode->removeChild(mBaseNode); } CSMWorld::CellCoordinates CSVRender::CellArrow::getCoordinates() const diff --git a/apps/opencs/view/render/cellarrow.hpp b/apps/opencs/view/render/cellarrow.hpp index 9a49b80db03..7bb39e02dac 100644 --- a/apps/opencs/view/render/cellarrow.hpp +++ b/apps/opencs/view/render/cellarrow.hpp @@ -3,6 +3,8 @@ #include "tagbase.hpp" +#include + #include #include "../../model/world/cellcoordinates.hpp" @@ -16,58 +18,53 @@ namespace osg namespace CSVRender { class CellArrow; + struct WorldspaceHitResult; class CellArrowTag : public TagBase { - CellArrow *mArrow; - - public: + CellArrow* mArrow; - CellArrowTag (CellArrow *arrow); + public: + CellArrowTag(CellArrow* arrow); - CellArrow *getCellArrow() const; + CellArrow* getCellArrow() const; - QString getToolTip (bool hideBasics) const override; + QString getToolTip(bool hideBasics, const WorldspaceHitResult& hit) const override; }; - class CellArrow { - public: - - enum Direction - { - Direction_North = 1, - Direction_West = 2, - Direction_South = 4, - Direction_East = 8 - }; - - private: - - // not implemented - CellArrow (const CellArrow&); - CellArrow& operator= (const CellArrow&); + public: + enum Direction + { + Direction_North = 1, + Direction_West = 2, + Direction_South = 4, + Direction_East = 8 + }; - Direction mDirection; - osg::Group* mParentNode; - osg::ref_ptr mBaseNode; - CSMWorld::CellCoordinates mCoordinates; + private: + // not implemented + CellArrow(const CellArrow&); + CellArrow& operator=(const CellArrow&); - void adjustTransform(); + Direction mDirection; + osg::Group* mParentNode; + osg::ref_ptr mBaseNode; + CSMWorld::CellCoordinates mCoordinates; - void buildShape(); + void adjustTransform(); - public: + void buildShape(); - CellArrow (osg::Group *cellNode, Direction direction, - const CSMWorld::CellCoordinates& coordinates); + public: + CellArrow(osg::Group* cellNode, Direction direction, const CSMWorld::CellCoordinates& coordinates); - ~CellArrow(); + ~CellArrow(); - CSMWorld::CellCoordinates getCoordinates() const; + CSMWorld::CellCoordinates getCoordinates() const; - Direction getDirection() const; + Direction getDirection() const; }; } diff --git a/apps/opencs/view/render/cellborder.cpp b/apps/opencs/view/render/cellborder.cpp index d8ff638010b..704db13e891 100644 --- a/apps/opencs/view/render/cellborder.cpp +++ b/apps/opencs/view/render/cellborder.cpp @@ -1,11 +1,17 @@ #include "cellborder.hpp" +#include +#include +#include #include #include -#include #include +#include +#include +#include +#include -#include +#include #include "mask.hpp" @@ -19,12 +25,11 @@ const int CSVRender::CellBorder::CellSize = ESM::Land::REAL_SIZE; */ const int CSVRender::CellBorder::VertexCount = (ESM::Land::LAND_SIZE * 4) - 4; - CSVRender::CellBorder::CellBorder(osg::Group* cellNode, const CSMWorld::CellCoordinates& coords) : mParentNode(cellNode) { mBorderGeometry = new osg::Geometry(); - + mBaseNode = new osg::PositionAttitudeTransform(); mBaseNode->setNodeMask(Mask_CellBorder); mBaseNode->setPosition(osg::Vec3f(coords.getX() * CellSize, coords.getY() * CellSize, 10)); @@ -42,9 +47,6 @@ void CSVRender::CellBorder::buildShape(const ESM::Land& esmLand) { const ESM::Land::LandData* landData = esmLand.getLandData(ESM::Land::DATA_VHGT); - if (!landData) - return; - mBaseNode->removeChild(mBorderGeometry); mBorderGeometry = new osg::Geometry(); @@ -57,20 +59,40 @@ void CSVRender::CellBorder::buildShape(const ESM::Land& esmLand) Traverse the cell border counter-clockwise starting at the SW corner vertex (0, 0). Each loop starts at a corner vertex and ends right before the next corner vertex. */ - for (; x < ESM::Land::LAND_SIZE - 1; ++x) - vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); - - x = ESM::Land::LAND_SIZE - 1; - for (; y < ESM::Land::LAND_SIZE - 1; ++y) - vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); - - y = ESM::Land::LAND_SIZE - 1; - for (; x > 0; --x) - vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); - - x = 0; - for (; y > 0; --y) - vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); + if (landData) + { + for (; x < ESM::Land::LAND_SIZE - 1; ++x) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); + + x = ESM::Land::LAND_SIZE - 1; + for (; y < ESM::Land::LAND_SIZE - 1; ++y) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); + + y = ESM::Land::LAND_SIZE - 1; + for (; x > 0; --x) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); + + x = 0; + for (; y > 0; --y) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), landData->mHeights[landIndex(x, y)])); + } + else + { + for (; x < ESM::Land::LAND_SIZE - 1; ++x) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), ESM::Land::DEFAULT_HEIGHT)); + + x = ESM::Land::LAND_SIZE - 1; + for (; y < ESM::Land::LAND_SIZE - 1; ++y) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), ESM::Land::DEFAULT_HEIGHT)); + + y = ESM::Land::LAND_SIZE - 1; + for (; x > 0; --x) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), ESM::Land::DEFAULT_HEIGHT)); + + x = 0; + for (; y > 0; --y) + vertices->push_back(osg::Vec3f(scaleToWorld(x), scaleToWorld(y), ESM::Land::DEFAULT_HEIGHT)); + } mBorderGeometry->setVertexArray(vertices); @@ -79,8 +101,8 @@ void CSVRender::CellBorder::buildShape(const ESM::Land& esmLand) mBorderGeometry->setColorArray(colors, osg::Array::BIND_PER_PRIMITIVE_SET); - osg::ref_ptr primitives = - new osg::DrawElementsUShort(osg::PrimitiveSet::LINE_STRIP, VertexCount + 1); + osg::ref_ptr primitives + = new osg::DrawElementsUShort(osg::PrimitiveSet::LINE_STRIP, VertexCount + 1); // Assign one primitive to each vertex. for (size_t i = 0; i < VertexCount; ++i) diff --git a/apps/opencs/view/render/cellborder.hpp b/apps/opencs/view/render/cellborder.hpp index be2e18eeee5..e1201a1f392 100644 --- a/apps/opencs/view/render/cellborder.hpp +++ b/apps/opencs/view/render/cellborder.hpp @@ -28,14 +28,12 @@ namespace CSVRender class CellBorder { public: - CellBorder(osg::Group* cellNode, const CSMWorld::CellCoordinates& coords); ~CellBorder(); void buildShape(const ESM::Land& esmLand); private: - static const int CellSize; static const int VertexCount; diff --git a/apps/opencs/view/render/cellmarker.cpp b/apps/opencs/view/render/cellmarker.cpp index 3de96ab0238..5b116ec04dd 100644 --- a/apps/opencs/view/render/cellmarker.cpp +++ b/apps/opencs/view/render/cellmarker.cpp @@ -1,17 +1,31 @@ #include "cellmarker.hpp" +#include + #include +#include +#include #include -#include +#include +#include +#include +#include #include +#include #include -CSVRender::CellMarkerTag::CellMarkerTag(CellMarker *marker) -: TagBase(Mask_CellMarker), mMarker(marker) -{} +#include +#include +#include + +CSVRender::CellMarkerTag::CellMarkerTag(CellMarker* marker) + : TagBase(Mask_CellMarker) + , mMarker(marker) +{ +} -CSVRender::CellMarker *CSVRender::CellMarkerTag::getCellMarker() const +CSVRender::CellMarker* CSVRender::CellMarkerTag::getCellMarker() const { return mMarker; } @@ -21,7 +35,7 @@ void CSVRender::CellMarker::buildMarker() const int characterSize = 20; // Set up attributes of marker text. - osg::ref_ptr markerText (new osgText::Text); + osg::ref_ptr markerText(new osgText::Text); markerText->setLayout(osgText::Text::LEFT_TO_RIGHT); markerText->setCharacterSize(characterSize); markerText->setAlignment(osgText::Text::CENTER_CENTER); @@ -38,15 +52,11 @@ void CSVRender::CellMarker::buildMarker() } // Add text containing cell's coordinates. - std::string coordinatesText = - std::to_string(mCoordinates.getX()) + "," + - std::to_string(mCoordinates.getY()); + std::string coordinatesText = std::to_string(mCoordinates.getX()) + "," + std::to_string(mCoordinates.getY()); markerText->setText(coordinatesText); // Add text to marker node. - osg::ref_ptr geode (new osg::Geode); - geode->addDrawable(markerText); - mMarkerNode->addChild(geode); + mMarkerNode->addChild(markerText); } void CSVRender::CellMarker::positionMarker() @@ -61,12 +71,10 @@ void CSVRender::CellMarker::positionMarker() } CSVRender::CellMarker::CellMarker( - osg::Group *cellNode, - const CSMWorld::CellCoordinates& coordinates, - const bool cellExists -) : mCellNode(cellNode), - mCoordinates(coordinates), - mExists(cellExists) + osg::Group* cellNode, const CSMWorld::CellCoordinates& coordinates, const bool cellExists) + : mCellNode(cellNode) + , mCoordinates(coordinates) + , mExists(cellExists) { // Set up node for cell marker. mMarkerNode = new osg::AutoTransform(); diff --git a/apps/opencs/view/render/cellmarker.hpp b/apps/opencs/view/render/cellmarker.hpp index 4246b20b8de..c623a296053 100644 --- a/apps/opencs/view/render/cellmarker.hpp +++ b/apps/opencs/view/render/cellmarker.hpp @@ -19,49 +19,42 @@ namespace CSVRender class CellMarkerTag : public TagBase { - private: + private: + CellMarker* mMarker; - CellMarker *mMarker; + public: + CellMarkerTag(CellMarker* marker); - public: - - CellMarkerTag(CellMarker *marker); - - CellMarker *getCellMarker() const; + CellMarker* getCellMarker() const; }; /// \brief Marker to display cell coordinates. class CellMarker { - private: - - osg::Group* mCellNode; - osg::ref_ptr mMarkerNode; - CSMWorld::CellCoordinates mCoordinates; - bool mExists; - - // Not implemented. - CellMarker(const CellMarker&); - CellMarker& operator=(const CellMarker&); - - /// \brief Build marker containing cell's coordinates. - void buildMarker(); - - /// \brief Position marker at center of cell. - void positionMarker(); - - public: - - /// \brief Constructor. - /// \param cellNode Cell to create marker for. - /// \param coordinates Coordinates of cell. - /// \param cellExists Whether or not cell exists. - CellMarker( - osg::Group *cellNode, - const CSMWorld::CellCoordinates& coordinates, - const bool cellExists); - - ~CellMarker(); + private: + osg::Group* mCellNode; + osg::ref_ptr mMarkerNode; + CSMWorld::CellCoordinates mCoordinates; + bool mExists; + + // Not implemented. + CellMarker(const CellMarker&); + CellMarker& operator=(const CellMarker&); + + /// \brief Build marker containing cell's coordinates. + void buildMarker(); + + /// \brief Position marker at center of cell. + void positionMarker(); + + public: + /// \brief Constructor. + /// \param cellNode Cell to create marker for. + /// \param coordinates Coordinates of cell. + /// \param cellExists Whether or not cell exists. + CellMarker(osg::Group* cellNode, const CSMWorld::CellCoordinates& coordinates, const bool cellExists); + + ~CellMarker(); }; } diff --git a/apps/opencs/view/render/cellwater.cpp b/apps/opencs/view/render/cellwater.cpp index f8857c3afcb..5ad860b4350 100644 --- a/apps/opencs/view/render/cellwater.cpp +++ b/apps/opencs/view/render/cellwater.cpp @@ -1,13 +1,26 @@ #include "cellwater.hpp" -#include +#include +#include + #include #include #include - -#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include #include -#include +#include #include #include #include @@ -22,29 +35,29 @@ namespace CSVRender { const int CellWater::CellSize = ESM::Land::REAL_SIZE; - CellWater::CellWater(CSMWorld::Data& data, osg::Group* cellNode, const std::string& id, - const CSMWorld::CellCoordinates& cellCoords) + CellWater::CellWater( + CSMWorld::Data& data, osg::Group* cellNode, const std::string& id, const CSMWorld::CellCoordinates& cellCoords) : mData(data) , mId(id) , mParentNode(cellNode) , mWaterTransform(nullptr) - , mWaterNode(nullptr) + , mWaterGroup(nullptr) , mWaterGeometry(nullptr) , mDeleted(false) , mExterior(false) , mHasWater(false) { mWaterTransform = new osg::PositionAttitudeTransform(); - mWaterTransform->setPosition(osg::Vec3f(cellCoords.getX() * CellSize + CellSize / 2.f, - cellCoords.getY() * CellSize + CellSize / 2.f, 0)); + mWaterTransform->setPosition(osg::Vec3f( + cellCoords.getX() * CellSize + CellSize / 2.f, cellCoords.getY() * CellSize + CellSize / 2.f, 0)); mWaterTransform->setNodeMask(Mask_Water); mParentNode->addChild(mWaterTransform); - mWaterNode = new osg::Geode(); - mWaterTransform->addChild(mWaterNode); + mWaterGroup = new osg::Group(); + mWaterTransform->addChild(mWaterGroup); - int cellIndex = mData.getCells().searchId(mId); + const int cellIndex = mData.getCells().searchId(ESM::RefId::stringRefId(mId)); if (cellIndex > -1) { updateCellData(mData.getCells().getRecord(cellIndex)); @@ -52,8 +65,7 @@ namespace CSVRender // Keep water existence/height up to date QAbstractItemModel* cells = mData.getTableModel(CSMWorld::UniversalId::Type_Cells); - connect(cells, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), - this, SLOT(cellDataChanged(const QModelIndex&, const QModelIndex&))); + connect(cells, &QAbstractItemModel::dataChanged, this, &CellWater::cellDataChanged); } CellWater::~CellWater() @@ -119,7 +131,7 @@ namespace CSVRender { const CSMWorld::Record& cellRecord = cells.getRecord(row); - if (Misc::StringUtils::lowerCase(cellRecord.get().mId) == mId) + if (cellRecord.get().mId == ESM::RefId::stringRefId(mId)) updateCellData(cellRecord); } } @@ -136,7 +148,7 @@ namespace CSVRender if (mWaterGeometry) { - mWaterNode->removeDrawable(mWaterGeometry); + mWaterGroup->removeChild(mWaterGeometry); mWaterGeometry = nullptr; } @@ -164,19 +176,19 @@ namespace CSVRender mWaterGeometry->setStateSet(SceneUtil::createSimpleWaterStateSet(Alpha, RenderBin)); // Add water texture - std::string textureName = Fallback::Map::getString("Water_SurfaceTexture"); - textureName = "textures/water/" + textureName + "00.dds"; + constexpr VFS::Path::NormalizedView prefix("textures/water"); + VFS::Path::Normalized texturePath(prefix); + texturePath /= std::string(Fallback::Map::getString("Water_SurfaceTexture")) + "00.dds"; Resource::ImageManager* imageManager = mData.getResourceSystem()->getImageManager(); osg::ref_ptr waterTexture = new osg::Texture2D(); - waterTexture->setImage(imageManager->getImage(textureName)); + waterTexture->setImage(imageManager->getImage(texturePath)); waterTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); waterTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); mWaterGeometry->getStateSet()->setTextureAttributeAndModes(0, waterTexture, osg::StateAttribute::ON); - - mWaterNode->addDrawable(mWaterGeometry); + mWaterGroup->addChild(mWaterGeometry); } } diff --git a/apps/opencs/view/render/cellwater.hpp b/apps/opencs/view/render/cellwater.hpp index 47e5867071c..249849d784f 100644 --- a/apps/opencs/view/render/cellwater.hpp +++ b/apps/opencs/view/render/cellwater.hpp @@ -5,14 +5,13 @@ #include -#include #include +#include -#include "../../model/world/record.hpp" +class QModelIndex; namespace osg { - class Geode; class Geometry; class Group; class PositionAttitudeTransform; @@ -23,6 +22,9 @@ namespace CSMWorld struct Cell; class CellCoordinates; class Data; + + template + struct Record; } namespace CSVRender @@ -31,41 +33,39 @@ namespace CSVRender /// adds a large patch of water much larger than the typical size of a cell. class CellWater : public QObject { - Q_OBJECT - - public: - - CellWater(CSMWorld::Data& data, osg::Group* cellNode, const std::string& id, - const CSMWorld::CellCoordinates& cellCoords); + Q_OBJECT - ~CellWater(); + public: + CellWater(CSMWorld::Data& data, osg::Group* cellNode, const std::string& id, + const CSMWorld::CellCoordinates& cellCoords); - void updateCellData(const CSMWorld::Record& cellRecord); + ~CellWater(); - void reloadAssets(); + void updateCellData(const CSMWorld::Record& cellRecord); - private slots: + void reloadAssets(); - void cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); + private slots: - private: + void cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - void recreate(); + private: + void recreate(); - static const int CellSize; + static const int CellSize; - CSMWorld::Data& mData; - std::string mId; + CSMWorld::Data& mData; + std::string mId; - osg::Group* mParentNode; + osg::Group* mParentNode; - osg::ref_ptr mWaterTransform; - osg::ref_ptr mWaterNode; - osg::ref_ptr mWaterGeometry; + osg::ref_ptr mWaterTransform; + osg::ref_ptr mWaterGroup; + osg::ref_ptr mWaterGeometry; - bool mDeleted; - bool mExterior; - bool mHasWater; + bool mDeleted; + bool mExterior; + bool mHasWater; }; } diff --git a/apps/opencs/view/render/commands.cpp b/apps/opencs/view/render/commands.cpp index 7b37602961b..68e72778ae1 100644 --- a/apps/opencs/view/render/commands.cpp +++ b/apps/opencs/view/render/commands.cpp @@ -1,19 +1,45 @@ #include "commands.hpp" -#include +#include -#include "terrainselection.hpp" +#include +#include -CSVRender::DrawTerrainSelectionCommand::DrawTerrainSelectionCommand(TerrainSelection& terrainSelection, QUndoCommand* parent) - : mTerrainSelection(terrainSelection) -{ } +#include + +#include "terrainshapemode.hpp" +#include "worldspacewidget.hpp" + +CSVRender::DrawTerrainSelectionCommand::DrawTerrainSelectionCommand( + WorldspaceWidget* worldspaceWidget, QUndoCommand* parent) + : mWorldspaceWidget(worldspaceWidget) +{ +} void CSVRender::DrawTerrainSelectionCommand::redo() { - mTerrainSelection.update(); + tryUpdate(); } void CSVRender::DrawTerrainSelectionCommand::undo() { - mTerrainSelection.update(); + tryUpdate(); +} + +void CSVRender::DrawTerrainSelectionCommand::tryUpdate() +{ + if (!mWorldspaceWidget) + { + Log(Debug::Verbose) << "Can't update terrain selection, no WorldspaceWidget found!"; + return; + } + + auto terrainMode = dynamic_cast(mWorldspaceWidget->getEditMode()); + if (!terrainMode) + { + Log(Debug::Verbose) << "Can't update terrain selection in current EditMode"; + return; + } + + terrainMode->getTerrainSelection()->update(); } diff --git a/apps/opencs/view/render/commands.hpp b/apps/opencs/view/render/commands.hpp index cdc389e33a6..69ba9b0f7fe 100644 --- a/apps/opencs/view/render/commands.hpp +++ b/apps/opencs/view/render/commands.hpp @@ -1,11 +1,12 @@ #ifndef CSV_RENDER_COMMANDS_HPP #define CSV_RENDER_COMMANDS_HPP +#include #include namespace CSVRender { - class TerrainSelection; + class WorldspaceWidget; /* Current solution to force a redrawing of the terrain-selection grid @@ -21,14 +22,17 @@ namespace CSVRender */ class DrawTerrainSelectionCommand : public QUndoCommand { + private: - TerrainSelection& mTerrainSelection; + QPointer mWorldspaceWidget; public: - DrawTerrainSelectionCommand(TerrainSelection& terrainSelection, QUndoCommand* parent = nullptr); + DrawTerrainSelectionCommand(WorldspaceWidget* worldspaceWidget, QUndoCommand* parent = nullptr); void redo() override; void undo() override; + + void tryUpdate(); }; } diff --git a/apps/opencs/view/render/editmode.cpp b/apps/opencs/view/render/editmode.cpp index ca4aa0fd552..6f40f8446ee 100644 --- a/apps/opencs/view/render/editmode.cpp +++ b/apps/opencs/view/render/editmode.cpp @@ -1,79 +1,90 @@ #include "editmode.hpp" -#include "tagbase.hpp" +#include + #include "worldspacewidget.hpp" +class QMouseEvent; +class QWidget; + +namespace CSVWidget +{ + class SceneToolbar; +} + CSVRender::WorldspaceWidget& CSVRender::EditMode::getWorldspaceWidget() { return *mWorldspaceWidget; } -CSVRender::EditMode::EditMode (WorldspaceWidget *worldspaceWidget, const QIcon& icon, - unsigned int mask, const QString& tooltip, QWidget *parent) -: ModeButton (icon, tooltip, parent), mWorldspaceWidget (worldspaceWidget), mMask (mask) -{} +CSVRender::EditMode::EditMode( + WorldspaceWidget* worldspaceWidget, const QIcon& icon, unsigned int mask, const QString& tooltip, QWidget* parent) + : ModeButton(icon, tooltip, parent) + , mWorldspaceWidget(worldspaceWidget) + , mMask(mask) +{ +} unsigned int CSVRender::EditMode::getInteractionMask() const { return mMask; } -void CSVRender::EditMode::activate (CSVWidget::SceneToolbar *toolbar) +void CSVRender::EditMode::activate(CSVWidget::SceneToolbar* toolbar) { - mWorldspaceWidget->setInteractionMask (mMask); - mWorldspaceWidget->clearSelection (~mMask); + mWorldspaceWidget->setInteractionMask(mMask); + mWorldspaceWidget->clearSelection(~mMask); } -void CSVRender::EditMode::setEditLock (bool locked) -{ +void CSVRender::EditMode::setEditLock(bool locked) {} -} +void CSVRender::EditMode::primaryOpenPressed(const WorldspaceHitResult& hit) {} -void CSVRender::EditMode::primaryOpenPressed (const WorldspaceHitResult& hit) {} +void CSVRender::EditMode::primaryEditPressed(const WorldspaceHitResult& hit) {} -void CSVRender::EditMode::primaryEditPressed (const WorldspaceHitResult& hit) {} +void CSVRender::EditMode::secondaryEditPressed(const WorldspaceHitResult& hit) {} -void CSVRender::EditMode::secondaryEditPressed (const WorldspaceHitResult& hit) {} +void CSVRender::EditMode::primarySelectPressed(const WorldspaceHitResult& hit) {} -void CSVRender::EditMode::primarySelectPressed (const WorldspaceHitResult& hit) {} +void CSVRender::EditMode::secondarySelectPressed(const WorldspaceHitResult& hit) {} -void CSVRender::EditMode::secondarySelectPressed (const WorldspaceHitResult& hit) {} +void CSVRender::EditMode::tertiarySelectPressed(const WorldspaceHitResult& hit) {} -bool CSVRender::EditMode::primaryEditStartDrag (const QPoint& pos) +bool CSVRender::EditMode::primaryEditStartDrag(const QPoint& pos) { return false; } -bool CSVRender::EditMode::secondaryEditStartDrag (const QPoint& pos) +bool CSVRender::EditMode::secondaryEditStartDrag(const QPoint& pos) { return false; } -bool CSVRender::EditMode::primarySelectStartDrag (const QPoint& pos) +bool CSVRender::EditMode::primarySelectStartDrag(const QPoint& pos) { return false; } -bool CSVRender::EditMode::secondarySelectStartDrag (const QPoint& pos) +bool CSVRender::EditMode::secondarySelectStartDrag(const QPoint& pos) { return false; } -void CSVRender::EditMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) {} +void CSVRender::EditMode::drag(const QPoint& pos, int diffX, int diffY, double speedFactor) {} void CSVRender::EditMode::dragCompleted(const QPoint& pos) {} void CSVRender::EditMode::dragAborted() {} -void CSVRender::EditMode::dragWheel (int diff, double speedFactor) {} +void CSVRender::EditMode::dragWheel(int diff, double speedFactor) {} -void CSVRender::EditMode::dragEnterEvent (QDragEnterEvent *event) {} +void CSVRender::EditMode::dragEnterEvent(QDragEnterEvent* event) {} -void CSVRender::EditMode::dropEvent (QDropEvent *event) {} +void CSVRender::EditMode::dropEvent(QDropEvent* event) {} -void CSVRender::EditMode::dragMoveEvent (QDragMoveEvent *event) {} +void CSVRender::EditMode::dragMoveEvent(QDragMoveEvent* event) {} -void CSVRender::EditMode::mouseMoveEvent (QMouseEvent *event) {} +void CSVRender::EditMode::mouseMoveEvent(QMouseEvent* event) {} int CSVRender::EditMode::getSubMode() const { diff --git a/apps/opencs/view/render/editmode.hpp b/apps/opencs/view/render/editmode.hpp index 52c35811d26..3492dbe0e64 100644 --- a/apps/opencs/view/render/editmode.hpp +++ b/apps/opencs/view/render/editmode.hpp @@ -1,107 +1,113 @@ #ifndef CSV_RENDER_EDITMODE_H #define CSV_RENDER_EDITMODE_H -#include - #include "../widget/modebutton.hpp" class QDragEnterEvent; class QDropEvent; class QDragMoveEvent; class QPoint; +class QMouseEvent; +class QObject; +class QWidget; + +namespace CSVWidget +{ + class SceneToolbar; +} namespace CSVRender { class WorldspaceWidget; struct WorldspaceHitResult; - class TagBase; class EditMode : public CSVWidget::ModeButton { - Q_OBJECT - - WorldspaceWidget *mWorldspaceWidget; - unsigned int mMask; + Q_OBJECT - protected: + WorldspaceWidget* mWorldspaceWidget; + unsigned int mMask; - WorldspaceWidget& getWorldspaceWidget(); + protected: + WorldspaceWidget& getWorldspaceWidget(); - public: + public: + EditMode(WorldspaceWidget* worldspaceWidget, const QIcon& icon, unsigned int mask, const QString& tooltip = "", + QWidget* parent = nullptr); - EditMode (WorldspaceWidget *worldspaceWidget, const QIcon& icon, unsigned int mask, - const QString& tooltip = "", QWidget *parent = nullptr); + unsigned int getInteractionMask() const; - unsigned int getInteractionMask() const; + void activate(CSVWidget::SceneToolbar* toolbar) override; - void activate (CSVWidget::SceneToolbar *toolbar) override; + /// Default-implementation: Ignored. + virtual void setEditLock(bool locked); - /// Default-implementation: Ignored. - virtual void setEditLock (bool locked); + /// Default-implementation: Ignored. + virtual void primaryOpenPressed(const WorldspaceHitResult& hit); - /// Default-implementation: Ignored. - virtual void primaryOpenPressed (const WorldspaceHitResult& hit); + /// Default-implementation: Ignored. + virtual void primaryEditPressed(const WorldspaceHitResult& hit); - /// Default-implementation: Ignored. - virtual void primaryEditPressed (const WorldspaceHitResult& hit); + /// Default-implementation: Ignored. + virtual void secondaryEditPressed(const WorldspaceHitResult& hit); - /// Default-implementation: Ignored. - virtual void secondaryEditPressed (const WorldspaceHitResult& hit); + /// Default-implementation: Ignored. + virtual void primarySelectPressed(const WorldspaceHitResult& hit); - /// Default-implementation: Ignored. - virtual void primarySelectPressed (const WorldspaceHitResult& hit); + /// Default-implementation: Ignored. + virtual void secondarySelectPressed(const WorldspaceHitResult& hit); - /// Default-implementation: Ignored. - virtual void secondarySelectPressed (const WorldspaceHitResult& hit); + /// Default-implementation: Ignored. + virtual void tertiarySelectPressed(const WorldspaceHitResult& hit); - /// Default-implementation: ignore and return false - /// - /// \return Drag accepted? - virtual bool primaryEditStartDrag (const QPoint& pos); + /// Default-implementation: ignore and return false + /// + /// \return Drag accepted? + virtual bool primaryEditStartDrag(const QPoint& pos); - /// Default-implementation: ignore and return false - /// - /// \return Drag accepted? - virtual bool secondaryEditStartDrag (const QPoint& pos); + /// Default-implementation: ignore and return false + /// + /// \return Drag accepted? + virtual bool secondaryEditStartDrag(const QPoint& pos); - /// Default-implementation: ignore and return false - /// - /// \return Drag accepted? - virtual bool primarySelectStartDrag (const QPoint& pos); + /// Default-implementation: ignore and return false + /// + /// \return Drag accepted? + virtual bool primarySelectStartDrag(const QPoint& pos); - /// Default-implementation: ignore and return false - /// - /// \return Drag accepted? - virtual bool secondarySelectStartDrag (const QPoint& pos); + /// Default-implementation: ignore and return false + /// + /// \return Drag accepted? + virtual bool secondarySelectStartDrag(const QPoint& pos); - /// Default-implementation: ignored - virtual void drag (const QPoint& pos, int diffX, int diffY, double speedFactor); + /// Default-implementation: ignored + virtual void drag(const QPoint& pos, int diffX, int diffY, double speedFactor); - /// Default-implementation: ignored - virtual void dragCompleted(const QPoint& pos); + /// Default-implementation: ignored + virtual void dragCompleted(const QPoint& pos); - /// Default-implementation: ignored - /// - /// \note dragAborted will not be called, if the drag is aborted via changing - /// editing mode - virtual void dragAborted(); + /// Default-implementation: ignored + /// + /// \note dragAborted will not be called, if the drag is aborted via changing + /// editing mode + virtual void dragAborted(); - /// Default-implementation: ignored - virtual void dragWheel (int diff, double speedFactor); + /// Default-implementation: ignored + virtual void dragWheel(int diff, double speedFactor); - /// Default-implementation: ignored - void dragEnterEvent (QDragEnterEvent *event) override; + /// Default-implementation: ignored + void dragEnterEvent(QDragEnterEvent* event) override; - /// Default-implementation: ignored - void dropEvent (QDropEvent *event) override; + /// Default-implementation: ignored + void dropEvent(QDropEvent* event) override; - /// Default-implementation: ignored - void dragMoveEvent (QDragMoveEvent *event) override; + /// Default-implementation: ignored + void dragMoveEvent(QDragMoveEvent* event) override; - void mouseMoveEvent (QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent* event) override; - /// Default: return -1 - virtual int getSubMode() const; + /// Default: return -1 + virtual int getSubMode() const; }; } diff --git a/apps/opencs/view/render/instancedragmodes.hpp b/apps/opencs/view/render/instancedragmodes.hpp index 01547545ae4..2629a9d2f99 100644 --- a/apps/opencs/view/render/instancedragmodes.hpp +++ b/apps/opencs/view/render/instancedragmodes.hpp @@ -12,7 +12,10 @@ namespace CSVRender DragMode_Select_Only, DragMode_Select_Add, DragMode_Select_Remove, - DragMode_Select_Invert + DragMode_Select_Invert, + DragMode_Move_Snap, + DragMode_Rotate_Snap, + DragMode_Scale_Snap }; } #endif diff --git a/apps/opencs/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index 99ddce7f7de..58513110355 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -1,37 +1,68 @@ - #include "instancemode.hpp" #include #include #include +#include +#include +#include +#include +#include +#include + #include "../../model/prefs/state.hpp" +#include +#include #include #include +#include +#include +#include #include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../../model/prefs/shortcut.hpp" +#include "../../model/world/commandmacro.hpp" +#include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" -#include "../../model/world/commands.hpp" -#include "../../model/world/commandmacro.hpp" -#include "../../model/prefs/shortcut.hpp" +#include "../../model/world/tablemimedata.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" #include "mask.hpp" +#include "instancemovemode.hpp" +#include "instanceselectionmode.hpp" #include "object.hpp" -#include "worldspacewidget.hpp" #include "pagedworldspacewidget.hpp" -#include "instanceselectionmode.hpp" -#include "instancemovemode.hpp" +#include "worldspacewidget.hpp" -int CSVRender::InstanceMode::getSubModeFromId (const std::string& id) const +int CSVRender::InstanceMode::getSubModeFromId(const std::string& id) const { - return id=="move" ? 0 : (id=="rotate" ? 1 : 2); + return id == "move" ? 0 : (id == "rotate" ? 1 : 2); } osg::Vec3f CSVRender::InstanceMode::quatToEuler(const osg::Quat& rot) const @@ -57,21 +88,54 @@ osg::Vec3f CSVRender::InstanceMode::quatToEuler(const osg::Quat& rot) const osg::Quat CSVRender::InstanceMode::eulerToQuat(const osg::Vec3f& euler) const { - osg::Quat xr = osg::Quat(-euler[0], osg::Vec3f(1,0,0)); - osg::Quat yr = osg::Quat(-euler[1], osg::Vec3f(0,1,0)); - osg::Quat zr = osg::Quat(-euler[2], osg::Vec3f(0,0,1)); + osg::Quat xr = osg::Quat(-euler[0], osg::Vec3f(1, 0, 0)); + osg::Quat yr = osg::Quat(-euler[1], osg::Vec3f(0, 1, 0)); + osg::Quat zr = osg::Quat(-euler[2], osg::Vec3f(0, 0, 1)); return zr * yr * xr; } -osg::Vec3f CSVRender::InstanceMode::getSelectionCenter(const std::vector >& selection) const +float CSVRender::InstanceMode::roundFloatToMult(const float val, const double mult) const +{ + if (mult == 0) + return val; + return round(val / mult) * mult; +} + +osg::Vec3 CSVRender::InstanceMode::calculateSnapPositionRelativeToTarget(osg::Vec3 initalPosition, + osg::Vec3 targetPosition, osg::Vec3 targetRotation, osg::Vec3 translation, double snap) const +{ + auto quatTargetRotation + = osg::Quat(targetRotation[0], osg::X_AXIS, targetRotation[1], osg::Y_AXIS, targetRotation[2], osg::Z_AXIS); + + // Break object world coords into snap target space + auto localWorld = osg::Matrix::translate(initalPosition) + * osg::Matrix::inverse(osg::Matrix::translate(targetPosition)) * osg::Matrix::rotate(quatTargetRotation); + + osg::Vec3 localPosition = localWorld.getTrans(); + + osg::Vec3 newTranslation; + newTranslation[0] = CSVRender::InstanceMode::roundFloatToMult(localPosition[0] + translation[0], snap); + newTranslation[1] = CSVRender::InstanceMode::roundFloatToMult(localPosition[1] + translation[1], snap); + newTranslation[2] = CSVRender::InstanceMode::roundFloatToMult(localPosition[2] + translation[2], snap); + + // rebuild object's world coordinates (note: inverse operations from local construction) + auto newObjectWorld = osg::Matrix::translate(newTranslation) + * osg::Matrix::inverse(osg::Matrix::rotate(quatTargetRotation)) * osg::Matrix::translate(targetPosition); + + osg::Vec3 newObjectPosition = newObjectWorld.getTrans(); + + return newObjectPosition; +} + +osg::Vec3f CSVRender::InstanceMode::getSelectionCenter(const std::vector>& selection) const { osg::Vec3f center = osg::Vec3f(0, 0, 0); int objectCount = 0; - for (std::vector >::const_iterator iter (selection.begin()); iter!=selection.end(); ++iter) + for (std::vector>::const_iterator iter(selection.begin()); iter != selection.end(); ++iter) { - if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(iter->get())) { const ESM::Position& position = objectTag->mObject->getPosition(); center += osg::Vec3f(position.pos[0], position.pos[1], position.pos[2]); @@ -123,93 +187,244 @@ osg::Vec3f CSVRender::InstanceMode::getMousePlaneCoords(const QPoint& point, con return mousePlanePoint; } -CSVRender::InstanceMode::InstanceMode (WorldspaceWidget *worldspaceWidget, osg::ref_ptr parentNode, QWidget *parent) -: EditMode (worldspaceWidget, QIcon (":scenetoolbar/editing-instance"), Mask_Reference | Mask_Terrain, "Instance editing", - parent), mSubMode (nullptr), mSubModeId ("move"), mSelectionMode (nullptr), mDragMode (DragMode_None), - mDragAxis (-1), mLocked (false), mUnitScaleDist(1), mParentNode (parentNode) +void CSVRender::InstanceMode::saveSelectionGroup(const int group) +{ + QStringList strings; + QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); + QVariant selectionObjects; + CSMWorld::CommandMacro macro(undoStack, "Replace Selection Group"); + std::string groupName = "project::" + std::to_string(group); + + const auto& selection = getWorldspaceWidget().getSelection(Mask_Reference); + const int selectionObjectsIndex + = mSelectionGroups->findColumnIndex(CSMWorld::Columns::ColumnId_SelectionGroupObjects); + + if (dynamic_cast(&getWorldspaceWidget())) + groupName += "-ext"; + else + groupName += "-" + getWorldspaceWidget().getCellId(osg::Vec3f(0, 0, 0)); + + CSMWorld::CreateCommand* newGroup = new CSMWorld::CreateCommand(*mSelectionGroups, groupName); + + newGroup->setType(CSMWorld::UniversalId::Type_SelectionGroup); + + for (const auto& object : selection) + if (const CSVRender::ObjectTag* objectTag = dynamic_cast(object.get())) + strings << QString::fromStdString(objectTag->mObject->getReferenceId()); + + selectionObjects.setValue(strings); + + newGroup->addValue(selectionObjectsIndex, selectionObjects); + + if (mSelectionGroups->getModelIndex(groupName, 0).row() != -1) + macro.push(new CSMWorld::DeleteCommand(*mSelectionGroups, groupName)); + + macro.push(newGroup); + + getWorldspaceWidget().clearSelection(Mask_Reference); +} + +void CSVRender::InstanceMode::getSelectionGroup(const int group) +{ + std::string groupName = "project::" + std::to_string(group); + std::vector targets; + + const auto& selection = getWorldspaceWidget().getSelection(Mask_Reference); + const int selectionObjectsIndex + = mSelectionGroups->findColumnIndex(CSMWorld::Columns::ColumnId_SelectionGroupObjects); + + if (dynamic_cast(&getWorldspaceWidget())) + groupName += "-ext"; + else + groupName += "-" + getWorldspaceWidget().getCellId(osg::Vec3f(0, 0, 0)); + + const QModelIndex groupSearch = mSelectionGroups->getModelIndex(groupName, selectionObjectsIndex); + + if (groupSearch.row() == -1) + return; + + for (const QString& target : groupSearch.data().toStringList()) + targets.push_back(target.toStdString()); + + if (!selection.empty()) + getWorldspaceWidget().clearSelection(Mask_Reference); + + getWorldspaceWidget().selectGroup(targets); +} + +void CSVRender::InstanceMode::setDragAxis(const char axis) +{ + int newDragAxis; + + const std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); + + if (selection.empty()) + return; + + switch (axis) + { + case 'x': + newDragAxis = 0; + break; + case 'y': + newDragAxis = 1; + break; + case 'z': + newDragAxis = 2; + break; + default: + return; + } + + if (newDragAxis == mDragAxis) + newDragAxis = -1; + + if (mSubModeId == "move") + { + mObjectsAtDragStart.clear(); + + for (const auto& object : selection) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(object.get())) + { + const osg::Vec3f thisPoint = objectTag->mObject->getPosition().asVec3(); + mDragStart = thisPoint; + mObjectsAtDragStart.emplace_back(thisPoint); + } + } + mDragAxis = newDragAxis; +} + +CSVRender::InstanceMode::InstanceMode( + WorldspaceWidget* worldspaceWidget, osg::ref_ptr parentNode, QWidget* parent) + : EditMode(worldspaceWidget, Misc::ScalableIcon::load(":scenetoolbar/editing-instance"), + Mask_Reference | Mask_Terrain, "Instance editing", parent) + , mSubMode(nullptr) + , mSubModeId("move") + , mSelectionMode(nullptr) + , mDragMode(DragMode_None) + , mDragAxis(-1) + , mLocked(false) + , mUnitScaleDist(1) + , mParentNode(std::move(parentNode)) { - connect(this, SIGNAL(requestFocus(const std::string&)), - worldspaceWidget, SIGNAL(requestFocus(const std::string&))); + mSelectionGroups = dynamic_cast( + worldspaceWidget->getDocument().getData().getTableModel(CSMWorld::UniversalId::Type_SelectionGroup)); + + connect(this, &InstanceMode::requestFocus, worldspaceWidget, &WorldspaceWidget::requestFocus); CSMPrefs::Shortcut* deleteShortcut = new CSMPrefs::Shortcut("scene-delete", worldspaceWidget); - connect(deleteShortcut, SIGNAL(activated(bool)), this, SLOT(deleteSelectedInstances(bool))); + connect(deleteShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &InstanceMode::deleteSelectedInstances); + + CSMPrefs::Shortcut* duplicateShortcut = new CSMPrefs::Shortcut("scene-duplicate", worldspaceWidget); + + connect( + duplicateShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &InstanceMode::cloneSelectedInstances); + + // Following classes could be simplified by using QSignalMapper, which is obsolete in Qt5.10, but not in Qt4.8 and + // Qt5.14 + CSMPrefs::Shortcut* dropToCollisionShortcut + = new CSMPrefs::Shortcut("scene-instance-drop-collision", worldspaceWidget); + + connect(dropToCollisionShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, + &InstanceMode::dropSelectedInstancesToCollision); + + CSMPrefs::Shortcut* dropToTerrainLevelShortcut + = new CSMPrefs::Shortcut("scene-instance-drop-terrain", worldspaceWidget); + connect(dropToTerrainLevelShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, + &InstanceMode::dropSelectedInstancesToTerrain); + CSMPrefs::Shortcut* dropToCollisionShortcut2 + = new CSMPrefs::Shortcut("scene-instance-drop-collision-separately", worldspaceWidget); + connect(dropToCollisionShortcut2, qOverload<>(&CSMPrefs::Shortcut::activated), this, + &InstanceMode::dropSelectedInstancesToCollisionSeparately); + CSMPrefs::Shortcut* dropToTerrainLevelShortcut2 + = new CSMPrefs::Shortcut("scene-instance-drop-terrain-separately", worldspaceWidget); + connect(dropToTerrainLevelShortcut2, qOverload<>(&CSMPrefs::Shortcut::activated), this, + &InstanceMode::dropSelectedInstancesToTerrainSeparately); + + for (short i = 0; i <= 9; i++) + { + connect(new CSMPrefs::Shortcut("scene-group-" + std::to_string(i), worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this, i] { this->getSelectionGroup(i); }); + connect(new CSMPrefs::Shortcut("scene-save-" + std::to_string(i), worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this, i] { this->saveSelectionGroup(i); }); + } - // Following classes could be simplified by using QSignalMapper, which is obsolete in Qt5.10, but not in Qt4.8 and Qt5.14 - CSMPrefs::Shortcut* dropToCollisionShortcut = new CSMPrefs::Shortcut("scene-instance-drop-collision", worldspaceWidget); - connect(dropToCollisionShortcut, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToCollision())); - CSMPrefs::Shortcut* dropToTerrainLevelShortcut = new CSMPrefs::Shortcut("scene-instance-drop-terrain", worldspaceWidget); - connect(dropToTerrainLevelShortcut, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToTerrain())); - CSMPrefs::Shortcut* dropToCollisionShortcut2 = new CSMPrefs::Shortcut("scene-instance-drop-collision-separately", worldspaceWidget); - connect(dropToCollisionShortcut2, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToCollisionSeparately())); - CSMPrefs::Shortcut* dropToTerrainLevelShortcut2 = new CSMPrefs::Shortcut("scene-instance-drop-terrain-separately", worldspaceWidget); - connect(dropToTerrainLevelShortcut2, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToTerrainSeparately())); + connect(new CSMPrefs::Shortcut("scene-submode-move", worldspaceWidget), qOverload<>(&CSMPrefs::Shortcut::activated), + this, [this] { mSubMode->setButton("move"); }); + + connect(new CSMPrefs::Shortcut("scene-submode-scale", worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this] { mSubMode->setButton("scale"); }); + + connect(new CSMPrefs::Shortcut("scene-submode-rotate", worldspaceWidget), + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this] { mSubMode->setButton("rotate"); }); + + 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); }); } -void CSVRender::InstanceMode::activate (CSVWidget::SceneToolbar *toolbar) +void CSVRender::InstanceMode::activate(CSVWidget::SceneToolbar* toolbar) { if (!mSubMode) { - mSubMode = new CSVWidget::SceneToolMode (toolbar, "Edit Sub-Mode"); - mSubMode->addButton (new InstanceMoveMode (this), "move"); - mSubMode->addButton (":scenetoolbar/transform-rotate", "rotate", + mSubMode = new CSVWidget::SceneToolMode(toolbar, "Edit Sub-Mode"); + mSubMode->addButton(new InstanceMoveMode(this), "move"); + mSubMode->addButton(":scenetoolbar/transform-rotate", "rotate", "Rotate selected instances" "
  • Use {scene-edit-primary} to rotate instances freely
  • " "
  • Use {scene-edit-secondary} to rotate instances within the grid
  • " "
  • The center of the view acts as the axis of rotation
  • " - "
" - "Grid rotate not implemented yet"); - mSubMode->addButton (":scenetoolbar/transform-scale", "scale", + ""); + mSubMode->addButton(":scenetoolbar/transform-scale", "scale", "Scale selected instances" "
  • Use {scene-edit-primary} to scale instances freely
  • " "
  • Use {scene-edit-secondary} to scale instances along the grid
  • " "
  • The scaling rate is based on how close the start of a drag is to the center of the screen
  • " - "
" - "Grid scale not implemented yet"); + ""); - mSubMode->setButton (mSubModeId); + mSubMode->setButton(mSubModeId); - connect (mSubMode, SIGNAL (modeChanged (const std::string&)), - this, SLOT (subModeChanged (const std::string&))); + connect(mSubMode, &CSVWidget::SceneToolMode::modeChanged, this, &InstanceMode::subModeChanged); } if (!mSelectionMode) - mSelectionMode = new InstanceSelectionMode (toolbar, getWorldspaceWidget(), mParentNode); + mSelectionMode = new InstanceSelectionMode(toolbar, getWorldspaceWidget(), mParentNode); mDragMode = DragMode_None; - EditMode::activate (toolbar); + EditMode::activate(toolbar); - toolbar->addTool (mSubMode); - toolbar->addTool (mSelectionMode); + toolbar->addTool(mSubMode); + toolbar->addTool(mSelectionMode); std::string subMode = mSubMode->getCurrentId(); - getWorldspaceWidget().setSubMode (getSubModeFromId (subMode), Mask_Reference); + getWorldspaceWidget().setSubMode(getSubModeFromId(subMode), Mask_Reference); } -void CSVRender::InstanceMode::deactivate (CSVWidget::SceneToolbar *toolbar) +void CSVRender::InstanceMode::deactivate(CSVWidget::SceneToolbar* toolbar) { mDragMode = DragMode_None; - getWorldspaceWidget().reset (Mask_Reference); + getWorldspaceWidget().reset(Mask_Reference); if (mSelectionMode) { - toolbar->removeTool (mSelectionMode); + toolbar->removeTool(mSelectionMode); delete mSelectionMode; mSelectionMode = nullptr; } if (mSubMode) { - toolbar->removeTool (mSubMode); + toolbar->removeTool(mSubMode); delete mSubMode; mSubMode = nullptr; } - EditMode::deactivate (toolbar); + EditMode::deactivate(toolbar); } -void CSVRender::InstanceMode::setEditLock (bool locked) +void CSVRender::InstanceMode::setEditLock(bool locked) { mLocked = locked; @@ -217,17 +432,17 @@ void CSVRender::InstanceMode::setEditLock (bool locked) getWorldspaceWidget().abortDrag(); } -void CSVRender::InstanceMode::primaryEditPressed (const WorldspaceHitResult& hit) +void CSVRender::InstanceMode::primaryEditPressed(const WorldspaceHitResult& hit) { if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) - primarySelectPressed (hit); + primarySelectPressed(hit); } -void CSVRender::InstanceMode::primaryOpenPressed (const WorldspaceHitResult& hit) +void CSVRender::InstanceMode::primaryOpenPressed(const WorldspaceHitResult& hit) { - if(hit.tag) + if (hit.tag) { - if (CSVRender::ObjectTag *objectTag = dynamic_cast (hit.tag.get())) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) { const std::string refId = objectTag->mObject->getReferenceId(); emit requestFocus(refId); @@ -235,90 +450,118 @@ void CSVRender::InstanceMode::primaryOpenPressed (const WorldspaceHitResult& hit } } -void CSVRender::InstanceMode::secondaryEditPressed (const WorldspaceHitResult& hit) +void CSVRender::InstanceMode::secondaryEditPressed(const WorldspaceHitResult& hit) { if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) - secondarySelectPressed (hit); + secondarySelectPressed(hit); } -void CSVRender::InstanceMode::primarySelectPressed (const WorldspaceHitResult& hit) +void CSVRender::InstanceMode::primarySelectPressed(const WorldspaceHitResult& hit) { - getWorldspaceWidget().clearSelection (Mask_Reference); + getWorldspaceWidget().clearSelection(Mask_Reference); if (hit.tag) { - 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); + object->setSelected(true); + return; + } + } +} + +void CSVRender::InstanceMode::secondarySelectPressed(const WorldspaceHitResult& hit) +{ + if (hit.tag) + { + 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; } } } -void CSVRender::InstanceMode::secondarySelectPressed (const WorldspaceHitResult& hit) +void CSVRender::InstanceMode::tertiarySelectPressed(const WorldspaceHitResult& hit) { + auto* snapTarget = dynamic_cast(getWorldspaceWidget().getSnapTarget(Mask_Reference).get()); + + if (snapTarget) + { + snapTarget->mObject->setSnapTarget(false); + } + if (hit.tag) { - 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()); + object->setSnapTarget(!object->getSnapTarget()); return; } } } -bool CSVRender::InstanceMode::primaryEditStartDrag (const QPoint& pos) +bool CSVRender::InstanceMode::primaryEditStartDrag(const QPoint& pos) { - if (mDragMode!=DragMode_None || mLocked) + if (mDragMode != DragMode_None || mLocked) return false; - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); - std::vector > selection = getWorldspaceWidget().getSelection (Mask_Reference); + std::vector> selection = getWorldspaceWidget().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); - if (CSVRender::ObjectTag *objectTag = dynamic_cast (hit.tag.get())) + getWorldspaceWidget().clearSelection(Mask_Reference); + if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) { CSVRender::Object* object = objectTag->mObject; - object->setSelected (true); + object->setSelected(true); } } - selection = getWorldspaceWidget().getSelection (Mask_Reference); + selection = getWorldspaceWidget().getSelection(Mask_Reference); if (selection.empty()) return false; } - for (std::vector >::iterator iter (selection.begin()); - iter!=selection.end(); ++iter) + mObjectsAtDragStart.clear(); + + for (std::vector>::iterator iter(selection.begin()); iter != selection.end(); ++iter) { - if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(iter->get())) { if (mSubModeId == "move") { - objectTag->mObject->setEdited (Object::Override_Position); + objectTag->mObject->setEdited(Object::Override_Position); + float x = objectTag->mObject->getPosition().pos[0]; + float y = objectTag->mObject->getPosition().pos[1]; + float z = objectTag->mObject->getPosition().pos[2]; + osg::Vec3f thisPoint(x, y, z); + mDragStart = getMousePlaneCoords(pos, getProjectionSpaceCoords(thisPoint)); + mObjectsAtDragStart.emplace_back(thisPoint); mDragMode = DragMode_Move; } else if (mSubModeId == "rotate") { - objectTag->mObject->setEdited (Object::Override_Rotation); + objectTag->mObject->setEdited(Object::Override_Rotation); mDragMode = DragMode_Rotate; } else if (mSubModeId == "scale") { - objectTag->mObject->setEdited (Object::Override_Scale); + objectTag->mObject->setEdited(Object::Override_Scale); mDragMode = DragMode_Scale; // Calculate scale factor - std::vector > editedSelection = getWorldspaceWidget().getEdited (Mask_Reference); + std::vector> editedSelection = getWorldspaceWidget().getEdited(Mask_Reference); osg::Vec3f center = getScreenCoords(getSelectionCenter(editedSelection)); int widgetHeight = getWorldspaceWidget().height(); @@ -331,7 +574,7 @@ bool CSVRender::InstanceMode::primaryEditStartDrag (const QPoint& pos) } } - if (CSVRender::ObjectMarkerTag *objectTag = dynamic_cast (hit.tag.get())) + if (CSVRender::ObjectMarkerTag* objectTag = dynamic_cast(hit.tag.get())) { mDragAxis = objectTag->mAxis; } @@ -341,84 +584,143 @@ bool CSVRender::InstanceMode::primaryEditStartDrag (const QPoint& pos) return true; } -bool CSVRender::InstanceMode::secondaryEditStartDrag (const QPoint& pos) +bool CSVRender::InstanceMode::secondaryEditStartDrag(const QPoint& pos) { - if (mLocked) + if (mDragMode != DragMode_None || mLocked) return false; - return false; + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + + std::vector> selection = getWorldspaceWidget().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); + if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) + { + CSVRender::Object* object = objectTag->mObject; + object->setSelected(true); + } + } + + selection = getWorldspaceWidget().getSelection(Mask_Reference); + if (selection.empty()) + return false; + } + + mObjectsAtDragStart.clear(); + + for (std::vector>::iterator iter(selection.begin()); iter != selection.end(); ++iter) + { + if (CSVRender::ObjectTag* objectTag = dynamic_cast(iter->get())) + { + if (mSubModeId == "move") + { + objectTag->mObject->setEdited(Object::Override_Position); + float x = objectTag->mObject->getPosition().pos[0]; + float y = objectTag->mObject->getPosition().pos[1]; + float z = objectTag->mObject->getPosition().pos[2]; + osg::Vec3f thisPoint(x, y, z); + + mDragStart = getMousePlaneCoords(pos, getProjectionSpaceCoords(thisPoint)); + mObjectsAtDragStart.emplace_back(thisPoint); + mDragMode = DragMode_Move_Snap; + } + else if (mSubModeId == "rotate") + { + objectTag->mObject->setEdited(Object::Override_Rotation); + mDragMode = DragMode_Rotate_Snap; + } + else if (mSubModeId == "scale") + { + objectTag->mObject->setEdited(Object::Override_Scale); + mDragMode = DragMode_Scale_Snap; + + // Calculate scale factor + std::vector> editedSelection = getWorldspaceWidget().getEdited(Mask_Reference); + osg::Vec3f center = getScreenCoords(getSelectionCenter(editedSelection)); + + int widgetHeight = getWorldspaceWidget().height(); + + float dx = pos.x() - center.x(); + float dy = (widgetHeight - pos.y()) - center.y(); + + mUnitScaleDist = std::sqrt(dx * dx + dy * dy); + } + } + } + + if (CSVRender::ObjectMarkerTag* objectTag = dynamic_cast(hit.tag.get())) + { + mDragAxis = objectTag->mAxis; + } + else + mDragAxis = -1; + + return true; } -bool CSVRender::InstanceMode::primarySelectStartDrag (const QPoint& pos) +bool CSVRender::InstanceMode::primarySelectStartDrag(const QPoint& pos) { - if (mDragMode!=DragMode_None || mLocked) + if (mDragMode != DragMode_None || mLocked) return false; std::string primarySelectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString(); - if ( primarySelectAction == "Select only" ) mDragMode = DragMode_Select_Only; - else if ( primarySelectAction == "Add to selection" ) mDragMode = DragMode_Select_Add; - else if ( primarySelectAction == "Remove from selection" ) mDragMode = DragMode_Select_Remove; - else if ( primarySelectAction == "Invert selection" ) mDragMode = DragMode_Select_Invert; + if (primarySelectAction == "Select only") + mDragMode = DragMode_Select_Only; + else if (primarySelectAction == "Add to selection") + mDragMode = DragMode_Select_Add; + else if (primarySelectAction == "Remove from selection") + mDragMode = DragMode_Select_Remove; + else if (primarySelectAction == "Invert selection") + mDragMode = DragMode_Select_Invert; - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); mSelectionMode->setDragStart(hit.worldPos); return true; } -bool CSVRender::InstanceMode::secondarySelectStartDrag (const QPoint& pos) +bool CSVRender::InstanceMode::secondarySelectStartDrag(const QPoint& pos) { - if (mDragMode!=DragMode_None || mLocked) + if (mDragMode != DragMode_None || mLocked) return false; std::string secondarySelectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString(); - if ( secondarySelectAction == "Select only" ) mDragMode = DragMode_Select_Only; - else if ( secondarySelectAction == "Add to selection" ) mDragMode = DragMode_Select_Add; - else if ( secondarySelectAction == "Remove from selection" ) mDragMode = DragMode_Select_Remove; - else if ( secondarySelectAction == "Invert selection" ) mDragMode = DragMode_Select_Invert; + if (secondarySelectAction == "Select only") + mDragMode = DragMode_Select_Only; + else if (secondarySelectAction == "Add to selection") + mDragMode = DragMode_Select_Add; + else if (secondarySelectAction == "Remove from selection") + mDragMode = DragMode_Select_Remove; + else if (secondarySelectAction == "Invert selection") + mDragMode = DragMode_Select_Invert; - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); mSelectionMode->setDragStart(hit.worldPos); return true; } -void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) +void CSVRender::InstanceMode::drag(const QPoint& pos, int diffX, int diffY, double speedFactor) { osg::Vec3f offset; osg::Quat rotation; - std::vector > selection = getWorldspaceWidget().getEdited (Mask_Reference); + std::vector> selection = getWorldspaceWidget().getEdited(Mask_Reference); + auto* snapTarget = dynamic_cast(getWorldspaceWidget().getSnapTarget(Mask_Reference).get()); - if (mDragMode == DragMode_Move) + if (mDragMode == DragMode_Move || mDragMode == DragMode_Move_Snap) { - osg::Vec3f eye, centre, up; - getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt (eye, centre, up); - - if (diffY) - { - offset += up * diffY * speedFactor; - } - if (diffX) - { - offset += ((centre-eye) ^ up) * diffX * speedFactor; - } - - if (mDragAxis!=-1) - { - for (int i=0; i<3; ++i) - { - if (i!=mDragAxis) - offset[i] = 0; - } - } } - else if (mDragMode == DragMode_Rotate) + else if (mDragMode == DragMode_Rotate || mDragMode == DragMode_Rotate_Snap) { osg::Vec3f eye, centre, up; - getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt (eye, centre, up); + getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt(eye, centre, up); float angle; osg::Vec3f axis; @@ -434,7 +736,7 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou osg::Vec3f screenDir = cameraRotation * osg::Vec3f(diffX, diffY, 0); screenDir.normalize(); - angle = std::sqrt(diffX*diffX + diffY*diffY) * rotationFactor; + angle = std::sqrt(diffX * diffX + diffY * diffY) * rotationFactor; axis = screenDir ^ camForward; } else @@ -479,7 +781,7 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou rotation = osg::Quat(angle, axis); } - else if (mDragMode == DragMode_Scale) + else if (mDragMode == DragMode_Scale || mDragMode == DragMode_Scale_Snap) { osg::Vec3f center = getScreenCoords(getSelectionCenter(selection)); @@ -498,38 +800,77 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou else if (mSelectionMode->getCurrentId() == "cube-centre") { osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); - mSelectionMode->drawSelectionCubeCentre (mousePlanePoint); + mSelectionMode->drawSelectionCubeCentre(mousePlanePoint); return; } else if (mSelectionMode->getCurrentId() == "cube-corner") { osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); - mSelectionMode->drawSelectionCubeCorner (mousePlanePoint); + mSelectionMode->drawSelectionCubeCorner(mousePlanePoint); return; } else if (mSelectionMode->getCurrentId() == "sphere") { osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); - mSelectionMode->drawSelectionSphere (mousePlanePoint); + mSelectionMode->drawSelectionSphere(mousePlanePoint); return; } + int i = 0; + // Apply - for (std::vector >::iterator iter (selection.begin()); iter!=selection.end(); ++iter) + for (std::vector>::iterator iter(selection.begin()); iter != selection.end(); ++iter, i++) { - if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(iter->get())) { - if (mDragMode == DragMode_Move) + if (mDragMode == DragMode_Move || mDragMode == DragMode_Move_Snap) { ESM::Position position = objectTag->mObject->getPosition(); - for (int i=0; i<3; ++i) + osg::Vec3f mousePos = getMousePlaneCoords(pos, getProjectionSpaceCoords(mDragStart)); + float addToX = mousePos.x() - mDragStart.x(); + float addToY = mousePos.y() - mDragStart.y(); + float addToZ = mousePos.z() - mDragStart.z(); + position.pos[0] = mObjectsAtDragStart[i].x() + addToX; + position.pos[1] = mObjectsAtDragStart[i].y() + addToY; + position.pos[2] = mObjectsAtDragStart[i].z() + addToZ; + + if (mDragMode == DragMode_Move_Snap) { - position.pos[i] += offset[i]; + double snap = CSMPrefs::get()["3D Scene Editing"]["gridsnap-movement"].toDouble(); + + if (snapTarget) + { + osg::Vec3 translation(addToX, addToY, addToZ); + + auto snapTargetPosition = snapTarget->mObject->getPosition(); + auto newPosition = calculateSnapPositionRelativeToTarget(mObjectsAtDragStart[i], + snapTargetPosition.asVec3(), snapTargetPosition.asRotationVec3(), translation, snap); + + position.pos[0] = newPosition[0]; + position.pos[1] = newPosition[1]; + position.pos[2] = newPosition[2]; + } + else + { + position.pos[0] = CSVRender::InstanceMode::roundFloatToMult(position.pos[0], snap); + position.pos[1] = CSVRender::InstanceMode::roundFloatToMult(position.pos[1], snap); + position.pos[2] = CSVRender::InstanceMode::roundFloatToMult(position.pos[2], snap); + } + } + + // XYZ-locking + if (mDragAxis != -1) + { + for (int j = 0; j < 3; ++j) + { + if (j != mDragAxis) + position.pos[j] = mObjectsAtDragStart[i][j]; + } } objectTag->mObject->setPosition(position.pos); } - else if (mDragMode == DragMode_Rotate) + else if (mDragMode == DragMode_Rotate || mDragMode == DragMode_Rotate_Snap) { ESM::Position position = objectTag->mObject->getPosition(); @@ -547,7 +888,7 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou objectTag->mObject->setRotation(position.rot); } - else if (mDragMode == DragMode_Scale) + else if (mDragMode == DragMode_Scale || mDragMode == DragMode_Scale_Snap) { // Reset scale objectTag->mObject->setEdited(0); @@ -556,7 +897,13 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou float scale = objectTag->mObject->getScale(); scale *= offset.x(); - objectTag->mObject->setScale (scale); + if (mDragMode == DragMode_Scale_Snap) + { + scale = CSVRender::InstanceMode::roundFloatToMult( + scale, CSMPrefs::get()["3D Scene Editing"]["gridsnap-scale"].toDouble()); + } + + objectTag->mObject->setScale(scale); } } } @@ -564,8 +911,9 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou void CSVRender::InstanceMode::dragCompleted(const QPoint& pos) { - std::vector > selection = - getWorldspaceWidget().getEdited (Mask_Reference); + std::vector> selection = getWorldspaceWidget().getEdited(Mask_Reference); + + auto* snapTarget = dynamic_cast(getWorldspaceWidget().getSnapTarget(Mask_Reference).get()); QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); @@ -573,189 +921,263 @@ void CSVRender::InstanceMode::dragCompleted(const QPoint& pos) switch (mDragMode) { - case DragMode_Move: description = "Move Instances"; break; - case DragMode_Rotate: description = "Rotate Instances"; break; - case DragMode_Scale: description = "Scale Instances"; break; - case DragMode_Select_Only : + case DragMode_Move: + description = "Move Instances"; + break; + case DragMode_Rotate: + description = "Rotate Instances"; + break; + case DragMode_Scale: + description = "Scale Instances"; + break; + case DragMode_Select_Only: handleSelectDrag(pos); return; break; - case DragMode_Select_Add : + case DragMode_Select_Add: handleSelectDrag(pos); return; break; - case DragMode_Select_Remove : + case DragMode_Select_Remove: handleSelectDrag(pos); return; break; - case DragMode_Select_Invert : + case DragMode_Select_Invert: handleSelectDrag(pos); return; break; - - case DragMode_None: break; + case DragMode_Move_Snap: + description = "Move Instances"; + break; + case DragMode_Rotate_Snap: + description = "Rotate Instances"; + break; + case DragMode_Scale_Snap: + description = "Scale Instances"; + break; + case DragMode_None: + break; } + CSMWorld::CommandMacro macro(undoStack, description); - CSMWorld::CommandMacro macro (undoStack, description); - - for (std::vector >::iterator iter (selection.begin()); - iter!=selection.end(); ++iter) + // Is this even supposed to be here? + for (std::vector>::iterator iter(selection.begin()); iter != selection.end(); ++iter) { - if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(iter->get())) { - objectTag->mObject->apply (macro); + if (mDragMode == DragMode_Rotate_Snap) + { + ESM::Position position = objectTag->mObject->getPosition(); + double snap = CSMPrefs::get()["3D Scene Editing"]["gridsnap-rotation"].toDouble(); + + float xOffset = 0; + float yOffset = 0; + float zOffset = 0; + + if (snapTarget) + { + auto snapTargetPosition = snapTarget->mObject->getPosition(); + auto rotation = snapTargetPosition.rot; + if (rotation) + { + xOffset = remainder(rotation[0], osg::DegreesToRadians(snap)); + yOffset = remainder(rotation[1], osg::DegreesToRadians(snap)); + zOffset = remainder(rotation[2], osg::DegreesToRadians(snap)); + } + } + + position.rot[0] + = CSVRender::InstanceMode::roundFloatToMult(position.rot[0], osg::DegreesToRadians(snap)) + xOffset; + position.rot[1] + = CSVRender::InstanceMode::roundFloatToMult(position.rot[1], osg::DegreesToRadians(snap)) + yOffset; + position.rot[2] + = CSVRender::InstanceMode::roundFloatToMult(position.rot[2], osg::DegreesToRadians(snap)) + zOffset; + + objectTag->mObject->setRotation(position.rot); + } + + objectTag->mObject->apply(macro); } } + mObjectsAtDragStart.clear(); mDragMode = DragMode_None; } void CSVRender::InstanceMode::dragAborted() { - getWorldspaceWidget().reset (Mask_Reference); + getWorldspaceWidget().reset(Mask_Reference); mDragMode = DragMode_None; } -void CSVRender::InstanceMode::dragWheel (int diff, double speedFactor) +void CSVRender::InstanceMode::dragWheel(int diff, double speedFactor) { - if (mDragMode==DragMode_Move) + if (mDragMode == DragMode_Move || mDragMode == DragMode_Move_Snap) { osg::Vec3f eye; osg::Vec3f centre; osg::Vec3f up; - getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt (eye, centre, up); + getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt(eye, centre, up); osg::Vec3f offset = centre - eye; offset.normalize(); offset *= diff * speedFactor; - std::vector > selection = - getWorldspaceWidget().getEdited (Mask_Reference); + std::vector> selection = getWorldspaceWidget().getEdited(Mask_Reference); + auto snapTarget + = dynamic_cast(getWorldspaceWidget().getSnapTarget(Mask_Reference).get()); + + int j = 0; - for (std::vector >::iterator iter (selection.begin()); - iter!=selection.end(); ++iter) + for (std::vector>::iterator iter(selection.begin()); iter != selection.end(); ++iter, j++) { - if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(iter->get())) { ESM::Position position = objectTag->mObject->getPosition(); - for (int i=0; i<3; ++i) + auto preMovedObjectPosition = position.asVec3(); + for (int i = 0; i < 3; ++i) position.pos[i] += offset[i]; - objectTag->mObject->setPosition (position.pos); + + if (mDragMode == DragMode_Move_Snap) + { + double snap = CSMPrefs::get()["3D Scene Editing"]["gridsnap-movement"].toDouble(); + + if (snapTarget) + { + osg::Vec3 translation(snap, snap, snap); + + auto snapTargetPosition = snapTarget->mObject->getPosition(); + auto newPosition = calculateSnapPositionRelativeToTarget(preMovedObjectPosition, + snapTargetPosition.asVec3(), snapTargetPosition.asRotationVec3(), translation, snap); + + position.pos[0] = newPosition[0]; + position.pos[1] = newPosition[1]; + position.pos[2] = newPosition[2]; + } + else + { + position.pos[0] = CSVRender::InstanceMode::roundFloatToMult(position.pos[0], snap); + position.pos[1] = CSVRender::InstanceMode::roundFloatToMult(position.pos[1], snap); + position.pos[2] = CSVRender::InstanceMode::roundFloatToMult(position.pos[2], snap); + } + } + + objectTag->mObject->setPosition(position.pos); + osg::Vec3f thisPoint(position.pos[0], position.pos[1], position.pos[2]); + mDragStart = getMousePlaneCoords( + getWorldspaceWidget().mapFromGlobal(QCursor::pos()), getProjectionSpaceCoords(thisPoint)); + mObjectsAtDragStart[j] = thisPoint; } } } } -void CSVRender::InstanceMode::dragEnterEvent (QDragEnterEvent *event) +void CSVRender::InstanceMode::dragEnterEvent(QDragEnterEvent* event) { - if (const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData())) + if (const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData())) { - if (!mime->fromDocument (getWorldspaceWidget().getDocument())) + if (!mime->fromDocument(getWorldspaceWidget().getDocument())) return; - if (mime->holdsType (CSMWorld::UniversalId::Type_Referenceable)) + if (mime->holdsType(CSMWorld::UniversalId::Type_Referenceable)) event->accept(); } } -void CSVRender::InstanceMode::dropEvent (QDropEvent* event) +void CSVRender::InstanceMode::dropEvent(QDropEvent* event) { - if (const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData())) + if (const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData())) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - if (!mime->fromDocument (document)) + if (!mime->fromDocument(document)) return; - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (event->pos(), getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit + = getWorldspaceWidget().mousePick(event->pos(), getWorldspaceWidget().getInteractionMask()); - std::string cellId = getWorldspaceWidget().getCellId (hit.worldPos); + std::string cellId = getWorldspaceWidget().getCellId(hit.worldPos); - CSMWorld::IdTree& cellTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + CSMWorld::IdTree& cellTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Cells)); - bool noCell = document.getData().getCells().searchId (cellId)==-1; + const bool noCell = document.getData().getCells().searchId(ESM::RefId::stringRefId(cellId)) == -1; if (noCell) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-drop"].toString(); // target cell does not exist - if (mode=="Discard") + if (mode == "Discard") return; - if (mode=="Create cell and insert") + if (mode == "Create cell and insert") { - std::unique_ptr createCommand ( - new CSMWorld::CreateCommand (cellTable, cellId)); + std::unique_ptr createCommand(new CSMWorld::CreateCommand(cellTable, cellId)); - int parentIndex = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); - int index = cellTable.findNestedColumnIndex (parentIndex, CSMWorld::Columns::ColumnId_Interior); - createCommand->addNestedValue (parentIndex, index, false); + int parentIndex = cellTable.findColumnIndex(CSMWorld::Columns::ColumnId_Cell); + int index = cellTable.findNestedColumnIndex(parentIndex, CSMWorld::Columns::ColumnId_Interior); + createCommand->addNestedValue(parentIndex, index, false); - document.getUndoStack().push (createCommand.release()); + document.getUndoStack().push(createCommand.release()); - if (CSVRender::PagedWorldspaceWidget *paged = - dynamic_cast (&getWorldspaceWidget())) + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); - selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); - paged->setCellSelection (selection); + selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); + paged->setCellSelection(selection); } } } - else if (CSVRender::PagedWorldspaceWidget *paged = - dynamic_cast (&getWorldspaceWidget())) + else if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); - if (!selection.has (CSMWorld::CellCoordinates::fromId (cellId).first)) + if (!selection.has(CSMWorld::CellCoordinates::fromId(cellId).first)) { // target cell exists, but is not shown - std::string mode = - CSMPrefs::get()["3D Scene Editing"]["outside-visible-drop"].toString(); + std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-visible-drop"].toString(); - if (mode=="Discard") + if (mode == "Discard") return; - if (mode=="Show cell and insert") + if (mode == "Show cell and insert") { - selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); - paged->setCellSelection (selection); + selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); + paged->setCellSelection(selection); } } } - CSMWorld::IdTable& referencesTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_References)); + CSMWorld::IdTable& referencesTable = dynamic_cast( + *document.getData().getTableModel(CSMWorld::UniversalId::Type_References)); bool dropped = false; std::vector ids = mime->getData(); - for (std::vector::const_iterator iter (ids.begin()); - iter!=ids.end(); ++iter) - if (mime->isReferencable (iter->getType())) + for (std::vector::const_iterator iter(ids.begin()); iter != ids.end(); ++iter) + if (mime->isReferencable(iter->getType())) { // create reference - std::unique_ptr createCommand ( - new CSMWorld::CreateCommand ( - referencesTable, document.getData().getReferences().getNewId())); - - createCommand->addValue (referencesTable.findColumnIndex ( - CSMWorld::Columns::ColumnId_Cell), QString::fromUtf8 (cellId.c_str())); - createCommand->addValue (referencesTable.findColumnIndex ( - CSMWorld::Columns::ColumnId_PositionXPos), hit.worldPos.x()); - createCommand->addValue (referencesTable.findColumnIndex ( - CSMWorld::Columns::ColumnId_PositionYPos), hit.worldPos.y()); - createCommand->addValue (referencesTable.findColumnIndex ( - CSMWorld::Columns::ColumnId_PositionZPos), hit.worldPos.z()); - createCommand->addValue (referencesTable.findColumnIndex ( - CSMWorld::Columns::ColumnId_ReferenceableId), - QString::fromUtf8 (iter->getId().c_str())); - - document.getUndoStack().push (createCommand.release()); + std::unique_ptr createCommand( + new CSMWorld::CreateCommand(referencesTable, document.getData().getReferences().getNewId())); + + createCommand->addValue(referencesTable.findColumnIndex(CSMWorld::Columns::ColumnId_Cell), + QString::fromUtf8(cellId.c_str())); + createCommand->addValue( + referencesTable.findColumnIndex(CSMWorld::Columns::ColumnId_PositionXPos), hit.worldPos.x()); + createCommand->addValue( + referencesTable.findColumnIndex(CSMWorld::Columns::ColumnId_PositionYPos), hit.worldPos.y()); + createCommand->addValue( + referencesTable.findColumnIndex(CSMWorld::Columns::ColumnId_PositionZPos), hit.worldPos.z()); + createCommand->addValue(referencesTable.findColumnIndex(CSMWorld::Columns::ColumnId_ReferenceableId), + QString::fromUtf8(iter->getId().c_str())); + + document.getUndoStack().push(createCommand.release()); dropped = true; } @@ -767,39 +1189,61 @@ void CSVRender::InstanceMode::dropEvent (QDropEvent* event) int CSVRender::InstanceMode::getSubMode() const { - return mSubMode ? getSubModeFromId (mSubMode->getCurrentId()) : 0; + return mSubMode ? getSubModeFromId(mSubMode->getCurrentId()) : 0; } -void CSVRender::InstanceMode::subModeChanged (const std::string& id) +void CSVRender::InstanceMode::subModeChanged(const std::string& id) { mSubModeId = id; getWorldspaceWidget().abortDrag(); - getWorldspaceWidget().setSubMode (getSubModeFromId (id), Mask_Reference); + getWorldspaceWidget().setSubMode(getSubModeFromId(id), Mask_Reference); } void CSVRender::InstanceMode::handleSelectDrag(const QPoint& pos) { osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); - mSelectionMode->dragEnded (mousePlanePoint, mDragMode); + mSelectionMode->dragEnded(mousePlanePoint, mDragMode); mDragMode = DragMode_None; } -void CSVRender::InstanceMode::deleteSelectedInstances(bool active) +void CSVRender::InstanceMode::deleteSelectedInstances() { - std::vector > selection = getWorldspaceWidget().getSelection (Mask_Reference); - if (selection.empty()) return; + std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); + if (selection.empty()) + return; CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& referencesTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_References)); + CSMWorld::IdTable& referencesTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_References)); QUndoStack& undoStack = document.getUndoStack(); - CSMWorld::CommandMacro macro (undoStack, "Delete Instances"); - for(osg::ref_ptr tag: selection) - if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) + CSMWorld::CommandMacro macro(undoStack, "Delete Instances"); + for (osg::ref_ptr tag : selection) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) macro.push(new CSMWorld::DeleteCommand(referencesTable, objectTag->mObject->getReferenceId())); - getWorldspaceWidget().clearSelection (Mask_Reference); + getWorldspaceWidget().clearSelection(Mask_Reference); +} + +void CSVRender::InstanceMode::cloneSelectedInstances() +{ + std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); + if (selection.empty()) + return; + + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& referencesTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_References)); + QUndoStack& undoStack = document.getUndoStack(); + + CSMWorld::CommandMacro macro(undoStack, "Clone Instances"); + for (osg::ref_ptr tag : selection) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) + { + macro.push(new CSMWorld::CloneCommand(referencesTable, objectTag->mObject->getReferenceId(), + "ref#" + std::to_string(referencesTable.rowCount()), CSMWorld::UniversalId::Type_Reference)); + } + // getWorldspaceWidget().clearSelection(Mask_Reference); } void CSVRender::InstanceMode::dropInstance(CSVRender::Object* object, float dropHeight) @@ -819,8 +1263,8 @@ float CSVRender::InstanceMode::calculateDropHeight(DropMode dropMode, CSVRender: osg::Vec3d end = point; end.z() = std::numeric_limits::lowest(); - osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector( - osgUtil::Intersector::MODEL, start, end) ); + osg::ref_ptr intersector( + new osgUtil::LineSegmentIntersector(osgUtil::Intersector::MODEL, start, end)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); osgUtil::IntersectionVisitor visitor(intersector); @@ -864,18 +1308,18 @@ void CSVRender::InstanceMode::dropSelectedInstancesToTerrainSeparately() void CSVRender::InstanceMode::handleDropMethod(DropMode dropMode, QString commandMsg) { - std::vector > selection = getWorldspaceWidget().getSelection (Mask_Reference); + std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); if (selection.empty()) return; CSMDoc::Document& document = getWorldspaceWidget().getDocument(); QUndoStack& undoStack = document.getUndoStack(); - CSMWorld::CommandMacro macro (undoStack, commandMsg); + CSMWorld::CommandMacro macro(undoStack, commandMsg); DropObjectHeightHandler dropObjectDataHandler(&getWorldspaceWidget()); - if(dropMode & Separate) + if (dropMode & Separate) { int counter = 0; for (osg::ref_ptr tag : selection) @@ -913,10 +1357,10 @@ void CSVRender::InstanceMode::handleDropMethod(DropMode dropMode, QString comman CSVRender::DropObjectHeightHandler::DropObjectHeightHandler(WorldspaceWidget* worldspacewidget) : mWorldspaceWidget(worldspacewidget) { - std::vector > selection = mWorldspaceWidget->getSelection (Mask_Reference); - for(osg::ref_ptr tag: selection) + std::vector> selection = mWorldspaceWidget->getSelection(Mask_Reference); + for (osg::ref_ptr tag : selection) { - if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) { osg::ref_ptr objectNodeWithGUI = objectTag->mObject->getRootNode(); osg::ref_ptr objectNodeWithoutGUI = objectTag->mObject->getBaseNode(); @@ -939,11 +1383,11 @@ CSVRender::DropObjectHeightHandler::DropObjectHeightHandler(WorldspaceWidget* wo CSVRender::DropObjectHeightHandler::~DropObjectHeightHandler() { - std::vector > selection = mWorldspaceWidget->getSelection (Mask_Reference); + std::vector> selection = mWorldspaceWidget->getSelection(Mask_Reference); int counter = 0; - for(osg::ref_ptr tag: selection) + for (osg::ref_ptr tag : selection) { - if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) + if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) { osg::ref_ptr objectNodeWithGUI = objectTag->mObject->getRootNode(); objectNodeWithGUI->setNodeMask(mOldMasks[counter]); diff --git a/apps/opencs/view/render/instancemode.hpp b/apps/opencs/view/render/instancemode.hpp index 73b7fff12ad..45d7c2b6fd1 100644 --- a/apps/opencs/view/render/instancemode.hpp +++ b/apps/opencs/view/render/instancemode.hpp @@ -3,17 +3,30 @@ #include -#include +#include +#include + #include +#include #include -#include +#include +#include #include "editmode.hpp" #include "instancedragmodes.hpp" +#include +#include + +class QDragEnterEvent; +class QDropEvent; +class QObject; +class QPoint; +class QWidget; namespace CSVWidget { class SceneToolMode; + class SceneToolbar; } namespace CSVRender @@ -21,114 +34,129 @@ namespace CSVRender class TagBase; class InstanceSelectionMode; class Object; + class WorldspaceWidget; + struct WorldspaceHitResult; class InstanceMode : public EditMode { - Q_OBJECT + Q_OBJECT + + enum DropMode + { + Separate = 0b1, - enum DropMode - { - Separate = 0b1, + Collision = 0b10, + Terrain = 0b100, - Collision = 0b10, - Terrain = 0b100, + CollisionSep = Collision | Separate, + TerrainSep = Terrain | Separate, + }; - CollisionSep = Collision | Separate, - TerrainSep = Terrain | Separate, - }; + CSVWidget::SceneToolMode* mSubMode; + std::string mSubModeId; + InstanceSelectionMode* mSelectionMode; + DragMode mDragMode; + int mDragAxis; + bool mLocked; + float mUnitScaleDist; + osg::ref_ptr mParentNode; + osg::Vec3 mDragStart; + std::vector mObjectsAtDragStart; + CSMWorld::IdTable* mSelectionGroups; - CSVWidget::SceneToolMode *mSubMode; - std::string mSubModeId; - InstanceSelectionMode *mSelectionMode; - DragMode mDragMode; - int mDragAxis; - bool mLocked; - float mUnitScaleDist; - osg::ref_ptr mParentNode; + int getSubModeFromId(const std::string& id) const; - int getSubModeFromId (const std::string& id) const; + osg::Vec3 quatToEuler(const osg::Quat& quat) const; + osg::Quat eulerToQuat(const osg::Vec3& euler) const; - osg::Vec3f quatToEuler(const osg::Quat& quat) const; - osg::Quat eulerToQuat(const osg::Vec3f& euler) const; + float roundFloatToMult(const float val, const double mult) const; - osg::Vec3f getSelectionCenter(const std::vector >& selection) const; - osg::Vec3f getScreenCoords(const osg::Vec3f& pos); - osg::Vec3f getProjectionSpaceCoords(const osg::Vec3f& pos); - osg::Vec3f getMousePlaneCoords(const QPoint& point, const osg::Vec3d& dragStart); - void handleSelectDrag(const QPoint& pos); - void dropInstance(CSVRender::Object* object, float dropHeight); - float calculateDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight); + osg::Vec3 getSelectionCenter(const std::vector>& selection) const; + osg::Vec3 getScreenCoords(const osg::Vec3& pos); + osg::Vec3 getProjectionSpaceCoords(const osg::Vec3& pos); + osg::Vec3 getMousePlaneCoords(const QPoint& point, const osg::Vec3d& dragStart); + void handleSelectDrag(const QPoint& pos); + void dropInstance(CSVRender::Object* object, float dropHeight); + float calculateDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight); + osg::Vec3 calculateSnapPositionRelativeToTarget(osg::Vec3 initalPosition, osg::Vec3 targetPosition, + osg::Vec3 targetRotation, osg::Vec3 translation, double snap) const; - public: + public: + InstanceMode( + WorldspaceWidget* worldspaceWidget, osg::ref_ptr parentNode, QWidget* parent = nullptr); - InstanceMode (WorldspaceWidget *worldspaceWidget, osg::ref_ptr parentNode, QWidget *parent = nullptr); + void activate(CSVWidget::SceneToolbar* toolbar) override; - void activate (CSVWidget::SceneToolbar *toolbar) override; + void deactivate(CSVWidget::SceneToolbar* toolbar) override; - void deactivate (CSVWidget::SceneToolbar *toolbar) override; + void setEditLock(bool locked) override; - void setEditLock (bool locked) override; + void primaryOpenPressed(const WorldspaceHitResult& hit) override; - void primaryOpenPressed (const WorldspaceHitResult& hit) override; + void primaryEditPressed(const WorldspaceHitResult& hit) override; - void primaryEditPressed (const WorldspaceHitResult& hit) override; + void secondaryEditPressed(const WorldspaceHitResult& hit) override; - void secondaryEditPressed (const WorldspaceHitResult& hit) override; + void primarySelectPressed(const WorldspaceHitResult& hit) override; - void primarySelectPressed (const WorldspaceHitResult& hit) override; + void secondarySelectPressed(const WorldspaceHitResult& hit) override; - void secondarySelectPressed (const WorldspaceHitResult& hit) override; + void tertiarySelectPressed(const WorldspaceHitResult& hit) override; - bool primaryEditStartDrag (const QPoint& pos) override; + bool primaryEditStartDrag(const QPoint& pos) override; - bool secondaryEditStartDrag (const QPoint& pos) override; + bool secondaryEditStartDrag(const QPoint& pos) override; - bool primarySelectStartDrag(const QPoint& pos) override; + bool primarySelectStartDrag(const QPoint& pos) override; - bool secondarySelectStartDrag(const QPoint& pos) override; + bool secondarySelectStartDrag(const QPoint& pos) override; - void drag (const QPoint& pos, int diffX, int diffY, double speedFactor) override; + void drag(const QPoint& pos, int diffX, int diffY, double speedFactor) override; - void dragCompleted(const QPoint& pos) override; + void dragCompleted(const QPoint& pos) override; - /// \note dragAborted will not be called, if the drag is aborted via changing - /// editing mode - void dragAborted() override; + /// \note dragAborted will not be called, if the drag is aborted via changing + /// editing mode + void dragAborted() override; - void dragWheel (int diff, double speedFactor) override; + void dragWheel(int diff, double speedFactor) override; - void dragEnterEvent (QDragEnterEvent *event) override; + void dragEnterEvent(QDragEnterEvent* event) override; - void dropEvent (QDropEvent *event) override; + void dropEvent(QDropEvent* event) override; - int getSubMode() const override; + int getSubMode() const override; - signals: + signals: - void requestFocus (const std::string& id); + void requestFocus(const std::string& id); - private slots: + private slots: - void subModeChanged (const std::string& id); - void deleteSelectedInstances(bool active); - void dropSelectedInstancesToCollision(); - void dropSelectedInstancesToTerrain(); - void dropSelectedInstancesToCollisionSeparately(); - void dropSelectedInstancesToTerrainSeparately(); - void handleDropMethod(DropMode dropMode, QString commandMsg); + void setDragAxis(const char axis); + void subModeChanged(const std::string& id); + void deleteSelectedInstances(); + void cloneSelectedInstances(); + void getSelectionGroup(const int group); + void saveSelectionGroup(const int group); + void dropSelectedInstancesToCollision(); + void dropSelectedInstancesToTerrain(); + void dropSelectedInstancesToCollisionSeparately(); + void dropSelectedInstancesToTerrainSeparately(); + void handleDropMethod(DropMode dropMode, QString commandMsg); }; /// \brief Helper class to handle object mask data in safe way class DropObjectHeightHandler { - public: - DropObjectHeightHandler(WorldspaceWidget* worldspacewidget); - ~DropObjectHeightHandler(); - std::vector mObjectHeights; - - private: - WorldspaceWidget* mWorldspaceWidget; - std::vector mOldMasks; + public: + DropObjectHeightHandler(WorldspaceWidget* worldspacewidget); + ~DropObjectHeightHandler(); + std::vector mObjectHeights; + + private: + WorldspaceWidget* mWorldspaceWidget; + std::vector mOldMasks; }; } diff --git a/apps/opencs/view/render/instancemovemode.cpp b/apps/opencs/view/render/instancemovemode.cpp index 723af811de9..bf0ef9d181e 100644 --- a/apps/opencs/view/render/instancemovemode.cpp +++ b/apps/opencs/view/render/instancemovemode.cpp @@ -1,12 +1,20 @@ - #include "instancemovemode.hpp" -CSVRender::InstanceMoveMode::InstanceMoveMode (QWidget *parent) -: ModeButton (QIcon (QPixmap (":scenetoolbar/transform-move")), - "Move selected instances" - "
  • Use {scene-edit-primary} to move instances around freely
  • " - "
  • Use {scene-edit-secondary} to move instances around within the grid
  • " - "
" - "Grid move not implemented yet", - parent) -{} +#include +#include + +#include + +#include + +class QWidget; + +CSVRender::InstanceMoveMode::InstanceMoveMode(QWidget* parent) + : ModeButton(Misc::ScalableIcon::load(":scenetoolbar/transform-move"), + "Move selected instances" + "
  • Use {scene-edit-primary} to move instances around freely
  • " + "
  • Use {scene-edit-secondary} to move instances around within the grid
  • " + "
", + parent) +{ +} diff --git a/apps/opencs/view/render/instancemovemode.hpp b/apps/opencs/view/render/instancemovemode.hpp index 62e6b6a1f8d..fd7ed9d3f83 100644 --- a/apps/opencs/view/render/instancemovemode.hpp +++ b/apps/opencs/view/render/instancemovemode.hpp @@ -7,11 +7,10 @@ namespace CSVRender { class InstanceMoveMode : public CSVWidget::ModeButton { - Q_OBJECT + Q_OBJECT - public: - - InstanceMoveMode (QWidget *parent = nullptr); + public: + InstanceMoveMode(QWidget* parent = nullptr); }; } diff --git a/apps/opencs/view/render/instanceselectionmode.cpp b/apps/opencs/view/render/instanceselectionmode.cpp index 9b5fb759cc0..d3e2379640c 100644 --- a/apps/opencs/view/render/instanceselectionmode.cpp +++ b/apps/opencs/view/render/instanceselectionmode.cpp @@ -1,36 +1,65 @@ #include "instanceselectionmode.hpp" -#include -#include -#include - +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include #include +#include #include -#include +#include +#include +#include #include +#include +#include +#include + +#include +#include +#include -#include "../../model/world/idtable.hpp" #include "../../model/world/commands.hpp" +#include "../../model/world/idtable.hpp" #include "instancedragmodes.hpp" -#include "worldspacewidget.hpp" #include "object.hpp" +#include "worldspacewidget.hpp" + +namespace CSVWidget +{ + class SceneToolbar; +} namespace CSVRender { - InstanceSelectionMode::InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, osg::Group *cellNode) - : SelectionMode(parent, worldspaceWidget, Mask_Reference), mParentNode(cellNode) + class TagBase; + + InstanceSelectionMode::InstanceSelectionMode( + CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, osg::Group* cellNode) + : SelectionMode(parent, worldspaceWidget, Mask_Reference) + , mParentNode(cellNode) { mSelectSame = new QAction("Extend selection to instances with same object ID", this); mDeleteSelection = new QAction("Delete selected instances", this); - connect(mSelectSame, SIGNAL(triggered()), this, SLOT(selectSame())); - connect(mDeleteSelection, SIGNAL(triggered()), this, SLOT(deleteSelection())); + connect(mSelectSame, &QAction::triggered, this, &InstanceSelectionMode::selectSame); + connect(mDeleteSelection, &QAction::triggered, this, &InstanceSelectionMode::deleteSelection); } InstanceSelectionMode::~InstanceSelectionMode() { - mParentNode->removeChild(mBaseNode); + if (mBaseNode) + mParentNode->removeChild(mBaseNode); } void InstanceSelectionMode::setDragStart(const osg::Vec3d& dragStart) @@ -46,7 +75,8 @@ namespace CSVRender void InstanceSelectionMode::dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode) { float dragDistance = (mDragStart - dragEndPoint).length(); - if (mBaseNode) mParentNode->removeChild (mBaseNode); + if (mBaseNode) + mParentNode->removeChild(mBaseNode); if (getCurrentId() == "cube-centre") { osg::Vec3d pointA(mDragStart[0] - dragDistance, mDragStart[1] - dragDistance, mDragStart[2] - dragDistance); @@ -76,196 +106,198 @@ namespace CSVRender void InstanceSelectionMode::drawSelectionBox(const osg::Vec3d& pointA, const osg::Vec3d& pointB) { - if (mBaseNode) mParentNode->removeChild (mBaseNode); + if (mBaseNode) + mParentNode->removeChild(mBaseNode); mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->setPosition(pointA); - osg::ref_ptr geometry (new osg::Geometry); + osg::ref_ptr geometry(new osg::Geometry); - osg::Vec3Array *vertices = new osg::Vec3Array; - vertices->push_back (osg::Vec3f (0.0f, 0.0f, 0.0f)); - vertices->push_back (osg::Vec3f (0.0f, 0.0f, pointB[2] - pointA[2])); - vertices->push_back (osg::Vec3f (0.0f, pointB[1] - pointA[1], 0.0f)); - vertices->push_back (osg::Vec3f (0.0f, pointB[1] - pointA[1], pointB[2] - pointA[2])); + osg::Vec3Array* vertices = new osg::Vec3Array; + vertices->push_back(osg::Vec3f(0.0f, 0.0f, 0.0f)); + vertices->push_back(osg::Vec3f(0.0f, 0.0f, pointB[2] - pointA[2])); + vertices->push_back(osg::Vec3f(0.0f, pointB[1] - pointA[1], 0.0f)); + vertices->push_back(osg::Vec3f(0.0f, pointB[1] - pointA[1], pointB[2] - pointA[2])); - vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], 0.0f, 0.0f)); - vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], 0.0f, pointB[2] - pointA[2])); - vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], pointB[1] - pointA[1], 0.0f)); - vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], pointB[1] - pointA[1], pointB[2] - pointA[2])); + vertices->push_back(osg::Vec3f(pointB[0] - pointA[0], 0.0f, 0.0f)); + vertices->push_back(osg::Vec3f(pointB[0] - pointA[0], 0.0f, pointB[2] - pointA[2])); + vertices->push_back(osg::Vec3f(pointB[0] - pointA[0], pointB[1] - pointA[1], 0.0f)); + vertices->push_back(osg::Vec3f(pointB[0] - pointA[0], pointB[1] - pointA[1], pointB[2] - pointA[2])); - geometry->setVertexArray (vertices); + geometry->setVertexArray(vertices); - osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); + osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, 0); // top - primitives->push_back (2); - primitives->push_back (1); - primitives->push_back (0); + primitives->push_back(2); + primitives->push_back(1); + primitives->push_back(0); - primitives->push_back (3); - primitives->push_back (1); - primitives->push_back (2); + primitives->push_back(3); + primitives->push_back(1); + primitives->push_back(2); // bottom - primitives->push_back (4); - primitives->push_back (5); - primitives->push_back (6); + primitives->push_back(4); + primitives->push_back(5); + primitives->push_back(6); - primitives->push_back (6); - primitives->push_back (5); - primitives->push_back (7); + primitives->push_back(6); + primitives->push_back(5); + primitives->push_back(7); // sides - primitives->push_back (1); - primitives->push_back (4); - primitives->push_back (0); + primitives->push_back(1); + primitives->push_back(4); + primitives->push_back(0); - primitives->push_back (4); - primitives->push_back (1); - primitives->push_back (5); + primitives->push_back(4); + primitives->push_back(1); + primitives->push_back(5); - primitives->push_back (4); - primitives->push_back (2); - primitives->push_back (0); + primitives->push_back(4); + primitives->push_back(2); + primitives->push_back(0); - primitives->push_back (6); - primitives->push_back (2); - primitives->push_back (4); + primitives->push_back(6); + primitives->push_back(2); + primitives->push_back(4); - primitives->push_back (6); - primitives->push_back (3); - primitives->push_back (2); + primitives->push_back(6); + primitives->push_back(3); + primitives->push_back(2); - primitives->push_back (7); - primitives->push_back (3); - primitives->push_back (6); + primitives->push_back(7); + primitives->push_back(3); + primitives->push_back(6); - primitives->push_back (1); - primitives->push_back (3); - primitives->push_back (5); + primitives->push_back(1); + primitives->push_back(3); + primitives->push_back(5); - primitives->push_back (5); - primitives->push_back (3); - primitives->push_back (7); + primitives->push_back(5); + primitives->push_back(3); + primitives->push_back(7); - geometry->addPrimitiveSet (primitives); + geometry->addPrimitiveSet(primitives); - osg::Vec4Array *colours = new osg::Vec4Array; + osg::Vec4Array* colours = new osg::Vec4Array; - colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.5f, 0.2f)); - colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); - colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); - colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); - colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); - colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); - colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); - colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.5f, 0.2f)); + colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f)); - geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); + geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); - geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); - geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON); + geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - mBaseNode->addChild (geometry); + mBaseNode->addChild(geometry); mParentNode->addChild(mBaseNode); } void InstanceSelectionMode::drawSelectionCube(const osg::Vec3d& point, float radius) { - if (mBaseNode) mParentNode->removeChild (mBaseNode); + if (mBaseNode) + mParentNode->removeChild(mBaseNode); mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->setPosition(point); - osg::ref_ptr geometry (new osg::Geometry); + osg::ref_ptr geometry(new osg::Geometry); - osg::Vec3Array *vertices = new osg::Vec3Array; + osg::Vec3Array* vertices = new osg::Vec3Array; for (int i = 0; i < 2; ++i) { float height = i ? -radius : radius; - vertices->push_back (osg::Vec3f (height, -radius, -radius)); - vertices->push_back (osg::Vec3f (height, -radius, radius)); - vertices->push_back (osg::Vec3f (height, radius, -radius)); - vertices->push_back (osg::Vec3f (height, radius, radius)); + vertices->push_back(osg::Vec3f(height, -radius, -radius)); + vertices->push_back(osg::Vec3f(height, -radius, radius)); + vertices->push_back(osg::Vec3f(height, radius, -radius)); + vertices->push_back(osg::Vec3f(height, radius, radius)); } - geometry->setVertexArray (vertices); + geometry->setVertexArray(vertices); - osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); + osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, 0); // top - primitives->push_back (2); - primitives->push_back (1); - primitives->push_back (0); + primitives->push_back(2); + primitives->push_back(1); + primitives->push_back(0); - primitives->push_back (3); - primitives->push_back (1); - primitives->push_back (2); + primitives->push_back(3); + primitives->push_back(1); + primitives->push_back(2); // bottom - primitives->push_back (4); - primitives->push_back (5); - primitives->push_back (6); + primitives->push_back(4); + primitives->push_back(5); + primitives->push_back(6); - primitives->push_back (6); - primitives->push_back (5); - primitives->push_back (7); + primitives->push_back(6); + primitives->push_back(5); + primitives->push_back(7); // sides - primitives->push_back (1); - primitives->push_back (4); - primitives->push_back (0); + primitives->push_back(1); + primitives->push_back(4); + primitives->push_back(0); - primitives->push_back (4); - primitives->push_back (1); - primitives->push_back (5); + primitives->push_back(4); + primitives->push_back(1); + primitives->push_back(5); - primitives->push_back (4); - primitives->push_back (2); - primitives->push_back (0); + primitives->push_back(4); + primitives->push_back(2); + primitives->push_back(0); - primitives->push_back (6); - primitives->push_back (2); - primitives->push_back (4); + primitives->push_back(6); + primitives->push_back(2); + primitives->push_back(4); - primitives->push_back (6); - primitives->push_back (3); - primitives->push_back (2); + primitives->push_back(6); + primitives->push_back(3); + primitives->push_back(2); - primitives->push_back (7); - primitives->push_back (3); - primitives->push_back (6); + primitives->push_back(7); + primitives->push_back(3); + primitives->push_back(6); - primitives->push_back (1); - primitives->push_back (3); - primitives->push_back (5); + primitives->push_back(1); + primitives->push_back(3); + primitives->push_back(5); - primitives->push_back (5); - primitives->push_back (3); - primitives->push_back (7); + primitives->push_back(5); + primitives->push_back(3); + primitives->push_back(7); - geometry->addPrimitiveSet (primitives); + geometry->addPrimitiveSet(primitives); - osg::Vec4Array *colours = new osg::Vec4Array; + osg::Vec4Array* colours = new osg::Vec4Array; - colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.5f, 0.2f)); - colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); - colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); - colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); - colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); - colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); - colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f)); - colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.5f, 0.2f)); + colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f)); + colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f)); + colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f)); - geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); + geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); - geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); - geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON); + geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - mBaseNode->addChild (geometry); + mBaseNode->addChild(geometry); mParentNode->addChild(mBaseNode); } @@ -277,32 +309,33 @@ namespace CSVRender void InstanceSelectionMode::drawSelectionSphere(const osg::Vec3d& point, float radius) { - if (mBaseNode) mParentNode->removeChild (mBaseNode); + if (mBaseNode) + mParentNode->removeChild(mBaseNode); mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->setPosition(point); - osg::ref_ptr geometry (new osg::Geometry); + osg::ref_ptr geometry(new osg::Geometry); - osg::Vec3Array *vertices = new osg::Vec3Array; - int resolution = 32; + osg::Vec3Array* vertices = new osg::Vec3Array; + constexpr int resolution = 32; float radiusPerResolution = radius / resolution; float reciprocalResolution = 1.0f / resolution; float doubleReciprocalRes = reciprocalResolution * 2; - osg::Vec4Array *colours = new osg::Vec4Array; + osg::Vec4Array* colours = new osg::Vec4Array; - for (float i = 0.0; i <= resolution; i += 2) + for (int i = 0; i <= resolution; i += 2) { float iShifted = (static_cast(i) - resolution / 2.0f); // i - 16 = -16 ... 16 float xPercentile = iShifted * doubleReciprocalRes; float x = xPercentile * radius; - float thisRadius = sqrt (radius * radius - x * x); + float thisRadius = sqrt(radius * radius - x * x); - //the next row + // the next row float iShifted2 = (static_cast(i + 1) - resolution / 2.0f); float xPercentile2 = iShifted2 * doubleReciprocalRes; float x2 = xPercentile2 * radius; - float thisRadius2 = sqrt (radius * radius - x2 * x2); + float thisRadius2 = sqrt(radius * radius - x2 * x2); for (int j = 0; j < resolution; ++j) { @@ -310,57 +343,62 @@ namespace CSVRender float vertexY = i * radiusPerResolution * 2 - radius; float vertexZ = thisRadius * cos(j * reciprocalResolution * osg::PI * 2); float heightPercentage = (vertexZ + radius) / (radius * 2); - vertices->push_back (osg::Vec3f (vertexX, vertexY, vertexZ)); - colours->push_back (osg::Vec4f (heightPercentage, heightPercentage, heightPercentage, 0.3f)); + vertices->push_back(osg::Vec3f(vertexX, vertexY, vertexZ)); + colours->push_back(osg::Vec4f(heightPercentage, heightPercentage, heightPercentage, 0.3f)); float vertexNextRowX = thisRadius2 * sin(j * reciprocalResolution * osg::PI * 2); float vertexNextRowY = (i + 1) * radiusPerResolution * 2 - radius; float vertexNextRowZ = thisRadius2 * cos(j * reciprocalResolution * osg::PI * 2); float heightPercentageNextRow = (vertexZ + radius) / (radius * 2); - vertices->push_back (osg::Vec3f (vertexNextRowX, vertexNextRowY, vertexNextRowZ)); - colours->push_back (osg::Vec4f (heightPercentageNextRow, heightPercentageNextRow, heightPercentageNextRow, 0.3f)); + vertices->push_back(osg::Vec3f(vertexNextRowX, vertexNextRowY, vertexNextRowZ)); + colours->push_back( + osg::Vec4f(heightPercentageNextRow, heightPercentageNextRow, heightPercentageNextRow, 0.3f)); } } - geometry->setVertexArray (vertices); + geometry->setVertexArray(vertices); - osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLE_STRIP, 0); + osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLE_STRIP, 0); for (int i = 0; i < resolution; ++i) { - //Even + // Even for (int j = 0; j < resolution * 2; ++j) { - if (i * resolution * 2 + j > static_cast(vertices->size()) - 1) continue; - primitives->push_back (i * resolution * 2 + j); + if (i * resolution * 2 + j > static_cast(vertices->size()) - 1) + continue; + primitives->push_back(i * resolution * 2 + j); } - if (i * resolution * 2 > static_cast(vertices->size()) - 1) continue; - primitives->push_back (i * resolution * 2); - primitives->push_back (i * resolution * 2 + 1); + if (i * resolution * 2 > static_cast(vertices->size()) - 1) + continue; + primitives->push_back(i * resolution * 2); + primitives->push_back(i * resolution * 2 + 1); - //Odd + // Odd for (int j = 1; j < resolution * 2 - 2; j += 2) { - if ((i + 1) * resolution * 2 + j - 1 > static_cast(vertices->size()) - 1) continue; - primitives->push_back ((i + 1) * resolution * 2 + j - 1); - primitives->push_back (i * resolution * 2 + j + 2); + if ((i + 1) * resolution * 2 + j - 1 > static_cast(vertices->size()) - 1) + continue; + primitives->push_back((i + 1) * resolution * 2 + j - 1); + primitives->push_back(i * resolution * 2 + j + 2); } - if ((i + 2) * resolution * 2 - 2 > static_cast(vertices->size()) - 1) continue; - primitives->push_back ((i + 2) * resolution * 2 - 2); - primitives->push_back (i * resolution * 2 + 1); - primitives->push_back ((i + 1) * resolution * 2); + if ((i + 2) * resolution * 2 - 2 > static_cast(vertices->size()) - 1) + continue; + primitives->push_back((i + 2) * resolution * 2 - 2); + primitives->push_back(i * resolution * 2 + 1); + primitives->push_back((i + 1) * resolution * 2); } - geometry->addPrimitiveSet (primitives); + geometry->addPrimitiveSet(primitives); - geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX); + geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); - geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF); - geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON); + geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - mBaseNode->addChild (geometry); + mBaseNode->addChild(geometry); mParentNode->addChild(mBaseNode); } @@ -384,15 +422,15 @@ namespace CSVRender void InstanceSelectionMode::deleteSelection() { - std::vector > selection = getWorldspaceWidget().getSelection(Mask_Reference); + std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); CSMWorld::IdTable& referencesTable = dynamic_cast( *getWorldspaceWidget().getDocument().getData().getTableModel(CSMWorld::UniversalId::Type_References)); - for (std::vector >::iterator iter = selection.begin(); iter != selection.end(); ++iter) + for (std::vector>::iterator iter = selection.begin(); iter != selection.end(); ++iter) { - CSMWorld::DeleteCommand* command = new CSMWorld::DeleteCommand(referencesTable, - static_cast(iter->get())->mObject->getReferenceId()); + CSMWorld::DeleteCommand* command = new CSMWorld::DeleteCommand( + referencesTable, static_cast(iter->get())->mObject->getReferenceId()); getWorldspaceWidget().getDocument().getUndoStack().push(command); } diff --git a/apps/opencs/view/render/instanceselectionmode.hpp b/apps/opencs/view/render/instanceselectionmode.hpp index 81795d5d3eb..9551f964d6f 100644 --- a/apps/opencs/view/render/instanceselectionmode.hpp +++ b/apps/opencs/view/render/instanceselectionmode.hpp @@ -1,67 +1,82 @@ #ifndef CSV_RENDER_INSTANCE_SELECTION_MODE_H #define CSV_RENDER_INSTANCE_SELECTION_MODE_H -#include - -#include #include +#include + +class QAction; +class QMenu; +class QObject; +class QPoint; + +namespace CSVWidget +{ + class SceneToolbar; +} + +namespace osg +{ + class PositionAttitudeTransform; + class Group; + class Vec3f; +} -#include "selectionmode.hpp" #include "instancedragmodes.hpp" +#include "selectionmode.hpp" namespace CSVRender { + class WorldspaceWidget; class InstanceSelectionMode : public SelectionMode { - Q_OBJECT - - public: - - InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, osg::Group *cellNode); + Q_OBJECT - ~InstanceSelectionMode(); + public: + InstanceSelectionMode( + CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, osg::Group* cellNode); - /// Store the worldspace-coordinate when drag begins - void setDragStart(const osg::Vec3d& dragStart); + ~InstanceSelectionMode(); - /// Store the worldspace-coordinate when drag begins - const osg::Vec3d& getDragStart(); + /// Store the worldspace-coordinate when drag begins + void setDragStart(const osg::Vec3d& dragStart); - /// Store the screen-coordinate when drag begins - void setScreenDragStart(const QPoint& dragStartPoint); + /// Store the worldspace-coordinate when drag begins + const osg::Vec3d& getDragStart(); - /// Apply instance selection changes - void dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode); + /// Store the screen-coordinate when drag begins + void setScreenDragStart(const QPoint& dragStartPoint); - void drawSelectionCubeCentre(const osg::Vec3f& mousePlanePoint ); - void drawSelectionCubeCorner(const osg::Vec3f& mousePlanePoint ); - void drawSelectionSphere(const osg::Vec3f& mousePlanePoint ); - protected: + /// Apply instance selection changes + void dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode); - /// Add context menu items to \a menu. - /// - /// \attention menu can be a 0-pointer - /// - /// \return Have there been any menu items to be added (if menu is 0 and there - /// items to be added, the function must return true anyway. - bool createContextMenu(QMenu* menu) override; + void drawSelectionCubeCentre(const osg::Vec3f& mousePlanePoint); + void drawSelectionCubeCorner(const osg::Vec3f& mousePlanePoint); + void drawSelectionSphere(const osg::Vec3f& mousePlanePoint); - private: + protected: + /// Add context menu items to \a menu. + /// + /// \attention menu can be a 0-pointer + /// + /// \return Have there been any menu items to be added (if menu is 0 and there + /// items to be added, the function must return true anyway. + bool createContextMenu(QMenu* menu) override; - void drawSelectionBox(const osg::Vec3d& pointA, const osg::Vec3d& pointB); - void drawSelectionCube(const osg::Vec3d& point, float radius); - void drawSelectionSphere(const osg::Vec3d& point, float radius); + private: + void drawSelectionBox(const osg::Vec3d& pointA, const osg::Vec3d& pointB); + void drawSelectionCube(const osg::Vec3d& point, float radius); + void drawSelectionSphere(const osg::Vec3d& point, float radius); - QAction* mDeleteSelection; - QAction* mSelectSame; - osg::Vec3d mDragStart; - osg::Group* mParentNode; - osg::ref_ptr mBaseNode; + QAction* mDeleteSelection; + QAction* mSelectSame; + osg::Vec3d mDragStart; + osg::Group* mParentNode; + osg::ref_ptr mBaseNode; - private slots: + private slots: - void deleteSelection(); - void selectSame(); + void deleteSelection(); + void selectSame(); }; } diff --git a/apps/opencs/view/render/lighting.cpp b/apps/opencs/view/render/lighting.cpp index 82ad43e6ad7..94b3f02ea79 100644 --- a/apps/opencs/view/render/lighting.cpp +++ b/apps/opencs/view/render/lighting.cpp @@ -1,23 +1,59 @@ #include "lighting.hpp" +#include + +#include #include #include +#include #include +#include + +#include +#include #include +#include "../../model/prefs/state.hpp" + class DayNightSwitchVisitor : public osg::NodeVisitor { public: DayNightSwitchVisitor(int index) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mIndex(index) - { } + { + } - void apply(osg::Switch &switchNode) override + void apply(osg::Switch& switchNode) override { - if (switchNode.getName() == Constants::NightDayLabel) - switchNode.setSingleChildOn(mIndex); + constexpr int NoIndex = -1; + + int initialIndex = NoIndex; + if (!switchNode.getUserValue("initialIndex", initialIndex)) + { + for (size_t i = 0; i < switchNode.getValueList().size(); ++i) + { + if (switchNode.getValueList()[i]) + { + initialIndex = i; + break; + } + } + + if (initialIndex != NoIndex) + switchNode.setUserValue("initialIndex", initialIndex); + } + + if (CSMPrefs::get()["Rendering"]["scene-day-night-switch-nodes"].isTrue()) + { + if (switchNode.getName() == Constants::NightDayLabel) + switchNode.setSingleChildOn(mIndex); + } + else if (initialIndex != NoIndex) + { + switchNode.setSingleChildOn(initialIndex); + } traverse(switchNode); } @@ -26,8 +62,6 @@ class DayNightSwitchVisitor : public osg::NodeVisitor int mIndex; }; -CSVRender::Lighting::~Lighting() {} - void CSVRender::Lighting::updateDayNightMode(int index) { if (mRootNode == nullptr) diff --git a/apps/opencs/view/render/lighting.hpp b/apps/opencs/view/render/lighting.hpp index d9d90767f09..88d9c710c01 100644 --- a/apps/opencs/view/render/lighting.hpp +++ b/apps/opencs/view/render/lighting.hpp @@ -14,23 +14,24 @@ namespace CSVRender { class Lighting { - public: + public: + Lighting() + : mRootNode(nullptr) + { + } + virtual ~Lighting() = default; - Lighting() : mRootNode(nullptr) {} - virtual ~Lighting(); + virtual void activate(osg::Group* rootNode, bool isExterior) = 0; - virtual void activate (osg::Group* rootNode, bool isExterior) = 0; + virtual void deactivate() = 0; - virtual void deactivate() = 0; + virtual osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) = 0; - virtual osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) = 0; + protected: + void updateDayNightMode(int index); - protected: - - void updateDayNightMode(int index); - - osg::ref_ptr mLightSource; - osg::Group* mRootNode; + osg::ref_ptr mLightSource; + osg::Group* mRootNode; }; } diff --git a/apps/opencs/view/render/lightingbright.cpp b/apps/opencs/view/render/lightingbright.cpp index d76823fb3cf..7acd24fc629 100644 --- a/apps/opencs/view/render/lightingbright.cpp +++ b/apps/opencs/view/render/lightingbright.cpp @@ -1,16 +1,19 @@ #include "lightingbright.hpp" +#include +#include #include +#include CSVRender::LightingBright::LightingBright() {} -void CSVRender::LightingBright::activate (osg::Group* rootNode, bool /*isExterior*/) +void CSVRender::LightingBright::activate(osg::Group* rootNode, bool /*isExterior*/) { mRootNode = rootNode; mLightSource = (new osg::LightSource); - osg::ref_ptr light (new osg::Light); + osg::ref_ptr light(new osg::Light); light->setAmbient(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); light->setPosition(osg::Vec4f(0.f, 0.f, 1.f, 0.f)); light->setDiffuse(osg::Vec4f(1.f, 1.f, 1.f, 1.f)); diff --git a/apps/opencs/view/render/lightingbright.hpp b/apps/opencs/view/render/lightingbright.hpp index aa149275249..8ee570e2a20 100644 --- a/apps/opencs/view/render/lightingbright.hpp +++ b/apps/opencs/view/render/lightingbright.hpp @@ -3,9 +3,10 @@ #include "lighting.hpp" +#include + namespace osg { - class Light; class Group; } @@ -13,15 +14,14 @@ namespace CSVRender { class LightingBright : public Lighting { - public: - - LightingBright(); + public: + LightingBright(); - void activate (osg::Group* rootNode, bool /*isExterior*/) override; + void activate(osg::Group* rootNode, bool /*isExterior*/) override; - void deactivate() override; + void deactivate() override; - osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) override; + osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) override; }; } diff --git a/apps/opencs/view/render/lightingday.cpp b/apps/opencs/view/render/lightingday.cpp index dc4592b21cf..50aef93f1fc 100644 --- a/apps/opencs/view/render/lightingday.cpp +++ b/apps/opencs/view/render/lightingday.cpp @@ -1,16 +1,17 @@ #include "lightingday.hpp" +#include +#include #include +#include -CSVRender::LightingDay::LightingDay(){} - -void CSVRender::LightingDay::activate (osg::Group* rootNode, bool /*isExterior*/) +void CSVRender::LightingDay::activate(osg::Group* rootNode, bool /*isExterior*/) { mRootNode = rootNode; mLightSource = new osg::LightSource; - osg::ref_ptr light (new osg::Light); + osg::ref_ptr light(new osg::Light); light->setPosition(osg::Vec4f(0.f, 0.f, 1.f, 0.f)); light->setAmbient(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); light->setDiffuse(osg::Vec4f(1.f, 1.f, 1.f, 1.f)); @@ -29,7 +30,7 @@ void CSVRender::LightingDay::deactivate() mRootNode->removeChild(mLightSource); } -osg::Vec4f CSVRender::LightingDay::getAmbientColour(osg::Vec4f *defaultAmbient) +osg::Vec4f CSVRender::LightingDay::getAmbientColour(osg::Vec4f* defaultAmbient) { if (defaultAmbient) return *defaultAmbient; diff --git a/apps/opencs/view/render/lightingday.hpp b/apps/opencs/view/render/lightingday.hpp index eafc6b8e818..e68d496b219 100644 --- a/apps/opencs/view/render/lightingday.hpp +++ b/apps/opencs/view/render/lightingday.hpp @@ -3,19 +3,25 @@ #include "lighting.hpp" +#include + +namespace osg +{ + class Group; +} + namespace CSVRender { class LightingDay : public Lighting { - public: - - LightingDay(); + public: + LightingDay() = default; - void activate (osg::Group* rootNode, bool /*isExterior*/) override; + void activate(osg::Group* rootNode, bool /*isExterior*/) override; - void deactivate() override; + void deactivate() override; - osg::Vec4f getAmbientColour(osg::Vec4f *defaultAmbient) override; + osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) override; }; } diff --git a/apps/opencs/view/render/lightingnight.cpp b/apps/opencs/view/render/lightingnight.cpp index fbebb46a114..6628a4983b5 100644 --- a/apps/opencs/view/render/lightingnight.cpp +++ b/apps/opencs/view/render/lightingnight.cpp @@ -1,16 +1,17 @@ #include "lightingnight.hpp" +#include +#include #include +#include -CSVRender::LightingNight::LightingNight() {} - -void CSVRender::LightingNight::activate (osg::Group* rootNode, bool isExterior) +void CSVRender::LightingNight::activate(osg::Group* rootNode, bool isExterior) { mRootNode = rootNode; mLightSource = new osg::LightSource; - osg::ref_ptr light (new osg::Light); + osg::ref_ptr light(new osg::Light); light->setPosition(osg::Vec4f(0.f, 0.f, 1.f, 0.f)); light->setAmbient(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); light->setDiffuse(osg::Vec4f(0.2f, 0.2f, 0.2f, 1.f)); @@ -30,7 +31,7 @@ void CSVRender::LightingNight::deactivate() mRootNode->removeChild(mLightSource); } -osg::Vec4f CSVRender::LightingNight::getAmbientColour(osg::Vec4f *defaultAmbient) +osg::Vec4f CSVRender::LightingNight::getAmbientColour(osg::Vec4f* defaultAmbient) { if (defaultAmbient) return *defaultAmbient; diff --git a/apps/opencs/view/render/lightingnight.hpp b/apps/opencs/view/render/lightingnight.hpp index bfa94ce9729..1a813bd540d 100644 --- a/apps/opencs/view/render/lightingnight.hpp +++ b/apps/opencs/view/render/lightingnight.hpp @@ -3,18 +3,24 @@ #include "lighting.hpp" +#include + +namespace osg +{ + class Group; +} + namespace CSVRender { class LightingNight : public Lighting { - public: - - LightingNight(); + public: + LightingNight() = default; - void activate (osg::Group* rootNode, bool isExterior) override; - void deactivate() override; + void activate(osg::Group* rootNode, bool isExterior) override; + void deactivate() override; - osg::Vec4f getAmbientColour(osg::Vec4f *defaultAmbient) override; + osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) override; }; } diff --git a/apps/opencs/view/render/mask.hpp b/apps/opencs/view/render/mask.hpp index 818be8b2283..1c84d886d34 100644 --- a/apps/opencs/view/render/mask.hpp +++ b/apps/opencs/view/render/mask.hpp @@ -11,11 +11,11 @@ namespace CSVRender enum Mask : unsigned int { // elements that are part of the actual scene - Mask_Reference = 0x2, - Mask_Pathgrid = 0x4, - Mask_Water = 0x8, - Mask_Fog = 0x10, - Mask_Terrain = 0x20, + Mask_Hidden = 0x0, + Mask_Reference = 0x1, + Mask_Pathgrid = 0x2, + Mask_Water = 0x4, + Mask_Terrain = 0x8, // used within models Mask_ParticleSystem = 0x100, diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index 2bb537d74ab..fe4b6e9b7fd 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -1,90 +1,117 @@ #include "object.hpp" +#include +#include +#include +#include #include #include -#include - -#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include #include +#include +#include #include - -#include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include #include -#include "../../model/world/data.hpp" -#include "../../model/world/ref.hpp" -#include "../../model/world/refidcollection.hpp" -#include "../../model/world/commands.hpp" -#include "../../model/world/universalid.hpp" -#include "../../model/world/commandmacro.hpp" -#include "../../model/world/cellcoordinates.hpp" #include "../../model/prefs/state.hpp" +#include "../../model/world/cellcoordinates.hpp" +#include "../../model/world/commandmacro.hpp" +#include "../../model/world/commands.hpp" +#include "../../model/world/data.hpp" #include +#include +#include #include -#include +#include #include +#include #include "actor.hpp" #include "mask.hpp" +namespace CSVRender +{ + struct WorldspaceHitResult; +} + +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 { - osg::ref_ptr createErrorCube() + osg::ref_ptr createErrorCube() { - osg::ref_ptr shape(new osg::Box(osg::Vec3f(0,0,0), 50.f)); + osg::ref_ptr shape(new osg::Box(osg::Vec3f(0, 0, 0), 50.f)); osg::ref_ptr shapedrawable(new osg::ShapeDrawable); shapedrawable->setShape(shape); - osg::ref_ptr geode (new osg::Geode); - geode->addDrawable(shapedrawable); - return geode; + osg::ref_ptr group(new osg::Group); + group->addChild(shapedrawable); + return group; } } - -CSVRender::ObjectTag::ObjectTag (Object* object) -: TagBase (Mask_Reference), mObject (object) -{} - -QString CSVRender::ObjectTag::getToolTip (bool hideBasics) const +CSVRender::ObjectTag::ObjectTag(Object* object) + : TagBase(Mask_Reference) + , mObject(object) { - return QString::fromUtf8 (mObject->getReferenceableId().c_str()); } +QString CSVRender::ObjectTag::getToolTip(bool /*hideBasics*/, const WorldspaceHitResult& /*hit*/) const +{ + return QString::fromUtf8(mObject->getReferenceableId().c_str()); +} -CSVRender::ObjectMarkerTag::ObjectMarkerTag (Object* object, int axis) -: ObjectTag (object), mAxis (axis) -{} - - -void CSVRender::Object::clear() +CSVRender::ObjectMarkerTag::ObjectMarkerTag(Object* object, int axis) + : ObjectTag(object) + , mAxis(axis) { } +void CSVRender::Object::clear() {} + void CSVRender::Object::update() { clear(); const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables(); const int TypeIndex = referenceables.findColumnIndex(CSMWorld::Columns::ColumnId_RecordType); - const int ModelIndex = referenceables.findColumnIndex (CSMWorld::Columns::ColumnId_Model); + const int ModelIndex = referenceables.findColumnIndex(CSMWorld::Columns::ColumnId_Model); - int index = referenceables.searchId (mReferenceableId); + int index = referenceables.searchId(mReferenceableId); const ESM::Light* light = nullptr; mBaseNode->removeChildren(0, mBaseNode->getNumChildren()); @@ -102,7 +129,7 @@ void CSVRender::Object::update() if (recordType == CSMWorld::UniversalId::Type_Light) { - light = &dynamic_cast& >(referenceables.getRecord(index)).get(); + light = &dynamic_cast&>(referenceables.getRecord(index)).get(); if (model.empty()) model = "marker_light.nif"; } @@ -117,18 +144,21 @@ void CSVRender::Object::update() { if (recordType == CSMWorld::UniversalId::Type_Npc || recordType == CSMWorld::UniversalId::Type_Creature) { - if (!mActor) mActor.reset(new Actor(mReferenceableId, mData)); + if (!mActor) + mActor = std::make_unique(mReferenceableId, mData); mActor->update(); mBaseNode->addChild(mActor->getBaseNode()); } else if (!model.empty()) { - std::string path = "meshes\\" + model; + constexpr VFS::Path::NormalizedView meshes("meshes"); + VFS::Path::Normalized path(meshes); + path /= model; mResourceSystem->getSceneManager()->getInstance(path, mBaseNode); } else { - throw std::runtime_error(mReferenceableId + " has no model"); + throw std::runtime_error(mReferenceableId.getRefIdString() + " has no model"); } } catch (std::exception& e) @@ -140,7 +170,7 @@ void CSVRender::Object::update() if (light) { bool isExterior = false; // FIXME - SceneUtil::addLight(mBaseNode, light, Mask_ParticleSystem, Mask_Lighting, isExterior); + SceneUtil::addLight(mBaseNode, SceneUtil::LightCommon(*light), Mask_Lighting, isExterior); } } @@ -152,13 +182,14 @@ void CSVRender::Object::adjustTransform() ESM::Position position = getPosition(); // position - mRootNode->setPosition(mForceBaseToZero ? osg::Vec3() : osg::Vec3f(position.pos[0], position.pos[1], position.pos[2])); + mRootNode->setPosition( + mForceBaseToZero ? osg::Vec3() : osg::Vec3f(position.pos[0], position.pos[1], position.pos[2])); // orientation - osg::Quat xr (-position.rot[0], osg::Vec3f(1,0,0)); - osg::Quat yr (-position.rot[1], osg::Vec3f(0,1,0)); - osg::Quat zr (-position.rot[2], osg::Vec3f(0,0,1)); - mBaseNode->setAttitude(zr*yr*xr); + osg::Quat xr(-position.rot[0], osg::Vec3f(1, 0, 0)); + osg::Quat yr(-position.rot[1], osg::Vec3f(0, 1, 0)); + osg::Quat zr(-position.rot[2], osg::Vec3f(0, 0, 1)); + mBaseNode->setAttitude(zr * yr * xr); float scale = getScale(); @@ -168,147 +199,147 @@ void CSVRender::Object::adjustTransform() const CSMWorld::CellRef& CSVRender::Object::getReference() const { if (mReferenceId.empty()) - throw std::logic_error ("object does not represent a reference"); + throw std::logic_error("object does not represent a reference"); - return mData.getReferences().getRecord (mReferenceId).get(); + return mData.getReferences().getRecord(mReferenceId).get(); } void CSVRender::Object::updateMarker() { - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) { if (mMarker[i]) { - mRootNode->removeChild (mMarker[i]); + mRootNode->removeChild(mMarker[i]); mMarker[i] = osg::ref_ptr(); } if (mSelected) { - if (mSubMode==0) + if (mSubMode == 0) { - mMarker[i] = makeMoveOrScaleMarker (i); - mMarker[i]->setUserData(new ObjectMarkerTag (this, i)); + mMarker[i] = makeMoveOrScaleMarker(i); + mMarker[i]->setUserData(new ObjectMarkerTag(this, i)); - mRootNode->addChild (mMarker[i]); + mRootNode->addChild(mMarker[i]); } - else if (mSubMode==1) + else if (mSubMode == 1) { - mMarker[i] = makeRotateMarker (i); - mMarker[i]->setUserData(new ObjectMarkerTag (this, i)); + mMarker[i] = makeRotateMarker(i); + mMarker[i]->setUserData(new ObjectMarkerTag(this, i)); - mRootNode->addChild (mMarker[i]); + mRootNode->addChild(mMarker[i]); } - else if (mSubMode==2) + else if (mSubMode == 2) { - mMarker[i] = makeMoveOrScaleMarker (i); - mMarker[i]->setUserData(new ObjectMarkerTag (this, i)); + mMarker[i] = makeMoveOrScaleMarker(i); + mMarker[i]->setUserData(new ObjectMarkerTag(this, i)); - mRootNode->addChild (mMarker[i]); + mRootNode->addChild(mMarker[i]); } } } } -osg::ref_ptr CSVRender::Object::makeMoveOrScaleMarker (int axis) +osg::ref_ptr CSVRender::Object::makeMoveOrScaleMarker(int axis) { - osg::ref_ptr geometry (new osg::Geometry); + osg::ref_ptr geometry(new osg::Geometry); float shaftLength = MarkerShaftBaseLength + mBaseNode->getBound().radius(); // shaft - osg::Vec3Array *vertices = new osg::Vec3Array; + osg::Vec3Array* vertices = new osg::Vec3Array; - for (int i=0; i<2; ++i) + 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)); + 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)); + 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)); + vertices->push_back(getMarkerPosition(0, 0, shaftLength + MarkerHeadLength, axis)); - geometry->setVertexArray (vertices); + geometry->setVertexArray(vertices); - osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); + osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, 0); // shaft - for (int i=0; i<4; ++i) + 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); + 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(0); + primitives->push_back(1); + primitives->push_back(2); - primitives->push_back (2); - primitives->push_back (3); - primitives->push_back (0); + 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(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); + primitives->push_back(2 + 8); + primitives->push_back(3 + 8); + primitives->push_back(0 + 8); - for (int i=0; i<4; ++i) + 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); + primitives->push_back(12); + primitives->push_back(8 + (i == 3 ? 0 : i + 1)); + primitives->push_back(8 + i); } - geometry->addPrimitiveSet (primitives); + geometry->addPrimitiveSet(primitives); - osg::Vec4Array *colours = new osg::Vec4Array; + 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 = 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)); + 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); + geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); setupCommonMarkerState(geometry); - osg::ref_ptr geode (new osg::Geode); - geode->addDrawable (geometry); + osg::ref_ptr group(new osg::Group); + group->addChild(geometry); - return geode; + return group; } -osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) +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::min(64, std::max(24, (int)(OuterRadius * 2 * osg::PI / SegmentDistance))); + const size_t SegmentCount = std::clamp(OuterRadius * 2 * osg::PI / SegmentDistance, 24, 64); const size_t VerticesPerSegment = 4; const size_t IndicesPerSegment = 24; @@ -317,24 +348,18 @@ osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) 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 - }; - + 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); + 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); + osg::Vec3f offset = getMarkerPosition(0, MarkerShaftWidth / 4, 0, axis); for (size_t i = 0; i < SegmentCount; ++i) { @@ -346,17 +371,14 @@ osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) 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(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; 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); + 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) { @@ -382,10 +404,10 @@ osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) setupCommonMarkerState(geometry); - osg::ref_ptr geode = new osg::Geode(); - geode->addDrawable (geometry); + osg::ref_ptr group = new osg::Group(); + group->addChild(geometry); - return geode; + return group; } void CSVRender::Object::setupCommonMarkerState(osg::ref_ptr geometry) @@ -397,24 +419,35 @@ void CSVRender::Object::setupCommonMarkerState(osg::ref_ptr geome state->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); } -osg::Vec3f CSVRender::Object::getMarkerPosition (float x, float y, float z, int axis) +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); + 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"); + 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), mBaseNode(nullptr), mSelected(false), mParentNode(parentNode), mResourceSystem(data.getResourceSystem().get()), mForceBaseToZero (forceBaseToZero), - mScaleOverride (1), mOverrideFlags (0), mSubMode (-1), mMarkerTransparency(0.5f) +CSVRender::Object::Object( + CSMWorld::Data& data, osg::Group* parentNode, const std::string& id, bool referenceable, bool forceBaseToZero) + : mData(data) + , mBaseNode(nullptr) + , mSelected(false) + , mParentNode(parentNode) + , mResourceSystem(data.getResourceSystem().get()) + , mForceBaseToZero(forceBaseToZero) + , mScaleOverride(1) + , mOverrideFlags(0) + , mSubMode(-1) + , mMarkerTransparency(0.5f) { mRootNode = new osg::PositionAttitudeTransform; @@ -425,19 +458,19 @@ CSVRender::Object::Object (CSMWorld::Data& data, osg::Group* parentNode, mBaseNode->setUserData(new ObjectTag(this)); - mRootNode->addChild (mBaseNode); + mRootNode->addChild(mBaseNode); - parentNode->addChild (mRootNode); + parentNode->addChild(mRootNode); mRootNode->setNodeMask(Mask_Reference); - + ESM::RefId refId = ESM::RefId::stringRefId(id); if (referenceable) { - mReferenceableId = id; + mReferenceableId = refId; } else { - mReferenceId = id; + mReferenceId = refId; mReferenceableId = getReference().mRefID; } @@ -450,18 +483,24 @@ CSVRender::Object::~Object() { clear(); - mParentNode->removeChild (mRootNode); + mParentNode->removeChild(mRootNode); } -void CSVRender::Object::setSelected(bool selected) +void CSVRender::Object::setSelected(bool selected, const osg::Vec4f& color) { mSelected = selected; + if (mSnapTarget) + { + setSnapTarget(false); + } + mOutline->removeChild(mBaseNode); mRootNode->removeChild(mOutline); mRootNode->removeChild(mBaseNode); if (selected) { + mOutline->setWireframeColor(color); mOutline->addChild(mBaseNode); mRootNode->addChild(mOutline); } @@ -477,6 +516,36 @@ bool CSVRender::Object::getSelected() const return mSelected; } +void CSVRender::Object::setSnapTarget(bool isSnapTarget) +{ + mSnapTarget = isSnapTarget; + + if (mSelected) + { + setSelected(false); + } + + mOutline->removeChild(mBaseNode); + mRootNode->removeChild(mOutline); + mRootNode->removeChild(mBaseNode); + if (isSnapTarget) + { + mOutline->setWireframeColor(osg::Vec4f(1, 1, 0, 1)); + mOutline->addChild(mBaseNode); + mRootNode->addChild(mOutline); + } + else + mRootNode->addChild(mBaseNode); + + mMarkerTransparency = CSMPrefs::get()["Rendering"]["object-marker-alpha"].toDouble(); + updateMarker(); +} + +bool CSVRender::Object::getSnapTarget() const +{ + return mSnapTarget; +} + osg::ref_ptr CSVRender::Object::getRootNode() { return mRootNode; @@ -487,14 +556,13 @@ osg::ref_ptr CSVRender::Object::getBaseNode() return mBaseNode; } -bool CSVRender::Object::referenceableDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +bool CSVRender::Object::referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables(); - int index = referenceables.searchId (mReferenceableId); + int index = referenceables.searchId(mReferenceableId); - if (index!=-1 && index>=topLeft.row() && index<=bottomRight.row()) + if (index != -1 && index >= topLeft.row() && index <= bottomRight.row()) { adjustTransform(); update(); @@ -505,14 +573,13 @@ bool CSVRender::Object::referenceableDataChanged (const QModelIndex& topLeft, return false; } -bool CSVRender::Object::referenceableAboutToBeRemoved (const QModelIndex& parent, int start, - int end) +bool CSVRender::Object::referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end) { const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables(); - int index = referenceables.searchId (mReferenceableId); + int index = referenceables.searchId(mReferenceableId); - if (index!=-1 && index>=start && index<=end) + if (index != -1 && index >= start && index <= end) { // Deletion of referenceable-type objects is handled outside of Object. if (!mReferenceId.empty()) @@ -526,27 +593,25 @@ bool CSVRender::Object::referenceableAboutToBeRemoved (const QModelIndex& parent return false; } -bool CSVRender::Object::referenceDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +bool CSVRender::Object::referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mReferenceId.empty()) return false; const CSMWorld::RefCollection& references = mData.getReferences(); - int index = references.searchId (mReferenceId); + int index = references.searchId(mReferenceId); - if (index!=-1 && index>=topLeft.row() && index<=bottomRight.row()) + if (index != -1 && index >= topLeft.row() && index <= bottomRight.row()) { - int columnIndex = - references.findColumnIndex (CSMWorld::Columns::ColumnId_ReferenceableId); + int columnIndex = references.findColumnIndex(CSMWorld::Columns::ColumnId_ReferenceableId); adjustTransform(); - if (columnIndex>=topLeft.column() && columnIndex<=bottomRight.row()) + if (columnIndex >= topLeft.column() && columnIndex <= bottomRight.row()) { - mReferenceableId = - references.getData (index, columnIndex).toString().toUtf8().constData(); + mReferenceableId + = ESM::RefId::stringRefId(references.getData(index, columnIndex).toString().toUtf8().constData()); update(); updateMarker(); @@ -566,17 +631,17 @@ void CSVRender::Object::reloadAssets() std::string CSVRender::Object::getReferenceId() const { - return mReferenceId; + return mReferenceId.getRefIdString(); } std::string CSVRender::Object::getReferenceableId() const { - return mReferenceableId; + return mReferenceableId.getRefIdString(); } osg::ref_ptr CSVRender::Object::getTag() const { - return static_cast (mBaseNode->getUserData()); + return static_cast(mBaseNode->getUserData()); } bool CSVRender::Object::isEdited() const @@ -584,7 +649,7 @@ bool CSVRender::Object::isEdited() const return mOverrideFlags; } -void CSVRender::Object::setEdited (int flags) +void CSVRender::Object::setEdited(int flags) { bool discard = mOverrideFlags & ~flags; int added = flags & ~mOverrideFlags; @@ -592,11 +657,11 @@ void CSVRender::Object::setEdited (int flags) mOverrideFlags = flags; if (added & Override_Position) - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) mPositionOverride.pos[i] = getReference().mPos.pos[i]; if (added & Override_Rotation) - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) mPositionOverride.rot[i] = getReference().mPos.rot[i]; if (added & Override_Scale) @@ -611,11 +676,11 @@ ESM::Position CSVRender::Object::getPosition() const ESM::Position position = getReference().mPos; if (mOverrideFlags & Override_Position) - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) position.pos[i] = mPositionOverride.pos[i]; if (mOverrideFlags & Override_Rotation) - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) position.rot[i] = mPositionOverride.rot[i]; return position; @@ -626,31 +691,31 @@ float CSVRender::Object::getScale() const return (mOverrideFlags & Override_Scale) ? mScaleOverride : getReference().mScale; } -void CSVRender::Object::setPosition (const float position[3]) +void CSVRender::Object::setPosition(const float position[3]) { mOverrideFlags |= Override_Position; - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) mPositionOverride.pos[i] = position[i]; adjustTransform(); } -void CSVRender::Object::setRotation (const float rotation[3]) +void CSVRender::Object::setRotation(const float rotation[3]) { mOverrideFlags |= Override_Rotation; - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) mPositionOverride.rot[i] = rotation[i]; adjustTransform(); } -void CSVRender::Object::setScale (float scale) +void CSVRender::Object::setScale(float scale) { mOverrideFlags |= Override_Scale; - mScaleOverride = scale; + mScaleOverride = std::clamp(scale, 0.5f, 2.0f); adjustTransform(); } @@ -661,78 +726,79 @@ void CSVRender::Object::setMarkerTransparency(float value) updateMarker(); } -void CSVRender::Object::apply (CSMWorld::CommandMacro& commands) +void CSVRender::Object::apply(CSMWorld::CommandMacro& commands) { const CSMWorld::RefCollection& collection = mData.getReferences(); - QAbstractItemModel *model = mData.getTableModel (CSMWorld::UniversalId::Type_References); + QAbstractItemModel* model = mData.getTableModel(CSMWorld::UniversalId::Type_References); - int recordIndex = collection.getIndex (mReferenceId); + int recordIndex = collection.getIndex(mReferenceId); if (mOverrideFlags & Override_Position) { - //Do cell check first so positions can be compared + // Do cell check first so positions can be compared const CSMWorld::CellRef& ref = collection.getRecord(recordIndex).get(); if (CSMWorld::CellCoordinates::isExteriorCell(ref.mCell)) { // Find cell index at new position - std::pair cellIndex = CSMWorld::CellCoordinates::coordinatesToCellIndex( - mPositionOverride.pos[0], mPositionOverride.pos[1]); + std::pair cellIndex + = CSMWorld::CellCoordinates::coordinatesToCellIndex(mPositionOverride.pos[0], mPositionOverride.pos[1]); std::pair originalIndex = ref.getCellIndex(); - int cellColumn = collection.findColumnIndex (static_cast ( - CSMWorld::Columns::ColumnId_Cell)); - int refNumColumn = collection.findColumnIndex (static_cast ( - CSMWorld::Columns::ColumnId_RefNum)); + int cellColumn = collection.findColumnIndex( + static_cast(CSMWorld::Columns::ColumnId_Cell)); + int origCellColumn = collection.findColumnIndex( + static_cast(CSMWorld::Columns::ColumnId_OriginalCell)); if (cellIndex != originalIndex) { /// \todo figure out worldspace (not important until multiple worldspaces are supported) - std::string cellId = CSMWorld::CellCoordinates (cellIndex).getId (""); - - commands.push (new CSMWorld::ModifyCommand (*model, - model->index (recordIndex, cellColumn), QString::fromUtf8 (cellId.c_str()))); - commands.push (new CSMWorld::ModifyCommand( *model, - model->index (recordIndex, refNumColumn), 0)); + std::string origCellId = CSMWorld::CellCoordinates(originalIndex).getId(""); + std::string cellId = CSMWorld::CellCoordinates(cellIndex).getId(""); + + commands.push(new CSMWorld::ModifyCommand( + *model, model->index(recordIndex, origCellColumn), QString::fromUtf8(origCellId.c_str()))); + commands.push(new CSMWorld::ModifyCommand( + *model, model->index(recordIndex, cellColumn), QString::fromUtf8(cellId.c_str()))); + // NOTE: refnum is not modified for moving a reference to another cell } } - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) { - int column = collection.findColumnIndex (static_cast ( - CSMWorld::Columns::ColumnId_PositionXPos+i)); + int column = collection.findColumnIndex( + static_cast(CSMWorld::Columns::ColumnId_PositionXPos + i)); - commands.push (new CSMWorld::ModifyCommand (*model, - model->index (recordIndex, column), mPositionOverride.pos[i])); + commands.push( + new CSMWorld::ModifyCommand(*model, model->index(recordIndex, column), mPositionOverride.pos[i])); } } if (mOverrideFlags & Override_Rotation) { - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) { - int column = collection.findColumnIndex (static_cast ( - CSMWorld::Columns::ColumnId_PositionXRot+i)); + int column = collection.findColumnIndex( + static_cast(CSMWorld::Columns::ColumnId_PositionXRot + i)); - commands.push (new CSMWorld::ModifyCommand (*model, - model->index (recordIndex, column), osg::RadiansToDegrees(mPositionOverride.rot[i]))); + commands.push(new CSMWorld::ModifyCommand( + *model, model->index(recordIndex, column), osg::RadiansToDegrees(mPositionOverride.rot[i]))); } } if (mOverrideFlags & Override_Scale) { - int column = collection.findColumnIndex (CSMWorld::Columns::ColumnId_Scale); + int column = collection.findColumnIndex(CSMWorld::Columns::ColumnId_Scale); - commands.push (new CSMWorld::ModifyCommand (*model, - model->index (recordIndex, column), mScaleOverride)); + commands.push(new CSMWorld::ModifyCommand(*model, model->index(recordIndex, column), mScaleOverride)); } mOverrideFlags = 0; } -void CSVRender::Object::setSubMode (int subMode) +void CSVRender::Object::setSubMode(int subMode) { - if (subMode!=mSubMode) + if (subMode != mSubMode) { mSubMode = subMode; updateMarker(); diff --git a/apps/opencs/view/render/object.hpp b/apps/opencs/view/render/object.hpp index a19d6422340..31f0d93ac40 100644 --- a/apps/opencs/view/render/object.hpp +++ b/apps/opencs/view/render/object.hpp @@ -4,23 +4,23 @@ #include #include +#include +#include #include -#include -#include -#include +#include +#include #include "tagbase.hpp" class QModelIndex; -class QUndoStack; namespace osg { class PositionAttitudeTransform; + class Geometry; class Group; class Node; - class Geode; } namespace osgFX @@ -44,165 +44,166 @@ namespace CSVRender { class Actor; class Object; + struct WorldspaceHitResult; - // An object to attach as user data to the osg::Node, allows us to get an Object back from a Node when we are doing a ray query + // An object to attach as user data to the osg::Node, allows us to get an Object back from a Node when we are doing + // a ray query class ObjectTag : public TagBase { - public: + public: + ObjectTag(Object* object); - ObjectTag (Object* object); + Object* mObject; - Object* mObject; - - QString getToolTip (bool hideBasics) const override; + QString getToolTip(bool hideBasics, const WorldspaceHitResult& hit) const override; }; class ObjectMarkerTag : public ObjectTag { - public: - - ObjectMarkerTag (Object* object, int axis); + public: + ObjectMarkerTag(Object* object, int axis); - int mAxis; + int mAxis; }; class Object { - public: - - enum OverrideFlags - { - Override_Position = 1, - Override_Rotation = 2, - Override_Scale = 4 - }; + public: + enum OverrideFlags + { + Override_Position = 1, + Override_Rotation = 2, + Override_Scale = 4 + }; - private: + private: + static const float MarkerShaftWidth; + static const float MarkerShaftBaseLength; + static const float MarkerHeadWidth; + static const float MarkerHeadLength; - static const float MarkerShaftWidth; - static const float MarkerShaftBaseLength; - static const float MarkerHeadWidth; - static const float MarkerHeadLength; + CSMWorld::Data& mData; + ESM::RefId mReferenceId; + ESM::RefId mReferenceableId; + osg::ref_ptr mRootNode; + osg::ref_ptr mBaseNode; + osg::ref_ptr mOutline; + bool mSelected; + bool mSnapTarget; + osg::Group* mParentNode; + Resource::ResourceSystem* mResourceSystem; + bool mForceBaseToZero; + ESM::Position mPositionOverride; + float mScaleOverride; + int mOverrideFlags; + osg::ref_ptr mMarker[3]; + int mSubMode; + float mMarkerTransparency; + std::unique_ptr mActor; - CSMWorld::Data& mData; - std::string mReferenceId; - std::string mReferenceableId; - osg::ref_ptr mRootNode; - osg::ref_ptr mBaseNode; - osg::ref_ptr mOutline; - bool mSelected; - osg::Group* mParentNode; - Resource::ResourceSystem* mResourceSystem; - bool mForceBaseToZero; - ESM::Position mPositionOverride; - float mScaleOverride; - int mOverrideFlags; - osg::ref_ptr mMarker[3]; - int mSubMode; - float mMarkerTransparency; - std::unique_ptr mActor; + /// Not implemented + Object(const Object&); - /// Not implemented - Object (const Object&); + /// Not implemented + Object& operator=(const Object&); - /// Not implemented - Object& operator= (const Object&); + /// Remove object from node (includes deleting) + void clear(); - /// Remove object from node (includes deleting) - void clear(); + /// Update model + /// @note Make sure adjustTransform() was called first so world space particles get positioned correctly + void update(); - /// Update model - /// @note Make sure adjustTransform() was called first so world space particles get positioned correctly - void update(); + /// Adjust position, orientation and scale + void adjustTransform(); - /// Adjust position, orientation and scale - void adjustTransform(); + /// Throws an exception if *this was constructed with referenceable + const CSMWorld::CellRef& getReference() const; - /// Throws an exception if *this was constructed with referenceable - const CSMWorld::CellRef& getReference() const; + void updateMarker(); - void updateMarker(); + osg::ref_ptr makeMoveOrScaleMarker(int axis); + osg::ref_ptr makeRotateMarker(int axis); - 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); - /// 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); - 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); + /// \param forceBaseToZero If this is a reference ignore the coordinates and place + /// it at 0, 0, 0 instead. - public: + ~Object(); - Object (CSMWorld::Data& data, osg::Group *cellNode, - const std::string& id, bool referenceable, - bool forceBaseToZero = false); - /// \param forceBaseToZero If this is a reference ignore the coordinates and place - /// it at 0, 0, 0 instead. + /// Mark the object as selected, selected objects show an outline effect + void setSelected(bool selected, const osg::Vec4f& color = osg::Vec4f(1, 1, 1, 1)); - ~Object(); + bool getSelected() const; - /// Mark the object as selected, selected objects show an outline effect - void setSelected(bool selected); + /// Mark Object as "snap target" + void setSnapTarget(bool isSnapTarget); - bool getSelected() const; + bool getSnapTarget() const; - /// Get object node with GUI graphics - osg::ref_ptr getRootNode(); + /// Get object node with GUI graphics + osg::ref_ptr getRootNode(); - /// Get object node without GUI graphics - osg::ref_ptr getBaseNode(); + /// Get object node without GUI graphics + osg::ref_ptr getBaseNode(); - /// \return Did this call result in a modification of the visual representation of - /// this object? - bool referenceableDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight); + /// \return Did this call result in a modification of the visual representation of + /// this object? + bool referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - /// \return Did this call result in a modification of the visual representation of - /// this object? - bool referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); + /// \return Did this call result in a modification of the visual representation of + /// this object? + bool referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end); - /// \return Did this call result in a modification of the visual representation of - /// this object? - bool referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + /// \return Did this call result in a modification of the visual representation of + /// this object? + bool referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - /// Reloads the underlying asset - void reloadAssets(); + /// Reloads the underlying asset + void reloadAssets(); - /// Returns an empty string if this is a refereceable-type object. - std::string getReferenceId() const; + /// Returns an empty string if this is a refereceable-type object. + std::string getReferenceId() const; - std::string getReferenceableId() const; + std::string getReferenceableId() const; - osg::ref_ptr getTag() const; + osg::ref_ptr getTag() const; - /// Is there currently an editing operation running on this object? - bool isEdited() const; + /// Is there currently an editing operation running on this object? + bool isEdited() const; - void setEdited (int flags); + void setEdited(int flags); - ESM::Position getPosition() const; + ESM::Position getPosition() const; - float getScale() const; + float getScale() const; - /// Set override position. - void setPosition (const float position[3]); + /// Set override position. + void setPosition(const float position[3]); - /// Set override rotation - void setRotation (const float rotation[3]); + /// Set override rotation + void setRotation(const float rotation[3]); - /// Set override scale - void setScale (float scale); + /// Set override scale + void setScale(float scale); - void setMarkerTransparency(float value); + void setMarkerTransparency(float value); - /// Apply override changes via command and end edit mode - void apply (CSMWorld::CommandMacro& commands); + /// Apply override changes via command and end edit mode + void apply(CSMWorld::CommandMacro& commands); - void setSubMode (int subMode); + void setSubMode(int subMode); - /// Erase all overrides and restore the visual representation of the object to its - /// true state. - void reset(); + /// Erase all overrides and restore the visual representation of the object to its + /// true state. + void reset(); }; } diff --git a/apps/opencs/view/render/orbitcameramode.cpp b/apps/opencs/view/render/orbitcameramode.cpp index c81402ed1e5..26fc015cf18 100644 --- a/apps/opencs/view/render/orbitcameramode.cpp +++ b/apps/opencs/view/render/orbitcameramode.cpp @@ -2,32 +2,37 @@ #include +#include + #include "../../model/prefs/shortcut.hpp" +#include + #include "worldspacewidget.hpp" +namespace CSVWidget +{ + class SceneToolbar; +} + namespace CSVRender { - OrbitCameraMode::OrbitCameraMode(WorldspaceWidget* worldspaceWidget, const QIcon& icon, const QString& tooltip, - QWidget* parent) + OrbitCameraMode::OrbitCameraMode( + WorldspaceWidget* worldspaceWidget, const QIcon& icon, const QString& tooltip, QWidget* parent) : ModeButton(icon, tooltip, parent) , mWorldspaceWidget(worldspaceWidget) , mCenterOnSelection(nullptr) { mCenterShortcut = new CSMPrefs::Shortcut("orbit-center-selection", worldspaceWidget); mCenterShortcut->enable(false); - connect(mCenterShortcut, SIGNAL(activated()), this, SLOT(centerSelection())); - } - - OrbitCameraMode::~OrbitCameraMode() - { + connect(mCenterShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &OrbitCameraMode::centerSelection); } void OrbitCameraMode::activate(CSVWidget::SceneToolbar* toolbar) { mCenterOnSelection = new QAction("Center on selected object", this); mCenterShortcut->associateAction(mCenterOnSelection); - connect(mCenterOnSelection, SIGNAL(triggered()), this, SLOT(centerSelection())); + connect(mCenterOnSelection, &QAction::triggered, this, &OrbitCameraMode::centerSelection); mCenterShortcut->enable(true); } diff --git a/apps/opencs/view/render/orbitcameramode.hpp b/apps/opencs/view/render/orbitcameramode.hpp index 10bc97b0f1a..30a92fdb4e1 100644 --- a/apps/opencs/view/render/orbitcameramode.hpp +++ b/apps/opencs/view/render/orbitcameramode.hpp @@ -1,10 +1,18 @@ #ifndef CSV_RENDER_ORBITCAMERAPICKMODE_H #define CSV_RENDER_ORBITCAMERAPICKMODE_H -#include - #include "../widget/modebutton.hpp" +class QAction; +class QMenu; +class QObject; +class QWidget; + +namespace CSVWidget +{ + class SceneToolbar; +} + namespace CSMPrefs { class Shortcut; @@ -16,27 +24,25 @@ namespace CSVRender class OrbitCameraMode : public CSVWidget::ModeButton { - Q_OBJECT - - public: - - OrbitCameraMode(WorldspaceWidget* worldspaceWidget, const QIcon& icon, const QString& tooltip = "", - QWidget* parent = nullptr); - ~OrbitCameraMode(); + Q_OBJECT - void activate(CSVWidget::SceneToolbar* toolbar) override; - void deactivate(CSVWidget::SceneToolbar* toolbar) override; - bool createContextMenu(QMenu* menu) override; + public: + OrbitCameraMode(WorldspaceWidget* worldspaceWidget, const QIcon& icon, const QString& tooltip = "", + QWidget* parent = nullptr); + ~OrbitCameraMode() override = default; - private: + void activate(CSVWidget::SceneToolbar* toolbar) override; + void deactivate(CSVWidget::SceneToolbar* toolbar) override; + bool createContextMenu(QMenu* menu) override; - WorldspaceWidget* mWorldspaceWidget; - QAction* mCenterOnSelection; - CSMPrefs::Shortcut* mCenterShortcut; + private: + WorldspaceWidget* mWorldspaceWidget; + QAction* mCenterOnSelection; + CSMPrefs::Shortcut* mCenterShortcut; - private slots: + private slots: - void centerSelection(); + void centerSelection(); }; } diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index a0b4de979df..3fd35b7740b 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -1,27 +1,59 @@ #include "pagedworldspacewidget.hpp" +#include +#include #include #include #include - -#include -#include - +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include #include +#include + +#include +#include +#include +#include #include "../../model/prefs/shortcut.hpp" #include "../../model/world/idtable.hpp" -#include "../widget/scenetooltoggle2.hpp" #include "../widget/scenetoolmode.hpp" +#include "../widget/scenetooltoggle2.hpp" +#include "cellarrow.hpp" #include "editmode.hpp" #include "mask.hpp" -#include "cameracontroller.hpp" -#include "cellarrow.hpp" -#include "terraintexturemode.hpp" #include "terrainshapemode.hpp" +#include "terraintexturemode.hpp" + +class QWidget; + +namespace CSMWorld +{ + struct Cell; +} + +namespace CSVWidget +{ + class SceneToolbar; +} bool CSVRender::PagedWorldspaceWidget::adjustCells() { @@ -31,32 +63,31 @@ bool CSVRender::PagedWorldspaceWidget::adjustCells() { // remove/update - std::map::iterator iter (mCells.begin()); + std::map::iterator iter(mCells.begin()); - while (iter!=mCells.end()) + while (iter != mCells.end()) { - if (!mSelection.has (iter->first)) + if (!mSelection.has(iter->first)) { // remove delete iter->second; - mCells.erase (iter++); + mCells.erase(iter++); modified = true; } else { // update - int index = cells.searchId (iter->first.getId (mWorldspace)); + const int index = cells.searchId(ESM::RefId::stringRefId(iter->first.getId(mWorldspace))); - bool deleted = index==-1 || - cells.getRecord (index).mState==CSMWorld::RecordBase::State_Deleted; + bool deleted = index == -1 || cells.getRecord(index).mState == CSMWorld::RecordBase::State_Deleted; - if (deleted!=iter->second->isDeleted()) + if (deleted != iter->second->isDeleted()) { modified = true; - std::unique_ptr cell (new Cell (mDocument.getData(), mRootNode, - iter->first.getId (mWorldspace), deleted)); + auto cell + = std::make_unique(mDocument, mRootNode, iter->first.getId(mWorldspace), deleted, true); delete iter->second; iter->second = cell.release(); @@ -67,8 +98,8 @@ bool CSVRender::PagedWorldspaceWidget::adjustCells() // TODO check if name or region field has changed (cell marker) // FIXME: config setting - //std::string name = cells.getRecord(index).get().mName; - //std::string region = cells.getRecord(index).get().mRegion; + // std::string name = cells.getRecord(index).get().mName; + // std::string region = cells.getRecord(index).get().mRegion; modified = true; } @@ -79,79 +110,79 @@ bool CSVRender::PagedWorldspaceWidget::adjustCells() } // add - for (CSMWorld::CellSelection::Iterator iter (mSelection.begin()); iter!=mSelection.end(); - ++iter) + for (CSMWorld::CellSelection::Iterator iter(mSelection.begin()); iter != mSelection.end(); ++iter) { - if (mCells.find (*iter)==mCells.end()) + if (mCells.find(*iter) == mCells.end()) { - addCellToScene (*iter); + addCellToScene(*iter); modified = true; } } if (modified) { - for (std::map::const_iterator iter (mCells.begin()); - iter!=mCells.end(); ++iter) + for (std::map::const_iterator iter(mCells.begin()); iter != mCells.end(); + ++iter) { int mask = 0; - for (int i=CellArrow::Direction_North; i<=CellArrow::Direction_East; i *= 2) + for (int i = CellArrow::Direction_North; i <= CellArrow::Direction_East; i *= 2) { - CSMWorld::CellCoordinates coordinates (iter->second->getCoordinates()); + CSMWorld::CellCoordinates coordinates(iter->second->getCoordinates()); switch (i) { - case CellArrow::Direction_North: coordinates = coordinates.move (0, 1); break; - case CellArrow::Direction_West: coordinates = coordinates.move (-1, 0); break; - case CellArrow::Direction_South: coordinates = coordinates.move (0, -1); break; - case CellArrow::Direction_East: coordinates = coordinates.move (1, 0); break; + case CellArrow::Direction_North: + coordinates = coordinates.move(0, 1); + break; + case CellArrow::Direction_West: + coordinates = coordinates.move(-1, 0); + break; + case CellArrow::Direction_South: + coordinates = coordinates.move(0, -1); + break; + case CellArrow::Direction_East: + coordinates = coordinates.move(1, 0); + break; } - if (!mSelection.has (coordinates)) + if (!mSelection.has(coordinates)) mask |= i; } - iter->second->setCellArrows (mask); + iter->second->setCellArrows(mask); } } return modified; } -void CSVRender::PagedWorldspaceWidget::addVisibilitySelectorButtons ( - CSVWidget::SceneToolToggle2 *tool) +void CSVRender::PagedWorldspaceWidget::addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool) { - WorldspaceWidget::addVisibilitySelectorButtons (tool); - tool->addButton (Button_Terrain, Mask_Terrain, "Terrain"); - tool->addButton (Button_Fog, Mask_Fog, "Fog", "", true); + WorldspaceWidget::addVisibilitySelectorButtons(tool); + tool->addButton(Button_Terrain, Mask_Terrain, "Terrain"); } -void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons ( - CSVWidget::SceneToolMode *tool) +void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons(CSVWidget::SceneToolMode* tool) { - WorldspaceWidget::addEditModeSelectorButtons (tool); + WorldspaceWidget::addEditModeSelectorButtons(tool); /// \todo replace EditMode with suitable subclasses - tool->addButton ( - new TerrainShapeMode (this, mRootNode, tool), "terrain-shape"); - tool->addButton ( - new TerrainTextureMode (this, mRootNode, tool), "terrain-texture"); - tool->addButton ( - new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain vertex paint editing"), - "terrain-vertex"); - tool->addButton ( - new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain movement"), - "terrain-move"); + tool->addButton(new TerrainShapeMode(this, mRootNode, tool), "terrain-shape"); + tool->addButton(new TerrainTextureMode(this, mRootNode, tool), "terrain-texture"); + const QIcon vertexIcon = Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-vertex-paint"); + const QIcon movementIcon = Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-movement"); + tool->addButton(new EditMode(this, vertexIcon, Mask_Reference, "Terrain vertex paint editing"), "terrain-vertex"); + tool->addButton(new EditMode(this, movementIcon, Mask_Reference, "Terrain movement"), "terrain-move"); } -void CSVRender::PagedWorldspaceWidget::handleInteractionPress (const WorldspaceHitResult& hit, InteractionType type) +void CSVRender::PagedWorldspaceWidget::handleInteractionPress(const WorldspaceHitResult& hit, InteractionType type) { - if (hit.tag && hit.tag->getMask()==Mask_CellArrow) + if (hit.tag && hit.tag->getMask() == Mask_CellArrow) { - if (CellArrowTag *cellArrowTag = dynamic_cast (hit.tag.get())) + if (CellArrowTag* cellArrowTag = dynamic_cast(hit.tag.get())) { - CellArrow *arrow = cellArrowTag->getCellArrow(); + CellArrow* arrow = cellArrowTag->getCellArrow(); CSMWorld::CellCoordinates coordinates = arrow->getCoordinates(); @@ -162,41 +193,49 @@ void CSVRender::PagedWorldspaceWidget::handleInteractionPress (const WorldspaceH switch (direction) { - case CellArrow::Direction_North: y = 1; break; - case CellArrow::Direction_West: x = -1; break; - case CellArrow::Direction_South: y = -1; break; - case CellArrow::Direction_East: x = 1; break; + case CellArrow::Direction_North: + y = 1; + break; + case CellArrow::Direction_West: + x = -1; + break; + case CellArrow::Direction_South: + y = -1; + break; + case CellArrow::Direction_East: + x = 1; + break; } bool modified = false; if (type == InteractionType_PrimarySelect) { - addCellSelection (x, y); + addCellSelection(x, y); modified = true; } else if (type == InteractionType_SecondarySelect) { - moveCellSelection (x, y); + moveCellSelection(x, y); modified = true; } else // Primary/SecondaryEdit { - CSMWorld::CellCoordinates newCoordinates = coordinates.move (x, y); + CSMWorld::CellCoordinates newCoordinates = coordinates.move(x, y); - if (mCells.find (newCoordinates)==mCells.end()) + if (mCells.find(newCoordinates) == mCells.end()) { - addCellToScene (newCoordinates); - mSelection.add (newCoordinates); + addCellToScene(newCoordinates); + mSelection.add(newCoordinates); modified = true; } if (type == InteractionType_SecondaryEdit) { - if (mCells.find (coordinates)!=mCells.end()) + if (mCells.find(coordinates) != mCells.end()) { - removeCellFromScene (coordinates); - mSelection.remove (coordinates); + removeCellFromScene(coordinates); + mSelection.remove(coordinates); modified = true; } } @@ -209,73 +248,61 @@ void CSVRender::PagedWorldspaceWidget::handleInteractionPress (const WorldspaceH } } - WorldspaceWidget::handleInteractionPress (hit, type); + WorldspaceWidget::handleInteractionPress(hit, type); } -void CSVRender::PagedWorldspaceWidget::referenceableDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSVRender::PagedWorldspaceWidget::referenceableDataChanged( + const QModelIndex& topLeft, const QModelIndex& bottomRight) { - for (std::map::iterator iter (mCells.begin()); - iter!=mCells.end(); ++iter) - if (iter->second->referenceableDataChanged (topLeft, bottomRight)) + for (std::map::iterator iter(mCells.begin()); iter != mCells.end(); ++iter) + if (iter->second->referenceableDataChanged(topLeft, bottomRight)) flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::referenceableAboutToBeRemoved ( - const QModelIndex& parent, int start, int end) +void CSVRender::PagedWorldspaceWidget::referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end) { - for (std::map::iterator iter (mCells.begin()); - iter!=mCells.end(); ++iter) - if (iter->second->referenceableAboutToBeRemoved (parent, start, end)) + for (std::map::iterator iter(mCells.begin()); iter != mCells.end(); ++iter) + if (iter->second->referenceableAboutToBeRemoved(parent, start, end)) flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::referenceableAdded (const QModelIndex& parent, - int start, int end) +void CSVRender::PagedWorldspaceWidget::referenceableAdded(const QModelIndex& parent, int start, int end) { - CSMWorld::IdTable& referenceables = dynamic_cast ( - *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_Referenceables)); + CSMWorld::IdTable& referenceables = dynamic_cast( + *mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_Referenceables)); - for (std::map::iterator iter (mCells.begin()); - iter!=mCells.end(); ++iter) + for (std::map::iterator iter(mCells.begin()); iter != mCells.end(); ++iter) { - QModelIndex topLeft = referenceables.index (start, 0); - QModelIndex bottomRight = - referenceables.index (end, referenceables.columnCount()); + QModelIndex topLeft = referenceables.index(start, 0); + QModelIndex bottomRight = referenceables.index(end, referenceables.columnCount()); - if (iter->second->referenceableDataChanged (topLeft, bottomRight)) + if (iter->second->referenceableDataChanged(topLeft, bottomRight)) flagAsModified(); } } -void CSVRender::PagedWorldspaceWidget::referenceDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSVRender::PagedWorldspaceWidget::referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { - for (std::map::iterator iter (mCells.begin()); - iter!=mCells.end(); ++iter) - if (iter->second->referenceDataChanged (topLeft, bottomRight)) + for (std::map::iterator iter(mCells.begin()); iter != mCells.end(); ++iter) + if (iter->second->referenceDataChanged(topLeft, bottomRight)) flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::referenceAboutToBeRemoved (const QModelIndex& parent, - int start, int end) +void CSVRender::PagedWorldspaceWidget::referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end) { - for (std::map::iterator iter (mCells.begin()); - iter!=mCells.end(); ++iter) - if (iter->second->referenceAboutToBeRemoved (parent, start, end)) + for (std::map::iterator iter(mCells.begin()); iter != mCells.end(); ++iter) + if (iter->second->referenceAboutToBeRemoved(parent, start, end)) flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::referenceAdded (const QModelIndex& parent, int start, - int end) +void CSVRender::PagedWorldspaceWidget::referenceAdded(const QModelIndex& parent, int start, int end) { - for (std::map::iterator iter (mCells.begin()); - iter!=mCells.end(); ++iter) - if (iter->second->referenceAdded (parent, start, end)) + for (std::map::iterator iter(mCells.begin()); iter != mCells.end(); ++iter) + if (iter->second->referenceAdded(parent, start, end)) flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::pathgridDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +void CSVRender::PagedWorldspaceWidget::pathgridDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); @@ -307,7 +334,7 @@ void CSVRender::PagedWorldspaceWidget::pathgridDataChanged (const QModelIndex& t } } -void CSVRender::PagedWorldspaceWidget::pathgridAboutToBeRemoved (const QModelIndex& parent, int start, int end) +void CSVRender::PagedWorldspaceWidget::pathgridAboutToBeRemoved(const QModelIndex& parent, int start, int end) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); @@ -331,7 +358,7 @@ void CSVRender::PagedWorldspaceWidget::pathgridAboutToBeRemoved (const QModelInd void CSVRender::PagedWorldspaceWidget::pathgridAdded(const QModelIndex& parent, int start, int end) { - const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); + const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); if (!parent.isValid()) { @@ -350,13 +377,13 @@ void CSVRender::PagedWorldspaceWidget::pathgridAdded(const QModelIndex& parent, } } -void CSVRender::PagedWorldspaceWidget::landDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +void CSVRender::PagedWorldspaceWidget::landDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { for (int r = topLeft.row(); r <= bottomRight.row(); ++r) { - std::string id = mDocument.getData().getLand().getId(r); + const auto& id = mDocument.getData().getLand().getId(r); - auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id).first); + auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id.getRefIdString()).first); if (cellIt != mCells.end()) { cellIt->second->landDataChanged(topLeft, bottomRight); @@ -365,13 +392,13 @@ void CSVRender::PagedWorldspaceWidget::landDataChanged (const QModelIndex& topLe } } -void CSVRender::PagedWorldspaceWidget::landAboutToBeRemoved (const QModelIndex& parent, int start, int end) +void CSVRender::PagedWorldspaceWidget::landAboutToBeRemoved(const QModelIndex& parent, int start, int end) { for (int r = start; r <= end; ++r) { - std::string id = mDocument.getData().getLand().getId(r); + const auto& id = mDocument.getData().getLand().getId(r); - auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id).first); + auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id.getRefIdString()).first); if (cellIt != mCells.end()) { cellIt->second->landAboutToBeRemoved(parent, start, end); @@ -380,13 +407,13 @@ void CSVRender::PagedWorldspaceWidget::landAboutToBeRemoved (const QModelIndex& } } -void CSVRender::PagedWorldspaceWidget::landAdded (const QModelIndex& parent, int start, int end) +void CSVRender::PagedWorldspaceWidget::landAdded(const QModelIndex& parent, int start, int end) { for (int r = start; r <= end; ++r) { - std::string id = mDocument.getData().getLand().getId(r); + const auto& id = mDocument.getData().getLand().getId(r); - auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id).first); + auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id.getRefIdString()).first); if (cellIt != mCells.end()) { cellIt->second->landAdded(parent, start, end); @@ -395,28 +422,28 @@ void CSVRender::PagedWorldspaceWidget::landAdded (const QModelIndex& parent, int } } -void CSVRender::PagedWorldspaceWidget::landTextureDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +void CSVRender::PagedWorldspaceWidget::landTextureDataChanged( + const QModelIndex& topLeft, const QModelIndex& bottomRight) { for (auto cellIt : mCells) cellIt.second->landTextureChanged(topLeft, bottomRight); flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end) +void CSVRender::PagedWorldspaceWidget::landTextureAboutToBeRemoved(const QModelIndex& parent, int start, int end) { for (auto cellIt : mCells) cellIt.second->landTextureAboutToBeRemoved(parent, start, end); flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::landTextureAdded (const QModelIndex& parent, int start, int end) +void CSVRender::PagedWorldspaceWidget::landTextureAdded(const QModelIndex& parent, int start, int end) { for (auto cellIt : mCells) cellIt.second->landTextureAdded(parent, start, end); flagAsModified(); } - std::string CSVRender::PagedWorldspaceWidget::getStartupInstruction() { osg::Vec3d eye, center, up; @@ -425,84 +452,73 @@ std::string CSVRender::PagedWorldspaceWidget::getStartupInstruction() std::ostringstream stream; - stream - << "player->position " - << position.x() << ", " << position.y() << ", " << position.z() - << ", 0"; + stream << "player->position " << position.x() << ", " << position.y() << ", " << position.z() << ", 0"; return stream.str(); } -void CSVRender::PagedWorldspaceWidget::addCellToScene ( - const CSMWorld::CellCoordinates& coordinates) +void CSVRender::PagedWorldspaceWidget::addCellToScene(const CSMWorld::CellCoordinates& coordinates) { const CSMWorld::IdCollection& cells = mDocument.getData().getCells(); - int index = cells.searchId (coordinates.getId (mWorldspace)); + const int index = cells.searchId(ESM::RefId::stringRefId(coordinates.getId(mWorldspace))); - bool deleted = index==-1 || - cells.getRecord (index).mState==CSMWorld::RecordBase::State_Deleted; + bool deleted = index == -1 || cells.getRecord(index).mState == CSMWorld::RecordBase::State_Deleted; - std::unique_ptr cell ( - new Cell (mDocument.getData(), mRootNode, coordinates.getId (mWorldspace), - deleted)); - EditMode *editMode = getEditMode(); - cell->setSubMode (editMode->getSubMode(), editMode->getInteractionMask()); + auto cell = std::make_unique(mDocument, mRootNode, coordinates.getId(mWorldspace), deleted, true); + EditMode* editMode = getEditMode(); + cell->setSubMode(editMode->getSubMode(), editMode->getInteractionMask()); - mCells.insert (std::make_pair (coordinates, cell.release())); + mCells.insert(std::make_pair(coordinates, cell.release())); } -void CSVRender::PagedWorldspaceWidget::removeCellFromScene ( - const CSMWorld::CellCoordinates& coordinates) +void CSVRender::PagedWorldspaceWidget::removeCellFromScene(const CSMWorld::CellCoordinates& coordinates) { - std::map::iterator iter = mCells.find (coordinates); + std::map::iterator iter = mCells.find(coordinates); - if (iter!=mCells.end()) + if (iter != mCells.end()) { delete iter->second; - mCells.erase (iter); + mCells.erase(iter); } } -void CSVRender::PagedWorldspaceWidget::addCellSelection (int x, int y) +void CSVRender::PagedWorldspaceWidget::addCellSelection(int x, int y) { CSMWorld::CellSelection newSelection = mSelection; - newSelection.move (x, y); + newSelection.move(x, y); - for (CSMWorld::CellSelection::Iterator iter (newSelection.begin()); iter!=newSelection.end(); - ++iter) + for (CSMWorld::CellSelection::Iterator iter(newSelection.begin()); iter != newSelection.end(); ++iter) { - if (mCells.find (*iter)==mCells.end()) + if (mCells.find(*iter) == mCells.end()) { - addCellToScene (*iter); - mSelection.add (*iter); + addCellToScene(*iter); + mSelection.add(*iter); } } } -void CSVRender::PagedWorldspaceWidget::moveCellSelection (int x, int y) +void CSVRender::PagedWorldspaceWidget::moveCellSelection(int x, int y) { CSMWorld::CellSelection newSelection = mSelection; - newSelection.move (x, y); + newSelection.move(x, y); - for (CSMWorld::CellSelection::Iterator iter (mSelection.begin()); iter!=mSelection.end(); - ++iter) + for (CSMWorld::CellSelection::Iterator iter(mSelection.begin()); iter != mSelection.end(); ++iter) { - if (!newSelection.has (*iter)) - removeCellFromScene (*iter); + if (!newSelection.has(*iter)) + removeCellFromScene(*iter); } - for (CSMWorld::CellSelection::Iterator iter (newSelection.begin()); iter!=newSelection.end(); - ++iter) + for (CSMWorld::CellSelection::Iterator iter(newSelection.begin()); iter != newSelection.end(); ++iter) { - if (!mSelection.has (*iter)) - addCellToScene (*iter); + if (!mSelection.has(*iter)) + addCellToScene(*iter); } - mSelection = newSelection; + mSelection = std::move(newSelection); } -void CSVRender::PagedWorldspaceWidget::addCellToSceneFromCamera (int offsetX, int offsetY) +void CSVRender::PagedWorldspaceWidget::addCellToSceneFromCamera(int offsetX, int offsetY) { osg::Vec3f eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); @@ -521,79 +537,76 @@ void CSVRender::PagedWorldspaceWidget::addCellToSceneFromCamera (int offsetX, in } } -CSVRender::PagedWorldspaceWidget::PagedWorldspaceWidget (QWidget* parent, CSMDoc::Document& document) -: WorldspaceWidget (document, parent), mDocument (document), mWorldspace ("std::default"), - mControlElements(nullptr), mDisplayCellCoord(true) +CSVRender::PagedWorldspaceWidget::PagedWorldspaceWidget(QWidget* parent, CSMDoc::Document& document) + : WorldspaceWidget(document, parent) + , mDocument(document) + , mWorldspace("std::default") + , mControlElements(nullptr) + , mDisplayCellCoord(true) { - QAbstractItemModel *cells = - document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells); + QAbstractItemModel* cells = document.getData().getTableModel(CSMWorld::UniversalId::Type_Cells); - connect (cells, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (cellDataChanged (const QModelIndex&, const QModelIndex&))); - connect (cells, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), - this, SLOT (cellRemoved (const QModelIndex&, int, int))); - connect (cells, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (cellAdded (const QModelIndex&, int, int))); + connect(cells, &QAbstractItemModel::dataChanged, this, &PagedWorldspaceWidget::cellDataChanged); + connect(cells, &QAbstractItemModel::rowsRemoved, this, &PagedWorldspaceWidget::cellRemoved); + connect(cells, &QAbstractItemModel::rowsInserted, this, &PagedWorldspaceWidget::cellAdded); - connect (&document.getData(), SIGNAL (assetTablesChanged ()), - this, SLOT (assetTablesChanged ())); + connect(&document.getData(), &CSMWorld::Data::assetTablesChanged, this, &PagedWorldspaceWidget::assetTablesChanged); - QAbstractItemModel *lands = document.getData().getTableModel (CSMWorld::UniversalId::Type_Lands); + QAbstractItemModel* lands = document.getData().getTableModel(CSMWorld::UniversalId::Type_Lands); - connect (lands, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (landDataChanged (const QModelIndex&, const QModelIndex&))); - connect (lands, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (landAboutToBeRemoved (const QModelIndex&, int, int))); - connect (lands, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (landAdded (const QModelIndex&, int, int))); + connect(lands, &QAbstractItemModel::dataChanged, this, &PagedWorldspaceWidget::landDataChanged); + connect(lands, &QAbstractItemModel::rowsAboutToBeRemoved, this, &PagedWorldspaceWidget::landAboutToBeRemoved); + connect(lands, &QAbstractItemModel::rowsInserted, this, &PagedWorldspaceWidget::landAdded); - QAbstractItemModel *ltexs = document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures); + QAbstractItemModel* ltexs = document.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures); - connect (ltexs, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (landTextureDataChanged (const QModelIndex&, const QModelIndex&))); - connect (ltexs, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (landTextureAboutToBeRemoved (const QModelIndex&, int, int))); - connect (ltexs, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (landTextureAdded (const QModelIndex&, int, int))); + connect(ltexs, &QAbstractItemModel::dataChanged, this, &PagedWorldspaceWidget::landTextureDataChanged); + connect( + ltexs, &QAbstractItemModel::rowsAboutToBeRemoved, this, &PagedWorldspaceWidget::landTextureAboutToBeRemoved); + connect(ltexs, &QAbstractItemModel::rowsInserted, this, &PagedWorldspaceWidget::landTextureAdded); // Shortcuts CSMPrefs::Shortcut* loadCameraCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-cell", this); - connect(loadCameraCellShortcut, SIGNAL(activated()), this, SLOT(loadCameraCell())); + connect(loadCameraCellShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, + &PagedWorldspaceWidget::loadCameraCell); CSMPrefs::Shortcut* loadCameraEastCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-eastcell", this); - connect(loadCameraEastCellShortcut, SIGNAL(activated()), this, SLOT(loadEastCell())); + connect(loadCameraEastCellShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, + &PagedWorldspaceWidget::loadEastCell); CSMPrefs::Shortcut* loadCameraNorthCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-northcell", this); - connect(loadCameraNorthCellShortcut, SIGNAL(activated()), this, SLOT(loadNorthCell())); + connect(loadCameraNorthCellShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, + &PagedWorldspaceWidget::loadNorthCell); CSMPrefs::Shortcut* loadCameraWestCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-westcell", this); - connect(loadCameraWestCellShortcut, SIGNAL(activated()), this, SLOT(loadWestCell())); + connect(loadCameraWestCellShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, + &PagedWorldspaceWidget::loadWestCell); CSMPrefs::Shortcut* loadCameraSouthCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-southcell", this); - connect(loadCameraSouthCellShortcut, SIGNAL(activated()), this, SLOT(loadSouthCell())); + connect(loadCameraSouthCellShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, + &PagedWorldspaceWidget::loadSouthCell); } CSVRender::PagedWorldspaceWidget::~PagedWorldspaceWidget() { - for (std::map::iterator iter (mCells.begin()); - iter!=mCells.end(); ++iter) + for (std::map::iterator iter(mCells.begin()); iter != mCells.end(); ++iter) { delete iter->second; } } -void CSVRender::PagedWorldspaceWidget::useViewHint (const std::string& hint) +void CSVRender::PagedWorldspaceWidget::useViewHint(const std::string& hint) { if (!hint.empty()) { CSMWorld::CellSelection selection; - if (hint[0]=='c') + if (hint[0] == 'c') { // syntax: c:#x1 y1; #x2 y2 (number of coordinate pairs can be 0 or larger) char ignore; - std::istringstream stream (hint.c_str()); + std::istringstream stream(hint.c_str()); if (stream >> ignore) { char ignore1; // : or ; @@ -603,61 +616,63 @@ void CSVRender::PagedWorldspaceWidget::useViewHint (const std::string& hint) // Loop through all the coordinates to add them to selection while (stream >> ignore1 >> ignore2 >> x >> y) - selection.add (CSMWorld::CellCoordinates (x, y)); + selection.add(CSMWorld::CellCoordinates(x, y)); // Mark that camera needs setup - mCamPositionSet=false; + mCamPositionSet = false; } } - else if (hint[0]=='r') + else if (hint[0] == 'r') { // syntax r:ref#number (e.g. r:ref#100) char ignore; - std::istringstream stream (hint.c_str()); + std::istringstream stream(hint.c_str()); if (stream >> ignore) // ignore r { char ignore1; // : or ; std::string refCode; // ref#number (e.g. ref#100) - while (stream >> ignore1 >> refCode) {} + while (stream >> ignore1 >> refCode) + { + } - //Find out cell coordinate - CSMWorld::IdTable& references = dynamic_cast ( - *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_References)); + // Find out cell coordinate + CSMWorld::IdTable& references = dynamic_cast( + *mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_References)); int cellColumn = references.findColumnIndex(CSMWorld::Columns::ColumnId_Cell); QVariant cell = references.data(references.getModelIndex(refCode, cellColumn)).value(); QString cellqs = cell.toString(); - std::istringstream streamCellCoord (cellqs.toStdString().c_str()); + std::istringstream streamCellCoord(cellqs.toStdString().c_str()); - if (streamCellCoord >> ignore) //ignore # + if (streamCellCoord >> ignore) // ignore # { // Current coordinate int x, y; // Loop through all the coordinates to add them to selection while (streamCellCoord >> x >> y) - selection.add (CSMWorld::CellCoordinates (x, y)); + selection.add(CSMWorld::CellCoordinates(x, y)); // Mark that camera needs setup - mCamPositionSet=false; + mCamPositionSet = false; } } } - setCellSelection (selection); + setCellSelection(selection); } } -void CSVRender::PagedWorldspaceWidget::setCellSelection (const CSMWorld::CellSelection& selection) +void CSVRender::PagedWorldspaceWidget::setCellSelection(const CSMWorld::CellSelection& selection) { mSelection = selection; if (adjustCells()) flagAsModified(); - emit cellSelectionChanged (mSelection); + emit cellSelectionChanged(mSelection); } const CSMWorld::CellSelection& CSVRender::PagedWorldspaceWidget::getCellSelection() const @@ -665,28 +680,28 @@ const CSMWorld::CellSelection& CSVRender::PagedWorldspaceWidget::getCellSelectio return mSelection; } -std::pair< int, int > CSVRender::PagedWorldspaceWidget::getCoordinatesFromId (const std::string& record) const +std::pair CSVRender::PagedWorldspaceWidget::getCoordinatesFromId(const std::string& record) const { - std::istringstream stream (record.c_str()); + std::istringstream stream(record.c_str()); char ignore; int x, y; stream >> ignore >> x >> y; return std::make_pair(x, y); } -bool CSVRender::PagedWorldspaceWidget::handleDrop ( - const std::vector< CSMWorld::UniversalId >& universalIdData, DropType type) +bool CSVRender::PagedWorldspaceWidget::handleDrop( + const std::vector& universalIdData, DropType type) { - if (WorldspaceWidget::handleDrop (universalIdData, type)) + if (WorldspaceWidget::handleDrop(universalIdData, type)) return true; - if (type!=Type_CellsExterior) + if (type != Type_CellsExterior) return false; bool selectionChanged = false; - for (unsigned i = 0; i < universalIdData.size(); ++i) + for (const auto& id : universalIdData) { - std::pair coordinates(getCoordinatesFromId(universalIdData[i].getId())); + std::pair coordinates(getCoordinatesFromId(id.getId())); if (mSelection.add(CSMWorld::CellCoordinates(coordinates.first, coordinates.second))) { selectionChanged = true; @@ -703,11 +718,12 @@ bool CSVRender::PagedWorldspaceWidget::handleDrop ( return true; } -CSVRender::WorldspaceWidget::dropRequirments CSVRender::PagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::DropType type) const +CSVRender::WorldspaceWidget::dropRequirments CSVRender::PagedWorldspaceWidget::getDropRequirements( + CSVRender::WorldspaceWidget::DropType type) const { - dropRequirments requirements = WorldspaceWidget::getDropRequirements (type); + dropRequirments requirements = WorldspaceWidget::getDropRequirements(type); - if (requirements!=ignored) + if (requirements != ignored) return requirements; switch (type) @@ -728,47 +744,44 @@ unsigned int CSVRender::PagedWorldspaceWidget::getVisibilityMask() const return WorldspaceWidget::getVisibilityMask() | mControlElements->getSelectionMask(); } -void CSVRender::PagedWorldspaceWidget::clearSelection (int elementMask) +void CSVRender::PagedWorldspaceWidget::clearSelection(int elementMask) { - for (std::map::iterator iter = mCells.begin(); - iter!=mCells.end(); ++iter) - iter->second->setSelection (elementMask, Cell::Selection_Clear); + for (std::map::iterator iter = mCells.begin(); iter != mCells.end(); ++iter) + iter->second->setSelection(elementMask, Cell::Selection_Clear); flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::invertSelection (int elementMask) +void CSVRender::PagedWorldspaceWidget::invertSelection(int elementMask) { - for (std::map::iterator iter = mCells.begin(); - iter!=mCells.end(); ++iter) - iter->second->setSelection (elementMask, Cell::Selection_Invert); + for (std::map::iterator iter = mCells.begin(); iter != mCells.end(); ++iter) + iter->second->setSelection(elementMask, Cell::Selection_Invert); flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::selectAll (int elementMask) +void CSVRender::PagedWorldspaceWidget::selectAll(int elementMask) { - for (std::map::iterator iter = mCells.begin(); - iter!=mCells.end(); ++iter) - iter->second->setSelection (elementMask, Cell::Selection_All); + for (std::map::iterator iter = mCells.begin(); iter != mCells.end(); ++iter) + iter->second->setSelection(elementMask, Cell::Selection_All); flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::selectAllWithSameParentId (int elementMask) +void CSVRender::PagedWorldspaceWidget::selectAllWithSameParentId(int elementMask) { - for (std::map::iterator iter = mCells.begin(); - iter!=mCells.end(); ++iter) - iter->second->selectAllWithSameParentId (elementMask); + for (std::map::iterator iter = mCells.begin(); iter != mCells.end(); ++iter) + iter->second->selectAllWithSameParentId(elementMask); flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) +void CSVRender::PagedWorldspaceWidget::selectInsideCube( + const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) { for (auto& cell : mCells) { - cell.second->selectInsideCube (pointA, pointB, dragMode); + cell.second->selectInsideCube(pointA, pointB, dragMode); } } @@ -776,24 +789,22 @@ void CSVRender::PagedWorldspaceWidget::selectWithinDistance(const osg::Vec3d& po { for (auto& cell : mCells) { - cell.second->selectWithinDistance (point, distance, dragMode); + cell.second->selectWithinDistance(point, distance, dragMode); } } -std::string CSVRender::PagedWorldspaceWidget::getCellId (const osg::Vec3f& point) const +std::string CSVRender::PagedWorldspaceWidget::getCellId(const osg::Vec3f& point) const { - CSMWorld::CellCoordinates cellCoordinates ( - static_cast (std::floor (point.x() / Constants::CellSizeInUnits)), - static_cast (std::floor (point.y() / Constants::CellSizeInUnits))); + CSMWorld::CellCoordinates cellCoordinates(static_cast(std::floor(point.x() / Constants::CellSizeInUnits)), + static_cast(std::floor(point.y() / Constants::CellSizeInUnits))); - return cellCoordinates.getId (mWorldspace); + return cellCoordinates.getId(mWorldspace); } CSVRender::Cell* CSVRender::PagedWorldspaceWidget::getCell(const osg::Vec3d& point) const { - CSMWorld::CellCoordinates coords( - static_cast (std::floor (point.x() / Constants::CellSizeInUnits)), - static_cast (std::floor (point.y() / Constants::CellSizeInUnits))); + CSMWorld::CellCoordinates coords(static_cast(std::floor(point.x() / Constants::CellSizeInUnits)), + static_cast(std::floor(point.y() / Constants::CellSizeInUnits))); std::map::const_iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) @@ -811,14 +822,16 @@ CSVRender::Cell* CSVRender::PagedWorldspaceWidget::getCell(const CSMWorld::CellC return nullptr; } -void CSVRender::PagedWorldspaceWidget::setCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY, float height) +void CSVRender::PagedWorldspaceWidget::setCellAlteredHeight( + const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY, float height) { std::map::iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) searchResult->second->setAlteredHeight(inCellX, inCellY, height); } -float* CSVRender::PagedWorldspaceWidget::getCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY) +float* CSVRender::PagedWorldspaceWidget::getCellAlteredHeight( + const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY) { std::map::iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) @@ -832,89 +845,108 @@ void CSVRender::PagedWorldspaceWidget::resetAllAlteredHeights() cell.second->resetAlteredHeights(); } -std::vector > CSVRender::PagedWorldspaceWidget::getSelection ( +osg::ref_ptr CSVRender::PagedWorldspaceWidget::getSnapTarget(unsigned int elementMask) const +{ + osg::ref_ptr result; + + for (auto& [coords, cell] : mCells) + { + auto snapTarget = cell->getSnapTarget(elementMask); + if (snapTarget) + { + return snapTarget; + } + } + + return result; +} + +std::vector> CSVRender::PagedWorldspaceWidget::getSelection( unsigned int elementMask) const { - std::vector > result; + std::vector> result; - for (std::map::const_iterator iter = mCells.begin(); - iter!=mCells.end(); ++iter) + for (std::map::const_iterator iter = mCells.begin(); iter != mCells.end(); ++iter) { - std::vector > cellResult = - iter->second->getSelection (elementMask); + std::vector> cellResult = iter->second->getSelection(elementMask); - result.insert (result.end(), cellResult.begin(), cellResult.end()); + result.insert(result.end(), cellResult.begin(), cellResult.end()); } return result; } -std::vector > CSVRender::PagedWorldspaceWidget::getEdited ( +void CSVRender::PagedWorldspaceWidget::selectGroup(const std::vector& group) const +{ + for (const auto& [_, cell] : mCells) + cell->selectFromGroup(group); +} + +void CSVRender::PagedWorldspaceWidget::unhideAll() const +{ + for (const auto& [_, cell] : mCells) + cell->unhideAll(); +} + +std::vector> CSVRender::PagedWorldspaceWidget::getEdited( unsigned int elementMask) const { - std::vector > result; + std::vector> result; - for (std::map::const_iterator iter = mCells.begin(); - iter!=mCells.end(); ++iter) + for (std::map::const_iterator iter = mCells.begin(); iter != mCells.end(); ++iter) { - std::vector > cellResult = - iter->second->getEdited (elementMask); + std::vector> cellResult = iter->second->getEdited(elementMask); - result.insert (result.end(), cellResult.begin(), cellResult.end()); + result.insert(result.end(), cellResult.begin(), cellResult.end()); } return result; } -void CSVRender::PagedWorldspaceWidget::setSubMode (int subMode, unsigned int elementMask) +void CSVRender::PagedWorldspaceWidget::setSubMode(int subMode, unsigned int elementMask) { - for (std::map::const_iterator iter = mCells.begin(); - iter!=mCells.end(); ++iter) - iter->second->setSubMode (subMode, elementMask); + for (std::map::const_iterator iter = mCells.begin(); iter != mCells.end(); ++iter) + iter->second->setSubMode(subMode, elementMask); } -void CSVRender::PagedWorldspaceWidget::reset (unsigned int elementMask) +void CSVRender::PagedWorldspaceWidget::reset(unsigned int elementMask) { - for (std::map::const_iterator iter = mCells.begin(); - iter!=mCells.end(); ++iter) - iter->second->reset (elementMask); + for (std::map::const_iterator iter = mCells.begin(); iter != mCells.end(); ++iter) + iter->second->reset(elementMask); } -CSVWidget::SceneToolToggle2 *CSVRender::PagedWorldspaceWidget::makeControlVisibilitySelector ( - CSVWidget::SceneToolbar *parent) +CSVWidget::SceneToolToggle2* CSVRender::PagedWorldspaceWidget::makeControlVisibilitySelector( + CSVWidget::SceneToolbar* parent) { - mControlElements = new CSVWidget::SceneToolToggle2 (parent, - "Controls & Guides Visibility", ":scenetoolbar/scene-view-marker-c", ":scenetoolbar/scene-view-marker-"); + mControlElements = new CSVWidget::SceneToolToggle2(parent, "Controls & Guides Visibility", + ":scenetoolbar/scene-view-marker-c", ":scenetoolbar/scene-view-marker-"); - mControlElements->addButton (1, Mask_CellMarker, "Cell Marker"); - mControlElements->addButton (2, Mask_CellArrow, "Cell Arrows"); - mControlElements->addButton (4, Mask_CellBorder, "Cell Border"); + mControlElements->addButton(1, Mask_CellMarker, "Cell Marker"); + mControlElements->addButton(2, Mask_CellArrow, "Cell Arrows"); + mControlElements->addButton(4, Mask_CellBorder, "Cell Border"); - mControlElements->setSelectionMask (0xffffffff); + mControlElements->setSelectionMask(0xffffffff); - connect (mControlElements, SIGNAL (selectionChanged()), - this, SLOT (elementSelectionChanged())); + connect(mControlElements, &CSVWidget::SceneToolToggle2::selectionChanged, this, + &PagedWorldspaceWidget::elementSelectionChanged); return mControlElements; } -void CSVRender::PagedWorldspaceWidget::cellDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSVRender::PagedWorldspaceWidget::cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { /// \todo check if no selected cell is affected and do not update, if that is the case if (adjustCells()) flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::cellRemoved (const QModelIndex& parent, int start, - int end) +void CSVRender::PagedWorldspaceWidget::cellRemoved(const QModelIndex& parent, int start, int end) { if (adjustCells()) flagAsModified(); } -void CSVRender::PagedWorldspaceWidget::cellAdded (const QModelIndex& index, int start, - int end) +void CSVRender::PagedWorldspaceWidget::cellAdded(const QModelIndex& index, int start, int end) { /// \todo check if no selected cell is affected and do not update, if that is the case if (adjustCells()) @@ -923,8 +955,8 @@ void CSVRender::PagedWorldspaceWidget::cellAdded (const QModelIndex& index, int void CSVRender::PagedWorldspaceWidget::assetTablesChanged() { - std::map::iterator iter = mCells.begin(); - for ( ; iter != mCells.end(); ++iter) + std::map::iterator iter = mCells.begin(); + for (; iter != mCells.end(); ++iter) { iter->second->reloadAssets(); } diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index beab0c575b9..744cc7ccb9b 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -2,192 +2,216 @@ #define OPENCS_VIEW_PAGEDWORLDSPACEWIDGET_H #include +#include +#include +#include + +#include #include "../../model/world/cellselection.hpp" -#include "worldspacewidget.hpp" #include "cell.hpp" #include "instancedragmodes.hpp" +#include "worldspacewidget.hpp" + +class QModelIndex; +class QObject; +class QWidget; + +namespace osg +{ + class Vec3f; + template + class ref_ptr; +} + +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class UniversalId; +} namespace CSVWidget { - class SceneToolToggle; - class SceneToolToggle2; + class SceneToolToggle2; + class SceneToolMode; + class SceneToolBar; } namespace CSVRender { - class TextOverlay; - class OverlayMask; + class Cell; + class TagBase; class PagedWorldspaceWidget : public WorldspaceWidget { - Q_OBJECT - - CSMDoc::Document& mDocument; - CSMWorld::CellSelection mSelection; - std::map mCells; - std::string mWorldspace; - CSVWidget::SceneToolToggle2 *mControlElements; - bool mDisplayCellCoord; + Q_OBJECT - private: + CSMDoc::Document& mDocument; + CSMWorld::CellSelection mSelection; + std::map mCells; + std::string mWorldspace; + CSVWidget::SceneToolToggle2* mControlElements; + bool mDisplayCellCoord; - std::pair getCoordinatesFromId(const std::string& record) const; + private: + std::pair getCoordinatesFromId(const std::string& record) const; - /// Bring mCells into sync with mSelection again. - /// - /// \return Any cells added or removed? - bool adjustCells(); + /// Bring mCells into sync with mSelection again. + /// + /// \return Any cells added or removed? + bool adjustCells(); - void referenceableDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) override; + void referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) override; - void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; + void referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; - void referenceableAdded (const QModelIndex& index, int start, int end) override; + void referenceableAdded(const QModelIndex& index, int start, int end) override; - void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) override; + void referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) override; - void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; + void referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; - void referenceAdded (const QModelIndex& index, int start, int end) override; + void referenceAdded(const QModelIndex& index, int start, int end) override; - void pathgridDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) override; + void pathgridDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) override; - void pathgridAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; + void pathgridAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; - void pathgridAdded (const QModelIndex& parent, int start, int end) override; + void pathgridAdded(const QModelIndex& parent, int start, int end) override; - std::string getStartupInstruction() override; + std::string getStartupInstruction() override; - /// \note Does not update the view or any cell marker - void addCellToScene (const CSMWorld::CellCoordinates& coordinates); + /// \note Does not update the view or any cell marker + void addCellToScene(const CSMWorld::CellCoordinates& coordinates); - /// \note Does not update the view or any cell marker - /// - /// \note Calling this function for a cell that is not in the selection is a no-op. - void removeCellFromScene (const CSMWorld::CellCoordinates& coordinates); + /// \note Does not update the view or any cell marker + /// + /// \note Calling this function for a cell that is not in the selection is a no-op. + void removeCellFromScene(const CSMWorld::CellCoordinates& coordinates); - /// \note Does not update the view or any cell marker - void addCellSelection (int x, int y); + /// \note Does not update the view or any cell marker + void addCellSelection(int x, int y); - /// \note Does not update the view or any cell marker - void moveCellSelection (int x, int y); + /// \note Does not update the view or any cell marker + void moveCellSelection(int x, int y); - void addCellToSceneFromCamera (int offsetX, int offsetY); + void addCellToSceneFromCamera(int offsetX, int offsetY); - public: + public: + PagedWorldspaceWidget(QWidget* parent, CSMDoc::Document& document); + ///< \note Sets the cell area selection to an invalid value to indicate that currently + /// no cells are displayed. The cells to be displayed will be specified later through + /// hint system. - PagedWorldspaceWidget (QWidget *parent, CSMDoc::Document& document); - ///< \note Sets the cell area selection to an invalid value to indicate that currently - /// no cells are displayed. The cells to be displayed will be specified later through - /// hint system. + virtual ~PagedWorldspaceWidget(); - virtual ~PagedWorldspaceWidget(); + /// Decodes the the hint string to set of cell that are rendered. + void useViewHint(const std::string& hint) override; - /// Decodes the the hint string to set of cell that are rendered. - void useViewHint (const std::string& hint) override; + void setCellSelection(const CSMWorld::CellSelection& selection); - void setCellSelection(const CSMWorld::CellSelection& selection); + const CSMWorld::CellSelection& getCellSelection() const; - const CSMWorld::CellSelection& getCellSelection() const; + /// \return Drop handled? + bool handleDrop(const std::vector& data, DropType type) override; - /// \return Drop handled? - bool handleDrop (const std::vector& data, - DropType type) override; + dropRequirments getDropRequirements(DropType type) const override; - dropRequirments getDropRequirements(DropType type) const override; + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + virtual CSVWidget::SceneToolToggle2* makeControlVisibilitySelector(CSVWidget::SceneToolbar* parent); - /// \attention The created tool is not added to the toolbar (via addTool). Doing - /// that is the responsibility of the calling function. - virtual CSVWidget::SceneToolToggle2 *makeControlVisibilitySelector ( - CSVWidget::SceneToolbar *parent); + unsigned int getVisibilityMask() const override; - unsigned int getVisibilityMask() const override; + /// \param elementMask Elements to be affected by the clear operation + void clearSelection(int elementMask) override; - /// \param elementMask Elements to be affected by the clear operation - void clearSelection (int elementMask) override; + /// \param elementMask Elements to be affected by the select operation + void invertSelection(int elementMask) override; - /// \param elementMask Elements to be affected by the select operation - void invertSelection (int elementMask) override; + /// \param elementMask Elements to be affected by the select operation + void selectAll(int elementMask) override; - /// \param elementMask Elements to be affected by the select operation - void selectAll (int elementMask) override; + // Select everything that references the same ID as at least one of the elements + // already selected + // + /// \param elementMask Elements to be affected by the select operation + void selectAllWithSameParentId(int elementMask) override; - // Select everything that references the same ID as at least one of the elements - // already selected - // - /// \param elementMask Elements to be affected by the select operation - void selectAllWithSameParentId (int elementMask) override; + void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override; - void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override; + void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override; - void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override; + std::string getCellId(const osg::Vec3f& point) const override; - std::string getCellId (const osg::Vec3f& point) const override; + Cell* getCell(const osg::Vec3d& point) const override; - Cell* getCell(const osg::Vec3d& point) const override; + Cell* getCell(const CSMWorld::CellCoordinates& coords) const override; - Cell* getCell(const CSMWorld::CellCoordinates& coords) const override; + void setCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY, float height); - void setCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY, float height); + float* getCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY); - float* getCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY); + void resetAllAlteredHeights(); - void resetAllAlteredHeights(); + osg::ref_ptr getSnapTarget(unsigned int elementMask) const override; - std::vector > getSelection (unsigned int elementMask) - const override; + std::vector> getSelection(unsigned int elementMask) const override; - std::vector > getEdited (unsigned int elementMask) - const override; + void selectGroup(const std::vector& group) const override; - void setSubMode (int subMode, unsigned int elementMask) override; + void unhideAll() const override; - /// Erase all overrides and restore the visual representation to its true state. - void reset (unsigned int elementMask) override; + std::vector> getEdited(unsigned int elementMask) const override; - protected: + void setSubMode(int subMode, unsigned int elementMask) override; - void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool) override; + /// Erase all overrides and restore the visual representation to its true state. + void reset(unsigned int elementMask) override; - void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) override; + protected: + void addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool) override; - void handleInteractionPress (const WorldspaceHitResult& hit, InteractionType type) override; + void addEditModeSelectorButtons(CSVWidget::SceneToolMode* tool) override; - signals: + void handleInteractionPress(const WorldspaceHitResult& hit, InteractionType type) override; - void cellSelectionChanged (const CSMWorld::CellSelection& selection); + signals: - private slots: + void cellSelectionChanged(const CSMWorld::CellSelection& selection); - virtual void cellDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + private slots: - virtual void cellRemoved (const QModelIndex& parent, int start, int end); + virtual void cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - virtual void cellAdded (const QModelIndex& index, int start, int end); + virtual void cellRemoved(const QModelIndex& parent, int start, int end); - virtual void landDataChanged (const QModelIndex& topLeft, const QModelIndex& botomRight); - virtual void landAboutToBeRemoved (const QModelIndex& parent, int start, int end); - virtual void landAdded (const QModelIndex& parent, int start, int end); + virtual void cellAdded(const QModelIndex& index, int start, int end); - virtual void landTextureDataChanged (const QModelIndex& topLeft, const QModelIndex& botomRight); - virtual void landTextureAboutToBeRemoved (const QModelIndex& parent, int start, int end); - virtual void landTextureAdded (const QModelIndex& parent, int start, int end); + virtual void landDataChanged(const QModelIndex& topLeft, const QModelIndex& botomRight); + virtual void landAboutToBeRemoved(const QModelIndex& parent, int start, int end); + virtual void landAdded(const QModelIndex& parent, int start, int end); - void assetTablesChanged (); + virtual void landTextureDataChanged(const QModelIndex& topLeft, const QModelIndex& botomRight); + virtual void landTextureAboutToBeRemoved(const QModelIndex& parent, int start, int end); + virtual void landTextureAdded(const QModelIndex& parent, int start, int end); - void loadCameraCell(); + void assetTablesChanged(); - void loadEastCell(); + void loadCameraCell(); - void loadNorthCell(); + void loadEastCell(); - void loadWestCell(); + void loadNorthCell(); - void loadSouthCell(); + void loadWestCell(); + void loadSouthCell(); }; } diff --git a/apps/opencs/view/render/pathgrid.cpp b/apps/opencs/view/render/pathgrid.cpp index 470a3d09282..44e85e71930 100644 --- a/apps/opencs/view/render/pathgrid.cpp +++ b/apps/opencs/view/render/pathgrid.cpp @@ -1,37 +1,67 @@ #include "pathgrid.hpp" #include +#include +#include +#include +#include #include -#include +#include +#include #include #include +#include +#include #include -#include - +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include #include -#include "../../model/world/cell.hpp" -#include "../../model/world/commands.hpp" #include "../../model/world/commandmacro.hpp" +#include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtree.hpp" +#include "worldspacewidget.hpp" + +namespace osg +{ + class NodeVisitor; +} namespace CSVRender { class PathgridNodeCallback : public osg::NodeCallback { - public: - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override - { - PathgridTag* tag = static_cast(node->getUserData()); - tag->getPathgrid()->update(); - } + public: + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + PathgridTag* tag = static_cast(node->getUserData()); + tag->getPathgrid()->update(); + } }; PathgridTag::PathgridTag(Pathgrid* pathgrid) - : TagBase(Mask_Pathgrid), mPathgrid(pathgrid) + : TagBase(Mask_Pathgrid) + , mPathgrid(pathgrid) { } @@ -40,10 +70,13 @@ namespace CSVRender return mPathgrid; } - QString PathgridTag::getToolTip(bool hideBasics) const + QString PathgridTag::getToolTip(bool /*hideBasics*/, const WorldspaceHitResult& hit) const { QString text("Pathgrid: "); text += mPathgrid->getId().c_str(); + text += " ("; + text += QString::number(SceneUtil::getPathgridNode(static_cast(hit.index0))); + text += ")"; return text; } @@ -52,7 +85,7 @@ namespace CSVRender const CSMWorld::CellCoordinates& coordinates) : mData(data) , mPathgridCollection(mData.getPathgrids()) - , mId(pathgridId) + , mId(ESM::RefId::stringRefId(pathgridId)) , mCoords(coordinates) , mInterior(false) , mDragOrigin(0) @@ -66,15 +99,15 @@ namespace CSVRender { const float CoordScalar = ESM::Land::REAL_SIZE; - mBaseNode = new osg::PositionAttitudeTransform (); + mBaseNode = new osg::PositionAttitudeTransform(); mBaseNode->setPosition(osg::Vec3f(mCoords.getX() * CoordScalar, mCoords.getY() * CoordScalar, 0.f)); mBaseNode->setUserData(mTag); mBaseNode->setUpdateCallback(new PathgridNodeCallback()); mBaseNode->setNodeMask(Mask_Pathgrid); mParent->addChild(mBaseNode); - mPathgridGeode = new osg::Geode(); - mBaseNode->addChild(mPathgridGeode); + mPathgridGroup = new osg::Group(); + mBaseNode->addChild(mPathgridGroup); recreateGeometry(); @@ -98,7 +131,7 @@ namespace CSVRender const std::string& Pathgrid::getId() const { - return mId; + return mId.getRefIdString(); } bool Pathgrid::isSelected() const @@ -218,14 +251,15 @@ namespace CSVRender mUseOffset = false; mMoveOffset.set(0, 0, 0); - mPathgridGeode->removeDrawable(mDragGeometry); + mPathgridGroup->removeChild(mDragGeometry); mDragGeometry = nullptr; } void Pathgrid::applyPoint(CSMWorld::CommandMacro& commands, const osg::Vec3d& worldPos) { - CSMWorld::IdTree* model = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); - + CSMWorld::IdTree* model + = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); + const std::string& idString = mId.getRefIdString(); const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { @@ -235,23 +269,23 @@ namespace CSVRender int posY = clampToCell(static_cast(localCoords.y())); int posZ = clampToCell(static_cast(localCoords.z())); - int recordIndex = mPathgridCollection.getIndex (mId); + int recordIndex = mPathgridCollection.getIndex(mId); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints); - int posXColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, - CSMWorld::Columns::ColumnId_PathgridPosX); + int posXColumn + = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosX); - int posYColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, - CSMWorld::Columns::ColumnId_PathgridPosY); + int posYColumn + = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosY); - int posZColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, - CSMWorld::Columns::ColumnId_PathgridPosZ); + int posZColumn + = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosZ); QModelIndex parent = model->index(recordIndex, parentColumn); int row = static_cast(source->mPoints.size()); // Add node to end of list - commands.push(new CSMWorld::AddNestedCommand(*model, mId, row, parentColumn)); + commands.push(new CSMWorld::AddNestedCommand(*model, idString, row, parentColumn)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posXColumn, parent), posX)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posYColumn, parent), posY)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posZColumn, parent), posZ)); @@ -262,25 +296,25 @@ namespace CSVRender if (index == -1) { // Does not exist - commands.push(new CSMWorld::CreatePathgridCommand(*model, mId)); + commands.push(new CSMWorld::CreatePathgridCommand(*model, idString)); } else { source = &mPathgridCollection.getRecord(index).get(); // Deleted, so revert and remove all data - commands.push(new CSMWorld::RevertCommand(*model, mId)); + commands.push(new CSMWorld::RevertCommand(*model, idString)); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints); for (int row = source->mPoints.size() - 1; row >= 0; --row) { - commands.push(new CSMWorld::DeleteNestedCommand(*model, mId, row, parentColumn)); + commands.push(new CSMWorld::DeleteNestedCommand(*model, idString, row, parentColumn)); } parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges); for (int row = source->mEdges.size() - 1; row >= 0; --row) { - commands.push(new CSMWorld::DeleteNestedCommand(*model, mId, row, parentColumn)); + commands.push(new CSMWorld::DeleteNestedCommand(*model, idString, row, parentColumn)); } } } @@ -302,30 +336,30 @@ namespace CSVRender int recordIndex = mPathgridCollection.getIndex(mId); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints); - int posXColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, - CSMWorld::Columns::ColumnId_PathgridPosX); + int posXColumn + = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosX); - int posYColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, - CSMWorld::Columns::ColumnId_PathgridPosY); + int posYColumn + = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosY); - int posZColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, - CSMWorld::Columns::ColumnId_PathgridPosZ); + int posZColumn + = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosZ); QModelIndex parent = model->index(recordIndex, parentColumn); - for (size_t i = 0; i < mSelected.size(); ++i) + for (const auto& selected : mSelected) { - const CSMWorld::Pathgrid::Point& point = source->mPoints[mSelected[i]]; - int row = static_cast(mSelected[i]); + const CSMWorld::Pathgrid::Point& point = source->mPoints[selected]; + int row = static_cast(selected); - commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posXColumn, parent), - clampToCell(point.mX + offsetX))); + commands.push(new CSMWorld::ModifyCommand( + *model, model->index(row, posXColumn, parent), clampToCell(point.mX + offsetX))); - commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posYColumn, parent), - clampToCell(point.mY + offsetY))); + commands.push(new CSMWorld::ModifyCommand( + *model, model->index(row, posYColumn, parent), clampToCell(point.mY + offsetY))); - commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posZColumn, parent), - clampToCell(point.mZ + offsetZ))); + commands.push(new CSMWorld::ModifyCommand( + *model, model->index(row, posZColumn, parent), clampToCell(point.mZ + offsetZ))); } } } @@ -344,9 +378,9 @@ namespace CSVRender const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { - for (size_t i = 0; i < mSelected.size(); ++i) + for (const auto& selected : mSelected) { - addEdge(commands, *source, node, mSelected[i]); + addEdge(commands, *source, node, selected); } } } @@ -356,7 +390,8 @@ namespace CSVRender const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { - CSMWorld::IdTree* model = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); + CSMWorld::IdTree* model + = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); // Want to remove nodes from end of list first std::sort(mSelected.begin(), mSelected.end(), std::greater()); @@ -366,19 +401,20 @@ namespace CSVRender for (std::vector::iterator row = mSelected.begin(); row != mSelected.end(); ++row) { - commands.push(new CSMWorld::DeleteNestedCommand(*model, mId, static_cast(*row), parentColumn)); + commands.push(new CSMWorld::DeleteNestedCommand( + *model, mId.getRefIdString(), static_cast(*row), parentColumn)); } // Fix/remove edges - std::set > edgeRowsToRemove; + std::set> edgeRowsToRemove; parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges); - int edge0Column = mPathgridCollection.searchNestedColumnIndex(parentColumn, - CSMWorld::Columns::ColumnId_PathgridEdge0); + int edge0Column + = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge0); - int edge1Column = mPathgridCollection.searchNestedColumnIndex(parentColumn, - CSMWorld::Columns::ColumnId_PathgridEdge1); + int edge1Column + = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge1); QModelIndex parent = model->index(recordIndex, parentColumn); @@ -388,9 +424,9 @@ namespace CSVRender int adjustment1 = 0; // Determine necessary adjustment - for (std::vector::iterator point = mSelected.begin(); point != mSelected.end(); ++point) + for (const auto point : mSelected) { - if (source->mEdges[edge].mV0 == *point || source->mEdges[edge].mV1 == *point) + if (source->mEdges[edge].mV0 == point || source->mEdges[edge].mV1 == point) { edgeRowsToRemove.insert(static_cast(edge)); @@ -399,32 +435,31 @@ namespace CSVRender break; } - if (source->mEdges[edge].mV0 > *point) + if (source->mEdges[edge].mV0 > point) --adjustment0; - if (source->mEdges[edge].mV1 > *point) + if (source->mEdges[edge].mV1 > point) --adjustment1; } if (adjustment0 != 0) { int adjustedEdge = source->mEdges[edge].mV0 + adjustment0; - commands.push(new CSMWorld::ModifyCommand(*model, model->index(edge, edge0Column, parent), - adjustedEdge)); + commands.push( + new CSMWorld::ModifyCommand(*model, model->index(edge, edge0Column, parent), adjustedEdge)); } if (adjustment1 != 0) { int adjustedEdge = source->mEdges[edge].mV1 + adjustment1; - commands.push(new CSMWorld::ModifyCommand(*model, model->index(edge, edge1Column, parent), - adjustedEdge)); + commands.push( + new CSMWorld::ModifyCommand(*model, model->index(edge, edge1Column, parent), adjustedEdge)); } } - std::set >::iterator row; - for (row = edgeRowsToRemove.begin(); row != edgeRowsToRemove.end(); ++row) + for (const auto row : edgeRowsToRemove) { - commands.push(new CSMWorld::DeleteNestedCommand(*model, mId, *row, parentColumn)); + commands.push(new CSMWorld::DeleteNestedCommand(*model, mId.getRefIdString(), row, parentColumn)); } } @@ -437,7 +472,7 @@ namespace CSVRender if (source) { // Want to remove from end of row first - std::set > rowsToRemove; + std::set> rowsToRemove; for (size_t i = 0; i <= mSelected.size(); ++i) { for (size_t j = i + 1; j < mSelected.size(); ++j) @@ -456,13 +491,14 @@ namespace CSVRender } } - CSMWorld::IdTree* model = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); + CSMWorld::IdTree* model + = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges); - std::set >::iterator row; + std::set>::iterator row; for (row = rowsToRemove.begin(); row != rowsToRemove.end(); ++row) { - commands.push(new CSMWorld::DeleteNestedCommand(*model, mId, *row, parentColumn)); + commands.push(new CSMWorld::DeleteNestedCommand(*model, mId.getRefIdString(), *row, parentColumn)); } } } @@ -520,7 +556,7 @@ namespace CSVRender removePathgridGeometry(); mPathgridGeometry = SceneUtil::createPathgridGeometry(*source); - mPathgridGeode->addDrawable(mPathgridGeometry); + mPathgridGroup->addChild(mPathgridGeometry); createSelectedGeometry(*source); } @@ -549,14 +585,14 @@ namespace CSVRender removeSelectedGeometry(); mSelectedGeometry = SceneUtil::createPathgridSelectedWireframe(source, mSelected); - mPathgridGeode->addDrawable(mSelectedGeometry); + mPathgridGroup->addChild(mSelectedGeometry); } void Pathgrid::removePathgridGeometry() { if (mPathgridGeometry) { - mPathgridGeode->removeDrawable(mPathgridGeometry); + mPathgridGroup->removeChild(mPathgridGeometry); mPathgridGeometry = nullptr; } } @@ -565,7 +601,7 @@ namespace CSVRender { if (mSelectedGeometry) { - mPathgridGeode->removeDrawable(mSelectedGeometry); + mPathgridGroup->removeChild(mSelectedGeometry); mSelectedGeometry = nullptr; } } @@ -573,7 +609,7 @@ namespace CSVRender void Pathgrid::createDragGeometry(const osg::Vec3f& start, const osg::Vec3f& end, bool valid) { if (mDragGeometry) - mPathgridGeode->removeDrawable(mDragGeometry); + mPathgridGroup->removeChild(mDragGeometry); mDragGeometry = new osg::Geometry(); @@ -601,7 +637,7 @@ namespace CSVRender mDragGeometry->addPrimitiveSet(indices); mDragGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - mPathgridGeode->addDrawable(mDragGeometry); + mPathgridGroup->addChild(mDragGeometry); } const CSMWorld::Pathgrid* Pathgrid::getPathgridSource() @@ -626,26 +662,27 @@ namespace CSVRender return -1; } - void Pathgrid::addEdge(CSMWorld::CommandMacro& commands, const CSMWorld::Pathgrid& source, unsigned short node1, - unsigned short node2) + void Pathgrid::addEdge( + CSMWorld::CommandMacro& commands, const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2) { - CSMWorld::IdTree* model = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); + CSMWorld::IdTree* model + = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); int recordIndex = mPathgridCollection.getIndex(mId); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges); - int edge0Column = mPathgridCollection.searchNestedColumnIndex(parentColumn, - CSMWorld::Columns::ColumnId_PathgridEdge0); + int edge0Column + = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge0); - int edge1Column = mPathgridCollection.searchNestedColumnIndex(parentColumn, - CSMWorld::Columns::ColumnId_PathgridEdge1); + int edge1Column + = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge1); QModelIndex parent = model->index(recordIndex, parentColumn); int row = static_cast(source.mEdges.size()); if (edgeExists(source, node1, node2) == -1) { - commands.push(new CSMWorld::AddNestedCommand(*model, mId, row, parentColumn)); + commands.push(new CSMWorld::AddNestedCommand(*model, mId.getRefIdString(), row, parentColumn)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge0Column, parent), node1)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge1Column, parent), node2)); ++row; @@ -653,7 +690,7 @@ namespace CSVRender if (edgeExists(source, node2, node1) == -1) { - commands.push(new CSMWorld::AddNestedCommand(*model, mId, row, parentColumn)); + commands.push(new CSMWorld::AddNestedCommand(*model, mId.getRefIdString(), row, parentColumn)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge0Column, parent), node2)); commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge1Column, parent), node1)); } diff --git a/apps/opencs/view/render/pathgrid.hpp b/apps/opencs/view/render/pathgrid.hpp index 8f5d45a487a..4c37a7fa7eb 100644 --- a/apps/opencs/view/render/pathgrid.hpp +++ b/apps/opencs/view/render/pathgrid.hpp @@ -1,21 +1,22 @@ #ifndef CSV_RENDER_PATHGRID_H #define CSV_RENDER_PATHGRID_H +#include +#include #include #include -#include #include +#include #include "../../model/world/cellcoordinates.hpp" -#include "../../model/world/idcollection.hpp" #include "../../model/world/subcellcollection.hpp" - #include "tagbase.hpp" +#include namespace osg { - class Geode; + class Vec3f; class Geometry; class Group; class PositionAttitudeTransform; @@ -32,105 +33,103 @@ namespace CSVRender { class Pathgrid; + struct WorldspaceHitResult; + class PathgridTag : public TagBase { - public: + public: + PathgridTag(Pathgrid* pathgrid); - PathgridTag (Pathgrid* pathgrid); + Pathgrid* getPathgrid() const; - Pathgrid* getPathgrid () const; + QString getToolTip(bool hideBasics, const WorldspaceHitResult& hit) const override; - QString getToolTip (bool hideBasics) const override; - - private: - - Pathgrid* mPathgrid; + private: + Pathgrid* mPathgrid; }; class Pathgrid { - public: - - typedef std::vector NodeList; - - Pathgrid(CSMWorld::Data& data, osg::Group* parent, const std::string& pathgridId, - const CSMWorld::CellCoordinates& coordinates); + public: + typedef std::vector NodeList; - ~Pathgrid(); + Pathgrid(CSMWorld::Data& data, osg::Group* parent, const std::string& pathgridId, + const CSMWorld::CellCoordinates& coordinates); - const CSMWorld::CellCoordinates& getCoordinates() const; - const std::string& getId() const; + ~Pathgrid(); - bool isSelected() const; - const NodeList& getSelected() const; - void selectAll(); - void toggleSelected(unsigned short node); // Adds to end of vector - void invertSelected(); - void clearSelected(); + const CSMWorld::CellCoordinates& getCoordinates() const; + const std::string& getId() const; - void moveSelected(const osg::Vec3d& offset); - void setDragOrigin(unsigned short node); - void setDragEndpoint(unsigned short node); - void setDragEndpoint(const osg::Vec3d& pos); + bool isSelected() const; + const NodeList& getSelected() const; + void selectAll(); + void toggleSelected(unsigned short node); // Adds to end of vector + void invertSelected(); + void clearSelected(); - void resetIndicators(); + void moveSelected(const osg::Vec3d& offset); + void setDragOrigin(unsigned short node); + void setDragEndpoint(unsigned short node); + void setDragEndpoint(const osg::Vec3d& pos); - void applyPoint(CSMWorld::CommandMacro& commands, const osg::Vec3d& worldPos); - void applyPosition(CSMWorld::CommandMacro& commands); - void applyEdge(CSMWorld::CommandMacro& commands, unsigned short node1, unsigned short node2); - void applyEdges(CSMWorld::CommandMacro& commands, unsigned short node); - void applyRemoveNodes(CSMWorld::CommandMacro& commands); - void applyRemoveEdges(CSMWorld::CommandMacro& commands); + void resetIndicators(); - osg::ref_ptr getTag() const; + void applyPoint(CSMWorld::CommandMacro& commands, const osg::Vec3d& worldPos); + void applyPosition(CSMWorld::CommandMacro& commands); + void applyEdge(CSMWorld::CommandMacro& commands, unsigned short node1, unsigned short node2); + void applyEdges(CSMWorld::CommandMacro& commands, unsigned short node); + void applyRemoveNodes(CSMWorld::CommandMacro& commands); + void applyRemoveEdges(CSMWorld::CommandMacro& commands); - void recreateGeometry(); - void removeGeometry(); + osg::ref_ptr getTag() const; - void update(); + void recreateGeometry(); + void removeGeometry(); - private: + void update(); - CSMWorld::Data& mData; - CSMWorld::SubCellCollection& mPathgridCollection; - std::string mId; - CSMWorld::CellCoordinates mCoords; - bool mInterior; + private: + CSMWorld::Data& mData; + CSMWorld::SubCellCollection& mPathgridCollection; + ESM::RefId mId; + CSMWorld::CellCoordinates mCoords; + bool mInterior; - NodeList mSelected; - osg::Vec3d mMoveOffset; - unsigned short mDragOrigin; + NodeList mSelected; + osg::Vec3d mMoveOffset; + unsigned short mDragOrigin; - bool mChangeGeometry; - bool mRemoveGeometry; - bool mUseOffset; + bool mChangeGeometry; + bool mRemoveGeometry; + bool mUseOffset; - osg::Group* mParent; - osg::ref_ptr mBaseNode; - osg::ref_ptr mPathgridGeode; - osg::ref_ptr mPathgridGeometry; - osg::ref_ptr mSelectedGeometry; - osg::ref_ptr mDragGeometry; + osg::Group* mParent; + osg::ref_ptr mBaseNode; + osg::ref_ptr mPathgridGroup; + osg::ref_ptr mPathgridGeometry; + osg::ref_ptr mSelectedGeometry; + osg::ref_ptr mDragGeometry; - osg::ref_ptr mTag; + osg::ref_ptr mTag; - void createGeometry(); - void createSelectedGeometry(); - void createSelectedGeometry(const CSMWorld::Pathgrid& source); - void removePathgridGeometry(); - void removeSelectedGeometry(); + void createGeometry(); + void createSelectedGeometry(); + void createSelectedGeometry(const CSMWorld::Pathgrid& source); + void removePathgridGeometry(); + void removeSelectedGeometry(); - void createDragGeometry(const osg::Vec3f& start, const osg::Vec3f& end, bool valid); + void createDragGeometry(const osg::Vec3f& start, const osg::Vec3f& end, bool valid); - const CSMWorld::Pathgrid* getPathgridSource(); + const CSMWorld::Pathgrid* getPathgridSource(); - int edgeExists(const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2); - void addEdge(CSMWorld::CommandMacro& commands, const CSMWorld::Pathgrid& source, unsigned short node1, - unsigned short node2); - void removeEdge(CSMWorld::CommandMacro& commands, const CSMWorld::Pathgrid& source, unsigned short node1, - unsigned short node2); + int edgeExists(const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2); + void addEdge(CSMWorld::CommandMacro& commands, const CSMWorld::Pathgrid& source, unsigned short node1, + unsigned short node2); + void removeEdge(CSMWorld::CommandMacro& commands, const CSMWorld::Pathgrid& source, unsigned short node1, + unsigned short node2); - int clampToCell(int v); + int clampToCell(int v); }; } diff --git a/apps/opencs/view/render/pathgridmode.cpp b/apps/opencs/view/render/pathgridmode.cpp index 193cb664dbd..8d68ef9650c 100644 --- a/apps/opencs/view/render/pathgridmode.cpp +++ b/apps/opencs/view/render/pathgridmode.cpp @@ -1,13 +1,12 @@ #include "pathgridmode.hpp" -#include -#include +#include +#include #include #include "../../model/prefs/state.hpp" -#include "../../model/world/commands.hpp" #include "../../model/world/commandmacro.hpp" #include "../widget/scenetoolbar.hpp" @@ -18,11 +17,28 @@ #include "pathgridselectionmode.hpp" #include "worldspacewidget.hpp" +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +class QPoint; +class QUndoStack; +class QWidget; + namespace CSVRender { PathgridMode::PathgridMode(WorldspaceWidget* worldspaceWidget, QWidget* parent) - : EditMode(worldspaceWidget, QIcon(":placeholder"), Mask_Pathgrid | Mask_Terrain | Mask_Reference, - getTooltip(), parent) + : EditMode(worldspaceWidget, Misc::ScalableIcon::load(":scenetoolbar/editing-pathgrid"), + Mask_Pathgrid | Mask_Terrain | Mask_Reference, getTooltip(), parent) , mDragMode(DragMode_None) , mFromNode(0) , mSelectionMode(nullptr) @@ -55,24 +71,22 @@ namespace CSVRender { if (mSelectionMode) { - toolbar->removeTool (mSelectionMode); + toolbar->removeTool(mSelectionMode); delete mSelectionMode; mSelectionMode = nullptr; } } - void PathgridMode::primaryOpenPressed(const WorldspaceHitResult& hitResult) - { - } + void PathgridMode::primaryOpenPressed(const WorldspaceHitResult& hitResult) {} void PathgridMode::primaryEditPressed(const WorldspaceHitResult& hitResult) { - if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue() && - dynamic_cast(hitResult.tag.get())) + if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue() + && dynamic_cast(hitResult.tag.get())) { primarySelectPressed(hitResult); } - else if (Cell* cell = getWorldspaceWidget().getCell (hitResult.worldPos)) + else if (Cell* cell = getWorldspaceWidget().getCell(hitResult.worldPos)) { if (cell->getPathgrid()) { @@ -145,16 +159,16 @@ namespace CSVRender bool PathgridMode::primaryEditStartDrag(const QPoint& pos) { - std::vector > selection = getWorldspaceWidget().getSelection (Mask_Pathgrid); + std::vector> selection = getWorldspaceWidget().getSelection(Mask_Pathgrid); if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); if (dynamic_cast(hit.tag.get())) { primarySelectPressed(hit); - selection = getWorldspaceWidget().getSelection (Mask_Pathgrid); + selection = getWorldspaceWidget().getSelection(Mask_Pathgrid); } } @@ -169,7 +183,7 @@ namespace CSVRender bool PathgridMode::secondaryEditStartDrag(const QPoint& pos) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); if (hit.tag) { if (PathgridTag* tag = dynamic_cast(hit.tag.get())) @@ -190,14 +204,14 @@ namespace CSVRender { if (mDragMode == DragMode_Move) { - std::vector > selection = getWorldspaceWidget().getSelection(Mask_Pathgrid); + std::vector> selection = getWorldspaceWidget().getSelection(Mask_Pathgrid); - for (std::vector >::iterator it = selection.begin(); it != selection.end(); ++it) + for (std::vector>::iterator it = selection.begin(); it != selection.end(); ++it) { if (PathgridTag* tag = dynamic_cast(it->get())) { osg::Vec3d eye, center, up, offset; - getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt (eye, center, up); + getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt(eye, center, up); offset = (up * diffY * speedFactor) + (((center - eye) ^ up) * diffX * speedFactor); @@ -207,13 +221,14 @@ namespace CSVRender } else if (mDragMode == DragMode_Edge) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); Cell* cell = getWorldspaceWidget().getCell(hit.worldPos); if (cell && cell->getPathgrid()) { PathgridTag* tag = nullptr; - if (hit.tag && (tag = dynamic_cast(hit.tag.get())) && tag->getPathgrid()->getId() == mEdgeId) + if (hit.tag && (tag = dynamic_cast(hit.tag.get())) + && tag->getPathgrid()->getId() == mEdgeId) { unsigned short node = SceneUtil::getPathgridNode(static_cast(hit.index0)); cell->getPathgrid()->setDragEndpoint(node); @@ -222,7 +237,6 @@ namespace CSVRender { cell->getPathgrid()->setDragEndpoint(hit.worldPos); } - } } } @@ -231,8 +245,8 @@ namespace CSVRender { if (mDragMode == DragMode_Move) { - std::vector > selection = getWorldspaceWidget().getSelection (Mask_Pathgrid); - for (std::vector >::iterator it = selection.begin(); it != selection.end(); ++it) + std::vector> selection = getWorldspaceWidget().getSelection(Mask_Pathgrid); + for (std::vector>::iterator it = selection.begin(); it != selection.end(); ++it) { if (PathgridTag* tag = dynamic_cast(it->get())) { diff --git a/apps/opencs/view/render/pathgridmode.hpp b/apps/opencs/view/render/pathgridmode.hpp index cc61dfe9b0f..39ba5158d3f 100644 --- a/apps/opencs/view/render/pathgridmode.hpp +++ b/apps/opencs/view/render/pathgridmode.hpp @@ -5,60 +5,65 @@ #include "editmode.hpp" +namespace CSVWidget +{ + class SceneToolbar; +} + namespace CSVRender { class PathgridSelectionMode; + class WorldspaceWidget; + struct WorldspaceHitResult; class PathgridMode : public EditMode { - Q_OBJECT - - public: - - PathgridMode(WorldspaceWidget* worldspace, QWidget* parent=nullptr); + Q_OBJECT - void activate(CSVWidget::SceneToolbar* toolbar) override; + public: + PathgridMode(WorldspaceWidget* worldspace, QWidget* parent = nullptr); - void deactivate(CSVWidget::SceneToolbar* toolbar) override; + void activate(CSVWidget::SceneToolbar* toolbar) override; - void primaryOpenPressed(const WorldspaceHitResult& hit) override; + void deactivate(CSVWidget::SceneToolbar* toolbar) override; - void primaryEditPressed(const WorldspaceHitResult& hit) override; + void primaryOpenPressed(const WorldspaceHitResult& hit) override; - void secondaryEditPressed(const WorldspaceHitResult& hit) override; + void primaryEditPressed(const WorldspaceHitResult& hit) override; - void primarySelectPressed(const WorldspaceHitResult& hit) override; + void secondaryEditPressed(const WorldspaceHitResult& hit) override; - void secondarySelectPressed(const WorldspaceHitResult& hit) override; + void primarySelectPressed(const WorldspaceHitResult& hit) override; - bool primaryEditStartDrag (const QPoint& pos) override; + void secondarySelectPressed(const WorldspaceHitResult& hit) override; - bool secondaryEditStartDrag (const QPoint& pos) override; + bool primaryEditStartDrag(const QPoint& pos) override; - void drag (const QPoint& pos, int diffX, int diffY, double speedFactor) override; + bool secondaryEditStartDrag(const QPoint& pos) override; - void dragCompleted(const QPoint& pos) override; + void drag(const QPoint& pos, int diffX, int diffY, double speedFactor) override; - /// \note dragAborted will not be called, if the drag is aborted via changing - /// editing mode - void dragAborted() override; + void dragCompleted(const QPoint& pos) override; - private: + /// \note dragAborted will not be called, if the drag is aborted via changing + /// editing mode + void dragAborted() override; - enum DragMode - { - DragMode_None, - DragMode_Move, - DragMode_Edge - }; + private: + enum DragMode + { + DragMode_None, + DragMode_Move, + DragMode_Edge + }; - DragMode mDragMode; - std::string mLastId, mEdgeId; - unsigned short mFromNode; + DragMode mDragMode; + std::string mLastId, mEdgeId; + unsigned short mFromNode; - PathgridSelectionMode* mSelectionMode; + PathgridSelectionMode* mSelectionMode; - QString getTooltip(); + QString getTooltip(); }; } diff --git a/apps/opencs/view/render/pathgridselectionmode.cpp b/apps/opencs/view/render/pathgridselectionmode.cpp index db41faf50de..1e154197a40 100644 --- a/apps/opencs/view/render/pathgridselectionmode.cpp +++ b/apps/opencs/view/render/pathgridselectionmode.cpp @@ -1,14 +1,26 @@ #include "pathgridselectionmode.hpp" -#include #include +#include + +#include + +#include -#include "../../model/world/idtable.hpp" -#include "../../model/world/commands.hpp" #include "../../model/world/commandmacro.hpp" -#include "worldspacewidget.hpp" +#include +#include +#include +#include + #include "pathgrid.hpp" +#include "worldspacewidget.hpp" + +namespace CSVWidget +{ + class SceneToolbar; +} namespace CSVRender { @@ -18,8 +30,8 @@ namespace CSVRender mRemoveSelectedNodes = new QAction("Remove selected nodes", this); mRemoveSelectedEdges = new QAction("Remove edges between selected nodes", this); - connect(mRemoveSelectedNodes, SIGNAL(triggered()), this, SLOT(removeSelectedNodes())); - connect(mRemoveSelectedEdges, SIGNAL(triggered()), this, SLOT(removeSelectedEdges())); + connect(mRemoveSelectedNodes, &QAction::triggered, this, &PathgridSelectionMode::removeSelectedNodes); + connect(mRemoveSelectedEdges, &QAction::triggered, this, &PathgridSelectionMode::removeSelectedEdges); } bool PathgridSelectionMode::createContextMenu(QMenu* menu) @@ -37,9 +49,9 @@ namespace CSVRender void PathgridSelectionMode::removeSelectedNodes() { - std::vector > selection = getWorldspaceWidget().getSelection (Mask_Pathgrid); + std::vector> selection = getWorldspaceWidget().getSelection(Mask_Pathgrid); - for (std::vector >::iterator it = selection.begin(); it != selection.end(); ++it) + for (std::vector>::iterator it = selection.begin(); it != selection.end(); ++it) { if (PathgridTag* tag = dynamic_cast(it->get())) { @@ -54,9 +66,9 @@ namespace CSVRender void PathgridSelectionMode::removeSelectedEdges() { - std::vector > selection = getWorldspaceWidget().getSelection (Mask_Pathgrid); + std::vector> selection = getWorldspaceWidget().getSelection(Mask_Pathgrid); - for (std::vector >::iterator it = selection.begin(); it != selection.end(); ++it) + for (std::vector>::iterator it = selection.begin(); it != selection.end(); ++it) { if (PathgridTag* tag = dynamic_cast(it->get())) { diff --git a/apps/opencs/view/render/pathgridselectionmode.hpp b/apps/opencs/view/render/pathgridselectionmode.hpp index 19bfca803ed..8c13ba495f4 100644 --- a/apps/opencs/view/render/pathgridselectionmode.hpp +++ b/apps/opencs/view/render/pathgridselectionmode.hpp @@ -5,33 +5,40 @@ namespace CSVRender { - class PathgridSelectionMode : public SelectionMode - { - Q_OBJECT - - public: + class WorldspaceWidget; +} - PathgridSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget); +namespace CSVWidget +{ + class SceneToolbar; +} - protected: +namespace CSVRender +{ + class PathgridSelectionMode : public SelectionMode + { + Q_OBJECT - /// Add context menu items to \a menu. - /// - /// \attention menu can be a 0-pointer - /// - /// \return Have there been any menu items to be added (if menu is 0 and there - /// items to be added, the function must return true anyway. - bool createContextMenu(QMenu* menu) override; + public: + PathgridSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget); - private: + protected: + /// Add context menu items to \a menu. + /// + /// \attention menu can be a 0-pointer + /// + /// \return Have there been any menu items to be added (if menu is 0 and there + /// items to be added, the function must return true anyway. + bool createContextMenu(QMenu* menu) override; - QAction* mRemoveSelectedNodes; - QAction* mRemoveSelectedEdges; + private: + QAction* mRemoveSelectedNodes; + QAction* mRemoveSelectedEdges; - private slots: + private slots: - void removeSelectedNodes(); - void removeSelectedEdges(); + void removeSelectedNodes(); + void removeSelectedEdges(); }; } diff --git a/apps/opencs/view/render/previewwidget.cpp b/apps/opencs/view/render/previewwidget.cpp index 522534adb6b..0f9064aaaca 100644 --- a/apps/opencs/view/render/previewwidget.cpp +++ b/apps/opencs/view/render/previewwidget.cpp @@ -1,73 +1,75 @@ #include "previewwidget.hpp" +#include +#include +#include +#include +#include + #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" -CSVRender::PreviewWidget::PreviewWidget (CSMWorld::Data& data, - const std::string& id, bool referenceable, QWidget *parent) -: SceneWidget (data.getResourceSystem(), parent), mData (data), mObject(data, mRootNode, id, referenceable) +class QWidget; + +CSVRender::PreviewWidget::PreviewWidget( + CSMWorld::Data& data, const std::string& id, bool referenceable, QWidget* parent) + : SceneWidget(data.getResourceSystem(), parent) + , mData(data) + , mObject(data, mRootNode, id, referenceable) { selectNavigationMode("orbit"); - QAbstractItemModel *referenceables = - mData.getTableModel (CSMWorld::UniversalId::Type_Referenceables); + QAbstractItemModel* referenceables = mData.getTableModel(CSMWorld::UniversalId::Type_Referenceables); - connect (referenceables, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (referenceableDataChanged (const QModelIndex&, const QModelIndex&))); - connect (referenceables, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (referenceableAboutToBeRemoved (const QModelIndex&, int, int))); + connect(referenceables, &QAbstractItemModel::dataChanged, this, &PreviewWidget::referenceableDataChanged); + connect( + referenceables, &QAbstractItemModel::rowsAboutToBeRemoved, this, &PreviewWidget::referenceableAboutToBeRemoved); - connect (&mData, SIGNAL (assetTablesChanged ()), - this, SLOT (assetTablesChanged ())); + connect(&mData, &CSMWorld::Data::assetTablesChanged, this, &PreviewWidget::assetTablesChanged); setExterior(false); if (!referenceable) { - QAbstractItemModel *references = - mData.getTableModel (CSMWorld::UniversalId::Type_References); + QAbstractItemModel* references = mData.getTableModel(CSMWorld::UniversalId::Type_References); - connect (references, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (referenceDataChanged (const QModelIndex&, const QModelIndex&))); - connect (references, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (referenceAboutToBeRemoved (const QModelIndex&, int, int))); + connect(references, &QAbstractItemModel::dataChanged, this, &PreviewWidget::referenceDataChanged); + connect(references, &QAbstractItemModel::rowsAboutToBeRemoved, this, &PreviewWidget::referenceAboutToBeRemoved); } } -void CSVRender::PreviewWidget::referenceableDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSVRender::PreviewWidget::referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { - if (mObject.referenceableDataChanged (topLeft, bottomRight)) + if (mObject.referenceableDataChanged(topLeft, bottomRight)) flagAsModified(); if (mObject.getReferenceId().empty()) { - CSMWorld::IdTable& referenceables = dynamic_cast ( - *mData.getTableModel (CSMWorld::UniversalId::Type_Referenceables)); + CSMWorld::IdTable& referenceables + = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Referenceables)); - QModelIndex index = referenceables.getModelIndex (mObject.getReferenceableId(), - referenceables.findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); + QModelIndex index = referenceables.getModelIndex( + mObject.getReferenceableId(), referenceables.findColumnIndex(CSMWorld::Columns::ColumnId_Modification)); - if (referenceables.data (index).toInt()==CSMWorld::RecordBase::State_Deleted) + if (referenceables.data(index).toInt() == CSMWorld::RecordBase::State_Deleted) emit closeRequest(); } } -void CSVRender::PreviewWidget::referenceableAboutToBeRemoved (const QModelIndex& parent, int start, - int end) +void CSVRender::PreviewWidget::referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end) { - if (mObject.referenceableAboutToBeRemoved (parent, start, end)) + if (mObject.referenceableAboutToBeRemoved(parent, start, end)) flagAsModified(); if (mObject.getReferenceableId().empty()) return; - CSMWorld::IdTable& referenceables = dynamic_cast ( - *mData.getTableModel (CSMWorld::UniversalId::Type_Referenceables)); + CSMWorld::IdTable& referenceables + = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Referenceables)); - QModelIndex index = referenceables.getModelIndex (mObject.getReferenceableId(), 0); + QModelIndex index = referenceables.getModelIndex(mObject.getReferenceableId(), 0); - if (index.row()>=start && index.row()<=end) + if (index.row() >= start && index.row() <= end) { if (mObject.getReferenceId().empty()) { @@ -77,55 +79,53 @@ void CSVRender::PreviewWidget::referenceableAboutToBeRemoved (const QModelIndex& } } -void CSVRender::PreviewWidget::referenceDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSVRender::PreviewWidget::referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { - if (mObject.referenceDataChanged (topLeft, bottomRight)) + if (mObject.referenceDataChanged(topLeft, bottomRight)) flagAsModified(); if (mObject.getReferenceId().empty()) return; - CSMWorld::IdTable& references = dynamic_cast ( - *mData.getTableModel (CSMWorld::UniversalId::Type_References)); + CSMWorld::IdTable& references + = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_References)); // check for deleted state { - QModelIndex index = references.getModelIndex (mObject.getReferenceId(), - references.findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); + QModelIndex index = references.getModelIndex( + mObject.getReferenceId(), references.findColumnIndex(CSMWorld::Columns::ColumnId_Modification)); - if (references.data (index).toInt()==CSMWorld::RecordBase::State_Deleted) + if (references.data(index).toInt() == CSMWorld::RecordBase::State_Deleted) { emit closeRequest(); return; } } - int columnIndex = references.findColumnIndex (CSMWorld::Columns::ColumnId_ReferenceableId); + int columnIndex = references.findColumnIndex(CSMWorld::Columns::ColumnId_ReferenceableId); - QModelIndex index = references.getModelIndex (mObject.getReferenceId(), columnIndex); + QModelIndex index = references.getModelIndex(mObject.getReferenceId(), columnIndex); - if (index.row()>=topLeft.row() && index.row()<=bottomRight.row()) - if (index.column()>=topLeft.column() && index.column()<=bottomRight.row()) - emit referenceableIdChanged (mObject.getReferenceableId()); + if (index.row() >= topLeft.row() && index.row() <= bottomRight.row()) + if (index.column() >= topLeft.column() && index.column() <= bottomRight.row()) + emit referenceableIdChanged(mObject.getReferenceableId()); } -void CSVRender::PreviewWidget::referenceAboutToBeRemoved (const QModelIndex& parent, int start, - int end) +void CSVRender::PreviewWidget::referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end) { if (mObject.getReferenceId().empty()) return; - CSMWorld::IdTable& references = dynamic_cast ( - *mData.getTableModel (CSMWorld::UniversalId::Type_References)); + CSMWorld::IdTable& references + = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_References)); - QModelIndex index = references.getModelIndex (mObject.getReferenceId(), 0); + QModelIndex index = references.getModelIndex(mObject.getReferenceId(), 0); - if (index.row()>=start && index.row()<=end) + if (index.row() >= start && index.row() <= end) emit closeRequest(); } -void CSVRender::PreviewWidget::assetTablesChanged () +void CSVRender::PreviewWidget::assetTablesChanged() { mObject.reloadAssets(); } diff --git a/apps/opencs/view/render/previewwidget.hpp b/apps/opencs/view/render/previewwidget.hpp index a8d73729a41..9f926d46409 100644 --- a/apps/opencs/view/render/previewwidget.hpp +++ b/apps/opencs/view/render/previewwidget.hpp @@ -3,14 +3,13 @@ #include "scenewidget.hpp" +#include + #include "object.hpp" class QModelIndex; - -namespace VFS -{ - class Manager; -} +class QObject; +class QWidget; namespace CSMWorld { @@ -21,34 +20,31 @@ namespace CSVRender { class PreviewWidget : public SceneWidget { - Q_OBJECT - - CSMWorld::Data& mData; - CSVRender::Object mObject; + Q_OBJECT - public: + CSMWorld::Data& mData; + CSVRender::Object mObject; - PreviewWidget (CSMWorld::Data& data, const std::string& id, bool referenceable, - QWidget *parent = nullptr); + public: + PreviewWidget(CSMWorld::Data& data, const std::string& id, bool referenceable, QWidget* parent = nullptr); - signals: + signals: - void closeRequest(); + void closeRequest(); - void referenceableIdChanged (const std::string& id); + void referenceableIdChanged(const std::string& id); - private slots: + private slots: - void referenceableDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight); + void referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end); - void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end); - void assetTablesChanged (); + void assetTablesChanged(); }; } diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index dbed1ba97cd..716a087d029 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -1,604 +1,564 @@ #include "scenewidget.hpp" #include +#include #include -#include -#include -#include #include - -#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include #include -#include -#include +#include #include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include #include -#include #include +#include +#include #include #include "../widget/scenetoolmode.hpp" -#include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcut.hpp" +#include "../../model/prefs/state.hpp" +#include "cameracontroller.hpp" #include "lighting.hpp" #include "mask.hpp" -#include "cameracontroller.hpp" namespace CSVRender { -RenderWidget::RenderWidget(QWidget *parent, Qt::WindowFlags f) - : QWidget(parent, f) - , mRootNode(nullptr) -{ - - osgViewer::CompositeViewer& viewer = CompositeViewer::get(); - - osg::DisplaySettings* ds = osg::DisplaySettings::instance().get(); - //ds->setNumMultiSamples(8); - - osg::ref_ptr traits = new osg::GraphicsContext::Traits; - traits->windowName = ""; - traits->windowDecoration = true; - traits->x = 0; - traits->y = 0; - traits->width = width(); - traits->height = height(); - traits->doubleBuffer = true; - traits->alpha = ds->getMinimumNumAlphaBits(); - traits->stencil = ds->getMinimumNumStencilBits(); - traits->sampleBuffers = ds->getMultiSamples(); - traits->samples = ds->getNumMultiSamples(); - // Doesn't make much sense as we're running on demand updates, and there seems to be a bug with the refresh rate when running multiple QGLWidgets - traits->vsync = false; - - mView = new osgViewer::View; - updateCameraParameters( traits->width / static_cast(traits->height) ); - - osg::ref_ptr window = new osgQt::GraphicsWindowQt(traits.get()); - QLayout* layout = new QHBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(window->getGLWidget()); - setLayout(layout); - - mView->getCamera()->setGraphicsContext(window); - mView->getCamera()->setViewport( new osg::Viewport(0, 0, traits->width, traits->height) ); - - SceneUtil::LightManager* lightMgr = new SceneUtil::LightManager; - lightMgr->setStartLight(1); - lightMgr->setLightingMask(Mask_Lighting); - mRootNode = lightMgr; - - mView->getCamera()->getOrCreateStateSet()->setMode(GL_NORMALIZE, osg::StateAttribute::ON); - mView->getCamera()->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::ON); - osg::ref_ptr defaultMat (new osg::Material); - defaultMat->setColorMode(osg::Material::OFF); - defaultMat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - defaultMat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mView->getCamera()->getOrCreateStateSet()->setAttribute(defaultMat); - - mView->setSceneData(mRootNode); - - // Add ability to signal osg to show its statistics for debugging purposes - mView->addEventHandler(new osgViewer::StatsHandler); - - viewer.addView(mView); - viewer.setDone(false); - viewer.realize(); -} - -RenderWidget::~RenderWidget() -{ - try - { - CompositeViewer::get().removeView(mView); - -#if OSG_VERSION_LESS_THAN(3,6,5) - // before OSG 3.6.4, the default font was a static object, and if it wasn't attached to the scene when a graphics context was destroyed, it's program wouldn't be released. - // 3.6.4 moved it into the object cache, which meant it usually got released, but not here. - // 3.6.5 improved cleanup with osgViewer::CompositeViewer::removeView so it more reliably released associated state for objects in the object cache. - osg::ref_ptr graphicsContext = mView->getCamera()->getGraphicsContext(); - osgText::Font::getDefaultFont()->releaseGLObjects(graphicsContext->getState()); -#endif - } - catch(const std::exception& e) + RenderWidget::RenderWidget(QWidget* parent, Qt::WindowFlags f) + : QWidget(parent, f) + , mRootNode(nullptr) { - Log(Debug::Error) << "Error in the destructor: " << e.what(); - } -} - -void RenderWidget::flagAsModified() -{ - mView->requestRedraw(); -} - -void RenderWidget::setVisibilityMask(unsigned int mask) -{ - mView->getCamera()->setCullMask(mask | Mask_ParticleSystem | Mask_Lighting); -} - -osg::Camera *RenderWidget::getCamera() -{ - return mView->getCamera(); -} - -void RenderWidget::toggleRenderStats() -{ - osgViewer::GraphicsWindow* window = - static_cast(mView->getCamera()->getGraphicsContext()); + mView = new osgViewer::View; + updateCameraParameters(width() / static_cast(height())); - window->getEventQueue()->keyPress(osgGA::GUIEventAdapter::KEY_S); - window->getEventQueue()->keyRelease(osgGA::GUIEventAdapter::KEY_S); -} + mWidget = new osgQOpenGLWidget(this); + mRenderer = mWidget->getCompositeViewer(); + osg::ref_ptr window + = new osgViewer::GraphicsWindowEmbedded(0, 0, width(), height()); + mWidget->setGraphicsWindowEmbedded(window); -// -------------------------------------------------- + mRenderer->setRealizeOperation(new SceneUtil::GetGLExtensionsOperation()); -CompositeViewer::CompositeViewer() - : mSimulationTime(0.0) -{ - // TODO: Upgrade osgQt to support osgViewer::ViewerBase::DrawThreadPerContext - // https://gitlab.com/OpenMW/openmw/-/issues/5481 - setThreadingModel(osgViewer::ViewerBase::SingleThreaded); + int frameRateLimit = CSMPrefs::get()["Rendering"]["framerate-limit"].toInt(); + mRenderer->setRunMaxFrameRate(frameRateLimit); + mRenderer->setUseConfigureAffinity(false); -#if OSG_VERSION_GREATER_OR_EQUAL(3,5,5) - setUseConfigureAffinity(false); -#endif + QLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(mWidget); + setLayout(layout); - // disable the default setting of viewer.done() by pressing Escape. - setKeyEventSetsDone(0); + mView->getCamera()->setGraphicsContext(window); - // Only render when the camera position changed, or content flagged dirty - //setRunFrameScheme(osgViewer::ViewerBase::ON_DEMAND); - setRunFrameScheme(osgViewer::ViewerBase::CONTINUOUS); + osg::ref_ptr lightMgr = new SceneUtil::LightManager; + lightMgr->setStartLight(1); + lightMgr->setLightingMask(Mask_Lighting); + mRootNode = std::move(lightMgr); - connect( &mTimer, SIGNAL(timeout()), this, SLOT(update()) ); - mTimer.start( 10 ); + mView->getCamera()->setViewport(new osg::Viewport(0, 0, width(), height())); - int frameRateLimit = CSMPrefs::get()["Rendering"]["framerate-limit"].toInt(); - setRunMaxFrameRate(frameRateLimit); -} + mView->getCamera()->getOrCreateStateSet()->setMode(GL_NORMALIZE, osg::StateAttribute::ON); + mView->getCamera()->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::ON); + osg::ref_ptr defaultMat(new osg::Material); + defaultMat->setColorMode(osg::Material::OFF); + defaultMat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, 1)); + defaultMat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, 1)); + defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); + mView->getCamera()->getOrCreateStateSet()->setAttribute(defaultMat); -CompositeViewer &CompositeViewer::get() -{ - static CompositeViewer sThis; - return sThis; -} + mView->setSceneData(mRootNode); -void CompositeViewer::update() -{ - double dt = mFrameTimer.time_s(); - mFrameTimer.setStartTick(); + // Add ability to signal osg to show its statistics for debugging purposes + mView->addEventHandler(new osgViewer::StatsHandler); - emit simulationUpdated(dt); - - mSimulationTime += dt; - frame(mSimulationTime); + mRenderer->addView(mView); + mRenderer->setDone(false); + } - double minFrameTime = _runMaxFrameRate > 0.0 ? 1.0 / _runMaxFrameRate : 0.0; - if (dt < minFrameTime) + RenderWidget::~RenderWidget() { - std::this_thread::sleep_for(std::chrono::duration(minFrameTime - dt)); + try + { + mRenderer->removeView(mView); + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Error in the destructor: " << e.what(); + } + delete mWidget; } -} -// --------------------------------------------------- - -SceneWidget::SceneWidget(std::shared_ptr resourceSystem, QWidget *parent, Qt::WindowFlags f, - bool retrieveInput) - : RenderWidget(parent, f) - , mResourceSystem(resourceSystem) - , mLighting(nullptr) - , mHasDefaultAmbient(false) - , mIsExterior(true) - , mPrevMouseX(0) - , mPrevMouseY(0) - , mCamPositionSet(false) -{ - mFreeCamControl = new FreeCameraController(this); - mOrbitCamControl = new OrbitCameraController(this); - mCurrentCamControl = mFreeCamControl; - - mOrbitCamControl->setPickingMask(Mask_Reference | Mask_Terrain); - - mOrbitCamControl->setConstRoll( CSMPrefs::get()["3D Scene Input"]["navi-orbit-const-roll"].isTrue() ); - - // set up gradient view or configured clear color - QColor bgColour = CSMPrefs::get()["Rendering"]["scene-day-background-colour"].toColor(); - - if (CSMPrefs::get()["Rendering"]["scene-use-gradient"].isTrue()) { - QColor gradientColour = CSMPrefs::get()["Rendering"]["scene-day-gradient-colour"].toColor(); - mGradientCamera = createGradientCamera(bgColour, gradientColour); + void RenderWidget::flagAsModified() + { + mView->requestRedraw(); + } - mView->getCamera()->setClearMask(0); - mView->getCamera()->addChild(mGradientCamera.get()); + void RenderWidget::setVisibilityMask(unsigned int mask) + { + mView->getCamera()->setCullMask(mask | Mask_ParticleSystem | Mask_Lighting); } - else { - mView->getCamera()->setClearColor(osg::Vec4( - bgColour.redF(), - bgColour.greenF(), - bgColour.blueF(), - 1.0f - )); + + osg::Camera* RenderWidget::getCamera() + { + return mView->getCamera(); } - // we handle lighting manually - mView->setLightingMode(osgViewer::View::NO_LIGHT); + void RenderWidget::toggleRenderStats() + { + osgViewer::GraphicsWindow* window + = static_cast(mView->getCamera()->getGraphicsContext()); - setLighting(&mLightingDay); + window->getEventQueue()->keyPress(osgGA::GUIEventAdapter::KEY_S); + window->getEventQueue()->keyRelease(osgGA::GUIEventAdapter::KEY_S); + } - mResourceSystem->getSceneManager()->setParticleSystemMask(Mask_ParticleSystem); + // --------------------------------------------------- - // Recieve mouse move event even if mouse button is not pressed - setMouseTracking(true); - setFocusPolicy(Qt::ClickFocus); + SceneWidget::SceneWidget(std::shared_ptr resourceSystem, QWidget* parent, + Qt::WindowFlags f, bool retrieveInput) + : RenderWidget(parent, f) + , mResourceSystem(std::move(resourceSystem)) + , mLighting(nullptr) + , mHasDefaultAmbient(false) + , mIsExterior(true) + , mPrevMouseX(0) + , mPrevMouseY(0) + , mCamPositionSet(false) + { + mFreeCamControl = new FreeCameraController(this); + mOrbitCamControl = new OrbitCameraController(this); + mCurrentCamControl = mFreeCamControl; - connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), - this, SLOT (settingChanged (const CSMPrefs::Setting *))); + mOrbitCamControl->setPickingMask(Mask_Reference | Mask_Terrain); - // TODO update this outside of the constructor where virtual methods can be used - if (retrieveInput) - { - CSMPrefs::get()["3D Scene Input"].update(); - CSMPrefs::get()["Tooltips"].update(); - } + mOrbitCamControl->setConstRoll(CSMPrefs::get()["3D Scene Input"]["navi-orbit-const-roll"].isTrue()); - connect (&CompositeViewer::get(), SIGNAL (simulationUpdated(double)), this, SLOT (update(double))); + // set up gradient view or configured clear color + QColor bgColour = CSMPrefs::get()["Rendering"]["scene-day-background-colour"].toColor(); - // Shortcuts - CSMPrefs::Shortcut* focusToolbarShortcut = new CSMPrefs::Shortcut("scene-focus-toolbar", this); - connect(focusToolbarShortcut, SIGNAL(activated()), this, SIGNAL(focusToolbarRequest())); + if (CSMPrefs::get()["Rendering"]["scene-use-gradient"].isTrue()) + { + QColor gradientColour = CSMPrefs::get()["Rendering"]["scene-day-gradient-colour"].toColor(); + mGradientCamera = createGradientCamera(bgColour, gradientColour); - CSMPrefs::Shortcut* renderStatsShortcut = new CSMPrefs::Shortcut("scene-render-stats", this); - connect(renderStatsShortcut, SIGNAL(activated()), this, SLOT(toggleRenderStats())); -} + mView->getCamera()->setClearMask(0); + mView->getCamera()->addChild(mGradientCamera.get()); + } + else + { + mView->getCamera()->setClearColor(osg::Vec4(bgColour.redF(), bgColour.greenF(), bgColour.blueF(), 1.0f)); + } -SceneWidget::~SceneWidget() -{ - // Since we're holding on to the resources past the existence of this graphics context, we'll need to manually release the created objects - mResourceSystem->releaseGLObjects(mView->getCamera()->getGraphicsContext()->getState()); -} + // we handle lighting manually + mView->setLightingMode(osgViewer::View::NO_LIGHT); + setLighting(&mLightingDay); -osg::ref_ptr SceneWidget::createGradientRectangle(QColor bgColour, QColor gradientColour) -{ - osg::ref_ptr geometry = new osg::Geometry; + mResourceSystem->getSceneManager()->setParticleSystemMask(Mask_ParticleSystem); - osg::ref_ptr vertices = new osg::Vec3Array; + // Recieve mouse move event even if mouse button is not pressed + setMouseTracking(true); + setFocusPolicy(Qt::ClickFocus); - vertices->push_back(osg::Vec3(0.0f, 0.0f, -1.0f)); - vertices->push_back(osg::Vec3(1.0f, 0.0f, -1.0f)); - vertices->push_back(osg::Vec3(0.0f, 1.0f, -1.0f)); - vertices->push_back(osg::Vec3(1.0f, 1.0f, -1.0f)); + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &SceneWidget::settingChanged); - geometry->setVertexArray(vertices); + // TODO update this outside of the constructor where virtual methods can be used + if (retrieveInput) + { + CSMPrefs::get()["3D Scene Input"].update(); + CSMPrefs::get()["Tooltips"].update(); + } - osg::ref_ptr primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0); + connect(mRenderer, &CompositeOsgRenderer::simulationUpdated, this, &SceneWidget::update); - // triangle 1 - primitives->push_back (0); - primitives->push_back (1); - primitives->push_back (2); + // Shortcuts + CSMPrefs::Shortcut* focusToolbarShortcut = new CSMPrefs::Shortcut("scene-focus-toolbar", this); + connect( + focusToolbarShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &SceneWidget::focusToolbarRequest); - // triangle 2 - primitives->push_back (2); - primitives->push_back (1); - primitives->push_back (3); + CSMPrefs::Shortcut* renderStatsShortcut = new CSMPrefs::Shortcut("scene-render-stats", this); + connect( + renderStatsShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &SceneWidget::toggleRenderStats); + } - geometry->addPrimitiveSet(primitives); + SceneWidget::~SceneWidget() + { + // Since we're holding on to the resources past the existence of this graphics context, we'll need to manually + // release the created objects + mResourceSystem->releaseGLObjects(mView->getCamera()->getGraphicsContext()->getState()); + } - osg::ref_ptr colours = new osg::Vec4ubArray; - colours->push_back(osg::Vec4ub(gradientColour.red(), gradientColour.green(), gradientColour.blue(), 1.0f)); - colours->push_back(osg::Vec4ub(gradientColour.red(), gradientColour.green(), gradientColour.blue(), 1.0f)); - colours->push_back(osg::Vec4ub(bgColour.red(), bgColour.green(), bgColour.blue(), 1.0f)); - colours->push_back(osg::Vec4ub(bgColour.red(), bgColour.green(), bgColour.blue(), 1.0f)); + osg::ref_ptr SceneWidget::createGradientRectangle(QColor& bgColour, QColor& gradientColour) + { + osg::ref_ptr geometry = new osg::Geometry; - geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); + osg::ref_ptr vertices = new osg::Vec3Array; - geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - geometry->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + vertices->push_back(osg::Vec3(0.0f, 0.0f, -1.0f)); + vertices->push_back(osg::Vec3(1.0f, 0.0f, -1.0f)); + vertices->push_back(osg::Vec3(0.0f, 1.0f, -1.0f)); + vertices->push_back(osg::Vec3(1.0f, 1.0f, -1.0f)); - return geometry; -} + geometry->setVertexArray(vertices); + osg::ref_ptr primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, 0); -osg::ref_ptr SceneWidget::createGradientCamera(QColor bgColour, QColor gradientColour) -{ - osg::ref_ptr camera = new osg::Camera(); - camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); - camera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1.0f, 0, 1.0f)); - camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); - camera->setViewMatrix(osg::Matrix::identity()); + // triangle 1 + primitives->push_back(0); + primitives->push_back(1); + primitives->push_back(2); - camera->setClearMask(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); - camera->setAllowEventFocus(false); + // triangle 2 + primitives->push_back(2); + primitives->push_back(1); + primitives->push_back(3); - // draw subgraph before main camera view. - camera->setRenderOrder(osg::Camera::PRE_RENDER); + geometry->addPrimitiveSet(primitives); - camera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + osg::ref_ptr colours = new osg::Vec4ubArray; + colours->push_back(osg::Vec4ub(gradientColour.red(), gradientColour.green(), gradientColour.blue(), 1.0f)); + colours->push_back(osg::Vec4ub(gradientColour.red(), gradientColour.green(), gradientColour.blue(), 1.0f)); + colours->push_back(osg::Vec4ub(bgColour.red(), bgColour.green(), bgColour.blue(), 1.0f)); + colours->push_back(osg::Vec4ub(bgColour.red(), bgColour.green(), bgColour.blue(), 1.0f)); - osg::ref_ptr gradientQuad = createGradientRectangle(bgColour, gradientColour); + geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); - camera->addChild(gradientQuad); - return camera; -} + geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + geometry->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + return geometry; + } -void SceneWidget::updateGradientCamera(QColor bgColour, QColor gradientColour) -{ - osg::ref_ptr gradientRect = createGradientRectangle(bgColour, gradientColour); - // Replaces previous rectangle - mGradientCamera->setChild(0, gradientRect.get()); -} + osg::ref_ptr SceneWidget::createGradientCamera(QColor& bgColour, QColor& gradientColour) + { + osg::ref_ptr camera = new osg::Camera(); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + camera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1.0f, 0, 1.0f)); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + camera->setViewMatrix(osg::Matrix::identity()); -void SceneWidget::setLighting(Lighting *lighting) -{ - if (mLighting) - mLighting->deactivate(); + camera->setClearMask(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + camera->setAllowEventFocus(false); - mLighting = lighting; - mLighting->activate (mRootNode, mIsExterior); + // draw subgraph before main camera view. + camera->setRenderOrder(osg::Camera::PRE_RENDER); - osg::Vec4f ambient = mLighting->getAmbientColour(mHasDefaultAmbient ? &mDefaultAmbient : nullptr); - setAmbient(ambient); + camera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - flagAsModified(); -} + osg::ref_ptr gradientQuad = createGradientRectangle(bgColour, gradientColour); -void SceneWidget::setAmbient(const osg::Vec4f& ambient) -{ - osg::ref_ptr stateset = new osg::StateSet; - osg::ref_ptr lightmodel = new osg::LightModel; - lightmodel->setAmbientIntensity(ambient); - stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON); - stateset->setMode(GL_LIGHT0, osg::StateAttribute::ON); - stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON); - mRootNode->setStateSet(stateset); -} + camera->addChild(std::move(gradientQuad)); + return camera; + } -void SceneWidget::selectLightingMode (const std::string& mode) -{ - QColor backgroundColour; - QColor gradientColour; - if (mode == "day") + void SceneWidget::updateGradientCamera(QColor& bgColour, QColor& gradientColour) { - backgroundColour = CSMPrefs::get()["Rendering"]["scene-day-background-colour"].toColor(); - gradientColour = CSMPrefs::get()["Rendering"]["scene-day-gradient-colour"].toColor(); - setLighting(&mLightingDay); + osg::ref_ptr gradientRect = createGradientRectangle(bgColour, gradientColour); + // Replaces previous rectangle + mGradientCamera->setChild(0, gradientRect.get()); } - else if (mode == "night") + + void SceneWidget::setLighting(Lighting* lighting) { - backgroundColour = CSMPrefs::get()["Rendering"]["scene-night-background-colour"].toColor(); - gradientColour = CSMPrefs::get()["Rendering"]["scene-night-gradient-colour"].toColor(); - setLighting(&mLightingNight); + if (mLighting) + mLighting->deactivate(); + + mLighting = lighting; + mLighting->activate(mRootNode, mIsExterior); + + osg::Vec4f ambient = mLighting->getAmbientColour(mHasDefaultAmbient ? &mDefaultAmbient : nullptr); + setAmbient(ambient); + + flagAsModified(); } - else if (mode == "bright") + + void SceneWidget::setAmbient(const osg::Vec4f& ambient) { - backgroundColour = CSMPrefs::get()["Rendering"]["scene-bright-background-colour"].toColor(); - gradientColour = CSMPrefs::get()["Rendering"]["scene-bright-gradient-colour"].toColor(); - setLighting(&mLightingBright); + osg::ref_ptr stateset = new osg::StateSet; + osg::ref_ptr lightmodel = new osg::LightModel; + lightmodel->setAmbientIntensity(ambient); + stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON); + stateset->setMode(GL_LIGHT0, osg::StateAttribute::ON); + stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON); + mRootNode->setStateSet(stateset); } - if (CSMPrefs::get()["Rendering"]["scene-use-gradient"].isTrue()) { - if (mGradientCamera.get() != nullptr) { - // we can go ahead and update since this camera still exists - updateGradientCamera(backgroundColour, gradientColour); - if (!mView->getCamera()->containsNode(mGradientCamera.get())) + void SceneWidget::selectLightingMode(const std::string& mode) + { + QColor backgroundColour; + QColor gradientColour; + if (mode == "day") + { + backgroundColour = CSMPrefs::get()["Rendering"]["scene-day-background-colour"].toColor(); + gradientColour = CSMPrefs::get()["Rendering"]["scene-day-gradient-colour"].toColor(); + setLighting(&mLightingDay); + } + else if (mode == "night") + { + backgroundColour = CSMPrefs::get()["Rendering"]["scene-night-background-colour"].toColor(); + gradientColour = CSMPrefs::get()["Rendering"]["scene-night-gradient-colour"].toColor(); + setLighting(&mLightingNight); + } + else if (mode == "bright") + { + backgroundColour = CSMPrefs::get()["Rendering"]["scene-bright-background-colour"].toColor(); + gradientColour = CSMPrefs::get()["Rendering"]["scene-bright-gradient-colour"].toColor(); + setLighting(&mLightingBright); + } + if (CSMPrefs::get()["Rendering"]["scene-use-gradient"].isTrue()) + { + if (mGradientCamera.get() != nullptr) + { + // we can go ahead and update since this camera still exists + updateGradientCamera(backgroundColour, gradientColour); + + if (!mView->getCamera()->containsNode(mGradientCamera.get())) + { + // need to re-attach the gradient camera + mView->getCamera()->setClearMask(0); + mView->getCamera()->addChild(mGradientCamera.get()); + } + } + else { - // need to re-attach the gradient camera + // need to create the gradient camera + mGradientCamera = createGradientCamera(backgroundColour, gradientColour); mView->getCamera()->setClearMask(0); mView->getCamera()->addChild(mGradientCamera.get()); } - } - else { - // need to create the gradient camera - mGradientCamera = createGradientCamera(backgroundColour, gradientColour); - mView->getCamera()->setClearMask(0); - mView->getCamera()->addChild(mGradientCamera.get()); } - } - else { - // Fall back to using the clear color for the camera - mView->getCamera()->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - mView->getCamera()->setClearColor(osg::Vec4( - backgroundColour.redF(), - backgroundColour.greenF(), - backgroundColour.blueF(), - 1.0f - )); - if (mGradientCamera.get() != nullptr && mView->getCamera()->containsNode(mGradientCamera.get())) { - // Remove the child to prevent the gradient from rendering - mView->getCamera()->removeChild(mGradientCamera.get()); + else + { + // Fall back to using the clear color for the camera + mView->getCamera()->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + mView->getCamera()->setClearColor( + osg::Vec4(backgroundColour.redF(), backgroundColour.greenF(), backgroundColour.blueF(), 1.0f)); + if (mGradientCamera.get() != nullptr && mView->getCamera()->containsNode(mGradientCamera.get())) + { + // Remove the child to prevent the gradient from rendering + mView->getCamera()->removeChild(mGradientCamera.get()); + } } } -} - -CSVWidget::SceneToolMode *SceneWidget::makeLightingSelector (CSVWidget::SceneToolbar *parent) -{ - CSVWidget::SceneToolMode *tool = new CSVWidget::SceneToolMode (parent, "Lighting Mode"); - - /// \todo replace icons - tool->addButton (":scenetoolbar/day", "day", - "Day" - "
  • Cell specific ambient in interiors
  • " - "
  • Low ambient in exteriors
  • " - "
  • Strong directional light source
  • " - "
  • This mode closely resembles day time in-game
"); - tool->addButton (":scenetoolbar/night", "night", - "Night" - "
  • Cell specific ambient in interiors
  • " - "
  • Low ambient in exteriors
  • " - "
  • Weak directional light source
  • " - "
  • This mode closely resembles night time in-game
"); - tool->addButton (":scenetoolbar/bright", "bright", - "Bright" - "
  • Maximum ambient
  • " - "
  • Strong directional light source
"); - - connect (tool, SIGNAL (modeChanged (const std::string&)), - this, SLOT (selectLightingMode (const std::string&))); - - return tool; -} - -void SceneWidget::setDefaultAmbient (const osg::Vec4f& colour) -{ - mDefaultAmbient = colour; - mHasDefaultAmbient = true; - - setAmbient(mLighting->getAmbientColour(&mDefaultAmbient)); -} - -void SceneWidget::setExterior (bool isExterior) -{ - mIsExterior = isExterior; -} - -void SceneWidget::mouseMoveEvent (QMouseEvent *event) -{ - mCurrentCamControl->handleMouseMoveEvent(event->x() - mPrevMouseX, event->y() - mPrevMouseY); - - mPrevMouseX = event->x(); - mPrevMouseY = event->y(); -} - -void SceneWidget::wheelEvent(QWheelEvent *event) -{ - mCurrentCamControl->handleMouseScrollEvent(event->angleDelta().y()); -} -void SceneWidget::update(double dt) -{ - if (mCamPositionSet) + CSVWidget::SceneToolMode* SceneWidget::makeLightingSelector(CSVWidget::SceneToolbar* parent) { - mCurrentCamControl->update(dt); - } - else + CSVWidget::SceneToolMode* tool = new CSVWidget::SceneToolMode(parent, "Lighting Mode"); + + /// \todo replace icons + tool->addButton(":scenetoolbar/day", "day", + "Day" + "
  • Cell specific ambient in interiors
  • " + "
  • Low ambient in exteriors
  • " + "
  • Strong directional light source
  • " + "
  • This mode closely resembles day time in-game
"); + tool->addButton(":scenetoolbar/night", "night", + "Night" + "
  • Cell specific ambient in interiors
  • " + "
  • Low ambient in exteriors
  • " + "
  • Weak directional light source
  • " + "
  • This mode closely resembles night time in-game
"); + tool->addButton(":scenetoolbar/bright", "bright", + "Bright" + "
  • Maximum ambient
  • " + "
  • Strong directional light source
"); + + connect(tool, &CSVWidget::SceneToolMode::modeChanged, this, &SceneWidget::selectLightingMode); + + return tool; + } + + void SceneWidget::setDefaultAmbient(const osg::Vec4f& colour) { - mCurrentCamControl->setup(mRootNode, Mask_Reference | Mask_Terrain, CameraController::WorldUp); - mCamPositionSet = true; - } -} + mDefaultAmbient = colour; + mHasDefaultAmbient = true; -void SceneWidget::settingChanged (const CSMPrefs::Setting *setting) -{ - if (*setting=="3D Scene Input/p-navi-free-sensitivity") - { - mFreeCamControl->setCameraSensitivity(setting->toDouble()); - } - else if (*setting=="3D Scene Input/p-navi-orbit-sensitivity") - { - mOrbitCamControl->setCameraSensitivity(setting->toDouble()); + setAmbient(mLighting->getAmbientColour(&mDefaultAmbient)); } - else if (*setting=="3D Scene Input/p-navi-free-invert") - { - mFreeCamControl->setInverted(setting->isTrue()); - } - else if (*setting=="3D Scene Input/p-navi-orbit-invert") - { - mOrbitCamControl->setInverted(setting->isTrue()); - } - else if (*setting=="3D Scene Input/s-navi-sensitivity") - { - mFreeCamControl->setSecondaryMovementMultiplier(setting->toDouble()); - mOrbitCamControl->setSecondaryMovementMultiplier(setting->toDouble()); - } - else if (*setting=="3D Scene Input/navi-wheel-factor") - { - mFreeCamControl->setWheelMovementMultiplier(setting->toDouble()); - mOrbitCamControl->setWheelMovementMultiplier(setting->toDouble()); - } - else if (*setting=="3D Scene Input/navi-free-lin-speed") - { - mFreeCamControl->setLinearSpeed(setting->toDouble()); - } - else if (*setting=="3D Scene Input/navi-free-rot-speed") - { - mFreeCamControl->setRotationalSpeed(setting->toDouble()); - } - else if (*setting=="3D Scene Input/navi-free-speed-mult") - { - mFreeCamControl->setSpeedMultiplier(setting->toDouble()); - } - else if (*setting=="3D Scene Input/navi-orbit-rot-speed") + + void SceneWidget::setExterior(bool isExterior) { - mOrbitCamControl->setOrbitSpeed(setting->toDouble()); + mIsExterior = isExterior; } - else if (*setting=="3D Scene Input/navi-orbit-speed-mult") + + void SceneWidget::mouseMoveEvent(QMouseEvent* event) { - mOrbitCamControl->setOrbitSpeedMultiplier(setting->toDouble()); + mCurrentCamControl->handleMouseMoveEvent(event->x() - mPrevMouseX, event->y() - mPrevMouseY); + + mPrevMouseX = event->x(); + mPrevMouseY = event->y(); } - else if (*setting=="3D Scene Input/navi-orbit-const-roll") + + void SceneWidget::wheelEvent(QWheelEvent* event) { - mOrbitCamControl->setConstRoll(setting->isTrue()); + mCurrentCamControl->handleMouseScrollEvent(event->angleDelta().y()); } - else if (*setting=="Rendering/framerate-limit") + + void SceneWidget::update(double dt) { - CompositeViewer::get().setRunMaxFrameRate(setting->toInt()); + if (mCamPositionSet) + { + mCurrentCamControl->update(dt); + } + else + { + mCurrentCamControl->setup(mRootNode, Mask_Reference | Mask_Terrain, CameraController::WorldUp); + mCamPositionSet = true; + } } - else if (*setting=="Rendering/camera-fov" || - *setting=="Rendering/camera-ortho" || - *setting=="Rendering/camera-ortho-size") + + void SceneWidget::settingChanged(const CSMPrefs::Setting* setting) { - updateCameraParameters(); + if (*setting == "3D Scene Input/p-navi-free-sensitivity") + { + mFreeCamControl->setCameraSensitivity(setting->toDouble()); + } + else if (*setting == "3D Scene Input/p-navi-orbit-sensitivity") + { + mOrbitCamControl->setCameraSensitivity(setting->toDouble()); + } + else if (*setting == "3D Scene Input/p-navi-free-invert") + { + mFreeCamControl->setInverted(setting->isTrue()); + } + else if (*setting == "3D Scene Input/p-navi-orbit-invert") + { + mOrbitCamControl->setInverted(setting->isTrue()); + } + else if (*setting == "3D Scene Input/s-navi-sensitivity") + { + mFreeCamControl->setSecondaryMovementMultiplier(setting->toDouble()); + mOrbitCamControl->setSecondaryMovementMultiplier(setting->toDouble()); + } + else if (*setting == "3D Scene Input/navi-wheel-factor") + { + mFreeCamControl->setWheelMovementMultiplier(setting->toDouble()); + mOrbitCamControl->setWheelMovementMultiplier(setting->toDouble()); + } + else if (*setting == "3D Scene Input/navi-free-lin-speed") + { + mFreeCamControl->setLinearSpeed(setting->toDouble()); + } + else if (*setting == "3D Scene Input/navi-free-rot-speed") + { + mFreeCamControl->setRotationalSpeed(setting->toDouble()); + } + else if (*setting == "3D Scene Input/navi-free-speed-mult") + { + mFreeCamControl->setSpeedMultiplier(setting->toDouble()); + } + else if (*setting == "3D Scene Input/navi-orbit-rot-speed") + { + mOrbitCamControl->setOrbitSpeed(setting->toDouble()); + } + else if (*setting == "3D Scene Input/navi-orbit-speed-mult") + { + mOrbitCamControl->setOrbitSpeedMultiplier(setting->toDouble()); + } + else if (*setting == "3D Scene Input/navi-orbit-const-roll") + { + mOrbitCamControl->setConstRoll(setting->isTrue()); + } + else if (*setting == "Rendering/framerate-limit") + { + mRenderer->setRunMaxFrameRate(setting->toInt()); + } + else if (*setting == "Rendering/camera-fov" || *setting == "Rendering/camera-ortho" + || *setting == "Rendering/camera-ortho-size") + { + updateCameraParameters(); + } + else if (*setting == "Rendering/scene-day-night-switch-nodes") + { + if (mLighting) + setLighting(mLighting); + } } -} - -void RenderWidget::updateCameraParameters(double overrideAspect) -{ - const float nearDist = 1.0; - const float farDist = 1000.0; - if (CSMPrefs::get()["Rendering"]["camera-ortho"].isTrue()) + void RenderWidget::updateCameraParameters(double overrideAspect) { - const float size = CSMPrefs::get()["Rendering"]["camera-ortho-size"].toInt(); - const float aspect = overrideAspect >= 0.0 ? overrideAspect : (width() / static_cast(height())); - const float halfH = size * 10.0; - const float halfW = halfH * aspect; + const float nearDist = 1.0; + const float farDist = 1000.0; - mView->getCamera()->setProjectionMatrixAsOrtho( - -halfW, halfW, -halfH, halfH, nearDist, farDist); - } - else - { - mView->getCamera()->setProjectionMatrixAsPerspective( - CSMPrefs::get()["Rendering"]["camera-fov"].toInt(), - static_cast(width())/static_cast(height()), - nearDist, farDist); - } -} + if (CSMPrefs::get()["Rendering"]["camera-ortho"].isTrue()) + { + const float size = CSMPrefs::get()["Rendering"]["camera-ortho-size"].toInt(); + const float aspect = overrideAspect >= 0.0 ? overrideAspect : (width() / static_cast(height())); + const float halfH = size * 10.0; + const float halfW = halfH * aspect; -void SceneWidget::selectNavigationMode (const std::string& mode) -{ - if (mode=="1st") - { - mCurrentCamControl->setCamera(nullptr); - mCurrentCamControl = mFreeCamControl; - mFreeCamControl->setCamera(getCamera()); - mFreeCamControl->fixUpAxis(CameraController::WorldUp); - } - else if (mode=="free") - { - mCurrentCamControl->setCamera(nullptr); - mCurrentCamControl = mFreeCamControl; - mFreeCamControl->setCamera(getCamera()); - mFreeCamControl->unfixUpAxis(); + mView->getCamera()->setProjectionMatrixAsOrtho(-halfW, halfW, -halfH, halfH, nearDist, farDist); + } + else + { + mView->getCamera()->setProjectionMatrixAsPerspective(CSMPrefs::get()["Rendering"]["camera-fov"].toInt(), + static_cast(width()) / static_cast(height()), nearDist, farDist); + } } - else if (mode=="orbit") + + void SceneWidget::selectNavigationMode(const std::string& mode) { - mCurrentCamControl->setCamera(nullptr); - mCurrentCamControl = mOrbitCamControl; - mOrbitCamControl->setCamera(getCamera()); - mOrbitCamControl->reset(); + if (mode == "1st") + { + mCurrentCamControl->setCamera(nullptr); + mCurrentCamControl = mFreeCamControl; + mFreeCamControl->setCamera(getCamera()); + mFreeCamControl->fixUpAxis(CameraController::WorldUp); + } + else if (mode == "free") + { + mCurrentCamControl->setCamera(nullptr); + mCurrentCamControl = mFreeCamControl; + mFreeCamControl->setCamera(getCamera()); + mFreeCamControl->unfixUpAxis(); + } + else if (mode == "orbit") + { + mCurrentCamControl->setCamera(nullptr); + mCurrentCamControl = mOrbitCamControl; + mOrbitCamControl->setCamera(getCamera()); + mOrbitCamControl->reset(); + } } -} } diff --git a/apps/opencs/view/render/scenewidget.hpp b/apps/opencs/view/render/scenewidget.hpp index 922776e9fbd..228581a0ef6 100644 --- a/apps/opencs/view/render/scenewidget.hpp +++ b/apps/opencs/view/render/scenewidget.hpp @@ -1,19 +1,28 @@ #ifndef OPENCS_VIEW_SCENEWIDGET_H #define OPENCS_VIEW_SCENEWIDGET_H -#include #include +#include -#include +#include +#include #include +#include + +#include +#include -#include #include +#include "lightingbright.hpp" #include "lightingday.hpp" #include "lightingnight.hpp" -#include "lightingbright.hpp" +class QMouseEvent; +class QWheelEvent; + +class osgQOpenGLWidget; +class CompositeOsgRenderer; namespace Resource { @@ -24,6 +33,12 @@ namespace osg { class Group; class Camera; + class Geometry; +} + +namespace osg +{ + class View; } namespace CSVWidget @@ -46,126 +61,101 @@ namespace CSVRender class RenderWidget : public QWidget { - Q_OBJECT + Q_OBJECT - public: - RenderWidget(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); - virtual ~RenderWidget(); + public: + RenderWidget(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); + virtual ~RenderWidget(); - /// Initiates a request to redraw the view - void flagAsModified(); + /// Initiates a request to redraw the view + void flagAsModified(); - void setVisibilityMask(unsigned int mask); + void setVisibilityMask(unsigned int mask); - osg::Camera *getCamera(); + osg::Camera* getCamera(); - protected: + protected: + osgQOpenGLWidget* mWidget; + CompositeOsgRenderer* mRenderer; + osg::ref_ptr mView; + osg::ref_ptr mRootNode; - osg::ref_ptr mView; - osg::ref_ptr mRootNode; + void updateCameraParameters(double overrideAspect = -1.0); - void updateCameraParameters(double overrideAspect = -1.0); + protected slots: - QTimer mTimer; - - protected slots: - - void toggleRenderStats(); + void toggleRenderStats(); }; /// Extension of RenderWidget to support lighting mode selection & toolbar class SceneWidget : public RenderWidget { - Q_OBJECT - public: - SceneWidget(std::shared_ptr resourceSystem, QWidget* parent = nullptr, - Qt::WindowFlags f = Qt::WindowFlags(), bool retrieveInput = true); - virtual ~SceneWidget(); - - CSVWidget::SceneToolMode *makeLightingSelector (CSVWidget::SceneToolbar *parent); - ///< \attention The created tool is not added to the toolbar (via addTool). Doing that - /// is the responsibility of the calling function. - - void setDefaultAmbient (const osg::Vec4f& colour); - ///< \note The actual ambient colour may differ based on lighting settings. - - void setExterior (bool isExterior); + Q_OBJECT + public: + SceneWidget(std::shared_ptr resourceSystem, QWidget* parent = nullptr, + Qt::WindowFlags f = Qt::WindowFlags(), bool retrieveInput = true); + virtual ~SceneWidget(); - protected: - void setLighting (Lighting *lighting); - ///< \attention The ownership of \a lighting is not transferred to *this. + CSVWidget::SceneToolMode* makeLightingSelector(CSVWidget::SceneToolbar* parent); + ///< \attention The created tool is not added to the toolbar (via addTool). Doing that + /// is the responsibility of the calling function. - void setAmbient(const osg::Vec4f& ambient); + void setDefaultAmbient(const osg::Vec4f& colour); + ///< \note The actual ambient colour may differ based on lighting settings. - void mouseMoveEvent (QMouseEvent *event) override; - void wheelEvent (QWheelEvent *event) override; + void setExterior(bool isExterior); - osg::ref_ptr createGradientRectangle(QColor bgColour, QColor gradientColour); - osg::ref_ptr createGradientCamera(QColor bgColour, QColor gradientColour); - void updateGradientCamera(QColor bgColour, QColor gradientColour); + protected: + void setLighting(Lighting* lighting); + ///< \attention The ownership of \a lighting is not transferred to *this. - std::shared_ptr mResourceSystem; + void setAmbient(const osg::Vec4f& ambient); - Lighting* mLighting; - - osg::ref_ptr mGradientCamera; - osg::Vec4f mDefaultAmbient; - bool mHasDefaultAmbient; - bool mIsExterior; - LightingDay mLightingDay; - LightingNight mLightingNight; - LightingBright mLightingBright; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; - int mPrevMouseX, mPrevMouseY; - - /// Tells update that camera isn't set - bool mCamPositionSet; + osg::ref_ptr createGradientRectangle(QColor& bgColour, QColor& gradientColour); + osg::ref_ptr createGradientCamera(QColor& bgColour, QColor& gradientColour); + void updateGradientCamera(QColor& bgColour, QColor& gradientColour); - FreeCameraController* mFreeCamControl; - OrbitCameraController* mOrbitCamControl; - CameraController* mCurrentCamControl; + std::shared_ptr mResourceSystem; - public slots: - void update(double dt); + Lighting* mLighting; - protected slots: + osg::ref_ptr mGradientCamera; + osg::Vec4f mDefaultAmbient; + bool mHasDefaultAmbient; + bool mIsExterior; + LightingDay mLightingDay; + LightingNight mLightingNight; + LightingBright mLightingBright; - virtual void settingChanged (const CSMPrefs::Setting *setting); + int mPrevMouseX, mPrevMouseY; - void selectNavigationMode (const std::string& mode); + /// Tells update that camera isn't set + bool mCamPositionSet; - private slots: + FreeCameraController* mFreeCamControl; + OrbitCameraController* mOrbitCamControl; + CameraController* mCurrentCamControl; - void selectLightingMode (const std::string& mode); + public slots: + void update(double dt); - signals: - - void focusToolbarRequest(); - }; + protected slots: + virtual void settingChanged(const CSMPrefs::Setting* setting); - // There are rendering glitches when using multiple Viewer instances, work around using CompositeViewer with multiple views - class CompositeViewer : public QObject, public osgViewer::CompositeViewer - { - Q_OBJECT - public: - CompositeViewer(); + void selectNavigationMode(const std::string& mode); - static CompositeViewer& get(); + private slots: - QTimer mTimer; + void selectLightingMode(const std::string& mode); - private: - osg::Timer mFrameTimer; - double mSimulationTime; + signals: - public slots: - void update(); - - signals: - void simulationUpdated(double dt); + void focusToolbarRequest(); }; - } #endif diff --git a/apps/opencs/view/render/selectionmode.cpp b/apps/opencs/view/render/selectionmode.cpp index e7e7d47b5a9..1dabaed35be 100644 --- a/apps/opencs/view/render/selectionmode.cpp +++ b/apps/opencs/view/render/selectionmode.cpp @@ -1,49 +1,61 @@ #include "selectionmode.hpp" -#include #include +#include + +#include + +#include #include "worldspacewidget.hpp" +namespace CSVWidget +{ + class SceneToolbar; +} + namespace CSVRender { - SelectionMode::SelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, - unsigned int interactionMask) + SelectionMode::SelectionMode( + CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, unsigned int interactionMask) : SceneToolMode(parent, "Selection mode") , mWorldspaceWidget(worldspaceWidget) , mInteractionMask(interactionMask) { addButton(":scenetoolbar/selection-mode-cube", "cube-centre", "Centred cube" - "
  • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select " - "from the centre of the selection cube outwards.
  • " + "
    • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary " + "select " + "from the centre of the selection cube outwards.
    • " "
    • The selection cube is aligned to the word space axis
    • " "
    • If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not " - "starting on an instance will have the same effect
    • " + "starting on an instance will have the same effect" "
    "); addButton(":scenetoolbar/selection-mode-cube-corner", "cube-corner", "Cube corner to corner" - "
    • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select " - "from one corner of the selection cube to the opposite corner
    • " + "
      • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary " + "select " + "from one corner of the selection cube to the opposite corner
      • " "
      • The selection cube is aligned to the word space axis
      • " "
      • If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not " - "starting on an instance will have the same effect
      • " + "starting on an instance will have the same effect" "
      "); addButton(":scenetoolbar/selection-mode-cube-sphere", "sphere", "Centred sphere" - "
      • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select " - "from the centre of the selection sphere outwards
      • " + "
        • Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary " + "select " + "from the centre of the selection sphere outwards
        • " "
        • If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not " - "starting on an instance will have the same effect
        • " + "starting on an instance will have the same effect" "
        "); mSelectAll = new QAction("Select all", this); mDeselectAll = new QAction("Clear selection", this); mInvertSelection = new QAction("Invert selection", this); - connect(mSelectAll, SIGNAL(triggered()), this, SLOT(selectAll())); - connect(mDeselectAll, SIGNAL(triggered()), this, SLOT(clearSelection())); - connect(mInvertSelection, SIGNAL(triggered()), this, SLOT(invertSelection())); + connect(mSelectAll, &QAction::triggered, this, &SelectionMode::selectAll); + connect(mDeselectAll, &QAction::triggered, this, &SelectionMode::clearSelection); + connect(mInvertSelection, &QAction::triggered, this, &SelectionMode::invertSelection); } WorldspaceWidget& SelectionMode::getWorldspaceWidget() @@ -51,7 +63,7 @@ namespace CSVRender return mWorldspaceWidget; } - bool SelectionMode::createContextMenu (QMenu* menu) + bool SelectionMode::createContextMenu(QMenu* menu) { if (menu) { diff --git a/apps/opencs/view/render/selectionmode.hpp b/apps/opencs/view/render/selectionmode.hpp index 95f6de41b40..34d186c9401 100644 --- a/apps/opencs/view/render/selectionmode.hpp +++ b/apps/opencs/view/render/selectionmode.hpp @@ -3,48 +3,48 @@ #include "../widget/scenetoolmode.hpp" -#include "mask.hpp" - class QAction; +namespace CSVWidget +{ + class SceneToolbar; +} + namespace CSVRender { class WorldspaceWidget; class SelectionMode : public CSVWidget::SceneToolMode { - Q_OBJECT - - public: - - SelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, - unsigned int interactionMask); - - protected: - - WorldspaceWidget& getWorldspaceWidget(); - - /// Add context menu items to \a menu. - /// - /// \attention menu can be a 0-pointer - /// - /// \return Have there been any menu items to be added (if menu is 0 and there - /// items to be added, the function must return true anyway. - bool createContextMenu (QMenu* menu) override; - - private: - - WorldspaceWidget& mWorldspaceWidget; - unsigned int mInteractionMask; - QAction* mSelectAll; - QAction* mDeselectAll; - QAction* mInvertSelection; - - protected slots: - - virtual void selectAll(); - virtual void clearSelection(); - virtual void invertSelection(); + Q_OBJECT + + public: + SelectionMode( + CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, unsigned int interactionMask); + + protected: + WorldspaceWidget& getWorldspaceWidget(); + + /// Add context menu items to \a menu. + /// + /// \attention menu can be a 0-pointer + /// + /// \return Have there been any menu items to be added (if menu is 0 and there + /// items to be added, the function must return true anyway. + bool createContextMenu(QMenu* menu) override; + + private: + WorldspaceWidget& mWorldspaceWidget; + unsigned int mInteractionMask; + QAction* mSelectAll; + QAction* mDeselectAll; + QAction* mInvertSelection; + + protected slots: + + virtual void selectAll(); + virtual void clearSelection(); + virtual void invertSelection(); }; } diff --git a/apps/opencs/view/render/tagbase.cpp b/apps/opencs/view/render/tagbase.cpp index 3ddd68690f7..b52a553c978 100644 --- a/apps/opencs/view/render/tagbase.cpp +++ b/apps/opencs/view/render/tagbase.cpp @@ -1,14 +1,18 @@ - #include "tagbase.hpp" -CSVRender::TagBase::TagBase (Mask mask) : mMask (mask) {} +#include + +CSVRender::TagBase::TagBase(Mask mask) + : mMask(mask) +{ +} CSVRender::Mask CSVRender::TagBase::getMask() const { return mMask; } -QString CSVRender::TagBase::getToolTip (bool hideBasics) const +QString CSVRender::TagBase::getToolTip(bool hideBasics, const WorldspaceHitResult& /*hit*/) const { return ""; } diff --git a/apps/opencs/view/render/tagbase.hpp b/apps/opencs/view/render/tagbase.hpp index d1ecd2cfd96..a93267b6f44 100644 --- a/apps/opencs/view/render/tagbase.hpp +++ b/apps/opencs/view/render/tagbase.hpp @@ -9,18 +9,18 @@ namespace CSVRender { + struct WorldspaceHitResult; + class TagBase : public osg::Referenced { - Mask mMask; - - public: - - TagBase (Mask mask); + Mask mMask; - Mask getMask() const; + public: + TagBase(Mask mask); - virtual QString getToolTip (bool hideBasics) const; + Mask getMask() const; + virtual QString getToolTip(bool hideBasics, const WorldspaceHitResult& hit) const; }; } diff --git a/apps/opencs/view/render/terrainselection.cpp b/apps/opencs/view/render/terrainselection.cpp index 0593917e0a7..bdeb1a0958f 100644 --- a/apps/opencs/view/render/terrainselection.cpp +++ b/apps/opencs/view/render/terrainselection.cpp @@ -1,26 +1,45 @@ #include "terrainselection.hpp" #include +#include -#include +#include #include +#include #include +#include +#include +#include +#include -#include +#include +#include +#include +#include +#include +#include -#include "../../model/world/cellcoordinates.hpp" -#include "../../model/world/columnimp.hpp" -#include "../../model/world/idtable.hpp" +#include #include "cell.hpp" #include "worldspacewidget.hpp" -CSVRender::TerrainSelection::TerrainSelection(osg::Group* parentNode, WorldspaceWidget *worldspaceWidget, TerrainSelectionType type): -mParentNode(parentNode), mWorldspaceWidget (worldspaceWidget), mDraggedOperationFlag(false), mSelectionType(type) +namespace CSMWorld +{ + struct Cell; +} + +CSVRender::TerrainSelection::TerrainSelection( + osg::Group* parentNode, WorldspaceWidget* worldspaceWidget, TerrainSelectionType type) + : mParentNode(parentNode) + , mWorldspaceWidget(worldspaceWidget) + , mSelectionType(type) { mGeometry = new osg::Geometry(); mSelectionNode = new osg::Group(); + mSelectionNode->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + mSelectionNode->getOrCreateStateSet()->setRenderBinDetails(11, "RenderBin"); mSelectionNode->addChild(mGeometry); activate(); @@ -36,31 +55,37 @@ std::vector> CSVRender::TerrainSelection::getTerrainSelectio return mSelection; } -void CSVRender::TerrainSelection::onlySelect(const std::vector> &localPositions) +void CSVRender::TerrainSelection::onlySelect(const std::vector>& localPositions) { mSelection = localPositions; update(); } -void CSVRender::TerrainSelection::addSelect(const std::vector>& localPositions, bool toggleInProgress) +void CSVRender::TerrainSelection::addSelect(const std::vector>& localPositions) { - handleSelection(localPositions, toggleInProgress, SelectionMethod::AddSelect); + handleSelection(localPositions, SelectionMethod::AddSelect); } -void CSVRender::TerrainSelection::removeSelect(const std::vector>& localPositions, bool toggleInProgress) +void CSVRender::TerrainSelection::removeSelect(const std::vector>& localPositions) { - handleSelection(localPositions, toggleInProgress, SelectionMethod::RemoveSelect); + handleSelection(localPositions, SelectionMethod::RemoveSelect); } -void CSVRender::TerrainSelection::toggleSelect(const std::vector>& localPositions, bool toggleInProgress) +void CSVRender::TerrainSelection::toggleSelect(const std::vector>& localPositions) { - handleSelection(localPositions, toggleInProgress, SelectionMethod::ToggleSelect); + handleSelection(localPositions, SelectionMethod::ToggleSelect); +} + +void CSVRender::TerrainSelection::clearTemporarySelection() +{ + mTemporarySelection.clear(); } void CSVRender::TerrainSelection::activate() { - if (!mParentNode->containsNode(mSelectionNode)) mParentNode->addChild(mSelectionNode); + if (!mParentNode->containsNode(mSelectionNode)) + mParentNode->addChild(mSelectionNode); } void CSVRender::TerrainSelection::deactivate() @@ -73,20 +98,23 @@ void CSVRender::TerrainSelection::update() mSelectionNode->removeChild(mGeometry); mGeometry = new osg::Geometry(); - const osg::ref_ptr vertices (new osg::Vec3Array); + const osg::ref_ptr vertices(new osg::Vec3Array); switch (mSelectionType) { - case TerrainSelectionType::Texture : drawTextureSelection(vertices); - break; - case TerrainSelectionType::Shape : drawShapeSelection(vertices); - break; + case TerrainSelectionType::Texture: + drawTextureSelection(vertices); + break; + case TerrainSelectionType::Shape: + drawShapeSelection(vertices); + break; } mGeometry->setVertexArray(vertices); osg::ref_ptr drawArrays = new osg::DrawArrays(osg::PrimitiveSet::LINES); drawArrays->setCount(vertices->size()); - if (vertices->size() != 0) mGeometry->addPrimitiveSet(drawArrays); + if (vertices->size() != 0) + mGeometry->addPrimitiveSet(drawArrays); mSelectionNode->addChild(mGeometry); } @@ -94,34 +122,28 @@ void CSVRender::TerrainSelection::drawShapeSelection(const osg::ref_ptr &localPos : mSelection) + for (std::pair& localPos : mSelection) { - int x (localPos.first); - int y (localPos.second); + int x(localPos.first); + int y(localPos.second); float xWorldCoord(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x)); float yWorldCoord(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y)); - osg::Vec3f pointXY(xWorldCoord, yWorldCoord, calculateLandHeight(x, y) + 2); + osg::Vec3f pointXY(xWorldCoord, yWorldCoord, calculateLandHeight(x, y)); vertices->push_back(pointXY); - vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y - 1), calculateLandHeight(x, y - 1) + 2)); + vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y - 1), + calculateLandHeight(x, y - 1))); vertices->push_back(pointXY); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x - 1), yWorldCoord, calculateLandHeight(x - 1, y) + 2)); - - const auto north = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x, y + 1)); - if (north == mSelection.end()) - { - vertices->push_back(pointXY); - vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y + 1), calculateLandHeight(x, y + 1) + 2)); - } - - const auto east = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x + 1, y)); - if (east == mSelection.end()) - { - vertices->push_back(pointXY); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x + 1), yWorldCoord, calculateLandHeight(x + 1, y) + 2)); - } + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x - 1), yWorldCoord, + calculateLandHeight(x - 1, y))); + vertices->push_back(pointXY); + vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y + 1), + calculateLandHeight(x, y + 1))); + vertices->push_back(pointXY); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x + 1), yWorldCoord, + calculateLandHeight(x + 1, y))); } } } @@ -130,14 +152,15 @@ void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptr &localPos : mSelection) + for (std::pair& localPos : mSelection) { - int x (localPos.first); - int y (localPos.second); + int x(localPos.first); + int y(localPos.second); // convert texture selection to global vertex coordinates at selection box corners int x1 = x * textureSizeToLandSizeModifier + landHeightsNudge; @@ -150,143 +173,120 @@ void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptrpush_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+(i-1), y2)+2)); - vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+i, y2)+2)); + float drawPreviousX = CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x) + + (i - 1) * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); + float drawCurrentX = CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x) + + i * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); + vertices->push_back( + osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), + calculateLandHeight(x1 + (i - 1), y2))); + vertices->push_back( + osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), + calculateLandHeight(x1 + i, y2))); } } const auto south = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x, y - 1)); if (south == mSelection.end()) { - for(int i = 1; i < (textureSizeToLandSizeModifier + 1); i++) + for (int i = 1; i < (textureSizeToLandSizeModifier + 1); i++) { - float drawPreviousX = CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x) + (i - 1) *(ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); - float drawCurrentX = CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x) + i * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); - vertices->push_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+(i-1), y1)+2)); - vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+i, y1)+2)); + float drawPreviousX = CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x) + + (i - 1) * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); + float drawCurrentX = CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x) + + i * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); + vertices->push_back( + osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), + calculateLandHeight(x1 + (i - 1), y1))); + vertices->push_back(osg::Vec3f(drawCurrentX, + CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1 + i, y1))); } } const auto east = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x + 1, y)); if (east == mSelection.end()) { - for(int i = 1; i < (textureSizeToLandSizeModifier + 1); i++) + for (int i = 1; i < (textureSizeToLandSizeModifier + 1); i++) { - float drawPreviousY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + (i - 1) * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); - float drawCurrentY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + i * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawPreviousY, calculateLandHeight(x2, y1+(i-1))+2)); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawCurrentY, calculateLandHeight(x2, y1+i)+2)); + float drawPreviousY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + + (i - 1) * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); + float drawCurrentY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + + i * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), + drawPreviousY, calculateLandHeight(x2, y1 + (i - 1)))); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), + drawCurrentY, calculateLandHeight(x2, y1 + i))); } } const auto west = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x - 1, y)); if (west == mSelection.end()) { - for(int i = 1; i < (textureSizeToLandSizeModifier + 1); i++) + for (int i = 1; i < (textureSizeToLandSizeModifier + 1); i++) { - float drawPreviousY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + (i - 1) * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); - float drawCurrentY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + i * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawPreviousY, calculateLandHeight(x1, y1+(i-1))+2)); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawCurrentY, calculateLandHeight(x1, y1+i)+2)); + float drawPreviousY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + + (i - 1) * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); + float drawCurrentY = CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y) + + i * (ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1)); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), + drawPreviousY, calculateLandHeight(x1, y1 + (i - 1)))); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), + drawCurrentY, calculateLandHeight(x1, y1 + i))); } } } } } -void CSVRender::TerrainSelection::handleSelection(const std::vector>& localPositions, bool toggleInProgress, SelectionMethod selectionMethod) +void CSVRender::TerrainSelection::handleSelection( + const std::vector>& localPositions, SelectionMethod selectionMethod) { - if (toggleInProgress) + for (auto const& localPos : localPositions) { - for (auto const& localPos : localPositions) - { - auto iterTemp = std::find(mTemporarySelection.begin(), mTemporarySelection.end(), localPos); - mDraggedOperationFlag = true; - - if (iterTemp == mTemporarySelection.end()) - { - auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); - - switch (selectionMethod) - { - case SelectionMethod::AddSelect: - if (iter == mSelection.end()) - { - mSelection.emplace_back(localPos); - } - - break; - case SelectionMethod::RemoveSelect: - if (iter != mSelection.end()) - { - mSelection.erase(iter); - } - - break; - case SelectionMethod::ToggleSelect: - if (iter == mSelection.end()) - { - mSelection.emplace_back(localPos); - } - else - { - mSelection.erase(iter); - } + const auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); - break; - default: break; - } - } - - mTemporarySelection.push_back(localPos); - } - } - else if (mDraggedOperationFlag == false) - { - for (auto const& localPos : localPositions) + switch (selectionMethod) { - const auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); + case SelectionMethod::OnlySelect: + break; - switch (selectionMethod) - { case SelectionMethod::AddSelect: if (iter == mSelection.end()) { mSelection.emplace_back(localPos); } - break; + case SelectionMethod::RemoveSelect: if (iter != mSelection.end()) { mSelection.erase(iter); } - break; + case SelectionMethod::ToggleSelect: - if (iter == mSelection.end()) - { - mSelection.emplace_back(localPos); - } - else + { + const auto iterTemp = std::find(mTemporarySelection.begin(), mTemporarySelection.end(), localPos); + if (iterTemp == mTemporarySelection.end()) { - mSelection.erase(iter); + if (iter == mSelection.end()) + { + mSelection.emplace_back(localPos); + } + else + { + mSelection.erase(iter); + } } - + mTemporarySelection.emplace_back(localPos); break; - default: break; } - } - } - else - { - mDraggedOperationFlag = false; - mTemporarySelection.clear(); + default: + break; + } } update(); @@ -296,26 +296,27 @@ bool CSVRender::TerrainSelection::noCell(const std::string& cellId) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); const CSMWorld::IdCollection& cellCollection = document.getData().getCells(); - return cellCollection.searchId (cellId) == -1; + return cellCollection.searchId(ESM::RefId::stringRefId(cellId)) == -1; } bool CSVRender::TerrainSelection::noLand(const std::string& cellId) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); const CSMWorld::IdCollection& landCollection = document.getData().getLand(); - return landCollection.searchId (cellId) == -1; + return landCollection.searchId(ESM::RefId::stringRefId(cellId)) == -1; } bool CSVRender::TerrainSelection::noLandLoaded(const std::string& cellId) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); const CSMWorld::IdCollection& landCollection = document.getData().getLand(); - return !landCollection.getRecord(cellId).get().isDataLoaded(ESM::Land::DATA_VNML); + return !landCollection.getRecord(ESM::RefId::stringRefId(cellId)).get().isDataLoaded(ESM::Land::DATA_VNML); } bool CSVRender::TerrainSelection::isLandLoaded(const std::string& cellId) { - if (!noCell(cellId) && !noLand(cellId) && !noLandLoaded(cellId)) return true; + if (!noCell(cellId) && !noLand(cellId) && !noLandLoaded(cellId)) + return true; return false; } @@ -326,7 +327,7 @@ int CSVRender::TerrainSelection::calculateLandHeight(int x, int y) // global ver int localX = x - cellX * (ESM::Land::LAND_SIZE - 1); int localY = y - cellY * (ESM::Land::LAND_SIZE - 1); - CSMWorld::CellCoordinates coords (cellX, cellY); + CSMWorld::CellCoordinates coords(cellX, cellY); float landHeight = 0.f; if (CSVRender::Cell* cell = dynamic_cast(mWorldspaceWidget->getCell(coords))) @@ -336,11 +337,13 @@ int CSVRender::TerrainSelection::calculateLandHeight(int x, int y) // global ver else if (isLandLoaded(CSMWorld::CellCoordinates::generateId(cellX, cellY))) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); std::string cellId = CSMWorld::CellCoordinates::generateId(cellX, cellY); - int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); - const CSMWorld::LandHeightsColumn::DataType mPointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); - return mPointer[localY*ESM::Land::LAND_SIZE + localX]; + const ESM::Land::LandData* landData = document.getData() + .getLand() + .getRecord(ESM::RefId::stringRefId(cellId)) + .get() + .getLandData(ESM::Land::DATA_VHGT); + return landData->mHeights[localY * ESM::Land::LAND_SIZE + localX]; } return landHeight; diff --git a/apps/opencs/view/render/terrainselection.hpp b/apps/opencs/view/render/terrainselection.hpp index 18622ad13a8..b451a007b80 100644 --- a/apps/opencs/view/render/terrainselection.hpp +++ b/apps/opencs/view/render/terrainselection.hpp @@ -1,24 +1,22 @@ #ifndef CSV_RENDER_TERRAINSELECTION_H #define CSV_RENDER_TERRAINSELECTION_H +#include #include #include -#include +#include #include -#include - -#include -#include "../../model/world/cellcoordinates.hpp" namespace osg { class Group; + class Geometry; + class PositionAttitudeTransform; } namespace CSVRender { - struct WorldspaceHitResult; class WorldspaceWidget; enum class TerrainSelectionType @@ -38,51 +36,50 @@ namespace CSVRender /// \brief Class handling the terrain selection data and rendering class TerrainSelection { - public: - - TerrainSelection(osg::Group* parentNode, WorldspaceWidget *worldspaceWidget, TerrainSelectionType type); - ~TerrainSelection(); - - void onlySelect(const std::vector> &localPositions); - void addSelect(const std::vector>& localPositions, bool toggleInProgress); - void removeSelect(const std::vector>& localPositions, bool toggleInProgress); - void toggleSelect(const std::vector> &localPositions, bool toggleInProgress); + public: + TerrainSelection(osg::Group* parentNode, WorldspaceWidget* worldspaceWidget, TerrainSelectionType type); + ~TerrainSelection(); - void activate(); - void deactivate(); + void onlySelect(const std::vector>& localPositions); + void addSelect(const std::vector>& localPositions); + void removeSelect(const std::vector>& localPositions); + void toggleSelect(const std::vector>& localPositions); + void clearTemporarySelection(); - std::vector> getTerrainSelection() const; + void activate(); + void deactivate(); - void update(); + std::vector> getTerrainSelection() const; - protected: + void update(); - void drawShapeSelection(const osg::ref_ptr vertices); - void drawTextureSelection(const osg::ref_ptr vertices); + protected: + void drawShapeSelection(const osg::ref_ptr vertices); + void drawTextureSelection(const osg::ref_ptr vertices); - int calculateLandHeight(int x, int y); + int calculateLandHeight(int x, int y); - private: + private: + void handleSelection(const std::vector>& localPositions, SelectionMethod selectionMethod); - void handleSelection(const std::vector>& localPositions, bool toggleInProgress, SelectionMethod selectionMethod); + bool noCell(const std::string& cellId); - bool noCell(const std::string& cellId); + bool noLand(const std::string& cellId); - bool noLand(const std::string& cellId); + bool noLandLoaded(const std::string& cellId); - bool noLandLoaded(const std::string& cellId); + bool isLandLoaded(const std::string& cellId); - bool isLandLoaded(const std::string& cellId); - - osg::Group* mParentNode; - WorldspaceWidget *mWorldspaceWidget; - osg::ref_ptr mBaseNode; - osg::ref_ptr mGeometry; - osg::ref_ptr mSelectionNode; - std::vector> mSelection; // Global terrain selection coordinate in either vertex or texture units - std::vector> mTemporarySelection; // Used during toggle to compare the most recent drag operation - bool mDraggedOperationFlag; //true during drag operation, false when click-operation - TerrainSelectionType mSelectionType; + osg::Group* mParentNode; + WorldspaceWidget* mWorldspaceWidget; + osg::ref_ptr mBaseNode; + osg::ref_ptr mGeometry; + osg::ref_ptr mSelectionNode; + std::vector> + mSelection; // Global terrain selection coordinate in either vertex or texture units + std::vector> + mTemporarySelection; // Used during toggle to compare the most recent drag operation + TerrainSelectionType mSelectionType; }; } diff --git a/apps/opencs/view/render/terrainshapemode.cpp b/apps/opencs/view/render/terrainshapemode.cpp index 866ff69cde0..b9dc301efa3 100644 --- a/apps/opencs/view/render/terrainshapemode.cpp +++ b/apps/opencs/view/render/terrainshapemode.cpp @@ -1,50 +1,71 @@ #include "terrainshapemode.hpp" #include -#include -#include +#include #include +#include -#include -#include -#include +#include #include -#include -#include +#include +#include +#include -#include +#include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include +#include +#include -#include "../widget/brushshapes.hpp" -#include "../widget/modebutton.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolshapebrush.hpp" -#include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" #include "../../model/world/commands.hpp" -#include "../../model/world/data.hpp" -#include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" -#include "../../model/world/land.hpp" -#include "../../model/world/tablemimedata.hpp" -#include "../../model/world/universalid.hpp" #include "brushdraw.hpp" #include "commands.hpp" #include "editmode.hpp" -#include "pagedworldspacewidget.hpp" #include "mask.hpp" -#include "tagbase.hpp" +#include "pagedworldspacewidget.hpp" #include "terrainselection.hpp" #include "worldspacewidget.hpp" -CSVRender::TerrainShapeMode::TerrainShapeMode (WorldspaceWidget *worldspaceWidget, osg::Group* parentNode, QWidget *parent) -: EditMode (worldspaceWidget, QIcon {":scenetoolbar/editing-terrain-shape"}, Mask_Terrain, "Terrain land editing", parent), - mParentNode(parentNode) +class QPoint; +class QWidget; + +namespace CSMWorld +{ + struct Cell; +} + +namespace osg +{ + class Group; +} + +CSVRender::TerrainShapeMode::TerrainShapeMode( + WorldspaceWidget* worldspaceWidget, osg::Group* parentNode, QWidget* parent) + : EditMode(worldspaceWidget, Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-shape"), Mask_Terrain, + "Terrain land editing", parent) + , mParentNode(parentNode) { } @@ -52,32 +73,40 @@ void CSVRender::TerrainShapeMode::activate(CSVWidget::SceneToolbar* toolbar) { if (!mTerrainShapeSelection) { - mTerrainShapeSelection.reset(new TerrainSelection(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Shape)); + mTerrainShapeSelection + = std::make_shared(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Shape); } - if(!mShapeBrushScenetool) + if (!mShapeBrushScenetool) { - mShapeBrushScenetool = new CSVWidget::SceneToolShapeBrush (toolbar, "scenetoolshapebrush", getWorldspaceWidget().getDocument()); - connect(mShapeBrushScenetool, SIGNAL (clicked()), mShapeBrushScenetool, SLOT (activate())); - connect(mShapeBrushScenetool->mShapeBrushWindow, SIGNAL(passBrushSize(int)), this, SLOT(setBrushSize(int))); - connect(mShapeBrushScenetool->mShapeBrushWindow, SIGNAL(passBrushShape(CSVWidget::BrushShape)), this, SLOT(setBrushShape(CSVWidget::BrushShape))); - connect(mShapeBrushScenetool->mShapeBrushWindow->mSizeSliders->mBrushSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(setBrushSize(int))); - connect(mShapeBrushScenetool->mShapeBrushWindow->mToolSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(setShapeEditTool(int))); - connect(mShapeBrushScenetool->mShapeBrushWindow->mToolStrengthSlider, SIGNAL(valueChanged(int)), this, SLOT(setShapeEditToolStrength(int))); + mShapeBrushScenetool + = new CSVWidget::SceneToolShapeBrush(toolbar, "scenetoolshapebrush", getWorldspaceWidget().getDocument()); + connect(mShapeBrushScenetool, &CSVWidget::SceneTool::clicked, mShapeBrushScenetool, + &CSVWidget::SceneToolShapeBrush::activate); + connect(mShapeBrushScenetool->mShapeBrushWindow, &CSVWidget::ShapeBrushWindow::passBrushSize, this, + &TerrainShapeMode::setBrushSize); + connect(mShapeBrushScenetool->mShapeBrushWindow, &CSVWidget::ShapeBrushWindow::passBrushShape, this, + &TerrainShapeMode::setBrushShape); + connect(mShapeBrushScenetool->mShapeBrushWindow->mSizeSliders->mBrushSizeSlider, &QSlider::valueChanged, this, + &TerrainShapeMode::setBrushSize); + connect(mShapeBrushScenetool->mShapeBrushWindow->mToolSelector, qOverload(&QComboBox::currentIndexChanged), + this, &TerrainShapeMode::setShapeEditTool); + connect(mShapeBrushScenetool->mShapeBrushWindow->mToolStrengthSlider, &QSlider::valueChanged, this, + &TerrainShapeMode::setShapeEditToolStrength); } if (!mBrushDraw) - mBrushDraw.reset(new BrushDraw(mParentNode)); + mBrushDraw = std::make_unique(mParentNode); EditMode::activate(toolbar); - toolbar->addTool (mShapeBrushScenetool); + toolbar->addTool(mShapeBrushScenetool); } void CSVRender::TerrainShapeMode::deactivate(CSVWidget::SceneToolbar* toolbar) { - if(mShapeBrushScenetool) + if (mShapeBrushScenetool) { - toolbar->removeTool (mShapeBrushScenetool); + toolbar->removeTool(mShapeBrushScenetool); } if (mTerrainShapeSelection) @@ -91,7 +120,7 @@ void CSVRender::TerrainShapeMode::deactivate(CSVWidget::SceneToolbar* toolbar) EditMode::deactivate(toolbar); } -void CSVRender::TerrainShapeMode::primaryOpenPressed (const WorldspaceHitResult& hit) // Apply changes here +void CSVRender::TerrainShapeMode::primaryOpenPressed(const WorldspaceHitResult& hit) // Apply changes here { } @@ -99,46 +128,38 @@ void CSVRender::TerrainShapeMode::primaryEditPressed(const WorldspaceHitResult& { if (hit.hit && hit.tag == nullptr) { - if (mShapeEditTool == ShapeEditTool_Flatten) + if (mShapeEditTool == ShapeEditTool_Flatten || mShapeEditTool == ShapeEditTool_Equalize) setFlattenToolTargetHeight(hit); if (mDragMode == InteractionType_PrimaryEdit && mShapeEditTool != ShapeEditTool_Drag) { editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); applyTerrainEditChanges(); } - - if (mDragMode == InteractionType_PrimarySelect) - { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); - } - - if (mDragMode == InteractionType_SecondarySelect) - { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); - } } clearTransientEdits(); } void CSVRender::TerrainShapeMode::primarySelectPressed(const WorldspaceHitResult& hit) { - if(hit.hit && hit.tag == nullptr) + if (hit.hit && hit.tag == nullptr) { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, false); + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); + mTerrainShapeSelection->clearTemporarySelection(); } } void CSVRender::TerrainShapeMode::secondarySelectPressed(const WorldspaceHitResult& hit) { - if(hit.hit && hit.tag == nullptr) + if (hit.hit && hit.tag == nullptr) { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, false); + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); + mTerrainShapeSelection->clearTemporarySelection(); } } -bool CSVRender::TerrainShapeMode::primaryEditStartDrag (const QPoint& pos) +bool CSVRender::TerrainShapeMode::primaryEditStartDrag(const QPoint& pos) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_PrimaryEdit; @@ -146,67 +167,71 @@ bool CSVRender::TerrainShapeMode::primaryEditStartDrag (const QPoint& pos) { mEditingPos = hit.worldPos; mIsEditing = true; - if (mShapeEditTool == ShapeEditTool_Flatten) + if (mShapeEditTool == ShapeEditTool_Flatten || mShapeEditTool == ShapeEditTool_Equalize) setFlattenToolTargetHeight(hit); } return true; } -bool CSVRender::TerrainShapeMode::secondaryEditStartDrag (const QPoint& pos) +bool CSVRender::TerrainShapeMode::secondaryEditStartDrag(const QPoint& pos) { return false; } -bool CSVRender::TerrainShapeMode::primarySelectStartDrag (const QPoint& pos) +bool CSVRender::TerrainShapeMode::primarySelectStartDrag(const QPoint& pos) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_PrimarySelect; if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); - return false; + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); + return true; } -bool CSVRender::TerrainShapeMode::secondarySelectStartDrag (const QPoint& pos) +bool CSVRender::TerrainShapeMode::secondarySelectStartDrag(const QPoint& pos) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_SecondarySelect; if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); - return false; + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); + return true; } -void CSVRender::TerrainShapeMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) +void CSVRender::TerrainShapeMode::drag(const QPoint& pos, int diffX, int diffY, double speedFactor) { if (mDragMode == InteractionType_PrimaryEdit) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); mTotalDiffY += diffY; if (mIsEditing) { - if (mShapeEditTool == ShapeEditTool_Drag) editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(mEditingPos), true); - else editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); + if (mShapeEditTool == ShapeEditTool_Drag) + editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(mEditingPos), true); + else + editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); } } if (mDragMode == InteractionType_PrimarySelect) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == nullptr) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + if (hit.hit && hit.tag == nullptr) + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); } if (mDragMode == InteractionType_SecondarySelect) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == nullptr) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + if (hit.hit && hit.tag == nullptr) + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); } } @@ -217,17 +242,19 @@ void CSVRender::TerrainShapeMode::dragCompleted(const QPoint& pos) applyTerrainEditChanges(); clearTransientEdits(); } + if (mDragMode == InteractionType_PrimarySelect || mDragMode == InteractionType_SecondarySelect) + { + mTerrainShapeSelection->clearTemporarySelection(); + } } - void CSVRender::TerrainShapeMode::dragAborted() { - clearTransientEdits(); + clearTransientEdits(); + mDragMode = InteractionType_None; } -void CSVRender::TerrainShapeMode::dragWheel (int diff, double speedFactor) -{ -} +void CSVRender::TerrainShapeMode::dragWheel(int diff, double speedFactor) {} void CSVRender::TerrainShapeMode::sortAndLimitAlteredCells() { @@ -240,19 +267,23 @@ void CSVRender::TerrainShapeMode::sortAndLimitAlteredCells() while (!passing) // Multiple passes are needed when steepness problems arise for both x and y axis simultaneously { passing = true; - for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) + for (CSMWorld::CellCoordinates cellCoordinates : mAlteredCells) { limitAlteredHeights(cellCoordinates); } - std::reverse(mAlteredCells.begin(), mAlteredCells.end()); //Instead of alphabetical order, this should be fixed to sort cells by cell coordinates - for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) + std::reverse(mAlteredCells.begin(), + mAlteredCells + .end()); // Instead of alphabetical order, this should be fixed to sort cells by cell coordinates + for (CSMWorld::CellCoordinates cellCoordinates : mAlteredCells) { - if (!limitAlteredHeights(cellCoordinates, true)) passing = false; + if (!limitAlteredHeights(cellCoordinates, true)) + passing = false; } ++passes; if (passes > 2) { - Log(Debug::Warning) << "Warning: User edit exceeds accepted slope steepness. Automatic limiting has failed, edit has been discarded."; + Log(Debug::Warning) << "Warning: User edit exceeds accepted slope steepness. Automatic limiting has " + "failed, edit has been discarded."; clearTransientEdits(); return; } @@ -264,7 +295,8 @@ void CSVRender::TerrainShapeMode::clearTransientEdits() mTotalDiffY = 0; mIsEditing = false; mAlteredCells.clear(); - if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) paged->resetAllAlteredHeights(); mTerrainShapeSelection->update(); } @@ -272,10 +304,10 @@ void CSVRender::TerrainShapeMode::clearTransientEdits() void CSVRender::TerrainShapeMode::applyTerrainEditChanges() { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); - CSMWorld::IdTable& ltexTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& ltexTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); int landnormalsColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex); @@ -284,26 +316,30 @@ void CSVRender::TerrainShapeMode::applyTerrainEditChanges() sortAndLimitAlteredCells(); - undoStack.beginMacro ("Edit shape and normal records"); + undoStack.beginMacro("Edit shape and normal records"); // One command at the beginning of the macro for redrawing the terrain-selection grid when undoing the changes. - undoStack.push(new DrawTerrainSelectionCommand(*mTerrainShapeSelection)); + undoStack.push(new DrawTerrainSelectionCommand(&getWorldspaceWidget())); - for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) + for (CSMWorld::CellCoordinates cellCoordinates : mAlteredCells) { std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY()); - undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); - const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + undoStack.push(new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); - CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget()); + CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget()); // Generate land height record - for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) + for (int i = 0; i < ESM::Land::LAND_SIZE; ++i) { - for(int j = 0; j < ESM::Land::LAND_SIZE; ++j) + for (int j = 0; j < ESM::Land::LAND_SIZE; ++j) { if (paged && paged->getCellAlteredHeight(cellCoordinates, i, j)) - landShapeNew[j * ESM::Land::LAND_SIZE + i] = landShapePointer[j * ESM::Land::LAND_SIZE + i] + *paged->getCellAlteredHeight(cellCoordinates, i, j); + landShapeNew[j * ESM::Land::LAND_SIZE + i] = landShapePointer[j * ESM::Land::LAND_SIZE + i] + + *paged->getCellAlteredHeight(cellCoordinates, i, j); else landShapeNew[j * ESM::Land::LAND_SIZE + i] = 0; } @@ -312,37 +348,59 @@ void CSVRender::TerrainShapeMode::applyTerrainEditChanges() pushEditToCommand(landShapeNew, document, landTable, cellId); } - for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) + for (CSMWorld::CellCoordinates cellCoordinates : mAlteredCells) { std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY()); - const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); - const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = landTable.data(landTable.getModelIndex(CSMWorld::CellCoordinates::generateId(cellCoordinates.getX() + 1, cellCoordinates.getY()), landshapeColumn)).value(); - const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = landTable.data(landTable.getModelIndex(CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY() + 1), landshapeColumn)).value(); - const CSMWorld::LandNormalsColumn::DataType landNormalsPointer = landTable.data(landTable.getModelIndex(cellId, landnormalsColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer + = landTable + .data(landTable.getModelIndex( + CSMWorld::CellCoordinates::generateId(cellCoordinates.getX() + 1, cellCoordinates.getY()), + landshapeColumn)) + .value(); + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer + = landTable + .data(landTable.getModelIndex( + CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY() + 1), + landshapeColumn)) + .value(); + const CSMWorld::LandNormalsColumn::DataType landNormalsPointer + = landTable.data(landTable.getModelIndex(cellId, landnormalsColumn)) + .value(); CSMWorld::LandNormalsColumn::DataType landNormalsNew(landNormalsPointer); // Generate land normals record - for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) + for (int i = 0; i < ESM::Land::LAND_SIZE; ++i) { - for(int j = 0; j < ESM::Land::LAND_SIZE; ++j) + for (int j = 0; j < ESM::Land::LAND_SIZE; ++j) { osg::Vec3f v1(128, 0, 0); osg::Vec3f v2(0, 128, 0); - if (i < ESM::Land::LAND_SIZE - 1) v1.z() = landShapePointer[j * ESM::Land::LAND_SIZE + i + 1] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; + if (i < ESM::Land::LAND_SIZE - 1) + v1.z() = landShapePointer[j * ESM::Land::LAND_SIZE + i + 1] + - landShapePointer[j * ESM::Land::LAND_SIZE + i]; else { - std::string shiftedCellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX() + 1, cellCoordinates.getY()); + std::string shiftedCellId + = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX() + 1, cellCoordinates.getY()); if (isLandLoaded(shiftedCellId)) - v1.z() = landRightShapePointer[j * ESM::Land::LAND_SIZE + 1] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; + v1.z() = landRightShapePointer[j * ESM::Land::LAND_SIZE + 1] + - landShapePointer[j * ESM::Land::LAND_SIZE + i]; } - if (j < ESM::Land::LAND_SIZE - 1) v2.z() = landShapePointer[(j + 1) * ESM::Land::LAND_SIZE + i] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; + if (j < ESM::Land::LAND_SIZE - 1) + v2.z() = landShapePointer[(j + 1) * ESM::Land::LAND_SIZE + i] + - landShapePointer[j * ESM::Land::LAND_SIZE + i]; else { - std::string shiftedCellId = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY() + 1); + std::string shiftedCellId + = CSMWorld::CellCoordinates::generateId(cellCoordinates.getX(), cellCoordinates.getY() + 1); if (isLandLoaded(shiftedCellId)) - v2.z() = landDownShapePointer[ESM::Land::LAND_SIZE + i] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; + v2.z() = landDownShapePointer[ESM::Land::LAND_SIZE + i] + - landShapePointer[j * ESM::Land::LAND_SIZE + i]; } osg::Vec3f normal = v1 ^ v2; @@ -358,7 +416,7 @@ void CSVRender::TerrainShapeMode::applyTerrainEditChanges() pushNormalsEditToCommand(landNormalsNew, document, landTable, cellId); } // One command at the end of the macro for redrawing the terrain-selection grid when redoing the changes. - undoStack.push(new DrawTerrainSelectionCommand(*mTerrainShapeSelection)); + undoStack.push(new DrawTerrainSelectionCommand(&getWorldspaceWidget())); undoStack.endMacro(); clearTransientEdits(); @@ -367,18 +425,22 @@ void CSVRender::TerrainShapeMode::applyTerrainEditChanges() float CSVRender::TerrainShapeMode::calculateBumpShape(float distance, int radius, float height) { float distancePerRadius = distance / radius; - return height - height * (3 * distancePerRadius * distancePerRadius - 2 * distancePerRadius * distancePerRadius * distancePerRadius); + return height + - height + * (3 * distancePerRadius * distancePerRadius - 2 * distancePerRadius * distancePerRadius * distancePerRadius); } void CSVRender::TerrainShapeMode::editTerrainShapeGrid(const std::pair& vertexCoords, bool dragOperation) { int r = mBrushSize / 2; - if (r == 0) r = 1; // Prevent division by zero later, which might happen when mBrushSize == 1 + if (r == 0) + r = 1; // Prevent division by zero later, which might happen when mBrushSize == 1 - if (CSVRender::PagedWorldspaceWidget *paged = - dynamic_cast (&getWorldspaceWidget())) + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { - if (mShapeEditTool == ShapeEditTool_Drag) paged->resetAllAlteredHeights(); + if (mShapeEditTool == ShapeEditTool_Drag) + paged->resetAllAlteredHeights(); } if (mBrushShape == CSVWidget::BrushShape_Point) @@ -387,45 +449,59 @@ void CSVRender::TerrainShapeMode::editTerrainShapeGrid(const std::pair CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; int x = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.first); int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second); - if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, mTotalDiffY); + if (mShapeEditTool == ShapeEditTool_Drag) + alterHeight(cellCoords, x, y, mTotalDiffY); if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) { alterHeight(cellCoords, x, y, mShapeEditToolStrength); - float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); - if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); + float smoothMultiplier + = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); + if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) + smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); } - if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); - if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Smooth) + smoothHeight(cellCoords, x, y, mShapeEditToolStrength); + if (mShapeEditTool == ShapeEditTool_Flatten) + flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Equalize) + equalizeHeight(cellCoords, x, y, mTargetHeight); } if (mBrushShape == CSVWidget::BrushShape_Square) { - for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) + for (int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) { - for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) + for (int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) { std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(std::make_pair(i, j)); CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; int x = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(i); int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(j); - if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, mTotalDiffY); + if (mShapeEditTool == ShapeEditTool_Drag) + alterHeight(cellCoords, x, y, mTotalDiffY); if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) { alterHeight(cellCoords, x, y, mShapeEditToolStrength); - float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); - if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); + float smoothMultiplier = static_cast( + CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); + if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) + smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); } - if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); - if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Smooth) + smoothHeight(cellCoords, x, y, mShapeEditToolStrength); + if (mShapeEditTool == ShapeEditTool_Flatten) + flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Equalize) + equalizeHeight(cellCoords, x, y, mTargetHeight); } } } if (mBrushShape == CSVWidget::BrushShape_Circle) { - for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) + for (int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) { - for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) + for (int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) { std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(std::make_pair(i, j)); CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; @@ -433,46 +509,63 @@ void CSVRender::TerrainShapeMode::editTerrainShapeGrid(const std::pair int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(j); int distanceX = abs(i - vertexCoords.first); int distanceY = abs(j - vertexCoords.second); - float distance = sqrt(pow(distanceX, 2)+pow(distanceY, 2)); + float distance = sqrt(pow(distanceX, 2) + pow(distanceY, 2)); float smoothedByDistance = 0.0f; - if (mShapeEditTool == ShapeEditTool_Drag) smoothedByDistance = calculateBumpShape(distance, r, mTotalDiffY); - if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) smoothedByDistance = calculateBumpShape(distance, r, r + mShapeEditToolStrength); + if (mShapeEditTool == ShapeEditTool_Drag) + smoothedByDistance = calculateBumpShape(distance, r, mTotalDiffY); + if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) + smoothedByDistance = calculateBumpShape(distance, r, r + mShapeEditToolStrength); // Using floating-point radius here to prevent selecting too few vertices. if (distance <= mBrushSize / 2.0f) { - if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, smoothedByDistance); + if (mShapeEditTool == ShapeEditTool_Drag) + alterHeight(cellCoords, x, y, smoothedByDistance); if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) { alterHeight(cellCoords, x, y, smoothedByDistance); - float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); - if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); + float smoothMultiplier = static_cast( + CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); + if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) + smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); } - if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); - if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Smooth) + smoothHeight(cellCoords, x, y, mShapeEditToolStrength); + if (mShapeEditTool == ShapeEditTool_Flatten) + flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Equalize) + equalizeHeight(cellCoords, x, y, mTargetHeight); } } } } if (mBrushShape == CSVWidget::BrushShape_Custom) { - if(!mCustomBrushShape.empty()) + if (!mCustomBrushShape.empty()) { - for(auto const& value: mCustomBrushShape) + for (auto const& value : mCustomBrushShape) { - std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(std::make_pair(vertexCoords.first + value.first, vertexCoords.second + value.second)); + std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId( + std::make_pair(vertexCoords.first + value.first, vertexCoords.second + value.second)); CSMWorld::CellCoordinates cellCoords = CSMWorld::CellCoordinates::fromId(cellId).first; int x = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.first + value.first); int y = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second + value.second); - if (mShapeEditTool == ShapeEditTool_Drag) alterHeight(cellCoords, x, y, mTotalDiffY); + if (mShapeEditTool == ShapeEditTool_Drag) + alterHeight(cellCoords, x, y, mTotalDiffY); if (mShapeEditTool == ShapeEditTool_PaintToRaise || mShapeEditTool == ShapeEditTool_PaintToLower) { alterHeight(cellCoords, x, y, mShapeEditToolStrength); - float smoothMultiplier = static_cast(CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); - if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); + float smoothMultiplier = static_cast( + CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothstrength"].toDouble()); + if (CSMPrefs::get()["3D Scene Editing"]["landedit-post-smoothpainting"].isTrue()) + smoothHeight(cellCoords, x, y, mShapeEditToolStrength * smoothMultiplier); } - if (mShapeEditTool == ShapeEditTool_Smooth) smoothHeight(cellCoords, x, y, mShapeEditToolStrength); - if (mShapeEditTool == ShapeEditTool_Flatten) flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Smooth) + smoothHeight(cellCoords, x, y, mShapeEditToolStrength); + if (mShapeEditTool == ShapeEditTool_Flatten) + flattenHeight(cellCoords, x, y, mShapeEditToolStrength, mTargetHeight); + if (mShapeEditTool == ShapeEditTool_Equalize) + equalizeHeight(cellCoords, x, y, mTargetHeight); } } } @@ -487,23 +580,24 @@ void CSVRender::TerrainShapeMode::setFlattenToolTargetHeight(const WorldspaceHit int inCellY = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second); CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); - const CSMWorld::LandHeightsColumn::DataType landShapePointer = - landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); mTargetHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; } - -void CSVRender::TerrainShapeMode::alterHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float alteredHeight, bool useTool) +void CSVRender::TerrainShapeMode::alterHeight( + const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float alteredHeight, bool useTool) { std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); if (!(allowLandShapeEditing(cellId, useTool) && (useTool || (isLandLoaded(cellId))))) return; - CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget()); + CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget()); if (!paged) return; @@ -527,75 +621,96 @@ void CSVRender::TerrainShapeMode::alterHeight(const CSMWorld::CellCoordinates& c osg::Vec3d distance = eye - mEditingPos; alteredHeight = alteredHeight * (distance.length() / 500); } - if (mShapeEditTool == ShapeEditTool_PaintToRaise) alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) + alteredHeight; - if (mShapeEditTool == ShapeEditTool_PaintToLower) alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) - alteredHeight; - if (mShapeEditTool == ShapeEditTool_Smooth) alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) + alteredHeight; + if (mShapeEditTool == ShapeEditTool_PaintToRaise) + alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) + alteredHeight; + if (mShapeEditTool == ShapeEditTool_PaintToLower) + alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) - alteredHeight; + if (mShapeEditTool == ShapeEditTool_Smooth) + alteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY) + alteredHeight; } if (inCellX != 0 && inCellY != 0 && inCellX != ESM::Land::LAND_SIZE - 1 && inCellY != ESM::Land::LAND_SIZE - 1) - paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); + paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); // Change values of cornering cells if ((inCellX == 0 && inCellY == 0) && (useTool || isLandLoaded(cellUpLeftId))) { - if(allowLandShapeEditing(cellUpLeftId, useTool) && allowLandShapeEditing(cellLeftId, useTool) && allowLandShapeEditing(cellUpId, useTool)) + if (allowLandShapeEditing(cellUpLeftId, useTool) && allowLandShapeEditing(cellLeftId, useTool) + && allowLandShapeEditing(cellUpId, useTool)) { CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(-1, -1); - if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) - mAlteredCells.emplace_back(cornerCellCoords); - paged->setCellAlteredHeight(cornerCellCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE - 1, alteredHeight); - } else return; + if (useTool + && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(cornerCellCoords); + paged->setCellAlteredHeight( + cornerCellCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE - 1, alteredHeight); + } + else + return; } else if ((inCellX == 0 && inCellY == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellDownLeftId))) { - if (allowLandShapeEditing(cellDownLeftId, useTool) && allowLandShapeEditing(cellLeftId, useTool) && allowLandShapeEditing(cellDownId, useTool)) + if (allowLandShapeEditing(cellDownLeftId, useTool) && allowLandShapeEditing(cellLeftId, useTool) + && allowLandShapeEditing(cellDownId, useTool)) { CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(-1, 1); - if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) - mAlteredCells.emplace_back(cornerCellCoords); + if (useTool + && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(cornerCellCoords); paged->setCellAlteredHeight(cornerCellCoords, ESM::Land::LAND_SIZE - 1, 0, alteredHeight); - } else return; + } + else + return; } else if ((inCellX == ESM::Land::LAND_SIZE - 1 && inCellY == 0) && (useTool || isLandLoaded(cellUpRightId))) { - if (allowLandShapeEditing(cellUpRightId, useTool) && allowLandShapeEditing(cellRightId, useTool) && allowLandShapeEditing(cellUpId, useTool)) + if (allowLandShapeEditing(cellUpRightId, useTool) && allowLandShapeEditing(cellRightId, useTool) + && allowLandShapeEditing(cellUpId, useTool)) { CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(1, -1); - if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) - mAlteredCells.emplace_back(cornerCellCoords); + if (useTool + && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(cornerCellCoords); paged->setCellAlteredHeight(cornerCellCoords, 0, ESM::Land::LAND_SIZE - 1, alteredHeight); - } else return; + } + else + return; } - else if ((inCellX == ESM::Land::LAND_SIZE - 1 && inCellY == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellDownRightId))) + else if ((inCellX == ESM::Land::LAND_SIZE - 1 && inCellY == ESM::Land::LAND_SIZE - 1) + && (useTool || isLandLoaded(cellDownRightId))) { - if(allowLandShapeEditing(cellDownRightId, useTool) && allowLandShapeEditing(cellRightId, useTool) && allowLandShapeEditing(cellDownId, useTool)) + if (allowLandShapeEditing(cellDownRightId, useTool) && allowLandShapeEditing(cellRightId, useTool) + && allowLandShapeEditing(cellDownId, useTool)) { CSMWorld::CellCoordinates cornerCellCoords = cellCoords.move(1, 1); - if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) - mAlteredCells.emplace_back(cornerCellCoords); + if (useTool + && std::find(mAlteredCells.begin(), mAlteredCells.end(), cornerCellCoords) == mAlteredCells.end()) + mAlteredCells.emplace_back(cornerCellCoords); paged->setCellAlteredHeight(cornerCellCoords, 0, 0, alteredHeight); - } else return; + } + else + return; } // Change values of edging cells if ((inCellX == 0) && (useTool || isLandLoaded(cellLeftId))) { - if(allowLandShapeEditing(cellLeftId, useTool)) + if (allowLandShapeEditing(cellLeftId, useTool)) { CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(-1, 0); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) - mAlteredCells.emplace_back(edgeCellCoords); + mAlteredCells.emplace_back(edgeCellCoords); paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); paged->setCellAlteredHeight(edgeCellCoords, ESM::Land::LAND_SIZE - 1, inCellY, alteredHeight); } } if ((inCellY == 0) && (useTool || isLandLoaded(cellUpId))) { - if(allowLandShapeEditing(cellUpId, useTool)) + if (allowLandShapeEditing(cellUpId, useTool)) { CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(0, -1); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) - mAlteredCells.emplace_back(edgeCellCoords); + mAlteredCells.emplace_back(edgeCellCoords); paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); paged->setCellAlteredHeight(edgeCellCoords, inCellX, ESM::Land::LAND_SIZE - 1, alteredHeight); } @@ -603,41 +718,43 @@ void CSVRender::TerrainShapeMode::alterHeight(const CSMWorld::CellCoordinates& c if ((inCellX == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellRightId))) { - if(allowLandShapeEditing(cellRightId, useTool)) + if (allowLandShapeEditing(cellRightId, useTool)) { CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(1, 0); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) - mAlteredCells.emplace_back(edgeCellCoords); + mAlteredCells.emplace_back(edgeCellCoords); paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); paged->setCellAlteredHeight(edgeCellCoords, 0, inCellY, alteredHeight); } } if ((inCellY == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellDownId))) { - if(allowLandShapeEditing(cellDownId, useTool)) + if (allowLandShapeEditing(cellDownId, useTool)) { CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(0, 1); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) - mAlteredCells.emplace_back(edgeCellCoords); + mAlteredCells.emplace_back(edgeCellCoords); paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); paged->setCellAlteredHeight(edgeCellCoords, inCellX, 0, alteredHeight); } } - } -void CSVRender::TerrainShapeMode::smoothHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength) +void CSVRender::TerrainShapeMode::smoothHeight( + const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength) { - if (CSVRender::PagedWorldspaceWidget *paged = - dynamic_cast (&getWorldspaceWidget())) + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); - const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); // ### Variable naming key ### // Variables here hold either the real value, or the altered value of current edit. @@ -657,24 +774,30 @@ void CSVRender::TerrainShapeMode::smoothHeight(const CSMWorld::CellCoordinates& float downAlteredHeight = 0.0f; float upHeight = 0.0f; - if(allowLandShapeEditing(cellId)) + if (allowLandShapeEditing(cellId)) { - //Get key values for calculating average, handle cell edges, check for null pointers + // Get key values for calculating average, handle cell edges, check for null pointers if (inCellX == 0) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); - const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); leftHeight = landLeftShapePointer[inCellY * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE - 2)]; if (paged->getCellAlteredHeight(cellCoords.move(-1, 0), inCellX, ESM::Land::LAND_SIZE - 2)) - leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY); + leftAlteredHeight + = *paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY); } if (inCellY == 0) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); - const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landUpShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); upHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 2) * ESM::Land::LAND_SIZE + inCellX]; if (paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2)) - upAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2); + upAlteredHeight + = *paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2); } if (inCellX > 0) { @@ -689,8 +812,9 @@ void CSVRender::TerrainShapeMode::smoothHeight(const CSMWorld::CellCoordinates& if (inCellX == ESM::Land::LAND_SIZE - 1) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); - const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = - landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); rightHeight = landRightShapePointer[inCellY * ESM::Land::LAND_SIZE + 1]; if (paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY)) { @@ -700,8 +824,9 @@ void CSVRender::TerrainShapeMode::smoothHeight(const CSMWorld::CellCoordinates& if (inCellY == ESM::Land::LAND_SIZE - 1) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); - const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = - landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); downHeight = landDownShapePointer[1 * ESM::Land::LAND_SIZE + inCellX]; if (paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1)) { @@ -711,31 +836,37 @@ void CSVRender::TerrainShapeMode::smoothHeight(const CSMWorld::CellCoordinates& if (inCellX < ESM::Land::LAND_SIZE - 1) { rightHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX + 1]; - if(paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY)) + if (paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY)) rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY); } if (inCellY < ESM::Land::LAND_SIZE - 1) { downHeight = landShapePointer[(inCellY + 1) * ESM::Land::LAND_SIZE + inCellX]; - if(paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1)) + if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1)) downAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1); } - float averageHeight = (upHeight + downHeight + rightHeight + leftHeight + - upAlteredHeight + downAlteredHeight + rightAlteredHeight + leftAlteredHeight) / 4; - if ((thisHeight + thisAlteredHeight) != averageHeight) mAlteredCells.emplace_back(cellCoords); - if (toolStrength > abs(thisHeight + thisAlteredHeight - averageHeight)) toolStrength = abs(thisHeight + thisAlteredHeight - averageHeight); - if (thisHeight + thisAlteredHeight > averageHeight) alterHeight(cellCoords, inCellX, inCellY, - toolStrength); - if (thisHeight + thisAlteredHeight < averageHeight) alterHeight(cellCoords, inCellX, inCellY, + toolStrength); + float averageHeight = (upHeight + downHeight + rightHeight + leftHeight + upAlteredHeight + + downAlteredHeight + rightAlteredHeight + leftAlteredHeight) + / 4; + if ((thisHeight + thisAlteredHeight) != averageHeight) + mAlteredCells.emplace_back(cellCoords); + if (toolStrength > abs(thisHeight + thisAlteredHeight - averageHeight)) + toolStrength = abs(thisHeight + thisAlteredHeight - averageHeight); + if (thisHeight + thisAlteredHeight > averageHeight) + alterHeight(cellCoords, inCellX, inCellY, -toolStrength); + if (thisHeight + thisAlteredHeight < averageHeight) + alterHeight(cellCoords, inCellX, inCellY, +toolStrength); } } } -void CSVRender::TerrainShapeMode::flattenHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength, int targetHeight) +void CSVRender::TerrainShapeMode::flattenHeight( + const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength, int targetHeight) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); float thisHeight = 0.0f; @@ -743,33 +874,61 @@ void CSVRender::TerrainShapeMode::flattenHeight(const CSMWorld::CellCoordinates& std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); - if (CSVRender::PagedWorldspaceWidget *paged = - dynamic_cast (&getWorldspaceWidget())) + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { if (!noCell(cellId) && !noLand(cellId)) { - const CSMWorld::LandHeightsColumn::DataType landShapePointer = - landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); - if(paged->getCellAlteredHeight(cellCoords, inCellX, inCellY)) + if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY)) thisAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY); thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; } } - if (toolStrength > abs(thisHeight - targetHeight) && toolStrength > 8.0f) toolStrength = - abs(thisHeight - targetHeight); //Cut down excessive changes - if (thisHeight + thisAlteredHeight > targetHeight) alterHeight(cellCoords, inCellX, inCellY, thisAlteredHeight - toolStrength); - if (thisHeight + thisAlteredHeight < targetHeight) alterHeight(cellCoords, inCellX, inCellY, thisAlteredHeight + toolStrength); + if (toolStrength > abs(thisHeight - targetHeight) && toolStrength > 8.0f) + toolStrength = abs(thisHeight - targetHeight); // Cut down excessive changes + if (thisHeight + thisAlteredHeight > targetHeight) + alterHeight(cellCoords, inCellX, inCellY, thisAlteredHeight - toolStrength); + if (thisHeight + thisAlteredHeight < targetHeight) + alterHeight(cellCoords, inCellX, inCellY, thisAlteredHeight + toolStrength); } -void CSVRender::TerrainShapeMode::updateKeyHeightValues(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* thisHeight, - float* thisAlteredHeight, float* leftHeight, float* leftAlteredHeight, float* upHeight, float* upAlteredHeight, float* rightHeight, - float* rightAlteredHeight, float* downHeight, float* downAlteredHeight) +void CSVRender::TerrainShapeMode::equalizeHeight( + const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int targetHeight) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); + + float thisHeight = 0.0f; + + const std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); + + if (!noCell(cellId) && !noLand(cellId)) + { + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); + + thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; + } + + alterHeight(cellCoords, inCellX, inCellY, targetHeight - thisHeight); +} + +void CSVRender::TerrainShapeMode::updateKeyHeightValues(const CSMWorld::CellCoordinates& cellCoords, int inCellX, + int inCellY, float* thisHeight, float* thisAlteredHeight, float* leftHeight, float* leftAlteredHeight, + float* upHeight, float* upAlteredHeight, float* rightHeight, float* rightAlteredHeight, float* downHeight, + float* downAlteredHeight) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); @@ -779,7 +938,7 @@ void CSVRender::TerrainShapeMode::updateKeyHeightValues(const CSMWorld::CellCoor std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); *thisHeight = 0.0f; // real + altered height - *thisAlteredHeight = 0.0f; // only altered height + *thisAlteredHeight = 0.0f; // only altered height *leftHeight = 0.0f; *leftAlteredHeight = 0.0f; *upHeight = 0.0f; @@ -789,60 +948,66 @@ void CSVRender::TerrainShapeMode::updateKeyHeightValues(const CSMWorld::CellCoor *downHeight = 0.0f; *downAlteredHeight = 0.0f; - if (CSVRender::PagedWorldspaceWidget *paged = - dynamic_cast (&getWorldspaceWidget())) + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { if (!noCell(cellId) && !noLand(cellId)) { - const CSMWorld::LandHeightsColumn::DataType landShapePointer = - landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); - if(paged->getCellAlteredHeight(cellCoords, inCellX, inCellY)) + if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY)) *thisAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY); *thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX] + *thisAlteredHeight; - // Default to the same value as thisHeight, which happens in the case of cell edge where next cell/land is not found, - // which is to prevent unnecessary action at limitHeightChange(). + // Default to the same value as thisHeight, which happens in the case of cell edge where next cell/land is + // not found, which is to prevent unnecessary action at limitHeightChange(). *leftHeight = *thisHeight; *upHeight = *thisHeight; *rightHeight = *thisHeight; *downHeight = *thisHeight; - //If at edge, get values from neighboring cell + // If at edge, get values from neighboring cell if (inCellX == 0) { - if(isLandLoaded(cellLeftId)) + if (isLandLoaded(cellLeftId)) { - const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = - landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer + = landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)) + .value(); *leftHeight = landLeftShapePointer[inCellY * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE - 2)]; if (paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY)) { - *leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY); + *leftAlteredHeight + = *paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY); *leftHeight += *leftAlteredHeight; } } } if (inCellY == 0) { - if(isLandLoaded(cellUpId)) + if (isLandLoaded(cellUpId)) { - const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = - landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landUpShapePointer + = landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)) + .value(); *upHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 2) * ESM::Land::LAND_SIZE + inCellX]; - if (paged->getCellAlteredHeight(cellCoords.move(0,-1), inCellX, ESM::Land::LAND_SIZE - 2)) + if (paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2)) { - *upAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2); + *upAlteredHeight + = *paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2); *upHeight += *upAlteredHeight; } } } if (inCellX == ESM::Land::LAND_SIZE - 1) { - if(isLandLoaded(cellRightId)) + if (isLandLoaded(cellRightId)) { - const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = - landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer + = landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)) + .value(); *rightHeight = landRightShapePointer[inCellY * ESM::Land::LAND_SIZE + 1]; if (paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY)) { @@ -853,10 +1018,11 @@ void CSVRender::TerrainShapeMode::updateKeyHeightValues(const CSMWorld::CellCoor } if (inCellY == ESM::Land::LAND_SIZE - 1) { - if(isLandLoaded(cellDownId)) + if (isLandLoaded(cellDownId)) { - const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = - landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer + = landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)) + .value(); *downHeight = landDownShapePointer[ESM::Land::LAND_SIZE + inCellX]; if (paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1)) { @@ -866,7 +1032,7 @@ void CSVRender::TerrainShapeMode::updateKeyHeightValues(const CSMWorld::CellCoor } } - //If not at edge, get values from the same cell + // If not at edge, get values from the same cell if (inCellX != 0) { *leftHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX - 1]; @@ -895,18 +1061,18 @@ void CSVRender::TerrainShapeMode::updateKeyHeightValues(const CSMWorld::CellCoor *downAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1); *downHeight += *downAlteredHeight; } - } } } -void CSVRender::TerrainShapeMode::compareAndLimit(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* limitedAlteredHeightXAxis, float* limitedAlteredHeightYAxis, bool* steepnessIsWithinLimits) +void CSVRender::TerrainShapeMode::compareAndLimit(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, + float* limitedAlteredHeightXAxis, float* limitedAlteredHeightYAxis, bool* steepnessIsWithinLimits) { if (limitedAlteredHeightXAxis) { if (limitedAlteredHeightYAxis) { - if(std::abs(*limitedAlteredHeightXAxis) >= std::abs(*limitedAlteredHeightYAxis)) + if (std::abs(*limitedAlteredHeightXAxis) >= std::abs(*limitedAlteredHeightYAxis)) { alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightXAxis, false); *steepnessIsWithinLimits = false; @@ -933,7 +1099,8 @@ void CSVRender::TerrainShapeMode::compareAndLimit(const CSMWorld::CellCoordinate bool CSVRender::TerrainShapeMode::limitAlteredHeights(const CSMWorld::CellCoordinates& cellCoords, bool reverseMode) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast (*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); @@ -943,8 +1110,9 @@ bool CSVRender::TerrainShapeMode::limitAlteredHeights(const CSMWorld::CellCoordi if (isLandLoaded(cellId)) { - const CSMWorld::LandHeightsColumn::DataType landShapePointer = - landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); float thisHeight = 0.0f; float thisAlteredHeight = 0.0f; @@ -959,58 +1127,70 @@ bool CSVRender::TerrainShapeMode::limitAlteredHeights(const CSMWorld::CellCoordi if (!reverseMode) { - for(int inCellY = 0; inCellY < ESM::Land::LAND_SIZE; ++inCellY) + for (int inCellY = 0; inCellY < ESM::Land::LAND_SIZE; ++inCellY) { - for(int inCellX = 0; inCellX < ESM::Land::LAND_SIZE; ++inCellX) + for (int inCellX = 0; inCellX < ESM::Land::LAND_SIZE; ++inCellX) { std::unique_ptr limitedAlteredHeightXAxis(nullptr); std::unique_ptr limitedAlteredHeightYAxis(nullptr); - updateKeyHeightValues(cellCoords, inCellX, inCellY, &thisHeight, &thisAlteredHeight, &leftHeight, &leftAlteredHeight, - &upHeight, &upAlteredHeight, &rightHeight, &rightAlteredHeight, &downHeight, &downAlteredHeight); + updateKeyHeightValues(cellCoords, inCellX, inCellY, &thisHeight, &thisAlteredHeight, &leftHeight, + &leftAlteredHeight, &upHeight, &upAlteredHeight, &rightHeight, &rightAlteredHeight, &downHeight, + &downAlteredHeight); // Check for height limits on x-axis if (leftHeight - thisHeight > limitHeightChange) - limitedAlteredHeightXAxis.reset(new float(leftHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightXAxis = std::make_unique( + leftHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (leftHeight - thisHeight < -limitHeightChange) - limitedAlteredHeightXAxis.reset(new float(leftHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightXAxis = std::make_unique( + leftHeight + limitHeightChange - (thisHeight - thisAlteredHeight)); // Check for height limits on y-axis if (upHeight - thisHeight > limitHeightChange) - limitedAlteredHeightYAxis.reset(new float(upHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightYAxis + = std::make_unique(upHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (upHeight - thisHeight < -limitHeightChange) - limitedAlteredHeightYAxis.reset(new float(upHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightYAxis + = std::make_unique(upHeight + limitHeightChange - (thisHeight - thisAlteredHeight)); // Limit altered height value based on x or y, whichever is the smallest - compareAndLimit(cellCoords, inCellX, inCellY, limitedAlteredHeightXAxis.get(), limitedAlteredHeightYAxis.get(), &steepnessIsWithinLimits); + compareAndLimit(cellCoords, inCellX, inCellY, limitedAlteredHeightXAxis.get(), + limitedAlteredHeightYAxis.get(), &steepnessIsWithinLimits); } } } if (reverseMode) { - for(int inCellY = ESM::Land::LAND_SIZE - 1; inCellY >= 0; --inCellY) + for (int inCellY = ESM::Land::LAND_SIZE - 1; inCellY >= 0; --inCellY) { - for(int inCellX = ESM::Land::LAND_SIZE - 1; inCellX >= 0; --inCellX) + for (int inCellX = ESM::Land::LAND_SIZE - 1; inCellX >= 0; --inCellX) { std::unique_ptr limitedAlteredHeightXAxis(nullptr); std::unique_ptr limitedAlteredHeightYAxis(nullptr); - updateKeyHeightValues(cellCoords, inCellX, inCellY, &thisHeight, &thisAlteredHeight, &leftHeight, &leftAlteredHeight, - &upHeight, &upAlteredHeight, &rightHeight, &rightAlteredHeight, &downHeight, &downAlteredHeight); + updateKeyHeightValues(cellCoords, inCellX, inCellY, &thisHeight, &thisAlteredHeight, &leftHeight, + &leftAlteredHeight, &upHeight, &upAlteredHeight, &rightHeight, &rightAlteredHeight, &downHeight, + &downAlteredHeight); // Check for height limits on x-axis if (rightHeight - thisHeight > limitHeightChange) - limitedAlteredHeightXAxis.reset(new float(rightHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightXAxis = std::make_unique( + rightHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (rightHeight - thisHeight < -limitHeightChange) - limitedAlteredHeightXAxis.reset(new float(rightHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightXAxis = std::make_unique( + rightHeight + limitHeightChange - (thisHeight - thisAlteredHeight)); // Check for height limits on y-axis if (downHeight - thisHeight > limitHeightChange) - limitedAlteredHeightYAxis.reset(new float(downHeight - limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightYAxis = std::make_unique( + downHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (downHeight - thisHeight < -limitHeightChange) - limitedAlteredHeightYAxis.reset(new float(downHeight + limitHeightChange - (thisHeight - thisAlteredHeight))); + limitedAlteredHeightYAxis = std::make_unique( + downHeight + limitHeightChange - (thisHeight - thisAlteredHeight)); // Limit altered height value based on x or y, whichever is the smallest - compareAndLimit(cellCoords, inCellX, inCellY, limitedAlteredHeightXAxis.get(), limitedAlteredHeightYAxis.get(), &steepnessIsWithinLimits); + compareAndLimit(cellCoords, inCellX, inCellY, limitedAlteredHeightXAxis.get(), + limitedAlteredHeightYAxis.get(), &steepnessIsWithinLimits); } } } @@ -1020,7 +1200,8 @@ bool CSVRender::TerrainShapeMode::limitAlteredHeights(const CSMWorld::CellCoordi bool CSVRender::TerrainShapeMode::isInCellSelection(int globalSelectionX, int globalSelectionY) { - if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { std::pair vertexCoords = std::make_pair(globalSelectionX, globalSelectionY); std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords); @@ -1029,9 +1210,11 @@ bool CSVRender::TerrainShapeMode::isInCellSelection(int globalSelectionX, int gl return false; } -void CSVRender::TerrainShapeMode::handleSelection(int globalSelectionX, int globalSelectionY, std::vector>* selections) +void CSVRender::TerrainShapeMode::handleSelection( + int globalSelectionX, int globalSelectionY, std::vector>* selections) { - if (isInCellSelection(globalSelectionX, globalSelectionY)) selections->emplace_back(globalSelectionX, globalSelectionY); + if (isInCellSelection(globalSelectionX, globalSelectionY)) + selections->emplace_back(globalSelectionX, globalSelectionY); else { int moduloX = globalSelectionX % (ESM::Land::LAND_SIZE - 1); @@ -1045,11 +1228,12 @@ void CSVRender::TerrainShapeMode::handleSelection(int globalSelectionX, int glob /* The northern and eastern edges don't belong to the current cell. - If the corresponding adjacent cell is not loaded, some special handling is necessary to select border vertices. + If the corresponding adjacent cell is not loaded, some special handling is necessary to select border + vertices. */ if (xIsAtCellBorder && yIsAtCellBorder) { - /* + /* Handle the NW, NE, and SE corner vertices. NW corner: (+1, -1) offset to reach current cell. NE corner: (-1, -1) offset to reach current cell. @@ -1076,7 +1260,7 @@ void CSVRender::TerrainShapeMode::handleSelection(int globalSelectionX, int glob } } -void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& vertexCoords, unsigned char selectMode, bool dragOperation) +void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& vertexCoords, unsigned char selectMode) { int r = mBrushSize / 2; std::vector> selections; @@ -1088,9 +1272,9 @@ void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& if (mBrushShape == CSVWidget::BrushShape_Square) { - for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) + for (int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) { - for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) + for (int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) { handleSelection(i, j, &selections); } @@ -1099,13 +1283,13 @@ void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& if (mBrushShape == CSVWidget::BrushShape_Circle) { - for(int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) + for (int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) { - for(int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) + for (int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) { int distanceX = abs(i - vertexCoords.first); int distanceY = abs(j - vertexCoords.second); - float distance = sqrt(pow(distanceX, 2)+pow(distanceY, 2)); + float distance = sqrt(pow(distanceX, 2) + pow(distanceY, 2)); // Using floating-point radius here to prevent selecting too few vertices. if (distance <= mBrushSize / 2.0f) @@ -1116,11 +1300,12 @@ void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& if (mBrushShape == CSVWidget::BrushShape_Custom) { - if(!mCustomBrushShape.empty()) + if (!mCustomBrushShape.empty()) { - for(auto const& value: mCustomBrushShape) + for (auto const& value : mCustomBrushShape) { - std::pair localVertexCoords (vertexCoords.first + value.first, vertexCoords.second + value.second); + std::pair localVertexCoords( + vertexCoords.first + value.first, vertexCoords.second + value.second); handleSelection(localVertexCoords.first, localVertexCoords.second, &selections); } } @@ -1132,75 +1317,78 @@ void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& selectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString(); else selectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString(); - + if (selectAction == "Select only") mTerrainShapeSelection->onlySelect(selections); else if (selectAction == "Add to selection") - mTerrainShapeSelection->addSelect(selections, dragOperation); + mTerrainShapeSelection->addSelect(selections); else if (selectAction == "Remove from selection") - mTerrainShapeSelection->removeSelect(selections, dragOperation); + mTerrainShapeSelection->removeSelect(selections); else if (selectAction == "Invert selection") - mTerrainShapeSelection->toggleSelect(selections, dragOperation); + mTerrainShapeSelection->toggleSelect(selections); } -void CSVRender::TerrainShapeMode::pushEditToCommand(const CSMWorld::LandHeightsColumn::DataType& newLandGrid, CSMDoc::Document& document, - CSMWorld::IdTable& landTable, const std::string& cellId) +void CSVRender::TerrainShapeMode::pushEditToCommand(const CSMWorld::LandHeightsColumn::DataType& newLandGrid, + CSMDoc::Document& document, CSMWorld::IdTable& landTable, const std::string& cellId) { QVariant changedLand; changedLand.setValue(newLandGrid); - QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandHeightsIndex))); + QModelIndex index( + landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex))); QUndoStack& undoStack = document.getUndoStack(); - undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); + undoStack.push(new CSMWorld::ModifyCommand(landTable, index, changedLand)); } -void CSVRender::TerrainShapeMode::pushNormalsEditToCommand(const CSMWorld::LandNormalsColumn::DataType& newLandGrid, CSMDoc::Document& document, - CSMWorld::IdTable& landTable, const std::string& cellId) +void CSVRender::TerrainShapeMode::pushNormalsEditToCommand(const CSMWorld::LandNormalsColumn::DataType& newLandGrid, + CSMDoc::Document& document, CSMWorld::IdTable& landTable, const std::string& cellId) { QVariant changedLand; changedLand.setValue(newLandGrid); - QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandNormalsIndex))); + QModelIndex index( + landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex))); QUndoStack& undoStack = document.getUndoStack(); - undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); + undoStack.push(new CSMWorld::ModifyCommand(landTable, index, changedLand)); } bool CSVRender::TerrainShapeMode::noCell(const std::string& cellId) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); const CSMWorld::IdCollection& cellCollection = document.getData().getCells(); - return cellCollection.searchId (cellId) == -1; + return cellCollection.searchId(ESM::RefId::stringRefId(cellId)) == -1; } bool CSVRender::TerrainShapeMode::noLand(const std::string& cellId) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); const CSMWorld::IdCollection& landCollection = document.getData().getLand(); - return landCollection.searchId (cellId) == -1; + return landCollection.searchId(ESM::RefId::stringRefId(cellId)) == -1; } bool CSVRender::TerrainShapeMode::noLandLoaded(const std::string& cellId) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); const CSMWorld::IdCollection& landCollection = document.getData().getLand(); - return !landCollection.getRecord(cellId).get().isDataLoaded(ESM::Land::DATA_VNML); + return !landCollection.getRecord(ESM::RefId::stringRefId(cellId)).get().isDataLoaded(ESM::Land::DATA_VNML); } bool CSVRender::TerrainShapeMode::isLandLoaded(const std::string& cellId) { - if (!noCell(cellId) && !noLand(cellId) && !noLandLoaded(cellId)) return true; + if (!noCell(cellId) && !noLand(cellId) && !noLandLoaded(cellId)) + return true; return false; } void CSVRender::TerrainShapeMode::createNewLandData(const CSMWorld::CellCoordinates& cellCoords) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); - CSMWorld::IdTable& ltexTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& ltexTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); int landnormalsColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex); @@ -1222,60 +1410,74 @@ void CSVRender::TerrainShapeMode::createNewLandData(const CSMWorld::CellCoordina float upCellSampleHeight = 0.0f; float downCellSampleHeight = 0.0f; - const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); - const CSMWorld::LandNormalsColumn::DataType landNormalsPointer = landTable.data(landTable.getModelIndex(cellId, landnormalsColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); + const CSMWorld::LandNormalsColumn::DataType landNormalsPointer + = landTable.data(landTable.getModelIndex(cellId, landnormalsColumn)) + .value(); CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); CSMWorld::LandNormalsColumn::DataType landNormalsNew(landNormalsPointer); - if (CSVRender::PagedWorldspaceWidget *paged = - dynamic_cast (&getWorldspaceWidget())) + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { if (isLandLoaded(cellLeftId)) { - const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = - landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer + = landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)) + .value(); ++averageDivider; - leftCellSampleHeight = landLeftShapePointer[(ESM::Land::LAND_SIZE / 2) * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]; - if(paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2)) - leftCellSampleHeight += *paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2); + leftCellSampleHeight + = landLeftShapePointer[(ESM::Land::LAND_SIZE / 2) * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]; + if (paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2)) + leftCellSampleHeight + += *paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2); } if (isLandLoaded(cellRightId)) { - const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = - landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer + = landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)) + .value(); ++averageDivider; rightCellSampleHeight = landRightShapePointer[(ESM::Land::LAND_SIZE / 2) * ESM::Land::LAND_SIZE]; - if(paged->getCellAlteredHeight(cellRightCoords, 0, ESM::Land::LAND_SIZE / 2)) + if (paged->getCellAlteredHeight(cellRightCoords, 0, ESM::Land::LAND_SIZE / 2)) rightCellSampleHeight += *paged->getCellAlteredHeight(cellRightCoords, 0, ESM::Land::LAND_SIZE / 2); } if (isLandLoaded(cellUpId)) { - const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = - landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landUpShapePointer + = landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)) + .value(); ++averageDivider; - upCellSampleHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE / 2)]; - if(paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1)) - upCellSampleHeight += *paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1); + upCellSampleHeight + = landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE / 2)]; + if (paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1)) + upCellSampleHeight + += *paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1); } if (isLandLoaded(cellDownId)) { - const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = - landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer + = landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)) + .value(); ++averageDivider; downCellSampleHeight = landDownShapePointer[ESM::Land::LAND_SIZE / 2]; - if(paged->getCellAlteredHeight(cellDownCoords, ESM::Land::LAND_SIZE / 2, 0)) + if (paged->getCellAlteredHeight(cellDownCoords, ESM::Land::LAND_SIZE / 2, 0)) downCellSampleHeight += *paged->getCellAlteredHeight(cellDownCoords, ESM::Land::LAND_SIZE / 2, 0); } } - if (averageDivider > 0) defaultHeight = (leftCellSampleHeight + rightCellSampleHeight + upCellSampleHeight + downCellSampleHeight) / averageDivider; + if (averageDivider > 0) + defaultHeight = (leftCellSampleHeight + rightCellSampleHeight + upCellSampleHeight + downCellSampleHeight) + / averageDivider; - for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) + for (int i = 0; i < ESM::Land::LAND_SIZE; ++i) { - for(int j = 0; j < ESM::Land::LAND_SIZE; ++j) + for (int j = 0; j < ESM::Land::LAND_SIZE; ++j) { landShapeNew[j * ESM::Land::LAND_SIZE + i] = defaultHeight; landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 0] = 0; @@ -1287,64 +1489,64 @@ void CSVRender::TerrainShapeMode::createNewLandData(const CSMWorld::CellCoordina changedShape.setValue(landShapeNew); QVariant changedNormals; changedNormals.setValue(landNormalsNew); - QModelIndex indexShape(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandHeightsIndex))); - QModelIndex indexNormal(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandNormalsIndex))); - document.getUndoStack().push (new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); - document.getUndoStack().push (new CSMWorld::ModifyCommand(landTable, indexShape, changedShape)); - document.getUndoStack().push (new CSMWorld::ModifyCommand(landTable, indexNormal, changedNormals)); + QModelIndex indexShape( + landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex))); + QModelIndex indexNormal( + landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex))); + document.getUndoStack().push(new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); + document.getUndoStack().push(new CSMWorld::ModifyCommand(landTable, indexShape, changedShape)); + document.getUndoStack().push(new CSMWorld::ModifyCommand(landTable, indexNormal, changedNormals)); } bool CSVRender::TerrainShapeMode::allowLandShapeEditing(const std::string& cellId, bool useTool) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); - CSMWorld::IdTree& cellTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTree& cellTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Cells)); if (noCell(cellId)) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); // target cell does not exist - if (mode=="Discard") + if (mode == "Discard") return false; - if (mode=="Create cell and land, then edit" && useTool) + if (mode == "Create cell and land, then edit" && useTool) { - std::unique_ptr createCommand ( - new CSMWorld::CreateCommand (cellTable, cellId)); - int parentIndex = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); - int index = cellTable.findNestedColumnIndex (parentIndex, CSMWorld::Columns::ColumnId_Interior); - createCommand->addNestedValue (parentIndex, index, false); - document.getUndoStack().push (createCommand.release()); - - if (CSVRender::PagedWorldspaceWidget *paged = - dynamic_cast (&getWorldspaceWidget())) + auto createCommand = std::make_unique(cellTable, cellId); + int parentIndex = cellTable.findColumnIndex(CSMWorld::Columns::ColumnId_Cell); + int index = cellTable.findNestedColumnIndex(parentIndex, CSMWorld::Columns::ColumnId_Interior); + createCommand->addNestedValue(parentIndex, index, false); + document.getUndoStack().push(createCommand.release()); + + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); - selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); - paged->setCellSelection (selection); + selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); + paged->setCellSelection(selection); } } } - else if (CSVRender::PagedWorldspaceWidget *paged = - dynamic_cast (&getWorldspaceWidget())) + else if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); - if (!selection.has (CSMWorld::CellCoordinates::fromId (cellId).first)) + if (!selection.has(CSMWorld::CellCoordinates::fromId(cellId).first)) { // target cell exists, but is not shown - std::string mode = - CSMPrefs::get()["3D Scene Editing"]["outside-visible-landedit"].toString(); + std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-visible-landedit"].toString(); - if (mode=="Discard") + if (mode == "Discard") return false; - if (mode=="Show cell and edit" && useTool) + if (mode == "Show cell and edit" && useTool) { - selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); - paged->setCellSelection (selection); + selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); + paged->setCellSelection(selection); } } } @@ -1354,12 +1556,12 @@ bool CSVRender::TerrainShapeMode::allowLandShapeEditing(const std::string& cellI std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); // target cell does not exist - if (mode=="Discard") + if (mode == "Discard") return false; - if (mode=="Create cell and land, then edit" && useTool) + if (mode == "Create cell and land, then edit" && useTool) { - document.getUndoStack().push (new CSMWorld::CreateCommand (landTable, cellId)); + document.getUndoStack().push(new CSMWorld::CreateCommand(landTable, cellId)); createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first); fixEdges(CSMWorld::CellCoordinates::fromId(cellId).first); sortAndLimitAlteredCells(); @@ -1369,10 +1571,10 @@ bool CSVRender::TerrainShapeMode::allowLandShapeEditing(const std::string& cellI { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); - if (mode=="Discard") + if (mode == "Discard") return false; - if (mode=="Create cell and land, then edit" && useTool) + if (mode == "Create cell and land, then edit" && useTool) { createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first); fixEdges(CSMWorld::CellCoordinates::fromId(cellId).first); @@ -1391,8 +1593,8 @@ bool CSVRender::TerrainShapeMode::allowLandShapeEditing(const std::string& cellI void CSVRender::TerrainShapeMode::fixEdges(CSMWorld::CellCoordinates cellCoords) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); @@ -1400,42 +1602,55 @@ void CSVRender::TerrainShapeMode::fixEdges(CSMWorld::CellCoordinates cellCoords) std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); - const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); - const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)).value(); - const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)).value(); - const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)).value(); - const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)).value(); + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value(); + const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer + = landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)) + .value(); + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer + = landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)) + .value(); + const CSMWorld::LandHeightsColumn::DataType landUpShapePointer + = landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)) + .value(); + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer + = landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)) + .value(); CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); - for(int i = 0; i < ESM::Land::LAND_SIZE; ++i) + for (int i = 0; i < ESM::Land::LAND_SIZE; ++i) { - if (isLandLoaded(cellLeftId) && - landShapePointer[i * ESM::Land::LAND_SIZE] != landLeftShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]) - landShapeNew[i * ESM::Land::LAND_SIZE] = landLeftShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]; - if (isLandLoaded(cellRightId) && - landShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1] != landRightShapePointer[i * ESM::Land::LAND_SIZE]) - landShapeNew[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1] = landRightShapePointer[i * ESM::Land::LAND_SIZE]; - if (isLandLoaded(cellUpId) && - landShapePointer[i] != landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i]) + if (isLandLoaded(cellLeftId) + && landShapePointer[i * ESM::Land::LAND_SIZE] + != landLeftShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]) + landShapeNew[i * ESM::Land::LAND_SIZE] + = landLeftShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]; + if (isLandLoaded(cellRightId) + && landShapePointer[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1] + != landRightShapePointer[i * ESM::Land::LAND_SIZE]) + landShapeNew[i * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1] + = landRightShapePointer[i * ESM::Land::LAND_SIZE]; + if (isLandLoaded(cellUpId) + && landShapePointer[i] != landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i]) landShapeNew[i] = landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i]; - if (isLandLoaded(cellDownId) && - landShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i] != landDownShapePointer[i]) + if (isLandLoaded(cellDownId) + && landShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i] != landDownShapePointer[i]) landShapeNew[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + i] = landDownShapePointer[i]; } QVariant changedLand; changedLand.setValue(landShapeNew); - QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandHeightsIndex))); + QModelIndex index( + landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex))); QUndoStack& undoStack = document.getUndoStack(); - undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); + undoStack.push(new CSMWorld::ModifyCommand(landTable, index, changedLand)); } -void CSVRender::TerrainShapeMode::dragMoveEvent (QDragMoveEvent *event) -{ -} +void CSVRender::TerrainShapeMode::dragMoveEvent(QDragMoveEvent* event) {} -void CSVRender::TerrainShapeMode::mouseMoveEvent (QMouseEvent *event) +void CSVRender::TerrainShapeMode::mouseMoveEvent(QMouseEvent* event) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); if (hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing)) @@ -1444,6 +1659,11 @@ void CSVRender::TerrainShapeMode::mouseMoveEvent (QMouseEvent *event) mBrushDraw->hide(); } +std::shared_ptr CSVRender::TerrainShapeMode::getTerrainSelection() +{ + return mTerrainShapeSelection; +} + void CSVRender::TerrainShapeMode::setBrushSize(int brushSize) { mBrushSize = brushSize; @@ -1453,7 +1673,7 @@ void CSVRender::TerrainShapeMode::setBrushShape(CSVWidget::BrushShape brushShape { mBrushShape = brushShape; - //Set custom brush shape + // Set custom brush shape if (mBrushShape == CSVWidget::BrushShape_Custom && !mTerrainShapeSelection->getTerrainSelection().empty()) { auto terrainSelection = mTerrainShapeSelection->getTerrainSelection(); @@ -1461,7 +1681,7 @@ void CSVRender::TerrainShapeMode::setBrushShape(CSVWidget::BrushShape brushShape int selectionCenterY = 0; int selectionAmount = 0; - for(auto const& value: terrainSelection) + for (auto const& value : terrainSelection) { selectionCenterX = selectionCenterX + value.first; selectionCenterY = selectionCenterY + value.second; @@ -1475,8 +1695,8 @@ void CSVRender::TerrainShapeMode::setBrushShape(CSVWidget::BrushShape brushShape } mCustomBrushShape.clear(); - std::pair differentialPos {}; - for(auto const& value: terrainSelection) + std::pair differentialPos{}; + for (auto const& value : terrainSelection) { differentialPos.first = value.first - selectionCenterX; differentialPos.second = value.second - selectionCenterY; diff --git a/apps/opencs/view/render/terrainshapemode.hpp b/apps/opencs/view/render/terrainshapemode.hpp index a88e60c9c41..2344676cbdb 100644 --- a/apps/opencs/view/render/terrainshapemode.hpp +++ b/apps/opencs/view/render/terrainshapemode.hpp @@ -3,196 +3,227 @@ #include "editmode.hpp" -#include #include +#include +#include +#include -#include -#include +#include + +#include #ifndef Q_MOC_RUN -#include "../../model/world/data.hpp" -#include "../../model/world/land.hpp" -#include "../../model/doc/document.hpp" -#include "../../model/world/commands.hpp" -#include "../../model/world/idtable.hpp" -#include "../../model/world/landtexture.hpp" +#include "../../model/world/columnimp.hpp" #include "../widget/brushshapes.hpp" #endif #include "brushdraw.hpp" -#include "terrainselection.hpp" + +class QDragMoveEvent; +class QMouseEvent; +class QObject; +class QPoint; +class QWidget; + +namespace osg +{ + class Group; +} + +namespace CSMDoc +{ + class Document; +} namespace CSVWidget { class SceneToolShapeBrush; } +namespace CSMWorld +{ + class IdTable; +} + namespace CSVRender { class PagedWorldspaceWidget; + class TerrainSelection; + class WorldspaceWidget; + struct WorldspaceHitResult; + class SceneToolbar; /// \brief EditMode for handling the terrain shape editing class TerrainShapeMode : public EditMode { Q_OBJECT - public: + public: + enum InteractionType + { + InteractionType_PrimaryEdit, + InteractionType_PrimarySelect, + InteractionType_SecondaryEdit, + InteractionType_SecondarySelect, + InteractionType_None + }; - enum InteractionType - { - InteractionType_PrimaryEdit, - InteractionType_PrimarySelect, - InteractionType_SecondaryEdit, - InteractionType_SecondarySelect, - InteractionType_None - }; + enum ShapeEditTool + { + ShapeEditTool_Drag = 0, + ShapeEditTool_PaintToRaise = 1, + ShapeEditTool_PaintToLower = 2, + ShapeEditTool_Smooth = 3, + ShapeEditTool_Flatten = 4, + ShapeEditTool_Equalize = 5 + }; - enum ShapeEditTool - { - ShapeEditTool_Drag = 0, - ShapeEditTool_PaintToRaise = 1, - ShapeEditTool_PaintToLower = 2, - ShapeEditTool_Smooth = 3, - ShapeEditTool_Flatten = 4 - }; + /// Editmode for terrain shape grid + TerrainShapeMode(WorldspaceWidget*, osg::Group* parentNode, QWidget* parent = nullptr); - /// Editmode for terrain shape grid - TerrainShapeMode(WorldspaceWidget*, osg::Group* parentNode, QWidget* parent = nullptr); + void primaryOpenPressed(const WorldspaceHitResult& hit) override; - void primaryOpenPressed (const WorldspaceHitResult& hit) override; + /// Create single command for one-click shape editing + void primaryEditPressed(const WorldspaceHitResult& hit) override; - /// Create single command for one-click shape editing - void primaryEditPressed (const WorldspaceHitResult& hit) override; + /// Open brush settings window + void primarySelectPressed(const WorldspaceHitResult&) override; - /// Open brush settings window - void primarySelectPressed(const WorldspaceHitResult&) override; + void secondarySelectPressed(const WorldspaceHitResult&) override; - void secondarySelectPressed(const WorldspaceHitResult&) override; + void activate(CSVWidget::SceneToolbar*) override; + void deactivate(CSVWidget::SceneToolbar*) override; - void activate(CSVWidget::SceneToolbar*) override; - void deactivate(CSVWidget::SceneToolbar*) override; + /// Start shape editing command macro + bool primaryEditStartDrag(const QPoint& pos) override; - /// Start shape editing command macro - bool primaryEditStartDrag (const QPoint& pos) override; + bool secondaryEditStartDrag(const QPoint& pos) override; + bool primarySelectStartDrag(const QPoint& pos) override; + bool secondarySelectStartDrag(const QPoint& pos) override; - bool secondaryEditStartDrag (const QPoint& pos) override; - bool primarySelectStartDrag (const QPoint& pos) override; - bool secondarySelectStartDrag (const QPoint& pos) override; + /// Handle shape edit behavior during dragging + void drag(const QPoint& pos, int diffX, int diffY, double speedFactor) override; - /// Handle shape edit behavior during dragging - void drag (const QPoint& pos, int diffX, int diffY, double speedFactor) override; + /// End shape editing command macro + void dragCompleted(const QPoint& pos) override; - /// End shape editing command macro - void dragCompleted(const QPoint& pos) override; + /// Cancel shape editing, and reset all pending changes + void dragAborted() override; - /// Cancel shape editing, and reset all pending changes - void dragAborted() override; + void dragWheel(int diff, double speedFactor) override; + void dragMoveEvent(QDragMoveEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; - void dragWheel (int diff, double speedFactor) override; - void dragMoveEvent (QDragMoveEvent *event) override; - void mouseMoveEvent (QMouseEvent *event) override; + std::shared_ptr getTerrainSelection(); - private: + private: + /// Remove duplicates and sort mAlteredCells, then limitAlteredHeights forward and reverse + void sortAndLimitAlteredCells(); - /// Remove duplicates and sort mAlteredCells, then limitAlteredHeights forward and reverse - void sortAndLimitAlteredCells(); + /// Reset everything in the current edit + void clearTransientEdits(); - /// Reset everything in the current edit - void clearTransientEdits(); + /// Move pending alteredHeights changes to omwgame/omwaddon -data + void applyTerrainEditChanges(); - /// Move pending alteredHeights changes to omwgame/omwaddon -data - void applyTerrainEditChanges(); + /// Handle brush mechanics for shape editing + void editTerrainShapeGrid(const std::pair& vertexCoords, bool dragOperation); - /// Handle brush mechanics for shape editing - void editTerrainShapeGrid (const std::pair& vertexCoords, bool dragOperation); + /// Calculate height, when aiming for bump-shaped terrain change + float calculateBumpShape(float distance, int radius, float height); - /// Calculate height, when aiming for bump-shaped terrain change - float calculateBumpShape(float distance, int radius, float height); + /// set the target height for flatten tool + void setFlattenToolTargetHeight(const WorldspaceHitResult& hit); - /// set the target height for flatten tool - void setFlattenToolTargetHeight(const WorldspaceHitResult& hit); + /// Do a single height alteration for transient shape edit map + void alterHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float alteredHeight, + bool useTool = true); - /// Do a single height alteration for transient shape edit map - void alterHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float alteredHeight, bool useTool = true); + /// Do a single smoothing height alteration for transient shape edit map + void smoothHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength); - /// Do a single smoothing height alteration for transient shape edit map - void smoothHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength); + /// Do a single flattening height alteration for transient shape edit map + void flattenHeight( + const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength, int targetHeight); - /// Do a single flattening height alteration for transient shape edit map - void flattenHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int toolStrength, int targetHeight); + /// Do a single equalize alteration for transient shape edit map + void equalizeHeight(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, int targetHeight); - /// Get altered height values around one vertex - void updateKeyHeightValues(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* thisHeight, - float* thisAlteredHeight, float* leftHeight, float* leftAlteredHeight, float* upHeight, float* upAlteredHeight, - float* rightHeight, float* rightAlteredHeight, float* downHeight, float* downAlteredHeight); + /// Get altered height values around one vertex + void updateKeyHeightValues(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, + float* thisHeight, float* thisAlteredHeight, float* leftHeight, float* leftAlteredHeight, float* upHeight, + float* upAlteredHeight, float* rightHeight, float* rightAlteredHeight, float* downHeight, + float* downAlteredHeight); - ///Limit steepness based on either X or Y and return false if steepness is limited - void compareAndLimit(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, float* limitedAlteredHeightXAxis, - float* limitedAlteredHeightYAxis, bool* steepnessIsWithinLimits); + /// Limit steepness based on either X or Y and return false if steepness is limited + void compareAndLimit(const CSMWorld::CellCoordinates& cellCoords, int inCellX, int inCellY, + float* limitedAlteredHeightXAxis, float* limitedAlteredHeightYAxis, bool* steepnessIsWithinLimits); - /// Check that the edit doesn't break save format limits, fix if necessary, return true if slope steepness is within limits - bool limitAlteredHeights(const CSMWorld::CellCoordinates& cellCoords, bool reverseMode = false); + /// Check that the edit doesn't break save format limits, fix if necessary, return true if slope steepness is + /// within limits + bool limitAlteredHeights(const CSMWorld::CellCoordinates& cellCoords, bool reverseMode = false); - /// Check if global selection coordinate belongs to cell in view - bool isInCellSelection(int globalSelectionX, int globalSelectionY); + /// Check if global selection coordinate belongs to cell in view + bool isInCellSelection(int globalSelectionX, int globalSelectionY); - /// Select vertex at global selection coordinate - void handleSelection(int globalSelectionX, int globalSelectionY, std::vector>* selections); + /// Select vertex at global selection coordinate + void handleSelection(int globalSelectionX, int globalSelectionY, std::vector>* selections); - /// Handle brush mechanics for terrain shape selection - void selectTerrainShapes (const std::pair& vertexCoords, unsigned char selectMode, bool dragOperation); + /// Handle brush mechanics for terrain shape selection + void selectTerrainShapes(const std::pair& vertexCoords, unsigned char selectMode); - /// Push terrain shape edits to command macro - void pushEditToCommand (const CSMWorld::LandHeightsColumn::DataType& newLandGrid, CSMDoc::Document& document, - CSMWorld::IdTable& landTable, const std::string& cellId); + /// Push terrain shape edits to command macro + void pushEditToCommand(const CSMWorld::LandHeightsColumn::DataType& newLandGrid, CSMDoc::Document& document, + CSMWorld::IdTable& landTable, const std::string& cellId); - /// Push land normals edits to command macro - void pushNormalsEditToCommand(const CSMWorld::LandNormalsColumn::DataType& newLandGrid, CSMDoc::Document& document, - CSMWorld::IdTable& landTable, const std::string& cellId); + /// Push land normals edits to command macro + void pushNormalsEditToCommand(const CSMWorld::LandNormalsColumn::DataType& newLandGrid, + CSMDoc::Document& document, CSMWorld::IdTable& landTable, const std::string& cellId); - bool noCell(const std::string& cellId); + bool noCell(const std::string& cellId); - bool noLand(const std::string& cellId); + bool noLand(const std::string& cellId); - bool noLandLoaded(const std::string& cellId); + bool noLandLoaded(const std::string& cellId); - bool isLandLoaded(const std::string& cellId); + bool isLandLoaded(const std::string& cellId); - /// Create new blank height record and new normals, if there are valid adjancent cell, take sample points and set the average height based on that - void createNewLandData(const CSMWorld::CellCoordinates& cellCoords); + /// Create new blank height record and new normals, if there are valid adjancent cell, take sample points and + /// set the average height based on that + void createNewLandData(const CSMWorld::CellCoordinates& cellCoords); - /// Create new cell and land if needed, only user tools may ask for opening new cells (useTool == false is for automated land changes) - bool allowLandShapeEditing(const std::string& textureFileName, bool useTool = true); + /// Create new cell and land if needed, only user tools may ask for opening new cells (useTool == false is for + /// automated land changes) + bool allowLandShapeEditing(const std::string& textureFileName, bool useTool = true); - /// Bind the edging vertice to the values of the adjancent cells - void fixEdges(CSMWorld::CellCoordinates cellCoords); + /// Bind the edging vertice to the values of the adjancent cells + void fixEdges(CSMWorld::CellCoordinates cellCoords); - std::string mBrushTexture; - int mBrushSize = 1; - CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; - std::unique_ptr mBrushDraw; - std::vector> mCustomBrushShape; - CSVWidget::SceneToolShapeBrush *mShapeBrushScenetool = nullptr; - int mDragMode = InteractionType_None; - osg::Group* mParentNode; - bool mIsEditing = false; - std::unique_ptr mTerrainShapeSelection; - int mTotalDiffY = 0; - std::vector mAlteredCells; - osg::Vec3d mEditingPos; - int mShapeEditTool = ShapeEditTool_Drag; - int mShapeEditToolStrength = 8; - int mTargetHeight = 0; + std::string mBrushTexture; + int mBrushSize = 1; + CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; + std::unique_ptr mBrushDraw; + std::vector> mCustomBrushShape; + CSVWidget::SceneToolShapeBrush* mShapeBrushScenetool = nullptr; + int mDragMode = InteractionType_None; + osg::Group* mParentNode; + bool mIsEditing = false; + std::shared_ptr mTerrainShapeSelection; + int mTotalDiffY = 0; + std::vector mAlteredCells; + osg::Vec3d mEditingPos; + int mShapeEditTool = ShapeEditTool_Drag; + int mShapeEditToolStrength = 8; + int mTargetHeight = 0; - PagedWorldspaceWidget& getPagedWorldspaceWidget(); + PagedWorldspaceWidget& getPagedWorldspaceWidget(); - public slots: - void setBrushSize(int brushSize); - void setBrushShape(CSVWidget::BrushShape brushShape); - void setShapeEditTool(int shapeEditTool); - void setShapeEditToolStrength(int shapeEditToolStrength); + public slots: + void setBrushSize(int brushSize); + void setBrushShape(CSVWidget::BrushShape brushShape); + void setShapeEditTool(int shapeEditTool); + void setShapeEditToolStrength(int shapeEditToolStrength); }; } - #endif diff --git a/apps/opencs/view/render/terrainstorage.cpp b/apps/opencs/view/render/terrainstorage.cpp index d9cc3015e13..1c494820dc8 100644 --- a/apps/opencs/view/render/terrainstorage.cpp +++ b/apps/opencs/view/render/terrainstorage.cpp @@ -1,43 +1,55 @@ #include "terrainstorage.hpp" -#include "../../model/world/land.hpp" -#include "../../model/world/landtexture.hpp" - +#include #include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include namespace CSVRender { - TerrainStorage::TerrainStorage(const CSMWorld::Data &data) + TerrainStorage::TerrainStorage(const CSMWorld::Data& data) : ESMTerrain::Storage(data.getResourceSystem()->getVFS()) , mData(data) { resetHeights(); } - osg::ref_ptr TerrainStorage::getLand(int cellX, int cellY) + osg::ref_ptr TerrainStorage::getLand(ESM::ExteriorCellLocation cellLocation) { // The cell isn't guaranteed to have Land. This is because the terrain implementation // has to wrap the vertices of the last row and column to the next cell, which may be a nonexisting cell - int index = mData.getLand().searchId(CSMWorld::Land::createUniqueRecordId(cellX, cellY)); + const int index = mData.getLand().searchId( + ESM::RefId::stringRefId(CSMWorld::Land::createUniqueRecordId(cellLocation.mX, cellLocation.mY))); if (index == -1) return nullptr; const ESM::Land& land = mData.getLand().getRecord(index).get(); - return new ESMTerrain::LandObject(&land, ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX); + return new ESMTerrain::LandObject( + land, ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX); } - const ESM::LandTexture* TerrainStorage::getLandTexture(int index, short plugin) + const std::string* TerrainStorage::getLandTexture(std::uint16_t index, int plugin) { - int row = mData.getLandTextures().searchId(CSMWorld::LandTexture::createUniqueRecordId(plugin, index)); - if (row == -1) - return nullptr; - - return &mData.getLandTextures().getRecord(row).get(); + return mData.getLandTextures().getLandTexture(index, plugin); } void TerrainStorage::setAlteredHeight(int inCellX, int inCellY, float height) { - mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX] = height - fmod(height, 8); //Limit to divisible by 8 to avoid cell seam breakage + mAlteredHeight[inCellY * ESM::Land::LAND_SIZE + inCellX] + = height - fmod(height, 8); // Limit to divisible by 8 to avoid cell seam breakage } void TerrainStorage::resetHeights() @@ -48,96 +60,101 @@ namespace CSVRender float TerrainStorage::getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY) { float height = 0.f; - osg::ref_ptr land = getLand (cellX, cellY); - if (land) - { - const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; - if (data) height = getVertexHeight(data, inCellX, inCellY); - } - else return height; - return mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX] + height; + const int index + = mData.getLand().searchId(ESM::RefId::stringRefId(CSMWorld::Land::createUniqueRecordId(cellX, cellY))); + if (index == -1) // no land! + return height; + + const ESM::Land::LandData* landData = mData.getLand().getRecord(index).get().getLandData(ESM::Land::DATA_VHGT); + height = landData->mHeights[inCellY * ESM::Land::LAND_SIZE + inCellX]; + return mAlteredHeight[inCellY * ESM::Land::LAND_SIZE + inCellX] + height; } float* TerrainStorage::getAlteredHeight(int inCellX, int inCellY) { - return &mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX]; + return &mAlteredHeight[inCellY * ESM::Land::LAND_SIZE + inCellX]; } - void TerrainStorage::getBounds(float &minX, float &maxX, float &minY, float &maxY) + void TerrainStorage::getBounds(float& minX, float& maxX, float& minY, float& maxY, ESM::RefId worldspace) { // not needed at the moment - this returns the bounds of the whole world, but we only edit individual cells throw std::runtime_error("getBounds not implemented"); } - int TerrainStorage::getThisHeight(int col, int row, const ESM::Land::LandData *heightData) const + int TerrainStorage::getThisHeight(int col, int row, std::span heightData) const { - return heightData->mHeights[col*ESM::Land::LAND_SIZE + row] + - mAlteredHeight[static_cast(col*ESM::Land::LAND_SIZE + row)]; + return heightData[col * ESM::Land::LAND_SIZE + row] + + mAlteredHeight[static_cast(col * ESM::Land::LAND_SIZE + row)]; } - int TerrainStorage::getLeftHeight(int col, int row, const ESM::Land::LandData *heightData) const + int TerrainStorage::getLeftHeight(int col, int row, std::span heightData) const { - return heightData->mHeights[(col)*ESM::Land::LAND_SIZE + row - 1] + - mAlteredHeight[static_cast((col)*ESM::Land::LAND_SIZE + row - 1)]; + return heightData[(col)*ESM::Land::LAND_SIZE + row - 1] + + mAlteredHeight[static_cast((col)*ESM::Land::LAND_SIZE + row - 1)]; } - int TerrainStorage::getRightHeight(int col, int row, const ESM::Land::LandData *heightData) const + int TerrainStorage::getRightHeight(int col, int row, std::span heightData) const { - return heightData->mHeights[col*ESM::Land::LAND_SIZE + row + 1] + - mAlteredHeight[static_cast(col*ESM::Land::LAND_SIZE + row + 1)]; + return heightData[col * ESM::Land::LAND_SIZE + row + 1] + + mAlteredHeight[static_cast(col * ESM::Land::LAND_SIZE + row + 1)]; } - int TerrainStorage::getUpHeight(int col, int row, const ESM::Land::LandData *heightData) const + int TerrainStorage::getUpHeight(int col, int row, std::span heightData) const { - return heightData->mHeights[(col - 1)*ESM::Land::LAND_SIZE + row] + - mAlteredHeight[static_cast((col - 1)*ESM::Land::LAND_SIZE + row)]; + return heightData[(col - 1) * ESM::Land::LAND_SIZE + row] + + mAlteredHeight[static_cast((col - 1) * ESM::Land::LAND_SIZE + row)]; } - int TerrainStorage::getDownHeight(int col, int row, const ESM::Land::LandData *heightData) const + int TerrainStorage::getDownHeight(int col, int row, std::span heightData) const { - return heightData->mHeights[(col + 1)*ESM::Land::LAND_SIZE + row] + - mAlteredHeight[static_cast((col + 1)*ESM::Land::LAND_SIZE + row)]; + return heightData[(col + 1) * ESM::Land::LAND_SIZE + row] + + mAlteredHeight[static_cast((col + 1) * ESM::Land::LAND_SIZE + row)]; } - int TerrainStorage::getHeightDifferenceToLeft(int col, int row, const ESM::Land::LandData *heightData) const + int TerrainStorage::getHeightDifferenceToLeft(int col, int row, std::span heightData) const { return abs(getThisHeight(col, row, heightData) - getLeftHeight(col, row, heightData)); } - int TerrainStorage::getHeightDifferenceToRight(int col, int row, const ESM::Land::LandData *heightData) const + int TerrainStorage::getHeightDifferenceToRight(int col, int row, std::span heightData) const { return abs(getThisHeight(col, row, heightData) - getRightHeight(col, row, heightData)); } - int TerrainStorage::getHeightDifferenceToUp(int col, int row, const ESM::Land::LandData *heightData) const + int TerrainStorage::getHeightDifferenceToUp(int col, int row, std::span heightData) const { return abs(getThisHeight(col, row, heightData) - getUpHeight(col, row, heightData)); } - int TerrainStorage::getHeightDifferenceToDown(int col, int row, const ESM::Land::LandData *heightData) const + int TerrainStorage::getHeightDifferenceToDown(int col, int row, std::span heightData) const { return abs(getThisHeight(col, row, heightData) - getDownHeight(col, row, heightData)); } - bool TerrainStorage::leftOrUpIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const + bool TerrainStorage::leftOrUpIsOverTheLimit( + int col, int row, int heightWarningLimit, std::span heightData) const { - return getHeightDifferenceToLeft(col, row, heightData) >= heightWarningLimit || - getHeightDifferenceToUp(col, row, heightData) >= heightWarningLimit; + return getHeightDifferenceToLeft(col, row, heightData) >= heightWarningLimit + || getHeightDifferenceToUp(col, row, heightData) >= heightWarningLimit; } - bool TerrainStorage::rightOrDownIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const + bool TerrainStorage::rightOrDownIsOverTheLimit( + int col, int row, int heightWarningLimit, std::span heightData) const { - return getHeightDifferenceToRight(col, row, heightData) >= heightWarningLimit || - getHeightDifferenceToDown(col, row, heightData) >= heightWarningLimit; + return getHeightDifferenceToRight(col, row, heightData) >= heightWarningLimit + || getHeightDifferenceToDown(col, row, heightData) >= heightWarningLimit; } - void TerrainStorage::adjustColor(int col, int row, const ESM::Land::LandData *heightData, osg::Vec4ub& color) const + void TerrainStorage::adjustColor(int col, int row, const ESM::LandData* heightData, osg::Vec4ub& color) const { + if (!heightData) + return; // Highlight broken height changes int heightWarningLimit = 1024; - if (((col > 0 && row > 0) && leftOrUpIsOverTheLimit(col, row, heightWarningLimit, heightData)) || - ((col < ESM::Land::LAND_SIZE - 1 && row < ESM::Land::LAND_SIZE - 1) && rightOrDownIsOverTheLimit(col, row, heightWarningLimit, heightData))) + if (((col > 0 && row > 0) && leftOrUpIsOverTheLimit(col, row, heightWarningLimit, heightData->getHeights())) + || ((col < ESM::Land::LAND_SIZE - 1 && row < ESM::Land::LAND_SIZE - 1) + && rightOrDownIsOverTheLimit(col, row, heightWarningLimit, heightData->getHeights()))) { color.r() = 255; color.g() = 0; @@ -147,6 +164,6 @@ namespace CSVRender float TerrainStorage::getAlteredHeight(int col, int row) const { - return mAlteredHeight[static_cast(col*ESM::Land::LAND_SIZE + row)]; + return mAlteredHeight[static_cast(col * ESM::Land::LAND_SIZE + row)]; } } diff --git a/apps/opencs/view/render/terrainstorage.hpp b/apps/opencs/view/render/terrainstorage.hpp index 762eb800368..f7a7f722011 100644 --- a/apps/opencs/view/render/terrainstorage.hpp +++ b/apps/opencs/view/render/terrainstorage.hpp @@ -3,9 +3,19 @@ #include +#include +#include #include +#include -#include "../../model/world/data.hpp" +namespace CSMWorld +{ + class Data; +} +namespace osg +{ + class Vec4ub; +} namespace CSVRender { @@ -27,24 +37,25 @@ namespace CSVRender const CSMWorld::Data& mData; std::array mAlteredHeight; - osg::ref_ptr getLand (int cellX, int cellY) override; - const ESM::LandTexture* getLandTexture(int index, short plugin) override; + osg::ref_ptr getLand(ESM::ExteriorCellLocation cellLocation) override; + const std::string* getLandTexture(std::uint16_t index, int plugin) override; - void getBounds(float& minX, float& maxX, float& minY, float& maxY) override; + void getBounds(float& minX, float& maxX, float& minY, float& maxY, ESM::RefId worldspace) override; - int getThisHeight(int col, int row, const ESM::Land::LandData *heightData) const; - int getLeftHeight(int col, int row, const ESM::Land::LandData *heightData) const; - int getRightHeight(int col, int row, const ESM::Land::LandData *heightData) const; - int getUpHeight(int col, int row, const ESM::Land::LandData *heightData) const; - int getDownHeight(int col, int row, const ESM::Land::LandData *heightData) const; - int getHeightDifferenceToLeft(int col, int row, const ESM::Land::LandData *heightData) const; - int getHeightDifferenceToRight(int col, int row, const ESM::Land::LandData *heightData) const; - int getHeightDifferenceToUp(int col, int row, const ESM::Land::LandData *heightData) const; - int getHeightDifferenceToDown(int col, int row, const ESM::Land::LandData *heightData) const; - bool leftOrUpIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const; - bool rightOrDownIsOverTheLimit(int col, int row, int heightWarningLimit, const ESM::Land::LandData *heightData) const; + int getThisHeight(int col, int row, std::span heightData) const; + int getLeftHeight(int col, int row, std::span heightData) const; + int getRightHeight(int col, int row, std::span heightData) const; + int getUpHeight(int col, int row, std::span heightData) const; + int getDownHeight(int col, int row, std::span heightData) const; + int getHeightDifferenceToLeft(int col, int row, std::span heightData) const; + int getHeightDifferenceToRight(int col, int row, std::span heightData) const; + int getHeightDifferenceToUp(int col, int row, std::span heightData) const; + int getHeightDifferenceToDown(int col, int row, std::span heightData) const; + bool leftOrUpIsOverTheLimit(int col, int row, int heightWarningLimit, std::span heightData) const; + bool rightOrDownIsOverTheLimit( + int col, int row, int heightWarningLimit, std::span heightData) const; - void adjustColor(int col, int row, const ESM::Land::LandData *heightData, osg::Vec4ub& color) const override; + void adjustColor(int col, int row, const ESM::LandData* heightData, osg::Vec4ub& color) const override; float getAlteredHeight(int col, int row) const override; }; diff --git a/apps/opencs/view/render/terraintexturemode.cpp b/apps/opencs/view/render/terraintexturemode.cpp index 4e267e94266..cfc7f50cf14 100644 --- a/apps/opencs/view/render/terraintexturemode.cpp +++ b/apps/opencs/view/render/terraintexturemode.cpp @@ -1,18 +1,34 @@ #include "terraintexturemode.hpp" +#include +#include +#include +#include +#include #include -#include -#include -#include -#include #include -#include -#include +#include +#include +#include -#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include -#include "../widget/modebutton.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetooltexturebrush.hpp" @@ -22,64 +38,73 @@ #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" -#include "../../model/world/landtexture.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/universalid.hpp" -#include "../widget/brushshapes.hpp" #include "brushdraw.hpp" #include "editmode.hpp" -#include "pagedworldspacewidget.hpp" #include "mask.hpp" -#include "object.hpp" // Something small needed regarding pointers from here () +#include "pagedworldspacewidget.hpp" #include "worldspacewidget.hpp" -CSVRender::TerrainTextureMode::TerrainTextureMode (WorldspaceWidget *worldspaceWidget, osg::Group* parentNode, QWidget *parent) -: EditMode (worldspaceWidget, QIcon {":scenetoolbar/editing-terrain-texture"}, Mask_Terrain | Mask_Reference, "Terrain texture editing", parent), - mBrushTexture("L0#0"), - mBrushSize(1), - mBrushShape(CSVWidget::BrushShape_Point), - mTextureBrushScenetool(nullptr), - mDragMode(InteractionType_None), - mParentNode(parentNode), - mIsEditing(false) +CSVRender::TerrainTextureMode::TerrainTextureMode( + WorldspaceWidget* worldspaceWidget, osg::Group* parentNode, QWidget* parent) + : EditMode(worldspaceWidget, Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-texture"), + Mask_Terrain | Mask_Reference, "Terrain texture editing", parent) + , mBrushSize(1) + , mBrushShape(CSVWidget::BrushShape_Point) + , mTextureBrushScenetool(nullptr) + , mDragMode(InteractionType_None) + , mParentNode(parentNode) + , mIsEditing(false) { } void CSVRender::TerrainTextureMode::activate(CSVWidget::SceneToolbar* toolbar) { - if(!mTextureBrushScenetool) + if (!mTextureBrushScenetool) { - mTextureBrushScenetool = new CSVWidget::SceneToolTextureBrush (toolbar, "scenetooltexturebrush", getWorldspaceWidget().getDocument()); - connect(mTextureBrushScenetool, SIGNAL (clicked()), mTextureBrushScenetool, SLOT (activate())); - connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passBrushSize(int)), this, SLOT(setBrushSize(int))); - connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passBrushShape(CSVWidget::BrushShape)), this, SLOT(setBrushShape(CSVWidget::BrushShape))); - connect(mTextureBrushScenetool->mTextureBrushWindow->mSizeSliders->mBrushSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(setBrushSize(int))); - connect(mTextureBrushScenetool, SIGNAL(passTextureId(std::string)), this, SLOT(setBrushTexture(std::string))); - connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passTextureId(std::string)), this, SLOT(setBrushTexture(std::string))); - - connect(mTextureBrushScenetool, SIGNAL(passEvent(QDropEvent*)), this, SLOT(handleDropEvent(QDropEvent*))); - connect(this, SIGNAL(passBrushTexture(std::string)), mTextureBrushScenetool->mTextureBrushWindow, SLOT(setBrushTexture(std::string))); - connect(this, SIGNAL(passBrushTexture(std::string)), mTextureBrushScenetool, SLOT(updateBrushHistory(std::string))); + mTextureBrushScenetool = new CSVWidget::SceneToolTextureBrush( + toolbar, "scenetooltexturebrush", getWorldspaceWidget().getDocument()); + connect(mTextureBrushScenetool, &CSVWidget::SceneTool::clicked, mTextureBrushScenetool, + &CSVWidget::SceneToolTextureBrush::activate); + connect(mTextureBrushScenetool->mTextureBrushWindow, &CSVWidget::TextureBrushWindow::passBrushSize, this, + &TerrainTextureMode::setBrushSize); + connect(mTextureBrushScenetool->mTextureBrushWindow, &CSVWidget::TextureBrushWindow::passBrushShape, this, + &TerrainTextureMode::setBrushShape); + connect(mTextureBrushScenetool->mTextureBrushWindow->mSizeSliders->mBrushSizeSlider, &QSlider::valueChanged, + this, &TerrainTextureMode::setBrushSize); + connect(mTextureBrushScenetool, &CSVWidget::SceneToolTextureBrush::passTextureId, this, + &TerrainTextureMode::setBrushTexture); + connect(mTextureBrushScenetool->mTextureBrushWindow, &CSVWidget::TextureBrushWindow::passTextureId, this, + &TerrainTextureMode::setBrushTexture); + + connect(mTextureBrushScenetool, qOverload(&CSVWidget::SceneToolTextureBrush::passEvent), this, + &TerrainTextureMode::handleDropEvent); + connect(this, &TerrainTextureMode::passBrushTexture, mTextureBrushScenetool->mTextureBrushWindow, + &CSVWidget::TextureBrushWindow::setBrushTexture); + connect(this, &TerrainTextureMode::passBrushTexture, mTextureBrushScenetool, + &CSVWidget::SceneToolTextureBrush::updateBrushHistory); } if (!mTerrainTextureSelection) { - mTerrainTextureSelection.reset(new TerrainSelection(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Texture)); + mTerrainTextureSelection + = std::make_shared(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Texture); } if (!mBrushDraw) - mBrushDraw.reset(new BrushDraw(mParentNode, true)); + mBrushDraw = std::make_unique(mParentNode, true); EditMode::activate(toolbar); - toolbar->addTool (mTextureBrushScenetool); + toolbar->addTool(mTextureBrushScenetool); } void CSVRender::TerrainTextureMode::deactivate(CSVWidget::SceneToolbar* toolbar) { - if(mTextureBrushScenetool) + if (mTextureBrushScenetool) { - toolbar->removeTool (mTextureBrushScenetool); + toolbar->removeTool(mTextureBrushScenetool); delete mTextureBrushScenetool; mTextureBrushScenetool = nullptr; } @@ -102,23 +127,23 @@ void CSVRender::TerrainTextureMode::primaryOpenPressed(const WorldspaceHitResult void CSVRender::TerrainTextureMode::primaryEditPressed(const WorldspaceHitResult& hit) // Apply changes here { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); - CSMWorld::IdTable& ltexTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& ltexTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); - mCellId = getWorldspaceWidget().getCellId (hit.worldPos); + mCellId = getWorldspaceWidget().getCellId(hit.worldPos); QUndoStack& undoStack = document.getUndoStack(); - CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { - undoStack.beginMacro ("Edit texture records"); - if(allowLandTextureEditing(mCellId)) + undoStack.beginMacro("Edit texture records"); + if (allowLandTextureEditing(mCellId)) { - undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId)); + undoStack.push(new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId)); editTerrainTextureGrid(hit); } undoStack.endMacro(); @@ -127,46 +152,48 @@ void CSVRender::TerrainTextureMode::primaryEditPressed(const WorldspaceHitResult void CSVRender::TerrainTextureMode::primarySelectPressed(const WorldspaceHitResult& hit) { - if(hit.hit && hit.tag == nullptr) + if (hit.hit && hit.tag == nullptr) { - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, false); + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0); + mTerrainTextureSelection->clearTemporarySelection(); } } void CSVRender::TerrainTextureMode::secondarySelectPressed(const WorldspaceHitResult& hit) { - if(hit.hit && hit.tag == nullptr) + if (hit.hit && hit.tag == nullptr) { - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, false); + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1); + mTerrainTextureSelection->clearTemporarySelection(); } } -bool CSVRender::TerrainTextureMode::primaryEditStartDrag (const QPoint& pos) +bool CSVRender::TerrainTextureMode::primaryEditStartDrag(const QPoint& pos) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); - CSMWorld::IdTable& ltexTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& ltexTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); - mCellId = getWorldspaceWidget().getCellId (hit.worldPos); + mCellId = getWorldspaceWidget().getCellId(hit.worldPos); QUndoStack& undoStack = document.getUndoStack(); mDragMode = InteractionType_PrimaryEdit; - CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); - int index = landtexturesCollection.searchId(mBrushTexture); + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + const int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { - undoStack.beginMacro ("Edit texture records"); + undoStack.beginMacro("Edit texture records"); mIsEditing = true; - if(allowLandTextureEditing(mCellId)) + if (allowLandTextureEditing(mCellId)) { - undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId)); + undoStack.push(new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId)); editTerrainTextureGrid(hit); } } @@ -174,47 +201,47 @@ bool CSVRender::TerrainTextureMode::primaryEditStartDrag (const QPoint& pos) return true; } -bool CSVRender::TerrainTextureMode::secondaryEditStartDrag (const QPoint& pos) +bool CSVRender::TerrainTextureMode::secondaryEditStartDrag(const QPoint& pos) { return false; } -bool CSVRender::TerrainTextureMode::primarySelectStartDrag (const QPoint& pos) +bool CSVRender::TerrainTextureMode::primarySelectStartDrag(const QPoint& pos) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_PrimarySelect; if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, true); - return false; + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0); + return true; } -bool CSVRender::TerrainTextureMode::secondarySelectStartDrag (const QPoint& pos) +bool CSVRender::TerrainTextureMode::secondarySelectStartDrag(const QPoint& pos) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_SecondarySelect; if (!hit.hit || hit.tag != nullptr) { mDragMode = InteractionType_None; return false; } - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, true); - return false; + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1); + return true; } -void CSVRender::TerrainTextureMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) +void CSVRender::TerrainTextureMode::drag(const QPoint& pos, int diffX, int diffY, double speedFactor) { if (mDragMode == InteractionType_PrimaryEdit) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - std::string cellId = getWorldspaceWidget().getCellId (hit.worldPos); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + std::string cellId = getWorldspaceWidget().getCellId(hit.worldPos); CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); - int index = landtexturesCollection.searchId(mBrushTexture); + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + const int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { @@ -224,14 +251,16 @@ void CSVRender::TerrainTextureMode::drag (const QPoint& pos, int diffX, int diff if (mDragMode == InteractionType_PrimarySelect) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == nullptr) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, true); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + if (hit.hit && hit.tag == nullptr) + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0); } if (mDragMode == InteractionType_SecondarySelect) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == nullptr) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, true); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + if (hit.hit && hit.tag == nullptr) + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1); } } @@ -242,72 +271,61 @@ void CSVRender::TerrainTextureMode::dragCompleted(const QPoint& pos) CSMDoc::Document& document = getWorldspaceWidget().getDocument(); QUndoStack& undoStack = document.getUndoStack(); - CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); - int index = landtexturesCollection.searchId(mBrushTexture); + CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); + const int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { - undoStack.endMacro(); - mIsEditing = false; + undoStack.endMacro(); + mIsEditing = false; } } -} -void CSVRender::TerrainTextureMode::dragAborted() -{ + mTerrainTextureSelection->clearTemporarySelection(); } -void CSVRender::TerrainTextureMode::dragWheel (int diff, double speedFactor) -{ -} +void CSVRender::TerrainTextureMode::dragAborted() {} -void CSVRender::TerrainTextureMode::handleDropEvent (QDropEvent *event) +void CSVRender::TerrainTextureMode::dragWheel(int diff, double speedFactor) {} + +void CSVRender::TerrainTextureMode::handleDropEvent(QDropEvent* event) { - const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); - - if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped - return; - - if (mime->holdsType (CSMWorld::UniversalId::Type_LandTexture)) - { - const std::vector ids = mime->getData(); - - for (const CSMWorld::UniversalId& uid : ids) - { - mBrushTexture = uid.getId(); - emit passBrushTexture(mBrushTexture); - } - } - if (mime->holdsType (CSMWorld::UniversalId::Type_Texture)) - { - const std::vector ids = mime->getData(); - - for (const CSMWorld::UniversalId& uid : ids) - { - std::string textureFileName = uid.toString(); - createTexture(textureFileName); - emit passBrushTexture(mBrushTexture); - } - } + const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); + + if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped + return; + + if (mime->holdsType(CSMWorld::UniversalId::Type_LandTexture)) + { + const std::vector ids = mime->getData(); + + for (const CSMWorld::UniversalId& uid : ids) + { + mBrushTexture = ESM::RefId::stringRefId(uid.getId()); + emit passBrushTexture(mBrushTexture); + } + } } void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitResult& hit) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); - mCellId = getWorldspaceWidget().getCellId (hit.worldPos); - if(allowLandTextureEditing(mCellId)) {} + mCellId = getWorldspaceWidget().getCellId(hit.worldPos); + if (allowLandTextureEditing(mCellId)) + { + } - std::pair cellCoordinates_pair = CSMWorld::CellCoordinates::fromId (mCellId); + std::pair cellCoordinates_pair = CSMWorld::CellCoordinates::fromId(mCellId); int cellX = cellCoordinates_pair.first.getX(); int cellY = cellCoordinates_pair.first.getY(); // The coordinates of hit in mCellId - int xHitInCell (float(((hit.worldPos.x() - (cellX* cellSize)) * landTextureSize / cellSize) - 0.25)); - int yHitInCell (float(((hit.worldPos.y() - (cellY* cellSize)) * landTextureSize / cellSize) + 0.25)); + int xHitInCell(float(((hit.worldPos.x() - (cellX * cellSize)) * landTextureSize / cellSize) - 0.25)); + int yHitInCell(float(((hit.worldPos.y() - (cellY * cellSize)) * landTextureSize / cellSize) + 0.25)); if (xHitInCell < 0) { xHitInCell = xHitInCell + landTextureSize; @@ -320,71 +338,86 @@ void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitRe } mCellId = CSMWorld::CellCoordinates::generateId(cellX, cellY); - if(allowLandTextureEditing(mCellId)) {} + if (allowLandTextureEditing(mCellId)) + { + } std::string iteratedCellId; int textureColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandTexturesIndex); - std::size_t hashlocation = mBrushTexture.find('#'); - std::string mBrushTextureInt = mBrushTexture.substr (hashlocation+1); - int brushInt = stoi(mBrushTexture.substr (hashlocation+1))+1; // All indices are offset by +1 + // All indices are offset by +1 + uint32_t brushInt = document.getData().getLandTextures().getRecord(mBrushTexture).get().mIndex + 1; int r = static_cast(mBrushSize) / 2; if (mBrushShape == CSVWidget::BrushShape_Point) { - CSMWorld::LandTexturesColumn::DataType newTerrainPointer = landTable.data(landTable.getModelIndex(mCellId, textureColumn)).value(); - CSMWorld::LandTexturesColumn::DataType newTerrain(newTerrainPointer); + CSMWorld::LandTexturesColumn::DataType newTerrain + = landTable.data(landTable.getModelIndex(mCellId, textureColumn)) + .value(); - if(allowLandTextureEditing(mCellId)) + if (allowLandTextureEditing(mCellId)) { - newTerrain[yHitInCell*landTextureSize+xHitInCell] = brushInt; + newTerrain[yHitInCell * landTextureSize + xHitInCell] = brushInt; pushEditToCommand(newTerrain, document, landTable, mCellId); } } if (mBrushShape == CSVWidget::BrushShape_Square) { - int upperLeftCellX = cellX - std::floor(r / landTextureSize); - int upperLeftCellY = cellY - std::floor(r / landTextureSize); - if (xHitInCell - (r % landTextureSize) < 0) upperLeftCellX--; - if (yHitInCell - (r % landTextureSize) < 0) upperLeftCellY--; + int upperLeftCellX = cellX - std::floor(r / landTextureSize); + int upperLeftCellY = cellY - std::floor(r / landTextureSize); + if (xHitInCell - (r % landTextureSize) < 0) + upperLeftCellX--; + if (yHitInCell - (r % landTextureSize) < 0) + upperLeftCellY--; int lowerrightCellX = cellX + std::floor(r / landTextureSize); int lowerrightCellY = cellY + std::floor(r / landTextureSize); - if (xHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellX++; - if (yHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellY++; + if (xHitInCell + (r % landTextureSize) > landTextureSize - 1) + lowerrightCellX++; + if (yHitInCell + (r % landTextureSize) > landTextureSize - 1) + lowerrightCellY++; - for(int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) + for (int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) { - for(int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) + for (int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) { iteratedCellId = CSMWorld::CellCoordinates::generateId(i_cell, j_cell); - if(allowLandTextureEditing(iteratedCellId)) + if (allowLandTextureEditing(iteratedCellId)) { - CSMWorld::LandTexturesColumn::DataType newTerrainPointer = landTable.data(landTable.getModelIndex(iteratedCellId, textureColumn)).value(); - CSMWorld::LandTexturesColumn::DataType newTerrain(newTerrainPointer); - for(int i = 0; i < landTextureSize; i++) + CSMWorld::LandTexturesColumn::DataType newTerrain + = landTable.data(landTable.getModelIndex(iteratedCellId, textureColumn)) + .value(); + for (int i = 0; i < landTextureSize; i++) { - for(int j = 0; j < landTextureSize; j++) + for (int j = 0; j < landTextureSize; j++) { - if (i_cell == cellX && j_cell == cellY && abs(i-xHitInCell) < r && abs(j-yHitInCell) < r) + if (i_cell == cellX && j_cell == cellY && abs(i - xHitInCell) < r + && abs(j - yHitInCell) < r) { - newTerrain[j*landTextureSize+i] = brushInt; + newTerrain[j * landTextureSize + i] = brushInt; } else { int distanceX(0); int distanceY(0); - if (i_cell < cellX) distanceX = xHitInCell + landTextureSize * abs(i_cell-cellX) - i; - if (j_cell < cellY) distanceY = yHitInCell + landTextureSize * abs(j_cell-cellY) - j; - if (i_cell > cellX) distanceX = -xHitInCell + landTextureSize * abs(i_cell-cellX) + i; - if (j_cell > cellY) distanceY = -yHitInCell + landTextureSize * abs(j_cell-cellY) + j; - if (i_cell == cellX) distanceX = abs(i-xHitInCell); - if (j_cell == cellY) distanceY = abs(j-yHitInCell); - if (distanceX < r && distanceY < r) newTerrain[j*landTextureSize+i] = brushInt; + if (i_cell < cellX) + distanceX = xHitInCell + landTextureSize * abs(i_cell - cellX) - i; + if (j_cell < cellY) + distanceY = yHitInCell + landTextureSize * abs(j_cell - cellY) - j; + if (i_cell > cellX) + distanceX = -xHitInCell + landTextureSize * abs(i_cell - cellX) + i; + if (j_cell > cellY) + distanceY = -yHitInCell + landTextureSize * abs(j_cell - cellY) + j; + if (i_cell == cellX) + distanceX = abs(i - xHitInCell); + if (j_cell == cellY) + distanceY = abs(j - yHitInCell); + if (distanceX < r && distanceY < r) + newTerrain[j * landTextureSize + i] = brushInt; } } } @@ -396,50 +429,64 @@ void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitRe if (mBrushShape == CSVWidget::BrushShape_Circle) { - int upperLeftCellX = cellX - std::floor(r / landTextureSize); - int upperLeftCellY = cellY - std::floor(r / landTextureSize); - if (xHitInCell - (r % landTextureSize) < 0) upperLeftCellX--; - if (yHitInCell - (r % landTextureSize) < 0) upperLeftCellY--; + int upperLeftCellX = cellX - std::floor(r / landTextureSize); + int upperLeftCellY = cellY - std::floor(r / landTextureSize); + if (xHitInCell - (r % landTextureSize) < 0) + upperLeftCellX--; + if (yHitInCell - (r % landTextureSize) < 0) + upperLeftCellY--; int lowerrightCellX = cellX + std::floor(r / landTextureSize); int lowerrightCellY = cellY + std::floor(r / landTextureSize); - if (xHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellX++; - if (yHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellY++; + if (xHitInCell + (r % landTextureSize) > landTextureSize - 1) + lowerrightCellX++; + if (yHitInCell + (r % landTextureSize) > landTextureSize - 1) + lowerrightCellY++; - for(int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) + for (int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) { - for(int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) + for (int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) { iteratedCellId = CSMWorld::CellCoordinates::generateId(i_cell, j_cell); - if(allowLandTextureEditing(iteratedCellId)) + if (allowLandTextureEditing(iteratedCellId)) { - CSMWorld::LandTexturesColumn::DataType newTerrainPointer = landTable.data(landTable.getModelIndex(iteratedCellId, textureColumn)).value(); - CSMWorld::LandTexturesColumn::DataType newTerrain(newTerrainPointer); - for(int i = 0; i < landTextureSize; i++) + CSMWorld::LandTexturesColumn::DataType newTerrain + = landTable.data(landTable.getModelIndex(iteratedCellId, textureColumn)) + .value(); + for (int i = 0; i < landTextureSize; i++) { - for(int j = 0; j < landTextureSize; j++) + for (int j = 0; j < landTextureSize; j++) { - if (i_cell == cellX && j_cell == cellY && abs(i-xHitInCell) < r && abs(j-yHitInCell) < r) + if (i_cell == cellX && j_cell == cellY && abs(i - xHitInCell) < r + && abs(j - yHitInCell) < r) { - int distanceX = abs(i-xHitInCell); - int distanceY = abs(j-yHitInCell); - float distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2))); + int distanceX = abs(i - xHitInCell); + int distanceY = abs(j - yHitInCell); + float distance = std::round(sqrt(pow(distanceX, 2) + pow(distanceY, 2))); float rf = static_cast(mBrushSize) / 2; - if (distance < rf) newTerrain[j*landTextureSize+i] = brushInt; + if (distance < rf) + newTerrain[j * landTextureSize + i] = brushInt; } else { int distanceX(0); int distanceY(0); - if (i_cell < cellX) distanceX = xHitInCell + landTextureSize * abs(i_cell-cellX) - i; - if (j_cell < cellY) distanceY = yHitInCell + landTextureSize * abs(j_cell-cellY) - j; - if (i_cell > cellX) distanceX = -xHitInCell + landTextureSize * abs(i_cell-cellX) + i; - if (j_cell > cellY) distanceY = -yHitInCell + landTextureSize * abs(j_cell-cellY) + j; - if (i_cell == cellX) distanceX = abs(i-xHitInCell); - if (j_cell == cellY) distanceY = abs(j-yHitInCell); - float distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2))); + if (i_cell < cellX) + distanceX = xHitInCell + landTextureSize * abs(i_cell - cellX) - i; + if (j_cell < cellY) + distanceY = yHitInCell + landTextureSize * abs(j_cell - cellY) - j; + if (i_cell > cellX) + distanceX = -xHitInCell + landTextureSize * abs(i_cell - cellX) + i; + if (j_cell > cellY) + distanceY = -yHitInCell + landTextureSize * abs(j_cell - cellY) + j; + if (i_cell == cellX) + distanceX = abs(i - xHitInCell); + if (j_cell == cellY) + distanceY = abs(j - yHitInCell); + float distance = std::round(sqrt(pow(distanceX, 2) + pow(distanceY, 2))); float rf = static_cast(mBrushSize) / 2; - if (distance < rf) newTerrain[j*landTextureSize+i] = brushInt; + if (distance < rf) + newTerrain[j * landTextureSize + i] = brushInt; } } } @@ -451,31 +498,35 @@ void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitRe if (mBrushShape == CSVWidget::BrushShape_Custom) { - CSMWorld::LandTexturesColumn::DataType newTerrainPointer = landTable.data(landTable.getModelIndex(mCellId, textureColumn)).value(); - CSMWorld::LandTexturesColumn::DataType newTerrain(newTerrainPointer); + CSMWorld::LandTexturesColumn::DataType newTerrain + = landTable.data(landTable.getModelIndex(mCellId, textureColumn)) + .value(); - if(allowLandTextureEditing(mCellId) && !mCustomBrushShape.empty()) + if (allowLandTextureEditing(mCellId) && !mCustomBrushShape.empty()) { - for(auto const& value: mCustomBrushShape) + for (auto const& value : mCustomBrushShape) { - if(yHitInCell + value.second >= 0 && yHitInCell + value.second <= 15 && xHitInCell + value.first >= 0 && xHitInCell + value.first <= 15) + if (yHitInCell + value.second >= 0 && yHitInCell + value.second <= 15 && xHitInCell + value.first >= 0 + && xHitInCell + value.first <= 15) { - newTerrain[(yHitInCell+value.second)*landTextureSize+xHitInCell+value.first] = brushInt; + newTerrain[(yHitInCell + value.second) * landTextureSize + xHitInCell + value.first] = brushInt; } else { - int cellXDifference = std::floor(1.0f*(xHitInCell + value.first)/landTextureSize); - int cellYDifference = std::floor(1.0f*(yHitInCell + value.second)/landTextureSize); + int cellXDifference = std::floor(1.0f * (xHitInCell + value.first) / landTextureSize); + int cellYDifference = std::floor(1.0f * (yHitInCell + value.second) / landTextureSize); int xInOtherCell = xHitInCell + value.first - cellXDifference * landTextureSize; int yInOtherCell = yHitInCell + value.second - cellYDifference * landTextureSize; - std::string cellId = CSMWorld::CellCoordinates::generateId(cellX+cellXDifference, cellY+cellYDifference); + std::string cellId + = CSMWorld::CellCoordinates::generateId(cellX + cellXDifference, cellY + cellYDifference); if (allowLandTextureEditing(cellId)) { - CSMWorld::LandTexturesColumn::DataType newTerrainPointerOtherCell = landTable.data(landTable.getModelIndex(cellId, textureColumn)).value(); - CSMWorld::LandTexturesColumn::DataType newTerrainOtherCell(newTerrainPointerOtherCell); - newTerrainOtherCell[yInOtherCell*landTextureSize+xInOtherCell] = brushInt; - pushEditToCommand(newTerrainOtherCell, document, landTable, cellId); + CSMWorld::LandTexturesColumn::DataType newTerrainOtherCell + = landTable.data(landTable.getModelIndex(cellId, textureColumn)) + .value(); + newTerrainOtherCell[yInOtherCell * landTextureSize + xInOtherCell] = brushInt; + pushEditToCommand(newTerrainOtherCell, document, landTable, std::move(cellId)); } } } @@ -486,7 +537,8 @@ void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitRe bool CSVRender::TerrainTextureMode::isInCellSelection(int globalSelectionX, int globalSelectionY) { - if (CSVRender::PagedWorldspaceWidget *paged = dynamic_cast (&getWorldspaceWidget())) + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { std::pair textureCoords = std::make_pair(globalSelectionX, globalSelectionY); std::string cellId = CSMWorld::CellCoordinates::textureGlobalToCellId(textureCoords); @@ -495,15 +547,16 @@ bool CSVRender::TerrainTextureMode::isInCellSelection(int globalSelectionX, int return false; } - -void CSVRender::TerrainTextureMode::selectTerrainTextures(const std::pair& texCoords, unsigned char selectMode, bool dragOperation) +void CSVRender::TerrainTextureMode::selectTerrainTextures( + const std::pair& texCoords, unsigned char selectMode) { int r = mBrushSize / 2; std::vector> selections; if (mBrushShape == CSVWidget::BrushShape_Point) { - if (isInCellSelection(texCoords.first, texCoords.second)) selections.emplace_back(texCoords); + if (isInCellSelection(texCoords.first, texCoords.second)) + selections.emplace_back(texCoords); } if (mBrushShape == CSVWidget::BrushShape_Square) @@ -528,7 +581,7 @@ void CSVRender::TerrainTextureMode::selectTerrainTextures(const std::pair(mBrushSize) / 2; if (std::round(coords.length()) < rf) { @@ -545,9 +598,9 @@ void CSVRender::TerrainTextureMode::selectTerrainTextures(const std::paironlySelect(selections); - if(selectMode == 1) mTerrainTextureSelection->toggleSelect(selections, dragOperation); + std::string selectAction; + + if (selectMode == 0) + selectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString(); + else + selectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString(); + + if (selectAction == "Select only") + mTerrainTextureSelection->onlySelect(selections); + else if (selectAction == "Add to selection") + mTerrainTextureSelection->addSelect(selections); + else if (selectAction == "Remove from selection") + mTerrainTextureSelection->removeSelect(selections); + else if (selectAction == "Invert selection") + mTerrainTextureSelection->toggleSelect(selections); } -void CSVRender::TerrainTextureMode::pushEditToCommand(CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, - CSMWorld::IdTable& landTable, std::string cellId) +void CSVRender::TerrainTextureMode::pushEditToCommand(CSMWorld::LandTexturesColumn::DataType& newLandGrid, + CSMDoc::Document& document, CSMWorld::IdTable& landTable, std::string cellId) { - CSMWorld::IdTable& ltexTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); + CSMWorld::IdTable& ltexTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); QVariant changedLand; changedLand.setValue(newLandGrid); - QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandTexturesIndex))); + QModelIndex index( + landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandTexturesIndex))); QUndoStack& undoStack = document.getUndoStack(); - undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand)); - undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); + undoStack.push(new CSMWorld::ModifyCommand(landTable, index, changedLand)); + undoStack.push(new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); } -void CSVRender::TerrainTextureMode::createTexture(std::string textureFileName) +bool CSVRender::TerrainTextureMode::allowLandTextureEditing(const std::string& cellId) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTree& cellTable + = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Cells)); - CSMWorld::IdTable& ltexTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); - - QUndoStack& undoStack = document.getUndoStack(); - - std::string newId; - - int counter=0; - bool freeIndexFound = false; - do - { - const size_t maxCounter = std::numeric_limits::max() - 1; - try - { - newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); - if (ltexTable.getRecord(newId).isDeleted() == 0) counter = (counter + 1) % maxCounter; - } - catch (const std::exception&) - { - newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); - freeIndexFound = true; - } - } while (freeIndexFound == false); - - std::size_t idlocation = textureFileName.find("Texture: "); - textureFileName = textureFileName.substr (idlocation + 9); - - QVariant textureNameVariant; - - QVariant textureFileNameVariant; - textureFileNameVariant.setValue(QString::fromStdString(textureFileName)); - - undoStack.beginMacro ("Add land texture record"); - - undoStack.push (new CSMWorld::CreateCommand (ltexTable, newId)); - QModelIndex index(ltexTable.getModelIndex (newId, ltexTable.findColumnIndex (CSMWorld::Columns::ColumnId_Texture))); - undoStack.push (new CSMWorld::ModifyCommand(ltexTable, index, textureFileNameVariant)); - undoStack.endMacro(); - mBrushTexture = newId; -} - -bool CSVRender::TerrainTextureMode::allowLandTextureEditing(std::string cellId) -{ - CSMDoc::Document& document = getWorldspaceWidget().getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); - CSMWorld::IdTree& cellTable = dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); - - bool noCell = document.getData().getCells().searchId (cellId)==-1; - bool noLand = document.getData().getLand().searchId (cellId)==-1; + const ESM::RefId cellRefId = ESM::RefId::stringRefId(cellId); + const bool noCell = document.getData().getCells().searchId(cellRefId) == -1; + const bool noLand = document.getData().getLand().searchId(cellRefId) == -1; if (noCell) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); // target cell does not exist - if (mode=="Discard") + if (mode == "Discard") return false; - if (mode=="Create cell and land, then edit") + if (mode == "Create cell and land, then edit") { - std::unique_ptr createCommand ( - new CSMWorld::CreateCommand (cellTable, cellId)); - int parentIndex = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); - int index = cellTable.findNestedColumnIndex (parentIndex, CSMWorld::Columns::ColumnId_Interior); - createCommand->addNestedValue (parentIndex, index, false); - document.getUndoStack().push (createCommand.release()); - - if (CSVRender::PagedWorldspaceWidget *paged = - dynamic_cast (&getWorldspaceWidget())) + auto createCommand = std::make_unique(cellTable, cellId); + int parentIndex = cellTable.findColumnIndex(CSMWorld::Columns::ColumnId_Cell); + int index = cellTable.findNestedColumnIndex(parentIndex, CSMWorld::Columns::ColumnId_Interior); + createCommand->addNestedValue(parentIndex, index, false); + document.getUndoStack().push(createCommand.release()); + + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); - selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); - paged->setCellSelection (selection); + selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); + paged->setCellSelection(selection); } } } - else if (CSVRender::PagedWorldspaceWidget *paged = - dynamic_cast (&getWorldspaceWidget())) + else if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast(&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); - if (!selection.has (CSMWorld::CellCoordinates::fromId (cellId).first)) + if (!selection.has(CSMWorld::CellCoordinates::fromId(cellId).first)) { // target cell exists, but is not shown - std::string mode = - CSMPrefs::get()["3D Scene Editing"]["outside-visible-landedit"].toString(); + std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-visible-landedit"].toString(); - if (mode=="Discard") + if (mode == "Discard") return false; - if (mode=="Show cell and edit") + if (mode == "Show cell and edit") { - selection.add (CSMWorld::CellCoordinates::fromId (cellId).first); - paged->setCellSelection (selection); + selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); + paged->setCellSelection(selection); } } } @@ -687,23 +708,21 @@ bool CSVRender::TerrainTextureMode::allowLandTextureEditing(std::string cellId) std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); // target cell does not exist - if (mode=="Discard") + if (mode == "Discard") return false; - if (mode=="Create cell and land, then edit") + if (mode == "Create cell and land, then edit") { - document.getUndoStack().push (new CSMWorld::CreateCommand (landTable, cellId)); + document.getUndoStack().push(new CSMWorld::CreateCommand(landTable, cellId)); } } return true; } -void CSVRender::TerrainTextureMode::dragMoveEvent (QDragMoveEvent *event) -{ -} +void CSVRender::TerrainTextureMode::dragMoveEvent(QDragMoveEvent* event) {} -void CSVRender::TerrainTextureMode::mouseMoveEvent (QMouseEvent *event) +void CSVRender::TerrainTextureMode::mouseMoveEvent(QMouseEvent* event) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); if (hit.hit && mBrushDraw) @@ -712,6 +731,10 @@ void CSVRender::TerrainTextureMode::mouseMoveEvent (QMouseEvent *event) mBrushDraw->hide(); } +std::shared_ptr CSVRender::TerrainTextureMode::getTerrainSelection() +{ + return mTerrainTextureSelection; +} void CSVRender::TerrainTextureMode::setBrushSize(int brushSize) { @@ -722,7 +745,7 @@ void CSVRender::TerrainTextureMode::setBrushShape(CSVWidget::BrushShape brushSha { mBrushShape = brushShape; - //Set custom brush shape + // Set custom brush shape if (mBrushShape == CSVWidget::BrushShape_Custom && !mTerrainTextureSelection->getTerrainSelection().empty()) { auto terrainSelection = mTerrainTextureSelection->getTerrainSelection(); @@ -730,7 +753,7 @@ void CSVRender::TerrainTextureMode::setBrushShape(CSVWidget::BrushShape brushSha int selectionCenterY = 0; int selectionAmount = 0; - for(auto const& value: terrainSelection) + for (auto const& value : terrainSelection) { selectionCenterX += value.first; selectionCenterY += value.second; @@ -744,12 +767,12 @@ void CSVRender::TerrainTextureMode::setBrushShape(CSVWidget::BrushShape brushSha } mCustomBrushShape.clear(); - for (auto const& value: terrainSelection) + for (auto const& value : terrainSelection) mCustomBrushShape.emplace_back(value.first - selectionCenterX, value.second - selectionCenterY); } } -void CSVRender::TerrainTextureMode::setBrushTexture(std::string brushTexture) +void CSVRender::TerrainTextureMode::setBrushTexture(ESM::RefId brushTexture) { mBrushTexture = brushTexture; } diff --git a/apps/opencs/view/render/terraintexturemode.hpp b/apps/opencs/view/render/terraintexturemode.hpp index 31932df2173..6045f2e26be 100644 --- a/apps/opencs/view/render/terraintexturemode.hpp +++ b/apps/opencs/view/render/terraintexturemode.hpp @@ -3,133 +3,147 @@ #include "editmode.hpp" -#include #include +#include +#include +#include #include -#include #ifndef Q_MOC_RUN -#include "../../model/world/data.hpp" -#include "../../model/world/land.hpp" - -#include "../../model/doc/document.hpp" -#include "../../model/world/commands.hpp" -#include "../../model/world/idtable.hpp" -#include "../../model/world/landtexture.hpp" +#include "../../model/world/columnimp.hpp" #include "../widget/brushshapes.hpp" #include "brushdraw.hpp" #endif -#include "terrainselection.hpp" +#include + +class QDragMoveEvent; +class QDropEvent; +class QMouseEvent; +class QObject; +class QPoint; +class QWidget; namespace osg { class Group; } +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class IdTable; +} + namespace CSVWidget { class SceneToolTextureBrush; + class SceneToolbar; } namespace CSVRender { + class TerrainSelection; + class WorldspaceWidget; + struct WorldspaceHitResult; + class TerrainTextureMode : public EditMode { Q_OBJECT - public: - - enum InteractionType - { - InteractionType_PrimaryEdit, - InteractionType_PrimarySelect, - InteractionType_SecondaryEdit, - InteractionType_SecondarySelect, - InteractionType_None - }; + public: + enum InteractionType + { + InteractionType_PrimaryEdit, + InteractionType_PrimarySelect, + InteractionType_SecondaryEdit, + InteractionType_SecondarySelect, + InteractionType_None + }; - /// \brief Editmode for terrain texture grid - TerrainTextureMode(WorldspaceWidget*, osg::Group* parentNode, QWidget* parent = nullptr); + /// \brief Editmode for terrain texture grid + TerrainTextureMode(WorldspaceWidget*, osg::Group* parentNode, QWidget* parent = nullptr); - void primaryOpenPressed (const WorldspaceHitResult& hit) override; + void primaryOpenPressed(const WorldspaceHitResult& hit) override; - /// \brief Create single command for one-click texture editing - void primaryEditPressed (const WorldspaceHitResult& hit) override; + /// \brief Create single command for one-click texture editing + void primaryEditPressed(const WorldspaceHitResult& hit) override; - /// \brief Open brush settings window - void primarySelectPressed(const WorldspaceHitResult&) override; + /// \brief Open brush settings window + void primarySelectPressed(const WorldspaceHitResult&) override; - void secondarySelectPressed(const WorldspaceHitResult&) override; + void secondarySelectPressed(const WorldspaceHitResult&) override; - void activate(CSVWidget::SceneToolbar*) override; - void deactivate(CSVWidget::SceneToolbar*) override; + void activate(CSVWidget::SceneToolbar*) override; + void deactivate(CSVWidget::SceneToolbar*) override; - /// \brief Start texture editing command macro - bool primaryEditStartDrag (const QPoint& pos) override; + /// \brief Start texture editing command macro + bool primaryEditStartDrag(const QPoint& pos) override; - bool secondaryEditStartDrag (const QPoint& pos) override; - bool primarySelectStartDrag (const QPoint& pos) override; - bool secondarySelectStartDrag (const QPoint& pos) override; + bool secondaryEditStartDrag(const QPoint& pos) override; + bool primarySelectStartDrag(const QPoint& pos) override; + bool secondarySelectStartDrag(const QPoint& pos) override; - /// \brief Handle texture edit behavior during dragging - void drag (const QPoint& pos, int diffX, int diffY, double speedFactor) override; + /// \brief Handle texture edit behavior during dragging + void drag(const QPoint& pos, int diffX, int diffY, double speedFactor) override; - /// \brief End texture editing command macro - void dragCompleted(const QPoint& pos) override; + /// \brief End texture editing command macro + void dragCompleted(const QPoint& pos) override; - void dragAborted() override; - void dragWheel (int diff, double speedFactor) override; - void dragMoveEvent (QDragMoveEvent *event) override; + void dragAborted() override; + void dragWheel(int diff, double speedFactor) override; + void dragMoveEvent(QDragMoveEvent* event) override; - void mouseMoveEvent (QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent* event) override; - private: - /// \brief Handle brush mechanics, maths regarding worldspace hit etc. - void editTerrainTextureGrid (const WorldspaceHitResult& hit); + std::shared_ptr getTerrainSelection(); - /// \brief Check if global selection coordinate belongs to cell in view - bool isInCellSelection(int globalSelectionX, int globalSelectionY); + private: + /// \brief Handle brush mechanics, maths regarding worldspace hit etc. + void editTerrainTextureGrid(const WorldspaceHitResult& hit); - /// \brief Handle brush mechanics for texture selection - void selectTerrainTextures (const std::pair& texCoords, unsigned char selectMode, bool dragOperation); + /// \brief Check if global selection coordinate belongs to cell in view + bool isInCellSelection(int globalSelectionX, int globalSelectionY); - /// \brief Push texture edits to command macro - void pushEditToCommand (CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, - CSMWorld::IdTable& landTable, std::string cellId); + /// \brief Handle brush mechanics for texture selection + void selectTerrainTextures(const std::pair& texCoords, unsigned char selectMode); - /// \brief Create new land texture record from texture asset - void createTexture(std::string textureFileName); + /// \brief Push texture edits to command macro + void pushEditToCommand(CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, + CSMWorld::IdTable& landTable, std::string cellId); - /// \brief Create new cell and land if needed - bool allowLandTextureEditing(std::string textureFileName); + /// \brief Create new cell and land if needed + bool allowLandTextureEditing(const std::string& textureFileName); - std::string mCellId; - std::string mBrushTexture; - int mBrushSize; - CSVWidget::BrushShape mBrushShape; - std::unique_ptr mBrushDraw; - std::vector> mCustomBrushShape; - CSVWidget::SceneToolTextureBrush *mTextureBrushScenetool; - int mDragMode; - osg::Group* mParentNode; - bool mIsEditing; - std::unique_ptr mTerrainTextureSelection; + std::string mCellId; + ESM::RefId mBrushTexture; + int mBrushSize; + CSVWidget::BrushShape mBrushShape; + std::unique_ptr mBrushDraw; + std::vector> mCustomBrushShape; + CSVWidget::SceneToolTextureBrush* mTextureBrushScenetool; + int mDragMode; + osg::Group* mParentNode; + bool mIsEditing; + std::shared_ptr mTerrainTextureSelection; - const int cellSize {ESM::Land::REAL_SIZE}; - const int landTextureSize {ESM::Land::LAND_TEXTURE_SIZE}; + const int cellSize{ ESM::Land::REAL_SIZE }; + const int landTextureSize{ ESM::Land::LAND_TEXTURE_SIZE }; - signals: - void passBrushTexture(std::string brushTexture); + signals: + void passBrushTexture(ESM::RefId brushTexture); - public slots: - void handleDropEvent(QDropEvent *event); - void setBrushSize(int brushSize); - void setBrushShape(CSVWidget::BrushShape brushShape); - void setBrushTexture(std::string brushShape); + public slots: + void handleDropEvent(QDropEvent* event); + void setBrushSize(int brushSize); + void setBrushShape(CSVWidget::BrushShape brushShape); + void setBrushTexture(ESM::RefId brushShape); }; } - #endif diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index 20dc5b8d1e4..a7d8af0a621 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -2,30 +2,51 @@ #include -#include - #include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + #include "../../model/doc/document.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" -#include "../../model/world/tablemimedata.hpp" #include "../widget/scenetooltoggle2.hpp" #include "cameracontroller.hpp" -#include "mask.hpp" -#include "tagbase.hpp" + +namespace CSVRender +{ + class TagBase; +} + +namespace osg +{ + class Vec3f; +} void CSVRender::UnpagedWorldspaceWidget::update() { - const CSMWorld::Record& record = - dynamic_cast&> (mCellsModel->getRecord (mCellId)); + const CSMWorld::Record& record + = dynamic_cast&>(mCellsModel->getRecord(mCellId)); osg::Vec4f colour = SceneUtil::colourFromRGB(record.get().mAmbi.mAmbient); - setDefaultAmbient (colour); + setDefaultAmbient(colour); bool isInterior = (record.get().mData.mFlags & ESM::Cell::Interior) != 0; bool behaveLikeExterior = (record.get().mData.mFlags & ESM::Cell::QuasiEx) != 0; @@ -37,37 +58,38 @@ void CSVRender::UnpagedWorldspaceWidget::update() flagAsModified(); } -CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget (const std::string& cellId, CSMDoc::Document& document, QWidget* parent) -: WorldspaceWidget (document, parent), mDocument(document), mCellId (cellId) +CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget( + const std::string& cellId, CSMDoc::Document& document, QWidget* parent) + : WorldspaceWidget(document, parent) + , mDocument(document) + , mCellId(cellId) { - mCellsModel = &dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + mCellsModel + = &dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Cells)); - mReferenceablesModel = &dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Referenceables)); + mReferenceablesModel = &dynamic_cast( + *document.getData().getTableModel(CSMWorld::UniversalId::Type_Referenceables)); - connect (mCellsModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (cellDataChanged (const QModelIndex&, const QModelIndex&))); - connect (mCellsModel, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (cellRowsAboutToBeRemoved (const QModelIndex&, int, int))); + connect(mCellsModel, &CSMWorld::IdTable::dataChanged, this, &UnpagedWorldspaceWidget::cellDataChanged); + connect(mCellsModel, &CSMWorld::IdTable::rowsAboutToBeRemoved, this, + &UnpagedWorldspaceWidget::cellRowsAboutToBeRemoved); - connect (&document.getData(), SIGNAL (assetTablesChanged ()), - this, SLOT (assetTablesChanged ())); + connect( + &document.getData(), &CSMWorld::Data::assetTablesChanged, this, &UnpagedWorldspaceWidget::assetTablesChanged); update(); - mCell.reset (new Cell (document.getData(), mRootNode, mCellId)); + mCell = std::make_unique(document, mRootNode, mCellId); } -void CSVRender::UnpagedWorldspaceWidget::cellDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSVRender::UnpagedWorldspaceWidget::cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { - int index = mCellsModel->findColumnIndex (CSMWorld::Columns::ColumnId_Modification); - QModelIndex cellIndex = mCellsModel->getModelIndex (mCellId, index); + int index = mCellsModel->findColumnIndex(CSMWorld::Columns::ColumnId_Modification); + QModelIndex cellIndex = mCellsModel->getModelIndex(mCellId, index); - if (cellIndex.row()>=topLeft.row() && cellIndex.row()<=bottomRight.row()) + if (cellIndex.row() >= topLeft.row() && cellIndex.row() <= bottomRight.row()) { - if (mCellsModel->data (cellIndex).toInt()==CSMWorld::RecordBase::State_Deleted) + if (mCellsModel->data(cellIndex).toInt() == CSMWorld::RecordBase::State_Deleted) { emit closeRequest(); } @@ -80,12 +102,11 @@ void CSVRender::UnpagedWorldspaceWidget::cellDataChanged (const QModelIndex& top } } -void CSVRender::UnpagedWorldspaceWidget::cellRowsAboutToBeRemoved (const QModelIndex& parent, - int start, int end) +void CSVRender::UnpagedWorldspaceWidget::cellRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { - QModelIndex cellIndex = mCellsModel->getModelIndex (mCellId, 0); + QModelIndex cellIndex = mCellsModel->getModelIndex(mCellId, 0); - if (cellIndex.row()>=start && cellIndex.row()<=end) + if (cellIndex.row() >= start && cellIndex.row() <= end) emit closeRequest(); } @@ -95,17 +116,18 @@ void CSVRender::UnpagedWorldspaceWidget::assetTablesChanged() mCell->reloadAssets(); } -bool CSVRender::UnpagedWorldspaceWidget::handleDrop (const std::vector& universalIdData, DropType type) +bool CSVRender::UnpagedWorldspaceWidget::handleDrop( + const std::vector& universalIdData, DropType type) { - if (WorldspaceWidget::handleDrop (universalIdData, type)) + if (WorldspaceWidget::handleDrop(universalIdData, type)) return true; - if (type!=Type_CellsInterior) + if (type != Type_CellsInterior) return false; mCellId = universalIdData.begin()->getId(); - mCell.reset (new Cell (getDocument().getData(), mRootNode, mCellId)); + mCell = std::make_unique(getDocument(), mRootNode, mCellId); mCamPositionSet = false; mOrbitCamControl->reset(); @@ -115,41 +137,43 @@ bool CSVRender::UnpagedWorldspaceWidget::handleDrop (const std::vectorsetSelection (elementMask, Cell::Selection_Clear); + mCell->setSelection(elementMask, Cell::Selection_Clear); flagAsModified(); } -void CSVRender::UnpagedWorldspaceWidget::invertSelection (int elementMask) +void CSVRender::UnpagedWorldspaceWidget::invertSelection(int elementMask) { - mCell->setSelection (elementMask, Cell::Selection_Invert); + mCell->setSelection(elementMask, Cell::Selection_Invert); flagAsModified(); } -void CSVRender::UnpagedWorldspaceWidget::selectAll (int elementMask) +void CSVRender::UnpagedWorldspaceWidget::selectAll(int elementMask) { - mCell->setSelection (elementMask, Cell::Selection_All); + mCell->setSelection(elementMask, Cell::Selection_All); flagAsModified(); } -void CSVRender::UnpagedWorldspaceWidget::selectAllWithSameParentId (int elementMask) +void CSVRender::UnpagedWorldspaceWidget::selectAllWithSameParentId(int elementMask) { - mCell->selectAllWithSameParentId (elementMask); + mCell->selectAllWithSameParentId(elementMask); flagAsModified(); } -void CSVRender::UnpagedWorldspaceWidget::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) +void CSVRender::UnpagedWorldspaceWidget::selectInsideCube( + const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) { - mCell->selectInsideCube (pointA, pointB, dragMode); + mCell->selectInsideCube(pointA, pointB, dragMode); } -void CSVRender::UnpagedWorldspaceWidget::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) +void CSVRender::UnpagedWorldspaceWidget::selectWithinDistance( + const osg::Vec3d& point, float distance, DragMode dragMode) { - mCell->selectWithinDistance (point, distance, dragMode); + mCell->selectWithinDistance(point, distance, dragMode); } -std::string CSVRender::UnpagedWorldspaceWidget::getCellId (const osg::Vec3f& point) const +std::string CSVRender::UnpagedWorldspaceWidget::getCellId(const osg::Vec3f& point) const { return mCellId; } @@ -164,83 +188,93 @@ CSVRender::Cell* CSVRender::UnpagedWorldspaceWidget::getCell(const CSMWorld::Cel return mCell.get(); } -std::vector > CSVRender::UnpagedWorldspaceWidget::getSelection ( +osg::ref_ptr CSVRender::UnpagedWorldspaceWidget::getSnapTarget(unsigned int elementMask) const +{ + return mCell->getSnapTarget(elementMask); +} + +std::vector> CSVRender::UnpagedWorldspaceWidget::getSelection( unsigned int elementMask) const { - return mCell->getSelection (elementMask); + return mCell->getSelection(elementMask); +} + +void CSVRender::UnpagedWorldspaceWidget::selectGroup(const std::vector& group) const +{ + mCell->selectFromGroup(group); +} + +void CSVRender::UnpagedWorldspaceWidget::unhideAll() const +{ + mCell->unhideAll(); } -std::vector > CSVRender::UnpagedWorldspaceWidget::getEdited ( +std::vector> CSVRender::UnpagedWorldspaceWidget::getEdited( unsigned int elementMask) const { - return mCell->getEdited (elementMask); + return mCell->getEdited(elementMask); } -void CSVRender::UnpagedWorldspaceWidget::setSubMode (int subMode, unsigned int elementMask) +void CSVRender::UnpagedWorldspaceWidget::setSubMode(int subMode, unsigned int elementMask) { - mCell->setSubMode (subMode, elementMask); + mCell->setSubMode(subMode, elementMask); } -void CSVRender::UnpagedWorldspaceWidget::reset (unsigned int elementMask) +void CSVRender::UnpagedWorldspaceWidget::reset(unsigned int elementMask) { - mCell->reset (elementMask); + mCell->reset(elementMask); } -void CSVRender::UnpagedWorldspaceWidget::referenceableDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSVRender::UnpagedWorldspaceWidget::referenceableDataChanged( + const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mCell.get()) - if (mCell.get()->referenceableDataChanged (topLeft, bottomRight)) + if (mCell.get()->referenceableDataChanged(topLeft, bottomRight)) flagAsModified(); } -void CSVRender::UnpagedWorldspaceWidget::referenceableAboutToBeRemoved ( - const QModelIndex& parent, int start, int end) +void CSVRender::UnpagedWorldspaceWidget::referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end) { if (mCell.get()) - if (mCell.get()->referenceableAboutToBeRemoved (parent, start, end)) + if (mCell.get()->referenceableAboutToBeRemoved(parent, start, end)) flagAsModified(); } -void CSVRender::UnpagedWorldspaceWidget::referenceableAdded (const QModelIndex& parent, - int start, int end) +void CSVRender::UnpagedWorldspaceWidget::referenceableAdded(const QModelIndex& parent, int start, int end) { if (mCell.get()) { - QModelIndex topLeft = mReferenceablesModel->index (start, 0); - QModelIndex bottomRight = - mReferenceablesModel->index (end, mReferenceablesModel->columnCount()); + QModelIndex topLeft = mReferenceablesModel->index(start, 0); + QModelIndex bottomRight = mReferenceablesModel->index(end, mReferenceablesModel->columnCount()); - if (mCell.get()->referenceableDataChanged (topLeft, bottomRight)) + if (mCell.get()->referenceableDataChanged(topLeft, bottomRight)) flagAsModified(); } } -void CSVRender::UnpagedWorldspaceWidget::referenceDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSVRender::UnpagedWorldspaceWidget::referenceDataChanged( + const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mCell.get()) - if (mCell.get()->referenceDataChanged (topLeft, bottomRight)) + if (mCell.get()->referenceDataChanged(topLeft, bottomRight)) flagAsModified(); } -void CSVRender::UnpagedWorldspaceWidget::referenceAboutToBeRemoved (const QModelIndex& parent, - int start, int end) +void CSVRender::UnpagedWorldspaceWidget::referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end) { if (mCell.get()) - if (mCell.get()->referenceAboutToBeRemoved (parent, start, end)) + if (mCell.get()->referenceAboutToBeRemoved(parent, start, end)) flagAsModified(); } -void CSVRender::UnpagedWorldspaceWidget::referenceAdded (const QModelIndex& parent, int start, - int end) +void CSVRender::UnpagedWorldspaceWidget::referenceAdded(const QModelIndex& parent, int start, int end) { if (mCell.get()) - if (mCell.get()->referenceAdded (parent, start, end)) + if (mCell.get()->referenceAdded(parent, start, end)) flagAsModified(); } -void CSVRender::UnpagedWorldspaceWidget::pathgridDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +void CSVRender::UnpagedWorldspaceWidget::pathgridDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); @@ -261,7 +295,7 @@ void CSVRender::UnpagedWorldspaceWidget::pathgridDataChanged (const QModelIndex& for (int row = rowStart; row <= rowEnd; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); - if (mCellId == pathgrid.mId) + if (ESM::RefId::stringRefId(mCellId) == pathgrid.mId) { mCell->pathgridModified(); flagAsModified(); @@ -270,7 +304,7 @@ void CSVRender::UnpagedWorldspaceWidget::pathgridDataChanged (const QModelIndex& } } -void CSVRender::UnpagedWorldspaceWidget::pathgridAboutToBeRemoved (const QModelIndex& parent, int start, int end) +void CSVRender::UnpagedWorldspaceWidget::pathgridAboutToBeRemoved(const QModelIndex& parent, int start, int end) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); @@ -280,7 +314,7 @@ void CSVRender::UnpagedWorldspaceWidget::pathgridAboutToBeRemoved (const QModelI for (int row = start; row <= end; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); - if (mCellId == pathgrid.mId) + if (ESM::RefId::stringRefId(mCellId) == pathgrid.mId) { mCell->pathgridRemoved(); flagAsModified(); @@ -290,7 +324,7 @@ void CSVRender::UnpagedWorldspaceWidget::pathgridAboutToBeRemoved (const QModelI } } -void CSVRender::UnpagedWorldspaceWidget::pathgridAdded (const QModelIndex& parent, int start, int end) +void CSVRender::UnpagedWorldspaceWidget::pathgridAdded(const QModelIndex& parent, int start, int end) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); @@ -299,7 +333,7 @@ void CSVRender::UnpagedWorldspaceWidget::pathgridAdded (const QModelIndex& paren for (int row = start; row <= end; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); - if (mCellId == pathgrid.mId) + if (ESM::RefId::stringRefId(mCellId) == pathgrid.mId) { mCell->pathgridModified(); flagAsModified(); @@ -309,12 +343,10 @@ void CSVRender::UnpagedWorldspaceWidget::pathgridAdded (const QModelIndex& paren } } -void CSVRender::UnpagedWorldspaceWidget::addVisibilitySelectorButtons ( - CSVWidget::SceneToolToggle2 *tool) +void CSVRender::UnpagedWorldspaceWidget::addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool) { - WorldspaceWidget::addVisibilitySelectorButtons (tool); - tool->addButton (Button_Terrain, Mask_Terrain, "Terrain", "", true); - tool->addButton (Button_Fog, Mask_Fog, "Fog"); + WorldspaceWidget::addVisibilitySelectorButtons(tool); + tool->addButton(Button_Terrain, Mask_Terrain, "Terrain", "", true); } std::string CSVRender::UnpagedWorldspaceWidget::getStartupInstruction() @@ -325,22 +357,21 @@ std::string CSVRender::UnpagedWorldspaceWidget::getStartupInstruction() std::ostringstream stream; - stream - << "player->positionCell " - << position.x() << ", " << position.y() << ", " << position.z() - << ", 0, \"" << mCellId << "\""; + stream << "player->positionCell " << position.x() << ", " << position.y() << ", " << position.z() << ", 0, \"" + << mCellId << "\""; return stream.str(); } -CSVRender::WorldspaceWidget::dropRequirments CSVRender::UnpagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::DropType type) const +CSVRender::WorldspaceWidget::dropRequirments CSVRender::UnpagedWorldspaceWidget::getDropRequirements( + CSVRender::WorldspaceWidget::DropType type) const { - dropRequirments requirements = WorldspaceWidget::getDropRequirements (type); + dropRequirments requirements = WorldspaceWidget::getDropRequirements(type); - if (requirements!=ignored) + if (requirements != ignored) return requirements; - switch(type) + switch (type) { case Type_CellsInterior: return canHandle; diff --git a/apps/opencs/view/render/unpagedworldspacewidget.hpp b/apps/opencs/view/render/unpagedworldspacewidget.hpp index 83233c32702..89c916415d0 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.hpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.hpp @@ -1,19 +1,39 @@ #ifndef OPENCS_VIEW_UNPAGEDWORLDSPACEWIDGET_H #define OPENCS_VIEW_UNPAGEDWORLDSPACEWIDGET_H -#include #include +#include +#include + +#include +#include + +#include -#include "worldspacewidget.hpp" #include "cell.hpp" +#include "worldspacewidget.hpp" class QModelIndex; +class QObject; +class QWidget; + +namespace osg +{ + class Vec3f; + template + class ref_ptr; +} namespace CSMDoc { class Document; } +namespace CSVWidget +{ + class SceneToolToggle2; +} + namespace CSMWorld { class IdTable; @@ -22,103 +42,103 @@ namespace CSMWorld namespace CSVRender { + class TagBase; + class UnpagedWorldspaceWidget : public WorldspaceWidget { - Q_OBJECT + Q_OBJECT - CSMDoc::Document& mDocument; - std::string mCellId; - CSMWorld::IdTable *mCellsModel; - CSMWorld::IdTable *mReferenceablesModel; - std::unique_ptr mCell; + CSMDoc::Document& mDocument; + std::string mCellId; + CSMWorld::IdTable* mCellsModel; + CSMWorld::IdTable* mReferenceablesModel; + std::unique_ptr mCell; - void update(); + void update(); - public: + public: + UnpagedWorldspaceWidget(const std::string& cellId, CSMDoc::Document& document, QWidget* parent); - UnpagedWorldspaceWidget (const std::string& cellId, CSMDoc::Document& document, - QWidget *parent); + dropRequirments getDropRequirements(DropType type) const override; - dropRequirments getDropRequirements(DropType type) const override; + /// \return Drop handled? + bool handleDrop(const std::vector& data, DropType type) override; - /// \return Drop handled? - bool handleDrop (const std::vector& data, - DropType type) override; + /// \param elementMask Elements to be affected by the clear operation + void clearSelection(int elementMask) override; - /// \param elementMask Elements to be affected by the clear operation - void clearSelection (int elementMask) override; + /// \param elementMask Elements to be affected by the select operation + void invertSelection(int elementMask) override; - /// \param elementMask Elements to be affected by the select operation - void invertSelection (int elementMask) override; + /// \param elementMask Elements to be affected by the select operation + void selectAll(int elementMask) override; - /// \param elementMask Elements to be affected by the select operation - void selectAll (int elementMask) override; + // Select everything that references the same ID as at least one of the elements + // already selected + // + /// \param elementMask Elements to be affected by the select operation + void selectAllWithSameParentId(int elementMask) override; - // Select everything that references the same ID as at least one of the elements - // already selected - // - /// \param elementMask Elements to be affected by the select operation - void selectAllWithSameParentId (int elementMask) override; + void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override; - void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override; + void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override; - void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override; + std::string getCellId(const osg::Vec3f& point) const override; - std::string getCellId (const osg::Vec3f& point) const override; + Cell* getCell(const osg::Vec3d& point) const override; - Cell* getCell(const osg::Vec3d& point) const override; + Cell* getCell(const CSMWorld::CellCoordinates& coords) const override; - Cell* getCell(const CSMWorld::CellCoordinates& coords) const override; + osg::ref_ptr getSnapTarget(unsigned int elementMask) const override; - std::vector > getSelection (unsigned int elementMask) - const override; + std::vector> getSelection(unsigned int elementMask) const override; - std::vector > getEdited (unsigned int elementMask) - const override; + void selectGroup(const std::vector& group) const override; - void setSubMode (int subMode, unsigned int elementMask) override; + void unhideAll() const override; - /// Erase all overrides and restore the visual representation to its true state. - void reset (unsigned int elementMask) override; + std::vector> getEdited(unsigned int elementMask) const override; - private: + void setSubMode(int subMode, unsigned int elementMask) override; - void referenceableDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) override; + /// Erase all overrides and restore the visual representation to its true state. + void reset(unsigned int elementMask) override; - void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; + private: + void referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) override; - void referenceableAdded (const QModelIndex& index, int start, int end) override; + void referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; - void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) override; + void referenceableAdded(const QModelIndex& index, int start, int end) override; - void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; + void referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) override; - void referenceAdded (const QModelIndex& index, int start, int end) override; + void referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; - void pathgridDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) override; + void referenceAdded(const QModelIndex& index, int start, int end) override; - void pathgridAboutToBeRemoved (const QModelIndex& parent, int start, int end) override; + void pathgridDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) override; - void pathgridAdded (const QModelIndex& parent, int start, int end) override; + void pathgridAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; - std::string getStartupInstruction() override; + void pathgridAdded(const QModelIndex& parent, int start, int end) override; - protected: + std::string getStartupInstruction() override; - void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool) override; + protected: + void addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool) override; - private slots: + private slots: - void cellDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - void cellRowsAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void cellRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); - void assetTablesChanged (); + void assetTablesChanged(); - signals: + signals: - void cellChanged(const CSMWorld::UniversalId& id); + void cellChanged(const CSMWorld::UniversalId& id); }; } diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 723ffcb6a2f..0a02ae456b8 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -1,20 +1,44 @@ #include "worldspacewidget.hpp" #include +#include -#include #include #include #include +#include #include -#include -#include +#include #include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include #include -#include "../../model/world/universalid.hpp" #include "../../model/world/idtable.hpp" +#include "../../model/world/tablemimedata.hpp" +#include "../../model/world/universalid.hpp" #include "../../model/prefs/shortcut.hpp" #include "../../model/prefs/state.hpp" @@ -22,118 +46,123 @@ #include "../render/orbitcameramode.hpp" #include "../widget/scenetoolmode.hpp" -#include "../widget/scenetooltoggle2.hpp" #include "../widget/scenetoolrun.hpp" +#include "../widget/scenetooltoggle2.hpp" -#include "object.hpp" -#include "mask.hpp" +#include "cameracontroller.hpp" #include "instancemode.hpp" +#include "mask.hpp" +#include "object.hpp" #include "pathgridmode.hpp" -#include "cameracontroller.hpp" -CSVRender::WorldspaceWidget::WorldspaceWidget (CSMDoc::Document& document, QWidget* parent) - : SceneWidget (document.getData().getResourceSystem(), parent, Qt::WindowFlags(), false) +CSVRender::WorldspaceWidget::WorldspaceWidget(CSMDoc::Document& document, QWidget* parent) + : SceneWidget(document.getData().getResourceSystem(), parent, Qt::WindowFlags(), false) , mSceneElements(nullptr) , mRun(nullptr) , mDocument(document) - , mInteractionMask (0) - , mEditMode (nullptr) - , mLocked (false) + , mInteractionMask(0) + , mEditMode(nullptr) + , mLocked(false) , mDragMode(InteractionType_None) - , mDragging (false) + , mDragging(false) , mDragX(0) , mDragY(0) , mSpeedMode(false) , mDragFactor(0) , mDragWheelFactor(0) , mDragShiftFactor(0) - , mToolTipPos (-1, -1) + , mToolTipPos(-1, -1) , mShowToolTips(false) , mToolTipDelay(0) , mInConstructor(true) + , mSelectedNavigationMode(0) { setAcceptDrops(true); - QAbstractItemModel *referenceables = - document.getData().getTableModel (CSMWorld::UniversalId::Type_Referenceables); + QAbstractItemModel* referenceables = document.getData().getTableModel(CSMWorld::UniversalId::Type_Referenceables); - connect (referenceables, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (referenceableDataChanged (const QModelIndex&, const QModelIndex&))); - connect (referenceables, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (referenceableAboutToBeRemoved (const QModelIndex&, int, int))); - connect (referenceables, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (referenceableAdded (const QModelIndex&, int, int))); + connect(referenceables, &QAbstractItemModel::dataChanged, this, &WorldspaceWidget::referenceableDataChanged); + connect(referenceables, &QAbstractItemModel::rowsAboutToBeRemoved, this, + &WorldspaceWidget::referenceableAboutToBeRemoved); + connect(referenceables, &QAbstractItemModel::rowsInserted, this, &WorldspaceWidget::referenceableAdded); - QAbstractItemModel *references = - document.getData().getTableModel (CSMWorld::UniversalId::Type_References); + QAbstractItemModel* references = document.getData().getTableModel(CSMWorld::UniversalId::Type_References); - connect (references, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (referenceDataChanged (const QModelIndex&, const QModelIndex&))); - connect (references, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (referenceAboutToBeRemoved (const QModelIndex&, int, int))); - connect (references, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (referenceAdded (const QModelIndex&, int, int))); + connect(references, &QAbstractItemModel::dataChanged, this, &WorldspaceWidget::referenceDataChanged); + connect(references, &QAbstractItemModel::rowsAboutToBeRemoved, this, &WorldspaceWidget::referenceAboutToBeRemoved); + connect(references, &QAbstractItemModel::rowsInserted, this, &WorldspaceWidget::referenceAdded); - QAbstractItemModel *pathgrids = document.getData().getTableModel (CSMWorld::UniversalId::Type_Pathgrids); + QAbstractItemModel* pathgrids = document.getData().getTableModel(CSMWorld::UniversalId::Type_Pathgrids); - connect (pathgrids, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (pathgridDataChanged (const QModelIndex&, const QModelIndex&))); - connect (pathgrids, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (pathgridAboutToBeRemoved (const QModelIndex&, int, int))); - connect (pathgrids, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (pathgridAdded (const QModelIndex&, int, int))); + connect(pathgrids, &QAbstractItemModel::dataChanged, this, &WorldspaceWidget::pathgridDataChanged); + connect(pathgrids, &QAbstractItemModel::rowsAboutToBeRemoved, this, &WorldspaceWidget::pathgridAboutToBeRemoved); + connect(pathgrids, &QAbstractItemModel::rowsInserted, this, &WorldspaceWidget::pathgridAdded); - QAbstractItemModel *debugProfiles = - document.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles); + QAbstractItemModel* debugProfiles = document.getData().getTableModel(CSMWorld::UniversalId::Type_DebugProfiles); - connect (debugProfiles, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (debugProfileDataChanged (const QModelIndex&, const QModelIndex&))); - connect (debugProfiles, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (debugProfileAboutToBeRemoved (const QModelIndex&, int, int))); + connect(debugProfiles, &QAbstractItemModel::dataChanged, this, &WorldspaceWidget::debugProfileDataChanged); + connect(debugProfiles, &QAbstractItemModel::rowsAboutToBeRemoved, this, + &WorldspaceWidget::debugProfileAboutToBeRemoved); - mToolTipDelayTimer.setSingleShot (true); - connect (&mToolTipDelayTimer, SIGNAL (timeout()), this, SLOT (showToolTip())); + mToolTipDelayTimer.setSingleShot(true); + connect(&mToolTipDelayTimer, &QTimer::timeout, this, &WorldspaceWidget::showToolTip); CSMPrefs::get()["3D Scene Input"].update(); CSMPrefs::get()["Tooltips"].update(); // Shortcuts - CSMPrefs::Shortcut* primaryEditShortcut = new CSMPrefs::Shortcut("scene-edit-primary", "scene-speed-modifier", - CSMPrefs::Shortcut::SM_Detach, this); + CSMPrefs::Shortcut* primaryEditShortcut + = new CSMPrefs::Shortcut("scene-edit-primary", "scene-speed-modifier", CSMPrefs::Shortcut::SM_Detach, this); CSMPrefs::Shortcut* primaryOpenShortcut = new CSMPrefs::Shortcut("scene-open-primary", this); - connect(primaryOpenShortcut, SIGNAL(activated(bool)), this, SLOT(primaryOpen(bool))); - connect(primaryEditShortcut, SIGNAL(activated(bool)), this, SLOT(primaryEdit(bool))); - connect(primaryEditShortcut, SIGNAL(secondary(bool)), this, SLOT(speedMode(bool))); + connect(primaryOpenShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::primaryOpen); + connect(primaryEditShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::primaryEdit); + connect(primaryEditShortcut, qOverload(&CSMPrefs::Shortcut::secondary), this, &WorldspaceWidget::speedMode); CSMPrefs::Shortcut* secondaryEditShortcut = new CSMPrefs::Shortcut("scene-edit-secondary", this); - connect(secondaryEditShortcut, SIGNAL(activated(bool)), this, SLOT(secondaryEdit(bool))); + connect( + secondaryEditShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::secondaryEdit); CSMPrefs::Shortcut* primarySelectShortcut = new CSMPrefs::Shortcut("scene-select-primary", this); - connect(primarySelectShortcut, SIGNAL(activated(bool)), this, SLOT(primarySelect(bool))); + connect( + primarySelectShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::primarySelect); CSMPrefs::Shortcut* secondarySelectShortcut = new CSMPrefs::Shortcut("scene-select-secondary", this); - connect(secondarySelectShortcut, SIGNAL(activated(bool)), this, SLOT(secondarySelect(bool))); + connect(secondarySelectShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, + &WorldspaceWidget::secondarySelect); + + CSMPrefs::Shortcut* tertiarySelectShortcut = new CSMPrefs::Shortcut("scene-select-tertiary", this); + connect(tertiarySelectShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, + &WorldspaceWidget::tertiarySelect); CSMPrefs::Shortcut* abortShortcut = new CSMPrefs::Shortcut("scene-edit-abort", this); - connect(abortShortcut, SIGNAL(activated()), this, SLOT(abortDrag())); + connect(abortShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::abortDrag); - mInConstructor = false; -} + connect(new CSMPrefs::Shortcut("scene-toggle-visibility", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, + &WorldspaceWidget::toggleHiddenInstances); -CSVRender::WorldspaceWidget::~WorldspaceWidget () -{ + connect(new CSMPrefs::Shortcut("scene-unhide-all", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, + &WorldspaceWidget::unhideAll); + + connect(new CSMPrefs::Shortcut("scene-clear-selection", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, + [this] { 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; } -void CSVRender::WorldspaceWidget::settingChanged (const CSMPrefs::Setting *setting) +void CSVRender::WorldspaceWidget::settingChanged(const CSMPrefs::Setting* setting) { - if (*setting=="3D Scene Input/drag-factor") + if (*setting == "3D Scene Input/drag-factor") mDragFactor = setting->toDouble(); - else if (*setting=="3D Scene Input/drag-wheel-factor") + else if (*setting == "3D Scene Input/drag-wheel-factor") mDragWheelFactor = setting->toDouble(); - else if (*setting=="3D Scene Input/drag-shift-factor") + else if (*setting == "3D Scene Input/drag-shift-factor") mDragShiftFactor = setting->toDouble(); - else if (*setting=="Rendering/object-marker-alpha" && !mInConstructor) + else if (*setting == "Rendering/object-marker-alpha" && !mInConstructor) { float alpha = setting->toDouble(); // getSelection is virtual, thus this can not be called from the constructor @@ -144,16 +173,15 @@ void CSVRender::WorldspaceWidget::settingChanged (const CSMPrefs::Setting *setti objTag->mObject->setMarkerTransparency(alpha); } } - else if (*setting=="Tooltips/scene-delay") + else if (*setting == "Tooltips/scene-delay") mToolTipDelay = setting->toInt(); - else if (*setting=="Tooltips/scene") + else if (*setting == "Tooltips/scene") mShowToolTips = setting->isTrue(); else SceneWidget::settingChanged(setting); } - -void CSVRender::WorldspaceWidget::useViewHint (const std::string& hint) {} +void CSVRender::WorldspaceWidget::useViewHint(const std::string& hint) {} void CSVRender::WorldspaceWidget::selectDefaultNavigationMode() { @@ -162,25 +190,24 @@ void CSVRender::WorldspaceWidget::selectDefaultNavigationMode() void CSVRender::WorldspaceWidget::centerOrbitCameraOnSelection() { - std::vector > selection = getSelection(~0u); + std::vector> selection = getSelection(Mask_Reference); - for (std::vector >::iterator it = selection.begin(); it!=selection.end(); ++it) + for (std::vector>::iterator it = selection.begin(); it != selection.end(); ++it) { - if (CSVRender::ObjectTag *objectTag = dynamic_cast (it->get())) + if (CSVRender::ObjectTag* objectTag = static_cast(it->get())) { mOrbitCamControl->setCenter(objectTag->mObject->getPosition().asVec3()); } } } -CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( - CSVWidget::SceneToolbar *parent) +CSVWidget::SceneToolMode* CSVRender::WorldspaceWidget::makeNavigationSelector(CSVWidget::SceneToolbar* parent) { - CSVWidget::SceneToolMode *tool = new CSVWidget::SceneToolMode (parent, "Camera Mode"); + CSVWidget::SceneToolMode* tool = new CSVWidget::SceneToolMode(parent, "Camera Mode"); /// \todo replace icons /// \todo consider user-defined button-mapping - tool->addButton (":scenetoolbar/1st-person", "1st", + tool->addButton(":scenetoolbar/1st-person", "1st", "First Person" "
        • Camera is held upright
        • " "
        • Mouse-Look while holding {scene-navi-primary}
        • " @@ -189,7 +216,7 @@ CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( "
        • Mouse wheel moves the camera forward/backward
        • " "
        • Hold {scene-speed-modifier} to speed up movement
        • " "
        "); - tool->addButton (":scenetoolbar/free-camera", "free", + tool->addButton(":scenetoolbar/free-camera", "free", "Free Camera" "
        • Mouse-Look while holding {scene-navi-primary}
        • " "
        • Movement keys: {free-forward}(forward), {free-left}(left), {free-backward}(back), {free-right}(right)
        • " @@ -199,135 +226,129 @@ CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( "
        • Hold {free-forward:mod} to speed up movement
        • " "
        "); tool->addButton( - new CSVRender::OrbitCameraMode(this, QIcon(":scenetoolbar/orbiting-camera"), + new CSVRender::OrbitCameraMode(this, Misc::ScalableIcon::load(":scenetoolbar/orbiting-camera"), "Orbiting Camera" "
        • Always facing the centre point
        • " "
        • Rotate around the centre point via {orbit-up}, {orbit-left}, {orbit-down}, {orbit-right} or by moving " - "the mouse while holding {scene-navi-primary}
        • " + "the mouse while holding {scene-navi-primary}" "
        • Roll camera with {orbit-roll-left} and {orbit-roll-right} keys
        • " - "
        • Strafing (also vertically) by holding {scene-navi-secondary} (includes relocation of the centre point)
        • " + "
        • Strafing (also vertically) by holding {scene-navi-secondary} (includes relocation of the centre " + "point)
        • " "
        • Mouse wheel moves camera away or towards centre point but can not pass through it
        • " "
        • Hold {scene-speed-modifier} to speed up movement
        • " - "
        ", tool), + "
      ", + tool), "orbit"); - connect (tool, SIGNAL (modeChanged (const std::string&)), - this, SLOT (selectNavigationMode (const std::string&))); + mCameraMode = tool; + + connect(mCameraMode, &CSVWidget::SceneToolMode::modeChanged, this, &WorldspaceWidget::selectNavigationMode); - return tool; + return mCameraMode; } -CSVWidget::SceneToolToggle2 *CSVRender::WorldspaceWidget::makeSceneVisibilitySelector (CSVWidget::SceneToolbar *parent) +CSVWidget::SceneToolToggle2* CSVRender::WorldspaceWidget::makeSceneVisibilitySelector(CSVWidget::SceneToolbar* parent) { - mSceneElements = new CSVWidget::SceneToolToggle2 (parent, - "Scene Element Visibility", ":scenetoolbar/scene-view-c", ":scenetoolbar/scene-view-"); + mSceneElements = new CSVWidget::SceneToolToggle2( + parent, "Scene Element Visibility", ":scenetoolbar/scene-view-c", ":scenetoolbar/scene-view-"); - addVisibilitySelectorButtons (mSceneElements); + addVisibilitySelectorButtons(mSceneElements); - mSceneElements->setSelectionMask (0xffffffff); + mSceneElements->setSelectionMask(0xffffffff); - connect (mSceneElements, SIGNAL (selectionChanged()), - this, SLOT (elementSelectionChanged())); + connect(mSceneElements, &CSVWidget::SceneToolToggle2::selectionChanged, this, + &WorldspaceWidget::elementSelectionChanged); return mSceneElements; } -CSVWidget::SceneToolRun *CSVRender::WorldspaceWidget::makeRunTool ( - CSVWidget::SceneToolbar *parent) +CSVWidget::SceneToolRun* CSVRender::WorldspaceWidget::makeRunTool(CSVWidget::SceneToolbar* parent) { - CSMWorld::IdTable& debugProfiles = dynamic_cast ( - *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); + CSMWorld::IdTable& debugProfiles = dynamic_cast( + *mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_DebugProfiles)); std::vector profiles; - int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); - int stateColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); - int defaultColumn = debugProfiles.findColumnIndex ( - CSMWorld::Columns::ColumnId_DefaultProfile); + int idColumn = debugProfiles.findColumnIndex(CSMWorld::Columns::ColumnId_Id); + int stateColumn = debugProfiles.findColumnIndex(CSMWorld::Columns::ColumnId_Modification); + int defaultColumn = debugProfiles.findColumnIndex(CSMWorld::Columns::ColumnId_DefaultProfile); int size = debugProfiles.rowCount(); - for (int i=0; i& data) +CSVRender::WorldspaceWidget::DropType CSVRender::WorldspaceWidget::getDropType( + const std::vector& data) { DropType output = Type_Other; - for (std::vector::const_iterator iter (data.begin()); - iter!=data.end(); ++iter) + for (std::vector::const_iterator iter(data.begin()); iter != data.end(); ++iter) { DropType type = Type_Other; - if (iter->getType()==CSMWorld::UniversalId::Type_Cell || - iter->getType()==CSMWorld::UniversalId::Type_Cell_Missing) + if (iter->getType() == CSMWorld::UniversalId::Type_Cell + || iter->getType() == CSMWorld::UniversalId::Type_Cell_Missing) { - type = iter->getId().substr (0, 1)=="#" ? Type_CellsExterior : Type_CellsInterior; + type = iter->getId()[0] == '#' ? Type_CellsExterior : Type_CellsInterior; } - else if (iter->getType()==CSMWorld::UniversalId::Type_DebugProfile) + else if (iter->getType() == CSMWorld::UniversalId::Type_DebugProfile) type = Type_DebugProfile; - if (iter==data.begin()) + if (iter == data.begin()) output = type; - else if (output!=type) // mixed types -> ignore + else if (output != type) // mixed types -> ignore return Type_Other; } return output; } -CSVRender::WorldspaceWidget::dropRequirments - CSVRender::WorldspaceWidget::getDropRequirements (DropType type) const +CSVRender::WorldspaceWidget::dropRequirments CSVRender::WorldspaceWidget::getDropRequirements(DropType type) const { - if (type==Type_DebugProfile) + if (type == Type_DebugProfile) return canHandle; return ignored; } -bool CSVRender::WorldspaceWidget::handleDrop (const std::vector& universalIdData, - DropType type) +bool CSVRender::WorldspaceWidget::handleDrop(const std::vector& universalIdData, DropType type) { - if (type==Type_DebugProfile) + if (type == Type_DebugProfile) { if (mRun) { - for (std::vector::const_iterator iter (universalIdData.begin()); - iter!=universalIdData.end(); ++iter) - mRun->addProfile (iter->getId()); + for (std::vector::const_iterator iter(universalIdData.begin()); + iter != universalIdData.end(); ++iter) + mRun->addProfile(iter->getId()); } return true; @@ -341,7 +362,7 @@ unsigned int CSVRender::WorldspaceWidget::getVisibilityMask() const return mSceneElements->getSelectionMask(); } -void CSVRender::WorldspaceWidget::setInteractionMask (unsigned int mask) +void CSVRender::WorldspaceWidget::setInteractionMask(unsigned int mask) { mInteractionMask = mask | Mask_CellMarker | Mask_CellArrow; } @@ -351,24 +372,23 @@ unsigned int CSVRender::WorldspaceWidget::getInteractionMask() const return mInteractionMask & getVisibilityMask(); } -void CSVRender::WorldspaceWidget::setEditLock (bool locked) +void CSVRender::WorldspaceWidget::setEditLock(bool locked) { - dynamic_cast (*mEditMode->getCurrent()).setEditLock (locked); + dynamic_cast(*mEditMode->getCurrent()).setEditLock(locked); } -void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons ( - CSVWidget::SceneToolToggle2 *tool) +void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool) { - tool->addButton (Button_Reference, Mask_Reference, "Instances"); - tool->addButton (Button_Water, Mask_Water, "Water"); - tool->addButton (Button_Pathgrid, Mask_Pathgrid, "Pathgrid"); + tool->addButton(Button_Reference, Mask_Reference, "Instances"); + tool->addButton(Button_Water, Mask_Water, "Water"); + tool->addButton(Button_Pathgrid, Mask_Pathgrid, "Pathgrid"); } -void CSVRender::WorldspaceWidget::addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) +void CSVRender::WorldspaceWidget::addEditModeSelectorButtons(CSVWidget::SceneToolMode* tool) { /// \todo replace EditMode with suitable subclasses - tool->addButton (new InstanceMode (this, mRootNode, tool), "object"); - tool->addButton (new PathgridMode (this, tool), "pathgrid"); + tool->addButton(new InstanceMode(this, mRootNode, tool), "object"); + tool->addButton(new PathgridMode(this, tool), "pathgrid"); } CSMDoc::Document& CSVRender::WorldspaceWidget::getDocument() @@ -376,27 +396,32 @@ CSMDoc::Document& CSVRender::WorldspaceWidget::getDocument() return mDocument; } -CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick (const QPoint& localPos, - unsigned int interactionMask) const +CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick( + const QPoint& localPos, unsigned int interactionMask) const { + // may be okay to just use devicePixelRatio() directly + QScreen* screen = SceneWidget::windowHandle() && SceneWidget::windowHandle()->screen() + ? SceneWidget::windowHandle()->screen() + : QGuiApplication::primaryScreen(); + // (0,0) is considered the lower left corner of an OpenGL window - int x = localPos.x(); - int y = height() - localPos.y(); + int x = localPos.x() * screen->devicePixelRatio(); + int y = height() * screen->devicePixelRatio() - localPos.y() * screen->devicePixelRatio(); // Convert from screen space to world space osg::Matrixd wpvMat; - wpvMat.preMult (mView->getCamera()->getViewport()->computeWindowMatrix()); - wpvMat.preMult (mView->getCamera()->getProjectionMatrix()); - wpvMat.preMult (mView->getCamera()->getViewMatrix()); - wpvMat = osg::Matrixd::inverse (wpvMat); + wpvMat.preMult(mView->getCamera()->getViewport()->computeWindowMatrix()); + wpvMat.preMult(mView->getCamera()->getProjectionMatrix()); + wpvMat.preMult(mView->getCamera()->getViewMatrix()); + wpvMat = osg::Matrixd::inverse(wpvMat); - osg::Vec3d start = wpvMat.preMult (osg::Vec3d(x, y, 0)); - osg::Vec3d end = wpvMat.preMult (osg::Vec3d(x, y, 1)); + 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; // Get intersection - osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector( - osgUtil::Intersector::MODEL, start, end)); + osg::ref_ptr intersector( + new osgUtil::LineSegmentIntersector(osgUtil::Intersector::MODEL, start, end)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); osgUtil::IntersectionVisitor visitor(intersector); @@ -417,12 +442,13 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick (const QPo continue; } - for (std::vector::iterator nodeIter = intersection.nodePath.begin(); nodeIter != intersection.nodePath.end(); ++nodeIter) + 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())) + if (osg::ref_ptr tag = dynamic_cast(node->getUserData())) { - WorldspaceHitResult hit = { true, tag, 0, 0, 0, intersection.getWorldIntersectPoint() }; + WorldspaceHitResult hit = { true, std::move(tag), 0, 0, 0, intersection.getWorldIntersectPoint() }; if (intersection.indexList.size() >= 3) { hit.index0 = intersection.indexList[0]; @@ -452,110 +478,111 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick (const QPo return hit; } +CSVRender::EditMode* CSVRender::WorldspaceWidget::getEditMode() +{ + return dynamic_cast(mEditMode->getCurrent()); +} + void CSVRender::WorldspaceWidget::abortDrag() { if (mDragging) { - EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); + EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); editMode.dragAborted(); - mDragging = false; mDragMode = InteractionType_None; } } -void CSVRender::WorldspaceWidget::dragEnterEvent (QDragEnterEvent* event) +void CSVRender::WorldspaceWidget::dragEnterEvent(QDragEnterEvent* event) { - const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped return; - if (mime->fromDocument (mDocument)) + if (mime->fromDocument(mDocument)) { - if (mime->holdsType (CSMWorld::UniversalId::Type_Cell) || - mime->holdsType (CSMWorld::UniversalId::Type_Cell_Missing) || - mime->holdsType (CSMWorld::UniversalId::Type_DebugProfile)) + if (mime->holdsType(CSMWorld::UniversalId::Type_Cell) + || mime->holdsType(CSMWorld::UniversalId::Type_Cell_Missing) + || mime->holdsType(CSMWorld::UniversalId::Type_DebugProfile)) { // These drops are handled through the subview object. event->accept(); } else - dynamic_cast (*mEditMode->getCurrent()).dragEnterEvent (event); + dynamic_cast(*mEditMode->getCurrent()).dragEnterEvent(event); } } -void CSVRender::WorldspaceWidget::dragMoveEvent(QDragMoveEvent *event) +void CSVRender::WorldspaceWidget::dragMoveEvent(QDragMoveEvent* event) { - const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped return; - if (mime->fromDocument (mDocument)) + if (mime->fromDocument(mDocument)) { - if (mime->holdsType (CSMWorld::UniversalId::Type_Cell) || - mime->holdsType (CSMWorld::UniversalId::Type_Cell_Missing) || - mime->holdsType (CSMWorld::UniversalId::Type_DebugProfile)) + if (mime->holdsType(CSMWorld::UniversalId::Type_Cell) + || mime->holdsType(CSMWorld::UniversalId::Type_Cell_Missing) + || mime->holdsType(CSMWorld::UniversalId::Type_DebugProfile)) { // These drops are handled through the subview object. event->accept(); } else - dynamic_cast (*mEditMode->getCurrent()).dragMoveEvent (event); + dynamic_cast(*mEditMode->getCurrent()).dragMoveEvent(event); } } -void CSVRender::WorldspaceWidget::dropEvent (QDropEvent* event) +void CSVRender::WorldspaceWidget::dropEvent(QDropEvent* event) { - const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped return; - if (mime->fromDocument (mDocument)) + if (mime->fromDocument(mDocument)) { - if (mime->holdsType (CSMWorld::UniversalId::Type_Cell) || - mime->holdsType (CSMWorld::UniversalId::Type_Cell_Missing) || - mime->holdsType (CSMWorld::UniversalId::Type_DebugProfile)) + if (mime->holdsType(CSMWorld::UniversalId::Type_Cell) + || mime->holdsType(CSMWorld::UniversalId::Type_Cell_Missing) + || mime->holdsType(CSMWorld::UniversalId::Type_DebugProfile)) { emit dataDropped(mime->getData()); } else - dynamic_cast (*mEditMode->getCurrent()).dropEvent (event); + dynamic_cast(*mEditMode->getCurrent()).dropEvent(event); } } -void CSVRender::WorldspaceWidget::runRequest (const std::string& profile) +void CSVRender::WorldspaceWidget::runRequest(const std::string& profile) { - mDocument.startRunning (profile, getStartupInstruction()); + mDocument.startRunning(profile, getStartupInstruction()); } -void CSVRender::WorldspaceWidget::debugProfileDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) +void CSVRender::WorldspaceWidget::debugProfileDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (!mRun) return; - CSMWorld::IdTable& debugProfiles = dynamic_cast ( - *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); + CSMWorld::IdTable& debugProfiles = dynamic_cast( + *mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_DebugProfiles)); - int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); - int stateColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); + int idColumn = debugProfiles.findColumnIndex(CSMWorld::Columns::ColumnId_Id); + int stateColumn = debugProfiles.findColumnIndex(CSMWorld::Columns::ColumnId_Modification); - for (int i=topLeft.row(); i<=bottomRight.row(); ++i) + for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { - int state = debugProfiles.data (debugProfiles.index (i, stateColumn)).toInt(); + int state = debugProfiles.data(debugProfiles.index(i, stateColumn)).toInt(); // As of version 0.33 this case can not happen because debug profiles exist only in // project or session scope, which means they will never be in deleted state. But we // are adding the code for the sake of completeness and to avoid surprises if debug // profile ever get extended to content scope. - if (state==CSMWorld::RecordBase::State_Deleted) - mRun->removeProfile (debugProfiles.data ( - debugProfiles.index (i, idColumn)).toString().toUtf8().constData()); + if (state == CSMWorld::RecordBase::State_Deleted) + mRun->removeProfile(debugProfiles.data(debugProfiles.index(i, idColumn)).toString().toUtf8().constData()); } } -void CSVRender::WorldspaceWidget::debugProfileAboutToBeRemoved (const QModelIndex& parent, - int start, int end) +void CSVRender::WorldspaceWidget::debugProfileAboutToBeRemoved(const QModelIndex& parent, int start, int end) { if (parent.isValid()) return; @@ -563,21 +590,20 @@ void CSVRender::WorldspaceWidget::debugProfileAboutToBeRemoved (const QModelInde if (!mRun) return; - CSMWorld::IdTable& debugProfiles = dynamic_cast ( - *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); + CSMWorld::IdTable& debugProfiles = dynamic_cast( + *mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_DebugProfiles)); - int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int idColumn = debugProfiles.findColumnIndex(CSMWorld::Columns::ColumnId_Id); - for (int i=start; i<=end; ++i) + for (int i = start; i <= end; ++i) { - mRun->removeProfile (debugProfiles.data ( - debugProfiles.index (i, idColumn)).toString().toUtf8().constData()); + mRun->removeProfile(debugProfiles.data(debugProfiles.index(i, idColumn)).toString().toUtf8().constData()); } } -void CSVRender::WorldspaceWidget::editModeChanged (const std::string& id) +void CSVRender::WorldspaceWidget::editModeChanged(const std::string& id) { - dynamic_cast (*mEditMode->getCurrent()).setEditLock (mLocked); + dynamic_cast(*mEditMode->getCurrent()).setEditLock(mLocked); mDragging = false; mDragMode = InteractionType_None; } @@ -588,29 +614,27 @@ void CSVRender::WorldspaceWidget::showToolTip() { QPoint pos = QCursor::pos(); - WorldspaceHitResult hit = mousePick (mapFromGlobal (pos), getInteractionMask()); + WorldspaceHitResult hit = mousePick(mapFromGlobal(pos), getInteractionMask()); if (hit.tag) { bool hideBasics = CSMPrefs::get()["Tooltips"]["scene-hide-basic"].isTrue(); - QToolTip::showText (pos, hit.tag->getToolTip (hideBasics), this); + QToolTip::showText(pos, hit.tag->getToolTip(hideBasics, hit), this); } } } void CSVRender::WorldspaceWidget::elementSelectionChanged() { - setVisibilityMask (getVisibilityMask()); + setVisibilityMask(getVisibilityMask()); flagAsModified(); updateOverlay(); } -void CSVRender::WorldspaceWidget::updateOverlay() -{ -} +void CSVRender::WorldspaceWidget::updateOverlay() {} -void CSVRender::WorldspaceWidget::mouseMoveEvent (QMouseEvent *event) +void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event) { - dynamic_cast (*mEditMode->getCurrent()).mouseMoveEvent (event); + dynamic_cast(*mEditMode->getCurrent()).mouseMoveEvent(event); if (mDragging) { @@ -625,22 +649,22 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent (QMouseEvent *event) if (mSpeedMode) factor *= mDragShiftFactor; - EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); + EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); - editMode.drag (event->pos(), diffX, diffY, factor); + editMode.drag(event->pos(), diffX, diffY, factor); } else if (mDragMode != InteractionType_None) { - EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); + EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); if (mDragMode == InteractionType_PrimaryEdit) - mDragging = editMode.primaryEditStartDrag (event->pos()); + mDragging = editMode.primaryEditStartDrag(event->pos()); else if (mDragMode == InteractionType_SecondaryEdit) - mDragging = editMode.secondaryEditStartDrag (event->pos()); + mDragging = editMode.secondaryEditStartDrag(event->pos()); else if (mDragMode == InteractionType_PrimarySelect) - mDragging = editMode.primarySelectStartDrag (event->pos()); + mDragging = editMode.primarySelectStartDrag(event->pos()); else if (mDragMode == InteractionType_SecondarySelect) - mDragging = editMode.secondarySelectStartDrag (event->pos()); + mDragging = editMode.secondarySelectStartDrag(event->pos()); if (mDragging) { @@ -650,14 +674,14 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent (QMouseEvent *event) } else { - if (event->globalPos()!=mToolTipPos) + if (event->globalPos() != mToolTipPos) { mToolTipPos = event->globalPos(); if (mShowToolTips) { QToolTip::hideText(); - mToolTipDelayTimer.start (mToolTipDelay); + mToolTipDelayTimer.start(mToolTipDelay); } } @@ -665,7 +689,7 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent (QMouseEvent *event) } } -void CSVRender::WorldspaceWidget::wheelEvent (QWheelEvent *event) +void CSVRender::WorldspaceWidget::wheelEvent(QWheelEvent* event) { if (mDragging) { @@ -674,32 +698,29 @@ void CSVRender::WorldspaceWidget::wheelEvent (QWheelEvent *event) if (mSpeedMode) factor *= mDragShiftFactor; - EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); - editMode.dragWheel (event->angleDelta().y(), factor); + EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); + editMode.dragWheel(event->angleDelta().y(), factor); } else SceneWidget::wheelEvent(event); } -void CSVRender::WorldspaceWidget::handleInteractionPress (const WorldspaceHitResult& hit, InteractionType type) +void CSVRender::WorldspaceWidget::handleInteractionPress(const WorldspaceHitResult& hit, InteractionType type) { - EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); + EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); if (type == InteractionType_PrimaryEdit) - editMode.primaryEditPressed (hit); + editMode.primaryEditPressed(hit); else if (type == InteractionType_SecondaryEdit) - editMode.secondaryEditPressed (hit); + editMode.secondaryEditPressed(hit); else if (type == InteractionType_PrimarySelect) - editMode.primarySelectPressed (hit); + editMode.primarySelectPressed(hit); else if (type == InteractionType_SecondarySelect) - editMode.secondarySelectPressed (hit); + editMode.secondarySelectPressed(hit); + else if (type == InteractionType_TertiarySelect) + editMode.tertiarySelectPressed(hit); else if (type == InteractionType_PrimaryOpen) - editMode.primaryOpenPressed (hit); -} - -CSVRender::EditMode *CSVRender::WorldspaceWidget::getEditMode() -{ - return dynamic_cast (mEditMode->getCurrent()); + editMode.primaryOpenPressed(hit); } void CSVRender::WorldspaceWidget::primaryOpen(bool activate) @@ -727,11 +748,54 @@ void CSVRender::WorldspaceWidget::secondarySelect(bool activate) handleInteraction(InteractionType_SecondarySelect, activate); } +void CSVRender::WorldspaceWidget::tertiarySelect(bool activate) +{ + handleInteraction(InteractionType_TertiarySelect, activate); +} + void CSVRender::WorldspaceWidget::speedMode(bool activate) { mSpeedMode = activate; } +void CSVRender::WorldspaceWidget::toggleHiddenInstances() +{ + const std::vector> selection = getSelection(Mask_Reference); + + if (selection.empty()) + return; + + const CSVRender::ObjectTag* firstSelection = static_cast(selection.begin()->get()); + assert(firstSelection != nullptr); + + const CSVRender::Mask firstMask + = firstSelection->mObject->getRootNode()->getNodeMask() == Mask_Hidden ? Mask_Reference : Mask_Hidden; + + for (const auto& object : selection) + if (const auto objectTag = static_cast(object.get())) + objectTag->mObject->getRootNode()->setNodeMask(firstMask); +} + +void CSVRender::WorldspaceWidget::cycleNavigationMode() +{ + switch (++mSelectedNavigationMode) + { + case (CameraMode::FirstPerson): + mCameraMode->setButton("1st"); + break; + case (CameraMode::Orbit): + mCameraMode->setButton("orbit"); + break; + case (CameraMode::Free): + mCameraMode->setButton("free"); + break; + default: + mCameraMode->setButton("1st"); + mSelectedNavigationMode = 0; + break; + } +} + void CSVRender::WorldspaceWidget::handleInteraction(InteractionType type, bool activate) { if (activate) diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index 5e224b38035..9a7df38620d 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -1,15 +1,38 @@ #ifndef OPENCS_VIEW_WORLDSPACEWIDGET_H #define OPENCS_VIEW_WORLDSPACEWIDGET_H +#include #include -#include -#include "../../model/doc/document.hpp" -#include "../../model/world/tablemimedata.hpp" +#include +#include + +#include +#include + +#include #include "instancedragmodes.hpp" #include "scenewidget.hpp" -#include "mask.hpp" + +class QDragEnterEvent; +class QDragMoveEvent; +class QDropEvent; +class QModelIndex; +class QMouseEvent; +class QObject; +class QWheelEvent; +class QWidget; + +namespace CSMDoc +{ + class Document; +} + +namespace osg +{ + class Vec3f; +} namespace CSMPrefs { @@ -32,9 +55,7 @@ namespace CSVWidget namespace CSVRender { - class TagBase; class Cell; - class CellArrow; class EditMode; struct WorldspaceHitResult @@ -47,254 +68,265 @@ namespace CSVRender class WorldspaceWidget : public SceneWidget { - Q_OBJECT - - CSVWidget::SceneToolToggle2 *mSceneElements; - CSVWidget::SceneToolRun *mRun; - CSMDoc::Document& mDocument; - unsigned int mInteractionMask; - CSVWidget::SceneToolMode *mEditMode; - bool mLocked; - int mDragMode; - bool mDragging; - int mDragX; - int mDragY; - bool mSpeedMode; - double mDragFactor; - double mDragWheelFactor; - double mDragShiftFactor; - QTimer mToolTipDelayTimer; - QPoint mToolTipPos; - bool mShowToolTips; - int mToolTipDelay; - bool mInConstructor; - - public: - - enum DropType - { - Type_CellsInterior, - Type_CellsExterior, - Type_Other, - Type_DebugProfile - }; - - enum dropRequirments - { - canHandle, - needPaged, - needUnpaged, - ignored //either mixed cells, or not cells - }; + Q_OBJECT + + CSVWidget::SceneToolToggle2* mSceneElements; + CSVWidget::SceneToolRun* mRun; + CSMDoc::Document& mDocument; + unsigned int mInteractionMask; + CSVWidget::SceneToolMode* mEditMode; + CSVWidget::SceneToolMode* mCameraMode; + bool mLocked; + int mDragMode; + bool mDragging; + int mDragX; + int mDragY; + bool mSpeedMode; + double mDragFactor; + double mDragWheelFactor; + double mDragShiftFactor; + QTimer mToolTipDelayTimer; + QPoint mToolTipPos; + bool mShowToolTips; + int mToolTipDelay; + bool mInConstructor; + int mSelectedNavigationMode; + + public: + enum DropType + { + Type_CellsInterior, + Type_CellsExterior, + Type_Other, + Type_DebugProfile + }; + + enum dropRequirments + { + canHandle, + needPaged, + needUnpaged, + ignored // either mixed cells, or not cells + }; + + enum InteractionType + { + InteractionType_PrimaryEdit, + InteractionType_PrimarySelect, + InteractionType_SecondaryEdit, + InteractionType_SecondarySelect, + InteractionType_TertiarySelect, + InteractionType_PrimaryOpen, + InteractionType_None + }; + + WorldspaceWidget(CSMDoc::Document& document, QWidget* parent = nullptr); + ~WorldspaceWidget() = default; + + CSVWidget::SceneToolMode* makeNavigationSelector(CSVWidget::SceneToolbar* parent); + ///< \attention The created tool is not added to the toolbar (via addTool). Doing that + /// is the responsibility of the calling function. + + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + CSVWidget::SceneToolToggle2* makeSceneVisibilitySelector(CSVWidget::SceneToolbar* parent); + + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + CSVWidget::SceneToolRun* makeRunTool(CSVWidget::SceneToolbar* parent); - enum InteractionType - { - InteractionType_PrimaryEdit, - InteractionType_PrimarySelect, - InteractionType_SecondaryEdit, - InteractionType_SecondarySelect, - InteractionType_PrimaryOpen, - InteractionType_None - }; + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + CSVWidget::SceneToolMode* makeEditModeSelector(CSVWidget::SceneToolbar* parent); - WorldspaceWidget (CSMDoc::Document& document, QWidget *parent = nullptr); - ~WorldspaceWidget (); - - CSVWidget::SceneToolMode *makeNavigationSelector (CSVWidget::SceneToolbar *parent); - ///< \attention The created tool is not added to the toolbar (via addTool). Doing that - /// is the responsibility of the calling function. + void selectDefaultNavigationMode(); - /// \attention The created tool is not added to the toolbar (via addTool). Doing - /// that is the responsibility of the calling function. - CSVWidget::SceneToolToggle2 *makeSceneVisibilitySelector ( - CSVWidget::SceneToolbar *parent); + void centerOrbitCameraOnSelection(); - /// \attention The created tool is not added to the toolbar (via addTool). Doing - /// that is the responsibility of the calling function. - CSVWidget::SceneToolRun *makeRunTool (CSVWidget::SceneToolbar *parent); + static DropType getDropType(const std::vector& data); - /// \attention The created tool is not added to the toolbar (via addTool). Doing - /// that is the responsibility of the calling function. - CSVWidget::SceneToolMode *makeEditModeSelector (CSVWidget::SceneToolbar *parent); + virtual dropRequirments getDropRequirements(DropType type) const; - void selectDefaultNavigationMode(); + virtual void useViewHint(const std::string& hint); + ///< Default-implementation: ignored. - void centerOrbitCameraOnSelection(); + /// \return Drop handled? + virtual bool handleDrop(const std::vector& data, DropType type); - static DropType getDropType(const std::vector& data); + virtual unsigned int getVisibilityMask() const; - virtual dropRequirments getDropRequirements(DropType type) const; + /// \note This function will implicitly add elements that are independent of the + /// selected edit mode. + virtual void setInteractionMask(unsigned int mask); - virtual void useViewHint (const std::string& hint); - ///< Default-implementation: ignored. + /// \note This function will only return those elements that are both visible and + /// marked for interaction. + unsigned int getInteractionMask() const; - /// \return Drop handled? - virtual bool handleDrop (const std::vector& data, - DropType type); + virtual void setEditLock(bool locked); - virtual unsigned int getVisibilityMask() const; + CSMDoc::Document& getDocument(); - /// \note This function will implicitly add elements that are independent of the - /// selected edit mode. - virtual void setInteractionMask (unsigned int mask); + /// \param elementMask Elements to be affected by the clear operation + virtual void clearSelection(int elementMask) = 0; - /// \note This function will only return those elements that are both visible and - /// marked for interaction. - unsigned int getInteractionMask() const; + /// \param elementMask Elements to be affected by the select operation + virtual void invertSelection(int elementMask) = 0; - virtual void setEditLock (bool locked); + /// \param elementMask Elements to be affected by the select operation + virtual void selectAll(int elementMask) = 0; - CSMDoc::Document& getDocument(); + // Select everything that references the same ID as at least one of the elements + // already selected + // + /// \param elementMask Elements to be affected by the select operation + virtual void selectAllWithSameParentId(int elementMask) = 0; - /// \param elementMask Elements to be affected by the clear operation - virtual void clearSelection (int elementMask) = 0; + virtual void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) = 0; - /// \param elementMask Elements to be affected by the select operation - virtual void invertSelection (int elementMask) = 0; + virtual void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) = 0; - /// \param elementMask Elements to be affected by the select operation - virtual void selectAll (int elementMask) = 0; + /// 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 + /// returned. + WorldspaceHitResult mousePick(const QPoint& localPos, unsigned int interactionMask) const; - // Select everything that references the same ID as at least one of the elements - // already selected - // - /// \param elementMask Elements to be affected by the select operation - virtual void selectAllWithSameParentId (int elementMask) = 0; + virtual std::string getCellId(const osg::Vec3f& point) const = 0; - virtual void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) = 0; + /// \note Returns the cell if it exists, otherwise a null pointer + virtual Cell* getCell(const osg::Vec3d& point) const = 0; - virtual void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) = 0; + virtual Cell* getCell(const CSMWorld::CellCoordinates& coords) const = 0; - /// 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 - /// returned. - WorldspaceHitResult mousePick (const QPoint& localPos, unsigned int interactionMask) const; + virtual osg::ref_ptr getSnapTarget(unsigned int elementMask) const = 0; - virtual std::string getCellId (const osg::Vec3f& point) const = 0; + virtual std::vector> getSelection(unsigned int elementMask) const = 0; - /// \note Returns the cell if it exists, otherwise a null pointer - virtual Cell* getCell(const osg::Vec3d& point) const = 0; + virtual void selectGroup(const std::vector&) const = 0; - virtual Cell* getCell(const CSMWorld::CellCoordinates& coords) const = 0; + virtual void unhideAll() const = 0; - virtual std::vector > getSelection (unsigned int elementMask) - const = 0; + virtual std::vector> getEdited(unsigned int elementMask) const = 0; - virtual std::vector > getEdited (unsigned int elementMask) - const = 0; + virtual void setSubMode(int subMode, unsigned int elementMask) = 0; - virtual void setSubMode (int subMode, unsigned int elementMask) = 0; + /// Erase all overrides and restore the visual representation to its true state. + virtual void reset(unsigned int elementMask) = 0; - /// Erase all overrides and restore the visual representation to its true state. - virtual void reset (unsigned int elementMask) = 0; + EditMode* getEditMode(); - protected: + protected: + /// Visual elements in a scene + /// @note do not change the enumeration values, they are used in pre-existing button file names! + enum ButtonId + { + Button_Reference = 0x1, + Button_Pathgrid = 0x2, + Button_Water = 0x4, + Button_Terrain = 0x8 + }; - /// Visual elements in a scene - /// @note do not change the enumeration values, they are used in pre-existing button file names! - enum ButtonId - { - Button_Reference = 0x1, - Button_Pathgrid = 0x2, - Button_Water = 0x4, - Button_Fog = 0x8, - Button_Terrain = 0x10 - }; + enum CameraMode + { + FirstPerson, + Orbit, + Free + }; - virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool); + virtual void addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool); - virtual void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool); + virtual void addEditModeSelectorButtons(CSVWidget::SceneToolMode* tool); - virtual void updateOverlay(); + virtual void updateOverlay(); - void mouseMoveEvent (QMouseEvent *event) override; - void wheelEvent (QWheelEvent *event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; - virtual void handleInteractionPress (const WorldspaceHitResult& hit, InteractionType type); + virtual void handleInteractionPress(const WorldspaceHitResult& hit, InteractionType type); - void settingChanged (const CSMPrefs::Setting *setting) override; + void settingChanged(const CSMPrefs::Setting* setting) override; - EditMode *getEditMode(); + bool getSpeedMode(); - bool getSpeedMode(); + void cycleNavigationMode(); - private: + private: + void dragEnterEvent(QDragEnterEvent* event) override; - void dragEnterEvent(QDragEnterEvent *event) override; + void dropEvent(QDropEvent* event) override; - void dropEvent(QDropEvent* event) override; + void dragMoveEvent(QDragMoveEvent* event) override; - void dragMoveEvent(QDragMoveEvent *event) override; + virtual std::string getStartupInstruction() = 0; - virtual std::string getStartupInstruction() = 0; + void handleInteraction(InteractionType type, bool activate); - void handleInteraction(InteractionType type, bool activate); + public slots: - public slots: + /// \note Drags will be automatically aborted when the aborting is triggered + /// (either explicitly or implicitly) from within this class. This function only + /// needs to be called, when the drag abort is triggered externally (e.g. from + /// an edit mode). + void abortDrag(); - /// \note Drags will be automatically aborted when the aborting is triggered - /// (either explicitly or implicitly) from within this class. This function only - /// needs to be called, when the drag abort is triggered externally (e.g. from - /// an edit mode). - void abortDrag(); + private slots: - private slots: + virtual void referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) = 0; - virtual void referenceableDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight) = 0; + virtual void referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end) = 0; - virtual void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end) = 0; + virtual void referenceableAdded(const QModelIndex& index, int start, int end) = 0; - virtual void referenceableAdded (const QModelIndex& index, int start, int end) = 0; + virtual void referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) = 0; - virtual void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) = 0; + virtual void referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end) = 0; - virtual void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) = 0; + virtual void referenceAdded(const QModelIndex& index, int start, int end) = 0; - virtual void referenceAdded (const QModelIndex& index, int start, int end) = 0; + virtual void pathgridDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) = 0; - virtual void pathgridDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) = 0; + virtual void pathgridAboutToBeRemoved(const QModelIndex& parent, int start, int end) = 0; - virtual void pathgridAboutToBeRemoved (const QModelIndex& parent, int start, int end) = 0; + virtual void pathgridAdded(const QModelIndex& parent, int start, int end) = 0; - virtual void pathgridAdded (const QModelIndex& parent, int start, int end) = 0; + virtual void runRequest(const std::string& profile); + void debugProfileDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - virtual void runRequest (const std::string& profile); + void debugProfileAboutToBeRemoved(const QModelIndex& parent, int start, int end); - void debugProfileDataChanged (const QModelIndex& topLeft, - const QModelIndex& bottomRight); + void editModeChanged(const std::string& id); - void debugProfileAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void showToolTip(); - void editModeChanged (const std::string& id); + void primaryOpen(bool activate); - void showToolTip(); + void primaryEdit(bool activate); - void primaryOpen(bool activate); + void secondaryEdit(bool activate); - void primaryEdit(bool activate); + void primarySelect(bool activate); - void secondaryEdit(bool activate); + void secondarySelect(bool activate); - void primarySelect(bool activate); + void tertiarySelect(bool activate); - void secondarySelect(bool activate); + void speedMode(bool activate); - void speedMode(bool activate); + void toggleHiddenInstances(); - protected slots: + protected slots: - void elementSelectionChanged(); + void elementSelectionChanged(); - signals: + signals: - void closeRequest(); + void closeRequest(); - void dataDropped(const std::vector& data); + void dataDropped(const std::vector& data); - void requestFocus (const std::string& id); + void requestFocus(const std::string& id); friend class MouseState; }; diff --git a/apps/opencs/view/tools/merge.cpp b/apps/opencs/view/tools/merge.cpp index f50a85f2fcc..0e67a1b1d8c 100644 --- a/apps/opencs/view/tools/merge.cpp +++ b/apps/opencs/view/tools/merge.cpp @@ -1,116 +1,119 @@ #include "merge.hpp" -#include #include -#include -#include -#include -#include #include +#include +#include +#include +#include +#include + +#include #include "../../model/doc/document.hpp" #include "../../model/doc/documentmanager.hpp" +#include "../../model/doc/state.hpp" -#include "../doc/filewidget.hpp" #include "../doc/adjusterwidget.hpp" +#include "../doc/filewidget.hpp" -void CSVTools::Merge::keyPressEvent (QKeyEvent *event) +void CSVTools::Merge::keyPressEvent(QKeyEvent* event) { - if (event->key()==Qt::Key_Escape) + if (event->key() == Qt::Key_Escape) { event->accept(); cancel(); } else - QWidget::keyPressEvent (event); + QWidget::keyPressEvent(event); } -CSVTools::Merge::Merge (CSMDoc::DocumentManager& documentManager, QWidget *parent) -: QWidget (parent), mDocument (nullptr), mDocumentManager (documentManager) +CSVTools::Merge::Merge(CSMDoc::DocumentManager& documentManager, QWidget* parent) + : QWidget(parent) + , mDocument(nullptr) + , mDocumentManager(documentManager) { - setWindowTitle ("Merge Content Files into a new Game File"); + setWindowTitle("Merge Content Files into a new Game File"); - QVBoxLayout *mainLayout = new QVBoxLayout; - setLayout (mainLayout); + QVBoxLayout* mainLayout = new QVBoxLayout; + setLayout(mainLayout); - QSplitter *splitter = new QSplitter (Qt::Horizontal, this); + QSplitter* splitter = new QSplitter(Qt::Horizontal, this); - mainLayout->addWidget (splitter, 1); + mainLayout->addWidget(splitter, 1); // left panel (files to be merged) - QWidget *left = new QWidget (this); - left->setContentsMargins (0, 0, 0, 0); - splitter->addWidget (left); + QWidget* left = new QWidget(this); + left->setContentsMargins(0, 0, 0, 0); + splitter->addWidget(left); - QVBoxLayout *leftLayout = new QVBoxLayout; - left->setLayout (leftLayout); + QVBoxLayout* leftLayout = new QVBoxLayout; + left->setLayout(leftLayout); - leftLayout->addWidget (new QLabel ("Files to be merged", this)); + leftLayout->addWidget(new QLabel("Files to be merged", this)); - mFiles = new QListWidget (this); - leftLayout->addWidget (mFiles, 1); + mFiles = new QListWidget(this); + leftLayout->addWidget(mFiles, 1); // right panel (new game file) - QWidget *right = new QWidget (this); - right->setContentsMargins (0, 0, 0, 0); - splitter->addWidget (right); + QWidget* right = new QWidget(this); + right->setContentsMargins(0, 0, 0, 0); + splitter->addWidget(right); - QVBoxLayout *rightLayout = new QVBoxLayout; - rightLayout->setAlignment (Qt::AlignTop); - right->setLayout (rightLayout); + QVBoxLayout* rightLayout = new QVBoxLayout; + rightLayout->setAlignment(Qt::AlignTop); + right->setLayout(rightLayout); - rightLayout->addWidget (new QLabel ("New game file", this)); + rightLayout->addWidget(new QLabel("New game file", this)); - mNewFile = new CSVDoc::FileWidget (this); - mNewFile->setType (false); - mNewFile->extensionLabelIsVisible (true); - rightLayout->addWidget (mNewFile); + mNewFile = new CSVDoc::FileWidget(this); + mNewFile->setType(false); + mNewFile->extensionLabelIsVisible(true); + rightLayout->addWidget(mNewFile); - mAdjuster = new CSVDoc::AdjusterWidget (this); + mAdjuster = new CSVDoc::AdjusterWidget(this); - rightLayout->addWidget (mAdjuster); + rightLayout->addWidget(mAdjuster); - connect (mNewFile, SIGNAL (nameChanged (const QString&, bool)), - mAdjuster, SLOT (setName (const QString&, bool))); - connect (mAdjuster, SIGNAL (stateChanged (bool)), this, SLOT (stateChanged (bool))); + connect(mNewFile, &CSVDoc::FileWidget::nameChanged, mAdjuster, &CSVDoc::AdjusterWidget::setName); + connect(mAdjuster, &CSVDoc::AdjusterWidget::stateChanged, this, &Merge::stateChanged); // buttons - QDialogButtonBox *buttons = new QDialogButtonBox (QDialogButtonBox::Cancel, Qt::Horizontal, this); + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel, Qt::Horizontal, this); - connect (buttons->button (QDialogButtonBox::Cancel), SIGNAL (clicked()), this, SLOT (cancel())); + connect(buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &Merge::cancel); - mOkay = new QPushButton ("Merge", this); - connect (mOkay, SIGNAL (clicked()), this, SLOT (accept())); - mOkay->setDefault (true); - buttons->addButton (mOkay, QDialogButtonBox::AcceptRole); + mOkay = new QPushButton("Merge", this); + connect(mOkay, &QPushButton::clicked, this, &Merge::accept); + mOkay->setDefault(true); + buttons->addButton(mOkay, QDialogButtonBox::AcceptRole); - mainLayout->addWidget (buttons); + mainLayout->addWidget(buttons); } -void CSVTools::Merge::configure (CSMDoc::Document *document) +void CSVTools::Merge::configure(CSMDoc::Document* document) { mDocument = document; - mNewFile->setName (""); + mNewFile->setName(""); // content files while (mFiles->count()) - delete mFiles->takeItem (0); + delete mFiles->takeItem(0); - std::vector files = document->getContentFiles(); + std::vector files = document->getContentFiles(); - for (std::vector::const_iterator iter (files.begin()); - iter!=files.end(); ++iter) - mFiles->addItem (QString::fromUtf8 (iter->filename().string().c_str())); + for (std::vector::const_iterator iter(files.begin()); iter != files.end(); ++iter) + mFiles->addItem(Files::pathToQString(iter->filename())); } -void CSVTools::Merge::setLocalData (const boost::filesystem::path& localData) +void CSVTools::Merge::setLocalData(const std::filesystem::path& localData) { - mAdjuster->setLocalData (localData); + mAdjuster->setLocalData(localData); } -CSMDoc::Document *CSVTools::Merge::getDocument() const +CSMDoc::Document* CSVTools::Merge::getDocument() const { return mDocument; } @@ -123,20 +126,19 @@ void CSVTools::Merge::cancel() void CSVTools::Merge::accept() { - if ((mDocument->getState() & CSMDoc::State_Merging)==0) + if ((mDocument->getState() & CSMDoc::State_Merging) == 0) { - std::vector< boost::filesystem::path > files (1, mAdjuster->getPath()); + std::vector files{ mAdjuster->getPath() }; - std::unique_ptr target ( - mDocumentManager.makeDocument (files, files[0], true)); + std::unique_ptr target(mDocumentManager.makeDocument(files, files[0], true)); - mDocument->runMerge (std::move(target)); + mDocument->runMerge(std::move(target)); hide(); } } -void CSVTools::Merge::stateChanged (bool valid) +void CSVTools::Merge::stateChanged(bool valid) { - mOkay->setEnabled (valid); + mOkay->setEnabled(valid); } diff --git a/apps/opencs/view/tools/merge.hpp b/apps/opencs/view/tools/merge.hpp index d394a431edf..cfb36d26132 100644 --- a/apps/opencs/view/tools/merge.hpp +++ b/apps/opencs/view/tools/merge.hpp @@ -1,11 +1,9 @@ -#ifndef CSV_TOOLS_REPORTTABLE_H -#define CSV_TOOLS_REPORTTABLE_H +#ifndef CSV_TOOLS_MERGE_H +#define CSV_TOOLS_MERGE_H #include -#ifndef Q_MOC_RUN -#include -#endif +#include class QPushButton; class QListWidget; @@ -26,37 +24,36 @@ namespace CSVTools { class Merge : public QWidget { - Q_OBJECT - - CSMDoc::Document *mDocument; - QPushButton *mOkay; - QListWidget *mFiles; - CSVDoc::FileWidget *mNewFile; - CSVDoc::AdjusterWidget *mAdjuster; - CSMDoc::DocumentManager& mDocumentManager; + Q_OBJECT - void keyPressEvent (QKeyEvent *event) override; + CSMDoc::Document* mDocument; + QPushButton* mOkay; + QListWidget* mFiles; + CSVDoc::FileWidget* mNewFile; + CSVDoc::AdjusterWidget* mAdjuster; + CSMDoc::DocumentManager& mDocumentManager; - public: + void keyPressEvent(QKeyEvent* event) override; - Merge (CSMDoc::DocumentManager& documentManager, QWidget *parent = nullptr); + public: + Merge(CSMDoc::DocumentManager& documentManager, QWidget* parent = nullptr); - /// Configure dialogue for a new merge - void configure (CSMDoc::Document *document); + /// Configure dialogue for a new merge + void configure(CSMDoc::Document* document); - void setLocalData (const boost::filesystem::path& localData); + void setLocalData(const std::filesystem::path& localData); - CSMDoc::Document *getDocument() const; + CSMDoc::Document* getDocument() const; - public slots: + public slots: - void cancel(); + void cancel(); - private slots: + private slots: - void accept(); + void accept(); - void stateChanged (bool valid); + void stateChanged(bool valid); }; } diff --git a/apps/opencs/view/tools/reportsubview.cpp b/apps/opencs/view/tools/reportsubview.cpp index c7712f29cf8..f9603aca6a0 100644 --- a/apps/opencs/view/tools/reportsubview.cpp +++ b/apps/opencs/view/tools/reportsubview.cpp @@ -2,27 +2,32 @@ #include "reporttable.hpp" -CSVTools::ReportSubView::ReportSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: CSVDoc::SubView (id), mDocument (document), mRefreshState (0) +#include + +#include "../../model/doc/document.hpp" +#include "../../model/doc/state.hpp" + +CSVTools::ReportSubView::ReportSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) + : CSVDoc::SubView(id) + , mDocument(document) + , mRefreshState(0) { - if (id.getType()==CSMWorld::UniversalId::Type_VerificationResults) + if (id.getType() == CSMWorld::UniversalId::Type_VerificationResults) mRefreshState = CSMDoc::State_Verifying; - setWidget (mTable = new ReportTable (document, id, false, mRefreshState, this)); + setWidget(mTable = new ReportTable(document, id, false, mRefreshState, this)); - connect (mTable, SIGNAL (editRequest (const CSMWorld::UniversalId&, const std::string&)), - SIGNAL (focusId (const CSMWorld::UniversalId&, const std::string&))); + connect(mTable, &ReportTable::editRequest, this, &ReportSubView::focusId); - if (mRefreshState==CSMDoc::State_Verifying) + if (mRefreshState == CSMDoc::State_Verifying) { - connect (mTable, SIGNAL (refreshRequest()), this, SLOT (refreshRequest())); + connect(mTable, &ReportTable::refreshRequest, this, &ReportSubView::refreshRequest); - connect (&document, SIGNAL (stateChanged (int, CSMDoc::Document *)), - mTable, SLOT (stateChanged (int, CSMDoc::Document *))); + connect(&document, &CSMDoc::Document::stateChanged, mTable, &ReportTable::stateChanged); } } -void CSVTools::ReportSubView::setEditLock (bool locked) +void CSVTools::ReportSubView::setEditLock(bool locked) { // ignored. We don't change document state anyway. } @@ -31,10 +36,10 @@ void CSVTools::ReportSubView::refreshRequest() { if (!(mDocument.getState() & mRefreshState)) { - if (mRefreshState==CSMDoc::State_Verifying) + if (mRefreshState == CSMDoc::State_Verifying) { mTable->clear(); - mDocument.verify (getUniversalId()); + mDocument.verify(getUniversalId()); } } } diff --git a/apps/opencs/view/tools/reportsubview.hpp b/apps/opencs/view/tools/reportsubview.hpp index 6d48690b430..5b0cef5b184 100644 --- a/apps/opencs/view/tools/reportsubview.hpp +++ b/apps/opencs/view/tools/reportsubview.hpp @@ -2,9 +2,9 @@ #define CSV_TOOLS_REPORTSUBVIEW_H #include "../doc/subview.hpp" +#include -class QTableView; -class QModelIndex; +class QObject; namespace CSMDoc { @@ -17,21 +17,20 @@ namespace CSVTools class ReportSubView : public CSVDoc::SubView { - Q_OBJECT + Q_OBJECT - ReportTable *mTable; - CSMDoc::Document& mDocument; - int mRefreshState; + ReportTable* mTable; + CSMDoc::Document& mDocument; + int mRefreshState; - public: + public: + ReportSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); - ReportSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); + void setEditLock(bool locked) override; - void setEditLock (bool locked) override; + private slots: - private slots: - - void refreshRequest(); + void refreshRequest(); }; } diff --git a/apps/opencs/view/tools/reporttable.cpp b/apps/opencs/view/tools/reporttable.cpp index c1297d475be..7ec55b96b5d 100644 --- a/apps/opencs/view/tools/reporttable.cpp +++ b/apps/opencs/view/tools/reporttable.cpp @@ -1,21 +1,28 @@ #include "reporttable.hpp" #include +#include -#include #include -#include -#include -#include -#include #include +#include +#include #include +#include #include +#include +#include + +#include +#include +#include +#include +#include #include "../../model/tools/reportmodel.hpp" -#include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcut.hpp" +#include "../../model/prefs/state.hpp" #include "../../view/world/idtypedelegate.hpp" @@ -23,52 +30,50 @@ namespace CSVTools { class RichTextDelegate : public QStyledItemDelegate { - public: + public: + RichTextDelegate(QObject* parent = nullptr); - RichTextDelegate (QObject *parent = nullptr); - - void paint(QPainter *painter, const QStyleOptionViewItem& option, - const QModelIndex& index) const override; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; }; } -CSVTools::RichTextDelegate::RichTextDelegate (QObject *parent) : QStyledItemDelegate (parent) -{} +CSVTools::RichTextDelegate::RichTextDelegate(QObject* parent) + : QStyledItemDelegate(parent) +{ +} -void CSVTools::RichTextDelegate::paint(QPainter *painter, const QStyleOptionViewItem& option, - const QModelIndex& index) const +void CSVTools::RichTextDelegate::paint( + QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QTextDocument document; - QVariant value = index.data (Qt::DisplayRole); + QVariant value = index.data(Qt::DisplayRole); if (value.isValid() && !value.isNull()) { - document.setHtml (value.toString()); - painter->translate (option.rect.topLeft()); - document.drawContents (painter); - painter->translate (-option.rect.topLeft()); + document.setHtml(value.toString()); + painter->translate(option.rect.topLeft()); + document.drawContents(painter); + painter->translate(-option.rect.topLeft()); } } - -void CSVTools::ReportTable::contextMenuEvent (QContextMenuEvent *event) +void CSVTools::ReportTable::contextMenuEvent(QContextMenuEvent* event) { QModelIndexList selectedRows = selectionModel()->selectedRows(); // create context menu - QMenu menu (this); + QMenu menu(this); if (!selectedRows.empty()) { - menu.addAction (mShowAction); - menu.addAction (mRemoveAction); + menu.addAction(mShowAction); + menu.addAction(mRemoveAction); bool found = false; - for (QModelIndexList::const_iterator iter (selectedRows.begin()); - iter!=selectedRows.end(); ++iter) + for (QModelIndexList::const_iterator iter(selectedRows.begin()); iter != selectedRows.end(); ++iter) { - QString hint = mProxyModel->data (mProxyModel->index (iter->row(), 2)).toString(); + QString hint = mProxyModel->data(mProxyModel->index(iter->row(), 2)).toString(); - if (!hint.isEmpty() && hint[0]=='R') + if (!hint.isEmpty() && hint[0] == 'R') { found = true; break; @@ -76,35 +81,33 @@ void CSVTools::ReportTable::contextMenuEvent (QContextMenuEvent *event) } if (found) - menu.addAction (mReplaceAction); + menu.addAction(mReplaceAction); } if (mRefreshAction) - menu.addAction (mRefreshAction); + menu.addAction(mRefreshAction); - menu.exec (event->globalPos()); + menu.exec(event->globalPos()); } -void CSVTools::ReportTable::mouseMoveEvent (QMouseEvent *event) +void CSVTools::ReportTable::mouseMoveEvent(QMouseEvent* event) { if (event->buttons() & Qt::LeftButton) - startDragFromTable (*this); + startDragFromTable(*this, indexAt(event->pos())); } -void CSVTools::ReportTable::mouseDoubleClickEvent (QMouseEvent *event) +void CSVTools::ReportTable::mouseDoubleClickEvent(QMouseEvent* event) { - Qt::KeyboardModifiers modifiers = - event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier); + Qt::KeyboardModifiers modifiers = event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier); QModelIndex index = currentIndex(); - selectionModel()->select (index, - QItemSelectionModel::Clear | QItemSelectionModel::Select | QItemSelectionModel::Rows); + selectionModel()->select( + index, QItemSelectionModel::Clear | QItemSelectionModel::Select | QItemSelectionModel::Rows); - std::map::iterator iter = - mDoubleClickActions.find (modifiers); + std::map::iterator iter = mDoubleClickActions.find(modifiers); - if (iter==mDoubleClickActions.end()) + if (iter == mDoubleClickActions.end()) { event->accept(); return; @@ -138,69 +141,68 @@ void CSVTools::ReportTable::mouseDoubleClickEvent (QMouseEvent *event) } } -CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, - const CSMWorld::UniversalId& id, bool richTextDescription, int refreshState, - QWidget *parent) -: CSVWorld::DragRecordTable (document, parent), mModel (document.getReport (id)), - mRefreshAction (nullptr), mRefreshState (refreshState) +CSVTools::ReportTable::ReportTable(CSMDoc::Document& document, const CSMWorld::UniversalId& id, + bool richTextDescription, int refreshState, QWidget* parent) + : CSVWorld::DragRecordTable(document, parent) + , mModel(document.getReport(id)) + , mRefreshAction(nullptr) + , mRefreshState(refreshState) { - horizontalHeader()->setSectionResizeMode (QHeaderView::Interactive); - horizontalHeader()->setStretchLastSection (true); + horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + horizontalHeader()->setStretchLastSection(true); verticalHeader()->hide(); - setSortingEnabled (true); - setSelectionBehavior (QAbstractItemView::SelectRows); - setSelectionMode (QAbstractItemView::ExtendedSelection); + setSortingEnabled(true); + setSelectionBehavior(QAbstractItemView::SelectRows); + setSelectionMode(QAbstractItemView::ExtendedSelection); - mProxyModel = new QSortFilterProxyModel (this); + mProxyModel = new QSortFilterProxyModel(this); mProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); - mProxyModel->setSourceModel (mModel); + mProxyModel->setSourceModel(mModel); mProxyModel->setSortRole(Qt::UserRole); - setModel (mProxyModel); - setColumnHidden (2, true); + setModel(mProxyModel); + setColumnHidden(2, true); - mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate (nullptr, - mDocument, this); + mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate(nullptr, mDocument, this); - setItemDelegateForColumn (0, mIdTypeDelegate); + setItemDelegateForColumn(0, mIdTypeDelegate); if (richTextDescription) - setItemDelegateForColumn (mModel->columnCount()-1, new RichTextDelegate (this)); + setItemDelegateForColumn(mModel->columnCount() - 1, new RichTextDelegate(this)); - mShowAction = new QAction (tr ("Show"), this); - connect (mShowAction, SIGNAL (triggered()), this, SLOT (showSelection())); - addAction (mShowAction); + mShowAction = new QAction(tr("Show"), this); + connect(mShowAction, &QAction::triggered, this, &ReportTable::showSelection); + addAction(mShowAction); CSMPrefs::Shortcut* showShortcut = new CSMPrefs::Shortcut("reporttable-show", this); showShortcut->associateAction(mShowAction); - mRemoveAction = new QAction (tr ("Remove from list"), this); - connect (mRemoveAction, SIGNAL (triggered()), this, SLOT (removeSelection())); - addAction (mRemoveAction); + mRemoveAction = new QAction(tr("Remove from list"), this); + connect(mRemoveAction, &QAction::triggered, this, &ReportTable::removeSelection); + addAction(mRemoveAction); CSMPrefs::Shortcut* removeShortcut = new CSMPrefs::Shortcut("reporttable-remove", this); removeShortcut->associateAction(mRemoveAction); - mReplaceAction = new QAction (tr ("Replace"), this); - connect (mReplaceAction, SIGNAL (triggered()), this, SIGNAL (replaceRequest())); - addAction (mReplaceAction); + mReplaceAction = new QAction(tr("Replace"), this); + connect(mReplaceAction, &QAction::triggered, this, &ReportTable::replaceRequest); + addAction(mReplaceAction); CSMPrefs::Shortcut* replaceShortcut = new CSMPrefs::Shortcut("reporttable-replace", this); replaceShortcut->associateAction(mReplaceAction); if (mRefreshState) { - mRefreshAction = new QAction (tr ("Refresh"), this); - mRefreshAction->setEnabled (!(mDocument.getState() & mRefreshState)); - connect (mRefreshAction, SIGNAL (triggered()), this, SIGNAL (refreshRequest())); - addAction (mRefreshAction); + mRefreshAction = new QAction(tr("Refresh"), this); + mRefreshAction->setEnabled(!(mDocument.getState() & mRefreshState)); + connect(mRefreshAction, &QAction::triggered, this, &ReportTable::refreshRequest); + addAction(mRefreshAction); CSMPrefs::Shortcut* refreshShortcut = new CSMPrefs::Shortcut("reporttable-refresh", this); refreshShortcut->associateAction(mRefreshAction); } - mDoubleClickActions.insert (std::make_pair (Qt::NoModifier, Action_Edit)); - mDoubleClickActions.insert (std::make_pair (Qt::ShiftModifier, Action_Remove)); - mDoubleClickActions.insert (std::make_pair (Qt::ControlModifier, Action_EditAndRemove)); + mDoubleClickActions.insert(std::make_pair(Qt::NoModifier, Action_Edit)); + mDoubleClickActions.insert(std::make_pair(Qt::ShiftModifier, Action_Remove)); + mDoubleClickActions.insert(std::make_pair(Qt::ControlModifier, Action_EditAndRemove)); - connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), - this, SLOT (settingChanged (const CSMPrefs::Setting *))); + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &ReportTable::settingChanged); CSMPrefs::get()["Reports"].update(); } @@ -210,16 +212,15 @@ std::vector CSVTools::ReportTable::getDraggedRecords() co QModelIndexList selectedRows = selectionModel()->selectedRows(); - for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); - ++iter) + for (QModelIndexList::const_iterator iter(selectedRows.begin()); iter != selectedRows.end(); ++iter) { - ids.push_back (mModel->getUniversalId (mProxyModel->mapToSource (*iter).row())); + ids.push_back(mModel->getUniversalId(mProxyModel->mapToSource(*iter).row())); } return ids; } -std::vector CSVTools::ReportTable::getReplaceIndices (bool selection) const +std::vector CSVTools::ReportTable::getReplaceIndices(bool selection) const { std::vector indices; @@ -229,68 +230,67 @@ std::vector CSVTools::ReportTable::getReplaceIndices (bool selection) const std::vector rows; - for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); - ++iter) + for (QModelIndexList::const_iterator iter(selectedRows.begin()); iter != selectedRows.end(); ++iter) { - rows.push_back (mProxyModel->mapToSource (*iter).row()); + rows.push_back(mProxyModel->mapToSource(*iter).row()); } - std::sort (rows.begin(), rows.end()); + std::sort(rows.begin(), rows.end()); - for (std::vector::const_iterator iter (rows.begin()); iter!=rows.end(); ++iter) + for (std::vector::const_iterator iter(rows.begin()); iter != rows.end(); ++iter) { - QString hint = mModel->data (mModel->index (*iter, 2)).toString(); + QString hint = mModel->data(mModel->index(*iter, 2)).toString(); - if (!hint.isEmpty() && hint[0]=='R') - indices.push_back (*iter); + if (!hint.isEmpty() && hint[0] == 'R') + indices.push_back(*iter); } } else { - for (int i=0; irowCount(); ++i) + for (int i = 0; i < mModel->rowCount(); ++i) { - QString hint = mModel->data (mModel->index (i, 2)).toString(); + QString hint = mModel->data(mModel->index(i, 2)).toString(); - if (!hint.isEmpty() && hint[0]=='R') - indices.push_back (i); + if (!hint.isEmpty() && hint[0] == 'R') + indices.push_back(i); } } return indices; } -void CSVTools::ReportTable::flagAsReplaced (int index) +void CSVTools::ReportTable::flagAsReplaced(int index) { - mModel->flagAsReplaced (index); + mModel->flagAsReplaced(index); } -void CSVTools::ReportTable::settingChanged (const CSMPrefs::Setting *setting) +void CSVTools::ReportTable::settingChanged(const CSMPrefs::Setting* setting) { - if (setting->getParent()->getKey()=="Reports") + if (setting->getParent()->getKey() == "Reports") { - QString base ("double"); + QString base("double"); QString key = setting->getKey().c_str(); - if (key.startsWith (base)) + if (key.startsWith(base)) { - QString modifierString = key.mid (base.size()); + QString modifierString = key.mid(base.size()); Qt::KeyboardModifiers modifiers; - if (modifierString=="-s") + if (modifierString == "-s") modifiers = Qt::ShiftModifier; - else if (modifierString=="-c") + else if (modifierString == "-c") modifiers = Qt::ControlModifier; - else if (modifierString=="-sc") + else if (modifierString == "-sc") modifiers = Qt::ShiftModifier | Qt::ControlModifier; DoubleClickAction action = Action_None; std::string value = setting->toString(); - if (value=="Edit") + if (value == "Edit") action = Action_Edit; - else if (value=="Remove") + else if (value == "Remove") action = Action_Remove; - else if (value=="Edit And Remove") + else if (value == "Edit And Remove") action = Action_EditAndRemove; mDoubleClickActions[modifiers] = action; @@ -298,19 +298,18 @@ void CSVTools::ReportTable::settingChanged (const CSMPrefs::Setting *setting) return; } } - else if (*setting=="Records/type-format") - mIdTypeDelegate->settingChanged (setting); + else if (*setting == "Records/type-format") + mIdTypeDelegate->settingChanged(setting); } void CSVTools::ReportTable::showSelection() { QModelIndexList selectedRows = selectionModel()->selectedRows(); - for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); - ++iter) + for (QModelIndexList::const_iterator iter(selectedRows.begin()); iter != selectedRows.end(); ++iter) { - int row = mProxyModel->mapToSource (*iter).row(); - emit editRequest (mModel->getUniversalId (row), mModel->getHint (row)); + int row = mProxyModel->mapToSource(*iter).row(); + emit editRequest(mModel->getUniversalId(row), mModel->getHint(row)); } } @@ -320,16 +319,15 @@ void CSVTools::ReportTable::removeSelection() std::vector rows; - for (QModelIndexList::iterator iter (selectedRows.begin()); iter!=selectedRows.end(); - ++iter) + for (QModelIndexList::iterator iter(selectedRows.begin()); iter != selectedRows.end(); ++iter) { - rows.push_back (mProxyModel->mapToSource (*iter).row()); + rows.push_back(mProxyModel->mapToSource(*iter).row()); } - std::sort (rows.begin(), rows.end()); + std::sort(rows.begin(), rows.end()); - for (std::vector::const_reverse_iterator iter (rows.rbegin()); iter!=rows.rend(); ++iter) - mProxyModel->removeRows (*iter, 1); + for (std::vector::const_reverse_iterator iter(rows.rbegin()); iter != rows.rend(); ++iter) + mProxyModel->removeRows(*iter, 1); selectionModel()->clear(); } @@ -339,8 +337,8 @@ void CSVTools::ReportTable::clear() mModel->clear(); } -void CSVTools::ReportTable::stateChanged (int state, CSMDoc::Document *document) +void CSVTools::ReportTable::stateChanged(int state, CSMDoc::Document* document) { if (mRefreshAction) - mRefreshAction->setEnabled (!(state & mRefreshState)); + mRefreshAction->setEnabled(!(state & mRefreshState)); } diff --git a/apps/opencs/view/tools/reporttable.hpp b/apps/opencs/view/tools/reporttable.hpp index f39dd6f8575..25f8d1b0171 100644 --- a/apps/opencs/view/tools/reporttable.hpp +++ b/apps/opencs/view/tools/reporttable.hpp @@ -2,11 +2,24 @@ #define CSV_TOOLS_REPORTTABLE_H #include +#include +#include + +#include #include "../world/dragrecordtable.hpp" class QAction; class QSortFilterProxyModel; +class QContextMenuEvent; +class QMouseEvent; +class QObject; +class QWidget; + +namespace CSMDoc +{ + class Document; +} namespace CSMTools { @@ -27,76 +40,74 @@ namespace CSVTools { class ReportTable : public CSVWorld::DragRecordTable { - Q_OBJECT - - enum DoubleClickAction - { - Action_None, - Action_Edit, - Action_Remove, - Action_EditAndRemove - }; - - QSortFilterProxyModel *mProxyModel; - CSMTools::ReportModel *mModel; - CSVWorld::CommandDelegate *mIdTypeDelegate; - QAction *mShowAction; - QAction *mRemoveAction; - QAction *mReplaceAction; - QAction *mRefreshAction; - std::map mDoubleClickActions; - int mRefreshState; + Q_OBJECT - private: + enum DoubleClickAction + { + Action_None, + Action_Edit, + Action_Remove, + Action_EditAndRemove + }; - void contextMenuEvent (QContextMenuEvent *event) override; + QSortFilterProxyModel* mProxyModel; + CSMTools::ReportModel* mModel; + CSVWorld::CommandDelegate* mIdTypeDelegate; + QAction* mShowAction; + QAction* mRemoveAction; + QAction* mReplaceAction; + QAction* mRefreshAction; + std::map mDoubleClickActions; + int mRefreshState; - void mouseMoveEvent (QMouseEvent *event) override; + private: + void contextMenuEvent(QContextMenuEvent* event) override; - void mouseDoubleClickEvent (QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent* event) override; - public: + void mouseDoubleClickEvent(QMouseEvent* event) override; - /// \param richTextDescription Use rich text in the description column. - /// \param refreshState Document state to check for refresh function. If value is - /// 0 no refresh function exists. If the document current has the specified state - /// the refresh function is disabled. - ReportTable (CSMDoc::Document& document, const CSMWorld::UniversalId& id, - bool richTextDescription, int refreshState = 0, QWidget *parent = nullptr); + public: + /// \param richTextDescription Use rich text in the description column. + /// \param refreshState Document state to check for refresh function. If value is + /// 0 no refresh function exists. If the document current has the specified state + /// the refresh function is disabled. + ReportTable(CSMDoc::Document& document, const CSMWorld::UniversalId& id, bool richTextDescription, + int refreshState = 0, QWidget* parent = nullptr); - std::vector getDraggedRecords() const override; + std::vector getDraggedRecords() const override; - void clear(); + void clear(); - /// Return indices of rows that are suitable for replacement. - /// - /// \param selection Only list selected rows. - /// - /// \return rows in the original model - std::vector getReplaceIndices (bool selection) const; + /// Return indices of rows that are suitable for replacement. + /// + /// \param selection Only list selected rows. + /// + /// \return rows in the original model + std::vector getReplaceIndices(bool selection) const; - /// \param index row in the original model - void flagAsReplaced (int index); + /// \param index row in the original model + void flagAsReplaced(int index); - private slots: + private slots: - void settingChanged (const CSMPrefs::Setting *setting); + void settingChanged(const CSMPrefs::Setting* setting); - void showSelection(); + void showSelection(); - void removeSelection(); + void removeSelection(); - public slots: + public slots: - void stateChanged (int state, CSMDoc::Document *document); + void stateChanged(int state, CSMDoc::Document* document); - signals: + signals: - void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); + void editRequest(const CSMWorld::UniversalId& id, const std::string& hint); - void replaceRequest(); + void replaceRequest(); - void refreshRequest(); + void refreshRequest(); }; } diff --git a/apps/opencs/view/tools/searchbox.cpp b/apps/opencs/view/tools/searchbox.cpp index e0f965e3c81..a2871b5489f 100644 --- a/apps/opencs/view/tools/searchbox.cpp +++ b/apps/opencs/view/tools/searchbox.cpp @@ -3,8 +3,6 @@ #include #include -#include -#include #include "../../model/world/columns.hpp" @@ -13,7 +11,7 @@ void CSVTools::SearchBox::updateSearchButton() { if (!mSearchEnabled) - mSearch.setEnabled (false); + mSearch.setEnabled(false); else { switch (mMode.currentIndex()) @@ -23,75 +21,77 @@ void CSVTools::SearchBox::updateSearchButton() case 2: case 3: - mSearch.setEnabled (!mText.text().isEmpty()); + mSearch.setEnabled(!mText.text().isEmpty()); break; case 4: - mSearch.setEnabled (true); + mSearch.setEnabled(true); break; } } } -CSVTools::SearchBox::SearchBox (QWidget *parent) -: QWidget (parent), mSearch (tr("Search")), mSearchEnabled (false), mReplace (tr("Replace All")) +CSVTools::SearchBox::SearchBox(QWidget* parent) + : QWidget(parent) + , mSearch(tr("Search")) + , mSearchEnabled(false) + , mReplace(tr("Replace All")) { - mLayout = new QGridLayout (this); + mLayout = new QGridLayout(this); // search panel - std::vector> states = - CSMWorld::Columns::getEnums (CSMWorld::Columns::ColumnId_Modification); - states.resize (states.size()-1); // ignore erased state - - for (std::vector>::const_iterator iter (states.begin()); iter!=states.end(); - ++iter) - mRecordState.addItem (QString::fromUtf8 (iter->second.c_str())); - - mMode.addItem (tr("Text")); - mMode.addItem (tr("Text (RegEx)")); - mMode.addItem (tr("ID")); - mMode.addItem (tr("ID (RegEx)")); - mMode.addItem (tr("Record State")); - connect (&mMode, SIGNAL (activated (int)), this, SLOT (modeSelected (int))); - mLayout->addWidget (&mMode, 0, 0); - - connect (&mText, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); - connect (&mText, SIGNAL (returnPressed()), this, SLOT (startSearch())); - mInput.insertWidget (0, &mText); - - mInput.insertWidget (1, &mRecordState); - mLayout->addWidget (&mInput, 0, 1); - - mCaseSensitive.setText (tr ("Case")); - mLayout->addWidget (&mCaseSensitive, 0, 2); - - connect (&mSearch, SIGNAL (clicked (bool)), this, SLOT (startSearch (bool))); - mLayout->addWidget (&mSearch, 0, 3); + std::vector> states + = CSMWorld::Columns::getEnums(CSMWorld::Columns::ColumnId_Modification); + states.resize(states.size() - 1); // ignore erased state + + for (std::vector>::const_iterator iter(states.begin()); iter != states.end(); ++iter) + mRecordState.addItem(QString::fromUtf8(iter->second.c_str())); + + mMode.addItem(tr("Text")); + mMode.addItem(tr("Text (RegEx)")); + mMode.addItem(tr("ID")); + mMode.addItem(tr("ID (RegEx)")); + mMode.addItem(tr("Record State")); + connect(&mMode, qOverload(&QComboBox::activated), this, &SearchBox::modeSelected); + mLayout->addWidget(&mMode, 0, 0); + + connect(&mText, &QLineEdit::textChanged, this, &SearchBox::textChanged); + connect(&mText, &QLineEdit::returnPressed, this, [this]() { this->startSearch(false); }); + mInput.insertWidget(0, &mText); + + mInput.insertWidget(1, &mRecordState); + mLayout->addWidget(&mInput, 0, 1); + + mCaseSensitive.setText(tr("Case")); + mLayout->addWidget(&mCaseSensitive, 0, 2); + + connect(&mSearch, &QPushButton::clicked, this, qOverload(&SearchBox::startSearch)); + mLayout->addWidget(&mSearch, 0, 3); // replace panel - mReplaceInput.insertWidget (0, &mReplaceText); - mReplaceInput.insertWidget (1, &mReplacePlaceholder); + mReplaceInput.insertWidget(0, &mReplaceText); + mReplaceInput.insertWidget(1, &mReplacePlaceholder); - mLayout->addWidget (&mReplaceInput, 1, 1); + mLayout->addWidget(&mReplaceInput, 1, 1); + + mLayout->addWidget(&mReplace, 1, 3); - mLayout->addWidget (&mReplace, 1, 3); - // layout adjustments - mLayout->setColumnMinimumWidth (2, 50); - mLayout->setColumnStretch (1, 1); + mLayout->setColumnMinimumWidth(2, 50); + mLayout->setColumnStretch(1, 1); + + mLayout->setContentsMargins(0, 0, 0, 0); - mLayout->setContentsMargins (0, 0, 0, 0); + connect(&mReplace, &QPushButton::clicked, this, qOverload(&SearchBox::replaceAll)); - connect (&mReplace, (SIGNAL (clicked (bool))), this, SLOT (replaceAll (bool))); - // update - modeSelected (0); + modeSelected(0); updateSearchButton(); } -void CSVTools::SearchBox::setSearchMode (bool enabled) +void CSVTools::SearchBox::setSearchMode(bool enabled) { mSearchEnabled = enabled; updateSearchButton(); @@ -99,7 +99,7 @@ void CSVTools::SearchBox::setSearchMode (bool enabled) CSMTools::Search CSVTools::SearchBox::getSearch() const { - CSMTools::Search::Type type = static_cast (mMode.currentIndex()); + CSMTools::Search::Type type = static_cast(mMode.currentIndex()); bool caseSensitive = mCaseSensitive.isChecked(); switch (type) @@ -107,29 +107,30 @@ CSMTools::Search CSVTools::SearchBox::getSearch() const case CSMTools::Search::Type_Text: case CSMTools::Search::Type_Id: - return CSMTools::Search (type, caseSensitive, std::string (mText.text().toUtf8().data())); - + return CSMTools::Search(type, caseSensitive, std::string(mText.text().toUtf8().data())); + case CSMTools::Search::Type_TextRegEx: case CSMTools::Search::Type_IdRegEx: - return CSMTools::Search (type, caseSensitive, QRegExp (mText.text().toUtf8().data(), Qt::CaseInsensitive)); - + return CSMTools::Search(type, caseSensitive, + QRegularExpression(mText.text().toUtf8().data(), QRegularExpression::CaseInsensitiveOption)); + case CSMTools::Search::Type_RecordState: - return CSMTools::Search (type, caseSensitive, mRecordState.currentIndex()); + return CSMTools::Search(type, caseSensitive, mRecordState.currentIndex()); case CSMTools::Search::Type_None: break; } - throw std::logic_error ("invalid search mode index"); + throw std::logic_error("invalid search mode index"); } std::string CSVTools::SearchBox::getReplaceText() const { - CSMTools::Search::Type type = static_cast (mMode.currentIndex()); - + CSMTools::Search::Type type = static_cast(mMode.currentIndex()); + switch (type) { case CSMTools::Search::Type_Text: @@ -141,13 +142,13 @@ std::string CSVTools::SearchBox::getReplaceText() const default: - throw std::logic_error ("Invalid search mode for replace"); + throw std::logic_error("Invalid search mode for replace"); } } -void CSVTools::SearchBox::setEditLock (bool locked) +void CSVTools::SearchBox::setEditLock(bool locked) { - mReplace.setEnabled (!locked); + mReplace.setEnabled(!locked); } void CSVTools::SearchBox::focus() @@ -155,7 +156,7 @@ void CSVTools::SearchBox::focus() mInput.currentWidget()->setFocus(); } -void CSVTools::SearchBox::modeSelected (int index) +void CSVTools::SearchBox::modeSelected(int index) { switch (index) { @@ -164,33 +165,33 @@ void CSVTools::SearchBox::modeSelected (int index) case CSMTools::Search::Type_Id: case CSMTools::Search::Type_IdRegEx: - mInput.setCurrentIndex (0); - mReplaceInput.setCurrentIndex (0); + mInput.setCurrentIndex(0); + mReplaceInput.setCurrentIndex(0); break; case CSMTools::Search::Type_RecordState: - mInput.setCurrentIndex (1); - mReplaceInput.setCurrentIndex (1); + mInput.setCurrentIndex(1); + mReplaceInput.setCurrentIndex(1); break; } mInput.currentWidget()->setFocus(); - + updateSearchButton(); } -void CSVTools::SearchBox::textChanged (const QString& text) +void CSVTools::SearchBox::textChanged(const QString& text) { updateSearchButton(); } -void CSVTools::SearchBox::startSearch (bool checked) +void CSVTools::SearchBox::startSearch(bool checked) { if (mSearch.isEnabled()) - emit startSearch (getSearch()); + emit startSearch(getSearch()); } -void CSVTools::SearchBox::replaceAll (bool checked) +void CSVTools::SearchBox::replaceAll(bool checked) { emit replaceAll(); } diff --git a/apps/opencs/view/tools/searchbox.hpp b/apps/opencs/view/tools/searchbox.hpp index cbeb150d8b3..76712fee4b4 100644 --- a/apps/opencs/view/tools/searchbox.hpp +++ b/apps/opencs/view/tools/searchbox.hpp @@ -1,13 +1,13 @@ #ifndef CSV_TOOLS_SEARCHBOX_H #define CSV_TOOLS_SEARCHBOX_H -#include -#include -#include #include -#include -#include +#include #include +#include +#include +#include +#include class QGridLayout; @@ -20,54 +20,52 @@ namespace CSVTools { class SearchBox : public QWidget { - Q_OBJECT - - QStackedWidget mInput; - QLineEdit mText; - QComboBox mRecordState; - QCheckBox mCaseSensitive; - QPushButton mSearch; - QGridLayout *mLayout; - QComboBox mMode; - bool mSearchEnabled; - QStackedWidget mReplaceInput; - QLineEdit mReplaceText; - QLabel mReplacePlaceholder; - QPushButton mReplace; + Q_OBJECT - private: + QStackedWidget mInput; + QLineEdit mText; + QComboBox mRecordState; + QCheckBox mCaseSensitive; + QPushButton mSearch; + QGridLayout* mLayout; + QComboBox mMode; + bool mSearchEnabled; + QStackedWidget mReplaceInput; + QLineEdit mReplaceText; + QLabel mReplacePlaceholder; + QPushButton mReplace; - void updateSearchButton(); - - public: + private: + void updateSearchButton(); - SearchBox (QWidget *parent = nullptr); + public: + SearchBox(QWidget* parent = nullptr); - void setSearchMode (bool enabled); + void setSearchMode(bool enabled); - CSMTools::Search getSearch() const; + CSMTools::Search getSearch() const; - std::string getReplaceText() const; + std::string getReplaceText() const; - void setEditLock (bool locked); + void setEditLock(bool locked); - void focus(); + void focus(); - private slots: + private slots: - void modeSelected (int index); + void modeSelected(int index); - void textChanged (const QString& text); + void textChanged(const QString& text); - void startSearch (bool checked = true); + void startSearch(bool checked = true); - void replaceAll (bool checked); + void replaceAll(bool checked); - signals: + signals: - void startSearch (const CSMTools::Search& search); + void startSearch(const CSMTools::Search& search); - void replaceAll(); + void replaceAll(); }; } diff --git a/apps/opencs/view/tools/searchsubview.cpp b/apps/opencs/view/tools/searchsubview.cpp index 07ba7907e7f..98fd97e7ae0 100644 --- a/apps/opencs/view/tools/searchsubview.cpp +++ b/apps/opencs/view/tools/searchsubview.cpp @@ -4,160 +4,158 @@ #include "../../model/doc/document.hpp" #include "../../model/doc/state.hpp" -#include "../../model/tools/search.hpp" +#include "../../model/prefs/state.hpp" #include "../../model/tools/reportmodel.hpp" +#include "../../model/tools/search.hpp" #include "../../model/world/idtablebase.hpp" -#include "../../model/prefs/state.hpp" -#include "../world/tablebottombox.hpp" #include "../world/creator.hpp" +#include "../world/tablebottombox.hpp" + +#include +#include +#include +#include #include "reporttable.hpp" #include "searchbox.hpp" -void CSVTools::SearchSubView::replace (bool selection) +void CSVTools::SearchSubView::replace(bool selection) { if (mLocked) return; - std::vector indices = mTable->getReplaceIndices (selection); + std::vector indices = mTable->getReplaceIndices(selection); std::string replace = mSearchBox.getReplaceText(); - const CSMTools::ReportModel& model = - dynamic_cast (*mTable->model()); + const CSMTools::ReportModel& model = dynamic_cast(*mTable->model()); bool autoDelete = CSMPrefs::get()["Search & Replace"]["auto-delete"].isTrue(); - CSMTools::Search search (mSearch); - CSMWorld::IdTableBase *currentTable = nullptr; + CSMTools::Search search(mSearch); + CSMWorld::IdTableBase* currentTable = nullptr; // We are running through the indices in reverse order to avoid messing up multiple results // in a single string. - for (std::vector::const_reverse_iterator iter (indices.rbegin()); iter!=indices.rend(); ++iter) + for (std::vector::const_reverse_iterator iter(indices.rbegin()); iter != indices.rend(); ++iter) { - CSMWorld::UniversalId id = model.getUniversalId (*iter); + const CSMWorld::UniversalId& id = model.getUniversalId(*iter); - CSMWorld::UniversalId::Type type = CSMWorld::UniversalId::getParentType (id.getType()); + CSMWorld::UniversalId::Type type = CSMWorld::UniversalId::getParentType(id.getType()); - CSMWorld::IdTableBase *table = &dynamic_cast ( - *mDocument.getData().getTableModel (type)); + CSMWorld::IdTableBase* table = &dynamic_cast(*mDocument.getData().getTableModel(type)); - if (table!=currentTable) + if (table != currentTable) { - search.configure (table); + search.configure(table); currentTable = table; } - std::string hint = model.getHint (*iter); + std::string hint = model.getHint(*iter); - if (search.verify (mDocument, table, id, hint)) + if (search.verify(mDocument, table, id, hint)) { - search.replace (mDocument, table, id, hint, replace); - mTable->flagAsReplaced (*iter); + search.replace(mDocument, table, id, hint, replace); + mTable->flagAsReplaced(*iter); if (autoDelete) - mTable->model()->removeRows (*iter, 1); + mTable->model()->removeRows(*iter, 1); } } } -void CSVTools::SearchSubView::showEvent (QShowEvent *event) +void CSVTools::SearchSubView::showEvent(QShowEvent* event) { - CSVDoc::SubView::showEvent (event); + CSVDoc::SubView::showEvent(event); mSearchBox.focus(); } -CSVTools::SearchSubView::SearchSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: CSVDoc::SubView (id), mDocument (document), mLocked (false) +CSVTools::SearchSubView::SearchSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) + : CSVDoc::SubView(id) + , mDocument(document) + , mLocked(false) { - QVBoxLayout *layout = new QVBoxLayout; + QVBoxLayout* layout = new QVBoxLayout; - layout->addWidget (&mSearchBox); + layout->addWidget(&mSearchBox); - layout->addWidget (mTable = new ReportTable (document, id, true), 2); + layout->addWidget(mTable = new ReportTable(document, id, true), 2); - layout->addWidget (mBottom = - new CSVWorld::TableBottomBox (CSVWorld::NullCreatorFactory(), document, id, this), 0); + layout->addWidget(mBottom = new CSVWorld::TableBottomBox(CSVWorld::NullCreatorFactory(), document, id, this), 0); - QWidget *widget = new QWidget; + QWidget* widget = new QWidget; - widget->setLayout (layout); + widget->setLayout(layout); - setWidget (widget); + setWidget(widget); - stateChanged (document.getState(), &document); + stateChanged(document.getState(), &document); - connect (mTable, SIGNAL (editRequest (const CSMWorld::UniversalId&, const std::string&)), - SIGNAL (focusId (const CSMWorld::UniversalId&, const std::string&))); + connect(mTable, &ReportTable::editRequest, this, &SearchSubView::focusId); - connect (mTable, SIGNAL (replaceRequest()), this, SLOT (replaceRequest())); + connect(mTable, &ReportTable::replaceRequest, this, &SearchSubView::replaceRequest); - connect (&document, SIGNAL (stateChanged (int, CSMDoc::Document *)), - this, SLOT (stateChanged (int, CSMDoc::Document *))); + connect(&document, &CSMDoc::Document::stateChanged, this, &SearchSubView::stateChanged); - connect (&mSearchBox, SIGNAL (startSearch (const CSMTools::Search&)), - this, SLOT (startSearch (const CSMTools::Search&))); + connect( + &mSearchBox, qOverload(&SearchBox::startSearch), this, &SearchSubView::startSearch); - connect (&mSearchBox, SIGNAL (replaceAll()), this, SLOT (replaceAllRequest())); + connect(&mSearchBox, qOverload<>(&SearchBox::replaceAll), this, &SearchSubView::replaceAllRequest); - connect (document.getReport (id), SIGNAL (rowsRemoved (const QModelIndex&, int, int)), - this, SLOT (tableSizeUpdate())); + connect(document.getReport(id), &CSMTools::ReportModel::rowsRemoved, this, &SearchSubView::tableSizeUpdate); - connect (document.getReport (id), SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (tableSizeUpdate())); + connect(document.getReport(id), &CSMTools::ReportModel::rowsInserted, this, &SearchSubView::tableSizeUpdate); - connect (&document, SIGNAL (operationDone (int, bool)), - this, SLOT (operationDone (int, bool))); + connect(&document, &CSMDoc::Document::operationDone, this, &SearchSubView::operationDone); } -void CSVTools::SearchSubView::setEditLock (bool locked) +void CSVTools::SearchSubView::setEditLock(bool locked) { mLocked = locked; - mSearchBox.setEditLock (locked); + mSearchBox.setEditLock(locked); } -void CSVTools::SearchSubView::setStatusBar (bool show) +void CSVTools::SearchSubView::setStatusBar(bool show) { mBottom->setStatusBar(show); } -void CSVTools::SearchSubView::stateChanged (int state, CSMDoc::Document *document) +void CSVTools::SearchSubView::stateChanged(int state, CSMDoc::Document* document) { - mSearchBox.setSearchMode (!(state & CSMDoc::State_Searching)); + mSearchBox.setSearchMode(!(state & CSMDoc::State_Searching)); } -void CSVTools::SearchSubView::startSearch (const CSMTools::Search& search) +void CSVTools::SearchSubView::startSearch(const CSMTools::Search& search) { CSMPrefs::Category& settings = CSMPrefs::get()["Search & Replace"]; mSearch = search; - mSearch.setPadding (settings["char-before"].toInt(), settings["char-after"].toInt()); + mSearch.setPadding(settings["char-before"].toInt(), settings["char-after"].toInt()); mTable->clear(); - mDocument.runSearch (getUniversalId(), mSearch); + mDocument.runSearch(getUniversalId(), mSearch); } void CSVTools::SearchSubView::replaceRequest() { - replace (true); + replace(true); } void CSVTools::SearchSubView::replaceAllRequest() { - replace (false); + replace(false); } void CSVTools::SearchSubView::tableSizeUpdate() { - mBottom->tableSizeChanged (mDocument.getReport (getUniversalId())->rowCount(), 0, 0); + mBottom->tableSizeChanged(mDocument.getReport(getUniversalId())->rowCount(), 0, 0); } -void CSVTools::SearchSubView::operationDone (int type, bool failed) +void CSVTools::SearchSubView::operationDone(int type, bool failed) { - if (type==CSMDoc::State_Searching && !failed && - !mDocument.getReport (getUniversalId())->rowCount()) + if (type == CSMDoc::State_Searching && !failed && !mDocument.getReport(getUniversalId())->rowCount()) { - mBottom->setStatusMessage ("No Results"); + mBottom->setStatusMessage("No Results"); } } diff --git a/apps/opencs/view/tools/searchsubview.hpp b/apps/opencs/view/tools/searchsubview.hpp index cbcb01577e7..270706d7aa0 100644 --- a/apps/opencs/view/tools/searchsubview.hpp +++ b/apps/opencs/view/tools/searchsubview.hpp @@ -5,10 +5,9 @@ #include "../doc/subview.hpp" -#include "searchbox.hpp" +#include -class QTableView; -class QModelIndex; +#include "searchbox.hpp" namespace CSMDoc { @@ -26,44 +25,41 @@ namespace CSVTools class SearchSubView : public CSVDoc::SubView { - Q_OBJECT - - ReportTable *mTable; - SearchBox mSearchBox; - CSMDoc::Document& mDocument; - CSMTools::Search mSearch; - bool mLocked; - CSVWorld::TableBottomBox *mBottom; - - private: - - void replace (bool selection); + Q_OBJECT - protected: + ReportTable* mTable; + SearchBox mSearchBox; + CSMDoc::Document& mDocument; + CSMTools::Search mSearch; + bool mLocked; + CSVWorld::TableBottomBox* mBottom; - void showEvent (QShowEvent *event) override; + private: + void replace(bool selection); - public: + protected: + void showEvent(QShowEvent* event) override; - SearchSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); + public: + SearchSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); - void setEditLock (bool locked) override; + void setEditLock(bool locked) override; - void setStatusBar (bool show) override; + void setStatusBar(bool show) override; - private slots: + private slots: - void stateChanged (int state, CSMDoc::Document *document); + void stateChanged(int state, CSMDoc::Document* document); - void startSearch (const CSMTools::Search& search); + void startSearch(const CSMTools::Search& search); - void replaceRequest(); + void replaceRequest(); - void replaceAllRequest(); + void replaceAllRequest(); - void tableSizeUpdate(); + void tableSizeUpdate(); - void operationDone (int type, bool failed); + void operationDone(int type, bool failed); }; } diff --git a/apps/opencs/view/tools/subviews.cpp b/apps/opencs/view/tools/subviews.cpp index 8c3d6d50ea6..417eba61a04 100644 --- a/apps/opencs/view/tools/subviews.cpp +++ b/apps/opencs/view/tools/subviews.cpp @@ -2,15 +2,16 @@ #include "../doc/subviewfactoryimp.hpp" +#include +#include +#include + #include "reportsubview.hpp" #include "searchsubview.hpp" -void CSVTools::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) +void CSVTools::addSubViewFactories(CSVDoc::SubViewFactoryManager& manager) { - manager.add (CSMWorld::UniversalId::Type_VerificationResults, - new CSVDoc::SubViewFactory); - manager.add (CSMWorld::UniversalId::Type_LoadErrorLog, - new CSVDoc::SubViewFactory); - manager.add (CSMWorld::UniversalId::Type_Search, - new CSVDoc::SubViewFactory); + manager.add(CSMWorld::UniversalId::Type_VerificationResults, new CSVDoc::SubViewFactory); + manager.add(CSMWorld::UniversalId::Type_LoadErrorLog, new CSVDoc::SubViewFactory); + manager.add(CSMWorld::UniversalId::Type_Search, new CSVDoc::SubViewFactory); } diff --git a/apps/opencs/view/tools/subviews.hpp b/apps/opencs/view/tools/subviews.hpp index 1bac3222823..bd76ec0c71c 100644 --- a/apps/opencs/view/tools/subviews.hpp +++ b/apps/opencs/view/tools/subviews.hpp @@ -8,7 +8,7 @@ namespace CSVDoc namespace CSVTools { - void addSubViewFactories (CSVDoc::SubViewFactoryManager& manager); + void addSubViewFactories(CSVDoc::SubViewFactoryManager& manager); } #endif diff --git a/apps/opencs/view/widget/coloreditor.cpp b/apps/opencs/view/widget/coloreditor.cpp index 1cc649313b5..a8e5f670a05 100644 --- a/apps/opencs/view/widget/coloreditor.cpp +++ b/apps/opencs/view/widget/coloreditor.cpp @@ -1,51 +1,48 @@ #include "coloreditor.hpp" -#include -#include -#include +#include #include -#include #include #include "colorpickerpopup.hpp" -CSVWidget::ColorEditor::ColorEditor(const QColor &color, QWidget *parent, const bool popupOnStart) +class QShowEvent; + +CSVWidget::ColorEditor::ColorEditor(const QColor& color, QWidget* parent, const bool popupOnStart) : ColorEditor(parent, popupOnStart) { setColor(color); } -CSVWidget::ColorEditor::ColorEditor(const int colorInt, QWidget *parent, const bool popupOnStart) +CSVWidget::ColorEditor::ColorEditor(const int colorInt, QWidget* parent, const bool popupOnStart) : ColorEditor(parent, popupOnStart) { setColor(colorInt); } -CSVWidget::ColorEditor::ColorEditor(QWidget *parent, const bool popupOnStart) - : QPushButton(parent), - mColorPicker(new ColorPickerPopup(this)), - mPopupOnStart(popupOnStart) +CSVWidget::ColorEditor::ColorEditor(QWidget* parent, const bool popupOnStart) + : QPushButton(parent) + , mColorPicker(new ColorPickerPopup(this)) + , mPopupOnStart(popupOnStart) { - connect(this, SIGNAL(clicked()), this, SLOT(showPicker())); - connect(mColorPicker, SIGNAL(colorChanged(const QColor &)), this, SLOT(pickerColorChanged(const QColor &))); + connect(this, &ColorEditor::clicked, this, &ColorEditor::showPicker); + connect(mColorPicker, &ColorPickerPopup::colorChanged, this, &ColorEditor::pickerColorChanged); } -void CSVWidget::ColorEditor::paintEvent(QPaintEvent *event) +void CSVWidget::ColorEditor::paintEvent(QPaintEvent* event) { QPushButton::paintEvent(event); QRect buttonRect = rect(); QRect coloredRect(buttonRect.x() + qRound(buttonRect.width() / 4.0), - buttonRect.y() + qRound(buttonRect.height() / 4.0), - buttonRect.width() / 2, - buttonRect.height() / 2); + buttonRect.y() + qRound(buttonRect.height() / 4.0), buttonRect.width() / 2, buttonRect.height() / 2); QPainter painter(this); painter.fillRect(coloredRect, mColor); painter.setPen(Qt::black); painter.drawRect(coloredRect); } -void CSVWidget::ColorEditor::showEvent(QShowEvent *event) +void CSVWidget::ColorEditor::showEvent(QShowEvent* event) { QPushButton::showEvent(event); if (isVisible() && mPopupOnStart) @@ -66,7 +63,7 @@ int CSVWidget::ColorEditor::colorInt() const return (mColor.blue() << 16) | (mColor.green() << 8) | (mColor.red()); } -void CSVWidget::ColorEditor::setColor(const QColor &color) +void CSVWidget::ColorEditor::setColor(const QColor& color) { mColor = color; update(); @@ -86,7 +83,7 @@ void CSVWidget::ColorEditor::showPicker() emit pickingFinished(); } -void CSVWidget::ColorEditor::pickerColorChanged(const QColor &color) +void CSVWidget::ColorEditor::pickerColorChanged(const QColor& color) { mColor = color; update(); diff --git a/apps/opencs/view/widget/coloreditor.hpp b/apps/opencs/view/widget/coloreditor.hpp index aa746da6826..284de9f6c60 100644 --- a/apps/opencs/view/widget/coloreditor.hpp +++ b/apps/opencs/view/widget/coloreditor.hpp @@ -13,42 +13,42 @@ namespace CSVWidget class ColorEditor : public QPushButton { - Q_OBJECT + Q_OBJECT - QColor mColor; - ColorPickerPopup *mColorPicker; - bool mPopupOnStart; + QColor mColor; + ColorPickerPopup* mColorPicker; + bool mPopupOnStart; - QPoint calculatePopupPosition(); + QPoint calculatePopupPosition(); - public: - ColorEditor(const QColor &color, QWidget *parent = nullptr, const bool popupOnStart = false); - ColorEditor(const int colorInt, QWidget *parent = nullptr, const bool popupOnStart = false); + public: + ColorEditor(const QColor& color, QWidget* parent = nullptr, const bool popupOnStart = false); + ColorEditor(const int colorInt, QWidget* parent = nullptr, const bool popupOnStart = false); - QColor color() const; + QColor color() const; - /// \return Color RGB value encoded in an int. - int colorInt() const; + /// \return Color RGB value encoded in an int. + int colorInt() const; - void setColor(const QColor &color); + void setColor(const QColor& color); - /// \brief Set color using given int value. - /// \param colorInt RGB color value encoded as an integer. - void setColor(const int colorInt); + /// \brief Set color using given int value. + /// \param colorInt RGB color value encoded as an integer. + void setColor(const int colorInt); - protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; + protected: + void paintEvent(QPaintEvent* event) override; + void showEvent(QShowEvent* event) override; - private: - ColorEditor(QWidget *parent = nullptr, const bool popupOnStart = false); + private: + ColorEditor(QWidget* parent = nullptr, const bool popupOnStart = false); - private slots: - void showPicker(); - void pickerColorChanged(const QColor &color); + private slots: + void showPicker(); + void pickerColorChanged(const QColor& color); - signals: - void pickingFinished(); + signals: + void pickingFinished(); }; } diff --git a/apps/opencs/view/widget/colorpickerpopup.cpp b/apps/opencs/view/widget/colorpickerpopup.cpp index 206a6672769..87c62e137bd 100644 --- a/apps/opencs/view/widget/colorpickerpopup.cpp +++ b/apps/opencs/view/widget/colorpickerpopup.cpp @@ -1,29 +1,26 @@ #include "colorpickerpopup.hpp" #include -#include #include #include -#include -#include +#include +#include +#include -CSVWidget::ColorPickerPopup::ColorPickerPopup(QWidget *parent) +CSVWidget::ColorPickerPopup::ColorPickerPopup(QWidget* parent) : QFrame(parent) { setWindowFlags(Qt::Popup); - setFrameStyle(QFrame::Box | QFrame::Plain); + setFrameStyle(QFrame::Box | static_cast(QFrame::Plain)); hide(); mColorPicker = new QColorDialog(this); mColorPicker->setWindowFlags(Qt::Widget); mColorPicker->setOptions(QColorDialog::NoButtons | QColorDialog::DontUseNativeDialog); mColorPicker->installEventFilter(this); - connect(mColorPicker, - SIGNAL(currentColorChanged(const QColor &)), - this, - SIGNAL(colorChanged(const QColor &))); + connect(mColorPicker, &QColorDialog::currentColorChanged, this, &ColorPickerPopup::colorChanged); - QVBoxLayout *layout = new QVBoxLayout(this); + QVBoxLayout* layout = new QVBoxLayout(this); layout->addWidget(mColorPicker); layout->setAlignment(Qt::AlignTop | Qt::AlignLeft); layout->setContentsMargins(0, 0, 0, 0); @@ -31,7 +28,7 @@ CSVWidget::ColorPickerPopup::ColorPickerPopup(QWidget *parent) setFixedSize(mColorPicker->size()); } -void CSVWidget::ColorPickerPopup::showPicker(const QPoint &position, const QColor &initialColor) +void CSVWidget::ColorPickerPopup::showPicker(const QPoint& position, const QColor& initialColor) { QRect geometry = this->geometry(); geometry.moveTo(position); @@ -43,13 +40,13 @@ void CSVWidget::ColorPickerPopup::showPicker(const QPoint &position, const QColo mColorPicker->setCurrentColor(color); } -void CSVWidget::ColorPickerPopup::mousePressEvent(QMouseEvent *event) +void CSVWidget::ColorPickerPopup::mousePressEvent(QMouseEvent* event) { - QPushButton *button = qobject_cast(parentWidget()); + QPushButton* button = qobject_cast(parentWidget()); if (button != nullptr) { QStyleOptionButton option; - option.init(button); + option.initFrom(button); QRect buttonRect = option.rect; buttonRect.moveTo(button->mapToGlobal(buttonRect.topLeft())); @@ -63,11 +60,11 @@ void CSVWidget::ColorPickerPopup::mousePressEvent(QMouseEvent *event) QFrame::mousePressEvent(event); } -bool CSVWidget::ColorPickerPopup::eventFilter(QObject *object, QEvent *event) +bool CSVWidget::ColorPickerPopup::eventFilter(QObject* object, QEvent* event) { if (object == mColorPicker && event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); + QKeyEvent* keyEvent = static_cast(event); // Prevent QColorDialog from closing when Escape is pressed. // Instead, hide the popup. if (keyEvent->key() == Qt::Key_Escape) diff --git a/apps/opencs/view/widget/colorpickerpopup.hpp b/apps/opencs/view/widget/colorpickerpopup.hpp index d653d68fbba..8a3eb030fb1 100644 --- a/apps/opencs/view/widget/colorpickerpopup.hpp +++ b/apps/opencs/view/widget/colorpickerpopup.hpp @@ -11,19 +11,19 @@ namespace CSVWidget { Q_OBJECT - QColorDialog *mColorPicker; + QColorDialog* mColorPicker; public: - explicit ColorPickerPopup(QWidget *parent); - - void showPicker(const QPoint &position, const QColor &initialColor); + explicit ColorPickerPopup(QWidget* parent); + + void showPicker(const QPoint& position, const QColor& initialColor); protected: - void mousePressEvent(QMouseEvent *event) override; - bool eventFilter(QObject *object, QEvent *event) override; + void mousePressEvent(QMouseEvent* event) override; + bool eventFilter(QObject* object, QEvent* event) override; signals: - void colorChanged(const QColor &color); + void colorChanged(const QColor& color); }; } diff --git a/apps/opencs/view/widget/completerpopup.cpp b/apps/opencs/view/widget/completerpopup.cpp index be509bcb93f..91b09026598 100644 --- a/apps/opencs/view/widget/completerpopup.cpp +++ b/apps/opencs/view/widget/completerpopup.cpp @@ -1,6 +1,6 @@ #include "completerpopup.hpp" -CSVWidget::CompleterPopup::CompleterPopup(QWidget *parent) +CSVWidget::CompleterPopup::CompleterPopup(QWidget* parent) : QListView(parent) { setEditTriggers(QAbstractItemView::NoEditTriggers); @@ -22,7 +22,12 @@ int CSVWidget::CompleterPopup::sizeHintForRow(int row) const ensurePolished(); QModelIndex index = model()->index(row, modelColumn()); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QStyleOptionViewItem option; + initViewItemOption(&option); +#else QStyleOptionViewItem option = viewOptions(); - QAbstractItemDelegate *delegate = itemDelegate(index); +#endif + QAbstractItemDelegate* delegate = itemDelegate(index); return delegate->sizeHint(option, index).height(); } diff --git a/apps/opencs/view/widget/completerpopup.hpp b/apps/opencs/view/widget/completerpopup.hpp index 96675f56f41..673d288000c 100644 --- a/apps/opencs/view/widget/completerpopup.hpp +++ b/apps/opencs/view/widget/completerpopup.hpp @@ -7,10 +7,10 @@ namespace CSVWidget { class CompleterPopup : public QListView { - public: - CompleterPopup(QWidget *parent = nullptr); + public: + CompleterPopup(QWidget* parent = nullptr); - int sizeHintForRow(int row) const override; + int sizeHintForRow(int row) const override; }; } diff --git a/apps/opencs/view/widget/droplineedit.cpp b/apps/opencs/view/widget/droplineedit.cpp index 2ca30646131..4b1bf2aef69 100644 --- a/apps/opencs/view/widget/droplineedit.cpp +++ b/apps/opencs/view/widget/droplineedit.cpp @@ -1,20 +1,24 @@ #include "droplineedit.hpp" +#include + #include #include "../../model/world/tablemimedata.hpp" #include "../../model/world/universalid.hpp" +#include + #include "../world/dragdroputils.hpp" -CSVWidget::DropLineEdit::DropLineEdit(CSMWorld::ColumnBase::Display type, QWidget *parent) - : QLineEdit(parent), - mDropType(type) +CSVWidget::DropLineEdit::DropLineEdit(CSMWorld::ColumnBase::Display type, QWidget* parent) + : QLineEdit(parent) + , mDropType(type) { setAcceptDrops(true); } -void CSVWidget::DropLineEdit::dragEnterEvent(QDragEnterEvent *event) +void CSVWidget::DropLineEdit::dragEnterEvent(QDragEnterEvent* event) { if (CSVWorld::DragDropUtils::canAcceptData(*event, mDropType)) { @@ -22,7 +26,7 @@ void CSVWidget::DropLineEdit::dragEnterEvent(QDragEnterEvent *event) } } -void CSVWidget::DropLineEdit::dragMoveEvent(QDragMoveEvent *event) +void CSVWidget::DropLineEdit::dragMoveEvent(QDragMoveEvent* event) { if (CSVWorld::DragDropUtils::canAcceptData(*event, mDropType)) { @@ -30,7 +34,7 @@ void CSVWidget::DropLineEdit::dragMoveEvent(QDragMoveEvent *event) } } -void CSVWidget::DropLineEdit::dropEvent(QDropEvent *event) +void CSVWidget::DropLineEdit::dropEvent(QDropEvent* event) { if (CSVWorld::DragDropUtils::canAcceptData(*event, mDropType)) { diff --git a/apps/opencs/view/widget/droplineedit.hpp b/apps/opencs/view/widget/droplineedit.hpp index 91105187361..3a707d80c15 100644 --- a/apps/opencs/view/widget/droplineedit.hpp +++ b/apps/opencs/view/widget/droplineedit.hpp @@ -12,7 +12,6 @@ namespace CSMDoc namespace CSMWorld { - class TableMimeData; class UniversalId; } @@ -20,21 +19,21 @@ namespace CSVWidget { class DropLineEdit : public QLineEdit { - Q_OBJECT + Q_OBJECT - CSMWorld::ColumnBase::Display mDropType; - ///< The accepted Display type for this LineEdit. + CSMWorld::ColumnBase::Display mDropType; + ///< The accepted Display type for this LineEdit. - public: - DropLineEdit(CSMWorld::ColumnBase::Display type, QWidget *parent = nullptr); + public: + DropLineEdit(CSMWorld::ColumnBase::Display type, QWidget* parent = nullptr); - protected: - void dragEnterEvent(QDragEnterEvent *event) override; - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; + protected: + void dragEnterEvent(QDragEnterEvent* event) override; + void dragMoveEvent(QDragMoveEvent* event) override; + void dropEvent(QDropEvent* event) override; - signals: - void tableMimeDataDropped(const CSMWorld::UniversalId &id, const CSMDoc::Document *document); + signals: + void tableMimeDataDropped(const CSMWorld::UniversalId& id, const CSMDoc::Document* document); }; } diff --git a/apps/opencs/view/widget/modebutton.cpp b/apps/opencs/view/widget/modebutton.cpp index 88f0502479b..f19baf6098f 100644 --- a/apps/opencs/view/widget/modebutton.cpp +++ b/apps/opencs/view/widget/modebutton.cpp @@ -1,14 +1,19 @@ #include "modebutton.hpp" -CSVWidget::ModeButton::ModeButton (const QIcon& icon, const QString& tooltip, QWidget *parent) -: PushButton (icon, Type_Mode, tooltip, parent) -{} +#include -void CSVWidget::ModeButton::activate (SceneToolbar *toolbar) {} +class QWidget; -void CSVWidget::ModeButton::deactivate (SceneToolbar *toolbar) {} +CSVWidget::ModeButton::ModeButton(const QIcon& icon, const QString& tooltip, QWidget* parent) + : PushButton(icon, Type_Mode, tooltip, parent) +{ +} + +void CSVWidget::ModeButton::activate(SceneToolbar* toolbar) {} + +void CSVWidget::ModeButton::deactivate(SceneToolbar* toolbar) {} -bool CSVWidget::ModeButton::createContextMenu (QMenu *menu) +bool CSVWidget::ModeButton::createContextMenu(QMenu* menu) { return false; } diff --git a/apps/opencs/view/widget/modebutton.hpp b/apps/opencs/view/widget/modebutton.hpp index f595969231f..75b05e28e38 100644 --- a/apps/opencs/view/widget/modebutton.hpp +++ b/apps/opencs/view/widget/modebutton.hpp @@ -12,26 +12,24 @@ namespace CSVWidget /// \brief Specialist PushButton of Type_Mode for use in SceneToolMode class ModeButton : public PushButton { - Q_OBJECT + Q_OBJECT - public: + public: + ModeButton(const QIcon& icon, const QString& tooltip = "", QWidget* parent = nullptr); - ModeButton (const QIcon& icon, const QString& tooltip = "", - QWidget *parent = nullptr); + /// Default-Implementation: do nothing + virtual void activate(SceneToolbar* toolbar); - /// Default-Implementation: do nothing - virtual void activate (SceneToolbar *toolbar); + /// Default-Implementation: do nothing + virtual void deactivate(SceneToolbar* toolbar); - /// Default-Implementation: do nothing - virtual void deactivate (SceneToolbar *toolbar); - - /// Add context menu items to \a menu. Default-implementation: return false - /// - /// \attention menu can be a 0-pointer - /// - /// \return Have there been any menu items to be added (if menu is 0 and there - /// items to be added, the function must return true anyway. - virtual bool createContextMenu (QMenu *menu); + /// Add context menu items to \a menu. Default-implementation: return false + /// + /// \attention menu can be a 0-pointer + /// + /// \return Have there been any menu items to be added (if menu is 0 and there + /// items to be added, the function must return true anyway. + virtual bool createContextMenu(QMenu* menu); }; } diff --git a/apps/opencs/view/widget/pushbutton.cpp b/apps/opencs/view/widget/pushbutton.cpp index c4e6a41442f..6eab07dccf2 100644 --- a/apps/opencs/view/widget/pushbutton.cpp +++ b/apps/opencs/view/widget/pushbutton.cpp @@ -1,10 +1,15 @@ #include "pushbutton.hpp" -#include #include +#include + +#include + +#include +#include -#include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcutmanager.hpp" +#include "../../model/prefs/state.hpp" void CSVWidget::PushButton::processShortcuts() { @@ -22,8 +27,7 @@ void CSVWidget::PushButton::setExtendedToolTip() { case Type_TopMode: - tooltip += - "

      (left click to change mode)"; + tooltip += "

      (left click to change mode)"; break; @@ -50,52 +54,56 @@ void CSVWidget::PushButton::setExtendedToolTip() break; } - setToolTip (tooltip); + setToolTip(tooltip); } -void CSVWidget::PushButton::keyPressEvent (QKeyEvent *event) +void CSVWidget::PushButton::keyPressEvent(QKeyEvent* event) { - if (event->key()!=Qt::Key_Shift) + if (event->key() != Qt::Key_Shift) mKeepOpen = false; - QPushButton::keyPressEvent (event); + QPushButton::keyPressEvent(event); } -void CSVWidget::PushButton::keyReleaseEvent (QKeyEvent *event) +void CSVWidget::PushButton::keyReleaseEvent(QKeyEvent* event) { - if (event->key()==Qt::Key_Space) + if (event->key() == Qt::Key_Space) mKeepOpen = event->modifiers() & Qt::ShiftModifier; - QPushButton::keyReleaseEvent (event); + QPushButton::keyReleaseEvent(event); } -void CSVWidget::PushButton::mouseReleaseEvent (QMouseEvent *event) +void CSVWidget::PushButton::mouseReleaseEvent(QMouseEvent* event) { - mKeepOpen = event->button()==Qt::LeftButton && (event->modifiers() & Qt::ShiftModifier); - QPushButton::mouseReleaseEvent (event); + mKeepOpen = event->button() == Qt::LeftButton && (event->modifiers() & Qt::ShiftModifier); + QPushButton::mouseReleaseEvent(event); } -CSVWidget::PushButton::PushButton (const QIcon& icon, Type type, const QString& tooltip, - QWidget *parent) -: QPushButton (icon, "", parent), mKeepOpen (false), mType (type), mToolTip (tooltip) +CSVWidget::PushButton::PushButton(const QIcon& icon, Type type, const QString& tooltip, QWidget* parent) + : QPushButton(icon, "", parent) + , mKeepOpen(false) + , mType(type) + , mToolTip(tooltip) { - if (type==Type_Mode || type==Type_Toggle) + if (type == Type_Mode || type == Type_Toggle) { - setCheckable (true); - connect (this, SIGNAL (toggled (bool)), this, SLOT (checkedStateChanged (bool))); + setCheckable(true); + connect(this, &PushButton::toggled, this, &PushButton::checkedStateChanged); } - setCheckable (type==Type_Mode || type==Type_Toggle); + setCheckable(type == Type_Mode || type == Type_Toggle); processShortcuts(); setExtendedToolTip(); - connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), - this, SLOT (settingChanged (const CSMPrefs::Setting *))); + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &PushButton::settingChanged); } -CSVWidget::PushButton::PushButton (Type type, const QString& tooltip, QWidget *parent) -: QPushButton (parent), mKeepOpen (false), mType (type), mToolTip (tooltip) +CSVWidget::PushButton::PushButton(Type type, const QString& tooltip, QWidget* parent) + : QPushButton(parent) + , mKeepOpen(false) + , mType(type) + , mToolTip(tooltip) { - setCheckable (type==Type_Mode || type==Type_Toggle); + setCheckable(type == Type_Mode || type == Type_Toggle); processShortcuts(); setExtendedToolTip(); } @@ -115,12 +123,12 @@ CSVWidget::PushButton::Type CSVWidget::PushButton::getType() const return mType; } -void CSVWidget::PushButton::checkedStateChanged (bool checked) +void CSVWidget::PushButton::checkedStateChanged(bool checked) { setExtendedToolTip(); } -void CSVWidget::PushButton::settingChanged (const CSMPrefs::Setting *setting) +void CSVWidget::PushButton::settingChanged(const CSMPrefs::Setting* setting) { if (setting->getParent()->getKey() == "Key Bindings") { diff --git a/apps/opencs/view/widget/pushbutton.hpp b/apps/opencs/view/widget/pushbutton.hpp index b3aaaebef28..584d311b577 100644 --- a/apps/opencs/view/widget/pushbutton.hpp +++ b/apps/opencs/view/widget/pushbutton.hpp @@ -3,6 +3,11 @@ #include +class QKeyEvent; +class QMouseEvent; +class QObject; +class QWidget; + namespace CSMPrefs { class Setting; @@ -12,59 +17,52 @@ namespace CSVWidget { class PushButton : public QPushButton { - Q_OBJECT - - public: - - enum Type - { - Type_TopMode, // top level button for mode selector panel - Type_TopAction, // top level button that triggers an action - Type_Mode, // mode button - Type_Toggle - }; - - private: - - bool mKeepOpen; - Type mType; - QString mToolTip; - QString mProcessedToolTip; - - private: + Q_OBJECT - void processShortcuts(); - void setExtendedToolTip(); + public: + enum Type + { + Type_TopMode, // top level button for mode selector panel + Type_TopAction, // top level button that triggers an action + Type_Mode, // mode button + Type_Toggle + }; - protected: + private: + bool mKeepOpen; + Type mType; + QString mToolTip; + QString mProcessedToolTip; - void keyPressEvent (QKeyEvent *event) override; + private: + void processShortcuts(); + void setExtendedToolTip(); - void keyReleaseEvent (QKeyEvent *event) override; + protected: + void keyPressEvent(QKeyEvent* event) override; - void mouseReleaseEvent (QMouseEvent *event) override; + void keyReleaseEvent(QKeyEvent* event) override; - public: + void mouseReleaseEvent(QMouseEvent* event) override; - /// \param push Do not maintain a toggle state - PushButton (const QIcon& icon, Type type, const QString& tooltip = "", - QWidget *parent = nullptr); + public: + /// \param push Do not maintain a toggle state + PushButton(const QIcon& icon, Type type, const QString& tooltip = "", QWidget* parent = nullptr); - /// \param push Do not maintain a toggle state - PushButton (Type type, const QString& tooltip = "", - QWidget *parent = nullptr); + /// \param push Do not maintain a toggle state + PushButton(Type type, const QString& tooltip = "", QWidget* parent = nullptr); - bool hasKeepOpen() const; + bool hasKeepOpen() const; - /// Return tooltip used at construction (without any button-specific modifications) - QString getBaseToolTip() const; + /// Return tooltip used at construction (without any button-specific modifications) + QString getBaseToolTip() const; - Type getType() const; + Type getType() const; - private slots: + private slots: - void checkedStateChanged (bool checked); - void settingChanged (const CSMPrefs::Setting *setting); + void checkedStateChanged(bool checked); + void settingChanged(const CSMPrefs::Setting* setting); }; } diff --git a/apps/opencs/view/widget/scenetool.cpp b/apps/opencs/view/widget/scenetool.cpp index 796b9856784..36c902f952c 100644 --- a/apps/opencs/view/widget/scenetool.cpp +++ b/apps/opencs/view/widget/scenetool.cpp @@ -2,32 +2,34 @@ #include +#include + #include "scenetoolbar.hpp" -CSVWidget::SceneTool::SceneTool (SceneToolbar *parent, Type type) -: PushButton (type, "", parent) +CSVWidget::SceneTool::SceneTool(SceneToolbar* parent, Type type) + : PushButton(type, "", parent) { - setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); - setIconSize (QSize (parent->getIconSize(), parent->getIconSize())); - setFixedSize (parent->getButtonSize(), parent->getButtonSize()); + setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + setIconSize(QSize(parent->getIconSize(), parent->getIconSize())); + setFixedSize(parent->getButtonSize(), parent->getButtonSize()); - connect (this, SIGNAL (clicked()), this, SLOT (openRequest())); + connect(this, &SceneTool::clicked, this, &SceneTool::openRequest); } void CSVWidget::SceneTool::activate() {} -void CSVWidget::SceneTool::mouseReleaseEvent (QMouseEvent *event) +void CSVWidget::SceneTool::mouseReleaseEvent(QMouseEvent* event) { - if (getType()==Type_TopAction && event->button()==Qt::RightButton) - showPanel (parentWidget()->mapToGlobal (pos())); + if (getType() == Type_TopAction && event->button() == Qt::RightButton) + showPanel(parentWidget()->mapToGlobal(pos())); else - PushButton::mouseReleaseEvent (event); + PushButton::mouseReleaseEvent(event); } void CSVWidget::SceneTool::openRequest() { - if (getType()==Type_TopAction) + if (getType() == Type_TopAction) activate(); else - showPanel (parentWidget()->mapToGlobal (pos())); + showPanel(parentWidget()->mapToGlobal(pos())); } diff --git a/apps/opencs/view/widget/scenetool.hpp b/apps/opencs/view/widget/scenetool.hpp index 295375f26de..bd37ed9b358 100644 --- a/apps/opencs/view/widget/scenetool.hpp +++ b/apps/opencs/view/widget/scenetool.hpp @@ -3,6 +3,10 @@ #include "pushbutton.hpp" +class QMouseEvent; +class QObject; +class QPoint; + namespace CSVWidget { class SceneToolbar; @@ -10,25 +14,23 @@ namespace CSVWidget ///< \brief Tool base class class SceneTool : public PushButton { - Q_OBJECT - - public: - - SceneTool (SceneToolbar *parent, Type type = Type_TopMode); + Q_OBJECT - virtual void showPanel (const QPoint& position) = 0; + public: + SceneTool(SceneToolbar* parent, Type type = Type_TopMode); - /// This function will only called for buttons of type Type_TopAction. The default - /// implementation is empty. - virtual void activate(); + virtual void showPanel(const QPoint& position) = 0; - protected: + /// This function will only called for buttons of type Type_TopAction. The default + /// implementation is empty. + virtual void activate(); - void mouseReleaseEvent (QMouseEvent *event) override; + protected: + void mouseReleaseEvent(QMouseEvent* event) override; - private slots: + private slots: - void openRequest(); + void openRequest(); }; } diff --git a/apps/opencs/view/widget/scenetoolbar.cpp b/apps/opencs/view/widget/scenetoolbar.cpp index a2458397f7d..09d99e36777 100644 --- a/apps/opencs/view/widget/scenetoolbar.cpp +++ b/apps/opencs/view/widget/scenetoolbar.cpp @@ -2,48 +2,52 @@ #include +#include + #include "../../model/prefs/shortcut.hpp" #include "scenetool.hpp" -void CSVWidget::SceneToolbar::focusInEvent (QFocusEvent *event) +void CSVWidget::SceneToolbar::focusInEvent(QFocusEvent* event) { - QWidget::focusInEvent (event); + QWidget::focusInEvent(event); if (mLayout->count()) - dynamic_cast (*mLayout->itemAt (0)).widget()->setFocus(); + dynamic_cast(*mLayout->itemAt(0)).widget()->setFocus(); } -CSVWidget::SceneToolbar::SceneToolbar (int buttonSize, QWidget *parent) -: QWidget (parent), mButtonSize (buttonSize), mIconSize (buttonSize-6) +CSVWidget::SceneToolbar::SceneToolbar(int buttonSize, QWidget* parent) + : QWidget(parent) + , mButtonSize(buttonSize) + , mIconSize(buttonSize - 6) { - setFixedWidth (mButtonSize); + setFixedWidth(mButtonSize); - mLayout = new QVBoxLayout (this); - mLayout->setAlignment (Qt::AlignTop); + mLayout = new QVBoxLayout(this); + mLayout->setAlignment(Qt::AlignTop); - mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); + mLayout->setContentsMargins(QMargins(0, 0, 0, 0)); - setLayout (mLayout); + setLayout(mLayout); CSMPrefs::Shortcut* focusSceneShortcut = new CSMPrefs::Shortcut("scene-focus-toolbar", this); - connect(focusSceneShortcut, SIGNAL(activated()), this, SIGNAL(focusSceneRequest())); + connect(focusSceneShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &SceneToolbar::focusSceneRequest); } -void CSVWidget::SceneToolbar::addTool (SceneTool *tool, SceneTool *insertPoint) +void CSVWidget::SceneToolbar::addTool(SceneTool* tool, SceneTool* insertPoint) { if (!insertPoint) - mLayout->addWidget (tool, 0, Qt::AlignTop); + mLayout->addWidget(tool, 0, Qt::AlignTop); else { - int index = mLayout->indexOf (insertPoint); - mLayout->insertWidget (index+1, tool, 0, Qt::AlignTop); + int index = mLayout->indexOf(insertPoint); + mLayout->insertWidget(index + 1, tool, 0, Qt::AlignTop); } } -void CSVWidget::SceneToolbar::removeTool (SceneTool *tool) +void CSVWidget::SceneToolbar::removeTool(SceneTool* tool) { - mLayout->removeWidget (tool); + mLayout->removeWidget(tool); } int CSVWidget::SceneToolbar::getButtonSize() const diff --git a/apps/opencs/view/widget/scenetoolbar.hpp b/apps/opencs/view/widget/scenetoolbar.hpp index 70f58076524..6b4e503bd2e 100644 --- a/apps/opencs/view/widget/scenetoolbar.hpp +++ b/apps/opencs/view/widget/scenetoolbar.hpp @@ -11,33 +11,31 @@ namespace CSVWidget class SceneToolbar : public QWidget { - Q_OBJECT + Q_OBJECT - QVBoxLayout *mLayout; - int mButtonSize; - int mIconSize; + QVBoxLayout* mLayout; + int mButtonSize; + int mIconSize; - protected: + protected: + void focusInEvent(QFocusEvent* event) override; - void focusInEvent (QFocusEvent *event) override; + public: + SceneToolbar(int buttonSize, QWidget* parent = nullptr); - public: + /// If insertPoint==0, insert \a tool at the end of the scrollbar. Otherwise + /// insert tool after \a insertPoint. + void addTool(SceneTool* tool, SceneTool* insertPoint = nullptr); - SceneToolbar (int buttonSize, QWidget *parent = nullptr); + void removeTool(SceneTool* tool); - /// If insertPoint==0, insert \a tool at the end of the scrollbar. Otherwise - /// insert tool after \a insertPoint. - void addTool (SceneTool *tool, SceneTool *insertPoint = nullptr); + int getButtonSize() const; - void removeTool (SceneTool *tool); + int getIconSize() const; - int getButtonSize() const; + signals: - int getIconSize() const; - - signals: - - void focusSceneRequest(); + void focusSceneRequest(); }; } diff --git a/apps/opencs/view/widget/scenetoolmode.cpp b/apps/opencs/view/widget/scenetoolmode.cpp index 3aec44f1bac..f11d7489a87 100644 --- a/apps/opencs/view/widget/scenetoolmode.cpp +++ b/apps/opencs/view/widget/scenetoolmode.cpp @@ -1,31 +1,39 @@ #include "scenetoolmode.hpp" -#include -#include -#include -#include #include #include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include -#include "scenetoolbar.hpp" #include "modebutton.hpp" +#include "scenetoolbar.hpp" -void CSVWidget::SceneToolMode::contextMenuEvent (QContextMenuEvent *event) +void CSVWidget::SceneToolMode::contextMenuEvent(QContextMenuEvent* event) { - QMenu menu (this); - if (createContextMenu (&menu)) - menu.exec (event->globalPos()); + QMenu menu(this); + if (createContextMenu(&menu)) + menu.exec(event->globalPos()); } -bool CSVWidget::SceneToolMode::createContextMenu (QMenu *menu) +bool CSVWidget::SceneToolMode::createContextMenu(QMenu* menu) { if (mCurrent) - return mCurrent->createContextMenu (menu); + return mCurrent->createContextMenu(menu); return false; } -void CSVWidget::SceneToolMode::adjustToolTip (const ModeButton *activeMode) +void CSVWidget::SceneToolMode::adjustToolTip(const ModeButton* activeMode) { QString toolTip = mToolTip; @@ -33,103 +41,107 @@ void CSVWidget::SceneToolMode::adjustToolTip (const ModeButton *activeMode) toolTip += "

      (left click to change mode)"; - if (createContextMenu (nullptr)) + if (createContextMenu(nullptr)) toolTip += "
      (right click to access context menu)"; - setToolTip (toolTip); + setToolTip(toolTip); } -void CSVWidget::SceneToolMode::setButton (std::map::iterator iter) +void CSVWidget::SceneToolMode::setButton(std::map::iterator iter) { - for (std::map::const_iterator iter2 = mButtons.begin(); - iter2!=mButtons.end(); ++iter2) - iter2->first->setChecked (iter2==iter); + for (std::map::const_iterator iter2 = mButtons.begin(); iter2 != mButtons.end(); ++iter2) + iter2->first->setChecked(iter2 == iter); - setIcon (iter->first->icon()); - adjustToolTip (iter->first); + setIcon(iter->first->icon()); + adjustToolTip(iter->first); - if (mCurrent!=iter->first) + if (mCurrent != iter->first) { if (mCurrent) - mCurrent->deactivate (mToolbar); + mCurrent->deactivate(mToolbar); mCurrent = iter->first; - mCurrent->activate (mToolbar); + mCurrent->activate(mToolbar); } - emit modeChanged (iter->second); + emit modeChanged(iter->second); } -CSVWidget::SceneToolMode::SceneToolMode (SceneToolbar *parent, const QString& toolTip) -: SceneTool (parent), mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()), - mToolTip (toolTip), mFirst (nullptr), mCurrent (nullptr), mToolbar (parent) +CSVWidget::SceneToolMode::SceneToolMode(SceneToolbar* parent, const QString& toolTip) + : SceneTool(parent) + , mButtonSize(parent->getButtonSize()) + , mIconSize(parent->getIconSize()) + , mToolTip(toolTip) + , mFirst(nullptr) + , mCurrent(nullptr) + , mToolbar(parent) { - mPanel = new QFrame (this, Qt::Popup); + mPanel = new QFrame(this, Qt::Popup); - mLayout = new QHBoxLayout (mPanel); + mLayout = new QHBoxLayout(mPanel); - mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); + mLayout->setContentsMargins(QMargins(0, 0, 0, 0)); - mPanel->setLayout (mLayout); + mPanel->setLayout(mLayout); } -void CSVWidget::SceneToolMode::showPanel (const QPoint& position) +void CSVWidget::SceneToolMode::showPanel(const QPoint& position) { - mPanel->move (position); + mPanel->move(position); mPanel->show(); if (mFirst) - mFirst->setFocus (Qt::OtherFocusReason); + mFirst->setFocus(Qt::OtherFocusReason); } -void CSVWidget::SceneToolMode::addButton (const std::string& icon, const std::string& id, - const QString& tooltip) +void CSVWidget::SceneToolMode::addButton(const std::string& icon, const std::string& id, const QString& tooltip) { - ModeButton *button = new ModeButton (QIcon (QPixmap (icon.c_str())), tooltip, mPanel); - addButton (button, id); + ModeButton* button = new ModeButton(Misc::ScalableIcon::load(icon.c_str()), tooltip, mPanel); + addButton(button, id); } -void CSVWidget::SceneToolMode::addButton (ModeButton *button, const std::string& id) +void CSVWidget::SceneToolMode::addButton(ModeButton* button, const std::string& id) { - button->setParent (mPanel); + button->setParent(mPanel); - button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); - button->setIconSize (QSize (mIconSize, mIconSize)); - button->setFixedSize (mButtonSize, mButtonSize); + button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + button->setIconSize(QSize(mIconSize, mIconSize)); + button->setFixedSize(mButtonSize, mButtonSize); - mLayout->addWidget (button); + mLayout->addWidget(button); - mButtons.insert (std::make_pair (button, id)); + mButtons.insert(std::make_pair(button, id)); - connect (button, SIGNAL (clicked()), this, SLOT (selected())); + connect(button, &ModeButton::clicked, this, &SceneToolMode::selected); - if (mButtons.size()==1) + if (mButtons.size() == 1) { mFirst = mCurrent = button; - setIcon (button->icon()); - button->setChecked (true); - adjustToolTip (button); - mCurrent->activate (mToolbar); + setIcon(button->icon()); + button->setChecked(true); + adjustToolTip(button); + mCurrent->activate(mToolbar); } } -CSVWidget::ModeButton *CSVWidget::SceneToolMode::getCurrent() +CSVWidget::ModeButton* CSVWidget::SceneToolMode::getCurrent() { return mCurrent; } std::string CSVWidget::SceneToolMode::getCurrentId() const { - return mButtons.find (mCurrent)->second; + auto currentButton = mButtons.find(mCurrent); + assert(currentButton != mButtons.end()); + return currentButton->second; } -void CSVWidget::SceneToolMode::setButton (const std::string& id) +void CSVWidget::SceneToolMode::setButton(const std::string& id) { - for (std::map::iterator iter = mButtons.begin(); - iter!=mButtons.end(); ++iter) - if (iter->second==id) + for (std::map::iterator iter = mButtons.begin(); iter != mButtons.end(); ++iter) + if (iter->second == id) { - setButton (iter); + setButton(iter); break; } } @@ -146,14 +158,13 @@ bool CSVWidget::SceneToolMode::event(QEvent* event) void CSVWidget::SceneToolMode::selected() { - std::map::iterator iter = - mButtons.find (dynamic_cast (sender())); + std::map::iterator iter = mButtons.find(dynamic_cast(sender())); - if (iter!=mButtons.end()) + if (iter != mButtons.end()) { if (!iter->first->hasKeepOpen()) mPanel->hide(); - setButton (iter); + setButton(iter); } } diff --git a/apps/opencs/view/widget/scenetoolmode.hpp b/apps/opencs/view/widget/scenetoolmode.hpp index 896a6d9c665..33e19965921 100644 --- a/apps/opencs/view/widget/scenetoolmode.hpp +++ b/apps/opencs/view/widget/scenetoolmode.hpp @@ -4,78 +4,81 @@ #include "scenetool.hpp" #include +#include class QHBoxLayout; class QMenu; class QEvent; +class QContextMenuEvent; +class QObject; +class QPoint; +class QWidget; namespace CSVWidget { class SceneToolbar; class ModeButton; + class PushButton; ///< \brief Mode selector tool class SceneToolMode : public SceneTool { - Q_OBJECT + Q_OBJECT - QWidget *mPanel; - QHBoxLayout *mLayout; - std::map mButtons; // widget, id - int mButtonSize; - int mIconSize; - QString mToolTip; - PushButton *mFirst; - ModeButton *mCurrent; - SceneToolbar *mToolbar; + QWidget* mPanel; + QHBoxLayout* mLayout; + std::map mButtons; // widget, id + int mButtonSize; + int mIconSize; + QString mToolTip; + PushButton* mFirst; + ModeButton* mCurrent; + SceneToolbar* mToolbar; - void adjustToolTip (const ModeButton *activeMode); + void adjustToolTip(const ModeButton* activeMode); - void contextMenuEvent (QContextMenuEvent *event) override; + void contextMenuEvent(QContextMenuEvent* event) override; - /// Add context menu items to \a menu. Default-implementation: Pass on request to - /// current mode button or return false, if there is no current mode button. - /// - /// \attention menu can be a 0-pointer - /// - /// \return Have there been any menu items to be added (if menu is 0 and there - /// items to be added, the function must return true anyway. - virtual bool createContextMenu (QMenu *menu); + /// Add context menu items to \a menu. Default-implementation: Pass on request to + /// current mode button or return false, if there is no current mode button. + /// + /// \attention menu can be a 0-pointer + /// + /// \return Have there been any menu items to be added (if menu is 0 and there + /// items to be added, the function must return true anyway. + virtual bool createContextMenu(QMenu* menu); - void setButton (std::map::iterator iter); + void setButton(std::map::iterator iter); - protected: + protected: + bool event(QEvent* event) override; - bool event(QEvent* event) override; + public: + SceneToolMode(SceneToolbar* parent, const QString& toolTip); - public: + void showPanel(const QPoint& position) override; - SceneToolMode (SceneToolbar *parent, const QString& toolTip); + void addButton(const std::string& icon, const std::string& id, const QString& tooltip = ""); - void showPanel (const QPoint& position) override; + /// The ownership of \a button is transferred to *this. + void addButton(ModeButton* button, const std::string& id); - void addButton (const std::string& icon, const std::string& id, - const QString& tooltip = ""); + /// Will return a 0-pointer only if the mode does not have any buttons yet. + ModeButton* getCurrent(); - /// The ownership of \a button is transferred to *this. - void addButton (ModeButton *button, const std::string& id); + /// Must not be called if there aren't any buttons yet. + std::string getCurrentId() const; - /// Will return a 0-pointer only if the mode does not have any buttons yet. - ModeButton *getCurrent(); + /// Manually change the current mode + void setButton(const std::string& id); - /// Must not be called if there aren't any buttons yet. - std::string getCurrentId() const; + signals: - /// Manually change the current mode - void setButton (const std::string& id); + void modeChanged(const std::string& id); - signals: + private slots: - void modeChanged (const std::string& id); - - private slots: - - void selected(); + void selected(); }; } diff --git a/apps/opencs/view/widget/scenetoolrun.cpp b/apps/opencs/view/widget/scenetoolrun.cpp index 24bcf3f1367..59d5bf4d5ef 100644 --- a/apps/opencs/view/widget/scenetoolrun.cpp +++ b/apps/opencs/view/widget/scenetoolrun.cpp @@ -2,124 +2,136 @@ #include +#include #include -#include #include #include -#include +#include + +#include + +#include +#include + +class QPoint; + +namespace CSVWidget +{ + class SceneToolbar; +} void CSVWidget::SceneToolRun::adjustToolTips() { QString toolTip = mToolTip; - if (mSelected==mProfiles.end()) + if (mSelected == mProfiles.end()) toolTip += "

      No debug profile selected (function disabled)"; else { - toolTip += "

      Debug profile: " + QString::fromUtf8 (mSelected->c_str()); + toolTip += "

      Debug profile: " + QString::fromUtf8(mSelected->c_str()); toolTip += "

      (right click to switch to a different profile)"; } - setToolTip (toolTip); + setToolTip(toolTip); } void CSVWidget::SceneToolRun::updateIcon() { - setDisabled (mSelected==mProfiles.end()); + setDisabled(mSelected == mProfiles.end()); } void CSVWidget::SceneToolRun::updatePanel() { - mTable->setRowCount (static_cast(mProfiles.size())); + mTable->setRowCount(static_cast(mProfiles.size())); int i = 0; - for (std::set::const_iterator iter (mProfiles.begin()); iter!=mProfiles.end(); - ++iter, ++i) + for (std::set::const_iterator iter(mProfiles.begin()); iter != mProfiles.end(); ++iter, ++i) { - mTable->setItem (i, 0, new QTableWidgetItem (QString::fromUtf8 (iter->c_str()))); + mTable->setItem(i, 0, new QTableWidgetItem(QString::fromUtf8(iter->c_str()))); - mTable->setItem (i, 1, new QTableWidgetItem ( - QApplication::style()->standardIcon (QStyle::SP_TitleBarCloseButton), "")); + mTable->setItem( + i, 1, new QTableWidgetItem(QApplication::style()->standardIcon(QStyle::SP_TitleBarCloseButton), "")); } } -CSVWidget::SceneToolRun::SceneToolRun (SceneToolbar *parent, const QString& toolTip, - const QString& icon, const std::vector& profiles) -: SceneTool (parent, Type_TopAction), mProfiles (profiles.begin(), profiles.end()), - mSelected (mProfiles.begin()), mToolTip (toolTip) +CSVWidget::SceneToolRun::SceneToolRun( + SceneToolbar* parent, const QString& toolTip, const QString& icon, const std::vector& profiles) + : SceneTool(parent, Type_TopAction) + , mProfiles(profiles.begin(), profiles.end()) + , mSelected(mProfiles.begin()) + , mToolTip(toolTip) { - setIcon (QIcon (icon)); + setIcon(Misc::ScalableIcon::load(icon)); updateIcon(); adjustToolTips(); - mPanel = new QFrame (this, Qt::Popup); + mPanel = new QFrame(this, Qt::Popup); - QHBoxLayout *layout = new QHBoxLayout (mPanel); + QHBoxLayout* layout = new QHBoxLayout(mPanel); - layout->setContentsMargins (QMargins (0, 0, 0, 0)); + layout->setContentsMargins(QMargins(0, 0, 0, 0)); - mTable = new QTableWidget (0, 2, this); + mTable = new QTableWidget(0, 2, this); - mTable->setShowGrid (false); + mTable->setShowGrid(false); mTable->verticalHeader()->hide(); mTable->horizontalHeader()->hide(); - mTable->horizontalHeader()->setSectionResizeMode (0, QHeaderView::Stretch); - mTable->horizontalHeader()->setSectionResizeMode (1, QHeaderView::ResizeToContents); - mTable->setSelectionMode (QAbstractItemView::NoSelection); + mTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + mTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + mTable->setSelectionMode(QAbstractItemView::NoSelection); - layout->addWidget (mTable); + layout->addWidget(mTable); - connect (mTable, SIGNAL (clicked (const QModelIndex&)), - this, SLOT (clicked (const QModelIndex&))); + connect(mTable, &QTableWidget::clicked, this, &SceneToolRun::clicked); } -void CSVWidget::SceneToolRun::showPanel (const QPoint& position) +void CSVWidget::SceneToolRun::showPanel(const QPoint& position) { updatePanel(); - mPanel->move (position); + mPanel->move(position); mPanel->show(); } void CSVWidget::SceneToolRun::activate() { - if (mSelected!=mProfiles.end()) - emit runRequest (*mSelected); + if (mSelected != mProfiles.end()) + emit runRequest(*mSelected); } -void CSVWidget::SceneToolRun::removeProfile (const std::string& profile) +void CSVWidget::SceneToolRun::removeProfile(const std::string& profile) { - std::set::iterator iter = mProfiles.find (profile); + std::set::iterator iter = mProfiles.find(profile); - if (iter!=mProfiles.end()) + if (iter != mProfiles.end()) { - if (iter==mSelected) + if (iter == mSelected) { - if (iter!=mProfiles.begin()) + if (iter != mProfiles.begin()) --mSelected; else ++mSelected; } - mProfiles.erase (iter); + mProfiles.erase(iter); - if (mSelected==mProfiles.end()) + if (mSelected == mProfiles.end()) updateIcon(); adjustToolTips(); } } -void CSVWidget::SceneToolRun::addProfile (const std::string& profile) +void CSVWidget::SceneToolRun::addProfile(const std::string& profile) { - std::set::iterator iter = mProfiles.find (profile); + std::set::iterator iter = mProfiles.find(profile); - if (iter==mProfiles.end()) + if (iter == mProfiles.end()) { - mProfiles.insert (profile); + mProfiles.insert(profile); - if (mSelected==mProfiles.end()) + if (mSelected == mProfiles.end()) { mSelected = mProfiles.begin(); updateIcon(); @@ -129,22 +141,22 @@ void CSVWidget::SceneToolRun::addProfile (const std::string& profile) } } -void CSVWidget::SceneToolRun::clicked (const QModelIndex& index) +void CSVWidget::SceneToolRun::clicked(const QModelIndex& index) { - if (index.column()==0) + if (index.column() == 0) { // select profile mSelected = mProfiles.begin(); - std::advance (mSelected, index.row()); + std::advance(mSelected, index.row()); mPanel->hide(); adjustToolTips(); } - else if (index.column()==1) + else if (index.column() == 1) { // remove profile from list std::set::iterator iter = mProfiles.begin(); - std::advance (iter, index.row()); - removeProfile (*iter); + std::advance(iter, index.row()); + removeProfile(*iter); updatePanel(); } } diff --git a/apps/opencs/view/widget/scenetoolrun.hpp b/apps/opencs/view/widget/scenetoolrun.hpp index 4a90aa7c061..6fd3c3822a8 100644 --- a/apps/opencs/view/widget/scenetoolrun.hpp +++ b/apps/opencs/view/widget/scenetoolrun.hpp @@ -3,59 +3,62 @@ #include #include +#include #include "scenetool.hpp" class QFrame; class QTableWidget; class QModelIndex; +class QObject; +class QPoint; namespace CSVWidget { + class SceneToolbar; + class SceneToolRun : public SceneTool { - Q_OBJECT - - std::set mProfiles; - std::set::iterator mSelected; - QString mToolTip; - QFrame *mPanel; - QTableWidget *mTable; - - private: + Q_OBJECT - void adjustToolTips(); + std::set mProfiles; + std::set::iterator mSelected; + QString mToolTip; + QFrame* mPanel; + QTableWidget* mTable; - void updateIcon(); + private: + void adjustToolTips(); - void updatePanel(); + void updateIcon(); - public: + void updatePanel(); - SceneToolRun (SceneToolbar *parent, const QString& toolTip, const QString& icon, - const std::vector& profiles); + public: + SceneToolRun(SceneToolbar* parent, const QString& toolTip, const QString& icon, + const std::vector& profiles); - void showPanel (const QPoint& position) override; + void showPanel(const QPoint& position) override; - void activate() override; + void activate() override; - /// \attention This function does not remove the profile from the profile selection - /// panel. - void removeProfile (const std::string& profile); + /// \attention This function does not remove the profile from the profile selection + /// panel. + void removeProfile(const std::string& profile); - /// \attention This function doe not add the profile to the profile selection - /// panel. This only happens when the panel is re-opened. - /// - /// \note Adding profiles that are already listed is a no-op. - void addProfile (const std::string& profile); + /// \attention This function doe not add the profile to the profile selection + /// panel. This only happens when the panel is re-opened. + /// + /// \note Adding profiles that are already listed is a no-op. + void addProfile(const std::string& profile); - private slots: + private slots: - void clicked (const QModelIndex& index); + void clicked(const QModelIndex& index); - signals: + signals: - void runRequest (const std::string& profile); + void runRequest(const std::string& profile); }; } diff --git a/apps/opencs/view/widget/scenetoolshapebrush.cpp b/apps/opencs/view/widget/scenetoolshapebrush.cpp index d0419a13ad4..d7034cac07d 100644 --- a/apps/opencs/view/widget/scenetoolshapebrush.cpp +++ b/apps/opencs/view/widget/scenetoolshapebrush.cpp @@ -1,35 +1,43 @@ #include "scenetoolshapebrush.hpp" -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include #include -#include +#include #include -#include -#include +#include +#include +#include +#include #include -#include +#include +#include #include +#include +#include +#include +#include + +#include + +#include +#include +#include #include "brushshapes.hpp" #include "scenetool.hpp" -#include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" -#include "../../model/world/commands.hpp" +namespace CSVWidget +{ + class SceneToolbar; +} + +namespace CSMDoc +{ + class Document; +} -CSVWidget::ShapeBrushSizeControls::ShapeBrushSizeControls(const QString &title, QWidget *parent) +CSVWidget::ShapeBrushSizeControls::ShapeBrushSizeControls(const QString& title, QWidget* parent) : QGroupBox(title, parent) { mBrushSizeSlider->setTickPosition(QSlider::TicksBothSides); @@ -40,44 +48,44 @@ CSVWidget::ShapeBrushSizeControls::ShapeBrushSizeControls(const QString &title, mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); mBrushSizeSpinBox->setSingleStep(1); - QHBoxLayout *layoutSliderSize = new QHBoxLayout; + QHBoxLayout* layoutSliderSize = new QHBoxLayout; layoutSliderSize->addWidget(mBrushSizeSlider); layoutSliderSize->addWidget(mBrushSizeSpinBox); - connect(mBrushSizeSlider, SIGNAL(valueChanged(int)), mBrushSizeSpinBox, SLOT(setValue(int))); - connect(mBrushSizeSpinBox, SIGNAL(valueChanged(int)), mBrushSizeSlider, SLOT(setValue(int))); + connect(mBrushSizeSlider, &QSlider::valueChanged, mBrushSizeSpinBox, &QSpinBox::setValue); + connect(mBrushSizeSpinBox, qOverload(&QSpinBox::valueChanged), mBrushSizeSlider, &QSlider::setValue); setLayout(layoutSliderSize); } -CSVWidget::ShapeBrushWindow::ShapeBrushWindow(CSMDoc::Document& document, QWidget *parent) - : QFrame(parent, Qt::Popup), - mDocument(document) +CSVWidget::ShapeBrushWindow::ShapeBrushWindow(CSMDoc::Document& document, QWidget* parent) + : QFrame(parent, Qt::Popup) + , mDocument(document) { - mButtonPoint = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-point")), "", this); - mButtonSquare = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-square")), "", this); - mButtonCircle = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-circle")), "", this); - mButtonCustom = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-custom")), "", this); + mButtonPoint = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-point"), "", this); + mButtonSquare = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-square"), "", this); + mButtonCircle = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-circle"), "", this); + mButtonCustom = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-custom"), "", this); mSizeSliders = new ShapeBrushSizeControls("Brush size", this); - QVBoxLayout *layoutMain = new QVBoxLayout; + QVBoxLayout* layoutMain = new QVBoxLayout; layoutMain->setSpacing(0); - layoutMain->setContentsMargins(4,0,4,4); + layoutMain->setContentsMargins(4, 0, 4, 4); - QHBoxLayout *layoutHorizontal = new QHBoxLayout; + QHBoxLayout* layoutHorizontal = new QHBoxLayout; layoutHorizontal->setSpacing(0); - layoutHorizontal->setContentsMargins (QMargins (0, 0, 0, 0)); + layoutHorizontal->setContentsMargins(QMargins(0, 0, 0, 0)); configureButtonInitialSettings(mButtonPoint); configureButtonInitialSettings(mButtonSquare); configureButtonInitialSettings(mButtonCircle); configureButtonInitialSettings(mButtonCustom); - mButtonPoint->setToolTip (toolTipPoint); - mButtonSquare->setToolTip (toolTipSquare); - mButtonCircle->setToolTip (toolTipCircle); - mButtonCustom->setToolTip (toolTipCustom); + mButtonPoint->setToolTip(toolTipPoint); + mButtonSquare->setToolTip(toolTipSquare); + mButtonCircle->setToolTip(toolTipCircle); + mButtonCustom->setToolTip(toolTipCustom); QButtonGroup* brushButtonGroup = new QButtonGroup(this); brushButtonGroup->addButton(mButtonPoint); @@ -101,8 +109,9 @@ CSVWidget::ShapeBrushWindow::ShapeBrushWindow(CSMDoc::Document& document, QWidge mToolSelector->addItem(tr("Height, lower (paint)")); mToolSelector->addItem(tr("Smooth (paint)")); mToolSelector->addItem(tr("Flatten (paint)")); + mToolSelector->addItem(tr("Equalize (paint)")); - QLabel *brushStrengthLabel = new QLabel(this); + QLabel* brushStrengthLabel = new QLabel(this); brushStrengthLabel->setText("Brush strength:"); mToolStrengthSlider = new QSlider(Qt::Horizontal); @@ -120,19 +129,19 @@ CSVWidget::ShapeBrushWindow::ShapeBrushWindow(CSMDoc::Document& document, QWidge setLayout(layoutMain); - connect(mButtonPoint, SIGNAL(clicked()), this, SLOT(setBrushShape())); - connect(mButtonSquare, SIGNAL(clicked()), this, SLOT(setBrushShape())); - connect(mButtonCircle, SIGNAL(clicked()), this, SLOT(setBrushShape())); - connect(mButtonCustom, SIGNAL(clicked()), this, SLOT(setBrushShape())); + connect(mButtonPoint, &QPushButton::clicked, this, &ShapeBrushWindow::setBrushShape); + connect(mButtonSquare, &QPushButton::clicked, this, &ShapeBrushWindow::setBrushShape); + connect(mButtonCircle, &QPushButton::clicked, this, &ShapeBrushWindow::setBrushShape); + connect(mButtonCustom, &QPushButton::clicked, this, &ShapeBrushWindow::setBrushShape); } -void CSVWidget::ShapeBrushWindow::configureButtonInitialSettings(QPushButton *button) +void CSVWidget::ShapeBrushWindow::configureButtonInitialSettings(QPushButton* button) { - button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); - button->setContentsMargins (QMargins (0, 0, 0, 0)); - button->setIconSize (QSize (48-6, 48-6)); - button->setFixedSize (48, 48); - button->setCheckable(true); + button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + button->setContentsMargins(QMargins(0, 0, 0, 0)); + button->setIconSize(QSize(48 - 6, 48 - 6)); + button->setFixedSize(48, 48); + button->setCheckable(true); } void CSVWidget::ShapeBrushWindow::setBrushSize(int brushSize) @@ -143,50 +152,51 @@ void CSVWidget::ShapeBrushWindow::setBrushSize(int brushSize) void CSVWidget::ShapeBrushWindow::setBrushShape() { - if(mButtonPoint->isChecked()) mBrushShape = BrushShape_Point; - if(mButtonSquare->isChecked()) mBrushShape = BrushShape_Square; - if(mButtonCircle->isChecked()) mBrushShape = BrushShape_Circle; - if(mButtonCustom->isChecked()) mBrushShape = BrushShape_Custom; + if (mButtonPoint->isChecked()) + mBrushShape = BrushShape_Point; + if (mButtonSquare->isChecked()) + mBrushShape = BrushShape_Square; + if (mButtonCircle->isChecked()) + mBrushShape = BrushShape_Circle; + if (mButtonCustom->isChecked()) + mBrushShape = BrushShape_Custom; emit passBrushShape(mBrushShape); } -void CSVWidget::SceneToolShapeBrush::adjustToolTips() -{ -} +void CSVWidget::SceneToolShapeBrush::adjustToolTips() {} -CSVWidget::SceneToolShapeBrush::SceneToolShapeBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document) -: SceneTool (parent, Type_TopAction), - mToolTip (toolTip), - mDocument (document), - mShapeBrushWindow(new ShapeBrushWindow(document, this)) +CSVWidget::SceneToolShapeBrush::SceneToolShapeBrush( + SceneToolbar* parent, const QString& toolTip, CSMDoc::Document& document) + : SceneTool(parent, Type_TopAction) + , mToolTip(toolTip) + , mDocument(document) + , mShapeBrushWindow(new ShapeBrushWindow(document, this)) { setAcceptDrops(true); - connect(mShapeBrushWindow, SIGNAL(passBrushShape(CSVWidget::BrushShape)), this, SLOT(setButtonIcon(CSVWidget::BrushShape))); + connect(mShapeBrushWindow, &ShapeBrushWindow::passBrushShape, this, &SceneToolShapeBrush::setButtonIcon); setButtonIcon(mShapeBrushWindow->mBrushShape); - mPanel = new QFrame (this, Qt::Popup); + mPanel = new QFrame(this, Qt::Popup); - QHBoxLayout *layout = new QHBoxLayout (mPanel); + QHBoxLayout* layout = new QHBoxLayout(mPanel); - layout->setContentsMargins (QMargins (0, 0, 0, 0)); + layout->setContentsMargins(QMargins(0, 0, 0, 0)); - mTable = new QTableWidget (0, 2, this); + mTable = new QTableWidget(0, 2, this); - mTable->setShowGrid (true); + mTable->setShowGrid(true); mTable->verticalHeader()->hide(); mTable->horizontalHeader()->hide(); - mTable->horizontalHeader()->setSectionResizeMode (0, QHeaderView::Stretch); - mTable->horizontalHeader()->setSectionResizeMode (1, QHeaderView::Stretch); - mTable->setSelectionMode (QAbstractItemView::NoSelection); - - layout->addWidget (mTable); + mTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + mTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + mTable->setSelectionMode(QAbstractItemView::NoSelection); - connect (mTable, SIGNAL (clicked (const QModelIndex&)), - this, SLOT (clicked (const QModelIndex&))); + layout->addWidget(mTable); + connect(mTable, &QTableWidget::clicked, this, &SceneToolShapeBrush::clicked); } -void CSVWidget::SceneToolShapeBrush::setButtonIcon (CSVWidget::BrushShape brushShape) +void CSVWidget::SceneToolShapeBrush::setButtonIcon(CSVWidget::BrushShape brushShape) { QString tooltip = "Change brush settings

      Currently selected: "; @@ -194,59 +204,55 @@ void CSVWidget::SceneToolShapeBrush::setButtonIcon (CSVWidget::BrushShape brushS { case BrushShape_Point: - setIcon (QIcon (QPixmap (":scenetoolbar/brush-point"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-point")); tooltip += mShapeBrushWindow->toolTipPoint; break; case BrushShape_Square: - setIcon (QIcon (QPixmap (":scenetoolbar/brush-square"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-square")); tooltip += mShapeBrushWindow->toolTipSquare; break; case BrushShape_Circle: - setIcon (QIcon (QPixmap (":scenetoolbar/brush-circle"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-circle")); tooltip += mShapeBrushWindow->toolTipCircle; break; case BrushShape_Custom: - setIcon (QIcon (QPixmap (":scenetoolbar/brush-custom"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-custom")); tooltip += mShapeBrushWindow->toolTipCustom; break; } - setToolTip (tooltip); + setToolTip(tooltip); } -void CSVWidget::SceneToolShapeBrush::showPanel (const QPoint& position) -{ -} +void CSVWidget::SceneToolShapeBrush::showPanel(const QPoint& position) {} -void CSVWidget::SceneToolShapeBrush::updatePanel () -{ -} +void CSVWidget::SceneToolShapeBrush::updatePanel() {} -void CSVWidget::SceneToolShapeBrush::clicked (const QModelIndex& index) -{ -} +void CSVWidget::SceneToolShapeBrush::clicked(const QModelIndex& index) {} -void CSVWidget::SceneToolShapeBrush::activate () +void CSVWidget::SceneToolShapeBrush::activate() { QPoint position = QCursor::pos(); - mShapeBrushWindow->mSizeSliders->mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); - mShapeBrushWindow->mSizeSliders->mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); - mShapeBrushWindow->move (position); + mShapeBrushWindow->mSizeSliders->mBrushSizeSlider->setRange( + 1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); + mShapeBrushWindow->mSizeSliders->mBrushSizeSpinBox->setRange( + 1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); + mShapeBrushWindow->move(position); mShapeBrushWindow->show(); } -void CSVWidget::SceneToolShapeBrush::dragEnterEvent (QDragEnterEvent *event) +void CSVWidget::SceneToolShapeBrush::dragEnterEvent(QDragEnterEvent* event) { emit passEvent(event); event->accept(); } -void CSVWidget::SceneToolShapeBrush::dropEvent (QDropEvent *event) +void CSVWidget::SceneToolShapeBrush::dropEvent(QDropEvent* event) { emit passEvent(event); event->accept(); diff --git a/apps/opencs/view/widget/scenetoolshapebrush.hpp b/apps/opencs/view/widget/scenetoolshapebrush.hpp index 3afd7f8b3b0..6143c8959f0 100644 --- a/apps/opencs/view/widget/scenetoolshapebrush.hpp +++ b/apps/opencs/view/widget/scenetoolshapebrush.hpp @@ -1,27 +1,30 @@ #ifndef CSV_WIDGET_SCENETOOLSHAPEBRUSH_H #define CSV_WIDGET_SCENETOOLSHAPEBRUSH_H -#include #include -#include - -#include -#include -#include -#include #include #include -#include -#include -#include +#include #ifndef Q_MOC_RUN #include "brushshapes.hpp" #include "scenetool.hpp" - -#include "../../model/doc/document.hpp" #endif +class QComboBox; +class QDragEnterEvent; +class QDropEvent; +class QModelIndex; +class QObject; +class QPoint; +class QPushButton; +class QWidget; + +namespace CSMDoc +{ + class Document; +} + class QTableWidget; namespace CSVRender @@ -31,17 +34,19 @@ namespace CSVRender namespace CSVWidget { + class SceneToolbar; + /// \brief Layout-box for some brush button settings class ShapeBrushSizeControls : public QGroupBox { Q_OBJECT - public: - ShapeBrushSizeControls(const QString &title, QWidget *parent); + public: + ShapeBrushSizeControls(const QString& title, QWidget* parent); - private: - QSlider *mBrushSizeSlider = new QSlider(Qt::Horizontal); - QSpinBox *mBrushSizeSpinBox = new QSpinBox; + private: + QSlider* mBrushSizeSlider = new QSlider(Qt::Horizontal); + QSpinBox* mBrushSizeSpinBox = new QSpinBox; friend class SceneToolShapeBrush; friend class CSVRender::TerrainShapeMode; @@ -52,75 +57,72 @@ namespace CSVWidget { Q_OBJECT - public: - - ShapeBrushWindow(CSMDoc::Document& document, QWidget *parent = nullptr); - void configureButtonInitialSettings(QPushButton *button); - - const QString toolTipPoint = "Paint single point"; - const QString toolTipSquare = "Paint with square brush"; - const QString toolTipCircle = "Paint with circle brush"; - const QString toolTipCustom = "Paint with custom brush, defined by terrain selection"; - - private: - CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; - int mBrushSize = 1; - CSMDoc::Document& mDocument; - QGroupBox *mHorizontalGroupBox; - QComboBox *mToolSelector; - QSlider *mToolStrengthSlider; - QPushButton *mButtonPoint; - QPushButton *mButtonSquare; - QPushButton *mButtonCircle; - QPushButton *mButtonCustom; - ShapeBrushSizeControls* mSizeSliders; + public: + ShapeBrushWindow(CSMDoc::Document& document, QWidget* parent = nullptr); + void configureButtonInitialSettings(QPushButton* button); + + const QString toolTipPoint = "Paint single point"; + const QString toolTipSquare = "Paint with square brush"; + const QString toolTipCircle = "Paint with circle brush"; + const QString toolTipCustom = "Paint with custom brush, defined by terrain selection"; + + private: + CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; + int mBrushSize = 1; + CSMDoc::Document& mDocument; + QGroupBox* mHorizontalGroupBox; + QComboBox* mToolSelector; + QSlider* mToolStrengthSlider; + QPushButton* mButtonPoint; + QPushButton* mButtonSquare; + QPushButton* mButtonCircle; + QPushButton* mButtonCustom; + ShapeBrushSizeControls* mSizeSliders; friend class SceneToolShapeBrush; friend class CSVRender::TerrainShapeMode; - public slots: - void setBrushShape(); - void setBrushSize(int brushSize); + public slots: + void setBrushShape(); + void setBrushSize(int brushSize); - signals: - void passBrushSize (int brushSize); - void passBrushShape(CSVWidget::BrushShape brushShape); + signals: + void passBrushSize(int brushSize); + void passBrushShape(CSVWidget::BrushShape brushShape); }; class SceneToolShapeBrush : public SceneTool { - Q_OBJECT - - QString mToolTip; - CSMDoc::Document& mDocument; - QFrame *mPanel; - QTableWidget *mTable; - ShapeBrushWindow *mShapeBrushWindow; - - private: + Q_OBJECT - void adjustToolTips(); + QString mToolTip; + CSMDoc::Document& mDocument; + QFrame* mPanel; + QTableWidget* mTable; + ShapeBrushWindow* mShapeBrushWindow; - public: + private: + void adjustToolTips(); - SceneToolShapeBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document); + public: + SceneToolShapeBrush(SceneToolbar* parent, const QString& toolTip, CSMDoc::Document& document); - void showPanel (const QPoint& position) override; - void updatePanel (); + void showPanel(const QPoint& position) override; + void updatePanel(); - void dropEvent (QDropEvent *event) override; - void dragEnterEvent (QDragEnterEvent *event) override; + void dropEvent(QDropEvent* event) override; + void dragEnterEvent(QDragEnterEvent* event) override; friend class CSVRender::TerrainShapeMode; - public slots: - void setButtonIcon(CSVWidget::BrushShape brushShape); - void clicked (const QModelIndex& index); - void activate() override; + public slots: + void setButtonIcon(CSVWidget::BrushShape brushShape); + void clicked(const QModelIndex& index); + void activate() override; - signals: - void passEvent(QDropEvent *event); - void passEvent(QDragEnterEvent *event); + signals: + void passEvent(QDropEvent* event); + void passEvent(QDragEnterEvent* event); }; } diff --git a/apps/opencs/view/widget/scenetooltexturebrush.cpp b/apps/opencs/view/widget/scenetooltexturebrush.cpp index 272a5de42e5..27d628a3847 100644 --- a/apps/opencs/view/widget/scenetooltexturebrush.cpp +++ b/apps/opencs/view/widget/scenetooltexturebrush.cpp @@ -1,42 +1,57 @@ #include "scenetooltexturebrush.hpp" -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include #include -#include #include -#include -#include +#include +#include +#include +#include #include -#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include + +#include +#include +#include #include "scenetool.hpp" +#include +#include +#include +#include +#include +#include +#include + +#include + #include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idcollection.hpp" #include "../../model/world/idtable.hpp" -#include "../../model/world/landtexture.hpp" #include "../../model/world/universalid.hpp" +namespace CSVWidget +{ + class SceneToolbar; +} -CSVWidget::BrushSizeControls::BrushSizeControls(const QString &title, QWidget *parent) - : QGroupBox(title, parent), - mLayoutSliderSize(new QHBoxLayout), - mBrushSizeSlider(new QSlider(Qt::Horizontal)), - mBrushSizeSpinBox(new QSpinBox) +CSVWidget::BrushSizeControls::BrushSizeControls(const QString& title, QWidget* parent) + : QGroupBox(title, parent) + , mLayoutSliderSize(new QHBoxLayout) + , mBrushSizeSlider(new QSlider(Qt::Horizontal)) + , mBrushSizeSpinBox(new QSpinBox) { mBrushSizeSlider->setTickPosition(QSlider::TicksBothSides); mBrushSizeSlider->setTickInterval(10); @@ -49,56 +64,58 @@ CSVWidget::BrushSizeControls::BrushSizeControls(const QString &title, QWidget *p mLayoutSliderSize->addWidget(mBrushSizeSlider); mLayoutSliderSize->addWidget(mBrushSizeSpinBox); - connect(mBrushSizeSlider, SIGNAL(valueChanged(int)), mBrushSizeSpinBox, SLOT(setValue(int))); - connect(mBrushSizeSpinBox, SIGNAL(valueChanged(int)), mBrushSizeSlider, SLOT(setValue(int))); + connect(mBrushSizeSlider, &QSlider::valueChanged, mBrushSizeSpinBox, &QSpinBox::setValue); + connect(mBrushSizeSpinBox, qOverload(&QSpinBox::valueChanged), mBrushSizeSlider, &QSlider::setValue); setLayout(mLayoutSliderSize); } -CSVWidget::TextureBrushWindow::TextureBrushWindow(CSMDoc::Document& document, QWidget *parent) - : QFrame(parent, Qt::Popup), - mDocument(document) +CSVWidget::TextureBrushWindow::TextureBrushWindow(CSMDoc::Document& document, QWidget* parent) + : QFrame(parent, Qt::Popup) + , mDocument(document) { - mBrushTextureLabel = "Selected texture: " + mBrushTexture + " "; + mBrushTextureLabel = "Selected texture: " + mBrushTexture.getRefIdString() + " "; - CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); - int index = landtexturesCollection.searchId(mBrushTexture); + const int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { - mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(index, landTextureFilename).value()); - } else + mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel) + + landtexturesCollection.getData(index, landTextureFilename).value()); + } + else { mBrushTextureLabel = "No selected texture or invalid texture"; mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel)); } - mButtonPoint = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-point")), "", this); - mButtonSquare = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-square")), "", this); - mButtonCircle = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-circle")), "", this); - mButtonCustom = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-custom")), "", this); + mButtonPoint = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-point"), "", this); + mButtonSquare = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-square"), "", this); + mButtonCircle = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-circle"), "", this); + mButtonCustom = new QPushButton(Misc::ScalableIcon::load(":scenetoolbar/brush-custom"), "", this); mSizeSliders = new BrushSizeControls("Brush size", this); - QVBoxLayout *layoutMain = new QVBoxLayout; + QVBoxLayout* layoutMain = new QVBoxLayout; layoutMain->setSpacing(0); - layoutMain->setContentsMargins(4,0,4,4); + layoutMain->setContentsMargins(4, 0, 4, 4); - QHBoxLayout *layoutHorizontal = new QHBoxLayout; + QHBoxLayout* layoutHorizontal = new QHBoxLayout; layoutHorizontal->setSpacing(0); - layoutHorizontal->setContentsMargins (QMargins (0, 0, 0, 0)); + layoutHorizontal->setContentsMargins(QMargins(0, 0, 0, 0)); configureButtonInitialSettings(mButtonPoint); configureButtonInitialSettings(mButtonSquare); configureButtonInitialSettings(mButtonCircle); configureButtonInitialSettings(mButtonCustom); - mButtonPoint->setToolTip (toolTipPoint); - mButtonSquare->setToolTip (toolTipSquare); - mButtonCircle->setToolTip (toolTipCircle); - mButtonCustom->setToolTip (toolTipCustom); + mButtonPoint->setToolTip(toolTipPoint); + mButtonSquare->setToolTip(toolTipSquare); + mButtonCircle->setToolTip(toolTipCircle); + mButtonCustom->setToolTip(toolTipCustom); QButtonGroup* brushButtonGroup = new QButtonGroup(this); brushButtonGroup->addButton(mButtonPoint); @@ -122,75 +139,53 @@ CSVWidget::TextureBrushWindow::TextureBrushWindow(CSMDoc::Document& document, QW setLayout(layoutMain); - connect(mButtonPoint, SIGNAL(clicked()), this, SLOT(setBrushShape())); - connect(mButtonSquare, SIGNAL(clicked()), this, SLOT(setBrushShape())); - connect(mButtonCircle, SIGNAL(clicked()), this, SLOT(setBrushShape())); - connect(mButtonCustom, SIGNAL(clicked()), this, SLOT(setBrushShape())); + connect(mButtonPoint, &QPushButton::clicked, this, &TextureBrushWindow::setBrushShape); + connect(mButtonSquare, &QPushButton::clicked, this, &TextureBrushWindow::setBrushShape); + connect(mButtonCircle, &QPushButton::clicked, this, &TextureBrushWindow::setBrushShape); + connect(mButtonCustom, &QPushButton::clicked, this, &TextureBrushWindow::setBrushShape); } -void CSVWidget::TextureBrushWindow::configureButtonInitialSettings(QPushButton *button) +void CSVWidget::TextureBrushWindow::configureButtonInitialSettings(QPushButton* button) { - button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); - button->setContentsMargins (QMargins (0, 0, 0, 0)); - button->setIconSize (QSize (48-6, 48-6)); - button->setFixedSize (48, 48); - button->setCheckable(true); + button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + button->setContentsMargins(QMargins(0, 0, 0, 0)); + button->setIconSize(QSize(48 - 6, 48 - 6)); + button->setFixedSize(48, 48); + button->setCheckable(true); } -void CSVWidget::TextureBrushWindow::setBrushTexture(std::string brushTexture) +void CSVWidget::TextureBrushWindow::setBrushTexture(ESM::RefId brushTexture) { - CSMWorld::IdTable& ltexTable = dynamic_cast ( - *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures)); + CSMWorld::IdTable& ltexTable = dynamic_cast( + *mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); QUndoStack& undoStack = mDocument.getUndoStack(); - CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); - int index = 0; - int pluginInDragged = 0; - CSMWorld::LandTexture::parseUniqueRecordId(brushTexture, pluginInDragged, index); - std::string newBrushTextureId = CSMWorld::LandTexture::createUniqueRecordId(0, index); - int rowInBase = landtexturesCollection.searchId(brushTexture); - int rowInNew = landtexturesCollection.searchId(newBrushTextureId); - - // Check if texture exists in current plugin, and clone if id found in base, otherwise reindex the texture - // TO-DO: Handle case when texture is not found in neither base or plugin properly (finding new index is not enough) - // TO-DO: Handle conflicting plugins properly - if (rowInNew == -1) + int row = landtexturesCollection.getIndex(brushTexture); + const auto& record = landtexturesCollection.getRecord(row); + + if (!record.isDeleted()) { - if (rowInBase == -1) + // Ensure the texture is defined by the current plugin + if (!record.isModified()) { - int counter=0; - bool freeIndexFound = false; - const int maxCounter = std::numeric_limits::max() - 1; - do { - newBrushTextureId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); - if (landtexturesCollection.searchId(brushTexture) != -1 && - landtexturesCollection.getRecord(brushTexture).isDeleted() == 0 && - landtexturesCollection.searchId(newBrushTextureId) != -1 && - landtexturesCollection.getRecord(newBrushTextureId).isDeleted() == 0) - counter = (counter + 1) % maxCounter; - else freeIndexFound = true; - } while (freeIndexFound == false || counter < maxCounter); + CSMWorld::CommandMacro macro(undoStack); + macro.push(new CSMWorld::TouchCommand(ltexTable, brushTexture.getRefIdString())); } - - undoStack.beginMacro ("Add land texture record"); - undoStack.push (new CSMWorld::CloneCommand (ltexTable, brushTexture, newBrushTextureId, CSMWorld::UniversalId::Type_LandTexture)); - undoStack.endMacro(); + mBrushTextureLabel = "Selected texture: " + brushTexture.getRefIdString() + " "; + mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel) + + landtexturesCollection.getData(row, landTextureFilename).value()); } - - if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) - { - mBrushTextureLabel = "Selected texture: " + newBrushTextureId + " "; - mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(index, landTextureFilename).value()); - } else + else { - newBrushTextureId = ""; + brushTexture = {}; mBrushTextureLabel = "No selected texture or invalid texture"; mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel)); } - mBrushTexture = newBrushTextureId; + mBrushTexture = brushTexture; emit passTextureId(mBrushTexture); emit passBrushShape(mBrushShape); // updates the icon tooltip @@ -215,46 +210,42 @@ void CSVWidget::TextureBrushWindow::setBrushShape() emit passBrushShape(mBrushShape); } -void CSVWidget::SceneToolTextureBrush::adjustToolTips() -{ -} +void CSVWidget::SceneToolTextureBrush::adjustToolTips() {} -CSVWidget::SceneToolTextureBrush::SceneToolTextureBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document) -: SceneTool (parent, Type_TopAction), - mToolTip (toolTip), - mDocument (document), - mTextureBrushWindow(new TextureBrushWindow(document, this)) +CSVWidget::SceneToolTextureBrush::SceneToolTextureBrush( + SceneToolbar* parent, const QString& toolTip, CSMDoc::Document& document) + : SceneTool(parent, Type_TopAction) + , mToolTip(toolTip) + , mDocument(document) + , mTextureBrushWindow(new TextureBrushWindow(document, this)) { mBrushHistory.resize(1); - mBrushHistory[0] = "L0#0"; setAcceptDrops(true); - connect(mTextureBrushWindow, SIGNAL(passBrushShape(CSVWidget::BrushShape)), this, SLOT(setButtonIcon(CSVWidget::BrushShape))); + connect(mTextureBrushWindow, &TextureBrushWindow::passBrushShape, this, &SceneToolTextureBrush::setButtonIcon); setButtonIcon(mTextureBrushWindow->mBrushShape); - mPanel = new QFrame (this, Qt::Popup); + mPanel = new QFrame(this, Qt::Popup); - QHBoxLayout *layout = new QHBoxLayout (mPanel); + QHBoxLayout* layout = new QHBoxLayout(mPanel); - layout->setContentsMargins (QMargins (0, 0, 0, 0)); + layout->setContentsMargins(QMargins(0, 0, 0, 0)); - mTable = new QTableWidget (0, 2, this); + mTable = new QTableWidget(0, 2, this); - mTable->setShowGrid (true); + mTable->setShowGrid(true); mTable->verticalHeader()->hide(); mTable->horizontalHeader()->hide(); - mTable->horizontalHeader()->setSectionResizeMode (0, QHeaderView::Stretch); - mTable->horizontalHeader()->setSectionResizeMode (1, QHeaderView::Stretch); - mTable->setSelectionMode (QAbstractItemView::NoSelection); + mTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + mTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + mTable->setSelectionMode(QAbstractItemView::NoSelection); - layout->addWidget (mTable); - - connect (mTable, SIGNAL (clicked (const QModelIndex&)), - this, SLOT (clicked (const QModelIndex&))); + layout->addWidget(mTable); + connect(mTable, &QTableWidget::clicked, this, &SceneToolTextureBrush::clicked); } -void CSVWidget::SceneToolTextureBrush::setButtonIcon (CSVWidget::BrushShape brushShape) +void CSVWidget::SceneToolTextureBrush::setButtonIcon(CSVWidget::BrushShape brushShape) { QString tooltip = "Change brush settings

      Currently selected: "; @@ -262,91 +253,95 @@ void CSVWidget::SceneToolTextureBrush::setButtonIcon (CSVWidget::BrushShape brus { case BrushShape_Point: - setIcon (QIcon (QPixmap (":scenetoolbar/brush-point"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-point")); tooltip += mTextureBrushWindow->toolTipPoint; break; case BrushShape_Square: - setIcon (QIcon (QPixmap (":scenetoolbar/brush-square"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-square")); tooltip += mTextureBrushWindow->toolTipSquare; break; case BrushShape_Circle: - setIcon (QIcon (QPixmap (":scenetoolbar/brush-circle"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-circle")); tooltip += mTextureBrushWindow->toolTipCircle; break; case BrushShape_Custom: - setIcon (QIcon (QPixmap (":scenetoolbar/brush-custom"))); + setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-custom")); tooltip += mTextureBrushWindow->toolTipCustom; break; } tooltip += "

      (right click to access of previously used brush settings)"; - - CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); - int index = landtexturesCollection.searchId(mTextureBrushWindow->mBrushTexture); + const int index = landtexturesCollection.searchId(mTextureBrushWindow->mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { - tooltip += "

      Selected texture: " + QString::fromStdString(mTextureBrushWindow->mBrushTexture) + " "; + tooltip += "

      Selected texture: " + QString::fromStdString(mTextureBrushWindow->mBrushTexture.getRefIdString()) + + " "; tooltip += landtexturesCollection.getData(index, landTextureFilename).value(); - } else + } + else { tooltip += "

      No selected texture or invalid texture"; } tooltip += "
      (drop texture here to change)"; - setToolTip (tooltip); + setToolTip(tooltip); } -void CSVWidget::SceneToolTextureBrush::showPanel (const QPoint& position) +void CSVWidget::SceneToolTextureBrush::showPanel(const QPoint& position) { updatePanel(); - mPanel->move (position); + mPanel->move(position); mPanel->show(); } void CSVWidget::SceneToolTextureBrush::updatePanel() { - mTable->setRowCount (mBrushHistory.size()); + mTable->setRowCount(mBrushHistory.size()); - for (int i = mBrushHistory.size()-1; i >= 0; --i) + for (int i = mBrushHistory.size() - 1; i >= 0; --i) { - CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); + CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); - int index = landtexturesCollection.searchId(mBrushHistory[i]); + const int index = landtexturesCollection.searchId(mBrushHistory[i]); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { - mTable->setItem (i, 1, new QTableWidgetItem (landtexturesCollection.getData(index, landTextureFilename).value())); - mTable->setItem (i, 0, new QTableWidgetItem (QString::fromStdString(mBrushHistory[i]))); - } else + mTable->setItem(i, 1, + new QTableWidgetItem(landtexturesCollection.getData(index, landTextureFilename).value())); + mTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(mBrushHistory[i].getRefIdString()))); + } + else { - mTable->setItem (i, 1, new QTableWidgetItem ("Invalid/deleted texture")); - mTable->setItem (i, 0, new QTableWidgetItem (QString::fromStdString(mBrushHistory[i]))); + mTable->setItem(i, 1, new QTableWidgetItem("Invalid/deleted texture")); + mTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(mBrushHistory[i].getRefIdString()))); } } } -void CSVWidget::SceneToolTextureBrush::updateBrushHistory (const std::string& brushTexture) +void CSVWidget::SceneToolTextureBrush::updateBrushHistory(ESM::RefId brushTexture) { mBrushHistory.insert(mBrushHistory.begin(), brushTexture); - if(mBrushHistory.size() > 5) mBrushHistory.pop_back(); + if (mBrushHistory.size() > 5) + mBrushHistory.pop_back(); } -void CSVWidget::SceneToolTextureBrush::clicked (const QModelIndex& index) +void CSVWidget::SceneToolTextureBrush::clicked(const QModelIndex& index) { - if (index.column()==0 || index.column()==1) + if (index.column() == 0 || index.column() == 1) { - std::string brushTexture = mBrushHistory[index.row()]; + ESM::RefId brushTexture = mBrushHistory[index.row()]; std::swap(mBrushHistory[index.row()], mBrushHistory[0]); mTextureBrushWindow->setBrushTexture(brushTexture); emit passTextureId(brushTexture); @@ -355,21 +350,23 @@ void CSVWidget::SceneToolTextureBrush::clicked (const QModelIndex& index) } } -void CSVWidget::SceneToolTextureBrush::activate () +void CSVWidget::SceneToolTextureBrush::activate() { QPoint position = QCursor::pos(); - mTextureBrushWindow->mSizeSliders->mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); - mTextureBrushWindow->mSizeSliders->mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); - mTextureBrushWindow->move (position); + mTextureBrushWindow->mSizeSliders->mBrushSizeSlider->setRange( + 1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); + mTextureBrushWindow->mSizeSliders->mBrushSizeSpinBox->setRange( + 1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); + mTextureBrushWindow->move(position); mTextureBrushWindow->show(); } -void CSVWidget::SceneToolTextureBrush::dragEnterEvent (QDragEnterEvent *event) +void CSVWidget::SceneToolTextureBrush::dragEnterEvent(QDragEnterEvent* event) { emit passEvent(event); event->accept(); } -void CSVWidget::SceneToolTextureBrush::dropEvent (QDropEvent *event) +void CSVWidget::SceneToolTextureBrush::dropEvent(QDropEvent* event) { emit passEvent(event); event->accept(); diff --git a/apps/opencs/view/widget/scenetooltexturebrush.hpp b/apps/opencs/view/widget/scenetooltexturebrush.hpp index 5f42800cb30..09c7ef43f0b 100644 --- a/apps/opencs/view/widget/scenetooltexturebrush.hpp +++ b/apps/opencs/view/widget/scenetooltexturebrush.hpp @@ -1,135 +1,137 @@ #ifndef CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H #define CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H -#include #include -#include - -#include -#include -#include #include -#include -#include -#include -#include #ifndef Q_MOC_RUN #include "brushshapes.hpp" #include "scenetool.hpp" - -#include "../../model/doc/document.hpp" #endif +#include + class QTableWidget; +class QDragEnterEvent; +class QDropEvent; +class QHBoxLayout; +class QLabel; +class QModelIndex; +class QObject; +class QPoint; +class QPushButton; +class QSlider; +class QSpinBox; +class QWidget; namespace CSVRender { class TerrainTextureMode; } +namespace CSMDoc +{ + class Document; +} + namespace CSVWidget { - class SceneToolTextureBrush; + class SceneToolbar; /// \brief Layout-box for some brush button settings class BrushSizeControls : public QGroupBox { Q_OBJECT - public: - BrushSizeControls(const QString &title, QWidget *parent); + public: + BrushSizeControls(const QString& title, QWidget* parent); - private: - QHBoxLayout *mLayoutSliderSize; - QSlider *mBrushSizeSlider; - QSpinBox *mBrushSizeSpinBox; + private: + QHBoxLayout* mLayoutSliderSize; + QSlider* mBrushSizeSlider; + QSpinBox* mBrushSizeSpinBox; friend class SceneToolTextureBrush; friend class CSVRender::TerrainTextureMode; }; - class SceneToolTextureBrush; - /// \brief Brush settings window class TextureBrushWindow : public QFrame { Q_OBJECT - public: - TextureBrushWindow(CSMDoc::Document& document, QWidget *parent = nullptr); - void configureButtonInitialSettings(QPushButton *button); - - const QString toolTipPoint = "Paint single point"; - const QString toolTipSquare = "Paint with square brush"; - const QString toolTipCircle = "Paint with circle brush"; - const QString toolTipCustom = "Paint custom selection (not implemented yet)"; - - private: - CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; - int mBrushSize = 1; - std::string mBrushTexture = "L0#0"; - CSMDoc::Document& mDocument; - QLabel *mSelectedBrush; - QGroupBox *mHorizontalGroupBox; - std::string mBrushTextureLabel; - QPushButton *mButtonPoint; - QPushButton *mButtonSquare; - QPushButton *mButtonCircle; - QPushButton *mButtonCustom; - BrushSizeControls* mSizeSliders; + public: + TextureBrushWindow(CSMDoc::Document& document, QWidget* parent = nullptr); + void configureButtonInitialSettings(QPushButton* button); + + const QString toolTipPoint = "Paint single point"; + const QString toolTipSquare = "Paint with square brush"; + const QString toolTipCircle = "Paint with circle brush"; + const QString toolTipCustom = "Paint custom selection (not implemented yet)"; + + private: + CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; + int mBrushSize = 1; + ESM::RefId mBrushTexture; + CSMDoc::Document& mDocument; + QLabel* mSelectedBrush; + QGroupBox* mHorizontalGroupBox; + std::string mBrushTextureLabel; + QPushButton* mButtonPoint; + QPushButton* mButtonSquare; + QPushButton* mButtonCircle; + QPushButton* mButtonCustom; + BrushSizeControls* mSizeSliders; friend class SceneToolTextureBrush; friend class CSVRender::TerrainTextureMode; - public slots: - void setBrushTexture(std::string brushTexture); - void setBrushShape(); - void setBrushSize(int brushSize); + public slots: + void setBrushTexture(ESM::RefId brushTexture); + void setBrushShape(); + void setBrushSize(int brushSize); - signals: - void passBrushSize (int brushSize); - void passBrushShape(CSVWidget::BrushShape brushShape); - void passTextureId(std::string brushTexture); + signals: + void passBrushSize(int brushSize); + void passBrushShape(CSVWidget::BrushShape brushShape); + void passTextureId(ESM::RefId brushTexture); }; class SceneToolTextureBrush : public SceneTool { - Q_OBJECT - - QString mToolTip; - CSMDoc::Document& mDocument; - QFrame *mPanel; - QTableWidget *mTable; - std::vector mBrushHistory; - TextureBrushWindow *mTextureBrushWindow; - - private: + Q_OBJECT - void adjustToolTips(); + QString mToolTip; + CSMDoc::Document& mDocument; + QFrame* mPanel; + QTableWidget* mTable; + std::vector mBrushHistory; + TextureBrushWindow* mTextureBrushWindow; - public: + private: + void adjustToolTips(); - SceneToolTextureBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document); + public: + SceneToolTextureBrush(SceneToolbar* parent, const QString& toolTip, CSMDoc::Document& document); - void showPanel (const QPoint& position) override; - void updatePanel (); + void showPanel(const QPoint& position) override; + void updatePanel(); - void dropEvent (QDropEvent *event) override; - void dragEnterEvent (QDragEnterEvent *event) override; + void dropEvent(QDropEvent* event) override; + void dragEnterEvent(QDragEnterEvent* event) override; friend class CSVRender::TerrainTextureMode; - public slots: - void setButtonIcon(CSVWidget::BrushShape brushShape); - void updateBrushHistory (const std::string& mBrushTexture); - void clicked (const QModelIndex& index); - void activate() override; + public slots: + void setButtonIcon(CSVWidget::BrushShape brushShape); + void updateBrushHistory(ESM::RefId mBrushTexture); + void clicked(const QModelIndex& index); + void activate() override; - signals: - void passEvent(QDropEvent *event); - void passEvent(QDragEnterEvent *event); - void passTextureId(std::string brushTexture); + signals: + void passEvent(QDropEvent* event); + void passEvent(QDragEnterEvent* event); + void passTextureId(ESM::RefId brushTexture); }; } diff --git a/apps/opencs/view/widget/scenetooltoggle.cpp b/apps/opencs/view/widget/scenetooltoggle.cpp index 04ac3322bfc..f32992f3abf 100644 --- a/apps/opencs/view/widget/scenetooltoggle.cpp +++ b/apps/opencs/view/widget/scenetooltoggle.cpp @@ -1,14 +1,18 @@ #include "scenetooltoggle.hpp" #include +#include +#include -#include #include +#include #include #include -#include "scenetoolbar.hpp" +#include + #include "pushbutton.hpp" +#include "scenetoolbar.hpp" void CSVWidget::SceneToolToggle::adjustToolTip() { @@ -18,8 +22,7 @@ void CSVWidget::SceneToolToggle::adjustToolTip() bool first = true; - for (std::map::const_iterator iter (mButtons.begin()); - iter!=mButtons.end(); ++iter) + for (std::map::const_iterator iter(mButtons.begin()); iter != mButtons.end(); ++iter) if (iter->first->isChecked()) { if (!first) @@ -35,36 +38,36 @@ void CSVWidget::SceneToolToggle::adjustToolTip() toolTip += "

      (left click to alter selection)"; - setToolTip (toolTip); + setToolTip(toolTip); } void CSVWidget::SceneToolToggle::adjustIcon() { unsigned int selection = getSelectionMask(); if (!selection) - setIcon (QIcon (QString::fromUtf8 (mEmptyIcon.c_str()))); + setIcon(QIcon(QString::fromUtf8(mEmptyIcon.c_str()))); else { - QPixmap pixmap (48, 48); - pixmap.fill (QColor (0, 0, 0, 0)); + QPixmap pixmap(48, 48); + pixmap.fill(QColor(0, 0, 0, 0)); { - QPainter painter (&pixmap); + QPainter painter(&pixmap); - for (std::map::const_iterator iter (mButtons.begin()); - iter!=mButtons.end(); ++iter) + for (std::map::const_iterator iter(mButtons.begin()); iter != mButtons.end(); + ++iter) if (iter->first->isChecked()) { - painter.drawImage (getIconBox (iter->second.mIndex), - QImage (QString::fromUtf8 (iter->second.mSmallIcon.c_str()))); + painter.drawImage( + getIconBox(iter->second.mIndex), QImage(QString::fromUtf8(iter->second.mSmallIcon.c_str()))); } } - setIcon (pixmap); + setIcon(pixmap); } } -QRect CSVWidget::SceneToolToggle::getIconBox (int index) const +QRect CSVWidget::SceneToolToggle::getIconBox(int index) const { // layout for a 3x3 grid int xMax = 3; @@ -74,27 +77,27 @@ QRect CSVWidget::SceneToolToggle::getIconBox (int index) const int xBorder = 1; int yBorder = 1; - int iconXSize = (mIconSize-xBorder*(xMax+1))/xMax; - int iconYSize = (mIconSize-yBorder*(yMax+1))/yMax; + int iconXSize = (mIconSize - xBorder * (xMax + 1)) / xMax; + int iconYSize = (mIconSize - yBorder * (yMax + 1)) / yMax; int y = index / xMax; int x = index % xMax; int total = static_cast(mButtons.size()); - int actualYIcons = total/xMax; + int actualYIcons = total / xMax; if (total % xMax) ++actualYIcons; - if (actualYIcons!=yMax) + if (actualYIcons != yMax) { // space out icons vertically, if there aren't enough to populate all rows int diff = yMax - actualYIcons; - yBorder += (diff*(yBorder+iconXSize)) / (actualYIcons+1); + yBorder += (diff * (yBorder + iconXSize)) / (actualYIcons + 1); } - if (y==actualYIcons-1) + if (y == actualYIcons - 1) { // generating the last row of icons int actualXIcons = total % xMax; @@ -104,51 +107,53 @@ QRect CSVWidget::SceneToolToggle::getIconBox (int index) const // space out icons horizontally, if there aren't enough to fill the last row int diff = xMax - actualXIcons; - xBorder += (diff*(xBorder+iconXSize)) / (actualXIcons+1); + xBorder += (diff * (xBorder + iconXSize)) / (actualXIcons + 1); } } - return QRect ((iconXSize+xBorder)*x+xBorder, (iconYSize+yBorder)*y+yBorder, - iconXSize, iconYSize); + return QRect((iconXSize + xBorder) * x + xBorder, (iconYSize + yBorder) * y + yBorder, iconXSize, iconYSize); } -CSVWidget::SceneToolToggle::SceneToolToggle (SceneToolbar *parent, const QString& toolTip, - const std::string& emptyIcon) -: SceneTool (parent), mEmptyIcon (emptyIcon), mButtonSize (parent->getButtonSize()), - mIconSize (parent->getIconSize()), mToolTip (toolTip), mFirst (nullptr) +CSVWidget::SceneToolToggle::SceneToolToggle(SceneToolbar* parent, const QString& toolTip, const std::string& emptyIcon) + : SceneTool(parent) + , mEmptyIcon(emptyIcon) + , mButtonSize(parent->getButtonSize()) + , mIconSize(parent->getIconSize()) + , mToolTip(toolTip) + , mFirst(nullptr) { - mPanel = new QFrame (this, Qt::Popup); + mPanel = new QFrame(this, Qt::Popup); - mLayout = new QHBoxLayout (mPanel); + mLayout = new QHBoxLayout(mPanel); - mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); + mLayout->setContentsMargins(QMargins(0, 0, 0, 0)); - mPanel->setLayout (mLayout); + mPanel->setLayout(mLayout); } -void CSVWidget::SceneToolToggle::showPanel (const QPoint& position) +void CSVWidget::SceneToolToggle::showPanel(const QPoint& position) { - mPanel->move (position); + mPanel->move(position); mPanel->show(); if (mFirst) - mFirst->setFocus (Qt::OtherFocusReason); + mFirst->setFocus(Qt::OtherFocusReason); } -void CSVWidget::SceneToolToggle::addButton (const std::string& icon, unsigned int mask, - const std::string& smallIcon, const QString& name, const QString& tooltip) +void CSVWidget::SceneToolToggle::addButton(const std::string& icon, unsigned int mask, const std::string& smallIcon, + const QString& name, const QString& tooltip) { - if (mButtons.size()>=9) - throw std::runtime_error ("Exceeded number of buttons in toggle type tool"); + if (mButtons.size() >= 9) + throw std::runtime_error("Exceeded number of buttons in toggle type tool"); - PushButton *button = new PushButton (QIcon (QPixmap (icon.c_str())), - PushButton::Type_Toggle, tooltip.isEmpty() ? name: tooltip, mPanel); + PushButton* button = new PushButton( + QIcon(QPixmap(icon.c_str())), PushButton::Type_Toggle, tooltip.isEmpty() ? name : tooltip, mPanel); - button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); - button->setIconSize (QSize (mIconSize, mIconSize)); - button->setFixedSize (mButtonSize, mButtonSize); + button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + button->setIconSize(QSize(mIconSize, mIconSize)); + button->setFixedSize(mButtonSize, mButtonSize); - mLayout->addWidget (button); + mLayout->addWidget(button); ButtonDesc desc; desc.mMask = mask; @@ -156,11 +161,11 @@ void CSVWidget::SceneToolToggle::addButton (const std::string& icon, unsigned in desc.mName = name; desc.mIndex = static_cast(mButtons.size()); - mButtons.insert (std::make_pair (button, desc)); + mButtons.insert(std::make_pair(button, desc)); - connect (button, SIGNAL (clicked()), this, SLOT (selected())); + connect(button, &PushButton::clicked, this, &SceneToolToggle::selected); - if (mButtons.size()==1) + if (mButtons.size() == 1) mFirst = button; } @@ -168,19 +173,17 @@ unsigned int CSVWidget::SceneToolToggle::getSelectionMask() const { unsigned int selection = 0; - for (std::map::const_iterator iter (mButtons.begin()); - iter!=mButtons.end(); ++iter) + for (std::map::const_iterator iter(mButtons.begin()); iter != mButtons.end(); ++iter) if (iter->first->isChecked()) selection |= iter->second.mMask; return selection; } -void CSVWidget::SceneToolToggle::setSelectionMask (unsigned int selection) +void CSVWidget::SceneToolToggle::setSelectionMask(unsigned int selection) { - for (std::map::iterator iter (mButtons.begin()); - iter!=mButtons.end(); ++iter) - iter->first->setChecked (selection & iter->second.mMask); + for (std::map::iterator iter(mButtons.begin()); iter != mButtons.end(); ++iter) + iter->first->setChecked(selection & iter->second.mMask); adjustToolTip(); adjustIcon(); @@ -188,10 +191,9 @@ void CSVWidget::SceneToolToggle::setSelectionMask (unsigned int selection) void CSVWidget::SceneToolToggle::selected() { - std::map::const_iterator iter = - mButtons.find (dynamic_cast (sender())); + std::map::const_iterator iter = mButtons.find(dynamic_cast(sender())); - if (iter!=mButtons.end()) + if (iter != mButtons.end()) { if (!iter->first->hasKeepOpen()) mPanel->hide(); diff --git a/apps/opencs/view/widget/scenetooltoggle.hpp b/apps/opencs/view/widget/scenetooltoggle.hpp index f08d117fb20..2a3b71bce0a 100644 --- a/apps/opencs/view/widget/scenetooltoggle.hpp +++ b/apps/opencs/view/widget/scenetooltoggle.hpp @@ -4,9 +4,13 @@ #include "scenetool.hpp" #include +#include class QHBoxLayout; class QRect; +class QObject; +class QPoint; +class QWidget; namespace CSVWidget { @@ -16,59 +20,57 @@ namespace CSVWidget ///< \brief Multi-Toggle tool class SceneToolToggle : public SceneTool { - Q_OBJECT + Q_OBJECT - struct ButtonDesc - { - unsigned int mMask; - std::string mSmallIcon; - QString mName; - int mIndex; - }; + struct ButtonDesc + { + unsigned int mMask; + std::string mSmallIcon; + QString mName; + int mIndex; + }; - std::string mEmptyIcon; - QWidget *mPanel; - QHBoxLayout *mLayout; - std::map mButtons; // widget, id - int mButtonSize; - int mIconSize; - QString mToolTip; - PushButton *mFirst; + std::string mEmptyIcon; + QWidget* mPanel; + QHBoxLayout* mLayout; + std::map mButtons; // widget, id + int mButtonSize; + int mIconSize; + QString mToolTip; + PushButton* mFirst; - void adjustToolTip(); + void adjustToolTip(); - void adjustIcon(); + void adjustIcon(); - QRect getIconBox (int index) const; + QRect getIconBox(int index) const; - public: + public: + SceneToolToggle(SceneToolbar* parent, const QString& toolTip, const std::string& emptyIcon); - SceneToolToggle (SceneToolbar *parent, const QString& toolTip, - const std::string& emptyIcon); + void showPanel(const QPoint& position) override; - void showPanel (const QPoint& position) override; + /// \attention After the last button has been added, setSelection must be called at + /// least once to finalise the layout. + /// + /// \note The layout algorithm can not handle more than 9 buttons. To prevent this An + /// attempt to add more will result in an exception being thrown. + /// The small icons will be sized at (x-4)/3 (where x is the main icon size). + void addButton(const std::string& icon, unsigned int mask, const std::string& smallIcon, const QString& name, + const QString& tooltip = ""); - /// \attention After the last button has been added, setSelection must be called at - /// least once to finalise the layout. - /// - /// \note The layout algorithm can not handle more than 9 buttons. To prevent this An - /// attempt to add more will result in an exception being thrown. - /// The small icons will be sized at (x-4)/3 (where x is the main icon size). - void addButton (const std::string& icon, unsigned int mask, - const std::string& smallIcon, const QString& name, const QString& tooltip = ""); + unsigned int getSelectionMask() const; - unsigned int getSelectionMask() const; + /// \param or'ed button masks. buttons that do not exist will be ignored. + void setSelectionMask(unsigned int selection); - /// \param or'ed button masks. buttons that do not exist will be ignored. - void setSelectionMask (unsigned int selection); + signals: - signals: + void selectionChanged(); - void selectionChanged(); + private slots: - private slots: - - void selected(); + void selected(); }; } diff --git a/apps/opencs/view/widget/scenetooltoggle2.cpp b/apps/opencs/view/widget/scenetooltoggle2.cpp index 363cae57091..9e1e8b11c69 100644 --- a/apps/opencs/view/widget/scenetooltoggle2.cpp +++ b/apps/opencs/view/widget/scenetooltoggle2.cpp @@ -1,15 +1,19 @@ #include "scenetooltoggle2.hpp" -#include #include +#include +#include -#include #include +#include #include -#include -#include "scenetoolbar.hpp" +#include + +#include + #include "pushbutton.hpp" +#include "scenetoolbar.hpp" void CSVWidget::SceneToolToggle2::adjustToolTip() { @@ -19,8 +23,7 @@ void CSVWidget::SceneToolToggle2::adjustToolTip() bool first = true; - for (std::map::const_iterator iter (mButtons.begin()); - iter!=mButtons.end(); ++iter) + for (std::map::const_iterator iter(mButtons.begin()); iter != mButtons.end(); ++iter) if (iter->first->isChecked()) { if (!first) @@ -36,64 +39,67 @@ void CSVWidget::SceneToolToggle2::adjustToolTip() toolTip += "

      (left click to alter selection)"; - setToolTip (toolTip); + setToolTip(toolTip); } void CSVWidget::SceneToolToggle2::adjustIcon() { unsigned int buttonIds = 0; - for (std::map::const_iterator iter (mButtons.begin()); - iter!=mButtons.end(); ++iter) + for (std::map::const_iterator iter(mButtons.begin()); iter != mButtons.end(); ++iter) if (iter->first->isChecked()) buttonIds |= iter->second.mButtonId; std::ostringstream stream; stream << mCompositeIcon << buttonIds; - setIcon (QIcon (QString::fromUtf8 (stream.str().c_str()))); + setIcon(Misc::ScalableIcon::load(QString::fromUtf8(stream.str().c_str()))); } -CSVWidget::SceneToolToggle2::SceneToolToggle2 (SceneToolbar *parent, const QString& toolTip, - const std::string& compositeIcon, const std::string& singleIcon) -: SceneTool (parent), mCompositeIcon (compositeIcon), mSingleIcon (singleIcon), - mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()), mToolTip (toolTip), - mFirst (nullptr) +CSVWidget::SceneToolToggle2::SceneToolToggle2( + SceneToolbar* parent, const QString& toolTip, const std::string& compositeIcon, const std::string& singleIcon) + : SceneTool(parent) + , mCompositeIcon(compositeIcon) + , mSingleIcon(singleIcon) + , mButtonSize(parent->getButtonSize()) + , mIconSize(parent->getIconSize()) + , mToolTip(toolTip) + , mFirst(nullptr) { - mPanel = new QFrame (this, Qt::Popup); + mPanel = new QFrame(this, Qt::Popup); - mLayout = new QHBoxLayout (mPanel); + mLayout = new QHBoxLayout(mPanel); - mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); + mLayout->setContentsMargins(QMargins(0, 0, 0, 0)); - mPanel->setLayout (mLayout); + mPanel->setLayout(mLayout); } -void CSVWidget::SceneToolToggle2::showPanel (const QPoint& position) +void CSVWidget::SceneToolToggle2::showPanel(const QPoint& position) { - mPanel->move (position); + mPanel->move(position); mPanel->show(); if (mFirst) - mFirst->setFocus (Qt::OtherFocusReason); + mFirst->setFocus(Qt::OtherFocusReason); } -void CSVWidget::SceneToolToggle2::addButton (unsigned int id, unsigned int mask, - const QString& name, const QString& tooltip, bool disabled) +void CSVWidget::SceneToolToggle2::addButton( + unsigned int id, unsigned int mask, const QString& name, const QString& tooltip, bool disabled) { std::ostringstream stream; stream << mSingleIcon << id; - PushButton *button = new PushButton (QIcon (QPixmap (stream.str().c_str())), - PushButton::Type_Toggle, tooltip.isEmpty() ? name: tooltip, mPanel); + PushButton* button = new PushButton(Misc::ScalableIcon::load(stream.str().c_str()), PushButton::Type_Toggle, + tooltip.isEmpty() ? name : tooltip, mPanel); - button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); - button->setIconSize (QSize (mIconSize, mIconSize)); - button->setFixedSize (mButtonSize, mButtonSize); + button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + button->setIconSize(QSize(mIconSize, mIconSize)); + button->setFixedSize(mButtonSize, mButtonSize); if (disabled) - button->setDisabled (true); + button->setDisabled(true); - mLayout->addWidget (button); + mLayout->addWidget(button); ButtonDesc desc; desc.mButtonId = id; @@ -101,11 +107,11 @@ void CSVWidget::SceneToolToggle2::addButton (unsigned int id, unsigned int mask, desc.mName = name; desc.mIndex = static_cast(mButtons.size()); - mButtons.insert (std::make_pair (button, desc)); + mButtons.insert(std::make_pair(button, desc)); - connect (button, SIGNAL (clicked()), this, SLOT (selected())); + connect(button, &QPushButton::clicked, this, &SceneToolToggle2::selected); - if (mButtons.size()==1 && !disabled) + if (mButtons.size() == 1 && !disabled) mFirst = button; } @@ -113,19 +119,17 @@ unsigned int CSVWidget::SceneToolToggle2::getSelectionMask() const { unsigned int selection = 0; - for (std::map::const_iterator iter (mButtons.begin()); - iter!=mButtons.end(); ++iter) + for (std::map::const_iterator iter(mButtons.begin()); iter != mButtons.end(); ++iter) if (iter->first->isChecked()) selection |= iter->second.mMask; return selection; } -void CSVWidget::SceneToolToggle2::setSelectionMask (unsigned int selection) +void CSVWidget::SceneToolToggle2::setSelectionMask(unsigned int selection) { - for (std::map::iterator iter (mButtons.begin()); - iter!=mButtons.end(); ++iter) - iter->first->setChecked (selection & iter->second.mMask); + for (std::map::iterator iter(mButtons.begin()); iter != mButtons.end(); ++iter) + iter->first->setChecked(selection & iter->second.mMask); adjustToolTip(); adjustIcon(); @@ -133,10 +137,9 @@ void CSVWidget::SceneToolToggle2::setSelectionMask (unsigned int selection) void CSVWidget::SceneToolToggle2::selected() { - std::map::const_iterator iter = - mButtons.find (dynamic_cast (sender())); + std::map::const_iterator iter = mButtons.find(dynamic_cast(sender())); - if (iter!=mButtons.end()) + if (iter != mButtons.end()) { if (!iter->first->hasKeepOpen()) mPanel->hide(); diff --git a/apps/opencs/view/widget/scenetooltoggle2.hpp b/apps/opencs/view/widget/scenetooltoggle2.hpp index e250192984c..44fb4411f42 100644 --- a/apps/opencs/view/widget/scenetooltoggle2.hpp +++ b/apps/opencs/view/widget/scenetooltoggle2.hpp @@ -4,9 +4,12 @@ #include "scenetool.hpp" #include +#include class QHBoxLayout; -class QRect; +class QObject; +class QPoint; +class QWidget; namespace CSVWidget { @@ -18,61 +21,60 @@ namespace CSVWidget /// Top level button is using predefined icons instead building a composite icon. class SceneToolToggle2 : public SceneTool { - Q_OBJECT + Q_OBJECT - struct ButtonDesc - { - unsigned int mButtonId; - unsigned int mMask; - QString mName; - int mIndex; - }; + struct ButtonDesc + { + unsigned int mButtonId; + unsigned int mMask; + QString mName; + int mIndex; + }; - std::string mCompositeIcon; - std::string mSingleIcon; - QWidget *mPanel; - QHBoxLayout *mLayout; - std::map mButtons; // widget, id - int mButtonSize; - int mIconSize; - QString mToolTip; - PushButton *mFirst; + std::string mCompositeIcon; + std::string mSingleIcon; + QWidget* mPanel; + QHBoxLayout* mLayout; + std::map mButtons; // widget, id + int mButtonSize; + int mIconSize; + QString mToolTip; + PushButton* mFirst; - void adjustToolTip(); + void adjustToolTip(); - void adjustIcon(); + void adjustIcon(); - public: + public: + /// The top level icon is compositeIcon + sum of bitpatterns for active buttons (in + /// decimal) + /// + /// The icon for individual toggle buttons is signleIcon + bitmask for button (in + /// decimal) + SceneToolToggle2(SceneToolbar* parent, const QString& toolTip, const std::string& compositeIcon, + const std::string& singleIcon); - /// The top level icon is compositeIcon + sum of bitpatterns for active buttons (in - /// decimal) - /// - /// The icon for individual toggle buttons is signleIcon + bitmask for button (in - /// decimal) - SceneToolToggle2 (SceneToolbar *parent, const QString& toolTip, - const std::string& compositeIcon, const std::string& singleIcon); + void showPanel(const QPoint& position) override; - void showPanel (const QPoint& position) override; + /// \param buttonId used to compose the icon filename + /// \param mask used for the reported getSelectionMask() / setSelectionMask() + /// \attention After the last button has been added, setSelection must be called at + /// least once to finalise the layout. + void addButton(unsigned int buttonId, unsigned int mask, const QString& name, const QString& tooltip = "", + bool disabled = false); - /// \param buttonId used to compose the icon filename - /// \param mask used for the reported getSelectionMask() / setSelectionMask() - /// \attention After the last button has been added, setSelection must be called at - /// least once to finalise the layout. - void addButton (unsigned int buttonId, unsigned int mask, - const QString& name, const QString& tooltip = "", bool disabled = false); + unsigned int getSelectionMask() const; - unsigned int getSelectionMask() const; + /// \param or'ed button masks. buttons that do not exist will be ignored. + void setSelectionMask(unsigned int selection); - /// \param or'ed button masks. buttons that do not exist will be ignored. - void setSelectionMask (unsigned int selection); + signals: - signals: + void selectionChanged(); - void selectionChanged(); + private slots: - private slots: - - void selected(); + void selected(); }; } diff --git a/apps/opencs/view/world/bodypartcreator.cpp b/apps/opencs/view/world/bodypartcreator.cpp index a9fc3e063f6..9c26b84465d 100644 --- a/apps/opencs/view/world/bodypartcreator.cpp +++ b/apps/opencs/view/world/bodypartcreator.cpp @@ -5,6 +5,8 @@ #include "../../model/world/data.hpp" #include "../../model/world/universalid.hpp" +#include + std::string CSVWorld::BodyPartCreator::getId() const { std::string id = CSVWorld::GenericCreator::getId(); @@ -17,16 +19,13 @@ std::string CSVWorld::BodyPartCreator::getId() const return id; } -CSVWorld::BodyPartCreator::BodyPartCreator( - CSMWorld::Data& data, - QUndoStack& undoStack, - const CSMWorld::UniversalId& id -) : GenericCreator(data, undoStack, id) +CSVWorld::BodyPartCreator::BodyPartCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) + : GenericCreator(data, undoStack, id) { mFirstPerson = new QCheckBox("First Person", this); insertBeforeButtons(mFirstPerson, false); - connect(mFirstPerson, SIGNAL(clicked(bool)), this, SLOT(checkboxClicked())); + connect(mFirstPerson, &QCheckBox::clicked, this, &BodyPartCreator::checkboxClicked); } std::string CSVWorld::BodyPartCreator::getErrors() const diff --git a/apps/opencs/view/world/bodypartcreator.hpp b/apps/opencs/view/world/bodypartcreator.hpp index f526b7faebd..adb795eace4 100644 --- a/apps/opencs/view/world/bodypartcreator.hpp +++ b/apps/opencs/view/world/bodypartcreator.hpp @@ -18,29 +18,24 @@ namespace CSVWorld { Q_OBJECT - QCheckBox *mFirstPerson; + QCheckBox* mFirstPerson; - private: + private: + /// \return ID entered by user. + std::string getId() const override; - /// \return ID entered by user. - std::string getId() const override; + public: + BodyPartCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); - public: + /// \return Error description for current user input. + std::string getErrors() const override; - BodyPartCreator( - CSMWorld::Data& data, - QUndoStack& undoStack, - const CSMWorld::UniversalId& id); + /// \brief Clear ID and checkbox input widgets. + void reset() override; - /// \return Error description for current user input. - std::string getErrors() const override; + private slots: - /// \brief Clear ID and checkbox input widgets. - void reset() override; - - private slots: - - void checkboxClicked(); + void checkboxClicked(); }; } diff --git a/apps/opencs/view/world/cellcreator.cpp b/apps/opencs/view/world/cellcreator.cpp index 5b428a4b371..7545a5c0822 100644 --- a/apps/opencs/view/world/cellcreator.cpp +++ b/apps/opencs/view/world/cellcreator.cpp @@ -4,15 +4,21 @@ #include #include -#include #include +#include + +#include +#include +#include #include "../../model/world/commands.hpp" #include "../../model/world/idtree.hpp" +class QUndoStack; + std::string CSVWorld::CellCreator::getId() const { - if (mType->currentIndex()==0) + if (mType->currentIndex() == 0) return GenericCreator::getId(); std::ostringstream stream; @@ -31,76 +37,83 @@ void CSVWorld::CellCreator::configureCreateCommand(CSMWorld::CreateCommand& comm command.addNestedValue(parentIndex, index, mType->currentIndex() == 0); } -CSVWorld::CellCreator::CellCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id) -: GenericCreator (data, undoStack, id) +CSVWorld::CellCreator::CellCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) + : GenericCreator(data, undoStack, id) { - mY = new QSpinBox (this); - mY->setVisible (false); - mY->setMinimum (std::numeric_limits::min()); - mY->setMaximum (std::numeric_limits::max()); - connect (mY, SIGNAL (valueChanged (int)), this, SLOT (valueChanged (int))); - insertAtBeginning (mY, true); + mY = new QSpinBox(this); + mY->setVisible(false); + mY->setMinimum(std::numeric_limits::min()); + mY->setMaximum(std::numeric_limits::max()); + connect(mY, qOverload(&QSpinBox::valueChanged), this, &CellCreator::valueChanged); + insertAtBeginning(mY, true); - mYLabel = new QLabel ("Y", this); - mYLabel->setVisible (false); - insertAtBeginning (mYLabel, false); + mYLabel = new QLabel("Y", this); + mYLabel->setVisible(false); + insertAtBeginning(mYLabel, false); - mX = new QSpinBox (this); - mX->setVisible (false); - mX->setMinimum (std::numeric_limits::min()); - mX->setMaximum (std::numeric_limits::max()); - connect (mX, SIGNAL (valueChanged (int)), this, SLOT (valueChanged (int))); - insertAtBeginning (mX, true); + mX = new QSpinBox(this); + mX->setVisible(false); + mX->setMinimum(std::numeric_limits::min()); + mX->setMaximum(std::numeric_limits::max()); + connect(mX, qOverload(&QSpinBox::valueChanged), this, &CellCreator::valueChanged); + insertAtBeginning(mX, true); - mXLabel = new QLabel ("X", this); - mXLabel->setVisible (false); - insertAtBeginning (mXLabel, false); + mXLabel = new QLabel("X", this); + mXLabel->setVisible(false); + insertAtBeginning(mXLabel, false); - mType = new QComboBox (this); + mType = new QComboBox(this); - mType->addItem ("Interior Cell"); - mType->addItem ("Exterior Cell"); + mType->addItem("Interior Cell"); + mType->addItem("Exterior Cell"); - connect (mType, SIGNAL (currentIndexChanged (int)), this, SLOT (setType (int))); + connect(mType, qOverload(&QComboBox::currentIndexChanged), this, &CellCreator::setType); - insertAtBeginning (mType, false); + insertAtBeginning(mType, false); } void CSVWorld::CellCreator::reset() { - mX->setValue (0); - mY->setValue (0); - mType->setCurrentIndex (0); + mX->setValue(0); + mY->setValue(0); + mType->setCurrentIndex(0); setType(0); GenericCreator::reset(); } -void CSVWorld::CellCreator::setType (int index) +void CSVWorld::CellCreator::setType(int index) { - setManualEditing (index==0); - mXLabel->setVisible (index==1); - mX->setVisible (index==1); - mYLabel->setVisible (index==1); - mY->setVisible (index==1); + setManualEditing(index == 0); + mXLabel->setVisible(index == 1); + mX->setVisible(index == 1); + mYLabel->setVisible(index == 1); + mY->setVisible(index == 1); + + // The cell name is limited to 64 characters. (ESM::Header::GMDT::mCurrentCell) + std::string text = mType->currentText().toStdString(); + if (text == "Interior Cell") + GenericCreator::setEditorMaxLength(64); + else + GenericCreator::setEditorMaxLength(32767); update(); } -void CSVWorld::CellCreator::valueChanged (int index) +void CSVWorld::CellCreator::valueChanged(int index) { update(); } -void CSVWorld::CellCreator::cloneMode(const std::string& originId, - const CSMWorld::UniversalId::Type type) +void CSVWorld::CellCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { CSVWorld::GenericCreator::cloneMode(originId, type); - if (*(originId.begin()) == '#') //if originid points to the exterior cell + if (*(originId.begin()) == '#') // if originid points to the exterior cell { - setType(1); //enable x and y controls + setType(1); // enable x and y controls mType->setCurrentIndex(1); - } else { + } + else + { setType(0); mType->setCurrentIndex(0); } diff --git a/apps/opencs/view/world/cellcreator.hpp b/apps/opencs/view/world/cellcreator.hpp index 032096aa247..0e0b4324a5a 100644 --- a/apps/opencs/view/world/cellcreator.hpp +++ b/apps/opencs/view/world/cellcreator.hpp @@ -1,49 +1,58 @@ #ifndef CSV_WORLD_CELLCREATOR_H #define CSV_WORLD_CELLCREATOR_H +#include + +#include + +#include "genericcreator.hpp" + +class QComboBox; class QLabel; +class QObject; class QSpinBox; -class QComboBox; +class QUndoStack; -#include "genericcreator.hpp" +namespace CSMWorld +{ + class CreateCommand; + class Data; +} namespace CSVWorld { class CellCreator : public GenericCreator { - Q_OBJECT - - QComboBox *mType; - QLabel *mXLabel; - QSpinBox *mX; - QLabel *mYLabel; - QSpinBox *mY; - - protected: + Q_OBJECT - std::string getId() const override; + QComboBox* mType; + QLabel* mXLabel; + QSpinBox* mX; + QLabel* mYLabel; + QSpinBox* mY; - /// Allow subclasses to add additional data to \a command. - void configureCreateCommand(CSMWorld::CreateCommand& command) const override; + protected: + std::string getId() const override; - public: + /// Allow subclasses to add additional data to \a command. + void configureCreateCommand(CSMWorld::CreateCommand& command) const override; - CellCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); + public: + CellCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); - void reset() override; + void reset() override; - void cloneMode(const std::string& originId, - const CSMWorld::UniversalId::Type type) override; + void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; - std::string getErrors() const override; - ///< Return formatted error descriptions for the current state of the creator. if an empty - /// string is returned, there is no error. + std::string getErrors() const override; + ///< Return formatted error descriptions for the current state of the creator. if an empty + /// string is returned, there is no error. - private slots: + private slots: - void setType (int index); + void setType(int index); - void valueChanged (int index); + void valueChanged(int index); }; } diff --git a/apps/opencs/view/world/colordelegate.cpp b/apps/opencs/view/world/colordelegate.cpp index 3b5f692fe29..18dd1086487 100644 --- a/apps/opencs/view/world/colordelegate.cpp +++ b/apps/opencs/view/world/colordelegate.cpp @@ -1,25 +1,36 @@ #include "colordelegate.hpp" +#include +#include #include -#include +#include -CSVWorld::ColorDelegate::ColorDelegate(CSMWorld::CommandDispatcher *dispatcher, - CSMDoc::Document& document, - QObject *parent) +#include + +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher; +} + +CSVWorld::ColorDelegate::ColorDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) : CommandDelegate(dispatcher, document, parent) -{} +{ +} -void CSVWorld::ColorDelegate::paint(QPainter *painter, - const QStyleOptionViewItem &option, - const QModelIndex &index) const +void CSVWorld::ColorDelegate::paint( + QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { int colorInt = index.data().toInt(); QColor color(colorInt & 0xff, (colorInt >> 8) & 0xff, (colorInt >> 16) & 0xff); QRect coloredRect(option.rect.x() + qRound(option.rect.width() / 4.0), - option.rect.y() + qRound(option.rect.height() / 4.0), - option.rect.width() / 2, - option.rect.height() / 2); + option.rect.y() + qRound(option.rect.height() / 4.0), option.rect.width() / 2, option.rect.height() / 2); painter->save(); painter->fillRect(coloredRect, color); painter->setPen(Qt::black); @@ -27,11 +38,8 @@ void CSVWorld::ColorDelegate::paint(QPainter *painter, painter->restore(); } -CSVWorld::CommandDelegate *CSVWorld::ColorDelegateFactory::makeDelegate(CSMWorld::CommandDispatcher *dispatcher, - CSMDoc::Document &document, - QObject *parent) const +CSVWorld::CommandDelegate* CSVWorld::ColorDelegateFactory::makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const { return new ColorDelegate(dispatcher, document, parent); } - - diff --git a/apps/opencs/view/world/colordelegate.hpp b/apps/opencs/view/world/colordelegate.hpp index 041051d1320..669fa5a4c5b 100644 --- a/apps/opencs/view/world/colordelegate.hpp +++ b/apps/opencs/view/world/colordelegate.hpp @@ -3,34 +3,36 @@ #include "util.hpp" -class QRect; +class QModelIndex; +class QObject; +class QPainter; -namespace CSVWidget +namespace CSMDoc { - class ColorEditButton; + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher; } namespace CSVWorld { class ColorDelegate : public CommandDelegate { - public: - ColorDelegate(CSMWorld::CommandDispatcher *dispatcher, - CSMDoc::Document& document, - QObject *parent); - - void paint(QPainter *painter, - const QStyleOptionViewItem &option, - const QModelIndex &index) const override; + public: + ColorDelegate(CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent); + + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; }; class ColorDelegateFactory : public CommandDelegateFactory { - public: - CommandDelegate *makeDelegate(CSMWorld::CommandDispatcher *dispatcher, - CSMDoc::Document &document, - QObject *parent) const override; - ///< The ownership of the returned CommandDelegate is transferred to the caller. + public: + CommandDelegate* makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const override; + ///< The ownership of the returned CommandDelegate is transferred to the caller. }; } diff --git a/apps/opencs/view/world/creator.cpp b/apps/opencs/view/world/creator.cpp index 53664c186a3..45b2faf3e31 100644 --- a/apps/opencs/view/world/creator.cpp +++ b/apps/opencs/view/world/creator.cpp @@ -1,21 +1,23 @@ #include "creator.hpp" -#include +#include +#include -CSVWorld::Creator::~Creator() {} +#include -void CSVWorld::Creator::setScope (unsigned int scope) +namespace CSMDoc { - if (scope!=CSMWorld::Scope_Content) - throw std::logic_error ("Invalid scope in creator"); + class Document; } +void CSVWorld::Creator::setScope(unsigned int scope) +{ + if (scope != CSMWorld::Scope_Content) + throw std::logic_error("Invalid scope in creator"); +} -CSVWorld::CreatorFactoryBase::~CreatorFactoryBase() {} - - -CSVWorld::Creator *CSVWorld::NullCreatorFactory::makeCreator (CSMDoc::Document& document, - const CSMWorld::UniversalId& id) const +CSVWorld::Creator* CSVWorld::NullCreatorFactory::makeCreator( + CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return nullptr; } diff --git a/apps/opencs/view/world/creator.hpp b/apps/opencs/view/world/creator.hpp index 516f71f15c4..ed2c754b1af 100644 --- a/apps/opencs/view/world/creator.hpp +++ b/apps/opencs/view/world/creator.hpp @@ -2,6 +2,7 @@ #define CSV_WORLD_CREATOR_H #include +#include #include @@ -12,93 +13,83 @@ #include "../../model/world/universalid.hpp" #endif -namespace CSMDoc -{ - class Document; -} - namespace CSVWorld { /// \brief Record creator UI base class class Creator : public QWidget { - Q_OBJECT - - public: + Q_OBJECT - virtual ~Creator(); + public: + ~Creator() override = default; - virtual void reset() = 0; + virtual void reset() = 0; - virtual void cloneMode(const std::string& originId, - const CSMWorld::UniversalId::Type type) = 0; + virtual void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) = 0; - /// Touches a record, if the creator supports it. - virtual void touch(const std::vector& ids) = 0; + /// Touches a record, if the creator supports it. + virtual void touch(const std::vector& ids) = 0; - virtual void setEditLock (bool locked) = 0; + virtual void setEditLock(bool locked) = 0; - virtual void toggleWidgets(bool active = true) = 0; + virtual void toggleWidgets(bool active = true) = 0; - /// Default implementation: Throw an exception if scope!=Scope_Content. - virtual void setScope (unsigned int scope); + /// Default implementation: Throw an exception if scope!=Scope_Content. + virtual void setScope(unsigned int scope); - /// Focus main input widget - virtual void focus() = 0; + /// Focus main input widget + virtual void focus() = 0; - signals: + signals: - void done(); + void done(); - void requestFocus (const std::string& id); - ///< Request owner of this creator to focus the just created \a id. The owner may - /// ignore this request. + void requestFocus(const std::string& id); + ///< Request owner of this creator to focus the just created \a id. The owner may + /// ignore this request. }; /// \brief Base class for Creator factory class CreatorFactoryBase { - public: - - virtual ~CreatorFactoryBase(); - - virtual Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const = 0; - ///< The ownership of the returned Creator is transferred to the caller. - /// - /// \note The function can return a 0-pointer, which means no UI for creating/deleting - /// records should be provided. + public: + virtual ~CreatorFactoryBase() = default; + + virtual Creator* makeCreator(CSMDoc::Document& document, const CSMWorld::UniversalId& id) const = 0; + ///< The ownership of the returned Creator is transferred to the caller. + /// + /// \note The function can return a 0-pointer, which means no UI for creating/deleting + /// records should be provided. }; /// \brief Creator factory that does not produces any creator class NullCreatorFactory : public CreatorFactoryBase { - public: - - Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; - ///< The ownership of the returned Creator is transferred to the caller. - /// - /// \note The function always returns 0. + public: + Creator* makeCreator(CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; + ///< The ownership of the returned Creator is transferred to the caller. + /// + /// \note The function always returns 0. }; - template + template class CreatorFactory : public CreatorFactoryBase { - public: - - Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; - ///< The ownership of the returned Creator is transferred to the caller. - /// - /// \note The function can return a 0-pointer, which means no UI for creating/deleting - /// records should be provided. + public: + Creator* makeCreator(CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; + ///< The ownership of the returned Creator is transferred to the caller. + /// + /// \note The function can return a 0-pointer, which means no UI for creating/deleting + /// records should be provided. }; - template - Creator *CreatorFactory::makeCreator (CSMDoc::Document& document, - const CSMWorld::UniversalId& id) const + template + Creator* CreatorFactory::makeCreator( + CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { - std::unique_ptr creator (new CreatorT (document.getData(), document.getUndoStack(), id)); + auto creator = std::make_unique(document.getData(), document.getUndoStack(), id); - creator->setScope (scope); + creator->setScope(scope); return creator.release(); } diff --git a/apps/opencs/view/world/datadisplaydelegate.cpp b/apps/opencs/view/world/datadisplaydelegate.cpp index 6da62d4089a..c642a05e2b0 100644 --- a/apps/opencs/view/world/datadisplaydelegate.cpp +++ b/apps/opencs/view/world/datadisplaydelegate.cpp @@ -3,27 +3,84 @@ #include "../../model/prefs/state.hpp" #include +#include #include +#include +#include +#include +#include -CSVWorld::DataDisplayDelegate::DataDisplayDelegate(const ValueList &values, - const IconList &icons, - CSMWorld::CommandDispatcher *dispatcher, - CSMDoc::Document& document, - const std::string &pageName, - const std::string &settingName, - QObject *parent) - : EnumDelegate (values, dispatcher, document, parent), mDisplayMode (Mode_TextOnly), - mIcons (icons), mIconSize (QSize(16, 16)), - mHorizontalMargin(QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1), - mTextLeftOffset(8), mSettingKey (pageName + '/' + settingName) +#include + +#include +#include +#include +#include + +#include + +class QModelIndex; +class QObject; + +namespace CSMDoc { + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher; +} + +CSVWorld::DataDisplayDelegate::DataDisplayDelegate(const ValueList& values, const IconList& icons, + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, const std::string& pageName, + const std::string& settingName, QObject* parent) + : EnumDelegate(values, dispatcher, document, parent) + , mDisplayMode(Mode_TextOnly) + , mIcons(icons) + , mIconSize(QSize(16, 16)) + , mHorizontalMargin(QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1) + , mTextLeftOffset(8) + , mPixmapsColor(QApplication::palette().text().color()) + , mUiScale(static_cast(QGuiApplication::instance())->devicePixelRatio()) + , mSettingKey(pageName + '/' + settingName) +{ + if (parent) + parent->installEventFilter(this); + buildPixmaps(); if (!pageName.empty()) - updateDisplayMode (CSMPrefs::get()[pageName][settingName].toString()); + updateDisplayMode(CSMPrefs::get()[pageName][settingName].toString()); } -void CSVWorld::DataDisplayDelegate::buildPixmaps () +bool CSVWorld::DataDisplayDelegate::eventFilter(QObject* target, QEvent* event) +{ + if (event->type() == QEvent::Resize) + { + auto uiScale = static_cast(QGuiApplication::instance())->devicePixelRatio(); + if (mUiScale != uiScale) + { + mUiScale = uiScale; + + buildPixmaps(); + } + } + else if (event->type() == QEvent::PaletteChange) + { + QColor themeColor = QApplication::palette().text().color(); + if (themeColor != mPixmapsColor) + { + mPixmapsColor = std::move(themeColor); + + buildPixmaps(); + } + } + + return false; +} + +void CSVWorld::DataDisplayDelegate::buildPixmaps() { if (!mPixmaps.empty()) mPixmaps.clear(); @@ -32,7 +89,7 @@ void CSVWorld::DataDisplayDelegate::buildPixmaps () while (it != mIcons.end()) { - mPixmaps.emplace_back (it->mValue, it->mIcon.pixmap (mIconSize) ); + mPixmaps.emplace_back(it->mValue, it->mIcon.pixmap(mIconSize)); ++it; } } @@ -48,7 +105,7 @@ void CSVWorld::DataDisplayDelegate::setTextLeftOffset(int offset) mTextLeftOffset = offset; } -QSize CSVWorld::DataDisplayDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +QSize CSVWorld::DataDisplayDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { QSize size = EnumDelegate::sizeHint(option, index); @@ -72,11 +129,12 @@ QSize CSVWorld::DataDisplayDelegate::sizeHint(const QStyleOptionViewItem &option return size; } -void CSVWorld::DataDisplayDelegate::paint (QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +void CSVWorld::DataDisplayDelegate::paint( + QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { painter->save(); - //default to enum delegate's paint method for text-only conditions + // default to enum delegate's paint method for text-only conditions if (mDisplayMode == Mode_TextOnly) EnumDelegate::paint(painter, option, index); else @@ -91,7 +149,7 @@ void CSVWorld::DataDisplayDelegate::paint (QPainter *painter, const QStyleOption painter->restore(); } -void CSVWorld::DataDisplayDelegate::paintIcon (QPainter *painter, const QStyleOptionViewItem &option, int index) const +void CSVWorld::DataDisplayDelegate::paintIcon(QPainter* painter, const QStyleOptionViewItem& option, int index) const { QRect iconRect = option.rect; QRect textRect = iconRect; @@ -104,20 +162,14 @@ void CSVWorld::DataDisplayDelegate::paintIcon (QPainter *painter, const QStyleOp textRect.setLeft(iconRect.right() + mTextLeftOffset); textRect.setRight(option.rect.right() - mHorizontalMargin); - QString text = option.fontMetrics.elidedText(mValues.at(index).second, - option.textElideMode, - textRect.width()); - QApplication::style()->drawItemText(painter, - textRect, - Qt::AlignLeft | Qt::AlignVCenter, - option.palette, - true, - text); + QString text = option.fontMetrics.elidedText(mValues.at(index).second, option.textElideMode, textRect.width()); + QApplication::style()->drawItemText( + painter, textRect, Qt::AlignLeft | Qt::AlignVCenter, option.palette, true, text); } QApplication::style()->drawItemPixmap(painter, iconRect, Qt::AlignCenter, mPixmaps.at(index).second); } -void CSVWorld::DataDisplayDelegate::updateDisplayMode (const std::string &mode) +void CSVWorld::DataDisplayDelegate::updateDisplayMode(const std::string& mode) { if (mode == "Icon and Text") mDisplayMode = Mode_IconAndText; @@ -129,27 +181,22 @@ void CSVWorld::DataDisplayDelegate::updateDisplayMode (const std::string &mode) mDisplayMode = Mode_TextOnly; } -CSVWorld::DataDisplayDelegate::~DataDisplayDelegate() +void CSVWorld::DataDisplayDelegate::settingChanged(const CSMPrefs::Setting* setting) { + if (*setting == mSettingKey) + updateDisplayMode(setting->toString()); } -void CSVWorld::DataDisplayDelegate::settingChanged (const CSMPrefs::Setting *setting) -{ - if (*setting==mSettingKey) - updateDisplayMode (setting->toString()); -} - - -void CSVWorld::DataDisplayDelegateFactory::add (int enumValue, const QString& enumName, const QString& iconFilename) +void CSVWorld::DataDisplayDelegateFactory::add(int enumValue, const QString& enumName, const QString& iconFilename) { EnumDelegateFactory::add(enumValue, enumName); Icon icon; icon.mValue = enumValue; icon.mName = enumName; - icon.mIcon = QIcon(iconFilename); + icon.mIcon = Misc::ScalableIcon::load(iconFilename); - for (auto it=mIcons.begin(); it!=mIcons.end(); ++it) + for (auto it = mIcons.begin(); it != mIcons.end(); ++it) { if (it->mName > enumName) { @@ -161,8 +208,8 @@ void CSVWorld::DataDisplayDelegateFactory::add (int enumValue, const QString& en mIcons.push_back(icon); } -CSVWorld::CommandDelegate *CSVWorld::DataDisplayDelegateFactory::makeDelegate ( - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const +CSVWorld::CommandDelegate* CSVWorld::DataDisplayDelegateFactory::makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const { - return new DataDisplayDelegate (mValues, mIcons, dispatcher, document, "", "", parent); + return new DataDisplayDelegate(mValues, mIcons, dispatcher, document, "", "", parent); } diff --git a/apps/opencs/view/world/datadisplaydelegate.hpp b/apps/opencs/view/world/datadisplaydelegate.hpp index df06359a04c..3b3c935d8bb 100755 --- a/apps/opencs/view/world/datadisplaydelegate.hpp +++ b/apps/opencs/view/world/datadisplaydelegate.hpp @@ -1,9 +1,28 @@ #ifndef DATADISPLAYDELEGATE_HPP #define DATADISPLAYDELEGATE_HPP -#include #include "enumdelegate.hpp" +#include +#include +#include +#include + +class QModelIndex; +class QObject; +class QPainter; +class QPixmap; + +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher; +} + namespace CSMPrefs { class Setting; @@ -11,6 +30,8 @@ namespace CSMPrefs namespace CSVWorld { + class CommandDelegate; + struct Icon { int mValue; @@ -20,13 +41,12 @@ namespace CSVWorld class DataDisplayDelegate : public EnumDelegate { + Q_OBJECT public: - typedef std::vector IconList; - typedef std::vector > ValueList; + typedef std::vector> ValueList; protected: - enum DisplayMode { Mode_TextOnly, @@ -38,60 +58,58 @@ namespace CSVWorld IconList mIcons; private: - - std::vector > mPixmaps; + std::vector> mPixmaps; QSize mIconSize; int mHorizontalMargin; int mTextLeftOffset; + QColor mPixmapsColor; + qreal mUiScale; std::string mSettingKey; public: - DataDisplayDelegate (const ValueList & values, const IconList & icons, - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, - const std::string& pageName, const std::string& settingName, QObject *parent); + DataDisplayDelegate(const ValueList& values, const IconList& icons, CSMWorld::CommandDispatcher* dispatcher, + CSMDoc::Document& document, const std::string& pageName, const std::string& settingName, QObject* parent); - ~DataDisplayDelegate(); + ~DataDisplayDelegate() = default; - void paint (QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; /// pass a QSize defining height / width of icon. Default is QSize (16,16). - void setIconSize (const QSize& icon); + void setIconSize(const QSize& icon); /// offset the horizontal position of the text from the right edge of the icon. Default is 8 pixels. - void setTextLeftOffset (int offset); + void setTextLeftOffset(int offset); - private: + bool eventFilter(QObject* target, QEvent* event) override; + private: /// update the display mode based on a passed string - void updateDisplayMode (const std::string &); + void updateDisplayMode(const std::string&); /// custom paint function for painting the icon. Mode_IconAndText and Mode_Icon only. - void paintIcon (QPainter *painter, const QStyleOptionViewItem &option, int i) const; + void paintIcon(QPainter* painter, const QStyleOptionViewItem& option, int i) const; /// rebuild the list of pixmaps from the provided icons (called when icon size is changed) void buildPixmaps(); - void settingChanged (const CSMPrefs::Setting *setting) override; + void settingChanged(const CSMPrefs::Setting* setting) override; }; class DataDisplayDelegateFactory : public EnumDelegateFactory { protected: - DataDisplayDelegate::IconList mIcons; public: - - CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const override; + CommandDelegate* makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const override; ///< The ownership of the returned CommandDelegate is transferred to the caller. protected: - - void add (int enumValue, const QString& enumName, const QString& iconFilename); - + void add(int enumValue, const QString& enumName, const QString& iconFilename); }; } diff --git a/apps/opencs/view/world/dialoguecreator.cpp b/apps/opencs/view/world/dialoguecreator.cpp index 7c6fb2e81f1..db78c35245f 100644 --- a/apps/opencs/view/world/dialoguecreator.cpp +++ b/apps/opencs/view/world/dialoguecreator.cpp @@ -1,36 +1,40 @@ #include "dialoguecreator.hpp" -#include +#include +#include +#include +#include -#include "../../model/doc/document.hpp" +#include -#include "../../model/world/data.hpp" #include "../../model/world/commands.hpp" -#include "../../model/world/columns.hpp" #include "../../model/world/idtable.hpp" -void CSVWorld::DialogueCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const +class QUndoStack; + +void CSVWorld::DialogueCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { - int index = - dynamic_cast (*getData().getTableModel (getCollectionId())). - findColumnIndex (CSMWorld::Columns::ColumnId_DialogueType); + int index = dynamic_cast(*getData().getTableModel(getCollectionId())) + .findColumnIndex(CSMWorld::Columns::ColumnId_DialogueType); - command.addValue (index, mType); + command.addValue(index, mType); } -CSVWorld::DialogueCreator::DialogueCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id, int type) -: GenericCreator (data, undoStack, id, true), mType (type) -{} +CSVWorld::DialogueCreator::DialogueCreator( + CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, int type) + : GenericCreator(data, undoStack, id, true) + , mType(type) +{ +} -CSVWorld::Creator *CSVWorld::TopicCreatorFactory::makeCreator (CSMDoc::Document& document, - const CSMWorld::UniversalId& id) const +CSVWorld::Creator* CSVWorld::TopicCreatorFactory::makeCreator( + CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { - return new DialogueCreator (document.getData(), document.getUndoStack(), id, ESM::Dialogue::Topic); + return new DialogueCreator(document.getData(), document.getUndoStack(), id, ESM::Dialogue::Topic); } -CSVWorld::Creator *CSVWorld::JournalCreatorFactory::makeCreator (CSMDoc::Document& document, - const CSMWorld::UniversalId& id) const +CSVWorld::Creator* CSVWorld::JournalCreatorFactory::makeCreator( + CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { - return new DialogueCreator (document.getData(), document.getUndoStack(), id, ESM::Dialogue::Journal); + return new DialogueCreator(document.getData(), document.getUndoStack(), id, ESM::Dialogue::Journal); } diff --git a/apps/opencs/view/world/dialoguecreator.hpp b/apps/opencs/view/world/dialoguecreator.hpp index 0aef2f84df9..6a89387b88c 100644 --- a/apps/opencs/view/world/dialoguecreator.hpp +++ b/apps/opencs/view/world/dialoguecreator.hpp @@ -3,36 +3,47 @@ #include "genericcreator.hpp" +#include +#include + +class QUndoStack; + +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class CreateCommand; + class Data; +} + namespace CSVWorld { class DialogueCreator : public GenericCreator { - int mType; - - protected: + int mType; - void configureCreateCommand (CSMWorld::CreateCommand& command) const override; + protected: + void configureCreateCommand(CSMWorld::CreateCommand& command) const override; - public: - - DialogueCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id, int type); + public: + DialogueCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, int type); }; class TopicCreatorFactory : public CreatorFactoryBase { - public: - - Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; - ///< The ownership of the returned Creator is transferred to the caller. + public: + Creator* makeCreator(CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; + ///< The ownership of the returned Creator is transferred to the caller. }; class JournalCreatorFactory : public CreatorFactoryBase { - public: - - Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; - ///< The ownership of the returned Creator is transferred to the caller. + public: + Creator* makeCreator(CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; + ///< The ownership of the returned Creator is transferred to the caller. }; } diff --git a/apps/opencs/view/world/dialoguespinbox.cpp b/apps/opencs/view/world/dialoguespinbox.cpp index 1228ca0da9e..25b15e51b64 100644 --- a/apps/opencs/view/world/dialoguespinbox.cpp +++ b/apps/opencs/view/world/dialoguespinbox.cpp @@ -2,24 +2,25 @@ #include -CSVWorld::DialogueSpinBox::DialogueSpinBox(QWidget *parent) : QSpinBox(parent) +CSVWorld::DialogueSpinBox::DialogueSpinBox(QWidget* parent) + : QSpinBox(parent) { setFocusPolicy(Qt::StrongFocus); } -void CSVWorld::DialogueSpinBox::focusInEvent(QFocusEvent *event) +void CSVWorld::DialogueSpinBox::focusInEvent(QFocusEvent* event) { setFocusPolicy(Qt::WheelFocus); QSpinBox::focusInEvent(event); } -void CSVWorld::DialogueSpinBox::focusOutEvent(QFocusEvent *event) +void CSVWorld::DialogueSpinBox::focusOutEvent(QFocusEvent* event) { setFocusPolicy(Qt::StrongFocus); QSpinBox::focusOutEvent(event); } -void CSVWorld::DialogueSpinBox::wheelEvent(QWheelEvent *event) +void CSVWorld::DialogueSpinBox::wheelEvent(QWheelEvent* event) { if (!hasFocus()) event->ignore(); @@ -27,24 +28,25 @@ void CSVWorld::DialogueSpinBox::wheelEvent(QWheelEvent *event) QSpinBox::wheelEvent(event); } -CSVWorld::DialogueDoubleSpinBox::DialogueDoubleSpinBox(QWidget *parent) : QDoubleSpinBox(parent) +CSVWorld::DialogueDoubleSpinBox::DialogueDoubleSpinBox(QWidget* parent) + : QDoubleSpinBox(parent) { setFocusPolicy(Qt::StrongFocus); } -void CSVWorld::DialogueDoubleSpinBox::focusInEvent(QFocusEvent *event) +void CSVWorld::DialogueDoubleSpinBox::focusInEvent(QFocusEvent* event) { setFocusPolicy(Qt::WheelFocus); QDoubleSpinBox::focusInEvent(event); } -void CSVWorld::DialogueDoubleSpinBox::focusOutEvent(QFocusEvent *event) +void CSVWorld::DialogueDoubleSpinBox::focusOutEvent(QFocusEvent* event) { setFocusPolicy(Qt::StrongFocus); QDoubleSpinBox::focusOutEvent(event); } -void CSVWorld::DialogueDoubleSpinBox::wheelEvent(QWheelEvent *event) +void CSVWorld::DialogueDoubleSpinBox::wheelEvent(QWheelEvent* event) { if (!hasFocus()) event->ignore(); diff --git a/apps/opencs/view/world/dialoguespinbox.hpp b/apps/opencs/view/world/dialoguespinbox.hpp index 90fe8d20cd4..beffb56562c 100644 --- a/apps/opencs/view/world/dialoguespinbox.hpp +++ b/apps/opencs/view/world/dialoguespinbox.hpp @@ -1,8 +1,8 @@ #ifndef CSV_WORLD_DIALOGUESPINBOX_H #define CSV_WORLD_DIALOGUESPINBOX_H -#include #include +#include namespace CSVWorld { @@ -10,30 +10,26 @@ namespace CSVWorld { Q_OBJECT - public: - - DialogueSpinBox (QWidget *parent = nullptr); + public: + DialogueSpinBox(QWidget* parent = nullptr); - protected: - - void focusInEvent(QFocusEvent *event) override; - void focusOutEvent(QFocusEvent *event) override; - void wheelEvent(QWheelEvent *event) override; + protected: + void focusInEvent(QFocusEvent* event) override; + void focusOutEvent(QFocusEvent* event) override; + void wheelEvent(QWheelEvent* event) override; }; class DialogueDoubleSpinBox : public QDoubleSpinBox { Q_OBJECT - public: - - DialogueDoubleSpinBox (QWidget *parent = nullptr); - - protected: + public: + DialogueDoubleSpinBox(QWidget* parent = nullptr); - void focusInEvent(QFocusEvent *event) override; - void focusOutEvent(QFocusEvent *event) override; - void wheelEvent(QWheelEvent *event) override; + protected: + void focusInEvent(QFocusEvent* event) override; + void focusOutEvent(QFocusEvent* event) override; + void wheelEvent(QWheelEvent* event) override; }; } diff --git a/apps/opencs/view/world/dialoguesubview.cpp b/apps/opencs/view/world/dialoguesubview.cpp index f2360b1378f..168e555eaee 100644 --- a/apps/opencs/view/world/dialoguesubview.cpp +++ b/apps/opencs/view/world/dialoguesubview.cpp @@ -1,58 +1,75 @@ #include "dialoguesubview.hpp" -#include +#include #include #include +#include +#include -#include -#include -#include #include -#include -#include -#include -#include -#include #include -#include -#include #include +#include +#include +#include #include -#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include -#include "../../model/world/nestedtableproxymodel.hpp" +#include "../../model/doc/document.hpp" #include "../../model/world/columnbase.hpp" +#include "../../model/world/columns.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" -#include "../../model/world/columns.hpp" +#include "../../model/world/nestedtableproxymodel.hpp" #include "../../model/world/record.hpp" #include "../../model/world/tablemimedata.hpp" -#include "../../model/world/commands.hpp" -#include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" #include "../widget/coloreditor.hpp" #include "../widget/droplineedit.hpp" -#include "recordstatusdelegate.hpp" -#include "util.hpp" -#include "tablebottombox.hpp" #include "nestedtable.hpp" #include "recordbuttonbar.hpp" +#include "tablebottombox.hpp" +#include "util.hpp" + +class QPainter; +class QPoint; + /* ==============================NotEditableSubDelegate========================================== */ -CSVWorld::NotEditableSubDelegate::NotEditableSubDelegate(const CSMWorld::IdTable* table, QObject * parent) : -QAbstractItemDelegate(parent), -mTable(table) -{} +CSVWorld::NotEditableSubDelegate::NotEditableSubDelegate(const CSMWorld::IdTable* table, QObject* parent) + : QAbstractItemDelegate(parent) + , mTable(table) +{ +} -void CSVWorld::NotEditableSubDelegate::setEditorData (QWidget* editor, const QModelIndex& index) const +void CSVWorld::NotEditableSubDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { QLabel* label = qobject_cast(editor); - if(!label) + if (!label) return; QVariant v = index.data(Qt::EditRole); @@ -65,75 +82,72 @@ void CSVWorld::NotEditableSubDelegate::setEditorData (QWidget* editor, const QMo } } - CSMWorld::Columns::ColumnId columnId = static_cast ( - mTable->getColumnId (index.column())); + CSMWorld::Columns::ColumnId columnId + = static_cast(mTable->getColumnId(index.column())); if (QVariant::String == v.type()) { label->setText(v.toString()); } - else if (CSMWorld::Columns::hasEnums (columnId)) + else if (CSMWorld::Columns::hasEnums(columnId)) { int data = v.toInt(); - std::vector> enumNames (CSMWorld::Columns::getEnums (columnId)); + std::vector> enumNames(CSMWorld::Columns::getEnums(columnId)); label->setText(QString::fromUtf8(enumNames.at(data).second.c_str())); } else { - label->setText (v.toString()); + label->setText(v.toString()); } } -void CSVWorld::NotEditableSubDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const +void CSVWorld::NotEditableSubDelegate::setModelData( + QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { - //not editable widgets will not save model data + // not editable widgets will not save model data } -void CSVWorld::NotEditableSubDelegate::paint (QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const +void CSVWorld::NotEditableSubDelegate::paint( + QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { - //does nothing + // does nothing } -QSize CSVWorld::NotEditableSubDelegate::sizeHint (const QStyleOptionViewItem& option, const QModelIndex& index) const +QSize CSVWorld::NotEditableSubDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { return QSize(); } -QWidget* CSVWorld::NotEditableSubDelegate::createEditor (QWidget *parent, - const QStyleOptionViewItem& option, - const QModelIndex& index) const +QWidget* CSVWorld::NotEditableSubDelegate::createEditor( + QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { - QLabel *label = new QLabel(parent); - label->setTextInteractionFlags (Qt::TextSelectableByMouse); + QLabel* label = new QLabel(parent); + label->setTextInteractionFlags(Qt::TextSelectableByMouse); return label; } /* ==============================DialogueDelegateDispatcherProxy========================================== */ -CSVWorld::DialogueDelegateDispatcherProxy::refWrapper::refWrapper(const QModelIndex& index) : -mIndex(index) -{} - -CSVWorld::DialogueDelegateDispatcherProxy::DialogueDelegateDispatcherProxy(QWidget* editor, CSMWorld::ColumnBase::Display display) : -mEditor(editor), -mDisplay(display), -mIndexWrapper(nullptr) +CSVWorld::DialogueDelegateDispatcherProxy::DialogueDelegateDispatcherProxy( + QWidget* editor, CSMWorld::ColumnBase::Display display) + : mEditor(editor) + , mDisplay(display) { } void CSVWorld::DialogueDelegateDispatcherProxy::editorDataCommited() { - if (mIndexWrapper.get()) + if (mIndex.has_value()) { - emit editorDataCommited(mEditor, mIndexWrapper->mIndex, mDisplay); + emit editorDataCommited(mEditor, mIndex.value(), mDisplay); } } void CSVWorld::DialogueDelegateDispatcherProxy::setIndex(const QModelIndex& index) { - mIndexWrapper.reset(new refWrapper(index)); + mIndex = index; } QWidget* CSVWorld::DialogueDelegateDispatcherProxy::getEditor() const @@ -145,54 +159,58 @@ QWidget* CSVWorld::DialogueDelegateDispatcherProxy::getEditor() const ==============================DialogueDelegateDispatcher========================================== */ -CSVWorld::DialogueDelegateDispatcher::DialogueDelegateDispatcher(QObject* parent, - CSMWorld::IdTable* table, CSMWorld::CommandDispatcher& commandDispatcher, - CSMDoc::Document& document, QAbstractItemModel *model) : -mParent(parent), -mTable(model ? model : table), -mCommandDispatcher (commandDispatcher), mDocument (document), -mNotEditableDelegate(table, parent) +CSVWorld::DialogueDelegateDispatcher::DialogueDelegateDispatcher(QObject* parent, CSMWorld::IdTable* table, + CSMWorld::CommandDispatcher& commandDispatcher, CSMDoc::Document& document, QAbstractItemModel* model) + : mParent(parent) + , mTable(model ? model : table) + , mCommandDispatcher(commandDispatcher) + , mDocument(document) + , mNotEditableDelegate(table, parent) { } CSVWorld::CommandDelegate* CSVWorld::DialogueDelegateDispatcher::makeDelegate(CSMWorld::ColumnBase::Display display) { - CommandDelegate *delegate = nullptr; + CommandDelegate* delegate = nullptr; std::map::const_iterator delegateIt(mDelegates.find(display)); if (delegateIt == mDelegates.end()) { - delegate = CommandDelegateFactoryCollection::get().makeDelegate ( - display, &mCommandDispatcher, mDocument, mParent); + delegate + = CommandDelegateFactoryCollection::get().makeDelegate(display, &mCommandDispatcher, mDocument, mParent); mDelegates.insert(std::make_pair(display, delegate)); - } else + } + else { delegate = delegateIt->second; } return delegate; } -void CSVWorld::DialogueDelegateDispatcher::editorDataCommited(QWidget* editor, - const QModelIndex& index, CSMWorld::ColumnBase::Display display) +void CSVWorld::DialogueDelegateDispatcher::editorDataCommited( + QWidget* editor, const QModelIndex& index, CSMWorld::ColumnBase::Display display) { setModelData(editor, mTable, index, display); } -void CSVWorld::DialogueDelegateDispatcher::setEditorData (QWidget* editor, const QModelIndex& index) const +void CSVWorld::DialogueDelegateDispatcher::setEditorData(QWidget* editor, const QModelIndex& index) const { CSMWorld::ColumnBase::Display display = CSMWorld::ColumnBase::Display_None; if (index.parent().isValid()) { - display = static_cast - (static_cast(mTable)->nestedHeaderData (index.parent().column(), index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); + display + = static_cast(static_cast(mTable) + ->nestedHeaderData(index.parent().column(), index.column(), + Qt::Horizontal, CSMWorld::ColumnBase::Role_Display) + .toInt()); } else { - display = static_cast - (mTable->headerData (index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); + display = static_cast( + mTable->headerData(index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); } QLabel* label = qobject_cast(editor); - if(label) + if (label) { mNotEditableDelegate.setEditorData(label, index); return; @@ -204,23 +222,23 @@ void CSVWorld::DialogueDelegateDispatcher::setEditorData (QWidget* editor, const delegateIt->second->setEditorData(editor, index, true); } - for (unsigned i = 0; i < mProxys.size(); ++i) + for (const auto& proxy : mProxys) { - if (mProxys[i]->getEditor() == editor) + if (proxy->getEditor() == editor) { - mProxys[i]->setIndex(index); + proxy->setIndex(index); } } } -void CSVWorld::DialogueDelegateDispatcher::setModelData(QWidget* editor, - QAbstractItemModel* model, const QModelIndex& index) const +void CSVWorld::DialogueDelegateDispatcher::setModelData( + QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { setModelData(editor, model, index, CSMWorld::ColumnBase::Display_None); } -void CSVWorld::DialogueDelegateDispatcher::setModelData(QWidget* editor, - QAbstractItemModel* model, const QModelIndex& index, CSMWorld::ColumnBase::Display display) const +void CSVWorld::DialogueDelegateDispatcher::setModelData( + QWidget* editor, QAbstractItemModel* model, const QModelIndex& index, CSMWorld::ColumnBase::Display display) const { std::map::const_iterator delegateIt(mDelegates.find(display)); if (delegateIt != mDelegates.end()) @@ -229,20 +247,19 @@ void CSVWorld::DialogueDelegateDispatcher::setModelData(QWidget* editor, } } -void CSVWorld::DialogueDelegateDispatcher::paint (QPainter* painter, - const QStyleOptionViewItem& option, const QModelIndex& index) const +void CSVWorld::DialogueDelegateDispatcher::paint( + QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { - //Does nothing + // Does nothing } -QSize CSVWorld::DialogueDelegateDispatcher::sizeHint (const QStyleOptionViewItem& option, - const QModelIndex& index) const +QSize CSVWorld::DialogueDelegateDispatcher::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { - return QSize(); //silencing warning, otherwise does nothing + return QSize(); // silencing warning, otherwise does nothing } -QWidget* CSVWorld::DialogueDelegateDispatcher::makeEditor(CSMWorld::ColumnBase::Display display, - const QModelIndex& index) +QWidget* CSVWorld::DialogueDelegateDispatcher::makeEditor( + CSMWorld::ColumnBase::Display display, const QModelIndex& index) { QVariant variant = index.data(); if (!variant.isValid()) @@ -255,18 +272,17 @@ QWidget* CSVWorld::DialogueDelegateDispatcher::makeEditor(CSMWorld::ColumnBase:: } QWidget* editor = nullptr; - if (! (mTable->flags (index) & Qt::ItemIsEditable)) + if (!(mTable->flags(index) & Qt::ItemIsEditable)) { - return mNotEditableDelegate.createEditor(qobject_cast(mParent), - QStyleOptionViewItem(), index); + return mNotEditableDelegate.createEditor(qobject_cast(mParent), QStyleOptionViewItem(), index); } std::map::iterator delegateIt(mDelegates.find(display)); if (delegateIt != mDelegates.end()) { - editor = delegateIt->second->createEditor(qobject_cast(mParent), - QStyleOptionViewItem(), index, display); + editor + = delegateIt->second->createEditor(qobject_cast(mParent), QStyleOptionViewItem(), index, display); DialogueDelegateDispatcherProxy* proxy = new DialogueDelegateDispatcherProxy(editor, display); @@ -274,70 +290,74 @@ QWidget* CSVWorld::DialogueDelegateDispatcher::makeEditor(CSMWorld::ColumnBase:: // is required here if (qobject_cast(editor)) { - connect(editor, SIGNAL(editingFinished()), proxy, SLOT(editorDataCommited())); + connect(static_cast(editor), &CSVWidget::DropLineEdit::editingFinished, proxy, + qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); - connect(editor, SIGNAL(tableMimeDataDropped(const CSMWorld::UniversalId&, const CSMDoc::Document*)), - proxy, SLOT(editorDataCommited())); + connect(static_cast(editor), &CSVWidget::DropLineEdit::tableMimeDataDropped, + proxy, qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); } else if (qobject_cast(editor)) { - connect(editor, SIGNAL(stateChanged(int)), proxy, SLOT(editorDataCommited())); + connect(static_cast(editor), &QCheckBox::stateChanged, proxy, + qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); } else if (qobject_cast(editor)) { - connect(editor, SIGNAL(textChanged()), proxy, SLOT(editorDataCommited())); + connect(static_cast(editor), &QPlainTextEdit::textChanged, proxy, + qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); } else if (qobject_cast(editor)) { - connect(editor, SIGNAL(currentIndexChanged (int)), proxy, SLOT(editorDataCommited())); + connect(static_cast(editor), qOverload(&QComboBox::currentIndexChanged), proxy, + qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); } else if (qobject_cast(editor) || qobject_cast(editor)) { - connect(editor, SIGNAL(editingFinished()), proxy, SLOT(editorDataCommited())); + connect(static_cast(editor), &QAbstractSpinBox::editingFinished, proxy, + qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); } - else if (qobject_cast(editor)) + else if (qobject_cast(editor)) { - connect(editor, SIGNAL(pickingFinished()), proxy, SLOT(editorDataCommited())); + connect(static_cast(editor), &CSVWidget::ColorEditor::pickingFinished, proxy, + qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); } else // throw an exception because this is a coding error - throw std::logic_error ("Dialogue editor type missing"); + throw std::logic_error("Dialogue editor type missing"); - connect(proxy, SIGNAL(editorDataCommited(QWidget*, const QModelIndex&, CSMWorld::ColumnBase::Display)), - this, SLOT(editorDataCommited(QWidget*, const QModelIndex&, CSMWorld::ColumnBase::Display))); + connect(proxy, + qOverload( + &DialogueDelegateDispatcherProxy::editorDataCommited), + this, &DialogueDelegateDispatcher::editorDataCommited); - mProxys.push_back(proxy); //deleted in the destructor + mProxys.push_back(proxy); // deleted in the destructor } return editor; } CSVWorld::DialogueDelegateDispatcher::~DialogueDelegateDispatcher() { - for (unsigned i = 0; i < mProxys.size(); ++i) + for (auto* proxy : mProxys) { - delete mProxys[i]; //unique_ptr could be handy + delete proxy; // unique_ptr could be handy } } - -CSVWorld::IdContextMenu::IdContextMenu(QWidget *widget, CSMWorld::ColumnBase::Display display) - : QObject(widget), - mWidget(widget), - mIdType(CSMWorld::TableMimeData::convertEnums(display)) +CSVWorld::IdContextMenu::IdContextMenu(QWidget* widget, CSMWorld::ColumnBase::Display display) + : QObject(widget) + , mWidget(widget) + , mIdType(CSMWorld::TableMimeData::convertEnums(display)) { Q_ASSERT(mWidget != nullptr); Q_ASSERT(CSMWorld::ColumnBase::isId(display)); Q_ASSERT(mIdType != CSMWorld::UniversalId::Type_None); mWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(mWidget, - SIGNAL(customContextMenuRequested(const QPoint &)), - this, - SLOT(showContextMenu(const QPoint &))); + connect(mWidget, &QWidget::customContextMenuRequested, this, &IdContextMenu::showContextMenu); mEditIdAction = new QAction(this); - connect(mEditIdAction, SIGNAL(triggered()), this, SLOT(editIdRequest())); + connect(mEditIdAction, &QAction::triggered, this, qOverload<>(&IdContextMenu::editIdRequest)); - QLineEdit *lineEdit = qobject_cast(mWidget); + QLineEdit* lineEdit = qobject_cast(mWidget); if (lineEdit != nullptr) { mContextMenu = lineEdit->createStandardContextMenu(); @@ -348,15 +368,15 @@ CSVWorld::IdContextMenu::IdContextMenu(QWidget *widget, CSMWorld::ColumnBase::Di } } -void CSVWorld::IdContextMenu::excludeId(const std::string &id) +void CSVWorld::IdContextMenu::excludeId(const std::string& id) { mExcludedIds.insert(id); } QString CSVWorld::IdContextMenu::getWidgetValue() const { - QLineEdit *lineEdit = qobject_cast(mWidget); - QLabel *label = qobject_cast(mWidget); + QLineEdit* lineEdit = qobject_cast(mWidget); + QLabel* label = qobject_cast(mWidget); QString value = ""; if (lineEdit != nullptr) @@ -370,7 +390,7 @@ QString CSVWorld::IdContextMenu::getWidgetValue() const return value; } -void CSVWorld::IdContextMenu::addEditIdActionToMenu(const QString &text) +void CSVWorld::IdContextMenu::addEditIdActionToMenu(const QString& text) { mEditIdAction->setText(text); if (mContextMenu->actions().isEmpty()) @@ -379,7 +399,7 @@ void CSVWorld::IdContextMenu::addEditIdActionToMenu(const QString &text) } else if (mContextMenu->actions().first() != mEditIdAction) { - QAction *action = mContextMenu->actions().first(); + QAction* action = mContextMenu->actions().first(); mContextMenu->insertAction(action, mEditIdAction); mContextMenu->insertSeparator(action); } @@ -402,7 +422,7 @@ void CSVWorld::IdContextMenu::removeEditIdActionFromMenu() } } -void CSVWorld::IdContextMenu::showContextMenu(const QPoint &pos) +void CSVWorld::IdContextMenu::showContextMenu(const QPoint& pos) { QString value = getWidgetValue(); bool isExcludedId = mExcludedIds.find(value.toUtf8().constData()) != mExcludedIds.end(); @@ -431,32 +451,29 @@ void CSVWorld::IdContextMenu::editIdRequest() =============================================================EditWidget===================================================== */ -void CSVWorld::EditWidget::createEditorContextMenu(QWidget *editor, - CSMWorld::ColumnBase::Display display, - int currentRow) const +void CSVWorld::EditWidget::createEditorContextMenu( + QWidget* editor, CSMWorld::ColumnBase::Display display, int currentRow) const { Q_ASSERT(editor != nullptr); - if (CSMWorld::ColumnBase::isId(display) && - CSMWorld::TableMimeData::convertEnums(display) != CSMWorld::UniversalId::Type_None) + if (CSMWorld::ColumnBase::isId(display) + && CSMWorld::TableMimeData::convertEnums(display) != CSMWorld::UniversalId::Type_None) { int idColumn = mTable->findColumnIndex(CSMWorld::Columns::ColumnId_Id); QString id = mTable->data(mTable->index(currentRow, idColumn)).toString(); - IdContextMenu *menu = new IdContextMenu(editor, display); + IdContextMenu* menu = new IdContextMenu(editor, display); // Current ID is already opened, so no need to create Edit 'ID' action for it menu->excludeId(id.toUtf8().constData()); - connect(menu, - SIGNAL(editIdRequest(const CSMWorld::UniversalId &, const std::string &)), - this, - SIGNAL(editIdRequest(const CSMWorld::UniversalId &, const std::string &))); + connect(menu, qOverload(&IdContextMenu::editIdRequest), this, + &EditWidget::editIdRequest); } } CSVWorld::EditWidget::~EditWidget() { - for (unsigned i = 0; i < mNestedModels.size(); ++i) - delete mNestedModels[i]; + for (auto* model : mNestedModels) + delete model; if (mDispatcher) delete mDispatcher; @@ -465,55 +482,53 @@ CSVWorld::EditWidget::~EditWidget() delete mNestedTableDispatcher; } -CSVWorld::EditWidget::EditWidget(QWidget *parent, - int row, CSMWorld::IdTable* table, CSMWorld::CommandDispatcher& commandDispatcher, - CSMDoc::Document& document, bool createAndDelete) : -QScrollArea(parent), -mWidgetMapper(nullptr), -mNestedTableMapper(nullptr), -mDispatcher(nullptr), -mNestedTableDispatcher(nullptr), -mMainWidget(nullptr), -mTable(table), -mCommandDispatcher (commandDispatcher), -mDocument (document) +CSVWorld::EditWidget::EditWidget(QWidget* parent, int row, CSMWorld::IdTable* table, + CSMWorld::CommandDispatcher& commandDispatcher, CSMDoc::Document& document, bool createAndDelete) + : QScrollArea(parent) + , mWidgetMapper(nullptr) + , mNestedTableMapper(nullptr) + , mDispatcher(nullptr) + , mNestedTableDispatcher(nullptr) + , mMainWidget(nullptr) + , mTable(table) + , mCommandDispatcher(commandDispatcher) + , mDocument(document) { - remake (row); + remake(row); } void CSVWorld::EditWidget::remake(int row) { if (mMainWidget) { - QWidget *del = this->takeWidget(); + QWidget* del = this->takeWidget(); del->deleteLater(); } - mMainWidget = new QWidget (this); + mMainWidget = new QWidget(this); - for (unsigned i = 0; i < mNestedModels.size(); ++i) - delete mNestedModels[i]; + for (auto* model : mNestedModels) + delete model; mNestedModels.clear(); if (mDispatcher) delete mDispatcher; - mDispatcher = new DialogueDelegateDispatcher(nullptr/*this*/, mTable, mCommandDispatcher, mDocument); + mDispatcher = new DialogueDelegateDispatcher(nullptr /*this*/, mTable, mCommandDispatcher, mDocument); if (mNestedTableDispatcher) delete mNestedTableDispatcher; - //not sure if widget mapper can handle deleting the widgets that were mapped + // not sure if widget mapper can handle deleting the widgets that were mapped if (mWidgetMapper) delete mWidgetMapper; - mWidgetMapper = new QDataWidgetMapper (this); + mWidgetMapper = new QDataWidgetMapper(this); mWidgetMapper->setModel(mTable); mWidgetMapper->setItemDelegate(mDispatcher); if (mNestedTableMapper) delete mNestedTableMapper; - QFrame* line = new QFrame(mMainWidget); line->setObjectName(QString::fromUtf8("line")); line->setGeometry(QRect(320, 150, 118, 3)); @@ -526,10 +541,10 @@ void CSVWorld::EditWidget::remake(int row) line2->setFrameShape(QFrame::HLine); line2->setFrameShadow(QFrame::Sunken); - QVBoxLayout *mainLayout = new QVBoxLayout(mMainWidget); - QGridLayout *lockedLayout = new QGridLayout(); - QGridLayout *unlockedLayout = new QGridLayout(); - QVBoxLayout *tablesLayout = new QVBoxLayout(); + QVBoxLayout* mainLayout = new QVBoxLayout(mMainWidget); + QGridLayout* lockedLayout = new QGridLayout(); + QGridLayout* unlockedLayout = new QGridLayout(); + QVBoxLayout* tablesLayout = new QVBoxLayout(); mainLayout->addLayout(lockedLayout, QSizePolicy::Fixed); mainLayout->addWidget(line, 1); @@ -538,51 +553,57 @@ void CSVWorld::EditWidget::remake(int row) mainLayout->addLayout(tablesLayout, QSizePolicy::Preferred); mainLayout->addStretch(1); + int blockedColumn = mTable->searchColumnIndex(CSMWorld::Columns::ColumnId_Blocked); + bool isBlocked = mTable->data(mTable->index(row, blockedColumn)).toInt(); + int unlocked = 0; int locked = 0; const int columns = mTable->columnCount(); - for (int i=0; iheaderData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags).toInt(); + int flags = mTable->headerData(i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags).toInt(); if (flags & CSMWorld::ColumnBase::Flag_Dialogue) { - CSMWorld::ColumnBase::Display display = static_cast - (mTable->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); + CSMWorld::ColumnBase::Display display = static_cast( + mTable->headerData(i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); - if (mTable->hasChildren(mTable->index(row, i)) && - !(flags & CSMWorld::ColumnBase::Flag_Dialogue_List)) + if (mTable->hasChildren(mTable->index(row, i)) && !(flags & CSMWorld::ColumnBase::Flag_Dialogue_List)) { CSMWorld::IdTree* innerTable = &dynamic_cast(*mTable); - mNestedModels.push_back(new CSMWorld::NestedTableProxyModel (mTable->index(row, i), display, innerTable)); + mNestedModels.push_back( + new CSMWorld::NestedTableProxyModel(mTable->index(row, i), display, innerTable)); - int idColumn = mTable->findColumnIndex (CSMWorld::Columns::ColumnId_Id); - int typeColumn = mTable->findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); + int idColumn = mTable->findColumnIndex(CSMWorld::Columns::ColumnId_Id); + int typeColumn = mTable->findColumnIndex(CSMWorld::Columns::ColumnId_RecordType); CSMWorld::UniversalId id = CSMWorld::UniversalId( - static_cast (mTable->data (mTable->index (row, typeColumn)).toInt()), - mTable->data (mTable->index (row, idColumn)).toString().toUtf8().constData()); + static_cast(mTable->data(mTable->index(row, typeColumn)).toInt()), + mTable->data(mTable->index(row, idColumn)).toString().toUtf8().constData()); bool editable = true; bool fixedRows = false; QVariant v = mTable->index(row, i).data(); if (v.canConvert()) { - assert (QString(v.typeName()) == "CSMWorld::ColumnBase::TableEditModes"); + assert(QString(v.typeName()) == "CSMWorld::ColumnBase::TableEditModes"); if (v.value() == CSMWorld::ColumnBase::TableEdit_None) editable = false; - else if (v.value() == CSMWorld::ColumnBase::TableEdit_FixedRows) + else if (v.value() + == CSMWorld::ColumnBase::TableEdit_FixedRows) fixedRows = true; } // Create and display nested table only if it's editable. if (editable) { - NestedTable* table = - new NestedTable(mDocument, id, mNestedModels.back(), this, editable, fixedRows); + NestedTable* table + = new NestedTable(mDocument, id, mNestedModels.back(), this, editable, fixedRows); table->resizeColumnsToContents(); + if (isBlocked) + table->setEditTriggers(QAbstractItemView::NoEditTriggers); int rows = mTable->rowCount(mTable->index(row, i)); int rowHeight = (rows == 0) ? table->horizontalHeader()->height() : table->rowHeight(0); @@ -590,47 +611,46 @@ void CSVWorld::EditWidget::remake(int row) int tableMaxHeight = (5 * rowHeight) + headerHeight + (2 * table->frameWidth()); table->setMinimumHeight(tableMaxHeight); - QString headerText = mTable->headerData (i, Qt::Horizontal, Qt::DisplayRole).toString(); - QLabel* label = new QLabel (headerText, mMainWidget); - label->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed); + QString headerText = mTable->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); + QLabel* label = new QLabel(headerText, mMainWidget); + label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); tablesLayout->addWidget(label); tablesLayout->addWidget(table); - connect(table, - SIGNAL(editRequest(const CSMWorld::UniversalId &, const std::string &)), - this, - SIGNAL(editIdRequest(const CSMWorld::UniversalId &, const std::string &))); + connect(table, &NestedTable::editRequest, this, &EditWidget::editIdRequest); } } else if (!(flags & CSMWorld::ColumnBase::Flag_Dialogue_List)) { - mDispatcher->makeDelegate (display); - QWidget* editor = mDispatcher->makeEditor (display, (mTable->index (row, i))); + mDispatcher->makeDelegate(display); + QWidget* editor = mDispatcher->makeEditor(display, (mTable->index(row, i))); if (editor) { - mWidgetMapper->addMapping (editor, i); + mWidgetMapper->addMapping(editor, i); - QLabel* label = new QLabel (mTable->headerData (i, Qt::Horizontal).toString(), mMainWidget); + QLabel* label = new QLabel(mTable->headerData(i, Qt::Horizontal).toString(), mMainWidget); - label->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed); - editor->setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + editor->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - if (! (mTable->flags (mTable->index (row, i)) & Qt::ItemIsEditable)) + // HACK: the blocked checkbox needs to keep the same position + // FIXME: unfortunately blocked record displays a little differently to unblocked one + if (!(mTable->flags(mTable->index(row, i)) & Qt::ItemIsEditable) || i == blockedColumn) { - lockedLayout->addWidget (label, locked, 0); - lockedLayout->addWidget (editor, locked, 1); + lockedLayout->addWidget(label, locked, 0); + lockedLayout->addWidget(editor, locked, 1); ++locked; } else { - unlockedLayout->addWidget (label, unlocked, 0); - unlockedLayout->addWidget (editor, unlocked, 1); + unlockedLayout->addWidget(label, unlocked, 0); + unlockedLayout->addWidget(editor, unlocked, 1); ++unlocked; } - if(mTable->index(row, i).data().type() == QVariant::UserType) + if (CSMWorld::DisableTag::isDisableTag(mTable->index(row, i).data())) { editor->setEnabled(false); label->setEnabled(false); @@ -639,54 +659,57 @@ void CSVWorld::EditWidget::remake(int row) createEditorContextMenu(editor, display, row); } } - else + else // Flag_Dialogue_List { - CSMWorld::IdTree *tree = static_cast(mTable); - mNestedTableMapper = new QDataWidgetMapper (this); + CSMWorld::IdTree* tree = static_cast(mTable); + mNestedTableMapper = new QDataWidgetMapper(this); mNestedTableMapper->setModel(tree); // FIXME: lack MIME support? - mNestedTableDispatcher = - new DialogueDelegateDispatcher (nullptr/*this*/, mTable, mCommandDispatcher, mDocument, tree); - mNestedTableMapper->setRootIndex (tree->index(row, i)); + mNestedTableDispatcher + = new DialogueDelegateDispatcher(nullptr /*this*/, mTable, mCommandDispatcher, mDocument, tree); + mNestedTableMapper->setRootIndex(tree->index(row, i)); mNestedTableMapper->setItemDelegate(mNestedTableDispatcher); int columnCount = tree->columnCount(tree->index(row, i)); for (int col = 0; col < columnCount; ++col) { - int displayRole = tree->nestedHeaderData (i, col, - Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt(); + int displayRole + = tree->nestedHeaderData(i, col, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt(); - display = static_cast (displayRole); + display = static_cast(displayRole); - mNestedTableDispatcher->makeDelegate (display); + mNestedTableDispatcher->makeDelegate(display); // FIXME: assumed all columns are editable - QWidget* editor = - mNestedTableDispatcher->makeEditor (display, tree->index (0, col, tree->index(row, i))); + QWidget* editor + = mNestedTableDispatcher->makeEditor(display, tree->index(0, col, tree->index(row, i))); if (editor) { - mNestedTableMapper->addMapping (editor, col); + mNestedTableMapper->addMapping(editor, col); // Need to use Qt::DisplayRole in order to get the correct string // from CSMWorld::Columns - QLabel* label = new QLabel (tree->nestedHeaderData (i, col, - Qt::Horizontal, Qt::DisplayRole).toString(), mMainWidget); + QLabel* label = new QLabel( + tree->nestedHeaderData(i, col, Qt::Horizontal, Qt::DisplayRole).toString(), mMainWidget); - label->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed); - editor->setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + editor->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - unlockedLayout->addWidget (label, unlocked, 0); - unlockedLayout->addWidget (editor, unlocked, 1); + unlockedLayout->addWidget(label, unlocked, 0); + unlockedLayout->addWidget(editor, unlocked, 1); ++unlocked; - if(tree->index(0, col, tree->index(row, i)).data().type() == QVariant::UserType) + if (CSMWorld::DisableTag::isDisableTag(tree->index(0, col, tree->index(row, i)).data())) { editor->setEnabled(false); label->setEnabled(false); } - createEditorContextMenu(editor, display, row); + if (!isBlocked) + createEditorContextMenu(editor, display, row); + else + editor->setEnabled(false); } } mNestedTableMapper->setCurrentModelIndex(tree->index(0, 0, tree->index(row, i))); @@ -703,7 +726,6 @@ void CSVWorld::EditWidget::remake(int row) this->setWidgetResizable(true); } - QVBoxLayout& CSVWorld::SimpleDialogueSubView::getMainLayout() { return *mMainLayout; @@ -729,98 +751,96 @@ bool CSVWorld::SimpleDialogueSubView::isLocked() const return mLocked; } -CSVWorld::SimpleDialogueSubView::SimpleDialogueSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) : - SubView (id), - mEditWidget(nullptr), - mMainLayout(nullptr), - mTable(dynamic_cast(document.getData().getTableModel(id))), - mLocked(false), - mDocument(document), - mCommandDispatcher (document, CSMWorld::UniversalId::getParentType (id.getType())) +CSVWorld::SimpleDialogueSubView::SimpleDialogueSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) + : SubView(id) + , mEditWidget(nullptr) + , mMainLayout(nullptr) + , mTable(dynamic_cast(document.getData().getTableModel(id))) + , mLocked(false) + , mDocument(document) + , mCommandDispatcher(document, CSMWorld::UniversalId::getParentType(id.getType())) { - connect(mTable, SIGNAL(dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT(dataChanged(const QModelIndex&))); - connect(mTable, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)), this, SLOT(rowsAboutToBeRemoved(const QModelIndex&, int, int))); + connect(mTable, &CSMWorld::IdTable::dataChanged, this, &SimpleDialogueSubView::dataChanged); + connect(mTable, &CSMWorld::IdTable::rowsAboutToBeRemoved, this, &SimpleDialogueSubView::rowsAboutToBeRemoved); updateCurrentId(); - QWidget *mainWidget = new QWidget(this); + QWidget* mainWidget = new QWidget(this); mMainLayout = new QVBoxLayout(mainWidget); - setWidget (mainWidget); + setWidget(mainWidget); - int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); - mEditWidget = new EditWidget(mainWidget, - mTable->getModelIndex(getUniversalId().getId(), idColumn).row(), mTable, mCommandDispatcher, document, false); + mEditWidget = new EditWidget(mainWidget, mTable->getModelIndex(getUniversalId().getId(), idColumn).row(), mTable, + mCommandDispatcher, document, false); mMainLayout->addWidget(mEditWidget); mEditWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); - dataChanged(mTable->getModelIndex (getUniversalId().getId(), idColumn)); + dataChanged(mTable->getModelIndex(getUniversalId().getId(), idColumn)); - connect(mEditWidget, - SIGNAL(editIdRequest(const CSMWorld::UniversalId &, const std::string &)), - this, - SIGNAL(focusId(const CSMWorld::UniversalId &, const std::string &))); + connect(mEditWidget, &EditWidget::editIdRequest, this, &SimpleDialogueSubView::focusId); } -void CSVWorld::SimpleDialogueSubView::setEditLock (bool locked) +void CSVWorld::SimpleDialogueSubView::setEditLock(bool locked) { if (!mEditWidget) // hack to indicate that getUniversalId().getId() is no longer valid return; - int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); mLocked = locked; QModelIndex currentIndex(mTable->getModelIndex(getUniversalId().getId(), idColumn)); if (currentIndex.isValid()) { - CSMWorld::RecordBase::State state = static_cast(mTable->data (mTable->index (currentIndex.row(), 1)).toInt()); + CSMWorld::RecordBase::State state + = static_cast(mTable->data(mTable->index(currentIndex.row(), 1)).toInt()); - mEditWidget->setDisabled (state==CSMWorld::RecordBase::State_Deleted || locked); + mEditWidget->setDisabled(state == CSMWorld::RecordBase::State_Deleted || locked); - mCommandDispatcher.setEditLock (locked); + mCommandDispatcher.setEditLock(locked); } - } -void CSVWorld::SimpleDialogueSubView::dataChanged (const QModelIndex & index) +void CSVWorld::SimpleDialogueSubView::dataChanged(const QModelIndex& index) { - int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); QModelIndex currentIndex(mTable->getModelIndex(getUniversalId().getId(), idColumn)); - if (currentIndex.isValid() && - (index.parent().isValid() ? index.parent().row() : index.row()) == currentIndex.row()) + if (currentIndex.isValid() && (index.parent().isValid() ? index.parent().row() : index.row()) == currentIndex.row()) { - CSMWorld::RecordBase::State state = static_cast(mTable->data (mTable->index (currentIndex.row(), 1)).toInt()); + CSMWorld::RecordBase::State state + = static_cast(mTable->data(mTable->index(currentIndex.row(), 1)).toInt()); - mEditWidget->setDisabled (state==CSMWorld::RecordBase::State_Deleted || mLocked); + mEditWidget->setDisabled(state == CSMWorld::RecordBase::State_Deleted || mLocked); // Check if the changed data should force refresh (rebuild) the dialogue subview int flags = 0; if (index.parent().isValid()) // TODO: check that index is topLeft { - flags = static_cast(mTable)->nestedHeaderData (index.parent().column(), - index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags).toInt(); + flags = static_cast(mTable) + ->nestedHeaderData( + index.parent().column(), index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags) + .toInt(); } else { - flags = mTable->headerData (index.column(), - Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags).toInt(); + flags = mTable->headerData(index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags).toInt(); } if (flags & CSMWorld::ColumnBase::Flag_Dialogue_Refresh) { int y = mEditWidget->verticalScrollBar()->value(); - mEditWidget->remake (index.parent().isValid() ? index.parent().row() : index.row()); + mEditWidget->remake(index.parent().isValid() ? index.parent().row() : index.row()); mEditWidget->verticalScrollBar()->setValue(y); } } } -void CSVWorld::SimpleDialogueSubView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) +void CSVWorld::SimpleDialogueSubView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { - int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); QModelIndex currentIndex(mTable->getModelIndex(getUniversalId().getId(), idColumn)); if (!currentIndex.isValid()) @@ -830,7 +850,7 @@ void CSVWorld::SimpleDialogueSubView::rowsAboutToBeRemoved(const QModelIndex &pa if (currentIndex.parent() == parent && currentIndex.row() >= start && currentIndex.row() <= end) { - if(mEditWidget) + if (mEditWidget) { delete mEditWidget; mEditWidget = nullptr; @@ -842,59 +862,55 @@ void CSVWorld::SimpleDialogueSubView::rowsAboutToBeRemoved(const QModelIndex &pa void CSVWorld::SimpleDialogueSubView::updateCurrentId() { std::vector selection; - selection.push_back (getUniversalId().getId()); + selection.push_back(getUniversalId().getId()); mCommandDispatcher.setSelection(selection); } - void CSVWorld::DialogueSubView::addButtonBar() { if (mButtons) return; - mButtons = new RecordButtonBar (getUniversalId(), getTable(), mBottom, - &getCommandDispatcher(), this); + mButtons = new RecordButtonBar(getUniversalId(), getTable(), mBottom, &getCommandDispatcher(), this); - getMainLayout().insertWidget (1, mButtons); + getMainLayout().insertWidget(1, mButtons); // connections - connect (mButtons, SIGNAL (showPreview()), this, SLOT (showPreview())); - connect (mButtons, SIGNAL (viewRecord()), this, SLOT (viewRecord())); - connect (mButtons, SIGNAL (switchToRow (int)), this, SLOT (switchToRow (int))); + connect(mButtons, &RecordButtonBar::showPreview, this, &DialogueSubView::showPreview); + connect(mButtons, &RecordButtonBar::viewRecord, this, &DialogueSubView::viewRecord); + connect(mButtons, &RecordButtonBar::switchToRow, this, &DialogueSubView::switchToRow); - connect (this, SIGNAL (universalIdChanged (const CSMWorld::UniversalId&)), - mButtons, SLOT (universalIdChanged (const CSMWorld::UniversalId&))); + connect(this, &DialogueSubView::universalIdChanged, mButtons, &RecordButtonBar::universalIdChanged); } -CSVWorld::DialogueSubView::DialogueSubView (const CSMWorld::UniversalId& id, - CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting) -: SimpleDialogueSubView (id, document), mButtons (nullptr) +CSVWorld::DialogueSubView::DialogueSubView( + const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting) + : SimpleDialogueSubView(id, document) + , mButtons(nullptr) { // bottom box - mBottom = new TableBottomBox (creatorFactory, document, id, this); + mBottom = new TableBottomBox(creatorFactory, document, id, this); - connect (mBottom, SIGNAL (requestFocus (const std::string&)), - this, SLOT (requestFocus (const std::string&))); + connect(mBottom, &TableBottomBox::requestFocus, this, &DialogueSubView::requestFocus); // layout - getMainLayout().addWidget (mBottom); + getMainLayout().addWidget(mBottom); - connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), - this, SLOT (settingChanged (const CSMPrefs::Setting *))); + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &DialogueSubView::settingChanged); CSMPrefs::get()["ID Dialogues"].update(); } -void CSVWorld::DialogueSubView::setEditLock (bool locked) +void CSVWorld::DialogueSubView::setEditLock(bool locked) { - SimpleDialogueSubView::setEditLock (locked); + SimpleDialogueSubView::setEditLock(locked); if (mButtons) - mButtons->setEditLock (locked); + mButtons->setEditLock(locked); } -void CSVWorld::DialogueSubView::settingChanged (const CSMPrefs::Setting *setting) +void CSVWorld::DialogueSubView::settingChanged(const CSMPrefs::Setting* setting) { - if (*setting=="ID Dialogues/toolbar") + if (*setting == "ID Dialogues/toolbar") { if (setting->isTrue()) { @@ -902,67 +918,65 @@ void CSVWorld::DialogueSubView::settingChanged (const CSMPrefs::Setting *setting } else if (mButtons) { - getMainLayout().removeWidget (mButtons); + getMainLayout().removeWidget(mButtons); delete mButtons; mButtons = nullptr; } } } -void CSVWorld::DialogueSubView::showPreview () +void CSVWorld::DialogueSubView::showPreview() { - int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); - QModelIndex currentIndex (getTable().getModelIndex (getUniversalId().getId(), idColumn)); + int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); + QModelIndex currentIndex(getTable().getModelIndex(getUniversalId().getId(), idColumn)); - if (currentIndex.isValid() && - getTable().getFeatures() & CSMWorld::IdTable::Feature_Preview && - currentIndex.row() < getTable().rowCount()) + if (currentIndex.isValid() && getTable().getFeatures() & CSMWorld::IdTable::Feature_Preview + && currentIndex.row() < getTable().rowCount()) { emit focusId(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Preview, getUniversalId().getId()), ""); } } -void CSVWorld::DialogueSubView::viewRecord () +void CSVWorld::DialogueSubView::viewRecord() { - int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); - QModelIndex currentIndex (getTable().getModelIndex (getUniversalId().getId(), idColumn)); + int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); + QModelIndex currentIndex(getTable().getModelIndex(getUniversalId().getId(), idColumn)); - if (currentIndex.isValid() && - currentIndex.row() < getTable().rowCount()) + if (currentIndex.isValid() && currentIndex.row() < getTable().rowCount()) { - std::pair params = getTable().view (currentIndex.row()); + std::pair params = getTable().view(currentIndex.row()); - if (params.first.getType()!=CSMWorld::UniversalId::Type_None) - emit focusId (params.first, params.second); + if (params.first.getType() != CSMWorld::UniversalId::Type_None) + emit focusId(params.first, params.second); } } -void CSVWorld::DialogueSubView::switchToRow (int row) +void CSVWorld::DialogueSubView::switchToRow(int row) { - int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); - std::string id = getTable().data (getTable().index (row, idColumn)).toString().toUtf8().constData(); + int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); + std::string id = getTable().data(getTable().index(row, idColumn)).toString().toUtf8().constData(); - int typeColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); - CSMWorld::UniversalId::Type type = static_cast ( - getTable().data (getTable().index (row, typeColumn)).toInt()); + int typeColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_RecordType); + CSMWorld::UniversalId::Type type + = static_cast(getTable().data(getTable().index(row, typeColumn)).toInt()); - setUniversalId (CSMWorld::UniversalId (type, id)); + setUniversalId(CSMWorld::UniversalId(type, id)); updateCurrentId(); - getEditWidget().remake (row); + getEditWidget().remake(row); - int stateColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Modification); - CSMWorld::RecordBase::State state = static_cast ( - getTable().data (getTable().index (row, stateColumn)).toInt()); + int stateColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Modification); + CSMWorld::RecordBase::State state + = static_cast(getTable().data(getTable().index(row, stateColumn)).toInt()); - getEditWidget().setDisabled (isLocked() || state==CSMWorld::RecordBase::State_Deleted); + getEditWidget().setDisabled(isLocked() || state == CSMWorld::RecordBase::State_Deleted); } -void CSVWorld::DialogueSubView::requestFocus (const std::string& id) +void CSVWorld::DialogueSubView::requestFocus(const std::string& id) { - int idColumn = getTable().findColumnIndex (CSMWorld::Columns::ColumnId_Id); - QModelIndex index = getTable().getModelIndex (id, idColumn); + int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); + QModelIndex index = getTable().getModelIndex(id, idColumn); if (index.isValid()) - switchToRow (index.row()); + switchToRow(index.row()); } diff --git a/apps/opencs/view/world/dialoguesubview.hpp b/apps/opencs/view/world/dialoguesubview.hpp index 2cf05f711e5..4d05a956c93 100644 --- a/apps/opencs/view/world/dialoguesubview.hpp +++ b/apps/opencs/view/world/dialoguesubview.hpp @@ -1,12 +1,16 @@ #ifndef CSV_WORLD_DIALOGUESUBVIEW_H #define CSV_WORLD_DIALOGUESUBVIEW_H -#include #include #include +#include +#include +#include #include +#include #include +#include #ifndef Q_MOC_RUN #include "../doc/subview.hpp" @@ -16,12 +20,16 @@ #include "../../model/world/universalid.hpp" #endif +class QAbstractItemModel; +class QAction; class QDataWidgetMapper; +class QMenu; +class QModelIndex; +class QPainter; +class QPoint; class QSize; -class QEvent; -class QLabel; class QVBoxLayout; -class QMenu; +class QWidget; namespace CSMWorld { @@ -48,49 +56,35 @@ namespace CSVWorld class NotEditableSubDelegate : public QAbstractItemDelegate { const CSMWorld::IdTable* mTable; + public: - NotEditableSubDelegate(const CSMWorld::IdTable* table, - QObject * parent = nullptr); + NotEditableSubDelegate(const CSMWorld::IdTable* table, QObject* parent = nullptr); - void setEditorData (QWidget* editor, const QModelIndex& index) const override; + void setEditorData(QWidget* editor, const QModelIndex& index) const override; - void setModelData (QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; - void paint (QPainter* painter, - const QStyleOptionViewItem& option, - const QModelIndex& index) const override; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; ///< does nothing - QSize sizeHint (const QStyleOptionViewItem& option, - const QModelIndex& index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; ///< does nothing - QWidget *createEditor (QWidget *parent, - const QStyleOptionViewItem& option, - const QModelIndex& index) const override; + QWidget* createEditor( + QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; }; - //this can't be nested into the DialogueDelegateDispatcher, because it needs to emit signals class DialogueDelegateDispatcherProxy : public QObject { Q_OBJECT - class refWrapper - { - public: - refWrapper(const QModelIndex& index); - - const QModelIndex& mIndex; - }; - QWidget* mEditor; CSMWorld::ColumnBase::Display mDisplay; - std::unique_ptr mIndexWrapper; + std::optional mIndex; public: - DialogueDelegateDispatcherProxy(QWidget* editor, - CSMWorld::ColumnBase::Display display); + DialogueDelegateDispatcherProxy(QWidget* editor, CSMWorld::ColumnBase::Display display); QWidget* getEditor() const; public slots: @@ -98,10 +92,7 @@ namespace CSVWorld void setIndex(const QModelIndex& index); signals: - void editorDataCommited(QWidget* editor, - const QModelIndex& index, - CSMWorld::ColumnBase::Display display); - + void editorDataCommited(QWidget* editor, const QModelIndex& index, CSMWorld::ColumnBase::Display display); }; class DialogueDelegateDispatcher : public QAbstractItemDelegate @@ -119,14 +110,12 @@ namespace CSVWorld NotEditableSubDelegate mNotEditableDelegate; std::vector mProxys; - //once we move to the C++11 we should use unique_ptr + // once we move to the C++11 we should use unique_ptr public: - DialogueDelegateDispatcher(QObject* parent, - CSMWorld::IdTable* table, - CSMWorld::CommandDispatcher& commandDispatcher, - CSMDoc::Document& document, - QAbstractItemModel* model = nullptr); + DialogueDelegateDispatcher(QObject* parent, CSMWorld::IdTable* table, + CSMWorld::CommandDispatcher& commandDispatcher, CSMDoc::Document& document, + QAbstractItemModel* model = nullptr); ~DialogueDelegateDispatcher(); @@ -134,161 +123,148 @@ namespace CSVWorld QWidget* makeEditor(CSMWorld::ColumnBase::Display display, const QModelIndex& index); ///< will return null if delegate is not present, parent of the widget is - //same as for dispatcher itself + // same as for dispatcher itself - void setEditorData (QWidget* editor, const QModelIndex& index) const override; + void setEditorData(QWidget* editor, const QModelIndex& index) const override; - void setModelData (QWidget* editor, QAbstractItemModel* model, - const QModelIndex& index) const override; + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; - virtual void setModelData (QWidget* editor, - QAbstractItemModel* model, const QModelIndex& index, - CSMWorld::ColumnBase::Display display) const; + virtual void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index, + CSMWorld::ColumnBase::Display display) const; - void paint (QPainter* painter, - const QStyleOptionViewItem& option, - const QModelIndex& index) const override; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; ///< does nothing - QSize sizeHint (const QStyleOptionViewItem& option, - const QModelIndex& index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; ///< does nothing private slots: - void editorDataCommited(QWidget* editor, const QModelIndex& index, - CSMWorld::ColumnBase::Display display); + void editorDataCommited(QWidget* editor, const QModelIndex& index, CSMWorld::ColumnBase::Display display); }; /// A context menu with "Edit 'ID'" action for editors in the dialogue subview class IdContextMenu : public QObject { - Q_OBJECT + Q_OBJECT - QWidget *mWidget; - CSMWorld::UniversalId::Type mIdType; - std::set mExcludedIds; - ///< A list of IDs that should not have the Edit 'ID' action. + QWidget* mWidget; + CSMWorld::UniversalId::Type mIdType; + std::set mExcludedIds; + ///< A list of IDs that should not have the Edit 'ID' action. - QMenu *mContextMenu; - QAction *mEditIdAction; + QMenu* mContextMenu; + QAction* mEditIdAction; - QString getWidgetValue() const; - void addEditIdActionToMenu(const QString &text); - void removeEditIdActionFromMenu(); + QString getWidgetValue() const; + void addEditIdActionToMenu(const QString& text); + void removeEditIdActionFromMenu(); - public: - IdContextMenu(QWidget *widget, CSMWorld::ColumnBase::Display display); + public: + IdContextMenu(QWidget* widget, CSMWorld::ColumnBase::Display display); - void excludeId(const std::string &id); + void excludeId(const std::string& id); - private slots: - void showContextMenu(const QPoint &pos); - void editIdRequest(); + private slots: + void showContextMenu(const QPoint& pos); + void editIdRequest(); - signals: - void editIdRequest(const CSMWorld::UniversalId &id, const std::string &hint); + signals: + void editIdRequest(const CSMWorld::UniversalId& id, const std::string& hint); }; class EditWidget : public QScrollArea { Q_OBJECT - QDataWidgetMapper *mWidgetMapper; - QDataWidgetMapper *mNestedTableMapper; - DialogueDelegateDispatcher *mDispatcher; - DialogueDelegateDispatcher *mNestedTableDispatcher; - QWidget* mMainWidget; - CSMWorld::IdTable* mTable; - CSMWorld::CommandDispatcher& mCommandDispatcher; - CSMDoc::Document& mDocument; - std::vector mNestedModels; //Plain, raw C pointers, deleted in the dtor - - void createEditorContextMenu(QWidget *editor, - CSMWorld::ColumnBase::Display display, - int currentRow) const; - public: - - EditWidget (QWidget *parent, int row, CSMWorld::IdTable* table, - CSMWorld::CommandDispatcher& commandDispatcher, - CSMDoc::Document& document, bool createAndDelete = false); - - virtual ~EditWidget(); - - void remake(int row); - - signals: - void editIdRequest(const CSMWorld::UniversalId &id, const std::string &hint); + QDataWidgetMapper* mWidgetMapper; + QDataWidgetMapper* mNestedTableMapper; + DialogueDelegateDispatcher* mDispatcher; + DialogueDelegateDispatcher* mNestedTableDispatcher; + QWidget* mMainWidget; + CSMWorld::IdTable* mTable; + CSMWorld::CommandDispatcher& mCommandDispatcher; + CSMDoc::Document& mDocument; + std::vector mNestedModels; // Plain, raw C pointers, deleted in the dtor + + void createEditorContextMenu(QWidget* editor, CSMWorld::ColumnBase::Display display, int currentRow) const; + + public: + EditWidget(QWidget* parent, int row, CSMWorld::IdTable* table, CSMWorld::CommandDispatcher& commandDispatcher, + CSMDoc::Document& document, bool createAndDelete = false); + + virtual ~EditWidget(); + + void remake(int row); + + signals: + void editIdRequest(const CSMWorld::UniversalId& id, const std::string& hint); }; class SimpleDialogueSubView : public CSVDoc::SubView { - Q_OBJECT - - EditWidget* mEditWidget; - QVBoxLayout* mMainLayout; - CSMWorld::IdTable* mTable; - bool mLocked; - const CSMDoc::Document& mDocument; - CSMWorld::CommandDispatcher mCommandDispatcher; - - protected: + Q_OBJECT - QVBoxLayout& getMainLayout(); + EditWidget* mEditWidget; + QVBoxLayout* mMainLayout; + CSMWorld::IdTable* mTable; + bool mLocked; + const CSMDoc::Document& mDocument; + CSMWorld::CommandDispatcher mCommandDispatcher; - CSMWorld::IdTable& getTable(); + protected: + QVBoxLayout& getMainLayout(); - CSMWorld::CommandDispatcher& getCommandDispatcher(); + CSMWorld::IdTable& getTable(); - EditWidget& getEditWidget(); + CSMWorld::CommandDispatcher& getCommandDispatcher(); - void updateCurrentId(); + EditWidget& getEditWidget(); - bool isLocked() const; + void updateCurrentId(); - public: + bool isLocked() const; - SimpleDialogueSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); + public: + SimpleDialogueSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); - void setEditLock (bool locked) override; + void setEditLock(bool locked) override; - private slots: + private slots: - void dataChanged(const QModelIndex & index); - ///\brief we need to care for deleting currently edited record + void dataChanged(const QModelIndex& index); + ///\brief we need to care for deleting currently edited record - void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); + void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); }; class RecordButtonBar; class DialogueSubView : public SimpleDialogueSubView { - Q_OBJECT - - TableBottomBox* mBottom; - RecordButtonBar *mButtons; - - private: + Q_OBJECT - void addButtonBar(); + TableBottomBox* mBottom; + RecordButtonBar* mButtons; - public: + private: + void addButtonBar(); - DialogueSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, - const CreatorFactoryBase& creatorFactory, bool sorting = false); + public: + DialogueSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document, + const CreatorFactoryBase& creatorFactory, bool sorting = false); - void setEditLock (bool locked) override; + void setEditLock(bool locked) override; - private slots: + private slots: - void settingChanged (const CSMPrefs::Setting *setting); + void settingChanged(const CSMPrefs::Setting* setting); - void showPreview(); + void showPreview(); - void viewRecord(); + void viewRecord(); - void switchToRow (int row); + void switchToRow(int row); - void requestFocus (const std::string& id); + void requestFocus(const std::string& id); }; } diff --git a/apps/opencs/view/world/dragdroputils.cpp b/apps/opencs/view/world/dragdroputils.cpp index bb4d9727378..e6c68fa12eb 100644 --- a/apps/opencs/view/world/dragdroputils.cpp +++ b/apps/opencs/view/world/dragdroputils.cpp @@ -2,33 +2,43 @@ #include +#include +#include + #include "../../model/world/tablemimedata.hpp" -const CSMWorld::TableMimeData *CSVWorld::DragDropUtils::getTableMimeData(const QDropEvent &event) +const CSMWorld::TableMimeData* CSVWorld::DragDropUtils::getTableMimeData(const QDropEvent& event) { - return dynamic_cast(event.mimeData()); + return dynamic_cast(event.mimeData()); } -bool CSVWorld::DragDropUtils::canAcceptData(const QDropEvent &event, CSMWorld::ColumnBase::Display type) +bool CSVWorld::DragDropUtils::canAcceptData(const QDropEvent& event, CSMWorld::ColumnBase::Display type) { - const CSMWorld::TableMimeData *data = getTableMimeData(event); + const CSMWorld::TableMimeData* data = getTableMimeData(event); return data != nullptr && data->holdsType(type); } -bool CSVWorld::DragDropUtils::isInfo(const QDropEvent &event, CSMWorld::ColumnBase::Display type) +bool CSVWorld::DragDropUtils::isTopicOrJournal(const QDropEvent& event, CSMWorld::ColumnBase::Display type) +{ + const CSMWorld::TableMimeData* data = getTableMimeData(event); + return data != nullptr + && (data->holdsType(CSMWorld::UniversalId::Type_Topic) || data->holdsType(CSMWorld::UniversalId::Type_Journal)); +} + +bool CSVWorld::DragDropUtils::isInfo(const QDropEvent& event, CSMWorld::ColumnBase::Display type) { - const CSMWorld::TableMimeData *data = getTableMimeData(event); - return data != nullptr && ( - data->holdsType(CSMWorld::UniversalId::Type_TopicInfo) || - data->holdsType(CSMWorld::UniversalId::Type_JournalInfo) ); + const CSMWorld::TableMimeData* data = getTableMimeData(event); + return data != nullptr + && (data->holdsType(CSMWorld::UniversalId::Type_TopicInfo) + || data->holdsType(CSMWorld::UniversalId::Type_JournalInfo)); } -CSMWorld::UniversalId CSVWorld::DragDropUtils::getAcceptedData(const QDropEvent &event, - CSMWorld::ColumnBase::Display type) +CSMWorld::UniversalId CSVWorld::DragDropUtils::getAcceptedData( + const QDropEvent& event, CSMWorld::ColumnBase::Display type) { if (canAcceptData(event, type)) { - if (const CSMWorld::TableMimeData *data = getTableMimeData(event)) + if (const CSMWorld::TableMimeData* data = getTableMimeData(event)) return data->returnMatching(type); } return CSMWorld::UniversalId::Type_None; diff --git a/apps/opencs/view/world/dragdroputils.hpp b/apps/opencs/view/world/dragdroputils.hpp index 2181e7606b8..bf822180cdf 100644 --- a/apps/opencs/view/world/dragdroputils.hpp +++ b/apps/opencs/view/world/dragdroputils.hpp @@ -15,15 +15,17 @@ namespace CSVWorld { namespace DragDropUtils { - const CSMWorld::TableMimeData *getTableMimeData(const QDropEvent &event); + const CSMWorld::TableMimeData* getTableMimeData(const QDropEvent& event); - bool canAcceptData(const QDropEvent &event, CSMWorld::ColumnBase::Display type); + bool canAcceptData(const QDropEvent& event, CSMWorld::ColumnBase::Display type); ///< Checks whether the \a event contains a valid CSMWorld::TableMimeData that holds the \a type - bool isInfo(const QDropEvent &event, CSMWorld::ColumnBase::Display type); + bool isTopicOrJournal(const QDropEvent& event, CSMWorld::ColumnBase::Display type); + + bool isInfo(const QDropEvent& event, CSMWorld::ColumnBase::Display type); ///< Info types can be dragged to sort the info table - CSMWorld::UniversalId getAcceptedData(const QDropEvent &event, CSMWorld::ColumnBase::Display type); + CSMWorld::UniversalId getAcceptedData(const QDropEvent& event, CSMWorld::ColumnBase::Display type); ///< Gets the accepted data from the \a event using the \a type ///< \return Type_None if the \a event data doesn't holds the \a type } diff --git a/apps/opencs/view/world/dragrecordtable.cpp b/apps/opencs/view/world/dragrecordtable.cpp index 58041af9fce..a006779ba4f 100644 --- a/apps/opencs/view/world/dragrecordtable.cpp +++ b/apps/opencs/view/world/dragrecordtable.cpp @@ -3,14 +3,22 @@ #include #include +#include +#include + #include "../../model/doc/document.hpp" -#include "../../model/world/tablemimedata.hpp" #include "../../model/world/commands.hpp" +#include "../../model/world/tablemimedata.hpp" + +#include +#include + +#include #include "dragdroputils.hpp" -void CSVWorld::DragRecordTable::startDragFromTable (const CSVWorld::DragRecordTable& table) +void CSVWorld::DragRecordTable::startDragFromTable(const CSVWorld::DragRecordTable& table, const QModelIndex& index) { std::vector records = table.getDraggedRecords(); if (records.empty()) @@ -18,36 +26,39 @@ void CSVWorld::DragRecordTable::startDragFromTable (const CSVWorld::DragRecordTa return; } - CSMWorld::TableMimeData* mime = new CSMWorld::TableMimeData (records, mDocument); - QDrag* drag = new QDrag (this); - drag->setMimeData (mime); - drag->setPixmap (QString::fromUtf8 (mime->getIcon().c_str())); - drag->exec (Qt::CopyAction); + CSMWorld::TableMimeData* mime = new CSMWorld::TableMimeData(records, mDocument); + mime->setTableOfDragStart(&table); + mime->setIndexAtDragStart(index); + QDrag* drag = new QDrag(this); + drag->setMimeData(mime); + drag->setPixmap(Misc::ScalableIcon::load(mime->getIcon().c_str()).pixmap(QSize(16, 16))); + drag->exec(Qt::CopyAction); } -CSVWorld::DragRecordTable::DragRecordTable (CSMDoc::Document& document, QWidget* parent) : -QTableView(parent), -mDocument(document), -mEditLock(false) +CSVWorld::DragRecordTable::DragRecordTable(CSMDoc::Document& document, QWidget* parent) + : QTableView(parent) + , mDocument(document) + , mEditLock(false) { setAcceptDrops(true); } -void CSVWorld::DragRecordTable::setEditLock (bool locked) +void CSVWorld::DragRecordTable::setEditLock(bool locked) { mEditLock = locked; } -void CSVWorld::DragRecordTable::dragEnterEvent(QDragEnterEvent *event) +void CSVWorld::DragRecordTable::dragEnterEvent(QDragEnterEvent* event) { event->acceptProposedAction(); } -void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent *event) +void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent* event) { QModelIndex index = indexAt(event->pos()); - if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index)) || - CSVWorld::DragDropUtils::isInfo(*event, getIndexDisplayType(index)) ) + if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index)) + || CSVWorld::DragDropUtils::isInfo(*event, getIndexDisplayType(index)) + || CSVWorld::DragDropUtils::isTopicOrJournal(*event, getIndexDisplayType(index))) { if (index.flags() & Qt::ItemIsEditable) { @@ -58,13 +69,13 @@ void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent *event) event->ignore(); } -void CSVWorld::DragRecordTable::dropEvent(QDropEvent *event) +void CSVWorld::DragRecordTable::dropEvent(QDropEvent* event) { QModelIndex index = indexAt(event->pos()); CSMWorld::ColumnBase::Display display = getIndexDisplayType(index); if (CSVWorld::DragDropUtils::canAcceptData(*event, display)) { - const CSMWorld::TableMimeData *tableMimeData = CSVWorld::DragDropUtils::getTableMimeData(*event); + const CSMWorld::TableMimeData* tableMimeData = CSVWorld::DragDropUtils::getTableMimeData(*event); if (tableMimeData->fromDocument(mDocument)) { CSMWorld::UniversalId id = CSVWorld::DragDropUtils::getAcceptedData(*event, display); @@ -80,9 +91,17 @@ void CSVWorld::DragRecordTable::dropEvent(QDropEvent *event) { emit moveRecordsFromSameTable(event); } + if (CSVWorld::DragDropUtils::isTopicOrJournal(*event, display)) + { + const CSMWorld::TableMimeData* tableMimeData = CSVWorld::DragDropUtils::getTableMimeData(*event); + for (const auto& universalId : tableMimeData->getData()) + { + emit createNewInfoRecord(universalId.getId()); + } + } } -CSMWorld::ColumnBase::Display CSVWorld::DragRecordTable::getIndexDisplayType(const QModelIndex &index) const +CSMWorld::ColumnBase::Display CSVWorld::DragRecordTable::getIndexDisplayType(const QModelIndex& index) const { Q_ASSERT(model() != nullptr); @@ -96,3 +115,12 @@ CSMWorld::ColumnBase::Display CSVWorld::DragRecordTable::getIndexDisplayType(con } return CSMWorld::ColumnBase::Display_None; } + +int CSVWorld::DragRecordTable::sizeHintForColumn(int column) const +{ + // Prevent the column width from getting too long or too short + constexpr int minWidth = 100; + constexpr int maxWidth = 300; + int width = QTableView::sizeHintForColumn(column); + return std::clamp(width, minWidth, maxWidth); +} diff --git a/apps/opencs/view/world/dragrecordtable.hpp b/apps/opencs/view/world/dragrecordtable.hpp index f6c3fa89099..8653c2747fb 100644 --- a/apps/opencs/view/world/dragrecordtable.hpp +++ b/apps/opencs/view/world/dragrecordtable.hpp @@ -2,12 +2,17 @@ #define CSV_WORLD_DRAGRECORDTABLE_H #include -#include + +#include #include "../../model/world/columnbase.hpp" +class QDragEnterEvent; +class QDragMoveEvent; +class QDropEvent; +class QModelIndex; +class QObject; class QWidget; -class QAction; namespace CSMDoc { @@ -25,33 +30,35 @@ namespace CSVWorld { Q_OBJECT - protected: - CSMDoc::Document& mDocument; - bool mEditLock; + protected: + CSMDoc::Document& mDocument; + bool mEditLock; + + public: + DragRecordTable(CSMDoc::Document& document, QWidget* parent = nullptr); - public: - DragRecordTable(CSMDoc::Document& document, QWidget* parent = nullptr); + virtual std::vector getDraggedRecords() const = 0; - virtual std::vector getDraggedRecords() const = 0; + void setEditLock(bool locked); - void setEditLock(bool locked); + protected: + void startDragFromTable(const DragRecordTable& table, const QModelIndex& index); - protected: - void startDragFromTable(const DragRecordTable& table); + void dragEnterEvent(QDragEnterEvent* event) override; - void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent* event) override; - void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent* event) override; - void dropEvent(QDropEvent *event) override; + int sizeHintForColumn(int column) const override; - private: - CSMWorld::ColumnBase::Display getIndexDisplayType(const QModelIndex &index) const; + private: + CSMWorld::ColumnBase::Display getIndexDisplayType(const QModelIndex& index) const; - signals: - void moveRecordsFromSameTable(QDropEvent *event); + signals: + void moveRecordsFromSameTable(QDropEvent* event); + void createNewInfoRecord(const std::string& id); }; } #endif - diff --git a/apps/opencs/view/world/enumdelegate.cpp b/apps/opencs/view/world/enumdelegate.cpp index 65ded46c7f5..188860e2a6a 100644 --- a/apps/opencs/view/world/enumdelegate.cpp +++ b/apps/opencs/view/world/enumdelegate.cpp @@ -1,15 +1,16 @@ #include "enumdelegate.hpp" #include -#include +#include -#include #include -#include +#include #include "../../model/world/commands.hpp" -int CSVWorld::EnumDelegate::getValueIndex(const QModelIndex &index, int role) const +#include + +int CSVWorld::EnumDelegate::getValueIndex(const QModelIndex& index, int role) const { if (index.isValid() && index.data(role).isValid()) { @@ -27,64 +28,60 @@ int CSVWorld::EnumDelegate::getValueIndex(const QModelIndex &index, int role) co return -1; } -void CSVWorld::EnumDelegate::setModelDataImp (QWidget *editor, QAbstractItemModel *model, - const QModelIndex& index) const +void CSVWorld::EnumDelegate::setModelDataImp(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { - if (QComboBox *comboBox = dynamic_cast (editor)) + if (QComboBox* comboBox = dynamic_cast(editor)) { QString value = comboBox->currentText(); - for (std::vector >::const_iterator iter (mValues.begin()); - iter!=mValues.end(); ++iter) - if (iter->second==value) + for (std::vector>::const_iterator iter(mValues.begin()); iter != mValues.end(); ++iter) + if (iter->second == value) { // do nothing if the value has not changed if (model->data(index).toInt() != iter->first) - addCommands (model, index, iter->first); + addCommands(model, index, iter->first); break; } } } -void CSVWorld::EnumDelegate::addCommands (QAbstractItemModel *model, - const QModelIndex& index, int type) const +void CSVWorld::EnumDelegate::addCommands(QAbstractItemModel* model, const QModelIndex& index, int type) const { - getUndoStack().push (new CSMWorld::ModifyCommand (*model, index, type)); + getUndoStack().push(new CSMWorld::ModifyCommand(*model, index, type)); } - -CSVWorld::EnumDelegate::EnumDelegate (const std::vector >& values, - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) -: CommandDelegate (dispatcher, document, parent), mValues (values) +CSVWorld::EnumDelegate::EnumDelegate(const std::vector>& values, + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) + : CommandDelegate(dispatcher, document, parent) + , mValues(values) { - } -QWidget *CSVWorld::EnumDelegate::createEditor(QWidget *parent, - const QStyleOptionViewItem& option, - const QModelIndex& index) const +QWidget* CSVWorld::EnumDelegate::createEditor( + QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_None); } -QWidget *CSVWorld::EnumDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem& option, +QWidget* CSVWorld::EnumDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index, CSMWorld::ColumnBase::Display display) const { if (!index.data(Qt::EditRole).isValid() && !index.data(Qt::DisplayRole).isValid()) return nullptr; - QComboBox *comboBox = new QComboBox (parent); + QComboBox* comboBox = new QComboBox(parent); + + for (std::vector>::const_iterator iter(mValues.begin()); iter != mValues.end(); ++iter) + comboBox->addItem(iter->second); - for (std::vector >::const_iterator iter (mValues.begin()); - iter!=mValues.end(); ++iter) - comboBox->addItem (iter->second); + comboBox->setMaxVisibleItems(20); return comboBox; } -void CSVWorld::EnumDelegate::setEditorData (QWidget *editor, const QModelIndex& index, bool tryDisplay) const +void CSVWorld::EnumDelegate::setEditorData(QWidget* editor, const QModelIndex& index, bool tryDisplay) const { - if (QComboBox *comboBox = dynamic_cast(editor)) + if (QComboBox* comboBox = dynamic_cast(editor)) { int role = Qt::EditRole; if (tryDisplay && !index.data(role).isValid()) @@ -104,8 +101,8 @@ void CSVWorld::EnumDelegate::setEditorData (QWidget *editor, const QModelIndex& } } -void CSVWorld::EnumDelegate::paint (QPainter *painter, const QStyleOptionViewItem& option, - const QModelIndex& index) const +void CSVWorld::EnumDelegate::paint( + QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { int valueIndex = getValueIndex(index); if (valueIndex != -1) @@ -116,7 +113,7 @@ void CSVWorld::EnumDelegate::paint (QPainter *painter, const QStyleOptionViewIte } } -QSize CSVWorld::EnumDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +QSize CSVWorld::EnumDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { int valueIndex = getValueIndex(index); if (valueIndex != -1) @@ -129,7 +126,7 @@ QSize CSVWorld::EnumDelegate::sizeHint(const QStyleOptionViewItem &option, const itemOption.rect = option.rect; itemOption.state = option.state; - const QString &valueText = mValues.at(valueIndex).second; + const QString& valueText = mValues.at(valueIndex).second; QSize valueSize = QSize(itemOption.fontMetrics.horizontalAdvance(valueText), itemOption.fontMetrics.height()); itemOption.currentText = valueText; return QApplication::style()->sizeFromContents(QStyle::CT_ComboBox, &itemOption, valueSize); @@ -137,42 +134,40 @@ QSize CSVWorld::EnumDelegate::sizeHint(const QStyleOptionViewItem &option, const return option.rect.size(); } -CSVWorld::EnumDelegateFactory::EnumDelegateFactory() {} - -CSVWorld::EnumDelegateFactory::EnumDelegateFactory (const char **names, bool allowNone) +CSVWorld::EnumDelegateFactory::EnumDelegateFactory(const char** names, bool allowNone) { - assert (names); + assert(names); if (allowNone) - add (-1, ""); + add(-1, ""); - for (int i=0; names[i]; ++i) - add (i, names[i]); + for (int i = 0; names[i]; ++i) + add(i, names[i]); } -CSVWorld::EnumDelegateFactory::EnumDelegateFactory (const std::vector>& names, - bool allowNone) +CSVWorld::EnumDelegateFactory::EnumDelegateFactory( + const std::vector>& names, bool allowNone) { if (allowNone) - add (-1, ""); + add(-1, ""); - int size = static_cast (names.size()); + int size = static_cast(names.size()); - for (int i=0; isecond > name) { @@ -181,5 +176,5 @@ void CSVWorld::EnumDelegateFactory::add (int value, const QString& name) } } - mValues.emplace_back (value, name); + mValues.emplace_back(value, name); } diff --git a/apps/opencs/view/world/enumdelegate.hpp b/apps/opencs/view/world/enumdelegate.hpp index 91326e2c051..e80b61fa8fd 100644 --- a/apps/opencs/view/world/enumdelegate.hpp +++ b/apps/opencs/view/world/enumdelegate.hpp @@ -1,80 +1,80 @@ #ifndef CSV_WORLD_ENUMDELEGATE_H #define CSV_WORLD_ENUMDELEGATE_H +#include +#include #include #include -#include -#include +#include #include "util.hpp" +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher; +} + namespace CSVWorld { /// \brief Integer value that represents an enum and is interacted with via a combobox class EnumDelegate : public CommandDelegate { - protected: + protected: + std::vector> mValues; - std::vector > mValues; + int getValueIndex(const QModelIndex& index, int role = Qt::DisplayRole) const; - int getValueIndex(const QModelIndex &index, int role = Qt::DisplayRole) const; + private: + void setModelDataImp(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; - private: + virtual void addCommands(QAbstractItemModel* model, const QModelIndex& index, int type) const; - void setModelDataImp (QWidget *editor, QAbstractItemModel *model, - const QModelIndex& index) const override; + public: + EnumDelegate(const std::vector>& values, CSMWorld::CommandDispatcher* dispatcher, + CSMDoc::Document& document, QObject* parent); - virtual void addCommands (QAbstractItemModel *model, - const QModelIndex& index, int type) const; + QWidget* createEditor( + QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; - public: + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index, + CSMWorld::ColumnBase::Display display = CSMWorld::ColumnBase::Display_None) const override; - EnumDelegate (const std::vector >& values, - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent); + void setEditorData(QWidget* editor, const QModelIndex& index, bool tryDisplay = false) const override; - QWidget *createEditor(QWidget *parent, - const QStyleOptionViewItem& option, - const QModelIndex& index) const override; - - QWidget *createEditor(QWidget *parent, - const QStyleOptionViewItem& option, - const QModelIndex& index, - CSMWorld::ColumnBase::Display display = CSMWorld::ColumnBase::Display_None) const override; - - void setEditorData (QWidget *editor, const QModelIndex& index, bool tryDisplay = false) const override; - - void paint (QPainter *painter, const QStyleOptionViewItem& option, - const QModelIndex& index) const override; - - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; }; class EnumDelegateFactory : public CommandDelegateFactory { - protected: - std::vector > mValues; - - public: + protected: + std::vector> mValues; - EnumDelegateFactory(); + public: + EnumDelegateFactory() = default; - EnumDelegateFactory (const char **names, bool allowNone = false); - ///< \param names Array of char pointer with a 0-pointer as end mark - /// \param allowNone Use value of -1 for "none selected" (empty string) + EnumDelegateFactory(const char** names, bool allowNone = false); + ///< \param names Array of char pointer with a 0-pointer as end mark + /// \param allowNone Use value of -1 for "none selected" (empty string) - EnumDelegateFactory (const std::vector>& names, bool allowNone = false); - /// \param allowNone Use value of -1 for "none selected" (empty string) + EnumDelegateFactory(const std::vector>& names, bool allowNone = false); + /// \param allowNone Use value of -1 for "none selected" (empty string) - CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const override; - ///< The ownership of the returned CommandDelegate is transferred to the caller. + CommandDelegate* makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const override; + ///< The ownership of the returned CommandDelegate is transferred to the caller. - void add (int value, const QString& name); + void add(int value, const QString& name); }; - } #endif diff --git a/apps/opencs/view/world/extendedcommandconfigurator.cpp b/apps/opencs/view/world/extendedcommandconfigurator.cpp index 89474202429..97494fa0762 100644 --- a/apps/opencs/view/world/extendedcommandconfigurator.cpp +++ b/apps/opencs/view/world/extendedcommandconfigurator.cpp @@ -1,48 +1,50 @@ #include "extendedcommandconfigurator.hpp" #include +#include +#include -#include -#include #include -#include +#include #include +#include #include "../../model/doc/document.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../../model/world/data.hpp" -CSVWorld::ExtendedCommandConfigurator::ExtendedCommandConfigurator(CSMDoc::Document &document, - const CSMWorld::UniversalId &id, - QWidget *parent) - : QWidget(parent), - mNumUsedCheckBoxes(0), - mNumChecked(0), - mMode(Mode_None), - mData(document.getData()), - mEditLock(false) +#include + +CSVWorld::ExtendedCommandConfigurator::ExtendedCommandConfigurator( + CSMDoc::Document& document, const CSMWorld::UniversalId& id, QWidget* parent) + : QWidget(parent) + , mNumUsedCheckBoxes(0) + , mNumChecked(0) + , mMode(Mode_None) + , mData(document.getData()) + , mEditLock(false) { mCommandDispatcher = new CSMWorld::CommandDispatcher(document, id, this); - connect(&mData, SIGNAL(idListChanged()), this, SLOT(dataIdListChanged())); + connect(&mData, &CSMWorld::Data::idListChanged, this, &ExtendedCommandConfigurator::dataIdListChanged); mPerformButton = new QPushButton(this); mPerformButton->setDefault(true); mPerformButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - connect(mPerformButton, SIGNAL(clicked(bool)), this, SLOT(performExtendedCommand())); + connect(mPerformButton, &QPushButton::clicked, this, &ExtendedCommandConfigurator::performExtendedCommand); mCancelButton = new QPushButton("Cancel", this); mCancelButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - connect(mCancelButton, SIGNAL(clicked(bool)), this, SIGNAL(done())); + connect(mCancelButton, &QPushButton::clicked, this, &ExtendedCommandConfigurator::done); mTypeGroup = new QGroupBox(this); - QGridLayout *groupLayout = new QGridLayout(mTypeGroup); + QGridLayout* groupLayout = new QGridLayout(mTypeGroup); groupLayout->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); mTypeGroup->setLayout(groupLayout); - QHBoxLayout *mainLayout = new QHBoxLayout(this); + QHBoxLayout* mainLayout = new QHBoxLayout(this); mainLayout->setSizeConstraint(QLayout::SetNoConstraint); mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->addWidget(mTypeGroup); @@ -50,8 +52,8 @@ CSVWorld::ExtendedCommandConfigurator::ExtendedCommandConfigurator(CSMDoc::Docum mainLayout->addWidget(mCancelButton); } -void CSVWorld::ExtendedCommandConfigurator::configure(CSVWorld::ExtendedCommandConfigurator::Mode mode, - const std::vector &selectedIds) +void CSVWorld::ExtendedCommandConfigurator::configure( + CSVWorld::ExtendedCommandConfigurator::Mode mode, const std::vector& selectedIds) { mMode = mode; if (mMode != Mode_None) @@ -75,7 +77,7 @@ void CSVWorld::ExtendedCommandConfigurator::setEditLock(bool locked) } } -void CSVWorld::ExtendedCommandConfigurator::resizeEvent(QResizeEvent *event) +void CSVWorld::ExtendedCommandConfigurator::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); setupGroupLayout(); @@ -89,7 +91,7 @@ void CSVWorld::ExtendedCommandConfigurator::setupGroupLayout() } int groupWidth = mTypeGroup->geometry().width(); - QGridLayout *layout = qobject_cast(mTypeGroup->layout()); + QGridLayout* layout = qobject_cast(mTypeGroup->layout()); // Find the optimal number of rows to place the checkboxes within the available space int divider = 1; @@ -115,21 +117,20 @@ void CSVWorld::ExtendedCommandConfigurator::setupGroupLayout() ++counter; } divider *= 2; - } - while (groupWidth < mTypeGroup->sizeHint().width() && divider <= mNumUsedCheckBoxes); + } while (groupWidth < mTypeGroup->sizeHint().width() && divider <= mNumUsedCheckBoxes); } -void CSVWorld::ExtendedCommandConfigurator::setupCheckBoxes(const std::vector &types) +void CSVWorld::ExtendedCommandConfigurator::setupCheckBoxes(const std::vector& types) { // Make sure that we have enough checkboxes - int numTypes = static_cast(types.size()); + int numTypes = static_cast(types.size()); int numCheckBoxes = static_cast(mTypeCheckBoxes.size()); if (numTypes > numCheckBoxes) { for (int i = numTypes - numCheckBoxes; i > 0; --i) { - QCheckBox *checkBox = new QCheckBox(mTypeGroup); - connect(checkBox, SIGNAL(stateChanged(int)), this, SLOT(checkBoxStateChanged(int))); + QCheckBox* checkBox = new QCheckBox(mTypeGroup); + connect(checkBox, &QCheckBox::stateChanged, this, &ExtendedCommandConfigurator::checkBoxStateChanged); mTypeCheckBoxes.insert(std::make_pair(checkBox, CSMWorld::UniversalId::Type_None)); } } @@ -145,7 +146,7 @@ void CSVWorld::ExtendedCommandConfigurator::setupCheckBoxes(const std::vectorfirst->setText(QString::fromUtf8(type.getTypeName().c_str())); current->first->setChecked(true); - current->second = type; + current->second = std::move(type); ++counter; } else @@ -171,7 +172,7 @@ void CSVWorld::ExtendedCommandConfigurator::lockWidgets(bool locked) void CSVWorld::ExtendedCommandConfigurator::performExtendedCommand() { std::vector types; - + CheckBoxMap::const_iterator current = mTypeCheckBoxes.begin(); CheckBoxMap::const_iterator end = mTypeCheckBoxes.end(); for (; current != end; ++current) diff --git a/apps/opencs/view/world/extendedcommandconfigurator.hpp b/apps/opencs/view/world/extendedcommandconfigurator.hpp index 85862ac49e2..e441cc39377 100644 --- a/apps/opencs/view/world/extendedcommandconfigurator.hpp +++ b/apps/opencs/view/world/extendedcommandconfigurator.hpp @@ -2,6 +2,7 @@ #define CSVWORLD_EXTENDEDCOMMANDCONFIGURATOR_HPP #include +#include #include @@ -10,8 +11,7 @@ class QPushButton; class QGroupBox; class QCheckBox; -class QLabel; -class QHBoxLayout; +class QResizeEvent; namespace CSMDoc { @@ -28,50 +28,54 @@ namespace CSVWorld { class ExtendedCommandConfigurator : public QWidget { - Q_OBJECT - - public: - enum Mode { Mode_None, Mode_Delete, Mode_Revert }; - - private: - typedef std::map CheckBoxMap; - - QPushButton *mPerformButton; - QPushButton *mCancelButton; - QGroupBox *mTypeGroup; - CheckBoxMap mTypeCheckBoxes; - int mNumUsedCheckBoxes; - int mNumChecked; - - Mode mMode; - CSMWorld::CommandDispatcher *mCommandDispatcher; - CSMWorld::Data &mData; - std::vector mSelectedIds; - - bool mEditLock; - - void setupGroupLayout(); - void setupCheckBoxes(const std::vector &types); - void lockWidgets(bool locked); - - public: - ExtendedCommandConfigurator(CSMDoc::Document &document, - const CSMWorld::UniversalId &id, - QWidget *parent = nullptr); - - void configure(Mode mode, const std::vector &selectedIds); - void setEditLock(bool locked); - - protected: - void resizeEvent(QResizeEvent *event) override; - - private slots: - void performExtendedCommand(); - void checkBoxStateChanged(int state); - void dataIdListChanged(); - - signals: - void done(); + Q_OBJECT + + public: + enum Mode + { + Mode_None, + Mode_Delete, + Mode_Revert + }; + + private: + typedef std::map CheckBoxMap; + + QPushButton* mPerformButton; + QPushButton* mCancelButton; + QGroupBox* mTypeGroup; + CheckBoxMap mTypeCheckBoxes; + int mNumUsedCheckBoxes; + int mNumChecked; + + Mode mMode; + CSMWorld::CommandDispatcher* mCommandDispatcher; + CSMWorld::Data& mData; + std::vector mSelectedIds; + + bool mEditLock; + + void setupGroupLayout(); + void setupCheckBoxes(const std::vector& types); + void lockWidgets(bool locked); + + public: + ExtendedCommandConfigurator( + CSMDoc::Document& document, const CSMWorld::UniversalId& id, QWidget* parent = nullptr); + + void configure(Mode mode, const std::vector& selectedIds); + void setEditLock(bool locked); + + protected: + void resizeEvent(QResizeEvent* event) override; + + private slots: + void performExtendedCommand(); + void checkBoxStateChanged(int state); + void dataIdListChanged(); + + signals: + void done(); }; } diff --git a/apps/opencs/view/world/genericcreator.cpp b/apps/opencs/view/world/genericcreator.cpp index 23813f80660..e4c084e46be 100644 --- a/apps/opencs/view/world/genericcreator.cpp +++ b/apps/opencs/view/world/genericcreator.cpp @@ -1,15 +1,18 @@ #include "genericcreator.hpp" #include +#include +#include #include -#include +#include #include +#include #include -#include -#include -#include +#include + +#include #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" @@ -21,25 +24,25 @@ void CSVWorld::GenericCreator::update() { mErrors = getErrors(); - mCreate->setToolTip (QString::fromUtf8 (mErrors.c_str())); - mId->setToolTip (QString::fromUtf8 (mErrors.c_str())); + mCreate->setToolTip(QString::fromUtf8(mErrors.c_str())); + mId->setToolTip(QString::fromUtf8(mErrors.c_str())); - mCreate->setEnabled (mErrors.empty() && !mLocked); + mCreate->setEnabled(mErrors.empty() && !mLocked); } -void CSVWorld::GenericCreator::setManualEditing (bool enabled) +void CSVWorld::GenericCreator::setManualEditing(bool enabled) { - mId->setVisible (enabled); + mId->setVisible(enabled); } -void CSVWorld::GenericCreator::insertAtBeginning (QWidget *widget, bool stretched) +void CSVWorld::GenericCreator::insertAtBeginning(QWidget* widget, bool stretched) { - mLayout->insertWidget (0, widget, stretched ? 1 : 0); + mLayout->insertWidget(0, widget, stretched ? 1 : 0); } -void CSVWorld::GenericCreator::insertBeforeButtons (QWidget *widget, bool stretched) +void CSVWorld::GenericCreator::insertBeforeButtons(QWidget* widget, bool stretched) { - mLayout->insertWidget (mLayout->count()-2, widget, stretched ? 1 : 0); + mLayout->insertWidget(mLayout->count() - 2, widget, stretched ? 1 : 0); // Reset tab order relative to buttons. setTabOrder(widget, mCreate); @@ -66,12 +69,11 @@ std::string CSVWorld::GenericCreator::getIdValidatorResult() const return errors; } -void CSVWorld::GenericCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const {} +void CSVWorld::GenericCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const {} -void CSVWorld::GenericCreator::pushCommand (std::unique_ptr command, - const std::string& id) +void CSVWorld::GenericCreator::pushCommand(std::unique_ptr command, const std::string& id) { - mUndoStack.push (command.release()); + mUndoStack.push(command.release()); } CSMWorld::Data& CSVWorld::GenericCreator::getData() const @@ -95,7 +97,7 @@ std::string CSVWorld::GenericCreator::getNamespace() const if (mScope) { - scope = static_cast (mScope->itemData (mScope->currentIndex()).toInt()); + scope = static_cast(mScope->itemData(mScope->currentIndex()).toInt()); } else { @@ -107,9 +109,12 @@ std::string CSVWorld::GenericCreator::getNamespace() const switch (scope) { - case CSMWorld::Scope_Content: return ""; - case CSMWorld::Scope_Project: return "project::"; - case CSMWorld::Scope_Session: return "session::"; + case CSMWorld::Scope_Content: + return ""; + case CSMWorld::Scope_Project: + return "project::"; + case CSMWorld::Scope_Session: + return "session::"; } return ""; @@ -119,37 +124,41 @@ void CSVWorld::GenericCreator::updateNamespace() { std::string namespace_ = getNamespace(); - mValidator->setNamespace (namespace_); + mValidator->setNamespace(namespace_); - int index = mId->text().indexOf ("::"); + int index = mId->text().indexOf("::"); - if (index==-1) + if (index == -1) { // no namespace in old text - mId->setText (QString::fromUtf8 (namespace_.c_str()) + mId->text()); + mId->setText(QString::fromUtf8(namespace_.c_str()) + mId->text()); } else { - std::string oldNamespace = - Misc::StringUtils::lowerCase (mId->text().left (index).toUtf8().constData()); + std::string oldNamespace = Misc::StringUtils::lowerCase(mId->text().left(index).toUtf8().constData()); - if (oldNamespace=="project" || oldNamespace=="session") - mId->setText (QString::fromUtf8 (namespace_.c_str()) + mId->text().mid (index+2)); + if (oldNamespace == "project" || oldNamespace == "session") + mId->setText(QString::fromUtf8(namespace_.c_str()) + mId->text().mid(index + 2)); } } -void CSVWorld::GenericCreator::addScope (const QString& name, CSMWorld::Scope scope, - const QString& tooltip) +void CSVWorld::GenericCreator::addScope(const QString& name, CSMWorld::Scope scope, const QString& tooltip) { - mScope->addItem (name, static_cast (scope)); - mScope->setItemData (mScope->count()-1, tooltip, Qt::ToolTipRole); + mScope->addItem(name, static_cast(scope)); + mScope->setItemData(mScope->count() - 1, tooltip, Qt::ToolTipRole); } -CSVWorld::GenericCreator::GenericCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id, bool relaxedIdRules) -: mData (data), mUndoStack (undoStack), mListId (id), mLocked (false), - mClonedType (CSMWorld::UniversalId::Type_None), mScopes (CSMWorld::Scope_Content), mScope (nullptr), - mScopeLabel (nullptr), mCloneMode (false) +CSVWorld::GenericCreator::GenericCreator( + CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, bool relaxedIdRules) + : mData(data) + , mUndoStack(undoStack) + , mListId(id) + , mLocked(false) + , mClonedType(CSMWorld::UniversalId::Type_None) + , mScopes(CSMWorld::Scope_Content) + , mScope(nullptr) + , mScopeLabel(nullptr) + , mCloneMode(false) { // If the collection ID has a parent type, use it instead. // It will change IDs with Record/SubRecord class (used for creators in Dialogue subviews) @@ -161,30 +170,35 @@ CSVWorld::GenericCreator::GenericCreator (CSMWorld::Data& data, QUndoStack& undo } mLayout = new QHBoxLayout; - mLayout->setContentsMargins (0, 0, 0, 0); + mLayout->setContentsMargins(0, 0, 0, 0); mId = new QLineEdit; - mId->setValidator (mValidator = new IdValidator (relaxedIdRules, this)); - mLayout->addWidget (mId, 1); + mId->setValidator(mValidator = new IdValidator(relaxedIdRules, this)); + mLayout->addWidget(mId, 1); - mCreate = new QPushButton ("Create"); - mLayout->addWidget (mCreate); + mCreate = new QPushButton("Create"); + mLayout->addWidget(mCreate); mCancel = new QPushButton("Cancel"); mLayout->addWidget(mCancel); - setLayout (mLayout); + setLayout(mLayout); - connect (mCancel, SIGNAL (clicked (bool)), this, SIGNAL (done())); - connect (mCreate, SIGNAL (clicked (bool)), this, SLOT (create())); + connect(mCancel, &QPushButton::clicked, this, &GenericCreator::done); + connect(mCreate, &QPushButton::clicked, this, &GenericCreator::create); - connect (mId, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); - connect (mId, SIGNAL (returnPressed()), this, SLOT (inputReturnPressed())); + connect(mId, &QLineEdit::textChanged, this, &GenericCreator::textChanged); + connect(mId, &QLineEdit::returnPressed, this, &GenericCreator::inputReturnPressed); - connect (&mData, SIGNAL (idListChanged()), this, SLOT (dataIdListChanged())); + connect(&mData, &CSMWorld::Data::idListChanged, this, &GenericCreator::dataIdListChanged); } -void CSVWorld::GenericCreator::setEditLock (bool locked) +void CSVWorld::GenericCreator::setEditorMaxLength(int length) +{ + mId->setMaxLength(length); +} + +void CSVWorld::GenericCreator::setEditLock(bool locked) { mLocked = locked; update(); @@ -193,7 +207,7 @@ void CSVWorld::GenericCreator::setEditLock (bool locked) void CSVWorld::GenericCreator::reset() { mCloneMode = false; - mId->setText (""); + mId->setText(""); update(); updateNamespace(); } @@ -204,13 +218,13 @@ std::string CSVWorld::GenericCreator::getErrors() const if (!mId->hasAcceptableInput()) errors = mValidator->getError(); - else if (mData.hasId (getId())) + else if (mData.hasId(getId())) errors = "ID is already in use"; return errors; } -void CSVWorld::GenericCreator::textChanged (const QString& text) +void CSVWorld::GenericCreator::textChanged(const QString& text) { update(); } @@ -233,26 +247,24 @@ void CSVWorld::GenericCreator::create() if (mCloneMode) { - command.reset (new CSMWorld::CloneCommand ( - dynamic_cast (*mData.getTableModel(mListId)), mClonedId, id, mClonedType)); + command = std::make_unique( + dynamic_cast(*mData.getTableModel(mListId)), mClonedId, id, mClonedType); } else { - command.reset (new CSMWorld::CreateCommand ( - dynamic_cast (*mData.getTableModel (mListId)), id)); - + command = std::make_unique( + dynamic_cast(*mData.getTableModel(mListId)), id); } - configureCreateCommand (*command); - pushCommand (std::move(command), id); + configureCreateCommand(*command); + pushCommand(std::move(command), id); emit done(); emit requestFocus(id); } } -void CSVWorld::GenericCreator::cloneMode(const std::string& originId, - const CSMWorld::UniversalId::Type type) +void CSVWorld::GenericCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { mCloneMode = true; mClonedId = originId; @@ -275,49 +287,46 @@ void CSVWorld::GenericCreator::touch(const std::vector& i mUndoStack.endMacro(); } -void CSVWorld::GenericCreator::toggleWidgets(bool active) -{ -} +void CSVWorld::GenericCreator::toggleWidgets(bool active) {} void CSVWorld::GenericCreator::focus() { mId->setFocus(); } -void CSVWorld::GenericCreator::setScope (unsigned int scope) +void CSVWorld::GenericCreator::setScope(unsigned int scope) { mScopes = scope; - int count = (mScopes & CSMWorld::Scope_Content) + (mScopes & CSMWorld::Scope_Project) + - (mScopes & CSMWorld::Scope_Session); + int count = (mScopes & CSMWorld::Scope_Content) + (mScopes & CSMWorld::Scope_Project) + + (mScopes & CSMWorld::Scope_Session); // scope selector widget - if (count>1) + if (count > 1) { - mScope = new QComboBox (this); - insertAtBeginning (mScope, false); + mScope = new QComboBox(this); + insertAtBeginning(mScope, false); if (mScopes & CSMWorld::Scope_Content) - addScope ("Content", CSMWorld::Scope_Content, - "Record will be stored in the currently edited content file."); + addScope("Content", CSMWorld::Scope_Content, "Record will be stored in the currently edited content file."); if (mScopes & CSMWorld::Scope_Project) - addScope ("Project", CSMWorld::Scope_Project, + addScope("Project", CSMWorld::Scope_Project, "Record will be stored in a local project file.

      " "Record will be created in the reserved namespace \"project\".

      " "Record is available when running OpenMW via OpenCS."); if (mScopes & CSMWorld::Scope_Session) - addScope ("Session", CSMWorld::Scope_Session, + addScope("Session", CSMWorld::Scope_Session, "Record exists only for the duration of the current editing session.

      " "Record will be created in the reserved namespace \"session\".

      " "Record is not available when running OpenMW via OpenCS."); - connect (mScope, SIGNAL (currentIndexChanged (int)), this, SLOT (scopeChanged (int))); + connect(mScope, qOverload(&QComboBox::currentIndexChanged), this, &GenericCreator::scopeChanged); - mScopeLabel = new QLabel ("Scope", this); - insertAtBeginning (mScopeLabel, false); + mScopeLabel = new QLabel("Scope", this); + insertAtBeginning(mScopeLabel, false); - mScope->setCurrentIndex (0); + mScope->setCurrentIndex(0); } else { @@ -331,7 +340,7 @@ void CSVWorld::GenericCreator::setScope (unsigned int scope) updateNamespace(); } -void CSVWorld::GenericCreator::scopeChanged (int index) +void CSVWorld::GenericCreator::scopeChanged(int index) { update(); updateNamespace(); diff --git a/apps/opencs/view/world/genericcreator.hpp b/apps/opencs/view/world/genericcreator.hpp index 3e2a43c918d..a39b530ef2a 100644 --- a/apps/opencs/view/world/genericcreator.hpp +++ b/apps/opencs/view/world/genericcreator.hpp @@ -1,13 +1,18 @@ #ifndef CSV_WORLD_GENERICCREATOR_H #define CSV_WORLD_GENERICCREATOR_H +#include + #include +#include +#include + +#include #include "../../model/world/universalid.hpp" #include "creator.hpp" -class QString; class QPushButton; class QLineEdit; class QHBoxLayout; @@ -27,107 +32,105 @@ namespace CSVWorld class GenericCreator : public Creator { - Q_OBJECT - - CSMWorld::Data& mData; - QUndoStack& mUndoStack; - CSMWorld::UniversalId mListId; - QPushButton *mCreate; - QPushButton *mCancel; - QLineEdit *mId; - std::string mErrors; - QHBoxLayout *mLayout; - bool mLocked; - std::string mClonedId; - CSMWorld::UniversalId::Type mClonedType; - unsigned int mScopes; - QComboBox *mScope; - QLabel *mScopeLabel; - IdValidator *mValidator; + Q_OBJECT - protected: - bool mCloneMode; + CSMWorld::Data& mData; + QUndoStack& mUndoStack; + CSMWorld::UniversalId mListId; + QPushButton* mCreate; + QPushButton* mCancel; + QLineEdit* mId; + std::string mErrors; + QHBoxLayout* mLayout; + bool mLocked; + std::string mClonedId; + CSMWorld::UniversalId::Type mClonedType; + unsigned int mScopes; + QComboBox* mScope; + QLabel* mScopeLabel; + IdValidator* mValidator; - protected: + protected: + bool mCloneMode; - void update(); + protected: + void update(); - virtual void setManualEditing (bool enabled); - ///< Enable/disable manual ID editing (enabled by default). + virtual void setManualEditing(bool enabled); + ///< Enable/disable manual ID editing (enabled by default). - void insertAtBeginning (QWidget *widget, bool stretched); + void insertAtBeginning(QWidget* widget, bool stretched); - /// \brief Insert given widget before Create and Cancel buttons. - /// \param widget Widget to add to layout. - /// \param stretched Whether widget should be streched or not. - void insertBeforeButtons (QWidget *widget, bool stretched); + /// \brief Insert given widget before Create and Cancel buttons. + /// \param widget Widget to add to layout. + /// \param stretched Whether widget should be streched or not. + void insertBeforeButtons(QWidget* widget, bool stretched); - virtual std::string getId() const; + virtual std::string getId() const; - std::string getClonedId() const; + std::string getClonedId() const; - virtual std::string getIdValidatorResult() const; + virtual std::string getIdValidatorResult() const; - /// Allow subclasses to add additional data to \a command. - virtual void configureCreateCommand (CSMWorld::CreateCommand& command) const; + /// Allow subclasses to add additional data to \a command. + virtual void configureCreateCommand(CSMWorld::CreateCommand& command) const; - /// Allow subclasses to wrap the create command together with additional commands - /// into a macro. - virtual void pushCommand (std::unique_ptr command, - const std::string& id); + /// Allow subclasses to wrap the create command together with additional commands + /// into a macro. + virtual void pushCommand(std::unique_ptr command, const std::string& id); - CSMWorld::Data& getData() const; + CSMWorld::Data& getData() const; - QUndoStack& getUndoStack(); + QUndoStack& getUndoStack(); - const CSMWorld::UniversalId& getCollectionId() const; + const CSMWorld::UniversalId& getCollectionId() const; - std::string getNamespace() const; + std::string getNamespace() const; - private: + void setEditorMaxLength(int length); - void updateNamespace(); + private: + void updateNamespace(); - void addScope (const QString& name, CSMWorld::Scope scope, - const QString& tooltip); + void addScope(const QString& name, CSMWorld::Scope scope, const QString& tooltip); - public: + public: + GenericCreator( + CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, bool relaxedIdRules = false); - GenericCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id, bool relaxedIdRules = false); + void setEditLock(bool locked) override; - void setEditLock (bool locked) override; + void reset() override; - void reset() override; + void toggleWidgets(bool active = true) override; - void toggleWidgets (bool active = true) override; + void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; - void cloneMode(const std::string& originId, - const CSMWorld::UniversalId::Type type) override; + void touch(const std::vector& ids) override; - void touch(const std::vector& ids) override; + virtual std::string getErrors() const; + ///< Return formatted error descriptions for the current state of the creator. if an empty + /// string is returned, there is no error. - virtual std::string getErrors() const; - ///< Return formatted error descriptions for the current state of the creator. if an empty - /// string is returned, there is no error. + void setScope(unsigned int scope) override; - void setScope (unsigned int scope) override; + /// Focus main input widget + void focus() override; - /// Focus main input widget - void focus() override; + protected slots: - private slots: + /// \brief Create record if able to after Return key is pressed on input. + void inputReturnPressed(); - void textChanged (const QString& text); + private slots: - /// \brief Create record if able to after Return key is pressed on input. - void inputReturnPressed(); + void textChanged(const QString& text); - void create(); + void create(); - void scopeChanged (int index); + void scopeChanged(int index); - void dataIdListChanged(); + void dataIdListChanged(); }; } diff --git a/apps/opencs/view/world/globalcreator.cpp b/apps/opencs/view/world/globalcreator.cpp index c7b140e1564..43c12105902 100644 --- a/apps/opencs/view/world/globalcreator.cpp +++ b/apps/opencs/view/world/globalcreator.cpp @@ -1,15 +1,19 @@ #include "globalcreator.hpp" -#include +#include +#include +#include + +#include -#include "../../model/world/data.hpp" #include "../../model/world/commands.hpp" -#include "../../model/world/columns.hpp" #include "../../model/world/idtable.hpp" +class QUndoStack; + namespace CSVWorld { - void GlobalCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const + void GlobalCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { CSMWorld::IdTable* table = static_cast(getData().getTableModel(getCollectionId())); @@ -20,7 +24,7 @@ namespace CSVWorld } GlobalCreator::GlobalCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) - : GenericCreator (data, undoStack, id, true) + : GenericCreator(data, undoStack, id, true) { } } diff --git a/apps/opencs/view/world/globalcreator.hpp b/apps/opencs/view/world/globalcreator.hpp index 057798a4c21..81f4c7c88ee 100644 --- a/apps/opencs/view/world/globalcreator.hpp +++ b/apps/opencs/view/world/globalcreator.hpp @@ -3,19 +3,28 @@ #include "genericcreator.hpp" +class QObject; +class QUndoStack; + +#include + +namespace CSMWorld +{ + class CreateCommand; + class Data; +} + namespace CSVWorld { class GlobalCreator : public GenericCreator { - Q_OBJECT - - public: - - GlobalCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); + Q_OBJECT - protected: + public: + GlobalCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); - void configureCreateCommand(CSMWorld::CreateCommand& command) const override; + protected: + void configureCreateCommand(CSMWorld::CreateCommand& command) const override; }; } diff --git a/apps/opencs/view/world/idcompletiondelegate.cpp b/apps/opencs/view/world/idcompletiondelegate.cpp index 447bcc25d90..e39be392f4f 100644 --- a/apps/opencs/view/world/idcompletiondelegate.cpp +++ b/apps/opencs/view/world/idcompletiondelegate.cpp @@ -3,25 +3,35 @@ #include "../../model/world/idcompletionmanager.hpp" #include "../../model/world/infoselectwrapper.hpp" +#include +#include + +#include + #include "../widget/droplineedit.hpp" -CSVWorld::IdCompletionDelegate::IdCompletionDelegate(CSMWorld::CommandDispatcher *dispatcher, - CSMDoc::Document& document, - QObject *parent) +namespace CSMWorld +{ + class CommandDispatcher; +} + +class QObject; +class QWidget; + +CSVWorld::IdCompletionDelegate::IdCompletionDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) : CommandDelegate(dispatcher, document, parent) -{} +{ +} -QWidget *CSVWorld::IdCompletionDelegate::createEditor(QWidget *parent, - const QStyleOptionViewItem &option, - const QModelIndex &index) const +QWidget* CSVWorld::IdCompletionDelegate::createEditor( + QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { return createEditor(parent, option, index, getDisplayTypeFromIndex(index)); } -QWidget *CSVWorld::IdCompletionDelegate::createEditor(QWidget *parent, - const QStyleOptionViewItem &option, - const QModelIndex &index, - CSMWorld::ColumnBase::Display display) const +QWidget* CSVWorld::IdCompletionDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, + const QModelIndex& index, CSMWorld::ColumnBase::Display display) const { if (!index.data(Qt::EditRole).isValid() && !index.data(Qt::DisplayRole).isValid()) { @@ -36,57 +46,72 @@ QWidget *CSVWorld::IdCompletionDelegate::createEditor(QWidget *parent, switch (conditionFunction) { - case CSMWorld::ConstInfoSelectWrapper::Function_Global: + case ESM::DialogueCondition::Function_Global: { - return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_GlobalVariable); + return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_GlobalVariable); } - case CSMWorld::ConstInfoSelectWrapper::Function_Journal: + case ESM::DialogueCondition::Function_Journal: { - return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Journal); + return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Journal); } - case CSMWorld::ConstInfoSelectWrapper::Function_Item: + case ESM::DialogueCondition::Function_Item: { - return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Referenceable); + return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Referenceable); } - case CSMWorld::ConstInfoSelectWrapper::Function_Dead: - case CSMWorld::ConstInfoSelectWrapper::Function_NotId: + case ESM::DialogueCondition::Function_Dead: + case ESM::DialogueCondition::Function_NotId: { - return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Referenceable); + return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Referenceable); } - case CSMWorld::ConstInfoSelectWrapper::Function_NotFaction: + case ESM::DialogueCondition::Function_NotFaction: { - return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Faction); + return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Faction); } - case CSMWorld::ConstInfoSelectWrapper::Function_NotClass: + case ESM::DialogueCondition::Function_NotClass: { - return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Class); + return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Class); } - case CSMWorld::ConstInfoSelectWrapper::Function_NotRace: + case ESM::DialogueCondition::Function_NotRace: { - return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Race); + return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Race); } - case CSMWorld::ConstInfoSelectWrapper::Function_NotCell: + case ESM::DialogueCondition::Function_NotCell: { - return createEditor (parent, option, index, CSMWorld::ColumnBase::Display_Cell); + return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Cell); } - case CSMWorld::ConstInfoSelectWrapper::Function_Local: - case CSMWorld::ConstInfoSelectWrapper::Function_NotLocal: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_NotLocal: { return new CSVWidget::DropLineEdit(display, parent); } - default: return nullptr; // The rest of them can't be edited anyway + default: + return nullptr; // The rest of them can't be edited anyway } } - CSMWorld::IdCompletionManager &completionManager = getDocument().getIdCompletionManager(); - CSVWidget::DropLineEdit *editor = new CSVWidget::DropLineEdit(display, parent); + CSMWorld::IdCompletionManager& completionManager = getDocument().getIdCompletionManager(); + CSVWidget::DropLineEdit* editor = new CSVWidget::DropLineEdit(display, parent); editor->setCompleter(completionManager.getCompleter(display).get()); + + // The savegame format limits the player faction string to 32 characters. + // The region sound name is limited to 32 characters. (ESM::Region::SoundRef::mSound) + // The script name is limited to 32 characters. (ESM::Script::SCHD::mName) + // The cell name is limited to 64 characters. (ESM::Header::GMDT::mCurrentCell) + if (display == CSMWorld::ColumnBase::Display_Faction || display == CSMWorld::ColumnBase::Display_Sound + || display == CSMWorld::ColumnBase::Display_Script || display == CSMWorld::ColumnBase::Display_Referenceable) + { + editor->setMaxLength(32); + } + else if (display == CSMWorld::ColumnBase::Display_Cell) + { + editor->setMaxLength(64); + } + return editor; } -CSVWorld::CommandDelegate *CSVWorld::IdCompletionDelegateFactory::makeDelegate(CSMWorld::CommandDispatcher *dispatcher, - CSMDoc::Document& document, - QObject *parent) const +CSVWorld::CommandDelegate* CSVWorld::IdCompletionDelegateFactory::makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const { return new IdCompletionDelegate(dispatcher, document, parent); } diff --git a/apps/opencs/view/world/idcompletiondelegate.hpp b/apps/opencs/view/world/idcompletiondelegate.hpp index 57c2c11c43c..9326e3b69cb 100644 --- a/apps/opencs/view/world/idcompletiondelegate.hpp +++ b/apps/opencs/view/world/idcompletiondelegate.hpp @@ -3,33 +3,43 @@ #include "util.hpp" +#include + +class QModelIndex; +class QObject; +class QWidget; + +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher; +} + namespace CSVWorld { /// \brief Enables the Id completion for a column class IdCompletionDelegate : public CommandDelegate { - public: - IdCompletionDelegate(CSMWorld::CommandDispatcher *dispatcher, - CSMDoc::Document& document, - QObject *parent); - - QWidget *createEditor (QWidget *parent, - const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - - QWidget *createEditor (QWidget *parent, - const QStyleOptionViewItem &option, - const QModelIndex &index, - CSMWorld::ColumnBase::Display display) const override; + public: + IdCompletionDelegate(CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent); + + QWidget* createEditor( + QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index, + CSMWorld::ColumnBase::Display display) const override; }; class IdCompletionDelegateFactory : public CommandDelegateFactory { - public: - CommandDelegate *makeDelegate(CSMWorld::CommandDispatcher *dispatcher, - CSMDoc::Document& document, - QObject *parent) const override; - ///< The ownership of the returned CommandDelegate is transferred to the caller. + public: + CommandDelegate* makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const override; + ///< The ownership of the returned CommandDelegate is transferred to the caller. }; } diff --git a/apps/opencs/view/world/idtypedelegate.cpp b/apps/opencs/view/world/idtypedelegate.cpp index ee35e07d419..6aa77b38f74 100755 --- a/apps/opencs/view/world/idtypedelegate.cpp +++ b/apps/opencs/view/world/idtypedelegate.cpp @@ -2,26 +2,42 @@ #include "../../model/world/universalid.hpp" -CSVWorld::IdTypeDelegate::IdTypeDelegate - (const ValueList &values, const IconList &icons, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) - : DataDisplayDelegate (values, icons, dispatcher, document, - "Records", "type-format", - parent) -{} +#include +#include + +#include + +class QObject; + +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher; +} + +CSVWorld::IdTypeDelegate::IdTypeDelegate(const ValueList& values, const IconList& icons, + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) + : DataDisplayDelegate(values, icons, dispatcher, document, "Records", "type-format", parent) +{ +} CSVWorld::IdTypeDelegateFactory::IdTypeDelegateFactory() { - for (int i=0; i (i)); + CSMWorld::UniversalId id(static_cast(i)); - DataDisplayDelegateFactory::add (id.getType(), QString::fromUtf8 (id.getTypeName().c_str()), - QString::fromUtf8 (id.getIcon().c_str())); + DataDisplayDelegateFactory::add( + id.getType(), QString::fromUtf8(id.getTypeName().c_str()), QString::fromUtf8(id.getIcon().c_str())); } } -CSVWorld::CommandDelegate *CSVWorld::IdTypeDelegateFactory::makeDelegate ( - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const +CSVWorld::CommandDelegate* CSVWorld::IdTypeDelegateFactory::makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const { - return new IdTypeDelegate (mValues, mIcons, dispatcher, document, parent); + return new IdTypeDelegate(mValues, mIcons, dispatcher, document, parent); } diff --git a/apps/opencs/view/world/idtypedelegate.hpp b/apps/opencs/view/world/idtypedelegate.hpp index f1c3b539c91..4a138910c5f 100755 --- a/apps/opencs/view/world/idtypedelegate.hpp +++ b/apps/opencs/view/world/idtypedelegate.hpp @@ -1,27 +1,39 @@ #ifndef IDTYPEDELEGATE_HPP #define IDTYPEDELEGATE_HPP -#include "enumdelegate.hpp" -#include "util.hpp" -#include "../../model/world/universalid.hpp" #include "datadisplaydelegate.hpp" +class QObject; + +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher; +} + namespace CSVWorld { + class CommandDelegate; + class IdTypeDelegate : public DataDisplayDelegate { - public: - IdTypeDelegate (const ValueList &mValues, const IconList &icons, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent); + public: + IdTypeDelegate(const ValueList& mValues, const IconList& icons, CSMWorld::CommandDispatcher* dispatcher, + CSMDoc::Document& document, QObject* parent); }; class IdTypeDelegateFactory : public DataDisplayDelegateFactory { - public: - - IdTypeDelegateFactory(); + public: + IdTypeDelegateFactory(); - CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const override; - ///< The ownership of the returned CommandDelegate is transferred to the caller. + CommandDelegate* makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const override; + ///< The ownership of the returned CommandDelegate is transferred to the caller. }; } diff --git a/apps/opencs/view/world/idvalidator.cpp b/apps/opencs/view/world/idvalidator.cpp index 442157ac5d8..078bd6bce5f 100644 --- a/apps/opencs/view/world/idvalidator.cpp +++ b/apps/opencs/view/world/idvalidator.cpp @@ -1,29 +1,20 @@ #include "idvalidator.hpp" -#include +#include -bool CSVWorld::IdValidator::isValid (const QChar& c, bool first) const +CSVWorld::IdValidator::IdValidator(bool relaxed, QObject* parent) + : QValidator(parent) + , mRelaxed(relaxed) { - if (c.isLetter() || c=='_') - return true; - - if (!first && (c.isDigit() || c.isSpace())) - return true; - - return false; } -CSVWorld::IdValidator::IdValidator (bool relaxed, QObject *parent) -: QValidator (parent), mRelaxed (relaxed) -{} - -QValidator::State CSVWorld::IdValidator::validate (QString& input, int& pos) const +QValidator::State CSVWorld::IdValidator::validate(QString& input, int& pos) const { mError.clear(); if (mRelaxed) { - if (input.indexOf ('"')!=-1 || input.indexOf ("::")!=-1 || input.indexOf ("#")!=-1) + if (input.indexOf('"') != -1 || input.indexOf("::") != -1 || input.indexOf("#") != -1) return QValidator::Invalid; } else @@ -42,9 +33,9 @@ QValidator::State CSVWorld::IdValidator::validate (QString& input, int& pos) con if (!mNamespace.empty()) { - std::string namespace_ = input.left (static_cast(mNamespace.size())).toUtf8().constData(); + std::string namespace_ = input.left(static_cast(mNamespace.size())).toUtf8().constData(); - if (Misc::StringUtils::lowerCase (namespace_)!=mNamespace) + if (Misc::StringUtils::lowerCase(namespace_) != mNamespace) return QValidator::Invalid; // incorrect namespace iter += namespace_.size(); @@ -53,20 +44,20 @@ QValidator::State CSVWorld::IdValidator::validate (QString& input, int& pos) con } else { - int index = input.indexOf (":"); + int index = input.indexOf(":"); - if (index!=-1) + if (index != -1) { - QString namespace_ = input.left (index); + QString namespace_ = input.left(index); - if (namespace_=="project" || namespace_=="session") + if (namespace_ == "project" || namespace_ == "session") return QValidator::Invalid; // reserved namespace } } - for (; iter!=input.end(); ++iter, first = false) + for (; iter != input.end(); ++iter, first = false) { - if (*iter==':') + if (*iter == ':') { if (first) return QValidator::Invalid; // scope operator at the beginning @@ -90,7 +81,7 @@ QValidator::State CSVWorld::IdValidator::validate (QString& input, int& pos) con { prevScope = false; - if (!isValid (*iter, first)) + if (!iter->isPrint()) return QValidator::Invalid; } } @@ -111,9 +102,9 @@ QValidator::State CSVWorld::IdValidator::validate (QString& input, int& pos) con return QValidator::Acceptable; } -void CSVWorld::IdValidator::setNamespace (const std::string& namespace_) +void CSVWorld::IdValidator::setNamespace(const std::string& namespace_) { - mNamespace = Misc::StringUtils::lowerCase (namespace_); + mNamespace = Misc::StringUtils::lowerCase(namespace_); } std::string CSVWorld::IdValidator::getError() const diff --git a/apps/opencs/view/world/idvalidator.hpp b/apps/opencs/view/world/idvalidator.hpp index 278335a65b9..6b98d356723 100644 --- a/apps/opencs/view/world/idvalidator.hpp +++ b/apps/opencs/view/world/idvalidator.hpp @@ -9,29 +9,23 @@ namespace CSVWorld { class IdValidator : public QValidator { - bool mRelaxed; - std::string mNamespace; - mutable std::string mError; + bool mRelaxed; + std::string mNamespace; + mutable std::string mError; - private: + public: + IdValidator(bool relaxed = false, QObject* parent = nullptr); + ///< \param relaxed Relaxed rules for IDs that also functino as user visible text - bool isValid (const QChar& c, bool first) const; + State validate(QString& input, int& pos) const override; - public: - - IdValidator (bool relaxed = false, QObject *parent = nullptr); - ///< \param relaxed Relaxed rules for IDs that also functino as user visible text - - State validate (QString& input, int& pos) const override; - - void setNamespace (const std::string& namespace_); - - /// Return a description of the error that resulted in the last call of validate - /// returning QValidator::Intermediate. If the last call to validate returned - /// a different value (or if there was no such call yet), an empty string is - /// returned. - std::string getError() const; + void setNamespace(const std::string& namespace_); + /// Return a description of the error that resulted in the last call of validate + /// returning QValidator::Intermediate. If the last call to validate returned + /// a different value (or if there was no such call yet), an empty string is + /// returned. + std::string getError() const; }; } diff --git a/apps/opencs/view/world/infocreator.cpp b/apps/opencs/view/world/infocreator.cpp index cf1b48a195a..f98fe5be5e1 100644 --- a/apps/opencs/view/world/infocreator.cpp +++ b/apps/opencs/view/world/infocreator.cpp @@ -1,48 +1,56 @@ #include "infocreator.hpp" #include +#include #include +#include #include -#include +#include +#include +#include + +#include #include "../../model/doc/document.hpp" -#include "../../model/world/data.hpp" -#include "../../model/world/commands.hpp" #include "../../model/world/columns.hpp" -#include "../../model/world/idtable.hpp" +#include "../../model/world/commands.hpp" +#include "../../model/world/data.hpp" #include "../../model/world/idcompletionmanager.hpp" +#include "../../model/world/idtable.hpp" #include "../widget/droplineedit.hpp" +class QUndoStack; + std::string CSVWorld::InfoCreator::getId() const { - std::string id = Misc::StringUtils::lowerCase (mTopic->text().toUtf8().constData()); + const std::string topic = mTopic->text().toStdString(); std::string unique = QUuid::createUuid().toByteArray().data(); - unique.erase (std::remove (unique.begin(), unique.end(), '-'), unique.end()); + unique.erase(std::remove(unique.begin(), unique.end(), '-'), unique.end()); - unique = unique.substr (1, unique.size()-2); + unique = unique.substr(1, unique.size() - 2); - return id + '#' + unique; + return topic + '#' + unique; } -void CSVWorld::InfoCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const +void CSVWorld::InfoCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { - CSMWorld::IdTable& table = dynamic_cast (*getData().getTableModel (getCollectionId())); + CSMWorld::IdTable& table = dynamic_cast(*getData().getTableModel(getCollectionId())); - CSMWorld::CloneCommand* cloneCommand = dynamic_cast (&command); + CSMWorld::CloneCommand* cloneCommand = dynamic_cast(&command); if (getCollectionId() == CSMWorld::UniversalId::Type_TopicInfos) { if (!cloneCommand) { - command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Topic), mTopic->text()); - command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Rank), -1); - command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Gender), -1); - command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_PcRank), -1); + command.addValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Topic), mTopic->text()); + command.addValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Rank), -1); + command.addValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Gender), -1); + command.addValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_PcRank), -1); } else { @@ -53,16 +61,16 @@ void CSVWorld::InfoCreator::configureCreateCommand (CSMWorld::CreateCommand& com { if (!cloneCommand) { - command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Journal), mTopic->text()); + command.addValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Journal), mTopic->text()); } else cloneCommand->setOverrideValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Journal), mTopic->text()); } } -CSVWorld::InfoCreator::InfoCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager& completionManager) -: GenericCreator (data, undoStack, id) +CSVWorld::InfoCreator::InfoCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, + CSMWorld::IdCompletionManager& completionManager) + : GenericCreator(data, undoStack, id) { // Determine if we're dealing with topics or journals. CSMWorld::ColumnBase::Display displayType = CSMWorld::ColumnBase::Display_Topic; @@ -73,52 +81,57 @@ CSVWorld::InfoCreator::InfoCreator (CSMWorld::Data& data, QUndoStack& undoStack, labelText = "Journal"; } - QLabel *label = new QLabel (labelText, this); - insertBeforeButtons (label, false); + QLabel* label = new QLabel(labelText, this); + insertBeforeButtons(label, false); // Add topic/journal ID input with auto-completion. // Only existing topic/journal IDs are accepted so no ID validation is performed. mTopic = new CSVWidget::DropLineEdit(displayType, this); mTopic->setCompleter(completionManager.getCompleter(displayType).get()); - insertBeforeButtons (mTopic, true); + insertBeforeButtons(mTopic, true); - setManualEditing (false); + setManualEditing(false); - connect (mTopic, SIGNAL (textChanged (const QString&)), this, SLOT (topicChanged())); - connect (mTopic, SIGNAL (returnPressed()), this, SLOT (inputReturnPressed())); + connect(mTopic, &CSVWidget::DropLineEdit::textChanged, this, &InfoCreator::topicChanged); + connect(mTopic, &CSVWidget::DropLineEdit::returnPressed, this, &InfoCreator::inputReturnPressed); } -void CSVWorld::InfoCreator::cloneMode (const std::string& originId, - const CSMWorld::UniversalId::Type type) +void CSVWorld::InfoCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { - CSMWorld::IdTable& infoTable = - dynamic_cast (*getData().getTableModel (getCollectionId())); + CSMWorld::IdTable& infoTable = dynamic_cast(*getData().getTableModel(getCollectionId())); - int topicColumn = infoTable.findColumnIndex ( - getCollectionId().getType()==CSMWorld::UniversalId::Type_TopicInfos ? - CSMWorld::Columns::ColumnId_Topic : CSMWorld::Columns::ColumnId_Journal); + int topicColumn = infoTable.findColumnIndex(getCollectionId().getType() == CSMWorld::UniversalId::Type_TopicInfos + ? CSMWorld::Columns::ColumnId_Topic + : CSMWorld::Columns::ColumnId_Journal); - mTopic->setText ( - infoTable.data (infoTable.getModelIndex (originId, topicColumn)).toString()); + mTopic->setText(infoTable.data(infoTable.getModelIndex(originId, topicColumn)).toString()); - GenericCreator::cloneMode (originId, type); + GenericCreator::cloneMode(originId, type); } void CSVWorld::InfoCreator::reset() { - mTopic->setText (""); + mTopic->setText(""); GenericCreator::reset(); } +void CSVWorld::InfoCreator::setText(const std::string& text) +{ + QString qText = QString::fromStdString(text); + mTopic->setText(qText); +} + std::string CSVWorld::InfoCreator::getErrors() const { // We ignore errors from GenericCreator here, because they can never happen in an InfoCreator. std::string errors; - std::string topic = mTopic->text().toUtf8().constData(); + const ESM::RefId topic = ESM::RefId::stringRefId(mTopic->text().toStdString()); - if ((getCollectionId().getType()==CSMWorld::UniversalId::Type_TopicInfos ? - getData().getTopics() : getData().getJournals()).searchId (topic)==-1) + if ((getCollectionId().getType() == CSMWorld::UniversalId::Type_TopicInfos ? getData().getTopics() + : getData().getJournals()) + .searchId(topic) + == -1) { errors += "Invalid Topic ID"; } @@ -131,16 +144,18 @@ void CSVWorld::InfoCreator::focus() mTopic->setFocus(); } +void CSVWorld::InfoCreator::callReturnPressed() +{ + emit inputReturnPressed(); +} + void CSVWorld::InfoCreator::topicChanged() { update(); } -CSVWorld::Creator *CSVWorld::InfoCreatorFactory::makeCreator(CSMDoc::Document& document, - const CSMWorld::UniversalId& id) const +CSVWorld::Creator* CSVWorld::InfoCreatorFactory::makeCreator( + CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { - return new InfoCreator(document.getData(), - document.getUndoStack(), - id, - document.getIdCompletionManager()); + return new InfoCreator(document.getData(), document.getUndoStack(), id, document.getIdCompletionManager()); } diff --git a/apps/opencs/view/world/infocreator.hpp b/apps/opencs/view/world/infocreator.hpp index 404dcf37278..0c4a9fd4d37 100644 --- a/apps/opencs/view/world/infocreator.hpp +++ b/apps/opencs/view/world/infocreator.hpp @@ -3,10 +3,18 @@ #include "genericcreator.hpp" +#include + +#include +#include + +class QUndoStack; + namespace CSMWorld { - class InfoCollection; class IdCompletionManager; + class CreateCommand; + class Data; } namespace CSVWidget @@ -14,46 +22,54 @@ namespace CSVWidget class DropLineEdit; } +namespace CSMDoc +{ + class Document; +} + namespace CSVWorld { class InfoCreator : public GenericCreator { - Q_OBJECT + Q_OBJECT - CSVWidget::DropLineEdit *mTopic; + CSVWidget::DropLineEdit* mTopic; - std::string getId() const override; + std::string getId() const override; - void configureCreateCommand (CSMWorld::CreateCommand& command) const override; + void configureCreateCommand(CSMWorld::CreateCommand& command) const override; - public: + public: + InfoCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, + CSMWorld::IdCompletionManager& completionManager); - InfoCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager& completionManager); + void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; - void cloneMode (const std::string& originId, - const CSMWorld::UniversalId::Type type) override; + void reset() override; - void reset() override; + void setText(const std::string& text); - std::string getErrors() const override; - ///< Return formatted error descriptions for the current state of the creator. if an empty - /// string is returned, there is no error. - - /// Focus main input widget - void focus() override; - - private slots: + std::string getErrors() const override; + ///< Return formatted error descriptions for the current state of the creator. if an empty + /// string is returned, there is no error. - void topicChanged(); + /// Focus main input widget + void focus() override; + + public slots: + + void callReturnPressed(); + + private slots: + + void topicChanged(); }; class InfoCreatorFactory : public CreatorFactoryBase { - public: - - Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; - ///< The ownership of the returned Creator is transferred to the caller. + public: + Creator* makeCreator(CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; + ///< The ownership of the returned Creator is transferred to the caller. }; } diff --git a/apps/opencs/view/world/landcreator.cpp b/apps/opencs/view/world/landcreator.cpp index 2ebfe186982..5ba2de603e2 100644 --- a/apps/opencs/view/world/landcreator.cpp +++ b/apps/opencs/view/world/landcreator.cpp @@ -5,6 +5,10 @@ #include #include +#include +#include +#include + #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/land.hpp" @@ -37,8 +41,8 @@ namespace CSVWorld insertBeforeButtons(mYLabel, false); insertBeforeButtons(mY, true); - connect (mX, SIGNAL(valueChanged(int)), this, SLOT(coordChanged(int))); - connect (mY, SIGNAL(valueChanged(int)), this, SLOT(coordChanged(int))); + connect(mX, qOverload(&QSpinBox::valueChanged), this, &LandCreator::coordChanged); + connect(mY, qOverload(&QSpinBox::valueChanged), this, &LandCreator::coordChanged); } void LandCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) @@ -57,8 +61,10 @@ namespace CSVWorld // Combine multiple touch commands into one "macro" command getUndoStack().beginMacro("Touch records"); - CSMWorld::IdTable& lands = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_Lands)); - CSMWorld::IdTable& ltexs = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); + CSMWorld::IdTable& lands + = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_Lands)); + CSMWorld::IdTable& ltexs + = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); for (const CSMWorld::UniversalId& uid : ids) { CSMWorld::TouchLandCommand* touchCmd = new CSMWorld::TouchLandCommand(lands, ltexs, uid.getId()); @@ -83,7 +89,7 @@ namespace CSVWorld std::string LandCreator::getErrors() const { - if (getData().getLand().searchId(getId()) >= 0) + if (getData().getLand().searchId(ESM::RefId::stringRefId(getId())) >= 0) return "A land with that name already exists."; return ""; @@ -98,19 +104,22 @@ namespace CSVWorld { if (mCloneMode) { - CSMWorld::IdTable& lands = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_Lands)); - CSMWorld::IdTable& ltexs = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); + CSMWorld::IdTable& lands + = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_Lands)); + CSMWorld::IdTable& ltexs + = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); getUndoStack().beginMacro(("Clone " + id).c_str()); getUndoStack().push(command.release()); - CSMWorld::CopyLandTexturesCommand* ltexCopy = new CSMWorld::CopyLandTexturesCommand(lands, ltexs, getClonedId(), getId()); + CSMWorld::CopyLandTexturesCommand* ltexCopy + = new CSMWorld::CopyLandTexturesCommand(lands, ltexs, getClonedId(), getId()); getUndoStack().push(ltexCopy); getUndoStack().endMacro(); } else - getUndoStack().push (command.release()); + getUndoStack().push(command.release()); } void LandCreator::coordChanged(int value) diff --git a/apps/opencs/view/world/landcreator.hpp b/apps/opencs/view/world/landcreator.hpp index e0c5577e426..ae7eecf7583 100644 --- a/apps/opencs/view/world/landcreator.hpp +++ b/apps/opencs/view/world/landcreator.hpp @@ -3,6 +3,18 @@ #include "genericcreator.hpp" +#include +#include +#include + +#include + +namespace CSMWorld +{ + class CreateCommand; + class Data; +} + class QLabel; class QSpinBox; @@ -10,37 +22,34 @@ namespace CSVWorld { class LandCreator : public GenericCreator { - Q_OBJECT - - QLabel* mXLabel; - QLabel* mYLabel; - QSpinBox* mX; - QSpinBox* mY; - - public: + Q_OBJECT - LandCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); + QLabel* mXLabel; + QLabel* mYLabel; + QSpinBox* mX; + QSpinBox* mY; - void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; + public: + LandCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); - void touch(const std::vector& ids) override; + void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; - void focus() override; + void touch(const std::vector& ids) override; - void reset() override; + void focus() override; - std::string getErrors() const override; + void reset() override; - protected: + std::string getErrors() const override; - std::string getId() const override; + protected: + std::string getId() const override; - void pushCommand(std::unique_ptr command, - const std::string& id) override; + void pushCommand(std::unique_ptr command, const std::string& id) override; - private slots: + private slots: - void coordChanged(int value); + void coordChanged(int value); }; } diff --git a/apps/opencs/view/world/landtexturecreator.cpp b/apps/opencs/view/world/landtexturecreator.cpp deleted file mode 100644 index 43d911e50dc..00000000000 --- a/apps/opencs/view/world/landtexturecreator.cpp +++ /dev/null @@ -1,101 +0,0 @@ -#include "landtexturecreator.hpp" - -#include -#include - -#include -#include -#include - -#include "../../model/world/commands.hpp" -#include "../../model/world/idtable.hpp" -#include "../../model/world/landtexture.hpp" - -namespace CSVWorld -{ - LandTextureCreator::LandTextureCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) - : GenericCreator(data, undoStack, id) - { - // One index is reserved for a default texture - const size_t MaxIndex = std::numeric_limits::max() - 1; - - setManualEditing(false); - - QLabel* nameLabel = new QLabel("Name"); - insertBeforeButtons(nameLabel, false); - - mNameEdit = new QLineEdit(this); - insertBeforeButtons(mNameEdit, true); - - QLabel* indexLabel = new QLabel("Index"); - insertBeforeButtons(indexLabel, false); - - mIndexBox = new QSpinBox(this); - mIndexBox->setMinimum(0); - mIndexBox->setMaximum(MaxIndex); - insertBeforeButtons(mIndexBox, true); - - connect(mNameEdit, SIGNAL(textChanged(const QString&)), this, SLOT(nameChanged(const QString&))); - connect(mIndexBox, SIGNAL(valueChanged(int)), this, SLOT(indexChanged(int))); - } - - void LandTextureCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) - { - GenericCreator::cloneMode(originId, type); - - CSMWorld::IdTable& table = dynamic_cast(*getData().getTableModel(getCollectionId())); - - int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureNickname); - mNameEdit->setText((table.data(table.getModelIndex(originId, column)).toString())); - - column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureIndex); - mIndexBox->setValue((table.data(table.getModelIndex(originId, column)).toInt())); - } - - void LandTextureCreator::focus() - { - mIndexBox->setFocus(); - } - - void LandTextureCreator::reset() - { - GenericCreator::reset(); - mNameEdit->setText(""); - mIndexBox->setValue(0); - } - - std::string LandTextureCreator::getErrors() const - { - if (getData().getLandTextures().searchId(getId()) >= 0) - { - return "Index is already in use"; - } - - return ""; - } - - void LandTextureCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const - { - GenericCreator::configureCreateCommand(command); - - CSMWorld::IdTable& table = dynamic_cast(*getData().getTableModel(getCollectionId())); - int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureNickname); - command.addValue(column, mName.c_str()); - } - - std::string LandTextureCreator::getId() const - { - return CSMWorld::LandTexture::createUniqueRecordId(0, mIndexBox->value()); - } - - void LandTextureCreator::nameChanged(const QString& value) - { - mName = value.toUtf8().constData(); - update(); - } - - void LandTextureCreator::indexChanged(int value) - { - update(); - } -} diff --git a/apps/opencs/view/world/landtexturecreator.hpp b/apps/opencs/view/world/landtexturecreator.hpp deleted file mode 100644 index b11c47758aa..00000000000 --- a/apps/opencs/view/world/landtexturecreator.hpp +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef CSV_WORLD_LANDTEXTURECREATOR_H -#define CSV_WORLD_LANDTEXTURECREATOR_H - -#include - -#include "genericcreator.hpp" - -class QLineEdit; -class QSpinBox; - -namespace CSVWorld -{ - class LandTextureCreator : public GenericCreator - { - Q_OBJECT - - public: - - LandTextureCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); - - void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; - - void focus() override; - - void reset() override; - - std::string getErrors() const override; - - protected: - - void configureCreateCommand(CSMWorld::CreateCommand& command) const override; - - std::string getId() const override; - - private slots: - - void nameChanged(const QString& val); - void indexChanged(int val); - - private: - - QLineEdit* mNameEdit; - QSpinBox* mIndexBox; - - std::string mName; - }; -} - -#endif diff --git a/apps/opencs/view/world/nestedtable.cpp b/apps/opencs/view/world/nestedtable.cpp index d52f7ca7366..5cfdab53217 100644 --- a/apps/opencs/view/world/nestedtable.cpp +++ b/apps/opencs/view/world/nestedtable.cpp @@ -1,52 +1,49 @@ #include "nestedtable.hpp" -#include #include +#include #include -#include #include "../../model/prefs/shortcut.hpp" -#include "../../model/world/nestedtableproxymodel.hpp" -#include "../../model/world/universalid.hpp" -#include "../../model/world/commands.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../../model/world/commandmacro.hpp" +#include "../../model/world/commands.hpp" +#include "../../model/world/nestedtableproxymodel.hpp" +#include "../../model/world/universalid.hpp" + +#include +#include +#include #include "tableeditidaction.hpp" #include "util.hpp" -CSVWorld::NestedTable::NestedTable(CSMDoc::Document& document, - CSMWorld::UniversalId id, - CSMWorld::NestedTableProxyModel* model, - QWidget* parent, - bool editable, - bool fixedRows) - : DragRecordTable(document, parent), - mAddNewRowAction(nullptr), - mRemoveRowAction(nullptr), - mEditIdAction(nullptr), - mModel(model) +CSVWorld::NestedTable::NestedTable(CSMDoc::Document& document, const CSMWorld::UniversalId& id, + CSMWorld::NestedTableProxyModel* model, QWidget* parent, bool editable, bool fixedRows) + : DragRecordTable(document, parent) + , mAddNewRowAction(nullptr) + , mRemoveRowAction(nullptr) + , mEditIdAction(nullptr) + , mModel(model) { - mDispatcher = new CSMWorld::CommandDispatcher (document, id, this); + mDispatcher = new CSMWorld::CommandDispatcher(document, id, this); - setSelectionBehavior (QAbstractItemView::SelectRows); - setSelectionMode (QAbstractItemView::ExtendedSelection); + setSelectionBehavior(QAbstractItemView::SelectRows); + setSelectionMode(QAbstractItemView::ExtendedSelection); - horizontalHeader()->setSectionResizeMode (QHeaderView::Interactive); + horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); verticalHeader()->hide(); int columns = model->columnCount(QModelIndex()); - for(int i = 0 ; i < columns; ++i) + for (int i = 0; i < columns; ++i) { - CSMWorld::ColumnBase::Display display = static_cast ( - model->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); + CSMWorld::ColumnBase::Display display = static_cast( + model->headerData(i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); - CommandDelegate *delegate = CommandDelegateFactoryCollection::get().makeDelegate(display, - mDispatcher, - document, - this); + CommandDelegate* delegate + = CommandDelegateFactoryCollection::get().makeDelegate(display, mDispatcher, document, this); setItemDelegateForColumn(i, delegate); } @@ -57,21 +54,19 @@ CSVWorld::NestedTable::NestedTable(CSMDoc::Document& document, { if (!fixedRows) { - mAddNewRowAction = new QAction (tr ("Add new row"), this); - connect(mAddNewRowAction, SIGNAL(triggered()), - this, SLOT(addNewRowActionTriggered())); + mAddNewRowAction = new QAction(tr("Add new row"), this); + connect(mAddNewRowAction, &QAction::triggered, this, &NestedTable::addNewRowActionTriggered); CSMPrefs::Shortcut* addRowShortcut = new CSMPrefs::Shortcut("table-add", this); addRowShortcut->associateAction(mAddNewRowAction); - mRemoveRowAction = new QAction (tr ("Remove rows"), this); - connect(mRemoveRowAction, SIGNAL(triggered()), - this, SLOT(removeRowActionTriggered())); + mRemoveRowAction = new QAction(tr("Remove rows"), this); + connect(mRemoveRowAction, &QAction::triggered, this, &NestedTable::removeRowActionTriggered); CSMPrefs::Shortcut* removeRowShortcut = new CSMPrefs::Shortcut("table-remove", this); removeRowShortcut->associateAction(mRemoveRowAction); } mEditIdAction = new TableEditIdAction(*this, this); - connect(mEditIdAction, SIGNAL(triggered()), this, SLOT(editCell())); + connect(mEditIdAction, &QAction::triggered, this, &NestedTable::editCell); } } @@ -81,7 +76,7 @@ std::vector CSVWorld::NestedTable::getDraggedRecords() co return std::vector(); } -void CSVWorld::NestedTable::contextMenuEvent (QContextMenuEvent *event) +void CSVWorld::NestedTable::contextMenuEvent(QContextMenuEvent* event) { if (!mEditIdAction) return; @@ -105,13 +100,13 @@ void CSVWorld::NestedTable::contextMenuEvent (QContextMenuEvent *event) menu.addAction(mRemoveRowAction); } - menu.exec (event->globalPos()); + menu.exec(event->globalPos()); } void CSVWorld::NestedTable::removeRowActionTriggered() { - CSMWorld::CommandMacro macro(mDocument.getUndoStack(), - selectionModel()->selectedRows().size() > 1 ? tr("Remove rows") : ""); + CSMWorld::CommandMacro macro( + mDocument.getUndoStack(), selectionModel()->selectedRows().size() > 1 ? tr("Remove rows") : ""); // Remove rows in reverse order for (int i = selectionModel()->selectedRows().size() - 1; i >= 0; --i) @@ -128,10 +123,8 @@ void CSVWorld::NestedTable::addNewRowActionTriggered() if (!selectionModel()->selectedRows().empty()) row = selectionModel()->selectedRows().back().row() + 1; - mDocument.getUndoStack().push(new CSMWorld::AddNestedCommand(*(mModel->model()), - mModel->getParentId(), - row, - mModel->getParentColumn())); + mDocument.getUndoStack().push( + new CSMWorld::AddNestedCommand(*(mModel->model()), mModel->getParentId(), row, mModel->getParentColumn())); } void CSVWorld::NestedTable::editCell() diff --git a/apps/opencs/view/world/nestedtable.hpp b/apps/opencs/view/world/nestedtable.hpp index f864f5d80c0..07f0acdf5bd 100644 --- a/apps/opencs/view/world/nestedtable.hpp +++ b/apps/opencs/view/world/nestedtable.hpp @@ -1,10 +1,11 @@ #ifndef CSV_WORLD_NESTEDTABLE_H #define CSV_WORLD_NESTEDTABLE_H -#include - #include "dragrecordtable.hpp" +#include +#include + class QAction; class QContextMenuEvent; @@ -28,24 +29,20 @@ namespace CSVWorld { Q_OBJECT - QAction *mAddNewRowAction; - QAction *mRemoveRowAction; - TableEditIdAction *mEditIdAction; + QAction* mAddNewRowAction; + QAction* mRemoveRowAction; + TableEditIdAction* mEditIdAction; CSMWorld::NestedTableProxyModel* mModel; - CSMWorld::CommandDispatcher *mDispatcher; + CSMWorld::CommandDispatcher* mDispatcher; public: - NestedTable(CSMDoc::Document& document, - CSMWorld::UniversalId id, - CSMWorld::NestedTableProxyModel* model, - QWidget* parent = nullptr, - bool editable = true, - bool fixedRows = false); + NestedTable(CSMDoc::Document& document, const CSMWorld::UniversalId& id, CSMWorld::NestedTableProxyModel* model, + QWidget* parent = nullptr, bool editable = true, bool fixedRows = false); std::vector getDraggedRecords() const override; private: - void contextMenuEvent (QContextMenuEvent *event) override; + void contextMenuEvent(QContextMenuEvent* event) override; private slots: void removeRowActionTriggered(); @@ -55,7 +52,7 @@ namespace CSVWorld void editCell(); signals: - void editRequest(const CSMWorld::UniversalId &id, const std::string &hint); + void editRequest(const CSMWorld::UniversalId& id, const std::string& hint); }; } diff --git a/apps/opencs/view/world/pathgridcreator.cpp b/apps/opencs/view/world/pathgridcreator.cpp index 95628a5d962..51c3b1d282a 100644 --- a/apps/opencs/view/world/pathgridcreator.cpp +++ b/apps/opencs/view/world/pathgridcreator.cpp @@ -2,6 +2,13 @@ #include +#include + +#include +#include +#include +#include + #include "../../model/doc/document.hpp" #include "../../model/world/columns.hpp" @@ -11,6 +18,8 @@ #include "../widget/droplineedit.hpp" +class QUndoStack; + std::string CSVWorld::PathgridCreator::getId() const { return mCell->text().toUtf8().constData(); @@ -18,21 +27,16 @@ std::string CSVWorld::PathgridCreator::getId() const CSMWorld::IdTable& CSVWorld::PathgridCreator::getPathgridsTable() const { - return dynamic_cast ( - *getData().getTableModel(getCollectionId()) - ); + return dynamic_cast(*getData().getTableModel(getCollectionId())); } -CSVWorld::PathgridCreator::PathgridCreator( - CSMWorld::Data& data, - QUndoStack& undoStack, - const CSMWorld::UniversalId& id, - CSMWorld::IdCompletionManager& completionManager -) : GenericCreator(data, undoStack, id) +CSVWorld::PathgridCreator::PathgridCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, + CSMWorld::IdCompletionManager& completionManager) + : GenericCreator(data, undoStack, id) { setManualEditing(false); - QLabel *label = new QLabel("Cell", this); + QLabel* label = new QLabel("Cell", this); insertBeforeButtons(label, false); // Add cell ID input with auto-completion. @@ -42,13 +46,11 @@ CSVWorld::PathgridCreator::PathgridCreator( mCell->setCompleter(completionManager.getCompleter(displayType).get()); insertBeforeButtons(mCell, true); - connect(mCell, SIGNAL (textChanged(const QString&)), this, SLOT (cellChanged())); - connect(mCell, SIGNAL (returnPressed()), this, SLOT (inputReturnPressed())); + connect(mCell, &CSVWidget::DropLineEdit::textChanged, this, &PathgridCreator::cellChanged); + connect(mCell, &CSVWidget::DropLineEdit::returnPressed, this, &PathgridCreator::inputReturnPressed); } -void CSVWorld::PathgridCreator::cloneMode( - const std::string& originId, - const CSMWorld::UniversalId::Type type) +void CSVWorld::PathgridCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { CSVWorld::GenericCreator::cloneMode(originId, type); @@ -60,7 +62,7 @@ void CSVWorld::PathgridCreator::cloneMode( std::string CSVWorld::PathgridCreator::getErrors() const { - std::string cellId = getId(); + const ESM::RefId cellId = ESM::RefId::stringRefId(getId()); // Check user input for any errors. std::string errors; @@ -96,14 +98,8 @@ void CSVWorld::PathgridCreator::cellChanged() update(); } -CSVWorld::Creator *CSVWorld::PathgridCreatorFactory::makeCreator( - CSMDoc::Document& document, - const CSMWorld::UniversalId& id) const +CSVWorld::Creator* CSVWorld::PathgridCreatorFactory::makeCreator( + CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { - return new PathgridCreator( - document.getData(), - document.getUndoStack(), - id, - document.getIdCompletionManager() - ); + return new PathgridCreator(document.getData(), document.getUndoStack(), id, document.getIdCompletionManager()); } diff --git a/apps/opencs/view/world/pathgridcreator.hpp b/apps/opencs/view/world/pathgridcreator.hpp index 773735e25ca..ce26dc370eb 100644 --- a/apps/opencs/view/world/pathgridcreator.hpp +++ b/apps/opencs/view/world/pathgridcreator.hpp @@ -1,8 +1,13 @@ #ifndef PATHGRIDCREATOR_HPP #define PATHGRIDCREATOR_HPP +#include + #include "genericcreator.hpp" +#include +#include + namespace CSMDoc { class Document; @@ -13,7 +18,6 @@ namespace CSMWorld class Data; class IdCompletionManager; class IdTable; - class UniversalId; } namespace CSVWidget @@ -28,54 +32,44 @@ namespace CSVWorld { Q_OBJECT - CSVWidget::DropLineEdit *mCell; - - private: + CSVWidget::DropLineEdit* mCell; - /// \return Cell ID entered by user. - std::string getId() const override; + private: + /// \return Cell ID entered by user. + std::string getId() const override; - /// \return reference to table containing pathgrids. - CSMWorld::IdTable& getPathgridsTable() const; + /// \return reference to table containing pathgrids. + CSMWorld::IdTable& getPathgridsTable() const; - public: + public: + PathgridCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, + CSMWorld::IdCompletionManager& completionManager); - PathgridCreator( - CSMWorld::Data& data, - QUndoStack& undoStack, - const CSMWorld::UniversalId& id, - CSMWorld::IdCompletionManager& completionManager); + /// \brief Set cell ID input widget to ID of record to be cloned. + /// \param originId Cell ID to be cloned. + /// \param type Type of record to be cloned. + void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; - /// \brief Set cell ID input widget to ID of record to be cloned. - /// \param originId Cell ID to be cloned. - /// \param type Type of record to be cloned. - void cloneMode( - const std::string& originId, - const CSMWorld::UniversalId::Type type) override; + /// \return Error description for current user input. + std::string getErrors() const override; - /// \return Error description for current user input. - std::string getErrors() const override; + /// \brief Set focus to cell ID input widget. + void focus() override; - /// \brief Set focus to cell ID input widget. - void focus() override; + /// \brief Clear cell ID input widget. + void reset() override; - /// \brief Clear cell ID input widget. - void reset() override; + private slots: - private slots: - - /// \brief Check user input for errors. - void cellChanged(); + /// \brief Check user input for errors. + void cellChanged(); }; /// \brief Creator factory for pathgrid record creator. class PathgridCreatorFactory : public CreatorFactoryBase { - public: - - Creator *makeCreator( - CSMDoc::Document& document, - const CSMWorld::UniversalId& id) const override; + public: + Creator* makeCreator(CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; }; } diff --git a/apps/opencs/view/world/previewsubview.cpp b/apps/opencs/view/world/previewsubview.cpp index f3312bb208d..73ecb2157ae 100644 --- a/apps/opencs/view/world/previewsubview.cpp +++ b/apps/opencs/view/world/previewsubview.cpp @@ -2,66 +2,81 @@ #include +#include +#include +#include +#include +#include +#include + #include "../render/previewwidget.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" -CSVWorld::PreviewSubView::PreviewSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: SubView (id), mTitle (id.toString().c_str()) +#include "../../model/doc/document.hpp" + +CSVWorld::PreviewSubView::PreviewSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) + : SubView(id) + , mTitle(id.toString().c_str()) { - QHBoxLayout *layout = new QHBoxLayout; + QHBoxLayout* layout = new QHBoxLayout; - if (document.getData().getReferenceables().searchId (id.getId())==-1) + if (document.getData().getReferenceables().searchId(ESM::RefId::stringRefId(id.getId())) == -1) { - std::string referenceableId = - document.getData().getReferences().getRecord (id.getId()).get().mRefID; + std::string referenceableId = document.getData() + .getReferences() + .getRecord(ESM::RefId::stringRefId(id.getId())) + .get() + .mRefID.getRefIdString(); - referenceableIdChanged (referenceableId); + referenceableIdChanged(referenceableId); - mScene = - new CSVRender::PreviewWidget (document.getData(), id.getId(), false, this); + mScene = new CSVRender::PreviewWidget(document.getData(), id.getId(), false, this); } else - mScene = new CSVRender::PreviewWidget (document.getData(), id.getId(), true, this); + mScene = new CSVRender::PreviewWidget(document.getData(), id.getId(), true, this); + + mScene->setExterior(true); - CSVWidget::SceneToolbar *toolbar = new CSVWidget::SceneToolbar (48+6, this); + CSVWidget::SceneToolbar* toolbar = new CSVWidget::SceneToolbar(48 + 6, this); - CSVWidget::SceneToolMode *lightingTool = mScene->makeLightingSelector (toolbar); - toolbar->addTool (lightingTool); + CSVWidget::SceneToolMode* lightingTool = mScene->makeLightingSelector(toolbar); + toolbar->addTool(lightingTool); - layout->addWidget (toolbar, 0); + layout->addWidget(toolbar, 0); - layout->addWidget (mScene, 1); + layout->addWidget(mScene, 1); - QWidget *widget = new QWidget; + QWidget* widget = new QWidget; - widget->setLayout (layout); + widget->setLayout(layout); - setWidget (widget); + setWidget(widget); - connect (mScene, SIGNAL (closeRequest()), this, SLOT (closeRequest())); - connect (mScene, SIGNAL (referenceableIdChanged (const std::string&)), - this, SLOT (referenceableIdChanged (const std::string&))); - connect (mScene, SIGNAL (focusToolbarRequest()), toolbar, SLOT (setFocus())); - connect (toolbar, SIGNAL (focusSceneRequest()), mScene, SLOT (setFocus())); + connect(mScene, &CSVRender::PreviewWidget::closeRequest, this, qOverload<>(&PreviewSubView::closeRequest)); + connect(mScene, &CSVRender::PreviewWidget::referenceableIdChanged, this, &PreviewSubView::referenceableIdChanged); + connect(mScene, &CSVRender::PreviewWidget::focusToolbarRequest, toolbar, + qOverload<>(&CSVWidget::SceneToolbar::setFocus)); + connect( + toolbar, &CSVWidget::SceneToolbar::focusSceneRequest, mScene, qOverload<>(&CSVRender::PreviewWidget::setFocus)); } -void CSVWorld::PreviewSubView::setEditLock (bool locked) {} +void CSVWorld::PreviewSubView::setEditLock(bool locked) {} std::string CSVWorld::PreviewSubView::getTitle() const { return mTitle; } -void CSVWorld::PreviewSubView::referenceableIdChanged (const std::string& id) +void CSVWorld::PreviewSubView::referenceableIdChanged(const std::string& id) { if (id.empty()) mTitle = "Preview: Reference to "; else mTitle = "Preview: Reference to " + id; - setWindowTitle (QString::fromUtf8 (mTitle.c_str())); + setWindowTitle(QString::fromUtf8(mTitle.c_str())); emit updateTitle(); } diff --git a/apps/opencs/view/world/previewsubview.hpp b/apps/opencs/view/world/previewsubview.hpp index ed88d048874..8ae3c9dbad4 100644 --- a/apps/opencs/view/world/previewsubview.hpp +++ b/apps/opencs/view/world/previewsubview.hpp @@ -3,6 +3,10 @@ #include "../doc/subview.hpp" +#include + +#include + namespace CSMDoc { class Document; @@ -17,22 +21,21 @@ namespace CSVWorld { class PreviewSubView : public CSVDoc::SubView { - Q_OBJECT - - CSVRender::PreviewWidget *mScene; - std::string mTitle; + Q_OBJECT - public: + CSVRender::PreviewWidget* mScene; + std::string mTitle; - PreviewSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); + public: + PreviewSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); - void setEditLock (bool locked) override; + void setEditLock(bool locked) override; - std::string getTitle() const override; + std::string getTitle() const override; - private slots: + private slots: - void referenceableIdChanged (const std::string& id); + void referenceableIdChanged(const std::string& id); }; } diff --git a/apps/opencs/view/world/recordbuttonbar.cpp b/apps/opencs/view/world/recordbuttonbar.cpp index 9fea7b303c4..e2655136a1e 100644 --- a/apps/opencs/view/world/recordbuttonbar.cpp +++ b/apps/opencs/view/world/recordbuttonbar.cpp @@ -3,156 +3,166 @@ #include #include -#include "../../model/world/idtable.hpp" +#include + #include "../../model/world/commanddispatcher.hpp" +#include "../../model/world/idtable.hpp" #include "../../model/prefs/state.hpp" #include "../world/tablebottombox.hpp" +#include + +#include +#include +#include +#include +#include + void CSVWorld::RecordButtonBar::updateModificationButtons() { bool createAndDeleteDisabled = !mBottom || !mBottom->canCreateAndDelete() || mLocked; - mCloneButton->setDisabled (createAndDeleteDisabled); - mAddButton->setDisabled (createAndDeleteDisabled); + mCloneButton->setDisabled(createAndDeleteDisabled); + mAddButton->setDisabled(createAndDeleteDisabled); bool commandDisabled = !mCommandDispatcher || mLocked; - mRevertButton->setDisabled (commandDisabled); - mDeleteButton->setDisabled (commandDisabled || createAndDeleteDisabled); + mRevertButton->setDisabled(commandDisabled); + mDeleteButton->setDisabled(commandDisabled || createAndDeleteDisabled); } void CSVWorld::RecordButtonBar::updatePrevNextButtons() { int rows = mTable.rowCount(); - if (rows<=1) + if (rows <= 1) { - mPrevButton->setDisabled (true); - mNextButton->setDisabled (true); + mPrevButton->setDisabled(true); + mNextButton->setDisabled(true); } else if (CSMPrefs::get()["General Input"]["cycle"].isTrue()) { - mPrevButton->setDisabled (false); - mNextButton->setDisabled (false); + mPrevButton->setDisabled(false); + mNextButton->setDisabled(false); } else { - int row = mTable.getModelIndex (mId.getId(), 0).row(); + int row = mTable.getModelIndex(mId.getId(), 0).row(); - mPrevButton->setDisabled (row<=0); - mNextButton->setDisabled (row>=rows-1); + mPrevButton->setDisabled(row <= 0); + mNextButton->setDisabled(row >= rows - 1); } } -CSVWorld::RecordButtonBar::RecordButtonBar (const CSMWorld::UniversalId& id, - CSMWorld::IdTable& table, TableBottomBox *bottomBox, - CSMWorld::CommandDispatcher *commandDispatcher, QWidget *parent) -: QWidget (parent), mId (id), mTable (table), mBottom (bottomBox), - mCommandDispatcher (commandDispatcher), mLocked (false) +CSVWorld::RecordButtonBar::RecordButtonBar(const CSMWorld::UniversalId& id, CSMWorld::IdTable& table, + TableBottomBox* bottomBox, CSMWorld::CommandDispatcher* commandDispatcher, QWidget* parent) + : QWidget(parent) + , mId(id) + , mTable(table) + , mBottom(bottomBox) + , mCommandDispatcher(commandDispatcher) + , mLocked(false) { - QHBoxLayout *buttonsLayout = new QHBoxLayout; - buttonsLayout->setContentsMargins (0, 0, 0, 0); + QHBoxLayout* buttonsLayout = new QHBoxLayout; + buttonsLayout->setContentsMargins(0, 0, 0, 0); // left section - mPrevButton = new QToolButton (this); - mPrevButton->setIcon(QIcon(":record-previous")); - mPrevButton->setToolTip ("Switch to previous record"); - buttonsLayout->addWidget (mPrevButton, 0); + mPrevButton = new QToolButton(this); + mPrevButton->setIcon(Misc::ScalableIcon::load(":record-previous")); + mPrevButton->setToolTip("Switch to previous record"); + buttonsLayout->addWidget(mPrevButton, 0); - mNextButton = new QToolButton (this); - mNextButton->setIcon(QIcon(":/record-next")); - mNextButton->setToolTip ("Switch to next record"); - buttonsLayout->addWidget (mNextButton, 1); + mNextButton = new QToolButton(this); + mNextButton->setIcon(Misc::ScalableIcon::load(":/record-next")); + mNextButton->setToolTip("Switch to next record"); + buttonsLayout->addWidget(mNextButton, 1); buttonsLayout->addStretch(2); // optional buttons of the right section if (mTable.getFeatures() & CSMWorld::IdTable::Feature_Preview) { - QToolButton* previewButton = new QToolButton (this); - previewButton->setIcon(QIcon(":edit-preview")); - previewButton->setToolTip ("Open a preview of this record"); + QToolButton* previewButton = new QToolButton(this); + previewButton->setIcon(Misc::ScalableIcon::load(":edit-preview")); + previewButton->setToolTip("Open a preview of this record"); buttonsLayout->addWidget(previewButton); - connect (previewButton, SIGNAL(clicked()), this, SIGNAL (showPreview())); + connect(previewButton, &QToolButton::clicked, this, &RecordButtonBar::showPreview); } if (mTable.getFeatures() & CSMWorld::IdTable::Feature_View) { - QToolButton* viewButton = new QToolButton (this); - viewButton->setIcon(QIcon(":/cell.png")); - viewButton->setToolTip ("Open a scene view of the cell this record is located in"); + QToolButton* viewButton = new QToolButton(this); + viewButton->setIcon(Misc::ScalableIcon::load(":cell")); + viewButton->setToolTip("Open a scene view of the cell this record is located in"); buttonsLayout->addWidget(viewButton); - connect (viewButton, SIGNAL(clicked()), this, SIGNAL (viewRecord())); + connect(viewButton, &QToolButton::clicked, this, &RecordButtonBar::viewRecord); } // right section - mCloneButton = new QToolButton (this); - mCloneButton->setIcon(QIcon(":edit-clone")); - mCloneButton->setToolTip ("Clone record"); + mCloneButton = new QToolButton(this); + mCloneButton->setIcon(Misc::ScalableIcon::load(":edit-clone")); + mCloneButton->setToolTip("Clone record"); buttonsLayout->addWidget(mCloneButton); - mAddButton = new QToolButton (this); - mAddButton->setIcon(QIcon(":edit-add")); - mAddButton->setToolTip ("Add new record"); + mAddButton = new QToolButton(this); + mAddButton->setIcon(Misc::ScalableIcon::load(":edit-add")); + mAddButton->setToolTip("Add new record"); buttonsLayout->addWidget(mAddButton); - mDeleteButton = new QToolButton (this); - mDeleteButton->setIcon(QIcon(":edit-delete")); - mDeleteButton->setToolTip ("Delete record"); + mDeleteButton = new QToolButton(this); + mDeleteButton->setIcon(Misc::ScalableIcon::load(":edit-delete")); + mDeleteButton->setToolTip("Delete record"); buttonsLayout->addWidget(mDeleteButton); - mRevertButton = new QToolButton (this); - mRevertButton->setIcon(QIcon(":edit-undo")); - mRevertButton->setToolTip ("Revert record"); + mRevertButton = new QToolButton(this); + mRevertButton->setIcon(Misc::ScalableIcon::load(":edit-undo")); + mRevertButton->setToolTip("Revert record"); buttonsLayout->addWidget(mRevertButton); - setLayout (buttonsLayout); + setLayout(buttonsLayout); // connections - if(mBottom && mBottom->canCreateAndDelete()) + if (mBottom && mBottom->canCreateAndDelete()) { - connect (mAddButton, SIGNAL (clicked()), mBottom, SLOT (createRequest())); - connect (mCloneButton, SIGNAL (clicked()), this, SLOT (cloneRequest())); + connect(mAddButton, &QToolButton::clicked, mBottom, &TableBottomBox::createRequest); + connect(mCloneButton, &QToolButton::clicked, this, &RecordButtonBar::cloneRequest); } - connect (mNextButton, SIGNAL (clicked()), this, SLOT (nextId())); - connect (mPrevButton, SIGNAL (clicked()), this, SLOT (prevId())); + connect(mNextButton, &QToolButton::clicked, this, &RecordButtonBar::nextId); + connect(mPrevButton, &QToolButton::clicked, this, &RecordButtonBar::prevId); if (mCommandDispatcher) { - connect (mRevertButton, SIGNAL (clicked()), mCommandDispatcher, SLOT (executeRevert())); - connect (mDeleteButton, SIGNAL (clicked()), mCommandDispatcher, SLOT (executeDelete())); + connect(mRevertButton, &QToolButton::clicked, mCommandDispatcher, &CSMWorld::CommandDispatcher::executeRevert); + connect(mDeleteButton, &QToolButton::clicked, mCommandDispatcher, &CSMWorld::CommandDispatcher::executeDelete); } - connect (&mTable, SIGNAL (rowsInserted (const QModelIndex&, int, int)), - this, SLOT (rowNumberChanged (const QModelIndex&, int, int))); - connect (&mTable, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), - this, SLOT (rowNumberChanged (const QModelIndex&, int, int))); + connect(&mTable, &CSMWorld::IdTable::rowsInserted, this, &RecordButtonBar::rowNumberChanged); + connect(&mTable, &CSMWorld::IdTable::rowsRemoved, this, &RecordButtonBar::rowNumberChanged); - connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), - this, SLOT (settingChanged (const CSMPrefs::Setting *))); + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &RecordButtonBar::settingChanged); updateModificationButtons(); updatePrevNextButtons(); } -void CSVWorld::RecordButtonBar::setEditLock (bool locked) +void CSVWorld::RecordButtonBar::setEditLock(bool locked) { mLocked = locked; updateModificationButtons(); } -void CSVWorld::RecordButtonBar::universalIdChanged (const CSMWorld::UniversalId& id) +void CSVWorld::RecordButtonBar::universalIdChanged(const CSMWorld::UniversalId& id) { mId = id; updatePrevNextButtons(); } -void CSVWorld::RecordButtonBar::settingChanged (const CSMPrefs::Setting *setting) +void CSVWorld::RecordButtonBar::settingChanged(const CSMPrefs::Setting* setting) { - if (*setting=="General Input/cycle") + if (*setting == "General Input/cycle") updatePrevNextButtons(); } @@ -160,19 +170,18 @@ void CSVWorld::RecordButtonBar::cloneRequest() { if (mBottom) { - int typeColumn = mTable.findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); + int typeColumn = mTable.findColumnIndex(CSMWorld::Columns::ColumnId_RecordType); - QModelIndex typeIndex = mTable.getModelIndex (mId.getId(), typeColumn); - CSMWorld::UniversalId::Type type = static_cast ( - mTable.data (typeIndex).toInt()); + QModelIndex typeIndex = mTable.getModelIndex(mId.getId(), typeColumn); + CSMWorld::UniversalId::Type type = static_cast(mTable.data(typeIndex).toInt()); - mBottom->cloneRequest (mId.getId(), type); + mBottom->cloneRequest(mId.getId(), type); } } void CSVWorld::RecordButtonBar::nextId() { - int newRow = mTable.getModelIndex (mId.getId(), 0).row() + 1; + int newRow = mTable.getModelIndex(mId.getId(), 0).row() + 1; if (newRow >= mTable.rowCount()) { @@ -182,25 +191,25 @@ void CSVWorld::RecordButtonBar::nextId() return; } - emit switchToRow (newRow); + emit switchToRow(newRow); } void CSVWorld::RecordButtonBar::prevId() { - int newRow = mTable.getModelIndex (mId.getId(), 0).row() - 1; + int newRow = mTable.getModelIndex(mId.getId(), 0).row() - 1; if (newRow < 0) { if (CSMPrefs::get()["General Input"]["cycle"].isTrue()) - newRow = mTable.rowCount()-1; + newRow = mTable.rowCount() - 1; else return; } - emit switchToRow (newRow); + emit switchToRow(newRow); } -void CSVWorld::RecordButtonBar::rowNumberChanged (const QModelIndex& parent, int start, int end) +void CSVWorld::RecordButtonBar::rowNumberChanged(const QModelIndex& parent, int start, int end) { updatePrevNextButtons(); } diff --git a/apps/opencs/view/world/recordbuttonbar.hpp b/apps/opencs/view/world/recordbuttonbar.hpp index aca3211f8ad..605738670c9 100644 --- a/apps/opencs/view/world/recordbuttonbar.hpp +++ b/apps/opencs/view/world/recordbuttonbar.hpp @@ -35,57 +35,54 @@ namespace CSVWorld /// - view (optional) class RecordButtonBar : public QWidget { - Q_OBJECT + Q_OBJECT - CSMWorld::UniversalId mId; - CSMWorld::IdTable& mTable; - TableBottomBox *mBottom; - CSMWorld::CommandDispatcher *mCommandDispatcher; - QToolButton *mPrevButton; - QToolButton *mNextButton; - QToolButton *mCloneButton; - QToolButton *mAddButton; - QToolButton *mDeleteButton; - QToolButton *mRevertButton; - bool mLocked; + CSMWorld::UniversalId mId; + CSMWorld::IdTable& mTable; + TableBottomBox* mBottom; + CSMWorld::CommandDispatcher* mCommandDispatcher; + QToolButton* mPrevButton; + QToolButton* mNextButton; + QToolButton* mCloneButton; + QToolButton* mAddButton; + QToolButton* mDeleteButton; + QToolButton* mRevertButton; + bool mLocked; - private: + private: + void updateModificationButtons(); - void updateModificationButtons(); + void updatePrevNextButtons(); - void updatePrevNextButtons(); + public: + RecordButtonBar(const CSMWorld::UniversalId& id, CSMWorld::IdTable& table, TableBottomBox* bottomBox = nullptr, + CSMWorld::CommandDispatcher* commandDispatcher = nullptr, QWidget* parent = nullptr); - public: + void setEditLock(bool locked); - RecordButtonBar (const CSMWorld::UniversalId& id, - CSMWorld::IdTable& table, TableBottomBox *bottomBox = nullptr, - CSMWorld::CommandDispatcher *commandDispatcher = nullptr, QWidget *parent = nullptr); + public slots: - void setEditLock (bool locked); + void universalIdChanged(const CSMWorld::UniversalId& id); - public slots: + private slots: - void universalIdChanged (const CSMWorld::UniversalId& id); + void settingChanged(const CSMPrefs::Setting* setting); - private slots: + void cloneRequest(); - void settingChanged (const CSMPrefs::Setting *setting); + void nextId(); - void cloneRequest(); + void prevId(); - void nextId(); + void rowNumberChanged(const QModelIndex& parent, int start, int end); - void prevId(); + signals: - void rowNumberChanged (const QModelIndex& parent, int start, int end); + void showPreview(); - signals: + void viewRecord(); - void showPreview(); - - void viewRecord(); - - void switchToRow (int row); + void switchToRow(int row); }; } diff --git a/apps/opencs/view/world/recordstatusdelegate.cpp b/apps/opencs/view/world/recordstatusdelegate.cpp index fd98fe6cd96..9e172f33628 100644 --- a/apps/opencs/view/world/recordstatusdelegate.cpp +++ b/apps/opencs/view/world/recordstatusdelegate.cpp @@ -1,38 +1,49 @@ #include "recordstatusdelegate.hpp" -#include -#include -#include - #include "../../model/world/columns.hpp" -CSVWorld::RecordStatusDelegate::RecordStatusDelegate(const ValueList& values, - const IconList & icons, - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) - : DataDisplayDelegate (values, icons, dispatcher, document, - "Records", "status-format", - parent) -{} +#include +#include + +#include +#include +#include -CSVWorld::CommandDelegate *CSVWorld::RecordStatusDelegateFactory::makeDelegate ( - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const +class QObject; + +namespace CSMDoc { - return new RecordStatusDelegate (mValues, mIcons, dispatcher, document, parent); + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher; +} + +CSVWorld::RecordStatusDelegate::RecordStatusDelegate(const ValueList& values, const IconList& icons, + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) + : DataDisplayDelegate(values, icons, dispatcher, document, "Records", "status-format", parent) +{ +} + +CSVWorld::CommandDelegate* CSVWorld::RecordStatusDelegateFactory::makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const +{ + return new RecordStatusDelegate(mValues, mIcons, dispatcher, document, parent); } CSVWorld::RecordStatusDelegateFactory::RecordStatusDelegateFactory() { - std::vector> enums = - CSMWorld::Columns::getEnums (CSMWorld::Columns::ColumnId_Modification); + std::vector> enums + = CSMWorld::Columns::getEnums(CSMWorld::Columns::ColumnId_Modification); - static const char *sIcons[] = - { - ":list-base", ":list-modified", ":list-added", ":list-removed", ":list-removed", 0 - }; + static const char* sIcons[] + = { ":list-base", ":list-modified", ":list-added", ":list-removed", ":list-removed", 0 }; - for (int i=0; sIcons[i]; ++i) + for (int i = 0; sIcons[i]; ++i) { auto& enumPair = enums.at(i); - add (enumPair.first, enumPair.second.c_str(), sIcons[i]); + add(enumPair.first, enumPair.second.c_str(), sIcons[i]); } } diff --git a/apps/opencs/view/world/recordstatusdelegate.hpp b/apps/opencs/view/world/recordstatusdelegate.hpp index 38f066862bc..9afac712955 100644 --- a/apps/opencs/view/world/recordstatusdelegate.hpp +++ b/apps/opencs/view/world/recordstatusdelegate.hpp @@ -1,37 +1,39 @@ #ifndef RECORDSTATUSDELEGATE_H #define RECORDSTATUSDELEGATE_H -#include "util.hpp" -#include -#include - #include "datadisplaydelegate.hpp" -#include "../../model/world/record.hpp" -class QIcon; -class QFont; +class QObject; + +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher; +} namespace CSVWorld { + class CommandDelegate; + class RecordStatusDelegate : public DataDisplayDelegate { public: - - RecordStatusDelegate (const ValueList& values, const IconList& icons, - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, - QObject *parent = nullptr); + RecordStatusDelegate(const ValueList& values, const IconList& icons, CSMWorld::CommandDispatcher* dispatcher, + CSMDoc::Document& document, QObject* parent = nullptr); }; class RecordStatusDelegateFactory : public DataDisplayDelegateFactory { - public: - - RecordStatusDelegateFactory(); - - CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const override; - ///< The ownership of the returned CommandDelegate is transferred to the caller. + public: + RecordStatusDelegateFactory(); + CommandDelegate* makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const override; + ///< The ownership of the returned CommandDelegate is transferred to the caller. }; } #endif // RECORDSTATUSDELEGATE_HPP - diff --git a/apps/opencs/view/world/referenceablecreator.cpp b/apps/opencs/view/world/referenceablecreator.cpp index 836e8ac7dcb..c4e20adf72a 100644 --- a/apps/opencs/view/world/referenceablecreator.cpp +++ b/apps/opencs/view/world/referenceablecreator.cpp @@ -1,51 +1,80 @@ #include "referenceablecreator.hpp" +#include + #include #include -#include "../../model/world/universalid.hpp" +#include + +#include + #include "../../model/world/commands.hpp" +#include "../../model/world/universalid.hpp" + +class QUndoStack; + +namespace CSMWorld +{ + class Data; +} -void CSVWorld::ReferenceableCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const +void CSVWorld::ReferenceableCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { - command.setType ( - static_cast (mType->itemData (mType->currentIndex()).toInt())); + command.setType(static_cast(mType->itemData(mType->currentIndex()).toInt())); } -CSVWorld::ReferenceableCreator::ReferenceableCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id) -: GenericCreator (data, undoStack, id) +CSVWorld::ReferenceableCreator::ReferenceableCreator( + CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) + : GenericCreator(data, undoStack, id) { - QLabel *label = new QLabel ("Type", this); - insertBeforeButtons (label, false); + QLabel* label = new QLabel("Type", this); + insertBeforeButtons(label, false); std::vector types = CSMWorld::UniversalId::listReferenceableTypes(); - mType = new QComboBox (this); + mType = new QComboBox(this); + mType->setMaxVisibleItems(20); - for (std::vector::const_iterator iter (types.begin()); - iter!=types.end(); ++iter) + for (std::vector::const_iterator iter(types.begin()); iter != types.end(); ++iter) { - CSMWorld::UniversalId id2 (*iter, ""); + CSMWorld::UniversalId id2(*iter, ""); - mType->addItem (QIcon (id2.getIcon().c_str()), id2.getTypeName().c_str(), - static_cast (id2.getType())); + mType->addItem(Misc::ScalableIcon::load(id2.getIcon().c_str()), id2.getTypeName().c_str(), + static_cast(id2.getType())); } - insertBeforeButtons (mType, false); + mType->model()->sort(0); + + insertBeforeButtons(mType, false); + + connect(mType, qOverload(&QComboBox::currentIndexChanged), this, &ReferenceableCreator::setType); } void CSVWorld::ReferenceableCreator::reset() { - mType->setCurrentIndex (0); + mType->setCurrentIndex(0); GenericCreator::reset(); } -void CSVWorld::ReferenceableCreator::cloneMode (const std::string& originId, - const CSMWorld::UniversalId::Type type) +void CSVWorld::ReferenceableCreator::setType(int index) +{ + // container items have name limit of 32 characters + std::string text = mType->currentText().toStdString(); + if (text == "Potion" || text == "Apparatus" || text == "Armor" || text == "Book" || text == "Clothing" + || text == "Ingredient" || text == "ItemLevelledList" || text == "Light" || text == "Lockpick" + || text == "Miscellaneous" || text == "Probe" || text == "Repair" || text == "Weapon") + { + GenericCreator::setEditorMaxLength(32); + } + else + GenericCreator::setEditorMaxLength(32767); +} + +void CSVWorld::ReferenceableCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { - GenericCreator::cloneMode (originId, type); - mType->setCurrentIndex (mType->findData (static_cast (type))); + GenericCreator::cloneMode(originId, type); + mType->setCurrentIndex(mType->findData(static_cast(type))); } void CSVWorld::ReferenceableCreator::toggleWidgets(bool active) diff --git a/apps/opencs/view/world/referenceablecreator.hpp b/apps/opencs/view/world/referenceablecreator.hpp index d4657bcf7fe..9858327968b 100644 --- a/apps/opencs/view/world/referenceablecreator.hpp +++ b/apps/opencs/view/world/referenceablecreator.hpp @@ -1,34 +1,45 @@ #ifndef CSV_WORLD_REFERENCEABLECREATOR_H #define CSV_WORLD_REFERENCEABLECREATOR_H +#include "genericcreator.hpp" + +#include + +#include + class QComboBox; +class QObject; +class QUndoStack; -#include "genericcreator.hpp" +namespace CSMWorld +{ + class CreateCommand; + class Data; +} namespace CSVWorld { class ReferenceableCreator : public GenericCreator { - Q_OBJECT - - QComboBox *mType; + Q_OBJECT - private: + QComboBox* mType; - void configureCreateCommand (CSMWorld::CreateCommand& command) const override; + private: + void configureCreateCommand(CSMWorld::CreateCommand& command) const override; - public: + public: + ReferenceableCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); - ReferenceableCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id); + void reset() override; - void reset() override; + void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; - void cloneMode (const std::string& originId, - const CSMWorld::UniversalId::Type type) override; + void toggleWidgets(bool active = true) override; - void toggleWidgets(bool active = true) override; + private slots: + void setType(int index); }; } diff --git a/apps/opencs/view/world/referencecreator.cpp b/apps/opencs/view/world/referencecreator.cpp index e939b9baf07..925ae537893 100644 --- a/apps/opencs/view/world/referencecreator.cpp +++ b/apps/opencs/view/world/referencecreator.cpp @@ -2,14 +2,20 @@ #include +#include + +#include +#include +#include +#include + #include "../../model/doc/document.hpp" -#include "../../model/world/data.hpp" -#include "../../model/world/commands.hpp" #include "../../model/world/columns.hpp" -#include "../../model/world/idtable.hpp" +#include "../../model/world/commands.hpp" +#include "../../model/world/data.hpp" #include "../../model/world/idcompletionmanager.hpp" -#include "../../model/world/commandmacro.hpp" +#include "../../model/world/idtable.hpp" #include "../widget/droplineedit.hpp" @@ -18,39 +24,38 @@ std::string CSVWorld::ReferenceCreator::getId() const return mId; } -void CSVWorld::ReferenceCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const +void CSVWorld::ReferenceCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { // Set cellID - int cellIdColumn = - dynamic_cast (*getData().getTableModel (getCollectionId())). - findColumnIndex (CSMWorld::Columns::ColumnId_Cell); + int cellIdColumn = dynamic_cast(*getData().getTableModel(getCollectionId())) + .findColumnIndex(CSMWorld::Columns::ColumnId_Cell); - command.addValue (cellIdColumn, mCell->text()); + command.addValue(cellIdColumn, mCell->text()); } -CSVWorld::ReferenceCreator::ReferenceCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager &completionManager) -: GenericCreator (data, undoStack, id) +CSVWorld::ReferenceCreator::ReferenceCreator(CSMWorld::Data& data, QUndoStack& undoStack, + const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager& completionManager) + : GenericCreator(data, undoStack, id) { - QLabel *label = new QLabel ("Cell", this); - insertBeforeButtons (label, false); + QLabel* label = new QLabel("Cell", this); + insertBeforeButtons(label, false); // Add cell ID input with auto-completion. // Only existing cell IDs are accepted so no ID validation is performed. mCell = new CSVWidget::DropLineEdit(CSMWorld::ColumnBase::Display_Cell, this); mCell->setCompleter(completionManager.getCompleter(CSMWorld::ColumnBase::Display_Cell).get()); - insertBeforeButtons (mCell, true); + insertBeforeButtons(mCell, true); - setManualEditing (false); + setManualEditing(false); - connect (mCell, SIGNAL (textChanged (const QString&)), this, SLOT (cellChanged())); - connect (mCell, SIGNAL (returnPressed()), this, SLOT (inputReturnPressed())); + connect(mCell, &CSVWidget::DropLineEdit::textChanged, this, &ReferenceCreator::cellChanged); + connect(mCell, &CSVWidget::DropLineEdit::returnPressed, this, &ReferenceCreator::inputReturnPressed); } void CSVWorld::ReferenceCreator::reset() { GenericCreator::reset(); - mCell->setText (""); + mCell->setText(""); mId = getData().getReferences().getNewId(); } @@ -60,11 +65,11 @@ std::string CSVWorld::ReferenceCreator::getErrors() const // record is internal and requires neither user input nor verification. std::string errors; - std::string cell = mCell->text().toUtf8().constData(); + const ESM::RefId cell = ESM::RefId::stringRefId(mCell->text().toStdString()); if (cell.empty()) errors += "Missing Cell ID"; - else if (getData().getCells().searchId (cell)==-1) + else if (getData().getCells().searchId(cell) == -1) errors += "Invalid Cell ID"; return errors; @@ -80,26 +85,21 @@ void CSVWorld::ReferenceCreator::cellChanged() update(); } -void CSVWorld::ReferenceCreator::cloneMode(const std::string& originId, - const CSMWorld::UniversalId::Type type) +void CSVWorld::ReferenceCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { - CSMWorld::IdTable& referenceTable = dynamic_cast ( - *getData().getTableModel (CSMWorld::UniversalId::Type_References)); + CSMWorld::IdTable& referenceTable + = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_References)); - int cellIdColumn = referenceTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); + int cellIdColumn = referenceTable.findColumnIndex(CSMWorld::Columns::ColumnId_Cell); - mCell->setText ( - referenceTable.data (referenceTable.getModelIndex (originId, cellIdColumn)).toString()); + mCell->setText(referenceTable.data(referenceTable.getModelIndex(originId, cellIdColumn)).toString()); CSVWorld::GenericCreator::cloneMode(originId, type); - cellChanged(); //otherwise ok button will remain disabled + cellChanged(); // otherwise ok button will remain disabled } -CSVWorld::Creator *CSVWorld::ReferenceCreatorFactory::makeCreator (CSMDoc::Document& document, - const CSMWorld::UniversalId& id) const +CSVWorld::Creator* CSVWorld::ReferenceCreatorFactory::makeCreator( + CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { - return new ReferenceCreator(document.getData(), - document.getUndoStack(), - id, - document.getIdCompletionManager()); + return new ReferenceCreator(document.getData(), document.getUndoStack(), id, document.getIdCompletionManager()); } diff --git a/apps/opencs/view/world/referencecreator.hpp b/apps/opencs/view/world/referencecreator.hpp index 3903900ad27..1e6ab5a7465 100644 --- a/apps/opencs/view/world/referencecreator.hpp +++ b/apps/opencs/view/world/referencecreator.hpp @@ -1,11 +1,26 @@ #ifndef CSV_WORLD_REFERENCECREATOR_H #define CSV_WORLD_REFERENCECREATOR_H +#include + #include "genericcreator.hpp" +#include +#include + +class QObject; +class QUndoStack; + namespace CSMWorld { class IdCompletionManager; + class CreateCommand; + class Data; +} + +namespace CSMDoc +{ + class Document; } namespace CSVWidget @@ -18,45 +33,41 @@ namespace CSVWorld class ReferenceCreator : public GenericCreator { - Q_OBJECT - - CSVWidget::DropLineEdit *mCell; - std::string mId; + Q_OBJECT - private: + CSVWidget::DropLineEdit* mCell; + std::string mId; - std::string getId() const override; + private: + std::string getId() const override; - void configureCreateCommand (CSMWorld::CreateCommand& command) const override; + void configureCreateCommand(CSMWorld::CreateCommand& command) const override; - public: + public: + ReferenceCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, + CSMWorld::IdCompletionManager& completionManager); - ReferenceCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager &completionManager); + void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; - void cloneMode(const std::string& originId, - const CSMWorld::UniversalId::Type type) override; + void reset() override; - void reset() override; + std::string getErrors() const override; + ///< Return formatted error descriptions for the current state of the creator. if an empty + /// string is returned, there is no error. - std::string getErrors() const override; - ///< Return formatted error descriptions for the current state of the creator. if an empty - /// string is returned, there is no error. + /// Focus main input widget + void focus() override; - /// Focus main input widget - void focus() override; + private slots: - private slots: - - void cellChanged(); + void cellChanged(); }; class ReferenceCreatorFactory : public CreatorFactoryBase { - public: - - Creator *makeCreator (CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; - ///< The ownership of the returned Creator is transferred to the caller. + public: + Creator* makeCreator(CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; + ///< The ownership of the returned Creator is transferred to the caller. }; } diff --git a/apps/opencs/view/world/regionmap.cpp b/apps/opencs/view/world/regionmap.cpp index d2d4fbd237f..17d0016afc6 100644 --- a/apps/opencs/view/world/regionmap.cpp +++ b/apps/opencs/view/world/regionmap.cpp @@ -1,116 +1,123 @@ #include "regionmap.hpp" #include +#include #include #include +#include +#include +#include +#include #include -#include +#include #include +#include + +#include #include "../../model/doc/document.hpp" -#include "../../model/world/regionmap.hpp" -#include "../../model/world/universalid.hpp" +#include "../../model/world/columns.hpp" +#include "../../model/world/commandmacro.hpp" +#include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" -#include "../../model/world/commands.hpp" -#include "../../model/world/columns.hpp" +#include "../../model/world/regionmap.hpp" #include "../../model/world/tablemimedata.hpp" -#include "../../model/world/commandmacro.hpp" +#include "../../model/world/universalid.hpp" -void CSVWorld::RegionMap::contextMenuEvent (QContextMenuEvent *event) +void CSVWorld::RegionMap::contextMenuEvent(QContextMenuEvent* event) { - QMenu menu (this); + QMenu menu(this); - if (getUnselectedCells().size()>0) - menu.addAction (mSelectAllAction); + if (getUnselectedCells().size() > 0) + menu.addAction(mSelectAllAction); - if (selectionModel()->selectedIndexes().size()>0) - menu.addAction (mClearSelectionAction); + if (selectionModel()->selectedIndexes().size() > 0) + menu.addAction(mClearSelectionAction); - if (getMissingRegionCells().size()>0) - menu.addAction (mSelectRegionsAction); + if (getMissingRegionCells().size() > 0) + menu.addAction(mSelectRegionsAction); - int selectedNonExistentCells = getSelectedCells (false, true).size(); + int selectedNonExistentCells = getSelectedCells(false, true).size(); - if (selectedNonExistentCells>0) + if (selectedNonExistentCells > 0) { - if (selectedNonExistentCells==1) - mCreateCellsAction->setText ("Create one Cell"); + if (selectedNonExistentCells == 1) + mCreateCellsAction->setText("Create one Cell"); else { std::ostringstream stream; stream << "Create " << selectedNonExistentCells << " cells"; - mCreateCellsAction->setText (QString::fromUtf8 (stream.str().c_str())); + mCreateCellsAction->setText(QString::fromUtf8(stream.str().c_str())); } - menu.addAction (mCreateCellsAction); + menu.addAction(mCreateCellsAction); } - if (getSelectedCells().size()>0) + if (getSelectedCells().size() > 0) { if (!mRegionId.empty()) { - mSetRegionAction->setText (QString::fromUtf8 (("Set Region to " + mRegionId).c_str())); - menu.addAction (mSetRegionAction); + mSetRegionAction->setText(QString::fromUtf8(("Set Region to " + mRegionId).c_str())); + menu.addAction(mSetRegionAction); } - menu.addAction (mUnsetRegionAction); + menu.addAction(mUnsetRegionAction); - menu.addAction (mViewInTableAction); + menu.addAction(mViewInTableAction); } - if (selectionModel()->selectedIndexes().size()>0) - menu.addAction (mViewAction); + if (selectionModel()->selectedIndexes().size() > 0) + menu.addAction(mViewAction); - menu.exec (event->globalPos()); + menu.exec(event->globalPos()); } QModelIndexList CSVWorld::RegionMap::getUnselectedCells() const { - const QAbstractItemModel *model = QTableView::model(); + const QAbstractItemModel* model = QTableView::model(); int rows = model->rowCount(); int columns = model->columnCount(); QModelIndexList selected = selectionModel()->selectedIndexes(); - std::sort (selected.begin(), selected.end()); + std::sort(selected.begin(), selected.end()); QModelIndexList all; - for (int y=0; yindex (y, x); - if (model->data (index, Qt::BackgroundRole)!=QBrush (Qt::DiagCrossPattern)) - all.push_back (index); + QModelIndex index = model->index(y, x); + if (model->data(index, Qt::BackgroundRole) != QBrush(Qt::DiagCrossPattern)) + all.push_back(index); } - std::sort (all.begin(), all.end()); + std::sort(all.begin(), all.end()); QModelIndexList list; - std::set_difference (all.begin(), all.end(), selected.begin(), selected.end(), - std::back_inserter (list)); + std::set_difference(all.begin(), all.end(), selected.begin(), selected.end(), std::back_inserter(list)); return list; } -QModelIndexList CSVWorld::RegionMap::getSelectedCells (bool existent, bool nonExistent) const +QModelIndexList CSVWorld::RegionMap::getSelectedCells(bool existent, bool nonExistent) const { - const QAbstractItemModel *model = QTableView::model(); + const QAbstractItemModel* model = QTableView::model(); QModelIndexList selected = selectionModel()->selectedIndexes(); QModelIndexList list; - for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) + for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { - bool exists = model->data (*iter, Qt::BackgroundRole)!=QBrush (Qt::DiagCrossPattern); + bool exists = model->data(*iter, Qt::BackgroundRole) != QBrush(Qt::DiagCrossPattern); if ((exists && existent) || (!exists && nonExistent)) - list.push_back (*iter); + list.push_back(*iter); } return list; @@ -118,117 +125,117 @@ QModelIndexList CSVWorld::RegionMap::getSelectedCells (bool existent, bool nonEx QModelIndexList CSVWorld::RegionMap::getMissingRegionCells() const { - const QAbstractItemModel *model = QTableView::model(); + const QAbstractItemModel* model = QTableView::model(); QModelIndexList selected = selectionModel()->selectedIndexes(); std::set regions; - for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) + for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { - std::string region = - model->data (*iter, CSMWorld::RegionMap::Role_Region).toString().toUtf8().constData(); + std::string region = model->data(*iter, CSMWorld::RegionMap::Role_Region).toString().toUtf8().constData(); if (!region.empty()) - regions.insert (region); + regions.insert(region); } QModelIndexList list; QModelIndexList unselected = getUnselectedCells(); - for (QModelIndexList::const_iterator iter (unselected.begin()); iter!=unselected.end(); ++iter) + for (QModelIndexList::const_iterator iter(unselected.begin()); iter != unselected.end(); ++iter) { - std::string region = - model->data (*iter, CSMWorld::RegionMap::Role_Region).toString().toUtf8().constData(); + std::string region = model->data(*iter, CSMWorld::RegionMap::Role_Region).toString().toUtf8().constData(); - if (!region.empty() && regions.find (region)!=regions.end()) - list.push_back (*iter); + if (!region.empty() && regions.find(region) != regions.end()) + list.push_back(*iter); } return list; } -void CSVWorld::RegionMap::setRegion (const std::string& regionId) +void CSVWorld::RegionMap::setRegion(const std::string& regionId) { QModelIndexList selected = getSelectedCells(); - QAbstractItemModel *regionModel = model(); + QAbstractItemModel* regionModel = model(); - CSMWorld::IdTable *cellsModel = &dynamic_cast (* - mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + CSMWorld::IdTable* cellsModel + = &dynamic_cast(*mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_Cells)); - QString regionId2 = QString::fromUtf8 (regionId.c_str()); + QString regionId2 = QString::fromUtf8(regionId.c_str()); - CSMWorld::CommandMacro macro (mDocument.getUndoStack(), selected.size()>1 ? tr ("Set Region") : ""); + CSMWorld::CommandMacro macro(mDocument.getUndoStack(), selected.size() > 1 ? tr("Set Region") : ""); - for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) + for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { - std::string cellId = regionModel->data (*iter, CSMWorld::RegionMap::Role_CellId). - toString().toUtf8().constData(); + std::string cellId = regionModel->data(*iter, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData(); - QModelIndex index = cellsModel->getModelIndex (cellId, - cellsModel->findColumnIndex (CSMWorld::Columns::ColumnId_Region)); + QModelIndex index + = cellsModel->getModelIndex(cellId, cellsModel->findColumnIndex(CSMWorld::Columns::ColumnId_Region)); - macro.push (new CSMWorld::ModifyCommand (*cellsModel, index, regionId2)); + macro.push(new CSMWorld::ModifyCommand(*cellsModel, index, regionId2)); } } -CSVWorld::RegionMap::RegionMap (const CSMWorld::UniversalId& universalId, - CSMDoc::Document& document, QWidget *parent) -: DragRecordTable(document, parent) +CSVWorld::RegionMap::RegionMap(const CSMWorld::UniversalId& universalId, CSMDoc::Document& document, QWidget* parent) + : DragRecordTable(document, parent) { verticalHeader()->hide(); horizontalHeader()->hide(); - setSelectionMode (QAbstractItemView::ExtendedSelection); + setSelectionMode(QAbstractItemView::ExtendedSelection); - setModel (document.getData().getTableModel (universalId)); + setModel(document.getData().getTableModel(universalId)); resizeColumnsToContents(); resizeRowsToContents(); - mSelectAllAction = new QAction (tr ("Select All"), this); - connect (mSelectAllAction, SIGNAL (triggered()), this, SLOT (selectAll())); - addAction (mSelectAllAction); + mSelectAllAction = new QAction(tr("Select All"), this); + connect(mSelectAllAction, &QAction::triggered, this, &RegionMap::selectAll); + addAction(mSelectAllAction); - mClearSelectionAction = new QAction (tr ("Clear Selection"), this); - connect (mClearSelectionAction, SIGNAL (triggered()), this, SLOT (clearSelection())); - addAction (mClearSelectionAction); + mClearSelectionAction = new QAction(tr("Clear Selection"), this); + connect(mClearSelectionAction, &QAction::triggered, this, &RegionMap::clearSelection); + addAction(mClearSelectionAction); - mSelectRegionsAction = new QAction (tr ("Select Regions"), this); - connect (mSelectRegionsAction, SIGNAL (triggered()), this, SLOT (selectRegions())); - addAction (mSelectRegionsAction); + mSelectRegionsAction = new QAction(tr("Select Regions"), this); + connect(mSelectRegionsAction, &QAction::triggered, this, &RegionMap::selectRegions); + addAction(mSelectRegionsAction); - mCreateCellsAction = new QAction (tr ("Create Cells Action"), this); - connect (mCreateCellsAction, SIGNAL (triggered()), this, SLOT (createCells())); - addAction (mCreateCellsAction); + mCreateCellsAction = new QAction(tr("Create Cells Action"), this); + connect(mCreateCellsAction, &QAction::triggered, this, &RegionMap::createCells); + addAction(mCreateCellsAction); - mSetRegionAction = new QAction (tr ("Set Region"), this); - connect (mSetRegionAction, SIGNAL (triggered()), this, SLOT (setRegion())); - addAction (mSetRegionAction); + mSetRegionAction = new QAction(tr("Set Region"), this); + connect(mSetRegionAction, &QAction::triggered, this, qOverload<>(&RegionMap::setRegion)); + addAction(mSetRegionAction); - mUnsetRegionAction = new QAction (tr ("Unset Region"), this); - connect (mUnsetRegionAction, SIGNAL (triggered()), this, SLOT (unsetRegion())); - addAction (mUnsetRegionAction); + mUnsetRegionAction = new QAction(tr("Unset Region"), this); + connect(mUnsetRegionAction, &QAction::triggered, this, &RegionMap::unsetRegion); + addAction(mUnsetRegionAction); - mViewAction = new QAction (tr ("View Cells"), this); - connect (mViewAction, SIGNAL (triggered()), this, SLOT (view())); - addAction (mViewAction); + mViewAction = new QAction(tr("View Cells"), this); + connect(mViewAction, &QAction::triggered, this, &RegionMap::view); + addAction(mViewAction); - mViewInTableAction = new QAction (tr ("View Cells in Table"), this); - connect (mViewInTableAction, SIGNAL (triggered()), this, SLOT (viewInTable())); - addAction (mViewInTableAction); + mViewInTableAction = new QAction(tr("View Cells in Table"), this); + connect(mViewInTableAction, &QAction::triggered, this, &RegionMap::viewInTable); + addAction(mViewInTableAction); setAcceptDrops(true); + + // Make columns square incase QSizeHint doesnt apply + for (int column = 0; column < this->model()->columnCount(); ++column) + this->setColumnWidth(column, this->rowHeight(0)); } void CSVWorld::RegionMap::selectAll() { QModelIndexList unselected = getUnselectedCells(); - for (QModelIndexList::const_iterator iter (unselected.begin()); iter!=unselected.end(); ++iter) - selectionModel()->select (*iter, QItemSelectionModel::Select); + for (QModelIndexList::const_iterator iter(unselected.begin()); iter != unselected.end(); ++iter) + selectionModel()->select(*iter, QItemSelectionModel::Select); } void CSVWorld::RegionMap::clearSelection() @@ -240,8 +247,8 @@ void CSVWorld::RegionMap::selectRegions() { QModelIndexList unselected = getMissingRegionCells(); - for (QModelIndexList::const_iterator iter (unselected.begin()); iter!=unselected.end(); ++iter) - selectionModel()->select (*iter, QItemSelectionModel::Select); + for (QModelIndexList::const_iterator iter(unselected.begin()); iter != unselected.end(); ++iter) + selectionModel()->select(*iter, QItemSelectionModel::Select); } void CSVWorld::RegionMap::createCells() @@ -249,19 +256,18 @@ void CSVWorld::RegionMap::createCells() if (mEditLock) return; - QModelIndexList selected = getSelectedCells (false, true); + QModelIndexList selected = getSelectedCells(false, true); - CSMWorld::IdTable *cellsModel = &dynamic_cast (* - mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + CSMWorld::IdTable* cellsModel + = &dynamic_cast(*mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_Cells)); - CSMWorld::CommandMacro macro (mDocument.getUndoStack(), selected.size()>1 ? tr ("Create cells"): ""); + CSMWorld::CommandMacro macro(mDocument.getUndoStack(), selected.size() > 1 ? tr("Create cells") : ""); - for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) + for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { - std::string cellId = model()->data (*iter, CSMWorld::RegionMap::Role_CellId). - toString().toUtf8().constData(); + std::string cellId = model()->data(*iter, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData(); - macro.push (new CSMWorld::CreateCommand (*cellsModel, cellId)); + macro.push(new CSMWorld::CreateCommand(*cellsModel, cellId)); } } @@ -270,7 +276,7 @@ void CSVWorld::RegionMap::setRegion() if (mEditLock) return; - setRegion (mRegionId); + setRegion(mRegionId); } void CSVWorld::RegionMap::unsetRegion() @@ -278,7 +284,7 @@ void CSVWorld::RegionMap::unsetRegion() if (mEditLock) return; - setRegion (""); + setRegion(""); } void CSVWorld::RegionMap::view() @@ -290,10 +296,9 @@ void CSVWorld::RegionMap::view() bool first = true; - for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) + for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { - std::string cellId = model()->data (*iter, CSMWorld::RegionMap::Role_CellId). - toString().toUtf8().constData(); + std::string cellId = model()->data(*iter, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData(); if (first) first = false; @@ -303,7 +308,8 @@ void CSVWorld::RegionMap::view() hint << cellId; } - emit editRequest (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Scene, ESM::CellId::sDefaultWorldspace), + emit editRequest( + CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Scene, ESM::Cell::sDefaultWorldspaceId.getValue()), hint.str()); } @@ -316,10 +322,9 @@ void CSVWorld::RegionMap::viewInTable() bool first = true; - for (QModelIndexList::const_iterator iter (selected.begin()); iter!=selected.end(); ++iter) + for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { - std::string cellId = model()->data (*iter, CSMWorld::RegionMap::Role_CellId). - toString().toUtf8().constData(); + std::string cellId = model()->data(*iter, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData(); if (first) first = false; @@ -331,66 +336,74 @@ void CSVWorld::RegionMap::viewInTable() hint << ")"; - emit editRequest (CSMWorld::UniversalId::Type_Cells, hint.str()); + emit editRequest(CSMWorld::UniversalId::Type_Cells, hint.str()); } -void CSVWorld::RegionMap::mouseMoveEvent (QMouseEvent* event) +void CSVWorld::RegionMap::mouseMoveEvent(QMouseEvent* event) { - startDragFromTable(*this); + startDragFromTable(*this, indexAt(event->pos())); } -std::vector< CSMWorld::UniversalId > CSVWorld::RegionMap::getDraggedRecords() const +std::vector CSVWorld::RegionMap::getDraggedRecords() const { QModelIndexList selected(getSelectedCells(true, false)); std::vector ids; for (const QModelIndex& it : selected) { - ids.emplace_back( - CSMWorld::UniversalId::Type_Cell, - model()->data(it, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData()); + ids.emplace_back(CSMWorld::UniversalId::Type_Cell, + model()->data(it, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData()); } selected = getSelectedCells(false, true); for (const QModelIndex& it : selected) { - ids.emplace_back( - CSMWorld::UniversalId::Type_Cell_Missing, - model()->data(it, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData()); + ids.emplace_back(CSMWorld::UniversalId::Type_Cell_Missing, + model()->data(it, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData()); } return ids; } -void CSVWorld::RegionMap::dropEvent (QDropEvent* event) +void CSVWorld::RegionMap::dragMoveEvent(QDragMoveEvent* event) { - QModelIndex index = indexAt (event->pos()); + const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); + if (mime != nullptr && (mime->holdsType(CSMWorld::UniversalId::Type_Region))) + { + event->accept(); + return; + } + + event->ignore(); +} - bool exists = QTableView::model()->data(index, Qt::BackgroundRole)!=QBrush (Qt::DiagCrossPattern); +void CSVWorld::RegionMap::dropEvent(QDropEvent* event) +{ + QModelIndex index = indexAt(event->pos()); + bool exists = QTableView::model()->data(index, Qt::BackgroundRole) != QBrush(Qt::DiagCrossPattern); if (!index.isValid() || !exists) { return; } - const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped return; if (mime->fromDocument(mDocument) && mime->holdsType(CSMWorld::UniversalId::Type_Region)) { - CSMWorld::UniversalId record (mime->returnMatching (CSMWorld::UniversalId::Type_Region)); + CSMWorld::UniversalId record(mime->returnMatching(CSMWorld::UniversalId::Type_Region)); - QAbstractItemModel *regionModel = model(); + QAbstractItemModel* regionModel = model(); - CSMWorld::IdTable *cellsModel = &dynamic_cast (* - mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + CSMWorld::IdTable* cellsModel + = &dynamic_cast(*mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_Cells)); - std::string cellId(regionModel->data (index, CSMWorld::RegionMap::Role_CellId). - toString().toUtf8().constData()); + std::string cellId(regionModel->data(index, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData()); - QModelIndex index2(cellsModel->getModelIndex (cellId, - cellsModel->findColumnIndex (CSMWorld::Columns::ColumnId_Region))); + QModelIndex index2( + cellsModel->getModelIndex(cellId, cellsModel->findColumnIndex(CSMWorld::Columns::ColumnId_Region))); - mDocument.getUndoStack().push(new CSMWorld::ModifyCommand - (*cellsModel, index2, QString::fromUtf8(record.getId().c_str()))); + mDocument.getUndoStack().push( + new CSMWorld::ModifyCommand(*cellsModel, index2, QString::fromUtf8(record.getId().c_str()))); mRegionId = record.getId(); } diff --git a/apps/opencs/view/world/regionmap.hpp b/apps/opencs/view/world/regionmap.hpp index 443de9ce3d5..137b47ed836 100644 --- a/apps/opencs/view/world/regionmap.hpp +++ b/apps/opencs/view/world/regionmap.hpp @@ -1,15 +1,19 @@ #ifndef CSV_WORLD_REGIONMAP_H #define CSV_WORLD_REGIONMAP_H -#include -#include +#include -#include -#include +#include +#include #include "./dragrecordtable.hpp" class QAction; +class QContextMenuEvent; +class QDropEvent; +class QMouseEvent; +class QObject; +class QWidget; namespace CSMDoc { @@ -25,67 +29,66 @@ namespace CSVWorld { class RegionMap : public DragRecordTable { - Q_OBJECT - - QAction *mSelectAllAction; - QAction *mClearSelectionAction; - QAction *mSelectRegionsAction; - QAction *mCreateCellsAction; - QAction *mSetRegionAction; - QAction *mUnsetRegionAction; - QAction *mViewAction; - QAction *mViewInTableAction; - std::string mRegionId; + Q_OBJECT - private: + QAction* mSelectAllAction; + QAction* mClearSelectionAction; + QAction* mSelectRegionsAction; + QAction* mCreateCellsAction; + QAction* mSetRegionAction; + QAction* mUnsetRegionAction; + QAction* mViewAction; + QAction* mViewInTableAction; + std::string mRegionId; - void contextMenuEvent (QContextMenuEvent *event) override; + private: + void contextMenuEvent(QContextMenuEvent* event) override; - QModelIndexList getUnselectedCells() const; - ///< \note Non-existent cells are not listed. + QModelIndexList getUnselectedCells() const; + ///< \note Non-existent cells are not listed. - QModelIndexList getSelectedCells (bool existent = true, bool nonExistent = false) const; - ///< \param existent Include existent cells. - /// \param nonExistent Include non-existent cells. + QModelIndexList getSelectedCells(bool existent = true, bool nonExistent = false) const; + ///< \param existent Include existent cells. + /// \param nonExistent Include non-existent cells. - QModelIndexList getMissingRegionCells() const; - ///< Unselected cells within all regions that have at least one selected cell. + QModelIndexList getMissingRegionCells() const; + ///< Unselected cells within all regions that have at least one selected cell. - void setRegion (const std::string& regionId); - ///< Set region Id of selected cells. + void setRegion(const std::string& regionId); + ///< Set region Id of selected cells. - void mouseMoveEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent* event) override; - void dropEvent(QDropEvent* event) override; + void dragMoveEvent(QDragMoveEvent* event) override; - public: + void dropEvent(QDropEvent* event) override; - RegionMap (const CSMWorld::UniversalId& universalId, CSMDoc::Document& document, - QWidget *parent = nullptr); + public: + RegionMap(const CSMWorld::UniversalId& universalId, CSMDoc::Document& document, QWidget* parent = nullptr); - std::vector getDraggedRecords() const override; + std::vector getDraggedRecords() const override; - signals: + signals: - void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); + void editRequest(const CSMWorld::UniversalId& id, const std::string& hint); - private slots: + private slots: - void selectAll() override; + void selectAll() override; - void clearSelection(); + void clearSelection(); - void selectRegions(); + void selectRegions(); - void createCells(); + void createCells(); - void setRegion(); + void setRegion(); - void unsetRegion(); + void unsetRegion(); - void view(); + void view(); - void viewInTable(); + void viewInTable(); }; } diff --git a/apps/opencs/view/world/regionmapsubview.cpp b/apps/opencs/view/world/regionmapsubview.cpp index 996d1dc8b80..7b07f2b65db 100644 --- a/apps/opencs/view/world/regionmapsubview.cpp +++ b/apps/opencs/view/world/regionmapsubview.cpp @@ -1,26 +1,25 @@ #include "regionmapsubview.hpp" +#include + #include "regionmap.hpp" -CSVWorld::RegionMapSubView::RegionMapSubView (CSMWorld::UniversalId universalId, - CSMDoc::Document& document) -: CSVDoc::SubView (universalId) +CSVWorld::RegionMapSubView::RegionMapSubView(CSMWorld::UniversalId universalId, CSMDoc::Document& document) + : CSVDoc::SubView(universalId) { - mRegionMap = new RegionMap (universalId, document, this); + mRegionMap = new RegionMap(universalId, document, this); - setWidget (mRegionMap); + setWidget(mRegionMap); - connect (mRegionMap, SIGNAL (editRequest (const CSMWorld::UniversalId&, const std::string&)), - this, SLOT (editRequest (const CSMWorld::UniversalId&, const std::string&))); + connect(mRegionMap, &RegionMap::editRequest, this, &RegionMapSubView::editRequest); } -void CSVWorld::RegionMapSubView::setEditLock (bool locked) +void CSVWorld::RegionMapSubView::setEditLock(bool locked) { - mRegionMap->setEditLock (locked); + mRegionMap->setEditLock(locked); } -void CSVWorld::RegionMapSubView::editRequest (const CSMWorld::UniversalId& id, - const std::string& hint) +void CSVWorld::RegionMapSubView::editRequest(const CSMWorld::UniversalId& id, const std::string& hint) { - focusId (id, hint); + focusId(id, hint); } diff --git a/apps/opencs/view/world/regionmapsubview.hpp b/apps/opencs/view/world/regionmapsubview.hpp index 232d88fc640..3ac607a28ae 100644 --- a/apps/opencs/view/world/regionmapsubview.hpp +++ b/apps/opencs/view/world/regionmapsubview.hpp @@ -1,9 +1,11 @@ #ifndef CSV_WORLD_REGIONMAPSUBVIEW_H #define CSV_WORLD_REGIONMAPSUBVIEW_H +#include + #include "../doc/subview.hpp" -class QAction; +#include namespace CSMDoc { @@ -16,19 +18,18 @@ namespace CSVWorld class RegionMapSubView : public CSVDoc::SubView { - Q_OBJECT - - RegionMap *mRegionMap; + Q_OBJECT - public: + RegionMap* mRegionMap; - RegionMapSubView (CSMWorld::UniversalId universalId, CSMDoc::Document& document); + public: + RegionMapSubView(CSMWorld::UniversalId universalId, CSMDoc::Document& document); - void setEditLock (bool locked) override; + void setEditLock(bool locked) override; - private slots: + private slots: - void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); + void editRequest(const CSMWorld::UniversalId& id, const std::string& hint); }; } diff --git a/apps/opencs/view/world/scenesubview.cpp b/apps/opencs/view/world/scenesubview.cpp index 58d159a178c..3b3ada43b51 100644 --- a/apps/opencs/view/world/scenesubview.cpp +++ b/apps/opencs/view/world/scenesubview.cpp @@ -1,11 +1,18 @@ #include "scenesubview.hpp" +#include +#include +#include #include -#include #include -#include -#include +#include +#include +#include + +#include +#include +#include #include "../../model/doc/document.hpp" @@ -18,30 +25,33 @@ #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" -#include "../widget/scenetooltoggle.hpp" -#include "../widget/scenetooltoggle2.hpp" #include "../widget/scenetoolrun.hpp" +#include "../widget/scenetooltoggle2.hpp" -#include "tablebottombox.hpp" #include "creator.hpp" +#include "tablebottombox.hpp" -CSVWorld::SceneSubView::SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: SubView (id), mScene(nullptr), mLayout(new QHBoxLayout), mDocument(document), mToolbar(nullptr) +CSVWorld::SceneSubView::SceneSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) + : SubView(id) + , mScene(nullptr) + , mLayout(new QHBoxLayout) + , mDocument(document) + , mToolbar(nullptr) { - QVBoxLayout *layout = new QVBoxLayout; + QVBoxLayout* layout = new QVBoxLayout; - layout->addWidget (mBottom = new TableBottomBox (NullCreatorFactory(), document, id, this), 0); + layout->addWidget(mBottom = new TableBottomBox(NullCreatorFactory(), document, id, this), 0); - mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); + mLayout->setContentsMargins(QMargins(0, 0, 0, 0)); CSVRender::WorldspaceWidget* worldspaceWidget = nullptr; widgetType whatWidget; - if (id.getId()==ESM::CellId::sDefaultWorldspace) + if (Misc::StringUtils::ciEqual(id.getId(), ESM::Cell::sDefaultWorldspaceId.getValue())) { whatWidget = widget_Paged; - CSVRender::PagedWorldspaceWidget *newWidget = new CSVRender::PagedWorldspaceWidget (this, document); + CSVRender::PagedWorldspaceWidget* newWidget = new CSVRender::PagedWorldspaceWidget(this, document); worldspaceWidget = newWidget; @@ -51,7 +61,8 @@ CSVWorld::SceneSubView::SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::D { whatWidget = widget_Unpaged; - CSVRender::UnpagedWorldspaceWidget *newWidget = new CSVRender::UnpagedWorldspaceWidget (id.getId(), document, this); + CSVRender::UnpagedWorldspaceWidget* newWidget + = new CSVRender::UnpagedWorldspaceWidget(id.getId(), document, this); worldspaceWidget = newWidget; @@ -60,91 +71,85 @@ CSVWorld::SceneSubView::SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::D replaceToolbarAndWorldspace(worldspaceWidget, makeToolbar(worldspaceWidget, whatWidget)); - layout->insertLayout (0, mLayout, 1); + layout->insertLayout(0, mLayout, 1); - CSVFilter::FilterBox *filterBox = new CSVFilter::FilterBox (document.getData(), this); + CSVFilter::FilterBox* filterBox = new CSVFilter::FilterBox(document.getData(), this); - layout->insertWidget (0, filterBox); + layout->insertWidget(0, filterBox); - QWidget *widget = new QWidget; + QWidget* widget = new QWidget; - widget->setLayout (layout); + widget->setLayout(layout); - setWidget (widget); + setWidget(widget); } -void CSVWorld::SceneSubView::makeConnections (CSVRender::UnpagedWorldspaceWidget* widget) +void CSVWorld::SceneSubView::makeConnections(CSVRender::UnpagedWorldspaceWidget* widget) { - connect (widget, SIGNAL (closeRequest()), this, SLOT (closeRequest())); + connect(widget, &CSVRender::UnpagedWorldspaceWidget::closeRequest, this, qOverload<>(&SceneSubView::closeRequest)); - connect(widget, SIGNAL(dataDropped(const std::vector&)), - this, SLOT(handleDrop(const std::vector&))); + connect(widget, &CSVRender::UnpagedWorldspaceWidget::dataDropped, this, &SceneSubView::handleDrop); - connect(widget, SIGNAL(cellChanged(const CSMWorld::UniversalId&)), - this, SLOT(cellSelectionChanged(const CSMWorld::UniversalId&))); + connect(widget, &CSVRender::UnpagedWorldspaceWidget::cellChanged, this, + qOverload(&SceneSubView::cellSelectionChanged)); - connect(widget, SIGNAL(requestFocus (const std::string&)), - this, SIGNAL(requestFocus (const std::string&))); + connect(widget, &CSVRender::UnpagedWorldspaceWidget::requestFocus, this, &SceneSubView::requestFocus); } -void CSVWorld::SceneSubView::makeConnections (CSVRender::PagedWorldspaceWidget* widget) +void CSVWorld::SceneSubView::makeConnections(CSVRender::PagedWorldspaceWidget* widget) { - connect (widget, SIGNAL (closeRequest()), this, SLOT (closeRequest())); + connect(widget, &CSVRender::PagedWorldspaceWidget::closeRequest, this, qOverload<>(&SceneSubView::closeRequest)); - connect(widget, SIGNAL(dataDropped(const std::vector&)), - this, SLOT(handleDrop(const std::vector&))); + connect(widget, &CSVRender::PagedWorldspaceWidget::dataDropped, this, &SceneSubView::handleDrop); - connect (widget, SIGNAL (cellSelectionChanged (const CSMWorld::CellSelection&)), - this, SLOT (cellSelectionChanged (const CSMWorld::CellSelection&))); + connect(widget, &CSVRender::PagedWorldspaceWidget::cellSelectionChanged, this, + qOverload(&SceneSubView::cellSelectionChanged)); - connect(widget, SIGNAL(requestFocus (const std::string&)), - this, SIGNAL(requestFocus (const std::string&))); + connect(widget, &CSVRender::PagedWorldspaceWidget::requestFocus, this, &SceneSubView::requestFocus); } -CSVWidget::SceneToolbar* CSVWorld::SceneSubView::makeToolbar (CSVRender::WorldspaceWidget* widget, widgetType type) +CSVWidget::SceneToolbar* CSVWorld::SceneSubView::makeToolbar(CSVRender::WorldspaceWidget* widget, widgetType type) { - CSVWidget::SceneToolbar* toolbar = new CSVWidget::SceneToolbar (48+6, this); + CSVWidget::SceneToolbar* toolbar = new CSVWidget::SceneToolbar(48 + 6, this); - CSVWidget::SceneToolMode *navigationTool = widget->makeNavigationSelector (toolbar); - toolbar->addTool (navigationTool); + CSVWidget::SceneToolMode* navigationTool = widget->makeNavigationSelector(toolbar); + toolbar->addTool(navigationTool); - CSVWidget::SceneToolMode *lightingTool = widget->makeLightingSelector (toolbar); - toolbar->addTool (lightingTool); + CSVWidget::SceneToolMode* lightingTool = widget->makeLightingSelector(toolbar); + toolbar->addTool(lightingTool); - CSVWidget::SceneToolToggle2 *sceneVisibilityTool = - widget->makeSceneVisibilitySelector (toolbar); - toolbar->addTool (sceneVisibilityTool); + CSVWidget::SceneToolToggle2* sceneVisibilityTool = widget->makeSceneVisibilitySelector(toolbar); + toolbar->addTool(sceneVisibilityTool); - if (type==widget_Paged) + if (type == widget_Paged) { - CSVWidget::SceneToolToggle2 *controlVisibilityTool = - dynamic_cast (*widget). - makeControlVisibilitySelector (toolbar); + CSVWidget::SceneToolToggle2* controlVisibilityTool + = dynamic_cast(*widget).makeControlVisibilitySelector(toolbar); - toolbar->addTool (controlVisibilityTool); + toolbar->addTool(controlVisibilityTool); } - CSVWidget::SceneToolRun *runTool = widget->makeRunTool (toolbar); - toolbar->addTool (runTool); + CSVWidget::SceneToolRun* runTool = widget->makeRunTool(toolbar); + toolbar->addTool(runTool); - toolbar->addTool (widget->makeEditModeSelector (toolbar), runTool); + toolbar->addTool(widget->makeEditModeSelector(toolbar), runTool); return toolbar; } -void CSVWorld::SceneSubView::setEditLock (bool locked) +void CSVWorld::SceneSubView::setEditLock(bool locked) { - mScene->setEditLock (locked); + mScene->setEditLock(locked); } -void CSVWorld::SceneSubView::setStatusBar (bool show) +void CSVWorld::SceneSubView::setStatusBar(bool show) { - mBottom->setStatusBar (show); + mBottom->setStatusBar(show); } -void CSVWorld::SceneSubView::useHint (const std::string& hint) +void CSVWorld::SceneSubView::useHint(const std::string& hint) { - mScene->useViewHint (hint); + mScene->useViewHint(hint); } std::string CSVWorld::SceneSubView::getTitle() const @@ -152,56 +157,57 @@ std::string CSVWorld::SceneSubView::getTitle() const return mTitle; } -void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::UniversalId& id) +void CSVWorld::SceneSubView::cellSelectionChanged(const CSMWorld::UniversalId& id) { setUniversalId(id); mTitle = "Scene: " + getUniversalId().getId(); - setWindowTitle (QString::fromUtf8 (mTitle.c_str())); + setWindowTitle(QString::fromUtf8(mTitle.c_str())); emit updateTitle(); } -void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::CellSelection& selection) +void CSVWorld::SceneSubView::cellSelectionChanged(const CSMWorld::CellSelection& selection) { - setUniversalId(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Scene, ESM::CellId::sDefaultWorldspace)); + setUniversalId( + CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Scene, ESM::Cell::sDefaultWorldspaceId.getValue())); int size = selection.getSize(); std::ostringstream stream; stream << "Scene: " << getUniversalId().getId(); - if (size==0) + if (size == 0) stream << " (empty)"; - else if (size==1) + else if (size == 1) { stream << " (" << *selection.begin() << ")"; } else { - stream << " (" << selection.getCentre() << " and " << size-1 << " more "; + stream << " (" << selection.getCentre() << " and " << size - 1 << " more "; - if (size>1) + if (size > 1) stream << "cells around it)"; else stream << "cell around it)"; } mTitle = stream.str(); - setWindowTitle (QString::fromUtf8 (mTitle.c_str())); + setWindowTitle(QString::fromUtf8(mTitle.c_str())); emit updateTitle(); } -void CSVWorld::SceneSubView::handleDrop (const std::vector< CSMWorld::UniversalId >& universalIdData) +void CSVWorld::SceneSubView::handleDrop(const std::vector& universalIdData) { CSVRender::PagedWorldspaceWidget* pagedNewWidget = nullptr; CSVRender::UnpagedWorldspaceWidget* unPagedNewWidget = nullptr; CSVWidget::SceneToolbar* toolbar = nullptr; - CSVRender::WorldspaceWidget::DropType type = CSVRender::WorldspaceWidget::getDropType (universalIdData); + CSVRender::WorldspaceWidget::DropType type = CSVRender::WorldspaceWidget::getDropType(universalIdData); - switch (mScene->getDropRequirements (type)) + switch (mScene->getDropRequirements(type)) { case CSVRender::WorldspaceWidget::canHandle: - mScene->handleDrop (universalIdData, type); + mScene->handleDrop(universalIdData, type); break; case CSVRender::WorldspaceWidget::needPaged: @@ -209,11 +215,12 @@ void CSVWorld::SceneSubView::handleDrop (const std::vector< CSMWorld::UniversalI toolbar = makeToolbar(pagedNewWidget, widget_Paged); makeConnections(pagedNewWidget); replaceToolbarAndWorldspace(pagedNewWidget, toolbar); - mScene->handleDrop (universalIdData, type); + mScene->handleDrop(universalIdData, type); break; case CSVRender::WorldspaceWidget::needUnpaged: - unPagedNewWidget = new CSVRender::UnpagedWorldspaceWidget(universalIdData.begin()->getId(), mDocument, this); + unPagedNewWidget + = new CSVRender::UnpagedWorldspaceWidget(universalIdData.begin()->getId(), mDocument, this); toolbar = makeToolbar(unPagedNewWidget, widget_Unpaged); makeConnections(unPagedNewWidget); replaceToolbarAndWorldspace(unPagedNewWidget, toolbar); @@ -225,7 +232,8 @@ void CSVWorld::SceneSubView::handleDrop (const std::vector< CSMWorld::UniversalI } } -void CSVWorld::SceneSubView::replaceToolbarAndWorldspace (CSVRender::WorldspaceWidget* widget, CSVWidget::SceneToolbar* toolbar) +void CSVWorld::SceneSubView::replaceToolbarAndWorldspace( + CSVRender::WorldspaceWidget* widget, CSVWidget::SceneToolbar* toolbar) { assert(mLayout); @@ -244,12 +252,14 @@ void CSVWorld::SceneSubView::replaceToolbarAndWorldspace (CSVRender::WorldspaceW mScene = widget; mToolbar = toolbar; - connect (mScene, SIGNAL (focusToolbarRequest()), mToolbar, SLOT (setFocus())); - connect (mToolbar, SIGNAL (focusSceneRequest()), mScene, SLOT (setFocus())); + connect(mScene, &CSVRender::WorldspaceWidget::focusToolbarRequest, mToolbar, + qOverload<>(&CSVWidget::SceneToolbar::setFocus)); + connect(mToolbar, &CSVWidget::SceneToolbar::focusSceneRequest, mScene, + qOverload<>(&CSVRender::WorldspaceWidget::setFocus)); - mLayout->addWidget (mToolbar, 0); - mLayout->addWidget (mScene, 1); + mLayout->addWidget(mToolbar, 0); + mLayout->addWidget(mScene, 1); mScene->selectDefaultNavigationMode(); - setFocusProxy (mScene); + setFocusProxy(mScene); } diff --git a/apps/opencs/view/world/scenesubview.hpp b/apps/opencs/view/world/scenesubview.hpp index aabb7ca2a71..7248efd1451 100644 --- a/apps/opencs/view/world/scenesubview.hpp +++ b/apps/opencs/view/world/scenesubview.hpp @@ -3,9 +3,12 @@ #include -#include "../doc/subview.hpp" +#include +#include + +#include -class QModelIndex; +#include "../doc/subview.hpp" namespace CSMWorld { @@ -27,65 +30,60 @@ namespace CSVRender namespace CSVWidget { class SceneToolbar; - class SceneToolMode; } namespace CSVWorld { - class Table; class TableBottomBox; - class CreatorFactoryBase; class SceneSubView : public CSVDoc::SubView { - Q_OBJECT - - TableBottomBox *mBottom; - CSVRender::WorldspaceWidget *mScene; - QHBoxLayout* mLayout; - CSMDoc::Document& mDocument; - CSVWidget::SceneToolbar* mToolbar; - std::string mTitle; - - public: + Q_OBJECT - SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); + TableBottomBox* mBottom; + CSVRender::WorldspaceWidget* mScene; + QHBoxLayout* mLayout; + CSMDoc::Document& mDocument; + CSVWidget::SceneToolbar* mToolbar; + std::string mTitle; - void setEditLock (bool locked) override; + public: + SceneSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); - void setStatusBar (bool show) override; + void setEditLock(bool locked) override; - void useHint (const std::string& hint) override; + void setStatusBar(bool show) override; - std::string getTitle() const override; + void useHint(const std::string& hint) override; - private: + std::string getTitle() const override; - void makeConnections(CSVRender::PagedWorldspaceWidget* widget); + private: + void makeConnections(CSVRender::PagedWorldspaceWidget* widget); - void makeConnections(CSVRender::UnpagedWorldspaceWidget* widget); + void makeConnections(CSVRender::UnpagedWorldspaceWidget* widget); - void replaceToolbarAndWorldspace(CSVRender::WorldspaceWidget* widget, CSVWidget::SceneToolbar* toolbar); + void replaceToolbarAndWorldspace(CSVRender::WorldspaceWidget* widget, CSVWidget::SceneToolbar* toolbar); - enum widgetType - { - widget_Paged, - widget_Unpaged - }; + enum widgetType + { + widget_Paged, + widget_Unpaged + }; - CSVWidget::SceneToolbar* makeToolbar(CSVRender::WorldspaceWidget* widget, widgetType type); + CSVWidget::SceneToolbar* makeToolbar(CSVRender::WorldspaceWidget* widget, widgetType type); - private slots: + private slots: - void cellSelectionChanged (const CSMWorld::CellSelection& selection); + void cellSelectionChanged(const CSMWorld::CellSelection& selection); - void cellSelectionChanged (const CSMWorld::UniversalId& id); + void cellSelectionChanged(const CSMWorld::UniversalId& id); - void handleDrop(const std::vector& data); + void handleDrop(const std::vector& data); - signals: + signals: - void requestFocus (const std::string& id); + void requestFocus(const std::string& id); }; } diff --git a/apps/opencs/view/world/scriptedit.cpp b/apps/opencs/view/world/scriptedit.cpp index 743df9c7656..00acf712352 100644 --- a/apps/opencs/view/world/scriptedit.cpp +++ b/apps/opencs/view/world/scriptedit.cpp @@ -1,22 +1,28 @@ #include "scriptedit.hpp" -#include +#include +#include #include -#include -#include +#include #include +#include #include -#include + +#include +#include +#include +#include #include "../../model/doc/document.hpp" -#include "../../model/world/universalid.hpp" -#include "../../model/world/tablemimedata.hpp" -#include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcut.hpp" +#include "../../model/prefs/state.hpp" +#include "../../model/world/tablemimedata.hpp" +#include "../../model/world/universalid.hpp" -CSVWorld::ScriptEdit::ChangeLock::ChangeLock (ScriptEdit& edit) : mEdit (edit) +CSVWorld::ScriptEdit::ChangeLock::ChangeLock(ScriptEdit& edit) + : mEdit(edit) { ++mEdit.mChangeLocked; } @@ -26,95 +32,77 @@ CSVWorld::ScriptEdit::ChangeLock::~ChangeLock() --mEdit.mChangeLocked; } -bool CSVWorld::ScriptEdit::event (QEvent *event) +bool CSVWorld::ScriptEdit::event(QEvent* event) { // ignore undo and redo shortcuts - if (event->type()==QEvent::ShortcutOverride) + if (event->type() == QEvent::ShortcutOverride) { - QKeyEvent *keyEvent = static_cast (event); + QKeyEvent* keyEvent = static_cast(event); - if (keyEvent->matches (QKeySequence::Undo) || keyEvent->matches (QKeySequence::Redo)) + if (keyEvent->matches(QKeySequence::Undo) || keyEvent->matches(QKeySequence::Redo)) return true; } - return QPlainTextEdit::event (event); + return QPlainTextEdit::event(event); } -CSVWorld::ScriptEdit::ScriptEdit( - const CSMDoc::Document& document, - ScriptHighlighter::Mode mode, - QWidget* parent -) : QPlainTextEdit(parent), - mChangeLocked(0), - mShowLineNum(false), - mLineNumberArea(nullptr), - mDefaultFont(font()), - mMonoFont(QFont("Monospace")), - mTabCharCount(4), - mMarkOccurrences(true), - mDocument(document), - mWhiteListQoutes("^[a-z|_]{1}[a-z|0-9|_]{0,}$", Qt::CaseInsensitive) +CSVWorld::ScriptEdit::ScriptEdit(const CSMDoc::Document& document, ScriptHighlighter::Mode mode, QWidget* parent) + : QPlainTextEdit(parent) + , mChangeLocked(0) + , mShowLineNum(false) + , mLineNumberArea(nullptr) + , mDefaultFont(font()) + , mMonoFont(QFont("Monospace")) + , mTabCharCount(4) + , mMarkOccurrences(true) + , mDocument(document) + , mWhiteListQuotes("^[a-zA-Z_][a-zA-Z0-9_]*$") { wrapLines(false); setTabWidth(); - setUndoRedoEnabled (false); // we use OpenCS-wide undo/redo instead - - mAllowedTypes <associateAction(mCommentAction); - mUncommentAction = new QAction (tr ("Uncomment Selection"), this); - connect(mUncommentAction, SIGNAL (triggered()), this, SLOT (uncommentSelection())); - CSMPrefs::Shortcut *uncommentShortcut = new CSMPrefs::Shortcut("script-editor-uncomment", this); + mUncommentAction = new QAction(tr("Uncomment Selection"), this); + connect(mUncommentAction, &QAction::triggered, this, &ScriptEdit::uncommentSelection); + CSMPrefs::Shortcut* uncommentShortcut = new CSMPrefs::Shortcut("script-editor-uncomment", this); uncommentShortcut->associateAction(mUncommentAction); - mHighlighter = new ScriptHighlighter (document.getData(), mode, ScriptEdit::document()); + mHighlighter = new ScriptHighlighter(document.getData(), mode, ScriptEdit::document()); - connect (&document.getData(), SIGNAL (idListChanged()), this, SLOT (idListChanged())); + connect(&document.getData(), &CSMWorld::Data::idListChanged, this, &ScriptEdit::idListChanged); - connect (&mUpdateTimer, SIGNAL (timeout()), this, SLOT (updateHighlighting())); + connect(&mUpdateTimer, &QTimer::timeout, this, &ScriptEdit::updateHighlighting); - connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), - this, SLOT (settingChanged (const CSMPrefs::Setting *))); + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &ScriptEdit::settingChanged); { - ChangeLock lock (*this); + ChangeLock lock(*this); CSMPrefs::get()["Scripts"].update(); } - mUpdateTimer.setSingleShot (true); + mUpdateTimer.setSingleShot(true); // TODO: provide a font selector dialogue mMonoFont.setStyleHint(QFont::TypeWriter); @@ -122,14 +110,14 @@ CSVWorld::ScriptEdit::ScriptEdit( mLineNumberArea = new LineNumberArea(this); updateLineNumberAreaWidth(0); - connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int))); - connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateLineNumberArea(QRect,int))); + connect(this, &ScriptEdit::blockCountChanged, this, &ScriptEdit::updateLineNumberAreaWidth); + connect(this, &ScriptEdit::updateRequest, this, &ScriptEdit::updateLineNumberArea); updateHighlighting(); } void CSVWorld::ScriptEdit::showLineNum(bool show) { - if(show!=mShowLineNum) + if (show != mShowLineNum) { mShowLineNum = show; updateLineNumberAreaWidth(0); @@ -138,68 +126,70 @@ void CSVWorld::ScriptEdit::showLineNum(bool show) bool CSVWorld::ScriptEdit::isChangeLocked() const { - return mChangeLocked!=0; + return mChangeLocked != 0; } -void CSVWorld::ScriptEdit::dragEnterEvent (QDragEnterEvent* event) +void CSVWorld::ScriptEdit::dragEnterEvent(QDragEnterEvent* event) { - const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); if (!mime) QPlainTextEdit::dragEnterEvent(event); else { - setTextCursor (cursorForPosition (event->pos())); + setTextCursor(cursorForPosition(event->pos())); event->acceptProposedAction(); } } -void CSVWorld::ScriptEdit::dragMoveEvent (QDragMoveEvent* event) +void CSVWorld::ScriptEdit::dragMoveEvent(QDragMoveEvent* event) { - const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); if (!mime) QPlainTextEdit::dragMoveEvent(event); else { - setTextCursor (cursorForPosition (event->pos())); + setTextCursor(cursorForPosition(event->pos())); event->accept(); } } -void CSVWorld::ScriptEdit::dropEvent (QDropEvent* event) +void CSVWorld::ScriptEdit::dropEvent(QDropEvent* event) { - const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped { QPlainTextEdit::dropEvent(event); return; } - setTextCursor (cursorForPosition (event->pos())); + setTextCursor(cursorForPosition(event->pos())); - if (mime->fromDocument (mDocument)) + if (mime->fromDocument(mDocument)) { - std::vector records (mime->getData()); + std::vector records(mime->getData()); for (std::vector::iterator it = records.begin(); it != records.end(); ++it) { - if (mAllowedTypes.contains (it->getType())) + if (mAllowedTypes.contains(it->getType())) { if (stringNeedsQuote(it->getId())) { - insertPlainText(QString::fromUtf8 (('"' + it->getId() + '"').c_str())); - } else { - insertPlainText(QString::fromUtf8 (it->getId().c_str())); + insertPlainText(QString::fromUtf8(('"' + it->getId() + '"').c_str())); + } + else + { + insertPlainText(QString::fromUtf8(it->getId().c_str())); } } } } } -bool CSVWorld::ScriptEdit::stringNeedsQuote (const std::string& id) const +bool CSVWorld::ScriptEdit::stringNeedsQuote(const std::string& id) const { - const QString string(QString::fromUtf8(id.c_str())); // is only for c++11, so let's use qregexp for now. - //I'm not quite sure when do we need to put quotes. To be safe we will use quotes for anything other than… - return !(string.contains(mWhiteListQoutes)); + const QString string(QString::fromUtf8(id.c_str())); + // I'm not quite sure when do we need to put quotes. To be safe we will use quotes for anything other than… + return !(string.contains(mWhiteListQuotes)); } void CSVWorld::ScriptEdit::setTabWidth() @@ -220,7 +210,7 @@ void CSVWorld::ScriptEdit::wrapLines(bool wrap) } } -void CSVWorld::ScriptEdit::settingChanged(const CSMPrefs::Setting *setting) +void CSVWorld::ScriptEdit::settingChanged(const CSMPrefs::Setting* setting) { // Determine which setting was changed. if (mHighlighter->settingChanged(setting)) @@ -259,7 +249,7 @@ void CSVWorld::ScriptEdit::idListChanged() mHighlighter->invalidateIds(); if (!mUpdateTimer.isActive()) - mUpdateTimer.start (0); + mUpdateTimer.start(0); } void CSVWorld::ScriptEdit::updateHighlighting() @@ -267,14 +257,14 @@ void CSVWorld::ScriptEdit::updateHighlighting() if (isChangeLocked()) return; - ChangeLock lock (*this); + ChangeLock lock(*this); mHighlighter->rehighlight(); } int CSVWorld::ScriptEdit::lineNumberAreaWidth() { - if(!mShowLineNum) + if (!mShowLineNum) return 0; int digits = 1; @@ -294,7 +284,7 @@ void CSVWorld::ScriptEdit::updateLineNumberAreaWidth(int /* newBlockCount */) setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); } -void CSVWorld::ScriptEdit::updateLineNumberArea(const QRect &rect, int dy) +void CSVWorld::ScriptEdit::updateLineNumberArea(const QRect& rect, int dy) { if (dy) mLineNumberArea->scroll(0, dy); @@ -314,16 +304,16 @@ void CSVWorld::ScriptEdit::markOccurrences() // prevent infinite recursion with cursor.select(), // which ends up calling this function again // could be fixed with blockSignals, but mDocument is const - disconnect(this, SIGNAL(cursorPositionChanged()), this, nullptr); + disconnect(this, &ScriptEdit::cursorPositionChanged, this, nullptr); cursor.select(QTextCursor::WordUnderCursor); - connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(markOccurrences())); + connect(this, &ScriptEdit::cursorPositionChanged, this, &ScriptEdit::markOccurrences); QString word = cursor.selectedText(); mHighlighter->setMarkedWord(word.toStdString()); mHighlighter->rehighlight(); } } - + void CSVWorld::ScriptEdit::commentSelection() { QTextCursor begin = textCursor(); @@ -356,7 +346,8 @@ void CSVWorld::ScriptEdit::uncommentSelection() begin.beginEditBlock(); - for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) { + for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) + { begin.select(QTextCursor::LineUnderCursor); QString line = begin.selectedText(); @@ -383,7 +374,7 @@ void CSVWorld::ScriptEdit::uncommentSelection() begin.endEditBlock(); } -void CSVWorld::ScriptEdit::resizeEvent(QResizeEvent *e) +void CSVWorld::ScriptEdit::resizeEvent(QResizeEvent* e) { QPlainTextEdit::resizeEvent(e); @@ -391,9 +382,9 @@ void CSVWorld::ScriptEdit::resizeEvent(QResizeEvent *e) mLineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())); } -void CSVWorld::ScriptEdit::contextMenuEvent(QContextMenuEvent *event) +void CSVWorld::ScriptEdit::contextMenuEvent(QContextMenuEvent* event) { - QMenu *menu = createStandardContextMenu(); + QMenu* menu = createStandardContextMenu(); // remove redo/undo since they are disabled QList menuActions = menu->actions(); @@ -411,22 +402,22 @@ void CSVWorld::ScriptEdit::contextMenuEvent(QContextMenuEvent *event) delete menu; } -void CSVWorld::ScriptEdit::lineNumberAreaPaintEvent(QPaintEvent *event) +void CSVWorld::ScriptEdit::lineNumberAreaPaintEvent(QPaintEvent* event) { QPainter painter(mLineNumberArea); QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); - int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); - int bottom = top + (int) blockBoundingRect(block).height(); + int top = (int)blockBoundingGeometry(block).translated(contentOffset()).top(); + int bottom = top + (int)blockBoundingRect(block).height(); int startBlock = textCursor().blockNumber(); int endBlock = textCursor().blockNumber(); - if(textCursor().hasSelection()) + if (textCursor().hasSelection()) { QString str = textCursor().selection().toPlainText(); int offset = str.count("\n"); - if(textCursor().position() < textCursor().anchor()) + if (textCursor().position() < textCursor().anchor()) endBlock += offset; else startBlock -= offset; @@ -434,6 +425,7 @@ void CSVWorld::ScriptEdit::lineNumberAreaPaintEvent(QPaintEvent *event) painter.setBackgroundMode(Qt::OpaqueMode); QFont font = painter.font(); QBrush background = painter.background(); + QColor textColor = QApplication::palette().text().color(); while (block.isValid() && top <= event->rect().bottom()) { @@ -441,7 +433,7 @@ void CSVWorld::ScriptEdit::lineNumberAreaPaintEvent(QPaintEvent *event) { QFont newFont = painter.font(); QString number = QString::number(blockNumber + 1); - if(blockNumber >= startBlock && blockNumber <= endBlock) + if (blockNumber >= startBlock && blockNumber <= endBlock) { painter.setBackground(Qt::cyan); painter.setPen(Qt::darkMagenta); @@ -450,30 +442,32 @@ void CSVWorld::ScriptEdit::lineNumberAreaPaintEvent(QPaintEvent *event) else { painter.setBackground(background); - painter.setPen(Qt::black); + painter.setPen(textColor); } painter.setFont(newFont); - painter.drawText(0, top, mLineNumberArea->width(), fontMetrics().height(), - Qt::AlignRight, number); + painter.drawText(0, top, mLineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number); painter.setFont(font); } block = block.next(); top = bottom; - bottom = top + (int) blockBoundingRect(block).height(); + bottom = top + (int)blockBoundingRect(block).height(); ++blockNumber; } } -CSVWorld::LineNumberArea::LineNumberArea(ScriptEdit *editor) : QWidget(editor), mScriptEdit(editor) -{} +CSVWorld::LineNumberArea::LineNumberArea(ScriptEdit* editor) + : QWidget(editor) + , mScriptEdit(editor) +{ +} QSize CSVWorld::LineNumberArea::sizeHint() const { return QSize(mScriptEdit->lineNumberAreaWidth(), 0); } -void CSVWorld::LineNumberArea::paintEvent(QPaintEvent *event) +void CSVWorld::LineNumberArea::paintEvent(QPaintEvent* event) { mScriptEdit->lineNumberAreaPaintEvent(event); } diff --git a/apps/opencs/view/world/scriptedit.hpp b/apps/opencs/view/world/scriptedit.hpp index 21fabee5875..53fa88ced32 100644 --- a/apps/opencs/view/world/scriptedit.hpp +++ b/apps/opencs/view/world/scriptedit.hpp @@ -1,18 +1,34 @@ #ifndef SCRIPTEDIT_H #define SCRIPTEDIT_H +#include #include -#include -#include +#include #include -#include -#include +#include +#include + +#include #include "../../model/world/universalid.hpp" #include "scripthighlighter.hpp" -class QRegExp; +class QAction; +class QContextMenuEvent; +class QDragEnterEvent; +class QDragMoveEvent; +class QDropEvent; +class QEvent; +class QObject; +class QPaintEvent; +class QRect; +class QResizeEvent; + +namespace CSMPrefs +{ + class Setting; +} namespace CSMDoc { @@ -26,118 +42,107 @@ namespace CSVWorld /// \brief Editor for scripts. class ScriptEdit : public QPlainTextEdit { - Q_OBJECT - - public: + Q_OBJECT - class ChangeLock - { - ScriptEdit& mEdit; + public: + class ChangeLock + { + ScriptEdit& mEdit; - ChangeLock (const ChangeLock&); - ChangeLock& operator= (const ChangeLock&); + ChangeLock(const ChangeLock&); + ChangeLock& operator=(const ChangeLock&); - public: - - ChangeLock (ScriptEdit& edit); - ~ChangeLock(); - }; - - friend class ChangeLock; - - private: - - int mChangeLocked; - ScriptHighlighter *mHighlighter; - QTimer mUpdateTimer; - bool mShowLineNum; - LineNumberArea *mLineNumberArea; - QFont mDefaultFont; - QFont mMonoFont; - int mTabCharCount; - bool mMarkOccurrences; - QAction *mCommentAction; - QAction *mUncommentAction; - - protected: + public: + ChangeLock(ScriptEdit& edit); + ~ChangeLock(); + }; - bool event (QEvent *event) override; + friend class ChangeLock; - public: + private: + int mChangeLocked; + ScriptHighlighter* mHighlighter; + QTimer mUpdateTimer; + bool mShowLineNum; + LineNumberArea* mLineNumberArea; + QFont mDefaultFont; + QFont mMonoFont; + int mTabCharCount; + bool mMarkOccurrences; + QAction* mCommentAction; + QAction* mUncommentAction; - ScriptEdit (const CSMDoc::Document& document, ScriptHighlighter::Mode mode, - QWidget* parent); + protected: + bool event(QEvent* event) override; - /// Should changes to the data be ignored (i.e. not cause updated)? - /// - /// \note This mechanism is used to avoid infinite update recursions - bool isChangeLocked() const; + public: + ScriptEdit(const CSMDoc::Document& document, ScriptHighlighter::Mode mode, QWidget* parent); - void lineNumberAreaPaintEvent(QPaintEvent *event); - int lineNumberAreaWidth(); - void showLineNum(bool show); + /// Should changes to the data be ignored (i.e. not cause updated)? + /// + /// \note This mechanism is used to avoid infinite update recursions + bool isChangeLocked() const; - protected: + void lineNumberAreaPaintEvent(QPaintEvent* event); + int lineNumberAreaWidth(); + void showLineNum(bool show); - void resizeEvent(QResizeEvent *e) override; + protected: + void resizeEvent(QResizeEvent* e) override; - void contextMenuEvent(QContextMenuEvent *event) override; + void contextMenuEvent(QContextMenuEvent* event) override; - private: + private: + QVector mAllowedTypes; + const CSMDoc::Document& mDocument; + const QRegularExpression mWhiteListQuotes; - QVector mAllowedTypes; - const CSMDoc::Document& mDocument; - const QRegExp mWhiteListQoutes; + void dragEnterEvent(QDragEnterEvent* event) override; - void dragEnterEvent (QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; - void dropEvent (QDropEvent* event) override; + void dragMoveEvent(QDragMoveEvent* event) override; - void dragMoveEvent (QDragMoveEvent* event) override; + bool stringNeedsQuote(const std::string& id) const; - bool stringNeedsQuote(const std::string& id) const; + /// \brief Set tab width for script editor. + void setTabWidth(); - /// \brief Set tab width for script editor. - void setTabWidth(); + /// \brief Turn line wrapping in script editor on or off. + /// \param wrap Whether or not to wrap lines. + void wrapLines(bool wrap); - /// \brief Turn line wrapping in script editor on or off. - /// \param wrap Whether or not to wrap lines. - void wrapLines(bool wrap); + private slots: - private slots: + /// \brief Update editor when related setting is changed. + /// \param setting Setting that was changed. + void settingChanged(const CSMPrefs::Setting* setting); - /// \brief Update editor when related setting is changed. - /// \param setting Setting that was changed. - void settingChanged(const CSMPrefs::Setting *setting); + void idListChanged(); - void idListChanged(); + void updateHighlighting(); - void updateHighlighting(); + void updateLineNumberAreaWidth(int newBlockCount); - void updateLineNumberAreaWidth(int newBlockCount); + void updateLineNumberArea(const QRect&, int); - void updateLineNumberArea(const QRect &, int); + void markOccurrences(); - void markOccurrences(); - - void commentSelection(); + void commentSelection(); - void uncommentSelection(); - + void uncommentSelection(); }; class LineNumberArea : public QWidget { - ScriptEdit *mScriptEdit; - - public: - - LineNumberArea(ScriptEdit *editor); - QSize sizeHint() const override; + ScriptEdit* mScriptEdit; - protected: + public: + LineNumberArea(ScriptEdit* editor); + QSize sizeHint() const override; - void paintEvent(QPaintEvent *event) override; + protected: + void paintEvent(QPaintEvent* event) override; }; } #endif // SCRIPTEDIT_H diff --git a/apps/opencs/view/world/scripterrortable.cpp b/apps/opencs/view/world/scripterrortable.cpp index 45809b28c69..4bd8c3f3900 100644 --- a/apps/opencs/view/world/scripterrortable.cpp +++ b/apps/opencs/view/world/scripterrortable.cpp @@ -1,119 +1,131 @@ #include "scripterrortable.hpp" +#include +#include + #include -#include -#include -#include +#include +#include +#include +#include + #include #include +#include +#include +#include #include "../../model/doc/document.hpp" #include "../../model/prefs/state.hpp" -void CSVWorld::ScriptErrorTable::report (const std::string& message, const Compiler::TokenLoc& loc, Type type) +void CSVWorld::ScriptErrorTable::report(const std::string& message, const Compiler::TokenLoc& loc, Type type) { std::ostringstream stream; stream << message << " (" << loc.mLiteral << ")"; - addMessage (stream.str(), type==Compiler::ErrorHandler::WarningMessage ? - CSMDoc::Message::Severity_Warning : CSMDoc::Message::Severity_Error, - loc.mLine, loc.mColumn-loc.mLiteral.length()); + addMessage(stream.str(), + type == Compiler::ErrorHandler::WarningMessage ? CSMDoc::Message::Severity_Warning + : CSMDoc::Message::Severity_Error, + loc.mLine, loc.mColumn - loc.mLiteral.length()); } -void CSVWorld::ScriptErrorTable::report (const std::string& message, Type type) +void CSVWorld::ScriptErrorTable::report(const std::string& message, Type type) { - addMessage (message, type==Compiler::ErrorHandler::WarningMessage ? - CSMDoc::Message::Severity_Warning : CSMDoc::Message::Severity_Error); + addMessage(message, + type == Compiler::ErrorHandler::WarningMessage ? CSMDoc::Message::Severity_Warning + : CSMDoc::Message::Severity_Error); } -void CSVWorld::ScriptErrorTable::addMessage (const std::string& message, - CSMDoc::Message::Severity severity, int line, int column) +void CSVWorld::ScriptErrorTable::addMessage( + const std::string& message, CSMDoc::Message::Severity severity, int line, int column) { int row = rowCount(); - setRowCount (row+1); + setRowCount(row + 1); - QTableWidgetItem *severityItem = new QTableWidgetItem ( - QString::fromUtf8 (CSMDoc::Message::toString (severity).c_str())); - severityItem->setFlags (severityItem->flags() ^ Qt::ItemIsEditable); - setItem (row, 0, severityItem); + QTableWidgetItem* severityItem + = new QTableWidgetItem(QString::fromUtf8(CSMDoc::Message::toString(severity).c_str())); + severityItem->setFlags(severityItem->flags() ^ Qt::ItemIsEditable); + setItem(row, 0, severityItem); - if (line!=-1) + if (line != -1) { - QTableWidgetItem *lineItem = new QTableWidgetItem; - lineItem->setData (Qt::DisplayRole, line+1); - lineItem->setFlags (lineItem->flags() ^ Qt::ItemIsEditable); - setItem (row, 1, lineItem); - - QTableWidgetItem *columnItem = new QTableWidgetItem; - columnItem->setData (Qt::DisplayRole, column); - columnItem->setFlags (columnItem->flags() ^ Qt::ItemIsEditable); - setItem (row, 3, columnItem); + QTableWidgetItem* lineItem = new QTableWidgetItem; + lineItem->setData(Qt::DisplayRole, line + 1); + lineItem->setFlags(lineItem->flags() ^ Qt::ItemIsEditable); + setItem(row, 1, lineItem); + + QTableWidgetItem* columnItem = new QTableWidgetItem; + columnItem->setData(Qt::DisplayRole, column); + columnItem->setFlags(columnItem->flags() ^ Qt::ItemIsEditable); + setItem(row, 3, columnItem); } else { - QTableWidgetItem *lineItem = new QTableWidgetItem; - lineItem->setData (Qt::DisplayRole, "-"); - lineItem->setFlags (lineItem->flags() ^ Qt::ItemIsEditable); - setItem (row, 1, lineItem); + QTableWidgetItem* lineItem = new QTableWidgetItem; + lineItem->setData(Qt::DisplayRole, "-"); + lineItem->setFlags(lineItem->flags() ^ Qt::ItemIsEditable); + setItem(row, 1, lineItem); } - QTableWidgetItem *messageItem = new QTableWidgetItem (QString::fromUtf8 (message.c_str())); - messageItem->setFlags (messageItem->flags() ^ Qt::ItemIsEditable); - setItem (row, 2, messageItem); + QTableWidgetItem* messageItem = new QTableWidgetItem(QString::fromUtf8(message.c_str())); + messageItem->setFlags(messageItem->flags() ^ Qt::ItemIsEditable); + setItem(row, 2, messageItem); } -void CSVWorld::ScriptErrorTable::setWarningsMode (const std::string& value) +void CSVWorld::ScriptErrorTable::setWarningsMode(const std::string& value) { - if (value=="Ignore") - Compiler::ErrorHandler::setWarningsMode (0); - else if (value=="Normal") - Compiler::ErrorHandler::setWarningsMode (1); - else if (value=="Strict") - Compiler::ErrorHandler::setWarningsMode (2); + if (value == "Ignore") + Compiler::ErrorHandler::setWarningsMode(0); + else if (value == "Normal") + Compiler::ErrorHandler::setWarningsMode(1); + else if (value == "Strict") + Compiler::ErrorHandler::setWarningsMode(2); } -CSVWorld::ScriptErrorTable::ScriptErrorTable (const CSMDoc::Document& document, QWidget *parent) -: QTableWidget (parent), mContext (document.getData()) +CSVWorld::ScriptErrorTable::ScriptErrorTable(const CSMDoc::Document& document, QWidget* parent) + : QTableWidget(parent) + , mContext(document.getData()) { - setColumnCount (4); + setColumnCount(4); QStringList headers; - headers << "Severity" << "Line" << "Description"; - setHorizontalHeaderLabels (headers); - horizontalHeader()->setSectionResizeMode (0, QHeaderView::ResizeToContents); - horizontalHeader()->setSectionResizeMode (1, QHeaderView::ResizeToContents); - horizontalHeader()->setStretchLastSection (true); + headers << "Severity" + << "Line" + << "Description"; + setHorizontalHeaderLabels(headers); + horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + horizontalHeader()->setStretchLastSection(true); verticalHeader()->hide(); - setColumnHidden (3, true); + setColumnHidden(3, true); - setSelectionMode (QAbstractItemView::NoSelection); + setSelectionMode(QAbstractItemView::NoSelection); - Compiler::registerExtensions (mExtensions); - mContext.setExtensions (&mExtensions); + Compiler::registerExtensions(mExtensions); + mContext.setExtensions(&mExtensions); - connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), - this, SLOT (settingChanged (const CSMPrefs::Setting *))); + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &ScriptErrorTable::settingChanged); CSMPrefs::get()["Scripts"].update(); - connect (this, SIGNAL (cellClicked (int, int)), this, SLOT (cellClicked (int, int))); + connect(this, &QTableWidget::cellClicked, this, &ScriptErrorTable::cellClicked); } -void CSVWorld::ScriptErrorTable::update (const std::string& source) +void CSVWorld::ScriptErrorTable::update(const std::string& source) { clear(); try { - std::istringstream input (source); + std::istringstream input(source); - Compiler::Scanner scanner (*this, input, mContext.getExtensions()); + Compiler::Scanner scanner(*this, input, mContext.getExtensions()); - Compiler::FileParser parser (*this, mContext); + Compiler::FileParser parser(*this, mContext); - scanner.scan (parser); + scanner.scan(parser); } catch (const Compiler::SourceException&) { @@ -121,32 +133,32 @@ void CSVWorld::ScriptErrorTable::update (const std::string& source) } catch (const std::exception& error) { - addMessage (error.what(), CSMDoc::Message::Severity_SeriousError); + addMessage(error.what(), CSMDoc::Message::Severity_SeriousError); } } void CSVWorld::ScriptErrorTable::clear() { - setRowCount (0); + setRowCount(0); } -bool CSVWorld::ScriptErrorTable::clearLocals (const std::string& script) +bool CSVWorld::ScriptErrorTable::clearLocals(const std::string& script) { - return mContext.clearLocals (script); + return mContext.clearLocals(script); } -void CSVWorld::ScriptErrorTable::settingChanged (const CSMPrefs::Setting *setting) +void CSVWorld::ScriptErrorTable::settingChanged(const CSMPrefs::Setting* setting) { - if (*setting=="Scripts/warnings") - setWarningsMode (setting->toString()); + if (*setting == "Scripts/warnings") + setWarningsMode(setting->toString()); } -void CSVWorld::ScriptErrorTable::cellClicked (int row, int column) +void CSVWorld::ScriptErrorTable::cellClicked(int row, int column) { - if (item (row, 3)) + if (item(row, 3)) { - int scriptLine = item (row, 1)->data (Qt::DisplayRole).toInt(); - int scriptColumn = item (row, 3)->data (Qt::DisplayRole).toInt(); - emit highlightError (scriptLine-1, scriptColumn); + int scriptLine = item(row, 1)->data(Qt::DisplayRole).toInt(); + int scriptColumn = item(row, 3)->data(Qt::DisplayRole).toInt(); + emit highlightError(scriptLine - 1, scriptColumn); } } diff --git a/apps/opencs/view/world/scripterrortable.hpp b/apps/opencs/view/world/scripterrortable.hpp index 7165d0fc6a8..d05449a4f58 100644 --- a/apps/opencs/view/world/scripterrortable.hpp +++ b/apps/opencs/view/world/scripterrortable.hpp @@ -3,11 +3,18 @@ #include +#include + #include #include -#include "../../model/world/scriptcontext.hpp" #include "../../model/doc/messages.hpp" +#include "../../model/world/scriptcontext.hpp" + +namespace Compiler +{ + struct TokenLoc; +} namespace CSMDoc { @@ -23,44 +30,42 @@ namespace CSVWorld { class ScriptErrorTable : public QTableWidget, private Compiler::ErrorHandler { - Q_OBJECT - - Compiler::Extensions mExtensions; - CSMWorld::ScriptContext mContext; + Q_OBJECT - void report (const std::string& message, const Compiler::TokenLoc& loc, Type type) override; - ///< Report error to the user. + Compiler::Extensions mExtensions; + CSMWorld::ScriptContext mContext; - void report (const std::string& message, Type type) override; - ///< Report a file related error + void report(const std::string& message, const Compiler::TokenLoc& loc, Type type) override; + ///< Report error to the user. - void addMessage (const std::string& message, CSMDoc::Message::Severity severity, - int line = -1, int column = -1); + void report(const std::string& message, Type type) override; + ///< Report a file related error - void setWarningsMode (const std::string& value); + void addMessage(const std::string& message, CSMDoc::Message::Severity severity, int line = -1, int column = -1); - public: + void setWarningsMode(const std::string& value); - ScriptErrorTable (const CSMDoc::Document& document, QWidget *parent = nullptr); + public: + ScriptErrorTable(const CSMDoc::Document& document, QWidget* parent = nullptr); - void update (const std::string& source); + void update(const std::string& source); - void clear(); + void clear(); - /// Clear local variable cache for \a script. - /// - /// \return Were there any locals that needed clearing? - bool clearLocals (const std::string& script); + /// Clear local variable cache for \a script. + /// + /// \return Were there any locals that needed clearing? + bool clearLocals(const std::string& script); - private slots: + private slots: - void settingChanged (const CSMPrefs::Setting *setting); + void settingChanged(const CSMPrefs::Setting* setting); - void cellClicked (int row, int column); + void cellClicked(int row, int column); - signals: + signals: - void highlightError (int line, int column); + void highlightError(int line, int column); }; } diff --git a/apps/opencs/view/world/scripthighlighter.cpp b/apps/opencs/view/world/scripthighlighter.cpp index 147beb82a88..46154129510 100644 --- a/apps/opencs/view/world/scripthighlighter.cpp +++ b/apps/opencs/view/world/scripthighlighter.cpp @@ -1,74 +1,80 @@ #include "scripthighlighter.hpp" #include +#include -#include +#include +#include +#include #include +#include +#include +#include -#include "../../model/prefs/setting.hpp" -#include "../../model/prefs/category.hpp" +class QTextDocument; -bool CSVWorld::ScriptHighlighter::parseInt (int value, const Compiler::TokenLoc& loc, - Compiler::Scanner& scanner) +namespace CSMWorld { - highlight (loc, Type_Int); + class Data; +} + +bool CSVWorld::ScriptHighlighter::parseInt(int value, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) +{ + highlight(loc, Type_Int); return true; } -bool CSVWorld::ScriptHighlighter::parseFloat (float value, const Compiler::TokenLoc& loc, - Compiler::Scanner& scanner) +bool CSVWorld::ScriptHighlighter::parseFloat(float value, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { - highlight (loc, Type_Float); + highlight(loc, Type_Float); return true; } -bool CSVWorld::ScriptHighlighter::parseName (const std::string& name, const Compiler::TokenLoc& loc, - Compiler::Scanner& scanner) +bool CSVWorld::ScriptHighlighter::parseName( + const std::string& name, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { - highlight (loc, mContext.isId (name) ? Type_Id : Type_Name); + highlight(loc, mContext.isId(ESM::RefId::stringRefId(name)) ? Type_Id : Type_Name); return true; } -bool CSVWorld::ScriptHighlighter::parseKeyword (int keyword, const Compiler::TokenLoc& loc, - Compiler::Scanner& scanner) +bool CSVWorld::ScriptHighlighter::parseKeyword(int keyword, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { - if (((mMode==Mode_Console || mMode==Mode_Dialogue) && - (keyword==Compiler::Scanner::K_begin || keyword==Compiler::Scanner::K_end || - keyword==Compiler::Scanner::K_short || keyword==Compiler::Scanner::K_long || - keyword==Compiler::Scanner::K_float)) - || (mMode==Mode_Console && (keyword==Compiler::Scanner::K_if || - keyword==Compiler::Scanner::K_endif || keyword==Compiler::Scanner::K_else || - keyword==Compiler::Scanner::K_elseif || keyword==Compiler::Scanner::K_while || - keyword==Compiler::Scanner::K_endwhile))) - return parseName (loc.mLiteral, loc, scanner); - - highlight (loc, Type_Keyword); + if (((mMode == Mode_Console || mMode == Mode_Dialogue) + && (keyword == Compiler::Scanner::K_begin || keyword == Compiler::Scanner::K_end + || keyword == Compiler::Scanner::K_short || keyword == Compiler::Scanner::K_long + || keyword == Compiler::Scanner::K_float)) + || (mMode == Mode_Console + && (keyword == Compiler::Scanner::K_if || keyword == Compiler::Scanner::K_endif + || keyword == Compiler::Scanner::K_else || keyword == Compiler::Scanner::K_elseif + || keyword == Compiler::Scanner::K_while || keyword == Compiler::Scanner::K_endwhile))) + return parseName(loc.mLiteral, loc, scanner); + + highlight(loc, Type_Keyword); return true; } -bool CSVWorld::ScriptHighlighter::parseSpecial (int code, const Compiler::TokenLoc& loc, - Compiler::Scanner& scanner) +bool CSVWorld::ScriptHighlighter::parseSpecial(int code, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { - highlight (loc, Type_Special); + highlight(loc, Type_Special); return true; } -bool CSVWorld::ScriptHighlighter::parseComment (const std::string& comment, - const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) +bool CSVWorld::ScriptHighlighter::parseComment( + const std::string& comment, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { - highlight (loc, Type_Comment); + highlight(loc, Type_Comment); return true; } -void CSVWorld::ScriptHighlighter::parseEOF (Compiler::Scanner& scanner) -{} +void CSVWorld::ScriptHighlighter::parseEOF(Compiler::Scanner& scanner) {} -void CSVWorld::ScriptHighlighter::highlight (const Compiler::TokenLoc& loc, Type type) +void CSVWorld::ScriptHighlighter::highlight(const Compiler::TokenLoc& loc, Type type) { // We should take in account multibyte characters int length = 0; const char* token = loc.mLiteral.c_str(); - while (*token) length += (*token++ & 0xc0) != 0x80; + while (*token) + length += (*token++ & 0xc0) != 0x80; int index = loc.mColumn; @@ -79,40 +85,41 @@ void CSVWorld::ScriptHighlighter::highlight (const Compiler::TokenLoc& loc, Type if (mMarkOccurrences && type == Type_Name && loc.mLiteral == mMarkedWord) scheme.merge(mScheme[Type_Highlight]); - setFormat (index, length, scheme); + setFormat(index, length, scheme); } -CSVWorld::ScriptHighlighter::ScriptHighlighter (const CSMWorld::Data& data, Mode mode, - QTextDocument *parent) - : QSyntaxHighlighter (parent) - , Compiler::Parser (mErrorHandler, mContext) - , mContext (data) - , mMode (mode) - , mMarkOccurrences (false) +CSVWorld::ScriptHighlighter::ScriptHighlighter(const CSMWorld::Data& data, Mode mode, QTextDocument* parent) + : QSyntaxHighlighter(parent) + , Compiler::Parser(mErrorHandler, mContext) + , mContext(data) + , mMode(mode) + , mMarkOccurrences(false) { - QColor color ("black"); + QColor color("black"); QTextCharFormat format; - format.setForeground (color); + format.setForeground(color); - for (int i=0; i<=Type_Id; ++i) - mScheme.insert (std::make_pair (static_cast (i), format)); + for (int i = 0; i <= Type_Id; ++i) + mScheme.insert(std::make_pair(static_cast(i), format)); // configure compiler - Compiler::registerExtensions (mExtensions); - mContext.setExtensions (&mExtensions); + Compiler::registerExtensions(mExtensions); + mContext.setExtensions(&mExtensions); } -void CSVWorld::ScriptHighlighter::highlightBlock (const QString& text) +void CSVWorld::ScriptHighlighter::highlightBlock(const QString& text) { - std::istringstream stream (text.toUtf8().constData()); + std::istringstream stream(text.toUtf8().constData()); - Compiler::Scanner scanner (mErrorHandler, stream, mContext.getExtensions()); + Compiler::Scanner scanner(mErrorHandler, stream, mContext.getExtensions()); try { - scanner.scan (*this); + scanner.scan(*this); } - catch (...) {} // ignore syntax errors + catch (...) + { + } // ignore syntax errors } void CSVWorld::ScriptHighlighter::setMarkOccurrences(bool flag) @@ -130,26 +137,22 @@ void CSVWorld::ScriptHighlighter::invalidateIds() mContext.invalidateIds(); } -bool CSVWorld::ScriptHighlighter::settingChanged (const CSMPrefs::Setting *setting) +bool CSVWorld::ScriptHighlighter::settingChanged(const CSMPrefs::Setting* setting) { - if (setting->getParent()->getKey()=="Scripts") + if (setting->getParent()->getKey() == "Scripts") { - static const char *const colours[Type_Id+2] = - { - "colour-int", "colour-float", "colour-name", "colour-keyword", - "colour-special", "colour-comment", "colour-highlight", "colour-id", - 0 - }; - - for (int i=0; colours[i]; ++i) - if (setting->getKey()==colours[i]) + static const char* const colours[Type_Id + 2] = { "colour-int", "colour-float", "colour-name", "colour-keyword", + "colour-special", "colour-comment", "colour-highlight", "colour-id", 0 }; + + for (int i = 0; colours[i]; ++i) + if (setting->getKey() == colours[i]) { QTextCharFormat format; if (i == Type_Highlight) - format.setBackground (setting->toColor()); + format.setBackground(setting->toColor()); else - format.setForeground (setting->toColor()); - mScheme[static_cast (i)] = format; + format.setForeground(setting->toColor()); + mScheme[static_cast(i)] = format; return true; } } diff --git a/apps/opencs/view/world/scripthighlighter.hpp b/apps/opencs/view/world/scripthighlighter.hpp index 9b4a5b7be08..517db522442 100644 --- a/apps/opencs/view/world/scripthighlighter.hpp +++ b/apps/opencs/view/world/scripthighlighter.hpp @@ -4,14 +4,29 @@ #include #include +#include #include +#include +#include #include #include -#include #include "../../model/world/scriptcontext.hpp" +class QTextDocument; + +namespace CSMWorld +{ + class Data; +} + +namespace Compiler +{ + class Scanner; + struct TokenLoc; +} + namespace CSMPrefs { class Setting; @@ -21,87 +36,78 @@ namespace CSVWorld { class ScriptHighlighter : public QSyntaxHighlighter, private Compiler::Parser { - public: - - enum Type - { - Type_Int = 0, - Type_Float = 1, - Type_Name = 2, - Type_Keyword = 3, - Type_Special = 4, - Type_Comment = 5, - Type_Highlight = 6, - Type_Id = 7 - }; - - enum Mode - { - Mode_General, - Mode_Console, - Mode_Dialogue - }; - - private: - - Compiler::NullErrorHandler mErrorHandler; - Compiler::Extensions mExtensions; - CSMWorld::ScriptContext mContext; - std::map mScheme; - Mode mMode; - bool mMarkOccurrences; - std::string mMarkedWord; - - private: - - bool parseInt (int value, const Compiler::TokenLoc& loc, - Compiler::Scanner& scanner) override; - ///< Handle an int token. - /// \return fetch another token? - - bool parseFloat (float value, const Compiler::TokenLoc& loc, - Compiler::Scanner& scanner) override; - ///< Handle a float token. - /// \return fetch another token? - - bool parseName (const std::string& name, - const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; - ///< Handle a name token. - /// \return fetch another token? - - bool parseKeyword (int keyword, const Compiler::TokenLoc& loc, - Compiler::Scanner& scanner) override; - ///< Handle a keyword token. - /// \return fetch another token? - - bool parseSpecial (int code, const Compiler::TokenLoc& loc, - Compiler::Scanner& scanner) override; - ///< Handle a special character token. - /// \return fetch another token? - - bool parseComment (const std::string& comment, const Compiler::TokenLoc& loc, - Compiler::Scanner& scanner) override; - ///< Handle comment token. - /// \return fetch another token? - - void parseEOF (Compiler::Scanner& scanner) override; - ///< Handle EOF token. - - void highlight (const Compiler::TokenLoc& loc, Type type); - - public: - - ScriptHighlighter (const CSMWorld::Data& data, Mode mode, QTextDocument *parent); - - void highlightBlock (const QString& text) override; - - void setMarkOccurrences(bool); - - void setMarkedWord(const std::string& name); - - void invalidateIds(); - - bool settingChanged (const CSMPrefs::Setting *setting); + public: + enum Type + { + Type_Int = 0, + Type_Float = 1, + Type_Name = 2, + Type_Keyword = 3, + Type_Special = 4, + Type_Comment = 5, + Type_Highlight = 6, + Type_Id = 7 + }; + + enum Mode + { + Mode_General, + Mode_Console, + Mode_Dialogue + }; + + private: + Compiler::NullErrorHandler mErrorHandler; + Compiler::Extensions mExtensions; + CSMWorld::ScriptContext mContext; + std::map mScheme; + Mode mMode; + bool mMarkOccurrences; + std::string mMarkedWord; + + private: + bool parseInt(int value, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; + ///< Handle an int token. + /// \return fetch another token? + + bool parseFloat(float value, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; + ///< Handle a float token. + /// \return fetch another token? + + bool parseName(const std::string& name, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; + ///< Handle a name token. + /// \return fetch another token? + + bool parseKeyword(int keyword, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; + ///< Handle a keyword token. + /// \return fetch another token? + + bool parseSpecial(int code, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; + ///< Handle a special character token. + /// \return fetch another token? + + bool parseComment( + const std::string& comment, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) override; + ///< Handle comment token. + /// \return fetch another token? + + void parseEOF(Compiler::Scanner& scanner) override; + ///< Handle EOF token. + + void highlight(const Compiler::TokenLoc& loc, Type type); + + public: + ScriptHighlighter(const CSMWorld::Data& data, Mode mode, QTextDocument* parent); + + void highlightBlock(const QString& text) override; + + void setMarkOccurrences(bool); + + void setMarkedWord(const std::string& name); + + void invalidateIds(); + + bool settingChanged(const CSMPrefs::Setting* setting); }; } diff --git a/apps/opencs/view/world/scriptsubview.cpp b/apps/opencs/view/world/scriptsubview.cpp index 6eab7aaa531..aa6890904d7 100644 --- a/apps/opencs/view/world/scriptsubview.cpp +++ b/apps/opencs/view/world/scriptsubview.cpp @@ -1,52 +1,59 @@ #include "scriptsubview.hpp" -#include +#include +#include -#include -#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include + #include #include "../../model/doc/document.hpp" -#include "../../model/world/universalid.hpp" -#include "../../model/world/data.hpp" +#include "../../model/prefs/state.hpp" #include "../../model/world/commands.hpp" +#include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" -#include "../../model/prefs/state.hpp" +#include "../../model/world/universalid.hpp" -#include "scriptedit.hpp" -#include "recordbuttonbar.hpp" -#include "tablebottombox.hpp" #include "genericcreator.hpp" +#include "recordbuttonbar.hpp" +#include "scriptedit.hpp" #include "scripterrortable.hpp" +#include "tablebottombox.hpp" void CSVWorld::ScriptSubView::addButtonBar() { if (mButtons) return; - mButtons = new RecordButtonBar (getUniversalId(), *mModel, mBottom, &mCommandDispatcher, this); + mButtons = new RecordButtonBar(getUniversalId(), *mModel, mBottom, &mCommandDispatcher, this); - mLayout.insertWidget (1, mButtons); + mLayout.insertWidget(1, mButtons); - connect (mButtons, SIGNAL (switchToRow (int)), this, SLOT (switchToRow (int))); + connect(mButtons, &RecordButtonBar::switchToRow, this, &ScriptSubView::switchToRow); - connect (this, SIGNAL (universalIdChanged (const CSMWorld::UniversalId&)), - mButtons, SLOT (universalIdChanged (const CSMWorld::UniversalId&))); + connect(this, &ScriptSubView::universalIdChanged, mButtons, &RecordButtonBar::universalIdChanged); } void CSVWorld::ScriptSubView::recompile() { if (!mCompileDelay->isActive() && !isDeleted()) - mCompileDelay->start (CSMPrefs::get()["Scripts"]["compile-delay"].toInt()); + mCompileDelay->start(CSMPrefs::get()["Scripts"]["compile-delay"].toInt()); } bool CSVWorld::ScriptSubView::isDeleted() const { - return mModel->data (mModel->getModelIndex (getUniversalId().getId(), mStateColumn)).toInt() - ==CSMWorld::RecordBase::State_Deleted; + return mModel->data(mModel->getModelIndex(getUniversalId().getId(), mStateColumn)).toInt() + == CSMWorld::RecordBase::State_Deleted; } void CSVWorld::ScriptSubView::updateDeletedState() @@ -55,11 +62,11 @@ void CSVWorld::ScriptSubView::updateDeletedState() { mErrors->clear(); adjustSplitter(); - mEditor->setEnabled (false); + mEditor->setEnabled(false); } else { - mEditor->setEnabled (true); + mEditor->setEnabled(true); recompile(); } } @@ -73,7 +80,7 @@ void CSVWorld::ScriptSubView::adjustSplitter() if (mErrors->height()) return; // keep old height if the error panel was already open - sizes << (mMain->height()-mErrorHeight-mMain->handleWidth()) << mErrorHeight; + sizes << (mMain->height() - mErrorHeight - mMain->handleWidth()) << mErrorHeight; } else { @@ -83,90 +90,88 @@ void CSVWorld::ScriptSubView::adjustSplitter() sizes << 1 << 0; } - mMain->setSizes (sizes); + mMain->setSizes(sizes); } -CSVWorld::ScriptSubView::ScriptSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: SubView (id), mDocument (document), mColumn (-1), mBottom(nullptr), mButtons (nullptr), - mCommandDispatcher (document, CSMWorld::UniversalId::getParentType (id.getType())), - mErrorHeight (CSMPrefs::get()["Scripts"]["error-height"].toInt()) +CSVWorld::ScriptSubView::ScriptSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) + : SubView(id) + , mDocument(document) + , mColumn(-1) + , mBottom(nullptr) + , mButtons(nullptr) + , mCommandDispatcher(document, CSMWorld::UniversalId::getParentType(id.getType())) + , mErrorHeight(CSMPrefs::get()["Scripts"]["error-height"].toInt()) { - std::vector selection (1, id.getId()); - mCommandDispatcher.setSelection (selection); + std::vector selection(1, id.getId()); + mCommandDispatcher.setSelection(selection); - mMain = new QSplitter (this); - mMain->setOrientation (Qt::Vertical); - mLayout.addWidget (mMain, 2); + mMain = new QSplitter(this); + mMain->setOrientation(Qt::Vertical); + mLayout.addWidget(mMain, 2); - mEditor = new ScriptEdit (mDocument, ScriptHighlighter::Mode_General, this); - mMain->addWidget (mEditor); - mMain->setCollapsible (0, false); + mEditor = new ScriptEdit(mDocument, ScriptHighlighter::Mode_General, this); + mMain->addWidget(mEditor); + mMain->setCollapsible(0, false); - mErrors = new ScriptErrorTable (document, this); - mMain->addWidget (mErrors); + mErrors = new ScriptErrorTable(document, this); + mMain->addWidget(mErrors); QList sizes; sizes << 1 << 0; - mMain->setSizes (sizes); + mMain->setSizes(sizes); - QWidget *widget = new QWidget (this);; - widget->setLayout (&mLayout); - setWidget (widget); + QWidget* widget = new QWidget(this); + widget->setLayout(&mLayout); + setWidget(widget); - mModel = &dynamic_cast ( - *document.getData().getTableModel (CSMWorld::UniversalId::Type_Scripts)); + mModel = &dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Scripts)); - mColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_ScriptText); - mIdColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); - mStateColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Modification); + mColumn = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_ScriptText); + mIdColumn = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); + mStateColumn = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Modification); - QString source = mModel->data (mModel->getModelIndex (id.getId(), mColumn)).toString(); + QString source = mModel->data(mModel->getModelIndex(id.getId(), mColumn)).toString(); - mEditor->setPlainText (source); + mEditor->setPlainText(source); // bottom box and buttons - mBottom = new TableBottomBox (CreatorFactory(), document, id, this); + mBottom = new TableBottomBox(CreatorFactory(), document, id, this); - connect (mBottom, SIGNAL (requestFocus (const std::string&)), - this, SLOT (switchToId (const std::string&))); + connect(mBottom, &TableBottomBox::requestFocus, this, &ScriptSubView::switchToId); - mLayout.addWidget (mBottom); + mLayout.addWidget(mBottom); // signals - connect (mEditor, SIGNAL (textChanged()), this, SLOT (textChanged())); + connect(mEditor, &ScriptEdit::textChanged, this, &ScriptSubView::textChanged); - connect (mModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (dataChanged (const QModelIndex&, const QModelIndex&))); + connect(mModel, &CSMWorld::IdTable::dataChanged, this, &ScriptSubView::dataChanged); - connect (mModel, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (rowsAboutToBeRemoved (const QModelIndex&, int, int))); + connect(mModel, &CSMWorld::IdTable::rowsAboutToBeRemoved, this, &ScriptSubView::rowsAboutToBeRemoved); updateStatusBar(); - connect(mEditor, SIGNAL(cursorPositionChanged()), this, SLOT(updateStatusBar())); + connect(mEditor, &ScriptEdit::cursorPositionChanged, this, &ScriptSubView::updateStatusBar); - mErrors->update (source.toUtf8().constData()); + mErrors->update(source.toUtf8().constData()); - connect (mErrors, SIGNAL (highlightError (int, int)), - this, SLOT (highlightError (int, int))); + connect(mErrors, &ScriptErrorTable::highlightError, this, &ScriptSubView::highlightError); - mCompileDelay = new QTimer (this); - mCompileDelay->setSingleShot (true); - connect (mCompileDelay, SIGNAL (timeout()), this, SLOT (updateRequest())); + mCompileDelay = new QTimer(this); + mCompileDelay->setSingleShot(true); + connect(mCompileDelay, &QTimer::timeout, this, &ScriptSubView::updateRequest); updateDeletedState(); - connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), - this, SLOT (settingChanged (const CSMPrefs::Setting *))); + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &ScriptSubView::settingChanged); CSMPrefs::get()["Scripts"].update(); } -void CSVWorld::ScriptSubView::setStatusBar (bool show) +void CSVWorld::ScriptSubView::setStatusBar(bool show) { - mBottom->setStatusBar (show); + mBottom->setStatusBar(show); } -void CSVWorld::ScriptSubView::settingChanged (const CSMPrefs::Setting *setting) +void CSVWorld::ScriptSubView::settingChanged(const CSMPrefs::Setting* setting) { - if (*setting=="Scripts/toolbar") + if (*setting == "Scripts/toolbar") { if (setting->isTrue()) { @@ -174,67 +179,67 @@ void CSVWorld::ScriptSubView::settingChanged (const CSMPrefs::Setting *setting) } else if (mButtons) { - mLayout.removeWidget (mButtons); + mLayout.removeWidget(mButtons); delete mButtons; mButtons = nullptr; } } - else if (*setting=="Scripts/compile-delay") + else if (*setting == "Scripts/compile-delay") { - mCompileDelay->setInterval (setting->toInt()); + mCompileDelay->setInterval(setting->toInt()); } - else if (*setting=="Scripts/warnings") + else if (*setting == "Scripts/warnings") recompile(); } -void CSVWorld::ScriptSubView::updateStatusBar () +void CSVWorld::ScriptSubView::updateStatusBar() { - mBottom->positionChanged (mEditor->textCursor().blockNumber() + 1, - mEditor->textCursor().columnNumber() + 1); + mBottom->positionChanged(mEditor->textCursor().blockNumber() + 1, mEditor->textCursor().columnNumber() + 1); } -void CSVWorld::ScriptSubView::setEditLock (bool locked) +void CSVWorld::ScriptSubView::setEditLock(bool locked) { - mEditor->setReadOnly (locked); + mEditor->setReadOnly(locked); if (mButtons) - mButtons->setEditLock (locked); + mButtons->setEditLock(locked); - mCommandDispatcher.setEditLock (locked); + mCommandDispatcher.setEditLock(locked); } -void CSVWorld::ScriptSubView::useHint (const std::string& hint) +void CSVWorld::ScriptSubView::useHint(const std::string& hint) { if (hint.empty()) return; - unsigned line = 0, column = 0; + int line = 0, column = 0; char c; - std::istringstream stream (hint.c_str()+1); - switch(hint[0]) + std::istringstream stream(hint.c_str() + 1); + switch (hint[0]) { case 'R': case 'r': { - QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); - QString source = mModel->data (index).toString(); - unsigned stringSize = source.length(); - unsigned pos, dummy; - if (!(stream >> c >> dummy >> pos) ) + QModelIndex index = mModel->getModelIndex(getUniversalId().getId(), mColumn); + QString source = mModel->data(index).toString(); + int stringSize = static_cast(source.length()); + int pos, dummy; + if (!(stream >> c >> dummy >> pos)) return; if (pos > stringSize) { - Log(Debug::Warning) << "CSVWorld::ScriptSubView: requested position is higher than actual string length"; + Log(Debug::Warning) + << "CSVWorld::ScriptSubView: requested position is higher than actual string length"; pos = stringSize; } - for (unsigned i = 0; i <= pos; ++i) + for (int i = 0; i <= pos; ++i) { if (source[i] == '\n') { ++line; - column = i+1; + column = i + 1; } } column = pos - column; @@ -247,12 +252,12 @@ void CSVWorld::ScriptSubView::useHint (const std::string& hint) QTextCursor cursor = mEditor->textCursor(); - cursor.movePosition (QTextCursor::Start); - if (cursor.movePosition (QTextCursor::Down, QTextCursor::MoveAnchor, line)) - cursor.movePosition (QTextCursor::Right, QTextCursor::MoveAnchor, column); + cursor.movePosition(QTextCursor::Start); + if (cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, line)) + cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, column); mEditor->setFocus(); - mEditor->setTextCursor (cursor); + mEditor->setTextCursor(cursor); } void CSVWorld::ScriptSubView::textChanged() @@ -260,46 +265,46 @@ void CSVWorld::ScriptSubView::textChanged() if (mEditor->isChangeLocked()) return; - ScriptEdit::ChangeLock lock (*mEditor); + ScriptEdit::ChangeLock lock(*mEditor); QString source = mEditor->toPlainText(); - mDocument.getUndoStack().push (new CSMWorld::ModifyCommand (*mModel, - mModel->getModelIndex (getUniversalId().getId(), mColumn), source)); + mDocument.getUndoStack().push( + new CSMWorld::ModifyCommand(*mModel, mModel->getModelIndex(getUniversalId().getId(), mColumn), source)); recompile(); } -void CSVWorld::ScriptSubView::dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +void CSVWorld::ScriptSubView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mEditor->isChangeLocked()) return; - ScriptEdit::ChangeLock lock (*mEditor); + ScriptEdit::ChangeLock lock(*mEditor); bool updateRequired = false; - for (int i=topLeft.row(); i<=bottomRight.row(); ++i) + for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { - std::string id = mModel->data (mModel->index (i, mIdColumn)).toString().toUtf8().constData(); - if (mErrors->clearLocals (id)) + std::string id = mModel->data(mModel->index(i, mIdColumn)).toString().toUtf8().constData(); + if (mErrors->clearLocals(id)) updateRequired = true; } - QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); + QModelIndex index = mModel->getModelIndex(getUniversalId().getId(), mColumn); - if (index.row()>=topLeft.row() && index.row()<=bottomRight.row()) + if (index.row() >= topLeft.row() && index.row() <= bottomRight.row()) { - if (mStateColumn>=topLeft.column() && mStateColumn<=bottomRight.column()) + if (mStateColumn >= topLeft.column() && mStateColumn <= bottomRight.column()) updateDeletedState(); - if (mColumn>=topLeft.column() && mColumn<=bottomRight.column()) + if (mColumn >= topLeft.column() && mColumn <= bottomRight.column()) { - QString source = mModel->data (index).toString(); + QString source = mModel->data(index).toString(); QTextCursor cursor = mEditor->textCursor(); - mEditor->setPlainText (source); - mEditor->setTextCursor (cursor); + mEditor->setPlainText(source); + mEditor->setTextCursor(cursor); updateRequired = true; } @@ -309,66 +314,66 @@ void CSVWorld::ScriptSubView::dataChanged (const QModelIndex& topLeft, const QMo recompile(); } -void CSVWorld::ScriptSubView::rowsAboutToBeRemoved (const QModelIndex& parent, int start, int end) +void CSVWorld::ScriptSubView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { bool updateRequired = false; - for (int i=start; i<=end; ++i) + for (int i = start; i <= end; ++i) { - std::string id = mModel->data (mModel->index (i, mIdColumn)).toString().toUtf8().constData(); - if (mErrors->clearLocals (id)) + std::string id = mModel->data(mModel->index(i, mIdColumn)).toString().toUtf8().constData(); + if (mErrors->clearLocals(id)) updateRequired = true; } if (updateRequired) recompile(); - QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); + QModelIndex index = mModel->getModelIndex(getUniversalId().getId(), mColumn); - if (!parent.isValid() && index.row()>=start && index.row()<=end) + if (!parent.isValid() && index.row() >= start && index.row() <= end) emit closeRequest(); } -void CSVWorld::ScriptSubView::switchToRow (int row) +void CSVWorld::ScriptSubView::switchToRow(int row) { - int idColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); - std::string id = mModel->data (mModel->index (row, idColumn)).toString().toUtf8().constData(); - setUniversalId (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Script, id)); + int idColumn = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); + std::string id = mModel->data(mModel->index(row, idColumn)).toString().toUtf8().constData(); + setUniversalId(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Script, id)); - bool oldSignalsState = mEditor->blockSignals( true ); - mEditor->setPlainText( mModel->data(mModel->index(row, mColumn)).toString() ); - mEditor->blockSignals( oldSignalsState ); + bool oldSignalsState = mEditor->blockSignals(true); + mEditor->setPlainText(mModel->data(mModel->index(row, mColumn)).toString()); + mEditor->blockSignals(oldSignalsState); - std::vector selection (1, id); - mCommandDispatcher.setSelection (selection); + std::vector selection(1, id); + mCommandDispatcher.setSelection(selection); updateDeletedState(); } -void CSVWorld::ScriptSubView::switchToId (const std::string& id) +void CSVWorld::ScriptSubView::switchToId(const std::string& id) { - switchToRow (mModel->getModelIndex (id, 0).row()); + switchToRow(mModel->getModelIndex(id, 0).row()); } -void CSVWorld::ScriptSubView::highlightError (int line, int column) +void CSVWorld::ScriptSubView::highlightError(int line, int column) { QTextCursor cursor = mEditor->textCursor(); - cursor.movePosition (QTextCursor::Start); - if (cursor.movePosition (QTextCursor::Down, QTextCursor::MoveAnchor, line)) - cursor.movePosition (QTextCursor::Right, QTextCursor::MoveAnchor, column); + cursor.movePosition(QTextCursor::Start); + if (cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, line)) + cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, column); mEditor->setFocus(); - mEditor->setTextCursor (cursor); + mEditor->setTextCursor(cursor); } void CSVWorld::ScriptSubView::updateRequest() { - QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); + QModelIndex index = mModel->getModelIndex(getUniversalId().getId(), mColumn); - QString source = mModel->data (index).toString(); + QString source = mModel->data(index).toString(); - mErrors->update (source.toUtf8().constData()); + mErrors->update(source.toUtf8().constData()); adjustSplitter(); } diff --git a/apps/opencs/view/world/scriptsubview.hpp b/apps/opencs/view/world/scriptsubview.hpp index dc352cc5b07..2f53295f6be 100644 --- a/apps/opencs/view/world/scriptsubview.hpp +++ b/apps/opencs/view/world/scriptsubview.hpp @@ -3,15 +3,18 @@ #include +#include + +#include + #include "../../model/world/commanddispatcher.hpp" #include "../doc/subview.hpp" class QModelIndex; -class QLabel; -class QVBoxLayout; +class QObject; class QSplitter; -class QTime; +class QTimer; namespace CSMDoc { @@ -37,66 +40,64 @@ namespace CSVWorld class ScriptSubView : public CSVDoc::SubView { - Q_OBJECT - - ScriptEdit *mEditor; - CSMDoc::Document& mDocument; - CSMWorld::IdTable *mModel; - int mColumn; - int mIdColumn; - int mStateColumn; - TableBottomBox *mBottom; - RecordButtonBar *mButtons; - CSMWorld::CommandDispatcher mCommandDispatcher; - QVBoxLayout mLayout; - QSplitter *mMain; - ScriptErrorTable *mErrors; - QTimer *mCompileDelay; - int mErrorHeight; - - private: + Q_OBJECT - void addButtonBar(); + ScriptEdit* mEditor; + CSMDoc::Document& mDocument; + CSMWorld::IdTable* mModel; + int mColumn; + int mIdColumn; + int mStateColumn; + TableBottomBox* mBottom; + RecordButtonBar* mButtons; + CSMWorld::CommandDispatcher mCommandDispatcher; + QVBoxLayout mLayout; + QSplitter* mMain; + ScriptErrorTable* mErrors; + QTimer* mCompileDelay; + int mErrorHeight; - void recompile(); + private: + void addButtonBar(); - bool isDeleted() const; + void recompile(); - void updateDeletedState(); + bool isDeleted() const; - void adjustSplitter(); + void updateDeletedState(); - public: + void adjustSplitter(); - ScriptSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); + public: + ScriptSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); - void setEditLock (bool locked) override; + void setEditLock(bool locked) override; - void useHint (const std::string& hint) override; + void useHint(const std::string& hint) override; - void setStatusBar (bool show) override; + void setStatusBar(bool show) override; - public slots: + public slots: - void textChanged(); + void textChanged(); - void dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); - void rowsAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); - private slots: + private slots: - void settingChanged (const CSMPrefs::Setting *setting); + void settingChanged(const CSMPrefs::Setting* setting); - void updateStatusBar(); + void updateStatusBar(); - void switchToRow (int row); + void switchToRow(int row); - void switchToId (const std::string& id); + void switchToId(const std::string& id); - void highlightError (int line, int column); + void highlightError(int line, int column); - void updateRequest(); + void updateRequest(); }; } diff --git a/apps/opencs/view/world/startscriptcreator.cpp b/apps/opencs/view/world/startscriptcreator.cpp index 0eb6bae40e9..fcbbc761596 100644 --- a/apps/opencs/view/world/startscriptcreator.cpp +++ b/apps/opencs/view/world/startscriptcreator.cpp @@ -2,16 +2,23 @@ #include +#include + +#include +#include +#include + #include "../../model/doc/document.hpp" #include "../../model/world/columns.hpp" -#include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idcompletionmanager.hpp" #include "../../model/world/idtable.hpp" #include "../widget/droplineedit.hpp" +class QUndoStack; + std::string CSVWorld::StartScriptCreator::getId() const { return mScript->text().toUtf8().constData(); @@ -19,22 +26,17 @@ std::string CSVWorld::StartScriptCreator::getId() const CSMWorld::IdTable& CSVWorld::StartScriptCreator::getStartScriptsTable() const { - return dynamic_cast ( - *getData().getTableModel(getCollectionId()) - ); + return dynamic_cast(*getData().getTableModel(getCollectionId())); } -CSVWorld::StartScriptCreator::StartScriptCreator( - CSMWorld::Data &data, - QUndoStack &undoStack, - const CSMWorld::UniversalId &id, - CSMWorld::IdCompletionManager& completionManager -) : GenericCreator(data, undoStack, id) +CSVWorld::StartScriptCreator::StartScriptCreator(CSMWorld::Data& data, QUndoStack& undoStack, + const CSMWorld::UniversalId& id, CSMWorld::IdCompletionManager& completionManager) + : GenericCreator(data, undoStack, id) { setManualEditing(false); // Add script ID input label. - QLabel *label = new QLabel("Script", this); + QLabel* label = new QLabel("Script", this); insertBeforeButtons(label, false); // Add script ID input with auto-completion. @@ -44,13 +46,11 @@ CSVWorld::StartScriptCreator::StartScriptCreator( mScript->setCompleter(completionManager.getCompleter(displayType).get()); insertBeforeButtons(mScript, true); - connect(mScript, SIGNAL (textChanged(const QString&)), this, SLOT (scriptChanged())); - connect(mScript, SIGNAL (returnPressed()), this, SLOT (inputReturnPressed())); + connect(mScript, &CSVWidget::DropLineEdit::textChanged, this, &StartScriptCreator::scriptChanged); + connect(mScript, &CSVWidget::DropLineEdit::returnPressed, this, &StartScriptCreator::inputReturnPressed); } -void CSVWorld::StartScriptCreator::cloneMode( - const std::string& originId, - const CSMWorld::UniversalId::Type type) +void CSVWorld::StartScriptCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { CSVWorld::GenericCreator::cloneMode(originId, type); @@ -62,7 +62,7 @@ void CSVWorld::StartScriptCreator::cloneMode( std::string CSVWorld::StartScriptCreator::getErrors() const { - std::string scriptId = getId(); + const ESM::RefId scriptId = ESM::RefId::stringRefId(getId()); // Check user input for any errors. std::string errors; @@ -98,14 +98,8 @@ void CSVWorld::StartScriptCreator::scriptChanged() update(); } -CSVWorld::Creator *CSVWorld::StartScriptCreatorFactory::makeCreator( - CSMDoc::Document& document, - const CSMWorld::UniversalId& id) const +CSVWorld::Creator* CSVWorld::StartScriptCreatorFactory::makeCreator( + CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { - return new StartScriptCreator( - document.getData(), - document.getUndoStack(), - id, - document.getIdCompletionManager() - ); + return new StartScriptCreator(document.getData(), document.getUndoStack(), id, document.getIdCompletionManager()); } diff --git a/apps/opencs/view/world/startscriptcreator.hpp b/apps/opencs/view/world/startscriptcreator.hpp index cb7f3f619cb..8d8f51bd8ee 100644 --- a/apps/opencs/view/world/startscriptcreator.hpp +++ b/apps/opencs/view/world/startscriptcreator.hpp @@ -3,12 +3,26 @@ #include "genericcreator.hpp" +#include + +#include +#include + +class QObject; +class QUndoStack; + namespace CSMWorld { + class Data; class IdCompletionManager; class IdTable; } +namespace CSMDoc +{ + class Document; +} + namespace CSVWidget { class DropLineEdit; @@ -21,55 +35,45 @@ namespace CSVWorld { Q_OBJECT - CSVWidget::DropLineEdit *mScript; - - private: - - /// \return script ID entered by user. - std::string getId() const override; + CSVWidget::DropLineEdit* mScript; - /// \return reference to table containing start scripts. - CSMWorld::IdTable& getStartScriptsTable() const; + private: + /// \return script ID entered by user. + std::string getId() const override; - public: + /// \return reference to table containing start scripts. + CSMWorld::IdTable& getStartScriptsTable() const; - StartScriptCreator( - CSMWorld::Data& data, - QUndoStack& undoStack, - const CSMWorld::UniversalId& id, - CSMWorld::IdCompletionManager& completionManager); + public: + StartScriptCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, + CSMWorld::IdCompletionManager& completionManager); - /// \brief Set script ID input widget to ID of record to be cloned. - /// \param originId Script ID to be cloned. - /// \param type Type of record to be cloned. - void cloneMode( - const std::string& originId, - const CSMWorld::UniversalId::Type type) override; + /// \brief Set script ID input widget to ID of record to be cloned. + /// \param originId Script ID to be cloned. + /// \param type Type of record to be cloned. + void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; - /// \return Error description for current user input. - std::string getErrors() const override; + /// \return Error description for current user input. + std::string getErrors() const override; - /// \brief Set focus to script ID input widget. - void focus() override; + /// \brief Set focus to script ID input widget. + void focus() override; - /// \brief Clear script ID input widget. - void reset() override; + /// \brief Clear script ID input widget. + void reset() override; - private slots: + private slots: - /// \brief Check user input for any errors. - void scriptChanged(); - }; + /// \brief Check user input for any errors. + void scriptChanged(); + }; - /// \brief Creator factory for start script record creator. - class StartScriptCreatorFactory : public CreatorFactoryBase - { - public: - - Creator *makeCreator( - CSMDoc::Document& document, - const CSMWorld::UniversalId& id) const override; - }; + /// \brief Creator factory for start script record creator. + class StartScriptCreatorFactory : public CreatorFactoryBase + { + public: + Creator* makeCreator(CSMDoc::Document& document, const CSMWorld::UniversalId& id) const override; + }; } #endif // STARTSCRIPTCREATOR_HPP diff --git a/apps/opencs/view/world/subviews.cpp b/apps/opencs/view/world/subviews.cpp index 169bc2e94e7..8a47f95e637 100644 --- a/apps/opencs/view/world/subviews.cpp +++ b/apps/opencs/view/world/subviews.cpp @@ -2,40 +2,44 @@ #include "../doc/subviewfactoryimp.hpp" -#include "tablesubview.hpp" +#include "bodypartcreator.hpp" +#include "cellcreator.hpp" +#include "dialoguecreator.hpp" #include "dialoguesubview.hpp" -#include "scriptsubview.hpp" -#include "regionmapsubview.hpp" #include "genericcreator.hpp" #include "globalcreator.hpp" -#include "cellcreator.hpp" -#include "referenceablecreator.hpp" -#include "referencecreator.hpp" -#include "startscriptcreator.hpp" -#include "scenesubview.hpp" -#include "dialoguecreator.hpp" #include "infocreator.hpp" +#include "landcreator.hpp" #include "pathgridcreator.hpp" #include "previewsubview.hpp" -#include "bodypartcreator.hpp" -#include "landcreator.hpp" -#include "landtexturecreator.hpp" +#include "referenceablecreator.hpp" +#include "referencecreator.hpp" +#include "regionmapsubview.hpp" +#include "scenesubview.hpp" +#include "scriptsubview.hpp" +#include "startscriptcreator.hpp" +#include "tablesubview.hpp" + +#include +#include +#include +#include +#include -void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) +void CSVWorld::addSubViewFactories(CSVDoc::SubViewFactoryManager& manager) { // Regular record tables (including references which are actually sub-records, but are promoted // to top-level records within the editor) - manager.add (CSMWorld::UniversalId::Type_Gmsts, - new CSVDoc::SubViewFactoryWithCreator); + manager.add( + CSMWorld::UniversalId::Type_Gmsts, new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_Skills, - new CSVDoc::SubViewFactoryWithCreator); + manager.add( + CSMWorld::UniversalId::Type_Skills, new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_MagicEffects, + manager.add(CSMWorld::UniversalId::Type_MagicEffects, new CSVDoc::SubViewFactoryWithCreator); - static const CSMWorld::UniversalId::Type sTableTypes[] = - { + static const CSMWorld::UniversalId::Type sTableTypes[] = { CSMWorld::UniversalId::Type_Classes, CSMWorld::UniversalId::Type_Factions, CSMWorld::UniversalId::Type_Races, @@ -45,92 +49,90 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) CSMWorld::UniversalId::Type_Spells, CSMWorld::UniversalId::Type_Enchantments, CSMWorld::UniversalId::Type_SoundGens, - - CSMWorld::UniversalId::Type_None // end marker + // end marker + CSMWorld::UniversalId::Type_None, }; - for (int i=0; sTableTypes[i]!=CSMWorld::UniversalId::Type_None; ++i) - manager.add (sTableTypes[i], - new CSVDoc::SubViewFactoryWithCreator >); + for (int i = 0; sTableTypes[i] != CSMWorld::UniversalId::Type_None; ++i) + manager.add( + sTableTypes[i], new CSVDoc::SubViewFactoryWithCreator>); - manager.add (CSMWorld::UniversalId::Type_BodyParts, - new CSVDoc::SubViewFactoryWithCreator >); + manager.add(CSMWorld::UniversalId::Type_BodyParts, + new CSVDoc::SubViewFactoryWithCreator>); - manager.add (CSMWorld::UniversalId::Type_StartScripts, + manager.add(CSMWorld::UniversalId::Type_StartScripts, new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_Cells, - new CSVDoc::SubViewFactoryWithCreator >); + manager.add(CSMWorld::UniversalId::Type_Cells, + new CSVDoc::SubViewFactoryWithCreator>); - manager.add (CSMWorld::UniversalId::Type_Referenceables, - new CSVDoc::SubViewFactoryWithCreator >); + manager.add(CSMWorld::UniversalId::Type_Referenceables, + new CSVDoc::SubViewFactoryWithCreator>); - manager.add (CSMWorld::UniversalId::Type_References, + manager.add(CSMWorld::UniversalId::Type_References, new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_Topics, - new CSVDoc::SubViewFactoryWithCreator); + manager.add( + CSMWorld::UniversalId::Type_Topics, new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_Journals, + manager.add(CSMWorld::UniversalId::Type_Journals, new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_TopicInfos, + manager.add(CSMWorld::UniversalId::Type_TopicInfos, new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_JournalInfos, + manager.add(CSMWorld::UniversalId::Type_JournalInfos, new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_Pathgrids, + manager.add(CSMWorld::UniversalId::Type_Pathgrids, new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_Lands, - new CSVDoc::SubViewFactoryWithCreator >); + manager.add(CSMWorld::UniversalId::Type_Lands, + new CSVDoc::SubViewFactoryWithCreator>); - manager.add (CSMWorld::UniversalId::Type_LandTextures, - new CSVDoc::SubViewFactoryWithCreator >); + manager.add(CSMWorld::UniversalId::Type_LandTextures, + new CSVDoc::SubViewFactoryWithCreator>); - manager.add (CSMWorld::UniversalId::Type_Globals, - new CSVDoc::SubViewFactoryWithCreator >); + manager.add(CSMWorld::UniversalId::Type_Globals, + new CSVDoc::SubViewFactoryWithCreator>); // Subviews for resources tables - manager.add (CSMWorld::UniversalId::Type_Meshes, - new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_Icons, - new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_Musics, - new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_SoundsRes, - new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_Textures, - new CSVDoc::SubViewFactoryWithCreator); - manager.add (CSMWorld::UniversalId::Type_Videos, - new CSVDoc::SubViewFactoryWithCreator); - + manager.add( + CSMWorld::UniversalId::Type_Meshes, new CSVDoc::SubViewFactoryWithCreator); + manager.add( + CSMWorld::UniversalId::Type_Icons, new CSVDoc::SubViewFactoryWithCreator); + manager.add( + CSMWorld::UniversalId::Type_Musics, new CSVDoc::SubViewFactoryWithCreator); + manager.add( + CSMWorld::UniversalId::Type_SoundsRes, new CSVDoc::SubViewFactoryWithCreator); + manager.add( + CSMWorld::UniversalId::Type_Textures, new CSVDoc::SubViewFactoryWithCreator); + manager.add( + CSMWorld::UniversalId::Type_Videos, new CSVDoc::SubViewFactoryWithCreator); // Subviews for editing/viewing individual records - manager.add (CSMWorld::UniversalId::Type_Script, new CSVDoc::SubViewFactory); + manager.add(CSMWorld::UniversalId::Type_Script, new CSVDoc::SubViewFactory); // Other stuff (combined record tables) - manager.add (CSMWorld::UniversalId::Type_RegionMap, new CSVDoc::SubViewFactory); + manager.add(CSMWorld::UniversalId::Type_RegionMap, new CSVDoc::SubViewFactory); - manager.add (CSMWorld::UniversalId::Type_Scene, new CSVDoc::SubViewFactory); + manager.add(CSMWorld::UniversalId::Type_Scene, new CSVDoc::SubViewFactory); // More other stuff - manager.add (CSMWorld::UniversalId::Type_Filters, + manager.add(CSMWorld::UniversalId::Type_Filters, new CSVDoc::SubViewFactoryWithCreator >); + CreatorFactory>); - manager.add (CSMWorld::UniversalId::Type_DebugProfiles, + manager.add(CSMWorld::UniversalId::Type_DebugProfiles, new CSVDoc::SubViewFactoryWithCreator >); + CreatorFactory>); - manager.add (CSMWorld::UniversalId::Type_Scripts, - new CSVDoc::SubViewFactoryWithCreator >); + manager.add(CSMWorld::UniversalId::Type_Scripts, + new CSVDoc::SubViewFactoryWithCreator>); // Dialogue subviews - static const CSMWorld::UniversalId::Type sTableTypes2[] = - { + static const CSMWorld::UniversalId::Type sTableTypes2[] = { CSMWorld::UniversalId::Type_Region, CSMWorld::UniversalId::Type_Spell, CSMWorld::UniversalId::Type_Birthsign, @@ -141,69 +143,69 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) CSMWorld::UniversalId::Type_Faction, CSMWorld::UniversalId::Type_Enchantment, CSMWorld::UniversalId::Type_SoundGen, - - CSMWorld::UniversalId::Type_None // end marker + // end marker + CSMWorld::UniversalId::Type_None, }; - for (int i=0; sTableTypes2[i]!=CSMWorld::UniversalId::Type_None; ++i) - manager.add (sTableTypes2[i], - new CSVDoc::SubViewFactoryWithCreator > (false)); + for (int i = 0; sTableTypes2[i] != CSMWorld::UniversalId::Type_None; ++i) + manager.add(sTableTypes2[i], + new CSVDoc::SubViewFactoryWithCreator>(false)); - manager.add (CSMWorld::UniversalId::Type_BodyPart, - new CSVDoc::SubViewFactoryWithCreator > (false)); + manager.add(CSMWorld::UniversalId::Type_BodyPart, + new CSVDoc::SubViewFactoryWithCreator>(false)); - manager.add (CSMWorld::UniversalId::Type_StartScript, + manager.add(CSMWorld::UniversalId::Type_StartScript, new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_Skill, - new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add(CSMWorld::UniversalId::Type_Skill, + new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_MagicEffect, - new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add(CSMWorld::UniversalId::Type_MagicEffect, + new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_Gmst, - new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add(CSMWorld::UniversalId::Type_Gmst, + new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_Referenceable, - new CSVDoc::SubViewFactoryWithCreator > (false)); + manager.add(CSMWorld::UniversalId::Type_Referenceable, + new CSVDoc::SubViewFactoryWithCreator>(false)); - manager.add (CSMWorld::UniversalId::Type_Reference, - new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add(CSMWorld::UniversalId::Type_Reference, + new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_Cell, - new CSVDoc::SubViewFactoryWithCreator > (false)); + manager.add(CSMWorld::UniversalId::Type_Cell, + new CSVDoc::SubViewFactoryWithCreator>(false)); - manager.add (CSMWorld::UniversalId::Type_JournalInfo, - new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add(CSMWorld::UniversalId::Type_JournalInfo, + new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_TopicInfo, + manager.add(CSMWorld::UniversalId::Type_TopicInfo, new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_Topic, - new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add(CSMWorld::UniversalId::Type_Topic, + new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_Journal, - new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add(CSMWorld::UniversalId::Type_Journal, + new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_Pathgrid, - new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add(CSMWorld::UniversalId::Type_Pathgrid, + new CSVDoc::SubViewFactoryWithCreator(false)); - manager.add (CSMWorld::UniversalId::Type_Land, - new CSVDoc::SubViewFactoryWithCreator >(false)); + manager.add(CSMWorld::UniversalId::Type_Land, + new CSVDoc::SubViewFactoryWithCreator>(false)); - manager.add (CSMWorld::UniversalId::Type_LandTexture, - new CSVDoc::SubViewFactoryWithCreator >(false)); + manager.add(CSMWorld::UniversalId::Type_LandTexture, + new CSVDoc::SubViewFactoryWithCreator>(false)); - manager.add (CSMWorld::UniversalId::Type_DebugProfile, - new CSVDoc::SubViewFactoryWithCreator > (false)); + manager.add(CSMWorld::UniversalId::Type_DebugProfile, + new CSVDoc::SubViewFactoryWithCreator>(false)); - manager.add (CSMWorld::UniversalId::Type_Filter, - new CSVDoc::SubViewFactoryWithCreator > (false)); + manager.add(CSMWorld::UniversalId::Type_Filter, + new CSVDoc::SubViewFactoryWithCreator>(false)); - manager.add (CSMWorld::UniversalId::Type_MetaData, - new CSVDoc::SubViewFactory); + manager.add(CSMWorld::UniversalId::Type_MetaData, new CSVDoc::SubViewFactory); - //preview - manager.add (CSMWorld::UniversalId::Type_Preview, new CSVDoc::SubViewFactory); + // preview + manager.add(CSMWorld::UniversalId::Type_Preview, new CSVDoc::SubViewFactory); } diff --git a/apps/opencs/view/world/subviews.hpp b/apps/opencs/view/world/subviews.hpp index 51e4cb08307..cfa5a821393 100644 --- a/apps/opencs/view/world/subviews.hpp +++ b/apps/opencs/view/world/subviews.hpp @@ -8,7 +8,7 @@ namespace CSVDoc namespace CSVWorld { - void addSubViewFactories (CSVDoc::SubViewFactoryManager& manager); + void addSubViewFactories(CSVDoc::SubViewFactoryManager& manager); } #endif diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index c676a5ecc0e..86a1e93bbd7 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -1,46 +1,66 @@ #include "table.hpp" -#include #include -#include #include +#include +#include +#include #include -#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include -#include +#include +#include #include "../../model/doc/document.hpp" +#include "../../model/world/commanddispatcher.hpp" #include "../../model/world/commands.hpp" -#include "../../model/world/infotableproxymodel.hpp" -#include "../../model/world/idtableproxymodel.hpp" -#include "../../model/world/idtablebase.hpp" #include "../../model/world/idtable.hpp" +#include "../../model/world/idtablebase.hpp" +#include "../../model/world/infotableproxymodel.hpp" #include "../../model/world/landtexturetableproxymodel.hpp" -#include "../../model/world/record.hpp" -#include "../../model/world/columns.hpp" -#include "../../model/world/commanddispatcher.hpp" -#include "../../model/prefs/state.hpp" #include "../../model/prefs/shortcut.hpp" +#include "../../model/prefs/state.hpp" #include "tableeditidaction.hpp" +#include "tableheadermouseeventhandler.hpp" #include "util.hpp" -void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) +namespace CSMFilter +{ + class Node; +} + +void CSVWorld::Table::contextMenuEvent(QContextMenuEvent* event) { // configure dispatcher - mDispatcher->setSelection (getSelectedIds()); + mDispatcher->setSelection(getSelectedIds()); std::vector extendedTypes = mDispatcher->getExtendedTypes(); - mDispatcher->setExtendedTypes (extendedTypes); + mDispatcher->setExtendedTypes(extendedTypes); // create context menu QModelIndexList selectedRows = selectionModel()->selectedRows(); - QMenu menu (this); + QMenu menu(this); /// \todo add menu items for select all and clear selection @@ -55,50 +75,49 @@ void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) if (!mEditLock && !(mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) { - if (selectedRows.size()==1) + if (selectedRows.size() == 1) { - menu.addAction (mEditAction); + menu.addAction(mEditAction); if (mCreateAction) menu.addAction(mCloneAction); } if (mTouchAction) - menu.addAction (mTouchAction); + menu.addAction(mTouchAction); if (mCreateAction) - menu.addAction (mCreateAction); + menu.addAction(mCreateAction); if (mDispatcher->canRevert()) { - menu.addAction (mRevertAction); + menu.addAction(mRevertAction); if (!extendedTypes.empty()) - menu.addAction (mExtendedRevertAction); + menu.addAction(mExtendedRevertAction); } if (mDispatcher->canDelete()) { - menu.addAction (mDeleteAction); + menu.addAction(mDeleteAction); if (!extendedTypes.empty()) - menu.addAction (mExtendedDeleteAction); + menu.addAction(mExtendedDeleteAction); } if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_ReorderWithinTopic) { /// \todo allow reordering of multiple rows - if (selectedRows.size()==1) + if (selectedRows.size() == 1) { - int column = mModel->searchColumnIndex (CSMWorld::Columns::ColumnId_Topic); + int column = mModel->searchColumnIndex(CSMWorld::Columns::ColumnId_Topic); - if (column==-1) - column = mModel->searchColumnIndex (CSMWorld::Columns::ColumnId_Journal); + if (column == -1) + column = mModel->searchColumnIndex(CSMWorld::Columns::ColumnId_Journal); - if (column!=-1) + if (column != -1) { - int row = mProxyModel->mapToSource ( - mProxyModel->index (selectedRows.begin()->row(), 0)).row(); + int row = mProxyModel->mapToSource(mProxyModel->index(selectedRows.begin()->row(), 0)).row(); QString curData = mModel->data(mModel->index(row, column)).toString(); if (row > 0) @@ -123,22 +142,22 @@ void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) } } - if (selectedRows.size()==1) + if (selectedRows.size() == 1) { int row = selectedRows.begin()->row(); - row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); + row = mProxyModel->mapToSource(mProxyModel->index(row, 0)).row(); if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_View) { - CSMWorld::UniversalId id = mModel->view (row).first; + CSMWorld::UniversalId id = mModel->view(row).first; - int index = mDocument.getData().getCells().searchId (id.getId()); + const int index = mDocument.getData().getCells().searchId(ESM::RefId::stringRefId(id.getId())); // index==-1: the ID references a worldspace instead of a cell (ignore for now and go // ahead) - if (index==-1 || !mDocument.getData().getCells().getRecord (index).isDeleted()) - menu.addAction (mViewAction); + if (index == -1 || !mDocument.getData().getCells().getRecord(index).isDeleted()) + menu.addAction(mViewAction); } if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Preview) @@ -146,37 +165,33 @@ void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) const CSMWorld::UniversalId id = getUniversalId(currentRow); const CSMWorld::UniversalId::Type type = id.getType(); - QModelIndex index = mModel->index (row, - mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); + QModelIndex index = mModel->index(row, mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Modification)); - CSMWorld::RecordBase::State state = static_cast ( - mModel->data (index).toInt()); + CSMWorld::RecordBase::State state = static_cast(mModel->data(index).toInt()); - if (state!=CSMWorld::RecordBase::State_Deleted && type != CSMWorld::UniversalId::Type_ItemLevelledList) - menu.addAction (mPreviewAction); + if (state != CSMWorld::RecordBase::State_Deleted && type != CSMWorld::UniversalId::Type_ItemLevelledList) + menu.addAction(mPreviewAction); } } if (mHelpAction) - menu.addAction (mHelpAction); + menu.addAction(mHelpAction); - menu.exec (event->globalPos()); + menu.exec(event->globalPos()); } -void CSVWorld::Table::mouseDoubleClickEvent (QMouseEvent *event) +void CSVWorld::Table::mouseDoubleClickEvent(QMouseEvent* event) { - Qt::KeyboardModifiers modifiers = - event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier); + Qt::KeyboardModifiers modifiers = event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier); QModelIndex index = currentIndex(); - selectionModel()->select (index, - QItemSelectionModel::Clear | QItemSelectionModel::Select | QItemSelectionModel::Rows); + selectionModel()->select( + index, QItemSelectionModel::Clear | QItemSelectionModel::Select | QItemSelectionModel::Rows); - std::map::iterator iter = - mDoubleClickActions.find (modifiers); + std::map::iterator iter = mDoubleClickActions.find(modifiers); - if (iter==mDoubleClickActions.end()) + if (iter == mDoubleClickActions.end()) { event->accept(); return; @@ -191,7 +206,7 @@ void CSVWorld::Table::mouseDoubleClickEvent (QMouseEvent *event) case Action_InPlaceEdit: - DragRecordTable::mouseDoubleClickEvent (event); + DragRecordTable::mouseDoubleClickEvent(event); break; case Action_EditRecord: @@ -236,84 +251,96 @@ void CSVWorld::Table::mouseDoubleClickEvent (QMouseEvent *event) } } -CSVWorld::Table::Table (const CSMWorld::UniversalId& id, - bool createAndDelete, bool sorting, CSMDoc::Document& document) - : DragRecordTable(document), mCreateAction (nullptr), mCloneAction(nullptr), mTouchAction(nullptr), - mRecordStatusDisplay (0), mJumpToAddedRecord(false), mUnselectAfterJump(false) +CSVWorld::Table::Table(const CSMWorld::UniversalId& id, bool createAndDelete, bool sorting, CSMDoc::Document& document) + : DragRecordTable(document) + , mCreateAction(nullptr) + , mCloneAction(nullptr) + , mTouchAction(nullptr) + , mRecordStatusDisplay(0) + , mJumpToAddedRecord(false) + , mUnselectAfterJump(false) + , mAutoJump(false) { - mModel = &dynamic_cast (*mDocument.getData().getTableModel (id)); + mModel = &dynamic_cast(*mDocument.getData().getTableModel(id)); - bool isInfoTable = id.getType() == CSMWorld::UniversalId::Type_TopicInfos || - id.getType() == CSMWorld::UniversalId::Type_JournalInfos; + bool isInfoTable = id.getType() == CSMWorld::UniversalId::Type_TopicInfos + || id.getType() == CSMWorld::UniversalId::Type_JournalInfos; bool isLtexTable = (id.getType() == CSMWorld::UniversalId::Type_LandTextures); if (isInfoTable) { mProxyModel = new CSMWorld::InfoTableProxyModel(id.getType(), this); - connect (this, &CSVWorld::DragRecordTable::moveRecordsFromSameTable, this, &CSVWorld::Table::moveRecords); + connect(this, &CSVWorld::DragRecordTable::moveRecordsFromSameTable, this, &CSVWorld::Table::moveRecords); + connect(this, &CSVWorld::DragRecordTable::createNewInfoRecord, this, + &CSVWorld::Table::createRecordsDirectlyRequest); } else if (isLtexTable) { - mProxyModel = new CSMWorld::LandTextureTableProxyModel (this); + mProxyModel = new CSMWorld::LandTextureTableProxyModel(this); } else { - mProxyModel = new CSMWorld::IdTableProxyModel (this); + mProxyModel = new CSMWorld::IdTableProxyModel(this); } - mProxyModel->setSourceModel (mModel); + mProxyModel->setSourceModel(mModel); - mDispatcher = new CSMWorld::CommandDispatcher (document, id, this); + mDispatcher = new CSMWorld::CommandDispatcher(document, id, this); - setModel (mProxyModel); - horizontalHeader()->setSectionResizeMode (QHeaderView::Interactive); + setModel(mProxyModel); + horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + // The number is arbitrary but autoresize would be way too slow otherwise. + constexpr int autoResizePrecision = 500; + horizontalHeader()->setResizeContentsPrecision(autoResizePrecision); + resizeColumnsToContents(); verticalHeader()->hide(); - setSelectionBehavior (QAbstractItemView::SelectRows); - setSelectionMode (QAbstractItemView::ExtendedSelection); - - setSortingEnabled (sorting); - if (sorting) - { - sortByColumn (mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id), Qt::AscendingOrder); - } + setSelectionBehavior(QAbstractItemView::SelectRows); + setSelectionMode(QAbstractItemView::ExtendedSelection); int columns = mModel->columnCount(); - for (int i=0; iheaderData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags).toInt(); + int flags = mModel->headerData(i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags).toInt(); if (flags & CSMWorld::ColumnBase::Flag_Table) { - CSMWorld::ColumnBase::Display display = static_cast ( - mModel->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); + CSMWorld::ColumnBase::Display display = static_cast( + mModel->headerData(i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); - CommandDelegate *delegate = CommandDelegateFactoryCollection::get().makeDelegate (display, - mDispatcher, document, this); + CommandDelegate* delegate + = CommandDelegateFactoryCollection::get().makeDelegate(display, mDispatcher, document, this); - mDelegates.push_back (delegate); - setItemDelegateForColumn (i, delegate); + mDelegates.push_back(delegate); + setItemDelegateForColumn(i, delegate); } else - hideColumn (i); + hideColumn(i); + } + + if (sorting) + { + // FIXME: some tables (e.g. CellRef) have this column hidden, which makes it confusing + sortByColumn(mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id), Qt::AscendingOrder); } + setSortingEnabled(sorting); - mEditAction = new QAction (tr ("Edit Record"), this); - connect (mEditAction, SIGNAL (triggered()), this, SLOT (editRecord())); - mEditAction->setIcon(QIcon(":edit-edit")); - addAction (mEditAction); + mEditAction = new QAction(tr("Edit Record"), this); + connect(mEditAction, &QAction::triggered, this, &Table::editRecord); + mEditAction->setIcon(Misc::ScalableIcon::load(":edit-edit")); + addAction(mEditAction); CSMPrefs::Shortcut* editShortcut = new CSMPrefs::Shortcut("table-edit", this); editShortcut->associateAction(mEditAction); if (createAndDelete) { - mCreateAction = new QAction (tr ("Add Record"), this); - connect (mCreateAction, SIGNAL (triggered()), this, SIGNAL (createRequest())); - mCreateAction->setIcon(QIcon(":edit-add")); - addAction (mCreateAction); + mCreateAction = new QAction(tr("Add Record"), this); + connect(mCreateAction, &QAction::triggered, this, &Table::createRequest); + mCreateAction->setIcon(Misc::ScalableIcon::load(":edit-add")); + addAction(mCreateAction); CSMPrefs::Shortcut* createShortcut = new CSMPrefs::Shortcut("table-add", this); createShortcut->associateAction(mCreateAction); - mCloneAction = new QAction (tr ("Clone Record"), this); - connect(mCloneAction, SIGNAL (triggered()), this, SLOT (cloneRecord())); - mCloneAction->setIcon(QIcon(":edit-clone")); + mCloneAction = new QAction(tr("Clone Record"), this); + connect(mCloneAction, &QAction::triggered, this, &Table::cloneRecord); + mCloneAction->setIcon(Misc::ScalableIcon::load(":edit-clone")); addAction(mCloneAction); CSMPrefs::Shortcut* cloneShortcut = new CSMPrefs::Shortcut("table-clone", this); cloneShortcut->associateAction(mCloneAction); @@ -322,139 +349,134 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_AllowTouch) { mTouchAction = new QAction(tr("Touch Record"), this); - connect(mTouchAction, SIGNAL(triggered()), this, SLOT(touchRecord())); - mTouchAction->setIcon(QIcon(":edit-touch")); + connect(mTouchAction, &QAction::triggered, this, &Table::touchRecord); + mTouchAction->setIcon(Misc::ScalableIcon::load(":edit-touch")); addAction(mTouchAction); CSMPrefs::Shortcut* touchShortcut = new CSMPrefs::Shortcut("table-touch", this); touchShortcut->associateAction(mTouchAction); } - mRevertAction = new QAction (tr ("Revert Record"), this); - connect (mRevertAction, SIGNAL (triggered()), mDispatcher, SLOT (executeRevert())); - mRevertAction->setIcon(QIcon(":edit-undo")); - addAction (mRevertAction); + mRevertAction = new QAction(tr("Revert Record"), this); + connect(mRevertAction, &QAction::triggered, mDispatcher, &CSMWorld::CommandDispatcher::executeRevert); + mRevertAction->setIcon(Misc::ScalableIcon::load(":edit-undo")); + addAction(mRevertAction); CSMPrefs::Shortcut* revertShortcut = new CSMPrefs::Shortcut("table-revert", this); revertShortcut->associateAction(mRevertAction); - mDeleteAction = new QAction (tr ("Delete Record"), this); - connect (mDeleteAction, SIGNAL (triggered()), mDispatcher, SLOT (executeDelete())); - mDeleteAction->setIcon(QIcon(":edit-delete")); - addAction (mDeleteAction); + mDeleteAction = new QAction(tr("Delete Record"), this); + connect(mDeleteAction, &QAction::triggered, mDispatcher, &CSMWorld::CommandDispatcher::executeDelete); + mDeleteAction->setIcon(Misc::ScalableIcon::load(":edit-delete")); + addAction(mDeleteAction); CSMPrefs::Shortcut* deleteShortcut = new CSMPrefs::Shortcut("table-remove", this); deleteShortcut->associateAction(mDeleteAction); - mMoveUpAction = new QAction (tr ("Move Up"), this); - connect (mMoveUpAction, SIGNAL (triggered()), this, SLOT (moveUpRecord())); - mMoveUpAction->setIcon(QIcon(":record-up")); - addAction (mMoveUpAction); + mMoveUpAction = new QAction(tr("Move Up"), this); + connect(mMoveUpAction, &QAction::triggered, this, &Table::moveUpRecord); + mMoveUpAction->setIcon(Misc::ScalableIcon::load(":record-up")); + addAction(mMoveUpAction); CSMPrefs::Shortcut* moveUpShortcut = new CSMPrefs::Shortcut("table-moveup", this); moveUpShortcut->associateAction(mMoveUpAction); - mMoveDownAction = new QAction (tr ("Move Down"), this); - connect (mMoveDownAction, SIGNAL (triggered()), this, SLOT (moveDownRecord())); - mMoveDownAction->setIcon(QIcon(":record-down")); - addAction (mMoveDownAction); + mMoveDownAction = new QAction(tr("Move Down"), this); + connect(mMoveDownAction, &QAction::triggered, this, &Table::moveDownRecord); + mMoveDownAction->setIcon(Misc::ScalableIcon::load(":record-down")); + addAction(mMoveDownAction); CSMPrefs::Shortcut* moveDownShortcut = new CSMPrefs::Shortcut("table-movedown", this); moveDownShortcut->associateAction(mMoveDownAction); - mViewAction = new QAction (tr ("View"), this); - connect (mViewAction, SIGNAL (triggered()), this, SLOT (viewRecord())); - mViewAction->setIcon(QIcon(":/cell.png")); - addAction (mViewAction); + mViewAction = new QAction(tr("View"), this); + connect(mViewAction, &QAction::triggered, this, &Table::viewRecord); + mViewAction->setIcon(Misc::ScalableIcon::load(":cell")); + addAction(mViewAction); CSMPrefs::Shortcut* viewShortcut = new CSMPrefs::Shortcut("table-view", this); viewShortcut->associateAction(mViewAction); - mPreviewAction = new QAction (tr ("Preview"), this); - connect (mPreviewAction, SIGNAL (triggered()), this, SLOT (previewRecord())); - mPreviewAction->setIcon(QIcon(":edit-preview")); - addAction (mPreviewAction); + mPreviewAction = new QAction(tr("Preview"), this); + connect(mPreviewAction, &QAction::triggered, this, &Table::previewRecord); + mPreviewAction->setIcon(Misc::ScalableIcon::load(":edit-preview")); + addAction(mPreviewAction); CSMPrefs::Shortcut* previewShortcut = new CSMPrefs::Shortcut("table-preview", this); previewShortcut->associateAction(mPreviewAction); - mExtendedDeleteAction = new QAction (tr ("Extended Delete Record"), this); - connect (mExtendedDeleteAction, SIGNAL (triggered()), this, SLOT (executeExtendedDelete())); - mExtendedDeleteAction->setIcon(QIcon(":edit-delete")); - addAction (mExtendedDeleteAction); + mExtendedDeleteAction = new QAction(tr("Extended Delete Record"), this); + connect(mExtendedDeleteAction, &QAction::triggered, this, &Table::executeExtendedDelete); + mExtendedDeleteAction->setIcon(Misc::ScalableIcon::load(":edit-delete")); + addAction(mExtendedDeleteAction); CSMPrefs::Shortcut* extendedDeleteShortcut = new CSMPrefs::Shortcut("table-extendeddelete", this); extendedDeleteShortcut->associateAction(mExtendedDeleteAction); - mExtendedRevertAction = new QAction (tr ("Extended Revert Record"), this); - connect (mExtendedRevertAction, SIGNAL (triggered()), this, SLOT (executeExtendedRevert())); - mExtendedRevertAction->setIcon(QIcon(":edit-undo")); - addAction (mExtendedRevertAction); + mExtendedRevertAction = new QAction(tr("Extended Revert Record"), this); + connect(mExtendedRevertAction, &QAction::triggered, this, &Table::executeExtendedRevert); + mExtendedRevertAction->setIcon(Misc::ScalableIcon::load(":edit-undo")); + addAction(mExtendedRevertAction); CSMPrefs::Shortcut* extendedRevertShortcut = new CSMPrefs::Shortcut("table-extendedrevert", this); extendedRevertShortcut->associateAction(mExtendedRevertAction); - mEditIdAction = new TableEditIdAction (*this, this); - connect (mEditIdAction, SIGNAL (triggered()), this, SLOT (editCell())); - addAction (mEditIdAction); + mEditIdAction = new TableEditIdAction(*this, this); + connect(mEditIdAction, &QAction::triggered, this, &Table::editCell); + addAction(mEditIdAction); - mHelpAction = new QAction (tr ("Help"), this); - connect (mHelpAction, SIGNAL (triggered()), this, SLOT (openHelp())); - mHelpAction->setIcon(QIcon(":/info.png")); - addAction (mHelpAction); + mHelpAction = new QAction(tr("Help"), this); + connect(mHelpAction, &QAction::triggered, this, &Table::openHelp); + mHelpAction->setIcon(Misc::ScalableIcon::load(":info")); + addAction(mHelpAction); CSMPrefs::Shortcut* openHelpShortcut = new CSMPrefs::Shortcut("help", this); openHelpShortcut->associateAction(mHelpAction); - connect (mProxyModel, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), - this, SLOT (tableSizeUpdate())); + connect(mProxyModel, &CSMWorld::IdTableProxyModel::rowsRemoved, this, &Table::tableSizeUpdate); - connect (mProxyModel, SIGNAL (rowAdded (const std::string &)), - this, SLOT (rowAdded (const std::string &))); + connect(mProxyModel, &CSMWorld::IdTableProxyModel::rowAdded, this, &Table::rowAdded); /// \note This signal could instead be connected to a slot that filters out changes not affecting /// the records status column (for permanence reasons) - connect (mProxyModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (tableSizeUpdate())); + connect(mProxyModel, &CSMWorld::IdTableProxyModel::dataChanged, this, &Table::dataChangedEvent); - connect (selectionModel(), SIGNAL (selectionChanged (const QItemSelection&, const QItemSelection&)), - this, SLOT (selectionSizeUpdate ())); + connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &Table::selectionSizeUpdate); setAcceptDrops(true); - mDoubleClickActions.insert (std::make_pair (Qt::NoModifier, Action_InPlaceEdit)); - mDoubleClickActions.insert (std::make_pair (Qt::ShiftModifier, Action_EditRecord)); - mDoubleClickActions.insert (std::make_pair (Qt::ControlModifier, Action_View)); - mDoubleClickActions.insert (std::make_pair (Qt::ShiftModifier | Qt::ControlModifier, Action_EditRecordAndClose)); + mDoubleClickActions.insert(std::make_pair(Qt::NoModifier, Action_InPlaceEdit)); + mDoubleClickActions.insert(std::make_pair(Qt::ShiftModifier, Action_EditRecord)); + mDoubleClickActions.insert(std::make_pair(Qt::ControlModifier, Action_View)); + mDoubleClickActions.insert(std::make_pair(Qt::ShiftModifier | Qt::ControlModifier, Action_EditRecordAndClose)); - connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), - this, SLOT (settingChanged (const CSMPrefs::Setting *))); + connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &Table::settingChanged); CSMPrefs::get()["ID Tables"].update(); + + new TableHeaderMouseEventHandler(this); } -void CSVWorld::Table::setEditLock (bool locked) +void CSVWorld::Table::setEditLock(bool locked) { - for (std::vector::iterator iter (mDelegates.begin()); iter!=mDelegates.end(); ++iter) - (*iter)->setEditLock (locked); + for (std::vector::iterator iter(mDelegates.begin()); iter != mDelegates.end(); ++iter) + (*iter)->setEditLock(locked); DragRecordTable::setEditLock(locked); - mDispatcher->setEditLock (locked); + mDispatcher->setEditLock(locked); } -CSMWorld::UniversalId CSVWorld::Table::getUniversalId (int row) const +CSMWorld::UniversalId CSVWorld::Table::getUniversalId(int row) const { - row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); + row = mProxyModel->mapToSource(mProxyModel->index(row, 0)).row(); - int idColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); - int typeColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); + int idColumn = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); + int typeColumn = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_RecordType); - return CSMWorld::UniversalId ( - static_cast (mModel->data (mModel->index (row, typeColumn)).toInt()), - mModel->data (mModel->index (row, idColumn)).toString().toUtf8().constData()); + return CSMWorld::UniversalId( + static_cast(mModel->data(mModel->index(row, typeColumn)).toInt()), + mModel->data(mModel->index(row, idColumn)).toString().toUtf8().constData()); } std::vector CSVWorld::Table::getSelectedIds() const { std::vector ids; QModelIndexList selectedRows = selectionModel()->selectedRows(); - int columnIndex = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int columnIndex = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); - for (QModelIndexList::const_iterator iter (selectedRows.begin()); - iter != selectedRows.end(); - ++iter) + for (QModelIndexList::const_iterator iter(selectedRows.begin()); iter != selectedRows.end(); ++iter) { - int row = mProxyModel->mapToSource (mProxyModel->index (iter->row(), 0)).row(); - ids.emplace_back(mModel->data (mModel->index (row, columnIndex)).toString().toUtf8().constData()); + int row = mProxyModel->mapToSource(mProxyModel->index(iter->row(), 0)).row(); + ids.emplace_back(mModel->data(mModel->index(row, columnIndex)).toString().toUtf8().constData()); } return ids; } @@ -465,8 +487,8 @@ void CSVWorld::Table::editRecord() { QModelIndexList selectedRows = selectionModel()->selectedRows(); - if (selectedRows.size()==1) - emit editRequest (getUniversalId (selectedRows.begin()->row()), ""); + if (selectedRows.size() == 1) + emit editRequest(getUniversalId(selectedRows.begin()->row()), ""); } } @@ -478,7 +500,7 @@ void CSVWorld::Table::cloneRecord() const CSMWorld::UniversalId& toClone = getUniversalId(selectedRows.begin()->row()); if (selectedRows.size() == 1) { - emit cloneRequest (toClone); + emit cloneRequest(toClone); } } } @@ -506,28 +528,28 @@ void CSVWorld::Table::moveUpRecord() QModelIndexList selectedRows = selectionModel()->selectedRows(); - if (selectedRows.size()==1) + if (selectedRows.size() == 1) { - int row2 =selectedRows.begin()->row(); + int row2 = selectedRows.begin()->row(); - if (row2>0) + if (row2 > 0) { - int row = row2-1; + int row = row2 - 1; - row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); - row2 = mProxyModel->mapToSource (mProxyModel->index (row2, 0)).row(); + row = mProxyModel->mapToSource(mProxyModel->index(row, 0)).row(); + row2 = mProxyModel->mapToSource(mProxyModel->index(row2, 0)).row(); - if (row2<=row) - throw std::runtime_error ("Inconsistent row order"); + if (row2 <= row) + throw std::runtime_error("Inconsistent row order"); - std::vector newOrder (row2-row+1); - newOrder[0] = row2-row; - newOrder[row2-row] = 0; - for (int i=1; i newOrder(row2 - row + 1); + newOrder[0] = row2 - row; + newOrder[row2 - row] = 0; + for (int i = 1; i < row2 - row; ++i) newOrder[i] = i; - mDocument.getUndoStack().push (new CSMWorld::ReorderRowsCommand ( - dynamic_cast (*mModel), row, newOrder)); + mDocument.getUndoStack().push( + new CSMWorld::ReorderRowsCommand(dynamic_cast(*mModel), row, newOrder)); } } } @@ -539,33 +561,33 @@ void CSVWorld::Table::moveDownRecord() QModelIndexList selectedRows = selectionModel()->selectedRows(); - if (selectedRows.size()==1) + if (selectedRows.size() == 1) { - int row =selectedRows.begin()->row(); + int row = selectedRows.begin()->row(); - if (rowrowCount()-1) + if (row < mProxyModel->rowCount() - 1) { - int row2 = row+1; + int row2 = row + 1; - row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); - row2 = mProxyModel->mapToSource (mProxyModel->index (row2, 0)).row(); + row = mProxyModel->mapToSource(mProxyModel->index(row, 0)).row(); + row2 = mProxyModel->mapToSource(mProxyModel->index(row2, 0)).row(); - if (row2<=row) - throw std::runtime_error ("Inconsistent row order"); + if (row2 <= row) + throw std::runtime_error("Inconsistent row order"); - std::vector newOrder (row2-row+1); - newOrder[0] = row2-row; - newOrder[row2-row] = 0; - for (int i=1; i newOrder(row2 - row + 1); + newOrder[0] = row2 - row; + newOrder[row2 - row] = 0; + for (int i = 1; i < row2 - row; ++i) newOrder[i] = i; - mDocument.getUndoStack().push (new CSMWorld::ReorderRowsCommand ( - dynamic_cast (*mModel), row, newOrder)); + mDocument.getUndoStack().push( + new CSMWorld::ReorderRowsCommand(dynamic_cast(*mModel), row, newOrder)); } } } -void CSVWorld::Table::moveRecords(QDropEvent *event) +void CSVWorld::Table::moveRecords(QDropEvent* event) { if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) return; @@ -574,21 +596,23 @@ void CSVWorld::Table::moveRecords(QDropEvent *event) QModelIndexList selectedRows = selectionModel()->selectedRows(); int targetRowRaw = targedIndex.row(); - int targetRow = mProxyModel->mapToSource (mProxyModel->index (targetRowRaw, 0)).row(); + int targetRow = mProxyModel->mapToSource(mProxyModel->index(targetRowRaw, 0)).row(); int baseRowRaw = targedIndex.row() - 1; - int baseRow = mProxyModel->mapToSource (mProxyModel->index (baseRowRaw, 0)).row(); + int baseRow = mProxyModel->mapToSource(mProxyModel->index(baseRowRaw, 0)).row(); int highestDifference = 0; for (const auto& thisRowData : selectedRows) { - int thisRow = mProxyModel->mapToSource (mProxyModel->index (thisRowData.row(), 0)).row(); - if (std::abs(targetRow - thisRow) > highestDifference) highestDifference = std::abs(targetRow - thisRow); - if (thisRow - 1 < baseRow) baseRow = thisRow - 1; + int thisRow = mProxyModel->mapToSource(mProxyModel->index(thisRowData.row(), 0)).row(); + if (std::abs(targetRow - thisRow) > highestDifference) + highestDifference = std::abs(targetRow - thisRow); + if (thisRow - 1 < baseRow) + baseRow = thisRow - 1; } - std::vector newOrder (highestDifference + 1); + std::vector newOrder(highestDifference + 1); - for (long unsigned int i = 0; i < newOrder.size(); ++i) + for (int i = 0; i <= highestDifference; ++i) { newOrder[i] = i; } @@ -611,9 +635,9 @@ void CSVWorld::Table::moveRecords(QDropEvent *event) */ int originRowRaw = thisRowData.row(); - int originRow = mProxyModel->mapToSource (mProxyModel->index (originRowRaw, 0)).row(); + int originRow = mProxyModel->mapToSource(mProxyModel->index(originRowRaw, 0)).row(); - newOrder.erase(newOrder.begin() + originRow - baseRow - 1); + newOrder.erase(newOrder.begin() + originRow - baseRow - 1); newOrder.emplace(newOrder.begin() + originRow - baseRow - 1, targetRow - baseRow - 1); if (originRow > targetRow) @@ -630,10 +654,9 @@ void CSVWorld::Table::moveRecords(QDropEvent *event) --newOrder[i]; } } - } - mDocument.getUndoStack().push (new CSMWorld::ReorderRowsCommand ( - dynamic_cast (*mModel), baseRow + 1, newOrder)); + mDocument.getUndoStack().push( + new CSMWorld::ReorderRowsCommand(dynamic_cast(*mModel), baseRow + 1, newOrder)); } void CSVWorld::Table::editCell() @@ -653,16 +676,16 @@ void CSVWorld::Table::viewRecord() QModelIndexList selectedRows = selectionModel()->selectedRows(); - if (selectedRows.size()==1) + if (selectedRows.size() == 1) { int row = selectedRows.begin()->row(); - row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); + row = mProxyModel->mapToSource(mProxyModel->index(row, 0)).row(); - std::pair params = mModel->view (row); + std::pair params = mModel->view(row); - if (params.first.getType()!=CSMWorld::UniversalId::Type_None) - emit editRequest (params.first, params.second); + if (params.first.getType() != CSMWorld::UniversalId::Type_None) + emit editRequest(params.first, params.second); } } @@ -670,16 +693,15 @@ void CSVWorld::Table::previewRecord() { QModelIndexList selectedRows = selectionModel()->selectedRows(); - if (selectedRows.size()==1) + if (selectedRows.size() == 1) { - std::string id = getUniversalId (selectedRows.begin()->row()).getId(); + CSMWorld::UniversalId id = getUniversalId(selectedRows.begin()->row()); - QModelIndex index = mModel->getModelIndex (id, - mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); + QModelIndex index + = mModel->getModelIndex(id.getId(), mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Modification)); - if (mModel->data (index)!=CSMWorld::RecordBase::State_Deleted) - emit editRequest (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Preview, id), - ""); + if (mModel->data(index) != CSMWorld::RecordBase::State_Deleted) + emit editRequest(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Preview, id), ""); } } @@ -707,16 +729,16 @@ void CSVWorld::Table::executeExtendedRevert() } } -void CSVWorld::Table::settingChanged (const CSMPrefs::Setting *setting) +void CSVWorld::Table::settingChanged(const CSMPrefs::Setting* setting) { - if (*setting=="ID Tables/jump-to-added") + if (*setting == "ID Tables/jump-to-added") { - if (setting->toString()=="Jump and Select") + if (setting->toString() == "Jump and Select") { mJumpToAddedRecord = true; mUnselectAfterJump = false; } - else if (setting->toString()=="Jump Only") + else if (setting->toString() == "Jump Only") { mJumpToAddedRecord = true; mUnselectAfterJump = true; @@ -727,49 +749,47 @@ void CSVWorld::Table::settingChanged (const CSMPrefs::Setting *setting) mUnselectAfterJump = false; } } - else if (*setting=="Records/type-format" || *setting=="Records/status-format") + else if (*setting == "Records/type-format" || *setting == "Records/status-format") { int columns = mModel->columnCount(); - for (int i=0; i (*delegate).settingChanged (setting); - emit dataChanged (mModel->index (0, i), - mModel->index (mModel->rowCount()-1, i)); + dynamic_cast(*delegate).settingChanged(setting); + emit dataChanged(mModel->index(0, i), mModel->index(mModel->rowCount() - 1, i)); } } - else if (setting->getParent()->getKey()=="ID Tables" && - setting->getKey().substr (0, 6)=="double") + else if (setting->getParent()->getKey() == "ID Tables" && setting->getKey().substr(0, 6) == "double") { - std::string modifierString = setting->getKey().substr (6); + std::string modifierString = setting->getKey().substr(6); Qt::KeyboardModifiers modifiers; - if (modifierString=="-s") + if (modifierString == "-s") modifiers = Qt::ShiftModifier; - else if (modifierString=="-c") + else if (modifierString == "-c") modifiers = Qt::ControlModifier; - else if (modifierString=="-sc") + else if (modifierString == "-sc") modifiers = Qt::ShiftModifier | Qt::ControlModifier; DoubleClickAction action = Action_None; std::string value = setting->toString(); - if (value=="Edit in Place") + if (value == "Edit in Place") action = Action_InPlaceEdit; - else if (value=="Edit Record") + else if (value == "Edit Record") action = Action_EditRecord; - else if (value=="View") + else if (value == "View") action = Action_View; - else if (value=="Revert") + else if (value == "Revert") action = Action_Revert; - else if (value=="Delete") + else if (value == "Delete") action = Action_Delete; - else if (value=="Edit Record and Close") + else if (value == "Edit Record and Close") action = Action_EditRecordAndClose; - else if (value=="View and Close") + else if (value == "View and Close") action = Action_ViewAndClose; mDoubleClickActions[modifiers] = action; @@ -782,26 +802,37 @@ void CSVWorld::Table::tableSizeUpdate() int deleted = 0; int modified = 0; - if (mProxyModel->columnCount()>0) + if (mProxyModel->columnCount() > 0) { int rows = mProxyModel->rowCount(); - int columnIndex = mModel->searchColumnIndex (CSMWorld::Columns::ColumnId_Modification); + int columnIndex = mModel->searchColumnIndex(CSMWorld::Columns::ColumnId_Modification); - if (columnIndex!=-1) + if (columnIndex != -1) { - for (int i=0; imapToSource (mProxyModel->index (i, 0)); + QModelIndex index = mProxyModel->mapToSource(mProxyModel->index(i, 0)); - int state = mModel->data (mModel->index (index.row(), columnIndex)).toInt(); + int state = mModel->data(mModel->index(index.row(), columnIndex)).toInt(); switch (state) { - case CSMWorld::RecordBase::State_BaseOnly: ++size; break; - case CSMWorld::RecordBase::State_Modified: ++size; ++modified; break; - case CSMWorld::RecordBase::State_ModifiedOnly: ++size; ++modified; break; - case CSMWorld::RecordBase:: State_Deleted: ++deleted; ++modified; break; + case CSMWorld::RecordBase::State_BaseOnly: + ++size; + break; + case CSMWorld::RecordBase::State_Modified: + ++size; + ++modified; + break; + case CSMWorld::RecordBase::State_ModifiedOnly: + ++size; + ++modified; + break; + case CSMWorld::RecordBase::State_Deleted: + ++deleted; + ++modified; + break; } } } @@ -809,39 +840,39 @@ void CSVWorld::Table::tableSizeUpdate() size = rows; } - emit tableSizeChanged (size, deleted, modified); + emit tableSizeChanged(size, deleted, modified); } void CSVWorld::Table::selectionSizeUpdate() { - emit selectionSizeChanged (selectionModel()->selectedRows().size()); + emit selectionSizeChanged(selectionModel()->selectedRows().size()); } -void CSVWorld::Table::requestFocus (const std::string& id) +void CSVWorld::Table::requestFocus(const std::string& id) { - QModelIndex index = mProxyModel->getModelIndex (id, 0); + QModelIndex index = mProxyModel->getModelIndex(id, 0); if (index.isValid()) { // This will scroll to the row. - selectRow (index.row()); + selectRow(index.row()); // This will actually select it. - selectionModel()->select (index, QItemSelectionModel::Select | QItemSelectionModel::Rows); + selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); } } -void CSVWorld::Table::recordFilterChanged (std::shared_ptr filter) +void CSVWorld::Table::recordFilterChanged(std::shared_ptr filter) { - mProxyModel->setFilter (filter); + mProxyModel->setFilter(filter); tableSizeUpdate(); selectionSizeUpdate(); } -void CSVWorld::Table::mouseMoveEvent (QMouseEvent* event) +void CSVWorld::Table::mouseMoveEvent(QMouseEvent* event) { if (event->buttons() & Qt::LeftButton) { - startDragFromTable(*this); + startDragFromTable(*this, indexAt(event->pos())); } } @@ -852,37 +883,69 @@ std::vector CSVWorld::Table::getColumnsWithDisplay(CSMWorld::Column std::vector titles; for (int i = 0; i < count; ++i) { - CSMWorld::ColumnBase::Display columndisplay = static_cast - (mModel->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); + CSMWorld::ColumnBase::Display columndisplay = static_cast( + mModel->headerData(i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); if (display == columndisplay) { - titles.emplace_back(mModel->headerData (i, Qt::Horizontal).toString().toUtf8().constData()); + titles.emplace_back(mModel->headerData(i, Qt::Horizontal).toString().toUtf8().constData()); } } return titles; } -std::vector< CSMWorld::UniversalId > CSVWorld::Table::getDraggedRecords() const +std::vector CSVWorld::Table::getDraggedRecords() const { QModelIndexList selectedRows = selectionModel()->selectedRows(); std::vector idToDrag; for (QModelIndex& it : selectedRows) - idToDrag.push_back (getUniversalId (it.row())); + idToDrag.push_back(getUniversalId(it.row())); return idToDrag; } -void CSVWorld::Table::rowAdded(const std::string &id) +// parent, start and end depend on the model sending the signal, in this case mProxyModel +// +// If, for example, mModel was used instead, then scrolTo() should use the index +// mProxyModel->mapFromSource(mModel->index(end, 0)) +void CSVWorld::Table::rowAdded(const std::string& id) { tableSizeUpdate(); - if(mJumpToAddedRecord) + if (mJumpToAddedRecord) { int idColumn = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); - selectRow(mProxyModel->getModelIndex(id, idColumn).row()); + int end = mProxyModel->getModelIndex(id, idColumn).row(); + selectRow(end); + + // without this delay the scroll works but goes to top for add/clone + QMetaObject::invokeMethod(this, "queuedScrollTo", Qt::QueuedConnection, Q_ARG(int, end)); - if(mUnselectAfterJump) + if (mUnselectAfterJump) clearSelection(); } } + +void CSVWorld::Table::queuedScrollTo(int row) +{ + scrollTo(mProxyModel->index(row, 0), QAbstractItemView::PositionAtCenter); +} + +void CSVWorld::Table::dataChangedEvent(const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + tableSizeUpdate(); + + if (mAutoJump) + { + selectRow(bottomRight.row()); + scrollTo(bottomRight, QAbstractItemView::PositionAtCenter); + } +} + +void CSVWorld::Table::jumpAfterModChanged(int state) +{ + if (state == Qt::Checked) + mAutoJump = true; + else + mAutoJump = false; +} diff --git a/apps/opencs/view/world/table.hpp b/apps/opencs/view/world/table.hpp index 9c4b9b5e5a0..e7a30f4a282 100644 --- a/apps/opencs/view/world/table.hpp +++ b/apps/opencs/view/world/table.hpp @@ -1,17 +1,26 @@ #ifndef CSV_WORLD_TABLE_H #define CSV_WORLD_TABLE_H -#include +#include +#include #include +#include -#include - -#include "../../model/filter/node.hpp" #include "../../model/world/columnbase.hpp" #include "../../model/world/universalid.hpp" #include "dragrecordtable.hpp" class QAction; +class QContextMenuEvent; +class QDropEvent; +class QModelIndex; +class QMouseEvent; +class QObject; + +namespace CSMFilter +{ + class Node; +} namespace CSMDoc { @@ -38,132 +47,137 @@ namespace CSVWorld ///< Table widget class Table : public DragRecordTable { - Q_OBJECT + Q_OBJECT + + enum DoubleClickAction + { + Action_None, + Action_InPlaceEdit, + Action_EditRecord, + Action_View, + Action_Revert, + Action_Delete, + Action_EditRecordAndClose, + Action_ViewAndClose + }; - enum DoubleClickAction - { - Action_None, - Action_InPlaceEdit, - Action_EditRecord, - Action_View, - Action_Revert, - Action_Delete, - Action_EditRecordAndClose, - Action_ViewAndClose - }; + std::vector mDelegates; + QAction* mEditAction; + QAction* mCreateAction; + QAction* mCloneAction; + QAction* mTouchAction; + QAction* mRevertAction; + QAction* mDeleteAction; + QAction* mMoveUpAction; + QAction* mMoveDownAction; + QAction* mViewAction; + QAction* mPreviewAction; + QAction* mExtendedDeleteAction; + QAction* mExtendedRevertAction; + QAction* mHelpAction; + TableEditIdAction* mEditIdAction; + CSMWorld::IdTableProxyModel* mProxyModel; + CSMWorld::IdTableBase* mModel; + int mRecordStatusDisplay; + CSMWorld::CommandDispatcher* mDispatcher; + std::map mDoubleClickActions; + bool mJumpToAddedRecord; + bool mUnselectAfterJump; + bool mAutoJump; - std::vector mDelegates; - QAction *mEditAction; - QAction *mCreateAction; - QAction *mCloneAction; - QAction *mTouchAction; - QAction *mRevertAction; - QAction *mDeleteAction; - QAction *mMoveUpAction; - QAction *mMoveDownAction; - QAction *mViewAction; - QAction *mPreviewAction; - QAction *mExtendedDeleteAction; - QAction *mExtendedRevertAction; - QAction *mHelpAction; - TableEditIdAction *mEditIdAction; - CSMWorld::IdTableProxyModel *mProxyModel; - CSMWorld::IdTableBase *mModel; - int mRecordStatusDisplay; - CSMWorld::CommandDispatcher *mDispatcher; - std::map mDoubleClickActions; - bool mJumpToAddedRecord; - bool mUnselectAfterJump; + private: + void contextMenuEvent(QContextMenuEvent* event) override; - private: + void mouseMoveEvent(QMouseEvent* event) override; - void contextMenuEvent (QContextMenuEvent *event) override; + protected: + void mouseDoubleClickEvent(QMouseEvent* event) override; - void mouseMoveEvent(QMouseEvent *event) override; + public: + Table(const CSMWorld::UniversalId& id, bool createAndDelete, bool sorting, CSMDoc::Document& document); + ///< \param createAndDelete Allow creation and deletion of records. + /// \param sorting Allow changing order of rows in the view via column headers. - protected: + virtual void setEditLock(bool locked); - void mouseDoubleClickEvent (QMouseEvent *event) override; + CSMWorld::UniversalId getUniversalId(int row) const; - public: + std::vector getColumnsWithDisplay(CSMWorld::ColumnBase::Display display) const; - Table (const CSMWorld::UniversalId& id, bool createAndDelete, - bool sorting, CSMDoc::Document& document); - ///< \param createAndDelete Allow creation and deletion of records. - /// \param sorting Allow changing order of rows in the view via column headers. + std::vector getSelectedIds() const; - virtual void setEditLock (bool locked); + std::vector getDraggedRecords() const override; - CSMWorld::UniversalId getUniversalId (int row) const; + signals: - std::vector getColumnsWithDisplay(CSMWorld::ColumnBase::Display display) const; + void editRequest(const CSMWorld::UniversalId& id, const std::string& hint); - std::vector getSelectedIds() const; + void selectionSizeChanged(int size); - std::vector getDraggedRecords() const override; + void tableSizeChanged(int size, int deleted, int modified); + ///< \param size Number of not deleted records + /// \param deleted Number of deleted records + /// \param modified Number of added and modified records - signals: + void createRequest(); - void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); + void createRecordsDirectlyRequest(const std::string& id); - void selectionSizeChanged (int size); + void cloneRequest(const CSMWorld::UniversalId&); - void tableSizeChanged (int size, int deleted, int modified); - ///< \param size Number of not deleted records - /// \param deleted Number of deleted records - /// \param modified Number of added and modified records + void touchRequest(const std::vector& ids); - void createRequest(); + void closeRequest(); - void cloneRequest(const CSMWorld::UniversalId&); + void extendedDeleteConfigRequest(const std::vector& selectedIds); - void touchRequest(const std::vector& ids); + void extendedRevertConfigRequest(const std::vector& selectedIds); - void closeRequest(); + private slots: - void extendedDeleteConfigRequest(const std::vector &selectedIds); + void editCell(); - void extendedRevertConfigRequest(const std::vector &selectedIds); + static void openHelp(); - private slots: + void editRecord(); - void editCell(); + void cloneRecord(); - static void openHelp(); + void touchRecord(); - void editRecord(); + void moveUpRecord(); - void cloneRecord(); + void moveDownRecord(); - void touchRecord(); + void moveRecords(QDropEvent* event); - void moveUpRecord(); + void viewRecord(); - void moveDownRecord(); + void previewRecord(); - void moveRecords(QDropEvent *event); + void executeExtendedDelete(); - void viewRecord(); + void executeExtendedRevert(); - void previewRecord(); + public slots: - void executeExtendedDelete(); + void settingChanged(const CSMPrefs::Setting* setting); - void executeExtendedRevert(); + void tableSizeUpdate(); - public slots: + void selectionSizeUpdate(); - void settingChanged (const CSMPrefs::Setting *setting); + void requestFocus(const std::string& id); - void tableSizeUpdate(); + void recordFilterChanged(std::shared_ptr filter); - void selectionSizeUpdate(); + void rowAdded(const std::string& id); - void requestFocus (const std::string& id); + void dataChangedEvent(const QModelIndex& topLeft, const QModelIndex& bottomRight); - void recordFilterChanged (std::shared_ptr filter); + void jumpAfterModChanged(int state); - void rowAdded(const std::string &id); + void queuedScrollTo(int state); }; } diff --git a/apps/opencs/view/world/tablebottombox.cpp b/apps/opencs/view/world/tablebottombox.cpp index 1b065da49ec..396ba2de339 100644 --- a/apps/opencs/view/world/tablebottombox.cpp +++ b/apps/opencs/view/world/tablebottombox.cpp @@ -2,13 +2,23 @@ #include -#include -#include -#include #include #include +#include +#include +#include + +#include + +#include #include "creator.hpp" +#include "infocreator.hpp" + +namespace +{ + constexpr const char* statusBarStyle = "QStatusBar::item { border: 0px }"; +} void CSVWorld::TableBottomBox::updateSize() { @@ -30,29 +40,27 @@ void CSVWorld::TableBottomBox::updateStatus() { if (!mStatusMessage.isEmpty()) { - mStatus->setText (mStatusMessage); + mStatus->setText(mStatusMessage); return; } - static const char *sLabels[4] = { "record", "deleted", "touched", "selected" }; - static const char *sLabelsPlural[4] = { "records", "deleted", "touched", "selected" }; + static const char* sLabels[4] = { "record", "deleted", "touched", "selected" }; + static const char* sLabelsPlural[4] = { "records", "deleted", "touched", "selected" }; std::ostringstream stream; bool first = true; - for (int i=0; i<4; ++i) + for (int i = 0; i < 4; ++i) { - if (mStatusCount[i]>0) + if (mStatusCount[i] > 0) { if (first) first = false; else stream << ", "; - stream - << mStatusCount[i] << ' ' - << (mStatusCount[i]==1 ? sLabels[i] : sLabelsPlural[i]); + stream << mStatusCount[i] << ' ' << (mStatusCount[i] == 1 ? sLabels[i] : sLabelsPlural[i]); } } @@ -64,71 +72,86 @@ void CSVWorld::TableBottomBox::updateStatus() stream << "(" << mRow << ", " << mColumn << ")"; } - mStatus->setText (QString::fromUtf8 (stream.str().c_str())); + mStatus->setText(QString::fromUtf8(stream.str().c_str())); } } -void CSVWorld::TableBottomBox::extendedConfigRequest(CSVWorld::ExtendedCommandConfigurator::Mode mode, - const std::vector &selectedIds) +void CSVWorld::TableBottomBox::extendedConfigRequest( + CSVWorld::ExtendedCommandConfigurator::Mode mode, const std::vector& selectedIds) { - mExtendedConfigurator->configure (mode, selectedIds); - mLayout->setCurrentWidget (mExtendedConfigurator); + mExtendedConfigurator->configure(mode, selectedIds); + mLayout->setCurrentWidget(mExtendedConfigurator); mEditMode = EditMode_ExtendedConfig; - setVisible (true); + setVisible(true); mExtendedConfigurator->setFocus(); } -CSVWorld::TableBottomBox::TableBottomBox (const CreatorFactoryBase& creatorFactory, - CSMDoc::Document& document, - const CSMWorld::UniversalId& id, - QWidget *parent) -: QWidget (parent), mShowStatusBar (false), mEditMode(EditMode_None), mHasPosition(false), mRow(0), mColumn(0) +CSVWorld::TableBottomBox::TableBottomBox(const CreatorFactoryBase& creatorFactory, CSMDoc::Document& document, + const CSMWorld::UniversalId& id, QWidget* parent) + : QWidget(parent) + , mShowStatusBar(false) + , mEditMode(EditMode_None) + , mHasPosition(false) + , mRow(0) + , mColumn(0) { - for (int i=0; i<4; ++i) + for (int i = 0; i < 4; ++i) mStatusCount[i] = 0; - setVisible (false); + setVisible(false); mLayout = new QStackedLayout; - mLayout->setContentsMargins (0, 0, 0, 0); - connect (mLayout, SIGNAL (currentChanged (int)), this, SLOT (currentWidgetChanged (int))); + mLayout->setContentsMargins(0, 0, 0, 0); + connect(mLayout, &QStackedLayout::currentChanged, this, &TableBottomBox::currentWidgetChanged); mStatus = new QLabel; mStatusBar = new QStatusBar(this); - mStatusBar->addWidget (mStatus); + mStatusBar->addWidget(mStatus); + mStatusBar->setStyleSheet(statusBarStyle); - mLayout->addWidget (mStatusBar); + mLayout->addWidget(mStatusBar); - setLayout (mLayout); + setLayout(mLayout); - mCreator = creatorFactory.makeCreator (document, id); + mCreator = creatorFactory.makeCreator(document, id); if (mCreator) { mCreator->installEventFilter(this); - mLayout->addWidget (mCreator); + mLayout->addWidget(mCreator); - connect (mCreator, SIGNAL (done()), this, SLOT (requestDone())); + connect(mCreator, &Creator::done, this, &TableBottomBox::requestDone); - connect (mCreator, SIGNAL (requestFocus (const std::string&)), - this, SIGNAL (requestFocus (const std::string&))); + connect(mCreator, &Creator::requestFocus, this, &TableBottomBox::requestFocus); } - mExtendedConfigurator = new ExtendedCommandConfigurator (document, id, this); + mExtendedConfigurator = new ExtendedCommandConfigurator(document, id, this); mExtendedConfigurator->installEventFilter(this); - mLayout->addWidget (mExtendedConfigurator); - connect (mExtendedConfigurator, SIGNAL (done()), this, SLOT (requestDone())); + mLayout->addWidget(mExtendedConfigurator); + connect(mExtendedConfigurator, &ExtendedCommandConfigurator::done, this, &TableBottomBox::requestDone); updateSize(); } -void CSVWorld::TableBottomBox::setEditLock (bool locked) +bool CSVWorld::TableBottomBox::event(QEvent* event) +{ + // Apply style sheet again if style was changed + if (event->type() == QEvent::PaletteChange) + { + if (mStatusBar != nullptr) + mStatusBar->setStyleSheet(statusBarStyle); + } + + return QWidget::event(event); +} + +void CSVWorld::TableBottomBox::setEditLock(bool locked) { if (mCreator) - mCreator->setEditLock (locked); - mExtendedConfigurator->setEditLock (locked); + mCreator->setEditLock(locked); + mExtendedConfigurator->setEditLock(locked); } CSVWorld::TableBottomBox::~TableBottomBox() @@ -136,11 +159,11 @@ CSVWorld::TableBottomBox::~TableBottomBox() delete mCreator; } -bool CSVWorld::TableBottomBox::eventFilter(QObject *object, QEvent *event) +bool CSVWorld::TableBottomBox::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); + QKeyEvent* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Escape) { requestDone(); @@ -150,11 +173,11 @@ bool CSVWorld::TableBottomBox::eventFilter(QObject *object, QEvent *event) return QWidget::eventFilter(object, event); } -void CSVWorld::TableBottomBox::setStatusBar (bool show) +void CSVWorld::TableBottomBox::setStatusBar(bool show) { - if (show!=mShowStatusBar) + if (show != mShowStatusBar) { - setVisible (show || (mEditMode != EditMode_None)); + setVisible(show || (mEditMode != EditMode_None)); mShowStatusBar = show; @@ -171,11 +194,11 @@ bool CSVWorld::TableBottomBox::canCreateAndDelete() const void CSVWorld::TableBottomBox::requestDone() { if (!mShowStatusBar) - setVisible (false); + setVisible(false); else updateStatus(); - mLayout->setCurrentWidget (mStatusBar); + mLayout->setCurrentWidget(mStatusBar); mEditMode = EditMode_None; } @@ -184,15 +207,15 @@ void CSVWorld::TableBottomBox::currentWidgetChanged(int /*index*/) updateSize(); } -void CSVWorld::TableBottomBox::setStatusMessage (const QString& message) +void CSVWorld::TableBottomBox::setStatusMessage(const QString& message) { mStatusMessage = message; updateStatus(); } -void CSVWorld::TableBottomBox::selectionSizeChanged (int size) +void CSVWorld::TableBottomBox::selectionSizeChanged(int size) { - if (mStatusCount[3]!=size) + if (mStatusCount[3] != size) { mStatusMessage = ""; mStatusCount[3] = size; @@ -200,23 +223,23 @@ void CSVWorld::TableBottomBox::selectionSizeChanged (int size) } } -void CSVWorld::TableBottomBox::tableSizeChanged (int size, int deleted, int modified) +void CSVWorld::TableBottomBox::tableSizeChanged(int size, int deleted, int modified) { bool changed = false; - if (mStatusCount[0]!=size) + if (mStatusCount[0] != size) { mStatusCount[0] = size; changed = true; } - if (mStatusCount[1]!=deleted) + if (mStatusCount[1] != deleted) { mStatusCount[1] = deleted; changed = true; } - if (mStatusCount[2]!=modified) + if (mStatusCount[2] != modified) { mStatusCount[2] = modified; changed = true; @@ -229,7 +252,7 @@ void CSVWorld::TableBottomBox::tableSizeChanged (int size, int deleted, int modi } } -void CSVWorld::TableBottomBox::positionChanged (int row, int column) +void CSVWorld::TableBottomBox::positionChanged(int row, int column) { mRow = row; mColumn = column; @@ -247,20 +270,33 @@ void CSVWorld::TableBottomBox::createRequest() { mCreator->reset(); mCreator->toggleWidgets(true); - mLayout->setCurrentWidget (mCreator); - setVisible (true); + mLayout->setCurrentWidget(mCreator); + setVisible(true); mEditMode = EditMode_Creation; mCreator->focus(); } -void CSVWorld::TableBottomBox::cloneRequest(const std::string& id, - const CSMWorld::UniversalId::Type type) +void CSVWorld::TableBottomBox::createRecordsDirectlyRequest(const std::string& id) +{ + if (InfoCreator* creator = dynamic_cast(mCreator)) + { + creator->reset(); + creator->setText(id); + creator->callReturnPressed(); + } + else + { + Log(Debug::Warning) << "Creating a record directly failed."; + } +} + +void CSVWorld::TableBottomBox::cloneRequest(const std::string& id, const CSMWorld::UniversalId::Type type) { mCreator->reset(); mCreator->cloneMode(id, type); mLayout->setCurrentWidget(mCreator); mCreator->toggleWidgets(false); - setVisible (true); + setVisible(true); mEditMode = EditMode_Creation; mCreator->focus(); } @@ -270,12 +306,12 @@ void CSVWorld::TableBottomBox::touchRequest(const std::vectortouch(ids); } -void CSVWorld::TableBottomBox::extendedDeleteConfigRequest(const std::vector &selectedIds) +void CSVWorld::TableBottomBox::extendedDeleteConfigRequest(const std::vector& selectedIds) { extendedConfigRequest(ExtendedCommandConfigurator::Mode_Delete, selectedIds); } -void CSVWorld::TableBottomBox::extendedRevertConfigRequest(const std::vector &selectedIds) +void CSVWorld::TableBottomBox::extendedRevertConfigRequest(const std::vector& selectedIds) { extendedConfigRequest(ExtendedCommandConfigurator::Mode_Revert, selectedIds); } diff --git a/apps/opencs/view/world/tablebottombox.hpp b/apps/opencs/view/world/tablebottombox.hpp index 6ad2dbd821b..193ca330266 100644 --- a/apps/opencs/view/world/tablebottombox.hpp +++ b/apps/opencs/view/world/tablebottombox.hpp @@ -1,7 +1,11 @@ #ifndef CSV_WORLD_BOTTOMBOX_H #define CSV_WORLD_BOTTOMBOX_H +#include +#include + #include + #include #include "extendedcommandconfigurator.hpp" @@ -22,93 +26,96 @@ namespace CSVWorld class TableBottomBox : public QWidget { - Q_OBJECT - - enum EditMode { EditMode_None, EditMode_Creation, EditMode_ExtendedConfig }; + Q_OBJECT - bool mShowStatusBar; - QLabel *mStatus; - QStatusBar *mStatusBar; - int mStatusCount[4]; + enum EditMode + { + EditMode_None, + EditMode_Creation, + EditMode_ExtendedConfig + }; - EditMode mEditMode; - Creator *mCreator; - ExtendedCommandConfigurator *mExtendedConfigurator; + bool mShowStatusBar; + QLabel* mStatus; + QStatusBar* mStatusBar; + int mStatusCount[4]; - QStackedLayout *mLayout; - bool mHasPosition; - int mRow; - int mColumn; - QString mStatusMessage; + EditMode mEditMode; + Creator* mCreator; + ExtendedCommandConfigurator* mExtendedConfigurator; - private: + QStackedLayout* mLayout; + bool mHasPosition; + int mRow; + int mColumn; + QString mStatusMessage; - // not implemented - TableBottomBox (const TableBottomBox&); - TableBottomBox& operator= (const TableBottomBox&); + private: + // not implemented + TableBottomBox(const TableBottomBox&); + TableBottomBox& operator=(const TableBottomBox&); - void updateSize(); + void updateSize(); - void updateStatus(); + void updateStatus(); - void extendedConfigRequest(ExtendedCommandConfigurator::Mode mode, - const std::vector &selectedIds); + void extendedConfigRequest(ExtendedCommandConfigurator::Mode mode, const std::vector& selectedIds); - public: + protected: + bool event(QEvent* event) override; - TableBottomBox (const CreatorFactoryBase& creatorFactory, - CSMDoc::Document& document, - const CSMWorld::UniversalId& id, - QWidget *parent = nullptr); + public: + TableBottomBox(const CreatorFactoryBase& creatorFactory, CSMDoc::Document& document, + const CSMWorld::UniversalId& id, QWidget* parent = nullptr); - virtual ~TableBottomBox(); + ~TableBottomBox() override; - bool eventFilter(QObject *object, QEvent *event) override; + bool eventFilter(QObject* object, QEvent* event) override; - void setEditLock (bool locked); + void setEditLock(bool locked); - void setStatusBar (bool show); + void setStatusBar(bool show); - bool canCreateAndDelete() const; - ///< Is record creation and deletion supported? - /// - /// \note The BotomBox does not partake in the deletion of records. + bool canCreateAndDelete() const; + ///< Is record creation and deletion supported? + /// + /// \note The BotomBox does not partake in the deletion of records. - void setStatusMessage (const QString& message); + void setStatusMessage(const QString& message); - signals: + signals: - void requestFocus (const std::string& id); - ///< Request owner of this box to focus the just created \a id. The owner may - /// ignore this request. + void requestFocus(const std::string& id); + ///< Request owner of this box to focus the just created \a id. The owner may + /// ignore this request. - private slots: + private slots: - void requestDone(); - ///< \note This slot being called does not imply success. + void requestDone(); + ///< \note This slot being called does not imply success. - void currentWidgetChanged(int index); + void currentWidgetChanged(int index); - public slots: + public slots: - void selectionSizeChanged (int size); + void selectionSizeChanged(int size); - void tableSizeChanged (int size, int deleted, int modified); - ///< \param size Number of not deleted records - /// \param deleted Number of deleted records - /// \param modified Number of added and modified records + void tableSizeChanged(int size, int deleted, int modified); + ///< \param size Number of not deleted records + /// \param deleted Number of deleted records + /// \param modified Number of added and modified records - void positionChanged (int row, int column); + void positionChanged(int row, int column); - void noMorePosition(); + void noMorePosition(); - void createRequest(); - void cloneRequest(const std::string& id, - const CSMWorld::UniversalId::Type type); - void touchRequest(const std::vector&); + void createRequest(); + void createRecordsDirectlyRequest(const std::string& id); + void cloneRequest(const std::string& id, const CSMWorld::UniversalId::Type type); + void touchRequest(const std::vector&); - void extendedDeleteConfigRequest(const std::vector &selectedIds); - void extendedRevertConfigRequest(const std::vector &selectedIds); + void extendedDeleteConfigRequest(const std::vector& selectedIds); + void extendedRevertConfigRequest(const std::vector& selectedIds); }; } diff --git a/apps/opencs/view/world/tableeditidaction.cpp b/apps/opencs/view/world/tableeditidaction.cpp index 4dfc537cc84..27064a93c98 100644 --- a/apps/opencs/view/world/tableeditidaction.cpp +++ b/apps/opencs/view/world/tableeditidaction.cpp @@ -2,8 +2,14 @@ #include +#include +#include + #include "../../model/world/tablemimedata.hpp" +#include +#include + CSVWorld::TableEditIdAction::CellData CSVWorld::TableEditIdAction::getCellData(int row, int column) const { QModelIndex index = mTable.model()->index(row, column); @@ -16,11 +22,12 @@ CSVWorld::TableEditIdAction::CellData CSVWorld::TableEditIdAction::getCellData(i return std::make_pair(CSMWorld::ColumnBase::Display_None, ""); } -CSVWorld::TableEditIdAction::TableEditIdAction(const QTableView &table, QWidget *parent) - : QAction(parent), - mTable(table), - mCurrentId(CSMWorld::UniversalId::Type_None) -{} +CSVWorld::TableEditIdAction::TableEditIdAction(const QTableView& table, QWidget* parent) + : QAction(parent) + , mTable(table) + , mCurrentId(CSMWorld::UniversalId::Type_None) +{ +} void CSVWorld::TableEditIdAction::setCell(int row, int column) { @@ -43,7 +50,6 @@ bool CSVWorld::TableEditIdAction::isValidIdCell(int row, int column) const { CellData data = getCellData(row, column); CSMWorld::UniversalId::Type idType = CSMWorld::TableMimeData::convertEnums(data.first); - return CSMWorld::ColumnBase::isId(data.first) && - idType != CSMWorld::UniversalId::Type_None && - !data.second.isEmpty(); + return CSMWorld::ColumnBase::isId(data.first) && idType != CSMWorld::UniversalId::Type_None + && !data.second.isEmpty(); } diff --git a/apps/opencs/view/world/tableeditidaction.hpp b/apps/opencs/view/world/tableeditidaction.hpp index 9fe41b0de25..fb88516bb60 100644 --- a/apps/opencs/view/world/tableeditidaction.hpp +++ b/apps/opencs/view/world/tableeditidaction.hpp @@ -2,6 +2,9 @@ #define CSVWORLD_TABLEEDITIDACTION_HPP #include +#include + +#include #include "../../model/world/columnbase.hpp" #include "../../model/world/universalid.hpp" @@ -12,19 +15,19 @@ namespace CSVWorld { class TableEditIdAction : public QAction { - const QTableView &mTable; - CSMWorld::UniversalId mCurrentId; + const QTableView& mTable; + CSMWorld::UniversalId mCurrentId; - typedef std::pair CellData; - CellData getCellData(int row, int column) const; + typedef std::pair CellData; + CellData getCellData(int row, int column) const; - public: - TableEditIdAction(const QTableView &table, QWidget *parent = nullptr); + public: + TableEditIdAction(const QTableView& table, QWidget* parent = nullptr); - void setCell(int row, int column); + void setCell(int row, int column); - CSMWorld::UniversalId getCurrentId() const; - bool isValidIdCell(int row, int column) const; + CSMWorld::UniversalId getCurrentId() const; + bool isValidIdCell(int row, int column) const; }; } diff --git a/apps/opencs/view/world/tableheadermouseeventhandler.cpp b/apps/opencs/view/world/tableheadermouseeventhandler.cpp new file mode 100644 index 00000000000..dcd2e659a6b --- /dev/null +++ b/apps/opencs/view/world/tableheadermouseeventhandler.cpp @@ -0,0 +1,69 @@ +#include "tableheadermouseeventhandler.hpp" +#include "dragrecordtable.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace CSVWorld +{ + + TableHeaderMouseEventHandler::TableHeaderMouseEventHandler(DragRecordTable* parent) + : QWidget(parent) + , table(*parent) + , header(*table.horizontalHeader()) + { + header.setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); + connect(&header, &QHeaderView::customContextMenuRequested, + [this](const QPoint& position) { showContextMenu(position); }); + + header.viewport()->installEventFilter(this); + } + + bool TableHeaderMouseEventHandler::eventFilter(QObject* tableWatched, QEvent* event) + { + if (event->type() == QEvent::Type::MouseButtonPress) + { + auto& clickEvent = static_cast(*event); + if ((clickEvent.button() == Qt::MiddleButton)) + { + const auto& index = table.indexAt(clickEvent.pos()); + table.setColumnHidden(index.column(), true); + clickEvent.accept(); + return true; + } + } + return false; + } + + void TableHeaderMouseEventHandler::showContextMenu(const QPoint& position) + { + auto& menu{ createContextMenu() }; + menu.popup(header.viewport()->mapToGlobal(position)); + } + + QMenu& TableHeaderMouseEventHandler::createContextMenu() + { + auto* menu = new QMenu(this); + for (int i = 0; i < table.model()->columnCount(); ++i) + { + const auto& name = table.model()->headerData(i, Qt::Horizontal, Qt::DisplayRole); + QAction* action{ new QAction(name.toString(), this) }; + action->setCheckable(true); + action->setChecked(!table.isColumnHidden(i)); + menu->addAction(action); + + connect(action, &QAction::triggered, [this, action, i]() { + table.setColumnHidden(i, !action->isChecked()); + action->setChecked(!action->isChecked()); + action->toggle(); + }); + } + return *menu; + } + +} // namespace CSVWorld diff --git a/apps/opencs/view/world/tableheadermouseeventhandler.hpp b/apps/opencs/view/world/tableheadermouseeventhandler.hpp new file mode 100644 index 00000000000..bef0b3389a2 --- /dev/null +++ b/apps/opencs/view/world/tableheadermouseeventhandler.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +class QEvent; +class QHeaderView; +class QMenu; +class QObject; +class QPoint; + +namespace CSVWorld +{ + class DragRecordTable; + + class TableHeaderMouseEventHandler : public QWidget + { + public: + explicit TableHeaderMouseEventHandler(DragRecordTable* parent); + + void showContextMenu(const QPoint&); + + private: + DragRecordTable& table; + QHeaderView& header; + + QMenu& createContextMenu(); + bool eventFilter(QObject*, QEvent*) override; + + }; // class TableHeaderMouseEventHandler +} // namespace CSVWorld diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index 5413f87a6e5..e7701ee7784 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -1,57 +1,105 @@ #include "tablesubview.hpp" -#include -#include -#include #include -#include +#include #include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include #include "../../model/doc/document.hpp" #include "../../model/world/tablemimedata.hpp" #include "../doc/sizehint.hpp" #include "../filter/filterbox.hpp" +#include "../filter/filterdata.hpp" #include "table.hpp" #include "tablebottombox.hpp" -#include "creator.hpp" -CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, - const CreatorFactoryBase& creatorFactory, bool sorting) -: SubView (id) +CSVWorld::TableSubView::TableSubView( + const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting) + : SubView(id) + , mShowOptions(false) + , mOptions(0) { - QVBoxLayout *layout = new QVBoxLayout; + QVBoxLayout* layout = new QVBoxLayout; + + layout->addWidget(mBottom = new TableBottomBox(creatorFactory, document, id, this), 0); + + layout->insertWidget(0, mTable = new Table(id, mBottom->canCreateAndDelete(), sorting, document), 2); + + mFilterBox = new CSVFilter::FilterBox(document.getData(), this); + + QHBoxLayout* hLayout = new QHBoxLayout; + hLayout->insertWidget(0, mFilterBox); + + mOptions = new QWidget; + + QHBoxLayout* optHLayout = new QHBoxLayout; + QCheckBox* autoJump = new QCheckBox("Auto Jump"); + autoJump->setToolTip( + "Whether to jump to the modified record." + "\nCan be useful in finding the moved or modified" + "\nobject instance while 3D editing."); + autoJump->setCheckState(Qt::Unchecked); + connect(autoJump, &QCheckBox::stateChanged, mTable, &Table::jumpAfterModChanged); + optHLayout->insertWidget(0, autoJump); + optHLayout->setContentsMargins(QMargins(0, 3, 0, 0)); + mOptions->setLayout(optHLayout); + mOptions->resize(mOptions->width(), mFilterBox->height()); + mOptions->hide(); + + QPushButton* opt = new QPushButton(); + opt->setIcon(Misc::ScalableIcon::load(":startup/configure")); + opt->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + opt->setToolTip("Open additional options for this subview."); + connect(opt, &QPushButton::clicked, this, &TableSubView::toggleOptions); - layout->addWidget (mBottom = - new TableBottomBox (creatorFactory, document, id, this), 0); + QVBoxLayout* buttonLayout = new QVBoxLayout; // work around margin issues + buttonLayout->setContentsMargins(QMargins(0 /*left*/, 3 /*top*/, 3 /*right*/, 0 /*bottom*/)); + buttonLayout->insertWidget(0, opt, 0, Qt::AlignVCenter | Qt::AlignRight); + hLayout->insertWidget(1, mOptions); + hLayout->insertLayout(2, buttonLayout); - layout->insertWidget (0, mTable = - new Table (id, mBottom->canCreateAndDelete(), sorting, document), 2); + layout->insertLayout(0, hLayout); - mFilterBox = new CSVFilter::FilterBox (document.getData(), this); + CSVDoc::SizeHintWidget* widget = new CSVDoc::SizeHintWidget; - layout->insertWidget (0, mFilterBox); + widget->setLayout(layout); - CSVDoc::SizeHintWidget *widget = new CSVDoc::SizeHintWidget; + setWidget(widget); - widget->setLayout (layout); + // Widget position can be negative, we should clamp it. + QPoint position = pos(); + if (position.x() <= 0) + position.setX(0); + if (position.y() <= 0) + position.setY(0); - setWidget (widget); // prefer height of the screen and full width of the table - const QRect rect = QApplication::desktop()->screenGeometry(this); + const QRect rect = QApplication::screenAt(position)->geometry(); int frameHeight = 40; // set a reasonable default - QWidget *topLevel = QApplication::topLevelAt(pos()); + QWidget* topLevel = QApplication::topLevelAt(pos()); if (topLevel) frameHeight = topLevel->frameGeometry().height() - topLevel->height(); - widget->setSizeHint(QSize(mTable->horizontalHeader()->length(), rect.height()-frameHeight)); + widget->setSizeHint(QSize(mTable->horizontalHeader()->length(), rect.height() - frameHeight)); - connect (mTable, SIGNAL (editRequest (const CSMWorld::UniversalId&, const std::string&)), - this, SLOT (editRequest (const CSMWorld::UniversalId&, const std::string&))); + connect(mTable, &Table::editRequest, this, &TableSubView::editRequest); - connect (mTable, SIGNAL (selectionSizeChanged (int)), - mBottom, SLOT (selectionSizeChanged (int))); - connect (mTable, SIGNAL (tableSizeChanged (int, int, int)), - mBottom, SLOT (tableSizeChanged (int, int, int))); + connect(mTable, &Table::selectionSizeChanged, mBottom, &TableBottomBox::selectionSizeChanged); + connect(mTable, &Table::tableSizeChanged, mBottom, &TableBottomBox::tableSizeChanged); mTable->tableSizeUpdate(); mTable->selectionSizeUpdate(); @@ -61,58 +109,53 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D if (mBottom->canCreateAndDelete()) { - connect (mTable, SIGNAL (createRequest()), mBottom, SLOT (createRequest())); + connect(mTable, &Table::createRequest, mBottom, &TableBottomBox::createRequest); - connect (mTable, SIGNAL (cloneRequest(const CSMWorld::UniversalId&)), this, - SLOT(cloneRequest(const CSMWorld::UniversalId&))); + connect( + mTable, &Table::cloneRequest, this, qOverload(&TableSubView::cloneRequest)); - connect (this, SIGNAL(cloneRequest(const std::string&, const CSMWorld::UniversalId::Type)), - mBottom, SLOT(cloneRequest(const std::string&, const CSMWorld::UniversalId::Type))); + connect(this, qOverload(&TableSubView::cloneRequest), + mBottom, &TableBottomBox::cloneRequest); - connect (mTable, SIGNAL(touchRequest(const std::vector&)), - mBottom, SLOT(touchRequest(const std::vector&))); + connect(mTable, &Table::createRecordsDirectlyRequest, mBottom, &TableBottomBox::createRecordsDirectlyRequest); - connect (mTable, SIGNAL(extendedDeleteConfigRequest(const std::vector &)), - mBottom, SLOT(extendedDeleteConfigRequest(const std::vector &))); - connect (mTable, SIGNAL(extendedRevertConfigRequest(const std::vector &)), - mBottom, SLOT(extendedRevertConfigRequest(const std::vector &))); + connect(mTable, &Table::touchRequest, mBottom, &TableBottomBox::touchRequest); + + connect(mTable, &Table::extendedDeleteConfigRequest, mBottom, &TableBottomBox::extendedDeleteConfigRequest); + connect(mTable, &Table::extendedRevertConfigRequest, mBottom, &TableBottomBox::extendedRevertConfigRequest); } - connect (mBottom, SIGNAL (requestFocus (const std::string&)), - mTable, SLOT (requestFocus (const std::string&))); + connect(mBottom, &TableBottomBox::requestFocus, mTable, &Table::requestFocus); - connect (mFilterBox, - SIGNAL (recordFilterChanged (std::shared_ptr)), - mTable, SLOT (recordFilterChanged (std::shared_ptr))); + connect(mFilterBox, &CSVFilter::FilterBox::recordFilterChanged, mTable, &Table::recordFilterChanged); - connect(mFilterBox, SIGNAL(recordDropped(std::vector&, Qt::DropAction)), - this, SLOT(createFilterRequest(std::vector&, Qt::DropAction))); + connect(mFilterBox, &CSVFilter::FilterBox::recordDropped, this, &TableSubView::createFilterRequest); - connect (mTable, SIGNAL (closeRequest()), this, SLOT (closeRequest())); + connect(mTable, &Table::closeRequest, this, qOverload<>(&TableSubView::closeRequest)); } -void CSVWorld::TableSubView::setEditLock (bool locked) +void CSVWorld::TableSubView::setEditLock(bool locked) { - mTable->setEditLock (locked); - mBottom->setEditLock (locked); + mTable->setEditLock(locked); + mBottom->setEditLock(locked); } -void CSVWorld::TableSubView::editRequest (const CSMWorld::UniversalId& id, const std::string& hint) +void CSVWorld::TableSubView::editRequest(const CSMWorld::UniversalId& id, const std::string& hint) { - focusId (id, hint); + focusId(id, hint); } -void CSVWorld::TableSubView::setStatusBar (bool show) +void CSVWorld::TableSubView::setStatusBar(bool show) { - mBottom->setStatusBar (show); + mBottom->setStatusBar(show); } -void CSVWorld::TableSubView::useHint (const std::string& hint) +void CSVWorld::TableSubView::useHint(const std::string& hint) { if (hint.empty()) return; - if (hint[0]=='f' && hint.size()>=2) - mFilterBox->setRecordFilter (hint.substr (2)); + if (hint[0] == 'f' && hint.size() >= 2) + mFilterBox->setRecordFilter(hint.substr(2)); } void CSVWorld::TableSubView::cloneRequest(const CSMWorld::UniversalId& toClone) @@ -120,38 +163,64 @@ void CSVWorld::TableSubView::cloneRequest(const CSMWorld::UniversalId& toClone) emit cloneRequest(toClone.getId(), toClone.getType()); } -void CSVWorld::TableSubView::createFilterRequest (std::vector< CSMWorld::UniversalId>& types, Qt::DropAction action) +void CSVWorld::TableSubView::createFilterRequest(std::vector& types, + const std::pair& columnSearchData, Qt::DropAction action) { - std::vector > > filterSource; - - std::vector refIdColumns = mTable->getColumnsWithDisplay(CSMWorld::TableMimeData::convertEnums(CSMWorld::UniversalId::Type_Referenceable)); + std::vector sourceFilter; + std::vector refIdColumns = mTable->getColumnsWithDisplay( + CSMWorld::TableMimeData::convertEnums(CSMWorld::UniversalId::Type_Referenceable)); bool hasRefIdDisplay = !refIdColumns.empty(); for (std::vector::iterator it(types.begin()); it != types.end(); ++it) { CSMWorld::UniversalId::Type type = it->getType(); std::vector col = mTable->getColumnsWithDisplay(CSMWorld::TableMimeData::convertEnums(type)); - if(!col.empty()) + if (!col.empty()) { - filterSource.emplace_back(it->getId(), col); + CSVFilter::FilterData filterData; + filterData.searchData = it->getId(); + filterData.columns = std::move(col); + sourceFilter.emplace_back(filterData); } - if(hasRefIdDisplay && CSMWorld::TableMimeData::isReferencable(type)) + if (hasRefIdDisplay && CSMWorld::TableMimeData::isReferencable(type)) { - filterSource.emplace_back(it->getId(), refIdColumns); + CSVFilter::FilterData filterData; + filterData.searchData = it->getId(); + filterData.columns = refIdColumns; + sourceFilter.emplace_back(filterData); } } - mFilterBox->createFilterRequest(filterSource, action); + if (!sourceFilter.empty()) + mFilterBox->createFilterRequest(sourceFilter, action); + else + { + std::vector sourceFilterByValue; + + QVariant qData = columnSearchData.first; + std::string searchColumn = columnSearchData.second; + std::vector searchColumns; + searchColumns.emplace_back(searchColumn); + + CSVFilter::FilterData filterData; + filterData.searchData = qData; + filterData.columns = std::move(searchColumns); + + sourceFilterByValue.emplace_back(filterData); + + mFilterBox->createFilterRequest(sourceFilterByValue, action); + } } -bool CSVWorld::TableSubView::eventFilter (QObject* object, QEvent* event) +bool CSVWorld::TableSubView::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::Drop) { if (QDropEvent* drop = dynamic_cast(event)) { - const CSMWorld::TableMimeData* tableMimeData = dynamic_cast(drop->mimeData()); + const CSMWorld::TableMimeData* tableMimeData + = dynamic_cast(drop->mimeData()); if (!tableMimeData) // May happen when non-records (e.g. plain text) are dragged and dropped return false; @@ -166,7 +235,21 @@ bool CSVWorld::TableSubView::eventFilter (QObject* object, QEvent* event) return false; } -void CSVWorld::TableSubView::requestFocus (const std::string& id) +void CSVWorld::TableSubView::toggleOptions() +{ + if (mShowOptions) + { + mShowOptions = false; + mOptions->hide(); + } + else + { + mShowOptions = true; + mOptions->show(); + } +} + +void CSVWorld::TableSubView::requestFocus(const std::string& id) { mTable->requestFocus(id); } diff --git a/apps/opencs/view/world/tablesubview.hpp b/apps/opencs/view/world/tablesubview.hpp index 337d2c7621d..967002a93d2 100644 --- a/apps/opencs/view/world/tablesubview.hpp +++ b/apps/opencs/view/world/tablesubview.hpp @@ -3,14 +3,18 @@ #include "../doc/subview.hpp" -#include +#include +#include +#include -class QModelIndex; +#include +#include -namespace CSMWorld -{ - class IdTable; -} +#include + +class QEvent; +class QObject; +class QWidget; namespace CSMDoc { @@ -30,40 +34,41 @@ namespace CSVWorld class TableSubView : public CSVDoc::SubView { - Q_OBJECT - - Table *mTable; - TableBottomBox *mBottom; - CSVFilter::FilterBox *mFilterBox; + Q_OBJECT - public: + Table* mTable; + TableBottomBox* mBottom; + CSVFilter::FilterBox* mFilterBox; + bool mShowOptions; + QWidget* mOptions; - TableSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, - const CreatorFactoryBase& creatorFactory, bool sorting); + public: + TableSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document, + const CreatorFactoryBase& creatorFactory, bool sorting); - void setEditLock (bool locked) override; + void setEditLock(bool locked) override; - void setStatusBar (bool show) override; + void setStatusBar(bool show) override; - void useHint (const std::string& hint) override; + void useHint(const std::string& hint) override; - protected: - bool eventFilter(QObject* object, QEvent *event) override; + protected: + bool eventFilter(QObject* object, QEvent* event) override; - signals: - void cloneRequest(const std::string&, - const CSMWorld::UniversalId::Type); + signals: + void cloneRequest(const std::string&, const CSMWorld::UniversalId::Type); - private slots: + private slots: - void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); - void cloneRequest (const CSMWorld::UniversalId& toClone); - void createFilterRequest(std::vector< CSMWorld::UniversalId >& types, - Qt::DropAction action); + void editRequest(const CSMWorld::UniversalId& id, const std::string& hint); + void cloneRequest(const CSMWorld::UniversalId& toClone); + void createFilterRequest(std::vector& types, + const std::pair& columnSearchData, Qt::DropAction action); + void toggleOptions(); - public slots: + public slots: - void requestFocus (const std::string& id); + void requestFocus(const std::string& id); }; } diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index 58d3d49e445..dfb587cd96b 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -2,19 +2,20 @@ #include #include +#include +#include -#include -#include -#include -#include -#include #include -#include -#include #include +#include +#include +#include +#include + +#include +#include +#include -#include "../../model/world/commands.hpp" -#include "../../model/world/tablemimedata.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../widget/coloreditor.hpp" @@ -23,26 +24,27 @@ #include "dialoguespinbox.hpp" #include "scriptedit.hpp" -CSVWorld::NastyTableModelHack::NastyTableModelHack (QAbstractItemModel& model) -: mModel (model) -{} +CSVWorld::NastyTableModelHack::NastyTableModelHack(QAbstractItemModel& model) + : mModel(model) +{ +} -int CSVWorld::NastyTableModelHack::rowCount (const QModelIndex & parent) const +int CSVWorld::NastyTableModelHack::rowCount(const QModelIndex& parent) const { - return mModel.rowCount (parent); + return mModel.rowCount(parent); } -int CSVWorld::NastyTableModelHack::columnCount (const QModelIndex & parent) const +int CSVWorld::NastyTableModelHack::columnCount(const QModelIndex& parent) const { - return mModel.columnCount (parent); + return mModel.columnCount(parent); } -QVariant CSVWorld::NastyTableModelHack::data (const QModelIndex & index, int role) const +QVariant CSVWorld::NastyTableModelHack::data(const QModelIndex& index, int role) const { - return mModel.data (index, role); + return mModel.data(index, role); } -bool CSVWorld::NastyTableModelHack::setData ( const QModelIndex &index, const QVariant &value, int role) +bool CSVWorld::NastyTableModelHack::setData(const QModelIndex& index, const QVariant& value, int role) { mData = value; return true; @@ -53,16 +55,12 @@ QVariant CSVWorld::NastyTableModelHack::getData() const return mData; } - -CSVWorld::CommandDelegateFactory::~CommandDelegateFactory() {} - - -CSVWorld::CommandDelegateFactoryCollection *CSVWorld::CommandDelegateFactoryCollection::sThis = nullptr; +CSVWorld::CommandDelegateFactoryCollection* CSVWorld::CommandDelegateFactoryCollection::sThis = nullptr; CSVWorld::CommandDelegateFactoryCollection::CommandDelegateFactoryCollection() { if (sThis) - throw std::logic_error ("multiple instances of CSVWorld::CommandDelegateFactoryCollection"); + throw std::logic_error("multiple instances of CSVWorld::CommandDelegateFactoryCollection"); sThis = this; } @@ -71,39 +69,37 @@ CSVWorld::CommandDelegateFactoryCollection::~CommandDelegateFactoryCollection() { sThis = nullptr; - for (std::map::iterator iter ( - mFactories.begin()); - iter!=mFactories.end(); ++iter) - delete iter->second; + for (std::map::iterator iter(mFactories.begin()); + iter != mFactories.end(); ++iter) + delete iter->second; } -void CSVWorld::CommandDelegateFactoryCollection::add (CSMWorld::ColumnBase::Display display, - CommandDelegateFactory *factory) +void CSVWorld::CommandDelegateFactoryCollection::add( + CSMWorld::ColumnBase::Display display, CommandDelegateFactory* factory) { - mFactories.insert (std::make_pair (display, factory)); + mFactories.insert(std::make_pair(display, factory)); } -CSVWorld::CommandDelegate *CSVWorld::CommandDelegateFactoryCollection::makeDelegate ( - CSMWorld::ColumnBase::Display display, CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const +CSVWorld::CommandDelegate* CSVWorld::CommandDelegateFactoryCollection::makeDelegate( + CSMWorld::ColumnBase::Display display, CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, + QObject* parent) const { - std::map::const_iterator iter = - mFactories.find (display); + std::map::const_iterator iter = mFactories.find(display); - if (iter!=mFactories.end()) - return iter->second->makeDelegate (dispatcher, document, parent); + if (iter != mFactories.end()) + return iter->second->makeDelegate(dispatcher, document, parent); - return new CommandDelegate (dispatcher, document, parent); + return new CommandDelegate(dispatcher, document, parent); } const CSVWorld::CommandDelegateFactoryCollection& CSVWorld::CommandDelegateFactoryCollection::get() { if (!sThis) - throw std::logic_error ("no instance of CSVWorld::CommandDelegateFactoryCollection"); + throw std::logic_error("no instance of CSVWorld::CommandDelegateFactoryCollection"); return *sThis; } - QUndoStack& CSVWorld::CommandDelegate::getUndoStack() const { return mDocument.getUndoStack(); @@ -114,14 +110,14 @@ CSMDoc::Document& CSVWorld::CommandDelegate::getDocument() const return mDocument; } -CSMWorld::ColumnBase::Display CSVWorld::CommandDelegate::getDisplayTypeFromIndex(const QModelIndex &index) const +CSMWorld::ColumnBase::Display CSVWorld::CommandDelegate::getDisplayTypeFromIndex(const QModelIndex& index) const { int rawDisplay = index.data(CSMWorld::ColumnBase::Role_Display).toInt(); return static_cast(rawDisplay); } -void CSVWorld::CommandDelegate::setModelDataImp (QWidget *editor, QAbstractItemModel *model, - const QModelIndex& index) const +void CSVWorld::CommandDelegate::setModelDataImp( + QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { if (!mCommandDispatcher) return; @@ -129,41 +125,43 @@ void CSVWorld::CommandDelegate::setModelDataImp (QWidget *editor, QAbstractItemM QVariant variant; // Color columns use a custom editor, so we need to fetch selected color from it. - CSVWidget::ColorEditor *colorEditor = qobject_cast(editor); + CSVWidget::ColorEditor* colorEditor = qobject_cast(editor); if (colorEditor != nullptr) { variant = colorEditor->colorInt(); } else { - NastyTableModelHack hack (*model); - QStyledItemDelegate::setModelData (editor, &hack, index); + NastyTableModelHack hack(*model); + QStyledItemDelegate::setModelData(editor, &hack, index); variant = hack.getData(); } - if ((model->data (index)!=variant) && (model->flags(index) & Qt::ItemIsEditable)) - mCommandDispatcher->executeModify (model, index, variant); + if ((model->data(index) != variant) && (model->flags(index) & Qt::ItemIsEditable)) + mCommandDispatcher->executeModify(model, index, variant); } -CSVWorld::CommandDelegate::CommandDelegate (CSMWorld::CommandDispatcher *commandDispatcher, - CSMDoc::Document& document, QObject *parent) -: QStyledItemDelegate (parent), mEditLock (false), - mCommandDispatcher (commandDispatcher), mDocument (document) -{} +CSVWorld::CommandDelegate::CommandDelegate( + CSMWorld::CommandDispatcher* commandDispatcher, CSMDoc::Document& document, QObject* parent) + : QStyledItemDelegate(parent) + , mEditLock(false) + , mCommandDispatcher(commandDispatcher) + , mDocument(document) +{ +} -void CSVWorld::CommandDelegate::setModelData (QWidget *editor, QAbstractItemModel *model, - const QModelIndex& index) const +void CSVWorld::CommandDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { if (!mEditLock) { - setModelDataImp (editor, model, index); + setModelDataImp(editor, model, index); } ///< \todo provide some kind of feedback to the user, indicating that editing is currently not possible. } -QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleOptionViewItem& option, - const QModelIndex& index) const +QWidget* CSVWorld::CommandDelegate::createEditor( + QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { CSMWorld::ColumnBase::Display display = getDisplayTypeFromIndex(index); @@ -181,10 +179,10 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO { return new CSVWidget::ColorEditor(index.data().toInt(), parent, true); } - return createEditor (parent, option, index, display); + return createEditor(parent, option, index, display); } -QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleOptionViewItem& option, +QWidget* CSVWorld::CommandDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index, CSMWorld::ColumnBase::Display display) const { QVariant variant = index.data(); @@ -207,34 +205,34 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO } case CSMWorld::ColumnBase::Display_Integer: { - DialogueSpinBox *sb = new DialogueSpinBox(parent); + DialogueSpinBox* sb = new DialogueSpinBox(parent); sb->setRange(std::numeric_limits::min(), std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_SignedInteger8: { - DialogueSpinBox *sb = new DialogueSpinBox(parent); + DialogueSpinBox* sb = new DialogueSpinBox(parent); sb->setRange(std::numeric_limits::min(), std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_SignedInteger16: { - DialogueSpinBox *sb = new DialogueSpinBox(parent); + DialogueSpinBox* sb = new DialogueSpinBox(parent); sb->setRange(std::numeric_limits::min(), std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_UnsignedInteger8: { - DialogueSpinBox *sb = new DialogueSpinBox(parent); + DialogueSpinBox* sb = new DialogueSpinBox(parent); sb->setRange(0, std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_UnsignedInteger16: { - DialogueSpinBox *sb = new DialogueSpinBox(parent); + DialogueSpinBox* sb = new DialogueSpinBox(parent); sb->setRange(0, std::numeric_limits::max()); return sb; } @@ -245,7 +243,7 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO case CSMWorld::ColumnBase::Display_Float: { - DialogueDoubleSpinBox *dsb = new DialogueDoubleSpinBox(parent); + DialogueDoubleSpinBox* dsb = new DialogueDoubleSpinBox(parent); dsb->setRange(-std::numeric_limits::max(), std::numeric_limits::max()); dsb->setSingleStep(0.01f); dsb->setDecimals(3); @@ -254,7 +252,7 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO case CSMWorld::ColumnBase::Display_Double: { - DialogueDoubleSpinBox *dsb = new DialogueDoubleSpinBox(parent); + DialogueDoubleSpinBox* dsb = new DialogueDoubleSpinBox(parent); dsb->setRange(-std::numeric_limits::max(), std::numeric_limits::max()); dsb->setSingleStep(0.01f); dsb->setDecimals(6); @@ -265,8 +263,8 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO case CSMWorld::ColumnBase::Display_LongString: case CSMWorld::ColumnBase::Display_LongString256: { - QPlainTextEdit *edit = new QPlainTextEdit(parent); - edit->setUndoRedoEnabled (false); + QPlainTextEdit* edit = new QPlainTextEdit(parent); + edit->setUndoRedoEnabled(false); return edit; } @@ -276,28 +274,36 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO case CSMWorld::ColumnBase::Display_ScriptLines: - return new ScriptEdit (mDocument, ScriptHighlighter::Mode_Console, parent); + return new ScriptEdit(mDocument, ScriptHighlighter::Mode_Console, parent); case CSMWorld::ColumnBase::Display_String: - // For other Display types (that represent record IDs) with drop support IdCompletionDelegate is used + // For other Display types (that represent record IDs) with drop support IdCompletionDelegate is used return new CSVWidget::DropLineEdit(display, parent); case CSMWorld::ColumnBase::Display_String32: { - // For other Display types (that represent record IDs) with drop support IdCompletionDelegate is used - CSVWidget::DropLineEdit *widget = new CSVWidget::DropLineEdit(display, parent); - widget->setMaxLength (32); + // For other Display types (that represent record IDs) with drop support IdCompletionDelegate is used + CSVWidget::DropLineEdit* widget = new CSVWidget::DropLineEdit(display, parent); + widget->setMaxLength(32); + return widget; + } + + case CSMWorld::ColumnBase::Display_String64: + { + // For other Display types (that represent record IDs) with drop support IdCompletionDelegate is used + CSVWidget::DropLineEdit* widget = new CSVWidget::DropLineEdit(display, parent); + widget->setMaxLength(64); return widget; } default: - return QStyledItemDelegate::createEditor (parent, option, index); + return QStyledItemDelegate::createEditor(parent, option, index); } } -void CSVWorld::CommandDelegate::setEditLock (bool locked) +void CSVWorld::CommandDelegate::setEditLock(bool locked) { mEditLock = locked; } @@ -307,12 +313,12 @@ bool CSVWorld::CommandDelegate::isEditLocked() const return mEditLock; } -void CSVWorld::CommandDelegate::setEditorData (QWidget *editor, const QModelIndex& index) const +void CSVWorld::CommandDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { - setEditorData (editor, index, false); + setEditorData(editor, index, false); } -void CSVWorld::CommandDelegate::setEditorData (QWidget *editor, const QModelIndex& index, bool tryDisplay) const +void CSVWorld::CommandDelegate::setEditorData(QWidget* editor, const QModelIndex& index, bool tryDisplay) const { QVariant variant = index.data(Qt::EditRole); if (tryDisplay) @@ -326,7 +332,7 @@ void CSVWorld::CommandDelegate::setEditorData (QWidget *editor, const QModelInde } } QPlainTextEdit* plainTextEdit = qobject_cast(editor); - if(plainTextEdit) //for some reason it is easier to brake the loop here + if (plainTextEdit) // for some reason it is easier to brake the loop here { if (plainTextEdit->toPlainText() == variant.toString()) { @@ -336,7 +342,7 @@ void CSVWorld::CommandDelegate::setEditorData (QWidget *editor, const QModelInde } // Color columns use a custom editor, so we need explicitly set a data for it - CSVWidget::ColorEditor *colorEditor = qobject_cast(editor); + CSVWidget::ColorEditor* colorEditor = qobject_cast(editor); if (colorEditor != nullptr) { colorEditor->setColor(variant.toInt()); @@ -356,10 +362,13 @@ void CSVWorld::CommandDelegate::setEditorData (QWidget *editor, const QModelInde if (!n.isEmpty()) { if (!variant.isValid()) - variant = QVariant(editor->property(n).userType(), (const void *)nullptr); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + variant = QVariant(editor->property(n).metaType(), (const void*)nullptr); +#else + variant = QVariant(editor->property(n).userType(), (const void*)nullptr); +#endif editor->setProperty(n, variant); } - } -void CSVWorld::CommandDelegate::settingChanged (const CSMPrefs::Setting *setting) {} +void CSVWorld::CommandDelegate::settingChanged(const CSMPrefs::Setting* setting) {} diff --git a/apps/opencs/view/world/util.hpp b/apps/opencs/view/world/util.hpp index 2c4537dac9e..243608e2e5a 100644 --- a/apps/opencs/view/world/util.hpp +++ b/apps/opencs/view/world/util.hpp @@ -6,18 +6,20 @@ #include #include - #ifndef Q_MOC_RUN #include "../../model/world/columnbase.hpp" -#include "../../model/doc/document.hpp" #endif class QUndoStack; +class QWidget; + +namespace CSMDoc +{ + class Document; +} namespace CSMWorld { - class TableMimeData; - class UniversalId; class CommandDispatcher; } @@ -33,122 +35,108 @@ namespace CSVWorld /// Really, Qt? Really? class NastyTableModelHack : public QAbstractTableModel { - QAbstractItemModel& mModel; - QVariant mData; - - public: + QAbstractItemModel& mModel; + QVariant mData; - NastyTableModelHack (QAbstractItemModel& model); + public: + NastyTableModelHack(QAbstractItemModel& model); - int rowCount (const QModelIndex & parent = QModelIndex()) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; - int columnCount (const QModelIndex & parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - bool setData (const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; - QVariant getData() const; + QVariant getData() const; }; class CommandDelegate; class CommandDelegateFactory { - public: - - virtual ~CommandDelegateFactory(); + public: + virtual ~CommandDelegateFactory() = default; - virtual CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, - CSMDoc::Document& document, QObject *parent) - const = 0; - ///< The ownership of the returned CommandDelegate is transferred to the caller. + virtual CommandDelegate* makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const = 0; + ///< The ownership of the returned CommandDelegate is transferred to the caller. }; class CommandDelegateFactoryCollection { - static CommandDelegateFactoryCollection *sThis; - std::map mFactories; + static CommandDelegateFactoryCollection* sThis; + std::map mFactories; - private: + private: + // not implemented + CommandDelegateFactoryCollection(const CommandDelegateFactoryCollection&); + CommandDelegateFactoryCollection& operator=(const CommandDelegateFactoryCollection&); - // not implemented - CommandDelegateFactoryCollection (const CommandDelegateFactoryCollection&); - CommandDelegateFactoryCollection& operator= (const CommandDelegateFactoryCollection&); + public: + CommandDelegateFactoryCollection(); - public: + ~CommandDelegateFactoryCollection(); - CommandDelegateFactoryCollection(); + void add(CSMWorld::ColumnBase::Display display, CommandDelegateFactory* factory); + ///< The ownership of \a factory is transferred to *this. + /// + /// This function must not be called more than once per value of \a display. - ~CommandDelegateFactoryCollection(); - - void add (CSMWorld::ColumnBase::Display display, CommandDelegateFactory *factory); - ///< The ownership of \a factory is transferred to *this. - /// - /// This function must not be called more than once per value of \a display. - - CommandDelegate *makeDelegate (CSMWorld::ColumnBase::Display display, - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, - QObject *parent) const; - ///< The ownership of the returned CommandDelegate is transferred to the caller. - /// - /// If no factory is registered for \a display, a CommandDelegate will be returned. - - static const CommandDelegateFactoryCollection& get(); + CommandDelegate* makeDelegate(CSMWorld::ColumnBase::Display display, CSMWorld::CommandDispatcher* dispatcher, + CSMDoc::Document& document, QObject* parent) const; + ///< The ownership of the returned CommandDelegate is transferred to the caller. + /// + /// If no factory is registered for \a display, a CommandDelegate will be returned. + static const CommandDelegateFactoryCollection& get(); }; ///< \brief Use commands instead of manipulating the model directly class CommandDelegate : public QStyledItemDelegate { - Q_OBJECT - - bool mEditLock; - CSMWorld::CommandDispatcher *mCommandDispatcher; - CSMDoc::Document& mDocument; - - protected: + Q_OBJECT - QUndoStack& getUndoStack() const; + bool mEditLock; + CSMWorld::CommandDispatcher* mCommandDispatcher; + CSMDoc::Document& mDocument; - CSMDoc::Document& getDocument() const; + protected: + QUndoStack& getUndoStack() const; - CSMWorld::ColumnBase::Display getDisplayTypeFromIndex(const QModelIndex &index) const; + CSMDoc::Document& getDocument() const; - virtual void setModelDataImp (QWidget *editor, QAbstractItemModel *model, - const QModelIndex& index) const; + CSMWorld::ColumnBase::Display getDisplayTypeFromIndex(const QModelIndex& index) const; - public: + virtual void setModelDataImp(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const; - /// \param commandDispatcher If CommandDelegate will be only be used on read-only - /// cells, a 0-pointer can be passed here. - CommandDelegate (CSMWorld::CommandDispatcher *commandDispatcher, CSMDoc::Document& document, QObject *parent); + public: + /// \param commandDispatcher If CommandDelegate will be only be used on read-only + /// cells, a 0-pointer can be passed here. + CommandDelegate(CSMWorld::CommandDispatcher* commandDispatcher, CSMDoc::Document& document, QObject* parent); - void setModelData (QWidget *editor, QAbstractItemModel *model, - const QModelIndex& index) const override; + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; - QWidget *createEditor (QWidget *parent, - const QStyleOptionViewItem& option, - const QModelIndex& index) const override; + QWidget* createEditor( + QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; - virtual QWidget *createEditor (QWidget *parent, - const QStyleOptionViewItem& option, - const QModelIndex& index, - CSMWorld::ColumnBase::Display display) const; + virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index, + CSMWorld::ColumnBase::Display display) const; - void setEditLock (bool locked); + void setEditLock(bool locked); - bool isEditLocked() const; + bool isEditLocked() const; - ///< \return Does column require update? + ///< \return Does column require update? - void setEditorData (QWidget *editor, const QModelIndex& index) const override; + void setEditorData(QWidget* editor, const QModelIndex& index) const override; - virtual void setEditorData (QWidget *editor, const QModelIndex& index, bool tryDisplay) const; + virtual void setEditorData(QWidget* editor, const QModelIndex& index, bool tryDisplay) const; - /// \attention This is not a slot. For ordering reasons this function needs to be - /// called manually from the parent object's settingChanged function. - virtual void settingChanged (const CSMPrefs::Setting *setting); + /// \attention This is not a slot. For ordering reasons this function needs to be + /// called manually from the parent object's settingChanged function. + virtual void settingChanged(const CSMPrefs::Setting* setting); }; } diff --git a/apps/opencs/view/world/vartypedelegate.cpp b/apps/opencs/view/world/vartypedelegate.cpp index 48fb4ab874d..acc94004c91 100644 --- a/apps/opencs/view/world/vartypedelegate.cpp +++ b/apps/opencs/view/world/vartypedelegate.cpp @@ -1,17 +1,31 @@ #include "vartypedelegate.hpp" -#include +#include +#include +#include +#include + +#include -#include "../../model/world/commands.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/commandmacro.hpp" +#include "../../model/world/commands.hpp" + +namespace CSMDoc +{ + class Document; +} -void CSVWorld::VarTypeDelegate::addCommands (QAbstractItemModel *model, const QModelIndex& index, int type) - const +namespace CSMWorld { - QModelIndex next = model->index (index.row(), index.column()+1); + class CommandDispatcher; +} + +void CSVWorld::VarTypeDelegate::addCommands(QAbstractItemModel* model, const QModelIndex& index, int type) const +{ + QModelIndex next = model->index(index.row(), index.column() + 1); - QVariant old = model->data (next); + QVariant old = model->data(next); QVariant value; @@ -34,50 +48,51 @@ void CSVWorld::VarTypeDelegate::addCommands (QAbstractItemModel *model, const QM value = old.toString(); break; - default: break; // ignore the rest + default: + break; // ignore the rest } - CSMWorld::CommandMacro macro (getUndoStack(), "Modify " + model->headerData (index.column(), Qt::Horizontal, Qt::DisplayRole).toString()); + CSMWorld::CommandMacro macro( + getUndoStack(), "Modify " + model->headerData(index.column(), Qt::Horizontal, Qt::DisplayRole).toString()); - macro.push (new CSMWorld::ModifyCommand (*model, index, type)); - macro.push (new CSMWorld::ModifyCommand (*model, next, value)); + macro.push(new CSMWorld::ModifyCommand(*model, index, type)); + macro.push(new CSMWorld::ModifyCommand(*model, next, value)); } -CSVWorld::VarTypeDelegate::VarTypeDelegate (const std::vector >& values, - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) -: EnumDelegate (values, dispatcher, document, parent) -{} - +CSVWorld::VarTypeDelegate::VarTypeDelegate(const std::vector>& values, + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) + : EnumDelegate(values, dispatcher, document, parent) +{ +} -CSVWorld::VarTypeDelegateFactory::VarTypeDelegateFactory (ESM::VarType type0, - ESM::VarType type1, ESM::VarType type2, ESM::VarType type3) +CSVWorld::VarTypeDelegateFactory::VarTypeDelegateFactory( + ESM::VarType type0, ESM::VarType type1, ESM::VarType type2, ESM::VarType type3) { - if (type0!=ESM::VT_Unknown) - add (type0); + if (type0 != ESM::VT_Unknown) + add(type0); - if (type1!=ESM::VT_Unknown) - add (type1); + if (type1 != ESM::VT_Unknown) + add(type1); - if (type2!=ESM::VT_Unknown) - add (type2); + if (type2 != ESM::VT_Unknown) + add(type2); - if (type3!=ESM::VT_Unknown) - add (type3); + if (type3 != ESM::VT_Unknown) + add(type3); } -CSVWorld::CommandDelegate *CSVWorld::VarTypeDelegateFactory::makeDelegate ( - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent) const +CSVWorld::CommandDelegate* CSVWorld::VarTypeDelegateFactory::makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const { - return new VarTypeDelegate (mValues, dispatcher, document, parent); + return new VarTypeDelegate(mValues, dispatcher, document, parent); } -void CSVWorld::VarTypeDelegateFactory::add (ESM::VarType type) +void CSVWorld::VarTypeDelegateFactory::add(ESM::VarType type) { - std::vector> enums = - CSMWorld::Columns::getEnums (CSMWorld::Columns::ColumnId_ValueType); + std::vector> enums = CSMWorld::Columns::getEnums(CSMWorld::Columns::ColumnId_ValueType); if (static_cast(type) >= enums.size()) - throw std::logic_error ("Unsupported variable type"); + throw std::logic_error("Unsupported variable type"); - mValues.emplace_back(type, QString::fromUtf8 (enums[type].second.c_str())); + mValues.emplace_back(type, QString::fromUtf8(enums[type].second.c_str())); } diff --git a/apps/opencs/view/world/vartypedelegate.hpp b/apps/opencs/view/world/vartypedelegate.hpp index 44705e80ec4..4398d4e4bdf 100644 --- a/apps/opencs/view/world/vartypedelegate.hpp +++ b/apps/opencs/view/world/vartypedelegate.hpp @@ -1,40 +1,50 @@ #ifndef CSV_WORLD_VARTYPEDELEGATE_H #define CSV_WORLD_VARTYPEDELEGATE_H -#include +#include +#include + +#include + +#include #include "enumdelegate.hpp" +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher; +} + namespace CSVWorld { class VarTypeDelegate : public EnumDelegate { - private: - - void addCommands (QAbstractItemModel *model, - const QModelIndex& index, int type) const override; + private: + void addCommands(QAbstractItemModel* model, const QModelIndex& index, int type) const override; - public: - - VarTypeDelegate (const std::vector >& values, - CSMWorld::CommandDispatcher *dispatcher, CSMDoc::Document& document, QObject *parent); + public: + VarTypeDelegate(const std::vector>& values, CSMWorld::CommandDispatcher* dispatcher, + CSMDoc::Document& document, QObject* parent); }; class VarTypeDelegateFactory : public CommandDelegateFactory { - std::vector > mValues; - - public: + std::vector> mValues; - VarTypeDelegateFactory (ESM::VarType type0 = ESM::VT_Unknown, - ESM::VarType type1 = ESM::VT_Unknown, ESM::VarType type2 = ESM::VT_Unknown, - ESM::VarType type3 = ESM::VT_Unknown); + public: + VarTypeDelegateFactory(ESM::VarType type0 = ESM::VT_Unknown, ESM::VarType type1 = ESM::VT_Unknown, + ESM::VarType type2 = ESM::VT_Unknown, ESM::VarType type3 = ESM::VT_Unknown); - CommandDelegate *makeDelegate (CSMWorld::CommandDispatcher *dispatcher, - CSMDoc::Document& document, QObject *parent) const override; - ///< The ownership of the returned CommandDelegate is transferred to the caller. + CommandDelegate* makeDelegate( + CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const override; + ///< The ownership of the returned CommandDelegate is transferred to the caller. - void add (ESM::VarType type); + void add(ESM::VarType type); }; } diff --git a/apps/opencs_tests/CMakeLists.txt b/apps/opencs_tests/CMakeLists.txt new file mode 100644 index 00000000000..3bf783bb689 --- /dev/null +++ b/apps/opencs_tests/CMakeLists.txt @@ -0,0 +1,33 @@ +file(GLOB OPENCS_TESTS_SRC_FILES + main.cpp + model/world/testinfocollection.cpp + model/world/testuniversalid.cpp +) + +source_group(apps\\openmw-cs-tests FILES ${OPENCS_TESTS_SRC_FILES}) + +openmw_add_executable(openmw-cs-tests ${OPENCS_TESTS_SRC_FILES}) + +target_include_directories(openmw-cs-tests SYSTEM PRIVATE ${GTEST_INCLUDE_DIRS}) +target_include_directories(openmw-cs-tests SYSTEM PRIVATE ${GMOCK_INCLUDE_DIRS}) + +target_link_libraries(openmw-cs-tests PRIVATE + openmw-cs-lib + GTest::GTest + GMock::GMock +) + +if (UNIX AND NOT APPLE) + target_link_libraries(openmw-cs-tests PRIVATE ${CMAKE_THREAD_LIBS_INIT}) +endif() + +if (BUILD_WITH_CODE_COVERAGE) + target_compile_options(openmw-cs-tests PRIVATE --coverage) + target_link_libraries(openmw-cs-tests PRIVATE gcov) +endif() + +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw-cs-tests PRIVATE + + ) +endif() diff --git a/apps/opencs_tests/main.cpp b/apps/opencs_tests/main.cpp new file mode 100644 index 00000000000..fd7d4900c8a --- /dev/null +++ b/apps/opencs_tests/main.cpp @@ -0,0 +1,11 @@ +#include + +#include + +int main(int argc, char* argv[]) +{ + Log::sMinDebugLevel = Debug::getDebugLevel(); + + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/apps/opencs_tests/model/world/testinfocollection.cpp b/apps/opencs_tests/model/world/testinfocollection.cpp new file mode 100644 index 00000000000..6bd0694b34a --- /dev/null +++ b/apps/opencs_tests/model/world/testinfocollection.cpp @@ -0,0 +1,637 @@ +#include "apps/opencs/model/world/infocollection.hpp" + +#include "components/esm3/esmreader.hpp" +#include "components/esm3/esmwriter.hpp" +#include "components/esm3/formatversion.hpp" +#include "components/esm3/loaddial.hpp" +#include "components/esm3/loadinfo.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace CSMWorld +{ + inline std::ostream& operator<<(std::ostream& stream, const Record* value) + { + return stream << "&Record{.mState=" << value->mState << ", .mId=" << value->get().mId << "}"; + } + + namespace + { + using namespace ::testing; + + struct DialInfoData + { + ESM::DialInfo mValue; + bool mDeleted = false; + + void save(ESM::ESMWriter& writer) const { mValue.save(writer, mDeleted); } + }; + + template + struct DialogueData + { + ESM::Dialogue mDialogue; + std::vector mInfos; + }; + + DialogueData generateDialogueWithInfos( + std::size_t infoCount, std::string_view dialogueId = "dialogue") + { + DialogueData result; + + result.mDialogue.blank(); + result.mDialogue.mId = ESM::RefId::stringRefId(dialogueId); + result.mDialogue.mStringId = dialogueId; + + for (std::size_t i = 0; i < infoCount; ++i) + { + ESM::DialInfo& info = result.mInfos.emplace_back(); + info.blank(); + info.mId = ESM::RefId::stringRefId("info" + std::to_string(i)); + } + + if (infoCount >= 2) + { + result.mInfos[0].mNext = result.mInfos[1].mId; + result.mInfos[infoCount - 1].mPrev = result.mInfos[infoCount - 2].mId; + } + + for (std::size_t i = 1; i < infoCount - 1; ++i) + { + result.mInfos[i].mPrev = result.mInfos[i - 1].mId; + result.mInfos[i].mNext = result.mInfos[i + 1].mId; + } + + return result; + } + + template + std::unique_ptr saveDialogueWithInfos(const ESM::Dialogue& dialogue, Infos&& infos) + { + auto stream = std::make_unique(); + + ESM::ESMWriter writer; + writer.setFormatVersion(ESM::CurrentSaveGameFormatVersion); + writer.save(*stream); + + writer.startRecord(ESM::REC_DIAL); + dialogue.save(writer); + writer.endRecord(ESM::REC_DIAL); + + for (const auto& info : infos) + { + writer.startRecord(ESM::REC_INFO); + info.save(writer); + writer.endRecord(ESM::REC_INFO); + } + + return stream; + } + + void loadDialogueWithInfos(bool base, std::unique_ptr stream, InfoCollection& infoCollection, + InfoOrderByTopic& infoOrder) + { + ESM::ESMReader reader; + reader.open(std::move(stream), "test"); + + ASSERT_TRUE(reader.hasMoreRecs()); + ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_DIAL); + reader.getRecHeader(); + bool isDeleted; + ESM::Dialogue dialogue; + dialogue.load(reader, isDeleted); + + while (reader.hasMoreRecs()) + { + ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_INFO); + reader.getRecHeader(); + infoCollection.load(reader, base, dialogue, infoOrder); + } + } + + template + void saveAndLoadDialogueWithInfos(const ESM::Dialogue& dialogue, Infos&& infos, bool base, + InfoCollection& infoCollection, InfoOrderByTopic& infoOrder) + { + loadDialogueWithInfos(base, saveDialogueWithInfos(dialogue, infos), infoCollection, infoOrder); + } + + template + void saveAndLoadDialogueWithInfos( + const DialogueData& data, bool base, InfoCollection& infoCollection, InfoOrderByTopic& infoOrder) + { + saveAndLoadDialogueWithInfos(data.mDialogue, data.mInfos, base, infoCollection, infoOrder); + } + + MATCHER_P(InfoId, v, "") + { + return arg.mId == v; + } + + TEST(CSMWorldInfoCollectionTest, loadShouldAddRecord) + { + ESM::Dialogue dialogue; + dialogue.blank(); + dialogue.mId = ESM::RefId::stringRefId("dialogue"); + dialogue.mStringId = "Dialogue"; + + ESM::DialInfo info; + info.blank(); + info.mId = ESM::RefId::stringRefId("info0"); + + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder); + + EXPECT_EQ(collection.getSize(), 1); + ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0); + const Record& record = collection.getRecord(0); + ASSERT_EQ(record.mState, RecordBase::State_BaseOnly); + EXPECT_EQ(record.mBase.mTopicId, dialogue.mId); + EXPECT_EQ(record.mBase.mOriginalId, info.mId); + EXPECT_EQ(record.mBase.mId, ESM::RefId::stringRefId("dialogue#info0")); + + ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId))); + EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre(InfoId(info.mId))); + } + + TEST(CSMWorldInfoCollectionTest, loadShouldAddRecordAndMarkModifiedOnlyWhenNotBase) + { + ESM::Dialogue dialogue; + dialogue.blank(); + dialogue.mId = ESM::RefId::stringRefId("dialogue"); + dialogue.mStringId = "Dialogue"; + + ESM::DialInfo info; + info.blank(); + info.mId = ESM::RefId::stringRefId("info0"); + + const bool base = false; + InfoOrderByTopic infoOrder; + InfoCollection collection; + saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder); + + EXPECT_EQ(collection.getSize(), 1); + ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0); + const Record& record = collection.getRecord(0); + ASSERT_EQ(record.mState, RecordBase::State_ModifiedOnly); + EXPECT_EQ(record.mModified.mTopicId, dialogue.mId); + EXPECT_EQ(record.mModified.mOriginalId, info.mId); + EXPECT_EQ(record.mModified.mId, ESM::RefId::stringRefId("dialogue#info0")); + + ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId))); + EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre(InfoId(info.mId))); + } + + TEST(CSMWorldInfoCollectionTest, loadShouldUpdateRecord) + { + ESM::Dialogue dialogue; + dialogue.blank(); + dialogue.mId = ESM::RefId::stringRefId("dialogue"); + dialogue.mStringId = "Dialogue"; + + ESM::DialInfo info; + info.blank(); + info.mId = ESM::RefId::stringRefId("info0"); + + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder); + + ESM::DialInfo updatedInfo = info; + updatedInfo.mActor = ESM::RefId::stringRefId("newActor"); + + saveAndLoadDialogueWithInfos(dialogue, std::array{ updatedInfo }, base, collection, infoOrder); + + ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0); + const Record& record = collection.getRecord(0); + ASSERT_EQ(record.mState, RecordBase::State_BaseOnly); + EXPECT_EQ(record.mBase.mActor, ESM::RefId::stringRefId("newActor")); + + ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId))); + EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre(InfoId(info.mId))); + } + + TEST(CSMWorldInfoCollectionTest, loadShouldUpdateRecordAndMarkModifiedWhenNotBase) + { + ESM::Dialogue dialogue; + dialogue.blank(); + dialogue.mId = ESM::RefId::stringRefId("dialogue"); + dialogue.mStringId = "Dialogue"; + + ESM::DialInfo info; + info.blank(); + info.mId = ESM::RefId::stringRefId("info0"); + + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder); + + ESM::DialInfo updatedInfo = info; + updatedInfo.mActor = ESM::RefId::stringRefId("newActor"); + + saveAndLoadDialogueWithInfos(dialogue, std::array{ updatedInfo }, false, collection, infoOrder); + + ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0); + const Record& record = collection.getRecord(0); + ASSERT_EQ(record.mState, RecordBase::State_Modified); + EXPECT_EQ(record.mModified.mActor, ESM::RefId::stringRefId("newActor")); + + ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId))); + EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre(InfoId(info.mId))); + } + + TEST(CSMWorldInfoCollectionTest, loadShouldSkipAbsentDeletedRecord) + { + ESM::Dialogue dialogue; + dialogue.blank(); + dialogue.mId = ESM::RefId::stringRefId("dialogue"); + dialogue.mStringId = "Dialogue"; + + DialInfoData info; + info.mValue.blank(); + info.mValue.mId = ESM::RefId::stringRefId("info0"); + info.mDeleted = true; + + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder); + + EXPECT_EQ(collection.getSize(), 0); + + ASSERT_THAT(infoOrder, ElementsAre()); + } + + TEST(CSMWorldInfoCollectionTest, loadShouldRemovePresentDeletedBaseRecord) + { + ESM::Dialogue dialogue; + dialogue.blank(); + dialogue.mId = ESM::RefId::stringRefId("dialogue"); + dialogue.mStringId = "Dialogue"; + + DialInfoData info; + info.mValue.blank(); + info.mValue.mId = ESM::RefId::stringRefId("info0"); + + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder); + + info.mDeleted = true; + + saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder); + + EXPECT_EQ(collection.getSize(), 0); + + ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId))); + EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre()); + } + + TEST(CSMWorldInfoCollectionTest, loadShouldMarkAsDeletedNotBaseRecord) + { + ESM::Dialogue dialogue; + dialogue.blank(); + dialogue.mId = ESM::RefId::stringRefId("dialogue"); + dialogue.mStringId = "Dialogue"; + + DialInfoData info; + info.mValue.blank(); + info.mValue.mId = ESM::RefId::stringRefId("info0"); + + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder); + + info.mDeleted = true; + + saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, false, collection, infoOrder); + + EXPECT_EQ(collection.getSize(), 1); + EXPECT_EQ( + collection.getRecord(ESM::RefId::stringRefId("dialogue#info0")).mState, RecordBase::State_Deleted); + + ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId))); + EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre(InfoId(info.mValue.mId))); + } + + TEST(CSMWorldInfoCollectionTest, sortShouldOrderRecordsBasedOnPrev) + { + const DialogueData data = generateDialogueWithInfos(3); + + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + saveAndLoadDialogueWithInfos(data, base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_EQ(collection.getSize(), 3); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 1); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 2); + } + + TEST(CSMWorldInfoCollectionTest, sortShouldOrderRecordsBasedOnPrevWhenReversed) + { + DialogueData data = generateDialogueWithInfos(3); + + std::reverse(data.mInfos.begin(), data.mInfos.end()); + + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + saveAndLoadDialogueWithInfos(data, base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_EQ(collection.getSize(), 3); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 1); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 2); + } + + TEST(CSMWorldInfoCollectionTest, sortShouldInsertNewRecordBasedOnPrev) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + const DialogueData data = generateDialogueWithInfos(3); + + saveAndLoadDialogueWithInfos(data, base, collection, infoOrder); + + ESM::DialInfo newInfo; + newInfo.blank(); + newInfo.mId = ESM::RefId::stringRefId("newInfo"); + newInfo.mPrev = data.mInfos[1].mId; + newInfo.mNext = ESM::RefId::stringRefId("invalid"); + + saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ newInfo }, base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_EQ(collection.getSize(), 4); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 1); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#newInfo")), 2); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 3); + } + + TEST(CSMWorldInfoCollectionTest, sortShouldInsertNewRecordToFrontWhenPrevIsEmpty) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + const DialogueData data = generateDialogueWithInfos(3); + + saveAndLoadDialogueWithInfos(data, base, collection, infoOrder); + + ESM::DialInfo newInfo; + newInfo.blank(); + newInfo.mId = ESM::RefId::stringRefId("newInfo"); + newInfo.mNext = ESM::RefId::stringRefId("invalid"); + + saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ newInfo }, base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_EQ(collection.getSize(), 4); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#newInfo")), 0); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 1); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 2); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 3); + } + + TEST(CSMWorldInfoCollectionTest, sortShouldInsertNewRecordToBackWhenPrevIsNotFound) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + const DialogueData data = generateDialogueWithInfos(3); + + saveAndLoadDialogueWithInfos(data, base, collection, infoOrder); + + ESM::DialInfo newInfo; + newInfo.blank(); + newInfo.mId = ESM::RefId::stringRefId("newInfo"); + newInfo.mPrev = ESM::RefId::stringRefId("invalid"); + + saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ newInfo }, base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_EQ(collection.getSize(), 4); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 1); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 2); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#newInfo")), 3); + } + + TEST(CSMWorldInfoCollectionTest, sortShouldMoveBackwardUpdatedRecordBasedOnPrev) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + const DialogueData data = generateDialogueWithInfos(3); + + saveAndLoadDialogueWithInfos(data, base, collection, infoOrder); + + ESM::DialInfo updatedInfo = data.mInfos[2]; + updatedInfo.mPrev = data.mInfos[0].mId; + updatedInfo.mNext = ESM::RefId::stringRefId("invalid"); + + saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 2); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 1); + } + + TEST(CSMWorldInfoCollectionTest, sortShouldMoveForwardUpdatedRecordBasedOnPrev) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + const DialogueData data = generateDialogueWithInfos(3); + + saveAndLoadDialogueWithInfos(data, base, collection, infoOrder); + + ESM::DialInfo updatedInfo = data.mInfos[0]; + updatedInfo.mPrev = data.mInfos[1].mId; + updatedInfo.mNext = ESM::RefId::stringRefId("invalid"); + + saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 1); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 0); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 2); + } + + TEST(CSMWorldInfoCollectionTest, sortShouldMoveToFrontUpdatedRecordWhenPrevIsEmpty) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + const DialogueData data = generateDialogueWithInfos(3); + + saveAndLoadDialogueWithInfos(data, base, collection, infoOrder); + + ESM::DialInfo updatedInfo = data.mInfos[2]; + updatedInfo.mPrev = ESM::RefId(); + updatedInfo.mNext = ESM::RefId::stringRefId("invalid"); + + saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 1); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 2); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 0); + } + + TEST(CSMWorldInfoCollectionTest, sortShouldMoveToBackUpdatedRecordWhenPrevIsNotFound) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + const DialogueData data = generateDialogueWithInfos(3); + + saveAndLoadDialogueWithInfos(data, base, collection, infoOrder); + + ESM::DialInfo updatedInfo = data.mInfos[0]; + updatedInfo.mPrev = ESM::RefId::stringRefId("invalid"); + updatedInfo.mNext = ESM::RefId(); + + saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 2); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 0); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 1); + } + + TEST(CSMWorldInfoCollectionTest, sortShouldProvideStableOrderByTopic) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue2"), base, collection, infoOrder); + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue0"), base, collection, infoOrder); + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue1"), base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info0")), 0); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info1")), 1); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue1#info0")), 2); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue1#info1")), 3); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue2#info0")), 4); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue2#info1")), 5); + } + + TEST(CSMWorldInfoCollectionTest, getAppendIndexShouldReturnFirstIndexAfterInfoTopic) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue0"), base, collection, infoOrder); + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue1"), base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_EQ(collection.getAppendIndex(ESM::RefId::stringRefId("dialogue0#info2")), 2); + } + + TEST(CSMWorldInfoCollectionTest, reorderRowsShouldFailWhenOutOfBounds) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue0"), base, collection, infoOrder); + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue1"), base, collection, infoOrder); + + EXPECT_FALSE(collection.reorderRows(5, {})); + } + + TEST(CSMWorldInfoCollectionTest, reorderRowsShouldFailWhenAppliedToDifferentTopics) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue0"), base, collection, infoOrder); + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue1"), base, collection, infoOrder); + + EXPECT_FALSE(collection.reorderRows(0, { 0, 1, 2 })); + } + + TEST(CSMWorldInfoCollectionTest, reorderRowsShouldSucceedWhenAppliedToOneTopic) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(3, "dialogue0"), base, collection, infoOrder); + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(3, "dialogue1"), base, collection, infoOrder); + + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info0")), 0); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info1")), 1); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info2")), 2); + + EXPECT_TRUE(collection.reorderRows(1, { 1, 0 })); + + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info0")), 0); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info1")), 2); + EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info2")), 1); + } + + MATCHER_P(RecordPtrIdIs, v, "") + { + return v == arg->get().mId; + } + + TEST(CSMWorldInfoCollectionTest, getInfosByTopicShouldReturnRecordsGroupedByTopic) + { + const bool base = true; + InfoOrderByTopic infoOrder; + InfoCollection collection; + + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "d0"), base, collection, infoOrder); + saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "d1"), base, collection, infoOrder); + + collection.sort(infoOrder); + + EXPECT_THAT(collection.getInfosByTopic(), + UnorderedElementsAre(Pair("d0", ElementsAre(RecordPtrIdIs("d0#info0"), RecordPtrIdIs("d0#info1"))), + Pair("d1", ElementsAre(RecordPtrIdIs("d1#info0"), RecordPtrIdIs("d1#info1"))))); + } + } +} diff --git a/apps/opencs_tests/model/world/testuniversalid.cpp b/apps/opencs_tests/model/world/testuniversalid.cpp new file mode 100644 index 00000000000..871a5218d45 --- /dev/null +++ b/apps/opencs_tests/model/world/testuniversalid.cpp @@ -0,0 +1,186 @@ +#include "apps/opencs/model/world/universalid.hpp" + +#include +#include + +#include +#include + +namespace CSMWorld +{ + namespace + { + using namespace ::testing; + + TEST(CSMWorldUniversalIdTest, shouldFailToConstructFromNoneWithInvalidType) + { + EXPECT_THROW( + UniversalId{ static_cast(std::numeric_limits::max()) }, std::logic_error); + } + + TEST(CSMWorldUniversalIdTest, shouldFailToConstructFromStringWithInvalidType) + { + EXPECT_THROW(UniversalId(UniversalId::Type_Search, "invalid"), std::logic_error); + } + + TEST(CSMWorldUniversalIdTest, shouldFailToConstructFromIntWithInvalidType) + { + EXPECT_THROW(UniversalId(UniversalId::Type_Activator, 42), std::logic_error); + } + + TEST(CSMWorldUniversalIdTest, shouldFailToConstructFromRefIdWithInvalidType) + { + EXPECT_THROW(UniversalId(UniversalId::Type_Search, ESM::RefId()), std::logic_error); + } + + TEST(CSMWorldUniversalIdTest, shouldFailToConstructFromInvalidUniversalIdString) + { + EXPECT_THROW(UniversalId("invalid"), std::runtime_error); + } + + TEST(CSMWorldUniversalIdTest, getIndexShouldThrowExceptionForDefaultConstructed) + { + const UniversalId id; + EXPECT_THROW(id.getIndex(), std::logic_error); + } + + TEST(CSMWorldUniversalIdTest, getIndexShouldThrowExceptionForConstructedFromString) + { + const UniversalId id(UniversalId::Type_Activator, "a"); + EXPECT_THROW(id.getIndex(), std::logic_error); + } + + TEST(CSMWorldUniversalIdTest, getIndexShouldReturnValueForConstructedFromInt) + { + const UniversalId id(UniversalId::Type_Search, 42); + EXPECT_EQ(id.getIndex(), 42); + } + + TEST(CSMWorldUniversalIdTest, getIdShouldThrowExceptionForConstructedFromInt) + { + const UniversalId id(UniversalId::Type_Search, 42); + EXPECT_THROW(id.getId(), std::logic_error); + } + + TEST(CSMWorldUniversalIdTest, getIdShouldReturnValueForConstructedFromString) + { + const UniversalId id(UniversalId::Type_Activator, "a"); + EXPECT_EQ(id.getId(), "a"); + } + + TEST(CSMWorldUniversalIdTest, getRefIdShouldThrowExceptionForDefaultConstructed) + { + const UniversalId id; + EXPECT_THROW(id.getRefId(), std::logic_error); + } + + TEST(CSMWorldUniversalIdTest, getRefIdShouldReturnValueForConstructedFromRefId) + { + const UniversalId id(UniversalId::Type_Skill, ESM::IndexRefId(ESM::REC_SKIL, 42)); + EXPECT_EQ(id.getRefId(), ESM::IndexRefId(ESM::REC_SKIL, 42)); + } + + struct Params + { + UniversalId mId; + UniversalId::Type mType; + UniversalId::Class mClass; + UniversalId::ArgumentType mArgumentType; + std::string mTypeName; + std::string mString; + std::string mIcon; + }; + + std::ostream& operator<<(std::ostream& stream, const Params& value) + { + return stream << ".mType = " << value.mType << " .mClass = " << value.mClass + << " .mArgumentType = " << value.mArgumentType << " .mTypeName = " << value.mTypeName + << " .mString = " << value.mString << " .mIcon = " << value.mIcon; + } + + struct CSMWorldUniversalIdValidPerTypeTest : TestWithParam + { + }; + + TEST_P(CSMWorldUniversalIdValidPerTypeTest, getTypeShouldReturnExpected) + { + EXPECT_EQ(GetParam().mId.getType(), GetParam().mType); + } + + TEST_P(CSMWorldUniversalIdValidPerTypeTest, getClassShouldReturnExpected) + { + EXPECT_EQ(GetParam().mId.getClass(), GetParam().mClass); + } + + TEST_P(CSMWorldUniversalIdValidPerTypeTest, getArgumentTypeShouldReturnExpected) + { + EXPECT_EQ(GetParam().mId.getArgumentType(), GetParam().mArgumentType); + } + + TEST_P(CSMWorldUniversalIdValidPerTypeTest, shouldBeEqualToCopy) + { + EXPECT_EQ(GetParam().mId, UniversalId(GetParam().mId)); + } + + TEST_P(CSMWorldUniversalIdValidPerTypeTest, shouldNotBeLessThanCopy) + { + EXPECT_FALSE(GetParam().mId < UniversalId(GetParam().mId)); + } + + TEST_P(CSMWorldUniversalIdValidPerTypeTest, getTypeNameShouldReturnExpected) + { + EXPECT_EQ(GetParam().mId.getTypeName(), GetParam().mTypeName); + } + + TEST_P(CSMWorldUniversalIdValidPerTypeTest, toStringShouldReturnExpected) + { + EXPECT_EQ(GetParam().mId.toString(), GetParam().mString); + } + + TEST_P(CSMWorldUniversalIdValidPerTypeTest, getIconShouldReturnExpected) + { + EXPECT_EQ(GetParam().mId.getIcon(), GetParam().mIcon); + } + + const std::array validParams = { + Params{ UniversalId(), UniversalId::Type_None, UniversalId::Class_None, UniversalId::ArgumentType_None, "-", + "-", ":placeholder" }, + + Params{ UniversalId(UniversalId::Type_None), UniversalId::Type_None, UniversalId::Class_None, + UniversalId::ArgumentType_None, "-", "-", ":placeholder" }, + Params{ UniversalId(UniversalId::Type_RegionMap), UniversalId::Type_RegionMap, UniversalId::Class_NonRecord, + UniversalId::ArgumentType_None, "Region Map", "Region Map", ":region-map" }, + Params{ UniversalId(UniversalId::Type_RunLog), UniversalId::Type_RunLog, UniversalId::Class_Transient, + UniversalId::ArgumentType_None, "Run Log", "Run Log", ":run-log" }, + Params{ UniversalId(UniversalId::Type_Lands), UniversalId::Type_Lands, UniversalId::Class_RecordList, + UniversalId::ArgumentType_None, "Lands", "Lands", ":land-heightmap" }, + Params{ UniversalId(UniversalId::Type_Icons), UniversalId::Type_Icons, UniversalId::Class_ResourceList, + UniversalId::ArgumentType_None, "Icons", "Icons", ":resources-icon" }, + + Params{ UniversalId(UniversalId::Type_Activator, "a"), UniversalId::Type_Activator, + UniversalId::Class_RefRecord, UniversalId::ArgumentType_Id, "Activator", "Activator: a", ":activator" }, + Params{ UniversalId(UniversalId::Type_Gmst, "b"), UniversalId::Type_Gmst, UniversalId::Class_Record, + UniversalId::ArgumentType_Id, "Game Setting", "Game Setting: b", ":gmst" }, + Params{ UniversalId(UniversalId::Type_Mesh, "c"), UniversalId::Type_Mesh, UniversalId::Class_Resource, + UniversalId::ArgumentType_Id, "Mesh", "Mesh: c", ":resources-mesh" }, + Params{ UniversalId(UniversalId::Type_Scene, "d"), UniversalId::Type_Scene, UniversalId::Class_Collection, + UniversalId::ArgumentType_Id, "Scene", "Scene: d", ":scene" }, + Params{ UniversalId(UniversalId::Type_Reference, "e"), UniversalId::Type_Reference, + UniversalId::Class_SubRecord, UniversalId::ArgumentType_Id, "Instance", "Instance: e", ":instance" }, + + Params{ UniversalId(UniversalId::Type_Search, 42), UniversalId::Type_Search, UniversalId::Class_Transient, + UniversalId::ArgumentType_Index, "Global Search", "Global Search: 42", ":menu-search" }, + + Params{ UniversalId("Instance: f"), UniversalId::Type_Reference, UniversalId::Class_SubRecord, + UniversalId::ArgumentType_Id, "Instance", "Instance: f", ":instance" }, + + Params{ UniversalId(UniversalId::Type_Reference, ESM::RefId::stringRefId("g")), UniversalId::Type_Reference, + UniversalId::Class_SubRecord, UniversalId::ArgumentType_RefId, "Instance", "Instance: g", ":instance" }, + Params{ UniversalId(UniversalId::Type_Reference, ESM::RefId::index(ESM::REC_SKIL, 42)), + UniversalId::Type_Reference, UniversalId::Class_SubRecord, UniversalId::ArgumentType_RefId, "Instance", + "Instance: SKIL:0x2a", ":instance" }, + }; + + INSTANTIATE_TEST_SUITE_P(ValidParams, CSMWorldUniversalIdValidPerTypeTest, ValuesIn(validParams)); + } +} diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 3fb762d3054..a1257614548 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -1,32 +1,34 @@ -# local files -set(GAME - main.cpp +set(OPENMW_SOURCES engine.cpp + options.cpp +) +set(OPENMW_RESOURCES ${CMAKE_SOURCE_DIR}/files/windows/openmw.rc ${CMAKE_SOURCE_DIR}/files/windows/openmw.exe.manifest ) -if (ANDROID) - set(GAME ${GAME} android_main.cpp) -endif() - -set(GAME_HEADER +set(OPENMW_HEADERS + doc.hpp engine.hpp + options.hpp + profile.hpp ) -source_group(game FILES ${GAME} ${GAME_HEADER}) +source_group(apps/openmw FILES main.cpp android_main.cpp ${OPENMW_SOURCES} ${OPENMW_HEADERS} ${OPENMW_RESOURCES}) add_openmw_dir (mwrender - actors objects renderingmanager animation rotatecontroller sky npcanimation vismask + actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation esm4npcanimation vismask creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager - bulletdebugdraw globalmap characterpreview camera viewovershoulder localmap water terrainstorage ripplesimulation + bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover + postprocessor pingpongcull luminancecalculator pingpongcanvas transparentpass precipitationocclusion ripples + actorutil distortion animationpriority bonegroup blendmask animblendcontroller ) add_openmw_dir (mwinput actions actionmanager bindingsmanager controllermanager controlswitch - inputmanagerimp mousemanager keyboardmanager sdlmappings sensormanager + inputmanagerimp mousemanager keyboardmanager sensormanager gyromanager ) add_openmw_dir (mwgui @@ -42,6 +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 ) add_openmw_dir (mwdialogue @@ -55,39 +58,53 @@ add_openmw_dir (mwscript animationextensions transformationextensions consoleextensions userextensions ) +add_openmw_dir (mwlua + luamanagerimp object objectlists userdataserializer luaevents engineevents objectvariant + context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings + mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings dialoguebindings + postprocessingbindings stats recordstore debugbindings corebindings worldbindings worker 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 + types/potion types/ingredient types/misc types/repair types/armor types/light types/static + types/clothing types/levelledlist types/terminal + ) + add_openmw_dir (mwsound soundmanagerimp openal_output ffmpeg_decoder sound sound_buffer sound_decoder sound_output - loudness movieaudiofactory alext efx efx-presets regionsoundselector watersoundupdater volumesettings + loudness movieaudiofactory alext efx efx-presets regionsoundselector watersoundupdater ) add_openmw_dir (mwworld refdata worldimp scene globals class action nullaction actionteleport containerstore actiontalk actiontake manualref player cellvisitors failedaction - cells localscripts customdata inventorystore ptr actionopen actionread actionharvest + worldmodel localscripts customdata inventorystore ptr actionopen actionread actionharvest actionequip timestamp actionalchemy cellstore actionapply actioneat - store esmstore recordcmp fallback actionrepair actionsoulgem livecellref actiondoor + store esmstore fallback actionrepair actionsoulgem livecellref actiondoor contentloader esmloader actiontrap cellreflist cellref weather projectilemanager - cellpreloader datetimemanager + cellpreloader datetimemanager groundcoverstore magiceffects cell ptrregistry + positioncellgrid ) add_openmw_dir (mwphysics physicssystem trace collisiontype actor convert object heightfield closestnotmerayresultcallback - contacttestresultcallback deepestnotmecontacttestresultcallback stepper movementsolver projectile + contacttestresultcallback stepper movementsolver projectile actorconvexcallback raycasting mtphysics contacttestwrapper projectileconvexcallback ) add_openmw_dir (mwclass classes activator creature npc weapon armor potion apparatus book clothing container door ingredient creaturelevlist itemlevlist light lockpick misc probe repair static actor bodypart + esm4base esm4npc light4 ) add_openmw_dir (mwmechanics mechanicsmanagerimp stat creaturestats magiceffects movement actorutil spelllist drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance - disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning - character actors objects aistate trading weaponpriority spellpriority weapontype spellutil tickableeffects - spellabsorption linkedeffects + disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction summoning + character actors objects aistate weaponpriority spellpriority weapontype spellutil + spelleffects ) add_openmw_dir (mwstate @@ -96,24 +113,33 @@ add_openmw_dir (mwstate add_openmw_dir (mwbase environment world scriptmanager dialoguemanager journal soundmanager mechanicsmanager - inputmanager windowmanager statemanager + inputmanager windowmanager statemanager luamanager ) # Main executable -if (NOT ANDROID) - openmw_add_executable(openmw - ${OPENMW_FILES} - ${GAME} ${GAME_HEADER} - ${APPLE_BUNDLE_RESOURCES} - ) -else () - add_library(openmw - SHARED - ${OPENMW_FILES} - ${GAME} ${GAME_HEADER} - ) -endif () +add_library(openmw-lib STATIC + ${OPENMW_FILES} + ${OPENMW_SOURCES} +) + +if(BUILD_OPENMW) + if (ANDROID) + add_library(openmw SHARED + ${OPENMW_FILES} + main.cpp + android_main.cpp + ) + else() + openmw_add_executable(openmw + ${APPLE_BUNDLE_RESOURCES} + ${OPENMW_RESOURCES} + main.cpp + ) + endif() + + target_link_libraries(openmw openmw-lib) +endif() # Sound stuff - here so CMake doesn't stupidly recompile EVERYTHING # when we change the backend. @@ -121,7 +147,7 @@ include_directories( ${FFmpeg_INCLUDE_DIRS} ) -target_link_libraries(openmw +target_link_libraries(openmw-lib # CMake's built-in OSG finder does not use pkgconfig, so we have to # manually ensure the order is correct for inter-library dependencies. # This only makes a difference with `-DOPENMW_USE_SYSTEM_OSG=ON -DOSG_STATIC=ON`. @@ -133,68 +159,66 @@ target_link_libraries(openmw ${OSGDB_LIBRARIES} ${OSGUTIL_LIBRARIES} ${OSG_LIBRARIES} - - ${Boost_SYSTEM_LIBRARY} - ${Boost_THREAD_LIBRARY} - ${Boost_FILESYSTEM_LIBRARY} - ${Boost_PROGRAM_OPTIONS_LIBRARY} + Boost::program_options ${OPENAL_LIBRARY} ${FFmpeg_LIBRARIES} ${MyGUI_LIBRARIES} - ${SDL2_LIBRARY} + SDL2::SDL2 ${RecastNavigation_LIBRARIES} "osg-ffmpeg-videoplayer" "oics" components ) -if(OSG_STATIC) - unset(_osg_plugins_static_files) - add_library(openmw_osg_plugins INTERFACE) - foreach(_plugin ${USED_OSG_PLUGINS}) - string(TOUPPER ${_plugin} _plugin_uc) - if(OPENMW_USE_SYSTEM_OSG) - list(APPEND _osg_plugins_static_files ${${_plugin_uc}_LIBRARY}) - else() - list(APPEND _osg_plugins_static_files $) - target_link_libraries(openmw_osg_plugins INTERFACE $) - add_dependencies(openmw_osg_plugins ${${_plugin_uc}_LIBRARY}) - endif() - endforeach() - # We use --whole-archive because OSG plugins use registration. - get_whole_archive_options(_opts ${_osg_plugins_static_files}) - target_link_options(openmw_osg_plugins INTERFACE ${_opts}) - target_link_libraries(openmw openmw_osg_plugins) - - if(OPENMW_USE_SYSTEM_OSG) - # OSG plugin pkgconfig files are missing these dependencies. - # https://github.com/openscenegraph/OpenSceneGraph/issues/1052 - target_link_libraries(openmw freetype jpeg png) - endif() -endif(OSG_STATIC) +if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) + target_precompile_headers(openmw-lib PRIVATE + + + + + + + + + + + + + + + + + + + + + + ) +endif() + +add_definitions(-DMYGUI_DONT_USE_OBSOLETE=ON) if (ANDROID) - target_link_libraries(openmw EGL android log z) + target_link_libraries(openmw-lib EGL android log z) endif (ANDROID) if (USE_SYSTEM_TINYXML) - target_link_libraries(openmw ${TinyXML_LIBRARIES}) + target_link_libraries(openmw-lib ${TinyXML_LIBRARIES}) endif() if (NOT UNIX) -target_link_libraries(openmw ${SDL2MAIN_LIBRARY}) + target_link_libraries(openmw-lib ${SDL2MAIN_LIBRARY}) endif() # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) -target_link_libraries(openmw ${CMAKE_THREAD_LIBS_INIT}) + target_link_libraries(openmw-lib ${CMAKE_THREAD_LIBS_INIT}) endif() -if(APPLE) +if(APPLE AND BUILD_OPENMW) set(BUNDLE_RESOURCES_DIR "${APP_BUNDLE_DIR}/Contents/Resources") - set(OPENMW_MYGUI_FILES_ROOT ${BUNDLE_RESOURCES_DIR}) - set(OPENMW_SHADERS_ROOT ${BUNDLE_RESOURCES_DIR}) + set(OPENMW_RESOURCES_ROOT ${BUNDLE_RESOURCES_DIR}) add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files) @@ -211,24 +235,23 @@ if(APPLE) target_link_libraries(openmw ${COCOA_FRAMEWORK} ${IOKIT_FRAMEWORK}) if (FFmpeg_FOUND) - find_library(COREVIDEO_FRAMEWORK CoreVideo) - find_library(VDA_FRAMEWORK VideoDecodeAcceleration) - target_link_libraries(openmw z ${COREVIDEO_FRAMEWORK} ${VDA_FRAMEWORK}) + target_link_options(openmw PRIVATE "LINKER:SHELL:-framework CoreVideo" + "LINKER:SHELL:-framework CoreMedia" + "LINKER:SHELL:-framework VideoToolbox" + "LINKER:SHELL:-framework AudioToolbox" + "LINKER:SHELL:-framework VideoDecodeAcceleration") endif() -endif(APPLE) +endif() if (BUILD_WITH_CODE_COVERAGE) - add_definitions (--coverage) - target_link_libraries(openmw gcov) + target_compile_options(openmw-lib PRIVATE --coverage) + target_link_libraries(openmw-lib gcov) + if (NOT ANDROID AND BUILD_OPENMW) + target_compile_options(openmw PRIVATE --coverage) + target_link_libraries(openmw gcov) + endif() endif() -if (MSVC) - # Debug version needs increased number of sections beyond 2^16 - if (CMAKE_CL_64) - set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /bigobj") - endif (CMAKE_CL_64) -endif (MSVC) - -if (WIN32) +if (WIN32 AND BUILD_OPENMW) INSTALL(TARGETS openmw RUNTIME DESTINATION ".") -endif (WIN32) +endif() diff --git a/apps/openmw/android_main.cpp b/apps/openmw/android_main.cpp index cc36388b0b6..ade009ae07c 100644 --- a/apps/openmw/android_main.cpp +++ b/apps/openmw/android_main.cpp @@ -1,9 +1,11 @@ +#ifndef stderr int stderr = 0; // Hack: fix linker error +#endif #include "SDL_main.h" +#include #include #include -#include /******************************************************************************* Functions called by JNI @@ -14,43 +16,50 @@ int stderr = 0; // Hack: fix linker error extern void SDL_Android_Init(JNIEnv* env, jclass cls); extern int argcData; -extern const char **argvData; +extern const char** argvData; void releaseArgv(); - -extern "C" int Java_org_libsdl_app_SDLActivity_getMouseX(JNIEnv *env, jclass cls, jobject obj) { +extern "C" int Java_org_libsdl_app_SDLActivity_getMouseX(JNIEnv* env, jclass cls, jobject obj) +{ int ret = 0; SDL_GetMouseState(&ret, nullptr); return ret; } - -extern "C" int Java_org_libsdl_app_SDLActivity_getMouseY(JNIEnv *env, jclass cls, jobject obj) { +extern "C" int Java_org_libsdl_app_SDLActivity_getMouseY(JNIEnv* env, jclass cls, jobject obj) +{ int ret = 0; SDL_GetMouseState(nullptr, &ret); return ret; } -extern "C" int Java_org_libsdl_app_SDLActivity_isMouseShown(JNIEnv *env, jclass cls, jobject obj) { +extern "C" int Java_org_libsdl_app_SDLActivity_isMouseShown(JNIEnv* env, jclass cls, jobject obj) +{ return SDL_ShowCursor(SDL_QUERY); } -extern SDL_Window *Android_Window; -extern "C" int SDL_SendMouseMotion(SDL_Window * window, int mouseID, int relative, int x, int y); -extern "C" void Java_org_libsdl_app_SDLActivity_sendRelativeMouseMotion(JNIEnv *env, jclass cls, int x, int y) { +extern SDL_Window* Android_Window; +extern "C" int SDL_SendMouseMotion(SDL_Window* window, int mouseID, int relative, int x, int y); +extern "C" void Java_org_libsdl_app_SDLActivity_sendRelativeMouseMotion(JNIEnv* env, jclass cls, int x, int y) +{ SDL_SendMouseMotion(Android_Window, 0, 1, x, y); } -extern "C" int SDL_SendMouseButton(SDL_Window * window, int mouseID, Uint8 state, Uint8 button); -extern "C" void Java_org_libsdl_app_SDLActivity_sendMouseButton(JNIEnv *env, jclass cls, int state, int button) { +extern "C" int SDL_SendMouseButton(SDL_Window* window, int mouseID, Uint8 state, Uint8 button); +extern "C" void Java_org_libsdl_app_SDLActivity_sendMouseButton(JNIEnv* env, jclass cls, int state, int button) +{ SDL_SendMouseButton(Android_Window, 0, state, button); } -extern "C" int Java_org_libsdl_app_SDLActivity_nativeInit(JNIEnv* env, jclass cls, jobject obj) { +extern "C" int Java_org_libsdl_app_SDLActivity_nativeInit(JNIEnv* env, jclass cls, jobject obj) +{ setenv("OPENMW_DECOMPRESS_TEXTURES", "1", 1); // On Android, we use a virtual controller with guid="Virtual" - SDL_GameControllerAddMapping("5669727475616c000000000000000000,Virtual,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b16,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4"); + SDL_GameControllerAddMapping( + "5669727475616c000000000000000000,Virtual,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1," + "guide:b16,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14," + "righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4"); return 0; } diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index a06faa4d71a..ca1b0f577d1 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -1,15 +1,12 @@ #include "engine.hpp" -#include -#include +#include #include -#include +#include +#include -#include - -#include -#include #include +#include #include @@ -17,12 +14,13 @@ #include #include +#include #include #include -#include #include +#include #include #include @@ -30,27 +28,47 @@ #include +#include +#include + +#include #include #include #include -#include +#include + +#include +#include #include +#include +#include +#include +#include +#include + +#include +#include + #include "mwinput/inputmanagerimp.hpp" #include "mwgui/windowmanagerimp.hpp" -#include "mwscript/scriptmanagerimp.hpp" +#include "mwlua/luamanagerimp.hpp" +#include "mwlua/worker.hpp" + #include "mwscript/interpretercontext.hpp" +#include "mwscript/scriptmanagerimp.hpp" +#include "mwsound/constants.hpp" #include "mwsound/soundmanagerimp.hpp" #include "mwworld/class.hpp" -#include "mwworld/player.hpp" +#include "mwworld/datetimemanager.hpp" #include "mwworld/worldimp.hpp" #include "mwrender/vismask.hpp" @@ -65,6 +83,8 @@ #include "mwstate/statemanagerimp.hpp" +#include "profile.hpp" + namespace { void checkSDLError(int ret) @@ -73,236 +93,177 @@ namespace Log(Debug::Error) << "SDL error: " << SDL_GetError(); } - struct UserStats + void initStatsHandler(Resource::Profiler& profiler) { - const std::string mLabel; - const std::string mBegin; - const std::string mEnd; - const std::string mTaken; - - UserStats(const std::string& label, const std::string& prefix) - : mLabel(label), - mBegin(prefix + "_time_begin"), - mEnd(prefix + "_time_end"), - mTaken(prefix + "_time_taken") - {} - }; + const osg::Vec4f textColor(1.f, 1.f, 1.f, 1.f); + const osg::Vec4f barColor(1.f, 1.f, 1.f, 1.f); + const float multiplier = 1000; + const bool average = true; + const bool averageInInverseSpace = false; + const float maxValue = 10000; - enum class UserStatsType : std::size_t - { - Input, - Sound, - State, - Script, - Mechanics, - Physics, - PhysicsWorker, - World, - Gui, - - Number, - }; + OMW::forEachUserStatsValue([&](const OMW::UserStats& v) { + profiler.addUserStatsLine(v.mLabel, textColor, barColor, v.mTaken, multiplier, average, + averageInInverseSpace, v.mBegin, v.mEnd, maxValue); + }); + // the forEachUserStatsValue loop is "run" at compile time, hence the settings manager is not available. + // Unconditionnally add the async physics stats, and then remove it at runtime if necessary + if (Settings::physics().mAsyncNumThreads == 0) + profiler.removeUserStatsLine(" -Async"); + } - template - struct UserStatsValue + struct ScreenCaptureMessageBox { - static const UserStats sValue; - }; - - template <> - const UserStats UserStatsValue::sValue {"Input", "input"}; - - template <> - const UserStats UserStatsValue::sValue {"Sound", "sound"}; - - template <> - const UserStats UserStatsValue::sValue {"State", "state"}; - - template <> - const UserStats UserStatsValue::sValue {"Script", "script"}; - - template <> - const UserStats UserStatsValue::sValue {"Mech", "mechanics"}; - - template <> - const UserStats UserStatsValue::sValue {"Phys", "physics"}; + void operator()(std::string filePath) const + { + if (filePath.empty()) + { + MWBase::Environment::get().getWindowManager()->scheduleMessageBox( + "#{OMWEngine:ScreenshotFailed}", MWGui::ShowInDialogueMode_Never); - template <> - const UserStats UserStatsValue::sValue {" -Async", "physicsworker"}; + return; + } - template <> - const UserStats UserStatsValue::sValue {"World", "world"}; + std::string messageFormat + = MWBase::Environment::get().getL10nManager()->getMessage("OMWEngine", "ScreenshotMade"); - template <> - const UserStats UserStatsValue::sValue {"Gui", "gui"}; + std::string message = Misc::StringUtils::format(messageFormat, filePath); - template - struct ForEachUserStatsValue - { - template - static void apply(F&& f) - { - f(UserStatsValue::sValue); - using Next = ForEachUserStatsValue(static_cast(type) + 1)>; - Next::apply(std::forward(f)); + MWBase::Environment::get().getWindowManager()->scheduleMessageBox( + std::move(message), MWGui::ShowInDialogueMode_Never); } }; - template <> - struct ForEachUserStatsValue + struct IgnoreString { - template - static void apply(F&&) {} + void operator()(std::string) const {} }; - template - void forEachUserStatsValue(F&& f) + class IdentifyOpenGLOperation : public osg::GraphicsOperation { - ForEachUserStatsValue(0)>::apply(std::forward(f)); - } - - template - class ScopedProfile - { - public: - ScopedProfile(osg::Timer_t frameStart, unsigned int frameNumber, const osg::Timer& timer, osg::Stats& stats) - : mScopeStart(timer.tick()), - mFrameStart(frameStart), - mFrameNumber(frameNumber), - mTimer(timer), - mStats(stats) - { - } + public: + IdentifyOpenGLOperation() + : GraphicsOperation("IdentifyOpenGLOperation", false) + { + } - ScopedProfile(const ScopedProfile&) = delete; - ScopedProfile& operator=(const ScopedProfile&) = delete; + void operator()(osg::GraphicsContext* graphicsContext) override + { + Log(Debug::Info) << "OpenGL Vendor: " << glGetString(GL_VENDOR); + Log(Debug::Info) << "OpenGL Renderer: " << glGetString(GL_RENDERER); + Log(Debug::Info) << "OpenGL Version: " << glGetString(GL_VERSION); + glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &mMaxTextureImageUnits); + } - ~ScopedProfile() - { - if (!mStats.collectStats("engine")) - return; - const osg::Timer_t end = mTimer.tick(); - const UserStats& stats = UserStatsValue::sValue; - - mStats.setAttribute(mFrameNumber, stats.mBegin, mTimer.delta_s(mFrameStart, mScopeStart)); - mStats.setAttribute(mFrameNumber, stats.mTaken, mTimer.delta_s(mScopeStart, end)); - mStats.setAttribute(mFrameNumber, stats.mEnd, mTimer.delta_s(mFrameStart, end)); - } + int getMaxTextureImageUnits() const + { + if (mMaxTextureImageUnits == 0) + throw std::logic_error("mMaxTextureImageUnits is not initialized"); + return mMaxTextureImageUnits; + } - private: - const osg::Timer_t mScopeStart; - const osg::Timer_t mFrameStart; - const unsigned int mFrameNumber; - const osg::Timer& mTimer; - osg::Stats& mStats; + private: + int mMaxTextureImageUnits = 0; }; - void initStatsHandler(Resource::Profiler& profiler) + void reportStats(unsigned frameNumber, osgViewer::Viewer& viewer, std::ostream& stream) { - const osg::Vec4f textColor(1.f, 1.f, 1.f, 1.f); - const osg::Vec4f barColor(1.f, 1.f, 1.f, 1.f); - const float multiplier = 1000; - const bool average = true; - const bool averageInInverseSpace = false; - const float maxValue = 10000; - - forEachUserStatsValue([&] (const UserStats& v) - { - profiler.addUserStatsLine(v.mLabel, textColor, barColor, v.mTaken, multiplier, - average, averageInInverseSpace, v.mBegin, v.mEnd, maxValue); - }); - // the forEachUserStatsValue loop is "run" at compile time, hence the settings manager is not available. - // Unconditionnally add the async physics stats, and then remove it at runtime if necessary - if (Settings::Manager::getInt("async num threads", "Physics") == 0) - profiler.removeUserStatsLine(" -Async"); + viewer.getViewerStats()->report(stream, frameNumber); + osgViewer::Viewer::Cameras cameras; + viewer.getCameras(cameras); + for (osg::Camera* camera : cameras) + camera->getStats()->report(stream, frameNumber); } } void OMW::Engine::executeLocalScripts() { - MWWorld::LocalScripts& localScripts = mEnvironment.getWorld()->getLocalScripts(); + MWWorld::LocalScripts& localScripts = mWorld->getLocalScripts(); localScripts.startIteration(); - std::pair script; + std::pair script; while (localScripts.getNext(script)) { - MWScript::InterpreterContext interpreterContext ( - &script.second.getRefData().getLocals(), script.second); - mEnvironment.getScriptManager()->run (script.first, interpreterContext); + MWScript::InterpreterContext interpreterContext(&script.second.getRefData().getLocals(), script.second); + mScriptManager->run(script.first, interpreterContext); } } -bool OMW::Engine::frame(float frametime) +bool OMW::Engine::frame(unsigned frameNumber, float frametime) { - try - { - const osg::Timer_t frameStart = mViewer->getStartTick(); - const unsigned int frameNumber = mViewer->getFrameStamp()->getFrameNumber(); - const osg::Timer* const timer = osg::Timer::instance(); - osg::Stats* const stats = mViewer->getViewerStats(); + const osg::Timer_t frameStart = mViewer->getStartTick(); + const osg::Timer* const timer = osg::Timer::instance(); + osg::Stats* const stats = mViewer->getViewerStats(); - mEnvironment.setFrameDuration(frametime); + mEnvironment.setFrameDuration(frametime); + try + { // update input { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - mEnvironment.getInputManager()->update(frametime, false); + mInputManager->update(frametime, false); } // When the window is minimized, pause the game. Currently this *has* to be here to work around a MyGUI bug. - // If we are not currently rendering, then RenderItems will not be reused resulting in a memory leak upon changing widget textures (fixed in MyGUI 3.3.2), - // and destroyed widgets will not be deleted (not fixed yet, https://github.com/MyGUI/mygui/issues/21) + // If we are not currently rendering, then RenderItems will not be reused resulting in a memory leak upon + // changing widget textures (fixed in MyGUI 3.3.2), and destroyed widgets will not be deleted (not fixed yet, + // https://github.com/MyGUI/mygui/issues/21) { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - if (!mEnvironment.getWindowManager()->isWindowVisible()) + if (!mWindowManager->isWindowVisible()) { - mEnvironment.getSoundManager()->pausePlayback(); + mSoundManager->pausePlayback(); return false; } else - mEnvironment.getSoundManager()->resumePlayback(); + mSoundManager->resumePlayback(); // sound if (mUseSound) - mEnvironment.getSoundManager()->update(frametime); + mSoundManager->update(frametime); } - // Main menu opened? Then scripts are also paused. - bool paused = mEnvironment.getWindowManager()->containsMode(MWGui::GM_MainMenu); + { + ScopedProfile profile(frameStart, frameNumber, *timer, *stats); + // Should be called after input manager update and before any change to the game world. + // It applies to the game world queued changes from the previous frame. + mLuaManager->synchronizedUpdate(); + } // update game state { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - mEnvironment.getStateManager()->update (frametime); + mStateManager->update(frametime); } - bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); + bool paused = mWorld->getTimeManager()->isPaused(); { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) + if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { - if (!paused) + if (!mWindowManager->containsMode(MWGui::GM_MainMenu) || !paused) { - if (mEnvironment.getWorld()->getScriptsEnabled()) + if (mWorld->getScriptsEnabled()) { // local scripts executeLocalScripts(); // global scripts - mEnvironment.getScriptManager()->getGlobalScripts().run(); + mScriptManager->getGlobalScripts().run(); } - mEnvironment.getWorld()->markCellAsUnchanged(); + mWorld->getWorldScene().markCellAsUnchanged(); } - if (!guiActive) + if (!paused) { - double hours = (frametime * mEnvironment.getWorld()->getTimeScaleFactor()) / 3600.0; - mEnvironment.getWorld()->advanceTime(hours, true); - mEnvironment.getWorld()->rechargeItems(frametime, true); + double hours = (frametime * mWorld->getTimeManager()->getGameTimeScale()) / 3600.0; + mWorld->advanceTime(hours, true); + mWorld->rechargeItems(frametime, true); } } } @@ -311,16 +272,16 @@ bool OMW::Engine::frame(float frametime) { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) + if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { - mEnvironment.getMechanicsManager()->update(frametime, guiActive); + mMechanicsManager->update(frametime, paused); } - if (mEnvironment.getStateManager()->getState() == MWBase::StateManager::State_Running) + if (mStateManager->getState() == MWBase::StateManager::State_Running) { - MWWorld::Ptr player = mEnvironment.getWorld()->getPlayerPtr(); - if(!guiActive && player.getClass().getCreatureStats(player).isDead()) - mEnvironment.getStateManager()->endGame(); + MWWorld::Ptr player = mWorld->getPlayerPtr(); + if (!paused && player.getClass().getCreatureStats(player).isDead()) + mStateManager->endGame(); } } @@ -328,9 +289,9 @@ bool OMW::Engine::frame(float frametime) { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) + if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { - mEnvironment.getWorld()->updatePhysics(frametime, guiActive, frameStart, frameNumber, *stats); + mWorld->updatePhysics(frametime, paused, frameStart, frameNumber, *stats); } } @@ -338,65 +299,94 @@ bool OMW::Engine::frame(float frametime) { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - if (mEnvironment.getStateManager()->getState() != MWBase::StateManager::State_NoGame) + if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { - mEnvironment.getWorld()->update(frametime, guiActive); + mWorld->update(frametime, paused); } } // update GUI { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); - mEnvironment.getWindowManager()->update(frametime); + mWindowManager->update(frametime); } + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Error in frame: " << e.what(); + } - if (stats->collectStats("resource")) - { - stats->setAttribute(frameNumber, "FrameNumber", frameNumber); + const bool reportResource = stats->collectStats("resource"); - mResourceSystem->reportStats(frameNumber, stats); + if (reportResource) + stats->setAttribute(frameNumber, "UnrefQueue", mUnrefQueue->getSize()); - stats->setAttribute(frameNumber, "WorkQueue", mWorkQueue->getNumItems()); - stats->setAttribute(frameNumber, "WorkThread", mWorkQueue->getNumActiveThreads()); + mUnrefQueue->flush(*mWorkQueue); - mEnvironment.reportStats(frameNumber, *stats); - } + if (reportResource) + { + stats->setAttribute(frameNumber, "FrameNumber", frameNumber); + + mResourceSystem->reportStats(frameNumber, stats); + + stats->setAttribute(frameNumber, "WorkQueue", mWorkQueue->getNumItems()); + stats->setAttribute(frameNumber, "WorkThread", mWorkQueue->getNumActiveThreads()); + + mMechanicsManager->reportStats(frameNumber, *stats); + mWorld->reportStats(frameNumber, *stats); + mLuaManager->reportStats(frameNumber, *stats); } - catch (const std::exception& e) + + mStereoManager->updateSettings(Settings::camera().mNearClip, Settings::camera().mViewingDistance); + + mViewer->eventTraversal(); + mViewer->updateTraversal(); + + // update GUI by world data { - Log(Debug::Error) << "Error in frame: " << e.what(); + ScopedProfile profile(frameStart, frameNumber, *timer, *stats); + mWorld->updateWindowManager(); } + + // if there is a separate Lua thread, it starts the update now + mLuaWorker->allowUpdate(frameStart, frameNumber, *stats); + + mViewer->renderingTraversals(); + + mLuaWorker->finishUpdate(frameStart, frameNumber, *stats); + return true; } OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) - : mWindow(nullptr) - , mEncoding(ToUTF8::WINDOWS_1252) - , mEncoder(nullptr) - , mScreenCaptureOperation(nullptr) - , mSkipMenu (false) - , mUseSound (true) - , mCompileAll (false) - , mCompileAllDialogue (false) - , mWarningsMode (1) - , mScriptConsoleMode (false) - , mActivationDistanceOverride(-1) - , mGrab(true) - , mExportFonts(false) - , mRandomSeed(0) - , mScriptContext (nullptr) - , mFSStrict (false) - , mScriptBlacklistUse (true) - , mNewGame (false) - , mCfgMgr(configurationManager) + : mWindow(nullptr) + , mEncoding(ToUTF8::WINDOWS_1252) + , mScreenCaptureOperation(nullptr) + , mSelectDepthFormatOperation(new SceneUtil::SelectDepthFormatOperation()) + , mSelectColorFormatOperation(new SceneUtil::Color::SelectColorFormatOperation()) + , mStereoManager(nullptr) + , mSkipMenu(false) + , mUseSound(true) + , mCompileAll(false) + , mCompileAllDialogue(false) + , mWarningsMode(1) + , mScriptConsoleMode(false) + , mActivationDistanceOverride(-1) + , mGrab(true) + , mRandomSeed(0) + , mScriptBlacklistUse(true) + , mNewGame(false) + , mCfgMgr(configurationManager) + , mGlMaxTextureImageUnits(0) { SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0"); // We use only gamepads - Uint32 flags = SDL_INIT_VIDEO|SDL_INIT_NOPARACHUTE|SDL_INIT_GAMECONTROLLER|SDL_INIT_JOYSTICK|SDL_INIT_SENSOR; - if(SDL_WasInit(flags) == 0) + Uint32 flags + = SDL_INIT_VIDEO | SDL_INIT_NOPARACHUTE | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_SENSOR; + if (SDL_WasInit(flags) == 0) { SDL_SetMainReady(); - if(SDL_Init(flags) != 0) + if (SDL_Init(flags) != 0) { throw std::runtime_error("Could not initialize SDL! " + std::string(SDL_GetError())); } @@ -405,18 +395,32 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) OMW::Engine::~Engine() { - mEnvironment.cleanup(); + if (mScreenCaptureOperation != nullptr) + mScreenCaptureOperation->stop(); + + mMechanicsManager = nullptr; + mDialogueManager = nullptr; + mJournal = nullptr; + mScriptManager = nullptr; + mWindowManager = nullptr; + mWorld = nullptr; + mStereoManager = nullptr; + mSoundManager = nullptr; + mInputManager = nullptr; + mStateManager = nullptr; + mLuaWorker = nullptr; + mLuaManager = nullptr; + mL10nManager = nullptr; - delete mScriptContext; mScriptContext = nullptr; + mUnrefQueue = nullptr; mWorkQueue = nullptr; mViewer = nullptr; mResourceSystem.reset(); - delete mEncoder; mEncoder = nullptr; if (mWindow) @@ -428,33 +432,32 @@ OMW::Engine::~Engine() SDL_Quit(); } -void OMW::Engine::enableFSStrict(bool fsStrict) -{ - mFSStrict = fsStrict; -} - // Set data dir -void OMW::Engine::setDataDirs (const Files::PathContainer& dataDirs) +void OMW::Engine::setDataDirs(const Files::PathContainer& dataDirs) { mDataDirs = dataDirs; - mDataDirs.insert(mDataDirs.begin(), (mResDir / "vfs")); - mFileCollections = Files::Collections (mDataDirs, !mFSStrict); + mDataDirs.insert(mDataDirs.begin(), mResDir / "vfs"); + mFileCollections = Files::Collections(mDataDirs); } // Add BSA archive -void OMW::Engine::addArchive (const std::string& archive) { +void OMW::Engine::addArchive(const std::string& archive) +{ mArchives.push_back(archive); } // Set resource dir -void OMW::Engine::setResourceDir (const boost::filesystem::path& parResDir) +void OMW::Engine::setResourceDir(const std::filesystem::path& parResDir) { mResDir = parResDir; + if (!Version::checkResourcesVersion(mResDir)) + Log(Debug::Error) << "Resources dir " << mResDir + << " doesn't match OpenMW binary, the game may work incorrectly."; } // Set start cell name -void OMW::Engine::setCell (const std::string& cellName) +void OMW::Engine::setCell(const std::string& cellName) { mCellName = cellName; } @@ -469,62 +472,44 @@ void OMW::Engine::addGroundcoverFile(const std::string& file) mGroundcoverFiles.emplace_back(file); } -void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) +void OMW::Engine::setSkipMenu(bool skipMenu, bool newGame) { mSkipMenu = skipMenu; mNewGame = newGame; } -std::string OMW::Engine::loadSettings (Settings::Manager & settings) +void OMW::Engine::createWindow() { - // Create the settings manager and load default settings file - const std::string localdefault = (mCfgMgr.getLocalPath() / "defaults.bin").string(); - const std::string globaldefault = (mCfgMgr.getGlobalPath() / "defaults.bin").string(); - - // prefer local - if (boost::filesystem::exists(localdefault)) - settings.loadDefault(localdefault); - else if (boost::filesystem::exists(globaldefault)) - settings.loadDefault(globaldefault); - else - throw std::runtime_error ("No default settings file found! Make sure the file \"defaults.bin\" was properly installed."); - - // load user settings if they exist - const std::string settingspath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); - if (boost::filesystem::exists(settingspath)) - settings.loadUser(settingspath); + const int screen = Settings::video().mScreen; + const int width = Settings::video().mResolutionX; + const int height = Settings::video().mResolutionY; + const Settings::WindowMode windowMode = Settings::video().mWindowMode; + const bool windowBorder = Settings::video().mWindowBorder; + const SDLUtil::VSyncMode vsync = Settings::video().mVsyncMode; + unsigned antialiasing = static_cast(Settings::video().mAntialiasing); - return settingspath; -} + int pos_x = SDL_WINDOWPOS_CENTERED_DISPLAY(screen), pos_y = SDL_WINDOWPOS_CENTERED_DISPLAY(screen); -void OMW::Engine::createWindow(Settings::Manager& settings) -{ - int screen = settings.getInt("screen", "Video"); - int width = settings.getInt("resolution x", "Video"); - int height = settings.getInt("resolution y", "Video"); - bool fullscreen = settings.getBool("fullscreen", "Video"); - bool windowBorder = settings.getBool("window border", "Video"); - bool vsync = settings.getBool("vsync", "Video"); - unsigned int antialiasing = std::max(0, settings.getInt("antialiasing", "Video")); - - int pos_x = SDL_WINDOWPOS_CENTERED_DISPLAY(screen), - pos_y = SDL_WINDOWPOS_CENTERED_DISPLAY(screen); - - if(fullscreen) + if (windowMode == Settings::WindowMode::Fullscreen || windowMode == Settings::WindowMode::WindowedFullscreen) { pos_x = SDL_WINDOWPOS_UNDEFINED_DISPLAY(screen); pos_y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(screen); } - Uint32 flags = SDL_WINDOW_OPENGL|SDL_WINDOW_SHOWN|SDL_WINDOW_RESIZABLE; - if(fullscreen) + Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; + if (windowMode == Settings::WindowMode::Fullscreen) flags |= SDL_WINDOW_FULLSCREEN; + else if (windowMode == Settings::WindowMode::WindowedFullscreen) + flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; + + // Allows for Windows snapping features to properly work in borderless window + SDL_SetHint("SDL_BORDERLESS_WINDOWED_STYLE", "1"); + SDL_SetHint("SDL_BORDERLESS_RESIZABLE_STYLE", "1"); if (!windowBorder) flags |= SDL_WINDOW_BORDERLESS; - SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, - settings.getBool("minimize on focus loss", "Video") ? "1" : "0"); + SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, Settings::video().mMinimizeOnFocusLoss ? "1" : "0"); checkSDLError(SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8)); @@ -551,9 +536,10 @@ void OMW::Engine::createWindow(Settings::Manager& settings) // Try with a lower AA if (antialiasing > 0) { - Log(Debug::Warning) << "Warning: " << antialiasing << "x antialiasing not supported, trying " << antialiasing/2; + Log(Debug::Warning) << "Warning: " << antialiasing << "x antialiasing not supported, trying " + << antialiasing / 2; antialiasing /= 2; - Settings::Manager::setInt("antialiasing", "Video", antialiasing); + Settings::video().mAntialiasing.set(antialiasing); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); continue; } @@ -566,28 +552,41 @@ void OMW::Engine::createWindow(Settings::Manager& settings) } } + // Since we use physical resolution internally, we have to create the window with scaled resolution, + // but we can't get the scale before the window exists, so instead we have to resize aftewards. + int w, h; + SDL_GetWindowSize(mWindow, &w, &h); + int dw, dh; + SDL_GL_GetDrawableSize(mWindow, &dw, &dh); + if (dw != w || dh != h) + { + SDL_SetWindowSize(mWindow, width / (dw / w), height / (dh / h)); + } + setWindowIcon(); osg::ref_ptr traits = new osg::GraphicsContext::Traits; SDL_GetWindowPosition(mWindow, &traits->x, &traits->y); - SDL_GetWindowSize(mWindow, &traits->width, &traits->height); + SDL_GL_GetDrawableSize(mWindow, &traits->width, &traits->height); traits->windowName = SDL_GetWindowTitle(mWindow); - traits->windowDecoration = !(SDL_GetWindowFlags(mWindow)&SDL_WINDOW_BORDERLESS); + traits->windowDecoration = !(SDL_GetWindowFlags(mWindow) & SDL_WINDOW_BORDERLESS); traits->screenNum = SDL_GetWindowDisplayIndex(mWindow); - traits->vsync = vsync; + traits->vsync = 0; traits->inheritedWindowData = new SDLUtil::GraphicsWindowSDL2::WindowData(mWindow); - graphicsWindow = new SDLUtil::GraphicsWindowSDL2(traits); - if (!graphicsWindow->valid()) throw std::runtime_error("Failed to create GraphicsContext"); + graphicsWindow = new SDLUtil::GraphicsWindowSDL2(traits, vsync); + if (!graphicsWindow->valid()) + throw std::runtime_error("Failed to create GraphicsContext"); if (traits->samples < antialiasing) { - Log(Debug::Warning) << "Warning: Framebuffer MSAA level is only " << traits->samples << "x instead of " << antialiasing << "x. Trying " << antialiasing / 2 << "x instead."; + Log(Debug::Warning) << "Warning: Framebuffer MSAA level is only " << traits->samples << "x instead of " + << antialiasing << "x. Trying " << antialiasing / 2 << "x instead."; graphicsWindow->closeImplementation(); SDL_DestroyWindow(mWindow); mWindow = nullptr; antialiasing /= 2; - Settings::Manager::setInt("antialiasing", "Video", antialiasing); + Settings::video().mAntialiasing.set(antialiasing); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); continue; } @@ -608,18 +607,88 @@ void OMW::Engine::createWindow(Settings::Manager& settings) camera->setGraphicsContext(graphicsWindow); camera->setViewport(0, 0, graphicsWindow->getTraits()->width, graphicsWindow->getTraits()->height); + osg::ref_ptr realizeOperations = new SceneUtil::OperationSequence(false); + mViewer->setRealizeOperation(realizeOperations); + osg::ref_ptr identifyOp = new IdentifyOpenGLOperation(); + realizeOperations->add(identifyOp); + realizeOperations->add(new SceneUtil::GetGLExtensionsOperation()); + if (Debug::shouldDebugOpenGL()) - mViewer->setRealizeOperation(new Debug::EnableGLDebugOperation()); + realizeOperations->add(new Debug::EnableGLDebugOperation()); + + realizeOperations->add(mSelectDepthFormatOperation); + realizeOperations->add(mSelectColorFormatOperation); + + if (Stereo::getStereo()) + { + Stereo::Settings settings; + + settings.mMultiview = Settings::stereo().mMultiview; + settings.mAllowDisplayListsForMultiview = Settings::stereo().mAllowDisplayListsForMultiview; + settings.mSharedShadowMaps = Settings::stereo().mSharedShadowMaps; + + if (Settings::stereo().mUseCustomView) + { + const osg::Vec3 leftEyeOffset(Settings::stereoView().mLeftEyeOffsetX, + Settings::stereoView().mLeftEyeOffsetY, Settings::stereoView().mLeftEyeOffsetZ); + + const osg::Quat leftEyeOrientation(Settings::stereoView().mLeftEyeOrientationX, + Settings::stereoView().mLeftEyeOrientationY, Settings::stereoView().mLeftEyeOrientationZ, + Settings::stereoView().mLeftEyeOrientationW); + + const osg::Vec3 rightEyeOffset(Settings::stereoView().mRightEyeOffsetX, + Settings::stereoView().mRightEyeOffsetY, Settings::stereoView().mRightEyeOffsetZ); + + const osg::Quat rightEyeOrientation(Settings::stereoView().mRightEyeOrientationX, + Settings::stereoView().mRightEyeOrientationY, Settings::stereoView().mRightEyeOrientationZ, + Settings::stereoView().mRightEyeOrientationW); + + settings.mCustomView = Stereo::CustomView{ + .mLeft = Stereo::View{ + .pose = Stereo::Pose{ + .position = leftEyeOffset, + .orientation = leftEyeOrientation, + }, + .fov = Stereo::FieldOfView{ + .angleLeft = Settings::stereoView().mLeftEyeFovLeft, + .angleRight = Settings::stereoView().mLeftEyeFovRight, + .angleUp = Settings::stereoView().mLeftEyeFovUp, + .angleDown = Settings::stereoView().mLeftEyeFovDown, + }, + }, + .mRight = Stereo::View{ + .pose = Stereo::Pose{ + .position = rightEyeOffset, + .orientation = rightEyeOrientation, + }, + .fov = Stereo::FieldOfView{ + .angleLeft = Settings::stereoView().mRightEyeFovLeft, + .angleRight = Settings::stereoView().mRightEyeFovRight, + .angleUp = Settings::stereoView().mRightEyeFovUp, + .angleDown = Settings::stereoView().mRightEyeFovDown, + }, + }, + }; + } + + if (Settings::stereo().mUseCustomEyeResolution) + settings.mEyeResolution + = osg::Vec2i(Settings::stereoView().mEyeResolutionX, Settings::stereoView().mEyeResolutionY); + + realizeOperations->add(new Stereo::InitializeStereoOperation(settings)); + } mViewer->realize(); + mGlMaxTextureImageUnits = identifyOp->getMaxTextureImageUnits(); - mViewer->getEventQueue()->getCurrentEventState()->setWindowRectangle(0, 0, graphicsWindow->getTraits()->width, graphicsWindow->getTraits()->height); + mViewer->getEventQueue()->getCurrentEventState()->setWindowRectangle( + 0, 0, graphicsWindow->getTraits()->width, graphicsWindow->getTraits()->height); } void OMW::Engine::setWindowIcon() { - boost::filesystem::ifstream windowIconStream; - std::string windowIcon = (mResDir / "mygui" / "openmw.png").string(); + std::ifstream windowIconStream; + const auto windowIcon = mResDir / "openmw.png"; windowIconStream.open(windowIcon, std::ios_base::in | std::ios_base::binary); if (windowIconStream.fail()) Log(Debug::Error) << "Error: Failed to open " << windowIcon; @@ -631,7 +700,8 @@ void OMW::Engine::setWindowIcon() } osgDB::ReaderWriter::ReadResult result = reader->readImage(windowIconStream); if (!result.success()) - Log(Debug::Error) << "Error: Failed to read " << windowIcon << ": " << result.message() << " code " << result.status(); + Log(Debug::Error) << "Error: Failed to read " << windowIcon << ": " << result.message() << " code " + << result.status(); else { osg::ref_ptr image = result.getImage(); @@ -640,347 +710,373 @@ void OMW::Engine::setWindowIcon() } } -void OMW::Engine::prepareEngine (Settings::Manager & settings) +void OMW::Engine::prepareEngine() { - mEnvironment.setStateManager ( - new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles.at (0))); + mStateManager = std::make_unique(mCfgMgr.getUserDataPath() / "saves", mContentFiles); + mEnvironment.setStateManager(*mStateManager); - createWindow(settings); + const bool stereoEnabled = Settings::stereo().mStereoEnabled || osg::DisplaySettings::instance().get()->getStereo(); + mStereoManager = std::make_unique( + mViewer, stereoEnabled, Settings::camera().mNearClip, Settings::camera().mViewingDistance); - osg::ref_ptr rootNode (new osg::Group); + osg::ref_ptr rootNode(new osg::Group); mViewer->setSceneData(rootNode); - mVFS.reset(new VFS::Manager(mFSStrict)); + createWindow(); + + mVFS = std::make_unique(); VFS::registerArchives(mVFS.get(), mFileCollections, mArchives, true); - mResourceSystem.reset(new Resource::ResourceSystem(mVFS.get())); - mResourceSystem->getSceneManager()->setUnRefImageDataAfterApply(false); // keep to Off for now to allow better state sharing - mResourceSystem->getSceneManager()->setFilterSettings( - Settings::Manager::getString("texture mag filter", "General"), - Settings::Manager::getString("texture min filter", "General"), - Settings::Manager::getString("texture mipmap", "General"), - Settings::Manager::getInt("anisotropy", "General") - ); + mResourceSystem = std::make_unique( + mVFS.get(), Settings::cells().mCacheExpiryDelay, &mEncoder.get()->getStatelessEncoder()); + mResourceSystem->getSceneManager()->getShaderManager().setMaxTextureUnits(mGlMaxTextureImageUnits); + mResourceSystem->getSceneManager()->setUnRefImageDataAfterApply( + false); // keep to Off for now to allow better state sharing + mResourceSystem->getSceneManager()->setFilterSettings(Settings::general().mTextureMagFilter, + Settings::general().mTextureMinFilter, Settings::general().mTextureMipmap, Settings::general().mAnisotropy); + mEnvironment.setResourceSystem(*mResourceSystem); + + mWorkQueue = new SceneUtil::WorkQueue(Settings::cells().mPreloadNumThreads); + mUnrefQueue = std::make_unique(); + + mScreenCaptureOperation = new SceneUtil::AsyncScreenCaptureOperation(mWorkQueue, + new SceneUtil::WriteScreenshotToFileOperation(mCfgMgr.getScreenshotPath(), + Settings::general().mScreenshotFormat, + Settings::general().mNotifyOnSavedScreenshot ? std::function(ScreenCaptureMessageBox{}) + : std::function(IgnoreString{}))); + + mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(mScreenCaptureOperation); + + mViewer->addEventHandler(mScreenCaptureHandler); - int numThreads = Settings::Manager::getInt("preload num threads", "Cells"); - if (numThreads <= 0) - throw std::runtime_error("Invalid setting: 'preload num threads' must be >0"); - mWorkQueue = new SceneUtil::WorkQueue(numThreads); + mL10nManager = std::make_unique(mVFS.get()); + mL10nManager->setPreferredLocales(Settings::general().mPreferredLocales, Settings::general().mGmstOverridesL10n); + mEnvironment.setL10nManager(*mL10nManager); + + mLuaManager = std::make_unique(mVFS.get(), mResDir / "lua_libs"); + mEnvironment.setLuaManager(*mLuaManager); // Create input and UI first to set up a bootstrapping environment for // showing a loading screen and keeping the window responsive while doing so - std::string keybinderUser = (mCfgMgr.getUserConfigPath() / "input_v3.xml").string(); - bool keybinderUserExists = boost::filesystem::exists(keybinderUser); - if(!keybinderUserExists) + const auto keybinderUser = mCfgMgr.getUserConfigPath() / "input_v3.xml"; + bool keybinderUserExists = std::filesystem::exists(keybinderUser); + if (!keybinderUserExists) { - std::string input2 = (mCfgMgr.getUserConfigPath() / "input_v2.xml").string(); - if(boost::filesystem::exists(input2)) { - boost::filesystem::copy_file(input2, keybinderUser); - keybinderUserExists = boost::filesystem::exists(keybinderUser); + const auto input2 = (mCfgMgr.getUserConfigPath() / "input_v2.xml"); + if (std::filesystem::exists(input2)) + { + keybinderUserExists = std::filesystem::copy_file(input2, keybinderUser); Log(Debug::Info) << "Loading keybindings file: " << keybinderUser; } } else Log(Debug::Info) << "Loading keybindings file: " << keybinderUser; - const std::string userdefault = mCfgMgr.getUserConfigPath().string() + "/gamecontrollerdb.txt"; - const std::string localdefault = mCfgMgr.getLocalPath().string() + "/gamecontrollerdb.txt"; - const std::string globaldefault = mCfgMgr.getGlobalPath().string() + "/gamecontrollerdb.txt"; + const auto userdefault = mCfgMgr.getUserConfigPath() / "gamecontrollerdb.txt"; + const auto localdefault = mCfgMgr.getLocalPath() / "gamecontrollerdb.txt"; + const auto globaldefault = mCfgMgr.getGlobalPath() / "gamecontrollerdb.txt"; - std::string userGameControllerdb; - if (boost::filesystem::exists(userdefault)){ + std::filesystem::path userGameControllerdb; + if (std::filesystem::exists(userdefault)) userGameControllerdb = userdefault; - } - else - userGameControllerdb = ""; - std::string gameControllerdb; - if (boost::filesystem::exists(localdefault)) + std::filesystem::path gameControllerdb; + if (std::filesystem::exists(localdefault)) gameControllerdb = localdefault; - else if (boost::filesystem::exists(globaldefault)) + else if (std::filesystem::exists(globaldefault)) gameControllerdb = globaldefault; - else - gameControllerdb = ""; //if it doesn't exist, pass in an empty string + // else if it doesn't exist, pass in an empty string + + // gui needs our shaders path before everything else + mResourceSystem->getSceneManager()->setShaderPath(mResDir / "shaders"); + + osg::GLExtensions& exts = SceneUtil::getGLExtensions(); + bool shadersSupported = exts.glslLanguageVersion >= 1.2f; + +#if OSG_VERSION_LESS_THAN(3, 6, 6) + // hack fix for https://github.com/openscenegraph/OpenSceneGraph/issues/1028 + if (!osg::isGLExtensionSupported(exts.contextID, "NV_framebuffer_multisample_coverage")) + exts.glRenderbufferStorageMultisampleCoverageNV = nullptr; +#endif - std::string myguiResources = (mResDir / "mygui").string(); osg::ref_ptr guiRoot = new osg::Group; guiRoot->setName("GUI Root"); guiRoot->setNodeMask(MWRender::Mask_GUI); + mStereoManager->disableStereoForNode(guiRoot); rootNode->addChild(guiRoot); - MWGui::WindowManager* window = new MWGui::WindowManager(mWindow, mViewer, guiRoot, mResourceSystem.get(), mWorkQueue.get(), - mCfgMgr.getLogPath().string() + std::string("/"), myguiResources, - mScriptConsoleMode, mTranslationDataStorage, mEncoding, mExportFonts, - Version::getOpenmwVersionDescription(mResDir.string()), mCfgMgr.getUserConfigPath().string()); - mEnvironment.setWindowManager (window); - MWInput::InputManager* input = new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); - mEnvironment.setInputManager (input); + mWindowManager = std::make_unique(mWindow, mViewer, guiRoot, mResourceSystem.get(), + mWorkQueue.get(), mCfgMgr.getLogPath(), mScriptConsoleMode, mTranslationDataStorage, mEncoding, + Version::getOpenmwVersionDescription(), shadersSupported, mCfgMgr); + mEnvironment.setWindowManager(*mWindowManager); + + mInputManager = std::make_unique(mWindow, mViewer, mScreenCaptureHandler, keybinderUser, + keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); + mEnvironment.setInputManager(*mInputManager); // Create sound system - mEnvironment.setSoundManager (new MWSound::SoundManager(mVFS.get(), mUseSound)); + mSoundManager = std::make_unique(mVFS.get(), mUseSound); + mEnvironment.setSoundManager(*mSoundManager); + + // Create the world + mWorld = std::make_unique( + mResourceSystem.get(), mActivationDistanceOverride, mCellName, mCfgMgr.getUserDataPath()); + mEnvironment.setWorld(*mWorld); + mEnvironment.setWorldModel(mWorld->getWorldModel()); + mEnvironment.setESMStore(mWorld->getStore()); + + Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); + Loading::AsyncListener asyncListener(*listener); + auto dataLoading = std::async(std::launch::async, + [&] { mWorld->loadData(mFileCollections, mContentFiles, mGroundcoverFiles, mEncoder.get(), &asyncListener); }); if (!mSkipMenu) { - const std::string& logo = Fallback::Map::getString("Movies_Company_Logo"); + std::string_view logo = Fallback::Map::getString("Movies_Company_Logo"); if (!logo.empty()) - window->playVideo(logo, true); + mWindowManager->playVideo(logo, true); } - // Create the world - mEnvironment.setWorld( new MWWorld::World (mViewer, rootNode, mResourceSystem.get(), mWorkQueue.get(), - mFileCollections, mContentFiles, mGroundcoverFiles, mEncoder, mActivationDistanceOverride, mCellName, - mStartupScript, mResDir.string(), mCfgMgr.getUserDataPath().string())); - mEnvironment.getWorld()->setupPlayer(); + listener->loadingOn(); + { + using namespace std::chrono_literals; + while (dataLoading.wait_for(50ms) != std::future_status::ready) + asyncListener.update(); + dataLoading.get(); + } + listener->loadingOff(); + + mWorld->init(mViewer, std::move(rootNode), mWorkQueue.get(), *mUnrefQueue); + mEnvironment.setWorldScene(mWorld->getWorldScene()); + mWorld->setupPlayer(); + mWorld->setRandomSeed(mRandomSeed); + + const MWWorld::Store* gmst = &mWorld->getStore().get(); + mL10nManager->setGmstLoader( + [gmst, misses = std::set>()](std::string_view gmstName) mutable { + const ESM::GameSetting* res = gmst->search(gmstName); + if (res && res->mValue.getType() == ESM::VT_String) + return res->mValue.getString(); + else + { + if (misses.count(gmstName) == 0) + { + misses.emplace(gmstName); + Log(Debug::Error) << "GMST " << gmstName << " not found"; + } + return std::string("GMST:") + std::string(gmstName); + } + }); - window->setStore(mEnvironment.getWorld()->getStore()); - window->initUI(); + mWindowManager->setStore(mWorld->getStore()); + mWindowManager->initUI(); - //Load translation data - mTranslationDataStorage.setEncoder(mEncoder); - for (size_t i = 0; i < mContentFiles.size(); i++) - mTranslationDataStorage.loadTranslationData(mFileCollections, mContentFiles[i]); + // Load translation data + mTranslationDataStorage.setEncoder(mEncoder.get()); + for (auto& mContentFile : mContentFiles) + mTranslationDataStorage.loadTranslationData(mFileCollections, mContentFile); - Compiler::registerExtensions (mExtensions); + Compiler::registerExtensions(mExtensions); // Create script system - mScriptContext = new MWScript::CompilerContext (MWScript::CompilerContext::Type_Full); - mScriptContext->setExtensions (&mExtensions); + mScriptContext = std::make_unique(MWScript::CompilerContext::Type_Full); + mScriptContext->setExtensions(&mExtensions); - mEnvironment.setScriptManager (new MWScript::ScriptManager (mEnvironment.getWorld()->getStore(), *mScriptContext, mWarningsMode, - mScriptBlacklistUse ? mScriptBlacklist : std::vector())); + mScriptManager = std::make_unique(mWorld->getStore(), *mScriptContext, mWarningsMode, + mScriptBlacklistUse ? mScriptBlacklist : std::vector()); + mEnvironment.setScriptManager(*mScriptManager); // Create game mechanics system - MWMechanics::MechanicsManager* mechanics = new MWMechanics::MechanicsManager; - mEnvironment.setMechanicsManager (mechanics); + mMechanicsManager = std::make_unique(); + mEnvironment.setMechanicsManager(*mMechanicsManager); // Create dialog system - mEnvironment.setJournal (new MWDialogue::Journal); - mEnvironment.setDialogueManager (new MWDialogue::DialogueManager (mExtensions, mTranslationDataStorage)); - mEnvironment.setResourceSystem(mResourceSystem.get()); + mJournal = std::make_unique(); + mEnvironment.setJournal(*mJournal); + + mDialogueManager = std::make_unique(mExtensions, mTranslationDataStorage); + mEnvironment.setDialogueManager(*mDialogueManager); // scripts if (mCompileAll) { - std::pair result = mEnvironment.getScriptManager()->compileAll(); + std::pair result = mScriptManager->compileAll(); if (result.first) - Log(Debug::Info) - << "compiled " << result.second << " of " << result.first << " scripts (" - << 100*static_cast (result.second)/result.first - << "%)"; + Log(Debug::Info) << "compiled " << result.second << " of " << result.first << " scripts (" + << 100 * static_cast(result.second) / result.first << "%)"; } if (mCompileAllDialogue) { std::pair result = MWDialogue::ScriptTest::compileAll(&mExtensions, mWarningsMode); if (result.first) - Log(Debug::Info) - << "compiled " << result.second << " of " << result.first << " dialogue script/actor combinations a(" - << 100*static_cast (result.second)/result.first - << "%)"; + Log(Debug::Info) << "compiled " << result.second << " of " << result.first << " dialogue scripts (" + << 100 * static_cast(result.second) / result.first << "%)"; } -} - -class WriteScreenshotToFileOperation : public osgViewer::ScreenCaptureHandler::CaptureOperation -{ -public: - WriteScreenshotToFileOperation(const std::string& screenshotPath, const std::string& screenshotFormat) - : mScreenshotPath(screenshotPath) - , mScreenshotFormat(screenshotFormat) - { - } - - void operator()(const osg::Image& image, const unsigned int context_id) override - { - // Count screenshots. - int shotCount = 0; - - // Find the first unused filename with a do-while - std::ostringstream stream; - do - { - // Reset the stream - stream.str(""); - stream.clear(); - - stream << mScreenshotPath << "/screenshot" << std::setw(3) << std::setfill('0') << shotCount++ << "." << mScreenshotFormat; - - } while (boost::filesystem::exists(stream.str())); - - boost::filesystem::ofstream outStream; - outStream.open(boost::filesystem::path(stream.str()), std::ios::binary); - - osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension(mScreenshotFormat); - if (!readerwriter) - { - Log(Debug::Error) << "Error: Can't write screenshot, no '" << mScreenshotFormat << "' readerwriter found"; - return; - } - osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(image, outStream); - if (!result.success()) - { - Log(Debug::Error) << "Error: Can't write screenshot: " << result.message() << " code " << result.status(); - } - } + mLuaManager->loadPermanentStorage(mCfgMgr.getUserConfigPath()); + mLuaManager->init(); -private: - std::string mScreenshotPath; - std::string mScreenshotFormat; -}; + // starts a separate lua thread if "lua num threads" > 0 + mLuaWorker = std::make_unique(*mLuaManager); +} // Initialise and enter main loop. void OMW::Engine::go() { - assert (!mContentFiles.empty()); + assert(!mContentFiles.empty()); Log(Debug::Info) << "OSG version: " << osgGetVersion(); SDL_version sdlVersion; SDL_GetVersion(&sdlVersion); - Log(Debug::Info) << "SDL version: " << (int)sdlVersion.major << "." << (int)sdlVersion.minor << "." << (int)sdlVersion.patch; + Log(Debug::Info) << "SDL version: " << (int)sdlVersion.major << "." << (int)sdlVersion.minor << "." + << (int)sdlVersion.patch; Misc::Rng::init(mRandomSeed); - // Load settings - Settings::Manager settings; - std::string settingspath; - settingspath = loadSettings (settings); + Settings::ShaderManager::get().load(mCfgMgr.getUserConfigPath() / "shaders.yaml"); MWClass::registerClasses(); // Create encoder - mEncoder = new ToUTF8::Utf8Encoder(mEncoding); + mEncoder = std::make_unique(mEncoding); // Setup viewer mViewer = new osgViewer::Viewer; mViewer->setReleaseContextAtEndOfFrameHint(false); -#if OSG_VERSION_GREATER_OR_EQUAL(3,5,5) // Do not try to outsmart the OS thread scheduler (see bug #4785). mViewer->setUseConfigureAffinity(false); -#endif - mScreenCaptureOperation = new WriteScreenshotToFileOperation( - mCfgMgr.getScreenshotPath().string(), - Settings::Manager::getString("screenshot format", "General")); + mEnvironment.setFrameRateLimit(Settings::video().mFramerateLimit); - mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(mScreenCaptureOperation); - - mViewer->addEventHandler(mScreenCaptureHandler); + prepareEngine(); - mEnvironment.setFrameRateLimit(Settings::Manager::getFloat("framerate limit", "Video")); +#ifdef _WIN32 + const auto* stats_file = _wgetenv(L"OPENMW_OSG_STATS_FILE"); +#else + const auto* stats_file = std::getenv("OPENMW_OSG_STATS_FILE"); +#endif - prepareEngine (settings); + std::filesystem::path path; + if (stats_file != nullptr) + path = stats_file; std::ofstream stats; - if (const auto path = std::getenv("OPENMW_OSG_STATS_FILE")) + if (!path.empty()) { stats.open(path, std::ios_base::out); if (stats.is_open()) - Log(Debug::Info) << "Stats will be written to: " << path; + Log(Debug::Info) << "OSG stats will be written to: " << path; else - Log(Debug::Warning) << "Failed to open file for stats: " << path; + Log(Debug::Warning) << "Failed to open file to write OSG stats \"" << path + << "\": " << std::generic_category().message(errno); } // Setup profiler - osg::ref_ptr statshandler = new Resource::Profiler(stats.is_open()); + osg::ref_ptr statsHandler = new Resource::Profiler(stats.is_open(), *mVFS); - initStatsHandler(*statshandler); + initStatsHandler(*statsHandler); - mViewer->addEventHandler(statshandler); + mViewer->addEventHandler(statsHandler); - osg::ref_ptr resourceshandler = new Resource::StatsHandler(stats.is_open()); - mViewer->addEventHandler(resourceshandler); + osg::ref_ptr resourcesHandler = new Resource::StatsHandler(stats.is_open(), *mVFS); + mViewer->addEventHandler(resourcesHandler); if (stats.is_open()) - Resource::CollectStatistics(mViewer); + Resource::collectStatistics(*mViewer); // Start the game if (!mSaveGameFile.empty()) { - mEnvironment.getStateManager()->loadGame(mSaveGameFile); + mStateManager->loadGame(mSaveGameFile); } else if (!mSkipMenu) { // start in main menu - mEnvironment.getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); - mEnvironment.getSoundManager()->playTitleMusic(); - const std::string& logo = Fallback::Map::getString("Movies_Morrowind_Logo"); + mWindowManager->pushGuiMode(MWGui::GM_MainMenu); + + if (mVFS->exists(MWSound::titleMusic)) + mSoundManager->streamMusic(MWSound::titleMusic, MWSound::MusicType::Normal); + else + Log(Debug::Warning) << "Title music not found"; + + std::string_view logo = Fallback::Map::getString("Movies_Morrowind_Logo"); if (!logo.empty()) - mEnvironment.getWindowManager()->playVideo(logo, true); + mWindowManager->playVideo(logo, /*allowSkipping*/ true, /*overrideSounds*/ false); } else { - mEnvironment.getStateManager()->newGame (!mNewGame); + mStateManager->newGame(!mNewGame); } - if (!mStartupScript.empty() && mEnvironment.getStateManager()->getState() == MWState::StateManager::State_Running) + if (!mStartupScript.empty() && mStateManager->getState() == MWState::StateManager::State_Running) { - mEnvironment.getWindowManager()->executeInConsole(mStartupScript); + mWindowManager->executeInConsole(mStartupScript); } // Start the main rendering loop - double simulationTime = 0.0; + MWWorld::DateTimeManager& timeManager = *mWorld->getTimeManager(); Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(mEnvironment.getFrameRateLimit()); const std::chrono::steady_clock::duration maxSimulationInterval(std::chrono::milliseconds(200)); - while (!mViewer->done() && !mEnvironment.getStateManager()->hasQuitRequest()) + while (!mViewer->done() && !mStateManager->hasQuitRequest()) { - const double dt = std::chrono::duration_cast>(std::min( - frameRateLimiter.getLastFrameDuration(), - maxSimulationInterval - )).count(); + const double dt = std::chrono::duration_cast>( + std::min(frameRateLimiter.getLastFrameDuration(), maxSimulationInterval)) + .count() + * timeManager.getSimulationTimeScale(); + + mViewer->advance(timeManager.getRenderingSimulationTime()); - mViewer->advance(simulationTime); + const unsigned frameNumber = mViewer->getFrameStamp()->getFrameNumber(); - if (!frame(dt)) + if (!frame(frameNumber, dt)) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); continue; } - else + timeManager.updateIsPaused(); + if (!timeManager.isPaused()) { - mViewer->eventTraversal(); - mViewer->updateTraversal(); - - mEnvironment.getWorld()->updateWindowManager(); - - mViewer->renderingTraversals(); - - bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); - if (!guiActive) - simulationTime += dt; + timeManager.setSimulationTime(timeManager.getSimulationTime() + dt); + timeManager.setRenderingSimulationTime(timeManager.getRenderingSimulationTime() + dt); } if (stats) { - const auto frameNumber = mViewer->getFrameStamp()->getFrameNumber(); - if (frameNumber >= 2) + // The delay is required because rendering happens in parallel to the main thread and stats from there is + // available with delay. + constexpr unsigned statsReportDelay = 3; + if (frameNumber >= statsReportDelay) { - mViewer->getViewerStats()->report(stats, frameNumber - 2); - osgViewer::Viewer::Cameras cameras; - mViewer->getCameras(cameras); - for (auto camera : cameras) - camera->getStats()->report(stats, frameNumber - 2); + // Viewer frame number can be different from frameNumber because of loading screens which render new + // frames inside a simulation frame. + const unsigned currentFrameNumber = mViewer->getFrameStamp()->getFrameNumber(); + for (unsigned i = frameNumber; i <= currentFrameNumber; ++i) + reportStats(i - statsReportDelay, *mViewer, stats); } } frameRateLimiter.limit(); } - // Save user settings - settings.saveUser(settingspath); + mLuaWorker->join(); - mViewer->stopThreading(); + // Save user settings + 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) +void OMW::Engine::setCompileAll(bool all) { mCompileAll = all; } -void OMW::Engine::setCompileAllDialogue (bool all) +void OMW::Engine::setCompileAllDialogue(bool all) { mCompileAllDialogue = all; } @@ -995,42 +1091,37 @@ void OMW::Engine::setEncoding(const ToUTF8::FromType& encoding) mEncoding = encoding; } -void OMW::Engine::setScriptConsoleMode (bool enabled) +void OMW::Engine::setScriptConsoleMode(bool enabled) { mScriptConsoleMode = enabled; } -void OMW::Engine::setStartupScript (const std::string& path) +void OMW::Engine::setStartupScript(const std::filesystem::path& path) { mStartupScript = path; } -void OMW::Engine::setActivationDistanceOverride (int distance) +void OMW::Engine::setActivationDistanceOverride(int distance) { mActivationDistanceOverride = distance; } -void OMW::Engine::setWarningsMode (int mode) +void OMW::Engine::setWarningsMode(int mode) { mWarningsMode = mode; } -void OMW::Engine::setScriptBlacklist (const std::vector& list) +void OMW::Engine::setScriptBlacklist(const std::vector& list) { mScriptBlacklist = list; } -void OMW::Engine::setScriptBlacklistUse (bool use) +void OMW::Engine::setScriptBlacklistUse(bool use) { mScriptBlacklistUse = use; } -void OMW::Engine::enableFontExport(bool exportFonts) -{ - mExportFonts = exportFonts; -} - -void OMW::Engine::setSaveGameFile(const std::string &savegame) +void OMW::Engine::setSaveGameFile(const std::filesystem::path& savegame) { mSaveGameFile = savegame; } diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 1aef62df53f..bf7bf7441b0 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -1,18 +1,19 @@ #ifndef ENGINE_H #define ENGINE_H +#include + #include +#include #include -#include #include +#include #include #include #include "mwbase/environment.hpp" -#include "mwworld/ptr.hpp" - namespace Resource { class ResourceSystem; @@ -21,6 +22,8 @@ namespace Resource namespace SceneUtil { class WorkQueue; + class AsyncScreenCaptureOperation; + class UnrefQueue; } namespace VFS @@ -33,6 +36,17 @@ namespace Compiler class Context; } +namespace MWLua +{ + class LuaManager; + class Worker; +} + +namespace Stereo +{ + class Manager; +} + namespace Files { struct ConfigurationManager; @@ -43,148 +57,214 @@ namespace osgViewer class ScreenCaptureHandler; } -struct SDL_Window; - -namespace OMW +namespace SceneUtil { - /// \brief Main engine class, that brings together all the components of OpenMW - class Engine - { - SDL_Window* mWindow; - std::unique_ptr mVFS; - std::unique_ptr mResourceSystem; - osg::ref_ptr mWorkQueue; - MWBase::Environment mEnvironment; - ToUTF8::FromType mEncoding; - ToUTF8::Utf8Encoder* mEncoder; - Files::PathContainer mDataDirs; - std::vector mArchives; - boost::filesystem::path mResDir; - osg::ref_ptr mViewer; - osg::ref_ptr mScreenCaptureHandler; - osgViewer::ScreenCaptureHandler::CaptureOperation *mScreenCaptureOperation; - std::string mCellName; - std::vector mContentFiles; - std::vector mGroundcoverFiles; - bool mSkipMenu; - bool mUseSound; - bool mCompileAll; - bool mCompileAllDialogue; - int mWarningsMode; - std::string mFocusName; - bool mScriptConsoleMode; - std::string mStartupScript; - int mActivationDistanceOverride; - std::string mSaveGameFile; - // Grab mouse? - bool mGrab; - - bool mExportFonts; - unsigned int mRandomSeed; - - Compiler::Extensions mExtensions; - Compiler::Context *mScriptContext; - - Files::Collections mFileCollections; - bool mFSStrict; - Translation::Storage mTranslationDataStorage; - std::vector mScriptBlacklist; - bool mScriptBlacklistUse; - bool mNewGame; - - // not implemented - Engine (const Engine&); - Engine& operator= (const Engine&); - - void executeLocalScripts(); + class SelectDepthFormatOperation; - bool frame (float dt); - - /// Load settings from various files, returns the path to the user settings file - std::string loadSettings (Settings::Manager & settings); - - /// Prepare engine for game play - void prepareEngine (Settings::Manager & settings); - - void createWindow(Settings::Manager& settings); - void setWindowIcon(); - - public: - Engine(Files::ConfigurationManager& configurationManager); - virtual ~Engine(); + namespace Color + { + class SelectColorFormatOperation; + } +} - /// Enable strict filesystem mode (do not fold case) - /// - /// \attention The strict mode must be specified before any path-related settings - /// are given to the engine. - void enableFSStrict(bool fsStrict); +namespace MWState +{ + class StateManager; +} - /// Set data dirs - void setDataDirs(const Files::PathContainer& dataDirs); +namespace MWGui +{ + class WindowManager; +} - /// Add BSA archive - void addArchive(const std::string& archive); +namespace MWInput +{ + class InputManager; +} - /// Set resource dir - void setResourceDir(const boost::filesystem::path& parResDir); +namespace MWSound +{ + class SoundManager; +} - /// Set start cell name - void setCell(const std::string& cellName); +namespace MWWorld +{ + class World; +} - /** - * @brief addContentFile - Adds content file (ie. esm/esp, or omwgame/omwaddon) to the content files container. - * @param file - filename (extension is required) - */ - void addContentFile(const std::string& file); - void addGroundcoverFile(const std::string& file); +namespace MWScript +{ + class ScriptManager; +} - /// Disable or enable all sounds - void setSoundUsage(bool soundUsage); +namespace MWMechanics +{ + class MechanicsManager; +} - /// Skip main menu and go directly into the game - /// - /// \param newGame Start a new game instead off dumping the player into the game - /// (ignored if !skipMenu). - void setSkipMenu (bool skipMenu, bool newGame); +namespace MWDialogue +{ + class DialogueManager; +} - void setGrabMouse(bool grab) { mGrab = grab; } +namespace MWDialogue +{ + class Journal; +} - /// Initialise and enter main loop. - void go(); +namespace l10n +{ + class Manager; +} - /// Compile all scripts (excludign dialogue scripts) at startup? - void setCompileAll (bool all); +struct SDL_Window; - /// Compile all dialogue scripts at startup? - void setCompileAllDialogue (bool all); +namespace OMW +{ + /// \brief Main engine class, that brings together all the components of OpenMW + class Engine + { + SDL_Window* mWindow; + std::unique_ptr mVFS; + std::unique_ptr mResourceSystem; + osg::ref_ptr mWorkQueue; + std::unique_ptr mUnrefQueue; + std::unique_ptr mWorld; + std::unique_ptr mSoundManager; + std::unique_ptr mScriptManager; + std::unique_ptr mWindowManager; + std::unique_ptr mMechanicsManager; + std::unique_ptr mDialogueManager; + std::unique_ptr mJournal; + std::unique_ptr mInputManager; + std::unique_ptr mStateManager; + std::unique_ptr mLuaManager; + std::unique_ptr mLuaWorker; + std::unique_ptr mL10nManager; + MWBase::Environment mEnvironment; + ToUTF8::FromType mEncoding; + std::unique_ptr mEncoder; + Files::PathContainer mDataDirs; + std::vector mArchives; + std::filesystem::path mResDir; + osg::ref_ptr mViewer; + osg::ref_ptr mScreenCaptureHandler; + osg::ref_ptr mScreenCaptureOperation; + osg::ref_ptr mSelectDepthFormatOperation; + osg::ref_ptr mSelectColorFormatOperation; + std::string mCellName; + std::vector mContentFiles; + std::vector mGroundcoverFiles; + + std::unique_ptr mStereoManager; + + bool mSkipMenu; + bool mUseSound; + bool mCompileAll; + bool mCompileAllDialogue; + int mWarningsMode; + std::string mFocusName; + bool mScriptConsoleMode; + std::filesystem::path mStartupScript; + int mActivationDistanceOverride; + std::filesystem::path mSaveGameFile; + // Grab mouse? + bool mGrab; + + unsigned int mRandomSeed; + + Compiler::Extensions mExtensions; + std::unique_ptr mScriptContext; + + Files::Collections mFileCollections; + Translation::Storage mTranslationDataStorage; + std::vector mScriptBlacklist; + bool mScriptBlacklistUse; + bool mNewGame; + + // not implemented + Engine(const Engine&); + Engine& operator=(const Engine&); + + void executeLocalScripts(); + + bool frame(unsigned frameNumber, float dt); + + /// Prepare engine for game play + void prepareEngine(); + + void createWindow(); + void setWindowIcon(); + + public: + Engine(Files::ConfigurationManager& configurationManager); + virtual ~Engine(); + + /// Set data dirs + void setDataDirs(const Files::PathContainer& dataDirs); + + /// Add BSA archive + void addArchive(const std::string& archive); + + /// Set resource dir + void setResourceDir(const std::filesystem::path& parResDir); + + /// Set start cell name + void setCell(const std::string& cellName); + + /** + * @brief addContentFile - Adds content file (ie. esm/esp, or omwgame/omwaddon) to the content files container. + * @param file - filename (extension is required) + */ + void addContentFile(const std::string& file); + void addGroundcoverFile(const std::string& file); + + /// Disable or enable all sounds + void setSoundUsage(bool soundUsage); + + /// Skip main menu and go directly into the game + /// + /// \param newGame Start a new game instead off dumping the player into the game + /// (ignored if !skipMenu). + void setSkipMenu(bool skipMenu, bool newGame); + + void setGrabMouse(bool grab) { mGrab = grab; } + + /// Initialise and enter main loop. + void go(); + + /// Compile all scripts (excludign dialogue scripts) at startup? + void setCompileAll(bool all); - /// Font encoding - void setEncoding(const ToUTF8::FromType& encoding); + /// Compile all dialogue scripts at startup? + void setCompileAllDialogue(bool all); - /// Enable console-only script functionality - void setScriptConsoleMode (bool enabled); + /// Font encoding + void setEncoding(const ToUTF8::FromType& encoding); - /// Set path for a script that is run on startup in the console. - void setStartupScript (const std::string& path); + /// Enable console-only script functionality + void setScriptConsoleMode(bool enabled); - /// Override the game setting specified activation distance. - void setActivationDistanceOverride (int distance); + /// Set path for a script that is run on startup in the console. + void setStartupScript(const std::filesystem::path& path); - void setWarningsMode (int mode); + /// Override the game setting specified activation distance. + void setActivationDistanceOverride(int distance); - void setScriptBlacklist (const std::vector& list); + void setWarningsMode(int mode); - void setScriptBlacklistUse (bool use); + void setScriptBlacklist(const std::vector& list); - void enableFontExport(bool exportFonts); + void setScriptBlacklistUse(bool use); - /// Set the save game file to load after initialising the engine. - void setSaveGameFile(const std::string& savegame); + /// Set the save game file to load after initialising the engine. + void setSaveGameFile(const std::filesystem::path& savegame); - void setRandomSeed(unsigned int seed); + void setRandomSeed(unsigned int seed); - private: - Files::ConfigurationManager& mCfgMgr; + private: + Files::ConfigurationManager& mCfgMgr; + int mGlMaxTextureImageUnits; }; } diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 709ffda2cb2..4fa4e4f39ff 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -1,27 +1,33 @@ -#include -#include -#include +#include #include #include -#include +#include +#include #include +#include +#include + +#include "mwgui/debugwindow.hpp" #include "engine.hpp" +#include "options.hpp" + +#include #if defined(_WIN32) -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include +#include // makes __argc and __argv available on windows #include + +extern "C" __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 0x00000001; #endif +#include + #if (defined(__APPLE__) || defined(__linux) || defined(__unix) || defined(__posix)) #include #endif - using namespace Fallback; /** @@ -33,198 +39,130 @@ using namespace Fallback; * \retval true - Everything goes OK * \retval false - Error */ -bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::ConfigurationManager& cfgMgr) +bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::ConfigurationManager& cfgMgr) { // Create a local alias for brevity namespace bpo = boost::program_options; typedef std::vector StringsVector; - bpo::options_description desc("Syntax: openmw \nAllowed options"); - - desc.add_options() - ("help", "print help message") - ("version", "print version information and quit") - ("data", bpo::value()->default_value(Files::EscapePathContainer(), "data") - ->multitoken()->composing(), "set data directories (later directories have higher priority)") - - ("data-local", bpo::value()->default_value(Files::EscapePath(), ""), - "set local data directory (highest priority)") - - ("fallback-archive", bpo::value()->default_value(Files::EscapeStringVector(), "fallback-archive") - ->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)") - - ("resources", bpo::value()->default_value(Files::EscapePath(), "resources"), - "set resources directory") - - ("start", bpo::value()->default_value(""), - "set initial cell") - - ("content", bpo::value()->default_value(Files::EscapeStringVector(), "") - ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon") - - ("groundcover", bpo::value()->default_value(Files::EscapeStringVector(), "") - ->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon") - - ("no-sound", bpo::value()->implicit_value(true) - ->default_value(false), "disable all sounds") - - ("script-all", bpo::value()->implicit_value(true) - ->default_value(false), "compile all scripts (excluding dialogue scripts) at startup") - - ("script-all-dialogue", bpo::value()->implicit_value(true) - ->default_value(false), "compile all dialogue scripts at startup") - - ("script-console", bpo::value()->implicit_value(true) - ->default_value(false), "enable console-only script functionality") - - ("script-run", bpo::value()->default_value(""), - "select a file containing a list of console commands that is executed on startup") - - ("script-warn", bpo::value()->implicit_value (1) - ->default_value (1), - "handling of warnings when compiling scripts\n" - "\t0 - ignore warning\n" - "\t1 - show warning but consider script as correctly compiled anyway\n" - "\t2 - treat warnings as errors") - - ("script-blacklist", bpo::value()->default_value(Files::EscapeStringVector(), "") - ->multitoken()->composing(), "ignore the specified script (if the use of the blacklist is enabled)") - - ("script-blacklist-use", bpo::value()->implicit_value(true) - ->default_value(true), "enable script blacklisting") - - ("load-savegame", bpo::value()->default_value(Files::EscapePath(), ""), - "load a save game file on game startup (specify an absolute filename or a filename relative to the current working directory)") - - ("skip-menu", bpo::value()->implicit_value(true) - ->default_value(false), "skip main menu on game startup") - - ("new-game", bpo::value()->implicit_value(true) - ->default_value(false), "run new game sequence (ignored if skip-menu=0)") - - ("fs-strict", bpo::value()->implicit_value(true) - ->default_value(false), "strict file system handling (no case folding)") - - ("encoding", bpo::value()-> - default_value("win1252"), - "Character encoding used in OpenMW game messages:\n" - "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" - "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" - "\n\twin1252 - Western European (Latin) alphabet, used by default") - - ("fallback", bpo::value()->default_value(FallbackMap(), "") - ->multitoken()->composing(), "fallback values") - - ("no-grab", bpo::value()->implicit_value(true)->default_value(false), "Don't grab mouse cursor") - - ("export-fonts", bpo::value()->implicit_value(true) - ->default_value(false), "Export Morrowind .fnt fonts to PNG image and XML file in current directory") - - ("activate-dist", bpo::value ()->default_value (-1), "activation distance override") - - ("random-seed", bpo::value () - ->default_value(Misc::Rng::generateDefaultSeed()), - "seed value for random number generator") - ; - - bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv) - .options(desc).allow_unregistered().run(); - + bpo::options_description desc = OpenMW::makeOptionsDescription(); bpo::variables_map variables; - // Runtime options override settings from all configs - bpo::store(valid_opts, variables); + Files::parseArgs(argc, argv, variables, desc); bpo::notify(variables); - if (variables.count ("help")) + if (variables.count("help")) { - getRawStdout() << desc << std::endl; + Debug::getRawStdout() << desc << std::endl; return false; } - if (variables.count ("version")) + if (variables.count("version")) { - cfgMgr.readConfiguration(variables, desc, true); - - Version::Version v = Version::getOpenmwVersion(variables["resources"].as().mPath.string()); - getRawStdout() << v.describe() << std::endl; + Debug::getRawStdout() << Version::getOpenmwVersionDescription() << std::endl; return false; } - bpo::variables_map composingVariables = cfgMgr.separateComposingVariables(variables, desc); cfgMgr.readConfiguration(variables, desc); - cfgMgr.mergeComposingVariables(variables, composingVariables, desc); - Version::Version v = Version::getOpenmwVersion(variables["resources"].as().mPath.string()); - Log(Debug::Info) << v.describe(); + Debug::setupLogging(cfgMgr.getLogPath(), "OpenMW"); + Log(Debug::Info) << Version::getOpenmwVersionDescription(); + + Settings::Manager::load(cfgMgr); + + MWGui::DebugWindow::startLogRecording(); engine.setGrabMouse(!variables["no-grab"].as()); // Font encoding settings - std::string encoding(variables["encoding"].as().toStdString()); + std::string encoding(variables["encoding"].as()); Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); engine.setEncoding(ToUTF8::calculateEncoding(encoding)); - // directory settings - engine.enableFSStrict(variables["fs-strict"].as()); - - Files::PathContainer dataDirs(Files::EscapePath::toPathContainer(variables["data"].as())); + Files::PathContainer dataDirs(asPathContainer(variables["data"].as())); - Files::PathContainer::value_type local(variables["data-local"].as().mPath); + Files::PathContainer::value_type local(variables["data-local"] + .as() + .u8string()); // This call to u8string is redundant, but required to + // build on MSVC 14.26 due to implementation bugs. if (!local.empty()) dataDirs.push_back(local); - cfgMgr.processPaths(dataDirs); + cfgMgr.filterOutNonExistingPaths(dataDirs); - engine.setResourceDir(variables["resources"].as().mPath); + engine.setResourceDir(variables["resources"] + .as() + .u8string()); // This call to u8string is redundant, but required to build on MSVC 14.26 + // due to implementation bugs. engine.setDataDirs(dataDirs); // fallback archives - StringsVector archives = variables["fallback-archive"].as().toStdStringVector(); + StringsVector archives = variables["fallback-archive"].as(); for (StringsVector::const_iterator it = archives.begin(); it != archives.end(); ++it) { engine.addArchive(*it); } - StringsVector content = variables["content"].as().toStdStringVector(); + StringsVector content = variables["content"].as(); if (content.empty()) { Log(Debug::Error) << "No content file given (esm/esp, nor omwgame/omwaddon). Aborting..."; return false; } + engine.addContentFile("builtin.omwscripts"); + std::set contentDedupe{ "builtin.omwscripts" }; + for (const auto& contentFile : content) + { + if (!contentDedupe.insert(contentFile).second) + { + Log(Debug::Error) << "Content file specified more than once: " << contentFile << ". Aborting..."; + return false; + } + } for (auto& file : content) { engine.addContentFile(file); } - StringsVector groundcover = variables["groundcover"].as().toStdStringVector(); + StringsVector groundcover = variables["groundcover"].as(); for (auto& file : groundcover) { engine.addGroundcoverFile(file); } + if (variables.count("lua-scripts")) + { + Log(Debug::Warning) << "Lua scripts have been specified via the old lua-scripts option and will not be loaded. " + "Please update them to a version which uses the new omwscripts format."; + } + // startup-settings - engine.setCell(variables["start"].as().toStdString()); - engine.setSkipMenu (variables["skip-menu"].as(), variables["new-game"].as()); + engine.setCell(variables["start"].as()); + engine.setSkipMenu(variables["skip-menu"].as(), variables["new-game"].as()); if (!variables["skip-menu"].as() && variables["new-game"].as()) Log(Debug::Warning) << "Warning: new-game used without skip-menu -> ignoring it"; // scripts engine.setCompileAll(variables["script-all"].as()); engine.setCompileAllDialogue(variables["script-all-dialogue"].as()); - engine.setScriptConsoleMode (variables["script-console"].as()); - engine.setStartupScript (variables["script-run"].as().toStdString()); - engine.setWarningsMode (variables["script-warn"].as()); - engine.setScriptBlacklist (variables["script-blacklist"].as().toStdStringVector()); - engine.setScriptBlacklistUse (variables["script-blacklist-use"].as()); - engine.setSaveGameFile (variables["load-savegame"].as().mPath.string()); + engine.setScriptConsoleMode(variables["script-console"].as()); + engine.setStartupScript(variables["script-run"].as()); + engine.setWarningsMode(variables["script-warn"].as()); + std::vector scriptBlacklist; + auto& scriptBlacklistString = variables["script-blacklist"].as(); + for (const auto& blacklistString : scriptBlacklistString) + { + scriptBlacklist.push_back(ESM::RefId::stringRefId(blacklistString)); + } + engine.setScriptBlacklist(scriptBlacklist); + engine.setScriptBlacklistUse(variables["script-blacklist-use"].as()); + engine.setSaveGameFile(variables["load-savegame"].as().u8string()); // other settings Fallback::Map::init(variables["fallback"].as().mMap); engine.setSoundUsage(!variables["no-sound"].as()); - engine.setActivationDistanceOverride (variables["activate-dist"].as()); - engine.enableFontExport(variables["export-fonts"].as()); + engine.setActivationDistanceOverride(variables["activate-dist"].as()); engine.setRandomSeed(variables["random-seed"].as()); return true; @@ -244,21 +182,21 @@ namespace Debug::Level level; switch (severity) { - case osg::ALWAYS: - case osg::FATAL: - level = Debug::Error; - break; - case osg::WARN: - case osg::NOTICE: - level = Debug::Warning; - break; - case osg::INFO: - level = Debug::Info; - break; - case osg::DEBUG_INFO: - case osg::DEBUG_FP: - default: - level = Debug::Debug; + case osg::ALWAYS: + case osg::FATAL: + level = Debug::Error; + break; + case osg::WARN: + case osg::NOTICE: + level = Debug::Warning; + break; + case osg::INFO: + level = Debug::Info; + break; + case osg::DEBUG_INFO: + case osg::DEBUG_FP: + default: + level = Debug::Debug; } std::string_view s(msgCopy); if (s.size() < 1024) @@ -278,21 +216,23 @@ namespace }; } -int runApplication(int argc, char *argv[]) +int runApplication(int argc, char* argv[]) { + Platform::init(); + #ifdef __APPLE__ - boost::filesystem::path binary_path = boost::filesystem::system_complete(boost::filesystem::path(argv[0])); - boost::filesystem::current_path(binary_path.parent_path()); setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif osg::setNotifyHandler(new OSGLogHandler()); Files::ConfigurationManager cfgMgr; - std::unique_ptr engine; - engine.reset(new OMW::Engine(cfgMgr)); + std::unique_ptr engine = std::make_unique(cfgMgr); if (parseOptions(argc, argv, *engine, cfgMgr)) { + if (!Misc::checkRequiredOSGPluginsArePresent()) + return 1; + engine->go(); } @@ -300,12 +240,12 @@ int runApplication(int argc, char *argv[]) } #ifdef ANDROID -extern "C" int SDL_main(int argc, char**argv) +extern "C" int SDL_main(int argc, char** argv) #else -int main(int argc, char**argv) +int main(int argc, char** argv) #endif { - return wrapApplication(&runApplication, argc, argv, "OpenMW"); + return Debug::wrapApplication(&runApplication, argc, argv, "OpenMW"); } // Platform specific for Windows when there is no console built into the executable. diff --git a/apps/openmw/mwbase/dialoguemanager.hpp b/apps/openmw/mwbase/dialoguemanager.hpp index 6103921e02b..e4c60e596db 100644 --- a/apps/openmw/mwbase/dialoguemanager.hpp +++ b/apps/openmw/mwbase/dialoguemanager.hpp @@ -1,11 +1,13 @@ #ifndef GAME_MWBASE_DIALOGUEMANAGER_H #define GAME_MWBASE_DIALOGUEMANAGER_H +#include +#include #include +#include #include -#include -#include +#include namespace Loading { @@ -16,6 +18,7 @@ namespace ESM { class ESMReader; class ESMWriter; + class RefId; } namespace MWWorld @@ -28,93 +31,95 @@ namespace MWBase /// \brief Interface for dialogue manager (implemented in MWDialogue) class DialogueManager { - DialogueManager (const DialogueManager&); - ///< not implemented + DialogueManager(const DialogueManager&); + ///< not implemented - DialogueManager& operator= (const DialogueManager&); - ///< not implemented + DialogueManager& operator=(const DialogueManager&); + ///< not implemented + public: + class ResponseCallback + { public: + virtual ~ResponseCallback() = default; + virtual void addResponse(std::string_view title, std::string_view text) = 0; + }; - class ResponseCallback - { - public: - virtual ~ResponseCallback() = default; - virtual void addResponse(const std::string& title, const std::string& text) = 0; - }; + DialogueManager() {} - DialogueManager() {} + virtual void clear() = 0; - virtual void clear() = 0; + virtual ~DialogueManager() {} - virtual ~DialogueManager() {} + virtual bool isInChoice() const = 0; - virtual bool isInChoice() const = 0; + virtual bool startDialogue(const MWWorld::Ptr& actor, ResponseCallback* callback) = 0; - virtual bool startDialogue (const MWWorld::Ptr& actor, ResponseCallback* callback) = 0; + virtual bool inJournal(const ESM::RefId& topicId, const ESM::RefId& infoId) const = 0; - virtual bool inJournal (const std::string& topicId, const std::string& infoId) = 0; + virtual void addTopic(const ESM::RefId& topic) = 0; - virtual void addTopic (const std::string& topic) = 0; + virtual void addChoice(std::string_view text, int choice) = 0; + virtual const std::vector>& getChoices() const = 0; - virtual void addChoice (const std::string& text,int choice) = 0; - virtual const std::vector >& getChoices() = 0; + virtual bool isGoodbye() const = 0; - virtual bool isGoodbye() = 0; + virtual void goodbye() = 0; - virtual void goodbye() = 0; + virtual bool say(const MWWorld::Ptr& actor, const ESM::RefId& topic) = 0; - virtual void say(const MWWorld::Ptr &actor, const std::string &topic) = 0; + virtual void keywordSelected(std::string_view keyword, ResponseCallback* callback) = 0; + virtual void goodbyeSelected() = 0; + virtual void questionAnswered(int answer, ResponseCallback* callback) = 0; - virtual void keywordSelected (const std::string& keyword, ResponseCallback* callback) = 0; - virtual void goodbyeSelected() = 0; - virtual void questionAnswered (int answer, ResponseCallback* callback) = 0; + enum TopicType + { + Specific = 1, + Exhausted = 2 + }; - enum TopicType - { - Specific = 1, - Exhausted = 2 - }; + enum ServiceType + { + Any = -1, + Barter = 1, + Repair = 2, + Spells = 3, + Training = 4, + Travel = 5, + Spellmaking = 6, + Enchanting = 7 + }; - enum ServiceType - { - Any = -1, - Barter = 1, - Repair = 2, - Spells = 3, - Training = 4, - Travel = 5, - Spellmaking = 6, - Enchanting = 7 - }; + virtual std::list getAvailableTopics() = 0; + virtual int getTopicFlag(const ESM::RefId&) const = 0; - virtual std::list getAvailableTopics() = 0; - virtual int getTopicFlag(const std::string&) = 0; + virtual bool checkServiceRefused(ResponseCallback* callback, ServiceType service = ServiceType::Any) = 0; - virtual bool checkServiceRefused (ResponseCallback* callback, ServiceType service = ServiceType::Any) = 0; + virtual void persuade(int type, ResponseCallback* callback) = 0; - virtual void persuade (int type, ResponseCallback* callback) = 0; - virtual int getTemporaryDispositionChange () const = 0; + /// @note Controlled by an option, gets discarded when dialogue ends by default + virtual void applyBarterDispositionChange(int delta) = 0; - /// @note Controlled by an option, gets discarded when dialogue ends by default - virtual void applyBarterDispositionChange (int delta) = 0; + virtual int countSavedGameRecords() const = 0; - virtual int countSavedGameRecords() const = 0; + virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) const = 0; - virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const = 0; + virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; - virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; + /// Changes faction1's opinion of faction2 by \a diff. + virtual void modFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int diff) = 0; - /// Changes faction1's opinion of faction2 by \a diff. - virtual void modFactionReaction (const std::string& faction1, const std::string& faction2, int diff) = 0; + /// Set faction1's opinion of faction2. + virtual void setFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int absolute) = 0; - virtual void setFactionReaction (const std::string& faction1, const std::string& faction2, int absolute) = 0; + /// @return faction1's opinion of faction2 + virtual int getFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2) const = 0; - /// @return faction1's opinion of faction2 - virtual int getFactionReaction (const std::string& faction1, const std::string& faction2) const = 0; + /// @return all faction's opinion overrides + virtual const std::map* getFactionReactionOverrides(const ESM::RefId& faction) const = 0; - /// Removes the last added topic response for the given actor from the journal - virtual void clearInfoActor (const MWWorld::Ptr& actor) const = 0; + /// Removes the last added topic response for the given actor from the journal + virtual void clearInfoActor(const MWWorld::Ptr& actor) const = 0; }; } diff --git a/apps/openmw/mwbase/environment.cpp b/apps/openmw/mwbase/environment.cpp index b7235edd4b0..f2393f21c44 100644 --- a/apps/openmw/mwbase/environment.cpp +++ b/apps/openmw/mwbase/environment.cpp @@ -4,200 +4,15 @@ #include -#include "world.hpp" -#include "scriptmanager.hpp" -#include "dialoguemanager.hpp" -#include "journal.hpp" -#include "soundmanager.hpp" -#include "mechanicsmanager.hpp" -#include "inputmanager.hpp" -#include "windowmanager.hpp" -#include "statemanager.hpp" - -MWBase::Environment *MWBase::Environment::sThis = nullptr; +MWBase::Environment* MWBase::Environment::sThis = nullptr; MWBase::Environment::Environment() -: mWorld (nullptr), mSoundManager (nullptr), mScriptManager (nullptr), mWindowManager (nullptr), - mMechanicsManager (nullptr), mDialogueManager (nullptr), mJournal (nullptr), mInputManager (nullptr), - mStateManager (nullptr), mResourceSystem (nullptr), mFrameDuration (0), mFrameRateLimit(0.f) { - assert (!sThis); + assert(sThis == nullptr); sThis = this; } MWBase::Environment::~Environment() { - cleanup(); sThis = nullptr; } - -void MWBase::Environment::setWorld (World *world) -{ - mWorld = world; -} - -void MWBase::Environment::setSoundManager (SoundManager *soundManager) -{ - mSoundManager = soundManager; -} - -void MWBase::Environment::setScriptManager (ScriptManager *scriptManager) -{ - mScriptManager = scriptManager; -} - -void MWBase::Environment::setWindowManager (WindowManager *windowManager) -{ - mWindowManager = windowManager; -} - -void MWBase::Environment::setMechanicsManager (MechanicsManager *mechanicsManager) -{ - mMechanicsManager = mechanicsManager; -} - -void MWBase::Environment::setDialogueManager (DialogueManager *dialogueManager) -{ - mDialogueManager = dialogueManager; -} - -void MWBase::Environment::setJournal (Journal *journal) -{ - mJournal = journal; -} - -void MWBase::Environment::setInputManager (InputManager *inputManager) -{ - mInputManager = inputManager; -} - -void MWBase::Environment::setStateManager (StateManager *stateManager) -{ - mStateManager = stateManager; -} - -void MWBase::Environment::setResourceSystem (Resource::ResourceSystem *resourceSystem) -{ - mResourceSystem = resourceSystem; -} - -void MWBase::Environment::setFrameDuration (float duration) -{ - mFrameDuration = duration; -} - -void MWBase::Environment::setFrameRateLimit(float limit) -{ - mFrameRateLimit = limit; -} - -float MWBase::Environment::getFrameRateLimit() const -{ - return mFrameRateLimit; -} - -MWBase::World *MWBase::Environment::getWorld() const -{ - assert (mWorld); - return mWorld; -} - -MWBase::SoundManager *MWBase::Environment::getSoundManager() const -{ - assert (mSoundManager); - return mSoundManager; -} - -MWBase::ScriptManager *MWBase::Environment::getScriptManager() const -{ - assert (mScriptManager); - return mScriptManager; -} - -MWBase::WindowManager *MWBase::Environment::getWindowManager() const -{ - assert (mWindowManager); - return mWindowManager; -} - -MWBase::MechanicsManager *MWBase::Environment::getMechanicsManager() const -{ - assert (mMechanicsManager); - return mMechanicsManager; -} - -MWBase::DialogueManager *MWBase::Environment::getDialogueManager() const -{ - assert (mDialogueManager); - return mDialogueManager; -} - -MWBase::Journal *MWBase::Environment::getJournal() const -{ - assert (mJournal); - return mJournal; -} - -MWBase::InputManager *MWBase::Environment::getInputManager() const -{ - assert (mInputManager); - return mInputManager; -} - -MWBase::StateManager *MWBase::Environment::getStateManager() const -{ - assert (mStateManager); - return mStateManager; -} - -Resource::ResourceSystem *MWBase::Environment::getResourceSystem() const -{ - return mResourceSystem; -} - -float MWBase::Environment::getFrameDuration() const -{ - return mFrameDuration; -} - -void MWBase::Environment::cleanup() -{ - delete mMechanicsManager; - mMechanicsManager = nullptr; - - delete mDialogueManager; - mDialogueManager = nullptr; - - delete mJournal; - mJournal = nullptr; - - delete mScriptManager; - mScriptManager = nullptr; - - delete mWindowManager; - mWindowManager = nullptr; - - delete mWorld; - mWorld = nullptr; - - delete mSoundManager; - mSoundManager = nullptr; - - delete mInputManager; - mInputManager = nullptr; - - delete mStateManager; - mStateManager = nullptr; -} - -const MWBase::Environment& MWBase::Environment::get() -{ - assert (sThis); - return *sThis; -} - -void MWBase::Environment::reportStats(unsigned int frameNumber, osg::Stats& stats) const -{ - mMechanicsManager->reportStats(frameNumber, stats); - mWorld->reportStats(frameNumber, stats); -} diff --git a/apps/openmw/mwbase/environment.hpp b/apps/openmw/mwbase/environment.hpp index 3b57e4e7c15..aa8a41b7c10 100644 --- a/apps/openmw/mwbase/environment.hpp +++ b/apps/openmw/mwbase/environment.hpp @@ -1,16 +1,27 @@ #ifndef GAME_BASE_ENVIRONMENT_H #define GAME_BASE_ENVIRONMENT_H -namespace osg -{ - class Stats; -} +#include + +#include namespace Resource { class ResourceSystem; } +namespace l10n +{ + class Manager; +} + +namespace MWWorld +{ + class ESMStore; + class WorldModel; + class Scene; +} + namespace MWBase { class World; @@ -22,97 +33,111 @@ namespace MWBase class InputManager; class WindowManager; class StateManager; + class LuaManager; /// \brief Central hub for mw-subsystems /// /// This class allows each mw-subsystem to access any others subsystem's top-level manager class. /// - /// \attention Environment takes ownership of the manager class instances it is handed over in - /// the set* functions. class Environment { - static Environment *sThis; + static Environment* sThis; + + World* mWorld = nullptr; + MWWorld::WorldModel* mWorldModel = nullptr; + MWWorld::Scene* mWorldScene = nullptr; + MWWorld::ESMStore* mESMStore = nullptr; + SoundManager* mSoundManager = nullptr; + ScriptManager* mScriptManager = nullptr; + WindowManager* mWindowManager = nullptr; + MechanicsManager* mMechanicsManager = nullptr; + DialogueManager* mDialogueManager = nullptr; + Journal* mJournal = nullptr; + InputManager* mInputManager = nullptr; + StateManager* mStateManager = nullptr; + LuaManager* mLuaManager = nullptr; + Resource::ResourceSystem* mResourceSystem = nullptr; + l10n::Manager* mL10nManager = nullptr; + float mFrameRateLimit = 0; + float mFrameDuration = 0; + + public: + Environment(); - World *mWorld; - SoundManager *mSoundManager; - ScriptManager *mScriptManager; - WindowManager *mWindowManager; - MechanicsManager *mMechanicsManager; - DialogueManager *mDialogueManager; - Journal *mJournal; - InputManager *mInputManager; - StateManager *mStateManager; - Resource::ResourceSystem *mResourceSystem; - float mFrameDuration; - float mFrameRateLimit; + ~Environment(); - Environment (const Environment&); - ///< not implemented + Environment(const Environment&) = delete; - Environment& operator= (const Environment&); - ///< not implemented + Environment& operator=(const Environment&) = delete; - public: + void setWorld(World& value) { mWorld = &value; } + void setWorldModel(MWWorld::WorldModel& value) { mWorldModel = &value; } + void setWorldScene(MWWorld::Scene& value) { mWorldScene = &value; } + void setESMStore(MWWorld::ESMStore& value) { mESMStore = &value; } - Environment(); + void setSoundManager(SoundManager& value) { mSoundManager = &value; } - ~Environment(); + void setScriptManager(ScriptManager& value) { mScriptManager = &value; } - void setWorld (World *world); + void setWindowManager(WindowManager& value) { mWindowManager = &value; } - void setSoundManager (SoundManager *soundManager); + void setMechanicsManager(MechanicsManager& value) { mMechanicsManager = &value; } - void setScriptManager (MWBase::ScriptManager *scriptManager); + void setDialogueManager(DialogueManager& value) { mDialogueManager = &value; } - void setWindowManager (WindowManager *windowManager); + void setJournal(Journal& value) { mJournal = &value; } - void setMechanicsManager (MechanicsManager *mechanicsManager); + void setInputManager(InputManager& value) { mInputManager = &value; } - void setDialogueManager (DialogueManager *dialogueManager); + void setStateManager(StateManager& value) { mStateManager = &value; } - void setJournal (Journal *journal); + void setLuaManager(LuaManager& value) { mLuaManager = &value; } - void setInputManager (InputManager *inputManager); + void setResourceSystem(Resource::ResourceSystem& value) { mResourceSystem = &value; } - void setStateManager (StateManager *stateManager); + void setL10nManager(l10n::Manager& value) { mL10nManager = &value; } - void setResourceSystem (Resource::ResourceSystem *resourceSystem); + Misc::NotNullPtr getWorld() const { return mWorld; } + Misc::NotNullPtr getWorldModel() const { return mWorldModel; } + Misc::NotNullPtr getWorldScene() const { return mWorldScene; } + Misc::NotNullPtr getESMStore() const { return mESMStore; } - void setFrameDuration (float duration); - ///< Set length of current frame in seconds. + Misc::NotNullPtr getSoundManager() const { return mSoundManager; } - void setFrameRateLimit(float frameRateLimit); - float getFrameRateLimit() const; + Misc::NotNullPtr getScriptManager() const { return mScriptManager; } - World *getWorld() const; + Misc::NotNullPtr getWindowManager() const { return mWindowManager; } - SoundManager *getSoundManager() const; + Misc::NotNullPtr getMechanicsManager() const { return mMechanicsManager; } - ScriptManager *getScriptManager() const; + Misc::NotNullPtr getDialogueManager() const { return mDialogueManager; } - WindowManager *getWindowManager() const; + Misc::NotNullPtr getJournal() const { return mJournal; } - MechanicsManager *getMechanicsManager() const; + Misc::NotNullPtr getInputManager() const { return mInputManager; } - DialogueManager *getDialogueManager() const; + Misc::NotNullPtr getStateManager() const { return mStateManager; } - Journal *getJournal() const; + Misc::NotNullPtr getLuaManager() const { return mLuaManager; } - InputManager *getInputManager() const; + Misc::NotNullPtr getResourceSystem() const { return mResourceSystem; } - StateManager *getStateManager() const; + Misc::NotNullPtr getL10nManager() const { return mL10nManager; } - Resource::ResourceSystem *getResourceSystem() const; + float getFrameRateLimit() const { return mFrameRateLimit; } - float getFrameDuration() const; + void setFrameRateLimit(float value) { mFrameRateLimit = value; } - void cleanup(); - ///< Delete all mw*-subsystems. + float getFrameDuration() const { return mFrameDuration; } - static const Environment& get(); - ///< Return instance of this class. + void setFrameDuration(float value) { mFrameDuration = value; } - void reportStats(unsigned int frameNumber, osg::Stats& stats) const; + /// Return instance of this class. + static const Environment& get() + { + assert(sThis != nullptr); + return *sThis; + } }; } diff --git a/apps/openmw/mwbase/inputmanager.hpp b/apps/openmw/mwbase/inputmanager.hpp index 951b5053a2c..5ee20476b3e 100644 --- a/apps/openmw/mwbase/inputmanager.hpp +++ b/apps/openmw/mwbase/inputmanager.hpp @@ -1,11 +1,12 @@ #ifndef GAME_MWBASE_INPUTMANAGER_H #define GAME_MWBASE_INPUTMANAGER_H -#include #include +#include #include -#include +#include +#include namespace Loading { @@ -23,61 +24,70 @@ namespace MWBase /// \brief Interface for input manager (implemented in MWInput) class InputManager { - InputManager (const InputManager&); - ///< not implemented + InputManager(const InputManager&); + ///< not implemented + + InputManager& operator=(const InputManager&); + ///< not implemented - InputManager& operator= (const InputManager&); - ///< not implemented + public: + InputManager() {} - public: + /// Clear all savegame-specific data + virtual void clear() = 0; - InputManager() {} + virtual ~InputManager() {} - /// Clear all savegame-specific data - virtual void clear() = 0; + virtual void update(float dt, bool disableControls, bool disableEvents = false) = 0; - virtual ~InputManager() {} + virtual void changeInputMode(bool guiMode) = 0; - virtual void update(float dt, bool disableControls, bool disableEvents=false) = 0; + virtual void processChangedSettings(const std::set>& changed) = 0; - virtual void changeInputMode(bool guiMode) = 0; + virtual void setDragDrop(bool dragDrop) = 0; + virtual bool isGamepadGuiCursorEnabled() = 0; + virtual void setGamepadGuiCursorEnabled(bool enabled) = 0; - virtual void processChangedSettings(const std::set< std::pair >& changed) = 0; + virtual void toggleControlSwitch(std::string_view sw, bool value) = 0; + virtual bool getControlSwitch(std::string_view sw) = 0; - virtual void setDragDrop(bool dragDrop) = 0; - virtual void setGamepadGuiCursorEnabled(bool enabled) = 0; - virtual void setAttemptJump(bool jumping) = 0; + virtual std::string_view getActionDescription(int action) const = 0; + virtual std::string getActionKeyBindingName(int action) const = 0; + virtual std::string getActionControllerBindingName(int action) const = 0; + virtual bool actionIsActive(int action) const = 0; - virtual void toggleControlSwitch (const std::string& sw, bool value) = 0; - virtual bool getControlSwitch (const std::string& sw) = 0; + virtual float getActionValue(int action) const = 0; // returns value in range [0, 1] + virtual bool isControllerButtonPressed(SDL_GameControllerButton button) const = 0; + virtual float getControllerAxisValue(SDL_GameControllerAxis axis) const = 0; // returns value in range [-1, 1] + virtual int getMouseMoveX() const = 0; + virtual int getMouseMoveY() const = 0; - virtual std::string getActionDescription (int action) = 0; - virtual std::string getActionKeyBindingName (int action) = 0; - virtual std::string getActionControllerBindingName (int action) = 0; - ///Actions available for binding to keyboard buttons - virtual std::vector getActionKeySorting() = 0; - ///Actions available for binding to controller buttons - virtual std::vector getActionControllerSorting() = 0; - virtual int getNumActions() = 0; - ///If keyboard is true, only pay attention to keyboard events. If false, only pay attention to controller events (excluding esc) - virtual void enableDetectingBindingMode (int action, bool keyboard) = 0; - virtual void resetToDefaultKeyBindings() = 0; - virtual void resetToDefaultControllerBindings() = 0; + /// Actions available for binding to keyboard buttons + virtual const std::initializer_list& getActionKeySorting() = 0; + /// Actions available for binding to controller buttons + virtual const std::initializer_list& getActionControllerSorting() = 0; + virtual int getNumActions() = 0; + /// If keyboard is true, only pay attention to keyboard events. If false, only pay attention to controller + /// events (excluding esc) + virtual void enableDetectingBindingMode(int action, bool keyboard) = 0; + virtual void resetToDefaultKeyBindings() = 0; + virtual void resetToDefaultControllerBindings() = 0; - /// Returns if the last used input device was a joystick or a keyboard - /// @return true if joystick, false otherwise - virtual bool joystickLastUsed() = 0; - virtual void setJoystickLastUsed(bool enabled) = 0; + /// Returns if the last used input device was a joystick or a keyboard + /// @return true if joystick, false otherwise + virtual bool joystickLastUsed() = 0; + virtual void setJoystickLastUsed(bool enabled) = 0; - virtual int countSavedGameRecords() const = 0; - virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) = 0; - virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; + virtual int countSavedGameRecords() const = 0; + virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) = 0; + virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; - virtual void resetIdleTime() = 0; + virtual void resetIdleTime() = 0; + virtual bool isIdle() const = 0; - virtual void executeAction(int action) = 0; + virtual void executeAction(int action) = 0; - virtual bool controlsDisabled() = 0; + virtual bool controlsDisabled() = 0; }; } diff --git a/apps/openmw/mwbase/journal.hpp b/apps/openmw/mwbase/journal.hpp index cd87928903e..662689376bd 100644 --- a/apps/openmw/mwbase/journal.hpp +++ b/apps/openmw/mwbase/journal.hpp @@ -1,15 +1,16 @@ #ifndef GAME_MWBASE_JOURNAL_H #define GAME_MWBASE_JOURNAL_H -#include #include #include +#include +#include -#include +#include #include "../mwdialogue/journalentry.hpp" -#include "../mwdialogue/topic.hpp" #include "../mwdialogue/quest.hpp" +#include "../mwdialogue/topic.hpp" namespace Loading { @@ -27,73 +28,76 @@ namespace MWBase /// \brief Interface for the player's journal (implemented in MWDialogue) class Journal { - Journal (const Journal&); - ///< not implemented - - Journal& operator= (const Journal&); - ///< not implemented + Journal(const Journal&); + ///< not implemented - public: + Journal& operator=(const Journal&); + ///< not implemented - typedef std::deque TEntryContainer; - typedef TEntryContainer::const_iterator TEntryIter; - typedef std::map TQuestContainer; // topic, quest - typedef TQuestContainer::const_iterator TQuestIter; - typedef std::map TTopicContainer; // topic-id, topic-content - typedef TTopicContainer::const_iterator TTopicIter; + public: + typedef std::deque TEntryContainer; + typedef TEntryContainer::const_iterator TEntryIter; + typedef std::map TQuestContainer; // topic, quest + typedef TQuestContainer::const_iterator TQuestIter; + typedef std::map TTopicContainer; // topic-id, topic-content + typedef TTopicContainer::const_iterator TTopicIter; - public: + public: + Journal() {} - Journal() {} + virtual void clear() = 0; - virtual void clear() = 0; + virtual ~Journal() {} - virtual ~Journal() {} + virtual MWDialogue::Quest& getOrStartQuest(const ESM::RefId& id) = 0; + ///< Gets the quest requested. Creates it and inserts it in quests if it is not yet started. + virtual MWDialogue::Quest* getQuestOrNull(const ESM::RefId& id) = 0; + ///< Gets a pointer to the requested quest. Will return nullptr if the quest has not been started. - virtual void addEntry (const std::string& id, int index, const MWWorld::Ptr& actor) = 0; - ///< Add a journal entry. - /// @param actor Used as context for replacing of escape sequences (%name, etc). + virtual void addEntry(const ESM::RefId& id, int index, const MWWorld::Ptr& actor) = 0; + ///< Add a journal entry. + /// @param actor Used as context for replacing of escape sequences (%name, etc). - virtual void setJournalIndex (const std::string& id, int index) = 0; - ///< Set the journal index without adding an entry. + virtual void setJournalIndex(const ESM::RefId& id, int index) = 0; + ///< Set the journal index without adding an entry. - virtual int getJournalIndex (const std::string& id) const = 0; - ///< Get the journal index. + virtual int getJournalIndex(const ESM::RefId& id) const = 0; + ///< Get the journal index. - virtual void addTopic (const std::string& topicId, const std::string& infoId, const MWWorld::Ptr& actor) = 0; - /// \note topicId must be lowercase + virtual void addTopic(const ESM::RefId& topicId, const ESM::RefId& infoId, const MWWorld::Ptr& actor) = 0; + /// \note topicId must be lowercase - virtual void removeLastAddedTopicResponse (const std::string& topicId, const std::string& actorName) = 0; - ///< Removes the last topic response added for the given topicId and actor name. - /// \note topicId must be lowercase + virtual void removeLastAddedTopicResponse(const ESM::RefId& topicId, std::string_view actorName) = 0; + ///< Removes the last topic response added for the given topicId and actor name. + /// \note topicId must be lowercase - virtual TEntryIter begin() const = 0; - ///< Iterator pointing to the begin of the main journal. - /// - /// \note Iterators to main journal entries will never become invalid. + virtual TEntryIter begin() const = 0; + ///< Iterator pointing to the begin of the main journal. + /// + /// \note Iterators to main journal entries will never become invalid. - virtual TEntryIter end() const = 0; - ///< Iterator pointing past the end of the main journal. + virtual TEntryIter end() const = 0; + ///< Iterator pointing past the end of the main journal. - virtual TQuestIter questBegin() const = 0; - ///< Iterator pointing to the first quest (sorted by topic ID) + virtual TQuestIter questBegin() const = 0; + ///< Iterator pointing to the first quest (sorted by topic ID) - virtual TQuestIter questEnd() const = 0; - ///< Iterator pointing past the last quest. + virtual TQuestIter questEnd() const = 0; + ///< Iterator pointing past the last quest. - virtual TTopicIter topicBegin() const = 0; - ///< Iterator pointing to the first topic (sorted by topic ID) - /// - /// \note The topic ID is identical with the user-visible topic string. + virtual TTopicIter topicBegin() const = 0; + ///< Iterator pointing to the first topic (sorted by topic ID) + /// + /// \note The topic ID is identical with the user-visible topic string. - virtual TTopicIter topicEnd() const = 0; - ///< Iterator pointing past the last topic. + virtual TTopicIter topicEnd() const = 0; + ///< Iterator pointing past the last topic. - virtual int countSavedGameRecords() const = 0; + virtual int countSavedGameRecords() const = 0; - virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const = 0; + virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) const = 0; - virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; + virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; }; } diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp new file mode 100644 index 00000000000..a5d6fe11142 --- /dev/null +++ b/apps/openmw/mwbase/luamanager.hpp @@ -0,0 +1,157 @@ +#ifndef GAME_MWBASE_LUAMANAGER_H +#define GAME_MWBASE_LUAMANAGER_H + +#include +#include +#include + +#include + +#include "../mwgui/mode.hpp" +#include "../mwrender/animationpriority.hpp" +#include + +namespace MWWorld +{ + class CellStore; + class Ptr; +} + +namespace Loading +{ + class Listener; +} + +namespace ESM +{ + class ESMReader; + class ESMWriter; + class RefId; + struct LuaScripts; +} + +namespace LuaUtil +{ + namespace InputAction + { + class Registry; + } +} + +namespace MWBase +{ + // \brief LuaManager is the central interface through which the engine invokes lua scripts. + // + // The native side invokes functions on this interface, which queues events to be handled by the + // scripts in the lua thread. Synchronous calls are not possible. + // + // The main implementation is in apps/openmw/mwlua/luamanagerimp.cpp. + // Lua logic in general lives under apps/openmw/mwlua and this interface is + // the main way for the rest of the engine to interact with the logic there. + class LuaManager + { + public: + virtual ~LuaManager() = default; + + virtual void newGameStarted() = 0; + virtual void gameLoaded() = 0; + virtual void gameEnded() = 0; + virtual void noGame() = 0; + virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0; + virtual void objectRemovedFromScene(const MWWorld::Ptr& ptr) = 0; + virtual void objectTeleported(const MWWorld::Ptr& ptr) = 0; + virtual void itemConsumed(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) = 0; + virtual void objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; + virtual void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) = 0; + virtual void animationTextKey(const MWWorld::Ptr& actor, const std::string& key) = 0; + virtual void playAnimation(const MWWorld::Ptr& object, 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) + = 0; + virtual void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) = 0; + virtual void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) = 0; + virtual void exteriorCreated(MWWorld::CellStore& cell) = 0; + virtual void actorDied(const MWWorld::Ptr& actor) = 0; + virtual void questUpdated(const ESM::RefId& questId, int stage) = 0; + // `arg` is either forwarded from MWGui::pushGuiMode or empty + virtual void uiModeChanged(const MWWorld::Ptr& arg) = 0; + + // TODO: notify LuaManager about other events + // virtual void objectOnHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, + // const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful, + // DamageSourceType sourceType) = 0; + + struct InputEvent + { + struct WheelChange + { + int x; + int y; + }; + + enum + { + KeyPressed, + KeyReleased, + ControllerPressed, + ControllerReleased, + Action, + TouchPressed, + TouchReleased, + TouchMoved, + MouseButtonPressed, + MouseButtonReleased, + MouseWheel, + } mType; + std::variant mValue; + }; + virtual void inputEvent(const InputEvent& event) = 0; + + struct ActorControls + { + bool mDisableAI = false; + bool mChanged = false; + + bool mJump = false; + bool mRun = false; + bool mSneak = false; + float mMovement = 0; + float mSideMovement = 0; + float mPitchChange = 0; + float mYawChange = 0; + int mUse = 0; + }; + + virtual ActorControls* getActorControls(const MWWorld::Ptr&) const = 0; + + virtual void clear() = 0; + virtual void setupPlayer(const MWWorld::Ptr&) = 0; + + // Saving + int countSavedGameRecords() const { return 1; } + virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) = 0; + virtual void saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) = 0; + + // Must be called before save, otherwise the world can be saved in an inconsistent state. + virtual void applyDelayedActions() = 0; + + // Loading from a save + virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; + virtual void loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) = 0; + + // Should be called before loading. The map is used to fix refnums if the order of content files was changed. + virtual void setContentFileMapping(const std::map&) = 0; + + // Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script. + virtual void reloadAllScripts() = 0; + + virtual void handleConsoleCommand( + const std::string& consoleMode, const std::string& command, const MWWorld::Ptr& selectedPtr) + = 0; + + virtual std::string formatResourceUsageStats() const = 0; + }; + +} + +#endif // GAME_MWBASE_LUAMANAGER_H diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index 967504552d0..4883fa20003 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -1,14 +1,15 @@ #ifndef GAME_MWBASE_MECHANICSMANAGER_H #define GAME_MWBASE_MECHANICSMANAGER_H +#include +#include +#include #include +#include #include -#include -#include -#include -#include "../mwmechanics/actorutil.hpp" -// For MWMechanics::GreetingState +#include "../mwmechanics/greetingstate.hpp" +#include "../mwrender/animationpriority.hpp" #include "../mwworld/ptr.hpp" @@ -21,7 +22,7 @@ namespace osg namespace ESM { struct Class; - + class RefId; class ESMReader; class ESMWriter; } @@ -43,242 +44,269 @@ namespace MWBase /// \brief Interface for game mechanics manager (implemented in MWMechanics) class MechanicsManager { - MechanicsManager (const MechanicsManager&); - ///< not implemented - - MechanicsManager& operator= (const MechanicsManager&); - ///< not implemented - - public: - - MechanicsManager() {} - - virtual ~MechanicsManager() {} - - virtual void add (const MWWorld::Ptr& ptr) = 0; - ///< Register an object for management - - virtual void remove (const MWWorld::Ptr& ptr) = 0; - ///< Deregister an object for management - - virtual void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) = 0; - ///< Moves an object to a new cell - - virtual void drop (const MWWorld::CellStore *cellStore) = 0; - ///< Deregister all objects in the given cell. - - virtual void update (float duration, bool paused) = 0; - ///< Update objects - /// - /// \param paused In game type does not currently advance (this usually means some GUI - /// component is up). - - virtual void setPlayerName (const std::string& name) = 0; - ///< Set player name. - - virtual void setPlayerRace (const std::string& id, bool male, const std::string &head, const std::string &hair) = 0; - ///< Set player race. - - virtual void setPlayerBirthsign (const std::string& id) = 0; - ///< Set player birthsign. - - virtual void setPlayerClass (const std::string& id) = 0; - ///< Set player class to stock class. - - virtual void setPlayerClass (const ESM::Class& class_) = 0; - ///< Set player class to custom class. - - virtual void restoreDynamicStats(MWWorld::Ptr actor, double hours, bool sleep) = 0; - - virtual void rest(double hours, bool sleep) = 0; - ///< If the player is sleeping or waiting, this should be called every hour. - /// @param sleep is the player sleeping or waiting? - - virtual int getHoursToRest() const = 0; - ///< Calculate how many hours the player needs to rest in order to be fully healed - - virtual int getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) = 0; - ///< This is used by every service to determine the price of objects given the trading skills of the player and NPC. - - virtual int getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange = true) = 0; - ///< Calculate the diposition of an NPC toward the player. - - virtual int countDeaths (const std::string& id) const = 0; - ///< Return the number of deaths for actors with the given ID. - - /// Check if \a observer is potentially aware of \a ptr. Does not do a line of sight check! - virtual bool awarenessCheck (const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) = 0; - - /// Makes \a ptr fight \a target. Also shouts a combat taunt. - virtual void startCombat (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0; - - enum OffenseType - { - OT_Theft, // Taking items owned by an NPC or a faction you are not a member of - OT_Assault, // Attacking a peaceful NPC - OT_Murder, // Murdering a peaceful NPC - OT_Trespassing, // Picking the lock of an owned door/chest - OT_SleepingInOwnedBed, // Sleeping in a bed owned by an NPC or a faction you are not a member of - OT_Pickpocket // Entering pickpocket mode, leaving it, and being detected. Any items stolen are a separate crime (Theft) - }; - /** - * @note victim may be empty - * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. - * @param victimAware Is the victim already aware of the crime? - * If this parameter is false, it will be determined by a line-of-sight and awareness check. - * @return was the crime seen? - */ - virtual bool commitCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, OffenseType type, - const std::string& factionId="", int arg=0, bool victimAware=false) = 0; - /// @return false if the attack was considered a "friendly hit" and forgiven - virtual bool actorAttacked (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) = 0; - - /// Notify that actor was killed, add a murder bounty if applicable - /// @note No-op for non-player attackers - virtual void actorKilled (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) = 0; - - /// Utility to check if taking this item is illegal and calling commitCrime if so - /// @param container The container the item is in; may be empty for an item in the world - virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, - int count, bool alarm = true) = 0; - /// Utility to check if unlocking this object is illegal and calling commitCrime if so - virtual void unlockAttempted (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item) = 0; - /// Attempt sleeping in a bed. If this is illegal, call commitCrime. - /// @return was it illegal, and someone saw you doing it? - virtual bool sleepInBed (const MWWorld::Ptr& ptr, const MWWorld::Ptr& bed) = 0; - - enum PersuasionType - { - PT_Admire, - PT_Intimidate, - PT_Taunt, - PT_Bribe10, - PT_Bribe100, - PT_Bribe1000 - }; - virtual void getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) = 0; - ///< Perform a persuasion action on NPC - - virtual void forceStateUpdate(const MWWorld::Ptr &ptr) = 0; - ///< Forces an object to refresh its animation state. - - virtual bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number=1, bool persist=false) = 0; - ///< Run animation for a MW-reference. Calls to this function for references that are currently not - /// in the scene should be ignored. - /// - /// \param mode 0 normal, 1 immediate start, 2 immediate loop - /// \param count How many times the animation should be run - /// \param persist Whether the animation state should be stored in saved games - /// and persist after cell unload. - /// \return Success or error - - virtual void skipAnimation(const MWWorld::Ptr& ptr) = 0; - ///< Skip the animation for the given MW-reference for one frame. Calls to this function for - /// references that are currently not in the scene should be ignored. - - virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) = 0; - - /// Save the current animation state of managed references to their RefData. - virtual void persistAnimationStates() = 0; - - /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently - /// paused we may want to do it manually (after equipping permanent enchantment) - virtual void updateMagicEffects (const MWWorld::Ptr& ptr) = 0; - - virtual bool toggleAI() = 0; - virtual bool isAIActive() = 0; - - virtual void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector& objects) = 0; - virtual void getActorsInRange(const osg::Vec3f &position, float radius, std::vector &objects) = 0; - - /// Check if there are actors in selected range - virtual bool isAnyActorInRange(const osg::Vec3f &position, float radius) = 0; - - ///Returns the list of actors which are siding with the given actor in fights - /**ie AiFollow or AiEscort is active and the target is the actor **/ - virtual std::list getActorsSidingWith(const MWWorld::Ptr& actor) = 0; - virtual std::list getActorsFollowing(const MWWorld::Ptr& actor) = 0; - virtual std::list getActorsFollowingIndices(const MWWorld::Ptr& actor) = 0; - virtual std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) = 0; - - ///Returns a list of actors who are fighting the given actor within the fAlarmDistance - /** ie AiCombat is active and the target is the actor **/ - virtual std::list getActorsFighting(const MWWorld::Ptr& actor) = 0; - - virtual std::list getEnemiesNearby(const MWWorld::Ptr& actor) = 0; - - /// Recursive versions of above methods - virtual void getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) = 0; - virtual void getActorsSidingWith(const MWWorld::Ptr& actor, std::set& out) = 0; - - virtual void playerLoaded() = 0; - - virtual int countSavedGameRecords() const = 0; - - virtual void write (ESM::ESMWriter& writer, Loading::Listener& listener) const = 0; - - virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; + MechanicsManager(const MechanicsManager&); + ///< not implemented + + MechanicsManager& operator=(const MechanicsManager&); + ///< not implemented + + public: + MechanicsManager() {} + + virtual ~MechanicsManager() {} + + virtual void add(const MWWorld::Ptr& ptr) = 0; + ///< Register an object for management + + virtual void remove(const MWWorld::Ptr& ptr, bool keepActive) = 0; + ///< Deregister an object for management + + virtual void updateCell(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) = 0; + ///< Moves an object to a new cell + + virtual void drop(const MWWorld::CellStore* cellStore) = 0; + ///< Deregister all objects in the given cell. + + virtual void setPlayerName(const std::string& name) = 0; + ///< Set player name. + + virtual void setPlayerRace(const ESM::RefId& id, bool male, const ESM::RefId& head, const ESM::RefId& hair) = 0; + ///< Set player race. + + virtual void setPlayerBirthsign(const ESM::RefId& id) = 0; + ///< Set player birthsign. + + virtual void setPlayerClass(const ESM::RefId& id) = 0; + ///< Set player class to stock class. + + virtual void setPlayerClass(const ESM::Class& class_) = 0; + ///< Set player class to custom class. + + virtual void restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep) = 0; + + virtual void rest(double hours, bool sleep) = 0; + ///< If the player is sleeping or waiting, this should be called every hour. + /// @param sleep is the player sleeping or waiting? + + virtual int getHoursToRest() const = 0; + ///< Calculate how many hours the player needs to rest in order to be fully healed + + virtual int getBarterOffer(const MWWorld::Ptr& ptr, int basePrice, bool buying) = 0; + ///< This is used by every service to determine the price of objects given the trading skills of the player and + ///< NPC. + + virtual int getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp = true) = 0; + ///< Calculate the diposition of an NPC toward the player. + + virtual int countDeaths(const ESM::RefId& id) const = 0; + ///< Return the number of deaths for actors with the given ID. + + /// Check if \a observer is potentially aware of \a ptr. Does not do a line of sight check! + virtual bool awarenessCheck(const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) = 0; + + /// Makes \a ptr fight \a target. Also shouts a combat taunt. + virtual void startCombat( + const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set* targetAllies) + = 0; + + /// Removes an actor and its allies from combat with the actor's targets. + virtual void stopCombat(const MWWorld::Ptr& ptr) = 0; + + enum OffenseType + { + OT_Theft, // Taking items owned by an NPC or a faction you are not a member of + OT_Assault, // Attacking a peaceful NPC + OT_Murder, // Murdering a peaceful NPC + OT_Trespassing, // Picking the lock of an owned door/chest + OT_SleepingInOwnedBed, // Sleeping in a bed owned by an NPC or a faction you are not a member of + OT_Pickpocket // Entering pickpocket mode, leaving it, and being detected. Any items stolen are a separate + // crime (Theft) + }; + /** + * @note victim may be empty + * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. + * @param victimAware Is the victim already aware of the crime? + * If this parameter is false, it will be determined by a line-of-sight and awareness check. + * @return was the crime seen? + */ + virtual bool commitCrime(const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, OffenseType type, + const ESM::RefId& factionId = ESM::RefId(), int arg = 0, bool victimAware = false) + = 0; + /// @return false if the attack was considered a "friendly hit" and forgiven + virtual bool actorAttacked(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) = 0; + + /// Notify that actor was killed, add a murder bounty if applicable + /// @note No-op for non-player attackers + virtual void actorKilled(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) = 0; + + /// Utility to check if taking this item is illegal and calling commitCrime if so + /// @param container The container the item is in; may be empty for an item in the world + virtual void itemTaken(const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, + int count, bool alarm = true) + = 0; + /// Utility to check if unlocking this object is illegal and calling commitCrime if so + virtual void unlockAttempted(const MWWorld::Ptr& ptr, const MWWorld::Ptr& item) = 0; + /// Attempt sleeping in a bed. If this is illegal, call commitCrime. + /// @return was it illegal, and someone saw you doing it? + virtual bool sleepInBed(const MWWorld::Ptr& ptr, const MWWorld::Ptr& bed) = 0; + + enum PersuasionType + { + PT_Admire, + PT_Intimidate, + PT_Taunt, + PT_Bribe10, + PT_Bribe100, + PT_Bribe1000 + }; + virtual void getPersuasionDispositionChange( + const MWWorld::Ptr& npc, PersuasionType type, bool& success, int& tempChange, int& permChange) + = 0; + ///< Perform a persuasion action on NPC + + virtual void forceStateUpdate(const MWWorld::Ptr& ptr) = 0; + ///< Forces an object to refresh its animation state. + + virtual bool playAnimationGroup( + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number = 1, bool scripted = false) + = 0; + ///< Run animation for a MW-reference. Calls to this function for references that are currently not + /// in the scene should be ignored. + /// + /// \param mode 0 normal, 1 immediate start, 2 immediate loop + /// \param number How many times the animation should be run + /// \param scripted Whether the animation should be treated as a scripted animation. + /// \return Success or error + virtual bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, + float speed, std::string_view startKey, std::string_view stopKey, bool forceLoop) + = 0; + ///< Lua variant of playAnimationGroup. The mode parameter is omitted + /// and forced to 0. modes 1 and 2 can be emulated by doing clearAnimationQueue() and + /// setting the startKey. + /// + /// \param number How many times the animation should be run + /// \param speed How fast to play the animation, where 1.f = normal speed + /// \param startKey Which textkey to start the animation from + /// \param stopKey Which textkey to stop the animation on + /// \param forceLoop Force the animation to be looping, even if it's normally not looping. + /// \param blendMask See MWRender::Animation::BlendMask + /// \param scripted Whether the animation should be treated as as scripted animation + /// \return Success or error + /// + + virtual void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) = 0; + + virtual void skipAnimation(const MWWorld::Ptr& ptr) = 0; + ///< Skip the animation for the given MW-reference for one frame. Calls to this function for + /// references that are currently not in the scene should be ignored. + + virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) = 0; + + virtual bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const = 0; + + /// Save the current animation state of managed references to their RefData. + virtual void persistAnimationStates() = 0; + + /// Clear out the animation queue, and cancel any animation currently playing from the queue + virtual void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) = 0; + + /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently + /// paused we may want to do it manually (after equipping permanent enchantment) + virtual void updateMagicEffects(const MWWorld::Ptr& ptr) = 0; + + virtual bool toggleAI() = 0; + virtual bool isAIActive() = 0; + + virtual void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& objects) + = 0; + virtual void getActorsInRange(const osg::Vec3f& position, float radius, std::vector& objects) = 0; + + /// Check if there are actors in selected range + virtual bool isAnyActorInRange(const osg::Vec3f& position, float radius) = 0; + + /// Returns the list of actors which are siding with the given actor in fights + /**ie AiFollow or AiEscort is active and the target is the actor **/ + virtual std::vector getActorsSidingWith(const MWWorld::Ptr& actor) = 0; + virtual std::vector getActorsFollowing(const MWWorld::Ptr& actor) = 0; + virtual std::vector getActorsFollowingIndices(const MWWorld::Ptr& actor) = 0; + virtual std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) = 0; + + /// Returns a list of actors who are fighting the given actor within the fAlarmDistance + /** ie AiCombat is active and the target is the actor **/ + virtual std::vector getActorsFighting(const MWWorld::Ptr& actor) = 0; + + virtual std::vector getEnemiesNearby(const MWWorld::Ptr& actor) = 0; + + /// Recursive versions of above methods + virtual void getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) = 0; + virtual void getActorsSidingWith(const MWWorld::Ptr& actor, std::set& out) = 0; + + virtual void playerLoaded() = 0; + + virtual int countSavedGameRecords() const = 0; - virtual void clear() = 0; + virtual void write(ESM::ESMWriter& writer, Loading::Listener& listener) const = 0; - virtual bool isAggressive (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0; + virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; - /// Resurrects the player if necessary - virtual void resurrect(const MWWorld::Ptr& ptr) = 0; + virtual void clear() = 0; - virtual bool isCastingSpell (const MWWorld::Ptr& ptr) const = 0; - virtual bool isReadyToBlock (const MWWorld::Ptr& ptr) const = 0; - virtual bool isAttackingOrSpell(const MWWorld::Ptr &ptr) const = 0; + virtual bool isAggressive(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0; - virtual void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) = 0; + /// Resurrects the player if necessary + virtual void resurrect(const MWWorld::Ptr& ptr) = 0; - virtual void processChangedSettings (const std::set< std::pair >& settings) = 0; + virtual bool isCastingSpell(const MWWorld::Ptr& ptr) const = 0; + virtual bool isReadyToBlock(const MWWorld::Ptr& ptr) const = 0; + virtual bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const = 0; - virtual float getActorsProcessingRange() const = 0; + virtual void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) = 0; - virtual void notifyDied(const MWWorld::Ptr& actor) = 0; + virtual void processChangedSettings(const std::set>& settings) = 0; - virtual bool onOpen(const MWWorld::Ptr& ptr) = 0; - virtual void onClose(const MWWorld::Ptr& ptr) = 0; + virtual void notifyDied(const MWWorld::Ptr& actor) = 0; - /// Check if the target actor was detected by an observer - /// If the observer is a non-NPC, check all actors in AI processing distance as observers - virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) = 0; + virtual bool onOpen(const MWWorld::Ptr& ptr) = 0; + virtual void onClose(const MWWorld::Ptr& ptr) = 0; - virtual void confiscateStolenItems (const MWWorld::Ptr& player, const MWWorld::Ptr& targetContainer) = 0; + /// Check if the target actor was detected by an observer + /// If the observer is a non-NPC, check all actors in AI processing distance as observers + virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) = 0; - /// List the owners that the player has stolen this item from (the owner can be an NPC or a faction). - /// - virtual std::vector > getStolenItemOwners(const std::string& itemid) = 0; + virtual void confiscateStolenItems(const MWWorld::Ptr& player, const MWWorld::Ptr& targetContainer) = 0; - /// Has the player stolen this item from the given owner? - virtual bool isItemStolenFrom(const std::string& itemid, const MWWorld::Ptr& ptr) = 0; + /// List the owners that the player has stolen this item from (the owner can be an NPC or a faction). + /// + virtual std::vector> getStolenItemOwners(const ESM::RefId& itemid) = 0; - virtual bool isBoundItem(const MWWorld::Ptr& item) = 0; - virtual bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) = 0; + /// Has the player stolen this item from the given owner? + virtual bool isItemStolenFrom(const ESM::RefId& itemid, const MWWorld::Ptr& ptr) = 0; - /// Turn actor into werewolf or normal form. - virtual void setWerewolf(const MWWorld::Ptr& actor, bool werewolf) = 0; + virtual bool isBoundItem(const MWWorld::Ptr& item) = 0; + virtual bool isAllowedToUse(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) = 0; - /// Sets the NPC's Acrobatics skill to match the fWerewolfAcrobatics GMST. - /// It only applies to the current form the NPC is in. - virtual void applyWerewolfAcrobatics(const MWWorld::Ptr& actor) = 0; + /// Turn actor into werewolf or normal form. + virtual void setWerewolf(const MWWorld::Ptr& actor, bool werewolf) = 0; - virtual void cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) = 0; + /// Sets the NPC's Acrobatics skill to match the fWerewolfAcrobatics GMST. + /// It only applies to the current form the NPC is in. + virtual void applyWerewolfAcrobatics(const MWWorld::Ptr& actor) = 0; - virtual void confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count) = 0; - virtual bool isAttackPreparing(const MWWorld::Ptr& ptr) = 0; - virtual bool isRunning(const MWWorld::Ptr& ptr) = 0; - virtual bool isSneaking(const MWWorld::Ptr& ptr) = 0; + virtual void cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) = 0; - virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; + virtual void confiscateStolenItemToOwner( + const MWWorld::Ptr& player, const MWWorld::Ptr& item, const MWWorld::Ptr& victim, int count) + = 0; + virtual bool isAttackPreparing(const MWWorld::Ptr& ptr) = 0; + virtual bool isRunning(const MWWorld::Ptr& ptr) = 0; + virtual bool isSneaking(const MWWorld::Ptr& ptr) = 0; - virtual int getGreetingTimer(const MWWorld::Ptr& ptr) const = 0; - virtual float getAngleToPlayer(const MWWorld::Ptr& ptr) const = 0; - virtual MWMechanics::GreetingState getGreetingState(const MWWorld::Ptr& ptr) const = 0; - virtual bool isTurningToPlayer(const MWWorld::Ptr& ptr) const = 0; + virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; - virtual void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) = 0; + virtual int getGreetingTimer(const MWWorld::Ptr& ptr) const = 0; + virtual float getAngleToPlayer(const MWWorld::Ptr& ptr) const = 0; + virtual MWMechanics::GreetingState getGreetingState(const MWWorld::Ptr& ptr) const = 0; + virtual bool isTurningToPlayer(const MWWorld::Ptr& ptr) const = 0; }; } diff --git a/apps/openmw/mwbase/scriptmanager.hpp b/apps/openmw/mwbase/scriptmanager.hpp index ac8333ed103..ceb651e691a 100644 --- a/apps/openmw/mwbase/scriptmanager.hpp +++ b/apps/openmw/mwbase/scriptmanager.hpp @@ -1,15 +1,21 @@ #ifndef GAME_MWBASE_SCRIPTMANAGER_H #define GAME_MWBASE_SCRIPTMANAGER_H -#include +#include namespace Interpreter { class Context; } +namespace ESM +{ + class RefId; +} + namespace Compiler { + class Extensions; class Locals; } @@ -23,36 +29,37 @@ namespace MWBase /// \brief Interface for script manager (implemented in MWScript) class ScriptManager { - ScriptManager (const ScriptManager&); - ///< not implemented + ScriptManager(const ScriptManager&); + ///< not implemented - ScriptManager& operator= (const ScriptManager&); - ///< not implemented + ScriptManager& operator=(const ScriptManager&); + ///< not implemented - public: + public: + ScriptManager() {} - ScriptManager() {} + virtual ~ScriptManager() {} - virtual ~ScriptManager() {} + virtual void clear() = 0; - virtual void clear() = 0; + virtual bool run(const ESM::RefId& name, Interpreter::Context& interpreterContext) = 0; + ///< Run the script with the given name (compile first, if not compiled yet) - virtual bool run (const std::string& name, Interpreter::Context& interpreterContext) = 0; - ///< Run the script with the given name (compile first, if not compiled yet) + virtual bool compile(const ESM::RefId& name) = 0; + ///< Compile script with the given namen + /// \return Success? - virtual bool compile (const std::string& name) = 0; - ///< Compile script with the given namen - /// \return Success? + virtual std::pair compileAll() = 0; + ///< Compile all scripts + /// \return count, success - virtual std::pair compileAll() = 0; - ///< Compile all scripts - /// \return count, success + virtual const Compiler::Locals& getLocals(const ESM::RefId& name) = 0; + ///< Return locals for script \a name. - virtual const Compiler::Locals& getLocals (const std::string& name) = 0; - ///< Return locals for script \a name. + virtual MWScript::GlobalScripts& getGlobalScripts() = 0; - virtual MWScript::GlobalScripts& getGlobalScripts() = 0; - }; + virtual const Compiler::Extensions& getExtensions() const = 0; + }; } #endif diff --git a/apps/openmw/mwbase/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index 2bac561fdd2..5f96a4e095f 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -2,17 +2,25 @@ #define GAME_MWBASE_SOUNDMANAGER_H #include -#include #include +#include +#include + +#include -#include "../mwworld/ptr.hpp" #include "../mwsound/type.hpp" +#include "../mwworld/ptr.hpp" namespace MWWorld { class CellStore; } +namespace ESM +{ + class RefId; +} + namespace MWSound { // Each entry excepts of MaxCount should be used only in one place @@ -23,34 +31,56 @@ namespace MWSound MaxCount }; + enum class MusicType + { + Normal, + MWScript + }; + class Sound; class Stream; struct Sound_Decoder; typedef std::shared_ptr DecoderPtr; /* These must all fit together */ - enum class PlayMode { - Normal = 0, /* non-looping, affected by environment */ - Loop = 1<<0, /* Sound will continually loop until explicitly stopped */ - NoEnv = 1<<1, /* Do not apply environment effects (eg, underwater filters) */ - RemoveAtDistance = 1<<2, /* (3D only) If the listener gets further than 2000 units away - * from the sound source, the sound is removed. - * This is weird stuff but apparently how vanilla works for sounds - * played by the PlayLoopSound family of script functions. Perhaps - * we can make this cut off a more subtle fade later, but have to - * be careful to not change the overall volume of areas by too - * much. */ - NoPlayerLocal = 1<<3, /* (3D only) Don't play the sound local to the listener even if the - * player is making it. */ + enum class PlayMode + { + Normal = 0, /* non-looping, affected by environment */ + Loop = 1 << 0, /* Sound will continually loop until explicitly stopped */ + NoEnv = 1 << 1, /* Do not apply environment effects (eg, underwater filters) */ + RemoveAtDistance = 1 << 2, /* (3D only) If the listener gets further than 2000 units away + * from the sound source, the sound is removed. + * This is weird stuff but apparently how vanilla works for sounds + * played by the PlayLoopSound family of script functions. Perhaps + * we can make this cut off a more subtle fade later, but have to + * be careful to not change the overall volume of areas by too + * much. */ + NoPlayerLocal = 1 << 3, /* (3D only) Don't play the sound local to the listener even if the + * player is making it. */ + NoScaling = 1 << 4, /* Don't scale audio with simulation time */ + NoEnvNoScaling = NoEnv | NoScaling, LoopNoEnv = Loop | NoEnv, + LoopNoEnvNoScaling = Loop | NoEnv | NoScaling, LoopRemoveAtDistance = Loop | RemoveAtDistance }; // Used for creating a type mask for SoundManager::pauseSounds and resumeSounds - inline int operator~(Type a) { return ~static_cast(a); } - inline int operator&(Type a, Type b) { return static_cast(a) & static_cast(b); } - inline int operator&(int a, Type b) { return a & static_cast(b); } - inline int operator|(Type a, Type b) { return static_cast(a) | static_cast(b); } + inline int operator~(Type a) + { + return ~static_cast(a); + } + inline int operator&(Type a, Type b) + { + return static_cast(a) & static_cast(b); + } + inline int operator&(int a, Type b) + { + return a & static_cast(b); + } + inline int operator|(Type a, Type b) + { + return static_cast(a) | static_cast(b); + } } namespace MWBase @@ -61,128 +91,155 @@ namespace MWBase /// \brief Interface for sound manager (implemented in MWSound) class SoundManager { - SoundManager (const SoundManager&); - ///< not implemented + SoundManager(const SoundManager&); + ///< not implemented + + SoundManager& operator=(const SoundManager&); + ///< not implemented + + protected: + using PlayMode = MWSound::PlayMode; + using Type = MWSound::Type; + + float mSimulationTimeScale = 1.0; + + public: + SoundManager() {} + virtual ~SoundManager() {} + + virtual void processChangedSettings(const std::set>& settings) = 0; - SoundManager& operator= (const SoundManager&); - ///< not implemented + virtual bool isEnabled() const = 0; + ///< Returns true if sound system is enabled - protected: - using PlayMode = MWSound::PlayMode; - using Type = MWSound::Type; + virtual void stopMusic() = 0; + ///< Stops music if it's playing - public: - SoundManager() {} - virtual ~SoundManager() {} + virtual MWSound::MusicType getMusicType() const = 0; - virtual void processChangedSettings(const std::set< std::pair >& settings) = 0; + virtual void streamMusic(VFS::Path::NormalizedView filename, MWSound::MusicType type, float fade = 1.f) = 0; + ///< Play a soundifle + /// \param filename name of a sound file in the data directory. + /// \param type music type. + /// \param fade time in seconds to fade out current track before start this one. - virtual void stopMusic() = 0; - ///< Stops music if it's playing + virtual bool isMusicPlaying() = 0; + ///< Returns true if music is playing - virtual void streamMusic(const std::string& filename) = 0; - ///< Play a soundifle - /// \param filename name of a sound file in "Music/" in the data directory. + virtual void say(const MWWorld::ConstPtr& reference, VFS::Path::NormalizedView filename) = 0; + ///< Make an actor say some text. + /// \param filename name of a sound file in the VFS - virtual bool isMusicPlaying() = 0; - ///< Returns true if music is playing + virtual void say(VFS::Path::NormalizedView filename) = 0; + ///< Say some text, without an actor ref + /// \param filename name of a sound file in the VFS - virtual void playPlaylist(const std::string &playlist) = 0; - ///< Start playing music from the selected folder - /// \param name of the folder that contains the playlist + virtual bool sayActive(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) const = 0; + ///< Is actor not speaking? - virtual void playTitleMusic() = 0; - ///< Start playing title music + virtual bool sayDone(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) const = 0; + ///< For scripting backward compatibility - virtual void say(const MWWorld::ConstPtr &reference, const std::string& filename) = 0; - ///< Make an actor say some text. - /// \param filename name of a sound file in "Sound/" in the data directory. + virtual void stopSay(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) = 0; + ///< Stop an actor speaking - virtual void say(const std::string& filename) = 0; - ///< Say some text, without an actor ref - /// \param filename name of a sound file in "Sound/" in the data directory. + virtual float getSaySoundLoudness(const MWWorld::ConstPtr& reference) const = 0; + ///< Check the currently playing say sound for this actor + /// and get an average loudness value (scale [0,1]) at the current time position. + /// If the actor is not saying anything, returns 0. - virtual bool sayActive(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) const = 0; - ///< Is actor not speaking? + virtual SoundStream* playTrack(const MWSound::DecoderPtr& decoder, Type type) = 0; + ///< Play a 2D audio track, using a custom decoder. The caller is expected to call + /// stopTrack with the returned handle when done. - virtual bool sayDone(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) const = 0; - ///< For scripting backward compatibility + virtual void stopTrack(SoundStream* stream) = 0; + ///< Stop the given audio track from playing - virtual void stopSay(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) = 0; - ///< Stop an actor speaking + virtual double getTrackTimeDelay(SoundStream* stream) = 0; + ///< Retives the time delay, in seconds, of the audio track (must be a sound + /// returned by \ref playTrack). Only intended to be called by the track + /// decoder's read method. - virtual float getSaySoundLoudness(const MWWorld::ConstPtr& reference) const = 0; - ///< Check the currently playing say sound for this actor - /// and get an average loudness value (scale [0,1]) at the current time position. - /// If the actor is not saying anything, returns 0. + virtual Sound* playSound(const ESM::RefId& soundId, float volume, float pitch, Type type = Type::Sfx, + PlayMode mode = PlayMode::Normal, float offset = 0) + = 0; + ///< Play a sound, independently of 3D-position + ///< @param offset Number of seconds into the sound to start playback. - virtual SoundStream *playTrack(const MWSound::DecoderPtr& decoder, Type type) = 0; - ///< Play a 2D audio track, using a custom decoder. The caller is expected to call - /// stopTrack with the returned handle when done. + virtual Sound* playSound(std::string_view fileName, float volume, float pitch, Type type = Type::Sfx, + PlayMode mode = PlayMode::Normal, float offset = 0) + = 0; + ///< Play a sound, independently of 3D-position + ///< @param offset Number of seconds into the sound to start playback. - virtual void stopTrack(SoundStream *stream) = 0; - ///< Stop the given audio track from playing + virtual Sound* playSound3D(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId, float volume, + float pitch, Type type = Type::Sfx, PlayMode mode = PlayMode::Normal, float offset = 0) + = 0; + ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless + ///< Play_NoTrack is specified. + ///< @param offset Number of seconds into the sound to start playback. - virtual double getTrackTimeDelay(SoundStream *stream) = 0; - ///< Retives the time delay, in seconds, of the audio track (must be a sound - /// returned by \ref playTrack). Only intended to be called by the track - /// decoder's read method. + virtual Sound* playSound3D(const MWWorld::ConstPtr& reference, std::string_view fileName, float volume, + float pitch, Type type = Type::Sfx, PlayMode mode = PlayMode::Normal, float offset = 0) + = 0; + ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless + ///< Play_NoTrack is specified. + ///< @param offset Number of seconds into the sound to start playback. - virtual Sound *playSound(const std::string& soundId, float volume, float pitch, - Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, - float offset=0) = 0; - ///< Play a sound, independently of 3D-position - ///< @param offset Number of seconds into the sound to start playback. + virtual Sound* playSound3D(const osg::Vec3f& initialPos, const ESM::RefId& soundId, float volume, float pitch, + Type type = Type::Sfx, PlayMode mode = PlayMode::Normal, float offset = 0) + = 0; + ///< Play a 3D sound at \a initialPos. If the sound should be moving, it must be updated using + ///< Sound::setPosition. - virtual Sound *playSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, - float volume, float pitch, Type type=Type::Sfx, - PlayMode mode=PlayMode::Normal, float offset=0) = 0; - ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless Play_NoTrack is specified. - ///< @param offset Number of seconds into the sound to start playback. + virtual void stopSound(Sound* sound) = 0; + ///< Stop the given sound from playing - virtual Sound *playSound3D(const osg::Vec3f& initialPos, const std::string& soundId, - float volume, float pitch, Type type=Type::Sfx, - PlayMode mode=PlayMode::Normal, float offset=0) = 0; - ///< Play a 3D sound at \a initialPos. If the sound should be moving, it must be updated using Sound::setPosition. + virtual void stopSound3D(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId) = 0; + ///< Stop the given object from playing the given sound. - virtual void stopSound(Sound *sound) = 0; - ///< Stop the given sound from playing + virtual void stopSound3D(const MWWorld::ConstPtr& reference, std::string_view fileName) = 0; + ///< Stop the given object from playing the given sound. - virtual void stopSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId) = 0; - ///< Stop the given object from playing the given sound, + virtual void stopSound3D(const MWWorld::ConstPtr& reference) = 0; + ///< Stop the given object from playing all sounds. - virtual void stopSound3D(const MWWorld::ConstPtr &reference) = 0; - ///< Stop the given object from playing all sounds. + virtual void stopSound(const MWWorld::CellStore* cell) = 0; + ///< Stop all sounds for the given cell. - virtual void stopSound(const MWWorld::CellStore *cell) = 0; - ///< Stop all sounds for the given cell. + virtual void fadeOutSound3D(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId, float duration) = 0; + ///< Fade out given sound (that is already playing) of given object + ///< @param reference Reference to object, whose sound is faded out + ///< @param soundId ID of the sound to fade out. + ///< @param duration Time until volume reaches 0. - virtual void fadeOutSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, float duration) = 0; - ///< Fade out given sound (that is already playing) of given object - ///< @param reference Reference to object, whose sound is faded out - ///< @param soundId ID of the sound to fade out. - ///< @param duration Time until volume reaches 0. + virtual bool getSoundPlaying(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId) const = 0; + ///< Is the given sound currently playing on the given object? + /// If you want to check if sound played with playSound is playing, use empty Ptr - virtual bool getSoundPlaying(const MWWorld::ConstPtr &reference, const std::string& soundId) const = 0; - ///< Is the given sound currently playing on the given object? - /// If you want to check if sound played with playSound is playing, use empty Ptr + virtual bool getSoundPlaying(const MWWorld::ConstPtr& reference, std::string_view fileName) const = 0; + ///< Is the given sound currently playing on the given object? + /// If you want to check if sound played with playSound is playing, use empty Ptr - virtual void pauseSounds(MWSound::BlockerType blocker, int types=int(Type::Mask)) = 0; - ///< Pauses all currently playing sounds, including music. + virtual void pauseSounds(MWSound::BlockerType blocker, int types = int(Type::Mask)) = 0; + ///< Pauses all currently playing sounds, including music. - virtual void resumeSounds(MWSound::BlockerType blocker) = 0; - ///< Resumes all previously paused sounds. + virtual void resumeSounds(MWSound::BlockerType blocker) = 0; + ///< Resumes all previously paused sounds. - virtual void pausePlayback() = 0; - virtual void resumePlayback() = 0; + virtual void pausePlayback() = 0; + virtual void resumePlayback() = 0; - virtual void update(float duration) = 0; + virtual void setListenerPosDir( + const osg::Vec3f& pos, const osg::Vec3f& dir, const osg::Vec3f& up, bool underwater) + = 0; - virtual void setListenerPosDir(const osg::Vec3f &pos, const osg::Vec3f &dir, const osg::Vec3f &up, bool underwater) = 0; + virtual void updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) = 0; - virtual void updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) = 0; + void setSimulationTimeScale(float scale) { mSimulationTimeScale = scale; } + float getSimulationTimeScale() const { return mSimulationTimeScale; } - virtual void clear() = 0; + virtual void clear() = 0; }; } diff --git a/apps/openmw/mwbase/statemanager.hpp b/apps/openmw/mwbase/statemanager.hpp index 157833a0eeb..35435e14301 100644 --- a/apps/openmw/mwbase/statemanager.hpp +++ b/apps/openmw/mwbase/statemanager.hpp @@ -1,6 +1,7 @@ #ifndef GAME_MWSTATE_STATEMANAGER_H #define GAME_MWSTATE_STATEMANAGER_H +#include #include #include @@ -15,81 +16,77 @@ namespace MWBase /// \brief Interface for game state manager (implemented in MWState) class StateManager { - public: + public: + enum State + { + State_NoGame, + State_Ended, + State_Running + }; - enum State - { - State_NoGame, - State_Ended, - State_Running - }; + typedef std::list::const_iterator CharacterIterator; - typedef std::list::const_iterator CharacterIterator; + private: + StateManager(const StateManager&); + ///< not implemented - private: + StateManager& operator=(const StateManager&); + ///< not implemented - StateManager (const StateManager&); - ///< not implemented + public: + StateManager() {} - StateManager& operator= (const StateManager&); - ///< not implemented + virtual ~StateManager() {} - public: + virtual void requestQuit() = 0; - StateManager() {} + virtual bool hasQuitRequest() const = 0; - virtual ~StateManager() {} + virtual void askLoadRecent() = 0; - virtual void requestQuit() = 0; + virtual void requestNewGame() = 0; + virtual void requestLoad(const std::filesystem::path& filepath) = 0; - virtual bool hasQuitRequest() const = 0; + virtual State getState() const = 0; - virtual void askLoadRecent() = 0; + virtual void newGame(bool bypass = false) = 0; + ///< Start a new game. + /// + /// \param bypass Skip new game mechanics. - virtual State getState() const = 0; + virtual void resumeGame() = 0; - virtual void newGame (bool bypass = false) = 0; - ///< Start a new game. - /// - /// \param bypass Skip new game mechanics. + virtual void deleteGame(const MWState::Character* character, const MWState::Slot* slot) = 0; - virtual void endGame() = 0; + virtual void saveGame(std::string_view description, const MWState::Slot* slot = nullptr) = 0; + ///< Write a saved game to \a slot or create a new slot if \a slot == 0. + /// + /// \note Slot must belong to the current character. - virtual void resumeGame() = 0; + virtual void loadGame(const std::filesystem::path& filepath) = 0; + ///< Load a saved game directly from the given file path. This will search the CharacterManager + /// for a Character containing this save file, and set this Character current if one was found. + /// Otherwise, a new Character will be created. - virtual void deleteGame (const MWState::Character *character, const MWState::Slot *slot) = 0; + virtual void loadGame(const MWState::Character* character, const std::filesystem::path& filepath) = 0; + ///< Load a saved game file belonging to the given character. - virtual void saveGame (const std::string& description, const MWState::Slot *slot = nullptr) = 0; - ///< Write a saved game to \a slot or create a new slot if \a slot == 0. - /// - /// \note Slot must belong to the current character. + /// Simple saver, writes over the file if already existing + /** Used for quick save and autosave **/ + virtual void quickSave(std::string = "Quicksave") = 0; - virtual void loadGame (const std::string& filepath) = 0; - ///< Load a saved game directly from the given file path. This will search the CharacterManager - /// for a Character containing this save file, and set this Character current if one was found. - /// Otherwise, a new Character will be created. + /// Simple loader, loads the last saved file + /** Used for quickload **/ + virtual void quickLoad() = 0; - virtual void loadGame (const MWState::Character *character, const std::string& filepath) = 0; - ///< Load a saved game file belonging to the given character. + virtual MWState::Character* getCurrentCharacter() = 0; + ///< @note May return null. - ///Simple saver, writes over the file if already existing - /** Used for quick save and autosave **/ - virtual void quickSave(std::string = "Quicksave")=0; + virtual CharacterIterator characterBegin() = 0; + ///< Any call to SaveGame and getCurrentCharacter can invalidate the returned + /// iterator. - ///Simple loader, loads the last saved file - /** Used for quickload **/ - virtual void quickLoad()=0; - - virtual MWState::Character *getCurrentCharacter () = 0; - ///< @note May return null. - - virtual CharacterIterator characterBegin() = 0; - ///< Any call to SaveGame and getCurrentCharacter can invalidate the returned - /// iterator. - - virtual CharacterIterator characterEnd() = 0; - - virtual void update (float duration) = 0; + virtual CharacterIterator characterEnd() = 0; }; } diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 31d188879f3..0b2e85f3e1e 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -1,11 +1,13 @@ #ifndef GAME_MWBASE_WINDOWMANAGER_H #define GAME_MWBASE_WINDOWMANAGER_H -#include -#include -#include +#include +#include #include +#include #include +#include +#include #include @@ -13,6 +15,11 @@ #include +namespace ESM +{ + class RefId; +} + namespace Loading { class Listener; @@ -34,13 +41,12 @@ namespace ESM { class ESMReader; class ESMWriter; - struct CellId; } namespace MWMechanics { class AttributeValue; - template + template class DynamicStat; class SkillValue; } @@ -69,8 +75,12 @@ namespace MWGui class DialogueWindow; class WindowModal; class JailScreen; + class MessageBox; + class PostProcessorHud; + class SettingsWindow; - enum ShowInDialogueMode { + enum ShowInDialogueMode + { ShowInDialogueMode_IfPossible, ShowInDialogueMode_Only, ShowInDialogueMode_Never @@ -89,266 +99,293 @@ namespace MWBase /// \brief Interface for widnow manager (implemented in MWGui) class WindowManager : public SDLUtil::WindowListener { - WindowManager (const WindowManager&); - ///< not implemented + WindowManager(const WindowManager&); + ///< not implemented + + WindowManager& operator=(const WindowManager&); + ///< not implemented + + public: + typedef std::vector SkillList; - WindowManager& operator= (const WindowManager&); - ///< not implemented + WindowManager() {} - public: + virtual ~WindowManager() {} - typedef std::vector SkillList; + /// @note This method will block until the video finishes playing + /// (and will continually update the window while doing so) + virtual void playVideo(std::string_view name, bool allowSkipping, bool overrideSounds = true) = 0; - WindowManager() {} + virtual void setNewGame(bool newgame) = 0; - virtual ~WindowManager() {} + virtual void pushGuiMode(MWGui::GuiMode mode, const MWWorld::Ptr& arg) = 0; + virtual void pushGuiMode(MWGui::GuiMode mode) = 0; + virtual void popGuiMode(bool forceExit = false) = 0; - /// @note This method will block until the video finishes playing - /// (and will continually update the window while doing so) - virtual void playVideo(const std::string& name, bool allowSkipping) = 0; + virtual void removeGuiMode(MWGui::GuiMode mode) = 0; + ///< can be anywhere in the stack - virtual void setNewGame(bool newgame) = 0; + virtual void goToJail(int days) = 0; - virtual void pushGuiMode (MWGui::GuiMode mode, const MWWorld::Ptr& arg) = 0; - virtual void pushGuiMode (MWGui::GuiMode mode) = 0; - virtual void popGuiMode(bool noSound=false) = 0; + virtual void updatePlayer() = 0; - virtual void removeGuiMode (MWGui::GuiMode mode, bool noSound=false) = 0; - ///< can be anywhere in the stack + virtual MWGui::GuiMode getMode() const = 0; + virtual bool containsMode(MWGui::GuiMode) const = 0; - virtual void goToJail(int days) = 0; + virtual bool isGuiMode() const = 0; - virtual void updatePlayer() = 0; + virtual bool isConsoleMode() const = 0; + virtual bool isPostProcessorHudVisible() const = 0; + virtual bool isSettingsWindowVisible() const = 0; + virtual bool isInteractiveMessageBoxActive() const = 0; - virtual MWGui::GuiMode getMode() const = 0; - virtual bool containsMode(MWGui::GuiMode) const = 0; + virtual void toggleVisible(MWGui::GuiWindow wnd) = 0; - virtual bool isGuiMode() const = 0; + virtual void forceHide(MWGui::GuiWindow wnd) = 0; + virtual void unsetForceHide(MWGui::GuiWindow wnd) = 0; - virtual bool isConsoleMode() const = 0; + /// Disallow all inventory mode windows + virtual void disallowAll() = 0; - virtual void toggleVisible (MWGui::GuiWindow wnd) = 0; + /// Allow one or more windows + virtual void allow(MWGui::GuiWindow wnd) = 0; - virtual void forceHide(MWGui::GuiWindow wnd) = 0; - virtual void unsetForceHide(MWGui::GuiWindow wnd) = 0; + virtual bool isAllowed(MWGui::GuiWindow wnd) const = 0; - /// Disallow all inventory mode windows - virtual void disallowAll() = 0; + /// \todo investigate, if we really need to expose every single lousy UI element to the outside world + virtual MWGui::InventoryWindow* getInventoryWindow() = 0; + virtual MWGui::CountDialog* getCountDialog() = 0; + virtual MWGui::ConfirmationDialog* getConfirmationDialog() = 0; + virtual MWGui::TradeWindow* getTradeWindow() = 0; + virtual MWGui::PostProcessorHud* getPostProcessorHud() = 0; - /// Allow one or more windows - virtual void allow (MWGui::GuiWindow wnd) = 0; + /// Make the player use an item, while updating GUI state accordingly + virtual void useItem(const MWWorld::Ptr& item, bool force = false) = 0; - virtual bool isAllowed (MWGui::GuiWindow wnd) const = 0; + virtual void updateSpellWindow() = 0; - /// \todo investigate, if we really need to expose every single lousy UI element to the outside world - virtual MWGui::InventoryWindow* getInventoryWindow() = 0; - virtual MWGui::CountDialog* getCountDialog() = 0; - virtual MWGui::ConfirmationDialog* getConfirmationDialog() = 0; - virtual MWGui::TradeWindow* getTradeWindow() = 0; + virtual void setConsoleSelectedObject(const MWWorld::Ptr& object) = 0; + virtual MWWorld::Ptr getConsoleSelectedObject() const = 0; + virtual void setConsoleMode(std::string_view mode) = 0; + virtual const std::string& getConsoleMode() = 0; - /// Make the player use an item, while updating GUI state accordingly - virtual void useItem(const MWWorld::Ptr& item, bool force=false) = 0; + static constexpr std::string_view sConsoleColor_Default = "#FFFFFF"; + static constexpr std::string_view sConsoleColor_Error = "#FF2222"; + static constexpr std::string_view sConsoleColor_Success = "#FF00FF"; + static constexpr std::string_view sConsoleColor_Info = "#AAAAAA"; + virtual void printToConsole(const std::string& msg, std::string_view color) = 0; - virtual void updateSpellWindow() = 0; + /// Set time left for the player to start drowning (update the drowning bar) + /// @param time time left to start drowning + /// @param maxTime how long we can be underwater (in total) until drowning starts + virtual void setDrowningTimeLeft(float time, float maxTime) = 0; - virtual void setConsoleSelectedObject(const MWWorld::Ptr& object) = 0; + virtual void changeCell(const MWWorld::CellStore* cell) = 0; + ///< change the active cell - /// Set time left for the player to start drowning (update the drowning bar) - /// @param time time left to start drowning - /// @param maxTime how long we can be underwater (in total) until drowning starts - virtual void setDrowningTimeLeft (float time, float maxTime) = 0; + virtual void setFocusObject(const MWWorld::Ptr& focus) = 0; + virtual void setFocusObjectScreenCoords(float min_x, float min_y, float max_x, float max_y) = 0; - virtual void changeCell(const MWWorld::CellStore* cell) = 0; - ///< change the active cell + virtual void setCursorVisible(bool visible) = 0; + virtual void setCursorActive(bool active) = 0; + virtual void getMousePosition(int& x, int& y) = 0; + virtual void getMousePosition(float& x, float& y) = 0; + virtual void setDragDrop(bool dragDrop) = 0; + virtual bool getWorldMouseOver() = 0; - virtual void setFocusObject(const MWWorld::Ptr& focus) = 0; - virtual void setFocusObjectScreenCoords(float min_x, float min_y, float max_x, float max_y) = 0; + virtual float getScalingFactor() const = 0; - virtual void setCursorVisible(bool visible) = 0; - virtual void setCursorActive(bool active) = 0; - virtual void getMousePosition(int &x, int &y) = 0; - virtual void getMousePosition(float &x, float &y) = 0; - virtual void setDragDrop(bool dragDrop) = 0; - virtual bool getWorldMouseOver() = 0; + virtual bool toggleFogOfWar() = 0; - virtual float getScalingFactor() = 0; + virtual bool toggleFullHelp() = 0; + ///< show extra info in item tooltips (owner, script) - virtual bool toggleFogOfWar() = 0; + virtual bool getFullHelp() const = 0; - virtual bool toggleFullHelp() = 0; - ///< show extra info in item tooltips (owner, script) + /// sets the visibility of the drowning bar + virtual void setDrowningBarVisibility(bool visible) = 0; - virtual bool getFullHelp() const = 0; + /// sets the visibility of the hud health/magicka/stamina bars + virtual void setHMSVisibility(bool visible) = 0; - virtual void setActiveMap(int x, int y, bool interior) = 0; - ///< set the indices of the map texture that should be used + /// sets the visibility of the hud minimap + virtual void setMinimapVisibility(bool visible) = 0; + virtual void setWeaponVisibility(bool visible) = 0; + virtual void setSpellVisibility(bool visible) = 0; + virtual void setSneakVisibility(bool visible) = 0; - /// sets the visibility of the drowning bar - virtual void setDrowningBarVisibility(bool visible) = 0; + /// activate selected quick key + virtual void activateQuickKey(int index) = 0; + /// update activated quick key state (if action executing was delayed for some reason) + virtual void updateActivatedQuickKey() = 0; - /// sets the visibility of the hud health/magicka/stamina bars - virtual void setHMSVisibility(bool visible) = 0; + virtual const ESM::RefId& getSelectedSpell() = 0; + virtual void setSelectedSpell(const ESM::RefId& spellId, int successChancePercent) = 0; + virtual void setSelectedEnchantItem(const MWWorld::Ptr& item) = 0; + virtual const MWWorld::Ptr& getSelectedEnchantItem() const = 0; + virtual void setSelectedWeapon(const MWWorld::Ptr& item) = 0; + virtual const MWWorld::Ptr& getSelectedWeapon() const = 0; + virtual void unsetSelectedSpell() = 0; + virtual void unsetSelectedWeapon() = 0; - /// sets the visibility of the hud minimap - virtual void setMinimapVisibility(bool visible) = 0; - virtual void setWeaponVisibility(bool visible) = 0; - virtual void setSpellVisibility(bool visible) = 0; - virtual void setSneakVisibility(bool visible) = 0; + virtual void showCrosshair(bool show) = 0; + virtual bool setHudVisibility(bool show) = 0; + virtual bool isHudVisible() const = 0; - /// activate selected quick key - virtual void activateQuickKey (int index) = 0; - /// update activated quick key state (if action executing was delayed for some reason) - virtual void updateActivatedQuickKey () = 0; + virtual void disallowMouse() = 0; + virtual void allowMouse() = 0; + virtual void notifyInputActionBound() = 0; - virtual std::string getSelectedSpell() = 0; - virtual void setSelectedSpell(const std::string& spellId, int successChancePercent) = 0; - virtual void setSelectedEnchantItem(const MWWorld::Ptr& item) = 0; - virtual const MWWorld::Ptr& getSelectedEnchantItem() const = 0; - virtual void setSelectedWeapon(const MWWorld::Ptr& item) = 0; - virtual const MWWorld::Ptr& getSelectedWeapon() const = 0; - virtual int getFontHeight() const = 0; - virtual void unsetSelectedSpell() = 0; - virtual void unsetSelectedWeapon() = 0; + virtual void addVisitedLocation(const std::string& name, int x, int y) = 0; - virtual void showCrosshair(bool show) = 0; - virtual bool getSubtitlesEnabled() = 0; - virtual bool toggleHud() = 0; + /// Hides dialog and schedules dialog to be deleted. + virtual void removeDialog(std::unique_ptr&& dialog) = 0; - virtual void disallowMouse() = 0; - virtual void allowMouse() = 0; - virtual void notifyInputActionBound() = 0; + /// Gracefully attempts to exit the topmost GUI mode + /** No guarantee of actually closing the window **/ + virtual void exitCurrentGuiMode() = 0; - virtual void addVisitedLocation(const std::string& name, int x, int y) = 0; + virtual void messageBox(std::string_view message, + enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) + = 0; + /// Puts message into a queue to show on the next update. Thread safe alternative for messageBox. + virtual void scheduleMessageBox(std::string message, + enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) + = 0; + virtual void staticMessageBox(std::string_view message) = 0; + virtual void removeStaticMessageBox() = 0; + virtual void interactiveMessageBox(std::string_view message, const std::vector& buttons = {}, + bool block = false, int defaultFocus = -1) + = 0; - /// Hides dialog and schedules dialog to be deleted. - virtual void removeDialog(MWGui::Layout* dialog) = 0; + /// returns the index of the pressed button or -1 if no button was pressed + /// (->MessageBoxmanager->InteractiveMessageBox) + virtual int readPressedButton() = 0; - ///Gracefully attempts to exit the topmost GUI mode - /** No guarantee of actually closing the window **/ - virtual void exitCurrentGuiMode() = 0; + virtual void updateConsoleObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) = 0; - virtual void messageBox (const std::string& message, enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) = 0; - virtual void staticMessageBox(const std::string& message) = 0; - virtual void removeStaticMessageBox() = 0; - virtual void interactiveMessageBox (const std::string& message, - const std::vector& buttons = std::vector(), bool block=false) = 0; + /** + * Fetches a GMST string from the store, if there is no setting with the given + * ID or it is not a string the default string is returned. + * + * @param id Identifier for the GMST setting, e.g. "aName" + * @param default Default value if the GMST setting cannot be used. + */ + virtual std::string_view getGameSettingString(std::string_view id, std::string_view default_) = 0; - /// returns the index of the pressed button or -1 if no button was pressed (->MessageBoxmanager->InteractiveMessageBox) - virtual int readPressedButton() = 0; + virtual void processChangedSettings(const std::set>& changed) = 0; - virtual void update (float duration) = 0; + virtual void executeInConsole(const std::filesystem::path& path) = 0; - virtual void updateConsoleObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) = 0; + virtual void enableRest() = 0; + virtual bool getRestEnabled() = 0; + virtual bool getJournalAllowed() = 0; - /** - * Fetches a GMST string from the store, if there is no setting with the given - * ID or it is not a string the default string is returned. - * - * @param id Identifier for the GMST setting, e.g. "aName" - * @param default Default value if the GMST setting cannot be used. - */ - virtual std::string getGameSettingString(const std::string &id, const std::string &default_) = 0; + virtual bool getPlayerSleeping() = 0; + virtual void wakeUpPlayer() = 0; - virtual void processChangedSettings(const std::set< std::pair >& changed) = 0; + virtual void showSoulgemDialog(MWWorld::Ptr item) = 0; - virtual void executeInConsole (const std::string& path) = 0; + virtual void changePointer(const std::string& name) = 0; - virtual void enableRest() = 0; - virtual bool getRestEnabled() = 0; - virtual bool getJournalAllowed() = 0; + virtual void setEnemy(const MWWorld::Ptr& enemy) = 0; - virtual bool getPlayerSleeping() = 0; - virtual void wakeUpPlayer() = 0; + virtual std::size_t getMessagesCount() const = 0; - virtual void showSoulgemDialog (MWWorld::Ptr item) = 0; + virtual const Translation::Storage& getTranslationDataStorage() const = 0; - virtual void changePointer (const std::string& name) = 0; + /// Warning: do not use MyGUI::InputManager::setKeyFocusWidget directly. Instead use this. + virtual void setKeyFocusWidget(MyGUI::Widget* widget) = 0; - virtual void setEnemy (const MWWorld::Ptr& enemy) = 0; + virtual Loading::Listener* getLoadingScreen() = 0; - virtual int getMessagesCount() const = 0; + /// Should the cursor be visible? + virtual bool getCursorVisible() = 0; - virtual const Translation::Storage& getTranslationDataStorage() const = 0; + /// Clear all savegame-specific data + virtual void clear() = 0; - /// Warning: do not use MyGUI::InputManager::setKeyFocusWidget directly. Instead use this. - virtual void setKeyFocusWidget (MyGUI::Widget* widget) = 0; + virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) = 0; + virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; + virtual int countSavedGameRecords() const = 0; - virtual void loadUserFonts() = 0; + /// Does the current stack of GUI-windows permit saving? + virtual bool isSavingAllowed() const = 0; - virtual Loading::Listener* getLoadingScreen() = 0; + /// Send exit command to active Modal window + virtual void exitCurrentModal() = 0; - /// Should the cursor be visible? - virtual bool getCursorVisible() = 0; + /// Sets the current Modal + /** Used to send exit command to active Modal when Esc is pressed **/ + virtual void addCurrentModal(MWGui::WindowModal* input) = 0; - /// Clear all savegame-specific data - virtual void clear() = 0; + /// Removes the top Modal + /** Used when one Modal adds another Modal + \param input Pointer to the current modal, to ensure proper modal is removed **/ + virtual void removeCurrentModal(MWGui::WindowModal* input) = 0; - virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) = 0; - virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; - virtual int countSavedGameRecords() const = 0; + virtual void pinWindow(MWGui::GuiWindow window) = 0; + virtual void toggleMaximized(MWGui::Layout* layout) = 0; - /// Does the current stack of GUI-windows permit saving? - virtual bool isSavingAllowed() const = 0; + /// Fade the screen in, over \a time seconds + virtual void fadeScreenIn(const float time, bool clearQueue = true, float delay = 0.f) = 0; + /// Fade the screen out to black, over \a time seconds + virtual void fadeScreenOut(const float time, bool clearQueue = true, float delay = 0.f) = 0; + /// Fade the screen to a specified percentage of black, over \a time seconds + virtual void fadeScreenTo(const int percent, const float time, bool clearQueue = true, float delay = 0.f) = 0; + /// Darken the screen to a specified percentage + virtual void setBlindness(const int percent) = 0; - /// Send exit command to active Modal window - virtual void exitCurrentModal() = 0; + virtual void activateHitOverlay(bool interrupt = true) = 0; + virtual void setWerewolfOverlay(bool set) = 0; - /// Sets the current Modal - /** Used to send exit command to active Modal when Esc is pressed **/ - virtual void addCurrentModal(MWGui::WindowModal* input) = 0; + virtual void toggleConsole() = 0; + virtual void toggleDebugWindow() = 0; + virtual void togglePostProcessorHud() = 0; + virtual void toggleSettingsWindow() = 0; - /// Removes the top Modal - /** Used when one Modal adds another Modal - \param input Pointer to the current modal, to ensure proper modal is removed **/ - virtual void removeCurrentModal(MWGui::WindowModal* input) = 0; + /// Cycle to next or previous spell + virtual void cycleSpell(bool next) = 0; + /// Cycle to next or previous weapon + virtual void cycleWeapon(bool next) = 0; - virtual void pinWindow (MWGui::GuiWindow window) = 0; - virtual void toggleMaximized(MWGui::Layout *layout) = 0; + virtual void playSound(const ESM::RefId& soundId, float volume = 1.f, float pitch = 1.f) = 0; - /// Fade the screen in, over \a time seconds - virtual void fadeScreenIn(const float time, bool clearQueue=true, float delay=0.f) = 0; - /// Fade the screen out to black, over \a time seconds - virtual void fadeScreenOut(const float time, bool clearQueue=true, float delay=0.f) = 0; - /// Fade the screen to a specified percentage of black, over \a time seconds - virtual void fadeScreenTo(const int percent, const float time, bool clearQueue=true, float delay=0.f) = 0; - /// Darken the screen to a specified percentage - virtual void setBlindness(const int percent) = 0; + virtual void addCell(MWWorld::CellStore* cell) = 0; + virtual void removeCell(MWWorld::CellStore* cell) = 0; + virtual void writeFog(MWWorld::CellStore* cell) = 0; - virtual void activateHitOverlay(bool interrupt=true) = 0; - virtual void setWerewolfOverlay(bool set) = 0; + virtual const MWGui::TextColours& getTextColours() = 0; - virtual void toggleConsole() = 0; - virtual void toggleDebugWindow() = 0; + virtual bool injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat) = 0; + virtual bool injectKeyRelease(MyGUI::KeyCode key) = 0; - /// Cycle to next or previous spell - virtual void cycleSpell(bool next) = 0; - /// Cycle to next or previous weapon - virtual void cycleWeapon(bool next) = 0; + void windowVisibilityChange(bool visible) override = 0; + void windowResized(int x, int y) override = 0; + void windowClosed() override = 0; + virtual bool isWindowVisible() = 0; - virtual void playSound(const std::string& soundId, float volume = 1.f, float pitch = 1.f) = 0; + virtual void watchActor(const MWWorld::Ptr& ptr) = 0; + virtual MWWorld::Ptr getWatchedActor() const = 0; - // In WindowManager for now since there isn't a VFS singleton - virtual std::string correctIconPath(const std::string& path) = 0; - virtual std::string correctBookartPath(const std::string& path, int width, int height, bool* exists = nullptr) = 0; - virtual std::string correctTexturePath(const std::string& path) = 0; - virtual bool textureExists(const std::string& path) = 0; + virtual const std::string& getVersionDescription() const = 0; - virtual void addCell(MWWorld::CellStore* cell) = 0; - virtual void removeCell(MWWorld::CellStore* cell) = 0; - virtual void writeFog(MWWorld::CellStore* cell) = 0; + virtual void onDeleteCustomData(const MWWorld::Ptr& ptr) = 0; + virtual void forceLootMode(const MWWorld::Ptr& ptr) = 0; - virtual const MWGui::TextColours& getTextColours() = 0; + virtual void asyncPrepareSaveMap() = 0; - virtual bool injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat) = 0; - virtual bool injectKeyRelease(MyGUI::KeyCode key) = 0; + /// Sets the cull masks for all applicable views + virtual void setCullMask(uint32_t mask) = 0; - void windowVisibilityChange(bool visible) override = 0; - void windowResized(int x, int y) override = 0; - void windowClosed() override = 0; - virtual bool isWindowVisible() = 0; + /// Same as viewer->getCamera()->getCullMask(), provided for consistency. + virtual uint32_t getCullMask() = 0; - virtual void watchActor(const MWWorld::Ptr& ptr) = 0; - virtual MWWorld::Ptr getWatchedActor() const = 0; + // Used in Lua bindings + virtual const std::vector& getGuiModeStack() const = 0; + virtual void setDisabledByLua(std::string_view windowId, bool disabled) = 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 7afd0697ebe..6ab5ab64fa3 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -3,17 +3,19 @@ #include "rotationflags.hpp" -#include +#include #include #include -#include - -#include +#include +#include +#include -#include +#include -#include "../mwworld/ptr.hpp" #include "../mwworld/doorstate.hpp" +#include "../mwworld/globalvariablename.hpp" +#include "../mwworld/ptr.hpp" +#include "../mwworld/spellcaststate.hpp" #include "../mwrender/rendermode.hpp" @@ -52,16 +54,22 @@ namespace ESM struct CreatureLevList; struct ItemLevList; struct TimeStamp; + class RefId; + struct ExteriorCellLocation; } namespace MWPhysics { + class RayCastingResult; class RayCastingInterface; } namespace MWRender { class Animation; + class Camera; + class RenderingManager; + class PostProcessor; } namespace MWMechanics @@ -72,6 +80,7 @@ namespace MWMechanics namespace DetourNavigator { struct Navigator; + struct AgentBounds; } namespace MWWorld @@ -82,8 +91,10 @@ namespace MWWorld class TimeStamp; class ESMStore; class RefData; + class Cell; + class DateTimeManager; - typedef std::vector > PtrMovementList; + typedef std::vector> PtrMovementList; } namespace MWBase @@ -91,572 +102,495 @@ namespace MWBase /// \brief Interface for the World (implemented in MWWorld) class World { - World (const World&); - ///< not implemented - - World& operator= (const World&); - ///< not implemented - - public: - - struct DoorMarker - { - std::string name; - float x, y; // world position - ESM::CellId dest; - }; - - World() {} - - virtual ~World() {} - - virtual void startNewGame (bool bypass) = 0; - ///< \param bypass Bypass regular game start. - - virtual void clear() = 0; - - virtual int countSavedGameRecords() const = 0; - virtual int countSavedGameCells() const = 0; - - virtual void write (ESM::ESMWriter& writer, Loading::Listener& listener) const = 0; - - virtual void readRecord (ESM::ESMReader& reader, uint32_t type, - const std::map& contentFileMap) = 0; - - virtual MWWorld::CellStore *getExterior (int x, int y) = 0; - - virtual MWWorld::CellStore *getInterior (const std::string& name) = 0; - - virtual MWWorld::CellStore *getCell (const ESM::CellId& id) = 0; - - virtual void testExteriorCells() = 0; - virtual void testInteriorCells() = 0; - - virtual void useDeathCamera() = 0; - - virtual void setWaterHeight(const float height) = 0; - - virtual bool toggleWater() = 0; - virtual bool toggleWorld() = 0; - virtual bool toggleBorders() = 0; - - virtual void adjustSky() = 0; - - virtual MWWorld::Player& getPlayer() = 0; - virtual MWWorld::Ptr getPlayerPtr() = 0; - virtual MWWorld::ConstPtr getPlayerConstPtr() const = 0; - - virtual const MWWorld::ESMStore& getStore() const = 0; - - virtual std::vector& getEsmReader() = 0; - - virtual MWWorld::LocalScripts& getLocalScripts() = 0; - - virtual bool hasCellChanged() const = 0; - ///< Has the set of active cells changed, since the last frame? - - virtual bool isCellExterior() const = 0; - - virtual bool isCellQuasiExterior() const = 0; - - virtual osg::Vec2f getNorthVector (const MWWorld::CellStore* cell) = 0; - ///< get north vector for given interior cell - - virtual void getDoorMarkers (MWWorld::CellStore* cell, std::vector& out) = 0; - ///< get a list of teleport door markers for a given cell, to be displayed on the local map - - virtual void setGlobalInt (const std::string& name, int value) = 0; - ///< Set value independently from real type. - - virtual void setGlobalFloat (const std::string& name, float value) = 0; - ///< Set value independently from real type. - - virtual int getGlobalInt (const std::string& name) const = 0; - ///< Get value independently from real type. - - virtual float getGlobalFloat (const std::string& name) const = 0; - ///< Get value independently from real type. - - virtual char getGlobalVariableType (const std::string& name) const = 0; - ///< Return ' ', if there is no global variable with this name. - - virtual std::string getCellName (const MWWorld::CellStore *cell = nullptr) const = 0; - ///< Return name of the cell. - /// - /// \note If cell==0, the cell the player is currently in will be used instead to - /// generate a name. - virtual std::string getCellName(const ESM::Cell* cell) const = 0; - - virtual void removeRefScript (MWWorld::RefData *ref) = 0; - //< Remove the script attached to ref from mLocalScripts + World(const World&); + ///< not implemented - virtual MWWorld::Ptr getPtr (const std::string& name, bool activeOnly) = 0; - ///< Return a pointer to a liveCellRef with the given name. - /// \param activeOnly do non search inactive cells. + World& operator=(const World&); + ///< not implemented - virtual MWWorld::Ptr searchPtr (const std::string& name, bool activeOnly, bool searchInContainers = true) = 0; - ///< Return a pointer to a liveCellRef with the given name. - /// \param activeOnly do non search inactive cells. + public: + struct DoorMarker + { + std::string name; + float x, y; // world position + ESM::RefId dest; + }; - virtual MWWorld::Ptr searchPtrViaActorId (int actorId) = 0; - ///< Search is limited to the active cells. + World() {} - virtual MWWorld::Ptr searchPtrViaRefNum (const std::string& id, const ESM::RefNum& refNum) = 0; + virtual ~World() {} - virtual MWWorld::Ptr findContainer (const MWWorld::ConstPtr& ptr) = 0; - ///< Return a pointer to a liveCellRef which contains \a ptr. - /// \note Search is limited to the active cells. + virtual void setRandomSeed(uint32_t seed) = 0; + ///< \param seed The seed used when starting a new game. - virtual void enable (const MWWorld::Ptr& ptr) = 0; + virtual void startNewGame(bool bypass) = 0; + ///< \param bypass Bypass regular game start. - virtual void disable (const MWWorld::Ptr& ptr) = 0; + virtual void clear() = 0; - virtual void advanceTime (double hours, bool incremental = false) = 0; - ///< Advance in-game time. + virtual int countSavedGameRecords() const = 0; + virtual int countSavedGameCells() const = 0; - virtual std::string getMonthName (int month = -1) const = 0; - ///< Return name of month (-1: current month) + virtual void write(ESM::ESMWriter& writer, Loading::Listener& listener) const = 0; - virtual MWWorld::TimeStamp getTimeStamp() const = 0; - ///< Return current in-game time and number of day since new game start. + virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; - virtual ESM::EpochTimeStamp getEpochTimeStamp() const = 0; - ///< Return current in-game date and time. + virtual void useDeathCamera() = 0; - virtual bool toggleSky() = 0; - ///< \return Resulting mode + virtual void setWaterHeight(const float height) = 0; - virtual void changeWeather(const std::string& region, const unsigned int id) = 0; + virtual bool toggleWater() = 0; + virtual bool toggleWorld() = 0; + virtual bool toggleBorders() = 0; - virtual int getCurrentWeather() const = 0; + virtual MWWorld::Player& getPlayer() = 0; + virtual MWWorld::Ptr getPlayerPtr() = 0; + virtual MWWorld::ConstPtr getPlayerConstPtr() const = 0; - virtual unsigned int getNightDayMode() const = 0; + virtual MWWorld::ESMStore& getStore() = 0; + virtual const MWWorld::ESMStore& getStore() const = 0; - virtual int getMasserPhase() const = 0; + virtual const std::vector& getESMVersions() const = 0; - virtual int getSecundaPhase() const = 0; + virtual MWWorld::LocalScripts& getLocalScripts() = 0; - virtual void setMoonColour (bool red) = 0; + virtual bool isCellExterior() const = 0; - virtual void modRegion(const std::string ®ionid, const std::vector &chances) = 0; + virtual bool isCellQuasiExterior() const = 0; - virtual float getTimeScaleFactor() const = 0; + virtual void getDoorMarkers(MWWorld::CellStore& cell, std::vector& out) = 0; + ///< get a list of teleport door markers for a given cell, to be displayed on the local map - virtual void changeToInteriorCell (const std::string& cellName, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent=true) = 0; - ///< Move to interior cell. - ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes + virtual void setGlobalInt(MWWorld::GlobalVariableName name, int value) = 0; + ///< Set value independently from real type. - virtual void changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos, bool changeEvent=true) = 0; - ///< Move to exterior cell. - ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes + virtual void setGlobalFloat(MWWorld::GlobalVariableName name, float value) = 0; + ///< Set value independently from real type. - virtual void changeToCell (const ESM::CellId& cellId, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent=true) = 0; - ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes + virtual int getGlobalInt(MWWorld::GlobalVariableName name) const = 0; + ///< Get value independently from real type. - virtual const ESM::Cell *getExterior (const std::string& cellName) const = 0; - ///< Return a cell matching the given name or a 0-pointer, if there is no such cell. + virtual float getGlobalFloat(MWWorld::GlobalVariableName name) const = 0; + ///< Get value independently from real type. - virtual void markCellAsUnchanged() = 0; + virtual char getGlobalVariableType(MWWorld::GlobalVariableName name) const = 0; + ///< Return ' ', if there is no global variable with this name. - virtual MWWorld::Ptr getFacedObject() = 0; - ///< Return pointer to the object the player is looking at, if it is within activation range + virtual std::string_view getCellName(const MWWorld::CellStore* cell = nullptr) const = 0; + ///< Return name of the cell. + /// + /// \note If cell==0, the cell the player is currently in will be used instead to + /// generate a name. + virtual std::string_view getCellName(const MWWorld::Cell& cell) const = 0; - virtual float getDistanceToFacedObject() = 0; + virtual void removeRefScript(const MWWorld::CellRef* ref) = 0; + //< Remove the script attached to ref from mLocalScripts - virtual float getMaxActivationDistance() = 0; + virtual MWWorld::Ptr getPtr(const ESM::RefId& name, bool activeOnly) = 0; + ///< Return a pointer to a liveCellRef with the given name. + /// \param activeOnly do non search inactive cells. - /// Returns a pointer to the object the provided object would hit (if within the - /// specified distance), and the point where the hit occurs. This will attempt to - /// use the "Head" node, or alternatively the "Bip01 Head" node as a basis. - virtual std::pair getHitContact(const MWWorld::ConstPtr &ptr, float distance, std::vector &targets) = 0; + virtual MWWorld::Ptr searchPtr(const ESM::RefId& name, bool activeOnly, bool searchInContainers = true) = 0; + ///< Return a pointer to a liveCellRef with the given name. + /// \param activeOnly do non search inactive cells. - virtual void adjustPosition (const MWWorld::Ptr& ptr, bool force) = 0; - ///< Adjust position after load to be on ground. Must be called after model load. - /// @param force do this even if the ptr is flying + virtual MWWorld::Ptr searchPtrViaActorId(int actorId) = 0; + ///< Search is limited to the active cells. - virtual void fixPosition () = 0; - ///< Attempt to fix position so that the player is not stuck inside the geometry. + virtual MWWorld::Ptr findContainer(const MWWorld::ConstPtr& ptr) = 0; + ///< Return a pointer to a liveCellRef which contains \a ptr. + /// \note Search is limited to the active cells. - /// @note No-op for items in containers. Use ContainerStore::removeItem instead. - virtual void deleteObject (const MWWorld::Ptr& ptr) = 0; - virtual void undeleteObject (const MWWorld::Ptr& ptr) = 0; + virtual void enable(const MWWorld::Ptr& ptr) = 0; - virtual MWWorld::Ptr moveObject (const MWWorld::Ptr& ptr, float x, float y, float z, bool movePhysics=true, bool moveToActive=false) = 0; - ///< @return an updated Ptr in case the Ptr's cell changes + virtual void disable(const MWWorld::Ptr& ptr) = 0; - virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, float x, float y, float z, bool movePhysics=true) = 0; - ///< @return an updated Ptr + virtual void advanceTime(double hours, bool incremental = false) = 0; + ///< Advance in-game time. - virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, osg::Vec3f vec, bool moveToActive, bool ignoreCollisions) = 0; - ///< @return an updated Ptr + virtual MWWorld::TimeStamp getTimeStamp() const = 0; + ///< Return current in-game time and number of day since new game start. - virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0; + virtual bool toggleSky() = 0; + ///< \return Resulting mode - virtual void rotateObject(const MWWorld::Ptr& ptr, float x, float y, float z, - RotationFlags flags = RotationFlag_inverseOrder) = 0; + virtual void changeWeather(const ESM::RefId& region, const unsigned int id) = 0; - virtual MWWorld::Ptr placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, ESM::Position pos) = 0; - ///< Place an object. Makes a copy of the Ptr. + virtual int getCurrentWeather() const = 0; - virtual MWWorld::Ptr safePlaceObject (const MWWorld::ConstPtr& ptr, const MWWorld::ConstPtr& referenceObject, MWWorld::CellStore* referenceCell, int direction, float distance) = 0; - ///< Place an object in a safe place next to \a referenceObject. \a direction and \a distance specify the wanted placement - /// relative to \a referenceObject (but the object may be placed somewhere else if the wanted location is obstructed). + virtual int getNextWeather() const = 0; - virtual void indexToPosition (int cellX, int cellY, float &x, float &y, bool centre = false) - const = 0; - ///< Convert cell numbers to position. + virtual float getWeatherTransition() const = 0; - virtual void positionToIndex (float x, float y, int &cellX, int &cellY) const = 0; - ///< Convert position to cell numbers + virtual unsigned int getNightDayMode() const = 0; - virtual void queueMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity) = 0; - ///< Queues movement for \a ptr (in local space), to be applied in the next call to - /// doPhysics. + virtual int getMasserPhase() const = 0; - virtual void updateAnimatedCollisionShape(const MWWorld::Ptr &ptr) = 0; + virtual int getSecundaPhase() const = 0; - virtual const MWPhysics::RayCastingInterface* getRayCasting() const = 0; + virtual void setMoonColour(bool red) = 0; - virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2, int mask) = 0; - ///< cast a Ray and return true if there is an object in the ray path. + virtual void modRegion(const ESM::RefId& regionid, const std::vector& chances) = 0; - virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) = 0; + virtual void changeToInteriorCell( + std::string_view cellName, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent = true) + = 0; + ///< Move to interior cell. + ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes - virtual bool castRay(const osg::Vec3f& from, const osg::Vec3f& to, int mask, const MWWorld::ConstPtr& ignore) = 0; + virtual void changeToCell( + const ESM::RefId& cellId, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent = true) + = 0; + ///< @param changeEvent If false, do not trigger cell change flag or detect worldspace changes - virtual void setActorCollisionMode(const MWWorld::Ptr& ptr, bool internal, bool external) = 0; - virtual bool isActorCollisionEnabled(const MWWorld::Ptr& ptr) = 0; + virtual MWWorld::Ptr getFacedObject() = 0; + ///< Return pointer to the object the player is looking at, if it is within activation range - virtual bool toggleCollisionMode() = 0; - ///< Toggle collision mode for player. If disabled player object should ignore - /// collisions and gravity. - /// \return Resulting mode + virtual float getDistanceToFacedObject() = 0; - virtual bool toggleRenderMode (MWRender::RenderMode mode) = 0; - ///< Toggle a render mode. - ///< \return Resulting mode + virtual float getMaxActivationDistance() const = 0; - virtual const ESM::Potion *createRecord (const ESM::Potion& record) = 0; - ///< Create a new record (of type potion) in the ESM store. - /// \return pointer to created record + virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) = 0; + ///< Adjust position after load to be on ground. Must be called after model load. + /// @param force do this even if the ptr is flying - virtual const ESM::Spell *createRecord (const ESM::Spell& record) = 0; - ///< Create a new record (of type spell) in the ESM store. - /// \return pointer to created record + virtual void fixPosition() = 0; + ///< Attempt to fix position so that the player is not stuck inside the geometry. - virtual const ESM::Class *createRecord (const ESM::Class& record) = 0; - ///< Create a new record (of type class) in the ESM store. - /// \return pointer to created record + /// @note No-op for items in containers. Use ContainerStore::removeItem instead. + virtual void deleteObject(const MWWorld::Ptr& ptr) = 0; + virtual void undeleteObject(const MWWorld::Ptr& ptr) = 0; - virtual const ESM::Cell *createRecord (const ESM::Cell& record) = 0; - ///< Create a new record (of type cell) in the ESM store. - /// \return pointer to created record + virtual MWWorld::Ptr moveObject( + const MWWorld::Ptr& ptr, const osg::Vec3f& position, bool movePhysics = true, bool moveToActive = false) + = 0; + ///< @return an updated Ptr in case the Ptr's cell changes - virtual const ESM::NPC *createRecord(const ESM::NPC &record) = 0; - ///< Create a new record (of type npc) in the ESM store. - /// \return pointer to created record + virtual MWWorld::Ptr moveObject(const MWWorld::Ptr& ptr, MWWorld::CellStore* newCell, + const osg::Vec3f& position, bool movePhysics = true, bool keepActive = false) + = 0; + ///< @return an updated Ptr - virtual const ESM::Armor *createRecord (const ESM::Armor& record) = 0; - ///< Create a new record (of type armor) in the ESM store. - /// \return pointer to created record + virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr& ptr, const osg::Vec3f& vec, bool moveToActive) = 0; + ///< @return an updated Ptr - virtual const ESM::Weapon *createRecord (const ESM::Weapon& record) = 0; - ///< Create a new record (of type weapon) in the ESM store. - /// \return pointer to created record + virtual void scaleObject(const MWWorld::Ptr& ptr, float scale, bool force = false) = 0; - virtual const ESM::Clothing *createRecord (const ESM::Clothing& record) = 0; - ///< Create a new record (of type clothing) in the ESM store. - /// \return pointer to created record + virtual void rotateObject( + const MWWorld::Ptr& ptr, const osg::Vec3f& rot, RotationFlags flags = RotationFlag_inverseOrder) + = 0; - virtual const ESM::Enchantment *createRecord (const ESM::Enchantment& record) = 0; - ///< Create a new record (of type enchantment) in the ESM store. - /// \return pointer to created record + virtual MWWorld::Ptr placeObject( + const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, const ESM::Position& pos) + = 0; + ///< Place an object. Makes a copy of the Ptr. - virtual const ESM::Book *createRecord (const ESM::Book& record) = 0; - ///< Create a new record (of type book) in the ESM store. - /// \return pointer to created record + virtual MWWorld::Ptr safePlaceObject(const MWWorld::ConstPtr& ptr, const MWWorld::ConstPtr& referenceObject, + MWWorld::CellStore* referenceCell, int direction, float distance) + = 0; + ///< Place an object in a safe place next to \a referenceObject. \a direction and \a distance specify the wanted + ///< placement + /// relative to \a referenceObject (but the object may be placed somewhere else if the wanted location is + /// obstructed). - virtual const ESM::CreatureLevList *createOverrideRecord (const ESM::CreatureLevList& record) = 0; - ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. - /// \return pointer to created record + virtual void queueMovement(const MWWorld::Ptr& ptr, const osg::Vec3f& velocity) = 0; + ///< Queues movement for \a ptr (in local space), to be applied in the next call to + /// doPhysics. - virtual const ESM::ItemLevList *createOverrideRecord (const ESM::ItemLevList& record) = 0; - ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. - /// \return pointer to created record + virtual void updateAnimatedCollisionShape(const MWWorld::Ptr& ptr) = 0; - virtual const ESM::Creature *createOverrideRecord (const ESM::Creature& record) = 0; - ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. - /// \return pointer to created record - - virtual const ESM::NPC *createOverrideRecord (const ESM::NPC& record) = 0; - ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. - /// \return pointer to created record - - virtual const ESM::Container *createOverrideRecord (const ESM::Container& record) = 0; - ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. - /// \return pointer to created record - - virtual void update (float duration, bool paused) = 0; - virtual void updatePhysics (float duration, bool paused, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) = 0; - - virtual void updateWindowManager () = 0; - - virtual MWWorld::Ptr placeObject (const MWWorld::ConstPtr& object, float cursorX, float cursorY, int amount) = 0; - ///< copy and place an object into the gameworld at the specified cursor position - /// @param object - /// @param cursor X (relative 0-1) - /// @param cursor Y (relative 0-1) - /// @param number of objects to place - - virtual MWWorld::Ptr dropObjectOnGround (const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object, int amount) = 0; - ///< copy and place an object into the gameworld at the given actor's position - /// @param actor giving the dropped object position - /// @param object - /// @param number of objects to place - - virtual bool canPlaceObject (float cursorX, float cursorY) = 0; - ///< @return true if it is possible to place on object at specified cursor location - - virtual void processChangedSettings (const std::set< std::pair >& settings) = 0; - - virtual bool isFlying(const MWWorld::Ptr &ptr) const = 0; - virtual bool isSlowFalling(const MWWorld::Ptr &ptr) const = 0; - virtual bool isSwimming(const MWWorld::ConstPtr &object) const = 0; - virtual bool isWading(const MWWorld::ConstPtr &object) const = 0; - ///Is the head of the creature underwater? - virtual bool isSubmerged(const MWWorld::ConstPtr &object) const = 0; - virtual bool isUnderwater(const MWWorld::CellStore* cell, const osg::Vec3f &pos) const = 0; - virtual bool isUnderwater(const MWWorld::ConstPtr &object, const float heightRatio) const = 0; - virtual bool isWaterWalkingCastableOnTarget(const MWWorld::ConstPtr &target) const = 0; - virtual bool isOnGround(const MWWorld::Ptr &ptr) const = 0; - - virtual osg::Matrixf getActorHeadTransform(const MWWorld::ConstPtr& actor) const = 0; - - virtual void togglePOV(bool force = false) = 0; - virtual bool isFirstPerson() const = 0; - virtual bool isPreviewModeEnabled() const = 0; - virtual void togglePreviewMode(bool enable) = 0; - virtual bool toggleVanityMode(bool enable) = 0; - virtual void allowVanityMode(bool allow) = 0; - virtual bool vanityRotateCamera(float * rot) = 0; - virtual void adjustCameraDistance(float dist) = 0; - virtual void applyDeferredPreviewRotationToPlayer(float dt) = 0; - virtual void disableDeferredPreviewRotation() = 0; - - virtual void saveLoaded() = 0; - - virtual void setupPlayer() = 0; - virtual void renderPlayer() = 0; - - /// open or close a non-teleport door (depending on current state) - virtual void activateDoor(const MWWorld::Ptr& door) = 0; - /// update movement state of a non-teleport door as specified - /// @param state see MWClass::setDoorState - /// @note throws an exception when invoked on a teleport door - virtual void activateDoor(const MWWorld::Ptr& door, MWWorld::DoorState state) = 0; + virtual const MWPhysics::RayCastingInterface* getRayCasting() const = 0; - virtual void getActorsStandingOn (const MWWorld::ConstPtr& object, std::vector &actors) = 0; ///< get a list of actors standing on \a object - virtual bool getPlayerStandingOn (const MWWorld::ConstPtr& object) = 0; ///< @return true if the player is standing on \a object - virtual bool getActorStandingOn (const MWWorld::ConstPtr& object) = 0; ///< @return true if any actor is standing on \a object - virtual bool getPlayerCollidingWith(const MWWorld::ConstPtr& object) = 0; ///< @return true if the player is colliding with \a object - virtual bool getActorCollidingWith (const MWWorld::ConstPtr& object) = 0; ///< @return true if any actor is colliding with \a object - virtual void hurtStandingActors (const MWWorld::ConstPtr& object, float dmgPerSecond) = 0; - ///< Apply a health difference to any actors standing on \a object. - /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. - virtual void hurtCollidingActors (const MWWorld::ConstPtr& object, float dmgPerSecond) = 0; - ///< Apply a health difference to any actors colliding with \a object. - /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. - - virtual float getWindSpeed() = 0; - - virtual void getContainersOwnedBy (const MWWorld::ConstPtr& npc, std::vector& out) = 0; - ///< get all containers in active cells owned by this Npc - virtual void getItemsOwnedBy (const MWWorld::ConstPtr& npc, std::vector& out) = 0; - ///< get all items in active cells owned by this Npc - - virtual bool getLOS(const MWWorld::ConstPtr& actor,const MWWorld::ConstPtr& targetActor) = 0; - ///< get Line of Sight (morrowind stupid implementation) - - virtual float getDistToNearestRayHit(const osg::Vec3f& from, const osg::Vec3f& dir, float maxDist, bool includeWater = false) = 0; - - virtual void enableActorCollision(const MWWorld::Ptr& actor, bool enable) = 0; - - enum RestPermitted - { - Rest_Allowed = 0, - Rest_OnlyWaiting = 1, - Rest_PlayerIsInAir = 2, - Rest_PlayerIsUnderwater = 3, - Rest_EnemiesAreNearby = 4 - }; - - /// check if the player is allowed to rest - virtual RestPermitted canRest() const = 0; - - /// \todo Probably shouldn't be here - virtual MWRender::Animation* getAnimation(const MWWorld::Ptr &ptr) = 0; - virtual const MWRender::Animation* getAnimation(const MWWorld::ConstPtr &ptr) const = 0; - virtual void reattachPlayerCamera() = 0; + virtual bool castRenderingRay(MWPhysics::RayCastingResult& res, const osg::Vec3f& from, const osg::Vec3f& to, + bool ignorePlayer, bool ignoreActors, std::span ignoreList = {}) + = 0; - /// \todo this does not belong here - virtual void screenshot (osg::Image* image, int w, int h) = 0; - virtual bool screenshot360 (osg::Image* image) = 0; - - /// Find default position inside exterior cell specified by name - /// \return false if exterior with given name not exists, true otherwise - virtual bool findExteriorPosition(const std::string &name, ESM::Position &pos) = 0; + virtual void setActorCollisionMode(const MWWorld::Ptr& ptr, bool internal, bool external) = 0; + virtual bool isActorCollisionEnabled(const MWWorld::Ptr& ptr) = 0; - /// Find default position inside interior cell specified by name - /// \return false if interior with given name not exists, true otherwise - virtual bool findInteriorPosition(const std::string &name, ESM::Position &pos) = 0; + virtual bool toggleCollisionMode() = 0; + ///< Toggle collision mode for player. If disabled player object should ignore + /// collisions and gravity. + /// \return Resulting mode + + virtual bool toggleRenderMode(MWRender::RenderMode mode) = 0; + ///< Toggle a render mode. + ///< \return Resulting mode + + virtual MWWorld::Ptr placeObject( + const MWWorld::Ptr& object, float cursorX, float cursorY, int amount, bool copy = true) + = 0; + ///< copy and place an object into the gameworld at the specified cursor position + /// @param object + /// @param cursor X (relative 0-1) + /// @param cursor Y (relative 0-1) + /// @param number of objects to place + + virtual MWWorld::Ptr dropObjectOnGround( + const MWWorld::Ptr& actor, const MWWorld::Ptr& object, int amount, bool copy = true) + = 0; + ///< copy and place an object into the gameworld at the given actor's position + /// @param actor giving the dropped object position + /// @param object + /// @param number of objects to place + + virtual bool canPlaceObject(float cursorX, float cursorY) = 0; + ///< @return true if it is possible to place on object at specified cursor location + + virtual void processChangedSettings(const std::set>& settings) = 0; + + virtual bool isFlying(const MWWorld::Ptr& ptr) const = 0; + virtual bool isSlowFalling(const MWWorld::Ptr& ptr) const = 0; + virtual bool isSwimming(const MWWorld::ConstPtr& object) const = 0; + virtual bool isWading(const MWWorld::ConstPtr& object) const = 0; + /// Is the head of the creature underwater? + virtual bool isSubmerged(const MWWorld::ConstPtr& object) const = 0; + virtual bool isUnderwater(const MWWorld::CellStore* cell, const osg::Vec3f& pos) const = 0; + virtual bool isUnderwater(const MWWorld::ConstPtr& object, const float heightRatio) const = 0; + virtual bool isWaterWalkingCastableOnTarget(const MWWorld::ConstPtr& target) const = 0; + virtual bool isOnGround(const MWWorld::Ptr& ptr) const = 0; + + virtual osg::Matrixf getActorHeadTransform(const MWWorld::ConstPtr& actor) const = 0; + + virtual MWRender::Camera* getCamera() = 0; + virtual void togglePOV(bool force = false) = 0; + virtual bool isFirstPerson() const = 0; + virtual bool isPreviewModeEnabled() const = 0; + virtual bool toggleVanityMode(bool enable) = 0; + virtual bool vanityRotateCamera(const float* rot) = 0; + virtual void applyDeferredPreviewRotationToPlayer(float dt) = 0; + virtual void disableDeferredPreviewRotation() = 0; + + virtual void saveLoaded() = 0; + + virtual void setupPlayer() = 0; + virtual void renderPlayer() = 0; + + /// open or close a non-teleport door (depending on current state) + virtual void activateDoor(const MWWorld::Ptr& door) = 0; + /// update movement state of a non-teleport door as specified + /// @param state see MWClass::setDoorState + /// @note throws an exception when invoked on a teleport door + virtual void activateDoor(const MWWorld::Ptr& door, MWWorld::DoorState state) = 0; + + virtual void getActorsStandingOn(const MWWorld::ConstPtr& object, std::vector& actors) + = 0; ///< get a list of actors standing on \a object + virtual bool getPlayerStandingOn(const MWWorld::ConstPtr& object) + = 0; ///< @return true if the player is standing on \a object + virtual bool getActorStandingOn(const MWWorld::ConstPtr& object) + = 0; ///< @return true if any actor is standing on \a object + virtual bool getPlayerCollidingWith(const MWWorld::ConstPtr& object) + = 0; ///< @return true if the player is colliding with \a object + virtual bool getActorCollidingWith(const MWWorld::ConstPtr& object) + = 0; ///< @return true if any actor is colliding with \a object + virtual void hurtStandingActors(const MWWorld::ConstPtr& object, float dmgPerSecond) = 0; + ///< Apply a health difference to any actors standing on \a object. + /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. + virtual void hurtCollidingActors(const MWWorld::ConstPtr& object, float dmgPerSecond) = 0; + ///< Apply a health difference to any actors colliding with \a object. + /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. + + virtual float getWindSpeed() = 0; + + virtual void getContainersOwnedBy(const MWWorld::ConstPtr& npc, std::vector& out) = 0; + ///< get all containers in active cells owned by this Npc + virtual void getItemsOwnedBy(const MWWorld::ConstPtr& npc, std::vector& out) = 0; + ///< get all items in active cells owned by this Npc + + virtual bool getLOS(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& targetActor) = 0; + ///< get Line of Sight (morrowind stupid implementation) + + virtual float getDistToNearestRayHit( + const osg::Vec3f& from, const osg::Vec3f& dir, float maxDist, bool includeWater = false) + = 0; + + virtual void enableActorCollision(const MWWorld::Ptr& actor, bool enable) = 0; + + enum RestPermitted + { + Rest_Allowed = 0, + Rest_OnlyWaiting = 1, + Rest_PlayerIsInAir = 2, + Rest_PlayerIsUnderwater = 3, + Rest_EnemiesAreNearby = 4 + }; + + /// check if the player is allowed to rest + virtual RestPermitted canRest() const = 0; + + /// \todo Probably shouldn't be here + virtual MWRender::Animation* getAnimation(const MWWorld::Ptr& ptr) = 0; + virtual const MWRender::Animation* getAnimation(const MWWorld::ConstPtr& ptr) const = 0; + virtual void reattachPlayerCamera() = 0; + + /// \todo this does not belong here + virtual void screenshot(osg::Image* image, int w, int h) = 0; + + /// Find default position inside exterior cell specified by name + /// \return empty RefId if exterior with given name not exists, the cell's RefId otherwise + virtual ESM::RefId findExteriorPosition(std::string_view name, ESM::Position& pos) = 0; + + /// Find default position inside interior cell specified by name + /// \return empty RefId if interior with given name not exists, the cell's RefId otherwise + virtual ESM::RefId findInteriorPosition(std::string_view name, ESM::Position& pos) = 0; + + /// Enables or disables use of teleport spell effects (recall, intervention, etc). + virtual void enableTeleporting(bool enable) = 0; + + /// Returns true if teleport spell effects are allowed. + virtual bool isTeleportingEnabled() const = 0; + + /// Enables or disables use of levitation spell effect. + virtual void enableLevitation(bool enable) = 0; - /// Enables or disables use of teleport spell effects (recall, intervention, etc). - virtual void enableTeleporting(bool enable) = 0; + /// Returns true if levitation spell effect is allowed. + virtual bool isLevitationEnabled() const = 0; - /// Returns true if teleport spell effects are allowed. - virtual bool isTeleportingEnabled() const = 0; + virtual bool getGodModeState() const = 0; - /// Enables or disables use of levitation spell effect. - virtual void enableLevitation(bool enable) = 0; + virtual bool toggleGodMode() = 0; - /// Returns true if levitation spell effect is allowed. - virtual bool isLevitationEnabled() const = 0; + virtual bool toggleScripts() = 0; + virtual bool getScriptsEnabled() const = 0; - virtual bool getGodModeState() const = 0; + /** + * @brief startSpellCast attempt to start casting a spell. Might fail immediately if conditions are not met. + * @param actor + * @return Success or the failure condition. + */ + virtual MWWorld::SpellCastState startSpellCast(const MWWorld::Ptr& actor) = 0; - virtual bool toggleGodMode() = 0; + virtual void castSpell(const MWWorld::Ptr& actor, bool scriptedSpell = false) = 0; - virtual bool toggleScripts() = 0; - virtual bool getScriptsEnabled() const = 0; + virtual void launchMagicBolt(const ESM::RefId& spellId, const MWWorld::Ptr& caster, + const osg::Vec3f& fallbackDirection, ESM::RefNum item) + = 0; + virtual void launchProjectile(MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, + const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) + = 0; + virtual void updateProjectilesCasters() = 0; - /** - * @brief startSpellCast attempt to start casting a spell. Might fail immediately if conditions are not met. - * @param actor - * @return true if the spell can be casted (i.e. the animation should start) - */ - virtual bool startSpellCast (const MWWorld::Ptr& actor) = 0; + virtual void applyLoopingParticles(const MWWorld::Ptr& ptr) const = 0; - virtual void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) = 0; + virtual const std::vector& getContentFiles() const = 0; - virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) = 0; - virtual void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, - const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) = 0; - virtual void updateProjectilesCasters() = 0; + virtual void breakInvisibility(const MWWorld::Ptr& actor) = 0; - virtual void applyLoopingParticles(const MWWorld::Ptr& ptr) = 0; + // Allow NPCs to use torches? + virtual bool useTorches() const = 0; - virtual const std::vector& getContentFiles() const = 0; + virtual float getSunVisibility() const = 0; + virtual float getSunPercentage() const = 0; - virtual void breakInvisibility (const MWWorld::Ptr& actor) = 0; + virtual bool findInteriorPositionInWorldSpace(const MWWorld::CellStore* cell, osg::Vec3f& result) = 0; - // Allow NPCs to use torches? - virtual bool useTorches() const = 0; + /// Teleports \a ptr to the closest reference of \a id (e.g. DivineMarker, PrisonMarker, TempleMarker) + /// @note id must be lower case + virtual void teleportToClosestMarker(const MWWorld::Ptr& ptr, const ESM::RefId& id) = 0; - virtual bool findInteriorPositionInWorldSpace(const MWWorld::CellStore* cell, osg::Vec3f& result) = 0; + enum DetectionType + { + Detect_Enchantment, + Detect_Key, + Detect_Creature + }; + /// List all references (filtered by \a type) detected by \a ptr. The range + /// is determined by the current magnitude of the "Detect X" magic effect belonging to \a type. + /// @note This also works for references in containers. + virtual void listDetectedReferences(const MWWorld::Ptr& ptr, std::vector& out, DetectionType type) + = 0; - /// Teleports \a ptr to the closest reference of \a id (e.g. DivineMarker, PrisonMarker, TempleMarker) - /// @note id must be lower case - virtual void teleportToClosestMarker (const MWWorld::Ptr& ptr, - const std::string& id) = 0; + /// Update the value of some globals according to the world state, which may be used by dialogue entries. + /// This should be called when initiating a dialogue. + virtual void updateDialogueGlobals() = 0; - enum DetectionType - { - Detect_Enchantment, - Detect_Key, - Detect_Creature - }; - /// List all references (filtered by \a type) detected by \a ptr. The range - /// is determined by the current magnitude of the "Detect X" magic effect belonging to \a type. - /// @note This also works for references in containers. - virtual void listDetectedReferences (const MWWorld::Ptr& ptr, std::vector& out, - DetectionType type) = 0; + /// Moves all stolen items from \a ptr to the closest evidence chest. + virtual void confiscateStolenItems(const MWWorld::Ptr& ptr) = 0; - /// Update the value of some globals according to the world state, which may be used by dialogue entries. - /// This should be called when initiating a dialogue. - virtual void updateDialogueGlobals() = 0; + virtual void goToJail() = 0; - /// Moves all stolen items from \a ptr to the closest evidence chest. - virtual void confiscateStolenItems(const MWWorld::Ptr& ptr) = 0; + /// Spawn a random creature from a levelled list next to the player + virtual void spawnRandomCreature(const ESM::RefId& creatureList) = 0; - virtual void goToJail () = 0; + /// Spawn a blood effect for \a ptr at \a worldPosition + virtual void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; - /// Spawn a random creature from a levelled list next to the player - virtual void spawnRandomCreature(const std::string& creatureList) = 0; + virtual void spawnEffect(const std::string& model, const std::string& textureOverride, + const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) + = 0; - /// Spawn a blood effect for \a ptr at \a worldPosition - virtual void spawnBloodEffect (const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; + /// @see MWWorld::WeatherManager::isInStorm + virtual bool isInStorm() const = 0; - virtual void spawnEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) = 0; + /// @see MWWorld::WeatherManager::getStormDirection + virtual osg::Vec3f getStormDirection() const = 0; - virtual void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, - const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, - const std::string& sourceName, const bool fromProjectile=false) = 0; + /// Resets all actors in the current active cells to their original location within that cell. + virtual void resetActors() = 0; - virtual void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; + virtual bool isWalkingOnWater(const MWWorld::ConstPtr& actor) const = 0; - /// @see MWWorld::WeatherManager::isInStorm - virtual bool isInStorm() const = 0; + /// Return a vector aiming the actor's weapon towards a target. + /// @note The length of the vector is the distance between actor and target. + virtual osg::Vec3f aimToTarget( + const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target, bool isRangedCombat) + = 0; - /// @see MWWorld::WeatherManager::getStormDirection - virtual osg::Vec3f getStormDirection() const = 0; + virtual void addContainerScripts(const MWWorld::Ptr& reference, MWWorld::CellStore* cell) = 0; + virtual void removeContainerScripts(const MWWorld::Ptr& reference) = 0; - /// Resets all actors in the current active cells to their original location within that cell. - virtual void resetActors() = 0; + virtual bool isPlayerInJail() const = 0; - virtual bool isWalkingOnWater (const MWWorld::ConstPtr& actor) const = 0; + virtual void rest(double hours) = 0; - /// Return a vector aiming the actor's weapon towards a target. - /// @note The length of the vector is the distance between actor and target. - virtual osg::Vec3f aimToTarget(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target, bool isRangedCombat) = 0; + virtual void setPlayerTraveling(bool traveling) = 0; + virtual bool isPlayerTraveling() const = 0; - /// Return the distance between actor's weapon and target's collision box. - virtual float getHitDistance(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) = 0; + virtual void rotateWorldObject(const MWWorld::Ptr& ptr, const osg::Quat& rotate) = 0; - virtual void addContainerScripts(const MWWorld::Ptr& reference, MWWorld::CellStore* cell) = 0; - virtual void removeContainerScripts(const MWWorld::Ptr& reference) = 0; + /// Return terrain height at \a worldPos position. + virtual float getTerrainHeightAt(const osg::Vec3f& worldPos, ESM::RefId worldspace) const = 0; - virtual bool isPlayerInJail() const = 0; + /// Return physical or rendering half extents of the given actor. + virtual osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& actor, bool rendering = false) const = 0; - virtual void rest(double hours) = 0; - virtual void rechargeItems(double duration, bool activeOnly) = 0; + /// Export scene graph to a file and return the filename. + /// \param ptr object to export scene graph for (if empty, export entire scene graph) + virtual std::filesystem::path exportSceneGraph(const MWWorld::Ptr& ptr) = 0; - virtual void setPlayerTraveling(bool traveling) = 0; - virtual bool isPlayerTraveling() const = 0; + /// Preload VFX associated with this effect list + virtual void preloadEffects(const ESM::EffectList* effectList) = 0; - virtual void rotateWorldObject (const MWWorld::Ptr& ptr, osg::Quat rotate) = 0; + virtual DetourNavigator::Navigator* getNavigator() const = 0; - /// Return terrain height at \a worldPos position. - virtual float getTerrainHeightAt(const osg::Vec3f& worldPos) const = 0; + virtual void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end) const = 0; - /// Return physical or rendering half extents of the given actor. - virtual osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& actor, bool rendering=false) const = 0; + virtual void removeActorPath(const MWWorld::ConstPtr& actor) const = 0; - /// Export scene graph to a file and return the filename. - /// \param ptr object to export scene graph for (if empty, export entire scene graph) - virtual std::string exportSceneGraph(const MWWorld::Ptr& ptr) = 0; + virtual void setNavMeshNumberToRender(const std::size_t value) = 0; - /// Preload VFX associated with this effect list - virtual void preloadEffects(const ESM::EffectList* effectList) = 0; + virtual DetourNavigator::AgentBounds getPathfindingAgentBounds(const MWWorld::ConstPtr& actor) const = 0; - virtual DetourNavigator::Navigator* getNavigator() const = 0; + virtual bool hasCollisionWithDoor( + const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; - virtual void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const = 0; + virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + std::span ignore, std::vector* occupyingActors = nullptr) const = 0; - virtual void removeActorPath(const MWWorld::ConstPtr& actor) const = 0; + virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; - virtual void setNavMeshNumberToRender(const std::size_t value) = 0; + virtual std::vector getAll(const ESM::RefId& id) = 0; - /// Return physical half extents of the given actor to be used in pathfinding - virtual osg::Vec3f getPathfindingHalfExtents(const MWWorld::ConstPtr& actor) const = 0; + virtual Misc::Rng::Generator& getPrng() = 0; - virtual bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; + virtual MWRender::RenderingManager* getRenderingManager() = 0; - virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const = 0; + virtual MWRender::PostProcessor* getPostProcessor() = 0; - virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; + virtual MWWorld::DateTimeManager* getTimeManager() = 0; - virtual std::vector getAll(const std::string& id) = 0; + virtual void setActorActive(const MWWorld::Ptr& ptr, bool value) = 0; }; } diff --git a/apps/openmw/mwclass/activator.cpp b/apps/openmw/mwclass/activator.cpp index c54b1c3691b..e0ee315bc1a 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -1,6 +1,12 @@ #include "activator.hpp" -#include +#include +#include + +#include +#include +#include +#include #include #include @@ -8,12 +14,12 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwworld/action.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwworld/ptr.hpp" -#include "../mwworld/action.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/nullaction.hpp" +#include "../mwworld/ptr.hpp" #include "../mwphysics/physicssystem.hpp" @@ -25,34 +31,40 @@ #include "../mwmechanics/npcstats.hpp" +#include "classmodel.hpp" namespace MWClass { + Activator::Activator() + : MWWorld::RegisteredClass(ESM::Activator::sRecordId) + { + } - void Activator::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + void Activator::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model, true); + renderingInterface.getObjects().insertModel(ptr, model); ptr.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Static); } } - void Activator::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Activator::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const { - if(!model.empty()) - physics.addObject(ptr, model); + insertObjectPhysics(ptr, model, rotation, physics); } - std::string Activator::getModel(const MWWorld::ConstPtr &ptr) const + void Activator::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); + } - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + std::string_view Activator::getModel(const MWWorld::ConstPtr& ptr) const + { + return getClassModel(ptr); } bool Activator::isActivator() const @@ -65,89 +77,78 @@ namespace MWClass return true; } - std::string Activator::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Activator::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mName; } - std::string Activator::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Activator::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = - ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - void Activator::registerSelf() - { - std::shared_ptr instance (new Activator); - - registerClass (typeid (ESM::Activator).name(), instance); - } - - bool Activator::hasToolTip (const MWWorld::ConstPtr& ptr) const + bool Activator::hasToolTip(const MWWorld::ConstPtr& ptr) const { return !getName(ptr).empty(); } - bool Activator::allowTelekinesis(const MWWorld::ConstPtr &ptr) const { - return false; - } - - MWGui::ToolTipInfo Activator::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Activator::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); - std::string text; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; return info; } - std::shared_ptr Activator::activate(const MWWorld::Ptr &ptr, const MWWorld::Ptr &actor) const + std::unique_ptr Activator::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { - if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfActivator"); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound* sound = store.get().searchRandom("WolfActivator", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); - if(sound) action->setSound(sound->mId); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); + if (sound) + action->setSound(sound->mId); return action; } - return std::shared_ptr(new MWWorld::NullAction); + return std::make_unique(); } - - MWWorld::Ptr Activator::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Activator::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - std::string Activator::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const + ESM::RefId Activator::getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const { - const std::string model = getModel(ptr); // Assume it's not empty, since we wouldn't have gotten the soundgen otherwise - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - std::string creatureId; + // Assume it's not empty, since we wouldn't have gotten the soundgen otherwise + const std::string_view model = getModel(ptr); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const ESM::RefId* creatureId = nullptr; - for (const ESM::Creature &iter : store.get()) + for (const ESM::Creature& iter : store.get()) { - if (!iter.mModel.empty() && Misc::StringUtils::ciEqual(model, "meshes\\" + iter.mModel)) + if (!iter.mModel.empty() && Misc::StringUtils::ciEqual(model, iter.mModel)) { - creatureId = !iter.mOriginal.empty() ? iter.mOriginal : iter.mId; + creatureId = !iter.mOriginal.empty() ? &iter.mOriginal : &iter.mId; break; } } @@ -155,37 +156,40 @@ namespace MWClass int type = getSndGenTypeFromName(name); std::vector fallbacksounds; - if (!creatureId.empty()) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (creatureId && !creatureId->empty()) { std::vector sounds; - for (auto sound = store.get().begin(); sound != store.get().end(); ++sound) + for (auto sound = store.get().begin(); sound != store.get().end(); + ++sound) { - if (type == sound->mType && !sound->mCreature.empty() && (Misc::StringUtils::ciEqual(creatureId, sound->mCreature))) + if (type == sound->mType && !sound->mCreature.empty() && (*creatureId == sound->mCreature)) sounds.push_back(&*sound); if (type == sound->mType && sound->mCreature.empty()) fallbacksounds.push_back(&*sound); } if (!sounds.empty()) - return sounds[Misc::Rng::rollDice(sounds.size())]->mSound; + return sounds[Misc::Rng::rollDice(sounds.size(), prng)]->mSound; if (!fallbacksounds.empty()) - return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound; + return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size(), prng)]->mSound; } else { // The activator doesn't have a corresponding creature ID, but we can try to use the defaults - for (auto sound = store.get().begin(); sound != store.get().end(); ++sound) + for (auto sound = store.get().begin(); sound != store.get().end(); + ++sound) if (type == sound->mType && sound->mCreature.empty()) fallbacksounds.push_back(&*sound); if (!fallbacksounds.empty()) - return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound; + return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size(), prng)]->mSound; } - return std::string(); + return ESM::RefId(); } - int Activator::getSndGenTypeFromName(const std::string &name) + int Activator::getSndGenTypeFromName(std::string_view name) { if (name == "left") return ESM::SoundGenerator::LeftFoot; @@ -204,6 +208,6 @@ namespace MWClass if (name == "land") return ESM::SoundGenerator::Land; - throw std::runtime_error(std::string("Unexpected soundgen type: ")+name); + throw std::runtime_error("Unexpected soundgen type: " + std::string(name)); } } diff --git a/apps/openmw/mwclass/activator.hpp b/apps/openmw/mwclass/activator.hpp index 10ace6f74d9..ec0f1bf2821 100644 --- a/apps/openmw/mwclass/activator.hpp +++ b/apps/openmw/mwclass/activator.hpp @@ -1,52 +1,54 @@ #ifndef GAME_MWCLASS_ACTIVATOR_H #define GAME_MWCLASS_ACTIVATOR_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Activator : public MWWorld::Class + class Activator final : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + Activator(); - static int getSndGenTypeFromName(const std::string &name); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - public: + static int getSndGenTypeFromName(std::string_view name); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override; - bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; - ///< @return true if this object has a tooltip when focused (default implementation: true) + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const override; - ///< Return whether this class of object can be activated with telekinesis + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; + ///< @return true if this object has a tooltip when focused (default implementation: true) - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - static void registerSelf(); + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + bool useAnim() const override; + ///< Whether or not to use animated variant of model (default false) - bool useAnim() const override; - ///< Whether or not to use animated variant of model (default false) + bool isActivator() const override; - bool isActivator() const override; - - std::string getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const override; + ESM::RefId getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const override; }; } diff --git a/apps/openmw/mwclass/actor.cpp b/apps/openmw/mwclass/actor.cpp index 33aeb26bb0c..0a45a85a742 100644 --- a/apps/openmw/mwclass/actor.cpp +++ b/apps/openmw/mwclass/actor.cpp @@ -1,39 +1,35 @@ #include "actor.hpp" -#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" -#include "../mwmechanics/movement.hpp" #include "../mwmechanics/magiceffects.hpp" +#include "../mwmechanics/movement.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/worldmodel.hpp" namespace MWClass { - Actor::Actor() {} - - Actor::~Actor() {} - void Actor::adjustPosition(const MWWorld::Ptr& ptr, bool force) const { MWBase::Environment::get().getWorld()->adjustPosition(ptr, force); } - void Actor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Actor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const { - if (!model.empty()) - { - physics.addActor(ptr, model); - if (getCreatureStats(ptr).isDead() && getCreatureStats(ptr).isDeathAnimationFinished()) - MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); - } + physics.addActor(ptr, model); + if (getCreatureStats(ptr).isDead() && getCreatureStats(ptr).isDeathAnimationFinished()) + MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); } bool Actor::useAnim() const @@ -41,33 +37,9 @@ namespace MWClass return true; } - void Actor::block(const MWWorld::Ptr &ptr) const - { - const MWWorld::InventoryStore& inv = getInventoryStore(ptr); - MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield == inv.end()) - return; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - switch (shield->getClass().getEquipmentSkill(*shield)) - { - case ESM::Skill::LightArmor: - sndMgr->playSound3D(ptr, "Light Armor Hit", 1.0f, 1.0f); - break; - case ESM::Skill::MediumArmor: - sndMgr->playSound3D(ptr, "Medium Armor Hit", 1.0f, 1.0f); - break; - case ESM::Skill::HeavyArmor: - sndMgr->playSound3D(ptr, "Heavy Armor Hit", 1.0f, 1.0f); - break; - default: - return; - } - } - osg::Vec3f Actor::getRotationVector(const MWWorld::Ptr& ptr) const { - MWMechanics::Movement &movement = getMovementSettings(ptr); + MWMechanics::Movement& movement = getMovementSettings(ptr); osg::Vec3f vec(movement.mRotation[0], movement.mRotation[1], movement.mRotation[2]); movement.mRotation[0] = 0.0f; movement.mRotation[1] = 0.0f; @@ -79,13 +51,14 @@ namespace MWClass { float weight = getContainerStore(ptr).getWeight(); const MWMechanics::MagicEffects& effects = getCreatureStats(ptr).getMagicEffects(); - weight -= effects.get(MWMechanics::EffectKey(ESM::MagicEffect::Feather)).getMagnitude(); + weight -= effects.getOrDefault(MWMechanics::EffectKey(ESM::MagicEffect::Feather)).getMagnitude(); if (ptr != MWMechanics::getPlayer() || !MWBase::Environment::get().getWorld()->getGodModeState()) - weight += effects.get(MWMechanics::EffectKey(ESM::MagicEffect::Burden)).getMagnitude(); + weight += effects.getOrDefault(MWMechanics::EffectKey(ESM::MagicEffect::Burden)).getMagnitude(); return (weight < 0) ? 0.0f : weight; } - bool Actor::allowTelekinesis(const MWWorld::ConstPtr &ptr) const { + bool Actor::allowTelekinesis(const MWWorld::ConstPtr& ptr) const + { return false; } @@ -102,4 +75,19 @@ namespace MWClass moveSpeed *= 0.75f; return moveSpeed; } + + bool Actor::consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const + { + MWMechanics::CastSpell cast(actor, actor); + const ESM::RefId& recordId = consumable.getCellRef().getRefId(); + MWBase::Environment::get().getWorldModel()->registerPtr(consumable); + MWBase::Environment::get().getLuaManager()->itemConsumed(consumable, actor); + actor.getClass().getContainerStore(actor).remove(consumable, 1); + if (cast.cast(recordId)) + { + MWBase::Environment::get().getWorld()->breakInvisibility(actor); + return true; + } + return false; + } } diff --git a/apps/openmw/mwclass/actor.hpp b/apps/openmw/mwclass/actor.hpp index 3d509b2768d..cf0cb1eaa58 100644 --- a/apps/openmw/mwclass/actor.hpp +++ b/apps/openmw/mwclass/actor.hpp @@ -3,6 +3,11 @@ #include "../mwworld/class.hpp" +#include "../mwmechanics/magiceffects.hpp" + +#include +#include + namespace ESM { struct GameSetting; @@ -14,22 +19,32 @@ namespace MWClass class Actor : public MWWorld::Class { protected: + explicit Actor(unsigned type) + : Class(type) + { + } - Actor(); + template + float getSwimSpeedImpl(const MWWorld::Ptr& ptr, const GMST& gmst, const MWMechanics::MagicEffects& mageffects, + float baseSpeed) const + { + return baseSpeed * (1.0f + 0.01f * mageffects.getOrDefault(ESM::MagicEffect::SwiftSwim).getMagnitude()) + * (gmst.fSwimRunBase->mValue.getFloat() + + 0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->mValue.getFloat()); + } public: - virtual ~Actor(); + ~Actor() override = default; void adjustPosition(const MWWorld::Ptr& ptr, bool force) const override; ///< Adjust position to stand on ground. Must be called post model load /// @param force do this even if the ptr is flying - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override; bool useAnim() const override; - void block(const MWWorld::Ptr &ptr) const override; - osg::Vec3f getRotationVector(const MWWorld::Ptr& ptr) const override; ///< Return desired rotations, as euler angles. Sets getMovementSettings(ptr).mRotation to zero. @@ -44,10 +59,12 @@ namespace MWClass /// Return current movement speed. float getCurrentSpeed(const MWWorld::Ptr& ptr) const override; - + + bool consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const override; + // not implemented - Actor(const Actor&); - Actor& operator= (const Actor&); + Actor(const Actor&) = delete; + Actor& operator=(const Actor&) = delete; }; } diff --git a/apps/openmw/mwclass/apparatus.cpp b/apps/openmw/mwclass/apparatus.cpp index 518695fabfd..90112648b5f 100644 --- a/apps/openmw/mwclass/apparatus.cpp +++ b/apps/openmw/mwclass/apparatus.cpp @@ -1,105 +1,96 @@ #include "apparatus.hpp" -#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include +#include -#include "../mwworld/ptr.hpp" #include "../mwworld/actionalchemy.hpp" #include "../mwworld/cellstore.hpp" -#include "../mwphysics/physicssystem.hpp" -#include "../mwworld/nullaction.hpp" +#include "../mwworld/ptr.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { + Apparatus::Apparatus() + : MWWorld::RegisteredClass(ESM::Apparatus::sRecordId) + { + } - void Apparatus::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + void Apparatus::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - if (!model.empty()) { + if (!model.empty()) + { renderingInterface.getObjects().insertModel(ptr, model); } } - void Apparatus::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + std::string_view Apparatus::getModel(const MWWorld::ConstPtr& ptr) const { - // TODO: add option somewhere to enable collision for placeable objects + return getClassModel(ptr); } - std::string Apparatus::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Apparatus::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getNameOrId(ptr); } - std::string Apparatus::getName (const MWWorld::ConstPtr& ptr) const - { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; - } - - std::shared_ptr Apparatus::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Apparatus::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } - std::string Apparatus::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Apparatus::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - int Apparatus::getValue (const MWWorld::ConstPtr& ptr) const + int Apparatus::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } - void Apparatus::registerSelf() - { - std::shared_ptr instance (new Apparatus); - - registerClass (typeid (ESM::Apparatus).name(), instance); - } - - std::string Apparatus::getUpSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Apparatus::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Apparatus Up"); + static const auto sound = ESM::RefId::stringRefId("Item Apparatus Up"); + return sound; } - std::string Apparatus::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Apparatus::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Apparatus Down"); + static const auto sound = ESM::RefId::stringRefId("Item Apparatus Down"); + return sound; } - std::string Apparatus::getInventoryIcon (const MWWorld::ConstPtr& ptr) const + const std::string& Apparatus::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - MWGui::ToolTipInfo Apparatus::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Apparatus::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -107,35 +98,36 @@ namespace MWClass text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } - std::shared_ptr Apparatus::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Apparatus::use(const MWWorld::Ptr& ptr, bool force) const { - return std::shared_ptr(new MWWorld::ActionAlchemy(force)); + return std::make_unique(force); } - MWWorld::Ptr Apparatus::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Apparatus::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - bool Apparatus::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Apparatus::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Apparatus) != 0; } - float Apparatus::getWeight(const MWWorld::ConstPtr &ptr) const + float Apparatus::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } diff --git a/apps/openmw/mwclass/apparatus.hpp b/apps/openmw/mwclass/apparatus.hpp index 8087c57ba33..c6bd45858ab 100644 --- a/apps/openmw/mwclass/apparatus.hpp +++ b/apps/openmw/mwclass/apparatus.hpp @@ -1,57 +1,57 @@ #ifndef GAME_MWCLASS_APPARATUS_H #define GAME_MWCLASS_APPARATUS_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Apparatus : public MWWorld::Class + class Apparatus : public MWWorld::RegisteredClass { + friend MWWorld::RegisteredClass; - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + Apparatus(); - public: + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - float getWeight (const MWWorld::ConstPtr& ptr) const override; + public: + float getWeight(const MWWorld::ConstPtr& ptr) const override; - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + bool isItem(const MWWorld::ConstPtr&) const override { return true; } - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - static void registerSelf(); + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - std::string getModel(const MWWorld::ConstPtr &ptr) const override; - - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } diff --git a/apps/openmw/mwclass/armor.cpp b/apps/openmw/mwclass/armor.cpp index 3f9bfb859ff..8bf9071f0c1 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -1,99 +1,93 @@ #include "armor.hpp" -#include -#include -#include +#include +#include + +#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwworld/ptr.hpp" #include "../mwworld/actionequip.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwphysics/physicssystem.hpp" -#include "../mwworld/nullaction.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/ptr.hpp" -#include "../mwrender/objects.hpp" -#include "../mwrender/renderinginterface.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/weapontype.hpp" +#include "../mwrender/objects.hpp" +#include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { + Armor::Armor() + : MWWorld::RegisteredClass(ESM::Armor::sRecordId) + { + } - void Armor::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + void Armor::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - if (!model.empty()) { + if (!model.empty()) + { renderingInterface.getObjects().insertModel(ptr, model); } } - void Armor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + std::string_view Armor::getModel(const MWWorld::ConstPtr& ptr) const { - // TODO: add option somewhere to enable collision for placeable objects + return getClassModel(ptr); } - std::string Armor::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Armor::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getNameOrId(ptr); } - std::string Armor::getName (const MWWorld::ConstPtr& ptr) const - { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; - } - - std::shared_ptr Armor::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Armor::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } - bool Armor::hasItemHealth (const MWWorld::ConstPtr& ptr) const + bool Armor::hasItemHealth(const MWWorld::ConstPtr& ptr) const { return true; } - int Armor::getItemMaxHealth (const MWWorld::ConstPtr& ptr) const + int Armor::getItemMaxHealth(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mHealth; } - std::string Armor::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Armor::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - std::pair, bool> Armor::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const + std::pair, bool> Armor::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); std::vector slots_; const int size = 11; - static const int sMapping[size][2] = - { - { ESM::Armor::Helmet, MWWorld::InventoryStore::Slot_Helmet }, + static const int sMapping[size][2] = { { ESM::Armor::Helmet, MWWorld::InventoryStore::Slot_Helmet }, { ESM::Armor::Cuirass, MWWorld::InventoryStore::Slot_Cuirass }, { ESM::Armor::LPauldron, MWWorld::InventoryStore::Slot_LeftPauldron }, { ESM::Armor::RPauldron, MWWorld::InventoryStore::Slot_RightPauldron }, @@ -103,120 +97,138 @@ namespace MWClass { ESM::Armor::RGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet }, { ESM::Armor::Shield, MWWorld::InventoryStore::Slot_CarriedLeft }, { ESM::Armor::LBracer, MWWorld::InventoryStore::Slot_LeftGauntlet }, - { ESM::Armor::RBracer, MWWorld::InventoryStore::Slot_RightGauntlet } - }; + { ESM::Armor::RBracer, MWWorld::InventoryStore::Slot_RightGauntlet } }; - for (int i=0; imBase->mData.mType) + for (int i = 0; i < size; ++i) + if (sMapping[i][0] == ref->mBase->mData.mType) { - slots_.push_back (int (sMapping[i][1])); + slots_.push_back(int(sMapping[i][1])); break; } - return std::make_pair (slots_, false); + return std::make_pair(slots_, false); } - int Armor::getEquipmentSkill (const MWWorld::ConstPtr& ptr) const + ESM::RefId Armor::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); - std::string typeGmst; + std::string_view typeGmst; switch (ref->mBase->mData.mType) { - case ESM::Armor::Helmet: typeGmst = "iHelmWeight"; break; - case ESM::Armor::Cuirass: typeGmst = "iCuirassWeight"; break; + case ESM::Armor::Helmet: + typeGmst = "iHelmWeight"; + break; + case ESM::Armor::Cuirass: + typeGmst = "iCuirassWeight"; + break; case ESM::Armor::LPauldron: - case ESM::Armor::RPauldron: typeGmst = "iPauldronWeight"; break; - case ESM::Armor::Greaves: typeGmst = "iGreavesWeight"; break; - case ESM::Armor::Boots: typeGmst = "iBootsWeight"; break; + case ESM::Armor::RPauldron: + typeGmst = "iPauldronWeight"; + break; + case ESM::Armor::Greaves: + typeGmst = "iGreavesWeight"; + break; + case ESM::Armor::Boots: + typeGmst = "iBootsWeight"; + break; case ESM::Armor::LGauntlet: - case ESM::Armor::RGauntlet: typeGmst = "iGauntletWeight"; break; - case ESM::Armor::Shield: typeGmst = "iShieldWeight"; break; + case ESM::Armor::RGauntlet: + typeGmst = "iGauntletWeight"; + break; + case ESM::Armor::Shield: + typeGmst = "iShieldWeight"; + break; case ESM::Armor::LBracer: - case ESM::Armor::RBracer: typeGmst = "iGauntletWeight"; break; + case ESM::Armor::RBracer: + typeGmst = "iGauntletWeight"; + break; } if (typeGmst.empty()) - return -1; + return {}; - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); float iWeight = floor(gmst.find(typeGmst)->mValue.getFloat()); float epsilon = 0.0005f; - if (ref->mBase->mData.mWeight <= iWeight * gmst.find ("fLightMaxMod")->mValue.getFloat() + epsilon) + if (ref->mBase->mData.mWeight <= iWeight * gmst.find("fLightMaxMod")->mValue.getFloat() + epsilon) return ESM::Skill::LightArmor; - if (ref->mBase->mData.mWeight <= iWeight * gmst.find ("fMedMaxMod")->mValue.getFloat() + epsilon) + if (ref->mBase->mData.mWeight <= iWeight * gmst.find("fMedMaxMod")->mValue.getFloat() + epsilon) return ESM::Skill::MediumArmor; else return ESM::Skill::HeavyArmor; } - int Armor::getValue (const MWWorld::ConstPtr& ptr) const + int Armor::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } - void Armor::registerSelf() + const ESM::RefId& Armor::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - std::shared_ptr instance (new Armor); + const ESM::RefId es = getEquipmentSkill(ptr); + static const ESM::RefId lightUp = ESM::RefId::stringRefId("Item Armor Light Up"); + static const ESM::RefId mediumUp = ESM::RefId::stringRefId("Item Armor Medium Up"); + static const ESM::RefId heavyUp = ESM::RefId::stringRefId("Item Armor Heavy Up"); - registerClass (typeid (ESM::Armor).name(), instance); - } - - std::string Armor::getUpSoundId (const MWWorld::ConstPtr& ptr) const - { - int es = getEquipmentSkill(ptr); if (es == ESM::Skill::LightArmor) - return std::string("Item Armor Light Up"); + return lightUp; else if (es == ESM::Skill::MediumArmor) - return std::string("Item Armor Medium Up"); + return mediumUp; else - return std::string("Item Armor Heavy Up"); + return heavyUp; } - std::string Armor::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Armor::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - int es = getEquipmentSkill(ptr); + const ESM::RefId es = getEquipmentSkill(ptr); + static const ESM::RefId lightDown = ESM::RefId::stringRefId("Item Armor Light Down"); + static const ESM::RefId mediumDown = ESM::RefId::stringRefId("Item Armor Medium Down"); + static const ESM::RefId heavyDown = ESM::RefId::stringRefId("Item Armor Heavy Down"); if (es == ESM::Skill::LightArmor) - return std::string("Item Armor Light Down"); + return lightDown; else if (es == ESM::Skill::MediumArmor) - return std::string("Item Armor Medium Down"); + return mediumDown; else - return std::string("Item Armor Heavy Down"); + return heavyDown; } - std::string Armor::getInventoryIcon (const MWWorld::ConstPtr& ptr) const + const std::string& Armor::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - MWGui::ToolTipInfo Armor::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Armor::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; // get armor type string (light/medium/heavy) - std::string typeText; + std::string_view typeText; if (ref->mBase->mData.mWeight == 0) - typeText = ""; + { + // no type + } else { - int armorType = getEquipmentSkill(ptr); + const ESM::RefId armorType = getEquipmentSkill(ptr); if (armorType == ESM::Skill::LightArmor) typeText = "#{sLight}"; else if (armorType == ESM::Skill::MediumArmor) @@ -225,153 +237,162 @@ namespace MWClass typeText = "#{sHeavy}"; } - text += "\n#{sArmorRating}: " + MWGui::ToolTips::toString(static_cast(getEffectiveArmorRating(ptr, - MWMechanics::getPlayer()))); + text += "\n#{sArmorRating}: " + + MWGui::ToolTips::toString(static_cast(getEffectiveArmorRating(ptr, MWMechanics::getPlayer()))); int remainingHealth = getItemHealth(ptr); text += "\n#{sCondition}: " + MWGui::ToolTips::toString(remainingHealth) + "/" - + MWGui::ToolTips::toString(ref->mBase->mData.mHealth); + + MWGui::ToolTips::toString(ref->mBase->mData.mHealth); - if (typeText != "") - text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight) + " (" + typeText + ")"; + if (!typeText.empty()) + { + text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight) + " ("; + text += typeText; + text += ')'; + } text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } info.enchant = ref->mBase->mEnchant; if (!info.enchant.empty()) info.remainingEnchantCharge = static_cast(ptr.getCellRef().getEnchantmentCharge()); - info.text = text; + info.text = std::move(text); return info; } - std::string Armor::getEnchantment (const MWWorld::ConstPtr& ptr) const + ESM::RefId Armor::getEnchantment(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mEnchant; } - std::string Armor::applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const + const ESM::RefId& Armor::applyEnchantment( + const MWWorld::ConstPtr& ptr, const ESM::RefId& enchId, int enchCharge, const std::string& newName) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); ESM::Armor newItem = *ref->mBase; - newItem.mId=""; - newItem.mName=newName; - newItem.mData.mEnchant=enchCharge; - newItem.mEnchant=enchId; - const ESM::Armor *record = MWBase::Environment::get().getWorld()->createRecord (newItem); + newItem.mId = ESM::RefId(); + newItem.mName = newName; + newItem.mData.mEnchant = enchCharge; + newItem.mEnchant = enchId; + const ESM::Armor* record = MWBase::Environment::get().getESMStore()->insert(newItem); return record->mId; } - float Armor::getEffectiveArmorRating(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &actor) const + float Armor::getEffectiveArmorRating(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& actor) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); - int armorSkillType = getEquipmentSkill(ptr); + const ESM::RefId armorSkillType = getEquipmentSkill(ptr); float armorSkill = actor.getClass().getSkill(actor, armorSkillType); - const MWBase::World *world = MWBase::Environment::get().getWorld(); - int iBaseArmorSkill = world->getStore().get().find("iBaseArmorSkill")->mValue.getInteger(); + int iBaseArmorSkill = MWBase::Environment::get() + .getESMStore() + ->get() + .find("iBaseArmorSkill") + ->mValue.getInteger(); - if(ref->mBase->mData.mWeight == 0) + if (ref->mBase->mData.mWeight == 0) return ref->mBase->mData.mArmor; else return ref->mBase->mData.mArmor * armorSkill / static_cast(iBaseArmorSkill); } - std::pair Armor::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const + std::pair Armor::canBeEquipped(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { const MWWorld::InventoryStore& invStore = npc.getClass().getInventoryStore(npc); if (getItemHealth(ptr) == 0) - return std::make_pair(0, "#{sInventoryMessage1}"); + return { 0, "#{sInventoryMessage1}" }; // slots that this item can be equipped in std::pair, bool> slots_ = getEquipmentSlots(ptr); if (slots_.first.empty()) - return std::make_pair(0, ""); + return { 0, {} }; if (npc.getClass().isNpc()) { - std::string npcRace = npc.get()->mBase->mRace; + const ESM::RefId& npcRace = npc.get()->mBase->mRace; // Beast races cannot equip shoes / boots, or full helms (head part vs hair part) - const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(npcRace); - if(race->mData.mFlags & ESM::Race::Beast) + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(npcRace); + if (race->mData.mFlags & ESM::Race::Beast) { std::vector parts = ptr.get()->mBase->mParts.mParts; - for(std::vector::iterator itr = parts.begin(); itr != parts.end(); ++itr) + for (std::vector::iterator itr = parts.begin(); itr != parts.end(); ++itr) { - if((*itr).mPart == ESM::PRT_Head) - return std::make_pair(0, "#{sNotifyMessage13}"); - if((*itr).mPart == ESM::PRT_LFoot || (*itr).mPart == ESM::PRT_RFoot) - return std::make_pair(0, "#{sNotifyMessage14}"); + if ((*itr).mPart == ESM::PRT_Head) + return { 0, "#{sNotifyMessage13}" }; + if ((*itr).mPart == ESM::PRT_LFoot || (*itr).mPart == ESM::PRT_RFoot) + return { 0, "#{sNotifyMessage14}" }; } } } - for (std::vector::const_iterator slot=slots_.first.begin(); - slot!=slots_.first.end(); ++slot) + for (std::vector::const_iterator slot = slots_.first.begin(); slot != slots_.first.end(); ++slot) { // If equipping a shield, check if there's a twohanded weapon conflicting with it - if(*slot == MWWorld::InventoryStore::Slot_CarriedLeft) + if (*slot == MWWorld::InventoryStore::Slot_CarriedLeft) { - MWWorld::ConstContainerStoreIterator weapon = invStore.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon != invStore.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()) + MWWorld::ConstContainerStoreIterator weapon + = invStore.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weapon != invStore.end() && weapon->getType() == ESM::Weapon::sRecordId) { - const MWWorld::LiveCellRef *ref = weapon->get(); + const MWWorld::LiveCellRef* ref = weapon->get(); if (MWMechanics::getWeaponType(ref->mBase->mData.mType)->mFlags & ESM::WeaponType::TwoHanded) - return std::make_pair(3,""); + return { 3, {} }; } - return std::make_pair(1,""); + return { 1, {} }; } } - return std::make_pair(1,""); + return { 1, {} }; } - std::shared_ptr Armor::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Armor::use(const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); return action; } - MWWorld::Ptr Armor::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Armor::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - int Armor::getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const + int Armor::getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mEnchant; } - bool Armor::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Armor::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Armor) - || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } - float Armor::getWeight(const MWWorld::ConstPtr &ptr) const + float Armor::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } diff --git a/apps/openmw/mwclass/armor.hpp b/apps/openmw/mwclass/armor.hpp index 4f04e0824b3..808bc078f42 100644 --- a/apps/openmw/mwclass/armor.hpp +++ b/apps/openmw/mwclass/armor.hpp @@ -1,85 +1,87 @@ #ifndef GAME_MWCLASS_ARMOR_H #define GAME_MWCLASS_ARMOR_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Armor : public MWWorld::Class + class Armor : public MWWorld::RegisteredClass { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + Armor(); - float getWeight (const MWWorld::ConstPtr& ptr) const override; + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + public: + float getWeight(const MWWorld::ConstPtr& ptr) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + bool isItem(const MWWorld::ConstPtr&) const override { return true; } - bool hasItemHealth (const MWWorld::ConstPtr& ptr) const override; - ///< \return Item health data available? + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - int getItemMaxHealth (const MWWorld::ConstPtr& ptr) const override; - ///< Return item max health or throw an exception, if class does not have item health + bool hasItemHealth(const MWWorld::ConstPtr& ptr) const override; + ///< \return Item health data available? - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + int getItemMaxHealth(const MWWorld::ConstPtr& ptr) const override; + ///< Return item max health or throw an exception, if class does not have item health - std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; - ///< \return first: Return IDs of the slot this object can be equipped in; second: can object - /// stay stacked when equipped? + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - int getEquipmentSkill (const MWWorld::ConstPtr& ptr) const override; - /// Return the index of the skill this item corresponds to when equipped or -1, if there is - /// no such skill. + std::pair, bool> getEquipmentSlots(const MWWorld::ConstPtr& ptr) const override; + ///< \return first: Return IDs of the slot this object can be equipped in; second: can object + /// stay stacked when equipped? - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - static void registerSelf(); + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - std::string getEnchantment (const MWWorld::ConstPtr& ptr) const override; - ///< @return the enchantment ID if the object is enchanted, otherwise an empty string + ESM::RefId getEnchantment(const MWWorld::ConstPtr& ptr) const override; + ///< @return the enchantment ID if the object is enchanted, otherwise an empty string - std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const override; - ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. + const ESM::RefId& applyEnchantment(const MWWorld::ConstPtr& ptr, const ESM::RefId& enchId, int enchCharge, + const std::string& newName) const override; + ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. - std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; - ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. \n - /// Second item in the pair specifies the error message + std::pair canBeEquipped( + const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const override; + ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon + ///< conflicts with that. \n + /// Second item in the pair specifies the error message - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - int getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const override; + int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const override; - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; - /// Get the effective armor rating, factoring in the actor's skills, for the given armor. - float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const override; + /// Get the effective armor rating, factoring in the actor's skills, for the given armor. + float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const override; }; } diff --git a/apps/openmw/mwclass/bodypart.cpp b/apps/openmw/mwclass/bodypart.cpp index 0315d3ddb08..81e42ac725a 100644 --- a/apps/openmw/mwclass/bodypart.cpp +++ b/apps/openmw/mwclass/bodypart.cpp @@ -1,34 +1,40 @@ #include "bodypart.hpp" -#include "../mwrender/renderinginterface.hpp" +#include + #include "../mwrender/objects.hpp" +#include "../mwrender/renderinginterface.hpp" #include "../mwworld/cellstore.hpp" +#include "classmodel.hpp" + namespace MWClass { + BodyPart::BodyPart() + : MWWorld::RegisteredClass(ESM::BodyPart::sRecordId) + { + } - MWWorld::Ptr BodyPart::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr BodyPart::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - void BodyPart::insertObjectRendering(const MWWorld::Ptr &ptr, const std::string &model, MWRender::RenderingInterface &renderingInterface) const + void BodyPart::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - if (!model.empty()) { + if (!model.empty()) + { renderingInterface.getObjects().insertModel(ptr, model); } } - void BodyPart::insertObject(const MWWorld::Ptr &ptr, const std::string &model, MWPhysics::PhysicsSystem &physics) const + std::string_view BodyPart::getName(const MWWorld::ConstPtr& ptr) const { - } - - std::string BodyPart::getName(const MWWorld::ConstPtr &ptr) const - { - return std::string(); + return {}; } bool BodyPart::hasToolTip(const MWWorld::ConstPtr& ptr) const @@ -36,22 +42,9 @@ namespace MWClass return false; } - void BodyPart::registerSelf() + std::string_view BodyPart::getModel(const MWWorld::ConstPtr& ptr) const { - std::shared_ptr instance (new BodyPart); - - registerClass (typeid (ESM::BodyPart).name(), instance); - } - - std::string BodyPart::getModel(const MWWorld::ConstPtr &ptr) const - { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } } diff --git a/apps/openmw/mwclass/bodypart.hpp b/apps/openmw/mwclass/bodypart.hpp index 13d91413862..4268c1ecf57 100644 --- a/apps/openmw/mwclass/bodypart.hpp +++ b/apps/openmw/mwclass/bodypart.hpp @@ -1,31 +1,31 @@ #ifndef GAME_MWCLASS_BODYPART_H #define GAME_MWCLASS_BODYPART_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class BodyPart : public MWWorld::Class + class BodyPart : public MWWorld::RegisteredClass { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + BodyPart(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. - bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; ///< @return true if this object has a tooltip when focused (default implementation: true) - static void registerSelf(); - - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; }; } diff --git a/apps/openmw/mwclass/book.cpp b/apps/openmw/mwclass/book.cpp index 4ea71e3ac28..9cbaaa9071e 100644 --- a/apps/openmw/mwclass/book.cpp +++ b/apps/openmw/mwclass/book.cpp @@ -1,18 +1,20 @@ #include "book.hpp" -#include +#include +#include + +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/ptr.hpp" #include "../mwworld/actionread.hpp" -#include "../mwworld/failedaction.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwphysics/physicssystem.hpp" +#include "../mwworld/failedaction.hpp" +#include "../mwworld/ptr.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" @@ -21,101 +23,93 @@ #include "../mwmechanics/npcstats.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { - - void Book::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + Book::Book() + : MWWorld::RegisteredClass(ESM::Book::sRecordId) { - if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); - } } - void Book::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Book::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - // TODO: add option somewhere to enable collision for placeable objects + if (!model.empty()) + { + renderingInterface.getObjects().insertModel(ptr, model); + } } - std::string Book::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Book::getModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } - std::string Book::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Book::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - std::shared_ptr Book::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Book::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { - if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfItem"); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound* sound = store.get().searchRandom("WolfItem", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); - if(sound) action->setSound(sound->mId); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); + if (sound) + action->setSound(sound->mId); return action; } - return std::shared_ptr(new MWWorld::ActionRead(ptr)); + return std::make_unique(ptr); } - std::string Book::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Book::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - int Book::getValue (const MWWorld::ConstPtr& ptr) const + int Book::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } - void Book::registerSelf() + const ESM::RefId& Book::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - std::shared_ptr instance (new Book); - - registerClass (typeid (ESM::Book).name(), instance); + static auto var = ESM::RefId::stringRefId("Item Book Up"); + return var; } - std::string Book::getUpSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Book::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Book Up"); + static auto var = ESM::RefId::stringRefId("Item Book Down"); + return var; } - std::string Book::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const std::string& Book::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Book Down"); - } - - std::string Book::getInventoryIcon (const MWWorld::ConstPtr& ptr) const - { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - MWGui::ToolTipInfo Book::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Book::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -123,67 +117,69 @@ namespace MWClass text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } info.enchant = ref->mBase->mEnchant; - info.text = text; + info.text = std::move(text); return info; } - std::string Book::getEnchantment (const MWWorld::ConstPtr& ptr) const + ESM::RefId Book::getEnchantment(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mEnchant; } - std::string Book::applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const + const ESM::RefId& Book::applyEnchantment( + const MWWorld::ConstPtr& ptr, const ESM::RefId& enchId, int enchCharge, const std::string& newName) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); ESM::Book newItem = *ref->mBase; - newItem.mId=""; - newItem.mName=newName; + newItem.mId = ESM::RefId(); + newItem.mName = newName; newItem.mData.mIsScroll = 1; - newItem.mData.mEnchant=enchCharge; - newItem.mEnchant=enchId; - const ESM::Book *record = MWBase::Environment::get().getWorld()->createRecord (newItem); + newItem.mData.mEnchant = enchCharge; + newItem.mEnchant = enchId; + const ESM::Book* record = MWBase::Environment::get().getESMStore()->insert(newItem); return record->mId; } - std::shared_ptr Book::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Book::use(const MWWorld::Ptr& ptr, bool force) const { - return std::shared_ptr(new MWWorld::ActionRead(ptr)); + return std::make_unique(ptr); } - MWWorld::Ptr Book::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Book::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - int Book::getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const + int Book::getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mEnchant; } - bool Book::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Book::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Books) - || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } - float Book::getWeight(const MWWorld::ConstPtr &ptr) const + float Book::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } diff --git a/apps/openmw/mwclass/book.hpp b/apps/openmw/mwclass/book.hpp index c58e68ad87d..ca804a32e6a 100644 --- a/apps/openmw/mwclass/book.hpp +++ b/apps/openmw/mwclass/book.hpp @@ -1,64 +1,66 @@ #ifndef GAME_MWCLASS_BOOK_H #define GAME_MWCLASS_BOOK_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Book : public MWWorld::Class + class Book : public MWWorld::RegisteredClass { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + Book(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + bool isItem(const MWWorld::ConstPtr&) const override { return true; } - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - static void registerSelf(); + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - std::string getEnchantment (const MWWorld::ConstPtr& ptr) const override; - ///< @return the enchantment ID if the object is enchanted, otherwise an empty string + ESM::RefId getEnchantment(const MWWorld::ConstPtr& ptr) const override; + ///< @return the enchantment ID if the object is enchanted, otherwise an empty string - std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const override; - ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. + const ESM::RefId& applyEnchantment(const MWWorld::ConstPtr& ptr, const ESM::RefId& enchId, int enchCharge, + const std::string& newName) const override; + ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - int getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const override; + int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const override; - float getWeight (const MWWorld::ConstPtr& ptr) const override; + float getWeight(const MWWorld::ConstPtr& ptr) const override; - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } diff --git a/apps/openmw/mwclass/classes.cpp b/apps/openmw/mwclass/classes.cpp index a552dfebf0e..e7d5cf394b0 100644 --- a/apps/openmw/mwclass/classes.cpp +++ b/apps/openmw/mwclass/classes.cpp @@ -1,26 +1,53 @@ #include "classes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "activator.hpp" -#include "creature.hpp" -#include "npc.hpp" -#include "weapon.hpp" -#include "armor.hpp" -#include "potion.hpp" #include "apparatus.hpp" +#include "armor.hpp" +#include "bodypart.hpp" #include "book.hpp" #include "clothing.hpp" #include "container.hpp" +#include "creature.hpp" +#include "creaturelevlist.hpp" #include "door.hpp" #include "ingredient.hpp" -#include "creaturelevlist.hpp" #include "itemlevlist.hpp" #include "light.hpp" #include "lockpick.hpp" #include "misc.hpp" +#include "npc.hpp" +#include "potion.hpp" #include "probe.hpp" #include "repair.hpp" #include "static.hpp" -#include "bodypart.hpp" +#include "weapon.hpp" + +#include "esm4base.hpp" +#include "esm4npc.hpp" +#include "light4.hpp" namespace MWClass { @@ -47,5 +74,28 @@ namespace MWClass Repair::registerSelf(); Static::registerSelf(); BodyPart::registerSelf(); + + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Light::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Npc::registerSelf(); + ESM4Named::registerSelf(); + ESM4Static::registerSelf(); + ESM4Named::registerSelf(); + ESM4Named::registerSelf(); + ESM4Tree::registerSelf(); + ESM4Named::registerSelf(); } } diff --git a/apps/openmw/mwclass/classmodel.hpp b/apps/openmw/mwclass/classmodel.hpp new file mode 100644 index 00000000000..f063ae72920 --- /dev/null +++ b/apps/openmw/mwclass/classmodel.hpp @@ -0,0 +1,20 @@ +#ifndef OPENMW_MWCLASS_CLASSMODEL_H +#define OPENMW_MWCLASS_CLASSMODEL_H + +#include "../mwworld/livecellref.hpp" +#include "../mwworld/ptr.hpp" + +#include +#include + +namespace MWClass +{ + template + std::string_view getClassModel(const MWWorld::ConstPtr& ptr) + { + const MWWorld::LiveCellRef* ref = ptr.get(); + return ref->mBase->mModel; + } +} + +#endif diff --git a/apps/openmw/mwclass/clothing.cpp b/apps/openmw/mwclass/clothing.cpp index 6d7960aac2e..87d34c56d61 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -1,89 +1,83 @@ #include "clothing.hpp" -#include +#include +#include + +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwworld/ptr.hpp" #include "../mwworld/actionequip.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwphysics/physicssystem.hpp" -#include "../mwworld/nullaction.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { + Clothing::Clothing() + : MWWorld::RegisteredClass(ESM::Clothing::sRecordId) + { + } - void Clothing::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + void Clothing::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - if (!model.empty()) { + if (!model.empty()) + { renderingInterface.getObjects().insertModel(ptr, model); } } - void Clothing::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + std::string_view Clothing::getModel(const MWWorld::ConstPtr& ptr) const { - // TODO: add option somewhere to enable collision for placeable objects + return getClassModel(ptr); } - std::string Clothing::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Clothing::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getNameOrId(ptr); } - std::string Clothing::getName (const MWWorld::ConstPtr& ptr) const - { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; - } - - std::shared_ptr Clothing::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Clothing::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } - std::string Clothing::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Clothing::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - std::pair, bool> Clothing::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const + std::pair, bool> Clothing::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); std::vector slots_; - if (ref->mBase->mData.mType==ESM::Clothing::Ring) + if (ref->mBase->mData.mType == ESM::Clothing::Ring) { - slots_.push_back (int (MWWorld::InventoryStore::Slot_LeftRing)); - slots_.push_back (int (MWWorld::InventoryStore::Slot_RightRing)); + slots_.push_back(int(MWWorld::InventoryStore::Slot_LeftRing)); + slots_.push_back(int(MWWorld::InventoryStore::Slot_RightRing)); } else { const int size = 9; - static const int sMapping[size][2] = - { - { ESM::Clothing::Shirt, MWWorld::InventoryStore::Slot_Shirt }, + static const int sMapping[size][2] = { { ESM::Clothing::Shirt, MWWorld::InventoryStore::Slot_Shirt }, { ESM::Clothing::Belt, MWWorld::InventoryStore::Slot_Belt }, { ESM::Clothing::Robe, MWWorld::InventoryStore::Slot_Robe }, { ESM::Clothing::Pants, MWWorld::InventoryStore::Slot_Pants }, @@ -91,79 +85,74 @@ namespace MWClass { ESM::Clothing::LGlove, MWWorld::InventoryStore::Slot_LeftGauntlet }, { ESM::Clothing::RGlove, MWWorld::InventoryStore::Slot_RightGauntlet }, { ESM::Clothing::Skirt, MWWorld::InventoryStore::Slot_Skirt }, - { ESM::Clothing::Amulet, MWWorld::InventoryStore::Slot_Amulet } - }; + { ESM::Clothing::Amulet, MWWorld::InventoryStore::Slot_Amulet } }; - for (int i=0; imBase->mData.mType) + for (int i = 0; i < size; ++i) + if (sMapping[i][0] == ref->mBase->mData.mType) { - slots_.push_back (int (sMapping[i][1])); + slots_.push_back(int(sMapping[i][1])); break; } } - return std::make_pair (slots_, false); + return std::make_pair(slots_, false); } - int Clothing::getEquipmentSkill (const MWWorld::ConstPtr& ptr) const + ESM::RefId Clothing::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); - if (ref->mBase->mData.mType==ESM::Clothing::Shoes) + if (ref->mBase->mData.mType == ESM::Clothing::Shoes) return ESM::Skill::Unarmored; - return -1; + return {}; } - int Clothing::getValue (const MWWorld::ConstPtr& ptr) const + int Clothing::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } - void Clothing::registerSelf() + const ESM::RefId& Clothing::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - std::shared_ptr instance (new Clothing); - - registerClass (typeid (ESM::Clothing).name(), instance); - } - - std::string Clothing::getUpSoundId (const MWWorld::ConstPtr& ptr) const - { - const MWWorld::LiveCellRef *ref = ptr.get(); - - if (ref->mBase->mData.mType == 8) + const MWWorld::LiveCellRef* ref = ptr.get(); + static const ESM::RefId ringUp = ESM::RefId::stringRefId("Item Ring Up"); + static const ESM::RefId clothsUp = ESM::RefId::stringRefId("Item Clothes Up"); + if (ref->mBase->mData.mType == ESM::Clothing::Ring) { - return std::string("Item Ring Up"); + return ringUp; } - return std::string("Item Clothes Up"); + return clothsUp; } - std::string Clothing::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Clothing::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - if (ref->mBase->mData.mType == 8) + const MWWorld::LiveCellRef* ref = ptr.get(); + static const ESM::RefId ringDown = ESM::RefId::stringRefId("Item Ring Down"); + static const ESM::RefId clothsDown = ESM::RefId::stringRefId("Item Clothes Down"); + if (ref->mBase->mData.mType == ESM::Clothing::Ring) { - return std::string("Item Ring Down"); + return ringDown; } - return std::string("Item Clothes Down"); + return clothsDown; } - std::string Clothing::getInventoryIcon (const MWWorld::ConstPtr& ptr) const + const std::string& Clothing::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - MWGui::ToolTipInfo Clothing::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Clothing::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -171,103 +160,106 @@ namespace MWClass text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } info.enchant = ref->mBase->mEnchant; if (!info.enchant.empty()) info.remainingEnchantCharge = static_cast(ptr.getCellRef().getEnchantmentCharge()); - info.text = text; + info.text = std::move(text); return info; } - std::string Clothing::getEnchantment (const MWWorld::ConstPtr& ptr) const + ESM::RefId Clothing::getEnchantment(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mEnchant; } - std::string Clothing::applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const + const ESM::RefId& Clothing::applyEnchantment( + const MWWorld::ConstPtr& ptr, const ESM::RefId& enchId, int enchCharge, const std::string& newName) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); ESM::Clothing newItem = *ref->mBase; - newItem.mId=""; - newItem.mName=newName; - newItem.mData.mEnchant=enchCharge; - newItem.mEnchant=enchId; - const ESM::Clothing *record = MWBase::Environment::get().getWorld()->createRecord (newItem); + newItem.mId = ESM::RefId(); + newItem.mName = newName; + newItem.mData.mEnchant = enchCharge; + newItem.mEnchant = enchId; + const ESM::Clothing* record = MWBase::Environment::get().getESMStore()->insert(newItem); return record->mId; } - std::pair Clothing::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const + std::pair Clothing::canBeEquipped( + const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { // slots that this item can be equipped in std::pair, bool> slots_ = getEquipmentSlots(ptr); if (slots_.first.empty()) - return std::make_pair(0, ""); + return { 0, {} }; if (npc.getClass().isNpc()) { - std::string npcRace = npc.get()->mBase->mRace; + const ESM::RefId& npcRace = npc.get()->mBase->mRace; // Beast races cannot equip shoes / boots, or full helms (head part vs hair part) - const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(npcRace); - if(race->mData.mFlags & ESM::Race::Beast) + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(npcRace); + if (race->mData.mFlags & ESM::Race::Beast) { std::vector parts = ptr.get()->mBase->mParts.mParts; - for(std::vector::iterator itr = parts.begin(); itr != parts.end(); ++itr) + for (std::vector::iterator itr = parts.begin(); itr != parts.end(); ++itr) { - if((*itr).mPart == ESM::PRT_Head) - return std::make_pair(0, "#{sNotifyMessage13}"); - if((*itr).mPart == ESM::PRT_LFoot || (*itr).mPart == ESM::PRT_RFoot) - return std::make_pair(0, "#{sNotifyMessage15}"); + if ((*itr).mPart == ESM::PRT_Head) + return { 0, "#{sNotifyMessage13}" }; + if ((*itr).mPart == ESM::PRT_LFoot || (*itr).mPart == ESM::PRT_RFoot) + return { 0, "#{sNotifyMessage15}" }; } } } - return std::make_pair (1, ""); + return { 1, {} }; } - std::shared_ptr Clothing::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Clothing::use(const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); return action; } - MWWorld::Ptr Clothing::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Clothing::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - int Clothing::getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const + int Clothing::getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mEnchant; } - bool Clothing::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Clothing::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Clothing) - || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } - float Clothing::getWeight(const MWWorld::ConstPtr &ptr) const + float Clothing::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } diff --git a/apps/openmw/mwclass/clothing.hpp b/apps/openmw/mwclass/clothing.hpp index a87e0cbe00c..f95559f9c04 100644 --- a/apps/openmw/mwclass/clothing.hpp +++ b/apps/openmw/mwclass/clothing.hpp @@ -1,76 +1,78 @@ #ifndef GAME_MWCLASS_CLOTHING_H #define GAME_MWCLASS_CLOTHING_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Clothing : public MWWorld::Class + class Clothing : public MWWorld::RegisteredClass { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + Clothing(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + bool isItem(const MWWorld::ConstPtr&) const override { return true; } - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; - ///< \return first: Return IDs of the slot this object can be equipped in; second: can object - /// stay stacked when equipped? + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - int getEquipmentSkill (const MWWorld::ConstPtr& ptr) const override; - /// Return the index of the skill this item corresponds to when equipped or -1, if there is - /// no such skill. + std::pair, bool> getEquipmentSlots(const MWWorld::ConstPtr& ptr) const override; + ///< \return first: Return IDs of the slot this object can be equipped in; second: can object + /// stay stacked when equipped? - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - static void registerSelf(); + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - std::string getEnchantment (const MWWorld::ConstPtr& ptr) const override; - ///< @return the enchantment ID if the object is enchanted, otherwise an empty string + ESM::RefId getEnchantment(const MWWorld::ConstPtr& ptr) const override; + ///< @return the enchantment ID if the object is enchanted, otherwise an empty string - std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const override; - ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. + const ESM::RefId& applyEnchantment(const MWWorld::ConstPtr& ptr, const ESM::RefId& enchId, int enchCharge, + const std::string& newName) const override; + ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. - std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; - ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. - /// Second item in the pair specifies the error message + std::pair canBeEquipped( + const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const override; + ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon + ///< conflicts with that. + /// Second item in the pair specifies the error message - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - int getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const override; + int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const override; - float getWeight (const MWWorld::ConstPtr& ptr) const override; + float getWeight(const MWWorld::ConstPtr& ptr) const override; - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index de560608c04..5cf9bf919ad 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -1,26 +1,28 @@ #include "container.hpp" -#include -#include -#include +#include +#include + +#include +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/ptr.hpp" -#include "../mwworld/failedaction.hpp" -#include "../mwworld/nullaction.hpp" -#include "../mwworld/containerstore.hpp" -#include "../mwworld/customdata.hpp" -#include "../mwworld/cellstore.hpp" -#include "../mwworld/esmstore.hpp" +#include "../mwphysics/physicssystem.hpp" #include "../mwworld/actionharvest.hpp" #include "../mwworld/actionopen.hpp" #include "../mwworld/actiontrap.hpp" -#include "../mwphysics/physicssystem.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/failedaction.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/nullaction.hpp" +#include "../mwworld/worldmodel.hpp" #include "../mwgui/tooltips.hpp" @@ -28,17 +30,21 @@ #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" -#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/inventory.hpp" #include "../mwmechanics/npcstats.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { ContainerCustomData::ContainerCustomData(const ESM::Container& container, MWWorld::CellStore* cell) { - unsigned int seed = Misc::Rng::rollDice(std::numeric_limits::max()); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + unsigned int seed = Misc::Rng::rollDice(std::numeric_limits::max(), prng); // setting ownership not needed, since taking items from a container inherits the // container's owner automatically - mStore.fillNonRandom(container.mInventory, "", seed); + mStore.fillNonRandom(container.mInventory, ESM::RefId(), seed); } ContainerCustomData::ContainerCustomData(const ESM::InventoryState& inventory) @@ -56,18 +62,20 @@ namespace MWClass } Container::Container() + : MWWorld::RegisteredClass(ESM::Container::sRecordId) { - mHarvestEnabled = Settings::Manager::getBool("graphic herbalism", "Game"); } - void Container::ensureCustomData (const MWWorld::Ptr& ptr) const + void Container::ensureCustomData(const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { - MWWorld::LiveCellRef *ref = ptr.get(); + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + MWWorld::LiveCellRef* ref = ptr.get(); // store - ptr.getRefData().setCustomData (std::make_unique(*ref->mBase, ptr.getCell())); + ptr.getRefData().setCustomData(std::make_unique(*ref->mBase, ptr.getCell())); + getContainerStore(ptr).setPtr(ptr); MWBase::Environment::get().getWorld()->addContainerScripts(ptr, ptr.getCell()); } @@ -75,7 +83,7 @@ namespace MWClass bool Container::canBeHarvested(const MWWorld::ConstPtr& ptr) const { - if (!mHarvestEnabled) + if (!Settings::game().mGraphicHerbalism) return false; const MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (animation == nullptr) @@ -84,10 +92,9 @@ namespace MWClass return animation->canBeHarvested(); } - void Container::respawn(const MWWorld::Ptr &ptr) const + void Container::respawn(const MWWorld::Ptr& ptr) const { - MWWorld::LiveCellRef *ref = - ptr.get(); + MWWorld::LiveCellRef* ref = ptr.get(); if (ref->mBase->mFlags & ESM::Container::Respawn) { // Container was not touched, there is no need to modify its content. @@ -99,28 +106,30 @@ namespace MWClass } } - void Container::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + void Container::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model, true); + if (!model.empty()) + { + renderingInterface.getObjects().insertModel(ptr, model); } } - void Container::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Container::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const { - if(!model.empty()) - physics.addObject(ptr, model); + insertObjectPhysics(ptr, model, rotation, physics); } - std::string Container::getModel(const MWWorld::ConstPtr &ptr) const + void Container::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); + } - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + std::string_view Container::getModel(const MWWorld::ConstPtr& ptr) const + { + return getClassModel(ptr); } bool Container::useAnim() const @@ -128,35 +137,33 @@ namespace MWClass return true; } - std::shared_ptr Container::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Container::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) - return std::shared_ptr (new MWWorld::NullAction ()); + return std::make_unique(); - if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfContainer"); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound* sound = store.get().searchRandom("WolfContainer", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); - if(sound) action->setSound(sound->mId); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); + if (sound) + action->setSound(sound->mId); return action; } - const std::string lockedSound = "LockedChest"; - const std::string trapActivationSound = "Disarm Trap Fail"; - - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWWorld::InventoryStore& invStore = player.getClass().getInventoryStore(player); - bool isLocked = ptr.getCellRef().getLockLevel() > 0; + bool isLocked = ptr.getCellRef().isLocked(); bool isTrapped = !ptr.getCellRef().getTrap().empty(); bool hasKey = false; - std::string keyName; + std::string_view keyName; - const std::string keyId = ptr.getCellRef().getKey(); + const ESM::RefId& keyId = ptr.getCellRef().getKey(); if (!keyId.empty()) { MWWorld::Ptr keyPtr = invStore.search(keyId); @@ -169,153 +176,152 @@ namespace MWClass if (isLocked && hasKey) { - MWBase::Environment::get().getWindowManager ()->messageBox (keyName + " #{sKeyUsed}"); + MWBase::Environment::get().getWindowManager()->messageBox(std::string{ keyName } + " #{sKeyUsed}"); ptr.getCellRef().unlock(); // using a key disarms the trap - if(isTrapped) + if (isTrapped) { - ptr.getCellRef().setTrap(""); - MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Disarm Trap", 1.0f, 1.0f); + ptr.getCellRef().setTrap(ESM::RefId()); + MWBase::Environment::get().getSoundManager()->playSound3D( + ptr, ESM::RefId::stringRefId("Disarm Trap"), 1.0f, 1.0f); isTrapped = false; } } - if (!isLocked || hasKey) { - if(!isTrapped) + if (!isTrapped) { if (canBeHarvested(ptr)) { - std::shared_ptr action (new MWWorld::ActionHarvest(ptr)); - return action; + return std::make_unique(ptr); } - std::shared_ptr action (new MWWorld::ActionOpen(ptr)); - return action; + return std::make_unique(ptr); } else { // Activate trap - std::shared_ptr action(new MWWorld::ActionTrap(ptr.getCellRef().getTrap(), ptr)); - action->setSound(trapActivationSound); + std::unique_ptr action + = std::make_unique(ptr.getCellRef().getTrap(), ptr); + action->setSound(ESM::RefId::stringRefId("Disarm Trap Fail")); return action; } } else { - std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); - action->setSound(lockedSound); + std::unique_ptr action = std::make_unique(std::string_view{}, ptr); + action->setSound(ESM::RefId::stringRefId("LockedChest")); return action; } } - std::string Container::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Container::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - MWWorld::ContainerStore& Container::getContainerStore (const MWWorld::Ptr& ptr) const + MWWorld::ContainerStore& Container::getContainerStore(const MWWorld::Ptr& ptr) const { - ensureCustomData (ptr); - auto& data = ptr.getRefData().getCustomData()->asContainerCustomData(); - data.mStore.mPtr = ptr; - return data.mStore; + ensureCustomData(ptr); + return ptr.getRefData().getCustomData()->asContainerCustomData().mStore; } - std::string Container::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Container::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - void Container::registerSelf() - { - std::shared_ptr instance (new Container); - - registerClass (typeid (ESM::Container).name(), instance); - } - - bool Container::hasToolTip (const MWWorld::ConstPtr& ptr) const + bool Container::hasToolTip(const MWWorld::ConstPtr& ptr) const { if (const MWWorld::CustomData* data = ptr.getRefData().getCustomData()) return !canBeHarvested(ptr) || data->asContainerCustomData().mStore.hasVisibleItems(); return true; } - MWGui::ToolTipInfo Container::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Container::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); std::string text; int lockLevel = ptr.getCellRef().getLockLevel(); - if (lockLevel > 0 && lockLevel != ESM::UnbreakableLock) - text += "\n#{sLockLevel}: " + MWGui::ToolTips::toString(lockLevel); - else if (lockLevel < 0) - text += "\n#{sUnlocked}"; - if (ptr.getCellRef().getTrap() != "") + if (lockLevel) + { + if (ptr.getCellRef().isLocked()) + text += "\n#{sLockLevel}: " + MWGui::ToolTips::toString(lockLevel); + else + text += "\n#{sUnlocked}"; + } + if (ptr.getCellRef().getTrap() != ESM::RefId()) text += "\n#{sTrapped}"; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) - { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); - if (Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "stolen_goods")) - text += "\nYou can not use evidence chests"; + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); + if (ptr.getCellRef().getRefId() == "stolen_goods") + info.extra += "\nYou cannot use evidence chests"; } - info.text = text; + info.text = std::move(text); return info; } - float Container::getCapacity (const MWWorld::Ptr& ptr) const + float Container::getCapacity(const MWWorld::Ptr& ptr) const { - MWWorld::LiveCellRef *ref = - ptr.get(); + MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mWeight; } - float Container::getEncumbrance (const MWWorld::Ptr& ptr) const + float Container::getEncumbrance(const MWWorld::Ptr& ptr) const { - return getContainerStore (ptr).getWeight(); + return getContainerStore(ptr).getWeight(); } - bool Container::canLock(const MWWorld::ConstPtr &ptr) const + bool Container::canLock(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return !(ref->mBase->mFlags & ESM::Container::Organic); } - void Container::modifyBaseInventory(const std::string& containerId, const std::string& itemId, int amount) const + void Container::modifyBaseInventory(const ESM::RefId& containerId, const ESM::RefId& itemId, int amount) const { MWMechanics::modifyBaseInventory(containerId, itemId, amount); } - MWWorld::Ptr Container::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Container::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - return MWWorld::Ptr(cell.insert(ref), &cell); + const MWWorld::LiveCellRef* ref = ptr.get(); + MWWorld::Ptr newPtr(cell.insert(ref), &cell); + if (newPtr.getRefData().getCustomData()) + { + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); + getContainerStore(newPtr).setPtr(newPtr); + } + return newPtr; } - void Container::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const + void Container::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; const ESM::ContainerState& containerState = state.asContainerState(); ptr.getRefData().setCustomData(std::make_unique(containerState.mInventory)); + + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + getContainerStore(ptr).setPtr(ptr); } - void Container::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const + void Container::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { @@ -331,6 +337,6 @@ namespace MWClass } ESM::ContainerState& containerState = state.asContainerState(); - customData.mStore.writeState (containerState.mInventory); + customData.mStore.writeState(containerState.mInventory); } } diff --git a/apps/openmw/mwclass/container.hpp b/apps/openmw/mwclass/container.hpp index 1c893700682..88d8148fdca 100644 --- a/apps/openmw/mwclass/container.hpp +++ b/apps/openmw/mwclass/container.hpp @@ -1,9 +1,9 @@ #ifndef GAME_MWCLASS_CONTAINER_H #define GAME_MWCLASS_CONTAINER_H -#include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/customdata.hpp" +#include "../mwworld/registeredclass.hpp" namespace ESM { @@ -16,6 +16,7 @@ namespace MWClass class ContainerCustomData : public MWWorld::TypedCustomData { MWWorld::ContainerStore mStore; + public: ContainerCustomData(const ESM::Container& container, MWWorld::CellStore* cell); ContainerCustomData(const ESM::InventoryState& inventory); @@ -26,69 +27,69 @@ namespace MWClass friend class Container; }; - class Container : public MWWorld::Class + class Container : public MWWorld::RegisteredClass { - bool mHarvestEnabled; - - void ensureCustomData (const MWWorld::Ptr& ptr) const; + friend MWWorld::RegisteredClass; - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + Container(); - bool canBeHarvested(const MWWorld::ConstPtr& ptr) const; + void ensureCustomData(const MWWorld::Ptr& ptr) const; - public: - Container(); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + bool canBeHarvested(const MWWorld::ConstPtr& ptr) const; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override; - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; - ///< @return true if this object has a tooltip when focused (default implementation: true) + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; + ///< @return true if this object has a tooltip when focused (default implementation: true) - MWWorld::ContainerStore& getContainerStore (const MWWorld::Ptr& ptr) const override; - ///< Return container store + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + MWWorld::ContainerStore& getContainerStore(const MWWorld::Ptr& ptr) const override; + ///< Return container store - float getCapacity (const MWWorld::Ptr& ptr) const override; - ///< Return total weight that fits into the object. Throws an exception, if the object can't - /// hold other objects. + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - float getEncumbrance (const MWWorld::Ptr& ptr) const override; - ///< Returns total weight of objects inside this object (including modifications from magic - /// effects). Throws an exception, if the object can't hold other objects. + float getCapacity(const MWWorld::Ptr& ptr) const override; + ///< Return total weight that fits into the object. Throws an exception, if the object can't + /// hold other objects. - bool canLock(const MWWorld::ConstPtr &ptr) const override; + float getEncumbrance(const MWWorld::Ptr& ptr) const override; + ///< Returns total weight of objects inside this object (including modifications from magic + /// effects). Throws an exception, if the object can't hold other objects. - void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) - const override; - ///< Read additional state from \a state into \a ptr. + bool canLock(const MWWorld::ConstPtr& ptr) const override; - void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; - ///< Write additional state from \a ptr into \a state. + void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; + ///< Read additional state from \a state into \a ptr. - static void registerSelf(); + void writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; + ///< Write additional state from \a ptr into \a state. - void respawn (const MWWorld::Ptr& ptr) const override; + void respawn(const MWWorld::Ptr& ptr) const override; - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - bool useAnim() const override; + bool useAnim() const override; - void modifyBaseInventory(const std::string& containerId, const std::string& itemId, int amount) const override; + void modifyBaseInventory(const ESM::RefId& containerId, const ESM::RefId& itemId, int amount) const override; }; } diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index acf969bd3a3..007338095ff 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -1,47 +1,58 @@ #include "creature.hpp" +#include +#include + +#include +#include +#include +#include +#include #include -#include -#include -#include -#include +#include +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/aisetting.hpp" +#include "../mwmechanics/combat.hpp" +#include "../mwmechanics/creaturecustomdataresetter.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/difficultyscaling.hpp" +#include "../mwmechanics/disease.hpp" +#include "../mwmechanics/inventory.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwmechanics/movement.hpp" -#include "../mwmechanics/disease.hpp" -#include "../mwmechanics/spellcasting.hpp" -#include "../mwmechanics/difficultyscaling.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/setbaseaisetting.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwbase/soundmanager.hpp" -#include "../mwworld/ptr.hpp" -#include "../mwworld/actiontalk.hpp" #include "../mwworld/actionopen.hpp" -#include "../mwworld/failedaction.hpp" -#include "../mwworld/customdata.hpp" -#include "../mwworld/containerstore.hpp" +#include "../mwworld/actiontalk.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/containerstore.hpp" +#include "../mwworld/customdata.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/failedaction.hpp" +#include "../mwworld/inventorystore.hpp" #include "../mwworld/localscripts.hpp" +#include "../mwworld/ptr.hpp" +#include "../mwworld/worldmodel.hpp" -#include "../mwrender/renderinginterface.hpp" #include "../mwrender/objects.hpp" +#include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwworld/inventorystore.hpp" - -#include "../mwmechanics/npcstats.hpp" -#include "../mwmechanics/combat.hpp" -#include "../mwmechanics/actorutil.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" namespace { - bool isFlagBitSet(const MWWorld::ConstPtr &ptr, ESM::Creature::Flags bitMask) + bool isFlagBitSet(const MWWorld::ConstPtr& ptr, ESM::Creature::Flags bitMask) { return (ptr.get()->mBase->mFlags & bitMask) != 0; } @@ -61,31 +72,30 @@ namespace MWClass CreatureCustomData(const CreatureCustomData& other); CreatureCustomData(CreatureCustomData&& other) = default; - CreatureCustomData& asCreatureCustomData() override - { - return *this; - } - const CreatureCustomData& asCreatureCustomData() const override - { - return *this; - } + CreatureCustomData& asCreatureCustomData() override { return *this; } + const CreatureCustomData& asCreatureCustomData() const override { return *this; } }; CreatureCustomData::CreatureCustomData(const CreatureCustomData& other) - : mCreatureStats(other.mCreatureStats), - mContainerStore(other.mContainerStore->clone()), - mMovement(other.mMovement) + : mCreatureStats(other.mCreatureStats) + , mContainerStore(other.mContainerStore->clone()) + , mMovement(other.mMovement) + { + } + + Creature::Creature() + : MWWorld::RegisteredClass(ESM::Creature::sRecordId) { } const Creature::GMST& Creature::getGmst() { - static GMST gmst; - static bool inited = false; - if (!inited) - { - const MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &store = world->getStore().get(); + static const GMST staticGmst = [] { + GMST gmst; + + const MWWorld::Store& store + = MWBase::Environment::get().getESMStore()->get(); + gmst.fMinWalkSpeedCreature = store.find("fMinWalkSpeedCreature"); gmst.fMaxWalkSpeedCreature = store.find("fMaxWalkSpeedCreature"); gmst.fEncumberedMoveEffect = store.find("fEncumberedMoveEffect"); @@ -99,28 +109,27 @@ namespace MWClass gmst.fKnockDownMult = store.find("fKnockDownMult"); gmst.iKnockDownOddsMult = store.find("iKnockDownOddsMult"); gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); - inited = true; - } - return gmst; + + return gmst; + }(); + return staticGmst; } - void Creature::ensureCustomData (const MWWorld::Ptr& ptr) const + void Creature::ensureCustomData(const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data (new CreatureCustomData); + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + auto tempData = std::make_unique(); + CreatureCustomData* data = tempData.get(); + MWMechanics::CreatureCustomDataResetter resetter{ ptr }; + ptr.getRefData().setCustomData(std::move(tempData)); - MWWorld::LiveCellRef *ref = ptr.get(); + MWWorld::LiveCellRef* ref = ptr.get(); // creature stats - data->mCreatureStats.setAttribute(ESM::Attribute::Strength, ref->mBase->mData.mStrength); - data->mCreatureStats.setAttribute(ESM::Attribute::Intelligence, ref->mBase->mData.mIntelligence); - data->mCreatureStats.setAttribute(ESM::Attribute::Willpower, ref->mBase->mData.mWillpower); - data->mCreatureStats.setAttribute(ESM::Attribute::Agility, ref->mBase->mData.mAgility); - data->mCreatureStats.setAttribute(ESM::Attribute::Speed, ref->mBase->mData.mSpeed); - data->mCreatureStats.setAttribute(ESM::Attribute::Endurance, ref->mBase->mData.mEndurance); - data->mCreatureStats.setAttribute(ESM::Attribute::Personality, ref->mBase->mData.mPersonality); - data->mCreatureStats.setAttribute(ESM::Attribute::Luck, ref->mBase->mData.mLuck); + for (size_t i = 0; i < ref->mBase->mData.mAttributes.size(); ++i) + data->mCreatureStats.setAttribute(ESM::Attribute::indexToRefId(i), ref->mBase->mData.mAttributes[i]); data->mCreatureStats.setHealth(static_cast(ref->mBase->mData.mHealth)); data->mCreatureStats.setMagicka(static_cast(ref->mBase->mData.mMana)); data->mCreatureStats.setFatigue(static_cast(ref->mBase->mData.mFatigue)); @@ -129,10 +138,10 @@ namespace MWClass data->mCreatureStats.getAiSequence().fill(ref->mBase->mAiPackage); - data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Hello, ref->mBase->mAiData.mHello); - data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, ref->mBase->mAiData.mFight); - data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, ref->mBase->mAiData.mFlee); - data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Alarm, ref->mBase->mAiData.mAlarm); + data->mCreatureStats.setAiSetting(MWMechanics::AiSetting::Hello, ref->mBase->mAiData.mHello); + data->mCreatureStats.setAiSetting(MWMechanics::AiSetting::Fight, ref->mBase->mAiData.mFight); + data->mCreatureStats.setAiSetting(MWMechanics::AiSetting::Flee, ref->mBase->mAiData.mFlee); + data->mCreatureStats.setAiSetting(MWMechanics::AiSetting::Alarm, ref->mBase->mAiData.mAlarm); // Persistent actors with 0 health do not play death animation if (data->mCreatureStats.isDead()) @@ -149,48 +158,43 @@ namespace MWClass data->mContainerStore = std::make_unique(); else data->mContainerStore = std::make_unique(); + data->mContainerStore->setPtr(ptr); data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold); - data->mCreatureStats.setNeedRecalcDynamicStats(false); + resetter.mPtr = {}; - // store - ptr.getRefData().setCustomData(std::move(data)); - - getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId(), prng); if (hasInventory) - getInventoryStore(ptr).autoEquip(ptr); + getInventoryStore(ptr).autoEquip(); } } - void Creature::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + void Creature::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { MWRender::Objects& objects = renderingInterface.getObjects(); objects.insertCreature(ptr, model, hasInventoryStore(ptr)); } - std::string Creature::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Creature::getModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } - void Creature::getModelsToPreload(const MWWorld::Ptr &ptr, std::vector &models) const + void Creature::getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const { - std::string model = getModel(ptr); + std::string_view model = getModel(ptr); if (!model.empty()) models.push_back(model); - // FIXME: use const version of InventoryStore functions once they are available - if (hasInventoryStore(ptr)) + const MWWorld::CustomData* customData = ptr.getRefData().getCustomData(); + if (customData && hasInventoryStore(ptr)) { - const MWWorld::InventoryStore& invStore = getInventoryStore(ptr); + const auto& invStore + = static_cast(*customData->asCreatureCustomData().mContainerStore); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); @@ -204,107 +208,123 @@ namespace MWClass } } - std::string Creature::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Creature::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - MWMechanics::CreatureStats& Creature::getCreatureStats (const MWWorld::Ptr& ptr) const + MWMechanics::CreatureStats& Creature::getCreatureStats(const MWWorld::Ptr& ptr) const { - ensureCustomData (ptr); + ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asCreatureCustomData().mCreatureStats; } - - void Creature::hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const + bool Creature::evaluateHit(const MWWorld::Ptr& ptr, MWWorld::Ptr& victim, osg::Vec3f& hitPosition) const { - MWWorld::LiveCellRef *ref = - ptr.get(); - const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); - MWMechanics::CreatureStats &stats = getCreatureStats(ptr); - - if (stats.getDrawState() != MWMechanics::DrawState_Weapon) - return; + victim = MWWorld::Ptr(); + hitPosition = osg::Vec3f(); // Get the weapon used (if hand-to-hand, weapon = inv.end()) MWWorld::Ptr weapon; if (hasInventoryStore(ptr)) { - MWWorld::InventoryStore &inv = getInventoryStore(ptr); + MWWorld::InventoryStore& inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weaponslot != inv.end() && weaponslot->getTypeName() == typeid(ESM::Weapon).name()) + if (weaponslot != inv.end() && weaponslot->getType() == ESM::Weapon::sRecordId) weapon = *weaponslot; } - MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength); - - float dist = gmst.find("fCombatDistance")->mValue.getFloat(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Store& store = world->getStore().get(); + float dist = store.find("fCombatDistance")->mValue.getFloat(); if (!weapon.isEmpty()) dist *= weapon.get()->mBase->mData.mReach; - // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. - std::vector targetActors; - stats.getAiSequence().getCombatTargets(targetActors); + const std::pair result = MWMechanics::getHitContact(ptr, dist); + if (result.first.isEmpty()) // Didn't hit anything + return true; - std::pair result = MWBase::Environment::get().getWorld()->getHitContact(ptr, dist, targetActors); - if (result.first.isEmpty()) - return; // Didn't hit anything + // Note that earlier we returned true in spite of an apparent failure to hit anything alive. + // This is because hitting nothing is not a "miss" and should be handled as such character controller-side. + victim = result.first; + hitPosition = result.second; - MWWorld::Ptr victim = result.first; + float hitchance = MWMechanics::getHitChance(ptr, victim, ptr.get()->mBase->mData.mCombat); + return Misc::Rng::roll0to99(world->getPrng()) < hitchance; + } + + void Creature::hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim, + const osg::Vec3f& hitPosition, bool success) const + { + MWMechanics::CreatureStats& stats = getCreatureStats(ptr); + + if (stats.getDrawState() != MWMechanics::DrawState::Weapon) + return; + + MWWorld::Ptr weapon; + if (hasInventoryStore(ptr)) + { + MWWorld::InventoryStore& inv = getInventoryStore(ptr); + MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weaponslot != inv.end() && weaponslot->getType() == ESM::Weapon::sRecordId) + weapon = *weaponslot; + } - if (!victim.getClass().isActor()) - return; // Can't hit non-actors + MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength); - osg::Vec3f hitPosition (result.second); + if (victim.isEmpty()) + return; // Didn't hit anything - float hitchance = MWMechanics::getHitChance(ptr, victim, ref->mBase->mData.mCombat); + const MWWorld::Class& othercls = victim.getClass(); + MWMechanics::CreatureStats& otherstats = othercls.getCreatureStats(victim); + if (otherstats.isDead()) // Can't hit dead actors + return; - if(Misc::Rng::roll0to99() >= hitchance) + if (!success) { - victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false); + victim.getClass().onHit( + victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } - int min,max; + MWWorld::LiveCellRef* ref = ptr.get(); + int min, max; switch (type) { - case 0: - min = ref->mBase->mData.mAttack[0]; - max = ref->mBase->mData.mAttack[1]; - break; - case 1: - min = ref->mBase->mData.mAttack[2]; - max = ref->mBase->mData.mAttack[3]; - break; - case 2: - default: - min = ref->mBase->mData.mAttack[4]; - max = ref->mBase->mData.mAttack[5]; - break; + case 0: + min = ref->mBase->mData.mAttack[0]; + max = ref->mBase->mData.mAttack[1]; + break; + case 1: + min = ref->mBase->mData.mAttack[2]; + max = ref->mBase->mData.mAttack[3]; + break; + case 2: + default: + min = ref->mBase->mData.mAttack[4]; + max = ref->mBase->mData.mAttack[5]; + break; } float damage = min + (max - min) * attackStrength; bool healthdmg = true; if (!weapon.isEmpty()) { - const unsigned char *attack = nullptr; - if(type == ESM::Weapon::AT_Chop) - attack = weapon.get()->mBase->mData.mChop; - else if(type == ESM::Weapon::AT_Slash) - attack = weapon.get()->mBase->mData.mSlash; - else if(type == ESM::Weapon::AT_Thrust) - attack = weapon.get()->mBase->mData.mThrust; - if(attack) + const unsigned char* attack = nullptr; + if (type == ESM::Weapon::AT_Chop) + attack = weapon.get()->mBase->mData.mChop.data(); + else if (type == ESM::Weapon::AT_Slash) + attack = weapon.get()->mBase->mData.mSlash.data(); + else if (type == ESM::Weapon::AT_Thrust) + attack = weapon.get()->mBase->mData.mThrust.data(); + if (attack) { - damage = attack[0] + ((attack[1]-attack[0])*attackStrength); + damage = attack[0] + ((attack[1] - attack[0]) * attackStrength); MWMechanics::adjustWeaponDamage(damage, weapon, ptr); - MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); + MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); } // Apply "On hit" enchanted weapons @@ -322,23 +342,35 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); + victim.getClass().onHit( + victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); } - void Creature::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const + void Creature::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const { MWMechanics::CreatureStats& stats = getCreatureStats(ptr); - // NOTE: 'object' and/or 'attacker' may be empty. + // Self defense + bool setOnPcHitMe = true; + + // NOTE: 'object' and/or 'attacker' may be empty. if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker)) + { stats.setAttacked(true); - // Self defense - bool setOnPcHitMe = true; // Note OnPcHitMe is not set for friendly hits. - - // No retaliation for totally static creatures (they have no movement or attacks anyway) - if (isMobile(ptr) && !attacker.isEmpty()) - setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + // 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); + } + } // Attacker and target store each other as hitattemptactor if they have no one stored yet if (!attacker.isEmpty() && attacker.getClass().isActor()) @@ -346,14 +378,12 @@ namespace MWClass MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker); // First handle the attacked actor if ((stats.getHitAttemptActorId() == -1) - && (statsAttacker.getAiSequence().isInCombat(ptr) - || attacker == MWMechanics::getPlayer())) + && (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer())) stats.setHitAttemptActorId(statsAttacker.getActorId()); // Next handle the attacking actor if ((statsAttacker.getHitAttemptActorId() == -1) - && (statsAttacker.getAiSequence().isInCombat(ptr) - || attacker == MWMechanics::getPlayer())) + && (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer())) statsAttacker.setHitAttemptActorId(stats.getActorId()); } @@ -362,9 +392,9 @@ namespace MWClass if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) { - const std::string &script = ptr.get()->mBase->mScript; + const ESM::RefId& script = ptr.get()->mBase->mScript; /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ - if(!script.empty()) + if (!script.empty()) ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); } @@ -372,7 +402,8 @@ namespace MWClass { // Missed if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) - MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "miss", 1.0f, 1.0f); + MWBase::Environment::get().getSoundManager()->playSound3D( + ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f); return; } @@ -387,16 +418,19 @@ namespace MWClass if (!attacker.isEmpty()) { // Check for knockdown - float agilityTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * getGmst().fKnockDownMult->mValue.getFloat(); + float agilityTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() + * getGmst().fKnockDownMult->mValue.getFloat(); float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() - * getGmst().iKnockDownOddsMult->mValue.getInteger() * 0.01f + getGmst().iKnockDownOddsBase->mValue.getInteger(); - if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99()) + * getGmst().iKnockDownOddsMult->mValue.getInteger() * 0.01f + + getGmst().iKnockDownOddsBase->mValue.getInteger(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng)) stats.setKnockedDown(true); else stats.setHitRecovery(true); // Is this supposed to always occur? } - if(ishealth) + if (ishealth) { damage *= damage / (damage + getArmorRating(ptr)); damage = std::max(1.f, damage); @@ -406,7 +440,8 @@ namespace MWClass MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition); } - MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); + MWBase::Environment::get().getSoundManager()->playSound3D( + ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f); MWMechanics::DynamicStat health(stats.getHealth()); health.setCurrent(health.getCurrent() - damage); @@ -421,84 +456,74 @@ namespace MWClass } } - std::shared_ptr Creature::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Creature::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { - if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfCreature"); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound* sound = store.get().searchRandom("WolfCreature", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); - if(sound) action->setSound(sound->mId); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); + if (sound) + action->setSound(sound->mId); return action; } const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); - - if(stats.isDead()) + if (stats.isDead()) { - bool canLoot = Settings::Manager::getBool ("can loot during death animation", "Game"); - - // by default user can loot friendly actors during death animation - if (canLoot && !stats.getAiSequence().isInCombat()) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); + // by default user can loot non-fighting actors during death animation + if (Settings::game().mCanLootDuringDeathAnimation) + return std::make_unique(ptr); // otherwise wait until death animation - if(stats.isDeathAnimationFinished()) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); + if (stats.isDeathAnimationFinished()) + return std::make_unique(ptr); } - else if (!stats.getAiSequence().isInCombat() && !stats.getKnockedDown()) - return std::shared_ptr(new MWWorld::ActionTalk(ptr)); + else if (!stats.getKnockedDown()) + return std::make_unique(ptr); // Tribunal and some mod companions oddly enough must use open action as fallback if (!getScript(ptr).empty() && ptr.getRefData().getLocals().getIntVar(getScript(ptr), "companion")) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); + return std::make_unique(ptr); - return std::shared_ptr(new MWWorld::FailedAction("")); + return std::make_unique(); } - MWWorld::ContainerStore& Creature::getContainerStore (const MWWorld::Ptr& ptr) const + MWWorld::ContainerStore& Creature::getContainerStore(const MWWorld::Ptr& ptr) const { - ensureCustomData (ptr); - + ensureCustomData(ptr); return *ptr.getRefData().getCustomData()->asCreatureCustomData().mContainerStore; } - MWWorld::InventoryStore& Creature::getInventoryStore(const MWWorld::Ptr &ptr) const + MWWorld::InventoryStore& Creature::getInventoryStore(const MWWorld::Ptr& ptr) const { if (hasInventoryStore(ptr)) - return dynamic_cast(getContainerStore(ptr)); + return static_cast(getContainerStore(ptr)); else throw std::runtime_error("this creature has no inventory store"); } - bool Creature::hasInventoryStore(const MWWorld::Ptr &ptr) const + bool Creature::hasInventoryStore(const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, ESM::Creature::Weapon); } - std::string Creature::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Creature::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - bool Creature::isEssential (const MWWorld::ConstPtr& ptr) const + bool Creature::isEssential(const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, ESM::Creature::Essential); } - void Creature::registerSelf() - { - std::shared_ptr instance (new Creature); - - registerClass (typeid (ESM::Creature).name(), instance); - } - - float Creature::getMaxSpeed(const MWWorld::Ptr &ptr) const + float Creature::getMaxSpeed(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); @@ -507,25 +532,27 @@ namespace MWClass const GMST& gmst = getGmst(); - const MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects(); + const MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); float moveSpeed; - if(getEncumbrance(ptr) > getCapacity(ptr)) + if (getEncumbrance(ptr) > getCapacity(ptr)) moveSpeed = 0.0f; - else if(canFly(ptr) || (mageffects.get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && - world->isLevitationEnabled())) + else if (canFly(ptr) + || (mageffects.getOrDefault(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled())) { - float flySpeed = 0.01f*(stats.getAttribute(ESM::Attribute::Speed).getModified() + - mageffects.get(ESM::MagicEffect::Levitate).getMagnitude()); - flySpeed = gmst.fMinFlySpeed->mValue.getFloat() + flySpeed*(gmst.fMaxFlySpeed->mValue.getFloat() - gmst.fMinFlySpeed->mValue.getFloat()); + float flySpeed = 0.01f + * (stats.getAttribute(ESM::Attribute::Speed).getModified() + + 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; } - else if(world->isSwimming(ptr)) + else if (world->isSwimming(ptr)) moveSpeed = getSwimSpeed(ptr); else moveSpeed = getWalkSpeed(ptr); @@ -533,9 +560,9 @@ namespace MWClass return moveSpeed; } - MWMechanics::Movement& Creature::getMovementSettings (const MWWorld::Ptr& ptr) const + MWMechanics::Movement& Creature::getMovementSettings(const MWWorld::Ptr& ptr) const { - ensureCustomData (ptr); + ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asCreatureCustomData().mMovement; } @@ -550,65 +577,65 @@ namespace MWClass if (customData.mCreatureStats.isDead() && customData.mCreatureStats.isDeathAnimationFinished()) return true; - return !customData.mCreatureStats.getAiSequence().isInCombat(); + const MWMechanics::AiSequence& aiSeq = customData.mCreatureStats.getAiSequence(); + return !aiSeq.isInCombat() || aiSeq.isFleeing(); } - MWGui::ToolTipInfo Creature::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Creature::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); - std::string text; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); - info.text = text; + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); return info; } - float Creature::getArmorRating (const MWWorld::Ptr& ptr) const + float Creature::getArmorRating(const MWWorld::Ptr& ptr) const { // Equipment armor rating is deliberately ignored. - return getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Shield).getMagnitude(); + return getCreatureStats(ptr).getMagicEffects().getOrDefault(ESM::MagicEffect::Shield).getMagnitude(); } - float Creature::getCapacity (const MWWorld::Ptr& ptr) const + float Creature::getCapacity(const MWWorld::Ptr& ptr) const { - const MWMechanics::CreatureStats& stats = getCreatureStats (ptr); + const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); return stats.getAttribute(ESM::Attribute::Strength).getModified() * 5; } - int Creature::getServices(const MWWorld::ConstPtr &actor) const + int Creature::getServices(const MWWorld::ConstPtr& actor) const { return actor.get()->mBase->mAiData.mServices; } - bool Creature::isPersistent(const MWWorld::ConstPtr &actor) const + bool Creature::isPersistent(const MWWorld::ConstPtr& actor) const { const MWWorld::LiveCellRef* ref = actor.get(); - return ref->mBase->mPersistent; + return (ref->mBase->mRecordFlags & ESM::FLAG_Persistent) != 0; } - std::string Creature::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const + ESM::RefId Creature::getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const { int type = getSndGenTypeFromName(ptr, name); if (type < 0) - return std::string(); + return ESM::RefId(); std::vector sounds; std::vector fallbacksounds; MWWorld::LiveCellRef* ref = ptr.get(); - const std::string& ourId = (ref->mBase->mOriginal.empty()) ? ptr.getCellRef().getRefId() : ref->mBase->mOriginal; + const ESM::RefId& ourId = (ref->mBase->mOriginal.empty()) ? ptr.getCellRef().getRefId() : ref->mBase->mOriginal; - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); auto sound = store.get().begin(); while (sound != store.get().end()) { - if (type == sound->mType && !sound->mCreature.empty() && Misc::StringUtils::ciEqual(ourId, sound->mCreature)) + if (type == sound->mType && !sound->mCreature.empty() && ourId == sound->mCreature) sounds.push_back(&*sound); if (type == sound->mType && sound->mCreature.empty()) fallbacksounds.push_back(&*sound); @@ -617,20 +644,19 @@ namespace MWClass if (sounds.empty()) { - const std::string model = getModel(ptr); + const std::string_view model = getModel(ptr); if (!model.empty()) { - for (const ESM::Creature &creature : store.get()) + for (const ESM::Creature& creature : store.get()) { if (creature.mId != ourId && creature.mOriginal != ourId && !creature.mModel.empty() - && Misc::StringUtils::ciEqual(model, "meshes\\" + creature.mModel)) + && Misc::StringUtils::ciEqual(model, creature.mModel)) { - const std::string& fallbackId = !creature.mOriginal.empty() ? creature.mOriginal : creature.mId; + const ESM::RefId& fallbackId = !creature.mOriginal.empty() ? creature.mOriginal : creature.mId; sound = store.get().begin(); while (sound != store.get().end()) { - if (type == sound->mType && !sound->mCreature.empty() - && Misc::StringUtils::ciEqual(fallbackId, sound->mCreature)) + if (type == sound->mType && !sound->mCreature.empty() && fallbackId == sound->mCreature) sounds.push_back(&*sound); ++sound; } @@ -640,143 +666,151 @@ namespace MWClass } } + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (!sounds.empty()) - return sounds[Misc::Rng::rollDice(sounds.size())]->mSound; + return sounds[Misc::Rng::rollDice(sounds.size(), prng)]->mSound; if (!fallbacksounds.empty()) - return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound; + return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size(), prng)]->mSound; - return std::string(); + return ESM::RefId(); } - MWWorld::Ptr Creature::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Creature::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - return MWWorld::Ptr(cell.insert(ref), &cell); + const MWWorld::LiveCellRef* ref = ptr.get(); + MWWorld::Ptr newPtr(cell.insert(ref), &cell); + if (newPtr.getRefData().getCustomData()) + { + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); + newPtr.getClass().getContainerStore(newPtr).setPtr(newPtr); + } + return newPtr; } - bool Creature::isBipedal(const MWWorld::ConstPtr &ptr) const + bool Creature::isBipedal(const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, ESM::Creature::Bipedal); } - bool Creature::canFly(const MWWorld::ConstPtr &ptr) const + bool Creature::canFly(const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, ESM::Creature::Flies); } - bool Creature::canSwim(const MWWorld::ConstPtr &ptr) const + bool Creature::canSwim(const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, static_cast(ESM::Creature::Swims | ESM::Creature::Bipedal)); } - bool Creature::canWalk(const MWWorld::ConstPtr &ptr) const + bool Creature::canWalk(const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, static_cast(ESM::Creature::Walks | ESM::Creature::Bipedal)); } - int Creature::getSndGenTypeFromName(const MWWorld::Ptr &ptr, const std::string &name) + int Creature::getSndGenTypeFromName(const MWWorld::Ptr& ptr, std::string_view name) { - if(name == "left") + if (name == "left") { - MWBase::World *world = MWBase::Environment::get().getWorld(); - if(world->isFlying(ptr)) + MWBase::World* world = MWBase::Environment::get().getWorld(); + if (world->isFlying(ptr)) return -1; osg::Vec3f pos(ptr.getRefData().getPosition().asVec3()); - if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) + if (world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) return ESM::SoundGenerator::SwimLeft; - if(world->isOnGround(ptr)) + if (world->isOnGround(ptr)) return ESM::SoundGenerator::LeftFoot; return -1; } - if(name == "right") + if (name == "right") { - MWBase::World *world = MWBase::Environment::get().getWorld(); - if(world->isFlying(ptr)) + MWBase::World* world = MWBase::Environment::get().getWorld(); + if (world->isFlying(ptr)) return -1; osg::Vec3f pos(ptr.getRefData().getPosition().asVec3()); - if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) + if (world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) return ESM::SoundGenerator::SwimRight; - if(world->isOnGround(ptr)) + if (world->isOnGround(ptr)) return ESM::SoundGenerator::RightFoot; return -1; } - if(name == "swimleft") + if (name == "swimleft") return ESM::SoundGenerator::SwimLeft; - if(name == "swimright") + if (name == "swimright") return ESM::SoundGenerator::SwimRight; - if(name == "moan") + if (name == "moan") return ESM::SoundGenerator::Moan; - if(name == "roar") + if (name == "roar") return ESM::SoundGenerator::Roar; - if(name == "scream") + if (name == "scream") return ESM::SoundGenerator::Scream; - if(name == "land") + if (name == "land") return ESM::SoundGenerator::Land; - throw std::runtime_error(std::string("Unexpected soundgen type: ")+name); + throw std::runtime_error("Unexpected soundgen type: " + std::string(name)); } - float Creature::getSkill(const MWWorld::Ptr &ptr, int skill) const + float Creature::getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const { - MWWorld::LiveCellRef *ref = - ptr.get(); + MWWorld::LiveCellRef* ref = ptr.get(); - const ESM::Skill* skillRecord = MWBase::Environment::get().getWorld()->getStore().get().find(skill); + const ESM::Skill* skillRecord = MWBase::Environment::get().getESMStore()->get().find(id); switch (skillRecord->mData.mSpecialization) { - case ESM::Class::Combat: - return ref->mBase->mData.mCombat; - case ESM::Class::Magic: - return ref->mBase->mData.mMagic; - case ESM::Class::Stealth: - return ref->mBase->mData.mStealth; - default: - throw std::runtime_error("invalid specialisation"); + case ESM::Class::Combat: + return ref->mBase->mData.mCombat; + case ESM::Class::Magic: + return ref->mBase->mData.mMagic; + case ESM::Class::Stealth: + return ref->mBase->mData.mStealth; + default: + throw std::runtime_error("invalid specialisation"); } } - int Creature::getBloodTexture(const MWWorld::ConstPtr &ptr) const + int Creature::getBloodTexture(const MWWorld::ConstPtr& ptr) const { return ptr.get()->mBase->mBloodType; } - void Creature::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) - const + void Creature::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; - if (state.mVersion > 0) + const ESM::CreatureState& creatureState = state.asCreatureState(); + + if (!ptr.getRefData().getCustomData()) { - if (!ptr.getRefData().getCustomData()) + if (creatureState.mCreatureStats.mMissingACDT) + ensureCustomData(ptr); + else { // Create a CustomData, but don't fill it from ESM records (not needed) - std::unique_ptr data (new CreatureCustomData); + auto data = std::make_unique(); if (hasInventoryStore(ptr)) data->mContainerStore = std::make_unique(); else data->mContainerStore = std::make_unique(); - ptr.getRefData().setCustomData (std::move(data)); + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + data->mContainerStore->setPtr(ptr); + + ptr.getRefData().setCustomData(std::move(data)); } } - else - ensureCustomData(ptr); // in openmw 0.30 savegames not all state was saved yet, so need to load it regardless. CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); - const ESM::CreatureState& creatureState = state.asCreatureState(); - customData.mContainerStore->readState (creatureState.mInventory); + + customData.mContainerStore->readState(creatureState.mInventory); bool spellsInitialised = customData.mCreatureStats.getSpells().setSpells(ptr.get()->mBase->mId); - if(spellsInitialised) + if (spellsInitialised) customData.mCreatureStats.getSpells().clear(); - customData.mCreatureStats.readState (creatureState.mCreatureStats); + customData.mCreatureStats.readState(creatureState.mCreatureStats); } - void Creature::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) - const + void Creature::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { @@ -784,16 +818,17 @@ namespace MWClass return; } - if (ptr.getRefData().getCount() <= 0) + const CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); + if (ptr.getCellRef().getCount() <= 0 + && (!isFlagBitSet(ptr, ESM::Creature::Respawn) || !customData.mCreatureStats.isDead())) { state.mHasCustomState = false; return; } - const CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); ESM::CreatureState& creatureState = state.asCreatureState(); - customData.mContainerStore->writeState (creatureState.mInventory); - customData.mCreatureStats.writeState (creatureState.mCreatureStats); + customData.mContainerStore->writeState(creatureState.mInventory); + customData.mCreatureStats.writeState(creatureState.mCreatureStats); } int Creature::getBaseGold(const MWWorld::ConstPtr& ptr) const @@ -801,63 +836,66 @@ namespace MWClass return ptr.get()->mBase->mData.mGold; } - void Creature::respawn(const MWWorld::Ptr &ptr) const + void Creature::respawn(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr); - if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) + if (ptr.getCellRef().getCount() > 0 && !creatureStats.isDead()) return; if (!creatureStats.isDeathAnimationFinished()) return; - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->mValue.getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->mValue.getFloat(); - float delay = ptr.getRefData().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); + float delay + = ptr.getCellRef().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (isFlagBitSet(ptr, ESM::Creature::Respawn) - && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) + && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) { if (ptr.getCellRef().hasContentFile()) { - if (ptr.getRefData().getCount() == 0) + if (ptr.getCellRef().getCount() == 0) { - ptr.getRefData().setCount(1); - const std::string& script = getScript(ptr); - if(!script.empty()) + ptr.getCellRef().setCount(1); + const ESM::RefId& script = getScript(ptr); + if (!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, ptr); } MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); + MWBase::Environment::get().getWindowManager()->onDeleteCustomData(ptr); ptr.getRefData().setCustomData(nullptr); // Reset to original position - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], - ptr.getCellRef().getPosition().pos[1], - ptr.getCellRef().getPosition().pos[2]); + MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->rotateObject( + ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } } } - int Creature::getBaseFightRating(const MWWorld::ConstPtr &ptr) const + int Creature::getBaseFightRating(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mAiData.mFight; } - void Creature::adjustScale(const MWWorld::ConstPtr &ptr, osg::Vec3f &scale, bool /* rendering */) const + void Creature::adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool /* rendering */) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); scale *= ref->mBase->mScale; } - void Creature::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const + void Creature::setBaseAISetting(const ESM::RefId& id, MWMechanics::AiSetting setting, int value) const { MWMechanics::setBaseAISetting(id, setting, value); } - void Creature::modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const + void Creature::modifyBaseInventory(const ESM::RefId& actorId, const ESM::RefId& itemId, int amount) const { MWMechanics::modifyBaseInventory(actorId, itemId, amount); } @@ -868,8 +906,8 @@ namespace MWClass const GMST& gmst = getGmst(); return gmst.fMinWalkSpeedCreature->mValue.getFloat() - + 0.01f * stats.getAttribute(ESM::Attribute::Speed).getModified() - * (gmst.fMaxWalkSpeedCreature->mValue.getFloat() - gmst.fMinWalkSpeedCreature->mValue.getFloat()); + + 0.01f * stats.getAttribute(ESM::Attribute::Speed).getModified() + * (gmst.fMaxWalkSpeedCreature->mValue.getFloat() - gmst.fMinWalkSpeedCreature->mValue.getFloat()); } float Creature::getRunSpeed(const MWWorld::Ptr& ptr) const @@ -880,12 +918,8 @@ namespace MWClass float Creature::getSwimSpeed(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); - const GMST& gmst = getGmst(); const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); - return getWalkSpeed(ptr) - * (1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude()) - * (gmst.fSwimRunBase->mValue.getFloat() - + 0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->mValue.getFloat()); + return getSwimSpeedImpl(ptr, getGmst(), mageffects, getWalkSpeed(ptr)); } } diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index 5bb50303482..b8619128c24 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -1,6 +1,8 @@ #ifndef GAME_MWCLASS_CREATURE_H #define GAME_MWCLASS_CREATURE_H +#include "../mwworld/registeredclass.hpp" + #include "actor.hpp" namespace ESM @@ -10,133 +12,139 @@ namespace ESM namespace MWClass { - class Creature : public Actor + class Creature : public MWWorld::RegisteredClass { - void ensureCustomData (const MWWorld::Ptr& ptr) const; + friend MWWorld::RegisteredClass; + + Creature(); - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + void ensureCustomData(const MWWorld::Ptr& ptr) const; - static int getSndGenTypeFromName(const MWWorld::Ptr &ptr, const std::string &name); + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - // cached GMSTs - struct GMST - { - const ESM::GameSetting *fMinWalkSpeedCreature; - const ESM::GameSetting *fMaxWalkSpeedCreature; - const ESM::GameSetting *fEncumberedMoveEffect; - const ESM::GameSetting *fSneakSpeedMultiplier; - const ESM::GameSetting *fAthleticsRunBonus; - const ESM::GameSetting *fBaseRunMultiplier; - const ESM::GameSetting *fMinFlySpeed; - const ESM::GameSetting *fMaxFlySpeed; - const ESM::GameSetting *fSwimRunBase; - const ESM::GameSetting *fSwimRunAthleticsMult; - const ESM::GameSetting *fKnockDownMult; - const ESM::GameSetting *iKnockDownOddsMult; - const ESM::GameSetting *iKnockDownOddsBase; - }; + static int getSndGenTypeFromName(const MWWorld::Ptr& ptr, std::string_view name); - static const GMST& getGmst(); + // cached GMSTs + struct GMST + { + const ESM::GameSetting* fMinWalkSpeedCreature; + const ESM::GameSetting* fMaxWalkSpeedCreature; + const ESM::GameSetting* fEncumberedMoveEffect; + const ESM::GameSetting* fSneakSpeedMultiplier; + const ESM::GameSetting* fAthleticsRunBonus; + const ESM::GameSetting* fBaseRunMultiplier; + const ESM::GameSetting* fMinFlySpeed; + const ESM::GameSetting* fMaxFlySpeed; + const ESM::GameSetting* fSwimRunBase; + const ESM::GameSetting* fSwimRunAthleticsMult; + const ESM::GameSetting* fKnockDownMult; + const ESM::GameSetting* iKnockDownOddsMult; + const ESM::GameSetting* iKnockDownOddsBase; + }; - public: + static const GMST& getGmst(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; - ///< @return true if this object has a tooltip when focused (default implementation: true) + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; + ///< @return true if this object has a tooltip when focused (default implementation: true) - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - MWMechanics::CreatureStats& getCreatureStats (const MWWorld::Ptr& ptr) const override; - ///< Return creature stats + MWMechanics::CreatureStats& getCreatureStats(const MWWorld::Ptr& ptr) const override; + ///< Return creature stats - void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const override; + bool evaluateHit(const MWWorld::Ptr& ptr, MWWorld::Ptr& victim, osg::Vec3f& hitPosition) const override; - void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const override; + void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim, + const osg::Vec3f& hitPosition, bool success) const override; - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const override; - MWWorld::ContainerStore& getContainerStore ( - const MWWorld::Ptr& ptr) const override; - ///< Return container store + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - MWWorld::InventoryStore& getInventoryStore (const MWWorld::Ptr& ptr) const override; - ///< Return inventory store + MWWorld::ContainerStore& getContainerStore(const MWWorld::Ptr& ptr) const override; + ///< Return container store - bool hasInventoryStore (const MWWorld::Ptr &ptr) const override; + MWWorld::InventoryStore& getInventoryStore(const MWWorld::Ptr& ptr) const override; + ///< Return inventory store - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + bool hasInventoryStore(const MWWorld::ConstPtr& ptr) const override; - float getCapacity (const MWWorld::Ptr& ptr) const override; - ///< Return total weight that fits into the object. Throws an exception, if the object can't - /// hold other objects. + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - float getArmorRating (const MWWorld::Ptr& ptr) const override; - ///< @return combined armor rating of this actor + float getCapacity(const MWWorld::Ptr& ptr) const override; + ///< Return total weight that fits into the object. Throws an exception, if the object can't + /// hold other objects. - bool isEssential (const MWWorld::ConstPtr& ptr) const override; - ///< Is \a ptr essential? (i.e. may losing \a ptr make the game unwinnable) + float getArmorRating(const MWWorld::Ptr& ptr) const override; + ///< @return combined armor rating of this actor - int getServices (const MWWorld::ConstPtr& actor) const override; + bool isEssential(const MWWorld::ConstPtr& ptr) const override; + ///< Is \a ptr essential? (i.e. may losing \a ptr make the game unwinnable) - bool isPersistent (const MWWorld::ConstPtr& ptr) const override; + int getServices(const MWWorld::ConstPtr& actor) const override; - std::string getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const override; + bool isPersistent(const MWWorld::ConstPtr& ptr) const override; - MWMechanics::Movement& getMovementSettings (const MWWorld::Ptr& ptr) const override; - ///< Return desired movement. + ESM::RefId getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const override; - float getMaxSpeed (const MWWorld::Ptr& ptr) const override; + MWMechanics::Movement& getMovementSettings(const MWWorld::Ptr& ptr) const override; + ///< Return desired movement. - static void registerSelf(); + float getMaxSpeed(const MWWorld::Ptr& ptr) const override; - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; - ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel(). + void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const override; + ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: + ///< list getModel(). - bool isBipedal (const MWWorld::ConstPtr &ptr) const override; - bool canFly (const MWWorld::ConstPtr &ptr) const override; - bool canSwim (const MWWorld::ConstPtr &ptr) const override; - bool canWalk (const MWWorld::ConstPtr &ptr) const override; + bool isBipedal(const MWWorld::ConstPtr& ptr) const override; + bool canFly(const MWWorld::ConstPtr& ptr) const override; + bool canSwim(const MWWorld::ConstPtr& ptr) const override; + bool canWalk(const MWWorld::ConstPtr& ptr) const override; - float getSkill(const MWWorld::Ptr &ptr, int skill) const override; + float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override; - /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) - int getBloodTexture (const MWWorld::ConstPtr& ptr) const override; + /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) + int getBloodTexture(const MWWorld::ConstPtr& ptr) const override; - void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; - ///< Read additional state from \a state into \a ptr. + void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; + ///< Read additional state from \a state into \a ptr. - void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; - ///< Write additional state from \a ptr into \a state. + void writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; + ///< Write additional state from \a ptr into \a state. - int getBaseGold(const MWWorld::ConstPtr& ptr) const override; + int getBaseGold(const MWWorld::ConstPtr& ptr) const override; - void respawn (const MWWorld::Ptr& ptr) const override; + void respawn(const MWWorld::Ptr& ptr) const override; - int getBaseFightRating(const MWWorld::ConstPtr &ptr) const override; + int getBaseFightRating(const MWWorld::ConstPtr& ptr) const override; - void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const override; - /// @param rendering Indicates if the scale to adjust is for the rendering mesh, or for the collision mesh + void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const override; + /// @param rendering Indicates if the scale to adjust is for the rendering mesh, or for the collision mesh - void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const override; + void setBaseAISetting(const ESM::RefId& id, MWMechanics::AiSetting setting, int value) const override; - void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const override; + void modifyBaseInventory(const ESM::RefId& actorId, const ESM::RefId& itemId, int amount) const override; - float getWalkSpeed(const MWWorld::Ptr& ptr) const override; + float getWalkSpeed(const MWWorld::Ptr& ptr) const override; - float getRunSpeed(const MWWorld::Ptr& ptr) const override; + float getRunSpeed(const MWWorld::Ptr& ptr) const override; - float getSwimSpeed(const MWWorld::Ptr& ptr) const override; + float getSwimSpeed(const MWWorld::Ptr& ptr) const override; }; } diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp index f86004c619f..f16601531da 100644 --- a/apps/openmw/mwclass/creaturelevlist.cpp +++ b/apps/openmw/mwclass/creaturelevlist.cpp @@ -1,13 +1,20 @@ #include "creaturelevlist.hpp" -#include -#include +#include +#include #include "../mwmechanics/levelledlist.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/customdata.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/manualref.hpp" + #include "../mwmechanics/creaturestats.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + namespace MWClass { class CreatureLevListCustomData : public MWWorld::TypedCustomData @@ -17,19 +24,38 @@ namespace MWClass int mSpawnActorId; bool mSpawn; // Should a new creature be spawned? - CreatureLevListCustomData& asCreatureLevListCustomData() override - { - return *this; - } - const CreatureLevListCustomData& asCreatureLevListCustomData() const override - { - return *this; - } + CreatureLevListCustomData& asCreatureLevListCustomData() override { return *this; } + const CreatureLevListCustomData& asCreatureLevListCustomData() const override { return *this; } }; - std::string CreatureLevList::getName (const MWWorld::ConstPtr& ptr) const + CreatureLevList::CreatureLevList() + : MWWorld::RegisteredClass(ESM::CreatureLevList::sRecordId) + { + } + + MWWorld::Ptr CreatureLevList::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - return ""; + const MWWorld::LiveCellRef* ref = ptr.get(); + + return MWWorld::Ptr(cell.insert(ref), &cell); + } + + void CreatureLevList::adjustPosition(const MWWorld::Ptr& ptr, bool force) const + { + if (ptr.getRefData().getCustomData() == nullptr) + return; + + CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); + MWWorld::Ptr creature = (customData.mSpawnActorId == -1) + ? MWWorld::Ptr() + : MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + if (!creature.isEmpty()) + MWBase::Environment::get().getWorld()->adjustPosition(creature, force); + } + + std::string_view CreatureLevList::getName(const MWWorld::ConstPtr& ptr) const + { + return {}; } bool CreatureLevList::hasToolTip(const MWWorld::ConstPtr& ptr) const @@ -37,7 +63,7 @@ namespace MWClass return false; } - void CreatureLevList::respawn(const MWWorld::Ptr &ptr) const + void CreatureLevList::respawn(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); @@ -45,15 +71,22 @@ namespace MWClass if (customData.mSpawn) return; - MWWorld::Ptr creature = (customData.mSpawnActorId == -1) ? MWWorld::Ptr() : MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + MWWorld::Ptr creature; + if (customData.mSpawnActorId != -1) + { + creature = MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + if (creature.isEmpty()) + creature = ptr.getCell()->getMovedActor(customData.mSpawnActorId); + } if (!creature.isEmpty()) { const MWMechanics::CreatureStats& creatureStats = creature.getClass().getCreatureStats(creature); - if (creature.getRefData().getCount() == 0) + if (creature.getCellRef().getCount() == 0) customData.mSpawn = true; else if (creatureStats.isDead()) { - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->mValue.getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->mValue.getFloat(); @@ -66,32 +99,8 @@ namespace MWClass customData.mSpawn = true; } - void CreatureLevList::registerSelf() - { - std::shared_ptr instance (new CreatureLevList); - - registerClass (typeid (ESM::CreatureLevList).name(), instance); - } - - void CreatureLevList::getModelsToPreload(const MWWorld::Ptr &ptr, std::vector &models) const - { - // disable for now, too many false positives - /* - const MWWorld::LiveCellRef *ref = ptr.get(); - for (std::vector::const_iterator it = ref->mBase->mList.begin(); it != ref->mBase->mList.end(); ++it) - { - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - if (it->mLevel > player.getClass().getCreatureStats(player).getLevel()) - continue; - - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - MWWorld::ManualRef ref(store, it->mId); - ref.getPtr().getClass().getModelsToPreload(ref.getPtr(), models); - } - */ - } - - void CreatureLevList::insertObjectRendering(const MWWorld::Ptr &ptr, const std::string& model, MWRender::RenderingInterface &renderingInterface) const + void CreatureLevList::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { ensureCustomData(ptr); @@ -99,27 +108,28 @@ namespace MWClass if (!customData.mSpawn) return; - MWWorld::LiveCellRef *ref = - ptr.get(); - - std::string id = MWMechanics::getLevelledItem(ref->mBase, true); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::RefId& id = MWMechanics::getLevelledItem( + store.get().find(ptr.getCellRef().getRefId()), true, prng); if (!id.empty()) { // Delete the previous creature if (customData.mSpawnActorId != -1) { - MWWorld::Ptr creature = MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + MWWorld::Ptr creature + = MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); if (!creature.isEmpty()) MWBase::Environment::get().getWorld()->deleteObject(creature); customData.mSpawnActorId = -1; } - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); MWWorld::ManualRef manualRef(store, id); manualRef.getPtr().getCellRef().setPosition(ptr.getCellRef().getPosition()); manualRef.getPtr().getCellRef().setScale(ptr.getCellRef().getScale()); - MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject(manualRef.getPtr(), ptr.getCell() , ptr.getCellRef().getPosition()); + MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject( + manualRef.getPtr(), ptr.getCell(), ptr.getRefData().getPosition()); customData.mSpawnActorId = placed.getClass().getCreatureStats(placed).getActorId(); customData.mSpawn = false; } @@ -127,7 +137,7 @@ namespace MWClass customData.mSpawn = false; } - void CreatureLevList::ensureCustomData(const MWWorld::Ptr &ptr) const + void CreatureLevList::ensureCustomData(const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { @@ -139,8 +149,7 @@ namespace MWClass } } - void CreatureLevList::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) - const + void CreatureLevList::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; @@ -152,8 +161,7 @@ namespace MWClass customData.mSpawn = levListState.mSpawn; } - void CreatureLevList::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) - const + void CreatureLevList::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { diff --git a/apps/openmw/mwclass/creaturelevlist.hpp b/apps/openmw/mwclass/creaturelevlist.hpp index 35152a94227..ded8f77de55 100644 --- a/apps/openmw/mwclass/creaturelevlist.hpp +++ b/apps/openmw/mwclass/creaturelevlist.hpp @@ -1,37 +1,40 @@ #ifndef GAME_MWCLASS_CREATURELEVLIST_H #define GAME_MWCLASS_CREATURELEVLIST_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class CreatureLevList : public MWWorld::Class + class CreatureLevList : public MWWorld::RegisteredClass { - void ensureCustomData (const MWWorld::Ptr& ptr) const; + friend MWWorld::RegisteredClass; - public: + CreatureLevList(); - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + void ensureCustomData(const MWWorld::Ptr& ptr) const; - bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; - ///< @return true if this object has a tooltip when focused (default implementation: true) + public: + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - static void registerSelf(); + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; + ///< @return true if this object has a tooltip when focused (default implementation: true) - void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; - ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel(). + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; + ///< Read additional state from \a state into \a ptr. - void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; - ///< Read additional state from \a state into \a ptr. + void writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; + ///< Write additional state from \a ptr into \a state. - void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; - ///< Write additional state from \a ptr into \a state. + void respawn(const MWWorld::Ptr& ptr) const override; - void respawn (const MWWorld::Ptr& ptr) const override; + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; + + void adjustPosition(const MWWorld::Ptr& ptr, bool force) const override; }; } diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index 3a5ff0d9aec..e31da8415df 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -1,34 +1,42 @@ #include "door.hpp" -#include -#include +#include +#include + +#include +#include +#include #include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/ptr.hpp" -#include "../mwworld/failedaction.hpp" -#include "../mwworld/actionteleport.hpp" -#include "../mwworld/actiondoor.hpp" -#include "../mwworld/cellstore.hpp" -#include "../mwworld/esmstore.hpp" #include "../mwphysics/physicssystem.hpp" -#include "../mwworld/inventorystore.hpp" +#include "../mwworld/actiondoor.hpp" +#include "../mwworld/actionteleport.hpp" #include "../mwworld/actiontrap.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/containerstore.hpp" #include "../mwworld/customdata.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/failedaction.hpp" +#include "../mwworld/ptr.hpp" +#include "../mwworld/worldmodel.hpp" #include "../mwgui/tooltips.hpp" +#include "../mwrender/animation.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" -#include "../mwrender/animation.hpp" #include "../mwrender/vismask.hpp" #include "../mwmechanics/actorutil.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { class DoorCustomData : public MWWorld::TypedCustomData @@ -36,29 +44,29 @@ namespace MWClass public: MWWorld::DoorState mDoorState = MWWorld::DoorState::Idle; - DoorCustomData& asDoorCustomData() override - { - return *this; - } - const DoorCustomData& asDoorCustomData() const override - { - return *this; - } + DoorCustomData& asDoorCustomData() override { return *this; } + const DoorCustomData& asDoorCustomData() const override { return *this; } }; - void Door::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + Door::Door() + : MWWorld::RegisteredClass(ESM::Door::sRecordId) + { + } + + void Door::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model, true); + renderingInterface.getObjects().insertModel(ptr, model); ptr.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Static); } } - void Door::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Door::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const { - if(!model.empty()) - physics.addObject(ptr, model, MWPhysics::CollisionType_Door); + insertObjectPhysics(ptr, model, rotation, physics); // Resume the door's opening/closing animation if it wasn't finished if (ptr.getRefData().getCustomData()) @@ -71,6 +79,12 @@ namespace MWClass } } + void Door::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const + { + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_Door); + } + bool Door::isDoor() const { return true; @@ -81,66 +95,56 @@ namespace MWClass return true; } - std::string Door::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Door::getModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } - std::string Door::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Door::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - std::shared_ptr Door::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Door::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { - MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &openSound = ref->mBase->mOpenSound; - const std::string &closeSound = ref->mBase->mCloseSound; - const std::string lockedSound = "LockedDoor"; - const std::string trapActivationSound = "Disarm Trap Fail"; + MWWorld::LiveCellRef* ref = ptr.get(); - MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); + const ESM::RefId& openSound = ref->mBase->mOpenSound; + const ESM::RefId& closeSound = ref->mBase->mCloseSound; + const ESM::RefId lockedSound = ESM::RefId::stringRefId("LockedDoor"); - bool isLocked = ptr.getCellRef().getLockLevel() > 0; - bool isTrapped = !ptr.getCellRef().getTrap().empty(); - bool hasKey = false; - std::string keyName; - - // FIXME: If NPC activate teleporting door, it can lead to crash due to iterator invalidation in the Actors update. - // Make such activation a no-op for now, like how it is in the vanilla game. + // FIXME: If NPC activate teleporting door, it can lead to crash due to iterator invalidation in the Actors + // update. Make such activation a no-op for now, like how it is in the vanilla game. if (actor != MWMechanics::getPlayer() && ptr.getCellRef().getTeleport()) { - std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); + std::unique_ptr action = std::make_unique(std::string_view{}, ptr); action->setSound(lockedSound); return action; } // make door glow if player activates it with telekinesis - if (actor == MWMechanics::getPlayer() && - MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > - MWBase::Environment::get().getWorld()->getMaxActivationDistance()) + if (actor == MWMechanics::getPlayer() + && MWBase::Environment::get().getWorld()->getDistanceToFacedObject() + > MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if (animation) + { + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const ESM::MagicEffect* effect = store.get().find(ESM::MagicEffect::Telekinesis); - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - int index = ESM::MagicEffect::effectStringToId("sEffectTelekinesis"); - const ESM::MagicEffect *effect = store.get().find(index); - - animation->addSpellCastGlow(effect, 1); // 1 second glow to match the time taken for a door opening or closing + animation->addSpellCastGlow( + effect->getColor(), 1); // 1 second glow to match the time taken for a door opening or closing + } } - const std::string keyId = ptr.getCellRef().getKey(); + MWWorld::ContainerStore& invStore = actor.getClass().getContainerStore(actor); + + bool isLocked = ptr.getCellRef().isLocked(); + bool isTrapped = !ptr.getCellRef().getTrap().empty(); + bool hasKey = false; + std::string_view keyName; + const ESM::RefId& keyId = ptr.getCellRef().getKey(); if (!keyId.empty()) { MWWorld::Ptr keyPtr = invStore.search(keyId); @@ -153,39 +157,43 @@ namespace MWClass if (isLocked && hasKey) { - if(actor == MWMechanics::getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox(keyName + " #{sKeyUsed}"); - ptr.getCellRef().unlock(); //Call the function here. because that makes sense. + if (actor == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox(std::string{ keyName } + " #{sKeyUsed}"); + ptr.getCellRef().unlock(); // Call the function here. because that makes sense. // using a key disarms the trap - if(isTrapped) + if (isTrapped) { - ptr.getCellRef().setTrap(""); - MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Disarm Trap", 1.0f, 1.0f); + ptr.getCellRef().setTrap(ESM::RefId()); + MWBase::Environment::get().getSoundManager()->playSound3D( + ptr, ESM::RefId::stringRefId("Disarm Trap"), 1.0f, 1.0f); isTrapped = false; } } if (!isLocked || hasKey) { - if(isTrapped) + if (isTrapped) { // Trap activation - std::shared_ptr action(new MWWorld::ActionTrap(ptr.getCellRef().getTrap(), ptr)); - action->setSound(trapActivationSound); + std::unique_ptr action + = std::make_unique(ptr.getCellRef().getTrap(), ptr); + action->setSound(ESM::RefId::stringRefId("Disarm Trap Fail")); return action; } if (ptr.getCellRef().getTeleport()) { - if (actor == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > MWBase::Environment::get().getWorld()->getMaxActivationDistance()) + if (actor == MWMechanics::getPlayer() + && MWBase::Environment::get().getWorld()->getDistanceToFacedObject() + > MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { // player activated teleport door with telekinesis - std::shared_ptr action(new MWWorld::FailedAction); - return action; + return std::make_unique(); } else { - std::shared_ptr action(new MWWorld::ActionTeleport (ptr.getCellRef().getDestCell(), ptr.getCellRef().getDoorDest(), true)); + std::unique_ptr action = std::make_unique( + ptr.getCellRef().getDestCell(), ptr.getCellRef().getDoorDest(), true); action->setSound(openSound); return action; } @@ -193,7 +201,7 @@ namespace MWClass else { // animated door - std::shared_ptr action(new MWWorld::ActionDoor(ptr)); + std::unique_ptr action = std::make_unique(ptr); const auto doorState = getDoorState(ptr); bool opening = true; float doorRot = ptr.getRefData().getPosition().rot[2] - ptr.getCellRef().getPosition().rot[2]; @@ -204,19 +212,17 @@ namespace MWClass if (opening) { - MWBase::Environment::get().getSoundManager()->fadeOutSound3D(ptr, - closeSound, 0.5f); + MWBase::Environment::get().getSoundManager()->fadeOutSound3D(ptr, closeSound, 0.5f); // Doors rotate at 90 degrees per second, so start the sound at // where it would be at the current rotation. - float offset = doorRot/(osg::PI * 0.5f); + float offset = doorRot / (osg::PI * 0.5f); action->setSoundOffset(offset); action->setSound(openSound); } else { - MWBase::Environment::get().getSoundManager()->fadeOutSound3D(ptr, - openSound, 0.5f); - float offset = 1.0f - doorRot/(osg::PI * 0.5f); + MWBase::Environment::get().getSoundManager()->fadeOutSound3D(ptr, openSound, 0.5f); + float offset = 1.0f - doorRot / (osg::PI * 0.5f); action->setSoundOffset(std::max(offset, 0.0f)); action->setSound(closeSound); } @@ -227,45 +233,39 @@ namespace MWClass else { // locked, and we can't open. - std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); + std::unique_ptr action = std::make_unique(std::string_view{}, ptr); action->setSound(lockedSound); return action; } } - bool Door::canLock(const MWWorld::ConstPtr &ptr) const + bool Door::canLock(const MWWorld::ConstPtr& ptr) const { return true; } - bool Door::allowTelekinesis(const MWWorld::ConstPtr &ptr) const + bool Door::allowTelekinesis(const MWWorld::ConstPtr& ptr) const { - if (ptr.getCellRef().getTeleport() && ptr.getCellRef().getLockLevel() <= 0 && ptr.getCellRef().getTrap().empty()) + if (ptr.getCellRef().getTeleport() && !ptr.getCellRef().isLocked() && ptr.getCellRef().getTrap().empty()) return false; else return true; } - std::string Door::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Door::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - void Door::registerSelf() + MWGui::ToolTipInfo Door::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - std::shared_ptr instance (new Door); - - registerClass (typeid (ESM::Door).name(), instance); - } - - MWGui::ToolTipInfo Door::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const - { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); std::string text; @@ -276,47 +276,42 @@ namespace MWClass } int lockLevel = ptr.getCellRef().getLockLevel(); - if (lockLevel > 0 && lockLevel != ESM::UnbreakableLock) - text += "\n#{sLockLevel}: " + MWGui::ToolTips::toString(ptr.getCellRef().getLockLevel()); - else if (ptr.getCellRef().getLockLevel() < 0) - text += "\n#{sUnlocked}"; - if (ptr.getCellRef().getTrap() != "") + if (lockLevel) + { + if (ptr.getCellRef().isLocked()) + text += "\n#{sLockLevel}: " + MWGui::ToolTips::toString(lockLevel); + else + text += "\n#{sUnlocked}"; + } + if (!ptr.getCellRef().getTrap().empty()) text += "\n#{sTrapped}"; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } - std::string Door::getDestination (const MWWorld::LiveCellRef& door) + std::string Door::getDestination(const MWWorld::LiveCellRef& door) { - std::string dest = door.mRef.getDestCell(); - if (dest.empty()) - { - // door leads to exterior, use cell name (if any), otherwise translated region name - int x,y; - auto world = MWBase::Environment::get().getWorld(); - world->positionToIndex (door.mRef.getDoorDest().pos[0], door.mRef.getDoorDest().pos[1], x, y); - const ESM::Cell* cell = world->getStore().get().search(x,y); - dest = world->getCellName(cell); - } + std::string_view dest = MWBase::Environment::get().getWorld()->getCellName( + &MWBase::Environment::get().getWorldModel()->getCell(door.mRef.getDestCell())); - return "#{sCell=" + dest + "}"; + return "#{sCell=" + std::string{ dest } + "}"; } - MWWorld::Ptr Door::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Door::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - void Door::ensureCustomData(const MWWorld::Ptr &ptr) const + void Door::ensureCustomData(const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { @@ -324,7 +319,7 @@ namespace MWClass } } - MWWorld::DoorState Door::getDoorState (const MWWorld::ConstPtr &ptr) const + MWWorld::DoorState Door::getDoorState(const MWWorld::ConstPtr& ptr) const { if (!ptr.getRefData().getCustomData()) return MWWorld::DoorState::Idle; @@ -332,7 +327,7 @@ namespace MWClass return customData.mDoorState; } - void Door::setDoorState (const MWWorld::Ptr &ptr, MWWorld::DoorState state) const + void Door::setDoorState(const MWWorld::Ptr& ptr, MWWorld::DoorState state) const { if (ptr.getCellRef().getTeleport()) throw std::runtime_error("load doors can't be moved"); @@ -342,7 +337,7 @@ namespace MWClass customData.mDoorState = state; } - void Door::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const + void Door::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; @@ -353,7 +348,7 @@ namespace MWClass customData.mDoorState = MWWorld::DoorState(doorState.mDoorState); } - void Door::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const + void Door::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { diff --git a/apps/openmw/mwclass/door.hpp b/apps/openmw/mwclass/door.hpp index 6c2fa26b804..18dd2348ab8 100644 --- a/apps/openmw/mwclass/door.hpp +++ b/apps/openmw/mwclass/door.hpp @@ -1,64 +1,67 @@ #ifndef GAME_MWCLASS_DOOR_H #define GAME_MWCLASS_DOOR_H -#include +#include -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Door : public MWWorld::Class + class Door : public MWWorld::RegisteredClass { - void ensureCustomData (const MWWorld::Ptr& ptr) const; + friend MWWorld::RegisteredClass; - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + Door(); - public: + void ensureCustomData(const MWWorld::Ptr& ptr) const; - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - bool isDoor() const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override; - bool useAnim() const override; + bool isDoor() const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + bool useAnim() const override; - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - static std::string getDestination (const MWWorld::LiveCellRef& door); - ///< @return destination cell name or token + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - bool canLock(const MWWorld::ConstPtr &ptr) const override; + static std::string getDestination(const MWWorld::LiveCellRef& door); + ///< @return destination cell name or token - bool allowTelekinesis(const MWWorld::ConstPtr &ptr) const override; - ///< Return whether this class of object can be activated with telekinesis + bool canLock(const MWWorld::ConstPtr& ptr) const override; - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const override; + ///< Return whether this class of object can be activated with telekinesis - static void registerSelf(); + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - MWWorld::DoorState getDoorState (const MWWorld::ConstPtr &ptr) const override; - /// This does not actually cause the door to move. Use World::activateDoor instead. - void setDoorState (const MWWorld::Ptr &ptr, MWWorld::DoorState state) const override; + MWWorld::DoorState getDoorState(const MWWorld::ConstPtr& ptr) const override; + /// This does not actually cause the door to move. Use World::activateDoor instead. + void setDoorState(const MWWorld::Ptr& ptr, MWWorld::DoorState state) const override; + void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; + ///< Read additional state from \a state into \a ptr. - void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; - ///< Read additional state from \a state into \a ptr. - - void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; - ///< Write additional state from \a ptr into \a state. + void writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; + ///< Write additional state from \a ptr into \a state. }; } diff --git a/apps/openmw/mwclass/esm4base.cpp b/apps/openmw/mwclass/esm4base.cpp new file mode 100644 index 00000000000..77a5ad94a62 --- /dev/null +++ b/apps/openmw/mwclass/esm4base.cpp @@ -0,0 +1,41 @@ +#include "esm4base.hpp" + +#include +#include + +#include + +#include "../mwgui/tooltips.hpp" + +#include "../mwrender/objects.hpp" +#include "../mwrender/renderinginterface.hpp" +#include "../mwrender/vismask.hpp" + +#include "../mwphysics/physicssystem.hpp" +#include "../mwworld/ptr.hpp" + +namespace MWClass +{ + void ESM4Impl::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) + { + if (!model.empty()) + { + renderingInterface.getObjects().insertModel(ptr, model); + ptr.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Static); + } + } + + void ESM4Impl::insertObjectPhysics( + const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) + { + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); + } + + MWGui::ToolTipInfo ESM4Impl::getToolTipInfo(std::string_view name, int count) + { + MWGui::ToolTipInfo info; + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); + return info; + } +} diff --git a/apps/openmw/mwclass/esm4base.hpp b/apps/openmw/mwclass/esm4base.hpp new file mode 100644 index 00000000000..f13d6007cd8 --- /dev/null +++ b/apps/openmw/mwclass/esm4base.hpp @@ -0,0 +1,157 @@ +#ifndef GAME_MWCLASS_ESM4BASE_H +#define GAME_MWCLASS_ESM4BASE_H + +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" + +#include "../mwgui/tooltips.hpp" + +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/registeredclass.hpp" + +#include "classmodel.hpp" + +namespace MWClass +{ + + namespace ESM4Impl + { + void insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface); + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics); + MWGui::ToolTipInfo getToolTipInfo(std::string_view name, int count); + + // We don't handle ESM4 player stats yet, so for resolving levelled object we use an arbitrary number. + constexpr int sDefaultLevel = 5; + + template + const TargetRecord* resolveLevelled(const ESM::RefId& id, int level = sDefaultLevel) + { + if (id.empty()) + return nullptr; + const MWWorld::ESMStore* esmStore = MWBase::Environment::get().getESMStore(); + const auto& targetStore = esmStore->get(); + const TargetRecord* res = targetStore.search(id); + if (res) + return res; + const LevelledRecord* lvlRec = esmStore->get().search(id); + if (!lvlRec) + return nullptr; + for (const ESM4::LVLO& obj : lvlRec->mLvlObject) + { + ESM::RefId candidateId = ESM::FormId::fromUint32(obj.item); + if (candidateId == id) + continue; + const TargetRecord* candidate = resolveLevelled(candidateId, level); + if (candidate && (!res || obj.level <= level)) + res = candidate; + } + return res; + } + } + + // Base for many ESM4 Classes + template + class ESM4Base : public MWWorld::Class + { + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override + { + const MWWorld::LiveCellRef* ref = ptr.get(); + return MWWorld::Ptr(cell.insert(ref), &cell); + } + + protected: + explicit ESM4Base(unsigned type) + : MWWorld::Class(type) + { + } + + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override + { + ESM4Impl::insertObjectRendering(ptr, model, renderingInterface); + } + + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override + { + insertObjectPhysics(ptr, model, rotation, physics); + } + + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override + { + ESM4Impl::insertObjectPhysics(ptr, model, rotation, physics); + } + + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return false; } + + std::string_view getName(const MWWorld::ConstPtr& ptr) const override { return {}; } + + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override + { + 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")) + return {}; + + return model; + } + }; + + class ESM4Static final : public MWWorld::RegisteredClass> + { + friend MWWorld::RegisteredClass>; + ESM4Static() + : MWWorld::RegisteredClass>(ESM4::Static::sRecordId) + { + } + }; + + class ESM4Tree final : public MWWorld::RegisteredClass> + { + friend MWWorld::RegisteredClass>; + ESM4Tree() + : MWWorld::RegisteredClass>(ESM4::Tree::sRecordId) + { + } + }; + + // For records with `mFullName` that should be shown as a tooltip. + // All objects with a tooltip can be activated (activation can be handled in Lua). + template + class ESM4Named : public MWWorld::RegisteredClass, ESM4Base> + { + public: + ESM4Named() + : MWWorld::RegisteredClass>(Record::sRecordId) + { + } + + std::string_view getName(const MWWorld::ConstPtr& ptr) const override + { + return ptr.get()->mBase->mFullName; + } + + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override + { + return ESM4Impl::getToolTipInfo(getName(ptr), count); + } + + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return !getName(ptr).empty(); } + }; +} + +#endif // GAME_MWCLASS_ESM4BASE_H diff --git a/apps/openmw/mwclass/esm4npc.cpp b/apps/openmw/mwclass/esm4npc.cpp new file mode 100644 index 00000000000..a7fb0a3544d --- /dev/null +++ b/apps/openmw/mwclass/esm4npc.cpp @@ -0,0 +1,195 @@ +#include "esm4npc.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../mwworld/customdata.hpp" +#include "../mwworld/esmstore.hpp" + +#include "esm4base.hpp" + +namespace MWClass +{ + template + static std::vector withBaseTemplates( + const TargetRecord* rec, int level = MWClass::ESM4Impl::sDefaultLevel) + { + std::vector res{ rec }; + while (true) + { + const TargetRecord* newRec + = MWClass::ESM4Impl::resolveLevelled(rec->mBaseTemplate, level); + if (!newRec || newRec == rec) + return res; + res.push_back(rec = newRec); + } + } + + static const ESM4::Npc* chooseTemplate(const std::vector& recs, uint16_t flag) + { + for (const auto* rec : recs) + { + if (rec->mIsTES4) + return rec; + else if (rec->mIsFONV) + { + // TODO: FO3 should use this branch as well. But it is not clear how to distinguish FO3 from + // TES5. Currently FO3 uses wrong template flags that can lead to "ESM4 NPC traits not found" + // exception the NPC will not be added to the scene. But in any way it shouldn't cause a crash. + if (!(rec->mBaseConfig.fo3.templateFlags & flag)) + return rec; + } + else if (rec->mIsFO4) + { + if (!(rec->mBaseConfig.fo4.templateFlags & flag)) + return rec; + } + else if (!(rec->mBaseConfig.tes5.templateFlags & flag)) + return rec; + } + return nullptr; + } + + class ESM4NpcCustomData : public MWWorld::TypedCustomData + { + public: + const ESM4::Npc* mTraits; + const ESM4::Npc* mBaseData; + const ESM4::Race* mRace; + bool mIsFemale; + + // TODO: Use InventoryStore instead (currently doesn't support ESM4 objects) + std::vector mEquippedArmor; + std::vector mEquippedClothing; + + ESM4NpcCustomData& asESM4NpcCustomData() override { return *this; } + const ESM4NpcCustomData& asESM4NpcCustomData() const override { return *this; } + }; + + ESM4NpcCustomData& ESM4Npc::getCustomData(const MWWorld::ConstPtr& ptr) + { + // Note: the argument is ConstPtr because this function is used in `getModel` and `getName` + // which are virtual and work with ConstPtr. `getModel` and `getName` use custom data + // because they require a lot of work including levelled records resolving and it would be + // stupid to not to cache the results. Maybe we should stop using ConstPtr at all + // to avoid such workarounds. + MWWorld::RefData& refData = const_cast(ptr.getRefData()); + + if (auto* data = refData.getCustomData()) + return data->asESM4NpcCustomData(); + + auto data = std::make_unique(); + + const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); + const ESM4::Npc* const base = ptr.get()->mBase; + auto npcRecs = withBaseTemplates(base); + + data->mTraits = chooseTemplate(npcRecs, ESM4::Npc::Template_UseTraits); + + if (data->mTraits == nullptr) + Log(Debug::Warning) << "Traits are not found for ESM4 NPC base record: \"" << base->mEditorId << "\" (" + << ESM::RefId(base->mId) << ")"; + + data->mBaseData = chooseTemplate(npcRecs, ESM4::Npc::Template_UseBaseData); + + if (data->mBaseData == nullptr) + Log(Debug::Warning) << "Base data is not found for ESM4 NPC base record: \"" << base->mEditorId << "\" (" + << ESM::RefId(base->mId) << ")"; + + if (data->mTraits != nullptr) + { + data->mRace = store->get().find(data->mTraits->mRace); + if (data->mTraits->mIsTES4) + data->mIsFemale = data->mTraits->mBaseConfig.tes4.flags & ESM4::Npc::TES4_Female; + else if (data->mTraits->mIsFONV) + data->mIsFemale = data->mTraits->mBaseConfig.fo3.flags & ESM4::Npc::FO3_Female; + else if (data->mTraits->mIsFO4) + data->mIsFemale + = data->mTraits->mBaseConfig.fo4.flags & ESM4::Npc::TES5_Female; // FO4 flags are the same as TES5 + else + data->mIsFemale = data->mTraits->mBaseConfig.tes5.flags & ESM4::Npc::TES5_Female; + } + + if (auto inv = chooseTemplate(npcRecs, ESM4::Npc::Template_UseInventory)) + { + for (const ESM4::InventoryItem& item : inv->mInventory) + { + if (auto* armor + = ESM4Impl::resolveLevelled(ESM::FormId::fromUint32(item.item))) + data->mEquippedArmor.push_back(armor); + else if (data->mTraits != nullptr && data->mTraits->mIsTES4) + { + const auto* clothing = ESM4Impl::resolveLevelled( + ESM::FormId::fromUint32(item.item)); + if (clothing) + data->mEquippedClothing.push_back(clothing); + } + } + if (!inv->mDefaultOutfit.isZeroOrUnset()) + { + if (const ESM4::Outfit* outfit = store->get().search(inv->mDefaultOutfit)) + { + for (ESM::FormId itemId : outfit->mInventory) + if (auto* armor = ESM4Impl::resolveLevelled(itemId)) + data->mEquippedArmor.push_back(armor); + } + else + Log(Debug::Error) << "Outfit not found: " << ESM::RefId(inv->mDefaultOutfit); + } + } + + ESM4NpcCustomData& res = *data; + refData.setCustomData(std::move(data)); + return res; + } + + const std::vector& ESM4Npc::getEquippedArmor(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mEquippedArmor; + } + + const std::vector& ESM4Npc::getEquippedClothing(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mEquippedClothing; + } + + const ESM4::Npc* ESM4Npc::getTraitsRecord(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mTraits; + } + + const ESM4::Race* ESM4Npc::getRace(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mRace; + } + + bool ESM4Npc::isFemale(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mIsFemale; + } + + std::string_view ESM4Npc::getModel(const MWWorld::ConstPtr& ptr) const + { + const ESM4NpcCustomData& data = getCustomData(ptr); + if (data.mTraits == nullptr) + return {}; + if (data.mTraits->mIsTES4) + return data.mTraits->mModel; + return data.mIsFemale ? data.mRace->mModelFemale : data.mRace->mModelMale; + } + + std::string_view ESM4Npc::getName(const MWWorld::ConstPtr& ptr) const + { + const ESM4::Npc* const baseData = getCustomData(ptr).mBaseData; + if (baseData == nullptr) + return {}; + return baseData->mFullName; + } +} diff --git a/apps/openmw/mwclass/esm4npc.hpp b/apps/openmw/mwclass/esm4npc.hpp new file mode 100644 index 00000000000..39116586f17 --- /dev/null +++ b/apps/openmw/mwclass/esm4npc.hpp @@ -0,0 +1,71 @@ +#ifndef GAME_MWCLASS_ESM4ACTOR_H +#define GAME_MWCLASS_ESM4ACTOR_H + +#include +#include + +#include "../mwgui/tooltips.hpp" + +#include "../mwrender/objects.hpp" +#include "../mwrender/renderinginterface.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" + +#include "esm4base.hpp" + +namespace MWClass +{ + class ESM4Npc final : public MWWorld::RegisteredClass + { + public: + ESM4Npc() + : MWWorld::RegisteredClass(ESM4::Npc::sRecordId) + { + } + + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override + { + const MWWorld::LiveCellRef* ref = ptr.get(); + return MWWorld::Ptr(cell.insert(ref), &cell); + } + + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override + { + renderingInterface.getObjects().insertNPC(ptr); + } + + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override + { + insertObjectPhysics(ptr, model, rotation, physics); + } + + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override + { + // ESM4Impl::insertObjectPhysics(ptr, getModel(ptr), rotation, physics); + } + + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; } + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override + { + return ESM4Impl::getToolTipInfo(getName(ptr), count); + } + + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + + static const ESM4::Npc* getTraitsRecord(const MWWorld::Ptr& ptr); + static const ESM4::Race* getRace(const MWWorld::Ptr& ptr); + static bool isFemale(const MWWorld::Ptr& ptr); + static const std::vector& getEquippedArmor(const MWWorld::Ptr& ptr); + static const std::vector& getEquippedClothing(const MWWorld::Ptr& ptr); + + private: + static ESM4NpcCustomData& getCustomData(const MWWorld::ConstPtr& ptr); + }; +} + +#endif // GAME_MWCLASS_ESM4ACTOR_H diff --git a/apps/openmw/mwclass/ingredient.cpp b/apps/openmw/mwclass/ingredient.cpp index a007ad115f9..94a1e8cc892 100644 --- a/apps/openmw/mwclass/ingredient.cpp +++ b/apps/openmw/mwclass/ingredient.cpp @@ -1,117 +1,111 @@ #include "ingredient.hpp" -#include +#include +#include + +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/ptr.hpp" +#include "../mwworld/actioneat.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwphysics/physicssystem.hpp" -#include "../mwworld/actioneat.hpp" #include "../mwworld/nullaction.hpp" +#include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { - - void Ingredient::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + Ingredient::Ingredient() + : MWWorld::RegisteredClass(ESM::Ingredient::sRecordId) { - if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); - } } - void Ingredient::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Ingredient::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - // TODO: add option somewhere to enable collision for placeable objects + if (!model.empty()) + { + renderingInterface.getObjects().insertModel(ptr, model); + } } - std::string Ingredient::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Ingredient::getModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } - std::string Ingredient::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Ingredient::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - std::shared_ptr Ingredient::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Ingredient::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } - std::string Ingredient::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Ingredient::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - int Ingredient::getValue (const MWWorld::ConstPtr& ptr) const + int Ingredient::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } - - std::shared_ptr Ingredient::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Ingredient::use(const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action (new MWWorld::ActionEat (ptr)); + if (ptr.get()->mBase->mData.mEffectID[0] < 0) + return std::make_unique(); + std::unique_ptr action = std::make_unique(ptr); - action->setSound ("Swallow"); + action->setSound(ESM::RefId::stringRefId("Swallow")); return action; } - void Ingredient::registerSelf() - { - std::shared_ptr instance (new Ingredient); - - registerClass (typeid (ESM::Ingredient).name(), instance); - } - - std::string Ingredient::getUpSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Ingredient::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Ingredient Up"); + static auto sound = ESM::RefId::stringRefId("Item Ingredient Up"); + return sound; } - std::string Ingredient::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Ingredient::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Ingredient Down"); + static auto sound = ESM::RefId::stringRefId("Item Ingredient Down"); + return sound; } - std::string Ingredient::getInventoryIcon (const MWWorld::ConstPtr& ptr) const + const std::string& Ingredient::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - MWGui::ToolTipInfo Ingredient::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Ingredient::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -119,58 +113,57 @@ namespace MWClass text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); float alchemySkill = player.getClass().getSkill(player, ESM::Skill::Alchemy); - static const float fWortChanceValue = - MWBase::Environment::get().getWorld()->getStore().get().find("fWortChanceValue")->mValue.getFloat(); + static const float fWortChanceValue = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fWortChanceValue") + ->mValue.getFloat(); MWGui::Widgets::SpellEffectList list; - for (int i=0; i<4; ++i) + for (int i = 0; i < 4; ++i) { if (ref->mBase->mData.mEffectID[i] < 0) continue; MWGui::Widgets::SpellEffectParams params; params.mEffectID = ref->mBase->mData.mEffectID[i]; - params.mAttribute = ref->mBase->mData.mAttributes[i]; - params.mSkill = ref->mBase->mData.mSkills[i]; - - params.mKnown = ( (i == 0 && alchemySkill >= fWortChanceValue) - || (i == 1 && alchemySkill >= fWortChanceValue*2) - || (i == 2 && alchemySkill >= fWortChanceValue*3) - || (i == 3 && alchemySkill >= fWortChanceValue*4)); + params.mAttribute = ESM::Attribute::indexToRefId(ref->mBase->mData.mAttributes[i]); + params.mSkill = ESM::Skill::indexToRefId(ref->mBase->mData.mSkills[i]); + params.mKnown = alchemySkill >= fWortChanceValue * (i + 1); list.push_back(params); } - info.effects = list; + info.effects = std::move(list); - info.text = text; + info.text = std::move(text); info.isIngredient = true; return info; } - MWWorld::Ptr Ingredient::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Ingredient::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - bool Ingredient::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Ingredient::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Ingredients) != 0; } - - float Ingredient::getWeight(const MWWorld::ConstPtr &ptr) const + float Ingredient::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } diff --git a/apps/openmw/mwclass/ingredient.hpp b/apps/openmw/mwclass/ingredient.hpp index 5219cf39ce3..2e7632cd5e8 100644 --- a/apps/openmw/mwclass/ingredient.hpp +++ b/apps/openmw/mwclass/ingredient.hpp @@ -1,56 +1,57 @@ #ifndef GAME_MWCLASS_INGREDIENT_H #define GAME_MWCLASS_INGREDIENT_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Ingredient : public MWWorld::Class + class Ingredient : public MWWorld::RegisteredClass { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + Ingredient(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + bool isItem(const MWWorld::ConstPtr&) const override { return true; } - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu - - static void registerSelf(); + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - float getWeight (const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + float getWeight(const MWWorld::ConstPtr& ptr) const override; + + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } diff --git a/apps/openmw/mwclass/itemlevlist.cpp b/apps/openmw/mwclass/itemlevlist.cpp index 5608a8d233a..95b8e22402f 100644 --- a/apps/openmw/mwclass/itemlevlist.cpp +++ b/apps/openmw/mwclass/itemlevlist.cpp @@ -1,24 +1,21 @@ #include "itemlevlist.hpp" -#include +#include namespace MWClass { - - std::string ItemLevList::getName (const MWWorld::ConstPtr& ptr) const + ItemLevList::ItemLevList() + : MWWorld::RegisteredClass(ESM::ItemLevList::sRecordId) { - return ""; } - bool ItemLevList::hasToolTip(const MWWorld::ConstPtr& ptr) const + std::string_view ItemLevList::getName(const MWWorld::ConstPtr& ptr) const { - return false; + return {}; } - void ItemLevList::registerSelf() + bool ItemLevList::hasToolTip(const MWWorld::ConstPtr& ptr) const { - std::shared_ptr instance (new ItemLevList); - - registerClass (typeid (ESM::ItemLevList).name(), instance); + return false; } } diff --git a/apps/openmw/mwclass/itemlevlist.hpp b/apps/openmw/mwclass/itemlevlist.hpp index 771f8b7a764..56652c788b3 100644 --- a/apps/openmw/mwclass/itemlevlist.hpp +++ b/apps/openmw/mwclass/itemlevlist.hpp @@ -1,21 +1,22 @@ #ifndef GAME_MWCLASS_ITEMLEVLIST_H #define GAME_MWCLASS_ITEMLEVLIST_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class ItemLevList : public MWWorld::Class + class ItemLevList : public MWWorld::RegisteredClass { - public: + friend MWWorld::RegisteredClass; - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + ItemLevList(); - bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; - ///< @return true if this object has a tooltip when focused (default implementation: true) + public: + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - static void registerSelf(); + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; + ///< @return true if this object has a tooltip when focused (default implementation: true) }; } diff --git a/apps/openmw/mwclass/light.cpp b/apps/openmw/mwclass/light.cpp index 3bdf10f4758..763ce863264 100644 --- a/apps/openmw/mwclass/light.cpp +++ b/apps/openmw/mwclass/light.cpp @@ -1,52 +1,69 @@ #include "light.hpp" -#include -#include -#include +#include +#include + +#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwworld/ptr.hpp" +#include "../mwphysics/physicssystem.hpp" #include "../mwworld/actionequip.hpp" -#include "../mwworld/nullaction.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/inventorystore.hpp" -#include "../mwworld/cellstore.hpp" -#include "../mwphysics/physicssystem.hpp" +#include "../mwworld/nullaction.hpp" +#include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { + Light::Light() + : MWWorld::RegisteredClass(ESM::Light::sRecordId) + { + } - void Light::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + void Light::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - MWWorld::LiveCellRef *ref = - ptr.get(); + MWWorld::LiveCellRef* ref = ptr.get(); // Insert even if model is empty, so that the light is added - renderingInterface.getObjects().insertModel(ptr, model, true, !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)); + renderingInterface.getObjects().insertModel(ptr, model, !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)); } - void Light::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Light::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const { - MWWorld::LiveCellRef *ref = - ptr.get(); - assert (ref->mBase != nullptr); + MWWorld::LiveCellRef* ref = ptr.get(); + assert(ref->mBase != nullptr); - // TODO: add option somewhere to enable collision for placeable objects - if (!model.empty() && (ref->mBase->mData.mFlags & ESM::Light::Carry) == 0) - physics.addObject(ptr, model); + insertObjectPhysics(ptr, model, rotation, physics); if (!ref->mBase->mSound.empty() && !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)) - MWBase::Environment::get().getSoundManager()->playSound3D(ptr, ref->mBase->mSound, 1.0, 1.0, - MWSound::Type::Sfx, - MWSound::PlayMode::Loop); + MWBase::Environment::get().getSoundManager()->playSound3D( + ptr, ref->mBase->mSound, 1.0, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::Loop); + } + + void Light::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const + { + // TODO: add option somewhere to enable collision for placeable objects + if ((ptr.get()->mBase->mData.mFlags & ESM::Light::Carry) == 0) + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); } bool Light::useAnim() const @@ -54,125 +71,117 @@ namespace MWClass return true; } - std::string Light::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Light::getModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } - std::string Light::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Light::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); if (ref->mBase->mModel.empty()) - return std::string(); + return {}; + return getNameOrId(ptr); + } - const std::string& name = ref->mBase->mName; - return !name.empty() ? name : ref->mBase->mId; + bool Light::isItem(const MWWorld::ConstPtr& ptr) const + { + return ptr.get()->mBase->mData.mFlags & ESM::Light::Carry; } - std::shared_ptr Light::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Light::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { - if(!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) - return std::shared_ptr(new MWWorld::NullAction()); + if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) + return std::make_unique(); - MWWorld::LiveCellRef *ref = ptr.get(); - if(!(ref->mBase->mData.mFlags&ESM::Light::Carry)) - return std::shared_ptr(new MWWorld::FailedAction()); + MWWorld::LiveCellRef* ref = ptr.get(); + if (!(ref->mBase->mData.mFlags & ESM::Light::Carry)) + return std::make_unique(); return defaultItemActivate(ptr, actor); } - std::string Light::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Light::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - std::pair, bool> Light::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const + std::pair, bool> Light::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); std::vector slots_; if (ref->mBase->mData.mFlags & ESM::Light::Carry) - slots_.push_back (int (MWWorld::InventoryStore::Slot_CarriedLeft)); + slots_.push_back(int(MWWorld::InventoryStore::Slot_CarriedLeft)); - return std::make_pair (slots_, false); + return std::make_pair(slots_, false); } - int Light::getValue (const MWWorld::ConstPtr& ptr) const + int Light::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } - void Light::registerSelf() + const ESM::RefId& Light::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - std::shared_ptr instance (new Light); - - registerClass (typeid (ESM::Light).name(), instance); + static const auto sound = ESM::RefId::stringRefId("Item Misc Up"); + return sound; } - std::string Light::getUpSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Light::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Misc Up"); + static const auto sound = ESM::RefId::stringRefId("Item Misc Down"); + return sound; } - std::string Light::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const std::string& Light::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Misc Down"); - } - - - std::string Light::getInventoryIcon (const MWWorld::ConstPtr& ptr) const - { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - bool Light::hasToolTip (const MWWorld::ConstPtr& ptr) const + bool Light::hasToolTip(const MWWorld::ConstPtr& ptr) const { return showsInInventory(ptr); } - MWGui::ToolTipInfo Light::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Light::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; // Don't show duration for infinite light sources. - if (Settings::Manager::getBool("show effect duration","Game") && ptr.getClass().getRemainingUsageTime(ptr) != -1) + if (Settings::game().mShowEffectDuration && ptr.getClass().getRemainingUsageTime(ptr) != -1) text += MWGui::ToolTips::getDurationString(ptr.getClass().getRemainingUsageTime(ptr), "\n#{sDuration}"); text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } - bool Light::showsInInventory (const MWWorld::ConstPtr& ptr) const + bool Light::showsInInventory(const MWWorld::ConstPtr& ptr) const { const ESM::Light* light = ptr.get()->mBase; @@ -182,58 +191,59 @@ namespace MWClass return Class::showsInInventory(ptr); } - std::shared_ptr Light::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Light::use(const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); return action; } - void Light::setRemainingUsageTime (const MWWorld::Ptr& ptr, float duration) const + void Light::setRemainingUsageTime(const MWWorld::Ptr& ptr, float duration) const { ptr.getCellRef().setChargeFloat(duration); } - float Light::getRemainingUsageTime (const MWWorld::ConstPtr& ptr) const + float Light::getRemainingUsageTime(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); if (ptr.getCellRef().getCharge() == -1) return static_cast(ref->mBase->mData.mTime); else return ptr.getCellRef().getChargeFloat(); } - MWWorld::Ptr Light::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Light::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - bool Light::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Light::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Lights) != 0; } - float Light::getWeight(const MWWorld::ConstPtr &ptr) const + float Light::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } - std::pair Light::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const + std::pair Light::canBeEquipped(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); if (!(ref->mBase->mData.mFlags & ESM::Light::Carry)) - return std::make_pair(0,""); + return { 0, {} }; - return std::make_pair(1,""); + return { 1, {} }; } - std::string Light::getSound(const MWWorld::ConstPtr& ptr) const + ESM::RefId Light::getSound(const MWWorld::ConstPtr& ptr) const { - return ptr.get()->mBase->mSound; + return ptr.get()->mBase->mSound; } + } diff --git a/apps/openmw/mwclass/light.hpp b/apps/openmw/mwclass/light.hpp index e37dddc250f..97625ee5f8d 100644 --- a/apps/openmw/mwclass/light.hpp +++ b/apps/openmw/mwclass/light.hpp @@ -1,78 +1,86 @@ #ifndef GAME_MWCLASS_LIGHT_H #define GAME_MWCLASS_LIGHT_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Light : public MWWorld::Class + class Light : public MWWorld::RegisteredClass { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + Light(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - bool useAnim() const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override; - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + bool useAnim() const override; - bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; - ///< @return true if this object has a tooltip when focused (default implementation: true) + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; + ///< @return true if this object has a tooltip when focused (default implementation: true) - bool showsInInventory (const MWWorld::ConstPtr& ptr) const override; + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + bool showsInInventory(const MWWorld::ConstPtr& ptr) const override; - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + bool isItem(const MWWorld::ConstPtr&) const override; - std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; - ///< \return first: Return IDs of the slot this object can be equipped in; second: can object - /// stay stacked when equipped? + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - static void registerSelf(); + std::pair, bool> getEquipmentSlots(const MWWorld::ConstPtr& ptr) const override; + ///< \return first: Return IDs of the slot this object can be equipped in; second: can object + /// stay stacked when equipped? - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - void setRemainingUsageTime (const MWWorld::Ptr& ptr, float duration) const override; - ///< Sets the remaining duration of the object. + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu - float getRemainingUsageTime (const MWWorld::ConstPtr& ptr) const override; - ///< Returns the remaining duration of the object. + void setRemainingUsageTime(const MWWorld::Ptr& ptr, float duration) const override; + ///< Sets the remaining duration of the object. - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + float getRemainingUsageTime(const MWWorld::ConstPtr& ptr) const override; + ///< Returns the remaining duration of the object. - float getWeight (const MWWorld::ConstPtr& ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + float getWeight(const MWWorld::ConstPtr& ptr) const override; - std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; - std::string getSound(const MWWorld::ConstPtr& ptr) const override; + std::pair canBeEquipped( + const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const override; + + ESM::RefId getSound(const MWWorld::ConstPtr& ptr) const override; }; + } #endif diff --git a/apps/openmw/mwclass/light4.cpp b/apps/openmw/mwclass/light4.cpp new file mode 100644 index 00000000000..6d3fa5d9d81 --- /dev/null +++ b/apps/openmw/mwclass/light4.cpp @@ -0,0 +1,24 @@ +#include "light4.hpp" + +#include "../mwrender/objects.hpp" +#include "../mwrender/renderinginterface.hpp" +#include "../mwworld/ptr.hpp" + +#include + +namespace MWClass +{ + ESM4Light::ESM4Light() + : MWWorld::RegisteredClass>(ESM4::Light::sRecordId) + { + } + + void ESM4Light ::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + { + MWWorld::LiveCellRef* ref = ptr.get(); + + // Insert even if model is empty, so that the light is added + renderingInterface.getObjects().insertModel(ptr, model, !(ref->mBase->mData.flags & ESM4::Light::OffDefault)); + } +} diff --git a/apps/openmw/mwclass/light4.hpp b/apps/openmw/mwclass/light4.hpp new file mode 100644 index 00000000000..755f6b580da --- /dev/null +++ b/apps/openmw/mwclass/light4.hpp @@ -0,0 +1,22 @@ +#ifndef OPENW_MWCLASS_LIGHT4 +#define OPENW_MWCLASS_LIGHT4 + +#include "../mwworld/registeredclass.hpp" + +#include "esm4base.hpp" + +namespace MWClass +{ + class ESM4Light : public MWWorld::RegisteredClass> + { + friend MWWorld::RegisteredClass>; + + ESM4Light(); + + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering + }; +} +#endif diff --git a/apps/openmw/mwclass/lockpick.cpp b/apps/openmw/mwclass/lockpick.cpp index 9b8abc8f237..7955d5af202 100644 --- a/apps/openmw/mwclass/lockpick.cpp +++ b/apps/openmw/mwclass/lockpick.cpp @@ -1,116 +1,108 @@ #include "lockpick.hpp" -#include +#include +#include + +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwworld/ptr.hpp" #include "../mwworld/actionequip.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" -#include "../mwphysics/physicssystem.hpp" -#include "../mwworld/nullaction.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { - - void Lockpick::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + Lockpick::Lockpick() + : MWWorld::RegisteredClass(ESM::Lockpick::sRecordId) { - if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); - } } - void Lockpick::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Lockpick::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - // TODO: add option somewhere to enable collision for placeable objects + if (!model.empty()) + { + renderingInterface.getObjects().insertModel(ptr, model); + } } - std::string Lockpick::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Lockpick::getModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } - std::string Lockpick::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Lockpick::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - std::shared_ptr Lockpick::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Lockpick::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } - std::string Lockpick::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Lockpick::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - std::pair, bool> Lockpick::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const + std::pair, bool> Lockpick::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { std::vector slots_; - slots_.push_back (int (MWWorld::InventoryStore::Slot_CarriedRight)); + slots_.push_back(int(MWWorld::InventoryStore::Slot_CarriedRight)); - return std::make_pair (slots_, false); + return std::make_pair(slots_, false); } - int Lockpick::getValue (const MWWorld::ConstPtr& ptr) const + int Lockpick::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } - void Lockpick::registerSelf() - { - std::shared_ptr instance (new Lockpick); - - registerClass (typeid (ESM::Lockpick).name(), instance); - } - - std::string Lockpick::getUpSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Lockpick::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Lockpick Up"); + static ESM::RefId sound = ESM::RefId::stringRefId("Item Lockpick Up"); + return sound; } - std::string Lockpick::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Lockpick::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Lockpick Down"); + static ESM::RefId sound = ESM::RefId::stringRefId("Item Lockpick Down"); + return sound; } - std::string Lockpick::getInventoryIcon (const MWWorld::ConstPtr& ptr) const + const std::string& Lockpick::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - MWGui::ToolTipInfo Lockpick::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Lockpick::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -122,57 +114,59 @@ namespace MWClass text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } - std::shared_ptr Lockpick::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Lockpick::use(const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); return action; } - MWWorld::Ptr Lockpick::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Lockpick::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - std::pair Lockpick::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const + std::pair Lockpick::canBeEquipped( + const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { // Do not allow equip tools from inventory during attack if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) && MWBase::Environment::get().getWindowManager()->isGuiMode()) - return std::make_pair(0, "#{sCantEquipWeapWarning}"); + return { 0, "#{sCantEquipWeapWarning}" }; - return std::make_pair(1, ""); + return { 1, {} }; } - bool Lockpick::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Lockpick::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Picks) != 0; } - int Lockpick::getItemMaxHealth (const MWWorld::ConstPtr& ptr) const + int Lockpick::getItemMaxHealth(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mUses; } - float Lockpick::getWeight(const MWWorld::ConstPtr &ptr) const + float Lockpick::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } diff --git a/apps/openmw/mwclass/lockpick.hpp b/apps/openmw/mwclass/lockpick.hpp index fabae334351..48c18411a6b 100644 --- a/apps/openmw/mwclass/lockpick.hpp +++ b/apps/openmw/mwclass/lockpick.hpp @@ -1,68 +1,75 @@ #ifndef GAME_MWCLASS_LOCKPICK_H #define GAME_MWCLASS_LOCKPICK_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" + +namespace ESM +{ + class RefId; +} namespace MWClass { - class Lockpick : public MWWorld::Class + class Lockpick : public MWWorld::RegisteredClass { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + Lockpick(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + bool isItem(const MWWorld::ConstPtr&) const override { return true; } - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; - ///< \return first: Return IDs of the slot this object can be equipped in; second: can object - /// stay stacked when equipped? + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + std::pair, bool> getEquipmentSlots(const MWWorld::ConstPtr& ptr) const override; + ///< \return first: Return IDs of the slot this object can be equipped in; second: can object + /// stay stacked when equipped? - static void registerSelf(); + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; + std::pair canBeEquipped( + const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const override; - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; - float getWeight (const MWWorld::ConstPtr& ptr) const override; + float getWeight(const MWWorld::ConstPtr& ptr) const override; - int getItemMaxHealth (const MWWorld::ConstPtr& ptr) const override; - ///< Return item max health or throw an exception, if class does not have item health + int getItemMaxHealth(const MWWorld::ConstPtr& ptr) const override; + ///< Return item max health or throw an exception, if class does not have item health - bool hasItemHealth (const MWWorld::ConstPtr& ptr) const override { return true; } - ///< \return Item health data available? (default implementation: false) + bool hasItemHealth(const MWWorld::ConstPtr& ptr) const override { return true; } + ///< \return Item health data available? (default implementation: false) }; } diff --git a/apps/openmw/mwclass/misc.cpp b/apps/openmw/mwclass/misc.cpp index 8d3cda6fe5a..cd97ecf216e 100644 --- a/apps/openmw/mwclass/misc.cpp +++ b/apps/openmw/mwclass/misc.cpp @@ -1,100 +1,99 @@ #include "misc.hpp" -#include -#include +#include +#include + +#include +#include +#include + +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwworld/actionsoulgem.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwphysics/physicssystem.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/nullaction.hpp" -#include "../mwworld/actionsoulgem.hpp" +#include "../mwworld/worldmodel.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { - bool Miscellaneous::isGold (const MWWorld::ConstPtr& ptr) const + Miscellaneous::Miscellaneous() + : MWWorld::RegisteredClass(ESM::Miscellaneous::sRecordId) { - return Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "gold_001") - || Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "gold_005") - || Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "gold_010") - || Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "gold_025") - || Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "gold_100"); } - void Miscellaneous::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + bool Miscellaneous::isGold(const MWWorld::ConstPtr& ptr) const { - if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); - } + return ptr.getCellRef().getRefId() == "gold_001" || ptr.getCellRef().getRefId() == "gold_005" + || ptr.getCellRef().getRefId() == "gold_010" || ptr.getCellRef().getRefId() == "gold_025" + || ptr.getCellRef().getRefId() == "gold_100"; } - void Miscellaneous::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Miscellaneous::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - // TODO: add option somewhere to enable collision for placeable objects + if (!model.empty()) + { + renderingInterface.getObjects().insertModel(ptr, model); + } } - std::string Miscellaneous::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Miscellaneous::getModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } - std::string Miscellaneous::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Miscellaneous::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - std::shared_ptr Miscellaneous::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Miscellaneous::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } - std::string Miscellaneous::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Miscellaneous::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - int Miscellaneous::getValue (const MWWorld::ConstPtr& ptr) const + int Miscellaneous::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); int value = ref->mBase->mData.mValue; - if (ptr.getCellRef().getGoldValue() > 1 && ptr.getRefData().getCount() == 1) - value = ptr.getCellRef().getGoldValue(); + if (isGold(ptr) && ptr.getCellRef().getCount() != 1) + value = 1; - if (ptr.getCellRef().getSoul() != "") + if (!ptr.getCellRef().getSoul().empty()) { - const ESM::Creature *creature = MWBase::Environment::get().getWorld()->getStore().get().search(ref->mRef.getSoul()); + const ESM::Creature* creature + = MWBase::Environment::get().getESMStore()->get().search(ref->mRef.getSoul()); if (creature) { int soul = creature->mData.mSoul; - if (Settings::Manager::getBool("rebalance soul gem values", "Game")) + if (Settings::game().mRebalanceSoulGemValues) { - // use the 'soul gem value rebalance' formula from the Morrowind Code Patch + // use the 'soul gem value rebalance' formula from the Morrowind Code Patch float soulValue = 0.0001 * pow(soul, 3) + 2 * soul; - + // for Azura's star add the unfilled value - if (Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "Misc_SoulGem_Azura")) + if (ptr.getCellRef().getRefId() == "Misc_SoulGem_Azura") value += soulValue; else value = soulValue; @@ -107,37 +106,35 @@ namespace MWClass return value; } - void Miscellaneous::registerSelf() - { - std::shared_ptr instance (new Miscellaneous); - - registerClass (typeid (ESM::Miscellaneous).name(), instance); - } - - std::string Miscellaneous::getUpSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Miscellaneous::getUpSoundId(const MWWorld::ConstPtr& ptr) const { + static const ESM::RefId soundGold = ESM::RefId::stringRefId("Item Gold Up"); + static const ESM::RefId soundMisc = ESM::RefId::stringRefId("Item Misc Up"); if (isGold(ptr)) - return std::string("Item Gold Up"); - return std::string("Item Misc Up"); + return soundGold; + + return soundMisc; } - std::string Miscellaneous::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Miscellaneous::getDownSoundId(const MWWorld::ConstPtr& ptr) const { + static const ESM::RefId soundGold = ESM::RefId::stringRefId("Item Gold Down"); + static const ESM::RefId soundMisc = ESM::RefId::stringRefId("Item Misc Down"); if (isGold(ptr)) - return std::string("Item Gold Down"); - return std::string("Item Misc Down"); + return soundGold; + return soundMisc; } - std::string Miscellaneous::getInventoryIcon (const MWWorld::ConstPtr& ptr) const + const std::string& Miscellaneous::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - MWGui::ToolTipInfo Miscellaneous::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Miscellaneous::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; @@ -151,90 +148,115 @@ namespace MWClass else // gold displays its count also if it's 1. countString = " (" + std::to_string(count) + ")"; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + countString + MWGui::ToolTips::getSoulString(ptr.getCellRef()); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count) + + MWGui::ToolTips::getSoulString(ptr.getCellRef()); info.icon = ref->mBase->mIcon; std::string text; text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); - if (!gold && !ref->mBase->mData.mIsKey) + if (!gold && !(ref->mBase->mData.mFlags & ESM::Miscellaneous::Key)) text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } - MWWorld::Ptr Miscellaneous::copyToCell(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell, int count) const + static MWWorld::Ptr createGold(MWWorld::CellStore& cell, int goldAmount) { - MWWorld::Ptr newPtr; - - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); - - if (isGold(ptr)) { - int goldAmount = getValue(ptr) * count; - - std::string base = "Gold_001"; - if (goldAmount >= 100) - base = "Gold_100"; - else if (goldAmount >= 25) - base = "Gold_025"; - else if (goldAmount >= 10) - base = "Gold_010"; - else if (goldAmount >= 5) - base = "Gold_005"; - - // Really, I have no idea why moving ref out of conditional - // scope causes list::push_back throwing std::bad_alloc - MWWorld::ManualRef newRef(store, base); - const MWWorld::LiveCellRef *ref = - newRef.getPtr().get(); + std::string_view base = "gold_001"; + if (goldAmount >= 100) + base = "gold_100"; + else if (goldAmount >= 25) + base = "gold_025"; + else if (goldAmount >= 10) + base = "gold_010"; + else if (goldAmount >= 5) + base = "gold_005"; + + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + MWWorld::ManualRef newRef(store, ESM::RefId::stringRefId(base)); + const MWWorld::LiveCellRef* ref = newRef.getPtr().get(); + + MWWorld::Ptr ptr(cell.insert(ref), &cell); + ptr.getCellRef().setCount(goldAmount); + return ptr; + } + MWWorld::Ptr Miscellaneous::copyToCell(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell, int count) const + { + MWWorld::Ptr newPtr; + if (isGold(ptr)) + newPtr = createGold(cell, getValue(ptr) * count); + else + { + const MWWorld::LiveCellRef* ref = ptr.get(); newPtr = MWWorld::Ptr(cell.insert(ref), &cell); - newPtr.getCellRef().setGoldValue(goldAmount); - newPtr.getRefData().setCount(1); - } else { - const MWWorld::LiveCellRef *ref = - ptr.get(); - newPtr = MWWorld::Ptr(cell.insert(ref), &cell); - newPtr.getRefData().setCount(count); + newPtr.getCellRef().setCount(count); } newPtr.getCellRef().unsetRefNum(); - + newPtr.getRefData().setLuaScripts(nullptr); + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); return newPtr; } - std::shared_ptr Miscellaneous::use (const MWWorld::Ptr& ptr, bool force) const + MWWorld::Ptr Miscellaneous::moveToCell(const MWWorld::Ptr& ptr, MWWorld::CellStore& cell) const { - if (ptr.getCellRef().getSoul().empty() || !MWBase::Environment::get().getWorld()->getStore().get().search(ptr.getCellRef().getSoul())) - return std::shared_ptr(new MWWorld::NullAction()); + MWWorld::Ptr newPtr; + if (isGold(ptr)) + { + newPtr = createGold(cell, getValue(ptr) * ptr.getCellRef().getCount()); + newPtr.getRefData() = ptr.getRefData(); + newPtr.getCellRef().setRefNum(ptr.getCellRef().getRefNum()); + } else - return std::shared_ptr(new MWWorld::ActionSoulgem(ptr)); + { + const MWWorld::LiveCellRef* ref = ptr.get(); + newPtr = MWWorld::Ptr(cell.insert(ref), &cell); + } + ptr.getRefData().setLuaScripts(nullptr); + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); + return newPtr; + } + + std::unique_ptr Miscellaneous::use(const MWWorld::Ptr& ptr, bool force) const + { + if (isSoulGem(ptr)) + return std::make_unique(ptr); + + return std::make_unique(); } - bool Miscellaneous::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Miscellaneous::canSell(const MWWorld::ConstPtr& item, int npcServices) const { - const MWWorld::LiveCellRef *ref = item.get(); + const MWWorld::LiveCellRef* ref = item.get(); - return !ref->mBase->mData.mIsKey && (npcServices & ESM::NPC::Misc) && !isGold(item); + return !(ref->mBase->mData.mFlags & ESM::Miscellaneous::Key) && (npcServices & ESM::NPC::Misc) && !isGold(item); } - float Miscellaneous::getWeight(const MWWorld::ConstPtr &ptr) const + float Miscellaneous::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } - bool Miscellaneous::isKey(const MWWorld::ConstPtr &ptr) const + bool Miscellaneous::isKey(const MWWorld::ConstPtr& ptr) const + { + const MWWorld::LiveCellRef* ref = ptr.get(); + return ref->mBase->mData.mFlags & ESM::Miscellaneous::Key; + } + + bool Miscellaneous::isSoulGem(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - return ref->mBase->mData.mIsKey != 0; + return ptr.getCellRef().getRefId().startsWith("misc_soulgem"); } } diff --git a/apps/openmw/mwclass/misc.hpp b/apps/openmw/mwclass/misc.hpp index 9bff85ca567..6b7b8389536 100644 --- a/apps/openmw/mwclass/misc.hpp +++ b/apps/openmw/mwclass/misc.hpp @@ -1,60 +1,64 @@ #ifndef GAME_MWCLASS_MISC_H #define GAME_MWCLASS_MISC_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Miscellaneous : public MWWorld::Class + class Miscellaneous : public MWWorld::RegisteredClass { - public: + friend MWWorld::RegisteredClass; - MWWorld::Ptr copyToCell(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell, int count) const override; + Miscellaneous(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + public: + MWWorld::Ptr copyToCell(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell, int count) const override; + MWWorld::Ptr moveToCell(const MWWorld::Ptr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + bool isItem(const MWWorld::ConstPtr&) const override { return true; } - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - static void registerSelf(); + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu - float getWeight (const MWWorld::ConstPtr& ptr) const override; + float getWeight(const MWWorld::ConstPtr& ptr) const override; - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; - bool isKey (const MWWorld::ConstPtr &ptr) const override; + bool isKey(const MWWorld::ConstPtr& ptr) const override; - bool isGold (const MWWorld::ConstPtr& ptr) const override; + bool isGold(const MWWorld::ConstPtr& ptr) const override; + + bool isSoulGem(const MWWorld::ConstPtr& ptr) const override; }; } diff --git a/apps/openmw/mwclass/nameorid.hpp b/apps/openmw/mwclass/nameorid.hpp new file mode 100644 index 00000000000..b001cc5fc83 --- /dev/null +++ b/apps/openmw/mwclass/nameorid.hpp @@ -0,0 +1,25 @@ +#ifndef OPENMW_MWCLASS_NAMEORID_H +#define OPENMW_MWCLASS_NAMEORID_H + +#include + +#include "../mwworld/livecellref.hpp" +#include "../mwworld/ptr.hpp" + +#include + +namespace MWClass +{ + template + std::string_view getNameOrId(const MWWorld::ConstPtr& ptr) + { + const MWWorld::LiveCellRef* ref = ptr.get(); + if (!ref->mBase->mName.empty()) + return ref->mBase->mName; + if (const auto* id = ref->mBase->mId.template getIf()) + return id->getValue(); + return {}; + } +} + +#endif diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 933ba5cd38f..6e833854e07 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -1,128 +1,159 @@ #include "npc.hpp" +#include +#include + +#include #include #include +#include #include #include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" -#include "../mwbase/windowmanager.hpp" -#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/aisetting.hpp" +#include "../mwmechanics/autocalcspell.hpp" +#include "../mwmechanics/combat.hpp" +#include "../mwmechanics/creaturecustomdataresetter.hpp" #include "../mwmechanics/creaturestats.hpp" -#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/difficultyscaling.hpp" +#include "../mwmechanics/disease.hpp" +#include "../mwmechanics/inventory.hpp" #include "../mwmechanics/movement.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/setbaseaisetting.hpp" #include "../mwmechanics/spellcasting.hpp" -#include "../mwmechanics/disease.hpp" -#include "../mwmechanics/combat.hpp" -#include "../mwmechanics/autocalcspell.hpp" -#include "../mwmechanics/difficultyscaling.hpp" #include "../mwmechanics/weapontype.hpp" -#include "../mwmechanics/actorutil.hpp" -#include "../mwworld/ptr.hpp" -#include "../mwworld/actiontalk.hpp" #include "../mwworld/actionopen.hpp" +#include "../mwworld/actiontalk.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/customdata.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/inventorystore.hpp" -#include "../mwworld/customdata.hpp" -#include "../mwworld/cellstore.hpp" #include "../mwworld/localscripts.hpp" +#include "../mwworld/ptr.hpp" +#include "../mwworld/worldmodel.hpp" +#include "../mwrender/npcanimation.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" -#include "../mwrender/npcanimation.hpp" #include "../mwgui/tooltips.hpp" +#include "nameorid.hpp" + namespace { + struct NpcParts + { + const ESM::RefId mSwimLeft = ESM::RefId::stringRefId("Swim Left"); + const ESM::RefId mSwimRight = ESM::RefId::stringRefId("Swim Right"); + const ESM::RefId mFootWaterLeft = ESM::RefId::stringRefId("FootWaterLeft"); + const ESM::RefId mFootWaterRight = ESM::RefId::stringRefId("FootWaterRight"); + const ESM::RefId mFootBareLeft = ESM::RefId::stringRefId("FootBareLeft"); + const ESM::RefId mFootBareRight = ESM::RefId::stringRefId("FootBareRight"); + const ESM::RefId mFootLightLeft = ESM::RefId::stringRefId("footLightLeft"); + const ESM::RefId mFootLightRight = ESM::RefId::stringRefId("footLightRight"); + const ESM::RefId mFootMediumRight = ESM::RefId::stringRefId("FootMedRight"); + const ESM::RefId mFootMediumLeft = ESM::RefId::stringRefId("FootMedLeft"); + const ESM::RefId mFootHeavyLeft = ESM::RefId::stringRefId("footHeavyLeft"); + const ESM::RefId mFootHeavyRight = ESM::RefId::stringRefId("footHeavyRight"); + }; + + const NpcParts npcParts; - int is_even(double d) { + int is_even(double d) + { double int_part; modf(d / 2.0, &int_part); return 2.0 * int_part == d; } - int round_ieee_754(double d) { + int round_ieee_754(double d) + { double i = floor(d); d -= i; - if(d < 0.5) + if (d < 0.5) return static_cast(i); - if(d > 0.5) + if (d > 0.5) return static_cast(i) + 1; - if(is_even(i)) + if (is_even(i)) return static_cast(i); return static_cast(i) + 1; } - void autoCalculateAttributes (const ESM::NPC* npc, MWMechanics::CreatureStats& creatureStats) + void autoCalculateAttributes(const ESM::NPC* npc, MWMechanics::CreatureStats& creatureStats) { // race bonus - const ESM::Race *race = - MWBase::Environment::get().getWorld()->getStore().get().find(npc->mRace); + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(npc->mRace); bool male = (npc->mFlags & ESM::NPC::Female) == 0; + const auto& attributes = MWBase::Environment::get().getESMStore()->get(); int level = creatureStats.getLevel(); - for (int i=0; imData.mAttributeValues[i]; - creatureStats.setAttribute(i, male ? attribute.mMale : attribute.mFemale); - } + for (const ESM::Attribute& attribute : attributes) + creatureStats.setAttribute(attribute.mId, race->mData.getAttribute(attribute.mId, male)); // class bonus - const ESM::Class *class_ = - MWBase::Environment::get().getWorld()->getStore().get().find(npc->mClass); + const ESM::Class* class_ = MWBase::Environment::get().getESMStore()->get().find(npc->mClass); - for (int i=0; i<2; ++i) + for (int attribute : class_->mData.mAttribute) { - int attribute = class_->mData.mAttribute[i]; - if (attribute>=0 && attribute<8) + if (attribute >= 0 && attribute < ESM::Attribute::Length) { - creatureStats.setAttribute(attribute, - creatureStats.getAttribute(attribute).getBase() + 10); + auto id = ESM::Attribute::indexToRefId(attribute); + creatureStats.setAttribute(id, creatureStats.getAttribute(id).getBase() + 10); } } // skill bonus - for (int attribute=0; attribute < ESM::Attribute::Length; ++attribute) + for (const ESM::Attribute& attribute : attributes) { float modifierSum = 0; + int attributeIndex = ESM::Attribute::refIdToIndex(attribute.mId); - for (int j=0; jget()) { - const ESM::Skill* skill = MWBase::Environment::get().getWorld()->getStore().get().find(j); - - if (skill->mData.mAttribute != attribute) + if (skill.mData.mAttribute != attributeIndex) continue; // is this a minor or major skill? - float add=0.2f; - for (int k=0; k<5; ++k) + float add = 0.2f; + int index = ESM::Skill::refIdToIndex(skill.mId); + for (const auto& skills : class_->mData.mSkills) { - if (class_->mData.mSkills[k][0] == j) - add=0.5; - } - for (int k=0; k<5; ++k) - { - if (class_->mData.mSkills[k][1] == j) - add=1.0; + if (skills[0] == index) + add = 0.5; + if (skills[1] == index) + add = 1.0; } modifierSum += add; } - creatureStats.setAttribute(attribute, std::min( - round_ieee_754(creatureStats.getAttribute(attribute).getBase() - + (level-1) * modifierSum), 100) ); + creatureStats.setAttribute(attribute.mId, + std::min( + round_ieee_754(creatureStats.getAttribute(attribute.mId).getBase() + (level - 1) * modifierSum), + 100)); } // initial health @@ -136,8 +167,9 @@ namespace else if (class_->mData.mSpecialization == ESM::Class::Stealth) multiplier += 1; - if (class_->mData.mAttribute[0] == ESM::Attribute::Endurance - || class_->mData.mAttribute[1] == ESM::Attribute::Endurance) + if (std::find(class_->mData.mAttribute.begin(), class_->mData.mAttribute.end(), + ESM::Attribute::refIdToIndex(ESM::Attribute::Endurance)) + != class_->mData.mAttribute.end()) multiplier += 1; creatureStats.setHealth(floor(0.5f * (strength + endurance)) + multiplier * (creatureStats.getLevel() - 1)); @@ -157,31 +189,30 @@ namespace * * and by adding class, race, specialization bonus. */ - void autoCalculateSkills(const ESM::NPC* npc, MWMechanics::NpcStats& npcStats, const MWWorld::Ptr& ptr, bool spellsInitialised) + void autoCalculateSkills( + const ESM::NPC* npc, MWMechanics::NpcStats& npcStats, const MWWorld::Ptr& ptr, bool spellsInitialised) { - const ESM::Class *class_ = - MWBase::Environment::get().getWorld()->getStore().get().find(npc->mClass); + const ESM::Class* class_ = MWBase::Environment::get().getESMStore()->get().find(npc->mClass); unsigned int level = npcStats.getLevel(); - const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().find(npc->mRace); - + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(npc->mRace); for (int i = 0; i < 2; ++i) { - int bonus = (i==0) ? 10 : 25; + int bonus = (i == 0) ? 10 : 25; - for (int i2 = 0; i2 < 5; ++i2) + for (const auto& skills : class_->mData.mSkills) { - int index = class_->mData.mSkills[i2][i]; - if (index >= 0 && index < ESM::Skill::Length) + ESM::RefId id = ESM::Skill::indexToRefId(skills[i]); + if (!id.empty()) { - npcStats.getSkill(index).setBase (npcStats.getSkill(index).getBase() + bonus); + npcStats.getSkill(id).setBase(npcStats.getSkill(id).getBase() + bonus); } } } - for (int skillIndex = 0; skillIndex < ESM::Skill::Length; ++skillIndex) + for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) { float majorMultiplier = 0.1f; float specMultiplier = 0.0f; @@ -189,19 +220,16 @@ namespace int raceBonus = 0; int specBonus = 0; - for (int raceSkillIndex = 0; raceSkillIndex < 7; ++raceSkillIndex) - { - if (race->mData.mBonus[raceSkillIndex].mSkill == skillIndex) - { - raceBonus = race->mData.mBonus[raceSkillIndex].mBonus; - break; - } - } + int index = ESM::Skill::refIdToIndex(skill.mId); + auto bonusIt = std::find_if(race->mData.mBonus.begin(), race->mData.mBonus.end(), + [&](const auto& bonus) { return bonus.mSkill == index; }); + if (bonusIt != race->mData.mBonus.end()) + raceBonus = bonusIt->mBonus; - for (int k = 0; k < 5; ++k) + for (const auto& skills : class_->mData.mSkills) { // is this a minor or major skill? - if ((class_->mData.mSkills[k][0] == skillIndex) || (class_->mData.mSkills[k][1] == skillIndex)) + if (std::find(skills.begin(), skills.end(), index) != skills.end()) { majorMultiplier = 1.0f; break; @@ -209,34 +237,22 @@ namespace } // is this skill in the same Specialization as the class? - const ESM::Skill* skill = MWBase::Environment::get().getWorld()->getStore().get().find(skillIndex); - if (skill->mData.mSpecialization == class_->mData.mSpecialization) + if (skill.mData.mSpecialization == class_->mData.mSpecialization) { specMultiplier = 0.5f; specBonus = 5; } - npcStats.getSkill(skillIndex).setBase( - std::min( - round_ieee_754( - npcStats.getSkill(skillIndex).getBase() - + 5 - + raceBonus - + specBonus - +(int(level)-1) * (majorMultiplier + specMultiplier)), 100)); // Must gracefully handle level 0 + npcStats.getSkill(skill.mId).setBase( + std::min(round_ieee_754(npcStats.getSkill(skill.mId).getBase() + 5 + raceBonus + specBonus + + (int(level) - 1) * (majorMultiplier + specMultiplier)), + 100)); // Must gracefully handle level 0 } - int skills[ESM::Skill::Length]; - for (int i=0; i spells = MWMechanics::autoCalcNpcSpells(skills, attributes, race); + std::vector spells + = MWMechanics::autoCalcNpcSpells(npcStats.getSkills(), npcStats.getAttributes(), race); npcStats.getSpells().addAllToInstance(spells); } } @@ -244,6 +260,10 @@ namespace namespace MWClass { + Npc::Npc() + : MWWorld::RegisteredClass(ESM::NPC::sRecordId) + { + } class NpcCustomData : public MWWorld::TypedCustomData { @@ -252,24 +272,17 @@ namespace MWClass MWMechanics::Movement mMovement; MWWorld::InventoryStore mInventoryStore; - NpcCustomData& asNpcCustomData() override - { - return *this; - } - const NpcCustomData& asNpcCustomData() const override - { - return *this; - } + NpcCustomData& asNpcCustomData() override { return *this; } + const NpcCustomData& asNpcCustomData() const override { return *this; } }; const Npc::GMST& Npc::getGmst() { - static GMST gmst; - static bool inited = false; - if(!inited) - { - const MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &store = world->getStore().get(); + static const GMST staticGmst = [] { + GMST gmst; + + const MWWorld::Store& store + = MWBase::Environment::get().getESMStore()->get(); gmst.fMinWalkSpeed = store.find("fMinWalkSpeed"); gmst.fMaxWalkSpeed = store.find("fMaxWalkSpeed"); @@ -292,55 +305,52 @@ namespace MWClass gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); gmst.fCombatArmorMinMult = store.find("fCombatArmorMinMult"); - inited = true; - } - return gmst; + return gmst; + }(); + return staticGmst; } - void Npc::ensureCustomData (const MWWorld::Ptr& ptr) const + void Npc::ensureCustomData(const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data(new NpcCustomData); + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + bool recalculate = false; + auto tempData = std::make_unique(); + NpcCustomData* data = tempData.get(); + MWMechanics::CreatureCustomDataResetter resetter{ ptr }; + ptr.getRefData().setCustomData(std::move(tempData)); - MWWorld::LiveCellRef *ref = ptr.get(); + MWWorld::LiveCellRef* ref = ptr.get(); bool spellsInitialised = data->mNpcStats.getSpells().setSpells(ref->mBase->mId); // creature stats - int gold=0; - if(ref->mBase->mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + int gold = 0; + if (ref->mBase->mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) { gold = ref->mBase->mNpdt.mGold; - for (unsigned int i=0; i< ESM::Skill::Length; ++i) - data->mNpcStats.getSkill (i).setBase (ref->mBase->mNpdt.mSkills[i]); + for (size_t i = 0; i < ref->mBase->mNpdt.mSkills.size(); ++i) + data->mNpcStats.getSkill(ESM::Skill::indexToRefId(i)).setBase(ref->mBase->mNpdt.mSkills[i]); - data->mNpcStats.setAttribute(ESM::Attribute::Strength, ref->mBase->mNpdt.mStrength); - data->mNpcStats.setAttribute(ESM::Attribute::Intelligence, ref->mBase->mNpdt.mIntelligence); - data->mNpcStats.setAttribute(ESM::Attribute::Willpower, ref->mBase->mNpdt.mWillpower); - data->mNpcStats.setAttribute(ESM::Attribute::Agility, ref->mBase->mNpdt.mAgility); - data->mNpcStats.setAttribute(ESM::Attribute::Speed, ref->mBase->mNpdt.mSpeed); - data->mNpcStats.setAttribute(ESM::Attribute::Endurance, ref->mBase->mNpdt.mEndurance); - data->mNpcStats.setAttribute(ESM::Attribute::Personality, ref->mBase->mNpdt.mPersonality); - data->mNpcStats.setAttribute(ESM::Attribute::Luck, ref->mBase->mNpdt.mLuck); + for (size_t i = 0; i < ref->mBase->mNpdt.mAttributes.size(); ++i) + data->mNpcStats.setAttribute(ESM::Attribute::indexToRefId(i), ref->mBase->mNpdt.mAttributes[i]); - data->mNpcStats.setHealth (ref->mBase->mNpdt.mHealth); - data->mNpcStats.setMagicka (ref->mBase->mNpdt.mMana); - data->mNpcStats.setFatigue (ref->mBase->mNpdt.mFatigue); + data->mNpcStats.setHealth(ref->mBase->mNpdt.mHealth); + data->mNpcStats.setMagicka(ref->mBase->mNpdt.mMana); + data->mNpcStats.setFatigue(ref->mBase->mNpdt.mFatigue); data->mNpcStats.setLevel(ref->mBase->mNpdt.mLevel); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt.mDisposition); data->mNpcStats.setReputation(ref->mBase->mNpdt.mReputation); - - data->mNpcStats.setNeedRecalcDynamicStats(false); } else { gold = ref->mBase->mNpdt.mGold; - for (int i=0; i<3; ++i) - data->mNpcStats.setDynamic (i, 10); + for (int i = 0; i < 3; ++i) + data->mNpcStats.setDynamic(i, 10); data->mNpcStats.setLevel(ref->mBase->mNpdt.mLevel); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt.mDisposition); @@ -349,7 +359,7 @@ namespace MWClass autoCalculateAttributes(ref->mBase, data->mNpcStats); autoCalculateSkills(ref->mBase, data->mNpcStats, ptr, spellsInitialised); - data->mNpcStats.setNeedRecalcDynamicStats(true); + recalculate = true; } // Persistent actors with 0 health do not play death animation @@ -357,273 +367,312 @@ namespace MWClass data->mNpcStats.setDeathAnimationFinished(isPersistent(ptr)); // race powers - const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(ref->mBase->mRace); data->mNpcStats.getSpells().addAllToInstance(race->mPowers.mList); if (!ref->mBase->mFaction.empty()) { - static const int iAutoRepFacMod = MWBase::Environment::get().getWorld()->getStore().get() - .find("iAutoRepFacMod")->mValue.getInteger(); - static const int iAutoRepLevMod = MWBase::Environment::get().getWorld()->getStore().get() - .find("iAutoRepLevMod")->mValue.getInteger(); + static const int iAutoRepFacMod = MWBase::Environment::get() + .getESMStore() + ->get() + .find("iAutoRepFacMod") + ->mValue.getInteger(); + static const int iAutoRepLevMod = MWBase::Environment::get() + .getESMStore() + ->get() + .find("iAutoRepLevMod") + ->mValue.getInteger(); int rank = ref->mBase->getFactionRank(); - data->mNpcStats.setReputation(iAutoRepFacMod * (rank+1) + iAutoRepLevMod * (data->mNpcStats.getLevel()-1)); + data->mNpcStats.setReputation( + iAutoRepFacMod * (rank + 1) + iAutoRepLevMod * (data->mNpcStats.getLevel() - 1)); } data->mNpcStats.getAiSequence().fill(ref->mBase->mAiPackage); - data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Hello, ref->mBase->mAiData.mHello); - data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, ref->mBase->mAiData.mFight); - data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, ref->mBase->mAiData.mFlee); - data->mNpcStats.setAiSetting (MWMechanics::CreatureStats::AI_Alarm, ref->mBase->mAiData.mAlarm); + data->mNpcStats.setAiSetting(MWMechanics::AiSetting::Hello, ref->mBase->mAiData.mHello); + data->mNpcStats.setAiSetting(MWMechanics::AiSetting::Fight, ref->mBase->mAiData.mFight); + data->mNpcStats.setAiSetting(MWMechanics::AiSetting::Flee, ref->mBase->mAiData.mFlee); + data->mNpcStats.setAiSetting(MWMechanics::AiSetting::Alarm, ref->mBase->mAiData.mAlarm); // spells if (!spellsInitialised) data->mNpcStats.getSpells().addAllToInstance(ref->mBase->mSpells.mList); - // inventory - // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items - data->mInventoryStore.fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); - data->mNpcStats.setGoldPool(gold); // store - ptr.getRefData().setCustomData(std::move(data)); + resetter.mPtr = {}; + if (recalculate) + data->mNpcStats.recalculateMagicka(); - getInventoryStore(ptr).autoEquip(ptr); + // inventory + // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + MWWorld::InventoryStore& inventory = getInventoryStore(ptr); + inventory.setPtr(ptr); + inventory.fill(ref->mBase->mInventory, ptr.getCellRef().getRefId(), prng); + inventory.autoEquip(); } } - void Npc::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + void Npc::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { renderingInterface.getObjects().insertNPC(ptr); } - bool Npc::isPersistent(const MWWorld::ConstPtr &actor) const + bool Npc::isPersistent(const MWWorld::ConstPtr& actor) const { const MWWorld::LiveCellRef* ref = actor.get(); - return ref->mBase->mPersistent; + return (ref->mBase->mRecordFlags & ESM::FLAG_Persistent) != 0; + } + + std::string_view Npc::getModel(const MWWorld::ConstPtr& ptr) const + { + const MWWorld::LiveCellRef* ref = ptr.get(); + std::string_view model = Settings::models().mBaseanim.get(); + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(ref->mBase->mRace); + if (race->mData.mFlags & ESM::Race::Beast) + model = Settings::models().mBaseanimkna.get(); + // Base animations should be in the meshes dir + constexpr std::string_view prefix = "meshes/"; + assert(VFS::Path::pathEqual(prefix, model.substr(0, prefix.size()))); + return model.substr(prefix.size()); } - std::string Npc::getModel(const MWWorld::ConstPtr &ptr) const + std::string Npc::getCorrectedModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); - std::string model = Settings::Manager::getString("baseanim", "Models"); - const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); - if(race->mData.mFlags & ESM::Race::Beast) - model = Settings::Manager::getString("baseanimkna", "Models"); + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(ref->mBase->mRace); + if (race->mData.mFlags & ESM::Race::Beast) + return Settings::models().mBaseanimkna.get().value(); - return model; + return Settings::models().mBaseanim.get().value(); } - void Npc::getModelsToPreload(const MWWorld::Ptr &ptr, std::vector &models) const + void Npc::getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const { - const MWWorld::LiveCellRef *npc = ptr.get(); - const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().search(npc->mBase->mRace); - if(race && race->mData.mFlags & ESM::Race::Beast) - models.emplace_back(Settings::Manager::getString("baseanimkna", "Models")); - - // keep these always loaded just in case - models.emplace_back(Settings::Manager::getString("xargonianswimkna", "Models")); - models.emplace_back(Settings::Manager::getString("xbaseanimfemale", "Models")); - models.emplace_back(Settings::Manager::getString("xbaseanim", "Models")); + const MWWorld::LiveCellRef* npc = ptr.get(); + const auto& esmStore = MWBase::Environment::get().getESMStore(); + models.push_back(getModel(ptr)); if (!npc->mBase->mModel.empty()) - models.push_back("meshes/"+npc->mBase->mModel); + models.push_back(npc->mBase->mModel); if (!npc->mBase->mHead.empty()) { - const ESM::BodyPart* head = MWBase::Environment::get().getWorld()->getStore().get().search(npc->mBase->mHead); + const ESM::BodyPart* head = esmStore->get().search(npc->mBase->mHead); if (head) - models.push_back("meshes/"+head->mModel); + models.push_back(head->mModel); } if (!npc->mBase->mHair.empty()) { - const ESM::BodyPart* hair = MWBase::Environment::get().getWorld()->getStore().get().search(npc->mBase->mHair); + const ESM::BodyPart* hair = esmStore->get().search(npc->mBase->mHair); if (hair) - models.push_back("meshes/"+hair->mModel); + models.push_back(hair->mModel); } bool female = (npc->mBase->mFlags & ESM::NPC::Female); - // FIXME: use const version of InventoryStore functions once they are available - // preload equipped items - const MWWorld::InventoryStore& invStore = getInventoryStore(ptr); - for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + const MWWorld::CustomData* customData = ptr.getRefData().getCustomData(); + if (customData) { - MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); - if (equipped != invStore.end()) + const MWWorld::InventoryStore& invStore = customData->asNpcCustomData().mInventoryStore; + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { - std::vector parts; - if(equipped->getTypeName() == typeid(ESM::Clothing).name()) - { - const ESM::Clothing *clothes = equipped->get()->mBase; - parts = clothes->mParts.mParts; - } - else if(equipped->getTypeName() == typeid(ESM::Armor).name()) - { - const ESM::Armor *armor = equipped->get()->mBase; - parts = armor->mParts.mParts; - } - else - { - std::string model = equipped->getClass().getModel(*equipped); - if (!model.empty()) - models.push_back(model); - } - - for (std::vector::const_iterator it = parts.begin(); it != parts.end(); ++it) + MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); + if (equipped != invStore.end()) { - std::string partname = female ? it->mFemale : it->mMale; - if (partname.empty()) - partname = female ? it->mMale : it->mFemale; - const ESM::BodyPart* part = MWBase::Environment::get().getWorld()->getStore().get().search(partname); - if (part && !part->mModel.empty()) - models.push_back("meshes/"+part->mModel); + const auto addParts = [&](const std::vector& parts) { + for (const ESM::PartReference& partRef : parts) + { + const ESM::RefId& partname + = (female && !partRef.mFemale.empty()) || (!female && partRef.mMale.empty()) + ? partRef.mFemale + : partRef.mMale; + + const ESM::BodyPart* part = esmStore->get().search(partname); + if (part && !part->mModel.empty()) + models.push_back(part->mModel); + } + }; + if (equipped->getType() == ESM::Clothing::sRecordId) + { + const ESM::Clothing* clothes = equipped->get()->mBase; + addParts(clothes->mParts.mParts); + } + else if (equipped->getType() == ESM::Armor::sRecordId) + { + const ESM::Armor* armor = equipped->get()->mBase; + addParts(armor->mParts.mParts); + } + else + { + std::string_view model = equipped->getClass().getModel(*equipped); + if (!model.empty()) + models.push_back(model); + } } } } // preload body parts - if (race) + if (const ESM::Race* race = esmStore->get().search(npc->mBase->mRace)) { - const std::vector& parts = MWRender::NpcAnimation::getBodyParts(Misc::StringUtils::lowerCase(race->mId), female, false, false); - for (std::vector::const_iterator it = parts.begin(); it != parts.end(); ++it) + const std::vector& parts + = MWRender::NpcAnimation::getBodyParts(race->mId, female, false, false); + for (const ESM::BodyPart* part : parts) { - const ESM::BodyPart* part = *it; if (part && !part->mModel.empty()) - models.push_back("meshes/"+part->mModel); + models.push_back(part->mModel); } } - } - std::string Npc::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Npc::getName(const MWWorld::ConstPtr& ptr) const { - if(ptr.getRefData().getCustomData() && ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats.isWerewolf()) + if (ptr.getRefData().getCustomData() + && ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats.isWerewolf()) { - const MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &store = world->getStore().get(); + const MWWorld::Store& store + = MWBase::Environment::get().getESMStore()->get(); return store.find("sWerewolfPopup")->mValue.getString(); } - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - MWMechanics::CreatureStats& Npc::getCreatureStats (const MWWorld::Ptr& ptr) const + MWMechanics::CreatureStats& Npc::getCreatureStats(const MWWorld::Ptr& ptr) const { - ensureCustomData (ptr); + ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats; } - MWMechanics::NpcStats& Npc::getNpcStats (const MWWorld::Ptr& ptr) const + MWMechanics::NpcStats& Npc::getNpcStats(const MWWorld::Ptr& ptr) const { - ensureCustomData (ptr); + ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats; } - - void Npc::hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const + bool Npc::evaluateHit(const MWWorld::Ptr& ptr, MWWorld::Ptr& victim, osg::Vec3f& hitPosition) const { - MWBase::World *world = MWBase::Environment::get().getWorld(); - - const MWWorld::Store &store = world->getStore().get(); + victim = MWWorld::Ptr(); + hitPosition = osg::Vec3f(); // Get the weapon used (if hand-to-hand, weapon = inv.end()) - MWWorld::InventoryStore &inv = getInventoryStore(ptr); + MWWorld::InventoryStore& inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - MWWorld::Ptr weapon = ((weaponslot != inv.end()) ? *weaponslot : MWWorld::Ptr()); - if(!weapon.isEmpty() && weapon.getTypeName() != typeid(ESM::Weapon).name()) - weapon = MWWorld::Ptr(); - - MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength); + MWWorld::Ptr weapon; + if (weaponslot != inv.end() && weaponslot->getType() == ESM::Weapon::sRecordId) + weapon = *weaponslot; + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Store& store = world->getStore().get(); const float fCombatDistance = store.find("fCombatDistance")->mValue.getFloat(); - float dist = fCombatDistance * (!weapon.isEmpty() ? - weapon.get()->mBase->mData.mReach : - store.find("fHandToHandReach")->mValue.getFloat()); - - // 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 (ptr != MWMechanics::getPlayer()) - getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors); - - // TODO: Use second to work out the hit angle - std::pair result = world->getHitContact(ptr, dist, targetActors); - MWWorld::Ptr victim = result.first; - osg::Vec3f hitPosition (result.second); - if(victim.isEmpty()) // Didn't hit anything - return; + float dist = fCombatDistance + * (!weapon.isEmpty() ? weapon.get()->mBase->mData.mReach + : store.find("fHandToHandReach")->mValue.getFloat()); - const MWWorld::Class &othercls = victim.getClass(); - if(!othercls.isActor()) // Can't hit non-actors - return; - MWMechanics::CreatureStats &otherstats = othercls.getCreatureStats(victim); - if(otherstats.isDead()) // Can't hit dead actors - return; + const std::pair result = MWMechanics::getHitContact(ptr, dist); + if (result.first.isEmpty()) // Didn't hit anything + return true; - if(ptr == MWMechanics::getPlayer()) - MWBase::Environment::get().getWindowManager()->setEnemy(victim); + // Note that earlier we returned true in spite of an apparent failure to hit anything alive. + // This is because hitting nothing is not a "miss" and should be handled as such character controller-side. + victim = result.first; + hitPosition = result.second; - int weapskill = ESM::Skill::HandToHand; - if(!weapon.isEmpty()) + ESM::RefId weapskill = ESM::Skill::HandToHand; + if (!weapon.isEmpty()) weapskill = weapon.getClass().getEquipmentSkill(weapon); float hitchance = MWMechanics::getHitChance(ptr, victim, getSkill(ptr, weapskill)); - if (Misc::Rng::roll0to99() >= hitchance) + return Misc::Rng::roll0to99(world->getPrng()) < hitchance; + } + + void Npc::hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim, + const osg::Vec3f& hitPosition, bool success) const + { + MWWorld::InventoryStore& inv = getInventoryStore(ptr); + MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + MWWorld::Ptr weapon; + if (weaponslot != inv.end() && weaponslot->getType() == ESM::Weapon::sRecordId) + weapon = *weaponslot; + + MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength); + + if (victim.isEmpty()) // Didn't hit anything + return; + + const MWWorld::Class& othercls = victim.getClass(); + MWMechanics::CreatureStats& otherstats = othercls.getCreatureStats(victim); + if (otherstats.isDead()) // Can't hit dead actors + return; + + if (ptr == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->setEnemy(victim); + + float damage = 0.0f; + if (!success) { - othercls.onHit(victim, 0.0f, false, weapon, ptr, osg::Vec3f(), false); - MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); + othercls.onHit( + victim, damage, false, weapon, ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); + MWMechanics::reduceWeaponCondition(damage, false, weapon, ptr); + MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); return; } bool healthdmg; - float damage = 0.0f; - if(!weapon.isEmpty()) + if (!weapon.isEmpty()) { - const unsigned char *attack = nullptr; - if(type == ESM::Weapon::AT_Chop) - attack = weapon.get()->mBase->mData.mChop; - else if(type == ESM::Weapon::AT_Slash) - attack = weapon.get()->mBase->mData.mSlash; - else if(type == ESM::Weapon::AT_Thrust) - attack = weapon.get()->mBase->mData.mThrust; - if(attack) + const unsigned char* attack = nullptr; + if (type == ESM::Weapon::AT_Chop) + attack = weapon.get()->mBase->mData.mChop.data(); + else if (type == ESM::Weapon::AT_Slash) + attack = weapon.get()->mBase->mData.mSlash.data(); + else if (type == ESM::Weapon::AT_Thrust) + attack = weapon.get()->mBase->mData.mThrust.data(); + if (attack) { - damage = attack[0] + ((attack[1]-attack[0])*attackStrength); + damage = attack[0] + ((attack[1] - attack[0]) * attackStrength); } MWMechanics::adjustWeaponDamage(damage, weapon, ptr); + MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); MWMechanics::applyWerewolfDamageMult(victim, weapon, damage); - MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); healthdmg = true; } else { MWMechanics::getHandToHandDamage(ptr, victim, damage, healthdmg, attackStrength); } - if(ptr == MWMechanics::getPlayer()) + + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Store& store = world->getStore().get(); + + if (ptr == MWMechanics::getPlayer()) { - skillUsageSucceeded(ptr, weapskill, 0); + ESM::RefId weapskill = ESM::Skill::HandToHand; + if (!weapon.isEmpty()) + weapskill = weapon.getClass().getEquipmentSkill(weapon); + skillUsageSucceeded(ptr, weapskill, ESM::Skill::Weapon_SuccessfulHit); const MWMechanics::AiSequence& seq = victim.getClass().getCreatureStats(victim).getAiSequence(); - bool unaware = !seq.isInCombat() - && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(ptr, victim); - if(unaware) + bool unaware + = !seq.isInCombat() && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(ptr, victim); + if (unaware) { damage *= store.find("fCombatCriticalStrikeMult")->mValue.getFloat(); MWBase::Environment::get().getWindowManager()->messageBox("#{sTargetCriticalStrike}"); - MWBase::Environment::get().getSoundManager()->playSound3D(victim, "critical damage", 1.0f, 1.0f); + MWBase::Environment::get().getSoundManager()->playSound3D( + victim, ESM::RefId::stringRefId("critical damage"), 1.0f, 1.0f); } } @@ -643,23 +692,29 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); + othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); } - void Npc::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const + void Npc::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); MWMechanics::CreatureStats& stats = getCreatureStats(ptr); bool wasDead = stats.isDead(); - // Note OnPcHitMe is not set for friendly hits. bool setOnPcHitMe = true; // NOTE: 'object' and/or 'attacker' may be empty. if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker)) { stats.setAttacked(true); - 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 @@ -668,14 +723,12 @@ namespace MWClass MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker); // First handle the attacked actor if ((stats.getHitAttemptActorId() == -1) - && (statsAttacker.getAiSequence().isInCombat(ptr) - || attacker == MWMechanics::getPlayer())) + && (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer())) stats.setHitAttemptActorId(statsAttacker.getActorId()); // Next handle the attacking actor if ((statsAttacker.getHitAttemptActorId() == -1) - && (statsAttacker.getAiSequence().isInCombat(ptr) - || attacker == MWMechanics::getPlayer())) + && (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer())) statsAttacker.setHitAttemptActorId(stats.getActorId()); } @@ -684,9 +737,9 @@ namespace MWClass if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) { - const std::string &script = getScript(ptr); + const ESM::RefId& script = getScript(ptr); /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ - if(!script.empty()) + if (!script.empty()) ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); } @@ -694,17 +747,13 @@ namespace MWClass { // Missed if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) - sndMgr->playSound3D(ptr, "miss", 1.0f, 1.0f); + sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f); return; } if (!object.isEmpty()) stats.setLastHitObject(object.getCellRef().getRefId()); - - if (damage > 0.0f && !object.isEmpty()) - MWMechanics::resistNormalWeapon(ptr, attacker, object, damage); - if (damage < 0.001f) damage = 0; @@ -718,18 +767,21 @@ namespace MWClass // 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying // something, alert the character controller, scripts, etc. - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const GMST& gmst = getGmst(); int chance = store.get().find("iVoiceHitOdds")->mValue.getInteger(); - if (Misc::Rng::roll0to99() < chance) - MWBase::Environment::get().getDialogueManager()->say(ptr, "hit"); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::roll0to99(prng) < chance) + MWBase::Environment::get().getDialogueManager()->say(ptr, ESM::RefId::stringRefId("hit")); // Check for knockdown - float agilityTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * gmst.fKnockDownMult->mValue.getFloat(); + float agilityTerm + = stats.getAttribute(ESM::Attribute::Agility).getModified() * gmst.fKnockDownMult->mValue.getFloat(); float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() - * gmst.iKnockDownOddsMult->mValue.getInteger() * 0.01f + gmst.iKnockDownOddsBase->mValue.getInteger(); - if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99()) + * gmst.iKnockDownOddsMult->mValue.getInteger() * 0.01f + + gmst.iKnockDownOddsBase->mValue.getInteger(); + if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng)) stats.setKnockedDown(true); else stats.setHitRecovery(true); // Is this supposed to always occur? @@ -740,19 +792,18 @@ namespace MWClass // cuirass = 30% // shield, helmet, greaves, boots, pauldrons = 10% each // guantlets = 5% each - static const int hitslots[20] = { - MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_CarriedLeft, - MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Helmet, - MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Greaves, - MWWorld::InventoryStore::Slot_Boots, MWWorld::InventoryStore::Slot_Boots, - MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_LeftPauldron, - MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_RightPauldron, - MWWorld::InventoryStore::Slot_LeftGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet - }; - int hitslot = hitslots[Misc::Rng::rollDice(20)]; + static const int hitslots[20] + = { MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, + MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, + MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, + MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_CarriedLeft, + MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Helmet, + MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Greaves, + MWWorld::InventoryStore::Slot_Boots, MWWorld::InventoryStore::Slot_Boots, + MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_LeftPauldron, + MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_RightPauldron, + MWWorld::InventoryStore::Slot_LeftGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet }; + int hitslot = hitslots[Misc::Rng::rollDice(20, prng)]; float unmitigatedDamage = damage; float x = damage / (damage + getArmorRating(ptr)); @@ -761,14 +812,14 @@ namespace MWClass damage = std::max(1.f, damage); damageDiff = std::max(1, damageDiff); - MWWorld::InventoryStore &inv = getInventoryStore(ptr); + MWWorld::InventoryStore& inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot); MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr()); - bool hasArmor = !armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name(); + bool hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; // If there's no item in the carried left slot or if it is not a shield redistribute the hit. if (!hasArmor && hitslot == MWWorld::InventoryStore::Slot_CarriedLeft) { - if (Misc::Rng::rollDice(2) == 0) + if (Misc::Rng::rollDice(2, prng) == 0) hitslot = MWWorld::InventoryStore::Slot_Cuirass; else hitslot = MWWorld::InventoryStore::Slot_LeftPauldron; @@ -776,12 +827,15 @@ namespace MWClass if (armorslot != inv.end()) { armor = *armorslot; - hasArmor = !armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name(); + hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; } } if (hasArmor) { - if (!object.isEmpty() || attacker.isEmpty() || attacker.getClass().isNpc()) // Unarmed creature attacks don't affect armor condition + // Unarmed creature attacks don't affect armor condition unless it was + // explicitly requested. + if (!object.isEmpty() || attacker.isEmpty() || attacker.getClass().isNpc() + || Settings::game().mUnarmedCreatureAttacksDamageArmor) { int armorhealth = armor.getClass().getItemHealth(armor); armorhealth -= std::min(damageDiff, armorhealth); @@ -789,27 +843,22 @@ namespace MWClass // Armor broken? unequip it if (armorhealth == 0) - armor = *inv.unequipItem(armor, ptr); + armor = *inv.unequipItem(armor); } + ESM::RefId skill = armor.getClass().getEquipmentSkill(armor); if (ptr == MWMechanics::getPlayer()) - skillUsageSucceeded(ptr, armor.getClass().getEquipmentSkill(armor), 0); - - switch(armor.getClass().getEquipmentSkill(armor)) - { - case ESM::Skill::LightArmor: - sndMgr->playSound3D(ptr, "Light Armor Hit", 1.0f, 1.0f); - break; - case ESM::Skill::MediumArmor: - sndMgr->playSound3D(ptr, "Medium Armor Hit", 1.0f, 1.0f); - break; - case ESM::Skill::HeavyArmor: - sndMgr->playSound3D(ptr, "Heavy Armor Hit", 1.0f, 1.0f); - break; - } + skillUsageSucceeded(ptr, skill, ESM::Skill::Armor_HitByOpponent); + + if (skill == ESM::Skill::LightArmor) + sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f); + else if (skill == ESM::Skill::MediumArmor) + sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Medium Armor Hit"), 1.0f, 1.0f); + else if (skill == ESM::Skill::HeavyArmor) + sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f); } - else if(ptr == MWMechanics::getPlayer()) - skillUsageSucceeded(ptr, ESM::Skill::Unarmored, 0); + else if (ptr == MWMechanics::getPlayer()) + skillUsageSucceeded(ptr, ESM::Skill::Unarmored, ESM::Skill::Armor_HitByOpponent); } } @@ -820,7 +869,7 @@ namespace MWClass if (damage > 0.0f) { - sndMgr->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); + sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f); if (ptr == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->activateHitOverlay(); if (!attacker.isEmpty()) @@ -840,7 +889,8 @@ namespace MWClass if (!wasDead && getCreatureStats(ptr).isDead()) { // NPC was killed - if (!attacker.isEmpty() && attacker.getClass().isNpc() && attacker.getClass().getNpcStats(attacker).isWerewolf()) + if (!attacker.isEmpty() && attacker.getClass().isNpc() + && attacker.getClass().getNpcStats(attacker).isWerewolf()) { attacker.getClass().getNpcStats(attacker).addWerewolfKill(); } @@ -849,81 +899,77 @@ namespace MWClass } } - std::shared_ptr Npc::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Npc::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { // player got activated by another NPC - if(ptr == MWMechanics::getPlayer()) - return std::shared_ptr(new MWWorld::ActionTalk(actor)); + if (ptr == MWMechanics::getPlayer()) + return std::make_unique(actor); // Werewolfs can't activate NPCs - if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfNPC"); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound* sound = store.get().searchRandom("WolfNPC", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); - if(sound) action->setSound(sound->mId); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); + if (sound) + action->setSound(sound->mId); return action; } const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); + const MWMechanics::AiSequence& aiSequence = stats.getAiSequence(); + const bool isPursuing = aiSequence.isInPursuit() && actor == MWMechanics::getPlayer(); + const bool inCombatWithActor = aiSequence.isInCombat(actor) || isPursuing; - if(stats.isDead()) + if (stats.isDead()) { - bool canLoot = Settings::Manager::getBool ("can loot during death animation", "Game"); - - // by default user can loot friendly actors during death animation - if (canLoot && !stats.getAiSequence().isInCombat()) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); + // by default user can loot non-fighting actors during death animation + if (Settings::game().mCanLootDuringDeathAnimation) + return std::make_unique(ptr); // otherwise wait until death animation - if(stats.isDeathAnimationFinished()) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); - } - else if (!stats.getAiSequence().isInCombat()) - { - if (stats.getKnockedDown() || MWBase::Environment::get().getMechanicsManager()->isSneaking(actor)) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); // stealing - - // Can't talk to werewolves - if (!getNpcStats(ptr).isWerewolf()) - return std::shared_ptr(new MWWorld::ActionTalk(ptr)); + if (stats.isDeathAnimationFinished()) + return std::make_unique(ptr); } - else // In combat + else { - const bool stealingInCombat = Settings::Manager::getBool ("always allow stealing from knocked out actors", "Game"); - if (stealingInCombat && stats.getKnockedDown()) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); // stealing + const bool allowStealingFromKO + = Settings::game().mAlwaysAllowStealingFromKnockedOutActors || !inCombatWithActor; + if (stats.getKnockedDown() && allowStealingFromKO) + return std::make_unique(ptr); + + const bool allowStealingWhileSneaking = !inCombatWithActor; + if (MWBase::Environment::get().getMechanicsManager()->isSneaking(actor) && allowStealingWhileSneaking) + return std::make_unique(ptr); + + const bool allowTalking = !inCombatWithActor && !getNpcStats(ptr).isWerewolf(); + if (allowTalking) + return std::make_unique(ptr); } - // Tribunal and some mod companions oddly enough must use open action as fallback - if (!getScript(ptr).empty() && ptr.getRefData().getLocals().getIntVar(getScript(ptr), "companion")) - return std::shared_ptr(new MWWorld::ActionOpen(ptr)); + if (inCombatWithActor) + return std::make_unique("#{sActorInCombat}"); - return std::shared_ptr (new MWWorld::FailedAction("")); + return std::make_unique(); } - MWWorld::ContainerStore& Npc::getContainerStore (const MWWorld::Ptr& ptr) - const + MWWorld::ContainerStore& Npc::getContainerStore(const MWWorld::Ptr& ptr) const { - ensureCustomData (ptr); - - return ptr.getRefData().getCustomData()->asNpcCustomData().mInventoryStore; + return getInventoryStore(ptr); } - MWWorld::InventoryStore& Npc::getInventoryStore (const MWWorld::Ptr& ptr) - const + MWWorld::InventoryStore& Npc::getInventoryStore(const MWWorld::Ptr& ptr) const { - ensureCustomData (ptr); - + ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mInventoryStore; } - std::string Npc::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Npc::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } @@ -932,16 +978,14 @@ namespace MWClass { // TODO: This function is called several times per frame for each NPC. // It would be better to calculate it only once per frame for each NPC and save the result in CreatureStats. - const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); - bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead()) + const MWMechanics::NpcStats& stats = getNpcStats(ptr); + if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead()) return 0.f; - const MWBase::World *world = MWBase::Environment::get().getWorld(); + const MWBase::World* world = MWBase::Environment::get().getWorld(); const GMST& gmst = getGmst(); - const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); - const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); + const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); @@ -952,14 +996,15 @@ namespace MWClass running = running && (inair || MWBase::Environment::get().getMechanicsManager()->isRunning(ptr)); float moveSpeed; - if(getEncumbrance(ptr) > getCapacity(ptr)) + if (getEncumbrance(ptr) > getCapacity(ptr)) moveSpeed = 0.0f; - else if(mageffects.get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && - world->isLevitationEnabled()) + else if (mageffects.getOrDefault(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled()) { - float flySpeed = 0.01f*(npcdata->mNpcStats.getAttribute(ESM::Attribute::Speed).getModified() + - mageffects.get(ESM::MagicEffect::Levitate).getMagnitude()); - flySpeed = gmst.fMinFlySpeed->mValue.getFloat() + flySpeed*(gmst.fMaxFlySpeed->mValue.getFloat() - gmst.fMinFlySpeed->mValue.getFloat()); + float flySpeed = 0.01f + * (stats.getAttribute(ESM::Attribute::Speed).getModified() + + mageffects.getOrDefault(ESM::MagicEffect::Levitate).getMagnitude()); + flySpeed = gmst.fMinFlySpeed->mValue.getFloat() + + flySpeed * (gmst.fMaxFlySpeed->mValue.getFloat() - gmst.fMinFlySpeed->mValue.getFloat()); flySpeed *= 1.0f - gmst.fEncumberedMoveEffect->mValue.getFloat() * normalizedEncumbrance; flySpeed = std::max(0.0f, flySpeed); moveSpeed = flySpeed; @@ -971,72 +1016,63 @@ namespace MWClass else moveSpeed = getWalkSpeed(ptr); - if(npcdata->mNpcStats.isWerewolf() && running && npcdata->mNpcStats.getDrawState() == MWMechanics::DrawState_Nothing) + if (stats.isWerewolf() && running && stats.getDrawState() == MWMechanics::DrawState::Nothing) moveSpeed *= gmst.fWereWolfRunMult->mValue.getFloat(); return moveSpeed; } - float Npc::getJump(const MWWorld::Ptr &ptr) const + float Npc::getJump(const MWWorld::Ptr& ptr) const { - if(getEncumbrance(ptr) > getCapacity(ptr)) + if (getEncumbrance(ptr) > getCapacity(ptr)) return 0.f; - const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); - bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead()) + const MWMechanics::NpcStats& stats = getNpcStats(ptr); + if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead()) return 0.f; - const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); const GMST& gmst = getGmst(); - const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); - const float encumbranceTerm = gmst.fJumpEncumbranceBase->mValue.getFloat() + - gmst.fJumpEncumbranceMultiplier->mValue.getFloat() * - (1.0f - Npc::getNormalizedEncumbrance(ptr)); + const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); + const float encumbranceTerm = gmst.fJumpEncumbranceBase->mValue.getFloat() + + gmst.fJumpEncumbranceMultiplier->mValue.getFloat() * (1.0f - Npc::getNormalizedEncumbrance(ptr)); float a = getSkill(ptr, ESM::Skill::Acrobatics); float b = 0.0f; - if(a > 50.0f) + if (a > 50.0f) { b = a - 50.0f; a = 50.0f; } - float x = gmst.fJumpAcrobaticsBase->mValue.getFloat() + - std::pow(a / 15.0f, gmst.fJumpAcroMultiplier->mValue.getFloat()); + float x = gmst.fJumpAcrobaticsBase->mValue.getFloat() + + std::pow(a / 15.0f, gmst.fJumpAcroMultiplier->mValue.getFloat()); x += 3.0f * b * gmst.fJumpAcroMultiplier->mValue.getFloat(); - x += mageffects.get(ESM::MagicEffect::Jump).getMagnitude() * 64; + x += mageffects.getOrDefault(ESM::MagicEffect::Jump).getMagnitude() * 64; x *= encumbranceTerm; - if(stats.getStance(MWMechanics::CreatureStats::Stance_Run)) + if (stats.getStance(MWMechanics::CreatureStats::Stance_Run)) x *= gmst.fJumpRunMultiplier->mValue.getFloat(); - x *= npcdata->mNpcStats.getFatigueTerm(); + x *= stats.getFatigueTerm(); x -= -Constants::GravityConst * Constants::UnitsPerMeter; x /= 3.0f; return x; } - MWMechanics::Movement& Npc::getMovementSettings (const MWWorld::Ptr& ptr) const + MWMechanics::Movement& Npc::getMovementSettings(const MWWorld::Ptr& ptr) const { - ensureCustomData (ptr); + ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mMovement; } - bool Npc::isEssential (const MWWorld::ConstPtr& ptr) const + bool Npc::isEssential(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return (ref->mBase->mFlags & ESM::NPC::Essential) != 0; } - void Npc::registerSelf() - { - std::shared_ptr instance (new Npc); - registerClass (typeid (ESM::NPC).name(), instance); - } - bool Npc::hasToolTip(const MWWorld::ConstPtr& ptr) const { if (!ptr.getRefData().getCustomData() || MWBase::Environment::get().getWindowManager()->isGuiMode()) @@ -1047,92 +1083,79 @@ namespace MWClass if (customData.mNpcStats.isDead() && customData.mNpcStats.isDeathAnimationFinished()) return true; - if (!customData.mNpcStats.getAiSequence().isInCombat()) + const MWMechanics::AiSequence& aiSeq = customData.mNpcStats.getAiSequence(); + if (!aiSeq.isInCombat() || aiSeq.isFleeing()) return true; - const bool stealingInCombat = Settings::Manager::getBool ("always allow stealing from knocked out actors", "Game"); - if (stealingInCombat && customData.mNpcStats.getKnockedDown()) + if (Settings::game().mAlwaysAllowStealingFromKnockedOutActors && customData.mNpcStats.getKnockedDown()) return true; return false; } - MWGui::ToolTipInfo Npc::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Npc::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); bool fullHelp = MWBase::Environment::get().getWindowManager()->getFullHelp(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)); - if(fullHelp && !ref->mBase->mName.empty() && ptr.getRefData().getCustomData() && ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats.isWerewolf()) + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); + if (fullHelp && !ref->mBase->mName.empty() && ptr.getRefData().getCustomData() + && ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats.isWerewolf()) { info.caption += " ("; info.caption += MyGUI::TextIterator::toTagsString(ref->mBase->mName); info.caption += ")"; } - if(fullHelp) - info.text = MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (fullHelp) + info.extra = MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); return info; } - float Npc::getCapacity (const MWWorld::Ptr& ptr) const + float Npc::getCapacity(const MWWorld::Ptr& ptr) const { - const MWMechanics::CreatureStats& stats = getCreatureStats (ptr); - static const float fEncumbranceStrMult = MWBase::Environment::get().getWorld()->getStore().get().find("fEncumbranceStrMult")->mValue.getFloat(); - return stats.getAttribute(ESM::Attribute::Strength).getModified()*fEncumbranceStrMult; + const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); + static const float fEncumbranceStrMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fEncumbranceStrMult") + ->mValue.getFloat(); + return stats.getAttribute(ESM::Attribute::Strength).getModified() * fEncumbranceStrMult; } - float Npc::getEncumbrance (const MWWorld::Ptr& ptr) const + float Npc::getEncumbrance(const MWWorld::Ptr& ptr) const { // According to UESP, inventory weight is ignored in werewolf form. Does that include // feather and burden effects? return getNpcStats(ptr).isWerewolf() ? 0.0f : Actor::getEncumbrance(ptr); } - bool Npc::apply (const MWWorld::Ptr& ptr, const std::string& id, - const MWWorld::Ptr& actor) const + void Npc::skillUsageSucceeded(const MWWorld::Ptr& ptr, ESM::RefId skill, int usageType, float extraFactor) const { - MWMechanics::CastSpell cast(ptr, ptr); - return cast.cast(id); + MWBase::Environment::get().getLuaManager()->skillUse(ptr, skill, usageType, extraFactor); } - void Npc::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor) const + float Npc::getArmorRating(const MWWorld::Ptr& ptr) const { - MWMechanics::NpcStats& stats = getNpcStats (ptr); - - if (stats.isWerewolf()) - return; + const MWWorld::Store& store + = MWBase::Environment::get().getESMStore()->get(); - MWWorld::LiveCellRef *ref = ptr.get(); - - const ESM::Class *class_ = - MWBase::Environment::get().getWorld()->getStore().get().find ( - ref->mBase->mClass - ); - - stats.useSkill (skill, *class_, usageType, extraFactor); - } - - float Npc::getArmorRating (const MWWorld::Ptr& ptr) const - { - const MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &store = world->getStore().get(); - - MWMechanics::NpcStats &stats = getNpcStats(ptr); - const MWWorld::InventoryStore &invStore = getInventoryStore(ptr); + MWMechanics::NpcStats& stats = getNpcStats(ptr); + const MWWorld::InventoryStore& invStore = getInventoryStore(ptr); float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat(); float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat(); float unarmoredSkill = getSkill(ptr, ESM::Skill::Unarmored); float ratings[MWWorld::InventoryStore::Slots]; - for(int i = 0;i < MWWorld::InventoryStore::Slots;i++) + for (int i = 0; i < MWWorld::InventoryStore::Slots; i++) { MWWorld::ConstContainerStoreIterator it = invStore.getSlot(i); - if (it == invStore.end() || it->getTypeName() != typeid(ESM::Armor).name()) + if (it == invStore.end() || it->getType() != ESM::Armor::sRecordId) { // unarmored ratings[i] = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); @@ -1150,168 +1173,181 @@ namespace MWClass } } - float shield = stats.getMagicEffects().get(ESM::MagicEffect::Shield).getMagnitude(); + float shield = stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Shield).getMagnitude(); return ratings[MWWorld::InventoryStore::Slot_Cuirass] * 0.3f - + (ratings[MWWorld::InventoryStore::Slot_CarriedLeft] + ratings[MWWorld::InventoryStore::Slot_Helmet] - + ratings[MWWorld::InventoryStore::Slot_Greaves] + ratings[MWWorld::InventoryStore::Slot_Boots] - + ratings[MWWorld::InventoryStore::Slot_LeftPauldron] + ratings[MWWorld::InventoryStore::Slot_RightPauldron] - ) * 0.1f - + (ratings[MWWorld::InventoryStore::Slot_LeftGauntlet] + ratings[MWWorld::InventoryStore::Slot_RightGauntlet]) - * 0.05f - + shield; + + (ratings[MWWorld::InventoryStore::Slot_CarriedLeft] + ratings[MWWorld::InventoryStore::Slot_Helmet] + + ratings[MWWorld::InventoryStore::Slot_Greaves] + ratings[MWWorld::InventoryStore::Slot_Boots] + + ratings[MWWorld::InventoryStore::Slot_LeftPauldron] + + ratings[MWWorld::InventoryStore::Slot_RightPauldron]) + * 0.1f + + (ratings[MWWorld::InventoryStore::Slot_LeftGauntlet] + + ratings[MWWorld::InventoryStore::Slot_RightGauntlet]) + * 0.05f + + shield; } - void Npc::adjustScale(const MWWorld::ConstPtr &ptr, osg::Vec3f&scale, bool rendering) const + void Npc::adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const { if (!rendering) return; // collision meshes are not scaled based on race height // having the same collision extents for all races makes the environments easier to test - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); - const ESM::Race* race = - MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(ref->mBase->mRace); - // Race weight should not affect 1st-person meshes, otherwise it will change hand proportions and can break aiming. + // Race weight should not affect 1st-person meshes, otherwise it will change hand proportions and can break + // aiming. if (ptr == MWMechanics::getPlayer() && ptr.isInCell() && MWBase::Environment::get().getWorld()->isFirstPerson()) { if (ref->mBase->isMale()) - scale *= race->mData.mHeight.mMale; + scale *= race->mData.mMaleHeight; else - scale *= race->mData.mHeight.mFemale; + scale *= race->mData.mFemaleHeight; return; } if (ref->mBase->isMale()) { - scale.x() *= race->mData.mWeight.mMale; - scale.y() *= race->mData.mWeight.mMale; - scale.z() *= race->mData.mHeight.mMale; + scale.x() *= race->mData.mMaleWeight; + scale.y() *= race->mData.mMaleWeight; + scale.z() *= race->mData.mMaleHeight; } else { - scale.x() *= race->mData.mWeight.mFemale; - scale.y() *= race->mData.mWeight.mFemale; - scale.z() *= race->mData.mHeight.mFemale; + scale.x() *= race->mData.mFemaleWeight; + scale.y() *= race->mData.mFemaleWeight; + scale.z() *= race->mData.mFemaleHeight; } } - int Npc::getServices(const MWWorld::ConstPtr &actor) const + int Npc::getServices(const MWWorld::ConstPtr& actor) const { - return actor.get()->mBase->mAiData.mServices; + const ESM::NPC* npc = actor.get()->mBase; + if (npc->mFlags & ESM::NPC::Autocalc) + { + const ESM::Class* class_ = MWBase::Environment::get().getESMStore()->get().find(npc->mClass); + return class_->mData.mServices; + } + return npc->mAiData.mServices; } - - std::string Npc::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const + ESM::RefId Npc::getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const { - if(name == "left" || name == "right") + if (name == "left" || name == "right") { - MWBase::World *world = MWBase::Environment::get().getWorld(); - if(world->isFlying(ptr)) - return std::string(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + if (world->isFlying(ptr)) + return ESM::RefId(); osg::Vec3f pos(ptr.getRefData().getPosition().asVec3()); - if(world->isSwimming(ptr)) - return (name == "left") ? "Swim Left" : "Swim Right"; - if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) - return (name == "left") ? "FootWaterLeft" : "FootWaterRight"; - if(world->isOnGround(ptr)) + if (world->isSwimming(ptr)) + return (name == "left") ? npcParts.mSwimLeft : npcParts.mSwimRight; + if (world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) + return (name == "left") ? npcParts.mFootWaterLeft : npcParts.mFootWaterRight; + if (world->isOnGround(ptr)) { if (getNpcStats(ptr).isWerewolf() - && getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run)) + && getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run)) { int weaponType = ESM::Weapon::None; MWMechanics::getActiveWeapon(ptr, &weaponType); if (weaponType == ESM::Weapon::None) - return std::string(); + return ESM::RefId(); } - const MWWorld::InventoryStore &inv = Npc::getInventoryStore(ptr); + const MWWorld::InventoryStore& inv = Npc::getInventoryStore(ptr); MWWorld::ConstContainerStoreIterator boots = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); - if(boots == inv.end() || boots->getTypeName() != typeid(ESM::Armor).name()) - return (name == "left") ? "FootBareLeft" : "FootBareRight"; - - switch(boots->getClass().getEquipmentSkill(*boots)) - { - case ESM::Skill::LightArmor: - return (name == "left") ? "FootLightLeft" : "FootLightRight"; - case ESM::Skill::MediumArmor: - return (name == "left") ? "FootMedLeft" : "FootMedRight"; - case ESM::Skill::HeavyArmor: - return (name == "left") ? "FootHeavyLeft" : "FootHeavyRight"; - } + if (boots == inv.end() || boots->getType() != ESM::Armor::sRecordId) + return (name == "left") ? npcParts.mFootBareLeft : npcParts.mFootBareRight; + + ESM::RefId skill = boots->getClass().getEquipmentSkill(*boots); + if (skill == ESM::Skill::LightArmor) + return (name == "left") ? npcParts.mFootLightLeft : npcParts.mFootLightRight; + else if (skill == ESM::Skill::MediumArmor) + return (name == "left") ? npcParts.mFootMediumLeft : npcParts.mFootMediumRight; + else if (skill == ESM::Skill::HeavyArmor) + return (name == "left") ? npcParts.mFootHeavyLeft : npcParts.mFootHeavyRight; } - return std::string(); + return ESM::RefId(); } // Morrowind ignores land soundgen for NPCs - if(name == "land") - return std::string(); - if(name == "swimleft") - return "Swim Left"; - if(name == "swimright") - return "Swim Right"; + if (name == "land") + return ESM::RefId(); + if (name == "swimleft") + return npcParts.mSwimLeft; + if (name == "swimright") + return npcParts.mSwimRight; // TODO: I have no idea what these are supposed to do for NPCs since they use // voiced dialog for various conditions like health loss and combat taunts. Maybe // only for biped creatures? - if(name == "moan") - return std::string(); - if(name == "roar") - return std::string(); - if(name == "scream") - return std::string(); + if (name == "moan") + return ESM::RefId(); + if (name == "roar") + return ESM::RefId(); + if (name == "scream") + return ESM::RefId(); - throw std::runtime_error(std::string("Unexpected soundgen type: ")+name); + throw std::runtime_error("Unexpected soundgen type: " + std::string(name)); } - MWWorld::Ptr Npc::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Npc::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - return MWWorld::Ptr(cell.insert(ref), &cell); + const MWWorld::LiveCellRef* ref = ptr.get(); + MWWorld::Ptr newPtr(cell.insert(ref), &cell); + if (newPtr.getRefData().getCustomData()) + { + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); + newPtr.getClass().getContainerStore(newPtr).setPtr(newPtr); + } + return newPtr; } - float Npc::getSkill(const MWWorld::Ptr& ptr, int skill) const + float Npc::getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const { - return getNpcStats(ptr).getSkill(skill).getModified(); + return getNpcStats(ptr).getSkill(id).getModified(); } - int Npc::getBloodTexture(const MWWorld::ConstPtr &ptr) const + int Npc::getBloodTexture(const MWWorld::ConstPtr& ptr) const { return ptr.get()->mBase->mBloodType; } - void Npc::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) - const + void Npc::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; - if (state.mVersion > 0) + const ESM::NpcState& npcState = state.asNpcState(); + + if (!ptr.getRefData().getCustomData()) { - if (!ptr.getRefData().getCustomData()) + if (npcState.mCreatureStats.mMissingACDT) + ensureCustomData(ptr); + else { // Create a CustomData, but don't fill it from ESM records (not needed) - ptr.getRefData().setCustomData(std::make_unique()); + auto data = std::make_unique(); + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + data->mInventoryStore.setPtr(ptr); + ptr.getRefData().setCustomData(std::move(data)); } } - else - ensureCustomData(ptr); // in openmw 0.30 savegames not all state was saved yet, so need to load it regardless. NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); - const ESM::NpcState& npcState = state.asNpcState(); - customData.mInventoryStore.readState (npcState.mInventory); - customData.mNpcStats.readState (npcState.mNpcStats); + + customData.mInventoryStore.readState(npcState.mInventory); + customData.mNpcStats.readState(npcState.mNpcStats); bool spellsInitialised = customData.mNpcStats.getSpells().setSpells(ptr.get()->mBase->mId); - if(spellsInitialised) + if (spellsInitialised) customData.mNpcStats.getSpells().clear(); - customData.mNpcStats.readState (npcState.mCreatureStats); + customData.mNpcStats.readState(npcState.mCreatureStats); } - void Npc::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) - const + void Npc::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { @@ -1319,100 +1355,104 @@ namespace MWClass return; } - if (ptr.getRefData().getCount() <= 0) + const NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); + if (ptr.getCellRef().getCount() <= 0 + && (!(ptr.get()->mBase->mFlags & ESM::NPC::Respawn) || !customData.mNpcStats.isDead())) { state.mHasCustomState = false; return; } - const NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); ESM::NpcState& npcState = state.asNpcState(); - customData.mInventoryStore.writeState (npcState.mInventory); - customData.mNpcStats.writeState (npcState.mNpcStats); - customData.mNpcStats.writeState (npcState.mCreatureStats); + customData.mInventoryStore.writeState(npcState.mInventory); + customData.mNpcStats.writeState(npcState.mNpcStats); + customData.mNpcStats.writeState(npcState.mCreatureStats); } int Npc::getBaseGold(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mNpdt.mGold; } - bool Npc::isClass(const MWWorld::ConstPtr& ptr, const std::string &className) const + bool Npc::isClass(const MWWorld::ConstPtr& ptr, std::string_view className) const { - return Misc::StringUtils::ciEqual(ptr.get()->mBase->mClass, className); + return ptr.get()->mBase->mClass == className; } - bool Npc::canSwim(const MWWorld::ConstPtr &ptr) const + bool Npc::canSwim(const MWWorld::ConstPtr& ptr) const { return true; } - bool Npc::canWalk(const MWWorld::ConstPtr &ptr) const + bool Npc::canWalk(const MWWorld::ConstPtr& ptr) const { return true; } - void Npc::respawn(const MWWorld::Ptr &ptr) const + void Npc::respawn(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr); - if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) + if (ptr.getCellRef().getCount() > 0 && !creatureStats.isDead()) return; if (!creatureStats.isDeathAnimationFinished()) return; - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->mValue.getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->mValue.getFloat(); - float delay = ptr.getRefData().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); + float delay + = ptr.getCellRef().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (ptr.get()->mBase->mFlags & ESM::NPC::Respawn - && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) + && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) { if (ptr.getCellRef().hasContentFile()) { - if (ptr.getRefData().getCount() == 0) + if (ptr.getCellRef().getCount() == 0) { - ptr.getRefData().setCount(1); - const std::string& script = getScript(ptr); + ptr.getCellRef().setCount(1); + const ESM::RefId& script = getScript(ptr); if (!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, ptr); } MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); + MWBase::Environment::get().getWindowManager()->onDeleteCustomData(ptr); ptr.getRefData().setCustomData(nullptr); // Reset to original position - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], - ptr.getCellRef().getPosition().pos[1], - ptr.getCellRef().getPosition().pos[2]); + MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->rotateObject( + ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } } } - int Npc::getBaseFightRating (const MWWorld::ConstPtr& ptr) const + int Npc::getBaseFightRating(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mAiData.mFight; } - bool Npc::isBipedal(const MWWorld::ConstPtr &ptr) const + bool Npc::isBipedal(const MWWorld::ConstPtr& ptr) const { return true; } - std::string Npc::getPrimaryFaction (const MWWorld::ConstPtr& ptr) const + ESM::RefId Npc::getPrimaryFaction(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mFaction; } - int Npc::getPrimaryFactionRank (const MWWorld::ConstPtr& ptr) const + int Npc::getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const { - std::string factionID = ptr.getClass().getPrimaryFaction(ptr); - if(factionID.empty()) + const ESM::RefId& factionID = ptr.getClass().getPrimaryFaction(ptr); + if (factionID.empty()) return -1; // Search in the NPC data first @@ -1424,16 +1464,16 @@ namespace MWClass } // Use base NPC record as a fallback - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->getFactionRank(); } - void Npc::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const + void Npc::setBaseAISetting(const ESM::RefId& id, MWMechanics::AiSetting setting, int value) const { MWMechanics::setBaseAISetting(id, setting, value); } - void Npc::modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const + void Npc::modifyBaseInventory(const ESM::RefId& actorId, const ESM::RefId& itemId, int amount) const { MWMechanics::modifyBaseInventory(actorId, itemId, amount); } @@ -1441,16 +1481,16 @@ namespace MWClass float Npc::getWalkSpeed(const MWWorld::Ptr& ptr) const { const GMST& gmst = getGmst(); - const NpcCustomData* npcdata = static_cast(ptr.getRefData().getCustomData()); + const MWMechanics::NpcStats& stats = getNpcStats(ptr); const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); const bool sneaking = MWBase::Environment::get().getMechanicsManager()->isSneaking(ptr); float walkSpeed = gmst.fMinWalkSpeed->mValue.getFloat() - + 0.01f * npcdata->mNpcStats.getAttribute(ESM::Attribute::Speed).getModified() + + 0.01f * stats.getAttribute(ESM::Attribute::Speed).getModified() * (gmst.fMaxWalkSpeed->mValue.getFloat() - gmst.fMinWalkSpeed->mValue.getFloat()); - walkSpeed *= 1.0f - gmst.fEncumberedMoveEffect->mValue.getFloat()*normalizedEncumbrance; + walkSpeed *= 1.0f - gmst.fEncumberedMoveEffect->mValue.getFloat() * normalizedEncumbrance; walkSpeed = std::max(0.0f, walkSpeed); - if(sneaking) + if (sneaking) walkSpeed *= gmst.fSneakSpeedMultiplier->mValue.getFloat(); return walkSpeed; @@ -1460,33 +1500,20 @@ namespace MWClass { const GMST& gmst = getGmst(); return getWalkSpeed(ptr) - * (0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fAthleticsRunBonus->mValue.getFloat() - + gmst.fBaseRunMultiplier->mValue.getFloat()); + * (0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fAthleticsRunBonus->mValue.getFloat() + + gmst.fBaseRunMultiplier->mValue.getFloat()); } float Npc::getSwimSpeed(const MWWorld::Ptr& ptr) const { - const GMST& gmst = getGmst(); const MWBase::World* world = MWBase::Environment::get().getWorld(); - const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); - const NpcCustomData* npcdata = static_cast(ptr.getRefData().getCustomData()); - const MWMechanics::MagicEffects& mageffects = npcdata->mNpcStats.getMagicEffects(); + 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)); - - float swimSpeed; - - if (running) - swimSpeed = getRunSpeed(ptr); - else - swimSpeed = getWalkSpeed(ptr); - - swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude(); - swimSpeed *= gmst.fSwimRunBase->mValue.getFloat() - + 0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->mValue.getFloat(); + && (inair || MWBase::Environment::get().getMechanicsManager()->isRunning(ptr)); - return swimSpeed; + return getSwimSpeedImpl(ptr, getGmst(), mageffects, running ? getRunSpeed(ptr) : getWalkSpeed(ptr)); } } diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index 612763d12eb..1d70c5e1bab 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -1,6 +1,8 @@ #ifndef GAME_MWCLASS_NPC_H #define GAME_MWCLASS_NPC_H +#include "../mwworld/registeredclass.hpp" + #include "actor.hpp" namespace ESM @@ -10,167 +12,169 @@ namespace ESM namespace MWClass { - class Npc : public Actor + class Npc : public MWWorld::RegisteredClass { - void ensureCustomData (const MWWorld::Ptr& ptr) const; + friend MWWorld::RegisteredClass; + + Npc(); + + void ensureCustomData(const MWWorld::Ptr& ptr) const; - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - struct GMST - { - const ESM::GameSetting *fMinWalkSpeed; - const ESM::GameSetting *fMaxWalkSpeed; - const ESM::GameSetting *fEncumberedMoveEffect; - const ESM::GameSetting *fSneakSpeedMultiplier; - const ESM::GameSetting *fAthleticsRunBonus; - const ESM::GameSetting *fBaseRunMultiplier; - const ESM::GameSetting *fMinFlySpeed; - const ESM::GameSetting *fMaxFlySpeed; - const ESM::GameSetting *fSwimRunBase; - const ESM::GameSetting *fSwimRunAthleticsMult; - const ESM::GameSetting *fJumpEncumbranceBase; - const ESM::GameSetting *fJumpEncumbranceMultiplier; - const ESM::GameSetting *fJumpAcrobaticsBase; - const ESM::GameSetting *fJumpAcroMultiplier; - const ESM::GameSetting *fJumpRunMultiplier; - const ESM::GameSetting *fWereWolfRunMult; - const ESM::GameSetting *fKnockDownMult; - const ESM::GameSetting *iKnockDownOddsMult; - const ESM::GameSetting *iKnockDownOddsBase; - const ESM::GameSetting *fCombatArmorMinMult; - }; + struct GMST + { + const ESM::GameSetting* fMinWalkSpeed; + const ESM::GameSetting* fMaxWalkSpeed; + const ESM::GameSetting* fEncumberedMoveEffect; + const ESM::GameSetting* fSneakSpeedMultiplier; + const ESM::GameSetting* fAthleticsRunBonus; + const ESM::GameSetting* fBaseRunMultiplier; + const ESM::GameSetting* fMinFlySpeed; + const ESM::GameSetting* fMaxFlySpeed; + const ESM::GameSetting* fSwimRunBase; + const ESM::GameSetting* fSwimRunAthleticsMult; + const ESM::GameSetting* fJumpEncumbranceBase; + const ESM::GameSetting* fJumpEncumbranceMultiplier; + const ESM::GameSetting* fJumpAcrobaticsBase; + const ESM::GameSetting* fJumpAcroMultiplier; + const ESM::GameSetting* fJumpRunMultiplier; + const ESM::GameSetting* fWereWolfRunMult; + const ESM::GameSetting* fKnockDownMult; + const ESM::GameSetting* iKnockDownOddsMult; + const ESM::GameSetting* iKnockDownOddsBase; + const ESM::GameSetting* fCombatArmorMinMult; + }; - static const GMST& getGmst(); + static const GMST& getGmst(); - public: + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + MWMechanics::CreatureStats& getCreatureStats(const MWWorld::Ptr& ptr) const override; + ///< Return creature stats - MWMechanics::CreatureStats& getCreatureStats (const MWWorld::Ptr& ptr) const override; - ///< Return creature stats + MWMechanics::NpcStats& getNpcStats(const MWWorld::Ptr& ptr) const override; + ///< Return NPC stats - MWMechanics::NpcStats& getNpcStats (const MWWorld::Ptr& ptr) const override; - ///< Return NPC stats + MWWorld::ContainerStore& getContainerStore(const MWWorld::Ptr& ptr) const override; + ///< Return container store - MWWorld::ContainerStore& getContainerStore (const MWWorld::Ptr& ptr) const override; - ///< Return container store + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; + ///< @return true if this object has a tooltip when focused (default implementation: true) - bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; - ///< @return true if this object has a tooltip when focused (default implementation: true) + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + MWWorld::InventoryStore& getInventoryStore(const MWWorld::Ptr& ptr) const override; + ///< Return inventory store - MWWorld::InventoryStore& getInventoryStore (const MWWorld::Ptr& ptr) const override; - ///< Return inventory store + bool hasInventoryStore(const MWWorld::ConstPtr& ptr) const override { return true; } - bool hasInventoryStore(const MWWorld::Ptr &ptr) const override { return true; } + bool evaluateHit(const MWWorld::Ptr& ptr, MWWorld::Ptr& victim, osg::Vec3f& hitPosition) const override; - void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const override; + void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim, + const osg::Vec3f& hitPosition, bool success) const override; - void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const override; + void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const override; - void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; - ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel(). + void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const override; + ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: + ///< list getModel(). - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - float getMaxSpeed (const MWWorld::Ptr& ptr) const override; - ///< Return maximal movement speed. + float getMaxSpeed(const MWWorld::Ptr& ptr) const override; + ///< Return maximal movement speed. - float getJump(const MWWorld::Ptr &ptr) const override; - ///< Return jump velocity (not accounting for movement) + float getJump(const MWWorld::Ptr& ptr) const override; + ///< Return jump velocity (not accounting for movement) - MWMechanics::Movement& getMovementSettings (const MWWorld::Ptr& ptr) const override; - ///< Return desired movement. + MWMechanics::Movement& getMovementSettings(const MWWorld::Ptr& ptr) const override; + ///< Return desired movement. - float getCapacity (const MWWorld::Ptr& ptr) const override; - ///< Return total weight that fits into the object. Throws an exception, if the object can't - /// hold other objects. + float getCapacity(const MWWorld::Ptr& ptr) const override; + ///< Return total weight that fits into the object. Throws an exception, if the object can't + /// hold other objects. - float getEncumbrance (const MWWorld::Ptr& ptr) const override; - ///< Returns total weight of objects inside this object (including modifications from magic - /// effects). Throws an exception, if the object can't hold other objects. + float getEncumbrance(const MWWorld::Ptr& ptr) const override; + ///< Returns total weight of objects inside this object (including modifications from magic + /// effects). Throws an exception, if the object can't hold other objects. - float getArmorRating (const MWWorld::Ptr& ptr) const override; - ///< @return combined armor rating of this actor + float getArmorRating(const MWWorld::Ptr& ptr) const override; + ///< @return combined armor rating of this actor - bool apply (const MWWorld::Ptr& ptr, const std::string& id, - const MWWorld::Ptr& actor) const override; - ///< Apply \a id on \a ptr. - /// \param actor Actor that is resposible for the ID being applied to \a ptr. - /// \return Any effect? + void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const override; + /// @param rendering Indicates if the scale to adjust is for the rendering mesh, or for the collision mesh - void adjustScale (const MWWorld::ConstPtr &ptr, osg::Vec3f &scale, bool rendering) const override; - /// @param rendering Indicates if the scale to adjust is for the rendering mesh, or for the collision mesh + void skillUsageSucceeded( + const MWWorld::Ptr& ptr, ESM::RefId skill, int usageType, float extraFactor = 1.f) const override; + ///< Inform actor \a ptr that a skill use has succeeded. - void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor=1.f) const override; - ///< Inform actor \a ptr that a skill use has succeeded. + bool isEssential(const MWWorld::ConstPtr& ptr) const override; + ///< Is \a ptr essential? (i.e. may losing \a ptr make the game unwinnable) - bool isEssential (const MWWorld::ConstPtr& ptr) const override; - ///< Is \a ptr essential? (i.e. may losing \a ptr make the game unwinnable) + int getServices(const MWWorld::ConstPtr& actor) const override; - int getServices (const MWWorld::ConstPtr& actor) const override; + bool isPersistent(const MWWorld::ConstPtr& ptr) const override; - bool isPersistent (const MWWorld::ConstPtr& ptr) const override; + ESM::RefId getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const override; - std::string getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - static void registerSelf(); + std::string getCorrectedModel(const MWWorld::ConstPtr& ptr) const override; - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override; - float getSkill(const MWWorld::Ptr& ptr, int skill) const override; + /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) + int getBloodTexture(const MWWorld::ConstPtr& ptr) const override; - /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) - int getBloodTexture (const MWWorld::ConstPtr& ptr) const override; + bool isNpc() const override { return true; } - bool isNpc() const override - { - return true; - } + void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; + ///< Read additional state from \a state into \a ptr. - void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; - ///< Read additional state from \a state into \a ptr. + void writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; + ///< Write additional state from \a ptr into \a state. - void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const override; - ///< Write additional state from \a ptr into \a state. + int getBaseGold(const MWWorld::ConstPtr& ptr) const override; - int getBaseGold(const MWWorld::ConstPtr& ptr) const override; + bool isClass(const MWWorld::ConstPtr& ptr, std::string_view className) const override; - bool isClass(const MWWorld::ConstPtr& ptr, const std::string &className) const override; + bool canSwim(const MWWorld::ConstPtr& ptr) const override; - bool canSwim (const MWWorld::ConstPtr &ptr) const override; + bool canWalk(const MWWorld::ConstPtr& ptr) const override; - bool canWalk (const MWWorld::ConstPtr &ptr) const override; + bool isBipedal(const MWWorld::ConstPtr& ptr) const override; - bool isBipedal (const MWWorld::ConstPtr &ptr) const override; + void respawn(const MWWorld::Ptr& ptr) const override; - void respawn (const MWWorld::Ptr& ptr) const override; + int getBaseFightRating(const MWWorld::ConstPtr& ptr) const override; - int getBaseFightRating (const MWWorld::ConstPtr& ptr) const override; + ESM::RefId getPrimaryFaction(const MWWorld::ConstPtr& ptr) const override; - std::string getPrimaryFaction(const MWWorld::ConstPtr &ptr) const override; - int getPrimaryFactionRank(const MWWorld::ConstPtr &ptr) const override; + int getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const override; - void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const override; + void setBaseAISetting(const ESM::RefId& id, MWMechanics::AiSetting setting, int value) const override; - void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const override; + void modifyBaseInventory(const ESM::RefId& actorId, const ESM::RefId& itemId, int amount) const override; - float getWalkSpeed(const MWWorld::Ptr& ptr) const override; + float getWalkSpeed(const MWWorld::Ptr& ptr) const override; - float getRunSpeed(const MWWorld::Ptr& ptr) const override; + float getRunSpeed(const MWWorld::Ptr& ptr) const override; - float getSwimSpeed(const MWWorld::Ptr& ptr) const override; + float getSwimSpeed(const MWWorld::Ptr& ptr) const override; }; } diff --git a/apps/openmw/mwclass/potion.cpp b/apps/openmw/mwclass/potion.cpp index 4af97e63454..9dca460724e 100644 --- a/apps/openmw/mwclass/potion.cpp +++ b/apps/openmw/mwclass/potion.cpp @@ -1,17 +1,17 @@ #include "potion.hpp" -#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" +#include +#include -#include "../mwworld/ptr.hpp" #include "../mwworld/actionapply.hpp" #include "../mwworld/cellstore.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwphysics/physicssystem.hpp" -#include "../mwworld/nullaction.hpp" +#include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" @@ -19,146 +19,133 @@ #include "../mwrender/renderinginterface.hpp" #include "../mwmechanics/alchemy.hpp" +#include "../mwmechanics/spellutil.hpp" + +#include "classmodel.hpp" +#include "nameorid.hpp" namespace MWClass { - - void Potion::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + Potion::Potion() + : MWWorld::RegisteredClass(ESM::Potion::sRecordId) { - if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); - } } - void Potion::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Potion::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - // TODO: add option somewhere to enable collision for placeable objects + if (!model.empty()) + { + renderingInterface.getObjects().insertModel(ptr, model); + } } - std::string Potion::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Potion::getModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } - std::string Potion::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Potion::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - std::shared_ptr Potion::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Potion::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } - std::string Potion::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Potion::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = - ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - int Potion::getValue (const MWWorld::ConstPtr& ptr) const + int Potion::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - return ref->mBase->mData.mValue; - } - - void Potion::registerSelf() - { - std::shared_ptr instance (new Potion); - - registerClass (typeid (ESM::Potion).name(), instance); + return MWMechanics::getPotionValue(*ptr.get()->mBase); } - std::string Potion::getUpSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Potion::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Potion Up"); + static const auto sound = ESM::RefId::stringRefId("Item Potion Up"); + return sound; } - std::string Potion::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Potion::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Potion Down"); + static const auto sound = ESM::RefId::stringRefId("Item Potion Down"); + return sound; } - std::string Potion::getInventoryIcon (const MWWorld::ConstPtr& ptr) const + const std::string& Potion::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - MWGui::ToolTipInfo Potion::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Potion::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); + text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); info.effects = MWGui::Widgets::MWEffectList::effectListFromESM(&ref->mBase->mEffects); // hide effects the player doesn't know about - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); - for (unsigned int i=0; igetPlayerPtr(); + for (size_t i = 0; i < info.effects.size(); ++i) info.effects[i].mKnown = MWMechanics::Alchemy::knownEffect(i, player); info.isPotion = true; - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } - std::shared_ptr Potion::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Potion::use(const MWWorld::Ptr& ptr, bool force) const { - MWWorld::LiveCellRef *ref = - ptr.get(); + MWWorld::LiveCellRef* ref = ptr.get(); - std::shared_ptr action ( - new MWWorld::ActionApply (ptr, ref->mBase->mId)); + auto action = std::make_unique(ptr, ref->mBase->mId); - action->setSound ("Drink"); + action->setSound(ESM::RefId::stringRefId("Drink")); return action; } - MWWorld::Ptr Potion::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Potion::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - bool Potion::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Potion::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Potions) != 0; } - float Potion::getWeight(const MWWorld::ConstPtr &ptr) const + float Potion::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } diff --git a/apps/openmw/mwclass/potion.hpp b/apps/openmw/mwclass/potion.hpp index 75d923f0bad..057874ca1e1 100644 --- a/apps/openmw/mwclass/potion.hpp +++ b/apps/openmw/mwclass/potion.hpp @@ -1,56 +1,57 @@ #ifndef GAME_MWCLASS_POTION_H #define GAME_MWCLASS_POTION_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Potion : public MWWorld::Class + class Potion : public MWWorld::RegisteredClass { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + Potion(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + bool isItem(const MWWorld::ConstPtr&) const override { return true; } - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - static void registerSelf(); + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - float getWeight (const MWWorld::ConstPtr& ptr) const override; + float getWeight(const MWWorld::ConstPtr& ptr) const override; - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } diff --git a/apps/openmw/mwclass/probe.cpp b/apps/openmw/mwclass/probe.cpp index dba4e8c0631..4039f72cc51 100644 --- a/apps/openmw/mwclass/probe.cpp +++ b/apps/openmw/mwclass/probe.cpp @@ -1,116 +1,107 @@ #include "probe.hpp" -#include +#include +#include + +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwworld/ptr.hpp" #include "../mwworld/actionequip.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" -#include "../mwphysics/physicssystem.hpp" -#include "../mwworld/nullaction.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { - - void Probe::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + Probe::Probe() + : MWWorld::RegisteredClass(ESM::Probe::sRecordId) { - if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); - } } - void Probe::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Probe::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - // TODO: add option somewhere to enable collision for placeable objects + if (!model.empty()) + { + renderingInterface.getObjects().insertModel(ptr, model); + } } - std::string Probe::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Probe::getModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } - std::string Probe::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Probe::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - std::shared_ptr Probe::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Probe::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } - std::string Probe::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Probe::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = - ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - std::pair, bool> Probe::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const + std::pair, bool> Probe::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { std::vector slots_; - slots_.push_back (int (MWWorld::InventoryStore::Slot_CarriedRight)); + slots_.push_back(int(MWWorld::InventoryStore::Slot_CarriedRight)); - return std::make_pair (slots_, false); + return std::make_pair(slots_, false); } - int Probe::getValue (const MWWorld::ConstPtr& ptr) const + int Probe::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } - void Probe::registerSelf() - { - std::shared_ptr instance (new Probe); - - registerClass (typeid (ESM::Probe).name(), instance); - } - - std::string Probe::getUpSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Probe::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Probe Up"); + static const ESM::RefId sound = ESM::RefId::stringRefId("Item Probe Up"); + return sound; } - std::string Probe::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Probe::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Probe Down"); + static const ESM::RefId sound = ESM::RefId::stringRefId("Item Probe Down"); + return sound; } - std::string Probe::getInventoryIcon (const MWWorld::ConstPtr& ptr) const + const std::string& Probe::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - MWGui::ToolTipInfo Probe::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Probe::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -122,57 +113,58 @@ namespace MWClass text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } - std::shared_ptr Probe::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Probe::use(const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); return action; } - MWWorld::Ptr Probe::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Probe::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - std::pair Probe::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const + std::pair Probe::canBeEquipped(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { // Do not allow equip tools from inventory during attack if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) && MWBase::Environment::get().getWindowManager()->isGuiMode()) - return std::make_pair(0, "#{sCantEquipWeapWarning}"); + return { 0, "#{sCantEquipWeapWarning}" }; - return std::make_pair(1, ""); + return { 1, {} }; } - bool Probe::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Probe::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Probes) != 0; } - int Probe::getItemMaxHealth (const MWWorld::ConstPtr& ptr) const + int Probe::getItemMaxHealth(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mUses; } - float Probe::getWeight(const MWWorld::ConstPtr &ptr) const + float Probe::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } diff --git a/apps/openmw/mwclass/probe.hpp b/apps/openmw/mwclass/probe.hpp index a0a41dcfb6e..fc1092546ec 100644 --- a/apps/openmw/mwclass/probe.hpp +++ b/apps/openmw/mwclass/probe.hpp @@ -1,68 +1,70 @@ #ifndef GAME_MWCLASS_PROBE_H #define GAME_MWCLASS_PROBE_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Probe : public MWWorld::Class + class Probe : public MWWorld::RegisteredClass { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + Probe(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + bool isItem(const MWWorld::ConstPtr&) const override { return true; } - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; - ///< \return first: Return IDs of the slot this object can be equipped in; second: can object - /// stay stacked when equipped? + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + std::pair, bool> getEquipmentSlots(const MWWorld::ConstPtr& ptr) const override; + ///< \return first: Return IDs of the slot this object can be equipped in; second: can object + /// stay stacked when equipped? - static void registerSelf(); + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; + std::pair canBeEquipped( + const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const override; - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; - float getWeight (const MWWorld::ConstPtr& ptr) const override; + float getWeight(const MWWorld::ConstPtr& ptr) const override; - int getItemMaxHealth (const MWWorld::ConstPtr& ptr) const override; - ///< Return item max health or throw an exception, if class does not have item health + int getItemMaxHealth(const MWWorld::ConstPtr& ptr) const override; + ///< Return item max health or throw an exception, if class does not have item health - bool hasItemHealth (const MWWorld::ConstPtr& ptr) const override { return true; } - ///< \return Item health data available? (default implementation: false) + bool hasItemHealth(const MWWorld::ConstPtr& ptr) const override { return true; } + ///< \return Item health data available? (default implementation: false) }; } diff --git a/apps/openmw/mwclass/repair.cpp b/apps/openmw/mwclass/repair.cpp index 8907c8212e0..922b33b67e0 100644 --- a/apps/openmw/mwclass/repair.cpp +++ b/apps/openmw/mwclass/repair.cpp @@ -1,117 +1,109 @@ #include "repair.hpp" -#include +#include +#include + +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwworld/ptr.hpp" -#include "../mwworld/cellstore.hpp" -#include "../mwphysics/physicssystem.hpp" #include "../mwworld/actionrepair.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { - - void Repair::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + Repair::Repair() + : MWWorld::RegisteredClass(ESM::Repair::sRecordId) { - if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); - } } - void Repair::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Repair::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - // TODO: add option somewhere to enable collision for placeable objects + if (!model.empty()) + { + renderingInterface.getObjects().insertModel(ptr, model); + } } - std::string Repair::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Repair::getModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } - std::string Repair::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Repair::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - std::shared_ptr Repair::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Repair::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } - std::string Repair::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Repair::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = - ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - int Repair::getValue (const MWWorld::ConstPtr& ptr) const + int Repair::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } - void Repair::registerSelf() - { - std::shared_ptr instance (new Repair); - - registerClass (typeid (ESM::Repair).name(), instance); - } - - std::string Repair::getUpSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Repair::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Repair Up"); + static auto val = ESM::RefId::stringRefId("Item Repair Up"); + return val; } - std::string Repair::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Repair::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - return std::string("Item Repair Down"); + static auto val = ESM::RefId::stringRefId("Item Repair Down"); + return val; } - std::string Repair::getInventoryIcon (const MWWorld::ConstPtr& ptr) const + const std::string& Repair::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - bool Repair::hasItemHealth (const MWWorld::ConstPtr& ptr) const + bool Repair::hasItemHealth(const MWWorld::ConstPtr& ptr) const { return true; } - int Repair::getItemMaxHealth (const MWWorld::ConstPtr& ptr) const + int Repair::getItemMaxHealth(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mUses; } - MWGui::ToolTipInfo Repair::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Repair::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; std::string text; @@ -123,36 +115,37 @@ namespace MWClass text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } - MWWorld::Ptr Repair::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Repair::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - std::shared_ptr Repair::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Repair::use(const MWWorld::Ptr& ptr, bool force) const { - return std::shared_ptr(new MWWorld::ActionRepair(ptr, force)); + return std::make_unique(ptr, force); } - bool Repair::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Repair::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::RepairItem) != 0; } - float Repair::getWeight(const MWWorld::ConstPtr &ptr) const + float Repair::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } diff --git a/apps/openmw/mwclass/repair.hpp b/apps/openmw/mwclass/repair.hpp index b9791e9cf46..50c58231ce6 100644 --- a/apps/openmw/mwclass/repair.hpp +++ b/apps/openmw/mwclass/repair.hpp @@ -1,64 +1,65 @@ #ifndef GAME_MWCLASS_REPAIR_H #define GAME_MWCLASS_REPAIR_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Repair : public MWWorld::Class + class Repair : public MWWorld::RegisteredClass { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + Repair(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + bool isItem(const MWWorld::ConstPtr&) const override { return true; } - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - static void registerSelf(); + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu (default implementation: return a - /// null action). + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu (default implementation: return a + /// null action). - bool hasItemHealth (const MWWorld::ConstPtr& ptr) const override; - ///< \return Item health data available? (default implementation: false) + bool hasItemHealth(const MWWorld::ConstPtr& ptr) const override; + ///< \return Item health data available? (default implementation: false) - int getItemMaxHealth (const MWWorld::ConstPtr& ptr) const override; - ///< Return item max health or throw an exception, if class does not have item health - /// (default implementation: throw an exception) + int getItemMaxHealth(const MWWorld::ConstPtr& ptr) const override; + ///< Return item max health or throw an exception, if class does not have item health + /// (default implementation: throw an exception) - float getWeight (const MWWorld::ConstPtr& ptr) const override; + float getWeight(const MWWorld::ConstPtr& ptr) const override; - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } diff --git a/apps/openmw/mwclass/static.cpp b/apps/openmw/mwclass/static.cpp index 5551b3d7315..502a4fcb661 100644 --- a/apps/openmw/mwclass/static.cpp +++ b/apps/openmw/mwclass/static.cpp @@ -1,20 +1,28 @@ #include "static.hpp" -#include +#include +#include #include -#include "../mwworld/ptr.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/ptr.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwrender/vismask.hpp" +#include "classmodel.hpp" + namespace MWClass { + Static::Static() + : MWWorld::RegisteredClass(ESM::Static::sRecordId) + { + } - void Static::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + void Static::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { @@ -23,43 +31,36 @@ namespace MWClass } } - void Static::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Static::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const { - if(!model.empty()) - physics.addObject(ptr, model); + insertObjectPhysics(ptr, model, rotation, physics); } - std::string Static::getModel(const MWWorld::ConstPtr &ptr) const + void Static::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); } - std::string Static::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Static::getModel(const MWWorld::ConstPtr& ptr) const { - return ""; + return getClassModel(ptr); } - bool Static::hasToolTip(const MWWorld::ConstPtr& ptr) const + std::string_view Static::getName(const MWWorld::ConstPtr& ptr) const { - return false; + return {}; } - void Static::registerSelf() + bool Static::hasToolTip(const MWWorld::ConstPtr& ptr) const { - std::shared_ptr instance (new Static); - - registerClass (typeid (ESM::Static).name(), instance); + return false; } - MWWorld::Ptr Static::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Static::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } diff --git a/apps/openmw/mwclass/static.hpp b/apps/openmw/mwclass/static.hpp index 6bc783dad09..a3ea68a86f2 100644 --- a/apps/openmw/mwclass/static.hpp +++ b/apps/openmw/mwclass/static.hpp @@ -1,30 +1,38 @@ #ifndef GAME_MWCLASS_STATIC_H #define GAME_MWCLASS_STATIC_H -#include "../mwworld/class.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/registeredclass.hpp" + +#include namespace MWClass { - class Static : public MWWorld::Class + class Static : public MWWorld::RegisteredClass { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + Static(); - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + public: + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override; - bool hasToolTip (const MWWorld::ConstPtr& ptr) const override; - ///< @return true if this object has a tooltip when focused (default implementation: true) + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - static void registerSelf(); + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override; + ///< @return true if this object has a tooltip when focused (default implementation: true) - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; }; } diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 0d6a27cf607..089c8c28942 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -1,21 +1,22 @@ #include "weapon.hpp" -#include +#include +#include + +#include +#include #include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwworld/ptr.hpp" #include "../mwworld/actionequip.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwphysics/physicssystem.hpp" -#include "../mwworld/nullaction.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/ptr.hpp" #include "../mwmechanics/weapontype.hpp" @@ -24,72 +25,65 @@ #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" +#include "classmodel.hpp" +#include "nameorid.hpp" + namespace MWClass { - - void Weapon::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const + Weapon::Weapon() + : MWWorld::RegisteredClass(ESM::Weapon::sRecordId) { - if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); - } } - void Weapon::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const + void Weapon::insertObjectRendering( + const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - // TODO: add option somewhere to enable collision for placeable objects + if (!model.empty()) + { + renderingInterface.getObjects().insertModel(ptr, model); + } } - std::string Weapon::getModel(const MWWorld::ConstPtr &ptr) const + std::string_view Weapon::getModel(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - - const std::string &model = ref->mBase->mModel; - if (!model.empty()) { - return "meshes\\" + model; - } - return ""; + return getClassModel(ptr); } - std::string Weapon::getName (const MWWorld::ConstPtr& ptr) const + std::string_view Weapon::getName(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); - const std::string& name = ref->mBase->mName; - - return !name.empty() ? name : ref->mBase->mId; + return getNameOrId(ptr); } - std::shared_ptr Weapon::activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const + std::unique_ptr Weapon::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } - bool Weapon::hasItemHealth (const MWWorld::ConstPtr& ptr) const + bool Weapon::hasItemHealth(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); int type = ref->mBase->mData.mType; return MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::HasHealth; } - int Weapon::getItemMaxHealth (const MWWorld::ConstPtr& ptr) const + int Weapon::getItemMaxHealth(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mHealth; } - std::string Weapon::getScript (const MWWorld::ConstPtr& ptr) const + ESM::RefId Weapon::getScript(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = - ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } - std::pair, bool> Weapon::getEquipmentSlots (const MWWorld::ConstPtr& ptr) const + std::pair, bool> Weapon::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); ESM::WeaponType::Class weapClass = MWMechanics::getWeaponType(ref->mBase->mData.mType)->mWeaponClass; std::vector slots_; @@ -97,86 +91,78 @@ namespace MWClass if (weapClass == ESM::WeaponType::Ammo) { - slots_.push_back (int (MWWorld::InventoryStore::Slot_Ammunition)); + slots_.push_back(int(MWWorld::InventoryStore::Slot_Ammunition)); stack = true; } else if (weapClass == ESM::WeaponType::Thrown) { - slots_.push_back (int (MWWorld::InventoryStore::Slot_CarriedRight)); + slots_.push_back(int(MWWorld::InventoryStore::Slot_CarriedRight)); stack = true; } else - slots_.push_back (int (MWWorld::InventoryStore::Slot_CarriedRight)); + slots_.push_back(int(MWWorld::InventoryStore::Slot_CarriedRight)); - return std::make_pair (slots_, stack); + return std::make_pair(slots_, stack); } - int Weapon::getEquipmentSkill (const MWWorld::ConstPtr& ptr) const + ESM::RefId Weapon::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); int type = ref->mBase->mData.mType; return MWMechanics::getWeaponType(type)->mSkill; } - int Weapon::getValue (const MWWorld::ConstPtr& ptr) const + int Weapon::getValue(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } - void Weapon::registerSelf() - { - std::shared_ptr instance (new Weapon); - - registerClass (typeid (ESM::Weapon).name(), instance); - } - - std::string Weapon::getUpSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Weapon::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); int type = ref->mBase->mData.mType; - std::string soundId = MWMechanics::getWeaponType(type)->mSoundId; - return soundId + " Up"; + return MWMechanics::getWeaponType(type)->mSoundIdUp; } - std::string Weapon::getDownSoundId (const MWWorld::ConstPtr& ptr) const + const ESM::RefId& Weapon::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); int type = ref->mBase->mData.mType; - std::string soundId = MWMechanics::getWeaponType(type)->mSoundId; - return soundId + " Down"; + return MWMechanics::getWeaponType(type)->mSoundIdDown; } - std::string Weapon::getInventoryIcon (const MWWorld::ConstPtr& ptr) const + const std::string& Weapon::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } - MWGui::ToolTipInfo Weapon::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Weapon::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); const ESM::WeaponType* weaponType = MWMechanics::getWeaponType(ref->mBase->mData.mType); MWGui::ToolTipInfo info; - info.caption = MyGUI::TextIterator::toTagsString(getName(ptr)) + MWGui::ToolTips::getCountString(count); + std::string_view name = getName(ptr); + info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); info.icon = ref->mBase->mIcon; - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); std::string text; // weapon type & damage - if (weaponType->mWeaponClass != ESM::WeaponType::Ammo || Settings::Manager::getBool("show projectile damage", "Game")) + if (weaponType->mWeaponClass != ESM::WeaponType::Ammo || Settings::game().mShowProjectileDamage) { text += "\n#{sType} "; - int skill = MWMechanics::getWeaponType(ref->mBase->mData.mType)->mSkill; - const std::string type = ESM::Skill::sSkillNameIds[skill]; - std::string oneOrTwoHanded; + const ESM::Skill* skill + = store.get().find(MWMechanics::getWeaponType(ref->mBase->mData.mType)->mSkill); + std::string_view oneOrTwoHanded; if (weaponType->mWeaponClass == ESM::WeaponType::Melee) { if (weaponType->mFlags & ESM::WeaponType::TwoHanded) @@ -185,38 +171,34 @@ namespace MWClass oneOrTwoHanded = "sOneHanded"; } - text += store.get().find(type)->mValue.getString() + - ((oneOrTwoHanded != "") ? ", " + store.get().find(oneOrTwoHanded)->mValue.getString() : ""); + text += skill->mName; + if (!oneOrTwoHanded.empty()) + text += ", " + store.get().find(oneOrTwoHanded)->mValue.getString(); // weapon damage if (weaponType->mWeaponClass == ESM::WeaponType::Thrown) { // Thrown weapons have 2x real damage applied // as they're both the weapon and the ammo - text += "\n#{sAttack}: " - + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[0] * 2)) + text += "\n#{sAttack}: " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[0] * 2)) + " - " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[1] * 2)); } else if (weaponType->mWeaponClass == ESM::WeaponType::Melee) { // Chop - text += "\n#{sChop}: " - + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[0])) - + " - " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[1])); + text += "\n#{sChop}: " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[0])) + " - " + + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[1])); // Slash - text += "\n#{sSlash}: " - + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mSlash[0])) + text += "\n#{sSlash}: " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mSlash[0])) + " - " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mSlash[1])); // Thrust - text += "\n#{sThrust}: " - + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mThrust[0])) + text += "\n#{sThrust}: " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mThrust[0])) + " - " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mThrust[1])); } else { // marksman - text += "\n#{sAttack}: " - + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[0])) + text += "\n#{sAttack}: " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[0])) + " - " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[1])); } } @@ -225,15 +207,16 @@ namespace MWClass { int remainingHealth = getItemHealth(ptr); text += "\n#{sCondition}: " + MWGui::ToolTips::toString(remainingHealth) + "/" - + MWGui::ToolTips::toString(ref->mBase->mData.mHealth); + + MWGui::ToolTips::toString(ref->mBase->mData.mHealth); } - const bool verbose = Settings::Manager::getBool("show melee info", "Game"); + const bool verbose = Settings::game().mShowMeleeInfo; // add reach for melee weapon if (weaponType->mWeaponClass == ESM::WeaponType::Melee && verbose) { // display value in feet - const float combatDistance = store.get().find("fCombatDistance")->mValue.getFloat() * ref->mBase->mData.mReach; + const float combatDistance + = store.get().find("fCombatDistance")->mValue.getFloat() * ref->mBase->mData.mReach; text += MWGui::ToolTips::getWeightString(combatDistance / Constants::UnitsPerFoot, "#{sRange}"); text += " #{sFeet}"; } @@ -252,93 +235,95 @@ namespace MWClass if (!info.enchant.empty()) info.remainingEnchantCharge = static_cast(ptr.getCellRef().getEnchantmentCharge()); - if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); - text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); + if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); + info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } - info.text = text; + info.text = std::move(text); return info; } - std::string Weapon::getEnchantment (const MWWorld::ConstPtr& ptr) const + ESM::RefId Weapon::getEnchantment(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mEnchant; } - std::string Weapon::applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const + const ESM::RefId& Weapon::applyEnchantment( + const MWWorld::ConstPtr& ptr, const ESM::RefId& enchId, int enchCharge, const std::string& newName) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); ESM::Weapon newItem = *ref->mBase; - newItem.mId=""; - newItem.mName=newName; - newItem.mData.mEnchant=enchCharge; - newItem.mEnchant=enchId; + newItem.mId = ESM::RefId(); + newItem.mName = newName; + newItem.mData.mEnchant = enchCharge; + newItem.mEnchant = enchId; newItem.mData.mFlags |= ESM::Weapon::Magical; - const ESM::Weapon *record = MWBase::Environment::get().getWorld()->createRecord (newItem); + const ESM::Weapon* record = MWBase::Environment::get().getESMStore()->insert(newItem); return record->mId; } - std::pair Weapon::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const + std::pair Weapon::canBeEquipped(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { if (hasItemHealth(ptr) && getItemHealth(ptr) == 0) - return std::make_pair(0, "#{sInventoryMessage1}"); + return { 0, "#{sInventoryMessage1}" }; // Do not allow equip weapons from inventory during attack - if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) - && MWBase::Environment::get().getWindowManager()->isGuiMode()) - return std::make_pair(0, "#{sCantEquipWeapWarning}"); + if (npc.isInCell() && MWBase::Environment::get().getWindowManager()->isGuiMode() + && MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc)) + return { 0, "#{sCantEquipWeapWarning}" }; std::pair, bool> slots_ = getEquipmentSlots(ptr); if (slots_.first.empty()) - return std::make_pair (0, ""); + return { 0, {} }; int type = ptr.get()->mBase->mData.mType; - if(MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::TwoHanded) + if (MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::TwoHanded) { - return std::make_pair (2, ""); + return { 2, {} }; } - return std::make_pair(1, ""); + return { 1, {} }; } - std::shared_ptr Weapon::use (const MWWorld::Ptr& ptr, bool force) const + std::unique_ptr Weapon::use(const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); + std::unique_ptr action = std::make_unique(ptr, force); action->setSound(getUpSoundId(ptr)); return action; } - MWWorld::Ptr Weapon::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + MWWorld::Ptr Weapon::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } - int Weapon::getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const + int Weapon::getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mEnchant; } - bool Weapon::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Weapon::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Weapon) - || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } - float Weapon::getWeight(const MWWorld::ConstPtr &ptr) const + float Weapon::getWeight(const MWWorld::ConstPtr& ptr) const { - const MWWorld::LiveCellRef *ref = ptr.get(); + const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } diff --git a/apps/openmw/mwclass/weapon.hpp b/apps/openmw/mwclass/weapon.hpp index f1824b7d14b..9e79532bc0f 100644 --- a/apps/openmw/mwclass/weapon.hpp +++ b/apps/openmw/mwclass/weapon.hpp @@ -1,83 +1,84 @@ #ifndef GAME_MWCLASS_WEAPON_H #define GAME_MWCLASS_WEAPON_H -#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" namespace MWClass { - class Weapon : public MWWorld::Class + class Weapon : public MWWorld::RegisteredClass { - MWWorld::Ptr - copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + friend MWWorld::RegisteredClass; - public: + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; - void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; - ///< Add reference into a cell for rendering + public: + Weapon(); - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override; + ///< Add reference into a cell for rendering - std::string getName (const MWWorld::ConstPtr& ptr) const override; - ///< \return name or ID; can return an empty string. + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + ///< \return name or ID; can return an empty string. - std::shared_ptr activate (const MWWorld::Ptr& ptr, - const MWWorld::Ptr& actor) const override; - ///< Generate action for activation + bool isItem(const MWWorld::ConstPtr&) const override { return true; } - MWGui::ToolTipInfo getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const override; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; + ///< Generate action for activation - bool hasItemHealth (const MWWorld::ConstPtr& ptr) const override; - ///< \return Item health data available? + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - int getItemMaxHealth (const MWWorld::ConstPtr& ptr) const override; - ///< Return item max health or throw an exception, if class does not have item health + bool hasItemHealth(const MWWorld::ConstPtr& ptr) const override; + ///< \return Item health data available? - std::string getScript (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of the script attached to ptr + int getItemMaxHealth(const MWWorld::ConstPtr& ptr) const override; + ///< Return item max health or throw an exception, if class does not have item health - std::pair, bool> getEquipmentSlots (const MWWorld::ConstPtr& ptr) const override; - ///< \return first: Return IDs of the slot this object can be equipped in; second: can object - /// stay stacked when equipped? + ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of the script attached to ptr - int getEquipmentSkill (const MWWorld::ConstPtr& ptr) const override; - /// Return the index of the skill this item corresponds to when equipped or -1, if there is - /// no such skill. + std::pair, bool> getEquipmentSlots(const MWWorld::ConstPtr& ptr) const override; + ///< \return first: Return IDs of the slot this object can be equipped in; second: can object + /// stay stacked when equipped? - int getValue (const MWWorld::ConstPtr& ptr) const override; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. + ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; - static void registerSelf(); + int getValue(const MWWorld::ConstPtr& ptr) const override; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. - std::string getUpSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the pick up sound Id + const ESM::RefId& getUpSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the pick up sound Id - std::string getDownSoundId (const MWWorld::ConstPtr& ptr) const override; - ///< Return the put down sound Id + const ESM::RefId& getDownSoundId(const MWWorld::ConstPtr& ptr) const override; + ///< Return the put down sound Id - std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const override; - ///< Return name of inventory icon. + const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; + ///< Return name of inventory icon. - std::string getEnchantment (const MWWorld::ConstPtr& ptr) const override; - ///< @return the enchantment ID if the object is enchanted, otherwise an empty string + ESM::RefId getEnchantment(const MWWorld::ConstPtr& ptr) const override; + ///< @return the enchantment ID if the object is enchanted, otherwise an empty string - std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const override; - ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. + const ESM::RefId& applyEnchantment(const MWWorld::ConstPtr& ptr, const ESM::RefId& enchId, int enchCharge, + const std::string& newName) const override; + ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. - std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const override; - ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. - /// Second item in the pair specifies the error message + std::pair canBeEquipped( + const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const override; + ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon + ///< conflicts with that. + /// Second item in the pair specifies the error message - std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const override; - ///< Generate action for using via inventory menu + std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; + ///< Generate action for using via inventory menu - std::string getModel(const MWWorld::ConstPtr &ptr) const override; + std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; - bool canSell (const MWWorld::ConstPtr& item, int npcServices) const override; + bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; - float getWeight (const MWWorld::ConstPtr& ptr) const override; + float getWeight(const MWWorld::ConstPtr& ptr) const override; - int getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const override; + int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const override; }; } diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index 3c6349ad859..9f3bb0b26f8 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -2,81 +2,90 @@ #include #include +#include +#include #include -#include -#include -#include -#include +#include +#include +#include +#include +#include -#include #include -#include +#include #include #include +#include #include -#include #include +#include + +#include -#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/journal.hpp" -#include "../mwbase/scriptmanager.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwscript/compilercontext.hpp" -#include "../mwscript/interpretercontext.hpp" #include "../mwscript/extensions.hpp" +#include "../mwscript/interpretercontext.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" -#include "../mwmechanics/actorutil.hpp" #include "filter.hpp" #include "hypertextparser.hpp" namespace MWDialogue { - DialogueManager::DialogueManager (const Compiler::Extensions& extensions, Translation::Storage& translationDataStorage) : - mTranslationDataStorage(translationDataStorage) - , mCompilerContext (MWScript::CompilerContext::Type_Dialogue) - , mErrorHandler() - , mTalkedTo(false) - , mTemporaryDispositionChange(0.f) - , mPermanentDispositionChange(0.f) + DialogueManager::DialogueManager( + const Compiler::Extensions& extensions, Translation::Storage& translationDataStorage) + : mTranslationDataStorage(translationDataStorage) + , mCompilerContext(MWScript::CompilerContext::Type_Dialogue) + , mErrorHandler() + , mTalkedTo(false) + , mOriginalDisposition(0) + , mCurrentDisposition(0) + , mPermanentDispositionChange(0) { mChoice = -1; mIsInChoice = false; mGoodbye = false; - mCompilerContext.setExtensions (&extensions); + mCompilerContext.setExtensions(&extensions); } void DialogueManager::clear() { mKnownTopics.clear(); mTalkedTo = false; - mTemporaryDispositionChange = 0; + mOriginalDisposition = 0; + mCurrentDisposition = 0; mPermanentDispositionChange = 0; } - void DialogueManager::addTopic (const std::string& topic) + void DialogueManager::addTopic(const ESM::RefId& topic) { - mKnownTopics.insert( Misc::StringUtils::lowerCase(topic) ); + mKnownTopics.insert(topic); } - void DialogueManager::parseText (const std::string& text) + std::vector DialogueManager::parseTopicIdsFromText(const std::string& text) { - updateActorKnownTopics(); + std::vector topicIdList; + std::vector hypertext = HyperTextParser::parseHyperText(text); for (std::vector::iterator tok = hypertext.begin(); tok != hypertext.end(); ++tok) @@ -87,18 +96,44 @@ namespace MWDialogue { // calculation of standard form for all hyperlinks size_t asterisk_count = HyperTextParser::removePseudoAsterisks(topicId); - for(; asterisk_count > 0; --asterisk_count) + for (; asterisk_count > 0; --asterisk_count) topicId.append("*"); topicId = mTranslationDataStorage.topicStandardForm(topicId); } - if (mActorKnownTopics.count( topicId )) - mKnownTopics.insert( topicId ); + topicIdList.push_back(ESM::RefId::stringRefId(topicId)); + } + + return topicIdList; + } + + void DialogueManager::addTopicsFromText(const std::string& text) + { + updateActorKnownTopics(); + + for (const auto& topicId : parseTopicIdsFromText(text)) + { + if (mActorKnownTopics.count(topicId)) + mKnownTopics.insert(topicId); } } - bool DialogueManager::startDialogue (const MWWorld::Ptr& actor, ResponseCallback* callback) + void DialogueManager::updateOriginalDisposition() + { + if (mActor.getClass().isNpc()) + { + const auto& stats = mActor.getClass().getNpcStats(mActor); + // Disposition changed by script; discard our preconceived notions + if (stats.getBaseDisposition() != mCurrentDisposition) + { + mCurrentDisposition = stats.getBaseDisposition(); + mOriginalDisposition = mCurrentDisposition; + } + } + } + + bool DialogueManager::startDialogue(const MWWorld::Ptr& actor, ResponseCallback* callback) { updateGlobals(); @@ -106,9 +141,8 @@ namespace MWDialogue if (actor.getClass().getCreatureStats(actor).isDead()) return false; - mLastTopic = ""; - mPermanentDispositionChange = 0; - mTemporaryDispositionChange = 0; + mLastTopic = ESM::RefId(); + // Note that we intentionally don't reset mPermanentDispositionChange mChoice = -1; mIsInChoice = false; @@ -117,24 +151,22 @@ namespace MWDialogue mActor = actor; - MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats (actor); + MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); mTalkedTo = creatureStats.hasTalkedToPlayer(); mActorKnownTopics.clear(); - mActorKnownTopicsFlag.clear(); - //greeting - const MWWorld::Store &dialogs = - MWBase::Environment::get().getWorld()->getStore().get(); + // greeting + const MWWorld::Store& dialogs = MWBase::Environment::get().getESMStore()->get(); - Filter filter (actor, mChoice, mTalkedTo); + Filter filter(actor, mChoice, mTalkedTo); - for (MWWorld::Store::iterator it = dialogs.begin(); it != dialogs.end(); ++it) + for (const ESM::Dialogue& dialogue : dialogs) { - if(it->mType == ESM::Dialogue::Greeting) + if (dialogue.mType == ESM::Dialogue::Greeting) { // Search a response (we do not accept a fallback to "Info refusal" here) - if (const ESM::DialInfo *info = filter.search (*it, false)) + if (const ESM::DialInfo* info = filter.search(dialogue, false).second) { creatureStats.talkedToPlayer(); @@ -143,12 +175,12 @@ namespace MWDialogue // TODO play sound } - MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor); - callback->addResponse("", Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); - executeScript (info->mResultScript, mActor); - mLastTopic = it->mId; + MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(), mActor); + callback->addResponse({}, Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); + executeScript(info->mResultScript, mActor); + mLastTopic = dialogue.mId; - parseText (info->mResponse); + addTopicsFromText(info->mResponse); return true; } @@ -157,9 +189,10 @@ namespace MWDialogue return false; } - bool DialogueManager::compile (const std::string& cmd, std::vector& code, const MWWorld::Ptr& actor) + std::optional DialogueManager::compile(const std::string& cmd, const MWWorld::Ptr& actor) { bool success = true; + std::optional program; try { @@ -167,29 +200,29 @@ namespace MWDialogue mErrorHandler.setContext("[dialogue script]"); - std::istringstream input (cmd + "\n"); + std::istringstream input(cmd + "\n"); - Compiler::Scanner scanner (mErrorHandler, input, mCompilerContext.getExtensions()); + Compiler::Scanner scanner(mErrorHandler, input, mCompilerContext.getExtensions()); Compiler::Locals locals; - std::string actorScript = actor.getClass().getScript (actor); + const ESM::RefId& actorScript = actor.getClass().getScript(actor); if (!actorScript.empty()) { // grab local variables from actor's script, if available. - locals = MWBase::Environment::get().getScriptManager()->getLocals (actorScript); + locals = MWBase::Environment::get().getScriptManager()->getLocals(actorScript); } - Compiler::ScriptParser parser(mErrorHandler,mCompilerContext, locals, false); + Compiler::ScriptParser parser(mErrorHandler, mCompilerContext, locals, false); - scanner.scan (parser); + scanner.scan(parser); if (!mErrorHandler.isGood()) success = false; if (success) - parser.getCode (code); + program = parser.getProgram(); } catch (const Compiler::SourceException& /* error */) { @@ -198,7 +231,7 @@ namespace MWDialogue } catch (const std::exception& error) { - Log(Debug::Error) << std::string ("Dialogue error: An exception has been thrown: ") + error.what(); + Log(Debug::Error) << std::string("Dialogue error: An exception has been thrown: ") + error.what(); success = false; } @@ -207,32 +240,31 @@ namespace MWDialogue Log(Debug::Error) << "Error: compiling failed (dialogue script): \n" << cmd << "\n"; } - return success; + return program; } - void DialogueManager::executeScript (const std::string& script, const MWWorld::Ptr& actor) + void DialogueManager::executeScript(const std::string& script, const MWWorld::Ptr& actor) { - std::vector code; - if(compile(script, code, actor)) + if (const std::optional program = compile(script, actor)) { try { MWScript::InterpreterContext interpreterContext(&actor.getRefData().getLocals(), actor); Interpreter::Interpreter interpreter; - MWScript::installOpcodes (interpreter); - interpreter.run (&code[0], code.size(), interpreterContext); + MWScript::installOpcodes(interpreter); + interpreter.run(*program, interpreterContext); } catch (const std::exception& error) { - Log(Debug::Error) << std::string ("Dialogue error: An exception has been thrown: ") + error.what(); + Log(Debug::Error) << std::string("Dialogue error: An exception has been thrown: ") + error.what(); } } } - bool DialogueManager::inJournal (const std::string& topicId, const std::string& infoId) + bool DialogueManager::inJournal(const ESM::RefId& topicId, const ESM::RefId& infoId) const { - const MWDialogue::Topic *topicHistory = nullptr; - MWBase::Journal *journal = MWBase::Environment::get().getJournal(); + const MWDialogue::Topic* topicHistory = nullptr; + MWBase::Journal* journal = MWBase::Environment::get().getJournal(); for (auto it = journal->topicBegin(); it != journal->topicEnd(); ++it) { if (it->first == topicId) @@ -245,7 +277,7 @@ namespace MWDialogue if (!topicHistory) return false; - for(const auto& topic : *topicHistory) + for (const auto& topic : *topicHistory) { if (topic.mInfoId == infoId) return true; @@ -253,65 +285,58 @@ namespace MWDialogue return false; } - void DialogueManager::executeTopic (const std::string& topic, ResponseCallback* callback) + void DialogueManager::executeTopic(const ESM::RefId& topic, ResponseCallback* callback) { - Filter filter (mActor, mChoice, mTalkedTo); + Filter filter(mActor, mChoice, mTalkedTo); - const MWWorld::Store &dialogues = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& dialogues = MWBase::Environment::get().getESMStore()->get(); - const ESM::Dialogue& dialogue = *dialogues.find (topic); + const ESM::Dialogue& dialogue = *dialogues.find(topic); + + const auto [responseTopic, info] = filter.search(dialogue, true); - const ESM::DialInfo* info = filter.search(dialogue, true); if (info) { - std::string title; - if (dialogue.mType==ESM::Dialogue::Persuasion) + std::string_view title; + if (dialogue.mType == ESM::Dialogue::Persuasion) { // Determine GMST from dialogue topic. GMSTs are: // sAdmireSuccess, sAdmireFail, sIntimidateSuccess, sIntimidateFail, // sTauntSuccess, sTauntFail, sBribeSuccess, sBribeFail - std::string modifiedTopic = "s" + topic; + std::string modifiedTopic = "s" + topic.getRefIdString(); - modifiedTopic.erase (std::remove (modifiedTopic.begin(), modifiedTopic.end(), ' '), modifiedTopic.end()); + modifiedTopic.erase(std::remove(modifiedTopic.begin(), modifiedTopic.end(), ' '), modifiedTopic.end()); - const MWWorld::Store& gmsts = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmsts + = MWBase::Environment::get().getESMStore()->get(); - title = gmsts.find (modifiedTopic)->mValue.getString(); + title = gmsts.find(modifiedTopic)->mValue.getString(); } else - title = topic; + title = dialogue.mStringId; - MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor); + MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(), mActor); callback->addResponse(title, Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); if (dialogue.mType == ESM::Dialogue::Topic) { - // Make sure the returned DialInfo is from the Dialogue we supplied. If could also be from the Info refusal group, - // in which case it should not be added to the journal. - for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue.mInfo.begin(); - iter!=dialogue.mInfo.end(); ++iter) - { - if (iter->mId == info->mId) - { - MWBase::Environment::get().getJournal()->addTopic (Misc::StringUtils::lowerCase(topic), info->mId, mActor); - break; - } - } + // Make sure the returned DialInfo is from the Dialogue we supplied. If could also be from the Info + // refusal group, in which case it should not be added to the journal. + if (responseTopic == &dialogue) + MWBase::Environment::get().getJournal()->addTopic(topic, info->mId, mActor); } mLastTopic = topic; - executeScript (info->mResultScript, mActor); + executeScript(info->mResultScript, mActor); - parseText (info->mResponse); + addTopicsFromText(info->mResponse); } } - const ESM::Dialogue *DialogueManager::searchDialogue(const std::string& id) + const ESM::Dialogue* DialogueManager::searchDialogue(const ESM::RefId& id) { - return MWBase::Environment::get().getWorld()->getStore().get().search(id); + return MWBase::Environment::get().getESMStore()->get().search(id); } void DialogueManager::updateGlobals() @@ -324,34 +349,51 @@ namespace MWDialogue updateGlobals(); mActorKnownTopics.clear(); - mActorKnownTopicsFlag.clear(); - const auto& dialogs = MWBase::Environment::get().getWorld()->getStore().get(); + const auto& dialogs = MWBase::Environment::get().getESMStore()->get(); - Filter filter (mActor, -1, mTalkedTo); + Filter filter(mActor, -1, mTalkedTo); for (const auto& dialog : dialogs) { if (dialog.mType == ESM::Dialogue::Topic) { - const auto* answer = filter.search(dialog, true); - auto topicId = Misc::StringUtils::lowerCase(dialog.mId); + const auto* answer = filter.search(dialog, true).second; + const auto& topicId = dialog.mId; if (answer != nullptr) { - int flag = 0; - if(!inJournal(topicId, answer->mId)) + int topicFlags = 0; + if (!inJournal(topicId, answer->mId)) { // Does this dialogue contains some actor-specific answer? - if (Misc::StringUtils::ciEqual(answer->mActor, mActor.getCellRef().getRefId())) - flag |= MWBase::DialogueManager::TopicType::Specific; + if (answer->mActor == mActor.getCellRef().getRefId()) + topicFlags |= MWBase::DialogueManager::TopicType::Specific; } else - flag |= MWBase::DialogueManager::TopicType::Exhausted; - mActorKnownTopics.insert (dialog.mId); - mActorKnownTopicsFlag[dialog.mId] = flag; + topicFlags |= MWBase::DialogueManager::TopicType::Exhausted; + mActorKnownTopics.insert(std::make_pair(dialog.mId, ActorKnownTopicInfo{ topicFlags, answer })); } + } + } + // If response to a topic leads to a new topic, the original topic is not exhausted. + + for (auto& [dialogId, topicInfo] : mActorKnownTopics) + { + // If the topic is not marked as exhausted, we don't need to do anything about it. + // If the topic will not be shown to the player, the flag actually does not matter. + + if (!(topicInfo.mFlags & MWBase::DialogueManager::TopicType::Exhausted) || !mKnownTopics.count(dialogId)) + continue; + + for (const auto& topicId : parseTopicIdsFromText(topicInfo.mInfo->mResponse)) + { + if (mActorKnownTopics.count(topicId) && !mKnownTopics.count(topicId)) + { + topicInfo.mFlags &= ~MWBase::DialogueManager::TopicType::Exhausted; + break; + } } } } @@ -361,12 +403,12 @@ namespace MWDialogue updateActorKnownTopics(); std::list keywordList; - - for (const std::string& topic : mActorKnownTopics) + const auto& store = MWBase::Environment::get().getESMStore()->get(); + for (const auto& [topic, topicInfo] : mActorKnownTopics) { - //does the player know the topic? - if (mKnownTopics.count(topic)) - keywordList.push_back(topic); + // does the player know the topic? + if (mKnownTopics.contains(topic)) + keywordList.push_back(store.find(topic)->mStringId); } // sort again, because the previous sort was case-sensitive @@ -374,19 +416,22 @@ namespace MWDialogue return keywordList; } - int DialogueManager::getTopicFlag(const std::string& topicId) + int DialogueManager::getTopicFlag(const ESM::RefId& topicId) const { - return mActorKnownTopicsFlag[topicId]; + auto known = mActorKnownTopics.find(topicId); + if (known != mActorKnownTopics.end()) + return known->second.mFlags; + return 0; } - void DialogueManager::keywordSelected (const std::string& keyword, ResponseCallback* callback) + void DialogueManager::keywordSelected(std::string_view keyword, ResponseCallback* callback) { - if(!mIsInChoice) + if (!mIsInChoice) { - const ESM::Dialogue* dialogue = searchDialogue(keyword); + const ESM::Dialogue* dialogue = searchDialogue(ESM::RefId::stringRefId(keyword)); if (dialogue && dialogue->mType == ESM::Dialogue::Topic) { - executeTopic (keyword, callback); + executeTopic(dialogue->mId, callback); } } } @@ -398,60 +443,57 @@ namespace MWDialogue void DialogueManager::goodbyeSelected() { - // Apply disposition change to NPC's base disposition - if (mActor.getClass().isNpc()) + // Apply disposition change to NPC's base disposition if we **think** we need to change something + if ((mPermanentDispositionChange || mOriginalDisposition != mCurrentDisposition) && mActor.getClass().isNpc()) { - // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate) - float curDisp = static_cast(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false)); - if (curDisp + mPermanentDispositionChange < 0) - mPermanentDispositionChange = -curDisp; - + updateOriginalDisposition(); MWMechanics::NpcStats& npcStats = mActor.getClass().getNpcStats(mActor); - npcStats.setBaseDisposition(static_cast(npcStats.getBaseDisposition() + mPermanentDispositionChange)); + // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with + // intimidate) + npcStats.setBaseDisposition(0); + int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false); + int disposition = std::clamp(mOriginalDisposition + mPermanentDispositionChange, -zero, 100 - zero); + + npcStats.setBaseDisposition(disposition); } mPermanentDispositionChange = 0; - mTemporaryDispositionChange = 0; + mOriginalDisposition = 0; + mCurrentDisposition = 0; } - void DialogueManager::questionAnswered (int answer, ResponseCallback* callback) + void DialogueManager::questionAnswered(int answer, ResponseCallback* callback) { mChoice = answer; const ESM::Dialogue* dialogue = searchDialogue(mLastTopic); if (dialogue) { - Filter filter (mActor, mChoice, mTalkedTo); + Filter filter(mActor, mChoice, mTalkedTo); if (dialogue->mType == ESM::Dialogue::Topic || dialogue->mType == ESM::Dialogue::Greeting) { - if (const ESM::DialInfo *info = filter.search (*dialogue, true)) + const auto [responseTopic, info] = filter.search(*dialogue, true); + if (info) { - std::string text = info->mResponse; - parseText (text); + const std::string& text = info->mResponse; + addTopicsFromText(text); mChoice = -1; mIsInChoice = false; mChoices.clear(); - MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor); - callback->addResponse("", Interpreter::fixDefinesDialog(text, interpreterContext)); + MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(), mActor); + callback->addResponse({}, Interpreter::fixDefinesDialog(text, interpreterContext)); if (dialogue->mType == ESM::Dialogue::Topic) { - // Make sure the returned DialInfo is from the Dialogue we supplied. If could also be from the Info refusal group, - // in which case it should not be added to the journal - for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue->mInfo.begin(); - iter!=dialogue->mInfo.end(); ++iter) - { - if (iter->mId == info->mId) - { - MWBase::Environment::get().getJournal()->addTopic (Misc::StringUtils::lowerCase(mLastTopic), info->mId, mActor); - break; - } - } + // Make sure the returned DialInfo is from the Dialogue we supplied. If could also be from the + // Info refusal group, in which case it should not be added to the journal + if (responseTopic == dialogue) + MWBase::Environment::get().getJournal()->addTopic(mLastTopic, info->mId, mActor); } - executeScript (info->mResultScript, mActor); + executeScript(info->mResultScript, mActor); } else { @@ -465,18 +507,18 @@ namespace MWDialogue updateActorKnownTopics(); } - void DialogueManager::addChoice (const std::string& text, int choice) + void DialogueManager::addChoice(std::string_view text, int choice) { mIsInChoice = true; mChoices.emplace_back(text, choice); } - const std::vector >& DialogueManager::getChoices() + const std::vector>& DialogueManager::getChoices() const { return mChoices; } - bool DialogueManager::isGoodbye() + bool DialogueManager::isGoodbye() const { return mGoodbye; } @@ -490,26 +532,23 @@ namespace MWDialogue void DialogueManager::persuade(int type, ResponseCallback* callback) { bool success; - float temp, perm; + int temp, perm; MWBase::Environment::get().getMechanicsManager()->getPersuasionDispositionChange( - mActor, MWBase::MechanicsManager::PersuasionType(type), - success, temp, perm); - mTemporaryDispositionChange += temp; + mActor, MWBase::MechanicsManager::PersuasionType(type), success, temp, perm); + updateOriginalDisposition(); + if (temp > 0 && perm > 0 && mOriginalDisposition + perm + mPermanentDispositionChange < 0) + perm = -(mOriginalDisposition + mPermanentDispositionChange); + mCurrentDisposition += temp; + mActor.getClass().getNpcStats(mActor).setBaseDisposition(mCurrentDisposition); mPermanentDispositionChange += perm; - // change temp disposition so that final disposition is between 0...100 - float curDisp = static_cast(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false)); - if (curDisp + mTemporaryDispositionChange < 0) - mTemporaryDispositionChange = -curDisp; - else if (curDisp + mTemporaryDispositionChange > 100) - mTemporaryDispositionChange = 100 - curDisp; - MWWorld::Ptr player = MWMechanics::getPlayer(); - player.getClass().skillUsageSucceeded(player, ESM::Skill::Speechcraft, success ? 0 : 1); + player.getClass().skillUsageSucceeded( + player, ESM::Skill::Speechcraft, success ? ESM::Skill::Speechcraft_Success : ESM::Skill::Speechcraft_Fail); if (success) { - int gold=0; + int gold = 0; if (type == MWBase::MechanicsManager::PT_Bribe10) gold = 10; else if (type == MWBase::MechanicsManager::PT_Bribe100) @@ -519,8 +558,8 @@ namespace MWDialogue if (gold) { - player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, gold, player); - mActor.getClass().getContainerStore(mActor).add(MWWorld::ContainerStore::sGoldId, gold, mActor); + player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, gold); + mActor.getClass().getContainerStore(mActor).add(MWWorld::ContainerStore::sGoldId, gold); } } @@ -532,91 +571,93 @@ namespace MWDialogue text = "Taunt"; else if (type == MWBase::MechanicsManager::PT_Intimidate) text = "Intimidate"; - else{ + else + { text = "Bribe"; } - executeTopic (text + (success ? " Success" : " Fail"), callback); - } - - int DialogueManager::getTemporaryDispositionChange() const - { - return static_cast(mTemporaryDispositionChange); + executeTopic(ESM::RefId::stringRefId(text + (success ? " Success" : " Fail")), callback); } void DialogueManager::applyBarterDispositionChange(int delta) { - mTemporaryDispositionChange += delta; - if (Settings::Manager::getBool("barter disposition change is permanent", "Game")) - mPermanentDispositionChange += delta; + if (!mActor.isEmpty() && mActor.getClass().isNpc()) + { + updateOriginalDisposition(); + mCurrentDisposition += delta; + mActor.getClass().getNpcStats(mActor).setBaseDisposition(mCurrentDisposition); + if (Settings::game().mBarterDispositionChangeIsPermanent) + mPermanentDispositionChange += delta; + } } bool DialogueManager::checkServiceRefused(ResponseCallback* callback, ServiceType service) { - Filter filter (mActor, service, mTalkedTo); + Filter filter(mActor, service, mTalkedTo); - const MWWorld::Store &dialogues = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& dialogues = MWBase::Environment::get().getESMStore()->get(); - const ESM::Dialogue& dialogue = *dialogues.find ("Service Refusal"); + const ESM::Dialogue& dialogue = *dialogues.find(ESM::RefId::stringRefId("Service Refusal")); - std::vector infos = filter.list (dialogue, false, false, true); + std::vector infos = filter.list(dialogue, false, false, true); if (!infos.empty()) { - const ESM::DialInfo* info = infos[0]; + const ESM::DialInfo* info = infos[0].second; - parseText (info->mResponse); + addTopicsFromText(info->mResponse); - const MWWorld::Store& gmsts = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmsts + = MWBase::Environment::get().getESMStore()->get(); - MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(),mActor); + MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(), mActor); - callback->addResponse(gmsts.find ("sServiceRefusal")->mValue.getString(), Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); + callback->addResponse(gmsts.find("sServiceRefusal")->mValue.getString(), + Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); - executeScript (info->mResultScript, mActor); + executeScript(info->mResultScript, mActor); return true; } return false; } - void DialogueManager::say(const MWWorld::Ptr &actor, const std::string &topic) + bool DialogueManager::say(const MWWorld::Ptr& actor, const ESM::RefId& topic) { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(sndMgr->sayActive(actor)) + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + if (sndMgr->sayActive(actor)) { // Actor is already saying something. - return; + return false; } if (actor.getClass().isNpc() && MWBase::Environment::get().getWorld()->isSwimming(actor)) { // NPCs don't talk while submerged - return; + return false; } if (actor.getClass().getCreatureStats(actor).getKnockedDown()) { // Unconscious actors can not speak - return; + return false; } - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Dialogue *dial = store.get().find(topic); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const ESM::Dialogue* dial = store.get().find(topic); const MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); Filter filter(actor, 0, creatureStats.hasTalkedToPlayer()); - const ESM::DialInfo *info = filter.search(*dial, false); - if(info != nullptr) + const ESM::DialInfo* info = filter.search(*dial, false).second; + if (info != nullptr) { - MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); - if(winMgr->getSubtitlesEnabled()) + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + if (Settings::gui().mSubtitles) winMgr->messageBox(info->mResponse); if (!info->mSound.empty()) - sndMgr->say(actor, info->mSound); + sndMgr->say(actor, Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(info->mSound))); if (!info->mResultScript.empty()) executeScript(info->mResultScript, actor); } + return info != nullptr; } int DialogueManager::countSavedGameRecords() const @@ -624,95 +665,97 @@ namespace MWDialogue return 1; // known topics } - void DialogueManager::write (ESM::ESMWriter& writer, Loading::Listener& progress) const + void DialogueManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) const { ESM::DialogueState state; - for (std::set::const_iterator iter (mKnownTopics.begin()); - iter!=mKnownTopics.end(); ++iter) - { - state.mKnownTopics.push_back (*iter); - } + state.mKnownTopics.reserve(mKnownTopics.size()); + std::copy(mKnownTopics.begin(), mKnownTopics.end(), std::back_inserter(state.mKnownTopics)); state.mChangedFactionReaction = mChangedFactionReaction; - writer.startRecord (ESM::REC_DIAS); - state.save (writer); - writer.endRecord (ESM::REC_DIAS); + writer.startRecord(ESM::REC_DIAS); + state.save(writer); + writer.endRecord(ESM::REC_DIAS); } - void DialogueManager::readRecord (ESM::ESMReader& reader, uint32_t type) + void DialogueManager::readRecord(ESM::ESMReader& reader, uint32_t type) { - if (type==ESM::REC_DIAS) + if (type == ESM::REC_DIAS) { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); ESM::DialogueState state; - state.load (reader); + state.load(reader); - for (std::vector::const_iterator iter (state.mKnownTopics.begin()); - iter!=state.mKnownTopics.end(); ++iter) - if (store.get().search (*iter)) - mKnownTopics.insert (*iter); + for (const auto& knownTopic : state.mKnownTopics) + if (store.get().search(knownTopic)) + mKnownTopics.insert(knownTopic); mChangedFactionReaction = state.mChangedFactionReaction; } } - void DialogueManager::modFactionReaction(const std::string &faction1, const std::string &faction2, int diff) + void DialogueManager::modFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int diff) { - std::string fact1 = Misc::StringUtils::lowerCase(faction1); - std::string fact2 = Misc::StringUtils::lowerCase(faction2); - // Make sure the factions exist - MWBase::Environment::get().getWorld()->getStore().get().find(fact1); - MWBase::Environment::get().getWorld()->getStore().get().find(fact2); + MWBase::Environment::get().getESMStore()->get().find(faction1); + MWBase::Environment::get().getESMStore()->get().find(faction2); int newValue = getFactionReaction(faction1, faction2) + diff; - std::map& map = mChangedFactionReaction[fact1]; - map[fact2] = newValue; + auto& map = mChangedFactionReaction[faction1]; + map[faction2] = newValue; } - void DialogueManager::setFactionReaction(const std::string &faction1, const std::string &faction2, int absolute) + void DialogueManager::setFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int absolute) { - std::string fact1 = Misc::StringUtils::lowerCase(faction1); - std::string fact2 = Misc::StringUtils::lowerCase(faction2); - // Make sure the factions exist - MWBase::Environment::get().getWorld()->getStore().get().find(fact1); - MWBase::Environment::get().getWorld()->getStore().get().find(fact2); + MWBase::Environment::get().getESMStore()->get().find(faction1); + MWBase::Environment::get().getESMStore()->get().find(faction2); - std::map& map = mChangedFactionReaction[fact1]; - map[fact2] = absolute; + auto& map = mChangedFactionReaction[faction1]; + map[faction2] = absolute; } - int DialogueManager::getFactionReaction(const std::string &faction1, const std::string &faction2) const + int DialogueManager::getFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2) const { - std::string fact1 = Misc::StringUtils::lowerCase(faction1); - std::string fact2 = Misc::StringUtils::lowerCase(faction2); - - ModFactionReactionMap::const_iterator map = mChangedFactionReaction.find(fact1); - if (map != mChangedFactionReaction.end() && map->second.find(fact2) != map->second.end()) - return map->second.at(fact2); + ModFactionReactionMap::const_iterator map = mChangedFactionReaction.find(faction1); + if (map != mChangedFactionReaction.end()) + { + auto it = map->second.find(faction2); + if (it != map->second.end()) + return it->second; + } - const ESM::Faction* faction = MWBase::Environment::get().getWorld()->getStore().get().find(fact1); + const ESM::Faction* faction = MWBase::Environment::get().getESMStore()->get().find(faction1); - std::map::const_iterator it = faction->mReactions.begin(); + auto it = faction->mReactions.begin(); for (; it != faction->mReactions.end(); ++it) { - if (Misc::StringUtils::ciEqual(it->first, fact2)) - return it->second; + if (it->first == faction2) + return it->second; } return 0; } - void DialogueManager::clearInfoActor(const MWWorld::Ptr &actor) const + const std::map* DialogueManager::getFactionReactionOverrides(const ESM::RefId& faction) const + { + // Make sure the faction exists + MWBase::Environment::get().getESMStore()->get().find(faction); + + const auto found = mChangedFactionReaction.find(faction); + if (found != mChangedFactionReaction.end()) + return &found->second; + return nullptr; + } + + void DialogueManager::clearInfoActor(const MWWorld::Ptr& actor) const { if (actor == mActor && !mLastTopic.empty()) { MWBase::Environment::get().getJournal()->removeLastAddedTopicResponse( - Misc::StringUtils::lowerCase(mLastTopic), actor.getClass().getName(actor)); + mLastTopic, actor.getClass().getName(actor)); } } } diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp index b35bee6d434..a735c57fefb 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp @@ -4,12 +4,15 @@ #include "../mwbase/dialoguemanager.hpp" #include +#include #include #include #include +#include +#include +#include #include -#include #include "../mwworld/ptr.hpp" @@ -24,99 +27,109 @@ namespace MWDialogue { class DialogueManager : public MWBase::DialogueManager { - std::set mKnownTopics;// Those are the topics the player knows. + struct ActorKnownTopicInfo + { + int mFlags; + const ESM::DialInfo* mInfo; + }; - // Modified faction reactions. > - typedef std::map > ModFactionReactionMap; - ModFactionReactionMap mChangedFactionReaction; + std::set mKnownTopics; // Those are the topics the player knows. - std::set mActorKnownTopics; - std::unordered_map mActorKnownTopicsFlag; + // Modified faction reactions. > + typedef std::map> ModFactionReactionMap; + ModFactionReactionMap mChangedFactionReaction; - Translation::Storage& mTranslationDataStorage; - MWScript::CompilerContext mCompilerContext; - Compiler::StreamErrorHandler mErrorHandler; + std::map mActorKnownTopics; - MWWorld::Ptr mActor; - bool mTalkedTo; + Translation::Storage& mTranslationDataStorage; + MWScript::CompilerContext mCompilerContext; + Compiler::StreamErrorHandler mErrorHandler; - int mChoice; - std::string mLastTopic; // last topic ID, lowercase - bool mIsInChoice; - bool mGoodbye; + MWWorld::Ptr mActor; + bool mTalkedTo; - std::vector > mChoices; + int mChoice; + ESM::RefId mLastTopic; // last topic ID, lowercase + bool mIsInChoice; + bool mGoodbye; - float mTemporaryDispositionChange; - float mPermanentDispositionChange; + std::vector> mChoices; - void parseText (const std::string& text); + int mOriginalDisposition; + int mCurrentDisposition; + int mPermanentDispositionChange; - void updateActorKnownTopics(); - void updateGlobals(); + std::vector parseTopicIdsFromText(const std::string& text); + void addTopicsFromText(const std::string& text); - bool compile (const std::string& cmd, std::vector& code, const MWWorld::Ptr& actor); - void executeScript (const std::string& script, const MWWorld::Ptr& actor); + void updateActorKnownTopics(); + void updateGlobals(); - void executeTopic (const std::string& topic, ResponseCallback* callback); + std::optional compile(const std::string& cmd, const MWWorld::Ptr& actor); - const ESM::Dialogue* searchDialogue(const std::string& id); + void executeScript(const std::string& script, const MWWorld::Ptr& actor); - public: + void executeTopic(const ESM::RefId& topic, ResponseCallback* callback); - DialogueManager (const Compiler::Extensions& extensions, Translation::Storage& translationDataStorage); + const ESM::Dialogue* searchDialogue(const ESM::RefId& id); - void clear() override; + void updateOriginalDisposition(); - bool isInChoice() const override; + public: + DialogueManager(const Compiler::Extensions& extensions, Translation::Storage& translationDataStorage); - bool startDialogue (const MWWorld::Ptr& actor, ResponseCallback* callback) override; + void clear() override; - std::list getAvailableTopics() override; - int getTopicFlag(const std::string& topicId) override; + bool isInChoice() const override; - bool inJournal (const std::string& topicId, const std::string& infoId) override; + bool startDialogue(const MWWorld::Ptr& actor, ResponseCallback* callback) override; - void addTopic (const std::string& topic) override; + std::list getAvailableTopics() override; + int getTopicFlag(const ESM::RefId& topicId) const override; - void addChoice (const std::string& text,int choice) override; - const std::vector >& getChoices() override; + bool inJournal(const ESM::RefId& topicId, const ESM::RefId& infoId) const override; - bool isGoodbye() override; + void addTopic(const ESM::RefId& topic) override; - void goodbye() override; + void addChoice(std::string_view text, int choice) override; + const std::vector>& getChoices() const override; - bool checkServiceRefused (ResponseCallback* callback, ServiceType service = ServiceType::Any) override; + bool isGoodbye() const override; - void say(const MWWorld::Ptr &actor, const std::string &topic) override; + void goodbye() override; - //calbacks for the GUI - void keywordSelected (const std::string& keyword, ResponseCallback* callback) override; - void goodbyeSelected() override; - void questionAnswered (int answer, ResponseCallback* callback) override; + bool checkServiceRefused(ResponseCallback* callback, ServiceType service = ServiceType::Any) override; - void persuade (int type, ResponseCallback* callback) override; - int getTemporaryDispositionChange () const override; + bool say(const MWWorld::Ptr& actor, const ESM::RefId& topic) override; - /// @note Controlled by an option, gets discarded when dialogue ends by default - void applyBarterDispositionChange (int delta) override; + // calbacks for the GUI + void keywordSelected(std::string_view keyword, ResponseCallback* callback) override; + void goodbyeSelected() override; + void questionAnswered(int answer, ResponseCallback* callback) override; - int countSavedGameRecords() const override; + void persuade(int type, ResponseCallback* callback) override; - void write (ESM::ESMWriter& writer, Loading::Listener& progress) const override; + /// @note Controlled by an option, gets discarded when dialogue ends by default + void applyBarterDispositionChange(int delta) override; - void readRecord (ESM::ESMReader& reader, uint32_t type) override; + int countSavedGameRecords() const override; - /// Changes faction1's opinion of faction2 by \a diff. - void modFactionReaction (const std::string& faction1, const std::string& faction2, int diff) override; + void write(ESM::ESMWriter& writer, Loading::Listener& progress) const override; - void setFactionReaction (const std::string& faction1, const std::string& faction2, int absolute) override; + void readRecord(ESM::ESMReader& reader, uint32_t type) override; - /// @return faction1's opinion of faction2 - int getFactionReaction (const std::string& faction1, const std::string& faction2) const override; + /// Changes faction1's opinion of faction2 by \a diff. + void modFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int diff) override; - /// Removes the last added topic response for the given actor from the journal - void clearInfoActor (const MWWorld::Ptr& actor) const override; + void setFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int absolute) override; + + /// @return faction1's opinion of faction2 + int getFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2) const override; + + const std::map* getFactionReactionOverrides(const ESM::RefId& faction) const override; + + /// Removes the last added topic response for the given actor from the journal + void clearInfoActor(const MWWorld::Ptr& actor) const override; }; } diff --git a/apps/openmw/mwdialogue/filter.cpp b/apps/openmw/mwdialogue/filter.cpp index 334a9db39fd..295d690ce53 100644 --- a/apps/openmw/mwdialogue/filter.cpp +++ b/apps/openmw/mwdialogue/filter.cpp @@ -1,34 +1,92 @@ #include "filter.hpp" #include +#include +#include +#include +#include +#include +#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/mechanicsmanager.hpp" -#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/scriptmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/class.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" -#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/magiceffects.hpp" -#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/npcstats.hpp" #include "selectwrapper.hpp" -bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const +namespace { - bool isCreature = (mActor.getTypeName() != typeid (ESM::NPC).name()); + bool matchesStaticFilters(const MWDialogue::SelectWrapper& select, const MWWorld::Ptr& actor) + { + const ESM::RefId selectId = select.getId(); + if (select.getFunction() == ESM::DialogueCondition::Function_NotId) + return actor.getCellRef().getRefId() != selectId; + if (actor.getClass().isNpc()) + { + if (select.getFunction() == ESM::DialogueCondition::Function_NotFaction) + return actor.getClass().getPrimaryFaction(actor) != selectId; + else if (select.getFunction() == ESM::DialogueCondition::Function_NotClass) + return actor.get()->mBase->mClass != selectId; + else if (select.getFunction() == ESM::DialogueCondition::Function_NotRace) + return actor.get()->mBase->mRace != selectId; + } + return true; + } + + bool matchesStaticFilters(const ESM::DialInfo& info, const MWWorld::Ptr& actor) + { + for (const auto& select : info.mSelects) + { + MWDialogue::SelectWrapper wrapper = select; + if (wrapper.getType() == MWDialogue::SelectWrapper::Type_Boolean) + { + if (!wrapper.selectCompare(matchesStaticFilters(wrapper, actor))) + return false; + } + else if (wrapper.getType() == MWDialogue::SelectWrapper::Type_Inverted) + { + if (!matchesStaticFilters(wrapper, actor)) + return false; + } + else if (wrapper.getType() == MWDialogue::SelectWrapper::Type_Numeric) + { + if (wrapper.getFunction() == ESM::DialogueCondition::Function_Local) + { + const ESM::RefId& scriptName = actor.getClass().getScript(actor); + if (scriptName.empty()) + return false; + const Compiler::Locals& localDefs + = MWBase::Environment::get().getScriptManager()->getLocals(scriptName); + char type = localDefs.getType(wrapper.getName()); + if (type == ' ') + return false; // script does not have a variable of this name. + } + } + } + return true; + } +} + +bool MWDialogue::Filter::testActor(const ESM::DialInfo& info) const +{ + bool isCreature = (mActor.getType() != ESM::NPC::sRecordId); // actor id if (!info.mActor.empty()) { - if ( !Misc::StringUtils::ciEqual(info.mActor, mActor.getCellRef().getRefId())) + if (info.mActor != mActor.getCellRef().getRefId()) return false; } else if (isCreature) @@ -43,9 +101,9 @@ bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const if (isCreature) return true; - MWWorld::LiveCellRef *cellRef = mActor.get(); + MWWorld::LiveCellRef* cellRef = mActor.get(); - if (!Misc::StringUtils::ciEqual(info.mRace, cellRef->mBase->mRace)) + if (!(info.mRace == cellRef->mBase->mRace)) return false; } @@ -55,9 +113,9 @@ bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const if (isCreature) return true; - MWWorld::LiveCellRef *cellRef = mActor.get(); + MWWorld::LiveCellRef* cellRef = mActor.get(); - if ( !Misc::StringUtils::ciEqual(info.mClass, cellRef->mBase->mClass)) + if (!(info.mClass == cellRef->mBase->mClass)) return false; } @@ -75,7 +133,7 @@ bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const if (isCreature) return true; - if (!Misc::StringUtils::ciEqual(mActor.getClass().getPrimaryFaction(mActor), info.mFaction)) + if (!(mActor.getClass().getPrimaryFaction(mActor) == info.mFaction)) return false; // check rank @@ -97,24 +155,24 @@ bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const if (!isCreature) { MWWorld::LiveCellRef* npc = mActor.get(); - if (info.mData.mGender==(npc->mBase->mFlags & npc->mBase->Female ? 0 : 1)) + if (info.mData.mGender == ((npc->mBase->mFlags & ESM::NPC::Female) ? 0 : 1)) return false; } return true; } -bool MWDialogue::Filter::testPlayer (const ESM::DialInfo& info) const +bool MWDialogue::Filter::testPlayer(const ESM::DialInfo& info) const { const MWWorld::Ptr player = MWMechanics::getPlayer(); - MWMechanics::NpcStats& stats = player.getClass().getNpcStats (player); + MWMechanics::NpcStats& stats = player.getClass().getNpcStats(player); // check player faction and rank if (!info.mPcFaction.empty()) { - std::map::const_iterator iter = stats.getFactionRanks().find (Misc::StringUtils::lowerCase (info.mPcFaction)); + std::map::const_iterator iter = stats.getFactionRanks().find(info.mPcFaction); - if(iter==stats.getFactionRanks().end()) + if (iter == stats.getFactionRanks().end()) return false; // check rank @@ -124,9 +182,10 @@ bool MWDialogue::Filter::testPlayer (const ESM::DialInfo& info) const else if (info.mData.mPCrank != -1) { // required PC faction is not specified but PC rank is; use speaker's faction - std::map::const_iterator iter = stats.getFactionRanks().find (Misc::StringUtils::lowerCase (mActor.getClass().getPrimaryFaction(mActor))); + std::map::const_iterator iter + = stats.getFactionRanks().find(mActor.getClass().getPrimaryFaction(mActor)); - if(iter==stats.getFactionRanks().end()) + if (iter == stats.getFactionRanks().end()) return false; // check rank @@ -138,29 +197,26 @@ bool MWDialogue::Filter::testPlayer (const ESM::DialInfo& info) const if (!info.mCell.empty()) { // supports partial matches, just like getPcCell - const std::string& playerCell = MWBase::Environment::get().getWorld()->getCellName(player.getCell()); - bool match = playerCell.length()>=info.mCell.length() && - Misc::StringUtils::ciEqual(playerCell.substr (0, info.mCell.length()), info.mCell); - if (!match) + std::string_view playerCell = MWBase::Environment::get().getWorld()->getCellName(player.getCell()); + if (!Misc::StringUtils::ciStartsWith(playerCell, info.mCell.getRefIdString())) return false; } return true; } -bool MWDialogue::Filter::testSelectStructs (const ESM::DialInfo& info) const +bool MWDialogue::Filter::testSelectStructs(const ESM::DialInfo& info) const { - for (std::vector::const_iterator iter (info.mSelects.begin()); - iter != info.mSelects.end(); ++iter) - if (!testSelectStruct (*iter)) + for (const auto& select : info.mSelects) + if (!testSelectStruct(select)) return false; return true; } -bool MWDialogue::Filter::testDisposition (const ESM::DialInfo& info, bool invert) const +bool MWDialogue::Filter::testDisposition(const ESM::DialInfo& info, bool invert) const { - bool isCreature = (mActor.getTypeName() != typeid (ESM::NPC).name()); + bool isCreature = (mActor.getType() != ESM::NPC::sRecordId); if (isCreature) return true; @@ -173,22 +229,21 @@ bool MWDialogue::Filter::testDisposition (const ESM::DialInfo& info, bool invert bool MWDialogue::Filter::testFunctionLocal(const MWDialogue::SelectWrapper& select) const { - std::string scriptName = mActor.getClass().getScript (mActor); + const ESM::RefId& scriptName = mActor.getClass().getScript(mActor); if (scriptName.empty()) return false; // no script - std::string name = Misc::StringUtils::lowerCase (select.getName()); + std::string name = select.getName(); - const Compiler::Locals& localDefs = - MWBase::Environment::get().getScriptManager()->getLocals (scriptName); + const Compiler::Locals& localDefs = MWBase::Environment::get().getScriptManager()->getLocals(scriptName); - char type = localDefs.getType (name); + char type = localDefs.getType(name); - if (type==' ') + if (type == ' ') return false; // script does not have a variable of this name. - int index = localDefs.getIndex (name); + int index = localDefs.getIndex(name); if (index < 0) return false; // shouldn't happen, we checked that variable has a type above, so must exist @@ -197,245 +252,296 @@ bool MWDialogue::Filter::testFunctionLocal(const MWDialogue::SelectWrapper& sele return select.selectCompare(0); switch (type) { - case 's': return select.selectCompare (static_cast (locals.mShorts[index])); - case 'l': return select.selectCompare (locals.mLongs[index]); - case 'f': return select.selectCompare (locals.mFloats[index]); + case 's': + return select.selectCompare(static_cast(locals.mShorts[index])); + case 'l': + return select.selectCompare(locals.mLongs[index]); + case 'f': + return select.selectCompare(locals.mFloats[index]); } - throw std::logic_error ("unknown local variable type in dialogue filter"); + throw std::logic_error("unknown local variable type in dialogue filter"); } -bool MWDialogue::Filter::testSelectStruct (const SelectWrapper& select) const +bool MWDialogue::Filter::testSelectStruct(const SelectWrapper& select) const { - if (select.isNpcOnly() && (mActor.getTypeName() != typeid (ESM::NPC).name())) + if (select.isNpcOnly() && (mActor.getType() != ESM::NPC::sRecordId)) // If the actor is a creature, we pass all conditions only applicable to NPCs. return true; - if (select.getFunction() == SelectWrapper::Function_Choice && mChoice == -1) + 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; - if (select.getFunction() == SelectWrapper::Function_Weather && !(MWBase::Environment::get().getWorld()->isCellExterior() || MWBase::Environment::get().getWorld()->isCellQuasiExterior())) + if (select.getFunction() == ESM::DialogueCondition::Function_Weather + && !(MWBase::Environment::get().getWorld()->isCellExterior() + || MWBase::Environment::get().getWorld()->isCellQuasiExterior())) // Reject weather conditions in interior cells - // Note that the original engine doesn't include the "|| isCellQuasiExterior()" check, which could be considered a bug. + // Note that the original engine doesn't include the "|| isCellQuasiExterior()" check, which could be considered + // a bug. return false; switch (select.getType()) { - case SelectWrapper::Type_None: return true; - case SelectWrapper::Type_Integer: return select.selectCompare (getSelectStructInteger (select)); - case SelectWrapper::Type_Numeric: return testSelectStructNumeric (select); - case SelectWrapper::Type_Boolean: return select.selectCompare (getSelectStructBoolean (select)); + case SelectWrapper::Type_None: + return true; + case SelectWrapper::Type_Integer: + return select.selectCompare(getSelectStructInteger(select)); + case SelectWrapper::Type_Numeric: + return testSelectStructNumeric(select); + case SelectWrapper::Type_Boolean: + return select.selectCompare(getSelectStructBoolean(select)); // We must not do the comparison for inverted functions (eg. Function_NotClass) - case SelectWrapper::Type_Inverted: return getSelectStructBoolean (select); + case SelectWrapper::Type_Inverted: + return getSelectStructBoolean(select); } return true; } -bool MWDialogue::Filter::testSelectStructNumeric (const SelectWrapper& select) const +bool MWDialogue::Filter::testSelectStructNumeric(const SelectWrapper& select) const { switch (select.getFunction()) { - case SelectWrapper::Function_Global: + case ESM::DialogueCondition::Function_Global: // internally all globals are float :( - return select.selectCompare ( - MWBase::Environment::get().getWorld()->getGlobalFloat (select.getName())); + return select.selectCompare(MWBase::Environment::get().getWorld()->getGlobalFloat(select.getName())); - case SelectWrapper::Function_Local: + case ESM::DialogueCondition::Function_Local: { return testFunctionLocal(select); } - case SelectWrapper::Function_NotLocal: + case ESM::DialogueCondition::Function_NotLocal: { return !testFunctionLocal(select); } - case SelectWrapper::Function_PcHealthPercent: + case ESM::DialogueCondition::Function_PcHealthPercent: { MWWorld::Ptr player = MWMechanics::getPlayer(); - - float ratio = player.getClass().getCreatureStats (player).getHealth().getCurrent() / - player.getClass().getCreatureStats (player).getHealth().getModified(); - - return select.selectCompare (static_cast(ratio*100)); + return select.selectCompare( + static_cast(player.getClass().getCreatureStats(player).getHealth().getRatio() * 100)); } - case SelectWrapper::Function_PcDynamicStat: + case ESM::DialogueCondition::Function_PcMagicka: + case ESM::DialogueCondition::Function_PcFatigue: + case ESM::DialogueCondition::Function_PcHealth: { MWWorld::Ptr player = MWMechanics::getPlayer(); - float value = player.getClass().getCreatureStats (player). - getDynamic (select.getArgument()).getCurrent(); + float value = player.getClass().getCreatureStats(player).getDynamic(select.getArgument()).getCurrent(); - return select.selectCompare (value); + return select.selectCompare(value); } - case SelectWrapper::Function_HealthPercent: + case ESM::DialogueCondition::Function_Health_Percent: { - float ratio = mActor.getClass().getCreatureStats (mActor).getHealth().getCurrent() / - mActor.getClass().getCreatureStats (mActor).getHealth().getModified(); - - return select.selectCompare (static_cast(ratio*100)); + return select.selectCompare( + static_cast(mActor.getClass().getCreatureStats(mActor).getHealth().getRatio() * 100)); } default: - throw std::runtime_error ("unknown numeric select function"); + throw std::runtime_error("unknown numeric select function"); } } -int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) const +int MWDialogue::Filter::getSelectStructInteger(const SelectWrapper& select) const { MWWorld::Ptr player = MWMechanics::getPlayer(); switch (select.getFunction()) { - case SelectWrapper::Function_Journal: + case ESM::DialogueCondition::Function_Journal: - return MWBase::Environment::get().getJournal()->getJournalIndex (select.getName()); + return MWBase::Environment::get().getJournal()->getJournalIndex(select.getId()); - case SelectWrapper::Function_Item: + case ESM::DialogueCondition::Function_Item: { - MWWorld::ContainerStore& store = player.getClass().getContainerStore (player); + MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); - return store.count(select.getName()); + return store.count(select.getId()); } - case SelectWrapper::Function_Dead: + case ESM::DialogueCondition::Function_Dead: - return MWBase::Environment::get().getMechanicsManager()->countDeaths (select.getName()); + return MWBase::Environment::get().getMechanicsManager()->countDeaths(select.getId()); - case SelectWrapper::Function_Choice: + case ESM::DialogueCondition::Function_Choice: return mChoice; - case SelectWrapper::Function_AiSetting: - - return mActor.getClass().getCreatureStats (mActor).getAiSetting ( - (MWMechanics::CreatureStats::AiSetting)select.getArgument()).getModified(false); - - case SelectWrapper::Function_PcAttribute: - - return player.getClass().getCreatureStats (player). - getAttribute (select.getArgument()).getModified(); - - case SelectWrapper::Function_PcSkill: - - return static_cast (player.getClass(). - getNpcStats (player).getSkill (select.getArgument()).getModified()); + case ESM::DialogueCondition::Function_Fight: + case ESM::DialogueCondition::Function_Hello: + case ESM::DialogueCondition::Function_Alarm: + case ESM::DialogueCondition::Function_Flee: + { + int argument = select.getArgument(); + if (argument < 0 || argument > 3) + { + throw std::runtime_error("AiSetting index is out of range"); + } - case SelectWrapper::Function_FriendlyHit: + return mActor.getClass() + .getCreatureStats(mActor) + .getAiSetting(static_cast(argument)) + .getModified(false); + } + case ESM::DialogueCondition::Function_PcStrength: + case ESM::DialogueCondition::Function_PcIntelligence: + case ESM::DialogueCondition::Function_PcWillpower: + case ESM::DialogueCondition::Function_PcAgility: + case ESM::DialogueCondition::Function_PcSpeed: + case ESM::DialogueCondition::Function_PcEndurance: + case ESM::DialogueCondition::Function_PcPersonality: + case ESM::DialogueCondition::Function_PcLuck: { - int hits = mActor.getClass().getCreatureStats (mActor).getFriendlyHits(); + ESM::RefId attribute = ESM::Attribute::indexToRefId(select.getArgument()); + return player.getClass().getCreatureStats(player).getAttribute(attribute).getModified(); + } + case ESM::DialogueCondition::Function_PcBlock: + case ESM::DialogueCondition::Function_PcArmorer: + case ESM::DialogueCondition::Function_PcMediumArmor: + case ESM::DialogueCondition::Function_PcHeavyArmor: + case ESM::DialogueCondition::Function_PcBluntWeapon: + case ESM::DialogueCondition::Function_PcLongBlade: + case ESM::DialogueCondition::Function_PcAxe: + case ESM::DialogueCondition::Function_PcSpear: + case ESM::DialogueCondition::Function_PcAthletics: + case ESM::DialogueCondition::Function_PcEnchant: + case ESM::DialogueCondition::Function_PcDestruction: + case ESM::DialogueCondition::Function_PcAlteration: + case ESM::DialogueCondition::Function_PcIllusion: + case ESM::DialogueCondition::Function_PcConjuration: + case ESM::DialogueCondition::Function_PcMysticism: + case ESM::DialogueCondition::Function_PcRestoration: + case ESM::DialogueCondition::Function_PcAlchemy: + case ESM::DialogueCondition::Function_PcUnarmored: + case ESM::DialogueCondition::Function_PcSecurity: + case ESM::DialogueCondition::Function_PcSneak: + case ESM::DialogueCondition::Function_PcAcrobatics: + case ESM::DialogueCondition::Function_PcLightArmor: + case ESM::DialogueCondition::Function_PcShortBlade: + case ESM::DialogueCondition::Function_PcMarksman: + case ESM::DialogueCondition::Function_PcMerchantile: + case ESM::DialogueCondition::Function_PcSpeechcraft: + case ESM::DialogueCondition::Function_PcHandToHand: + { + ESM::RefId skill = ESM::Skill::indexToRefId(select.getArgument()); + return static_cast(player.getClass().getNpcStats(player).getSkill(skill).getModified()); + } + case ESM::DialogueCondition::Function_FriendHit: + { + int hits = mActor.getClass().getCreatureStats(mActor).getFriendlyHits(); - return hits>4 ? 4 : hits; + return hits > 4 ? 4 : hits; } - case SelectWrapper::Function_PcLevel: + case ESM::DialogueCondition::Function_PcLevel: - return player.getClass().getCreatureStats (player).getLevel(); + return player.getClass().getCreatureStats(player).getLevel(); - case SelectWrapper::Function_PcGender: + case ESM::DialogueCondition::Function_PcGender: return player.get()->mBase->isMale() ? 0 : 1; - case SelectWrapper::Function_PcClothingModifier: + case ESM::DialogueCondition::Function_PcClothingModifier: { - const MWWorld::InventoryStore& store = player.getClass().getInventoryStore (player); + const MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); int value = 0; - for (int i=0; i<=15; ++i) // everything except things held in hands and ammunition + for (int i = 0; i <= 15; ++i) // everything except things held in hands and ammunition { - MWWorld::ConstContainerStoreIterator slot = store.getSlot (i); + MWWorld::ConstContainerStoreIterator slot = store.getSlot(i); - if (slot!=store.end()) - value += slot->getClass().getValue (*slot); + if (slot != store.end()) + value += slot->getClass().getValue(*slot); } return value; } - case SelectWrapper::Function_PcCrimeLevel: + case ESM::DialogueCondition::Function_PcCrimeLevel: - return player.getClass().getNpcStats (player).getBounty(); + return player.getClass().getNpcStats(player).getBounty(); - case SelectWrapper::Function_RankRequirement: + case ESM::DialogueCondition::Function_RankRequirement: { - std::string faction = mActor.getClass().getPrimaryFaction(mActor); + const ESM::RefId& faction = mActor.getClass().getPrimaryFaction(mActor); if (faction.empty()) return 0; - int rank = getFactionRank (player, faction); + int rank = getFactionRank(player, faction); - if (rank>=9) + if (rank >= 9) return 0; // max rank int result = 0; - if (hasFactionRankSkillRequirements (player, faction, rank+1)) + if (hasFactionRankSkillRequirements(player, faction, rank + 1)) result += 1; - if (hasFactionRankReputationRequirements (player, faction, rank+1)) + if (hasFactionRankReputationRequirements(player, faction, rank + 1)) result += 2; return result; } - case SelectWrapper::Function_Level: + case ESM::DialogueCondition::Function_Level: - return mActor.getClass().getCreatureStats (mActor).getLevel(); + return mActor.getClass().getCreatureStats(mActor).getLevel(); - case SelectWrapper::Function_PCReputation: + case ESM::DialogueCondition::Function_PcReputation: - return player.getClass().getNpcStats (player).getReputation(); + return player.getClass().getNpcStats(player).getReputation(); - case SelectWrapper::Function_Weather: + case ESM::DialogueCondition::Function_Weather: return MWBase::Environment::get().getWorld()->getCurrentWeather(); - case SelectWrapper::Function_Reputation: + case ESM::DialogueCondition::Function_Reputation: - return mActor.getClass().getNpcStats (mActor).getReputation(); + return mActor.getClass().getNpcStats(mActor).getReputation(); - case SelectWrapper::Function_FactionRankDiff: + case ESM::DialogueCondition::Function_FactionRankDifference: { - std::string faction = mActor.getClass().getPrimaryFaction(mActor); + const ESM::RefId& faction = mActor.getClass().getPrimaryFaction(mActor); if (faction.empty()) return 0; - int rank = getFactionRank (player, faction); + int rank = getFactionRank(player, faction); int npcRank = mActor.getClass().getPrimaryFactionRank(mActor); - return rank-npcRank; + return rank - npcRank; } - case SelectWrapper::Function_WerewolfKills: + case ESM::DialogueCondition::Function_PcWerewolfKills: - return player.getClass().getNpcStats (player).getWerewolfKills(); + return player.getClass().getNpcStats(player).getWerewolfKills(); - case SelectWrapper::Function_RankLow: - case SelectWrapper::Function_RankHigh: + case ESM::DialogueCondition::Function_FacReactionLowest: + case ESM::DialogueCondition::Function_FacReactionHighest: { - bool low = select.getFunction()==SelectWrapper::Function_RankLow; + bool low = select.getFunction() == ESM::DialogueCondition::Function_FacReactionLowest; - std::string factionId = mActor.getClass().getPrimaryFaction(mActor); + const ESM::RefId& factionId = mActor.getClass().getPrimaryFaction(mActor); if (factionId.empty()) return 0; int value = 0; - MWMechanics::NpcStats& playerStats = player.getClass().getNpcStats (player); + MWMechanics::NpcStats& playerStats = player.getClass().getNpcStats(player); - std::map::const_iterator playerFactionIt = playerStats.getFactionRanks().begin(); + std::map::const_iterator playerFactionIt = playerStats.getFactionRanks().begin(); for (; playerFactionIt != playerStats.getFactionRanks().end(); ++playerFactionIt) { - int reaction = MWBase::Environment::get().getDialogueManager()->getFactionReaction(factionId, playerFactionIt->first); + int reaction = MWBase::Environment::get().getDialogueManager()->getFactionReaction( + factionId, playerFactionIt->first); if (low ? reaction < value : reaction > value) value = reaction; } @@ -443,88 +549,87 @@ int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) con return value; } - case SelectWrapper::Function_CreatureTargetted: + case ESM::DialogueCondition::Function_CreatureTarget: + { + MWWorld::Ptr target; + mActor.getClass().getCreatureStats(mActor).getAiSequence().getCombatTarget(target); + if (!target.isEmpty()) { - MWWorld::Ptr target; - mActor.getClass().getCreatureStats(mActor).getAiSequence().getCombatTarget(target); - if (target) - { - if (target.getClass().isNpc() && target.getClass().getNpcStats(target).isWerewolf()) - return 2; - if (target.getTypeName() == typeid(ESM::Creature).name()) - return 1; - } + if (target.getClass().isNpc() && target.getClass().getNpcStats(target).isWerewolf()) + return 2; + if (target.getType() == ESM::Creature::sRecordId) + return 1; } + } return 0; default: - throw std::runtime_error ("unknown integer select function"); + throw std::runtime_error("unknown integer select function"); } } -bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) const +bool MWDialogue::Filter::getSelectStructBoolean(const SelectWrapper& select) const { MWWorld::Ptr player = MWMechanics::getPlayer(); switch (select.getFunction()) { - case SelectWrapper::Function_False: + case ESM::DialogueCondition::Function_NotId: - return false; + return mActor.getCellRef().getRefId() != select.getId(); - case SelectWrapper::Function_NotId: + case ESM::DialogueCondition::Function_NotFaction: - return !Misc::StringUtils::ciEqual(mActor.getCellRef().getRefId(), select.getName()); + return mActor.getClass().getPrimaryFaction(mActor) != select.getId(); - case SelectWrapper::Function_NotFaction: + case ESM::DialogueCondition::Function_NotClass: - return !Misc::StringUtils::ciEqual(mActor.getClass().getPrimaryFaction(mActor), select.getName()); + return mActor.get()->mBase->mClass != select.getId(); - case SelectWrapper::Function_NotClass: + case ESM::DialogueCondition::Function_NotRace: - return !Misc::StringUtils::ciEqual(mActor.get()->mBase->mClass, select.getName()); + return mActor.get()->mBase->mRace != select.getId(); - case SelectWrapper::Function_NotRace: - - return !Misc::StringUtils::ciEqual(mActor.get()->mBase->mRace, select.getName()); - - case SelectWrapper::Function_NotCell: - { - const std::string& actorCell = MWBase::Environment::get().getWorld()->getCellName(mActor.getCell()); - return !(actorCell.length() >= select.getName().length() - && Misc::StringUtils::ciEqual(actorCell.substr(0, select.getName().length()), select.getName())); - } - case SelectWrapper::Function_SameGender: + case ESM::DialogueCondition::Function_NotCell: + { + std::string_view actorCell = MWBase::Environment::get().getWorld()->getCellName(mActor.getCell()); + return !Misc::StringUtils::ciStartsWith(actorCell, select.getCellName()); + } + case ESM::DialogueCondition::Function_SameSex: - return (player.get()->mBase->mFlags & ESM::NPC::Female)== - (mActor.get()->mBase->mFlags & ESM::NPC::Female); + return (player.get()->mBase->mFlags & ESM::NPC::Female) + == (mActor.get()->mBase->mFlags & ESM::NPC::Female); - case SelectWrapper::Function_SameRace: + case ESM::DialogueCondition::Function_SameRace: - return Misc::StringUtils::ciEqual(mActor.get()->mBase->mRace, player.get()->mBase->mRace); + return mActor.get()->mBase->mRace == player.get()->mBase->mRace; - case SelectWrapper::Function_SameFaction: + case ESM::DialogueCondition::Function_SameFaction: - return player.getClass().getNpcStats (player).isInFaction(mActor.getClass().getPrimaryFaction(mActor)); + return player.getClass().getNpcStats(player).isInFaction(mActor.getClass().getPrimaryFaction(mActor)); - case SelectWrapper::Function_PcCommonDisease: + case ESM::DialogueCondition::Function_PcCommonDisease: - return player.getClass().getCreatureStats (player).hasCommonDisease(); + return player.getClass().getCreatureStats(player).hasCommonDisease(); - case SelectWrapper::Function_PcBlightDisease: + case ESM::DialogueCondition::Function_PcBlightDisease: - return player.getClass().getCreatureStats (player).hasBlightDisease(); + return player.getClass().getCreatureStats(player).hasBlightDisease(); - case SelectWrapper::Function_PcCorprus: + case ESM::DialogueCondition::Function_PcCorprus: - return player.getClass().getCreatureStats (player). - getMagicEffects().get (ESM::MagicEffect::Corprus).getMagnitude()!=0; + return player.getClass() + .getCreatureStats(player) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Corprus) + .getMagnitude() + != 0; - case SelectWrapper::Function_PcExpelled: + case ESM::DialogueCondition::Function_PcExpelled: { - std::string faction = mActor.getClass().getPrimaryFaction(mActor); + const ESM::RefId& faction = mActor.getClass().getPrimaryFaction(mActor); if (faction.empty()) return false; @@ -532,126 +637,115 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co return player.getClass().getNpcStats(player).getExpelled(faction); } - case SelectWrapper::Function_PcVampire: + case ESM::DialogueCondition::Function_PcVampire: - return player.getClass().getCreatureStats(player).getMagicEffects(). - get(ESM::MagicEffect::Vampirism).getMagnitude() > 0; + return player.getClass() + .getCreatureStats(player) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Vampirism) + .getMagnitude() + > 0; - case SelectWrapper::Function_TalkedToPc: + case ESM::DialogueCondition::Function_TalkedToPc: return mTalkedToPlayer; - case SelectWrapper::Function_Alarmed: + case ESM::DialogueCondition::Function_Alarmed: - return mActor.getClass().getCreatureStats (mActor).isAlarmed(); + return mActor.getClass().getCreatureStats(mActor).isAlarmed(); - case SelectWrapper::Function_Detected: + case ESM::DialogueCondition::Function_Detected: return MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, mActor); - case SelectWrapper::Function_Attacked: + case ESM::DialogueCondition::Function_Attacked: - return mActor.getClass().getCreatureStats (mActor).getAttacked(); + return mActor.getClass().getCreatureStats(mActor).getAttacked(); - case SelectWrapper::Function_ShouldAttack: + case ESM::DialogueCondition::Function_ShouldAttack: - return MWBase::Environment::get().getMechanicsManager()->isAggressive(mActor, - MWMechanics::getPlayer()); + return MWBase::Environment::get().getMechanicsManager()->isAggressive(mActor, MWMechanics::getPlayer()); - case SelectWrapper::Function_Werewolf: + case ESM::DialogueCondition::Function_Werewolf: - return mActor.getClass().getNpcStats (mActor).isWerewolf(); + return mActor.getClass().getNpcStats(mActor).isWerewolf(); default: - throw std::runtime_error ("unknown boolean select function"); + throw std::runtime_error("unknown boolean select function"); } } -int MWDialogue::Filter::getFactionRank (const MWWorld::Ptr& actor, const std::string& factionId) const +int MWDialogue::Filter::getFactionRank(const MWWorld::Ptr& actor, const ESM::RefId& factionId) const { - MWMechanics::NpcStats& stats = actor.getClass().getNpcStats (actor); - - std::map::const_iterator iter = stats.getFactionRanks().find (Misc::StringUtils::lowerCase(factionId)); - - if (iter==stats.getFactionRanks().end()) - return -1; - - return iter->second; + MWMechanics::NpcStats& stats = actor.getClass().getNpcStats(actor); + return stats.getFactionRank(factionId); } -bool MWDialogue::Filter::hasFactionRankSkillRequirements (const MWWorld::Ptr& actor, - const std::string& factionId, int rank) const +bool MWDialogue::Filter::hasFactionRankSkillRequirements( + const MWWorld::Ptr& actor, const ESM::RefId& factionId, int rank) const { - if (rank<0 || rank>=10) - throw std::runtime_error ("rank index out of range"); - - if (!actor.getClass().getNpcStats (actor).hasSkillsForRank (factionId, rank)) + if (!actor.getClass().getNpcStats(actor).hasSkillsForRank(factionId, rank)) return false; - const ESM::Faction& faction = - *MWBase::Environment::get().getWorld()->getStore().get().find (factionId); + const ESM::Faction& faction = *MWBase::Environment::get().getESMStore()->get().find(factionId); - MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats (actor); + MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - return stats.getAttribute (faction.mData.mAttribute[0]).getBase()>=faction.mData.mRankData[rank].mAttribute1 && - stats.getAttribute (faction.mData.mAttribute[1]).getBase()>=faction.mData.mRankData[rank].mAttribute2; + return stats.getAttribute(ESM::Attribute::indexToRefId(faction.mData.mAttribute[0])).getBase() + >= faction.mData.mRankData[rank].mAttribute1 + && stats.getAttribute(ESM::Attribute::indexToRefId(faction.mData.mAttribute[1])).getBase() + >= faction.mData.mRankData[rank].mAttribute2; } -bool MWDialogue::Filter::hasFactionRankReputationRequirements (const MWWorld::Ptr& actor, - const std::string& factionId, int rank) const +bool MWDialogue::Filter::hasFactionRankReputationRequirements( + const MWWorld::Ptr& actor, const ESM::RefId& factionId, int rank) const { - if (rank<0 || rank>=10) - throw std::runtime_error ("rank index out of range"); - - MWMechanics::NpcStats& stats = actor.getClass().getNpcStats (actor); + MWMechanics::NpcStats& stats = actor.getClass().getNpcStats(actor); - const ESM::Faction& faction = - *MWBase::Environment::get().getWorld()->getStore().get().find (factionId); + const ESM::Faction& faction = *MWBase::Environment::get().getESMStore()->get().find(factionId); - return stats.getFactionReputation (factionId)>=faction.mData.mRankData[rank].mFactReaction; + return stats.getFactionReputation(factionId) >= faction.mData.mRankData.at(rank).mFactReaction; } -MWDialogue::Filter::Filter (const MWWorld::Ptr& actor, int choice, bool talkedToPlayer) -: mActor (actor), mChoice (choice), mTalkedToPlayer (talkedToPlayer) -{} +MWDialogue::Filter::Filter(const MWWorld::Ptr& actor, int choice, bool talkedToPlayer) + : mActor(actor) + , mChoice(choice) + , mTalkedToPlayer(talkedToPlayer) +{ +} -const ESM::DialInfo* MWDialogue::Filter::search (const ESM::Dialogue& dialogue, const bool fallbackToInfoRefusal) const +MWDialogue::Filter::Response MWDialogue::Filter::search( + const ESM::Dialogue& dialogue, const bool fallbackToInfoRefusal) const { - std::vector suitableInfos = list (dialogue, fallbackToInfoRefusal, false); + auto suitableInfos = list(dialogue, fallbackToInfoRefusal, false); if (suitableInfos.empty()) - return nullptr; + return {}; else return suitableInfos[0]; } -std::vector MWDialogue::Filter::listAll (const ESM::Dialogue& dialogue) const +bool MWDialogue::Filter::couldPotentiallyMatch(const ESM::DialInfo& info) const { - std::vector infos; - for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue.mInfo.begin(); iter!=dialogue.mInfo.end(); ++iter) - { - if (testActor (*iter)) - infos.push_back(&*iter); - } - return infos; + return testActor(info) && matchesStaticFilters(info, mActor); } -std::vector MWDialogue::Filter::list (const ESM::Dialogue& dialogue, - bool fallbackToInfoRefusal, bool searchAll, bool invertDisposition) const +std::vector MWDialogue::Filter::list( + const ESM::Dialogue& dialogue, bool fallbackToInfoRefusal, bool searchAll, bool invertDisposition) const { - std::vector infos; + std::vector infos; bool infoRefusal = false; // Iterate over topic responses to find a matching one - for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue.mInfo.begin(); - iter!=dialogue.mInfo.end(); ++iter) + for (const auto& info : dialogue.mInfo) { - if (testActor (*iter) && testPlayer (*iter) && testSelectStructs (*iter)) + if (testActor(info) && testPlayer(info) && testSelectStructs(info)) { - if (testDisposition (*iter, invertDisposition)) { - infos.push_back(&*iter); + if (testDisposition(info, invertDisposition)) + { + infos.emplace_back(&dialogue, &info); if (!searchAll) break; } @@ -665,15 +759,15 @@ std::vector MWDialogue::Filter::list (const ESM::Dialogue // No response is valid because of low NPC disposition, // search a response in the topic "Info Refusal" - const MWWorld::Store &dialogues = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& dialogues = MWBase::Environment::get().getESMStore()->get(); - const ESM::Dialogue& infoRefusalDialogue = *dialogues.find ("Info Refusal"); + const ESM::Dialogue& infoRefusalDialogue = *dialogues.find(ESM::RefId::stringRefId("Info Refusal")); - for (ESM::Dialogue::InfoContainer::const_iterator iter = infoRefusalDialogue.mInfo.begin(); - iter!=infoRefusalDialogue.mInfo.end(); ++iter) - if (testActor (*iter) && testPlayer (*iter) && testSelectStructs (*iter) && testDisposition(*iter, invertDisposition)) { - infos.push_back(&*iter); + for (const auto& info : infoRefusalDialogue.mInfo) + if (testActor(info) && testPlayer(info) && testSelectStructs(info) + && testDisposition(info, invertDisposition)) + { + infos.emplace_back(&infoRefusalDialogue, &info); if (!searchAll) break; } diff --git a/apps/openmw/mwdialogue/filter.hpp b/apps/openmw/mwdialogue/filter.hpp index d2747d59ae5..c44122aa52f 100644 --- a/apps/openmw/mwdialogue/filter.hpp +++ b/apps/openmw/mwdialogue/filter.hpp @@ -1,6 +1,7 @@ #ifndef GAME_MWDIALOGUE_FILTER_H #define GAME_MWDIALOGUE_FILTER_H +#include #include #include "../mwworld/ptr.hpp" @@ -9,6 +10,7 @@ namespace ESM { struct DialInfo; struct Dialogue; + class RefId; } namespace MWDialogue @@ -17,55 +19,56 @@ namespace MWDialogue class Filter { - MWWorld::Ptr mActor; - int mChoice; - bool mTalkedToPlayer; + MWWorld::Ptr mActor; + int mChoice; + bool mTalkedToPlayer; - bool testActor (const ESM::DialInfo& info) const; - ///< Is this the right actor for this \a info? + bool testActor(const ESM::DialInfo& info) const; + ///< Is this the right actor for this \a info? - bool testPlayer (const ESM::DialInfo& info) const; - ///< Do the player and the cell the player is currently in match \a info? + bool testPlayer(const ESM::DialInfo& info) const; + ///< Do the player and the cell the player is currently in match \a info? - bool testSelectStructs (const ESM::DialInfo& info) const; - ///< Are all select structs matching? + bool testSelectStructs(const ESM::DialInfo& info) const; + ///< Are all select structs matching? - bool testDisposition (const ESM::DialInfo& info, bool invert=false) const; - ///< Is the actor disposition toward the player high enough (or low enough, if \a invert is true)? + bool testDisposition(const ESM::DialInfo& info, bool invert = false) const; + ///< Is the actor disposition toward the player high enough (or low enough, if \a invert is true)? - bool testFunctionLocal(const SelectWrapper& select) const; + bool testFunctionLocal(const SelectWrapper& select) const; - bool testSelectStruct (const SelectWrapper& select) const; + bool testSelectStruct(const SelectWrapper& select) const; - bool testSelectStructNumeric (const SelectWrapper& select) const; + bool testSelectStructNumeric(const SelectWrapper& select) const; - int getSelectStructInteger (const SelectWrapper& select) const; + int getSelectStructInteger(const SelectWrapper& select) const; - bool getSelectStructBoolean (const SelectWrapper& select) const; + bool getSelectStructBoolean(const SelectWrapper& select) const; - int getFactionRank (const MWWorld::Ptr& actor, const std::string& factionId) const; + int getFactionRank(const MWWorld::Ptr& actor, const ESM::RefId& factionId) const; - bool hasFactionRankSkillRequirements (const MWWorld::Ptr& actor, const std::string& factionId, - int rank) const; + bool hasFactionRankSkillRequirements(const MWWorld::Ptr& actor, const ESM::RefId& factionId, int rank) const; - bool hasFactionRankReputationRequirements (const MWWorld::Ptr& actor, const std::string& factionId, - int rank) const; + bool hasFactionRankReputationRequirements( + const MWWorld::Ptr& actor, const ESM::RefId& factionId, int rank) const; - public: + public: + using Response = std::pair; - Filter (const MWWorld::Ptr& actor, int choice, bool talkedToPlayer); + Filter(const MWWorld::Ptr& actor, int choice, bool talkedToPlayer); - std::vector list (const ESM::Dialogue& dialogue, - bool fallbackToInfoRefusal, bool searchAll, bool invertDisposition=false) const; - ///< List all infos that could be used on the given actor, using the current runtime state of the actor. - /// \note If fallbackToInfoRefusal is used, the returned DialInfo might not be from the supplied ESM::Dialogue. + std::vector list(const ESM::Dialogue& dialogue, bool fallbackToInfoRefusal, bool searchAll, + bool invertDisposition = false) const; + ///< List all infos that could be used on the given actor, using the current runtime state of the actor. + /// \note If fallbackToInfoRefusal is used, the returned DialInfo might not be from the supplied ESM::Dialogue. - std::vector listAll (const ESM::Dialogue& dialogue) const; - ///< List all infos that could possibly be used on the given actor, ignoring runtime state filters and ignoring player filters. + bool couldPotentiallyMatch(const ESM::DialInfo& info) const; + ///< Check if this INFO could potentially be said by the given actor, ignoring runtime state filters and + ///< ignoring player filters. - const ESM::DialInfo* search (const ESM::Dialogue& dialogue, const bool fallbackToInfoRefusal) const; - ///< Get a matching response for the requested dialogue. - /// Redirect to "Info Refusal" topic if a response fulfills all conditions but disposition. + Response search(const ESM::Dialogue& dialogue, const bool fallbackToInfoRefusal) const; + ///< Get a matching response for the requested dialogue. + /// Redirect to "Info Refusal" topic if a response fulfills all conditions but disposition. }; } diff --git a/apps/openmw/mwdialogue/hypertextparser.cpp b/apps/openmw/mwdialogue/hypertextparser.cpp index fa7de97d2ec..de82aff7aa7 100644 --- a/apps/openmw/mwdialogue/hypertextparser.cpp +++ b/apps/openmw/mwdialogue/hypertextparser.cpp @@ -1,10 +1,9 @@ -#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwworld/store.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/store.hpp" #include "keywordsearch.hpp" @@ -14,11 +13,11 @@ namespace MWDialogue { namespace HyperTextParser { - std::vector parseHyperText(const std::string & text) + std::vector parseHyperText(const std::string& text) { std::vector result; size_t pos_end = std::string::npos, iteration_pos = 0; - for(;;) + for (;;) { size_t pos_begin = text.find('@', iteration_pos); if (pos_begin != std::string::npos) @@ -45,40 +44,30 @@ namespace MWDialogue return result; } - void tokenizeKeywords(const std::string & text, std::vector & tokens) + void tokenizeKeywords(const std::string& text, std::vector& tokens) { - const MWWorld::Store & dialogs = - MWBase::Environment::get().getWorld()->getStore().get(); - - std::list keywordList; - for (MWWorld::Store::iterator it = dialogs.begin(); it != dialogs.end(); ++it) - keywordList.push_back(Misc::StringUtils::lowerCase(it->mId)); - keywordList.sort(Misc::StringUtils::ciLess); - - KeywordSearch keywordSearch; - - for (std::list::const_iterator it = keywordList.begin(); it != keywordList.end(); ++it) - keywordSearch.seed(*it, 0 /*unused*/); + const auto& keywordSearch + = MWBase::Environment::get().getESMStore()->get().getDialogIdKeywordSearch(); - std::vector::Match> matches; + std::vector::Match> matches; keywordSearch.highlightKeywords(text.begin(), text.end(), matches); - for (std::vector::Match>::const_iterator it = matches.begin(); it != matches.end(); ++it) + for (const auto& match : matches) { - tokens.emplace_back(std::string(it->mBeg, it->mEnd), Token::ImplicitKeyword); + tokens.emplace_back(std::string(match.mBeg, match.mEnd), Token::ImplicitKeyword); } } - size_t removePseudoAsterisks(std::string & phrase) + size_t removePseudoAsterisks(std::string& phrase) { size_t pseudoAsterisksCount = 0; - if( !phrase.empty() ) + if (!phrase.empty()) { std::string::reverse_iterator rit = phrase.rbegin(); const char specialPseudoAsteriskCharacter = 127; - while( rit != phrase.rend() && *rit == specialPseudoAsteriskCharacter ) + while (rit != phrase.rend() && *rit == specialPseudoAsteriskCharacter) { pseudoAsterisksCount++; ++rit; diff --git a/apps/openmw/mwdialogue/hypertextparser.hpp b/apps/openmw/mwdialogue/hypertextparser.hpp index 4ae0474c429..efd0982dee6 100644 --- a/apps/openmw/mwdialogue/hypertextparser.hpp +++ b/apps/openmw/mwdialogue/hypertextparser.hpp @@ -16,7 +16,11 @@ namespace MWDialogue ImplicitKeyword }; - Token(const std::string & text, Type type) : mText(text), mType(type) {} + Token(const std::string& text, Type type) + : mText(text) + , mType(type) + { + } bool isExplicitLink() { return mType == ExplicitLink; } @@ -26,9 +30,9 @@ namespace MWDialogue // In translations (at least Russian) the links are marked with @#, so // it should be a function to parse it - std::vector parseHyperText(const std::string & text); - void tokenizeKeywords(const std::string & text, std::vector & tokens); - size_t removePseudoAsterisks(std::string & phrase); + std::vector parseHyperText(const std::string& text); + void tokenizeKeywords(const std::string& text, std::vector& tokens); + size_t removePseudoAsterisks(std::string& phrase); } } diff --git a/apps/openmw/mwdialogue/journalentry.cpp b/apps/openmw/mwdialogue/journalentry.cpp index 5eab6d5cae3..c691e0d4620 100644 --- a/apps/openmw/mwdialogue/journalentry.cpp +++ b/apps/openmw/mwdialogue/journalentry.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include @@ -10,22 +10,19 @@ #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/globals.hpp" #include "../mwscript/interpretercontext.hpp" - namespace MWDialogue { - Entry::Entry() {} - - Entry::Entry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor) - : mInfoId (infoId) + Entry::Entry(const ESM::RefId& topic, const ESM::RefId& infoId, const MWWorld::Ptr& actor) + : mInfoId(infoId) { - const ESM::Dialogue *dialogue = - MWBase::Environment::get().getWorld()->getStore().get().find (topic); + const ESM::Dialogue* dialogue = MWBase::Environment::get().getESMStore()->get().find(topic); - for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); - iter!=dialogue->mInfo.end(); ++iter) + for (ESM::Dialogue::InfoContainer::const_iterator iter(dialogue->mInfo.begin()); iter != dialogue->mInfo.end(); + ++iter) if (iter->mId == mInfoId) { if (actor.isEmpty()) @@ -35,96 +32,111 @@ namespace MWDialogue } else { - MWScript::InterpreterContext interpreterContext(&actor.getRefData().getLocals(),actor); + MWScript::InterpreterContext interpreterContext(&actor.getRefData().getLocals(), actor); mText = Interpreter::fixDefinesDialog(iter->mResponse, interpreterContext); } return; } - throw std::runtime_error ("unknown info ID " + mInfoId + " for topic " + topic); + throw std::runtime_error("unknown info ID " + mInfoId.toDebugString() + " for topic " + topic.toDebugString()); } - Entry::Entry (const ESM::JournalEntry& record) : mInfoId (record.mInfo), mText (record.mText), mActorName(record.mActorName) {} + Entry::Entry(const ESM::JournalEntry& record) + : mInfoId(record.mInfo) + , mText(record.mText) + , mActorName(record.mActorName) + { + } - std::string Entry::getText() const + const std::string& Entry::getText() const { return mText; } - void Entry::write (ESM::JournalEntry& entry) const + void Entry::write(ESM::JournalEntry& entry) const { entry.mInfo = mInfoId; entry.mText = mText; entry.mActorName = mActorName; } + JournalEntry::JournalEntry(const ESM::RefId& topic, const ESM::RefId& infoId, const MWWorld::Ptr& actor) + : Entry(topic, infoId, actor) + , mTopic(topic) + { + } - JournalEntry::JournalEntry() {} - - JournalEntry::JournalEntry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor) - : Entry (topic, infoId, actor), mTopic (topic) - {} - - JournalEntry::JournalEntry (const ESM::JournalEntry& record) - : Entry (record), mTopic (record.mTopic) - {} + JournalEntry::JournalEntry(const ESM::JournalEntry& record) + : Entry(record) + , mTopic(record.mTopic) + { + } - void JournalEntry::write (ESM::JournalEntry& entry) const + void JournalEntry::write(ESM::JournalEntry& entry) const { - Entry::write (entry); + Entry::write(entry); entry.mTopic = mTopic; } - JournalEntry JournalEntry::makeFromQuest (const std::string& topic, int index) + JournalEntry JournalEntry::makeFromQuest(const ESM::RefId& topic, int index) { - return JournalEntry (topic, idFromIndex (topic, index), MWWorld::Ptr()); + return JournalEntry(topic, idFromIndex(topic, index), MWWorld::Ptr()); } - std::string JournalEntry::idFromIndex (const std::string& topic, int index) + const ESM::RefId& JournalEntry::idFromIndex(const ESM::RefId& topic, int index) { - const ESM::Dialogue *dialogue = - MWBase::Environment::get().getWorld()->getStore().get().find (topic); + const ESM::Dialogue* dialogue = MWBase::Environment::get().getESMStore()->get().find(topic); - for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); - iter!=dialogue->mInfo.end(); ++iter) - if (iter->mData.mJournalIndex==index) + for (ESM::Dialogue::InfoContainer::const_iterator iter(dialogue->mInfo.begin()); iter != dialogue->mInfo.end(); + ++iter) + if (iter->mData.mJournalIndex == index) { return iter->mId; } - throw std::runtime_error ("unknown journal index for topic " + topic); + throw std::runtime_error("unknown journal index for topic " + topic.toDebugString()); } - StampedJournalEntry::StampedJournalEntry() - : mDay (0), mMonth (0), mDayOfMonth (0) - {} + : mDay(0) + , mMonth(0) + , mDayOfMonth(0) + { + } - StampedJournalEntry::StampedJournalEntry (const std::string& topic, const std::string& infoId, - int day, int month, int dayOfMonth, const MWWorld::Ptr& actor) - : JournalEntry (topic, infoId, actor), mDay (day), mMonth (month), mDayOfMonth (dayOfMonth) - {} + StampedJournalEntry::StampedJournalEntry(const ESM::RefId& topic, const ESM::RefId& infoId, int day, int month, + int dayOfMonth, const MWWorld::Ptr& actor) + : JournalEntry(topic, infoId, actor) + , mDay(day) + , mMonth(month) + , mDayOfMonth(dayOfMonth) + { + } - StampedJournalEntry::StampedJournalEntry (const ESM::JournalEntry& record) - : JournalEntry (record), mDay (record.mDay), mMonth (record.mMonth), - mDayOfMonth (record.mDayOfMonth) - {} + StampedJournalEntry::StampedJournalEntry(const ESM::JournalEntry& record) + : JournalEntry(record) + , mDay(record.mDay) + , mMonth(record.mMonth) + , mDayOfMonth(record.mDayOfMonth) + { + } - void StampedJournalEntry::write (ESM::JournalEntry& entry) const + void StampedJournalEntry::write(ESM::JournalEntry& entry) const { - JournalEntry::write (entry); + JournalEntry::write(entry); entry.mDay = mDay; entry.mMonth = mMonth; entry.mDayOfMonth = mDayOfMonth; } - StampedJournalEntry StampedJournalEntry::makeFromQuest (const std::string& topic, int index, const MWWorld::Ptr& actor) + StampedJournalEntry StampedJournalEntry::makeFromQuest( + const ESM::RefId& topic, int index, const MWWorld::Ptr& actor) { - int day = MWBase::Environment::get().getWorld()->getGlobalInt ("dayspassed"); - int month = MWBase::Environment::get().getWorld()->getGlobalInt ("month"); - int dayOfMonth = MWBase::Environment::get().getWorld()->getGlobalInt ("day"); + const int day = MWBase::Environment::get().getWorld()->getGlobalInt(MWWorld::Globals::sDaysPassed); + const int month = MWBase::Environment::get().getWorld()->getGlobalInt(MWWorld::Globals::sMonth); + const int dayOfMonth = MWBase::Environment::get().getWorld()->getGlobalInt(MWWorld::Globals::sDay); - return StampedJournalEntry (topic, idFromIndex (topic, index), day, month, dayOfMonth, actor); + return StampedJournalEntry(topic, idFromIndex(topic, index), day, month, dayOfMonth, actor); } } diff --git a/apps/openmw/mwdialogue/journalentry.hpp b/apps/openmw/mwdialogue/journalentry.hpp index 8711ab53a78..34decf0ddfd 100644 --- a/apps/openmw/mwdialogue/journalentry.hpp +++ b/apps/openmw/mwdialogue/journalentry.hpp @@ -1,7 +1,9 @@ #ifndef GAME_MWDIALOGUE_JOURNALENTRY_H #define GAME_MWDIALOGUE_JOURNALENTRY_H +#include #include +#include namespace ESM { @@ -18,20 +20,20 @@ namespace MWDialogue /// \brief Basic quest/dialogue/topic entry struct Entry { - std::string mInfoId; + ESM::RefId mInfoId; std::string mText; std::string mActorName; // optional - Entry(); + Entry() = default; /// actor is optional - Entry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor); + Entry(const ESM::RefId& topic, const ESM::RefId& infoId, const MWWorld::Ptr& actor); - Entry (const ESM::JournalEntry& record); + Entry(const ESM::JournalEntry& record); - std::string getText() const; + const std::string& getText() const; - void write (ESM::JournalEntry& entry) const; + void write(ESM::JournalEntry& entry) const; }; /// \brief A dialogue entry @@ -39,19 +41,19 @@ namespace MWDialogue /// Same as entry, but store TopicID struct JournalEntry : public Entry { - std::string mTopic; + ESM::RefId mTopic; - JournalEntry(); + JournalEntry() = default; - JournalEntry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor); + JournalEntry(const ESM::RefId& topic, const ESM::RefId& infoId, const MWWorld::Ptr& actor); - JournalEntry (const ESM::JournalEntry& record); + JournalEntry(const ESM::JournalEntry& record); - void write (ESM::JournalEntry& entry) const; + void write(ESM::JournalEntry& entry) const; - static JournalEntry makeFromQuest (const std::string& topic, int index); + static JournalEntry makeFromQuest(const ESM::RefId& topic, int index); - static std::string idFromIndex (const std::string& topic, int index); + static const ESM::RefId& idFromIndex(const ESM::RefId& topic, int index); }; /// \brief A quest entry with a timestamp. @@ -63,14 +65,14 @@ namespace MWDialogue StampedJournalEntry(); - StampedJournalEntry (const std::string& topic, const std::string& infoId, - int day, int month, int dayOfMonth, const MWWorld::Ptr& actor); + StampedJournalEntry(const ESM::RefId& topic, const ESM::RefId& infoId, int day, int month, int dayOfMonth, + const MWWorld::Ptr& actor); - StampedJournalEntry (const ESM::JournalEntry& record); + StampedJournalEntry(const ESM::JournalEntry& record); - void write (ESM::JournalEntry& entry) const; + void write(ESM::JournalEntry& entry) const; - static StampedJournalEntry makeFromQuest (const std::string& topic, int index, const MWWorld::Ptr& actor); + static StampedJournalEntry makeFromQuest(const ESM::RefId& topic, int index, const MWWorld::Ptr& actor); }; } diff --git a/apps/openmw/mwdialogue/journalimp.cpp b/apps/openmw/mwdialogue/journalimp.cpp index b2195161839..28a2e16699a 100644 --- a/apps/openmw/mwdialogue/journalimp.cpp +++ b/apps/openmw/mwdialogue/journalimp.cpp @@ -2,45 +2,50 @@ #include -#include -#include -#include -#include +#include +#include +#include +#include + +#include -#include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" - -#include "../mwgui/messagebox.hpp" +#include "../mwbase/world.hpp" namespace MWDialogue { - Quest& Journal::getQuest (const std::string& id) + Quest& Journal::getOrStartQuest(const ESM::RefId& id) { - TQuestContainer::iterator iter = mQuests.find (id); + TQuestContainer::iterator iter = mQuests.find(id); - if (iter==mQuests.end()) - { - std::pair result = - mQuests.insert (std::make_pair (id, Quest (id))); + if (iter == mQuests.end()) + iter = mQuests.emplace(id, Quest(id)).first; - iter = result.first; + return iter->second; + } + + Quest* Journal::getQuestOrNull(const ESM::RefId& id) + { + TQuestContainer::iterator iter = mQuests.find(id); + if (iter == mQuests.end()) + { + return nullptr; } - return iter->second; + return &(iter->second); } - Topic& Journal::getTopic (const std::string& id) + Topic& Journal::getTopic(const ESM::RefId& id) { - TTopicContainer::iterator iter = mTopics.find (id); + TTopicContainer::iterator iter = mTopics.find(id); - if (iter==mTopics.end()) + if (iter == mTopics.end()) { - std::pair result - = mTopics.insert (std::make_pair (id, Topic (id))); + std::pair result = mTopics.insert(std::make_pair(id, Topic(id))); iter = result.first; } @@ -48,16 +53,16 @@ namespace MWDialogue return iter->second; } - bool Journal::isThere (const std::string& topicId, const std::string& infoId) const + bool Journal::isThere(const ESM::RefId& topicId, const ESM::RefId& infoId) const { - if (const ESM::Dialogue *dialogue = - MWBase::Environment::get().getWorld()->getStore().get().search (topicId)) + if (const ESM::Dialogue* dialogue + = MWBase::Environment::get().getESMStore()->get().search(topicId)) { if (infoId.empty()) return true; - for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); - iter!=dialogue->mInfo.end(); ++iter) + for (ESM::Dialogue::InfoContainer::const_iterator iter(dialogue->mInfo.begin()); + iter != dialogue->mInfo.end(); ++iter) if (iter->mId == infoId) return true; } @@ -65,8 +70,7 @@ namespace MWDialogue return false; } - Journal::Journal() - {} + Journal::Journal() {} void Journal::clear() { @@ -75,53 +79,62 @@ namespace MWDialogue mTopics.clear(); } - void Journal::addEntry (const std::string& id, int index, const MWWorld::Ptr& actor) + void Journal::addEntry(const ESM::RefId& id, int index, const MWWorld::Ptr& actor) { // bail out if we already have heard this... - std::string infoId = JournalEntry::idFromIndex (id, index); - for (TEntryIter i = mJournal.begin (); i != mJournal.end (); ++i) + const ESM::RefId& infoId = JournalEntry::idFromIndex(id, index); + for (TEntryIter i = mJournal.begin(); i != mJournal.end(); ++i) if (i->mTopic == id && i->mInfoId == infoId) { if (getJournalIndex(id) < index) { setJournalIndex(id, index); - MWBase::Environment::get().getWindowManager()->messageBox ("#{sJournalEntry}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sJournalEntry}"); } return; } - StampedJournalEntry entry = StampedJournalEntry::makeFromQuest (id, index, actor); + StampedJournalEntry entry = StampedJournalEntry::makeFromQuest(id, index, actor); - Quest& quest = getQuest (id); - quest.addEntry (entry); // we are doing slicing on purpose here + Quest& quest = getOrStartQuest(id); + if (quest.addEntry(entry)) // we are doing slicing on purpose here + { + // Restart all "other" quests with the same name as well + std::string_view name = quest.getName(); + for (auto& it : mQuests) + { + if (it.second.isFinished() && Misc::StringUtils::ciEqual(it.second.getName(), name)) + it.second.setFinished(false); + } + } // there is no need to show empty entries in journal if (!entry.getText().empty()) { - mJournal.push_back (entry); - MWBase::Environment::get().getWindowManager()->messageBox ("#{sJournalEntry}"); + mJournal.push_back(entry); + MWBase::Environment::get().getWindowManager()->messageBox("#{sJournalEntry}"); } } - void Journal::setJournalIndex (const std::string& id, int index) + void Journal::setJournalIndex(const ESM::RefId& id, int index) { - Quest& quest = getQuest (id); + Quest& quest = getOrStartQuest(id); - quest.setIndex (index); + quest.setIndex(index); } - void Journal::addTopic (const std::string& topicId, const std::string& infoId, const MWWorld::Ptr& actor) + void Journal::addTopic(const ESM::RefId& topicId, const ESM::RefId& infoId, const MWWorld::Ptr& actor) { - Topic& topic = getTopic (topicId); + Topic& topic = getTopic(topicId); JournalEntry entry(topicId, infoId, actor); entry.mActorName = actor.getClass().getName(actor); - topic.addEntry (entry); + topic.addEntry(entry); } - void Journal::removeLastAddedTopicResponse(const std::string &topicId, const std::string &actorName) + void Journal::removeLastAddedTopicResponse(const ESM::RefId& topicId, std::string_view actorName) { - Topic& topic = getTopic (topicId); + Topic& topic = getTopic(topicId); topic.removeLastAddedResponse(actorName); @@ -129,11 +142,11 @@ namespace MWDialogue mTopics.erase(mTopics.find(topicId)); // All responses removed -> remove topic } - int Journal::getJournalIndex (const std::string& id) const + int Journal::getJournalIndex(const ESM::RefId& id) const { - TQuestContainer::const_iterator iter = mQuests.find (id); + TQuestContainer::const_iterator iter = mQuests.find(id); - if (iter==mQuests.end()) + if (iter == mQuests.end()) return 0; return iter->second.getIndex(); @@ -171,104 +184,105 @@ namespace MWDialogue int Journal::countSavedGameRecords() const { - int count = static_cast (mQuests.size()); + int count = static_cast(mQuests.size()); - for (TQuestIter iter (mQuests.begin()); iter!=mQuests.end(); ++iter) - count += std::distance (iter->second.begin(), iter->second.end()); + for (TQuestIter iter(mQuests.begin()); iter != mQuests.end(); ++iter) + count += std::distance(iter->second.begin(), iter->second.end()); - count += std::distance (mJournal.begin(), mJournal.end()); + count += std::distance(mJournal.begin(), mJournal.end()); - for (TTopicIter iter (mTopics.begin()); iter!=mTopics.end(); ++iter) - count += std::distance (iter->second.begin(), iter->second.end()); + for (TTopicIter iter(mTopics.begin()); iter != mTopics.end(); ++iter) + count += std::distance(iter->second.begin(), iter->second.end()); return count; } - void Journal::write (ESM::ESMWriter& writer, Loading::Listener& progress) const + void Journal::write(ESM::ESMWriter& writer, Loading::Listener& progress) const { - for (TQuestIter iter (mQuests.begin()); iter!=mQuests.end(); ++iter) + for (TQuestIter iter(mQuests.begin()); iter != mQuests.end(); ++iter) { const Quest& quest = iter->second; ESM::QuestState state; - quest.write (state); - writer.startRecord (ESM::REC_QUES); - state.save (writer); - writer.endRecord (ESM::REC_QUES); + quest.write(state); + writer.startRecord(ESM::REC_QUES); + state.save(writer); + writer.endRecord(ESM::REC_QUES); - for (Topic::TEntryIter entryIter (quest.begin()); entryIter!=quest.end(); ++entryIter) + for (Topic::TEntryIter entryIter(quest.begin()); entryIter != quest.end(); ++entryIter) { ESM::JournalEntry entry; entry.mType = ESM::JournalEntry::Type_Quest; entry.mTopic = quest.getTopic(); - entryIter->write (entry); - writer.startRecord (ESM::REC_JOUR); - entry.save (writer); - writer.endRecord (ESM::REC_JOUR); + entryIter->write(entry); + writer.startRecord(ESM::REC_JOUR); + entry.save(writer); + writer.endRecord(ESM::REC_JOUR); } } - for (TEntryIter iter (mJournal.begin()); iter!=mJournal.end(); ++iter) + for (TEntryIter iter(mJournal.begin()); iter != mJournal.end(); ++iter) { ESM::JournalEntry entry; entry.mType = ESM::JournalEntry::Type_Journal; - iter->write (entry); - writer.startRecord (ESM::REC_JOUR); - entry.save (writer); - writer.endRecord (ESM::REC_JOUR); + iter->write(entry); + writer.startRecord(ESM::REC_JOUR); + entry.save(writer); + writer.endRecord(ESM::REC_JOUR); } - for (TTopicIter iter (mTopics.begin()); iter!=mTopics.end(); ++iter) + for (TTopicIter iter(mTopics.begin()); iter != mTopics.end(); ++iter) { const Topic& topic = iter->second; - for (Topic::TEntryIter entryIter (topic.begin()); entryIter!=topic.end(); ++entryIter) + for (Topic::TEntryIter entryIter(topic.begin()); entryIter != topic.end(); ++entryIter) { ESM::JournalEntry entry; entry.mType = ESM::JournalEntry::Type_Topic; entry.mTopic = topic.getTopic(); - entryIter->write (entry); - writer.startRecord (ESM::REC_JOUR); - entry.save (writer); - writer.endRecord (ESM::REC_JOUR); + entryIter->write(entry); + writer.startRecord(ESM::REC_JOUR); + entry.save(writer); + writer.endRecord(ESM::REC_JOUR); } } } - void Journal::readRecord (ESM::ESMReader& reader, uint32_t type) + void Journal::readRecord(ESM::ESMReader& reader, uint32_t type) { - if (type==ESM::REC_JOUR || type==ESM::REC_JOUR_LEGACY) + if (type == ESM::REC_JOUR) { ESM::JournalEntry record; - record.load (reader); + record.load(reader); - if (isThere (record.mTopic, record.mInfo)) + if (isThere(record.mTopic, record.mInfo)) switch (record.mType) { case ESM::JournalEntry::Type_Quest: - getQuest (record.mTopic).insertEntry (record); + getOrStartQuest(record.mTopic).insertEntry(record); break; case ESM::JournalEntry::Type_Journal: - mJournal.push_back (record); + mJournal.push_back(record); break; case ESM::JournalEntry::Type_Topic: - getTopic (record.mTopic).insertEntry (record); + getTopic(record.mTopic).insertEntry(record); break; } } - else if (type==ESM::REC_QUES) + else if (type == ESM::REC_QUES) { ESM::QuestState record; - record.load (reader); + record.load(reader); - if (isThere (record.mTopic)) + if (isThere(record.mTopic)) { - std::pair result = mQuests.insert (std::make_pair (record.mTopic, record)); + std::pair result + = mQuests.insert(std::make_pair(record.mTopic, record)); // reapply quest index, this is to handle users upgrading from only // Morrowind.esm (no quest states) to Morrowind.esm + Tribunal.esm result.first->second.setIndex(record.mState); diff --git a/apps/openmw/mwdialogue/journalimp.hpp b/apps/openmw/mwdialogue/journalimp.hpp index 0c778d2ba22..b30c469cb8b 100644 --- a/apps/openmw/mwdialogue/journalimp.hpp +++ b/apps/openmw/mwdialogue/journalimp.hpp @@ -10,68 +10,70 @@ namespace MWDialogue /// \brief The player's journal class Journal : public MWBase::Journal { - TEntryContainer mJournal; - TQuestContainer mQuests; - TTopicContainer mTopics; + TEntryContainer mJournal; + TQuestContainer mQuests; + TTopicContainer mTopics; - private: + private: + Topic& getTopic(const ESM::RefId& id); - Quest& getQuest (const std::string& id); + bool isThere(const ESM::RefId& topicId, const ESM::RefId& infoId = ESM::RefId()) const; - Topic& getTopic (const std::string& id); + public: + Journal(); - bool isThere (const std::string& topicId, const std::string& infoId = "") const; + void clear() override; - public: + Quest* getQuestOrNull(const ESM::RefId& id) override; + ///< Gets a pointer to the requested quest. Will return nullptr if the quest has not been started. - Journal(); + Quest& getOrStartQuest(const ESM::RefId& id) override; + ///< Gets the quest requested. Attempts to create it and inserts it in quests if it is not yet started. - void clear() override; + void addEntry(const ESM::RefId& id, int index, const MWWorld::Ptr& actor) override; + ///< Add a journal entry. + /// @param actor Used as context for replacing of escape sequences (%name, etc). - void addEntry (const std::string& id, int index, const MWWorld::Ptr& actor) override; - ///< Add a journal entry. - /// @param actor Used as context for replacing of escape sequences (%name, etc). + void setJournalIndex(const ESM::RefId& id, int index) override; + ///< Set the journal index without adding an entry. - void setJournalIndex (const std::string& id, int index) override; - ///< Set the journal index without adding an entry. + int getJournalIndex(const ESM::RefId& id) const override; + ///< Get the journal index. - int getJournalIndex (const std::string& id) const override; - ///< Get the journal index. + void addTopic(const ESM::RefId& topicId, const ESM::RefId& infoId, const MWWorld::Ptr& actor) override; + /// \note topicId must be lowercase - void addTopic (const std::string& topicId, const std::string& infoId, const MWWorld::Ptr& actor) override; - /// \note topicId must be lowercase + void removeLastAddedTopicResponse(const ESM::RefId& topicId, std::string_view actorName) override; + ///< Removes the last topic response added for the given topicId and actor name. + /// \note topicId must be lowercase - void removeLastAddedTopicResponse (const std::string& topicId, const std::string& actorName) override; - ///< Removes the last topic response added for the given topicId and actor name. - /// \note topicId must be lowercase + TEntryIter begin() const override; + ///< Iterator pointing to the begin of the main journal. + /// + /// \note Iterators to main journal entries will never become invalid. - TEntryIter begin() const override; - ///< Iterator pointing to the begin of the main journal. - /// - /// \note Iterators to main journal entries will never become invalid. + TEntryIter end() const override; + ///< Iterator pointing past the end of the main journal. - TEntryIter end() const override; - ///< Iterator pointing past the end of the main journal. + TQuestIter questBegin() const override; + ///< Iterator pointing to the first quest (sorted by topic ID) - TQuestIter questBegin() const override; - ///< Iterator pointing to the first quest (sorted by topic ID) + TQuestIter questEnd() const override; + ///< Iterator pointing past the last quest. - TQuestIter questEnd() const override; - ///< Iterator pointing past the last quest. + TTopicIter topicBegin() const override; + ///< Iterator pointing to the first topic (sorted by topic ID) + /// + /// \note The topic ID is identical with the user-visible topic string. - TTopicIter topicBegin() const override; - ///< Iterator pointing to the first topic (sorted by topic ID) - /// - /// \note The topic ID is identical with the user-visible topic string. + TTopicIter topicEnd() const override; + ///< Iterator pointing past the last topic. - TTopicIter topicEnd() const override; - ///< Iterator pointing past the last topic. + int countSavedGameRecords() const override; - int countSavedGameRecords() const override; + void write(ESM::ESMWriter& writer, Loading::Listener& progress) const override; - void write (ESM::ESMWriter& writer, Loading::Listener& progress) const override; - - void readRecord (ESM::ESMReader& reader, uint32_t type) override; + void readRecord(ESM::ESMReader& reader, uint32_t type) override; }; } diff --git a/apps/openmw/mwdialogue/keywordsearch.cpp b/apps/openmw/mwdialogue/keywordsearch.cpp deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apps/openmw/mwdialogue/keywordsearch.hpp b/apps/openmw/mwdialogue/keywordsearch.hpp index f296f223fb3..2c98eac2182 100644 --- a/apps/openmw/mwdialogue/keywordsearch.hpp +++ b/apps/openmw/mwdialogue/keywordsearch.hpp @@ -1,255 +1,240 @@ #ifndef GAME_MWDIALOGUE_KEYWORDSEARCH_H #define GAME_MWDIALOGUE_KEYWORDSEARCH_H -#include +#include // std::reverse #include +#include #include #include -#include // std::reverse -#include +#include +#include namespace MWDialogue { -template -class KeywordSearch -{ -public: - - typedef typename string_t::const_iterator Point; - - struct Match + template + class KeywordSearch { - Point mBeg; - Point mEnd; - value_t mValue; - }; - - void seed (string_t keyword, value_t value) - { - if (keyword.empty()) - return; - seed_impl (/*std::move*/ (keyword), /*std::move*/ (value), 0, mRoot); - } + public: + using Point = std::string::const_iterator; - void clear () - { - mRoot.mChildren.clear (); - mRoot.mKeyword.clear (); - } + struct Match + { + Point mBeg; + Point mEnd; + value_t mValue; + }; - bool containsKeyword (string_t keyword, value_t& value) - { - typename Entry::childen_t::iterator current; - typename Entry::childen_t::iterator next; + void seed(std::string_view keyword, value_t value) + { + if (keyword.empty()) + return; + seed_impl(keyword, std::move(value), 0, mRoot); + } - current = mRoot.mChildren.find (Misc::StringUtils::toLower (*keyword.begin())); - if (current == mRoot.mChildren.end()) - return false; - else if (current->second.mKeyword.size() && Misc::StringUtils::ciEqual(current->second.mKeyword, keyword)) + void clear() { - value = current->second.mValue; - return true; + mRoot.mChildren.clear(); + mRoot.mKeyword.clear(); } - for (Point i = ++keyword.begin(); i != keyword.end(); ++i) + bool containsKeyword(std::string_view keyword, value_t& value) { - next = current->second.mChildren.find(Misc::StringUtils::toLower (*i)); - if (next == current->second.mChildren.end()) + auto it = keyword.begin(); + auto current = mRoot.mChildren.find(Misc::StringUtils::toLower(*it)); + if (current == mRoot.mChildren.end()) return false; - if (Misc::StringUtils::ciEqual(next->second.mKeyword, keyword)) + else if (Misc::StringUtils::ciEqual(current->second.mKeyword, keyword)) { - value = next->second.mValue; + value = current->second.mValue; return true; } - current = next; + + for (++it; it != keyword.end(); ++it) + { + auto next = current->second.mChildren.find(Misc::StringUtils::toLower(*it)); + if (next == current->second.mChildren.end()) + return false; + if (Misc::StringUtils::ciEqual(next->second.mKeyword, keyword)) + { + value = next->second.mValue; + return true; + } + current = next; + } + return false; } - return false; - } - static bool sortMatches(const Match& left, const Match& right) - { - return left.mBeg < right.mBeg; - } + static bool sortMatches(const Match& left, const Match& right) { return left.mBeg < right.mBeg; } - void highlightKeywords (Point beg, Point end, std::vector& out) - { - std::vector matches; - for (Point i = beg; i != end; ++i) + void highlightKeywords(Point beg, Point end, std::vector& out) const { - // check if previous character marked start of new word - if (i != beg) + std::vector matches; + for (Point i = beg; i != end; ++i) { - Point prev = i; - --prev; - if(isalpha(*prev)) - continue; - } + // check first character + typename Entry::childen_t::const_iterator candidate + = mRoot.mChildren.find(Misc::StringUtils::toLower(*i)); + // no match, on to next character + if (candidate == mRoot.mChildren.end()) + continue; - // check first character - typename Entry::childen_t::iterator candidate = mRoot.mChildren.find (Misc::StringUtils::toLower (*i)); + // see how far the match goes + Point j = i; - // no match, on to next character - if (candidate == mRoot.mChildren.end ()) - continue; + // 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; - // see how far the match goes - Point j = i; + while ((j + 1) != end) + { + typename Entry::childen_t::const_iterator next + = candidate->second.mChildren.find(Misc::StringUtils::toLower(*++j)); - // 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< typename std::pair > candidates; + if (next == candidate->second.mChildren.end()) + { + if (candidate->second.mKeyword.size() > 0) + candidates.push_back(std::make_pair((j - i), candidate)); + break; + } - while ((j + 1) != end) - { - typename Entry::childen_t::iterator next = candidate->second.mChildren.find (Misc::StringUtils::toLower (*++j)); + candidate = next; - if (next == candidate->second.mChildren.end ()) - { if (candidate->second.mKeyword.size() > 0) - candidates.push_back(std::make_pair((j-i), candidate)); - break; + candidates.push_back(std::make_pair((j - i), candidate)); } - candidate = next; + if (candidates.empty()) + continue; // didn't match enough to disambiguate, on to next character - if (candidate->second.mKeyword.size() > 0) - candidates.push_back(std::make_pair((j-i), candidate)); - } - - if (candidates.empty()) - continue; // didn't match enough to disambiguate, on to next character + // shorter candidates will be added to the vector first. however, we want to check against longer + // candidates first + std::reverse(candidates.begin(), candidates.end()); - // shorter candidates will be added to the vector first. however, we want to check against longer candidates first - std::reverse(candidates.begin(), candidates.end()); - - for (typename std::vector< std::pair >::iterator it = candidates.begin(); - it != candidates.end(); ++it) - { - candidate = it->second; - // try to match the rest of the keyword - Point k = i + it->first; - typename string_t::const_iterator t = candidate->second.mKeyword.begin () + (k - i); - - - while (k != end && t != candidate->second.mKeyword.end ()) + for (const auto& [pos, c] : candidates) { - if (Misc::StringUtils::toLower (*k) != Misc::StringUtils::toLower (*t)) - break; - - ++k, ++t; + candidate = c; + // try to match the rest of the keyword + Point k = i + pos; + Point t = candidate->second.mKeyword.begin() + (k - i); + + while (k != end && t != candidate->second.mKeyword.end()) + { + if (Misc::StringUtils::toLower(*k) != Misc::StringUtils::toLower(*t)) + break; + + ++k, ++t; + } + + // didn't match full keyword, try the next candidate + if (t != candidate->second.mKeyword.end()) + continue; + + // found a keyword, but there might still be longer keywords that start somewhere _within_ this + // keyword we will resolve these overlapping keywords later, choosing the longest one in case of + // conflict + Match match; + match.mValue = candidate->second.mValue; + match.mBeg = i; + match.mEnd = k; + matches.push_back(match); + break; } - - // didn't match full keyword, try the next candidate - if (t != candidate->second.mKeyword.end ()) - continue; - - // found a keyword, but there might still be longer keywords that start somewhere _within_ this keyword - // we will resolve these overlapping keywords later, choosing the longest one in case of conflict - Match match; - match.mValue = candidate->second.mValue; - match.mBeg = i; - match.mEnd = k; - matches.push_back(match); - break; } - } - // resolve overlapping keywords - while (!matches.empty()) - { - int longestKeywordSize = 0; - typename std::vector::iterator longestKeyword = matches.begin(); - for (typename std::vector::iterator it = matches.begin(); it != matches.end(); ++it) + // resolve overlapping keywords + while (!matches.empty()) { - int size = it->mEnd - it->mBeg; - if (size > longestKeywordSize) + std::size_t longestKeywordSize = 0; + typename std::vector::iterator longestKeyword = matches.begin(); + for (typename std::vector::iterator it = matches.begin(); it != matches.end(); ++it) { - longestKeywordSize = size; - longestKeyword = it; - } + std::size_t size = it->mEnd - it->mBeg; + if (size > longestKeywordSize) + { + longestKeywordSize = size; + longestKeyword = it; + } - typename std::vector::iterator next = it; - ++next; + typename std::vector::iterator next = it; + ++next; - if (next == matches.end()) - break; + if (next == matches.end()) + break; - if (it->mEnd <= next->mBeg) + if (it->mEnd <= next->mBeg) + { + break; // no overlap + } + } + + Match keyword = *longestKeyword; + matches.erase(longestKeyword); + out.push_back(keyword); + // erase anything that overlaps with the keyword we just added to the output + for (typename std::vector::iterator it = matches.begin(); it != matches.end();) { - break; // no overlap + if (it->mBeg < keyword.mEnd && it->mEnd > keyword.mBeg) + it = matches.erase(it); + else + ++it; } } - Match keyword = *longestKeyword; - matches.erase(longestKeyword); - out.push_back(keyword); - // erase anything that overlaps with the keyword we just added to the output - for (typename std::vector::iterator it = matches.begin(); it != matches.end();) - { - if (it->mBeg < keyword.mEnd && it->mEnd > keyword.mBeg) - it = matches.erase(it); - else - ++it; - } + std::sort(out.begin(), out.end(), sortMatches); } - std::sort(out.begin(), out.end(), sortMatches); - } - -private: - - struct Entry - { - typedef std::map childen_t; + private: + struct Entry + { + typedef std::map childen_t; - string_t mKeyword; - value_t mValue; - childen_t mChildren; - }; + std::string mKeyword; + value_t mValue; + childen_t mChildren; + }; - void seed_impl (string_t keyword, value_t value, size_t depth, Entry & entry) - { - int ch = Misc::StringUtils::toLower (keyword.at (depth)); + void seed_impl(std::string_view keyword, value_t value, size_t depth, Entry& entry) + { + auto ch = Misc::StringUtils::toLower(keyword.at(depth)); - typename Entry::childen_t::iterator j = entry.mChildren.find (ch); + typename Entry::childen_t::iterator j = entry.mChildren.find(ch); - if (j == entry.mChildren.end ()) - { - entry.mChildren [ch].mValue = /*std::move*/ (value); - entry.mChildren [ch].mKeyword = /*std::move*/ (keyword); - } - else - { - if (j->second.mKeyword.size () > 0) + if (j == entry.mChildren.end()) + { + entry.mChildren[ch].mValue = std::move(value); + entry.mChildren[ch].mKeyword = keyword; + } + else { - if (keyword == j->second.mKeyword) - throw std::runtime_error ("duplicate keyword inserted"); + if (j->second.mKeyword.size() > 0) + { + if (keyword == j->second.mKeyword) + throw std::runtime_error("duplicate keyword inserted"); - value_t pushValue = /*std::move*/ (j->second.mValue); - string_t pushKeyword = /*std::move*/ (j->second.mKeyword); + const auto& pushKeyword = j->second.mKeyword; - if (depth >= pushKeyword.size ()) - throw std::runtime_error ("unexpected"); + if (depth >= pushKeyword.size()) + throw std::runtime_error("unexpected"); - if (depth+1 < pushKeyword.size()) - { - seed_impl (/*std::move*/ (pushKeyword), /*std::move*/ (pushValue), depth+1, j->second); - j->second.mKeyword.clear (); + if (depth + 1 < pushKeyword.size()) + { + seed_impl(pushKeyword, j->second.mValue, depth + 1, j->second); + j->second.mKeyword.clear(); + } } + if (depth + 1 == keyword.size()) + j->second.mKeyword = value; + else // depth+1 < keyword.size() + seed_impl(keyword, std::move(value), depth + 1, j->second); } - if (depth+1 == keyword.size()) - j->second.mKeyword = value; - else // depth+1 < keyword.size() - seed_impl (/*std::move*/ (keyword), /*std::move*/ (value), depth+1, j->second); } - } - - Entry mRoot; -}; + Entry mRoot; + }; } diff --git a/apps/openmw/mwdialogue/quest.cpp b/apps/openmw/mwdialogue/quest.cpp index 5f20a8abb2c..68d7fdc627c 100644 --- a/apps/openmw/mwdialogue/quest.cpp +++ b/apps/openmw/mwdialogue/quest.cpp @@ -1,37 +1,47 @@ #include "quest.hpp" -#include +#include +#include + +#include "../mwbase/luamanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" namespace MWDialogue { Quest::Quest() - : Topic(), mIndex (0), mFinished (false) - {} + : Topic() + , mIndex(0) + , mFinished(false) + { + } - Quest::Quest (const std::string& topic) - : Topic (topic), mIndex (0), mFinished (false) - {} + Quest::Quest(const ESM::RefId& topic) + : Topic(topic) + , mIndex(0) + , mFinished(false) + { + } - Quest::Quest (const ESM::QuestState& state) - : Topic (state.mTopic), mIndex (state.mState), mFinished (state.mFinished!=0) - {} + Quest::Quest(const ESM::QuestState& state) + : Topic(state.mTopic) + , mIndex(state.mState) + , mFinished(state.mFinished != 0) + { + } - std::string Quest::getName() const + std::string_view Quest::getName() const { - const ESM::Dialogue *dialogue = - MWBase::Environment::get().getWorld()->getStore().get().find (mTopic); + const ESM::Dialogue* dialogue = MWBase::Environment::get().getESMStore()->get().find(mTopic); - for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); - iter!=dialogue->mInfo.end(); ++iter) - if (iter->mQuestStatus==ESM::DialInfo::QS_Name) + for (ESM::Dialogue::InfoContainer::const_iterator iter(dialogue->mInfo.begin()); iter != dialogue->mInfo.end(); + ++iter) + if (iter->mQuestStatus == ESM::DialInfo::QS_Name) return iter->mResponse; - return ""; + return {}; } int Quest::getIndex() const @@ -39,9 +49,10 @@ namespace MWDialogue return mIndex; } - void Quest::setIndex (int index) + void Quest::setIndex(int index) { // The index must be set even if no related journal entry was found + MWBase::Environment::get().getLuaManager()->questUpdated(mTopic, index); mIndex = index; } @@ -50,45 +61,40 @@ namespace MWDialogue return mFinished; } - void Quest::addEntry (const JournalEntry& entry) + void Quest::setFinished(bool finished) { - int index = -1; + mFinished = finished; + } - const ESM::Dialogue *dialogue = - MWBase::Environment::get().getWorld()->getStore().get().find (entry.mTopic); + bool Quest::addEntry(const JournalEntry& entry) + { + const ESM::Dialogue* dialogue + = MWBase::Environment::get().getESMStore()->get().find(entry.mTopic); - for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); - iter!=dialogue->mInfo.end(); ++iter) - if (iter->mId == entry.mInfoId) - { - index = iter->mData.mJournalIndex; - break; - } + auto info = std::find_if(dialogue->mInfo.begin(), dialogue->mInfo.end(), + [&](const auto& info) { return info.mId == entry.mInfoId; }); - if (index==-1) - throw std::runtime_error ("unknown journal entry for topic " + mTopic); + if (info == dialogue->mInfo.end() || info->mData.mJournalIndex == -1) + throw std::runtime_error("unknown journal entry for topic " + mTopic.toDebugString()); - for (auto &info : dialogue->mInfo) + if (info->mQuestStatus == ESM::DialInfo::QS_Finished || info->mQuestStatus == ESM::DialInfo::QS_Restart) + mFinished = info->mQuestStatus == ESM::DialInfo::QS_Finished; + + if (info->mData.mJournalIndex > mIndex) { - if (info.mData.mJournalIndex == index - && (info.mQuestStatus == ESM::DialInfo::QS_Finished || info.mQuestStatus == ESM::DialInfo::QS_Restart)) - { - mFinished = (info.mQuestStatus == ESM::DialInfo::QS_Finished); - break; - } + mIndex = info->mData.mJournalIndex; + MWBase::Environment::get().getLuaManager()->questUpdated(mTopic, mIndex); } - if (index > mIndex) - mIndex = index; - - for (TEntryIter iter (mEntries.begin()); iter!=mEntries.end(); ++iter) - if (iter->mInfoId==entry.mInfoId) - return; + for (TEntryIter iter(mEntries.begin()); iter != mEntries.end(); ++iter) + if (iter->mInfoId == entry.mInfoId) + return info->mQuestStatus == ESM::DialInfo::QS_Restart; - mEntries.push_back (entry); // we want slicing here + mEntries.push_back(entry); // we want slicing here + return info->mQuestStatus == ESM::DialInfo::QS_Restart; } - void Quest::write (ESM::QuestState& state) const + void Quest::write(ESM::QuestState& state) const { state.mTopic = getTopic(); state.mState = mIndex; diff --git a/apps/openmw/mwdialogue/quest.hpp b/apps/openmw/mwdialogue/quest.hpp index 712f94fae4c..1976c467920 100644 --- a/apps/openmw/mwdialogue/quest.hpp +++ b/apps/openmw/mwdialogue/quest.hpp @@ -13,33 +13,33 @@ namespace MWDialogue /// \brief A quest in progress or a completed quest class Quest : public Topic { - int mIndex; - bool mFinished; + int mIndex; + bool mFinished; - public: + public: + Quest(); - Quest(); + Quest(const ESM::RefId& topic); - Quest (const std::string& topic); + Quest(const ESM::QuestState& state); - Quest (const ESM::QuestState& state); + std::string_view getName() const override; + ///< May be an empty string - std::string getName() const override; - ///< May be an empty string + int getIndex() const; - int getIndex() const; + void setIndex(int index); + ///< Calling this function with a non-existent index will throw an exception. - void setIndex (int index); - ///< Calling this function with a non-existent index will throw an exception. + bool isFinished() const; + void setFinished(bool finished); - bool isFinished() const; + bool addEntry(const JournalEntry& entry) override; + ///< Add entry and adjust index accordingly. Returns true if the quest should be restarted. + /// + /// \note Redundant entries are ignored, but the index is still adjusted. - void addEntry (const JournalEntry& entry) override; - ///< Add entry and adjust index accordingly. - /// - /// \note Redundant entries are ignored, but the index is still adjusted. - - void write (ESM::QuestState& state) const; + void write(ESM::QuestState& state) const; }; } diff --git a/apps/openmw/mwdialogue/scripttest.cpp b/apps/openmw/mwdialogue/scripttest.cpp index c3d7b12c7d5..6ca48d94903 100644 --- a/apps/openmw/mwdialogue/scripttest.cpp +++ b/apps/openmw/mwdialogue/scripttest.cpp @@ -1,122 +1,171 @@ #include "scripttest.hpp" -#include "../mwworld/manualref.hpp" -#include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/manualref.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwscript/compilercontext.hpp" -#include #include -#include -#include #include +#include #include +#include +#include +#include +#include #include "filter.hpp" -namespace -{ +#include +#include +#include -void test(const MWWorld::Ptr& actor, int &compiled, int &total, const Compiler::Extensions* extensions, int warningsMode) +namespace { - MWDialogue::Filter filter(actor, 0, false); - - MWScript::CompilerContext compilerContext(MWScript::CompilerContext::Type_Dialogue); - compilerContext.setExtensions(extensions); - Compiler::StreamErrorHandler errorHandler; - errorHandler.setWarningsMode (warningsMode); - const MWWorld::Store& dialogues = MWBase::Environment::get().getWorld()->getStore().get(); - for (MWWorld::Store::iterator it = dialogues.begin(); it != dialogues.end(); ++it) + bool test(const MWWorld::Ptr& actor, const ESM::DialInfo& info, int& compiled, int& total, + const Compiler::Extensions* extensions, const MWScript::CompilerContext& context, + Compiler::StreamErrorHandler& errorHandler) { - std::vector infos = filter.listAll(*it); - - for (std::vector::iterator iter = infos.begin(); iter != infos.end(); ++iter) + bool success = true; + ++total; + try { - const ESM::DialInfo* info = *iter; - if (!info->mResultScript.empty()) - { - bool success = true; - ++total; - try - { - errorHandler.reset(); + std::istringstream input(info.mResultScript + "\n"); - std::istringstream input (info->mResultScript + "\n"); + Compiler::Scanner scanner(errorHandler, input, extensions); - Compiler::Scanner scanner (errorHandler, input, extensions); + Compiler::Locals locals; - Compiler::Locals locals; - - std::string actorScript = actor.getClass().getScript(actor); + const ESM::RefId& actorScript = actor.getClass().getScript(actor); + if (!actorScript.empty()) + { + // grab local variables from actor's script, if available. + locals = MWBase::Environment::get().getScriptManager()->getLocals(actorScript); + } - if (!actorScript.empty()) - { - // grab local variables from actor's script, if available. - locals = MWBase::Environment::get().getScriptManager()->getLocals (actorScript); - } + Compiler::ScriptParser parser(errorHandler, context, locals, false); - Compiler::ScriptParser parser(errorHandler, compilerContext, locals, false); + scanner.scan(parser); - scanner.scan (parser); + if (!errorHandler.isGood()) + success = false; - if (!errorHandler.isGood()) - success = false; + ++compiled; + } + catch (const Compiler::SourceException&) + { + // error has already been reported via error handler + success = false; + } + catch (const std::exception& error) + { + Log(Debug::Error) << "Dialogue error: An exception has been thrown: " << error.what(); + success = false; + } - ++compiled; - } - catch (const Compiler::SourceException& /* error */) - { - // error has already been reported via error handler - success = false; - } - catch (const std::exception& error) - { - Log(Debug::Error) << std::string ("Dialogue error: An exception has been thrown: ") + error.what(); - success = false; - } + return success; + } - if (!success) - { - Log(Debug::Error) << "Error: compiling failed (dialogue script): \n" << info->mResultScript << "\n"; - } - } - } + bool superficiallyMatches(const MWWorld::Ptr& ptr, const ESM::DialInfo& info) + { + if (ptr.isEmpty()) + return false; + MWDialogue::Filter filter(ptr, 0, false); + return filter.couldPotentiallyMatch(info); } -} } namespace MWDialogue { -namespace ScriptTest -{ - - std::pair compileAll(const Compiler::Extensions *extensions, int warningsMode) + namespace ScriptTest { - int compiled = 0, total = 0; - const MWWorld::Store& npcs = MWBase::Environment::get().getWorld()->getStore().get(); - for (MWWorld::Store::iterator it = npcs.begin(); it != npcs.end(); ++it) - { - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), it->mId); - test(ref.getPtr(), compiled, total, extensions, warningsMode); - } - const MWWorld::Store& creatures = MWBase::Environment::get().getWorld()->getStore().get(); - for (MWWorld::Store::iterator it = creatures.begin(); it != creatures.end(); ++it) + std::pair compileAll(const Compiler::Extensions* extensions, int warningsMode) { - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), it->mId); - test(ref.getPtr(), compiled, total, extensions, warningsMode); + int compiled = 0, total = 0; + const auto& store = *MWBase::Environment::get().getESMStore(); + + MWScript::CompilerContext compilerContext(MWScript::CompilerContext::Type_Dialogue); + compilerContext.setExtensions(extensions); + Compiler::StreamErrorHandler errorHandler; + errorHandler.setWarningsMode(warningsMode); + + std::unique_ptr ref; + for (const ESM::Dialogue& topic : store.get()) + { + MWWorld::Ptr ptr; + for (const ESM::DialInfo& info : topic.mInfo) + { + if (info.mResultScript.empty()) + continue; + if (!info.mActor.empty()) + { + // We know it can only ever be this actor + try + { + ref = std::make_unique(store, info.mActor); + ptr = ref->getPtr(); + } + catch (const std::logic_error&) + { + // Too bad it doesn't exist + ptr = {}; + } + } + else + { + // Try to find a matching actor + if (!superficiallyMatches(ptr, info)) + { + ptr = {}; + bool found = false; + for (const auto& npc : store.get()) + { + ref = std::make_unique(store, npc.mId); + if (superficiallyMatches(ref->getPtr(), info)) + { + ptr = ref->getPtr(); + found = true; + break; + } + } + if (!found) + { + for (const auto& creature : store.get()) + { + ref = std::make_unique(store, creature.mId); + if (superficiallyMatches(ref->getPtr(), info)) + { + ptr = ref->getPtr(); + break; + } + } + } + } + } + if (ptr.isEmpty()) + Log(Debug::Error) << "Could not find an actor to test " << info.mId << " in " << topic.mId; + else + { + errorHandler.reset(); + errorHandler.setContext(info.mId.getRefIdString() + " in " + topic.mStringId); + if (!test(ptr, info, compiled, total, extensions, compilerContext, errorHandler)) + Log(Debug::Error) << "Test failed for " << info.mId << " in " << topic.mId << '\n' + << info.mResultScript; + } + } + } + + return std::make_pair(total, compiled); } - return std::make_pair(total, compiled); - } -} + } } diff --git a/apps/openmw/mwdialogue/scripttest.hpp b/apps/openmw/mwdialogue/scripttest.hpp index 0ac2597256e..4b770ae12c6 100644 --- a/apps/openmw/mwdialogue/scripttest.hpp +++ b/apps/openmw/mwdialogue/scripttest.hpp @@ -6,14 +6,14 @@ namespace MWDialogue { -namespace ScriptTest -{ + namespace ScriptTest + { -/// Attempt to compile all dialogue scripts, use for verification purposes -/// @return A pair containing -std::pair compileAll(const Compiler::Extensions* extensions, int warningsMode); + /// Attempt to compile all dialogue scripts, use for verification purposes + /// @return A pair containing + std::pair compileAll(const Compiler::Extensions* extensions, int warningsMode); -} + } } diff --git a/apps/openmw/mwdialogue/selectwrapper.cpp b/apps/openmw/mwdialogue/selectwrapper.cpp index 28dbd214ba1..02c9d29b595 100644 --- a/apps/openmw/mwdialogue/selectwrapper.cpp +++ b/apps/openmw/mwdialogue/selectwrapper.cpp @@ -1,297 +1,301 @@ #include "selectwrapper.hpp" -#include -#include #include +#include +#include -#include +#include +#include namespace { - template - bool selectCompareImp (char comp, T1 value1, T2 value2) + template + bool selectCompareImp(ESM::DialogueCondition::Comparison comp, T1 value1, T2 value2) { switch (comp) { - case '0': return value1==value2; - case '1': return value1!=value2; - case '2': return value1>value2; - case '3': return value1>=value2; - case '4': return value1 value2; + case ESM::DialogueCondition::Comp_Ge: + return value1 >= value2; + case ESM::DialogueCondition::Comp_Ls: + return value1 < value2; + case ESM::DialogueCondition::Comp_Le: + return value1 <= value2; + default: + throw std::runtime_error("unknown compare type in dialogue info select"); } - - throw std::runtime_error ("unknown compare type in dialogue info select"); } - template - bool selectCompareImp (const ESM::DialInfo::SelectStruct& select, T value1) + template + bool selectCompareImp(const ESM::DialogueCondition& select, T value1) { - if (select.mValue.getType()==ESM::VT_Int) - { - return selectCompareImp (select.mSelectRule[4], value1, select.mValue.getInteger()); - } - else if (select.mValue.getType()==ESM::VT_Float) - { - return selectCompareImp (select.mSelectRule[4], value1, select.mValue.getFloat()); - } - else - throw std::runtime_error ( - "unsupported variable type in dialogue info select"); + return std::visit( + [&](auto value) { return selectCompareImp(select.mComparison, value1, value); }, select.mValue); } } -MWDialogue::SelectWrapper::Function MWDialogue::SelectWrapper::decodeFunction() const +MWDialogue::SelectWrapper::SelectWrapper(const ESM::DialogueCondition& select) + : mSelect(select) { - int index = 0; - - std::istringstream (mSelect.mSelectRule.substr(2,2)) >> index; - - switch (index) - { - case 0: return Function_RankLow; - case 1: return Function_RankHigh; - case 2: return Function_RankRequirement; - case 3: return Function_Reputation; - case 4: return Function_HealthPercent; - case 5: return Function_PCReputation; - case 6: return Function_PcLevel; - case 7: return Function_PcHealthPercent; - case 8: case 9: return Function_PcDynamicStat; - case 10: return Function_PcAttribute; - case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 19: case 20: - case 21: case 22: case 23: case 24: case 25: case 26: case 27: case 28: case 29: case 30: - case 31: case 32: case 33: case 34: case 35: case 36: case 37: return Function_PcSkill; - case 38: return Function_PcGender; - case 39: return Function_PcExpelled; - case 40: return Function_PcCommonDisease; - case 41: return Function_PcBlightDisease; - case 42: return Function_PcClothingModifier; - case 43: return Function_PcCrimeLevel; - case 44: return Function_SameGender; - case 45: return Function_SameRace; - case 46: return Function_SameFaction; - case 47: return Function_FactionRankDiff; - case 48: return Function_Detected; - case 49: return Function_Alarmed; - case 50: return Function_Choice; - case 51: case 52: case 53: case 54: case 55: case 56: case 57: return Function_PcAttribute; - case 58: return Function_PcCorprus; - case 59: return Function_Weather; - case 60: return Function_PcVampire; - case 61: return Function_Level; - case 62: return Function_Attacked; - case 63: return Function_TalkedToPc; - case 64: return Function_PcDynamicStat; - case 65: return Function_CreatureTargetted; - case 66: return Function_FriendlyHit; - case 67: case 68: case 69: case 70: return Function_AiSetting; - case 71: return Function_ShouldAttack; - case 72: return Function_Werewolf; - case 73: return Function_WerewolfKills; - } - - return Function_False; } -MWDialogue::SelectWrapper::SelectWrapper (const ESM::DialInfo::SelectStruct& select) : mSelect (select) {} - -MWDialogue::SelectWrapper::Function MWDialogue::SelectWrapper::getFunction() const +ESM::DialogueCondition::Function MWDialogue::SelectWrapper::getFunction() const { - char type = mSelect.mSelectRule[1]; - - switch (type) - { - case '1': return decodeFunction(); - case '2': return Function_Global; - case '3': return Function_Local; - case '4': return Function_Journal; - case '5': return Function_Item; - case '6': return Function_Dead; - case '7': return Function_NotId; - case '8': return Function_NotFaction; - case '9': return Function_NotClass; - case 'A': return Function_NotRace; - case 'B': return Function_NotCell; - case 'C': return Function_NotLocal; - } - - return Function_None; + return mSelect.mFunction; } int MWDialogue::SelectWrapper::getArgument() const { - if (mSelect.mSelectRule[1]!='1') - return 0; - - int index = 0; - - std::istringstream (mSelect.mSelectRule.substr(2,2)) >> index; - - switch (index) + switch (mSelect.mFunction) { // AI settings - case 67: return 1; - case 68: return 0; - case 69: return 3; - case 70: return 2; + case ESM::DialogueCondition::Function_Fight: + return 1; + case ESM::DialogueCondition::Function_Hello: + return 0; + case ESM::DialogueCondition::Function_Alarm: + return 3; + case ESM::DialogueCondition::Function_Flee: + return 2; // attributes - case 10: return 0; - case 51: return 1; - case 52: return 2; - case 53: return 3; - case 54: return 4; - case 55: return 5; - case 56: return 6; - case 57: return 7; + case ESM::DialogueCondition::Function_PcStrength: + return 0; + case ESM::DialogueCondition::Function_PcIntelligence: + return 1; + case ESM::DialogueCondition::Function_PcWillpower: + return 2; + case ESM::DialogueCondition::Function_PcAgility: + return 3; + case ESM::DialogueCondition::Function_PcSpeed: + return 4; + case ESM::DialogueCondition::Function_PcEndurance: + return 5; + case ESM::DialogueCondition::Function_PcPersonality: + return 6; + case ESM::DialogueCondition::Function_PcLuck: + return 7; // skills - case 11: return 0; - case 12: return 1; - case 13: return 2; - case 14: return 3; - case 15: return 4; - case 16: return 5; - case 17: return 6; - case 18: return 7; - case 19: return 8; - case 20: return 9; - case 21: return 10; - case 22: return 11; - case 23: return 12; - case 24: return 13; - case 25: return 14; - case 26: return 15; - case 27: return 16; - case 28: return 17; - case 29: return 18; - case 30: return 19; - case 31: return 20; - case 32: return 21; - case 33: return 22; - case 34: return 23; - case 35: return 24; - case 36: return 25; - case 37: return 26; + case ESM::DialogueCondition::Function_PcBlock: + return 0; + case ESM::DialogueCondition::Function_PcArmorer: + return 1; + case ESM::DialogueCondition::Function_PcMediumArmor: + return 2; + case ESM::DialogueCondition::Function_PcHeavyArmor: + return 3; + case ESM::DialogueCondition::Function_PcBluntWeapon: + return 4; + case ESM::DialogueCondition::Function_PcLongBlade: + return 5; + case ESM::DialogueCondition::Function_PcAxe: + return 6; + case ESM::DialogueCondition::Function_PcSpear: + return 7; + case ESM::DialogueCondition::Function_PcAthletics: + return 8; + case ESM::DialogueCondition::Function_PcEnchant: + return 9; + case ESM::DialogueCondition::Function_PcDestruction: + return 10; + case ESM::DialogueCondition::Function_PcAlteration: + return 11; + case ESM::DialogueCondition::Function_PcIllusion: + return 12; + case ESM::DialogueCondition::Function_PcConjuration: + return 13; + case ESM::DialogueCondition::Function_PcMysticism: + return 14; + case ESM::DialogueCondition::Function_PcRestoration: + return 15; + case ESM::DialogueCondition::Function_PcAlchemy: + return 16; + case ESM::DialogueCondition::Function_PcUnarmored: + return 17; + case ESM::DialogueCondition::Function_PcSecurity: + return 18; + case ESM::DialogueCondition::Function_PcSneak: + return 19; + case ESM::DialogueCondition::Function_PcAcrobatics: + return 20; + case ESM::DialogueCondition::Function_PcLightArmor: + return 21; + case ESM::DialogueCondition::Function_PcShortBlade: + return 22; + case ESM::DialogueCondition::Function_PcMarksman: + return 23; + case ESM::DialogueCondition::Function_PcMerchantile: + return 24; + case ESM::DialogueCondition::Function_PcSpeechcraft: + return 25; + case ESM::DialogueCondition::Function_PcHandToHand: + return 26; // dynamic stats - case 8: return 1; - case 9: return 2; - case 64: return 0; + case ESM::DialogueCondition::Function_PcMagicka: + return 1; + case ESM::DialogueCondition::Function_PcFatigue: + return 2; + case ESM::DialogueCondition::Function_PcHealth: + return 0; + default: + return 0; } - - return 0; } MWDialogue::SelectWrapper::Type MWDialogue::SelectWrapper::getType() const { - static const Function integerFunctions[] = - { - Function_Journal, Function_Item, Function_Dead, - Function_Choice, - Function_AiSetting, - Function_PcAttribute, Function_PcSkill, - Function_FriendlyHit, - Function_PcLevel, Function_PcGender, Function_PcClothingModifier, - Function_PcCrimeLevel, - Function_RankRequirement, - Function_Level, Function_PCReputation, - Function_Weather, - Function_Reputation, Function_FactionRankDiff, - Function_WerewolfKills, - Function_RankLow, Function_RankHigh, - Function_CreatureTargetted, - Function_None // end marker - }; - - static const Function numericFunctions[] = + switch (mSelect.mFunction) { - Function_Global, Function_Local, Function_NotLocal, - Function_PcDynamicStat, Function_PcHealthPercent, - Function_HealthPercent, - Function_None // end marker - }; - - static const Function booleanFunctions[] = - { - Function_False, - Function_SameGender, Function_SameRace, Function_SameFaction, - Function_PcCommonDisease, Function_PcBlightDisease, Function_PcCorprus, - Function_PcExpelled, - Function_PcVampire, Function_TalkedToPc, - Function_Alarmed, Function_Detected, - Function_Attacked, Function_ShouldAttack, - Function_Werewolf, - Function_None // end marker - }; - - static const Function invertedBooleanFunctions[] = - { - Function_NotId, Function_NotFaction, Function_NotClass, - Function_NotRace, Function_NotCell, - Function_None // end marker - }; - - Function function = getFunction(); - - for (int i=0; integerFunctions[i]!=Function_None; ++i) - if (integerFunctions[i]==function) + case ESM::DialogueCondition::Function_Journal: + case ESM::DialogueCondition::Function_Item: + case ESM::DialogueCondition::Function_Dead: + case ESM::DialogueCondition::Function_Choice: + case ESM::DialogueCondition::Function_Fight: + case ESM::DialogueCondition::Function_Hello: + case ESM::DialogueCondition::Function_Alarm: + case ESM::DialogueCondition::Function_Flee: + case ESM::DialogueCondition::Function_PcStrength: + case ESM::DialogueCondition::Function_PcIntelligence: + case ESM::DialogueCondition::Function_PcWillpower: + case ESM::DialogueCondition::Function_PcAgility: + case ESM::DialogueCondition::Function_PcSpeed: + case ESM::DialogueCondition::Function_PcEndurance: + case ESM::DialogueCondition::Function_PcPersonality: + case ESM::DialogueCondition::Function_PcLuck: + case ESM::DialogueCondition::Function_PcBlock: + case ESM::DialogueCondition::Function_PcArmorer: + case ESM::DialogueCondition::Function_PcMediumArmor: + case ESM::DialogueCondition::Function_PcHeavyArmor: + case ESM::DialogueCondition::Function_PcBluntWeapon: + case ESM::DialogueCondition::Function_PcLongBlade: + case ESM::DialogueCondition::Function_PcAxe: + case ESM::DialogueCondition::Function_PcSpear: + case ESM::DialogueCondition::Function_PcAthletics: + case ESM::DialogueCondition::Function_PcEnchant: + case ESM::DialogueCondition::Function_PcDestruction: + case ESM::DialogueCondition::Function_PcAlteration: + case ESM::DialogueCondition::Function_PcIllusion: + case ESM::DialogueCondition::Function_PcConjuration: + case ESM::DialogueCondition::Function_PcMysticism: + case ESM::DialogueCondition::Function_PcRestoration: + case ESM::DialogueCondition::Function_PcAlchemy: + case ESM::DialogueCondition::Function_PcUnarmored: + case ESM::DialogueCondition::Function_PcSecurity: + case ESM::DialogueCondition::Function_PcSneak: + case ESM::DialogueCondition::Function_PcAcrobatics: + case ESM::DialogueCondition::Function_PcLightArmor: + case ESM::DialogueCondition::Function_PcShortBlade: + case ESM::DialogueCondition::Function_PcMarksman: + case ESM::DialogueCondition::Function_PcMerchantile: + case ESM::DialogueCondition::Function_PcSpeechcraft: + case ESM::DialogueCondition::Function_PcHandToHand: + case ESM::DialogueCondition::Function_FriendHit: + case ESM::DialogueCondition::Function_PcLevel: + case ESM::DialogueCondition::Function_PcGender: + case ESM::DialogueCondition::Function_PcClothingModifier: + case ESM::DialogueCondition::Function_PcCrimeLevel: + case ESM::DialogueCondition::Function_RankRequirement: + case ESM::DialogueCondition::Function_Level: + case ESM::DialogueCondition::Function_PcReputation: + case ESM::DialogueCondition::Function_Weather: + case ESM::DialogueCondition::Function_Reputation: + case ESM::DialogueCondition::Function_FactionRankDifference: + case ESM::DialogueCondition::Function_PcWerewolfKills: + case ESM::DialogueCondition::Function_FacReactionLowest: + case ESM::DialogueCondition::Function_FacReactionHighest: + case ESM::DialogueCondition::Function_CreatureTarget: return Type_Integer; - - for (int i=0; numericFunctions[i]!=Function_None; ++i) - if (numericFunctions[i]==function) + case ESM::DialogueCondition::Function_Global: + case ESM::DialogueCondition::Function_Local: + case ESM::DialogueCondition::Function_NotLocal: + case ESM::DialogueCondition::Function_PcHealth: + case ESM::DialogueCondition::Function_PcMagicka: + case ESM::DialogueCondition::Function_PcFatigue: + case ESM::DialogueCondition::Function_PcHealthPercent: + case ESM::DialogueCondition::Function_Health_Percent: return Type_Numeric; - - for (int i=0; booleanFunctions[i]!=Function_None; ++i) - if (booleanFunctions[i]==function) + case ESM::DialogueCondition::Function_SameSex: + case ESM::DialogueCondition::Function_SameRace: + case ESM::DialogueCondition::Function_SameFaction: + case ESM::DialogueCondition::Function_PcCommonDisease: + case ESM::DialogueCondition::Function_PcBlightDisease: + case ESM::DialogueCondition::Function_PcCorprus: + case ESM::DialogueCondition::Function_PcExpelled: + case ESM::DialogueCondition::Function_PcVampire: + case ESM::DialogueCondition::Function_TalkedToPc: + case ESM::DialogueCondition::Function_Alarmed: + case ESM::DialogueCondition::Function_Detected: + case ESM::DialogueCondition::Function_Attacked: + case ESM::DialogueCondition::Function_ShouldAttack: + case ESM::DialogueCondition::Function_Werewolf: return Type_Boolean; - - for (int i=0; invertedBooleanFunctions[i]!=Function_None; ++i) - if (invertedBooleanFunctions[i]==function) + case ESM::DialogueCondition::Function_NotId: + case ESM::DialogueCondition::Function_NotFaction: + case ESM::DialogueCondition::Function_NotClass: + case ESM::DialogueCondition::Function_NotRace: + case ESM::DialogueCondition::Function_NotCell: return Type_Inverted; - - return Type_None; + default: + return Type_None; + }; } bool MWDialogue::SelectWrapper::isNpcOnly() const { - static const Function functions[] = + switch (mSelect.mFunction) { - Function_NotFaction, Function_NotClass, Function_NotRace, - Function_SameGender, Function_SameRace, Function_SameFaction, - Function_RankRequirement, - Function_Reputation, Function_FactionRankDiff, - Function_Werewolf, Function_WerewolfKills, - Function_RankLow, Function_RankHigh, - Function_None // end marker - }; - - Function function = getFunction(); - - for (int i=0; functions[i]!=Function_None; ++i) - if (functions[i]==function) + 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; - - return false; + default: + return false; + } } -bool MWDialogue::SelectWrapper::selectCompare (int value) const +bool MWDialogue::SelectWrapper::selectCompare(int value) const { - return selectCompareImp (mSelect, value); + return selectCompareImp(mSelect, value); } -bool MWDialogue::SelectWrapper::selectCompare (float value) const +bool MWDialogue::SelectWrapper::selectCompare(float value) const { - return selectCompareImp (mSelect, value); + return selectCompareImp(mSelect, value); } -bool MWDialogue::SelectWrapper::selectCompare (bool value) const +bool MWDialogue::SelectWrapper::selectCompare(bool value) const { - return selectCompareImp (mSelect, static_cast (value)); + return selectCompareImp(mSelect, static_cast(value)); } std::string MWDialogue::SelectWrapper::getName() const { - return Misc::StringUtils::lowerCase (mSelect.mSelectRule.substr (5)); + return Misc::StringUtils::lowerCase(mSelect.mVariable); +} + +std::string_view MWDialogue::SelectWrapper::getCellName() const +{ + return mSelect.mVariable; +} + +ESM::RefId MWDialogue::SelectWrapper::getId() const +{ + return ESM::RefId::stringRefId(mSelect.mVariable); } diff --git a/apps/openmw/mwdialogue/selectwrapper.hpp b/apps/openmw/mwdialogue/selectwrapper.hpp index ef787d8eecc..d831b6cea01 100644 --- a/apps/openmw/mwdialogue/selectwrapper.hpp +++ b/apps/openmw/mwdialogue/selectwrapper.hpp @@ -1,85 +1,48 @@ #ifndef GAME_MWDIALOGUE_SELECTWRAPPER_H #define GAME_MWDIALOGUE_SELECTWRAPPER_H -#include +#include namespace MWDialogue { class SelectWrapper { - const ESM::DialInfo::SelectStruct& mSelect; + const ESM::DialogueCondition& mSelect; - public: + public: + enum Type + { + Type_None, + Type_Integer, + Type_Numeric, + Type_Boolean, + Type_Inverted + }; - enum Function - { - Function_None, Function_False, - Function_Journal, - Function_Item, - Function_Dead, - Function_NotId, - Function_NotFaction, - Function_NotClass, - Function_NotRace, - Function_NotCell, - Function_NotLocal, - Function_Local, - Function_Global, - Function_SameGender, Function_SameRace, Function_SameFaction, - Function_Choice, - Function_PcCommonDisease, Function_PcBlightDisease, Function_PcCorprus, - Function_AiSetting, - Function_PcAttribute, Function_PcSkill, - Function_PcExpelled, - Function_PcVampire, - Function_FriendlyHit, - Function_TalkedToPc, - Function_PcLevel, Function_PcHealthPercent, Function_PcDynamicStat, - Function_PcGender, Function_PcClothingModifier, Function_PcCrimeLevel, - Function_RankRequirement, - Function_HealthPercent, Function_Level, Function_PCReputation, - Function_Weather, - Function_Reputation, Function_Alarmed, Function_FactionRankDiff, Function_Detected, - Function_Attacked, Function_ShouldAttack, - Function_CreatureTargetted, - Function_Werewolf, Function_WerewolfKills, - Function_RankLow, Function_RankHigh - }; + public: + SelectWrapper(const ESM::DialogueCondition& select); - enum Type - { - Type_None, - Type_Integer, - Type_Numeric, - Type_Boolean, - Type_Inverted - }; + ESM::DialogueCondition::Function getFunction() const; - private: + int getArgument() const; - Function decodeFunction() const; + Type getType() const; - public: + bool isNpcOnly() const; + ///< \attention Do not call any of the select functions for this select struct! - SelectWrapper (const ESM::DialInfo::SelectStruct& select); + bool selectCompare(int value) const; - Function getFunction() const; + bool selectCompare(float value) const; - int getArgument() const; + bool selectCompare(bool value) const; - Type getType() const; + std::string getName() const; + ///< Return case-smashed name. - bool isNpcOnly() const; - ///< \attention Do not call any of the select functions for this select struct! + std::string_view getCellName() const; - bool selectCompare (int value) const; - - bool selectCompare (float value) const; - - bool selectCompare (bool value) const; - - std::string getName() const; - ///< Return case-smashed name. + ESM::RefId getId() const; }; } diff --git a/apps/openmw/mwdialogue/topic.cpp b/apps/openmw/mwdialogue/topic.cpp index eb7fbdc1dec..356162da627 100644 --- a/apps/openmw/mwdialogue/topic.cpp +++ b/apps/openmw/mwdialogue/topic.cpp @@ -1,49 +1,48 @@ #include "topic.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" namespace MWDialogue { - Topic::Topic() - {} + Topic::Topic() {} - Topic::Topic (const std::string& topic) - : mTopic (topic), mName ( - MWBase::Environment::get().getWorld()->getStore().get().find (topic)->mId) - {} + Topic::Topic(const ESM::RefId& topic) + : mTopic(topic) + , mName(MWBase::Environment::get().getESMStore()->get().find(topic)->mStringId) + { + } - Topic::~Topic() - {} + Topic::~Topic() {} - void Topic::addEntry (const JournalEntry& entry) + bool Topic::addEntry(const JournalEntry& entry) { - if (entry.mTopic!=mTopic) - throw std::runtime_error ("topic does not match: " + mTopic); + if (entry.mTopic != mTopic) + throw std::runtime_error("topic does not match: " + mTopic.toDebugString()); // bail out if we already have heard this for (Topic::TEntryIter it = mEntries.begin(); it != mEntries.end(); ++it) { if (it->mInfoId == entry.mInfoId) - return; + return false; } - mEntries.push_back (entry); // we want slicing here + mEntries.push_back(entry); // we want slicing here + return false; } - void Topic::insertEntry (const ESM::JournalEntry& entry) + void Topic::insertEntry(const ESM::JournalEntry& entry) { - mEntries.push_back (entry); + mEntries.push_back(entry); } - std::string Topic::getTopic() const + const ESM::RefId& Topic::getTopic() const { return mTopic; } - std::string Topic::getName() const + std::string_view Topic::getName() const { return mName; } @@ -58,14 +57,13 @@ namespace MWDialogue return mEntries.end(); } - void Topic::removeLastAddedResponse (const std::string& actorName) + void Topic::removeLastAddedResponse(std::string_view actorName) { - for (std::vector::reverse_iterator it = mEntries.rbegin(); - it != mEntries.rend(); ++it) + for (std::vector::reverse_iterator it = mEntries.rbegin(); it != mEntries.rend(); ++it) { if (it->mActorName == actorName) { - mEntries.erase( (++it).base() ); // erase doesn't take a reverse_iterator + mEntries.erase((++it).base()); // erase doesn't take a reverse_iterator return; } } diff --git a/apps/openmw/mwdialogue/topic.hpp b/apps/openmw/mwdialogue/topic.hpp index 72486ef8af1..d95d2470c7b 100644 --- a/apps/openmw/mwdialogue/topic.hpp +++ b/apps/openmw/mwdialogue/topic.hpp @@ -2,6 +2,7 @@ #define GAME_MWDIALOG_TOPIC_H #include +#include #include #include "journalentry.hpp" @@ -16,45 +17,42 @@ namespace MWDialogue /// \brief Collection of seen responses for a topic class Topic { - public: + public: + typedef std::vector TEntryContainer; + typedef TEntryContainer::const_iterator TEntryIter; - typedef std::vector TEntryContainer; - typedef TEntryContainer::const_iterator TEntryIter; + protected: + ESM::RefId mTopic; + std::string mName; + TEntryContainer mEntries; - protected: + public: + Topic(); - std::string mTopic; - std::string mName; - TEntryContainer mEntries; + Topic(const ESM::RefId& topic); - public: + virtual ~Topic(); - Topic(); + virtual bool addEntry(const JournalEntry& entry); + ///< Add entry + /// + /// \note Redundant entries are ignored. - Topic (const std::string& topic); + void insertEntry(const ESM::JournalEntry& entry); + ///< Add entry without checking for redundant entries or modifying the state of the + /// topic otherwise - virtual ~Topic(); + const ESM::RefId& getTopic() const; - virtual void addEntry (const JournalEntry& entry); - ///< Add entry - /// - /// \note Redundant entries are ignored. + virtual std::string_view getName() const; - void insertEntry (const ESM::JournalEntry& entry); - ///< Add entry without checking for redundant entries or modifying the state of the - /// topic otherwise + void removeLastAddedResponse(std::string_view actorName); - std::string getTopic() const; + TEntryIter begin() const; + ///< Iterator pointing to the begin of the journal for this topic. - virtual std::string getName() const; - - void removeLastAddedResponse (const std::string& actorName); - - TEntryIter begin() const; - ///< Iterator pointing to the begin of the journal for this topic. - - TEntryIter end() const; - ///< Iterator pointing past the end of the journal for this topic. + TEntryIter end() const; + ///< Iterator pointing past the end of the journal for this topic. }; } diff --git a/apps/openmw/mwgui/alchemywindow.cpp b/apps/openmw/mwgui/alchemywindow.cpp index bacd1c76953..5a6245fca0c 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -1,30 +1,34 @@ #include "alchemywindow.hpp" -#include #include -#include #include #include #include +#include +#include +#include + +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwmechanics/magiceffects.hpp" -#include "../mwmechanics/alchemy.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/alchemy.hpp" +#include "../mwmechanics/magiceffects.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include -#include #include "inventoryitemmodel.hpp" -#include "sortfilteritemmodel.hpp" #include "itemview.hpp" #include "itemwidget.hpp" +#include "sortfilteritemmodel.hpp" #include "widgets.hpp" namespace MWGui @@ -34,9 +38,9 @@ namespace MWGui , mCurrentFilter(FilterType::ByName) , mModel(nullptr) , mSortModel(nullptr) - , mAlchemy(new MWMechanics::Alchemy()) - , mApparatus (4) - , mIngredients (4) + , mAlchemy(std::make_unique()) + , mApparatus(4) + , mIngredients(4) { getWidget(mCreateButton, "CreateButton"); getWidget(mCancelButton, "CancelButton"); @@ -74,6 +78,11 @@ namespace MWGui mIngredients[2]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); mIngredients[3]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); + mApparatus[0]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mApparatus[1]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mApparatus[2]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mApparatus[3]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onApparatusSelected); + mCreateButton->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onCreateButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onCancelButtonClicked); @@ -109,40 +118,41 @@ namespace MWGui void AlchemyWindow::createPotions(int count) { MWMechanics::Alchemy::Result result = mAlchemy->create(mNameEdit->getCaption(), count); - MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); switch (result) { - case MWMechanics::Alchemy::Result_NoName: - winMgr->messageBox("#{sNotifyMessage37}"); - break; - case MWMechanics::Alchemy::Result_NoMortarAndPestle: - winMgr->messageBox("#{sNotifyMessage45}"); - break; - case MWMechanics::Alchemy::Result_LessThanTwoIngredients: - winMgr->messageBox("#{sNotifyMessage6a}"); - break; - case MWMechanics::Alchemy::Result_Success: - winMgr->playSound("potion success"); - if (count == 1) - winMgr->messageBox("#{sPotionSuccess}"); - else - winMgr->messageBox("#{sPotionSuccess} "+mNameEdit->getCaption()+" ("+std::to_string(count)+")"); - break; - case MWMechanics::Alchemy::Result_NoEffects: - case MWMechanics::Alchemy::Result_RandomFailure: - winMgr->messageBox("#{sNotifyMessage8}"); - winMgr->playSound("potion fail"); - break; + case MWMechanics::Alchemy::Result_NoName: + winMgr->messageBox("#{sNotifyMessage37}"); + break; + case MWMechanics::Alchemy::Result_NoMortarAndPestle: + winMgr->messageBox("#{sNotifyMessage45}"); + break; + case MWMechanics::Alchemy::Result_LessThanTwoIngredients: + winMgr->messageBox("#{sNotifyMessage6a}"); + break; + case MWMechanics::Alchemy::Result_Success: + winMgr->playSound(ESM::RefId::stringRefId("potion success")); + if (count == 1) + winMgr->messageBox("#{sPotionSuccess}"); + else + winMgr->messageBox( + "#{sPotionSuccess} " + mNameEdit->getCaption().asUTF8() + " (" + std::to_string(count) + ")"); + break; + case MWMechanics::Alchemy::Result_NoEffects: + case MWMechanics::Alchemy::Result_RandomFailure: + winMgr->messageBox("#{sNotifyMessage8}"); + winMgr->playSound(ESM::RefId::stringRefId("potion fail")); + break; } // remove ingredient slots that have been fully used up - for (int i=0; i<4; ++i) + for (size_t i = 0; i < mIngredients.size(); ++i) if (mIngredients[i]->isUserString("ToolTipType")) { MWWorld::Ptr ingred = *mIngredients[i]->getUserData(); - if (ingred.getRefData().getCount() == 0) - removeIngredient(mIngredients[i]); + if (ingred.getCellRef().getCount() == 0) + mAlchemy->removeIngredient(i); } updateFilters(); @@ -152,8 +162,7 @@ namespace MWGui void AlchemyWindow::initFilter() { auto const& wm = MWBase::Environment::get().getWindowManager(); - auto const ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); - auto const effect = wm->getGameSettingString("sMagicEffects", "Magic Effects"); + std::string_view ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); if (mFilterType->getCaption() == ingredient) mCurrentFilter = FilterType::ByName; @@ -167,18 +176,17 @@ namespace MWGui void AlchemyWindow::switchFilterType(MyGUI::Widget* _sender) { auto const& wm = MWBase::Environment::get().getWindowManager(); - auto const ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); - auto const effect = wm->getGameSettingString("sMagicEffects", "Magic Effects"); - auto *button = _sender->castType(); + std::string_view ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); + auto* button = _sender->castType(); if (button->getCaption() == ingredient) { - button->setCaption(effect); + button->setCaption(MyGUI::UString(wm->getGameSettingString("sMagicEffects", "Magic Effects"))); mCurrentFilter = FilterType::ByEffect; } else { - button->setCaption(ingredient); + button->setCaption(MyGUI::UString(ingredient)); mCurrentFilter = FilterType::ByName; } mSortModel->setNameFilter({}); @@ -194,12 +202,12 @@ namespace MWGui for (size_t i = 0; i < mModel->getItemCount(); ++i) { MWWorld::Ptr item = mModel->getItem(i).mBase; - if (item.getTypeName() != typeid(ESM::Ingredient).name()) + if (item.getType() != ESM::Ingredient::sRecordId) continue; - itemNames.insert(item.getClass().getName(item)); + itemNames.emplace(item.getClass().getName(item)); - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); auto const alchemySkill = player.getClass().getSkill(player, ESM::Skill::Alchemy); auto const effects = MWMechanics::Alchemy::effectsDescription(item, alchemySkill); @@ -207,15 +215,18 @@ namespace MWGui } mFilterValue->removeAllItems(); - auto const addItems = [&](auto const& container) - { + auto const addItems = [&](auto const& container) { for (auto const& item : container) mFilterValue->addItem(item); }; switch (mCurrentFilter) { - case FilterType::ByName: addItems(itemNames); break; - case FilterType::ByEffect: addItems(itemEffects); break; + case FilterType::ByName: + addItems(itemNames); + break; + case FilterType::ByEffect: + addItems(itemEffects); + break; } } @@ -223,8 +234,12 @@ namespace MWGui { switch (mCurrentFilter) { - case FilterType::ByName: mSortModel->setNameFilter(filter); break; - case FilterType::ByEffect: mSortModel->setEffectFilter(filter); break; + case FilterType::ByName: + mSortModel->setNameFilter(filter); + break; + case FilterType::ByEffect: + mSortModel->setEffectFilter(filter); + break; } mItemView->update(); } @@ -245,27 +260,30 @@ namespace MWGui void AlchemyWindow::onOpen() { mAlchemy->clear(); - mAlchemy->setAlchemist (MWMechanics::getPlayer()); + mAlchemy->setAlchemist(MWMechanics::getPlayer()); - mModel = new InventoryItemModel(MWMechanics::getPlayer()); - mSortModel = new SortFilterItemModel(mModel); + auto model = std::make_unique(MWMechanics::getPlayer()); + mModel = model.get(); + auto sortModel = std::make_unique(std::move(model)); + mSortModel = sortModel.get(); mSortModel->setFilter(SortFilterItemModel::Filter_OnlyIngredients); - mItemView->setModel (mSortModel); + mItemView->setModel(std::move(sortModel)); mItemView->resetScrollBars(); - mNameEdit->setCaption(""); + mNameEdit->setCaption({}); mBrewCountEdit->setValue(1); - int index = 0; - for (MWMechanics::Alchemy::TToolsIterator iter (mAlchemy->beginTools()); - iter!=mAlchemy->endTools() && index (mApparatus.size()); ++iter, ++index) + size_t index = 0; + for (auto iter = mAlchemy->beginTools(); iter != mAlchemy->endTools() && index < mApparatus.size(); + ++iter, ++index) { - mApparatus.at (index)->setItem(*iter); - mApparatus.at (index)->clearUserStrings(); + const auto& widget = mApparatus[index]; + widget->setItem(*iter); + widget->clearUserStrings(); if (!iter->isEmpty()) { - mApparatus.at (index)->setUserString ("ToolTipType", "ItemPtr"); - mApparatus.at (index)->setUserData (MWWorld::Ptr(*iter)); + widget->setUserString("ToolTipType", "ItemPtr"); + widget->setUserData(MWWorld::Ptr(*iter)); } } @@ -277,7 +295,85 @@ namespace MWGui void AlchemyWindow::onIngredientSelected(MyGUI::Widget* _sender) { - removeIngredient(_sender); + size_t i = std::distance(mIngredients.begin(), std::find(mIngredients.begin(), mIngredients.end(), _sender)); + mAlchemy->removeIngredient(i); + update(); + } + + void AlchemyWindow::onItemSelected(MWWorld::Ptr item) + { + mItemSelectionDialog->setVisible(false); + + int32_t index = item.get()->mBase->mData.mType; + const auto& widget = mApparatus[index]; + + widget->setItem(item); + + if (item.isEmpty()) + { + widget->clearUserStrings(); + return; + } + + mAlchemy->addApparatus(item); + + widget->setUserString("ToolTipType", "ItemPtr"); + widget->setUserData(MWWorld::Ptr(item)); + + MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); + update(); + } + + void AlchemyWindow::onItemCancel() + { + mItemSelectionDialog->setVisible(false); + } + + void AlchemyWindow::onApparatusSelected(MyGUI::Widget* _sender) + { + size_t i = std::distance(mApparatus.begin(), std::find(mApparatus.begin(), mApparatus.end(), _sender)); + if (_sender->getUserData()->isEmpty()) // if this apparatus slot is empty + { + std::string title; + switch (i) + { + case ESM::Apparatus::AppaType::MortarPestle: + title = "#{sMortar}"; + break; + case ESM::Apparatus::AppaType::Alembic: + title = "#{sAlembic}"; + break; + case ESM::Apparatus::AppaType::Calcinator: + title = "#{sCalcinator}"; + break; + case ESM::Apparatus::AppaType::Retort: + title = "#{sRetort}"; + break; + default: + title = "#{sApparatus}"; + } + + mItemSelectionDialog = std::make_unique(title); + mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &AlchemyWindow::onItemSelected); + mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &AlchemyWindow::onItemCancel); + mItemSelectionDialog->setVisible(true); + mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); + mItemSelectionDialog->getSortModel()->setApparatusTypeFilter(i); + mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyAlchemyTools); + } + else + { + const auto& widget = mApparatus[i]; + mAlchemy->removeApparatus(i); + + if (widget->getChildCount()) + MyGUI::Gui::getInstance().destroyWidget(widget->getChildAt(0)); + + widget->clearUserStrings(); + widget->setItem(MWWorld::Ptr()); + widget->setUserData(MWWorld::Ptr()); + } + update(); } @@ -290,7 +386,7 @@ namespace MWGui { update(); - std::string sound = item.getClass().getUpSoundId(item); + const ESM::RefId& sound = item.getClass().getUpSoundId(item); MWBase::Environment::get().getWindowManager()->playSound(sound); } } @@ -299,52 +395,55 @@ namespace MWGui { std::string suggestedName = mAlchemy->suggestPotionName(); if (suggestedName != mSuggestedPotionName) + { mNameEdit->setCaptionWithReplacing(suggestedName); - mSuggestedPotionName = suggestedName; + mSuggestedPotionName = std::move(suggestedName); + } mSortModel->clearDragItems(); - MWMechanics::Alchemy::TIngredientsIterator it = mAlchemy->beginIngredients (); - for (int i=0; i<4; ++i) + MWMechanics::Alchemy::TIngredientsIterator it = mAlchemy->beginIngredients(); + for (int i = 0; i < 4; ++i) { ItemWidget* ingredient = mIngredients[i]; MWWorld::Ptr item; - if (it != mAlchemy->endIngredients ()) + if (it != mAlchemy->endIngredients()) { item = *it; ++it; } if (!item.isEmpty()) - mSortModel->addDragItem(item, item.getRefData().getCount()); + mSortModel->addDragItem(item, item.getCellRef().getCount()); if (ingredient->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(ingredient->getChildAt(0)); - ingredient->clearUserStrings (); + ingredient->clearUserStrings(); ingredient->setItem(item); - if (item.isEmpty ()) + if (item.isEmpty()) continue; ingredient->setUserString("ToolTipType", "ItemPtr"); ingredient->setUserData(MWWorld::Ptr(item)); - ingredient->setCount(item.getRefData().getCount()); + ingredient->setCount(item.getCellRef().getCount()); } mItemView->update(); - std::set effectIds = mAlchemy->listEffects(); + std::vector effectIds = mAlchemy->listEffects(); Widgets::SpellEffectList list; - unsigned int effectIndex=0; + unsigned int effectIndex = 0; for (const MWMechanics::EffectKey& effectKey : effectIds) { Widgets::SpellEffectParams params; params.mEffectID = effectKey.mId; - const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectKey.mId); + const ESM::MagicEffect* magicEffect + = MWBase::Environment::get().getESMStore()->get().find(effectKey.mId); if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) params.mSkill = effectKey.mArg; else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) @@ -363,8 +462,8 @@ namespace MWGui MyGUI::Gui::getInstance().destroyWidget(mEffectsBox->getChildAt(0)); MyGUI::IntCoord coord(0, 0, mEffectsBox->getWidth(), 24); - Widgets::MWEffectListPtr effectsWidget = mEffectsBox->createWidget - ("MW_StatName", coord, MyGUI::Align::Left | MyGUI::Align::Top); + Widgets::MWEffectListPtr effectsWidget = mEffectsBox->createWidget( + "MW_StatName", coord, MyGUI::Align::Left | MyGUI::Align::Top); effectsWidget->setEffectList(list); @@ -373,18 +472,10 @@ namespace MWGui effectsWidget->setCoord(coord); } - void AlchemyWindow::removeIngredient(MyGUI::Widget* ingredient) - { - for (int i=0; i<4; ++i) - if (mIngredients[i] == ingredient) - mAlchemy->removeIngredient (i); - - update(); - } - - void AlchemyWindow::addRepeatController(MyGUI::Widget *widget) + void AlchemyWindow::addRepeatController(MyGUI::Widget* widget) { - MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MyGUI::ControllerRepeatClick::getClassTypeName()); + MyGUI::ControllerItem* item + = MyGUI::ControllerManager::getInstance().createItem(MyGUI::ControllerRepeatClick::getClassTypeName()); MyGUI::ControllerRepeatClick* controller = static_cast(item); controller->eventRepeatClick += newDelegate(this, &AlchemyWindow::onRepeatClick); MyGUI::ControllerManager::getInstance().addItem(widget, controller); @@ -410,7 +501,7 @@ namespace MWGui onDecreaseButtonTriggered(); } - void AlchemyWindow::onCountButtonReleased(MyGUI::Widget *_sender, int _left, int _top, MyGUI::MouseButton _id) + void AlchemyWindow::onCountButtonReleased(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { MyGUI::ControllerManager::getInstance().removeItem(_sender); } @@ -428,13 +519,13 @@ namespace MWGui if (currentCount == std::numeric_limits::max()) return; - mBrewCountEdit->setValue(currentCount+1); + mBrewCountEdit->setValue(currentCount + 1); } void AlchemyWindow::onDecreaseButtonTriggered() { int currentCount = mBrewCountEdit->getValue(); if (currentCount > 1) - mBrewCountEdit->setValue(currentCount-1); + mBrewCountEdit->setValue(currentCount - 1); } } diff --git a/apps/openmw/mwgui/alchemywindow.hpp b/apps/openmw/mwgui/alchemywindow.hpp index 33bd1f97432..82e5c3f583b 100644 --- a/apps/openmw/mwgui/alchemywindow.hpp +++ b/apps/openmw/mwgui/alchemywindow.hpp @@ -4,18 +4,16 @@ #include #include -#include #include +#include #include #include +#include "itemselection.hpp" #include "windowbase.hpp" -namespace MWMechanics -{ - class Alchemy; -} +#include "../mwmechanics/alchemy.hpp" namespace MWGui { @@ -33,15 +31,22 @@ namespace MWGui void onResChange(int, int) override { center(); } - private: + std::string_view getWindowIdForLua() const override { return "Alchemy"; } + private: static const float sCountChangeInitialPause; // in seconds static const float sCountChangeInterval; // in seconds std::string mSuggestedPotionName; - enum class FilterType { ByName, ByEffect }; + enum class FilterType + { + ByName, + ByEffect + }; FilterType mCurrentFilter; + std::unique_ptr mItemSelectionDialog; + ItemView* mItemView; InventoryItemModel* mModel; SortFilterItemModel* mSortModel; @@ -61,6 +66,7 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* _sender); void onCreateButtonClicked(MyGUI::Widget* _sender); void onIngredientSelected(MyGUI::Widget* _sender); + void onApparatusSelected(MyGUI::Widget* _sender); void onAccept(MyGUI::EditBox*); void onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); @@ -82,7 +88,8 @@ namespace MWGui void onSelectedItem(int index); - void removeIngredient(MyGUI::Widget* ingredient); + void onItemSelected(MWWorld::Ptr item); + void onItemCancel(); void createPotions(int count); diff --git a/apps/openmw/mwgui/backgroundimage.cpp b/apps/openmw/mwgui/backgroundimage.cpp index 55c825ebbd4..e2e2c62cd16 100644 --- a/apps/openmw/mwgui/backgroundimage.cpp +++ b/apps/openmw/mwgui/backgroundimage.cpp @@ -5,59 +5,59 @@ namespace MWGui { -void BackgroundImage::setBackgroundImage (const std::string& image, bool fixedRatio, bool stretch) -{ - if (mChild) + void BackgroundImage::setBackgroundImage(const std::string& image, bool fixedRatio, bool stretch) { - MyGUI::Gui::getInstance().destroyWidget(mChild); - mChild = nullptr; + if (mChild) + { + MyGUI::Gui::getInstance().destroyWidget(mChild); + mChild = nullptr; + } + if (!stretch) + { + setImageTexture("black"); + + if (fixedRatio) + mAspect = 4.0 / 3.0; + else + mAspect = 0; // TODO + + mChild + = createWidgetReal("ImageBox", MyGUI::FloatCoord(0, 0, 1, 1), MyGUI::Align::Default); + mChild->setImageTexture(image); + + adjustSize(); + } + else + { + mAspect = 0; + setImageTexture(image); + } } - if (!stretch) + + void BackgroundImage::adjustSize() { - setImageTexture("black"); + if (mAspect == 0) + return; - if (fixedRatio) - mAspect = 4.0/3.0; - else - mAspect = 0; // TODO + MyGUI::IntSize screenSize = getSize(); - mChild = createWidgetReal("ImageBox", - MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); - mChild->setImageTexture(image); + int leftPadding = std::max(0, static_cast(screenSize.width - screenSize.height * mAspect) / 2); + int topPadding = std::max(0, static_cast(screenSize.height - screenSize.width / mAspect) / 2); - adjustSize(); + mChild->setCoord( + leftPadding, topPadding, screenSize.width - leftPadding * 2, screenSize.height - topPadding * 2); } - else + + void BackgroundImage::setSize(const MyGUI::IntSize& _value) { - mAspect = 0; - setImageTexture(image); + MyGUI::Widget::setSize(_value); + adjustSize(); } -} - -void BackgroundImage::adjustSize() -{ - if (mAspect == 0) - return; - - MyGUI::IntSize screenSize = getSize(); - - int leftPadding = std::max(0, static_cast(screenSize.width - screenSize.height * mAspect) / 2); - int topPadding = std::max(0, static_cast(screenSize.height - screenSize.width / mAspect) / 2); - - mChild->setCoord(leftPadding, topPadding, screenSize.width - leftPadding*2, screenSize.height - topPadding*2); -} - -void BackgroundImage::setSize (const MyGUI::IntSize& _value) -{ - MyGUI::Widget::setSize (_value); - adjustSize(); -} - -void BackgroundImage::setCoord (const MyGUI::IntCoord& _value) -{ - MyGUI::Widget::setCoord (_value); - adjustSize(); -} + void BackgroundImage::setCoord(const MyGUI::IntCoord& _value) + { + MyGUI::Widget::setCoord(_value); + adjustSize(); + } } diff --git a/apps/openmw/mwgui/backgroundimage.hpp b/apps/openmw/mwgui/backgroundimage.hpp index 32cdf1a657b..b151a26e278 100644 --- a/apps/openmw/mwgui/backgroundimage.hpp +++ b/apps/openmw/mwgui/backgroundimage.hpp @@ -11,19 +11,23 @@ namespace MWGui */ class BackgroundImage final : public MyGUI::ImageBox { - MYGUI_RTTI_DERIVED(BackgroundImage) + MYGUI_RTTI_DERIVED(BackgroundImage) public: - BackgroundImage() : mChild(nullptr), mAspect(0) {} + BackgroundImage() + : mChild(nullptr) + , mAspect(0) + { + } /** * @param fixedRatio Use a fixed ratio of 4:3, regardless of the image dimensions * @param stretch Stretch to fill the whole screen, or add black bars? */ - void setBackgroundImage (const std::string& image, bool fixedRatio=true, bool stretch=true); + void setBackgroundImage(const std::string& image, bool fixedRatio = true, bool stretch = true); - void setSize (const MyGUI::IntSize &_value) override; - void setCoord (const MyGUI::IntCoord &_value) override; + void setSize(const MyGUI::IntSize& _value) override; + void setCoord(const MyGUI::IntCoord& _value) override; private: MyGUI::ImageBox* mChild; diff --git a/apps/openmw/mwgui/birth.cpp b/apps/openmw/mwgui/birth.cpp index 41711f5e4e3..3dfdd176274 100644 --- a/apps/openmw/mwgui/birth.cpp +++ b/apps/openmw/mwgui/birth.cpp @@ -1,13 +1,20 @@ #include "birth.hpp" -#include -#include #include +#include +#include #include +#include + +#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" @@ -17,9 +24,10 @@ namespace { - bool sortBirthSigns(const std::pair& left, const std::pair& right) + bool sortBirthSigns(const std::pair& left, + const std::pair& right) { - return left.second->mName.compare (right.second->mName) < 0; + return left.second->mName.compare(right.second->mName) < 0; } } @@ -28,7 +36,7 @@ namespace MWGui { BirthDialog::BirthDialog() - : WindowModal("openmw_chargen_birth.layout") + : WindowModal("openmw_chargen_birth.layout") { // Centre dialog center(); @@ -48,7 +56,8 @@ namespace MWGui MyGUI::Button* okButton; getWidget(okButton, "OKButton"); - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BirthDialog::onOkClicked); updateBirths(); @@ -61,9 +70,11 @@ namespace MWGui getWidget(okButton, "OKButton"); if (shown) - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void BirthDialog::onOpen() @@ -74,21 +85,20 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mBirthList); // Show the current birthsign by default - const std::string &signId = - MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); + const auto& signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); if (!signId.empty()) setBirthId(signId); } - void BirthDialog::setBirthId(const std::string &birthId) + void BirthDialog::setBirthId(const ESM::RefId& birthId) { mCurrentBirthId = birthId; mBirthList->setIndexSelected(MyGUI::ITEM_NONE); size_t count = mBirthList->getItemCount(); for (size_t i = 0; i < count; ++i) { - if (Misc::StringUtils::ciEqual(*mBirthList->getItemDataAt(i), birthId)) + if (*mBirthList->getItemDataAt(i) == birthId) { mBirthList->setIndexSelected(i); break; @@ -102,15 +112,15 @@ namespace MWGui void BirthDialog::onOkClicked(MyGUI::Widget* _sender) { - if(mBirthList->getIndexSelected() == MyGUI::ITEM_NONE) + if (mBirthList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } - void BirthDialog::onAccept(MyGUI::ListBox *_sender, size_t _index) + void BirthDialog::onAccept(MyGUI::ListBox* _sender, size_t _index) { onSelectBirth(_sender, _index); - if(mBirthList->getIndexSelected() == MyGUI::ITEM_NONE) + if (mBirthList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } @@ -125,11 +135,11 @@ namespace MWGui if (_index == MyGUI::ITEM_NONE) return; - const std::string *birthId = mBirthList->getItemDataAt(_index); - if (Misc::StringUtils::ciEqual(mCurrentBirthId, *birthId)) + const ESM::RefId& birthId = *mBirthList->getItemDataAt(_index); + if (mCurrentBirthId == birthId) return; - mCurrentBirthId = *birthId; + mCurrentBirthId = birthId; updateSpells(); } @@ -139,11 +149,10 @@ namespace MWGui { mBirthList->removeAllItems(); - const MWWorld::Store &signs = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& signs = MWBase::Environment::get().getESMStore()->get(); // sort by name - std::vector < std::pair > birthSigns; + std::vector> birthSigns; for (const ESM::BirthSign& sign : signs) { @@ -160,7 +169,7 @@ namespace MWGui mBirthList->setIndexSelected(index); mCurrentBirthId = birthsignPair.first; } - else if (Misc::StringUtils::ciEqual(birthsignPair.first, mCurrentBirthId)) + else if (birthsignPair.first == mCurrentBirthId) { mBirthList->setIndexSelected(index); } @@ -181,25 +190,24 @@ namespace MWGui return; Widgets::MWSpellPtr spellWidget; - const int lineHeight = 18; - MyGUI::IntCoord coord(0, 0, mSpellArea->getWidth(), 18); + const int lineHeight = Settings::gui().mFontSize + 2; + MyGUI::IntCoord coord(0, 0, mSpellArea->getWidth(), lineHeight); - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - const ESM::BirthSign *birth = - store.get().find(mCurrentBirthId); + const ESM::BirthSign* birth = store.get().find(mCurrentBirthId); - mBirthImage->setImageTexture(MWBase::Environment::get().getWindowManager()->correctTexturePath(birth->mTexture)); + mBirthImage->setImageTexture(Misc::ResourceHelpers::correctTexturePath( + birth->mTexture, MWBase::Environment::get().getResourceSystem()->getVFS())); - std::vector abilities, powers, spells; + std::vector abilities, powers, spells; - std::vector::const_iterator it = birth->mPowers.mList.begin(); - std::vector::const_iterator end = birth->mPowers.mList.end(); + std::vector::const_iterator it = birth->mPowers.mList.begin(); + std::vector::const_iterator end = birth->mPowers.mList.end(); for (; it != end; ++it) { - const std::string &spellId = *it; - const ESM::Spell *spell = store.get().search(spellId); + const ESM::RefId& spellId = *it; + const ESM::Spell* spell = store.get().search(spellId); if (!spell) continue; // Skip spells which cannot be found ESM::Spell::SpellType type = static_cast(spell->mData.mType); @@ -216,38 +224,39 @@ namespace MWGui int i = 0; - struct { - const std::vector &spells; - const char *label; - } - categories[3] = { - {abilities, "sBirthsignmenu1"}, - {powers, "sPowers"}, - {spells, "sBirthsignmenu2"} - }; + struct + { + const std::vector& spells; + std::string_view label; + } categories[3] = { { abilities, "sBirthsignmenu1" }, { powers, "sPowers" }, { spells, "sBirthsignmenu2" } }; - for (int category = 0; category < 3; ++category) + for (size_t category = 0; category < 3; ++category) { if (!categories[category].spells.empty()) { - MyGUI::TextBox* label = mSpellArea->createWidget("SandBrightText", coord, MyGUI::Align::Default, std::string("Label")); - label->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString(categories[category].label, "")); + MyGUI::TextBox* label + = mSpellArea->createWidget("SandBrightText", coord, MyGUI::Align::Default, "Label"); + label->setCaption(MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + categories[category].label, {}))); mSpellItems.push_back(label); coord.top += lineHeight; end = categories[category].spells.end(); for (it = categories[category].spells.begin(); it != end; ++it) { - const std::string &spellId = *it; - spellWidget = mSpellArea->createWidget("MW_StatName", coord, MyGUI::Align::Default, std::string("Spell") + MyGUI::utility::toString(i)); + const ESM::RefId& spellId = *it; + spellWidget = mSpellArea->createWidget( + "MW_StatName", coord, MyGUI::Align::Default, "Spell" + MyGUI::utility::toString(i)); spellWidget->setSpellId(spellId); mSpellItems.push_back(spellWidget); coord.top += lineHeight; MyGUI::IntCoord spellCoord = coord; - spellCoord.height = 24; // TODO: This should be fetched from the skin somehow, or perhaps a widget in the layout as a template? - spellWidget->createEffectWidgets(mSpellItems, mSpellArea, spellCoord, (category == 0) ? Widgets::MWEffectList::EF_Constant : 0); + spellCoord.height = 24; // TODO: This should be fetched from the skin somehow, or perhaps a widget + // in the layout as a template? + spellWidget->createEffectWidgets( + mSpellItems, mSpellArea, spellCoord, (category == 0) ? Widgets::MWEffectList::EF_Constant : 0); coord.top = spellCoord.top; ++i; @@ -255,7 +264,8 @@ namespace MWGui } } - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden mSpellArea->setVisibleVScroll(false); mSpellArea->setCanvasSize(MyGUI::IntSize(mSpellArea->getWidth(), std::max(mSpellArea->getHeight(), coord.top))); mSpellArea->setVisibleVScroll(true); diff --git a/apps/openmw/mwgui/birth.hpp b/apps/openmw/mwgui/birth.hpp index 9f9d4332f1a..db9e997b6c5 100644 --- a/apps/openmw/mwgui/birth.hpp +++ b/apps/openmw/mwgui/birth.hpp @@ -2,6 +2,7 @@ #define MWGUI_BIRTH_H #include "windowbase.hpp" +#include namespace MWGui { @@ -16,8 +17,8 @@ namespace MWGui GM_Female }; - const std::string &getBirthId() const { return mCurrentBirthId; } - void setBirthId(const std::string &raceId); + const ESM::RefId& getBirthId() const { return mCurrentBirthId; } + void setBirthId(const ESM::RefId& raceId); void setNextButtonShow(bool shown); void onOpen() override; @@ -25,7 +26,7 @@ namespace MWGui bool exit() override { return false; } // Events - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n @@ -53,7 +54,7 @@ namespace MWGui MyGUI::ImageBox* mBirthImage; std::vector mSpellItems; - std::string mCurrentBirthId; + ESM::RefId mCurrentBirthId; }; } #endif diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index fba136f88f5..19664425137 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -2,1409 +2,1405 @@ #include +#include "MyGUI_FactoryManager.h" +#include "MyGUI_FontManager.h" #include "MyGUI_RenderItem.h" #include "MyGUI_RenderManager.h" #include "MyGUI_TextureUtility.h" -#include "MyGUI_FactoryManager.h" #include - -#include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" +#include +#include namespace MWGui { -struct TypesetBookImpl; -class PageDisplay; -class BookPageImpl; - -static bool ucsSpace (int codePoint); -static bool ucsLineBreak (int codePoint); -static bool ucsCarriageReturn (int codePoint); -static bool ucsBreakingSpace (int codePoint); + struct TypesetBookImpl; + class PageDisplay; + class BookPageImpl; -struct BookTypesetter::Style { virtual ~Style () {} }; + static bool ucsSpace(int codePoint); + static bool ucsLineBreak(int codePoint); + static bool ucsCarriageReturn(int codePoint); + static bool ucsBreakingSpace(int codePoint); -struct TypesetBookImpl : TypesetBook -{ - typedef std::vector Content; - typedef std::list Contents; - typedef Utf8Stream::Point Utf8Point; - typedef std::pair Range; + struct BookTypesetter::Style + { + virtual ~Style() {} + }; - struct StyleImpl : BookTypesetter::Style + struct TypesetBookImpl : TypesetBook { - MyGUI::IFont* mFont; - MyGUI::Colour mHotColour; - MyGUI::Colour mActiveColour; - MyGUI::Colour mNormalColour; - InteractiveId mInteractiveId; - - bool match (MyGUI::IFont* tstFont, const MyGUI::Colour& tstHotColour, const MyGUI::Colour& tstActiveColour, - const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId) - { - return (mFont == tstFont) && - partal_match (tstHotColour, tstActiveColour, tstNormalColour, tstInteractiveId); - } + typedef std::vector Content; + typedef std::list Contents; + typedef Utf8Stream::Point Utf8Point; + typedef std::pair Range; - bool match (char const * tstFont, const MyGUI::Colour& tstHotColour, const MyGUI::Colour& tstActiveColour, - const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId) + struct StyleImpl : BookTypesetter::Style { - return (mFont->getResourceName () == tstFont) && - partal_match (tstHotColour, tstActiveColour, tstNormalColour, tstInteractiveId); - } + MyGUI::IFont* mFont; + MyGUI::Colour mHotColour; + MyGUI::Colour mActiveColour; + MyGUI::Colour mNormalColour; + InteractiveId mInteractiveId; + + bool match(MyGUI::IFont* tstFont, const MyGUI::Colour& tstHotColour, const MyGUI::Colour& tstActiveColour, + const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId) + { + return (mFont == tstFont) + && partal_match(tstHotColour, tstActiveColour, tstNormalColour, tstInteractiveId); + } - bool partal_match (const MyGUI::Colour& tstHotColour, const MyGUI::Colour& tstActiveColour, - const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId) - { - return - (mHotColour == tstHotColour ) && - (mActiveColour == tstActiveColour ) && - (mNormalColour == tstNormalColour ) && - (mInteractiveId == tstInteractiveId ) ; - } - }; + bool match(std::string_view tstFont, const MyGUI::Colour& tstHotColour, + const MyGUI::Colour& tstActiveColour, const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId) + { + return (mFont->getResourceName() == tstFont) + && partal_match(tstHotColour, tstActiveColour, tstNormalColour, tstInteractiveId); + } - typedef std::list Styles; + bool partal_match(const MyGUI::Colour& tstHotColour, const MyGUI::Colour& tstActiveColour, + const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId) + { + return (mHotColour == tstHotColour) && (mActiveColour == tstActiveColour) + && (mNormalColour == tstNormalColour) && (mInteractiveId == tstInteractiveId); + } + }; - struct Run - { - StyleImpl* mStyle; - Range mRange; - int mLeft, mRight; - int mPrintableChars; - }; + typedef std::list Styles; - typedef std::vector Runs; + struct Run + { + StyleImpl* mStyle; + Range mRange; + int mLeft, mRight; + int mPrintableChars; + }; - struct Line - { - Runs mRuns; - MyGUI::IntRect mRect; - }; + typedef std::vector Runs; - typedef std::vector Lines; + struct Line + { + Runs mRuns; + MyGUI::IntRect mRect; + }; - struct Section - { - Lines mLines; - MyGUI::IntRect mRect; - }; + typedef std::vector Lines; - typedef std::vector

      Sections; + struct Section + { + Lines mLines; + MyGUI::IntRect mRect; + }; - // Holds "top" and "bottom" vertical coordinates in the source text. - // A page is basically a "window" into a portion of the source text, similar to a ScrollView. - typedef std::pair Page; + typedef std::vector
      Sections; - typedef std::vector Pages; + // Holds "top" and "bottom" vertical coordinates in the source text. + // A page is basically a "window" into a portion of the source text, similar to a ScrollView. + typedef std::pair Page; - Pages mPages; - Sections mSections; - Contents mContents; - Styles mStyles; - MyGUI::IntRect mRect; + typedef std::vector Pages; - virtual ~TypesetBookImpl () {} + Pages mPages; + Sections mSections; + Contents mContents; + Styles mStyles; + MyGUI::IntRect mRect; - Range addContent (BookTypesetter::Utf8Span text) - { - Contents::iterator i = mContents.insert (mContents.end (), Content (text.first, text.second)); + virtual ~TypesetBookImpl() {} - if (i->empty()) - return Range (Utf8Point (nullptr), Utf8Point (nullptr)); + Range addContent(const BookTypesetter::Utf8Span& text) + { + Contents::iterator i = mContents.insert(mContents.end(), Content(text.first, text.second)); - return Range (i->data(), i->data() + i->size()); - } + if (i->empty()) + return Range(Utf8Point(nullptr), Utf8Point(nullptr)); - size_t pageCount () const override { return mPages.size (); } + return Range(i->data(), i->data() + i->size()); + } - std::pair getSize () const override - { - return std::make_pair (mRect.width (), mRect.height ()); - } + size_t pageCount() const override { return mPages.size(); } - template - void visitRuns (int top, int bottom, MyGUI::IFont* Font, Visitor const & visitor) const - { - for (Sections::const_iterator i = mSections.begin (); i != mSections.end (); ++i) + std::pair getSize() const override { - if (top >= mRect.bottom || bottom <= i->mRect.top) - continue; + return std::make_pair(mRect.width(), mRect.height()); + } - for (Lines::const_iterator j = i->mLines.begin (); j != i->mLines.end (); ++j) + template + void visitRuns(int top, int bottom, MyGUI::IFont* Font, Visitor const& visitor) const + { + for (Sections::const_iterator i = mSections.begin(); i != mSections.end(); ++i) { - if (top >= j->mRect.bottom || bottom <= j->mRect.top) + if (top >= mRect.bottom || bottom <= i->mRect.top) continue; - for (Runs::const_iterator k = j->mRuns.begin (); k != j->mRuns.end (); ++k) - if (!Font || k->mStyle->mFont == Font) - visitor (*i, *j, *k); + for (Lines::const_iterator j = i->mLines.begin(); j != i->mLines.end(); ++j) + { + if (top >= j->mRect.bottom || bottom <= j->mRect.top) + continue; + + for (Runs::const_iterator k = j->mRuns.begin(); k != j->mRuns.end(); ++k) + if (!Font || k->mStyle->mFont == Font) + visitor(*i, *j, *k); + } } } - } - - template - void visitRuns (int top, int bottom, Visitor const & visitor) const - { - visitRuns (top, bottom, nullptr, visitor); - } - /// hit test with a margin for error. only hits on interactive text fragments are reported. - StyleImpl * hitTestWithMargin (int left, int top) - { - StyleImpl * hit = hitTest(left, top); - if (hit && hit->mInteractiveId != 0) - return hit; + template + void visitRuns(int top, int bottom, Visitor const& visitor) const + { + visitRuns(top, bottom, nullptr, visitor); + } - const int maxMargin = 10; - for (int margin=1; margin < maxMargin; ++margin) + /// hit test with a margin for error. only hits on interactive text fragments are reported. + StyleImpl* hitTestWithMargin(int left, int top) { - for (int i=0; i<4; ++i) - { - if (i==0) - hit = hitTest(left, top-margin); - else if (i==1) - hit = hitTest(left, top+margin); - else if (i==2) - hit = hitTest(left-margin, top); - else - hit = hitTest(left+margin, top); + StyleImpl* hit = hitTest(left, top); + if (hit && hit->mInteractiveId != 0) + return hit; - if (hit && hit->mInteractiveId != 0) - return hit; + const int maxMargin = 10; + for (int margin = 1; margin < maxMargin; ++margin) + { + for (int i = 0; i < 4; ++i) + { + if (i == 0) + hit = hitTest(left, top - margin); + else if (i == 1) + hit = hitTest(left, top + margin); + else if (i == 2) + hit = hitTest(left - margin, top); + else + hit = hitTest(left + margin, top); + + if (hit && hit->mInteractiveId != 0) + return hit; + } } + return nullptr; } - return nullptr; - } - StyleImpl * hitTest (int left, int top) const - { - for (Sections::const_iterator i = mSections.begin (); i != mSections.end (); ++i) + StyleImpl* hitTest(int left, int top) const { - if (top < i->mRect.top || top >= i->mRect.bottom) - continue; - - int left1 = left - i->mRect.left; - - for (Lines::const_iterator j = i->mLines.begin (); j != i->mLines.end (); ++j) + for (Sections::const_iterator i = mSections.begin(); i != mSections.end(); ++i) { - if (top < j->mRect.top || top >= j->mRect.bottom) + if (top < i->mRect.top || top >= i->mRect.bottom) continue; - int left2 = left1 - j->mRect.left; + int left1 = left - i->mRect.left; - for (Runs::const_iterator k = j->mRuns.begin (); k != j->mRuns.end (); ++k) + for (Lines::const_iterator j = i->mLines.begin(); j != i->mLines.end(); ++j) { - if (left2 < k->mLeft || left2 >= k->mRight) + if (top < j->mRect.top || top >= j->mRect.bottom) continue; - return k->mStyle; + int left2 = left1 - j->mRect.left; + + for (Runs::const_iterator k = j->mRuns.begin(); k != j->mRuns.end(); ++k) + { + if (left2 < k->mLeft || left2 >= k->mRight) + continue; + + return k->mStyle; + } } } + + return nullptr; } - return nullptr; - } + MyGUI::IFont* affectedFont(StyleImpl* style) + { + for (Styles::iterator i = mStyles.begin(); i != mStyles.end(); ++i) + if (&*i == style) + return i->mFont; + return nullptr; + } - MyGUI::IFont* affectedFont (StyleImpl* style) - { - for (Styles::iterator i = mStyles.begin (); i != mStyles.end (); ++i) - if (&*i == style) - return i->mFont; - return nullptr; - } + struct Typesetter; + }; - struct Typesetter; -}; + struct TypesetBookImpl::Typesetter : BookTypesetter + { + struct PartialText + { + StyleImpl* mStyle; + Utf8Stream::Point mBegin; + Utf8Stream::Point mEnd; + int mWidth; + + PartialText(StyleImpl* style, Utf8Stream::Point begin, Utf8Stream::Point end, int width) + : mStyle(style) + , mBegin(begin) + , mEnd(end) + , mWidth(width) + { + } + }; + + typedef TypesetBookImpl Book; + typedef std::shared_ptr BookPtr; + typedef std::vector::const_iterator PartialTextConstIterator; + + int mPageWidth; + int mPageHeight; + + BookPtr mBook; + Section* mSection; + Line* mLine; + Run* mRun; + + std::vector mSectionAlignment; + std::vector mPartialWhitespace; + std::vector mPartialWord; + + Book::Content const* mCurrentContent; + Alignment mCurrentAlignment; + + Typesetter(size_t width, size_t height) + : mPageWidth(width) + , mPageHeight(height) + , mSection(nullptr) + , mLine(nullptr) + , mRun(nullptr) + , mCurrentContent(nullptr) + , mCurrentAlignment(AlignLeft) + { + mBook = std::make_shared(); + } -struct TypesetBookImpl::Typesetter : BookTypesetter -{ - struct PartialText { - StyleImpl *mStyle; - Utf8Stream::Point mBegin; - Utf8Stream::Point mEnd; - int mWidth; - - PartialText( StyleImpl *style, Utf8Stream::Point begin, Utf8Stream::Point end, int width) : - mStyle(style), mBegin(begin), mEnd(end), mWidth(width) - {} - }; + virtual ~Typesetter() {} - typedef TypesetBookImpl Book; - typedef std::shared_ptr BookPtr; - typedef std::vector::const_iterator PartialTextConstIterator; + Style* createStyle(const std::string& fontName, const Colour& fontColour, bool useBookFont) override + { + std::string fullFontName; + if (fontName.empty()) + fullFontName = MyGUI::FontManager::getInstance().getDefaultFont(); + else + fullFontName = fontName; - int mPageWidth; - int mPageHeight; + if (useBookFont) + fullFontName = "Journalbook " + fullFontName; - BookPtr mBook; - Section * mSection; - Line * mLine; - Run * mRun; + for (Styles::iterator i = mBook->mStyles.begin(); i != mBook->mStyles.end(); ++i) + if (i->match(fullFontName, fontColour, fontColour, fontColour, 0)) + return &*i; - std::vector mSectionAlignment; - std::vector mPartialWhitespace; - std::vector mPartialWord; + MyGUI::IFont* font = MyGUI::FontManager::getInstance().getByName(fullFontName); + if (!font) + throw std::runtime_error(std::string("can't find font ") + fullFontName); - Book::Content const * mCurrentContent; - Alignment mCurrentAlignment; + StyleImpl& style = *mBook->mStyles.insert(mBook->mStyles.end(), StyleImpl()); + style.mFont = font; + style.mHotColour = fontColour; + style.mActiveColour = fontColour; + style.mNormalColour = fontColour; + style.mInteractiveId = 0; - Typesetter (size_t width, size_t height) : - mPageWidth (width), mPageHeight(height), - mSection (nullptr), mLine (nullptr), mRun (nullptr), - mCurrentContent (nullptr), - mCurrentAlignment (AlignLeft) - { - mBook = std::make_shared (); - } + return &style; + } - virtual ~Typesetter () - { - } + Style* createHotStyle(Style* baseStyle, const Colour& normalColour, const Colour& hoverColour, + const Colour& activeColour, InteractiveId id, bool unique) override + { + StyleImpl* BaseStyle = static_cast(baseStyle); - Style * createStyle (const std::string& fontName, const Colour& fontColour, bool useBookFont) override - { - std::string fullFontName; - if (fontName.empty()) - fullFontName = MyGUI::FontManager::getInstance().getDefaultFont(); - else - fullFontName = fontName; - - if (useBookFont) - fullFontName = "Journalbook " + fullFontName; - - for (Styles::iterator i = mBook->mStyles.begin (); i != mBook->mStyles.end (); ++i) - if (i->match (fullFontName.c_str(), fontColour, fontColour, fontColour, 0)) - return &*i; - - MyGUI::IFont* font = MyGUI::FontManager::getInstance().getByName(fullFontName); - if (!font) - throw std::runtime_error(std::string("can't find font ") + fullFontName); - - StyleImpl & style = *mBook->mStyles.insert (mBook->mStyles.end (), StyleImpl ()); - style.mFont = font; - style.mHotColour = fontColour; - style.mActiveColour = fontColour; - style.mNormalColour = fontColour; - style.mInteractiveId = 0; - - return &style; - } + if (!unique) + for (Styles::iterator i = mBook->mStyles.begin(); i != mBook->mStyles.end(); ++i) + if (i->match(BaseStyle->mFont, hoverColour, activeColour, normalColour, id)) + return &*i; - Style* createHotStyle (Style* baseStyle, const Colour& normalColour, const Colour& hoverColour, - const Colour& activeColour, InteractiveId id, bool unique) override - { - StyleImpl* BaseStyle = static_cast (baseStyle); + StyleImpl& style = *mBook->mStyles.insert(mBook->mStyles.end(), StyleImpl()); - if (!unique) - for (Styles::iterator i = mBook->mStyles.begin (); i != mBook->mStyles.end (); ++i) - if (i->match (BaseStyle->mFont, hoverColour, activeColour, normalColour, id)) - return &*i; + style.mFont = BaseStyle->mFont; + style.mHotColour = hoverColour; + style.mActiveColour = activeColour; + style.mNormalColour = normalColour; + style.mInteractiveId = id; - StyleImpl & style = *mBook->mStyles.insert (mBook->mStyles.end (), StyleImpl ()); + return &style; + } - style.mFont = BaseStyle->mFont; - style.mHotColour = hoverColour; - style.mActiveColour = activeColour; - style.mNormalColour = normalColour; - style.mInteractiveId = id; + void write(Style* style, Utf8Span text) override + { + Range range = mBook->addContent(text); - return &style; - } + writeImpl(static_cast(style), range.first, range.second); + } - void write (Style * style, Utf8Span text) override - { - Range range = mBook->addContent (text); + intptr_t addContent(Utf8Span text, bool select) override + { + add_partial_text(); - writeImpl (static_cast (style), range.first, range.second); - } + Contents::iterator i = mBook->mContents.insert(mBook->mContents.end(), Content(text.first, text.second)); - intptr_t addContent (Utf8Span text, bool select) override - { - add_partial_text(); + if (select) + mCurrentContent = &(*i); - Contents::iterator i = mBook->mContents.insert (mBook->mContents.end (), Content (text.first, text.second)); + return reinterpret_cast(&(*i)); + } - if (select) - mCurrentContent = &(*i); + void selectContent(intptr_t contentHandle) override + { + add_partial_text(); - return reinterpret_cast (&(*i)); - } + mCurrentContent = reinterpret_cast(contentHandle); + } - void selectContent (intptr_t contentHandle) override - { - add_partial_text(); + void write(Style* style, size_t begin, size_t end) override + { + assert(mCurrentContent != nullptr); + assert(end <= mCurrentContent->size()); + assert(begin <= mCurrentContent->size()); - mCurrentContent = reinterpret_cast (contentHandle); - } + Utf8Point begin_ = mCurrentContent->data() + begin; + Utf8Point end_ = mCurrentContent->data() + end; - void write (Style * style, size_t begin, size_t end) override - { - assert (mCurrentContent != nullptr); - assert (end <= mCurrentContent->size ()); - assert (begin <= mCurrentContent->size ()); + writeImpl(static_cast(style), begin_, end_); + } - Utf8Point begin_ = mCurrentContent->data() + begin; - Utf8Point end_ = mCurrentContent->data() + end; + void lineBreak(float margin) override + { + assert(margin == 0); // TODO: figure out proper behavior here... - writeImpl (static_cast (style), begin_, end_); - } + add_partial_text(); - void lineBreak (float margin) override - { - assert (margin == 0); //TODO: figure out proper behavior here... + mRun = nullptr; + mLine = nullptr; + } - add_partial_text(); + void sectionBreak(int margin) override + { + add_partial_text(); - mRun = nullptr; - mLine = nullptr; - } + if (mBook->mSections.size() > 0) + { + mRun = nullptr; + mLine = nullptr; + mSection = nullptr; - void sectionBreak (int margin) override - { - add_partial_text(); + if (mBook->mRect.bottom < (mBook->mSections.back().mRect.bottom + margin)) + mBook->mRect.bottom = (mBook->mSections.back().mRect.bottom + margin); + } + } - if (mBook->mSections.size () > 0) + void setSectionAlignment(Alignment sectionAlignment) override { - mRun = nullptr; - mLine = nullptr; - mSection = nullptr; + add_partial_text(); - if (mBook->mRect.bottom < (mBook->mSections.back ().mRect.bottom + margin)) - mBook->mRect.bottom = (mBook->mSections.back ().mRect.bottom + margin); + if (mSection != nullptr) + mSectionAlignment.back() = sectionAlignment; + mCurrentAlignment = sectionAlignment; } - } - void setSectionAlignment (Alignment sectionAlignment) override - { - add_partial_text(); + TypesetBook::Ptr complete() override + { + int curPageStart = 0; + int curPageStop = 0; - if (mSection != nullptr) - mSectionAlignment.back () = sectionAlignment; - mCurrentAlignment = sectionAlignment; - } + add_partial_text(); - TypesetBook::Ptr complete () override - { - int curPageStart = 0; - int curPageStop = 0; + std::vector::iterator sa = mSectionAlignment.begin(); + for (Sections::iterator i = mBook->mSections.begin(); i != mBook->mSections.end(); ++i, ++sa) + { + // apply alignment to individual lines... + for (Lines::iterator j = i->mLines.begin(); j != i->mLines.end(); ++j) + { + int width = j->mRect.width(); + int excess = mPageWidth - width; - add_partial_text(); + switch (*sa) + { + default: + case AlignLeft: + j->mRect.left = 0; + break; + case AlignCenter: + j->mRect.left = excess / 2; + break; + case AlignRight: + j->mRect.left = excess; + break; + } - std::vector ::iterator sa = mSectionAlignment.begin (); - for (Sections::iterator i = mBook->mSections.begin (); i != mBook->mSections.end (); ++i, ++sa) - { - // apply alignment to individual lines... - for (Lines::iterator j = i->mLines.begin (); j != i->mLines.end (); ++j) - { - int width = j->mRect.width (); - int excess = mPageWidth - width; + j->mRect.right = j->mRect.left + width; + } - switch (*sa) + if (curPageStop == curPageStart) { - default: - case AlignLeft: j->mRect.left = 0; break; - case AlignCenter: j->mRect.left = excess/2; break; - case AlignRight: j->mRect.left = excess; break; + curPageStart = i->mRect.top; + curPageStop = i->mRect.top; } - j->mRect.right = j->mRect.left + width; - } - - if (curPageStop == curPageStart) - { - curPageStart = i->mRect.top; - curPageStop = i->mRect.top; - } - - int spaceLeft = mPageHeight - (curPageStop - curPageStart); - int sectionHeight = i->mRect.height (); + int spaceLeft = mPageHeight - (curPageStop - curPageStart); + int sectionHeight = i->mRect.height(); - // This is NOT equal to i->mRect.height(), which doesn't account for section breaks. - int spaceRequired = (i->mRect.bottom - curPageStop); - if (curPageStart == curPageStop) // If this is a new page, the section break is not needed - spaceRequired = i->mRect.height(); + // This is NOT equal to i->mRect.height(), which doesn't account for section breaks. + int spaceRequired = (i->mRect.bottom - curPageStop); + if (curPageStart == curPageStop) // If this is a new page, the section break is not needed + spaceRequired = i->mRect.height(); - if (spaceRequired <= mPageHeight) - { - if (spaceRequired > spaceLeft) + if (spaceRequired <= mPageHeight) { - // The section won't completely fit on the current page. Finish the current page and start a new one. - assert (curPageStart != curPageStop); + if (spaceRequired > spaceLeft) + { + // The section won't completely fit on the current page. Finish the current page and start a new + // one. + assert(curPageStart != curPageStop); - mBook->mPages.push_back (Page (curPageStart, curPageStop)); + mBook->mPages.push_back(Page(curPageStart, curPageStop)); - curPageStart = i->mRect.top; - curPageStop = i->mRect.bottom; + curPageStart = i->mRect.top; + curPageStop = i->mRect.bottom; + } + else + curPageStop = i->mRect.bottom; } else - curPageStop = i->mRect.bottom; - } - else - { - // The section won't completely fit on the current page. Finish the current page and start a new one. - mBook->mPages.push_back (Page (curPageStart, curPageStop)); + { + // The section won't completely fit on the current page. Finish the current page and start a new + // one. + mBook->mPages.push_back(Page(curPageStart, curPageStop)); - curPageStart = i->mRect.top; - curPageStop = i->mRect.bottom; + curPageStart = i->mRect.top; + curPageStop = i->mRect.bottom; - //split section - int sectionHeightLeft = sectionHeight; - while (sectionHeightLeft >= mPageHeight) - { - // Adjust to the top of the first line that does not fit on the current page anymore - int splitPos = curPageStop; - for (Lines::iterator j = i->mLines.begin (); j != i->mLines.end (); ++j) + // split section + int sectionHeightLeft = sectionHeight; + while (sectionHeightLeft >= mPageHeight) { - if (j->mRect.bottom > curPageStart + mPageHeight) + // Adjust to the top of the first line that does not fit on the current page anymore + int splitPos = curPageStop; + for (Lines::iterator j = i->mLines.begin(); j != i->mLines.end(); ++j) { - splitPos = j->mRect.top; - break; + if (j->mRect.bottom > curPageStart + mPageHeight) + { + splitPos = j->mRect.top; + break; + } } - } - mBook->mPages.push_back (Page (curPageStart, splitPos)); - curPageStart = splitPos; - curPageStop = splitPos; + mBook->mPages.push_back(Page(curPageStart, splitPos)); + curPageStart = splitPos; + curPageStop = splitPos; - sectionHeightLeft = (i->mRect.bottom - splitPos); + sectionHeightLeft = (i->mRect.bottom - splitPos); + } + curPageStop = i->mRect.bottom; } - curPageStop = i->mRect.bottom; } - } - if (curPageStart != curPageStop) - mBook->mPages.push_back (Page (curPageStart, curPageStop)); - - return mBook; - } + if (curPageStart != curPageStop) + mBook->mPages.push_back(Page(curPageStart, curPageStop)); - void writeImpl (StyleImpl * style, Utf8Stream::Point _begin, Utf8Stream::Point _end) - { - Utf8Stream stream (_begin, _end); + return mBook; + } - while (!stream.eof ()) + void writeImpl(StyleImpl* style, Utf8Stream::Point _begin, Utf8Stream::Point _end) { - if (ucsLineBreak (stream.peek ())) + Utf8Stream stream(_begin, _end); + + while (!stream.eof()) { - add_partial_text(); - stream.consume (); - mLine = nullptr, mRun = nullptr; - continue; - } + if (ucsLineBreak(stream.peek())) + { + add_partial_text(); + stream.consume(); + mLine = nullptr; + mRun = nullptr; + continue; + } - if (ucsBreakingSpace (stream.peek ()) && !mPartialWord.empty()) - add_partial_text(); + if (ucsBreakingSpace(stream.peek()) && !mPartialWord.empty()) + add_partial_text(); - int word_width = 0; - int space_width = 0; + int word_width = 0; + int space_width = 0; - Utf8Stream::Point lead = stream.current (); + Utf8Stream::Point lead = stream.current(); - while (!stream.eof () && !ucsLineBreak (stream.peek ()) && ucsBreakingSpace (stream.peek ())) - { - MWGui::GlyphInfo info = GlyphInfo(style->mFont, stream.peek()); - if (info.charFound) - space_width += static_cast(info.advance + info.bearingX); - stream.consume (); - } + while (!stream.eof() && !ucsLineBreak(stream.peek()) && ucsBreakingSpace(stream.peek())) + { + MWGui::GlyphInfo info = GlyphInfo(style->mFont, stream.peek()); + if (info.charFound) + space_width += static_cast(info.advance + info.bearingX); + stream.consume(); + } - Utf8Stream::Point origin = stream.current (); + Utf8Stream::Point origin = stream.current(); - while (!stream.eof () && !ucsLineBreak (stream.peek ()) && !ucsBreakingSpace (stream.peek ())) - { - MWGui::GlyphInfo info = GlyphInfo(style->mFont, stream.peek()); - if (info.charFound) - word_width += static_cast(info.advance + info.bearingX); - stream.consume (); - } + while (!stream.eof() && !ucsLineBreak(stream.peek()) && !ucsBreakingSpace(stream.peek())) + { + MWGui::GlyphInfo info = GlyphInfo(style->mFont, stream.peek()); + if (info.charFound) + word_width += static_cast(info.advance + info.bearingX); + stream.consume(); + } - Utf8Stream::Point extent = stream.current (); + Utf8Stream::Point extent = stream.current(); - if (lead == extent) - break; + if (lead == extent) + break; - if ( lead != origin ) - mPartialWhitespace.emplace_back(style, lead, origin, space_width); - if ( origin != extent ) - mPartialWord.emplace_back(style, origin, extent, word_width); + if (lead != origin) + mPartialWhitespace.emplace_back(style, lead, origin, space_width); + if (origin != extent) + mPartialWord.emplace_back(style, origin, extent, word_width); + } } - } - void add_partial_text () - { - if (mPartialWhitespace.empty() && mPartialWord.empty()) - return; + void add_partial_text() + { + if (mPartialWhitespace.empty() && mPartialWord.empty()) + return; - int fontHeight = MWBase::Environment::get().getWindowManager()->getFontHeight(); - int space_width = 0; - int word_width = 0; + const int fontHeight = Settings::gui().mFontSize; + int space_width = 0; + int word_width = 0; - for (PartialTextConstIterator i = mPartialWhitespace.begin (); i != mPartialWhitespace.end (); ++i) - space_width += i->mWidth; - for (PartialTextConstIterator i = mPartialWord.begin (); i != mPartialWord.end (); ++i) - word_width += i->mWidth; + for (PartialTextConstIterator i = mPartialWhitespace.begin(); i != mPartialWhitespace.end(); ++i) + space_width += i->mWidth; + for (PartialTextConstIterator i = mPartialWord.begin(); i != mPartialWord.end(); ++i) + word_width += i->mWidth; - int left = mLine ? mLine->mRect.right : 0; + int left = mLine ? mLine->mRect.right : 0; - if (left + space_width + word_width > mPageWidth) - { - mLine = nullptr, mRun = nullptr, left = 0; - } - else - { - for (PartialTextConstIterator i = mPartialWhitespace.begin (); i != mPartialWhitespace.end (); ++i) + if (left + space_width + word_width > mPageWidth) { - int top = mLine ? mLine->mRect.top : mBook->mRect.bottom; + mLine = nullptr; + mRun = nullptr; + left = 0; + } + else + { + for (PartialTextConstIterator i = mPartialWhitespace.begin(); i != mPartialWhitespace.end(); ++i) + { + int top = mLine ? mLine->mRect.top : mBook->mRect.bottom; - append_run ( i->mStyle, i->mBegin, i->mEnd, 0, left + i->mWidth, top + fontHeight); + append_run(i->mStyle, i->mBegin, i->mEnd, 0, left + i->mWidth, top + fontHeight); - left = mLine->mRect.right; + left = mLine->mRect.right; + } } - } - - for (PartialTextConstIterator i = mPartialWord.begin (); i != mPartialWord.end (); ++i) - { - int top = mLine ? mLine->mRect.top : mBook->mRect.bottom; - append_run (i->mStyle, i->mBegin, i->mEnd, i->mEnd - i->mBegin, left + i->mWidth, top + fontHeight); + for (PartialTextConstIterator i = mPartialWord.begin(); i != mPartialWord.end(); ++i) + { + int top = mLine ? mLine->mRect.top : mBook->mRect.bottom; - left = mLine->mRect.right; - } + append_run(i->mStyle, i->mBegin, i->mEnd, i->mEnd - i->mBegin, left + i->mWidth, top + fontHeight); - mPartialWhitespace.clear(); - mPartialWord.clear(); - } + left = mLine->mRect.right; + } - void append_run (StyleImpl * style, Utf8Stream::Point begin, Utf8Stream::Point end, int pc, int right, int bottom) - { - if (mSection == nullptr) - { - mBook->mSections.push_back (Section ()); - mSection = &mBook->mSections.back (); - mSection->mRect = MyGUI::IntRect (0, mBook->mRect.bottom, 0, mBook->mRect.bottom); - mSectionAlignment.push_back (mCurrentAlignment); + mPartialWhitespace.clear(); + mPartialWord.clear(); } - if (mLine == nullptr) + void append_run(StyleImpl* style, Utf8Stream::Point begin, Utf8Stream::Point end, int pc, int right, int bottom) { - mSection->mLines.push_back (Line ()); - mLine = &mSection->mLines.back (); - mLine->mRect = MyGUI::IntRect (0, mSection->mRect.bottom, 0, mBook->mRect.bottom); - } + if (mSection == nullptr) + { + mBook->mSections.push_back(Section()); + mSection = &mBook->mSections.back(); + mSection->mRect = MyGUI::IntRect(0, mBook->mRect.bottom, 0, mBook->mRect.bottom); + mSectionAlignment.push_back(mCurrentAlignment); + } + + if (mLine == nullptr) + { + mSection->mLines.push_back(Line()); + mLine = &mSection->mLines.back(); + mLine->mRect = MyGUI::IntRect(0, mSection->mRect.bottom, 0, mBook->mRect.bottom); + } - if (mBook->mRect.right < right) - mBook->mRect.right = right; + if (mBook->mRect.right < right) + mBook->mRect.right = right; - if (mBook->mRect.bottom < bottom) - mBook->mRect.bottom = bottom; + if (mBook->mRect.bottom < bottom) + mBook->mRect.bottom = bottom; - if (mSection->mRect.right < right) - mSection->mRect.right = right; + if (mSection->mRect.right < right) + mSection->mRect.right = right; - if (mSection->mRect.bottom < bottom) - mSection->mRect.bottom = bottom; + if (mSection->mRect.bottom < bottom) + mSection->mRect.bottom = bottom; - if (mLine->mRect.right < right) - mLine->mRect.right = right; + if (mLine->mRect.right < right) + mLine->mRect.right = right; - if (mLine->mRect.bottom < bottom) - mLine->mRect.bottom = bottom; + if (mLine->mRect.bottom < bottom) + mLine->mRect.bottom = bottom; - if (mRun == nullptr || mRun->mStyle != style || mRun->mRange.second != begin) - { - int left = mRun ? mRun->mRight : mLine->mRect.left; - - mLine->mRuns.push_back (Run ()); - mRun = &mLine->mRuns.back (); - mRun->mStyle = style; - mRun->mLeft = left; - mRun->mRight = right; - mRun->mRange.first = begin; - mRun->mRange.second = end; - mRun->mPrintableChars = pc; - //Run->Locale = Locale; - } - else - { - mRun->mRight = right; - mRun->mRange.second = end; - mRun->mPrintableChars += pc; + if (mRun == nullptr || mRun->mStyle != style || mRun->mRange.second != begin) + { + int left = mRun ? mRun->mRight : mLine->mRect.left; + + mLine->mRuns.push_back(Run()); + mRun = &mLine->mRuns.back(); + mRun->mStyle = style; + mRun->mLeft = left; + mRun->mRight = right; + mRun->mRange.first = begin; + mRun->mRange.second = end; + mRun->mPrintableChars = pc; + // Run->Locale = Locale; + } + else + { + mRun->mRight = right; + mRun->mRange.second = end; + mRun->mPrintableChars += pc; + } } - } -}; + }; -BookTypesetter::Ptr BookTypesetter::create (int pageWidth, int pageHeight) -{ - return std::make_shared (pageWidth, pageHeight); -} + BookTypesetter::Ptr BookTypesetter::create(int pageWidth, int pageHeight) + { + return std::make_shared(pageWidth, pageHeight); + } -namespace -{ - struct RenderXform + namespace { - public: + struct RenderXform + { + public: + float clipTop; + float clipLeft; + float clipRight; + float clipBottom; + + float absoluteLeft; + float absoluteTop; + float leftOffset; + float topOffset; + + float pixScaleX; + float pixScaleY; + float hOffset; + float vOffset; + + RenderXform(MyGUI::ICroppedRectangle* croppedParent, MyGUI::RenderTargetInfo const& renderTargetInfo) + { + clipTop = static_cast(croppedParent->_getMarginTop()); + clipLeft = static_cast(croppedParent->_getMarginLeft()); + clipRight = static_cast(croppedParent->getWidth() - croppedParent->_getMarginRight()); + clipBottom = static_cast(croppedParent->getHeight() - croppedParent->_getMarginBottom()); + + absoluteLeft = static_cast(croppedParent->getAbsoluteLeft()); + absoluteTop = static_cast(croppedParent->getAbsoluteTop()); + leftOffset = static_cast(renderTargetInfo.leftOffset); + topOffset = static_cast(renderTargetInfo.topOffset); + + pixScaleX = renderTargetInfo.pixScaleX; + pixScaleY = renderTargetInfo.pixScaleY; + hOffset = renderTargetInfo.hOffset; + vOffset = renderTargetInfo.vOffset; + } - float clipTop; - float clipLeft; - float clipRight; - float clipBottom; + bool clip(MyGUI::FloatRect& vr, MyGUI::FloatRect& tr) + { + if (vr.bottom <= clipTop || vr.right <= clipLeft || vr.left >= clipRight || vr.top >= clipBottom) + return false; - float absoluteLeft; - float absoluteTop; - float leftOffset; - float topOffset; + if (vr.top < clipTop) + { + tr.top += tr.height() * (clipTop - vr.top) / vr.height(); + vr.top = clipTop; + } - float pixScaleX; - float pixScaleY; - float hOffset; - float vOffset; + if (vr.left < clipLeft) + { + tr.left += tr.width() * (clipLeft - vr.left) / vr.width(); + vr.left = clipLeft; + } - RenderXform (MyGUI::ICroppedRectangle* croppedParent, MyGUI::RenderTargetInfo const & renderTargetInfo) - { - clipTop = static_cast(croppedParent->_getMarginTop()); - clipLeft = static_cast(croppedParent->_getMarginLeft ()); - clipRight = static_cast(croppedParent->getWidth () - croppedParent->_getMarginRight ()); - clipBottom = static_cast(croppedParent->getHeight() - croppedParent->_getMarginBottom()); - - absoluteLeft = static_cast(croppedParent->getAbsoluteLeft()); - absoluteTop = static_cast(croppedParent->getAbsoluteTop()); - leftOffset = static_cast(renderTargetInfo.leftOffset); - topOffset = static_cast(renderTargetInfo.topOffset); - - pixScaleX = renderTargetInfo.pixScaleX; - pixScaleY = renderTargetInfo.pixScaleY; - hOffset = renderTargetInfo.hOffset; - vOffset = renderTargetInfo.vOffset; - } + if (vr.right > clipRight) + { + tr.right -= tr.width() * (vr.right - clipRight) / vr.width(); + vr.right = clipRight; + } - bool clip (MyGUI::FloatRect & vr, MyGUI::FloatRect & tr) - { - if (vr.bottom <= clipTop || vr.right <= clipLeft || - vr.left >= clipRight || vr.top >= clipBottom ) - return false; + if (vr.bottom > clipBottom) + { + tr.bottom -= tr.height() * (vr.bottom - clipBottom) / vr.height(); + vr.bottom = clipBottom; + } - if (vr.top < clipTop) - { - tr.top += tr.height () * (clipTop - vr.top) / vr.height (); - vr.top = clipTop; + return true; } - if (vr.left < clipLeft) + MyGUI::FloatPoint operator()(MyGUI::FloatPoint pt) { - tr.left += tr.width () * (clipLeft - vr.left) / vr.width (); - vr.left = clipLeft; - } + pt.left = absoluteLeft - leftOffset + pt.left; + pt.top = absoluteTop - topOffset + pt.top; - if (vr.right > clipRight) - { - tr.right -= tr.width () * (vr.right - clipRight) / vr.width (); - vr.right = clipRight; + pt.left = +(((pixScaleX * pt.left + hOffset) * 2.0f) - 1.0f); + pt.top = -(((pixScaleY * pt.top + vOffset) * 2.0f) - 1.0f); + + return pt; } + }; - if (vr.bottom > clipBottom) + struct GlyphStream + { + float mZ; + uint32_t mC; + MyGUI::IFont* mFont; + MyGUI::FloatPoint mOrigin; + MyGUI::FloatPoint mCursor; + MyGUI::Vertex* mVertices; + RenderXform mRenderXform; + MyGUI::VertexColourType mVertexColourType; + + GlyphStream(MyGUI::IFont* font, float left, float top, float Z, MyGUI::Vertex* vertices, + RenderXform const& renderXform) + : mZ(Z) + , mC(0) + , mFont(font) + , mOrigin(left, top) + , mVertices(vertices) + , mRenderXform(renderXform) { - tr.bottom -= tr.height () * (vr.bottom - clipBottom) / vr.height (); - vr.bottom = clipBottom; + assert(font != nullptr); + mVertexColourType = MyGUI::RenderManager::getInstance().getVertexFormat(); } - return true; - } + ~GlyphStream() = default; - MyGUI::FloatPoint operator () (MyGUI::FloatPoint pt) - { - pt.left = absoluteLeft - leftOffset + pt.left; - pt.top = absoluteTop - topOffset + pt.top; + MyGUI::Vertex* end() const { return mVertices; } - pt.left = +(((pixScaleX * pt.left + hOffset) * 2.0f) - 1.0f); - pt.top = -(((pixScaleY * pt.top + vOffset) * 2.0f) - 1.0f); - - return pt; - } - }; - - struct GlyphStream - { - float mZ; - uint32_t mC; - MyGUI::IFont* mFont; - MyGUI::FloatPoint mOrigin; - MyGUI::FloatPoint mCursor; - MyGUI::Vertex* mVertices; - RenderXform mRenderXform; - MyGUI::VertexColourType mVertexColourType; - - GlyphStream (MyGUI::IFont* font, float left, float top, float Z, - MyGUI::Vertex* vertices, RenderXform const & renderXform) : - mZ(Z), - mC(0), mFont (font), mOrigin (left, top), - mVertices (vertices), - mRenderXform (renderXform) - { - assert(font != nullptr); - mVertexColourType = MyGUI::RenderManager::getInstance().getVertexFormat(); - } - - ~GlyphStream () - { - } - - MyGUI::Vertex* end () const { return mVertices; } - - void reset (float left, float top, MyGUI::Colour colour) - { - mC = MyGUI::texture_utility::toColourARGB(colour) | 0xFF000000; - MyGUI::texture_utility::convertColour(mC, mVertexColourType); - - mCursor.left = mOrigin.left + left; - mCursor.top = mOrigin.top + top; - } - - void emitGlyph (wchar_t ch) - { - MWGui::GlyphInfo info = GlyphInfo(mFont, ch); + void reset(float left, float top, MyGUI::Colour colour) + { + mC = MyGUI::texture_utility::toNativeColour(colour, MyGUI::VertexColourType::ColourARGB) | 0xFF000000; + MyGUI::texture_utility::convertColour(mC, mVertexColourType); - if (!info.charFound) - return; + mCursor.left = mOrigin.left + left; + mCursor.top = mOrigin.top + top; + } - MyGUI::FloatRect vr; + void emitGlyph(wchar_t ch) + { + MWGui::GlyphInfo info = GlyphInfo(mFont, ch); - vr.left = mCursor.left + info.bearingX; - vr.top = mCursor.top + info.bearingY; - vr.right = vr.left + info.width; - vr.bottom = vr.top + info.height; + if (!info.charFound) + return; - MyGUI::FloatRect tr = info.uvRect; + MyGUI::FloatRect vr; - if (mRenderXform.clip (vr, tr)) - quad (vr, tr); + vr.left = mCursor.left + info.bearingX; + vr.top = mCursor.top + info.bearingY; + vr.right = vr.left + info.width; + vr.bottom = vr.top + info.height; - mCursor.left += static_cast(info.bearingX + info.advance); - } + MyGUI::FloatRect tr = info.uvRect; - void emitSpace (wchar_t ch) - { - MWGui::GlyphInfo info = GlyphInfo(mFont, ch); + if (mRenderXform.clip(vr, tr)) + quad(vr, tr); - if (info.charFound) mCursor.left += static_cast(info.bearingX + info.advance); - } - - private: + } - void quad (const MyGUI::FloatRect& vr, const MyGUI::FloatRect& tr) - { - vertex (vr.left, vr.top, tr.left, tr.top); - vertex (vr.right, vr.top, tr.right, tr.top); - vertex (vr.left, vr.bottom, tr.left, tr.bottom); - vertex (vr.right, vr.top, tr.right, tr.top); - vertex (vr.left, vr.bottom, tr.left, tr.bottom); - vertex (vr.right, vr.bottom, tr.right, tr.bottom); - } + void emitSpace(wchar_t ch) + { + MWGui::GlyphInfo info = GlyphInfo(mFont, ch); - void vertex (float x, float y, float u, float v) - { - MyGUI::FloatPoint pt = mRenderXform (MyGUI::FloatPoint (x, y)); + if (info.charFound) + mCursor.left += static_cast(info.bearingX + info.advance); + } - mVertices->x = pt.left; - mVertices->y = pt.top ; - mVertices->z = mZ; - mVertices->u = u; - mVertices->v = v; - mVertices->colour = mC; + private: + void quad(const MyGUI::FloatRect& vr, const MyGUI::FloatRect& tr) + { + vertex(vr.left, vr.top, tr.left, tr.top); + vertex(vr.right, vr.top, tr.right, tr.top); + vertex(vr.left, vr.bottom, tr.left, tr.bottom); + vertex(vr.right, vr.top, tr.right, tr.top); + vertex(vr.left, vr.bottom, tr.left, tr.bottom); + vertex(vr.right, vr.bottom, tr.right, tr.bottom); + } - ++mVertices; - } - }; -} + void vertex(float x, float y, float u, float v) + { + MyGUI::FloatPoint pt = mRenderXform(MyGUI::FloatPoint(x, y)); -class PageDisplay final : public MyGUI::ISubWidgetText -{ - MYGUI_RTTI_DERIVED(PageDisplay) -protected: + mVertices->x = pt.left; + mVertices->y = pt.top; + mVertices->z = mZ; + mVertices->u = u; + mVertices->v = v; + mVertices->colour = mC; - typedef TypesetBookImpl::Section Section; - typedef TypesetBookImpl::Line Line; - typedef TypesetBookImpl::Run Run; - bool mIsPageReset; - size_t mPage; + ++mVertices; + } + }; + } - struct TextFormat : ISubWidget + class PageDisplay final : public MyGUI::ISubWidgetText { - typedef MyGUI::IFont* Id; - - Id mFont; - int mCountVertex; - MyGUI::ITexture* mTexture; - MyGUI::RenderItem* mRenderItem; - PageDisplay * mDisplay; - - TextFormat (MyGUI::IFont* id, PageDisplay * display) : - mFont (id), - mCountVertex (0), - mTexture (nullptr), - mRenderItem (nullptr), - mDisplay (display) + MYGUI_RTTI_DERIVED(PageDisplay) + protected: + typedef TypesetBookImpl::Section Section; + typedef TypesetBookImpl::Line Line; + typedef TypesetBookImpl::Run Run; + bool mIsPageReset; + size_t mPage; + + struct TextFormat : ISubWidget { - } - - void createDrawItem (MyGUI::ILayerNode* node) - { - assert (mRenderItem == nullptr); - - if (mTexture != nullptr) + typedef MyGUI::IFont* Id; + + Id mFont; + int mCountVertex; + MyGUI::ITexture* mTexture; + MyGUI::RenderItem* mRenderItem; + PageDisplay* mDisplay; + + TextFormat(MyGUI::IFont* id, PageDisplay* display) + : mFont(id) + , mCountVertex(0) + , mTexture(nullptr) + , mRenderItem(nullptr) + , mDisplay(display) { - mRenderItem = node->addToRenderItem(mTexture, false, false); - mRenderItem->addDrawItem(this, mCountVertex); } - } - void destroyDrawItem (MyGUI::ILayerNode* node) - { - assert (mTexture != nullptr ? mRenderItem != nullptr : mRenderItem == nullptr); + void createDrawItem(MyGUI::ILayerNode* node) + { + assert(mRenderItem == nullptr); + + if (mTexture != nullptr) + { + mRenderItem = node->addToRenderItem(mTexture, false, false); + mRenderItem->addDrawItem(this, mCountVertex); + } + } - if (mTexture != nullptr) + void destroyDrawItem(MyGUI::ILayerNode* node) { - mRenderItem->removeDrawItem (this); - mRenderItem = nullptr; + assert(mTexture != nullptr ? mRenderItem != nullptr : mRenderItem == nullptr); + + if (mTexture != nullptr) + { + mRenderItem->removeDrawItem(this); + mRenderItem = nullptr; + } } - } - void doRender() override { mDisplay->doRender (*this); } + void doRender() override { mDisplay->doRender(*this); } - // this isn't really a sub-widget, its just a "drawitem" which - // should have its own interface - void createDrawItem(MyGUI::ITexture* _texture, MyGUI::ILayerNode* _node) override {} - void destroyDrawItem() override {} - }; + // this isn't really a sub-widget, its just a "drawitem" which + // should have its own interface + void createDrawItem(MyGUI::ITexture* _texture, MyGUI::ILayerNode* _node) override {} + void destroyDrawItem() override {} + }; - void resetPage() - { - mIsPageReset = true; - mPage = 0; - } + void resetPage() + { + mIsPageReset = true; + mPage = 0; + } - void setPage(size_t page) - { - mIsPageReset = false; - mPage = page; - } + void setPage(size_t page) + { + mIsPageReset = false; + mPage = page; + } - bool isPageDifferent(size_t page) - { - return mIsPageReset || (mPage != page); - } + bool isPageDifferent(size_t page) { return mIsPageReset || (mPage != page); } - std::optional getAdjustedPos(int left, int top, bool move = false) - { - if (!mBook) - return {}; - - if (mPage >= mBook->mPages.size()) - return {}; - - MyGUI::IntPoint pos (left, top); -#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) - // work around inconsistency in MyGUI where the mouse press coordinates aren't - // transformed by the current Layer (even though mouse *move* events are). - if(!move) - pos = mNode->getLayer()->getPosition(left, top); -#endif - pos.left -= mCroppedParent->getAbsoluteLeft (); - pos.top -= mCroppedParent->getAbsoluteTop (); - pos.top += mViewTop; - return pos; - } + std::optional getAdjustedPos(int left, int top, bool move = false) + { + if (!mBook) + return {}; -public: + if (mPage >= mBook->mPages.size()) + return {}; - typedef TypesetBookImpl::StyleImpl Style; - typedef std::map > ActiveTextFormats; + MyGUI::IntPoint pos(left, top); + pos.left -= mCroppedParent->getAbsoluteLeft(); + pos.top -= mCroppedParent->getAbsoluteTop(); + pos.top += mViewTop; + return pos; + } - int mViewTop; - int mViewBottom; + public: + typedef TypesetBookImpl::StyleImpl Style; + typedef std::map> ActiveTextFormats; - Style* mFocusItem; - bool mItemActive; - MyGUI::MouseButton mLastDown; - std::function mLinkClicked; + int mViewTop; + int mViewBottom; + Style* mFocusItem; + bool mItemActive; + MyGUI::MouseButton mLastDown; + std::function mLinkClicked; - std::shared_ptr mBook; + std::shared_ptr mBook; - MyGUI::ILayerNode* mNode; - ActiveTextFormats mActiveTextFormats; + MyGUI::ILayerNode* mNode; + ActiveTextFormats mActiveTextFormats; - PageDisplay () - { - resetPage (); - mViewTop = 0; - mViewBottom = 0; - mFocusItem = nullptr; - mItemActive = false; - mNode = nullptr; - } + PageDisplay() + { + resetPage(); + mViewTop = 0; + mViewBottom = 0; + mFocusItem = nullptr; + mItemActive = false; + mNode = nullptr; + } - void dirtyFocusItem () - { - if (mFocusItem != nullptr) + void dirtyFocusItem() { - MyGUI::IFont* Font = mBook->affectedFont (mFocusItem); + if (mFocusItem != nullptr) + { + MyGUI::IFont* Font = mBook->affectedFont(mFocusItem); - ActiveTextFormats::iterator i = mActiveTextFormats.find (Font); + ActiveTextFormats::iterator i = mActiveTextFormats.find(Font); - if (mNode) - mNode->outOfDate (i->second->mRenderItem); + if (mNode && i != mActiveTextFormats.end()) + mNode->outOfDate(i->second->mRenderItem); + } } - } - - void onMouseLostFocus () - { - if (!mBook) - return; - if (mPage >= mBook->mPages.size()) - return; + void onMouseLostFocus() + { + if (!mBook) + return; - dirtyFocusItem (); + if (mPage >= mBook->mPages.size()) + return; - mFocusItem = nullptr; - mItemActive = false; - } + dirtyFocusItem(); - void onMouseMove (int left, int top) - { - Style * hit = nullptr; - if(auto pos = getAdjustedPos(left, top, true)) - if(pos->top <= mViewBottom) - hit = mBook->hitTestWithMargin (pos->left, pos->top); + mFocusItem = nullptr; + mItemActive = false; + } - if (mLastDown == MyGUI::MouseButton::None) + void onMouseMove(int left, int top) { - if (hit != mFocusItem) + Style* hit = nullptr; + if (auto pos = getAdjustedPos(left, top, true)) + if (pos->top <= mViewBottom) + hit = mBook->hitTestWithMargin(pos->left, pos->top); + + if (mLastDown == MyGUI::MouseButton::None) { - dirtyFocusItem (); + if (hit != mFocusItem) + { + dirtyFocusItem(); - mFocusItem = hit; - mItemActive = false; + mFocusItem = hit; + mItemActive = false; - dirtyFocusItem (); + dirtyFocusItem(); + } } - } - else - if (mFocusItem != nullptr) - { - bool newItemActive = hit == mFocusItem; - - if (newItemActive != mItemActive) + else if (mFocusItem != nullptr) { - mItemActive = newItemActive; + bool newItemActive = hit == mFocusItem; + + if (newItemActive != mItemActive) + { + mItemActive = newItemActive; - dirtyFocusItem (); + dirtyFocusItem(); + } } } - } - void onMouseButtonPressed (int left, int top, MyGUI::MouseButton id) - { - auto pos = getAdjustedPos(left, top); - - if (pos && mLastDown == MyGUI::MouseButton::None) + void onMouseButtonPressed(int left, int top, MyGUI::MouseButton id) { - mFocusItem = pos->top <= mViewBottom ? mBook->hitTestWithMargin (pos->left, pos->top) : nullptr; - mItemActive = true; + auto pos = getAdjustedPos(left, top); - dirtyFocusItem (); + if (pos && mLastDown == MyGUI::MouseButton::None) + { + mFocusItem = pos->top <= mViewBottom ? mBook->hitTestWithMargin(pos->left, pos->top) : nullptr; + mItemActive = true; - mLastDown = id; - } - } + dirtyFocusItem(); - void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id) - { - auto pos = getAdjustedPos(left, top); + mLastDown = id; + } + } - if (pos && mLastDown == id) + void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id) { - Style * item = pos->top <= mViewBottom ? mBook->hitTestWithMargin (pos->left, pos->top) : nullptr; + auto pos = getAdjustedPos(left, top); - bool clicked = mFocusItem == item; + if (pos && mLastDown == id) + { + Style* item = pos->top <= mViewBottom ? mBook->hitTestWithMargin(pos->left, pos->top) : nullptr; - mItemActive = false; + bool clicked = mFocusItem == item; - dirtyFocusItem (); + mItemActive = false; - mLastDown = MyGUI::MouseButton::None; + dirtyFocusItem(); - if (clicked && mLinkClicked && item && item->mInteractiveId != 0) - mLinkClicked (item->mInteractiveId); - } - } + mLastDown = MyGUI::MouseButton::None; - void showPage (TypesetBook::Ptr book, size_t newPage) - { - std::shared_ptr newBook = std::dynamic_pointer_cast (book); + if (clicked && mLinkClicked && item && item->mInteractiveId != 0) + mLinkClicked(item->mInteractiveId); + } + } - if (mBook != newBook) + void showPage(TypesetBook::Ptr book, size_t newPage) { - mFocusItem = nullptr; - mItemActive = 0; + std::shared_ptr newBook = std::dynamic_pointer_cast(book); - for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) + if (mBook != newBook) { - if (mNode != nullptr) - i->second->destroyDrawItem (mNode); - i->second.reset(); - } - - mActiveTextFormats.clear (); + mFocusItem = nullptr; + mItemActive = 0; - if (newBook != nullptr) - { - createActiveFormats (newBook); + for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) + { + if (mNode != nullptr && i->second != nullptr) + i->second->destroyDrawItem(mNode); + i->second.reset(); + } - mBook = newBook; - setPage (newPage); + mActiveTextFormats.clear(); - if (newPage < mBook->mPages.size ()) + if (newBook != nullptr) { - mViewTop = mBook->mPages [newPage].first; - mViewBottom = mBook->mPages [newPage].second; + createActiveFormats(newBook); + + mBook = std::move(newBook); + setPage(newPage); + + if (newPage < mBook->mPages.size()) + { + mViewTop = mBook->mPages[newPage].first; + mViewBottom = mBook->mPages[newPage].second; + } + else + { + mViewTop = 0; + mViewBottom = 0; + } } else { + mBook.reset(); + resetPage(); mViewTop = 0; mViewBottom = 0; } } - else + else if (mBook && isPageDifferent(newPage)) { - mBook.reset (); - resetPage (); - mViewTop = 0; - mViewBottom = 0; - } - } - else - if (mBook && isPageDifferent (newPage)) - { - if (mNode != nullptr) - for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) - mNode->outOfDate(i->second->mRenderItem); + if (mNode != nullptr) + for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) + mNode->outOfDate(i->second->mRenderItem); - setPage (newPage); + setPage(newPage); - if (newPage < mBook->mPages.size ()) - { - mViewTop = mBook->mPages [newPage].first; - mViewBottom = mBook->mPages [newPage].second; - } - else - { - mViewTop = 0; - mViewBottom = 0; + if (newPage < mBook->mPages.size()) + { + mViewTop = mBook->mPages[newPage].first; + mViewBottom = mBook->mPages[newPage].second; + } + else + { + mViewTop = 0; + mViewBottom = 0; + } } } - } - - struct CreateActiveFormat - { - PageDisplay * this_; - - CreateActiveFormat (PageDisplay * this_) : this_ (this_) {} - void operator () (Section const & section, Line const & line, Run const & run) const + struct CreateActiveFormat { - MyGUI::IFont* Font = run.mStyle->mFont; - - ActiveTextFormats::iterator j = this_->mActiveTextFormats.find (Font); + PageDisplay* this_; - if (j == this_->mActiveTextFormats.end ()) + CreateActiveFormat(PageDisplay* this_) + : this_(this_) { - std::unique_ptr textFormat(new TextFormat (Font, this_)); - - textFormat->mTexture = Font->getTextureFont (); - - j = this_->mActiveTextFormats.insert (std::make_pair (Font, std::move(textFormat))).first; } - j->second->mCountVertex += run.mPrintableChars * 6; - } - }; + void operator()(Section const& section, Line const& line, Run const& run) const + { + MyGUI::IFont* Font = run.mStyle->mFont; - void createActiveFormats (std::shared_ptr newBook) - { - newBook->visitRuns (0, 0x7FFFFFFF, CreateActiveFormat (this)); + ActiveTextFormats::iterator j = this_->mActiveTextFormats.find(Font); - if (mNode != nullptr) - for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) - i->second->createDrawItem (mNode); - } + if (j == this_->mActiveTextFormats.end()) + { + auto textFormat = std::make_unique(Font, this_); - void setVisible (bool newVisible) override - { - if (mVisible == newVisible) - return; + textFormat->mTexture = Font->getTextureFont(); + + j = this_->mActiveTextFormats.insert(std::make_pair(Font, std::move(textFormat))).first; + } - mVisible = newVisible; + j->second->mCountVertex += run.mPrintableChars * 6; + } + }; - if (mVisible) + void createActiveFormats(std::shared_ptr newBook) { - // reset input state - mLastDown = MyGUI::MouseButton::None; - mFocusItem = nullptr; - mItemActive = 0; + newBook->visitRuns(0, 0x7FFFFFFF, CreateActiveFormat(this)); + + if (mNode != nullptr) + for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) + i->second->createDrawItem(mNode); } - if (nullptr != mNode) + void setVisible(bool newVisible) override { - for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) - mNode->outOfDate(i->second->mRenderItem); - } - } + if (mVisible == newVisible) + return; - void createDrawItem(MyGUI::ITexture* texture, MyGUI::ILayerNode* node) override - { - mNode = node; + mVisible = newVisible; - for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) - i->second->createDrawItem (node); - } + if (mVisible) + { + // reset input state + mLastDown = MyGUI::MouseButton::None; + mFocusItem = nullptr; + mItemActive = 0; + } - struct RenderRun - { - PageDisplay * this_; - GlyphStream &glyphStream; + if (nullptr != mNode) + { + for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) + mNode->outOfDate(i->second->mRenderItem); + } + } - RenderRun (PageDisplay * this_, GlyphStream &glyphStream) : - this_(this_), glyphStream (glyphStream) + void createDrawItem(MyGUI::ITexture* texture, MyGUI::ILayerNode* node) override { + mNode = node; + + for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) + i->second->createDrawItem(node); } - void operator () (Section const & section, Line const & line, Run const & run) const + struct RenderRun { - bool isActive = run.mStyle->mInteractiveId && (run.mStyle == this_->mFocusItem); + PageDisplay* this_; + GlyphStream& glyphStream; - MyGUI::Colour colour = isActive ? (this_->mItemActive ? run.mStyle->mActiveColour: run.mStyle->mHotColour) : run.mStyle->mNormalColour; + RenderRun(PageDisplay* this_, GlyphStream& glyphStream) + : this_(this_) + , glyphStream(glyphStream) + { + } - glyphStream.reset(static_cast(section.mRect.left + line.mRect.left + run.mLeft), static_cast(line.mRect.top), colour); + void operator()(Section const& section, Line const& line, Run const& run) const + { + bool isActive = run.mStyle->mInteractiveId && (run.mStyle == this_->mFocusItem); - Utf8Stream stream (run.mRange); + MyGUI::Colour colour = isActive + ? (this_->mItemActive ? run.mStyle->mActiveColour : run.mStyle->mHotColour) + : run.mStyle->mNormalColour; - while (!stream.eof ()) - { - Utf8Stream::UnicodeChar code_point = stream.consume (); + glyphStream.reset(static_cast(section.mRect.left + line.mRect.left + run.mLeft), + static_cast(line.mRect.top), colour); - if (ucsCarriageReturn (code_point)) - continue; + Utf8Stream stream(run.mRange); - if (!ucsSpace (code_point)) - glyphStream.emitGlyph (code_point); - else - glyphStream.emitSpace (code_point); - } - } - }; + while (!stream.eof()) + { + Utf8Stream::UnicodeChar code_point = stream.consume(); - /* - queue up rendering operations for this text format - */ - void doRender(TextFormat & textFormat) - { - if (!mVisible) - return; + if (ucsCarriageReturn(code_point)) + continue; - MyGUI::Vertex* vertices = textFormat.mRenderItem->getCurrentVertexBuffer(); + if (!ucsSpace(code_point)) + glyphStream.emitGlyph(code_point); + else + glyphStream.emitSpace(code_point); + } + } + }; - RenderXform renderXform (mCroppedParent, textFormat.mRenderItem->getRenderTarget()->getInfo()); + /* + queue up rendering operations for this text format + */ + void doRender(TextFormat& textFormat) + { + if (!mVisible) + return; - GlyphStream glyphStream(textFormat.mFont, static_cast(mCoord.left), static_cast(mCoord.top - mViewTop), - -1 /*mNode->getNodeDepth()*/, vertices, renderXform); + MyGUI::Vertex* vertices = textFormat.mRenderItem->getCurrentVertexBuffer(); - int visit_top = (std::max) (mViewTop, mViewTop + int (renderXform.clipTop )); - int visit_bottom = (std::min) (mViewBottom, mViewTop + int (renderXform.clipBottom)); + RenderXform renderXform(mCroppedParent, textFormat.mRenderItem->getRenderTarget()->getInfo()); - mBook->visitRuns (visit_top, visit_bottom, textFormat.mFont, RenderRun (this, glyphStream)); + float z = SceneUtil::AutoDepth::isReversed() ? 1.f : -1.f; - textFormat.mRenderItem->setLastVertexCount(glyphStream.end () - vertices); - } + GlyphStream glyphStream(textFormat.mFont, static_cast(mCoord.left), + static_cast(mCoord.top - mViewTop), z /*mNode->getNodeDepth()*/, vertices, renderXform); - // ISubWidget should not necessarily be a drawitem - // in this case, it is not... - void doRender() override { } + int visit_top = (std::max)(mViewTop, mViewTop + int(renderXform.clipTop)); + int visit_bottom = (std::min)(mViewBottom, mViewTop + int(renderXform.clipBottom)); - void _updateView () override - { - _checkMargin(); + mBook->visitRuns(visit_top, visit_bottom, textFormat.mFont, RenderRun(this, glyphStream)); - if (mNode != nullptr) - for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) - mNode->outOfDate (i->second->mRenderItem); - } + textFormat.mRenderItem->setLastVertexCount(glyphStream.end() - vertices); + } - void _correctView() override - { - _checkMargin (); + // ISubWidget should not necessarily be a drawitem + // in this case, it is not... + void doRender() override {} - if (mNode != nullptr) - for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) - mNode->outOfDate (i->second->mRenderItem); + void _updateView() override + { + _checkMargin(); - } + if (mNode != nullptr) + for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) + mNode->outOfDate(i->second->mRenderItem); + } - void destroyDrawItem() override - { - for (ActiveTextFormats::iterator i = mActiveTextFormats.begin (); i != mActiveTextFormats.end (); ++i) - i->second->destroyDrawItem (mNode); + void _correctView() override + { + _checkMargin(); - mNode = nullptr; - } -}; + if (mNode != nullptr) + for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) + mNode->outOfDate(i->second->mRenderItem); + } + void destroyDrawItem() override + { + for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) + i->second->destroyDrawItem(mNode); -class BookPageImpl final : public BookPage -{ -MYGUI_RTTI_DERIVED(BookPage) -public: + mNode = nullptr; + } + }; - BookPageImpl() - : mPageDisplay(nullptr) + class BookPageImpl final : public BookPage { - } + MYGUI_RTTI_DERIVED(BookPage) + public: + BookPageImpl() + : mPageDisplay(nullptr) + { + } - void showPage (TypesetBook::Ptr book, size_t page) override - { - mPageDisplay->showPage (book, page); - } + void showPage(TypesetBook::Ptr book, size_t page) override { mPageDisplay->showPage(std::move(book), page); } - void adviseLinkClicked (std::function linkClicked) override - { - mPageDisplay->mLinkClicked = linkClicked; - } + void adviseLinkClicked(std::function linkClicked) override + { + mPageDisplay->mLinkClicked = std::move(linkClicked); + } - void unadviseLinkClicked () override - { - mPageDisplay->mLinkClicked = std::function (); - } + void unadviseLinkClicked() override { mPageDisplay->mLinkClicked = std::function(); } -protected: + protected: + void initialiseOverride() override + { + Base::initialiseOverride(); - void initialiseOverride() override - { - Base::initialiseOverride(); + if (getSubWidgetText()) + { + mPageDisplay = getSubWidgetText()->castType(); + } + else + { + throw std::runtime_error("BookPage unable to find page display sub widget"); + } + } - if (getSubWidgetText()) + void onMouseLostFocus(Widget* _new) override { - mPageDisplay = getSubWidgetText()->castType(); + // NOTE: MyGUI also fires eventMouseLostFocus for widgets that are about to be destroyed (if they had + // focus). Child widgets may already be destroyed! So be careful. + mPageDisplay->onMouseLostFocus(); } - else + + void onMouseMove(int left, int top) override { mPageDisplay->onMouseMove(left, top); } + + void onMouseButtonPressed(int left, int top, MyGUI::MouseButton id) override { - throw std::runtime_error("BookPage unable to find page display sub widget"); + mPageDisplay->onMouseButtonPressed(left, top, id); } - } - void onMouseLostFocus(Widget* _new) override - { - // NOTE: MyGUI also fires eventMouseLostFocus for widgets that are about to be destroyed (if they had focus). - // Child widgets may already be destroyed! So be careful. - mPageDisplay->onMouseLostFocus (); - } + void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id) override + { + mPageDisplay->onMouseButtonReleased(left, top, id); + } - void onMouseMove(int left, int top) override + PageDisplay* mPageDisplay; + }; + + void BookPage::registerMyGUIComponents() { - mPageDisplay->onMouseMove (left, top); + MyGUI::FactoryManager& factory = MyGUI::FactoryManager::getInstance(); + + factory.registerFactory("Widget"); + factory.registerFactory("BasisSkin"); } - void onMouseButtonPressed (int left, int top, MyGUI::MouseButton id) override + static bool ucsLineBreak(int codePoint) { - mPageDisplay->onMouseButtonPressed (left, top, id); + return codePoint == '\n'; } - void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id) override + static bool ucsCarriageReturn(int codePoint) { - mPageDisplay->onMouseButtonReleased (left, top, id); + return codePoint == '\r'; } - PageDisplay* mPageDisplay; -}; - -void BookPage::registerMyGUIComponents () -{ - MyGUI::FactoryManager & factory = MyGUI::FactoryManager::getInstance(); - - factory.registerFactory("Widget"); - factory.registerFactory("BasisSkin"); -} - -static bool ucsLineBreak (int codePoint) -{ - return codePoint == '\n'; -} - -static bool ucsCarriageReturn (int codePoint) -{ - return codePoint == '\r'; -} - -static bool ucsSpace (int codePoint) -{ - switch (codePoint) + static bool ucsSpace(int codePoint) { - case 0x0020: // SPACE - case 0x00A0: // NO-BREAK SPACE - case 0x1680: // OGHAM SPACE MARK - case 0x180E: // MONGOLIAN VOWEL SEPARATOR - case 0x2000: // EN QUAD - case 0x2001: // EM QUAD - case 0x2002: // EN SPACE - case 0x2003: // EM SPACE - case 0x2004: // THREE-PER-EM SPACE - case 0x2005: // FOUR-PER-EM SPACE - case 0x2006: // SIX-PER-EM SPACE - case 0x2007: // FIGURE SPACE - case 0x2008: // PUNCTUATION SPACE - case 0x2009: // THIN SPACE - case 0x200A: // HAIR SPACE - case 0x200B: // ZERO WIDTH SPACE - case 0x202F: // NARROW NO-BREAK SPACE - case 0x205F: // MEDIUM MATHEMATICAL SPACE - case 0x3000: // IDEOGRAPHIC SPACE - case 0xFEFF: // ZERO WIDTH NO-BREAK SPACE - return true; - default: - return false; + switch (codePoint) + { + case 0x0020: // SPACE + case 0x00A0: // NO-BREAK SPACE + case 0x1680: // OGHAM SPACE MARK + case 0x180E: // MONGOLIAN VOWEL SEPARATOR + case 0x2000: // EN QUAD + case 0x2001: // EM QUAD + case 0x2002: // EN SPACE + case 0x2003: // EM SPACE + case 0x2004: // THREE-PER-EM SPACE + case 0x2005: // FOUR-PER-EM SPACE + case 0x2006: // SIX-PER-EM SPACE + case 0x2007: // FIGURE SPACE + case 0x2008: // PUNCTUATION SPACE + case 0x2009: // THIN SPACE + case 0x200A: // HAIR SPACE + case 0x200B: // ZERO WIDTH SPACE + case 0x202F: // NARROW NO-BREAK SPACE + case 0x205F: // MEDIUM MATHEMATICAL SPACE + case 0x3000: // IDEOGRAPHIC SPACE + case 0xFEFF: // ZERO WIDTH NO-BREAK SPACE + return true; + default: + return false; + } } -} -static bool ucsBreakingSpace (int codePoint) -{ - switch (codePoint) + static bool ucsBreakingSpace(int codePoint) { - case 0x0020: // SPACE - //case 0x00A0: // NO-BREAK SPACE - case 0x1680: // OGHAM SPACE MARK - case 0x180E: // MONGOLIAN VOWEL SEPARATOR - case 0x2000: // EN QUAD - case 0x2001: // EM QUAD - case 0x2002: // EN SPACE - case 0x2003: // EM SPACE - case 0x2004: // THREE-PER-EM SPACE - case 0x2005: // FOUR-PER-EM SPACE - case 0x2006: // SIX-PER-EM SPACE - case 0x2007: // FIGURE SPACE - case 0x2008: // PUNCTUATION SPACE - case 0x2009: // THIN SPACE - case 0x200A: // HAIR SPACE - case 0x200B: // ZERO WIDTH SPACE - case 0x202F: // NARROW NO-BREAK SPACE - case 0x205F: // MEDIUM MATHEMATICAL SPACE - case 0x3000: // IDEOGRAPHIC SPACE - //case 0xFEFF: // ZERO WIDTH NO-BREAK SPACE - return true; - default: - return false; + switch (codePoint) + { + case 0x0020: // SPACE + // case 0x00A0: // NO-BREAK SPACE + case 0x1680: // OGHAM SPACE MARK + case 0x180E: // MONGOLIAN VOWEL SEPARATOR + case 0x2000: // EN QUAD + case 0x2001: // EM QUAD + case 0x2002: // EN SPACE + case 0x2003: // EM SPACE + case 0x2004: // THREE-PER-EM SPACE + case 0x2005: // FOUR-PER-EM SPACE + case 0x2006: // SIX-PER-EM SPACE + case 0x2007: // FIGURE SPACE + case 0x2008: // PUNCTUATION SPACE + case 0x2009: // THIN SPACE + case 0x200A: // HAIR SPACE + case 0x200B: // ZERO WIDTH SPACE + case 0x202F: // NARROW NO-BREAK SPACE + case 0x205F: // MEDIUM MATHEMATICAL SPACE + case 0x3000: // IDEOGRAPHIC SPACE + // case 0xFEFF: // ZERO WIDTH NO-BREAK SPACE + return true; + default: + return false; + } } -} } diff --git a/apps/openmw/mwgui/bookpage.hpp b/apps/openmw/mwgui/bookpage.hpp index 512a4399292..d42fb4783f8 100644 --- a/apps/openmw/mwgui/bookpage.hpp +++ b/apps/openmw/mwgui/bookpage.hpp @@ -2,17 +2,14 @@ #define MWGUI_BOOKPAGE_HPP #include "MyGUI_Colour.h" +#include "MyGUI_IFont.h" #include "MyGUI_Widget.h" -#include "MyGUI_FontManager.h" +#include #include #include -#include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" +#include namespace MWGui { @@ -20,11 +17,11 @@ namespace MWGui /// the book page widget. struct TypesetBook { - typedef std::shared_ptr Ptr; + typedef std::shared_ptr Ptr; typedef intptr_t InteractiveId; /// Returns the number of pages in the document. - virtual size_t pageCount () const = 0; + virtual size_t pageCount() const = 0; /// Return the area covered by the document. The first /// integer is the maximum with of any line. This is not @@ -32,7 +29,7 @@ namespace MWGui /// it is the largest distance from the left edge to the /// right edge. The second integer is the height of all /// text combined prior to pagination. - virtual std::pair getSize () const = 0; + virtual std::pair getSize() const = 0; virtual ~TypesetBook() = default; }; @@ -50,19 +47,17 @@ namespace MWGui GlyphInfo(MyGUI::IFont* font, MyGUI::Char ch) { - static const int fontHeight = MWBase::Environment::get().getWindowManager()->getFontHeight(); - const MyGUI::GlyphInfo* gi = font->getGlyphInfo(ch); if (gi) { - const float scale = font->getDefaultHeight() / (float) fontHeight; + const float scale = font->getDefaultHeight() / static_cast(Settings::gui().mFontSize); codePoint = gi->codePoint; - bearingX = (int) gi->bearingX / scale; - bearingY = (int) gi->bearingY / scale; - width = (int) gi->width / scale; - height = (int) gi->height / scale; - advance = (int) gi->advance / scale; + bearingX = (int)gi->bearingX / scale; + bearingY = (int)gi->bearingY / scale; + width = (int)gi->width / scale; + height = (int)gi->height / scale; + advance = (int)gi->advance / scale; uvRect = gi->uvRect; charFound = true; } @@ -82,18 +77,19 @@ namespace MWGui /// A factory class for creating a typeset book instance. struct BookTypesetter { - typedef std::shared_ptr Ptr; + typedef std::shared_ptr Ptr; typedef TypesetBook::InteractiveId InteractiveId; typedef MyGUI::Colour Colour; - typedef uint8_t const * Utf8Point; - typedef std::pair Utf8Span; + typedef uint8_t const* Utf8Point; + typedef std::pair Utf8Span; virtual ~BookTypesetter() = default; - enum Alignment { - AlignLeft = -1, + enum Alignment + { + AlignLeft = -1, AlignCenter = 0, - AlignRight = +1 + AlignRight = +1 }; /// Styles are used to control the character level formatting @@ -103,71 +99,71 @@ namespace MWGui struct Style; /// A factory function for creating the default implementation of a book typesetter - static Ptr create (int pageWidth, int pageHeight); + static Ptr create(int pageWidth, int pageHeight); /// Create a simple text style consisting of a font and a text color. - virtual Style* createStyle (const std::string& fontName, const Colour& colour, bool useBookFont=true) = 0; + virtual Style* createStyle(const std::string& fontName, const Colour& colour, bool useBookFont = true) = 0; /// Create a hyper-link style with a user-defined identifier based on an /// existing style. The unique flag forces a new instance of this style /// to be created even if an existing instance is present. - virtual Style* createHotStyle (Style * BaseStyle, const Colour& NormalColour, const Colour& HoverColour, - const Colour& ActiveColour, InteractiveId Id, bool Unique = true) = 0; + virtual Style* createHotStyle(Style* BaseStyle, const Colour& NormalColour, const Colour& HoverColour, + const Colour& ActiveColour, InteractiveId Id, bool Unique = true) + = 0; /// Insert a line break into the document. Newline characters in the input /// text have the same affect. The margin parameter adds additional space /// before the next line of text. - virtual void lineBreak (float margin = 0) = 0; + virtual void lineBreak(float margin = 0) = 0; /// Insert a section break into the document. This causes a new section /// to begin when additional text is inserted. Pagination attempts to keep /// sections together on a single page. The margin parameter adds additional space /// before the next line of text. - virtual void sectionBreak (int margin = 0) = 0; + virtual void sectionBreak(int margin = 0) = 0; /// Changes the alignment for the current section of text. - virtual void setSectionAlignment (Alignment sectionAlignment) = 0; + virtual void setSectionAlignment(Alignment sectionAlignment) = 0; // Layout a block of text with the specified style into the document. - virtual void write (Style * Style, Utf8Span Text) = 0; + virtual void write(Style* Style, Utf8Span Text) = 0; /// Adds a content block to the document without laying it out. An /// identifier is returned that can be used to refer to it. If select /// is true, the block is activated to be references by future writes. - virtual intptr_t addContent (Utf8Span Text, bool Select = true) = 0; + virtual intptr_t addContent(Utf8Span Text, bool Select = true) = 0; /// Select a previously created content block for future writes. - virtual void selectContent (intptr_t contentHandle) = 0; + virtual void selectContent(intptr_t contentHandle) = 0; /// Layout a span of the selected content block into the document /// using the specified style. - virtual void write (Style * Style, size_t Begin, size_t End) = 0; + virtual void write(Style* Style, size_t Begin, size_t End) = 0; /// Finalize the document layout, and return a pointer to it. - virtual TypesetBook::Ptr complete () = 0; + virtual TypesetBook::Ptr complete() = 0; }; /// An interface to the BookPage widget. class BookPage : public MyGUI::Widget { - MYGUI_RTTI_DERIVED(BookPage) + MYGUI_RTTI_DERIVED(BookPage) public: - typedef TypesetBook::InteractiveId InteractiveId; - typedef std::function ClickCallback; + typedef std::function ClickCallback; /// Make the widget display the specified page from the specified book. - virtual void showPage (TypesetBook::Ptr Book, size_t Page) = 0; + virtual void showPage(TypesetBook::Ptr Book, size_t Page) = 0; /// Set the callback for a clicking a hyper-link in the document. - virtual void adviseLinkClicked (ClickCallback callback) = 0; + virtual void adviseLinkClicked(ClickCallback callback) = 0; /// Clear the hyper-link click callback. - virtual void unadviseLinkClicked () = 0; + virtual void unadviseLinkClicked() = 0; /// Register the widget and associated sub-widget with MyGUI. Should be /// called once near the beginning of the program. - static void registerMyGUIComponents (); + static void registerMyGUIComponents(); }; } diff --git a/apps/openmw/mwgui/bookwindow.cpp b/apps/openmw/mwgui/bookwindow.cpp index 86089051d63..ef875a18b93 100644 --- a/apps/openmw/mwgui/bookwindow.cpp +++ b/apps/openmw/mwgui/bookwindow.cpp @@ -1,12 +1,12 @@ #include "bookwindow.hpp" -#include #include +#include -#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" @@ -19,7 +19,7 @@ namespace MWGui { - BookWindow::BookWindow () + BookWindow::BookWindow() : BookWindowBase("openmw_book.layout") , mCurrentPage(0) , mTakeButtonShow(true) @@ -61,14 +61,15 @@ namespace MWGui if (mNextPageButton->getSize().width == 64) { // english button has a 7 pixel wide strip of garbage on its right edge - mNextPageButton->setSize(64-7, mNextPageButton->getSize().height); - mNextPageButton->setImageCoord(MyGUI::IntCoord(0,0,(64-7)*scale,mNextPageButton->getSize().height*scale)); + mNextPageButton->setSize(64 - 7, mNextPageButton->getSize().height); + mNextPageButton->setImageCoord( + MyGUI::IntCoord(0, 0, (64 - 7) * scale, mNextPageButton->getSize().height * scale)); } center(); } - void BookWindow::onMouseWheel(MyGUI::Widget *_sender, int _rel) + void BookWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { if (_rel < 0) nextPage(); @@ -81,8 +82,10 @@ namespace MWGui mPages.clear(); } - void BookWindow::setPtr (const MWWorld::Ptr& book) + void BookWindow::setPtr(const MWWorld::Ptr& book) { + if (book.isEmpty() || (book.getType() != ESM::REC_BOOK && book.getType() != ESM::REC_BOOK4)) + throw std::runtime_error("Invalid argument in BookWindow::setPtr"); mBook = book; MWWorld::Ptr player = MWMechanics::getPlayer(); @@ -91,11 +94,16 @@ namespace MWGui clearPages(); mCurrentPage = 0; - MWWorld::LiveCellRef *ref = mBook.get(); + const std::string* text; + if (book.getType() == ESM::REC_BOOK) + text = &book.get()->mBase->mText; + else + text = &book.get()->mBase->mText; + bool shrinkTextAtLastTag = book.getType() == ESM::REC_BOOK; Formatting::BookFormatter formatter; - mPages = formatter.markupToWidget(mLeftPage, ref->mBase->mText); - formatter.markupToWidget(mRightPage, ref->mBase->mText); + mPages = formatter.markupToWidget(mLeftPage, *text, shrinkTextAtLastTag); + formatter.markupToWidget(mRightPage, *text, shrinkTextAtLastTag); updatePages(); @@ -110,7 +118,7 @@ namespace MWGui mTakeButton->setVisible(mTakeButtonShow && mTakeButtonAllowed); } - void BookWindow::onKeyButtonPressed(MyGUI::Widget *sender, MyGUI::KeyCode key, MyGUI::Char character) + void BookWindow::onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character) { if (key == MyGUI::KeyCode::ArrowUp) prevPage(); @@ -124,38 +132,38 @@ namespace MWGui mTakeButton->setVisible(mTakeButtonShow && mTakeButtonAllowed); } - void BookWindow::onCloseButtonClicked (MyGUI::Widget* sender) + void BookWindow::onCloseButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Book); } - void BookWindow::onTakeButtonClicked (MyGUI::Widget* sender) + void BookWindow::onTakeButtonClicked(MyGUI::Widget* sender) { - MWBase::Environment::get().getWindowManager()->playSound("Item Book Up"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Book Up")); MWWorld::ActionTake take(mBook); - take.execute (MWMechanics::getPlayer()); + take.execute(MWMechanics::getPlayer()); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Book); } - void BookWindow::onNextPageButtonClicked (MyGUI::Widget* sender) + void BookWindow::onNextPageButtonClicked(MyGUI::Widget* sender) { nextPage(); } - void BookWindow::onPrevPageButtonClicked (MyGUI::Widget* sender) + void BookWindow::onPrevPageButtonClicked(MyGUI::Widget* sender) { prevPage(); } void BookWindow::updatePages() { - mLeftPageNumber->setCaption( MyGUI::utility::toString(mCurrentPage*2 + 1) ); - mRightPageNumber->setCaption( MyGUI::utility::toString(mCurrentPage*2 + 2) ); + mLeftPageNumber->setCaption(MyGUI::utility::toString(mCurrentPage * 2 + 1)); + mRightPageNumber->setCaption(MyGUI::utility::toString(mCurrentPage * 2 + 2)); MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); - bool nextPageVisible = (mCurrentPage+1)*2 < mPages.size(); + bool nextPageVisible = (mCurrentPage + 1) * 2 < mPages.size(); mNextPageButton->setVisible(nextPageVisible); bool prevPageVisible = mCurrentPage != 0; mPrevPageButton->setVisible(prevPageVisible); @@ -168,17 +176,17 @@ namespace MWGui if (mPages.empty()) return; - MyGUI::Widget * paper; + MyGUI::Widget* paper; paper = mLeftPage->getChildAt(0); - paper->setCoord(paper->getPosition().left, -mPages[mCurrentPage*2].first, - paper->getWidth(), mPages[mCurrentPage*2].second); + paper->setCoord(paper->getPosition().left, -mPages[mCurrentPage * 2].first, paper->getWidth(), + mPages[mCurrentPage * 2].second); paper = mRightPage->getChildAt(0); - if ((mCurrentPage+1)*2 <= mPages.size()) + if ((mCurrentPage + 1) * 2 <= mPages.size()) { - paper->setCoord(paper->getPosition().left, -mPages[mCurrentPage*2+1].first, - paper->getWidth(), mPages[mCurrentPage*2+1].second); + paper->setCoord(paper->getPosition().left, -mPages[mCurrentPage * 2 + 1].first, paper->getWidth(), + mPages[mCurrentPage * 2 + 1].second); paper->setVisible(true); } else @@ -189,9 +197,9 @@ namespace MWGui void BookWindow::nextPage() { - if ((mCurrentPage+1)*2 < mPages.size()) + if ((mCurrentPage + 1) * 2 < mPages.size()) { - MWBase::Environment::get().getWindowManager()->playSound("book page2"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page2")); ++mCurrentPage; @@ -202,7 +210,7 @@ namespace MWGui { if (mCurrentPage > 0) { - MWBase::Environment::get().getWindowManager()->playSound("book page"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); --mCurrentPage; diff --git a/apps/openmw/mwgui/bookwindow.hpp b/apps/openmw/mwgui/bookwindow.hpp index 116437f22e7..5a3dfdf5847 100644 --- a/apps/openmw/mwgui/bookwindow.hpp +++ b/apps/openmw/mwgui/bookwindow.hpp @@ -11,51 +11,53 @@ namespace MWGui { class BookWindow : public BookWindowBase { - public: - BookWindow(); + public: + BookWindow(); - void setPtr(const MWWorld::Ptr& book) override; - void setInventoryAllowed(bool allowed); + void setPtr(const MWWorld::Ptr& book) override; + void setInventoryAllowed(bool allowed); - void onResChange(int, int) override { center(); } + void onResChange(int, int) override { center(); } - protected: - void onNextPageButtonClicked (MyGUI::Widget* sender); - void onPrevPageButtonClicked (MyGUI::Widget* sender); - void onCloseButtonClicked (MyGUI::Widget* sender); - void onTakeButtonClicked (MyGUI::Widget* sender); - void onMouseWheel(MyGUI::Widget* _sender, int _rel); - void setTakeButtonShow(bool show); + std::string_view getWindowIdForLua() const override { return "Book"; } - void onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character); + protected: + void onNextPageButtonClicked(MyGUI::Widget* sender); + void onPrevPageButtonClicked(MyGUI::Widget* sender); + void onCloseButtonClicked(MyGUI::Widget* sender); + void onTakeButtonClicked(MyGUI::Widget* sender); + void onMouseWheel(MyGUI::Widget* _sender, int _rel); + void setTakeButtonShow(bool show); - void nextPage(); - void prevPage(); + void onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character); - void updatePages(); - void clearPages(); + void nextPage(); + void prevPage(); - private: - typedef std::pair Page; - typedef std::vector Pages; + void updatePages(); + void clearPages(); - Gui::ImageButton* mCloseButton; - Gui::ImageButton* mTakeButton; - Gui::ImageButton* mNextPageButton; - Gui::ImageButton* mPrevPageButton; + private: + typedef std::pair Page; + typedef std::vector Pages; - MyGUI::TextBox* mLeftPageNumber; - MyGUI::TextBox* mRightPageNumber; - MyGUI::Widget* mLeftPage; - MyGUI::Widget* mRightPage; + Gui::ImageButton* mCloseButton; + Gui::ImageButton* mTakeButton; + Gui::ImageButton* mNextPageButton; + Gui::ImageButton* mPrevPageButton; - unsigned int mCurrentPage; // 0 is first page - Pages mPages; + MyGUI::TextBox* mLeftPageNumber; + MyGUI::TextBox* mRightPageNumber; + MyGUI::Widget* mLeftPage; + MyGUI::Widget* mRightPage; - MWWorld::Ptr mBook; + unsigned int mCurrentPage; // 0 is first page + Pages mPages; - bool mTakeButtonShow; - bool mTakeButtonAllowed; + MWWorld::Ptr mBook; + + bool mTakeButtonShow; + bool mTakeButtonAllowed; }; } diff --git a/apps/openmw/mwgui/charactercreation.cpp b/apps/openmw/mwgui/charactercreation.cpp index 827f87c7d60..be2d22ae841 100644 --- a/apps/openmw/mwgui/charactercreation.cpp +++ b/apps/openmw/mwgui/charactercreation.cpp @@ -1,28 +1,31 @@ #include "charactercreation.hpp" +#include + #include #include +#include #include #include "../mwbase/environment.hpp" -#include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" -#include "../mwbase/world.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" -#include "textinput.hpp" -#include "race.hpp" -#include "class.hpp" #include "birth.hpp" -#include "review.hpp" +#include "class.hpp" #include "inventorywindow.hpp" +#include "race.hpp" +#include "review.hpp" +#include "textinput.hpp" namespace { @@ -36,22 +39,23 @@ namespace { const std::string mText; const Response mResponses[3]; - const std::string mSound; + const VFS::Path::Normalized mSound; }; Step sGenerateClassSteps(int number) { number++; - std::string question = Fallback::Map::getString("Question_" + MyGUI::utility::toString(number) + "_Question"); - std::string answer0 = Fallback::Map::getString("Question_" + MyGUI::utility::toString(number) + "_AnswerOne"); - std::string answer1 = Fallback::Map::getString("Question_" + MyGUI::utility::toString(number) + "_AnswerTwo"); - std::string answer2 = Fallback::Map::getString("Question_" + MyGUI::utility::toString(number) + "_AnswerThree"); + std::string question{ Fallback::Map::getString("Question_" + MyGUI::utility::toString(number) + "_Question") }; + std::string answer0{ Fallback::Map::getString("Question_" + MyGUI::utility::toString(number) + "_AnswerOne") }; + std::string answer1{ Fallback::Map::getString("Question_" + MyGUI::utility::toString(number) + "_AnswerTwo") }; + std::string answer2{ Fallback::Map::getString( + "Question_" + MyGUI::utility::toString(number) + "_AnswerThree") }; std::string sound = "vo\\misc\\chargen qa" + MyGUI::utility::toString(number) + ".wav"; - Response r0 = {answer0, ESM::Class::Combat}; - Response r1 = {answer1, ESM::Class::Magic}; - Response r2 = {answer2, ESM::Class::Stealth}; + Response r0 = { std::move(answer0), ESM::Class::Combat }; + Response r1 = { std::move(answer1), ESM::Class::Magic }; + Response r2 = { std::move(answer2), ESM::Class::Stealth }; // randomize order in which responses are displayed int order = Misc::Rng::rollDice(6); @@ -59,26 +63,19 @@ namespace switch (order) { case 0: - return {question, {r0, r1, r2}, sound}; + return { std::move(question), { std::move(r0), std::move(r1), std::move(r2) }, std::move(sound) }; case 1: - return {question, {r0, r2, r1}, sound}; + return { std::move(question), { std::move(r0), std::move(r2), std::move(r1) }, std::move(sound) }; case 2: - return {question, {r1, r0, r2}, sound}; + return { std::move(question), { std::move(r1), std::move(r0), std::move(r2) }, std::move(sound) }; case 3: - return {question, {r1, r2, r0}, sound}; + return { std::move(question), { std::move(r1), std::move(r2), std::move(r0) }, std::move(sound) }; case 4: - return {question, {r2, r0, r1}, sound}; + return { std::move(question), { std::move(r2), std::move(r0), std::move(r1) }, std::move(sound) }; default: - return {question, {r2, r1, r0}, sound}; + return { std::move(question), { std::move(r2), std::move(r1), std::move(r0) }, std::move(sound) }; } } - - void updatePlayerHealth() - { - MWWorld::Ptr player = MWMechanics::getPlayer(); - MWMechanics::NpcStats& npcStats = player.getClass().getNpcStats(player); - npcStats.updateHealth(); - } } namespace MWGui @@ -87,15 +84,6 @@ namespace MWGui CharacterCreation::CharacterCreation(osg::Group* parent, Resource::ResourceSystem* resourceSystem) : mParent(parent) , mResourceSystem(resourceSystem) - , mNameDialog(nullptr) - , mRaceDialog(nullptr) - , mClassChoiceDialog(nullptr) - , mGenerateClassQuestionDialog(nullptr) - , mGenerateClassResultDialog(nullptr) - , mPickClassDialog(nullptr) - , mCreateClassDialog(nullptr) - , mBirthSignDialog(nullptr) - , mReviewDialog(nullptr) , mGenerateClassStep(0) { mCreationStage = CSE_NotStarted; @@ -107,61 +95,48 @@ namespace MWGui mGenerateClassSpecializations[2] = 0; // Setup player stats - for (int i = 0; i < ESM::Attribute::Length; ++i) - mPlayerAttributes.emplace(ESM::Attribute::sAttributeIds[i], MWMechanics::AttributeValue()); + const auto& store = MWBase::Environment::get().getWorld()->getStore(); + for (const ESM::Attribute& attribute : store.get()) + mPlayerAttributes.emplace(attribute.mId, MWMechanics::AttributeValue()); - for (int i = 0; i < ESM::Skill::Length; ++i) - mPlayerSkillValues.emplace(ESM::Skill::sSkillIds[i], MWMechanics::SkillValue()); + for (const auto& skill : store.get()) + mPlayerSkillValues.emplace(skill.mId, MWMechanics::SkillValue()); } - void CharacterCreation::setValue (const std::string& id, const MWMechanics::AttributeValue& value) + void CharacterCreation::setAttribute(ESM::RefId id, const MWMechanics::AttributeValue& value) { - static const char *ids[] = - { - "AttribVal1", "AttribVal2", "AttribVal3", "AttribVal4", - "AttribVal5", "AttribVal6", "AttribVal7", "AttribVal8", 0 - }; - - for (int i=0; ids[i]; ++i) - { - if (ids[i]==id) - { - mPlayerAttributes[static_cast(i)] = value; - if (mReviewDialog) - mReviewDialog->setAttribute(static_cast(i), value); - - break; - } - } + mPlayerAttributes[id] = value; + if (mReviewDialog) + mReviewDialog->setAttribute(id, value); } - void CharacterCreation::setValue (const std::string& id, const MWMechanics::DynamicStat& value) + void CharacterCreation::setValue(std::string_view id, const MWMechanics::DynamicStat& value) { if (mReviewDialog) { if (id == "HBar") { - mReviewDialog->setHealth (value); + mReviewDialog->setHealth(value); } else if (id == "MBar") { - mReviewDialog->setMagicka (value); + mReviewDialog->setMagicka(value); } else if (id == "FBar") { - mReviewDialog->setFatigue (value); + mReviewDialog->setFatigue(value); } } } - void CharacterCreation::setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) + void CharacterCreation::setValue(ESM::RefId id, const MWMechanics::SkillValue& value) { - mPlayerSkillValues[parSkill] = value; + mPlayerSkillValues[id] = value; if (mReviewDialog) - mReviewDialog->setSkillValue(parSkill, value); + mReviewDialog->setSkillValue(id, value); } - void CharacterCreation::configureSkills (const SkillList& major, const SkillList& minor) + void CharacterCreation::configureSkills(const std::vector& major, const std::vector& minor) { if (mReviewDialog) mReviewDialog->configureSkills(major, minor); @@ -183,10 +158,10 @@ namespace MWGui switch (id) { case GM_Name: - MWBase::Environment::get().getWindowManager()->removeDialog(mNameDialog); - mNameDialog = nullptr; - mNameDialog = new TextInputDialog(); - mNameDialog->setTextLabel(MWBase::Environment::get().getWindowManager()->getGameSettingString("sName", "Name")); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mNameDialog)); + mNameDialog = std::make_unique(); + mNameDialog->setTextLabel( + MWBase::Environment::get().getWindowManager()->getGameSettingString("sName", "Name")); mNameDialog->setTextInput(mPlayerName); mNameDialog->setNextButtonShow(mCreationStage >= CSE_NameChosen); mNameDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onNameDialogDone); @@ -194,9 +169,8 @@ namespace MWGui break; case GM_Race: - MWBase::Environment::get().getWindowManager()->removeDialog(mRaceDialog); - mRaceDialog = nullptr; - mRaceDialog = new RaceDialog(mParent, mResourceSystem); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mRaceDialog)); + mRaceDialog = std::make_unique(mParent, mResourceSystem); mRaceDialog->setNextButtonShow(mCreationStage >= CSE_RaceChosen); mRaceDialog->setRaceId(mPlayerRaceId); mRaceDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onRaceDialogDone); @@ -207,19 +181,18 @@ namespace MWGui break; case GM_Class: - MWBase::Environment::get().getWindowManager()->removeDialog(mClassChoiceDialog); - mClassChoiceDialog = nullptr; - mClassChoiceDialog = new ClassChoiceDialog(); - mClassChoiceDialog->eventButtonSelected += MyGUI::newDelegate(this, &CharacterCreation::onClassChoice); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mClassChoiceDialog)); + mClassChoiceDialog = std::make_unique(); + mClassChoiceDialog->eventButtonSelected + += MyGUI::newDelegate(this, &CharacterCreation::onClassChoice); mClassChoiceDialog->setVisible(true); if (mCreationStage < CSE_RaceChosen) mCreationStage = CSE_RaceChosen; break; case GM_ClassPick: - MWBase::Environment::get().getWindowManager()->removeDialog(mPickClassDialog); - mPickClassDialog = nullptr; - mPickClassDialog = new PickClassDialog(); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mPickClassDialog)); + mPickClassDialog = std::make_unique(); mPickClassDialog->setNextButtonShow(mCreationStage >= CSE_ClassChosen); mPickClassDialog->setClassId(mPlayerClass.mId); mPickClassDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onPickClassDialogDone); @@ -230,9 +203,8 @@ namespace MWGui break; case GM_Birth: - MWBase::Environment::get().getWindowManager()->removeDialog(mBirthSignDialog); - mBirthSignDialog = nullptr; - mBirthSignDialog = new BirthDialog(); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mBirthSignDialog)); + mBirthSignDialog = std::make_unique(); mBirthSignDialog->setNextButtonShow(mCreationStage >= CSE_BirthSignChosen); mBirthSignDialog->setBirthId(mPlayerBirthSignId); mBirthSignDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onBirthSignDialogDone); @@ -243,11 +215,13 @@ namespace MWGui break; case GM_ClassCreate: - if (!mCreateClassDialog) + if (mCreateClassDialog == nullptr) { - mCreateClassDialog = new CreateClassDialog(); - mCreateClassDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onCreateClassDialogDone); - mCreateClassDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onCreateClassDialogBack); + mCreateClassDialog = std::make_unique(); + mCreateClassDialog->eventDone + += MyGUI::newDelegate(this, &CharacterCreation::onCreateClassDialogDone); + mCreateClassDialog->eventBack + += MyGUI::newDelegate(this, &CharacterCreation::onCreateClassDialogBack); } mCreateClassDialog->setNextButtonShow(mCreationStage >= CSE_ClassChosen); mCreateClassDialog->setVisible(true); @@ -256,7 +230,7 @@ namespace MWGui break; case GM_ClassGenerate: mGenerateClassStep = 0; - mGenerateClass = ""; + mGenerateClass = ESM::RefId(); mGenerateClassSpecializations[0] = 0; mGenerateClassSpecializations[1] = 0; mGenerateClassSpecializations[2] = 0; @@ -265,17 +239,16 @@ namespace MWGui mCreationStage = CSE_RaceChosen; break; case GM_Review: - MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); - mReviewDialog = nullptr; - mReviewDialog = new ReviewDialog(); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mReviewDialog)); + mReviewDialog = std::make_unique(); - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); - const ESM::NPC *playerNpc = world->getPlayerPtr().get()->mBase; + const ESM::NPC* playerNpc = world->getPlayerPtr().get()->mBase; - const MWWorld::Player player = world->getPlayer(); + const MWWorld::Player& player = world->getPlayer(); - const ESM::Class *playerClass = world->getStore().get().find(playerNpc->mClass); + const ESM::Class* playerClass = world->getStore().get().find(playerNpc->mClass); mReviewDialog->setPlayerName(playerNpc->mName); mReviewDialog->setRace(playerNpc->mRace); @@ -290,17 +263,18 @@ namespace MWGui mReviewDialog->setFatigue(stats.getFatigue()); for (auto& attributePair : mPlayerAttributes) { - mReviewDialog->setAttribute(static_cast (attributePair.first), attributePair.second); + mReviewDialog->setAttribute(attributePair.first, attributePair.second); } - for (auto& skillPair : mPlayerSkillValues) + for (const auto& [skill, value] : mPlayerSkillValues) { - mReviewDialog->setSkillValue(static_cast (skillPair.first), skillPair.second); + mReviewDialog->setSkillValue(skill, value); } mReviewDialog->configureSkills(mPlayerMajorSkills, mPlayerMinorSkills); mReviewDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onReviewDialogDone); mReviewDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onReviewDialogBack); - mReviewDialog->eventActivateDialog += MyGUI::newDelegate(this, &CharacterCreation::onReviewActivateDialog); + mReviewDialog->eventActivateDialog + += MyGUI::newDelegate(this, &CharacterCreation::onReviewActivateDialog); mReviewDialog->setVisible(true); if (mCreationStage < CSE_BirthSignChosen) mCreationStage = CSE_BirthSignChosen; @@ -315,16 +289,13 @@ namespace MWGui void CharacterCreation::onReviewDialogDone(WindowBase* parWindow) { - MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); - mReviewDialog = nullptr; - + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mReviewDialog)); MWBase::Environment::get().getWindowManager()->popGuiMode(); } void CharacterCreation::onReviewDialogBack() { - MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); - mReviewDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mReviewDialog)); mCreationStage = CSE_ReviewBack; MWBase::Environment::get().getWindowManager()->popGuiMode(); @@ -333,13 +304,12 @@ namespace MWGui void CharacterCreation::onReviewActivateDialog(int parDialog) { - MWBase::Environment::get().getWindowManager()->removeDialog(mReviewDialog); - mReviewDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mReviewDialog)); mCreationStage = CSE_ReviewNext; MWBase::Environment::get().getWindowManager()->popGuiMode(); - switch(parDialog) + switch (parDialog) { case ReviewDialog::NAME_DIALOG: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Name); @@ -359,21 +329,17 @@ namespace MWGui { if (mPickClassDialog) { - const std::string &classId = mPickClassDialog->getClassId(); + const ESM::RefId& classId = mPickClassDialog->getClassId(); if (!classId.empty()) MWBase::Environment::get().getMechanicsManager()->setPlayerClass(classId); - const ESM::Class *klass = - MWBase::Environment::get().getWorld()->getStore().get().find(classId); - if (klass) + const ESM::Class* pickedClass = MWBase::Environment::get().getESMStore()->get().find(classId); + if (pickedClass) { - mPlayerClass = *klass; + mPlayerClass = *pickedClass; } - MWBase::Environment::get().getWindowManager()->removeDialog(mPickClassDialog); - mPickClassDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mPickClassDialog)); } - - updatePlayerHealth(); } void CharacterCreation::onPickClassDialogDone(WindowBase* parWindow) @@ -393,12 +359,11 @@ namespace MWGui void CharacterCreation::onClassChoice(int _index) { - MWBase::Environment::get().getWindowManager()->removeDialog(mClassChoiceDialog); - mClassChoiceDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mClassChoiceDialog)); MWBase::Environment::get().getWindowManager()->popGuiMode(); - switch(_index) + switch (_index) { case ClassChoiceDialog::Class_Generate: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_ClassGenerate); @@ -412,7 +377,6 @@ namespace MWGui case ClassChoiceDialog::Class_Back: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Race); break; - }; } @@ -422,8 +386,7 @@ namespace MWGui { mPlayerName = mNameDialog->getTextInput(); MWBase::Environment::get().getMechanicsManager()->setPlayerName(mPlayerName); - MWBase::Environment::get().getWindowManager()->removeDialog(mNameDialog); - mNameDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mNameDialog)); } handleDialogDone(CSE_NameChosen, GM_Race); @@ -433,23 +396,17 @@ namespace MWGui { if (mRaceDialog) { - const ESM::NPC &data = mRaceDialog->getResult(); + const ESM::NPC& data = mRaceDialog->getResult(); mPlayerRaceId = data.mRace; - if (!mPlayerRaceId.empty()) { + if (!mPlayerRaceId.empty()) + { MWBase::Environment::get().getMechanicsManager()->setPlayerRace( - data.mRace, - data.isMale(), - data.mHead, - data.mHair - ); + data.mRace, data.isMale(), data.mHead, data.mHair); } MWBase::Environment::get().getWindowManager()->getInventoryWindow()->rebuildAvatar(); - MWBase::Environment::get().getWindowManager()->removeDialog(mRaceDialog); - mRaceDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mRaceDialog)); } - - updatePlayerHealth(); } void CharacterCreation::onRaceDialogBack() @@ -474,11 +431,8 @@ namespace MWGui mPlayerBirthSignId = mBirthSignDialog->getBirthId(); if (!mPlayerBirthSignId.empty()) MWBase::Environment::get().getMechanicsManager()->setPlayerBirthsign(mPlayerBirthSignId); - MWBase::Environment::get().getWindowManager()->removeDialog(mBirthSignDialog); - mBirthSignDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mBirthSignDialog)); } - - updatePlayerHealth(); } void CharacterCreation::onBirthSignDialogDone(WindowBase* parWindow) @@ -500,34 +454,34 @@ namespace MWGui { if (mCreateClassDialog) { - ESM::Class klass; - klass.mName = mCreateClassDialog->getName(); - klass.mDescription = mCreateClassDialog->getDescription(); - klass.mData.mSpecialization = mCreateClassDialog->getSpecializationId(); - klass.mData.mIsPlayable = 0x1; - - std::vector attributes = mCreateClassDialog->getFavoriteAttributes(); - assert(attributes.size() == 2); - klass.mData.mAttribute[0] = attributes[0]; - klass.mData.mAttribute[1] = attributes[1]; - - std::vector majorSkills = mCreateClassDialog->getMajorSkills(); - std::vector minorSkills = mCreateClassDialog->getMinorSkills(); - assert(majorSkills.size() >= sizeof(klass.mData.mSkills)/sizeof(klass.mData.mSkills[0])); - assert(minorSkills.size() >= sizeof(klass.mData.mSkills)/sizeof(klass.mData.mSkills[0])); - for (size_t i = 0; i < sizeof(klass.mData.mSkills)/sizeof(klass.mData.mSkills[0]); ++i) + ESM::Class createdClass; + createdClass.mName = mCreateClassDialog->getName(); + createdClass.mDescription = mCreateClassDialog->getDescription(); + createdClass.mData.mSpecialization = mCreateClassDialog->getSpecializationId(); + createdClass.mData.mIsPlayable = 0x1; + createdClass.mRecordFlags = 0; + + std::vector attributes = mCreateClassDialog->getFavoriteAttributes(); + assert(attributes.size() >= createdClass.mData.mAttribute.size()); + for (size_t i = 0; i < createdClass.mData.mAttribute.size(); ++i) + createdClass.mData.mAttribute[i] = ESM::Attribute::refIdToIndex(attributes[i]); + + std::vector majorSkills = mCreateClassDialog->getMajorSkills(); + std::vector minorSkills = mCreateClassDialog->getMinorSkills(); + assert(majorSkills.size() >= createdClass.mData.mSkills.size()); + assert(minorSkills.size() >= createdClass.mData.mSkills.size()); + for (size_t i = 0; i < createdClass.mData.mSkills.size(); ++i) { - klass.mData.mSkills[i][1] = majorSkills[i]; - klass.mData.mSkills[i][0] = minorSkills[i]; + createdClass.mData.mSkills[i][1] = ESM::Skill::refIdToIndex(majorSkills[i]); + createdClass.mData.mSkills[i][0] = ESM::Skill::refIdToIndex(minorSkills[i]); } - MWBase::Environment::get().getMechanicsManager()->setPlayerClass(klass); - mPlayerClass = klass; + MWBase::Environment::get().getMechanicsManager()->setPlayerClass(createdClass); + mPlayerClass = std::move(createdClass); // Do not delete dialog, so that choices are remembered in case we want to go back and adjust them later mCreateClassDialog->setVisible(false); } - updatePlayerHealth(); } void CharacterCreation::onCreateClassDialogDone(WindowBase* parWindow) @@ -550,8 +504,7 @@ namespace MWGui { MWBase::Environment::get().getSoundManager()->stopSay(); - MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassQuestionDialog); - mGenerateClassQuestionDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mGenerateClassQuestionDialog)); if (_index < 0 || _index >= 3) { @@ -578,100 +531,99 @@ namespace MWGui unsigned combat = mGenerateClassSpecializations[0]; unsigned magic = mGenerateClassSpecializations[1]; unsigned stealth = mGenerateClassSpecializations[2]; - + std::string_view className; if (combat > 7) { - mGenerateClass = "Warrior"; + className = "Warrior"; } else if (magic > 7) { - mGenerateClass = "Mage"; + className = "Mage"; } else if (stealth > 7) { - mGenerateClass = "Thief"; + className = "Thief"; } else { switch (combat) { case 4: - mGenerateClass = "Rogue"; + className = "Rogue"; break; case 5: if (stealth == 3) - mGenerateClass = "Scout"; + className = "Scout"; else - mGenerateClass = "Archer"; + className = "Archer"; break; case 6: if (stealth == 1) - mGenerateClass = "Barbarian"; + className = "Barbarian"; else if (stealth == 3) - mGenerateClass = "Crusader"; + className = "Crusader"; else - mGenerateClass = "Knight"; + className = "Knight"; break; case 7: - mGenerateClass = "Warrior"; + className = "Warrior"; break; default: switch (magic) { case 4: - mGenerateClass = "Spellsword"; + className = "Spellsword"; break; case 5: - mGenerateClass = "Witchhunter"; + className = "Witchhunter"; break; case 6: if (combat == 2) - mGenerateClass = "Sorcerer"; + className = "Sorcerer"; else if (combat == 3) - mGenerateClass = "Healer"; + className = "Healer"; else - mGenerateClass = "Battlemage"; + className = "Battlemage"; break; case 7: - mGenerateClass = "Mage"; + className = "Mage"; break; default: switch (stealth) { case 3: if (magic == 3) - mGenerateClass = "Bard"; // unreachable + className = "Bard"; // unreachable else - mGenerateClass = "Warrior"; + className = "Warrior"; break; case 5: if (magic == 3) - mGenerateClass = "Monk"; + className = "Monk"; else - mGenerateClass = "Pilgrim"; + className = "Pilgrim"; break; case 6: if (magic == 1) - mGenerateClass = "Agent"; + className = "Agent"; else if (magic == 3) - mGenerateClass = "Assassin"; + className = "Assassin"; else - mGenerateClass = "Acrobat"; + className = "Acrobat"; break; case 7: - mGenerateClass = "Thief"; + className = "Thief"; break; default: - mGenerateClass = "Warrior"; + className = "Warrior"; } } } } + mGenerateClass = ESM::RefId::stringRefId(className); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mGenerateClassResultDialog)); - MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassResultDialog); - mGenerateClassResultDialog = nullptr; - - mGenerateClassResultDialog = new GenerateClassResultDialog(); + mGenerateClassResultDialog = std::make_unique(); mGenerateClassResultDialog->setClassId(mGenerateClass); mGenerateClassResultDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onGenerateClassBack); mGenerateClassResultDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onGenerateClassDone); @@ -686,10 +638,9 @@ namespace MWGui return; } - MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassQuestionDialog); - mGenerateClassQuestionDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mGenerateClassQuestionDialog)); - mGenerateClassQuestionDialog = new InfoBoxDialog(); + mGenerateClassQuestionDialog = std::make_unique(); Step step = sGenerateClassSteps(mGenerateClassStep); mGenerateClassResponses[0] = step.mResponses[0].mSpecialization; @@ -702,25 +653,23 @@ namespace MWGui buttons.push_back(step.mResponses[1].mText); buttons.push_back(step.mResponses[2].mText); mGenerateClassQuestionDialog->setButtons(buttons); - mGenerateClassQuestionDialog->eventButtonSelected += MyGUI::newDelegate(this, &CharacterCreation::onClassQuestionChosen); + mGenerateClassQuestionDialog->eventButtonSelected + += MyGUI::newDelegate(this, &CharacterCreation::onClassQuestionChosen); mGenerateClassQuestionDialog->setVisible(true); - MWBase::Environment::get().getSoundManager()->say(step.mSound); + MWBase::Environment::get().getSoundManager()->say(Misc::ResourceHelpers::correctSoundPath(step.mSound)); } void CharacterCreation::selectGeneratedClass() { - MWBase::Environment::get().getWindowManager()->removeDialog(mGenerateClassResultDialog); - mGenerateClassResultDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mGenerateClassResultDialog)); MWBase::Environment::get().getMechanicsManager()->setPlayerClass(mGenerateClass); - const ESM::Class *klass = - MWBase::Environment::get().getWorld()->getStore().get().find(mGenerateClass); + const ESM::Class* generatedClass + = MWBase::Environment::get().getESMStore()->get().find(mGenerateClass); - mPlayerClass = *klass; - - updatePlayerHealth(); + mPlayerClass = *generatedClass; } void CharacterCreation::onGenerateClassBack() @@ -738,18 +687,7 @@ namespace MWGui handleDialogDone(CSE_ClassChosen, GM_Birth); } - CharacterCreation::~CharacterCreation() - { - delete mNameDialog; - delete mRaceDialog; - delete mClassChoiceDialog; - delete mGenerateClassQuestionDialog; - delete mGenerateClassResultDialog; - delete mPickClassDialog; - delete mCreateClassDialog; - delete mBirthSignDialog; - delete mReviewDialog; - } + CharacterCreation::~CharacterCreation() = default; void CharacterCreation::handleDialogDone(CSE currentStage, int nextMode) { diff --git a/apps/openmw/mwgui/charactercreation.hpp b/apps/openmw/mwgui/charactercreation.hpp index beb8715fcdb..b5bd35b49cd 100644 --- a/apps/openmw/mwgui/charactercreation.hpp +++ b/apps/openmw/mwgui/charactercreation.hpp @@ -1,9 +1,10 @@ #ifndef CHARACTER_CREATION_HPP #define CHARACTER_CREATION_HPP -#include +#include #include +#include #include #include "statswatcher.hpp" @@ -37,99 +38,98 @@ namespace MWGui class CharacterCreation : public StatsListener { public: - typedef std::vector SkillList; + CharacterCreation(osg::Group* parent, Resource::ResourceSystem* resourceSystem); + virtual ~CharacterCreation(); - CharacterCreation(osg::Group* parent, Resource::ResourceSystem* resourceSystem); - virtual ~CharacterCreation(); + // Show a dialog + void spawnDialog(const char id); - //Show a dialog - void spawnDialog(const char id); + void setAttribute(ESM::RefId id, const MWMechanics::AttributeValue& value) override; + void setValue(std::string_view id, const MWMechanics::DynamicStat& value) override; + void setValue(ESM::RefId id, const MWMechanics::SkillValue& value) override; + void configureSkills(const std::vector& major, const std::vector& minor) override; - void setValue (const std::string& id, const MWMechanics::AttributeValue& value) override; - void setValue (const std::string& id, const MWMechanics::DynamicStat& value) override; - void setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) override; - void configureSkills(const SkillList& major, const SkillList& minor) override; - - void onFrame(float duration); + void onFrame(float duration); private: - osg::Group* mParent; - Resource::ResourceSystem* mResourceSystem; - - SkillList mPlayerMajorSkills, mPlayerMinorSkills; - std::map mPlayerAttributes; - std::map mPlayerSkillValues; - - //Dialogs - TextInputDialog* mNameDialog; - RaceDialog* mRaceDialog; - ClassChoiceDialog* mClassChoiceDialog; - InfoBoxDialog* mGenerateClassQuestionDialog; - GenerateClassResultDialog* mGenerateClassResultDialog; - PickClassDialog* mPickClassDialog; - CreateClassDialog* mCreateClassDialog; - BirthDialog* mBirthSignDialog; - ReviewDialog* mReviewDialog; - - //Player data - std::string mPlayerName; - std::string mPlayerRaceId; - std::string mPlayerBirthSignId; - ESM::Class mPlayerClass; - - //Class generation vars - unsigned mGenerateClassStep; // Keeps track of current step in Generate Class dialog - ESM::Class::Specialization mGenerateClassResponses[3]; - unsigned mGenerateClassSpecializations[3]; // A counter for each specialization which is increased when an answer is chosen - std::string mGenerateClass; // In order: Combat, Magic, Stealth - - ////Dialog events - //Name dialog - void onNameDialogDone(WindowBase* parWindow); - - //Race dialog - void onRaceDialogDone(WindowBase* parWindow); - void onRaceDialogBack(); - void selectRace(); - - //Class dialogs - void onClassChoice(int _index); - void onPickClassDialogDone(WindowBase* parWindow); - void onPickClassDialogBack(); - void onCreateClassDialogDone(WindowBase* parWindow); - void onCreateClassDialogBack(); - void showClassQuestionDialog(); - void onClassQuestionChosen(int _index); - void onGenerateClassBack(); - void onGenerateClassDone(WindowBase* parWindow); - void selectGeneratedClass(); - void selectCreatedClass(); - void selectPickedClass(); - - //Birthsign dialog - void onBirthSignDialogDone(WindowBase* parWindow); - void onBirthSignDialogBack(); - void selectBirthSign(); - - //Review dialog - void onReviewDialogDone(WindowBase* parWindow); - void onReviewDialogBack(); - void onReviewActivateDialog(int parDialog); - - enum CSE //Creation Stage Enum - { - CSE_NotStarted, - CSE_NameChosen, - CSE_RaceChosen, - CSE_ClassChosen, - CSE_BirthSignChosen, - CSE_ReviewBack, - CSE_ReviewNext - }; - - CSE mCreationStage; // Which state the character creating is in, controls back/next/ok buttons - - void handleDialogDone(CSE currentStage, int nextMode); + osg::Group* mParent; + Resource::ResourceSystem* mResourceSystem; + + std::vector mPlayerMajorSkills, mPlayerMinorSkills; + std::map mPlayerAttributes; + std::map mPlayerSkillValues; + + // Dialogs + std::unique_ptr mNameDialog; + std::unique_ptr mRaceDialog; + std::unique_ptr mClassChoiceDialog; + std::unique_ptr mGenerateClassQuestionDialog; + std::unique_ptr mGenerateClassResultDialog; + std::unique_ptr mPickClassDialog; + std::unique_ptr mCreateClassDialog; + std::unique_ptr mBirthSignDialog; + std::unique_ptr mReviewDialog; + + // Player data + std::string mPlayerName; + ESM::RefId mPlayerRaceId; + ESM::RefId mPlayerBirthSignId; + ESM::Class mPlayerClass; + + // Class generation vars + unsigned mGenerateClassStep; // Keeps track of current step in Generate Class dialog + ESM::Class::Specialization mGenerateClassResponses[3]; + unsigned mGenerateClassSpecializations[3]; // A counter for each specialization which is increased when an + // answer is chosen + ESM::RefId mGenerateClass; // In order: Combat, Magic, Stealth + + ////Dialog events + // Name dialog + void onNameDialogDone(WindowBase* parWindow); + + // Race dialog + void onRaceDialogDone(WindowBase* parWindow); + void onRaceDialogBack(); + void selectRace(); + + // Class dialogs + void onClassChoice(int _index); + void onPickClassDialogDone(WindowBase* parWindow); + void onPickClassDialogBack(); + void onCreateClassDialogDone(WindowBase* parWindow); + void onCreateClassDialogBack(); + void showClassQuestionDialog(); + void onClassQuestionChosen(int _index); + void onGenerateClassBack(); + void onGenerateClassDone(WindowBase* parWindow); + void selectGeneratedClass(); + void selectCreatedClass(); + void selectPickedClass(); + + // Birthsign dialog + void onBirthSignDialogDone(WindowBase* parWindow); + void onBirthSignDialogBack(); + void selectBirthSign(); + + // Review dialog + void onReviewDialogDone(WindowBase* parWindow); + void onReviewDialogBack(); + void onReviewActivateDialog(int parDialog); + + enum CSE // Creation Stage Enum + { + CSE_NotStarted, + CSE_NameChosen, + CSE_RaceChosen, + CSE_ClassChosen, + CSE_BirthSignChosen, + CSE_ReviewBack, + CSE_ReviewNext + }; + + CSE mCreationStage; // Which state the character creating is in, controls back/next/ok buttons + + void handleDialogDone(CSE currentStage, int nextMode); }; } diff --git a/apps/openmw/mwgui/class.cpp b/apps/openmw/mwgui/class.cpp index ee5fe593995..839f0f5072d 100644 --- a/apps/openmw/mwgui/class.cpp +++ b/apps/openmw/mwgui/class.cpp @@ -1,25 +1,31 @@ #include "class.hpp" +#include #include #include -#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/esmstore.hpp" #include +#include +#include +#include +#include #include "tooltips.hpp" namespace { - bool sortClasses(const std::pair& left, const std::pair& right) + bool sortClasses(const std::pair& left, const std::pair& right) { return left.second.compare(right.second) < 0; } @@ -32,9 +38,10 @@ namespace MWGui /* GenerateClassResultDialog */ GenerateClassResultDialog::GenerateClassResultDialog() - : WindowModal("openmw_chargen_generate_class_result.layout") + : WindowModal("openmw_chargen_generate_class_result.layout") { - setText("ReflectT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sMessageQuestionAnswer1", "")); + setText("ReflectT", + MWBase::Environment::get().getWindowManager()->getGameSettingString("sMessageQuestionAnswer1", {})); getWidget(mClassImage, "ClassImage"); getWidget(mClassName, "ClassName"); @@ -52,18 +59,14 @@ namespace MWGui center(); } - std::string GenerateClassResultDialog::getClassId() const - { - return mClassName->getCaption(); - } - - void GenerateClassResultDialog::setClassId(const std::string &classId) + void GenerateClassResultDialog::setClassId(const ESM::RefId& classId) { mCurrentClassId = classId; setClassImage(mClassImage, mCurrentClassId); - mClassName->setCaption(MWBase::Environment::get().getWorld()->getStore().get().find(mCurrentClassId)->mName); + mClassName->setCaption( + MWBase::Environment::get().getESMStore()->get().find(mCurrentClassId)->mName); center(); } @@ -83,7 +86,7 @@ namespace MWGui /* PickClassDialog */ PickClassDialog::PickClassDialog() - : WindowModal("openmw_chargen_class.layout") + : WindowModal("openmw_chargen_class.layout") { // Centre dialog center(); @@ -93,9 +96,9 @@ namespace MWGui getWidget(mFavoriteAttribute[0], "FavoriteAttribute0"); getWidget(mFavoriteAttribute[1], "FavoriteAttribute1"); - for(int i = 0; i < 5; i++) + for (int i = 0; i < 5; i++) { - char theIndex = '0'+i; + char theIndex = '0' + i; getWidget(mMajorSkill[i], std::string("MajorSkill").append(1, theIndex)); getWidget(mMinorSkill[i], std::string("MinorSkill").append(1, theIndex)); } @@ -125,14 +128,16 @@ namespace MWGui getWidget(okButton, "OKButton"); if (shown) - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void PickClassDialog::onOpen() { - WindowModal::onOpen (); + WindowModal::onOpen(); updateClasses(); updateStats(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mClassList); @@ -140,21 +145,20 @@ namespace MWGui // Show the current class by default MWWorld::Ptr player = MWMechanics::getPlayer(); - const std::string &classId = - player.get()->mBase->mClass; + const ESM::RefId& classId = player.get()->mBase->mClass; if (!classId.empty()) setClassId(classId); } - void PickClassDialog::setClassId(const std::string &classId) + void PickClassDialog::setClassId(const ESM::RefId& classId) { mCurrentClassId = classId; mClassList->setIndexSelected(MyGUI::ITEM_NONE); size_t count = mClassList->getItemCount(); for (size_t i = 0; i < count; ++i) { - if (Misc::StringUtils::ciEqual(*mClassList->getItemDataAt(i), classId)) + if (*mClassList->getItemDataAt(i) == classId) { mClassList->setIndexSelected(i); break; @@ -168,7 +172,7 @@ namespace MWGui void PickClassDialog::onOkClicked(MyGUI::Widget* _sender) { - if(mClassList->getIndexSelected() == MyGUI::ITEM_NONE) + if (mClassList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } @@ -181,7 +185,7 @@ namespace MWGui void PickClassDialog::onAccept(MyGUI::ListBox* _sender, size_t _index) { onSelectClass(_sender, _index); - if(mClassList->getIndexSelected() == MyGUI::ITEM_NONE) + if (mClassList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } @@ -191,11 +195,11 @@ namespace MWGui if (_index == MyGUI::ITEM_NONE) return; - const std::string *classId = mClassList->getItemDataAt(_index); - if (Misc::StringUtils::ciEqual(mCurrentClassId, *classId)) + const ESM::RefId& classId = *mClassList->getItemDataAt(_index); + if (mCurrentClassId == classId) return; - mCurrentClassId = *classId; + mCurrentClassId = classId; updateStats(); } @@ -205,9 +209,9 @@ namespace MWGui { mClassList->removeAllItems(); - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - std::vector > items; // class id, class name + std::vector> items; // class id, class name for (const ESM::Class& classInfo : store.get()) { bool playable = (classInfo.mData.mIsPlayable != 0); @@ -224,14 +228,14 @@ namespace MWGui int index = 0; for (auto& itemPair : items) { - const std::string &id = itemPair.first; + const ESM::RefId& id = itemPair.first; mClassList->addItem(itemPair.second, id); if (mCurrentClassId.empty()) { mCurrentClassId = id; mClassList->setIndexSelected(index); } - else if (Misc::StringUtils::ciEqual(id, mCurrentClassId)) + else if (id == mCurrentClassId) { mClassList->setIndexSelected(index); } @@ -243,33 +247,32 @@ namespace MWGui { if (mCurrentClassId.empty()) return; - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Class *klass = store.get().search(mCurrentClassId); - if (!klass) + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const ESM::Class* currentClass = store.get().search(mCurrentClassId); + if (!currentClass) return; - ESM::Class::Specialization specialization = static_cast(klass->mData.mSpecialization); + ESM::Class::Specialization specialization + = static_cast(currentClass->mData.mSpecialization); - static const char *specIds[3] = { - "sSpecializationCombat", - "sSpecializationMagic", - "sSpecializationStealth" - }; - std::string specName = MWBase::Environment::get().getWindowManager()->getGameSettingString(specIds[specialization], specIds[specialization]); + std::string specName{ MWBase::Environment::get().getWindowManager()->getGameSettingString( + ESM::Class::sGmstSpecializationIds[specialization], ESM::Class::sGmstSpecializationIds[specialization]) }; mSpecializationName->setCaption(specName); ToolTips::createSpecializationToolTip(mSpecializationName, specName, specialization); - mFavoriteAttribute[0]->setAttributeId(klass->mData.mAttribute[0]); - mFavoriteAttribute[1]->setAttributeId(klass->mData.mAttribute[1]); + mFavoriteAttribute[0]->setAttributeId(ESM::Attribute::indexToRefId(currentClass->mData.mAttribute[0])); + mFavoriteAttribute[1]->setAttributeId(ESM::Attribute::indexToRefId(currentClass->mData.mAttribute[1])); ToolTips::createAttributeToolTip(mFavoriteAttribute[0], mFavoriteAttribute[0]->getAttributeId()); ToolTips::createAttributeToolTip(mFavoriteAttribute[1], mFavoriteAttribute[1]->getAttributeId()); - for (int i = 0; i < 5; ++i) + for (size_t i = 0; i < currentClass->mData.mSkills.size(); ++i) { - mMinorSkill[i]->setSkillNumber(klass->mData.mSkills[i][0]); - mMajorSkill[i]->setSkillNumber(klass->mData.mSkills[i][1]); - ToolTips::createSkillToolTip(mMinorSkill[i], klass->mData.mSkills[i][0]); - ToolTips::createSkillToolTip(mMajorSkill[i], klass->mData.mSkills[i][1]); + ESM::RefId minor = ESM::Skill::indexToRefId(currentClass->mData.mSkills[i][0]); + ESM::RefId major = ESM::Skill::indexToRefId(currentClass->mData.mSkills[i][1]); + mMinorSkill[i]->setSkillId(minor); + mMajorSkill[i]->setSkillId(major); + ToolTips::createSkillToolTip(mMinorSkill[i], minor); + ToolTips::createSkillToolTip(mMajorSkill[i], major); } setClassImage(mClassImage, mCurrentClassId); @@ -303,7 +306,7 @@ namespace MWGui width = std::max(width, child->getWidth()); pos += child->getHeight() + margin; } - width += margin*2; + width += margin * 2; widget->setSize(width, pos); } @@ -318,7 +321,7 @@ namespace MWGui center(); } - void InfoBoxDialog::setText(const std::string &str) + void InfoBoxDialog::setText(const std::string& str) { mText->setCaption(str); mTextBox->setVisible(!str.empty()); @@ -330,7 +333,7 @@ namespace MWGui return mText->getCaption(); } - void InfoBoxDialog::setButtons(ButtonList &buttons) + void InfoBoxDialog::setButtons(ButtonList& buttons) { for (MyGUI::Button* button : this->mButtons) { @@ -341,9 +344,10 @@ namespace MWGui // TODO: The buttons should be generated from a template in the layout file, ie. cloning an existing widget MyGUI::Button* button; MyGUI::IntCoord coord = MyGUI::IntCoord(0, 0, mButtonBar->getWidth(), 10); - for (const std::string &text : buttons) + for (const std::string& text : buttons) { - button = mButtonBar->createWidget("MW_Button", coord, MyGUI::Align::Top | MyGUI::Align::HCenter, ""); + button = mButtonBar->createWidget( + "MW_Button", coord, MyGUI::Align::Top | MyGUI::Align::HCenter, {}); button->getSubWidgetText()->setWordWrap(true); button->setCaption(text); fitToText(button); @@ -383,44 +387,49 @@ namespace MWGui ClassChoiceDialog::ClassChoiceDialog() : InfoBoxDialog() { - setText(""); + setText({}); ButtonList buttons; - buttons.push_back(MWBase::Environment::get().getWindowManager()->getGameSettingString("sClassChoiceMenu1", "")); - buttons.push_back(MWBase::Environment::get().getWindowManager()->getGameSettingString("sClassChoiceMenu2", "")); - buttons.push_back(MWBase::Environment::get().getWindowManager()->getGameSettingString("sClassChoiceMenu3", "")); - buttons.push_back(MWBase::Environment::get().getWindowManager()->getGameSettingString("sBack", "")); + buttons.emplace_back( + MWBase::Environment::get().getWindowManager()->getGameSettingString("sClassChoiceMenu1", {})); + buttons.emplace_back( + MWBase::Environment::get().getWindowManager()->getGameSettingString("sClassChoiceMenu2", {})); + buttons.emplace_back( + MWBase::Environment::get().getWindowManager()->getGameSettingString("sClassChoiceMenu3", {})); + buttons.emplace_back(MWBase::Environment::get().getWindowManager()->getGameSettingString("sBack", {})); setButtons(buttons); } /* CreateClassDialog */ CreateClassDialog::CreateClassDialog() - : WindowModal("openmw_chargen_create_class.layout") - , mSpecDialog(nullptr) - , mAttribDialog(nullptr) - , mSkillDialog(nullptr) - , mDescDialog(nullptr) - , mAffectedAttribute(nullptr) - , mAffectedSkill(nullptr) + : WindowModal("openmw_chargen_create_class.layout") + , mAffectedAttribute(nullptr) + , mAffectedSkill(nullptr) { // Centre dialog center(); - setText("SpecializationT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sChooseClassMenu1", "Specialization")); + setText("SpecializationT", + MWBase::Environment::get().getWindowManager()->getGameSettingString("sChooseClassMenu1", "Specialization")); getWidget(mSpecializationName, "SpecializationName"); - mSpecializationName->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onSpecializationClicked); + mSpecializationName->eventMouseButtonClick + += MyGUI::newDelegate(this, &CreateClassDialog::onSpecializationClicked); - setText("FavoriteAttributesT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sChooseClassMenu2", "Favorite Attributes:")); + setText("FavoriteAttributesT", + MWBase::Environment::get().getWindowManager()->getGameSettingString( + "sChooseClassMenu2", "Favorite Attributes:")); getWidget(mFavoriteAttribute0, "FavoriteAttribute0"); getWidget(mFavoriteAttribute1, "FavoriteAttribute1"); mFavoriteAttribute0->eventClicked += MyGUI::newDelegate(this, &CreateClassDialog::onAttributeClicked); mFavoriteAttribute1->eventClicked += MyGUI::newDelegate(this, &CreateClassDialog::onAttributeClicked); - setText("MajorSkillT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSkillClassMajor", "")); - setText("MinorSkillT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSkillClassMinor", "")); - for(int i = 0; i < 5; i++) + setText( + "MajorSkillT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSkillClassMajor", {})); + setText( + "MinorSkillT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSkillClassMinor", {})); + for (int i = 0; i < 5; i++) { - char theIndex = '0'+i; + char theIndex = '0' + i; getWidget(mMajorSkill[i], std::string("MajorSkill").append(1, theIndex)); getWidget(mMinorSkill[i], std::string("MinorSkill").append(1, theIndex)); mSkills.push_back(mMajorSkill[i]); @@ -432,7 +441,7 @@ namespace MWGui skill->eventClicked += MyGUI::newDelegate(this, &CreateClassDialog::onSkillClicked); } - setText("LabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sName", "")); + setText("LabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sName", {})); getWidget(mEditName, "EditName"); // Make sure the edit box has focus @@ -471,13 +480,7 @@ namespace MWGui update(); } - CreateClassDialog::~CreateClassDialog() - { - delete mSpecDialog; - delete mAttribDialog; - delete mSkillDialog; - delete mDescDialog; - } + CreateClassDialog::~CreateClassDialog() = default; void CreateClassDialog::update() { @@ -506,30 +509,32 @@ namespace MWGui return mSpecializationId; } - std::vector CreateClassDialog::getFavoriteAttributes() const + std::vector CreateClassDialog::getFavoriteAttributes() const { - std::vector v; + std::vector v; v.push_back(mFavoriteAttribute0->getAttributeId()); v.push_back(mFavoriteAttribute1->getAttributeId()); return v; } - std::vector CreateClassDialog::getMajorSkills() const + std::vector CreateClassDialog::getMajorSkills() const { - std::vector v; - for(int i = 0; i < 5; i++) + std::vector v; + v.reserve(mMajorSkill.size()); + for (const auto& widget : mMajorSkill) { - v.push_back(mMajorSkill[i]->getSkillId()); + v.push_back(widget->getSkillId()); } return v; } - std::vector CreateClassDialog::getMinorSkills() const + std::vector CreateClassDialog::getMinorSkills() const { - std::vector v; - for(int i=0; i < 5; i++) + std::vector v; + v.reserve(mMinorSkill.size()); + for (const auto& widget : mMinorSkill) { - v.push_back(mMinorSkill[i]->getSkillId()); + v.push_back(widget->getSkillId()); } return v; } @@ -540,32 +545,26 @@ namespace MWGui getWidget(okButton, "OKButton"); if (shown) - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } // widget controls void CreateClassDialog::onDialogCancel() { - MWBase::Environment::get().getWindowManager()->removeDialog(mSpecDialog); - mSpecDialog = nullptr; - - MWBase::Environment::get().getWindowManager()->removeDialog(mAttribDialog); - mAttribDialog = nullptr; - - MWBase::Environment::get().getWindowManager()->removeDialog(mSkillDialog); - mSkillDialog = nullptr; - - MWBase::Environment::get().getWindowManager()->removeDialog(mDescDialog); - mDescDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSpecDialog)); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mAttribDialog)); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSkillDialog)); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mDescDialog)); } void CreateClassDialog::onSpecializationClicked(MyGUI::Widget* _sender) { - delete mSpecDialog; - mSpecDialog = new SelectSpecializationDialog(); + mSpecDialog = std::make_unique(); mSpecDialog->eventCancel += MyGUI::newDelegate(this, &CreateClassDialog::onDialogCancel); mSpecDialog->eventItemSelected += MyGUI::newDelegate(this, &CreateClassDialog::onSpecializationSelected); mSpecDialog->setVisible(true); @@ -576,27 +575,22 @@ namespace MWGui mSpecializationId = mSpecDialog->getSpecializationId(); setSpecialization(mSpecializationId); - MWBase::Environment::get().getWindowManager()->removeDialog(mSpecDialog); - mSpecDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSpecDialog)); } void CreateClassDialog::setSpecialization(int id) { - mSpecializationId = (ESM::Class::Specialization) id; - static const char *specIds[3] = { - "sSpecializationCombat", - "sSpecializationMagic", - "sSpecializationStealth" - }; - std::string specName = MWBase::Environment::get().getWindowManager()->getGameSettingString(specIds[mSpecializationId], specIds[mSpecializationId]); + mSpecializationId = ESM::Class::Specialization(id); + std::string specName{ MWBase::Environment::get().getWindowManager()->getGameSettingString( + ESM::Class::sGmstSpecializationIds[mSpecializationId], + ESM::Class::sGmstSpecializationIds[mSpecializationId]) }; mSpecializationName->setCaption(specName); ToolTips::createSpecializationToolTip(mSpecializationName, specName, mSpecializationId); } void CreateClassDialog::onAttributeClicked(Widgets::MWAttributePtr _sender) { - delete mAttribDialog; - mAttribDialog = new SelectAttributeDialog(); + mAttribDialog = std::make_unique(); mAffectedAttribute = _sender; mAttribDialog->eventCancel += MyGUI::newDelegate(this, &CreateClassDialog::onDialogCancel); mAttribDialog->eventItemSelected += MyGUI::newDelegate(this, &CreateClassDialog::onAttributeSelected); @@ -605,7 +599,7 @@ namespace MWGui void CreateClassDialog::onAttributeSelected() { - ESM::Attribute::AttributeID id = mAttribDialog->getAttributeId(); + ESM::RefId id = mAttribDialog->getAttributeId(); if (mAffectedAttribute == mFavoriteAttribute0) { if (mFavoriteAttribute1->getAttributeId() == id) @@ -617,16 +611,14 @@ namespace MWGui mFavoriteAttribute0->setAttributeId(mFavoriteAttribute1->getAttributeId()); } mAffectedAttribute->setAttributeId(id); - MWBase::Environment::get().getWindowManager()->removeDialog(mAttribDialog); - mAttribDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mAttribDialog)); update(); } void CreateClassDialog::onSkillClicked(Widgets::MWSkillPtr _sender) { - delete mSkillDialog; - mSkillDialog = new SelectSkillDialog(); + mSkillDialog = std::make_unique(); mAffectedSkill = _sender; mSkillDialog->eventCancel += MyGUI::newDelegate(this, &CreateClassDialog::onDialogCancel); mSkillDialog->eventItemSelected += MyGUI::newDelegate(this, &CreateClassDialog::onSkillSelected); @@ -635,7 +627,7 @@ namespace MWGui void CreateClassDialog::onSkillSelected() { - ESM::Skill::SkillEnum id = mSkillDialog->getSkillId(); + ESM::RefId id = mSkillDialog->getSkillId(); // Avoid duplicate skills by swapping any skill field that matches the selected one for (Widgets::MWSkillPtr& skill : mSkills) @@ -650,14 +642,13 @@ namespace MWGui } mAffectedSkill->setSkillId(mSkillDialog->getSkillId()); - MWBase::Environment::get().getWindowManager()->removeDialog(mSkillDialog); - mSkillDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSkillDialog)); update(); } void CreateClassDialog::onDescriptionClicked(MyGUI::Widget* _sender) { - mDescDialog = new DescriptionDialog(); + mDescDialog = std::make_unique(); mDescDialog->setTextInput(mDescription); mDescDialog->eventDone += MyGUI::newDelegate(this, &CreateClassDialog::onDescriptionEntered); mDescDialog->setVisible(true); @@ -666,13 +657,12 @@ namespace MWGui void CreateClassDialog::onDescriptionEntered(WindowBase* parWindow) { mDescription = mDescDialog->getTextInput(); - MWBase::Environment::get().getWindowManager()->removeDialog(mDescDialog); - mDescDialog = nullptr; + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mDescDialog)); } void CreateClassDialog::onOkClicked(MyGUI::Widget* _sender) { - if(getName().size() <= 0) + if (getName().size() <= 0) return; eventDone(this); } @@ -685,7 +675,7 @@ namespace MWGui /* SelectSpecializationDialog */ SelectSpecializationDialog::SelectSpecializationDialog() - : WindowModal("openmw_chargen_select_specialization.layout") + : WindowModal("openmw_chargen_select_specialization.layout") { // Centre dialog center(); @@ -693,16 +683,22 @@ namespace MWGui getWidget(mSpecialization0, "Specialization0"); getWidget(mSpecialization1, "Specialization1"); getWidget(mSpecialization2, "Specialization2"); - std::string combat = MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Class::sGmstSpecializationIds[ESM::Class::Combat], ""); - std::string magic = MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Class::sGmstSpecializationIds[ESM::Class::Magic], ""); - std::string stealth = MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Class::sGmstSpecializationIds[ESM::Class::Stealth], ""); + std::string combat{ MWBase::Environment::get().getWindowManager()->getGameSettingString( + ESM::Class::sGmstSpecializationIds[ESM::Class::Combat], {}) }; + std::string magic{ MWBase::Environment::get().getWindowManager()->getGameSettingString( + ESM::Class::sGmstSpecializationIds[ESM::Class::Magic], {}) }; + std::string stealth{ MWBase::Environment::get().getWindowManager()->getGameSettingString( + ESM::Class::sGmstSpecializationIds[ESM::Class::Stealth], {}) }; mSpecialization0->setCaption(combat); - mSpecialization0->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); + mSpecialization0->eventMouseButtonClick + += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); mSpecialization1->setCaption(magic); - mSpecialization1->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); + mSpecialization1->eventMouseButtonClick + += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); mSpecialization2->setCaption(stealth); - mSpecialization2->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); + mSpecialization2->eventMouseButtonClick + += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); mSpecializationId = ESM::Class::Combat; ToolTips::createSpecializationToolTip(mSpecialization0, combat, ESM::Class::Combat); @@ -714,9 +710,7 @@ namespace MWGui cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onCancelClicked); } - SelectSpecializationDialog::~SelectSpecializationDialog() - { - } + SelectSpecializationDialog::~SelectSpecializationDialog() {} // widget controls @@ -748,38 +742,41 @@ namespace MWGui /* SelectAttributeDialog */ SelectAttributeDialog::SelectAttributeDialog() - : WindowModal("openmw_chargen_select_attribute.layout") - , mAttributeId(ESM::Attribute::Strength) + : WindowModal("openmw_chargen_select_attribute.layout") + , mAttributeId(ESM::Attribute::Strength) { // Centre dialog center(); - for (int i = 0; i < 8; ++i) + const auto& store = MWBase::Environment::get().getWorld()->getStore().get(); + MyGUI::ScrollView* attributes; + getWidget(attributes, "Attributes"); + MyGUI::IntCoord coord{ 0, 0, attributes->getWidth(), 18 }; + for (const ESM::Attribute& attribute : store) { - Widgets::MWAttributePtr attribute; - char theIndex = '0'+i; - - getWidget(attribute, std::string("Attribute").append(1, theIndex)); - attribute->setAttributeId(ESM::Attribute::sAttributeIds[i]); - attribute->eventClicked += MyGUI::newDelegate(this, &SelectAttributeDialog::onAttributeClicked); - ToolTips::createAttributeToolTip(attribute, attribute->getAttributeId()); + auto* widget + = attributes->createWidget("MW_StatNameButtonC", coord, MyGUI::Align::Default); + coord.top += coord.height; + widget->setAttributeId(attribute.mId); + widget->eventClicked += MyGUI::newDelegate(this, &SelectAttributeDialog::onAttributeClicked); + ToolTips::createAttributeToolTip(widget, attribute.mId); } + attributes->setVisibleVScroll(false); + attributes->setCanvasSize(MyGUI::IntSize(attributes->getWidth(), std::max(attributes->getHeight(), coord.top))); + attributes->setVisibleVScroll(true); + attributes->setViewOffset(MyGUI::IntPoint()); + MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectAttributeDialog::onCancelClicked); } - SelectAttributeDialog::~SelectAttributeDialog() - { - } - // widget controls void SelectAttributeDialog::onAttributeClicked(Widgets::MWAttributePtr _sender) { - // TODO: Change MWAttribute to set and get AttributeID enum instead of int - mAttributeId = static_cast(_sender->getAttributeId()); + mAttributeId = _sender->getAttributeId(); eventItemSelected(); } @@ -794,68 +791,42 @@ namespace MWGui return true; } - /* SelectSkillDialog */ SelectSkillDialog::SelectSkillDialog() - : WindowModal("openmw_chargen_select_skill.layout") - , mSkillId(ESM::Skill::Block) + : WindowModal("openmw_chargen_select_skill.layout") + , mSkillId(ESM::Skill::Block) { // Centre dialog center(); - for(int i = 0; i < 9; i++) + std::array, 3> specializations; + getWidget(specializations[ESM::Class::Combat].first, "CombatSkills"); + getWidget(specializations[ESM::Class::Magic].first, "MagicSkills"); + getWidget(specializations[ESM::Class::Stealth].first, "StealthSkills"); + for (auto& [widget, coord] : specializations) { - char theIndex = '0'+i; - getWidget(mCombatSkill[i], std::string("CombatSkill").append(1, theIndex)); - getWidget(mMagicSkill[i], std::string("MagicSkill").append(1, theIndex)); - getWidget(mStealthSkill[i], std::string("StealthSkill").append(1, theIndex)); + coord.width = widget->getCoord().width; + coord.height = 18; + while (widget->getChildCount() > 0) + MyGUI::Gui::getInstance().destroyWidget(widget->getChildAt(0)); } - - struct {Widgets::MWSkillPtr widget; ESM::Skill::SkillEnum skillId;} mSkills[3][9] = { - { - {mCombatSkill[0], ESM::Skill::Block}, - {mCombatSkill[1], ESM::Skill::Armorer}, - {mCombatSkill[2], ESM::Skill::MediumArmor}, - {mCombatSkill[3], ESM::Skill::HeavyArmor}, - {mCombatSkill[4], ESM::Skill::BluntWeapon}, - {mCombatSkill[5], ESM::Skill::LongBlade}, - {mCombatSkill[6], ESM::Skill::Axe}, - {mCombatSkill[7], ESM::Skill::Spear}, - {mCombatSkill[8], ESM::Skill::Athletics} - }, - { - {mMagicSkill[0], ESM::Skill::Enchant}, - {mMagicSkill[1], ESM::Skill::Destruction}, - {mMagicSkill[2], ESM::Skill::Alteration}, - {mMagicSkill[3], ESM::Skill::Illusion}, - {mMagicSkill[4], ESM::Skill::Conjuration}, - {mMagicSkill[5], ESM::Skill::Mysticism}, - {mMagicSkill[6], ESM::Skill::Restoration}, - {mMagicSkill[7], ESM::Skill::Alchemy}, - {mMagicSkill[8], ESM::Skill::Unarmored} - }, - { - {mStealthSkill[0], ESM::Skill::Security}, - {mStealthSkill[1], ESM::Skill::Sneak}, - {mStealthSkill[2], ESM::Skill::Acrobatics}, - {mStealthSkill[3], ESM::Skill::LightArmor}, - {mStealthSkill[4], ESM::Skill::ShortBlade}, - {mStealthSkill[5] ,ESM::Skill::Marksman}, - {mStealthSkill[6] ,ESM::Skill::Mercantile}, - {mStealthSkill[7] ,ESM::Skill::Speechcraft}, - {mStealthSkill[8] ,ESM::Skill::HandToHand} - } - }; - - for (int spec = 0; spec < 3; ++spec) + for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) { - for (int i = 0; i < 9; ++i) - { - mSkills[spec][i].widget->setSkillId(mSkills[spec][i].skillId); - mSkills[spec][i].widget->eventClicked += MyGUI::newDelegate(this, &SelectSkillDialog::onSkillClicked); - ToolTips::createSkillToolTip(mSkills[spec][i].widget, mSkills[spec][i].widget->getSkillId()); - } + auto& [widget, coord] = specializations[skill.mData.mSpecialization]; + auto* skillWidget + = widget->createWidget("MW_StatNameButton", coord, MyGUI::Align::Default); + coord.top += coord.height; + skillWidget->setSkillId(skill.mId); + skillWidget->eventClicked += MyGUI::newDelegate(this, &SelectSkillDialog::onSkillClicked); + ToolTips::createSkillToolTip(skillWidget, skill.mId); + } + for (const auto& [widget, coord] : specializations) + { + widget->setVisibleVScroll(false); + widget->setCanvasSize(MyGUI::IntSize(widget->getWidth(), std::max(widget->getHeight(), coord.top))); + widget->setVisibleVScroll(true); + widget->setViewOffset(MyGUI::IntPoint()); } MyGUI::Button* cancelButton; @@ -863,9 +834,7 @@ namespace MWGui cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSkillDialog::onCancelClicked); } - SelectSkillDialog::~SelectSkillDialog() - { - } + SelectSkillDialog::~SelectSkillDialog() {} // widget controls @@ -889,7 +858,7 @@ namespace MWGui /* DescriptionDialog */ DescriptionDialog::DescriptionDialog() - : WindowModal("openmw_chargen_class_description.layout") + : WindowModal("openmw_chargen_class_description.layout") { // Centre dialog center(); @@ -899,15 +868,14 @@ namespace MWGui MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &DescriptionDialog::onOkClicked); - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sInputMenu1", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sInputMenu1", {}))); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } - DescriptionDialog::~DescriptionDialog() - { - } + DescriptionDialog::~DescriptionDialog() {} // widget controls @@ -916,14 +884,23 @@ namespace MWGui eventDone(this); } - void setClassImage(MyGUI::ImageBox* imageBox, const std::string &classId) + void setClassImage(MyGUI::ImageBox* imageBox, const ESM::RefId& classId) { - std::string classImage = std::string("textures\\levelup\\") + classId + ".dds"; - if (!MWBase::Environment::get().getWindowManager()->textureExists(classImage)) + std::string_view fallback = "textures\\levelup\\warrior.dds"; + std::string classImage; + if (const auto* id = classId.getIf()) { - Log(Debug::Warning) << "No class image for " << classId << ", falling back to default"; - classImage = "textures\\levelup\\warrior.dds"; + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + classImage + = Misc::ResourceHelpers::correctTexturePath("textures\\levelup\\" + id->getValue() + ".dds", vfs); + if (!vfs->exists(classImage)) + { + Log(Debug::Warning) << "No class image for " << classId << ", falling back to default"; + classImage = fallback; + } } + else + classImage = fallback; imageBox->setImageTexture(classImage); } diff --git a/apps/openmw/mwgui/class.hpp b/apps/openmw/mwgui/class.hpp index bb34a055305..f89a0c7d881 100644 --- a/apps/openmw/mwgui/class.hpp +++ b/apps/openmw/mwgui/class.hpp @@ -1,14 +1,21 @@ #ifndef MWGUI_CLASS_H #define MWGUI_CLASS_H +#include +#include + +#include + #include -#include +#include +#include + #include "widgets.hpp" #include "windowbase.hpp" namespace MWGui { - void setClassImage(MyGUI::ImageBox* imageBox, const std::string& classId); + void setClassImage(MyGUI::ImageBox* imageBox, const ESM::RefId& classId); class InfoBoxDialog : public WindowModal { @@ -17,16 +24,16 @@ namespace MWGui typedef std::vector ButtonList; - void setText(const std::string &str); + void setText(const std::string& str); std::string getText() const; - void setButtons(ButtonList &buttons); + void setButtons(ButtonList& buttons); void onOpen() override; bool exit() override { return false; } // Events - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Int; + typedef MyGUI::delegates::MultiDelegate EventHandle_Int; /** Event : Button was clicked.\n signature : void method(int index)\n @@ -37,7 +44,6 @@ namespace MWGui void onButtonClicked(MyGUI::Widget* _sender); private: - void fitToText(MyGUI::TextBox* widget); void layoutVertically(MyGUI::Widget* widget, int margin); MyGUI::Widget* mTextBox; @@ -66,13 +72,12 @@ namespace MWGui public: GenerateClassResultDialog(); - std::string getClassId() const; - void setClassId(const std::string &classId); + void setClassId(const ESM::RefId& classId); bool exit() override { return false; } // Events - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n @@ -90,9 +95,9 @@ namespace MWGui private: MyGUI::ImageBox* mClassImage; - MyGUI::TextBox* mClassName; + MyGUI::TextBox* mClassName; - std::string mCurrentClassId; + ESM::RefId mCurrentClassId; }; class PickClassDialog : public WindowModal @@ -100,8 +105,8 @@ namespace MWGui public: PickClassDialog(); - const std::string &getClassId() const { return mCurrentClassId; } - void setClassId(const std::string &classId); + const ESM::RefId& getClassId() const { return mCurrentClassId; } + void setClassId(const ESM::RefId& classId); void setNextButtonShow(bool shown); void onOpen() override; @@ -109,7 +114,7 @@ namespace MWGui bool exit() override { return false; } // Events - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n @@ -133,13 +138,13 @@ namespace MWGui void updateStats(); MyGUI::ImageBox* mClassImage; - MyGUI::ListBox* mClassList; - MyGUI::TextBox* mSpecializationName; + MyGUI::ListBox* mClassList; + MyGUI::TextBox* mSpecializationName; Widgets::MWAttributePtr mFavoriteAttribute[2]; - Widgets::MWSkillPtr mMajorSkill[5]; - Widgets::MWSkillPtr mMinorSkill[5]; + Widgets::MWSkillPtr mMajorSkill[5]; + Widgets::MWSkillPtr mMinorSkill[5]; - std::string mCurrentClassId; + ESM::RefId mCurrentClassId; }; class SelectSpecializationDialog : public WindowModal @@ -153,7 +158,7 @@ namespace MWGui ESM::Class::Specialization getSpecializationId() const { return mSpecializationId; } // Events - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Cancel button clicked.\n signature : void method()\n @@ -179,14 +184,14 @@ namespace MWGui { public: SelectAttributeDialog(); - ~SelectAttributeDialog(); + ~SelectAttributeDialog() override = default; bool exit() override; - ESM::Attribute::AttributeID getAttributeId() const { return mAttributeId; } + ESM::RefId getAttributeId() const { return mAttributeId; } // Events - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Cancel button clicked.\n signature : void method()\n @@ -203,7 +208,7 @@ namespace MWGui void onCancelClicked(MyGUI::Widget* _sender); private: - ESM::Attribute::AttributeID mAttributeId; + ESM::RefId mAttributeId; }; class SelectSkillDialog : public WindowModal @@ -214,10 +219,10 @@ namespace MWGui bool exit() override; - ESM::Skill::SkillEnum getSkillId() const { return mSkillId; } + ESM::RefId getSkillId() const { return mSkillId; } // Events - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Cancel button clicked.\n signature : void method()\n @@ -234,11 +239,7 @@ namespace MWGui void onCancelClicked(MyGUI::Widget* _sender); private: - Widgets::MWSkillPtr mCombatSkill[9]; - Widgets::MWSkillPtr mMagicSkill[9]; - Widgets::MWSkillPtr mStealthSkill[9]; - - ESM::Skill::SkillEnum mSkillId; + ESM::RefId mSkillId; }; class DescriptionDialog : public WindowModal @@ -248,7 +249,7 @@ namespace MWGui ~DescriptionDialog(); std::string getTextInput() const { return mTextEdit->getCaption(); } - void setTextInput(const std::string &text) { mTextEdit->setCaption(text); } + void setTextInput(const std::string& text) { mTextEdit->setCaption(text); } /** Event : Dialog finished, OK button clicked.\n signature : void method()\n @@ -273,14 +274,14 @@ namespace MWGui std::string getName() const; std::string getDescription() const; ESM::Class::Specialization getSpecializationId() const; - std::vector getFavoriteAttributes() const; - std::vector getMajorSkills() const; - std::vector getMinorSkills() const; + std::vector getFavoriteAttributes() const; + std::vector getMajorSkills() const; + std::vector getMinorSkills() const; void setNextButtonShow(bool shown); // Events - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n @@ -311,23 +312,23 @@ namespace MWGui void update(); private: - MyGUI::EditBox* mEditName; - MyGUI::TextBox* mSpecializationName; - Widgets::MWAttributePtr mFavoriteAttribute0, mFavoriteAttribute1; - Widgets::MWSkillPtr mMajorSkill[5]; - Widgets::MWSkillPtr mMinorSkill[5]; + MyGUI::EditBox* mEditName; + MyGUI::TextBox* mSpecializationName; + Widgets::MWAttributePtr mFavoriteAttribute0, mFavoriteAttribute1; + std::array mMajorSkill; + std::array mMinorSkill; std::vector mSkills; - std::string mDescription; + std::string mDescription; - SelectSpecializationDialog *mSpecDialog; - SelectAttributeDialog *mAttribDialog; - SelectSkillDialog *mSkillDialog; - DescriptionDialog *mDescDialog; + std::unique_ptr mSpecDialog; + std::unique_ptr mAttribDialog; + std::unique_ptr mSkillDialog; + std::unique_ptr mDescDialog; - ESM::Class::Specialization mSpecializationId; + ESM::Class::Specialization mSpecializationId; - Widgets::MWAttributePtr mAffectedAttribute; - Widgets::MWSkillPtr mAffectedSkill; + Widgets::MWAttributePtr mAffectedAttribute; + Widgets::MWSkillPtr mAffectedSkill; }; } #endif diff --git a/apps/openmw/mwgui/companionitemmodel.cpp b/apps/openmw/mwgui/companionitemmodel.cpp index 3a272aa868b..6bae8aef52d 100644 --- a/apps/openmw/mwgui/companionitemmodel.cpp +++ b/apps/openmw/mwgui/companionitemmodel.cpp @@ -7,7 +7,7 @@ namespace void modifyProfit(const MWWorld::Ptr& actor, int diff) { - std::string script = actor.getClass().getScript(actor); + const ESM::RefId& script = actor.getClass().getScript(actor); if (!script.empty()) { int profit = actor.getRefData().getLocals().getIntVar(script, "minimumprofit"); @@ -20,12 +20,20 @@ namespace namespace MWGui { - CompanionItemModel::CompanionItemModel(const MWWorld::Ptr &actor) + CompanionItemModel::CompanionItemModel(const MWWorld::Ptr& actor) : InventoryItemModel(actor) { } - MWWorld::Ptr CompanionItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) + MWWorld::Ptr CompanionItemModel::addItem(const ItemStack& item, size_t count, bool allowAutoEquip) + { + if (hasProfit(mActor)) + modifyProfit(mActor, item.mBase.getClass().getValue(item.mBase) * count); + + return InventoryItemModel::addItem(item, count, allowAutoEquip); + } + + MWWorld::Ptr CompanionItemModel::copyItem(const ItemStack& item, size_t count, bool allowAutoEquip) { if (hasProfit(mActor)) modifyProfit(mActor, item.mBase.getClass().getValue(item.mBase) * count); @@ -33,7 +41,7 @@ namespace MWGui return InventoryItemModel::copyItem(item, count, allowAutoEquip); } - void CompanionItemModel::removeItem (const ItemStack& item, size_t count) + void CompanionItemModel::removeItem(const ItemStack& item, size_t count) { if (hasProfit(mActor)) modifyProfit(mActor, -item.mBase.getClass().getValue(item.mBase) * count); @@ -41,9 +49,9 @@ namespace MWGui InventoryItemModel::removeItem(item, count); } - bool CompanionItemModel::hasProfit(const MWWorld::Ptr &actor) + bool CompanionItemModel::hasProfit(const MWWorld::Ptr& actor) { - std::string script = actor.getClass().getScript(actor); + const ESM::RefId& script = actor.getClass().getScript(actor); if (script.empty()) return false; return actor.getRefData().getLocals().hasVar(script, "minimumprofit"); diff --git a/apps/openmw/mwgui/companionitemmodel.hpp b/apps/openmw/mwgui/companionitemmodel.hpp index 1872dd1569c..b9ee5006935 100644 --- a/apps/openmw/mwgui/companionitemmodel.hpp +++ b/apps/openmw/mwgui/companionitemmodel.hpp @@ -11,10 +11,11 @@ namespace MWGui class CompanionItemModel : public InventoryItemModel { public: - CompanionItemModel (const MWWorld::Ptr& actor); + CompanionItemModel(const MWWorld::Ptr& actor); - MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; - void removeItem (const ItemStack& item, size_t count) override; + MWWorld::Ptr addItem(const ItemStack& item, size_t count, bool allowAutoEquip = true) override; + MWWorld::Ptr copyItem(const ItemStack& item, size_t count, bool allowAutoEquip = true) override; + void removeItem(const ItemStack& item, size_t count) override; bool hasProfit(const MWWorld::Ptr& actor); }; diff --git a/apps/openmw/mwgui/companionwindow.cpp b/apps/openmw/mwgui/companionwindow.cpp index 926456219ea..240198eddc0 100644 --- a/apps/openmw/mwgui/companionwindow.cpp +++ b/apps/openmw/mwgui/companionwindow.cpp @@ -2,6 +2,8 @@ #include +#include +#include #include #include "../mwbase/environment.hpp" @@ -9,21 +11,21 @@ #include "../mwworld/class.hpp" -#include "messagebox.hpp" -#include "itemview.hpp" -#include "sortfilteritemmodel.hpp" #include "companionitemmodel.hpp" -#include "draganddrop.hpp" #include "countdialog.hpp" -#include "widgets.hpp" +#include "draganddrop.hpp" +#include "itemview.hpp" +#include "messagebox.hpp" +#include "sortfilteritemmodel.hpp" #include "tooltips.hpp" +#include "widgets.hpp" namespace { int getProfit(const MWWorld::Ptr& actor) { - std::string script = actor.getClass().getScript(actor); + const ESM::RefId& script = actor.getClass().getScript(actor); if (!script.empty()) { return actor.getRefData().getLocals().getIntVar(script, "minimumprofit"); @@ -36,164 +38,168 @@ namespace namespace MWGui { -CompanionWindow::CompanionWindow(DragAndDrop *dragAndDrop, MessageBoxManager* manager) - : WindowBase("openmw_companion_window.layout") - , mSortModel(nullptr) - , mModel(nullptr) - , mSelectedItem(-1) - , mDragAndDrop(dragAndDrop) - , mMessageBoxManager(manager) -{ - getWidget(mCloseButton, "CloseButton"); - getWidget(mProfitLabel, "ProfitLabel"); - getWidget(mEncumbranceBar, "EncumbranceBar"); - getWidget(mFilterEdit, "FilterEdit"); - getWidget(mItemView, "ItemView"); - mItemView->eventBackgroundClicked += MyGUI::newDelegate(this, &CompanionWindow::onBackgroundSelected); - mItemView->eventItemClicked += MyGUI::newDelegate(this, &CompanionWindow::onItemSelected); - mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &CompanionWindow::onNameFilterChanged); - - mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CompanionWindow::onCloseButtonClicked); - - setCoord(200,0,600,300); -} - -void CompanionWindow::onItemSelected(int index) -{ - if (mDragAndDrop->mIsOnDragAndDrop) + CompanionWindow::CompanionWindow(DragAndDrop* dragAndDrop, MessageBoxManager* manager) + : WindowBase("openmw_companion_window.layout") + , mSortModel(nullptr) + , mModel(nullptr) + , mSelectedItem(-1) + , mDragAndDrop(dragAndDrop) + , mMessageBoxManager(manager) { - mDragAndDrop->drop(mModel, mItemView); - updateEncumbranceBar(); - return; + getWidget(mCloseButton, "CloseButton"); + getWidget(mProfitLabel, "ProfitLabel"); + getWidget(mEncumbranceBar, "EncumbranceBar"); + getWidget(mFilterEdit, "FilterEdit"); + getWidget(mItemView, "ItemView"); + mItemView->eventBackgroundClicked += MyGUI::newDelegate(this, &CompanionWindow::onBackgroundSelected); + mItemView->eventItemClicked += MyGUI::newDelegate(this, &CompanionWindow::onItemSelected); + mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &CompanionWindow::onNameFilterChanged); + + mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CompanionWindow::onCloseButtonClicked); + + setCoord(200, 0, 600, 300); } - const ItemStack& item = mSortModel->getItem(index); - - // We can't take conjured items from a companion NPC - if (item.mFlags & ItemStack::Flag_Bound) + void CompanionWindow::onItemSelected(int index) { - MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); - return; - } + if (mDragAndDrop->mIsOnDragAndDrop) + { + mDragAndDrop->drop(mModel, mItemView); + updateEncumbranceBar(); + return; + } - MWWorld::Ptr object = item.mBase; - int count = item.mCount; - bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); - if (MyGUI::InputManager::getInstance().isControlPressed()) - count = 1; + const ItemStack& item = mSortModel->getItem(index); - mSelectedItem = mSortModel->mapToSource(index); + // We can't take conjured items from a companion actor + if (item.mFlags & ItemStack::Flag_Bound) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); + return; + } - if (count > 1 && !shift) - { - CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); - std::string name = object.getClass().getName(object) + MWGui::ToolTips::getSoulString(object.getCellRef()); - dialog->openCountDialog(name, "#{sTake}", count); - dialog->eventOkClicked.clear(); - dialog->eventOkClicked += MyGUI::newDelegate(this, &CompanionWindow::dragItem); + MWWorld::Ptr object = item.mBase; + int count = item.mCount; + bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); + if (MyGUI::InputManager::getInstance().isControlPressed()) + count = 1; + + mSelectedItem = mSortModel->mapToSource(index); + + if (count > 1 && !shift) + { + CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); + std::string name{ object.getClass().getName(object) }; + name += MWGui::ToolTips::getSoulString(object.getCellRef()); + dialog->openCountDialog(name, "#{sTake}", count); + dialog->eventOkClicked.clear(); + dialog->eventOkClicked += MyGUI::newDelegate(this, &CompanionWindow::dragItem); + } + else + dragItem(nullptr, count); } - else - dragItem (nullptr, count); -} -void CompanionWindow::onNameFilterChanged(MyGUI::EditBox* _sender) + void CompanionWindow::onNameFilterChanged(MyGUI::EditBox* _sender) { mSortModel->setNameFilter(_sender->getCaption()); mItemView->update(); } -void CompanionWindow::dragItem(MyGUI::Widget* sender, int count) -{ - mDragAndDrop->startDrag(mSelectedItem, mSortModel, mModel, mItemView, count); -} - -void CompanionWindow::onBackgroundSelected() -{ - if (mDragAndDrop->mIsOnDragAndDrop) + void CompanionWindow::dragItem(MyGUI::Widget* sender, int count) { - mDragAndDrop->drop(mModel, mItemView); - updateEncumbranceBar(); + mDragAndDrop->startDrag(mSelectedItem, mSortModel, mModel, mItemView, count); } -} -void CompanionWindow::setPtr(const MWWorld::Ptr& npc) -{ - mPtr = npc; - updateEncumbranceBar(); + void CompanionWindow::onBackgroundSelected() + { + if (mDragAndDrop->mIsOnDragAndDrop) + { + mDragAndDrop->drop(mModel, mItemView); + updateEncumbranceBar(); + } + } - mModel = new CompanionItemModel(npc); - mSortModel = new SortFilterItemModel(mModel); - mFilterEdit->setCaption(std::string()); - mItemView->setModel(mSortModel); - mItemView->resetScrollBars(); + void CompanionWindow::setPtr(const MWWorld::Ptr& actor) + { + if (actor.isEmpty() || !actor.getClass().isActor()) + throw std::runtime_error("Invalid argument in CompanionWindow::setPtr"); + mPtr = actor; + updateEncumbranceBar(); + auto model = std::make_unique(actor); + mModel = model.get(); + auto sortModel = std::make_unique(std::move(model)); + mSortModel = sortModel.get(); + mFilterEdit->setCaption({}); + mItemView->setModel(std::move(sortModel)); + mItemView->resetScrollBars(); + + setTitle(actor.getClass().getName(actor)); + } - setTitle(npc.getClass().getName(npc)); -} + void CompanionWindow::onFrame(float dt) + { + checkReferenceAvailable(); + updateEncumbranceBar(); + } -void CompanionWindow::onFrame(float dt) -{ - checkReferenceAvailable(); - updateEncumbranceBar(); -} + void CompanionWindow::updateEncumbranceBar() + { + if (mPtr.isEmpty()) + return; + float capacity = mPtr.getClass().getCapacity(mPtr); + float encumbrance = mPtr.getClass().getEncumbrance(mPtr); + mEncumbranceBar->setValue(std::ceil(encumbrance), static_cast(capacity)); -void CompanionWindow::updateEncumbranceBar() -{ - if (mPtr.isEmpty()) - return; - float capacity = mPtr.getClass().getCapacity(mPtr); - float encumbrance = mPtr.getClass().getEncumbrance(mPtr); - mEncumbranceBar->setValue(std::ceil(encumbrance), static_cast(capacity)); + if (mModel && mModel->hasProfit(mPtr)) + { + mProfitLabel->setCaptionWithReplacing("#{sProfitValue} " + MyGUI::utility::toString(getProfit(mPtr))); + } + else + mProfitLabel->setCaption({}); + } - if (mModel && mModel->hasProfit(mPtr)) + void CompanionWindow::onCloseButtonClicked(MyGUI::Widget* _sender) { - mProfitLabel->setCaptionWithReplacing("#{sProfitValue} " + MyGUI::utility::toString(getProfit(mPtr))); + if (exit()) + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); } - else - mProfitLabel->setCaption(""); -} -void CompanionWindow::onCloseButtonClicked(MyGUI::Widget* _sender) -{ - if (exit()) - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); -} + bool CompanionWindow::exit() + { + if (mModel && mModel->hasProfit(mPtr) && getProfit(mPtr) < 0) + { + std::vector buttons; + buttons.emplace_back("#{sCompanionWarningButtonOne}"); + buttons.emplace_back("#{sCompanionWarningButtonTwo}"); + mMessageBoxManager->createInteractiveMessageBox("#{sCompanionWarningMessage}", buttons); + mMessageBoxManager->eventButtonPressed + += MyGUI::newDelegate(this, &CompanionWindow::onMessageBoxButtonClicked); + return false; + } + return true; + } -bool CompanionWindow::exit() -{ - if (mModel && mModel->hasProfit(mPtr) && getProfit(mPtr) < 0) + void CompanionWindow::onMessageBoxButtonClicked(int button) { - std::vector buttons; - buttons.emplace_back("#{sCompanionWarningButtonOne}"); - buttons.emplace_back("#{sCompanionWarningButtonTwo}"); - mMessageBoxManager->createInteractiveMessageBox("#{sCompanionWarningMessage}", buttons); - mMessageBoxManager->eventButtonPressed += MyGUI::newDelegate(this, &CompanionWindow::onMessageBoxButtonClicked); - return false; + if (button == 0) + { + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); + // Important for Calvus' contract script to work properly + MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); + } } - return true; -} -void CompanionWindow::onMessageBoxButtonClicked(int button) -{ - if (button == 0) + void CompanionWindow::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); - // Important for Calvus' contract script to work properly - MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } -} - -void CompanionWindow::onReferenceUnavailable() -{ - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); -} - -void CompanionWindow::resetReference() -{ - ReferenceInterface::resetReference(); - mItemView->setModel(nullptr); - mModel = nullptr; - mSortModel = nullptr; -} + void CompanionWindow::resetReference() + { + ReferenceInterface::resetReference(); + mItemView->setModel(nullptr); + mModel = nullptr; + mSortModel = nullptr; + } } diff --git a/apps/openmw/mwgui/companionwindow.hpp b/apps/openmw/mwgui/companionwindow.hpp index 7a7c92e3dcc..97f3a0072e7 100644 --- a/apps/openmw/mwgui/companionwindow.hpp +++ b/apps/openmw/mwgui/companionwindow.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_MWGUI_COMPANIONWINDOW_H #define OPENMW_MWGUI_COMPANIONWINDOW_H -#include "windowbase.hpp" #include "referenceinterface.hpp" +#include "windowbase.hpp" namespace MWGui { @@ -26,10 +26,12 @@ namespace MWGui void resetReference() override; - void setPtr(const MWWorld::Ptr& npc) override; - void onFrame (float dt) override; + void setPtr(const MWWorld::Ptr& actor) override; + void onFrame(float dt) override; void clear() override { resetReference(); } + std::string_view getWindowIdForLua() const override { return "Companion"; } + private: ItemView* mItemView; SortFilterItemModel* mSortModel; diff --git a/apps/openmw/mwgui/confirmationdialog.cpp b/apps/openmw/mwgui/confirmationdialog.cpp index 9ebaf3919f8..48b209f17e1 100644 --- a/apps/openmw/mwgui/confirmationdialog.cpp +++ b/apps/openmw/mwgui/confirmationdialog.cpp @@ -8,8 +8,8 @@ namespace MWGui { - ConfirmationDialog::ConfirmationDialog() : - WindowModal("openmw_confirmation_dialog.layout") + ConfirmationDialog::ConfirmationDialog() + : WindowModal("openmw_confirmation_dialog.layout") { getWidget(mMessage, "Message"); getWidget(mOkButton, "OkButton"); diff --git a/apps/openmw/mwgui/confirmationdialog.hpp b/apps/openmw/mwgui/confirmationdialog.hpp index 2acefd54c1e..1344f2a5012 100644 --- a/apps/openmw/mwgui/confirmationdialog.hpp +++ b/apps/openmw/mwgui/confirmationdialog.hpp @@ -7,26 +7,26 @@ namespace MWGui { class ConfirmationDialog : public WindowModal { - public: - ConfirmationDialog(); - void askForConfirmation(const std::string& message); - bool exit() override; - - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; - - /** Event : Ok button was clicked.\n - signature : void method()\n - */ - EventHandle_Void eventOkClicked; - EventHandle_Void eventCancelClicked; - - private: - MyGUI::EditBox* mMessage; - MyGUI::Button* mOkButton; - MyGUI::Button* mCancelButton; - - void onCancelButtonClicked(MyGUI::Widget* _sender); - void onOkButtonClicked(MyGUI::Widget* _sender); + public: + ConfirmationDialog(); + void askForConfirmation(const std::string& message); + bool exit() override; + + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; + + /** Event : Ok button was clicked.\n + signature : void method()\n + */ + EventHandle_Void eventOkClicked; + EventHandle_Void eventCancelClicked; + + private: + MyGUI::EditBox* mMessage; + MyGUI::Button* mOkButton; + MyGUI::Button* mCancelButton; + + void onCancelButtonClicked(MyGUI::Widget* _sender); + void onOkButtonClicked(MyGUI::Widget* _sender); }; } diff --git a/apps/openmw/mwgui/console.cpp b/apps/openmw/mwgui/console.cpp index b9004fcdbb7..a188f3c86b0 100644 --- a/apps/openmw/mwgui/console.cpp +++ b/apps/openmw/mwgui/console.cpp @@ -1,68 +1,74 @@ #include "console.hpp" +#include #include #include #include -#include -#include +#include +#include +#include #include #include #include -#include #include +#include +#include #include +#include +#include + +#include "apps/openmw/mwgui/textcolours.hpp" #include "../mwscript/extensions.hpp" +#include "../mwscript/interpretercontext.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" -#include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" namespace MWGui { class ConsoleInterpreterContext : public MWScript::InterpreterContext { - Console& mConsole; - - public: + Console& mConsole; - ConsoleInterpreterContext (Console& console, MWWorld::Ptr reference); + public: + ConsoleInterpreterContext(Console& console, MWWorld::Ptr reference); - void report (const std::string& message) override; + void report(const std::string& message) override; }; - ConsoleInterpreterContext::ConsoleInterpreterContext (Console& console, - MWWorld::Ptr reference) - : MWScript::InterpreterContext ( - reference.isEmpty() ? nullptr : &reference.getRefData().getLocals(), reference), - mConsole (console) - {} + ConsoleInterpreterContext::ConsoleInterpreterContext(Console& console, MWWorld::Ptr reference) + : MWScript::InterpreterContext(reference.isEmpty() ? nullptr : &reference.getRefData().getLocals(), reference) + , mConsole(console) + { + } - void ConsoleInterpreterContext::report (const std::string& message) + void ConsoleInterpreterContext::report(const std::string& message) { - mConsole.printOK (message); + mConsole.printOK(message); } - bool Console::compile (const std::string& cmd, Compiler::Output& output) + bool Console::compile(const std::string& cmd, Compiler::Output& output) { try { ErrorHandler::reset(); - std::istringstream input (cmd + '\n'); + std::istringstream input(cmd + '\n'); - Compiler::Scanner scanner (*this, input, mCompilerContext.getExtensions()); + Compiler::Scanner scanner(*this, input, mCompilerContext.getExtensions()); - Compiler::LineParser parser (*this, mCompilerContext, output.getLocals(), - output.getLiterals(), output.getCode(), true); + Compiler::LineParser parser( + *this, mCompilerContext, output.getLocals(), output.getLiterals(), output.getCode(), true); - scanner.scan (parser); + scanner.scan(parser); return isGood(); } @@ -72,24 +78,24 @@ namespace MWGui } catch (const std::exception& error) { - printError (std::string ("Error: ") + error.what()); + printError(std::string("Error: ") + error.what()); } return false; } - void Console::report (const std::string& message, const Compiler::TokenLoc& loc, Type type) + void Console::report(const std::string& message, const Compiler::TokenLoc& loc, Type type) { std::ostringstream error; error << "column " << loc.mColumn << " (" << loc.mLiteral << "):"; - printError (error.str()); - printError ((type==ErrorMessage ? "error: " : "warning: ") + message); + printError(error.str()); + printError((type == ErrorMessage ? "error: " : "warning: ") + message); } - void Console::report (const std::string& message, Type type) + void Console::report(const std::string& message, Type type) { - printError ((type==ErrorMessage ? "error: " : "warning: ") + message); + printError((type == ErrorMessage ? "error: " : "warning: ") + message); } void Console::listNames() @@ -97,60 +103,92 @@ namespace MWGui if (mNames.empty()) { // keywords - std::istringstream input (""); + std::istringstream input; - Compiler::Scanner scanner (*this, input, mCompilerContext.getExtensions()); + Compiler::Scanner scanner(*this, input, mCompilerContext.getExtensions()); - scanner.listKeywords (mNames); + scanner.listKeywords(mNames); // identifier - const MWWorld::ESMStore& store = - MWBase::Environment::get().getWorld()->getStore(); - - for (MWWorld::ESMStore::iterator it = store.begin(); it != store.end(); ++it) + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); + std::vector ids; + for (const auto* store : esmStore) { - it->second->listIdentifier (mNames); + store->listIdentifier(ids); + for (auto id : ids) + { + if (id.is()) + mNames.push_back(id.getRefIdString()); + } + ids.clear(); } - // exterior cell names aren't technically identifiers, but since the COC function accepts them, - // we should list them too - for (MWWorld::Store::iterator it = store.get().extBegin(); - it != store.get().extEnd(); ++it) + // exterior cell names and editor IDs aren't technically identifiers, + // but since the COC function accepts them, we should list them too + for (auto it = esmStore.get().extBegin(); it != esmStore.get().extEnd(); ++it) { if (!it->mName.empty()) mNames.push_back(it->mName); } + for (const auto& cell : esmStore.get()) + { + if (!cell.mEditorId.empty()) + mNames.push_back(cell.mEditorId); + } + // sort - std::sort (mNames.begin(), mNames.end()); + std::sort(mNames.begin(), mNames.end()); // remove duplicates - mNames.erase( std::unique( mNames.begin(), mNames.end() ), mNames.end() ); + mNames.erase(std::unique(mNames.begin(), mNames.end()), mNames.end()); } } - Console::Console(int w, int h, bool consoleOnlyScripts) - : WindowBase("openmw_console.layout"), - mCompilerContext (MWScript::CompilerContext::Type_Console), - mConsoleOnlyScripts (consoleOnlyScripts) + Console::Console(int w, int h, bool consoleOnlyScripts, Files::ConfigurationManager& cfgMgr) + : WindowBase("openmw_console.layout") + , mCaseSensitiveSearch(false) + , mRegExSearch(false) + , mCompilerContext(MWScript::CompilerContext::Type_Console) + , mConsoleOnlyScripts(consoleOnlyScripts) + , mCfgMgr(cfgMgr) { - setCoord(10,10, w-10, h/2); + setCoord(10, 10, w - 10, h / 2); getWidget(mCommandLine, "edit_Command"); getWidget(mHistory, "list_History"); + getWidget(mSearchTerm, "edit_SearchTerm"); + getWidget(mNextButton, "button_Next"); + getWidget(mPreviousButton, "button_Previous"); + getWidget(mCaseSensitiveToggleButton, "button_CaseSensitive"); + getWidget(mRegExSearchToggleButton, "button_RegExSearch"); // Set up the command line box - mCommandLine->eventEditSelectAccept += - newDelegate(this, &Console::acceptCommand); - mCommandLine->eventKeyButtonPressed += - newDelegate(this, &Console::keyPress); + mCommandLine->eventEditSelectAccept += newDelegate(this, &Console::acceptCommand); + mCommandLine->eventKeyButtonPressed += newDelegate(this, &Console::commandBoxKeyPress); + + // Set up the search term box + mSearchTerm->eventEditSelectAccept += newDelegate(this, &Console::acceptSearchTerm); + mNextButton->eventMouseButtonClick += newDelegate(this, &Console::findNextOccurrence); + mPreviousButton->eventMouseButtonClick += newDelegate(this, &Console::findPreviousOccurrence); + mCaseSensitiveToggleButton->eventMouseButtonClick += newDelegate(this, &Console::toggleCaseSensitiveSearch); + mRegExSearchToggleButton->eventMouseButtonClick += newDelegate(this, &Console::toggleRegExSearch); // Set up the log window mHistory->setOverflowToTheLeft(true); // compiler - Compiler::registerExtensions (mExtensions, mConsoleOnlyScripts); - mCompilerContext.setExtensions (&mExtensions); + Compiler::registerExtensions(mExtensions, mConsoleOnlyScripts); + mCompilerContext.setExtensions(&mExtensions); + + // command history file + initConsoleHistory(); + } + + Console::~Console() + { + if (mCommandHistoryFile && mCommandHistoryFile.is_open()) + mCommandHistoryFile.close(); } void Console::onOpen() @@ -161,67 +199,75 @@ namespace MWGui MyGUI::LayerManager::getInstance().upLayerItem(mMainWidget); } - void Console::print(const std::string &msg, const std::string& color) + void Console::print(const std::string& msg, std::string_view color) { - mHistory->addText(color + MyGUI::TextIterator::toTagsString(msg)); + mHistory->addText(std::string(color) + MyGUI::TextIterator::toTagsString(msg)); } - void Console::printOK(const std::string &msg) + void Console::printOK(const std::string& msg) { - print(msg + "\n", "#FF00FF"); + print(msg + "\n", MWBase::WindowManager::sConsoleColor_Success); } - void Console::printError(const std::string &msg) + void Console::printError(const std::string& msg) { - print(msg + "\n", "#FF2222"); + print(msg + "\n", MWBase::WindowManager::sConsoleColor_Error); } - void Console::execute (const std::string& command) + void Console::execute(const std::string& command) { // Log the command - print("> " + command + "\n"); + if (mConsoleMode.empty()) + print("> " + command + "\n"); + else + print(mConsoleMode + " " + command + "\n"); + + if (!mConsoleMode.empty() || (command.size() >= 3 && std::string_view(command).substr(0, 3) == "lua")) + { + MWBase::Environment::get().getLuaManager()->handleConsoleCommand(mConsoleMode, command, mPtr); + return; + } Compiler::Locals locals; if (!mPtr.isEmpty()) { - std::string script = mPtr.getClass().getScript(mPtr); + const ESM::RefId& script = mPtr.getClass().getScript(mPtr); if (!script.empty()) locals = MWBase::Environment::get().getScriptManager()->getLocals(script); } - Compiler::Output output (locals); + Compiler::Output output(locals); - if (compile (command + "\n", output)) + if (compile(command + "\n", output)) { try { - ConsoleInterpreterContext interpreterContext (*this, mPtr); + ConsoleInterpreterContext interpreterContext(*this, mPtr); Interpreter::Interpreter interpreter; - MWScript::installOpcodes (interpreter, mConsoleOnlyScripts); - std::vector code; - output.getCode (code); - interpreter.run (&code[0], code.size(), interpreterContext); + MWScript::installOpcodes(interpreter, mConsoleOnlyScripts); + const Interpreter::Program program = output.getProgram(); + interpreter.run(program, interpreterContext); } catch (const std::exception& error) { - printError (std::string ("Error: ") + error.what()); + printError(std::string("Error: ") + error.what()); } } } - void Console::executeFile (const std::string& path) + void Console::executeFile(const std::filesystem::path& path) { - namespace bfs = boost::filesystem; - bfs::ifstream stream ((bfs::path(path))); + std::ifstream stream(path); if (!stream.is_open()) - printError ("failed to open file: " + path); - else { - std::string line; - - while (std::getline (stream, line)) - execute (line); + printError("Failed to open script file \"" + Files::pathToUnicodeString(path) + + "\": " + std::generic_category().message(errno)); + return; } + + std::string line; + while (std::getline(stream, line)) + execute(line); } void Console::clear() @@ -234,58 +280,55 @@ namespace MWGui return c == ' ' || c == '\t'; } - void Console::keyPress(MyGUI::Widget* _sender, - MyGUI::KeyCode key, - MyGUI::Char _char) + void Console::commandBoxKeyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char) { - if(MyGUI::InputManager::getInstance().isControlPressed()) + if (MyGUI::InputManager::getInstance().isControlPressed()) { - if(key == MyGUI::KeyCode::W) + if (key == MyGUI::KeyCode::W) { - const auto& caption = mCommandLine->getCaption(); - if(caption.empty()) + auto caption = mCommandLine->getOnlyText(); + if (caption.empty()) return; size_t max = mCommandLine->getTextCursor(); - while(max > 0 && (isWhitespace(caption[max - 1]) || caption[max - 1] == '>')) + while (max > 0 && (isWhitespace(caption[max - 1]) || caption[max - 1] == '>')) max--; - while(max > 0 && !isWhitespace(caption[max - 1]) && caption[max - 1] != '>') + while (max > 0 && !isWhitespace(caption[max - 1]) && caption[max - 1] != '>') max--; size_t length = mCommandLine->getTextCursor() - max; - if(length > 0) + if (length > 0) { - auto text = caption; - text.erase(max, length); - mCommandLine->setCaption(text); + caption.erase(max, length); + mCommandLine->setOnlyText(caption); mCommandLine->setTextCursor(max); } } - else if(key == MyGUI::KeyCode::U) + else if (key == MyGUI::KeyCode::U) { - if(mCommandLine->getTextCursor() > 0) + if (mCommandLine->getTextCursor() > 0) { - auto text = mCommandLine->getCaption(); + auto text = mCommandLine->getOnlyText(); text.erase(0, mCommandLine->getTextCursor()); - mCommandLine->setCaption(text); + mCommandLine->setOnlyText(text); mCommandLine->setTextCursor(0); } } } - else if(key == MyGUI::KeyCode::Tab) + else if (key == MyGUI::KeyCode::Tab && mConsoleMode.empty()) { std::vector matches; listNames(); - std::string oldCaption = mCommandLine->getCaption(); - std::string newCaption = complete( mCommandLine->getOnlyText(), matches ); - mCommandLine->setCaption(newCaption); + std::string oldCaption = mCommandLine->getOnlyText(); + std::string newCaption = complete(mCommandLine->getOnlyText(), matches); + mCommandLine->setOnlyText(newCaption); // List candidates if repeatedly pressing tab if (oldCaption == newCaption && !matches.empty()) { int i = 0; - printOK(""); - for(std::string& match : matches) + printOK({}); + for (std::string& match : matches) { - if(i == 50) + if (i == 50) break; printOK(match); @@ -294,57 +337,295 @@ namespace MWGui } } - if(mCommandHistory.empty()) return; + if (mCommandHistory.empty()) + return; // Traverse history with up and down arrows - if(key == MyGUI::KeyCode::ArrowUp) + if (key == MyGUI::KeyCode::ArrowUp) { // If the user was editing a string, store it for later - if(mCurrent == mCommandHistory.end()) + if (mCurrent == mCommandHistory.end()) mEditString = mCommandLine->getOnlyText(); - if(mCurrent != mCommandHistory.begin()) + if (mCurrent != mCommandHistory.begin()) { --mCurrent; - mCommandLine->setCaption(*mCurrent); + mCommandLine->setOnlyText(*mCurrent); } } - else if(key == MyGUI::KeyCode::ArrowDown) + else if (key == MyGUI::KeyCode::ArrowDown) { - if(mCurrent != mCommandHistory.end()) + if (mCurrent != mCommandHistory.end()) { ++mCurrent; - if(mCurrent != mCommandHistory.end()) - mCommandLine->setCaption(*mCurrent); + if (mCurrent != mCommandHistory.end()) + mCommandLine->setOnlyText(*mCurrent); else // Restore the edit string - mCommandLine->setCaption(mEditString); + mCommandLine->setOnlyText(mEditString); } } } void Console::acceptCommand(MyGUI::EditBox* _sender) { - const std::string &cm = mCommandLine->getOnlyText(); - if(cm.empty()) return; + const std::string& cm = mCommandLine->getOnlyText(); + if (cm.empty()) + return; // Add the command to the history, and set the current pointer to // the end of the list if (mCommandHistory.empty() || mCommandHistory.back() != cm) + { mCommandHistory.push_back(cm); + + if (mCommandHistoryFile && mCommandHistoryFile.good()) + mCommandHistoryFile << cm << std::endl; + } mCurrent = mCommandHistory.end(); mEditString.clear(); + mHistory->setTextCursor(mHistory->getTextLength()); // Reset the command line before the command execution. - // It prevents the re-triggering of the acceptCommand() event for the same command + // It prevents the re-triggering of the acceptCommand() event for the same command // during the actual command execution - mCommandLine->setCaption(""); + mCommandLine->setCaption({}); + + execute(cm); + } + + void Console::toggleCaseSensitiveSearch(MyGUI::Widget* _sender) + { + mCaseSensitiveSearch = !mCaseSensitiveSearch; + + // Reset console search highlight position search parameters have changed + mCurrentOccurrenceIndex = std::string::npos; + + // Adjust color to reflect toggled status + const TextColours& textColours{ MWBase::Environment::get().getWindowManager()->getTextColours() }; + mCaseSensitiveToggleButton->setTextColour(mCaseSensitiveSearch ? textColours.link : textColours.normal); + } + + void Console::toggleRegExSearch(MyGUI::Widget* _sender) + { + mRegExSearch = !mRegExSearch; + + // Reset console search highlight position search parameters have changed + mCurrentOccurrenceIndex = std::string::npos; + + // Adjust color to reflect toggled status + const TextColours& textColours{ MWBase::Environment::get().getWindowManager()->getTextColours() }; + mRegExSearchToggleButton->setTextColour(mRegExSearch ? textColours.link : textColours.normal); + + // RegEx searches are always case sensitive + mCaseSensitiveSearch = mRegExSearch; + + // Dim case sensitive and set disabled if regex search toggled on, restore when toggled off + mCaseSensitiveToggleButton->setTextColour(mCaseSensitiveSearch ? textColours.linkPressed : textColours.normal); + mCaseSensitiveToggleButton->setEnabled(!mRegExSearch); + } + + void Console::acceptSearchTerm(MyGUI::EditBox* _sender) + { + const std::string& searchTerm = mSearchTerm->getOnlyText(); + + if (searchTerm.empty()) + { + return; + } + + std::string newSearchTerm = mCaseSensitiveSearch ? searchTerm : Utf8Stream::lowerCaseUtf8(searchTerm); + + // If new search term reset position, otherwise continue from current position + if (newSearchTerm != mCurrentSearchTerm) + { + mCurrentSearchTerm = std::move(newSearchTerm); + mCurrentOccurrenceIndex = std::string::npos; + } + + findNextOccurrence(nullptr); + } + + enum class Console::SearchDirection + { + Forward, + Reverse + }; + + void Console::findNextOccurrence(MyGUI::Widget* _sender) + { + findOccurrence(SearchDirection::Forward); + } + + void Console::findPreviousOccurrence(MyGUI::Widget* _sender) + { + findOccurrence(SearchDirection::Reverse); + } + + void Console::findOccurrence(const SearchDirection direction) + { + + if (mCurrentSearchTerm.empty()) + { + return; + } + + const auto historyText{ mCaseSensitiveSearch ? mHistory->getOnlyText().asUTF8() + : Utf8Stream::lowerCaseUtf8(mHistory->getOnlyText().asUTF8()) }; + + // Setup default search range + size_t firstIndex{ 0 }; + size_t lastIndex{ historyText.length() }; + + // If search is not the first adjust the range based on the direction and previous occurrence. + if (mCurrentOccurrenceIndex != std::string::npos) + { + if (direction == SearchDirection::Forward && mCurrentOccurrenceIndex > 1) + { + firstIndex = mCurrentOccurrenceIndex + mCurrentOccurrenceLength; + } + else if (direction == SearchDirection::Reverse + && (historyText.length() - mCurrentOccurrenceIndex) > mCurrentOccurrenceLength) + { + lastIndex = mCurrentOccurrenceIndex - 1; + } + } + + findInHistoryText(historyText, direction, firstIndex, lastIndex); + + // If the last search did not find anything AND... + if (mCurrentOccurrenceIndex == std::string::npos) + { + if (direction == SearchDirection::Forward && firstIndex != 0) + { + // ... We didn't start at the beginning, we apply the search to the other half of the text. + findInHistoryText(historyText, direction, 0, firstIndex); + } + else if (direction == SearchDirection::Reverse && lastIndex != historyText.length()) + { + // ... We didn't search to the end, we apply the search to the other half of the text. + findInHistoryText(historyText, direction, lastIndex, historyText.length()); + } + } + + // Only scroll & select if we actually found something + if (mCurrentOccurrenceIndex != std::string::npos) + { + markOccurrence(mCurrentOccurrenceIndex, mCurrentOccurrenceLength); + } + else + { + markOccurrence(0, 0); + } + } + + void Console::findInHistoryText(const std::string& historyText, const SearchDirection direction, + const size_t firstIndex, const size_t lastIndex) + { + if (mRegExSearch) + { + findWithRegex(historyText, direction, firstIndex, lastIndex); + } + else + { + findWithStringSearch(historyText, direction, firstIndex, lastIndex); + } + } + + void Console::findWithRegex(const std::string& historyText, const SearchDirection direction, + const size_t firstIndex, const size_t lastIndex) + { + // Search text for regex match in given interval + const std::regex pattern{ mCurrentSearchTerm }; + std::sregex_iterator match{ (historyText.cbegin() + firstIndex), (historyText.cbegin() + lastIndex), pattern }; + const std::sregex_iterator end{}; + + // If reverse search get last result in interval + if (direction == SearchDirection::Reverse) + { + std::sregex_iterator lastMatch{ end }; + while (match != end) + { + lastMatch = match; + ++match; + } + match = lastMatch; + } + + // If regex match is found in text, set new current occurrence values + if (match != end) + { + mCurrentOccurrenceIndex = match->position() + firstIndex; + mCurrentOccurrenceLength = match->length(); + } + else + { + mCurrentOccurrenceIndex = std::string::npos; + mCurrentOccurrenceLength = 0; + } + } + + void Console::findWithStringSearch(const std::string& historyText, const SearchDirection direction, + const size_t firstIndex, const size_t lastIndex) + { + // Search in given text interval for search term + const size_t substringLength{ (lastIndex - firstIndex) + 1 }; + const std::string_view historyTextView((historyText.c_str() + firstIndex), substringLength); + if (direction == SearchDirection::Forward) + { + mCurrentOccurrenceIndex = historyTextView.find(mCurrentSearchTerm); + } + else + { + mCurrentOccurrenceIndex = historyTextView.rfind(mCurrentSearchTerm); + } + + // If search term is found in text, set new current occurrence values + if (mCurrentOccurrenceIndex != std::string::npos) + { + mCurrentOccurrenceIndex += firstIndex; + mCurrentOccurrenceLength = mCurrentSearchTerm.length(); + } + else + { + mCurrentOccurrenceLength = 0; + } + } + void Console::markOccurrence(const size_t textPosition, const size_t length) + { + if (textPosition == 0 && length == 0) + { + mHistory->setTextSelection(0, 0); + mHistory->setVScrollPosition(mHistory->getVScrollRange()); + return; + } + + const auto historyText = mHistory->getOnlyText(); + const size_t upperLimit = std::min(historyText.length(), textPosition); - execute (cm); + // Since MyGUI::EditBox.setVScrollPosition() works on pixels instead of text positions + // we need to calculate the actual pixel position by counting lines. + size_t lineNumber = 0; + for (size_t i = 0; i < upperLimit; i++) + { + if (historyText[i] == '\n') + { + lineNumber++; + } + } + + // Make some space before the actual result + if (lineNumber >= 2) + { + lineNumber -= 2; + } + + mHistory->setTextSelection(textPosition, textPosition + length); + mHistory->setVScrollPosition(mHistory->getFontHeight() * lineNumber); } - std::string Console::complete( std::string input, std::vector &matches ) + std::string Console::complete(std::string input, std::vector& matches) { std::string output = input; std::string tmp = input; @@ -356,73 +637,87 @@ namespace MWGui size_t explicitPos = tmp.find("->"); if (explicitPos != std::string::npos) { - tmp.erase(0, explicitPos+2); + tmp.erase(0, explicitPos + 2); } /* Are there quotation marks? */ - if( tmp.find('"') != std::string::npos ) { - int numquotes=0; - for(std::string::iterator it=tmp.begin(); it < tmp.end(); ++it) { - if( *it == '"' ) + if (tmp.find('"') != std::string::npos) + { + int numquotes = 0; + for (std::string::iterator it = tmp.begin(); it < tmp.end(); ++it) + { + if (*it == '"') numquotes++; } /* Is it terminated?*/ - if( numquotes % 2 ) { - tmp.erase( 0, tmp.rfind('"')+1 ); + if (numquotes % 2) + { + tmp.erase(0, tmp.rfind('"') + 1); has_front_quote = true; } - else { + else + { size_t pos; - if( ( ((pos = tmp.rfind(' ')) != std::string::npos ) ) && ( pos > tmp.rfind('"') ) ) { - tmp.erase( 0, tmp.rfind(' ')+1); + if ((((pos = tmp.rfind(' ')) != std::string::npos)) && (pos > tmp.rfind('"'))) + { + tmp.erase(0, tmp.rfind(' ') + 1); } - else { + else + { tmp.clear(); } has_front_quote = false; } } /* No quotation marks. Are there spaces?*/ - else { + else + { size_t rpos; - if( (rpos=tmp.rfind(' ')) != std::string::npos ) { - if( rpos == 0 ) { + if ((rpos = tmp.rfind(' ')) != std::string::npos) + { + if (rpos == 0) + { tmp.clear(); } - else { - tmp.erase(0, rpos+1); + else + { + tmp.erase(0, rpos + 1); } } } /* Erase the input from the output string so we can easily append the completed form later. */ - output.erase(output.end()-tmp.length(), output.end()); + output.erase(output.end() - tmp.length(), output.end()); - /* Is there still something in the input string? If not just display all commands and return the unchanged input. */ - if( tmp.length() == 0 ) { - matches=mNames; + /* Is there still something in the input string? If not just display all commands and return the unchanged + * input. */ + if (tmp.length() == 0) + { + matches = mNames; return input; } /* Iterate through the vector. */ - for(std::string& name : mNames) + for (std::string& name : mNames) { - bool string_different=false; + bool string_different = false; /* Is the string shorter than the input string? If yes skip it. */ - if(name.length() < tmp.length()) + if (name.length() < tmp.length()) continue; /* Is the beginning of the string different from the input string? If yes skip it. */ - for( std::string::iterator iter=tmp.begin(), iter2=name.begin(); iter < tmp.end();++iter, ++iter2) { - if( Misc::StringUtils::toLower(*iter) != Misc::StringUtils::toLower(*iter2) ) { - string_different=true; + for (std::string::iterator iter = tmp.begin(), iter2 = name.begin(); iter < tmp.end(); ++iter, ++iter2) + { + if (Misc::StringUtils::toLower(*iter) != Misc::StringUtils::toLower(*iter2)) + { + string_different = true; break; } } - if( string_different ) + if (string_different) continue; /* The beginning of the string matches the input string, save it for the next test. */ @@ -430,23 +725,27 @@ namespace MWGui } /* There are no matches. Return the unchanged input. */ - if( matches.empty() ) + if (matches.empty()) { return input; } /* Only one match. We're done. */ - if( matches.size() == 1 ) { + if (matches.size() == 1) + { /* Adding quotation marks when the input string started with a quotation mark or has spaces in it*/ - if( ( matches.front().find(' ') != std::string::npos ) ) { - if( !has_front_quote ) - output.append(std::string("\"")); + if ((matches.front().find(' ') != std::string::npos)) + { + if (!has_front_quote) + output += '"'; return output.append(matches.front() + std::string("\" ")); } - else if( has_front_quote ) { - return output.append(matches.front() + std::string("\" ")); + else if (has_front_quote) + { + return output.append(matches.front() + std::string("\" ")); } - else { + else + { return output.append(matches.front() + std::string(" ")); } } @@ -454,11 +753,12 @@ namespace MWGui /* Check if all matching strings match further than input. If yes complete to this match. */ int i = tmp.length(); - for(std::string::iterator iter=matches.front().begin()+tmp.length(); iter < matches.front().end(); ++iter, ++i) + for (std::string::iterator iter = matches.front().begin() + tmp.length(); iter < matches.front().end(); + ++iter, ++i) { - for(std::string& match : matches) + for (std::string& match : matches) { - if(Misc::StringUtils::toLower(match[i]) != Misc::StringUtils::toLower(*iter)) + if (Misc::StringUtils::toLower(match[i]) != Misc::StringUtils::toLower(*iter)) { /* Append the longest match to the end of the output string*/ output.append(matches.front().substr(0, i)); @@ -471,11 +771,6 @@ namespace MWGui return output.append(matches.front()); } - void Console::onResChange(int width, int height) - { - setCoord(10,10, width-10, height/2); - } - void Console::updateSelectedObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) { if (mPtr == currentPtr) @@ -487,23 +782,31 @@ namespace MWGui if (!object.isEmpty()) { if (object == mPtr) - { - setTitle("#{sConsoleTitle}"); - mPtr=MWWorld::Ptr(); - } + mPtr = MWWorld::Ptr(); else - { - setTitle("#{sConsoleTitle} (" + object.getCellRef().getRefId() + ")"); mPtr = object; - } // User clicked on an object. Restore focus to the console command line. MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCommandLine); } else - { - setTitle("#{sConsoleTitle}"); mPtr = MWWorld::Ptr(); - } + updateConsoleTitle(); + } + + void Console::updateConsoleTitle() + { + std::string title = "#{OMWEngine:ConsoleWindow}"; + if (!mConsoleMode.empty()) + title = mConsoleMode + " " + title; + if (!mPtr.isEmpty()) + title.append(" (" + mPtr.getCellRef().getRefId().toDebugString() + ")"); + setTitle(title); + } + + void Console::setConsoleMode(std::string_view mode) + { + mConsoleMode = std::string(mode); + updateConsoleTitle(); } void Console::onReferenceUnavailable() @@ -516,4 +819,44 @@ namespace MWGui ReferenceInterface::resetReference(); setSelectedObject(MWWorld::Ptr()); } + + void Console::initConsoleHistory() + { + const auto filePath = mCfgMgr.getUserConfigPath() / "console_history.txt"; + const size_t retrievalLimit = Settings::general().mConsoleHistoryBufferSize; + + // Read the previous session's commands from the file + if (retrievalLimit > 0) + { + std::ifstream historyFile(filePath); + std::string line; + while (std::getline(historyFile, line)) + { + // Truncate the list if it exceeds the retrieval limit + if (mCommandHistory.size() >= retrievalLimit) + mCommandHistory.pop_front(); + mCommandHistory.push_back(line); + } + historyFile.close(); + } + + mCurrent = mCommandHistory.end(); + try + { + mCommandHistoryFile.exceptions(std::fstream::failbit | std::fstream::badbit); + mCommandHistoryFile.open(filePath, std::ios_base::trunc); + + // Update the history file + for (const auto& histObj : mCommandHistory) + mCommandHistoryFile << histObj << std::endl; + mCommandHistoryFile.close(); + + mCommandHistoryFile.open(filePath, std::ios_base::app); + } + catch (const std::ios_base::failure& e) + { + Log(Debug::Error) << "Error: Failed to write to console history file " << filePath << " : " << e.what() + << " : " << std::generic_category().message(errno); + } + } } diff --git a/apps/openmw/mwgui/console.hpp b/apps/openmw/mwgui/console.hpp index 2b8cecbc019..2b6ecfc8adb 100644 --- a/apps/openmw/mwgui/console.hpp +++ b/apps/openmw/mwgui/console.hpp @@ -6,11 +6,13 @@ #include #include -#include #include +#include +#include + +#include "../mwbase/windowmanager.hpp" #include "../mwscript/compilercontext.hpp" -#include "../mwscript/interpretercontext.hpp" #include "referenceinterface.hpp" #include "windowbase.hpp" @@ -19,79 +21,111 @@ namespace MWGui { class Console : public WindowBase, private Compiler::ErrorHandler, public ReferenceInterface { - public: - /// Set the implicit object for script execution - void setSelectedObject(const MWWorld::Ptr& object); + public: + /// Set the implicit object for script execution + void setSelectedObject(const MWWorld::Ptr& object); + MWWorld::Ptr getSelectedObject() const { return mPtr; } + + MyGUI::EditBox* mCommandLine; + MyGUI::EditBox* mHistory; + MyGUI::EditBox* mSearchTerm; + MyGUI::Button* mNextButton; + MyGUI::Button* mPreviousButton; + MyGUI::Button* mCaseSensitiveToggleButton; + MyGUI::Button* mRegExSearchToggleButton; - MyGUI::EditBox* mCommandLine; - MyGUI::EditBox* mHistory; + typedef std::list StringList; - typedef std::list StringList; + // History of previous entered commands + StringList mCommandHistory; + StringList::iterator mCurrent; + std::string mEditString; + std::ofstream mCommandHistoryFile; - // History of previous entered commands - StringList mCommandHistory; - StringList::iterator mCurrent; - std::string mEditString; + Console(int w, int h, bool consoleOnlyScripts, Files::ConfigurationManager& cfgMgr); + ~Console(); - Console(int w, int h, bool consoleOnlyScripts); + void onOpen() override; - void onOpen() override; + // Print a message to the console, in specified color. + void print(const std::string& msg, std::string_view color = MWBase::WindowManager::sConsoleColor_Default); - void onResChange(int width, int height) override; + // These are pre-colored versions that you should use. - // Print a message to the console, in specified color. - void print(const std::string &msg, const std::string& color = "#FFFFFF"); + /// Output from successful console command + void printOK(const std::string& msg); - // These are pre-colored versions that you should use. + /// Error message + void printError(const std::string& msg); - /// Output from successful console command - void printOK(const std::string &msg); + void execute(const std::string& command); - /// Error message - void printError(const std::string &msg); + void executeFile(const std::filesystem::path& path); - void execute (const std::string& command); + void updateSelectedObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr); - void executeFile (const std::string& path); + void onFrame(float dt) override { checkReferenceAvailable(); } + void clear() override; - void updateSelectedObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr); + void resetReference() override; - void clear() override; + const std::string& getConsoleMode() const { return mConsoleMode; } + void setConsoleMode(std::string_view mode); - void resetReference () override; + protected: + void onReferenceUnavailable() override; - protected: + private: + std::string mConsoleMode; - void onReferenceUnavailable() override; + void updateConsoleTitle(); - private: + void commandBoxKeyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char); + void acceptCommand(MyGUI::EditBox* _sender); - void keyPress(MyGUI::Widget* _sender, - MyGUI::KeyCode key, - MyGUI::Char _char); + enum class SearchDirection; + void toggleCaseSensitiveSearch(MyGUI::Widget* _sender); + void toggleRegExSearch(MyGUI::Widget* _sender); + void acceptSearchTerm(MyGUI::EditBox* _sender); + void findNextOccurrence(MyGUI::Widget* _sender); + void findPreviousOccurrence(MyGUI::Widget* _sender); + void findOccurrence(SearchDirection direction); + void findInHistoryText( + const std::string& historyText, SearchDirection direction, size_t firstIndex, size_t lastIndex); + void findWithRegex( + const std::string& historyText, SearchDirection direction, size_t firstIndex, size_t lastIndex); + void findWithStringSearch( + const std::string& historyText, SearchDirection direction, size_t firstIndex, size_t lastIndex); + void markOccurrence(size_t textPosition, size_t length); + size_t mCurrentOccurrenceIndex = std::string::npos; + size_t mCurrentOccurrenceLength = 0; + std::string mCurrentSearchTerm; + bool mCaseSensitiveSearch; + bool mRegExSearch; - void acceptCommand(MyGUI::EditBox* _sender); + std::string complete(std::string input, std::vector& matches); - std::string complete( std::string input, std::vector &matches ); + Compiler::Extensions mExtensions; + MWScript::CompilerContext mCompilerContext; + std::vector mNames; - Compiler::Extensions mExtensions; - MWScript::CompilerContext mCompilerContext; - std::vector mNames; - bool mConsoleOnlyScripts; + bool mConsoleOnlyScripts; + Files::ConfigurationManager& mCfgMgr; + bool compile(const std::string& cmd, Compiler::Output& output); - bool compile (const std::string& cmd, Compiler::Output& output); + /// Report error to the user. + void report(const std::string& message, const Compiler::TokenLoc& loc, Type type) override; - /// Report error to the user. - void report (const std::string& message, const Compiler::TokenLoc& loc, Type type) override; + /// Report a file related error + void report(const std::string& message, Type type) override; - /// Report a file related error - void report (const std::string& message, Type type) override; + /// Write all valid identifiers and keywords into mNames and sort them. + /// \note If mNames is not empty, this function is a no-op. + /// \note The list may contain duplicates (if a name is a keyword and an identifier at the same + /// time). + void listNames(); - /// Write all valid identifiers and keywords into mNames and sort them. - /// \note If mNames is not empty, this function is a no-op. - /// \note The list may contain duplicates (if a name is a keyword and an identifier at the same - /// time). - void listNames(); - }; + void initConsoleHistory(); + }; } #endif diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index 4cdd8b137f3..6ab2c862d43 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -1,13 +1,13 @@ #include "container.hpp" -#include #include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/scriptmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" @@ -21,12 +21,12 @@ #include "countdialog.hpp" #include "inventorywindow.hpp" -#include "itemview.hpp" -#include "inventoryitemmodel.hpp" #include "containeritemmodel.hpp" -#include "sortfilteritemmodel.hpp" -#include "pickpocketitemmodel.hpp" #include "draganddrop.hpp" +#include "inventoryitemmodel.hpp" +#include "itemview.hpp" +#include "pickpocketitemmodel.hpp" +#include "sortfilteritemmodel.hpp" #include "tooltips.hpp" namespace MWGui @@ -38,6 +38,7 @@ namespace MWGui , mSortModel(nullptr) , mModel(nullptr) , mSelectedItem(-1) + , mTreatNextOpenAsLoot(false) { getWidget(mDisposeCorpseButton, "DisposeCorpseButton"); getWidget(mTakeButton, "TakeButton"); @@ -47,11 +48,12 @@ namespace MWGui mItemView->eventBackgroundClicked += MyGUI::newDelegate(this, &ContainerWindow::onBackgroundSelected); mItemView->eventItemClicked += MyGUI::newDelegate(this, &ContainerWindow::onItemSelected); - mDisposeCorpseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ContainerWindow::onDisposeCorpseButtonClicked); + mDisposeCorpseButton->eventMouseButtonClick + += MyGUI::newDelegate(this, &ContainerWindow::onDisposeCorpseButtonClicked); mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ContainerWindow::onCloseButtonClicked); mTakeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ContainerWindow::onTakeAllButtonClicked); - setCoord(200,0,600,300); + setCoord(200, 0, 600, 300); } void ContainerWindow::onItemSelected(int index) @@ -82,13 +84,14 @@ namespace MWGui if (count > 1 && !shift) { CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); - std::string name = object.getClass().getName(object) + MWGui::ToolTips::getSoulString(object.getCellRef()); + std::string name{ object.getClass().getName(object) }; + name += MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, "#{sTake}", count); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &ContainerWindow::dragItem); } else - dragItem (nullptr, count); + dragItem(nullptr, count); } void ContainerWindow::dragItem(MyGUI::Widget* sender, int count) @@ -121,31 +124,37 @@ namespace MWGui void ContainerWindow::setPtr(const MWWorld::Ptr& container) { + if (container.isEmpty() || (container.getType() != ESM::REC_CONT && !container.getClass().isActor())) + throw std::runtime_error("Invalid argument in ContainerWindow::setPtr"); + bool lootAnyway = mTreatNextOpenAsLoot; + mTreatNextOpenAsLoot = false; mPtr = container; bool loot = mPtr.getClass().isActor() && mPtr.getClass().getCreatureStats(mPtr).isDead(); + std::unique_ptr model; if (mPtr.getClass().hasInventoryStore(mPtr)) { - if (mPtr.getClass().isNpc() && !loot) + if (mPtr.getClass().isNpc() && !loot && !lootAnyway) { // we are stealing stuff - mModel = new PickpocketItemModel(mPtr, new InventoryItemModel(container), - !mPtr.getClass().getCreatureStats(mPtr).getKnockedDown()); + model = std::make_unique(mPtr, std::make_unique(container), + !mPtr.getClass().getCreatureStats(mPtr).getKnockedDown()); } else - mModel = new InventoryItemModel(container); + model = std::make_unique(container); } else { - mModel = new ContainerItemModel(container); + model = std::make_unique(container); } mDisposeCorpseButton->setVisible(loot); + mModel = model.get(); + auto sortModel = std::make_unique(std::move(model)); + mSortModel = sortModel.get(); - mSortModel = new SortFilterItemModel(mModel); - - mItemView->setModel (mSortModel); + mItemView->setModel(std::move(sortModel)); mItemView->resetScrollBars(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); @@ -184,7 +193,9 @@ namespace MWGui void ContainerWindow::onTakeAllButtonClicked(MyGUI::Widget* _sender) { - if(mDragAndDrop != nullptr && mDragAndDrop->mIsOnDragAndDrop) + if (!mModel) + return; + if (mDragAndDrop != nullptr && mDragAndDrop->mIsOnDragAndDrop) return; MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); @@ -198,25 +209,25 @@ namespace MWGui if (mPtr.getClass().hasInventoryStore(mPtr)) { MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); - for (size_t i=0; igetItemCount(); ++i) + for (size_t i = 0; i < mModel->getItemCount(); ++i) { const ItemStack& item = mModel->getItem(i); if (invStore.isEquipped(item.mBase) == false) continue; - invStore.unequipItem(item.mBase, mPtr); + invStore.unequipItem(item.mBase); } } mModel->update(); - for (size_t i=0; igetItemCount(); ++i) + for (size_t i = 0; i < mModel->getItemCount(); ++i) { - if (i==0) + if (i == 0) { // play the sound of the first object MWWorld::Ptr item = mModel->getItem(i).mBase; - std::string sound = item.getClass().getUpSoundId(item); + const ESM::RefId& sound = item.getClass().getUpSoundId(item); MWBase::Environment::get().getWindowManager()->playSound(sound); } @@ -231,9 +242,9 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); } - void ContainerWindow::onDisposeCorpseButtonClicked(MyGUI::Widget *sender) + void ContainerWindow::onDisposeCorpseButtonClicked(MyGUI::Widget* sender) { - if(mDragAndDrop == nullptr || !mDragAndDrop->mIsOnDragAndDrop) + if (mDragAndDrop == nullptr || !mDragAndDrop->mIsOnDragAndDrop) { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); @@ -254,31 +265,33 @@ namespace MWGui creatureStats.setDeathAnimationFinished(true); MWBase::Environment::get().getMechanicsManager()->notifyDied(ptr); - const std::string script = ptr.getClass().getScript(ptr); + const ESM::RefId& script = ptr.getClass().getScript(ptr); if (!script.empty() && MWBase::Environment::get().getWorld()->getScriptsEnabled()) { - MWScript::InterpreterContext interpreterContext (&ptr.getRefData().getLocals(), ptr); - MWBase::Environment::get().getScriptManager()->run (script, interpreterContext); + MWScript::InterpreterContext interpreterContext(&ptr.getRefData().getLocals(), ptr); + MWBase::Environment::get().getScriptManager()->run(script, interpreterContext); } // Clean up summoned creatures as well - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); + auto& creatureMap = creatureStats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(ptr, creature.second); creatureMap.clear(); // Check if we are a summon and inform our master we've bit the dust - for(const auto& package : creatureStats.getAiSequence()) + for (const auto& package : creatureStats.getAiSequence()) { - if(package->followTargetThroughDoors() && !package->getTarget().isEmpty()) + if (package->followTargetThroughDoors() && !package->getTarget().isEmpty()) { const auto& summoner = package->getTarget(); auto& summons = summoner.getClass().getCreatureStats(summoner).getSummonedCreatureMap(); - auto it = std::find_if(summons.begin(), summons.end(), [&] (const auto& entry) { return entry.second == creatureStats.getActorId(); }); - if(it != summons.end()) + auto it = std::find_if(summons.begin(), summons.end(), + [&](const auto& entry) { return entry.second == creatureStats.getActorId(); }); + if (it != summons.end()) { - MWMechanics::purgeSummonEffect(summoner, *it); + auto summon = *it; summons.erase(it); + MWMechanics::purgeSummonEffect(summoner, summon); break; } } @@ -297,9 +310,14 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); } - bool ContainerWindow::onTakeItem(const ItemStack &item, int count) + 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); + } } diff --git a/apps/openmw/mwgui/container.hpp b/apps/openmw/mwgui/container.hpp index 85c0dddc67c..555fa8e1aea 100644 --- a/apps/openmw/mwgui/container.hpp +++ b/apps/openmw/mwgui/container.hpp @@ -1,8 +1,8 @@ #ifndef MGUI_CONTAINER_H #define MGUI_CONTAINER_H -#include "windowbase.hpp" #include "referenceinterface.hpp" +#include "windowbase.hpp" #include "itemmodel.hpp" @@ -19,7 +19,6 @@ namespace MWGui class SortFilterItemModel; } - namespace MWGui { class ContainerWindow : public WindowBase, public ReferenceInterface @@ -35,6 +34,12 @@ namespace MWGui void resetReference() override; + void onDeleteCustomData(const MWWorld::Ptr& ptr) override; + + void treatNextOpenAsLoot() { mTreatNextOpenAsLoot = true; } + + std::string_view getWindowIdForLua() const override { return "Container"; } + private: DragAndDrop* mDragAndDrop; @@ -42,7 +47,7 @@ namespace MWGui SortFilterItemModel* mSortModel; ItemModel* mModel; int mSelectedItem; - + bool mTreatNextOpenAsLoot; MyGUI::Button* mDisposeCorpseButton; MyGUI::Button* mTakeButton; MyGUI::Button* mCloseButton; diff --git a/apps/openmw/mwgui/containeritemmodel.cpp b/apps/openmw/mwgui/containeritemmodel.cpp index 56f084bb9da..09b66672bae 100644 --- a/apps/openmw/mwgui/containeritemmodel.cpp +++ b/apps/openmw/mwgui/containeritemmodel.cpp @@ -2,11 +2,12 @@ #include -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" -#include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/containerstore.hpp" +#include "../mwworld/manualref.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -16,15 +17,14 @@ namespace { - bool stacks (const MWWorld::Ptr& left, const MWWorld::Ptr& right) + bool stacks(const MWWorld::Ptr& left, const MWWorld::Ptr& right) { if (left == right) return true; // If one of the items is in an inventory and currently equipped, we need to check stacking both ways to be sure if (left.getContainerStore() && right.getContainerStore()) - return left.getContainerStore()->stacks(left, right) - && right.getContainerStore()->stacks(left, right); + return left.getContainerStore()->stacks(left, right) && right.getContainerStore()->stacks(left, right); if (left.getContainerStore()) return left.getContainerStore()->stacks(left, right); @@ -39,134 +39,168 @@ namespace namespace MWGui { -ContainerItemModel::ContainerItemModel(const std::vector& itemSources, const std::vector& worldItems) - : mWorldItems(worldItems) - , mTrading(true) -{ - assert (!itemSources.empty()); - // Tie resolution lifetimes to the ItemModel - mItemSources.reserve(itemSources.size()); - for(const MWWorld::Ptr& source : itemSources) + ContainerItemModel::ContainerItemModel( + const std::vector& itemSources, const std::vector& worldItems) + : mWorldItems(worldItems) + , mTrading(true) + { + assert(!itemSources.empty()); + // Tie resolution lifetimes to the ItemModel + mItemSources.reserve(itemSources.size()); + for (const MWWorld::Ptr& source : itemSources) + { + MWWorld::ContainerStore& store = source.getClass().getContainerStore(source); + mItemSources.emplace_back(source, store.resolveTemporarily()); + } + } + + ContainerItemModel::ContainerItemModel(const MWWorld::Ptr& source) + : mTrading(false) { MWWorld::ContainerStore& store = source.getClass().getContainerStore(source); mItemSources.emplace_back(source, store.resolveTemporarily()); } -} - -ContainerItemModel::ContainerItemModel (const MWWorld::Ptr& source) : mTrading(false) -{ - MWWorld::ContainerStore& store = source.getClass().getContainerStore(source); - mItemSources.emplace_back(source, store.resolveTemporarily()); -} - -bool ContainerItemModel::allowedToUseItems() const -{ - if (mItemSources.empty()) - return true; - MWWorld::Ptr ptr = MWMechanics::getPlayer(); - MWWorld::Ptr victim; + bool ContainerItemModel::allowedToUseItems() const + { + if (mItemSources.empty()) + return true; - // Check if the player is allowed to use items from opened container - MWBase::MechanicsManager* mm = MWBase::Environment::get().getMechanicsManager(); - return mm->isAllowedToUse(ptr, mItemSources[0].first, victim); -} + MWWorld::Ptr ptr = MWMechanics::getPlayer(); + MWWorld::Ptr victim; -ItemStack ContainerItemModel::getItem (ModelIndex index) -{ - if (index < 0) - throw std::runtime_error("Invalid index supplied"); - if (mItems.size() <= static_cast(index)) - throw std::runtime_error("Item index out of range"); - return mItems[index]; -} + // Check if the player is allowed to use items from opened container + MWBase::MechanicsManager* mm = MWBase::Environment::get().getMechanicsManager(); + return mm->isAllowedToUse(ptr, mItemSources[0].first, victim); + } -size_t ContainerItemModel::getItemCount() -{ - return mItems.size(); -} + ItemStack ContainerItemModel::getItem(ModelIndex index) + { + if (index < 0) + throw std::runtime_error("Invalid index supplied"); + if (mItems.size() <= static_cast(index)) + throw std::runtime_error("Item index out of range"); + return mItems[index]; + } -ItemModel::ModelIndex ContainerItemModel::getIndex (ItemStack item) -{ - size_t i = 0; - for (ItemStack& itemStack : mItems) + size_t ContainerItemModel::getItemCount() { - if (itemStack == item) - return i; - ++i; + return mItems.size(); } - return -1; -} -MWWorld::Ptr ContainerItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) -{ - auto& source = mItemSources[0]; - MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); - if (item.mBase.getContainerStore() == &store) - throw std::runtime_error("Item to copy needs to be from a different container!"); - return *store.add(item.mBase, count, source.first, allowAutoEquip); -} + ItemModel::ModelIndex ContainerItemModel::getIndex(const ItemStack& item) + { + size_t i = 0; + for (ItemStack& itemStack : mItems) + { + if (itemStack == item) + return i; + ++i; + } + return -1; + } -void ContainerItemModel::removeItem (const ItemStack& item, size_t count) -{ - int toRemove = count; + MWWorld::Ptr ContainerItemModel::addItem(const ItemStack& item, size_t count, bool allowAutoEquip) + { + auto& source = mItemSources[0]; + MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); + if (item.mBase.getContainerStore() == &store) + throw std::runtime_error("Item to add needs to be from a different container!"); + return *store.add(item.mBase, count, allowAutoEquip); + } - for (auto& source : mItemSources) + MWWorld::Ptr ContainerItemModel::copyItem(const ItemStack& item, size_t count, bool allowAutoEquip) { + auto& source = mItemSources[0]; MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); + if (item.mBase.getContainerStore() == &store) + throw std::runtime_error("Item to copy needs to be from a different container!"); + MWWorld::ManualRef newRef(*MWBase::Environment::get().getESMStore(), item.mBase, count); + return *store.add(newRef.getPtr(), count, allowAutoEquip); + } + + void ContainerItemModel::removeItem(const ItemStack& item, size_t count) + { + int toRemove = count; - for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + for (auto& source : mItemSources) { - if (stacks(*it, item.mBase)) + MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); + + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { - int quantity = it->mRef->mData.getCount(false); - // If this is a restocking quantity, just don't remove it - if(quantity < 0 && mTrading) - toRemove += quantity; + if (stacks(*it, item.mBase)) + { + int quantity = it->mRef->mRef.getCount(false); + // If this is a restocking quantity, just don't remove it + if (quantity < 0 && mTrading) + toRemove += quantity; + else + toRemove -= store.remove(*it, toRemove); + if (toRemove <= 0) + return; + } + } + } + for (MWWorld::Ptr& source : mWorldItems) + { + if (stacks(source, item.mBase)) + { + int refCount = source.getCellRef().getCount(); + if (refCount - toRemove <= 0) + MWBase::Environment::get().getWorld()->deleteObject(source); else - toRemove -= store.remove(*it, toRemove, source.first); + source.getCellRef().setCount(std::max(0, refCount - toRemove)); + toRemove -= refCount; if (toRemove <= 0) return; } } + + throw std::runtime_error("Not enough items to remove could be found"); } - for (MWWorld::Ptr& source : mWorldItems) + + void ContainerItemModel::update() { - if (stacks(source, item.mBase)) + mItems.clear(); + for (auto& source : mItemSources) { - int refCount = source.getRefData().getCount(); - if (refCount - toRemove <= 0) - MWBase::Environment::get().getWorld()->deleteObject(source); - else - source.getRefData().setCount(std::max(0, refCount - toRemove)); - toRemove -= refCount; - if (toRemove <= 0) - return; - } - } + MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); - throw std::runtime_error("Not enough items to remove could be found"); -} + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + if (!(*it).getClass().showsInInventory(*it)) + continue; -void ContainerItemModel::update() -{ - mItems.clear(); - for (auto& source : mItemSources) - { - MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); + bool found = false; + for (ItemStack& itemStack : mItems) + { + if (stacks(*it, itemStack.mBase)) + { + // we already have an item stack of this kind, add to it + itemStack.mCount += it->getCellRef().getCount(); + found = true; + break; + } + } - for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + if (!found) + { + // no stack yet, create one + ItemStack newItem(*it, this, it->getCellRef().getCount()); + mItems.push_back(newItem); + } + } + } + for (MWWorld::Ptr& source : mWorldItems) { - if (!(*it).getClass().showsInInventory(*it)) - continue; - bool found = false; for (ItemStack& itemStack : mItems) { - if (stacks(*it, itemStack.mBase)) + if (stacks(source, itemStack.mBase)) { // we already have an item stack of this kind, add to it - itemStack.mCount += it->getRefData().getCount(); + itemStack.mCount += source.getCellRef().getCount(); found = true; break; } @@ -175,78 +209,65 @@ void ContainerItemModel::update() if (!found) { // no stack yet, create one - ItemStack newItem (*it, this, it->getRefData().getCount()); + ItemStack newItem(source, this, source.getCellRef().getCount()); mItems.push_back(newItem); } } } - for (MWWorld::Ptr& source : mWorldItems) + bool ContainerItemModel::onDropItem(const MWWorld::Ptr& item, int count) { - bool found = false; - for (ItemStack& itemStack : mItems) + if (mItemSources.empty()) + return false; + + MWWorld::Ptr target = mItemSources[0].first; + + if (target.getType() != ESM::Container::sRecordId) + return true; + + // check container organic flag + MWWorld::LiveCellRef* ref = target.get(); + if (ref->mBase->mFlags & ESM::Container::Organic) { - if (stacks(source, itemStack.mBase)) - { - // we already have an item stack of this kind, add to it - itemStack.mCount += source.getRefData().getCount(); - found = true; - break; - } + MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage2}"); + return false; } - if (!found) + // 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) { - // no stack yet, create one - ItemStack newItem (source, this, source.getRefData().getCount()); - mItems.push_back(newItem); + MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage3}"); + return false; } - } -} -bool ContainerItemModel::onDropItem(const MWWorld::Ptr &item, int count) -{ - if (mItemSources.empty()) - return false; - - MWWorld::Ptr target = mItemSources[0].first; - if (target.getTypeName() != typeid(ESM::Container).name()) return true; - - // check container organic flag - MWWorld::LiveCellRef* ref = target.get(); - if (ref->mBase->mFlags & ESM::Container::Organic) - { - MWBase::Environment::get().getWindowManager()-> - messageBox("#{sContentsMessage2}"); - 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) + bool ContainerItemModel::onTakeItem(const MWWorld::Ptr& item, int count) { - MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage3}"); - return false; - } + if (mItemSources.empty()) + return false; - return true; -} + MWWorld::Ptr target = mItemSources[0].first; -bool ContainerItemModel::onTakeItem(const MWWorld::Ptr &item, int count) -{ - if (mItemSources.empty()) - return false; + // Looting a dead corpse is considered OK + if (target.getClass().isActor() && target.getClass().getCreatureStats(target).isDead()) + return true; - MWWorld::Ptr target = mItemSources[0].first; + MWWorld::Ptr player = MWMechanics::getPlayer(); + MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item, target, count); - // Looting a dead corpse is considered OK - if (target.getClass().isActor() && target.getClass().getCreatureStats(target).isDead()) return true; + } - MWWorld::Ptr player = MWMechanics::getPlayer(); - MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item, target, count); - - return true; -} + bool ContainerItemModel::usesContainer(const MWWorld::Ptr& container) + { + for (const auto& source : mItemSources) + { + if (source.first == container) + return true; + } + return false; + } } diff --git a/apps/openmw/mwgui/containeritemmodel.hpp b/apps/openmw/mwgui/containeritemmodel.hpp index c54f113147e..8e6f5a37efd 100644 --- a/apps/openmw/mwgui/containeritemmodel.hpp +++ b/apps/openmw/mwgui/containeritemmodel.hpp @@ -16,26 +16,30 @@ namespace MWGui class ContainerItemModel : public ItemModel { public: - ContainerItemModel (const std::vector& itemSources, const std::vector& worldItems); - ///< @note The order of elements \a itemSources matters here. The first element has the highest priority for removal, + ContainerItemModel(const std::vector& itemSources, const std::vector& worldItems); + ///< @note The order of elements \a itemSources matters here. The first element has the highest priority for + ///< removal, /// while the last element will be used to add new items to. - ContainerItemModel (const MWWorld::Ptr& source); + ContainerItemModel(const MWWorld::Ptr& source); bool allowedToUseItems() const override; - bool onDropItem(const MWWorld::Ptr &item, int count) override; - bool onTakeItem(const MWWorld::Ptr &item, int count) override; + bool onDropItem(const MWWorld::Ptr& item, int count) override; + bool onTakeItem(const MWWorld::Ptr& item, int count) override; - ItemStack getItem (ModelIndex index) override; - ModelIndex getIndex (ItemStack item) override; + ItemStack getItem(ModelIndex index) override; + ModelIndex getIndex(const ItemStack& item) override; size_t getItemCount() override; - MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; - void removeItem (const ItemStack& item, size_t count) override; + MWWorld::Ptr addItem(const ItemStack& item, size_t count, bool allowAutoEquip = true) override; + MWWorld::Ptr copyItem(const ItemStack& item, size_t count, bool allowAutoEquip = true) override; + void removeItem(const ItemStack& item, size_t count) override; void update() override; + bool usesContainer(const MWWorld::Ptr& container) override; + private: std::vector> mItemSources; std::vector mWorldItems; diff --git a/apps/openmw/mwgui/controllers.cpp b/apps/openmw/mwgui/controllers.cpp index f3932d3ce6d..f024def39b3 100644 --- a/apps/openmw/mwgui/controllers.cpp +++ b/apps/openmw/mwgui/controllers.cpp @@ -7,11 +7,9 @@ namespace MWGui { namespace Controllers { - void ControllerFollowMouse::prepareItem(MyGUI::Widget *_widget) - { - } + void ControllerFollowMouse::prepareItem(MyGUI::Widget* _widget) {} - bool ControllerFollowMouse::addTime(MyGUI::Widget *_widget, float _time) + bool ControllerFollowMouse::addTime(MyGUI::Widget* _widget, float _time) { _widget->setPosition(MyGUI::InputManager::getInstance().getMousePosition()); return true; diff --git a/apps/openmw/mwgui/controllers.hpp b/apps/openmw/mwgui/controllers.hpp index 416f104d9a7..a9e5565187b 100644 --- a/apps/openmw/mwgui/controllers.hpp +++ b/apps/openmw/mwgui/controllers.hpp @@ -1,8 +1,8 @@ #ifndef MWGUI_CONTROLLERS_H #define MWGUI_CONTROLLERS_H -#include #include +#include namespace MyGUI { @@ -10,16 +10,16 @@ namespace MyGUI } namespace MWGui::Controllers +{ + /// Automatically positions a widget below the mouse cursor. + class ControllerFollowMouse final : public MyGUI::ControllerItem { - /// Automatically positions a widget below the mouse cursor. - class ControllerFollowMouse final : public MyGUI::ControllerItem - { - MYGUI_RTTI_DERIVED( ControllerFollowMouse ) + MYGUI_RTTI_DERIVED(ControllerFollowMouse) - private: - bool addTime(MyGUI::Widget* _widget, float _time) override; - void prepareItem(MyGUI::Widget* _widget) override; - }; - } + private: + bool addTime(MyGUI::Widget* _widget, float _time) override; + void prepareItem(MyGUI::Widget* _widget) override; + }; +} #endif diff --git a/apps/openmw/mwgui/countdialog.cpp b/apps/openmw/mwgui/countdialog.cpp index baf3a43abdb..2ca6657a17c 100644 --- a/apps/openmw/mwgui/countdialog.cpp +++ b/apps/openmw/mwgui/countdialog.cpp @@ -1,8 +1,8 @@ #include "countdialog.hpp" #include -#include #include +#include #include @@ -11,8 +11,8 @@ namespace MWGui { - CountDialog::CountDialog() : - WindowModal("openmw_count_window.layout") + CountDialog::CountDialog() + : WindowModal("openmw_count_window.layout") { getWidget(mSlider, "CountSlider"); getWidget(mItemEdit, "ItemEdit"); @@ -40,16 +40,14 @@ namespace MWGui mSlider->setScrollRange(maxCount); mItemText->setCaption(item); - int width = std::max(mItemText->getTextSize().width + 128, 320); - setCoord(viewSize.width/2 - width/2, - viewSize.height/2 - mMainWidget->getHeight()/2, - width, - mMainWidget->getHeight()); + int width = std::max(mItemText->getTextSize().width + 160, 320); + setCoord(viewSize.width / 2 - width / 2, viewSize.height / 2 - mMainWidget->getHeight() / 2, width, + mMainWidget->getHeight()); // by default, the text edit field has the focus of the keyboard MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mItemEdit); - mSlider->setScrollPosition(maxCount-1); + mSlider->setScrollPosition(maxCount - 1); mItemEdit->setMinValue(1); mItemEdit->setMaxValue(maxCount); @@ -63,7 +61,7 @@ namespace MWGui void CountDialog::onOkButtonClicked(MyGUI::Widget* _sender) { - eventOkClicked(nullptr, mSlider->getScrollPosition()+1); + eventOkClicked(nullptr, mSlider->getScrollPosition() + 1); setVisible(false); } @@ -72,7 +70,7 @@ namespace MWGui // Enter key void CountDialog::onEnterKeyPressed(MyGUI::EditBox* _sender) { - eventOkClicked(nullptr, mSlider->getScrollPosition()+1); + eventOkClicked(nullptr, mSlider->getScrollPosition() + 1); setVisible(false); // To do not spam onEnterKeyPressed() again and again @@ -81,11 +79,11 @@ namespace MWGui void CountDialog::onEditValueChanged(int value) { - mSlider->setScrollPosition(value-1); + mSlider->setScrollPosition(value - 1); } void CountDialog::onSliderMoved(MyGUI::ScrollBar* _sender, size_t _position) { - mItemEdit->setValue(_position+1); + mItemEdit->setValue(_position + 1); } } diff --git a/apps/openmw/mwgui/countdialog.hpp b/apps/openmw/mwgui/countdialog.hpp index 766612f6812..9cdf2315493 100644 --- a/apps/openmw/mwgui/countdialog.hpp +++ b/apps/openmw/mwgui/countdialog.hpp @@ -12,30 +12,30 @@ namespace MWGui { class CountDialog : public WindowModal { - public: - CountDialog(); - void openCountDialog(const std::string& item, const std::string& message, const int maxCount); - - typedef MyGUI::delegates::CMultiDelegate2 EventHandle_WidgetInt; - - /** Event : Ok button was clicked.\n - signature : void method(MyGUI::Widget* _sender, int _count)\n - */ - EventHandle_WidgetInt eventOkClicked; - - private: - MyGUI::ScrollBar* mSlider; - Gui::NumericEditBox* mItemEdit; - MyGUI::TextBox* mItemText; - MyGUI::TextBox* mLabelText; - MyGUI::Button* mOkButton; - MyGUI::Button* mCancelButton; - - void onCancelButtonClicked(MyGUI::Widget* _sender); - void onOkButtonClicked(MyGUI::Widget* _sender); - void onEditValueChanged(int value); - void onSliderMoved(MyGUI::ScrollBar* _sender, size_t _position); - void onEnterKeyPressed(MyGUI::EditBox* _sender); + public: + 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 + */ + EventHandle_WidgetInt eventOkClicked; + + private: + MyGUI::ScrollBar* mSlider; + Gui::NumericEditBox* mItemEdit; + MyGUI::TextBox* mItemText; + MyGUI::TextBox* mLabelText; + MyGUI::Button* mOkButton; + MyGUI::Button* mCancelButton; + + void onCancelButtonClicked(MyGUI::Widget* _sender); + void onOkButtonClicked(MyGUI::Widget* _sender); + void onEditValueChanged(int value); + void onSliderMoved(MyGUI::ScrollBar* _sender, size_t _position); + void onEnterKeyPressed(MyGUI::EditBox* _sender); }; } diff --git a/apps/openmw/mwgui/cursor.cpp b/apps/openmw/mwgui/cursor.cpp index ed8a76eb87b..1b6431f0fb6 100644 --- a/apps/openmw/mwgui/cursor.cpp +++ b/apps/openmw/mwgui/cursor.cpp @@ -1,23 +1,20 @@ #include "cursor.hpp" -#include +#include #include +#include #include -#include namespace MWGui { - ResourceImageSetPointerFix::ResourceImageSetPointerFix() : mImageSet(nullptr) , mRotation(0) { } - ResourceImageSetPointerFix::~ResourceImageSetPointerFix() - { - } + ResourceImageSetPointerFix::~ResourceImageSetPointerFix() {} void ResourceImageSetPointerFix::deserialization(MyGUI::xml::ElementPtr _node, MyGUI::Version _version) { @@ -26,8 +23,8 @@ namespace MWGui MyGUI::xml::ElementEnumerator info = _node->getElementEnumerator(); while (info.next("Property")) { - const std::string& key = info->findAttribute("key"); - const std::string& value = info->findAttribute("value"); + auto key = info->findAttribute("key"); + auto value = info->findAttribute("value"); if (key == "Point") mPoint = MyGUI::IntPoint::parse(value); @@ -56,7 +53,7 @@ namespace MWGui _image->setCoord(_point.left - mPoint.left, _point.top - mPoint.top, mSize.width, mSize.height); } - MyGUI::ResourceImageSetPtr ResourceImageSetPointerFix:: getImageSet() + MyGUI::ResourceImageSetPtr ResourceImageSetPointerFix::getImageSet() { return mImageSet; } diff --git a/apps/openmw/mwgui/cursor.hpp b/apps/openmw/mwgui/cursor.hpp index 7e1f9adba93..b25db0ce062 100644 --- a/apps/openmw/mwgui/cursor.hpp +++ b/apps/openmw/mwgui/cursor.hpp @@ -9,12 +9,12 @@ namespace MWGui /// \brief Allows us to get the members of /// ResourceImageSetPointer that we need. - /// \example MyGUI::FactoryManager::getInstance().registerFactory("Resource", "ResourceImageSetPointer"); + /// \example MyGUI::FactoryManager::getInstance().registerFactory("Resource", + /// "ResourceImageSetPointer"); /// MyGUI::ResourceManager::getInstance().load("core.xml"); - class ResourceImageSetPointerFix final : - public MyGUI::IPointer + class ResourceImageSetPointerFix final : public MyGUI::IPointer { - MYGUI_RTTI_DERIVED( ResourceImageSetPointerFix ) + MYGUI_RTTI_DERIVED(ResourceImageSetPointerFix) public: ResourceImageSetPointerFix(); @@ -25,8 +25,8 @@ namespace MWGui void setImage(MyGUI::ImageBox* _image) override; void setPosition(MyGUI::ImageBox* _image, const MyGUI::IntPoint& _point) override; - //and now for the whole point of this class, allow us to get - //the hot spot, the image and the size of the cursor. + // and now for the whole point of this class, allow us to get + // the hot spot, the image and the size of the cursor. MyGUI::ResourceImageSetPtr getImageSet(); MyGUI::IntPoint getHotSpot(); MyGUI::IntSize getSize(); diff --git a/apps/openmw/mwgui/debugwindow.cpp b/apps/openmw/mwgui/debugwindow.cpp index a29910f0004..59f695e7f86 100644 --- a/apps/openmw/mwgui/debugwindow.cpp +++ b/apps/openmw/mwgui/debugwindow.cpp @@ -1,11 +1,17 @@ #include "debugwindow.hpp" +#include #include #include -#include -#include #include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" + +#include #ifndef BT_NO_PROFILE @@ -17,47 +23,56 @@ namespace if (pit->Is_Done()) return; - float accumulated_time=0,parent_time = pit->Is_Root() ? CProfileManager::Get_Time_Since_Reset() : pit->Get_Current_Parent_Total_Time(); - int i,j; + float accumulated_time = 0, + parent_time + = pit->Is_Root() ? CProfileManager::Get_Time_Since_Reset() : pit->Get_Current_Parent_Total_Time(); + int i, j; int frames_since_reset = CProfileManager::Get_Frame_Count_Since_Reset(); - for (i=0;iGet_Current_Parent_Name())+" (total running time: "+MyGUI::utility::toString(parent_time,3)+" ms) ---\n"; + for (i = 0; i < spacing; i++) + os << "."; + std::string s = "Profiling: " + std::string(pit->Get_Current_Parent_Name()) + + " (total running time: " + MyGUI::utility::toString(parent_time, 3) + " ms) ---\n"; os << s; - //float totalTime = 0.f; + // float totalTime = 0.f; int numChildren = 0; - for (i = 0; !pit->Is_Done(); i++,pit->Next()) + for (i = 0; !pit->Is_Done(); i++, pit->Next()) { numChildren++; float current_total_time = pit->Get_Current_Total_Time(); accumulated_time += current_total_time; float fraction = parent_time > SIMD_EPSILON ? (current_total_time / parent_time) * 100 : 0.f; - for (j=0;jGet_Current_Name()+" ("+MyGUI::utility::toString(fraction,2)+" %) :: "+MyGUI::utility::toString(ms,3)+" ms / frame ("+MyGUI::utility::toString(pit->Get_Current_Total_Calls())+" calls)\n"; + s = MyGUI::utility::toString(i) + " -- " + pit->Get_Current_Name() + " (" + + MyGUI::utility::toString(fraction, 2) + " %) :: " + MyGUI::utility::toString(ms, 3) + " ms / frame (" + + MyGUI::utility::toString(pit->Get_Current_Total_Calls()) + " calls)\n"; os << s; - //totalTime += current_total_time; - //recurse into children + // totalTime += current_total_time; + // recurse into children } if (parent_time < accumulated_time) { os << "what's wrong\n"; } - for (i=0;i SIMD_EPSILON ? ((parent_time - accumulated_time) / parent_time) * 100 : 0.f; - s = "Unaccounted: ("+MyGUI::utility::toString(unaccounted,3)+" %) :: "+MyGUI::utility::toString(parent_time - accumulated_time,3)+" ms\n"; + for (i = 0; i < spacing; i++) + os << "."; + double unaccounted = parent_time > SIMD_EPSILON ? ((parent_time - accumulated_time) / parent_time) * 100 : 0.f; + s = "Unaccounted: (" + MyGUI::utility::toString(unaccounted, 3) + + " %) :: " + MyGUI::utility::toString(parent_time - accumulated_time, 3) + " ms\n"; os << s; - for (i=0;iEnter_Child(i); - bulletDumpRecursive(pit, spacing+3, os); + bulletDumpRecursive(pit, spacing + 3, os); pit->Enter_Parent(); } } @@ -85,33 +100,141 @@ namespace MWGui // Ideas for other tabs: // - Texture / compositor texture viewer - // - Log viewer // - Material editor // - Shader editor - MyGUI::TabItem* item = mTabControl->addItem("Physics Profiler"); - mBulletProfilerEdit = item->createWidgetReal - ("LogEdit", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Stretch); + MyGUI::TabItem* itemLV = mTabControl->addItem("Log Viewer"); + itemLV->setCaptionWithReplacing(" #{OMWEngine:LogViewer} "); + mLogView + = itemLV->createWidgetReal("LogEdit", MyGUI::FloatCoord(0, 0, 1, 1), MyGUI::Align::Stretch); + mLogView->setEditReadOnly(true); - MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - mMainWidget->setSize(viewSize); + MyGUI::TabItem* itemLuaProfiler = mTabControl->addItem("Lua Profiler"); + itemLuaProfiler->setCaptionWithReplacing(" #{OMWEngine:LuaProfiler} "); + mLuaProfiler = itemLuaProfiler->createWidgetReal( + "LogEdit", MyGUI::FloatCoord(0, 0, 1, 1), MyGUI::Align::Stretch); + mLuaProfiler->setEditReadOnly(true); +#ifndef BT_NO_PROFILE + MyGUI::TabItem* item = mTabControl->addItem("Physics Profiler"); + item->setCaptionWithReplacing(" #{OMWEngine:PhysicsProfiler} "); + mBulletProfilerEdit + = item->createWidgetReal("LogEdit", MyGUI::FloatCoord(0, 0, 1, 1), MyGUI::Align::Stretch); +#else + mBulletProfilerEdit = nullptr; +#endif + } + static std::vector sLogCircularBuffer; + static std::mutex sBufferMutex; + static int64_t sLogStartIndex; + static int64_t sLogEndIndex; + static bool hasPrefix = false; + + void DebugWindow::startLogRecording() + { + sLogCircularBuffer.resize(Settings::general().mLogBufferSize); + Debug::setLogListener([](Debug::Level level, std::string_view prefix, std::string_view msg) { + if (sLogCircularBuffer.empty()) + return; // Log viewer is disabled. + std::string_view color; + switch (level) + { + case Debug::Error: + color = "#FF0000"; + break; + case Debug::Warning: + color = "#FFFF00"; + break; + case Debug::Info: + color = "#FFFFFF"; + break; + case Debug::Verbose: + case Debug::Debug: + color = "#666666"; + break; + default: + color = "#FFFFFF"; + } + bool bufferOverflow = false; + std::lock_guard lock(sBufferMutex); + const int64_t bufSize = sLogCircularBuffer.size(); + auto addChar = [&](char c) { + sLogCircularBuffer[sLogEndIndex++] = c; + if (sLogEndIndex == bufSize) + sLogEndIndex = 0; + bufferOverflow = bufferOverflow || sLogEndIndex == sLogStartIndex; + }; + auto addShieldedStr = [&](std::string_view s) { + for (char c : s) + { + addChar(c); + if (c == '#') + addChar(c); + if (c == '\n') + hasPrefix = false; + } + }; + for (char c : color) + addChar(c); + if (!hasPrefix) + { + addShieldedStr(prefix); + hasPrefix = true; + } + addShieldedStr(msg); + if (bufferOverflow) + sLogStartIndex = (sLogEndIndex + 1) % bufSize; + }); } - void DebugWindow::onFrame(float dt) + void DebugWindow::updateLogView() { -#ifndef BT_NO_PROFILE - if (!isVisible()) + std::lock_guard lock(sBufferMutex); + + if (!mLogView || sLogCircularBuffer.empty() || sLogStartIndex == sLogEndIndex) return; + if (mLogView->isTextSelection()) + return; // Don't change text while player is trying to copy something - static float timer = 0; - timer -= dt; + std::string addition; + const int64_t bufSize = sLogCircularBuffer.size(); + { + if (sLogStartIndex < sLogEndIndex) + addition = std::string(sLogCircularBuffer.data() + sLogStartIndex, sLogEndIndex - sLogStartIndex); + else + { + addition = std::string(sLogCircularBuffer.data() + sLogStartIndex, bufSize - sLogStartIndex); + addition.append(sLogCircularBuffer.data(), sLogEndIndex); + } + sLogStartIndex = sLogEndIndex; + } + + size_t scrollPos = mLogView->getVScrollPosition(); + bool scrolledToTheEnd = scrollPos + 1 >= mLogView->getVScrollRange(); + int64_t newSizeEstimation = mLogView->getTextLength() + addition.size(); + if (newSizeEstimation > bufSize) + mLogView->eraseText(0, newSizeEstimation - bufSize); + mLogView->addText(addition); + if (scrolledToTheEnd && mLogView->getVScrollRange() > 0) + mLogView->setVScrollPosition(mLogView->getVScrollRange() - 1); + else + mLogView->setVScrollPosition(scrollPos); + } - if (timer > 0) + void DebugWindow::updateLuaProfile() + { + if (mLuaProfiler->isTextSelection()) return; - timer = 1; + size_t previousPos = mLuaProfiler->getVScrollPosition(); + mLuaProfiler->setCaption(MWBase::Environment::get().getLuaManager()->formatResourceUsageStats()); + mLuaProfiler->setVScrollPosition(std::min(previousPos, mLuaProfiler->getVScrollRange() - 1)); + } + + void DebugWindow::updateBulletProfile() + { +#ifndef BT_NO_PROFILE std::stringstream stream; bulletDumpAll(stream); @@ -120,8 +243,30 @@ namespace MWGui size_t previousPos = mBulletProfilerEdit->getVScrollPosition(); mBulletProfilerEdit->setCaption(stream.str()); - mBulletProfilerEdit->setVScrollPosition(std::min(previousPos, mBulletProfilerEdit->getVScrollRange()-1)); + mBulletProfilerEdit->setVScrollPosition(std::min(previousPos, mBulletProfilerEdit->getVScrollRange() - 1)); #endif } + void DebugWindow::onFrame(float dt) + { + static float timer = 0; + timer -= dt; + if (timer > 0 || !isVisible()) + return; + timer = 0.25; + + switch (mTabControl->getIndexSelected()) + { + case 0: + updateLogView(); + break; + case 1: + updateLuaProfile(); + break; + case 2: + updateBulletProfile(); + break; + default:; + } + } } diff --git a/apps/openmw/mwgui/debugwindow.hpp b/apps/openmw/mwgui/debugwindow.hpp index 33647c0789c..7e65353c12a 100644 --- a/apps/openmw/mwgui/debugwindow.hpp +++ b/apps/openmw/mwgui/debugwindow.hpp @@ -13,9 +13,16 @@ namespace MWGui void onFrame(float dt) override; + static void startLogRecording(); + private: - MyGUI::TabControl* mTabControl; + void updateLogView(); + void updateLuaProfile(); + void updateBulletProfile(); + MyGUI::TabControl* mTabControl; + MyGUI::EditBox* mLogView; + MyGUI::EditBox* mLuaProfiler; MyGUI::EditBox* mBulletProfilerEdit; }; diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 0fda54ebab5..56f69eb9066 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -1,27 +1,33 @@ #include "dialogue.hpp" +#include #include -#include #include #include -#include +#include +#include #include -#include +#include +#include #include +#include +#include +#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwbase/dialoguemanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/player.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" #include "bookpage.hpp" #include "textcolours.hpp" @@ -30,35 +36,21 @@ namespace MWGui { - - class ResponseCallback : public MWBase::DialogueManager::ResponseCallback + void ResponseCallback::addResponse(std::string_view title, std::string_view text) { - public: - ResponseCallback(DialogueWindow* win, bool needMargin=true) - : mWindow(win) - , mNeedMargin(needMargin) - { - - } - - void addResponse(const std::string& title, const std::string& text) override - { - mWindow->addResponse(title, text, mNeedMargin); - } - - void updateTopics() - { - mWindow->updateTopics(); - } + mWindow->addResponse(title, text, mNeedMargin); + } - private: - DialogueWindow* mWindow; - bool mNeedMargin; - }; + void ResponseCallback::updateTopics() const + { + mWindow->updateTopics(); + } - PersuasionDialog::PersuasionDialog(ResponseCallback* callback) + PersuasionDialog::PersuasionDialog(std::unique_ptr callback) : WindowModal("openmw_persuasion_dialog.layout") - , mCallback(callback) + , mCallback(std::move(callback)) + , mInitialGoldLabelWidth(0) + , mInitialMainWidgetWidth(0) { getWidget(mCancelButton, "CancelButton"); getWidget(mAdmireButton, "AdmireButton"); @@ -68,6 +60,26 @@ namespace MWGui getWidget(mBribe100Button, "Bribe100Button"); getWidget(mBribe1000Button, "Bribe1000Button"); getWidget(mGoldLabel, "GoldLabel"); + getWidget(mActionsBox, "ActionsBox"); + + int totalHeight = 3; + adjustAction(mAdmireButton, totalHeight); + adjustAction(mIntimidateButton, totalHeight); + adjustAction(mTauntButton, totalHeight); + adjustAction(mBribe10Button, totalHeight); + adjustAction(mBribe100Button, totalHeight); + adjustAction(mBribe1000Button, totalHeight); + totalHeight += 3; + + int diff = totalHeight - mActionsBox->getSize().height; + if (diff > 0) + { + auto mainWidgetSize = mMainWidget->getSize(); + mMainWidget->setSize(mainWidgetSize.width, mainWidgetSize.height + diff); + } + + mInitialGoldLabelWidth = mActionsBox->getSize().width - mCancelButton->getSize().width - 8; + mInitialMainWidgetWidth = mMainWidget->getSize().width; mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onCancel); mAdmireButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); @@ -78,17 +90,28 @@ namespace MWGui mBribe1000Button->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); } - void PersuasionDialog::onCancel(MyGUI::Widget *sender) + void PersuasionDialog::adjustAction(MyGUI::Widget* action, int& totalHeight) + { + const int lineHeight = Settings::gui().mFontSize + 2; + auto currentCoords = action->getCoord(); + action->setCoord(currentCoords.left, totalHeight, currentCoords.width, lineHeight); + totalHeight += lineHeight; + } + + void PersuasionDialog::onCancel(MyGUI::Widget* sender) { setVisible(false); } - void PersuasionDialog::onPersuade(MyGUI::Widget *sender) + void PersuasionDialog::onPersuade(MyGUI::Widget* sender) { MWBase::MechanicsManager::PersuasionType type; - if (sender == mAdmireButton) type = MWBase::MechanicsManager::PT_Admire; - else if (sender == mIntimidateButton) type = MWBase::MechanicsManager::PT_Intimidate; - else if (sender == mTauntButton) type = MWBase::MechanicsManager::PT_Taunt; + if (sender == mAdmireButton) + type = MWBase::MechanicsManager::PT_Admire; + else if (sender == mIntimidateButton) + type = MWBase::MechanicsManager::PT_Intimidate; + else if (sender == mTauntButton) + type = MWBase::MechanicsManager::PT_Taunt; else if (sender == mBribe10Button) type = MWBase::MechanicsManager::PT_Bribe10; else if (sender == mBribe100Button) @@ -109,11 +132,18 @@ namespace MWGui MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - mBribe10Button->setEnabled (playerGold >= 10); - mBribe100Button->setEnabled (playerGold >= 100); - mBribe1000Button->setEnabled (playerGold >= 1000); + mBribe10Button->setEnabled(playerGold >= 10); + mBribe100Button->setEnabled(playerGold >= 100); + mBribe1000Button->setEnabled(playerGold >= 1000); mGoldLabel->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); + + int diff = mGoldLabel->getRequestedSize().width - mInitialGoldLabelWidth; + if (diff > 0) + mMainWidget->setSize(mInitialMainWidgetWidth + diff, mMainWidget->getSize().height); + else + mMainWidget->setSize(mInitialMainWidgetWidth, mMainWidget->getSize().height); + WindowModal::onOpen(); } @@ -124,21 +154,24 @@ namespace MWGui // -------------------------------------------------------------------------------------------------- - Response::Response(const std::string &text, const std::string &title, bool needMargin) - : mTitle(title), mNeedMargin(needMargin) + Response::Response(std::string_view text, std::string_view title, bool needMargin) + : mTitle(title) + , mNeedMargin(needMargin) { mText = text; } - void Response::write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const + void Response::write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, + std::map>& topicLinks) const { typesetter->sectionBreak(mNeedMargin ? 9 : 0); + auto windowManager = MWBase::Environment::get().getWindowManager(); - if (mTitle != "") + if (!mTitle.empty()) { - const MyGUI::Colour& headerColour = MWBase::Environment::get().getWindowManager()->getTextColours().header; - BookTypesetter::Style* title = typesetter->createStyle("", headerColour, false); - typesetter->write(title, to_utf8_span(mTitle.c_str())); + const MyGUI::Colour& headerColour = windowManager->getTextColours().header; + BookTypesetter::Style* title = typesetter->createStyle({}, headerColour, false); + typesetter->write(title, to_utf8_span(mTitle)); typesetter->sectionBreak(); } @@ -149,7 +182,7 @@ namespace MWGui std::string text = mText; size_t pos_end = std::string::npos; - for(;;) + for (;;) { size_t pos_begin = text.find('@'); if (pos_begin != std::string::npos) @@ -160,36 +193,37 @@ namespace MWGui std::string link = text.substr(pos_begin + 1, pos_end - pos_begin - 1); const char specialPseudoAsteriskCharacter = 127; std::replace(link.begin(), link.end(), specialPseudoAsteriskCharacter, '*'); - std::string topicName = MWBase::Environment::get().getWindowManager()-> - getTranslationDataStorage().topicStandardForm(link); + std::string topicName + = Misc::StringUtils::lowerCase(windowManager->getTranslationDataStorage().topicStandardForm(link)); - std::string displayName = link; - while (displayName[displayName.size()-1] == '*') - displayName.erase(displayName.size()-1, 1); + std::string displayName = std::move(link); + while (displayName[displayName.size() - 1] == '*') + displayName.erase(displayName.size() - 1, 1); - text.replace(pos_begin, pos_end+1-pos_begin, displayName); + text.replace(pos_begin, pos_end + 1 - pos_begin, displayName); - if (topicLinks.find(Misc::StringUtils::lowerCase(topicName)) != topicLinks.end()) - hyperLinks[std::make_pair(pos_begin, pos_begin+displayName.size())] = intptr_t(topicLinks[Misc::StringUtils::lowerCase(topicName)]); + if (topicLinks.find(topicName) != topicLinks.end()) + hyperLinks[std::make_pair(pos_begin, pos_begin + displayName.size())] + = intptr_t(topicLinks[topicName].get()); } else break; } - typesetter->addContent(to_utf8_span(text.c_str())); + typesetter->addContent(to_utf8_span(text)); - if (hyperLinks.size() && MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) + if (hyperLinks.size() + && MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) { const TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); - BookTypesetter::Style* style = typesetter->createStyle("", textColours.normal, false); + BookTypesetter::Style* style = typesetter->createStyle({}, textColours.normal, false); size_t formatted = 0; // points to the first character that is not laid out yet for (auto& hyperLink : hyperLinks) { intptr_t topicId = hyperLink.second; - BookTypesetter::Style* hotStyle = typesetter->createHotStyle (style, textColours.link, - textColours.linkOver, textColours.linkPressed, - topicId); + BookTypesetter::Style* hotStyle = typesetter->createHotStyle( + style, textColours.link, textColours.linkOver, textColours.linkPressed, topicId); if (formatted < hyperLink.first.first) typesetter->write(style, formatted, hyperLink.first.first); typesetter->write(hotStyle, hyperLink.first.first, hyperLink.first.second); @@ -203,18 +237,18 @@ namespace MWGui std::vector matches; keywordSearch->highlightKeywords(text.begin(), text.end(), matches); - std::string::const_iterator i = text.begin (); + std::string::const_iterator i = text.begin(); for (KeywordSearchT::Match& match : matches) { if (i != match.mBeg) - addTopicLink (typesetter, 0, i - text.begin (), match.mBeg - text.begin ()); + addTopicLink(typesetter, 0, i - text.begin(), match.mBeg - text.begin()); - addTopicLink (typesetter, match.mValue, match.mBeg - text.begin (), match.mEnd - text.begin ()); + addTopicLink(typesetter, match.mValue, match.mBeg - text.begin(), match.mEnd - text.begin()); i = match.mEnd; } - if (i != text.end ()) - addTopicLink (typesetter, 0, i - text.begin (), text.size ()); + if (i != text.end()) + addTopicLink(std::move(typesetter), 0, i - text.begin(), text.size()); } } @@ -222,44 +256,45 @@ namespace MWGui { const TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); - BookTypesetter::Style* style = typesetter->createStyle("", textColours.normal, false); - + BookTypesetter::Style* style = typesetter->createStyle({}, textColours.normal, false); if (topicId) - style = typesetter->createHotStyle (style, textColours.link, textColours.linkOver, textColours.linkPressed, topicId); - typesetter->write (style, begin, end); + style = typesetter->createHotStyle( + style, textColours.link, textColours.linkOver, textColours.linkPressed, topicId); + typesetter->write(style, begin, end); } - Message::Message(const std::string& text) + Message::Message(std::string_view text) { mText = text; } - void Message::write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const + void Message::write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, + std::map>& topicLinks) const { const MyGUI::Colour& textColour = MWBase::Environment::get().getWindowManager()->getTextColours().notify; - BookTypesetter::Style* title = typesetter->createStyle("", textColour, false); + BookTypesetter::Style* title = typesetter->createStyle({}, textColour, false); typesetter->sectionBreak(9); - typesetter->write(title, to_utf8_span(mText.c_str())); + typesetter->write(title, to_utf8_span(mText)); } // -------------------------------------------------------------------------------------------------- void Choice::activated() { - MWBase::Environment::get().getWindowManager()->playSound("Menu Click"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); eventChoiceActivated(mChoiceId); } void Topic::activated() { - MWBase::Environment::get().getWindowManager()->playSound("Menu Click"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); eventTopicActivated(mTopicId); } void Goodbye::activated() { - MWBase::Environment::get().getWindowManager()->playSound("Menu Click"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); eventActivated(); } @@ -269,19 +304,19 @@ namespace MWGui : WindowBase("openmw_dialogue_window.layout") , mIsCompanion(false) , mGoodbye(false) - , mPersuasionDialog(new ResponseCallback(this)) - , mCallback(new ResponseCallback(this)) - , mGreetingCallback(new ResponseCallback(this, false)) + , mPersuasionDialog(std::make_unique(this)) + , mCallback(std::make_unique(this)) + , mGreetingCallback(std::make_unique(this, false)) { // Centre dialog center(); mPersuasionDialog.setVisible(false); - //History view + // History view getWidget(mHistory, "History"); - //Topics list + // Topics list getWidget(mTopicsList, "TopicsList"); mTopicsList->eventItemSelected += MyGUI::newDelegate(this, &DialogueWindow::onSelectListItem); @@ -289,32 +324,23 @@ namespace MWGui mGoodbyeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &DialogueWindow::onByeClicked); getWidget(mDispositionBar, "Disposition"); - getWidget(mDispositionText,"DispositionText"); + getWidget(mDispositionText, "DispositionText"); getWidget(mScrollBar, "VScroll"); mScrollBar->eventScrollChangePosition += MyGUI::newDelegate(this, &DialogueWindow::onScrollbarMoved); mHistory->eventMouseWheel += MyGUI::newDelegate(this, &DialogueWindow::onMouseWheel); - BookPage::ClickCallback callback = std::bind (&DialogueWindow::notifyLinkClicked, this, std::placeholders::_1); - mHistory->adviseLinkClicked(callback); + BookPage::ClickCallback callback = [this](TypesetBook::InteractiveId link) { notifyLinkClicked(link); }; + mHistory->adviseLinkClicked(std::move(callback)); - mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &DialogueWindow::onWindowResize); - } - - DialogueWindow::~DialogueWindow() - { - deleteLater(); - for (Link* link : mLinks) - delete link; - for (const auto& link : mTopicLinks) - delete link.second; - for (auto history : mHistoryContents) - delete history; + mMainWidget->castType()->eventWindowChangeCoord + += MyGUI::newDelegate(this, &DialogueWindow::onWindowResize); } void DialogueWindow::onTradeComplete() { - addResponse("", MyGUI::LanguageManager::getInstance().replaceTags("#{sBarterDialog5}")); + MyGUI::UString message = MyGUI::LanguageManager::getInstance().replaceTags("#{sBarterDialog5}"); + addResponse({}, message); } bool DialogueWindow::exit() @@ -335,11 +361,11 @@ namespace MWGui void DialogueWindow::onWindowResize(MyGUI::Window* _sender) { // if the window has only been moved, not resized, we don't need to update - if (mCurrentWindowSize == _sender->getSize()) return; + if (mCurrentWindowSize == _sender->getSize()) + return; - mTopicsList->adjustSize(); + redrawTopicsList(); updateHistory(); - updateTopicFormat(); mCurrentWindowSize = _sender->getSize(); } @@ -347,8 +373,8 @@ namespace MWGui { if (!mScrollBar->getVisible()) return; - mScrollBar->setScrollPosition(std::min(static_cast(mScrollBar->getScrollRange()-1), - std::max(0, static_cast(mScrollBar->getScrollPosition() - _rel*0.3)))); + mScrollBar->setScrollPosition( + std::clamp(mScrollBar->getScrollPosition() - _rel * 0.3, 0, mScrollBar->getScrollRange() - 1)); onScrollbarMoved(mScrollBar, mScrollBar->getScrollPosition()); } @@ -367,21 +393,22 @@ namespace MWGui if (mGoodbye || dialogueManager->isInChoice()) return; - const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); - - const std::string sPersuasion = gmst.find("sPersuasion")->mValue.getString(); - const std::string sCompanionShare = gmst.find("sCompanionShare")->mValue.getString(); - const std::string sBarter = gmst.find("sBarter")->mValue.getString(); - const std::string sSpells = gmst.find("sSpells")->mValue.getString(); - const std::string sTravel = gmst.find("sTravel")->mValue.getString(); - const std::string sSpellMakingMenuTitle = gmst.find("sSpellMakingMenuTitle")->mValue.getString(); - const std::string sEnchanting = gmst.find("sEnchanting")->mValue.getString(); - const std::string sServiceTrainingTitle = gmst.find("sServiceTrainingTitle")->mValue.getString(); - const std::string sRepair = gmst.find("sRepair")->mValue.getString(); - - if (topic != sPersuasion && topic != sCompanionShare && topic != sBarter - && topic != sSpells && topic != sTravel && topic != sSpellMakingMenuTitle - && topic != sEnchanting && topic != sServiceTrainingTitle && topic != sRepair) + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); + + const std::string& sPersuasion = gmst.find("sPersuasion")->mValue.getString(); + const std::string& sCompanionShare = gmst.find("sCompanionShare")->mValue.getString(); + const std::string& sBarter = gmst.find("sBarter")->mValue.getString(); + const std::string& sSpells = gmst.find("sSpells")->mValue.getString(); + const std::string& sTravel = gmst.find("sTravel")->mValue.getString(); + const std::string& sSpellMakingMenuTitle = gmst.find("sSpellMakingMenuTitle")->mValue.getString(); + const std::string& sEnchanting = gmst.find("sEnchanting")->mValue.getString(); + const std::string& sServiceTrainingTitle = gmst.find("sServiceTrainingTitle")->mValue.getString(); + const std::string& sRepair = gmst.find("sRepair")->mValue.getString(); + + if (topic != sPersuasion && topic != sCompanionShare && topic != sBarter && topic != sSpells && topic != sTravel + && topic != sSpellMakingMenuTitle && topic != sEnchanting && topic != sServiceTrainingTitle + && topic != sRepair) { onTopicActivated(topic); if (mGoodbyeButton->getEnabled()) @@ -393,19 +420,26 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Companion, mPtr); else if (!dialogueManager->checkServiceRefused(mCallback.get())) { - if (topic == sBarter && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Barter)) + if (topic == sBarter + && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Barter)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Barter, mPtr); - else if (topic == sSpells && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Spells)) + else if (topic == sSpells + && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Spells)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_SpellBuying, mPtr); - else if (topic == sTravel && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Travel)) + else if (topic == sTravel + && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Travel)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Travel, mPtr); - else if (topic == sSpellMakingMenuTitle && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Spellmaking)) + else if (topic == sSpellMakingMenuTitle + && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Spellmaking)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_SpellCreation, mPtr); - else if (topic == sEnchanting && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Enchanting)) + else if (topic == sEnchanting + && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Enchanting)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Enchanting, mPtr); - else if (topic == sServiceTrainingTitle && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Training)) + else if (topic == sServiceTrainingTitle + && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Training)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Training, mPtr); - else if (topic == sRepair && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Repair)) + else if (topic == sRepair + && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Repair)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_MerchantRepair, mPtr); } else @@ -414,7 +448,7 @@ namespace MWGui void DialogueWindow::setPtr(const MWWorld::Ptr& actor) { - if (!actor.getClass().isActor()) + if (actor.isEmpty() || !actor.getClass().isActor()) { Log(Debug::Warning) << "Warning: can not talk with non-actor object."; return; @@ -426,8 +460,9 @@ namespace MWGui // The history is not reset here mKeywords.clear(); mTopicsList->clear(); - for (Link* link : mLinks) - mDeleteLater.push_back(link); // Links are not deleted right away to prevent issues with event handlers + for (auto& link : mLinks) + mDeleteLater.push_back( + std::move(link)); // Links are not deleted right away to prevent issues with event handlers mLinks.clear(); } @@ -459,8 +494,12 @@ namespace MWGui void DialogueWindow::restock() { - MWMechanics::CreatureStats &sellerStats = mPtr.getClass().getCreatureStats(mPtr); - float delay = MWBase::Environment::get().getWorld()->getStore().get().find("fBarterGoldResetDelay")->mValue.getFloat(); + MWMechanics::CreatureStats& sellerStats = mPtr.getClass().getCreatureStats(mPtr); + float delay = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fBarterGoldResetDelay") + ->mValue.getFloat(); // Gold is restocked every 24h if (MWBase::Environment::get().getWorld()->getTimeStamp() >= sellerStats.getLastRestockTime() + delay) @@ -473,8 +512,6 @@ namespace MWGui void DialogueWindow::deleteLater() { - for (Link* link : mDeleteLater) - delete link; mDeleteLater.clear(); } @@ -483,12 +520,10 @@ namespace MWGui if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Dialogue)) return; // Reset history - for (DialogueText* text : mHistoryContents) - delete text; mHistoryContents.clear(); } - bool DialogueWindow::setKeywords(std::list keyWords) + bool DialogueWindow::setKeywords(const std::list& keyWords) { if (mKeywords == keyWords && isCompanion() == mIsCompanion) return false; @@ -498,23 +533,32 @@ namespace MWGui return true; } + void DialogueWindow::redrawTopicsList() + { + mTopicsList->adjustSize(); + + // The topics list has been regenerated so topic formatting needs to be updated + updateTopicFormat(); + } + void DialogueWindow::updateTopicsPane() { mTopicsList->clear(); for (auto& linkPair : mTopicLinks) - mDeleteLater.push_back(linkPair.second); + mDeleteLater.push_back(std::move(linkPair.second)); mTopicLinks.clear(); mKeywordSearch.clear(); int services = mPtr.getClass().getServices(mPtr); - bool travel = (mPtr.getTypeName() == typeid(ESM::NPC).name() && !mPtr.get()->mBase->getTransport().empty()) - || (mPtr.getTypeName() == typeid(ESM::Creature).name() && !mPtr.get()->mBase->getTransport().empty()); + bool travel = (mPtr.getType() == ESM::NPC::sRecordId && !mPtr.get()->mBase->getTransport().empty()) + || (mPtr.getType() == ESM::Creature::sRecordId + && !mPtr.get()->mBase->getTransport().empty()); - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); - if (mPtr.getTypeName() == typeid(ESM::NPC).name()) + if (mPtr.getType() == ESM::NPC::sRecordId) mTopicsList->addItem(gmst.find("sPersuasion")->mValue.getString()); if (services & ESM::NPC::AllItems) @@ -544,44 +588,40 @@ namespace MWGui if (mTopicsList->getItemCount() > 0) mTopicsList->addSeparator(); - - for(const auto& keyword : mKeywords) + for (const auto& keyword : mKeywords) { std::string topicId = Misc::StringUtils::lowerCase(keyword); mTopicsList->addItem(keyword); - Topic* t = new Topic(keyword); + auto t = std::make_unique(keyword); + mKeywordSearch.seed(topicId, intptr_t(t.get())); t->eventTopicActivated += MyGUI::newDelegate(this, &DialogueWindow::onTopicActivated); - mTopicLinks[topicId] = t; - - mKeywordSearch.seed(topicId, intptr_t(t)); + mTopicLinks[topicId] = std::move(t); } - mTopicsList->adjustSize(); + redrawTopicsList(); updateHistory(); - // The topics list has been regenerated so topic formatting needs to be updated - updateTopicFormat(); } void DialogueWindow::updateHistory(bool scrollbar) { if (!scrollbar && mScrollBar->getVisible()) { - mHistory->setSize(mHistory->getSize()+MyGUI::IntSize(mScrollBar->getWidth(),0)); + mHistory->setSize(mHistory->getSize() + MyGUI::IntSize(mScrollBar->getWidth(), 0)); mScrollBar->setVisible(false); } if (scrollbar && !mScrollBar->getVisible()) { - mHistory->setSize(mHistory->getSize()-MyGUI::IntSize(mScrollBar->getWidth(),0)); + mHistory->setSize(mHistory->getSize() - MyGUI::IntSize(mScrollBar->getWidth(), 0)); mScrollBar->setVisible(true); } - BookTypesetter::Ptr typesetter = BookTypesetter::create (mHistory->getWidth(), std::numeric_limits::max()); + BookTypesetter::Ptr typesetter = BookTypesetter::create(mHistory->getWidth(), std::numeric_limits::max()); - for (DialogueText* text : mHistoryContents) + for (const auto& text : mHistoryContents) text->write(typesetter, &mKeywordSearch, mTopicLinks); - BookTypesetter::Style* body = typesetter->createStyle("", MyGUI::Colour::White, false); + BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::White, false); typesetter->sectionBreak(9); // choices @@ -589,29 +629,33 @@ namespace MWGui mChoices = MWBase::Environment::get().getDialogueManager()->getChoices(); for (std::pair& choice : mChoices) { - Choice* link = new Choice(choice.second); + auto link = std::make_unique(choice.second); link->eventChoiceActivated += MyGUI::newDelegate(this, &DialogueWindow::onChoiceActivated); - mLinks.push_back(link); + auto interactiveId = TypesetBook::InteractiveId(link.get()); + mLinks.push_back(std::move(link)); typesetter->lineBreak(); - BookTypesetter::Style* questionStyle = typesetter->createHotStyle(body, textColours.answer, textColours.answerOver, - textColours.answerPressed, - TypesetBook::InteractiveId(link)); - typesetter->write(questionStyle, to_utf8_span(choice.first.c_str())); + BookTypesetter::Style* questionStyle = typesetter->createHotStyle( + body, textColours.answer, textColours.answerOver, textColours.answerPressed, interactiveId); + typesetter->write(questionStyle, to_utf8_span(choice.first)); } mGoodbye = MWBase::Environment::get().getDialogueManager()->isGoodbye(); if (mGoodbye) { - Goodbye* link = new Goodbye(); + auto link = std::make_unique(); link->eventActivated += MyGUI::newDelegate(this, &DialogueWindow::onGoodbyeActivated); - mLinks.push_back(link); - std::string goodbye = MWBase::Environment::get().getWorld()->getStore().get().find("sGoodbye")->mValue.getString(); - BookTypesetter::Style* questionStyle = typesetter->createHotStyle(body, textColours.answer, textColours.answerOver, - textColours.answerPressed, - TypesetBook::InteractiveId(link)); + auto interactiveId = TypesetBook::InteractiveId(link.get()); + mLinks.push_back(std::move(link)); + const std::string& goodbye = MWBase::Environment::get() + .getESMStore() + ->get() + .find("sGoodbye") + ->mValue.getString(); + BookTypesetter::Style* questionStyle = typesetter->createHotStyle( + body, textColours.answer, textColours.answerOver, textColours.answerPressed, interactiveId); typesetter->lineBreak(); - typesetter->write(questionStyle, to_utf8_span(goodbye.c_str())); + typesetter->write(questionStyle, to_utf8_span(goodbye)); } TypesetBook::Ptr book = typesetter->complete(); @@ -624,9 +668,10 @@ namespace MWGui mHistory->setSize(MyGUI::IntSize(mHistory->getWidth(), book->getSize().second)); size_t range = book->getSize().second - viewHeight; mScrollBar->setScrollRange(range); - mScrollBar->setScrollPosition(range-1); - mScrollBar->setTrackSize(static_cast(viewHeight / static_cast(book->getSize().second) * mScrollBar->getLineSize())); - onScrollbarMoved(mScrollBar, range-1); + mScrollBar->setScrollPosition(range - 1); + mScrollBar->setTrackSize( + static_cast(viewHeight / static_cast(book->getSize().second) * mScrollBar->getLineSize())); + onScrollbarMoved(mScrollBar, range - 1); } else { @@ -644,12 +689,12 @@ namespace MWGui mTopicsList->setEnabled(topicsEnabled); } - void DialogueWindow::notifyLinkClicked (TypesetBook::InteractiveId link) + void DialogueWindow::notifyLinkClicked(TypesetBook::InteractiveId link) { reinterpret_cast(link)->activated(); } - void DialogueWindow::onTopicActivated(const std::string &topicId) + void DialogueWindow::onTopicActivated(const std::string& topicId) { if (mGoodbye) return; @@ -676,20 +721,20 @@ namespace MWGui resetReference(); } - void DialogueWindow::onScrollbarMoved(MyGUI::ScrollBar *sender, size_t pos) + void DialogueWindow::onScrollbarMoved(MyGUI::ScrollBar* sender, size_t pos) { mHistory->setPosition(0, static_cast(pos) * -1); } - void DialogueWindow::addResponse(const std::string &title, const std::string &text, bool needMargin) + void DialogueWindow::addResponse(std::string_view title, std::string_view text, bool needMargin) { - mHistoryContents.push_back(new Response(text, title, needMargin)); + mHistoryContents.push_back(std::make_unique(text, title, needMargin)); updateHistory(); } - void DialogueWindow::addMessageBox(const std::string& text) + void DialogueWindow::addMessageBox(std::string_view text) { - mHistoryContents.push_back(new Message(text)); + mHistoryContents.push_back(std::make_unique(text)); updateHistory(); } @@ -698,27 +743,30 @@ namespace MWGui bool dispositionVisible = false; if (!mPtr.isEmpty() && mPtr.getClass().isNpc()) { + // If actor was a witness to a crime which was payed off, + // restore original disposition immediately. + MWMechanics::NpcStats& npcStats = mPtr.getClass().getNpcStats(mPtr); + if (npcStats.getCrimeId() != -1 && npcStats.getCrimeDispositionModifier() != 0) + { + if (npcStats.getCrimeId() <= MWBase::Environment::get().getWorld()->getPlayer().getCrimeId()) + npcStats.setCrimeDispositionModifier(0); + } + dispositionVisible = true; mDispositionBar->setProgressRange(100); - mDispositionBar->setProgressPosition(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr)); - mDispositionText->setCaption(MyGUI::utility::toString(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr))+std::string("/100")); + mDispositionBar->setProgressPosition( + MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr)); + mDispositionText->setCaption( + MyGUI::utility::toString(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr)) + + std::string("/100")); } - bool dispositionWasVisible = mDispositionBar->getVisible(); - - if (dispositionVisible && !dispositionWasVisible) - { - mDispositionBar->setVisible(true); - int offset = mDispositionBar->getHeight()+5; - mTopicsList->setCoord(mTopicsList->getCoord() + MyGUI::IntCoord(0,offset,0,-offset)); - mTopicsList->adjustSize(); - } - else if (!dispositionVisible && dispositionWasVisible) + if (mDispositionBar->getVisible() != dispositionVisible) { - mDispositionBar->setVisible(false); - int offset = mDispositionBar->getHeight()+5; - mTopicsList->setCoord(mTopicsList->getCoord() - MyGUI::IntCoord(0,offset,0,-offset)); - mTopicsList->adjustSize(); + mDispositionBar->setVisible(dispositionVisible); + const int offset = (mDispositionBar->getHeight() + 5) * (dispositionVisible ? 1 : -1); + mTopicsList->setCoord(mTopicsList->getCoord() + MyGUI::IntCoord(0, offset, 0, -offset)); + redrawTopicsList(); } } @@ -737,27 +785,27 @@ namespace MWGui deleteLater(); if (mChoices != MWBase::Environment::get().getDialogueManager()->getChoices() - || mGoodbye != MWBase::Environment::get().getDialogueManager()->isGoodbye()) + || mGoodbye != MWBase::Environment::get().getDialogueManager()->isGoodbye()) updateHistory(); } void DialogueWindow::updateTopicFormat() { - if (!Settings::Manager::getBool("color topic enable", "GUI")) + if (!Settings::gui().mColorTopicEnable) return; - std::string specialColour = Settings::Manager::getString("color topic specific", "GUI"); - std::string oldColour = Settings::Manager::getString("color topic exhausted", "GUI"); + const MyGUI::Colour& specialColour = Settings::gui().mColorTopicSpecific; + const MyGUI::Colour& oldColour = Settings::gui().mColorTopicExhausted; for (const std::string& keyword : mKeywords) { - int flag = MWBase::Environment::get().getDialogueManager()->getTopicFlag(keyword); + int flag = MWBase::Environment::get().getDialogueManager()->getTopicFlag(ESM::RefId::stringRefId(keyword)); MyGUI::Button* button = mTopicsList->getItemWidget(keyword); - if (!specialColour.empty() && flag & MWBase::DialogueManager::TopicType::Specific) - button->getSubWidgetText()->setTextColour(MyGUI::Colour::parse(specialColour)); - else if (!oldColour.empty() && flag & MWBase::DialogueManager::TopicType::Exhausted) - button->getSubWidgetText()->setTextColour(MyGUI::Colour::parse(oldColour)); + if (flag & MWBase::DialogueManager::TopicType::Specific) + button->getSubWidgetText()->setTextColour(specialColour); + else if (flag & MWBase::DialogueManager::TopicType::Exhausted) + button->getSubWidgetText()->setTextColour(oldColour); } } @@ -779,7 +827,7 @@ namespace MWGui return false; return !actor.getClass().getScript(actor).empty() - && actor.getRefData().getLocals().getIntVar(actor.getClass().getScript(actor), "companion"); + && actor.getRefData().getLocals().getIntVar(actor.getClass().getScript(actor), "companion"); } } diff --git a/apps/openmw/mwgui/dialogue.hpp b/apps/openmw/mwgui/dialogue.hpp index ac6303e20ad..8a8b309401f 100644 --- a/apps/openmw/mwgui/dialogue.hpp +++ b/apps/openmw/mwgui/dialogue.hpp @@ -1,28 +1,49 @@ #ifndef MWGUI_DIALOGE_H #define MWGUI_DIALOGE_H -#include "windowbase.hpp" +#include + #include "referenceinterface.hpp" +#include "windowbase.hpp" #include "bookpage.hpp" +#include "../mwbase/dialoguemanager.hpp" #include "../mwdialogue/keywordsearch.hpp" #include namespace Gui { + class AutoSizedTextBox; class MWList; } namespace MWGui { - class ResponseCallback; + class DialogueWindow; + + class ResponseCallback : public MWBase::DialogueManager::ResponseCallback + { + DialogueWindow* mWindow; + bool mNeedMargin; + + public: + ResponseCallback(DialogueWindow* win, bool needMargin = true) + : mWindow(win) + , mNeedMargin(needMargin) + { + } + + void addResponse(std::string_view title, std::string_view text) override; + + void updateTopics() const; + }; class PersuasionDialog : public WindowModal { public: - PersuasionDialog(ResponseCallback* callback); + PersuasionDialog(std::unique_ptr callback); void onOpen() override; @@ -31,6 +52,9 @@ namespace MWGui private: std::unique_ptr mCallback; + int mInitialGoldLabelWidth; + int mInitialMainWidgetWidth; + MyGUI::Button* mCancelButton; MyGUI::Button* mAdmireButton; MyGUI::Button* mIntimidateButton; @@ -38,91 +62,101 @@ namespace MWGui MyGUI::Button* mBribe10Button; MyGUI::Button* mBribe100Button; MyGUI::Button* mBribe1000Button; - MyGUI::TextBox* mGoldLabel; + MyGUI::Widget* mActionsBox; + Gui::AutoSizedTextBox* mGoldLabel; - void onCancel (MyGUI::Widget* sender); - void onPersuade (MyGUI::Widget* sender); - }; + void adjustAction(MyGUI::Widget* action, int& totalHeight); + void onCancel(MyGUI::Widget* sender); + void onPersuade(MyGUI::Widget* sender); + }; struct Link { virtual ~Link() {} - virtual void activated () = 0; + virtual void activated() = 0; }; struct Topic : Link { - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_TopicId; + typedef MyGUI::delegates::MultiDelegate EventHandle_TopicId; EventHandle_TopicId eventTopicActivated; - Topic(const std::string& id) : mTopicId(id) {} + Topic(const std::string& id) + : mTopicId(id) + { + } std::string mTopicId; - void activated () override; + void activated() override; }; struct Choice : Link { - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_ChoiceId; + typedef MyGUI::delegates::MultiDelegate EventHandle_ChoiceId; EventHandle_ChoiceId eventChoiceActivated; - Choice(int id) : mChoiceId(id) {} + Choice(int id) + : mChoiceId(id) + { + } int mChoiceId; - void activated () override; + void activated() override; }; struct Goodbye : Link { - typedef MyGUI::delegates::CMultiDelegate0 Event_Activated; + typedef MyGUI::delegates::MultiDelegate<> Event_Activated; Event_Activated eventActivated; - void activated () override; + void activated() override; }; - typedef MWDialogue::KeywordSearch KeywordSearchT; + typedef MWDialogue::KeywordSearch KeywordSearchT; struct DialogueText { - virtual ~DialogueText() {} - virtual void write (BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const = 0; + virtual ~DialogueText() = default; + virtual void write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, + std::map>& topicLinks) const = 0; std::string mText; }; struct Response : DialogueText { - Response(const std::string& text, const std::string& title = "", bool needMargin = true); - void write (BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const override; - void addTopicLink (BookTypesetter::Ptr typesetter, intptr_t topicId, size_t begin, size_t end) const; + Response(std::string_view text, std::string_view title = {}, bool needMargin = true); + void write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, + std::map>& topicLinks) const override; + void addTopicLink(BookTypesetter::Ptr typesetter, intptr_t topicId, size_t begin, size_t end) const; std::string mTitle; bool mNeedMargin; }; struct Message : DialogueText { - Message(const std::string& text); - void write (BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const override; + Message(std::string_view text); + void write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, + std::map>& topicLinks) const override; }; - class DialogueWindow: public WindowBase, public ReferenceInterface + class DialogueWindow : public WindowBase, public ReferenceInterface { public: DialogueWindow(); - ~DialogueWindow(); void onTradeComplete(); bool exit() override; // Events - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; - void notifyLinkClicked (TypesetBook::InteractiveId link); + void notifyLinkClicked(TypesetBook::InteractiveId link); void setPtr(const MWWorld::Ptr& actor) override; /// @return true if stale keywords were updated successfully - bool setKeywords(std::list keyWord); + bool setKeywords(const std::list& keyWord); - void addResponse (const std::string& title, const std::string& text, bool needMargin = true); + void addResponse(std::string_view title, std::string_view text, bool needMargin = true); - void addMessageBox(const std::string& text); + void addMessageBox(std::string_view text); void onFrame(float dt) override; void clear() override { resetReference(); } @@ -131,6 +165,8 @@ namespace MWGui void onClose() override; + std::string_view getWindowIdForLua() const override { return "Dialogue"; } + protected: void updateTopicsPane(); bool isCompanion(const MWWorld::Ptr& actor); @@ -144,9 +180,9 @@ namespace MWGui void onChoiceActivated(int id); void onGoodbyeActivated(); - void onScrollbarMoved (MyGUI::ScrollBar* sender, size_t pos); + void onScrollbarMoved(MyGUI::ScrollBar* sender, size_t pos); - void updateHistory(bool scrollbar=false); + void updateHistory(bool scrollbar = false); void onReferenceUnavailable() override; @@ -154,26 +190,27 @@ namespace MWGui void updateDisposition(); void restock(); void deleteLater(); + void redrawTopicsList(); bool mIsCompanion; std::list mKeywords; - std::vector mHistoryContents; - std::vector > mChoices; + std::vector> mHistoryContents; + std::vector> mChoices; bool mGoodbye; - std::vector mLinks; - std::map mTopicLinks; + std::vector> mLinks; + std::map> mTopicLinks; - std::vector mDeleteLater; + std::vector> mDeleteLater; KeywordSearchT mKeywordSearch; BookPage* mHistory; - Gui::MWList* mTopicsList; + Gui::MWList* mTopicsList; MyGUI::ScrollBar* mScrollBar; MyGUI::ProgressBar* mDispositionBar; - MyGUI::TextBox* mDispositionText; + MyGUI::TextBox* mDispositionText; MyGUI::Button* mGoodbyeButton; PersuasionDialog mPersuasionDialog; diff --git a/apps/openmw/mwgui/draganddrop.cpp b/apps/openmw/mwgui/draganddrop.cpp index cfe60db5dde..0fa2cc4e210 100644 --- a/apps/openmw/mwgui/draganddrop.cpp +++ b/apps/openmw/mwgui/draganddrop.cpp @@ -1,142 +1,145 @@ #include "draganddrop.hpp" -#include #include +#include -#include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" -#include "sortfilteritemmodel.hpp" +#include "controllers.hpp" #include "inventorywindow.hpp" -#include "itemwidget.hpp" #include "itemview.hpp" -#include "controllers.hpp" +#include "itemwidget.hpp" +#include "sortfilteritemmodel.hpp" namespace MWGui { + DragAndDrop::DragAndDrop() + : mIsOnDragAndDrop(false) + , mDraggedWidget(nullptr) + , mSourceModel(nullptr) + , mSourceView(nullptr) + , mSourceSortModel(nullptr) + , mDraggedCount(0) + { + } -DragAndDrop::DragAndDrop() - : mIsOnDragAndDrop(false) - , mDraggedWidget(nullptr) - , mSourceModel(nullptr) - , mSourceView(nullptr) - , mSourceSortModel(nullptr) - , mDraggedCount(0) -{ -} - -void DragAndDrop::startDrag (int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count) -{ - mItem = sourceModel->getItem(index); - mDraggedCount = count; - mSourceModel = sourceModel; - mSourceView = sourceView; - mSourceSortModel = sortModel; - - // If picking up an item that isn't from the player's inventory, the item gets added to player inventory backend - // immediately, even though it's still floating beneath the mouse cursor. A bit counterintuitive, - // but this is how it works in vanilla, and not doing so would break quests (BM_beasts for instance). - ItemModel* playerModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getModel(); - if (mSourceModel != playerModel) + void DragAndDrop::startDrag( + int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count) { - MWWorld::Ptr item = mSourceModel->moveItem(mItem, mDraggedCount, playerModel); + mItem = sourceModel->getItem(index); + mDraggedCount = count; + mSourceModel = sourceModel; + mSourceView = sourceView; + mSourceSortModel = sortModel; + + // If picking up an item that isn't from the player's inventory, the item gets added to player inventory backend + // immediately, even though it's still floating beneath the mouse cursor. A bit counterintuitive, + // but this is how it works in vanilla, and not doing so would break quests (BM_beasts for instance). + ItemModel* playerModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getModel(); + if (mSourceModel != playerModel) + { + MWWorld::Ptr item = mSourceModel->moveItem(mItem, mDraggedCount, playerModel); - playerModel->update(); + playerModel->update(); - ItemModel::ModelIndex newIndex = -1; - for (unsigned int i=0; igetItemCount(); ++i) - { - if (playerModel->getItem(i).mBase == item) + ItemModel::ModelIndex newIndex = -1; + for (size_t i = 0; i < playerModel->getItemCount(); ++i) { - newIndex = i; - break; + if (playerModel->getItem(i).mBase == item) + { + newIndex = i; + break; + } } + mItem = playerModel->getItem(newIndex); + mSourceModel = playerModel; + + SortFilterItemModel* playerFilterModel + = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getSortFilterModel(); + mSourceSortModel = playerFilterModel; } - mItem = playerModel->getItem(newIndex); - mSourceModel = playerModel; - SortFilterItemModel* playerFilterModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getSortFilterModel(); - mSourceSortModel = playerFilterModel; - } + const ESM::RefId& sound = mItem.mBase.getClass().getUpSoundId(mItem.mBase); + MWBase::Environment::get().getWindowManager()->playSound(sound); - std::string sound = mItem.mBase.getClass().getUpSoundId(mItem.mBase); - MWBase::Environment::get().getWindowManager()->playSound (sound); + if (mSourceSortModel) + { + mSourceSortModel->clearDragItems(); + mSourceSortModel->addDragItem(mItem.mBase, count); + } - if (mSourceSortModel) - { - mSourceSortModel->clearDragItems(); - mSourceSortModel->addDragItem(mItem.mBase, count); - } + ItemWidget* baseWidget = MyGUI::Gui::getInstance().createWidget( + "MW_ItemIcon", 0, 0, 42, 42, MyGUI::Align::Default, "DragAndDrop"); - ItemWidget* baseWidget = 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); - Controllers::ControllerFollowMouse* controller = - MyGUI::ControllerManager::getInstance().createItem(Controllers::ControllerFollowMouse::getClassTypeName()) - ->castType(); - MyGUI::ControllerManager::getInstance().addItem(baseWidget, controller); + mDraggedWidget = baseWidget; + baseWidget->setItem(mItem.mBase); + baseWidget->setNeedMouseFocus(false); + baseWidget->setCount(count); - mDraggedWidget = baseWidget; - baseWidget->setItem(mItem.mBase); - baseWidget->setNeedMouseFocus(false); - baseWidget->setCount(count); + sourceView->update(); - sourceView->update(); + MWBase::Environment::get().getWindowManager()->setDragDrop(true); - MWBase::Environment::get().getWindowManager()->setDragDrop(true); + mIsOnDragAndDrop = true; + } - mIsOnDragAndDrop = true; -} + void DragAndDrop::drop(ItemModel* targetModel, ItemView* targetView) + { + const ESM::RefId& sound = mItem.mBase.getClass().getDownSoundId(mItem.mBase); + MWBase::Environment::get().getWindowManager()->playSound(sound); -void DragAndDrop::drop(ItemModel *targetModel, ItemView *targetView) -{ - std::string sound = mItem.mBase.getClass().getDownSoundId(mItem.mBase); - MWBase::Environment::get().getWindowManager()->playSound(sound); + // We can't drop a conjured item to the ground; the target container should always be the source container + if (mItem.mFlags & ItemStack::Flag_Bound && targetModel != mSourceModel) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); + return; + } - // We can't drop a conjured item to the ground; the target container should always be the source container - if (mItem.mFlags & ItemStack::Flag_Bound && targetModel != mSourceModel) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); - return; - } + // If item is dropped where it was taken from, we don't need to do anything - + // otherwise, do the transfer + if (targetModel != mSourceModel) + { + mSourceModel->moveItem(mItem, mDraggedCount, targetModel); + } - // If item is dropped where it was taken from, we don't need to do anything - - // otherwise, do the transfer - if (targetModel != mSourceModel) - { - mSourceModel->moveItem(mItem, mDraggedCount, targetModel); - } + mSourceModel->update(); - mSourceModel->update(); + finish(); + if (targetView) + targetView->update(); - finish(); - if (targetView) - targetView->update(); + MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); - MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); + // We need to update the view since an other item could be auto-equipped. + mSourceView->update(); + } - // We need to update the view since an other item could be auto-equipped. - mSourceView->update(); -} + void DragAndDrop::onFrame() + { + if (mIsOnDragAndDrop && mItem.mBase.getCellRef().getCount() == 0) + finish(); + } -void DragAndDrop::onFrame() -{ - if (mIsOnDragAndDrop && mItem.mBase.getRefData().getCount() == 0) - finish(); -} + void DragAndDrop::finish() + { + mIsOnDragAndDrop = false; + mSourceSortModel->clearDragItems(); + // since mSourceView doesn't get updated in drag() + MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); -void DragAndDrop::finish() -{ - mIsOnDragAndDrop = false; - mSourceSortModel->clearDragItems(); - // since mSourceView doesn't get updated in drag() - MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); - - MyGUI::Gui::getInstance().destroyWidget(mDraggedWidget); - mDraggedWidget = nullptr; - MWBase::Environment::get().getWindowManager()->setDragDrop(false); -} + 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 dff8cd73c09..fab7f30d75b 100644 --- a/apps/openmw/mwgui/draganddrop.hpp +++ b/apps/openmw/mwgui/draganddrop.hpp @@ -27,8 +27,9 @@ namespace MWGui DragAndDrop(); - void startDrag (int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count); - void drop (ItemModel* targetModel, ItemView* targetView); + void startDrag( + int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count); + void drop(ItemModel* targetModel, ItemView* targetView); void onFrame(); void finish(); diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index d0d2118c6e0..af4a3e8ce3b 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -3,15 +3,19 @@ #include #include -#include #include +#include +#include +#include +#include #include -#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" -#include "../mwbase/world.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" @@ -27,11 +31,9 @@ namespace MWGui { - EnchantingDialog::EnchantingDialog() : WindowBase("openmw_enchanting_dialog.layout") , EffectEditorBase(EffectEditorBase::Enchanting) - , mItemSelectionDialog(nullptr) { getWidget(mName, "NameEdit"); getWidget(mCancelButton, "CancelButton"); @@ -59,18 +61,13 @@ namespace MWGui mName->eventEditSelectAccept += MyGUI::newDelegate(this, &EnchantingDialog::onAccept); } - EnchantingDialog::~EnchantingDialog() - { - delete mItemSelectionDialog; - } - void EnchantingDialog::onOpen() { center(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mName); } - void EnchantingDialog::setSoulGem(const MWWorld::Ptr &gem) + void EnchantingDialog::setSoulGem(const MWWorld::Ptr& gem) { if (gem.isEmpty()) { @@ -81,13 +78,13 @@ namespace MWGui else { mSoulBox->setItem(gem); - mSoulBox->setUserString ("ToolTipType", "ItemPtr"); + mSoulBox->setUserString("ToolTipType", "ItemPtr"); mSoulBox->setUserData(MWWorld::Ptr(gem)); mEnchanting.setSoulGem(gem); } } - void EnchantingDialog::setItem(const MWWorld::Ptr &item) + void EnchantingDialog::setItem(const MWWorld::Ptr& item) { if (item.isEmpty()) { @@ -97,9 +94,10 @@ namespace MWGui } else { - mName->setCaption(item.getClass().getName(item)); + std::string_view name = item.getClass().getName(item); + mName->setCaption(MyGUI::UString(name)); mItemBox->setItem(item); - mItemBox->setUserString ("ToolTipType", "ItemPtr"); + mItemBox->setUserString("ToolTipType", "ItemPtr"); mItemBox->setUserData(MWWorld::Ptr(item)); mEnchanting.setOldItem(item); } @@ -107,36 +105,47 @@ namespace MWGui void EnchantingDialog::updateLabels() { - mEnchantmentPoints->setCaption(std::to_string(static_cast(mEnchanting.getEnchantPoints(false))) + " / " + std::to_string(mEnchanting.getMaxEnchantValue())); + mEnchantmentPoints->setCaption(std::to_string(static_cast(mEnchanting.getEnchantPoints(false))) + " / " + + std::to_string(mEnchanting.getMaxEnchantValue())); mCharge->setCaption(std::to_string(mEnchanting.getGemCharge())); - mSuccessChance->setCaption(std::to_string(std::max(0, std::min(100, mEnchanting.getEnchantChance())))); + mSuccessChance->setCaption(std::to_string(std::clamp(mEnchanting.getEnchantChance(), 0, 100))); mCastCost->setCaption(std::to_string(mEnchanting.getEffectiveCastCost())); mPrice->setCaption(std::to_string(mEnchanting.getEnchantPrice())); - switch(mEnchanting.getCastStyle()) + switch (mEnchanting.getCastStyle()) { case ESM::Enchantment::CastOnce: - mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastOnce","Cast Once")); + mTypeButton->setCaption(MyGUI::UString( + MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastOnce", "Cast Once"))); setConstantEffect(false); break; case ESM::Enchantment::WhenStrikes: - mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastWhenStrikes", "When Strikes")); + mTypeButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + "sItemCastWhenStrikes", "When Strikes"))); setConstantEffect(false); break; case ESM::Enchantment::WhenUsed: - mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastWhenUsed", "When Used")); + mTypeButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + "sItemCastWhenUsed", "When Used"))); setConstantEffect(false); break; case ESM::Enchantment::ConstantEffect: - mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastConstant", "Cast Constant")); + mTypeButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( + "sItemCastConstant", "Cast Constant"))); setConstantEffect(true); break; } } - void EnchantingDialog::setPtr (const MWWorld::Ptr& ptr) + void EnchantingDialog::setPtr(const MWWorld::Ptr& ptr) { - mName->setCaption(""); + if (ptr.isEmpty() || (ptr.getType() != ESM::REC_MISC && !ptr.getClass().isActor())) + throw std::runtime_error("Invalid argument in EnchantingDialog::setPtr"); + + mName->setCaption({}); if (ptr.getClass().isActor()) { @@ -154,8 +163,7 @@ namespace MWGui mEnchanting.setSelfEnchanting(true); mEnchanting.setEnchanter(MWMechanics::getPlayer()); mBuyButton->setCaptionWithReplacing("#{sCreate}"); - bool enabled = Settings::Manager::getBool("show enchant chance","Game"); - mChanceLayout->setVisible(enabled); + mChanceLayout->setVisible(Settings::game().mShowEnchantChance); mPtr = MWMechanics::getPlayer(); setSoulGem(ptr); mPrice->setVisible(false); @@ -163,13 +171,13 @@ namespace MWGui } setItem(MWWorld::Ptr()); - startEditing (); + startEditing(); updateLabels(); } - void EnchantingDialog::onReferenceUnavailable () + void EnchantingDialog::onReferenceUnavailable() { - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Enchanting); resetReference(); } @@ -187,12 +195,11 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Enchanting); } - void EnchantingDialog::onSelectItem(MyGUI::Widget *sender) + void EnchantingDialog::onSelectItem(MyGUI::Widget* sender) { if (mEnchanting.getOldItem().isEmpty()) { - delete mItemSelectionDialog; - mItemSelectionDialog = new ItemSelectionDialog("#{sEnchantItems}"); + mItemSelectionDialog = std::make_unique("#{sEnchantItems}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &EnchantingDialog::onItemSelected); mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &EnchantingDialog::onItemCancel); mItemSelectionDialog->setVisible(true); @@ -226,9 +233,9 @@ namespace MWGui mItemSelectionDialog->setVisible(false); mEnchanting.setSoulGem(item); - if(mEnchanting.getGemCharge()==0) + if (mEnchanting.getGemCharge() == 0) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage32}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage32}"); return; } @@ -242,19 +249,18 @@ namespace MWGui mItemSelectionDialog->setVisible(false); } - void EnchantingDialog::onSelectSoul(MyGUI::Widget *sender) + void EnchantingDialog::onSelectSoul(MyGUI::Widget* sender) { if (mEnchanting.getGem().isEmpty()) { - delete mItemSelectionDialog; - mItemSelectionDialog = new ItemSelectionDialog("#{sSoulGemsWithSouls}"); + mItemSelectionDialog = std::make_unique("#{sSoulGemsWithSouls}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &EnchantingDialog::onSoulSelected); mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &EnchantingDialog::onSoulCancel); mItemSelectionDialog->setVisible(true); mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyChargedSoulstones); - //MWBase::Environment::get().getWindowManager()->messageBox("#{sInventorySelectNoSoul}"); + // MWBase::Environment::get().getWindowManager()->messageBox("#{sInventorySelectNoSoul}"); } else { @@ -265,9 +271,9 @@ namespace MWGui } } - void EnchantingDialog::notifyEffectsChanged () + void EnchantingDialog::notifyEffectsChanged() { - mEffectList.mList = mEffects; + mEffectList.populate(mEffects); mEnchanting.setEffect(mEffectList); updateLabels(); } @@ -279,7 +285,7 @@ namespace MWGui updateEffectsView(); } - void EnchantingDialog::onAccept(MyGUI::EditBox *sender) + void EnchantingDialog::onAccept(MyGUI::EditBox* sender) { onBuyButtonClicked(sender); @@ -291,31 +297,31 @@ namespace MWGui { if (mEffects.size() <= 0) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sEnchantmentMenu11}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sEnchantmentMenu11}"); return; } - if (mName->getCaption ().empty()) + if (mName->getCaption().empty()) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage10}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage10}"); return; } if (mEnchanting.soulEmpty()) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage52}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage52}"); return; } if (mEnchanting.itemEmpty()) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage11}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage11}"); return; } if (static_cast(mEnchanting.getEnchantPoints(false)) > mEnchanting.getMaxEnchantValue()) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage29}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage29}"); return; } @@ -326,44 +332,48 @@ namespace MWGui int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); if (mPtr != player && mEnchanting.getEnchantPrice() > playerGold) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage18}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage18}"); return; } // check if the player is attempting to use a soulstone or item that was stolen from this actor if (mPtr != player) { - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { MWWorld::Ptr item = (i == 0) ? mEnchanting.getOldItem() : mEnchanting.getGem(); - if (MWBase::Environment::get().getMechanicsManager()->isItemStolenFrom(item.getCellRef().getRefId(), mPtr)) + if (MWBase::Environment::get().getMechanicsManager()->isItemStolenFrom( + item.getCellRef().getRefId(), mPtr)) { - std::string msg = MWBase::Environment::get().getWorld()->getStore().get().find("sNotifyMessage49")->mValue.getString(); + std::string msg = MWBase::Environment::get() + .getESMStore() + ->get() + .find("sNotifyMessage49") + ->mValue.getString(); msg = Misc::StringUtils::format(msg, item.getClass().getName(item)); MWBase::Environment::get().getWindowManager()->messageBox(msg); - MWBase::Environment::get().getMechanicsManager()->confiscateStolenItemToOwner(player, item, mPtr, 1); + MWBase::Environment::get().getMechanicsManager()->confiscateStolenItemToOwner( + player, item, mPtr, 1); - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Enchanting); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); return; } } } - int result = mEnchanting.create(); - - if(result==1) + if (mEnchanting.create()) { - MWBase::Environment::get().getWindowManager()->playSound("enchant success"); - MWBase::Environment::get().getWindowManager()->messageBox ("#{sEnchantmentMenu12}"); - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("enchant success")); + MWBase::Environment::get().getWindowManager()->messageBox("#{sEnchantmentMenu12}"); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Enchanting); } else { - MWBase::Environment::get().getWindowManager()->playSound("enchant fail"); - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage34}"); - if (!mEnchanting.getGem().isEmpty() && !mEnchanting.getGem().getRefData().getCount()) + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("enchant fail")); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage34}"); + if (!mEnchanting.getGem().isEmpty() && !mEnchanting.getGem().getCellRef().getCount()) { setSoulGem(MWWorld::Ptr()); mEnchanting.nextCastStyle(); diff --git a/apps/openmw/mwgui/enchantingdialog.hpp b/apps/openmw/mwgui/enchantingdialog.hpp index 3989ae076b4..4c720a11fc4 100644 --- a/apps/openmw/mwgui/enchantingdialog.hpp +++ b/apps/openmw/mwgui/enchantingdialog.hpp @@ -1,31 +1,31 @@ #ifndef MWGUI_ENCHANTINGDIALOG_H #define MWGUI_ENCHANTINGDIALOG_H -#include "spellcreationdialog.hpp" +#include -#include "../mwbase/windowmanager.hpp" +#include "itemselection.hpp" +#include "spellcreationdialog.hpp" #include "../mwmechanics/enchanting.hpp" namespace MWGui { - class ItemSelectionDialog; class ItemWidget; class EnchantingDialog : public WindowBase, public ReferenceInterface, public EffectEditorBase { public: EnchantingDialog(); - virtual ~EnchantingDialog(); + virtual ~EnchantingDialog() = default; void onOpen() override; void onFrame(float dt) override { checkReferenceAvailable(); } void clear() override { resetReference(); } - void setSoulGem (const MWWorld::Ptr& gem); - void setItem (const MWWorld::Ptr& item); + void setSoulGem(const MWWorld::Ptr& gem); + void setItem(const MWWorld::Ptr& item); /// Actor Ptr: buy enchantment from this actor /// Soulgem Ptr: player self-enchant @@ -33,13 +33,15 @@ namespace MWGui void resetReference() override; + std::string_view getWindowIdForLua() const override { return "EnchantingDialog"; } + protected: void onReferenceUnavailable() override; void notifyEffectsChanged() override; void onCancelButtonClicked(MyGUI::Widget* sender); - void onSelectItem (MyGUI::Widget* sender); - void onSelectSoul (MyGUI::Widget* sender); + void onSelectItem(MyGUI::Widget* sender); + void onSelectSoul(MyGUI::Widget* sender); void onItemSelected(MWWorld::Ptr item); void onItemCancel(); @@ -50,7 +52,7 @@ namespace MWGui void onTypeButtonClicked(MyGUI::Widget* sender); void onAccept(MyGUI::EditBox* sender); - ItemSelectionDialog* mItemSelectionDialog; + std::unique_ptr mItemSelectionDialog; MyGUI::Widget* mChanceLayout; diff --git a/apps/openmw/mwgui/exposedwindow.cpp b/apps/openmw/mwgui/exposedwindow.cpp index 90cfa09d347..36b47d98f26 100644 --- a/apps/openmw/mwgui/exposedwindow.cpp +++ b/apps/openmw/mwgui/exposedwindow.cpp @@ -2,18 +2,18 @@ namespace MWGui { - MyGUI::VectorWidgetPtr Window::getSkinWidgetsByName (const std::string &name) + MyGUI::VectorWidgetPtr Window::getSkinWidgetsByName(const std::string& name) { - return MyGUI::Widget::getSkinWidgetsByName (name); + return MyGUI::Widget::getSkinWidgetsByName(name); } - MyGUI::Widget* Window::getSkinWidget(const std::string & _name, bool _throw) + MyGUI::Widget* Window::getSkinWidget(const std::string& _name, bool _throw) { - MyGUI::VectorWidgetPtr widgets = getSkinWidgetsByName (_name); + MyGUI::VectorWidgetPtr widgets = getSkinWidgetsByName(_name); if (widgets.empty()) { - MYGUI_ASSERT( ! _throw, "widget name '" << _name << "' not found in skin of layout '" << getName() << "'"); + MYGUI_ASSERT(!_throw, "widget name '" << _name << "' not found in skin of layout '" << getName() << "'"); return nullptr; } else diff --git a/apps/openmw/mwgui/exposedwindow.hpp b/apps/openmw/mwgui/exposedwindow.hpp index f1f5d3c2f8b..d192a3db013 100644 --- a/apps/openmw/mwgui/exposedwindow.hpp +++ b/apps/openmw/mwgui/exposedwindow.hpp @@ -14,13 +14,12 @@ namespace MWGui MYGUI_RTTI_DERIVED(Window) public: - MyGUI::VectorWidgetPtr getSkinWidgetsByName (const std::string &name); + MyGUI::VectorWidgetPtr getSkinWidgetsByName(const std::string& name); - MyGUI::Widget* getSkinWidget(const std::string & _name, bool _throw = true); + MyGUI::Widget* getSkinWidget(const std::string& _name, bool _throw = true); ///< Get a widget defined in the inner skin of this window. }; } #endif - diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index 156dc5b0d68..b2d94158970 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -1,33 +1,43 @@ #include "formatting.hpp" +#include +#include + +#include #include #include -#include #include -// correctBookartPath -#include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" - #include +#include #include -#include +#include +#include +#include +#include +#include +#include "../mwbase/environment.hpp" #include "../mwscript/interpretercontext.hpp" -namespace MWGui +namespace MWGui::Formatting { - namespace Formatting + /* BookTextParser */ + BookTextParser::BookTextParser(const std::string& text, bool shrinkTextAtLastTag) + : mIndex(0) + , mText(text) + , mIgnoreNewlineTags(true) + , mIgnoreLineEndings(true) + , mClosingTag(false) { - /* BookTextParser */ - BookTextParser::BookTextParser(const std::string & text) - : mIndex(0), mText(text), mIgnoreNewlineTags(true), mIgnoreLineEndings(true), mClosingTag(false) - { - MWScript::InterpreterContext interpreterContext(nullptr, MWWorld::Ptr()); // empty arguments, because there is no locals or actor - mText = Interpreter::fixDefinesBook(mText, interpreterContext); + MWScript::InterpreterContext interpreterContext( + nullptr, MWWorld::Ptr()); // empty arguments, because there is no locals or actor + mText = Interpreter::fixDefinesBook(mText, interpreterContext); - Misc::StringUtils::replaceAll(mText, "\r", ""); + Misc::StringUtils::replaceAll(mText, "\r", {}); + if (shrinkTextAtLastTag) + { // vanilla game does not show any text after the last EOL tag. const std::string lowerText = Misc::StringUtils::lowerCase(mText); size_t brIndex = lowerText.rfind("
      "); @@ -42,451 +52,509 @@ namespace MWGui else mPlainTextEnd = pIndex; } - - registerTag("br", Event_BrTag); - registerTag("p", Event_PTag); - registerTag("img", Event_ImgTag); - registerTag("div", Event_DivTag); - registerTag("font", Event_FontTag); } + else + mPlainTextEnd = mText.size(); + + registerTag("br", Event_BrTag); + registerTag("p", Event_PTag); + registerTag("img", Event_ImgTag); + registerTag("div", Event_DivTag); + registerTag("font", Event_FontTag); + } - void BookTextParser::registerTag(const std::string & tag, BookTextParser::Events type) - { - mTagTypes[tag] = type; - } + void BookTextParser::registerTag(const std::string& tag, BookTextParser::Events type) + { + mTagTypes[tag] = type; + } - std::string BookTextParser::getReadyText() const - { - return mReadyText; - } + std::string BookTextParser::getReadyText() const + { + return mReadyText; + } - BookTextParser::Events BookTextParser::next() + BookTextParser::Events BookTextParser::next() + { + while (mIndex < mText.size()) { - while (mIndex < mText.size()) + char ch = mText[mIndex]; + if (ch == '[') { - char ch = mText[mIndex]; - if (ch == '<') + constexpr std::string_view pageBreakTag = "[pagebreak]\n"; + if (std::string_view(mText.data() + mIndex, mText.size() - mIndex).starts_with(pageBreakTag)) { - const size_t tagStart = mIndex + 1; - const size_t tagEnd = mText.find('>', tagStart); - if (tagEnd == std::string::npos) - throw std::runtime_error("BookTextParser Error: Tag is not terminated"); - parseTag(mText.substr(tagStart, tagEnd - tagStart)); - mIndex = tagEnd; - - if (mTagTypes.find(mTag) != mTagTypes.end()) - { - Events type = mTagTypes.at(mTag); + mIndex += pageBreakTag.size(); + flushBuffer(); + mIgnoreNewlineTags = false; + return Event_PageBreak; + } + } + if (ch == '<') + { + const size_t tagStart = mIndex + 1; + const size_t tagEnd = mText.find('>', tagStart); + if (tagEnd == std::string::npos) + throw std::runtime_error("BookTextParser Error: Tag is not terminated"); + parseTag(mText.substr(tagStart, tagEnd - tagStart)); + mIndex = tagEnd; + + if (auto it = mTagTypes.find(mTag); it != mTagTypes.end()) + { + Events type = it->second; - if (type == Event_BrTag || type == Event_PTag) + if (type == Event_BrTag || type == Event_PTag) + { + if (!mIgnoreNewlineTags) { - if (!mIgnoreNewlineTags) + if (type == Event_BrTag) + mBuffer.push_back('\n'); + else { - if (type == Event_BrTag) - mBuffer.push_back('\n'); - else - { - mBuffer.append("\n\n"); - } + mBuffer.append("\n\n"); } - mIgnoreLineEndings = true; } - else + mIgnoreLineEndings = true; + if (type == Event_PTag && !mAttributes.empty()) flushBuffer(); - - if (type == Event_ImgTag) - { - mIgnoreNewlineTags = false; - } - - ++mIndex; - return type; } - } - else - { - if (!mIgnoreLineEndings || ch != '\n') + else + flushBuffer(); + + if (type == Event_ImgTag) { - if (mIndex < mPlainTextEnd) - mBuffer.push_back(ch); - mIgnoreLineEndings = false; mIgnoreNewlineTags = false; } - } - ++mIndex; + ++mIndex; + return type; + } + } + else + { + if (!mIgnoreLineEndings || ch != '\n') + { + if (mIndex < mPlainTextEnd) + mBuffer.push_back(ch); + mIgnoreLineEndings = false; + mIgnoreNewlineTags = false; + } } - flushBuffer(); - return Event_EOF; + ++mIndex; } - void BookTextParser::flushBuffer() - { - mReadyText = mBuffer; - mBuffer.clear(); - } + flushBuffer(); + return Event_EOF; + } - const BookTextParser::Attributes & BookTextParser::getAttributes() const - { - return mAttributes; - } + void BookTextParser::flushBuffer() + { + mReadyText = mBuffer; + mBuffer.clear(); + } - bool BookTextParser::isClosingTag() const + const BookTextParser::Attributes& BookTextParser::getAttributes() const + { + return mAttributes; + } + + bool BookTextParser::isClosingTag() const + { + return mClosingTag; + } + + void BookTextParser::parseTag(std::string tag) + { + size_t tagNameEndPos = tag.find(' '); + mAttributes.clear(); + mTag = tag.substr(0, tagNameEndPos); + Misc::StringUtils::lowerCaseInPlace(mTag); + if (mTag.empty()) + return; + + mClosingTag = (mTag[0] == '/'); + if (mClosingTag) { - return mClosingTag; + mTag.erase(mTag.begin()); + return; } - void BookTextParser::parseTag(std::string tag) + if (tagNameEndPos == std::string::npos) + return; + tag.erase(0, tagNameEndPos + 1); + + while (!tag.empty()) { - size_t tagNameEndPos = tag.find(' '); - mAttributes.clear(); - mTag = tag.substr(0, tagNameEndPos); - Misc::StringUtils::lowerCaseInPlace(mTag); - if (mTag.empty()) + size_t sepPos = tag.find('='); + if (sepPos == std::string::npos) return; - mClosingTag = (mTag[0] == '/'); - if (mClosingTag) - { - mTag.erase(mTag.begin()); - return; - } + std::string key = tag.substr(0, sepPos); + Misc::StringUtils::lowerCaseInPlace(key); + tag.erase(0, sepPos + 1); + + std::string value; - if (tagNameEndPos == std::string::npos) + if (tag.empty()) return; - tag.erase(0, tagNameEndPos+1); - while (!tag.empty()) + if (tag[0] == '"' || tag[0] == '\'') { - size_t sepPos = tag.find('='); - if (sepPos == std::string::npos) - return; - - std::string key = tag.substr(0, sepPos); - Misc::StringUtils::lowerCaseInPlace(key); - tag.erase(0, sepPos+1); - - std::string value; - - if (tag.empty()) - return; - - if (tag[0] == '"') + size_t quoteEndPos = tag.find(tag[0], 1); + if (quoteEndPos == std::string::npos) + throw std::runtime_error("BookTextParser Error: Missing end quote in tag"); + value = tag.substr(1, quoteEndPos - 1); + tag.erase(0, quoteEndPos + 2); + } + else + { + size_t valEndPos = tag.find(' '); + if (valEndPos == std::string::npos) { - size_t quoteEndPos = tag.find('"', 1); - if (quoteEndPos == std::string::npos) - throw std::runtime_error("BookTextParser Error: Missing end quote in tag"); - value = tag.substr(1, quoteEndPos-1); - tag.erase(0, quoteEndPos+2); + value = tag; + tag.erase(); } else { - size_t valEndPos = tag.find(' '); - if (valEndPos == std::string::npos) - { - value = tag; - tag.erase(); - } - else - { - value = tag.substr(0, valEndPos); - tag.erase(0, valEndPos+1); - } + value = tag.substr(0, valEndPos); + tag.erase(0, valEndPos + 1); } - - mAttributes[key] = value; } + + mAttributes[key] = std::move(value); } + } - /* BookFormatter */ - Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget * parent, const std::string & markup, const int pageWidth, const int pageHeight) - { - Paginator pag(pageWidth, pageHeight); + /* BookFormatter */ + Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget* parent, const std::string& markup, + const int pageWidth, const int pageHeight, bool shrinkTextAtLastTag) + { + Paginator pag(pageWidth, pageHeight); - while (parent->getChildCount()) - { - MyGUI::Gui::getInstance().destroyWidget(parent->getChildAt(0)); - } + while (parent->getChildCount()) + { + MyGUI::Gui::getInstance().destroyWidget(parent->getChildAt(0)); + } - mTextStyle = TextStyle(); - mBlockStyle = BlockStyle(); + mTextStyle = TextStyle(); + mBlockStyle = BlockStyle(); - MyGUI::Widget * paper = parent->createWidget("Widget", MyGUI::IntCoord(0, 0, pag.getPageWidth(), pag.getPageHeight()), MyGUI::Align::Left | MyGUI::Align::Top); - paper->setNeedMouseFocus(false); + MyGUI::Widget* paper = parent->createWidget("Widget", + MyGUI::IntCoord(0, 0, pag.getPageWidth(), pag.getPageHeight()), MyGUI::Align::Left | MyGUI::Align::Top); + paper->setNeedMouseFocus(false); - BookTextParser parser(markup); + BookTextParser parser(markup, shrinkTextAtLastTag); - bool brBeforeLastTag = false; - bool isPrevImg = false; - for (;;) - { - BookTextParser::Events event = parser.next(); - if (event == BookTextParser::Event_BrTag || event == BookTextParser::Event_PTag) - continue; + bool brBeforeLastTag = false; + bool isPrevImg = false; + bool inlineImageInserted = false; + for (;;) + { + BookTextParser::Events event = parser.next(); + if (event == BookTextParser::Event_BrTag + || (event == BookTextParser::Event_PTag && parser.getAttributes().empty())) + continue; - std::string plainText = parser.getReadyText(); + std::string plainText = parser.getReadyText(); - // for cases when linebreaks are used to cause a shift to the next page - // if the split text block ends in an empty line, proceeding text block(s) should have leading empty lines removed - if (pag.getIgnoreLeadingEmptyLines()) + // for cases when linebreaks are used to cause a shift to the next page + // if the split text block ends in an empty line, proceeding text block(s) should have leading empty lines + // removed + if (pag.getIgnoreLeadingEmptyLines()) + { + while (!plainText.empty()) { - while (!plainText.empty()) + if (plainText[0] == '\n') + plainText.erase(plainText.begin()); + else { - if (plainText[0] == '\n') - plainText.erase(plainText.begin()); - else - { - pag.setIgnoreLeadingEmptyLines(false); - break; - } + pag.setIgnoreLeadingEmptyLines(false); + break; } } + } - if (plainText.empty()) - brBeforeLastTag = true; - else - { - // Each block of text (between two tags / boundary and tag) will be displayed in a separate editbox widget, - // which means an additional linebreak will be created between them. - // ^ This is not what vanilla MW assumes, so we must deal with line breaks around tags appropriately. - bool brAtStart = (plainText[0] == '\n'); - bool brAtEnd = (plainText[plainText.size()-1] == '\n'); + if (plainText.empty()) + brBeforeLastTag = true; + else + { + // Each block of text (between two tags / boundary and tag) will be displayed in a separate editbox + // widget, which means an additional linebreak will be created between them. ^ This is not what vanilla + // MW assumes, so we must deal with line breaks around tags appropriately. + bool brAtStart = (plainText[0] == '\n'); + bool brAtEnd = (plainText[plainText.size() - 1] == '\n'); - if (brAtStart && !brBeforeLastTag && !isPrevImg) - plainText.erase(plainText.begin()); + if (brAtStart && !brBeforeLastTag && !isPrevImg) + plainText.erase(plainText.begin()); - if (plainText.size() && brAtEnd) - plainText.erase(plainText.end()-1); + if (plainText.size() && brAtEnd) + plainText.erase(plainText.end() - 1); - if (!plainText.empty() || brBeforeLastTag || isPrevImg) + if (!plainText.empty() || brBeforeLastTag || isPrevImg) + { + if (inlineImageInserted) { - TextElement elem(paper, pag, mBlockStyle, - mTextStyle, plainText); - elem.paginate(); + pag.setCurrentTop(pag.getCurrentTop() - mTextStyle.mTextSize); + plainText = " " + plainText; + inlineImageInserted = false; } - - brBeforeLastTag = brAtEnd; + TextElement elem(paper, pag, mBlockStyle, mTextStyle, plainText); + elem.paginate(); } - if (event == BookTextParser::Event_EOF) - break; + brBeforeLastTag = brAtEnd; + } + + if (event == BookTextParser::Event_EOF) + break; - isPrevImg = (event == BookTextParser::Event_ImgTag); + isPrevImg = (event == BookTextParser::Event_ImgTag); - switch (event) + switch (event) + { + case BookTextParser::Event_PageBreak: + pag << Paginator::Page(pag.getStartTop(), pag.getCurrentTop()); + pag.setStartTop(pag.getCurrentTop()); + break; + case BookTextParser::Event_ImgTag: { - case BookTextParser::Event_ImgTag: - { - const BookTextParser::Attributes & attr = parser.getAttributes(); + const BookTextParser::Attributes& attr = parser.getAttributes(); - if (attr.find("src") == attr.end() || attr.find("width") == attr.end() || attr.find("height") == attr.end()) - continue; + auto srcIt = attr.find("src"); + if (srcIt == attr.end()) + continue; + int width = 0; + if (auto widthIt = attr.find("width"); widthIt != attr.end()) + width = MyGUI::utility::parseInt(widthIt->second); + int height = 0; + if (auto heightIt = attr.find("height"); heightIt != attr.end()) + height = MyGUI::utility::parseInt(heightIt->second); - std::string src = attr.at("src"); - int width = MyGUI::utility::parseInt(attr.at("width")); - int height = MyGUI::utility::parseInt(attr.at("height")); + const std::string& src = srcIt->second; + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - bool exists; - std::string correctedSrc = MWBase::Environment::get().getWindowManager()->correctBookartPath(src, width, height, &exists); + std::string correctedSrc; - if (!exists) + constexpr std::string_view imgPrefix = "img://"; + if (src.starts_with(imgPrefix)) + { + correctedSrc = src.substr(imgPrefix.size(), src.size() - imgPrefix.size()); + if (width == 0) { - Log(Debug::Warning) << "Warning: Could not find \"" << src << "\" referenced by an tag."; - break; + width = 50; + inlineImageInserted = true; } + if (height == 0) + height = 50; + } + else + { + if (width == 0 || height == 0) + continue; + correctedSrc = Misc::ResourceHelpers::correctBookartPath(src, width, height, vfs); + } - pag.setIgnoreLeadingEmptyLines(false); - - ImageElement elem(paper, pag, mBlockStyle, - correctedSrc, width, height); - elem.paginate(); + if (!vfs->exists(correctedSrc)) + { + Log(Debug::Warning) << "Warning: Could not find \"" << src << "\" referenced by an tag."; break; } - case BookTextParser::Event_FontTag: - if (parser.isClosingTag()) - resetFontProperties(); - else - handleFont(parser.getAttributes()); - break; - case BookTextParser::Event_DivTag: - handleDiv(parser.getAttributes()); - break; - default: - break; - } - } - - // insert last page - if (pag.getStartTop() != pag.getCurrentTop()) - pag << Paginator::Page(pag.getStartTop(), pag.getStartTop() + pag.getPageHeight()); - paper->setSize(paper->getWidth(), pag.getCurrentTop()); + pag.setIgnoreLeadingEmptyLines(false); - return pag.getPages(); + ImageElement elem(paper, pag, mBlockStyle, correctedSrc, width, height); + elem.paginate(); + break; + } + case BookTextParser::Event_FontTag: + if (parser.isClosingTag()) + resetFontProperties(); + else + handleFont(parser.getAttributes()); + break; + case BookTextParser::Event_PTag: + case BookTextParser::Event_DivTag: + handleDiv(parser.getAttributes()); + break; + default: + break; + } } - Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget * parent, const std::string & markup) - { - return markupToWidget(parent, markup, parent->getWidth(), parent->getHeight()); - } + // insert last page + if (pag.getStartTop() != pag.getCurrentTop()) + pag << Paginator::Page(pag.getStartTop(), pag.getStartTop() + pag.getPageHeight()); - void BookFormatter::resetFontProperties() - { - mTextStyle = TextStyle(); - } + paper->setSize(paper->getWidth(), pag.getCurrentTop()); - void BookFormatter::handleDiv(const BookTextParser::Attributes & attr) - { - if (attr.find("align") == attr.end()) - return; + return pag.getPages(); + } - std::string align = attr.at("align"); + Paginator::Pages BookFormatter::markupToWidget( + MyGUI::Widget* parent, const std::string& markup, bool shrinkTextAtLastTag) + { + return markupToWidget(parent, markup, parent->getWidth(), parent->getHeight(), shrinkTextAtLastTag); + } - if (Misc::StringUtils::ciEqual(align, "center")) - mBlockStyle.mAlign = MyGUI::Align::HCenter; - else if (Misc::StringUtils::ciEqual(align, "left")) - mBlockStyle.mAlign = MyGUI::Align::Left; - else if (Misc::StringUtils::ciEqual(align, "right")) - mBlockStyle.mAlign = MyGUI::Align::Right; - } + void BookFormatter::resetFontProperties() + { + mTextStyle = TextStyle(); + } - void BookFormatter::handleFont(const BookTextParser::Attributes & attr) - { - if (attr.find("color") != attr.end()) - { - unsigned int color; - std::stringstream ss; - ss << attr.at("color"); - ss >> std::hex >> color; - - mTextStyle.mColour = MyGUI::Colour( - (color>>16 & 0xFF) / 255.f, - (color>>8 & 0xFF) / 255.f, - (color & 0xFF) / 255.f); - } - if (attr.find("face") != attr.end()) - { - std::string face = attr.at("face"); - mTextStyle.mFont = "Journalbook "+face; - } - if (attr.find("size") != attr.end()) - { - /// \todo - } - } + void BookFormatter::handleDiv(const BookTextParser::Attributes& attr) + { + auto it = attr.find("align"); + if (it == attr.end()) + return; + + const std::string& align = it->second; + + if (Misc::StringUtils::ciEqual(align, "center")) + mBlockStyle.mAlign = MyGUI::Align::HCenter; + else if (Misc::StringUtils::ciEqual(align, "left")) + mBlockStyle.mAlign = MyGUI::Align::Left; + else if (Misc::StringUtils::ciEqual(align, "right")) + mBlockStyle.mAlign = MyGUI::Align::Right; + } - /* GraphicElement */ - GraphicElement::GraphicElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle) - : mParent(parent), mPaginator(pag), mBlockStyle(blockStyle) + void BookFormatter::handleFont(const BookTextParser::Attributes& attr) + { + auto it = attr.find("color"); + if (it != attr.end()) { - } + const auto& colorString = it->second; + unsigned int color = 0; + std::from_chars(colorString.data(), colorString.data() + colorString.size(), color, 16); - void GraphicElement::paginate() + mTextStyle.mColour + = MyGUI::Colour((color >> 16 & 0xFF) / 255.f, (color >> 8 & 0xFF) / 255.f, (color & 0xFF) / 255.f); + } + it = attr.find("face"); + if (it != attr.end()) { - int newTop = mPaginator.getCurrentTop() + getHeight(); - while (newTop-mPaginator.getStartTop() > mPaginator.getPageHeight()) - { - int newStartTop = pageSplit(); - mPaginator << Paginator::Page(mPaginator.getStartTop(), newStartTop); - mPaginator.setStartTop(newStartTop); - } + const std::string& face = it->second; + std::string name{ Gui::FontLoader::getFontForFace(face) }; - mPaginator.setCurrentTop(newTop); + mTextStyle.mFont = "Journalbook " + name; } - - int GraphicElement::pageSplit() + if (attr.find("size") != attr.end()) { - return mPaginator.getStartTop() + mPaginator.getPageHeight(); + /// \todo } + } - /* TextElement */ - TextElement::TextElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, - const TextStyle & textStyle, const std::string & text) - : GraphicElement(parent, pag, blockStyle), - mTextStyle(textStyle) - { - Gui::EditBox* box = parent->createWidget("NormalText", - MyGUI::IntCoord(0, pag.getCurrentTop(), pag.getPageWidth(), 0), MyGUI::Align::Left | MyGUI::Align::Top, - parent->getName() + MyGUI::utility::toString(parent->getChildCount())); - box->setEditStatic(true); - box->setEditMultiLine(true); - box->setEditWordWrap(true); - box->setNeedMouseFocus(false); - box->setNeedKeyFocus(false); - box->setMaxTextLength(text.size()); - box->setTextAlign(mBlockStyle.mAlign); - box->setTextColour(mTextStyle.mColour); - box->setFontName(mTextStyle.mFont); - box->setCaption(MyGUI::TextIterator::toTagsString(text)); - box->setSize(box->getSize().width, box->getTextSize().height); - mEditBox = box; - } + /* GraphicElement */ + GraphicElement::GraphicElement(MyGUI::Widget* parent, Paginator& pag, const BlockStyle& blockStyle) + : mParent(parent) + , mPaginator(pag) + , mBlockStyle(blockStyle) + { + } - int TextElement::getHeight() + void GraphicElement::paginate() + { + int newTop = mPaginator.getCurrentTop() + getHeight(); + while (newTop - mPaginator.getStartTop() > mPaginator.getPageHeight()) { - return mEditBox->getTextSize().height; + int newStartTop = pageSplit(); + mPaginator << Paginator::Page(mPaginator.getStartTop(), newStartTop); + mPaginator.setStartTop(newStartTop); } - int TextElement::pageSplit() + mPaginator.setCurrentTop(newTop); + } + + int GraphicElement::pageSplit() + { + return mPaginator.getStartTop() + mPaginator.getPageHeight(); + } + + /* TextElement */ + TextElement::TextElement(MyGUI::Widget* parent, Paginator& pag, const BlockStyle& blockStyle, + const TextStyle& textStyle, const std::string& text) + : GraphicElement(parent, pag, blockStyle) + , mTextStyle(textStyle) + { + Gui::EditBox* box = parent->createWidget("NormalText", + MyGUI::IntCoord(0, pag.getCurrentTop(), pag.getPageWidth(), 0), MyGUI::Align::Left | MyGUI::Align::Top, + parent->getName() + MyGUI::utility::toString(parent->getChildCount())); + box->setEditStatic(true); + box->setEditMultiLine(true); + box->setEditWordWrap(true); + box->setNeedMouseFocus(false); + box->setNeedKeyFocus(false); + box->setMaxTextLength(text.size()); + box->setTextAlign(mBlockStyle.mAlign); + box->setTextColour(mTextStyle.mColour); + box->setFontName(mTextStyle.mFont); + box->setCaption(MyGUI::TextIterator::toTagsString(text)); + box->setSize(box->getSize().width, box->getTextSize().height); + mEditBox = box; + } + + int TextElement::getHeight() + { + return mEditBox->getTextSize().height; + } + + int TextElement::pageSplit() + { + // split lines + const int lineHeight = Settings::gui().mFontSize; + unsigned int lastLine = (mPaginator.getStartTop() + mPaginator.getPageHeight() - mPaginator.getCurrentTop()); + if (lineHeight > 0) + lastLine /= lineHeight; + int ret = mPaginator.getCurrentTop() + lastLine * lineHeight; + + // first empty lines that would go to the next page should be ignored + mPaginator.setIgnoreLeadingEmptyLines(true); + + const MyGUI::VectorLineInfo& lines = mEditBox->getSubWidgetText()->castType()->getLineInfo(); + for (size_t i = lastLine; i < lines.size(); ++i) { - // split lines - const int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight(); - unsigned int lastLine = (mPaginator.getStartTop() + mPaginator.getPageHeight() - mPaginator.getCurrentTop()); - if (lineHeight > 0) - lastLine /= lineHeight; - int ret = mPaginator.getCurrentTop() + lastLine * lineHeight; - - // first empty lines that would go to the next page should be ignored - mPaginator.setIgnoreLeadingEmptyLines(true); - - const MyGUI::VectorLineInfo & lines = mEditBox->getSubWidgetText()->castType()->getLineInfo(); - for (unsigned int i = lastLine; i < lines.size(); ++i) + if (lines[i].width == 0) + ret += lineHeight; + else { - if (lines[i].width == 0) - ret += lineHeight; - else - { - mPaginator.setIgnoreLeadingEmptyLines(false); - break; - } + mPaginator.setIgnoreLeadingEmptyLines(false); + break; } - return ret; } + return ret; + } - /* ImageElement */ - ImageElement::ImageElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, - const std::string & src, int width, int height) - : GraphicElement(parent, pag, blockStyle), - mImageHeight(height) - { - int left = 0; - if (mBlockStyle.mAlign.isHCenter()) - left += (pag.getPageWidth() - width) / 2; - else if (mBlockStyle.mAlign.isLeft()) - left = 0; - else if (mBlockStyle.mAlign.isRight()) - left += pag.getPageWidth() - width; - - mImageBox = parent->createWidget ("ImageBox", - MyGUI::IntCoord(left, pag.getCurrentTop(), width, mImageHeight), MyGUI::Align::Left | MyGUI::Align::Top, - parent->getName() + MyGUI::utility::toString(parent->getChildCount())); - - mImageBox->setImageTexture(src); - mImageBox->setProperty("NeedMouse", "false"); - } + /* ImageElement */ + ImageElement::ImageElement(MyGUI::Widget* parent, Paginator& pag, const BlockStyle& blockStyle, + const std::string& src, int width, int height) + : GraphicElement(parent, pag, blockStyle) + , mImageHeight(height) + { + int left = 0; + if (mBlockStyle.mAlign.isHCenter()) + left += (pag.getPageWidth() - width) / 2; + else if (mBlockStyle.mAlign.isLeft()) + left = 0; + else if (mBlockStyle.mAlign.isRight()) + left += pag.getPageWidth() - width; + + mImageBox = parent->createWidget("ImageBox", + MyGUI::IntCoord(left, pag.getCurrentTop(), width, mImageHeight), MyGUI::Align::Left | MyGUI::Align::Top, + parent->getName() + MyGUI::utility::toString(parent->getChildCount())); + + mImageBox->setImageTexture(src); + mImageBox->setProperty("NeedMouse", "false"); + } - int ImageElement::getHeight() - { - return mImageHeight; - } + int ImageElement::getHeight() + { + return mImageHeight; + } - int ImageElement::pageSplit() - { - // if the image is larger than the page, fall back to the default pageSplit implementation - if (mImageHeight > mPaginator.getPageHeight()) - return GraphicElement::pageSplit(); - return mPaginator.getCurrentTop(); - } + int ImageElement::pageSplit() + { + // if the image is larger than the page, fall back to the default pageSplit implementation + if (mImageHeight > mPaginator.getPageHeight()) + return GraphicElement::pageSplit(); + return mPaginator.getCurrentTop(); } } diff --git a/apps/openmw/mwgui/formatting.hpp b/apps/openmw/mwgui/formatting.hpp index 12a3d46caa0..9a215b200b5 100644 --- a/apps/openmw/mwgui/formatting.hpp +++ b/apps/openmw/mwgui/formatting.hpp @@ -12,9 +12,9 @@ namespace MWGui { struct TextStyle { - TextStyle() : - mColour(0,0,0) - , mFont("Journalbook Magic Cards") + TextStyle() + : mColour(0, 0, 0) + , mFont("Journalbook DefaultFont") , mTextSize(16) { } @@ -26,8 +26,8 @@ namespace MWGui struct BlockStyle { - BlockStyle() : - mAlign(MyGUI::Align::Left | MyGUI::Align::Top) + BlockStyle() + : mAlign(MyGUI::Align::Left | MyGUI::Align::Top) { } @@ -36,140 +36,145 @@ namespace MWGui class BookTextParser { - public: - typedef std::map Attributes; - enum Events - { - Event_None = -2, - Event_EOF = -1, - Event_BrTag, - Event_PTag, - Event_ImgTag, - Event_DivTag, - Event_FontTag - }; - - BookTextParser(const std::string & text); - - Events next(); - - const Attributes & getAttributes() const; - std::string getReadyText() const; - bool isClosingTag() const; - - private: - void registerTag(const std::string & tag, Events type); - void flushBuffer(); - void parseTag(std::string tag); - - size_t mIndex; - std::string mText; - std::string mReadyText; - - bool mIgnoreNewlineTags; - bool mIgnoreLineEndings; - Attributes mAttributes; - std::string mTag; - bool mClosingTag; - std::map mTagTypes; - std::string mBuffer; - - size_t mPlainTextEnd; + public: + typedef std::map Attributes; + enum Events + { + Event_None = -2, + Event_EOF = -1, + Event_BrTag, + Event_PTag, + Event_ImgTag, + Event_DivTag, + Event_FontTag, + Event_PageBreak, + }; + + BookTextParser(const std::string& text, bool shrinkTextAtLastTag); + + Events next(); + + const Attributes& getAttributes() const; + std::string getReadyText() const; + bool isClosingTag() const; + + private: + void registerTag(const std::string& tag, Events type); + void flushBuffer(); + void parseTag(std::string tag); + + size_t mIndex; + std::string mText; + std::string mReadyText; + + bool mIgnoreNewlineTags; + bool mIgnoreLineEndings; + Attributes mAttributes; + std::string mTag; + bool mClosingTag; + std::map mTagTypes; + std::string mBuffer; + + size_t mPlainTextEnd; }; class Paginator { - public: - typedef std::pair Page; - typedef std::vector Pages; - - Paginator(int pageWidth, int pageHeight) - : mStartTop(0), mCurrentTop(0), - mPageWidth(pageWidth), mPageHeight(pageHeight), - mIgnoreLeadingEmptyLines(false) - { - } - - int getStartTop() const { return mStartTop; } - int getCurrentTop() const { return mCurrentTop; } - int getPageWidth() const { return mPageWidth; } - int getPageHeight() const { return mPageHeight; } - bool getIgnoreLeadingEmptyLines() const { return mIgnoreLeadingEmptyLines; } - Pages getPages() const { return mPages; } - - void setStartTop(int top) { mStartTop = top; } - void setCurrentTop(int top) { mCurrentTop = top; } - void setIgnoreLeadingEmptyLines(bool ignore) { mIgnoreLeadingEmptyLines = ignore; } - - Paginator & operator<<(const Page & page) - { - mPages.push_back(page); - return *this; - } - - private: - int mStartTop, mCurrentTop; - int mPageWidth, mPageHeight; - bool mIgnoreLeadingEmptyLines; - Pages mPages; + public: + typedef std::pair Page; + typedef std::vector Pages; + + Paginator(int pageWidth, int pageHeight) + : mStartTop(0) + , mCurrentTop(0) + , mPageWidth(pageWidth) + , mPageHeight(pageHeight) + , mIgnoreLeadingEmptyLines(false) + { + } + + int getStartTop() const { return mStartTop; } + int getCurrentTop() const { return mCurrentTop; } + int getPageWidth() const { return mPageWidth; } + int getPageHeight() const { return mPageHeight; } + bool getIgnoreLeadingEmptyLines() const { return mIgnoreLeadingEmptyLines; } + Pages getPages() const { return mPages; } + + void setStartTop(int top) { mStartTop = top; } + void setCurrentTop(int top) { mCurrentTop = top; } + void setIgnoreLeadingEmptyLines(bool ignore) { mIgnoreLeadingEmptyLines = ignore; } + + Paginator& operator<<(const Page& page) + { + mPages.push_back(page); + return *this; + } + + private: + int mStartTop, mCurrentTop; + int mPageWidth, mPageHeight; + bool mIgnoreLeadingEmptyLines; + Pages mPages; }; /// \brief utilities for parsing book/scroll text as mygui widgets class BookFormatter { - public: - Paginator::Pages markupToWidget(MyGUI::Widget * parent, const std::string & markup, const int pageWidth, const int pageHeight); - Paginator::Pages markupToWidget(MyGUI::Widget * parent, const std::string & markup); + public: + Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup, const int pageWidth, + const int pageHeight, bool shrinkTextAtLastTag); + Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup, bool shrinkTextAtLastTag); - private: - void resetFontProperties(); + private: + void resetFontProperties(); - void handleDiv(const BookTextParser::Attributes & attr); - void handleFont(const BookTextParser::Attributes & attr); + void handleDiv(const BookTextParser::Attributes& attr); + void handleFont(const BookTextParser::Attributes& attr); - TextStyle mTextStyle; - BlockStyle mBlockStyle; + TextStyle mTextStyle; + BlockStyle mBlockStyle; }; class GraphicElement { - public: - GraphicElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle); - virtual int getHeight() = 0; - virtual void paginate(); - virtual int pageSplit(); - - protected: - virtual ~GraphicElement() {} - MyGUI::Widget * mParent; - Paginator & mPaginator; - BlockStyle mBlockStyle; + public: + GraphicElement(MyGUI::Widget* parent, Paginator& pag, const BlockStyle& blockStyle); + virtual int getHeight() = 0; + virtual void paginate(); + virtual int pageSplit(); + + protected: + virtual ~GraphicElement() {} + MyGUI::Widget* mParent; + Paginator& mPaginator; + BlockStyle mBlockStyle; }; class TextElement : public GraphicElement { - public: - TextElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, - const TextStyle & textStyle, const std::string & text); - int getHeight() override; - int pageSplit() override; - private: - int currentFontHeight() const; - TextStyle mTextStyle; - Gui::EditBox * mEditBox; + public: + TextElement(MyGUI::Widget* parent, Paginator& pag, const BlockStyle& blockStyle, const TextStyle& textStyle, + const std::string& text); + int getHeight() override; + int pageSplit() override; + + private: + int currentFontHeight() const; + TextStyle mTextStyle; + Gui::EditBox* mEditBox; }; class ImageElement : public GraphicElement { - public: - ImageElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, - const std::string & src, int width, int height); - int getHeight() override; - int pageSplit() override; - - private: - int mImageHeight; - MyGUI::ImageBox * mImageBox; + public: + ImageElement(MyGUI::Widget* parent, Paginator& pag, const BlockStyle& blockStyle, const std::string& src, + int width, int height); + int getHeight() override; + int pageSplit() override; + + private: + int mImageHeight; + MyGUI::ImageBox* mImageBox; }; } } diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index 45defe9a56a..0a37c93b4f9 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -1,13 +1,17 @@ #include "hud.hpp" -#include -#include #include -#include #include +#include +#include +#include #include -#include +#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -16,14 +20,13 @@ #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/creaturestats.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "draganddrop.hpp" #include "inventorywindow.hpp" -#include "spellicons.hpp" #include "itemmodel.hpp" -#include "draganddrop.hpp" +#include "spellicons.hpp" #include "itemwidget.hpp" @@ -36,27 +39,46 @@ namespace MWGui class WorldItemModel : public ItemModel { public: - WorldItemModel(float left, float top) : mLeft(left), mTop(top) {} + WorldItemModel(float left, float top) + : mLeft(left) + , mTop(top) + { + } virtual ~WorldItemModel() override {} - MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool /*allowAutoEquip*/) 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); + dropped = world->placeObject(item.mBase, mLeft, mTop, count, copy); else - dropped = world->dropObjectOnGround(world->getPlayerPtr(), item.mBase, count); - dropped.getCellRef().setOwner(""); + dropped = world->dropObjectOnGround(world->getPlayerPtr(), item.mBase, count, copy); + dropped.getCellRef().setOwner(ESM::RefId()); return dropped; } - void removeItem (const ItemStack& item, size_t count) override { throw std::runtime_error("removeItem not implemented"); } - ModelIndex getIndex (ItemStack item) override { throw std::runtime_error("getIndex not implemented"); } + 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"); } + 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 @@ -64,10 +86,9 @@ namespace MWGui float mTop; }; - - HUD::HUD(CustomMarkerCollection &customMarkers, DragAndDrop* dragAndDrop, MWRender::LocalMap* localMapRender) + HUD::HUD(CustomMarkerCollection& customMarkers, DragAndDrop* dragAndDrop, MWRender::LocalMap* localMapRender) : WindowBase("openmw_hud.layout") - , LocalMapBase(customMarkers, localMapRender, Settings::Manager::getBool("local map hud fog of war", "Map")) + , LocalMapBase(customMarkers, localMapRender, Settings::map().mLocalMapHudFogOfWar) , mHealth(nullptr) , mMagicka(nullptr) , mStamina(nullptr) @@ -80,7 +101,7 @@ namespace MWGui , mMinimap(nullptr) , mCrosshair(nullptr) , mCellNameBox(nullptr) - , mDrowningFrame(nullptr) + , mDrowningBar(nullptr) , mDrowningFlash(nullptr) , mHealthManaStaminaBaseLeft(0) , mWeapBoxBaseLeft(0) @@ -99,8 +120,6 @@ namespace MWGui , mIsDrowning(false) , mDrowningFlashTheta(0.f) { - mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); - // Energy bars getWidget(mHealthFrame, "HealthFrame"); getWidget(mHealth, "Health"); @@ -117,7 +136,8 @@ namespace MWGui magickaFrame->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onHMSClicked); fatigueFrame->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onHMSClicked); - //Drowning bar + // Drowning bar + getWidget(mDrowningBar, "DrowningBar"); getWidget(mDrowningFrame, "DrowningFrame"); getWidget(mDrowning, "Drowning"); getWidget(mDrowningFlash, "Flash"); @@ -162,7 +182,7 @@ namespace MWGui mMainWidget->eventMouseMove += MyGUI::newDelegate(this, &HUD::onWorldMouseOver); mMainWidget->eventMouseLostFocus += MyGUI::newDelegate(this, &HUD::onWorldMouseLostFocus); - mSpellIcons = new SpellIcons(); + mSpellIcons = std::make_unique(); } HUD::~HUD() @@ -170,15 +190,12 @@ namespace MWGui mMainWidget->eventMouseLostFocus.clear(); mMainWidget->eventMouseMove.clear(); mMainWidget->eventMouseButtonClick.clear(); - - delete mSpellIcons; } - void HUD::setValue(const std::string& id, const MWMechanics::DynamicStat& value) + void HUD::setValue(std::string_view id, const MWMechanics::DynamicStat& value) { int current = static_cast(value.getCurrent()); int modified = static_cast(value.getModified()); - // Fatigue can be negative if (id != "FBar") current = std::max(0, current); @@ -223,27 +240,26 @@ namespace MWGui void HUD::setDrowningBarVisible(bool visible) { - mDrowningFrame->setVisible(visible); + mDrowningBar->setVisible(visible); } void HUD::onWorldClicked(MyGUI::Widget* _sender) { - if (!MWBase::Environment::get().getWindowManager ()->isGuiMode ()) + if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) return; - MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); if (mDragAndDrop->mIsOnDragAndDrop) { // drop item into the gameworld - MWBase::Environment::get().getWorld()->breakInvisibility( - MWMechanics::getPlayer()); + 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); + WorldItemModel drop(mouseX, mouseY); mDragAndDrop->drop(&drop, nullptr); winMgr->changePointer("arrow"); @@ -259,7 +275,7 @@ namespace MWGui if (winMgr->isConsoleMode()) winMgr->setConsoleSelectedObject(object); - else //if ((mode == GM_Container) || (mode == GM_Inventory)) + else // if ((mode == GM_Container) || (mode == GM_Inventory)) { // pick up object if (!object.isEmpty()) @@ -288,7 +304,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->changePointer("drop_ground"); else MWBase::Environment::get().getWindowManager()->changePointer("arrow"); - } else { @@ -364,12 +379,9 @@ namespace MWGui if (mEnemyHealth->getVisible() && mEnemyHealthTimer < 0) { mEnemyHealth->setVisible(false); - mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() + MyGUI::IntPoint(0,20)); + mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() + MyGUI::IntPoint(0, 20)); } - if (mIsDrowning) - mDrowningFlashTheta += dt * osg::PI*2; - mSpellIcons->updateWidgets(mEffectBox, true); if (mEnemyActorId != -1 && mEnemyHealth->getVisible()) @@ -377,20 +389,25 @@ namespace MWGui updateEnemyHealthBar(); } + if (mDrowningBar->getVisible()) + mDrowningBar->setPosition( + mMainWidget->getWidth() / 2 - mDrowningFrame->getWidth() / 2, mMainWidget->getTop()); + if (mIsDrowning) { + mDrowningFlashTheta += dt * osg::PI * 2; + float intensity = (cos(mDrowningFlashTheta) + 2.0f) / 3.0f; mDrowningFlash->setAlpha(intensity); } } - void HUD::setSelectedSpell(const std::string& spellId, int successChancePercent) + void HUD::setSelectedSpell(const ESM::RefId& spellId, int successChancePercent) { - const ESM::Spell* spell = - MWBase::Environment::get().getWorld()->getStore().get().find(spellId); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(spellId); - std::string spellName = spell->mName; + const std::string& spellName = spell->mName; if (spellName != mSpellName && mSpellVisible) { mWeaponSpellTimer = 5.0f; @@ -403,23 +420,29 @@ namespace MWGui mSpellStatus->setProgressPosition(successChancePercent); mSpellBox->setUserString("ToolTipType", "Spell"); - mSpellBox->setUserString("Spell", spellId); + mSpellBox->setUserString("Spell", spellId.serialize()); + mSpellBox->setUserData(MyGUI::Any::Null); - // use the icon of the first effect - const ESM::MagicEffect* effect = - MWBase::Environment::get().getWorld()->getStore().get().find(spell->mEffects.mList.front().mEffectID); - - std::string icon = effect->mIcon; - int slashPos = icon.rfind('\\'); - icon.insert(slashPos+1, "b_"); - icon = MWBase::Environment::get().getWindowManager()->correctIconPath(icon); - - mSpellImage->setSpellIcon(icon); + if (!spell->mEffects.mList.empty()) + { + // use the icon of the first effect + const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( + spell->mEffects.mList.front().mData.mEffectID); + std::string icon = effect->mIcon; + std::replace(icon.begin(), icon.end(), '/', '\\'); + size_t slashPos = icon.rfind('\\'); + icon.insert(slashPos + 1, "b_"); + icon = Misc::ResourceHelpers::correctIconPath( + icon, MWBase::Environment::get().getResourceSystem()->getVFS()); + mSpellImage->setSpellIcon(icon); + } + else + mSpellImage->setSpellIcon({}); } void HUD::setSelectedEnchantItem(const MWWorld::Ptr& item, int chargePercent) { - std::string itemName = item.getClass().getName(item); + std::string_view itemName = item.getClass().getName(item); if (itemName != mSpellName && mSpellVisible) { mWeaponSpellTimer = 5.0f; @@ -439,7 +462,7 @@ namespace MWGui void HUD::setSelectedWeapon(const MWWorld::Ptr& item, int durabilityPercent) { - std::string itemName = item.getClass().getName(item); + std::string_view itemName = item.getClass().getName(item); if (itemName != mWeaponName && mWeaponVisible) { mWeaponSpellTimer = 5.0f; @@ -460,7 +483,7 @@ namespace MWGui void HUD::unsetSelectedSpell() { - std::string spellName = "#{sNone}"; + std::string_view spellName = "#{Interface:None}"; if (spellName != mSpellName && mSpellVisible) { mWeaponSpellTimer = 5.0f; @@ -473,6 +496,7 @@ namespace MWGui mSpellStatus->setProgressPosition(0); mSpellImage->setItem(MWWorld::Ptr()); mSpellBox->clearUserStrings(); + mSpellBox->setUserData(MyGUI::Any::Null); } void HUD::unsetSelectedWeapon() @@ -489,11 +513,12 @@ namespace MWGui mWeapStatus->setProgressRange(100); mWeapStatus->setProgressPosition(0); - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); mWeapImage->setItem(MWWorld::Ptr()); - std::string icon = (player.getClass().getNpcStats(player).isWerewolf()) ? "icons\\k\\tx_werewolf_hand.dds" : "icons\\k\\stealth_handtohand.dds"; + std::string icon = (player.getClass().getNpcStats(player).isWerewolf()) ? "icons\\k\\tx_werewolf_hand.dds" + : "icons\\k\\stealth_handtohand.dds"; mWeapImage->setIcon(icon); mWeapBox->clearUserStrings(); @@ -501,16 +526,17 @@ namespace MWGui mWeapBox->setUserString("ToolTipLayout", "HandToHandToolTip"); mWeapBox->setUserString("Caption_HandToHandText", itemName); mWeapBox->setUserString("ImageTexture_HandToHandImage", icon); + mWeapBox->setUserData(MyGUI::Any::Null); } void HUD::setCrosshairVisible(bool visible) { - mCrosshair->setVisible (visible); + mCrosshair->setVisible(visible); } - + void HUD::setCrosshairOwned(bool owned) { - if(owned) + if (owned) { mCrosshair->changeWidgetSkin("HUD_Crosshair_Owned"); } @@ -519,7 +545,7 @@ namespace MWGui mCrosshair->changeWidgetSkin("HUD_Crosshair"); } } - + void HUD::setHmsVisible(bool visible) { mHealth->setVisible(visible); @@ -548,13 +574,13 @@ namespace MWGui void HUD::setEffectVisible(bool visible) { - mEffectBox->setVisible (visible); + mEffectBox->setVisible(visible); updatePositions(); } void HUD::setMinimapVisible(bool visible) { - mMinimapBox->setVisible (visible); + mMinimapBox->setVisible(visible); updatePositions(); } @@ -586,14 +612,15 @@ namespace MWGui // effect box can have variable width -> variable left coordinate int effectsDx = 0; - if (!mMinimapBox->getVisible ()) + if (!mMinimapBox->getVisible()) effectsDx = mEffectBoxBaseRight - mMinimapBoxBaseRight; - mMapVisible = mMinimapBox->getVisible (); + mMapVisible = mMinimapBox->getVisible(); if (!mMapVisible) mCellNameBox->setVisible(false); - mEffectBox->setPosition((viewSize.width - mEffectBoxBaseRight) - mEffectBox->getWidth() + effectsDx, mEffectBox->getTop()); + mEffectBox->setPosition( + (viewSize.width - mEffectBoxBaseRight) - mEffectBox->getWidth() + effectsDx, mEffectBox->getTop()); } void HUD::updateEnemyHealthBar() @@ -605,43 +632,64 @@ namespace MWGui mEnemyHealth->setProgressRange(100); // Health is usually cast to int before displaying. Actors die whenever they are < 1 health. // Therefore any value < 1 should show as an empty health bar. We do the same in statswindow :) - mEnemyHealth->setProgressPosition(static_cast(stats.getHealth().getCurrent() / stats.getHealth().getModified() * 100)); + mEnemyHealth->setProgressPosition(static_cast(stats.getHealth().getRatio() * 100)); - static const float fNPCHealthBarFade = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCHealthBarFade")->mValue.getFloat(); + static const float fNPCHealthBarFade = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fNPCHealthBarFade") + ->mValue.getFloat(); if (fNPCHealthBarFade > 0.f) - mEnemyHealth->setAlpha(std::max(0.f, std::min(1.f, mEnemyHealthTimer/fNPCHealthBarFade))); - + mEnemyHealth->setAlpha(std::clamp(mEnemyHealthTimer / fNPCHealthBarFade, 0.f, 1.f)); } - void HUD::setEnemy(const MWWorld::Ptr &enemy) + void HUD::setEnemy(const MWWorld::Ptr& enemy) { mEnemyActorId = enemy.getClass().getCreatureStats(enemy).getActorId(); - mEnemyHealthTimer = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCHealthBarTime")->mValue.getFloat(); + mEnemyHealthTimer = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fNPCHealthBarTime") + ->mValue.getFloat(); if (!mEnemyHealth->getVisible()) - mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() - MyGUI::IntPoint(0,20)); + mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() - MyGUI::IntPoint(0, 20)); mEnemyHealth->setVisible(true); updateEnemyHealthBar(); } - void HUD::resetEnemy() + void HUD::clear() { mEnemyActorId = -1; mEnemyHealthTimer = -1; - } - void HUD::clear() - { - unsetSelectedSpell(); - unsetSelectedWeapon(); - resetEnemy(); + mWeaponSpellTimer = 0.f; + mWeaponName = std::string(); + mSpellName = std::string(); + mWeaponSpellBox->setVisible(false); + + mWeapStatus->setProgressRange(100); + mWeapStatus->setProgressPosition(0); + mSpellStatus->setProgressRange(100); + mSpellStatus->setProgressPosition(0); + + mWeapImage->setItem(MWWorld::Ptr()); + mSpellImage->setItem(MWWorld::Ptr()); + + mWeapBox->clearUserStrings(); + mWeapBox->setUserData(MyGUI::Any::Null); + mSpellBox->clearUserStrings(); + mSpellBox->setUserData(MyGUI::Any::Null); + + mActiveCell = nullptr; + mHasALastActiveCell = false; } - void HUD::customMarkerCreated(MyGUI::Widget *marker) + void HUD::customMarkerCreated(MyGUI::Widget* marker) { marker->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMapClicked); } - void HUD::doorMarkerCreated(MyGUI::Widget *marker) + void HUD::doorMarkerCreated(MyGUI::Widget* marker) { marker->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMapClicked); } diff --git a/apps/openmw/mwgui/hud.hpp b/apps/openmw/mwgui/hud.hpp index 8a89320d8c7..8dd98628c45 100644 --- a/apps/openmw/mwgui/hud.hpp +++ b/apps/openmw/mwgui/hud.hpp @@ -1,7 +1,10 @@ #ifndef OPENMW_GAME_MWGUI_HUD_H #define OPENMW_GAME_MWGUI_HUD_H +#include + #include "mapwindow.hpp" +#include "spellicons.hpp" #include "statswatcher.hpp" namespace MWWorld @@ -12,7 +15,6 @@ namespace MWWorld namespace MWGui { class DragAndDrop; - class SpellIcons; class ItemWidget; class SpellWidget; @@ -21,7 +23,7 @@ namespace MWGui public: HUD(CustomMarkerCollection& customMarkers, DragAndDrop* dragAndDrop, MWRender::LocalMap* localMapRender); virtual ~HUD(); - void setValue (const std::string& id, const MWMechanics::DynamicStat& value) override; + void setValue(std::string_view id, const MWMechanics::DynamicStat& value) override; /// Set time left for the player to start drowning /// @param time time left to start drowning @@ -37,7 +39,7 @@ namespace MWGui void setEffectVisible(bool visible); void setMinimapVisible(bool visible); - void setSelectedSpell(const std::string& spellId, int successChancePercent); + void setSelectedSpell(const ESM::RefId& spellId, int successChancePercent); void setSelectedEnchantItem(const MWWorld::Ptr& item, int chargePercent); const MWWorld::Ptr& getSelectedEnchantItem(); void setSelectedWeapon(const MWWorld::Ptr& item, int durabilityPercent); @@ -56,7 +58,6 @@ namespace MWGui MyGUI::Widget* getEffectBox() { return mEffectBox; } void setEnemy(const MWWorld::Ptr& enemy); - void resetEnemy(); void clear() override; @@ -64,8 +65,8 @@ namespace MWGui MyGUI::ProgressBar *mHealth, *mMagicka, *mStamina, *mEnemyHealth, *mDrowning; MyGUI::Widget* mHealthFrame; MyGUI::Widget *mWeapBox, *mSpellBox, *mSneakBox; - ItemWidget *mWeapImage; - SpellWidget *mSpellImage; + ItemWidget* mWeapImage; + SpellWidget* mSpellImage; MyGUI::ProgressBar *mWeapStatus, *mSpellStatus; MyGUI::Widget *mEffectBox, *mMinimapBox; MyGUI::Button* mMinimapButton; @@ -73,7 +74,7 @@ namespace MWGui MyGUI::ImageBox* mCrosshair; MyGUI::TextBox* mCellNameBox; MyGUI::TextBox* mWeaponSpellBox; - MyGUI::Widget *mDrowningFrame, *mDrowningFlash; + MyGUI::Widget *mDrowningBar, *mDrowningFrame, *mDrowningFlash; // bottom left elements int mHealthManaStaminaBaseLeft, mWeapBoxBaseLeft, mSpellBoxBaseLeft, mSneakBoxBaseLeft; @@ -95,12 +96,12 @@ namespace MWGui bool mWorldMouseOver; - SpellIcons* mSpellIcons; + std::unique_ptr mSpellIcons; int mEnemyActorId; float mEnemyHealthTimer; - bool mIsDrowning; + bool mIsDrowning; float mDrowningFlashTheta; void onWorldClicked(MyGUI::Widget* _sender); diff --git a/apps/openmw/mwgui/inventoryitemmodel.cpp b/apps/openmw/mwgui/inventoryitemmodel.cpp index f2ff64aa16b..7464290947b 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.cpp +++ b/apps/openmw/mwgui/inventoryitemmodel.cpp @@ -3,11 +3,12 @@ #include #include "../mwmechanics/actorutil.hpp" -#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/creaturestats.hpp" -#include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/containerstore.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/manualref.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -15,120 +16,135 @@ namespace MWGui { -InventoryItemModel::InventoryItemModel(const MWWorld::Ptr &actor) - : mActor(actor) -{ -} - -ItemStack InventoryItemModel::getItem (ModelIndex index) -{ - if (index < 0) - throw std::runtime_error("Invalid index supplied"); - if (mItems.size() <= static_cast(index)) - throw std::runtime_error("Item index out of range"); - return mItems[index]; -} - -size_t InventoryItemModel::getItemCount() -{ - return mItems.size(); -} - -ItemModel::ModelIndex InventoryItemModel::getIndex (ItemStack item) -{ - size_t i = 0; - for (ItemStack& itemStack : mItems) + InventoryItemModel::InventoryItemModel(const MWWorld::Ptr& actor) + : mActor(actor) { - if (itemStack == item) - return i; - ++i; } - return -1; -} - -MWWorld::Ptr InventoryItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) -{ - if (item.mBase.getContainerStore() == &mActor.getClass().getContainerStore(mActor)) - throw std::runtime_error("Item to copy needs to be from a different container!"); - return *mActor.getClass().getContainerStore(mActor).add(item.mBase, count, mActor, allowAutoEquip); -} -void InventoryItemModel::removeItem (const ItemStack& item, size_t count) -{ - int removed = 0; - // Re-equipping makes sense only if a target has inventory - if (mActor.getClass().hasInventoryStore(mActor)) + ItemStack InventoryItemModel::getItem(ModelIndex index) { - MWWorld::InventoryStore& store = mActor.getClass().getInventoryStore(mActor); - removed = store.remove(item.mBase, count, mActor, true); + if (index < 0) + throw std::runtime_error("Invalid index supplied"); + if (mItems.size() <= static_cast(index)) + throw std::runtime_error("Item index out of range"); + return mItems[index]; } - else + + size_t InventoryItemModel::getItemCount() { - MWWorld::ContainerStore& store = mActor.getClass().getContainerStore(mActor); - removed = store.remove(item.mBase, count, mActor); + return mItems.size(); } - std::stringstream error; - - if (removed == 0) + ItemModel::ModelIndex InventoryItemModel::getIndex(const ItemStack& item) { - error << "Item '" << item.mBase.getCellRef().getRefId() << "' was not found in container store to remove"; - throw std::runtime_error(error.str()); + size_t i = 0; + for (ItemStack& itemStack : mItems) + { + if (itemStack == item) + return i; + ++i; + } + return -1; } - else if (removed < static_cast(count)) + + MWWorld::Ptr InventoryItemModel::addItem(const ItemStack& item, size_t count, bool allowAutoEquip) { - error << "Not enough items '" << item.mBase.getCellRef().getRefId() << "' in the stack to remove (" << static_cast(count) << " requested, " << removed << " found)"; - throw std::runtime_error(error.str()); + if (item.mBase.getContainerStore() == &mActor.getClass().getContainerStore(mActor)) + throw std::runtime_error("Item to add needs to be from a different container!"); + return *mActor.getClass().getContainerStore(mActor).add(item.mBase, count, allowAutoEquip); } -} -MWWorld::Ptr InventoryItemModel::moveItem(const ItemStack &item, size_t count, ItemModel *otherModel) -{ - // Can't move conjured items: This is a general fix that also takes care of issues with taking conjured items via the 'Take All' button. - if (item.mFlags & ItemStack::Flag_Bound) - return MWWorld::Ptr(); + MWWorld::Ptr InventoryItemModel::copyItem(const ItemStack& item, size_t count, bool allowAutoEquip) + { + if (item.mBase.getContainerStore() == &mActor.getClass().getContainerStore(mActor)) + throw std::runtime_error("Item to copy needs to be from a different container!"); - MWWorld::Ptr ret = otherModel->copyItem(item, count); - removeItem(item, count); - return ret; -} + MWWorld::ManualRef newRef(*MWBase::Environment::get().getESMStore(), item.mBase, count); + return *mActor.getClass().getContainerStore(mActor).add(newRef.getPtr(), count, allowAutoEquip); + } -void InventoryItemModel::update() -{ - MWWorld::ContainerStore& store = mActor.getClass().getContainerStore(mActor); + void InventoryItemModel::removeItem(const ItemStack& item, size_t count) + { + int removed = 0; + // Re-equipping makes sense only if a target has inventory + if (mActor.getClass().hasInventoryStore(mActor)) + { + MWWorld::InventoryStore& store = mActor.getClass().getInventoryStore(mActor); + removed = store.remove(item.mBase, count, true); + } + else + { + MWWorld::ContainerStore& store = mActor.getClass().getContainerStore(mActor); + removed = store.remove(item.mBase, count); + } - mItems.clear(); + std::stringstream error; - for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + if (removed == 0) + { + error << "Item '" << item.mBase.getCellRef().getRefId() << "' was not found in container store to remove"; + throw std::runtime_error(error.str()); + } + else if (removed < static_cast(count)) + { + error << "Not enough items '" << item.mBase.getCellRef().getRefId() << "' in the stack to remove (" + << static_cast(count) << " requested, " << removed << " found)"; + throw std::runtime_error(error.str()); + } + } + + MWWorld::Ptr InventoryItemModel::moveItem( + const ItemStack& item, size_t count, ItemModel* otherModel, bool allowAutoEquip) { - MWWorld::Ptr item = *it; + // Can't move conjured items: This is a general fix that also takes care of issues with taking conjured items + // via the 'Take All' button. + if (item.mFlags & ItemStack::Flag_Bound) + return MWWorld::Ptr(); - if (!item.getClass().showsInInventory(item)) - continue; + return ItemModel::moveItem(item, count, otherModel, allowAutoEquip); + } - ItemStack newItem (item, this, item.getRefData().getCount()); + void InventoryItemModel::update() + { + MWWorld::ContainerStore& store = mActor.getClass().getContainerStore(mActor); - if (mActor.getClass().hasInventoryStore(mActor)) + mItems.clear(); + + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { - MWWorld::InventoryStore& invStore = mActor.getClass().getInventoryStore(mActor); - if (invStore.isEquipped(newItem.mBase)) - newItem.mType = ItemStack::Type_Equipped; - } + MWWorld::Ptr item = *it; + + if (!item.getClass().showsInInventory(item)) + continue; + + ItemStack newItem(item, this, item.getCellRef().getCount()); + + if (mActor.getClass().hasInventoryStore(mActor)) + { + MWWorld::InventoryStore& invStore = mActor.getClass().getInventoryStore(mActor); + if (invStore.isEquipped(newItem.mBase)) + newItem.mType = ItemStack::Type_Equipped; + } - mItems.push_back(newItem); + mItems.push_back(newItem); + } } -} -bool InventoryItemModel::onTakeItem(const MWWorld::Ptr &item, int count) -{ - // Looting a dead corpse is considered OK - if (mActor.getClass().isActor() && mActor.getClass().getCreatureStats(mActor).isDead()) - return true; + bool InventoryItemModel::onTakeItem(const MWWorld::Ptr& item, int count) + { + // Looting a dead corpse is considered OK + if (mActor.getClass().isActor() && mActor.getClass().getCreatureStats(mActor).isDead()) + return true; - MWWorld::Ptr player = MWMechanics::getPlayer(); - MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item, mActor, count); + MWWorld::Ptr player = MWMechanics::getPlayer(); + MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item, mActor, count); - return true; -} + return true; + } + + bool InventoryItemModel::usesContainer(const MWWorld::Ptr& container) + { + return mActor == container; + } } diff --git a/apps/openmw/mwgui/inventoryitemmodel.hpp b/apps/openmw/mwgui/inventoryitemmodel.hpp index 30d17f3e6c3..560d1a0fbd2 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.hpp +++ b/apps/openmw/mwgui/inventoryitemmodel.hpp @@ -9,24 +9,29 @@ namespace MWGui class InventoryItemModel : public ItemModel { public: - InventoryItemModel (const MWWorld::Ptr& actor); + InventoryItemModel(const MWWorld::Ptr& actor); - ItemStack getItem (ModelIndex index) override; - ModelIndex getIndex (ItemStack item) override; + ItemStack getItem(ModelIndex index) override; + ModelIndex getIndex(const ItemStack& item) override; size_t getItemCount() override; - bool onTakeItem(const MWWorld::Ptr &item, int count) override; + bool onTakeItem(const MWWorld::Ptr& item, int count) override; - MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; - void removeItem (const ItemStack& item, size_t count) override; + MWWorld::Ptr addItem(const ItemStack& item, size_t count, bool allowAutoEquip = true) override; + MWWorld::Ptr copyItem(const ItemStack& item, size_t count, bool allowAutoEquip = true) override; + void removeItem(const ItemStack& item, size_t count) override; /// Move items from this model to \a otherModel. - MWWorld::Ptr moveItem (const ItemStack& item, size_t count, ItemModel* otherModel) override; + MWWorld::Ptr moveItem( + const ItemStack& item, size_t count, ItemModel* otherModel, bool allowAutoEquip = true) override; void update() override; + bool usesContainer(const MWWorld::Ptr& container) override; + protected: MWWorld::Ptr mActor; + private: std::vector mItems; }; diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 402f7656cb3..e2072e229fe 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -3,49 +3,50 @@ #include #include -#include -#include -#include -#include #include #include +#include +#include +#include +#include #include -#include +#include #include -#include +#include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/inventorystore.hpp" +#include "../mwworld/action.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/actionequip.hpp" +#include "../mwworld/inventorystore.hpp" #include "../mwmechanics/actorutil.hpp" -#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" -#include "itemview.hpp" +#include "countdialog.hpp" +#include "draganddrop.hpp" #include "inventoryitemmodel.hpp" +#include "itemview.hpp" +#include "settings.hpp" #include "sortfilteritemmodel.hpp" +#include "tooltips.hpp" #include "tradeitemmodel.hpp" -#include "countdialog.hpp" #include "tradewindow.hpp" -#include "draganddrop.hpp" -#include "widgets.hpp" -#include "tooltips.hpp" namespace { bool isRightHandWeapon(const MWWorld::Ptr& item) { - if (item.getClass().getTypeName() != typeid(ESM::Weapon).name()) + if (item.getClass().getType() != ESM::Weapon::sRecordId) return false; std::vector equipmentSlots = item.getClass().getEquipmentSlots(item).first; return (!equipmentSlots.empty() && equipmentSlots.front() == MWWorld::InventoryStore::Slot_CarriedRight); @@ -55,8 +56,26 @@ namespace namespace MWGui { + namespace + { + WindowSettingValues getModeSettings(GuiMode mode) + { + switch (mode) + { + case GM_Container: + return makeInventoryContainerWindowSettingValues(); + case GM_Companion: + return makeInventoryCompanionWindowSettingValues(); + case GM_Barter: + return makeInventoryBarterWindowSettingValues(); + default: + return makeInventoryWindowSettingValues(); + } + } + } - InventoryWindow::InventoryWindow(DragAndDrop* dragAndDrop, osg::Group* parent, Resource::ResourceSystem* resourceSystem) + InventoryWindow::InventoryWindow( + DragAndDrop* dragAndDrop, osg::Group* parent, Resource::ResourceSystem* resourceSystem) : WindowPinnableBase("openmw_inventory_window.layout") , mDragAndDrop(dragAndDrop) , mSelectedItem(-1) @@ -65,14 +84,16 @@ namespace MWGui , mGuiMode(GM_Inventory) , mLastXSize(0) , mLastYSize(0) - , mPreview(new MWRender::InventoryPreview(parent, resourceSystem, MWMechanics::getPlayer())) + , mPreview(std::make_unique(parent, resourceSystem, MWMechanics::getPlayer())) , mTrading(false) , mUpdateTimer(0.f) { - mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); + mPreviewTexture + = std::make_unique(mPreview->getTexture(), mPreview->getTextureStateSet()); mPreview->rebuild(); - mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); + mMainWidget->castType()->eventWindowChangeCoord + += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); getWidget(mAvatar, "Avatar"); getWidget(mAvatarImage, "AvatarImage"); @@ -113,27 +134,28 @@ namespace MWGui { const float aspect = 0.5; // fixed aspect ratio for the avatar image int leftPaneWidth = static_cast((mMainWidget->getSize().height - 44 - mArmorRating->getHeight()) * aspect); - mLeftPane->setSize( leftPaneWidth, mMainWidget->getSize().height-44 ); - mRightPane->setCoord( mLeftPane->getPosition().left + leftPaneWidth + 4, - mRightPane->getPosition().top, - mMainWidget->getSize().width - 12 - leftPaneWidth - 15, - mMainWidget->getSize().height-44 ); + mLeftPane->setSize(leftPaneWidth, mMainWidget->getSize().height - 44); + mRightPane->setCoord(mLeftPane->getPosition().left + leftPaneWidth + 4, mRightPane->getPosition().top, + mMainWidget->getSize().width - 12 - leftPaneWidth - 15, mMainWidget->getSize().height - 44); } void InventoryWindow::updatePlayer() { - mPtr = MWBase::Environment::get().getWorld ()->getPlayerPtr(); - mTradeModel = new TradeItemModel(new InventoryItemModel(mPtr), MWWorld::Ptr()); + mPtr = MWBase::Environment::get().getWorld()->getPlayerPtr(); + auto tradeModel = std::make_unique(std::make_unique(mPtr), MWWorld::Ptr()); + mTradeModel = tradeModel.get(); if (mSortModel) // reuse existing SortModel when possible to keep previous category/filter settings - mSortModel->setSourceModel(mTradeModel); + mSortModel->setSourceModel(std::move(tradeModel)); else - mSortModel = new SortFilterItemModel(mTradeModel); + { + auto sortModel = std::make_unique(std::move(tradeModel)); + mSortModel = sortModel.get(); + mItemView->setModel(std::move(sortModel)); + } mSortModel->setNameFilter(mFilterEdit->getCaption()); - mItemView->setModel(mSortModel); - mFilterAll->setStateSelected(true); mFilterWeapon->setStateSelected(false); mFilterApparel->setStateSelected(false); @@ -163,24 +185,18 @@ namespace MWGui void InventoryWindow::toggleMaximized() { - std::string setting = getModeSetting(); - - bool maximized = !Settings::Manager::getBool(setting + " maximized", "Windows"); - if (maximized) - setting += " maximized"; + const WindowSettingValues settings = getModeSettings(mGuiMode); + const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mRegular : settings.mMaximized; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - float x = Settings::Manager::getFloat(setting + " x", "Windows") * float(viewSize.width); - float y = Settings::Manager::getFloat(setting + " y", "Windows") * float(viewSize.height); - float w = Settings::Manager::getFloat(setting + " w", "Windows") * float(viewSize.width); - float h = Settings::Manager::getFloat(setting + " h", "Windows") * float(viewSize.height); + const float x = rect.mX * viewSize.width; + const float y = rect.mY * viewSize.height; + const float w = rect.mW * viewSize.width; + const float h = rect.mH * viewSize.height; MyGUI::Window* window = mMainWidget->castType(); window->setCoord(x, y, w, h); - if (maximized) - Settings::Manager::setBool(setting, "Windows", maximized); - else - Settings::Manager::setBool(setting + " maximized", "Windows", maximized); + settings.mIsMaximized.set(!settings.mIsMaximized); adjustPanes(); updatePreviewSize(); @@ -189,17 +205,14 @@ namespace MWGui void InventoryWindow::setGuiMode(GuiMode mode) { mGuiMode = mode; - std::string setting = getModeSetting(); + const WindowSettingValues settings = getModeSettings(mGuiMode); setPinButtonVisible(mode == GM_Inventory); - if (Settings::Manager::getBool(setting + " maximized", "Windows")) - setting += " maximized"; + const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mMaximized : settings.mRegular; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - MyGUI::IntPoint pos(static_cast(Settings::Manager::getFloat(setting + " x", "Windows") * viewSize.width), - static_cast(Settings::Manager::getFloat(setting + " y", "Windows") * viewSize.height)); - MyGUI::IntSize size(static_cast(Settings::Manager::getFloat(setting + " w", "Windows") * viewSize.width), - static_cast(Settings::Manager::getFloat(setting + " h", "Windows") * viewSize.height)); + MyGUI::IntPoint pos(static_cast(rect.mX * viewSize.width), static_cast(rect.mY * viewSize.height)); + MyGUI::IntSize size(static_cast(rect.mW * viewSize.width), static_cast(rect.mH * viewSize.height)); bool needUpdate = (size.width != mMainWidget->getWidth() || size.height != mMainWidget->getHeight()); @@ -233,12 +246,12 @@ namespace MWGui mDragAndDrop->drop(mTradeModel, mItemView); } - void InventoryWindow::onItemSelected (int index) + void InventoryWindow::onItemSelected(int index) { - onItemSelectedFromSourceModel (mSortModel->mapToSource(index)); + onItemSelectedFromSourceModel(mSortModel->mapToSource(index)); } - void InventoryWindow::onItemSelectedFromSourceModel (int index) + void InventoryWindow::onItemSelectedFromSourceModel(int index) { if (mDragAndDrop->mIsOnDragAndDrop) { @@ -247,7 +260,7 @@ namespace MWGui } const ItemStack& item = mTradeModel->getItem(index); - std::string sound = item.mBase.getClass().getDownSoundId(item.mBase); + const ESM::RefId& sound = item.mBase.getClass().getDownSoundId(item.mBase); MWWorld::Ptr object = item.mBase; int count = item.mCount; @@ -271,8 +284,7 @@ namespace MWGui if (!object.getClass().canSell(object, services)) { MWBase::Environment::get().getWindowManager()->playSound(sound); - MWBase::Environment::get().getWindowManager()-> - messageBox("#{sBarterDialog4}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog4}"); return; } } @@ -280,7 +292,7 @@ namespace MWGui // If we unequip weapon during attack, it can lead to unexpected behaviour if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(mPtr)) { - bool isWeapon = item.mBase.getTypeName() == typeid(ESM::Weapon).name(); + bool isWeapon = item.mBase.getType() == ESM::Weapon::sRecordId; MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); if (isWeapon && invStore.isEquipped(item.mBase)) @@ -294,7 +306,8 @@ namespace MWGui { CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); std::string message = mTrading ? "#{sQuanityMenuMessage01}" : "#{sTake}"; - std::string name = object.getClass().getName(object) + MWGui::ToolTips::getSoulString(object.getCellRef()); + std::string name{ object.getClass().getName(object) }; + name += MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, message, count); dialog->eventOkClicked.clear(); if (mTrading) @@ -307,9 +320,9 @@ namespace MWGui { mSelectedItem = index; if (mTrading) - sellItem (nullptr, count); + sellItem(nullptr, count); else - dragItem (nullptr, count); + dragItem(nullptr, count); } } @@ -319,17 +332,17 @@ namespace MWGui if (item.mType == ItemStack::Type_Equipped) { MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::Ptr newStack = *invStore.unequipItemQuantity(item.mBase, mPtr, count); + MWWorld::Ptr newStack = *invStore.unequipItemQuantity(item.mBase, count); // The unequipped item was re-stacked. We have to update the index // since the item pointed does not exist anymore. if (item.mBase != newStack) { - updateItemView(); // Unequipping can produce a new stack, not yet in the window... + updateItemView(); // Unequipping can produce a new stack, not yet in the window... // newIndex will store the index of the ItemStack the item was stacked on int newIndex = -1; - for (size_t i=0; i < mTradeModel->getItemCount(); ++i) + for (size_t i = 0; i < mTradeModel->getItemCount(); ++i) { if (mTradeModel->getItem(i).mBase == newStack) { @@ -357,7 +370,7 @@ namespace MWGui { ensureSelectedItemUnequipped(count); const ItemStack& item = mTradeModel->getItem(mSelectedItem); - std::string sound = item.mBase.getClass().getUpSoundId(item.mBase); + const ESM::RefId& sound = item.mBase.getClass().getUpSoundId(item.mBase); MWBase::Environment::get().getWindowManager()->playSound(sound); if (item.mType == ItemStack::Type_Barter) @@ -402,44 +415,20 @@ namespace MWGui adjustPanes(); } - std::string InventoryWindow::getModeSetting() const - { - std::string setting = "inventory"; - switch(mGuiMode) - { - case GM_Container: - setting += " container"; - break; - case GM_Companion: - setting += " companion"; - break; - case GM_Barter: - setting += " barter"; - break; - default: - break; - } - - return setting; - } - void InventoryWindow::onWindowResize(MyGUI::Window* _sender) { + WindowBase::clampWindowCoordinates(_sender); + adjustPanes(); - std::string setting = getModeSetting(); + const WindowSettingValues settings = getModeSettings(mGuiMode); MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - float x = _sender->getPosition().left / float(viewSize.width); - float y = _sender->getPosition().top / float(viewSize.height); - float w = _sender->getSize().width / float(viewSize.width); - float h = _sender->getSize().height / float(viewSize.height); - Settings::Manager::setFloat(setting + " x", "Windows", x); - Settings::Manager::setFloat(setting + " y", "Windows", y); - Settings::Manager::setFloat(setting + " w", "Windows", w); - Settings::Manager::setFloat(setting + " h", "Windows", h); - bool maximized = Settings::Manager::getBool(setting + " maximized", "Windows"); - if (maximized) - Settings::Manager::setBool(setting + " maximized", "Windows", false); + + settings.mRegular.mX.set(_sender->getPosition().left / static_cast(viewSize.width)); + settings.mRegular.mY.set(_sender->getPosition().top / static_cast(viewSize.height)); + settings.mRegular.mW.set(_sender->getSize().width / static_cast(viewSize.width)); + settings.mRegular.mH.set(_sender->getSize().height / static_cast(viewSize.height)); + settings.mIsMaximized.set(false); if (mMainWidget->getSize().width != mLastXSize || mMainWidget->getSize().height != mLastYSize) { @@ -453,22 +442,20 @@ namespace MWGui void InventoryWindow::updateArmorRating() { - mArmorRating->setCaptionWithReplacing ("#{sArmor}: " - + MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); + mArmorRating->setCaptionWithReplacing( + "#{sArmor}: " + MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); if (mArmorRating->getTextSize().width > mArmorRating->getSize().width) - mArmorRating->setCaptionWithReplacing (MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); + mArmorRating->setCaptionWithReplacing( + MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); } void InventoryWindow::updatePreviewSize() { - MyGUI::IntSize size = mAvatarImage->getSize(); - int width = std::min(mPreview->getTextureWidth(), size.width); - int height = std::min(mPreview->getTextureHeight(), size.height); - float scalingFactor = MWBase::Environment::get().getWindowManager()->getScalingFactor(); - mPreview->setViewport(int(width*scalingFactor), int(height*scalingFactor)); - - mAvatarImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, - width*scalingFactor/float(mPreview->getTextureWidth()), height*scalingFactor/float(mPreview->getTextureHeight()))); + const MyGUI::IntSize viewport = getPreviewViewportSize(); + mPreview->setViewport(viewport.width, viewport.height); + mAvatarImage->getSubWidgetMain()->_setUVSet( + MyGUI::FloatRect(0.f, 0.f, viewport.width / float(mPreview->getTextureWidth()), + viewport.height / float(mPreview->getTextureHeight()))); } void InventoryWindow::onNameFilterChanged(MyGUI::EditBox* _sender) @@ -502,7 +489,7 @@ namespace MWGui void InventoryWindow::onPinToggled() { - Settings::Manager::setBool("inventory pin", "Windows", mPinned); + Settings::windows().mInventoryPin.set(mPinned); MWBase::Environment::get().getWindowManager()->setWeaponVisibility(!mPinned); } @@ -515,9 +502,9 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Inventory); } - void InventoryWindow::useItem(const MWWorld::Ptr &ptr, bool force) + void InventoryWindow::useItem(const MWWorld::Ptr& ptr, bool force) { - const std::string& script = ptr.getClass().getScript(ptr); + const ESM::RefId& script = ptr.getClass().getScript(ptr); if (!script.empty()) { // Don't try to equip the item if PCSkipEquip is set to 1 @@ -531,7 +518,8 @@ namespace MWGui MWWorld::Ptr player = MWMechanics::getPlayer(); - // early-out for items that need to be equipped, but can't be equipped: we don't want to set OnPcEquip in that case + // 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()) { if (ptr.getClass().hasItemHealth(ptr) && ptr.getCellRef().getCharge() == 0) @@ -543,7 +531,7 @@ namespace MWGui if (!force) { - std::pair canEquip = ptr.getClass().canBeEquipped(ptr, player); + auto canEquip = ptr.getClass().canBeEquipped(ptr, player); if (canEquip.first == 0) { @@ -558,18 +546,32 @@ namespace MWGui if (!script.empty()) { // Ingredients, books and repair hammers must not have OnPCEquip set to 1 here - const std::string& type = ptr.getTypeName(); - bool isBook = type == typeid(ESM::Book).name(); - if (!isBook && type != typeid(ESM::Ingredient).name() && type != typeid(ESM::Repair).name()) + auto type = ptr.getType(); + bool isBook = type == ESM::Book::sRecordId; + if (!isBook && type != ESM::Ingredient::sRecordId && type != ESM::Repair::sRecordId) ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1); // Books must have PCSkipEquip set to 1 instead else if (isBook) ptr.getRefData().getLocals().setVarByInt(script, "pcskipequip", 1); } - std::shared_ptr action = ptr.getClass().use(ptr, force); + std::unique_ptr action = ptr.getClass().use(ptr, force); action->execute(player); + // Handles partial equipping (final part) + if (mEquippedStackableCount.has_value()) + { + // 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(); + } + if (isVisible()) { mItemView->update(); @@ -590,14 +592,30 @@ namespace MWGui if (mDragAndDrop->mSourceModel != mTradeModel) { // Move item to the player's inventory - ptr = mDragAndDrop->mSourceModel->moveItem(mDragAndDrop->mItem, mDragAndDrop->mDraggedCount, mTradeModel); + ptr = mDragAndDrop->mSourceModel->moveItem( + 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; } - useItem(ptr); + 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.getTypeName() == typeid(ESM::Potion).name() || - ptr.getTypeName() == typeid(ESM::Ingredient).name()) + // 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. @@ -608,14 +626,15 @@ namespace MWGui } else { - MyGUI::IntPoint mousePos = MyGUI::InputManager::getInstance ().getLastPressedPosition (MyGUI::MouseButton::Left); - MyGUI::IntPoint relPos = mousePos - mAvatarImage->getAbsolutePosition (); + MyGUI::IntPoint mousePos + = MyGUI::InputManager::getInstance().getLastPressedPosition(MyGUI::MouseButton::Left); + MyGUI::IntPoint relPos = mousePos - mAvatarImage->getAbsolutePosition(); - MWWorld::Ptr itemSelected = getAvatarSelectedItem (relPos.left, relPos.top); - if (itemSelected.isEmpty ()) + MWWorld::Ptr itemSelected = getAvatarSelectedItem(relPos.left, relPos.top); + if (itemSelected.isEmpty()) return; - for (size_t i=0; i < mTradeModel->getItemCount (); ++i) + for (size_t i = 0; i < mTradeModel->getItemCount(); ++i) { if (mTradeModel->getItem(i).mBase == itemSelected) { @@ -629,21 +648,14 @@ namespace MWGui MWWorld::Ptr InventoryWindow::getAvatarSelectedItem(int x, int y) { - // convert to OpenGL lower-left origin - y = (mAvatarImage->getHeight()-1) - y; - - // Scale coordinates - float scalingFactor = MWBase::Environment::get().getWindowManager()->getScalingFactor(); - x = static_cast(x*scalingFactor); - y = static_cast(y*scalingFactor); - - int slot = mPreview->getSlotSelected (x, y); + const osg::Vec2f viewport_coords = mapPreviewWindowToViewport(x, y); + int slot = mPreview->getSlotSelected(viewport_coords.x(), viewport_coords.y()); if (slot == -1) return MWWorld::Ptr(); MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); - if(invStore.getSlot(slot) != invStore.end()) + if (invStore.getSlot(slot) != invStore.end()) { MWWorld::Ptr item = *invStore.getSlot(slot); if (!item.getClass().showsInInventory(item)) @@ -702,66 +714,67 @@ namespace MWGui // update the spell window just in case new enchanted items were added to inventory MWBase::Environment::get().getWindowManager()->updateSpellWindow(); - MWBase::Environment::get().getMechanicsManager()->updateMagicEffects( - MWMechanics::getPlayer()); + MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(MWMechanics::getPlayer()); dirtyPreview(); } - void InventoryWindow::pickUpObject (MWWorld::Ptr object) + void InventoryWindow::pickUpObject(MWWorld::Ptr object) { // If the inventory is not yet enabled, don't pick anything up if (!MWBase::Environment::get().getWindowManager()->isAllowed(GW_Inventory)) return; // make sure the object is of a type that can be picked up - std::string type = object.getTypeName(); - if ( (type != typeid(ESM::Apparatus).name()) - && (type != typeid(ESM::Armor).name()) - && (type != typeid(ESM::Book).name()) - && (type != typeid(ESM::Clothing).name()) - && (type != typeid(ESM::Ingredient).name()) - && (type != typeid(ESM::Light).name()) - && (type != typeid(ESM::Miscellaneous).name()) - && (type != typeid(ESM::Lockpick).name()) - && (type != typeid(ESM::Probe).name()) - && (type != typeid(ESM::Repair).name()) - && (type != typeid(ESM::Weapon).name()) - && (type != typeid(ESM::Potion).name())) + auto type = object.getType(); + if ((type != ESM::Apparatus::sRecordId) && (type != ESM::Armor::sRecordId) && (type != ESM::Book::sRecordId) + && (type != ESM::Clothing::sRecordId) && (type != ESM::Ingredient::sRecordId) + && (type != ESM::Light::sRecordId) && (type != ESM::Miscellaneous::sRecordId) + && (type != ESM::Lockpick::sRecordId) && (type != ESM::Probe::sRecordId) && (type != ESM::Repair::sRecordId) + && (type != ESM::Weapon::sRecordId) && (type != ESM::Potion::sRecordId)) return; // An object that can be picked up must have a tooltip. if (!object.getClass().hasToolTip(object)) return; - int count = object.getRefData().getCount(); + int count = object.getCellRef().getCount(); if (object.getClass().isGold(object)) count *= object.getClass().getValue(object); MWWorld::Ptr player = MWMechanics::getPlayer(); MWBase::Environment::get().getWorld()->breakInvisibility(player); - + if (!object.getRefData().activate()) return; + // Player must not be paralyzed, knocked down, or dead to pick up an item. + const MWMechanics::NpcStats& playerStats = player.getClass().getNpcStats(player); + if (playerStats.isParalyzed() || playerStats.getKnockedDown() || playerStats.isDead()) + return; + MWBase::Environment::get().getMechanicsManager()->itemTaken(player, object, MWWorld::Ptr(), count); // add to player inventory // can't use ActionTake here because we need an MWWorld::Ptr to the newly inserted object - MWWorld::Ptr newObject = *player.getClass().getContainerStore (player).add (object, object.getRefData().getCount(), player); + MWWorld::Ptr newObject = *player.getClass().getContainerStore(player).add(object, count); // remove from world - MWBase::Environment::get().getWorld()->deleteObject (object); + MWBase::Environment::get().getWorld()->deleteObject(object); // get ModelIndex to the item mTradeModel->update(); - size_t i=0; - for (; igetItemCount(); ++i) + size_t i = 0; + for (; i < mTradeModel->getItemCount(); ++i) { if (mTradeModel->getItem(i).mBase == newObject) break; } if (i == mTradeModel->getItemCount()) throw std::runtime_error("Added item not found"); + + if (mDragAndDrop->mIsOnDragAndDrop) + mDragAndDrop->finish(); + mDragAndDrop->startDrag(i, mSortModel, mTradeModel, mItemView, count); MWBase::Environment::get().getWindowManager()->updateSpellWindow(); @@ -774,20 +787,19 @@ namespace MWGui if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player)) return; - const MWMechanics::CreatureStats &stats = player.getClass().getCreatureStats(player); - bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); - if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) + const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); + if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) return; ItemModel::ModelIndex selected = -1; // not using mSortFilterModel as we only need sorting, not filtering - SortFilterItemModel model(new InventoryItemModel(player)); + SortFilterItemModel model(std::make_unique(player)); model.setSortByType(false); model.update(); if (model.getItemCount() == 0) return; - for (ItemModel::ModelIndex i=0; irebuild(); } + + MyGUI::IntSize InventoryWindow::getPreviewViewportSize() const + { + const MyGUI::IntSize previewWindowSize = mAvatarImage->getSize(); + const float scale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + + return MyGUI::IntSize(std::min(mPreview->getTextureWidth(), previewWindowSize.width * scale), + std::min(mPreview->getTextureHeight(), previewWindowSize.height * scale)); + } + + osg::Vec2f InventoryWindow::mapPreviewWindowToViewport(int x, int y) const + { + const MyGUI::IntSize previewWindowSize = mAvatarImage->getSize(); + const float normalisedX = x / std::max(1.0f, previewWindowSize.width); + const float normalisedY = y / std::max(1.0f, previewWindowSize.height); + + const MyGUI::IntSize viewport = getPreviewViewportSize(); + return osg::Vec2f(normalisedX * float(viewport.width - 1), (1.0 - normalisedY) * float(viewport.height - 1)); + } } diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index 214245767bf..9fc77ceec5c 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -1,11 +1,11 @@ #ifndef MGUI_Inventory_H #define MGUI_Inventory_H -#include "windowpinnablebase.hpp" #include "mode.hpp" +#include "windowpinnablebase.hpp" -#include "../mwworld/ptr.hpp" #include "../mwrender/characterpreview.hpp" +#include "../mwworld/ptr.hpp" namespace osg { @@ -32,108 +32,113 @@ namespace MWGui class InventoryWindow : public WindowPinnableBase { - public: - InventoryWindow(DragAndDrop* dragAndDrop, osg::Group* parent, Resource::ResourceSystem* resourceSystem); + public: + InventoryWindow(DragAndDrop* dragAndDrop, osg::Group* parent, Resource::ResourceSystem* resourceSystem); + + void onOpen() override; + + /// start trading, disables item drag&drop + void setTrading(bool trading); - void onOpen() override; + void onFrame(float dt) override; - /// start trading, disables item drag&drop - void setTrading(bool trading); + void pickUpObject(MWWorld::Ptr object); - void onFrame(float dt) override; + MWWorld::Ptr getAvatarSelectedItem(int x, int y); - void pickUpObject (MWWorld::Ptr object); + void rebuildAvatar(); - MWWorld::Ptr getAvatarSelectedItem(int x, int y); + SortFilterItemModel* getSortFilterModel(); + TradeItemModel* getTradeModel(); + ItemModel* getModel(); - void rebuildAvatar(); + void updateItemView(); - SortFilterItemModel* getSortFilterModel(); - TradeItemModel* getTradeModel(); - ItemModel* getModel(); + void updatePlayer(); - void updateItemView(); + void clear() override; - void updatePlayer(); + void useItem(const MWWorld::Ptr& ptr, bool force = false); - void clear() override; + void setGuiMode(GuiMode mode); - void useItem(const MWWorld::Ptr& ptr, bool force=false); + /// Cycle to previous/next weapon + void cycle(bool next); - void setGuiMode(GuiMode mode); + std::string_view getWindowIdForLua() const override { return "Inventory"; } - /// Cycle to previous/next weapon - void cycle(bool next); + protected: + void onTitleDoubleClicked() override; - protected: - void onTitleDoubleClicked() override; + private: + DragAndDrop* mDragAndDrop; - private: - DragAndDrop* mDragAndDrop; + int mSelectedItem; + std::optional mEquippedStackableCount; - int mSelectedItem; + MWWorld::Ptr mPtr; - MWWorld::Ptr mPtr; + MWGui::ItemView* mItemView; + SortFilterItemModel* mSortModel; + TradeItemModel* mTradeModel; - MWGui::ItemView* mItemView; - SortFilterItemModel* mSortModel; - TradeItemModel* mTradeModel; + MyGUI::Widget* mAvatar; + MyGUI::ImageBox* mAvatarImage; + MyGUI::TextBox* mArmorRating; + Widgets::MWDynamicStat* mEncumbranceBar; - MyGUI::Widget* mAvatar; - MyGUI::ImageBox* mAvatarImage; - MyGUI::TextBox* mArmorRating; - Widgets::MWDynamicStat* mEncumbranceBar; + MyGUI::Widget* mLeftPane; + MyGUI::Widget* mRightPane; - MyGUI::Widget* mLeftPane; - MyGUI::Widget* mRightPane; + MyGUI::Button* mFilterAll; + MyGUI::Button* mFilterWeapon; + MyGUI::Button* mFilterApparel; + MyGUI::Button* mFilterMagic; + MyGUI::Button* mFilterMisc; - MyGUI::Button* mFilterAll; - MyGUI::Button* mFilterWeapon; - MyGUI::Button* mFilterApparel; - MyGUI::Button* mFilterMagic; - MyGUI::Button* mFilterMisc; - - MyGUI::EditBox* mFilterEdit; + MyGUI::EditBox* mFilterEdit; - GuiMode mGuiMode; + GuiMode mGuiMode; - int mLastXSize; - int mLastYSize; + int mLastXSize; + int mLastYSize; - std::unique_ptr mPreviewTexture; - std::unique_ptr mPreview; + std::unique_ptr mPreviewTexture; + std::unique_ptr mPreview; - bool mTrading; - float mUpdateTimer; + bool mTrading; + float mUpdateTimer; - void toggleMaximized(); + void toggleMaximized(); - void onItemSelected(int index); - void onItemSelectedFromSourceModel(int index); + void onItemSelected(int index); + void onItemSelectedFromSourceModel(int index); - void onBackgroundSelected(); + void onBackgroundSelected(); - std::string getModeSetting() const; + void sellItem(MyGUI::Widget* sender, int count); + void dragItem(MyGUI::Widget* sender, int count); - void sellItem(MyGUI::Widget* sender, int count); - void dragItem(MyGUI::Widget* sender, int count); + void onWindowResize(MyGUI::Window* _sender); + void onFilterChanged(MyGUI::Widget* _sender); + void onNameFilterChanged(MyGUI::EditBox* _sender); + void onAvatarClicked(MyGUI::Widget* _sender); + void onPinToggled() override; - void onWindowResize(MyGUI::Window* _sender); - void onFilterChanged(MyGUI::Widget* _sender); - void onNameFilterChanged(MyGUI::EditBox* _sender); - void onAvatarClicked(MyGUI::Widget* _sender); - void onPinToggled() override; + void updateEncumbranceBar(); + void notifyContentChanged(); + void dirtyPreview(); + void updatePreviewSize(); + void updateArmorRating(); - void updateEncumbranceBar(); - void notifyContentChanged(); - void dirtyPreview(); - void updatePreviewSize(); - void updateArmorRating(); + MyGUI::IntSize getPreviewViewportSize() const; + osg::Vec2f mapPreviewWindowToViewport(int x, int y) const; - void adjustPanes(); + void adjustPanes(); - /// Unequips count items from mSelectedItem, if it is equipped, and then updates mSelectedItem in case the items were re-stacked - void ensureSelectedItemUnequipped(int count); + /// Unequips count items from mSelectedItem, if it is equipped, and then updates mSelectedItem in case the items + /// were re-stacked + void ensureSelectedItemUnequipped(int count); }; } diff --git a/apps/openmw/mwgui/itemchargeview.cpp b/apps/openmw/mwgui/itemchargeview.cpp index 44fa94f3a29..02c3cc182cb 100644 --- a/apps/openmw/mwgui/itemchargeview.cpp +++ b/apps/openmw/mwgui/itemchargeview.cpp @@ -2,15 +2,17 @@ #include +#include #include -#include #include -#include +#include +#include -#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" + +#include "../mwmechanics/spellutil.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" @@ -21,8 +23,8 @@ namespace MWGui { ItemChargeView::ItemChargeView() - : mScrollView(nullptr), - mDisplayMode(DisplayMode_Health) + : mScrollView(nullptr) + , mDisplayMode(DisplayMode_Health) { } @@ -91,17 +93,20 @@ namespace MWGui Line line; line.mItemPtr = stack.mBase; - line.mText = mScrollView->createWidget("SandText", MyGUI::IntCoord(), MyGUI::Align::Default); + line.mText + = mScrollView->createWidget("SandText", MyGUI::IntCoord(), MyGUI::Align::Default); line.mText->setNeedMouseFocus(false); - line.mIcon = mScrollView->createWidget("MW_ItemIconSmall", MyGUI::IntCoord(), MyGUI::Align::Default); + line.mIcon = mScrollView->createWidget( + "MW_ItemIconSmall", MyGUI::IntCoord(), MyGUI::Align::Default); line.mIcon->setItem(line.mItemPtr); line.mIcon->setUserString("ToolTipType", "ItemPtr"); line.mIcon->setUserData(MWWorld::Ptr(line.mItemPtr)); line.mIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemChargeView::onIconClicked); line.mIcon->eventMouseWheel += MyGUI::newDelegate(this, &ItemChargeView::onMouseWheelMoved); - line.mCharge = mScrollView->createWidget("MW_ChargeBar", MyGUI::IntCoord(), MyGUI::Align::Default); + line.mCharge = mScrollView->createWidget( + "MW_ChargeBar", MyGUI::IntCoord(), MyGUI::Align::Default); line.mCharge->setNeedMouseFocus(false); updateLine(line); @@ -123,6 +128,11 @@ namespace MWGui mLines.swap(lines); + std::stable_sort(mLines.begin(), mLines.end(), + [](const MWGui::ItemChargeView::Line& a, const MWGui::ItemChargeView::Line& b) { + return Misc::StringUtils::ciLess(a.mText->getCaption(), b.mText->getCaption()); + }); + layoutWidgets(); } @@ -132,17 +142,19 @@ namespace MWGui for (Line& line : mLines) { - line.mText->setCoord(8, currentY, mScrollView->getWidth()-8, 18); + line.mText->setCoord(8, currentY, mScrollView->getWidth() - 8, 18); currentY += 19; line.mIcon->setCoord(16, currentY, 32, 32); - line.mCharge->setCoord(72, currentY+2, std::max(199, mScrollView->getWidth()-72-38), 20); + line.mCharge->setCoord(72, currentY + 2, std::max(199, mScrollView->getWidth() - 72 - 38), 20); currentY += 32 + 4; } - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden mScrollView->setVisibleVScroll(false); - mScrollView->setCanvasSize(MyGUI::IntSize(mScrollView->getWidth(), std::max(mScrollView->getHeight(), currentY))); + mScrollView->setCanvasSize( + MyGUI::IntSize(mScrollView->getWidth(), std::max(mScrollView->getHeight(), currentY))); mScrollView->setVisibleVScroll(true); } @@ -169,7 +181,8 @@ namespace MWGui void ItemChargeView::updateLine(const ItemChargeView::Line& line) { - line.mText->setCaption(line.mItemPtr.getClass().getName(line.mItemPtr)); + std::string_view name = line.mItemPtr.getClass().getName(line.mItemPtr); + line.mText->setCaption(MyGUI::UString(name)); line.mCharge->setVisible(false); switch (mDisplayMode) @@ -180,19 +193,20 @@ namespace MWGui line.mCharge->setVisible(true); line.mCharge->setValue(line.mItemPtr.getClass().getItemHealth(line.mItemPtr), - line.mItemPtr.getClass().getItemMaxHealth(line.mItemPtr)); + line.mItemPtr.getClass().getItemMaxHealth(line.mItemPtr)); break; case DisplayMode_EnchantmentCharge: - std::string enchId = line.mItemPtr.getClass().getEnchantment(line.mItemPtr); + const ESM::RefId& enchId = line.mItemPtr.getClass().getEnchantment(line.mItemPtr); if (enchId.empty()) break; - const ESM::Enchantment* ench = MWBase::Environment::get().getWorld()->getStore().get().search(enchId); + const ESM::Enchantment* ench + = MWBase::Environment::get().getESMStore()->get().search(enchId); if (!ench) break; line.mCharge->setVisible(true); line.mCharge->setValue(static_cast(line.mItemPtr.getCellRef().getEnchantmentCharge()), - ench->mData.mCharge); + MWMechanics::getEnchantmentCharge(*ench)); break; } } @@ -204,9 +218,10 @@ namespace MWGui void ItemChargeView::onMouseWheelMoved(MyGUI::Widget* /*sender*/, int rel) { - if (mScrollView->getViewOffset().top + rel*0.3f > 0) + if (mScrollView->getViewOffset().top + rel * 0.3f > 0) mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); else - mScrollView->setViewOffset(MyGUI::IntPoint(0, static_cast(mScrollView->getViewOffset().top + rel*0.3f))); + mScrollView->setViewOffset( + MyGUI::IntPoint(0, static_cast(mScrollView->getViewOffset().top + rel * 0.3f))); } } diff --git a/apps/openmw/mwgui/itemchargeview.hpp b/apps/openmw/mwgui/itemchargeview.hpp index 039dcaf4e64..f7617d37eb3 100644 --- a/apps/openmw/mwgui/itemchargeview.hpp +++ b/apps/openmw/mwgui/itemchargeview.hpp @@ -1,8 +1,8 @@ #ifndef MWGUI_ITEMCHARGEVIEW_H #define MWGUI_ITEMCHARGEVIEW_H -#include #include +#include #include @@ -24,54 +24,54 @@ namespace MWGui class ItemChargeView final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(ItemChargeView) - public: - enum DisplayMode - { - DisplayMode_Health, - DisplayMode_EnchantmentCharge - }; + public: + enum DisplayMode + { + DisplayMode_Health, + DisplayMode_EnchantmentCharge + }; - ItemChargeView(); + ItemChargeView(); - /// Register needed components with MyGUI's factory manager - static void registerComponents(); + /// Register needed components with MyGUI's factory manager + static void registerComponents(); - void initialiseOverride() override; + void initialiseOverride() override; - /// Takes ownership of \a model - void setModel(ItemModel* model); + /// Takes ownership of \a model + void setModel(ItemModel* model); - void setDisplayMode(DisplayMode type); + void setDisplayMode(DisplayMode type); - void update(); - void layoutWidgets(); - void resetScrollbars(); + void update(); + void layoutWidgets(); + void resetScrollbars(); - void setSize(const MyGUI::IntSize& value) override; - void setCoord(const MyGUI::IntCoord& value) override; + void setSize(const MyGUI::IntSize& value) override; + void setCoord(const MyGUI::IntCoord& value) override; - MyGUI::delegates::CMultiDelegate2 eventItemClicked; + MyGUI::delegates::MultiDelegate eventItemClicked; - private: - struct Line - { - MWWorld::Ptr mItemPtr; - MyGUI::TextBox* mText; - ItemWidget* mIcon; - Widgets::MWDynamicStatPtr mCharge; - }; + private: + struct Line + { + MWWorld::Ptr mItemPtr; + MyGUI::TextBox* mText; + ItemWidget* mIcon; + Widgets::MWDynamicStatPtr mCharge; + }; - void updateLine(const Line& line); + void updateLine(const Line& line); - void onIconClicked(MyGUI::Widget* sender); - void onMouseWheelMoved(MyGUI::Widget* sender, int rel); + void onIconClicked(MyGUI::Widget* sender); + void onMouseWheelMoved(MyGUI::Widget* sender, int rel); - typedef std::vector Lines; - Lines mLines; + typedef std::vector Lines; + Lines mLines; - std::unique_ptr mModel; - MyGUI::ScrollView* mScrollView; - DisplayMode mDisplayMode; + std::unique_ptr mModel; + MyGUI::ScrollView* mScrollView; + DisplayMode mDisplayMode; }; } diff --git a/apps/openmw/mwgui/itemmodel.cpp b/apps/openmw/mwgui/itemmodel.cpp index 4e4d77da47e..a4cf48fcbe0 100644 --- a/apps/openmw/mwgui/itemmodel.cpp +++ b/apps/openmw/mwgui/itemmodel.cpp @@ -9,14 +9,14 @@ namespace MWGui { - ItemStack::ItemStack(const MWWorld::Ptr &base, ItemModel *creator, size_t count) + ItemStack::ItemStack(const MWWorld::Ptr& base, ItemModel* creator, size_t count) : mType(Type_Normal) , mFlags(0) , mCreator(creator) , mCount(count) , mBase(base) { - if (base.getClass().getEnchantment(base) != "") + if (!base.getClass().getEnchantment(base).empty()) mFlags |= Flag_Enchanted; if (MWBase::Environment::get().getMechanicsManager()->isBoundItem(base)) @@ -31,18 +31,18 @@ namespace MWGui { } - bool operator == (const ItemStack& left, const ItemStack& right) + bool operator==(const ItemStack& left, const ItemStack& right) { if (left.mType != right.mType) return false; - if(left.mBase == right.mBase) + if (left.mBase == right.mBase) return true; // If one of the items is in an inventory and currently equipped, we need to check stacking both ways to be sure if (left.mBase.getContainerStore() && right.mBase.getContainerStore()) return left.mBase.getContainerStore()->stacks(left.mBase, right.mBase) - && right.mBase.getContainerStore()->stacks(left.mBase, right.mBase); + && right.mBase.getContainerStore()->stacks(left.mBase, right.mBase); if (left.mBase.getContainerStore()) return left.mBase.getContainerStore()->stacks(left.mBase, right.mBase); @@ -53,14 +53,24 @@ namespace MWGui return store.stacks(left.mBase, right.mBase); } - ItemModel::ItemModel() - { - } + ItemModel::ItemModel() {} - MWWorld::Ptr ItemModel::moveItem(const ItemStack &item, size_t count, ItemModel *otherModel) + MWWorld::Ptr ItemModel::moveItem(const ItemStack& item, size_t count, ItemModel* otherModel, bool allowAutoEquip) { - MWWorld::Ptr ret = otherModel->copyItem(item, count); - removeItem(item, count); + MWWorld::Ptr ret = MWWorld::Ptr(); + if (static_cast(item.mBase.getCellRef().getCount()) <= count) + { + // We are moving the full stack + ret = otherModel->addItem(item, count, allowAutoEquip); + removeItem(item, count); + } + else + { + // We are moving only part of the stack, so create a copy in the other model + // and then remove count from this model. + ret = otherModel->copyItem(item, count, allowAutoEquip); + removeItem(item, count); + } return ret; } @@ -69,46 +79,35 @@ namespace MWGui return true; } - bool ItemModel::onDropItem(const MWWorld::Ptr &item, int count) + bool ItemModel::onDropItem(const MWWorld::Ptr& item, int count) { return true; } - bool ItemModel::onTakeItem(const MWWorld::Ptr &item, int count) + bool ItemModel::onTakeItem(const MWWorld::Ptr& item, int count) { return true; } - - ProxyItemModel::ProxyItemModel() - : mSourceModel(nullptr) - { - } - - ProxyItemModel::~ProxyItemModel() - { - delete mSourceModel; - } - bool ProxyItemModel::allowedToUseItems() const { return mSourceModel->allowedToUseItems(); } - MWWorld::Ptr ProxyItemModel::copyItem (const ItemStack& item, size_t count, bool allowAutoEquip) + MWWorld::Ptr ProxyItemModel::copyItem(const ItemStack& item, size_t count, bool allowAutoEquip) { - return mSourceModel->copyItem (item, count, allowAutoEquip); + return mSourceModel->copyItem(item, count, allowAutoEquip); } - void ProxyItemModel::removeItem (const ItemStack& item, size_t count) + void ProxyItemModel::removeItem(const ItemStack& item, size_t count) { - mSourceModel->removeItem (item, count); + mSourceModel->removeItem(item, count); } - ItemModel::ModelIndex ProxyItemModel::mapToSource (ModelIndex index) + ItemModel::ModelIndex ProxyItemModel::mapToSource(ModelIndex index) { const ItemStack& itemToSearch = getItem(index); - for (size_t i=0; igetItemCount(); ++i) + for (size_t i = 0; i < mSourceModel->getItemCount(); ++i) { const ItemStack& item = mSourceModel->getItem(i); if (item.mBase == itemToSearch.mBase) @@ -117,10 +116,10 @@ namespace MWGui return -1; } - ItemModel::ModelIndex ProxyItemModel::mapFromSource (ModelIndex index) + ItemModel::ModelIndex ProxyItemModel::mapFromSource(ModelIndex index) { const ItemStack& itemToSearch = mSourceModel->getItem(index); - for (size_t i=0; igetIndex(item); } - void ProxyItemModel::setSourceModel(ItemModel *sourceModel) + void ProxyItemModel::setSourceModel(std::unique_ptr sourceModel) { - if (mSourceModel == sourceModel) - return; - - if (mSourceModel) - { - delete mSourceModel; - mSourceModel = nullptr; - } - - mSourceModel = sourceModel; + mSourceModel = std::move(sourceModel); } void ProxyItemModel::onClose() @@ -153,13 +143,23 @@ namespace MWGui mSourceModel->onClose(); } - bool ProxyItemModel::onDropItem(const MWWorld::Ptr &item, int count) + bool ProxyItemModel::onDropItem(const MWWorld::Ptr& item, int count) { return mSourceModel->onDropItem(item, count); } - bool ProxyItemModel::onTakeItem(const MWWorld::Ptr &item, int count) + bool ProxyItemModel::onTakeItem(const MWWorld::Ptr& item, int count) { return mSourceModel->onTakeItem(item, count); } + + MWWorld::Ptr ProxyItemModel::addItem(const ItemStack& item, size_t count, bool allowAutoEquip) + { + return mSourceModel->addItem(item, count, allowAutoEquip); + } + + bool ProxyItemModel::usesContainer(const MWWorld::Ptr& container) + { + return mSourceModel->usesContainer(container); + } } diff --git a/apps/openmw/mwgui/itemmodel.hpp b/apps/openmw/mwgui/itemmodel.hpp index e120dde0fa2..7af29878eb4 100644 --- a/apps/openmw/mwgui/itemmodel.hpp +++ b/apps/openmw/mwgui/itemmodel.hpp @@ -1,6 +1,8 @@ #ifndef MWGUI_ITEM_MODEL_H #define MWGUI_ITEM_MODEL_H +#include + #include "../mwworld/ptr.hpp" namespace MWGui @@ -11,7 +13,7 @@ namespace MWGui /// @brief A single item stack managed by an item model struct ItemStack { - ItemStack (const MWWorld::Ptr& base, ItemModel* creator, size_t count); + ItemStack(const MWWorld::Ptr& base, ItemModel* creator, size_t count); ItemStack(); ///< like operator==, only without checking mType @@ -25,8 +27,8 @@ namespace MWGui enum Flags { - Flag_Enchanted = (1<<0), - Flag_Bound = (1<<1) + Flag_Enchanted = (1 << 0), + Flag_Bound = (1 << 1) }; int mFlags; @@ -35,8 +37,7 @@ namespace MWGui MWWorld::Ptr mBase; }; - bool operator == (const ItemStack& left, const ItemStack& right); - + bool operator==(const ItemStack& left, const ItemStack& right); /// @brief The base class that all item models should derive from. class ItemModel @@ -48,63 +49,69 @@ namespace MWGui typedef int ModelIndex; // -1 means invalid index /// Throws for invalid index or out of range index - virtual ItemStack getItem (ModelIndex index) = 0; + virtual ItemStack getItem(ModelIndex index) = 0; /// The number of items in the model, this specifies the range of indices you can pass to /// the getItem function (but this range is only valid until the next call to update()) virtual size_t getItemCount() = 0; /// Returns an invalid index if the item was not found - virtual ModelIndex getIndex (ItemStack item) = 0; + virtual ModelIndex getIndex(const ItemStack& item) = 0; /// Rebuild the item model, this will invalidate existing model indices virtual void update() = 0; /// Move items from this model to \a otherModel. /// @note Derived implementations may return an empty Ptr if the move was unsuccessful. - virtual MWWorld::Ptr moveItem (const ItemStack& item, size_t count, ItemModel* otherModel); + virtual MWWorld::Ptr moveItem( + const ItemStack& item, size_t count, ItemModel* otherModel, bool allowAutoEquip = true); - virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) = 0; - virtual void removeItem (const ItemStack& item, size_t count) = 0; + virtual MWWorld::Ptr addItem(const ItemStack& item, size_t count, bool allowAutoEquip = true) = 0; + virtual MWWorld::Ptr copyItem(const ItemStack& item, size_t count, bool allowAutoEquip = true) = 0; + virtual void removeItem(const ItemStack& item, size_t count) = 0; /// Is the player allowed to use items from this item model? (default true) virtual bool allowedToUseItems() const; - virtual void onClose() - { - } - virtual bool onDropItem(const MWWorld::Ptr &item, int count); - virtual bool onTakeItem(const MWWorld::Ptr &item, int count); + virtual void onClose() {} + virtual bool onDropItem(const MWWorld::Ptr& item, int count); + virtual bool onTakeItem(const MWWorld::Ptr& item, int count); + + virtual bool usesContainer(const MWWorld::Ptr& container) = 0; private: ItemModel(const ItemModel&); ItemModel& operator=(const ItemModel&); }; - /// @brief A proxy item model can be used to filter or rearrange items from a source model (or even add new items to it). - /// The neat thing is that this does not actually alter the source model. + /// @brief A proxy item model can be used to filter or rearrange items from a source model (or even add new items to + /// it). The neat thing is that this does not actually alter the source model. class ProxyItemModel : public ItemModel { public: - ProxyItemModel(); - virtual ~ProxyItemModel(); + ProxyItemModel() = default; + virtual ~ProxyItemModel() = default; bool allowedToUseItems() const override; void onClose() override; - bool onDropItem(const MWWorld::Ptr &item, int count) override; - bool onTakeItem(const MWWorld::Ptr &item, int count) override; + bool onDropItem(const MWWorld::Ptr& item, int count) override; + bool onTakeItem(const MWWorld::Ptr& item, int count) override; - MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; - void removeItem (const ItemStack& item, size_t count) override; - ModelIndex getIndex (ItemStack item) override; + MWWorld::Ptr addItem(const ItemStack& item, size_t count, bool allowAutoEquip = true) override; + MWWorld::Ptr copyItem(const ItemStack& item, size_t count, bool allowAutoEquip = true) override; + void removeItem(const ItemStack& item, size_t count) override; + ModelIndex getIndex(const ItemStack& item) override; /// @note Takes ownership of the passed pointer. - void setSourceModel(ItemModel* sourceModel); + void setSourceModel(std::unique_ptr sourceModel); + + ModelIndex mapToSource(ModelIndex index); + ModelIndex mapFromSource(ModelIndex index); + + bool usesContainer(const MWWorld::Ptr& container) override; - ModelIndex mapToSource (ModelIndex index); - ModelIndex mapFromSource (ModelIndex index); protected: - ItemModel* mSourceModel; + std::unique_ptr mSourceModel; }; } diff --git a/apps/openmw/mwgui/itemselection.cpp b/apps/openmw/mwgui/itemselection.cpp index 23f1398eaad..4fe40ce693b 100644 --- a/apps/openmw/mwgui/itemselection.cpp +++ b/apps/openmw/mwgui/itemselection.cpp @@ -1,26 +1,25 @@ #include "itemselection.hpp" -#include #include +#include -#include "itemview.hpp" #include "inventoryitemmodel.hpp" +#include "itemview.hpp" #include "sortfilteritemmodel.hpp" namespace MWGui { - ItemSelectionDialog::ItemSelectionDialog(const std::string &label) + ItemSelectionDialog::ItemSelectionDialog(const std::string& label) : WindowModal("openmw_itemselection_dialog.layout") , mSortModel(nullptr) - , mModel(nullptr) { getWidget(mItemView, "ItemView"); mItemView->eventItemClicked += MyGUI::newDelegate(this, &ItemSelectionDialog::onSelectedItem); MyGUI::TextBox* l; getWidget(l, "Label"); - l->setCaptionWithReplacing (label); + l->setCaptionWithReplacing(label); MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); @@ -37,9 +36,9 @@ namespace MWGui void ItemSelectionDialog::openContainer(const MWWorld::Ptr& container) { - mModel = new InventoryItemModel(container); - mSortModel = new SortFilterItemModel(mModel); - mItemView->setModel(mSortModel); + auto sortModel = std::make_unique(std::make_unique(container)); + mSortModel = sortModel.get(); + mItemView->setModel(std::move(sortModel)); mItemView->resetScrollBars(); } diff --git a/apps/openmw/mwgui/itemselection.hpp b/apps/openmw/mwgui/itemselection.hpp index 6132bac7af1..fe87d7e38af 100644 --- a/apps/openmw/mwgui/itemselection.hpp +++ b/apps/openmw/mwgui/itemselection.hpp @@ -14,7 +14,6 @@ namespace MWGui { class ItemView; class SortFilterItemModel; - class InventoryItemModel; class ItemSelectionDialog : public WindowModal { @@ -23,20 +22,21 @@ namespace MWGui bool exit() override; - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Item; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate EventHandle_Item; EventHandle_Item eventItemSelected; EventHandle_Void eventDialogCanceled; - void openContainer (const MWWorld::Ptr& container); + void openContainer(const MWWorld::Ptr& container); void setCategory(int category); void setFilter(int filter); + SortFilterItemModel* getSortModel() { return mSortModel; } + private: ItemView* mItemView; SortFilterItemModel* mSortModel; - InventoryItemModel* mModel; void onSelectedItem(int index); diff --git a/apps/openmw/mwgui/itemview.cpp b/apps/openmw/mwgui/itemview.cpp index 14f2c1dd9bb..ff05a8b2d64 100644 --- a/apps/openmw/mwgui/itemview.cpp +++ b/apps/openmw/mwgui/itemview.cpp @@ -7,172 +7,162 @@ #include #include -#include "../mwworld/class.hpp" - #include "itemmodel.hpp" #include "itemwidget.hpp" namespace MWGui { -ItemView::ItemView() - : mModel(nullptr) - , mScrollView(nullptr) -{ -} - -ItemView::~ItemView() -{ - delete mModel; -} - -void ItemView::setModel(ItemModel *model) -{ - if (mModel == model) - return; + ItemView::ItemView() + : mScrollView(nullptr) + { + } - delete mModel; - mModel = model; + void ItemView::setModel(std::unique_ptr model) + { + mModel = std::move(model); - update(); -} + update(); + } -void ItemView::initialiseOverride() -{ - Base::initialiseOverride(); + void ItemView::initialiseOverride() + { + Base::initialiseOverride(); - assignWidget(mScrollView, "ScrollView"); - if (mScrollView == nullptr) - throw std::runtime_error("Item view needs a scroll view"); + assignWidget(mScrollView, "ScrollView"); + if (mScrollView == nullptr) + throw std::runtime_error("Item view needs a scroll view"); - mScrollView->setCanvasAlign(MyGUI::Align::Left | MyGUI::Align::Top); -} + mScrollView->setCanvasAlign(MyGUI::Align::Left | MyGUI::Align::Top); + } -void ItemView::layoutWidgets() -{ - if (!mScrollView->getChildCount()) - return; + void ItemView::layoutWidgets() + { + if (!mScrollView->getChildCount()) + return; - int x = 0; - int y = 0; - MyGUI::Widget* dragArea = mScrollView->getChildAt(0); - int maxHeight = mScrollView->getHeight(); + int x = 0; + int y = 0; + MyGUI::Widget* dragArea = mScrollView->getChildAt(0); + int maxHeight = mScrollView->getHeight(); - int rows = maxHeight/42; - rows = std::max(rows, 1); - bool showScrollbar = int(std::ceil(dragArea->getChildCount()/float(rows))) > mScrollView->getWidth()/42; - if (showScrollbar) - maxHeight -= 18; + int rows = maxHeight / 42; + rows = std::max(rows, 1); + bool showScrollbar = int(std::ceil(dragArea->getChildCount() / float(rows))) > mScrollView->getWidth() / 42; + if (showScrollbar) + maxHeight -= 18; - for (unsigned int i=0; igetChildCount(); ++i) - { - MyGUI::Widget* w = dragArea->getChildAt(i); + for (unsigned int i = 0; i < dragArea->getChildCount(); ++i) + { + MyGUI::Widget* w = dragArea->getChildAt(i); - w->setPosition(x, y); + w->setPosition(x, y); - y += 42; + y += 42; - if (y > maxHeight-42 && i < dragArea->getChildCount()-1) - { - x += 42; - y = 0; + if (y > maxHeight - 42 && i < dragArea->getChildCount() - 1) + { + x += 42; + y = 0; + } } + x += 42; + + MyGUI::IntSize size = MyGUI::IntSize(std::max(mScrollView->getSize().width, x), mScrollView->getSize().height); + + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden + mScrollView->setVisibleVScroll(false); + mScrollView->setVisibleHScroll(false); + mScrollView->setCanvasSize(size); + mScrollView->setVisibleVScroll(true); + mScrollView->setVisibleHScroll(true); + dragArea->setSize(size); } - x += 42; - MyGUI::IntSize size = MyGUI::IntSize(std::max(mScrollView->getSize().width, x), mScrollView->getSize().height); + void ItemView::update() + { + while (mScrollView->getChildCount()) + MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden - mScrollView->setVisibleVScroll(false); - mScrollView->setVisibleHScroll(false); - mScrollView->setCanvasSize(size); - mScrollView->setVisibleVScroll(true); - mScrollView->setVisibleHScroll(true); - dragArea->setSize(size); -} + if (!mModel) + return; -void ItemView::update() -{ - while (mScrollView->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); + mModel->update(); - if (!mModel) - return; + MyGUI::Widget* dragArea = mScrollView->createWidget( + {}, 0, 0, mScrollView->getWidth(), mScrollView->getHeight(), MyGUI::Align::Stretch); + dragArea->setNeedMouseFocus(true); + dragArea->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemView::onSelectedBackground); + dragArea->eventMouseWheel += MyGUI::newDelegate(this, &ItemView::onMouseWheelMoved); - mModel->update(); + for (ItemModel::ModelIndex i = 0; i < static_cast(mModel->getItemCount()); ++i) + { + const ItemStack& item = mModel->getItem(i); + + ItemWidget* itemWidget = dragArea->createWidget( + "MW_ItemIcon", MyGUI::IntCoord(0, 0, 42, 42), MyGUI::Align::Default); + itemWidget->setUserString("ToolTipType", "ItemModelIndex"); + itemWidget->setUserData(std::make_pair(i, mModel.get())); + ItemWidget::ItemState state = ItemWidget::None; + if (item.mType == ItemStack::Type_Barter) + state = ItemWidget::Barter; + if (item.mType == ItemStack::Type_Equipped) + state = ItemWidget::Equip; + itemWidget->setItem(item.mBase, state); + itemWidget->setCount(item.mCount); + + itemWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemView::onSelectedItem); + itemWidget->eventMouseWheel += MyGUI::newDelegate(this, &ItemView::onMouseWheelMoved); + } - MyGUI::Widget* dragArea = mScrollView->createWidget("",0,0,mScrollView->getWidth(),mScrollView->getHeight(), - MyGUI::Align::Stretch); - dragArea->setNeedMouseFocus(true); - dragArea->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemView::onSelectedBackground); - dragArea->eventMouseWheel += MyGUI::newDelegate(this, &ItemView::onMouseWheelMoved); + layoutWidgets(); + } - for (ItemModel::ModelIndex i=0; i(mModel->getItemCount()); ++i) + void ItemView::resetScrollBars() { - const ItemStack& item = mModel->getItem(i); - - ItemWidget* itemWidget = dragArea->createWidget("MW_ItemIcon", - MyGUI::IntCoord(0, 0, 42, 42), MyGUI::Align::Default); - itemWidget->setUserString("ToolTipType", "ItemModelIndex"); - itemWidget->setUserData(std::make_pair(i, mModel)); - ItemWidget::ItemState state = ItemWidget::None; - if (item.mType == ItemStack::Type_Barter) - state = ItemWidget::Barter; - if (item.mType == ItemStack::Type_Equipped) - state = ItemWidget::Equip; - itemWidget->setItem(item.mBase, state); - itemWidget->setCount(item.mCount); - - itemWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemView::onSelectedItem); - itemWidget->eventMouseWheel += MyGUI::newDelegate(this, &ItemView::onMouseWheelMoved); + mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); } - layoutWidgets(); -} - -void ItemView::resetScrollBars() -{ - mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); -} - -void ItemView::onSelectedItem(MyGUI::Widget *sender) -{ - ItemModel::ModelIndex index = (*sender->getUserData >()).first; - eventItemClicked(index); -} + void ItemView::onSelectedItem(MyGUI::Widget* sender) + { + ItemModel::ModelIndex index = (*sender->getUserData>()).first; + eventItemClicked(index); + } -void ItemView::onSelectedBackground(MyGUI::Widget *sender) -{ - eventBackgroundClicked(); -} + void ItemView::onSelectedBackground(MyGUI::Widget* sender) + { + eventBackgroundClicked(); + } -void ItemView::onMouseWheelMoved(MyGUI::Widget *_sender, int _rel) -{ - if (mScrollView->getViewOffset().left + _rel*0.3f > 0) - mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); - else - mScrollView->setViewOffset(MyGUI::IntPoint(static_cast(mScrollView->getViewOffset().left + _rel*0.3f), 0)); -} + void ItemView::onMouseWheelMoved(MyGUI::Widget* _sender, int _rel) + { + if (mScrollView->getViewOffset().left + _rel * 0.3f > 0) + mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); + else + mScrollView->setViewOffset( + MyGUI::IntPoint(static_cast(mScrollView->getViewOffset().left + _rel * 0.3f), 0)); + } -void ItemView::setSize(const MyGUI::IntSize &_value) -{ - bool changed = (_value.width != getWidth() || _value.height != getHeight()); - Base::setSize(_value); - if (changed) - layoutWidgets(); -} + void ItemView::setSize(const MyGUI::IntSize& _value) + { + bool changed = (_value.width != getWidth() || _value.height != getHeight()); + Base::setSize(_value); + if (changed) + layoutWidgets(); + } -void ItemView::setCoord(const MyGUI::IntCoord &_value) -{ - bool changed = (_value.width != getWidth() || _value.height != getHeight()); - Base::setCoord(_value); - if (changed) - layoutWidgets(); -} + void ItemView::setCoord(const MyGUI::IntCoord& _value) + { + bool changed = (_value.width != getWidth() || _value.height != getHeight()); + Base::setCoord(_value); + if (changed) + layoutWidgets(); + } -void ItemView::registerComponents() -{ - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); -} + void ItemView::registerComponents() + { + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + } } diff --git a/apps/openmw/mwgui/itemview.hpp b/apps/openmw/mwgui/itemview.hpp index 4074e55e48a..cfbc8a37ac7 100644 --- a/apps/openmw/mwgui/itemview.hpp +++ b/apps/openmw/mwgui/itemview.hpp @@ -10,19 +10,18 @@ namespace MWGui class ItemView final : public MyGUI::Widget { - MYGUI_RTTI_DERIVED(ItemView) + MYGUI_RTTI_DERIVED(ItemView) public: ItemView(); - ~ItemView() override; /// Register needed components with MyGUI's factory manager - static void registerComponents (); + static void registerComponents(); /// Takes ownership of \a model - void setModel (ItemModel* model); + void setModel(std::unique_ptr model); - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_ModelIndex; - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate EventHandle_ModelIndex; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /// Fired when an item was clicked EventHandle_ModelIndex eventItemClicked; /// Fired when the background was clicked (useful for drag and drop) @@ -40,13 +39,12 @@ namespace MWGui void setSize(const MyGUI::IntSize& _value) override; void setCoord(const MyGUI::IntCoord& _value) override; - void onSelectedItem (MyGUI::Widget* sender); - void onSelectedBackground (MyGUI::Widget* sender); + void onSelectedItem(MyGUI::Widget* sender); + void onSelectedBackground(MyGUI::Widget* sender); void onMouseWheelMoved(MyGUI::Widget* _sender, int _rel); - ItemModel* mModel; + std::unique_ptr mModel; MyGUI::ScrollView* mScrollView; - }; } diff --git a/apps/openmw/mwgui/itemwidget.cpp b/apps/openmw/mwgui/itemwidget.cpp index d2dfa827b4d..05fff2d40fd 100644 --- a/apps/openmw/mwgui/itemwidget.cpp +++ b/apps/openmw/mwgui/itemwidget.cpp @@ -6,12 +6,12 @@ #include #include -// correctIconPath +#include #include +#include #include #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" @@ -20,14 +20,31 @@ namespace std::string getCountString(int count) { if (count == 1) - return ""; + return {}; + + // With small text size we can use up to 4 characters, while with large ones - only up to 3. + if (Settings::gui().mFontSize > 16) + { + if (count > 999999999) + return MyGUI::utility::toString(count / 1000000000) + "b"; + else if (count > 99999999) + return ">9m"; + else if (count > 999999) + return MyGUI::utility::toString(count / 1000000) + "m"; + else if (count > 99999) + return ">9k"; + else if (count > 999) + return MyGUI::utility::toString(count / 1000) + "k"; + else + return MyGUI::utility::toString(count); + } if (count > 999999999) - return MyGUI::utility::toString(count/1000000000) + "b"; + return MyGUI::utility::toString(count / 1000000000) + "b"; else if (count > 999999) - return MyGUI::utility::toString(count/1000000) + "m"; + return MyGUI::utility::toString(count / 1000000) + "m"; else if (count > 9999) - return MyGUI::utility::toString(count/1000) + "k"; + return MyGUI::utility::toString(count / 1000) + "k"; else return MyGUI::utility::toString(count); } @@ -43,7 +60,6 @@ namespace MWGui , mFrame(nullptr) , mText(nullptr) { - } void ItemWidget::registerComponents() @@ -77,7 +93,7 @@ namespace MWGui mText->setCaption(getCountString(count)); } - void ItemWidget::setIcon(const std::string &icon) + void ItemWidget::setIcon(const std::string& icon) { if (mCurrentIcon != icon) { @@ -90,7 +106,7 @@ namespace MWGui } } - void ItemWidget::setFrame(const std::string &frame, const MyGUI::IntCoord &coord) + void ItemWidget::setFrame(const std::string& frame, const MyGUI::IntCoord& coord) { if (mFrame) { @@ -105,22 +121,23 @@ namespace MWGui } } - void ItemWidget::setIcon(const MWWorld::Ptr &ptr) + void ItemWidget::setIcon(const MWWorld::Ptr& ptr) { - std::string invIcon = ptr.getClass().getInventoryIcon(ptr); - if (invIcon.empty()) - invIcon = "default icon.tga"; - invIcon = MWBase::Environment::get().getWindowManager()->correctIconPath(invIcon); - if (!MWBase::Environment::get().getResourceSystem()->getVFS()->exists(invIcon)) + std::string_view icon = ptr.getClass().getInventoryIcon(ptr); + if (icon.empty()) + icon = "default icon.tga"; + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + std::string invIcon = Misc::ResourceHelpers::correctIconPath(icon, vfs); + if (!vfs->exists(invIcon)) { - Log(Debug::Error) << "Failed to open image: '" << invIcon << "' not found, falling back to 'default-icon.tga'"; - invIcon = MWBase::Environment::get().getWindowManager()->correctIconPath("default icon.tga"); + Log(Debug::Error) << "Failed to open image: '" << invIcon + << "' not found, falling back to 'default-icon.tga'"; + invIcon = Misc::ResourceHelpers::correctIconPath("default icon.tga", vfs); } setIcon(invIcon); } - - void ItemWidget::setItem(const MWWorld::Ptr &ptr, ItemState state) + void ItemWidget::setItem(const MWWorld::Ptr& ptr, ItemState state) { if (!mItem) return; @@ -128,11 +145,11 @@ namespace MWGui if (ptr.isEmpty()) { if (mFrame) - mFrame->setImageTexture(""); + mFrame->setImageTexture({}); if (mItemShadow) - mItemShadow->setImageTexture(""); - mItem->setImageTexture(""); - mText->setCaption(""); + mItemShadow->setImageTexture({}); + mItem->setImageTexture({}); + mText->setCaption({}); mCurrentIcon.clear(); mCurrentFrame.clear(); return; @@ -146,7 +163,7 @@ namespace MWGui if (state == None) { if (!isMagic) - backgroundTex = ""; + backgroundTex.clear(); } else if (state == Equip) { @@ -155,7 +172,7 @@ namespace MWGui else if (state == Barter) backgroundTex += "_barter"; - if (backgroundTex != "") + if (!backgroundTex.empty()) backgroundTex += ".dds"; float scale = 1.f; @@ -178,19 +195,19 @@ namespace MWGui } if (state == Barter && !isMagic) - setFrame(backgroundTex, MyGUI::IntCoord(2*scale,2*scale,44*scale,44*scale)); + setFrame(backgroundTex, MyGUI::IntCoord(2 * scale, 2 * scale, 44 * scale, 44 * scale)); else - setFrame(backgroundTex, MyGUI::IntCoord(0,0,44*scale,44*scale)); + setFrame(backgroundTex, MyGUI::IntCoord(0, 0, 44 * scale, 44 * scale)); setIcon(ptr); } - void SpellWidget::setSpellIcon(const std::string& icon) + void SpellWidget::setSpellIcon(std::string_view icon) { if (mFrame && !mCurrentFrame.empty()) { mCurrentFrame.clear(); - mFrame->setImageTexture(""); + mFrame->setImageTexture({}); } if (mCurrentIcon != icon) { diff --git a/apps/openmw/mwgui/itemwidget.hpp b/apps/openmw/mwgui/itemwidget.hpp index 550d7364334..63837ae92f7 100644 --- a/apps/openmw/mwgui/itemwidget.hpp +++ b/apps/openmw/mwgui/itemwidget.hpp @@ -14,12 +14,12 @@ namespace MWGui /// @brief A widget that shows an icon for an MWWorld::Ptr class ItemWidget : public MyGUI::Widget { - MYGUI_RTTI_DERIVED(ItemWidget) + MYGUI_RTTI_DERIVED(ItemWidget) public: ItemWidget(); /// Register needed components with MyGUI's factory manager - static void registerComponents (); + static void registerComponents(); enum ItemState { @@ -33,12 +33,12 @@ namespace MWGui void setCount(int count); /// \a ptr may be empty - void setItem (const MWWorld::Ptr& ptr, ItemState state = None); + void setItem(const MWWorld::Ptr& ptr, ItemState state = None); // Set icon and frame manually - void setIcon (const std::string& icon); - void setIcon (const MWWorld::Ptr& ptr); - void setFrame (const std::string& frame, const MyGUI::IntCoord& coord); + void setIcon(const std::string& icon); + void setIcon(const MWWorld::Ptr& ptr); + void setFrame(const std::string& frame, const MyGUI::IntCoord& coord); protected: void initialiseOverride() override; @@ -56,10 +56,9 @@ namespace MWGui class SpellWidget : public ItemWidget { - MYGUI_RTTI_DERIVED(SpellWidget) + MYGUI_RTTI_DERIVED(SpellWidget) public: - - void setSpellIcon (const std::string& icon); + void setSpellIcon(std::string_view icon); }; } diff --git a/apps/openmw/mwgui/jailscreen.cpp b/apps/openmw/mwgui/jailscreen.cpp index cc793073e38..c6aefdd177f 100644 --- a/apps/openmw/mwgui/jailscreen.cpp +++ b/apps/openmw/mwgui/jailscreen.cpp @@ -1,28 +1,29 @@ #include #include +#include -#include "../mwbase/windowmanager.hpp" +#include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwbase/environment.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/store.hpp" -#include "../mwworld/class.hpp" #include "jailscreen.hpp" namespace MWGui { JailScreen::JailScreen() - : WindowBase("openmw_jail_screen.layout"), - mDays(1), - mFadeTimeRemaining(0), - mTimeAdvancer(0.01f) + : WindowBase("openmw_jail_screen.layout") + , mDays(1) + , mFadeTimeRemaining(0) + , mTimeAdvancer(0.01f) { getWidget(mProgressBar, "ProgressBar"); @@ -40,7 +41,7 @@ namespace MWGui mFadeTimeRemaining = 0.5; setVisible(false); - mProgressBar->setScrollRange(100+1); + mProgressBar->setScrollRange(100 + 1); mProgressBar->setScrollPosition(0); mProgressBar->setTrackSize(0); } @@ -57,8 +58,10 @@ namespace MWGui if (mFadeTimeRemaining <= 0) { MWWorld::Ptr player = MWMechanics::getPlayer(); - MWBase::Environment::get().getWorld()->teleportToClosestMarker(player, "prisonmarker"); - MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.f); // override fade-in caused by cell transition + MWBase::Environment::get().getWorld()->teleportToClosestMarker( + player, ESM::RefId::stringRefId("prisonmarker")); + MWBase::Environment::get().getWindowManager()->fadeScreenOut( + 0.f); // override fade-in caused by cell transition setVisible(true); mTimeAdvancer.run(100); @@ -68,7 +71,8 @@ namespace MWGui void JailScreen::onJailProgressChanged(int cur, int /*total*/) { mProgressBar->setScrollPosition(0); - mProgressBar->setTrackSize(static_cast(cur / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); + mProgressBar->setTrackSize( + static_cast(cur / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); } void JailScreen::onJailFinished() @@ -82,25 +86,25 @@ namespace MWGui MWBase::Environment::get().getWorld()->advanceTime(mDays * 24); // We should not worsen corprus when in prison - for (auto& spell : player.getClass().getCreatureStats(player).getCorprusSpells()) - { - spell.second.mNextWorsening += mDays * 24; - } + player.getClass().getCreatureStats(player).getActiveSpells().skipWorsenings(mDays * 24); - std::set skills; - for (int day=0; dayget(); + std::set skills; + for (int day = 0; day < mDays; ++day) { - int skill = Misc::Rng::rollDice(ESM::Skill::Length); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Skill* skill = skillStore.searchRandom({}, prng); skills.insert(skill); - MWMechanics::SkillValue& value = player.getClass().getNpcStats(player).getSkill(skill); - if (skill == ESM::Skill::Security || skill == ESM::Skill::Sneak) - value.setBase(std::min(100.f, value.getBase()+1)); + MWMechanics::SkillValue& value = player.getClass().getNpcStats(player).getSkill(skill->mId); + if (skill->mId == ESM::Skill::Security || skill->mId == ESM::Skill::Sneak) + value.setBase(std::min(100.f, value.getBase() + 1)); else - value.setBase(std::max(0.f, value.getBase()-1)); + value.setBase(std::max(0.f, value.getBase() - 1)); } - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); std::string message; if (mDays == 1) @@ -110,20 +114,19 @@ namespace MWGui message = Misc::StringUtils::format(message, mDays); - for (const int& skill : skills) + for (const ESM::Skill* skill : skills) { - std::string skillName = gmst.find(ESM::Skill::sSkillNameIds[skill])->mValue.getString(); - int skillValue = player.getClass().getNpcStats(player).getSkill(skill).getBase(); + int skillValue = player.getClass().getNpcStats(player).getSkill(skill->mId).getBase(); std::string skillMsg = gmst.find("sNotifyMessage44")->mValue.getString(); - if (skill == ESM::Skill::Sneak || skill == ESM::Skill::Security) + if (skill->mId == ESM::Skill::Sneak || skill->mId == ESM::Skill::Security) skillMsg = gmst.find("sNotifyMessage39")->mValue.getString(); - skillMsg = Misc::StringUtils::format(skillMsg, skillName, skillValue); + skillMsg = Misc::StringUtils::format(skillMsg, skill->mName, skillValue); message += "\n" + skillMsg; } std::vector buttons; - buttons.emplace_back("#{sOk}"); + buttons.emplace_back("#{Interface:OK}"); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); } } diff --git a/apps/openmw/mwgui/jailscreen.hpp b/apps/openmw/mwgui/jailscreen.hpp index 871a861d778..3c219875974 100644 --- a/apps/openmw/mwgui/jailscreen.hpp +++ b/apps/openmw/mwgui/jailscreen.hpp @@ -1,32 +1,34 @@ #ifndef MWGUI_JAILSCREEN_H #define MWGUI_JAILSCREEN_H -#include "windowbase.hpp" #include "timeadvancer.hpp" +#include "windowbase.hpp" namespace MWGui { class JailScreen : public WindowBase { - public: - JailScreen(); - void goToJail(int days); + public: + JailScreen(); + void goToJail(int days); + + void onFrame(float dt) override; - void onFrame(float dt) override; + bool exit() override { return false; } - bool exit() override { return false; } + std::string_view getWindowIdForLua() const override { return "JailScreen"; } - private: - int mDays; + private: + int mDays; - float mFadeTimeRemaining; + float mFadeTimeRemaining; - MyGUI::ScrollBar* mProgressBar; + MyGUI::ScrollBar* mProgressBar; - void onJailProgressChanged(int cur, int total); - void onJailFinished(); + void onJailProgressChanged(int cur, int total); + void onJailFinished(); - TimeAdvancer mTimeAdvancer; + TimeAdvancer mTimeAdvancer; }; } diff --git a/apps/openmw/mwgui/journalbooks.cpp b/apps/openmw/mwgui/journalbooks.cpp index 40053b3c8a4..86b45b4863e 100644 --- a/apps/openmw/mwgui/journalbooks.cpp +++ b/apps/openmw/mwgui/journalbooks.cpp @@ -3,8 +3,8 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include #include +#include #include "textcolours.hpp" @@ -15,29 +15,30 @@ namespace MWGui::BookTypesetter::Ptr mTypesetter; MWGui::BookTypesetter::Style* mBodyStyle; - AddContent (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style) : - mTypesetter (typesetter), mBodyStyle (body_style) + AddContent(MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style) + : mTypesetter(std::move(typesetter)) + , mBodyStyle(body_style) { } }; struct AddSpan : AddContent { - AddSpan (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style) : - AddContent (typesetter, body_style) + AddSpan(MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style) + : AddContent(std::move(typesetter), body_style) { } - void operator () (intptr_t topicId, size_t begin, size_t end) + void operator()(intptr_t topicId, size_t begin, size_t end) { MWGui::BookTypesetter::Style* style = mBodyStyle; const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); if (topicId) - style = mTypesetter->createHotStyle (mBodyStyle, textColours.journalLink, textColours.journalLinkOver, - textColours.journalLinkPressed, topicId); + style = mTypesetter->createHotStyle(mBodyStyle, textColours.journalLink, textColours.journalLinkOver, + textColours.journalLinkPressed, topicId); - mTypesetter->write (style, begin, end); + mTypesetter->write(style, begin, end); } }; @@ -46,16 +47,17 @@ namespace MWGui::BookTypesetter::Ptr mTypesetter; MWGui::BookTypesetter::Style* mBodyStyle; - AddEntry (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style) : - mTypesetter (typesetter), mBodyStyle (body_style) + AddEntry(MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style) + : mTypesetter(std::move(typesetter)) + , mBodyStyle(body_style) { } - void operator () (MWGui::JournalViewModel::Entry const & entry) + void operator()(MWGui::JournalViewModel::Entry const& entry) { - mTypesetter->addContent (entry.body ()); + mTypesetter->addContent(entry.body()); - entry.visitSpans (AddSpan (mTypesetter, mBodyStyle)); + entry.visitSpans(AddSpan(mTypesetter, mBodyStyle)); } }; @@ -64,25 +66,25 @@ namespace bool mAddHeader; MWGui::BookTypesetter::Style* mHeaderStyle; - AddJournalEntry (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style, - MWGui::BookTypesetter::Style* header_style, bool add_header) : - AddEntry (typesetter, body_style), - mAddHeader (add_header), - mHeaderStyle (header_style) + AddJournalEntry(MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style, + MWGui::BookTypesetter::Style* header_style, bool add_header) + : AddEntry(std::move(typesetter), body_style) + , mAddHeader(add_header) + , mHeaderStyle(header_style) { } - void operator () (MWGui::JournalViewModel::JournalEntry const & entry) + void operator()(MWGui::JournalViewModel::JournalEntry const& entry) { if (mAddHeader) { - mTypesetter->write (mHeaderStyle, entry.timestamp ()); - mTypesetter->lineBreak (); + mTypesetter->write(mHeaderStyle, entry.timestamp()); + mTypesetter->lineBreak(); } - AddEntry::operator () (entry); + AddEntry::operator()(entry); - mTypesetter->sectionBreak (30); + mTypesetter->sectionBreak(30); } }; @@ -91,51 +93,53 @@ namespace intptr_t mContentId; MWGui::BookTypesetter::Style* mHeaderStyle; - AddTopicEntry (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style, - MWGui::BookTypesetter::Style* header_style, intptr_t contentId) : - AddEntry (typesetter, body_style), mContentId (contentId), mHeaderStyle (header_style) + AddTopicEntry(MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style, + MWGui::BookTypesetter::Style* header_style, intptr_t contentId) + : AddEntry(std::move(typesetter), body_style) + , mContentId(contentId) + , mHeaderStyle(header_style) { } - void operator () (MWGui::JournalViewModel::TopicEntry const & entry) + void operator()(MWGui::JournalViewModel::TopicEntry const& entry) { - mTypesetter->write (mBodyStyle, entry.source ()); - mTypesetter->write (mBodyStyle, 0, 3);// begin + mTypesetter->write(mBodyStyle, entry.source()); + mTypesetter->write(mBodyStyle, 0, 3); // begin - AddEntry::operator() (entry); + AddEntry::operator()(entry); - mTypesetter->selectContent (mContentId); - mTypesetter->write (mBodyStyle, 2, 3);// end quote + mTypesetter->selectContent(mContentId); + mTypesetter->write(mBodyStyle, 2, 3); // end quote - mTypesetter->sectionBreak (30); + mTypesetter->sectionBreak(30); } }; struct AddTopicName : AddContent { - AddTopicName (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* style) : - AddContent (typesetter, style) + AddTopicName(MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* style) + : AddContent(std::move(typesetter), style) { } - void operator () (MWGui::JournalViewModel::Utf8Span topicName) + void operator()(MWGui::JournalViewModel::Utf8Span topicName) { - mTypesetter->write (mBodyStyle, topicName); - mTypesetter->sectionBreak (); + mTypesetter->write(mBodyStyle, topicName); + mTypesetter->sectionBreak(); } }; struct AddQuestName : AddContent { - AddQuestName (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* style) : - AddContent (typesetter, style) + AddQuestName(MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* style) + : AddContent(std::move(typesetter), style) { } - void operator () (MWGui::JournalViewModel::Utf8Span topicName) + void operator()(MWGui::JournalViewModel::Utf8Span topicName) { - mTypesetter->write (mBodyStyle, topicName); - mTypesetter->sectionBreak (); + mTypesetter->write(mBodyStyle, topicName); + mTypesetter->sectionBreak(); } }; } @@ -143,176 +147,182 @@ namespace namespace MWGui { -MWGui::BookTypesetter::Utf8Span to_utf8_span (char const * text) -{ - typedef MWGui::BookTypesetter::Utf8Point point; + MWGui::BookTypesetter::Utf8Span to_utf8_span(std::string_view text) + { + typedef MWGui::BookTypesetter::Utf8Point point; - point begin = reinterpret_cast (text); + point begin = reinterpret_cast(text.data()); - return MWGui::BookTypesetter::Utf8Span (begin, begin + strlen (text)); -} + return MWGui::BookTypesetter::Utf8Span(begin, begin + text.length()); + } -typedef TypesetBook::Ptr book; + typedef TypesetBook::Ptr book; -JournalBooks::JournalBooks (JournalViewModel::Ptr model, ToUTF8::FromType encoding) : - mModel (model), mEncoding(encoding), mIndexPagesCount(0) -{ -} + JournalBooks::JournalBooks(JournalViewModel::Ptr model, ToUTF8::FromType encoding) + : mModel(std::move(model)) + , mEncoding(encoding) + , mIndexPagesCount(0) + { + } -book JournalBooks::createEmptyJournalBook () -{ - BookTypesetter::Ptr typesetter = createTypesetter (); + book JournalBooks::createEmptyJournalBook() + { + BookTypesetter::Ptr typesetter = createTypesetter(); - BookTypesetter::Style* header = typesetter->createStyle ("", MyGUI::Colour (0.60f, 0.00f, 0.00f)); - BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); + BookTypesetter::Style* header = typesetter->createStyle({}, MyGUI::Colour(0.60f, 0.00f, 0.00f)); + BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::Black); - typesetter->write (header, to_utf8_span ("You have no journal entries!")); - typesetter->lineBreak (); - typesetter->write (body, to_utf8_span ("You should have gone though the starting quest and got an initial quest.")); + typesetter->write(header, to_utf8_span("You have no journal entries!")); + typesetter->lineBreak(); + typesetter->write( + body, to_utf8_span("You should have gone though the starting quest and got an initial quest.")); - return typesetter->complete (); -} + return typesetter->complete(); + } -book JournalBooks::createJournalBook () -{ - BookTypesetter::Ptr typesetter = createTypesetter (); + book JournalBooks::createJournalBook() + { + BookTypesetter::Ptr typesetter = createTypesetter(); - BookTypesetter::Style* header = typesetter->createStyle ("", MyGUI::Colour (0.60f, 0.00f, 0.00f)); - BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); + BookTypesetter::Style* header = typesetter->createStyle({}, MyGUI::Colour(0.60f, 0.00f, 0.00f)); + BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::Black); - mModel->visitJournalEntries ("", AddJournalEntry (typesetter, body, header, true)); + mModel->visitJournalEntries({}, AddJournalEntry(typesetter, body, header, true)); - return typesetter->complete (); -} + return typesetter->complete(); + } -book JournalBooks::createTopicBook (uintptr_t topicId) -{ - BookTypesetter::Ptr typesetter = createTypesetter (); + book JournalBooks::createTopicBook(uintptr_t topicId) + { + BookTypesetter::Ptr typesetter = createTypesetter(); - BookTypesetter::Style* header = typesetter->createStyle ("", MyGUI::Colour (0.60f, 0.00f, 0.00f)); - BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); + BookTypesetter::Style* header = typesetter->createStyle({}, MyGUI::Colour(0.60f, 0.00f, 0.00f)); + BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::Black); - mModel->visitTopicName (topicId, AddTopicName (typesetter, header)); + mModel->visitTopicName(topicId, AddTopicName(typesetter, header)); - intptr_t contentId = typesetter->addContent (to_utf8_span (", \"")); + intptr_t contentId = typesetter->addContent(to_utf8_span(", \"")); - mModel->visitTopicEntries (topicId, AddTopicEntry (typesetter, body, header, contentId)); + mModel->visitTopicEntries(topicId, AddTopicEntry(typesetter, body, header, contentId)); - return typesetter->complete (); -} + return typesetter->complete(); + } -book JournalBooks::createQuestBook (const std::string& questName) -{ - BookTypesetter::Ptr typesetter = createTypesetter (); + book JournalBooks::createQuestBook(std::string_view questName) + { + BookTypesetter::Ptr typesetter = createTypesetter(); - BookTypesetter::Style* header = typesetter->createStyle ("", MyGUI::Colour (0.60f, 0.00f, 0.00f)); - BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); + BookTypesetter::Style* header = typesetter->createStyle({}, MyGUI::Colour(0.60f, 0.00f, 0.00f)); + BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::Black); - AddQuestName addName (typesetter, header); - addName(to_utf8_span(questName.c_str())); + AddQuestName addName(typesetter, header); + addName(to_utf8_span(questName)); - mModel->visitJournalEntries (questName, AddJournalEntry (typesetter, body, header, false)); + mModel->visitJournalEntries(questName, AddJournalEntry(typesetter, body, header, false)); - return typesetter->complete (); -} + return typesetter->complete(); + } -book JournalBooks::createTopicIndexBook () -{ - bool isRussian = (mEncoding == ToUTF8::WINDOWS_1251); + book JournalBooks::createTopicIndexBook() + { + bool isRussian = (mEncoding == ToUTF8::WINDOWS_1251); - BookTypesetter::Ptr typesetter = isRussian ? createCyrillicJournalIndex() : createLatinJournalIndex(); + BookTypesetter::Ptr typesetter = isRussian ? createCyrillicJournalIndex() : createLatinJournalIndex(); - return typesetter->complete (); -} + return typesetter->complete(); + } -BookTypesetter::Ptr JournalBooks::createLatinJournalIndex () -{ - BookTypesetter::Ptr typesetter = BookTypesetter::create (92, 260); + BookTypesetter::Ptr JournalBooks::createLatinJournalIndex() + { + BookTypesetter::Ptr typesetter = BookTypesetter::create(92, 260); - typesetter->setSectionAlignment (BookTypesetter::AlignCenter); + typesetter->setSectionAlignment(BookTypesetter::AlignCenter); - // Latin journal index always has two columns for now. - mIndexPagesCount = 2; + // Latin journal index always has two columns for now. + mIndexPagesCount = 2; - char ch = 'A'; + char ch = 'A'; + std::string buffer; - BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); - for (int i = 0; i < 26; ++i) - { - char buffer [32]; - sprintf (buffer, "( %c )", ch); + BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::Black); + for (int i = 0; i < 26; ++i) + { + buffer = "( "; + buffer += ch; + buffer += " )"; + + const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); + BookTypesetter::Style* style = typesetter->createHotStyle(body, textColours.journalTopic, + textColours.journalTopicOver, textColours.journalTopicPressed, (Utf8Stream::UnicodeChar)ch); - const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); - BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic, - textColours.journalTopicOver, - textColours.journalTopicPressed, (Utf8Stream::UnicodeChar) ch); + if (i == 13) + typesetter->sectionBreak(); - if (i == 13) - typesetter->sectionBreak (); + typesetter->write(style, to_utf8_span(buffer)); + typesetter->lineBreak(); - typesetter->write (style, to_utf8_span (buffer)); - typesetter->lineBreak (); + ch++; + } - ch++; + return typesetter; } - return typesetter; -} + BookTypesetter::Ptr JournalBooks::createCyrillicJournalIndex() + { + BookTypesetter::Ptr typesetter = BookTypesetter::create(92, 260); -BookTypesetter::Ptr JournalBooks::createCyrillicJournalIndex () -{ - BookTypesetter::Ptr typesetter = BookTypesetter::create (92, 260); + typesetter->setSectionAlignment(BookTypesetter::AlignCenter); - typesetter->setSectionAlignment (BookTypesetter::AlignCenter); + BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::Black); - BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); + // for small font size split alphabet to two columns (2x15 characers), for big font size split it to three + // colums (3x10 characters). + int sectionBreak = 10; + mIndexPagesCount = 3; + if (Settings::gui().mFontSize < 18) + { + sectionBreak = 15; + mIndexPagesCount = 2; + } - int fontHeight = MWBase::Environment::get().getWindowManager()->getFontHeight(); + unsigned char ch[3] = { 0xd0, 0x90, 0x00 }; // CYRILLIC CAPITAL A is a 0xd090 in UTF-8 - // for small font size split alphabet to two columns (2x15 characers), for big font size split it to three colums (3x10 characters). - int sectionBreak = 10; - mIndexPagesCount = 3; - if (fontHeight < 18) - { - sectionBreak = 15; - mIndexPagesCount = 2; - } + std::string buffer; - unsigned char ch[3] = {0xd0, 0x90, 0x00}; // CYRILLIC CAPITAL A is a 0xd090 in UTF-8 + for (int i = 0; i < 32; ++i) + { + buffer = "( "; + buffer += ch[0]; + buffer += ch[1]; + buffer += " )"; - for (int i = 0; i < 32; ++i) - { - char buffer [32]; - sprintf(buffer, "( %c%c )", ch[0], ch[1]); + Utf8Stream stream(ch, ch + 2); + Utf8Stream::UnicodeChar first = stream.peek(); - Utf8Stream stream ((char*) ch); - Utf8Stream::UnicodeChar first = stream.peek(); + const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); + BookTypesetter::Style* style = typesetter->createHotStyle( + body, textColours.journalTopic, textColours.journalTopicOver, textColours.journalTopicPressed, first); - const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); - BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic, - textColours.journalTopicOver, - textColours.journalTopicPressed, first); + ch[1]++; - ch[1]++; + // Words can not be started with these characters + if (i == 26 || i == 28) + continue; - // Words can not be started with these characters - if (i == 26 || i == 28) - continue; + if (i % sectionBreak == 0) + typesetter->sectionBreak(); - if (i % sectionBreak == 0) - typesetter->sectionBreak (); + typesetter->write(style, to_utf8_span(buffer)); + typesetter->lineBreak(); + } - typesetter->write (style, to_utf8_span (buffer)); - typesetter->lineBreak (); + return typesetter; } - return typesetter; -} - -BookTypesetter::Ptr JournalBooks::createTypesetter () -{ - //TODO: determine page size from layout... - return BookTypesetter::create (240, 320); -} + BookTypesetter::Ptr JournalBooks::createTypesetter() + { + // TODO: determine page size from layout... + return BookTypesetter::create(240, 320); + } } diff --git a/apps/openmw/mwgui/journalbooks.hpp b/apps/openmw/mwgui/journalbooks.hpp index 05eda6e2227..1970830eab6 100644 --- a/apps/openmw/mwgui/journalbooks.hpp +++ b/apps/openmw/mwgui/journalbooks.hpp @@ -8,29 +8,28 @@ namespace MWGui { - MWGui::BookTypesetter::Utf8Span to_utf8_span (char const * text); + MWGui::BookTypesetter::Utf8Span to_utf8_span(std::string_view text); struct JournalBooks { typedef TypesetBook::Ptr Book; JournalViewModel::Ptr mModel; - JournalBooks (JournalViewModel::Ptr model, ToUTF8::FromType encoding); + JournalBooks(JournalViewModel::Ptr model, ToUTF8::FromType encoding); - Book createEmptyJournalBook (); - Book createJournalBook (); - Book createTopicBook (uintptr_t topicId); - Book createTopicBook (const std::string& topicId); - Book createQuestBook (const std::string& questName); - Book createTopicIndexBook (); + Book createEmptyJournalBook(); + Book createJournalBook(); + Book createTopicBook(uintptr_t topicId); + Book createQuestBook(std::string_view questName); + Book createTopicIndexBook(); ToUTF8::FromType mEncoding; int mIndexPagesCount; private: - BookTypesetter::Ptr createTypesetter (); - BookTypesetter::Ptr createLatinJournalIndex (); - BookTypesetter::Ptr createCyrillicJournalIndex (); + BookTypesetter::Ptr createTypesetter(); + BookTypesetter::Ptr createLatinJournalIndex(); + BookTypesetter::Ptr createCyrillicJournalIndex(); }; } diff --git a/apps/openmw/mwgui/journalviewmodel.cpp b/apps/openmw/mwgui/journalviewmodel.cpp index 6b38cd0d9d4..46f77f6dc40 100644 --- a/apps/openmw/mwgui/journalviewmodel.cpp +++ b/apps/openmw/mwgui/journalviewmodel.cpp @@ -1,361 +1,358 @@ #include "journalviewmodel.hpp" #include -#include #include +#include #include -#include -#include "../mwbase/world.hpp" -#include "../mwbase/journal.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/journal.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwdialogue/keywordsearch.hpp" +#include "../mwworld/datetimemanager.hpp" -namespace MWGui { - -struct JournalViewModelImpl; - -struct JournalViewModelImpl : JournalViewModel +namespace MWGui { - typedef MWDialogue::KeywordSearch KeywordSearchT; - mutable bool mKeywordSearchLoaded; - mutable KeywordSearchT mKeywordSearch; + struct JournalViewModelImpl; - JournalViewModelImpl () + struct JournalViewModelImpl : JournalViewModel { - mKeywordSearchLoaded = false; - } + typedef MWDialogue::KeywordSearch KeywordSearchT; - virtual ~JournalViewModelImpl () - { - } + mutable bool mKeywordSearchLoaded; + mutable KeywordSearchT mKeywordSearch; - /// \todo replace this nasty BS - static Utf8Span toUtf8Span (std::string const & str) - { - if (str.size () == 0) - return Utf8Span (Utf8Point (nullptr), Utf8Point (nullptr)); + JournalViewModelImpl() { mKeywordSearchLoaded = false; } - Utf8Point point = reinterpret_cast (str.c_str ()); + virtual ~JournalViewModelImpl() {} - return Utf8Span (point, point + str.size ()); - } + /// \todo replace this nasty BS + static Utf8Span toUtf8Span(std::string_view str) + { + if (str.size() == 0) + return Utf8Span(Utf8Point(nullptr), Utf8Point(nullptr)); - void load () override - { - } + Utf8Point point = reinterpret_cast(str.data()); - void unload () override - { - mKeywordSearch.clear (); - mKeywordSearchLoaded = false; - } + return Utf8Span(point, point + str.size()); + } - void ensureKeyWordSearchLoaded () const - { - if (!mKeywordSearchLoaded) + void load() override {} + + void unload() override + { + mKeywordSearch.clear(); + mKeywordSearchLoaded = false; + } + + void ensureKeyWordSearchLoaded() const { - MWBase::Journal * journal = MWBase::Environment::get().getJournal(); + if (!mKeywordSearchLoaded) + { + MWBase::Journal* journal = MWBase::Environment::get().getJournal(); - for(MWBase::Journal::TTopicIter i = journal->topicBegin(); i != journal->topicEnd (); ++i) - mKeywordSearch.seed (i->first, intptr_t (&i->second)); + for (MWBase::Journal::TTopicIter i = journal->topicBegin(); i != journal->topicEnd(); ++i) + mKeywordSearch.seed(i->second.getName(), intptr_t(&i->second)); - mKeywordSearchLoaded = true; + mKeywordSearchLoaded = true; + } } - } - bool isEmpty () const override - { - MWBase::Journal * journal = MWBase::Environment::get().getJournal(); + bool isEmpty() const override + { + MWBase::Journal* journal = MWBase::Environment::get().getJournal(); - return journal->begin () == journal->end (); - } + return journal->begin() == journal->end(); + } - template - struct BaseEntry : Interface - { - typedef t_iterator iterator_t; + template + struct BaseEntry : Interface + { + typedef t_iterator iterator_t; - iterator_t itr; - JournalViewModelImpl const * mModel; + iterator_t itr; + JournalViewModelImpl const* mModel; - BaseEntry (JournalViewModelImpl const * model, iterator_t itr) : - itr (itr), mModel (model), loaded (false) - {} + BaseEntry(JournalViewModelImpl const* model, iterator_t itr) + : itr(itr) + , mModel(model) + , loaded(false) + { + } - virtual ~BaseEntry () {} + virtual ~BaseEntry() {} - mutable bool loaded; - mutable std::string utf8text; + mutable bool loaded; + mutable std::string utf8text; - typedef std::pair Range; + typedef std::pair Range; - // hyperlinks in @link# notation - mutable std::map mHyperLinks; + // hyperlinks in @link# notation + mutable std::map mHyperLinks; - virtual std::string getText () const = 0; + virtual std::string getText() const = 0; - void ensureLoaded () const - { - if (!loaded) + void ensureLoaded() const { - mModel->ensureKeyWordSearchLoaded (); - - utf8text = getText (); - - size_t pos_end = 0; - for(;;) + if (!loaded) { - size_t pos_begin = utf8text.find('@'); - if (pos_begin != std::string::npos) - pos_end = utf8text.find('#', pos_begin); + mModel->ensureKeyWordSearchLoaded(); - if (pos_begin != std::string::npos && pos_end != std::string::npos) - { - std::string link = utf8text.substr(pos_begin + 1, pos_end - pos_begin - 1); - const char specialPseudoAsteriskCharacter = 127; - std::replace(link.begin(), link.end(), specialPseudoAsteriskCharacter, '*'); - std::string topicName = MWBase::Environment::get().getWindowManager()-> - getTranslationDataStorage().topicStandardForm(link); - - std::string displayName = link; - while (displayName[displayName.size()-1] == '*') - displayName.erase(displayName.size()-1, 1); + utf8text = getText(); - utf8text.replace(pos_begin, pos_end+1-pos_begin, displayName); - - intptr_t value = 0; - if (mModel->mKeywordSearch.containsKeyword(topicName, value)) - mHyperLinks[std::make_pair(pos_begin, pos_begin+displayName.size())] = value; + size_t pos_end = 0; + for (;;) + { + size_t pos_begin = utf8text.find('@'); + if (pos_begin != std::string::npos) + pos_end = utf8text.find('#', pos_begin); + + if (pos_begin != std::string::npos && pos_end != std::string::npos) + { + std::string link = utf8text.substr(pos_begin + 1, pos_end - pos_begin - 1); + const char specialPseudoAsteriskCharacter = 127; + std::replace(link.begin(), link.end(), specialPseudoAsteriskCharacter, '*'); + std::string_view topicName = MWBase::Environment::get() + .getWindowManager() + ->getTranslationDataStorage() + .topicStandardForm(link); + + std::string displayName = link; + while (displayName[displayName.size() - 1] == '*') + displayName.erase(displayName.size() - 1, 1); + + utf8text.replace(pos_begin, pos_end + 1 - pos_begin, displayName); + + intptr_t value = 0; + if (mModel->mKeywordSearch.containsKeyword(topicName, value)) + mHyperLinks[std::make_pair(pos_begin, pos_begin + displayName.size())] = value; + } + else + break; } - else - break; - } - loaded = true; + loaded = true; + } } - } - - Utf8Span body () const override - { - ensureLoaded (); - return toUtf8Span (utf8text); - } + Utf8Span body() const override + { + ensureLoaded(); - void visitSpans (std::function < void (TopicId, size_t, size_t)> visitor) const override - { - ensureLoaded (); - mModel->ensureKeyWordSearchLoaded (); + return toUtf8Span(utf8text); + } - if (mHyperLinks.size() && MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) + void visitSpans(std::function visitor) const override { - size_t formatted = 0; // points to the first character that is not laid out yet - for (std::map::const_iterator it = mHyperLinks.begin(); it != mHyperLinks.end(); ++it) + ensureLoaded(); + mModel->ensureKeyWordSearchLoaded(); + + if (mHyperLinks.size() + && MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) { - intptr_t topicId = it->second; - if (formatted < it->first.first) - visitor (0, formatted, it->first.first); - visitor (topicId, it->first.first, it->first.second); - formatted = it->first.second; + size_t formatted = 0; // points to the first character that is not laid out yet + for (std::map::const_iterator it = mHyperLinks.begin(); it != mHyperLinks.end(); + ++it) + { + intptr_t topicId = it->second; + if (formatted < it->first.first) + visitor(0, formatted, it->first.first); + visitor(topicId, it->first.first, it->first.second); + formatted = it->first.second; + } + if (formatted < utf8text.size()) + visitor(0, formatted, utf8text.size()); } - if (formatted < utf8text.size()) - visitor (0, formatted, utf8text.size()); - } - else - { - std::vector matches; - mModel->mKeywordSearch.highlightKeywords(utf8text.begin(), utf8text.end(), matches); - - std::string::const_iterator i = utf8text.begin (); - for (std::vector::const_iterator it = matches.begin(); it != matches.end(); ++it) + else { - const KeywordSearchT::Match& match = *it; + std::vector matches; + mModel->mKeywordSearch.highlightKeywords(utf8text.begin(), utf8text.end(), matches); - if (i != match.mBeg) - visitor (0, i - utf8text.begin (), match.mBeg - utf8text.begin ()); - - visitor (match.mValue, match.mBeg - utf8text.begin (), match.mEnd - utf8text.begin ()); + std::string::const_iterator i = utf8text.begin(); + for (std::vector::const_iterator it = matches.begin(); it != matches.end(); + ++it) + { + const KeywordSearchT::Match& match = *it; - i = match.mEnd; - } + if (i != match.mBeg) + visitor(0, i - utf8text.begin(), match.mBeg - utf8text.begin()); - if (i != utf8text.end ()) - visitor (0, i - utf8text.begin (), utf8text.size ()); - } - } - - }; + visitor(match.mValue, match.mBeg - utf8text.begin(), match.mEnd - utf8text.begin()); - void visitQuestNames (bool active_only, std::function visitor) const override - { - MWBase::Journal * journal = MWBase::Environment::get ().getJournal (); + i = match.mEnd; + } - std::set visitedQuests; + if (i != utf8text.end()) + visitor(0, i - utf8text.begin(), utf8text.size()); + } + } + }; - // Note that for purposes of the journal GUI, quests are identified by the name, not the ID, so several - // different quest IDs can end up in the same quest log. A quest log should be considered finished - // when any quest ID in that log is finished. - for (MWBase::Journal::TQuestIter i = journal->questBegin (); i != journal->questEnd (); ++i) + void visitQuestNames(bool active_only, std::function visitor) const override { - const MWDialogue::Quest& quest = i->second; + MWBase::Journal* journal = MWBase::Environment::get().getJournal(); - bool isFinished = false; - for (MWBase::Journal::TQuestIter j = journal->questBegin (); j != journal->questEnd (); ++j) + std::set> visitedQuests; + + // Note that for purposes of the journal GUI, quests are identified by the name, not the ID, so several + // different quest IDs can end up in the same quest log. A quest log should be considered finished + // when any quest ID in that log is finished. + for (MWBase::Journal::TQuestIter i = journal->questBegin(); i != journal->questEnd(); ++i) { - if (quest.getName() == j->second.getName() && j->second.isFinished()) - isFinished = true; - } + const MWDialogue::Quest& quest = i->second; - if (active_only && isFinished) - continue; + bool isFinished = false; + for (MWBase::Journal::TQuestIter j = journal->questBegin(); j != journal->questEnd(); ++j) + { + if (quest.getName() == j->second.getName() && j->second.isFinished()) + isFinished = true; + } - // Unfortunately Morrowind.esm has no quest names, since the quest book was added with tribunal. - // Note that even with Tribunal, some quests still don't have quest names. I'm assuming those are not supposed - // to appear in the quest book. - if (!quest.getName().empty()) - { - // Don't list the same quest name twice - if (visitedQuests.find(quest.getName()) != visitedQuests.end()) + if (active_only && isFinished) continue; - visitor (quest.getName(), isFinished); + // Unfortunately Morrowind.esm has no quest names, since the quest book was added with tribunal. + // Note that even with Tribunal, some quests still don't have quest names. I'm assuming those are not + // supposed to appear in the quest book. + if (!quest.getName().empty()) + { + // Don't list the same quest name twice + if (visitedQuests.find(quest.getName()) != visitedQuests.end()) + continue; + + visitor(quest.getName(), isFinished); - visitedQuests.insert(quest.getName()); + visitedQuests.emplace(quest.getName()); + } } } - } - template - struct JournalEntryImpl : BaseEntry - { - using BaseEntry ::itr; + template + struct JournalEntryImpl : BaseEntry + { + using BaseEntry::itr; - mutable std::string timestamp_buffer; + mutable std::string timestamp_buffer; - JournalEntryImpl (JournalViewModelImpl const * model, iterator_t itr) : - BaseEntry (model, itr) - {} + JournalEntryImpl(JournalViewModelImpl const* model, iterator_t itr) + : BaseEntry(model, itr) + { + } - std::string getText () const override - { - return itr->getText(); - } + std::string getText() const override { return itr->getText(); } - Utf8Span timestamp () const override - { - if (timestamp_buffer.empty ()) + Utf8Span timestamp() const override { - std::string dayStr = MyGUI::LanguageManager::getInstance().replaceTags("#{sDay}"); - - std::ostringstream os; + if (timestamp_buffer.empty()) + { + std::string dayStr = MyGUI::LanguageManager::getInstance().replaceTags("#{sDay}"); - os - << itr->mDayOfMonth << ' ' - << MWBase::Environment::get().getWorld()->getMonthName (itr->mMonth) - << " (" << dayStr << " " << (itr->mDay) << ')'; + std::ostringstream os; - timestamp_buffer = os.str (); - } + os << itr->mDayOfMonth << ' ' + << MWBase::Environment::get().getWorld()->getTimeManager()->getMonthName(itr->mMonth) << " (" + << dayStr << " " << (itr->mDay) << ')'; - return toUtf8Span (timestamp_buffer); - } - }; + timestamp_buffer = os.str(); + } - void visitJournalEntries (const std::string& questName, std::function visitor) const override - { - MWBase::Journal * journal = MWBase::Environment::get().getJournal(); + return toUtf8Span(timestamp_buffer); + } + }; - if (!questName.empty()) + void visitJournalEntries( + std::string_view questName, std::function visitor) const override { - std::vector quests; - for (MWBase::Journal::TQuestIter questIt = journal->questBegin(); questIt != journal->questEnd(); ++questIt) - { - if (Misc::StringUtils::ciEqual(questIt->second.getName(), questName)) - quests.push_back(&questIt->second); - } + MWBase::Journal* journal = MWBase::Environment::get().getJournal(); - for(MWBase::Journal::TEntryIter i = journal->begin(); i != journal->end (); ++i) + if (!questName.empty()) { - for (std::vector::iterator questIt = quests.begin(); questIt != quests.end(); ++questIt) + std::vector quests; + for (MWBase::Journal::TQuestIter questIt = journal->questBegin(); questIt != journal->questEnd(); + ++questIt) { - MWDialogue::Quest const* quest = *questIt; - for (MWDialogue::Topic::TEntryIter j = quest->begin (); j != quest->end (); ++j) + if (Misc::StringUtils::ciEqual(questIt->second.getName(), questName)) + quests.push_back(&questIt->second); + } + + for (MWBase::Journal::TEntryIter i = journal->begin(); i != journal->end(); ++i) + { + for (std::vector::iterator questIt = quests.begin(); + questIt != quests.end(); ++questIt) { - if (i->mInfoId == j->mInfoId) - visitor (JournalEntryImpl (this, i)); + MWDialogue::Quest const* quest = *questIt; + for (MWDialogue::Topic::TEntryIter j = quest->begin(); j != quest->end(); ++j) + { + if (i->mInfoId == j->mInfoId) + visitor(JournalEntryImpl(this, i)); + } } } } + else + { + for (MWBase::Journal::TEntryIter i = journal->begin(); i != journal->end(); ++i) + visitor(JournalEntryImpl(this, i)); + } } - else + + void visitTopicName(TopicId topicId, std::function visitor) const override { - for(MWBase::Journal::TEntryIter i = journal->begin(); i != journal->end (); ++i) - visitor (JournalEntryImpl (this, i)); + MWDialogue::Topic const& topic = *reinterpret_cast(topicId); + visitor(toUtf8Span(topic.getName())); } - } - - void visitTopicName (TopicId topicId, std::function visitor) const override - { - MWDialogue::Topic const & topic = * reinterpret_cast (topicId); - visitor (toUtf8Span (topic.getName())); - } - - void visitTopicNamesStartingWith (Utf8Stream::UnicodeChar character, std::function < void (const std::string&) > visitor) const override - { - MWBase::Journal * journal = MWBase::Environment::get().getJournal(); - for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i) + void visitTopicNamesStartingWith( + Utf8Stream::UnicodeChar character, std::function visitor) const override { - Utf8Stream stream (i->first.c_str()); - Utf8Stream::UnicodeChar first = Misc::StringUtils::toLowerUtf8(stream.peek()); + MWBase::Journal* journal = MWBase::Environment::get().getJournal(); + + for (MWBase::Journal::TTopicIter i = journal->topicBegin(); i != journal->topicEnd(); ++i) + { + Utf8Stream stream(i->second.getName()); + Utf8Stream::UnicodeChar first = Utf8Stream::toLowerUtf8(stream.peek()); - if (first != Misc::StringUtils::toLowerUtf8(character)) - continue; + if (first != Utf8Stream::toLowerUtf8(character)) + continue; - visitor (i->second.getName()); + visitor(i->second.getName()); + } } - } - struct TopicEntryImpl : BaseEntry - { - MWDialogue::Topic const & mTopic; + struct TopicEntryImpl : BaseEntry + { + MWDialogue::Topic const& mTopic; + + TopicEntryImpl(JournalViewModelImpl const* model, MWDialogue::Topic const& topic, iterator_t itr) + : BaseEntry(model, itr) + , mTopic(topic) + { + } - TopicEntryImpl (JournalViewModelImpl const * model, MWDialogue::Topic const & topic, iterator_t itr) : - BaseEntry (model, itr), mTopic (topic) - {} + std::string getText() const override { return itr->getText(); } - std::string getText () const override - { - return itr->getText(); - } + Utf8Span source() const override { return toUtf8Span(itr->mActorName); } + }; - Utf8Span source () const override + void visitTopicEntries(TopicId topicId, std::function visitor) const override { - return toUtf8Span (itr->mActorName); - } + typedef MWDialogue::Topic::TEntryIter iterator_t; + MWDialogue::Topic const& topic = *reinterpret_cast(topicId); + + for (iterator_t i = topic.begin(); i != topic.end(); ++i) + visitor(TopicEntryImpl(this, topic, i)); + } }; - void visitTopicEntries (TopicId topicId, std::function visitor) const override + JournalViewModel::Ptr JournalViewModel::create() { - typedef MWDialogue::Topic::TEntryIter iterator_t; - - MWDialogue::Topic const & topic = * reinterpret_cast (topicId); - - for (iterator_t i = topic.begin (); i != topic.end (); ++i) - visitor (TopicEntryImpl (this, topic, i)); + return std::make_shared(); } -}; - -JournalViewModel::Ptr JournalViewModel::create () -{ - return std::make_shared (); -} } diff --git a/apps/openmw/mwgui/journalviewmodel.hpp b/apps/openmw/mwgui/journalviewmodel.hpp index 3a937213039..376a8e1156b 100644 --- a/apps/openmw/mwgui/journalviewmodel.hpp +++ b/apps/openmw/mwgui/journalviewmodel.hpp @@ -1,10 +1,10 @@ #ifndef MWGUI_JOURNALVIEWMODEL_HPP #define MWGUI_JOURNALVIEWMODEL_HPP -#include -#include +#include #include -#include +#include +#include #include @@ -18,12 +18,12 @@ namespace MWGui /// game data store. struct JournalViewModel { - typedef std::shared_ptr Ptr; + typedef std::shared_ptr Ptr; typedef intptr_t QuestId; typedef intptr_t TopicId; - typedef uint8_t const * Utf8Point; - typedef std::pair Utf8Span; + typedef uint8_t const* Utf8Point; + typedef std::pair Utf8Span; /// The base interface for both journal entries and topics. struct Entry @@ -33,12 +33,12 @@ namespace MWGui /// This function returns a borrowed reference to the body of the /// journal entry. The returned reference becomes invalid when the /// entry is destroyed. - virtual Utf8Span body () const = 0; + virtual Utf8Span body() const = 0; /// Visits each subset of text in the body, delivering the beginning /// and end of the span relative to the body, and a valid topic ID if /// the span represents a keyword, or zero if not. - virtual void visitSpans (std::function visitor) const = 0; + virtual void visitSpans(std::function visitor) const = 0; virtual ~Entry() = default; }; @@ -48,7 +48,7 @@ namespace MWGui { /// Returns a pre-formatted span of UTF8 encoded text representing /// the name of the NPC this portion of dialog was heard from. - virtual Utf8Span source () const = 0; + virtual Utf8Span source() const = 0; virtual ~TopicEntry() = default; }; @@ -58,38 +58,40 @@ namespace MWGui { /// Returns a pre-formatted span of UTF8 encoded text representing /// the in-game date this entry was added to the journal. - virtual Utf8Span timestamp () const = 0; + virtual Utf8Span timestamp() const = 0; virtual ~JournalEntry() = default; }; /// called prior to journal opening - virtual void load () = 0; + virtual void load() = 0; /// called prior to journal closing - virtual void unload () = 0; + virtual void unload() = 0; /// returns true if their are no journal entries to display - virtual bool isEmpty () const = 0; + virtual bool isEmpty() const = 0; /// walks the active and optionally completed, quests providing the name and completed status - virtual void visitQuestNames (bool active_only, std::function visitor) const = 0; + virtual void visitQuestNames(bool active_only, std::function visitor) const = 0; /// walks over the journal entries related to all quests with the given name /// If \a questName is empty, simply visits all journal entries - virtual void visitJournalEntries (const std::string& questName, std::function visitor) const = 0; + virtual void visitJournalEntries( + std::string_view questName, std::function visitor) const = 0; /// provides the name of the topic specified by its id - virtual void visitTopicName (TopicId topicId, std::function visitor) const = 0; + virtual void visitTopicName(TopicId topicId, std::function visitor) const = 0; /// walks over the topics whose names start with the character - virtual void visitTopicNamesStartingWith (Utf8Stream::UnicodeChar character, std::function < void (const std::string&) > visitor) const = 0; + virtual void visitTopicNamesStartingWith( + Utf8Stream::UnicodeChar character, std::function visitor) const = 0; /// walks over the topic entries for the topic specified by its identifier - virtual void visitTopicEntries (TopicId topicId, std::function visitor) const = 0; + virtual void visitTopicEntries(TopicId topicId, std::function visitor) const = 0; // create an instance of the default journal view model implementation - static Ptr create (); + static Ptr create(); virtual ~JournalViewModel() = default; }; diff --git a/apps/openmw/mwgui/journalwindow.cpp b/apps/openmw/mwgui/journalwindow.cpp index 96c42549a5c..574c425d3e3 100644 --- a/apps/openmw/mwgui/journalwindow.cpp +++ b/apps/openmw/mwgui/journalwindow.cpp @@ -5,45 +5,45 @@ #include #include -#include #include #include +#include -#include +#include #include #include #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/journal.hpp" +#include "../mwbase/windowmanager.hpp" #include "bookpage.hpp" -#include "windowbase.hpp" -#include "journalviewmodel.hpp" #include "journalbooks.hpp" +#include "journalviewmodel.hpp" +#include "windowbase.hpp" namespace { - static char const OptionsOverlay [] = "OptionsOverlay"; - static char const OptionsBTN [] = "OptionsBTN"; - static char const PrevPageBTN [] = "PrevPageBTN"; - static char const NextPageBTN [] = "NextPageBTN"; - static char const CloseBTN [] = "CloseBTN"; - static char const JournalBTN [] = "JournalBTN"; - static char const TopicsBTN [] = "TopicsBTN"; - static char const QuestsBTN [] = "QuestsBTN"; - static char const CancelBTN [] = "CancelBTN"; - static char const ShowAllBTN [] = "ShowAllBTN"; - static char const ShowActiveBTN [] = "ShowActiveBTN"; - static char const PageOneNum [] = "PageOneNum"; - static char const PageTwoNum [] = "PageTwoNum"; - static char const TopicsList [] = "TopicsList"; - static char const QuestsList [] = "QuestsList"; - static char const LeftBookPage [] = "LeftBookPage"; - static char const RightBookPage [] = "RightBookPage"; - static char const LeftTopicIndex [] = "LeftTopicIndex"; - static char const CenterTopicIndex [] = "CenterTopicIndex"; - static char const RightTopicIndex [] = "RightTopicIndex"; + static constexpr std::string_view OptionsOverlay = "OptionsOverlay"; + static constexpr std::string_view OptionsBTN = "OptionsBTN"; + static constexpr std::string_view PrevPageBTN = "PrevPageBTN"; + static constexpr std::string_view NextPageBTN = "NextPageBTN"; + static constexpr std::string_view CloseBTN = "CloseBTN"; + static constexpr std::string_view JournalBTN = "JournalBTN"; + static constexpr std::string_view TopicsBTN = "TopicsBTN"; + static constexpr std::string_view QuestsBTN = "QuestsBTN"; + static constexpr std::string_view CancelBTN = "CancelBTN"; + static constexpr std::string_view ShowAllBTN = "ShowAllBTN"; + static constexpr std::string_view ShowActiveBTN = "ShowActiveBTN"; + static constexpr std::string_view PageOneNum = "PageOneNum"; + static constexpr std::string_view PageTwoNum = "PageTwoNum"; + static constexpr std::string_view TopicsList = "TopicsList"; + static constexpr std::string_view QuestsList = "QuestsList"; + static constexpr std::string_view LeftBookPage = "LeftBookPage"; + static constexpr std::string_view RightBookPage = "RightBookPage"; + static constexpr std::string_view LeftTopicIndex = "LeftTopicIndex"; + static constexpr std::string_view CenterTopicIndex = "CenterTopicIndex"; + static constexpr std::string_view RightTopicIndex = "RightTopicIndex"; struct JournalWindowImpl : MWGui::JournalBooks, MWGui::JournalWindow { @@ -53,7 +53,7 @@ namespace Book mBook; }; - typedef std::stack DisplayStateStack; + typedef std::stack DisplayStateStack; DisplayStateStack mStates; Book mTopicIndexBook; @@ -63,66 +63,58 @@ namespace bool mAllQuests; template - T * getWidget (char const * name) + T* getWidget(std::string_view name) { - T * widget; - WindowBase::getWidget (widget, name); + T* widget; + WindowBase::getWidget(widget, name); return widget; } template - void setText (char const * name, value_type const & value) + void setText(std::string_view name, value_type const& value) { - getWidget (name) -> - setCaption (MyGUI::utility::toString (value)); + getWidget(name)->setCaption(MyGUI::utility::toString(value)); } - void setVisible (char const * name, bool visible) - { - getWidget (name) -> - setVisible (visible); - } + void setVisible(std::string_view name, bool visible) { getWidget(name)->setVisible(visible); } - void adviseButtonClick (char const * name, void (JournalWindowImpl::*Handler) (MyGUI::Widget* _sender)) + void adviseButtonClick(std::string_view name, void (JournalWindowImpl::*handler)(MyGUI::Widget*)) { - getWidget (name) -> - eventMouseButtonClick += newDelegate(this, Handler); + getWidget(name)->eventMouseButtonClick += newDelegate(this, handler); } - void adviseKeyPress (char const * name, void (JournalWindowImpl::*Handler) (MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char character)) + void adviseKeyPress( + std::string_view name, void (JournalWindowImpl::*handler)(MyGUI::Widget*, MyGUI::KeyCode, MyGUI::Char)) { - getWidget (name) -> - eventKeyButtonPressed += newDelegate(this, Handler); + getWidget(name)->eventKeyButtonPressed += newDelegate(this, handler); } - MWGui::BookPage* getPage (char const * name) - { - return getWidget (name); - } + MWGui::BookPage* getPage(std::string_view name) { return getWidget(name); } - JournalWindowImpl (MWGui::JournalViewModel::Ptr Model, bool questList, ToUTF8::FromType encoding) - : JournalBooks (Model, encoding), JournalWindow() + JournalWindowImpl(MWGui::JournalViewModel::Ptr model, bool questList, ToUTF8::FromType encoding) + : JournalBooks(std::move(model), encoding) + , JournalWindow() { center(); - adviseButtonClick (OptionsBTN, &JournalWindowImpl::notifyOptions ); - adviseButtonClick (PrevPageBTN, &JournalWindowImpl::notifyPrevPage ); - adviseButtonClick (NextPageBTN, &JournalWindowImpl::notifyNextPage ); - adviseButtonClick (CloseBTN, &JournalWindowImpl::notifyClose ); - adviseButtonClick (JournalBTN, &JournalWindowImpl::notifyJournal ); + adviseButtonClick(OptionsBTN, &JournalWindowImpl::notifyOptions); + adviseButtonClick(PrevPageBTN, &JournalWindowImpl::notifyPrevPage); + adviseButtonClick(NextPageBTN, &JournalWindowImpl::notifyNextPage); + adviseButtonClick(CloseBTN, &JournalWindowImpl::notifyClose); + adviseButtonClick(JournalBTN, &JournalWindowImpl::notifyJournal); - adviseButtonClick (TopicsBTN, &JournalWindowImpl::notifyTopics ); - adviseButtonClick (QuestsBTN, &JournalWindowImpl::notifyQuests ); - adviseButtonClick (CancelBTN, &JournalWindowImpl::notifyCancel ); + adviseButtonClick(TopicsBTN, &JournalWindowImpl::notifyTopics); + adviseButtonClick(QuestsBTN, &JournalWindowImpl::notifyQuests); + adviseButtonClick(CancelBTN, &JournalWindowImpl::notifyCancel); - adviseButtonClick (ShowAllBTN, &JournalWindowImpl::notifyShowAll ); - adviseButtonClick (ShowActiveBTN, &JournalWindowImpl::notifyShowActive); + adviseButtonClick(ShowAllBTN, &JournalWindowImpl::notifyShowAll); + adviseButtonClick(ShowActiveBTN, &JournalWindowImpl::notifyShowActive); - adviseKeyPress (OptionsBTN, &JournalWindowImpl::notifyKeyPress); - adviseKeyPress (PrevPageBTN, &JournalWindowImpl::notifyKeyPress); - adviseKeyPress (NextPageBTN, &JournalWindowImpl::notifyKeyPress); - adviseKeyPress (CloseBTN, &JournalWindowImpl::notifyKeyPress); - adviseKeyPress (JournalBTN, &JournalWindowImpl::notifyKeyPress); + adviseKeyPress(OptionsBTN, &JournalWindowImpl::notifyKeyPress); + adviseKeyPress(PrevPageBTN, &JournalWindowImpl::notifyKeyPress); + adviseKeyPress(NextPageBTN, &JournalWindowImpl::notifyKeyPress); + adviseKeyPress(CloseBTN, &JournalWindowImpl::notifyKeyPress); + adviseKeyPress(JournalBTN, &JournalWindowImpl::notifyKeyPress); Gui::MWList* list = getWidget(QuestsList); list->eventItemSelected += MyGUI::newDelegate(this, &JournalWindowImpl::notifyQuestClicked); @@ -131,25 +123,24 @@ namespace topicsList->eventItemSelected += MyGUI::newDelegate(this, &JournalWindowImpl::notifyTopicSelected); { - MWGui::BookPage::ClickCallback callback; - - callback = std::bind (&JournalWindowImpl::notifyTopicClicked, this, std::placeholders::_1); + MWGui::BookPage::ClickCallback callback = [this](intptr_t linkId) { notifyTopicClicked(linkId); }; - getPage (LeftBookPage)->adviseLinkClicked (callback); - getPage (RightBookPage)->adviseLinkClicked (callback); + getPage(LeftBookPage)->adviseLinkClicked(callback); + getPage(RightBookPage)->adviseLinkClicked(std::move(callback)); - getPage (LeftBookPage)->eventMouseWheel += MyGUI::newDelegate(this, &JournalWindowImpl::notifyMouseWheel); - getPage (RightBookPage)->eventMouseWheel += MyGUI::newDelegate(this, &JournalWindowImpl::notifyMouseWheel); + getPage(LeftBookPage)->eventMouseWheel + += MyGUI::newDelegate(this, &JournalWindowImpl::notifyMouseWheel); + getPage(RightBookPage)->eventMouseWheel + += MyGUI::newDelegate(this, &JournalWindowImpl::notifyMouseWheel); } { - MWGui::BookPage::ClickCallback callback; - - callback = std::bind(&JournalWindowImpl::notifyIndexLinkClicked, this, std::placeholders::_1); + MWGui::BookPage::ClickCallback callback + = [this](MWGui::TypesetBook::InteractiveId index) { notifyIndexLinkClicked(index); }; - getPage (LeftTopicIndex)->adviseLinkClicked (callback); - getPage (CenterTopicIndex)->adviseLinkClicked (callback); - getPage (RightTopicIndex)->adviseLinkClicked (callback); + getPage(LeftTopicIndex)->adviseLinkClicked(callback); + getPage(CenterTopicIndex)->adviseLinkClicked(callback); + getPage(RightTopicIndex)->adviseLinkClicked(std::move(callback)); } adjustButton(PrevPageBTN); @@ -167,8 +158,9 @@ namespace if (nextButton->getSize().width == 64) { // english button has a 7 pixel wide strip of garbage on its right edge - nextButton->setSize(64-7, nextButton->getSize().height); - nextButton->setImageCoord(MyGUI::IntCoord(0,0,(64-7)*nextButtonScale,nextButton->getSize().height*nextButtonScale)); + nextButton->setSize(64 - 7, nextButton->getSize().height); + nextButton->setImageCoord( + MyGUI::IntCoord(0, 0, (64 - 7) * nextButtonScale, nextButton->getSize().height * nextButtonScale)); } if (!questList) @@ -203,20 +195,26 @@ namespace adjustButton(TopicsBTN); int topicsWidth = getWidget(TopicsBTN)->getSize().width; int cancelLeft = getWidget(CancelBTN)->getPosition().left; - int cancelRight = getWidget(CancelBTN)->getPosition().left + getWidget(CancelBTN)->getSize().width; + int cancelRight = getWidget(CancelBTN)->getPosition().left + + getWidget(CancelBTN)->getSize().width; - getWidget(QuestsBTN)->setPosition(cancelRight, getWidget(QuestsBTN)->getPosition().top); + getWidget(QuestsBTN)->setPosition( + cancelRight, getWidget(QuestsBTN)->getPosition().top); - // Usually Topics, Quests, and Cancel buttons have the 64px width, so we can place the Topics left-up from the Cancel button, and the Quests right-up from the Cancel button. - // But in some installations, e.g. German one, the Topics button has the 128px width, so we should place it exactly left from the Quests button. + // Usually Topics, Quests, and Cancel buttons have the 64px width, so we can place the Topics left-up + // from the Cancel button, and the Quests right-up from the Cancel button. But in some installations, + // e.g. German one, the Topics button has the 128px width, so we should place it exactly left from the + // Quests button. if (topicsWidth == 64) { - getWidget(TopicsBTN)->setPosition(cancelLeft - topicsWidth, getWidget(TopicsBTN)->getPosition().top); + getWidget(TopicsBTN)->setPosition( + cancelLeft - topicsWidth, getWidget(TopicsBTN)->getPosition().top); } else { int questLeft = getWidget(QuestsBTN)->getPosition().left; - getWidget(TopicsBTN)->setPosition(questLeft - topicsWidth, getWidget(TopicsBTN)->getPosition().top); + getWidget(TopicsBTN)->setPosition( + questLeft - topicsWidth, getWidget(TopicsBTN)->getPosition().top); } } @@ -228,28 +226,24 @@ namespace void onOpen() override { - if (!MWBase::Environment::get().getWindowManager ()->getJournalAllowed ()) - { - MWBase::Environment::get().getWindowManager()->popGuiMode (); - } - mModel->load (); + mModel->load(); - setBookMode (); + setBookMode(); Book journalBook; - if (mModel->isEmpty ()) - journalBook = createEmptyJournalBook (); + if (mModel->isEmpty()) + journalBook = createEmptyJournalBook(); else - journalBook = createJournalBook (); + journalBook = createJournalBook(); - pushBook (journalBook, 0); + pushBook(journalBook, 0); // fast forward to the last page - if (!mStates.empty ()) + if (!mStates.empty()) { - unsigned int & page = mStates.top ().mPage; - page = mStates.top().mBook->pageCount()-1; - if (page%2) + unsigned int& page = mStates.top().mPage; + page = mStates.top().mBook->pageCount() - 1; + if (page % 2) --page; } updateShowingPages(); @@ -259,57 +253,54 @@ namespace void onClose() override { - mModel->unload (); + mModel->unload(); - getPage (LeftBookPage)->showPage (Book (), 0); - getPage (RightBookPage)->showPage (Book (), 0); + getPage(LeftBookPage)->showPage(Book(), 0); + getPage(RightBookPage)->showPage(Book(), 0); - while (!mStates.empty ()) - mStates.pop (); + while (!mStates.empty()) + mStates.pop(); - mTopicIndexBook.reset (); + mTopicIndexBook.reset(); } - void setVisible (bool newValue) override - { - WindowBase::setVisible (newValue); - } + void setVisible(bool newValue) override { WindowBase::setVisible(newValue); } - void setBookMode () + void setBookMode() { mOptionsMode = false; mTopicsMode = false; - setVisible (OptionsBTN, true); - setVisible (OptionsOverlay, false); + setVisible(OptionsBTN, true); + setVisible(OptionsOverlay, false); - updateShowingPages (); - updateCloseJournalButton (); + updateShowingPages(); + updateCloseJournalButton(); } - void setOptionsMode () + void setOptionsMode() { mOptionsMode = true; mTopicsMode = false; - setVisible (OptionsBTN, false); - setVisible (OptionsOverlay, true); + setVisible(OptionsBTN, false); + setVisible(OptionsOverlay, true); - setVisible (PrevPageBTN, false); - setVisible (NextPageBTN, false); - setVisible (CloseBTN, false); - setVisible (JournalBTN, false); + setVisible(PrevPageBTN, false); + setVisible(NextPageBTN, false); + setVisible(CloseBTN, false); + setVisible(JournalBTN, false); - setVisible (TopicsList, false); - setVisible (QuestsList, mQuestMode); - setVisible (LeftTopicIndex, !mQuestMode); - setVisible (CenterTopicIndex, !mQuestMode); - setVisible (RightTopicIndex, !mQuestMode); - setVisible (ShowAllBTN, mQuestMode && !mAllQuests); - setVisible (ShowActiveBTN, mQuestMode && mAllQuests); + setVisible(TopicsList, false); + setVisible(QuestsList, mQuestMode); + setVisible(LeftTopicIndex, !mQuestMode); + setVisible(CenterTopicIndex, !mQuestMode); + setVisible(RightTopicIndex, !mQuestMode); + setVisible(ShowAllBTN, mQuestMode && !mAllQuests); + setVisible(ShowActiveBTN, mQuestMode && mAllQuests); - //TODO: figure out how to make "options" page overlay book page - // correctly, so that text may show underneath - getPage (RightBookPage)->showPage (Book (), 0); + // TODO: figure out how to make "options" page overlay book page + // correctly, so that text may show underneath + getPage(RightBookPage)->showPage(Book(), 0); // If in quest mode, ensure the quest list is updated if (mQuestMode) @@ -318,48 +309,48 @@ namespace notifyTopics(getWidget(TopicsList)); } - void pushBook (Book book, unsigned int page) + void pushBook(Book& book, unsigned int page) { DisplayState bs; bs.mPage = page; bs.mBook = book; - mStates.push (bs); - updateShowingPages (); - updateCloseJournalButton (); + mStates.push(bs); + updateShowingPages(); + updateCloseJournalButton(); } - void replaceBook (Book book, unsigned int page) + void replaceBook(Book& book, unsigned int page) { - assert (!mStates.empty ()); - mStates.top ().mBook = book; - mStates.top ().mPage = page; - updateShowingPages (); + assert(!mStates.empty()); + mStates.top().mBook = book; + mStates.top().mPage = page; + updateShowingPages(); } - void popBook () + void popBook() { - mStates.pop (); - updateShowingPages (); - updateCloseJournalButton (); + mStates.pop(); + updateShowingPages(); + updateCloseJournalButton(); } - void updateCloseJournalButton () + void updateCloseJournalButton() { - setVisible (CloseBTN, mStates.size () < 2); - setVisible (JournalBTN, mStates.size () >= 2); + setVisible(CloseBTN, mStates.size() < 2); + setVisible(JournalBTN, mStates.size() >= 2); } - void updateShowingPages () + void updateShowingPages() { Book book; unsigned int page; unsigned int relPages; - if (!mStates.empty ()) + if (!mStates.empty()) { - book = mStates.top ().mBook; - page = mStates.top ().mPage; - relPages = book->pageCount () - page; + book = mStates.top().mBook; + page = mStates.top().mPage; + relPages = book->pageCount() - page; } else { @@ -381,14 +372,14 @@ namespace else if (focus == prevPageBtn && !prevPageVisible && nextPageVisible) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nextPageBtn); - setVisible (PageOneNum, relPages > 0); - setVisible (PageTwoNum, relPages > 1); + setVisible(PageOneNum, relPages > 0); + setVisible(PageTwoNum, relPages > 1); - getPage (LeftBookPage)->showPage ((relPages > 0) ? book : Book (), page+0); - getPage (RightBookPage)->showPage ((relPages > 0) ? book : Book (), page+1); + getPage(LeftBookPage)->showPage((relPages > 0) ? book : Book(), page + 0); + getPage(RightBookPage)->showPage((relPages > 0) ? std::move(book) : Book(), page + 1); - setText (PageOneNum, page + 1); - setText (PageTwoNum, page + 2); + setText(PageOneNum, page + 1); + setText(PageTwoNum, page + 2); } void notifyKeyPress(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character) @@ -399,90 +390,91 @@ namespace notifyNextPage(sender); } - void notifyTopicClicked (intptr_t linkId) + void notifyTopicClicked(intptr_t linkId) { - Book topicBook = createTopicBook (linkId); + Book topicBook = createTopicBook(linkId); - if (mStates.size () > 1) - replaceBook (topicBook, 0); + if (mStates.size() > 1) + replaceBook(topicBook, 0); else - pushBook (topicBook, 0); + pushBook(topicBook, 0); - setVisible (OptionsOverlay, false); - setVisible (OptionsBTN, true); - setVisible (JournalBTN, true); + setVisible(OptionsOverlay, false); + setVisible(OptionsBTN, true); + setVisible(JournalBTN, true); mOptionsMode = false; mTopicsMode = false; - MWBase::Environment::get().getWindowManager()->playSound("book page"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } - void notifyTopicSelected (const std::string& topic, int id) + void notifyTopicSelected(const std::string& topicIdString, int id) { + ESM::RefId topic = ESM::RefId::stringRefId(topicIdString); const MWBase::Journal* journal = MWBase::Environment::get().getJournal(); intptr_t topicId = 0; /// \todo get rid of intptr ids - for(MWBase::Journal::TTopicIter i = journal->topicBegin(); i != journal->topicEnd (); ++i) + for (MWBase::Journal::TTopicIter i = journal->topicBegin(); i != journal->topicEnd(); ++i) { - if (Misc::StringUtils::ciEqual(i->first, topic)) - topicId = intptr_t (&i->second); + if (i->first == topic) + topicId = intptr_t(&i->second); } notifyTopicClicked(topicId); } - void notifyQuestClicked (const std::string& name, int id) + void notifyQuestClicked(const std::string& name, int id) { - Book book = createQuestBook (name); + Book book = createQuestBook(name); - if (mStates.size () > 1) - replaceBook (book, 0); + if (mStates.size() > 1) + replaceBook(book, 0); else - pushBook (book, 0); + pushBook(book, 0); - setVisible (OptionsOverlay, false); - setVisible (OptionsBTN, true); - setVisible (JournalBTN, true); + setVisible(OptionsOverlay, false); + setVisible(OptionsBTN, true); + setVisible(JournalBTN, true); mOptionsMode = false; - MWBase::Environment::get().getWindowManager()->playSound("book page"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } void notifyOptions(MyGUI::Widget* _sender) { - setOptionsMode (); + setOptionsMode(); if (!mTopicIndexBook) - mTopicIndexBook = createTopicIndexBook (); + mTopicIndexBook = createTopicIndexBook(); if (mIndexPagesCount == 3) { - getPage (LeftTopicIndex)->showPage (mTopicIndexBook, 0); - getPage (CenterTopicIndex)->showPage (mTopicIndexBook, 1); - getPage (RightTopicIndex)->showPage (mTopicIndexBook, 2); + getPage(LeftTopicIndex)->showPage(mTopicIndexBook, 0); + getPage(CenterTopicIndex)->showPage(mTopicIndexBook, 1); + getPage(RightTopicIndex)->showPage(mTopicIndexBook, 2); } else { - getPage (LeftTopicIndex)->showPage (mTopicIndexBook, 0); - getPage (RightTopicIndex)->showPage (mTopicIndexBook, 1); + getPage(LeftTopicIndex)->showPage(mTopicIndexBook, 0); + getPage(RightTopicIndex)->showPage(mTopicIndexBook, 1); } } void notifyJournal(MyGUI::Widget* _sender) { - assert (mStates.size () > 1); - popBook (); + assert(mStates.size() > 1); + popBook(); - MWBase::Environment::get().getWindowManager()->playSound("book page"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } - void notifyIndexLinkClicked (MWGui::TypesetBook::InteractiveId index) + void notifyIndexLinkClicked(MWGui::TypesetBook::InteractiveId index) { - setVisible (LeftTopicIndex, false); - setVisible (CenterTopicIndex, false); - setVisible (RightTopicIndex, false); - setVisible (TopicsList, true); + setVisible(LeftTopicIndex, false); + setVisible(CenterTopicIndex, false); + setVisible(RightTopicIndex, false); + setVisible(TopicsList, true); mTopicsMode = true; @@ -495,40 +487,43 @@ namespace list->adjustSize(); - MWBase::Environment::get().getWindowManager()->playSound("book page"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } void notifyTopics(MyGUI::Widget* _sender) { mQuestMode = false; mTopicsMode = false; - setVisible (LeftTopicIndex, true); - setVisible (CenterTopicIndex, true); - setVisible (RightTopicIndex, true); - setVisible (TopicsList, false); - setVisible (QuestsList, false); - setVisible (ShowAllBTN, false); - setVisible (ShowActiveBTN, false); - - MWBase::Environment::get().getWindowManager()->playSound("book page"); + setVisible(LeftTopicIndex, true); + setVisible(CenterTopicIndex, true); + setVisible(RightTopicIndex, true); + setVisible(TopicsList, false); + setVisible(QuestsList, false); + setVisible(ShowAllBTN, false); + setVisible(ShowActiveBTN, false); + + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } struct AddNamesToList { - AddNamesToList(Gui::MWList* list) : mList(list) {} - - Gui::MWList* mList; - void operator () (const std::string& name, bool finished=false) + AddNamesToList(Gui::MWList* list) + : mList(list) { - mList->addItem(name); } + + Gui::MWList* mList; + void operator()(std::string_view name, bool finished = false) { mList->addItem(name); } }; struct SetNamesInactive { - SetNamesInactive(Gui::MWList* list) : mList(list) {} + SetNamesInactive(Gui::MWList* list) + : mList(list) + { + } Gui::MWList* mList; - void operator () (const std::string& name, bool finished) + void operator()(std::string_view name, bool finished) { if (finished) { @@ -541,13 +536,13 @@ namespace { mQuestMode = true; - setVisible (LeftTopicIndex, false); - setVisible (CenterTopicIndex, false); - setVisible (RightTopicIndex, false); - setVisible (TopicsList, false); - setVisible (QuestsList, true); - setVisible (ShowAllBTN, !mAllQuests); - setVisible (ShowActiveBTN, mAllQuests); + setVisible(LeftTopicIndex, false); + setVisible(CenterTopicIndex, false); + setVisible(RightTopicIndex, false); + setVisible(TopicsList, false); + setVisible(QuestsList, true); + setVisible(ShowAllBTN, !mAllQuests); + setVisible(ShowActiveBTN, mAllQuests); Gui::MWList* list = getWidget(QuestsList); list->clear(); @@ -556,6 +551,7 @@ namespace mModel->visitQuestNames(!mAllQuests, add); + list->sort(); list->adjustSize(); if (mAllQuests) @@ -564,7 +560,7 @@ namespace mModel->visitQuestNames(false, setInactive); } - MWBase::Environment::get().getWindowManager()->playSound("book page"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } void notifyShowAll(MyGUI::Widget* _sender) @@ -588,15 +584,14 @@ namespace else { setBookMode(); - MWBase::Environment::get().getWindowManager()->playSound("book page"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } - } void notifyClose(MyGUI::Widget* _sender) { - MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); - winMgr->playSound("book close"); + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + winMgr->playSound(ESM::RefId::stringRefId("book close")); winMgr->popGuiMode(); } @@ -612,17 +607,17 @@ namespace { if (mOptionsMode) return; - if (!mStates.empty ()) + if (!mStates.empty()) { - unsigned int & page = mStates.top ().mPage; - Book book = mStates.top ().mBook; + unsigned int& page = mStates.top().mPage; + Book book = mStates.top().mBook; - if (page+2 < book->pageCount()) + if (page + 2 < book->pageCount()) { - MWBase::Environment::get().getWindowManager()->playSound("book page"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); page += 2; - updateShowingPages (); + updateShowingPages(); } } } @@ -631,16 +626,16 @@ namespace { if (mOptionsMode) return; - if (!mStates.empty ()) + if (!mStates.empty()) { - unsigned int & page = mStates.top ().mPage; + unsigned int& page = mStates.top().mPage; - if(page >= 2) + if (page >= 2) { - MWBase::Environment::get().getWindowManager()->playSound("book page"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); page -= 2; - updateShowingPages (); + updateShowingPages(); } } } @@ -648,13 +643,13 @@ namespace } // glue the implementation to the interface -MWGui::JournalWindow * MWGui::JournalWindow::create (JournalViewModel::Ptr Model, bool questList, ToUTF8::FromType encoding) +std::unique_ptr MWGui::JournalWindow::create( + JournalViewModel::Ptr Model, bool questList, ToUTF8::FromType encoding) { - return new JournalWindowImpl (Model, questList, encoding); + return std::make_unique(Model, questList, encoding); } MWGui::JournalWindow::JournalWindow() : BookWindowBase("openmw_journal.layout") { - } diff --git a/apps/openmw/mwgui/journalwindow.hpp b/apps/openmw/mwgui/journalwindow.hpp index c18e6e4c081..22e7048acff 100644 --- a/apps/openmw/mwgui/journalwindow.hpp +++ b/apps/openmw/mwgui/journalwindow.hpp @@ -7,7 +7,10 @@ #include -namespace MWBase { class WindowManager; } +namespace MWBase +{ + class WindowManager; +} namespace MWGui { @@ -18,13 +21,16 @@ namespace MWGui JournalWindow(); /// construct a new instance of the one JournalWindow implementation - static JournalWindow * create (std::shared_ptr Model, bool questList, ToUTF8::FromType encoding); + static std::unique_ptr create( + std::shared_ptr model, bool questList, ToUTF8::FromType encoding); /// destroy this instance of the JournalWindow implementation - virtual ~JournalWindow () {} + virtual ~JournalWindow() {} /// show/hide the journal window - void setVisible (bool newValue) override = 0; + void setVisible(bool newValue) override = 0; + + std::string_view getWindowIdForLua() const override { return "Journal"; } }; } diff --git a/apps/openmw/mwgui/keyboardnavigation.cpp b/apps/openmw/mwgui/keyboardnavigation.cpp index b718b712c08..9d4971951a1 100644 --- a/apps/openmw/mwgui/keyboardnavigation.cpp +++ b/apps/openmw/mwgui/keyboardnavigation.cpp @@ -1,328 +1,305 @@ #include "keyboardnavigation.hpp" +#include #include #include -#include -#include #include #include -#include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" namespace MWGui { -bool shouldAcceptKeyFocus(MyGUI::Widget* w) -{ - return w && !w->castType(false) && w->getInheritedEnabled() && w->getInheritedVisible() && w->getVisible() && w->getEnabled(); -} - -/// Recursively get all child widgets that accept keyboard input -void getKeyFocusWidgets(MyGUI::Widget* parent, std::vector& results) -{ - assert(parent != nullptr); - - if (!parent->getVisible() || !parent->getEnabled()) - return; - - MyGUI::EnumeratorWidgetPtr enumerator = parent->getEnumerator(); - while (enumerator.next()) + bool shouldAcceptKeyFocus(MyGUI::Widget* w) { - MyGUI::Widget* w = enumerator.current(); - if (!w->getVisible() || !w->getEnabled()) - continue; - if (w->getNeedKeyFocus() && shouldAcceptKeyFocus(w)) - results.push_back(w); - else - getKeyFocusWidgets(w, results); + return w && !w->castType(false) && w->getInheritedEnabled() && w->getInheritedVisible() + && w->getVisible() && w->getEnabled(); } -} -KeyboardNavigation::KeyboardNavigation() - : mCurrentFocus(nullptr) - , mModalWindow(nullptr) - , mEnabled(true) -{ - MyGUI::WidgetManager::getInstance().registerUnlinker(this); -} - -KeyboardNavigation::~KeyboardNavigation() -{ - try - { - MyGUI::WidgetManager::getInstance().unregisterUnlinker(this); - } - catch(const MyGUI::Exception& e) + /// Recursively get all child widgets that accept keyboard input + void getKeyFocusWidgets(MyGUI::Widget* parent, std::vector& results) { - Log(Debug::Error) << "Error in the destructor: " << e.what(); - } -} + assert(parent != nullptr); -void KeyboardNavigation::saveFocus(int mode) -{ - MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); - if (shouldAcceptKeyFocus(focus)) - { - mKeyFocus[mode] = focus; + if (!parent->getVisible() || !parent->getEnabled()) + return; + + MyGUI::EnumeratorWidgetPtr enumerator = parent->getEnumerator(); + while (enumerator.next()) + { + MyGUI::Widget* w = enumerator.current(); + if (!w->getVisible() || !w->getEnabled()) + continue; + if (w->getNeedKeyFocus() && shouldAcceptKeyFocus(w)) + results.push_back(w); + else + getKeyFocusWidgets(w, results); + } } - else if(shouldAcceptKeyFocus(mCurrentFocus)) + + KeyboardNavigation::KeyboardNavigation() + : mCurrentFocus(nullptr) + , mModalWindow(nullptr) + , mEnabled(true) { - mKeyFocus[mode] = mCurrentFocus; + MyGUI::WidgetManager::getInstance().registerUnlinker(this); } -} -void KeyboardNavigation::restoreFocus(int mode) -{ - std::map::const_iterator found = mKeyFocus.find(mode); - if (found != mKeyFocus.end()) + KeyboardNavigation::~KeyboardNavigation() { - MyGUI::Widget* w = found->second; - if (w && w->getVisible() && w->getEnabled()) - MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(found->second); + try + { + MyGUI::WidgetManager::getInstance().unregisterUnlinker(this); + } + catch (const MyGUI::Exception& e) + { + Log(Debug::Error) << "Error in the destructor: " << e.what(); + } } -} -void KeyboardNavigation::_unlinkWidget(MyGUI::Widget *widget) -{ - for (std::pair& w : mKeyFocus) - if (w.second == widget) - w.second = nullptr; - if (widget == mCurrentFocus) - mCurrentFocus = nullptr; -} - -#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) -void styleFocusedButton(MyGUI::Widget* w) -{ - if (w) + void KeyboardNavigation::saveFocus(int mode) { - if (MyGUI::Button* b = w->castType(false)) + MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); + if (shouldAcceptKeyFocus(focus)) { - b->_setWidgetState("highlighted"); + mKeyFocus[mode] = focus; + } + else if (shouldAcceptKeyFocus(mCurrentFocus)) + { + mKeyFocus[mode] = mCurrentFocus; } } -} -#endif -bool isRootParent(MyGUI::Widget* widget, MyGUI::Widget* root) -{ - while (widget && widget->getParent()) - widget = widget->getParent(); - return widget == root; -} - -void KeyboardNavigation::onFrame() -{ - if (!mEnabled) - return; - - if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) + void KeyboardNavigation::restoreFocus(int mode) { - MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nullptr); - return; + std::map::const_iterator found = mKeyFocus.find(mode); + if (found != mKeyFocus.end()) + { + MyGUI::Widget* w = found->second; + if (w && w->getVisible() && w->getEnabled() && w->getInheritedVisible() && w->getInheritedEnabled()) + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(found->second); + } } - MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); - - if (focus == mCurrentFocus) + void KeyboardNavigation::_unlinkWidget(MyGUI::Widget* widget) { -#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) - styleFocusedButton(mCurrentFocus); -#endif - return; + for (std::pair& w : mKeyFocus) + if (w.second == widget) + w.second = nullptr; + if (widget == mCurrentFocus) + mCurrentFocus = nullptr; } - // workaround incorrect key focus resets (fix in MyGUI TBD) - if (!shouldAcceptKeyFocus(focus) && shouldAcceptKeyFocus(mCurrentFocus) && (!mModalWindow || isRootParent(mCurrentFocus, mModalWindow))) + bool isRootParent(MyGUI::Widget* widget, MyGUI::Widget* root) { - MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCurrentFocus); - focus = mCurrentFocus; + while (widget && widget->getParent()) + widget = widget->getParent(); + return widget == root; } - if (focus != mCurrentFocus) + void KeyboardNavigation::onFrame() { -#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) - if (mCurrentFocus) + if (!mEnabled) + return; + + if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) { - if (MyGUI::Button* b = mCurrentFocus->castType(false)) - b->_setWidgetState("normal"); + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nullptr); + return; } -#endif - mCurrentFocus = focus; - } -#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) - styleFocusedButton(mCurrentFocus); -#endif -} + MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); -void KeyboardNavigation::setDefaultFocus(MyGUI::Widget *window, MyGUI::Widget *defaultFocus) -{ - MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); - if (!focus || !shouldAcceptKeyFocus(focus)) - { - MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(defaultFocus); + if (focus == mCurrentFocus) + { + return; + } + + // workaround incorrect key focus resets (fix in MyGUI TBD) + if (!shouldAcceptKeyFocus(focus) && shouldAcceptKeyFocus(mCurrentFocus) + && (!mModalWindow || isRootParent(mCurrentFocus, mModalWindow))) + { + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCurrentFocus); + focus = mCurrentFocus; + } + + if (focus != mCurrentFocus) + { + mCurrentFocus = focus; + } } - else + + void KeyboardNavigation::setDefaultFocus(MyGUI::Widget* window, MyGUI::Widget* defaultFocus) { - if (!isRootParent(focus, window)) + MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); + if (!focus || !shouldAcceptKeyFocus(focus)) + { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(defaultFocus); + } + else + { + if (!isRootParent(focus, window)) + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(defaultFocus); + } } -} - -void KeyboardNavigation::setModalWindow(MyGUI::Widget *window) -{ - mModalWindow = window; -} -void KeyboardNavigation::setEnabled(bool enabled) -{ - mEnabled = enabled; -} + void KeyboardNavigation::setModalWindow(MyGUI::Widget* window) + { + mModalWindow = window; + } -enum Direction -{ - D_Left, - D_Up, - D_Right, - D_Down, - D_Next, - D_Prev -}; - -bool KeyboardNavigation::injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat) -{ - if (!mEnabled) - return false; + void KeyboardNavigation::setEnabled(bool enabled) + { + mEnabled = enabled; + } - switch (key.getValue()) + enum Direction { - case MyGUI::KeyCode::ArrowLeft: - return switchFocus(D_Left, false); - case MyGUI::KeyCode::ArrowRight: - return switchFocus(D_Right, false); - case MyGUI::KeyCode::ArrowUp: - return switchFocus(D_Up, false); - case MyGUI::KeyCode::ArrowDown: - return switchFocus(D_Down, false); - case MyGUI::KeyCode::Tab: - return switchFocus(MyGUI::InputManager::getInstance().isShiftPressed() ? D_Prev : D_Next, true); - case MyGUI::KeyCode::Return: - case MyGUI::KeyCode::NumpadEnter: - case MyGUI::KeyCode::Space: + D_Left, + D_Up, + D_Right, + D_Down, + D_Next, + D_Prev + }; + + bool KeyboardNavigation::injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat) { - // We should disable repeating for activation keys - MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::None); - if (repeat) - return true; + if (!mEnabled) + return false; - return accept(); - } - default: - return false; + switch (key.getValue()) + { + case MyGUI::KeyCode::ArrowLeft: + return switchFocus(D_Left, false); + case MyGUI::KeyCode::ArrowRight: + return switchFocus(D_Right, false); + case MyGUI::KeyCode::ArrowUp: + return switchFocus(D_Up, false); + case MyGUI::KeyCode::ArrowDown: + return switchFocus(D_Down, false); + case MyGUI::KeyCode::Tab: + return switchFocus(MyGUI::InputManager::getInstance().isShiftPressed() ? D_Prev : D_Next, true); + case MyGUI::KeyCode::Period: + return switchFocus(D_Prev, true); + case MyGUI::KeyCode::Slash: + return switchFocus(D_Next, true); + case MyGUI::KeyCode::Return: + case MyGUI::KeyCode::NumpadEnter: + case MyGUI::KeyCode::Space: + { + // We should disable repeating for activation keys + MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::None); + if (repeat) + return true; + + return accept(); + } + default: + return false; + } } -} -bool KeyboardNavigation::switchFocus(int direction, bool wrap) -{ - if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) - return false; + bool KeyboardNavigation::switchFocus(int direction, bool wrap) + { + if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) + return false; - MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); + MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); - bool isCycle = (direction == D_Prev || direction == D_Next); + bool isCycle = (direction == D_Prev || direction == D_Next); - if ((focus && focus->getTypeName().find("Button") == std::string::npos) && !isCycle) - return false; - - if (focus && isCycle && focus->getUserString("AcceptTab") == "true") - return false; + if ((focus && focus->getTypeName().find("Button") == std::string::npos) && !isCycle) + return false; - if ((!focus || !focus->getNeedKeyFocus()) && isCycle) - { - // if nothing is selected, select the first widget - return selectFirstWidget(); - } - if (!focus) - return false; + if (focus && isCycle && focus->getUserString("AcceptTab") == "true") + return false; - MyGUI::Widget* window = focus; - while (window && window->getParent()) - window = window->getParent(); - MyGUI::VectorWidgetPtr keyFocusList; - getKeyFocusWidgets(window, keyFocusList); + if ((!focus || !focus->getNeedKeyFocus()) && isCycle) + { + // if nothing is selected, select the first widget + return selectFirstWidget(); + } + if (!focus) + return false; - if (keyFocusList.empty()) - return false; + MyGUI::Widget* window = focus; + while (window && window->getParent()) + window = window->getParent(); + MyGUI::VectorWidgetPtr keyFocusList; + getKeyFocusWidgets(window, keyFocusList); - MyGUI::VectorWidgetPtr::iterator found = std::find(keyFocusList.begin(), keyFocusList.end(), focus); - if (found == keyFocusList.end()) - { - if (isCycle) - return selectFirstWidget(); - else + if (keyFocusList.empty()) return false; - } - bool forward = (direction == D_Next || direction == D_Right || direction == D_Down); + MyGUI::VectorWidgetPtr::iterator found = std::find(keyFocusList.begin(), keyFocusList.end(), focus); + if (found == keyFocusList.end()) + { + if (isCycle) + return selectFirstWidget(); + else + return false; + } - int index = found - keyFocusList.begin(); - index = forward ? (index+1) : (index-1); - if (wrap) - index = (index + keyFocusList.size())%keyFocusList.size(); - else - index = std::min(std::max(0, index), static_cast(keyFocusList.size())-1); + bool forward = (direction == D_Next || direction == D_Right || direction == D_Down); - MyGUI::Widget* next = keyFocusList[index]; - int vertdiff = next->getTop() - focus->getTop(); - int horizdiff = next->getLeft() - focus->getLeft(); - bool isVertical = std::abs(vertdiff) > std::abs(horizdiff); - if (direction == D_Right && (horizdiff <= 0 || isVertical)) - return false; - else if (direction == D_Left && (horizdiff >= 0 || isVertical)) - return false; - else if (direction == D_Down && (vertdiff <= 0 || !isVertical)) - return false; - else if (direction == D_Up && (vertdiff >= 0 || !isVertical)) - return false; + std::ptrdiff_t index{ found - keyFocusList.begin() }; + index = forward ? (index + 1) : (index - 1); + if (wrap) + index = (index + keyFocusList.size()) % keyFocusList.size(); + else + index = std::clamp(index, 0, keyFocusList.size() - 1); - MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(keyFocusList[index]); - return true; -} + MyGUI::Widget* next = keyFocusList[index]; + int vertdiff = next->getTop() - focus->getTop(); + int horizdiff = next->getLeft() - focus->getLeft(); + bool isVertical = std::abs(vertdiff) > std::abs(horizdiff); + if (direction == D_Right && (horizdiff <= 0 || isVertical)) + return false; + else if (direction == D_Left && (horizdiff >= 0 || isVertical)) + return false; + else if (direction == D_Down && (vertdiff <= 0 || !isVertical)) + return false; + else if (direction == D_Up && (vertdiff >= 0 || !isVertical)) + return false; -bool KeyboardNavigation::selectFirstWidget() -{ - MyGUI::VectorWidgetPtr keyFocusList; - MyGUI::EnumeratorWidgetPtr enumerator = MyGUI::Gui::getInstance().getEnumerator(); - if (mModalWindow) - enumerator = mModalWindow->getEnumerator(); - while (enumerator.next()) - getKeyFocusWidgets(enumerator.current(), keyFocusList); - - if (!keyFocusList.empty()) - { - MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(keyFocusList[0]); + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(keyFocusList[index]); return true; } - return false; -} -bool KeyboardNavigation::accept() -{ - MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); - if (!focus) - return false; - //MyGUI::Button* button = focus->castType(false); - //if (button && button->getEnabled()) - if (focus->getTypeName().find("Button") != std::string::npos && focus->getEnabled()) + bool KeyboardNavigation::selectFirstWidget() { - focus->eventMouseButtonClick(focus); - return true; + MyGUI::VectorWidgetPtr keyFocusList; + MyGUI::EnumeratorWidgetPtr enumerator = MyGUI::Gui::getInstance().getEnumerator(); + if (mModalWindow) + enumerator = mModalWindow->getEnumerator(); + while (enumerator.next()) + getKeyFocusWidgets(enumerator.current(), keyFocusList); + + if (!keyFocusList.empty()) + { + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(keyFocusList[0]); + return true; + } + return false; } - return false; -} + bool KeyboardNavigation::accept() + { + MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); + if (!focus) + return false; + // MyGUI::Button* button = focus->castType(false); + // if (button && button->getEnabled()) + if (focus->getTypeName().find("Button") != std::string::npos && focus->getEnabled()) + { + focus->eventMouseButtonClick(focus); + return true; + } + return false; + } } diff --git a/apps/openmw/mwgui/keyboardnavigation.hpp b/apps/openmw/mwgui/keyboardnavigation.hpp index d5159c24a02..e37fe9088b3 100644 --- a/apps/openmw/mwgui/keyboardnavigation.hpp +++ b/apps/openmw/mwgui/keyboardnavigation.hpp @@ -1,8 +1,8 @@ #ifndef OPENMW_MWGUI_KEYBOARDNAVIGATION_H #define OPENMW_MWGUI_KEYBOARDNAVIGATION_H -#include #include +#include namespace MWGui { diff --git a/apps/openmw/mwgui/layout.cpp b/apps/openmw/mwgui/layout.cpp index 9b9b9537f99..8d70bc956b1 100644 --- a/apps/openmw/mwgui/layout.cpp +++ b/apps/openmw/mwgui/layout.cpp @@ -1,36 +1,33 @@ #include "layout.hpp" -#include -#include #include +#include #include +#include +#include #include namespace MWGui { - void Layout::initialise(const std::string& _layout, MyGUI::Widget* _parent) + void Layout::initialise(std::string_view _layout) { - const std::string MAIN_WINDOW = "_Main"; + const auto MAIN_WINDOW = "_Main"; mLayoutName = _layout; - if (mLayoutName.empty()) - mMainWidget = _parent; - else + mPrefix = MyGUI::utility::toString(this, "_"); + mListWindowRoot = MyGUI::LayoutManager::getInstance().loadLayout(mLayoutName, mPrefix); + + const std::string main_name = mPrefix + MAIN_WINDOW; + for (MyGUI::Widget* widget : mListWindowRoot) { - mPrefix = MyGUI::utility::toString(this, "_"); - mListWindowRoot = MyGUI::LayoutManager::getInstance().loadLayout(mLayoutName, mPrefix, _parent); + if (widget->getName() == main_name) + mMainWidget = widget; - const std::string main_name = mPrefix + MAIN_WINDOW; - for (MyGUI::Widget* widget : mListWindowRoot) - { - if (widget->getName() == main_name) - { - mMainWidget = widget; - break; - } - } - MYGUI_ASSERT(mMainWidget, "root widget name '" << MAIN_WINDOW << "' in layout '" << mLayoutName << "' not found."); + // Force the alignment to update immediately + widget->_setAlign(widget->getSize(), widget->getParentSize()); } + MYGUI_ASSERT( + mMainWidget, "root widget name '" << MAIN_WINDOW << "' in layout '" << mLayoutName << "' not found."); } void Layout::shutdown() @@ -42,7 +39,7 @@ namespace MWGui void Layout::setCoord(int x, int y, int w, int h) { - mMainWidget->setCoord(x,y,w,h); + mMainWidget->setCoord(x, y, w, h); } void Layout::setVisible(bool b) @@ -50,26 +47,28 @@ namespace MWGui mMainWidget->setVisible(b); } - void Layout::setText(const std::string &name, const std::string &caption) + void Layout::setText(std::string_view name, std::string_view caption) { MyGUI::Widget* pt; getWidget(pt, name); - static_cast(pt)->setCaption(caption); + static_cast(pt)->setCaption(MyGUI::UString(caption)); } - void Layout::setTitle(const std::string& title) + void Layout::setTitle(std::string_view title) { MyGUI::Window* window = static_cast(mMainWidget); if (window->getCaption() != title) - window->setCaptionWithReplacing(title); + window->setCaptionWithReplacing(MyGUI::UString(title)); } - MyGUI::Widget* Layout::getWidget(const std::string &_name) + MyGUI::Widget* Layout::getWidget(std::string_view _name) { + std::string target = mPrefix; + target += _name; for (MyGUI::Widget* widget : mListWindowRoot) { - MyGUI::Widget* find = widget->findWidget(mPrefix + _name); + MyGUI::Widget* find = widget->findWidget(target); if (nullptr != find) { return find; diff --git a/apps/openmw/mwgui/layout.hpp b/apps/openmw/mwgui/layout.hpp index ea51bf541e7..07a3d1e66b5 100644 --- a/apps/openmw/mwgui/layout.hpp +++ b/apps/openmw/mwgui/layout.hpp @@ -2,74 +2,77 @@ #define OPENMW_MWGUI_LAYOUT_H #include -#include +#include + #include #include namespace MWGui { - /** The Layout class is an utility class used to load MyGUI layouts - from xml files, and to manipulate member widgets. - */ - class Layout - { - public: - Layout(const std::string & _layout, MyGUI::Widget* _parent = nullptr) - : mMainWidget(nullptr) - { initialise(_layout, _parent); } - virtual ~Layout() + /** The Layout class is an utility class used to load MyGUI layouts + from xml files, and to manipulate member widgets. + */ + class Layout { - try + public: + Layout(std::string_view layout) + : mMainWidget(nullptr) { - shutdown(); + initialise(layout); + assert(mMainWidget); } - catch(const MyGUI::Exception& e) + + virtual ~Layout() { - Log(Debug::Error) << "Error in the destructor: " << e.what(); + try + { + shutdown(); + } + catch (const MyGUI::Exception& e) + { + Log(Debug::Error) << "Error in the destructor: " << e.what(); + } } - } - MyGUI::Widget* getWidget(const std::string& _name); + MyGUI::Widget* getWidget(std::string_view name); - template - void getWidget(T * & _widget, const std::string & _name) - { - MyGUI::Widget* w = getWidget(_name); - T* cast = w->castType(false); - if (!cast) + template + void getWidget(T*& _widget, std::string_view _name) { - MYGUI_EXCEPT("Error cast : dest type = '" << T::getClassTypeName() - << "' source name = '" << w->getName() - << "' source type = '" << w->getTypeName() << "' in layout '" << mLayoutName << "'"); + MyGUI::Widget* w = getWidget(_name); + T* cast = w->castType(false); + if (!cast) + { + MYGUI_EXCEPT("Error cast : dest type = '" << T::getClassTypeName() << "' source name = '" + << w->getName() << "' source type = '" << w->getTypeName() + << "' in layout '" << mLayoutName << "'"); + } + else + _widget = cast; } - else - _widget = cast; - } - - private: - void initialise(const std::string & _layout, - MyGUI::Widget* _parent = nullptr); - void shutdown(); + private: + void initialise(std::string_view layout); - public: - void setCoord(int x, int y, int w, int h); + void shutdown(); - virtual void setVisible(bool b); + public: + void setCoord(int x, int y, int w, int h); - void setText(const std::string& name, const std::string& caption); + virtual void setVisible(bool b); - // NOTE: this assume that mMainWidget is of type Window. - void setTitle(const std::string& title); + void setText(std::string_view name, std::string_view caption); - MyGUI::Widget* mMainWidget; + // NOTE: this assume that mMainWidget is of type Window. + void setTitle(std::string_view title); - protected: + MyGUI::Widget* mMainWidget; - std::string mPrefix; - std::string mLayoutName; - MyGUI::VectorWidgetPtr mListWindowRoot; - }; + protected: + std::string mPrefix; + std::string mLayoutName; + MyGUI::VectorWidgetPtr mListWindowRoot; + }; } #endif diff --git a/apps/openmw/mwgui/levelupdialog.cpp b/apps/openmw/mwgui/levelupdialog.cpp index 14de3fd2740..87f2db55a54 100644 --- a/apps/openmw/mwgui/levelupdialog.cpp +++ b/apps/openmw/mwgui/levelupdialog.cpp @@ -1,30 +1,41 @@ #include "levelupdialog.hpp" #include -#include #include +#include +#include +#include +#include #include +#include -#include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" -#include "../mwmechanics/actorutil.hpp" + +#include "../mwsound/constants.hpp" #include "class.hpp" +namespace +{ + constexpr unsigned int sMaxCoins = 3; + constexpr int sColumnOffsets[] = { 32, 218 }; +} namespace MWGui { - const unsigned int LevelupDialog::sMaxCoins = 3; LevelupDialog::LevelupDialog() - : WindowBase("openmw_levelup_dialog.layout"), - mCoinCount(sMaxCoins) + : WindowBase("openmw_levelup_dialog.layout") + , mCoinCount(sMaxCoins) { getWidget(mOkButton, "OkButton"); getWidget(mClassImage, "ClassImage"); @@ -35,26 +46,47 @@ namespace MWGui mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &LevelupDialog::onOkButtonClicked); - for (int i=1; i<9; ++i) { - MyGUI::TextBox* t; - getWidget(t, "AttribVal" + MyGUI::utility::toString(i)); - mAttributeValues.push_back(t); - - MyGUI::Button* b; - getWidget(b, "Attrib" + MyGUI::utility::toString(i)); - b->setUserData (i-1); - b->eventMouseButtonClick += MyGUI::newDelegate(this, &LevelupDialog::onAttributeClicked); - mAttributes.push_back(b); - - getWidget(t, "AttribMultiplier" + MyGUI::utility::toString(i)); - mAttributeMultipliers.push_back(t); + const auto& store = MWBase::Environment::get().getESMStore()->get(); + const size_t perCol + = static_cast(std::ceil(store.getSize() / static_cast(std::size(sColumnOffsets)))); + size_t i = 0; + for (const ESM::Attribute& attribute : store) + { + const int offset = sColumnOffsets[i / perCol]; + const int row = static_cast(i % perCol); + Widgets widgets; + widgets.mMultiplier = mAssignWidget->createWidget( + "SandTextVCenter", { offset, 20 * row, 100, 20 }, MyGUI::Align::Default); + auto* hbox = mAssignWidget->createWidget( + {}, { offset + 20, 20 * row, 200, 20 }, MyGUI::Align::Default); + widgets.mButton = hbox->createWidget("SandTextButton", {}, MyGUI::Align::Default); + widgets.mButton->setUserData(attribute.mId); + widgets.mButton->eventMouseButtonClick += MyGUI::newDelegate(this, &LevelupDialog::onAttributeClicked); + widgets.mButton->setUserString("TextPadding", "0 0"); + widgets.mButton->setUserString("ToolTipType", "Layout"); + widgets.mButton->setUserString("ToolTipLayout", "AttributeToolTip"); + widgets.mButton->setUserString("Caption_AttributeName", attribute.mName); + widgets.mButton->setUserString("Caption_AttributeDescription", attribute.mDescription); + widgets.mButton->setUserString("ImageTexture_AttributeImage", attribute.mIcon); + widgets.mButton->setCaption(attribute.mName); + widgets.mValue = hbox->createWidget("SandText", {}, MyGUI::Align::Default); + mAttributeWidgets.emplace(attribute.mId, widgets); + ++i; + } + + mAssignWidget->setVisibleVScroll(false); + mAssignWidget->setCanvasSize(MyGUI::IntSize( + mAssignWidget->getWidth(), std::max(mAssignWidget->getHeight(), static_cast(20 * perCol)))); + mAssignWidget->setVisibleVScroll(true); + mAssignWidget->setViewOffset(MyGUI::IntPoint()); } - for (unsigned int i = 0; i < mCoinCount; ++i) + for (unsigned int i = 0; i < sMaxCoins; ++i) { - MyGUI::ImageBox* image = mCoinBox->createWidget("ImageBox", MyGUI::IntCoord(0,0,16,16), MyGUI::Align::Default); - image->setImageTexture ("icons\\tx_goldicon.dds"); + MyGUI::ImageBox* image = mCoinBox->createWidget( + "ImageBox", MyGUI::IntCoord(0, 0, 16, 16), MyGUI::Align::Default); + image->setImageTexture("icons\\tx_goldicon.dds"); mCoins.push_back(image); } @@ -63,31 +95,30 @@ namespace MWGui void LevelupDialog::setAttributeValues() { - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWMechanics::CreatureStats& creatureStats = player.getClass().getCreatureStats(player); - MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats (player); + MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); - for (int i = 0; i < 8; ++i) + for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) { - int val = creatureStats.getAttribute(i).getBase(); - if (std::find(mSpentAttributes.begin(), mSpentAttributes.end(), i) != mSpentAttributes.end()) + int val = creatureStats.getAttribute(attribute.mId).getBase(); + if (std::find(mSpentAttributes.begin(), mSpentAttributes.end(), attribute.mId) != mSpentAttributes.end()) { - val += pcStats.getLevelupAttributeMultiplier(i); + val += pcStats.getLevelupAttributeMultiplier(attribute.mId); } if (val >= 100) val = 100; - mAttributeValues[i]->setCaption(MyGUI::utility::toString(val)); + mAttributeWidgets[attribute.mId].mValue->setCaption(MyGUI::utility::toString(val)); } } - void LevelupDialog::resetCoins() { - const int coinSpacing = 33; - int curX = mCoinBox->getWidth()/2 - (coinSpacing*(mCoinCount - 1) + 16*mCoinCount)/2; - for (unsigned int i=0; igetWidth() / 2 - (coinSpacing * (mCoinCount - 1) + 16 * mCoinCount) / 2; + for (unsigned int i = 0; i < sMaxCoins; ++i) { MyGUI::ImageBox* image = mCoins[i]; image->detachFromWidget(); @@ -95,8 +126,8 @@ namespace MWGui if (i < mCoinCount) { mCoins[i]->setVisible(true); - image->setCoord(MyGUI::IntCoord(curX,0,16,16)); - curX += 16+coinSpacing; + image->setCoord(MyGUI::IntCoord(curX, 0, 16, 16)); + curX += 16 + coinSpacing; } else mCoins[i]->setVisible(false); @@ -106,18 +137,21 @@ namespace MWGui void LevelupDialog::assignCoins() { resetCoins(); - for (unsigned int i=0; idetachFromWidget(); image->attachToWidget(mAssignWidget); - int attribute = mSpentAttributes[i]; + const auto& attribute = mSpentAttributes[i]; + const auto& widgets = mAttributeWidgets[attribute]; - int xdiff = mAttributeMultipliers[attribute]->getCaption() == "" ? 0 : 20; + const int xdiff = widgets.mMultiplier->getCaption().empty() ? 0 : 20; + const auto* hbox = widgets.mButton->getParent(); - MyGUI::IntPoint pos = mAttributes[attribute]->getAbsolutePosition() - mAssignWidget->getAbsolutePosition() - MyGUI::IntPoint(22+xdiff,0); - pos.top += (mAttributes[attribute]->getHeight() - image->getHeight())/2; + MyGUI::IntPoint pos = hbox->getPosition(); + pos.left -= 22 + xdiff; + pos.top += (hbox->getHeight() - image->getHeight()) / 2; image->setPosition(pos); } @@ -126,46 +160,51 @@ namespace MWGui void LevelupDialog::onOpen() { - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - MWMechanics::CreatureStats& creatureStats = player.getClass().getCreatureStats(player); - MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); + const MWMechanics::CreatureStats& creatureStats = player.getClass().getCreatureStats(player); + const MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); - setClassImage(mClassImage, getLevelupClassImage(pcStats.getSkillIncreasesForSpecialization(0), - pcStats.getSkillIncreasesForSpecialization(1), - pcStats.getSkillIncreasesForSpecialization(2))); + setClassImage(mClassImage, + ESM::RefId::stringRefId( + getLevelupClassImage(pcStats.getSkillIncreasesForSpecialization(ESM::Class::Specialization::Combat), + pcStats.getSkillIncreasesForSpecialization(ESM::Class::Specialization::Magic), + pcStats.getSkillIncreasesForSpecialization(ESM::Class::Specialization::Stealth)))); - int level = creatureStats.getLevel ()+1; + int level = creatureStats.getLevel() + 1; mLevelText->setCaptionWithReplacing("#{sLevelUpMenu1} " + MyGUI::utility::toString(level)); - std::string levelupdescription; - levelupdescription = Fallback::Map::getString("Level_Up_Level"+MyGUI::utility::toString(level)); + std::string_view levelupdescription; + levelupdescription = Fallback::Map::getString("Level_Up_Level" + MyGUI::utility::toString(level)); - if (levelupdescription == "") + if (levelupdescription.empty()) levelupdescription = Fallback::Map::getString("Level_Up_Default"); - mLevelDescription->setCaption (levelupdescription); + mLevelDescription->setCaption(MyGUI::UString(levelupdescription)); unsigned int availableAttributes = 0; - for (int i = 0; i < 8; ++i) + for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) { - MyGUI::TextBox* text = mAttributeMultipliers[i]; - if (pcStats.getAttribute(i).getBase() < 100) + const auto& widgets = mAttributeWidgets[attribute.mId]; + if (pcStats.getAttribute(attribute.mId).getBase() < 100) { - mAttributes[i]->setEnabled(true); - mAttributeValues[i]->setEnabled(true); + widgets.mButton->setEnabled(true); + widgets.mValue->setEnabled(true); availableAttributes++; - float mult = pcStats.getLevelupAttributeMultiplier (i); - mult = std::min(mult, 100-pcStats.getAttribute(i).getBase()); - text->setCaption(mult <= 1 ? "" : "x" + MyGUI::utility::toString(mult)); + float mult = pcStats.getLevelupAttributeMultiplier(attribute.mId); + mult = std::min(mult, 100 - pcStats.getAttribute(attribute.mId).getBase()); + if (mult <= 1) + widgets.mMultiplier->setCaption({}); + else + widgets.mMultiplier->setCaption("x" + MyGUI::utility::toString(mult)); } else { - mAttributes[i]->setEnabled(false); - mAttributeValues[i]->setEnabled(false); + widgets.mButton->setEnabled(false); + widgets.mValue->setEnabled(false); - text->setCaption(""); + widgets.mMultiplier->setCaption({}); } } @@ -179,13 +218,13 @@ namespace MWGui center(); // Play LevelUp Music - MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Triumph.mp3"); + MWBase::Environment::get().getSoundManager()->streamMusic(MWSound::triumphMusic, MWSound::MusicType::Normal); } void LevelupDialog::onOkButtonClicked(MyGUI::Widget* sender) { MWWorld::Ptr player = MWMechanics::getPlayer(); - MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats (player); + MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); if (mSpentAttributes.size() < mCoinCount) MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage36}"); @@ -206,14 +245,13 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Levelup); } - } - void LevelupDialog::onAttributeClicked(MyGUI::Widget *sender) + void LevelupDialog::onAttributeClicked(MyGUI::Widget* sender) { - int attribute = *sender->getUserData(); + auto attribute = *sender->getUserData(); - std::vector::iterator found = std::find(mSpentAttributes.begin(), mSpentAttributes.end(), attribute); + auto found = std::find(mSpentAttributes.begin(), mSpentAttributes.end(), attribute); if (found != mSpentAttributes.end()) mSpentAttributes.erase(found); else @@ -226,9 +264,10 @@ namespace MWGui assignCoins(); } - std::string LevelupDialog::getLevelupClassImage(const int combatIncreases, const int magicIncreases, const int stealthIncreases) + std::string_view LevelupDialog::getLevelupClassImage( + const int combatIncreases, const int magicIncreases, const int stealthIncreases) { - std::string ret = "acrobat"; + std::string_view ret = "acrobat"; int total = combatIncreases + magicIncreases + stealthIncreases; if (total == 0) diff --git a/apps/openmw/mwgui/levelupdialog.hpp b/apps/openmw/mwgui/levelupdialog.hpp index 6c9182609ef..486390679bd 100644 --- a/apps/openmw/mwgui/levelupdialog.hpp +++ b/apps/openmw/mwgui/levelupdialog.hpp @@ -1,6 +1,8 @@ #ifndef MWGUI_LEVELUPDIALOG_H #define MWGUI_LEVELUPDIALOG_H +#include + #include "windowbase.hpp" namespace MWGui @@ -13,24 +15,29 @@ namespace MWGui void onOpen() override; + std::string_view getWindowIdForLua() const override { return "LevelUpDialog"; } + private: + struct Widgets + { + MyGUI::Button* mButton; + MyGUI::TextBox* mValue; + MyGUI::TextBox* mMultiplier; + }; MyGUI::Button* mOkButton; MyGUI::ImageBox* mClassImage; MyGUI::TextBox* mLevelText; MyGUI::EditBox* mLevelDescription; MyGUI::Widget* mCoinBox; - MyGUI::Widget* mAssignWidget; + MyGUI::ScrollView* mAssignWidget; - std::vector mAttributes; - std::vector mAttributeValues; - std::vector mAttributeMultipliers; + std::map mAttributeWidgets; std::vector mCoins; - std::vector mSpentAttributes; + std::vector mSpentAttributes; unsigned int mCoinCount; - static const unsigned int sMaxCoins; void onOkButtonClicked(MyGUI::Widget* sender); void onAttributeClicked(MyGUI::Widget* sender); @@ -40,7 +47,8 @@ namespace MWGui void setAttributeValues(); - std::string getLevelupClassImage(const int combatIncreases, const int magicIncreases, const int stealthIncreases); + std::string_view getLevelupClassImage( + const int combatIncreases, const int magicIncreases, const int stealthIncreases); }; } diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index 61fcacca467..263e676e150 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -1,29 +1,29 @@ #include "loadingscreen.hpp" #include -#include #include #include -#include -#include -#include #include +#include #include +#include -#include #include +#include +#include #include -#include -#include #include +#include +#include +#include #include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/inputmanager.hpp" #include "backgroundimage.hpp" @@ -39,87 +39,65 @@ namespace MWGui , mLastRenderTime(0.0) , mLoadingOnTime(0.0) , mImportantLabel(false) - , mVisible(false) , mNestedLoadingCount(0) , mProgress(0) , mShowWallpaper(true) { - mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); - getWidget(mLoadingText, "LoadingText"); getWidget(mProgressBar, "ProgressBar"); getWidget(mLoadingBox, "LoadingBox"); + getWidget(mSceneImage, "Scene"); + getWidget(mSplashImage, "Splash"); mProgressBar->setScrollViewPage(1); - mBackgroundImage = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, - MyGUI::Align::Stretch, "Menu"); - mSceneImage = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, - MyGUI::Align::Stretch, "Scene"); - findSplashScreens(); } - LoadingScreen::~LoadingScreen() - { - } + LoadingScreen::~LoadingScreen() {} void LoadingScreen::findSplashScreens() { - const std::map& index = mResourceSystem->getVFS()->getIndex(); - std::string pattern = "Splash/"; - mResourceSystem->getVFS()->normalizeFilename(pattern); - - /* priority given to the left */ - const std::array supported_extensions {{".tga", ".dds", ".ktx", ".png", ".bmp", ".jpeg", ".jpg"}}; - - auto found = index.lower_bound(pattern); - while (found != index.end()) + auto isSupportedExtension = [](const std::string_view& ext) { + static const std::array supported_extensions{ { "tga", "dds", "ktx", "png", "bmp", "jpeg", + "jpg" } }; + return !ext.empty() + && std::find(supported_extensions.begin(), supported_extensions.end(), ext) + != supported_extensions.end(); + }; + + constexpr VFS::Path::NormalizedView splash("splash/"); + for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(splash)) { - const std::string& name = found->first; - if (name.size() >= pattern.size() && name.substr(0, pattern.size()) == pattern) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos) - { - for(auto const& extension: supported_extensions) - { - if (name.compare(pos, name.size() - pos, extension) == 0) - { - mSplashScreens.push_back(found->first); - break; /* based on priority */ - } - } - } - } - else - break; - ++found; + if (isSupportedExtension(Misc::getFileExtension(name))) + mSplashScreens.push_back(name); } if (mSplashScreens.empty()) Log(Debug::Warning) << "Warning: no splash screens found!"; } - void LoadingScreen::setLabel(const std::string &label, bool important) + void LoadingScreen::setLabel(const std::string& label, bool important) { mImportantLabel = important; mLoadingText->setCaptionWithReplacing(label); int padding = mLoadingBox->getWidth() - mLoadingText->getWidth(); - MyGUI::IntSize size(mLoadingText->getTextSize().width+padding, mLoadingBox->getHeight()); + MyGUI::IntSize size(mLoadingText->getTextSize().width + padding, mLoadingBox->getHeight()); size.width = std::max(300, size.width); mLoadingBox->setSize(size); if (MWBase::Environment::get().getWindowManager()->getMessagesCount() > 0) - mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight()/2 - mLoadingBox->getHeight()/2); + mLoadingBox->setPosition(mMainWidget->getWidth() / 2 - mLoadingBox->getWidth() / 2, + mMainWidget->getHeight() / 2 - mLoadingBox->getHeight() / 2); else - mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight() - mLoadingBox->getHeight() - 8); + mLoadingBox->setPosition(mMainWidget->getWidth() / 2 - mLoadingBox->getWidth() / 2, + mMainWidget->getHeight() - mLoadingBox->getHeight() - 8); } void LoadingScreen::setVisible(bool visible) { WindowBase::setVisible(visible); - mBackgroundImage->setVisible(visible); + mSplashImage->setVisible(visible); mSceneImage->setVisible(visible); } @@ -141,7 +119,7 @@ namespace MWGui { } - void operator () (osg::RenderInfo& renderInfo) const override + void operator()(osg::RenderInfo& renderInfo) const override { int w = renderInfo.getCurrentCamera()->getViewport()->width(); int h = renderInfo.getCurrentCamera()->getViewport()->height(); @@ -150,10 +128,7 @@ namespace MWGui mOneshot = false; } - void reset() - { - mOneshot = true; - } + void reset() { mOneshot = true; } private: mutable bool mOneshot; @@ -166,7 +141,7 @@ namespace MWGui osg::BoundingSphere computeBound(const osg::Node&) const override { return osg::BoundingSphere(); } }; - void LoadingScreen::loadingOn(bool visible) + void LoadingScreen::loadingOn() { // Early-out if already on if (mNestedLoadingCount++ > 0 && mMainWidget->getVisible()) @@ -174,26 +149,19 @@ namespace MWGui mLoadingOnTime = mTimer.time_m(); - // Assign dummy bounding sphere callback to avoid the bounding sphere of the entire scene being recomputed after each frame of loading - // We are already using node masks to avoid the scene from being updated/rendered, but node masks don't work for computeBound() + // Assign dummy bounding sphere callback to avoid the bounding sphere of the entire scene being recomputed after + // each frame of loading We are already using node masks to avoid the scene from being updated/rendered, but + // node masks don't work for computeBound() mViewer->getSceneData()->setComputeBoundingSphereCallback(new DontComputeBoundCallback); - if (const osgUtil::IncrementalCompileOperation* ico = mViewer->getIncrementalCompileOperation()) { + if (const osgUtil::IncrementalCompileOperation* ico = mViewer->getIncrementalCompileOperation()) + { mOldIcoMin = ico->getMinimumTimeAvailableForGLCompileAndDeletePerFrame(); mOldIcoMax = ico->getMaximumNumOfObjectsToCompilePerFrame(); } - mVisible = visible; - mLoadingBox->setVisible(mVisible); setVisible(true); - if (!mVisible) - { - mShowWallpaper = false; - draw(); - return; - } - mShowWallpaper = MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame; if (mShowWallpaper) @@ -208,7 +176,6 @@ namespace MWGui { if (--mNestedLoadingCount > 0) return; - mLoadingBox->setVisible(true); // restore if (mLastRenderTime < mLoadingOnTime) { @@ -226,7 +193,6 @@ namespace MWGui mViewer->getSceneData()->setComputeBoundingSphereCallback(nullptr); mViewer->getSceneData()->dirtyBound(); - //std::cout << "loading took " << mTimer.time_m() - mLoadingOnTime << std::endl; setVisible(false); if (osgUtil::IncrementalCompileOperation* ico = mViewer->getIncrementalCompileOperation()) @@ -239,55 +205,58 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_LoadingWallpaper); } - void LoadingScreen::changeWallpaper () + void LoadingScreen::changeWallpaper() { if (!mSplashScreens.empty()) { - std::string const & randomSplash = mSplashScreens.at(Misc::Rng::rollDice(mSplashScreens.size())); + std::string const& randomSplash = mSplashScreens.at(Misc::Rng::rollDice(mSplashScreens.size())); // TODO: add option (filename pattern?) to use image aspect ratio instead of 4:3 - // we can't do this by default, because the Morrowind splash screens are 1024x1024, but should be displayed as 4:3 - bool stretch = Settings::Manager::getBool("stretch menu background", "GUI"); - mBackgroundImage->setVisible(true); - mBackgroundImage->setBackgroundImage(randomSplash, true, stretch); + // we can't do this by default, because the Morrowind splash screens are 1024x1024, but should be displayed + // as 4:3 + mSplashImage->setVisible(true); + mSplashImage->setBackgroundImage(randomSplash, true, Settings::gui().mStretchMenuBackground); } - mSceneImage->setBackgroundImage(""); + mSceneImage->setBackgroundImage({}); mSceneImage->setVisible(false); } - void LoadingScreen::setProgressRange (size_t range) + void LoadingScreen::setProgressRange(size_t range) { - mProgressBar->setScrollRange(range+1); + mProgressBar->setScrollRange(range + 1); mProgressBar->setScrollPosition(0); mProgressBar->setTrackSize(0); mProgress = 0; } - void LoadingScreen::setProgress (size_t value) + void LoadingScreen::setProgress(size_t value) { // skip expensive update if there isn't enough visible progress - if (mProgressBar->getWidth() <= 0 || value - mProgress < mProgressBar->getScrollRange()/mProgressBar->getWidth()) + if (mProgressBar->getWidth() <= 0 + || value - mProgress < mProgressBar->getScrollRange() / mProgressBar->getWidth()) return; - value = std::min(value, mProgressBar->getScrollRange()-1); + value = std::min(value, mProgressBar->getScrollRange() - 1); mProgress = value; mProgressBar->setScrollPosition(0); - mProgressBar->setTrackSize(static_cast(value / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); + mProgressBar->setTrackSize( + static_cast(value / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); draw(); } - void LoadingScreen::increaseProgress (size_t increase) + void LoadingScreen::increaseProgress(size_t increase) { mProgressBar->setScrollPosition(0); size_t value = mProgress + increase; - value = std::min(value, mProgressBar->getScrollRange()-1); + value = std::min(value, mProgressBar->getScrollRange() - 1); mProgress = value; - mProgressBar->setTrackSize(static_cast(value / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); + mProgressBar->setTrackSize( + static_cast(value / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); draw(); } bool LoadingScreen::needToDrawLoadingScreen() { - if ( mTimer.time_m() <= mLastRenderTime + (1.0/getTargetFrameRate()) * 1000.0) + if (mTimer.time_m() <= mLastRenderTime + (1.0 / getTargetFrameRate()) * 1000.0) return false; // the minimal delay before a loading screen shows @@ -303,7 +272,7 @@ namespace MWGui diff -= mProgress / static_cast(mProgressBar->getScrollRange()) * 100.f; } - if (!mShowWallpaper && diff < initialDelay*1000) + if (!mShowWallpaper && diff < initialDelay * 1000) return false; return true; } @@ -317,13 +286,15 @@ namespace MWGui if (!mTexture) { mTexture = new osg::Texture2D; + mTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mTexture->setInternalFormat(GL_RGB); mTexture->setResizeNonPowerOfTwoHint(false); } if (!mGuiTexture.get()) { - mGuiTexture.reset(new osgMyGUI::OSGTexture(mTexture)); + mGuiTexture = std::make_unique(mTexture); } if (!mCopyFramebufferToTextureCallback) @@ -331,16 +302,12 @@ namespace MWGui mCopyFramebufferToTextureCallback = new CopyFramebufferToTextureCallback(mTexture); } -#if OSG_VERSION_GREATER_OR_EQUAL(3, 5, 10) mViewer->getCamera()->removeInitialDrawCallback(mCopyFramebufferToTextureCallback); mViewer->getCamera()->addInitialDrawCallback(mCopyFramebufferToTextureCallback); -#else - mViewer->getCamera()->setInitialDrawCallback(mCopyFramebufferToTextureCallback); -#endif mCopyFramebufferToTextureCallback->reset(); - mBackgroundImage->setBackgroundImage(""); - mBackgroundImage->setVisible(false); + mSplashImage->setBackgroundImage({}); + mSplashImage->setVisible(false); mSceneImage->setRenderItemTexture(mGuiTexture.get()); mSceneImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); @@ -349,10 +316,10 @@ namespace MWGui void LoadingScreen::draw() { - if (mVisible && !needToDrawLoadingScreen()) + if (!needToDrawLoadingScreen()) return; - if (mShowWallpaper && mTimer.time_m() > mLastWallpaperChangeTime + 5000*1) + if (mShowWallpaper && mTimer.time_m() > mLastWallpaperChangeTime + 5000 * 1) { mLastWallpaperChangeTime = mTimer.time_m(); changeWallpaper(); @@ -365,10 +332,15 @@ namespace MWGui MWBase::Environment::get().getInputManager()->update(0, true, true); - mResourceSystem->reportStats(mViewer->getFrameStamp()->getFrameNumber(), mViewer->getViewerStats()); + osg::Stats* const stats = mViewer->getViewerStats(); + const unsigned frameNumber = mViewer->getFrameStamp()->getFrameNumber(); + + stats->setAttribute(frameNumber, "Loading", 1); + + mResourceSystem->reportStats(frameNumber, stats); if (osgUtil::IncrementalCompileOperation* ico = mViewer->getIncrementalCompileOperation()) { - ico->setMinimumTimeAvailableForGLCompileAndDeletePerFrame(1.f/getTargetFrameRate()); + ico->setMinimumTimeAvailableForGLCompileAndDeletePerFrame(1.f / getTargetFrameRate()); ico->setMaximumNumOfObjectsToCompilePerFrame(1000); } diff --git a/apps/openmw/mwgui/loadingscreen.hpp b/apps/openmw/mwgui/loadingscreen.hpp index c6439653427..35de331ee0b 100644 --- a/apps/openmw/mwgui/loadingscreen.hpp +++ b/apps/openmw/mwgui/loadingscreen.hpp @@ -37,12 +37,12 @@ namespace MWGui virtual ~LoadingScreen(); /// Overridden from Loading::Listener, see the Loading::Listener documentation for usage details - void setLabel (const std::string& label, bool important) override; - void loadingOn(bool visible=true) override; + void setLabel(const std::string& label, bool important) override; + void loadingOn() override; void loadingOff() override; - void setProgressRange (size_t range) override; - void setProgress (size_t value) override; - void increaseProgress (size_t increase=1) override; + void setProgressRange(size_t range) override; + void setProgress(size_t value) override; + void increaseProgress(size_t increase = 1) override; void setVisible(bool visible) override; @@ -66,7 +66,6 @@ namespace MWGui bool mImportantLabel; - bool mVisible; int mNestedLoadingCount; size_t mProgress; @@ -79,7 +78,7 @@ namespace MWGui MyGUI::TextBox* mLoadingText; MyGUI::ScrollBar* mProgressBar; - BackgroundImage* mBackgroundImage; + BackgroundImage* mSplashImage; BackgroundImage* mSceneImage; std::vector mSplashScreens; @@ -95,5 +94,4 @@ namespace MWGui } - #endif diff --git a/apps/openmw/mwgui/mainmenu.cpp b/apps/openmw/mwgui/mainmenu.cpp index 4e695710ff4..da747dd7a2e 100644 --- a/apps/openmw/mwgui/mainmenu.cpp +++ b/apps/openmw/mwgui/mainmenu.cpp @@ -1,46 +1,97 @@ #include "mainmenu.hpp" -#include #include #include +#include -#include -#include +#include +#include #include +#include +#include #include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwbase/statemanager.hpp" -#include "savegamedialog.hpp" -#include "confirmationdialog.hpp" +#include "../mwworld/globals.hpp" + #include "backgroundimage.hpp" +#include "confirmationdialog.hpp" +#include "savegamedialog.hpp" +#include "settingswindow.hpp" #include "videowidget.hpp" namespace MWGui { + void MenuVideo::run() + { + Misc::FrameRateLimiter frameRateLimiter + = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); + while (mRunning) + { + // If finished playing, start again + if (!mVideo->update()) + mVideo->playVideo("video\\menu_background.bik"); + frameRateLimiter.limit(); + } + } + + MenuVideo::MenuVideo(const VFS::Manager* vfs) + : mRunning(true) + { + // Use black background to correct aspect ratio + mVideoBackground = MyGUI::Gui::getInstance().createWidgetReal( + "ImageBox", 0, 0, 1, 1, MyGUI::Align::Default, "MainMenuBackground"); + mVideoBackground->setImageTexture("black"); + + mVideo = mVideoBackground->createWidget( + "ImageBox", 0, 0, 1, 1, MyGUI::Align::Stretch, "MainMenuBackground"); + mVideo->setVFS(vfs); + + mVideo->playVideo("video\\menu_background.bik"); + mThread = std::thread([this] { run(); }); + } + + void MenuVideo::resize(int screenWidth, int screenHeight) + { + const bool stretch = Settings::gui().mStretchMenuBackground; + mVideoBackground->setSize(screenWidth, screenHeight); + mVideo->autoResize(stretch); + mVideo->setVisible(true); + } + + MenuVideo::~MenuVideo() + { + mRunning = false; + mThread.join(); + try + { + MyGUI::Gui::getInstance().destroyWidget(mVideoBackground); + } + catch (const MyGUI::Exception& e) + { + Log(Debug::Error) << "Error in the destructor: " << e.what(); + } + } MainMenu::MainMenu(int w, int h, const VFS::Manager* vfs, const std::string& versionDescription) : WindowBase("openmw_mainmenu.layout") - , mWidth (w), mHeight (h) - , mVFS(vfs), mButtonBox(nullptr) + , mWidth(w) + , mHeight(h) + , mVFS(vfs) + , mButtonBox(nullptr) , mBackground(nullptr) - , mVideoBackground(nullptr) - , mVideo(nullptr) - , mSaveGameDialog(nullptr) { getWidget(mVersionText, "VersionText"); mVersionText->setCaption(versionDescription); - mHasAnimatedMenu = mVFS->exists("video/menu_background.bik"); + constexpr VFS::Path::NormalizedView menuBackgroundVideo("video/menu_background.bik"); - updateMenu(); - } + mHasAnimatedMenu = mVFS->exists(menuBackgroundVideo); - MainMenu::~MainMenu() - { - delete mSaveGameDialog; + updateMenu(); } void MainMenu::onResChange(int w, int h) @@ -49,16 +100,17 @@ namespace MWGui mHeight = h; updateMenu(); + if (mVideo) + mVideo->resize(w, h); } - void MainMenu::setVisible (bool visible) + void MainMenu::setVisible(bool visible) { if (visible) updateMenu(); - bool isMainMenu = - MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu) && - MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame; + bool isMainMenu = MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu) + && MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame; showBackground(isMainMenu); @@ -75,12 +127,12 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mButtons["return"]); } - Layout::setVisible (visible); + Layout::setVisible(visible); } void MainMenu::onNewGameConfirmed() { - MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_MainMenu); + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_MainMenu); MWBase::Environment::get().getStateManager()->newGame(); } @@ -89,18 +141,16 @@ namespace MWGui MWBase::Environment::get().getStateManager()->requestQuit(); } - void MainMenu::onButtonClicked(MyGUI::Widget *sender) + void MainMenu::onButtonClicked(MyGUI::Widget* sender) { - MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); - std::string name = *sender->getUserData(); - winMgr->playSound("Menu Click"); + const std::string& name = *sender->getUserData(); + winMgr->playSound(ESM::RefId::stringRefId("Menu Click")); if (name == "return") { - winMgr->removeGuiMode (GM_MainMenu); + winMgr->removeGuiMode(GM_MainMenu); } - else if (name == "options") - winMgr->pushGuiMode (GM_Settings); else if (name == "credits") winMgr->playVideo("mw_credits.bik", true); else if (name == "exitgame") @@ -110,7 +160,7 @@ namespace MWGui else { ConfirmationDialog* dialog = winMgr->getConfirmationDialog(); - dialog->askForConfirmation("#{sMessage2}"); + dialog->askForConfirmation("#{OMWEngine:QuitGameConfirmation}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &MainMenu::onExitConfirmed); dialog->eventCancelClicked.clear(); @@ -123,32 +173,31 @@ namespace MWGui else { ConfirmationDialog* dialog = winMgr->getConfirmationDialog(); - dialog->askForConfirmation("#{sNotifyMessage54}"); + dialog->askForConfirmation("#{OMWEngine:NewGameConfirmation}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &MainMenu::onNewGameConfirmed); dialog->eventCancelClicked.clear(); } } - - else + else if (name == "loadgame" || name == "savegame") { if (!mSaveGameDialog) - mSaveGameDialog = new SaveGameDialog(); - if (name == "loadgame") - mSaveGameDialog->setLoadOrSave(true); - else if (name == "savegame") - mSaveGameDialog->setLoadOrSave(false); + mSaveGameDialog = std::make_unique(); + mSaveGameDialog->setLoadOrSave(name == "loadgame"); mSaveGameDialog->setVisible(true); } + + if (winMgr->isSettingsWindowVisible() || name == "options") + { + winMgr->toggleSettingsWindow(); + } } void MainMenu::showBackground(bool show) { if (mVideo && !show) { - MyGUI::Gui::getInstance().destroyWidget(mVideoBackground); - mVideoBackground = nullptr; - mVideo = nullptr; + mVideo.reset(); } if (mBackground && !show) { @@ -159,68 +208,48 @@ namespace MWGui if (!show) return; - bool stretch = Settings::Manager::getBool("stretch menu background", "GUI"); + const bool stretch = Settings::gui().mStretchMenuBackground; if (mHasAnimatedMenu) { if (!mVideo) - { - // Use black background to correct aspect ratio - mVideoBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, - MyGUI::Align::Default, "Menu"); - mVideoBackground->setImageTexture("black"); + mVideo.emplace(mVFS); - mVideo = mVideoBackground->createWidget("ImageBox", 0,0,1,1, - MyGUI::Align::Stretch, "Menu"); - mVideo->setVFS(mVFS); - - mVideo->playVideo("video\\menu_background.bik"); - } - - MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + const auto& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); int screenWidth = viewSize.width; int screenHeight = viewSize.height; - mVideoBackground->setSize(screenWidth, screenHeight); - - mVideo->autoResize(stretch); - - mVideo->setVisible(true); + mVideo->resize(screenWidth, screenHeight); } else { if (!mBackground) { - mBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, - MyGUI::Align::Stretch, "Menu"); + mBackground = MyGUI::Gui::getInstance().createWidgetReal( + "ImageBox", 0, 0, 1, 1, MyGUI::Align::Stretch, "MainMenuBackground"); mBackground->setBackgroundImage("textures\\menu_morrowind.dds", true, stretch); } mBackground->setVisible(true); } } - void MainMenu::onFrame(float dt) + bool MainMenu::exit() { - if (mVideo) + if (MWBase::Environment::get().getWindowManager()->isSettingsWindowVisible()) { - if (!mVideo->update()) - { - // If finished playing, start again - mVideo->playVideo("video\\menu_background.bik"); - } + MWBase::Environment::get().getWindowManager()->toggleSettingsWindow(); + return false; } - } - bool MainMenu::exit() - { return MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_Running; } void MainMenu::updateMenu() { - setCoord(0,0, mWidth, mHeight); + setCoord(0, 0, mWidth, mHeight); if (!mButtonBox) - mButtonBox = mMainWidget->createWidget("", MyGUI::IntCoord(0, 0, 0, 0), MyGUI::Align::Default); + mButtonBox + = mMainWidget->createWidget({}, MyGUI::IntCoord(0, 0, 0, 0), MyGUI::Align::Default); int curH = 0; @@ -230,47 +259,46 @@ namespace MWGui std::vector buttons; - if (state==MWBase::StateManager::State_Running) + if (state == MWBase::StateManager::State_Running) buttons.emplace_back("return"); buttons.emplace_back("newgame"); - if (state==MWBase::StateManager::State_Running && - MWBase::Environment::get().getWorld()->getGlobalInt ("chargenstate")==-1 && - MWBase::Environment::get().getWindowManager()->isSavingAllowed()) + if (state == MWBase::StateManager::State_Running + && MWBase::Environment::get().getWorld()->getGlobalInt(MWWorld::Globals::sCharGenState) == -1 + && MWBase::Environment::get().getWindowManager()->isSavingAllowed()) buttons.emplace_back("savegame"); - if (MWBase::Environment::get().getStateManager()->characterBegin()!= - MWBase::Environment::get().getStateManager()->characterEnd()) + if (MWBase::Environment::get().getStateManager()->characterBegin() + != MWBase::Environment::get().getStateManager()->characterEnd()) buttons.emplace_back("loadgame"); buttons.emplace_back("options"); - if (state==MWBase::StateManager::State_NoGame) + if (state == MWBase::StateManager::State_NoGame) buttons.emplace_back("credits"); buttons.emplace_back("exitgame"); // Create new buttons if needed - std::vector allButtons { "return", "newgame", "savegame", "loadgame", "options", "credits", "exitgame"}; - for (std::string& buttonId : allButtons) + for (std::string_view id : { "return", "newgame", "savegame", "loadgame", "options", "credits", "exitgame" }) { - if (mButtons.find(buttonId) == mButtons.end()) + if (mButtons.find(id) == mButtons.end()) { - Gui::ImageButton* button = mButtonBox->createWidget - ("ImageBox", MyGUI::IntCoord(0, curH, 0, 0), MyGUI::Align::Default); + Gui::ImageButton* button = mButtonBox->createWidget( + "ImageBox", MyGUI::IntCoord(0, curH, 0, 0), MyGUI::Align::Default); + const std::string& buttonId = mButtons.emplace(id, button).first->first; button->setProperty("ImageHighlighted", "textures\\menu_" + buttonId + "_over.dds"); button->setProperty("ImageNormal", "textures\\menu_" + buttonId + ".dds"); button->setProperty("ImagePushed", "textures\\menu_" + buttonId + "_pressed.dds"); button->eventMouseButtonClick += MyGUI::newDelegate(this, &MainMenu::onButtonClicked); - button->setUserData(std::string(buttonId)); - mButtons[buttonId] = button; + button->setUserData(buttonId); } } // Start by hiding all buttons int maxwidth = 0; - for (auto& buttonPair : mButtons) + for (const auto& buttonPair : mButtons) { buttonPair.second->setVisible(false); MyGUI::IntSize requested = buttonPair.second->getRequestedSize(); @@ -279,10 +307,11 @@ namespace MWGui } // Now show and position the ones we want - for (std::string& buttonId : buttons) + for (const std::string& buttonId : buttons) { - assert(mButtons.find(buttonId) != mButtons.end()); - Gui::ImageButton* button = mButtons[buttonId]; + auto it = mButtons.find(buttonId); + assert(it != mButtons.end()); + Gui::ImageButton* button = it->second; button->setVisible(true); // By default, assume that all menu buttons textures should have 64 height. @@ -294,19 +323,19 @@ namespace MWGui // Trim off some of the excessive padding // TODO: perhaps do this within ImageButton? int height = requested.height; - button->setImageTile(MyGUI::IntSize(requested.width, requested.height-16*scale)); - button->setCoord((maxwidth-requested.width/scale) / 2, curH, requested.width/scale, height/scale-16); - curH += height/scale-16; + button->setImageTile(MyGUI::IntSize(requested.width, requested.height - 16 * scale)); + button->setCoord( + (maxwidth - requested.width / scale) / 2, curH, requested.width / scale, height / scale - 16); + curH += height / scale - 16; } if (state == MWBase::StateManager::State_NoGame) { // Align with the background image - int bottomPadding=24; - mButtonBox->setCoord (mWidth/2 - maxwidth/2, mHeight - curH - bottomPadding, maxwidth, curH); + int bottomPadding = 24; + mButtonBox->setCoord(mWidth / 2 - maxwidth / 2, mHeight - curH - bottomPadding, maxwidth, curH); } else - mButtonBox->setCoord (mWidth/2 - maxwidth/2, mHeight/2 - curH/2, maxwidth, curH); - + mButtonBox->setCoord(mWidth / 2 - maxwidth / 2, mHeight / 2 - curH / 2, maxwidth, curH); } } diff --git a/apps/openmw/mwgui/mainmenu.hpp b/apps/openmw/mwgui/mainmenu.hpp index 560eb93dcce..06a8c945c19 100644 --- a/apps/openmw/mwgui/mainmenu.hpp +++ b/apps/openmw/mwgui/mainmenu.hpp @@ -1,6 +1,11 @@ #ifndef OPENMW_GAME_MWGUI_MAINMENU_H #define OPENMW_GAME_MWGUI_MAINMENU_H +#include +#include +#include + +#include "savegamedialog.hpp" #include "windowbase.hpp" namespace Gui @@ -17,51 +22,59 @@ namespace MWGui { class BackgroundImage; - class SaveGameDialog; class VideoWidget; - - class MainMenu : public WindowBase + class MenuVideo { - int mWidth; - int mHeight; + MyGUI::ImageBox* mVideoBackground; + VideoWidget* mVideo; + std::thread mThread; + bool mRunning; - bool mHasAnimatedMenu; + void run(); - public: + public: + MenuVideo(const VFS::Manager* vfs); + void resize(int w, int h); + ~MenuVideo(); + }; + + class MainMenu : public WindowBase + { + int mWidth; + int mHeight; - MainMenu(int w, int h, const VFS::Manager* vfs, const std::string& versionDescription); - ~MainMenu(); + bool mHasAnimatedMenu; - void onResChange(int w, int h) override; + public: + MainMenu(int w, int h, const VFS::Manager* vfs, const std::string& versionDescription); - void setVisible (bool visible) override; + void onResChange(int w, int h) override; - void onFrame(float dt) override; + void setVisible(bool visible) override; - bool exit() override; + bool exit() override; - private: - const VFS::Manager* mVFS; + private: + const VFS::Manager* mVFS; - MyGUI::Widget* mButtonBox; - MyGUI::TextBox* mVersionText; + MyGUI::Widget* mButtonBox; + MyGUI::TextBox* mVersionText; - BackgroundImage* mBackground; + BackgroundImage* mBackground; - MyGUI::ImageBox* mVideoBackground; - VideoWidget* mVideo; // For animated main menus + std::optional mVideo; // For animated main menus - std::map mButtons; + std::map> mButtons; - void onButtonClicked (MyGUI::Widget* sender); - void onNewGameConfirmed(); - void onExitConfirmed(); + void onButtonClicked(MyGUI::Widget* sender); + void onNewGameConfirmed(); + void onExitConfirmed(); - void showBackground(bool show); + void showBackground(bool show); - void updateMenu(); + void updateMenu(); - SaveGameDialog* mSaveGameDialog; + std::unique_ptr mSaveGameDialog; }; } diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 6e8804a43fe..8cc63a08f77 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -2,38 +2,43 @@ #include -#include -#include -#include +#include +#include #include -#include +#include #include +#include +#include #include -#include +#include +#include -#include -#include -#include +#include +#include #include +#include +#include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwbase/environment.hpp" -#include "../mwworld/player.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/player.hpp" +#include "../mwworld/worldmodel.hpp" #include "../mwrender/globalmap.hpp" #include "../mwrender/localmap.hpp" #include "confirmationdialog.hpp" -#include "tooltips.hpp" + +#include namespace { const int cellSize = Constants::CellSizeInUnits; + constexpr float speed = 1.08f; // the zoom speed, it should be greater than 1 enum LocalMapWidgetDepth { @@ -52,7 +57,6 @@ namespace Global_MapLayer = 3 }; - /// @brief A widget that changes its color when hovered. class MarkerWidget final : public MyGUI::Widget { @@ -65,38 +69,52 @@ namespace setColour(colour); } - void setHoverColour(const MyGUI::Colour& colour) - { - mHoverColour = colour; - } + void setHoverColour(const MyGUI::Colour& colour) { mHoverColour = colour; } private: MyGUI::Colour mNormalColour; MyGUI::Colour mHoverColour; - void onMouseLostFocus(MyGUI::Widget* _new) override - { - setColour(mNormalColour); - } + void onMouseLostFocus(MyGUI::Widget* _new) override { setColour(mNormalColour); } - void onMouseSetFocus(MyGUI::Widget* _old) override - { - setColour(mHoverColour); - } + void onMouseSetFocus(MyGUI::Widget* _old) override { setColour(mHoverColour); } }; + + MyGUI::IntRect createRect(const MyGUI::IntPoint& center, int radius) + { + return { center.left - radius, center.top - radius, center.left + radius, center.top + radius }; + } + + int getLocalViewingDistance() + { + if (!Settings::map().mAllowZooming) + return Constants::CellGridRadius; + if (!Settings::terrain().mDistantTerrain) + return Constants::CellGridRadius; + const int viewingDistanceInCells = Settings::camera().mViewingDistance / Constants::CellSizeInUnits; + return std::clamp( + viewingDistanceInCells, Constants::CellGridRadius, Settings::map().mMaxLocalViewingDistance.get()); + } + + ESM::RefId getCellIdInWorldSpace(const MWWorld::Cell& cell, int x, int y) + { + if (cell.isExterior()) + return ESM::Cell::generateIdForCell(true, {}, x, y); + return cell.getId(); + } } namespace MWGui { - void CustomMarkerCollection::addMarker(const ESM::CustomMarker &marker, bool triggerEvent) + void CustomMarkerCollection::addMarker(const ESM::CustomMarker& marker, bool triggerEvent) { mMarkers.insert(std::make_pair(marker.mCell, marker)); if (triggerEvent) eventMarkersChanged(); } - void CustomMarkerCollection::deleteMarker(const ESM::CustomMarker &marker) + void CustomMarkerCollection::deleteMarker(const ESM::CustomMarker& marker) { std::pair range = mMarkers.equal_range(marker.mCell); @@ -112,7 +130,7 @@ namespace MWGui throw std::runtime_error("can't find marker to delete"); } - void CustomMarkerCollection::updateMarker(const ESM::CustomMarker &marker, const std::string &newNote) + void CustomMarkerCollection::updateMarker(const ESM::CustomMarker& marker, const std::string& newNote) { std::pair range = mMarkers.equal_range(marker.mCell); @@ -144,7 +162,7 @@ namespace MWGui return mMarkers.end(); } - CustomMarkerCollection::RangeType CustomMarkerCollection::getMarkers(const ESM::CellId &cellId) const + CustomMarkerCollection::RangeType CustomMarkerCollection::getMarkers(const ESM::RefId& cellId) const { return mMarkers.equal_range(cellId); } @@ -156,18 +174,15 @@ namespace MWGui // ------------------------------------------------------ - LocalMapBase::LocalMapBase(CustomMarkerCollection &markers, MWRender::LocalMap* localMapRender, bool fogOfWarEnabled) + LocalMapBase::LocalMapBase( + CustomMarkerCollection& markers, MWRender::LocalMap* localMapRender, bool fogOfWarEnabled) : mLocalMapRender(localMapRender) - , mCurX(0) - , mCurY(0) - , mInterior(false) + , mActiveCell(nullptr) , mLocalMap(nullptr) , mCompass(nullptr) - , mChanged(true) , mFogOfWarToggled(true) , mFogOfWarEnabled(fogOfWarEnabled) - , mMapWidgetSize(0) - , mNumCells(0) + , mNumCells(1) , mCellDistance(0) , mCustomMarkers(markers) , mMarkerUpdateTimer(0.0f) @@ -183,30 +198,31 @@ namespace MWGui mCustomMarkers.eventMarkersChanged -= MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); } - void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass) + void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, int cellDistance) { mLocalMap = widget; mCompass = compass; - mMapWidgetSize = std::max(1, Settings::Manager::getInt("local map widget size", "Map")); - mCellDistance = Constants::CellGridRadius; + mCellDistance = cellDistance; mNumCells = mCellDistance * 2 + 1; - mLocalMap->setCanvasSize(mMapWidgetSize*mNumCells, mMapWidgetSize*mNumCells); + const int mapWidgetSize = Settings::map().mLocalMapWidgetSize; + + mLocalMap->setCanvasSize(mapWidgetSize * mNumCells, mapWidgetSize * mNumCells); mCompass->setDepth(Local_CompassLayer); mCompass->setNeedMouseFocus(false); - for (int mx=0; mxcreateWidget("ImageBox", - MyGUI::IntCoord(mx*mMapWidgetSize, my*mMapWidgetSize, mMapWidgetSize, mMapWidgetSize), + MyGUI::IntCoord(mx * mapWidgetSize, my * mapWidgetSize, mapWidgetSize, mapWidgetSize), MyGUI::Align::Top | MyGUI::Align::Left); map->setDepth(Local_MapLayer); MyGUI::ImageBox* fog = mLocalMap->createWidget("ImageBox", - MyGUI::IntCoord(mx*mMapWidgetSize, my*mMapWidgetSize, mMapWidgetSize, mMapWidgetSize), + MyGUI::IntCoord(mx * mapWidgetSize, my * mapWidgetSize, mapWidgetSize, mapWidgetSize), MyGUI::Align::Top | MyGUI::Align::Left); fog->setDepth(Local_FogLayer); fog->setColour(MyGUI::Colour(0, 0, 0)); @@ -219,12 +235,6 @@ namespace MWGui } } - void LocalMapBase::setCellPrefix(const std::string& prefix) - { - mPrefix = prefix; - mChanged = true; - } - bool LocalMapBase::toggleFogOfWar() { mFogOfWarToggled = !mFogOfWarToggled; @@ -234,65 +244,97 @@ namespace MWGui void LocalMapBase::applyFogOfWar() { - for (int mx=0; mxsetImageTexture(""); - entry.mFogTexture.reset(); - continue; - } + entry.mFogWidget->setImageTexture({}); + entry.mFogTexture.reset(); } } redraw(); } - MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) + MyGUI::IntPoint LocalMapBase::getPosition(int cellX, int cellY, float nX, float nY) const { - MyGUI::IntPoint widgetPos; // normalized cell coordinates - float nX,nY; - - if (!mInterior) - { - int cellX, cellY; - MWBase::Environment::get().getWorld()->positionToIndex(worldX, worldY, cellX, cellY); - nX = (worldX - cellSize * cellX) / cellSize; - // Image space is -Y up, cells are Y up - nY = 1 - (worldY - cellSize * cellY) / cellSize; - - float cellDx = static_cast(cellX - mCurX); - float cellDy = static_cast(cellY - mCurY); + auto mapWidgetSize = getWidgetSize(); + return MyGUI::IntPoint(std::round((nX + mCellDistance + cellX - mActiveCell->getGridX()) * mapWidgetSize), + std::round((nY + mCellDistance - cellY + mActiveCell->getGridY()) * mapWidgetSize)); + } - markerPos.cellX = cellX; - markerPos.cellY = cellY; + MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) const + { + osg::Vec2i cellIndex; + // normalized cell coordinates + float nX, nY; - widgetPos = MyGUI::IntPoint(static_cast(nX * mMapWidgetSize + (mCellDistance + cellDx) * mMapWidgetSize), - static_cast(nY * mMapWidgetSize + (mCellDistance - cellDy) * mMapWidgetSize)); - } - else + if (mActiveCell->isExterior()) { - int cellX, cellY; - osg::Vec2f worldPos (worldX, worldY); - mLocalMapRender->worldToInteriorMapPosition(worldPos, nX, nY, cellX, cellY); - - markerPos.cellX = cellX; - markerPos.cellY = cellY; + ESM::ExteriorCellLocation cellPos = ESM::positionToExteriorCellLocation(worldX, worldY); + cellIndex.x() = cellPos.mX; + cellIndex.y() = cellPos.mY; + nX = (worldX - cellSize * cellIndex.x()) / cellSize; // Image space is -Y up, cells are Y up - widgetPos = MyGUI::IntPoint(static_cast(nX * mMapWidgetSize + (mCellDistance + (cellX - mCurX)) * mMapWidgetSize), - static_cast(nY * mMapWidgetSize + (mCellDistance - (cellY - mCurY)) * mMapWidgetSize)); + nY = 1 - (worldY - cellSize * cellIndex.y()) / cellSize; } + else + mLocalMapRender->worldToInteriorMapPosition({ worldX, worldY }, nX, nY, cellIndex.x(), cellIndex.y()); + markerPos.cellX = cellIndex.x(); + markerPos.cellY = cellIndex.y(); markerPos.nX = nX; markerPos.nY = nY; - return widgetPos; + return getPosition(markerPos.cellX, markerPos.cellY, markerPos.nX, markerPos.nY); + } + + MyGUI::IntCoord LocalMapBase::getMarkerCoordinates( + float worldX, float worldY, MarkerUserData& markerPos, size_t markerSize) const + { + int halfMarkerSize = markerSize / 2; + auto position = getMarkerPosition(worldX, worldY, markerPos); + return MyGUI::IntCoord(position.left - halfMarkerSize, position.top - halfMarkerSize, markerSize, markerSize); + } + + MyGUI::Widget* LocalMapBase::createDoorMarker(const std::string& name, float x, float y) const + { + MarkerUserData data(mLocalMapRender); + data.caption = name; + MarkerWidget* markerWidget = mLocalMap->createWidget( + "MarkerButton", getMarkerCoordinates(x, y, data, 8), MyGUI::Align::Default); + markerWidget->setNormalColour( + MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); + markerWidget->setHoverColour( + MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal_over}"))); + markerWidget->setDepth(Local_MarkerLayer); + markerWidget->setNeedMouseFocus(true); + // Used by tooltips to not show the tooltip if marker is hidden by fog of war + markerWidget->setUserString("ToolTipType", "MapMarker"); + + markerWidget->setUserData(data); + return markerWidget; + } + + void LocalMapBase::centerView() + { + MyGUI::IntPoint pos = mCompass->getPosition() + MyGUI::IntPoint{ 16, 16 }; + MyGUI::IntSize viewsize = mLocalMap->getSize(); + MyGUI::IntPoint viewOffset((viewsize.width / 2) - pos.left, (viewsize.height / 2) - pos.top); + mLocalMap->setViewOffset(viewOffset); + } + + MyGUI::IntCoord LocalMapBase::getMarkerCoordinates(MyGUI::Widget* widget, size_t markerSize) const + { + MarkerUserData& markerPos(*widget->getUserData()); + auto position = getPosition(markerPos.cellX, markerPos.cellY, markerPos.nX, markerPos.nY); + return MyGUI::IntCoord(position.left - markerSize / 2, position.top - markerSize / 2, markerSize, markerSize); + } + + std::vector& LocalMapBase::currentDoorMarkersWidgets() + { + return mActiveCell->isExterior() ? mExteriorDoorMarkerWidgets : mInteriorDoorMarkerWidgets; } void LocalMapBase::updateCustomMarkers() @@ -300,30 +342,24 @@ namespace MWGui for (MyGUI::Widget* widget : mCustomMarkerWidgets) MyGUI::Gui::getInstance().destroyWidget(widget); mCustomMarkerWidgets.clear(); - + if (!mActiveCell) + return; for (int dX = -mCellDistance; dX <= mCellDistance; ++dX) { - for (int dY =-mCellDistance; dY <= mCellDistance; ++dY) + for (int dY = -mCellDistance; dY <= mCellDistance; ++dY) { - ESM::CellId cellId; - cellId.mPaged = !mInterior; - cellId.mWorldspace = (mInterior ? mPrefix : ESM::CellId::sDefaultWorldspace); - cellId.mIndex.mX = mCurX+dX; - cellId.mIndex.mY = mCurY+dY; - - CustomMarkerCollection::RangeType markers = mCustomMarkers.getMarkers(cellId); - for (CustomMarkerCollection::ContainerType::const_iterator it = markers.first; it != markers.second; ++it) + ESM::RefId cellRefId + = getCellIdInWorldSpace(*mActiveCell, mActiveCell->getGridX() + dX, mActiveCell->getGridY() + dY); + + CustomMarkerCollection::RangeType markers = mCustomMarkers.getMarkers(cellRefId); + for (CustomMarkerCollection::ContainerType::const_iterator it = markers.first; it != markers.second; + ++it) { const ESM::CustomMarker& marker = it->second; - MarkerUserData markerPos (mLocalMapRender); - MyGUI::IntPoint widgetPos = getMarkerPosition(marker.mWorldX, marker.mWorldY, markerPos); - - MyGUI::IntCoord widgetCoord(widgetPos.left - 8, - widgetPos.top - 8, - 16, 16); + MarkerUserData markerPos(mLocalMapRender); MarkerWidget* markerWidget = mLocalMap->createWidget("CustomMarkerButton", - widgetCoord, MyGUI::Align::Default); + getMarkerCoordinates(marker.mWorldX, marker.mWorldY, markerPos, 16), MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setUserString("ToolTipType", "Layout"); markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); @@ -341,21 +377,52 @@ namespace MWGui redraw(); } - void LocalMapBase::setActiveCell(const int x, const int y, bool interior) + void LocalMapBase::setActiveCell(const MWWorld::Cell& cell) { - if (x==mCurX && y==mCurY && mInterior==interior && !mChanged) + if (&cell == mActiveCell) return; // don't do anything if we're still in the same cell - mCurX = x; - mCurY = y; - mInterior = interior; - mChanged = false; + const int x = cell.getGridX(); + const int y = cell.getGridY(); - for (int mx=0; mxsetVisible(false); + + if (mHasALastActiveCell) { - MapEntry& entry = mMaps[my + mNumCells*mx]; + for (const auto& entry : mMaps) + { + if (!currentView.inside({ entry.mCellX, entry.mCellY })) + mLocalMapRender->removeExteriorCell(entry.mCellX, entry.mCellY); + } + } + } + + mActiveCell = &cell; + + for (int mx = 0; mx < mNumCells; ++mx) + { + for (int my = 0; my < mNumCells; ++my) + { + MapEntry& entry = mMaps[my + mNumCells * mx]; entry.mMapWidget->setRenderItemTexture(nullptr); entry.mFogWidget->setRenderItemTexture(nullptr); entry.mMapTexture.reset(); @@ -370,11 +437,17 @@ namespace MWGui // If we don't do this, door markers that should be disabled will still appear on the map. mNeedDoorMarkersUpdate = true; + for (MyGUI::Widget* widget : currentDoorMarkersWidgets()) + widget->setCoord(getMarkerCoordinates(widget, 8)); + + if (mActiveCell->isExterior()) + mHasALastActiveCell = true; + updateMagicMarkers(); updateCustomMarkers(); } - void LocalMapBase::requestMapRender(const MWWorld::CellStore *cell) + void LocalMapBase::requestMapRender(const MWWorld::CellStore* cell) { mLocalMapRender->requestMap(cell); } @@ -385,21 +458,26 @@ namespace MWGui mLocalMap->getParent()->_updateChilds(); } + float LocalMapBase::getWidgetSize() const + { + return mLocalMapZoom * Settings::map().mLocalMapWidgetSize; + } + void LocalMapBase::setPlayerPos(int cellX, int cellY, const float nx, const float ny) { - MyGUI::IntPoint pos(static_cast(mMapWidgetSize * mCellDistance + nx*mMapWidgetSize - 16), static_cast(mMapWidgetSize * mCellDistance + ny*mMapWidgetSize - 16)); - pos.left += (cellX - mCurX) * mMapWidgetSize; - pos.top -= (cellY - mCurY) * mMapWidgetSize; + MyGUI::IntPoint pos = getPosition(cellX, cellY, nx, ny) - MyGUI::IntPoint{ 16, 16 }; if (pos != mCompass->getPosition()) { - notifyPlayerUpdate (); + notifyPlayerUpdate(); mCompass->setPosition(pos); - MyGUI::IntPoint middle (pos.left+16, pos.top+16); - MyGUI::IntCoord viewsize = mLocalMap->getCoord(); - MyGUI::IntPoint viewOffset((viewsize.width / 2) - middle.left, (viewsize.height / 2) - middle.top); - mLocalMap->setViewOffset(viewOffset); + } + osg::Vec2f curPos((cellX + nx) * cellSize, (cellY + 1 - ny) * cellSize); + if ((curPos - mCurPos).length2() > 0.001) + { + mCurPos = curPos; + centerView(); } } @@ -408,12 +486,12 @@ namespace MWGui if (x == mLastDirectionX && y == mLastDirectionY) return; - notifyPlayerUpdate (); + notifyPlayerUpdate(); MyGUI::ISubWidget* main = mCompass->getSubWidgetMain(); MyGUI::RotatingSkin* rotatingSubskin = main->castType(); - rotatingSubskin->setCenter(MyGUI::IntPoint(16,16)); - float angle = std::atan2(x,y); + rotatingSubskin->setCenter(MyGUI::IntPoint(16, 16)); + float angle = std::atan2(x, y); rotatingSubskin->setAngle(angle); mLastDirectionX = x; @@ -424,13 +502,11 @@ namespace MWGui { std::vector markers; MWBase::World* world = MWBase::Environment::get().getWorld(); - world->listDetectedReferences( - world->getPlayerPtr(), - markers, MWBase::World::DetectionType(type)); + world->listDetectedReferences(world->getPlayerPtr(), markers, MWBase::World::DetectionType(type)); if (markers.empty()) return; - std::string markerTexture; + std::string_view markerTexture; if (type == MWBase::World::Detect_Creature) { markerTexture = "textures\\detect_animal_icon.dds"; @@ -444,22 +520,17 @@ namespace MWGui markerTexture = "textures\\detect_enchantment_icon.dds"; } - int counter = 0; for (const MWWorld::Ptr& ptr : markers) { const ESM::Position& worldPos = ptr.getRefData().getPosition(); - MarkerUserData markerPos (mLocalMapRender); - MyGUI::IntPoint widgetPos = getMarkerPosition(worldPos.pos[0], worldPos.pos[1], markerPos); - MyGUI::IntCoord widgetCoord(widgetPos.left - 4, - widgetPos.top - 4, - 8, 8); - ++counter; + MarkerUserData markerPos(mLocalMapRender); MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", - widgetCoord, MyGUI::Align::Default); + getMarkerCoordinates(worldPos.pos[0], worldPos.pos[1], markerPos, 8), MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture(markerTexture); - markerWidget->setImageCoord(MyGUI::IntCoord(0,0,8,8)); + markerWidget->setImageCoord(MyGUI::IntCoord(0, 0, 8, 8)); markerWidget->setNeedMouseFocus(false); + markerWidget->setUserData(markerPos); mMagicMarkerWidgets.push_back(markerWidget); } } @@ -508,33 +579,34 @@ namespace MWGui if (!entry.mMapTexture) { - if (!mInterior) - requestMapRender(MWBase::Environment::get().getWorld()->getExterior (entry.mCellX, entry.mCellY)); + if (mActiveCell->isExterior()) + requestMapRender(&MWBase::Environment::get().getWorldModel()->getExterior( + ESM::ExteriorCellLocation(entry.mCellX, entry.mCellY, ESM::Cell::sDefaultWorldspaceId))); osg::ref_ptr texture = mLocalMapRender->getMapTexture(entry.mCellX, entry.mCellY); if (texture) { - entry.mMapTexture.reset(new osgMyGUI::OSGTexture(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.reset(new osgMyGUI::OSGTexture("", 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.reset(new osgMyGUI::OSGTexture(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.reset(new osgMyGUI::OSGTexture("", nullptr)); + entry.mFogTexture = std::make_unique(std::string(), nullptr); } needRedraw = true; } @@ -545,63 +617,72 @@ namespace MWGui void LocalMapBase::updateDoorMarkers() { - // clear all previous door markers - for (MyGUI::Widget* widget : mDoorMarkerWidgets) - MyGUI::Gui::getInstance().destroyWidget(widget); - mDoorMarkerWidgets.clear(); - + std::vector doors; MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::WorldModel* worldModel = MWBase::Environment::get().getWorldModel(); - // Retrieve the door markers we want to show - std::vector doors; - if (mInterior) + mDoorMarkersToRecycle.insert( + mDoorMarkersToRecycle.end(), mInteriorDoorMarkerWidgets.begin(), mInteriorDoorMarkerWidgets.end()); + mInteriorDoorMarkerWidgets.clear(); + + if (!mActiveCell->isExterior()) { - MWWorld::CellStore* cell = world->getInterior (mPrefix); + for (MyGUI::Widget* widget : mExteriorDoorMarkerWidgets) + widget->setVisible(false); + + MWWorld::CellStore& cell = worldModel->getInterior(mActiveCell->getNameId()); world->getDoorMarkers(cell, doors); } else { - for (int dX=-mCellDistance; dX<=mCellDistance; ++dX) + for (MapEntry& entry : mMaps) { - for (int dY=-mCellDistance; dY<=mCellDistance; ++dY) - { - MWWorld::CellStore* cell = world->getExterior (mCurX+dX, mCurY+dY); - world->getDoorMarkers(cell, doors); - } + if (!entry.mMapTexture && !widgetCropped(entry.mMapWidget, mLocalMap)) + world->getDoorMarkers(worldModel->getExterior(ESM::ExteriorCellLocation( + entry.mCellX, entry.mCellY, ESM::Cell::sDefaultWorldspaceId)), + doors); } + if (doors.empty()) + return; } // Create a widget for each marker - int counter = 0; for (MWBase::World::DoorMarker& marker : doors) { std::vector destNotes; CustomMarkerCollection::RangeType markers = mCustomMarkers.getMarkers(marker.dest); - for (CustomMarkerCollection::ContainerType::const_iterator iter = markers.first; iter != markers.second; ++iter) + for (CustomMarkerCollection::ContainerType::const_iterator iter = markers.first; iter != markers.second; + ++iter) destNotes.push_back(iter->second.mNote); - MarkerUserData data (mLocalMapRender); - data.notes = destNotes; - data.caption = marker.name; - MyGUI::IntPoint widgetPos = getMarkerPosition(marker.x, marker.y, data); - MyGUI::IntCoord widgetCoord(widgetPos.left - 4, - widgetPos.top - 4, - 8, 8); - ++counter; - MarkerWidget* markerWidget = mLocalMap->createWidget("MarkerButton", - widgetCoord, MyGUI::Align::Default); - markerWidget->setNormalColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); - markerWidget->setHoverColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal_over}"))); - markerWidget->setDepth(Local_MarkerLayer); - markerWidget->setNeedMouseFocus(true); - // Used by tooltips to not show the tooltip if marker is hidden by fog of war - markerWidget->setUserString("ToolTipType", "MapMarker"); - - markerWidget->setUserData(data); - doorMarkerCreated(markerWidget); + MyGUI::Widget* markerWidget = nullptr; + MarkerUserData* data; + if (mDoorMarkersToRecycle.empty()) + { + markerWidget = createDoorMarker(marker.name, marker.x, marker.y); + data = markerWidget->getUserData(); + data->notes = std::move(destNotes); + doorMarkerCreated(markerWidget); + } + else + { + markerWidget = (MarkerWidget*)mDoorMarkersToRecycle.back(); + mDoorMarkersToRecycle.pop_back(); + + data = markerWidget->getUserData(); + data->notes = std::move(destNotes); + data->caption = marker.name; + markerWidget->setCoord(getMarkerCoordinates(marker.x, marker.y, *data, 8)); + markerWidget->setVisible(true); + } - mDoorMarkerWidgets.push_back(markerWidget); + currentDoorMarkersWidgets().push_back(markerWidget); + if (mActiveCell->isExterior()) + mExteriorDoorsByCell[{ data->cellX, data->cellY }].push_back(markerWidget); } + + for (auto& widget : mDoorMarkersToRecycle) + widget->setVisible(false); } void LocalMapBase::updateMagicMarkers() @@ -619,38 +700,65 @@ namespace MWGui MWWorld::CellStore* markedCell = nullptr; ESM::Position markedPosition; MWBase::Environment::get().getWorld()->getPlayer().getMarkedPosition(markedCell, markedPosition); - if (markedCell && markedCell->isExterior() == !mInterior - && (!mInterior || Misc::StringUtils::ciEqual(markedCell->getCell()->mName, mPrefix))) + if (markedCell && markedCell->getCell()->getWorldSpace() == mActiveCell->getWorldSpace()) { - MarkerUserData markerPos (mLocalMapRender); - MyGUI::IntPoint widgetPos = getMarkerPosition(markedPosition.pos[0], markedPosition.pos[1], markerPos); - MyGUI::IntCoord widgetCoord(widgetPos.left - 4, - widgetPos.top - 4, - 8, 8); + MarkerUserData markerPos(mLocalMapRender); MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", - widgetCoord, MyGUI::Align::Default); + getMarkerCoordinates(markedPosition.pos[0], markedPosition.pos[1], markerPos, 8), + MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture("textures\\menu_map_smark.dds"); markerWidget->setNeedMouseFocus(false); + markerWidget->setUserData(markerPos); mMagicMarkerWidgets.push_back(markerWidget); } redraw(); } - // ------------------------------------------------------------------------------------------ + void LocalMapBase::updateLocalMap() + { + auto mapWidgetSize = getWidgetSize(); + mLocalMap->setCanvasSize(mapWidgetSize * mNumCells, mapWidgetSize * mNumCells); + + const auto size = MyGUI::IntSize(std::ceil(mapWidgetSize), std::ceil(mapWidgetSize)); + for (auto& entry : mMaps) + { + const auto position = getPosition(entry.mCellX, entry.mCellY, 0, 0); + entry.mMapWidget->setCoord({ position, size }); + entry.mFogWidget->setCoord({ position, size }); + } + + MarkerUserData markerPos(mLocalMapRender); + for (MyGUI::Widget* widget : currentDoorMarkersWidgets()) + widget->setCoord(getMarkerCoordinates(widget, 8)); + + for (MyGUI::Widget* widget : mCustomMarkerWidgets) + { + const auto& marker = *widget->getUserData(); + widget->setCoord(getMarkerCoordinates(marker.mWorldX, marker.mWorldY, markerPos, 16)); + } - MapWindow::MapWindow(CustomMarkerCollection &customMarkers, DragAndDrop* drag, MWRender::LocalMap* localMapRender, SceneUtil::WorkQueue* workQueue) + for (MyGUI::Widget* widget : mMagicMarkerWidgets) + widget->setCoord(getMarkerCoordinates(widget, 8)); + } + + // ------------------------------------------------------------------------------------------ + MapWindow::MapWindow(CustomMarkerCollection& customMarkers, DragAndDrop* drag, MWRender::LocalMap* localMapRender, + SceneUtil::WorkQueue* workQueue) +#ifdef USE_OPENXR + : WindowPinnableBase("openmw_map_window_vr.layout") +#else : WindowPinnableBase("openmw_map_window.layout") - , LocalMapBase(customMarkers, localMapRender) +#endif + , LocalMapBase(customMarkers, localMapRender, true) , NoDrop(drag, mMainWidget) , mGlobalMap(nullptr) , mGlobalMapImage(nullptr) , mGlobalMapOverlay(nullptr) - , mGlobal(Settings::Manager::getBool("global", "Map")) , mEventBoxGlobal(nullptr) , mEventBoxLocal(nullptr) - , mGlobalMapRender(new MWRender::GlobalMap(localMapRender->getRoot(), workQueue)) + , mGlobalMapRender(std::make_unique(localMapRender->getRoot(), workQueue)) , mEditNoteDialog() { static bool registered = false; @@ -664,7 +772,7 @@ namespace MWGui mEditNoteDialog.eventOkClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditOk); mEditNoteDialog.eventDeleteClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditDelete); - setCoord(500,0,320,300); + setCoord(500, 0, 320, 300); getWidget(mLocalMap, "LocalMap"); getWidget(mGlobalMap, "GlobalMap"); @@ -681,26 +789,36 @@ namespace MWGui mLastScrollWindowCoordinates = mLocalMap->getCoord(); mLocalMap->eventChangeCoord += MyGUI::newDelegate(this, &MapWindow::onChangeScrollWindowCoord); - mGlobalMap->setVisible (false); + mGlobalMap->setVisible(false); getWidget(mButton, "WorldButton"); mButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MapWindow::onWorldButtonClicked); - mButton->setCaptionWithReplacing( mGlobal ? "#{sLocal}" : "#{sWorld}"); + + const bool global = Settings::map().mGlobal; + + mButton->setCaptionWithReplacing(global ? "#{sLocal}" : "#{sWorld}"); getWidget(mEventBoxGlobal, "EventBoxGlobal"); mEventBoxGlobal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxGlobal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + + const bool allowZooming = Settings::map().mAllowZooming; + + if (allowZooming) + mEventBoxGlobal->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); mEventBoxGlobal->setDepth(Global_ExploreOverlayLayer); getWidget(mEventBoxLocal, "EventBoxLocal"); mEventBoxLocal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxLocal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); mEventBoxLocal->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onMapDoubleClicked); + if (allowZooming) + mEventBoxLocal->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); - LocalMapBase::init(mLocalMap, mPlayerArrowLocal); + LocalMapBase::init(mLocalMap, mPlayerArrowLocal, getLocalViewingDistance()); - mGlobalMap->setVisible(mGlobal); - mLocalMap->setVisible(!mGlobal); + mGlobalMap->setVisible(global); + mLocalMap->setVisible(!global); } void MapWindow::onNoteEditOk() @@ -732,7 +850,7 @@ namespace MWGui mEditNoteDialog.setVisible(false); } - void MapWindow::onCustomMarkerDoubleClicked(MyGUI::Widget *sender) + void MapWindow::onCustomMarkerDoubleClicked(MyGUI::Widget* sender) { mEditingMarker = *sender->getUserData(); mEditNoteDialog.setText(mEditingMarker.mNote); @@ -740,53 +858,146 @@ namespace MWGui mEditNoteDialog.setVisible(true); } - void MapWindow::onMapDoubleClicked(MyGUI::Widget *sender) + void MapWindow::onMapDoubleClicked(MyGUI::Widget* sender) { MyGUI::IntPoint clickedPos = MyGUI::InputManager::getInstance().getMousePosition(); MyGUI::IntPoint widgetPos = clickedPos - mEventBoxLocal->getAbsolutePosition(); - int x = int(widgetPos.left/float(mMapWidgetSize))-mCellDistance; - int y = (int(widgetPos.top/float(mMapWidgetSize))-mCellDistance)*-1; - float nX = widgetPos.left/float(mMapWidgetSize) - int(widgetPos.left/float(mMapWidgetSize)); - float nY = widgetPos.top/float(mMapWidgetSize) - int(widgetPos.top/float(mMapWidgetSize)); - x += mCurX; - y += mCurY; + auto mapWidgetSize = getWidgetSize(); + int x = int(widgetPos.left / float(mapWidgetSize)) - mCellDistance; + int y = (int(widgetPos.top / float(mapWidgetSize)) - mCellDistance) * -1; + float nX = widgetPos.left / float(mapWidgetSize) - int(widgetPos.left / float(mapWidgetSize)); + float nY = widgetPos.top / float(mapWidgetSize) - int(widgetPos.top / float(mapWidgetSize)); + x += mActiveCell->getGridX(); + y += mActiveCell->getGridY(); osg::Vec2f worldPos; - if (mInterior) + if (!mActiveCell->isExterior()) { worldPos = mLocalMapRender->interiorMapToWorldPosition(nX, nY, x, y); } else { worldPos.x() = (x + nX) * cellSize; - worldPos.y() = (y + (1.0f-nY)) * cellSize; + worldPos.y() = (y + (1.0f - nY)) * cellSize; } mEditingMarker.mWorldX = worldPos.x(); mEditingMarker.mWorldY = worldPos.y(); + ESM::RefId clickedId = getCellIdInWorldSpace(*mActiveCell, x, y); + + mEditingMarker.mCell = clickedId; + + mEditNoteDialog.setVisible(true); + mEditNoteDialog.showDeleteButton(false); + mEditNoteDialog.setText({}); + } + + void MapWindow::onMapZoomed(MyGUI::Widget* sender, int rel) + { + const int localWidgetSize = Settings::map().mLocalMapWidgetSize; + const bool zoomOut = rel < 0; + const bool zoomIn = !zoomOut; + const double speedDiff = zoomOut ? 1.0 / speed : speed; + const float localMapSizeInUnits = localWidgetSize * mNumCells; + + const float currentMinLocalMapZoom = std::max({ (float(Settings::map().mGlobalMapCellSize) * 4.f) + / float(localWidgetSize), + float(mLocalMap->getWidth()) / localMapSizeInUnits, float(mLocalMap->getHeight()) / localMapSizeInUnits }); + + if (Settings::map().mGlobal) + { + const float currentGlobalZoom = mGlobalMapZoom; + const float currentMinGlobalMapZoom + = std::min(float(mGlobalMap->getWidth()) / float(mGlobalMapRender->getWidth()), + float(mGlobalMap->getHeight()) / float(mGlobalMapRender->getHeight())); - mEditingMarker.mCell.mPaged = !mInterior; - if (mInterior) - mEditingMarker.mCell.mWorldspace = LocalMapBase::mPrefix; + mGlobalMapZoom *= speedDiff; + + if (zoomIn && mGlobalMapZoom > 4.f) + { + mGlobalMapZoom = currentGlobalZoom; + mLocalMapZoom = currentMinLocalMapZoom; + onWorldButtonClicked(nullptr); + updateLocalMap(); + return; // the zoom in is too big + } + + if (zoomOut && mGlobalMapZoom < currentMinGlobalMapZoom) + { + mGlobalMapZoom = currentGlobalZoom; + return; // the zoom out is too big, we have reach the borders of the widget + } + } else { - mEditingMarker.mCell.mWorldspace = ESM::CellId::sDefaultWorldspace; - mEditingMarker.mCell.mIndex.mX = x; - mEditingMarker.mCell.mIndex.mY = y; + auto const currentLocalZoom = mLocalMapZoom; + mLocalMapZoom *= speedDiff; + + if (zoomIn && mLocalMapZoom > 4.0f) + { + mLocalMapZoom = currentLocalZoom; + return; // the zoom in is too big + } + + if (zoomOut && mLocalMapZoom < currentMinLocalMapZoom) + { + mLocalMapZoom = currentLocalZoom; + + float zoomRatio = 4.f / mGlobalMapZoom; + mGlobalMapZoom = 4.f; + onWorldButtonClicked(nullptr); + + zoomOnCursor(zoomRatio); + return; // the zoom out is too big, we switch to the global map + } + + if (zoomOut) + mNeedDoorMarkersUpdate = true; } + zoomOnCursor(speedDiff); + } - mEditNoteDialog.setVisible(true); - mEditNoteDialog.showDeleteButton(false); - mEditNoteDialog.setText(""); + void MapWindow::zoomOnCursor(float speedDiff) + { + auto map = Settings::map().mGlobal ? mGlobalMap : mLocalMap; + auto cursor = MyGUI::InputManager::getInstance().getMousePosition() - map->getAbsolutePosition(); + auto centerView = map->getViewOffset() - cursor; + + Settings::map().mGlobal ? updateGlobalMap() : updateLocalMap(); + + map->setViewOffset(MyGUI::IntPoint(std::round(centerView.left * speedDiff) + cursor.left, + std::round(centerView.top * speedDiff) + cursor.top)); + } + + void MapWindow::updateGlobalMap() + { + resizeGlobalMap(); + + float x = mCurPos.x(), y = mCurPos.y(); + if (!mActiveCell->isExterior()) + { + auto pos = MWBase::Environment::get().getWorld()->getPlayer().getLastKnownExteriorPosition(); + x = pos.x(); + y = pos.y(); + } + setGlobalMapPlayerPosition(x, y); + + for (auto& [marker, col] : mGlobalMapMarkers) + { + marker.widget->setCoord(createMarkerCoords(marker.position.x(), marker.position.y(), col.size())); + marker.widget->setVisible(marker.widget->getHeight() >= 6); + } } void MapWindow::onChangeScrollWindowCoord(MyGUI::Widget* sender) { MyGUI::IntCoord currentCoordinates = sender->getCoord(); - MyGUI::IntPoint currentViewPortCenter = MyGUI::IntPoint(currentCoordinates.width / 2, currentCoordinates.height / 2); - MyGUI::IntPoint lastViewPortCenter = MyGUI::IntPoint(mLastScrollWindowCoordinates.width / 2, mLastScrollWindowCoordinates.height / 2); + MyGUI::IntPoint currentViewPortCenter + = MyGUI::IntPoint(currentCoordinates.width / 2, currentCoordinates.height / 2); + MyGUI::IntPoint lastViewPortCenter + = MyGUI::IntPoint(mLastScrollWindowCoordinates.width / 2, mLastScrollWindowCoordinates.height / 2); MyGUI::IntPoint viewPortCenterDiff = currentViewPortCenter - lastViewPortCenter; mLocalMap->setViewOffset(mLocalMap->getViewOffset() + viewPortCenterDiff); @@ -804,20 +1015,50 @@ namespace MWGui void MapWindow::renderGlobalMap() { mGlobalMapRender->render(); - mGlobalMap->setCanvasSize (mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); - mGlobalMapImage->setSize(mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); + resizeGlobalMap(); } - MapWindow::~MapWindow() - { - delete mGlobalMapRender; - } + MapWindow::~MapWindow() = default; void MapWindow::setCellName(const std::string& cellName) { setTitle("#{sCell=" + cellName + "}"); } + MyGUI::IntCoord MapWindow::createMarkerCoords(float x, float y, float agregatedWeight) const + { + float worldX, worldY; + worldPosToGlobalMapImageSpace( + (x + 0.5f) * Constants::CellSizeInUnits, (y + 0.5f) * Constants::CellSizeInUnits, worldX, worldY); + + const float markerSize = getMarkerSize(agregatedWeight); + const float halfMarkerSize = markerSize / 2.0f; + return MyGUI::IntCoord(static_cast(worldX - halfMarkerSize), static_cast(worldY - halfMarkerSize), + markerSize, markerSize); + } + + MyGUI::Widget* MapWindow::createMarker(const std::string& name, float x, float y, float agregatedWeight) + { + MyGUI::Widget* markerWidget = mGlobalMap->createWidget( + "MarkerButton", createMarkerCoords(x, y, agregatedWeight), MyGUI::Align::Default); + markerWidget->setVisible(markerWidget->getHeight() >= 6.0); + markerWidget->setUserString("Caption_TextOneLine", "#{sCell=" + name + "}"); + setGlobalMapMarkerTooltip(markerWidget, x, y); + + markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); + + markerWidget->setNeedMouseFocus(true); + markerWidget->setColour( + MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); + markerWidget->setDepth(Global_MarkerLayer); + markerWidget->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); + if (Settings::map().mAllowZooming) + markerWidget->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); + markerWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + + return markerWidget; + } + void MapWindow::addVisitedLocation(const std::string& name, int x, int y) { CellId cell; @@ -825,31 +1066,34 @@ namespace MWGui cell.second = y; if (mMarkers.insert(cell).second) { - float worldX, worldY; - mGlobalMapRender->cellTopLeftCornerToImageSpace (x, y, worldX, worldY); - - int markerSize = 12; - int offset = mGlobalMapRender->getCellSize()/2 - markerSize/2; - MyGUI::IntCoord widgetCoord( - static_cast(worldX * mGlobalMapRender->getWidth()+offset), - static_cast(worldY * mGlobalMapRender->getHeight() + offset), - markerSize, markerSize); - - MyGUI::Widget* markerWidget = mGlobalMap->createWidget("MarkerButton", - widgetCoord, MyGUI::Align::Default); + MapMarkerType mapMarkerWidget = { osg::Vec2f(x, y), createMarker(name, x, y, 0) }; + mGlobalMapMarkers.emplace(mapMarkerWidget, std::vector()); - markerWidget->setUserString("Caption_TextOneLine", "#{sCell=" + name + "}"); - - setGlobalMapMarkerTooltip(markerWidget, x, y); - - markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); + std::string name_ = name.substr(0, name.find(',')); + auto& entry = mGlobalMapMarkersByName[name_]; + if (!entry.widget) + { + entry = { osg::Vec2f(x, y), entry.widget }; // update the coords - markerWidget->setNeedMouseFocus(true); - markerWidget->setColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); - markerWidget->setDepth(Global_MarkerLayer); - markerWidget->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); - markerWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); - mGlobalMapMarkers[std::make_pair(x,y)] = markerWidget; + entry.widget = createMarker(name_, entry.position.x(), entry.position.y(), 1); + mGlobalMapMarkers.emplace(entry, std::vector{ entry }); + } + else + { + auto it = mGlobalMapMarkers.find(entry); + auto& marker = const_cast(it->first); + auto& elements = it->second; + elements.emplace_back(mapMarkerWidget); + + // we compute the barycenter of the entry elements => it will be the place on the world map for the + // agregated widget + marker.position = std::accumulate(elements.begin(), elements.end(), osg::Vec2f(0.f, 0.f), + [](const auto& left, const auto& right) { return left + right.position; }) + / float(elements.size()); + + marker.widget->setCoord(createMarkerCoords(marker.position.x(), marker.position.y(), elements.size())); + marker.widget->setVisible(marker.widget->getHeight() >= 6); + } } } @@ -867,20 +1111,16 @@ namespace MWGui void MapWindow::setGlobalMapMarkerTooltip(MyGUI::Widget* markerWidget, int x, int y) { - ESM::CellId cellId; - cellId.mIndex.mX = x; - cellId.mIndex.mY = y; - cellId.mWorldspace = ESM::CellId::sDefaultWorldspace; - cellId.mPaged = true; - CustomMarkerCollection::RangeType markers = mCustomMarkers.getMarkers(cellId); + ESM::RefId cellRefId = ESM::RefId::esm3ExteriorCell(x, y); + CustomMarkerCollection::RangeType markers = mCustomMarkers.getMarkers(cellRefId); std::vector destNotes; for (CustomMarkerCollection::ContainerType::const_iterator it = markers.first; it != markers.second; ++it) destNotes.push_back(it->second.mNote); if (!destNotes.empty()) { - MarkerUserData data (nullptr); - data.notes = destNotes; + MarkerUserData data(nullptr); + std::swap(data.notes, destNotes); data.caption = markerWidget->getUserString("Caption_TextOneLine"); markerWidget->setUserData(data); markerWidget->setUserString("ToolTipType", "MapMarker"); @@ -891,57 +1131,77 @@ namespace MWGui } } + float MapWindow::getMarkerSize(size_t agregatedWeight) const + { + float markerSize = 12.f * mGlobalMapZoom; + if (mGlobalMapZoom < 1) + return markerSize * std::sqrt(agregatedWeight); // we want to see agregated object + return agregatedWeight ? 0 : markerSize; // we want to see only original markers (i.e. non agregated) + } + + void MapWindow::resizeGlobalMap() + { + mGlobalMap->setCanvasSize( + mGlobalMapRender->getWidth() * mGlobalMapZoom, mGlobalMapRender->getHeight() * mGlobalMapZoom); + mGlobalMapImage->setSize( + mGlobalMapRender->getWidth() * mGlobalMapZoom, mGlobalMapRender->getHeight() * mGlobalMapZoom); + } + + void MapWindow::worldPosToGlobalMapImageSpace(float x, float y, float& imageX, float& imageY) const + { + mGlobalMapRender->worldPosToImageSpace(x, y, imageX, imageY); + imageX *= mGlobalMapZoom; + imageY *= mGlobalMapZoom; + } + void MapWindow::updateCustomMarkers() { LocalMapBase::updateCustomMarkers(); - for (auto& widgetPair : mGlobalMapMarkers) - { - int x = widgetPair.first.first; - int y = widgetPair.first.second; - MyGUI::Widget* markerWidget = widgetPair.second; - setGlobalMapMarkerTooltip(markerWidget, x, y); - } + for (auto& [widgetPair, ignore] : mGlobalMapMarkers) + setGlobalMapMarkerTooltip(widgetPair.widget, widgetPair.position.x(), widgetPair.position.y()); } void MapWindow::onDragStart(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { - if (_id!=MyGUI::MouseButton::Left) return; + if (_id != MyGUI::MouseButton::Left) + return; mLastDragPos = MyGUI::IntPoint(_left, _top); } void MapWindow::onMouseDrag(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { - if (_id!=MyGUI::MouseButton::Left) return; + if (_id != MyGUI::MouseButton::Left) + return; MyGUI::IntPoint diff = MyGUI::IntPoint(_left, _top) - mLastDragPos; - if (!mGlobal) - mLocalMap->setViewOffset( mLocalMap->getViewOffset() + diff ); + if (!Settings::map().mGlobal) + { + mNeedDoorMarkersUpdate = true; + mLocalMap->setViewOffset(mLocalMap->getViewOffset() + diff); + } else - mGlobalMap->setViewOffset( mGlobalMap->getViewOffset() + diff ); + mGlobalMap->setViewOffset(mGlobalMap->getViewOffset() + diff); mLastDragPos = MyGUI::IntPoint(_left, _top); } void MapWindow::onWorldButtonClicked(MyGUI::Widget* _sender) { - mGlobal = !mGlobal; - mGlobalMap->setVisible(mGlobal); - mLocalMap->setVisible(!mGlobal); + const bool global = !Settings::map().mGlobal; - Settings::Manager::setBool("global", "Map", mGlobal); + Settings::map().mGlobal.set(global); - mButton->setCaptionWithReplacing( mGlobal ? "#{sLocal}" : - "#{sWorld}"); + mGlobalMap->setVisible(global); + mLocalMap->setVisible(!global); - if (mGlobal) - globalMapUpdatePlayer (); + mButton->setCaptionWithReplacing(global ? "#{sLocal}" : "#{sWorld}"); } void MapWindow::onPinToggled() { - Settings::Manager::setBool("map pin", "Windows", mPinned); + Settings::windows().mMapPin.set(mPinned); MWBase::Environment::get().getWindowManager()->setMinimapVisibility(!mPinned); } @@ -961,44 +1221,47 @@ namespace MWGui globalMapUpdatePlayer(); } - void MapWindow::globalMapUpdatePlayer () + void MapWindow::globalMapUpdatePlayer() { // For interiors, position is set by WindowManager via setGlobalMapPlayerPosition - if (MWBase::Environment::get().getWorld ()->isCellExterior ()) + if (MWBase::Environment::get().getWorld()->isCellExterior()) { - osg::Vec3f pos = MWBase::Environment::get().getWorld ()->getPlayerPtr().getRefData().getPosition().asVec3(); + osg::Vec3f pos = MWBase::Environment::get().getWorld()->getPlayerPtr().getRefData().getPosition().asVec3(); setGlobalMapPlayerPosition(pos.x(), pos.y()); } } - void MapWindow::notifyPlayerUpdate () + void MapWindow::notifyPlayerUpdate() { - globalMapUpdatePlayer (); + globalMapUpdatePlayer(); setGlobalMapPlayerDir(mLastDirectionX, mLastDirectionY); } - void MapWindow::setGlobalMapPlayerPosition(float worldX, float worldY) + void MapWindow::centerView() { - float x, y; - mGlobalMapRender->worldPosToImageSpace (worldX, worldY, x, y); - x *= mGlobalMapRender->getWidth(); - y *= mGlobalMapRender->getHeight(); - - mPlayerArrowGlobal->setPosition(MyGUI::IntPoint(static_cast(x - 16), static_cast(y - 16))); - + LocalMapBase::centerView(); // set the view offset so that player is in the center MyGUI::IntSize viewsize = mGlobalMap->getSize(); - MyGUI::IntPoint viewoffs(static_cast(viewsize.width * 0.5f - x), static_cast(viewsize.height *0.5 - y)); + MyGUI::IntPoint pos = mPlayerArrowGlobal->getPosition() + MyGUI::IntPoint{ 16, 16 }; + MyGUI::IntPoint viewoffs( + static_cast(viewsize.width * 0.5f - pos.left), static_cast(viewsize.height * 0.5f - pos.top)); mGlobalMap->setViewOffset(viewoffs); } + void MapWindow::setGlobalMapPlayerPosition(float worldX, float worldY) + { + float x, y; + worldPosToGlobalMapImageSpace(worldX, worldY, x, y); + mPlayerArrowGlobal->setPosition(MyGUI::IntPoint(static_cast(x - 16), static_cast(y - 16))); + } + void MapWindow::setGlobalMapPlayerDir(const float x, const float y) { MyGUI::ISubWidget* main = mPlayerArrowGlobal->getSubWidgetMain(); MyGUI::RotatingSkin* rotatingSubskin = main->castType(); - rotatingSubskin->setCenter(MyGUI::IntPoint(16,16)); - float angle = std::atan2(x,y); + rotatingSubskin->setCenter(MyGUI::IntPoint(16, 16)); + float angle = std::atan2(x, y); rotatingSubskin->setAngle(angle); } @@ -1006,11 +1269,11 @@ namespace MWGui { if (!mGlobalMapTexture.get()) { - mGlobalMapTexture.reset(new osgMyGUI::OSGTexture(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.reset(new osgMyGUI::OSGTexture(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)); @@ -1024,14 +1287,15 @@ namespace MWGui mMarkers.clear(); mGlobalMapRender->clear(); - mChanged = true; + mActiveCell = nullptr; for (auto& widgetPair : mGlobalMapMarkers) - MyGUI::Gui::getInstance().destroyWidget(widgetPair.second); + MyGUI::Gui::getInstance().destroyWidget(widgetPair.first.widget); mGlobalMapMarkers.clear(); + mGlobalMapMarkersByName.clear(); } - void MapWindow::write(ESM::ESMWriter &writer, Loading::Listener& progress) + void MapWindow::write(ESM::ESMWriter& writer, Loading::Listener& progress) { ESM::GlobalMap map; mGlobalMapRender->write(map); @@ -1043,7 +1307,7 @@ namespace MWGui writer.endRecord(ESM::REC_GMAP); } - void MapWindow::readRecord(ESM::ESMReader &reader, uint32_t type) + void MapWindow::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_GMAP) { @@ -1054,7 +1318,8 @@ namespace MWGui for (const ESM::GlobalMap::CellId& cellId : map.mMarkers) { - const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getStore().get().search(cellId.first, cellId.second); + const ESM::Cell* cell + = MWBase::Environment::get().getESMStore()->get().search(cellId.first, cellId.second); if (cell && !cell->mName.empty()) addVisitedLocation(cell->mName, cellId.first, cellId.second); } @@ -1070,17 +1335,26 @@ namespace MWGui entry.mMapWidget->setVisible(alpha == 1); } - void MapWindow::customMarkerCreated(MyGUI::Widget *marker) + void MapWindow::customMarkerCreated(MyGUI::Widget* marker) { marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); marker->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onCustomMarkerDoubleClicked); + if (Settings::map().mAllowZooming) + marker->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); } - void MapWindow::doorMarkerCreated(MyGUI::Widget *marker) + void MapWindow::doorMarkerCreated(MyGUI::Widget* marker) { marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + if (Settings::map().mAllowZooming) + marker->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); + } + + void MapWindow::asyncPrepareSaveMap() + { + mGlobalMapRender->asyncWritePng(); } // ------------------------------------------------------------------- @@ -1108,7 +1382,7 @@ namespace MWGui return mDeleteButton->getVisible(); } - void EditNoteDialog::setText(const std::string &text) + void EditNoteDialog::setText(const std::string& text) { mTextEdit->setCaption(MyGUI::TextIterator::toTagsString(text)); } @@ -1125,17 +1399,17 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } - void EditNoteDialog::onCancelButtonClicked(MyGUI::Widget *sender) + void EditNoteDialog::onCancelButtonClicked(MyGUI::Widget* sender) { setVisible(false); } - void EditNoteDialog::onOkButtonClicked(MyGUI::Widget *sender) + void EditNoteDialog::onOkButtonClicked(MyGUI::Widget* sender) { eventOkClicked(); } - void EditNoteDialog::onDeleteButtonClicked(MyGUI::Widget *sender) + void EditNoteDialog::onDeleteButtonClicked(MyGUI::Widget* sender) { eventDeleteClicked(); } diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp index 7e8092f289a..8066256437a 100644 --- a/apps/openmw/mwgui/mapwindow.hpp +++ b/apps/openmw/mwgui/mapwindow.hpp @@ -1,14 +1,17 @@ #ifndef MWGUI_MAPWINDOW_H #define MWGUI_MAPWINDOW_H -#include +#include #include -#include "windowpinnablebase.hpp" +#include + +#include -#include +#include "windowpinnablebase.hpp" -#include +#include +#include namespace MWRender { @@ -24,6 +27,7 @@ namespace ESM namespace MWWorld { + class Cell; class CellStore; } @@ -43,24 +47,24 @@ namespace MWGui class CustomMarkerCollection { public: - void addMarker(const ESM::CustomMarker& marker, bool triggerEvent=true); - void deleteMarker (const ESM::CustomMarker& marker); + void addMarker(const ESM::CustomMarker& marker, bool triggerEvent = true); + void deleteMarker(const ESM::CustomMarker& marker); void updateMarker(const ESM::CustomMarker& marker, const std::string& newNote); void clear(); size_t size() const; - typedef std::multimap ContainerType; + typedef std::multimap ContainerType; typedef std::pair RangeType; ContainerType::const_iterator begin() const; ContainerType::const_iterator end() const; - RangeType getMarkers(const ESM::CellId& cellId) const; + RangeType getMarkers(const ESM::RefId& cellId) const; - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; EventHandle_Void eventMarkersChanged; private: @@ -70,12 +74,11 @@ namespace MWGui class LocalMapBase { public: - LocalMapBase(CustomMarkerCollection& markers, MWRender::LocalMap* localMapRender, bool fogOfWarEnabled = true); + LocalMapBase(CustomMarkerCollection& markers, MWRender::LocalMap* localMapRender, bool fogOfWarEnabled); virtual ~LocalMapBase(); - void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass); + void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, int cellDistance = Constants::CellGridRadius); - void setCellPrefix(const std::string& prefix); - void setActiveCell(const int x, const int y, bool interior=false); + void setActiveCell(const MWWorld::Cell& cell); void requestMapRender(const MWWorld::CellStore* cell); void setPlayerDir(const float x, const float y); void setPlayerPos(int cellX, int cellY, const float nx, const float ny); @@ -107,19 +110,20 @@ namespace MWGui }; protected: + void updateLocalMap(); + + float mLocalMapZoom = 1.f; MWRender::LocalMap* mLocalMapRender; - int mCurX, mCurY; - bool mInterior; + const MWWorld::Cell* mActiveCell; + bool mHasALastActiveCell = false; + osg::Vec2f mCurPos; // the position of the player in the world (in cell coords) + MyGUI::ScrollView* mLocalMap; MyGUI::ImageBox* mCompass; - std::string mPrefix; - bool mChanged; bool mFogOfWarToggled; bool mFogOfWarEnabled; - int mMapWidgetSize; - int mNumCells; // for convenience, mCellDistance * 2 + 1 int mCellDistance; @@ -129,29 +133,45 @@ namespace MWGui struct MapEntry { MapEntry(MyGUI::ImageBox* mapWidget, MyGUI::ImageBox* fogWidget) - : mMapWidget(mapWidget), mFogWidget(fogWidget), mCellX(0), mCellY(0) {} + : mMapWidget(mapWidget) + , mFogWidget(fogWidget) + , mCellX(0) + , mCellY(0) + { + } MyGUI::ImageBox* mMapWidget; MyGUI::ImageBox* mFogWidget; - std::shared_ptr mMapTexture; - std::shared_ptr mFogTexture; + std::unique_ptr mMapTexture; + std::unique_ptr mFogTexture; int mCellX; int mCellY; }; std::vector mMaps; // Keep track of created marker widgets, just to easily remove them later. - std::vector mDoorMarkerWidgets; + std::vector mExteriorDoorMarkerWidgets; + std::map, std::vector> mExteriorDoorsByCell; + std::vector mInteriorDoorMarkerWidgets; std::vector mMagicMarkerWidgets; std::vector mCustomMarkerWidgets; + std::vector mDoorMarkersToRecycle; + + std::vector& currentDoorMarkersWidgets(); virtual void updateCustomMarkers(); void applyFogOfWar(); - MyGUI::IntPoint getMarkerPosition (float worldX, float worldY, MarkerUserData& markerPos); + MyGUI::IntPoint getPosition(int cellX, int cellY, float nx, float ny) const; + MyGUI::IntPoint getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) const; + MyGUI::IntCoord getMarkerCoordinates( + float worldX, float worldY, MarkerUserData& markerPos, size_t markerSize) const; + MyGUI::Widget* createDoorMarker(const std::string& name, float x, float y) const; + MyGUI::IntCoord getMarkerCoordinates(MyGUI::Widget* widget, size_t markerSize) const; virtual void notifyPlayerUpdate() {} + virtual void centerView(); virtual void notifyMapChanged() {} virtual void customMarkerCreated(MyGUI::Widget* marker) {} @@ -163,15 +183,17 @@ namespace MWGui void addDetectionMarkers(int type); void redraw(); + float getWidgetSize() const; float mMarkerUpdateTimer; float mLastDirectionX; float mLastDirectionY; + bool mNeedDoorMarkersUpdate; + private: void updateDoorMarkers(); - bool mNeedDoorMarkersUpdate; }; class EditNoteDialog : public MWGui::WindowModal @@ -186,7 +208,7 @@ namespace MWGui void setText(const std::string& text); std::string getText(); - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; EventHandle_Void eventDeleteClicked; EventHandle_Void eventOkClicked; @@ -205,7 +227,8 @@ namespace MWGui class MapWindow : public MWGui::WindowPinnableBase, public LocalMapBase, public NoDrop { public: - MapWindow(CustomMarkerCollection& customMarkers, DragAndDrop* drag, MWRender::LocalMap* localMapRender, SceneUtil::WorkQueue* workQueue); + MapWindow(CustomMarkerCollection& customMarkers, DragAndDrop* drag, MWRender::LocalMap* localMapRender, + SceneUtil::WorkQueue* workQueue); virtual ~MapWindow(); void setCellName(const std::string& cellName); @@ -222,7 +245,7 @@ namespace MWGui // reveals this cell's map on the global map void cellExplored(int x, int y); - void setGlobalMapPlayerPosition (float worldX, float worldY); + void setGlobalMapPlayerPosition(float worldX, float worldY); void setGlobalMapPlayerDir(const float x, const float y); void ensureGlobalMapLoaded(); @@ -236,14 +259,21 @@ namespace MWGui /// Clear all savegame-specific data void clear() override; - void write (ESM::ESMWriter& writer, Loading::Listener& progress); - void readRecord (ESM::ESMReader& reader, uint32_t type); + void write(ESM::ESMWriter& writer, Loading::Listener& progress); + void readRecord(ESM::ESMReader& reader, uint32_t type); + + void asyncPrepareSaveMap(); + + std::string_view getWindowIdForLua() const override { return "Map"; } private: void onDragStart(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onMouseDrag(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onWorldButtonClicked(MyGUI::Widget* _sender); void onMapDoubleClicked(MyGUI::Widget* sender); + void onMapZoomed(MyGUI::Widget* sender, int rel); + void zoomOnCursor(float speedDiff); + void updateGlobalMap(); void onCustomMarkerDoubleClicked(MyGUI::Widget* sender); void onNoteEditOk(); void onNoteEditDelete(); @@ -252,6 +282,11 @@ namespace MWGui void onChangeScrollWindowCoord(MyGUI::Widget* sender); void globalMapUpdatePlayer(); void setGlobalMapMarkerTooltip(MyGUI::Widget* widget, int x, int y); + float getMarkerSize(size_t agregatedWeight) const; + void resizeGlobalMap(); + void worldPosToGlobalMapImageSpace(float x, float z, float& imageX, float& imageY) const; + MyGUI::IntCoord createMarkerCoords(float x, float y, float agregatedWeight) const; + MyGUI::Widget* createMarker(const std::string& name, float x, float y, float agregatedWeight); MyGUI::ScrollView* mGlobalMap; std::unique_ptr mGlobalMapTexture; @@ -262,7 +297,6 @@ namespace MWGui MyGUI::ImageBox* mPlayerArrowGlobal; MyGUI::Button* mButton; MyGUI::IntPoint mLastDragPos; - bool mGlobal; MyGUI::IntCoord mLastScrollWindowCoordinates; @@ -273,9 +307,19 @@ namespace MWGui MyGUI::Button* mEventBoxGlobal; MyGUI::Button* mEventBoxLocal; - MWRender::GlobalMap* mGlobalMapRender; + float mGlobalMapZoom = 1.0f; + std::unique_ptr mGlobalMapRender; + + struct MapMarkerType + { + osg::Vec2f position; + MyGUI::Widget* widget = nullptr; + + bool operator<(const MapMarkerType& right) const { return widget < right.widget; } + }; - std::map, MyGUI::Widget*> mGlobalMapMarkers; + std::map mGlobalMapMarkersByName; + std::map> mGlobalMapMarkers; EditNoteDialog mEditNoteDialog; ESM::CustomMarker mEditingMarker; @@ -284,10 +328,11 @@ namespace MWGui void onTitleDoubleClicked() override; void doorMarkerCreated(MyGUI::Widget* marker) override; - void customMarkerCreated(MyGUI::Widget *marker) override; + void customMarkerCreated(MyGUI::Widget* marker) override; void notifyPlayerUpdate() override; + void centerView() override; }; } #endif diff --git a/apps/openmw/mwgui/merchantrepair.cpp b/apps/openmw/mwgui/merchantrepair.cpp index e737cb2b292..3be0bb1c06d 100644 --- a/apps/openmw/mwgui/merchantrepair.cpp +++ b/apps/openmw/mwgui/merchantrepair.cpp @@ -1,18 +1,18 @@ #include "merchantrepair.hpp" -#include +#include +#include #include -#include #include +#include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" @@ -20,130 +20,141 @@ namespace MWGui { -MerchantRepair::MerchantRepair() - : WindowBase("openmw_merchantrepair.layout") -{ - getWidget(mList, "RepairView"); - getWidget(mOkButton, "OkButton"); - getWidget(mGoldLabel, "PlayerGold"); + MerchantRepair::MerchantRepair() + : WindowBase("openmw_merchantrepair.layout") + { + getWidget(mList, "RepairView"); + getWidget(mOkButton, "OkButton"); + getWidget(mGoldLabel, "PlayerGold"); - mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onOkButtonClick); -} + mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onOkButtonClick); + } -void MerchantRepair::setPtr(const MWWorld::Ptr &actor) -{ - mActor = actor; + void MerchantRepair::setPtr(const MWWorld::Ptr& actor) + { + if (actor.isEmpty() || !actor.getClass().isActor()) + throw std::runtime_error("Invalid argument in MerchantRepair::setPtr"); + mActor = actor; - while (mList->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mList->getChildAt(0)); + while (mList->getChildCount()) + MyGUI::Gui::getInstance().destroyWidget(mList->getChildAt(0)); - int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; - int currentY = 0; + const int lineHeight = Settings::gui().mFontSize + 2; + int currentY = 0; - MWWorld::Ptr player = MWMechanics::getPlayer(); - int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); + MWWorld::Ptr player = MWMechanics::getPlayer(); + int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); - int categories = MWWorld::ContainerStore::Type_Weapon | MWWorld::ContainerStore::Type_Armor; - for (MWWorld::ContainerStoreIterator iter (store.begin(categories)); iter!=store.end(); ++iter) - { - if (iter->getClass().hasItemHealth(*iter)) + MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); + int categories = MWWorld::ContainerStore::Type_Weapon | MWWorld::ContainerStore::Type_Armor; + std::vector> items; + + for (MWWorld::ContainerStoreIterator iter(store.begin(categories)); iter != store.end(); ++iter) { - int maxDurability = iter->getClass().getItemMaxHealth(*iter); - int durability = iter->getClass().getItemHealth(*iter); - if (maxDurability == durability || maxDurability == 0) - continue; - - int basePrice = iter->getClass().getValue(*iter); - float fRepairMult = MWBase::Environment::get().getWorld()->getStore().get() - .find("fRepairMult")->mValue.getFloat(); - - float p = static_cast(std::max(1, basePrice)); - float r = static_cast(std::max(1, static_cast(maxDurability / p))); - - int x = static_cast((maxDurability - durability) / r); - x = static_cast(fRepairMult * x); - x = std::max(1, x); - - int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mActor, x, true); - - std::string name = iter->getClass().getName(*iter) - + " - " + MyGUI::utility::toString(price) - + MWBase::Environment::get().getWorld()->getStore().get() - .find("sgp")->mValue.getString(); - - MyGUI::Button* button = - mList->createWidget(price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip - 0, - currentY, - 0, - lineHeight, - MyGUI::Align::Default - ); + if (iter->getClass().hasItemHealth(*iter)) + { + int maxDurability = iter->getClass().getItemMaxHealth(*iter); + int durability = iter->getClass().getItemHealth(*iter); + if (maxDurability == durability || maxDurability == 0) + continue; + + int basePrice = iter->getClass().getValue(*iter); + float fRepairMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fRepairMult") + ->mValue.getFloat(); + + float p = static_cast(std::max(1, basePrice)); + float r = static_cast(std::max(1, static_cast(maxDurability / p))); + + int x = static_cast((maxDurability - durability) / r); + x = static_cast(fRepairMult * x); + x = std::max(1, x); + + int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mActor, x, true); + + std::string name{ iter->getClass().getName(*iter) }; + name += " - " + MyGUI::utility::toString(price) + + MWBase::Environment::get().getESMStore()->get().find("sgp")->mValue.getString(); + + items.emplace_back(name, price, *iter); + } + } + + std::stable_sort(items.begin(), items.end(), + [](const auto& a, const auto& b) { return Misc::StringUtils::ciLess(std::get<0>(a), std::get<0>(b)); }); + + for (const auto& [name, price, ptr] : items) + { + MyGUI::Button* button = mList->createWidget(price <= playerGold + ? "SandTextButton" + : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip + 0, currentY, 0, lineHeight, MyGUI::Align::Default); currentY += lineHeight; button->setUserString("Price", MyGUI::utility::toString(price)); - button->setUserData(MWWorld::Ptr(*iter)); + button->setUserData(MWWorld::Ptr(ptr)); button->setCaptionWithReplacing(name); button->setSize(mList->getWidth(), lineHeight); button->eventMouseWheel += MyGUI::newDelegate(this, &MerchantRepair::onMouseWheel); button->setUserString("ToolTipType", "ItemPtr"); button->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onRepairButtonClick); } + + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden + mList->setVisibleVScroll(false); + mList->setCanvasSize(MyGUI::IntSize(mList->getWidth(), std::max(mList->getHeight(), currentY))); + mList->setVisibleVScroll(true); + + mGoldLabel->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); } - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden - mList->setVisibleVScroll(false); - mList->setCanvasSize (MyGUI::IntSize(mList->getWidth(), std::max(mList->getHeight(), currentY))); - mList->setVisibleVScroll(true); - mGoldLabel->setCaptionWithReplacing("#{sGold}: " - + MyGUI::utility::toString(playerGold)); -} + void MerchantRepair::onMouseWheel(MyGUI::Widget* _sender, int _rel) + { + if (mList->getViewOffset().top + _rel * 0.3f > 0) + mList->setViewOffset(MyGUI::IntPoint(0, 0)); + else + mList->setViewOffset(MyGUI::IntPoint(0, static_cast(mList->getViewOffset().top + _rel * 0.3f))); + } -void MerchantRepair::onMouseWheel(MyGUI::Widget* _sender, int _rel) -{ - if (mList->getViewOffset().top + _rel*0.3f > 0) + void MerchantRepair::onOpen() + { + center(); + // Reset scrollbars mList->setViewOffset(MyGUI::IntPoint(0, 0)); - else - mList->setViewOffset(MyGUI::IntPoint(0, static_cast(mList->getViewOffset().top + _rel*0.3f))); -} - -void MerchantRepair::onOpen() -{ - center(); - // Reset scrollbars - mList->setViewOffset(MyGUI::IntPoint(0, 0)); -} + } -void MerchantRepair::onRepairButtonClick(MyGUI::Widget *sender) -{ - MWWorld::Ptr player = MWMechanics::getPlayer(); + void MerchantRepair::onRepairButtonClick(MyGUI::Widget* sender) + { + MWWorld::Ptr player = MWMechanics::getPlayer(); - int price = MyGUI::utility::parseInt(sender->getUserString("Price")); - if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) - return; + int price = MyGUI::utility::parseInt(sender->getUserString("Price")); + if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) + return; - // repair - MWWorld::Ptr item = *sender->getUserData(); - item.getCellRef().setCharge(item.getClass().getItemMaxHealth(item)); + // repair + MWWorld::Ptr item = *sender->getUserData(); + item.getCellRef().setCharge(item.getClass().getItemMaxHealth(item)); - player.getClass().getContainerStore(player).restack(item); + player.getClass().getContainerStore(player).restack(item); - MWBase::Environment::get().getWindowManager()->playSound("Repair"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Repair")); - player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price); - // add gold to NPC trading gold pool - MWMechanics::CreatureStats& actorStats = mActor.getClass().getCreatureStats(mActor); - actorStats.setGoldPool(actorStats.getGoldPool() + price); + // add gold to NPC trading gold pool + MWMechanics::CreatureStats& actorStats = mActor.getClass().getCreatureStats(mActor); + actorStats.setGoldPool(actorStats.getGoldPool() + price); - setPtr(mActor); -} + setPtr(mActor); + } -void MerchantRepair::onOkButtonClick(MyGUI::Widget *sender) -{ - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_MerchantRepair); -} + void MerchantRepair::onOkButtonClick(MyGUI::Widget* sender) + { + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_MerchantRepair); + } } diff --git a/apps/openmw/mwgui/merchantrepair.hpp b/apps/openmw/mwgui/merchantrepair.hpp index f5276d7f6e5..ffe5b86bdbb 100644 --- a/apps/openmw/mwgui/merchantrepair.hpp +++ b/apps/openmw/mwgui/merchantrepair.hpp @@ -1,33 +1,35 @@ #ifndef OPENMW_MWGUI_MERCHANTREPAIR_H #define OPENMW_MWGUI_MERCHANTREPAIR_H -#include "windowbase.hpp" #include "../mwworld/ptr.hpp" +#include "windowbase.hpp" namespace MWGui { -class MerchantRepair : public WindowBase -{ -public: - MerchantRepair(); + class MerchantRepair : public WindowBase + { + public: + MerchantRepair(); + + void onOpen() override; - void onOpen() override; + void setPtr(const MWWorld::Ptr& actor) override; - void setPtr(const MWWorld::Ptr& actor) override; + std::string_view getWindowIdForLua() const override { return "MerchantRepair"; } -private: - MyGUI::ScrollView* mList; - MyGUI::Button* mOkButton; - MyGUI::TextBox* mGoldLabel; + private: + MyGUI::ScrollView* mList; + MyGUI::Button* mOkButton; + MyGUI::TextBox* mGoldLabel; - MWWorld::Ptr mActor; + MWWorld::Ptr mActor; -protected: - void onMouseWheel(MyGUI::Widget* _sender, int _rel); - void onRepairButtonClick(MyGUI::Widget* sender); - void onOkButtonClick(MyGUI::Widget* sender); -}; + protected: + void onMouseWheel(MyGUI::Widget* _sender, int _rel); + void onRepairButtonClick(MyGUI::Widget* sender); + void onOkButtonClick(MyGUI::Widget* sender); + }; } diff --git a/apps/openmw/mwgui/messagebox.cpp b/apps/openmw/mwgui/messagebox.cpp index 5bd8ceb5aea..1d6e1511c40 100644 --- a/apps/openmw/mwgui/messagebox.cpp +++ b/apps/openmw/mwgui/messagebox.cpp @@ -1,37 +1,34 @@ #include "messagebox.hpp" -#include +#include #include +#include #include -#include +#include #include -#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/soundmanager.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" -#undef MessageBox - namespace MWGui { - MessageBoxManager::MessageBoxManager (float timePerChar) + MessageBoxManager::MessageBoxManager(float timePerChar) { - mInterMessageBoxe = nullptr; mStaticMessageBox = nullptr; mLastButtonPressed = -1; mMessageBoxSpeed = timePerChar; } - MessageBoxManager::~MessageBoxManager () + MessageBoxManager::~MessageBoxManager() { MessageBoxManager::clear(); } - int MessageBoxManager::getMessagesCount() + std::size_t MessageBoxManager::getMessagesCount() { return mMessageBoxes.size(); } @@ -41,31 +38,36 @@ namespace MWGui if (mInterMessageBoxe) { mInterMessageBoxe->setVisible(false); - - delete mInterMessageBoxe; - mInterMessageBoxe = nullptr; + mInterMessageBoxe.reset(); } - for (MessageBox* messageBox : mMessageBoxes) - { - if (messageBox == mStaticMessageBox) - mStaticMessageBox = nullptr; - delete messageBox; - } mMessageBoxes.clear(); + mStaticMessageBox = nullptr; mLastButtonPressed = -1; } - void MessageBoxManager::onFrame (float frameDuration) + void MessageBoxManager::resetInteractiveMessageBox() + { + if (mInterMessageBoxe) + { + mInterMessageBoxe->setVisible(false); + mInterMessageBoxe.reset(); + } + } + + void MessageBoxManager::setLastButtonPressed(int index) { - std::vector::iterator it; - for(it = mMessageBoxes.begin(); it != mMessageBoxes.end();) + mLastButtonPressed = index; + } + + void MessageBoxManager::onFrame(float frameDuration) + { + for (auto it = mMessageBoxes.begin(); it != mMessageBoxes.end();) { (*it)->mCurrentTime += frameDuration; - if((*it)->mCurrentTime >= (*it)->mMaxTime && *it != mStaticMessageBox) + if ((*it)->mCurrentTime >= (*it)->mMaxTime && it->get() != mStaticMessageBox) { - delete *it; it = mMessageBoxes.erase(it); } else @@ -73,85 +75,84 @@ namespace MWGui } float height = 0; - it = mMessageBoxes.begin(); - while(it != mMessageBoxes.end()) + auto it = mMessageBoxes.begin(); + while (it != mMessageBoxes.end()) { (*it)->update(static_cast(height)); height += (*it)->getHeight(); ++it; } - if(mInterMessageBoxe != nullptr && mInterMessageBoxe->mMarkedToDelete) { + if (mInterMessageBoxe != nullptr && mInterMessageBoxe->mMarkedToDelete) + { mLastButtonPressed = mInterMessageBoxe->readPressedButton(); mInterMessageBoxe->setVisible(false); - delete mInterMessageBoxe; - mInterMessageBoxe = nullptr; + mInterMessageBoxe.reset(); MWBase::Environment::get().getInputManager()->changeInputMode( - MWBase::Environment::get().getWindowManager()->isGuiMode()); + MWBase::Environment::get().getWindowManager()->isGuiMode()); } } - void MessageBoxManager::createMessageBox (const std::string& message, bool stat) + void MessageBoxManager::createMessageBox(std::string_view message, bool stat) { - MessageBox *box = new MessageBox(*this, message); + auto box = std::make_unique(*this, message); box->mCurrentTime = 0; - std::string realMessage = MyGUI::LanguageManager::getInstance().replaceTags(message); - box->mMaxTime = realMessage.length()*mMessageBoxSpeed; + auto realMessage = MyGUI::LanguageManager::getInstance().replaceTags({ message.data(), message.size() }); + box->mMaxTime = realMessage.length() * mMessageBoxSpeed; - if(stat) - mStaticMessageBox = box; + if (stat) + mStaticMessageBox = box.get(); - mMessageBoxes.push_back(box); + box->setVisible(mVisible); - if(mMessageBoxes.size() > 3) { - delete *mMessageBoxes.begin(); + mMessageBoxes.push_back(std::move(box)); + + if (mMessageBoxes.size() > 3) + { mMessageBoxes.erase(mMessageBoxes.begin()); } int height = 0; - for (MessageBox* messageBox : mMessageBoxes) + for (const auto& messageBox : mMessageBoxes) { messageBox->update(height); height += messageBox->getHeight(); } } - void MessageBoxManager::removeStaticMessageBox () + void MessageBoxManager::removeStaticMessageBox() { removeMessageBox(mStaticMessageBox); mStaticMessageBox = nullptr; } - bool MessageBoxManager::createInteractiveMessageBox (const std::string& message, const std::vector& buttons) + bool MessageBoxManager::createInteractiveMessageBox( + std::string_view message, const std::vector& buttons, bool immediate, int defaultFocus) { if (mInterMessageBoxe != nullptr) { Log(Debug::Warning) << "Warning: replacing an interactive message box that was not answered yet"; mInterMessageBoxe->setVisible(false); - delete mInterMessageBoxe; - mInterMessageBoxe = nullptr; } - mInterMessageBoxe = new InteractiveMessageBox(*this, message, buttons); + mInterMessageBoxe + = std::make_unique(*this, std::string{ message }, buttons, immediate, defaultFocus); mLastButtonPressed = -1; return true; } - bool MessageBoxManager::isInteractiveMessageBox () + bool MessageBoxManager::isInteractiveMessageBox() { return mInterMessageBoxe != nullptr; } - - bool MessageBoxManager::removeMessageBox (MessageBox *msgbox) + bool MessageBoxManager::removeMessageBox(MessageBox* msgbox) { - std::vector::iterator it; - for(it = mMessageBoxes.begin(); it != mMessageBoxes.end(); ++it) + for (auto it = mMessageBoxes.begin(); it != mMessageBoxes.end(); ++it) { - if((*it) == msgbox) + if (it->get() == msgbox) { - delete (*it); mMessageBoxes.erase(it); return true; } @@ -159,7 +160,12 @@ namespace MWGui return false; } - int MessageBoxManager::readPressedButton (bool reset) + const std::vector>& MessageBoxManager::getActiveMessageBoxes() const + { + return mMessageBoxes; + } + + int MessageBoxManager::readPressedButton(bool reset) { int pressed = mLastButtonPressed; if (reset) @@ -167,15 +173,19 @@ namespace MWGui return pressed; } + void MessageBoxManager::setVisible(bool value) + { + mVisible = value; + for (const auto& messageBox : mMessageBoxes) + messageBox->setVisible(value); + } - - - MessageBox::MessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message) - : Layout("openmw_messagebox.layout") - , mCurrentTime(0) - , mMaxTime(0) - , mMessageBoxManager(parMessageBoxManager) - , mMessage(message) + MessageBox::MessageBox(MessageBoxManager& parMessageBoxManager, std::string_view message) + : Layout("openmw_messagebox.layout") + , mCurrentTime(0) + , mMaxTime(0) + , mMessageBoxManager(parMessageBoxManager) + , mMessage(message) { // defines mBottomPadding = 48; @@ -186,27 +196,35 @@ namespace MWGui mMessageWidget->setCaptionWithReplacing(mMessage); } - void MessageBox::update (int height) + void MessageBox::update(int height) { MyGUI::IntSize gameWindowSize = MyGUI::RenderManager::getInstance().getViewSize(); MyGUI::IntPoint pos; - pos.left = (gameWindowSize.width - mMainWidget->getWidth())/2; + pos.left = (gameWindowSize.width - mMainWidget->getWidth()) / 2; pos.top = (gameWindowSize.height - mMainWidget->getHeight() - height - mBottomPadding); mMainWidget->setPosition(pos); } - int MessageBox::getHeight () + int MessageBox::getHeight() { - return mMainWidget->getHeight()+mNextBoxPadding; + return mMainWidget->getHeight() + mNextBoxPadding; } + void MessageBox::setVisible(bool value) + { + mMainWidget->setVisible(value); + } - - InteractiveMessageBox::InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, const std::vector& buttons) - : WindowModal(MWBase::Environment::get().getWindowManager()->isGuiMode() ? "openmw_interactive_messagebox_notransp.layout" : "openmw_interactive_messagebox.layout") - , mMessageBoxManager(parMessageBoxManager) - , mButtonPressed(-1) + InteractiveMessageBox::InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, + const std::vector& buttons, bool immediate, int defaultFocus) + : WindowModal(MWBase::Environment::get().getWindowManager()->isGuiMode() + ? "openmw_interactive_messagebox_notransp.layout" + : "openmw_interactive_messagebox.layout") + , mMessageBoxManager(parMessageBoxManager) + , mButtonPressed(-1) + , mDefaultFocus(defaultFocus) + , mImmediate(immediate) { int textPadding = 10; // padding between text-widget and main-widget int textButtonPadding = 10; // padding between the text-widget und the button-widget @@ -218,7 +236,6 @@ namespace MWGui mMarkedToDelete = false; - getWidget(mMessageWidget, "message"); getWidget(mButtonsWidget, "buttons"); @@ -235,13 +252,10 @@ namespace MWGui int buttonHeight = 0; MyGUI::IntCoord dummyCoord(0, 0, 0, 0); - for(const std::string& buttonId : buttons) + for (const std::string& buttonId : buttons) { MyGUI::Button* button = mButtonsWidget->createWidget( - MyGUI::WidgetStyle::Child, - std::string("MW_Button"), - dummyCoord, - MyGUI::Align::Default); + MyGUI::WidgetStyle::Child, std::string("MW_Button"), dummyCoord, MyGUI::Align::Default); button->setCaptionWithReplacing(buttonId); button->eventMouseButtonClick += MyGUI::newDelegate(this, &InteractiveMessageBox::mousePressed); @@ -251,41 +265,42 @@ namespace MWGui if (buttonsWidth != 0) buttonsWidth += buttonLeftPadding; - int buttonWidth = button->getTextSize().width + 2*buttonLabelLeftPadding; + int buttonWidth = button->getTextSize().width + 2 * buttonLabelLeftPadding; buttonsWidth += buttonWidth; - buttonHeight = button->getTextSize().height + 2*buttonLabelTopPadding; + buttonHeight = button->getTextSize().height + 2 * buttonLabelTopPadding; if (buttonsHeight != 0) buttonsHeight += buttonTopPadding; buttonsHeight += buttonHeight; - if(buttonWidth > biggestButtonWidth) + if (buttonWidth > biggestButtonWidth) { biggestButtonWidth = buttonWidth; } } MyGUI::IntSize mainWidgetSize; - if(buttonsWidth < textSize.width) + if (buttonsWidth < textSize.width) { // on one line - mainWidgetSize.width = textSize.width + 3*textPadding; - mainWidgetSize.height = textPadding + textSize.height + textButtonPadding + buttonHeight + buttonMainPadding; + mainWidgetSize.width = textSize.width + 3 * textPadding; + mainWidgetSize.height + = textPadding + textSize.height + textButtonPadding + buttonHeight + buttonMainPadding; MyGUI::IntSize realSize = mainWidgetSize + - // To account for borders - (mMainWidget->getSize() - mMainWidget->getClientWidget()->getSize()); + // To account for borders + (mMainWidget->getSize() - mMainWidget->getClientWidget()->getSize()); MyGUI::IntPoint absPos; - absPos.left = (gameWindowSize.width - realSize.width)/2; - absPos.top = (gameWindowSize.height - realSize.height)/2; + absPos.left = (gameWindowSize.width - realSize.width) / 2; + absPos.top = (gameWindowSize.height - realSize.height) / 2; mMainWidget->setPosition(absPos); mMainWidget->setSize(realSize); MyGUI::IntCoord messageWidgetCoord; - messageWidgetCoord.left = (mainWidgetSize.width - textSize.width)/2; + messageWidgetCoord.left = (mainWidgetSize.width - textSize.width) / 2; messageWidgetCoord.top = textPadding; mMessageWidget->setCoord(messageWidgetCoord); @@ -293,15 +308,15 @@ namespace MWGui MyGUI::IntCoord buttonCord; MyGUI::IntSize buttonSize(0, buttonHeight); - int left = (mainWidgetSize.width - buttonsWidth)/2; + int left = (mainWidgetSize.width - buttonsWidth) / 2; - for(MyGUI::Button* button : mButtons) + for (MyGUI::Button* button : mButtons) { buttonCord.left = left; buttonCord.top = messageWidgetCoord.top + textSize.height + textButtonPadding; - buttonSize.width = button->getTextSize().width + 2*buttonLabelLeftPadding; - buttonSize.height = button->getTextSize().height + 2*buttonLabelTopPadding; + buttonSize.width = button->getTextSize().width + 2 * buttonLabelLeftPadding; + buttonSize.height = button->getTextSize().height + 2 * buttonLabelTopPadding; button->setCoord(buttonCord); button->setSize(buttonSize); @@ -312,11 +327,13 @@ namespace MWGui else { // among each other - if(biggestButtonWidth > textSize.width) { - mainWidgetSize.width = biggestButtonWidth + buttonTopPadding*2; + if (biggestButtonWidth > textSize.width) + { + mainWidgetSize.width = biggestButtonWidth + buttonTopPadding * 2; } - else { - mainWidgetSize.width = textSize.width + 3*textPadding; + else + { + mainWidgetSize.width = textSize.width + 3 * textPadding; } MyGUI::IntCoord buttonCord; @@ -324,13 +341,13 @@ namespace MWGui int top = textPadding + textSize.height + textButtonPadding; - for(MyGUI::Button* button : mButtons) + for (MyGUI::Button* button : mButtons) { - buttonSize.width = button->getTextSize().width + buttonLabelLeftPadding*2; - buttonSize.height = button->getTextSize().height + buttonLabelTopPadding*2; + buttonSize.width = button->getTextSize().width + buttonLabelLeftPadding * 2; + buttonSize.height = button->getTextSize().height + buttonLabelTopPadding * 2; buttonCord.top = top; - buttonCord.left = (mainWidgetSize.width - buttonSize.width)/2; + buttonCord.left = (mainWidgetSize.width - buttonSize.width) / 2; button->setCoord(buttonCord); button->setSize(buttonSize); @@ -338,19 +355,20 @@ namespace MWGui top += buttonSize.height + buttonTopPadding; } - mainWidgetSize.height = textPadding + textSize.height + textButtonPadding + buttonsHeight + buttonMainPadding; + mainWidgetSize.height + = textPadding + textSize.height + textButtonPadding + buttonsHeight + buttonMainPadding; mMainWidget->setSize(mainWidgetSize + - // To account for borders - (mMainWidget->getSize() - mMainWidget->getClientWidget()->getSize())); + // To account for borders + (mMainWidget->getSize() - mMainWidget->getClientWidget()->getSize())); MyGUI::IntPoint absPos; - absPos.left = (gameWindowSize.width - mainWidgetSize.width)/2; - absPos.top = (gameWindowSize.height - mainWidgetSize.height)/2; + absPos.left = (gameWindowSize.width - mainWidgetSize.width) / 2; + absPos.top = (gameWindowSize.height - mainWidgetSize.height) / 2; mMainWidget->setPosition(absPos); MyGUI::IntCoord messageWidgetCoord; - messageWidgetCoord.left = (mainWidgetSize.width - textSize.width)/2; + messageWidgetCoord.left = (mainWidgetSize.width - textSize.width) / 2; messageWidgetCoord.top = textPadding; messageWidgetCoord.width = textSize.width; messageWidgetCoord.height = textSize.height; @@ -362,12 +380,17 @@ namespace MWGui MyGUI::Widget* InteractiveMessageBox::getDefaultKeyFocus() { - std::vector keywords { "sOk", "sYes" }; - for(MyGUI::Button* button : mButtons) + if (mDefaultFocus >= 0 && mDefaultFocus < static_cast(mButtons.size())) + return mButtons[mDefaultFocus]; + auto& languageManager = MyGUI::LanguageManager::getInstance(); + std::vector keywords{ languageManager.replaceTags("#{sOk}"), + languageManager.replaceTags("#{sYes}") }; + + for (MyGUI::Button* button : mButtons) { - for (const std::string& keyword : keywords) + for (const MyGUI::UString& keyword : keywords) { - if(Misc::StringUtils::ciEqual(MyGUI::LanguageManager::getInstance().replaceTags("#{" + keyword + "}"), button->getCaption())) + if (Misc::StringUtils::ciEqual(keyword, button->getCaption())) { return button; } @@ -376,28 +399,34 @@ namespace MWGui return nullptr; } - void InteractiveMessageBox::mousePressed (MyGUI::Widget* pressed) + void InteractiveMessageBox::mousePressed(MyGUI::Widget* pressed) { - buttonActivated (pressed); + buttonActivated(pressed); } - void InteractiveMessageBox::buttonActivated (MyGUI::Widget* pressed) + void InteractiveMessageBox::buttonActivated(MyGUI::Widget* pressed) { mMarkedToDelete = true; int index = 0; - for(const MyGUI::Button* button : mButtons) + for (const MyGUI::Button* button : mButtons) { - if(button == pressed) + if (button == pressed) { mButtonPressed = index; mMessageBoxManager.onButtonPressed(mButtonPressed); + if (!mImmediate) + return; + + mMessageBoxManager.setLastButtonPressed(mButtonPressed); + MWBase::Environment::get().getInputManager()->changeInputMode( + MWBase::Environment::get().getWindowManager()->isGuiMode()); return; } index++; } } - int InteractiveMessageBox::readPressedButton () + int InteractiveMessageBox::readPressedButton() { return mButtonPressed; } diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp index aeb1b830025..feb717e0ad8 100644 --- a/apps/openmw/mwgui/messagebox.hpp +++ b/apps/openmw/mwgui/messagebox.hpp @@ -1,9 +1,9 @@ #ifndef MWGUI_MESSAGE_BOX_H #define MWGUI_MESSAGE_BOX_H -#include "windowbase.hpp" +#include -#undef MessageBox +#include "windowbase.hpp" namespace MyGUI { @@ -19,83 +19,101 @@ namespace MWGui class MessageBox; class MessageBoxManager { - public: - MessageBoxManager (float timePerChar); - ~MessageBoxManager (); - void onFrame (float frameDuration); - void createMessageBox (const std::string& message, bool stat = false); - void removeStaticMessageBox (); - bool createInteractiveMessageBox (const std::string& message, const std::vector& buttons); - bool isInteractiveMessageBox (); + public: + MessageBoxManager(float timePerChar); + ~MessageBoxManager(); + void onFrame(float frameDuration); + void createMessageBox(std::string_view message, bool stat = false); + void removeStaticMessageBox(); + bool createInteractiveMessageBox(std::string_view message, const std::vector& buttons, + bool immediate = false, int defaultFocus = -1); + bool isInteractiveMessageBox(); + + std::size_t getMessagesCount(); + + const InteractiveMessageBox* getInteractiveMessageBox() const { return mInterMessageBoxe.get(); } + + /// Remove all message boxes + void clear(); + + bool removeMessageBox(MessageBox* msgbox); - int getMessagesCount(); + /// @param reset Reset the pressed button to -1 after reading it. + int readPressedButton(bool reset = true); - const InteractiveMessageBox* getInteractiveMessageBox() const { return mInterMessageBoxe; } + void resetInteractiveMessageBox(); - /// Remove all message boxes - void clear(); + void setLastButtonPressed(int index); - bool removeMessageBox (MessageBox *msgbox); + typedef MyGUI::delegates::MultiDelegate EventHandle_Int; - /// @param reset Reset the pressed button to -1 after reading it. - int readPressedButton (bool reset=true); + // Note: this delegate unassigns itself after it was fired, i.e. works once. + EventHandle_Int eventButtonPressed; - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Int; + void onButtonPressed(int button) + { + eventButtonPressed(button); + eventButtonPressed.clear(); + } - // Note: this delegate unassigns itself after it was fired, i.e. works once. - EventHandle_Int eventButtonPressed; + void setVisible(bool value); - void onButtonPressed(int button) { eventButtonPressed(button); eventButtonPressed.clear(); } + const std::vector>& getActiveMessageBoxes() const; - private: - std::vector mMessageBoxes; - InteractiveMessageBox* mInterMessageBoxe; - MessageBox* mStaticMessageBox; - float mMessageBoxSpeed; - int mLastButtonPressed; + private: + std::vector> mMessageBoxes; + std::unique_ptr mInterMessageBoxe; + MessageBox* mStaticMessageBox; + float mMessageBoxSpeed; + int mLastButtonPressed; + bool mVisible = true; }; class MessageBox : public Layout { - public: - MessageBox (MessageBoxManager& parMessageBoxManager, const std::string& message); - void setMessage (const std::string& message); - int getHeight (); - void update (int height); - - float mCurrentTime; - float mMaxTime; - - protected: - MessageBoxManager& mMessageBoxManager; - const std::string& mMessage; - MyGUI::EditBox* mMessageWidget; - int mBottomPadding; - int mNextBoxPadding; + public: + MessageBox(MessageBoxManager& parMessageBoxManager, std::string_view message); + const std::string& getMessage() { return mMessage; } + int getHeight(); + void update(int height); + void setVisible(bool value); + + float mCurrentTime; + float mMaxTime; + + protected: + MessageBoxManager& mMessageBoxManager; + std::string mMessage; + MyGUI::EditBox* mMessageWidget; + int mBottomPadding; + int mNextBoxPadding; }; class InteractiveMessageBox : public WindowModal { - public: - InteractiveMessageBox (MessageBoxManager& parMessageBoxManager, const std::string& message, const std::vector& buttons); - void mousePressed (MyGUI::Widget* _widget); - int readPressedButton (); + public: + InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, + const std::vector& buttons, bool immediate, int defaultFocus); + void mousePressed(MyGUI::Widget* _widget); + int readPressedButton(); - MyGUI::Widget* getDefaultKeyFocus() override; + MyGUI::Widget* getDefaultKeyFocus() override; - bool exit() override { return false; } + bool exit() override { return false; } - bool mMarkedToDelete; + bool mMarkedToDelete; - private: - void buttonActivated (MyGUI::Widget* _widget); + private: + void buttonActivated(MyGUI::Widget* _widget); - MessageBoxManager& mMessageBoxManager; - MyGUI::EditBox* mMessageWidget; - MyGUI::Widget* mButtonsWidget; - std::vector mButtons; + MessageBoxManager& mMessageBoxManager; + MyGUI::EditBox* mMessageWidget; + MyGUI::Widget* mButtonsWidget; + std::vector mButtons; - int mButtonPressed; + int mButtonPressed; + int mDefaultFocus; + bool mImmediate; }; } diff --git a/apps/openmw/mwgui/mode.hpp b/apps/openmw/mwgui/mode.hpp index 62d73965773..44f97437436 100644 --- a/apps/openmw/mwgui/mode.hpp +++ b/apps/openmw/mwgui/mode.hpp @@ -3,63 +3,62 @@ namespace MWGui { - enum GuiMode + enum GuiMode { - GM_None, - GM_Settings, // Settings window - GM_Inventory, // Inventory mode - GM_Container, - GM_Companion, - GM_MainMenu, // Main menu mode + GM_None, + GM_Inventory, // Inventory mode + GM_Container, + GM_Companion, + GM_MainMenu, // Main menu mode - GM_Journal, // Journal mode + GM_Journal, // Journal mode - GM_Scroll, // Read scroll - GM_Book, // Read book - GM_Alchemy, // Make potions - GM_Repair, + GM_Scroll, // Read scroll + GM_Book, // Read book + GM_Alchemy, // Make potions + GM_Repair, - GM_Dialogue, // NPC interaction - GM_Barter, - GM_Rest, - GM_SpellBuying, - GM_Travel, - GM_SpellCreation, - GM_Enchanting, - GM_Recharge, - GM_Training, - GM_MerchantRepair, + GM_Dialogue, // NPC interaction + GM_Barter, + GM_Rest, + GM_SpellBuying, + GM_Travel, + GM_SpellCreation, + GM_Enchanting, + GM_Recharge, + GM_Training, + GM_MerchantRepair, - GM_Levelup, + GM_Levelup, - // Startup character creation dialogs - GM_Name, - GM_Race, - GM_Birth, - GM_Class, - GM_ClassGenerate, - GM_ClassPick, - GM_ClassCreate, - GM_Review, - - GM_Loading, - GM_LoadingWallpaper, - GM_Jail, + // Startup character creation dialogs + GM_Name, + GM_Race, + GM_Birth, + GM_Class, + GM_ClassGenerate, + GM_ClassPick, + GM_ClassCreate, + GM_Review, - GM_QuickKeysMenu + GM_Loading, + GM_LoadingWallpaper, + GM_Jail, + + GM_QuickKeysMenu }; - // Windows shown in inventory mode - enum GuiWindow + // Windows shown in inventory mode + enum GuiWindow { - GW_None = 0, + GW_None = 0, - GW_Map = 0x01, - GW_Inventory = 0x02, - GW_Magic = 0x04, - GW_Stats = 0x08, + GW_Map = 0x01, + GW_Inventory = 0x02, + GW_Magic = 0x04, + GW_Stats = 0x08, - GW_ALL = 0xFF + GW_ALL = 0xFF }; } diff --git a/apps/openmw/mwgui/pickpocketitemmodel.cpp b/apps/openmw/mwgui/pickpocketitemmodel.cpp index 5daea8f3f85..fa7bce449b8 100644 --- a/apps/openmw/mwgui/pickpocketitemmodel.cpp +++ b/apps/openmw/mwgui/pickpocketitemmodel.cpp @@ -1,7 +1,7 @@ #include "pickpocketitemmodel.hpp" +#include #include -#include #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" @@ -12,25 +12,28 @@ #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" namespace MWGui { - PickpocketItemModel::PickpocketItemModel(const MWWorld::Ptr& actor, ItemModel *sourceModel, bool hideItems) - : mActor(actor), mPickpocketDetected(false) + PickpocketItemModel::PickpocketItemModel( + const MWWorld::Ptr& actor, std::unique_ptr sourceModel, bool hideItems) + : mActor(actor) + , mPickpocketDetected(false) { MWWorld::Ptr player = MWMechanics::getPlayer(); - mSourceModel = sourceModel; + mSourceModel = std::move(sourceModel); float chance = player.getClass().getSkill(player, ESM::Skill::Sneak); mSourceModel->update(); - // build list of items that player is unable to find when attempts to pickpocket. if (hideItems) { - for (size_t i = 0; igetItemCount(); ++i) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + for (size_t i = 0; i < mSourceModel->getItemCount(); ++i) { - if (Misc::Rng::roll0to99() > chance) + if (Misc::Rng::roll0to99(prng) > chance) mHiddenItems.push_back(mSourceModel->getItem(i)); } } @@ -41,7 +44,7 @@ namespace MWGui return false; } - ItemStack PickpocketItemModel::getItem (ModelIndex index) + ItemStack PickpocketItemModel::getItem(ModelIndex index) { if (index < 0) throw std::runtime_error("Invalid index supplied"); @@ -59,7 +62,7 @@ namespace MWGui { mSourceModel->update(); mItems.clear(); - for (size_t i = 0; igetItemCount(); ++i) + for (size_t i = 0; i < mSourceModel->getItemCount(); ++i) { const ItemStack& item = mSourceModel->getItem(i); @@ -68,17 +71,17 @@ namespace MWGui continue; if (std::find(mHiddenItems.begin(), mHiddenItems.end(), item) == mHiddenItems.end() - && item.mType != ItemStack::Type_Equipped) + && item.mType != ItemStack::Type_Equipped) mItems.push_back(item); } } - void PickpocketItemModel::removeItem (const ItemStack &item, size_t count) + void PickpocketItemModel::removeItem(const ItemStack& item, size_t count) { ProxyItemModel::removeItem(item, count); } - bool PickpocketItemModel::onDropItem(const MWWorld::Ptr &item, int count) + bool PickpocketItemModel::onDropItem(const MWWorld::Ptr& item, int count) { // don't allow "reverse pickpocket" (it will be handled by scripts after 1.0) return false; @@ -88,8 +91,8 @@ namespace MWGui { // Make sure we were actually closed, rather than just temporarily hidden (e.g. console or main menu opened) if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Container) - // If it was already detected while taking an item, no need to check now - || mPickpocketDetected) + // If it was already detected while taking an item, no need to check now + || mPickpocketDetected) return; MWWorld::Ptr player = MWMechanics::getPlayer(); @@ -97,13 +100,13 @@ namespace MWGui if (pickpocket.finish()) { MWBase::Environment::get().getMechanicsManager()->commitCrime( - player, mActor, MWBase::MechanicsManager::OT_Pickpocket, std::string(), 0, true); + player, mActor, MWBase::MechanicsManager::OT_Pickpocket, ESM::RefId(), 0, true); MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); mPickpocketDetected = true; } } - bool PickpocketItemModel::onTakeItem(const MWWorld::Ptr &item, int count) + bool PickpocketItemModel::onTakeItem(const MWWorld::Ptr& item, int count) { if (mActor.getClass().getCreatureStats(mActor).getKnockedDown()) return mSourceModel->onTakeItem(item, count); @@ -118,20 +121,20 @@ namespace MWGui return success; } - bool PickpocketItemModel::stealItem(const MWWorld::Ptr &item, int count) + bool PickpocketItemModel::stealItem(const MWWorld::Ptr& item, int count) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::Pickpocket pickpocket(player, mActor); if (pickpocket.pick(item, count)) { MWBase::Environment::get().getMechanicsManager()->commitCrime( - player, mActor, MWBase::MechanicsManager::OT_Pickpocket, std::string(), 0, true); + player, mActor, MWBase::MechanicsManager::OT_Pickpocket, ESM::RefId(), 0, true); MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); mPickpocketDetected = true; return false; } else - player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 1); + player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, ESM::Skill::Sneak_PickPocket); return true; } diff --git a/apps/openmw/mwgui/pickpocketitemmodel.hpp b/apps/openmw/mwgui/pickpocketitemmodel.hpp index e28af73d78b..3ed1786d828 100644 --- a/apps/openmw/mwgui/pickpocketitemmodel.hpp +++ b/apps/openmw/mwgui/pickpocketitemmodel.hpp @@ -6,25 +6,26 @@ namespace MWGui { - /// @brief The pickpocket item model randomly hides item stacks based on a specified chance. Equipped items are always hidden. + /// @brief The pickpocket item model randomly hides item stacks based on a specified chance. Equipped items are + /// always hidden. class PickpocketItemModel : public ProxyItemModel { public: - PickpocketItemModel (const MWWorld::Ptr& thief, ItemModel* sourceModel, bool hideItems=true); + PickpocketItemModel(const MWWorld::Ptr& thief, std::unique_ptr sourceModel, bool hideItems = true); bool allowedToUseItems() const override; - ItemStack getItem (ModelIndex index) override; + ItemStack getItem(ModelIndex index) override; size_t getItemCount() override; void update() override; - void removeItem (const ItemStack& item, size_t count) override; + void removeItem(const ItemStack& item, size_t count) override; void onClose() override; - bool onDropItem(const MWWorld::Ptr &item, int count) override; - bool onTakeItem(const MWWorld::Ptr &item, int count) override; + bool onDropItem(const MWWorld::Ptr& item, int count) override; + bool onTakeItem(const MWWorld::Ptr& item, int count) override; protected: MWWorld::Ptr mActor; bool mPickpocketDetected; - bool stealItem(const MWWorld::Ptr &item, int count); + bool stealItem(const MWWorld::Ptr& item, int count); private: std::vector mHiddenItems; diff --git a/apps/openmw/mwgui/postprocessorhud.cpp b/apps/openmw/mwgui/postprocessorhud.cpp new file mode 100644 index 00000000000..8f5b20ba980 --- /dev/null +++ b/apps/openmw/mwgui/postprocessorhud.cpp @@ -0,0 +1,497 @@ +#include "postprocessorhud.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include "../mwrender/postprocessor.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +namespace MWGui +{ + void PostProcessorHud::ListWrapper::onKeyButtonPressed(MyGUI::KeyCode key, MyGUI::Char ch) + { + if (MyGUI::InputManager::getInstance().isShiftPressed() + && (key == MyGUI::KeyCode::ArrowUp || key == MyGUI::KeyCode::ArrowDown)) + return; + + MyGUI::ListBox::onKeyButtonPressed(key, ch); + } + + PostProcessorHud::PostProcessorHud() + : WindowBase("openmw_postprocessor_hud.layout") + { + getWidget(mActiveList, "ActiveList"); + getWidget(mInactiveList, "InactiveList"); + getWidget(mConfigLayout, "ConfigLayout"); + getWidget(mFilter, "Filter"); + getWidget(mButtonActivate, "ButtonActivate"); + getWidget(mButtonDeactivate, "ButtonDeactivate"); + getWidget(mButtonUp, "ButtonUp"); + getWidget(mButtonDown, "ButtonDown"); + + mButtonActivate->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyActivatePressed); + mButtonDeactivate->eventMouseButtonClick + += MyGUI::newDelegate(this, &PostProcessorHud::notifyDeactivatePressed); + mButtonUp->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyShaderUpPressed); + mButtonDown->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyShaderDownPressed); + + mActiveList->eventKeyButtonPressed += MyGUI::newDelegate(this, &PostProcessorHud::notifyKeyButtonPressed); + mInactiveList->eventKeyButtonPressed += MyGUI::newDelegate(this, &PostProcessorHud::notifyKeyButtonPressed); + + mActiveList->eventListChangePosition += MyGUI::newDelegate(this, &PostProcessorHud::notifyListChangePosition); + mInactiveList->eventListChangePosition += MyGUI::newDelegate(this, &PostProcessorHud::notifyListChangePosition); + + mFilter->eventEditTextChange += MyGUI::newDelegate(this, &PostProcessorHud::notifyFilterChanged); + + mMainWidget->castType()->eventWindowChangeCoord + += MyGUI::newDelegate(this, &PostProcessorHud::notifyWindowResize); + + mShaderInfo = mConfigLayout->createWidget("HeaderText", {}, MyGUI::Align::Default); + mShaderInfo->setUserString("VStretch", "true"); + mShaderInfo->setUserString("HStretch", "true"); + mShaderInfo->setTextAlign(MyGUI::Align::Left | MyGUI::Align::Top); + mShaderInfo->setEditReadOnly(true); + mShaderInfo->setEditWordWrap(true); + mShaderInfo->setEditMultiLine(true); + mShaderInfo->setNeedMouseFocus(false); + + mConfigLayout->setVisibleVScroll(true); + + mConfigArea = mConfigLayout->createWidget({}, {}, MyGUI::Align::Default); + + mConfigLayout->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel); + mConfigArea->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel); + } + + void PostProcessorHud::notifyFilterChanged(MyGUI::EditBox* sender) + { + updateTechniques(); + } + + void PostProcessorHud::notifyWindowResize(MyGUI::Window* sender) + { + layout(); + } + + void PostProcessorHud::notifyResetButtonClicked(MyGUI::Widget* sender) + { + for (size_t i = 1; i < mConfigArea->getChildCount(); ++i) + { + if (auto* child = dynamic_cast(mConfigArea->getChildAt(i))) + child->toDefault(); + } + } + + void PostProcessorHud::notifyListChangePosition(MyGUI::ListBox* sender, size_t index) + { + if (sender == mActiveList) + mInactiveList->clearIndexSelected(); + else if (sender == mInactiveList) + mActiveList->clearIndexSelected(); + + if (index >= sender->getItemCount()) + return; + + updateConfigView(sender->getItemNameAt(index)); + } + + void PostProcessorHud::toggleTechnique(bool enabled) + { + auto* list = enabled ? mInactiveList : mActiveList; + + size_t selected = list->getIndexSelected(); + + if (selected != MyGUI::ITEM_NONE) + { + auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); + mOverrideHint = list->getItemNameAt(selected); + + auto technique = *list->getItemDataAt>(selected); + if (technique->getDynamic()) + return; + + if (enabled) + processor->enableTechnique(std::move(technique)); + else + processor->disableTechnique(std::move(technique)); + processor->saveChain(); + } + } + + void PostProcessorHud::notifyActivatePressed(MyGUI::Widget* sender) + { + toggleTechnique(true); + } + + void PostProcessorHud::notifyDeactivatePressed(MyGUI::Widget* sender) + { + toggleTechnique(false); + } + + void PostProcessorHud::moveShader(Direction direction) + { + auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); + + size_t selected = mActiveList->getIndexSelected(); + + if (selected == MyGUI::ITEM_NONE) + return; + + int index = direction == Direction::Up ? static_cast(selected) - 1 : selected + 1; + index = std::clamp(index, 0, mActiveList->getItemCount() - 1); + + if (static_cast(index) != selected) + { + auto technique = *mActiveList->getItemDataAt>(selected); + if (technique->getDynamic() || technique->getInternal()) + return; + + if (processor->enableTechnique(std::move(technique), index - mOffset) + != MWRender::PostProcessor::Status_Error) + processor->saveChain(); + } + } + + void PostProcessorHud::notifyShaderUpPressed(MyGUI::Widget* sender) + { + moveShader(Direction::Up); + } + + void PostProcessorHud::notifyShaderDownPressed(MyGUI::Widget* sender) + { + moveShader(Direction::Down); + } + + void PostProcessorHud::notifyKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char ch) + { + MyGUI::ListBox* list = static_cast(sender); + + if (list->getIndexSelected() == MyGUI::ITEM_NONE) + return; + + if (key == MyGUI::KeyCode::ArrowLeft && list == mActiveList) + { + if (MyGUI::InputManager::getInstance().isShiftPressed()) + { + toggleTechnique(false); + } + else + { + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mInactiveList); + mActiveList->clearIndexSelected(); + select(mInactiveList, 0); + } + } + else if (key == MyGUI::KeyCode::ArrowRight && list == mInactiveList) + { + if (MyGUI::InputManager::getInstance().isShiftPressed()) + { + toggleTechnique(true); + } + else + { + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mActiveList); + mInactiveList->clearIndexSelected(); + select(mActiveList, 0); + } + } + else if (list == mActiveList && MyGUI::InputManager::getInstance().isShiftPressed() + && (key == MyGUI::KeyCode::ArrowUp || key == MyGUI::KeyCode::ArrowDown)) + { + moveShader(key == MyGUI::KeyCode::ArrowUp ? Direction::Up : Direction::Down); + } + } + + void PostProcessorHud::onOpen() + { + toggleMode(Settings::ShaderManager::Mode::Debug); + updateTechniques(); + } + + void PostProcessorHud::onClose() + { + toggleMode(Settings::ShaderManager::Mode::Normal); + } + + void PostProcessorHud::layout() + { + constexpr int padding = 12; + constexpr int padding2 = padding * 2; + mShaderInfo->setCoord( + padding, padding, mConfigLayout->getSize().width - padding2 - padding, mShaderInfo->getTextSize().height); + + int totalHeight = mShaderInfo->getTop() + mShaderInfo->getTextSize().height + padding; + + mConfigArea->setCoord({ padding, totalHeight, mShaderInfo->getSize().width, mConfigLayout->getHeight() }); + + int childHeights = 0; + MyGUI::EnumeratorWidgetPtr enumerator = mConfigArea->getEnumerator(); + while (enumerator.next()) + { + enumerator.current()->setCoord(padding, childHeights + padding, mShaderInfo->getSize().width - padding2, + enumerator.current()->getHeight()); + childHeights += enumerator.current()->getHeight() + padding; + } + totalHeight += childHeights; + + mConfigArea->setSize(mConfigArea->getWidth(), childHeights); + + mConfigLayout->setCanvasSize(mConfigLayout->getWidth() - padding2, totalHeight); + mConfigLayout->setSize(mConfigLayout->getWidth(), mConfigLayout->getParentSize().height - padding2); + } + + void PostProcessorHud::notifyMouseWheel(MyGUI::Widget* sender, int rel) + { + int offset = mConfigLayout->getViewOffset().top + rel * 0.3; + if (offset > 0) + mConfigLayout->setViewOffset(MyGUI::IntPoint(0, 0)); + else + mConfigLayout->setViewOffset(MyGUI::IntPoint(0, static_cast(offset))); + } + + void PostProcessorHud::select(ListWrapper* list, size_t index) + { + list->setIndexSelected(index); + notifyListChangePosition(list, index); + } + + void PostProcessorHud::toggleMode(Settings::ShaderManager::Mode mode) + { + Settings::ShaderManager::get().setMode(mode); + + MWBase::Environment::get().getWorld()->getPostProcessor()->toggleMode(); + + if (!isVisible()) + return; + + if (mInactiveList->getIndexSelected() != MyGUI::ITEM_NONE) + updateConfigView(mInactiveList->getItemNameAt(mInactiveList->getIndexSelected())); + else if (mActiveList->getIndexSelected() != MyGUI::ITEM_NONE) + updateConfigView(mActiveList->getItemNameAt(mActiveList->getIndexSelected())); + } + + void PostProcessorHud::updateConfigView(const std::string& name) + { + auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); + + auto technique = processor->loadTechnique(name); + + if (!technique || technique->getStatus() == fx::Technique::Status::File_Not_exists) + return; + + while (mConfigArea->getChildCount() > 0) + MyGUI::Gui::getInstance().destroyWidget(mConfigArea->getChildAt(0)); + + mShaderInfo->setCaption({}); + + std::ostringstream ss; + + const std::string_view NA = "#{Interface:NotAvailableShort}"; + const char endl = '\n'; + + std::string_view author = technique->getAuthor().empty() ? NA : technique->getAuthor(); + std::string_view version = technique->getVersion().empty() ? NA : technique->getVersion(); + std::string_view description = technique->getDescription().empty() ? NA : technique->getDescription(); + + auto serializeBool = [](bool value) { return value ? "#{Interface:Yes}" : "#{Interface:No}"; }; + + 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)); + + switch (technique->getStatus()) + { + case fx::Technique::Status::Success: + case fx::Technique::Status::Uncompiled: + { + if (technique->getDynamic()) + ss << "#{fontcolourhtml=header}#{OMWShaders:ShaderLocked}: #{fontcolourhtml=normal} " + "#{OMWShaders:ShaderLockedDescription}" + << endl + << endl; + ss << "#{fontcolourhtml=header}#{OMWShaders:Author}: #{fontcolourhtml=normal} " << author << endl + << endl + << "#{fontcolourhtml=header}#{OMWShaders:Version}: #{fontcolourhtml=normal} " << version << endl + << endl + << "#{fontcolourhtml=header}#{OMWShaders:Description}: #{fontcolourhtml=normal} " << description + << endl + << endl + << "#{fontcolourhtml=header}#{OMWShaders:InInteriors}: #{fontcolourhtml=normal} " << flag_interior + << "#{fontcolourhtml=header} #{OMWShaders:InExteriors}: #{fontcolourhtml=normal} " << flag_exterior + << "#{fontcolourhtml=header} #{OMWShaders:Underwater}: #{fontcolourhtml=normal} " + << flag_underwater + << "#{fontcolourhtml=header} #{OMWShaders:Abovewater}: #{fontcolourhtml=normal} " + << flag_abovewater; + break; + } + 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: + break; + } + + mShaderInfo->setCaptionWithReplacing(ss.str()); + + if (Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug) + { + if (technique->getUniformMap().size() > 0) + { + MyGUI::Button* resetButton + = mConfigArea->createWidget("MW_Button", { 0, 0, 0, 24 }, MyGUI::Align::Default); + resetButton->setCaptionWithReplacing("#{OMWShaders:ResetShader}"); + resetButton->setTextAlign(MyGUI::Align::Center); + resetButton->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel); + resetButton->eventMouseButtonClick + += MyGUI::newDelegate(this, &PostProcessorHud::notifyResetButtonClicked); + } + + for (const auto& uniform : technique->getUniformMap()) + { + if (!uniform->mStatic || uniform->mSamplerType) + continue; + + if (!uniform->mHeader.empty()) + { + Gui::AutoSizedTextBox* divider = mConfigArea->createWidget( + "MW_UniformGroup", { 0, 0, 0, 34 }, MyGUI::Align::Default); + divider->setNeedMouseFocus(false); + divider->setCaptionWithReplacing(uniform->mHeader); + } + + 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); + } + } + + layout(); + } + + void PostProcessorHud::updateTechniques() + { + if (!isVisible()) + return; + + std::string hint; + ListWrapper* hintWidget = nullptr; + if (mInactiveList->getIndexSelected() != MyGUI::ITEM_NONE) + { + hint = mInactiveList->getItemNameAt(mInactiveList->getIndexSelected()); + hintWidget = mInactiveList; + } + else if (mActiveList->getIndexSelected() != MyGUI::ITEM_NONE) + { + hint = mActiveList->getItemNameAt(mActiveList->getIndexSelected()); + hintWidget = mActiveList; + } + + mInactiveList->removeAllItems(); + mActiveList->removeAllItems(); + + 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); + + for (const std::string& name : techniques) + { + auto technique = processor->loadTechnique(name); + + if (!technique) + continue; + + if (!technique->getHidden() && !processor->isTechniqueEnabled(technique)) + { + std::string lowerName = Utf8Stream::lowerCaseUtf8(name); + std::string lowerCaption = mFilter->getCaption(); + lowerCaption = Utf8Stream::lowerCaseUtf8(lowerCaption); + if (lowerName.find(lowerCaption) != std::string::npos) + mInactiveList->addItem(name, technique); + } + } + + mOffset = 0; + for (auto technique : processor->getTechniques()) + { + if (!technique->getHidden()) + { + mActiveList->addItem(technique->getName(), technique); + + if (technique->getInternal()) + mOffset++; + } + } + + auto tryFocus = [this](ListWrapper* widget, const std::string& hint) { + MyGUI::Widget* oldFocus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); + if (oldFocus == mFilter) + return; + size_t index = widget->findItemIndexWith(hint); + + if (index != MyGUI::ITEM_NONE) + { + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(widget); + select(widget, index); + } + }; + + if (!mOverrideHint.empty()) + { + tryFocus(mActiveList, mOverrideHint); + tryFocus(mInactiveList, mOverrideHint); + + mOverrideHint.clear(); + } + else if (hintWidget && !hint.empty()) + tryFocus(hintWidget, hint); + } + + 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"); + } +} diff --git a/apps/openmw/mwgui/postprocessorhud.hpp b/apps/openmw/mwgui/postprocessorhud.hpp new file mode 100644 index 00000000000..20e27bac3a0 --- /dev/null +++ b/apps/openmw/mwgui/postprocessorhud.hpp @@ -0,0 +1,104 @@ +#ifndef MYGUI_POSTPROCESSOR_HUD_H +#define MYGUI_POSTPROCESSOR_HUD_H + +#include "windowbase.hpp" + +#include + +#include + +namespace MyGUI +{ + class ScrollView; + class EditBox; + class TabItem; +} +namespace Gui +{ + class AutoSizedButton; + class AutoSizedEditBox; +} + +namespace MWGui +{ + class PostProcessorHud : public WindowBase + { + class ListWrapper final : public MyGUI::ListBox + { + MYGUI_RTTI_DERIVED(ListWrapper) + protected: + void onKeyButtonPressed(MyGUI::KeyCode key, MyGUI::Char ch) override; + }; + + public: + PostProcessorHud(); + + void onOpen() override; + + void onClose() override; + + void updateTechniques(); + + void toggleMode(Settings::ShaderManager::Mode mode); + + static void registerMyGUIComponents(); + + private: + void notifyWindowResize(MyGUI::Window* sender); + + void notifyFilterChanged(MyGUI::EditBox* sender); + + void updateConfigView(const std::string& name); + + void notifyResetButtonClicked(MyGUI::Widget* sender); + + void notifyListChangePosition(MyGUI::ListBox* sender, size_t index); + + void notifyKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char ch); + + void notifyActivatePressed(MyGUI::Widget* sender); + + void notifyDeactivatePressed(MyGUI::Widget* sender); + + void notifyShaderUpPressed(MyGUI::Widget* sender); + + void notifyShaderDownPressed(MyGUI::Widget* sender); + + void notifyMouseWheel(MyGUI::Widget* sender, int rel); + + enum class Direction + { + Up, + Down + }; + + void moveShader(Direction direction); + + void toggleTechnique(bool enabled); + + void select(ListWrapper* list, size_t index); + + void layout(); + + ListWrapper* mActiveList; + ListWrapper* mInactiveList; + + Gui::AutoSizedButton* mButtonActivate; + Gui::AutoSizedButton* mButtonDeactivate; + Gui::AutoSizedButton* mButtonDown; + Gui::AutoSizedButton* mButtonUp; + + MyGUI::ScrollView* mConfigLayout; + + MyGUI::Widget* mConfigArea; + + MyGUI::EditBox* mFilter; + Gui::AutoSizedEditBox* mShaderInfo; + + std::string mOverrideHint; + + int mOffset = 0; + }; +} + +#endif diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index a6bfac2a456..93b0ef071f1 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -1,33 +1,35 @@ #include "quickkeysmenu.hpp" -#include #include +#include #include #include #include -#include -#include +#include +#include +#include +#include +#include -#include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/player.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/player.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwmechanics/spellutil.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/spellutil.hpp" #include "itemselection.hpp" -#include "spellview.hpp" #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" - +#include "spellview.hpp" namespace MWGui { @@ -37,25 +39,21 @@ namespace MWGui , mKey(std::vector(10)) , mSelected(nullptr) , mActivated(nullptr) - , mAssignDialog(nullptr) - , mItemSelectionDialog(nullptr) - , mMagicSelectionDialog(nullptr) { getWidget(mOkButton, "OKButton"); getWidget(mInstructionLabel, "InstructionLabel"); mMainWidget->setSize(mMainWidget->getWidth(), - mMainWidget->getHeight() + - (mInstructionLabel->getTextSize().height - mInstructionLabel->getHeight())); + mMainWidget->getHeight() + (mInstructionLabel->getTextSize().height - mInstructionLabel->getHeight())); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onOkButtonClicked); center(); for (int i = 0; i < 10; ++i) { - mKey[i].index = i+1; - getWidget(mKey[i].button, "QuickKey" + MyGUI::utility::toString(i+1)); + mKey[i].index = i + 1; + getWidget(mKey[i].button, "QuickKey" + MyGUI::utility::toString(i + 1)); mKey[i].button->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); unassign(&mKey[i]); @@ -66,57 +64,65 @@ namespace MWGui { mActivated = nullptr; - for (int i=0; i<10; ++i) + for (int i = 0; i < 10; ++i) { unassign(&mKey[i]); } } - QuickKeysMenu::~QuickKeysMenu() + inline void QuickKeysMenu::validate(int index) { - delete mAssignDialog; - delete mItemSelectionDialog; - delete mMagicSelectionDialog; - } - - void QuickKeysMenu::onOpen() - { - WindowBase::onOpen(); - MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - - // Check if quick keys are still valid - for (int i=0; i<10; ++i) + switch (mKey[index].type) { - switch (mKey[i].type) + case ESM::QuickKeys::Type::Unassigned: + case ESM::QuickKeys::Type::HandToHand: + case ESM::QuickKeys::Type::Magic: + break; + case ESM::QuickKeys::Type::Item: + case ESM::QuickKeys::Type::MagicItem: { - case Type_Unassigned: - case Type_HandToHand: - case Type_Magic: - break; - case Type_Item: - case 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)) { - MWWorld::Ptr item = *mKey[i].button->getUserData(); - // Make sure the item is available and is not broken - if (!item || item.getRefData().getCount() < 1 || - (item.getClass().hasItemHealth(item) && - item.getClass().getItemHealth(item) <= 0)) - { - // Try searching for a compatible replacement - item = store.findReplacement(mKey[i].id); + // Try searching for a compatible replacement + item = store.findReplacement(mKey[index].id); - if (item) - mKey[i].button->setUserData(MWWorld::Ptr(item)); + if (!item.isEmpty()) + mKey[index].button->setUserData(MWWorld::Ptr(item)); - break; - } + break; } } } } + void QuickKeysMenu::onOpen() + { + WindowBase::onOpen(); + + // Quick key index + for (int index = 0; index < 10; ++index) + { + validate(index); + } + } + + void QuickKeysMenu::onClose() + { + WindowBase::onClose(); + + if (mAssignDialog) + mAssignDialog->setVisible(false); + if (mItemSelectionDialog) + mItemSelectionDialog->setVisible(false); + if (mMagicSelectionDialog) + mMagicSelectionDialog->setVisible(false); + } + void QuickKeysMenu::unassign(keyData* key) { key->button->clearUserStrings(); @@ -127,22 +133,22 @@ namespace MWGui if (key->index == 10) { - key->type = Type_HandToHand; + key->type = ESM::QuickKeys::Type::HandToHand; - MyGUI::ImageBox* image = key->button->createWidget("ImageBox", - MyGUI::IntCoord(14, 13, 32, 32), MyGUI::Align::Default); + MyGUI::ImageBox* image = key->button->createWidget( + "ImageBox", MyGUI::IntCoord(14, 13, 32, 32), MyGUI::Align::Default); image->setImageTexture("icons\\k\\stealth_handtohand.dds"); image->setNeedMouseFocus(false); } else { - key->type = Type_Unassigned; - key->id = ""; - key->name = ""; + key->type = ESM::QuickKeys::Type::Unassigned; + key->id = ESM::RefId(); + key->name.clear(); - MyGUI::TextBox* textBox = key->button->createWidgetReal("SandText", - MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); + MyGUI::TextBox* textBox = key->button->createWidgetReal( + "SandText", MyGUI::FloatCoord(0, 0, 1, 1), MyGUI::Align::Default); textBox->setTextAlign(MyGUI::Align::Center); textBox->setCaption(MyGUI::utility::toString(key->index)); @@ -170,18 +176,18 @@ namespace MWGui mSelected = &mKey[index]; - // prevent reallocation of zero key from Type_HandToHand - if(mSelected->index == 10) + // prevent reallocation of zero key from ESM::QuickKeys::Type::HandToHand + if (mSelected->index == 10) return; // open assign dialog if (!mAssignDialog) - mAssignDialog = new QuickKeysMenuAssign(this); + mAssignDialog = std::make_unique(this); mAssignDialog->setVisible(true); } - void QuickKeysMenu::onOkButtonClicked (MyGUI::Widget *sender) + void QuickKeysMenu::onOkButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_QuickKeysMenu); } @@ -190,7 +196,7 @@ namespace MWGui { if (!mItemSelectionDialog) { - mItemSelectionDialog = new ItemSelectionDialog("#{sQuickMenu6}"); + mItemSelectionDialog = std::make_unique("#{sQuickMenu6}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &QuickKeysMenu::onAssignItem); mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &QuickKeysMenu::onAssignItemCancel); } @@ -205,7 +211,7 @@ namespace MWGui { if (!mMagicSelectionDialog) { - mMagicSelectionDialog = new MagicSelectionDialog(this); + mMagicSelectionDialog = std::make_unique(this); } mMagicSelectionDialog->setVisible(true); @@ -230,7 +236,7 @@ namespace MWGui while (mSelected->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); - mSelected->type = Type_Item; + mSelected->type = ESM::QuickKeys::Type::Item; mSelected->id = item.getCellRef().getRefId(); mSelected->name = item.getClass().getName(item); @@ -254,16 +260,18 @@ namespace MWGui while (mSelected->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); - mSelected->type = Type_MagicItem; + mSelected->type = ESM::QuickKeys::Type::MagicItem; mSelected->id = item.getCellRef().getRefId(); mSelected->name = item.getClass().getName(item); float scale = 1.f; - MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture("textures\\menu_icon_select_magic_magic.dds"); + MyGUI::ITexture* texture + = MyGUI::RenderManager::getInstance().getTexture("textures\\menu_icon_select_magic_magic.dds"); if (texture) scale = texture->getHeight() / 64.f; - mSelected->button->setFrame("textures\\menu_icon_select_magic_magic.dds", MyGUI::IntCoord(0, 0, 44*scale, 44*scale)); + mSelected->button->setFrame( + "textures\\menu_icon_select_magic_magic.dds", MyGUI::IntCoord(0, 0, 44 * scale, 44 * scale)); mSelected->button->setIcon(item); mSelected->button->setUserString("ToolTipType", "ItemPtr"); @@ -273,37 +281,41 @@ namespace MWGui mMagicSelectionDialog->setVisible(false); } - void QuickKeysMenu::onAssignMagic(const std::string& spellId) + void QuickKeysMenu::onAssignMagic(const ESM::RefId& spellId) { assert(mSelected); while (mSelected->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); - const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); const ESM::Spell* spell = esmStore.get().find(spellId); - mSelected->type = Type_Magic; + mSelected->type = ESM::QuickKeys::Type::Magic; mSelected->id = spellId; mSelected->name = spell->mName; mSelected->button->setItem(MWWorld::Ptr()); mSelected->button->setUserString("ToolTipType", "Spell"); - mSelected->button->setUserString("Spell", spellId); + mSelected->button->setUserString("Spell", spellId.serialize()); // use the icon of the first effect - const ESM::MagicEffect* effect = esmStore.get().find(spell->mEffects.mList.front().mEffectID); + const ESM::MagicEffect* effect + = esmStore.get().find(spell->mEffects.mList.front().mData.mEffectID); std::string path = effect->mIcon; + std::replace(path.begin(), path.end(), '/', '\\'); int slashPos = path.rfind('\\'); - path.insert(slashPos+1, "b_"); - path = MWBase::Environment::get().getWindowManager()->correctIconPath(path); + path.insert(slashPos + 1, "b_"); + path = Misc::ResourceHelpers::correctIconPath(path, MWBase::Environment::get().getResourceSystem()->getVFS()); float scale = 1.f; - MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture("textures\\menu_icon_select_magic.dds"); + MyGUI::ITexture* texture + = MyGUI::RenderManager::getInstance().getTexture("textures\\menu_icon_select_magic.dds"); if (texture) scale = texture->getHeight() / 64.f; - mSelected->button->setFrame("textures\\menu_icon_select_magic.dds", MyGUI::IntCoord(0, 0, 44*scale, 44*scale)); + mSelected->button->setFrame( + "textures\\menu_icon_select_magic.dds", MyGUI::IntCoord(0, 0, 44 * scale, 44 * scale)); mSelected->button->setIcon(path); if (mMagicSelectionDialog) @@ -328,26 +340,26 @@ namespace MWGui { assert(index >= 1 && index <= 10); - keyData *key = &mKey[index-1]; + keyData* key = &mKey[index - 1]; MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - const MWMechanics::CreatureStats &playerStats = player.getClass().getCreatureStats(player); + const MWMechanics::CreatureStats& playerStats = player.getClass().getCreatureStats(player); + + validate(index - 1); // Delay action executing, // if player is busy for now (casting a spell, attacking someone, etc.) bool isDelayNeeded = MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player) - || playerStats.getKnockedDown() - || playerStats.getHitRecovery(); + || playerStats.getKnockedDown() || playerStats.getHitRecovery(); - bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); - bool isReturnNeeded = (!godmode && playerStats.isParalyzed()) || playerStats.isDead(); + bool isReturnNeeded = playerStats.isParalyzed() || playerStats.isDead(); - if (isReturnNeeded && key->type != Type_Item) + if (isReturnNeeded) { return; } - else if (isDelayNeeded && key->type != Type_Item) + else if (isDelayNeeded) { mActivated = key; return; @@ -357,7 +369,7 @@ namespace MWGui mActivated = nullptr; } - if (key->type == Type_Item || key->type == Type_MagicItem) + if (key->type == ESM::QuickKeys::Type::Item || key->type == ESM::QuickKeys::Type::MagicItem) { MWWorld::Ptr item = *key->button->getUserData(); @@ -371,47 +383,32 @@ namespace MWGui item = nullptr; // check the item is available and not broken - if (!item || item.getRefData().getCount() < 1 || - (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) + if (item.isEmpty() || item.getCellRef().getCount() < 1 + || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) { item = store.findReplacement(key->id); - if (!item || item.getRefData().getCount() < 1) + if (item.isEmpty() || item.getCellRef().getCount() < 1) { - MWBase::Environment::get().getWindowManager()->messageBox( - "#{sQuickMenu5} " + key->name); + MWBase::Environment::get().getWindowManager()->messageBox("#{sQuickMenu5} " + key->name); return; } } - if (key->type == Type_Item) + if (key->type == ESM::QuickKeys::Type::Item) { - bool isWeapon = item.getTypeName() == typeid(ESM::Weapon).name(); - bool isTool = item.getTypeName() == typeid(ESM::Probe).name() || - item.getTypeName() == typeid(ESM::Lockpick).name(); - - // delay weapon switching if player is busy - if (isDelayNeeded && (isWeapon || isTool)) - { - mActivated = key; - return; - } - else if (isReturnNeeded && (isWeapon || isTool)) - { - return; - } - if (!store.isEquipped(item)) MWBase::Environment::get().getWindowManager()->useItem(item); - MWWorld::ConstContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + MWWorld::ConstContainerStoreIterator rightHand + = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); // change draw state only if the item is in player's right hand if (rightHand != store.end() && item == *rightHand) { - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Weapon); } } - else if (key->type == Type_MagicItem) + else if (key->type == ESM::QuickKeys::Type::MagicItem) { // equip, if it can be equipped and isn't yet equipped if (!item.getClass().getEquipmentSlots(item).first.empty() && !store.isEquipped(item)) @@ -424,12 +421,12 @@ namespace MWGui } store.setSelectedEnchantItem(it); - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Spell); } } - else if (key->type == Type_Magic) + else if (key->type == ESM::QuickKeys::Type::Magic) { - std::string spellId = key->id; + const ESM::RefId& spellId = key->id; // Make sure the player still has this spell MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); @@ -442,20 +439,20 @@ namespace MWGui } store.setSelectedEnchantItem(store.end()); - MWBase::Environment::get().getWindowManager() - ->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); + MWBase::Environment::get().getWindowManager()->setSelectedSpell( + spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Spell); } - else if (key->type == Type_HandToHand) + else if (key->type == ESM::QuickKeys::Type::HandToHand) { - store.unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, player); - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); + store.unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Weapon); } } // --------------------------------------------------------------------------------------------------------- - QuickKeysMenuAssign::QuickKeysMenuAssign (QuickKeysMenu* parent) + QuickKeysMenuAssign::QuickKeysMenuAssign(QuickKeysMenu* parent) : WindowModal("openmw_quickkeys_menu_assign.layout") , mParent(parent) { @@ -470,67 +467,57 @@ namespace MWGui mUnassignButton->eventMouseButtonClick += MyGUI::newDelegate(mParent, &QuickKeysMenu::onUnassignButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(mParent, &QuickKeysMenu::onCancelButtonClicked); - - int maxWidth = mLabel->getTextSize ().width + 24; - maxWidth = std::max(maxWidth, mItemButton->getTextSize ().width + 24); - maxWidth = std::max(maxWidth, mMagicButton->getTextSize ().width + 24); - maxWidth = std::max(maxWidth, mUnassignButton->getTextSize ().width + 24); - maxWidth = std::max(maxWidth, mCancelButton->getTextSize ().width + 24); + int maxWidth = mLabel->getTextSize().width + 24; + maxWidth = std::max(maxWidth, mItemButton->getTextSize().width + 24); + maxWidth = std::max(maxWidth, mMagicButton->getTextSize().width + 24); + maxWidth = std::max(maxWidth, mUnassignButton->getTextSize().width + 24); + maxWidth = std::max(maxWidth, mCancelButton->getTextSize().width + 24); mMainWidget->setSize(maxWidth + 24, mMainWidget->getHeight()); mLabel->setSize(maxWidth, mLabel->getHeight()); - mItemButton->setCoord((maxWidth - mItemButton->getTextSize().width-24)/2 + 8, - mItemButton->getTop(), - mItemButton->getTextSize().width + 24, - mItemButton->getHeight()); - mMagicButton->setCoord((maxWidth - mMagicButton->getTextSize().width-24)/2 + 8, - mMagicButton->getTop(), - mMagicButton->getTextSize().width + 24, - mMagicButton->getHeight()); - mUnassignButton->setCoord((maxWidth - mUnassignButton->getTextSize().width-24)/2 + 8, - mUnassignButton->getTop(), - mUnassignButton->getTextSize().width + 24, - mUnassignButton->getHeight()); - mCancelButton->setCoord((maxWidth - mCancelButton->getTextSize().width-24)/2 + 8, - mCancelButton->getTop(), - mCancelButton->getTextSize().width + 24, - mCancelButton->getHeight()); + mItemButton->setCoord((maxWidth - mItemButton->getTextSize().width - 24) / 2 + 8, mItemButton->getTop(), + mItemButton->getTextSize().width + 24, mItemButton->getHeight()); + mMagicButton->setCoord((maxWidth - mMagicButton->getTextSize().width - 24) / 2 + 8, mMagicButton->getTop(), + mMagicButton->getTextSize().width + 24, mMagicButton->getHeight()); + mUnassignButton->setCoord((maxWidth - mUnassignButton->getTextSize().width - 24) / 2 + 8, + mUnassignButton->getTop(), mUnassignButton->getTextSize().width + 24, mUnassignButton->getHeight()); + mCancelButton->setCoord((maxWidth - mCancelButton->getTextSize().width - 24) / 2 + 8, mCancelButton->getTop(), + mCancelButton->getTextSize().width + 24, mCancelButton->getHeight()); center(); } - void QuickKeysMenu::write(ESM::ESMWriter &writer) + void QuickKeysMenu::write(ESM::ESMWriter& writer) { writer.startRecord(ESM::REC_KEYS); ESM::QuickKeys keys; // NB: The quick key with index 9 always has Hand-to-Hand type and must not be saved - for (int i=0; i<9; ++i) + for (int i = 0; i < 9; ++i) { ItemWidget* button = mKey[i].button; - int type = mKey[i].type; + const ESM::QuickKeys::Type type = mKey[i].type; ESM::QuickKeys::QuickKey key; key.mType = type; switch (type) { - case Type_Unassigned: - case Type_HandToHand: + case ESM::QuickKeys::Type::Unassigned: + case ESM::QuickKeys::Type::HandToHand: break; - case Type_Item: - case Type_MagicItem: + case ESM::QuickKeys::Type::Item: + case ESM::QuickKeys::Type::MagicItem: { MWWorld::Ptr item = *button->getUserData(); key.mId = item.getCellRef().getRefId(); break; } - case Type_Magic: - std::string spellId = button->getUserString("Spell"); - key.mId = spellId; + case ESM::QuickKeys::Type::Magic: + key.mId = ESM::RefId::deserialize(button->getUserString("Spell")); break; } @@ -542,7 +529,7 @@ namespace MWGui writer.endRecord(ESM::REC_KEYS); } - void QuickKeysMenu::readRecord(ESM::ESMReader &reader, uint32_t type) + void QuickKeysMenu::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type != ESM::REC_KEYS) return; @@ -553,7 +540,7 @@ namespace MWGui MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - int i=0; + int i = 0; for (ESM::QuickKeys::QuickKey& quickKey : keys.mKeys) { // NB: The quick key with index 9 always has Hand-to-Hand type and must not be loaded @@ -564,32 +551,32 @@ namespace MWGui switch (quickKey.mType) { - case Type_Magic: - if (MWBase::Environment::get().getWorld()->getStore().get().search(quickKey.mId)) - onAssignMagic(quickKey.mId); - break; - case Type_Item: - case Type_MagicItem: - { - // Find the item by id - MWWorld::Ptr item = store.findReplacement(quickKey.mId); - - if (item.isEmpty()) - unassign(mSelected); - else + case ESM::QuickKeys::Type::Magic: + if (MWBase::Environment::get().getESMStore()->get().search(quickKey.mId)) + onAssignMagic(quickKey.mId); + break; + case ESM::QuickKeys::Type::Item: + case ESM::QuickKeys::Type::MagicItem: { - if (quickKey.mType == Type_Item) - onAssignItem(item); - else // if (quickKey.mType == Type_MagicItem) - onAssignMagicItem(item); - } + // Find the item by id + MWWorld::Ptr item = store.findReplacement(quickKey.mId); - break; - } - case Type_Unassigned: - case Type_HandToHand: - unassign(mSelected); - break; + if (item.isEmpty()) + unassign(mSelected); + else + { + if (quickKey.mType == ESM::QuickKeys::Type::Item) + onAssignItem(item); + else // if (quickKey.mType == ESM::QuickKeys::Type::MagicItem) + onAssignMagicItem(item); + } + + break; + } + case ESM::QuickKeys::Type::Unassigned: + case ESM::QuickKeys::Type::HandToHand: + unassign(mSelected); + break; } ++i; @@ -613,7 +600,7 @@ namespace MWGui center(); } - void MagicSelectionDialog::onCancelButtonClicked (MyGUI::Widget *sender) + void MagicSelectionDialog::onCancelButtonClicked(MyGUI::Widget* sender) { exit(); } @@ -624,7 +611,7 @@ namespace MWGui return true; } - void MagicSelectionDialog::onOpen () + void MagicSelectionDialog::onOpen() { WindowModal::onOpen(); diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index b2742df7965..904029b9a06 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -1,15 +1,18 @@ #ifndef MWGUI_QUICKKEYS_H #define MWGUI_QUICKKEYS_H -#include "windowbase.hpp" +#include + +#include "components/esm3/quickkeys.hpp" +#include "itemselection.hpp" #include "spellmodel.hpp" +#include "windowbase.hpp" namespace MWGui { class QuickKeysMenuAssign; - class ItemSelectionDialog; class MagicSelectionDialog; class ItemWidget; class SpellView; @@ -18,7 +21,6 @@ namespace MWGui { public: QuickKeysMenu(); - ~QuickKeysMenu(); void onResChange(int, int) override { center(); } @@ -27,40 +29,31 @@ namespace MWGui void onUnassignButtonClicked(MyGUI::Widget* sender); void onCancelButtonClicked(MyGUI::Widget* sender); - void onAssignItem (MWWorld::Ptr item); - void onAssignItemCancel (); - void onAssignMagicItem (MWWorld::Ptr item); - void onAssignMagic (const std::string& spellId); - void onAssignMagicCancel (); + void onAssignItem(MWWorld::Ptr item); + void onAssignItemCancel(); + void onAssignMagicItem(MWWorld::Ptr item); + void onAssignMagic(const ESM::RefId& spellId); + void onAssignMagicCancel(); void onOpen() override; + void onClose() override; void activateQuickKey(int index); void updateActivatedQuickKey(); - /// @note This enum is serialized, so don't move the items around! - enum QuickKeyType - { - Type_Item, - Type_Magic, - Type_MagicItem, - Type_Unassigned, - Type_HandToHand - }; - - void write (ESM::ESMWriter& writer); - void readRecord (ESM::ESMReader& reader, uint32_t type); + void write(ESM::ESMWriter& writer); + void readRecord(ESM::ESMReader& reader, uint32_t type); void clear() override; + std::string_view getWindowIdForLua() const override { return "QuickKeys"; } private: - - struct keyData { - int index; - ItemWidget* button; - QuickKeysMenu::QuickKeyType type; - std::string id; + struct keyData + { + int index = -1; + ItemWidget* button = nullptr; + ESM::QuickKeys::Type type = ESM::QuickKeys::Type::Unassigned; + ESM::RefId id; std::string name; - keyData(): index(-1), button(nullptr), type(Type_Unassigned), id(""), name("") {} }; std::vector mKey; @@ -70,13 +63,14 @@ namespace MWGui MyGUI::EditBox* mInstructionLabel; MyGUI::Button* mOkButton; - QuickKeysMenuAssign* mAssignDialog; - ItemSelectionDialog* mItemSelectionDialog; - MagicSelectionDialog* mMagicSelectionDialog; + std::unique_ptr mAssignDialog; + std::unique_ptr mItemSelectionDialog; + std::unique_ptr mMagicSelectionDialog; void onQuickKeyButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); - + // Check if quick key is still valid + inline void validate(int index); void unassign(keyData* key); }; @@ -109,10 +103,9 @@ namespace MWGui QuickKeysMenu* mParent; - void onCancelButtonClicked (MyGUI::Widget* sender); + void onCancelButtonClicked(MyGUI::Widget* sender); void onModelIndexSelected(SpellModel::ModelIndex index); }; } - #endif diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index 457594697db..7b445d419f8 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -1,19 +1,23 @@ #include "race.hpp" -#include -#include #include +#include +#include +#include +#include #include #include +#include +#include #include +#include -#include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwrender/characterpreview.hpp" +#include "../mwworld/esmstore.hpp" #include "tooltips.hpp" @@ -29,7 +33,7 @@ namespace return index; } - bool sortRaces(const std::pair& left, const std::pair& right) + bool sortRaces(const std::pair& left, const std::pair& right) { return left.second.compare(right.second) < 0; } @@ -40,21 +44,24 @@ namespace MWGui { RaceDialog::RaceDialog(osg::Group* parent, Resource::ResourceSystem* resourceSystem) - : WindowModal("openmw_chargen_race.layout") - , mParent(parent) - , mResourceSystem(resourceSystem) - , mGenderIndex(0) - , mFaceIndex(0) - , mHairIndex(0) - , mCurrentAngle(0) - , mPreviewDirty(true) + : WindowModal("openmw_chargen_race.layout") + , mParent(parent) + , mResourceSystem(resourceSystem) + , mGenderIndex(0) + , mFaceIndex(0) + , mHairIndex(0) + , mCurrentAngle(0) + , mPreviewDirty(true) { // Centre dialog center(); - setText("AppearanceT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu1", "Appearance")); + setText("AppearanceT", + MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu1", "Appearance")); getWidget(mPreviewImage, "PreviewImage"); + mPreviewImage->eventMouseWheel += MyGUI::newDelegate(this, &RaceDialog::onPreviewScroll); + getWidget(mHeadRotate, "HeadRotate"); mHeadRotate->setScrollRange(1000); @@ -67,19 +74,22 @@ namespace MWGui // Set up next/previous buttons MyGUI::Button *prevButton, *nextButton; - setText("GenderChoiceT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu2", "Change Sex")); + setText("GenderChoiceT", + MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu2", "Change Sex")); getWidget(prevButton, "PrevGenderButton"); getWidget(nextButton, "NextGenderButton"); prevButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectPreviousGender); nextButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectNextGender); - setText("FaceChoiceT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu3", "Change Face")); + setText("FaceChoiceT", + MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu3", "Change Face")); getWidget(prevButton, "PrevFaceButton"); getWidget(nextButton, "NextFaceButton"); prevButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectPreviousFace); nextButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectNextFace); - setText("HairChoiceT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu4", "Change Hair")); + setText("HairChoiceT", + MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu4", "Change Hair")); getWidget(prevButton, "PrevHairButton"); getWidget(nextButton, "NextHairButton"); prevButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectPreviousHair); @@ -91,9 +101,11 @@ namespace MWGui mRaceList->eventListSelectAccept += MyGUI::newDelegate(this, &RaceDialog::onAccept); mRaceList->eventListChangePosition += MyGUI::newDelegate(this, &RaceDialog::onSelectRace); - setText("SkillsT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sBonusSkillTitle", "Skill Bonus")); + setText("SkillsT", + MWBase::Environment::get().getWindowManager()->getGameSettingString("sBonusSkillTitle", "Skill Bonus")); getWidget(mSkillList, "SkillList"); - setText("SpellPowerT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu7", "Specials")); + setText("SpellPowerT", + MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu7", "Specials")); getWidget(mSpellPowerList, "SpellPowerList"); MyGUI::Button* backButton; @@ -102,7 +114,8 @@ namespace MWGui MyGUI::Button* okButton; getWidget(okButton, "OKButton"); - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onOkClicked); updateRaces(); @@ -116,9 +129,11 @@ namespace MWGui getWidget(okButton, "OKButton"); if (shown) - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void RaceDialog::onOpen() @@ -134,11 +149,12 @@ namespace MWGui mPreview.reset(nullptr); mPreviewTexture.reset(nullptr); - mPreview.reset(new MWRender::RaceSelectionPreview(mParent, mResourceSystem)); + mPreview = std::make_unique(mParent, mResourceSystem); mPreview->rebuild(); - mPreview->setAngle (mCurrentAngle); + mPreview->setAngle(mCurrentAngle); - mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); + mPreviewTexture + = std::make_unique(mPreview->getTexture(), mPreview->getTextureStateSet()); mPreviewImage->setRenderItemTexture(mPreviewTexture.get()); mPreviewImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); @@ -147,35 +163,35 @@ namespace MWGui setGender(proto.isMale() ? GM_Male : GM_Female); recountParts(); - for (unsigned int i=0; igetScrollRange()/2+mHeadRotate->getScrollRange()/10; + size_t initialPos = mHeadRotate->getScrollRange() / 2 + mHeadRotate->getScrollRange() / 10; mHeadRotate->setScrollPosition(initialPos); onHeadRotate(mHeadRotate, initialPos); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mRaceList); } - void RaceDialog::setRaceId(const std::string &raceId) + void RaceDialog::setRaceId(const ESM::RefId& raceId) { mCurrentRaceId = raceId; mRaceList->setIndexSelected(MyGUI::ITEM_NONE); size_t count = mRaceList->getItemCount(); for (size_t i = 0; i < count; ++i) { - if (Misc::StringUtils::ciEqual(*mRaceList->getItemDataAt(i), raceId)) + if (*mRaceList->getItemDataAt(i) == raceId) { mRaceList->setIndexSelected(i); break; @@ -200,7 +216,7 @@ namespace MWGui void RaceDialog::onOkClicked(MyGUI::Widget* _sender) { - if(mRaceList->getIndexSelected() == MyGUI::ITEM_NONE) + if (mRaceList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } @@ -210,10 +226,23 @@ namespace MWGui eventBack(); } + void RaceDialog::onPreviewScroll(MyGUI::Widget*, int _delta) + { + size_t oldPos = mHeadRotate->getScrollPosition(); + size_t maxPos = mHeadRotate->getScrollRange() - 1; + size_t scrollPage = mHeadRotate->getScrollWheelPage(); + if (_delta < 0) + mHeadRotate->setScrollPosition(oldPos + std::min(maxPos - oldPos, scrollPage)); + else + mHeadRotate->setScrollPosition(oldPos - std::min(oldPos, scrollPage)); + + onHeadRotate(mHeadRotate, mHeadRotate->getScrollPosition()); + } + void RaceDialog::onHeadRotate(MyGUI::ScrollBar* scroll, size_t _position) { - float angle = (float(_position) / (scroll->getScrollRange()-1) - 0.5f) * osg::PI * 2; - mPreview->setAngle (angle); + float angle = (float(_position) / (scroll->getScrollRange() - 1) - 0.5f) * osg::PI * 2; + mPreview->setAngle(angle); mCurrentAngle = angle; } @@ -263,11 +292,11 @@ namespace MWGui if (_index == MyGUI::ITEM_NONE) return; - const std::string *raceId = mRaceList->getItemDataAt(_index); - if (Misc::StringUtils::ciEqual(mCurrentRaceId, *raceId)) + ESM::RefId& raceId = *mRaceList->getItemDataAt(_index); + if (mCurrentRaceId == raceId) return; - mCurrentRaceId = *raceId; + mCurrentRaceId = raceId; recountParts(); @@ -276,19 +305,18 @@ namespace MWGui updateSpellPowers(); } - void RaceDialog::onAccept(MyGUI::ListBox *_sender, size_t _index) + void RaceDialog::onAccept(MyGUI::ListBox* _sender, size_t _index) { onSelectRace(_sender, _index); - if(mRaceList->getIndexSelected() == MyGUI::ITEM_NONE) + if (mRaceList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } - void RaceDialog::getBodyParts (int part, std::vector& out) + void RaceDialog::getBodyParts(int part, std::vector& out) { out.clear(); - const MWWorld::Store &store = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); for (const ESM::BodyPart& bodypart : store) { @@ -300,13 +328,9 @@ namespace MWGui continue; if (mGenderIndex != (bodypart.mData.mFlags & ESM::BodyPart::BPF_Female)) continue; - bool firstPerson = (bodypart.mId.size() >= 3) - && bodypart.mId[bodypart.mId.size()-3] == '1' - && bodypart.mId[bodypart.mId.size()-2] == 's' - && bodypart.mId[bodypart.mId.size()-1] == 't'; - if (firstPerson) + if (ESM::isFirstPersonBodyPart(bodypart)) continue; - if (Misc::StringUtils::ciEqual(bodypart.mRace, mCurrentRaceId)) + if (bodypart.mRace == mCurrentRaceId) out.push_back(bodypart.mId); } } @@ -348,10 +372,9 @@ namespace MWGui { mRaceList->removeAllItems(); - const MWWorld::Store &races = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& races = MWBase::Environment::get().getESMStore()->get(); - std::vector > items; // ID, name + std::vector> items; // ID, name for (const ESM::Race& race : races) { bool playable = race.mData.mFlags & ESM::Race::Playable; @@ -366,7 +389,7 @@ namespace MWGui for (auto& item : items) { mRaceList->addItem(item.second, item.first); - if (Misc::StringUtils::ciEqual(item.first, mCurrentRaceId)) + if (item.first == mCurrentRaceId) mRaceList->setIndexSelected(index); ++index; } @@ -384,24 +407,21 @@ namespace MWGui return; Widgets::MWSkillPtr skillWidget; - const int lineHeight = 18; + const int lineHeight = Settings::gui().mFontSize + 2; MyGUI::IntCoord coord1(0, 0, mSkillList->getWidth(), 18); - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Race *race = store.get().find(mCurrentRaceId); - int count = sizeof(race->mData.mBonus)/sizeof(race->mData.mBonus[0]); // TODO: Find a portable macro for this ARRAYSIZE? - for (int i = 0; i < count; ++i) + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const ESM::Race* race = store.get().find(mCurrentRaceId); + for (const auto& bonus : race->mData.mBonus) { - int skillId = race->mData.mBonus[i].mSkill; - if (skillId < 0 || skillId > ESM::Skill::Length) // Skip unknown skill indexes + ESM::RefId skill = ESM::Skill::indexToRefId(bonus.mSkill); + if (skill.empty()) // Skip unknown skill indexes continue; - skillWidget = mSkillList->createWidget("MW_StatNameValue", coord1, MyGUI::Align::Default, - std::string("Skill") + MyGUI::utility::toString(i)); - skillWidget->setSkillNumber(skillId); - skillWidget->setSkillValue(Widgets::MWSkill::SkillValue(static_cast(race->mData.mBonus[i].mBonus))); - ToolTips::createSkillToolTip(skillWidget, skillId); - + skillWidget = mSkillList->createWidget("MW_StatNameValue", coord1, MyGUI::Align::Default); + skillWidget->setSkillId(skill); + skillWidget->setSkillValue(Widgets::MWSkill::SkillValue(static_cast(bonus.mBonus), 0.f)); + ToolTips::createSkillToolTip(skillWidget, skill); mSkillItems.push_back(skillWidget); @@ -420,19 +440,20 @@ namespace MWGui if (mCurrentRaceId.empty()) return; - const int lineHeight = 18; - MyGUI::IntCoord coord(0, 0, mSpellPowerList->getWidth(), 18); + const int lineHeight = Settings::gui().mFontSize + 2; + MyGUI::IntCoord coord(0, 0, mSpellPowerList->getWidth(), lineHeight); - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Race *race = store.get().find(mCurrentRaceId); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const ESM::Race* race = store.get().find(mCurrentRaceId); int i = 0; - for (const std::string& spellpower : race->mPowers.mList) + for (const ESM::RefId& spellpower : race->mPowers.mList) { - Widgets::MWSpellPtr spellPowerWidget = mSpellPowerList->createWidget("MW_StatName", coord, MyGUI::Align::Default, std::string("SpellPower") + MyGUI::utility::toString(i)); + Widgets::MWSpellPtr spellPowerWidget = mSpellPowerList->createWidget( + "MW_StatName", coord, MyGUI::Align::Default, std::string("SpellPower") + MyGUI::utility::toString(i)); spellPowerWidget->setSpellId(spellpower); spellPowerWidget->setUserString("ToolTipType", "Spell"); - spellPowerWidget->setUserString("Spell", spellpower); + spellPowerWidget->setUserString("Spell", spellpower.serialize()); mSpellPowerItems.push_back(spellPowerWidget); diff --git a/apps/openmw/mwgui/race.hpp b/apps/openmw/mwgui/race.hpp index 170c1dbce3a..a6ac0e28138 100644 --- a/apps/openmw/mwgui/race.hpp +++ b/apps/openmw/mwgui/race.hpp @@ -1,11 +1,9 @@ #ifndef MWGUI_RACE_H #define MWGUI_RACE_H -#include - #include "windowbase.hpp" -#include - +#include +#include namespace MWRender { @@ -40,11 +38,11 @@ namespace MWGui GM_Female }; - const ESM::NPC &getResult() const; - const std::string &getRaceId() const { return mCurrentRaceId; } + const ESM::NPC& getResult() const; + const ESM::RefId& getRaceId() const { return mCurrentRaceId; } Gender getGender() const { return mGenderIndex == 0 ? GM_Male : GM_Female; } - void setRaceId(const std::string &raceId); + void setRaceId(const ESM::RefId& raceId); void setGender(Gender gender) { mGenderIndex = gender == GM_Male ? 0 : 1; } void setNextButtonShow(bool shown); @@ -54,7 +52,7 @@ namespace MWGui bool exit() override { return false; } // Events - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n @@ -67,6 +65,7 @@ namespace MWGui EventHandle_WindowBase eventDone; protected: + void onPreviewScroll(MyGUI::Widget* _sender, int _delta); void onHeadRotate(MyGUI::ScrollBar* _sender, size_t _position); void onSelectPreviousGender(MyGUI::Widget* _sender); @@ -91,16 +90,16 @@ namespace MWGui void updatePreview(); void recountParts(); - void getBodyParts (int part, std::vector& out); + void getBodyParts(int part, std::vector& out); osg::Group* mParent; Resource::ResourceSystem* mResourceSystem; - std::vector mAvailableHeads; - std::vector mAvailableHairs; + std::vector mAvailableHeads; + std::vector mAvailableHairs; - MyGUI::ImageBox* mPreviewImage; - MyGUI::ListBox* mRaceList; + MyGUI::ImageBox* mPreviewImage; + MyGUI::ListBox* mRaceList; MyGUI::ScrollBar* mHeadRotate; MyGUI::Widget* mSkillList; @@ -111,7 +110,7 @@ namespace MWGui int mGenderIndex, mFaceIndex, mHairIndex; - std::string mCurrentRaceId; + ESM::RefId mCurrentRaceId; float mCurrentAngle; diff --git a/apps/openmw/mwgui/recharge.cpp b/apps/openmw/mwgui/recharge.cpp index df6962c78f8..7d57988d972 100644 --- a/apps/openmw/mwgui/recharge.cpp +++ b/apps/openmw/mwgui/recharge.cpp @@ -4,136 +4,136 @@ #include -#include +#include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/recharge.hpp" +#include "inventoryitemmodel.hpp" +#include "itemchargeview.hpp" #include "itemselection.hpp" #include "itemwidget.hpp" -#include "itemchargeview.hpp" #include "sortfilteritemmodel.hpp" -#include "inventoryitemmodel.hpp" namespace MWGui { -Recharge::Recharge() - : WindowBase("openmw_recharge_dialog.layout") - , mItemSelectionDialog(nullptr) -{ - getWidget(mBox, "Box"); - getWidget(mGemBox, "GemBox"); - getWidget(mGemIcon, "GemIcon"); - getWidget(mChargeLabel, "ChargeLabel"); - getWidget(mCancelButton, "CancelButton"); + Recharge::Recharge() + : WindowBase("openmw_recharge_dialog.layout") + { + getWidget(mBox, "Box"); + getWidget(mGemBox, "GemBox"); + getWidget(mGemIcon, "GemIcon"); + getWidget(mChargeLabel, "ChargeLabel"); + getWidget(mCancelButton, "CancelButton"); - mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &Recharge::onCancel); - mBox->eventItemClicked += MyGUI::newDelegate(this, &Recharge::onItemClicked); + mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &Recharge::onCancel); + mBox->eventItemClicked += MyGUI::newDelegate(this, &Recharge::onItemClicked); - mBox->setDisplayMode(ItemChargeView::DisplayMode_EnchantmentCharge); + mBox->setDisplayMode(ItemChargeView::DisplayMode_EnchantmentCharge); - mGemIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &Recharge::onSelectItem); -} + mGemIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &Recharge::onSelectItem); + } -void Recharge::onOpen() -{ - center(); + void Recharge::onOpen() + { + center(); - SortFilterItemModel * model = new SortFilterItemModel(new InventoryItemModel(MWMechanics::getPlayer())); - model->setFilter(SortFilterItemModel::Filter_OnlyRechargable); - mBox->setModel(model); + SortFilterItemModel* model + = new SortFilterItemModel(std::make_unique(MWMechanics::getPlayer())); + model->setFilter(SortFilterItemModel::Filter_OnlyRechargable); + mBox->setModel(model); - // Reset scrollbars - mBox->resetScrollbars(); -} + // Reset scrollbars + mBox->resetScrollbars(); + } -void Recharge::setPtr (const MWWorld::Ptr &item) -{ - mGemIcon->setItem(item); - mGemIcon->setUserString("ToolTipType", "ItemPtr"); - mGemIcon->setUserData(MWWorld::Ptr(item)); + void Recharge::setPtr(const MWWorld::Ptr& item) + { + if (item.isEmpty() || !item.getClass().isItem(item)) + throw std::runtime_error("Invalid argument in Recharge::setPtr"); - updateView(); -} + mGemIcon->setItem(item); + mGemIcon->setUserString("ToolTipType", "ItemPtr"); + mGemIcon->setUserData(MWWorld::Ptr(item)); -void Recharge::updateView() -{ - MWWorld::Ptr gem = *mGemIcon->getUserData(); + updateView(); + } - std::string soul = gem.getCellRef().getSoul(); - const ESM::Creature *creature = MWBase::Environment::get().getWorld()->getStore().get().find(soul); + void Recharge::updateView() + { + MWWorld::Ptr gem = *mGemIcon->getUserData(); - mChargeLabel->setCaptionWithReplacing("#{sCharges} " + MyGUI::utility::toString(creature->mData.mSoul)); + const ESM::RefId& soul = gem.getCellRef().getSoul(); + const ESM::Creature* creature = MWBase::Environment::get().getESMStore()->get().find(soul); - bool toolBoxVisible = (gem.getRefData().getCount() != 0); - mGemBox->setVisible(toolBoxVisible); - mGemBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); + mChargeLabel->setCaptionWithReplacing("#{sCharges} " + MyGUI::utility::toString(creature->mData.mSoul)); - if (!toolBoxVisible) - { - mGemIcon->setItem(MWWorld::Ptr()); - mGemIcon->clearUserStrings(); - } + bool toolBoxVisible = gem.getCellRef().getCount() != 0; + mGemBox->setVisible(toolBoxVisible); + mGemBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); - mBox->update(); + if (!toolBoxVisible) + { + mGemIcon->setItem(MWWorld::Ptr()); + mGemIcon->clearUserStrings(); + } - Gui::Box* box = dynamic_cast(mMainWidget); - if (box == nullptr) - throw std::runtime_error("main widget must be a box"); + mBox->update(); - box->notifyChildrenSizeChanged(); - center(); -} + Gui::Box* box = dynamic_cast(mMainWidget); + if (box == nullptr) + throw std::runtime_error("main widget must be a box"); -void Recharge::onCancel(MyGUI::Widget *sender) -{ - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Recharge); -} + box->notifyChildrenSizeChanged(); + center(); + } -void Recharge::onSelectItem(MyGUI::Widget *sender) -{ - delete mItemSelectionDialog; - mItemSelectionDialog = new ItemSelectionDialog("#{sSoulGemsWithSouls}"); - mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &Recharge::onItemSelected); - mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &Recharge::onItemCancel); - mItemSelectionDialog->setVisible(true); - mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); - mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyChargedSoulstones); -} + void Recharge::onCancel(MyGUI::Widget* sender) + { + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Recharge); + } -void Recharge::onItemSelected(MWWorld::Ptr item) -{ - mItemSelectionDialog->setVisible(false); + void Recharge::onSelectItem(MyGUI::Widget* sender) + { + mItemSelectionDialog = std::make_unique("#{sSoulGemsWithSouls}"); + mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &Recharge::onItemSelected); + mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &Recharge::onItemCancel); + mItemSelectionDialog->setVisible(true); + mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); + mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyChargedSoulstones); + } - mGemIcon->setItem(item); - mGemIcon->setUserString ("ToolTipType", "ItemPtr"); - mGemIcon->setUserData(item); + void Recharge::onItemSelected(MWWorld::Ptr item) + { + mItemSelectionDialog->setVisible(false); - MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); - updateView(); -} + mGemIcon->setItem(item); + mGemIcon->setUserString("ToolTipType", "ItemPtr"); + mGemIcon->setUserData(item); -void Recharge::onItemCancel() -{ - mItemSelectionDialog->setVisible(false); -} + MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); + updateView(); + } -void Recharge::onItemClicked(MyGUI::Widget *sender, const MWWorld::Ptr& item) -{ - MWWorld::Ptr gem = *mGemIcon->getUserData(); - if (!MWMechanics::rechargeItem(item, gem)) - return; + void Recharge::onItemCancel() + { + mItemSelectionDialog->setVisible(false); + } - updateView(); -} + void Recharge::onItemClicked(MyGUI::Widget* sender, const MWWorld::Ptr& item) + { + MWWorld::Ptr gem = *mGemIcon->getUserData(); + if (!MWMechanics::rechargeItem(item, gem)) + return; + + updateView(); + } } diff --git a/apps/openmw/mwgui/recharge.hpp b/apps/openmw/mwgui/recharge.hpp index c260b155470..f8a037d2dbd 100644 --- a/apps/openmw/mwgui/recharge.hpp +++ b/apps/openmw/mwgui/recharge.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_MWGUI_RECHARGE_H #define OPENMW_MWGUI_RECHARGE_H +#include + #include "windowbase.hpp" namespace MWWorld @@ -11,44 +13,45 @@ namespace MWWorld namespace MWGui { -class ItemSelectionDialog; -class ItemWidget; -class ItemChargeView; + class ItemSelectionDialog; + class ItemWidget; + class ItemChargeView; -class Recharge : public WindowBase -{ -public: - Recharge(); + class Recharge : public WindowBase + { + public: + Recharge(); - void onOpen() override; + void onOpen() override; - void setPtr (const MWWorld::Ptr& gem) override; + void setPtr(const MWWorld::Ptr& gem) override; -protected: - ItemChargeView* mBox; + std::string_view getWindowIdForLua() const override { return "Recharge"; } - MyGUI::Widget* mGemBox; + protected: + ItemChargeView* mBox; - ItemWidget* mGemIcon; + MyGUI::Widget* mGemBox; - ItemSelectionDialog* mItemSelectionDialog; + ItemWidget* mGemIcon; - MyGUI::TextBox* mChargeLabel; + std::unique_ptr mItemSelectionDialog; - MyGUI::Button* mCancelButton; + MyGUI::TextBox* mChargeLabel; - void updateView(); + MyGUI::Button* mCancelButton; - void onSelectItem(MyGUI::Widget* sender); + void updateView(); - void onItemSelected(MWWorld::Ptr item); - void onItemCancel(); + void onSelectItem(MyGUI::Widget* sender); - void onItemClicked (MyGUI::Widget* sender, const MWWorld::Ptr& item); - void onCancel (MyGUI::Widget* sender); - void onMouseWheel(MyGUI::Widget* _sender, int _rel); + void onItemSelected(MWWorld::Ptr item); + void onItemCancel(); -}; + void onItemClicked(MyGUI::Widget* sender, const MWWorld::Ptr& item); + void onCancel(MyGUI::Widget* sender); + void onMouseWheel(MyGUI::Widget* _sender, int _rel); + }; } diff --git a/apps/openmw/mwgui/referenceinterface.cpp b/apps/openmw/mwgui/referenceinterface.cpp index 83221c4f4b7..6dad6b85439 100644 --- a/apps/openmw/mwgui/referenceinterface.cpp +++ b/apps/openmw/mwgui/referenceinterface.cpp @@ -2,18 +2,14 @@ namespace MWGui { - ReferenceInterface::ReferenceInterface() - { - } + ReferenceInterface::ReferenceInterface() = default; - ReferenceInterface::~ReferenceInterface() - { - } + ReferenceInterface::~ReferenceInterface() = default; void ReferenceInterface::checkReferenceAvailable() { // check if count of the reference has become 0 - if (!mPtr.isEmpty() && mPtr.getRefData().getCount() == 0) + if (!mPtr.isEmpty() && mPtr.getCellRef().getCount() == 0) { mPtr = MWWorld::Ptr(); onReferenceUnavailable(); diff --git a/apps/openmw/mwgui/referenceinterface.hpp b/apps/openmw/mwgui/referenceinterface.hpp index 6428a5b5402..e8b60693e47 100644 --- a/apps/openmw/mwgui/referenceinterface.hpp +++ b/apps/openmw/mwgui/referenceinterface.hpp @@ -8,7 +8,8 @@ namespace MWGui /// \brief this class is intended for GUI interfaces that access an MW-Reference /// for example dialogue window accesses an NPC, or Container window accesses a Container /// these classes have to be automatically closed if the reference becomes unavailable - /// make sure that checkReferenceAvailable() is called every frame and that onReferenceUnavailable() has been overridden + /// make sure that checkReferenceAvailable() is called every frame and that onReferenceUnavailable() has been + /// overridden class ReferenceInterface { public: diff --git a/apps/openmw/mwgui/repair.cpp b/apps/openmw/mwgui/repair.cpp index 351b9760335..c1602b84076 100644 --- a/apps/openmw/mwgui/repair.cpp +++ b/apps/openmw/mwgui/repair.cpp @@ -4,150 +4,150 @@ #include +#include #include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" -#include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" +#include "inventoryitemmodel.hpp" +#include "itemchargeview.hpp" #include "itemselection.hpp" #include "itemwidget.hpp" -#include "itemchargeview.hpp" #include "sortfilteritemmodel.hpp" -#include "inventoryitemmodel.hpp" namespace MWGui { -Repair::Repair() - : WindowBase("openmw_repair.layout") - , mItemSelectionDialog(nullptr) -{ - getWidget(mRepairBox, "RepairBox"); - getWidget(mToolBox, "ToolBox"); - getWidget(mToolIcon, "ToolIcon"); - getWidget(mUsesLabel, "UsesLabel"); - getWidget(mQualityLabel, "QualityLabel"); - getWidget(mCancelButton, "CancelButton"); - - mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &Repair::onCancel); + Repair::Repair() + : WindowBase("openmw_repair.layout") + { + getWidget(mRepairBox, "RepairBox"); + getWidget(mToolBox, "ToolBox"); + getWidget(mToolIcon, "ToolIcon"); + getWidget(mUsesLabel, "UsesLabel"); + getWidget(mQualityLabel, "QualityLabel"); + getWidget(mCancelButton, "CancelButton"); - mRepairBox->eventItemClicked += MyGUI::newDelegate(this, &Repair::onRepairItem); - mRepairBox->setDisplayMode(ItemChargeView::DisplayMode_Health); + mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &Repair::onCancel); - mToolIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &Repair::onSelectItem); -} + mRepairBox->eventItemClicked += MyGUI::newDelegate(this, &Repair::onRepairItem); + mRepairBox->setDisplayMode(ItemChargeView::DisplayMode_Health); -void Repair::onOpen() -{ - center(); + mToolIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &Repair::onSelectItem); + } - SortFilterItemModel * model = new SortFilterItemModel(new InventoryItemModel(MWMechanics::getPlayer())); - model->setFilter(SortFilterItemModel::Filter_OnlyRepairable); - mRepairBox->setModel(model); + void Repair::onOpen() + { + center(); + + SortFilterItemModel* model + = new SortFilterItemModel(std::make_unique(MWMechanics::getPlayer())); + model->setFilter(SortFilterItemModel::Filter_OnlyRepairable); + mRepairBox->setModel(model); + mRepairBox->update(); + // Reset scrollbars + mRepairBox->resetScrollbars(); + } - // Reset scrollbars - mRepairBox->resetScrollbars(); -} + void Repair::setPtr(const MWWorld::Ptr& item) + { + if (item.isEmpty() || !item.getClass().isItem(item)) + throw std::runtime_error("Invalid argument in Repair::setPtr"); -void Repair::setPtr(const MWWorld::Ptr &item) -{ - MWBase::Environment::get().getWindowManager()->playSound("Item Repair Up"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Repair Up")); - mRepair.setTool(item); + mRepair.setTool(item); - mToolIcon->setItem(item); - mToolIcon->setUserString("ToolTipType", "ItemPtr"); - mToolIcon->setUserData(MWWorld::Ptr(item)); + mToolIcon->setItem(item); + mToolIcon->setUserString("ToolTipType", "ItemPtr"); + mToolIcon->setUserData(MWWorld::Ptr(item)); - updateRepairView(); -} + updateRepairView(); + } -void Repair::updateRepairView() -{ - MWWorld::LiveCellRef *ref = - mRepair.getTool().get(); + void Repair::updateRepairView() + { + MWWorld::LiveCellRef* ref = mRepair.getTool().get(); - int uses = mRepair.getTool().getClass().getItemHealth(mRepair.getTool()); + int uses = mRepair.getTool().getClass().getItemHealth(mRepair.getTool()); - float quality = ref->mBase->mData.mQuality; + float quality = ref->mBase->mData.mQuality; - mToolIcon->setUserData(mRepair.getTool()); + mToolIcon->setUserData(mRepair.getTool()); - std::stringstream qualityStr; - qualityStr << std::setprecision(3) << quality; + std::stringstream qualityStr; + qualityStr << std::setprecision(3) << quality; - mUsesLabel->setCaptionWithReplacing("#{sUses} " + MyGUI::utility::toString(uses)); - mQualityLabel->setCaptionWithReplacing("#{sQuality} " + qualityStr.str()); + mUsesLabel->setCaptionWithReplacing("#{sUses} " + MyGUI::utility::toString(uses)); + mQualityLabel->setCaptionWithReplacing("#{sQuality} " + qualityStr.str()); - bool toolBoxVisible = (mRepair.getTool().getRefData().getCount() != 0); - mToolBox->setVisible(toolBoxVisible); - mToolBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); + bool toolBoxVisible = (mRepair.getTool().getCellRef().getCount() != 0); + mToolBox->setVisible(toolBoxVisible); + mToolBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); - if (!toolBoxVisible) - { - mToolIcon->setItem(MWWorld::Ptr()); - mToolIcon->clearUserStrings(); - } + if (!toolBoxVisible) + { + mToolIcon->setItem(MWWorld::Ptr()); + mToolIcon->clearUserStrings(); + } - mRepairBox->update(); + mRepairBox->update(); - Gui::Box* box = dynamic_cast(mMainWidget); - if (box == nullptr) - throw std::runtime_error("main widget must be a box"); + Gui::Box* box = dynamic_cast(mMainWidget); + if (box == nullptr) + throw std::runtime_error("main widget must be a box"); - box->notifyChildrenSizeChanged(); - center(); -} + box->notifyChildrenSizeChanged(); + center(); + } -void Repair::onSelectItem(MyGUI::Widget *sender) -{ - delete mItemSelectionDialog; - mItemSelectionDialog = new ItemSelectionDialog("#{sRepair}"); - mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &Repair::onItemSelected); - mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &Repair::onItemCancel); - mItemSelectionDialog->setVisible(true); - mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); - mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyRepairTools); -} + void Repair::onSelectItem(MyGUI::Widget* sender) + { + mItemSelectionDialog = std::make_unique("#{sRepair}"); + mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &Repair::onItemSelected); + mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &Repair::onItemCancel); + mItemSelectionDialog->setVisible(true); + mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); + mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyRepairTools); + } -void Repair::onItemSelected(MWWorld::Ptr item) -{ - mItemSelectionDialog->setVisible(false); + void Repair::onItemSelected(MWWorld::Ptr item) + { + mItemSelectionDialog->setVisible(false); - mToolIcon->setItem(item); - mToolIcon->setUserString ("ToolTipType", "ItemPtr"); - mToolIcon->setUserData(item); + mToolIcon->setItem(item); + mToolIcon->setUserString("ToolTipType", "ItemPtr"); + mToolIcon->setUserData(item); - mRepair.setTool(item); + mRepair.setTool(item); - MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); - updateRepairView(); -} + MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); + updateRepairView(); + } -void Repair::onItemCancel() -{ - mItemSelectionDialog->setVisible(false); -} + void Repair::onItemCancel() + { + mItemSelectionDialog->setVisible(false); + } -void Repair::onCancel(MyGUI::Widget* /*sender*/) -{ - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Repair); -} + void Repair::onCancel(MyGUI::Widget* /*sender*/) + { + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Repair); + } -void Repair::onRepairItem(MyGUI::Widget* /*sender*/, const MWWorld::Ptr& ptr) -{ - if (!mRepair.getTool().getRefData().getCount()) - return; + void Repair::onRepairItem(MyGUI::Widget* /*sender*/, const MWWorld::Ptr& ptr) + { + if (!mRepair.getTool().getCellRef().getCount()) + return; - mRepair.repair(ptr); + mRepair.repair(ptr); - updateRepairView(); -} + updateRepairView(); + } } diff --git a/apps/openmw/mwgui/repair.hpp b/apps/openmw/mwgui/repair.hpp index 701009f5415..093a10e3fa4 100644 --- a/apps/openmw/mwgui/repair.hpp +++ b/apps/openmw/mwgui/repair.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_MWGUI_REPAIR_H #define OPENMW_MWGUI_REPAIR_H +#include + #include "windowbase.hpp" #include "../mwmechanics/repair.hpp" @@ -8,46 +10,47 @@ namespace MWGui { -class ItemSelectionDialog; -class ItemWidget; -class ItemChargeView; + class ItemSelectionDialog; + class ItemWidget; + class ItemChargeView; -class Repair : public WindowBase -{ -public: - Repair(); + class Repair : public WindowBase + { + public: + Repair(); - void onOpen() override; + void onOpen() override; - void setPtr (const MWWorld::Ptr& item) override; + void setPtr(const MWWorld::Ptr& item) override; -protected: - ItemChargeView* mRepairBox; + std::string_view getWindowIdForLua() const override { return "Repair"; } - MyGUI::Widget* mToolBox; + protected: + ItemChargeView* mRepairBox; - ItemWidget* mToolIcon; + MyGUI::Widget* mToolBox; - ItemSelectionDialog* mItemSelectionDialog; + ItemWidget* mToolIcon; - MyGUI::TextBox* mUsesLabel; - MyGUI::TextBox* mQualityLabel; + std::unique_ptr mItemSelectionDialog; - MyGUI::Button* mCancelButton; + MyGUI::TextBox* mUsesLabel; + MyGUI::TextBox* mQualityLabel; - MWMechanics::Repair mRepair; + MyGUI::Button* mCancelButton; - void updateRepairView(); + MWMechanics::Repair mRepair; - void onSelectItem(MyGUI::Widget* sender); + void updateRepairView(); - void onItemSelected(MWWorld::Ptr item); - void onItemCancel(); + void onSelectItem(MyGUI::Widget* sender); - void onRepairItem(MyGUI::Widget* sender, const MWWorld::Ptr& ptr); - void onCancel(MyGUI::Widget* sender); + void onItemSelected(MWWorld::Ptr item); + void onItemCancel(); -}; + void onRepairItem(MyGUI::Widget* sender, const MWWorld::Ptr& ptr); + void onCancel(MyGUI::Widget* sender); + }; } diff --git a/apps/openmw/mwgui/resourceskin.cpp b/apps/openmw/mwgui/resourceskin.cpp index 21ca2de54ab..3d9f09952e4 100644 --- a/apps/openmw/mwgui/resourceskin.cpp +++ b/apps/openmw/mwgui/resourceskin.cpp @@ -2,40 +2,39 @@ #include -#include +#include namespace MWGui { void resizeSkin(MyGUI::xml::ElementPtr _node) { _node->setAttribute("type", "ResourceSkin"); - const std::string size = _node->findAttribute("size"); - if (!size.empty()) + if (!_node->findAttribute("size").empty()) return; - const std::string textureName = _node->findAttribute("texture"); + auto textureName = _node->findAttribute("texture"); if (textureName.empty()) return; - MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture(textureName); + MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture(std::string{ textureName }); if (!texture) return; MyGUI::IntCoord coord(0, 0, texture->getWidth(), texture->getHeight()); MyGUI::xml::ElementEnumerator basis = _node->getElementEnumerator(); - const std::string textureSize = std::to_string(coord.width) + " " + std::to_string(coord.height); + const std::string textureSize = std::to_string(coord.width) + " " + std::to_string(coord.height); _node->addAttribute("size", textureSize); while (basis.next()) { if (basis->getName() != "BasisSkin") continue; - const std::string basisSkinType = basis->findAttribute("type"); + auto basisSkinType = basis->findAttribute("type"); if (Misc::StringUtils::ciEqual(basisSkinType, "SimpleText")) continue; + bool isTileRect = Misc::StringUtils::ciEqual(basisSkinType, "TileRect"); - const std::string offset = basis->findAttribute("offset"); - if (!offset.empty()) + if (!basis->findAttribute("offset").empty()) continue; basis->addAttribute("offset", coord); @@ -45,19 +44,17 @@ namespace MWGui { if (state->getName() == "State") { - const std::string stateOffset = state->findAttribute("offset"); - if (!stateOffset.empty()) + if (!state->findAttribute("offset").empty()) continue; state->addAttribute("offset", coord); - if (Misc::StringUtils::ciEqual(basisSkinType, "TileRect")) + if (isTileRect) { MyGUI::xml::ElementEnumerator property = state->getElementEnumerator(); bool hasTileSize = false; while (property.next("Property")) { - const std::string key = property->findAttribute("key"); - if (key != "TileSize") + if (property->findAttribute("key") != "TileSize") continue; hasTileSize = true; diff --git a/apps/openmw/mwgui/resourceskin.hpp b/apps/openmw/mwgui/resourceskin.hpp index fd1977e6603..1b66c22fb2d 100644 --- a/apps/openmw/mwgui/resourceskin.hpp +++ b/apps/openmw/mwgui/resourceskin.hpp @@ -7,7 +7,7 @@ namespace MWGui { class AutoSizedResourceSkin final : public MyGUI::ResourceSkin { - MYGUI_RTTI_DERIVED( AutoSizedResourceSkin ) + MYGUI_RTTI_DERIVED(AutoSizedResourceSkin) public: void deserialization(MyGUI::xml::ElementPtr _node, MyGUI::Version _version) override; diff --git a/apps/openmw/mwgui/review.cpp b/apps/openmw/mwgui/review.cpp index 101b6956d0c..ddce2c5f503 100644 --- a/apps/openmw/mwgui/review.cpp +++ b/apps/openmw/mwgui/review.cpp @@ -2,21 +2,28 @@ #include -#include -#include +#include #include +#include +#include +#include + +#include +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwworld/esmstore.hpp" +#include "../mwbase/world.hpp" #include "../mwmechanics/autocalcspell.hpp" +#include "../mwworld/esmstore.hpp" #include "tooltips.hpp" namespace { - void adjustButtonSize(MyGUI::Button *button) + void adjustButtonSize(MyGUI::Button* button) { // adjust size of button to fit its text MyGUI::IntSize size = button->getTextSize(); @@ -27,8 +34,8 @@ namespace namespace MWGui { ReviewDialog::ReviewDialog() - : WindowModal("openmw_chargen_review.layout"), - mUpdateSkillArea(false) + : WindowModal("openmw_chargen_review.layout") + , mUpdateSkillArea(false) { // Centre dialog center(); @@ -57,36 +64,45 @@ namespace MWGui // Setup dynamic stats getWidget(mHealth, "Health"); - mHealth->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sHealth", "")); + mHealth->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sHealth", {})); mHealth->setValue(45, 45); getWidget(mMagicka, "Magicka"); - mMagicka->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sMagic", "")); + mMagicka->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sMagic", {})); mMagicka->setValue(50, 50); getWidget(mFatigue, "Fatigue"); - mFatigue->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sFatigue", "")); + mFatigue->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sFatigue", {})); mFatigue->setValue(160, 160); // Setup attributes - Widgets::MWAttributePtr attribute; - for (int idx = 0; idx < ESM::Attribute::Length; ++idx) + MyGUI::Widget* attributes = getWidget("Attributes"); + const auto& store = MWBase::Environment::get().getWorld()->getStore().get(); + MyGUI::IntCoord coord{ 8, 4, 250, 18 }; + for (const ESM::Attribute& attribute : store) { - getWidget(attribute, std::string("Attribute") + MyGUI::utility::toString(idx)); - mAttributeWidgets.insert(std::make_pair(static_cast(ESM::Attribute::sAttributeIds[idx]), attribute)); - attribute->setAttributeId(ESM::Attribute::sAttributeIds[idx]); - attribute->setAttributeValue(Widgets::MWAttribute::AttributeValue()); + auto* widget + = attributes->createWidget("MW_StatNameValue", coord, MyGUI::Align::Default); + mAttributeWidgets.emplace(attribute.mId, widget); + widget->setUserString("ToolTipType", "Layout"); + widget->setUserString("ToolTipLayout", "AttributeToolTip"); + widget->setUserString("Caption_AttributeName", attribute.mName); + widget->setUserString("Caption_AttributeDescription", attribute.mDescription); + widget->setUserString("ImageTexture_AttributeImage", attribute.mIcon); + widget->setAttributeId(attribute.mId); + widget->setAttributeValue(Widgets::MWAttribute::AttributeValue()); + coord.top += coord.height; } // Setup skills getWidget(mSkillView, "SkillView"); mSkillView->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); - for (int i = 0; i < ESM::Skill::Length; ++i) + for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) { - mSkillValues.insert(std::make_pair(i, MWMechanics::SkillValue())); - mSkillWidgetMap.insert(std::make_pair(i, static_cast (nullptr))); + mSkillValues.emplace(skill.mId, MWMechanics::SkillValue()); + mSkillWidgetMap.emplace(skill.mId, static_cast(nullptr)); } MyGUI::Button* backButton; @@ -113,17 +129,16 @@ namespace MWGui } } - void ReviewDialog::setPlayerName(const std::string &name) + void ReviewDialog::setPlayerName(const std::string& name) { mNameWidget->setCaption(name); } - void ReviewDialog::setRace(const std::string &raceId) + void ReviewDialog::setRace(const ESM::RefId& raceId) { mRaceId = raceId; - const ESM::Race *race = - MWBase::Environment::get().getWorld()->getStore().get().search(mRaceId); + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().search(mRaceId); if (race) { ToolTips::createRaceToolTip(mRaceWidget, race); @@ -133,19 +148,19 @@ namespace MWGui mUpdateSkillArea = true; } - void ReviewDialog::setClass(const ESM::Class& class_) + void ReviewDialog::setClass(const ESM::Class& playerClass) { - mKlass = class_; - mClassWidget->setCaption(mKlass.mName); - ToolTips::createClassToolTip(mClassWidget, mKlass); + mClass = playerClass; + mClassWidget->setCaption(mClass.mName); + ToolTips::createClassToolTip(mClassWidget, mClass); } - void ReviewDialog::setBirthSign(const std::string& signId) + void ReviewDialog::setBirthSign(const ESM::RefId& signId) { mBirthSignId = signId; - const ESM::BirthSign *sign = - MWBase::Environment::get().getWorld()->getStore().get().search(mBirthSignId); + const ESM::BirthSign* sign + = MWBase::Environment::get().getESMStore()->get().search(mBirthSignId); if (sign) { mBirthSignWidget->setCaption(sign->mName); @@ -161,7 +176,7 @@ namespace MWGui int modified = static_cast(value.getModified()); mHealth->setValue(current, modified); - std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); + std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); mHealth->setUserString("Caption_HealthDescription", "#{sHealthDesc}\n" + valStr); } @@ -171,7 +186,7 @@ namespace MWGui int modified = static_cast(value.getModified()); mMagicka->setValue(current, modified); - std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); + std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); mMagicka->setUserString("Caption_HealthDescription", "#{sMagDesc}\n" + valStr); } @@ -181,13 +196,13 @@ namespace MWGui int modified = static_cast(value.getModified()); mFatigue->setValue(current, modified); - std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); + std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); mFatigue->setUserString("Caption_HealthDescription", "#{sFatDesc}\n" + valStr); } - void ReviewDialog::setAttribute(ESM::Attribute::AttributeID attributeId, const MWMechanics::AttributeValue& value) + void ReviewDialog::setAttribute(ESM::RefId attributeId, const MWMechanics::AttributeValue& value) { - std::map::iterator attr = mAttributeWidgets.find(static_cast(attributeId)); + auto attr = mAttributeWidgets.find(attributeId); if (attr == mAttributeWidgets.end()) return; @@ -198,13 +213,14 @@ namespace MWGui } } - void ReviewDialog::setSkillValue(ESM::Skill::SkillEnum skillId, const MWMechanics::SkillValue& value) + void ReviewDialog::setSkillValue(ESM::RefId id, const MWMechanics::SkillValue& value) { - mSkillValues[skillId] = value; - MyGUI::TextBox* widget = mSkillWidgetMap[skillId]; + mSkillValues[id] = value; + MyGUI::TextBox* widget = mSkillWidgetMap[id]; if (widget) { - float modified = static_cast(value.getModified()), base = static_cast(value.getBase()); + float modified = value.getModified(); + float base = value.getBase(); std::string text = MyGUI::utility::toString(std::floor(modified)); std::string state = "normal"; if (modified > base) @@ -219,28 +235,30 @@ namespace MWGui mUpdateSkillArea = true; } - void ReviewDialog::configureSkills(const std::vector& major, const std::vector& minor) + void ReviewDialog::configureSkills(const std::vector& major, const std::vector& minor) { mMajorSkills = major; mMinorSkills = minor; // Update misc skills with the remaining skills not in major or minor - std::set skillSet; + std::set skillSet; std::copy(major.begin(), major.end(), std::inserter(skillSet, skillSet.begin())); std::copy(minor.begin(), minor.end(), std::inserter(skillSet, skillSet.begin())); mMiscSkills.clear(); - for (const int skill : ESM::Skill::sSkillIds) + const auto& store = MWBase::Environment::get().getWorld()->getStore().get(); + for (const ESM::Skill& skill : store) { - if (skillSet.find(skill) == skillSet.end()) - mMiscSkills.push_back(skill); + if (!skillSet.contains(skill.mId)) + mMiscSkills.push_back(skill.mId); } mUpdateSkillArea = true; } - void ReviewDialog::addSeparator(MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) + void ReviewDialog::addSeparator(MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { - MyGUI::ImageBox* separator = mSkillView->createWidget("MW_HLine", MyGUI::IntCoord(10, coord1.top, coord1.width + coord2.width - 4, 18), MyGUI::Align::Default); + MyGUI::ImageBox* separator = mSkillView->createWidget( + "MW_HLine", MyGUI::IntCoord(10, coord1.top, coord1.width + coord2.width - 4, 18), MyGUI::Align::Default); separator->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); mSkillWidgets.push_back(separator); @@ -249,25 +267,27 @@ namespace MWGui coord2.top += separator->getHeight(); } - void ReviewDialog::addGroup(const std::string &label, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) + void ReviewDialog::addGroup(std::string_view label, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { - MyGUI::TextBox* groupWidget = mSkillView->createWidget("SandBrightText", MyGUI::IntCoord(0, coord1.top, coord1.width + coord2.width, coord1.height), MyGUI::Align::Default); + MyGUI::TextBox* groupWidget = mSkillView->createWidget("SandBrightText", + MyGUI::IntCoord(0, coord1.top, coord1.width + coord2.width, coord1.height), MyGUI::Align::Default); groupWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); - groupWidget->setCaption(label); + groupWidget->setCaption(MyGUI::UString(label)); mSkillWidgets.push_back(groupWidget); - int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; + const int lineHeight = Settings::gui().mFontSize + 2; coord1.top += lineHeight; coord2.top += lineHeight; } - MyGUI::TextBox* ReviewDialog::addValueItem(const std::string& text, const std::string &value, const std::string& state, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) + MyGUI::TextBox* ReviewDialog::addValueItem(std::string_view text, const std::string& value, + const std::string& state, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { MyGUI::TextBox* skillNameWidget; MyGUI::TextBox* skillValueWidget; skillNameWidget = mSkillView->createWidget("SandText", coord1, MyGUI::Align::Default); - skillNameWidget->setCaption(text); + skillNameWidget->setCaption(MyGUI::UString(text)); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); skillValueWidget = mSkillView->createWidget("SandTextRight", coord2, MyGUI::Align::Default); @@ -278,44 +298,47 @@ namespace MWGui mSkillWidgets.push_back(skillNameWidget); mSkillWidgets.push_back(skillValueWidget); - int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; + const int lineHeight = Settings::gui().mFontSize + 2; coord1.top += lineHeight; coord2.top += lineHeight; return skillValueWidget; } - void ReviewDialog::addItem(const std::string& text, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) + void ReviewDialog::addItem(const std::string& text, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { MyGUI::TextBox* skillNameWidget; - skillNameWidget = mSkillView->createWidget("SandText", coord1 + MyGUI::IntSize(coord2.width, 0), MyGUI::Align::Default); + skillNameWidget = mSkillView->createWidget( + "SandText", coord1 + MyGUI::IntSize(coord2.width, 0), MyGUI::Align::Default); skillNameWidget->setCaption(text); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); mSkillWidgets.push_back(skillNameWidget); - int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; + const int lineHeight = Settings::gui().mFontSize + 2; coord1.top += lineHeight; coord2.top += lineHeight; } void ReviewDialog::addItem(const ESM::Spell* spell, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { - Widgets::MWSpellPtr widget = mSkillView->createWidget("MW_StatName", coord1 + MyGUI::IntSize(coord2.width, 0), MyGUI::Align::Default); + Widgets::MWSpellPtr widget = mSkillView->createWidget( + "MW_StatName", coord1 + MyGUI::IntSize(coord2.width, 0), MyGUI::Align::Default); widget->setSpellId(spell->mId); widget->setUserString("ToolTipType", "Spell"); - widget->setUserString("Spell", spell->mId); + widget->setUserString("Spell", spell->mId.serialize()); widget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); mSkillWidgets.push_back(widget); - int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; + const int lineHeight = Settings::gui().mFontSize + 2; coord1.top += lineHeight; coord2.top += lineHeight; } - void ReviewDialog::addSkills(const SkillList &skills, const std::string &titleId, const std::string &titleDefault, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) + void ReviewDialog::addSkills(const std::vector& skills, const std::string& titleId, + const std::string& titleDefault, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { // Add a line separator if there are items above if (!mSkillWidgets.empty()) @@ -323,15 +346,24 @@ namespace MWGui addSeparator(coord1, coord2); } - addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString(titleId, titleDefault), coord1, coord2); + addGroup( + MWBase::Environment::get().getWindowManager()->getGameSettingString(titleId, titleDefault), coord1, coord2); - for (const int& skillId : skills) + for (const ESM::RefId& skillId : skills) { - if (skillId < 0 || skillId >= ESM::Skill::Length) // Skip unknown skill indexes + const ESM::Skill* skill = MWBase::Environment::get().getESMStore()->get().search(skillId); + if (!skill) // Skip unknown skills + continue; + + auto skillValue = mSkillValues.find(skill->mId); + if (skillValue == mSkillValues.end()) + { + Log(Debug::Error) << "Failed to update stats review window: can not find value for skill " + << skill->mId; continue; - assert(skillId >= 0 && skillId < ESM::Skill::Length); - const std::string &skillNameId = ESM::Skill::sSkillNameIds[skillId]; - const MWMechanics::SkillValue &stat = mSkillValues.find(skillId)->second; + } + + const MWMechanics::SkillValue& stat = skillValue->second; int base = stat.getBase(); int modified = stat.getModified(); @@ -340,14 +372,15 @@ namespace MWGui state = "increased"; else if (modified < base) state = "decreased"; - MyGUI::TextBox* widget = addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString(skillNameId, skillNameId), MyGUI::utility::toString(static_cast(modified)), state, coord1, coord2); + MyGUI::TextBox* widget = addValueItem( + skill->mName, MyGUI::utility::toString(static_cast(modified)), state, coord1, coord2); - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { - ToolTips::createSkillToolTip(mSkillWidgets[mSkillWidgets.size()-1-i], skillId); + ToolTips::createSkillToolTip(mSkillWidgets[mSkillWidgets.size() - 1 - i], skill->mId); } - mSkillWidgetMap[skillId] = widget; + mSkillWidgetMap[skill->mId] = widget; } } @@ -373,80 +406,78 @@ namespace MWGui addSkills(mMiscSkills, "sSkillClassMisc", "Misc Skills", coord1, coord2); // starting spells - std::vector spells; + std::vector spells; const ESM::Race* race = nullptr; if (!mRaceId.empty()) - race = MWBase::Environment::get().getWorld()->getStore().get().find(mRaceId); - - int skills[ESM::Skill::Length]; - for (int i=0; isecond.getBase(); + race = MWBase::Environment::get().getESMStore()->get().find(mRaceId); - int attributes[ESM::Attribute::Length]; - for (int i=0; igetAttributeValue().getBase(); + std::map attributes; + for (const auto& [key, value] : mAttributeWidgets) + attributes[key] = value->getAttributeValue(); - std::vector selectedSpells = MWMechanics::autoCalcPlayerSpells(skills, attributes, race); - for (std::string& spellId : selectedSpells) + std::vector selectedSpells = MWMechanics::autoCalcPlayerSpells(mSkillValues, attributes, race); + for (ESM::RefId& spellId : selectedSpells) { - std::string lower = Misc::StringUtils::lowerCase(spellId); - if (std::find(spells.begin(), spells.end(), lower) == spells.end()) - spells.push_back(lower); + if (std::find(spells.begin(), spells.end(), spellId) == spells.end()) + spells.push_back(spellId); } if (race) { - for (const std::string& spellId : race->mPowers.mList) + for (const ESM::RefId& spellId : race->mPowers.mList) { - std::string lower = Misc::StringUtils::lowerCase(spellId); - if (std::find(spells.begin(), spells.end(), lower) == spells.end()) - spells.push_back(lower); + if (std::find(spells.begin(), spells.end(), spellId) == spells.end()) + spells.push_back(spellId); } } if (!mBirthSignId.empty()) { - const ESM::BirthSign* sign = MWBase::Environment::get().getWorld()->getStore().get().find(mBirthSignId); - for (const std::string& spellId : sign->mPowers.mList) + const ESM::BirthSign* sign + = MWBase::Environment::get().getESMStore()->get().find(mBirthSignId); + for (const auto& spellId : sign->mPowers.mList) { - std::string lower = Misc::StringUtils::lowerCase(spellId); - if (std::find(spells.begin(), spells.end(), lower) == spells.end()) - spells.push_back(lower); + if (std::find(spells.begin(), spells.end(), spellId) == spells.end()) + spells.push_back(spellId); } } if (!mSkillWidgets.empty()) addSeparator(coord1, coord2); - addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sTypeAbility", "Abilities"), coord1, coord2); - for (std::string& spellId : spells) + addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sTypeAbility", "Abilities"), + coord1, coord2); + for (auto& spellId : spells) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(spellId); if (spell->mData.mType == ESM::Spell::ST_Ability) addItem(spell, coord1, coord2); } addSeparator(coord1, coord2); - addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sTypePower", "Powers"), coord1, coord2); - for (std::string& spellId : spells) + addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sTypePower", "Powers"), coord1, + coord2); + for (auto& spellId : spells) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(spellId); if (spell->mData.mType == ESM::Spell::ST_Power) addItem(spell, coord1, coord2); } addSeparator(coord1, coord2); - addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sTypeSpell", "Spells"), coord1, coord2); - for (std::string& spellId : spells) + addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sTypeSpell", "Spells"), coord1, + coord2); + for (auto& spellId : spells) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(spellId); if (spell->mData.mType == ESM::Spell::ST_Spell) addItem(spell, coord1, coord2); } - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden mSkillView->setVisibleVScroll(false); - mSkillView->setCanvasSize (mSkillView->getWidth(), std::max(mSkillView->getHeight(), coord1.top)); + mSkillView->setCanvasSize(mSkillView->getWidth(), std::max(mSkillView->getHeight(), coord1.top)); mSkillView->setVisibleVScroll(true); } @@ -484,10 +515,11 @@ namespace MWGui void ReviewDialog::onMouseWheel(MyGUI::Widget* _sender, int _rel) { - if (mSkillView->getViewOffset().top + _rel*0.3 > 0) + if (mSkillView->getViewOffset().top + _rel * 0.3 > 0) mSkillView->setViewOffset(MyGUI::IntPoint(0, 0)); else - mSkillView->setViewOffset(MyGUI::IntPoint(0, static_cast(mSkillView->getViewOffset().top + _rel*0.3))); + mSkillView->setViewOffset( + MyGUI::IntPoint(0, static_cast(mSkillView->getViewOffset().top + _rel * 0.3))); } } diff --git a/apps/openmw/mwgui/review.hpp b/apps/openmw/mwgui/review.hpp index cb847536d30..7226ad628dc 100644 --- a/apps/openmw/mwgui/review.hpp +++ b/apps/openmw/mwgui/review.hpp @@ -1,10 +1,10 @@ #ifndef MWGUI_REVIEW_H #define MWGUI_REVIEW_H -#include -#include -#include "windowbase.hpp" #include "widgets.hpp" +#include "windowbase.hpp" +#include +#include namespace ESM { @@ -16,39 +16,39 @@ namespace MWGui class ReviewDialog : public WindowModal { public: - enum Dialogs { + enum Dialogs + { NAME_DIALOG, RACE_DIALOG, CLASS_DIALOG, BIRTHSIGN_DIALOG }; - typedef std::vector SkillList; ReviewDialog(); bool exit() override { return false; } - void setPlayerName(const std::string &name); - void setRace(const std::string &raceId); - void setClass(const ESM::Class& class_); - void setBirthSign (const std::string &signId); + void setPlayerName(const std::string& name); + void setRace(const ESM::RefId& raceId); + void setClass(const ESM::Class& playerClass); + void setBirthSign(const ESM::RefId& signId); void setHealth(const MWMechanics::DynamicStat& value); void setMagicka(const MWMechanics::DynamicStat& value); void setFatigue(const MWMechanics::DynamicStat& value); - void setAttribute(ESM::Attribute::AttributeID attributeId, const MWMechanics::AttributeValue& value); + void setAttribute(ESM::RefId attributeId, const MWMechanics::AttributeValue& value); - void configureSkills(const SkillList& major, const SkillList& minor); - void setSkillValue(ESM::Skill::SkillEnum skillId, const MWMechanics::SkillValue& value); + void configureSkills(const std::vector& major, const std::vector& minor); + void setSkillValue(ESM::RefId id, const MWMechanics::SkillValue& value); void onOpen() override; void onFrame(float duration) override; // Events - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Int; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate EventHandle_Int; /** Event : Back button clicked.\n signature : void method()\n @@ -74,12 +74,14 @@ namespace MWGui void onMouseWheel(MyGUI::Widget* _sender, int _rel); private: - void addSkills(const SkillList &skills, const std::string &titleId, const std::string &titleDefault, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); - void addSeparator(MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); - void addGroup(const std::string &label, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); - MyGUI::TextBox* addValueItem(const std::string& text, const std::string &value, const std::string& state, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); - void addItem(const std::string& text, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); - void addItem(const ESM::Spell* spell, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); + void addSkills(const std::vector& skills, const std::string& titleId, + const std::string& titleDefault, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); + void addSeparator(MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); + void addGroup(std::string_view label, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); + MyGUI::TextBox* addValueItem(std::string_view text, const std::string& value, const std::string& state, + MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); + void addItem(const std::string& text, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); + void addItem(const ESM::Spell* spell, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); void updateSkillArea(); MyGUI::TextBox *mNameWidget, *mRaceWidget, *mClassWidget, *mBirthSignWidget; @@ -87,13 +89,14 @@ namespace MWGui Widgets::MWDynamicStatPtr mHealth, mMagicka, mFatigue; - std::map mAttributeWidgets; + std::map mAttributeWidgets; - SkillList mMajorSkills, mMinorSkills, mMiscSkills; - std::map mSkillValues; - std::map mSkillWidgetMap; - std::string mName, mRaceId, mBirthSignId; - ESM::Class mKlass; + std::vector mMajorSkills, mMinorSkills, mMiscSkills; + std::map mSkillValues; + std::map mSkillWidgetMap; + ESM::RefId mRaceId, mBirthSignId; + std::string mName; + ESM::Class mClass; std::vector mSkillWidgets; //< Skills and other information bool mUpdateSkillArea; diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index c4d608443cf..94f25e118b3 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -1,31 +1,32 @@ #include "savegamedialog.hpp" -#include #include +#include #include #include -#include #include #include +#include -#include #include +#include #include - -#include - -#include - -#include - +#include +#include #include +#include +#include +#include +#include +#include -#include "../mwbase/statemanager.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwstate/character.hpp" @@ -48,7 +49,6 @@ namespace MWGui getWidget(mDeleteButton, "DeleteButton"); getWidget(mSaveList, "SaveList"); getWidget(mSaveNameEdit, "SaveNameEdit"); - getWidget(mSpacer, "Spacer"); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onOkButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onCancelButtonClicked); mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteButtonClicked); @@ -65,7 +65,7 @@ namespace MWGui mDeleteButton->setNeedKeyFocus(false); } - void SaveGameDialog::onSlotActivated(MyGUI::ListBox *sender, size_t pos) + void SaveGameDialog::onSlotActivated(MyGUI::ListBox* sender, size_t pos) { onSlotSelected(sender, pos); accept(); @@ -82,7 +82,7 @@ namespace MWGui void SaveGameDialog::confirmDeleteSave() { ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); - dialog->askForConfirmation("#{sMessage3}"); + dialog->askForConfirmation("#{OMWEngine:DeleteGameConfirmation}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteSlotConfirmed); dialog->eventCancelClicked.clear(); @@ -91,7 +91,7 @@ namespace MWGui void SaveGameDialog::onDeleteSlotConfirmed() { - MWBase::Environment::get().getStateManager()->deleteGame (mCurrentCharacter, mCurrentSlot); + MWBase::Environment::get().getStateManager()->deleteGame(mCurrentCharacter, mCurrentSlot); mSaveList->removeItemAt(mSaveList->getIndexSelected()); onSlotSelected(mSaveList, mSaveList->getIndexSelected()); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); @@ -103,12 +103,12 @@ namespace MWGui mCharacterSelection->removeItemAt(previousIndex); if (mCharacterSelection->getItemCount()) { - size_t nextCharacter = std::min(previousIndex, mCharacterSelection->getItemCount()-1); + size_t nextCharacter = std::min(previousIndex, mCharacterSelection->getItemCount() - 1); mCharacterSelection->setIndexSelected(nextCharacter); onCharacterSelected(mCharacterSelection, nextCharacter); } else - fillSaveList(); + mCharacterSelection->setIndexSelected(MyGUI::ITEM_NONE); } } @@ -117,14 +117,14 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); } - void SaveGameDialog::onSaveNameChanged(MyGUI::EditBox *sender) + void SaveGameDialog::onSaveNameChanged(MyGUI::EditBox* sender) { // This might have previously been a save slot from the list. If so, that is no longer the case mSaveList->setIndexSelected(MyGUI::ITEM_NONE); onSlotSelected(mSaveList, MyGUI::ITEM_NONE); } - void SaveGameDialog::onEditSelectAccept(MyGUI::EditBox *sender) + void SaveGameDialog::onEditSelectAccept(MyGUI::EditBox* sender) { accept(); @@ -143,7 +143,7 @@ namespace MWGui { WindowModal::onOpen(); - mSaveNameEdit->setCaption (""); + mSaveNameEdit->setCaption({}); if (mSaving) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveNameEdit); else @@ -151,7 +151,7 @@ namespace MWGui center(); - mCharacterSelection->setCaption(""); + mCharacterSelection->setCaption({}); mCharacterSelection->removeAllItems(); mCurrentCharacter = nullptr; mCurrentSlot = nullptr; @@ -164,54 +164,56 @@ namespace MWGui mCurrentCharacter = mgr->getCurrentCharacter(); - std::string directory = - Misc::StringUtils::lowerCase (Settings::Manager::getString ("character", "Saves")); + const std::string& directory = Settings::saves().mCharacter; size_t selectedIndex = MyGUI::ITEM_NONE; for (MWBase::StateManager::CharacterIterator it = mgr->characterBegin(); it != mgr->characterEnd(); ++it) { - if (it->begin()!=it->end()) + if (it->begin() != it->end()) { + const ESM::SavedGame& signature = it->getSignature(); + std::stringstream title; - title << it->getSignature().mPlayerName; + title << signature.mPlayerName; // For a custom class, we will not find it in the store (unless we loaded the savegame first). // Fall back to name stored in savegame header in that case. - std::string className; - if (it->getSignature().mPlayerClassId.empty()) - className = it->getSignature().mPlayerClassName; + std::string_view className; + if (signature.mPlayerClassId.empty()) + className = signature.mPlayerClassName; else { // Find the localised name for this class from the store - const ESM::Class* class_ = MWBase::Environment::get().getWorld()->getStore().get().search( - it->getSignature().mPlayerClassId); + const ESM::Class* class_ + = MWBase::Environment::get().getESMStore()->get().search(signature.mPlayerClassId); if (class_) className = class_->mName; else className = "?"; // From an older savegame format that did not support custom classes properly. } - title << " (#{sLevel} " << it->getSignature().mPlayerLevel << " " << MyGUI::TextIterator::toTagsString(className) << ")"; + title << " (#{OMWEngine:Level} " << signature.mPlayerLevel << " " + << MyGUI::TextIterator::toTagsString(MyGUI::UString(className)) << ")"; - mCharacterSelection->addItem (MyGUI::LanguageManager::getInstance().replaceTags(title.str())); + mCharacterSelection->addItem(MyGUI::LanguageManager::getInstance().replaceTags(title.str())); - if (mCurrentCharacter == &*it || - (!mCurrentCharacter && !mSaving && directory==Misc::StringUtils::lowerCase ( - it->begin()->mPath.parent_path().filename().string()))) + if (mCurrentCharacter == &*it + || (!mCurrentCharacter && !mSaving + && Misc::StringUtils::ciEqual( + directory, Files::pathToUnicodeString(it->begin()->mPath.parent_path().filename())))) { mCurrentCharacter = &*it; - selectedIndex = mCharacterSelection->getItemCount()-1; + selectedIndex = mCharacterSelection->getItemCount() - 1; } } } mCharacterSelection->setIndexSelected(selectedIndex); if (selectedIndex == MyGUI::ITEM_NONE) - mCharacterSelection->setCaption("Select Character ..."); + mCharacterSelection->setCaptionWithReplacing("#{OMWEngine:SelectCharacter}"); fillSaveList(); - } void SaveGameDialog::setLoadOrSave(bool load) @@ -220,7 +222,6 @@ namespace MWGui mSaveNameEdit->setVisible(!load); mCharacterSelection->setUserString("Hidden", load ? "false" : "true"); mCharacterSelection->setVisible(load); - mSpacer->setUserString("Hidden", load ? "false" : "true"); mDeleteButton->setUserString("Hidden", load ? "false" : "true"); mDeleteButton->setVisible(load); @@ -233,12 +234,12 @@ namespace MWGui center(); } - void SaveGameDialog::onCancelButtonClicked(MyGUI::Widget *sender) + void SaveGameDialog::onCancelButtonClicked(MyGUI::Widget* sender) { setVisible(false); } - void SaveGameDialog::onDeleteButtonClicked(MyGUI::Widget *sender) + void SaveGameDialog::onDeleteButtonClicked(MyGUI::Widget* sender) { if (mCurrentSlot) confirmDeleteSave(); @@ -262,7 +263,7 @@ namespace MWGui if (mCurrentSlot != nullptr && !reallySure) { ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); - dialog->askForConfirmation("#{sMessage4}"); + dialog->askForConfirmation("#{OMWEngine:OverwriteGameConfirmation}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SaveGameDialog::onConfirmationGiven); dialog->eventCancelClicked.clear(); @@ -271,7 +272,7 @@ namespace MWGui } if (mSaveNameEdit->getCaption().empty()) { - MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage65}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{OMWEngine:EmptySaveNameError}"); return; } } @@ -283,7 +284,7 @@ namespace MWGui if (state == MWBase::StateManager::State_Running && !reallySure) { ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); - dialog->askForConfirmation("#{sMessage1}"); + dialog->askForConfirmation("#{OMWEngine:LoadGameConfirmation}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SaveGameDialog::onConfirmationGiven); dialog->eventCancelClicked.clear(); @@ -293,16 +294,16 @@ namespace MWGui } setVisible(false); - MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_MainMenu); + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_MainMenu); if (mSaving) { - MWBase::Environment::get().getStateManager()->saveGame (mSaveNameEdit->getCaption(), mCurrentSlot); + MWBase::Environment::get().getStateManager()->saveGame(mSaveNameEdit->getCaption(), mCurrentSlot); } else { - assert (mCurrentCharacter && mCurrentSlot); - MWBase::Environment::get().getStateManager()->loadGame (mCurrentCharacter, mCurrentSlot->mPath.string()); + assert(mCurrentCharacter && mCurrentSlot); + MWBase::Environment::get().getStateManager()->loadGame(mCurrentCharacter, mCurrentSlot->mPath); } } @@ -312,16 +313,16 @@ namespace MWGui confirmDeleteSave(); } - void SaveGameDialog::onOkButtonClicked(MyGUI::Widget *sender) + void SaveGameDialog::onOkButtonClicked(MyGUI::Widget* sender) { accept(); } - void SaveGameDialog::onCharacterSelected(MyGUI::ComboBox *sender, size_t pos) + void SaveGameDialog::onCharacterSelected(MyGUI::ComboBox* sender, size_t pos) { MWBase::StateManager* mgr = MWBase::Environment::get().getStateManager(); - unsigned int i=0; + unsigned int i = 0; const MWState::Character* character = nullptr; for (MWBase::StateManager::CharacterIterator it = mgr->characterBegin(); it != mgr->characterEnd(); ++it, ++i) { @@ -362,21 +363,26 @@ namespace MWGui std::string formatTimeplayed(const double timeInSeconds) { - int timePlayed = (int)floor(timeInSeconds); - int days = timePlayed / 60 / 60 / 24; - int hours = (timePlayed / 60 / 60) % 24; - int minutes = (timePlayed / 60) % 60; - int seconds = timePlayed % 60; - - std::stringstream stream; - stream << std::setfill('0') << std::setw(2) << days << ":"; - stream << std::setfill('0') << std::setw(2) << hours << ":"; - stream << std::setfill('0') << std::setw(2) << minutes << ":"; - stream << std::setfill('0') << std::setw(2) << seconds; - return stream.str(); + auto l10n = MWBase::Environment::get().getL10nManager()->getContext("Interface"); + int duration = static_cast(timeInSeconds); + if (duration <= 0) + return l10n->formatMessage("DurationSecond", { "seconds" }, { 0 }); + + std::string result; + int hours = duration / 3600; + int minutes = (duration / 60) % 60; + int seconds = duration % 60; + if (hours) + result += l10n->formatMessage("DurationHour", { "hours" }, { hours }); + if (minutes) + result += l10n->formatMessage("DurationMinute", { "minutes" }, { minutes }); + if (seconds) + result += l10n->formatMessage("DurationSecond", { "seconds" }, { seconds }); + + return result; } - void SaveGameDialog::onSlotSelected(MyGUI::ListBox *sender, size_t pos) + void SaveGameDialog::onSlotSelected(MyGUI::ListBox* sender, size_t pos) { mOkButton->setEnabled(pos != MyGUI::ITEM_NONE || mSaving); mDeleteButton->setEnabled(pos != MyGUI::ITEM_NONE); @@ -384,8 +390,8 @@ namespace MWGui if (pos == MyGUI::ITEM_NONE || !mCurrentCharacter) { mCurrentSlot = nullptr; - mInfoText->setCaption(""); - mScreenshot->setImageTexture(""); + mInfoText->setCaption({}); + mScreenshot->setImageTexture({}); return; } @@ -393,8 +399,9 @@ namespace MWGui mSaveNameEdit->setCaption(sender->getItemNameAt(pos)); mCurrentSlot = nullptr; - unsigned int i=0; - for (MWState::Character::SlotIterator it = mCurrentCharacter->begin(); it != mCurrentCharacter->end(); ++it, ++i) + unsigned int i = 0; + for (MWState::Character::SlotIterator it = mCurrentCharacter->begin(); it != mCurrentCharacter->end(); + ++it, ++i) { if (i == pos) mCurrentSlot = &*it; @@ -403,53 +410,73 @@ namespace MWGui throw std::runtime_error("Can't find selected slot"); std::stringstream text; - time_t time = mCurrentSlot->mTimeStamp; - struct tm* timeinfo; - timeinfo = localtime(&time); - text << std::put_time(timeinfo, "%Y.%m.%d %T") << "\n"; + text << Misc::fileTimeToString(mCurrentSlot->mTimeStamp, "%Y.%m.%d %T") << "\n"; - text << "#{sLevel} " << mCurrentSlot->mProfile.mPlayerLevel << "\n"; - text << "#{sCell=" << mCurrentSlot->mProfile.mPlayerCell << "}\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) hour -= 12; - if (hour == 0) hour = 12; + if (hour >= 13) + hour -= 12; + 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()->getMonthName(mCurrentSlot->mProfile.mInGameTime.mMonth) - << " " << hour << " " << (pm ? "#{sSaveMenuHelp05}" : "#{sSaveMenuHelp04}"); + text << mCurrentSlot->mProfile.mInGameTime.mDay << " " + << MWBase::Environment::get().getWorld()->getTimeManager()->getMonthName( + mCurrentSlot->mProfile.mInGameTime.mMonth) + << " " << hour << " " << (pm ? "#{Calendar:pm}" : "#{Calendar:am}"); - if (Settings::Manager::getBool("timeplayed","Saves")) + if (mCurrentSlot->mProfile.mTimePlayed > 0) { - text << "\n" << "Time played: " << formatTimeplayed(mCurrentSlot->mProfile.mTimePlayed); + text << "\n" + << "#{OMWEngine:TimePlayed}: " << formatTimeplayed(mCurrentSlot->mProfile.mTimePlayed); } mInfoText->setCaptionWithReplacing(text.str()); + // Reset the image for the case we're unable to recover a screenshot + mScreenshotTexture.reset(); + mScreenshot->setRenderItemTexture(nullptr); + mScreenshot->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); // Decode screenshot const std::vector& data = mCurrentSlot->mProfile.mScreenshot; - Files::IMemStream instream (&data[0], data.size()); + if (!data.size()) + { + Log(Debug::Warning) << "Selected save file '" << Files::pathToUnicodeString(mCurrentSlot->mPath.filename()) + << "' has no savegame screenshot"; + return; + } + + Files::IMemStream instream(data.data(), data.size()); osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("jpg"); if (!readerwriter) { - Log(Debug::Error) << "Error: Can't open savegame screenshot, no jpg readerwriter found"; + Log(Debug::Error) << "Can't open savegame screenshot, no jpg readerwriter found"; return; } osgDB::ReaderWriter::ReadResult result = readerwriter->readImage(instream); if (!result.success()) { - Log(Debug::Error) << "Error: Failed to read savegame screenshot: " << result.message() << " code " << result.status(); + Log(Debug::Error) << "Failed to read savegame screenshot: " << result.message() << " code " + << result.status(); return; } - osg::ref_ptr texture (new osg::Texture2D); + osg::ref_ptr texture(new osg::Texture2D); texture->setImage(result.getImage()); + texture->setInternalFormat(GL_RGB); texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); @@ -457,9 +484,7 @@ namespace MWGui texture->setResizeNonPowerOfTwoHint(false); texture->setUnRefImageDataAfterApply(true); - mScreenshotTexture.reset(new osgMyGUI::OSGTexture(texture)); - + mScreenshotTexture = std::make_unique(texture); mScreenshot->setRenderItemTexture(mScreenshotTexture.get()); - mScreenshot->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); } } diff --git a/apps/openmw/mwgui/savegamedialog.hpp b/apps/openmw/mwgui/savegamedialog.hpp index c22d86fd11e..35e65fbed0b 100644 --- a/apps/openmw/mwgui/savegamedialog.hpp +++ b/apps/openmw/mwgui/savegamedialog.hpp @@ -28,27 +28,27 @@ namespace MWGui void confirmDeleteSave(); void onKeyButtonPressed(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char character); - void onCancelButtonClicked (MyGUI::Widget* sender); - void onOkButtonClicked (MyGUI::Widget* sender); - void onDeleteButtonClicked (MyGUI::Widget* sender); - void onCharacterSelected (MyGUI::ComboBox* sender, size_t pos); + void onCancelButtonClicked(MyGUI::Widget* sender); + void onOkButtonClicked(MyGUI::Widget* sender); + void onDeleteButtonClicked(MyGUI::Widget* sender); + void onCharacterSelected(MyGUI::ComboBox* sender, size_t pos); void onCharacterAccept(MyGUI::ComboBox* sender, size_t pos); // Slot selected (mouse click or arrow keys) - void onSlotSelected (MyGUI::ListBox* sender, size_t pos); + void onSlotSelected(MyGUI::ListBox* sender, size_t pos); // Slot activated (double click or enter key) - void onSlotActivated (MyGUI::ListBox* sender, size_t pos); + void onSlotActivated(MyGUI::ListBox* sender, size_t pos); // Slot clicked with mouse void onSlotMouseClick(MyGUI::ListBox* sender, size_t pos); void onDeleteSlotConfirmed(); void onDeleteSlotCancel(); - void onEditSelectAccept (MyGUI::EditBox* sender); - void onSaveNameChanged (MyGUI::EditBox* sender); + void onEditSelectAccept(MyGUI::EditBox* sender); + void onSaveNameChanged(MyGUI::EditBox* sender); void onConfirmationGiven(); void onConfirmationCancel(); - void accept(bool reallySure=false); + void accept(bool reallySure = false); void fillSaveList(); @@ -63,11 +63,9 @@ namespace MWGui MyGUI::Button* mDeleteButton; MyGUI::ListBox* mSaveList; MyGUI::EditBox* mSaveNameEdit; - MyGUI::Widget* mSpacer; const MWState::Character* mCurrentCharacter; const MWState::Slot* mCurrentSlot; - }; } diff --git a/apps/openmw/mwgui/screenfader.cpp b/apps/openmw/mwgui/screenfader.cpp index 619852a22b7..0068ba79603 100644 --- a/apps/openmw/mwgui/screenfader.cpp +++ b/apps/openmw/mwgui/screenfader.cpp @@ -1,20 +1,19 @@ #include "screenfader.hpp" -#include -#include #include +#include namespace MWGui { - FadeOp::FadeOp(ScreenFader * fader, float time, float targetAlpha, float delay) - : mFader(fader), - mRemainingTime(time+delay), - mTargetTime(time), - mTargetAlpha(targetAlpha), - mStartAlpha(0.f), - mDelay(delay), - mRunning(false) + FadeOp::FadeOp(ScreenFader* fader, float time, float targetAlpha, float delay) + : mFader(fader) + , mRemainingTime(time + delay) + , mTargetTime(time) + , mTargetAlpha(targetAlpha) + , mStartAlpha(0.f) + , mDelay(delay) + , mRunning(false) { } @@ -61,13 +60,13 @@ namespace MWGui float currentAlpha = mFader->getCurrentAlpha(); if (mStartAlpha > mTargetAlpha) { - currentAlpha -= dt/mTargetTime * (mStartAlpha-mTargetAlpha); + currentAlpha -= dt / mTargetTime * (mStartAlpha - mTargetAlpha); if (currentAlpha < mTargetAlpha) currentAlpha = mTargetAlpha; } else { - currentAlpha += dt/mTargetTime * (mTargetAlpha-mStartAlpha); + currentAlpha += dt / mTargetTime * (mTargetAlpha - mStartAlpha); if (currentAlpha > mTargetAlpha) currentAlpha = mTargetAlpha; } @@ -83,7 +82,8 @@ namespace MWGui mFader->notifyOperationFinished(); } - ScreenFader::ScreenFader(const std::string & texturePath, const std::string& layout, const MyGUI::FloatCoord& texCoordOverride) + ScreenFader::ScreenFader( + const std::string& texturePath, const std::string& layout, const MyGUI::FloatCoord& texCoordOverride) : WindowBase(layout) , mCurrentAlpha(0.f) , mFactor(1.f) @@ -91,17 +91,15 @@ namespace MWGui { MyGUI::Gui::getInstance().eventFrameStart += MyGUI::newDelegate(this, &ScreenFader::onFrameStart); - mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); - MyGUI::ImageBox* imageBox = mMainWidget->castType(false); if (imageBox) { imageBox->setImageTexture(texturePath); const MyGUI::IntSize imageSize = imageBox->getImageSize(); - imageBox->setImageCoord(MyGUI::IntCoord(texCoordOverride.left * imageSize.width, - texCoordOverride.top * imageSize.height, - texCoordOverride.width * imageSize.width, - texCoordOverride.height * imageSize.height)); + imageBox->setImageCoord(MyGUI::IntCoord(static_cast(texCoordOverride.left * imageSize.width), + static_cast(texCoordOverride.top * imageSize.height), + static_cast(texCoordOverride.width * imageSize.width), + static_cast(texCoordOverride.height * imageSize.height))); } } @@ -111,7 +109,7 @@ namespace MWGui { MyGUI::Gui::getInstance().eventFrameStart -= MyGUI::newDelegate(this, &ScreenFader::onFrameStart); } - catch(const MyGUI::Exception& e) + catch (const MyGUI::Exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } @@ -130,7 +128,7 @@ namespace MWGui void ScreenFader::applyAlpha() { setVisible(true); - mMainWidget->setAlpha(1.f-((1.f-mCurrentAlpha) * mFactor)); + mMainWidget->setAlpha(1.f - ((1.f - mCurrentAlpha) * mFactor)); } void ScreenFader::fadeIn(float time, float delay) @@ -145,7 +143,7 @@ namespace MWGui void ScreenFader::fadeTo(const int percent, const float time, float delay) { - queue(time, percent/100.f, delay); + queue(time, percent / 100.f, delay); } void ScreenFader::clear() @@ -197,7 +195,7 @@ namespace MWGui mCurrentAlpha = alpha; - if (1.f-((1.f-mCurrentAlpha) * mFactor) == 0.f) + if (1.f - ((1.f - mCurrentAlpha) * mFactor) == 0.f) mMainWidget->setVisible(false); else applyAlpha(); diff --git a/apps/openmw/mwgui/screenfader.hpp b/apps/openmw/mwgui/screenfader.hpp index 8eb8a68591b..4ef1ae2f43f 100644 --- a/apps/openmw/mwgui/screenfader.hpp +++ b/apps/openmw/mwgui/screenfader.hpp @@ -15,7 +15,7 @@ namespace MWGui public: typedef std::shared_ptr Ptr; - FadeOp(ScreenFader * fader, float time, float targetAlpha, float delay); + FadeOp(ScreenFader* fader, float time, float targetAlpha, float delay); bool isRunning(); @@ -24,7 +24,7 @@ namespace MWGui void finish(); private: - ScreenFader * mFader; + ScreenFader* mFader; float mRemainingTime; float mTargetTime; float mTargetAlpha; @@ -36,18 +36,19 @@ namespace MWGui class ScreenFader : public WindowBase { public: - ScreenFader(const std::string & texturePath, const std::string& layout = "openmw_screen_fader.layout", const MyGUI::FloatCoord& texCoordOverride = MyGUI::FloatCoord(0,0,1,1)); + ScreenFader(const std::string& texturePath, const std::string& layout = "openmw_screen_fader.layout", + const MyGUI::FloatCoord& texCoordOverride = MyGUI::FloatCoord(0, 0, 1, 1)); ~ScreenFader(); void onFrameStart(float dt); - void fadeIn(const float time, float delay=0); - void fadeOut(const float time, float delay=0); - void fadeTo(const int percent, const float time, float delay=0); + void fadeIn(const float time, float delay = 0); + void fadeOut(const float time, float delay = 0); + void fadeTo(const int percent, const float time, float delay = 0); void clear() override; - void setFactor (float factor); + void setFactor(float factor); void setRepeat(bool repeat); void queue(float time, float targetAlpha, float delay); diff --git a/apps/openmw/mwgui/scrollwindow.cpp b/apps/openmw/mwgui/scrollwindow.cpp index f2c967da4e3..0b1658fd84a 100644 --- a/apps/openmw/mwgui/scrollwindow.cpp +++ b/apps/openmw/mwgui/scrollwindow.cpp @@ -2,11 +2,11 @@ #include -#include +#include +#include #include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" @@ -19,7 +19,7 @@ namespace MWGui { - ScrollWindow::ScrollWindow () + ScrollWindow::ScrollWindow() : BookWindowBase("openmw_scroll.layout") , mTakeButtonShow(true) , mTakeButtonAllowed(true) @@ -41,20 +41,28 @@ namespace MWGui center(); } - void ScrollWindow::setPtr (const MWWorld::Ptr& scroll) + void ScrollWindow::setPtr(const MWWorld::Ptr& scroll) { + if (scroll.isEmpty() || (scroll.getType() != ESM::REC_BOOK && scroll.getType() != ESM::REC_BOOK4)) + throw std::runtime_error("Invalid argument in ScrollWindow::setPtr"); mScroll = scroll; MWWorld::Ptr player = MWMechanics::getPlayer(); bool showTakeButton = scroll.getContainerStore() != &player.getClass().getContainerStore(player); - MWWorld::LiveCellRef *ref = mScroll.get(); + const std::string* text; + if (scroll.getType() == ESM::REC_BOOK) + text = &scroll.get()->mBase->mText; + else + text = &scroll.get()->mBase->mText; + bool shrinkTextAtLastTag = scroll.getType() == ESM::REC_BOOK; Formatting::BookFormatter formatter; - formatter.markupToWidget(mTextView, ref->mBase->mText, 390, mTextView->getHeight()); + formatter.markupToWidget(mTextView, *text, 390, mTextView->getHeight(), shrinkTextAtLastTag); MyGUI::IntSize size = mTextView->getChildAt(0)->getSize(); - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden mTextView->setVisibleVScroll(false); if (size.height > mTextView->getSize().height) mTextView->setCanvasSize(mTextView->getWidth(), size.height); @@ -62,14 +70,14 @@ namespace MWGui mTextView->setCanvasSize(mTextView->getWidth(), mTextView->getSize().height); mTextView->setVisibleVScroll(true); - mTextView->setViewOffset(MyGUI::IntPoint(0,0)); + mTextView->setViewOffset(MyGUI::IntPoint(0, 0)); setTakeButtonShow(showTakeButton); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); } - void ScrollWindow::onKeyButtonPressed(MyGUI::Widget *sender, MyGUI::KeyCode key, MyGUI::Char character) + void ScrollWindow::onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character) { int scroll = 0; if (key == MyGUI::KeyCode::ArrowUp) @@ -93,18 +101,18 @@ namespace MWGui mTakeButton->setVisible(mTakeButtonShow && mTakeButtonAllowed); } - void ScrollWindow::onCloseButtonClicked (MyGUI::Widget* _sender) + void ScrollWindow::onCloseButtonClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Scroll); } - void ScrollWindow::onTakeButtonClicked (MyGUI::Widget* _sender) + void ScrollWindow::onTakeButtonClicked(MyGUI::Widget* _sender) { - MWBase::Environment::get().getWindowManager()->playSound("Item Book Up"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Book Up")); MWWorld::ActionTake take(mScroll); - take.execute (MWMechanics::getPlayer()); + take.execute(MWMechanics::getPlayer()); - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Scroll, true); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Scroll); } } diff --git a/apps/openmw/mwgui/scrollwindow.hpp b/apps/openmw/mwgui/scrollwindow.hpp index 8a1e323ab14..7daea988949 100644 --- a/apps/openmw/mwgui/scrollwindow.hpp +++ b/apps/openmw/mwgui/scrollwindow.hpp @@ -14,30 +14,31 @@ namespace MWGui { class ScrollWindow : public BookWindowBase { - public: - ScrollWindow (); + public: + ScrollWindow(); - void setPtr (const MWWorld::Ptr& scroll) override; - void setInventoryAllowed(bool allowed); + void setPtr(const MWWorld::Ptr& scroll) override; + void setInventoryAllowed(bool allowed); - void onResChange(int, int) override { center(); } + void onResChange(int, int) override { center(); } - protected: - void onCloseButtonClicked (MyGUI::Widget* _sender); - void onTakeButtonClicked (MyGUI::Widget* _sender); - void setTakeButtonShow(bool show); - void onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character); + std::string_view getWindowIdForLua() const override { return "Scroll"; } - private: - Gui::ImageButton* mCloseButton; - Gui::ImageButton* mTakeButton; - MyGUI::ScrollView* mTextView; + protected: + void onCloseButtonClicked(MyGUI::Widget* _sender); + void onTakeButtonClicked(MyGUI::Widget* _sender); + void setTakeButtonShow(bool show); + void onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character); - MWWorld::Ptr mScroll; + private: + Gui::ImageButton* mCloseButton; + Gui::ImageButton* mTakeButton; + MyGUI::ScrollView* mTextView; - bool mTakeButtonShow; - bool mTakeButtonAllowed; + MWWorld::Ptr mScroll; + bool mTakeButtonShow; + bool mTakeButtonAllowed; }; } diff --git a/apps/openmw/mwgui/settings.cpp b/apps/openmw/mwgui/settings.cpp new file mode 100644 index 00000000000..5005876d553 --- /dev/null +++ b/apps/openmw/mwgui/settings.cpp @@ -0,0 +1,311 @@ +#include "settings.hpp" + +#include "components/settings/values.hpp" + +namespace MWGui +{ + WindowSettingValues makeAlchemyWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mAlchemyX, + .mY = Settings::windows().mAlchemyY, + .mW = Settings::windows().mAlchemyW, + .mH = Settings::windows().mAlchemyH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mAlchemyMaximizedX, + .mY = Settings::windows().mAlchemyMaximizedY, + .mW = Settings::windows().mAlchemyMaximizedW, + .mH = Settings::windows().mAlchemyMaximizedH, + }, + .mIsMaximized = Settings::windows().mAlchemyMaximized, + }; + } + + WindowSettingValues makeBarterWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mBarterX, + .mY = Settings::windows().mBarterY, + .mW = Settings::windows().mBarterW, + .mH = Settings::windows().mBarterH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mBarterMaximizedX, + .mY = Settings::windows().mBarterMaximizedY, + .mW = Settings::windows().mBarterMaximizedW, + .mH = Settings::windows().mBarterMaximizedH, + }, + .mIsMaximized = Settings::windows().mBarterMaximized, + }; + } + + WindowSettingValues makeCompanionWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mCompanionX, + .mY = Settings::windows().mCompanionY, + .mW = Settings::windows().mCompanionW, + .mH = Settings::windows().mCompanionH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mCompanionMaximizedX, + .mY = Settings::windows().mCompanionMaximizedY, + .mW = Settings::windows().mCompanionMaximizedW, + .mH = Settings::windows().mCompanionMaximizedH, + }, + .mIsMaximized = Settings::windows().mCompanionMaximized, + }; + } + + WindowSettingValues makeConsoleWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mConsoleX, + .mY = Settings::windows().mConsoleY, + .mW = Settings::windows().mConsoleW, + .mH = Settings::windows().mConsoleH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mConsoleMaximizedX, + .mY = Settings::windows().mConsoleMaximizedY, + .mW = Settings::windows().mConsoleMaximizedW, + .mH = Settings::windows().mConsoleMaximizedH, + }, + .mIsMaximized = Settings::windows().mConsoleMaximized, + }; + } + + WindowSettingValues makeContainerWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mContainerX, + .mY = Settings::windows().mContainerY, + .mW = Settings::windows().mContainerW, + .mH = Settings::windows().mContainerH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mContainerMaximizedX, + .mY = Settings::windows().mContainerMaximizedY, + .mW = Settings::windows().mContainerMaximizedW, + .mH = Settings::windows().mContainerMaximizedH, + }, + .mIsMaximized = Settings::windows().mContainerMaximized, + }; + } + + WindowSettingValues makeDebugWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mDebugX, + .mY = Settings::windows().mDebugY, + .mW = Settings::windows().mDebugW, + .mH = Settings::windows().mDebugH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mDebugMaximizedX, + .mY = Settings::windows().mDebugMaximizedY, + .mW = Settings::windows().mDebugMaximizedW, + .mH = Settings::windows().mDebugMaximizedH, + }, + .mIsMaximized = Settings::windows().mDebugMaximized, + }; + } + + WindowSettingValues makeDialogueWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mDialogueX, + .mY = Settings::windows().mDialogueY, + .mW = Settings::windows().mDialogueW, + .mH = Settings::windows().mDialogueH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mDialogueMaximizedX, + .mY = Settings::windows().mDialogueMaximizedY, + .mW = Settings::windows().mDialogueMaximizedW, + .mH = Settings::windows().mDialogueMaximizedH, + }, + .mIsMaximized = Settings::windows().mDialogueMaximized, + }; + } + + WindowSettingValues makeInventoryWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mInventoryX, + .mY = Settings::windows().mInventoryY, + .mW = Settings::windows().mInventoryW, + .mH = Settings::windows().mInventoryH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mInventoryMaximizedX, + .mY = Settings::windows().mInventoryMaximizedY, + .mW = Settings::windows().mInventoryMaximizedW, + .mH = Settings::windows().mInventoryMaximizedH, + }, + .mIsMaximized = Settings::windows().mInventoryMaximized, + }; + } + + WindowSettingValues makeInventoryBarterWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mInventoryBarterX, + .mY = Settings::windows().mInventoryBarterY, + .mW = Settings::windows().mInventoryBarterW, + .mH = Settings::windows().mInventoryBarterH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mInventoryBarterMaximizedX, + .mY = Settings::windows().mInventoryBarterMaximizedY, + .mW = Settings::windows().mInventoryBarterMaximizedW, + .mH = Settings::windows().mInventoryBarterMaximizedH, + }, + .mIsMaximized = Settings::windows().mInventoryBarterMaximized, + }; + } + + WindowSettingValues makeInventoryCompanionWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mInventoryCompanionX, + .mY = Settings::windows().mInventoryCompanionY, + .mW = Settings::windows().mInventoryCompanionW, + .mH = Settings::windows().mInventoryCompanionH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mInventoryCompanionMaximizedX, + .mY = Settings::windows().mInventoryCompanionMaximizedY, + .mW = Settings::windows().mInventoryCompanionMaximizedW, + .mH = Settings::windows().mInventoryCompanionMaximizedH, + }, + .mIsMaximized = Settings::windows().mInventoryCompanionMaximized, + }; + } + + WindowSettingValues makeInventoryContainerWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mInventoryContainerX, + .mY = Settings::windows().mInventoryContainerY, + .mW = Settings::windows().mInventoryContainerW, + .mH = Settings::windows().mInventoryContainerH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mInventoryContainerMaximizedX, + .mY = Settings::windows().mInventoryContainerMaximizedY, + .mW = Settings::windows().mInventoryContainerMaximizedW, + .mH = Settings::windows().mInventoryContainerMaximizedH, + }, + .mIsMaximized = Settings::windows().mInventoryContainerMaximized, + }; + } + + WindowSettingValues makeMapWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mMapX, + .mY = Settings::windows().mMapY, + .mW = Settings::windows().mMapW, + .mH = Settings::windows().mMapH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mMapMaximizedX, + .mY = Settings::windows().mMapMaximizedY, + .mW = Settings::windows().mMapMaximizedW, + .mH = Settings::windows().mMapMaximizedH, + }, + .mIsMaximized = Settings::windows().mMapMaximized, + }; + } + + WindowSettingValues makePostprocessorWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mPostprocessorX, + .mY = Settings::windows().mPostprocessorY, + .mW = Settings::windows().mPostprocessorW, + .mH = Settings::windows().mPostprocessorH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mPostprocessorMaximizedX, + .mY = Settings::windows().mPostprocessorMaximizedY, + .mW = Settings::windows().mPostprocessorMaximizedW, + .mH = Settings::windows().mPostprocessorMaximizedH, + }, + .mIsMaximized = Settings::windows().mPostprocessorMaximized, + }; + } + + WindowSettingValues makeSettingsWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mSettingsX, + .mY = Settings::windows().mSettingsY, + .mW = Settings::windows().mSettingsW, + .mH = Settings::windows().mSettingsH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mSettingsMaximizedX, + .mY = Settings::windows().mSettingsMaximizedY, + .mW = Settings::windows().mSettingsMaximizedW, + .mH = Settings::windows().mSettingsMaximizedH, + }, + .mIsMaximized = Settings::windows().mSettingsMaximized, + }; + } + + WindowSettingValues makeSpellsWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mSpellsX, + .mY = Settings::windows().mSpellsY, + .mW = Settings::windows().mSpellsW, + .mH = Settings::windows().mSpellsH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mSpellsMaximizedX, + .mY = Settings::windows().mSpellsMaximizedY, + .mW = Settings::windows().mSpellsMaximizedW, + .mH = Settings::windows().mSpellsMaximizedH, + }, + .mIsMaximized = Settings::windows().mSpellsMaximized, + }; + } + + WindowSettingValues makeStatsWindowSettingValues() + { + return WindowSettingValues{ + .mRegular = WindowRectSettingValues { + .mX = Settings::windows().mStatsX, + .mY = Settings::windows().mStatsY, + .mW = Settings::windows().mStatsW, + .mH = Settings::windows().mStatsH, + }, + .mMaximized = WindowRectSettingValues { + .mX = Settings::windows().mStatsMaximizedX, + .mY = Settings::windows().mStatsMaximizedY, + .mW = Settings::windows().mStatsMaximizedW, + .mH = Settings::windows().mStatsMaximizedH, + }, + .mIsMaximized = Settings::windows().mStatsMaximized, + }; + } + +} diff --git a/apps/openmw/mwgui/settings.hpp b/apps/openmw/mwgui/settings.hpp new file mode 100644 index 00000000000..b51ac29ce56 --- /dev/null +++ b/apps/openmw/mwgui/settings.hpp @@ -0,0 +1,41 @@ +#ifndef OPENMW_APPS_OPENMW_MWGUI_SETTINGS_H +#define OPENMW_APPS_OPENMW_MWGUI_SETTINGS_H + +#include "components/settings/settingvalue.hpp" + +namespace MWGui +{ + struct WindowRectSettingValues + { + Settings::SettingValue& mX; + Settings::SettingValue& mY; + Settings::SettingValue& mW; + Settings::SettingValue& mH; + }; + + struct WindowSettingValues + { + WindowRectSettingValues mRegular; + WindowRectSettingValues mMaximized; + Settings::SettingValue& mIsMaximized; + }; + + WindowSettingValues makeAlchemyWindowSettingValues(); + WindowSettingValues makeBarterWindowSettingValues(); + WindowSettingValues makeCompanionWindowSettingValues(); + WindowSettingValues makeConsoleWindowSettingValues(); + WindowSettingValues makeContainerWindowSettingValues(); + WindowSettingValues makeDebugWindowSettingValues(); + WindowSettingValues makeDialogueWindowSettingValues(); + WindowSettingValues makeInventoryWindowSettingValues(); + WindowSettingValues makeInventoryBarterWindowSettingValues(); + WindowSettingValues makeInventoryCompanionWindowSettingValues(); + WindowSettingValues makeInventoryContainerWindowSettingValues(); + WindowSettingValues makeMapWindowSettingValues(); + WindowSettingValues makePostprocessorWindowSettingValues(); + WindowSettingValues makeSettingsWindowSettingValues(); + WindowSettingValues makeSpellsWindowSettingValues(); + WindowSettingValues makeStatsWindowSettingValues(); +} + +#endif diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 3b4afc852fd..f38f6dc0e14 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -1,100 +1,124 @@ #include "settingswindow.hpp" -#include -#include +#include +#include +#include + +#include + #include -#include #include +#include +#include +#include #include +#include +#include #include -#include -#include -#include - #include -#include +#include #include -#include -#include +#include +#include +#include +#include #include #include #include +#include +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/soundmanager.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "confirmationdialog.hpp" namespace { + std::string textureFilteringToStr(const std::string& mipFilter, const std::string& magFilter) + { + if (mipFilter == "none") + return "#{OMWEngine:TextureFilteringDisabled}"; - std::string textureMipmappingToStr(const std::string& val) + if (magFilter == "linear") + { + if (mipFilter == "linear") + return "#{OMWEngine:TextureFilteringTrilinear}"; + if (mipFilter == "nearest") + return "#{OMWEngine:TextureFilteringBilinear}"; + } + else if (magFilter == "nearest") + return "#{OMWEngine:TextureFilteringNearest}"; + + Log(Debug::Warning) << "Warning: Invalid texture filtering options: " << mipFilter << ", " << magFilter; + return "#{OMWEngine:TextureFilteringOther}"; + } + + std::string lightingMethodToStr(SceneUtil::LightingMethod method) { - if (val == "linear") return "Trilinear"; - if (val == "nearest") return "Bilinear"; - if (val != "none") - Log(Debug::Warning) << "Warning: Invalid texture mipmap option: "<< val; + std::string result; + switch (method) + { + case SceneUtil::LightingMethod::FFP: + result = "#{OMWEngine:LightingMethodLegacy}"; + break; + case SceneUtil::LightingMethod::PerObjectUniform: + result = "#{OMWEngine:LightingMethodShadersCompatibility}"; + break; + case SceneUtil::LightingMethod::SingleUBO: + default: + result = "#{OMWEngine:LightingMethodShaders}"; + break; + } - return "Other"; + return MyGUI::LanguageManager::getInstance().replaceTags(result); } - void parseResolution (int &x, int &y, const std::string& str) + void parseResolution(int& x, int& y, const std::string& str) { std::vector split; - Misc::StringUtils::split (str, split, "@(x"); - assert (split.size() >= 2); + Misc::StringUtils::split(str, split, "@(x"); + assert(split.size() >= 2); Misc::StringUtils::trim(split[0]); Misc::StringUtils::trim(split[1]); - x = MyGUI::utility::parseInt (split[0]); - y = MyGUI::utility::parseInt (split[1]); + x = MyGUI::utility::parseInt(split[0]); + y = MyGUI::utility::parseInt(split[1]); } - bool sortResolutions (std::pair left, std::pair right) + bool sortResolutions(std::pair left, std::pair right) { if (left.first == right.first) return left.second > right.second; return left.first > right.first; } - std::string getAspect (int x, int y) - { - int gcd = std::gcd (x, y); - if (gcd == 0) - return std::string(); - - int xaspect = x / gcd; - int yaspect = y / gcd; - // special case: 8 : 5 is usually referred to as 16:10 - if (xaspect == 8 && yaspect == 5) - return "16 : 10"; - return MyGUI::utility::toString(xaspect) + " : " + MyGUI::utility::toString(yaspect); - } - - const char* checkButtonType = "CheckButton"; - const char* sliderType = "Slider"; + const std::string_view checkButtonType = "CheckButton"; + const std::string_view sliderType = "Slider"; - std::string getSettingType(MyGUI::Widget* widget) + std::string_view getSettingType(MyGUI::Widget* widget) { return widget->getUserString("SettingType"); } - std::string getSettingName(MyGUI::Widget* widget) + std::string_view getSettingName(MyGUI::Widget* widget) { return widget->getUserString("SettingName"); } - std::string getSettingCategory(MyGUI::Widget* widget) + std::string_view getSettingCategory(MyGUI::Widget* widget) { return widget->getUserString("SettingCategory"); } - std::string getSettingValueType(MyGUI::Widget* widget) + std::string_view getSettingValueType(MyGUI::Widget* widget) { return widget->getUserString("SettingValueType"); } @@ -114,12 +138,12 @@ namespace void updateMaxLightsComboBox(MyGUI::ComboBox* box) { constexpr int min = 8; - constexpr int max = 32; + constexpr int max = 64; constexpr int increment = 8; - int maxLights = Settings::Manager::getInt("max lights", "Shaders"); + const int maxLights = Settings::shaders().mMaxLights; // show increments of 8 in dropdown if (maxLights >= min && maxLights <= max && !(maxLights % increment)) - box->setIndexSelected((maxLights / increment)-1); + box->setIndexSelected((maxLights / increment) - 1); else box->setIndexSelected(MyGUI::ITEM_NONE); } @@ -134,12 +158,12 @@ namespace MWGui { MyGUI::Widget* current = widgets.current(); - std::string type = getSettingType(current); + std::string_view type = getSettingType(current); if (type == checkButtonType) { - std::string initialValue = Settings::Manager::getBool(getSettingName(current), - getSettingCategory(current)) - ? "#{sOn}" : "#{sOff}"; + std::string_view initialValue + = Settings::get(getSettingCategory(current), getSettingName(current)) ? "#{Interface:On}" + : "#{Interface:Off}"; current->castType()->setCaptionWithReplacing(initialValue); if (init) current->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); @@ -148,42 +172,49 @@ namespace MWGui { MyGUI::ScrollBar* scroll = current->castType(); std::string valueStr; - std::string valueType = getSettingValueType(current); + std::string_view valueType = getSettingValueType(current); if (valueType == "Float" || valueType == "Integer" || valueType == "Cell") { // TODO: ScrollBar isn't meant for this. should probably use a dedicated FloatSlider widget - float min,max; + float min, max; getSettingMinMax(scroll, min, max); - float value = Settings::Manager::getFloat(getSettingName(current), getSettingCategory(current)); + float value; if (valueType == "Cell") { + value = Settings::get(getSettingCategory(current), getSettingName(current)); std::stringstream ss; - ss << std::fixed << std::setprecision(2) << value/Constants::CellSizeInUnits; + ss << std::fixed << std::setprecision(2) << value / Constants::CellSizeInUnits; valueStr = ss.str(); } else if (valueType == "Float") { + value = Settings::get(getSettingCategory(current), getSettingName(current)); std::stringstream ss; ss << std::fixed << std::setprecision(2) << value; valueStr = ss.str(); } else - valueStr = MyGUI::utility::toString(int(value)); + { + const int intValue = Settings::get(getSettingCategory(current), getSettingName(current)); + valueStr = MyGUI::utility::toString(intValue); + value = static_cast(intValue); + } - value = std::max(min, std::min(value, max)); - value = (value-min)/(max-min); + value = std::clamp(value, min, max); + value = (value - min) / (max - min); scroll->setScrollPosition(static_cast(value * (scroll->getScrollRange() - 1))); } else { - int value = Settings::Manager::getInt(getSettingName(current), getSettingCategory(current)); + const int value = Settings::get(getSettingCategory(current), getSettingName(current)); valueStr = MyGUI::utility::toString(value); scroll->setScrollPosition(value); } if (init) - scroll->eventScrollChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onSliderChangePosition); + scroll->eventScrollChangePosition + += MyGUI::newDelegate(this, &SettingsWindow::onSliderChangePosition); if (scroll->getVisible()) updateSliderLabel(scroll, valueStr); } @@ -192,56 +223,81 @@ namespace MWGui } } - void SettingsWindow::updateSliderLabel(MyGUI::ScrollBar *scroller, const std::string& value) + void SettingsWindow::onFrame(float duration) + { + if (mScriptView->getVisible()) + { + const auto scriptsSize = mScriptAdapter->getSize(); + if (mScriptView->getCanvasSize() != scriptsSize) + mScriptView->setCanvasSize(scriptsSize); + } + } + + void SettingsWindow::updateSliderLabel(MyGUI::ScrollBar* scroller, const std::string& value) { - std::string labelWidgetName = scroller->getUserString("SettingLabelWidget"); + auto labelWidgetName = scroller->getUserString("SettingLabelWidget"); if (!labelWidgetName.empty()) { MyGUI::TextBox* textBox; getWidget(textBox, labelWidgetName); - std::string labelCaption = scroller->getUserString("SettingLabelCaption"); + std::string labelCaption{ scroller->getUserString("SettingLabelCaption") }; labelCaption = Misc::StringUtils::format(labelCaption, value); textBox->setCaptionWithReplacing(labelCaption); } } - SettingsWindow::SettingsWindow() : - WindowBase("openmw_settings_window.layout"), - mKeyboardMode(true) + SettingsWindow::SettingsWindow() + : WindowBase("openmw_settings_window.layout") + , mKeyboardMode(true) + , mCurrentPage(-1) { - bool terrain = Settings::Manager::getBool("distant terrain", "Terrain"); - const std::string widgetName = terrain ? "RenderingDistanceSlider" : "LargeRenderingDistanceSlider"; + const bool terrain = Settings::terrain().mDistantTerrain; + const std::string_view widgetName = terrain ? "RenderingDistanceSlider" : "LargeRenderingDistanceSlider"; MyGUI::Widget* unusedSlider; getWidget(unusedSlider, widgetName); unusedSlider->setVisible(false); configureWidgets(mMainWidget, true); - setTitle("#{sOptions}"); + setTitle("#{OMWEngine:SettingsWindow}"); getWidget(mSettingsTab, "SettingsTab"); getWidget(mOkButton, "OkButton"); getWidget(mResolutionList, "ResolutionList"); - getWidget(mFullscreenButton, "FullscreenButton"); + getWidget(mWindowModeList, "WindowModeList"); + getWidget(mVSyncModeList, "VSyncModeList"); getWidget(mWindowBorderButton, "WindowBorderButton"); getWidget(mTextureFilteringButton, "TextureFilteringButton"); - getWidget(mAnisotropyBox, "AnisotropyBox"); getWidget(mControlsBox, "ControlsBox"); getWidget(mResetControlsButton, "ResetControlsButton"); getWidget(mKeyboardSwitch, "KeyboardButton"); getWidget(mControllerSwitch, "ControllerButton"); + getWidget(mWaterRefractionButton, "WaterRefractionButton"); + getWidget(mSunlightScatteringButton, "SunlightScatteringButton"); + getWidget(mWobblyShoresButton, "WobblyShoresButton"); getWidget(mWaterTextureSize, "WaterTextureSize"); getWidget(mWaterReflectionDetail, "WaterReflectionDetail"); + getWidget(mWaterRainRippleDetail, "WaterRainRippleDetail"); + getWidget(mPrimaryLanguage, "PrimaryLanguage"); + getWidget(mSecondaryLanguage, "SecondaryLanguage"); + getWidget(mGmstOverridesL10n, "GmstOverridesL10nButton"); + getWidget(mWindowModeHint, "WindowModeHint"); getWidget(mLightingMethodButton, "LightingMethodButton"); getWidget(mLightsResetButton, "LightsResetButton"); getWidget(mMaxLights, "MaxLights"); + getWidget(mScriptFilter, "ScriptFilter"); + getWidget(mScriptList, "ScriptList"); + getWidget(mScriptBox, "ScriptBox"); + getWidget(mScriptView, "ScriptView"); + getWidget(mScriptAdapter, "ScriptAdapter"); + getWidget(mScriptDisabled, "ScriptDisabled"); #ifndef WIN32 // hide gamma controls since it currently does not work under Linux - MyGUI::ScrollBar *gammaSlider; + MyGUI::ScrollBar* gammaSlider; getWidget(gammaSlider, "GammaSlider"); gammaSlider->setVisible(false); - MyGUI::TextBox *textBox; + MyGUI::TextBox* textBox; getWidget(textBox, "GammaText"); textBox->setVisible(false); getWidget(textBox, "GammaTextDark"); @@ -250,31 +306,55 @@ namespace MWGui textBox->setVisible(false); #endif - mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &SettingsWindow::onWindowResize); + mMainWidget->castType()->eventWindowChangeCoord + += MyGUI::newDelegate(this, &SettingsWindow::onWindowResize); mSettingsTab->eventTabChangeSelect += MyGUI::newDelegate(this, &SettingsWindow::onTabChanged); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onOkButtonClicked); - mTextureFilteringButton->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onTextureFilteringChanged); + mTextureFilteringButton->eventComboChangePosition + += MyGUI::newDelegate(this, &SettingsWindow::onTextureFilteringChanged); mResolutionList->eventListChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onResolutionSelected); - mWaterTextureSize->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterTextureSizeChanged); - mWaterReflectionDetail->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterReflectionDetailChanged); - - mLightingMethodButton->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onLightingMethodButtonChanged); - mLightsResetButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onLightsResetButtonClicked); + mWaterRefractionButton->eventMouseButtonClick + += MyGUI::newDelegate(this, &SettingsWindow::onRefractionButtonClicked); + mWaterTextureSize->eventComboChangePosition + += MyGUI::newDelegate(this, &SettingsWindow::onWaterTextureSizeChanged); + mWaterReflectionDetail->eventComboChangePosition + += MyGUI::newDelegate(this, &SettingsWindow::onWaterReflectionDetailChanged); + mWaterRainRippleDetail->eventComboChangePosition + += MyGUI::newDelegate(this, &SettingsWindow::onWaterRainRippleDetailChanged); + + mLightingMethodButton->eventComboChangePosition + += MyGUI::newDelegate(this, &SettingsWindow::onLightingMethodButtonChanged); + mLightsResetButton->eventMouseButtonClick + += MyGUI::newDelegate(this, &SettingsWindow::onLightsResetButtonClicked); mMaxLights->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onMaxLightsChanged); + mWindowModeList->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWindowModeChanged); + mVSyncModeList->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onVSyncModeChanged); + mKeyboardSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onKeyboardSwitchClicked); - mControllerSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onControllerSwitchClicked); + mControllerSwitch->eventMouseButtonClick + += MyGUI::newDelegate(this, &SettingsWindow::onControllerSwitchClicked); + + mPrimaryLanguage->eventComboChangePosition + += MyGUI::newDelegate(this, &SettingsWindow::onPrimaryLanguageChanged); + mSecondaryLanguage->eventComboChangePosition + += MyGUI::newDelegate(this, &SettingsWindow::onSecondaryLanguageChanged); + mGmstOverridesL10n->eventMouseButtonClick + += MyGUI::newDelegate(this, &SettingsWindow::onGmstOverridesL10nChanged); + + computeMinimumWindowSize(); center(); - mResetControlsButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindings); + mResetControlsButton->eventMouseButtonClick + += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindings); // fill resolution list - int screen = Settings::Manager::getInt("screen", "Video"); + const int screen = Settings::video().mScreen; int numDisplayModes = SDL_GetNumDisplayModes(screen); - std::vector < std::pair > resolutions; + std::vector> resolutions; for (int i = 0; i < numDisplayModes; i++) { SDL_DisplayMode mode; @@ -284,20 +364,17 @@ namespace MWGui std::sort(resolutions.begin(), resolutions.end(), sortResolutions); for (std::pair& resolution : resolutions) { - std::string str = MyGUI::utility::toString(resolution.first) + " x " + MyGUI::utility::toString(resolution.second); - std::string aspect = getAspect(resolution.first, resolution.second); - if (!aspect.empty()) - str = str + " (" + aspect + ")"; + std::string str = Misc::getResolutionText(resolution.first, resolution.second, "%i x %i (%i:%i)"); if (mResolutionList->findItemIndexWith(str) == MyGUI::ITEM_NONE) mResolutionList->addItem(str); } highlightCurrentResolution(); - std::string tmip = Settings::Manager::getString("texture mipmap", "General"); - mTextureFilteringButton->setCaption(textureMipmappingToStr(tmip)); + mTextureFilteringButton->setCaptionWithReplacing( + textureFilteringToStr(Settings::general().mTextureMipmap, Settings::general().mTextureMinFilter)); - int waterTextureSize = Settings::Manager::getInt("rtt size", "Water"); + int waterTextureSize = Settings::water().mRttSize; if (waterTextureSize >= 512) mWaterTextureSize->setIndexSelected(0); if (waterTextureSize >= 1024) @@ -305,16 +382,82 @@ namespace MWGui if (waterTextureSize >= 2048) mWaterTextureSize->setIndexSelected(2); - int waterReflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); - waterReflectionDetail = std::min(5, std::max(0, waterReflectionDetail)); + const int waterReflectionDetail = Settings::water().mReflectionDetail; mWaterReflectionDetail->setIndexSelected(waterReflectionDetail); + const int waterRainRippleDetail = Settings::water().mRainRippleDetail; + mWaterRainRippleDetail->setIndexSelected(waterRainRippleDetail); + + const bool waterRefraction = Settings::water().mRefraction; + mSunlightScatteringButton->setEnabled(waterRefraction); + mWobblyShoresButton->setEnabled(waterRefraction); + updateMaxLightsComboBox(mMaxLights); - mWindowBorderButton->setEnabled(!Settings::Manager::getBool("fullscreen", "Video")); + const Settings::WindowMode windowMode = Settings::video().mWindowMode; + mWindowBorderButton->setEnabled( + windowMode != Settings::WindowMode::Fullscreen && windowMode != Settings::WindowMode::WindowedFullscreen); + + mWindowModeHint->setVisible(windowMode == Settings::WindowMode::WindowedFullscreen); mKeyboardSwitch->setStateSelected(true); mControllerSwitch->setStateSelected(false); + + mScriptFilter->eventEditTextChange += MyGUI::newDelegate(this, &SettingsWindow::onScriptFilterChange); + mScriptList->eventListMouseItemActivate += MyGUI::newDelegate(this, &SettingsWindow::onScriptListSelection); + + std::vector availableLanguages; + const VFS::Manager* vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + constexpr VFS::Path::NormalizedView l10n("l10n/"); + for (const auto& path : vfs->getRecursiveDirectoryIterator(l10n)) + { + if (Misc::getFileExtension(path) == "yaml") + { + std::string localeName(Misc::stemFile(path)); + if (localeName == "gmst") + continue; // fake locale to get gmst strings from content files + if (std::find(availableLanguages.begin(), availableLanguages.end(), localeName) + == availableLanguages.end()) + availableLanguages.push_back(localeName); + } + } + + std::sort(availableLanguages.begin(), availableLanguages.end()); + + std::vector currentLocales = Settings::general().mPreferredLocales; + if (currentLocales.empty()) + currentLocales.push_back("en"); + + icu::Locale primaryLocale(currentLocales[0].c_str()); + + mPrimaryLanguage->removeAllItems(); + mPrimaryLanguage->setIndexSelected(MyGUI::ITEM_NONE); + + mSecondaryLanguage->removeAllItems(); + mSecondaryLanguage->addItem( + MyGUI::LanguageManager::getInstance().replaceTags("#{Interface:None}"), std::string()); + mSecondaryLanguage->setIndexSelected(0); + + size_t i = 0; + for (const auto& language : availableLanguages) + { + icu::Locale locale(language.c_str()); + + icu::UnicodeString str(language.c_str()); + locale.getDisplayName(primaryLocale, str); + std::string localeString; + str.toUTF8String(localeString); + + mPrimaryLanguage->addItem(localeString, language); + mSecondaryLanguage->addItem(localeString, language); + + if (language == currentLocales[0]) + mPrimaryLanguage->setIndexSelected(i); + if (currentLocales.size() > 1 && language == currentLocales[1]) + mSecondaryLanguage->setIndexSelected(i + 1); + + i++; + } } void SettingsWindow::onTabChanged(MyGUI::TabControl* /*_sender*/, size_t /*index*/) @@ -324,7 +467,7 @@ namespace MWGui void SettingsWindow::onOkButtonClicked(MyGUI::Widget* _sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Settings); + setVisible(false); } void SettingsWindow::onResolutionSelected(MyGUI::ListBox* _sender, size_t index) @@ -333,7 +476,7 @@ namespace MWGui return; ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); - dialog->askForConfirmation("#{sNotifyMessage67}"); + dialog->askForConfirmation("#{OMWEngine:ConfirmResolution}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SettingsWindow::onResolutionAccept); dialog->eventCancelClicked.clear(); @@ -342,12 +485,12 @@ namespace MWGui void SettingsWindow::onResolutionAccept() { - std::string resStr = mResolutionList->getItemNameAt(mResolutionList->getIndexSelected()); + const std::string& resStr = mResolutionList->getItemNameAt(mResolutionList->getIndexSelected()); int resX, resY; - parseResolution (resX, resY, resStr); + parseResolution(resX, resY, resStr); - Settings::Manager::setInt("resolution x", "Video", resX); - Settings::Manager::setInt("resolution y", "Video", resY); + Settings::video().mResolutionX.set(resX); + Settings::video().mResolutionY.set(resY); apply(); } @@ -361,13 +504,13 @@ namespace MWGui { mResolutionList->setIndexSelected(MyGUI::ITEM_NONE); - int currentX = Settings::Manager::getInt("resolution x", "Video"); - int currentY = Settings::Manager::getInt("resolution y", "Video"); + const int currentX = Settings::video().mResolutionX; + const int currentY = Settings::video().mResolutionY; - for (size_t i=0; igetItemCount(); ++i) + for (size_t i = 0; i < mResolutionList->getItemCount(); ++i) { int resX, resY; - parseResolution (resX, resY, mResolutionList->getItemNameAt(i)); + parseResolution(resX, resY, mResolutionList->getItemNameAt(i)); if (resX == currentX && resY == currentY) { @@ -377,6 +520,14 @@ namespace MWGui } } + void SettingsWindow::onRefractionButtonClicked(MyGUI::Widget* _sender) + { + const bool refractionEnabled = Settings::water().mRefraction; + + mSunlightScatteringButton->setEnabled(refractionEnabled); + mWobblyShoresButton->setEnabled(refractionEnabled); + } + void SettingsWindow::onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos) { int size = 0; @@ -386,14 +537,19 @@ namespace MWGui size = 1024; else if (pos == 2) size = 2048; - Settings::Manager::setInt("rtt size", "Water", size); + Settings::water().mRttSize.set(size); apply(); } void SettingsWindow::onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos) { - unsigned int level = std::min((unsigned int)5, (unsigned int)pos); - Settings::Manager::setInt("reflection detail", "Water", level); + Settings::water().mReflectionDetail.set(static_cast(pos)); + apply(); + } + + void SettingsWindow::onWaterRainRippleDetailChanged(MyGUI::ComboBox* _sender, size_t pos) + { + Settings::water().mRainRippleDetail.set(static_cast(pos)); apply(); } @@ -402,43 +558,108 @@ namespace MWGui if (pos == MyGUI::ITEM_NONE) return; - std::string message = "This change requires a restart to take effect."; - MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, {"#{sOK}"}, true); + _sender->setCaptionWithReplacing(_sender->getItemNameAt(_sender->getIndexSelected())); - Settings::Manager::setString("lighting method", "Shaders", _sender->getItemNameAt(pos)); + MWBase::Environment::get().getWindowManager()->interactiveMessageBox( + "#{OMWEngine:ChangeRequiresRestart}", { "#{Interface:OK}" }, true); + + Settings::shaders().mLightingMethod.set( + Settings::parseLightingMethod(*_sender->getItemDataAt(pos))); apply(); } - void SettingsWindow::onMaxLightsChanged(MyGUI::ComboBox* _sender, size_t pos) + void SettingsWindow::onLanguageChanged(size_t langPriority, MyGUI::ComboBox* _sender, size_t pos) { - int count = 8 * (pos + 1); + if (pos == MyGUI::ITEM_NONE) + return; - Settings::Manager::setInt("max lights", "Shaders", count); + _sender->setCaptionWithReplacing(_sender->getItemNameAt(_sender->getIndexSelected())); + + MWBase::Environment::get().getWindowManager()->interactiveMessageBox( + "#{OMWEngine:ChangeRequiresRestart}", { "#{Interface:OK}" }, true); + + std::vector currentLocales = Settings::general().mPreferredLocales; + if (currentLocales.size() <= langPriority) + currentLocales.resize(langPriority + 1, "en"); + + const auto& languageCode = *_sender->getItemDataAt(pos); + if (!languageCode.empty()) + currentLocales[langPriority] = languageCode; + else + currentLocales.resize(1); + + Settings::general().mPreferredLocales.set(currentLocales); + } + + void SettingsWindow::onGmstOverridesL10nChanged(MyGUI::Widget*) + { + MWBase::Environment::get().getWindowManager()->interactiveMessageBox( + "#{OMWEngine:ChangeRequiresRestart}", { "#{Interface:OK}" }, true); + } + + void SettingsWindow::onVSyncModeChanged(MyGUI::ComboBox* sender, size_t pos) + { + if (pos == MyGUI::ITEM_NONE) + return; + + Settings::video().mVsyncMode.set(static_cast(sender->getIndexSelected())); + apply(); + } + + void SettingsWindow::onWindowModeChanged(MyGUI::ComboBox* sender, size_t pos) + { + if (pos == MyGUI::ITEM_NONE) + return; + + const Settings::WindowMode windowMode = static_cast(sender->getIndexSelected()); + if (windowMode == Settings::WindowMode::WindowedFullscreen) + { + mResolutionList->setEnabled(false); + mWindowModeHint->setVisible(true); + } + else + { + mResolutionList->setEnabled(true); + mWindowModeHint->setVisible(false); + } + + if (windowMode == Settings::WindowMode::Windowed) + mWindowBorderButton->setEnabled(true); + else + mWindowBorderButton->setEnabled(false); + + Settings::video().mWindowMode.set(windowMode); + apply(); + } + + void SettingsWindow::onMaxLightsChanged(MyGUI::ComboBox* _sender, size_t pos) + { + Settings::shaders().mMaxLights.set(8 * (pos + 1)); apply(); configureWidgets(mMainWidget, false); } void SettingsWindow::onLightsResetButtonClicked(MyGUI::Widget* _sender) { - std::vector buttons = {"#{sYes}", "#{sNo}"}; - std::string message = "Resets to default values, would you like to continue? Changes to lighting method will require a restart."; - MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons, true); + std::vector buttons = { "#{Interface:Yes}", "#{Interface:No}" }; + MWBase::Environment::get().getWindowManager()->interactiveMessageBox( + "#{OMWEngine:LightingResetToDefaults}", buttons, true); int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); if (selectedButton == 1 || selectedButton == -1) return; - constexpr std::array settings = { - "light bounds multiplier", - "maximum light distance", - "light fade start", - "minimum interior brightness", - "max lights", - "lighting method", - }; - for (const auto& setting : settings) - Settings::Manager::setString(setting, "Shaders", Settings::Manager::mDefaultSettings[{"Shaders", setting}]); - - mLightingMethodButton->setIndexSelected(mLightingMethodButton->findItemIndexWith(Settings::Manager::mDefaultSettings[{"Shaders", "lighting method"}])); + Settings::shaders().mForcePerPixelLighting.reset(); + Settings::shaders().mClassicFalloff.reset(); + Settings::shaders().mLightBoundsMultiplier.reset(); + Settings::shaders().mMaximumLightDistance.reset(); + Settings::shaders().mLightFadeStart.reset(); + Settings::shaders().mMinimumInteriorBrightness.reset(); + Settings::shaders().mMaxLights.reset(); + Settings::shaders().mLightingMethod.reset(); + + const SceneUtil::LightingMethod lightingMethod = Settings::shaders().mLightingMethod; + const std::size_t lightIndex = mLightingMethodButton->findItemIndexWith(lightingMethodToStr(lightingMethod)); + mLightingMethodButton->setIndexSelected(lightIndex); updateMaxLightsComboBox(mMaxLights); apply(); @@ -447,66 +668,23 @@ namespace MWGui void SettingsWindow::onButtonToggled(MyGUI::Widget* _sender) { - std::string on = MWBase::Environment::get().getWindowManager()->getGameSettingString("sOn", "On"); - std::string off = MWBase::Environment::get().getWindowManager()->getGameSettingString("sOff", "On"); + std::string_view on = MWBase::Environment::get().getWindowManager()->getGameSettingString("sOn", "On"); bool newState; if (_sender->castType()->getCaption() == on) { - _sender->castType()->setCaption(off); + _sender->castType()->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOff", "Off"))); newState = false; } else { - _sender->castType()->setCaption(on); + _sender->castType()->setCaption(MyGUI::UString(on)); newState = true; } - if (_sender == mFullscreenButton) - { - // check if this resolution is supported in fullscreen - if (mResolutionList->getIndexSelected() != MyGUI::ITEM_NONE) - { - std::string resStr = mResolutionList->getItemNameAt(mResolutionList->getIndexSelected()); - int resX, resY; - parseResolution (resX, resY, resStr); - Settings::Manager::setInt("resolution x", "Video", resX); - Settings::Manager::setInt("resolution y", "Video", resY); - } - - bool supported = false; - int fallbackX = 0, fallbackY = 0; - for (unsigned int i=0; igetItemCount(); ++i) - { - std::string resStr = mResolutionList->getItemNameAt(i); - int resX, resY; - parseResolution (resX, resY, resStr); - - if (i == 0) - { - fallbackX = resX; - fallbackY = resY; - } - - if (resX == Settings::Manager::getInt("resolution x", "Video") - && resY == Settings::Manager::getInt("resolution y", "Video")) - supported = true; - } - - if (!supported && mResolutionList->getItemCount()) - { - if (fallbackX != 0 && fallbackY != 0) - { - Settings::Manager::setInt("resolution x", "Video", fallbackX); - Settings::Manager::setInt("resolution y", "Video", fallbackY); - } - } - - mWindowBorderButton->setEnabled(!newState); - } - if (getSettingType(_sender) == checkButtonType) { - Settings::Manager::setBool(getSettingName(_sender), getSettingCategory(_sender), newState); + Settings::get(getSettingCategory(_sender), getSettingName(_sender)).set(newState); apply(); return; } @@ -514,51 +692,71 @@ namespace MWGui void SettingsWindow::onTextureFilteringChanged(MyGUI::ComboBox* _sender, size_t pos) { - if(pos == 0) - Settings::Manager::setString("texture mipmap", "General", "nearest"); - else if(pos == 1) - Settings::Manager::setString("texture mipmap", "General", "linear"); - else - Log(Debug::Warning) << "Unexpected option pos " << pos; + auto& generalSettings = Settings::general(); + switch (pos) + { + case 0: // Bilinear with mips + generalSettings.mTextureMipmap.set("nearest"); + generalSettings.mTextureMagFilter.set("linear"); + generalSettings.mTextureMinFilter.set("linear"); + break; + case 1: // Trilinear with mips + generalSettings.mTextureMipmap.set("linear"); + generalSettings.mTextureMagFilter.set("linear"); + generalSettings.mTextureMinFilter.set("linear"); + break; + default: + Log(Debug::Warning) << "Unexpected texture filtering option pos " << pos; + break; + } + apply(); } + void SettingsWindow::onResChange(int width, int height) + { + center(); + highlightCurrentResolution(); + } + void SettingsWindow::onSliderChangePosition(MyGUI::ScrollBar* scroller, size_t pos) { if (getSettingType(scroller) == "Slider") { std::string valueStr; - std::string valueType = getSettingValueType(scroller); + std::string_view valueType = getSettingValueType(scroller); if (valueType == "Float" || valueType == "Integer" || valueType == "Cell") { - float value = pos / float(scroller->getScrollRange()-1); + float value = pos / float(scroller->getScrollRange() - 1); - float min,max; + float min, max; getSettingMinMax(scroller, min, max); - value = min + (max-min) * value; - if (valueType == "Float") - Settings::Manager::setFloat(getSettingName(scroller), getSettingCategory(scroller), value); - else - Settings::Manager::setInt(getSettingName(scroller), getSettingCategory(scroller), (int)value); + value = min + (max - min) * value; if (valueType == "Cell") { + Settings::get(getSettingCategory(scroller), getSettingName(scroller)).set(value); std::stringstream ss; - ss << std::fixed << std::setprecision(2) << value/Constants::CellSizeInUnits; + ss << std::fixed << std::setprecision(2) << value / Constants::CellSizeInUnits; valueStr = ss.str(); } else if (valueType == "Float") { + Settings::get(getSettingCategory(scroller), getSettingName(scroller)).set(value); std::stringstream ss; ss << std::fixed << std::setprecision(2) << value; valueStr = ss.str(); } else + { + Settings::get(getSettingCategory(scroller), getSettingName(scroller)) + .set(static_cast(value)); valueStr = MyGUI::utility::toString(int(value)); + } } else { - Settings::Manager::setInt(getSettingName(scroller), getSettingCategory(scroller), pos); + Settings::get(getSettingCategory(scroller), getSettingName(scroller)).set(pos); valueStr = MyGUI::utility::toString(pos); } updateSliderLabel(scroller, valueStr); @@ -580,7 +778,7 @@ namespace MWGui void SettingsWindow::onKeyboardSwitchClicked(MyGUI::Widget* _sender) { - if(mKeyboardMode) + if (mKeyboardMode) return; mKeyboardMode = true; mKeyboardSwitch->setStateSelected(true); @@ -591,7 +789,7 @@ namespace MWGui void SettingsWindow::onControllerSwitchClicked(MyGUI::Widget* _sender) { - if(!mKeyboardMode) + if (!mKeyboardMode) return; mKeyboardMode = false; mKeyboardSwitch->setStateSelected(false); @@ -606,30 +804,30 @@ namespace MWGui MyGUI::Gui::getInstance().destroyWidget(mControlsBox->getChildAt(0)); MWBase::Environment::get().getWindowManager()->removeStaticMessageBox(); - std::vector actions; - if(mKeyboardMode) - actions = MWBase::Environment::get().getInputManager()->getActionKeySorting(); - else - actions = MWBase::Environment::get().getInputManager()->getActionControllerSorting(); + const auto inputManager = MWBase::Environment::get().getInputManager(); + const auto& actions + = mKeyboardMode ? inputManager->getActionKeySorting() : inputManager->getActionControllerSorting(); for (const int& action : actions) { - std::string desc = MWBase::Environment::get().getInputManager()->getActionDescription (action); - if (desc == "") + std::string desc{ inputManager->getActionDescription(action) }; + if (desc.empty()) continue; std::string binding; - if(mKeyboardMode) - binding = MWBase::Environment::get().getInputManager()->getActionKeyBindingName(action); + if (mKeyboardMode) + binding = inputManager->getActionKeyBindingName(action); else - binding = MWBase::Environment::get().getInputManager()->getActionControllerBindingName(action); + binding = inputManager->getActionControllerBindingName(action); - Gui::SharedStateButton* leftText = mControlsBox->createWidget("SandTextButton", MyGUI::IntCoord(), MyGUI::Align::Default); + Gui::SharedStateButton* leftText = mControlsBox->createWidget( + "SandTextButton", MyGUI::IntCoord(), MyGUI::Align::Default); leftText->setCaptionWithReplacing(desc); - Gui::SharedStateButton* rightText = mControlsBox->createWidget("SandTextButton", MyGUI::IntCoord(), MyGUI::Align::Default); + Gui::SharedStateButton* rightText = mControlsBox->createWidget( + "SandTextButton", MyGUI::IntCoord(), MyGUI::Align::Default); rightText->setCaptionWithReplacing(binding); - rightText->setTextAlign (MyGUI::Align::Right); + rightText->setTextAlign(MyGUI::Align::Right); rightText->setUserData(action); // save the action id for callbacks rightText->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onRebindAction); rightText->eventMouseWheel += MyGUI::newDelegate(this, &SettingsWindow::onInputTabMouseWheel); @@ -646,7 +844,7 @@ namespace MWGui void SettingsWindow::updateLightSettings() { auto lightingMethod = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getLightingMethod(); - std::string lightingMethodStr = SceneUtil::LightManager::getLightingMethodString(lightingMethod); + std::string lightingMethodStr = lightingMethodToStr(lightingMethod); mLightingMethodButton->removeAllItems(); @@ -661,56 +859,211 @@ namespace MWGui if (!MWBase::Environment::get().getResourceSystem()->getSceneManager()->isSupportedLightingMethod(method)) continue; - mLightingMethodButton->addItem(SceneUtil::LightManager::getLightingMethodString(method)); + mLightingMethodButton->addItem( + lightingMethodToStr(method), SceneUtil::LightManager::getLightingMethodString(method)); } - mLightingMethodButton->setIndexSelected(mLightingMethodButton->findItemIndexWith(lightingMethodStr)); } + void SettingsWindow::updateWindowModeSettings() + { + const Settings::WindowMode windowMode = Settings::video().mWindowMode; + const std::size_t windowModeIndex = static_cast(windowMode); + + mWindowModeList->setIndexSelected(windowModeIndex); + + if (windowMode != Settings::WindowMode::Windowed && windowModeIndex != MyGUI::ITEM_NONE) + { + // check if this resolution is supported in fullscreen + if (mResolutionList->getIndexSelected() != MyGUI::ITEM_NONE) + { + const std::string& resStr = mResolutionList->getItemNameAt(mResolutionList->getIndexSelected()); + int resX, resY; + parseResolution(resX, resY, resStr); + Settings::video().mResolutionX.set(resX); + Settings::video().mResolutionY.set(resY); + } + + bool supported = false; + int fallbackX = 0, fallbackY = 0; + for (size_t i = 0; i < mResolutionList->getItemCount(); ++i) + { + const std::string& resStr = mResolutionList->getItemNameAt(i); + int resX, resY; + parseResolution(resX, resY, resStr); + + if (i == 0) + { + fallbackX = resX; + fallbackY = resY; + } + + if (resX == Settings::video().mResolutionX && resY == Settings::video().mResolutionY) + supported = true; + } + + if (!supported && mResolutionList->getItemCount()) + { + if (fallbackX != 0 && fallbackY != 0) + { + Settings::video().mResolutionX.set(fallbackX); + Settings::video().mResolutionY.set(fallbackY); + } + } + + mWindowBorderButton->setEnabled(false); + } + + if (windowMode == Settings::WindowMode::WindowedFullscreen) + mResolutionList->setEnabled(false); + } + + void SettingsWindow::updateVSyncModeSettings() + { + mVSyncModeList->setIndexSelected(static_cast(Settings::video().mVsyncMode)); + } + void SettingsWindow::layoutControlsBox() { - const int h = 18; + const int h = Settings::gui().mFontSize + 2; const int w = mControlsBox->getWidth() - 28; const int noWidgetsInRow = 2; const int totalH = mControlsBox->getChildCount() / noWidgetsInRow * h; for (size_t i = 0; i < mControlsBox->getChildCount(); i++) { - MyGUI::Widget * widget = mControlsBox->getChildAt(i); + MyGUI::Widget* widget = mControlsBox->getChildAt(i); widget->setCoord(0, i / noWidgetsInRow * h, w, h); } - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden mControlsBox->setVisibleVScroll(false); - mControlsBox->setCanvasSize (mControlsBox->getWidth(), std::max(totalH, mControlsBox->getHeight())); + mControlsBox->setCanvasSize(mControlsBox->getWidth(), std::max(totalH, mControlsBox->getHeight())); mControlsBox->setVisibleVScroll(true); } + namespace + { + std::string escapeRegex(const std::string& str) + { + static const std::regex specialChars(R"r([\^\.\[\$\(\)\|\*\+\?\{])r", std::regex_constants::extended); + return std::regex_replace(str, specialChars, R"(\$&)"); + } + + std::regex wordSearch(const std::string& query) + { + static const std::regex wordsRegex(R"([^[:space:]]+)", std::regex_constants::extended); + auto wordsBegin = std::sregex_iterator(query.begin(), query.end(), wordsRegex); + auto wordsEnd = std::sregex_iterator(); + std::string searchRegex("("); + for (auto it = wordsBegin; it != wordsEnd; ++it) + { + if (it != wordsBegin) + searchRegex += '|'; + searchRegex += escapeRegex(query.substr(it->position(), it->length())); + } + searchRegex += ')'; + // query had only whitespace characters + if (searchRegex == "()") + searchRegex = "^(.*)$"; + return std::regex(searchRegex, std::regex_constants::extended | std::regex_constants::icase); + } + + double weightedSearch(const std::regex& regex, const std::string& text) + { + std::smatch matches; + std::regex_search(text, matches, regex); + // need a signed value, so cast to double (not an integer type to guarantee no overflow) + return static_cast(matches.size()); + } + } + + void SettingsWindow::renderScriptSettings() + { + mScriptAdapter->detach(); + + mScriptList->removeAllItems(); + mScriptView->setCanvasSize({ 0, 0 }); + + struct WeightedPage + { + size_t mIndex; + std::string mName; + double mNameWeight; + double mHintWeight; + + constexpr auto tie() const { return std::tie(mNameWeight, mHintWeight, mName); } + + constexpr bool operator<(const WeightedPage& rhs) const { return tie() < rhs.tie(); } + }; + + std::regex searchRegex = wordSearch(mScriptFilter->getCaption()); + std::vector weightedPages; + weightedPages.reserve(LuaUi::scriptSettingsPageCount()); + for (size_t i = 0; i < LuaUi::scriptSettingsPageCount(); ++i) + { + LuaUi::ScriptSettingsPage page = LuaUi::scriptSettingsPageAt(i); + double nameWeight = weightedSearch(searchRegex, page.mName); + double hintWeight = weightedSearch(searchRegex, page.mSearchHints); + if ((nameWeight + hintWeight) > 0) + weightedPages.push_back({ i, page.mName, -nameWeight, -hintWeight }); + } + std::sort(weightedPages.begin(), weightedPages.end()); + for (const WeightedPage& weightedPage : weightedPages) + mScriptList->addItem(weightedPage.mName, weightedPage.mIndex); + + // Hide script settings when the game world isn't loaded + bool disabled = LuaUi::scriptSettingsPageCount() == 0; + mScriptFilter->setVisible(!disabled); + mScriptList->setVisible(!disabled); + mScriptBox->setVisible(!disabled); + mScriptDisabled->setVisible(disabled); + + LuaUi::attachPageAt(mCurrentPage, mScriptAdapter); + } + + void SettingsWindow::onScriptFilterChange(MyGUI::EditBox*) + { + renderScriptSettings(); + } + + void SettingsWindow::onScriptListSelection(MyGUI::ListBox*, size_t index) + { + mScriptAdapter->detach(); + mCurrentPage = -1; + if (index < mScriptList->getItemCount()) + { + mCurrentPage = *mScriptList->getItemDataAt(index); + LuaUi::attachPageAt(mCurrentPage, mScriptAdapter); + } + } + void SettingsWindow::onRebindAction(MyGUI::Widget* _sender) { int actionId = *_sender->getUserData(); - _sender->castType()->setCaptionWithReplacing("#{sNone}"); + _sender->castType()->setCaptionWithReplacing("#{Interface:None}"); - MWBase::Environment::get().getWindowManager ()->staticMessageBox ("#{sControlsMenu3}"); - MWBase::Environment::get().getWindowManager ()->disallowMouse(); - - MWBase::Environment::get().getInputManager ()->enableDetectingBindingMode (actionId, mKeyboardMode); + MWBase::Environment::get().getWindowManager()->staticMessageBox("#{OMWEngine:RebindAction}"); + MWBase::Environment::get().getWindowManager()->disallowMouse(); + MWBase::Environment::get().getInputManager()->enableDetectingBindingMode(actionId, mKeyboardMode); } void SettingsWindow::onInputTabMouseWheel(MyGUI::Widget* _sender, int _rel) { - if (mControlsBox->getViewOffset().top + _rel*0.3f > 0) + if (mControlsBox->getViewOffset().top + _rel * 0.3f > 0) mControlsBox->setViewOffset(MyGUI::IntPoint(0, 0)); else - mControlsBox->setViewOffset(MyGUI::IntPoint(0, static_cast(mControlsBox->getViewOffset().top + _rel*0.3f))); + mControlsBox->setViewOffset( + MyGUI::IntPoint(0, static_cast(mControlsBox->getViewOffset().top + _rel * 0.3f))); } void SettingsWindow::onResetDefaultBindings(MyGUI::Widget* _sender) { ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); - dialog->askForConfirmation("#{sNotifyMessage66}"); + dialog->askForConfirmation("#{OMWEngine:ConfirmResetBindings}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindingsAccept); dialog->eventCancelClicked.clear(); @@ -718,11 +1071,11 @@ namespace MWGui void SettingsWindow::onResetDefaultBindingsAccept() { - if(mKeyboardMode) - MWBase::Environment::get().getInputManager ()->resetToDefaultKeyBindings (); + if (mKeyboardMode) + MWBase::Environment::get().getInputManager()->resetToDefaultKeyBindings(); else MWBase::Environment::get().getInputManager()->resetToDefaultControllerBindings(); - updateControlsBox (); + updateControlsBox(); } void SettingsWindow::onOpen() @@ -730,15 +1083,44 @@ namespace MWGui highlightCurrentResolution(); updateControlsBox(); updateLightSettings(); + updateWindowModeSettings(); + updateVSyncModeSettings(); resetScrollbars(); + renderScriptSettings(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); } - void SettingsWindow::onWindowResize(MyGUI::Window *_sender) + void SettingsWindow::onWindowResize(MyGUI::Window* _sender) { layoutControlsBox(); } + void SettingsWindow::computeMinimumWindowSize() + { + auto* window = mMainWidget->castType(); + auto minSize = window->getMinSize(); + + // Window should be at minimum wide enough to show all tabs. + int tabBarWidth = 0; + for (uint32_t i = 0; i < mSettingsTab->getItemCount(); i++) + { + tabBarWidth += mSettingsTab->getButtonWidthAt(i); + } + + // Need to include window margins + int margins = mMainWidget->getWidth() - mSettingsTab->getWidth(); + int minimumWindowWidth = tabBarWidth + margins; + + if (minimumWindowWidth > minSize.width) + { + minSize.width = minimumWindowWidth; + window->setMinSize(minSize); + + // Make a dummy call to setSize so MyGUI can apply any resize resulting from the change in MinSize + mMainWidget->setSize(mMainWidget->getSize()); + } + } + void SettingsWindow::resetScrollbars() { mResolutionList->setScrollPosition(0); diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index 9c28733f9a7..dc4e09f8ac8 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -1,83 +1,125 @@ #ifndef MWGUI_SETTINGS_H #define MWGUI_SETTINGS_H +#include + #include "windowbase.hpp" namespace MWGui { class SettingsWindow : public WindowBase { - public: - SettingsWindow(); + public: + SettingsWindow(); + + void onOpen() override; + + void onFrame(float duration) override; + + void updateControlsBox(); - void onOpen() override; + void updateLightSettings(); - void updateControlsBox(); + void updateVSyncModeSettings(); - void updateLightSettings(); + void updateWindowModeSettings(); - void onResChange(int, int) override { center(); } + void onResChange(int, int) override; protected: - MyGUI::TabControl* mSettingsTab; - MyGUI::Button* mOkButton; - - // graphics - MyGUI::ListBox* mResolutionList; - MyGUI::Button* mFullscreenButton; - MyGUI::Button* mWindowBorderButton; - MyGUI::ComboBox* mTextureFilteringButton; - MyGUI::Widget* mAnisotropyBox; - - MyGUI::ComboBox* mWaterTextureSize; - MyGUI::ComboBox* mWaterReflectionDetail; - - MyGUI::ComboBox* mMaxLights; - MyGUI::ComboBox* mLightingMethodButton; - MyGUI::Button* mLightsResetButton; - - // controls - MyGUI::ScrollView* mControlsBox; - MyGUI::Button* mResetControlsButton; - MyGUI::Button* mKeyboardSwitch; - MyGUI::Button* mControllerSwitch; - bool mKeyboardMode; //if true, setting up the keyboard. Otherwise, it's controller - - void onTabChanged(MyGUI::TabControl* _sender, size_t index); - void onOkButtonClicked(MyGUI::Widget* _sender); - void onTextureFilteringChanged(MyGUI::ComboBox* _sender, size_t pos); - void onSliderChangePosition(MyGUI::ScrollBar* scroller, size_t pos); - void onButtonToggled(MyGUI::Widget* _sender); - void onResolutionSelected(MyGUI::ListBox* _sender, size_t index); - void onResolutionAccept(); - void onResolutionCancel(); - void highlightCurrentResolution(); - - void onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos); - void onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos); - - void onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos); - void onLightsResetButtonClicked(MyGUI::Widget* _sender); - void onMaxLightsChanged(MyGUI::ComboBox* _sender, size_t pos); - - void onRebindAction(MyGUI::Widget* _sender); - void onInputTabMouseWheel(MyGUI::Widget* _sender, int _rel); - void onResetDefaultBindings(MyGUI::Widget* _sender); - void onResetDefaultBindingsAccept (); - void onKeyboardSwitchClicked(MyGUI::Widget* _sender); - void onControllerSwitchClicked(MyGUI::Widget* _sender); - - void onWindowResize(MyGUI::Window* _sender); - - void apply(); - - void configureWidgets(MyGUI::Widget* widget, bool init); - void updateSliderLabel(MyGUI::ScrollBar* scroller, const std::string& value); - - void layoutControlsBox(); - - private: - void resetScrollbars(); + MyGUI::TabControl* mSettingsTab; + MyGUI::Button* mOkButton; + + // graphics + MyGUI::ListBox* mResolutionList; + MyGUI::ComboBox* mWindowModeList; + MyGUI::ComboBox* mVSyncModeList; + MyGUI::Button* mWindowBorderButton; + MyGUI::ComboBox* mTextureFilteringButton; + + MyGUI::Button* mWaterRefractionButton; + MyGUI::Button* mSunlightScatteringButton; + MyGUI::Button* mWobblyShoresButton; + MyGUI::ComboBox* mWaterTextureSize; + MyGUI::ComboBox* mWaterReflectionDetail; + MyGUI::ComboBox* mWaterRainRippleDetail; + + MyGUI::ComboBox* mMaxLights; + MyGUI::ComboBox* mLightingMethodButton; + MyGUI::Button* mLightsResetButton; + + MyGUI::ComboBox* mPrimaryLanguage; + MyGUI::ComboBox* mSecondaryLanguage; + MyGUI::Button* mGmstOverridesL10n; + + MyGUI::Widget* mWindowModeHint; + + // controls + MyGUI::ScrollView* mControlsBox; + MyGUI::Button* mResetControlsButton; + MyGUI::Button* mKeyboardSwitch; + MyGUI::Button* mControllerSwitch; + bool mKeyboardMode; // if true, setting up the keyboard. Otherwise, it's controller + + MyGUI::EditBox* mScriptFilter; + MyGUI::ListBox* mScriptList; + MyGUI::Widget* mScriptBox; + MyGUI::Widget* mScriptDisabled; + MyGUI::ScrollView* mScriptView; + LuaUi::LuaAdapter* mScriptAdapter; + int mCurrentPage; + + void onTabChanged(MyGUI::TabControl* _sender, size_t index); + void onOkButtonClicked(MyGUI::Widget* _sender); + void onTextureFilteringChanged(MyGUI::ComboBox* _sender, size_t pos); + void onSliderChangePosition(MyGUI::ScrollBar* scroller, size_t pos); + void onButtonToggled(MyGUI::Widget* _sender); + void onResolutionSelected(MyGUI::ListBox* _sender, size_t index); + void onResolutionAccept(); + void onResolutionCancel(); + void highlightCurrentResolution(); + + void onRefractionButtonClicked(MyGUI::Widget* _sender); + void onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos); + void onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos); + void onWaterRainRippleDetailChanged(MyGUI::ComboBox* _sender, size_t pos); + + void onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos); + void onLightsResetButtonClicked(MyGUI::Widget* _sender); + void onMaxLightsChanged(MyGUI::ComboBox* _sender, size_t pos); + + void onPrimaryLanguageChanged(MyGUI::ComboBox* _sender, size_t pos) { onLanguageChanged(0, _sender, pos); } + void onSecondaryLanguageChanged(MyGUI::ComboBox* _sender, size_t pos) { onLanguageChanged(1, _sender, pos); } + void onLanguageChanged(size_t langPriority, MyGUI::ComboBox* _sender, size_t pos); + void onGmstOverridesL10nChanged(MyGUI::Widget* _sender); + + void onWindowModeChanged(MyGUI::ComboBox* _sender, size_t pos); + void onVSyncModeChanged(MyGUI::ComboBox* _sender, size_t pos); + + void onRebindAction(MyGUI::Widget* _sender); + void onInputTabMouseWheel(MyGUI::Widget* _sender, int _rel); + void onResetDefaultBindings(MyGUI::Widget* _sender); + void onResetDefaultBindingsAccept(); + void onKeyboardSwitchClicked(MyGUI::Widget* _sender); + void onControllerSwitchClicked(MyGUI::Widget* _sender); + + void onWindowResize(MyGUI::Window* _sender); + + void onScriptFilterChange(MyGUI::EditBox*); + void onScriptListSelection(MyGUI::ListBox*, size_t index); + + void apply(); + + void configureWidgets(MyGUI::Widget* widget, bool init); + void updateSliderLabel(MyGUI::ScrollBar* scroller, const std::string& value); + + void layoutControlsBox(); + void renderScriptSettings(); + + void computeMinimumWindowSize(); + + private: + void resetScrollbars(); }; } diff --git a/apps/openmw/mwgui/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index 28b13cdf0dd..e5b87abff73 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -1,60 +1,80 @@ #include "sortfilteritemmodel.hpp" -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwworld/action.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/nullaction.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/alchemy.hpp" +#include "../mwmechanics/spellutil.hpp" namespace { - bool compareType(const std::string& type1, const std::string& type2) + unsigned int getTypeOrder(unsigned int type) { - // this defines the sorting order of types. types that are first in the vector appear before other types. - std::vector mapping; - mapping.emplace_back(typeid(ESM::Weapon).name() ); - mapping.emplace_back(typeid(ESM::Armor).name() ); - mapping.emplace_back(typeid(ESM::Clothing).name() ); - mapping.emplace_back(typeid(ESM::Potion).name() ); - mapping.emplace_back(typeid(ESM::Ingredient).name() ); - mapping.emplace_back(typeid(ESM::Apparatus).name() ); - mapping.emplace_back(typeid(ESM::Book).name() ); - mapping.emplace_back(typeid(ESM::Light).name() ); - mapping.emplace_back(typeid(ESM::Miscellaneous).name() ); - mapping.emplace_back(typeid(ESM::Lockpick).name() ); - mapping.emplace_back(typeid(ESM::Repair).name() ); - mapping.emplace_back(typeid(ESM::Probe).name() ); - - assert( std::find(mapping.begin(), mapping.end(), type1) != mapping.end() ); - assert( std::find(mapping.begin(), mapping.end(), type2) != mapping.end() ); - - return std::find(mapping.begin(), mapping.end(), type1) < std::find(mapping.begin(), mapping.end(), type2); + switch (type) + { + case ESM::Weapon::sRecordId: + return 0; + case ESM::Armor::sRecordId: + return 1; + case ESM::Clothing::sRecordId: + return 2; + case ESM::Potion::sRecordId: + return 3; + case ESM::Ingredient::sRecordId: + return 4; + case ESM::Apparatus::sRecordId: + return 5; + case ESM::Book::sRecordId: + return 6; + case ESM::Light::sRecordId: + return 7; + case ESM::Miscellaneous::sRecordId: + return 8; + case ESM::Lockpick::sRecordId: + return 9; + case ESM::Repair::sRecordId: + return 10; + case ESM::Probe::sRecordId: + return 11; + } + assert(false && "Invalid type value"); + return std::numeric_limits::max(); + } + + bool compareType(unsigned int type1, unsigned int type2) + { + return getTypeOrder(type1) < getTypeOrder(type2); } struct Compare { bool mSortByType; - Compare() : mSortByType(true) {} - bool operator() (const MWGui::ItemStack& left, const MWGui::ItemStack& right) + Compare() + : mSortByType(true) + { + } + bool operator()(const MWGui::ItemStack& left, const MWGui::ItemStack& right) { if (mSortByType && left.mType != right.mType) return left.mType < right.mType; @@ -62,15 +82,15 @@ namespace float result = 0; // compare items by type - std::string leftName = left.mBase.getTypeName(); - std::string rightName = right.mBase.getTypeName(); + auto leftType = left.mBase.getType(); + auto rightType = right.mBase.getType(); - if (leftName != rightName) - return compareType(leftName, rightName); + if (leftType != rightType) + return compareType(leftType, rightType); // compare items by name - leftName = Misc::StringUtils::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); - rightName = Misc::StringUtils::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); + std::string leftName = Utf8Stream::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); + std::string rightName = Utf8Stream::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); result = leftName.compare(rightName); if (result != 0) @@ -82,30 +102,34 @@ namespace // 3. item with constant effect comes before items with non-constant effects int leftChargePercent = -1; int rightChargePercent = -1; - leftName = left.mBase.getClass().getEnchantment(left.mBase); - rightName = right.mBase.getClass().getEnchantment(right.mBase); + const ESM::RefId& leftNameEnch = left.mBase.getClass().getEnchantment(left.mBase); + const ESM::RefId& rightNameEnch = right.mBase.getClass().getEnchantment(right.mBase); - if (!leftName.empty()) + if (!leftNameEnch.empty()) { - const ESM::Enchantment* ench = MWBase::Environment::get().getWorld()->getStore().get().search(leftName); + const ESM::Enchantment* ench + = MWBase::Environment::get().getESMStore()->get().search(leftNameEnch); if (ench) { if (ench->mData.mType == ESM::Enchantment::ConstantEffect) leftChargePercent = 101; else - leftChargePercent = static_cast(left.mBase.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100); + leftChargePercent + = static_cast(left.mBase.getCellRef().getNormalizedEnchantmentCharge(*ench) * 100); } } - if (!rightName.empty()) + if (!rightNameEnch.empty()) { - const ESM::Enchantment* ench = MWBase::Environment::get().getWorld()->getStore().get().search(rightName); + const ESM::Enchantment* ench + = MWBase::Environment::get().getESMStore()->get().search(rightNameEnch); if (ench) { if (ench->mData.mType == ESM::Enchantment::ConstantEffect) rightChargePercent = 101; else - rightChargePercent = static_cast(right.mBase.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100); + rightChargePercent + = static_cast(right.mBase.getCellRef().getNormalizedEnchantmentCharge(*ench) * 100); } } @@ -116,13 +140,15 @@ namespace // compare items by condition if (left.mBase.getClass().hasItemHealth(left.mBase) && right.mBase.getClass().hasItemHealth(right.mBase)) { - result = left.mBase.getClass().getItemHealth(left.mBase) - right.mBase.getClass().getItemHealth(right.mBase); + result = left.mBase.getClass().getItemHealth(left.mBase) + - right.mBase.getClass().getItemHealth(right.mBase); if (result != 0) return result > 0; } // compare items by remaining usage time - result = left.mBase.getClass().getRemainingUsageTime(left.mBase) - right.mBase.getClass().getRemainingUsageTime(right.mBase); + result = left.mBase.getClass().getRemainingUsageTime(left.mBase) + - right.mBase.getClass().getRemainingUsageTime(right.mBase); if (result != 0) return result > 0; @@ -136,12 +162,7 @@ namespace if (result != 0) return result > 0; - // compare items by Id - leftName = left.mBase.getCellRef().getRefId(); - rightName = right.mBase.getCellRef().getRefId(); - - result = leftName.compare(rightName); - return result < 0; + return left.mBase.getCellRef().getRefId() < right.mBase.getCellRef().getRefId(); } }; } @@ -149,14 +170,13 @@ namespace namespace MWGui { - SortFilterItemModel::SortFilterItemModel(ItemModel *sourceModel) + SortFilterItemModel::SortFilterItemModel(std::unique_ptr sourceModel) : mCategory(Category_All) , mFilter(0) , mSortByType(true) - , mNameFilter("") - , mEffectFilter("") + , mApparatusTypeFilter(-1) { - mSourceModel = sourceModel; + mSourceModel = std::move(sourceModel); } bool SortFilterItemModel::allowedToUseItems() const @@ -164,7 +184,7 @@ namespace MWGui return mSourceModel->allowedToUseItems(); } - void SortFilterItemModel::addDragItem (const MWWorld::Ptr& dragItem, size_t count) + void SortFilterItemModel::addDragItem(const MWWorld::Ptr& dragItem, size_t count) { mDragItems.emplace_back(dragItem, count); } @@ -174,28 +194,34 @@ namespace MWGui mDragItems.clear(); } - bool SortFilterItemModel::filterAccepts (const ItemStack& item) + bool SortFilterItemModel::filterAccepts(const ItemStack& item) { MWWorld::Ptr base = item.mBase; int category = 0; - if (base.getTypeName() == typeid(ESM::Armor).name() - || base.getTypeName() == typeid(ESM::Clothing).name()) - category = Category_Apparel; - else if (base.getTypeName() == typeid(ESM::Weapon).name()) - category = Category_Weapon; - else if (base.getTypeName() == typeid(ESM::Ingredient).name() - || base.getTypeName() == typeid(ESM::Potion).name()) - category = Category_Magic; - else if (base.getTypeName() == typeid(ESM::Miscellaneous).name() - || base.getTypeName() == typeid(ESM::Ingredient).name() - || base.getTypeName() == typeid(ESM::Repair).name() - || base.getTypeName() == typeid(ESM::Lockpick).name() - || base.getTypeName() == typeid(ESM::Light).name() - || base.getTypeName() == typeid(ESM::Apparatus).name() - || base.getTypeName() == typeid(ESM::Book).name() - || base.getTypeName() == typeid(ESM::Probe).name()) - category = Category_Misc; + switch (base.getType()) + { + case ESM::Armor::sRecordId: + case ESM::Clothing::sRecordId: + category = Category_Apparel; + break; + case ESM::Weapon::sRecordId: + category = Category_Weapon; + break; + case ESM::Ingredient::sRecordId: + case ESM::Potion::sRecordId: + category = Category_Magic; + break; + case ESM::Miscellaneous::sRecordId: + case ESM::Repair::sRecordId: + case ESM::Lockpick::sRecordId: + case ESM::Light::sRecordId: + case ESM::Apparatus::sRecordId: + case ESM::Book::sRecordId: + case ESM::Probe::sRecordId: + category = Category_Misc; + break; + } if (item.mFlags & ItemStack::Flag_Enchanted) category |= Category_Magic; @@ -205,7 +231,7 @@ namespace MWGui if (mFilter & Filter_OnlyIngredients) { - if (base.getTypeName() != typeid(ESM::Ingredient).name()) + if (base.getType() != ESM::Ingredient::sRecordId) return false; if (!mNameFilter.empty() && !mEffectFilter.empty()) @@ -213,20 +239,20 @@ namespace MWGui if (!mNameFilter.empty()) { - const auto itemName = Misc::StringUtils::lowerCaseUtf8(base.getClass().getName(base)); + const auto itemName = Utf8Stream::lowerCaseUtf8(base.getClass().getName(base)); return itemName.find(mNameFilter) != std::string::npos; } if (!mEffectFilter.empty()) { - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); const auto alchemySkill = player.getClass().getSkill(player, ESM::Skill::Alchemy); const auto effects = MWMechanics::Alchemy::effectsDescription(base, alchemySkill); for (const auto& effect : effects) { - const auto ciEffect = Misc::StringUtils::lowerCaseUtf8(effect); + const auto ciEffect = Utf8Stream::lowerCaseUtf8(effect); if (ciEffect.find(mEffectFilter) != std::string::npos) return true; @@ -238,33 +264,32 @@ namespace MWGui if ((mFilter & Filter_OnlyEnchanted) && !(item.mFlags & ItemStack::Flag_Enchanted)) return false; - if ((mFilter & Filter_OnlyChargedSoulstones) && (base.getTypeName() != typeid(ESM::Miscellaneous).name() - || base.getCellRef().getSoul() == "" || !MWBase::Environment::get().getWorld()->getStore().get().search(base.getCellRef().getSoul()))) + if ((mFilter & Filter_OnlyChargedSoulstones) + && (base.getType() != ESM::Miscellaneous::sRecordId || base.getCellRef().getSoul().empty() + || !MWBase::Environment::get().getESMStore()->get().search(base.getCellRef().getSoul()))) return false; - if ((mFilter & Filter_OnlyRepairTools) && (base.getTypeName() != typeid(ESM::Repair).name())) + if ((mFilter & Filter_OnlyRepairTools) && (base.getType() != ESM::Repair::sRecordId)) return false; - if ((mFilter & Filter_OnlyEnchantable) && (item.mFlags & ItemStack::Flag_Enchanted - || (base.getTypeName() != typeid(ESM::Armor).name() - && base.getTypeName() != typeid(ESM::Clothing).name() - && base.getTypeName() != typeid(ESM::Weapon).name() - && base.getTypeName() != typeid(ESM::Book).name()))) + if ((mFilter & Filter_OnlyEnchantable) + && (item.mFlags & ItemStack::Flag_Enchanted + || (base.getType() != ESM::Armor::sRecordId && base.getType() != ESM::Clothing::sRecordId + && base.getType() != ESM::Weapon::sRecordId && base.getType() != ESM::Book::sRecordId))) return false; - if ((mFilter & Filter_OnlyEnchantable) && base.getTypeName() == typeid(ESM::Book).name() - && !base.get()->mBase->mData.mIsScroll) + if ((mFilter & Filter_OnlyEnchantable) && base.getType() == ESM::Book::sRecordId + && !base.get()->mBase->mData.mIsScroll) return false; if ((mFilter & Filter_OnlyUsableItems) && base.getClass().getScript(base).empty()) { - std::shared_ptr actionOnUse = base.getClass().use(base); + std::unique_ptr actionOnUse = base.getClass().use(base); if (!actionOnUse || actionOnUse->isNullAction()) return false; } - if ((mFilter & Filter_OnlyRepairable) && ( - !base.getClass().hasItemHealth(base) - || (base.getClass().getItemHealth(base) == base.getClass().getItemMaxHealth(base)) - || (base.getTypeName() != typeid(ESM::Weapon).name() - && base.getTypeName() != typeid(ESM::Armor).name()))) + if ((mFilter & Filter_OnlyRepairable) + && (!base.getClass().hasItemHealth(base) + || (base.getClass().getItemHealth(base) == base.getClass().getItemMaxHealth(base)) + || (base.getType() != ESM::Weapon::sRecordId && base.getType() != ESM::Armor::sRecordId))) return false; if (mFilter & Filter_OnlyRechargable) @@ -272,27 +297,39 @@ namespace MWGui if (!(item.mFlags & ItemStack::Flag_Enchanted)) return false; - std::string enchId = base.getClass().getEnchantment(base); - const ESM::Enchantment* ench = MWBase::Environment::get().getWorld()->getStore().get().search(enchId); + const ESM::RefId& enchId = base.getClass().getEnchantment(base); + const ESM::Enchantment* ench + = MWBase::Environment::get().getESMStore()->get().search(enchId); if (!ench) { - Log(Debug::Warning) << "Warning: Can't find enchantment '" << enchId << "' on item " << base.getCellRef().getRefId(); + Log(Debug::Warning) << "Warning: Can't find enchantment '" << enchId << "' on item " + << base.getCellRef().getRefId(); return false; } - if (base.getCellRef().getEnchantmentCharge() >= ench->mData.mCharge - || base.getCellRef().getEnchantmentCharge() == -1) + if (base.getCellRef().getEnchantmentCharge() == -1 + || base.getCellRef().getEnchantmentCharge() >= MWMechanics::getEnchantmentCharge(*ench)) + return false; + } + + if ((mFilter & Filter_OnlyAlchemyTools)) + { + if (base.getType() != ESM::Apparatus::sRecordId) + return false; + + int32_t apparatusType = base.get()->mBase->mData.mType; + if (mApparatusTypeFilter >= 0 && apparatusType != mApparatusTypeFilter) return false; } - std::string compare = Misc::StringUtils::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); - if(compare.find(mNameFilter) == std::string::npos) + std::string compare = Utf8Stream::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); + if (compare.find(mNameFilter) == std::string::npos) return false; return true; } - ItemStack SortFilterItemModel::getItem (ModelIndex index) + ItemStack SortFilterItemModel::getItem(ModelIndex index) { if (index < 0) throw std::runtime_error("Invalid index supplied"); @@ -306,24 +343,29 @@ namespace MWGui return mItems.size(); } - void SortFilterItemModel::setCategory (int category) + void SortFilterItemModel::setCategory(int category) { mCategory = category; } - void SortFilterItemModel::setFilter (int filter) + void SortFilterItemModel::setFilter(int filter) { mFilter = filter; } - void SortFilterItemModel::setNameFilter (const std::string& filter) + void SortFilterItemModel::setNameFilter(const std::string& filter) + { + mNameFilter = Utf8Stream::lowerCaseUtf8(filter); + } + + void SortFilterItemModel::setEffectFilter(const std::string& filter) { - mNameFilter = Misc::StringUtils::lowerCaseUtf8(filter); + mEffectFilter = Utf8Stream::lowerCaseUtf8(filter); } - void SortFilterItemModel::setEffectFilter (const std::string& filter) + void SortFilterItemModel::setApparatusTypeFilter(const int32_t type) { - mEffectFilter = Misc::StringUtils::lowerCaseUtf8(filter); + mApparatusTypeFilter = type; } void SortFilterItemModel::update() @@ -333,11 +375,12 @@ namespace MWGui size_t count = mSourceModel->getItemCount(); mItems.clear(); - for (size_t i=0; igetItem(i); - for (std::vector >::iterator it = mDragItems.begin(); it != mDragItems.end(); ++it) + for (std::vector>::iterator it = mDragItems.begin(); it != mDragItems.end(); + ++it) { if (item.mBase == it->first) { @@ -361,12 +404,12 @@ namespace MWGui mSourceModel->onClose(); } - bool SortFilterItemModel::onDropItem(const MWWorld::Ptr &item, int count) + bool SortFilterItemModel::onDropItem(const MWWorld::Ptr& item, int count) { return mSourceModel->onDropItem(item, count); } - bool SortFilterItemModel::onTakeItem(const MWWorld::Ptr &item, int count) + bool SortFilterItemModel::onTakeItem(const MWWorld::Ptr& item, int count) { return mSourceModel->onTakeItem(item, count); } diff --git a/apps/openmw/mwgui/sortfilteritemmodel.hpp b/apps/openmw/mwgui/sortfilteritemmodel.hpp index 64a01f71bda..d8490f7db12 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.hpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.hpp @@ -9,57 +9,59 @@ namespace MWGui class SortFilterItemModel : public ProxyItemModel { public: - SortFilterItemModel (ItemModel* sourceModel); + SortFilterItemModel(std::unique_ptr sourceModel); void update() override; - bool filterAccepts (const ItemStack& item); + bool filterAccepts(const ItemStack& item); bool allowedToUseItems() const override; - ItemStack getItem (ModelIndex index) override; + ItemStack getItem(ModelIndex index) override; size_t getItemCount() override; /// Dragged items are not displayed. - void addDragItem (const MWWorld::Ptr& dragItem, size_t count); + void addDragItem(const MWWorld::Ptr& dragItem, size_t count); void clearDragItems(); - void setCategory (int category); - void setFilter (int filter); - void setNameFilter (const std::string& filter); - void setEffectFilter (const std::string& filter); + void setCategory(int category); + void setFilter(int filter); + void setNameFilter(const std::string& filter); + void setEffectFilter(const std::string& filter); + void setApparatusTypeFilter(const int32_t type); /// Use ItemStack::Type for sorting? void setSortByType(bool sort) { mSortByType = sort; } void onClose() override; - bool onDropItem(const MWWorld::Ptr &item, int count) override; - bool onTakeItem(const MWWorld::Ptr &item, int count) override; + bool onDropItem(const MWWorld::Ptr& item, int count) override; + bool onTakeItem(const MWWorld::Ptr& item, int count) override; - static constexpr int Category_Weapon = (1<<1); - static constexpr int Category_Apparel = (1<<2); - static constexpr int Category_Misc = (1<<3); - static constexpr int Category_Magic = (1<<4); + static constexpr int Category_Weapon = (1 << 1); + static constexpr int Category_Apparel = (1 << 2); + static constexpr int Category_Misc = (1 << 3); + static constexpr int Category_Magic = (1 << 4); static constexpr int Category_All = 255; - static constexpr int Filter_OnlyIngredients = (1<<0); - static constexpr int Filter_OnlyEnchanted = (1<<1); - static constexpr int Filter_OnlyEnchantable = (1<<2); - static constexpr int Filter_OnlyChargedSoulstones = (1<<3); - static constexpr int Filter_OnlyUsableItems = (1<<4); // Only items with a Use action - static constexpr int Filter_OnlyRepairable = (1<<5); - static constexpr int Filter_OnlyRechargable = (1<<6); - static constexpr int Filter_OnlyRepairTools = (1<<7); - + static constexpr int Filter_OnlyIngredients = (1 << 0); + static constexpr int Filter_OnlyEnchanted = (1 << 1); + static constexpr int Filter_OnlyEnchantable = (1 << 2); + static constexpr int Filter_OnlyChargedSoulstones = (1 << 3); + static constexpr int Filter_OnlyUsableItems = (1 << 4); // Only items with a Use action + static constexpr int Filter_OnlyRepairable = (1 << 5); + static constexpr int Filter_OnlyRechargable = (1 << 6); + static constexpr int Filter_OnlyRepairTools = (1 << 7); + static constexpr int Filter_OnlyAlchemyTools = (1 << 8); private: std::vector mItems; - std::vector > mDragItems; + std::vector> mDragItems; int mCategory; int mFilter; bool mSortByType; + int32_t mApparatusTypeFilter; // filter by apparatus type std::string mNameFilter; // filter by item name std::string mEffectFilter; // filter by magic effect }; diff --git a/apps/openmw/mwgui/soulgemdialog.cpp b/apps/openmw/mwgui/soulgemdialog.cpp index 345c8b7221e..8aaf182159f 100644 --- a/apps/openmw/mwgui/soulgemdialog.cpp +++ b/apps/openmw/mwgui/soulgemdialog.cpp @@ -8,7 +8,7 @@ namespace MWGui { - void SoulgemDialog::show(const MWWorld::Ptr &soulgem) + void SoulgemDialog::show(const MWWorld::Ptr& soulgem) { mSoulgem = soulgem; std::vector buttons; diff --git a/apps/openmw/mwgui/soulgemdialog.hpp b/apps/openmw/mwgui/soulgemdialog.hpp index 9aea1f33933..775378f763c 100644 --- a/apps/openmw/mwgui/soulgemdialog.hpp +++ b/apps/openmw/mwgui/soulgemdialog.hpp @@ -11,10 +11,12 @@ namespace MWGui class SoulgemDialog { public: - SoulgemDialog (MessageBoxManager* manager) - : mManager(manager) {} + SoulgemDialog(MessageBoxManager* manager) + : mManager(manager) + { + } - void show (const MWWorld::Ptr& soulgem); + void show(const MWWorld::Ptr& soulgem); void onButtonPressed(int button); diff --git a/apps/openmw/mwgui/spellbuyingwindow.cpp b/apps/openmw/mwgui/spellbuyingwindow.cpp index eb51f560be6..9fca86caba2 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.cpp +++ b/apps/openmw/mwgui/spellbuyingwindow.cpp @@ -1,25 +1,29 @@ #include "spellbuyingwindow.hpp" -#include #include +#include #include +#include +#include +#include + #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/spells.hpp" namespace MWGui { - SpellBuyingWindow::SpellBuyingWindow() : - WindowBase("openmw_spell_buying_window.layout") + SpellBuyingWindow::SpellBuyingWindow() + : WindowBase("openmw_spell_buying_window.layout") , mCurrentY(0) { getWidget(mCancelButton, "CancelButton"); @@ -29,96 +33,89 @@ namespace MWGui mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellBuyingWindow::onCancelButtonClicked); } - bool SpellBuyingWindow::sortSpells (const ESM::Spell* left, const ESM::Spell* right) + bool SpellBuyingWindow::sortSpells(const ESM::Spell* left, const ESM::Spell* right) { - std::string leftName = Misc::StringUtils::lowerCase(left->mName); - std::string rightName = Misc::StringUtils::lowerCase(right->mName); - - return leftName.compare(rightName) < 0; + return Misc::StringUtils::ciLess(left->mName, right->mName); } void SpellBuyingWindow::addSpell(const ESM::Spell& spell) { - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - int price = std::max(1, static_cast(spell.mData.mCost*store.get().find("fSpellValueMult")->mValue.getFloat())); - price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr,price,true); + int price = std::max(1, + static_cast( + spell.mData.mCost * store.get().find("fSpellValueMult")->mValue.getFloat())); + price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); // TODO: refactor to use MyGUI::ListBox - int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; + const int lineHeight = Settings::gui().mFontSize + 2; - MyGUI::Button* toAdd = - mSpellsView->createWidget( - price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip - 0, - mCurrentY, - 200, - lineHeight, - MyGUI::Align::Default - ); + MyGUI::Button* toAdd = mSpellsView->createWidget(price <= playerGold + ? "SandTextButton" + : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip + 0, mCurrentY, 200, lineHeight, MyGUI::Align::Default); 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"); - toAdd->setUserString("Spell", spell.mId); + toAdd->setUserString("Spell", spell.mId.serialize()); toAdd->setUserString("SpellCost", std::to_string(spell.mData.mCost)); toAdd->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellBuyingWindow::onSpellButtonClick); - mSpellsWidgetMap.insert(std::make_pair (toAdd, spell.mId)); + mSpellsWidgetMap.insert(std::make_pair(toAdd, spell.mId)); } void SpellBuyingWindow::clearSpells() { - mSpellsView->setViewOffset(MyGUI::IntPoint(0,0)); + mSpellsView->setViewOffset(MyGUI::IntPoint(0, 0)); mCurrentY = 0; while (mSpellsView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mSpellsView->getChildAt(0)); mSpellsWidgetMap.clear(); } - void SpellBuyingWindow::setPtr(const MWWorld::Ptr &actor) + void SpellBuyingWindow::setPtr(const MWWorld::Ptr& actor) { setPtr(actor, 0); } void SpellBuyingWindow::setPtr(const MWWorld::Ptr& actor, int startOffset) { + if (actor.isEmpty() || !actor.getClass().isActor()) + throw std::runtime_error("Invalid argument in SpellBuyingWindow::setPtr"); + center(); mPtr = actor; clearSpells(); - MWMechanics::Spells& merchantSpells = actor.getClass().getCreatureStats (actor).getSpells(); + MWMechanics::Spells& merchantSpells = actor.getClass().getCreatureStats(actor).getSpells(); std::vector spellsToSort; - for (MWMechanics::Spells::TIterator iter = merchantSpells.begin(); iter!=merchantSpells.end(); ++iter) + for (const ESM::Spell* spell : merchantSpells) { - const ESM::Spell* spell = iter->first; - - if (spell->mData.mType!=ESM::Spell::ST_Spell) + if (spell->mData.mType != ESM::Spell::ST_Spell) continue; // don't try to sell diseases, curses or powers if (actor.getClass().isNpc()) { - const ESM::Race* race = - MWBase::Environment::get().getWorld()->getStore().get().find( - actor.get()->mBase->mRace); + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find( + actor.get()->mBase->mRace); if (race->mPowers.exists(spell->mId)) continue; } - if (playerHasSpell(iter->first->mId)) + if (playerHasSpell(spell->mId)) continue; - spellsToSort.push_back(iter->first); + spellsToSort.push_back(spell); } std::stable_sort(spellsToSort.begin(), spellsToSort.end(), sortSpells); @@ -132,14 +129,16 @@ namespace MWGui updateLabels(); - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden mSpellsView->setVisibleVScroll(false); - mSpellsView->setCanvasSize (MyGUI::IntSize(mSpellsView->getWidth(), std::max(mSpellsView->getHeight(), mCurrentY))); + mSpellsView->setCanvasSize( + MyGUI::IntSize(mSpellsView->getWidth(), std::max(mSpellsView->getHeight(), mCurrentY))); mSpellsView->setVisibleVScroll(true); mSpellsView->setViewOffset(MyGUI::IntPoint(0, startOffset)); } - bool SpellBuyingWindow::playerHasSpell(const std::string &id) + bool SpellBuyingWindow::playerHasSpell(const ESM::RefId& id) { MWWorld::Ptr player = MWMechanics::getPlayer(); return player.getClass().getCreatureStats(player).getSpells().hasSpell(id); @@ -155,8 +154,11 @@ namespace MWGui MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); - spells.add (mSpellsWidgetMap.find(_sender)->second); - player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + auto spell = mSpellsWidgetMap.find(_sender); + assert(spell != mSpellsWidgetMap.end()); + + spells.add(spell->second); + player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price); // add gold to NPC trading gold pool MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); @@ -164,12 +166,12 @@ namespace MWGui setPtr(mPtr, mSpellsView->getViewOffset().top); - MWBase::Environment::get().getWindowManager()->playSound("Item Gold Up"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Gold Up")); } void SpellBuyingWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_SpellBuying); + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_SpellBuying); } void SpellBuyingWindow::updateLabels() @@ -178,25 +180,23 @@ namespace MWGui int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); mPlayerGold->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); - mPlayerGold->setCoord(8, - mPlayerGold->getTop(), - mPlayerGold->getTextSize().width, - mPlayerGold->getHeight()); + mPlayerGold->setCoord(8, mPlayerGold->getTop(), mPlayerGold->getTextSize().width, mPlayerGold->getHeight()); } void SpellBuyingWindow::onReferenceUnavailable() { - // remove both Spells and Dialogue (since you always trade with the NPC/creature that you have previously talked to) + // remove both Spells and Dialogue (since you always trade with the NPC/creature that you have previously talked + // to) MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_SpellBuying); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } void SpellBuyingWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { - if (mSpellsView->getViewOffset().top + _rel*0.3 > 0) + if (mSpellsView->getViewOffset().top + _rel * 0.3 > 0) mSpellsView->setViewOffset(MyGUI::IntPoint(0, 0)); else - mSpellsView->setViewOffset(MyGUI::IntPoint(0, static_cast(mSpellsView->getViewOffset().top + _rel*0.3f))); + mSpellsView->setViewOffset( + MyGUI::IntPoint(0, static_cast(mSpellsView->getViewOffset().top + _rel * 0.3f))); } } - diff --git a/apps/openmw/mwgui/spellbuyingwindow.hpp b/apps/openmw/mwgui/spellbuyingwindow.hpp index f46c4379631..257b8a0df91 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.hpp +++ b/apps/openmw/mwgui/spellbuyingwindow.hpp @@ -1,9 +1,9 @@ #ifndef MWGUI_SpellBuyingWINDOW_H #define MWGUI_SpellBuyingWINDOW_H -#include "windowbase.hpp" #include "referenceinterface.hpp" - +#include "windowbase.hpp" +#include namespace ESM { struct Spell; @@ -11,48 +11,50 @@ namespace ESM namespace MyGUI { - class Gui; - class Widget; + class Gui; + class Widget; } namespace MWGui { class SpellBuyingWindow : public ReferenceInterface, public WindowBase { - public: - SpellBuyingWindow(); + public: + SpellBuyingWindow(); + + void setPtr(const MWWorld::Ptr& actor) override; + void setPtr(const MWWorld::Ptr& actor, int startOffset); - void setPtr(const MWWorld::Ptr& actor) override; - void setPtr(const MWWorld::Ptr& actor, int startOffset); + void onFrame(float dt) override { checkReferenceAvailable(); } + void clear() override { resetReference(); } - void onFrame(float dt) override { checkReferenceAvailable(); } - void clear() override { resetReference(); } + void onResChange(int, int) override { center(); } - void onResChange(int, int) override { center(); } + std::string_view getWindowIdForLua() const override { return "SpellBuying"; } - protected: - MyGUI::Button* mCancelButton; - MyGUI::TextBox* mPlayerGold; + protected: + MyGUI::Button* mCancelButton; + MyGUI::TextBox* mPlayerGold; - MyGUI::ScrollView* mSpellsView; + MyGUI::ScrollView* mSpellsView; - std::map mSpellsWidgetMap; + std::map mSpellsWidgetMap; - void onCancelButtonClicked(MyGUI::Widget* _sender); - void onSpellButtonClick(MyGUI::Widget* _sender); - void onMouseWheel(MyGUI::Widget* _sender, int _rel); - void addSpell(const ESM::Spell& spell); - void clearSpells(); - int mCurrentY; + void onCancelButtonClicked(MyGUI::Widget* _sender); + void onSpellButtonClick(MyGUI::Widget* _sender); + void onMouseWheel(MyGUI::Widget* _sender, int _rel); + void addSpell(const ESM::Spell& spell); + void clearSpells(); + int mCurrentY; - void updateLabels(); + void updateLabels(); - void onReferenceUnavailable() override; + void onReferenceUnavailable() override; - bool playerHasSpell (const std::string& id); + bool playerHasSpell(const ESM::RefId& id); - private: - static bool sortSpells (const ESM::Spell* left, const ESM::Spell* right); + private: + static bool sortSpells(const ESM::Spell* left, const ESM::Spell* right); }; } diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 5a5dec60f7c..d8302df87c3 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -1,39 +1,43 @@ #include "spellcreationdialog.hpp" -#include +#include #include +#include +#include -#include +#include +#include #include -#include "../mwbase/windowmanager.hpp" -#include "../mwbase/mechanicsmanager.hpp" +#include + #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" -#include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/store.hpp" -#include "../mwmechanics/spells.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellutil.hpp" -#include "tooltips.hpp" #include "class.hpp" +#include "tooltips.hpp" #include "widgets.hpp" namespace { - bool sortMagicEffects (short id1, short id2) + bool sortMagicEffects(short id1, short id2) { - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); - return gmst.find(ESM::MagicEffect::effectIdToString (id1))->mValue.getString() - < gmst.find(ESM::MagicEffect::effectIdToString (id2))->mValue.getString(); + return gmst.find(ESM::MagicEffect::indexToGmstString(id1))->mValue.getString() + < gmst.find(ESM::MagicEffect::indexToGmstString(id2))->mValue.getString(); } void init(ESM::ENAMstruct& effect) @@ -85,8 +89,10 @@ namespace MWGui mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditEffectDialog::onCancelButtonClicked); mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditEffectDialog::onDeleteButtonClicked); - mMagnitudeMinSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onMagnitudeMinChanged); - mMagnitudeMaxSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onMagnitudeMaxChanged); + mMagnitudeMinSlider->eventScrollChangePosition + += MyGUI::newDelegate(this, &EditEffectDialog::onMagnitudeMinChanged); + mMagnitudeMaxSlider->eventScrollChangePosition + += MyGUI::newDelegate(this, &EditEffectDialog::onMagnitudeMaxChanged); mDurationSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onDurationChanged); mAreaSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onAreaChanged); } @@ -104,26 +110,22 @@ namespace MWGui bool EditEffectDialog::exit() { - if(mEditing) + if (mEditing) eventEffectModified(mOldEffect); else eventEffectRemoved(mEffect); return true; } - void EditEffectDialog::newEffect (const ESM::MagicEffect *effect) + void EditEffectDialog::newEffect(const ESM::MagicEffect* effect) { - bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; + bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0 || mConstantEffect; bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; - bool allowTarget = (effect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; - - if (!allowSelf && !allowTouch && !allowTarget) - return; // TODO: Show an error message popup? setMagicEffect(effect); mEditing = false; - mDeleteButton->setVisible (false); + mDeleteButton->setVisible(false); mEffect.mRange = ESM::RT_Self; if (!allowSelf) @@ -140,14 +142,14 @@ namespace MWGui onRangeButtonClicked(mRangeButton); - mMagnitudeMinSlider->setScrollPosition (0); - mMagnitudeMaxSlider->setScrollPosition (0); - mAreaSlider->setScrollPosition (0); - mDurationSlider->setScrollPosition (0); + mMagnitudeMinSlider->setScrollPosition(0); + mMagnitudeMaxSlider->setScrollPosition(0); + mAreaSlider->setScrollPosition(0); + mDurationSlider->setScrollPosition(0); mDurationValue->setCaption("1"); mMagnitudeMinValue->setCaption("1"); - const std::string to = MWBase::Environment::get().getWindowManager()->getGameSettingString("sTo", "-"); + const std::string to{ MWBase::Environment::get().getWindowManager()->getGameSettingString("sTo", "-") }; mMagnitudeMaxValue->setCaption(to + " 1"); mAreaValue->setCaption("0"); @@ -155,44 +157,45 @@ namespace MWGui setVisible(true); } - void EditEffectDialog::editEffect (ESM::ENAMstruct effect) + void EditEffectDialog::editEffect(ESM::ENAMstruct effect) { - const ESM::MagicEffect* magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); + const ESM::MagicEffect* magicEffect + = MWBase::Environment::get().getESMStore()->get().find(effect.mEffectID); setMagicEffect(magicEffect); mOldEffect = effect; mEffect = effect; mEditing = true; - mDeleteButton->setVisible (true); + mDeleteButton->setVisible(true); - mMagnitudeMinSlider->setScrollPosition (effect.mMagnMin-1); - mMagnitudeMaxSlider->setScrollPosition (effect.mMagnMax-1); - mAreaSlider->setScrollPosition (effect.mArea); - mDurationSlider->setScrollPosition (effect.mDuration-1); + mMagnitudeMinSlider->setScrollPosition(effect.mMagnMin - 1); + mMagnitudeMaxSlider->setScrollPosition(effect.mMagnMax - 1); + mAreaSlider->setScrollPosition(effect.mArea); + mDurationSlider->setScrollPosition(effect.mDuration - 1); if (mEffect.mRange == ESM::RT_Self) - mRangeButton->setCaptionWithReplacing ("#{sRangeSelf}"); + mRangeButton->setCaptionWithReplacing("#{sRangeSelf}"); else if (mEffect.mRange == ESM::RT_Target) - mRangeButton->setCaptionWithReplacing ("#{sRangeTarget}"); + mRangeButton->setCaptionWithReplacing("#{sRangeTarget}"); else if (mEffect.mRange == ESM::RT_Touch) - mRangeButton->setCaptionWithReplacing ("#{sRangeTouch}"); + mRangeButton->setCaptionWithReplacing("#{sRangeTouch}"); - onMagnitudeMinChanged (mMagnitudeMinSlider, effect.mMagnMin-1); - onMagnitudeMaxChanged (mMagnitudeMinSlider, effect.mMagnMax-1); - onAreaChanged (mAreaSlider, effect.mArea); - onDurationChanged (mDurationSlider, effect.mDuration-1); + onMagnitudeMinChanged(mMagnitudeMinSlider, effect.mMagnMin - 1); + onMagnitudeMaxChanged(mMagnitudeMinSlider, effect.mMagnMax - 1); + onAreaChanged(mAreaSlider, effect.mArea); + onDurationChanged(mDurationSlider, effect.mDuration - 1); eventEffectModified(mEffect); updateBoxes(); } - void EditEffectDialog::setMagicEffect (const ESM::MagicEffect *effect) + void EditEffectDialog::setMagicEffect(const ESM::MagicEffect* effect) { - mEffectImage->setImageTexture(MWBase::Environment::get().getWindowManager()->correctIconPath(effect->mIcon)); + mEffectImage->setImageTexture(Misc::ResourceHelpers::correctIconPath( + effect->mIcon, MWBase::Environment::get().getResourceSystem()->getVFS())); - mEffectName->setCaptionWithReplacing("#{"+ESM::MagicEffect::effectIdToString (effect->mIndex)+"}"); + mEffectName->setCaptionWithReplacing("#{" + ESM::MagicEffect::indexToGmstString(effect->mIndex) + "}"); mEffect.mEffectID = effect->mIndex; @@ -206,129 +209,131 @@ namespace MWGui static int startY = mMagnitudeBox->getPosition().top; int curY = startY; - mMagnitudeBox->setVisible (false); - mDurationBox->setVisible (false); - mAreaBox->setVisible (false); + mMagnitudeBox->setVisible(false); + mDurationBox->setVisible(false); + mAreaBox->setVisible(false); if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) { mMagnitudeBox->setPosition(mMagnitudeBox->getPosition().left, curY); - mMagnitudeBox->setVisible (true); + mMagnitudeBox->setVisible(true); curY += mMagnitudeBox->getSize().height; } - if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)&&mConstantEffect==false) + if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) && mConstantEffect == false) { mDurationBox->setPosition(mDurationBox->getPosition().left, curY); - mDurationBox->setVisible (true); + mDurationBox->setVisible(true); curY += mDurationBox->getSize().height; } if (mEffect.mRange != ESM::RT_Self) { mAreaBox->setPosition(mAreaBox->getPosition().left, curY); - mAreaBox->setVisible (true); - //curY += mAreaBox->getSize().height; + mAreaBox->setVisible(true); + // curY += mAreaBox->getSize().height; } } - void EditEffectDialog::onRangeButtonClicked (MyGUI::Widget* sender) + void EditEffectDialog::onRangeButtonClicked(MyGUI::Widget* sender) { - mEffect.mRange = (mEffect.mRange+1)%3; + mEffect.mRange = (mEffect.mRange + 1) % 3; // cycle through range types until we find something that's allowed - // does not handle the case where nothing is allowed (this should be prevented before opening the Add Effect dialog) - bool allowSelf = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0; + // does not handle the case where nothing is allowed (this should be prevented before opening the Add Effect + // dialog) + bool allowSelf = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0 || mConstantEffect; bool allowTouch = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; bool allowTarget = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; if (mEffect.mRange == ESM::RT_Self && !allowSelf) - mEffect.mRange = (mEffect.mRange+1)%3; + mEffect.mRange = (mEffect.mRange + 1) % 3; if (mEffect.mRange == ESM::RT_Touch && !allowTouch) - mEffect.mRange = (mEffect.mRange+1)%3; + mEffect.mRange = (mEffect.mRange + 1) % 3; if (mEffect.mRange == ESM::RT_Target && !allowTarget) - mEffect.mRange = (mEffect.mRange+1)%3; + mEffect.mRange = (mEffect.mRange + 1) % 3; - if(mEffect.mRange == ESM::RT_Self) + if (mEffect.mRange == ESM::RT_Self) { mAreaSlider->setScrollPosition(0); - onAreaChanged(mAreaSlider,0); + onAreaChanged(mAreaSlider, 0); } if (mEffect.mRange == ESM::RT_Self) - mRangeButton->setCaptionWithReplacing ("#{sRangeSelf}"); + mRangeButton->setCaptionWithReplacing("#{sRangeSelf}"); else if (mEffect.mRange == ESM::RT_Target) - mRangeButton->setCaptionWithReplacing ("#{sRangeTarget}"); + mRangeButton->setCaptionWithReplacing("#{sRangeTarget}"); else if (mEffect.mRange == ESM::RT_Touch) - mRangeButton->setCaptionWithReplacing ("#{sRangeTouch}"); + mRangeButton->setCaptionWithReplacing("#{sRangeTouch}"); updateBoxes(); eventEffectModified(mEffect); } - void EditEffectDialog::onDeleteButtonClicked (MyGUI::Widget* sender) + void EditEffectDialog::onDeleteButtonClicked(MyGUI::Widget* sender) { setVisible(false); eventEffectRemoved(mEffect); } - void EditEffectDialog::onOkButtonClicked (MyGUI::Widget* sender) + void EditEffectDialog::onOkButtonClicked(MyGUI::Widget* sender) { setVisible(false); } - void EditEffectDialog::onCancelButtonClicked (MyGUI::Widget* sender) + void EditEffectDialog::onCancelButtonClicked(MyGUI::Widget* sender) { setVisible(false); exit(); } - void EditEffectDialog::setSkill (int skill) + void EditEffectDialog::setSkill(ESM::RefId skill) { - mEffect.mSkill = skill; + mEffect.mSkill = ESM::Skill::refIdToIndex(skill); eventEffectModified(mEffect); } - void EditEffectDialog::setAttribute (int attribute) + void EditEffectDialog::setAttribute(ESM::RefId attribute) { - mEffect.mAttribute = attribute; + mEffect.mAttribute = ESM::Attribute::refIdToIndex(attribute); eventEffectModified(mEffect); } - void EditEffectDialog::onMagnitudeMinChanged (MyGUI::ScrollBar* sender, size_t pos) + void EditEffectDialog::onMagnitudeMinChanged(MyGUI::ScrollBar* sender, size_t pos) { - mMagnitudeMinValue->setCaption(MyGUI::utility::toString(pos+1)); - mEffect.mMagnMin = pos+1; + mMagnitudeMinValue->setCaption(MyGUI::utility::toString(pos + 1)); + mEffect.mMagnMin = pos + 1; // trigger the check again (see below) - onMagnitudeMaxChanged(mMagnitudeMaxSlider, mMagnitudeMaxSlider->getScrollPosition ()); + onMagnitudeMaxChanged(mMagnitudeMaxSlider, mMagnitudeMaxSlider->getScrollPosition()); eventEffectModified(mEffect); } - void EditEffectDialog::onMagnitudeMaxChanged (MyGUI::ScrollBar* sender, size_t pos) + void EditEffectDialog::onMagnitudeMaxChanged(MyGUI::ScrollBar* sender, size_t pos) { // make sure the max value is actually larger or equal than the min value - size_t magnMin = std::abs(mEffect.mMagnMin); // should never be < 0, this is just here to avoid the compiler warning - if (pos+1 < magnMin) + size_t magnMin + = std::abs(mEffect.mMagnMin); // should never be < 0, this is just here to avoid the compiler warning + if (pos + 1 < magnMin) { - pos = mEffect.mMagnMin-1; - sender->setScrollPosition (pos); + pos = mEffect.mMagnMin - 1; + sender->setScrollPosition(pos); } - mEffect.mMagnMax = pos+1; - const std::string to = MWBase::Environment::get().getWindowManager()->getGameSettingString("sTo", "-"); + mEffect.mMagnMax = pos + 1; + const std::string to{ MWBase::Environment::get().getWindowManager()->getGameSettingString("sTo", "-") }; - mMagnitudeMaxValue->setCaption(to + " " + MyGUI::utility::toString(pos+1)); + mMagnitudeMaxValue->setCaption(to + " " + MyGUI::utility::toString(pos + 1)); eventEffectModified(mEffect); } - void EditEffectDialog::onDurationChanged (MyGUI::ScrollBar* sender, size_t pos) + void EditEffectDialog::onDurationChanged(MyGUI::ScrollBar* sender, size_t pos) { - mDurationValue->setCaption(MyGUI::utility::toString(pos+1)); - mEffect.mDuration = pos+1; + mDurationValue->setCaption(MyGUI::utility::toString(pos + 1)); + mEffect.mDuration = pos + 1; eventEffectModified(mEffect); } - void EditEffectDialog::onAreaChanged (MyGUI::ScrollBar* sender, size_t pos) + void EditEffectDialog::onAreaChanged(MyGUI::ScrollBar* sender, size_t pos) { mAreaValue->setCaption(MyGUI::utility::toString(pos)); mEffect.mArea = pos; @@ -357,36 +362,39 @@ namespace MWGui setWidgets(mAvailableEffectsList, mUsedEffectsView); } - void SpellCreationDialog::setPtr (const MWWorld::Ptr& actor) + void SpellCreationDialog::setPtr(const MWWorld::Ptr& actor) { + if (actor.isEmpty() || !actor.getClass().isActor()) + throw std::runtime_error("Invalid argument in SpellCreationDialog::setPtr"); + mPtr = actor; - mNameEdit->setCaption(""); + mNameEdit->setCaption({}); startEditing(); } - void SpellCreationDialog::onCancelButtonClicked (MyGUI::Widget* sender) + void SpellCreationDialog::onCancelButtonClicked(MyGUI::Widget* sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_SpellCreation); + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_SpellCreation); } - void SpellCreationDialog::onBuyButtonClicked (MyGUI::Widget* sender) + void SpellCreationDialog::onBuyButtonClicked(MyGUI::Widget* sender) { if (mEffects.size() <= 0) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage30}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage30}"); return; } - if (mNameEdit->getCaption () == "") + if (mNameEdit->getCaption().empty()) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage10}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage10}"); return; } if (mMagickaCost->getCaption() == "0") { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sEnchantmentMenu8}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sEnchantmentMenu8}"); return; } @@ -396,30 +404,30 @@ namespace MWGui int price = MyGUI::utility::parseInt(mPriceLabel->getCaption()); if (price > playerGold) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage18}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage18}"); return; } mSpell.mName = mNameEdit->getCaption(); - player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price); // add gold to NPC trading gold pool MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); npcStats.setGoldPool(npcStats.getGoldPool() + price); - MWBase::Environment::get().getWindowManager()->playSound ("Mysticism Hit"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Mysticism Hit")); - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->createRecord(mSpell); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->insert(mSpell); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); - spells.add (spell->mId); + spells.add(spell->mId); - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_SpellCreation); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_SpellCreation); } - void SpellCreationDialog::onAccept(MyGUI::EditBox *sender) + void SpellCreationDialog::onAccept(MyGUI::EditBox* sender) { onBuyButtonClicked(sender); @@ -433,13 +441,13 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mNameEdit); } - void SpellCreationDialog::onReferenceUnavailable () + void SpellCreationDialog::onReferenceUnavailable() { - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Dialogue); - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_SpellCreation); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_SpellCreation); } - void SpellCreationDialog::notifyEffectsChanged () + void SpellCreationDialog::notifyEffectsChanged() { if (mEffects.empty()) { @@ -451,28 +459,25 @@ namespace MWGui float y = 0; - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); for (const ESM::ENAMstruct& effect : mEffects) { - y += std::max(1.f, MWMechanics::calcEffectCost(effect)); + y += std::max( + 1.f, MWMechanics::calcEffectCost(effect, nullptr, MWMechanics::EffectCostMethod::PlayerSpell)); if (effect.mRange == ESM::RT_Target) y *= 1.5; } - ESM::EffectList effectList; - effectList.mList = mEffects; - mSpell.mEffects = effectList; + mSpell.mEffects.populate(mEffects); mSpell.mData.mCost = int(y); mSpell.mData.mType = ESM::Spell::ST_Spell; mSpell.mData.mFlags = 0; mMagickaCost->setCaption(MyGUI::utility::toString(int(y))); - float fSpellMakingValueMult = - store.get().find("fSpellMakingValueMult")->mValue.getFloat(); + float fSpellMakingValueMult = store.get().find("fSpellMakingValueMult")->mValue.getFloat(); int price = std::max(1, static_cast(y * fSpellMakingValueMult)); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); @@ -487,13 +492,10 @@ namespace MWGui // ------------------------------------------------------------------------------------------------ - EffectEditorBase::EffectEditorBase(Type type) : mAvailableEffectsList(nullptr) , mUsedEffectsView(nullptr) , mAddEffectDialog() - , mSelectAttributeDialog(nullptr) - , mSelectSkillDialog(nullptr) , mSelectedEffect(0) , mSelectedKnownEffectId(0) , mConstantEffect(false) @@ -503,14 +505,12 @@ namespace MWGui mAddEffectDialog.eventEffectModified += MyGUI::newDelegate(this, &EffectEditorBase::onEffectModified); mAddEffectDialog.eventEffectRemoved += MyGUI::newDelegate(this, &EffectEditorBase::onEffectRemoved); - mAddEffectDialog.setVisible (false); + mAddEffectDialog.setVisible(false); } - EffectEditorBase::~EffectEditorBase() - { - } + EffectEditorBase::~EffectEditorBase() {} - void EffectEditorBase::startEditing () + void EffectEditorBase::startEditing() { // get the list of magic effects that are known to the player @@ -520,98 +520,101 @@ namespace MWGui std::vector knownEffects; - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; - // only normal spells count if (spell->mData.mType != ESM::Spell::ST_Spell) continue; - for (const ESM::ENAMstruct& effectInfo : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& effectInfo : spell->mEffects.mList) { - const ESM::MagicEffect * effect = MWBase::Environment::get().getWorld()->getStore().get().find(effectInfo.mEffectID); + int16_t effectId = effectInfo.mData.mEffectID; + const ESM::MagicEffect* effect + = MWBase::Environment::get().getESMStore()->get().find(effectId); // skip effects that do not allow spellmaking/enchanting - int requiredFlags = (mType == Spellmaking) ? ESM::MagicEffect::AllowSpellmaking : ESM::MagicEffect::AllowEnchanting; + int requiredFlags + = (mType == Spellmaking) ? ESM::MagicEffect::AllowSpellmaking : ESM::MagicEffect::AllowEnchanting; if (!(effect->mData.mFlags & requiredFlags)) continue; - if (std::find(knownEffects.begin(), knownEffects.end(), effectInfo.mEffectID) == knownEffects.end()) - knownEffects.push_back(effectInfo.mEffectID); + if (std::find(knownEffects.begin(), knownEffects.end(), effectId) == knownEffects.end()) + knownEffects.push_back(effectId); } } std::sort(knownEffects.begin(), knownEffects.end(), sortMagicEffects); - mAvailableEffectsList->clear (); + mAvailableEffectsList->clear(); - int i=0; + int i = 0; for (const short effectId : knownEffects) { - mAvailableEffectsList->addItem(MWBase::Environment::get().getWorld ()->getStore ().get().find( - ESM::MagicEffect::effectIdToString(effectId))->mValue.getString()); + mAvailableEffectsList->addItem(MWBase::Environment::get() + .getESMStore() + ->get() + .find(ESM::MagicEffect::indexToGmstString(effectId)) + ->mValue.getString()); mButtonMapping[i] = effectId; ++i; } - mAvailableEffectsList->adjustSize (); + mAvailableEffectsList->adjustSize(); mAvailableEffectsList->scrollToTop(); for (const short effectId : knownEffects) { - std::string name = MWBase::Environment::get().getWorld ()->getStore ().get().find( - ESM::MagicEffect::effectIdToString(effectId))->mValue.getString(); + const std::string& name = MWBase::Environment::get() + .getESMStore() + ->get() + .find(ESM::MagicEffect::indexToGmstString(effectId)) + ->mValue.getString(); MyGUI::Widget* w = mAvailableEffectsList->getItemWidget(name); - ToolTips::createMagicEffectToolTip (w, effectId); + ToolTips::createMagicEffectToolTip(w, effectId); } mEffects.clear(); - updateEffectsView (); + updateEffectsView(); } - void EffectEditorBase::setWidgets (Gui::MWList *availableEffectsList, MyGUI::ScrollView *usedEffectsView) + void EffectEditorBase::setWidgets(Gui::MWList* availableEffectsList, MyGUI::ScrollView* usedEffectsView) { mAvailableEffectsList = availableEffectsList; mUsedEffectsView = usedEffectsView; - mAvailableEffectsList->eventWidgetSelected += MyGUI::newDelegate(this, &EffectEditorBase::onAvailableEffectClicked); + mAvailableEffectsList->eventWidgetSelected + += MyGUI::newDelegate(this, &EffectEditorBase::onAvailableEffectClicked); } - void EffectEditorBase::onSelectAttribute () + void EffectEditorBase::onSelectAttribute() { - const ESM::MagicEffect* effect = - MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); + const ESM::MagicEffect* effect + = MWBase::Environment::get().getESMStore()->get().find(mSelectedKnownEffectId); mAddEffectDialog.newEffect(effect); - mAddEffectDialog.setAttribute (mSelectAttributeDialog->getAttributeId()); - MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectAttributeDialog); - mSelectAttributeDialog = nullptr; + mAddEffectDialog.setAttribute(mSelectAttributeDialog->getAttributeId()); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSelectAttributeDialog)); } - void EffectEditorBase::onSelectSkill () + void EffectEditorBase::onSelectSkill() { - const ESM::MagicEffect* effect = - MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); + const ESM::MagicEffect* effect + = MWBase::Environment::get().getESMStore()->get().find(mSelectedKnownEffectId); mAddEffectDialog.newEffect(effect); - mAddEffectDialog.setSkill (mSelectSkillDialog->getSkillId()); - MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectSkillDialog); - mSelectSkillDialog = nullptr; + mAddEffectDialog.setSkill(mSelectSkillDialog->getSkillId()); + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSelectSkillDialog)); } - void EffectEditorBase::onAttributeOrSkillCancel () + void EffectEditorBase::onAttributeOrSkillCancel() { - if (mSelectSkillDialog) - MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectSkillDialog); - if (mSelectAttributeDialog) - MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectAttributeDialog); - - mSelectSkillDialog = nullptr; - mSelectAttributeDialog = nullptr; + if (mSelectSkillDialog != nullptr) + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSelectSkillDialog)); + if (mSelectAttributeDialog != nullptr) + MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSelectAttributeDialog)); } - void EffectEditorBase::onAvailableEffectClicked (MyGUI::Widget* sender) + void EffectEditorBase::onAvailableEffectClicked(MyGUI::Widget* sender) { if (mEffects.size() >= 8) { @@ -622,24 +625,31 @@ namespace MWGui int buttonId = *sender->getUserData(); mSelectedKnownEffectId = mButtonMapping[buttonId]; - const ESM::MagicEffect* effect = - MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); + const ESM::MagicEffect* effect + = MWBase::Environment::get().getESMStore()->get().find(mSelectedKnownEffectId); + + bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0 || mConstantEffect; + bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; + bool allowTarget = (effect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; + + if (!allowSelf && !allowTouch && !allowTarget) + return; // TODO: Show an error message popup? if (effect->mData.mFlags & ESM::MagicEffect::TargetSkill) { - delete mSelectSkillDialog; - mSelectSkillDialog = new SelectSkillDialog(); + mSelectSkillDialog = std::make_unique(); mSelectSkillDialog->eventCancel += MyGUI::newDelegate(this, &SpellCreationDialog::onAttributeOrSkillCancel); mSelectSkillDialog->eventItemSelected += MyGUI::newDelegate(this, &SpellCreationDialog::onSelectSkill); - mSelectSkillDialog->setVisible (true); + mSelectSkillDialog->setVisible(true); } else if (effect->mData.mFlags & ESM::MagicEffect::TargetAttribute) { - delete mSelectAttributeDialog; - mSelectAttributeDialog = new SelectAttributeDialog(); - mSelectAttributeDialog->eventCancel += MyGUI::newDelegate(this, &SpellCreationDialog::onAttributeOrSkillCancel); - mSelectAttributeDialog->eventItemSelected += MyGUI::newDelegate(this, &SpellCreationDialog::onSelectAttribute); - mSelectAttributeDialog->setVisible (true); + mSelectAttributeDialog = std::make_unique(); + mSelectAttributeDialog->eventCancel + += MyGUI::newDelegate(this, &SpellCreationDialog::onAttributeOrSkillCancel); + mSelectAttributeDialog->eventItemSelected + += MyGUI::newDelegate(this, &SpellCreationDialog::onSelectAttribute); + mSelectAttributeDialog->setVisible(true); } else { @@ -647,7 +657,7 @@ namespace MWGui { if (effectInfo.mEffectID == mSelectedKnownEffectId) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sOnetypeEffectMessage}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sOnetypeEffectMessage}"); return; } } @@ -656,33 +666,33 @@ namespace MWGui } } - void EffectEditorBase::onEffectModified (ESM::ENAMstruct effect) + void EffectEditorBase::onEffectModified(ESM::ENAMstruct effect) { mEffects[mSelectedEffect] = effect; updateEffectsView(); } - void EffectEditorBase::onEffectRemoved (ESM::ENAMstruct effect) + void EffectEditorBase::onEffectRemoved(ESM::ENAMstruct effect) { mEffects.erase(mEffects.begin() + mSelectedEffect); updateEffectsView(); } - void EffectEditorBase::updateEffectsView () + void EffectEditorBase::updateEffectsView() { - MyGUI::EnumeratorWidgetPtr oldWidgets = mUsedEffectsView->getEnumerator (); - MyGUI::Gui::getInstance ().destroyWidgets (oldWidgets); + MyGUI::EnumeratorWidgetPtr oldWidgets = mUsedEffectsView->getEnumerator(); + MyGUI::Gui::getInstance().destroyWidgets(oldWidgets); - MyGUI::IntSize size(0,0); + MyGUI::IntSize size(0, 0); int i = 0; for (const ESM::ENAMstruct& effectInfo : mEffects) { Widgets::SpellEffectParams params; params.mEffectID = effectInfo.mEffectID; - params.mSkill = effectInfo.mSkill; - params.mAttribute = effectInfo.mAttribute; + params.mSkill = ESM::Skill::indexToRefId(effectInfo.mSkill); + params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mAttribute); params.mDuration = effectInfo.mDuration; params.mMagnMin = effectInfo.mMagnMin; params.mMagnMax = effectInfo.mMagnMax; @@ -690,25 +700,28 @@ namespace MWGui params.mArea = effectInfo.mArea; params.mIsConstant = mConstantEffect; - MyGUI::Button* button = mUsedEffectsView->createWidget("", MyGUI::IntCoord(0, size.height, 0, 24), MyGUI::Align::Default); + MyGUI::Button* button = mUsedEffectsView->createWidget( + {}, MyGUI::IntCoord(0, size.height, 0, 24), MyGUI::Align::Default); button->setUserData(i); button->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellCreationDialog::onEditEffect); - button->setNeedMouseFocus (true); + button->setNeedMouseFocus(true); - Widgets::MWSpellEffectPtr effect = button->createWidget("MW_EffectImage", MyGUI::IntCoord(0,0,0,24), MyGUI::Align::Default); + Widgets::MWSpellEffectPtr effect = button->createWidget( + "MW_EffectImage", MyGUI::IntCoord(0, 0, 0, 24), MyGUI::Align::Default); - effect->setNeedMouseFocus (false); - effect->setSpellEffect (params); + effect->setNeedMouseFocus(false); + effect->setSpellEffect(params); - effect->setSize(effect->getRequestedWidth (), 24); - button->setSize(effect->getRequestedWidth (), 24); + effect->setSize(effect->getRequestedWidth(), 24); + button->setSize(effect->getRequestedWidth(), 24); - size.width = std::max(size.width, effect->getRequestedWidth ()); + size.width = std::max(size.width, effect->getRequestedWidth()); size.height += 24; ++i; } - // Canvas size must be expressed with HScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + // Canvas size must be expressed with HScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden mUsedEffectsView->setVisibleHScroll(false); mUsedEffectsView->setCanvasSize(size); mUsedEffectsView->setVisibleHScroll(true); @@ -716,46 +729,30 @@ namespace MWGui notifyEffectsChanged(); } - void EffectEditorBase::onEffectAdded (ESM::ENAMstruct effect) + void EffectEditorBase::onEffectAdded(ESM::ENAMstruct effect) { mEffects.push_back(effect); - mSelectedEffect=mEffects.size()-1; + mSelectedEffect = mEffects.size() - 1; updateEffectsView(); } - void EffectEditorBase::onEditEffect (MyGUI::Widget *sender) + void EffectEditorBase::onEditEffect(MyGUI::Widget* sender) { int id = *sender->getUserData(); mSelectedEffect = id; - mAddEffectDialog.editEffect (mEffects[id]); - mAddEffectDialog.setVisible (true); + mAddEffectDialog.editEffect(mEffects[id]); + mAddEffectDialog.setVisible(true); } void EffectEditorBase::setConstantEffect(bool constant) { mAddEffectDialog.setConstantEffect(constant); + if (!mConstantEffect && constant) + for (ESM::ENAMstruct& effect : mEffects) + effect.mRange = ESM::RT_Self; mConstantEffect = constant; - - if (!constant) - return; - - for (auto it = mEffects.begin(); it != mEffects.end();) - { - if (it->mRange != ESM::RT_Self) - { - auto& store = MWBase::Environment::get().getWorld()->getStore(); - auto magicEffect = store.get().find(it->mEffectID); - if ((magicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) == 0) - { - it = mEffects.erase(it); - continue; - } - it->mRange = ESM::RT_Self; - } - ++it; - } } } diff --git a/apps/openmw/mwgui/spellcreationdialog.hpp b/apps/openmw/mwgui/spellcreationdialog.hpp index 73352ac238d..6dfe61fc57d 100644 --- a/apps/openmw/mwgui/spellcreationdialog.hpp +++ b/apps/openmw/mwgui/spellcreationdialog.hpp @@ -1,11 +1,13 @@ #ifndef MWGUI_SPELLCREATION_H #define MWGUI_SPELLCREATION_H -#include -#include +#include + +#include +#include -#include "windowbase.hpp" #include "referenceinterface.hpp" +#include "windowbase.hpp" namespace Gui { @@ -28,12 +30,12 @@ namespace MWGui void setConstantEffect(bool constant); - void setSkill(int skill); - void setAttribute(int attribute); + void setSkill(ESM::RefId skill); + void setAttribute(ESM::RefId attribute); - void newEffect (const ESM::MagicEffect* effect); - void editEffect (ESM::ENAMstruct effect); - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Effect; + void newEffect(const ESM::MagicEffect* effect); + void editEffect(ESM::ENAMstruct effect); + typedef MyGUI::delegates::MultiDelegate EventHandle_Effect; EventHandle_Effect eventEffectAdded; EventHandle_Effect eventEffectModified; @@ -68,15 +70,15 @@ namespace MWGui bool mEditing; protected: - void onRangeButtonClicked (MyGUI::Widget* sender); - void onDeleteButtonClicked (MyGUI::Widget* sender); - void onOkButtonClicked (MyGUI::Widget* sender); - void onCancelButtonClicked (MyGUI::Widget* sender); - - void onMagnitudeMinChanged (MyGUI::ScrollBar* sender, size_t pos); - void onMagnitudeMaxChanged (MyGUI::ScrollBar* sender, size_t pos); - void onDurationChanged (MyGUI::ScrollBar* sender, size_t pos); - void onAreaChanged (MyGUI::ScrollBar* sender, size_t pos); + void onRangeButtonClicked(MyGUI::Widget* sender); + void onDeleteButtonClicked(MyGUI::Widget* sender); + void onOkButtonClicked(MyGUI::Widget* sender); + void onCancelButtonClicked(MyGUI::Widget* sender); + + void onMagnitudeMinChanged(MyGUI::ScrollBar* sender, size_t pos); + void onMagnitudeMaxChanged(MyGUI::ScrollBar* sender, size_t pos); + void onDurationChanged(MyGUI::ScrollBar* sender, size_t pos); + void onAreaChanged(MyGUI::ScrollBar* sender, size_t pos); void setMagicEffect(const ESM::MagicEffect* effect); void updateBoxes(); @@ -90,7 +92,6 @@ namespace MWGui bool mConstantEffect; }; - class EffectEditorBase { public: @@ -112,8 +113,8 @@ namespace MWGui MyGUI::ScrollView* mUsedEffectsView; EditEffectDialog mAddEffectDialog; - SelectAttributeDialog* mSelectAttributeDialog; - SelectSkillDialog* mSelectSkillDialog; + std::unique_ptr mSelectAttributeDialog; + std::unique_ptr mSelectSkillDialog; int mSelectedEffect; short mSelectedKnownEffectId; @@ -126,7 +127,7 @@ namespace MWGui void onEffectModified(ESM::ENAMstruct effect); void onEffectRemoved(ESM::ENAMstruct effect); - void onAvailableEffectClicked (MyGUI::Widget* sender); + void onAvailableEffectClicked(MyGUI::Widget* sender); void onAttributeOrSkillCancel(); void onSelectAttribute(); @@ -137,9 +138,9 @@ namespace MWGui void updateEffectsView(); void startEditing(); - void setWidgets (Gui::MWList* availableEffectsList, MyGUI::ScrollView* usedEffectsView); + void setWidgets(Gui::MWList* availableEffectsList, MyGUI::ScrollView* usedEffectsView); - virtual void notifyEffectsChanged () {} + virtual void notifyEffectsChanged() {} private: Type mType; @@ -157,11 +158,13 @@ namespace MWGui void setPtr(const MWWorld::Ptr& actor) override; + std::string_view getWindowIdForLua() const override { return "SpellCreationDialog"; } + protected: void onReferenceUnavailable() override; - void onCancelButtonClicked (MyGUI::Widget* sender); - void onBuyButtonClicked (MyGUI::Widget* sender); + void onCancelButtonClicked(MyGUI::Widget* sender); + void onBuyButtonClicked(MyGUI::Widget* sender); void onAccept(MyGUI::EditBox* sender); void notifyEffectsChanged() override; @@ -174,7 +177,6 @@ namespace MWGui MyGUI::TextBox* mPriceLabel; ESM::Spell mSpell; - }; } diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 405abfbae70..aa29dfc1567 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -1,89 +1,70 @@ #include "spellicons.hpp" -#include #include +#include #include -#include -#include +#include +#include +#include +#include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" #include "tooltips.hpp" - namespace MWGui { - - void EffectSourceVisitor::visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime, float totalTime) - { - MagicEffectInfo newEffectSource; - newEffectSource.mKey = key; - newEffectSource.mMagnitude = static_cast(magnitude); - newEffectSource.mPermanent = mIsPermanent; - newEffectSource.mRemainingTime = remainingTime; - newEffectSource.mSource = sourceName; - newEffectSource.mTotalTime = totalTime; - - mEffectSources[key.mId].push_back(newEffectSource); - } - - - void SpellIcons::updateWidgets(MyGUI::Widget *parent, bool adjustSize) + void SpellIcons::updateWidgets(MyGUI::Widget* parent, bool adjustSize) { - // TODO: Tracking add/remove/expire would be better than force updating every frame - MWWorld::Ptr player = MWMechanics::getPlayer(); const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); + std::map> effects; + for (const auto& params : stats.getActiveSpells()) + { + for (const auto& effect : params.getEffects()) + { + if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + MagicEffectInfo newEffectSource; + newEffectSource.mKey = MWMechanics::EffectKey(effect.mEffectId, effect.getSkillOrAttribute()); + newEffectSource.mMagnitude = static_cast(effect.mMagnitude); + newEffectSource.mPermanent = effect.mDuration == -1.f; + newEffectSource.mRemainingTime = effect.mTimeLeft; + newEffectSource.mSource = params.getDisplayName(); + newEffectSource.mTotalTime = effect.mDuration; + effects[effect.mEffectId].push_back(newEffectSource); + } + } - EffectSourceVisitor visitor; - - // permanent item enchantments & permanent spells - visitor.mIsPermanent = true; - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - store.visitEffectSources(visitor); - stats.getSpells().visitEffectSources(visitor); - - // now add lasting effects - visitor.mIsPermanent = false; - stats.getActiveSpells().visitEffectSources(visitor); - - std::map >& effects = visitor.mEffectSources; - - int w=2; - - for (auto& effectInfoPair : effects) + int w = 2; + const auto& store = MWBase::Environment::get().getESMStore(); + for (const auto& [effectId, effectInfos] : effects) { - const int effectId = effectInfoPair.first; - const ESM::MagicEffect* effect = - MWBase::Environment::get().getWorld ()->getStore ().get().find(effectId); + const ESM::MagicEffect* effect = store->get().find(effectId); float remainingDuration = 0; float totalDuration = 0; std::string sourcesDescription; - static const float fadeTime = MWBase::Environment::get().getWorld()->getStore().get().find("fMagicStartIconBlink")->mValue.getFloat(); + static const float fadeTime + = store->get().find("fMagicStartIconBlink")->mValue.getFloat(); - std::vector& effectInfos = effectInfoPair.second; bool addNewLine = false; for (const MagicEffectInfo& effectInfo : effectInfos) { if (addNewLine) - sourcesDescription += "\n"; + sourcesDescription += '\n'; // if at least one of the effect sources is permanent, the effect will never wear off if (effectInfo.mPermanent) @@ -100,45 +81,62 @@ namespace MWGui sourcesDescription += effectInfo.mSource; if (effect->mData.mFlags & ESM::MagicEffect::TargetSkill) - sourcesDescription += " (" + - MWBase::Environment::get().getWindowManager()->getGameSettingString( - ESM::Skill::sSkillNameIds[effectInfo.mKey.mArg], "") + ")"; + { + const ESM::Skill* skill = store->get().find(effectInfo.mKey.mArg); + sourcesDescription += " (" + skill->mName + ')'; + } if (effect->mData.mFlags & ESM::MagicEffect::TargetAttribute) - sourcesDescription += " (" + - MWBase::Environment::get().getWindowManager()->getGameSettingString( - ESM::Attribute::sGmstAttributeIds[effectInfo.mKey.mArg], "") + ")"; - + { + const ESM::Attribute* attribute = store->get().find(effectInfo.mKey.mArg); + sourcesDescription += " (" + attribute->mName + ')'; + } ESM::MagicEffect::MagnitudeDisplayType displayType = effect->getMagnitudeDisplayType(); if (displayType == ESM::MagicEffect::MDT_TimesInt) { - std::string timesInt = MWBase::Environment::get().getWindowManager()->getGameSettingString("sXTimesINT", ""); + std::string_view timesInt + = MWBase::Environment::get().getWindowManager()->getGameSettingString("sXTimesINT", {}); std::stringstream formatter; - formatter << std::fixed << std::setprecision(1) << " " << (effectInfo.mMagnitude / 10.0f) << timesInt; + formatter << std::fixed << std::setprecision(1) << " " << (effectInfo.mMagnitude / 10.0f) + << timesInt; sourcesDescription += formatter.str(); } - else if ( displayType != ESM::MagicEffect::MDT_None ) + else if (displayType != ESM::MagicEffect::MDT_None) { sourcesDescription += ": " + MyGUI::utility::toString(effectInfo.mMagnitude); - if ( displayType == ESM::MagicEffect::MDT_Percentage ) - sourcesDescription += MWBase::Environment::get().getWindowManager()->getGameSettingString("spercent", ""); - else if ( displayType == ESM::MagicEffect::MDT_Feet ) - sourcesDescription += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sfeet", ""); - else if ( displayType == ESM::MagicEffect::MDT_Level ) + if (displayType == ESM::MagicEffect::MDT_Percentage) + sourcesDescription + += MWBase::Environment::get().getWindowManager()->getGameSettingString("spercent", {}); + else if (displayType == ESM::MagicEffect::MDT_Feet) + { + sourcesDescription += ' '; + sourcesDescription + += MWBase::Environment::get().getWindowManager()->getGameSettingString("sfeet", {}); + } + else if (displayType == ESM::MagicEffect::MDT_Level) { - sourcesDescription += " " + ((effectInfo.mMagnitude > 1) ? - MWBase::Environment::get().getWindowManager()->getGameSettingString("sLevels", "") : - MWBase::Environment::get().getWindowManager()->getGameSettingString("sLevel", "") ); + sourcesDescription += ' '; + if (effectInfo.mMagnitude > 1) + sourcesDescription + += MWBase::Environment::get().getWindowManager()->getGameSettingString("sLevels", {}); + else + sourcesDescription + += MWBase::Environment::get().getWindowManager()->getGameSettingString("sLevel", {}); } else // ESM::MagicEffect::MDT_Points { - sourcesDescription += " " + ((effectInfo.mMagnitude > 1) ? - MWBase::Environment::get().getWindowManager()->getGameSettingString("spoints", "") : - MWBase::Environment::get().getWindowManager()->getGameSettingString("spoint", "") ); + sourcesDescription += ' '; + if (effectInfo.mMagnitude > 1) + sourcesDescription + += MWBase::Environment::get().getWindowManager()->getGameSettingString("spoints", {}); + else + sourcesDescription + += MWBase::Environment::get().getWindowManager()->getGameSettingString("spoint", {}); } } - if (effectInfo.mRemainingTime > -1 && Settings::Manager::getBool("show effect duration","Game")) - sourcesDescription += MWGui::ToolTips::getDurationString(effectInfo.mRemainingTime, " #{sDuration}"); + if (effectInfo.mRemainingTime > -1 && Settings::game().mShowEffectDuration) + sourcesDescription + += MWGui::ToolTips::getDurationString(effectInfo.mRemainingTime, " #{sDuration}"); addNewLine = true; } @@ -148,13 +146,14 @@ namespace MWGui MyGUI::ImageBox* image; if (mWidgetMap.find(effectId) == mWidgetMap.end()) { - image = parent->createWidget - ("ImageBox", MyGUI::IntCoord(w,2,16,16), MyGUI::Align::Default); + image = parent->createWidget( + "ImageBox", MyGUI::IntCoord(w, 2, 16, 16), MyGUI::Align::Default); mWidgetMap[effectId] = image; - image->setImageTexture(MWBase::Environment::get().getWindowManager()->correctIconPath(effect->mIcon)); + image->setImageTexture(Misc::ResourceHelpers::correctIconPath( + effect->mIcon, MWBase::Environment::get().getResourceSystem()->getVFS())); - std::string name = ESM::MagicEffect::effectIdToString (effectId); + const std::string& name = ESM::MagicEffect::indexToGmstString(effectId); ToolTipInfo tooltipInfo; tooltipInfo.caption = "#{" + name + "}"; @@ -168,16 +167,16 @@ namespace MWGui else image = mWidgetMap[effectId]; - image->setPosition(w,2); + image->setPosition(w, 2); image->setVisible(true); w += 16; ToolTipInfo* tooltipInfo = image->getUserData(); - tooltipInfo->text = sourcesDescription; + tooltipInfo->text = std::move(sourcesDescription); // Fade out if (totalDuration >= fadeTime && fadeTime > 0.f) - image->setAlpha(std::min(remainingDuration/fadeTime, 1.f)); + image->setAlpha(std::min(remainingDuration / fadeTime, 1.f)); } else if (mWidgetMap.find(effectId) != mWidgetMap.end()) { @@ -194,7 +193,7 @@ namespace MWGui s = 0; int diff = parent->getWidth() - s; parent->setSize(s, parent->getHeight()); - parent->setPosition(parent->getLeft()+diff, parent->getTop()); + parent->setPosition(parent->getLeft() + diff, parent->getTop()); } // hide inactive effects diff --git a/apps/openmw/mwgui/spellicons.hpp b/apps/openmw/mwgui/spellicons.hpp index b6aa49e69ec..e53fa982843 100644 --- a/apps/openmw/mwgui/spellicons.hpp +++ b/apps/openmw/mwgui/spellicons.hpp @@ -28,7 +28,8 @@ namespace MWGui , mRemainingTime(0.f) , mTotalTime(0.f) , mPermanent(false) - {} + { + } std::string mSource; // display name for effect source (e.g. potion name) MWMechanics::EffectKey mKey; int mMagnitude; @@ -37,27 +38,12 @@ namespace MWGui bool mPermanent; // the effect is permanent }; - class EffectSourceVisitor : public MWMechanics::EffectSourceVisitor - { - public: - bool mIsPermanent; - - std::map > mEffectSources; - - virtual ~EffectSourceVisitor() {} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override; - }; - class SpellIcons { public: void updateWidgets(MyGUI::Widget* parent, bool adjustSize); private: - std::map mWidgetMap; }; diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index 78b9171e591..3d70c391c94 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -1,17 +1,20 @@ #include "spellmodel.hpp" #include +#include + +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellutil.hpp" +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" -#include "../mwworld/class.hpp" namespace { @@ -20,11 +23,7 @@ namespace { if (left.mType != right.mType) return left.mType < right.mType; - - std::string leftName = Misc::StringUtils::lowerCase(left.mName); - std::string rightName = Misc::StringUtils::lowerCase(right.mName); - - return leftName.compare(rightName) < 0; + return Misc::StringUtils::ciLess(left.mName, right.mName); } } @@ -32,44 +31,34 @@ namespace namespace MWGui { - SpellModel::SpellModel(const MWWorld::Ptr &actor, const std::string& filter) - : mActor(actor), mFilter(filter) + SpellModel::SpellModel(const MWWorld::Ptr& actor, const std::string& filter) + : mActor(actor) + , mFilter(filter) { } - SpellModel::SpellModel(const MWWorld::Ptr &actor) + SpellModel::SpellModel(const MWWorld::Ptr& actor) : mActor(actor) { } - bool SpellModel::matchingEffectExists(std::string filter, const ESM::EffectList &effects) + bool SpellModel::matchingEffectExists(std::string filter, const ESM::EffectList& effects) { - auto wm = MWBase::Environment::get().getWindowManager(); - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); for (const auto& effect : effects.mList) { - short effectId = effect.mEffectID; + short effectId = effect.mData.mEffectID; if (effectId != -1) { - const ESM::MagicEffect *magicEffect = - store.get().search(effectId); - std::string effectIDStr = ESM::MagicEffect::effectIdToString(effectId); - std::string fullEffectName = wm->getGameSettingString(effectIDStr, ""); - - if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill && effect.mSkill != -1) - { - fullEffectName += " " + wm->getGameSettingString(ESM::Skill::sSkillNameIds[effect.mSkill], ""); - } + const ESM::MagicEffect* magicEffect = store.get().find(effectId); + const ESM::Attribute* attribute + = store.get().search(ESM::Attribute::indexToRefId(effect.mData.mAttribute)); + const ESM::Skill* skill = store.get().search(ESM::Skill::indexToRefId(effect.mData.mSkill)); - if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute && effect.mAttribute != -1) - { - fullEffectName += " " + wm->getGameSettingString(ESM::Attribute::sGmstAttributeIds[effect.mAttribute], ""); - } - - std::string convert = Misc::StringUtils::lowerCaseUtf8(fullEffectName); + std::string fullEffectName = MWMechanics::getMagicEffectString(*magicEffect, attribute, skill); + std::string convert = Utf8Stream::lowerCaseUtf8(fullEffectName); if (convert.find(filter) != std::string::npos) { return true; @@ -87,21 +76,18 @@ namespace MWGui MWMechanics::CreatureStats& stats = mActor.getClass().getCreatureStats(mActor); const MWMechanics::Spells& spells = stats.getSpells(); - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); - std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter); + std::string filter = Utf8Stream::lowerCaseUtf8(mFilter); - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) continue; - std::string name = Misc::StringUtils::lowerCaseUtf8(spell->mName); - - if (name.find(filter) == std::string::npos - && !matchingEffectExists(filter, spell->mEffects)) + std::string name = Utf8Stream::lowerCaseUtf8(spell->mName); + + if (name.find(filter) == std::string::npos && !matchingEffectExists(filter, spell->mEffects)) continue; Spell newSpell; @@ -109,7 +95,7 @@ namespace MWGui if (spell->mData.mType == ESM::Spell::ST_Spell) { newSpell.mType = Spell::Type_Spell; - std::string cost = std::to_string(spell->mData.mCost); + std::string cost = std::to_string(MWMechanics::calcSpellCost(*spell)); std::string chance = std::to_string(int(MWMechanics::getSpellSuccessChance(spell, mActor))); newSpell.mCostColumn = cost + "/" + chance; } @@ -127,30 +113,31 @@ namespace MWGui for (MWWorld::ContainerStoreIterator it = invStore.begin(); it != invStore.end(); ++it) { MWWorld::Ptr item = *it; - const std::string enchantId = item.getClass().getEnchantment(item); + const ESM::RefId& enchantId = item.getClass().getEnchantment(item); if (enchantId.empty()) continue; const ESM::Enchantment* enchant = esmStore.get().search(enchantId); if (!enchant) { - Log(Debug::Warning) << "Warning: Can't find enchantment '" << enchantId << "' on item " << item.getCellRef().getRefId(); + Log(Debug::Warning) << "Warning: Can't find enchantment '" << enchantId << "' on item " + << item.getCellRef().getRefId(); continue; } - if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce) + if (enchant->mData.mType != ESM::Enchantment::WhenUsed + && enchant->mData.mType != ESM::Enchantment::CastOnce) continue; - std::string name = Misc::StringUtils::lowerCaseUtf8(item.getClass().getName(item)); + std::string name = Utf8Stream::lowerCaseUtf8(item.getClass().getName(item)); - if (name.find(filter) == std::string::npos - && !matchingEffectExists(filter, enchant->mEffects)) + if (name.find(filter) == std::string::npos && !matchingEffectExists(filter, enchant->mEffects)) continue; Spell newSpell; newSpell.mItem = item; newSpell.mId = item.getCellRef().getRefId(); newSpell.mName = item.getClass().getName(item); - newSpell.mCount = item.getRefData().getCount(); + newSpell.mCount = item.getCellRef().getCount(); newSpell.mType = Spell::Type_EnchantedItem; newSpell.mSelected = invStore.getSelectedEnchantItem() == it; @@ -163,15 +150,15 @@ namespace MWGui else { if (!item.getClass().getEquipmentSlots(item).first.empty() - && item.getClass().canBeEquipped(item, mActor).first == 0) + && item.getClass().canBeEquipped(item, mActor).first == 0) continue; - int castCost = MWMechanics::getEffectiveEnchantmentCastCost(static_cast(enchant->mData.mCost), mActor); + int castCost = MWMechanics::getEffectiveEnchantmentCastCost(*enchant, mActor); std::string cost = std::to_string(castCost); int currentCharge = int(item.getCellRef().getEnchantmentCharge()); - if (currentCharge == -1) - currentCharge = enchant->mData.mCharge; + if (currentCharge == -1) + currentCharge = MWMechanics::getEnchantmentCharge(*enchant); std::string charge = std::to_string(currentCharge); newSpell.mCostColumn = cost + "/" + charge; @@ -191,9 +178,10 @@ namespace MWGui SpellModel::ModelIndex SpellModel::getSelectedIndex() const { ModelIndex selected = -1; - for (SpellModel::ModelIndex i = 0; i +#include namespace MWGui { @@ -19,7 +19,7 @@ namespace MWGui Type mType; std::string mName; std::string mCostColumn; // Cost/chance or Cost/charge - std::string mId; // Item ID or spell ID + ESM::RefId mId; // Item ID or spell ID MWWorld::Ptr mItem; // Only for Type_EnchantedItem int mCount; // Only for Type_EnchantedItem bool mSelected; // Is this the currently selected spell/item (only one can be selected at a time) @@ -45,7 +45,7 @@ namespace MWGui void update(); - Spell getItem (ModelIndex index) const; + Spell getItem(ModelIndex index) const; ///< throws for invalid index size_t getItemCount() const; @@ -59,7 +59,7 @@ namespace MWGui std::string mFilter; - bool matchingEffectExists(std::string filter, const ESM::EffectList &effects); + bool matchingEffectExists(std::string filter, const ESM::EffectList& effects); }; } diff --git a/apps/openmw/mwgui/spellview.cpp b/apps/openmw/mwgui/spellview.cpp index a8b7cb6398f..678f6ffe1f0 100644 --- a/apps/openmw/mwgui/spellview.cpp +++ b/apps/openmw/mwgui/spellview.cpp @@ -1,12 +1,13 @@ #include "spellview.hpp" #include -#include -#include #include +#include +#include -#include +#include #include +#include #include "tooltips.hpp" @@ -15,12 +16,12 @@ namespace MWGui const char* SpellView::sSpellModelIndex = "SpellModelIndex"; - SpellView::LineInfo::LineInfo(MyGUI::Widget* leftWidget, MyGUI::Widget* rightWidget, SpellModel::ModelIndex spellIndex) + SpellView::LineInfo::LineInfo( + MyGUI::Widget* leftWidget, MyGUI::Widget* rightWidget, SpellModel::ModelIndex spellIndex) : mLeftWidget(leftWidget) , mRightWidget(rightWidget) , mSpellIndex(spellIndex) { - } SpellView::SpellView() @@ -46,7 +47,7 @@ namespace MWGui MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } - void SpellView::setModel(SpellModel *model) + void SpellView::setModel(SpellModel* model) { mModel.reset(model); update(); @@ -84,20 +85,20 @@ namespace MWGui int curType = -1; - const int spellHeight = 18; + const int spellHeight = Settings::gui().mFontSize + 2; mLines.clear(); while (mScrollView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); - for (SpellModel::ModelIndex i = 0; igetItemCount()); ++i) + for (SpellModel::ModelIndex i = 0; i < int(mModel->getItemCount()); ++i) { const Spell& spell = mModel->getItem(i); if (curType != spell.mType) { if (spell.mType == Spell::Type_Power) - addGroup("#{sPowers}", ""); + addGroup("#{sPowers}", {}); else if (spell.mType == Spell::Type_Spell) addGroup("#{sSpells}", mShowCostColumn ? "#{sCostChance}" : ""); else @@ -108,8 +109,8 @@ namespace MWGui const std::string skin = spell.mActive ? "SandTextButton" : "SpellTextUnequipped"; const std::string captionSuffix = MWGui::ToolTips::getCountString(spell.mCount); - Gui::SharedStateButton* t = mScrollView->createWidget(skin, - MyGUI::IntCoord(0, 0, 0, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); + Gui::SharedStateButton* t = mScrollView->createWidget( + skin, MyGUI::IntCoord(0, 0, 0, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); t->setNeedKeyFocus(true); t->setCaption(spell.mName + captionSuffix); t->setTextAlign(MyGUI::Align::Left); @@ -117,8 +118,8 @@ namespace MWGui if (!spell.mCostColumn.empty() && mShowCostColumn) { - Gui::SharedStateButton* costChance = mScrollView->createWidget(skin, - MyGUI::IntCoord(0, 0, 0, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); + Gui::SharedStateButton* costChance = mScrollView->createWidget( + skin, MyGUI::IntCoord(0, 0, 0, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); costChance->setCaption(spell.mCostColumn); costChance->setTextAlign(MyGUI::Align::Right); adjustSpellWidget(spell, i, costChance); @@ -159,7 +160,7 @@ namespace MWGui // match model against line // if don't match, then major change has happened, so do a full update - if (mModel->getItemCount() <= static_cast(spellIndex)) + if (mModel->getItemCount() <= static_cast(spellIndex)) { fullUpdateRequired = true; break; @@ -187,14 +188,12 @@ namespace MWGui // special case, look for spells added to model that are beyond last updatable item SpellModel::ModelIndex topSpellIndex = mModel->getItemCount() - 1; - if (fullUpdateRequired || - ((0 <= topSpellIndex) && (maxSpellIndexFound < topSpellIndex))) + if (fullUpdateRequired || ((0 <= topSpellIndex) && (maxSpellIndexFound < topSpellIndex))) { update(); } } - void SpellView::layoutWidgets() { int height = 0; @@ -222,35 +221,33 @@ namespace MWGui height += lineHeight; } - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden mScrollView->setVisibleVScroll(false); mScrollView->setCanvasSize(mScrollView->getWidth(), std::max(mScrollView->getHeight(), height)); mScrollView->setVisibleVScroll(true); } - void SpellView::addGroup(const std::string &label, const std::string& label2) + void SpellView::addGroup(const std::string& label, const std::string& label2) { if (mScrollView->getChildCount() > 0) { - MyGUI::ImageBox* separator = mScrollView->createWidget("MW_HLine", - MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 18), - MyGUI::Align::Left | MyGUI::Align::Top); + MyGUI::ImageBox* separator = mScrollView->createWidget( + "MW_HLine", MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 18), MyGUI::Align::Left | MyGUI::Align::Top); separator->setNeedMouseFocus(false); mLines.emplace_back(separator, (MyGUI::Widget*)nullptr, NoSpellIndex); } MyGUI::TextBox* groupWidget = mScrollView->createWidget("SandBrightText", - MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 24), - MyGUI::Align::Left | MyGUI::Align::Top); + MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 24), MyGUI::Align::Left | MyGUI::Align::Top); groupWidget->setCaptionWithReplacing(label); groupWidget->setTextAlign(MyGUI::Align::Left); groupWidget->setNeedMouseFocus(false); - if (label2 != "") + if (!label2.empty()) { MyGUI::TextBox* groupWidget2 = mScrollView->createWidget("SandBrightText", - MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 24), - MyGUI::Align::Left | MyGUI::Align::Top); + MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 24), MyGUI::Align::Left | MyGUI::Align::Top); groupWidget2->setCaptionWithReplacing(label2); groupWidget2->setTextAlign(MyGUI::Align::Right); groupWidget2->setNeedMouseFocus(false); @@ -261,8 +258,7 @@ namespace MWGui mLines.emplace_back(groupWidget, (MyGUI::Widget*)nullptr, NoSpellIndex); } - - void SpellView::setSize(const MyGUI::IntSize &_value) + void SpellView::setSize(const MyGUI::IntSize& _value) { bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setSize(_value); @@ -270,7 +266,7 @@ namespace MWGui layoutWidgets(); } - void SpellView::setCoord(const MyGUI::IntCoord &_value) + void SpellView::setCoord(const MyGUI::IntCoord& _value) { bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setCoord(_value); @@ -278,7 +274,7 @@ namespace MWGui layoutWidgets(); } - void SpellView::adjustSpellWidget(const Spell &spell, SpellModel::ModelIndex index, MyGUI::Widget *widget) + void SpellView::adjustSpellWidget(const Spell& spell, SpellModel::ModelIndex index, MyGUI::Widget* widget) { if (spell.mType == Spell::Type_EnchantedItem) { @@ -288,7 +284,7 @@ namespace MWGui else { widget->setUserString("ToolTipType", "Spell"); - widget->setUserString("Spell", spell.mId); + widget->setUserString("Spell", spell.mId.serialize()); } widget->setUserString(sSpellModelIndex, MyGUI::utility::toString(index)); @@ -309,10 +305,11 @@ namespace MWGui void SpellView::onMouseWheelMoved(MyGUI::Widget* _sender, int _rel) { - if (mScrollView->getViewOffset().top + _rel*0.3f > 0) + if (mScrollView->getViewOffset().top + _rel * 0.3f > 0) mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); else - mScrollView->setViewOffset(MyGUI::IntPoint(0, static_cast(mScrollView->getViewOffset().top + _rel*0.3f))); + mScrollView->setViewOffset( + MyGUI::IntPoint(0, static_cast(mScrollView->getViewOffset().top + _rel * 0.3f))); } void SpellView::resetScrollbars() diff --git a/apps/openmw/mwgui/spellview.hpp b/apps/openmw/mwgui/spellview.hpp index 6b3effc453a..caff43a33e4 100644 --- a/apps/openmw/mwgui/spellview.hpp +++ b/apps/openmw/mwgui/spellview.hpp @@ -26,7 +26,7 @@ namespace MWGui SpellView(); /// Register needed components with MyGUI's factory manager - static void registerComponents (); + static void registerComponents(); /// Should the cost/chance column be shown? void setShowCostColumn(bool show); @@ -34,7 +34,7 @@ namespace MWGui void setHighlightSelected(bool highlight); /// Takes ownership of \a model - void setModel (SpellModel* model); + void setModel(SpellModel* model); SpellModel* getModel(); @@ -43,7 +43,7 @@ namespace MWGui /// simplified update called each frame void incrementalUpdate(); - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_ModelIndex; + typedef MyGUI::delegates::MultiDelegate EventHandle_ModelIndex; /// Fired when a spell was clicked EventHandle_ModelIndex eventSpellClicked; @@ -75,9 +75,12 @@ namespace MWGui }; /// magic number indicating LineInfo does not correspond to an item in mModel - enum { NoSpellIndex = -1 }; + enum + { + NoSpellIndex = -1 + }; - std::vector< LineInfo > mLines; + std::vector mLines; bool mShowCostColumn; bool mHighlightSelected; diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index 873037db10c..10a83f2b2db 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -3,26 +3,28 @@ #include #include -#include -#include +#include +#include +#include +#include -#include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" #include "../mwworld/player.hpp" -#include "../mwmechanics/spellutil.hpp" -#include "../mwmechanics/spells.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/spells.hpp" +#include "../mwmechanics/spellutil.hpp" -#include "spellicons.hpp" #include "confirmationdialog.hpp" +#include "spellicons.hpp" #include "spellview.hpp" namespace MWGui @@ -34,7 +36,7 @@ namespace MWGui , mSpellView(nullptr) , mUpdateTimer(0.0f) { - mSpellIcons = new SpellIcons(); + mSpellIcons = std::make_unique(); MyGUI::Widget* deleteButton; getWidget(deleteButton, "DeleteSpellButton"); @@ -54,14 +56,9 @@ namespace MWGui mFilterEdit->setSize(filterWidth, mFilterEdit->getSize().height); } - SpellWindow::~SpellWindow() - { - delete mSpellIcons; - } - void SpellWindow::onPinToggled() { - Settings::Manager::setBool("spells pin", "Windows", mPinned); + Settings::windows().mSpellsPin.set(mPinned); MWBase::Environment::get().getWindowManager()->setSpellVisibility(!mPinned); } @@ -84,7 +81,7 @@ namespace MWGui updateSpells(); } - void SpellWindow::onFrame(float dt) + void SpellWindow::onFrame(float dt) { NoDrop::onFrame(dt); mUpdateTimer += dt; @@ -124,8 +121,7 @@ namespace MWGui throw std::runtime_error("can't find selected item"); // equip, if it can be equipped and is not already equipped - if (!alreadyEquipped - && !item.getClass().getEquipmentSlots(item).first.empty()) + if (!alreadyEquipped && !item.getClass().getEquipmentSlots(item).first.empty()) { MWBase::Environment::get().getWindowManager()->useItem(item); // make sure that item was successfully equipped @@ -140,34 +136,34 @@ namespace MWGui updateSpells(); } - void SpellWindow::askDeleteSpell(const std::string &spellId) + void SpellWindow::askDeleteSpell(const ESM::RefId& spellId) { // delete spell, if allowed - const ESM::Spell* spell = - MWBase::Environment::get().getWorld()->getStore().get().find(spellId); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(spellId); MWWorld::Ptr player = MWMechanics::getPlayer(); - std::string raceId = player.get()->mBase->mRace; - const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(raceId); + const ESM::RefId& raceId = player.get()->mBase->mRace; + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(raceId); // can't delete racial spells, birthsign spells or powers bool isInherent = race->mPowers.exists(spell->mId) || spell->mData.mType == ESM::Spell::ST_Power; - const std::string& signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); + const ESM::RefId& signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); if (!isInherent && !signId.empty()) { - const ESM::BirthSign* sign = MWBase::Environment::get().getWorld()->getStore().get().find(signId); + const ESM::BirthSign* sign = MWBase::Environment::get().getESMStore()->get().find(signId); isInherent = sign->mPowers.exists(spell->mId); } + const auto windowManager = MWBase::Environment::get().getWindowManager(); if (isInherent) { - MWBase::Environment::get().getWindowManager()->messageBox("#{sDeleteSpellError}"); + windowManager->messageBox("#{sDeleteSpellError}"); } else { // ask for confirmation mSpellToDelete = spellId; - ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); - std::string question = MWBase::Environment::get().getWindowManager()->getGameSettingString("sQuestionDeleteSpell", "Delete %s?"); + ConfirmationDialog* dialog = windowManager->getConfirmationDialog(); + std::string question{ windowManager->getGameSettingString("sQuestionDeleteSpell", "Delete %s?") }; question = Misc::StringUtils::format(question, spell->mName); dialog->askForConfirmation(question); dialog->eventOkClicked.clear(); @@ -192,12 +188,12 @@ namespace MWGui } } - void SpellWindow::onFilterChanged(MyGUI::EditBox *sender) + void SpellWindow::onFilterChanged(MyGUI::EditBox* sender) { mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), sender->getCaption())); } - void SpellWindow::onDeleteClicked(MyGUI::Widget *widget) + void SpellWindow::onDeleteClicked(MyGUI::Widget* widget) { SpellModel::ModelIndex selected = mSpellView->getModel()->getSelectedIndex(); if (selected < 0) @@ -208,12 +204,13 @@ namespace MWGui askDeleteSpell(spell.mId); } - void SpellWindow::onSpellSelected(const std::string& spellId) + void SpellWindow::onSpellSelected(const ESM::RefId& spellId) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); store.setSelectedEnchantItem(store.end()); - MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); + MWBase::Environment::get().getWindowManager()->setSelectedSpell( + spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); updateSpells(); } @@ -239,12 +236,11 @@ namespace MWGui if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player)) return; - bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); - const MWMechanics::CreatureStats &stats = player.getClass().getCreatureStats(player); - if ((!godmode && stats.isParalyzed()) || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) + const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); + if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) return; - mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), "")); + mSpellView->setModel(new SpellModel(MWMechanics::getPlayer())); SpellModel::ModelIndex selected = mSpellView->getModel()->getSelectedIndex(); if (selected < 0) diff --git a/apps/openmw/mwgui/spellwindow.hpp b/apps/openmw/mwgui/spellwindow.hpp index 786a7d877f9..e35c5cdc4cc 100644 --- a/apps/openmw/mwgui/spellwindow.hpp +++ b/apps/openmw/mwgui/spellwindow.hpp @@ -1,20 +1,20 @@ #ifndef MWGUI_SPELLWINDOW_H #define MWGUI_SPELLWINDOW_H -#include "windowpinnablebase.hpp" +#include +#include "spellicons.hpp" #include "spellmodel.hpp" +#include "windowpinnablebase.hpp" namespace MWGui { - class SpellIcons; class SpellView; class SpellWindow : public WindowPinnableBase, public NoDrop { public: SpellWindow(DragAndDrop* drag); - virtual ~SpellWindow(); void updateSpells(); @@ -23,25 +23,27 @@ namespace MWGui /// Cycle to next/previous spell void cycle(bool next); + std::string_view getWindowIdForLua() const override { return "Magic"; } + protected: MyGUI::Widget* mEffectBox; - std::string mSpellToDelete; + ESM::RefId mSpellToDelete; void onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped); - void onSpellSelected(const std::string& spellId); + void onSpellSelected(const ESM::RefId& spellId); void onModelIndexSelected(SpellModel::ModelIndex index); - void onFilterChanged(MyGUI::EditBox *sender); - void onDeleteClicked(MyGUI::Widget *widget); + void onFilterChanged(MyGUI::EditBox* sender); + void onDeleteClicked(MyGUI::Widget* widget); void onDeleteSpellAccept(); - void askDeleteSpell(const std::string& spellId); + void askDeleteSpell(const ESM::RefId& spellId); void onPinToggled() override; void onTitleDoubleClicked() override; void onOpen() override; SpellView* mSpellView; - SpellIcons* mSpellIcons; + std::unique_ptr mSpellIcons; MyGUI::EditBox* mFilterEdit; private: diff --git a/apps/openmw/mwgui/statswatcher.cpp b/apps/openmw/mwgui/statswatcher.cpp index ccb77de8f20..90872346f7b 100644 --- a/apps/openmw/mwgui/statswatcher.cpp +++ b/apps/openmw/mwgui/statswatcher.cpp @@ -1,22 +1,26 @@ #include "statswatcher.hpp" +#include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" #include "../mwmechanics/npcstats.hpp" -#include "../mwmechanics/spellutil.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" + +#include namespace MWGui { // mWatchedTimeToStartDrowning = -1 for correct drowning state check, // if stats.getTimeToStartDrowning() == 0 already on game start StatsWatcher::StatsWatcher() - : mWatchedLevel(-1), mWatchedTimeToStartDrowning(-1), mWatchedStatsEmpty(true) + : mWatchedLevel(-1) + , mWatchedTimeToStartDrowning(-1) + , mWatchedStatsEmpty(true) { } @@ -30,49 +34,48 @@ namespace MWGui if (mWatched.isEmpty()) return; - MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); - const MWMechanics::NpcStats &stats = mWatched.getClass().getNpcStats(mWatched); - for (int i = 0;i < ESM::Attribute::Length;++i) + const auto& store = MWBase::Environment::get().getESMStore(); + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + const MWMechanics::NpcStats& stats = mWatched.getClass().getNpcStats(mWatched); + for (const ESM::Attribute& attribute : store->get()) { - if (stats.getAttribute(i) != mWatchedAttributes[i] || mWatchedStatsEmpty) + const auto& value = stats.getAttribute(attribute.mId); + if (value != mWatchedAttributes[attribute.mId] || mWatchedStatsEmpty) { - std::stringstream attrname; - attrname << "AttribVal"<<(i+1); - - mWatchedAttributes[i] = stats.getAttribute(i); - setValue(attrname.str(), stats.getAttribute(i)); + mWatchedAttributes[attribute.mId] = value; + setAttribute(attribute.mId, value); } } if (stats.getHealth() != mWatchedHealth || mWatchedStatsEmpty) { - static const std::string hbar("HBar"); mWatchedHealth = stats.getHealth(); - setValue(hbar, stats.getHealth()); + setValue("HBar", stats.getHealth()); } if (stats.getMagicka() != mWatchedMagicka || mWatchedStatsEmpty) { - static const std::string mbar("MBar"); mWatchedMagicka = stats.getMagicka(); - setValue(mbar, stats.getMagicka()); + setValue("MBar", stats.getMagicka()); } if (stats.getFatigue() != mWatchedFatigue || mWatchedStatsEmpty) { - static const std::string fbar("FBar"); mWatchedFatigue = stats.getFatigue(); - setValue(fbar, stats.getFatigue()); + setValue("FBar", stats.getFatigue()); } float timeToDrown = stats.getTimeToStartDrowning(); if (timeToDrown != mWatchedTimeToStartDrowning) { - static const float fHoldBreathTime = MWBase::Environment::get().getWorld()->getStore().get() - .find("fHoldBreathTime")->mValue.getFloat(); + static const float fHoldBreathTime = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fHoldBreathTime") + ->mValue.getFloat(); mWatchedTimeToStartDrowning = timeToDrown; - if(timeToDrown >= fHoldBreathTime || timeToDrown == -1.0) // -1.0 is a special value during initialization + if (timeToDrown >= fHoldBreathTime || timeToDrown == -1.0) // -1.0 is a special value during initialization winMgr->setDrowningBarVisibility(false); else { @@ -81,13 +84,13 @@ namespace MWGui } } - //Loop over ESM::Skill::SkillEnum - for (int i = 0; i < ESM::Skill::Length; ++i) + for (const ESM::Skill& skill : store->get()) { - if(stats.getSkill(i) != mWatchedSkills[i] || mWatchedStatsEmpty) + const auto& value = stats.getSkill(skill.mId); + if (value != mWatchedSkills[skill.mId] || mWatchedStatsEmpty) { - mWatchedSkills[i] = stats.getSkill(i); - setValue((ESM::Skill::SkillEnum)i, stats.getSkill(i)); + mWatchedSkills[skill.mId] = value; + setValue(skill.mId, value); } } @@ -99,7 +102,7 @@ namespace MWGui if (mWatched.getClass().isNpc()) { - const ESM::NPC *watchedRecord = mWatched.get()->mBase; + const ESM::NPC* watchedRecord = mWatched.get()->mBase; if (watchedRecord->mName != mWatchedName || mWatchedStatsEmpty) { @@ -110,25 +113,24 @@ namespace MWGui if (watchedRecord->mRace != mWatchedRace || mWatchedStatsEmpty) { mWatchedRace = watchedRecord->mRace; - const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore() - .get().find(watchedRecord->mRace); + const ESM::Race* race = store->get().find(watchedRecord->mRace); setValue("race", race->mName); } if (watchedRecord->mClass != mWatchedClass || mWatchedStatsEmpty) { mWatchedClass = watchedRecord->mClass; - const ESM::Class *cls = MWBase::Environment::get().getWorld()->getStore() - .get().find(watchedRecord->mClass); + const ESM::Class* cls = store->get().find(watchedRecord->mClass); setValue("class", cls->mName); - MWBase::WindowManager::SkillList majorSkills (5); - MWBase::WindowManager::SkillList minorSkills (5); + size_t size = cls->mData.mSkills.size(); + std::vector majorSkills(size); + std::vector minorSkills(size); - for (int i=0; i<5; ++i) + for (size_t i = 0; i < size; ++i) { - minorSkills[i] = cls->mData.mSkills[i][0]; - majorSkills[i] = cls->mData.mSkills[i][1]; + minorSkills[i] = ESM::Skill::indexToRefId(cls->mData.mSkills[i][0]); + majorSkills[i] = ESM::Skill::indexToRefId(cls->mData.mSkills[i][1]); } configureSkills(majorSkills, minorSkills); @@ -148,39 +150,37 @@ namespace MWGui mListeners.erase(listener); } - void StatsWatcher::setValue(const std::string& id, const MWMechanics::AttributeValue& value) + void StatsWatcher::setAttribute(ESM::RefId id, const MWMechanics::AttributeValue& value) { for (StatsListener* listener : mListeners) - listener->setValue(id, value); + listener->setAttribute(id, value); } - void StatsWatcher::setValue(ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) + void StatsWatcher::setValue(ESM::RefId id, const MWMechanics::SkillValue& value) { - /// \todo Don't use the skill enum as a parameter type (we will have to drop it anyway, once we - /// allow custom skills. for (StatsListener* listener : mListeners) - listener->setValue(parSkill, value); + listener->setValue(id, value); } - void StatsWatcher::setValue(const std::string& id, const MWMechanics::DynamicStat& value) + void StatsWatcher::setValue(std::string_view id, const MWMechanics::DynamicStat& value) { for (StatsListener* listener : mListeners) listener->setValue(id, value); } - void StatsWatcher::setValue(const std::string& id, const std::string& value) + void StatsWatcher::setValue(std::string_view id, const std::string& value) { for (StatsListener* listener : mListeners) listener->setValue(id, value); } - void StatsWatcher::setValue(const std::string& id, int value) + void StatsWatcher::setValue(std::string_view id, int value) { for (StatsListener* listener : mListeners) listener->setValue(id, value); } - void StatsWatcher::configureSkills(const std::vector& major, const std::vector& minor) + void StatsWatcher::configureSkills(const std::vector& major, const std::vector& minor) { for (StatsListener* listener : mListeners) listener->configureSkills(major, minor); diff --git a/apps/openmw/mwgui/statswatcher.hpp b/apps/openmw/mwgui/statswatcher.hpp index 353779d8774..fafae4452cd 100644 --- a/apps/openmw/mwgui/statswatcher.hpp +++ b/apps/openmw/mwgui/statswatcher.hpp @@ -1,10 +1,11 @@ #ifndef MWGUI_STATSWATCHER_H #define MWGUI_STATSWATCHER_H +#include #include #include -#include +#include #include "../mwmechanics/stat.hpp" @@ -15,29 +16,31 @@ namespace MWGui class StatsListener { public: + virtual ~StatsListener() = default; + /// Set value for the given ID. - virtual void setValue(const std::string& id, const MWMechanics::AttributeValue& value) {} - virtual void setValue(const std::string& id, const MWMechanics::DynamicStat& value) {} - virtual void setValue(const std::string& id, const std::string& value) {} - virtual void setValue(const std::string& id, int value) {} - virtual void setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) {} - virtual void configureSkills(const std::vector& major, const std::vector& minor) {} + virtual void setAttribute(ESM::RefId id, const MWMechanics::AttributeValue& value) {} + virtual void setValue(std::string_view id, const MWMechanics::DynamicStat& value) {} + virtual void setValue(std::string_view, const std::string& value) {} + virtual void setValue(std::string_view, int value) {} + virtual void setValue(ESM::RefId id, const MWMechanics::SkillValue& value) {} + virtual void configureSkills(const std::vector& major, const std::vector& minor) {} }; class StatsWatcher { MWWorld::Ptr mWatched; - MWMechanics::AttributeValue mWatchedAttributes[ESM::Attribute::Length]; - MWMechanics::SkillValue mWatchedSkills[ESM::Skill::Length]; + std::map mWatchedAttributes; + std::map mWatchedSkills; MWMechanics::DynamicStat mWatchedHealth; MWMechanics::DynamicStat mWatchedMagicka; MWMechanics::DynamicStat mWatchedFatigue; std::string mWatchedName; - std::string mWatchedRace; - std::string mWatchedClass; + ESM::RefId mWatchedRace; + ESM::RefId mWatchedClass; int mWatchedLevel; @@ -47,12 +50,12 @@ namespace MWGui std::set mListeners; - void setValue(const std::string& id, const MWMechanics::AttributeValue& value); - void setValue(const std::string& id, const MWMechanics::DynamicStat& value); - void setValue(const std::string& id, const std::string& value); - void setValue(const std::string& id, int value); - void setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value); - void configureSkills(const std::vector& major, const std::vector& minor); + void setAttribute(ESM::RefId id, const MWMechanics::AttributeValue& value); + void setValue(std::string_view id, const MWMechanics::DynamicStat& value); + void setValue(std::string_view id, const std::string& value); + void setValue(std::string_view id, int value); + void setValue(ESM::RefId id, const MWMechanics::SkillValue& value); + void configureSkills(const std::vector& major, const std::vector& minor); public: StatsWatcher(); diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index 8e6f9512915..69f0b4b449a 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -1,76 +1,80 @@ #include "statswindow.hpp" -#include -#include -#include +#include +#include #include #include #include -#include +#include +#include +#include +#include + +#include -#include +#include +#include +#include +#include +#include + +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/player.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/player.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/npcstats.hpp" #include "tooltips.hpp" namespace MWGui { - StatsWindow::StatsWindow (DragAndDrop* drag) - : WindowPinnableBase("openmw_stats_window.layout") - , NoDrop(drag, mMainWidget) - , mSkillView(nullptr) - , mMajorSkills() - , mMinorSkills() - , mMiscSkills() - , mSkillValues() - , mSkillWidgetMap() - , mFactionWidgetMap() - , mFactions() - , mBirthSignId() - , mReputation(0) - , mBounty(0) - , mSkillWidgets() - , mChanged(true) - , mMinFullWidth(mMainWidget->getSize().width) + StatsWindow::StatsWindow(DragAndDrop* drag) + : WindowPinnableBase("openmw_stats_window.layout") + , NoDrop(drag, mMainWidget) + , mSkillView(nullptr) + , mReputation(0) + , mBounty(0) + , mChanged(true) + , mMinFullWidth(mMainWidget->getSize().width) { - const char *names[][2] = - { - { "Attrib1", "sAttributeStrength" }, - { "Attrib2", "sAttributeIntelligence" }, - { "Attrib3", "sAttributeWillpower" }, - { "Attrib4", "sAttributeAgility" }, - { "Attrib5", "sAttributeSpeed" }, - { "Attrib6", "sAttributeEndurance" }, - { "Attrib7", "sAttributePersonality" }, - { "Attrib8", "sAttributeLuck" }, - { 0, 0 } - }; - - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - for (int i=0; names[i][0]; ++i) + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + MyGUI::Widget* attributeView = getWidget("AttributeView"); + MyGUI::IntCoord coord{ 0, 0, 204, 18 }; + const MyGUI::Align alignment = MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch; + for (const ESM::Attribute& attribute : store.get()) { - setText (names[i][0], store.get().find (names[i][1])->mValue.getString()); + auto* box = attributeView->createWidget({}, coord, alignment); + box->setUserString("ToolTipType", "Layout"); + box->setUserString("ToolTipLayout", "AttributeToolTip"); + box->setUserString("Caption_AttributeName", attribute.mName); + box->setUserString("Caption_AttributeDescription", attribute.mDescription); + box->setUserString("ImageTexture_AttributeImage", attribute.mIcon); + coord.top += coord.height; + auto* name = box->createWidget("SandText", { 0, 0, 160, 18 }, alignment); + name->setNeedMouseFocus(false); + name->setCaption(attribute.mName); + auto* value = box->createWidget( + "SandTextRight", { 160, 0, 44, 18 }, MyGUI::Align::Right | MyGUI::Align::Top); + value->setNeedMouseFocus(false); + mAttributeWidgets.emplace(attribute.mId, value); } getWidget(mSkillView, "SkillView"); getWidget(mLeftPane, "LeftPane"); getWidget(mRightPane, "RightPane"); - for (int i = 0; i < ESM::Skill::Length; ++i) + for (const ESM::Skill& skill : store.get()) { - mSkillValues.insert(std::make_pair(i, MWMechanics::SkillValue())); - mSkillWidgetMap.insert(std::make_pair(i, std::make_pair((MyGUI::TextBox*)nullptr, (MyGUI::TextBox*)nullptr))); + mSkillValues.emplace(skill.mId, MWMechanics::SkillValue()); + mSkillWidgetMap.emplace(skill.mId, std::make_pair(nullptr, nullptr)); } MyGUI::Window* t = mMainWidget->castType(); @@ -81,10 +85,11 @@ namespace MWGui void StatsWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { - if (mSkillView->getViewOffset().top + _rel*0.3 > 0) + if (mSkillView->getViewOffset().top + _rel * 0.3 > 0) mSkillView->setViewOffset(MyGUI::IntPoint(0, 0)); else - mSkillView->setViewOffset(MyGUI::IntPoint(0, static_cast(mSkillView->getViewOffset().top + _rel*0.3))); + mSkillView->setViewOffset( + MyGUI::IntPoint(0, static_cast(mSkillView->getViewOffset().top + _rel * 0.3))); } void StatsWindow::onWindowResize(MyGUI::Window* window) @@ -92,7 +97,8 @@ namespace MWGui int windowWidth = window->getSize().width; int windowHeight = window->getSize().height; - //initial values defined in openmw_stats_window.layout, if custom options are not present in .layout, a default is loaded + // initial values defined in openmw_stats_window.layout, if custom options are not present in .layout, a default + // is loaded float leftPaneRatio = 0.44f; if (mLeftPane->isUserString("LeftPaneRatio")) leftPaneRatio = MyGUI::utility::parseFloat(mLeftPane->getUserString("LeftPaneRatio")); @@ -105,28 +111,30 @@ namespace MWGui int minLeftWidth = static_cast(mMinFullWidth * leftPaneRatio); int minLeftOffsetWidth = minLeftWidth + leftOffsetWidth; - //if there's no space for right pane + // if there's no space for right pane mRightPane->setVisible(windowWidth >= minLeftOffsetWidth); if (!mRightPane->getVisible()) { mLeftPane->setCoord(MyGUI::IntCoord(0, 0, windowWidth - leftOffsetWidth, windowHeight)); } - //if there's some space for right pane + // if there's some space for right pane else if (windowWidth < mMinFullWidth) { mLeftPane->setCoord(MyGUI::IntCoord(0, 0, minLeftWidth, windowHeight)); mRightPane->setCoord(MyGUI::IntCoord(minLeftWidth, 0, windowWidth - minLeftWidth, windowHeight)); } - //if there's enough space for both panes + // if there's enough space for both panes else { - mLeftPane->setCoord(MyGUI::IntCoord(0, 0, static_cast(leftPaneRatio*windowWidth), windowHeight)); - mRightPane->setCoord(MyGUI::IntCoord(static_cast(leftPaneRatio*windowWidth), 0, static_cast(rightPaneRatio*windowWidth), windowHeight)); + mLeftPane->setCoord(MyGUI::IntCoord(0, 0, static_cast(leftPaneRatio * windowWidth), windowHeight)); + mRightPane->setCoord(MyGUI::IntCoord(static_cast(leftPaneRatio * windowWidth), 0, + static_cast(rightPaneRatio * windowWidth), windowHeight)); } - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden mSkillView->setVisibleVScroll(false); - mSkillView->setCanvasSize (mSkillView->getWidth(), mSkillView->getCanvasSize().height); + mSkillView->setCanvasSize(mSkillView->getWidth(), mSkillView->getCanvasSize().height); mSkillView->setVisibleVScroll(true); } @@ -148,48 +156,36 @@ namespace MWGui mMainWidget->castType()->setCaption(playerName); } - void StatsWindow::setValue (const std::string& id, const MWMechanics::AttributeValue& value) + void StatsWindow::setAttribute(ESM::RefId id, const MWMechanics::AttributeValue& value) { - static const char *ids[] = + auto it = mAttributeWidgets.find(id); + if (it != mAttributeWidgets.end()) { - "AttribVal1", "AttribVal2", "AttribVal3", "AttribVal4", "AttribVal5", - "AttribVal6", "AttribVal7", "AttribVal8", - 0 - }; - - for (int i=0; ids[i]; ++i) - if (ids[i]==id) - { - setText (id, std::to_string(static_cast(value.getModified()))); - - MyGUI::TextBox* box; - getWidget(box, id); - - if (value.getModified()>value.getBase()) - box->_setWidgetState("increased"); - else if (value.getModified()_setWidgetState("decreased"); - else - box->_setWidgetState("normal"); - - break; - } + MyGUI::TextBox* box = it->second; + box->setCaption(std::to_string(static_cast(value.getModified()))); + if (value.getModified() > value.getBase()) + box->_setWidgetState("increased"); + else if (value.getModified() < value.getBase()) + box->_setWidgetState("decreased"); + else + box->_setWidgetState("normal"); + } } - void StatsWindow::setValue (const std::string& id, const MWMechanics::DynamicStat& value) + void StatsWindow::setValue(std::string_view id, const MWMechanics::DynamicStat& value) { int current = static_cast(value.getCurrent()); - int modified = static_cast(value.getModified()); + int modified = static_cast(value.getModified(false)); // Fatigue can be negative if (id != "FBar") current = std::max(0, current); - setBar (id, id + "T", current, modified); + setBar(std::string(id), std::string(id) + "T", current, modified); // health, magicka, fatigue tooltip MyGUI::Widget* w; - std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); + std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); if (id == "HBar") { getWidget(w, "Health"); @@ -207,19 +203,19 @@ namespace MWGui } } - void StatsWindow::setValue (const std::string& id, const std::string& value) + void StatsWindow::setValue(std::string_view id, const std::string& value) { - if (id=="name") - setPlayerName (value); - else if (id=="race") - setText ("RaceText", value); - else if (id=="class") - setText ("ClassText", value); + if (id == "name") + setPlayerName(value); + else if (id == "race") + setText("RaceText", value); + else if (id == "class") + setText("ClassText", value); } - void StatsWindow::setValue (const std::string& id, int value) + void StatsWindow::setValue(std::string_view id, int value) { - if (id=="level") + if (id == "level") { std::ostringstream text; text << value; @@ -227,14 +223,13 @@ namespace MWGui } } - void setSkillProgress(MyGUI::Widget* w, float progress, int skillId) + void setSkillProgress(MyGUI::Widget* w, float progress, ESM::RefId skillId) { MWWorld::Ptr player = MWMechanics::getPlayer(); - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); - float progressRequirement = player.getClass().getNpcStats(player).getSkillProgressRequirement(skillId, - *esmStore.get().find(player.get()->mBase->mClass)); + float progressRequirement = player.getClass().getNpcStats(player).getSkillProgressRequirement( + skillId, *esmStore.get().find(player.get()->mBase->mClass)); // This is how vanilla MW displays the progress bar (I think). Note it's slightly inaccurate, // due to the int casting in the skill levelup logic. Also the progress label could in rare cases @@ -242,14 +237,14 @@ namespace MWGui // Leaving the original display logic for now, for consistency with ess-imported savegames. int progressPercent = int(float(progress) / float(progressRequirement) * 100.f + 0.5f); - w->setUserString("Caption_SkillProgressText", MyGUI::utility::toString(progressPercent)+"/100"); + w->setUserString("Caption_SkillProgressText", MyGUI::utility::toString(progressPercent) + "/100"); w->setUserString("RangePosition_SkillProgress", MyGUI::utility::toString(progressPercent)); } - void StatsWindow::setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) + void StatsWindow::setValue(ESM::RefId id, const MWMechanics::SkillValue& value) { - mSkillValues[parSkill] = value; - std::pair widgets = mSkillWidgetMap[(int)parSkill]; + mSkillValues[id] = value; + std::pair widgets = mSkillWidgetMap[id]; MyGUI::TextBox* valueWidget = widgets.second; MyGUI::TextBox* nameWidget = widgets.first; if (valueWidget && nameWidget) @@ -270,8 +265,9 @@ namespace MWGui int widthAfter = valueWidget->getTextSize().width; if (widthBefore != widthAfter) { - valueWidget->setCoord(valueWidget->getLeft() - (widthAfter-widthBefore), valueWidget->getTop(), valueWidget->getWidth() + (widthAfter-widthBefore), valueWidget->getHeight()); - nameWidget->setSize(nameWidget->getWidth() - (widthAfter-widthBefore), nameWidget->getHeight()); + valueWidget->setCoord(valueWidget->getLeft() - (widthAfter - widthBefore), valueWidget->getTop(), + valueWidget->getWidth() + (widthAfter - widthBefore), valueWidget->getHeight()); + nameWidget->setSize(nameWidget->getWidth() - (widthAfter - widthBefore), nameWidget->getHeight()); } if (value.getBase() < 100) @@ -286,8 +282,8 @@ namespace MWGui valueWidget->setUserString("Visible_SkillProgressVBox", "true"); valueWidget->setUserString("UserData^Hidden_SkillProgressVBox", "false"); - setSkillProgress(nameWidget, value.getProgress(), parSkill); - setSkillProgress(valueWidget, value.getProgress(), parSkill); + setSkillProgress(nameWidget, value.getProgress(), id); + setSkillProgress(valueWidget, value.getProgress(), id); } else { @@ -304,71 +300,79 @@ namespace MWGui } } - void StatsWindow::configureSkills (const std::vector& major, const std::vector& minor) + void StatsWindow::configureSkills(const std::vector& major, const std::vector& minor) { mMajorSkills = major; mMinorSkills = minor; // Update misc skills with the remaining skills not in major or minor - std::set skillSet; + std::set skillSet; std::copy(major.begin(), major.end(), std::inserter(skillSet, skillSet.begin())); std::copy(minor.begin(), minor.end(), std::inserter(skillSet, skillSet.begin())); mMiscSkills.clear(); - for (const int skill : ESM::Skill::sSkillIds) + const auto& store = MWBase::Environment::get().getWorld()->getStore().get(); + for (const auto& skill : store) { - if (skillSet.find(skill) == skillSet.end()) - mMiscSkills.push_back(skill); + if (!skillSet.contains(skill.mId)) + mMiscSkills.push_back(skill.mId); } updateSkillArea(); } - void StatsWindow::onFrame (float dt) + void StatsWindow::onFrame(float dt) { NoDrop::onFrame(dt); MWWorld::Ptr player = MWMechanics::getPlayer(); - const MWMechanics::NpcStats &PCstats = player.getClass().getNpcStats(player); + const MWMechanics::NpcStats& PCstats = player.getClass().getNpcStats(player); + const auto& store = MWBase::Environment::get().getESMStore(); + + std::stringstream detail; + bool first = true; + for (const auto& attribute : store->get()) + { + float mult = PCstats.getLevelupAttributeMultiplier(attribute.mId); + mult = std::min(mult, 100 - PCstats.getAttribute(attribute.mId).getBase()); + if (mult > 1) + { + if (!first) + detail << '\n'; + detail << attribute.mName << " x" << MyGUI::utility::toString(mult); + first = false; + } + } + std::string detailText = detail.str(); // level progress MyGUI::Widget* levelWidget; - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { - int max = MWBase::Environment::get().getWorld()->getStore().get().find("iLevelUpTotal")->mValue.getInteger(); - getWidget(levelWidget, i==0 ? "Level_str" : "LevelText"); + int max = store->get().find("iLevelUpTotal")->mValue.getInteger(); + getWidget(levelWidget, i == 0 ? "Level_str" : "LevelText"); - levelWidget->setUserString("RangePosition_LevelProgress", MyGUI::utility::toString(PCstats.getLevelProgress())); + levelWidget->setUserString( + "RangePosition_LevelProgress", MyGUI::utility::toString(PCstats.getLevelProgress())); levelWidget->setUserString("Range_LevelProgress", MyGUI::utility::toString(max)); - levelWidget->setUserString("Caption_LevelProgressText", MyGUI::utility::toString(PCstats.getLevelProgress()) + "/" - + MyGUI::utility::toString(max)); - } - std::stringstream detail; - for (int attribute = 0; attribute < ESM::Attribute::Length; ++attribute) - { - float mult = PCstats.getLevelupAttributeMultiplier(attribute); - mult = std::min(mult, 100 - PCstats.getAttribute(attribute).getBase()); - if (mult > 1) - detail << (detail.str().empty() ? "" : "\n") << "#{" - << MyGUI::TextIterator::toTagsString(ESM::Attribute::sGmstAttributeIds[attribute]) - << "} x" << MyGUI::utility::toString(mult); + levelWidget->setUserString("Caption_LevelProgressText", + MyGUI::utility::toString(PCstats.getLevelProgress()) + "/" + MyGUI::utility::toString(max)); + levelWidget->setUserString("Caption_LevelDetailText", detailText); } - levelWidget->setUserString("Caption_LevelDetailText", MyGUI::LanguageManager::getInstance().replaceTags(detail.str())); setFactions(PCstats.getFactionRanks()); - setExpelled(PCstats.getExpelled ()); + setExpelled(PCstats.getExpelled()); - const std::string &signId = - MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); + const auto& signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); setBirthSign(signId); - setReputation (PCstats.getReputation ()); - setBounty (PCstats.getBounty ()); + setReputation(PCstats.getReputation()); + setBounty(PCstats.getBounty()); if (mChanged) updateSkillArea(); } - void StatsWindow::setFactions (const FactionList& factions) + void StatsWindow::setFactions(const FactionList& factions) { if (mFactions != factions) { @@ -377,7 +381,7 @@ namespace MWGui } } - void StatsWindow::setExpelled (const std::set& expelled) + void StatsWindow::setExpelled(const std::set& expelled) { if (mExpelled != expelled) { @@ -386,7 +390,7 @@ namespace MWGui } } - void StatsWindow::setBirthSign (const std::string& signId) + void StatsWindow::setBirthSign(const ESM::RefId& signId) { if (signId != mBirthSignId) { @@ -395,7 +399,7 @@ namespace MWGui } } - void StatsWindow::addSeparator(MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) + void StatsWindow::addSeparator(MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { MyGUI::ImageBox* separator = mSkillView->createWidget("MW_HLine", MyGUI::IntCoord(10, coord1.top, coord1.width + coord2.width - 4, 18), @@ -407,49 +411,53 @@ namespace MWGui coord2.top += separator->getHeight(); } - void StatsWindow::addGroup(const std::string &label, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) + void StatsWindow::addGroup(std::string_view label, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { MyGUI::TextBox* groupWidget = mSkillView->createWidget("SandBrightText", MyGUI::IntCoord(0, coord1.top, coord1.width + coord2.width, coord1.height), MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - groupWidget->setCaption(label); + groupWidget->setCaption(MyGUI::UString(label)); groupWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); mSkillWidgets.push_back(groupWidget); - int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; + const int lineHeight = Settings::gui().mFontSize + 2; coord1.top += lineHeight; coord2.top += lineHeight; } - std::pair StatsWindow::addValueItem(const std::string& text, const std::string &value, const std::string& state, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) + std::pair StatsWindow::addValueItem(std::string_view text, + const std::string& value, const std::string& state, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { MyGUI::TextBox *skillNameWidget, *skillValueWidget; - skillNameWidget = mSkillView->createWidget("SandText", coord1, MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - skillNameWidget->setCaption(text); + skillNameWidget = mSkillView->createWidget( + "SandText", coord1, MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); + skillNameWidget->setCaption(MyGUI::UString(text)); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); - skillValueWidget = mSkillView->createWidget("SandTextRight", coord2, MyGUI::Align::Right | MyGUI::Align::Top); + skillValueWidget = mSkillView->createWidget( + "SandTextRight", coord2, MyGUI::Align::Right | MyGUI::Align::Top); skillValueWidget->setCaption(value); skillValueWidget->_setWidgetState(state); skillValueWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); // resize dynamically according to text size int textWidthPlusMargin = skillValueWidget->getTextSize().width + 12; - skillValueWidget->setCoord(coord2.left + coord2.width - textWidthPlusMargin, coord2.top, textWidthPlusMargin, coord2.height); + skillValueWidget->setCoord( + coord2.left + coord2.width - textWidthPlusMargin, coord2.top, textWidthPlusMargin, coord2.height); skillNameWidget->setSize(skillNameWidget->getSize() + MyGUI::IntSize(coord2.width - textWidthPlusMargin, 0)); mSkillWidgets.push_back(skillNameWidget); mSkillWidgets.push_back(skillValueWidget); - int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; + const int lineHeight = Settings::gui().mFontSize + 2; coord1.top += lineHeight; coord2.top += lineHeight; return std::make_pair(skillNameWidget, skillValueWidget); } - MyGUI::Widget* StatsWindow::addItem(const std::string& text, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) + MyGUI::Widget* StatsWindow::addItem(const std::string& text, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { MyGUI::TextBox* skillNameWidget; @@ -463,14 +471,15 @@ namespace MWGui mSkillWidgets.push_back(skillNameWidget); - int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; + const int lineHeight = Settings::gui().mFontSize + 2; coord1.top += lineHeight; coord2.top += lineHeight; return skillNameWidget; } - void StatsWindow::addSkills(const SkillList &skills, const std::string &titleId, const std::string &titleDefault, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2) + void StatsWindow::addSkills(const std::vector& skills, const std::string& titleId, + const std::string& titleDefault, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2) { // Add a line separator if there are items above if (!mSkillWidgets.empty()) @@ -478,40 +487,45 @@ namespace MWGui addSeparator(coord1, coord2); } - addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString(titleId, titleDefault), coord1, coord2); + addGroup( + MWBase::Environment::get().getWindowManager()->getGameSettingString(titleId, titleDefault), coord1, coord2); - for (const int skillId : skills) + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); + for (const ESM::RefId& skillId : skills) { - if (skillId < 0 || skillId >= ESM::Skill::Length) // Skip unknown skill indexes + const ESM::Skill* skill = esmStore.get().search(skillId); + if (!skill) // Skip unknown skills continue; - const std::string &skillNameId = ESM::Skill::sSkillNameIds[skillId]; - - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); - - const ESM::Skill* skill = esmStore.get().find(skillId); - std::string icon = "icons\\k\\" + ESM::Skill::sIconNames[skillId]; + auto skillValue = mSkillValues.find(skill->mId); + if (skillValue == mSkillValues.end()) + { + Log(Debug::Error) << "Failed to update stats window: can not find value for skill " << skill->mId; + continue; + } - const ESM::Attribute* attr = - esmStore.get().find(skill->mData.mAttribute); + const ESM::Attribute* attr + = esmStore.get().find(ESM::Attribute::indexToRefId(skill->mData.mAttribute)); - std::pair widgets = addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString(skillNameId, skillNameId), - "", "normal", coord1, coord2); - mSkillWidgetMap[skillId] = widgets; + std::pair widgets + = addValueItem(skill->mName, {}, "normal", coord1, coord2); + mSkillWidgetMap[skill->mId] = std::move(widgets); - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipType", "Layout"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipLayout", "SkillToolTip"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillName", "#{"+skillNameId+"}"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillDescription", skill->mDescription); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillAttribute", "#{sGoverningAttribute}: #{" + attr->mName + "}"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ImageTexture_SkillImage", icon); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Range_SkillProgress", "100"); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString("ToolTipType", "Layout"); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString("ToolTipLayout", "SkillToolTip"); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString( + "Caption_SkillName", MyGUI::TextIterator::toTagsString(skill->mName)); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString( + "Caption_SkillDescription", skill->mDescription); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString("Caption_SkillAttribute", + "#{sGoverningAttribute}: " + MyGUI::TextIterator::toTagsString(attr->mName)); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString("ImageTexture_SkillImage", skill->mIcon); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString("Range_SkillProgress", "100"); } - setValue(static_cast(skillId), mSkillValues.find(skillId)->second); + setValue(skill->mId, skillValue->second); } } @@ -538,10 +552,9 @@ namespace MWGui if (!mMiscSkills.empty()) addSkills(mMiscSkills, "sSkillClassMisc", "Misc Skills", coord1, coord2); - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::ESMStore &store = world->getStore(); - const ESM::NPC *player = - world->getPlayerPtr().get()->mBase; + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::ESMStore& store = world->getStore(); + const ESM::NPC* player = world->getPlayerPtr().get()->mBase; // race tooltip const ESM::Race* playerRace = store.get().find(player->mRace); @@ -555,8 +568,7 @@ namespace MWGui // class tooltip MyGUI::Widget* classWidget; - const ESM::Class *playerClass = - store.get().find(player->mClass); + const ESM::Class* playerClass = store.get().find(player->mClass); getWidget(classWidget, "ClassText"); ToolTips::createClassToolTip(classWidget, *playerClass); @@ -566,15 +578,13 @@ namespace MWGui if (!mFactions.empty()) { MWWorld::Ptr playerPtr = MWMechanics::getPlayer(); - const MWMechanics::NpcStats &PCstats = playerPtr.getClass().getNpcStats(playerPtr); - const std::set &expelled = PCstats.getExpelled(); + const MWMechanics::NpcStats& PCstats = playerPtr.getClass().getNpcStats(playerPtr); + const std::set& expelled = PCstats.getExpelled(); - bool firstFaction=true; - for (auto& factionPair : mFactions) + bool firstFaction = true; + for (const auto& [factionId, factionRank] : mFactions) { - const std::string& factionId = factionPair.first; - const ESM::Faction *faction = - store.get().find(factionId); + const ESM::Faction* faction = store.get().find(factionId); if (faction->mData.mIsHidden == 1) continue; @@ -584,7 +594,8 @@ namespace MWGui if (!mSkillWidgets.empty()) addSeparator(coord1, coord2); - addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sFaction", "Faction"), coord1, coord2); + addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sFaction", "Faction"), + coord1, coord2); firstFaction = false; } @@ -599,35 +610,38 @@ namespace MWGui text += "\n#{fontcolourhtml=normal}#{sExpelled}"; else { - int rank = factionPair.second; - rank = std::max(0, std::min(9, rank)); - text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; - - if (rank < 9) + const auto rank = static_cast(std::max(0, factionRank)); + if (rank < faction->mRanks.size()) + text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; + if (rank + 1 < faction->mRanks.size() && !faction->mRanks[rank + 1].empty()) { // player doesn't have max rank yet - text += std::string("\n\n#{fontcolourhtml=header}#{sNextRank} ") + faction->mRanks[rank+1]; + text += std::string("\n\n#{fontcolourhtml=header}#{sNextRank} ") + faction->mRanks[rank + 1]; - ESM::RankData rankData = faction->mData.mRankData[rank+1]; - const ESM::Attribute* attr1 = store.get().find(faction->mData.mAttribute[0]); - const ESM::Attribute* attr2 = store.get().find(faction->mData.mAttribute[1]); + const ESM::RankData& rankData = faction->mData.mRankData[rank + 1]; + const ESM::Attribute* attr1 = store.get().find( + ESM::Attribute::indexToRefId(faction->mData.mAttribute[0])); + const ESM::Attribute* attr2 = store.get().find( + ESM::Attribute::indexToRefId(faction->mData.mAttribute[1])); - text += "\n#{fontcolourhtml=normal}#{" + attr1->mName + "}: " + MyGUI::utility::toString(rankData.mAttribute1) - + ", #{" + attr2->mName + "}: " + MyGUI::utility::toString(rankData.mAttribute2); + text += "\n#{fontcolourhtml=normal}" + MyGUI::TextIterator::toTagsString(attr1->mName) + ": " + + MyGUI::utility::toString(rankData.mAttribute1) + ", " + + MyGUI::TextIterator::toTagsString(attr2->mName) + ": " + + MyGUI::utility::toString(rankData.mAttribute2); text += "\n\n#{fontcolourhtml=header}#{sFavoriteSkills}"; text += "\n#{fontcolourhtml=normal}"; bool firstSkill = true; - for (int i=0; i<7; ++i) + for (int id : faction->mData.mSkills) { - if (faction->mData.mSkills[i] != -1) + const ESM::Skill* skill = store.get().search(ESM::Skill::indexToRefId(id)); + if (skill) { if (!firstSkill) text += ", "; firstSkill = false; - - text += "#{"+ESM::Skill::sSkillNameIds[faction->mData.mSkills[i]]+"}"; + text += MyGUI::TextIterator::toTagsString(skill->mName); } } @@ -652,9 +666,9 @@ namespace MWGui if (!mSkillWidgets.empty()) addSeparator(coord1, coord2); - addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sBirthSign", "Sign"), coord1, coord2); - const ESM::BirthSign *sign = - store.get().find(mBirthSignId); + addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sBirthSign", "Sign"), coord1, + coord2); + const ESM::BirthSign* sign = store.get().find(mBirthSignId); MyGUI::Widget* w = addItem(sign->mName, coord1, coord2); ToolTips::createBirthsignToolTip(w, mBirthSignId); @@ -665,34 +679,35 @@ namespace MWGui addSeparator(coord1, coord2); addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString("sReputation", "Reputation"), - MyGUI::utility::toString(static_cast(mReputation)), "normal", coord1, coord2); + MyGUI::utility::toString(static_cast(mReputation)), "normal", coord1, coord2); - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipType", "Layout"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipLayout", "TextToolTip"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_Text", "#{sSkillsMenuReputationHelp}"); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString("ToolTipType", "Layout"); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString("ToolTipLayout", "TextToolTip"); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString("Caption_Text", "#{sSkillsMenuReputationHelp}"); } addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString("sBounty", "Bounty"), - MyGUI::utility::toString(static_cast(mBounty)), "normal", coord1, coord2); + MyGUI::utility::toString(static_cast(mBounty)), "normal", coord1, coord2); - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipType", "Layout"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ToolTipLayout", "TextToolTip"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_Text", "#{sCrimeHelp}"); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString("ToolTipType", "Layout"); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString("ToolTipLayout", "TextToolTip"); + mSkillWidgets[mSkillWidgets.size() - 1 - i]->setUserString("Caption_Text", "#{sCrimeHelp}"); } - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden mSkillView->setVisibleVScroll(false); - mSkillView->setCanvasSize (mSkillView->getWidth(), std::max(mSkillView->getHeight(), coord1.top)); + mSkillView->setCanvasSize(mSkillView->getWidth(), std::max(mSkillView->getHeight(), coord1.top)); mSkillView->setVisibleVScroll(true); } void StatsWindow::onPinToggled() { - Settings::Manager::setBool("stats pin", "Windows", mPinned); + Settings::windows().mStatsPin.set(mPinned); MWBase::Environment::get().getWindowManager()->setHMSVisibility(!mPinned); } diff --git a/apps/openmw/mwgui/statswindow.hpp b/apps/openmw/mwgui/statswindow.hpp index bf78cde34a6..a3fc3157c5f 100644 --- a/apps/openmw/mwgui/statswindow.hpp +++ b/apps/openmw/mwgui/statswindow.hpp @@ -3,73 +3,88 @@ #include "statswatcher.hpp" #include "windowpinnablebase.hpp" +#include +#include namespace MWGui { class StatsWindow : public WindowPinnableBase, public NoDrop, public StatsListener { - public: - typedef std::map FactionList; - - typedef std::vector SkillList; - - StatsWindow(DragAndDrop* drag); - - /// automatically updates all the data in the stats window, but only if it has changed. - void onFrame(float dt) override; - - void setBar(const std::string& name, const std::string& tname, int val, int max); - void setPlayerName(const std::string& playerName); - - /// Set value for the given ID. - void setValue (const std::string& id, const MWMechanics::AttributeValue& value) override; - void setValue (const std::string& id, const MWMechanics::DynamicStat& value) override; - void setValue (const std::string& id, const std::string& value) override; - void setValue (const std::string& id, int value) override; - void setValue(const ESM::Skill::SkillEnum parSkill, const MWMechanics::SkillValue& value) override; - void configureSkills(const SkillList& major, const SkillList& minor) override; - - void setReputation (int reputation) { if (reputation != mReputation) mChanged = true; this->mReputation = reputation; } - void setBounty (int bounty) { if (bounty != mBounty) mChanged = true; this->mBounty = bounty; } - void updateSkillArea(); - - void onOpen() override { onWindowResize(mMainWidget->castType()); } - - private: - void addSkills(const SkillList &skills, const std::string &titleId, const std::string &titleDefault, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); - void addSeparator(MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); - void addGroup(const std::string &label, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); - std::pair addValueItem(const std::string& text, const std::string &value, const std::string& state, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); - MyGUI::Widget* addItem(const std::string& text, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); - - void setFactions (const FactionList& factions); - void setExpelled (const std::set& expelled); - void setBirthSign (const std::string &signId); - - void onWindowResize(MyGUI::Window* window); - void onMouseWheel(MyGUI::Widget* _sender, int _rel); - - MyGUI::Widget* mLeftPane; - MyGUI::Widget* mRightPane; - - MyGUI::ScrollView* mSkillView; - - SkillList mMajorSkills, mMinorSkills, mMiscSkills; - std::map mSkillValues; - std::map > mSkillWidgetMap; - std::map mFactionWidgetMap; - FactionList mFactions; ///< Stores a list of factions and the current rank - std::string mBirthSignId; - int mReputation, mBounty; - std::vector mSkillWidgets; //< Skills and other information - std::set mExpelled; - - bool mChanged; - const int mMinFullWidth; - - protected: - void onPinToggled() override; - void onTitleDoubleClicked() override; + public: + typedef std::map FactionList; + + StatsWindow(DragAndDrop* drag); + + /// automatically updates all the data in the stats window, but only if it has changed. + void onFrame(float dt) override; + + void setBar(const std::string& name, const std::string& tname, int val, int max); + void setPlayerName(const std::string& playerName); + + /// Set value for the given ID. + void setAttribute(ESM::RefId id, const MWMechanics::AttributeValue& value) override; + void setValue(std::string_view id, const MWMechanics::DynamicStat& value) override; + void setValue(std::string_view id, const std::string& value) override; + void setValue(std::string_view id, int value) override; + void setValue(ESM::RefId id, const MWMechanics::SkillValue& value) override; + void configureSkills(const std::vector& major, const std::vector& minor) override; + + void setReputation(int reputation) + { + if (reputation != mReputation) + mChanged = true; + this->mReputation = reputation; + } + void setBounty(int bounty) + { + if (bounty != mBounty) + mChanged = true; + this->mBounty = bounty; + } + void updateSkillArea(); + + void onOpen() override { onWindowResize(mMainWidget->castType()); } + + std::string_view getWindowIdForLua() const override { return "Stats"; } + + private: + void addSkills(const std::vector& skills, const std::string& titleId, + const std::string& titleDefault, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); + void addSeparator(MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); + void addGroup(std::string_view label, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); + std::pair addValueItem(std::string_view text, const std::string& value, + const std::string& state, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); + MyGUI::Widget* addItem(const std::string& text, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); + + void setFactions(const FactionList& factions); + void setExpelled(const std::set& expelled); + void setBirthSign(const ESM::RefId& signId); + + void onWindowResize(MyGUI::Window* window); + void onMouseWheel(MyGUI::Widget* _sender, int _rel); + + MyGUI::Widget* mLeftPane; + MyGUI::Widget* mRightPane; + + MyGUI::ScrollView* mSkillView; + + std::vector mMajorSkills, mMinorSkills, mMiscSkills; + std::map mSkillValues; + std::map mAttributeWidgets; + std::map> mSkillWidgetMap; + std::map mFactionWidgetMap; + FactionList mFactions; ///< Stores a list of factions and the current rank + ESM::RefId mBirthSignId; + int mReputation, mBounty; + std::vector mSkillWidgets; //< Skills and other information + std::set mExpelled; + + bool mChanged; + const int mMinFullWidth; + + protected: + void onPinToggled() override; + void onTitleDoubleClicked() override; }; } #endif diff --git a/apps/openmw/mwgui/textinput.cpp b/apps/openmw/mwgui/textinput.cpp index 54f2d3be9bb..5f47b96f033 100644 --- a/apps/openmw/mwgui/textinput.cpp +++ b/apps/openmw/mwgui/textinput.cpp @@ -1,16 +1,17 @@ #include "textinput.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" -#include #include +#include +#include namespace MWGui { TextInputDialog::TextInputDialog() - : WindowModal("openmw_text_input.layout") + : WindowModal("openmw_text_input.layout") { // Centre dialog center(); @@ -32,12 +33,14 @@ namespace MWGui getWidget(okButton, "OKButton"); if (shown) - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else - okButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", "")); + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } - void TextInputDialog::setTextLabel(const std::string &label) + void TextInputDialog::setTextLabel(std::string_view label) { setText("LabelT", label); } @@ -53,16 +56,16 @@ namespace MWGui void TextInputDialog::onOkClicked(MyGUI::Widget* _sender) { - if (mTextEdit->getCaption() == "") + if (mTextEdit->getCaption().empty()) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage37}"); - MWBase::Environment::get().getWindowManager()->setKeyFocusWidget (mTextEdit); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage37}"); + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } else eventDone(this); } - void TextInputDialog::onTextAccepted(MyGUI::Edit* _sender) + void TextInputDialog::onTextAccepted(MyGUI::EditBox* _sender) { onOkClicked(_sender); @@ -75,10 +78,9 @@ namespace MWGui return mTextEdit->getCaption(); } - void TextInputDialog::setTextInput(const std::string &text) + void TextInputDialog::setTextInput(const std::string& text) { mTextEdit->setCaption(text); } - } diff --git a/apps/openmw/mwgui/textinput.hpp b/apps/openmw/mwgui/textinput.hpp index 4d365eb44cd..c11d40f1a97 100644 --- a/apps/openmw/mwgui/textinput.hpp +++ b/apps/openmw/mwgui/textinput.hpp @@ -11,10 +11,10 @@ namespace MWGui TextInputDialog(); std::string getTextInput() const; - void setTextInput(const std::string &text); + void setTextInput(const std::string& text); void setNextButtonShow(bool shown); - void setTextLabel(const std::string &label); + void setTextLabel(std::string_view label); void onOpen() override; bool exit() override { return false; } @@ -26,7 +26,7 @@ namespace MWGui protected: void onOkClicked(MyGUI::Widget* _sender); - void onTextAccepted(MyGUI::Edit* _sender); + void onTextAccepted(MyGUI::EditBox* _sender); private: MyGUI::EditBox* mTextEdit; diff --git a/apps/openmw/mwgui/timeadvancer.cpp b/apps/openmw/mwgui/timeadvancer.cpp index c38094ae45d..2cdab127b9a 100644 --- a/apps/openmw/mwgui/timeadvancer.cpp +++ b/apps/openmw/mwgui/timeadvancer.cpp @@ -3,12 +3,12 @@ namespace MWGui { TimeAdvancer::TimeAdvancer(float delay) - : mRunning(false), - mCurHour(0), - mHours(1), - mInterruptAt(-1), - mDelay(delay), - mRemainingTime(delay) + : mRunning(false) + , mCurHour(0) + , mHours(1) + , mInterruptAt(-1) + , mDelay(delay) + , mRemainingTime(delay) { } @@ -54,7 +54,6 @@ namespace MWGui eventFinished(); return; } - } } diff --git a/apps/openmw/mwgui/timeadvancer.hpp b/apps/openmw/mwgui/timeadvancer.hpp index b8456f3761a..bb6aa649cba 100644 --- a/apps/openmw/mwgui/timeadvancer.hpp +++ b/apps/openmw/mwgui/timeadvancer.hpp @@ -1,39 +1,39 @@ #ifndef MWGUI_TIMEADVANCER_H #define MWGUI_TIMEADVANCER_H -#include +#include namespace MWGui { class TimeAdvancer { - public: - TimeAdvancer(float delay); + public: + TimeAdvancer(float delay); - void run(int hours, int interruptAt=-1); - void stop(); - void onFrame(float dt); + void run(int hours, int interruptAt = -1); + void stop(); + void onFrame(float dt); - int getHours() const; - bool isRunning() const; + int getHours() const; + bool isRunning() const; - // signals - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; - typedef MyGUI::delegates::CMultiDelegate2 EventHandle_IntInt; + // signals + typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; + typedef MyGUI::delegates::MultiDelegate EventHandle_IntInt; - EventHandle_IntInt eventProgressChanged; - EventHandle_Void eventInterrupted; - EventHandle_Void eventFinished; + EventHandle_IntInt eventProgressChanged; + EventHandle_Void eventInterrupted; + EventHandle_Void eventFinished; - private: - bool mRunning; + private: + bool mRunning; - int mCurHour; - int mHours; - int mInterruptAt; + int mCurHour; + int mHours; + int mInterruptAt; - float mDelay; - float mRemainingTime; + float mDelay; + float mRemainingTime; }; } diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index a821d0106b4..960a4a5a218 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -3,44 +3,44 @@ #include #include -#include -#include #include +#include +#include +#include +#include -#include +#include +#include +#include +#include #include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/spellutil.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/spellutil.hpp" -#include "../mwmechanics/actorutil.hpp" -#include "mapwindow.hpp" #include "inventorywindow.hpp" +#include "mapwindow.hpp" #include "itemmodel.hpp" namespace MWGui { - std::string ToolTips::sSchoolNames[] = {"#{sSchoolAlteration}", "#{sSchoolConjuration}", "#{sSchoolDestruction}", "#{sSchoolIllusion}", "#{sSchoolMysticism}", "#{sSchoolRestoration}"}; - - ToolTips::ToolTips() : - Layout("openmw_tooltips.layout") + ToolTips::ToolTips() + : Layout("openmw_tooltips.layout") , mFocusToolTipX(0.0) , mFocusToolTipY(0.0) , mHorizontalScrollIndex(0) - , mDelay(0.0) - , mRemainingDelay(0.0) + , mRemainingDelay(Settings::gui().mTooltipDelay) , mLastMouseX(0) , mLastMouseY(0) , mEnabled(true) , mFullHelp(false) - , mShowOwned(0) , mFrameDuration(0.f) { getWidget(mDynamicToolTipBox, "DynamicToolTipBox"); @@ -52,15 +52,10 @@ namespace MWGui mDynamicToolTipBox->setNeedMouseFocus(false); mMainWidget->setNeedMouseFocus(false); - mDelay = Settings::Manager::getFloat("tooltip delay", "GUI"); - mRemainingDelay = mDelay; - - for (unsigned int i=0; i < mMainWidget->getChildCount(); ++i) + for (unsigned int i = 0; i < mMainWidget->getChildCount(); ++i) { mMainWidget->getChildAt(i)->setVisible(false); } - - mShowOwned = Settings::Manager::getInt("show owned", "Game"); } void ToolTips::setEnabled(bool enabled) @@ -82,19 +77,19 @@ namespace MWGui } // start by hiding everything - for (unsigned int i=0; i < mMainWidget->getChildCount(); ++i) + for (unsigned int i = 0; i < mMainWidget->getChildCount(); ++i) { mMainWidget->getChildAt(i)->setVisible(false); } - const MyGUI::IntSize &viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); if (!mEnabled) { return; } - MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); bool guiMode = winMgr->isGuiMode(); if (guiMode) @@ -103,12 +98,11 @@ namespace MWGui return; const MyGUI::IntPoint& mousePos = MyGUI::InputManager::getInstance().getMousePosition(); - if (winMgr->getWorldMouseOver() && - (winMgr->isConsoleMode() || - (winMgr->getMode() == GM_Container) || - (winMgr->getMode() == GM_Inventory))) + if (winMgr->getWorldMouseOver() + && (winMgr->isConsoleMode() || (winMgr->getMode() == GM_Container) + || (winMgr->getMode() == GM_Inventory))) { - if (mFocusObject.isEmpty ()) + if (mFocusObject.isEmpty()) return; const MWWorld::Class& objectclass = mFocusObject.getClass(); @@ -121,12 +115,12 @@ namespace MWGui ToolTipInfo info; info.caption = mFocusObject.getClass().getName(mFocusObject); if (info.caption.empty()) - info.caption=mFocusObject.getCellRef().getRefId(); - info.icon=""; + info.caption = mFocusObject.getCellRef().getRefId().toDebugString(); + info.icon.clear(); tooltipSize = createToolTip(info, checkOwned()); } else - tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), true); + tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), true); MyGUI::IntPoint tooltipPosition = MyGUI::InputManager::getInstance().getMousePosition(); position(tooltipPosition, tooltipSize, viewSize); @@ -143,12 +137,11 @@ namespace MWGui else { mHorizontalScrollIndex = 0; - mRemainingDelay = mDelay; + mRemainingDelay = Settings::gui().mTooltipDelay; } mLastMouseX = mousePos.left; mLastMouseY = mousePos.top; - if (mRemainingDelay > 0) return; @@ -160,23 +153,20 @@ namespace MWGui // try to go 1 level up until there is a widget that has tooltip // this is necessary because some skin elements are actually separate widgets - int i=0; while (!focus->isUserString("ToolTipType")) { focus = focus->getParent(); if (!focus) return; - ++i; } - std::string type = focus->getUserString("ToolTipType"); + std::string_view type = focus->getUserString("ToolTipType"); - if (type == "") + if (type.empty()) { return; } - // special handling for markers on the local map: the tooltip should only be visible // if the marker is not hidden due to the fog of war. if (type == "MapMarker") @@ -194,14 +184,15 @@ namespace MWGui else if (type == "ItemPtr") { mFocusObject = *focus->getUserData(); - if (!mFocusObject) + if (mFocusObject.isEmpty()) return; - tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), false, checkOwned()); + tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), false, checkOwned()); } else if (type == "ItemModelIndex") { - std::pair pair = *focus->getUserData >(); + std::pair pair + = *focus->getUserData>(); mFocusObject = pair.second->getItem(pair.first).mBase; bool isAllowedToUse = pair.second->allowedToUseItems(); tooltipSize = getToolTipViaPtr(pair.second->getItem(pair.first).mCount, false, !isAllowedToUse); @@ -213,46 +204,54 @@ namespace MWGui else if (type == "AvatarItemSelection") { MyGUI::IntCoord avatarPos = focus->getAbsoluteCoord(); - MyGUI::IntPoint relMousePos = MyGUI::InputManager::getInstance ().getMousePosition () - MyGUI::IntPoint(avatarPos.left, avatarPos.top); - MWWorld::Ptr item = winMgr->getInventoryWindow ()->getAvatarSelectedItem (relMousePos.left, relMousePos.top); + MyGUI::IntPoint relMousePos = MyGUI::InputManager::getInstance().getMousePosition() + - MyGUI::IntPoint(avatarPos.left, avatarPos.top); + MWWorld::Ptr item + = winMgr->getInventoryWindow()->getAvatarSelectedItem(relMousePos.left, relMousePos.top); mFocusObject = item; - if (!mFocusObject.isEmpty ()) - tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), false); + if (!mFocusObject.isEmpty()) + tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), false); } else if (type == "Spell") { ToolTipInfo info; - const ESM::Spell *spell = - MWBase::Environment::get().getWorld()->getStore().get().find(focus->getUserString("Spell")); + const auto& store = MWBase::Environment::get().getESMStore(); + const ESM::Spell* spell + = store->get().find(ESM::RefId::deserialize(focus->getUserString("Spell"))); info.caption = spell->mName; Widgets::SpellEffectList effects; - for (const ESM::ENAMstruct& spellEffect : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& spellEffect : spell->mEffects.mList) { Widgets::SpellEffectParams params; - params.mEffectID = spellEffect.mEffectID; - params.mSkill = spellEffect.mSkill; - params.mAttribute = spellEffect.mAttribute; - params.mDuration = spellEffect.mDuration; - params.mMagnMin = spellEffect.mMagnMin; - params.mMagnMax = spellEffect.mMagnMax; - params.mRange = spellEffect.mRange; - params.mArea = spellEffect.mArea; + params.mEffectID = spellEffect.mData.mEffectID; + params.mSkill = ESM::Skill::indexToRefId(spellEffect.mData.mSkill); + params.mAttribute = ESM::Attribute::indexToRefId(spellEffect.mData.mAttribute); + params.mDuration = spellEffect.mData.mDuration; + params.mMagnMin = spellEffect.mData.mMagnMin; + params.mMagnMax = spellEffect.mData.mMagnMax; + params.mRange = spellEffect.mData.mRange; + params.mArea = spellEffect.mData.mArea; params.mIsConstant = (spell->mData.mType == ESM::Spell::ST_Ability); params.mNoTarget = false; effects.push_back(params); } - if (MWMechanics::spellIncreasesSkill(spell)) // display school of spells that contribute to skill progress + // display school of spells that contribute to skill progress + if (MWMechanics::spellIncreasesSkill(spell)) { - MWWorld::Ptr player = MWMechanics::getPlayer(); - int school = MWMechanics::getSpellSchool(spell, player); - info.text = "#{sSchool}: " + sSchoolNames[school]; + ESM::RefId id = MWMechanics::getSpellSchool(spell, MWMechanics::getPlayer()); + if (!id.empty()) + { + const auto& school = store->get().find(id)->mSchool; + info.text = "#{sSchool}: " + MyGUI::TextIterator::toTagsString(school->mName).asUTF8(); + } } - std::string cost = focus->getUserString("SpellCost"); - if (cost != "" && cost != "0") - info.text += MWGui::ToolTips::getValueString(spell->mData.mCost, "#{sCastCost}"); - info.effects = effects; + auto cost = focus->getUserString("SpellCost"); + if (!cost.empty() && cost != "0") + info.text + += MWGui::ToolTips::getValueString(MWMechanics::calcSpellCost(*spell), "#{sCastCost}"); + info.effects = std::move(effects); tooltipSize = createToolTip(info); } else if (type == "Layout") @@ -263,20 +262,21 @@ namespace MWGui tooltip->setVisible(true); - std::map userStrings = focus->getUserStrings(); + const auto& userStrings = focus->getUserStrings(); for (auto& userStringPair : userStrings) { size_t underscorePos = userStringPair.first.find('_'); if (underscorePos == std::string::npos) continue; std::string key = userStringPair.first.substr(0, underscorePos); - std::string widgetName = userStringPair.first.substr(underscorePos+1, userStringPair.first.size()-(underscorePos+1)); + std::string_view first = userStringPair.first; + std::string_view widgetName = first.substr(underscorePos + 1); type = "Property"; size_t caretPos = key.find('^'); if (caretPos != std::string::npos) { - type = key.substr(0, caretPos); + type = first.substr(0, caretPos); key.erase(key.begin(), key.begin() + caretPos + 1); } @@ -293,7 +293,7 @@ namespace MWGui tooltip->setCoord(0, 0, tooltipSize.width, tooltipSize.height); } else - throw std::runtime_error ("unknown tooltip type"); + throw std::runtime_error("unknown tooltip type"); MyGUI::IntPoint tooltipPosition = MyGUI::InputManager::getInstance().getMousePosition(); @@ -306,12 +306,11 @@ namespace MWGui { if (!mFocusObject.isEmpty()) { - MyGUI::IntSize tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), true, checkOwned()); + MyGUI::IntSize tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), true, checkOwned()); - setCoord(viewSize.width/2 - tooltipSize.width/2, - std::max(0, int(mFocusToolTipY*viewSize.height - tooltipSize.height)), - tooltipSize.width, - tooltipSize.height); + setCoord(viewSize.width / 2 - tooltipSize.width / 2, + std::max(0, int(mFocusToolTipY * viewSize.height - tooltipSize.height)), tooltipSize.width, + tooltipSize.height); mDynamicToolTipBox->setVisible(true); } @@ -321,7 +320,9 @@ namespace MWGui void ToolTips::position(MyGUI::IntPoint& position, MyGUI::IntSize size, MyGUI::IntSize viewportSize) { position += MyGUI::IntPoint(0, 32) - - MyGUI::IntPoint(static_cast(MyGUI::InputManager::getInstance().getMousePosition().left / float(viewportSize.width) * size.width), 0); + - MyGUI::IntPoint(static_cast(MyGUI::InputManager::getInstance().getMousePosition().left + / float(viewportSize.width) * size.width), + 0); if ((position.left + size.width) > viewportSize.width) { @@ -342,7 +343,7 @@ namespace MWGui MyGUI::Gui::getInstance().destroyWidget(mDynamicToolTipBox->getChildAt(0)); } - for (unsigned int i=0; i < mMainWidget->getChildCount(); ++i) + for (unsigned int i = 0; i < mMainWidget->getChildCount(); ++i) { mMainWidget->getChildAt(i)->setVisible(false); } @@ -355,7 +356,7 @@ namespace MWGui update(mFrameDuration); } - MyGUI::IntSize ToolTips::getToolTipViaPtr (int count, bool image, bool isOwned) + MyGUI::IntSize ToolTips::getToolTipViaPtr(int count, bool image, bool isOwned) { // this the maximum width of the tooltip before it starts word-wrapping setCoord(0, 0, 300, 300); @@ -373,16 +374,16 @@ namespace MWGui ToolTipInfo info = object.getToolTipInfo(mFocusObject, count); if (!image) - info.icon = ""; + info.icon.clear(); tooltipSize = createToolTip(info, isOwned); } return tooltipSize; } - + bool ToolTips::checkOwned() { - if(mFocusObject.isEmpty()) + if (mFocusObject.isEmpty()) return false; MWWorld::Ptr ptr = MWMechanics::getPlayer(); @@ -395,24 +396,31 @@ namespace MWGui MyGUI::IntSize ToolTips::createToolTip(const MWGui::ToolTipInfo& info, bool isOwned) { mDynamicToolTipBox->setVisible(true); - - if((mShowOwned == 1 || mShowOwned == 3) && isOwned) - mDynamicToolTipBox->changeWidgetSkin(MWBase::Environment::get().getWindowManager()->isGuiMode() ? "HUD_Box_NoTransp_Owned" : "HUD_Box_Owned"); + + const int showOwned = Settings::game().mShowOwned; + if ((showOwned == 1 || showOwned == 3) && isOwned) + mDynamicToolTipBox->changeWidgetSkin(MWBase::Environment::get().getWindowManager()->isGuiMode() + ? "HUD_Box_NoTransp_Owned" + : "HUD_Box_Owned"); else - mDynamicToolTipBox->changeWidgetSkin(MWBase::Environment::get().getWindowManager()->isGuiMode() ? "HUD_Box_NoTransp" : "HUD_Box"); + mDynamicToolTipBox->changeWidgetSkin( + MWBase::Environment::get().getWindowManager()->isGuiMode() ? "HUD_Box_NoTransp" : "HUD_Box"); - std::string caption = info.caption; - std::string image = info.icon; - int imageSize = (image != "") ? info.imageSize : 0; + const std::string& caption = info.caption; + const std::string& image = info.icon; + int imageSize = (!image.empty()) ? info.imageSize : 0; std::string text = info.text; + std::string_view extra = info.extra; // remove the first newline (easier this way) - if (text.size() > 0 && text[0] == '\n') + if (!text.empty() && text[0] == '\n') text.erase(0, 1); + if (!extra.empty() && extra[0] == '\n') + extra = extra.substr(1); const ESM::Enchantment* enchant = nullptr; - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - if (info.enchant != "") + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + if (!info.enchant.empty()) { enchant = store.get().search(info.enchant); if (enchant) @@ -433,22 +441,26 @@ namespace MWGui const MyGUI::IntPoint padding(8, 8); - const int imageCaptionHPadding = (caption != "" ? 8 : 0); - const int imageCaptionVPadding = (caption != "" ? 4 : 0); + const int imageCaptionHPadding = !caption.empty() ? 8 : 0; + const int imageCaptionVPadding = !caption.empty() ? 4 : 0; const int maximumWidth = MyGUI::RenderManager::getInstance().getViewSize().width - imageCaptionHPadding * 2; - std::string realImage = MWBase::Environment::get().getWindowManager()->correctIconPath(image); + const std::string realImage + = Misc::ResourceHelpers::correctIconPath(image, MWBase::Environment::get().getResourceSystem()->getVFS()); - Gui::EditBox* captionWidget = mDynamicToolTipBox->createWidget("NormalText", MyGUI::IntCoord(0, 0, 300, 300), MyGUI::Align::Left | MyGUI::Align::Top, "ToolTipCaption"); + Gui::EditBox* captionWidget = mDynamicToolTipBox->createWidget( + "NormalText", MyGUI::IntCoord(0, 0, 300, 300), MyGUI::Align::Left | MyGUI::Align::Top, "ToolTipCaption"); captionWidget->setEditStatic(true); captionWidget->setNeedKeyFocus(false); captionWidget->setCaptionWithReplacing(caption); MyGUI::IntSize captionSize = captionWidget->getTextSize(); - int captionHeight = std::max(caption != "" ? captionSize.height : 0, imageSize); + int captionHeight = std::max(!caption.empty() ? captionSize.height : 0, imageSize); - Gui::EditBox* textWidget = mDynamicToolTipBox->createWidget("SandText", MyGUI::IntCoord(0, captionHeight+imageCaptionVPadding, 300, 300-captionHeight-imageCaptionVPadding), MyGUI::Align::Stretch, "ToolTipText"); + Gui::EditBox* textWidget = mDynamicToolTipBox->createWidget("SandText", + MyGUI::IntCoord(0, captionHeight + imageCaptionVPadding, 300, 300 - captionHeight - imageCaptionVPadding), + MyGUI::Align::Stretch, "ToolTipText"); textWidget->setEditStatic(true); textWidget->setEditMultiLine(true); textWidget->setEditWordWrap(info.wordWrap); @@ -458,73 +470,83 @@ namespace MWGui MyGUI::IntSize textSize = textWidget->getTextSize(); captionSize += MyGUI::IntSize(imageSize, 0); // adjust for image - MyGUI::IntSize totalSize = MyGUI::IntSize( std::min(std::max(textSize.width,captionSize.width + ((image != "") ? imageCaptionHPadding : 0)),maximumWidth), - ((text != "") ? textSize.height + imageCaptionVPadding : 0) + captionHeight ); + MyGUI::IntSize totalSize = MyGUI::IntSize( + std::min(std::max(textSize.width, captionSize.width + ((!image.empty()) ? imageCaptionHPadding : 0)), + maximumWidth), + (!text.empty() ? textSize.height + imageCaptionVPadding : 0) + captionHeight); for (const std::string& note : info.notes) { MyGUI::ImageBox* icon = mDynamicToolTipBox->createWidget("MarkerButton", - MyGUI::IntCoord(padding.left, totalSize.height+padding.top, 8, 8), MyGUI::Align::Default); + MyGUI::IntCoord(padding.left, totalSize.height + padding.top, 8, 8), MyGUI::Align::Default); icon->setColour(MyGUI::Colour(1.0f, 0.3f, 0.3f)); Gui::EditBox* edit = mDynamicToolTipBox->createWidget("SandText", - MyGUI::IntCoord(padding.left+8+4, totalSize.height+padding.top, 300-padding.left-8-4, 300-totalSize.height), - MyGUI::Align::Default); - edit->setEditMultiLine(true); - edit->setEditWordWrap(true); - edit->setCaption(note); - edit->setSize(edit->getWidth(), edit->getTextSize().height); - icon->setPosition(icon->getLeft(),(edit->getTop()+edit->getBottom())/2-icon->getHeight()/2); + MyGUI::IntCoord(padding.left + 8 + 4, totalSize.height + padding.top, 300 - padding.left - 8 - 4, + 300 - totalSize.height), + MyGUI::Align::Default); + constexpr size_t maxLength = 60; + std::string shortenedNote = note.substr(0, std::min(maxLength, note.find('\n'))); + if (shortenedNote.size() < note.size()) + shortenedNote += " ..."; + edit->setCaption(shortenedNote); + MyGUI::IntSize noteTextSize = edit->getTextSize(); + edit->setSize(std::max(edit->getWidth(), noteTextSize.width), noteTextSize.height); + icon->setPosition(icon->getLeft(), (edit->getTop() + edit->getBottom()) / 2 - icon->getHeight() / 2); totalSize.height += std::max(edit->getHeight(), icon->getHeight()); - totalSize.width = std::max(totalSize.width, edit->getWidth()+8+4); + totalSize.width = std::max(totalSize.width, edit->getWidth() + 8 + 4); } if (!info.effects.empty()) { - MyGUI::Widget* effectArea = mDynamicToolTipBox->createWidget("", - MyGUI::IntCoord(padding.left, totalSize.height, 300-padding.left, 300-totalSize.height), + MyGUI::Widget* effectArea = mDynamicToolTipBox->createWidget({}, + MyGUI::IntCoord(padding.left, totalSize.height, 300 - padding.left, 300 - totalSize.height), MyGUI::Align::Stretch); MyGUI::IntCoord coord(0, 6, totalSize.width, 24); - Widgets::MWEffectListPtr effectsWidget = effectArea->createWidget - ("MW_StatName", coord, MyGUI::Align::Default); + Widgets::MWEffectListPtr effectsWidget + = effectArea->createWidget("MW_StatName", coord, MyGUI::Align::Default); effectsWidget->setEffectList(info.effects); std::vector effectItems; int flag = info.isPotion ? Widgets::MWEffectList::EF_NoTarget : 0; flag |= info.isIngredient ? Widgets::MWEffectList::EF_NoMagnitude : 0; - effectsWidget->createEffectWidgets(effectItems, effectArea, coord, true, flag); - totalSize.height += coord.top-6; + flag |= info.isIngredient ? Widgets::MWEffectList::EF_Constant : 0; + effectsWidget->createEffectWidgets( + effectItems, effectArea, coord, info.isPotion || info.isIngredient, flag); + totalSize.height += coord.top - 6; totalSize.width = std::max(totalSize.width, coord.width); } if (enchant) { - MyGUI::Widget* enchantArea = mDynamicToolTipBox->createWidget("", - MyGUI::IntCoord(padding.left, totalSize.height, 300-padding.left, 300-totalSize.height), + MyGUI::Widget* enchantArea = mDynamicToolTipBox->createWidget({}, + MyGUI::IntCoord(padding.left, totalSize.height, 300 - padding.left, 300 - totalSize.height), MyGUI::Align::Stretch); MyGUI::IntCoord coord(0, 6, totalSize.width, 24); - Widgets::MWEffectListPtr enchantWidget = enchantArea->createWidget - ("MW_StatName", coord, MyGUI::Align::Default); + Widgets::MWEffectListPtr enchantWidget + = enchantArea->createWidget("MW_StatName", coord, MyGUI::Align::Default); enchantWidget->setEffectList(Widgets::MWEffectList::effectListFromESM(&enchant->mEffects)); std::vector enchantEffectItems; - int flag = (enchant->mData.mType == ESM::Enchantment::ConstantEffect) ? Widgets::MWEffectList::EF_Constant : 0; - enchantWidget->createEffectWidgets(enchantEffectItems, enchantArea, coord, true, flag); - totalSize.height += coord.top-6; + int flag + = (enchant->mData.mType == ESM::Enchantment::ConstantEffect) ? Widgets::MWEffectList::EF_Constant : 0; + enchantWidget->createEffectWidgets(enchantEffectItems, enchantArea, coord, false, flag); + totalSize.height += coord.top - 6; totalSize.width = std::max(totalSize.width, coord.width); if (enchant->mData.mType == ESM::Enchantment::WhenStrikes || enchant->mData.mType == ESM::Enchantment::WhenUsed) { - int maxCharge = enchant->mData.mCharge; + const int maxCharge = MWMechanics::getEnchantmentCharge(*enchant); int charge = (info.remainingEnchantCharge == -1) ? maxCharge : info.remainingEnchantCharge; const int chargeWidth = 204; - MyGUI::TextBox* chargeText = enchantArea->createWidget("SandText", MyGUI::IntCoord(0, 0, 10, 18), MyGUI::Align::Default, "ToolTipEnchantChargeText"); + MyGUI::TextBox* chargeText = enchantArea->createWidget( + "SandText", MyGUI::IntCoord(0, 0, 10, 18), MyGUI::Align::Default, "ToolTipEnchantChargeText"); chargeText->setCaptionWithReplacing("#{sCharges}"); const int chargeTextWidth = chargeText->getTextSize().width + 5; @@ -533,60 +555,87 @@ namespace MWGui totalSize.width = std::max(totalSize.width, chargeAndTextWidth); - chargeText->setCoord((totalSize.width - chargeAndTextWidth)/2, coord.top+6, chargeTextWidth, 18); + chargeText->setCoord((totalSize.width - chargeAndTextWidth) / 2, coord.top + 6, chargeTextWidth, 18); MyGUI::IntCoord chargeCoord; if (totalSize.width < chargeWidth) { totalSize.width = chargeWidth; - chargeCoord = MyGUI::IntCoord(0, coord.top+6, chargeWidth, 18); + chargeCoord = MyGUI::IntCoord(0, coord.top + 6, chargeWidth, 18); } else { - chargeCoord = MyGUI::IntCoord((totalSize.width - chargeAndTextWidth)/2 + chargeTextWidth, coord.top+6, chargeWidth, 18); + chargeCoord = MyGUI::IntCoord( + (totalSize.width - chargeAndTextWidth) / 2 + chargeTextWidth, coord.top + 6, chargeWidth, 18); } - Widgets::MWDynamicStatPtr chargeWidget = enchantArea->createWidget - ("MW_ChargeBar", chargeCoord, MyGUI::Align::Default); + Widgets::MWDynamicStatPtr chargeWidget = enchantArea->createWidget( + "MW_ChargeBar", chargeCoord, MyGUI::Align::Default); chargeWidget->setValue(charge, maxCharge); totalSize.height += 24; } } - captionWidget->setCoord( (totalSize.width - captionSize.width)/2 + imageSize, - (captionHeight-captionSize.height)/2, - captionSize.width-imageSize, - captionSize.height); + if (!extra.empty()) + { + Gui::EditBox* extraWidget = mDynamicToolTipBox->createWidget("SandText", + MyGUI::IntCoord(padding.left, totalSize.height + 12, 300 - padding.left, 300 - totalSize.height), + MyGUI::Align::Stretch, "ToolTipExtraText"); + + extraWidget->setEditStatic(true); + extraWidget->setEditMultiLine(true); + extraWidget->setEditWordWrap(info.wordWrap); + extraWidget->setCaptionWithReplacing(extra); + extraWidget->setTextAlign(MyGUI::Align::HCenter | MyGUI::Align::Top); + extraWidget->setNeedKeyFocus(false); + + MyGUI::IntSize extraTextSize = extraWidget->getTextSize(); + totalSize.height += extraTextSize.height + 4; + totalSize.width = std::max(totalSize.width, extraTextSize.width); + } + + captionWidget->setCoord((totalSize.width - captionSize.width) / 2 + imageSize, + (captionHeight - captionSize.height) / 2, captionSize.width - imageSize, captionSize.height); - //if its too long we do hscroll with the caption + // if its too long we do hscroll with the caption if (captionSize.width > maximumWidth) { mHorizontalScrollIndex = mHorizontalScrollIndex + 2; - if (mHorizontalScrollIndex > captionSize.width){ + if (mHorizontalScrollIndex > captionSize.width) + { mHorizontalScrollIndex = -totalSize.width; } int horizontal_scroll = mHorizontalScrollIndex; - if (horizontal_scroll < 40){ + if (horizontal_scroll < 40) + { horizontal_scroll = 40; - }else{ + } + else + { horizontal_scroll = 80 - mHorizontalScrollIndex; } - captionWidget->setPosition (MyGUI::IntPoint(horizontal_scroll, captionWidget->getPosition().top + padding.top)); - } else { - captionWidget->setPosition (captionWidget->getPosition() + padding); + captionWidget->setPosition( + MyGUI::IntPoint(horizontal_scroll, captionWidget->getPosition().top + padding.top)); + } + else + { + captionWidget->setPosition(captionWidget->getPosition() + padding); } - textWidget->setPosition (textWidget->getPosition() + MyGUI::IntPoint(0, padding.top)); // only apply vertical padding, the horizontal works automatically due to Align::HCenter + textWidget->setPosition(textWidget->getPosition() + + MyGUI::IntPoint(0, + padding.top)); // only apply vertical padding, the horizontal works automatically due to Align::HCenter - if (image != "") + if (!image.empty()) { MyGUI::ImageBox* imageWidget = mDynamicToolTipBox->createWidget("ImageBox", - MyGUI::IntCoord((totalSize.width - captionSize.width - imageCaptionHPadding)/2, 0, imageSize, imageSize), + MyGUI::IntCoord( + (totalSize.width - captionSize.width - imageCaptionHPadding) / 2, 0, imageSize, imageSize), MyGUI::Align::Left | MyGUI::Align::Top); imageWidget->setImageTexture(realImage); - imageWidget->setPosition (imageWidget->getPosition() + padding); + imageWidget->setPosition(imageWidget->getPosition() + padding); } - totalSize += MyGUI::IntSize(padding.left*2, padding.top*2); + totalSize += MyGUI::IntSize(padding.left * 2, padding.top * 2); return totalSize; } @@ -610,7 +659,7 @@ namespace MWGui std::string ToolTips::getWeightString(const float weight, const std::string& prefix) { if (weight == 0) - return ""; + return {}; else return "\n" + prefix + ": " + toString(weight); } @@ -618,23 +667,23 @@ namespace MWGui std::string ToolTips::getPercentString(const float value, const std::string& prefix) { if (value == 0) - return ""; + return {}; else - return "\n" + prefix + ": " + toString(value*100) +"%"; + return "\n" + prefix + ": " + toString(value * 100) + "%"; } std::string ToolTips::getValueString(const int value, const std::string& prefix) { if (value == 0) - return ""; + return {}; else return "\n" + prefix + ": " + toString(value); } std::string ToolTips::getMiscString(const std::string& text, const std::string& prefix) { - if (text == "") - return ""; + if (text.empty()) + return {}; else return "\n" + prefix + ": " + text; } @@ -642,41 +691,41 @@ namespace MWGui std::string ToolTips::getCountString(const int value) { if (value == 1) - return ""; + return {}; else return " (" + MyGUI::utility::toString(value) + ")"; } std::string ToolTips::getSoulString(const MWWorld::CellRef& cellref) { - std::string soul = cellref.getSoul(); + const ESM::RefId& soul = cellref.getSoul(); if (soul.empty()) - return std::string(); - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Creature *creature = store.get().search(soul); + return {}; + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const ESM::Creature* creature = store.get().search(soul); if (!creature) - return std::string(); + return {}; if (creature->mName.empty()) - return " (" + creature->mId + ")"; + return " (" + creature->mId.toDebugString() + ")"; return " (" + creature->mName + ")"; } std::string ToolTips::getCellRefString(const MWWorld::CellRef& cellref) { std::string ret; - ret += getMiscString(cellref.getOwner(), "Owner"); - const std::string factionId = cellref.getFaction(); + ret += getMiscString(cellref.getOwner().getRefIdString(), "Owner"); + const ESM::RefId& factionId = cellref.getFaction(); if (!factionId.empty()) { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Faction *fact = store.get().search(factionId); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const ESM::Faction* fact = store.get().search(factionId); if (fact != nullptr) { - ret += getMiscString(fact->mName.empty() ? factionId : fact->mName, "Owner Faction"); + ret += getMiscString(fact->mName.empty() ? factionId.getRefIdString() : fact->mName, "Owner Faction"); if (cellref.getFactionRank() >= 0) { int rank = cellref.getFactionRank(); - const std::string rankName = fact->mRanks[rank]; + const std::string& rankName = fact->mRanks[rank]; if (rankName.empty()) ret += getValueString(cellref.getFactionRank(), "Rank"); else @@ -685,15 +734,16 @@ namespace MWGui } } - std::vector > itemOwners = - MWBase::Environment::get().getMechanicsManager()->getStolenItemOwners(cellref.getRefId()); + std::vector> itemOwners + = MWBase::Environment::get().getMechanicsManager()->getStolenItemOwners(cellref.getRefId()); - for (std::pair& owner : itemOwners) + for (std::pair& owner : itemOwners) { if (owner.second == std::numeric_limits::max()) - ret += std::string("\nStolen from ") + owner.first; // for legacy (ESS) savegames + ret += std::string("\nStolen from ") + owner.first.toDebugString(); // for legacy (ESS) savegames else - ret += std::string("\nStolen ") + MyGUI::utility::toString(owner.second) + " from " + owner.first; + ret += std::string("\nStolen ") + MyGUI::utility::toString(owner.second) + " from " + + owner.first.toDebugString(); } ret += getMiscString(cellref.getGlobalVariable(), "Global"); @@ -702,12 +752,14 @@ namespace MWGui std::string ToolTips::getDurationString(float duration, const std::string& prefix) { + auto l10n = MWBase::Environment::get().getL10nManager()->getContext("Interface"); + std::string ret; ret = prefix + ": "; if (duration < 1.f) { - ret += "0 s"; + ret += l10n->formatMessage("DurationSecond", { "seconds" }, { 0 }); return ret; } @@ -720,36 +772,37 @@ namespace MWGui int units = 0; int years = fullDuration / secondsPerYear; int months = fullDuration % secondsPerYear / secondsPerMonth; - int days = fullDuration % secondsPerYear % secondsPerMonth / secondsPerDay; // Because a year is not exactly 12 "months" + int days = fullDuration % secondsPerYear % secondsPerMonth + / secondsPerDay; // Because a year is not exactly 12 "months" int hours = fullDuration % secondsPerDay / secondsPerHour; int minutes = fullDuration % secondsPerHour / secondsPerMinute; int seconds = fullDuration % secondsPerMinute; if (years) { units++; - ret += toString(years) + " y "; + ret += l10n->formatMessage("DurationYear", { "years" }, { years }); } if (months) { units++; - ret += toString(months) + " mo "; + ret += l10n->formatMessage("DurationMonth", { "months" }, { months }); } if (units < 2 && days) { units++; - ret += toString(days) + " d "; + ret += l10n->formatMessage("DurationDay", { "days" }, { days }); } if (units < 2 && hours) { units++; - ret += toString(hours) + " h "; + ret += l10n->formatMessage("DurationHour", { "hours" }, { hours }); } if (units >= 2) return ret; if (minutes) - ret += toString(minutes) + " min "; + ret += l10n->formatMessage("DurationMinute", { "minutes" }, { minutes }); if (seconds) - ret += toString(seconds) + " s "; + ret += l10n->formatMessage("DurationSecond", { "seconds" }, { seconds }); return ret; } @@ -771,45 +824,38 @@ namespace MWGui mFocusToolTipY = min_y; } - void ToolTips::createSkillToolTip(MyGUI::Widget* widget, int skillId) + void ToolTips::createSkillToolTip(MyGUI::Widget* widget, ESM::RefId skillId) { - if (skillId == -1) + if (skillId.empty()) return; - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); - - const std::string &skillNameId = ESM::Skill::sSkillNameIds[skillId]; + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const ESM::Skill* skill = store.get().find(skillId); - assert(skill); - - const ESM::Attribute* attr = - store.get().find(skill->mData.mAttribute); - assert(attr); - std::string icon = "icons\\k\\" + ESM::Skill::sIconNames[skillId]; + const ESM::Attribute* attr + = store.get().find(ESM::Attribute::indexToRefId(skill->mData.mAttribute)); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "SkillNoProgressToolTip"); - widget->setUserString("Caption_SkillNoProgressName", "#{"+skillNameId+"}"); + widget->setUserString("Caption_SkillNoProgressName", MyGUI::TextIterator::toTagsString(skill->mName)); widget->setUserString("Caption_SkillNoProgressDescription", skill->mDescription); - widget->setUserString("Caption_SkillNoProgressAttribute", "#{sGoverningAttribute}: #{" + attr->mName + "}"); - widget->setUserString("ImageTexture_SkillNoProgressImage", icon); + widget->setUserString("Caption_SkillNoProgressAttribute", + "#{sGoverningAttribute}: " + MyGUI::TextIterator::toTagsString(attr->mName)); + widget->setUserString("ImageTexture_SkillNoProgressImage", skill->mIcon); } - void ToolTips::createAttributeToolTip(MyGUI::Widget* widget, int attributeId) + void ToolTips::createAttributeToolTip(MyGUI::Widget* widget, ESM::RefId attributeId) { - if (attributeId == -1) + const ESM::Attribute* attribute + = MWBase::Environment::get().getESMStore()->get().search(attributeId); + if (!attribute) return; - std::string icon = ESM::Attribute::sAttributeIcons[attributeId]; - std::string name = ESM::Attribute::sGmstAttributeIds[attributeId]; - std::string desc = ESM::Attribute::sGmstAttributeDescIds[attributeId]; - widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "AttributeToolTip"); - widget->setUserString("Caption_AttributeName", "#{"+name+"}"); - widget->setUserString("Caption_AttributeDescription", "#{"+desc+"}"); - widget->setUserString("ImageTexture_AttributeImage", icon); + widget->setUserString("Caption_AttributeName", MyGUI::TextIterator::toTagsString(attribute->mName)); + widget->setUserString( + "Caption_AttributeDescription", MyGUI::TextIterator::toTagsString(attribute->mDescription)); + widget->setUserString("ImageTexture_AttributeImage", attribute->mIcon); } void ToolTips::createSpecializationToolTip(MyGUI::Widget* widget, const std::string& name, int specId) @@ -817,20 +863,19 @@ namespace MWGui widget->setUserString("Caption_Caption", name); std::string specText; // get all skills of this specialisation - const MWWorld::Store &skills = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& skills = MWBase::Environment::get().getESMStore()->get(); bool isFirst = true; - for (auto& skillPair : skills) + for (const auto& skill : skills) { - if (skillPair.second.mData.mSpecialization == specId) + if (skill.mData.mSpecialization == specId) { if (isFirst) isFirst = false; else specText += "\n"; - specText += std::string("#{") + ESM::Skill::sSkillNameIds[skillPair.first] + "}"; + specText += MyGUI::TextIterator::toTagsString(skill.mName); } } widget->setUserString("Caption_ColumnText", specText); @@ -838,26 +883,24 @@ namespace MWGui widget->setUserString("ToolTipType", "Layout"); } - void ToolTips::createBirthsignToolTip(MyGUI::Widget* widget, const std::string& birthsignId) + void ToolTips::createBirthsignToolTip(MyGUI::Widget* widget, const ESM::RefId& birthsignId) { - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - const ESM::BirthSign *sign = store.get().find(birthsignId); + const ESM::BirthSign* sign = store.get().find(birthsignId); + const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "BirthSignToolTip"); - widget->setUserString("ImageTexture_BirthSignImage", MWBase::Environment::get().getWindowManager()->correctTexturePath(sign->mTexture)); - std::string text; + widget->setUserString( + "ImageTexture_BirthSignImage", Misc::ResourceHelpers::correctTexturePath(sign->mTexture, vfs)); + std::string text = sign->mName + "\n#{fontcolourhtml=normal}" + sign->mDescription; - text += sign->mName; - text += "\n#{fontcolourhtml=normal}" + sign->mDescription; + std::vector abilities, powers, spells; - std::vector abilities, powers, spells; - - for (const std::string& spellId : sign->mPowers.mList) + for (const ESM::RefId& spellId : sign->mPowers.mList) { - const ESM::Spell *spell = store.get().search(spellId); + const ESM::Spell* spell = store.get().search(spellId); if (!spell) continue; // Skip spells which cannot be found ESM::Spell::SpellType type = static_cast(spell->mData.mType); @@ -865,35 +908,28 @@ namespace MWGui continue; // We only want spell, ability and powers. if (type == ESM::Spell::ST_Ability) - abilities.push_back(spellId); + abilities.push_back(spell); else if (type == ESM::Spell::ST_Power) - powers.push_back(spellId); + powers.push_back(spell); else if (type == ESM::Spell::ST_Spell) - spells.push_back(spellId); - } - - struct { - const std::vector &spells; - std::string label; + spells.push_back(spell); } - categories[3] = { - {abilities, "sBirthsignmenu1"}, - {powers, "sPowers"}, - {spells, "sBirthsignmenu2"} - }; - for (int category = 0; category < 3; ++category) + using Category = std::pair&, std::string_view>; + for (const auto& [category, label] : std::initializer_list{ + { abilities, "sBirthsignmenu1" }, { powers, "sPowers" }, { spells, "sBirthsignmenu2" } }) { bool addHeader = true; - for (const std::string& spellId : categories[category].spells) + for (const ESM::Spell* spell : category) { if (addHeader) { - text += std::string("\n\n#{fontcolourhtml=header}") + std::string("#{") + categories[category].label + "}"; + text += "\n\n#{fontcolourhtml=header}#{"; + text += label; + text += '}'; addHeader = false; } - const ESM::Spell *spell = store.get().find(spellId); text += "\n#{fontcolourhtml=normal}" + spell->mName; } } @@ -911,17 +947,13 @@ namespace MWGui void ToolTips::createClassToolTip(MyGUI::Widget* widget, const ESM::Class& playerClass) { - if (playerClass.mName == "") + if (playerClass.mName.empty()) return; int spec = playerClass.mData.mSpecialization; - std::string specStr; - if (spec == 0) - specStr = "#{sSpecializationCombat}"; - else if (spec == 1) - specStr = "#{sSpecializationMagic}"; - else if (spec == 2) - specStr = "#{sSpecializationStealth}"; + std::string specStr = "#{"; + specStr += ESM::Class::sGmstSpecializationIds[spec]; + specStr += '}'; widget->setUserString("Caption_ClassName", playerClass.mName); widget->setUserString("Caption_ClassDescription", playerClass.mDescription); @@ -932,27 +964,23 @@ namespace MWGui void ToolTips::createMagicEffectToolTip(MyGUI::Widget* widget, short id) { - const ESM::MagicEffect* effect = - MWBase::Environment::get().getWorld ()->getStore ().get().find(id); - const std::string &name = ESM::MagicEffect::effectIdToString (id); + const auto& store = MWBase::Environment::get().getESMStore(); + const ESM::MagicEffect* effect = store->get().find(id); + const std::string& name = ESM::MagicEffect::indexToGmstString(id); std::string icon = effect->mIcon; int slashPos = icon.rfind('\\'); - icon.insert(slashPos+1, "b_"); - icon = MWBase::Environment::get().getWindowManager()->correctIconPath(icon); + icon.insert(slashPos + 1, "b_"); + icon = Misc::ResourceHelpers::correctIconPath(icon, MWBase::Environment::get().getResourceSystem()->getVFS()); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "MagicEffectToolTip"); widget->setUserString("Caption_MagicEffectName", "#{" + name + "}"); widget->setUserString("Caption_MagicEffectDescription", effect->mDescription); - widget->setUserString("Caption_MagicEffectSchool", "#{sSchool}: " + sSchoolNames[effect->mData.mSchool]); + widget->setUserString("Caption_MagicEffectSchool", + "#{sSchool}: " + + MyGUI::TextIterator::toTagsString( + store->get().find(effect->mData.mSchool)->mSchool->mName)); widget->setUserString("ImageTexture_MagicEffectImage", icon); } - - void ToolTips::setDelay(float delay) - { - mDelay = delay; - mRemainingDelay = mDelay; - } - } diff --git a/apps/openmw/mwgui/tooltips.hpp b/apps/openmw/mwgui/tooltips.hpp index d7bb87bdb00..132698475f1 100644 --- a/apps/openmw/mwgui/tooltips.hpp +++ b/apps/openmw/mwgui/tooltips.hpp @@ -1,8 +1,8 @@ #ifndef MWGUI_TOOLTIPS_H #define MWGUI_TOOLTIPS_H -#include "layout.hpp" #include "../mwworld/ptr.hpp" +#include "layout.hpp" #include "widgets.hpp" @@ -24,15 +24,17 @@ namespace MWGui , isPotion(false) , isIngredient(false) , wordWrap(true) - {} + { + } std::string caption; std::string text; + std::string extra; std::string icon; int imageSize; // enchantment (for cloth, armor, weapons) - std::string enchant; + ESM::RefId enchant; int remainingEnchantCharge; // effects (for potions, ingredients) @@ -59,8 +61,6 @@ namespace MWGui bool toggleFullHelp(); ///< show extra info in item tooltips (owner, script) bool getFullHelp() const; - void setDelay(float delay); - void clear(); void setFocusObject(const MWWorld::Ptr& focus); @@ -87,28 +87,28 @@ namespace MWGui static std::string getCellRefString(const MWWorld::CellRef& cellref); ///< Returns a string containing debug tooltip information about the given cellref. - static std::string getDurationString (float duration, const std::string& prefix); + static std::string getDurationString(float duration, const std::string& prefix); ///< Returns duration as two largest time units, rounded down. Note: not localized; no line break. // these do not create an actual tooltip, but they fill in the data that is required so the tooltip // system knows what to show in case this widget is hovered - static void createSkillToolTip(MyGUI::Widget* widget, int skillId); - static void createAttributeToolTip(MyGUI::Widget* widget, int attributeId); + static void createSkillToolTip(MyGUI::Widget* widget, ESM::RefId skillId); + static void createAttributeToolTip(MyGUI::Widget* widget, ESM::RefId attributeId); static void createSpecializationToolTip(MyGUI::Widget* widget, const std::string& name, int specId); - static void createBirthsignToolTip(MyGUI::Widget* widget, const std::string& birthsignId); + static void createBirthsignToolTip(MyGUI::Widget* widget, const ESM::RefId& birthsignId); static void createRaceToolTip(MyGUI::Widget* widget, const ESM::Race* playerRace); static void createClassToolTip(MyGUI::Widget* widget, const ESM::Class& playerClass); static void createMagicEffectToolTip(MyGUI::Widget* widget, short id); - + bool checkOwned(); /// Returns True if taking mFocusObject would be crime - + private: MyGUI::Widget* mDynamicToolTipBox; MWWorld::Ptr mFocusObject; - MyGUI::IntSize getToolTipViaPtr (int count, bool image = true, bool isOwned = false); + MyGUI::IntSize getToolTipViaPtr(int count, bool image = true, bool isOwned = false); ///< @return requested tooltip size MyGUI::IntSize createToolTip(const ToolTipInfo& info, bool isOwned = false); @@ -121,12 +121,8 @@ namespace MWGui /// Adjust position for a tooltip so that it doesn't leave the screen and does not obscure the mouse cursor void position(MyGUI::IntPoint& position, MyGUI::IntSize size, MyGUI::IntSize viewportSize); - static std::string sSchoolNames[6]; - int mHorizontalScrollIndex; - - float mDelay; float mRemainingDelay; // remaining time until tooltip will show int mLastMouseX; @@ -135,8 +131,6 @@ namespace MWGui bool mEnabled; bool mFullHelp; - - int mShowOwned; float mFrameDuration; }; diff --git a/apps/openmw/mwgui/tradeitemmodel.cpp b/apps/openmw/mwgui/tradeitemmodel.cpp index f5144ba8153..50a55f50615 100644 --- a/apps/openmw/mwgui/tradeitemmodel.cpp +++ b/apps/openmw/mwgui/tradeitemmodel.cpp @@ -1,7 +1,7 @@ #include "tradeitemmodel.hpp" -#include -#include +#include +#include #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" @@ -10,10 +10,10 @@ namespace MWGui { - TradeItemModel::TradeItemModel(ItemModel *sourceModel, const MWWorld::Ptr& merchant) + TradeItemModel::TradeItemModel(std::unique_ptr sourceModel, const MWWorld::Ptr& merchant) : mMerchant(merchant) { - mSourceModel = sourceModel; + mSourceModel = std::move(sourceModel); } bool TradeItemModel::allowedToUseItems() const @@ -21,7 +21,7 @@ namespace MWGui return true; } - ItemStack TradeItemModel::getItem (ModelIndex index) + ItemStack TradeItemModel::getItem(ModelIndex index) { if (index < 0) throw std::runtime_error("Invalid index supplied"); @@ -35,7 +35,7 @@ namespace MWGui return mItems.size(); } - void TradeItemModel::borrowImpl(const ItemStack &item, std::vector &out) + void TradeItemModel::borrowImpl(const ItemStack& item, std::vector& out) { bool found = false; for (ItemStack& itemStack : out) @@ -51,7 +51,7 @@ namespace MWGui out.push_back(item); } - void TradeItemModel::unborrowImpl(const ItemStack &item, size_t count, std::vector &out) + void TradeItemModel::unborrowImpl(const ItemStack& item, size_t count, std::vector& out) { std::vector::iterator it = out.begin(); bool found = false; @@ -72,33 +72,33 @@ namespace MWGui throw std::runtime_error("Can't find borrowed item to return"); } - void TradeItemModel::borrowItemFromUs (ModelIndex itemIndex, size_t count) + void TradeItemModel::borrowItemFromUs(ModelIndex itemIndex, size_t count) { ItemStack item = getItem(itemIndex); item.mCount = count; borrowImpl(item, mBorrowedFromUs); } - void TradeItemModel::borrowItemToUs (ModelIndex itemIndex, ItemModel* source, size_t count) + void TradeItemModel::borrowItemToUs(ModelIndex itemIndex, ItemModel* source, size_t count) { ItemStack item = source->getItem(itemIndex); item.mCount = count; borrowImpl(item, mBorrowedToUs); } - void TradeItemModel::returnItemBorrowedToUs (ModelIndex itemIndex, size_t count) + void TradeItemModel::returnItemBorrowedToUs(ModelIndex itemIndex, size_t count) { ItemStack item = getItem(itemIndex); unborrowImpl(item, count, mBorrowedToUs); } - void TradeItemModel::returnItemBorrowedFromUs (ModelIndex itemIndex, ItemModel* source, size_t count) + void TradeItemModel::returnItemBorrowedFromUs(ModelIndex itemIndex, ItemModel* source, size_t count) { ItemStack item = source->getItem(itemIndex); unborrowImpl(item, count, mBorrowedFromUs); } - void TradeItemModel::adjustEncumbrance(float &encumbrance) + void TradeItemModel::adjustEncumbrance(float& encumbrance) { for (ItemStack& itemStack : mBorrowedToUs) { @@ -130,8 +130,8 @@ namespace MWGui { // get index in the source model ItemModel* sourceModel = itemStack.mCreator; - size_t i=0; - for (; igetItemCount(); ++i) + size_t i = 0; + for (; i < sourceModel->getItemCount(); ++i) { if (itemStack.mBase == sourceModel->getItem(i).mBase) break; @@ -139,12 +139,8 @@ namespace MWGui if (i == sourceModel->getItemCount()) throw std::runtime_error("The borrowed item disappeared"); - const ItemStack& item = sourceModel->getItem(i); - static const bool prevent = Settings::Manager::getBool("prevent merchant equipping", "Game"); - // copy the borrowed items to our model - copyItem(item, itemStack.mCount, !prevent); - // then remove them from the source model - sourceModel->removeItem(item, itemStack.mCount); + sourceModel->moveItem( + sourceModel->getItem(i), itemStack.mCount, this, !Settings::game().mPreventMerchantEquipping); } mBorrowedToUs.clear(); mBorrowedFromUs.clear(); @@ -160,19 +156,19 @@ namespace MWGui mItems.clear(); // add regular items - for (size_t i=0; igetItemCount(); ++i) + for (size_t i = 0; i < mSourceModel->getItemCount(); ++i) { ItemStack item = mSourceModel->getItem(i); - if(!mMerchant.isEmpty()) + if (!mMerchant.isEmpty()) { MWWorld::Ptr base = item.mBase; - if(Misc::StringUtils::ciEqual(base.getCellRef().getRefId(), MWWorld::ContainerStore::sGoldId)) + if (base.getCellRef().getRefId() == MWWorld::ContainerStore::sGoldId) continue; if (!base.getClass().showsInInventory(base)) return; - if(!base.getClass().canSell(base, services)) + if (!base.getClass().canSell(base, services)) continue; // Bound items may not be bought @@ -180,7 +176,7 @@ namespace MWGui continue; // don't show equipped items - if(mMerchant.getClass().hasInventoryStore(mMerchant)) + if (mMerchant.getClass().hasInventoryStore(mMerchant)) { MWWorld::InventoryStore& store = mMerchant.getClass().getInventoryStore(mMerchant); if (store.isEquipped(base)) diff --git a/apps/openmw/mwgui/tradeitemmodel.hpp b/apps/openmw/mwgui/tradeitemmodel.hpp index 53b616aedaa..d395744d2a5 100644 --- a/apps/openmw/mwgui/tradeitemmodel.hpp +++ b/apps/openmw/mwgui/tradeitemmodel.hpp @@ -13,32 +13,32 @@ namespace MWGui class TradeItemModel : public ProxyItemModel { public: - TradeItemModel (ItemModel* sourceModel, const MWWorld::Ptr& merchant); + TradeItemModel(std::unique_ptr sourceModel, const MWWorld::Ptr& merchant); bool allowedToUseItems() const override; - ItemStack getItem (ModelIndex index) override; + ItemStack getItem(ModelIndex index) override; size_t getItemCount() override; void update() override; - void borrowItemFromUs (ModelIndex itemIndex, size_t count); + void borrowItemFromUs(ModelIndex itemIndex, size_t count); - void borrowItemToUs (ModelIndex itemIndex, ItemModel* source, size_t count); + void borrowItemToUs(ModelIndex itemIndex, ItemModel* source, size_t count); ///< @note itemIndex points to an item in \a source - void returnItemBorrowedToUs (ModelIndex itemIndex, size_t count); + void returnItemBorrowedToUs(ModelIndex itemIndex, size_t count); - void returnItemBorrowedFromUs (ModelIndex itemIndex, ItemModel* source, size_t count); + void returnItemBorrowedFromUs(ModelIndex itemIndex, ItemModel* source, size_t count); /// Permanently transfers items that were borrowed to us from another model to this model - void transferItems (); + void transferItems(); /// Aborts trade void abort(); /// Adjusts the given encumbrance by adding weight for items that have been lent to us, /// and removing weight for items we've lent to someone else. - void adjustEncumbrance (float& encumbrance); + void adjustEncumbrance(float& encumbrance); const std::vector getItemsBorrowedToUs() const; diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp index 19ea383483c..ba752303d2a 100644 --- a/apps/openmw/mwgui/tradewindow.cpp +++ b/apps/openmw/mwgui/tradewindow.cpp @@ -1,17 +1,19 @@ #include "tradewindow.hpp" #include -#include #include #include +#include +#include +#include #include +#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" -#include "../mwbase/dialoguemanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" @@ -20,18 +22,18 @@ #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "containeritemmodel.hpp" +#include "countdialog.hpp" #include "inventorywindow.hpp" #include "itemview.hpp" #include "sortfilteritemmodel.hpp" -#include "containeritemmodel.hpp" -#include "tradeitemmodel.hpp" -#include "countdialog.hpp" #include "tooltips.hpp" +#include "tradeitemmodel.hpp" namespace { - int getEffectiveValue (MWWorld::Ptr item, int count) + int getEffectiveValue(MWWorld::Ptr item, int count) { float price = static_cast(item.getClass().getValue(item)); if (item.getClass().hasItemHealth(item)) @@ -41,6 +43,75 @@ namespace return static_cast(price * count); } + bool haggle(const MWWorld::Ptr& player, const MWWorld::Ptr& merchant, int playerOffer, int merchantOffer) + { + // accept if merchant offer is better than player offer + if (playerOffer <= merchantOffer) + { + return true; + } + + // reject if npc is a creature + if (merchant.getType() != ESM::NPC::sRecordId) + { + return false; + } + + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); + + // Is the player buying? + bool buying = (merchantOffer < 0); + int a = std::abs(merchantOffer); + int b = std::abs(playerOffer); + int d = (buying) ? int(100 * (a - b) / a) : int(100 * (b - a) / b); + + int clampedDisposition = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(merchant); + + const MWMechanics::CreatureStats& merchantStats = merchant.getClass().getCreatureStats(merchant); + const MWMechanics::CreatureStats& playerStats = player.getClass().getCreatureStats(player); + + float a1 = static_cast(player.getClass().getSkill(player, ESM::Skill::Mercantile)); + float b1 = 0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified(); + float c1 = 0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified(); + float d1 = static_cast(merchant.getClass().getSkill(merchant, ESM::Skill::Mercantile)); + float e1 = 0.1f * merchantStats.getAttribute(ESM::Attribute::Luck).getModified(); + float f1 = 0.2f * merchantStats.getAttribute(ESM::Attribute::Personality).getModified(); + + float dispositionTerm = gmst.find("fDispositionMod")->mValue.getFloat() * (clampedDisposition - 50); + float pcTerm = (dispositionTerm + a1 + b1 + c1) * playerStats.getFatigueTerm(); + float npcTerm = (d1 + e1 + f1) * merchantStats.getFatigueTerm(); + float x = gmst.find("fBargainOfferMulti")->mValue.getFloat() * d + + gmst.find("fBargainOfferBase")->mValue.getFloat() + int(pcTerm - npcTerm); + + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::rollDice(100, prng) + 1; + + // reject if roll fails + // (or if player tries to buy things and get money) + if (roll > x || (merchantOffer < 0 && 0 < playerOffer)) + { + return false; + } + + // apply skill gain on successful barter + float skillGain = 0.f; + int finalPrice = std::abs(playerOffer); + int initialMerchantOffer = std::abs(merchantOffer); + + if (!buying && (finalPrice > initialMerchantOffer)) + { + skillGain = std::floor(100.f * (finalPrice - initialMerchantOffer) / finalPrice); + } + else if (buying && (finalPrice < initialMerchantOffer)) + { + skillGain = std::floor(100.f * (initialMerchantOffer - finalPrice) / initialMerchantOffer); + } + player.getClass().skillUsageSucceeded( + player, ESM::Skill::Mercantile, ESM::Skill::Mercantile_Success, skillGain); + + return true; + } } namespace MWGui @@ -93,13 +164,16 @@ namespace MWGui mTotalBalance->eventValueChanged += MyGUI::newDelegate(this, &TradeWindow::onBalanceValueChanged); mTotalBalance->eventEditSelectAccept += MyGUI::newDelegate(this, &TradeWindow::onAccept); - mTotalBalance->setMinValue(std::numeric_limits::min()+1); // disallow INT_MIN since abs(INT_MIN) is undefined + mTotalBalance->setMinValue( + std::numeric_limits::min() + 1); // disallow INT_MIN since abs(INT_MIN) is undefined setCoord(400, 0, 400, 300); } void TradeWindow::setPtr(const MWWorld::Ptr& actor) { + if (actor.isEmpty() || !actor.getClass().isActor()) + throw std::runtime_error("Invalid argument in TradeWindow::setPtr"); mPtr = actor; mCurrentBalance = 0; @@ -113,9 +187,12 @@ namespace MWGui std::vector worldItems; MWBase::Environment::get().getWorld()->getItemsOwnedBy(actor, worldItems); - mTradeModel = new TradeItemModel(new ContainerItemModel(itemSources, worldItems), mPtr); - mSortModel = new SortFilterItemModel(mTradeModel); - mItemView->setModel (mSortModel); + auto tradeModel + = std::make_unique(std::make_unique(itemSources, worldItems), mPtr); + mTradeModel = tradeModel.get(); + auto sortModel = std::make_unique(std::move(tradeModel)); + mSortModel = sortModel.get(); + mItemView->setModel(std::move(sortModel)); mItemView->resetScrollBars(); updateLabels(); @@ -123,7 +200,7 @@ namespace MWGui setTitle(actor.getClass().getName(actor)); onFilterChanged(mFilterAll); - mFilterEdit->setCaption(""); + mFilterEdit->setCaption({}); } void TradeWindow::onFrame(float dt) @@ -173,7 +250,7 @@ namespace MWGui return true; } - void TradeWindow::onItemSelected (int index) + void TradeWindow::onItemSelected(int index) { const ItemStack& item = mSortModel->getItem(index); @@ -187,7 +264,8 @@ namespace MWGui { CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); std::string message = "#{sQuanityMenuMessage02}"; - std::string name = object.getClass().getName(object) + MWGui::ToolTips::getSoulString(object.getCellRef()); + std::string name{ object.getClass().getName(object) }; + name += MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, message, count); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &TradeWindow::sellItem); @@ -196,17 +274,18 @@ namespace MWGui else { mItemToSell = mSortModel->mapToSource(index); - sellItem (nullptr, count); + sellItem(nullptr, count); } } void TradeWindow::sellItem(MyGUI::Widget* sender, int count) { const ItemStack& item = mTradeModel->getItem(mItemToSell); - std::string sound = item.mBase.getClass().getUpSoundId(item.mBase); + const ESM::RefId& sound = item.mBase.getClass().getUpSoundId(item.mBase); MWBase::Environment::get().getWindowManager()->playSound(sound); - TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); + TradeItemModel* playerTradeModel + = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); if (item.mType == ItemStack::Type_Barter) { @@ -227,17 +306,19 @@ namespace MWGui mItemView->update(); } - void TradeWindow::borrowItem (int index, size_t count) + void TradeWindow::borrowItem(int index, size_t count) { - TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); + TradeItemModel* playerTradeModel + = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); mTradeModel->borrowItemToUs(index, playerTradeModel, count); mItemView->update(); sellToNpc(playerTradeModel->getItem(index).mBase, count, false); } - void TradeWindow::returnItem (int index, size_t count) + void TradeWindow::returnItem(int index, size_t count) { - TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); + TradeItemModel* playerTradeModel + = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); const ItemStack& item = playerTradeModel->getItem(index); mTradeModel->returnItemBorrowedFromUs(index, playerTradeModel, count); mItemView->update(); @@ -250,20 +331,24 @@ namespace MWGui if (amount > 0) { - store.add(MWWorld::ContainerStore::sGoldId, amount, actor); + store.add(MWWorld::ContainerStore::sGoldId, amount); } else { - store.remove(MWWorld::ContainerStore::sGoldId, - amount, actor); + store.remove(MWWorld::ContainerStore::sGoldId, -amount); } } void TradeWindow::onOfferButtonClicked(MyGUI::Widget* _sender) { - TradeItemModel* playerItemModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); + TradeItemModel* playerItemModel + = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); + + if (mTotalBalance->getValue() == 0) + mCurrentBalance = 0; // were there any items traded at all? const std::vector& playerBought = playerItemModel->getItemsBorrowedToUs(); @@ -271,8 +356,7 @@ namespace MWGui if (playerBought.empty() && merchantBought.empty()) { // user notification - MWBase::Environment::get().getWindowManager()-> - messageBox("#{sBarterDialog11}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog11}"); return; } @@ -283,8 +367,7 @@ namespace MWGui if (mCurrentBalance < 0 && playerGold < std::abs(mCurrentBalance)) { // user notification - MWBase::Environment::get().getWindowManager()-> - messageBox("#{sBarterDialog1}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog1}"); return; } @@ -292,21 +375,22 @@ namespace MWGui if (mCurrentBalance > 0 && getMerchantGold() < mCurrentBalance) { // user notification - MWBase::Environment::get().getWindowManager()-> - messageBox("#{sBarterDialog2}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog2}"); return; } // check if the player is attempting to sell back an item stolen from this actor for (const ItemStack& itemStack : merchantBought) { - if (MWBase::Environment::get().getMechanicsManager()->isItemStolenFrom(itemStack.mBase.getCellRef().getRefId(), mPtr)) + if (MWBase::Environment::get().getMechanicsManager()->isItemStolenFrom( + itemStack.mBase.getCellRef().getRefId(), mPtr)) { std::string msg = gmst.find("sNotifyMessage49")->mValue.getString(); msg = Misc::StringUtils::format(msg, itemStack.mBase.getClass().getName(itemStack.mBase)); MWBase::Environment::get().getWindowManager()->messageBox(msg); - MWBase::Environment::get().getMechanicsManager()->confiscateStolenItemToOwner(player, itemStack.mBase, mPtr, itemStack.mCount); + MWBase::Environment::get().getMechanicsManager()->confiscateStolenItemToOwner( + player, itemStack.mBase, mPtr, itemStack.mCount); onCancelButtonClicked(mCancelButton); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); @@ -314,21 +398,21 @@ namespace MWGui } } - bool offerAccepted = mTrading.haggle(player, mPtr, mCurrentBalance, mCurrentMerchantOffer); + bool offerAccepted = haggle(player, mPtr, mCurrentBalance, mCurrentMerchantOffer); // apply disposition change if merchant is NPC - if ( mPtr.getClass().isNpc() ) { - int dispositionDelta = offerAccepted - ? gmst.find("iBarterSuccessDisposition")->mValue.getInteger() - : gmst.find("iBarterFailDisposition")->mValue.getInteger(); + if (mPtr.getClass().isNpc()) + { + int dispositionDelta = offerAccepted ? gmst.find("iBarterSuccessDisposition")->mValue.getInteger() + : gmst.find("iBarterFailDisposition")->mValue.getInteger(); MWBase::Environment::get().getDialogueManager()->applyBarterDispositionChange(dispositionDelta); } // display message on haggle failure - if ( !offerAccepted ) { - MWBase::Environment::get().getWindowManager()-> - messageBox("#{sNotifyMessage9}"); + if (!offerAccepted) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage9}"); return; } @@ -341,16 +425,16 @@ namespace MWGui { addOrRemoveGold(mCurrentBalance, player); mPtr.getClass().getCreatureStats(mPtr).setGoldPool( - mPtr.getClass().getCreatureStats(mPtr).getGoldPool() - mCurrentBalance ); + mPtr.getClass().getCreatureStats(mPtr).getGoldPool() - mCurrentBalance); } eventTradeDone(); - MWBase::Environment::get().getWindowManager()->playSound("Item Gold Up"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Gold Up")); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); } - void TradeWindow::onAccept(MyGUI::EditBox *sender) + void TradeWindow::onAccept(MyGUI::EditBox* sender) { onOfferButtonClicked(sender); @@ -370,9 +454,10 @@ namespace MWGui updateLabels(); } - void TradeWindow::addRepeatController(MyGUI::Widget *widget) + void TradeWindow::addRepeatController(MyGUI::Widget* widget) { - MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MyGUI::ControllerRepeatClick::getClassTypeName()); + MyGUI::ControllerItem* item + = MyGUI::ControllerManager::getInstance().createItem(MyGUI::ControllerRepeatClick::getClassTypeName()); MyGUI::ControllerRepeatClick* controller = static_cast(item); controller->eventRepeatClick += newDelegate(this, &TradeWindow::onRepeatClick); MyGUI::ControllerManager::getInstance().addItem(widget, controller); @@ -398,17 +483,22 @@ namespace MWGui onDecreaseButtonTriggered(); } - void TradeWindow::onBalanceButtonReleased(MyGUI::Widget *_sender, int _left, int _top, MyGUI::MouseButton _id) + void TradeWindow::onBalanceButtonReleased(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { MyGUI::ControllerManager::getInstance().removeItem(_sender); } void TradeWindow::onBalanceValueChanged(int value) { + int previousBalance = mCurrentBalance; + // Entering a "-" sign inverts the buying/selling state mCurrentBalance = (mCurrentBalance >= 0 ? 1 : -1) * value; updateLabels(); + if (mCurrentBalance == 0) + mCurrentBalance = previousBalance; + if (value != std::abs(value)) mTotalBalance->setValue(std::abs(value)); } @@ -416,17 +506,26 @@ namespace MWGui void TradeWindow::onIncreaseButtonTriggered() { // prevent overflows, and prevent entering INT_MIN since abs(INT_MIN) is undefined - if (mCurrentBalance == std::numeric_limits::max() || mCurrentBalance == std::numeric_limits::min()+1) + if (mCurrentBalance == std::numeric_limits::max() + || mCurrentBalance == std::numeric_limits::min() + 1) return; - if (mCurrentBalance < 0) mCurrentBalance -= 1; - else mCurrentBalance += 1; + if (mTotalBalance->getValue() == 0) + mCurrentBalance = 0; + if (mCurrentBalance < 0) + mCurrentBalance -= 1; + else + mCurrentBalance += 1; updateLabels(); } void TradeWindow::onDecreaseButtonTriggered() { - if (mCurrentBalance < 0) mCurrentBalance += 1; - else mCurrentBalance -= 1; + if (mTotalBalance->getValue() == 0) + mCurrentBalance = 0; + if (mCurrentBalance < 0) + mCurrentBalance += 1; + else + mCurrentBalance -= 1; updateLabels(); } @@ -434,9 +533,18 @@ namespace MWGui { MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - mPlayerGold->setCaptionWithReplacing("#{sYourGold} " + MyGUI::utility::toString(playerGold)); + TradeItemModel* playerTradeModel + = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); + const std::vector& playerBorrowed = playerTradeModel->getItemsBorrowedToUs(); + const std::vector& merchantBorrowed = mTradeModel->getItemsBorrowedToUs(); + + if (playerBorrowed.empty() && merchantBorrowed.empty()) + { + mCurrentBalance = 0; + } + if (mCurrentBalance < 0) { mTotalBalanceLabel->setCaptionWithReplacing("#{sTotalCost}"); @@ -453,7 +561,8 @@ namespace MWGui void TradeWindow::updateOffer() { - TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); + TradeItemModel* playerTradeModel + = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); int merchantOffer = 0; @@ -465,8 +574,10 @@ namespace MWGui for (const ItemStack& itemStack : playerBorrowed) { const int basePrice = getEffectiveValue(itemStack.mBase, itemStack.mCount); - const int cap = static_cast(std::max(1.f, 0.75f * basePrice)); // Minimum buying price -- 75% of the base - const int buyingPrice = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, basePrice, true); + const int cap + = static_cast(std::max(1.f, 0.75f * basePrice)); // Minimum buying price -- 75% of the base + const int buyingPrice + = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, basePrice, true); merchantOffer -= std::max(cap, buyingPrice); } @@ -474,8 +585,10 @@ namespace MWGui for (const ItemStack& itemStack : merchantBorrowed) { const int basePrice = getEffectiveValue(itemStack.mBase, itemStack.mCount); - const int cap = static_cast(std::max(1.f, 0.75f * basePrice)); // Maximum selling price -- 75% of the base - const int sellingPrice = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, basePrice, false); + const int cap + = static_cast(std::max(1.f, 0.75f * basePrice)); // Maximum selling price -- 75% of the base + const int sellingPrice + = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, basePrice, false); merchantOffer += mPtr.getClass().isNpc() ? std::min(cap, sellingPrice) : sellingPrice; } @@ -497,7 +610,8 @@ namespace MWGui void TradeWindow::onReferenceUnavailable() { - // remove both Trade and Dialogue (since you always trade with the NPC/creature that you have previously talked to) + // remove both Trade and Dialogue (since you always trade with the NPC/creature that you have previously talked + // to) MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } @@ -523,4 +637,10 @@ namespace MWGui return; resetReference(); } + + void TradeWindow::onDeleteCustomData(const MWWorld::Ptr& ptr) + { + if (mTradeModel && mTradeModel->usesContainer(ptr)) + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); + } } diff --git a/apps/openmw/mwgui/tradewindow.hpp b/apps/openmw/mwgui/tradewindow.hpp index f82d7b0f72e..33c39cb2699 100644 --- a/apps/openmw/mwgui/tradewindow.hpp +++ b/apps/openmw/mwgui/tradewindow.hpp @@ -1,8 +1,6 @@ #ifndef MWGUI_TRADEWINDOW_H #define MWGUI_TRADEWINDOW_H -#include "../mwmechanics/trading.hpp" - #include "referenceinterface.hpp" #include "windowbase.hpp" @@ -24,94 +22,99 @@ namespace MWGui class TradeWindow : public WindowBase, public ReferenceInterface { - public: - TradeWindow(); + public: + TradeWindow(); + + void setPtr(const MWWorld::Ptr& actor) override; + + void onClose() override; + void onFrame(float dt) override; + void clear() override { resetReference(); } - void setPtr(const MWWorld::Ptr& actor) override; + void borrowItem(int index, size_t count); + void returnItem(int index, size_t count); - void onClose() override; - void onFrame(float dt) override; - void clear() override { resetReference(); } + int getMerchantServices(); - void borrowItem (int index, size_t count); - void returnItem (int index, size_t count); + bool exit() override; - int getMerchantServices(); + void resetReference() override; - bool exit() override; + void onDeleteCustomData(const MWWorld::Ptr& ptr) override; - void resetReference() override; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_TradeDone; + EventHandle_TradeDone eventTradeDone; - typedef MyGUI::delegates::CMultiDelegate0 EventHandle_TradeDone; - EventHandle_TradeDone eventTradeDone; + std::string_view getWindowIdForLua() const override { return "Trade"; } - private: - ItemView* mItemView; - SortFilterItemModel* mSortModel; - TradeItemModel* mTradeModel; - MWMechanics::Trading mTrading; + private: + ItemView* mItemView; + SortFilterItemModel* mSortModel; + TradeItemModel* mTradeModel; - static const float sBalanceChangeInitialPause; // in seconds - static const float sBalanceChangeInterval; // in seconds + static const float sBalanceChangeInitialPause; // in seconds + static const float sBalanceChangeInterval; // in seconds - MyGUI::Button* mFilterAll; - MyGUI::Button* mFilterWeapon; - MyGUI::Button* mFilterApparel; - MyGUI::Button* mFilterMagic; - MyGUI::Button* mFilterMisc; + MyGUI::Button* mFilterAll; + MyGUI::Button* mFilterWeapon; + MyGUI::Button* mFilterApparel; + MyGUI::Button* mFilterMagic; + MyGUI::Button* mFilterMisc; - MyGUI::EditBox* mFilterEdit; + MyGUI::EditBox* mFilterEdit; - MyGUI::Button* mIncreaseButton; - MyGUI::Button* mDecreaseButton; - MyGUI::TextBox* mTotalBalanceLabel; - Gui::NumericEditBox* mTotalBalance; + MyGUI::Button* mIncreaseButton; + MyGUI::Button* mDecreaseButton; + MyGUI::TextBox* mTotalBalanceLabel; + Gui::NumericEditBox* mTotalBalance; - MyGUI::Widget* mBottomPane; + MyGUI::Widget* mBottomPane; - MyGUI::Button* mMaxSaleButton; - MyGUI::Button* mCancelButton; - MyGUI::Button* mOfferButton; - MyGUI::TextBox* mPlayerGold; - MyGUI::TextBox* mMerchantGold; + MyGUI::Button* mMaxSaleButton; + MyGUI::Button* mCancelButton; + MyGUI::Button* mOfferButton; + MyGUI::TextBox* mPlayerGold; + MyGUI::TextBox* mMerchantGold; - int mItemToSell; + int mItemToSell; - int mCurrentBalance; - int mCurrentMerchantOffer; + int mCurrentBalance; + int mCurrentMerchantOffer; - void sellToNpc(const MWWorld::Ptr& item, int count, bool boughtItem); ///< only used for adjusting the gold balance - void buyFromNpc(const MWWorld::Ptr& item, int count, bool soldItem); ///< only used for adjusting the gold balance + void sellToNpc( + const MWWorld::Ptr& item, int count, bool boughtItem); ///< only used for adjusting the gold balance + void buyFromNpc( + const MWWorld::Ptr& item, int count, bool soldItem); ///< only used for adjusting the gold balance - void updateOffer(); + void updateOffer(); - void onItemSelected (int index); - void sellItem (MyGUI::Widget* sender, int count); + void onItemSelected(int index); + void sellItem(MyGUI::Widget* sender, int count); - void onFilterChanged(MyGUI::Widget* _sender); - void onNameFilterChanged(MyGUI::EditBox* _sender); - void onOfferButtonClicked(MyGUI::Widget* _sender); - void onAccept(MyGUI::EditBox* sender); - void onCancelButtonClicked(MyGUI::Widget* _sender); - void onMaxSaleButtonClicked(MyGUI::Widget* _sender); - void onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); - void onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); - void onBalanceButtonReleased(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); - void onBalanceValueChanged(int value); - void onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller); + void onFilterChanged(MyGUI::Widget* _sender); + void onNameFilterChanged(MyGUI::EditBox* _sender); + void onOfferButtonClicked(MyGUI::Widget* _sender); + void onAccept(MyGUI::EditBox* sender); + void onCancelButtonClicked(MyGUI::Widget* _sender); + void onMaxSaleButtonClicked(MyGUI::Widget* _sender); + void onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); + void onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); + void onBalanceButtonReleased(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); + void onBalanceValueChanged(int value); + void onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller); - void addRepeatController(MyGUI::Widget* widget); + void addRepeatController(MyGUI::Widget* widget); - void onIncreaseButtonTriggered(); - void onDecreaseButtonTriggered(); + void onIncreaseButtonTriggered(); + void onDecreaseButtonTriggered(); - void addOrRemoveGold(int gold, const MWWorld::Ptr& actor); + void addOrRemoveGold(int gold, const MWWorld::Ptr& actor); - void updateLabels(); + void updateLabels(); - void onReferenceUnavailable() override; + void onReferenceUnavailable() override; - int getMerchantGold(); + int getMerchantGold(); }; } diff --git a/apps/openmw/mwgui/trainingwindow.cpp b/apps/openmw/mwgui/trainingwindow.cpp index 7fae33bae52..890aa0ba681 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -1,48 +1,33 @@ #include "trainingwindow.hpp" +#include #include +#include -#include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/npcstats.hpp" -#include +#include +#include #include "tooltips.hpp" -namespace -{ -// Sorts a container descending by skill value. If skill value is equal, sorts ascending by skill ID. -// pair -bool sortSkills (const std::pair& left, const std::pair& right) -{ - if (left == right) - return false; - - if (left.second > right.second) - return true; - else if (left.second < right.second) - return false; - - return left.first < right.first; -} -} - namespace MWGui { TrainingWindow::TrainingWindow() : WindowBase("openmw_trainingwindow.layout") , mTimeAdvancer(0.05f) - , mTrainingSkillBasedOnBaseSkill(Settings::Manager::getBool("trainers training skills based on base skill", "Game")) { getWidget(mTrainingOptions, "TrainingOptions"); getWidget(mCancelButton, "CancelButton"); @@ -67,8 +52,10 @@ namespace MWGui center(); } - void TrainingWindow::setPtr (const MWWorld::Ptr& actor) + void TrainingWindow::setPtr(const MWWorld::Ptr& actor) { + if (actor.isEmpty() || !actor.getClass().isActor()) + throw std::runtime_error("Invalid argument in TrainingWindow::setPtr"); mPtr = actor; MWWorld::Ptr player = MWMechanics::getPlayer(); @@ -76,114 +63,134 @@ namespace MWGui mPlayerGold->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); - // NPC can train you in his best 3 skills - std::vector< std::pair > skills; - - MWMechanics::NpcStats const& actorStats(actor.getClass().getNpcStats(actor)); - for (int i=0; i& gmst = store->get(); + const MWWorld::Store& skillStore = store->get(); + + // NPC can train you in their best 3 skills + constexpr size_t maxSkills = 3; + std::vector> skills; + skills.reserve(maxSkills); + + const auto sortByValue + = [](const std::pair& lhs, const std::pair& rhs) { + return lhs.second > rhs.second; + }; + // Maintain a sorted vector of max maxSkills elements, ordering skills by value and content file order + const MWMechanics::NpcStats& actorStats = actor.getClass().getNpcStats(actor); + for (const ESM::Skill& skill : skillStore) { - float value = getSkillForTraining(actorStats, i); - - skills.emplace_back(i, value); + float value = getSkillForTraining(actorStats, skill.mId); + if (skills.size() < maxSkills) + { + skills.emplace_back(&skill, value); + std::stable_sort(skills.begin(), skills.end(), sortByValue); + } + else + { + auto& lowest = skills[maxSkills - 1]; + if (lowest.second < value) + { + lowest.first = &skill; + lowest.second = value; + std::stable_sort(skills.begin(), skills.end(), sortByValue); + } + } } - std::sort(skills.begin(), skills.end(), sortSkills); + MyGUI::EnumeratorWidgetPtr widgets = mTrainingOptions->getEnumerator(); + MyGUI::Gui::getInstance().destroyWidgets(widgets); - MyGUI::EnumeratorWidgetPtr widgets = mTrainingOptions->getEnumerator (); - MyGUI::Gui::getInstance ().destroyWidgets (widgets); + MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); - MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats (player); + const int lineHeight = Settings::gui().mFontSize + 2; - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); - - for (int i=0; i<3; ++i) + for (size_t i = 0; i < skills.size(); ++i) { - int price = static_cast(pcStats.getSkill (skills[i].first).getBase() * gmst.find("iTrainingMod")->mValue.getInteger()); + const ESM::Skill* skill = skills[i].first; + int price = static_cast( + pcStats.getSkill(skill->mId).getBase() * gmst.find("iTrainingMod")->mValue.getInteger()); price = std::max(1, price); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); - MyGUI::Button* button = mTrainingOptions->createWidget(price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip - MyGUI::IntCoord(5, 5+i*18, mTrainingOptions->getWidth()-10, 18), MyGUI::Align::Default); + MyGUI::Button* button = mTrainingOptions->createWidget(price <= playerGold + ? "SandTextButton" + : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip + MyGUI::IntCoord(5, 5 + i * lineHeight, mTrainingOptions->getWidth() - 10, lineHeight), + MyGUI::Align::Default); button->setUserData(skills[i].first); button->eventMouseButtonClick += MyGUI::newDelegate(this, &TrainingWindow::onTrainingSelected); - button->setCaptionWithReplacing("#{" + ESM::Skill::sSkillNameIds[skills[i].first] + "} - " + MyGUI::utility::toString(price)); + button->setCaptionWithReplacing( + MyGUI::TextIterator::toTagsString(skill->mName) + " - " + MyGUI::utility::toString(price)); - button->setSize(button->getTextSize ().width+12, button->getSize().height); + button->setSize(button->getTextSize().width + 12, button->getSize().height); - ToolTips::createSkillToolTip (button, skills[i].first); + ToolTips::createSkillToolTip(button, skill->mId); } center(); } - void TrainingWindow::onReferenceUnavailable () + void TrainingWindow::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Training); } - void TrainingWindow::onCancelButtonClicked (MyGUI::Widget *sender) + void TrainingWindow::onCancelButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Training); } - void TrainingWindow::onTrainingSelected (MyGUI::Widget *sender) + void TrainingWindow::onTrainingSelected(MyGUI::Widget* sender) { - int skillId = *sender->getUserData(); + const ESM::Skill* skill = *sender->getUserData(); - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); - MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats (player); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - int price = pcStats.getSkill (skillId).getBase() * store.get().find("iTrainingMod")->mValue.getInteger(); - price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr,price,true); + int price = pcStats.getSkill(skill->mId).getBase() + * store.get().find("iTrainingMod")->mValue.getInteger(); + price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) return; - if (getSkillForTraining(mPtr.getClass().getNpcStats(mPtr), skillId) <= pcStats.getSkill(skillId).getBase()) + if (getSkillForTraining(mPtr.getClass().getNpcStats(mPtr), skill->mId) + <= pcStats.getSkill(skill->mId).getBase()) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sServiceTrainingWords}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sServiceTrainingWords}"); return; } // You can not train a skill above its governing attribute - const ESM::Skill* skill = MWBase::Environment::get().getWorld()->getStore().get().find(skillId); - if (pcStats.getSkill(skillId).getBase() >= pcStats.getAttribute(skill->mData.mAttribute).getBase()) + if (pcStats.getSkill(skill->mId).getBase() + >= pcStats.getAttribute(ESM::Attribute::indexToRefId(skill->mData.mAttribute)).getModified()) { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage17}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage17}"); return; } // increase skill - MWWorld::LiveCellRef *playerRef = player.get(); - - const ESM::Class *class_ = - store.get().find(playerRef->mBase->mClass); - pcStats.increaseSkill (skillId, *class_, true); + MWBase::Environment::get().getLuaManager()->skillLevelUp(player, skill->mId, "trainer"); // remove gold - player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price); // add gold to NPC trading gold pool MWMechanics::NpcStats& npcStats = mPtr.getClass().getNpcStats(mPtr); npcStats.setGoldPool(npcStats.getGoldPool() + price); - // advance time - MWBase::Environment::get().getMechanicsManager()->rest(2, false); - MWBase::Environment::get().getWorld ()->advanceTime (2); - setVisible(false); mProgressBar.setVisible(true); mProgressBar.setProgress(0, 2); mTimeAdvancer.run(2); - MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.25); - MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.25, false, 0.25); + MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.2f); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.2f, false, 0.2f); } void TrainingWindow::onTrainingProgressChanged(int cur, int total) @@ -195,16 +202,20 @@ namespace MWGui { mProgressBar.setVisible(false); + // advance time + MWBase::Environment::get().getMechanicsManager()->rest(2, false); + MWBase::Environment::get().getWorld()->advanceTime(2); + // go back to game mode - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Training); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Training); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } - float TrainingWindow::getSkillForTraining(const MWMechanics::NpcStats& stats, int skillId) const + float TrainingWindow::getSkillForTraining(const MWMechanics::NpcStats& stats, ESM::RefId id) const { - if (mTrainingSkillBasedOnBaseSkill) - return stats.getSkill(skillId).getBase(); - return stats.getSkill(skillId).getModified(); + if (Settings::game().mTrainersTrainingSkillsBasedOnBaseSkill) + return stats.getSkill(id).getBase(); + return stats.getSkill(id).getModified(); } void TrainingWindow::onFrame(float dt) diff --git a/apps/openmw/mwgui/trainingwindow.hpp b/apps/openmw/mwgui/trainingwindow.hpp index 57fdd323a44..ee13f24b23f 100644 --- a/apps/openmw/mwgui/trainingwindow.hpp +++ b/apps/openmw/mwgui/trainingwindow.hpp @@ -1,10 +1,10 @@ #ifndef MWGUI_TRAININGWINDOW_H #define MWGUI_TRAININGWINDOW_H -#include "windowbase.hpp" #include "referenceinterface.hpp" #include "timeadvancer.hpp" #include "waitdialog.hpp" +#include "windowbase.hpp" namespace MWMechanics { @@ -31,10 +31,12 @@ namespace MWGui void clear() override { resetReference(); } + std::string_view getWindowIdForLua() const override { return "Training"; } + protected: void onReferenceUnavailable() override; - void onCancelButtonClicked (MyGUI::Widget* sender); + void onCancelButtonClicked(MyGUI::Widget* sender); void onTrainingSelected(MyGUI::Widget* sender); void onTrainingProgressChanged(int cur, int total); @@ -42,7 +44,7 @@ namespace MWGui // Retrieve the base skill value if the setting 'training skills based on base skill' is set; // otherwise returns the modified skill - float getSkillForTraining(const MWMechanics::NpcStats& stats, int skillId) const; + float getSkillForTraining(const MWMechanics::NpcStats& stats, ESM::RefId id) const; MyGUI::Widget* mTrainingOptions; MyGUI::Button* mCancelButton; @@ -50,7 +52,6 @@ namespace MWGui WaitDialogProgressBar mProgressBar; TimeAdvancer mTimeAdvancer; - bool mTrainingSkillBasedOnBaseSkill; //corresponds to the setting 'training skills based on base skill' }; } diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index ed7a74b95f7..6ba3a552866 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -1,29 +1,34 @@ #include "travelwindow.hpp" #include -#include #include +#include -#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwmechanics/creaturestats.hpp" -#include "../mwmechanics/actorutil.hpp" - +#include "../mwworld/actionteleport.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" -#include "../mwworld/actionteleport.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwworld/cellstore.hpp" +#include "../mwworld/store.hpp" +#include "../mwworld/worldmodel.hpp" + +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" namespace MWGui { - TravelWindow::TravelWindow() : - WindowBase("openmw_travel_window.layout") + TravelWindow::TravelWindow() + : WindowBase("openmw_travel_window.layout") , mCurrentY(0) { getWidget(mCancelButton, "CancelButton"); @@ -34,24 +39,19 @@ namespace MWGui 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()); + 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 std::string& name, ESM::Position pos, bool interior) + void TravelWindow::addDestination(const ESM::RefId& name, const ESM::Position& pos, bool interior) { int price; - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); if (!mPtr.getCell()->isExterior()) @@ -61,7 +61,8 @@ namespace MWGui else { ESM::Position PlayerPos = player.getRefData().getPosition(); - float d = sqrt(pow(pos.pos[0] - PlayerPos.pos[0], 2) + pow(pos.pos[1] - PlayerPos.pos[1], 2) + pow(pos.pos[2] - PlayerPos.pos[2], 2)); + float d = sqrt(pow(pos.pos[0] - PlayerPos.pos[0], 2) + pow(pos.pos[1] - PlayerPos.pos[1], 2) + + pow(pos.pos[2] - PlayerPos.pos[2], 2)); float fTravelMult = gmst.find("fTravelMult")->mValue.getFloat(); if (fTravelMult != 0) price = static_cast(d / fTravelMult); @@ -74,33 +75,36 @@ namespace MWGui // Add price for the travelling followers std::set followers; - MWWorld::ActionTeleport::getFollowers(player, followers); + MWWorld::ActionTeleport::getFollowers(player, followers, !interior); // Apply followers cost, unlike vanilla the first follower doesn't travel for free price *= 1 + static_cast(followers.size()); - int lineHeight = MWBase::Environment::get().getWindowManager()->getFontHeight() + 2; + const int lineHeight = Settings::gui().mFontSize + 2; - MyGUI::Button* toAdd = mDestinationsView->createWidget("SandTextButton", 0, mCurrentY, 200, lineHeight, MyGUI::Align::Default); + MyGUI::Button* toAdd = mDestinationsView->createWidget( + "SandTextButton", 0, mCurrentY, 200, lineHeight, MyGUI::Align::Default); toAdd->setEnabled(price <= playerGold); mCurrentY += lineHeight; - if(interior) - toAdd->setUserString("interior","y"); + if (interior) + toAdd->setUserString("interior", "y"); else - toAdd->setUserString("interior","n"); + toAdd->setUserString("interior", "n"); + const std::string& nameString = name.getRefIdString(); toAdd->setUserString("price", std::to_string(price)); - toAdd->setCaptionWithReplacing("#{sCell=" + name + "} - " + MyGUI::utility::toString(price)+"#{sgp}"); - toAdd->setSize(mDestinationsView->getWidth(),lineHeight); + toAdd->setCaptionWithReplacing( + "#{sCell=" + nameString + "} - " + MyGUI::utility::toString(price) + "#{sgp}"); + toAdd->setSize(mDestinationsView->getWidth(), lineHeight); toAdd->eventMouseWheel += MyGUI::newDelegate(this, &TravelWindow::onMouseWheel); - toAdd->setUserString("Destination", name); + toAdd->setUserString("Destination", nameString); toAdd->setUserData(pos); toAdd->eventMouseButtonClick += MyGUI::newDelegate(this, &TravelWindow::onTravelButtonClick); } void TravelWindow::clearDestinations() { - mDestinationsView->setViewOffset(MyGUI::IntPoint(0,0)); + mDestinationsView->setViewOffset(MyGUI::IntPoint(0, 0)); mCurrentY = 0; while (mDestinationsView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mDestinationsView->getChildAt(0)); @@ -108,6 +112,9 @@ namespace MWGui void TravelWindow::setPtr(const MWWorld::Ptr& actor) { + if (actor.isEmpty() || !actor.getClass().isActor()) + throw std::runtime_error("Invalid argument in TravelWindow::setPtr"); + center(); mPtr = actor; clearDestinations(); @@ -115,42 +122,41 @@ namespace MWGui std::vector transport; if (mPtr.getClass().isNpc()) transport = mPtr.get()->mBase->getTransport(); - else if (mPtr.getTypeName() == typeid(ESM::Creature).name()) + else if (mPtr.getType() == ESM::Creature::sRecordId) transport = mPtr.get()->mBase->getTransport(); - for(unsigned int i = 0;ipositionToIndex(transport[i].mPos.pos[0], - transport[i].mPos.pos[1],x,y); - if (cellname == "") + const ESM::ExteriorCellLocation cellIndex + = ESM::positionToExteriorCellLocation(dest.mPos.pos[0], dest.mPos.pos[1]); + if (cellname.empty()) { - MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(x,y); - cellname = MWBase::Environment::get().getWorld()->getCellName(cell); + MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getExterior(cellIndex); + cellname = MWBase::Environment::get().getWorld()->getCellName(&cell); interior = false; } - addDestination(cellname,transport[i].mPos,interior); + addDestination(ESM::RefId::stringRefId(cellname), dest.mPos, interior); } updateLabels(); - // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the + // scrollbar is hidden mDestinationsView->setVisibleVScroll(false); - mDestinationsView->setCanvasSize (MyGUI::IntSize(mDestinationsView->getWidth(), std::max(mDestinationsView->getHeight(), mCurrentY))); + mDestinationsView->setCanvasSize( + MyGUI::IntSize(mDestinationsView->getWidth(), std::max(mDestinationsView->getHeight(), mCurrentY))); mDestinationsView->setVisibleVScroll(true); } void TravelWindow::onTravelButtonClick(MyGUI::Widget* _sender) { - std::istringstream iss(_sender->getUserString("price")); - int price; - iss >> price; + const int price = Misc::StringUtils::toNumeric(_sender->getUserString("price"), 0); MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - if (playerGoldisExterior()) // Interior cell -> mages guild transport - MWBase::Environment::get().getWindowManager()->playSound("mysticism cast"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("mysticism cast")); - player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price); // add gold to NPC trading gold pool MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); @@ -169,14 +175,20 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->fadeScreenOut(1); ESM::Position pos = *_sender->getUserData(); - std::string cellname = _sender->getUserString("Destination"); + std::string_view cellname = _sender->getUserString("Destination"); bool interior = _sender->getUserString("interior") == "y"; if (mPtr.getCell()->isExterior()) { ESM::Position playerPos = player.getRefData().getPosition(); - float d = (osg::Vec3f(pos.pos[0], pos.pos[1], 0) - osg::Vec3f(playerPos.pos[0], playerPos.pos[1], 0)).length(); - int hours = static_cast(d /MWBase::Environment::get().getWorld()->getStore().get().find("fTravelTimeMult")->mValue.getFloat()); - MWBase::Environment::get().getMechanicsManager ()->rest (hours, true); + float d + = (osg::Vec3f(pos.pos[0], pos.pos[1], 0) - osg::Vec3f(playerPos.pos[0], playerPos.pos[1], 0)).length(); + int hours = static_cast(d + / MWBase::Environment::get() + .getESMStore() + ->get() + .find("fTravelTimeMult") + ->mValue.getFloat()); + MWBase::Environment::get().getMechanicsManager()->rest(hours, true); MWBase::Environment::get().getWorld()->advanceTime(hours); } @@ -184,9 +196,11 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); MWBase::Environment::get().getWindowManager()->fadeScreenOut(1); + const ESM::ExteriorCellLocation posCell = ESM::positionToExteriorCellLocation(pos.pos[0], pos.pos[1]); + ESM::RefId cellId = ESM::Cell::generateIdForCell(!interior, cellname, posCell.mX, posCell.mY); // Teleports any followers, too. - MWWorld::ActionTeleport action(interior ? cellname : "", pos, true); + MWWorld::ActionTeleport action(cellId, pos, true); action.execute(player); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0); @@ -200,14 +214,11 @@ namespace MWGui void TravelWindow::updateLabels() { - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); mPlayerGold->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); - mPlayerGold->setCoord(8, - mPlayerGold->getTop(), - mPlayerGold->getTextSize().width, - mPlayerGold->getHeight()); + mPlayerGold->setCoord(8, mPlayerGold->getTop(), mPlayerGold->getTextSize().width, mPlayerGold->getHeight()); } void TravelWindow::onReferenceUnavailable() @@ -218,10 +229,10 @@ namespace MWGui void TravelWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { - if (mDestinationsView->getViewOffset().top + _rel*0.3f > 0) + if (mDestinationsView->getViewOffset().top + _rel * 0.3f > 0) mDestinationsView->setViewOffset(MyGUI::IntPoint(0, 0)); else - mDestinationsView->setViewOffset(MyGUI::IntPoint(0, static_cast(mDestinationsView->getViewOffset().top + _rel*0.3f))); + mDestinationsView->setViewOffset( + MyGUI::IntPoint(0, static_cast(mDestinationsView->getViewOffset().top + _rel * 0.3f))); } } - diff --git a/apps/openmw/mwgui/travelwindow.hpp b/apps/openmw/mwgui/travelwindow.hpp index 00b7db73059..6d7c1c7376a 100644 --- a/apps/openmw/mwgui/travelwindow.hpp +++ b/apps/openmw/mwgui/travelwindow.hpp @@ -1,43 +1,44 @@ #ifndef MWGUI_TravelWINDOW_H #define MWGUI_TravelWINDOW_H - -#include "windowbase.hpp" #include "referenceinterface.hpp" +#include "windowbase.hpp" namespace MyGUI { - class Gui; - class Widget; + class Gui; + class Widget; } namespace MWGui { class TravelWindow : public ReferenceInterface, public WindowBase { - public: - TravelWindow(); + public: + TravelWindow(); + + void setPtr(const MWWorld::Ptr& actor) override; - void setPtr (const MWWorld::Ptr& actor) override; + std::string_view getWindowIdForLua() const override { return "Travel"; } - protected: - MyGUI::Button* mCancelButton; - MyGUI::TextBox* mPlayerGold; - MyGUI::TextBox* mDestinations; - MyGUI::TextBox* mSelect; + protected: + MyGUI::Button* mCancelButton; + MyGUI::TextBox* mPlayerGold; + MyGUI::TextBox* mDestinations; + MyGUI::TextBox* mSelect; - MyGUI::ScrollView* mDestinationsView; + MyGUI::ScrollView* mDestinationsView; - void onCancelButtonClicked(MyGUI::Widget* _sender); - void onTravelButtonClick(MyGUI::Widget* _sender); - void onMouseWheel(MyGUI::Widget* _sender, int _rel); - void addDestination(const std::string& name, ESM::Position pos, bool interior); - void clearDestinations(); - int mCurrentY; + void onCancelButtonClicked(MyGUI::Widget* _sender); + void onTravelButtonClick(MyGUI::Widget* _sender); + void onMouseWheel(MyGUI::Widget* _sender, int _rel); + void addDestination(const ESM::RefId& name, const ESM::Position& pos, bool interior); + void clearDestinations(); + int mCurrentY; - void updateLabels(); + void updateLabels(); - void onReferenceUnavailable() override; + void onReferenceUnavailable() override; }; } diff --git a/apps/openmw/mwgui/videowidget.cpp b/apps/openmw/mwgui/videowidget.cpp index 2aea0018d53..a82d8ce67f3 100644 --- a/apps/openmw/mwgui/videowidget.cpp +++ b/apps/openmw/mwgui/videowidget.cpp @@ -7,113 +7,112 @@ #include #include -#include #include +#include #include "../mwsound/movieaudiofactory.hpp" namespace MWGui { -VideoWidget::VideoWidget() - : mVFS(nullptr) -{ - mPlayer.reset(new Video::VideoPlayer()); - setNeedKeyFocus(true); -} - -VideoWidget::~VideoWidget() = default; - -void VideoWidget::setVFS(const VFS::Manager *vfs) -{ - mVFS = vfs; -} + VideoWidget::VideoWidget() + : mVFS(nullptr) + { + mPlayer = std::make_unique(); + setNeedKeyFocus(true); + } -void VideoWidget::playVideo(const std::string &video) -{ - mPlayer->setAudioFactory(new MWSound::MovieAudioFactory()); + VideoWidget::~VideoWidget() = default; - Files::IStreamPtr videoStream; - try + void VideoWidget::setVFS(const VFS::Manager* vfs) { - videoStream = mVFS->get(video); + mVFS = vfs; } - catch (std::exception& e) + + void VideoWidget::playVideo(const std::string& video) { - Log(Debug::Error) << "Failed to open video: " << e.what(); - return; - } + mPlayer->setAudioFactory(new MWSound::MovieAudioFactory()); - mPlayer->playVideo(videoStream, video); + Files::IStreamPtr videoStream; + try + { + videoStream = mVFS->get(video); + } + catch (std::exception& e) + { + Log(Debug::Error) << "Failed to open video: " << e.what(); + return; + } - osg::ref_ptr texture = mPlayer->getVideoTexture(); - if (!texture) - return; + mPlayer->playVideo(std::move(videoStream), video); - mTexture.reset(new osgMyGUI::OSGTexture(texture)); + osg::ref_ptr texture = mPlayer->getVideoTexture(); + if (!texture) + return; - setRenderItemTexture(mTexture.get()); - getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 1.f, 1.f, 0.f)); -} + mTexture = std::make_unique(texture); -int VideoWidget::getVideoWidth() -{ - return mPlayer->getVideoWidth(); -} + setRenderItemTexture(mTexture.get()); + getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 1.f, 1.f, 0.f)); + } -int VideoWidget::getVideoHeight() -{ - return mPlayer->getVideoHeight(); -} + int VideoWidget::getVideoWidth() + { + return mPlayer->getVideoWidth(); + } -bool VideoWidget::update() -{ - return mPlayer->update(); -} + int VideoWidget::getVideoHeight() + { + return mPlayer->getVideoHeight(); + } -void VideoWidget::stop() -{ - mPlayer->close(); -} + bool VideoWidget::update() + { + return mPlayer->update(); + } -void VideoWidget::pause() -{ - mPlayer->pause(); -} + void VideoWidget::stop() + { + mPlayer->close(); + } -void VideoWidget::resume() -{ - mPlayer->play(); -} + void VideoWidget::pause() + { + mPlayer->pause(); + } -bool VideoWidget::isPaused() const -{ - return mPlayer->isPaused(); -} + void VideoWidget::resume() + { + mPlayer->play(); + } -bool VideoWidget::hasAudioStream() -{ - return mPlayer->hasAudioStream(); -} + bool VideoWidget::isPaused() const + { + return mPlayer->isPaused(); + } -void VideoWidget::autoResize(bool stretch) -{ - MyGUI::IntSize screenSize = MyGUI::RenderManager::getInstance().getViewSize(); - if (getParent()) - screenSize = getParent()->getSize(); + bool VideoWidget::hasAudioStream() + { + return mPlayer->hasAudioStream(); + } - if (getVideoHeight() > 0 && !stretch) + void VideoWidget::autoResize(bool stretch) { - double imageaspect = static_cast(getVideoWidth())/getVideoHeight(); + MyGUI::IntSize screenSize = MyGUI::RenderManager::getInstance().getViewSize(); + if (getParent()) + screenSize = getParent()->getSize(); + + if (getVideoHeight() > 0 && !stretch) + { + double imageaspect = static_cast(getVideoWidth()) / getVideoHeight(); - int leftPadding = std::max(0, static_cast(screenSize.width - screenSize.height * imageaspect) / 2); - int topPadding = std::max(0, static_cast(screenSize.height - screenSize.width / imageaspect) / 2); + int leftPadding = std::max(0, static_cast(screenSize.width - screenSize.height * imageaspect) / 2); + int topPadding = std::max(0, static_cast(screenSize.height - screenSize.width / imageaspect) / 2); - setCoord(leftPadding, topPadding, - screenSize.width - leftPadding*2, screenSize.height - topPadding*2); + setCoord(leftPadding, topPadding, screenSize.width - leftPadding * 2, screenSize.height - topPadding * 2); + } + else + setCoord(0, 0, screenSize.width, screenSize.height); } - else - setCoord(0,0,screenSize.width,screenSize.height); -} } diff --git a/apps/openmw/mwgui/videowidget.hpp b/apps/openmw/mwgui/videowidget.hpp index 814b9ca73a4..f6f70068e31 100644 --- a/apps/openmw/mwgui/videowidget.hpp +++ b/apps/openmw/mwgui/videowidget.hpp @@ -27,13 +27,13 @@ namespace MWGui MYGUI_RTTI_DERIVED(VideoWidget) VideoWidget(); - + ~VideoWidget(); /// Set the VFS (virtual file system) to find the videos on. void setVFS(const VFS::Manager* vfs); - void playVideo (const std::string& video); + void playVideo(const std::string& video); int getVideoWidth(); int getVideoHeight(); @@ -55,7 +55,7 @@ namespace MWGui /// based on the dimensions of the playing video. /// @param stretch Stretch the video to fill the whole screen? If false, /// black bars may be added to fix the aspect ratio. - void autoResize (bool stretch); + void autoResize(bool stretch); private: const VFS::Manager* mVFS; diff --git a/apps/openmw/mwgui/waitdialog.cpp b/apps/openmw/mwgui/waitdialog.cpp index 42a70dcfa77..568f05abc33 100644 --- a/apps/openmw/mwgui/waitdialog.cpp +++ b/apps/openmw/mwgui/waitdialog.cpp @@ -1,27 +1,30 @@ #include "waitdialog.hpp" -#include #include +#include #include #include +#include +#include +#include #include -#include -#include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/statemanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" -#include "../mwmechanics/actorutil.hpp" namespace MWGui { @@ -38,10 +41,10 @@ namespace MWGui center(); } - void WaitDialogProgressBar::setProgress (int cur, int total) + void WaitDialogProgressBar::setProgress(int cur, int total) { - mProgressBar->setProgressRange (total); - mProgressBar->setProgressPosition (cur); + mProgressBar->setProgressRange(total); + mProgressBar->setProgressPosition(cur); mProgressText->setCaption(MyGUI::utility::toString(cur) + "/" + MyGUI::utility::toString(total)); } @@ -79,17 +82,17 @@ namespace MWGui mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &WaitDialog::onWaitingFinished); } - void WaitDialog::setPtr(const MWWorld::Ptr &ptr) + void WaitDialog::setPtr(const MWWorld::Ptr& ptr) { - setCanRest(!ptr.isEmpty() || MWBase::Environment::get().getWorld ()->canRest () == MWBase::World::Rest_Allowed); + setCanRest(!ptr.isEmpty() || MWBase::Environment::get().getWorld()->canRest() == MWBase::World::Rest_Allowed); - if (ptr.isEmpty() && MWBase::Environment::get().getWorld ()->canRest() == MWBase::World::Rest_PlayerIsInAir) + if (ptr.isEmpty() && MWBase::Environment::get().getWorld()->canRest() == MWBase::World::Rest_PlayerIsInAir) { // Resting in air is not allowed unless you're using a bed - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage1}"); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage1}"); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Rest); } - + if (mUntilHealedButton->getVisible()) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mUntilHealedButton); else @@ -98,12 +101,22 @@ namespace MWGui bool WaitDialog::exit() { - return (!mTimeAdvancer.isRunning()); //Only exit if not currently waiting + bool canExit = !mTimeAdvancer.isRunning(); // Only exit if not currently waiting + if (canExit) + { + clear(); + stopWaiting(); + } + return canExit; } void WaitDialog::clear() { mSleeping = false; + mHours = 1; + mManualHours = 1; + mFadeTimeRemaining = 0; + mInterruptAt = -1; mTimeAdvancer.stop(); } @@ -120,39 +133,43 @@ namespace MWGui mProgressBar.setVisible(false); } - if (!MWBase::Environment::get().getWindowManager ()->getRestEnabled ()) + if (!MWBase::Environment::get().getWindowManager()->getRestEnabled()) { - MWBase::Environment::get().getWindowManager()->popGuiMode (); + MWBase::Environment::get().getWindowManager()->popGuiMode(); } - MWBase::World::RestPermitted canRest = MWBase::Environment::get().getWorld ()->canRest (); + 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 (); + 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 (); + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage1}"); + MWBase::Environment::get().getWindowManager()->popGuiMode(); } onHourSliderChangedPosition(mHourSlider, 0); - mHourSlider->setScrollPosition (0); + mHourSlider->setScrollPosition(0); - std::string month = MWBase::Environment::get().getWorld ()->getMonthName(); - int hour = static_cast(MWBase::Environment::get().getWorld()->getTimeStamp().getHour()); + const MWWorld::DateTimeManager& timeManager = *MWBase::Environment::get().getWorld()->getTimeManager(); + std::string_view month = timeManager.getMonthName(); + int hour = static_cast(timeManager.getTimeStamp().getHour()); bool pm = hour >= 12; - if (hour >= 13) hour -= 12; - if (hour == 0) hour = 12; - - ESM::EpochTimeStamp currentDate = MWBase::Environment::get().getWorld()->getEpochTimeStamp(); - int daysPassed = MWBase::Environment::get().getWorld()->getTimeStamp().getDay(); - std::string formattedHour = pm ? "#{sSaveMenuHelp05}" : "#{sSaveMenuHelp04}"; - std::string dateTimeText = Misc::StringUtils::format("%i %s (#{sDay} %i) %i %s", currentDate.mDay, month, daysPassed, hour, formattedHour); - mDateTimeText->setCaptionWithReplacing (dateTimeText); + if (hour >= 13) + hour -= 12; + if (hour == 0) + hour = 12; + + ESM::EpochTimeStamp currentDate = timeManager.getEpochTimeStamp(); + std::string daysPassed = Misc::StringUtils::format("(#{Calendar:day} %i)", timeManager.getTimeStamp().getDay()); + std::string_view formattedHour(pm ? "#{Calendar:pm}" : "#{Calendar:am}"); + std::string dateTimeText + = Misc::StringUtils::format("%i %s %s %i %s", currentDate.mDay, month, daysPassed, hour, formattedHour); + mDateTimeText->setCaptionWithReplacing(dateTimeText); } void WaitDialog::onUntilHealedButtonClicked(MyGUI::Widget* sender) @@ -169,7 +186,7 @@ namespace MWGui void WaitDialog::startWaiting(int hoursToWait) { - if(Settings::Manager::getBool("autosave","Saves")) //autosaves when enabled + if (Settings::saves().mAutosave) // autosaves when enabled MWBase::Environment::get().getStateManager()->quickSave("Autosave"); MWBase::World* world = MWBase::Environment::get().getWorld(); @@ -184,18 +201,20 @@ namespace MWGui MWWorld::Ptr player = world->getPlayerPtr(); if (mSleeping && player.getCell()->isExterior()) { - std::string regionstr = player.getCell()->getCell()->mRegion; + const ESM::RefId& regionstr = player.getCell()->getCell()->getRegion(); if (!regionstr.empty()) { - const ESM::Region *region = world->getStore().get().find (regionstr); + const ESM::Region* region = world->getStore().get().find(regionstr); if (!region->mSleepList.empty()) { // figure out if player will be woken while sleeping - int x = Misc::Rng::rollDice(hoursToWait); - float fSleepRandMod = world->getStore().get().find("fSleepRandMod")->mValue.getFloat(); + int x = Misc::Rng::rollDice(hoursToWait, world->getPrng()); + float fSleepRandMod + = world->getStore().get().find("fSleepRandMod")->mValue.getFloat(); if (x < fSleepRandMod * hoursToWait) { - float fSleepRestMod = world->getStore().get().find("fSleepRestMod")->mValue.getFloat(); + float fSleepRestMod + = world->getStore().get().find("fSleepRestMod")->mValue.getFloat(); int interruptAtHoursRemaining = int(fSleepRestMod * hoursToWait); if (interruptAtHoursRemaining != 0) { @@ -207,7 +226,7 @@ namespace MWGui } } - mProgressBar.setProgress (0, hoursToWait); + mProgressBar.setProgress(0, hoursToWait); } void WaitDialog::onCancelButtonClicked(MyGUI::Widget* sender) @@ -217,17 +236,18 @@ namespace MWGui void WaitDialog::onHourSliderChangedPosition(MyGUI::ScrollBar* sender, size_t position) { - mHourText->setCaptionWithReplacing (MyGUI::utility::toString(position+1) + " #{sRestMenu2}"); - mManualHours = position+1; + mHourText->setCaptionWithReplacing(MyGUI::utility::toString(position + 1) + " #{sRestMenu2}"); + mManualHours = position + 1; MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mWaitButton); } - void WaitDialog::onKeyButtonPressed(MyGUI::Widget *sender, MyGUI::KeyCode key, MyGUI::Char character) + void WaitDialog::onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character) { if (key == MyGUI::KeyCode::ArrowUp) - mHourSlider->setScrollPosition(std::min(mHourSlider->getScrollPosition()+1, mHourSlider->getScrollRange()-1)); + mHourSlider->setScrollPosition( + std::min(mHourSlider->getScrollPosition() + 1, mHourSlider->getScrollRange() - 1)); else if (key == MyGUI::KeyCode::ArrowDown) - mHourSlider->setScrollPosition(std::max(static_cast(mHourSlider->getScrollPosition())-1, 0)); + mHourSlider->setScrollPosition(std::max(static_cast(mHourSlider->getScrollPosition()) - 1, 0)); else return; onHourSliderChangedPosition(mHourSlider, mHourSlider->getScrollPosition()); @@ -256,31 +276,30 @@ namespace MWGui stopWaiting(); MWWorld::Ptr player = MWMechanics::getPlayer(); - const MWMechanics::NpcStats &pcstats = player.getClass().getNpcStats(player); + const MWMechanics::NpcStats& pcstats = player.getClass().getNpcStats(player); // trigger levelup if possible - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); - if (mSleeping && pcstats.getLevelProgress () >= gmst.find("iLevelUpTotal")->mValue.getInteger()) + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); + if (mSleeping && pcstats.getLevelProgress() >= gmst.find("iLevelUpTotal")->mValue.getInteger()) { - MWBase::Environment::get().getWindowManager()->pushGuiMode (GM_Levelup); + MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Levelup); } } - void WaitDialog::setCanRest (bool canRest) + void WaitDialog::setCanRest(bool canRest) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); bool full = (stats.getHealth().getCurrent() >= stats.getHealth().getModified()) - && (stats.getMagicka().getCurrent() >= stats.getMagicka().getModified()); + && (stats.getMagicka().getCurrent() >= stats.getMagicka().getModified()); MWMechanics::NpcStats& npcstats = player.getClass().getNpcStats(player); bool werewolf = npcstats.isWerewolf(); mUntilHealedButton->setVisible(canRest && !full); - mWaitButton->setCaptionWithReplacing (canRest ? "#{sRest}" : "#{sWait}"); - mRestText->setCaptionWithReplacing (canRest ? "#{sRestMenu3}" - : (werewolf ? "#{sWerewolfRestMessage}" - : "#{sRestIllegal}")); + mWaitButton->setCaptionWithReplacing(canRest ? "#{sRest}" : "#{sWait}"); + mRestText->setCaptionWithReplacing( + canRest ? "#{sRestMenu3}" : (werewolf ? "#{sWerewolfRestMessage}" : "#{sRestIllegal}")); mSleeping = canRest; @@ -307,16 +326,15 @@ namespace MWGui } } - void WaitDialog::stopWaiting () + void WaitDialog::stopWaiting() { MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.2f); - mProgressBar.setVisible (false); - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Rest); + mProgressBar.setVisible(false); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Rest); mTimeAdvancer.stop(); } - - void WaitDialog::wakeUp () + void WaitDialog::wakeUp() { mSleeping = false; if (mInterruptAt != -1) diff --git a/apps/openmw/mwgui/waitdialog.hpp b/apps/openmw/mwgui/waitdialog.hpp index bf84e7e819d..3d66584f544 100644 --- a/apps/openmw/mwgui/waitdialog.hpp +++ b/apps/openmw/mwgui/waitdialog.hpp @@ -2,8 +2,8 @@ #define MWGUI_WAIT_DIALOG_H #include "timeadvancer.hpp" - #include "windowbase.hpp" +#include namespace MWGui { @@ -27,7 +27,7 @@ namespace MWGui public: WaitDialog(); - void setPtr(const MWWorld::Ptr &ptr) override; + void setPtr(const MWWorld::Ptr& ptr) override; void onOpen() override; @@ -43,6 +43,8 @@ namespace MWGui WindowBase* getProgressBar() { return &mProgressBar; } + std::string_view getWindowIdForLua() const override { return "WaitDialog"; } + protected: MyGUI::TextBox* mDateTimeText; MyGUI::TextBox* mRestText; @@ -59,7 +61,7 @@ namespace MWGui float mFadeTimeRemaining; int mInterruptAt; - std::string mInterruptCreatureList; + ESM::RefId mInterruptCreatureList; WaitDialogProgressBar mProgressBar; diff --git a/apps/openmw/mwgui/widgets.cpp b/apps/openmw/mwgui/widgets.cpp index fb8521f0675..6cc5bdfdf50 100644 --- a/apps/openmw/mwgui/widgets.cpp +++ b/apps/openmw/mwgui/widgets.cpp @@ -1,538 +1,517 @@ #include "widgets.hpp" -#include #include -#include -#include +#include #include +#include +#include +#include + +#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwworld/esmstore.hpp" +#include "../mwmechanics/magiceffects.hpp" -#include "controllers.hpp" +#include "../mwworld/esmstore.hpp" -namespace MWGui +namespace MWGui::Widgets { - namespace Widgets + /* MWSkill */ + + MWSkill::MWSkill() + : mSkillNameWidget(nullptr) + , mSkillValueWidget(nullptr) { - /* MWSkill */ + } - MWSkill::MWSkill() - : mSkillId(ESM::Skill::Length) - , mSkillNameWidget(nullptr) - , mSkillValueWidget(nullptr) - { - } + void MWSkill::setSkillId(ESM::RefId skill) + { + mSkillId = skill; + updateWidgets(); + } - void MWSkill::setSkillId(ESM::Skill::SkillEnum skill) - { - mSkillId = skill; - updateWidgets(); - } + void MWSkill::setSkillValue(const SkillValue& value) + { + mValue = value; + updateWidgets(); + } - void MWSkill::setSkillNumber(int skill) + void MWSkill::updateWidgets() + { + if (mSkillNameWidget) { - if (skill < 0) - setSkillId(ESM::Skill::Length); - else if (skill < ESM::Skill::Length) - setSkillId(static_cast(skill)); + const ESM::Skill* skill = MWBase::Environment::get().getESMStore()->get().search(mSkillId); + if (skill == nullptr) + mSkillNameWidget->setCaption({}); else - throw std::runtime_error("Skill number out of range"); + mSkillNameWidget->setCaption(skill->mName); } - - void MWSkill::setSkillValue(const SkillValue& value) + if (mSkillValueWidget) { - mValue = value; - updateWidgets(); + SkillValue::Type modified = mValue.getModified(), base = mValue.getBase(); + mSkillValueWidget->setCaption(MyGUI::utility::toString(modified)); + if (modified > base) + mSkillValueWidget->_setWidgetState("increased"); + else if (modified < base) + mSkillValueWidget->_setWidgetState("decreased"); + else + mSkillValueWidget->_setWidgetState("normal"); } + } - void MWSkill::updateWidgets() - { - if (mSkillNameWidget) - { - if (mSkillId == ESM::Skill::Length) - { - mSkillNameWidget->setCaption(""); - } - else - { - const std::string &name = MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Skill::sSkillNameIds[mSkillId], ""); - mSkillNameWidget->setCaption(name); - } - } - if (mSkillValueWidget) - { - SkillValue::Type modified = mValue.getModified(), base = mValue.getBase(); - mSkillValueWidget->setCaption(MyGUI::utility::toString(modified)); - if (modified > base) - mSkillValueWidget->_setWidgetState("increased"); - else if (modified < base) - mSkillValueWidget->_setWidgetState("decreased"); - else - mSkillValueWidget->_setWidgetState("normal"); - } - } + void MWSkill::onClicked(MyGUI::Widget* _sender) + { + eventClicked(this); + } - void MWSkill::onClicked(MyGUI::Widget* _sender) - { - eventClicked(this); - } + MWSkill::~MWSkill() {} + + void MWSkill::initialiseOverride() + { + Base::initialiseOverride(); + + assignWidget(mSkillNameWidget, "StatName"); + assignWidget(mSkillValueWidget, "StatValue"); - MWSkill::~MWSkill() + MyGUI::Button* button; + assignWidget(button, "StatNameButton"); + if (button) { + mSkillNameWidget = button; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWSkill::onClicked); } - void MWSkill::initialiseOverride() + button = nullptr; + assignWidget(button, "StatValueButton"); + if (button) { - Base::initialiseOverride(); - - assignWidget(mSkillNameWidget, "StatName"); - assignWidget(mSkillValueWidget, "StatValue"); - - MyGUI::Button* button; - assignWidget(button, "StatNameButton"); - if (button) - { - mSkillNameWidget = button; - button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWSkill::onClicked); - } - - button = nullptr; - assignWidget(button, "StatValueButton"); - if (button) - { - mSkillValueWidget = button; - button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWSkill::onClicked); - } + mSkillValueWidget = button; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWSkill::onClicked); } + } - /* MWAttribute */ + /* MWAttribute */ - MWAttribute::MWAttribute() - : mId(-1) - , mAttributeNameWidget(nullptr) - , mAttributeValueWidget(nullptr) - { - } + MWAttribute::MWAttribute() + : mAttributeNameWidget(nullptr) + , mAttributeValueWidget(nullptr) + { + } - void MWAttribute::setAttributeId(int attributeId) - { - mId = attributeId; - updateWidgets(); - } + void MWAttribute::setAttributeId(ESM::RefId attributeId) + { + mId = attributeId; + updateWidgets(); + } - void MWAttribute::setAttributeValue(const AttributeValue& value) - { - mValue = value; - updateWidgets(); - } + void MWAttribute::setAttributeValue(const AttributeValue& value) + { + mValue = value; + updateWidgets(); + } - void MWAttribute::onClicked(MyGUI::Widget* _sender) - { - eventClicked(this); - } + void MWAttribute::onClicked(MyGUI::Widget* _sender) + { + eventClicked(this); + } - void MWAttribute::updateWidgets() + void MWAttribute::updateWidgets() + { + if (mAttributeNameWidget) { - if (mAttributeNameWidget) + const ESM::Attribute* attribute + = MWBase::Environment::get().getESMStore()->get().search(mId); + if (!attribute) { - if (mId < 0 || mId >= 8) - { - mAttributeNameWidget->setCaption(""); - } - else - { - static const char *attributes[8] = { - "sAttributeStrength", - "sAttributeIntelligence", - "sAttributeWillpower", - "sAttributeAgility", - "sAttributeSpeed", - "sAttributeEndurance", - "sAttributePersonality", - "sAttributeLuck" - }; - const std::string &name = MWBase::Environment::get().getWindowManager()->getGameSettingString(attributes[mId], ""); - mAttributeNameWidget->setCaption(name); - } + mAttributeNameWidget->setCaption({}); } - if (mAttributeValueWidget) + else { - int modified = mValue.getModified(), base = mValue.getBase(); - mAttributeValueWidget->setCaption(MyGUI::utility::toString(modified)); - if (modified > base) - mAttributeValueWidget->_setWidgetState("increased"); - else if (modified < base) - mAttributeValueWidget->_setWidgetState("decreased"); - else - mAttributeValueWidget->_setWidgetState("normal"); + mAttributeNameWidget->setCaption(MyGUI::UString(attribute->mName)); } } - - MWAttribute::~MWAttribute() + if (mAttributeValueWidget) { + int modified = mValue.getModified(), base = mValue.getBase(); + mAttributeValueWidget->setCaption(MyGUI::utility::toString(modified)); + if (modified > base) + mAttributeValueWidget->_setWidgetState("increased"); + else if (modified < base) + mAttributeValueWidget->_setWidgetState("decreased"); + else + mAttributeValueWidget->_setWidgetState("normal"); } + } - void MWAttribute::initialiseOverride() - { - Base::initialiseOverride(); - - assignWidget(mAttributeNameWidget, "StatName"); - assignWidget(mAttributeValueWidget, "StatValue"); - - MyGUI::Button* button; - assignWidget(button, "StatNameButton"); - if (button) - { - mAttributeNameWidget = button; - button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWAttribute::onClicked); - } - - button = nullptr; - assignWidget(button, "StatValueButton"); - if (button) - { - mAttributeValueWidget = button; - button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWAttribute::onClicked); - } - } + void MWAttribute::initialiseOverride() + { + Base::initialiseOverride(); - /* MWSpell */ + assignWidget(mAttributeNameWidget, "StatName"); + assignWidget(mAttributeValueWidget, "StatValue"); - MWSpell::MWSpell() - : mSpellNameWidget(nullptr) + MyGUI::Button* button; + assignWidget(button, "StatNameButton"); + if (button) { + mAttributeNameWidget = button; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWAttribute::onClicked); } - void MWSpell::setSpellId(const std::string &spellId) + button = nullptr; + assignWidget(button, "StatValueButton"); + if (button) { - mId = spellId; - updateWidgets(); + mAttributeValueWidget = button; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWAttribute::onClicked); } + } - void MWSpell::createEffectWidgets(std::vector &effects, MyGUI::Widget* creator, MyGUI::IntCoord &coord, int flags) - { - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); + /* MWSpell */ - const ESM::Spell *spell = store.get().search(mId); - MYGUI_ASSERT(spell, "spell with id '" << mId << "' not found"); + MWSpell::MWSpell() + : mSpellNameWidget(nullptr) + { + } - for (const ESM::ENAMstruct& effectInfo : spell->mEffects.mList) - { - MWSpellEffectPtr effect = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); - SpellEffectParams params; - params.mEffectID = effectInfo.mEffectID; - params.mSkill = effectInfo.mSkill; - params.mAttribute = effectInfo.mAttribute; - params.mDuration = effectInfo.mDuration; - params.mMagnMin = effectInfo.mMagnMin; - params.mMagnMax = effectInfo.mMagnMax; - params.mRange = effectInfo.mRange; - params.mIsConstant = (flags & MWEffectList::EF_Constant) != 0; - params.mNoTarget = (flags & MWEffectList::EF_NoTarget); - params.mNoMagnitude = (flags & MWEffectList::EF_NoMagnitude); - effect->setSpellEffect(params); - effects.push_back(effect); - coord.top += effect->getHeight(); - coord.width = std::max(coord.width, effect->getRequestedWidth()); - } - } + void MWSpell::setSpellId(const ESM::RefId& spellId) + { + mId = spellId; + updateWidgets(); + } - void MWSpell::updateWidgets() - { - if (mSpellNameWidget && MWBase::Environment::get().getWindowManager()) - { - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); - - const ESM::Spell *spell = store.get().search(mId); - if (spell) - mSpellNameWidget->setCaption(spell->mName); - else - mSpellNameWidget->setCaption(""); - } + void MWSpell::createEffectWidgets( + std::vector& effects, MyGUI::Widget* creator, MyGUI::IntCoord& coord, int flags) + { + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + + const ESM::Spell* spell = store.get().search(mId); + MYGUI_ASSERT(spell, "spell with id '" << mId << "' not found"); + + for (const ESM::IndexedENAMstruct& effectInfo : spell->mEffects.mList) + { + MWSpellEffectPtr effect + = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); + SpellEffectParams params; + params.mEffectID = effectInfo.mData.mEffectID; + params.mSkill = ESM::Skill::indexToRefId(effectInfo.mData.mSkill); + params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mData.mAttribute); + params.mDuration = effectInfo.mData.mDuration; + params.mMagnMin = effectInfo.mData.mMagnMin; + params.mMagnMax = effectInfo.mData.mMagnMax; + params.mRange = effectInfo.mData.mRange; + params.mIsConstant = (flags & MWEffectList::EF_Constant) != 0; + params.mNoTarget = (flags & MWEffectList::EF_NoTarget); + params.mNoMagnitude = (flags & MWEffectList::EF_NoMagnitude); + effect->setSpellEffect(params); + effects.push_back(effect); + coord.top += effect->getHeight(); + coord.width = std::max(coord.width, effect->getRequestedWidth()); } + } - void MWSpell::initialiseOverride() + void MWSpell::updateWidgets() + { + if (mSpellNameWidget && MWBase::Environment::get().getWindowManager()) { - Base::initialiseOverride(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - assignWidget(mSpellNameWidget, "StatName"); + const ESM::Spell* spell = store.get().search(mId); + if (spell) + mSpellNameWidget->setCaption(spell->mName); + else + mSpellNameWidget->setCaption({}); } + } - MWSpell::~MWSpell() - { - } + void MWSpell::initialiseOverride() + { + Base::initialiseOverride(); - /* MWEffectList */ + assignWidget(mSpellNameWidget, "StatName"); + } - MWEffectList::MWEffectList() - : mEffectList(0) - { - } + MWSpell::~MWSpell() {} + + /* MWEffectList */ + + MWEffectList::MWEffectList() + : mEffectList(0) + { + } - void MWEffectList::setEffectList(const SpellEffectList& list) + void MWEffectList::setEffectList(const SpellEffectList& list) + { + mEffectList = list; + updateWidgets(); + } + + void MWEffectList::createEffectWidgets( + std::vector& effects, MyGUI::Widget* creator, MyGUI::IntCoord& coord, bool center, int flags) + { + // We don't know the width of all the elements beforehand, so we do it in + // 2 steps: first, create all widgets and check their width.... + MWSpellEffectPtr effect = nullptr; + int maxwidth = coord.width; + + for (auto& effectInfo : mEffectList) { - mEffectList = list; - updateWidgets(); + effect = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); + effectInfo.mIsConstant = (flags & EF_Constant) || effectInfo.mIsConstant; + effectInfo.mNoTarget = (flags & EF_NoTarget) || effectInfo.mNoTarget; + effectInfo.mNoMagnitude = (flags & EF_NoMagnitude) || effectInfo.mNoMagnitude; + effect->setSpellEffect(effectInfo); + effects.push_back(effect); + if (effect->getRequestedWidth() > maxwidth) + maxwidth = effect->getRequestedWidth(); + + coord.top += effect->getHeight(); } - void MWEffectList::createEffectWidgets(std::vector &effects, MyGUI::Widget* creator, MyGUI::IntCoord &coord, bool center, int flags) + // ... then adjust the size for all widgets + for (MyGUI::Widget* effectWidget : effects) { - // We don't know the width of all the elements beforehand, so we do it in - // 2 steps: first, create all widgets and check their width.... - MWSpellEffectPtr effect = nullptr; - int maxwidth = coord.width; - - for (auto& effectInfo : mEffectList) + effect = effectWidget->castType(); + bool needcenter = center && (maxwidth > effect->getRequestedWidth()); + int diff = maxwidth - effect->getRequestedWidth(); + if (needcenter) { - effect = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); - effectInfo.mIsConstant = (flags & EF_Constant) || effectInfo.mIsConstant; - effectInfo.mNoTarget = (flags & EF_NoTarget) || effectInfo.mNoTarget; - effectInfo.mNoMagnitude = (flags & EF_NoMagnitude) || effectInfo.mNoMagnitude; - effect->setSpellEffect(effectInfo); - effects.push_back(effect); - if (effect->getRequestedWidth() > maxwidth) - maxwidth = effect->getRequestedWidth(); - - coord.top += effect->getHeight(); + effect->setCoord( + diff / 2, effect->getCoord().top, effect->getRequestedWidth(), effect->getCoord().height); } - - // ... then adjust the size for all widgets - for (MyGUI::Widget* effectWidget : effects) + else { - effect = effectWidget->castType(); - bool needcenter = center && (maxwidth > effect->getRequestedWidth()); - int diff = maxwidth - effect->getRequestedWidth(); - if (needcenter) - { - effect->setCoord(diff/2, effect->getCoord().top, effect->getRequestedWidth(), effect->getCoord().height); - } - else - { - effect->setCoord(0, effect->getCoord().top, effect->getRequestedWidth(), effect->getCoord().height); - } + effect->setCoord(0, effect->getCoord().top, effect->getRequestedWidth(), effect->getCoord().height); } - - // inform the parent about width - coord.width = maxwidth; } - void MWEffectList::updateWidgets() - { - } + // inform the parent about width + coord.width = maxwidth; + } - void MWEffectList::initialiseOverride() - { - Base::initialiseOverride(); - } + void MWEffectList::updateWidgets() {} - MWEffectList::~MWEffectList() - { - } + void MWEffectList::initialiseOverride() + { + Base::initialiseOverride(); + } - SpellEffectList MWEffectList::effectListFromESM(const ESM::EffectList* effects) - { - SpellEffectList result; - for (const ESM::ENAMstruct& effectInfo : effects->mList) - { - SpellEffectParams params; - params.mEffectID = effectInfo.mEffectID; - params.mSkill = effectInfo.mSkill; - params.mAttribute = effectInfo.mAttribute; - params.mDuration = effectInfo.mDuration; - params.mMagnMin = effectInfo.mMagnMin; - params.mMagnMax = effectInfo.mMagnMax; - params.mRange = effectInfo.mRange; - params.mArea = effectInfo.mArea; - result.push_back(params); - } - return result; - } + MWEffectList::~MWEffectList() {} - /* MWSpellEffect */ + SpellEffectList MWEffectList::effectListFromESM(const ESM::EffectList* effects) + { + SpellEffectList result; + for (const ESM::IndexedENAMstruct& effectInfo : effects->mList) + { + SpellEffectParams params; + params.mEffectID = effectInfo.mData.mEffectID; + params.mSkill = ESM::Skill::indexToRefId(effectInfo.mData.mSkill); + params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mData.mAttribute); + params.mDuration = effectInfo.mData.mDuration; + params.mMagnMin = effectInfo.mData.mMagnMin; + params.mMagnMax = effectInfo.mData.mMagnMax; + params.mRange = effectInfo.mData.mRange; + params.mArea = effectInfo.mData.mArea; + result.push_back(params); + } + return result; + } - MWSpellEffect::MWSpellEffect() - : mImageWidget(nullptr) - , mTextWidget(nullptr) - , mRequestedWidth(0) - { - } + /* MWSpellEffect */ - void MWSpellEffect::setSpellEffect(const SpellEffectParams& params) - { - mEffectParams = params; - updateWidgets(); - } + MWSpellEffect::MWSpellEffect() + : mImageWidget(nullptr) + , mTextWidget(nullptr) + , mRequestedWidth(0) + { + } + + void MWSpellEffect::setSpellEffect(const SpellEffectParams& params) + { + mEffectParams = params; + updateWidgets(); + } - void MWSpellEffect::updateWidgets() + void MWSpellEffect::updateWidgets() + { + if (!mEffectParams.mKnown) { - if (!mEffectParams.mKnown) - { - mTextWidget->setCaption ("?"); - mTextWidget->setCoord(sIconOffset / 2, mTextWidget->getCoord().top, mTextWidget->getCoord().width, mTextWidget->getCoord().height); // Compensates for the missing image when effect is not known - mRequestedWidth = mTextWidget->getTextSize().width + sIconOffset; - mImageWidget->setImageTexture (""); - return; - } + mTextWidget->setCaption("?"); + mTextWidget->setCoord(sIconOffset / 2, mTextWidget->getCoord().top, mTextWidget->getCoord().width, + mTextWidget->getCoord().height); // Compensates for the missing image when effect is not known + mRequestedWidth = mTextWidget->getTextSize().width + sIconOffset; + mImageWidget->setImageTexture({}); + return; + } - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - const ESM::MagicEffect *magicEffect = - store.get().search(mEffectParams.mEffectID); + const ESM::MagicEffect* magicEffect = store.get().find(mEffectParams.mEffectID); + const ESM::Attribute* attribute = store.get().search(mEffectParams.mAttribute); + const ESM::Skill* skill = store.get().search(mEffectParams.mSkill); - assert(magicEffect); + auto windowManager = MWBase::Environment::get().getWindowManager(); - std::string pt = MWBase::Environment::get().getWindowManager()->getGameSettingString("spoint", ""); - std::string pts = MWBase::Environment::get().getWindowManager()->getGameSettingString("spoints", ""); - std::string pct = MWBase::Environment::get().getWindowManager()->getGameSettingString("spercent", ""); - std::string ft = MWBase::Environment::get().getWindowManager()->getGameSettingString("sfeet", ""); - std::string lvl = MWBase::Environment::get().getWindowManager()->getGameSettingString("sLevel", ""); - std::string lvls = MWBase::Environment::get().getWindowManager()->getGameSettingString("sLevels", ""); - std::string to = " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sTo", "") + " "; - std::string sec = " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("ssecond", ""); - std::string secs = " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sseconds", ""); + std::string_view pt = windowManager->getGameSettingString("spoint", {}); + std::string_view pts = windowManager->getGameSettingString("spoints", {}); + std::string_view pct = windowManager->getGameSettingString("spercent", {}); + std::string_view ft = windowManager->getGameSettingString("sfeet", {}); + std::string_view lvl = windowManager->getGameSettingString("sLevel", {}); + std::string_view lvls = windowManager->getGameSettingString("sLevels", {}); + std::string to = " " + std::string{ windowManager->getGameSettingString("sTo", {}) } + " "; + std::string sec = " " + std::string{ windowManager->getGameSettingString("ssecond", {}) }; + std::string secs = " " + std::string{ windowManager->getGameSettingString("sseconds", {}) }; - std::string effectIDStr = ESM::MagicEffect::effectIdToString(mEffectParams.mEffectID); - std::string spellLine = MWBase::Environment::get().getWindowManager()->getGameSettingString(effectIDStr, ""); + std::string spellLine = MWMechanics::getMagicEffectString(*magicEffect, attribute, skill); - if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill && mEffectParams.mSkill != -1) - { - spellLine += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Skill::sSkillNameIds[mEffectParams.mSkill], ""); - } - if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute && mEffectParams.mAttribute != -1) + if ((mEffectParams.mMagnMin || mEffectParams.mMagnMax) && !mEffectParams.mNoMagnitude) + { + ESM::MagicEffect::MagnitudeDisplayType displayType = magicEffect->getMagnitudeDisplayType(); + if (displayType == ESM::MagicEffect::MDT_TimesInt) { - spellLine += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Attribute::sGmstAttributeIds[mEffectParams.mAttribute], ""); - } - - if (mEffectParams.mMagnMin || mEffectParams.mMagnMax) { - ESM::MagicEffect::MagnitudeDisplayType displayType = magicEffect->getMagnitudeDisplayType(); - if ( displayType == ESM::MagicEffect::MDT_TimesInt ) { - std::string timesInt = MWBase::Environment::get().getWindowManager()->getGameSettingString("sXTimesINT", ""); - std::stringstream formatter; + std::string_view timesInt = windowManager->getGameSettingString("sXTimesINT", {}); + std::stringstream formatter; - formatter << std::fixed << std::setprecision(1) << " " << (mEffectParams.mMagnMin / 10.0f); - if (mEffectParams.mMagnMin != mEffectParams.mMagnMax) - formatter << to << (mEffectParams.mMagnMax / 10.0f); - formatter << timesInt; + formatter << std::fixed << std::setprecision(1) << " " << (mEffectParams.mMagnMin / 10.0f); + if (mEffectParams.mMagnMin != mEffectParams.mMagnMax) + formatter << to << (mEffectParams.mMagnMax / 10.0f); + formatter << timesInt; - spellLine += formatter.str(); - } - else if ( displayType != ESM::MagicEffect::MDT_None && !mEffectParams.mNoMagnitude) { - spellLine += " " + MyGUI::utility::toString(mEffectParams.mMagnMin); - if (mEffectParams.mMagnMin != mEffectParams.mMagnMax) - spellLine += to + MyGUI::utility::toString(mEffectParams.mMagnMax); - - if ( displayType == ESM::MagicEffect::MDT_Percentage ) - spellLine += pct; - else if ( displayType == ESM::MagicEffect::MDT_Feet ) - spellLine += " " + ft; - else if ( displayType == ESM::MagicEffect::MDT_Level ) - spellLine += " " + ((mEffectParams.mMagnMin == mEffectParams.mMagnMax && std::abs(mEffectParams.mMagnMin) == 1) ? lvl : lvls ); - else // ESM::MagicEffect::MDT_Points - spellLine += " " + ((mEffectParams.mMagnMin == mEffectParams.mMagnMax && std::abs(mEffectParams.mMagnMin) == 1) ? pt : pts ); - } + spellLine += formatter.str(); } - - // constant effects have no duration and no target - if (!mEffectParams.mIsConstant) + else if (displayType != ESM::MagicEffect::MDT_None) { - if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) - mEffectParams.mDuration = std::max(1, mEffectParams.mDuration); + spellLine += " " + MyGUI::utility::toString(mEffectParams.mMagnMin); + if (mEffectParams.mMagnMin != mEffectParams.mMagnMax) + spellLine += to + MyGUI::utility::toString(mEffectParams.mMagnMax); - if (mEffectParams.mDuration > 0 && !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) + if (displayType == ESM::MagicEffect::MDT_Percentage) + spellLine += pct; + else if (displayType == ESM::MagicEffect::MDT_Feet) { - spellLine += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sfor", "") + " " + MyGUI::utility::toString(mEffectParams.mDuration) + ((mEffectParams.mDuration == 1) ? sec : secs); + spellLine += ' '; + spellLine += ft; } - - if (mEffectParams.mArea > 0) + else if (displayType == ESM::MagicEffect::MDT_Level) { - spellLine += " #{sin} " + MyGUI::utility::toString(mEffectParams.mArea) + " #{sfootarea}"; + spellLine += ' '; + if (mEffectParams.mMagnMin == mEffectParams.mMagnMax && std::abs(mEffectParams.mMagnMin) == 1) + spellLine += lvl; + else + spellLine += lvls; } - - // potions have no target - if (!mEffectParams.mNoTarget) + else // ESM::MagicEffect::MDT_Points { - std::string on = MWBase::Environment::get().getWindowManager()->getGameSettingString("sonword", ""); - if (mEffectParams.mRange == ESM::RT_Self) - spellLine += " " + on + " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sRangeSelf", ""); - else if (mEffectParams.mRange == ESM::RT_Touch) - spellLine += " " + on + " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sRangeTouch", ""); - else if (mEffectParams.mRange == ESM::RT_Target) - spellLine += " " + on + " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sRangeTarget", ""); + spellLine += ' '; + if (mEffectParams.mMagnMin == mEffectParams.mMagnMax && std::abs(mEffectParams.mMagnMin) == 1) + spellLine += pt; + else + spellLine += pts; } } - - mTextWidget->setCaptionWithReplacing(spellLine); - mRequestedWidth = mTextWidget->getTextSize().width + sIconOffset; - - mImageWidget->setImageTexture(MWBase::Environment::get().getWindowManager()->correctIconPath(magicEffect->mIcon)); } - MWSpellEffect::~MWSpellEffect() + // constant effects have no duration and no target + if (!mEffectParams.mIsConstant) { - } + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) + mEffectParams.mDuration = std::max(1, mEffectParams.mDuration); - void MWSpellEffect::initialiseOverride() - { - Base::initialiseOverride(); + if (mEffectParams.mDuration > 0 && !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) + { + spellLine += ' '; + spellLine += windowManager->getGameSettingString("sfor", {}); + spellLine += ' ' + MyGUI::utility::toString(mEffectParams.mDuration) + + ((mEffectParams.mDuration == 1) ? sec : secs); + } + + if (mEffectParams.mArea > 0) + { + spellLine += " #{sin} " + MyGUI::utility::toString(mEffectParams.mArea) + " #{sfootarea}"; + } - assignWidget(mTextWidget, "Text"); - assignWidget(mImageWidget, "Image"); + // potions have no target + if (!mEffectParams.mNoTarget) + { + spellLine += ' '; + spellLine += windowManager->getGameSettingString("sonword", {}); + spellLine += ' '; + if (mEffectParams.mRange == ESM::RT_Self) + spellLine += windowManager->getGameSettingString("sRangeSelf", {}); + else if (mEffectParams.mRange == ESM::RT_Touch) + spellLine += windowManager->getGameSettingString("sRangeTouch", {}); + else if (mEffectParams.mRange == ESM::RT_Target) + spellLine += windowManager->getGameSettingString("sRangeTarget", {}); + } } - /* MWDynamicStat */ + mTextWidget->setCaptionWithReplacing(spellLine); + mRequestedWidth = mTextWidget->getTextSize().width + sIconOffset; + + mImageWidget->setImageTexture(Misc::ResourceHelpers::correctIconPath( + magicEffect->mIcon, MWBase::Environment::get().getResourceSystem()->getVFS())); + } + + MWSpellEffect::~MWSpellEffect() {} + + void MWSpellEffect::initialiseOverride() + { + Base::initialiseOverride(); - MWDynamicStat::MWDynamicStat() + assignWidget(mTextWidget, "Text"); + assignWidget(mImageWidget, "Image"); + } + + /* MWDynamicStat */ + + MWDynamicStat::MWDynamicStat() : mValue(0) , mMax(1) , mTextWidget(nullptr) , mBarWidget(nullptr) , mBarTextWidget(nullptr) - { - } - - void MWDynamicStat::setValue(int cur, int max) - { - mValue = cur; - mMax = max; + { + } - if (mBarWidget) - { - mBarWidget->setProgressRange(std::max(0, mMax)); - mBarWidget->setProgressPosition(std::max(0, mValue)); - } + void MWDynamicStat::setValue(int cur, int max) + { + mValue = cur; + mMax = max; - if (mBarTextWidget) - { - std::stringstream out; - out << mValue << "/" << mMax; - mBarTextWidget->setCaption(out.str().c_str()); - } - } - void MWDynamicStat::setTitle(const std::string& text) + if (mBarWidget) { - if (mTextWidget) - mTextWidget->setCaption(text); + mBarWidget->setProgressRange(std::max(0, mMax)); + mBarWidget->setProgressPosition(std::max(0, mValue)); } - MWDynamicStat::~MWDynamicStat() + if (mBarTextWidget) { + std::stringstream out; + out << mValue << "/" << mMax; + mBarTextWidget->setCaption(out.str()); } + } + void MWDynamicStat::setTitle(std::string_view text) + { + if (mTextWidget) + mTextWidget->setCaption(MyGUI::UString(text)); + } - void MWDynamicStat::initialiseOverride() - { - Base::initialiseOverride(); + MWDynamicStat::~MWDynamicStat() {} - assignWidget(mTextWidget, "Text"); - assignWidget(mBarWidget, "Bar"); - assignWidget(mBarTextWidget, "BarText"); - } + void MWDynamicStat::initialiseOverride() + { + Base::initialiseOverride(); + + assignWidget(mTextWidget, "Text"); + assignWidget(mBarWidget, "Bar"); + assignWidget(mBarTextWidget, "BarText"); } } diff --git a/apps/openmw/mwgui/widgets.hpp b/apps/openmw/mwgui/widgets.hpp index 3c55287159d..d562e4e07f7 100644 --- a/apps/openmw/mwgui/widgets.hpp +++ b/apps/openmw/mwgui/widgets.hpp @@ -3,12 +3,14 @@ #include "../mwmechanics/stat.hpp" -#include -#include +#include +#include +#include -#include -#include -#include +#include +#include +#include +#include namespace MyGUI { @@ -31,8 +33,6 @@ namespace MWGui { class MWEffectList; - void fixTexturePath(std::string &path); - struct SpellEffectParams { SpellEffectParams() @@ -41,8 +41,6 @@ namespace MWGui , mNoMagnitude(false) , mKnown(true) , mEffectID(-1) - , mSkill(-1) - , mAttribute(-1) , mMagnMin(-1) , mMagnMax(-1) , mRange(-1) @@ -60,8 +58,7 @@ namespace MWGui // value of -1 here means the effect is unknown to the player short mEffectID; - // value of -1 here means there is no skill/attribute - signed char mSkill, mAttribute; + ESM::RefId mSkill, mAttribute; // value of -1 here means the value is unavailable int mMagnMin, mMagnMax, mRange, mDuration; @@ -71,20 +68,21 @@ namespace MWGui bool operator==(const SpellEffectParams& other) const { - if (mEffectID != other.mEffectID) + if (mEffectID != other.mEffectID) return false; bool involvesAttribute = (mEffectID == 74 // restore attribute - || mEffectID == 85 // absorb attribute - || mEffectID == 17 // drain attribute - || mEffectID == 79 // fortify attribute - || mEffectID == 22); // damage attribute + || mEffectID == 85 // absorb attribute + || mEffectID == 17 // drain attribute + || mEffectID == 79 // fortify attribute + || mEffectID == 22); // damage attribute bool involvesSkill = (mEffectID == 78 // restore skill - || mEffectID == 89 // absorb skill - || mEffectID == 21 // drain skill - || mEffectID == 83 // fortify skill - || mEffectID == 26); // damage skill - return ((other.mSkill == mSkill) || !involvesSkill) && ((other.mAttribute == mAttribute) && !involvesAttribute) && (other.mArea == mArea); + || mEffectID == 89 // absorb skill + || mEffectID == 21 // drain skill + || mEffectID == 83 // fortify skill + || mEffectID == 26); // damage skill + return ((other.mSkill == mSkill) || !involvesSkill) + && ((other.mAttribute == mAttribute) && !involvesAttribute) && (other.mArea == mArea); } }; @@ -92,21 +90,20 @@ namespace MWGui class MWSkill final : public MyGUI::Widget { - MYGUI_RTTI_DERIVED( MWSkill ) + MYGUI_RTTI_DERIVED(MWSkill) public: MWSkill(); typedef MWMechanics::Stat SkillValue; - void setSkillId(ESM::Skill::SkillEnum skillId); - void setSkillNumber(int skillId); + void setSkillId(ESM::RefId skillId); void setSkillValue(const SkillValue& value); - ESM::Skill::SkillEnum getSkillId() const { return mSkillId; } + ESM::RefId getSkillId() const { return mSkillId; } const SkillValue& getSkillValue() const { return mValue; } // Events - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_SkillVoid; + typedef MyGUI::delegates::MultiDelegate EventHandle_SkillVoid; /** Event : Skill clicked.\n signature : void method(MWSkill* _sender)\n @@ -121,10 +118,9 @@ namespace MWGui void onClicked(MyGUI::Widget* _sender); private: - void updateWidgets(); - ESM::Skill::SkillEnum mSkillId; + ESM::RefId mSkillId; SkillValue mValue; MyGUI::TextBox* mSkillNameWidget; MyGUI::TextBox* mSkillValueWidget; @@ -133,20 +129,20 @@ namespace MWGui class MWAttribute final : public MyGUI::Widget { - MYGUI_RTTI_DERIVED( MWAttribute ) + MYGUI_RTTI_DERIVED(MWAttribute) public: MWAttribute(); typedef MWMechanics::AttributeValue AttributeValue; - void setAttributeId(int attributeId); + void setAttributeId(ESM::RefId attributeId); void setAttributeValue(const AttributeValue& value); - int getAttributeId() const { return mId; } + ESM::RefId getAttributeId() const { return mId; } const AttributeValue& getAttributeValue() const { return mValue; } // Events - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_AttributeVoid; + typedef MyGUI::delegates::MultiDelegate EventHandle_AttributeVoid; /** Event : Attribute clicked.\n signature : void method(MWAttribute* _sender)\n @@ -154,17 +150,16 @@ namespace MWGui EventHandle_AttributeVoid eventClicked; protected: - virtual ~MWAttribute(); + ~MWAttribute() override = default; void initialiseOverride() override; void onClicked(MyGUI::Widget* _sender); private: - void updateWidgets(); - int mId; + ESM::RefId mId; AttributeValue mValue; MyGUI::TextBox* mAttributeNameWidget; MyGUI::TextBox* mAttributeValueWidget; @@ -177,24 +172,24 @@ namespace MWGui class MWSpellEffect; class MWSpell final : public MyGUI::Widget { - MYGUI_RTTI_DERIVED( MWSpell ) + MYGUI_RTTI_DERIVED(MWSpell) public: MWSpell(); - typedef MWMechanics::Stat SpellValue; - - void setSpellId(const std::string &id); + void setSpellId(const ESM::RefId& id); /** * @param vector to store the created effect widgets * @param parent widget * @param coordinates to use, will be expanded if more space is needed - * @param spell category, if this is 0, this means the spell effects are permanent and won't display e.g. duration + * @param spell category, if this is 0, this means the spell effects are permanent and won't display e.g. + * duration * @param various flags, see MWEffectList::EffectFlags */ - void createEffectWidgets(std::vector &effects, MyGUI::Widget* creator, MyGUI::IntCoord &coord, int flags); + void createEffectWidgets( + std::vector& effects, MyGUI::Widget* creator, MyGUI::IntCoord& coord, int flags); - const std::string &getSpellId() const { return mId; } + const ESM::RefId& getSpellId() const { return mId; } protected: virtual ~MWSpell(); @@ -204,19 +199,17 @@ namespace MWGui private: void updateWidgets(); - std::string mId; + ESM::RefId mId; MyGUI::TextBox* mSpellNameWidget; }; typedef MWSpell* MWSpellPtr; class MWEffectList final : public MyGUI::Widget { - MYGUI_RTTI_DERIVED( MWEffectList ) + MYGUI_RTTI_DERIVED(MWEffectList) public: MWEffectList(); - typedef MWMechanics::Stat EnchantmentValue; - enum EffectFlags { EF_NoTarget = 0x01, // potions have no target (target is always the player) @@ -236,7 +229,8 @@ namespace MWGui * @param center the effect widgets horizontally * @param various flags, see MWEffectList::EffectFlags */ - void createEffectWidgets(std::vector &effects, MyGUI::Widget* creator, MyGUI::IntCoord &coord, bool center, int flags); + void createEffectWidgets(std::vector& effects, MyGUI::Widget* creator, + MyGUI::IntCoord& coord, bool center, int flags); protected: virtual ~MWEffectList(); @@ -252,7 +246,7 @@ namespace MWGui class MWSpellEffect final : public MyGUI::Widget { - MYGUI_RTTI_DERIVED( MWSpellEffect ) + MYGUI_RTTI_DERIVED(MWSpellEffect) public: MWSpellEffect(); @@ -269,7 +263,7 @@ namespace MWGui private: static constexpr int sIconOffset = 24; - + void updateWidgets(); SpellEffectParams mEffectParams; @@ -281,12 +275,12 @@ namespace MWGui class MWDynamicStat final : public MyGUI::Widget { - MYGUI_RTTI_DERIVED( MWDynamicStat ) + MYGUI_RTTI_DERIVED(MWDynamicStat) public: MWDynamicStat(); void setValue(int value, int max); - void setTitle(const std::string& text); + void setTitle(std::string_view text); int getValue() const { return mValue; } int getMax() const { return mMax; } @@ -297,7 +291,6 @@ namespace MWGui void initialiseOverride() override; private: - int mValue, mMax; MyGUI::TextBox* mTextWidget; MyGUI::ProgressBar* mBarWidget; diff --git a/apps/openmw/mwgui/windowbase.cpp b/apps/openmw/mwgui/windowbase.cpp index 84e557fcdcc..f5d90590f81 100644 --- a/apps/openmw/mwgui/windowbase.cpp +++ b/apps/openmw/mwgui/windowbase.cpp @@ -4,8 +4,8 @@ #include #include -#include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include @@ -14,8 +14,8 @@ using namespace MWGui; -WindowBase::WindowBase(const std::string& parLayout) - : Layout(parLayout) +WindowBase::WindowBase(std::string_view parLayout) + : Layout(parLayout) { mMainWidget->setVisible(false); @@ -41,13 +41,14 @@ void WindowBase::onTitleDoubleClicked() MWBase::Environment::get().getWindowManager()->toggleMaximized(this); } -void WindowBase::onDoubleClick(MyGUI::Widget *_sender) +void WindowBase::onDoubleClick(MyGUI::Widget* _sender) { onTitleDoubleClicked(); } void WindowBase::setVisible(bool visible) { + visible = visible && !mDisabledByLua; bool wasVisible = mMainWidget->getVisible(); mMainWidget->setVisible(visible); @@ -57,7 +58,7 @@ void WindowBase::setVisible(bool visible) onClose(); } -bool WindowBase::isVisible() +bool WindowBase::isVisible() const { return mMainWidget->getVisible(); } @@ -71,11 +72,39 @@ void WindowBase::center() layerSize = mMainWidget->getLayer()->getSize(); MyGUI::IntCoord coord = mMainWidget->getCoord(); - coord.left = (layerSize.width - coord.width)/2; - coord.top = (layerSize.height - coord.height)/2; + coord.left = (layerSize.width - coord.width) / 2; + coord.top = (layerSize.height - coord.height) / 2; mMainWidget->setCoord(coord); } +void WindowBase::clampWindowCoordinates(MyGUI::Window* window) +{ + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + if (window->getLayer()) + viewSize = window->getLayer()->getSize(); + + // Window's minimum size is larger than the screen size, can not clamp coordinates + auto minSize = window->getMinSize(); + if (minSize.width > viewSize.width || minSize.height > viewSize.height) + return; + + int left = std::max(0, window->getPosition().left); + int top = std::max(0, window->getPosition().top); + int width = std::clamp(window->getSize().width, 0, viewSize.width); + int height = std::clamp(window->getSize().height, 0, viewSize.height); + if (left + width > viewSize.width) + left = viewSize.width - width; + + if (top + height > viewSize.height) + top = viewSize.height - height; + + if (window->getSize().width != width || window->getSize().height != height) + window->setSize(width, height); + + if (window->getPosition().left != left || window->getPosition().top != top) + window->setPosition(left, top); +} + WindowModal::WindowModal(const std::string& parLayout) : WindowBase(parLayout) { @@ -83,10 +112,10 @@ WindowModal::WindowModal(const std::string& parLayout) void WindowModal::onOpen() { - MWBase::Environment::get().getWindowManager()->addCurrentModal(this); //Set so we can escape it if needed + MWBase::Environment::get().getWindowManager()->addCurrentModal(this); // Set so we can escape it if needed MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); - MyGUI::InputManager::getInstance ().addWidgetModal (mMainWidget); + MyGUI::InputManager::getInstance().addWidgetModal(mMainWidget); MyGUI::InputManager::getInstance().setKeyFocusWidget(focus); } @@ -94,11 +123,13 @@ void WindowModal::onClose() { MWBase::Environment::get().getWindowManager()->removeCurrentModal(this); - MyGUI::InputManager::getInstance ().removeWidgetModal (mMainWidget); + MyGUI::InputManager::getInstance().removeWidgetModal(mMainWidget); } -NoDrop::NoDrop(DragAndDrop *drag, MyGUI::Widget *widget) - : mWidget(widget), mDrag(drag), mTransparent(false) +NoDrop::NoDrop(DragAndDrop* drag, MyGUI::Widget* widget) + : mWidget(widget) + , mDrag(drag) + , mTransparent(false) { } @@ -124,12 +155,12 @@ void NoDrop::onFrame(float dt) if (mTransparent) { mWidget->setNeedMouseFocus(false); // Allow click-through - setAlpha(std::max(0.13f, mWidget->getAlpha() - dt*5)); + setAlpha(std::max(0.13f, mWidget->getAlpha() - dt * 5)); } else { mWidget->setNeedMouseFocus(true); - setAlpha(std::min(1.0f, mWidget->getAlpha() + dt*5)); + setAlpha(std::min(1.0f, mWidget->getAlpha() + dt * 5)); } } @@ -139,15 +170,15 @@ void NoDrop::setAlpha(float alpha) mWidget->setAlpha(alpha); } -BookWindowBase::BookWindowBase(const std::string& parLayout) - : WindowBase(parLayout) +BookWindowBase::BookWindowBase(std::string_view parLayout) + : WindowBase(parLayout) { } -float BookWindowBase::adjustButton (char const * name) +float BookWindowBase::adjustButton(std::string_view name) { Gui::ImageButton* button; - WindowBase::getWidget (button, name); + WindowBase::getWidget(button, name); MyGUI::IntSize requested = button->getRequestedSize(); float scale = float(requested.height) / button->getSize().height; MyGUI::IntSize newSize = requested; @@ -160,7 +191,7 @@ float BookWindowBase::adjustButton (char const * name) MyGUI::IntSize diff = (button->getSize() - requested); diff.width /= scale; diff.height /= scale; - button->setPosition(button->getPosition() + MyGUI::IntPoint(diff.width,0)); + button->setPosition(button->getPosition() + MyGUI::IntPoint(diff.width, 0)); } return scale; diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 90ef2118de0..466060c6ad9 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -12,15 +12,15 @@ namespace MWGui { class DragAndDrop; - class WindowBase: public Layout + class WindowBase : public Layout { public: - WindowBase(const std::string& parLayout); + WindowBase(std::string_view parLayout); virtual MyGUI::Widget* getDefaultKeyFocus() { return nullptr; } // Events - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_WindowBase; + typedef MyGUI::delegates::MultiDelegate EventHandle_WindowBase; /// Open this object in the GUI, for windows that support it virtual void setPtr(const MWWorld::Ptr& ptr) {} @@ -31,13 +31,13 @@ namespace MWGui /// Notify that window has been made visible virtual void onOpen() {} /// Notify that window has been hidden - virtual void onClose () {} + virtual void onClose() {} /// Gracefully exits the window - virtual bool exit() {return true;} + virtual bool exit() { return true; } /// Sets the visibility of the window void setVisible(bool visible) override; /// Returns the visibility state of the window - bool isVisible(); + bool isVisible() const; void center(); @@ -47,11 +47,20 @@ namespace MWGui /// Called when GUI viewport changes size virtual void onResChange(int width, int height) {} + virtual void onDeleteCustomData(const MWWorld::Ptr& ptr) {} + + virtual std::string_view getWindowIdForLua() const { return ""; } + void setDisabledByLua(bool disabled) { mDisabledByLua = disabled; } + + static void clampWindowCoordinates(MyGUI::Window* window); + protected: virtual void onTitleDoubleClicked(); private: void onDoubleClick(MyGUI::Widget* _sender); + + bool mDisabledByLua = false; }; /* @@ -63,7 +72,7 @@ namespace MWGui WindowModal(const std::string& parLayout); void onOpen() override; void onClose() override; - bool exit() override {return true;} + bool exit() override { return true; } }; /// A window that cannot be the target of a drag&drop action. @@ -86,10 +95,10 @@ namespace MWGui class BookWindowBase : public WindowBase { public: - BookWindowBase(const std::string& parLayout); + BookWindowBase(std::string_view parLayout); protected: - float adjustButton (char const * name); + float adjustButton(std::string_view name); }; } diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index f45990f2151..1816cf8a614 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -3,205 +3,221 @@ #include #include #include +#include #include #include -#include -#include +#include #include +#include #include #include -#include -#include -#include -#include +#include // For BT_NO_PROFILE #include -#include #include +#include #include -#include -#include - -#include -#include +#include +#include #include -#include #include +#include +#include #include #include +#include #include #include -#include #include #include #include +#include -#include #include +#include + +#include +#include + +#include + +#include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" -#include "../mwbase/statemanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/statemanager.hpp" #include "../mwbase/world.hpp" #include "../mwrender/vismask.hpp" -#include "../mwworld/class.hpp" -#include "../mwworld/player.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/globals.hpp" +#include "../mwworld/player.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/npcstats.hpp" -#include "../mwrender/localmap.hpp" +#include "../mwrender/postprocessor.hpp" -#include "console.hpp" -#include "journalwindow.hpp" -#include "journalviewmodel.hpp" -#include "charactercreation.hpp" -#include "dialogue.hpp" -#include "statswindow.hpp" -#include "messagebox.hpp" -#include "tooltips.hpp" -#include "scrollwindow.hpp" +#include "alchemywindow.hpp" +#include "backgroundimage.hpp" +#include "bookpage.hpp" #include "bookwindow.hpp" -#include "hud.hpp" -#include "mainmenu.hpp" -#include "countdialog.hpp" -#include "tradewindow.hpp" -#include "spellbuyingwindow.hpp" -#include "travelwindow.hpp" -#include "settingswindow.hpp" +#include "companionwindow.hpp" #include "confirmationdialog.hpp" -#include "alchemywindow.hpp" -#include "spellwindow.hpp" -#include "quickkeysmenu.hpp" -#include "loadingscreen.hpp" -#include "levelupdialog.hpp" -#include "waitdialog.hpp" +#include "console.hpp" +#include "container.hpp" +#include "controllers.hpp" +#include "countdialog.hpp" +#include "cursor.hpp" +#include "debugwindow.hpp" +#include "dialogue.hpp" #include "enchantingdialog.hpp" -#include "trainingwindow.hpp" -#include "recharge.hpp" #include "exposedwindow.hpp" -#include "cursor.hpp" -#include "merchantrepair.hpp" -#include "repair.hpp" -#include "soulgemdialog.hpp" -#include "companionwindow.hpp" +#include "hud.hpp" #include "inventorywindow.hpp" -#include "bookpage.hpp" +#include "itemchargeview.hpp" #include "itemview.hpp" -#include "videowidget.hpp" -#include "backgroundimage.hpp" #include "itemwidget.hpp" -#include "screenfader.hpp" -#include "debugwindow.hpp" -#include "spellview.hpp" -#include "draganddrop.hpp" -#include "container.hpp" -#include "controllers.hpp" #include "jailscreen.hpp" -#include "itemchargeview.hpp" +#include "journalviewmodel.hpp" +#include "journalwindow.hpp" #include "keyboardnavigation.hpp" +#include "levelupdialog.hpp" +#include "loadingscreen.hpp" +#include "mainmenu.hpp" +#include "merchantrepair.hpp" +#include "postprocessorhud.hpp" +#include "quickkeysmenu.hpp" +#include "recharge.hpp" +#include "repair.hpp" #include "resourceskin.hpp" +#include "screenfader.hpp" +#include "scrollwindow.hpp" +#include "settingswindow.hpp" +#include "spellbuyingwindow.hpp" +#include "spellview.hpp" +#include "spellwindow.hpp" +#include "statswindow.hpp" +#include "tradewindow.hpp" +#include "trainingwindow.hpp" +#include "travelwindow.hpp" +#include "videowidget.hpp" +#include "waitdialog.hpp" namespace MWGui { - WindowManager::WindowManager( - SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, - const std::string& logpath, const std::string& resourcePath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, - ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& userDataPath) - : mOldUpdateMask(0) - , mOldCullMask(0) - , mStore(nullptr) - , mResourceSystem(resourceSystem) - , mWorkQueue(workQueue) - , mViewer(viewer) - , mConsoleOnlyScripts(consoleOnlyScripts) - , mCurrentModals() - , mHud(nullptr) - , mMap(nullptr) - , mLocalMapRender(nullptr) - , mToolTips(nullptr) - , mStatsWindow(nullptr) - , mMessageBoxManager(nullptr) - , mConsole(nullptr) - , mDialogueWindow(nullptr) - , mDragAndDrop(nullptr) - , mInventoryWindow(nullptr) - , mScrollWindow(nullptr) - , mBookWindow(nullptr) - , mCountDialog(nullptr) - , mTradeWindow(nullptr) - , mSettingsWindow(nullptr) - , mConfirmationDialog(nullptr) - , mSpellWindow(nullptr) - , mQuickKeysMenu(nullptr) - , mLoadingScreen(nullptr) - , mWaitDialog(nullptr) - , mSoulgemDialog(nullptr) - , mVideoBackground(nullptr) - , mVideoWidget(nullptr) - , mWerewolfFader(nullptr) - , mBlindnessFader(nullptr) - , mHitFader(nullptr) - , mScreenFader(nullptr) - , mDebugWindow(nullptr) - , mJailScreen(nullptr) - , mTranslationDataStorage (translationDataStorage) - , mCharGen(nullptr) - , mInputBlocker(nullptr) - , mCrosshairEnabled(Settings::Manager::getBool ("crosshair", "HUD")) - , mSubtitlesEnabled(Settings::Manager::getBool ("subtitles", "GUI")) - , mHitFaderEnabled(Settings::Manager::getBool ("hit fader", "GUI")) - , mWerewolfOverlayEnabled(Settings::Manager::getBool ("werewolf overlay", "GUI")) - , mHudEnabled(true) - , mCursorVisible(true) - , mCursorActive(true) - , mPlayerBounty(-1) - , mGui(nullptr) - , mGuiModes() - , mCursorManager(nullptr) - , mGarbageDialogs() - , mShown(GW_ALL) - , mForceHidden(GW_None) - , mAllowed(GW_ALL) - , mRestAllowed(true) - , mShowOwned(0) - , mEncoding(encoding) - , mVersionDescription(versionDescription) - , mWindowVisible(true) - { - mScalingFactor = std::clamp(Settings::Manager::getFloat("scaling factor", "GUI"), 0.5f, 8.f); - mGuiPlatform = new osgMyGUI::Platform(viewer, guiRoot, resourceSystem->getImageManager(), mScalingFactor); - mGuiPlatform->initialise(resourcePath, (boost::filesystem::path(logpath) / "MyGUI.log").generic_string()); - - mGui = new MyGUI::Gui; - mGui->initialise(""); + namespace + { + Settings::SettingValue* findHiddenSetting(GuiWindow window) + { + switch (window) + { + case GW_Inventory: + return &Settings::windows().mInventoryHidden; + case GW_Map: + return &Settings::windows().mMapHidden; + case GW_Magic: + return &Settings::windows().mSpellsHidden; + case GW_Stats: + return &Settings::windows().mStatsHidden; + default: + return nullptr; + } + } + } + + WindowManager::WindowManager(SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, + Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::filesystem::path& logpath, + bool consoleOnlyScripts, Translation::Storage& translationDataStorage, ToUTF8::FromType encoding, + const std::string& versionDescription, bool useShaders, Files::ConfigurationManager& cfgMgr) + : mOldUpdateMask(0) + , mOldCullMask(0) + , mStore(nullptr) + , mResourceSystem(resourceSystem) + , mWorkQueue(workQueue) + , mViewer(viewer) + , mConsoleOnlyScripts(consoleOnlyScripts) + , mCurrentModals() + , mHud(nullptr) + , mMap(nullptr) + , mStatsWindow(nullptr) + , mConsole(nullptr) + , mDialogueWindow(nullptr) + , mInventoryWindow(nullptr) + , mScrollWindow(nullptr) + , mBookWindow(nullptr) + , mCountDialog(nullptr) + , mTradeWindow(nullptr) + , mSettingsWindow(nullptr) + , mConfirmationDialog(nullptr) + , mSpellWindow(nullptr) + , mQuickKeysMenu(nullptr) + , mLoadingScreen(nullptr) + , mWaitDialog(nullptr) + , mVideoBackground(nullptr) + , mVideoWidget(nullptr) + , mWerewolfFader(nullptr) + , mBlindnessFader(nullptr) + , mHitFader(nullptr) + , mScreenFader(nullptr) + , mDebugWindow(nullptr) + , mPostProcessorHud(nullptr) + , mJailScreen(nullptr) + , mContainerWindow(nullptr) + , mTranslationDataStorage(translationDataStorage) + , mInputBlocker(nullptr) + , mHudEnabled(true) + , mCursorVisible(true) + , mCursorActive(true) + , mPlayerBounty(-1) + , mGuiModes() + , mGarbageDialogs() + , mShown(GW_ALL) + , mForceHidden(GW_None) + , mAllowed(GW_ALL) + , mRestAllowed(true) + , mEncoding(encoding) + , mVersionDescription(versionDescription) + , mWindowVisible(true) + , mCfgMgr(cfgMgr) + { + int w, h; + SDL_GetWindowSize(window, &w, &h); + int dw, dh; + SDL_GL_GetDrawableSize(window, &dw, &dh); + + mScalingFactor = Settings::gui().mScalingFactor * (dw / w); + mGuiPlatform = std::make_unique(viewer, guiRoot, resourceSystem->getImageManager(), + resourceSystem->getVFS(), mScalingFactor, "mygui", logpath / "MyGUI.log"); + + mGui = std::make_unique(); + mGui->initialise({}); createTextures(); MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag); // Load fonts - mFontLoader.reset(new Gui::FontLoader(encoding, resourceSystem->getVFS(), userDataPath, mScalingFactor)); - mFontLoader->loadBitmapFonts(exportFonts); + mFontLoader = std::make_unique(encoding, resourceSystem->getVFS(), mScalingFactor); - //Register own widgets with MyGUI + // Register own widgets with MyGUI MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); @@ -213,34 +229,40 @@ namespace MWGui MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Layer"); MyGUI::FactoryManager::getInstance().registerFactory("Layer"); - BookPage::registerMyGUIComponents (); + BookPage::registerMyGUIComponents(); + PostProcessorHud::registerMyGUIComponents(); ItemView::registerComponents(); ItemChargeView::registerComponents(); ItemWidget::registerComponents(); SpellView::registerComponents(); Gui::registerAllWidgets(); + LuaUi::registerAllWidgets(); MyGUI::FactoryManager::getInstance().registerFactory("Controller"); - MyGUI::FactoryManager::getInstance().registerFactory("Resource", "ResourceImageSetPointer"); - MyGUI::FactoryManager::getInstance().registerFactory("Resource", "AutoSizedResourceSkin"); + MyGUI::FactoryManager::getInstance().registerFactory( + "Resource", "ResourceImageSetPointer"); + MyGUI::FactoryManager::getInstance().registerFactory( + "Resource", "AutoSizedResourceSkin"); MyGUI::ResourceManager::getInstance().load("core.xml"); - WindowManager::loadUserFonts(); - bool keyboardNav = Settings::Manager::getBool("keyboard navigation", "GUI"); - mKeyboardNavigation.reset(new KeyboardNavigation()); + const bool keyboardNav = Settings::gui().mKeyboardNavigation; + mKeyboardNavigation = std::make_unique(); mKeyboardNavigation->setEnabled(keyboardNav); Gui::ImageButton::setDefaultNeedKeyFocus(keyboardNav); - mLoadingScreen = new LoadingScreen(mResourceSystem, mViewer); - mWindows.push_back(mLoadingScreen); + auto loadingScreen = std::make_unique(mResourceSystem, mViewer); + mLoadingScreen = loadingScreen.get(); + mWindows.push_back(std::move(loadingScreen)); - //set up the hardware cursor manager - mCursorManager = new SDLUtil::SDLCursorManager(); + // set up the hardware cursor manager + mCursorManager = std::make_unique(); - MyGUI::PointerManager::getInstance().eventChangeMousePointer += MyGUI::newDelegate(this, &WindowManager::onCursorChange); + MyGUI::PointerManager::getInstance().eventChangeMousePointer + += MyGUI::newDelegate(this, &WindowManager::onCursorChange); - MyGUI::InputManager::getInstance().eventChangeKeyFocus += MyGUI::newDelegate(this, &WindowManager::onKeyFocusChanged); + MyGUI::InputManager::getInstance().eventChangeKeyFocus + += MyGUI::newDelegate(this, &WindowManager::onKeyFocusChanged); // Create all cursors in advance createCursors(); @@ -250,14 +272,14 @@ namespace MWGui // hide mygui's pointer MyGUI::PointerManager::getInstance().setVisible(false); - mVideoBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, - MyGUI::Align::Default, "InputBlocker"); + mVideoBackground = MyGUI::Gui::getInstance().createWidgetReal( + "ImageBox", 0, 0, 1, 1, MyGUI::Align::Default, "Video"); mVideoBackground->setImageTexture("black"); mVideoBackground->setVisible(false); mVideoBackground->setNeedMouseFocus(true); mVideoBackground->setNeedKeyFocus(true); - mVideoWidget = mVideoBackground->createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Default); + mVideoWidget = mVideoBackground->createWidgetReal("ImageBox", 0, 0, 1, 1, MyGUI::Align::Default); mVideoWidget->setNeedMouseFocus(true); mVideoWidget->setNeedKeyFocus(true); mVideoWidget->setVFS(resourceSystem->getVFS()); @@ -266,21 +288,18 @@ namespace MWGui MyGUI::ClipboardManager::getInstance().eventClipboardChanged.clear(); MyGUI::ClipboardManager::getInstance().eventClipboardRequested.clear(); - MyGUI::ClipboardManager::getInstance().eventClipboardChanged += MyGUI::newDelegate(this, &WindowManager::onClipboardChanged); - MyGUI::ClipboardManager::getInstance().eventClipboardRequested += MyGUI::newDelegate(this, &WindowManager::onClipboardRequested); + MyGUI::ClipboardManager::getInstance().eventClipboardChanged + += MyGUI::newDelegate(this, &WindowManager::onClipboardChanged); + MyGUI::ClipboardManager::getInstance().eventClipboardRequested + += MyGUI::newDelegate(this, &WindowManager::onClipboardRequested); - mShowOwned = Settings::Manager::getInt("show owned", "Game"); + mVideoWrapper = std::make_unique(window, viewer); + mVideoWrapper->setGammaContrast(Settings::video().mGamma, Settings::video().mContrast); - mVideoWrapper = new SDLUtil::VideoWrapper(window, viewer); - mVideoWrapper->setGammaContrast(Settings::Manager::getFloat("gamma", "Video"), - Settings::Manager::getFloat("contrast", "Video")); + if (useShaders) + mGuiPlatform->getRenderManagerPtr()->enableShaders(mResourceSystem->getSceneManager()->getShaderManager()); - mStatsWatcher.reset(new StatsWatcher()); - } - - void WindowManager::loadUserFonts() - { - mFontLoader->loadTrueTypeFonts(); + mStatsWatcher = std::make_unique(); } void WindowManager::initUI() @@ -291,180 +310,206 @@ namespace MWGui mTextColours.loadColours(); - mDragAndDrop = new DragAndDrop(); + mDragAndDrop = std::make_unique(); - Recharge* recharge = new Recharge(); - mGuiModeStates[GM_Recharge] = GuiModeState(recharge); - mWindows.push_back(recharge); + auto recharge = std::make_unique(); + mGuiModeStates[GM_Recharge] = GuiModeState(recharge.get()); + mWindows.push_back(std::move(recharge)); - MainMenu* menu = new MainMenu(w, h, mResourceSystem->getVFS(), mVersionDescription); - mGuiModeStates[GM_MainMenu] = GuiModeState(menu); - mWindows.push_back(menu); + auto menu = std::make_unique(w, h, mResourceSystem->getVFS(), mVersionDescription); + mGuiModeStates[GM_MainMenu] = GuiModeState(menu.get()); + mWindows.push_back(std::move(menu)); - mLocalMapRender = new MWRender::LocalMap(mViewer->getSceneData()->asGroup()); - mMap = new MapWindow(mCustomMarkers, mDragAndDrop, mLocalMapRender, mWorkQueue); - mWindows.push_back(mMap); + mLocalMapRender = std::make_unique(mViewer->getSceneData()->asGroup()); + auto map = std::make_unique(mCustomMarkers, mDragAndDrop.get(), mLocalMapRender.get(), mWorkQueue); + mMap = map.get(); + mWindows.push_back(std::move(map)); mMap->renderGlobalMap(); - trackWindow(mMap, "map"); - - mStatsWindow = new StatsWindow(mDragAndDrop); - mWindows.push_back(mStatsWindow); - trackWindow(mStatsWindow, "stats"); - - mInventoryWindow = new InventoryWindow(mDragAndDrop, mViewer->getSceneData()->asGroup(), mResourceSystem); - mWindows.push_back(mInventoryWindow); - - mSpellWindow = new SpellWindow(mDragAndDrop); - mWindows.push_back(mSpellWindow); - trackWindow(mSpellWindow, "spells"); - - mGuiModeStates[GM_Inventory] = GuiModeState({mMap, mInventoryWindow, mSpellWindow, mStatsWindow}); - mGuiModeStates[GM_None] = GuiModeState({mMap, mInventoryWindow, mSpellWindow, mStatsWindow}); - - mTradeWindow = new TradeWindow(); - mWindows.push_back(mTradeWindow); - trackWindow(mTradeWindow, "barter"); - mGuiModeStates[GM_Barter] = GuiModeState({mInventoryWindow, mTradeWindow}); - - mConsole = new Console(w,h, mConsoleOnlyScripts); - mWindows.push_back(mConsole); - trackWindow(mConsole, "console"); - - bool questList = mResourceSystem->getVFS()->exists("textures/tx_menubook_options_over.dds"); - JournalWindow* journal = JournalWindow::create(JournalViewModel::create (), questList, mEncoding); - mWindows.push_back(journal); - mGuiModeStates[GM_Journal] = GuiModeState(journal); - mGuiModeStates[GM_Journal].mCloseSound = "book close"; - mGuiModeStates[GM_Journal].mOpenSound = "book open"; - - mMessageBoxManager = new MessageBoxManager(mStore->get().find("fMessageTimePerChar")->mValue.getFloat()); - - SpellBuyingWindow* spellBuyingWindow = new SpellBuyingWindow(); - mWindows.push_back(spellBuyingWindow); - mGuiModeStates[GM_SpellBuying] = GuiModeState(spellBuyingWindow); - - TravelWindow* travelWindow = new TravelWindow(); - mWindows.push_back(travelWindow); - mGuiModeStates[GM_Travel] = GuiModeState(travelWindow); - - mDialogueWindow = new DialogueWindow(); - mWindows.push_back(mDialogueWindow); - trackWindow(mDialogueWindow, "dialogue"); + trackWindow(mMap, makeMapWindowSettingValues()); + + auto statsWindow = std::make_unique(mDragAndDrop.get()); + mStatsWindow = statsWindow.get(); + mWindows.push_back(std::move(statsWindow)); + trackWindow(mStatsWindow, makeStatsWindowSettingValues()); + + auto inventoryWindow = std::make_unique( + mDragAndDrop.get(), mViewer->getSceneData()->asGroup(), mResourceSystem); + mInventoryWindow = inventoryWindow.get(); + mWindows.push_back(std::move(inventoryWindow)); + + auto spellWindow = std::make_unique(mDragAndDrop.get()); + mSpellWindow = spellWindow.get(); + mWindows.push_back(std::move(spellWindow)); + trackWindow(mSpellWindow, makeSpellsWindowSettingValues()); + + mGuiModeStates[GM_Inventory] = GuiModeState({ mMap, mInventoryWindow, mSpellWindow, mStatsWindow }); + mGuiModeStates[GM_None] = GuiModeState({ mMap, mInventoryWindow, mSpellWindow, mStatsWindow }); + + auto tradeWindow = std::make_unique(); + mTradeWindow = tradeWindow.get(); + mWindows.push_back(std::move(tradeWindow)); + trackWindow(mTradeWindow, makeBarterWindowSettingValues()); + mGuiModeStates[GM_Barter] = GuiModeState({ mInventoryWindow, mTradeWindow }); + + auto console = std::make_unique(w, h, mConsoleOnlyScripts, mCfgMgr); + mConsole = console.get(); + mWindows.push_back(std::move(console)); + trackWindow(mConsole, makeConsoleWindowSettingValues()); + + constexpr VFS::Path::NormalizedView menubookOptionsOverTexture("textures/tx_menubook_options_over.dds"); + const bool questList = mResourceSystem->getVFS()->exists(menubookOptionsOverTexture); + auto journal = JournalWindow::create(JournalViewModel::create(), questList, mEncoding); + mGuiModeStates[GM_Journal] = GuiModeState(journal.get()); + mWindows.push_back(std::move(journal)); + + mMessageBoxManager = std::make_unique( + mStore->get().find("fMessageTimePerChar")->mValue.getFloat()); + + auto spellBuyingWindow = std::make_unique(); + mGuiModeStates[GM_SpellBuying] = GuiModeState(spellBuyingWindow.get()); + mWindows.push_back(std::move(spellBuyingWindow)); + + auto travelWindow = std::make_unique(); + mGuiModeStates[GM_Travel] = GuiModeState(travelWindow.get()); + mWindows.push_back(std::move(travelWindow)); + + auto dialogueWindow = std::make_unique(); + mDialogueWindow = dialogueWindow.get(); + mWindows.push_back(std::move(dialogueWindow)); + trackWindow(mDialogueWindow, makeDialogueWindowSettingValues()); mGuiModeStates[GM_Dialogue] = GuiModeState(mDialogueWindow); mTradeWindow->eventTradeDone += MyGUI::newDelegate(mDialogueWindow, &DialogueWindow::onTradeComplete); - ContainerWindow* containerWindow = new ContainerWindow(mDragAndDrop); - mWindows.push_back(containerWindow); - trackWindow(containerWindow, "container"); - mGuiModeStates[GM_Container] = GuiModeState({containerWindow, mInventoryWindow}); + auto containerWindow = std::make_unique(mDragAndDrop.get()); + mContainerWindow = containerWindow.get(); + mWindows.push_back(std::move(containerWindow)); + trackWindow(mContainerWindow, makeContainerWindowSettingValues()); + mGuiModeStates[GM_Container] = GuiModeState({ mContainerWindow, mInventoryWindow }); - mHud = new HUD(mCustomMarkers, mDragAndDrop, mLocalMapRender); - mWindows.push_back(mHud); + auto hud = std::make_unique(mCustomMarkers, mDragAndDrop.get(), mLocalMapRender.get()); + mHud = hud.get(); + mWindows.push_back(std::move(hud)); - mToolTips = new ToolTips(); + mToolTips = std::make_unique(); - mScrollWindow = new ScrollWindow(); - mWindows.push_back(mScrollWindow); + auto scrollWindow = std::make_unique(); + mScrollWindow = scrollWindow.get(); + mWindows.push_back(std::move(scrollWindow)); mGuiModeStates[GM_Scroll] = GuiModeState(mScrollWindow); - mGuiModeStates[GM_Scroll].mOpenSound = "scroll"; - mGuiModeStates[GM_Scroll].mCloseSound = "scroll"; - mBookWindow = new BookWindow(); - mWindows.push_back(mBookWindow); + auto bookWindow = std::make_unique(); + mBookWindow = bookWindow.get(); + mWindows.push_back(std::move(bookWindow)); mGuiModeStates[GM_Book] = GuiModeState(mBookWindow); - mGuiModeStates[GM_Book].mOpenSound = "book open"; - mGuiModeStates[GM_Book].mCloseSound = "book close"; - mCountDialog = new CountDialog(); - mWindows.push_back(mCountDialog); + auto countDialog = std::make_unique(); + mCountDialog = countDialog.get(); + mWindows.push_back(std::move(countDialog)); - mSettingsWindow = new SettingsWindow(); - mWindows.push_back(mSettingsWindow); - mGuiModeStates[GM_Settings] = GuiModeState(mSettingsWindow); + auto settingsWindow = std::make_unique(); + mSettingsWindow = settingsWindow.get(); + mWindows.push_back(std::move(settingsWindow)); + trackWindow(mSettingsWindow, makeSettingsWindowSettingValues()); - mConfirmationDialog = new ConfirmationDialog(); - mWindows.push_back(mConfirmationDialog); + auto confirmationDialog = std::make_unique(); + mConfirmationDialog = confirmationDialog.get(); + mWindows.push_back(std::move(confirmationDialog)); - AlchemyWindow* alchemyWindow = new AlchemyWindow(); - mWindows.push_back(alchemyWindow); - trackWindow(alchemyWindow, "alchemy"); - mGuiModeStates[GM_Alchemy] = GuiModeState(alchemyWindow); + auto alchemyWindow = std::make_unique(); + trackWindow(alchemyWindow.get(), makeAlchemyWindowSettingValues()); + mGuiModeStates[GM_Alchemy] = GuiModeState(alchemyWindow.get()); + mWindows.push_back(std::move(alchemyWindow)); - mQuickKeysMenu = new QuickKeysMenu(); - mWindows.push_back(mQuickKeysMenu); + auto quickKeysMenu = std::make_unique(); + mQuickKeysMenu = quickKeysMenu.get(); + mWindows.push_back(std::move(quickKeysMenu)); mGuiModeStates[GM_QuickKeysMenu] = GuiModeState(mQuickKeysMenu); - LevelupDialog* levelupDialog = new LevelupDialog(); - mWindows.push_back(levelupDialog); - mGuiModeStates[GM_Levelup] = GuiModeState(levelupDialog); + auto levelupDialog = std::make_unique(); + mGuiModeStates[GM_Levelup] = GuiModeState(levelupDialog.get()); + mWindows.push_back(std::move(levelupDialog)); - mWaitDialog = new WaitDialog(); - mWindows.push_back(mWaitDialog); - mGuiModeStates[GM_Rest] = GuiModeState({mWaitDialog->getProgressBar(), mWaitDialog}); + auto waitDialog = std::make_unique(); + mWaitDialog = waitDialog.get(); + mWindows.push_back(std::move(waitDialog)); + mGuiModeStates[GM_Rest] = GuiModeState({ mWaitDialog->getProgressBar(), mWaitDialog }); - SpellCreationDialog* spellCreationDialog = new SpellCreationDialog(); - mWindows.push_back(spellCreationDialog); - mGuiModeStates[GM_SpellCreation] = GuiModeState(spellCreationDialog); + auto spellCreationDialog = std::make_unique(); + mGuiModeStates[GM_SpellCreation] = GuiModeState(spellCreationDialog.get()); + mWindows.push_back(std::move(spellCreationDialog)); - EnchantingDialog* enchantingDialog = new EnchantingDialog(); - mWindows.push_back(enchantingDialog); - mGuiModeStates[GM_Enchanting] = GuiModeState(enchantingDialog); + auto enchantingDialog = std::make_unique(); + mGuiModeStates[GM_Enchanting] = GuiModeState(enchantingDialog.get()); + mWindows.push_back(std::move(enchantingDialog)); - TrainingWindow* trainingWindow = new TrainingWindow(); - mWindows.push_back(trainingWindow); - mGuiModeStates[GM_Training] = GuiModeState({trainingWindow->getProgressBar(), trainingWindow}); + auto trainingWindow = std::make_unique(); + mGuiModeStates[GM_Training] = GuiModeState({ trainingWindow->getProgressBar(), trainingWindow.get() }); + mWindows.push_back(std::move(trainingWindow)); - MerchantRepair* merchantRepair = new MerchantRepair(); - mWindows.push_back(merchantRepair); - mGuiModeStates[GM_MerchantRepair] = GuiModeState(merchantRepair); + auto merchantRepair = std::make_unique(); + mGuiModeStates[GM_MerchantRepair] = GuiModeState(merchantRepair.get()); + mWindows.push_back(std::move(merchantRepair)); - Repair* repair = new Repair(); - mWindows.push_back(repair); - mGuiModeStates[GM_Repair] = GuiModeState(repair); + auto repair = std::make_unique(); + mGuiModeStates[GM_Repair] = GuiModeState(repair.get()); + mWindows.push_back(std::move(repair)); - mSoulgemDialog = new SoulgemDialog(mMessageBoxManager); + mSoulgemDialog = std::make_unique(mMessageBoxManager.get()); - CompanionWindow* companionWindow = new CompanionWindow(mDragAndDrop, mMessageBoxManager); - mWindows.push_back(companionWindow); - trackWindow(companionWindow, "companion"); - mGuiModeStates[GM_Companion] = GuiModeState({mInventoryWindow, companionWindow}); + auto companionWindow = std::make_unique(mDragAndDrop.get(), mMessageBoxManager.get()); + trackWindow(companionWindow.get(), makeCompanionWindowSettingValues()); + mGuiModeStates[GM_Companion] = GuiModeState({ mInventoryWindow, companionWindow.get() }); + mWindows.push_back(std::move(companionWindow)); - mJailScreen = new JailScreen(); - mWindows.push_back(mJailScreen); + auto jailScreen = std::make_unique(); + mJailScreen = jailScreen.get(); + mWindows.push_back(std::move(jailScreen)); mGuiModeStates[GM_Jail] = GuiModeState(mJailScreen); std::string werewolfFaderTex = "textures\\werewolfoverlay.dds"; if (mResourceSystem->getVFS()->exists(werewolfFaderTex)) { - mWerewolfFader = new ScreenFader(werewolfFaderTex); - mWindows.push_back(mWerewolfFader); + auto werewolfFader = std::make_unique(werewolfFaderTex); + mWerewolfFader = werewolfFader.get(); + mWindows.push_back(std::move(werewolfFader)); } - mBlindnessFader = new ScreenFader("black"); - mWindows.push_back(mBlindnessFader); + auto blindnessFader = std::make_unique("black"); + mBlindnessFader = blindnessFader.get(); + mWindows.push_back(std::move(blindnessFader)); // fall back to player_hit_01.dds if bm_player_hit_01.dds is not available std::string hitFaderTexture = "textures\\bm_player_hit_01.dds"; const std::string hitFaderLayout = "openmw_screen_fader_hit.layout"; - MyGUI::FloatCoord hitFaderCoord (0,0,1,1); - if(!mResourceSystem->getVFS()->exists(hitFaderTexture)) + MyGUI::FloatCoord hitFaderCoord(0, 0, 1, 1); + if (!mResourceSystem->getVFS()->exists(hitFaderTexture)) { hitFaderTexture = "textures\\player_hit_01.dds"; hitFaderCoord = MyGUI::FloatCoord(0.2, 0.25, 0.6, 0.5); } - mHitFader = new ScreenFader(hitFaderTexture, hitFaderLayout, hitFaderCoord); - mWindows.push_back(mHitFader); + auto hitFader = std::make_unique(hitFaderTexture, hitFaderLayout, hitFaderCoord); + mHitFader = hitFader.get(); + mWindows.push_back(std::move(hitFader)); + + auto screenFader = std::make_unique("black"); + mScreenFader = screenFader.get(); + mWindows.push_back(std::move(screenFader)); - mScreenFader = new ScreenFader("black"); - mWindows.push_back(mScreenFader); + auto debugWindow = std::make_unique(); + mDebugWindow = debugWindow.get(); + mWindows.push_back(std::move(debugWindow)); + trackWindow(mDebugWindow, makeDebugWindowSettingValues()); - mDebugWindow = new DebugWindow(); - mWindows.push_back(mDebugWindow); + auto postProcessorHud = std::make_unique(); + mPostProcessorHud = postProcessorHud.get(); + mWindows.push_back(std::move(postProcessorHud)); + trackWindow(mPostProcessorHud, makePostprocessorWindowSettingValues()); - mInputBlocker = MyGUI::Gui::getInstance().createWidget("",0,0,w,h,MyGUI::Align::Stretch,"InputBlocker"); + mInputBlocker = MyGUI::Gui::getInstance().createWidget( + {}, 0, 0, w, h, MyGUI::Align::Stretch, "InputBlocker"); mHud->setVisible(true); - mCharGen = new CharacterCreation(mViewer->getSceneData()->asGroup(), mResourceSystem); + mCharGen = std::make_unique(mViewer->getSceneData()->asGroup(), mResourceSystem); updatePinnedWindows(); @@ -473,12 +518,14 @@ namespace MWGui mStatsWatcher->addListener(mHud); mStatsWatcher->addListener(mStatsWindow); - mStatsWatcher->addListener(mCharGen); - } + mStatsWatcher->addListener(mCharGen.get()); - int WindowManager::getFontHeight() const - { - return mFontLoader->getFontHeight(); + for (auto& window : mWindows) + { + std::string_view id = window->getWindowIdForLua(); + if (!id.empty()) + mLuaIdToWindow.emplace(id, window.get()); + } } void WindowManager::setNewGame(bool newgame) @@ -487,10 +534,9 @@ namespace MWGui { disallowAll(); - mStatsWatcher->removeListener(mCharGen); - delete mCharGen; - mCharGen = new CharacterCreation(mViewer->getSceneData()->asGroup(), mResourceSystem); - mStatsWatcher->addListener(mCharGen); + mStatsWatcher->removeListener(mCharGen.get()); + mCharGen = std::make_unique(mViewer->getSceneData()->asGroup(), mResourceSystem); + mStatsWatcher->addListener(mCharGen.get()); } else allow(GW_ALL); @@ -502,6 +548,9 @@ namespace MWGui { try { + LuaUi::clearGameInterface(); + LuaUi::clearMenuInterface(); + mStatsWatcher.reset(); MyGUI::LanguageManager::getInstance().eventRequestTag.clear(); @@ -510,17 +559,10 @@ namespace MWGui MyGUI::ClipboardManager::getInstance().eventClipboardChanged.clear(); MyGUI::ClipboardManager::getInstance().eventClipboardRequested.clear(); - for (WindowBase* window : mWindows) - delete window; mWindows.clear(); - - delete mMessageBoxManager; - delete mLocalMapRender; - delete mCharGen; - delete mDragAndDrop; - delete mSoulgemDialog; - delete mCursorManager; - delete mToolTips; + mMessageBoxManager.reset(); + mToolTips.reset(); + mCharGen.reset(); mKeyboardNavigation.reset(); @@ -529,19 +571,16 @@ namespace MWGui mFontLoader.reset(); mGui->shutdown(); - delete mGui; mGuiPlatform->shutdown(); - delete mGuiPlatform; - delete mVideoWrapper; } - catch(const MyGUI::Exception& e) + catch (const MyGUI::Exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } - void WindowManager::setStore(const MWWorld::ESMStore &store) + void WindowManager::setStore(const MWWorld::ESMStore& store) { mStore = &store; } @@ -549,30 +588,23 @@ namespace MWGui void WindowManager::cleanupGarbage() { // Delete any dialogs which are no longer in use - if (!mGarbageDialogs.empty()) - { - for (Layout* widget : mGarbageDialogs) - { - delete widget; - } - mGarbageDialogs.clear(); - } + mGarbageDialogs.clear(); } void WindowManager::enableScene(bool enable) { - unsigned int disablemask = MWRender::Mask_GUI|MWRender::Mask_PreCompile; - if (!enable && mViewer->getCamera()->getCullMask() != disablemask) + unsigned int disablemask = MWRender::Mask_GUI | MWRender::Mask_PreCompile; + if (!enable && getCullMask() != disablemask) { mOldUpdateMask = mViewer->getUpdateVisitor()->getTraversalMask(); - mOldCullMask = mViewer->getCamera()->getCullMask(); + mOldCullMask = getCullMask(); mViewer->getUpdateVisitor()->setTraversalMask(disablemask); - mViewer->getCamera()->setCullMask(disablemask); + setCullMask(disablemask); } - else if (enable && mViewer->getCamera()->getCullMask() == disablemask) + else if (enable && getCullMask() == disablemask) { mViewer->getUpdateVisitor()->setTraversalMask(mOldUpdateMask); - mViewer->getCamera()->setCullMask(mOldCullMask); + setCullMask(mOldCullMask); } } @@ -585,7 +617,8 @@ namespace MWGui { bool loading = (getMode() == GM_Loading || getMode() == GM_LoadingWallpaper); - bool mainmenucover = containsMode(GM_MainMenu) && MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame; + bool mainmenucover = containsMode(GM_MainMenu) + && MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame; enableScene(!loading && !mainmenucover); @@ -599,7 +632,7 @@ namespace MWGui MWBase::Environment::get().getInputManager()->changeInputMode(!gameMode); - mInputBlocker->setVisible (gameMode); + mInputBlocker->setVisible(gameMode); if (loading) setCursorVisible(mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox()); @@ -607,11 +640,12 @@ namespace MWGui setCursorVisible(!gameMode); if (gameMode) - setKeyFocusWidget (nullptr); + setKeyFocusWidget(nullptr); // Icons of forced hidden windows are displayed setMinimapVisibility((mAllowed & GW_Map) && (!mMap->pinned() || (mForceHidden & GW_Map))); - setWeaponVisibility((mAllowed & GW_Inventory) && (!mInventoryWindow->pinned() || (mForceHidden & GW_Inventory))); + setWeaponVisibility( + (mAllowed & GW_Inventory) && (!mInventoryWindow->pinned() || (mForceHidden & GW_Inventory))); setSpellVisibility((mAllowed & GW_Magic) && (!mSpellWindow->pinned() || (mForceHidden & GW_Magic))); setHMSVisibility((mAllowed & GW_Stats) && (!mStatsWindow->pinned() || (mForceHidden & GW_Stats))); @@ -621,9 +655,12 @@ namespace MWGui if (mGuiModes.empty()) { mMap->setVisible(mMap->pinned() && !isConsoleMode() && !(mForceHidden & GW_Map) && (mAllowed & GW_Map)); - mStatsWindow->setVisible(mStatsWindow->pinned() && !isConsoleMode() && !(mForceHidden & GW_Stats) && (mAllowed & GW_Stats)); - mInventoryWindow->setVisible(mInventoryWindow->pinned() && !isConsoleMode() && !(mForceHidden & GW_Inventory) && (mAllowed & GW_Inventory)); - mSpellWindow->setVisible(mSpellWindow->pinned() && !isConsoleMode() && !(mForceHidden & GW_Magic) && (mAllowed & GW_Magic)); + mStatsWindow->setVisible( + mStatsWindow->pinned() && !isConsoleMode() && !(mForceHidden & GW_Stats) && (mAllowed & GW_Stats)); + mInventoryWindow->setVisible(mInventoryWindow->pinned() && !isConsoleMode() + && !(mForceHidden & GW_Inventory) && (mAllowed & GW_Inventory)); + mSpellWindow->setVisible( + mSpellWindow->pinned() && !isConsoleMode() && !(mForceHidden & GW_Magic) && (mAllowed & GW_Magic)); return; } else if (getMode() != GM_Inventory) @@ -631,7 +668,9 @@ namespace MWGui mMap->setVisible(false); mStatsWindow->setVisible(false); mSpellWindow->setVisible(false); - mInventoryWindow->setVisible(getMode() == GM_Container || getMode() == GM_Barter || getMode() == GM_Companion); + mHud->setDrowningBarVisible(false); + mInventoryWindow->setVisible( + getMode() == GM_Container || getMode() == GM_Barter || getMode() == GM_Companion); } GuiMode mode = mGuiModes.back(); @@ -653,33 +692,33 @@ namespace MWGui switch (mode) { - // FIXME: refactor chargen windows to use modes properly (or not use them at all) - case GM_Name: - case GM_Race: - case GM_Class: - case GM_ClassPick: - case GM_ClassCreate: - case GM_Birth: - case GM_ClassGenerate: - case GM_Review: - mCharGen->spawnDialog(mode); - break; - default: - break; + // FIXME: refactor chargen windows to use modes properly (or not use them at all) + case GM_Name: + case GM_Race: + case GM_Class: + case GM_ClassPick: + case GM_ClassCreate: + case GM_Birth: + case GM_ClassGenerate: + case GM_Review: + mCharGen->spawnDialog(mode); + break; + default: + break; } } - void WindowManager::setDrowningTimeLeft (float time, float maxTime) + void WindowManager::setDrowningTimeLeft(float time, float maxTime) { mHud->setDrowningTimeLeft(time, maxTime); } - void WindowManager::removeDialog(Layout*dialog) + void WindowManager::removeDialog(std::unique_ptr&& dialog) { if (!dialog) return; dialog->setVisible(false); - mGarbageDialogs.push_back(dialog); + mGarbageDialogs.push_back(std::move(dialog)); } void WindowManager::exitCurrentGuiMode() @@ -691,13 +730,13 @@ namespace MWGui } GuiModeState& state = mGuiModeStates[mGuiModes.back()]; - for (WindowBase* window : state.mWindows) + for (const auto& window : state.mWindows) { if (!window->exit()) { // unable to exit window, but give access to main menu if (!MyGUI::InputManager::getInstance().isModalAny() && getMode() != GM_MainMenu) - pushGuiMode (GM_MainMenu); + pushGuiMode(GM_MainMenu); return; } } @@ -705,18 +744,22 @@ namespace MWGui popGuiMode(); } - void WindowManager::interactiveMessageBox(const std::string &message, const std::vector &buttons, bool block) + void WindowManager::interactiveMessageBox( + std::string_view message, const std::vector& buttons, bool block, int defaultFocus) { - mMessageBoxManager->createInteractiveMessageBox(message, buttons); + mMessageBoxManager->createInteractiveMessageBox(message, buttons, block, defaultFocus); updateVisible(); if (block) { - Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); + Misc::FrameRateLimiter frameRateLimiter + = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); while (mMessageBoxManager->readPressedButton(false) == -1 - && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) + && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) { - const double dt = std::chrono::duration_cast>(frameRateLimiter.getLastFrameDuration()).count(); + const double dt + = std::chrono::duration_cast>(frameRateLimiter.getLastFrameDuration()) + .count(); mKeyboardNavigation->onFrame(); mMessageBoxManager->onFrame(dt); @@ -737,19 +780,30 @@ namespace MWGui frameRateLimiter.limit(); } + + mMessageBoxManager->resetInteractiveMessageBox(); } } - void WindowManager::messageBox (const std::string& message, enum MWGui::ShowInDialogueMode showInDialogueMode) + void WindowManager::messageBox(std::string_view message, enum MWGui::ShowInDialogueMode showInDialogueMode) { - if (getMode() == GM_Dialogue && showInDialogueMode != MWGui::ShowInDialogueMode_Never) { - mDialogueWindow->addMessageBox(MyGUI::LanguageManager::getInstance().replaceTags(message)); - } else if (showInDialogueMode != MWGui::ShowInDialogueMode_Only) { + if (getMode() == GM_Dialogue && showInDialogueMode != MWGui::ShowInDialogueMode_Never) + { + MyGUI::UString text = MyGUI::LanguageManager::getInstance().replaceTags(MyGUI::UString(message)); + mDialogueWindow->addMessageBox(text); + } + else if (showInDialogueMode != MWGui::ShowInDialogueMode_Only) + { mMessageBoxManager->createMessageBox(message); } } - void WindowManager::staticMessageBox(const std::string& message) + void WindowManager::scheduleMessageBox(std::string message, enum MWGui::ShowInDialogueMode showInDialogueMode) + { + mScheduledMessageBoxes.lock()->emplace_back(std::move(message), showInDialogueMode); + } + + void WindowManager::staticMessageBox(std::string_view message) { mMessageBoxManager->createMessageBox(message, true); } @@ -759,16 +813,16 @@ namespace MWGui mMessageBoxManager->removeStaticMessageBox(); } - int WindowManager::readPressedButton () + int WindowManager::readPressedButton() { return mMessageBoxManager->readPressedButton(); } - std::string WindowManager::getGameSettingString(const std::string &id, const std::string &default_) + std::string_view WindowManager::getGameSettingString(std::string_view id, std::string_view default_) { - const ESM::GameSetting *setting = mStore->get().search(id); + const ESM::GameSetting* setting = mStore->get().search(id); - if (setting && setting->mValue.getType()==ESM::VT_String) + if (setting && setting->mValue.getType() == ESM::VT_String) return setting->mValue.getString(); return default_; @@ -782,16 +836,16 @@ namespace MWGui MWWorld::ConstPtr player = MWMechanics::getPlayer(); osg::Vec3f playerPosition = player.getRefData().getPosition().asVec3(); - osg::Quat playerOrientation (-player.getRefData().getPosition().rot[2], osg::Vec3(0,0,1)); + osg::Quat playerOrientation(-player.getRefData().getPosition().rot[2], osg::Vec3(0, 0, 1)); osg::Vec3f playerdirection; - int x,y; - float u,v; + int x, y; + float u, v; mLocalMapRender->updatePlayer(playerPosition, playerOrientation, u, v, x, y, playerdirection); if (!player.getCell()->isExterior()) { - setActiveMap(x, y, true); + setActiveMap(*player.getCell()->getCell()); } // else: need to know the current grid center, call setActiveMap from changeCell @@ -801,10 +855,12 @@ namespace MWGui mHud->setPlayerPos(x, y, u, v); } - void WindowManager::update (float frameDuration) + void WindowManager::update(float frameDuration) { - bool gameRunning = MWBase::Environment::get().getStateManager()->getState()!= - MWBase::StateManager::State_NoGame; + handleScheduledMessageBoxes(); + + bool gameRunning + = MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame; if (gameRunning) updateMap(); @@ -826,9 +882,11 @@ namespace MWGui // Make sure message boxes are always in front // This is an awful workaround for a series of awfully interwoven issues that couldn't be worked around // in a better way because of an impressive number of even more awfully interwoven issues. - if (mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox() && mCurrentModals.back() != mMessageBoxManager->getInteractiveMessageBox()) + if (mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox() + && mCurrentModals.back() != mMessageBoxManager->getInteractiveMessageBox()) { - std::vector::iterator found = std::find(mCurrentModals.begin(), mCurrentModals.end(), mMessageBoxManager->getInteractiveMessageBox()); + std::vector::iterator found = std::find( + mCurrentModals.begin(), mCurrentModals.end(), mMessageBoxManager->getInteractiveMessageBox()); if (found != mCurrentModals.end()) { WindowModal* msgbox = *found; @@ -852,13 +910,22 @@ namespace MWGui if (mLocalMapRender) mLocalMapRender->cleanupCameras(); + mDebugWindow->onFrame(frameDuration); + + if (isConsoleMode()) + mConsole->onFrame(frameDuration); + + if (isSettingsWindowVisible()) + mSettingsWindow->onFrame(frameDuration); + if (!gameRunning) return; // We should display message about crime only once per frame, even if there are several crimes. // Otherwise we will get message spam when stealing several items via Take All button. const MWWorld::Ptr player = MWMechanics::getPlayer(); - int currentBounty = player.getClass().getNpcStats(player).getBounty(); + const MWWorld::Class& playerCls = player.getClass(); + int currentBounty = playerCls.getNpcStats(player).getBounty(); if (currentBounty != mPlayerBounty) { if (mPlayerBounty >= 0 && currentBounty > mPlayerBounty) @@ -867,11 +934,31 @@ namespace MWGui mPlayerBounty = currentBounty; } + MWBase::LuaManager::ActorControls* playerControls + = MWBase::Environment::get().getLuaManager()->getActorControls(player); + bool triedToMove = playerControls + && (playerControls->mMovement != 0 || playerControls->mSideMovement != 0 || playerControls->mJump); + if (mMessageBoxManager && triedToMove && playerCls.getEncumbrance(player) > playerCls.getCapacity(player)) + { + const auto& msgboxs = mMessageBoxManager->getActiveMessageBoxes(); + auto it + = std::find_if(msgboxs.begin(), msgboxs.end(), [](const std::unique_ptr& msgbox) { + return (msgbox->getMessage() == "#{sNotifyMessage59}"); + }); + + // if an overencumbered messagebox is already present, reset its expiry timer, + // otherwise create a new one. + if (it != msgboxs.end()) + (*it)->mCurrentTime = 0; + else + messageBox("#{sNotifyMessage59}"); + } + mDragAndDrop->onFrame(); mHud->onFrame(frameDuration); - mDebugWindow->onFrame(frameDuration); + mPostProcessorHud->onFrame(frameDuration); if (mCharGen) mCharGen->onFrame(frameDuration); @@ -887,40 +974,35 @@ namespace MWGui { mMap->requestMapRender(cell); - std::string name = MWBase::Environment::get().getWorld()->getCellName (cell); + std::string name{ MWBase::Environment::get().getWorld()->getCellName(cell) }; - mMap->setCellName( name ); - mHud->setCellName( name ); + mMap->setCellName(name); + mHud->setCellName(name); + auto cellCommon = cell->getCell(); - if (cell->getCell()->isExterior()) + if (cellCommon->isExterior()) { - if (!cell->getCell()->mName.empty()) - mMap->addVisitedLocation (name, cell->getCell()->getGridX (), cell->getCell()->getGridY ()); - - mMap->cellExplored (cell->getCell()->getGridX(), cell->getCell()->getGridY()); + if (!cellCommon->getNameId().empty()) + mMap->addVisitedLocation(name, cellCommon->getGridX(), cellCommon->getGridY()); - setActiveMap(cell->getCell()->getGridX(), cell->getCell()->getGridY(), false); + mMap->cellExplored(cellCommon->getGridX(), cellCommon->getGridY()); } else { - mMap->setCellPrefix (cell->getCell()->mName ); - mHud->setCellPrefix (cell->getCell()->mName ); - osg::Vec3f worldPos; if (!MWBase::Environment::get().getWorld()->findInteriorPositionInWorldSpace(cell, worldPos)) worldPos = MWBase::Environment::get().getWorld()->getPlayer().getLastKnownExteriorPosition(); else MWBase::Environment::get().getWorld()->getPlayer().setLastKnownExteriorPosition(worldPos); mMap->setGlobalMapPlayerPosition(worldPos.x(), worldPos.y()); - - setActiveMap(0, 0, true); } + setActiveMap(*cellCommon); } - void WindowManager::setActiveMap(int x, int y, bool interior) + void WindowManager::setActiveMap(const MWWorld::Cell& cell) { - mMap->setActiveCell(x,y, interior); - mHud->setActiveCell(x,y, interior); + mMap->setActiveCell(cell); + mHud->setActiveCell(cell); } void WindowManager::setDrowningBarVisibility(bool visible) @@ -930,12 +1012,12 @@ namespace MWGui void WindowManager::setHMSVisibility(bool visible) { - mHud->setHmsVisible (visible); + mHud->setHmsVisible(visible); } void WindowManager::setMinimapVisibility(bool visible) { - mHud->setMinimapVisible (visible); + mHud->setMinimapVisible(visible); } bool WindowManager::toggleFogOfWar() @@ -948,7 +1030,8 @@ namespace MWGui { mToolTips->setFocusObject(focus); - if(mHud && (mShowOwned == 2 || mShowOwned == 3)) + const int showOwned = Settings::game().mShowOwned; + if (mHud && (showOwned == 2 || showOwned == 3)) { bool owned = mToolTips->checkOwned(); mHud->setCrosshairOwned(owned); @@ -972,13 +1055,13 @@ namespace MWGui void WindowManager::setWeaponVisibility(bool visible) { - mHud->setWeapVisible (visible); + mHud->setWeapVisible(visible); } void WindowManager::setSpellVisibility(bool visible) { - mHud->setSpellVisible (visible); - mHud->setEffectVisible (visible); + mHud->setSpellVisible(visible); + mHud->setEffectVisible(visible); } void WindowManager::setSneakVisibility(bool visible) @@ -1004,26 +1087,28 @@ namespace MWGui void WindowManager::onRetrieveTag(const MyGUI::UString& _tag, MyGUI::UString& _result) { - std::string tag(_tag); - - std::string MyGuiPrefix = "setting="; - size_t MyGuiPrefixLength = MyGuiPrefix.length(); + std::string_view tag = _tag; + + std::string_view MyGuiPrefix = "setting="; - std::string tokenToFind = "sCell="; - size_t tokenLength = tokenToFind.length(); - - if(tag.compare(0, MyGuiPrefixLength, MyGuiPrefix) == 0) + std::string_view tokenToFind = "sCell="; + + if (tag.starts_with(MyGuiPrefix)) { - tag = tag.substr(MyGuiPrefixLength, tag.length()); + tag = tag.substr(MyGuiPrefix.length()); size_t comma_pos = tag.find(','); - std::string settingSection = tag.substr(0, comma_pos); - std::string settingTag = tag.substr(comma_pos+1, tag.length()); - - _result = Settings::Manager::getString(settingTag, settingSection); + if (comma_pos == std::string_view::npos) + throw std::runtime_error("Invalid setting tag (expected comma): " + std::string(tag)); + + std::string_view settingSection = tag.substr(0, comma_pos); + std::string_view settingTag = tag.substr(comma_pos + 1, tag.length()); + + _result = Settings::get(settingSection, settingTag).get().print(); } - else if (tag.compare(0, tokenLength, tokenToFind) == 0) + else if (tag.starts_with(tokenToFind)) { - _result = mTranslationDataStorage.translateCellName(tag.substr(tokenLength)); + std::string_view cellName = mTranslationDataStorage.translateCellName(tag.substr(tokenToFind.length())); + _result.assign(cellName.data(), cellName.size()); _result = MyGUI::TextIterator::toTagsString(_result); } else if (Gui::replaceTag(tag, _result)) @@ -1032,64 +1117,75 @@ namespace MWGui } else { + std::vector split; + Misc::StringUtils::split(tag, split, ":"); + + l10n::Manager& l10nManager = *MWBase::Environment::get().getL10nManager(); + + // If a key has a "Context:KeyName" format, use YAML to translate data + if (split.size() == 2) + { + _result = l10nManager.getContext(split[0])->formatMessage(split[1], {}, {}); + return; + } + + // If not, treat is as GMST name from legacy localization if (!mStore) { - Log(Debug::Error) << "Error: WindowManager::onRetrieveTag: no Store set up yet, can not replace '" << tag << "'"; + Log(Debug::Error) << "Error: WindowManager::onRetrieveTag: no Store set up yet, can not replace '" + << tag << "'"; + _result.assign(tag.data(), tag.size()); return; } - const ESM::GameSetting *setting = mStore->get().find(tag); + const ESM::GameSetting* setting = mStore->get().search(tag); - if (setting && setting->mValue.getType()==ESM::VT_String) + if (setting && setting->mValue.getType() == ESM::VT_String) _result = setting->mValue.getString(); else - _result = tag; + _result.assign(tag.data(), tag.size()); } } void WindowManager::processChangedSettings(const Settings::CategorySettingVector& changed) { - mToolTips->setDelay(Settings::Manager::getFloat("tooltip delay", "GUI")); - bool changeRes = false; for (const auto& setting : changed) { - if (setting.first == "HUD" && setting.second == "crosshair") - mCrosshairEnabled = Settings::Manager::getBool ("crosshair", "HUD"); - else if (setting.first == "GUI" && setting.second == "subtitles") - mSubtitlesEnabled = Settings::Manager::getBool ("subtitles", "GUI"); - else if (setting.first == "GUI" && setting.second == "menu transparency") - setMenuTransparency(Settings::Manager::getFloat("menu transparency", "GUI")); - else if (setting.first == "Video" && ( - setting.second == "resolution x" - || setting.second == "resolution y" - || setting.second == "fullscreen" - || setting.second == "window border")) + if (setting.first == "GUI" && setting.second == "menu transparency") + setMenuTransparency(Settings::gui().mMenuTransparency); + else if (setting.first == "Video" + && (setting.second == "resolution x" || setting.second == "resolution y" + || setting.second == "window mode" || setting.second == "window border")) changeRes = true; - else if (setting.first == "Video" && setting.second == "vsync") - mVideoWrapper->setSyncToVBlank(Settings::Manager::getBool("vsync", "Video")); + else if (setting.first == "Video" && setting.second == "vsync mode") + mVideoWrapper->setSyncToVBlank(Settings::video().mVsyncMode); else if (setting.first == "Video" && (setting.second == "gamma" || setting.second == "contrast")) - mVideoWrapper->setGammaContrast(Settings::Manager::getFloat("gamma", "Video"), - Settings::Manager::getFloat("contrast", "Video")); + mVideoWrapper->setGammaContrast(Settings::video().mGamma, Settings::video().mContrast); } if (changeRes) { - mVideoWrapper->setVideoMode(Settings::Manager::getInt("resolution x", "Video"), - Settings::Manager::getInt("resolution y", "Video"), - Settings::Manager::getBool("fullscreen", "Video"), - Settings::Manager::getBool("window border", "Video")); + mVideoWrapper->setVideoMode(Settings::video().mResolutionX, Settings::video().mResolutionY, + Settings::video().mWindowMode, Settings::video().mWindowBorder); } } void WindowManager::windowResized(int x, int y) { - // Note: this is a side effect of resolution change or window resize. - // There is no need to track these changes. - Settings::Manager::setInt("resolution x", "Video", x); - Settings::Manager::setInt("resolution y", "Video", y); - Settings::Manager::resetPendingChange("resolution x", "Video"); - Settings::Manager::resetPendingChange("resolution y", "Video"); + Settings::video().mResolutionX.set(x); + Settings::video().mResolutionY.set(y); + + // We only want to process changes to window-size related settings. + Settings::CategorySettingVector filter = { { "Video", "resolution x" }, { "Video", "resolution y" } }; + + // If the HUD has not been initialised, the World singleton will not be available. + if (mHud) + { + MWBase::Environment::get().getWorld()->processChangedSettings(Settings::Manager::getPendingChanges(filter)); + } + + Settings::Manager::resetPendingChanges(filter); mGuiPlatform->getRenderManagerPtr()->setViewSize(x, y); @@ -1103,26 +1199,18 @@ namespace MWGui if (!mHud) return; // UI not initialized yet - for (std::map::iterator it = mTrackedWindows.begin(); it != mTrackedWindows.end(); ++it) + for (const auto& [window, settings] : mTrackedWindows) { - std::string settingName = it->second; - if (Settings::Manager::getBool(settingName + " maximized", "Windows")) - settingName += " maximized"; + const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mMaximized : settings.mRegular; + window->setPosition(MyGUI::IntPoint(static_cast(rect.mX * x), static_cast(rect.mY * y))); + window->setSize(MyGUI::IntSize(static_cast(rect.mW * x), static_cast(rect.mH * y))); - MyGUI::IntPoint pos(static_cast(Settings::Manager::getFloat(settingName + " x", "Windows") * x), - static_cast(Settings::Manager::getFloat(settingName + " y", "Windows") * y)); - MyGUI::IntSize size(static_cast(Settings::Manager::getFloat(settingName + " w", "Windows") * x), - static_cast(Settings::Manager::getFloat(settingName + " h", "Windows") * y)); - it->first->setPosition(pos); - it->first->setSize(size); + WindowBase::clampWindowCoordinates(window); } - for (WindowBase* window : mWindows) + for (const auto& window : mWindows) window->onResChange(x, y); - // We should reload TrueType fonts to fit new resolution - loadUserFonts(); - // TODO: check if any windows are now off-screen and move them back if so } @@ -1141,7 +1229,7 @@ namespace MWGui MWBase::Environment::get().getStateManager()->requestQuit(); } - void WindowManager::onCursorChange(const std::string &name) + void WindowManager::onCursorChange(std::string_view name) { mCursorManager->cursorChanged(name); } @@ -1153,7 +1241,17 @@ namespace MWGui void WindowManager::pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg) { - if (mode==GM_Inventory && mAllowed==GW_None) + pushGuiMode(mode, arg, false); + } + + void WindowManager::forceLootMode(const MWWorld::Ptr& ptr) + { + pushGuiMode(MWGui::GM_Container, ptr, true); + } + + void WindowManager::pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg, bool force) + { + if (mode == GM_Inventory && mAllowed == GW_None) return; if (mGuiModes.empty() || mGuiModes.back() != mode) @@ -1172,17 +1270,43 @@ namespace MWGui mGuiModes.push_back(mode); mGuiModeStates[mode].update(true); - playSound(mGuiModeStates[mode].mOpenSound); } - for (WindowBase* window : mGuiModeStates[mode].mWindows) - window->setPtr(arg); + if (force) + mContainerWindow->treatNextOpenAsLoot(); + + try + { + for (WindowBase* window : mGuiModeStates[mode].mWindows) + window->setPtr(arg); + } + catch (...) + { + popGuiMode(); + throw; + } mKeyboardNavigation->restoreFocus(mode); updateVisible(); + MWBase::Environment::get().getLuaManager()->uiModeChanged(arg); + } + + void WindowManager::setCullMask(uint32_t mask) + { + mViewer->getCamera()->setCullMask(mask); + + // We could check whether stereo is enabled here, but these methods are + // trivial and have no effect in mono or multiview so just call them regardless. + mViewer->getCamera()->setCullMaskLeft(mask); + mViewer->getCamera()->setCullMaskRight(mask); } - void WindowManager::popGuiMode(bool noSound) + uint32_t WindowManager::getCullMask() + { + return mViewer->getCamera()->getCullMask(); + } + + void WindowManager::popGuiMode(bool forceExit) { if (mDragAndDrop && mDragAndDrop->mIsOnDragAndDrop) { @@ -1192,11 +1316,19 @@ namespace MWGui if (!mGuiModes.empty()) { const GuiMode mode = mGuiModes.back(); + if (forceExit) + { + GuiModeState& state = mGuiModeStates[mode]; + for (const auto& window : state.mWindows) + window->exit(); + } mKeyboardNavigation->saveFocus(mode); - mGuiModes.pop_back(); - mGuiModeStates[mode].update(false); - if (!noSound) - playSound(mGuiModeStates[mode].mCloseSound); + if (containsMode(mode)) + { + mGuiModes.pop_back(); + mGuiModeStates[mode].update(false); + MWBase::Environment::get().getLuaManager()->uiModeChanged(MWWorld::Ptr()); + } } if (!mGuiModes.empty()) @@ -1213,11 +1345,11 @@ namespace MWGui mConsole->onOpen(); } - void WindowManager::removeGuiMode(GuiMode mode, bool noSound) + void WindowManager::removeGuiMode(GuiMode mode) { if (!mGuiModes.empty() && mGuiModes.back() == mode) { - popGuiMode(noSound); + popGuiMode(); return; } @@ -1231,6 +1363,7 @@ namespace MWGui } updateVisible(); + MWBase::Environment::get().getLuaManager()->uiModeChanged(MWWorld::Ptr()); } void WindowManager::goToJail(int days) @@ -1239,7 +1372,7 @@ namespace MWGui mJailScreen->goToJail(days); } - void WindowManager::setSelectedSpell(const std::string& spellId, int successChancePercent) + void WindowManager::setSelectedSpell(const ESM::RefId& spellId, int successChancePercent) { mSelectedSpell = spellId; mSelectedEnchantItem = MWWorld::Ptr(); @@ -1253,16 +1386,15 @@ namespace MWGui void WindowManager::setSelectedEnchantItem(const MWWorld::Ptr& item) { mSelectedEnchantItem = item; - mSelectedSpell = ""; - const ESM::Enchantment* ench = mStore->get() - .find(item.getClass().getEnchantment(item)); + mSelectedSpell = ESM::RefId(); + const ESM::Enchantment* ench = mStore->get().find(item.getClass().getEnchantment(item)); - int chargePercent = static_cast(item.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100); + int chargePercent = static_cast(item.getCellRef().getNormalizedEnchantmentCharge(*ench) * 100); mHud->setSelectedEnchantItem(item, chargePercent); mSpellWindow->setTitle(item.getClass().getName(item)); } - const MWWorld::Ptr &WindowManager::getSelectedEnchantItem() const + const MWWorld::Ptr& WindowManager::getSelectedEnchantItem() const { return mSelectedEnchantItem; } @@ -1279,22 +1411,22 @@ namespace MWGui mInventoryWindow->setTitle(item.getClass().getName(item)); } - const MWWorld::Ptr &WindowManager::getSelectedWeapon() const + const MWWorld::Ptr& WindowManager::getSelectedWeapon() const { return mSelectedWeapon; } void WindowManager::unsetSelectedSpell() { - mSelectedSpell = ""; + mSelectedSpell = ESM::RefId(); mSelectedEnchantItem = MWWorld::Ptr(); mHud->unsetSelectedSpell(); MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); - if (player->getDrawState() == MWMechanics::DrawState_Spell) - player->setDrawState(MWMechanics::DrawState_Nothing); + if (player->getDrawState() == MWMechanics::DrawState::Spell) + player->setDrawState(MWMechanics::DrawState::Nothing); - mSpellWindow->setTitle("#{sNone}"); + mSpellWindow->setTitle("#{Interface:None}"); } void WindowManager::unsetSelectedWeapon() @@ -1304,14 +1436,14 @@ namespace MWGui mInventoryWindow->setTitle("#{sSkillHandtohand}"); } - void WindowManager::getMousePosition(int &x, int &y) + void WindowManager::getMousePosition(int& x, int& y) { const MyGUI::IntPoint& pos = MyGUI::InputManager::getInstance().getMousePosition(); x = pos.left; y = pos.top; } - void WindowManager::getMousePosition(float &x, float &y) + void WindowManager::getMousePosition(float& x, float& y) { const MyGUI::IntPoint& pos = MyGUI::InputManager::getInstance().getMousePosition(); x = static_cast(pos.left); @@ -1326,40 +1458,56 @@ namespace MWGui return mHud->getWorldMouseOver(); } - float WindowManager::getScalingFactor() + float WindowManager::getScalingFactor() const { return mScalingFactor; } - void WindowManager::executeInConsole (const std::string& path) + void WindowManager::executeInConsole(const std::filesystem::path& path) { - mConsole->executeFile (path); + mConsole->executeFile(path); } - MWGui::InventoryWindow* WindowManager::getInventoryWindow() { return mInventoryWindow; } - MWGui::CountDialog* WindowManager::getCountDialog() { return mCountDialog; } - MWGui::ConfirmationDialog* WindowManager::getConfirmationDialog() { return mConfirmationDialog; } - MWGui::TradeWindow* WindowManager::getTradeWindow() { return mTradeWindow; } + MWGui::InventoryWindow* WindowManager::getInventoryWindow() + { + return mInventoryWindow; + } + MWGui::CountDialog* WindowManager::getCountDialog() + { + return mCountDialog; + } + MWGui::ConfirmationDialog* WindowManager::getConfirmationDialog() + { + return mConfirmationDialog; + } + MWGui::TradeWindow* WindowManager::getTradeWindow() + { + return mTradeWindow; + } + MWGui::PostProcessorHud* WindowManager::getPostProcessorHud() + { + return mPostProcessorHud; + } - void WindowManager::useItem(const MWWorld::Ptr &item, bool bypassBeastRestrictions) + void WindowManager::useItem(const MWWorld::Ptr& item, bool bypassBeastRestrictions) { if (mInventoryWindow) mInventoryWindow->useItem(item, bypassBeastRestrictions); } - bool WindowManager::isAllowed (GuiWindow wnd) const + bool WindowManager::isAllowed(GuiWindow wnd) const { return (mAllowed & wnd) != 0; } - void WindowManager::allow (GuiWindow wnd) + void WindowManager::allow(GuiWindow wnd) { mAllowed = (GuiWindow)(mAllowed | wnd); if (wnd & GW_Inventory) { - mBookWindow->setInventoryAllowed (true); - mScrollWindow->setInventoryAllowed (true); + mBookWindow->setInventoryAllowed(true); + mScrollWindow->setInventoryAllowed(true); } updateVisible(); @@ -1370,42 +1518,19 @@ namespace MWGui mAllowed = GW_None; mRestAllowed = false; - mBookWindow->setInventoryAllowed (false); - mScrollWindow->setInventoryAllowed (false); + mBookWindow->setInventoryAllowed(false); + mScrollWindow->setInventoryAllowed(false); updateVisible(); } - void WindowManager::toggleVisible (GuiWindow wnd) + void WindowManager::toggleVisible(GuiWindow wnd) { if (getMode() != GM_Inventory) return; - std::string settingName; - switch (wnd) - { - case GW_Inventory: - settingName = "inventory"; - break; - case GW_Map: - settingName = "map"; - break; - case GW_Magic: - settingName = "spells"; - break; - case GW_Stats: - settingName = "stats"; - break; - default: - break; - } - - if (!settingName.empty()) - { - settingName += " hidden"; - bool hidden = Settings::Manager::getBool(settingName, "Windows"); - Settings::Manager::setBool(settingName, "Windows", !hidden); - } + if (Settings::SettingValue* const hidden = findHiddenSetting(wnd)) + hidden->set(!hidden->get()); mShown = (GuiWindow)(mShown ^ wnd); updateVisible(); @@ -1425,10 +1550,7 @@ namespace MWGui bool WindowManager::isGuiMode() const { - return - !mGuiModes.empty() || - isConsoleMode() || - (mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox()); + return !mGuiModes.empty() || isConsoleMode() || isPostProcessorHudVisible() || isInteractiveMessageBoxActive(); } bool WindowManager::isConsoleMode() const @@ -1436,6 +1558,21 @@ namespace MWGui return mConsole && mConsole->isVisible(); } + bool WindowManager::isPostProcessorHudVisible() const + { + return mPostProcessorHud && mPostProcessorHud->isVisible(); + } + + bool WindowManager::isSettingsWindowVisible() const + { + return mSettingsWindow && mSettingsWindow->isVisible(); + } + + bool WindowManager::isInteractiveMessageBoxActive() const + { + return mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox(); + } + MWGui::GuiMode WindowManager::getMode() const { if (mGuiModes.empty()) @@ -1445,65 +1582,62 @@ namespace MWGui void WindowManager::disallowMouse() { - mInputBlocker->setVisible (true); + mInputBlocker->setVisible(true); } void WindowManager::allowMouse() { - mInputBlocker->setVisible (!isGuiMode ()); + mInputBlocker->setVisible(!isGuiMode()); } - void WindowManager::notifyInputActionBound () + void WindowManager::notifyInputActionBound() { - mSettingsWindow->updateControlsBox (); + mSettingsWindow->updateControlsBox(); allowMouse(); } bool WindowManager::containsMode(GuiMode mode) const { - if(mGuiModes.empty()) + if (mGuiModes.empty()) return false; return std::find(mGuiModes.begin(), mGuiModes.end(), mode) != mGuiModes.end(); } - void WindowManager::showCrosshair (bool show) + void WindowManager::showCrosshair(bool show) { if (mHud) - mHud->setCrosshairVisible (show && mCrosshairEnabled); + mHud->setCrosshairVisible(show && Settings::hud().mCrosshair); } - void WindowManager::updateActivatedQuickKey () + void WindowManager::updateActivatedQuickKey() { mQuickKeysMenu->updateActivatedQuickKey(); } - void WindowManager::activateQuickKey (int index) + void WindowManager::activateQuickKey(int index) { mQuickKeysMenu->activateQuickKey(index); } - bool WindowManager::getSubtitlesEnabled () + bool WindowManager::setHudVisibility(bool show) { - return mSubtitlesEnabled; - } - - bool WindowManager::toggleHud() - { - mHudEnabled = !mHudEnabled; + mHudEnabled = show; updateVisible(); + mMessageBoxManager->setVisible(mHudEnabled); return mHudEnabled; } bool WindowManager::getRestEnabled() { - //Enable rest dialogue if character creation finished - if(mRestAllowed==false && MWBase::Environment::get().getWorld()->getGlobalFloat ("chargenstate")==-1) - mRestAllowed=true; + // Enable rest dialogue if character creation finished + if (mRestAllowed == false + && MWBase::Environment::get().getWorld()->getGlobalFloat(MWWorld::Globals::sCharGenState) == -1) + mRestAllowed = true; return mRestAllowed; } - bool WindowManager::getPlayerSleeping () + bool WindowManager::getPlayerSleeping() { return mWaitDialog->getSleeping(); } @@ -1515,7 +1649,7 @@ namespace MWGui void WindowManager::addVisitedLocation(const std::string& name, int x, int y) { - mMap->addVisitedLocation (name, x, y); + mMap->addVisitedLocation(name, x, y); } const Translation::Storage& WindowManager::getTranslationDataStorage() const @@ -1523,7 +1657,7 @@ namespace MWGui return mTranslationDataStorage; } - void WindowManager::changePointer(const std::string &name) + void WindowManager::changePointer(const std::string& name) { MyGUI::PointerManager::getInstance().setPointer(name); onCursorChange(name); @@ -1548,28 +1682,31 @@ namespace MWGui } // Remove this wrapper once onKeyFocusChanged call is rendered unnecessary - void WindowManager::setKeyFocusWidget(MyGUI::Widget *widget) + void WindowManager::setKeyFocusWidget(MyGUI::Widget* widget) { MyGUI::InputManager::getInstance().setKeyFocusWidget(widget); onKeyFocusChanged(widget); } - void WindowManager::onKeyFocusChanged(MyGUI::Widget *widget) + void WindowManager::onKeyFocusChanged(MyGUI::Widget* widget) { - if (widget && widget->castType(false)) + bool isEditBox = widget && widget->castType(false); + LuaUi::WidgetExtension* luaWidget = dynamic_cast(widget); + bool capturesInput = luaWidget ? luaWidget->isTextInput() : isEditBox; + if (widget && capturesInput) SDL_StartTextInput(); else SDL_StopTextInput(); } - void WindowManager::setEnemy(const MWWorld::Ptr &enemy) + void WindowManager::setEnemy(const MWWorld::Ptr& enemy) { mHud->setEnemy(enemy); } - int WindowManager::getMessagesCount() const + std::size_t WindowManager::getMessagesCount() const { - int count = 0; + std::size_t count = 0; if (mMessageBoxManager) count = mMessageBoxManager->getMessagesCount(); @@ -1586,69 +1723,72 @@ namespace MWGui return mCursorVisible && mCursorActive; } - void WindowManager::trackWindow(Layout *layout, const std::string &name) + void WindowManager::trackWindow(Layout* layout, const WindowSettingValues& settings) { - std::string settingName = name; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - bool isMaximized = Settings::Manager::getBool(name + " maximized", "Windows"); - if (isMaximized) - settingName += " maximized"; - MyGUI::IntPoint pos(static_cast(Settings::Manager::getFloat(settingName + " x", "Windows") * viewSize.width), - static_cast(Settings::Manager::getFloat(settingName + " y", "Windows") * viewSize.height)); - MyGUI::IntSize size (static_cast(Settings::Manager::getFloat(settingName + " w", "Windows") * viewSize.width), - static_cast(Settings::Manager::getFloat(settingName + " h", "Windows") * viewSize.height)); - layout->mMainWidget->setPosition(pos); - layout->mMainWidget->setSize(size); + const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mMaximized : settings.mRegular; MyGUI::Window* window = layout->mMainWidget->castType(); + window->setPosition( + MyGUI::IntPoint(static_cast(rect.mX * viewSize.width), static_cast(rect.mY * viewSize.height))); + window->setSize( + MyGUI::IntSize(static_cast(rect.mW * viewSize.width), static_cast(rect.mH * viewSize.height))); + window->eventWindowChangeCoord += MyGUI::newDelegate(this, &WindowManager::onWindowChangeCoord); - mTrackedWindows[window] = name; + WindowBase::clampWindowCoordinates(window); + + mTrackedWindows.emplace(window, settings); } - void WindowManager::toggleMaximized(Layout *layout) + void WindowManager::toggleMaximized(Layout* layout) { MyGUI::Window* window = layout->mMainWidget->castType(); - std::string setting = mTrackedWindows[window]; - if (setting.empty()) + const auto it = mTrackedWindows.find(window); + if (it == mTrackedWindows.end()) return; - bool maximized = !Settings::Manager::getBool(setting + " maximized", "Windows"); - if (maximized) - setting += " maximized"; + const WindowSettingValues& settings = it->second; + const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mRegular : settings.mMaximized; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - float x = Settings::Manager::getFloat(setting + " x", "Windows") * float(viewSize.width); - float y = Settings::Manager::getFloat(setting + " y", "Windows") * float(viewSize.height); - float w = Settings::Manager::getFloat(setting + " w", "Windows") * float(viewSize.width); - float h = Settings::Manager::getFloat(setting + " h", "Windows") * float(viewSize.height); + const float x = rect.mX * viewSize.width; + const float y = rect.mY * viewSize.height; + const float w = rect.mW * viewSize.width; + const float h = rect.mH * viewSize.height; window->setCoord(x, y, w, h); - Settings::Manager::setBool(mTrackedWindows[window] + " maximized", "Windows", maximized); + + settings.mIsMaximized.set(!settings.mIsMaximized.get()); } - void WindowManager::onWindowChangeCoord(MyGUI::Window *_sender) + void WindowManager::onWindowChangeCoord(MyGUI::Window* window) { - std::string setting = mTrackedWindows[_sender]; + const auto it = mTrackedWindows.find(window); + if (it == mTrackedWindows.end()) + return; + + WindowBase::clampWindowCoordinates(window); + + const WindowSettingValues& settings = it->second; + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - float x = _sender->getPosition().left / float(viewSize.width); - float y = _sender->getPosition().top / float(viewSize.height); - float w = _sender->getSize().width / float(viewSize.width); - float h = _sender->getSize().height / float(viewSize.height); - Settings::Manager::setFloat(setting + " x", "Windows", x); - Settings::Manager::setFloat(setting + " y", "Windows", y); - Settings::Manager::setFloat(setting + " w", "Windows", w); - Settings::Manager::setFloat(setting + " h", "Windows", h); - bool maximized = Settings::Manager::getBool(setting + " maximized", "Windows"); - if (maximized) - Settings::Manager::setBool(setting + " maximized", "Windows", false); + settings.mRegular.mX.set(window->getPosition().left / static_cast(viewSize.width)); + settings.mRegular.mY.set(window->getPosition().top / static_cast(viewSize.height)); + settings.mRegular.mW.set(window->getSize().width / static_cast(viewSize.width)); + settings.mRegular.mH.set(window->getSize().height / static_cast(viewSize.height)); + + settings.mIsMaximized.set(false); } void WindowManager::clear() { mPlayerBounty = -1; - for (WindowBase* window : mWindows) + for (const auto& window : mWindows) + { window->clear(); + window->setDisabledByLua(false); + } if (mLocalMapRender) mLocalMapRender->clear(); @@ -1657,7 +1797,7 @@ namespace MWGui mToolTips->clear(); - mSelectedSpell.clear(); + mSelectedSpell = ESM::RefId(); mCustomMarkers.clear(); mForceHidden = GW_None; @@ -1669,7 +1809,7 @@ namespace MWGui updateVisible(); } - void WindowManager::write(ESM::ESMWriter &writer, Loading::Listener& progress) + void WindowManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) { mMap->write(writer, progress); @@ -1678,11 +1818,12 @@ namespace MWGui if (!mSelectedSpell.empty()) { writer.startRecord(ESM::REC_ASPL); - writer.writeHNString("ID__", mSelectedSpell); + writer.writeHNRefId("ID__", mSelectedSpell); writer.endRecord(ESM::REC_ASPL); } - for (CustomMarkerCollection::ContainerType::const_iterator it = mCustomMarkers.begin(); it != mCustomMarkers.end(); ++it) + for (CustomMarkerCollection::ContainerType::const_iterator it = mCustomMarkers.begin(); + it != mCustomMarkers.end(); ++it) { writer.startRecord(ESM::REC_MARK); it->second.save(writer); @@ -1690,7 +1831,7 @@ namespace MWGui } } - void WindowManager::readRecord(ESM::ESMReader &reader, uint32_t type) + void WindowManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_GMAP) mMap->readRecord(reader, type); @@ -1699,7 +1840,7 @@ namespace MWGui else if (type == ESM::REC_ASPL) { reader.getSubNameIs("ID__"); - std::string spell = reader.getHString(); + ESM::RefId spell = reader.getRefId(); if (mStore->get().search(spell)) mSelectedSpell = spell; } @@ -1714,22 +1855,21 @@ namespace MWGui int WindowManager::countSavedGameRecords() const { return 1 // Global map - + 1 // QuickKeysMenu - + mCustomMarkers.size() - + (!mSelectedSpell.empty() ? 1 : 0); + + 1 // QuickKeysMenu + + mCustomMarkers.size() + (!mSelectedSpell.empty() ? 1 : 0); } bool WindowManager::isSavingAllowed() const { return !MyGUI::InputManager::getInstance().isModalAny() - && !isConsoleMode() - // TODO: remove this, once we have properly serialized the state of open windows - && (!isGuiMode() || (mGuiModes.size() == 1 && (getMode() == GM_MainMenu || getMode() == GM_Rest))); + && !isConsoleMode() + // TODO: remove this, once we have properly serialized the state of open windows + && (!isGuiMode() || (mGuiModes.size() == 1 && (getMode() == GM_MainMenu || getMode() == GM_Rest))); } - void WindowManager::playVideo(const std::string &name, bool allowSkipping) + void WindowManager::playVideo(std::string_view name, bool allowSkipping, bool overrideSounds) { - mVideoWidget->playVideo("video\\" + name); + mVideoWidget->playVideo("video\\" + std::string{ name }); mVideoWidget->eventKeyButtonPressed.clear(); mVideoBackground->eventKeyButtonPressed.clear(); @@ -1752,15 +1892,17 @@ namespace MWGui bool cursorWasVisible = mCursorVisible; setCursorVisible(false); - if (mVideoWidget->hasAudioStream()) - MWBase::Environment::get().getSoundManager()->pauseSounds(MWSound::VideoPlayback, - ~MWSound::Type::Movie & MWSound::Type::Mask - ); + if (overrideSounds && mVideoWidget->hasAudioStream()) + MWBase::Environment::get().getSoundManager()->pauseSounds( + MWSound::VideoPlayback, ~MWSound::Type::Movie & MWSound::Type::Mask); - Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); + Misc::FrameRateLimiter frameRateLimiter + = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); while (mVideoWidget->update() && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) { - const double dt = std::chrono::duration_cast>(frameRateLimiter.getLastFrameDuration()).count(); + const double dt + = std::chrono::duration_cast>(frameRateLimiter.getLastFrameDuration()) + .count(); MWBase::Environment::get().getInputManager()->update(dt, true, false); @@ -1802,9 +1944,8 @@ namespace MWGui void WindowManager::sizeVideo(int screenWidth, int screenHeight) { // Use black bars to correct aspect ratio - bool stretch = Settings::Manager::getBool("stretch menu background", "GUI"); mVideoBackground->setSize(screenWidth, screenHeight); - mVideoWidget->autoResize(stretch); + mVideoWidget->autoResize(Settings::gui().mStretchMenuBackground); } void WindowManager::exitCurrentModal() @@ -1818,7 +1959,7 @@ namespace MWGui } } - void WindowManager::addCurrentModal(WindowModal *input) + void WindowManager::addCurrentModal(WindowModal* input) { if (mCurrentModals.empty()) mKeyboardNavigation->saveFocus(getMode()); @@ -1832,9 +1973,9 @@ namespace MWGui void WindowManager::removeCurrentModal(WindowModal* input) { - if(!mCurrentModals.empty()) + if (!mCurrentModals.empty()) { - if(input == mCurrentModals.back()) + if (input == mCurrentModals.back()) { mCurrentModals.pop_back(); mKeyboardNavigation->saveFocus(-1); @@ -1857,7 +1998,7 @@ namespace MWGui mKeyboardNavigation->setModalWindow(mCurrentModals.back()->mMainWidget); } - void WindowManager::onVideoKeyPressed(MyGUI::Widget *_sender, MyGUI::KeyCode _key, MyGUI::Char _char) + void WindowManager::onVideoKeyPressed(MyGUI::Widget* _sender, MyGUI::KeyCode _key, MyGUI::Char _char) { if (_key == MyGUI::KeyCode::Escape) mVideoWidget->stop(); @@ -1865,20 +2006,20 @@ namespace MWGui void WindowManager::updatePinnedWindows() { - mInventoryWindow->setPinned(Settings::Manager::getBool("inventory pin", "Windows")); - if (Settings::Manager::getBool("inventory hidden", "Windows")) + mInventoryWindow->setPinned(Settings::windows().mInventoryPin); + if (Settings::windows().mInventoryHidden) mShown = (GuiWindow)(mShown ^ GW_Inventory); - mMap->setPinned(Settings::Manager::getBool("map pin", "Windows")); - if (Settings::Manager::getBool("map hidden", "Windows")) + mMap->setPinned(Settings::windows().mMapPin); + if (Settings::windows().mMapHidden) mShown = (GuiWindow)(mShown ^ GW_Map); - mSpellWindow->setPinned(Settings::Manager::getBool("spells pin", "Windows")); - if (Settings::Manager::getBool("spells hidden", "Windows")) + mSpellWindow->setPinned(Settings::windows().mSpellsPin); + if (Settings::windows().mSpellsHidden) mShown = (GuiWindow)(mShown ^ GW_Magic); - mStatsWindow->setPinned(Settings::Manager::getBool("stats pin", "Windows")); - if (Settings::Manager::getBool("stats hidden", "Windows")) + mStatsWindow->setPinned(Settings::windows().mStatsPin); + if (Settings::windows().mStatsHidden) mShown = (GuiWindow)(mShown ^ GW_Stats); } @@ -1886,20 +2027,20 @@ namespace MWGui { switch (window) { - case GW_Inventory: - mInventoryWindow->setPinned(true); - break; - case GW_Map: - mMap->setPinned(true); - break; - case GW_Magic: - mSpellWindow->setPinned(true); - break; - case GW_Stats: - mStatsWindow->setPinned(true); - break; - default: - break; + case GW_Inventory: + mInventoryWindow->setPinned(true); + break; + case GW_Map: + mMap->setPinned(true); + break; + case GW_Magic: + mSpellWindow->setPinned(true); + break; + case GW_Stats: + mStatsWindow->setPinned(true); + break; + default: + break; } updateVisible(); @@ -1933,7 +2074,7 @@ namespace MWGui void WindowManager::activateHitOverlay(bool interrupt) { - if (!mHitFaderEnabled) + if (!Settings::gui().mHitFader) return; if (!interrupt && !mHitFader->isEmpty()) @@ -1946,24 +2087,24 @@ namespace MWGui void WindowManager::setWerewolfOverlay(bool set) { - if (!mWerewolfOverlayEnabled) + if (!Settings::gui().mWerewolfOverlay) return; if (mWerewolfFader) mWerewolfFader->notifyAlphaChanged(set ? 1.0f : 0.0f); } - void WindowManager::onClipboardChanged(const std::string &_type, const std::string &_data) + void WindowManager::onClipboardChanged(std::string_view _type, std::string_view _data) { if (_type == "Text") SDL_SetClipboardText(MyGUI::TextIterator::getOnlyText(MyGUI::UString(_data)).asUTF8().c_str()); } - void WindowManager::onClipboardRequested(const std::string &_type, std::string &_data) + void WindowManager::onClipboardRequested(std::string_view _type, std::string& _data) { if (_type != "Text") return; - char* text=nullptr; + char* text = nullptr; text = SDL_GetClipboardText(); if (text) _data = MyGUI::TextIterator::toTagsString(text); @@ -1988,9 +2129,43 @@ namespace MWGui void WindowManager::toggleDebugWindow() { -#ifndef BT_NO_PROFILE mDebugWindow->setVisible(!mDebugWindow->isVisible()); -#endif + } + + void WindowManager::togglePostProcessorHud() + { + if (!MWBase::Environment::get().getWorld()->getPostProcessor()->isEnabled()) + { + messageBox("#{OMWEngine:PostProcessingIsNotEnabled}"); + return; + } + + bool visible = mPostProcessorHud->isVisible(); + + if (!visible && !mGuiModes.empty()) + mKeyboardNavigation->saveFocus(mGuiModes.back()); + + mPostProcessorHud->setVisible(!visible); + + if (visible && !mGuiModes.empty()) + mKeyboardNavigation->restoreFocus(mGuiModes.back()); + + updateVisible(); + } + + void WindowManager::toggleSettingsWindow() + { + bool visible = mSettingsWindow->isVisible(); + + if (!visible && !mGuiModes.empty()) + mKeyboardNavigation->saveFocus(mGuiModes.back()); + + mSettingsWindow->setVisible(!visible); + + if (visible && !mGuiModes.empty()) + mKeyboardNavigation->restoreFocus(mGuiModes.back()); + + updateVisible(); } void WindowManager::cycleSpell(bool next) @@ -2005,12 +2180,13 @@ namespace MWGui mInventoryWindow->cycle(next); } - void WindowManager::playSound(const std::string& soundId, float volume, float pitch) + void WindowManager::playSound(const ESM::RefId& soundId, float volume, float pitch) { if (soundId.empty()) return; - MWBase::Environment::get().getSoundManager()->playSound(soundId, volume, pitch, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); + MWBase::Environment::get().getSoundManager()->playSound( + soundId, volume, pitch, MWSound::Type::Sfx, MWSound::PlayMode::NoEnvNoScaling); } void WindowManager::updateSpellWindow() @@ -2019,40 +2195,33 @@ namespace MWGui mSpellWindow->updateSpells(); } - void WindowManager::setConsoleSelectedObject(const MWWorld::Ptr &object) + void WindowManager::setConsoleSelectedObject(const MWWorld::Ptr& object) { mConsole->setSelectedObject(object); } - std::string WindowManager::correctIconPath(const std::string& path) + MWWorld::Ptr WindowManager::getConsoleSelectedObject() const { - return Misc::ResourceHelpers::correctIconPath(path, mResourceSystem->getVFS()); + return mConsole->getSelectedObject(); } - std::string WindowManager::correctBookartPath(const std::string& path, int width, int height, bool* exists) + void WindowManager::printToConsole(const std::string& msg, std::string_view color) { - std::string corrected = Misc::ResourceHelpers::correctBookartPath(path, width, height, mResourceSystem->getVFS()); - if (exists) - *exists = mResourceSystem->getVFS()->exists(corrected); - return corrected; + mConsole->print(msg, color); } - std::string WindowManager::correctTexturePath(const std::string& path) + void WindowManager::setConsoleMode(std::string_view mode) { - return Misc::ResourceHelpers::correctTexturePath(path, mResourceSystem->getVFS()); + mConsole->setConsoleMode(mode); } - bool WindowManager::textureExists(const std::string &path) + const std::string& WindowManager::getConsoleMode() { - std::string corrected = Misc::ResourceHelpers::correctTexturePath(path, mResourceSystem->getVFS()); - return mResourceSystem->getVFS()->exists(corrected); + return mConsole->getConsoleMode(); } void WindowManager::createCursors() { - // FIXME: currently we do not scale cursor since it is not a MyGUI widget. - // In theory, we can do it manually (rescale the cursor image via osg::Imag::scaleImage() and scale the hotspot position). - // Unfortunately, this apploach can lead to driver crashes on some setups (e.g. on laptops with nvidia-prime on Linux). MyGUI::ResourceManager::EnumeratorPtr enumerator = MyGUI::ResourceManager::getInstance().getEnumerator(); while (enumerator.next()) { @@ -2060,18 +2229,21 @@ namespace MWGui ResourceImageSetPointerFix* imgSetPointer = resource->castType(false); if (!imgSetPointer) continue; - std::string tex_name = imgSetPointer->getImageSet()->getIndexInfo(0,0).texture; - osg::ref_ptr image = mResourceSystem->getImageManager()->getImage(tex_name); + const VFS::Path::Normalized path(imgSetPointer->getImageSet()->getIndexInfo(0, 0).texture); + + osg::ref_ptr image = mResourceSystem->getImageManager()->getImage(path); - if(image.valid()) + if (image.valid()) { - //everything looks good, send it to the cursor manager + // everything looks good, send it to the cursor manager Uint8 hotspot_x = imgSetPointer->getHotSpot().left; Uint8 hotspot_y = imgSetPointer->getHotSpot().top; int rotation = imgSetPointer->getRotation(); + MyGUI::IntSize pointerSize = imgSetPointer->getSize(); - mCursorManager->createCursor(imgSetPointer->getResourceName(), rotation, image, hotspot_x, hotspot_y); + mCursorManager->createCursor(imgSetPointer->getResourceName(), rotation, image, hotspot_x, hotspot_y, + pointerSize.width, pointerSize.height); } } } @@ -2082,8 +2254,8 @@ namespace MWGui MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().createTexture("white"); tex->createManual(8, 8, MyGUI::TextureUsage::Write, MyGUI::PixelFormat::R8G8B8); unsigned char* data = reinterpret_cast(tex->lock(MyGUI::TextureUsage::Write)); - for (int x=0; x<8; ++x) - for (int y=0; y<8; ++y) + for (int x = 0; x < 8; ++x) + for (int y = 0; y < 8; ++y) { *(data++) = 255; *(data++) = 255; @@ -2096,8 +2268,8 @@ namespace MWGui MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().createTexture("black"); tex->createManual(8, 8, MyGUI::TextureUsage::Write, MyGUI::PixelFormat::R8G8B8); unsigned char* data = reinterpret_cast(tex->lock(MyGUI::TextureUsage::Write)); - for (int x=0; x<8; ++x) - for (int y=0; y<8; ++y) + for (int x = 0; x < 8; ++x) + for (int y = 0; y < 8; ++y) { *(data++) = 0; *(data++) = 0; @@ -2109,7 +2281,7 @@ namespace MWGui { MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().createTexture("transparent"); tex->createManual(8, 8, MyGUI::TextureUsage::Write, MyGUI::PixelFormat::R8G8B8A8); - setMenuTransparency(Settings::Manager::getFloat("menu transparency", "GUI")); + setMenuTransparency(Settings::gui().mMenuTransparency); } } @@ -2117,13 +2289,13 @@ namespace MWGui { MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().getTexture("transparent"); unsigned char* data = reinterpret_cast(tex->lock(MyGUI::TextureUsage::Write)); - for (int x=0; x<8; ++x) - for (int y=0; y<8; ++y) + for (int x = 0; x < 8; ++x) + for (int y = 0; y < 8; ++y) { *(data++) = 255; *(data++) = 255; *(data++) = 255; - *(data++) = static_cast(value*255); + *(data++) = static_cast(value * 255); } tex->unlock(); } @@ -2133,12 +2305,12 @@ namespace MWGui mLocalMapRender->addCell(cell); } - void WindowManager::removeCell(MWWorld::CellStore *cell) + void WindowManager::removeCell(MWWorld::CellStore* cell) { mLocalMapRender->removeCell(cell); } - void WindowManager::writeFog(MWWorld::CellStore *cell) + void WindowManager::writeFog(MWWorld::CellStore* cell) { mLocalMapRender->saveFogOfWar(cell); } @@ -2161,16 +2333,16 @@ namespace MWGui { switch (key.getValue()) { - case MyGUI::KeyCode::ArrowDown: - case MyGUI::KeyCode::ArrowUp: - case MyGUI::KeyCode::ArrowLeft: - case MyGUI::KeyCode::ArrowRight: - case MyGUI::KeyCode::Return: - case MyGUI::KeyCode::NumpadEnter: - case MyGUI::KeyCode::Space: - return true; - default: - return false; + case MyGUI::KeyCode::ArrowDown: + case MyGUI::KeyCode::ArrowUp: + case MyGUI::KeyCode::ArrowLeft: + case MyGUI::KeyCode::ArrowRight: + case MyGUI::KeyCode::Return: + case MyGUI::KeyCode::NumpadEnter: + case MyGUI::KeyCode::Space: + return true; + default: + return false; } } return false; @@ -2186,8 +2358,8 @@ namespace MWGui void WindowManager::GuiModeState::update(bool visible) { - for (unsigned int i=0; isetVisible(visible); + for (const auto& window : mWindows) + window->setVisible(visible); } void WindowManager::watchActor(const MWWorld::Ptr& ptr) @@ -2199,4 +2371,69 @@ namespace MWGui { return mStatsWatcher->getWatchedActor(); } + + const std::string& WindowManager::getVersionDescription() const + { + return mVersionDescription; + } + + void WindowManager::handleScheduledMessageBoxes() + { + const auto scheduledMessageBoxes = mScheduledMessageBoxes.lock(); + for (const ScheduledMessageBox& v : *scheduledMessageBoxes) + messageBox(v.mMessage, v.mShowInDialogueMode); + scheduledMessageBoxes->clear(); + } + + void WindowManager::onDeleteCustomData(const MWWorld::Ptr& ptr) + { + for (const auto& window : mWindows) + window->onDeleteCustomData(ptr); + } + + void WindowManager::asyncPrepareSaveMap() + { + mMap->asyncPrepareSaveMap(); + } + + void WindowManager::setDisabledByLua(std::string_view windowId, bool disabled) + { + mLuaIdToWindow.at(windowId)->setDisabledByLua(disabled); + updateVisible(); + } + + std::vector WindowManager::getAllWindowIds() const + { + std::vector res; + for (const auto& [id, _] : mLuaIdToWindow) + res.push_back(id); + return res; + } + + std::vector WindowManager::getAllowedWindowIds(GuiMode mode) const + { + std::vector res; + if (mode == GM_Inventory) + { + if (mAllowed & GW_Map) + res.push_back(mMap->getWindowIdForLua()); + if (mAllowed & GW_Inventory) + res.push_back(mInventoryWindow->getWindowIdForLua()); + if (mAllowed & GW_Magic) + res.push_back(mSpellWindow->getWindowIdForLua()); + if (mAllowed & GW_Stats) + res.push_back(mStatsWindow->getWindowIdForLua()); + } + else + { + auto it = mGuiModeStates.find(mode); + if (it != mGuiModeStates.end()) + { + for (const auto* w : it->second.mWindows) + if (!w->getWindowIdForLua().empty()) + res.push_back(w->getWindowIdForLua()); + } + } + return res; + } } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 5a0d89ce10b..f7c778e10a4 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -7,26 +7,41 @@ and retrieving information from the Gui. **/ +#include #include +#include #include #include "../mwbase/windowmanager.hpp" +#include "../mwrender/localmap.hpp" +#include +#include #include +#include +#include #include #include +#include "charactercreation.hpp" +#include "draganddrop.hpp" #include "mapwindow.hpp" +#include "messagebox.hpp" +#include "settings.hpp" +#include "soulgemdialog.hpp" #include "statswatcher.hpp" #include "textcolours.hpp" +#include "tooltips.hpp" +#include "windowbase.hpp" +#include #include #include +#include namespace MyGUI { - class Gui; class Widget; class Window; class UString; @@ -35,6 +50,7 @@ namespace MyGUI namespace MWWorld { + class Cell; class ESMStore; } @@ -67,493 +83,516 @@ namespace SceneUtil class WorkQueue; } -namespace SDLUtil -{ - class SDLCursorManager; - class VideoWrapper; -} - -namespace osgMyGUI -{ - class Platform; -} - namespace Gui { class FontLoader; } -namespace MWRender -{ - class LocalMap; -} - namespace MWGui { - class WindowBase; - class HUD; - class MapWindow; - class MainMenu; - class StatsWindow; - class InventoryWindow; - struct JournalWindow; - class CharacterCreation; - class DragAndDrop; - class ToolTips; - class TextInputDialog; - class InfoBoxDialog; - class MessageBoxManager; - class SettingsWindow; - class AlchemyWindow; - class QuickKeysMenu; - class LoadingScreen; - class LevelupDialog; - class WaitDialog; - class SpellCreationDialog; - class EnchantingDialog; - class TrainingWindow; - class SpellIcons; - class MerchantRepair; - class SoulgemDialog; - class Recharge; - class CompanionWindow; - class VideoWidget; - class WindowModal; - class ScreenFader; - class DebugWindow; - class JailScreen; - class KeyboardNavigation; - - class WindowManager : - public MWBase::WindowManager - { - public: - typedef std::pair Faction; - typedef std::vector FactionList; - - WindowManager(SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, - const std::string& logpath, const std::string& cacheDir, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, - ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& localPath); - virtual ~WindowManager(); - - /// Set the ESMStore to use for retrieving of GUI-related strings. - void setStore (const MWWorld::ESMStore& store); - - void initUI(); - void loadUserFonts() override; - - Loading::Listener* getLoadingScreen() override; - - /// @note This method will block until the video finishes playing - /// (and will continually update the window while doing so) - void playVideo(const std::string& name, bool allowSkipping) override; - - /// Warning: do not use MyGUI::InputManager::setKeyFocusWidget directly. Instead use this. - void setKeyFocusWidget (MyGUI::Widget* widget) override; - - void setNewGame(bool newgame) override; - - void pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg) override; - void pushGuiMode (GuiMode mode) override; - void popGuiMode(bool noSound=false) override; - void removeGuiMode(GuiMode mode, bool noSound=false) override; ///< can be anywhere in the stack - - void goToJail(int days) override; - - GuiMode getMode() const override; - bool containsMode(GuiMode mode) const override; - - bool isGuiMode() const override; - - bool isConsoleMode() const override; - - void toggleVisible(GuiWindow wnd) override; - - void forceHide(MWGui::GuiWindow wnd) override; - void unsetForceHide(MWGui::GuiWindow wnd) override; - - /// Disallow all inventory mode windows - void disallowAll() override; - - /// Allow one or more windows - void allow(GuiWindow wnd) override; - - bool isAllowed(GuiWindow wnd) const override; - - /// \todo investigate, if we really need to expose every single lousy UI element to the outside world - MWGui::InventoryWindow* getInventoryWindow() override; - MWGui::CountDialog* getCountDialog() override; - MWGui::ConfirmationDialog* getConfirmationDialog() override; - MWGui::TradeWindow* getTradeWindow() override; - - /// Make the player use an item, while updating GUI state accordingly - void useItem(const MWWorld::Ptr& item, bool bypassBeastRestrictions=false) override; - - void updateSpellWindow() override; - - void setConsoleSelectedObject(const MWWorld::Ptr& object) override; - - /// Set time left for the player to start drowning (update the drowning bar) - /// @param time time left to start drowning - /// @param maxTime how long we can be underwater (in total) until drowning starts - void setDrowningTimeLeft (float time, float maxTime) override; + class HUD; + class MapWindow; + class MainMenu; + class StatsWindow; + class InventoryWindow; + struct JournalWindow; + class TextInputDialog; + class InfoBoxDialog; + class SettingsWindow; + class AlchemyWindow; + class QuickKeysMenu; + class LoadingScreen; + class LevelupDialog; + class WaitDialog; + class SpellCreationDialog; + class EnchantingDialog; + class TrainingWindow; + class SpellIcons; + class MerchantRepair; + class Recharge; + class CompanionWindow; + class VideoWidget; + class WindowModal; + class ScreenFader; + class DebugWindow; + class PostProcessorHud; + class JailScreen; + class KeyboardNavigation; + + class WindowManager : public MWBase::WindowManager + { + public: + typedef std::pair Faction; + typedef std::vector FactionList; - void changeCell(const MWWorld::CellStore* cell) override; ///< change the active cell + WindowManager(SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, + Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, + const std::filesystem::path& logpath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, + ToUTF8::FromType encoding, const std::string& versionDescription, bool useShaders, + Files::ConfigurationManager& cfgMgr); + virtual ~WindowManager(); - void setFocusObject(const MWWorld::Ptr& focus) override; - void setFocusObjectScreenCoords(float min_x, float min_y, float max_x, float max_y) override; + /// Set the ESMStore to use for retrieving of GUI-related strings. + void setStore(const MWWorld::ESMStore& store); - void getMousePosition(int &x, int &y) override; - void getMousePosition(float &x, float &y) override; - void setDragDrop(bool dragDrop) override; - bool getWorldMouseOver() override; + void initUI(); - float getScalingFactor() override; + Loading::Listener* getLoadingScreen() override; - bool toggleFogOfWar() override; - bool toggleFullHelp() override; ///< show extra info in item tooltips (owner, script) - bool getFullHelp() const override; + /// @note This method will block until the video finishes playing + /// (and will continually update the window while doing so) + void playVideo(std::string_view name, bool allowSkipping, bool overrideSounds = true) override; - void setActiveMap(int x, int y, bool interior) override; - ///< set the indices of the map texture that should be used + /// Warning: do not use MyGUI::InputManager::setKeyFocusWidget directly. Instead use this. + void setKeyFocusWidget(MyGUI::Widget* widget) override; - /// sets the visibility of the drowning bar - void setDrowningBarVisibility(bool visible) override; + void setNewGame(bool newgame) override; - // sets the visibility of the hud health/magicka/stamina bars - void setHMSVisibility(bool visible) override; - // sets the visibility of the hud minimap - void setMinimapVisibility(bool visible) override; - void setWeaponVisibility(bool visible) override; - void setSpellVisibility(bool visible) override; - void setSneakVisibility(bool visible) override; + void pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg) override; + void pushGuiMode(GuiMode mode) override; + void popGuiMode(bool forceExit = false) override; + void removeGuiMode(GuiMode mode) override; ///< can be anywhere in the stack - /// activate selected quick key - void activateQuickKey (int index) override; - /// update activated quick key state (if action executing was delayed for some reason) - void updateActivatedQuickKey () override; + void goToJail(int days) override; - std::string getSelectedSpell() override { return mSelectedSpell; } - void setSelectedSpell(const std::string& spellId, int successChancePercent) override; - void setSelectedEnchantItem(const MWWorld::Ptr& item) override; - const MWWorld::Ptr& getSelectedEnchantItem() const override; - void setSelectedWeapon(const MWWorld::Ptr& item) override; - const MWWorld::Ptr& getSelectedWeapon() const override; - int getFontHeight() const override; - void unsetSelectedSpell() override; - void unsetSelectedWeapon() override; + GuiMode getMode() const override; + bool containsMode(GuiMode mode) const override; - void updateConsoleObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) override; + bool isGuiMode() const override; - void showCrosshair(bool show) override; - bool getSubtitlesEnabled() override; + bool isConsoleMode() const override; + bool isPostProcessorHudVisible() const override; + bool isSettingsWindowVisible() const override; + bool isInteractiveMessageBoxActive() const override; - /// Turn visibility of HUD on or off - bool toggleHud() override; + void toggleVisible(GuiWindow wnd) override; - void disallowMouse() override; - void allowMouse() override; - void notifyInputActionBound() override; + void forceHide(MWGui::GuiWindow wnd) override; + void unsetForceHide(MWGui::GuiWindow wnd) override; - void addVisitedLocation(const std::string& name, int x, int y) override; + /// Disallow all inventory mode windows + void disallowAll() override; - ///Hides dialog and schedules dialog to be deleted. - void removeDialog(Layout* dialog) override; + /// Allow one or more windows + void allow(GuiWindow wnd) override; - ///Gracefully attempts to exit the topmost GUI mode - void exitCurrentGuiMode() override; + bool isAllowed(GuiWindow wnd) const override; - void messageBox (const std::string& message, enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) override; - void staticMessageBox(const std::string& message) override; - void removeStaticMessageBox() override; - void interactiveMessageBox (const std::string& message, - const std::vector& buttons = std::vector(), bool block=false) override; + /// \todo investigate, if we really need to expose every single lousy UI element to the outside world + MWGui::InventoryWindow* getInventoryWindow() override; + MWGui::CountDialog* getCountDialog() override; + MWGui::ConfirmationDialog* getConfirmationDialog() override; + MWGui::TradeWindow* getTradeWindow() override; + MWGui::PostProcessorHud* getPostProcessorHud() override; - int readPressedButton () override; ///< returns the index of the pressed button or -1 if no button was pressed (->MessageBoxmanager->InteractiveMessageBox) + /// Make the player use an item, while updating GUI state accordingly + void useItem(const MWWorld::Ptr& item, bool bypassBeastRestrictions = false) override; - void update (float duration) override; + void updateSpellWindow() override; - /** - * Fetches a GMST string from the store, if there is no setting with the given - * ID or it is not a string the default string is returned. - * - * @param id Identifier for the GMST setting, e.g. "aName" - * @param default Default value if the GMST setting cannot be used. - */ - std::string getGameSettingString(const std::string &id, const std::string &default_) override; + void setConsoleSelectedObject(const MWWorld::Ptr& object) override; + MWWorld::Ptr getConsoleSelectedObject() const override; + void printToConsole(const std::string& msg, std::string_view color) override; + void setConsoleMode(std::string_view mode) override; + const std::string& getConsoleMode() override; - void processChangedSettings(const Settings::CategorySettingVector& changed) override; + /// Set time left for the player to start drowning (update the drowning bar) + /// @param time time left to start drowning + /// @param maxTime how long we can be underwater (in total) until drowning starts + void setDrowningTimeLeft(float time, float maxTime) override; - void windowVisibilityChange(bool visible) override; - void windowResized(int x, int y) override; - void windowClosed() override; - bool isWindowVisible() override; + void changeCell(const MWWorld::CellStore* cell) override; ///< change the active cell - void watchActor(const MWWorld::Ptr& ptr) override; - MWWorld::Ptr getWatchedActor() const override; + void setFocusObject(const MWWorld::Ptr& focus) override; + void setFocusObjectScreenCoords(float min_x, float min_y, float max_x, float max_y) override; - void executeInConsole (const std::string& path) override; + void getMousePosition(int& x, int& y) override; + void getMousePosition(float& x, float& y) override; + void setDragDrop(bool dragDrop) override; + bool getWorldMouseOver() override; - void enableRest() override { mRestAllowed = true; } - bool getRestEnabled() override; + float getScalingFactor() const override; - bool getJournalAllowed() override { return (mAllowed & GW_Magic) != 0; } + bool toggleFogOfWar() override; + bool toggleFullHelp() override; ///< show extra info in item tooltips (owner, script) + bool getFullHelp() const override; - bool getPlayerSleeping() override; - void wakeUpPlayer() override; + /// sets the visibility of the drowning bar + void setDrowningBarVisibility(bool visible) override; - void updatePlayer() override; + // sets the visibility of the hud health/magicka/stamina bars + void setHMSVisibility(bool visible) override; + // sets the visibility of the hud minimap + void setMinimapVisibility(bool visible) override; + void setWeaponVisibility(bool visible) override; + void setSpellVisibility(bool visible) override; + void setSneakVisibility(bool visible) override; - void showSoulgemDialog (MWWorld::Ptr item) override; + /// activate selected quick key + void activateQuickKey(int index) override; + /// update activated quick key state (if action executing was delayed for some reason) + void updateActivatedQuickKey() override; - void changePointer (const std::string& name) override; + const ESM::RefId& getSelectedSpell() override { return mSelectedSpell; } + void setSelectedSpell(const ESM::RefId& spellId, int successChancePercent) override; + void setSelectedEnchantItem(const MWWorld::Ptr& item) override; + const MWWorld::Ptr& getSelectedEnchantItem() const override; + void setSelectedWeapon(const MWWorld::Ptr& item) override; + const MWWorld::Ptr& getSelectedWeapon() const override; + void unsetSelectedSpell() override; + void unsetSelectedWeapon() override; - void setEnemy (const MWWorld::Ptr& enemy) override; + void updateConsoleObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) override; - int getMessagesCount() const override; + void showCrosshair(bool show) override; - const Translation::Storage& getTranslationDataStorage() const override; + /// Turn visibility of HUD on or off + bool setHudVisibility(bool show) override; + bool isHudVisible() const override { return mHudEnabled; } - void onSoulgemDialogButtonPressed (int button); + void disallowMouse() override; + void allowMouse() override; + void notifyInputActionBound() override; - bool getCursorVisible() override; + void addVisitedLocation(const std::string& name, int x, int y) override; - /// Call when mouse cursor or buttons are used. - void setCursorActive(bool active) override; + /// Hides dialog and schedules dialog to be deleted. + void removeDialog(std::unique_ptr&& dialog) override; - /// Clear all savegame-specific data - void clear() override; + /// Gracefully attempts to exit the topmost GUI mode + void exitCurrentGuiMode() override; - void write (ESM::ESMWriter& writer, Loading::Listener& progress) override; - void readRecord (ESM::ESMReader& reader, uint32_t type) override; - int countSavedGameRecords() const override; + void messageBox(std::string_view message, + enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) override; + void scheduleMessageBox(std::string message, + enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) override; + void staticMessageBox(std::string_view message) override; + void removeStaticMessageBox() override; + void interactiveMessageBox(std::string_view message, const std::vector& buttons = {}, + bool block = false, int defaultFocus = -1) override; - /// Does the current stack of GUI-windows permit saving? - bool isSavingAllowed() const override; + int readPressedButton() override; ///< returns the index of the pressed button or -1 if no button was pressed + ///< (->MessageBoxmanager->InteractiveMessageBox) - /// Send exit command to active Modal window **/ - void exitCurrentModal() override; + void update(float duration); - /// Sets the current Modal - /** Used to send exit command to active Modal when Esc is pressed **/ - void addCurrentModal(WindowModal* input) override; + /** + * Fetches a GMST string from the store, if there is no setting with the given + * ID or it is not a string the default string is returned. + * + * @param id Identifier for the GMST setting, e.g. "aName" + * @param default Default value if the GMST setting cannot be used. + */ + std::string_view getGameSettingString(std::string_view id, std::string_view default_) override; - /// Removes the top Modal - /** Used when one Modal adds another Modal - \param input Pointer to the current modal, to ensure proper modal is removed **/ - void removeCurrentModal(WindowModal* input) override; + void processChangedSettings(const Settings::CategorySettingVector& changed) override; - void pinWindow (MWGui::GuiWindow window) override; - void toggleMaximized(Layout *layout) override; + void windowVisibilityChange(bool visible) override; + void windowResized(int x, int y) override; + void windowClosed() override; + bool isWindowVisible() override; - /// Fade the screen in, over \a time seconds - void fadeScreenIn(const float time, bool clearQueue, float delay) override; - /// Fade the screen out to black, over \a time seconds - void fadeScreenOut(const float time, bool clearQueue, float delay) override; - /// Fade the screen to a specified percentage of black, over \a time seconds - void fadeScreenTo(const int percent, const float time, bool clearQueue, float delay) override; - /// Darken the screen to a specified percentage - void setBlindness(const int percent) override; + void watchActor(const MWWorld::Ptr& ptr) override; + MWWorld::Ptr getWatchedActor() const override; - void activateHitOverlay(bool interrupt) override; - void setWerewolfOverlay(bool set) override; + void executeInConsole(const std::filesystem::path& path) override; - void toggleConsole() override; - void toggleDebugWindow() override; + void enableRest() override { mRestAllowed = true; } + bool getRestEnabled() override; - /// Cycle to next or previous spell - void cycleSpell(bool next) override; - /// Cycle to next or previous weapon - void cycleWeapon(bool next) override; + bool getJournalAllowed() override { return (mAllowed & GW_Magic) != 0; } - void playSound(const std::string& soundId, float volume = 1.f, float pitch = 1.f) override; + bool getPlayerSleeping() override; + void wakeUpPlayer() override; - // In WindowManager for now since there isn't a VFS singleton - std::string correctIconPath(const std::string& path) override; - std::string correctBookartPath(const std::string& path, int width, int height, bool* exists = nullptr) override; - std::string correctTexturePath(const std::string& path) override; - bool textureExists(const std::string& path) override; + void updatePlayer() override; - void addCell(MWWorld::CellStore* cell) override; - void removeCell(MWWorld::CellStore* cell) override; - void writeFog(MWWorld::CellStore* cell) override; + void showSoulgemDialog(MWWorld::Ptr item) override; - const MWGui::TextColours& getTextColours() override; + void changePointer(const std::string& name) override; - bool injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat=false) override; - bool injectKeyRelease(MyGUI::KeyCode key) override; + void setEnemy(const MWWorld::Ptr& enemy) override; - private: - unsigned int mOldUpdateMask; unsigned int mOldCullMask; + std::size_t getMessagesCount() const override; - const MWWorld::ESMStore* mStore; - Resource::ResourceSystem* mResourceSystem; - osg::ref_ptr mWorkQueue; + const Translation::Storage& getTranslationDataStorage() const override; - osgMyGUI::Platform* mGuiPlatform; - osgViewer::Viewer* mViewer; + void onSoulgemDialogButtonPressed(int button); - std::unique_ptr mFontLoader; - std::unique_ptr mStatsWatcher; + bool getCursorVisible() override; - bool mConsoleOnlyScripts; + /// Call when mouse cursor or buttons are used. + void setCursorActive(bool active) override; - std::map mTrackedWindows; - void trackWindow(Layout* layout, const std::string& name); - void onWindowChangeCoord(MyGUI::Window* _sender); + /// Clear all savegame-specific data + void clear() override; - std::string mSelectedSpell; - MWWorld::Ptr mSelectedEnchantItem; - MWWorld::Ptr mSelectedWeapon; + void write(ESM::ESMWriter& writer, Loading::Listener& progress) override; + void readRecord(ESM::ESMReader& reader, uint32_t type) override; + int countSavedGameRecords() const override; - std::vector mCurrentModals; + /// Does the current stack of GUI-windows permit saving? + bool isSavingAllowed() const override; - // Markers placed manually by the player. Must be shared between both map views (the HUD map and the map window). - CustomMarkerCollection mCustomMarkers; + /// Send exit command to active Modal window **/ + void exitCurrentModal() override; - HUD *mHud; - MapWindow *mMap; - MWRender::LocalMap* mLocalMapRender; - ToolTips *mToolTips; - StatsWindow *mStatsWindow; - MessageBoxManager *mMessageBoxManager; - Console *mConsole; - DialogueWindow *mDialogueWindow; - DragAndDrop* mDragAndDrop; - InventoryWindow *mInventoryWindow; - ScrollWindow* mScrollWindow; - BookWindow* mBookWindow; - CountDialog* mCountDialog; - TradeWindow* mTradeWindow; - SettingsWindow* mSettingsWindow; - ConfirmationDialog* mConfirmationDialog; - SpellWindow* mSpellWindow; - QuickKeysMenu* mQuickKeysMenu; - LoadingScreen* mLoadingScreen; - WaitDialog* mWaitDialog; - SoulgemDialog* mSoulgemDialog; - MyGUI::ImageBox* mVideoBackground; - VideoWidget* mVideoWidget; - ScreenFader* mWerewolfFader; - ScreenFader* mBlindnessFader; - ScreenFader* mHitFader; - ScreenFader* mScreenFader; - DebugWindow* mDebugWindow; - JailScreen* mJailScreen; - - std::vector mWindows; - - Translation::Storage& mTranslationDataStorage; - - CharacterCreation* mCharGen; - - MyGUI::Widget* mInputBlocker; - - bool mCrosshairEnabled; - bool mSubtitlesEnabled; - bool mHitFaderEnabled; - bool mWerewolfOverlayEnabled; - bool mHudEnabled; - bool mCursorVisible; - bool mCursorActive; - - int mPlayerBounty; - - void setCursorVisible(bool visible) override; - - MyGUI::Gui *mGui; // Gui - - struct GuiModeState - { - GuiModeState(WindowBase* window) + /// Sets the current Modal + /** Used to send exit command to active Modal when Esc is pressed **/ + void addCurrentModal(WindowModal* input) override; + + /// Removes the top Modal + /** Used when one Modal adds another Modal + \param input Pointer to the current modal, to ensure proper modal is removed **/ + void removeCurrentModal(WindowModal* input) override; + + void pinWindow(MWGui::GuiWindow window) override; + void toggleMaximized(Layout* layout) override; + + /// Fade the screen in, over \a time seconds + void fadeScreenIn(const float time, bool clearQueue, float delay) override; + /// Fade the screen out to black, over \a time seconds + void fadeScreenOut(const float time, bool clearQueue, float delay) override; + /// Fade the screen to a specified percentage of black, over \a time seconds + void fadeScreenTo(const int percent, const float time, bool clearQueue, float delay) override; + /// Darken the screen to a specified percentage + void setBlindness(const int percent) override; + + void activateHitOverlay(bool interrupt) override; + void setWerewolfOverlay(bool set) override; + + void toggleConsole() override; + void toggleDebugWindow() override; + void togglePostProcessorHud() override; + void toggleSettingsWindow() override; + + /// Cycle to next or previous spell + void cycleSpell(bool next) override; + /// Cycle to next or previous weapon + void cycleWeapon(bool next) override; + + void playSound(const ESM::RefId& soundId, float volume = 1.f, float pitch = 1.f) override; + + void addCell(MWWorld::CellStore* cell) override; + void removeCell(MWWorld::CellStore* cell) override; + void writeFog(MWWorld::CellStore* cell) override; + + const MWGui::TextColours& getTextColours() override; + + bool injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat = false) override; + bool injectKeyRelease(MyGUI::KeyCode key) override; + + const std::string& getVersionDescription() const override; + + void onDeleteCustomData(const MWWorld::Ptr& ptr) override; + void forceLootMode(const MWWorld::Ptr& ptr) override; + + void asyncPrepareSaveMap() override; + + // Used in Lua bindings + const std::vector& getGuiModeStack() const override { return mGuiModes; } + void setDisabledByLua(std::string_view windowId, bool disabled) override; + std::vector getAllWindowIds() const override; + std::vector getAllowedWindowIds(GuiMode mode) const override; + + private: + unsigned int mOldUpdateMask; + unsigned int mOldCullMask; + + const MWWorld::ESMStore* mStore; + Resource::ResourceSystem* mResourceSystem; + osg::ref_ptr mWorkQueue; + + std::unique_ptr mGuiPlatform; + osgViewer::Viewer* mViewer; + + std::unique_ptr mFontLoader; + std::unique_ptr mStatsWatcher; + + bool mConsoleOnlyScripts; + + std::map mTrackedWindows; + void trackWindow(Layout* layout, const WindowSettingValues& settings); + void onWindowChangeCoord(MyGUI::Window* _sender); + + ESM::RefId mSelectedSpell; + MWWorld::Ptr mSelectedEnchantItem; + MWWorld::Ptr mSelectedWeapon; + + std::vector mCurrentModals; + + // Markers placed manually by the player. Must be shared between both map views (the HUD map and the map + // window). + CustomMarkerCollection mCustomMarkers; + + HUD* mHud; + MapWindow* mMap; + std::unique_ptr mLocalMapRender; + std::unique_ptr mToolTips; + StatsWindow* mStatsWindow; + std::unique_ptr mMessageBoxManager; + Console* mConsole; + DialogueWindow* mDialogueWindow; + std::unique_ptr mDragAndDrop; + InventoryWindow* mInventoryWindow; + ScrollWindow* mScrollWindow; + BookWindow* mBookWindow; + CountDialog* mCountDialog; + TradeWindow* mTradeWindow; + SettingsWindow* mSettingsWindow; + ConfirmationDialog* mConfirmationDialog; + SpellWindow* mSpellWindow; + QuickKeysMenu* mQuickKeysMenu; + LoadingScreen* mLoadingScreen; + WaitDialog* mWaitDialog; + std::unique_ptr mSoulgemDialog; + MyGUI::ImageBox* mVideoBackground; + VideoWidget* mVideoWidget; + ScreenFader* mWerewolfFader; + ScreenFader* mBlindnessFader; + ScreenFader* mHitFader; + ScreenFader* mScreenFader; + DebugWindow* mDebugWindow; + PostProcessorHud* mPostProcessorHud; + JailScreen* mJailScreen; + ContainerWindow* mContainerWindow; + + std::vector> mWindows; + + // Mapping windowId -> Window; used by Lua bindings. + std::map mLuaIdToWindow; + + Translation::Storage& mTranslationDataStorage; + + std::unique_ptr mCharGen; + + MyGUI::Widget* mInputBlocker; + + bool mHudEnabled; + bool mCursorVisible; + bool mCursorActive; + + int mPlayerBounty; + + void setCursorVisible(bool visible) override; + + std::unique_ptr mGui; // Gui + + struct GuiModeState { - mWindows.push_back(window); - } - GuiModeState(const std::vector& windows) - : mWindows(windows) {} - GuiModeState() {} + GuiModeState(WindowBase* window) { mWindows.push_back(window); } + GuiModeState(const std::vector& windows) + : mWindows(windows) + { + } + GuiModeState() {} - void update(bool visible); + void update(bool visible); - std::vector mWindows; + std::vector mWindows; + }; + // Defines the windows that should be shown in a particular GUI mode. + std::map mGuiModeStates; + // The currently active stack of GUI modes (top mode is the one we are in). + std::vector mGuiModes; - std::string mCloseSound; - std::string mOpenSound; - }; - // Defines the windows that should be shown in a particular GUI mode. - std::map mGuiModeStates; - // The currently active stack of GUI modes (top mode is the one we are in). - std::vector mGuiModes; + std::unique_ptr mCursorManager; + + std::vector> mGarbageDialogs; + void cleanupGarbage(); - SDLUtil::SDLCursorManager* mCursorManager; + GuiWindow mShown; // Currently shown windows in inventory mode + GuiWindow mForceHidden; // Hidden windows (overrides mShown) - std::vector mGarbageDialogs; - void cleanupGarbage(); + /* Currently ALLOWED windows in inventory mode. This is used at + the start of the game, when windows are enabled one by one + through script commands. You can manipulate this through using + allow() and disableAll(). + */ + GuiWindow mAllowed; + // is the rest window allowed? + bool mRestAllowed; - GuiWindow mShown; // Currently shown windows in inventory mode - GuiWindow mForceHidden; // Hidden windows (overrides mShown) + void updateVisible(); // Update visibility of all windows based on mode, shown and allowed settings - /* Currently ALLOWED windows in inventory mode. This is used at - the start of the game, when windows are enabled one by one - through script commands. You can manipulate this through using - allow() and disableAll(). - */ - GuiWindow mAllowed; - // is the rest window allowed? - bool mRestAllowed; + void updateMap(); - void updateVisible(); // Update visibility of all windows based on mode, shown and allowed settings + ToUTF8::FromType mEncoding; - void updateMap(); + std::string mVersionDescription; - int mShowOwned; + bool mWindowVisible; - ToUTF8::FromType mEncoding; + MWGui::TextColours mTextColours; - std::string mVersionDescription; + std::unique_ptr mKeyboardNavigation; - bool mWindowVisible; + std::unique_ptr mVideoWrapper; - MWGui::TextColours mTextColours; + float mScalingFactor; - std::unique_ptr mKeyboardNavigation; + struct ScheduledMessageBox + { + std::string mMessage; + MWGui::ShowInDialogueMode mShowInDialogueMode; - SDLUtil::VideoWrapper* mVideoWrapper; + ScheduledMessageBox(std::string&& message, MWGui::ShowInDialogueMode showInDialogueMode) + : mMessage(std::move(message)) + , mShowInDialogueMode(showInDialogueMode) + { + } + }; - float mScalingFactor; + Misc::ScopeGuarded> mScheduledMessageBoxes; - /** - * Called when MyGUI tries to retrieve a tag's value. Tags must be denoted in #{tag} notation and will be replaced upon setting a user visible text/property. - * Supported syntax: - * #{GMSTName}: retrieves String value of the GMST called GMSTName - * #{setting=CATEGORY_NAME,SETTING_NAME}: retrieves String value of SETTING_NAME under category CATEGORY_NAME from settings.cfg - * #{sCell=CellID}: retrieves translated name of the given CellID (used only by some Morrowind localisations, in others cell ID is == cell name) - * #{fontcolour=FontColourName}: retrieves the value of the fallback setting "FontColor_color_" from openmw.cfg, - * in the format "r g b a", float values in range 0-1. Useful for "Colour" and "TextColour" properties in skins. - * #{fontcolourhtml=FontColourName}: retrieves the value of the fallback setting "FontColor_color_" from openmw.cfg, - * in the format "#xxxxxx" where x are hexadecimal numbers. Useful in an EditBox's caption to change the color of following text. - */ - void onRetrieveTag(const MyGUI::UString& _tag, MyGUI::UString& _result); + /** + * Called when MyGUI tries to retrieve a tag's value. Tags must be denoted in #{tag} notation and will be + * replaced upon setting a user visible text/property. Supported syntax: + * #{GMSTName}: retrieves String value of the GMST called GMSTName + * #{setting=CATEGORY_NAME,SETTING_NAME}: retrieves String value of SETTING_NAME under category CATEGORY_NAME + * from settings.cfg + * #{sCell=CellID}: retrieves translated name of the given CellID (used only by some Morrowind localisations, in + * others cell ID is == cell name) + * #{fontcolour=FontColourName}: retrieves the value of the fallback setting "FontColor_color_" + * from openmw.cfg, in the format "r g b a", float values in range 0-1. Useful for "Colour" and "TextColour" + * properties in skins. + * #{fontcolourhtml=FontColourName}: retrieves the value of the fallback setting + * "FontColor_color_" from openmw.cfg, in the format "#xxxxxx" where x are hexadecimal numbers. + * Useful in an EditBox's caption to change the color of following text. + */ + void onRetrieveTag(const MyGUI::UString& _tag, MyGUI::UString& _result); - void onCursorChange(const std::string& name); - void onKeyFocusChanged(MyGUI::Widget* widget); + void onCursorChange(std::string_view name); + void onKeyFocusChanged(MyGUI::Widget* widget); - // Key pressed while playing a video - void onVideoKeyPressed(MyGUI::Widget *_sender, MyGUI::KeyCode _key, MyGUI::Char _char); + // Key pressed while playing a video + void onVideoKeyPressed(MyGUI::Widget* _sender, MyGUI::KeyCode _key, MyGUI::Char _char); - void sizeVideo(int screenWidth, int screenHeight); + void sizeVideo(int screenWidth, int screenHeight); - void onClipboardChanged(const std::string& _type, const std::string& _data); - void onClipboardRequested(const std::string& _type, std::string& _data); + void onClipboardChanged(std::string_view _type, std::string_view _data); + void onClipboardRequested(std::string_view _type, std::string& _data); - void createTextures(); - void createCursors(); - void setMenuTransparency(float value); + void createTextures(); + void createCursors(); + void setMenuTransparency(float value); - void updatePinnedWindows(); + void updatePinnedWindows(); - void enableScene(bool enable); - }; + void enableScene(bool enable); + + void handleScheduledMessageBoxes(); + + void pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg, bool force); + + void setCullMask(uint32_t mask) override; + uint32_t getCullMask() override; + + void setActiveMap(const MWWorld::Cell& cell); + ///< set the indices of the map texture that should be used + + Files::ConfigurationManager& mCfgMgr; + }; } #endif diff --git a/apps/openmw/mwgui/windowpinnablebase.cpp b/apps/openmw/mwgui/windowpinnablebase.cpp index 6106d6b5635..1b3bd1d2d8f 100644 --- a/apps/openmw/mwgui/windowpinnablebase.cpp +++ b/apps/openmw/mwgui/windowpinnablebase.cpp @@ -5,10 +5,11 @@ namespace MWGui { WindowPinnableBase::WindowPinnableBase(const std::string& parLayout) - : WindowBase(parLayout), mPinned(false) + : WindowBase(parLayout) + , mPinned(false) { Window* window = mMainWidget->castType(); - mPinButton = window->getSkinWidget ("Button"); + mPinButton = window->getSkinWidget("Button"); mPinButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &WindowPinnableBase::onPinButtonPressed); } @@ -21,9 +22,9 @@ namespace MWGui mPinned = !mPinned; if (mPinned) - mPinButton->changeWidgetSkin ("PinDown"); + mPinButton->changeWidgetSkin("PinDown"); else - mPinButton->changeWidgetSkin ("PinUp"); + mPinButton->changeWidgetSkin("PinUp"); onPinToggled(); } diff --git a/apps/openmw/mwgui/windowpinnablebase.hpp b/apps/openmw/mwgui/windowpinnablebase.hpp index c91f0a14899..ebb3e2cf05e 100644 --- a/apps/openmw/mwgui/windowpinnablebase.hpp +++ b/apps/openmw/mwgui/windowpinnablebase.hpp @@ -5,12 +5,12 @@ namespace MWGui { - class WindowPinnableBase: public WindowBase + class WindowPinnableBase : public WindowBase { public: WindowPinnableBase(const std::string& parLayout); bool pinned() { return mPinned; } - void setPinned (bool pinned); + void setPinned(bool pinned); void setPinButtonVisible(bool visible); private: diff --git a/apps/openmw/mwinput/actionmanager.cpp b/apps/openmw/mwinput/actionmanager.cpp index e0fcc5ccfc6..154888c44cd 100644 --- a/apps/openmw/mwinput/actionmanager.cpp +++ b/apps/openmw/mwinput/actionmanager.cpp @@ -4,318 +4,153 @@ #include -#include +#include -#include "../mwbase/inputmanager.hpp" -#include "../mwbase/statemanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwworld/player.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/inventorystore.hpp" -#include "../mwworld/esmstore.hpp" +#include "../mwworld/globals.hpp" +#include "../mwworld/player.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/npcstats.hpp" #include "actions.hpp" #include "bindingsmanager.hpp" namespace MWInput { - const float ZOOM_SCALE = 10.f; /// Used for scrolling camera in and out - ActionManager::ActionManager(BindingsManager* bindingsManager, - osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, - osg::ref_ptr viewer, - osg::ref_ptr screenCaptureHandler) + ActionManager::ActionManager(BindingsManager* bindingsManager, osg::ref_ptr viewer, + osg::ref_ptr screenCaptureHandler) : mBindingsManager(bindingsManager) - , mViewer(viewer) - , mScreenCaptureHandler(screenCaptureHandler) - , mScreenCaptureOperation(screenCaptureOperation) - , mAlwaysRunActive(Settings::Manager::getBool("always run", "Input")) - , mSneaking(false) - , mAttemptJump(false) - , mOverencumberedMessageDelay(0.f) - , mPreviewPOVDelay(0.f) + , mViewer(std::move(viewer)) + , mScreenCaptureHandler(std::move(screenCaptureHandler)) , mTimeIdle(0.f) { } - void ActionManager::update(float dt, bool triedToMove) + void ActionManager::update(float dt) { - // Disable movement in Gui mode - if (MWBase::Environment::get().getWindowManager()->isGuiMode() - || MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_Running) - { - mAttemptJump = false; - return; - } - - // Configure player movement according to keyboard input. Actual movement will - // be done in the physics system. - if (MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) - { - bool alwaysRunAllowed = false; - - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - - if (mBindingsManager->actionIsActive(A_MoveLeft) != mBindingsManager->actionIsActive(A_MoveRight)) - { - alwaysRunAllowed = true; - triedToMove = true; - player.setLeftRight(mBindingsManager->actionIsActive(A_MoveRight) ? 1 : -1); - } - - if (mBindingsManager->actionIsActive(A_MoveForward) != mBindingsManager->actionIsActive(A_MoveBackward)) - { - alwaysRunAllowed = true; - triedToMove = true; - player.setAutoMove (false); - player.setForwardBackward(mBindingsManager->actionIsActive(A_MoveForward) ? 1 : -1); - } - - if (player.getAutoMove()) - { - alwaysRunAllowed = true; - triedToMove = true; - player.setForwardBackward (1); - } - - if (mAttemptJump && MWBase::Environment::get().getInputManager()->getControlSwitch("playerjumping")) - { - player.setUpDown(1); - triedToMove = true; - mOverencumberedMessageDelay = 0.f; - } - - // if player tried to start moving, but can't (due to being overencumbered), display a notification. - if (triedToMove) - { - MWWorld::Ptr playerPtr = MWBase::Environment::get().getWorld ()->getPlayerPtr(); - mOverencumberedMessageDelay -= dt; - if (playerPtr.getClass().getEncumbrance(playerPtr) > playerPtr.getClass().getCapacity(playerPtr)) - { - player.setAutoMove (false); - if (mOverencumberedMessageDelay <= 0) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage59}"); - mOverencumberedMessageDelay = 1.0; - } - } - } - - if (MWBase::Environment::get().getInputManager()->getControlSwitch("playerviewswitch")) - { - const float switchLimit = 0.25; - MWBase::World* world = MWBase::Environment::get().getWorld(); - if (mBindingsManager->actionIsActive(A_TogglePOV)) - { - if (world->isFirstPerson() ? mPreviewPOVDelay > switchLimit : mPreviewPOVDelay == 0) - world->togglePreviewMode(true); - mPreviewPOVDelay += dt; - } - else - { - //disable preview mode - if (mPreviewPOVDelay > 0) - world->togglePreviewMode(false); - if (mPreviewPOVDelay > 0.f && mPreviewPOVDelay <= switchLimit) - world->togglePOV(); - mPreviewPOVDelay = 0.f; - } - } - - if (triedToMove) - MWBase::Environment::get().getInputManager()->resetIdleTime(); - - static const bool isToggleSneak = Settings::Manager::getBool("toggle sneak", "Input"); - if (!isToggleSneak) - { - if(!MWBase::Environment::get().getInputManager()->joystickLastUsed()) - player.setSneak(mBindingsManager->actionIsActive(A_Sneak)); - } - - float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight); - float yAxis = mBindingsManager->getActionValue(A_MoveForwardBackward); - bool isRunning = osg::Vec2f(xAxis * 2 - 1, yAxis * 2 - 1).length2() > 0.25f; - if ((mAlwaysRunActive && alwaysRunAllowed) || isRunning) - player.setRunState(!mBindingsManager->actionIsActive(A_Run)); - else - player.setRunState(mBindingsManager->actionIsActive(A_Run)); - } - - if (mBindingsManager->actionIsActive(A_MoveForward) || - mBindingsManager->actionIsActive(A_MoveBackward) || - mBindingsManager->actionIsActive(A_MoveLeft) || - mBindingsManager->actionIsActive(A_MoveRight) || - mBindingsManager->actionIsActive(A_Jump) || - mBindingsManager->actionIsActive(A_Sneak) || - mBindingsManager->actionIsActive(A_TogglePOV) || - mBindingsManager->actionIsActive(A_ZoomIn) || - mBindingsManager->actionIsActive(A_ZoomOut)) + if (mBindingsManager->actionIsActive(A_MoveForward) || mBindingsManager->actionIsActive(A_MoveBackward) + || mBindingsManager->actionIsActive(A_MoveLeft) || mBindingsManager->actionIsActive(A_MoveRight) + || mBindingsManager->actionIsActive(A_Jump) || mBindingsManager->actionIsActive(A_Sneak) + || mBindingsManager->actionIsActive(A_TogglePOV) || mBindingsManager->actionIsActive(A_ZoomIn) + || mBindingsManager->actionIsActive(A_ZoomOut)) { resetIdleTime(); } else - { - updateIdleTime(dt); - } - - mAttemptJump = false; - } - - bool ActionManager::isPreviewModeEnabled() - { - return MWBase::Environment::get().getWorld()->isPreviewModeEnabled(); + mTimeIdle += dt; } void ActionManager::resetIdleTime() { - if (mTimeIdle < 0) - MWBase::Environment::get().getWorld()->toggleVanityMode(false); mTimeIdle = 0.f; } - void ActionManager::updateIdleTime(float dt) - { - static const float vanityDelay = MWBase::Environment::get().getWorld()->getStore().get() - .find("fVanityDelay")->mValue.getFloat(); - if (mTimeIdle >= 0.f) - mTimeIdle += dt; - if (mTimeIdle > vanityDelay) - { - MWBase::Environment::get().getWorld()->toggleVanityMode(true); - mTimeIdle = -1.f; - } - } - void ActionManager::executeAction(int action) { - auto* inputManager = MWBase::Environment::get().getInputManager(); - auto* windowManager = MWBase::Environment::get().getWindowManager(); + MWBase::Environment::get().getLuaManager()->inputEvent({ MWBase::LuaManager::InputEvent::Action, action }); + const auto inputManager = MWBase::Environment::get().getInputManager(); + const auto windowManager = MWBase::Environment::get().getWindowManager(); // trigger action activated switch (action) { - case A_GameMenu: - toggleMainMenu (); - break; - case A_Screenshot: - screenshot(); - break; - case A_Inventory: - toggleInventory (); - break; - case A_Console: - toggleConsole (); - break; - case A_Activate: - inputManager->resetIdleTime(); - activate(); - break; - case A_MoveLeft: - case A_MoveRight: - case A_MoveForward: - case A_MoveBackward: - handleGuiArrowKey(action); - break; - case A_Journal: - toggleJournal(); - break; - case A_AutoMove: - toggleAutoMove(); - break; - case A_AlwaysRun: - toggleWalking(); - break; - case A_ToggleWeapon: - toggleWeapon(); - break; - case A_Rest: - rest(); - break; - case A_ToggleSpell: - toggleSpell(); - break; - case A_QuickKey1: - quickKey(1); - break; - case A_QuickKey2: - quickKey(2); - break; - case A_QuickKey3: - quickKey(3); - break; - case A_QuickKey4: - quickKey(4); - break; - case A_QuickKey5: - quickKey(5); - break; - case A_QuickKey6: - quickKey(6); - break; - case A_QuickKey7: - quickKey(7); - break; - case A_QuickKey8: - quickKey(8); - break; - case A_QuickKey9: - quickKey(9); - break; - case A_QuickKey10: - quickKey(10); - break; - case A_QuickKeysMenu: - showQuickKeysMenu(); - break; - case A_ToggleHUD: - windowManager->toggleHud(); - break; - case A_ToggleDebug: - windowManager->toggleDebugWindow(); - break; - case A_ZoomIn: - if (inputManager->getControlSwitch("playerviewswitch") && inputManager->getControlSwitch("playercontrols") && !windowManager->isGuiMode()) - MWBase::Environment::get().getWorld()->adjustCameraDistance(-ZOOM_SCALE); - break; - case A_ZoomOut: - if (inputManager->getControlSwitch("playerviewswitch") && inputManager->getControlSwitch("playercontrols") && !windowManager->isGuiMode()) - MWBase::Environment::get().getWorld()->adjustCameraDistance(ZOOM_SCALE); - break; - case A_QuickSave: - quickSave(); - break; - case A_QuickLoad: - quickLoad(); - break; - case A_CycleSpellLeft: - if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Magic)) - MWBase::Environment::get().getWindowManager()->cycleSpell(false); - break; - case A_CycleSpellRight: - if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Magic)) - MWBase::Environment::get().getWindowManager()->cycleSpell(true); - break; - case A_CycleWeaponLeft: - if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Inventory)) - MWBase::Environment::get().getWindowManager()->cycleWeapon(false); - break; - case A_CycleWeaponRight: - if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Inventory)) - MWBase::Environment::get().getWindowManager()->cycleWeapon(true); - break; - case A_Sneak: - static const bool isToggleSneak = Settings::Manager::getBool("toggle sneak", "Input"); - if (isToggleSneak) - { - toggleSneaking(); - } - break; + case A_GameMenu: + toggleMainMenu(); + break; + case A_Screenshot: + screenshot(); + break; + case A_Console: + toggleConsole(); + break; + case A_Activate: + inputManager->resetIdleTime(); + activate(); + break; + case A_MoveLeft: + case A_MoveRight: + case A_MoveForward: + case A_MoveBackward: + handleGuiArrowKey(action); + break; + case A_Rest: + rest(); + break; + case A_QuickKey1: + quickKey(1); + break; + case A_QuickKey2: + quickKey(2); + break; + case A_QuickKey3: + quickKey(3); + break; + case A_QuickKey4: + quickKey(4); + break; + case A_QuickKey5: + quickKey(5); + break; + case A_QuickKey6: + quickKey(6); + break; + case A_QuickKey7: + quickKey(7); + break; + case A_QuickKey8: + quickKey(8); + break; + case A_QuickKey9: + quickKey(9); + break; + case A_QuickKey10: + quickKey(10); + break; + case A_ToggleHUD: + windowManager->setHudVisibility(!windowManager->isHudVisible()); + break; + case A_ToggleDebug: + windowManager->toggleDebugWindow(); + break; + case A_TogglePostProcessorHUD: + windowManager->togglePostProcessorHud(); + break; + case A_QuickSave: + quickSave(); + break; + case A_QuickLoad: + quickLoad(); + break; + case A_CycleSpellLeft: + if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Magic)) + MWBase::Environment::get().getWindowManager()->cycleSpell(false); + break; + case A_CycleSpellRight: + if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Magic)) + MWBase::Environment::get().getWindowManager()->cycleSpell(true); + break; + case A_CycleWeaponLeft: + if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Inventory)) + MWBase::Environment::get().getWindowManager()->cycleWeapon(false); + break; + case A_CycleWeaponRight: + if (checkAllowedToUseItems() && windowManager->isAllowed(MWGui::GW_Inventory)) + MWBase::Environment::get().getWindowManager()->cycleWeapon(true); + break; + case A_Inventory: + case A_Journal: + case A_QuickKeysMenu: + // Handled in Lua + break; } } @@ -333,24 +168,8 @@ namespace MWInput void ActionManager::screenshot() { - const std::string& settingStr = Settings::Manager::getString("screenshot type", "Video"); - bool regularScreenshot = settingStr.size() == 0 || settingStr.compare("regular") == 0; - - if (regularScreenshot) - { - mScreenCaptureHandler->setFramesToCapture(1); - mScreenCaptureHandler->captureNextFrame(*mViewer); - } - else - { - osg::ref_ptr screenshot (new osg::Image); - - if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get())) - { - (*mScreenCaptureOperation) (*(screenshot.get()), 0); - // FIXME: mScreenCaptureHandler->getCaptureOperation() causes crash for some reason - } - } + mScreenCaptureHandler->setFramesToCapture(1); + mScreenCaptureHandler->captureNextFrame(*mViewer); } void ActionManager::toggleMainMenu() @@ -367,44 +186,22 @@ namespace MWInput return; } - if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) //No open GUIs, open up the MainMenu + if (MWBase::Environment::get().getWindowManager()->isPostProcessorHudVisible()) { - MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); + MWBase::Environment::get().getWindowManager()->togglePostProcessorHud(); + return; } - else //Close current GUI + + if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) // No open GUIs, open up the MainMenu + { + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); + } + else // Close current GUI { MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } } - void ActionManager::toggleSpell() - { - if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; - - // Not allowed before the magic window is accessible - if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playermagic") || !MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) - return; - - if (!checkAllowedToUseItems()) - return; - - // Not allowed if no spell selected - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - MWWorld::InventoryStore& inventory = player.getPlayer().getClass().getInventoryStore(player.getPlayer()); - if (MWBase::Environment::get().getWindowManager()->getSelectedSpell().empty() && - inventory.getSelectedEnchantItem() == inventory.end()) - return; - - if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player.getPlayer())) - return; - - MWMechanics::DrawState_ state = player.getDrawState(); - if (state == MWMechanics::DrawState_Weapon || state == MWMechanics::DrawState_Nothing) - player.setDrawState(MWMechanics::DrawState_Spell); - else - player.setDrawState(MWMechanics::DrawState_Nothing); - } - void ActionManager::quickLoad() { if (!MyGUI::InputManager::getInstance().isModalAny()) @@ -417,62 +214,16 @@ namespace MWInput MWBase::Environment::get().getStateManager()->quickSave(); } - void ActionManager::toggleWeapon() - { - if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; - - // Not allowed before the inventory window is accessible - if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playerfighting") || !MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) - return; - - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - // We want to interrupt animation only if attack is preparing, but still is not triggered - // Otherwise we will get a "speedshooting" exploit, when player can skip reload animation by hitting "Toggle Weapon" key twice - if (MWBase::Environment::get().getMechanicsManager()->isAttackPreparing(player.getPlayer())) - player.setAttackingOrSpell(false); - else if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player.getPlayer())) - return; - - MWMechanics::DrawState_ state = player.getDrawState(); - if (state == MWMechanics::DrawState_Spell || state == MWMechanics::DrawState_Nothing) - player.setDrawState(MWMechanics::DrawState_Weapon); - else - player.setDrawState(MWMechanics::DrawState_Nothing); - } - void ActionManager::rest() { if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) return; - if (!MWBase::Environment::get().getWindowManager()->getRestEnabled() || MWBase::Environment::get().getWindowManager()->isGuiMode()) - return; - - MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Rest); //Open rest GUI - } - - void ActionManager::toggleInventory() - { - if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) - return; - - if (MyGUI::InputManager::getInstance().isModalAny()) - return; - - if (MWBase::Environment::get().getWindowManager()->isConsoleMode()) + if (!MWBase::Environment::get().getWindowManager()->getRestEnabled() + || MWBase::Environment::get().getWindowManager()->isGuiMode()) return; - // Toggle between game mode and inventory mode - if(!MWBase::Environment::get().getWindowManager()->isGuiMode()) - MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Inventory); - else - { - MWGui::GuiMode mode = MWBase::Environment::get().getWindowManager()->getMode(); - if(mode == MWGui::GM_Inventory || mode == MWGui::GM_Container) - MWBase::Environment::get().getWindowManager()->popGuiMode(); - } - - // .. but don't touch any other mode, except container. + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Rest); // Open rest GUI } void ActionManager::toggleConsole() @@ -483,58 +234,20 @@ namespace MWInput MWBase::Environment::get().getWindowManager()->toggleConsole(); } - void ActionManager::toggleJournal() + void ActionManager::quickKey(int index) { - if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) - return; - if (MyGUI::InputManager::getInstance ().isModalAny()) - return; - - if (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Journal - && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_MainMenu - && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Settings - && MWBase::Environment::get().getWindowManager ()->getJournalAllowed()) - { - MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Journal); - } - else if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Journal)) - { - MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Journal); - } - } - - void ActionManager::quickKey (int index) - { - if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols") || !MWBase::Environment::get().getInputManager()->getControlSwitch("playerfighting") || !MWBase::Environment::get().getInputManager()->getControlSwitch("playermagic")) + if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols") + || !MWBase::Environment::get().getInputManager()->getControlSwitch("playerfighting") + || !MWBase::Environment::get().getInputManager()->getControlSwitch("playermagic")) return; if (!checkAllowedToUseItems()) return; - if (MWBase::Environment::get().getWorld()->getGlobalFloat ("chargenstate")!=-1) + if (MWBase::Environment::get().getWorld()->getGlobalFloat(MWWorld::Globals::sCharGenState) != -1) return; if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) - MWBase::Environment::get().getWindowManager()->activateQuickKey (index); - } - - void ActionManager::showQuickKeysMenu() - { - if (!MWBase::Environment::get().getWindowManager()->isGuiMode () - && MWBase::Environment::get().getWorld()->getGlobalFloat ("chargenstate")==-1) - { - if (!checkAllowedToUseItems()) - return; - - MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_QuickKeysMenu); - } - else if (MWBase::Environment::get().getWindowManager()->getMode () == MWGui::GM_QuickKeysMenu) - { - while (MyGUI::InputManager::getInstance().isModalAny()) - { //Handle any open Modal windows - MWBase::Environment::get().getWindowManager()->exitCurrentModal(); - } - MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); //And handle the actual main window - } + MWBase::Environment::get().getWindowManager()->activateQuickKey(index); } void ActionManager::activate() @@ -552,32 +265,10 @@ namespace MWInput } } - void ActionManager::toggleAutoMove() - { - if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; - - if (MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) - { - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - player.setAutoMove (!player.getAutoMove()); - } - } - - void ActionManager::toggleWalking() - { - if (MWBase::Environment::get().getWindowManager()->isGuiMode() || SDL_IsTextInputActive()) return; - mAlwaysRunActive = !mAlwaysRunActive; - - Settings::Manager::setBool("always run", "Input", mAlwaysRunActive); - } - - void ActionManager::toggleSneaking() + bool ActionManager::isSneaking() const { - if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; - if (!MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) return; - mSneaking = !mSneaking; - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - player.setSneak(mSneaking); + const MWBase::Environment& env = MWBase::Environment::get(); + return env.getMechanicsManager()->isSneaking(env.getWorld()->getPlayer().getPlayer()); } void ActionManager::handleGuiArrowKey(int action) @@ -597,19 +288,19 @@ namespace MWInput MyGUI::KeyCode key; switch (action) { - case A_MoveLeft: - key = MyGUI::KeyCode::ArrowLeft; - break; - case A_MoveRight: - key = MyGUI::KeyCode::ArrowRight; - break; - case A_MoveForward: - key = MyGUI::KeyCode::ArrowUp; - break; - case A_MoveBackward: - default: - key = MyGUI::KeyCode::ArrowDown; - break; + case A_MoveLeft: + key = MyGUI::KeyCode::ArrowLeft; + break; + case A_MoveRight: + key = MyGUI::KeyCode::ArrowRight; + break; + case A_MoveForward: + key = MyGUI::KeyCode::ArrowUp; + break; + case A_MoveBackward: + default: + key = MyGUI::KeyCode::ArrowDown; + break; } MWBase::Environment::get().getWindowManager()->injectKeyPress(key, 0, false); diff --git a/apps/openmw/mwinput/actionmanager.hpp b/apps/openmw/mwinput/actionmanager.hpp index eceac2e94f5..eb21f7ef799 100644 --- a/apps/openmw/mwinput/actionmanager.hpp +++ b/apps/openmw/mwinput/actionmanager.hpp @@ -17,61 +17,37 @@ namespace MWInput class ActionManager { public: - - ActionManager(BindingsManager* bindingsManager, - osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, - osg::ref_ptr viewer, + ActionManager(BindingsManager* bindingsManager, osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler); - void update(float dt, bool triedToMove); + void update(float dt); void executeAction(int action); bool checkAllowedToUseItems() const; void toggleMainMenu(); - void toggleSpell(); - void toggleWeapon(); - void toggleInventory(); void toggleConsole(); void screenshot(); - void toggleJournal(); void activate(); - void toggleWalking(); - void toggleSneaking(); - void toggleAutoMove(); void rest(); void quickLoad(); void quickSave(); - void quickKey (int index); - void showQuickKeysMenu(); + void quickKey(int index); void resetIdleTime(); + float getIdleTime() const { return mTimeIdle; } - bool isAlwaysRunActive() const { return mAlwaysRunActive; }; - bool isSneaking() const { return mSneaking; }; - - void setAttemptJump(bool enabled) { mAttemptJump = enabled; } - - bool isPreviewModeEnabled(); + bool isSneaking() const; private: void handleGuiArrowKey(int action); - void updateIdleTime(float dt); - BindingsManager* mBindingsManager; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; - osgViewer::ScreenCaptureHandler::CaptureOperation* mScreenCaptureOperation; - - bool mAlwaysRunActive; - bool mSneaking; - bool mAttemptJump; - float mOverencumberedMessageDelay; - float mPreviewPOVDelay; float mTimeIdle; }; } diff --git a/apps/openmw/mwinput/actions.hpp b/apps/openmw/mwinput/actions.hpp index a1c1607126c..538d12f2c24 100644 --- a/apps/openmw/mwinput/actions.hpp +++ b/apps/openmw/mwinput/actions.hpp @@ -5,75 +5,70 @@ namespace MWInput { enum Actions { - // please add new actions at the bottom, in order to preserve the channel IDs in the key configuration files - - A_GameMenu, - - A_Unused, - - A_Screenshot, // Take a screenshot - - A_Inventory, // Toggle inventory screen - - A_Console, // Toggle console screen - - A_MoveLeft, // Move player left / right - A_MoveRight, - A_MoveForward, // Forward / Backward - A_MoveBackward, - - A_Activate, - - A_Use, //Use weapon, spell, etc. - A_Jump, - A_AutoMove, //Toggle Auto-move forward - A_Rest, //Rest - A_Journal, //Journal - A_Weapon, //Draw/Sheath weapon - A_Spell, //Ready/Unready Casting - A_Run, //Run when held - A_CycleSpellLeft, //cycling through spells - A_CycleSpellRight, - A_CycleWeaponLeft, //Cycling through weapons - A_CycleWeaponRight, - A_ToggleSneak, //Toggles Sneak - A_AlwaysRun, //Toggle Walking/Running - A_Sneak, - - A_QuickSave, - A_QuickLoad, - A_QuickMenu, - A_ToggleWeapon, - A_ToggleSpell, - - A_TogglePOV, - - A_QuickKey1, - A_QuickKey2, - A_QuickKey3, - A_QuickKey4, - A_QuickKey5, - A_QuickKey6, - A_QuickKey7, - A_QuickKey8, - A_QuickKey9, - A_QuickKey10, - - A_QuickKeysMenu, - - A_ToggleHUD, - - A_ToggleDebug, - - A_LookUpDown, //Joystick look - A_LookLeftRight, - A_MoveForwardBackward, - A_MoveLeftRight, - - A_ZoomIn, - A_ZoomOut, - - A_Last // Marker for the last item + // Action IDs are used in the configuration file input_v3.xml + + A_GameMenu = 0, + + A_Screenshot = 2, // Take a screenshot + + A_Inventory = 3, // Toggle inventory screen + A_Console = 4, // Toggle console screen + + A_MoveLeft = 5, // Move player left / right + A_MoveRight = 6, + A_MoveForward = 7, // Forward / Backward + A_MoveBackward = 8, + + A_Activate = 9, + + A_Use = 10, // Use weapon, spell, etc. + A_Jump = 11, + A_AutoMove = 12, // Toggle Auto-move forward + A_Rest = 13, // Rest + A_Journal = 14, // Journal + A_Run = 17, // Run when held + A_CycleSpellLeft = 18, // cycling through spells + A_CycleSpellRight = 19, + A_CycleWeaponLeft = 20, // Cycling through weapons + A_CycleWeaponRight = 21, + A_AlwaysRun = 23, // Toggle Walking/Running + A_Sneak = 24, + + A_QuickSave = 25, + A_QuickLoad = 26, + A_QuickMenu = 27, + A_ToggleWeapon = 28, + A_ToggleSpell = 29, + + A_TogglePOV = 30, + + A_QuickKey1 = 31, + A_QuickKey2 = 32, + A_QuickKey3 = 33, + A_QuickKey4 = 34, + A_QuickKey5 = 35, + A_QuickKey6 = 36, + A_QuickKey7 = 37, + A_QuickKey8 = 38, + A_QuickKey9 = 39, + A_QuickKey10 = 40, + + A_QuickKeysMenu = 41, + + A_ToggleHUD = 42, + A_ToggleDebug = 43, + + A_LookUpDown = 44, // Joystick look + A_LookLeftRight = 45, + A_MoveForwardBackward = 46, + A_MoveLeftRight = 47, + + A_ZoomIn = 48, + A_ZoomOut = 49, + + A_TogglePostProcessorHUD = 50, + + A_Last // Marker for the last item }; } #endif diff --git a/apps/openmw/mwinput/bindingsmanager.cpp b/apps/openmw/mwinput/bindingsmanager.cpp index 851e33a87c0..339ebf4276f 100644 --- a/apps/openmw/mwinput/bindingsmanager.cpp +++ b/apps/openmw/mwinput/bindingsmanager.cpp @@ -1,23 +1,26 @@ #include "bindingsmanager.hpp" +#include + #include #include #include +#include +#include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/player.hpp" #include "actions.hpp" -#include "sdlmappings.hpp" namespace MWInput { - static const int sFakeDeviceId = 1; //As we only support one controller at a time, use a fake deviceID so we don't lose bindings when switching controllers + static const int sFakeDeviceId = 1; // As we only support one controller at a time, use a fake deviceID so we don't + // lose bindings when switching controllers void clearAllKeyBindings(ICS::InputControlSystem* inputBinder, ICS::Control* control) { @@ -26,7 +29,8 @@ namespace MWInput inputBinder->removeKeyBinding(inputBinder->getKeyBinding(control, ICS::Control::INCREASE)); if (inputBinder->getMouseButtonBinding(control, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) inputBinder->removeMouseButtonBinding(inputBinder->getMouseButtonBinding(control, ICS::Control::INCREASE)); - if (inputBinder->getMouseWheelBinding(control, ICS::Control::INCREASE) != ICS::InputControlSystem::MouseWheelClick::UNASSIGNED) + if (inputBinder->getMouseWheelBinding(control, ICS::Control::INCREASE) + != ICS::InputControlSystem::MouseWheelClick::UNASSIGNED) inputBinder->removeMouseWheelBinding(inputBinder->getMouseWheelBinding(control, ICS::Control::INCREASE)); } @@ -34,23 +38,24 @@ namespace MWInput { // right now we don't really need multiple bindings for the same action, so remove all others first if (inputBinder->getJoystickAxisBinding(control, sFakeDeviceId, ICS::Control::INCREASE) != SDL_SCANCODE_UNKNOWN) - inputBinder->removeJoystickAxisBinding(sFakeDeviceId, inputBinder->getJoystickAxisBinding(control, sFakeDeviceId, ICS::Control::INCREASE)); - if (inputBinder->getJoystickButtonBinding(control, sFakeDeviceId, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) - inputBinder->removeJoystickButtonBinding(sFakeDeviceId, inputBinder->getJoystickButtonBinding(control, sFakeDeviceId, ICS::Control::INCREASE)); + inputBinder->removeJoystickAxisBinding( + sFakeDeviceId, inputBinder->getJoystickAxisBinding(control, sFakeDeviceId, ICS::Control::INCREASE)); + if (inputBinder->getJoystickButtonBinding(control, sFakeDeviceId, ICS::Control::INCREASE) + != ICS_MAX_DEVICE_BUTTONS) + inputBinder->removeJoystickButtonBinding( + sFakeDeviceId, inputBinder->getJoystickButtonBinding(control, sFakeDeviceId, ICS::Control::INCREASE)); } class InputControlSystem : public ICS::InputControlSystem { public: - InputControlSystem(const std::string& bindingsFile) - : ICS::InputControlSystem(bindingsFile, true, nullptr, nullptr, A_Last) + InputControlSystem(const std::filesystem::path& bindingsFile) + : ICS::InputControlSystem(Files::pathToUnicodeString(bindingsFile), true, nullptr, nullptr, A_Last) { } }; - class BindingsListener : - public ICS::ChannelListener, - public ICS::DetectingBindingListener + class BindingsListener : public ICS::ChannelListener, public ICS::DetectingBindingListener { public: BindingsListener(ICS::InputControlSystem* inputBinder, BindingsManager* bindingsManager) @@ -68,13 +73,13 @@ namespace MWInput mBindingsManager->actionValueChanged(action, currentValue, previousValue); } - void keyBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , SDL_Scancode key, ICS::Control::ControlChangingDirection direction) override + void keyBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control, SDL_Scancode key, + ICS::Control::ControlChangingDirection direction) override { - //Disallow binding escape key - if (key==SDL_SCANCODE_ESCAPE) + // Disallow binding escape key + if (key == SDL_SCANCODE_ESCAPE) { - //Stop binding if esc pressed + // Stop binding if esc pressed mInputBinder->cancelDetectingBindingState(); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); return; @@ -84,11 +89,11 @@ namespace MWInput if (key == SDL_SCANCODE_F3 || key == SDL_SCANCODE_F4 || key == SDL_SCANCODE_F10) return; - #ifndef __APPLE__ +#ifndef __APPLE__ // Disallow binding Windows/Meta keys if (key == SDL_SCANCODE_LGUI || key == SDL_SCANCODE_RGUI) return; - #endif +#endif if (!mDetectingKeyboard) return; @@ -99,15 +104,15 @@ namespace MWInput MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } - void mouseAxisBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , ICS::InputControlSystem::NamedAxis axis, ICS::Control::ControlChangingDirection direction) override + void mouseAxisBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control, + ICS::InputControlSystem::NamedAxis axis, ICS::Control::ControlChangingDirection direction) override { // we don't want mouse movement bindings return; } - void mouseButtonBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , unsigned int button, ICS::Control::ControlChangingDirection direction) override + void mouseButtonBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control, unsigned int button, + ICS::Control::ControlChangingDirection direction) override { if (!mDetectingKeyboard) return; @@ -117,8 +122,8 @@ namespace MWInput MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } - void mouseWheelBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , ICS::InputControlSystem::MouseWheelClick click, ICS::Control::ControlChangingDirection direction) override + void mouseWheelBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control, + ICS::InputControlSystem::MouseWheelClick click, ICS::Control::ControlChangingDirection direction) override { if (!mDetectingKeyboard) return; @@ -128,30 +133,30 @@ namespace MWInput MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } - void joystickAxisBindingDetected(ICS::InputControlSystem* ICS, int deviceID, ICS::Control* control - , int axis, ICS::Control::ControlChangingDirection direction) override + void joystickAxisBindingDetected(ICS::InputControlSystem* ICS, int deviceID, ICS::Control* control, int axis, + ICS::Control::ControlChangingDirection direction) override { - //only allow binding to the trigers + // only allow binding to the trigers if (axis != SDL_CONTROLLER_AXIS_TRIGGERLEFT && axis != SDL_CONTROLLER_AXIS_TRIGGERRIGHT) return; if (mDetectingKeyboard) return; clearAllControllerBindings(mInputBinder, control); - control->setValue(0.5f); //axis bindings must start at 0.5 + control->setValue(0.5f); // axis bindings must start at 0.5 control->setInitialValue(0.5f); ICS::DetectingBindingListener::joystickAxisBindingDetected(ICS, deviceID, control, axis, direction); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } - void joystickButtonBindingDetected(ICS::InputControlSystem* ICS, int deviceID, ICS::Control* control - , unsigned int button, ICS::Control::ControlChangingDirection direction) override + void joystickButtonBindingDetected(ICS::InputControlSystem* ICS, int deviceID, ICS::Control* control, + unsigned int button, ICS::Control::ControlChangingDirection direction) override { if (mDetectingKeyboard) return; - clearAllControllerBindings(mInputBinder,control); + clearAllControllerBindings(mInputBinder, control); control->setInitialValue(0.0f); - ICS::DetectingBindingListener::joystickButtonBindingDetected (ICS, deviceID, control, button, direction); + ICS::DetectingBindingListener::joystickButtonBindingDetected(ICS, deviceID, control, button, direction); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } @@ -166,11 +171,11 @@ namespace MWInput bool mDetectingKeyboard; }; - BindingsManager::BindingsManager(const std::string& userFile, bool userFileExists) + BindingsManager::BindingsManager(const std::filesystem::path& userFile, bool userFileExists) : mUserFile(userFile) , mDragDrop(false) { - std::string file = userFileExists ? userFile : ""; + const auto file = userFileExists ? userFile : std::filesystem::path(); mInputBinder = std::make_unique(file); mListener = std::make_unique(mInputBinder.get(), this); mInputBinder->setDetectingBindingListener(mListener.get()); @@ -191,7 +196,23 @@ namespace MWInput BindingsManager::~BindingsManager() { - mInputBinder->save(mUserFile); + 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(); + } } void BindingsManager::update(float dt) @@ -202,10 +223,12 @@ namespace MWInput bool BindingsManager::isLeftOrRightButton(int action, bool joystick) const { - int mouseBinding = mInputBinder->getMouseButtonBinding(mInputBinder->getControl(action), ICS::Control::INCREASE); + int mouseBinding + = mInputBinder->getMouseButtonBinding(mInputBinder->getControl(action), ICS::Control::INCREASE); if (mouseBinding != ICS_MAX_DEVICE_BUTTONS) return true; - int buttonBinding = mInputBinder->getJoystickButtonBinding(mInputBinder->getControl(action), sFakeDeviceId, ICS::Control::INCREASE); + int buttonBinding = mInputBinder->getJoystickButtonBinding( + mInputBinder->getControl(action), sFakeDeviceId, ICS::Control::INCREASE); if (joystick && (buttonBinding == 0 || buttonBinding == 1)) return true; return false; @@ -213,13 +236,11 @@ namespace MWInput void BindingsManager::setPlayerControlsEnabled(bool enabled) { - int playerChannels[] = {A_AutoMove, A_AlwaysRun, A_ToggleWeapon, - A_ToggleSpell, A_Rest, A_QuickKey1, A_QuickKey2, - A_QuickKey3, A_QuickKey4, A_QuickKey5, A_QuickKey6, - A_QuickKey7, A_QuickKey8, A_QuickKey9, A_QuickKey10, - A_Use, A_Journal}; + int playerChannels[] = { A_AutoMove, A_AlwaysRun, A_ToggleWeapon, A_ToggleSpell, A_Rest, A_QuickKey1, + A_QuickKey2, A_QuickKey3, A_QuickKey4, A_QuickKey5, A_QuickKey6, A_QuickKey7, A_QuickKey8, A_QuickKey9, + A_QuickKey10, A_Use, A_Journal }; - for(int pc : playerChannels) + for (int pc : playerChannels) { mInputBinder->getChannel(pc)->setEnabled(enabled); } @@ -230,23 +251,23 @@ namespace MWInput mInputBinder->setJoystickDeadZone(deadZone); } - float BindingsManager::getActionValue (int id) const + float BindingsManager::getActionValue(int id) const { return mInputBinder->getChannel(id)->getValue(); } - bool BindingsManager::actionIsActive (int id) const + bool BindingsManager::actionIsActive(int id) const { return getActionValue(id) == 1.0; } - void BindingsManager::loadKeyDefaults (bool force) + void BindingsManager::loadKeyDefaults(bool force) { // using hardcoded key defaults is inevitable, if we want the configuration files to stay valid // across different versions of OpenMW (in the case where another input action is added) std::map defaultKeyBindings; - //Gets the Keyvalue from the Scancode; gives the button in the same place reguardless of keyboard format + // Gets the Keyvalue from the Scancode; gives the button in the same place reguardless of keyboard format defaultKeyBindings[A_Activate] = SDL_SCANCODE_SPACE; defaultKeyBindings[A_MoveBackward] = SDL_SCANCODE_S; defaultKeyBindings[A_MoveForward] = SDL_SCANCODE_W; @@ -285,6 +306,7 @@ namespace MWInput defaultKeyBindings[A_AlwaysRun] = SDL_SCANCODE_CAPSLOCK; defaultKeyBindings[A_QuickSave] = SDL_SCANCODE_F5; defaultKeyBindings[A_QuickLoad] = SDL_SCANCODE_F9; + defaultKeyBindings[A_TogglePostProcessorHUD] = SDL_SCANCODE_F2; std::map defaultMouseButtonBindings; defaultMouseButtonBindings[A_Inventory] = SDL_BUTTON_RIGHT; @@ -309,38 +331,41 @@ namespace MWInput control = mInputBinder->getChannel(i)->getAttachedControls().front().control; } - if (!controlExists || force || - (mInputBinder->getKeyBinding(control, ICS::Control::INCREASE) == SDL_SCANCODE_UNKNOWN - && mInputBinder->getMouseButtonBinding(control, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS - && mInputBinder->getMouseWheelBinding(control, ICS::Control::INCREASE) == ICS::InputControlSystem::MouseWheelClick::UNASSIGNED)) + if (!controlExists || force + || (mInputBinder->getKeyBinding(control, ICS::Control::INCREASE) == SDL_SCANCODE_UNKNOWN + && mInputBinder->getMouseButtonBinding(control, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS + && mInputBinder->getMouseWheelBinding(control, ICS::Control::INCREASE) + == ICS::InputControlSystem::MouseWheelClick::UNASSIGNED)) { clearAllKeyBindings(mInputBinder.get(), control); if (defaultKeyBindings.find(i) != defaultKeyBindings.end() - && (force || !mInputBinder->isKeyBound(defaultKeyBindings[i]))) + && (force || !mInputBinder->isKeyBound(defaultKeyBindings[i]))) { control->setInitialValue(0.0f); mInputBinder->addKeyBinding(control, defaultKeyBindings[i], ICS::Control::INCREASE); } else if (defaultMouseButtonBindings.find(i) != defaultMouseButtonBindings.end() - && (force || !mInputBinder->isMouseButtonBound(defaultMouseButtonBindings[i]))) + && (force || !mInputBinder->isMouseButtonBound(defaultMouseButtonBindings[i]))) { control->setInitialValue(0.0f); mInputBinder->addMouseButtonBinding(control, defaultMouseButtonBindings[i], ICS::Control::INCREASE); } else if (defaultMouseWheelBindings.find(i) != defaultMouseWheelBindings.end() - && (force || !mInputBinder->isMouseWheelBound(defaultMouseWheelBindings[i]))) + && (force || !mInputBinder->isMouseWheelBound(defaultMouseWheelBindings[i]))) { control->setInitialValue(0.f); mInputBinder->addMouseWheelBinding(control, defaultMouseWheelBindings[i], ICS::Control::INCREASE); } - if (i == A_LookLeftRight && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_4) && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_6)) + if (i == A_LookLeftRight && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_4) + && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_6)) { mInputBinder->addKeyBinding(control, SDL_SCANCODE_KP_6, ICS::Control::INCREASE); mInputBinder->addKeyBinding(control, SDL_SCANCODE_KP_4, ICS::Control::DECREASE); } - if (i == A_LookUpDown && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_8) && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_2)) + if (i == A_LookUpDown && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_8) + && !mInputBinder->isKeyBound(SDL_SCANCODE_KP_2)) { mInputBinder->addKeyBinding(control, SDL_SCANCODE_KP_2, ICS::Control::INCREASE); mInputBinder->addKeyBinding(control, SDL_SCANCODE_KP_8, ICS::Control::DECREASE); @@ -358,7 +383,8 @@ namespace MWInput defaultButtonBindings[A_Activate] = SDL_CONTROLLER_BUTTON_A; defaultButtonBindings[A_ToggleWeapon] = SDL_CONTROLLER_BUTTON_X; defaultButtonBindings[A_ToggleSpell] = SDL_CONTROLLER_BUTTON_Y; - //defaultButtonBindings[A_QuickButtonsMenu] = SDL_GetButtonFromScancode(SDL_SCANCODE_F1); // Need to implement, should be ToggleSpell(5) AND Wait(9) + // defaultButtonBindings[A_QuickButtonsMenu] = SDL_GetButtonFromScancode(SDL_SCANCODE_F1); // Need to implement, + // should be ToggleSpell(5) AND Wait(9) defaultButtonBindings[A_Sneak] = SDL_CONTROLLER_BUTTON_LEFTSTICK; defaultButtonBindings[A_Journal] = SDL_CONTROLLER_BUTTON_LEFTSHOULDER; defaultButtonBindings[A_Rest] = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER; @@ -366,10 +392,6 @@ namespace MWInput defaultButtonBindings[A_Inventory] = SDL_CONTROLLER_BUTTON_B; defaultButtonBindings[A_GameMenu] = SDL_CONTROLLER_BUTTON_START; defaultButtonBindings[A_QuickSave] = SDL_CONTROLLER_BUTTON_GUIDE; - defaultButtonBindings[A_MoveForward] = SDL_CONTROLLER_BUTTON_DPAD_UP; - defaultButtonBindings[A_MoveLeft] = SDL_CONTROLLER_BUTTON_DPAD_LEFT; - defaultButtonBindings[A_MoveBackward] = SDL_CONTROLLER_BUTTON_DPAD_DOWN; - defaultButtonBindings[A_MoveRight] = SDL_CONTROLLER_BUTTON_DPAD_RIGHT; std::map defaultAxisBindings; defaultAxisBindings[A_MoveForwardBackward] = SDL_CONTROLLER_AXIS_LEFTY; @@ -388,7 +410,8 @@ namespace MWInput float initial; if (defaultAxisBindings.find(i) == defaultAxisBindings.end()) initial = 0.0f; - else initial = 0.5f; + else + initial = 0.5f; control = new ICS::Control(std::to_string(i), false, true, initial, ICS::ICS_MAX, ICS::ICS_MAX); mInputBinder->addControl(control); control->attachChannel(mInputBinder->getChannel(i), ICS::Channel::DIRECT); @@ -398,39 +421,45 @@ namespace MWInput control = mInputBinder->getChannel(i)->getAttachedControls().front().control; } - if (!controlExists || force || (mInputBinder->getJoystickAxisBinding(control, sFakeDeviceId, ICS::Control::INCREASE) == ICS::InputControlSystem::UNASSIGNED && - mInputBinder->getJoystickButtonBinding(control, sFakeDeviceId, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS)) + if (!controlExists || force + || (mInputBinder->getJoystickAxisBinding(control, sFakeDeviceId, ICS::Control::INCREASE) + == ICS::InputControlSystem::UNASSIGNED + && mInputBinder->getJoystickButtonBinding(control, sFakeDeviceId, ICS::Control::INCREASE) + == ICS_MAX_DEVICE_BUTTONS)) { clearAllControllerBindings(mInputBinder.get(), control); if (defaultButtonBindings.find(i) != defaultButtonBindings.end() - && (force || !mInputBinder->isJoystickButtonBound(sFakeDeviceId, defaultButtonBindings[i]))) + && (force || !mInputBinder->isJoystickButtonBound(sFakeDeviceId, defaultButtonBindings[i]))) { control->setInitialValue(0.0f); - mInputBinder->addJoystickButtonBinding(control, sFakeDeviceId, defaultButtonBindings[i], ICS::Control::INCREASE); + mInputBinder->addJoystickButtonBinding( + control, sFakeDeviceId, defaultButtonBindings[i], ICS::Control::INCREASE); } - else if (defaultAxisBindings.find(i) != defaultAxisBindings.end() && (force || !mInputBinder->isJoystickAxisBound(sFakeDeviceId, defaultAxisBindings[i]))) + else if (defaultAxisBindings.find(i) != defaultAxisBindings.end() + && (force || !mInputBinder->isJoystickAxisBound(sFakeDeviceId, defaultAxisBindings[i]))) { control->setValue(0.5f); control->setInitialValue(0.5f); - mInputBinder->addJoystickAxisBinding(control, sFakeDeviceId, defaultAxisBindings[i], ICS::Control::INCREASE); + mInputBinder->addJoystickAxisBinding( + control, sFakeDeviceId, defaultAxisBindings[i], ICS::Control::INCREASE); } } } } - std::string BindingsManager::getActionDescription(int action) + std::string_view BindingsManager::getActionDescription(int action) { switch (action) { case A_Screenshot: - return "Screenshot"; + return "#{OMWEngine:Screenshot}"; case A_ZoomIn: - return "Zoom In"; + return "#{OMWEngine:CameraZoomIn}"; case A_ZoomOut: - return "Zoom Out"; + return "#{OMWEngine:CameraZoomOut}"; case A_ToggleHUD: - return "Toggle HUD"; + return "#{OMWEngine:ToggleHUD}"; case A_Use: return "#{sUse}"; case A_Activate: @@ -456,7 +485,7 @@ namespace MWInput case A_CycleWeaponRight: return "#{sNextWeapon}"; case A_Console: - return "#{sConsoleTitle}"; + return "#{OMWEngine:ConsoleWindow}"; case A_Run: return "#{sRun}"; case A_Sneak: @@ -501,15 +530,17 @@ namespace MWInput return "#{sQuickSaveCmd}"; case A_QuickLoad: return "#{sQuickLoadCmd}"; + case A_TogglePostProcessorHUD: + return "#{OMWEngine:TogglePostProcessorHUD}"; default: - return std::string(); // not configurable + return {}; // not configurable } } std::string BindingsManager::getActionKeyBindingName(int action) { if (mInputBinder->getChannel(action)->getControlsCount() == 0) - return "#{sNone}"; + return "#{Interface:None}"; ICS::Control* c = mInputBinder->getChannel(action)->getAttachedControls().front().control; @@ -532,51 +563,50 @@ namespace MWInput case ICS::InputControlSystem::MouseWheelClick::LEFT: return "Mouse Wheel Left"; default: - return "#{sNone}"; + return "#{Interface:None}"; } else - return "#{sNone}"; + return "#{Interface:None}"; } std::string BindingsManager::getActionControllerBindingName(int action) { if (mInputBinder->getChannel(action)->getControlsCount() == 0) - return "#{sNone}"; + return "#{Interface:None}"; ICS::Control* c = mInputBinder->getChannel(action)->getAttachedControls().front().control; - if (mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE) != ICS::InputControlSystem::UNASSIGNED) - return sdlControllerAxisToString(mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); - else if (mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) - return sdlControllerButtonToString(mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); + if (mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE) + != ICS::InputControlSystem::UNASSIGNED) + return SDLUtil::sdlControllerAxisToString( + mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); + else if (mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE) + != ICS_MAX_DEVICE_BUTTONS) + return SDLUtil::sdlControllerButtonToString( + mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); else - return "#{sNone}"; + return "#{Interface:None}"; } - std::vector BindingsManager::getActionKeySorting() + const std::initializer_list& BindingsManager::getActionKeySorting() { - static const std::vector actions - { - A_MoveForward, A_MoveBackward, A_MoveLeft, A_MoveRight, A_TogglePOV, A_ZoomIn, A_ZoomOut, - A_Run, A_AlwaysRun, A_Sneak, A_Activate, A_Use, A_ToggleWeapon, A_ToggleSpell, - A_CycleSpellLeft, A_CycleSpellRight, A_CycleWeaponLeft, A_CycleWeaponRight, A_AutoMove, - A_Jump, A_Inventory, A_Journal, A_Rest, A_Console, A_QuickSave, A_QuickLoad, - A_ToggleHUD, A_Screenshot, A_QuickKeysMenu, A_QuickKey1, A_QuickKey2, A_QuickKey3, - A_QuickKey4, A_QuickKey5, A_QuickKey6, A_QuickKey7, A_QuickKey8, A_QuickKey9, A_QuickKey10 - }; + static const std::initializer_list actions{ A_MoveForward, A_MoveBackward, A_MoveLeft, A_MoveRight, + A_TogglePOV, A_ZoomIn, A_ZoomOut, A_Run, A_AlwaysRun, A_Sneak, A_Activate, A_Use, A_ToggleWeapon, + A_ToggleSpell, A_CycleSpellLeft, A_CycleSpellRight, A_CycleWeaponLeft, A_CycleWeaponRight, A_AutoMove, + A_Jump, A_Inventory, A_Journal, A_Rest, A_Console, A_QuickSave, A_QuickLoad, A_ToggleHUD, A_Screenshot, + A_QuickKeysMenu, A_QuickKey1, A_QuickKey2, A_QuickKey3, A_QuickKey4, A_QuickKey5, A_QuickKey6, A_QuickKey7, + A_QuickKey8, A_QuickKey9, A_QuickKey10, A_TogglePostProcessorHUD }; return actions; } - std::vector BindingsManager::getActionControllerSorting() + const std::initializer_list& BindingsManager::getActionControllerSorting() { - static const std::vector actions - { - A_TogglePOV, A_ZoomIn, A_ZoomOut, A_Sneak, A_Activate, A_Use, A_ToggleWeapon, A_ToggleSpell, - A_AutoMove, A_Jump, A_Inventory, A_Journal, A_Rest, A_QuickSave, A_QuickLoad, A_ToggleHUD, - A_Screenshot, A_QuickKeysMenu, A_QuickKey1, A_QuickKey2, A_QuickKey3, A_QuickKey4, - A_QuickKey5, A_QuickKey6, A_QuickKey7, A_QuickKey8, A_QuickKey9, A_QuickKey10, - A_CycleSpellLeft, A_CycleSpellRight, A_CycleWeaponLeft, A_CycleWeaponRight - }; + static const std::initializer_list actions{ A_MoveForward, A_MoveBackward, A_MoveLeft, A_MoveRight, + A_TogglePOV, A_ZoomIn, A_ZoomOut, A_Sneak, A_Activate, A_Use, A_ToggleWeapon, A_ToggleSpell, A_AutoMove, + A_Jump, A_Inventory, A_Journal, A_Rest, A_QuickSave, A_QuickLoad, A_ToggleHUD, A_Screenshot, + A_QuickKeysMenu, A_QuickKey1, A_QuickKey2, A_QuickKey3, A_QuickKey4, A_QuickKey5, A_QuickKey6, A_QuickKey7, + A_QuickKey8, A_QuickKey9, A_QuickKey10, A_CycleSpellLeft, A_CycleSpellRight, A_CycleWeaponLeft, + A_CycleWeaponRight }; return actions; } @@ -593,57 +623,57 @@ namespace MWInput return mInputBinder->detectingBindingState(); } - void BindingsManager::mousePressed(const SDL_MouseButtonEvent &arg, int deviceID) + void BindingsManager::mousePressed(const SDL_MouseButtonEvent& arg, Uint8 deviceID) { mInputBinder->mousePressed(arg, deviceID); } - void BindingsManager::mouseReleased(const SDL_MouseButtonEvent &arg, int deviceID) + void BindingsManager::mouseReleased(const SDL_MouseButtonEvent& arg, Uint8 deviceID) { mInputBinder->mouseReleased(arg, deviceID); } - void BindingsManager::mouseMoved(const SDLUtil::MouseMotionEvent &arg) + void BindingsManager::mouseMoved(const SDLUtil::MouseMotionEvent& arg) { mInputBinder->mouseMoved(arg); } - void BindingsManager::mouseWheelMoved(const SDL_MouseWheelEvent &arg) + void BindingsManager::mouseWheelMoved(const SDL_MouseWheelEvent& arg) { mInputBinder->mouseWheelMoved(arg); } - void BindingsManager::keyPressed(const SDL_KeyboardEvent &arg) + void BindingsManager::keyPressed(const SDL_KeyboardEvent& arg) { mInputBinder->keyPressed(arg); } - void BindingsManager::keyReleased(const SDL_KeyboardEvent &arg) + void BindingsManager::keyReleased(const SDL_KeyboardEvent& arg) { mInputBinder->keyReleased(arg); } - void BindingsManager::controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg) + void BindingsManager::controllerAdded(int deviceID, const SDL_ControllerDeviceEvent& arg) { mInputBinder->controllerAdded(deviceID, arg); } - void BindingsManager::controllerRemoved(const SDL_ControllerDeviceEvent &arg) + void BindingsManager::controllerRemoved(const SDL_ControllerDeviceEvent& arg) { mInputBinder->controllerRemoved(arg); } - void BindingsManager::controllerButtonPressed(int deviceID, const SDL_ControllerButtonEvent &arg) + void BindingsManager::controllerButtonPressed(int deviceID, const SDL_ControllerButtonEvent& arg) { mInputBinder->buttonPressed(deviceID, arg); } - void BindingsManager::controllerButtonReleased(int deviceID, const SDL_ControllerButtonEvent &arg) + void BindingsManager::controllerButtonReleased(int deviceID, const SDL_ControllerButtonEvent& arg) { mInputBinder->buttonReleased(deviceID, arg); } - void BindingsManager::controllerAxisMoved(int deviceID, const SDL_ControllerAxisEvent &arg) + void BindingsManager::controllerAxisMoved(int deviceID, const SDL_ControllerAxisEvent& arg) { mInputBinder->axisMoved(deviceID, arg); } @@ -653,66 +683,36 @@ namespace MWInput return mInputBinder->getKeyBinding(mInputBinder->getControl(actionId), ICS::Control::INCREASE); } + SDL_GameController* BindingsManager::getControllerOrNull() const + { + const auto& controllers = mInputBinder->getJoystickInstanceMap(); + if (controllers.empty()) + return nullptr; + else + return controllers.begin()->second; + } + void BindingsManager::actionValueChanged(int action, float currentValue, float previousValue) { - MWBase::Environment::get().getInputManager()->resetIdleTime(); + auto manager = MWBase::Environment::get().getInputManager(); + manager->resetIdleTime(); if (mDragDrop && action != A_GameMenu && action != A_Inventory) return; - if ((previousValue == 1 || previousValue == 0) && (currentValue==1 || currentValue==0)) - { - //Is a normal button press, so don't change it at all - } - //Otherwise only trigger button presses as they go through specific points - else if (previousValue >= 0.8 && currentValue < 0.8) + if (manager->joystickLastUsed() && manager->getControlSwitch("playercontrols")) { - currentValue = 0.0; - previousValue = 1.0; - } - else if (previousValue <= 0.6 && currentValue > 0.6) - { - currentValue = 1.0; - previousValue = 0.0; - } - else - { - //If it's not switching between those values, ignore the channel change. - return; - } - - if (MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) - { - bool joystickUsed = MWBase::Environment::get().getInputManager()->joystickLastUsed(); - if (action == A_Use) - { - if (joystickUsed && currentValue == 1.0 && actionIsActive(A_ToggleWeapon)) - action = A_CycleWeaponRight; - - else if (joystickUsed && currentValue == 1.0 && actionIsActive(A_ToggleSpell)) - action = A_CycleSpellRight; - - else - { - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - MWMechanics::DrawState_ state = player.getDrawState(); - player.setAttackingOrSpell(currentValue != 0 && state != MWMechanics::DrawState_Nothing); - } - } - else if (action == A_Jump) - { - if (joystickUsed && currentValue == 1.0 && actionIsActive(A_ToggleWeapon)) - action = A_CycleWeaponLeft; - - else if (joystickUsed && currentValue == 1.0 && actionIsActive(A_ToggleSpell)) - action = A_CycleSpellLeft; - - else - MWBase::Environment::get().getInputManager()->setAttemptJump(currentValue == 1.0 && previousValue == 0.0); - } + if (action == A_Use && actionIsActive(A_ToggleWeapon)) + action = A_CycleWeaponRight; + else if (action == A_Use && actionIsActive(A_ToggleSpell)) + action = A_CycleSpellRight; + else if (action == A_Jump && actionIsActive(A_ToggleWeapon)) + action = A_CycleWeaponLeft; + else if (action == A_Jump && actionIsActive(A_ToggleSpell)) + action = A_CycleSpellLeft; } - if (currentValue == 1) - MWBase::Environment::get().getInputManager()->executeAction(action); + if (previousValue <= 0.6 && currentValue > 0.6) + manager->executeAction(action); } } diff --git a/apps/openmw/mwinput/bindingsmanager.hpp b/apps/openmw/mwinput/bindingsmanager.hpp index 74416d3c7f1..bee9e07cf7c 100644 --- a/apps/openmw/mwinput/bindingsmanager.hpp +++ b/apps/openmw/mwinput/bindingsmanager.hpp @@ -1,6 +1,7 @@ #ifndef MWINPUT_MWBINDINGSMANAGER_H #define MWINPUT_MWBINDINGSMANAGER_H +#include #include #include #include @@ -15,17 +16,17 @@ namespace MWInput class BindingsManager { public: - BindingsManager(const std::string& userFile, bool userFileExists); + BindingsManager(const std::filesystem::path& userFile, bool userFileExists); virtual ~BindingsManager(); - std::string getActionDescription (int action); - std::string getActionKeyBindingName (int action); - std::string getActionControllerBindingName (int action); - std::vector getActionKeySorting(); - std::vector getActionControllerSorting(); + std::string_view getActionDescription(int action); + std::string getActionKeyBindingName(int action); + std::string getActionControllerBindingName(int action); + const std::initializer_list& getActionKeySorting(); + const std::initializer_list& getActionControllerSorting(); - void enableDetectingBindingMode (int action, bool keyboard); + void enableDetectingBindingMode(int action, bool keyboard); bool isDetectingBindingState() const; void loadKeyDefaults(bool force = false); @@ -42,21 +43,23 @@ namespace MWInput bool isLeftOrRightButton(int action, bool joystick) const; bool actionIsActive(int id) const; - float getActionValue(int id) const; + float getActionValue(int id) const; // returns value in range [0, 1] - void mousePressed(const SDL_MouseButtonEvent &evt, int deviceID); - void mouseReleased(const SDL_MouseButtonEvent &arg, int deviceID); - void mouseMoved(const SDLUtil::MouseMotionEvent &arg); - void mouseWheelMoved(const SDL_MouseWheelEvent &arg); + SDL_GameController* getControllerOrNull() const; - void keyPressed(const SDL_KeyboardEvent &arg); - void keyReleased(const SDL_KeyboardEvent &arg); + void mousePressed(const SDL_MouseButtonEvent& evt, Uint8 deviceID); + void mouseReleased(const SDL_MouseButtonEvent& arg, Uint8 deviceID); + void mouseMoved(const SDLUtil::MouseMotionEvent& arg); + void mouseWheelMoved(const SDL_MouseWheelEvent& arg); - void controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg); - void controllerRemoved(const SDL_ControllerDeviceEvent &arg); - void controllerButtonPressed(int deviceID, const SDL_ControllerButtonEvent &arg); - void controllerButtonReleased(int deviceID, const SDL_ControllerButtonEvent &arg); - void controllerAxisMoved(int deviceID, const SDL_ControllerAxisEvent &arg); + void keyPressed(const SDL_KeyboardEvent& arg); + void keyReleased(const SDL_KeyboardEvent& arg); + + void controllerAdded(int deviceID, const SDL_ControllerDeviceEvent& arg); + void controllerRemoved(const SDL_ControllerDeviceEvent& arg); + void controllerButtonPressed(int deviceID, const SDL_ControllerButtonEvent& arg); + void controllerButtonReleased(int deviceID, const SDL_ControllerButtonEvent& arg); + void controllerAxisMoved(int deviceID, const SDL_ControllerAxisEvent& arg); SDL_Scancode getKeyBinding(int actionId); @@ -68,7 +71,7 @@ namespace MWInput std::unique_ptr mInputBinder; std::unique_ptr mListener; - std::string mUserFile; + std::filesystem::path mUserFile; bool mDragDrop; }; diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index 03d492c9cf6..0c0a3de57c0 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -2,56 +2,59 @@ #include #include -#include + +#include #include +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/player.hpp" #include "actions.hpp" -#include "actionmanager.hpp" #include "bindingsmanager.hpp" #include "mousemanager.hpp" -#include "sdlmappings.hpp" namespace MWInput { - ControllerManager::ControllerManager(BindingsManager* bindingsManager, - ActionManager* actionManager, - MouseManager* mouseManager, - const std::string& userControllerBindingsFile, - const std::string& controllerBindingsFile) + ControllerManager::ControllerManager(BindingsManager* bindingsManager, MouseManager* mouseManager, + const std::filesystem::path& userControllerBindingsFile, const std::filesystem::path& controllerBindingsFile) : mBindingsManager(bindingsManager) - , mActionManager(actionManager) , mMouseManager(mouseManager) - , mJoystickEnabled (Settings::Manager::getBool("enable controller", "Input")) - , mGamepadCursorSpeed(Settings::Manager::getFloat("gamepad cursor speed", "Input")) - , mSneakToggleShortcutTimer(0.f) - , mGamepadZoom(0) + , mGyroAvailable(false) , mGamepadGuiCursorEnabled(true) , mGuiCursorEnabled(true) , mJoystickLastUsed(false) - , mSneakGamepadShortcut(false) - , mGamepadPreviewMode(false) { if (!controllerBindingsFile.empty()) { - SDL_GameControllerAddMappingsFromFile(controllerBindingsFile.c_str()); + const int result + = SDL_GameControllerAddMappingsFromFile(Files::pathToUnicodeString(controllerBindingsFile).c_str()); + if (result < 0) + Log(Debug::Error) << "Failed to add game controller mappings from file \"" << controllerBindingsFile + << "\": " << SDL_GetError(); } if (!userControllerBindingsFile.empty()) { - SDL_GameControllerAddMappingsFromFile(userControllerBindingsFile.c_str()); + const int result + = SDL_GameControllerAddMappingsFromFile(Files::pathToUnicodeString(userControllerBindingsFile).c_str()); + if (result < 0) + Log(Debug::Error) << "Failed to add game controller mappings from user file \"" + << userControllerBindingsFile << "\": " << SDL_GetError(); } // Open all presently connected sticks - int numSticks = SDL_NumJoysticks(); + const int numSticks = SDL_NumJoysticks(); + if (numSticks < 0) + Log(Debug::Error) << "Failed to get number of joysticks: " << SDL_GetError(); + for (int i = 0; i < numSticks; i++) { if (SDL_IsGameController(i)) @@ -59,33 +62,26 @@ namespace MWInput SDL_ControllerDeviceEvent evt; evt.which = i; static const int fakeDeviceID = 1; - controllerAdded(fakeDeviceID, evt); - Log(Debug::Info) << "Detected game controller: " << SDL_GameControllerNameForIndex(i); + ControllerManager::controllerAdded(fakeDeviceID, evt); + if (const char* name = SDL_GameControllerNameForIndex(i)) + Log(Debug::Info) << "Detected game controller: " << name; + else + Log(Debug::Warning) << "Detected game controller without a name: " << SDL_GetError(); } else { - Log(Debug::Info) << "Detected unusable controller: " << SDL_JoystickNameForIndex(i); + if (const char* name = SDL_JoystickNameForIndex(i)) + Log(Debug::Info) << "Detected unusable controller: " << name; + else + Log(Debug::Warning) << "Detected unusable controller without a name: " << SDL_GetError(); } } - float deadZoneRadius = Settings::Manager::getFloat("joystick dead zone", "Input"); - deadZoneRadius = std::min(std::max(deadZoneRadius, 0.0f), 0.5f); - mBindingsManager->setJoystickDeadZone(deadZoneRadius); + mBindingsManager->setJoystickDeadZone(Settings::input().mJoystickDeadZone); } - void ControllerManager::processChangedSettings(const Settings::CategorySettingVector& changed) + void ControllerManager::update(float dt) { - for (const auto& setting : changed) - { - if (setting.first == "Input" && setting.second == "enable controller") - mJoystickEnabled = Settings::Manager::getBool("enable controller", "Input"); - } - } - - bool ControllerManager::update(float dt) - { - mGamepadPreviewMode = mActionManager->isPreviewModeEnabled(); - if (mGuiCursorEnabled && !(mJoystickLastUsed && !mGamepadGuiCursorEnabled)) { float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight) * 2.0f - 1.0f; @@ -98,8 +94,9 @@ namespace MWInput // We keep track of our own mouse position, so that moving the mouse while in // game mode does not move the position of the GUI cursor float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); - float xMove = xAxis * dt * 1500.0f / uiScale; - float yMove = yAxis * dt * 1500.0f / uiScale; + const float gamepadCursorSpeed = Settings::input().mGamepadCursorSpeed; + const float xMove = xAxis * dt * 1500.0f / uiScale * gamepadCursorSpeed; + const float yMove = yAxis * dt * 1500.0f / uiScale * gamepadCursorSpeed; float mouseWheelMove = -zAxis * dt * 1500.0f; if (xMove != 0 || yMove != 0 || mouseWheelMove != 0) @@ -110,94 +107,28 @@ namespace MWInput } } - // Disable movement in Gui mode - if (MWBase::Environment::get().getWindowManager()->isGuiMode() - || MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_Running) - { - mGamepadZoom = 0; - return false; - } - - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - bool triedToMove = false; - - // Configure player movement according to controller input. Actual movement will - // be done in the physics system. - if (MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) + if (!MWBase::Environment::get().getWindowManager()->isGuiMode() + && MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_Running + && MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) { float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight); float yAxis = mBindingsManager->getActionValue(A_MoveForwardBackward); - if (xAxis != 0.5) - { - triedToMove = true; - player.setLeftRight((xAxis - 0.5f) * 2); - } - - if (yAxis != 0.5) - { - triedToMove = true; - player.setAutoMove (false); - player.setForwardBackward((0.5f - yAxis) * 2); - } - - if (triedToMove) + if (xAxis != 0.5 || yAxis != 0.5) { mJoystickLastUsed = true; MWBase::Environment::get().getInputManager()->resetIdleTime(); } - - static const bool isToggleSneak = Settings::Manager::getBool("toggle sneak", "Input"); - if (!isToggleSneak) - { - if (mJoystickLastUsed) - { - if (mBindingsManager->actionIsActive(A_Sneak)) - { - if (mSneakToggleShortcutTimer) // New Sneak Button Press - { - if (mSneakToggleShortcutTimer <= 0.3f) - { - mSneakGamepadShortcut = true; - mActionManager->toggleSneaking(); - } - else - mSneakGamepadShortcut = false; - } - - if (!mActionManager->isSneaking()) - mActionManager->toggleSneaking(); - mSneakToggleShortcutTimer = 0.f; - } - else - { - if (!mSneakGamepadShortcut && mActionManager->isSneaking()) - mActionManager->toggleSneaking(); - if (mSneakToggleShortcutTimer <= 0.3f) - mSneakToggleShortcutTimer += dt; - } - } - else - player.setSneak(mBindingsManager->actionIsActive(A_Sneak)); - } - } - - if (MWBase::Environment::get().getInputManager()->getControlSwitch("playerviewswitch")) - { - if (!mBindingsManager->actionIsActive(A_TogglePOV)) - mGamepadZoom = 0; - - if (mGamepadZoom) - MWBase::Environment::get().getWorld()->adjustCameraDistance(-mGamepadZoom); } - - return triedToMove; } - void ControllerManager::buttonPressed(int deviceID, const SDL_ControllerButtonEvent &arg) + void ControllerManager::buttonPressed(int deviceID, const SDL_ControllerButtonEvent& arg) { - if (!mJoystickEnabled || mBindingsManager->isDetectingBindingState()) + if (!Settings::input().mEnableController || mBindingsManager->isDetectingBindingState()) return; + MWBase::Environment::get().getLuaManager()->inputEvent( + { MWBase::LuaManager::InputEvent::ControllerPressed, arg.button }); + mJoystickLastUsed = true; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { @@ -212,9 +143,11 @@ namespace MWInput bool mousePressSuccess = mMouseManager->injectMouseButtonPress(SDL_BUTTON_LEFT); if (MyGUI::InputManager::getInstance().getMouseFocusWidget()) { - MyGUI::Button* b = MyGUI::InputManager::getInstance().getMouseFocusWidget()->castType(false); + MyGUI::Button* b + = MyGUI::InputManager::getInstance().getMouseFocusWidget()->castType(false); if (b && b->getEnabled()) - MWBase::Environment::get().getWindowManager()->playSound("Menu Click"); + MWBase::Environment::get().getWindowManager()->playSound( + ESM::RefId::stringRefId("Menu Click")); } mBindingsManager->setPlayerControlsEnabled(!mousePressSuccess); @@ -224,15 +157,15 @@ namespace MWInput else mBindingsManager->setPlayerControlsEnabled(true); - //esc, to leave initial movie screen - auto kc = sdlKeyToMyGUI(SDLK_ESCAPE); + // esc, to leave initial movie screen + auto kc = SDLUtil::sdlKeyToMyGUI(SDLK_ESCAPE); mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyPress(kc, 0)); if (!MWBase::Environment::get().getInputManager()->controlsDisabled()) mBindingsManager->controllerButtonPressed(deviceID, arg); } - void ControllerManager::buttonReleased(int deviceID, const SDL_ControllerButtonEvent &arg) + void ControllerManager::buttonReleased(int deviceID, const SDL_ControllerButtonEvent& arg) { if (mBindingsManager->isDetectingBindingState()) { @@ -240,7 +173,13 @@ namespace MWInput return; } - if (!mJoystickEnabled || MWBase::Environment::get().getInputManager()->controlsDisabled()) + if (Settings::input().mEnableController) + { + MWBase::Environment::get().getLuaManager()->inputEvent( + { MWBase::LuaManager::InputEvent::ControllerReleased, arg.button }); + } + + if (!Settings::input().mEnableController || MWBase::Environment::get().getInputManager()->controlsDisabled()) return; mJoystickLastUsed = true; @@ -252,7 +191,8 @@ namespace MWInput if (arg.button == SDL_CONTROLLER_BUTTON_A) // We'll pretend that A is left click. { bool mousePressSuccess = mMouseManager->injectMouseButtonRelease(SDL_BUTTON_LEFT); - if (mBindingsManager->isDetectingBindingState()) // If the player just triggered binding, don't let button release bind. + if (mBindingsManager->isDetectingBindingState()) // If the player just triggered binding, don't let + // button release bind. return; mBindingsManager->setPlayerControlsEnabled(!mousePressSuccess); @@ -262,16 +202,16 @@ namespace MWInput else mBindingsManager->setPlayerControlsEnabled(true); - //esc, to leave initial movie screen - auto kc = sdlKeyToMyGUI(SDLK_ESCAPE); + // esc, to leave initial movie screen + auto kc = SDLUtil::sdlKeyToMyGUI(SDLK_ESCAPE); mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc)); mBindingsManager->controllerButtonReleased(deviceID, arg); } - void ControllerManager::axisMoved(int deviceID, const SDL_ControllerAxisEvent &arg) + void ControllerManager::axisMoved(int deviceID, const SDL_ControllerAxisEvent& arg) { - if (!mJoystickEnabled || MWBase::Environment::get().getInputManager()->controlsDisabled()) + if (!Settings::input().mEnableController || MWBase::Environment::get().getInputManager()->controlsDisabled()) return; mJoystickLastUsed = true; @@ -279,36 +219,27 @@ namespace MWInput { gamepadToGuiControl(arg); } - else + else if (mBindingsManager->actionIsActive(A_TogglePOV) + && (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT || arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT)) { - if (mGamepadPreviewMode) // Preview Mode Gamepad Zooming - { - if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT) - { - mGamepadZoom = arg.value * 0.85f / 1000.f / 12.f; - return; // Do not propagate event. - } - else if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT) - { - mGamepadZoom = -arg.value * 0.85f / 1000.f / 12.f; - return; // Do not propagate event. - } - } + // Preview Mode Gamepad Zooming; do not propagate to mBindingsManager + return; } mBindingsManager->controllerAxisMoved(deviceID, arg); } - void ControllerManager::controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg) + void ControllerManager::controllerAdded(int deviceID, const SDL_ControllerDeviceEvent& arg) { mBindingsManager->controllerAdded(deviceID, arg); + enableGyroSensor(); } - void ControllerManager::controllerRemoved(const SDL_ControllerDeviceEvent &arg) + void ControllerManager::controllerRemoved(const SDL_ControllerDeviceEvent& arg) { mBindingsManager->controllerRemoved(arg); } - bool ControllerManager::gamepadToGuiControl(const SDL_ControllerButtonEvent &arg) + bool ControllerManager::gamepadToGuiControl(const SDL_ControllerButtonEvent& arg) { // Presumption of GUI mode will be removed in the future. // MyGUI KeyCodes *may* change. @@ -346,11 +277,11 @@ namespace MWInput key = MyGUI::KeyCode::Apostrophe; break; case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: - key = MyGUI::KeyCode::Period; - break; + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Period, 0, false); + return true; case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: - key = MyGUI::KeyCode::Slash; - break; + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Slash, 0, false); + return true; case SDL_CONTROLLER_BUTTON_LEFTSTICK: mGamepadGuiCursorEnabled = !mGamepadGuiCursorEnabled; MWBase::Environment::get().getWindowManager()->setCursorActive(mGamepadGuiCursorEnabled); @@ -367,7 +298,7 @@ namespace MWInput return true; } - bool ControllerManager::gamepadToGuiControl(const SDL_ControllerAxisEvent &arg) + bool ControllerManager::gamepadToGuiControl(const SDL_ControllerAxisEvent& arg) { switch (arg.axis) { @@ -393,4 +324,76 @@ namespace MWInput return true; } + + float ControllerManager::getAxisValue(SDL_GameControllerAxis axis) const + { + SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); + constexpr int AXIS_MAX_ABSOLUTE_VALUE = 32768; + if (cntrl) + return SDL_GameControllerGetAxis(cntrl, axis) / static_cast(AXIS_MAX_ABSOLUTE_VALUE); + else + return 0; + } + + bool ControllerManager::isButtonPressed(SDL_GameControllerButton button) const + { + SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); + if (cntrl) + return SDL_GameControllerGetButton(cntrl, button) > 0; + else + return false; + } + + void ControllerManager::enableGyroSensor() + { + mGyroAvailable = false; +#if SDL_VERSION_ATLEAST(2, 0, 14) + SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); + if (!cntrl) + return; + if (!SDL_GameControllerHasSensor(cntrl, SDL_SENSOR_GYRO)) + return; + if (const int result = SDL_GameControllerSetSensorEnabled(cntrl, SDL_SENSOR_GYRO, SDL_TRUE); result < 0) + { + Log(Debug::Error) << "Failed to enable game controller sensor: " << SDL_GetError(); + return; + } + mGyroAvailable = true; +#endif + } + + bool ControllerManager::isGyroAvailable() const + { + return mGyroAvailable; + } + + 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) + { + const int result = SDL_GameControllerGetSensorData(cntrl, SDL_SENSOR_GYRO, gyro, 3); + 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] }); + } + + void ControllerManager::touchpadMoved(int deviceId, const SDLUtil::TouchEvent& arg) + { + MWBase::Environment::get().getLuaManager()->inputEvent({ MWBase::LuaManager::InputEvent::TouchMoved, arg }); + } + + void ControllerManager::touchpadPressed(int deviceId, const SDLUtil::TouchEvent& arg) + { + MWBase::Environment::get().getLuaManager()->inputEvent({ MWBase::LuaManager::InputEvent::TouchPressed, arg }); + } + + void ControllerManager::touchpadReleased(int deviceId, const SDLUtil::TouchEvent& arg) + { + MWBase::Environment::get().getLuaManager()->inputEvent({ MWBase::LuaManager::InputEvent::TouchReleased, arg }); + } } diff --git a/apps/openmw/mwinput/controllermanager.hpp b/apps/openmw/mwinput/controllermanager.hpp index d8c62d57c41..596a9ef4652 100644 --- a/apps/openmw/mwinput/controllermanager.hpp +++ b/apps/openmw/mwinput/controllermanager.hpp @@ -1,64 +1,67 @@ #ifndef MWINPUT_MWCONTROLLERMANAGER_H #define MWINPUT_MWCONTROLLERMANAGER_H +#include +#include #include -#include #include +#include namespace MWInput { - class ActionManager; class BindingsManager; class MouseManager; class ControllerManager : public SDLUtil::ControllerListener { public: - ControllerManager(BindingsManager* bindingsManager, - ActionManager* actionManager, - MouseManager* mouseManager, - const std::string& userControllerBindingsFile, - const std::string& controllerBindingsFile); + ControllerManager(BindingsManager* bindingsManager, MouseManager* mouseManager, + const std::filesystem::path& userControllerBindingsFile, + const std::filesystem::path& controllerBindingsFile); virtual ~ControllerManager() = default; - bool update(float dt); + void update(float dt); - void buttonPressed(int deviceID, const SDL_ControllerButtonEvent &arg) override; - void buttonReleased(int deviceID, const SDL_ControllerButtonEvent &arg) override; - void axisMoved(int deviceID, const SDL_ControllerAxisEvent &arg) override; - void controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg) override; - void controllerRemoved(const SDL_ControllerDeviceEvent &arg) override; + void buttonPressed(int deviceID, const SDL_ControllerButtonEvent& arg) override; + void buttonReleased(int deviceID, const SDL_ControllerButtonEvent& arg) override; + void axisMoved(int deviceID, const SDL_ControllerAxisEvent& arg) override; + void controllerAdded(int deviceID, const SDL_ControllerDeviceEvent& arg) override; + void controllerRemoved(const SDL_ControllerDeviceEvent& arg) override; - void processChangedSettings(const Settings::CategorySettingVector& changed); + void touchpadMoved(int deviceId, const SDLUtil::TouchEvent& arg) override; + void touchpadPressed(int deviceId, const SDLUtil::TouchEvent& arg) override; + void touchpadReleased(int deviceId, const SDLUtil::TouchEvent& arg) override; void setJoystickLastUsed(bool enabled) { mJoystickLastUsed = enabled; } - bool joystickLastUsed() { return mJoystickLastUsed; } + bool joystickLastUsed() const { return mJoystickLastUsed; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } void setGamepadGuiCursorEnabled(bool enabled) { mGamepadGuiCursorEnabled = enabled; } - bool gamepadGuiCursorEnabled() { return mGamepadGuiCursorEnabled; } + bool gamepadGuiCursorEnabled() const { return mGamepadGuiCursorEnabled; } + + float getAxisValue(SDL_GameControllerAxis axis) const; // returns value in range [-1, 1] + bool isButtonPressed(SDL_GameControllerButton button) const; + + bool isGyroAvailable() const; + std::array getGyroValues() const; private: // Return true if GUI consumes input. - bool gamepadToGuiControl(const SDL_ControllerButtonEvent &arg); - bool gamepadToGuiControl(const SDL_ControllerAxisEvent &arg); + bool gamepadToGuiControl(const SDL_ControllerButtonEvent& arg); + bool gamepadToGuiControl(const SDL_ControllerAxisEvent& arg); + + void enableGyroSensor(); BindingsManager* mBindingsManager; - ActionManager* mActionManager; MouseManager* mMouseManager; - bool mJoystickEnabled; - float mGamepadCursorSpeed; - float mSneakToggleShortcutTimer; - float mGamepadZoom; + bool mGyroAvailable; bool mGamepadGuiCursorEnabled; bool mGuiCursorEnabled; bool mJoystickLastUsed; - bool mSneakGamepadShortcut; - bool mGamepadPreviewMode; }; } #endif diff --git a/apps/openmw/mwinput/controlswitch.cpp b/apps/openmw/mwinput/controlswitch.cpp index 33c4b75dcce..974cfbbbb56 100644 --- a/apps/openmw/mwinput/controlswitch.cpp +++ b/apps/openmw/mwinput/controlswitch.cpp @@ -1,16 +1,14 @@ #include "controlswitch.hpp" -#include -#include -#include +#include +#include +#include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" -#include "../mwworld/player.hpp" - namespace MWInput { ControlSwitch::ControlSwitch() @@ -20,46 +18,34 @@ namespace MWInput void ControlSwitch::clear() { - mSwitches["playercontrols"] = true; - mSwitches["playerfighting"] = true; - mSwitches["playerjumping"] = true; - mSwitches["playerlooking"] = true; - mSwitches["playermagic"] = true; - mSwitches["playerviewswitch"] = true; - mSwitches["vanitymode"] = true; + mSwitches["playercontrols"] = true; + mSwitches["playerfighting"] = true; + mSwitches["playerjumping"] = true; + mSwitches["playerlooking"] = true; + mSwitches["playermagic"] = true; + mSwitches["playerviewswitch"] = true; + mSwitches["vanitymode"] = true; } - bool ControlSwitch::get(const std::string& key) + bool ControlSwitch::get(std::string_view key) { - return mSwitches[key]; + auto it = mSwitches.find(key); + if (it == mSwitches.end()) + throw std::runtime_error("Incorrect ControlSwitch: " + std::string(key)); + return it->second; } - void ControlSwitch::set(const std::string& key, bool value) + void ControlSwitch::set(std::string_view key, bool value) { - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - - /// \note 7 switches at all, if-else is relevant - if (key == "playercontrols" && !value) - { - player.setLeftRight(0); - player.setForwardBackward(0); - player.setAutoMove(false); - player.setUpDown(0); - } - else if (key == "playerjumping" && !value) - { - /// \fixme maybe crouching at this time - player.setUpDown(0); - } - else if (key == "vanitymode") - { - MWBase::Environment::get().getWorld()->allowVanityMode(value); - } - else if (key == "playerlooking" && !value) + if (key == "playerlooking" && !value) { - MWBase::Environment::get().getWorld()->rotateObject(player.getPlayer(), 0.f, 0.f, 0.f); + auto world = MWBase::Environment::get().getWorld(); + world->rotateObject(world->getPlayerPtr(), osg::Vec3f()); } - mSwitches[key] = value; + auto it = mSwitches.find(key); + if (it == mSwitches.end()) + throw std::runtime_error("Incorrect ControlSwitch: " + std::string(key)); + it->second = value; } void ControlSwitch::write(ESM::ESMWriter& writer, Loading::Listener& /*progress*/) @@ -73,9 +59,9 @@ namespace MWInput controls.mWeaponDrawingDisabled = !mSwitches["playerfighting"]; controls.mSpellDrawingDisabled = !mSwitches["playermagic"]; - writer.startRecord (ESM::REC_INPU); + writer.startRecord(ESM::REC_INPU); controls.save(writer); - writer.endRecord (ESM::REC_INPU); + writer.endRecord(ESM::REC_INPU); } void ControlSwitch::readRecord(ESM::ESMReader& reader, uint32_t type) diff --git a/apps/openmw/mwinput/controlswitch.hpp b/apps/openmw/mwinput/controlswitch.hpp index 38d01066bd6..b86744a06fa 100644 --- a/apps/openmw/mwinput/controlswitch.hpp +++ b/apps/openmw/mwinput/controlswitch.hpp @@ -1,8 +1,10 @@ #ifndef MWINPUT_CONTROLSWITCH_H #define MWINPUT_CONTROLSWITCH_H +#include #include #include +#include namespace ESM { @@ -23,8 +25,8 @@ namespace MWInput public: ControlSwitch(); - bool get(const std::string& key); - void set(const std::string& key, bool value); + bool get(std::string_view key); + void set(std::string_view key, bool value); void clear(); void write(ESM::ESMWriter& writer, Loading::Listener& progress); @@ -32,7 +34,7 @@ namespace MWInput int countSavedGameRecords() const; private: - std::map mSwitches; + std::map> mSwitches; }; } #endif diff --git a/apps/openmw/mwinput/gyromanager.cpp b/apps/openmw/mwinput/gyromanager.cpp new file mode 100644 index 00000000000..01b3afdd89a --- /dev/null +++ b/apps/openmw/mwinput/gyromanager.cpp @@ -0,0 +1,71 @@ +#include "gyromanager.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/player.hpp" + +#include + +namespace MWInput +{ + namespace + { + float getAxisValue(Settings::GyroscopeAxis axis, float threshold, std::array values) + { + const float value = [&] { + switch (axis) + { + case Settings::GyroscopeAxis::X: + return values[0]; + case Settings::GyroscopeAxis::Y: + return values[1]; + case Settings::GyroscopeAxis::Z: + return values[2]; + case Settings::GyroscopeAxis::MinusX: + return -values[0]; + case Settings::GyroscopeAxis::MinusY: + return -values[1]; + case Settings::GyroscopeAxis::MinusZ: + return -values[2]; + }; + return 0.0f; + }(); + if (std::abs(value) <= threshold) + return 0; + return value; + } + } + + void GyroManager::update(float dt, std::array values) const + { + if (mGuiCursorEnabled) + return; + + const float threshold = Settings::input().mGyroInputThreshold; + const float gyroH = getAxisValue(Settings::input().mGyroHorizontalAxis, threshold, values); + const float gyroV = getAxisValue(Settings::input().mGyroVerticalAxis, threshold, values); + + if (gyroH == 0.f && gyroV == 0.f) + return; + + const float rot[3] = { + -gyroV * dt * Settings::input().mGyroVerticalSensitivity, + 0.0f, + -gyroH * dt * Settings::input().mGyroHorizontalSensitivity, + }; + + // Only actually turn player when we're not in vanity mode + const bool playerLooking = MWBase::Environment::get().getInputManager()->getControlSwitch("playerlooking"); + if (!MWBase::Environment::get().getWorld()->vanityRotateCamera(rot) && playerLooking) + { + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + player.yaw(-rot[2]); + player.pitch(-rot[0]); + } + else if (!playerLooking) + MWBase::Environment::get().getWorld()->disableDeferredPreviewRotation(); + + MWBase::Environment::get().getInputManager()->resetIdleTime(); + } +} diff --git a/apps/openmw/mwinput/gyromanager.hpp b/apps/openmw/mwinput/gyromanager.hpp new file mode 100644 index 00000000000..f1064e66652 --- /dev/null +++ b/apps/openmw/mwinput/gyromanager.hpp @@ -0,0 +1,20 @@ +#ifndef MWINPUT_GYROMANAGER +#define MWINPUT_GYROMANAGER + +#include + +namespace MWInput +{ + class GyroManager + { + public: + void update(float dt, std::array values) const; + + void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } + + private: + bool mGuiCursorEnabled = true; + }; +} + +#endif // !MWINPUT_GYROMANAGER diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 436eab7ad3a..328757a954e 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -2,12 +2,13 @@ #include +#include +#include #include -#include -#include +#include -#include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" @@ -16,42 +17,34 @@ #include "bindingsmanager.hpp" #include "controllermanager.hpp" #include "controlswitch.hpp" +#include "gyromanager.hpp" #include "keyboardmanager.hpp" #include "mousemanager.hpp" -#include "sdlmappings.hpp" #include "sensormanager.hpp" namespace MWInput { - InputManager::InputManager( - SDL_Window* window, - osg::ref_ptr viewer, - osg::ref_ptr screenCaptureHandler, - osgViewer::ScreenCaptureHandler::CaptureOperation *screenCaptureOperation, - const std::string& userFile, bool userFileExists, const std::string& userControllerBindingsFile, - const std::string& controllerBindingsFile, bool grab) + InputManager::InputManager(SDL_Window* window, osg::ref_ptr viewer, + osg::ref_ptr screenCaptureHandler, const std::filesystem::path& userFile, + bool userFileExists, const std::filesystem::path& userControllerBindingsFile, + const std::filesystem::path& controllerBindingsFile, bool grab) : mControlsDisabled(false) + , mInputWrapper(std::make_unique(window, viewer, grab)) + , mBindingsManager(std::make_unique(userFile, userFileExists)) + , mControlSwitch(std::make_unique()) + , mActionManager(std::make_unique(mBindingsManager.get(), viewer, screenCaptureHandler)) + , mKeyboardManager(std::make_unique(mBindingsManager.get())) + , mMouseManager(std::make_unique(mBindingsManager.get(), mInputWrapper.get(), window)) + , mControllerManager(std::make_unique( + mBindingsManager.get(), mMouseManager.get(), userControllerBindingsFile, controllerBindingsFile)) + , mSensorManager(std::make_unique()) + , mGyroManager(std::make_unique()) { - mInputWrapper = new SDLUtil::InputWrapper(window, viewer, grab); mInputWrapper->setWindowEventCallback(MWBase::Environment::get().getWindowManager()); - - mBindingsManager = new BindingsManager(userFile, userFileExists); - - mControlSwitch = new ControlSwitch(); - - mActionManager = new ActionManager(mBindingsManager, screenCaptureOperation, viewer, screenCaptureHandler); - - mKeyboardManager = new KeyboardManager(mBindingsManager); - mInputWrapper->setKeyboardEventCallback(mKeyboardManager); - - mMouseManager = new MouseManager(mBindingsManager, mInputWrapper, window); - mInputWrapper->setMouseEventCallback(mMouseManager); - - mControllerManager = new ControllerManager(mBindingsManager, mActionManager, mMouseManager, userControllerBindingsFile, controllerBindingsFile); - mInputWrapper->setControllerEventCallback(mControllerManager); - - mSensorManager = new SensorManager(); - mInputWrapper->setSensorEventCallback(mSensorManager); + mInputWrapper->setKeyboardEventCallback(mKeyboardManager.get()); + mInputWrapper->setMouseEventCallback(mMouseManager.get()); + mInputWrapper->setControllerEventCallback(mControllerManager.get()); + mInputWrapper->setSensorEventCallback(mSensorManager.get()); } void InputManager::clear() @@ -60,25 +53,7 @@ namespace MWInput mControlSwitch->clear(); } - InputManager::~InputManager() - { - delete mActionManager; - delete mControllerManager; - delete mKeyboardManager; - delete mMouseManager; - delete mSensorManager; - - delete mControlSwitch; - - delete mBindingsManager; - - delete mInputWrapper; - } - - void InputManager::setAttemptJump(bool jumping) - { - mActionManager->setAttemptJump(jumping); - } + InputManager::~InputManager() {} void InputManager::update(float dt, bool disableControls, bool disableEvents) { @@ -97,12 +72,21 @@ namespace MWInput mMouseManager->updateCursorMode(); - bool controllerMove = mControllerManager->update(dt); + mControllerManager->update(dt); mMouseManager->update(dt); mSensorManager->update(dt); - mActionManager->update(dt, controllerMove); + mActionManager->update(dt); - MWBase::Environment::get().getWorld()->applyDeferredPreviewRotationToPlayer(dt); + if (Settings::input().mEnableGyroscope) + { + bool controllerAvailable = mControllerManager->isGyroAvailable(); + bool sensorAvailable = mSensorManager->isGyroAvailable(); + if (controllerAvailable || sensorAvailable) + { + mGyroManager->update( + dt, controllerAvailable ? mControllerManager->getGyroValues() : mSensorManager->getGyroValues()); + } + } } void InputManager::setDragDrop(bool dragDrop) @@ -115,32 +99,37 @@ namespace MWInput mControllerManager->setGamepadGuiCursorEnabled(enabled); } + bool InputManager::isGamepadGuiCursorEnabled() + { + return mControllerManager->gamepadGuiCursorEnabled(); + } + void InputManager::changeInputMode(bool guiMode) { mControllerManager->setGuiCursorEnabled(guiMode); mMouseManager->setGuiCursorEnabled(guiMode); - mSensorManager->setGuiCursorEnabled(guiMode); + mGyroManager->setGuiCursorEnabled(guiMode); mMouseManager->setMouseLookEnabled(!guiMode); if (guiMode) MWBase::Environment::get().getWindowManager()->showCrosshair(false); - bool isCursorVisible = guiMode && (!mControllerManager->joystickLastUsed() || mControllerManager->gamepadGuiCursorEnabled()); + bool isCursorVisible + = guiMode && (!mControllerManager->joystickLastUsed() || mControllerManager->gamepadGuiCursorEnabled()); MWBase::Environment::get().getWindowManager()->setCursorVisible(isCursorVisible); // if not in gui mode, the camera decides whether to show crosshair or not. } void InputManager::processChangedSettings(const Settings::CategorySettingVector& changed) { - mMouseManager->processChangedSettings(changed); mSensorManager->processChangedSettings(changed); } - bool InputManager::getControlSwitch(const std::string& sw) + bool InputManager::getControlSwitch(std::string_view sw) { return mControlSwitch->get(sw); } - void InputManager::toggleControlSwitch(const std::string& sw, bool value) + void InputManager::toggleControlSwitch(std::string_view sw, bool value) { mControlSwitch->set(sw, value); } @@ -150,27 +139,62 @@ namespace MWInput mActionManager->resetIdleTime(); } - std::string InputManager::getActionDescription(int action) + bool InputManager::isIdle() const + { + return mActionManager->getIdleTime() > 0.5; + } + + std::string_view InputManager::getActionDescription(int action) const { return mBindingsManager->getActionDescription(action); } - std::string InputManager::getActionKeyBindingName(int action) + std::string InputManager::getActionKeyBindingName(int action) const { return mBindingsManager->getActionKeyBindingName(action); } - std::string InputManager::getActionControllerBindingName(int action) + std::string InputManager::getActionControllerBindingName(int action) const { return mBindingsManager->getActionControllerBindingName(action); } - std::vector InputManager::getActionKeySorting() + bool InputManager::actionIsActive(int action) const + { + return mBindingsManager->actionIsActive(action); + } + + float InputManager::getActionValue(int action) const + { + return mBindingsManager->getActionValue(action); + } + + bool InputManager::isControllerButtonPressed(SDL_GameControllerButton button) const + { + return mControllerManager->isButtonPressed(button); + } + + float InputManager::getControllerAxisValue(SDL_GameControllerAxis axis) const + { + return mControllerManager->getAxisValue(axis); + } + + int InputManager::getMouseMoveX() const + { + return mMouseManager->getMouseMoveX(); + } + + int InputManager::getMouseMoveY() const + { + return mMouseManager->getMouseMoveY(); + } + + const std::initializer_list& InputManager::getActionKeySorting() { return mBindingsManager->getActionKeySorting(); } - std::vector InputManager::getActionControllerSorting() + const std::initializer_list& InputManager::getActionControllerSorting() { return mBindingsManager->getActionControllerSorting(); } diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index f930836d1ce..6131d77c658 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -1,16 +1,17 @@ #ifndef MWINPUT_MWINPUTMANAGERIMP_H #define MWINPUT_MWINPUTMANAGERIMP_H +#include + #include #include -#include #include +#include +#include #include "../mwbase/inputmanager.hpp" -#include "../mwgui/mode.hpp" - #include "actions.hpp" namespace MWWorld @@ -39,28 +40,25 @@ namespace MWInput class KeyboardManager; class MouseManager; class SensorManager; + class GyroManager; /** - * @brief Class that provides a high-level API for game input - */ - class InputManager : public MWBase::InputManager + * @brief Class that provides a high-level API for game input + */ + class InputManager final : public MWBase::InputManager { public: - InputManager( - SDL_Window* window, - osg::ref_ptr viewer, - osg::ref_ptr screenCaptureHandler, - osgViewer::ScreenCaptureHandler::CaptureOperation *screenCaptureOperation, - const std::string& userFile, bool userFileExists, - const std::string& userControllerBindingsFile, - const std::string& controllerBindingsFile, bool grab); + InputManager(SDL_Window* window, osg::ref_ptr viewer, + osg::ref_ptr screenCaptureHandler, const std::filesystem::path& userFile, + bool userFileExists, const std::filesystem::path& userControllerBindingsFile, + const std::filesystem::path& controllerBindingsFile, bool grab); - virtual ~InputManager(); + ~InputManager() final; /// Clear all savegame-specific data void clear() override; - void update(float dt, bool disableControls=false, bool disableEvents=false) override; + void update(float dt, bool disableControls, bool disableEvents = false) override; void changeInputMode(bool guiMode) override; @@ -68,18 +66,26 @@ namespace MWInput void setDragDrop(bool dragDrop) override; void setGamepadGuiCursorEnabled(bool enabled) override; - void setAttemptJump(bool jumping) override; + bool isGamepadGuiCursorEnabled() override; - void toggleControlSwitch (const std::string& sw, bool value) override; - bool getControlSwitch (const std::string& sw) override; + void toggleControlSwitch(std::string_view sw, bool value) override; + bool getControlSwitch(std::string_view sw) override; + + std::string_view getActionDescription(int action) const override; + std::string getActionKeyBindingName(int action) const override; + std::string getActionControllerBindingName(int action) const override; + bool actionIsActive(int action) const override; + + float getActionValue(int action) const override; + bool isControllerButtonPressed(SDL_GameControllerButton button) const override; + float getControllerAxisValue(SDL_GameControllerAxis axis) const override; + int getMouseMoveX() const override; + int getMouseMoveY() const override; - std::string getActionDescription (int action) override; - std::string getActionKeyBindingName (int action) override; - std::string getActionControllerBindingName (int action) override; int getNumActions() override { return A_Last; } - std::vector getActionKeySorting() override; - std::vector getActionControllerSorting() override; - void enableDetectingBindingMode (int action, bool keyboard) override; + const std::initializer_list& getActionKeySorting() override; + const std::initializer_list& getActionControllerSorting() override; + void enableDetectingBindingMode(int action, bool keyboard) override; void resetToDefaultKeyBindings() override; void resetToDefaultControllerBindings() override; @@ -91,6 +97,7 @@ namespace MWInput void readRecord(ESM::ESMReader& reader, uint32_t type) override; void resetIdleTime() override; + bool isIdle() const override; void executeAction(int action) override; @@ -107,18 +114,17 @@ namespace MWInput void loadKeyDefaults(bool force = false); void loadControllerDefaults(bool force = false); - SDLUtil::InputWrapper* mInputWrapper; - bool mControlsDisabled; - ControlSwitch* mControlSwitch; - - ActionManager* mActionManager; - BindingsManager* mBindingsManager; - ControllerManager* mControllerManager; - KeyboardManager* mKeyboardManager; - MouseManager* mMouseManager; - SensorManager* mSensorManager; + std::unique_ptr mInputWrapper; + std::unique_ptr mBindingsManager; + std::unique_ptr mControlSwitch; + std::unique_ptr mActionManager; + std::unique_ptr mKeyboardManager; + std::unique_ptr mMouseManager; + std::unique_ptr mControllerManager; + std::unique_ptr mSensorManager; + std::unique_ptr mGyroManager; }; } #endif diff --git a/apps/openmw/mwinput/keyboardmanager.cpp b/apps/openmw/mwinput/keyboardmanager.cpp index 8540858461d..6b1daf45000 100644 --- a/apps/openmw/mwinput/keyboardmanager.cpp +++ b/apps/openmw/mwinput/keyboardmanager.cpp @@ -4,15 +4,15 @@ #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwworld/player.hpp" - #include "actions.hpp" #include "bindingsmanager.hpp" -#include "sdlmappings.hpp" namespace MWInput { @@ -21,7 +21,7 @@ namespace MWInput { } - void KeyboardManager::textInput(const SDL_TextInputEvent &arg) + void KeyboardManager::textInput(const SDL_TextInputEvent& arg) { MyGUI::UString ustring(&arg.text[0]); MyGUI::UString::utf32string utf32string = ustring.asUTF32(); @@ -29,21 +29,21 @@ namespace MWInput MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::None, *it); } - void KeyboardManager::keyPressed(const SDL_KeyboardEvent &arg) + void KeyboardManager::keyPressed(const SDL_KeyboardEvent& arg) { // HACK: to make default keybinding for the console work without printing an extra "^" upon closing // This assumes that SDL_TextInput events always come *after* the key event // (which is somewhat reasonable, and hopefully true for all SDL platforms) - auto kc = sdlKeyToMyGUI(arg.keysym.sym); + auto kc = SDLUtil::sdlKeyToMyGUI(arg.keysym.sym); if (mBindingsManager->getKeyBinding(A_Console) == arg.keysym.scancode - && MWBase::Environment::get().getWindowManager()->isConsoleMode()) + && MWBase::Environment::get().getWindowManager()->isConsoleMode()) SDL_StopTextInput(); - bool consumed = SDL_IsTextInputActive() && // Little trick to check if key is printable - (!(SDLK_SCANCODE_MASK & arg.keysym.sym) && - (std::isprint(arg.keysym.sym) || - // Don't trust isprint for symbols outside the extended ASCII range - (kc == MyGUI::KeyCode::None && arg.keysym.sym > 0xff))); + bool consumed = SDL_IsTextInputActive() && // Little trick to check if key is printable + (!(SDLK_SCANCODE_MASK & arg.keysym.sym) && + // Don't trust isprint for symbols outside the extended ASCII range + ((kc == MyGUI::KeyCode::None && arg.keysym.sym > 0xff) + || (arg.keysym.sym >= 0 && arg.keysym.sym <= 255 && std::isprint(arg.keysym.sym)))); if (kc != MyGUI::KeyCode::None && !mBindingsManager->isDetectingBindingState()) { if (MWBase::Environment::get().getWindowManager()->injectKeyPress(kc, 0, arg.repeat)) @@ -58,16 +58,24 @@ namespace MWInput if (!input->controlsDisabled() && !consumed) mBindingsManager->keyPressed(arg); + if (!consumed) + { + MWBase::Environment::get().getLuaManager()->inputEvent( + { MWBase::LuaManager::InputEvent::KeyPressed, arg.keysym }); + } + input->setJoystickLastUsed(false); } - void KeyboardManager::keyReleased(const SDL_KeyboardEvent &arg) + void KeyboardManager::keyReleased(const SDL_KeyboardEvent& arg) { MWBase::Environment::get().getInputManager()->setJoystickLastUsed(false); - auto kc = sdlKeyToMyGUI(arg.keysym.sym); + auto kc = SDLUtil::sdlKeyToMyGUI(arg.keysym.sym); if (!mBindingsManager->isDetectingBindingState()) mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc)); mBindingsManager->keyReleased(arg); + MWBase::Environment::get().getLuaManager()->inputEvent( + { MWBase::LuaManager::InputEvent::KeyReleased, arg.keysym }); } } diff --git a/apps/openmw/mwinput/keyboardmanager.hpp b/apps/openmw/mwinput/keyboardmanager.hpp index ca58461a209..f7b1bbee2b5 100644 --- a/apps/openmw/mwinput/keyboardmanager.hpp +++ b/apps/openmw/mwinput/keyboardmanager.hpp @@ -14,9 +14,9 @@ namespace MWInput virtual ~KeyboardManager() = default; - void textInput(const SDL_TextInputEvent &arg) override; - void keyPressed(const SDL_KeyboardEvent &arg) override; - void keyReleased(const SDL_KeyboardEvent &arg) override; + void textInput(const SDL_TextInputEvent& arg) override; + void keyPressed(const SDL_KeyboardEvent& arg) override; + void keyReleased(const SDL_KeyboardEvent& arg) override; private: BindingsManager* mBindingsManager; diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index cf151dfac75..eed95cf1c92 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -3,39 +3,39 @@ #include #include #include -#include -#include #include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwgui/settingswindow.hpp" + #include "../mwworld/player.hpp" #include "actions.hpp" #include "bindingsmanager.hpp" -#include "sdlmappings.hpp" namespace MWInput { - MouseManager::MouseManager(BindingsManager* bindingsManager, SDLUtil::InputWrapper* inputWrapper, SDL_Window* window) - : mInvertX(Settings::Manager::getBool("invert x axis", "Input")) - , mInvertY(Settings::Manager::getBool("invert y axis", "Input")) - , mGrabCursor(Settings::Manager::getBool("grab cursor", "Input")) - , mCameraSensitivity(Settings::Manager::getFloat("camera sensitivity", "Input")) - , mCameraYMultiplier(Settings::Manager::getFloat("camera y multiplier", "Input")) - , mBindingsManager(bindingsManager) + MouseManager::MouseManager( + BindingsManager* bindingsManager, SDLUtil::InputWrapper* inputWrapper, SDL_Window* window) + : mBindingsManager(bindingsManager) , mInputWrapper(inputWrapper) , mGuiCursorX(0) , mGuiCursorY(0) , mMouseWheel(0) , mMouseLookEnabled(false) , mGuiCursorEnabled(true) + , mMouseMoveX(0) + , mMouseMoveY(0) { - int w,h; + int w, h; SDL_GetWindowSize(window, &w, &h); float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); @@ -43,25 +43,7 @@ namespace MWInput mGuiCursorY = h / (2.f * uiScale); } - void MouseManager::processChangedSettings(const Settings::CategorySettingVector& changed) - { - for (const auto& setting : changed) - { - if (setting.first == "Input" && setting.second == "invert x axis") - mInvertX = Settings::Manager::getBool("invert x axis", "Input"); - - if (setting.first == "Input" && setting.second == "invert y axis") - mInvertY = Settings::Manager::getBool("invert y axis", "Input"); - - if (setting.first == "Input" && setting.second == "camera sensitivity") - mCameraSensitivity = Settings::Manager::getFloat("camera sensitivity", "Input"); - - if (setting.first == "Input" && setting.second == "grab cursor") - mGrabCursor = Settings::Manager::getBool("grab cursor", "Input"); - } - } - - void MouseManager::mouseMoved(const SDLUtil::MouseMotionEvent &arg) + void MouseManager::mouseMoved(const SDLUtil::MouseMotionEvent& arg) { mBindingsManager->mouseMoved(arg); @@ -81,9 +63,12 @@ namespace MWInput mMouseWheel = static_cast(arg.z); - MyGUI::InputManager::getInstance().injectMouseMove(static_cast(mGuiCursorX), static_cast(mGuiCursorY), mMouseWheel); - // FIXME: inject twice to force updating focused widget states (tooltips) resulting from changing the viewport by scroll wheel - MyGUI::InputManager::getInstance().injectMouseMove(static_cast(mGuiCursorX), static_cast(mGuiCursorY), mMouseWheel); + MyGUI::InputManager::getInstance().injectMouseMove( + static_cast(mGuiCursorX), static_cast(mGuiCursorY), mMouseWheel); + // FIXME: inject twice to force updating focused widget states (tooltips) resulting from changing the + // viewport by scroll wheel + MyGUI::InputManager::getInstance().injectMouseMove( + static_cast(mGuiCursorX), static_cast(mGuiCursorY), mMouseWheel); MWBase::Environment::get().getWindowManager()->setCursorActive(true); } @@ -92,8 +77,10 @@ namespace MWInput { MWBase::World* world = MWBase::Environment::get().getWorld(); - float x = arg.xrel * mCameraSensitivity * (mInvertX ? -1 : 1) / 256.f; - float y = arg.yrel * mCameraSensitivity * (mInvertY ? -1 : 1) * mCameraYMultiplier / 256.f; + const float cameraSensitivity = Settings::input().mCameraSensitivity; + float x = arg.xrel * cameraSensitivity * (Settings::input().mInvertXAxis ? -1 : 1) / 256.f; + float y = arg.yrel * cameraSensitivity * (Settings::input().mInvertYAxis ? -1 : 1) + * Settings::input().mCameraYMultiplier / 256.f; float rot[3]; rot[0] = -y; @@ -112,7 +99,7 @@ namespace MWInput } } - void MouseManager::mouseReleased(const SDL_MouseButtonEvent &arg, Uint8 id) + void MouseManager::mouseReleased(const SDL_MouseButtonEvent& arg, Uint8 id) { MWBase::Environment::get().getInputManager()->setJoystickLastUsed(false); @@ -123,7 +110,9 @@ namespace MWInput else { bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); - guiMode = MyGUI::InputManager::getInstance().injectMouseRelease(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(id)) && guiMode; + guiMode = MyGUI::InputManager::getInstance().injectMouseRelease(static_cast(mGuiCursorX), + static_cast(mGuiCursorY), SDLUtil::sdlMouseButtonToMyGui(id)) + && guiMode; if (mBindingsManager->isDetectingBindingState()) return; // don't allow same mouseup to bind as initiated bind @@ -131,18 +120,25 @@ namespace MWInput mBindingsManager->setPlayerControlsEnabled(!guiMode); mBindingsManager->mouseReleased(arg, id); } + + MWBase::Environment::get().getLuaManager()->inputEvent( + { MWBase::LuaManager::InputEvent::MouseButtonReleased, arg.button }); } - void MouseManager::mouseWheelMoved(const SDL_MouseWheelEvent &arg) + void MouseManager::mouseWheelMoved(const SDL_MouseWheelEvent& arg) { MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); if (mBindingsManager->isDetectingBindingState() || !input->controlsDisabled()) + { mBindingsManager->mouseWheelMoved(arg); + } input->setJoystickLastUsed(false); + MWBase::Environment::get().getLuaManager()->inputEvent({ MWBase::LuaManager::InputEvent::MouseWheel, + MWBase::LuaManager::InputEvent::WheelChange{ arg.x, arg.y } }); } - void MouseManager::mousePressed(const SDL_MouseButtonEvent &arg, Uint8 id) + void MouseManager::mousePressed(const SDL_MouseButtonEvent& arg, Uint8 id) { MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); input->setJoystickLastUsed(false); @@ -151,13 +147,16 @@ namespace MWInput if (id == SDL_BUTTON_LEFT || id == SDL_BUTTON_RIGHT) // MyGUI only uses these mouse events { guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); - guiMode = MyGUI::InputManager::getInstance().injectMousePress(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(id)) && guiMode; - if (MyGUI::InputManager::getInstance().getMouseFocusWidget () != nullptr) + guiMode = MyGUI::InputManager::getInstance().injectMousePress(static_cast(mGuiCursorX), + static_cast(mGuiCursorY), SDLUtil::sdlMouseButtonToMyGui(id)) + && guiMode; + if (MyGUI::InputManager::getInstance().getMouseFocusWidget() != nullptr) { - MyGUI::Button* b = MyGUI::InputManager::getInstance().getMouseFocusWidget()->castType(false); + MyGUI::Button* b + = MyGUI::InputManager::getInstance().getMouseFocusWidget()->castType(false); if (b && b->getEnabled() && id == SDL_BUTTON_LEFT) { - MWBase::Environment::get().getWindowManager()->playSound("Menu Click"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); } } MWBase::Environment::get().getWindowManager()->setCursorActive(true); @@ -167,14 +166,18 @@ namespace MWInput // Don't trigger any mouse bindings while in settings menu, otherwise rebinding controls becomes impossible // Also do not trigger bindings when input controls are disabled, e.g. during save loading - if (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Settings && !input->controlsDisabled()) + if (!MWBase::Environment::get().getWindowManager()->isSettingsWindowVisible() && !input->controlsDisabled()) + { mBindingsManager->mousePressed(arg, id); + } + MWBase::Environment::get().getLuaManager()->inputEvent( + { MWBase::LuaManager::InputEvent::MouseButtonPressed, arg.button }); } void MouseManager::updateCursorMode() { bool grab = !MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu) - && !MWBase::Environment::get().getWindowManager()->isConsoleMode(); + && !MWBase::Environment::get().getWindowManager()->isConsoleMode(); bool wasRelative = mInputWrapper->getMouseRelative(); bool isRelative = !MWBase::Environment::get().getWindowManager()->isGuiMode(); @@ -183,11 +186,11 @@ namespace MWInput // stop using raw mouse motions and switch to system cursor movements mInputWrapper->setMouseRelative(isRelative); - //we let the mouse escape in the main menu - mInputWrapper->setGrabPointer(grab && (mGrabCursor || isRelative)); + // we let the mouse escape in the main menu + mInputWrapper->setGrabPointer(grab && (Settings::input().mGrabCursor || isRelative)); - //we switched to non-relative mode, move our cursor to where the in-game - //cursor is + // we switched to non-relative mode, move our cursor to where the in-game + // cursor is if (!isRelative && wasRelative != isRelative) { warpMouse(); @@ -196,6 +199,8 @@ namespace MWInput void MouseManager::update(float dt) { + SDL_GetRelativeMouseState(&mMouseMoveX, &mMouseMoveY); + if (!mMouseLookEnabled) return; @@ -204,20 +209,23 @@ namespace MWInput if (xAxis == 0 && yAxis == 0) return; - float rot[3]; - rot[0] = -yAxis * dt * 1000.0f * mCameraSensitivity * (mInvertY ? -1 : 1) * mCameraYMultiplier / 256.f; - rot[1] = 0.0f; - rot[2] = -xAxis * dt * 1000.0f * mCameraSensitivity * (mInvertX ? -1 : 1) / 256.f; + const float cameraSensitivity = Settings::input().mCameraSensitivity; + const float rot[3] = { + -yAxis * dt * 1000.0f * cameraSensitivity * (Settings::input().mInvertYAxis ? -1 : 1) + * Settings::input().mCameraYMultiplier / 256.f, + 0.0f, + -xAxis * dt * 1000.0f * cameraSensitivity * (Settings::input().mInvertXAxis ? -1 : 1) / 256.f, + }; // Only actually turn player when we're not in vanity mode - bool controls = MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols"); - if (!MWBase::Environment::get().getWorld()->vanityRotateCamera(rot) && controls) + bool playerLooking = MWBase::Environment::get().getInputManager()->getControlSwitch("playerlooking"); + if (!MWBase::Environment::get().getWorld()->vanityRotateCamera(rot) && playerLooking) { MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); player.yaw(-rot[2]); player.pitch(-rot[0]); } - else if (!controls) + else if (!playerLooking) MWBase::Environment::get().getWorld()->disableDeferredPreviewRotation(); MWBase::Environment::get().getInputManager()->resetIdleTime(); @@ -225,12 +233,14 @@ namespace MWInput bool MouseManager::injectMouseButtonPress(Uint8 button) { - return MyGUI::InputManager::getInstance().injectMousePress(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(button)); + return MyGUI::InputManager::getInstance().injectMousePress( + static_cast(mGuiCursorX), static_cast(mGuiCursorY), SDLUtil::sdlMouseButtonToMyGui(button)); } bool MouseManager::injectMouseButtonRelease(Uint8 button) { - return MyGUI::InputManager::getInstance().injectMouseRelease(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(button)); + return MyGUI::InputManager::getInstance().injectMouseRelease( + static_cast(mGuiCursorX), static_cast(mGuiCursorY), SDLUtil::sdlMouseButtonToMyGui(button)); } void MouseManager::injectMouseMove(float xMove, float yMove, float mouseWheelMove) @@ -240,15 +250,17 @@ namespace MWInput mMouseWheel += mouseWheelMove; const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - mGuiCursorX = std::max(0.f, std::min(mGuiCursorX, float(viewSize.width - 1))); - mGuiCursorY = std::max(0.f, std::min(mGuiCursorY, float(viewSize.height - 1))); + mGuiCursorX = std::clamp(mGuiCursorX, 0.f, viewSize.width - 1); + mGuiCursorY = std::clamp(mGuiCursorY, 0.f, viewSize.height - 1); - MyGUI::InputManager::getInstance().injectMouseMove(static_cast(mGuiCursorX), static_cast(mGuiCursorY), static_cast(mMouseWheel)); + MyGUI::InputManager::getInstance().injectMouseMove( + static_cast(mGuiCursorX), static_cast(mGuiCursorY), static_cast(mMouseWheel)); } void MouseManager::warpMouse() { - float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); - mInputWrapper->warpMouse(static_cast(mGuiCursorX*uiScale), static_cast(mGuiCursorY*uiScale)); + float guiUiScale = Settings::gui().mScalingFactor; + mInputWrapper->warpMouse( + static_cast(mGuiCursorX * guiUiScale), static_cast(mGuiCursorY * guiUiScale)); } } diff --git a/apps/openmw/mwinput/mousemanager.hpp b/apps/openmw/mwinput/mousemanager.hpp index 000e7cd0b68..5de8a8f3bc0 100644 --- a/apps/openmw/mwinput/mousemanager.hpp +++ b/apps/openmw/mwinput/mousemanager.hpp @@ -1,8 +1,8 @@ #ifndef MWINPUT_MWMOUSEMANAGER_H #define MWINPUT_MWMOUSEMANAGER_H -#include #include +#include namespace SDLUtil { @@ -23,12 +23,10 @@ namespace MWInput void updateCursorMode(); void update(float dt); - void mouseMoved(const SDLUtil::MouseMotionEvent &arg) override; - void mousePressed(const SDL_MouseButtonEvent &arg, Uint8 id) override; - void mouseReleased(const SDL_MouseButtonEvent &arg, Uint8 id) override; - void mouseWheelMoved(const SDL_MouseWheelEvent &arg) override; - - void processChangedSettings(const Settings::CategorySettingVector& changed); + void mouseMoved(const SDLUtil::MouseMotionEvent& arg) override; + void mousePressed(const SDL_MouseButtonEvent& arg, Uint8 id) override; + void mouseReleased(const SDL_MouseButtonEvent& arg, Uint8 id) override; + void mouseWheelMoved(const SDL_MouseWheelEvent& arg) override; bool injectMouseButtonPress(Uint8 button); bool injectMouseButtonRelease(Uint8 button); @@ -38,13 +36,10 @@ namespace MWInput void setMouseLookEnabled(bool enabled) { mMouseLookEnabled = enabled; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } - private: - bool mInvertX; - bool mInvertY; - bool mGrabCursor; - float mCameraSensitivity; - float mCameraYMultiplier; + int getMouseMoveX() const { return mMouseMoveX; } + int getMouseMoveY() const { return mMouseMoveY; } + private: BindingsManager* mBindingsManager; SDLUtil::InputWrapper* mInputWrapper; @@ -53,6 +48,9 @@ namespace MWInput int mMouseWheel; bool mMouseLookEnabled; bool mGuiCursorEnabled; + + int mMouseMoveX; + int mMouseMoveY; }; } #endif diff --git a/apps/openmw/mwinput/sdlmappings.cpp b/apps/openmw/mwinput/sdlmappings.cpp deleted file mode 100644 index 0c3f5c5d856..00000000000 --- a/apps/openmw/mwinput/sdlmappings.cpp +++ /dev/null @@ -1,218 +0,0 @@ -#include "sdlmappings.hpp" - -#include - -#include - -#include -#include - -namespace MWInput -{ - std::string sdlControllerButtonToString(int button) - { - switch(button) - { - case SDL_CONTROLLER_BUTTON_A: - return "A Button"; - case SDL_CONTROLLER_BUTTON_B: - return "B Button"; - case SDL_CONTROLLER_BUTTON_BACK: - return "Back Button"; - case SDL_CONTROLLER_BUTTON_DPAD_DOWN: - return "DPad Down"; - case SDL_CONTROLLER_BUTTON_DPAD_LEFT: - return "DPad Left"; - case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: - return "DPad Right"; - case SDL_CONTROLLER_BUTTON_DPAD_UP: - return "DPad Up"; - case SDL_CONTROLLER_BUTTON_GUIDE: - return "Guide Button"; - case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: - return "Left Shoulder"; - case SDL_CONTROLLER_BUTTON_LEFTSTICK: - return "Left Stick Button"; - case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: - return "Right Shoulder"; - case SDL_CONTROLLER_BUTTON_RIGHTSTICK: - return "Right Stick Button"; - case SDL_CONTROLLER_BUTTON_START: - return "Start Button"; - case SDL_CONTROLLER_BUTTON_X: - return "X Button"; - case SDL_CONTROLLER_BUTTON_Y: - return "Y Button"; - default: - return "Button " + std::to_string(button); - } - } - - std::string sdlControllerAxisToString(int axis) - { - switch(axis) - { - case SDL_CONTROLLER_AXIS_LEFTX: - return "Left Stick X"; - case SDL_CONTROLLER_AXIS_LEFTY: - return "Left Stick Y"; - case SDL_CONTROLLER_AXIS_RIGHTX: - return "Right Stick X"; - case SDL_CONTROLLER_AXIS_RIGHTY: - return "Right Stick Y"; - case SDL_CONTROLLER_AXIS_TRIGGERLEFT: - return "Left Trigger"; - case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: - return "Right Trigger"; - default: - return "Axis " + std::to_string(axis); - } - } - - MyGUI::MouseButton sdlButtonToMyGUI(Uint8 button) - { - //The right button is the second button, according to MyGUI - if(button == SDL_BUTTON_RIGHT) - button = SDL_BUTTON_MIDDLE; - else if(button == SDL_BUTTON_MIDDLE) - button = SDL_BUTTON_RIGHT; - - //MyGUI's buttons are 0 indexed - return MyGUI::MouseButton::Enum(button - 1); - } - - void initKeyMap(std::map& keyMap) - { - keyMap[SDLK_UNKNOWN] = MyGUI::KeyCode::None; - keyMap[SDLK_ESCAPE] = MyGUI::KeyCode::Escape; - keyMap[SDLK_1] = MyGUI::KeyCode::One; - keyMap[SDLK_2] = MyGUI::KeyCode::Two; - keyMap[SDLK_3] = MyGUI::KeyCode::Three; - keyMap[SDLK_4] = MyGUI::KeyCode::Four; - keyMap[SDLK_5] = MyGUI::KeyCode::Five; - keyMap[SDLK_6] = MyGUI::KeyCode::Six; - keyMap[SDLK_7] = MyGUI::KeyCode::Seven; - keyMap[SDLK_8] = MyGUI::KeyCode::Eight; - keyMap[SDLK_9] = MyGUI::KeyCode::Nine; - keyMap[SDLK_0] = MyGUI::KeyCode::Zero; - keyMap[SDLK_MINUS] = MyGUI::KeyCode::Minus; - keyMap[SDLK_EQUALS] = MyGUI::KeyCode::Equals; - keyMap[SDLK_BACKSPACE] = MyGUI::KeyCode::Backspace; - keyMap[SDLK_TAB] = MyGUI::KeyCode::Tab; - keyMap[SDLK_q] = MyGUI::KeyCode::Q; - keyMap[SDLK_w] = MyGUI::KeyCode::W; - keyMap[SDLK_e] = MyGUI::KeyCode::E; - keyMap[SDLK_r] = MyGUI::KeyCode::R; - keyMap[SDLK_t] = MyGUI::KeyCode::T; - keyMap[SDLK_y] = MyGUI::KeyCode::Y; - keyMap[SDLK_u] = MyGUI::KeyCode::U; - keyMap[SDLK_i] = MyGUI::KeyCode::I; - keyMap[SDLK_o] = MyGUI::KeyCode::O; - keyMap[SDLK_p] = MyGUI::KeyCode::P; - keyMap[SDLK_RETURN] = MyGUI::KeyCode::Return; - keyMap[SDLK_a] = MyGUI::KeyCode::A; - keyMap[SDLK_s] = MyGUI::KeyCode::S; - keyMap[SDLK_d] = MyGUI::KeyCode::D; - keyMap[SDLK_f] = MyGUI::KeyCode::F; - keyMap[SDLK_g] = MyGUI::KeyCode::G; - keyMap[SDLK_h] = MyGUI::KeyCode::H; - keyMap[SDLK_j] = MyGUI::KeyCode::J; - keyMap[SDLK_k] = MyGUI::KeyCode::K; - keyMap[SDLK_l] = MyGUI::KeyCode::L; - keyMap[SDLK_SEMICOLON] = MyGUI::KeyCode::Semicolon; - keyMap[SDLK_QUOTE] = MyGUI::KeyCode::Apostrophe; - keyMap[SDLK_BACKQUOTE] = MyGUI::KeyCode::Grave; - keyMap[SDLK_LSHIFT] = MyGUI::KeyCode::LeftShift; - keyMap[SDLK_BACKSLASH] = MyGUI::KeyCode::Backslash; - keyMap[SDLK_z] = MyGUI::KeyCode::Z; - keyMap[SDLK_x] = MyGUI::KeyCode::X; - keyMap[SDLK_c] = MyGUI::KeyCode::C; - keyMap[SDLK_v] = MyGUI::KeyCode::V; - keyMap[SDLK_b] = MyGUI::KeyCode::B; - keyMap[SDLK_n] = MyGUI::KeyCode::N; - keyMap[SDLK_m] = MyGUI::KeyCode::M; - keyMap[SDLK_COMMA] = MyGUI::KeyCode::Comma; - keyMap[SDLK_PERIOD] = MyGUI::KeyCode::Period; - keyMap[SDLK_SLASH] = MyGUI::KeyCode::Slash; - keyMap[SDLK_RSHIFT] = MyGUI::KeyCode::RightShift; - keyMap[SDLK_KP_MULTIPLY] = MyGUI::KeyCode::Multiply; - keyMap[SDLK_LALT] = MyGUI::KeyCode::LeftAlt; - keyMap[SDLK_SPACE] = MyGUI::KeyCode::Space; - keyMap[SDLK_CAPSLOCK] = MyGUI::KeyCode::Capital; - keyMap[SDLK_F1] = MyGUI::KeyCode::F1; - keyMap[SDLK_F2] = MyGUI::KeyCode::F2; - keyMap[SDLK_F3] = MyGUI::KeyCode::F3; - keyMap[SDLK_F4] = MyGUI::KeyCode::F4; - keyMap[SDLK_F5] = MyGUI::KeyCode::F5; - keyMap[SDLK_F6] = MyGUI::KeyCode::F6; - keyMap[SDLK_F7] = MyGUI::KeyCode::F7; - keyMap[SDLK_F8] = MyGUI::KeyCode::F8; - keyMap[SDLK_F9] = MyGUI::KeyCode::F9; - keyMap[SDLK_F10] = MyGUI::KeyCode::F10; - keyMap[SDLK_NUMLOCKCLEAR] = MyGUI::KeyCode::NumLock; - keyMap[SDLK_SCROLLLOCK] = MyGUI::KeyCode::ScrollLock; - keyMap[SDLK_KP_7] = MyGUI::KeyCode::Numpad7; - keyMap[SDLK_KP_8] = MyGUI::KeyCode::Numpad8; - keyMap[SDLK_KP_9] = MyGUI::KeyCode::Numpad9; - keyMap[SDLK_KP_MINUS] = MyGUI::KeyCode::Subtract; - keyMap[SDLK_KP_4] = MyGUI::KeyCode::Numpad4; - keyMap[SDLK_KP_5] = MyGUI::KeyCode::Numpad5; - keyMap[SDLK_KP_6] = MyGUI::KeyCode::Numpad6; - keyMap[SDLK_KP_PLUS] = MyGUI::KeyCode::Add; - keyMap[SDLK_KP_1] = MyGUI::KeyCode::Numpad1; - keyMap[SDLK_KP_2] = MyGUI::KeyCode::Numpad2; - keyMap[SDLK_KP_3] = MyGUI::KeyCode::Numpad3; - keyMap[SDLK_KP_0] = MyGUI::KeyCode::Numpad0; - keyMap[SDLK_KP_PERIOD] = MyGUI::KeyCode::Decimal; - keyMap[SDLK_F11] = MyGUI::KeyCode::F11; - keyMap[SDLK_F12] = MyGUI::KeyCode::F12; - keyMap[SDLK_F13] = MyGUI::KeyCode::F13; - keyMap[SDLK_F14] = MyGUI::KeyCode::F14; - keyMap[SDLK_F15] = MyGUI::KeyCode::F15; - keyMap[SDLK_KP_EQUALS] = MyGUI::KeyCode::NumpadEquals; - keyMap[SDLK_COLON] = MyGUI::KeyCode::Colon; - keyMap[SDLK_KP_ENTER] = MyGUI::KeyCode::NumpadEnter; - keyMap[SDLK_KP_DIVIDE] = MyGUI::KeyCode::Divide; - keyMap[SDLK_SYSREQ] = MyGUI::KeyCode::SysRq; - keyMap[SDLK_RALT] = MyGUI::KeyCode::RightAlt; - keyMap[SDLK_HOME] = MyGUI::KeyCode::Home; - keyMap[SDLK_UP] = MyGUI::KeyCode::ArrowUp; - keyMap[SDLK_PAGEUP] = MyGUI::KeyCode::PageUp; - keyMap[SDLK_LEFT] = MyGUI::KeyCode::ArrowLeft; - keyMap[SDLK_RIGHT] = MyGUI::KeyCode::ArrowRight; - keyMap[SDLK_END] = MyGUI::KeyCode::End; - keyMap[SDLK_DOWN] = MyGUI::KeyCode::ArrowDown; - keyMap[SDLK_PAGEDOWN] = MyGUI::KeyCode::PageDown; - keyMap[SDLK_INSERT] = MyGUI::KeyCode::Insert; - keyMap[SDLK_DELETE] = MyGUI::KeyCode::Delete; - keyMap[SDLK_APPLICATION] = MyGUI::KeyCode::AppMenu; - -//The function of the Ctrl and Meta keys are switched on macOS compared to other platforms. -//For instance] = Cmd+C versus Ctrl+C to copy from the system clipboard -#if defined(__APPLE__) - keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftControl; - keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightControl; - keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftWindows; - keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightWindows; -#else - keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftWindows; - keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightWindows; - keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftControl; - keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightControl; -#endif - } - - MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code) - { - static std::map keyMap; - if (keyMap.empty()) - initKeyMap(keyMap); - - MyGUI::KeyCode kc = MyGUI::KeyCode::None; - auto foundKey = keyMap.find(code); - if (foundKey != keyMap.end()) - kc = foundKey->second; - - return kc; - } -} diff --git a/apps/openmw/mwinput/sdlmappings.hpp b/apps/openmw/mwinput/sdlmappings.hpp deleted file mode 100644 index 0cdd4694f53..00000000000 --- a/apps/openmw/mwinput/sdlmappings.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef MWINPUT_SDLMAPPINGS_H -#define MWINPUT_SDLMAPPINGS_H - -#include - -#include - -#include - -namespace MyGUI -{ - struct MouseButton; -} - -namespace MWInput -{ - std::string sdlControllerButtonToString(int button); - - std::string sdlControllerAxisToString(int axis); - - MyGUI::MouseButton sdlButtonToMyGUI(Uint8 button); - - MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code); -} -#endif diff --git a/apps/openmw/mwinput/sensormanager.cpp b/apps/openmw/mwinput/sensormanager.cpp index 3e8e70aefee..298006030a8 100644 --- a/apps/openmw/mwinput/sensormanager.cpp +++ b/apps/openmw/mwinput/sensormanager.cpp @@ -1,28 +1,15 @@ #include "sensormanager.hpp" #include - -#include "../mwbase/environment.hpp" -#include "../mwbase/inputmanager.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/player.hpp" +#include namespace MWInput { SensorManager::SensorManager() - : mInvertX(Settings::Manager::getBool("invert x axis", "Input")) - , mInvertY(Settings::Manager::getBool("invert y axis", "Input")) - , mGyroXSpeed(0.f) - , mGyroYSpeed(0.f) + : mRotation() + , mGyroValues() , mGyroUpdateTimer(0.f) - , mGyroHSensitivity(Settings::Manager::getFloat("gyro horizontal sensitivity", "Input")) - , mGyroVSensitivity(Settings::Manager::getFloat("gyro vertical sensitivity", "Input")) - , mGyroHAxis(GyroscopeAxis::Minus_X) - , mGyroVAxis(GyroscopeAxis::Y) - , mGyroInputThreshold(Settings::Manager::getFloat("gyro input threshold", "Input")) , mGyroscope(nullptr) - , mGuiCursorEnabled(true) { init(); } @@ -42,71 +29,49 @@ namespace MWInput } } - SensorManager::GyroscopeAxis SensorManager::mapGyroscopeAxis(const std::string& axis) - { - if (axis == "x") - return GyroscopeAxis::X; - else if (axis == "y") - return GyroscopeAxis::Y; - else if (axis == "z") - return GyroscopeAxis::Z; - else if (axis == "-x") - return GyroscopeAxis::Minus_X; - else if (axis == "-y") - return GyroscopeAxis::Minus_Y; - else if (axis == "-z") - return GyroscopeAxis::Minus_Z; - - return GyroscopeAxis::Unknown; - } - void SensorManager::correctGyroscopeAxes() { - if (!Settings::Manager::getBool("enable gyroscope", "Input")) + if (!Settings::input().mEnableGyroscope) return; // Treat setting from config as axes for landscape mode. // If the device does not support orientation change, do nothing. // Note: in is unclear how to correct axes for devices with non-standart Z axis direction. - mGyroHAxis = mapGyroscopeAxis(Settings::Manager::getString("gyro horizontal axis", "Input")); - mGyroVAxis = mapGyroscopeAxis(Settings::Manager::getString("gyro vertical axis", "Input")); - SDL_DisplayOrientation currentOrientation = SDL_GetDisplayOrientation(Settings::Manager::getInt("screen", "Video")); + mRotation = osg::Matrixf::identity(); + + float angle = 0; + + SDL_DisplayOrientation currentOrientation = SDL_GetDisplayOrientation(Settings::video().mScreen); switch (currentOrientation) { case SDL_ORIENTATION_UNKNOWN: - return; + break; case SDL_ORIENTATION_LANDSCAPE: break; case SDL_ORIENTATION_LANDSCAPE_FLIPPED: { - mGyroHAxis = GyroscopeAxis(-mGyroHAxis); - mGyroVAxis = GyroscopeAxis(-mGyroVAxis); - + angle = osg::PIf; break; } case SDL_ORIENTATION_PORTRAIT: { - GyroscopeAxis oldVAxis = mGyroVAxis; - mGyroVAxis = mGyroHAxis; - mGyroHAxis = GyroscopeAxis(-oldVAxis); - + angle = -0.5 * osg::PIf; break; } case SDL_ORIENTATION_PORTRAIT_FLIPPED: { - GyroscopeAxis oldVAxis = mGyroVAxis; - mGyroVAxis = GyroscopeAxis(-mGyroHAxis); - mGyroHAxis = oldVAxis; - + angle = 0.5 * osg::PIf; break; } } + + mRotation.makeRotate(angle, osg::Vec3f(0, 0, 1)); } void SensorManager::updateSensors() { - if (Settings::Manager::getBool("enable gyroscope", "Input")) + if (Settings::input().mEnableGyroscope) { int numSensors = SDL_NumSensors(); for (int i = 0; i < numSensors; ++i) @@ -114,19 +79,20 @@ namespace MWInput if (SDL_SensorGetDeviceType(i) == SDL_SENSOR_GYRO) { // It is unclear how to handle several enabled gyroscopes, so use the first one. - // Note: Android registers some gyroscope as two separate sensors, for non-wake-up mode and for wake-up mode. + // Note: Android registers some gyroscope as two separate sensors, for non-wake-up mode and for + // wake-up mode. if (mGyroscope != nullptr) { SDL_SensorClose(mGyroscope); mGyroscope = nullptr; - mGyroXSpeed = mGyroYSpeed = 0.f; mGyroUpdateTimer = 0.f; } // FIXME: SDL2 does not provide a way to configure a sensor update frequency so far. - SDL_Sensor *sensor = SDL_SensorOpen(i); + SDL_Sensor* sensor = SDL_SensorOpen(i); if (sensor == nullptr) - Log(Debug::Error) << "Couldn't open sensor " << SDL_SensorGetDeviceName(i) << ": " << SDL_GetError(); + Log(Debug::Error) + << "Couldn't open sensor " << SDL_SensorGetDeviceName(i) << ": " << SDL_GetError(); else { mGyroscope = sensor; @@ -141,7 +107,6 @@ namespace MWInput { SDL_SensorClose(mGyroscope); mGyroscope = nullptr; - mGyroXSpeed = mGyroYSpeed = 0.f; mGyroUpdateTimer = 0.f; } } @@ -151,46 +116,8 @@ namespace MWInput { for (const auto& setting : changed) { - if (setting.first == "Input" && setting.second == "invert x axis") - mInvertX = Settings::Manager::getBool("invert x axis", "Input"); - - if (setting.first == "Input" && setting.second == "invert y axis") - mInvertY = Settings::Manager::getBool("invert y axis", "Input"); - - if (setting.first == "Input" && setting.second == "gyro horizontal sensitivity") - mGyroHSensitivity = Settings::Manager::getFloat("gyro horizontal sensitivity", "Input"); - - if (setting.first == "Input" && setting.second == "gyro vertical sensitivity") - mGyroVSensitivity = Settings::Manager::getFloat("gyro vertical sensitivity", "Input"); - if (setting.first == "Input" && setting.second == "enable gyroscope") init(); - - if (setting.first == "Input" && setting.second == "gyro horizontal axis") - correctGyroscopeAxes(); - - if (setting.first == "Input" && setting.second == "gyro vertical axis") - correctGyroscopeAxes(); - - if (setting.first == "Input" && setting.second == "gyro input threshold") - mGyroInputThreshold = Settings::Manager::getFloat("gyro input threshold", "Input"); - } - } - - float SensorManager::getGyroAxisSpeed(GyroscopeAxis axis, const SDL_SensorEvent &arg) const - { - switch (axis) - { - case GyroscopeAxis::X: - case GyroscopeAxis::Y: - case GyroscopeAxis::Z: - return std::abs(arg.data[0]) >= mGyroInputThreshold ? arg.data[axis-1] : 0.f; - case GyroscopeAxis::Minus_X: - case GyroscopeAxis::Minus_Y: - case GyroscopeAxis::Minus_Z: - return std::abs(arg.data[0]) >= mGyroInputThreshold ? -arg.data[std::abs(axis)-1] : 0.f; - default: - return 0.f; } } @@ -199,12 +126,12 @@ namespace MWInput correctGyroscopeAxes(); } - void SensorManager::sensorUpdated(const SDL_SensorEvent &arg) + void SensorManager::sensorUpdated(const SDL_SensorEvent& arg) { - if (!Settings::Manager::getBool("enable gyroscope", "Input")) + if (!Settings::input().mEnableGyroscope) return; - SDL_Sensor *sensor = SDL_SensorFromInstanceID(arg.which); + SDL_Sensor* sensor = SDL_SensorFromInstanceID(arg.which); if (!sensor) { Log(Debug::Info) << "Couldn't get sensor for sensor event"; @@ -217,54 +144,36 @@ namespace MWInput break; case SDL_SENSOR_GYRO: { - mGyroXSpeed = getGyroAxisSpeed(mGyroHAxis, arg); - mGyroYSpeed = getGyroAxisSpeed(mGyroVAxis, arg); + osg::Vec3f gyro(arg.data[0], arg.data[1], arg.data[2]); + mGyroValues = mRotation * gyro; mGyroUpdateTimer = 0.f; - break; - } - default: - break; + } + default: + break; } } void SensorManager::update(float dt) { - if (mGyroXSpeed == 0.f && mGyroYSpeed == 0.f) - return; - + mGyroUpdateTimer += dt; if (mGyroUpdateTimer > 0.5f) { // More than half of second passed since the last gyroscope update. // A device more likely was disconnected or switched to the sleep mode. // Reset current rotation speed and wait for update. - mGyroXSpeed = 0.f; - mGyroYSpeed = 0.f; + mGyroValues = osg::Vec3f(); mGyroUpdateTimer = 0.f; - return; } + } - mGyroUpdateTimer += dt; - - if (!mGuiCursorEnabled) - { - float rot[3]; - rot[0] = -mGyroYSpeed * dt * mGyroVSensitivity * 4 * (mInvertY ? -1 : 1); - rot[1] = 0.0f; - rot[2] = -mGyroXSpeed * dt * mGyroHSensitivity * 4 * (mInvertX ? -1 : 1); - - // Only actually turn player when we're not in vanity mode - bool playerLooking = MWBase::Environment::get().getInputManager()->getControlSwitch("playerlooking"); - if (!MWBase::Environment::get().getWorld()->vanityRotateCamera(rot) && playerLooking) - { - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - player.yaw(-rot[2]); - player.pitch(-rot[0]); - } - else if (!playerLooking) - MWBase::Environment::get().getWorld()->disableDeferredPreviewRotation(); + bool SensorManager::isGyroAvailable() const + { + return mGyroscope != nullptr; + } - MWBase::Environment::get().getInputManager()->resetIdleTime(); - } + std::array SensorManager::getGyroValues() const + { + return { mGyroValues.x(), mGyroValues.y(), mGyroValues.z() }; } } diff --git a/apps/openmw/mwinput/sensormanager.hpp b/apps/openmw/mwinput/sensormanager.hpp index 75472d43b47..62a1a62d214 100644 --- a/apps/openmw/mwinput/sensormanager.hpp +++ b/apps/openmw/mwinput/sensormanager.hpp @@ -1,10 +1,15 @@ #ifndef MWINPUT_MWSENSORMANAGER_H #define MWINPUT_MWSENSORMANAGER_H +#include + #include -#include +#include +#include + #include +#include namespace SDLUtil { @@ -29,45 +34,22 @@ namespace MWInput void update(float dt); - void sensorUpdated(const SDL_SensorEvent &arg) override; + void sensorUpdated(const SDL_SensorEvent& arg) override; void displayOrientationChanged() override; void processChangedSettings(const Settings::CategorySettingVector& changed); - void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } + bool isGyroAvailable() const; + std::array getGyroValues() const; private: - enum GyroscopeAxis - { - Unknown = 0, - X = 1, - Y = 2, - Z = 3, - Minus_X = -1, - Minus_Y = -2, - Minus_Z = -3 - }; - void updateSensors(); void correctGyroscopeAxes(); - GyroscopeAxis mapGyroscopeAxis(const std::string& axis); - float getGyroAxisSpeed(GyroscopeAxis axis, const SDL_SensorEvent &arg) const; - - bool mInvertX; - bool mInvertY; - float mGyroXSpeed; - float mGyroYSpeed; + osg::Matrixf mRotation; + osg::Vec3f mGyroValues; float mGyroUpdateTimer; - float mGyroHSensitivity; - float mGyroVSensitivity; - GyroscopeAxis mGyroHAxis; - GyroscopeAxis mGyroVAxis; - float mGyroInputThreshold; - SDL_Sensor* mGyroscope; - - bool mGuiCursorEnabled; }; } #endif diff --git a/apps/openmw/mwlua/README.md b/apps/openmw/mwlua/README.md new file mode 100644 index 00000000000..ed911eb01d1 --- /dev/null +++ b/apps/openmw/mwlua/README.md @@ -0,0 +1,102 @@ +# MWLua + +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). +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 +root of the [docs folder](/docs/README.md). + +The Lua API reference is generated from the specifications in +[/files/lua_api](/files/lua_api/). They are written in the +Lua Development Tool [Documentation Language](https://wiki.eclipse.org/LDT/User_Area/Documentation_Language), +and also enable autocompletion for ([LDT](https://www.eclipse.org/ldt/)) users. +Please update them to reflect any changes you make. + +## MWLua::LuaManager + +The Lua manager is the central interface through which information flows +from the engine to the scripts and back. + +Lua is executed in a separate [thread](/apps/openmw/mwlua/worker.hpp) by +[default](https://openmw.readthedocs.io/en/latest/reference/modding/settings/lua.html#lua-num-threads). +This thread executes `update()` in parallel with rendering logic (specifically with OSG Cull traversal). +Because of this, Lua must not synchronously mutate anything that can directly or indirectly affect the scene graph. +Instead such changes are queued to `mActionQueue`. They are then processed by +`synchronizedUpdate()`, which is executed by the main thread. +The Lua thread is paused while other updates of the game state take place, +which means that state that doesn't affect the scene graph +can be mutated immediately. There is no easy way to characterize +which things affect the graph, you'll need to inspect the code. + +## Bindings + +The bulk of the code in this folder consists of bindings that expose C++ data to Lua. + +As explained in the [scripting overview](https://openmw.readthedocs.io/en/latest/reference/lua-scripting/overview.html), +there are Global and Local scripts, and they have different capabilities. +A Local script has read-only access to objects other the one it is attached to. +The bindings use the types `MWLua::GObject`, `MWLua::LObject`, `MWLua::SelfObject` to enforce this behaviour. + +* `MWLua::GObject` is used in global scripts +* `MWLua::LObject` is used in local scripts (readonly), +* `MWLua::SelfObject` is the object the local script is attached to. +* `MWLua::Object` is the common base of all 3. + +Functions that don't change objects are usually available in both local and global scripts so they accept `MWLua::Object`. +Some (for example `setEquipment` in [actor.cpp](https://gitlab.com/OpenMW/openmw/-/blob/master/apps/openmw/mwlua/types/actor.cpp)) +should work only on self and because of this have argument of type `SelfObject`. +There are also cases where a function is available in both local and global scripts, but has different effects in different cases. +For example see the binding `actor["inventory"]` in 'MWLua::addActorBindings` in [actor.cpp](https://gitlab.com/OpenMW/openmw/-/blob/master/apps/openmw/mwlua/types/actor.cpp): + +```cpp +actor["inventory"] = sol::overload([](const LObject& o) { return Inventory{ o }; }, + [](const GObject& o) { return Inventory{ o }; }); +``` + +The difference is that `Inventory` is readonly and `Inventory` is mutable. +The read-only bindings are defined for both, but some functions are exclusive for `Inventory`. + +### Mutations that affect the scene graph + +Because of the threading issues mentioned under `MWLua::LuaManager`, +bindings that mutate things that affect the scene graph +must be implemented by queuing an action with `LuaManager::addAction`. + +Here is an example that illustrates action queuing, +along with the differences between `GObject` and `LObject`: + +```cpp +// We can always read the value because OSG Cull doesn't modify `RefData`. +auto isEnabled = [](const Object& o) { return o.ptr().getRefData().isEnabled(); }; + +// Changing the value must be queued because `World::enable`/`World::disable` aside of +// changing `RefData` also adds/removes the object to the scene graph. +auto setEnabled = [context](const Object& object, bool enable) { + // It is important that the lambda state stores `object` and not the result of + // `object.ptr()` because when delayed will be executed the old Ptr can potentially + // be already invalidated. + context.mLuaManager->addAction([object, enable] { + if (enable) + MWBase::Environment::get().getWorld()->enable(object.ptr()); + else + MWBase::Environment::get().getWorld()->disable(object.ptr()); + }); +}; + +// Local scripts can only view the value (because in multiplayer local scripts +// will be client-side and we want to avoid synchronization issues). +LObjectMetatable["enabled"] = sol::readonly_property(isEnabled); + +// Global scripts can both read and modify the value. +GObjectMetatable["enabled"] = sol::property(isEnabled, setEnabled); +``` + +Please note that queueing means changes scripts make won't be visible to other scripts before the +next frame. If you want to avoid that, you can implement a cache in the bindings. +The first write will create the cache and queue the value to be synchronized from the +cache to the engine in the next synchronization. Later writes will update the cache. +Reads will read the cache if it exists. See [LocalScripts::SelfObject::mStatsCache](/apps/openmw/mwlua/localscripts.hpp) +for an example. diff --git a/apps/openmw/mwlua/animationbindings.cpp b/apps/openmw/mwlua/animationbindings.cpp new file mode 100644 index 00000000000..95294f46cb3 --- /dev/null +++ b/apps/openmw/mwlua/animationbindings.cpp @@ -0,0 +1,336 @@ +#include "animationbindings.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/character.hpp" + +#include "../mwworld/esmstore.hpp" + +#include "context.hpp" +#include "luamanagerimp.hpp" +#include "objectvariant.hpp" + +namespace MWLua +{ + using BlendMask = MWRender::Animation::BlendMask; + using BoneGroup = MWRender::Animation::BoneGroup; + using Priority = MWMechanics::Priority; + using AnimationPriorities = MWRender::Animation::AnimPriority; + + MWWorld::Ptr getMutablePtrOrThrow(const ObjectVariant& variant) + { + if (variant.isLObject()) + throw std::runtime_error("Local scripts can only modify animations of the object they are attached to."); + + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + if (!ptr.getRefData().isEnabled()) + throw std::runtime_error("Can't use a disabled object"); + + return ptr; + } + + MWWorld::Ptr getPtrOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + + return ptr; + } + + MWRender::Animation* getMutableAnimationOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = getMutablePtrOrThrow(variant); + auto world = MWBase::Environment::get().getWorld(); + MWRender::Animation* anim = world->getAnimation(ptr); + if (!anim) + throw std::runtime_error("Object has no animation"); + return anim; + } + + const MWRender::Animation* getConstAnimationOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = getPtrOrThrow(variant); + auto world = MWBase::Environment::get().getWorld(); + const MWRender::Animation* anim = world->getAnimation(ptr); + if (!anim) + throw std::runtime_error("Object has no animation"); + return anim; + } + + static AnimationPriorities getPriorityArgument(const sol::table& args) + { + auto asPriorityEnum = args.get>("priority"); + if (asPriorityEnum) + return asPriorityEnum.value(); + + auto asTable = args.get>("priority"); + if (asTable) + { + AnimationPriorities priorities = AnimationPriorities(Priority::Priority_Default); + for (const auto& entry : asTable.value()) + { + if (!entry.first.is() || !entry.second.is()) + throw std::runtime_error("Priority table must consist of BoneGroup-Priority pairs only"); + auto group = entry.first.as(); + auto priority = entry.second.as(); + if (group < 0 || group >= BoneGroup::Num_BoneGroups) + throw std::runtime_error("Invalid bonegroup: " + std::to_string(group)); + priorities[group] = priority; + } + + return priorities; + } + + return Priority::Priority_Default; + } + + sol::table initAnimationPackage(const Context& context) + { + auto view = context.sol(); + auto mechanics = MWBase::Environment::get().getMechanicsManager(); + auto world = MWBase::Environment::get().getWorld(); + + sol::table api(view, sol::create); + + api["PRIORITY"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(view, + { + { "Default", MWMechanics::Priority::Priority_Default }, + { "WeaponLowerBody", MWMechanics::Priority::Priority_WeaponLowerBody }, + { "SneakIdleLowerBody", MWMechanics::Priority::Priority_SneakIdleLowerBody }, + { "SwimIdle", MWMechanics::Priority::Priority_SwimIdle }, + { "Jump", MWMechanics::Priority::Priority_Jump }, + { "Movement", MWMechanics::Priority::Priority_Movement }, + { "Hit", MWMechanics::Priority::Priority_Hit }, + { "Weapon", MWMechanics::Priority::Priority_Weapon }, + { "Block", MWMechanics::Priority::Priority_Block }, + { "Knockdown", MWMechanics::Priority::Priority_Knockdown }, + { "Torch", MWMechanics::Priority::Priority_Torch }, + { "Storm", MWMechanics::Priority::Priority_Storm }, + { "Death", MWMechanics::Priority::Priority_Death }, + { "Scripted", MWMechanics::Priority::Priority_Scripted }, + })); + + api["BLEND_MASK"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(view, + { + { "LowerBody", BlendMask::BlendMask_LowerBody }, + { "Torso", BlendMask::BlendMask_Torso }, + { "LeftArm", BlendMask::BlendMask_LeftArm }, + { "RightArm", BlendMask::BlendMask_RightArm }, + { "UpperBody", BlendMask::BlendMask_UpperBody }, + { "All", BlendMask::BlendMask_All }, + })); + + api["BONE_GROUP"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(view, + { + { "LowerBody", BoneGroup::BoneGroup_LowerBody }, + { "Torso", BoneGroup::BoneGroup_Torso }, + { "LeftArm", BoneGroup::BoneGroup_LeftArm }, + { "RightArm", BoneGroup::BoneGroup_RightArm }, + })); + + api["hasAnimation"] = [world](const sol::object& object) -> bool { + return world->getAnimation(getPtrOrThrow(ObjectVariant(object))) != nullptr; + }; + + // equivalent to MWScript's SkipAnim + api["skipAnimationThisFrame"] = [mechanics](const sol::object& object) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + // This sets a flag that is only used during the update pass, so + // there's no need to queue + mechanics->skipAnimation(ptr); + }; + + api["getTextKeyTime"] = [](const sol::object& object, std::string_view key) -> sol::optional { + float time = getConstAnimationOrThrow(ObjectVariant(object))->getTextKeyTime(key); + if (time >= 0.f) + return time; + return sol::nullopt; + }; + api["isPlaying"] = [](const sol::object& object, std::string_view groupname) { + return getConstAnimationOrThrow(ObjectVariant(object))->isPlaying(groupname); + }; + api["getCurrentTime"] = [](const sol::object& object, std::string_view groupname) -> sol::optional { + float time = getConstAnimationOrThrow(ObjectVariant(object))->getCurrentTime(groupname); + if (time >= 0.f) + return time; + return sol::nullopt; + }; + api["isLoopingAnimation"] = [](const sol::object& object, std::string_view groupname) { + return getConstAnimationOrThrow(ObjectVariant(object))->isLoopingAnimation(groupname); + }; + api["cancel"] = [](const sol::object& object, std::string_view groupname) { + return getMutableAnimationOrThrow(ObjectVariant(object))->disable(groupname); + }; + api["setLoopingEnabled"] = [](const sol::object& object, std::string_view groupname, bool enabled) { + return getMutableAnimationOrThrow(ObjectVariant(object))->setLoopingEnabled(groupname, enabled); + }; + // MWRender::Animation::getInfo can also return the current speed multiplier, but this is never used. + api["getCompletion"] = [](const sol::object& object, std::string_view groupname) -> sol::optional { + float completion = 0.f; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, &completion)) + return completion; + return sol::nullopt; + }; + api["getLoopCount"] = [](const sol::object& object, std::string groupname) -> sol::optional { + size_t loops = 0; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, nullptr, nullptr, &loops)) + return loops; + return sol::nullopt; + }; + api["getSpeed"] = [](const sol::object& object, std::string groupname) -> sol::optional { + float speed = 0.f; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, nullptr, &speed, nullptr)) + return speed; + return sol::nullopt; + }; + api["setSpeed"] = [](const sol::object& object, std::string groupname, float speed) { + getMutableAnimationOrThrow(ObjectVariant(object))->adjustSpeedMult(groupname, speed); + }; + api["getActiveGroup"] = [](const sol::object& object, MWRender::BoneGroup boneGroup) -> std::string_view { + if (boneGroup < 0 || boneGroup >= BoneGroup::Num_BoneGroups) + throw std::runtime_error("Invalid bonegroup: " + std::to_string(boneGroup)); + return getConstAnimationOrThrow(ObjectVariant(object))->getActiveGroup(boneGroup); + }; + + // Clears out the animation queue, and cancel any animation currently playing from the queue + api["clearAnimationQueue"] = [mechanics](const sol::object& object, bool clearScripted) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->clearAnimationQueue(ptr, clearScripted); + }; + + // Extended variant of MWScript's PlayGroup and LoopGroup + api["playQueued"] = sol::overload( + [mechanics](const sol::object& object, const std::string& groupname, const sol::table& options) { + uint32_t numberOfLoops = options.get_or("loops", std::numeric_limits::max()); + float speed = options.get_or("speed", 1.f); + std::string startKey = options.get_or("startKey", "start"); + std::string stopKey = options.get_or("stopKey", "stop"); + bool forceLoop = options.get_or("forceLoop", false); + + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->playAnimationGroupLua(ptr, groupname, numberOfLoops, speed, startKey, stopKey, forceLoop); + }, + [mechanics](const sol::object& object, const std::string& groupname) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->playAnimationGroupLua( + ptr, groupname, std::numeric_limits::max(), 1, "start", "stop", false); + }); + + api["playBlended"] = [](const sol::object& object, std::string_view groupName, const sol::table& options) { + uint32_t loops = options.get_or("loops", 0u); + MWRender::Animation::AnimPriority priority = getPriorityArgument(options); + BlendMask blendMask = options.get_or("blendMask", BlendMask::BlendMask_All); + bool autoDisable = options.get_or("autoDisable", true); + float speed = options.get_or("speed", 1.0f); + std::string start = options.get_or("startKey", "start"); + std::string stop = options.get_or("stopKey", "stop"); + float startPoint = options.get_or("startPoint", 0.0f); + bool forceLoop = options.get_or("forceLoop", false); + + const std::string lowerGroup = Misc::StringUtils::lowerCase(groupName); + + auto animation = getMutableAnimationOrThrow(ObjectVariant(object)); + animation->play(lowerGroup, priority, blendMask, autoDisable, speed, start, stop, startPoint, loops, + forceLoop || animation->isLoopingAnimation(lowerGroup)); + }; + + api["hasGroup"] = [](const sol::object& object, std::string_view groupname) -> bool { + const MWRender::Animation* anim = getConstAnimationOrThrow(ObjectVariant(object)); + return anim->hasAnimation(groupname); + }; + + // Note: This checks the nodemap, and does not read the scene graph itself, and so should be thread safe. + api["hasBone"] = [](const sol::object& object, std::string_view bonename) -> bool { + const MWRender::Animation* anim = getConstAnimationOrThrow(ObjectVariant(object)); + return anim->getNode(bonename) != nullptr; + }; + + api["addVfx"] = [context]( + const sol::object& object, std::string_view model, sol::optional options) { + if (options) + { + context.mLuaManager->addAction( + [object = ObjectVariant(object), model = std::string(model), + effectId = options->get_or("vfxId", ""), loop = options->get_or("loop", false), + boneName = options->get_or("boneName", ""), + particleTexture = options->get_or("particleTextureOverride", "")] { + MWRender::Animation* anim = getMutableAnimationOrThrow(ObjectVariant(object)); + + anim->addEffect(model, effectId, loop, boneName, particleTexture); + }, + "addVfxAction"); + } + else + { + context.mLuaManager->addAction( + [object = ObjectVariant(object), model = std::string(model)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->addEffect(model, ""); + }, + "addVfxAction"); + } + }; + + api["removeVfx"] = [context](const sol::object& object, std::string_view effectId) { + context.mLuaManager->addAction( + [object = ObjectVariant(object), effectId = std::string(effectId)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->removeEffect(effectId); + }, + "removeVfxAction"); + }; + + api["removeAllVfx"] = [context](const sol::object& object) { + context.mLuaManager->addAction( + [object = ObjectVariant(object)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->removeEffects(); + }, + "removeVfxAction"); + }; + + return LuaUtil::makeReadOnly(api); + } + + sol::table initWorldVfxBindings(const Context& context) + { + sol::table api(context.mLua->unsafeState(), sol::create); + auto world = MWBase::Environment::get().getWorld(); + + api["spawn"] + = [world, context](std::string_view model, const osg::Vec3f& worldPos, sol::optional options) { + if (options) + { + bool magicVfx = options->get_or("mwMagicVfx", true); + std::string texture = options->get_or("particleTextureOverride", ""); + float scale = options->get_or("scale", 1.f); + context.mLuaManager->addAction( + [world, model = std::string(model), texture = std::move(texture), worldPos, scale, + magicVfx]() { world->spawnEffect(model, texture, worldPos, scale, magicVfx); }, + "openmw.vfx.spawn"); + } + else + { + context.mLuaManager->addAction( + [world, model = std::string(model), worldPos]() { world->spawnEffect(model, "", worldPos); }, + "openmw.vfx.spawn"); + } + }; + + return api; + } +} diff --git a/apps/openmw/mwlua/animationbindings.hpp b/apps/openmw/mwlua/animationbindings.hpp new file mode 100644 index 00000000000..aa89649a353 --- /dev/null +++ b/apps/openmw/mwlua/animationbindings.hpp @@ -0,0 +1,14 @@ +#ifndef MWLUA_ANIMATIONBINDINGS_H +#define MWLUA_ANIMATIONBINDINGS_H + +#include + +namespace MWLua +{ + struct Context; + + sol::table initAnimationPackage(const Context& context); + sol::table initWorldVfxBindings(const Context& context); +} + +#endif // MWLUA_ANIMATIONBINDINGS_H diff --git a/apps/openmw/mwlua/birthsignbindings.cpp b/apps/openmw/mwlua/birthsignbindings.cpp new file mode 100644 index 00000000000..218d05b8040 --- /dev/null +++ b/apps/openmw/mwlua/birthsignbindings.cpp @@ -0,0 +1,47 @@ +#include "birthsignbindings.hpp" + +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" + +#include "idcollectionbindings.hpp" +#include "types/types.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + sol::table initBirthSignRecordBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table birthSigns(lua, sol::create); + addRecordFunctionBinding(birthSigns, context); + + auto signT = lua.new_usertype("ESM3_BirthSign"); + signT[sol::meta_function::to_string] = [](const ESM::BirthSign& rec) -> std::string { + return "ESM3_BirthSign[" + rec.mId.toDebugString() + "]"; + }; + signT["id"] = sol::readonly_property([](const ESM::BirthSign& rec) { return rec.mId.serializeText(); }); + signT["name"] = sol::readonly_property([](const ESM::BirthSign& rec) -> std::string_view { return rec.mName; }); + signT["description"] + = sol::readonly_property([](const ESM::BirthSign& rec) -> std::string_view { return rec.mDescription; }); + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + signT["texture"] = sol::readonly_property([vfs](const ESM::BirthSign& rec) -> std::string { + return Misc::ResourceHelpers::correctTexturePath(rec.mTexture, vfs); + }); + signT["spells"] = sol::readonly_property([lua](const ESM::BirthSign& rec) -> sol::table { + return createReadOnlyRefIdTable(lua, rec.mPowers.mList); + }); + + return LuaUtil::makeReadOnly(birthSigns); + } +} diff --git a/apps/openmw/mwlua/birthsignbindings.hpp b/apps/openmw/mwlua/birthsignbindings.hpp new file mode 100644 index 00000000000..bf41707d473 --- /dev/null +++ b/apps/openmw/mwlua/birthsignbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_BIRTHSIGNBINDINGS_H +#define MWLUA_BIRTHSIGNBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initBirthSignRecordBindings(const Context& context); +} + +#endif // MWLUA_BIRTHSIGNBINDINGS_H diff --git a/apps/openmw/mwlua/camerabindings.cpp b/apps/openmw/mwlua/camerabindings.cpp new file mode 100644 index 00000000000..d64110ea983 --- /dev/null +++ b/apps/openmw/mwlua/camerabindings.cpp @@ -0,0 +1,129 @@ +#include "camerabindings.hpp" + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwrender/camera.hpp" +#include "../mwrender/renderingmanager.hpp" + +namespace MWLua +{ + + using CameraMode = MWRender::Camera::Mode; + + sol::table initCameraPackage(sol::state_view lua) + { + MWRender::Camera* camera = MWBase::Environment::get().getWorld()->getCamera(); + MWRender::RenderingManager* renderingManager = MWBase::Environment::get().getWorld()->getRenderingManager(); + + sol::table api(lua, sol::create); + api["MODE"] = LuaUtil::makeStrictReadOnly( + lua.create_table_with("Static", CameraMode::Static, "FirstPerson", CameraMode::FirstPerson, "ThirdPerson", + CameraMode::ThirdPerson, "Vanity", CameraMode::Vanity, "Preview", CameraMode::Preview)); + + api["getMode"] = [camera]() -> int { return static_cast(camera->getMode()); }; + api["getQueuedMode"] = [camera]() -> sol::optional { + std::optional mode = camera->getQueuedMode(); + if (mode) + return static_cast(*mode); + else + return sol::nullopt; + }; + api["setMode"] = [camera](int mode, sol::optional force) { + camera->setMode(static_cast(mode), force ? *force : false); + }; + + api["allowCharacterDeferredRotation"] = [camera](bool v) { camera->allowCharacterDeferredRotation(v); }; + api["showCrosshair"] = [camera](bool v) { camera->showCrosshair(v); }; + + api["getTrackedPosition"] = [camera]() -> osg::Vec3f { return camera->getTrackedPosition(); }; + api["getPosition"] = [camera]() -> osg::Vec3f { return camera->getPosition(); }; + + // All angles are negated in order to make camera rotation consistent with objects rotation. + // TODO: Fix the inconsistency of rotation direction in camera.cpp. + api["getPitch"] = [camera]() { return -camera->getPitch(); }; + api["getYaw"] = [camera]() { return -camera->getYaw(); }; + api["getRoll"] = [camera]() { return -camera->getRoll(); }; + + api["setStaticPosition"] = [camera](const osg::Vec3f& pos) { camera->setStaticPosition(pos); }; + api["setPitch"] = [camera](float v) { + camera->setPitch(-v, true); + if (camera->getMode() == CameraMode::ThirdPerson) + camera->calculateDeferredRotation(); + }; + api["setYaw"] = [camera](float v) { + camera->setYaw(-v, true); + if (camera->getMode() == CameraMode::ThirdPerson) + camera->calculateDeferredRotation(); + }; + api["setRoll"] = [camera](float v) { camera->setRoll(-v); }; + api["setExtraPitch"] = [camera](float v) { camera->setExtraPitch(-v); }; + api["setExtraYaw"] = [camera](float v) { camera->setExtraYaw(-v); }; + api["setExtraRoll"] = [camera](float v) { camera->setExtraRoll(-v); }; + api["getExtraPitch"] = [camera]() { return -camera->getExtraPitch(); }; + api["getExtraYaw"] = [camera]() { return -camera->getExtraYaw(); }; + api["getExtraRoll"] = [camera]() { return -camera->getExtraRoll(); }; + + api["getThirdPersonDistance"] = [camera]() { return camera->getCameraDistance(); }; + api["setPreferredThirdPersonDistance"] = [camera](float v) { camera->setPreferredCameraDistance(v); }; + + api["getFirstPersonOffset"] = [camera]() { return camera->getFirstPersonOffset(); }; + api["setFirstPersonOffset"] = [camera](const osg::Vec3f& v) { camera->setFirstPersonOffset(v); }; + + api["getFocalPreferredOffset"] = [camera]() -> osg::Vec2f { return camera->getFocalPointTargetOffset(); }; + api["setFocalPreferredOffset"] = [camera](const osg::Vec2f& v) { camera->setFocalPointTargetOffset(v); }; + api["getFocalTransitionSpeed"] = [camera]() { return camera->getFocalPointTransitionSpeed(); }; + api["setFocalTransitionSpeed"] = [camera](float v) { camera->setFocalPointTransitionSpeed(v); }; + api["instantTransition"] = [camera]() { camera->instantTransition(); }; + + api["getCollisionType"] = [camera]() { return camera->getCollisionType(); }; + api["setCollisionType"] = [camera](int collisionType) { camera->setCollisionType(collisionType); }; + + api["getBaseFieldOfView"] = [] { return osg::DegreesToRadians(Settings::camera().mFieldOfView); }; + api["getFieldOfView"] + = [renderingManager]() { return osg::DegreesToRadians(renderingManager->getFieldOfView()); }; + api["setFieldOfView"] + = [renderingManager](float v) { renderingManager->setFieldOfView(osg::RadiansToDegrees(v)); }; + + api["getBaseViewDistance"] = [] { return Settings::camera().mViewingDistance.get(); }; + api["getViewDistance"] = [renderingManager]() { return renderingManager->getViewDistance(); }; + api["setViewDistance"] = [renderingManager](float d) { renderingManager->setViewDistance(d, true); }; + + api["getViewTransform"] = [camera]() { return LuaUtil::TransformM{ camera->getViewMatrix() }; }; + + api["viewportToWorldVector"] = [camera, renderingManager](osg::Vec2f pos) -> osg::Vec3f { + const double width = Settings::video().mResolutionX; + const double height = Settings::video().mResolutionY; + double aspect = (height == 0.0) ? 1.0 : width / height; + double fovTan = std::tan(osg::DegreesToRadians(renderingManager->getFieldOfView()) / 2); + osg::Matrixf invertedViewMatrix; + invertedViewMatrix.invert(camera->getViewMatrix()); + float x = (pos.x() * 2 - 1) * aspect * fovTan; + float y = (1 - pos.y() * 2) * fovTan; + return invertedViewMatrix.preMult(osg::Vec3f(x, y, -1)) - camera->getPosition(); + }; + + api["worldToViewportVector"] = [camera](osg::Vec3f pos) { + const double width = Settings::video().mResolutionX; + const double height = Settings::video().mResolutionY; + + osg::Matrix windowMatrix + = osg::Matrix::translate(1.0, 1.0, 1.0) * osg::Matrix::scale(0.5 * width, 0.5 * height, 0.5); + osg::Vec3f vpCoords = pos * (camera->getViewMatrix() * camera->getProjectionMatrix() * windowMatrix); + + // Move 0,0 to top left to match viewportToWorldVector + vpCoords.y() = height - vpCoords.y(); + + // Set the z component to be distance from camera, in world space units + vpCoords.z() = (pos - camera->getPosition()).length(); + + return vpCoords; + }; + + return LuaUtil::makeReadOnly(api); + } + +} diff --git a/apps/openmw/mwlua/camerabindings.hpp b/apps/openmw/mwlua/camerabindings.hpp new file mode 100644 index 00000000000..001ef321671 --- /dev/null +++ b/apps/openmw/mwlua/camerabindings.hpp @@ -0,0 +1,11 @@ +#ifndef MWLUA_CAMERABINDINGS_H +#define MWLUA_CAMERABINDINGS_H + +#include + +namespace MWLua +{ + sol::table initCameraPackage(sol::state_view lua); +} + +#endif // MWLUA_CAMERABINDINGS_H diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp new file mode 100644 index 00000000000..933dba3fda8 --- /dev/null +++ b/apps/openmw/mwlua/cellbindings.cpp @@ -0,0 +1,301 @@ +#include "cellbindings.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/worldmodel.hpp" + +#include "types/types.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + template + static void initCellBindings(const std::string& prefix, const Context& context) + { + auto view = context.sol(); + sol::usertype cellT = view.new_usertype(prefix + "Cell"); + + cellT[sol::meta_function::equal_to] = [](const CellT& a, const CellT& b) { return a.mStore == b.mStore; }; + cellT[sol::meta_function::to_string] = [](const CellT& c) { + auto cell = c.mStore->getCell(); + std::stringstream res; + if (cell->isExterior()) + res << "exterior(" << cell->getGridX() << ", " << cell->getGridY() << ", " + << cell->getWorldSpace().toDebugString() << ")"; + else + res << "interior(" << cell->getNameId() << ")"; + return res.str(); + }; + + cellT["name"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getNameId(); }); + cellT["id"] + = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getId().serializeText(); }); + cellT["region"] = sol::readonly_property( + [](const CellT& c) -> std::string { return c.mStore->getCell()->getRegion().serializeText(); }); + cellT["worldSpaceId"] = sol::readonly_property( + [](const CellT& c) -> std::string { return c.mStore->getCell()->getWorldSpace().serializeText(); }); + cellT["gridX"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getGridX(); }); + cellT["gridY"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getGridY(); }); + cellT["hasWater"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->hasWater(); }); + cellT["hasSky"] = sol::readonly_property([](const CellT& c) { + return c.mStore->getCell()->isExterior() || (c.mStore->getCell()->isQuasiExterior()) != 0; + }); + cellT["isExterior"] = sol::readonly_property([](const CellT& c) { return c.mStore->isExterior(); }); + + // deprecated, use cell:hasTag("QuasiExterior") instead + cellT["isQuasiExterior"] + = sol::readonly_property([](const CellT& c) { return (c.mStore->getCell()->isQuasiExterior()) != 0; }); + + cellT["hasTag"] = [](const CellT& c, std::string_view tag) -> bool { + if (tag == "NoSleep") + return (c.mStore->getCell()->noSleep()) != 0; + else if (tag == "QuasiExterior") + return (c.mStore->getCell()->isQuasiExterior()) != 0; + return false; + }; + + cellT["isInSameSpace"] = [](const CellT& c, const ObjectT& obj) { + const MWWorld::Ptr& ptr = obj.ptr(); + if (!ptr.isInCell()) + return false; + MWWorld::CellStore* cell = ptr.getCell(); + return cell == c.mStore || (cell->getCell()->getWorldSpace() == c.mStore->getCell()->getWorldSpace()); + }; + + cellT["waterLevel"] = sol::readonly_property([](const CellT& c) -> sol::optional { + if (c.mStore->getCell()->hasWater()) + return c.mStore->getWaterLevel(); + else + return sol::nullopt; + }); + + if constexpr (std::is_same_v) + { // only for global scripts + cellT["getAll"] = [ids = getPackageToTypeTable(view)](const CellT& cell, sol::optional type) { + if (cell.mStore->getState() != MWWorld::CellStore::State_Loaded) + cell.mStore->load(); + ObjectIdList res = std::make_shared>(); + auto visitor = [&](const MWWorld::Ptr& ptr) { + if (ptr.mRef->isDeleted()) + return true; + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + if (getLiveCellRefType(ptr.mRef) == ptr.getType()) + res->push_back(getId(ptr)); + return true; + }; + + bool ok = true; + if (!type.has_value()) + cell.mStore->forEach(std::move(visitor)); + else if (ids[*type] == sol::nil) + ok = false; + else + { + uint32_t typeId = ids[*type]; + switch (typeId) + { + case ESM::REC_INTERNAL_PLAYER: + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (player.getCell() == cell.mStore) + res->push_back(getId(player)); + } + break; + + case ESM::REC_CREA: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_NPC_: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_ACTI: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_DOOR: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_CONT: + cell.mStore->template forEachType(visitor); + break; + + case ESM::REC_ALCH: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_ARMO: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_BOOK: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_CLOT: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_INGR: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_LIGH: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_MISC: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_WEAP: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_APPA: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_LOCK: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_PROB: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_REPA: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_STAT: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_LEVC: + cell.mStore->template forEachType(visitor); + break; + + case ESM::REC_ACTI4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_AMMO4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_ARMO4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_BOOK4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_CLOT4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_CONT4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_DOOR4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_FLOR4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_FURN4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_IMOD4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_INGR4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_LIGH4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_MISC4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_MSTT4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_ALCH4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_SCOL4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_STAT4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_TREE4: + cell.mStore->template forEachType(visitor); + break; + case ESM::REC_WEAP4: + cell.mStore->template forEachType(visitor); + break; + + default: + ok = false; + } + } + if (!ok) + throw std::runtime_error( + std::string("Incorrect type argument in cell:getAll: " + LuaUtil::toString(*type))); + return GObjectList{ std::move(res) }; + }; + } + } + + void initCellBindingsForLocalScripts(const Context& context) + { + initCellBindings("L", context); + } + + void initCellBindingsForGlobalScripts(const Context& context) + { + initCellBindings("G", context); + } + +} diff --git a/apps/openmw/mwlua/cellbindings.hpp b/apps/openmw/mwlua/cellbindings.hpp new file mode 100644 index 00000000000..0d8e90e989b --- /dev/null +++ b/apps/openmw/mwlua/cellbindings.hpp @@ -0,0 +1,12 @@ +#ifndef MWLUA_CELLBINDINGS_H +#define MWLUA_CELLBINDINGS_H + +#include "context.hpp" + +namespace MWLua +{ + void initCellBindingsForLocalScripts(const Context&); + void initCellBindingsForGlobalScripts(const Context&); +} + +#endif // MWLUA_CELLBINDINGS_H diff --git a/apps/openmw/mwlua/classbindings.cpp b/apps/openmw/mwlua/classbindings.cpp new file mode 100644 index 00000000000..c94631fb3c1 --- /dev/null +++ b/apps/openmw/mwlua/classbindings.cpp @@ -0,0 +1,53 @@ +#include "classbindings.hpp" + +#include +#include + +#include "idcollectionbindings.hpp" +#include "types/types.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + sol::table initClassRecordBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table classes(lua, sol::create); + addRecordFunctionBinding(classes, context); + + auto classT = lua.new_usertype("ESM3_Class"); + classT[sol::meta_function::to_string] + = [](const ESM::Class& rec) -> std::string { return "ESM3_Class[" + rec.mId.toDebugString() + "]"; }; + classT["id"] = sol::readonly_property([](const ESM::Class& rec) { return rec.mId.serializeText(); }); + classT["name"] = sol::readonly_property([](const ESM::Class& rec) -> std::string_view { return rec.mName; }); + classT["description"] + = sol::readonly_property([](const ESM::Class& rec) -> std::string_view { return rec.mDescription; }); + + classT["attributes"] = sol::readonly_property([lua](const ESM::Class& rec) -> sol::table { + return createReadOnlyRefIdTable(lua, rec.mData.mAttribute, ESM::Attribute::indexToRefId); + }); + classT["majorSkills"] = sol::readonly_property([lua](const ESM::Class& rec) -> sol::table { + return createReadOnlyRefIdTable( + lua, rec.mData.mSkills, [](const auto& pair) { return ESM::Skill::indexToRefId(pair[1]); }); + }); + classT["minorSkills"] = sol::readonly_property([lua](const ESM::Class& rec) -> sol::table { + return createReadOnlyRefIdTable( + lua, rec.mData.mSkills, [](const auto& pair) { return ESM::Skill::indexToRefId(pair[0]); }); + }); + + classT["specialization"] = sol::readonly_property([](const ESM::Class& rec) -> std::string_view { + return ESM::Class::specializationIndexToLuaId.at(rec.mData.mSpecialization); + }); + classT["isPlayable"] + = sol::readonly_property([](const ESM::Class& rec) -> bool { return rec.mData.mIsPlayable; }); + return LuaUtil::makeReadOnly(classes); + } +} diff --git a/apps/openmw/mwlua/classbindings.hpp b/apps/openmw/mwlua/classbindings.hpp new file mode 100644 index 00000000000..1acb0a9ad3c --- /dev/null +++ b/apps/openmw/mwlua/classbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_CLASSBINDINGS_H +#define MWLUA_CLASSBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initClassRecordBindings(const Context& context); +} + +#endif // MWLUA_CLASSBINDINGS_H diff --git a/apps/openmw/mwlua/context.hpp b/apps/openmw/mwlua/context.hpp new file mode 100644 index 00000000000..363507bdc90 --- /dev/null +++ b/apps/openmw/mwlua/context.hpp @@ -0,0 +1,108 @@ +#ifndef MWLUA_CONTEXT_H +#define MWLUA_CONTEXT_H + +#include + +namespace LuaUtil +{ + class LuaState; + class UserdataSerializer; +} + +namespace MWLua +{ + class LuaEvents; + class LuaManager; + class ObjectLists; + + struct Context + { + enum Type + { + Menu, + Global, + Local, + }; + Type mType; + LuaManager* mLuaManager; + LuaUtil::LuaState* mLua; + LuaUtil::UserdataSerializer* mSerializer; + ObjectLists* mObjectLists; + LuaEvents* mLuaEvents; + + std::string_view typeName() const + { + switch (mType) + { + case Menu: + return "menu"; + case Global: + return "global"; + case Local: + return "local"; + default: + throw std::domain_error("Unhandled context type"); + } + } + + template + sol::object getCachedPackage(std::string_view first, const Str&... str) const + { + sol::object package = sol()[first]; + if constexpr (sizeof...(str) == 0) + return package; + else + return LuaUtil::getFieldOrNil(package, str...); + } + + template + const sol::object& setCachedPackage(const sol::object& value, std::string_view first, const Str&... str) const + { + sol::state_view lua = sol(); + if constexpr (sizeof...(str) == 0) + lua[first] = value; + else + { + if (lua[first] == sol::nil) + lua[first] = sol::table(lua, sol::create); + sol::table table = lua[first]; + LuaUtil::setDeepField(table, value, str...); + } + return value; + } + + sol::object getTypePackage(std::string_view key) const { return getCachedPackage(key, typeName()); } + + const sol::object& setTypePackage(const sol::object& value, std::string_view key) const + { + return setCachedPackage(value, key, typeName()); + } + + template + sol::object cachePackage(std::string_view key, Factory factory) const + { + sol::object cached = getCachedPackage(key); + if (cached != sol::nil) + return cached; + else + return setCachedPackage(factory(), key); + } + + bool initializeOnce(std::string_view key) const + { + auto view = sol(); + sol::object flag = view[key]; + view[key] = sol::make_object(view, true); + return flag == sol::nil; + } + + sol::state_view sol() const + { + // Bindings are initialized in a safe context + return mLua->unsafeState(); + } + }; + +} + +#endif // MWLUA_CONTEXT_H diff --git a/apps/openmw/mwlua/corebindings.cpp b/apps/openmw/mwlua/corebindings.cpp new file mode 100644 index 00000000000..445bcdd6179 --- /dev/null +++ b/apps/openmw/mwlua/corebindings.cpp @@ -0,0 +1,154 @@ +#include "corebindings.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/datetimemanager.hpp" +#include "../mwworld/esmstore.hpp" + +#include "dialoguebindings.hpp" +#include "factionbindings.hpp" +#include "luaevents.hpp" +#include "magicbindings.hpp" +#include "soundbindings.hpp" +#include "stats.hpp" + +namespace MWLua +{ + static sol::table initContentFilesBindings(sol::state_view& lua) + { + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + sol::table list(lua, sol::create); + for (size_t i = 0; i < contentList.size(); ++i) + list[LuaUtil::toLuaIndex(i)] = Misc::StringUtils::lowerCase(contentList[i]); + sol::table res(lua, sol::create); + res["list"] = LuaUtil::makeReadOnly(list); + res["indexOf"] = [&contentList](std::string_view contentFile) -> sol::optional { + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return LuaUtil::toLuaIndex(i); + return sol::nullopt; + }; + res["has"] = [&contentList](std::string_view contentFile) -> bool { + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return true; + return false; + }; + return LuaUtil::makeReadOnly(res); + } + + void addCoreTimeBindings(sol::table& api, const Context& context) + { + MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager(); + + api["getSimulationTime"] = [timeManager]() { return timeManager->getSimulationTime(); }; + api["getSimulationTimeScale"] = [timeManager]() { return timeManager->getSimulationTimeScale(); }; + api["getGameTime"] = [timeManager]() { return timeManager->getGameTime(); }; + api["getGameTimeScale"] = [timeManager]() { return timeManager->getGameTimeScale(); }; + api["isWorldPaused"] = [timeManager]() { return timeManager->isPaused(); }; + api["getRealTime"] = []() { + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); + }; + + if (context.mType != Context::Global) + api["getRealFrameDuration"] = []() { return MWBase::Environment::get().getFrameDuration(); }; + } + + sol::table initCorePackage(const Context& context) + { + auto lua = context.sol(); + sol::object cached = context.getTypePackage("openmw_core"); + if (cached != sol::nil) + return cached; + + sol::table api(lua, sol::create); + api["API_REVISION"] = Version::getLuaApiRevision(); // specified in CMakeLists.txt + api["quit"] = [lua = context.mLua]() { + Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); + MWBase::Environment::get().getStateManager()->requestQuit(); + }; + api["contentFiles"] = initContentFilesBindings(lua); + api["getFormId"] = [](std::string_view contentFile, unsigned int index) -> std::string { + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return ESM::RefId(ESM::FormId{ index, int(i) }).serializeText(); + throw std::runtime_error("Content file not found: " + std::string(contentFile)); + }; + addCoreTimeBindings(api, context); + + api["magic"] + = context.cachePackage("openmw_core_magic", [context]() { return initCoreMagicBindings(context); }); + + api["stats"] + = context.cachePackage("openmw_core_stats", [context]() { return initCoreStatsBindings(context); }); + + api["factions"] + = context.cachePackage("openmw_core_factions", [context]() { return initCoreFactionBindings(context); }); + api["dialogue"] + = context.cachePackage("openmw_core_dialogue", [context]() { return initCoreDialogueBindings(context); }); + api["l10n"] = context.cachePackage("openmw_core_l10n", + [lua]() { return LuaUtil::initL10nLoader(lua, MWBase::Environment::get().getL10nManager()); }); + const MWWorld::Store* gmstStore + = &MWBase::Environment::get().getESMStore()->get(); + api["getGMST"] = [lua, gmstStore](const std::string& setting) -> sol::object { + const ESM::GameSetting* gmst = gmstStore->search(setting); + if (gmst == nullptr) + return sol::nil; + const ESM::Variant& value = gmst->mValue; + switch (value.getType()) + { + case ESM::VT_Float: + return sol::make_object(lua, value.getFloat()); + case ESM::VT_Short: + case ESM::VT_Long: + case ESM::VT_Int: + return sol::make_object(lua, value.getInteger()); + case ESM::VT_String: + return sol::make_object(lua, value.getString()); + case ESM::VT_Unknown: + case ESM::VT_None: + break; + } + return sol::nil; + }; + + if (context.mType != Context::Menu) + { + api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) { + context.mLuaEvents->addGlobalEvent( + { std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); + }; + api["sound"] + = context.cachePackage("openmw_core_sound", [context]() { return initCoreSoundBindings(context); }); + } + else + { + api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) { + if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) + { + throw std::logic_error("Can't send global events when no game is loaded"); + } + context.mLuaEvents->addGlobalEvent( + { std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); + }; + } + + sol::table readOnly = LuaUtil::makeReadOnly(api); + return context.setTypePackage(readOnly, "openmw_core"); + } +} diff --git a/apps/openmw/mwlua/corebindings.hpp b/apps/openmw/mwlua/corebindings.hpp new file mode 100644 index 00000000000..ef385ca9939 --- /dev/null +++ b/apps/openmw/mwlua/corebindings.hpp @@ -0,0 +1,15 @@ +#ifndef MWLUA_COREBINDINGS_H +#define MWLUA_COREBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + void addCoreTimeBindings(sol::table& api, const Context& context); + + sol::table initCorePackage(const Context&); +} + +#endif // MWLUA_COREBINDINGS_H diff --git a/apps/openmw/mwlua/debugbindings.cpp b/apps/openmw/mwlua/debugbindings.cpp new file mode 100644 index 00000000000..dcb77580ce0 --- /dev/null +++ b/apps/openmw/mwlua/debugbindings.cpp @@ -0,0 +1,99 @@ +#include "debugbindings.hpp" + +#include "context.hpp" +#include "luamanagerimp.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwrender/postprocessor.hpp" +#include "../mwrender/renderingmanager.hpp" + +#include +#include +#include + +#include + +namespace MWLua +{ + sol::table initDebugPackage(const Context& context) + { + auto view = context.sol(); + sol::table api(view, sol::create); + + api["RENDER_MODE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(view, + { + { "CollisionDebug", MWRender::Render_CollisionDebug }, + { "Wireframe", MWRender::Render_Wireframe }, + { "Pathgrid", MWRender::Render_Pathgrid }, + { "Water", MWRender::Render_Water }, + { "Scene", MWRender::Render_Scene }, + { "NavMesh", MWRender::Render_NavMesh }, + { "ActorsPaths", MWRender::Render_ActorsPaths }, + { "RecastMesh", MWRender::Render_RecastMesh }, + })); + + api["toggleRenderMode"] = [context](MWRender::RenderMode value) { + context.mLuaManager->addAction([value] { MWBase::Environment::get().getWorld()->toggleRenderMode(value); }); + }; + + api["toggleGodMode"] = []() { MWBase::Environment::get().getWorld()->toggleGodMode(); }; + api["isGodMode"] = []() { return MWBase::Environment::get().getWorld()->getGodModeState(); }; + + api["toggleAI"] = []() { MWBase::Environment::get().getMechanicsManager()->toggleAI(); }; + api["isAIEnabled"] = []() { return MWBase::Environment::get().getMechanicsManager()->isAIActive(); }; + + api["toggleCollision"] = []() { MWBase::Environment::get().getWorld()->toggleCollisionMode(); }; + api["isCollisionEnabled"] = []() { + auto world = MWBase::Environment::get().getWorld(); + return world->isActorCollisionEnabled(world->getPlayerPtr()); + }; + + api["toggleMWScript"] = []() { MWBase::Environment::get().getWorld()->toggleScripts(); }; + api["isMWScriptEnabled"] = []() { return MWBase::Environment::get().getWorld()->getScriptsEnabled(); }; + + api["reloadLua"] = []() { MWBase::Environment::get().getLuaManager()->reloadAllScripts(); }; + + api["NAV_MESH_RENDER_MODE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(view, + { + { "AreaType", Settings::NavMeshRenderMode::AreaType }, + { "UpdateFrequency", Settings::NavMeshRenderMode::UpdateFrequency }, + })); + + api["setNavMeshRenderMode"] = [context](Settings::NavMeshRenderMode value) { + context.mLuaManager->addAction( + [value] { MWBase::Environment::get().getWorld()->getRenderingManager()->setNavMeshMode(value); }); + }; + + api["triggerShaderReload"] = [context]() { + context.mLuaManager->addAction([] { + auto world = MWBase::Environment::get().getWorld(); + + world->getRenderingManager() + ->getResourceSystem() + ->getSceneManager() + ->getShaderManager() + .triggerShaderReload(); + world->getPostProcessor()->triggerShaderReload(); + }); + }; + + api["setShaderHotReloadEnabled"] = [context](bool value) { + context.mLuaManager->addAction([value] { + auto world = MWBase::Environment::get().getWorld(); + world->getRenderingManager() + ->getResourceSystem() + ->getSceneManager() + ->getShaderManager() + .setHotReloadEnabled(value); + world->getPostProcessor()->mEnableLiveReload = value; + }); + }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/debugbindings.hpp b/apps/openmw/mwlua/debugbindings.hpp new file mode 100644 index 00000000000..c508b54496a --- /dev/null +++ b/apps/openmw/mwlua/debugbindings.hpp @@ -0,0 +1,13 @@ +#ifndef OPENMW_MWLUA_DEBUGBINDINGS_H +#define OPENMW_MWLUA_DEBUGBINDINGS_H + +#include + +namespace MWLua +{ + struct Context; + + sol::table initDebugPackage(const Context& context); +} + +#endif // OPENMW_MWLUA_DEBUGBINDINGS_H diff --git a/apps/openmw/mwlua/dialoguebindings.cpp b/apps/openmw/mwlua/dialoguebindings.cpp new file mode 100644 index 00000000000..d0ca799cb64 --- /dev/null +++ b/apps/openmw/mwlua/dialoguebindings.cpp @@ -0,0 +1,343 @@ +#include "dialoguebindings.hpp" + +#include "context.hpp" + +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" +#include "apps/openmw/mwworld/store.hpp" + +#include +#include +#include +#include +#include + +namespace +{ + std::vector makeIndex(const MWWorld::Store& store, ESM::Dialogue::Type type) + { + std::vector result; + for (const ESM::Dialogue& v : store) + if (v.mType == type) + result.push_back(&v); + return result; + } + + template + class FilteredDialogueStore + { + const MWWorld::Store& mDialogueStore; + std::vector mIndex; + + public: + explicit FilteredDialogueStore(const MWWorld::Store& store) + : mDialogueStore(store) + , mIndex{ makeIndex(store, type) } + { + } + + const ESM::Dialogue* search(const ESM::RefId& id) const + { + const ESM::Dialogue* dialogue = mDialogueStore.search(id); + if (dialogue != nullptr && dialogue->mType == type) + return dialogue; + return nullptr; + } + + const ESM::Dialogue* at(std::size_t index) const + { + if (index >= mIndex.size()) + return nullptr; + return mIndex[index]; + } + + std::size_t getSize() const { return mIndex.size(); } + }; + + template + void prepareBindingsForDialogueRecordStores(sol::table& table, const MWLua::Context& context) + { + using StoreT = FilteredDialogueStore; + + sol::state_view lua = context.sol(); + sol::usertype storeBindingsClass + = lua.new_usertype("ESM3_Dialogue_Type" + std::to_string(filter) + " Store"); + storeBindingsClass[sol::meta_function::to_string] = [](const StoreT& store) { + return "{" + std::to_string(store.getSize()) + " ESM3_Dialogue_Type" + std::to_string(filter) + " records}"; + }; + storeBindingsClass[sol::meta_function::length] = [](const StoreT& store) { return store.getSize(); }; + storeBindingsClass[sol::meta_function::index] = sol::overload( + [](const StoreT& store, size_t index) -> const ESM::Dialogue* { + if (index == 0) + { + return nullptr; + } + return store.at(LuaUtil::fromLuaIndex(index)); + }, + [](const StoreT& store, std::string_view id) -> const ESM::Dialogue* { + return store.search(ESM::RefId::deserializeText(id)); + }); + storeBindingsClass[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + storeBindingsClass[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + + table["records"] = StoreT{ MWBase::Environment::get().getESMStore()->get() }; + } + + struct DialogueInfos + { + const ESM::Dialogue& parentDialogueRecord; + }; + + void prepareBindingsForDialogueRecord(sol::state_view& lua) + { + auto recordBindingsClass = lua.new_usertype("ESM3_Dialogue"); + recordBindingsClass[sol::meta_function::to_string] + = [](const ESM::Dialogue& rec) { return "ESM3_Dialogue[" + rec.mId.toDebugString() + "]"; }; + recordBindingsClass["id"] + = sol::readonly_property([](const ESM::Dialogue& rec) { return rec.mId.serializeText(); }); + recordBindingsClass["name"] + = sol::readonly_property([](const ESM::Dialogue& rec) -> std::string_view { return rec.mStringId; }); + recordBindingsClass["questName"] + = sol::readonly_property([](const ESM::Dialogue& rec) -> sol::optional { + if (rec.mType != ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + for (const auto& mwDialogueInfo : rec.mInfo) + { + if (mwDialogueInfo.mQuestStatus == ESM::DialInfo::QuestStatus::QS_Name) + { + return sol::optional(mwDialogueInfo.mResponse); + } + } + return sol::nullopt; + }); + recordBindingsClass["infos"] + = sol::readonly_property([](const ESM::Dialogue& rec) { return DialogueInfos{ rec }; }); + } + + void prepareBindingsForDialogueRecordInfoList(sol::state_view& lua) + { + auto recordInfosBindingsClass = lua.new_usertype("ESM3_Dialogue_Infos"); + recordInfosBindingsClass[sol::meta_function::to_string] = [](const DialogueInfos& store) { + const ESM::Dialogue& dialogueRecord = store.parentDialogueRecord; + return "{" + std::to_string(dialogueRecord.mInfo.size()) + " ESM3_Dialogue[" + + dialogueRecord.mId.toDebugString() + "] info elements}"; + }; + recordInfosBindingsClass[sol::meta_function::length] + = [](const DialogueInfos& store) { return store.parentDialogueRecord.mInfo.size(); }; + recordInfosBindingsClass[sol::meta_function::index] + = [](const DialogueInfos& store, size_t index) -> const ESM::DialInfo* { + const ESM::Dialogue& dialogueRecord = store.parentDialogueRecord; + if (index == 0 || index > dialogueRecord.mInfo.size()) + { + return nullptr; + } + ESM::Dialogue::InfoContainer::const_iterator iter{ dialogueRecord.mInfo.cbegin() }; + std::advance(iter, LuaUtil::fromLuaIndex(index)); + return &(*iter); + }; + recordInfosBindingsClass[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + recordInfosBindingsClass[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + } + + void prepareBindingsForDialogueRecordInfoListElement(sol::state_view& lua) + { + auto recordInfoBindingsClass = lua.new_usertype("ESM3_Dialogue_Info"); + + recordInfoBindingsClass[sol::meta_function::to_string] + = [](const ESM::DialInfo& rec) { return "ESM3_Dialogue_Info[" + rec.mId.toDebugString() + "]"; }; + recordInfoBindingsClass["id"] + = sol::readonly_property([](const ESM::DialInfo& rec) { return rec.mId.serializeText(); }); + recordInfoBindingsClass["text"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> std::string_view { return rec.mResponse; }); + recordInfoBindingsClass["questStage"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType != ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + return rec.mData.mJournalIndex; + }); + recordInfoBindingsClass["isQuestFinished"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType != ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + return (rec.mQuestStatus == ESM::DialInfo::QuestStatus::QS_Finished); + }); + recordInfoBindingsClass["isQuestRestart"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType != ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + return (rec.mQuestStatus == ESM::DialInfo::QuestStatus::QS_Restart); + }); + recordInfoBindingsClass["isQuestName"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType != ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + return (rec.mQuestStatus == ESM::DialInfo::QuestStatus::QS_Name); + }); + recordInfoBindingsClass["filterActorId"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mActor.empty()) + { + return sol::nullopt; + } + return rec.mActor.serializeText(); + }); + recordInfoBindingsClass["filterActorRace"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mRace.empty()) + { + return sol::nullopt; + } + return rec.mRace.serializeText(); + }); + recordInfoBindingsClass["filterActorClass"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mClass.empty()) + { + return sol::nullopt; + } + return rec.mClass.serializeText(); + }); + recordInfoBindingsClass["filterActorFaction"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mFaction.empty()) + { + return sol::nullopt; + } + if (rec.mFactionLess) + { + return sol::optional(""); + } + return rec.mFaction.serializeText(); + }); + recordInfoBindingsClass["filterActorFactionRank"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mData.mRank == -1) + { + return sol::nullopt; + } + return LuaUtil::toLuaIndex(rec.mData.mRank); + }); + recordInfoBindingsClass["filterPlayerCell"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mCell.empty()) + { + return sol::nullopt; + } + return rec.mCell.serializeText(); + }); + recordInfoBindingsClass["filterActorDisposition"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal) + { + return sol::nullopt; + } + return rec.mData.mDisposition; + }); + recordInfoBindingsClass["filterActorGender"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mData.mGender == -1) + { + return sol::nullopt; + } + return sol::optional(rec.mData.mGender == 0 ? "male" : "female"); + }); + recordInfoBindingsClass["filterPlayerFaction"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mPcFaction.empty()) + { + return sol::nullopt; + } + return rec.mPcFaction.serializeText(); + }); + recordInfoBindingsClass["filterPlayerFactionRank"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mData.mPCrank == -1) + { + return sol::nullopt; + } + return LuaUtil::toLuaIndex(rec.mData.mPCrank); + }); + recordInfoBindingsClass["sound"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mData.mType == ESM::Dialogue::Type::Journal || rec.mSound.empty()) + { + return sol::nullopt; + } + return Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(rec.mSound)).value(); + }); + recordInfoBindingsClass["resultScript"] + = sol::readonly_property([](const ESM::DialInfo& rec) -> sol::optional { + if (rec.mResultScript.empty()) + { + return sol::nullopt; + } + return sol::optional(rec.mResultScript); + }); + } + + void prepareBindingsForDialogueRecords(sol::state_view& lua) + { + prepareBindingsForDialogueRecord(lua); + prepareBindingsForDialogueRecordInfoList(lua); + prepareBindingsForDialogueRecordInfoListElement(lua); + } +} + +namespace sol +{ + template + struct is_automagical> : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + sol::table initCoreDialogueBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + + sol::table journalTable(lua, sol::create); + sol::table topicTable(lua, sol::create); + sol::table greetingTable(lua, sol::create); + sol::table persuasionTable(lua, sol::create); + sol::table voiceTable(lua, sol::create); + prepareBindingsForDialogueRecordStores(journalTable, context); + prepareBindingsForDialogueRecordStores(topicTable, context); + prepareBindingsForDialogueRecordStores(greetingTable, context); + prepareBindingsForDialogueRecordStores(persuasionTable, context); + prepareBindingsForDialogueRecordStores(voiceTable, context); + api["journal"] = LuaUtil::makeStrictReadOnly(journalTable); + api["topic"] = LuaUtil::makeStrictReadOnly(topicTable); + api["greeting"] = LuaUtil::makeStrictReadOnly(greetingTable); + api["persuasion"] = LuaUtil::makeStrictReadOnly(persuasionTable); + api["voice"] = LuaUtil::makeStrictReadOnly(voiceTable); + + prepareBindingsForDialogueRecords(lua); + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/dialoguebindings.hpp b/apps/openmw/mwlua/dialoguebindings.hpp new file mode 100644 index 00000000000..a4ee2424278 --- /dev/null +++ b/apps/openmw/mwlua/dialoguebindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_DIALOGUEBINDINGS_H +#define MWLUA_DIALOGUEBINDINGS_H + +#include + +namespace MWLua +{ + struct Context; + + sol::table initCoreDialogueBindings(const Context& context); +} + +#endif // MWLUA_DIALOGUEBINDINGS_H diff --git a/apps/openmw/mwlua/engineevents.cpp b/apps/openmw/mwlua/engineevents.cpp new file mode 100644 index 00000000000..6c652bccba9 --- /dev/null +++ b/apps/openmw/mwlua/engineevents.cpp @@ -0,0 +1,147 @@ +#include "engineevents.hpp" + +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/worldmodel.hpp" + +#include "globalscripts.hpp" +#include "localscripts.hpp" +#include "object.hpp" + +namespace MWLua +{ + + class EngineEvents::Visitor + { + public: + explicit Visitor(GlobalScripts& globalScripts) + : mGlobalScripts(globalScripts) + { + } + + void operator()(const OnActive& event) const + { + MWWorld::Ptr ptr = getPtr(event.mObject); + if (ptr.isEmpty()) + return; + if (ptr.getCellRef().getRefId() == "player") + mGlobalScripts.playerAdded(GObject(ptr)); + else + { + mGlobalScripts.objectActive(GObject(ptr)); + const MWWorld::Class& objClass = ptr.getClass(); + if (objClass.isActor()) + mGlobalScripts.actorActive(GObject(ptr)); + if (objClass.isItem(ptr)) + mGlobalScripts.itemActive(GObject(ptr)); + } + if (auto* scripts = getLocalScripts(ptr)) + scripts->setActive(true); + } + + void operator()(const OnInactive& event) const + { + if (auto* scripts = getLocalScripts(event.mObject)) + scripts->setActive(false); + } + + void operator()(const OnTeleported& event) const + { + if (auto* scripts = getLocalScripts(event.mObject)) + scripts->onTeleported(); + } + + void operator()(const OnActivate& event) const + { + MWWorld::Ptr obj = getPtr(event.mObject); + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty() || obj.isEmpty()) + return; + mGlobalScripts.onActivate(GObject(obj), GObject(actor)); + if (auto* scripts = getLocalScripts(obj)) + scripts->onActivated(LObject(actor)); + } + + void operator()(const OnUseItem& event) const + { + MWWorld::Ptr obj = getPtr(event.mObject); + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty() || obj.isEmpty()) + return; + mGlobalScripts.onUseItem(GObject(obj), GObject(actor), event.mForce); + } + + void operator()(const OnConsume& event) const + { + MWWorld::Ptr actor = getPtr(event.mActor); + MWWorld::Ptr consumable = getPtr(event.mConsumable); + if (actor.isEmpty() || consumable.isEmpty()) + return; + if (auto* scripts = getLocalScripts(actor)) + scripts->onConsume(LObject(consumable)); + } + + void operator()(const OnNewExterior& event) const { mGlobalScripts.onNewExterior(GCell{ &event.mCell }); } + + void operator()(const OnAnimationTextKey& event) const + { + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty()) + return; + if (auto* scripts = getLocalScripts(actor)) + scripts->onAnimationTextKey(event.mGroupname, event.mKey); + } + + void operator()(const OnSkillUse& event) const + { + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty()) + return; + if (auto* scripts = getLocalScripts(actor)) + scripts->onSkillUse(event.mSkill, event.useType, event.scale); + } + + void operator()(const OnSkillLevelUp& event) const + { + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty()) + return; + if (auto* scripts = getLocalScripts(actor)) + scripts->onSkillLevelUp(event.mSkill, event.mSource); + } + + private: + MWWorld::Ptr getPtr(ESM::RefNum id) const + { + MWWorld::Ptr res = mWorldModel->getPtr(id); + if (res.isEmpty() && Settings::lua().mLuaDebug) + Log(Debug::Verbose) << "Can not find object" << id.toString() << " when calling engine hanglers"; + return res; + } + + LocalScripts* getLocalScripts(const MWWorld::Ptr& ptr) const + { + if (ptr.isEmpty()) + return nullptr; + else + return ptr.getRefData().getLuaScripts(); + } + + LocalScripts* getLocalScripts(ESM::RefNum id) const { return getLocalScripts(getPtr(id)); } + + GlobalScripts& mGlobalScripts; + MWWorld::WorldModel* mWorldModel = MWBase::Environment::get().getWorldModel(); + }; + + void EngineEvents::callEngineHandlers() + { + Visitor vis(mGlobalScripts); + for (const Event& event : mQueue) + std::visit(vis, event); + mQueue.clear(); + } + +} diff --git a/apps/openmw/mwlua/engineevents.hpp b/apps/openmw/mwlua/engineevents.hpp new file mode 100644 index 00000000000..fb9183eb7ca --- /dev/null +++ b/apps/openmw/mwlua/engineevents.hpp @@ -0,0 +1,89 @@ +#ifndef MWLUA_ENGINEEVENTS_H +#define MWLUA_ENGINEEVENTS_H + +#include + +#include // defines RefNum that is used as a unique id + +#include "../mwworld/cellstore.hpp" + +namespace MWLua +{ + class GlobalScripts; + + class EngineEvents + { + public: + explicit EngineEvents(GlobalScripts& globalScripts) + : mGlobalScripts(globalScripts) + { + } + + struct OnActive + { + ESM::RefNum mObject; + }; + struct OnInactive + { + ESM::RefNum mObject; + }; + struct OnTeleported + { + ESM::RefNum mObject; + }; + struct OnActivate + { + ESM::RefNum mActor; + ESM::RefNum mObject; + }; + struct OnUseItem + { + ESM::RefNum mActor; + ESM::RefNum mObject; + bool mForce; + }; + struct OnConsume + { + ESM::RefNum mActor; + ESM::RefNum mConsumable; + }; + struct OnNewExterior + { + MWWorld::CellStore& mCell; + }; + struct OnAnimationTextKey + { + ESM::RefNum mActor; + std::string mGroupname; + std::string mKey; + }; + struct OnSkillUse + { + ESM::RefNum mActor; + std::string mSkill; + int useType; + float scale; + }; + struct OnSkillLevelUp + { + ESM::RefNum mActor; + std::string mSkill; + std::string mSource; + }; + using Event = std::variant; + + void clear() { mQueue.clear(); } + void addToQueue(Event e) { mQueue.push_back(std::move(e)); } + void callEngineHandlers(); + + private: + class Visitor; + + GlobalScripts& mGlobalScripts; + std::vector mQueue; + }; + +} + +#endif // MWLUA_ENGINEEVENTS_H diff --git a/apps/openmw/mwlua/factionbindings.cpp b/apps/openmw/mwlua/factionbindings.cpp new file mode 100644 index 00000000000..45234827ded --- /dev/null +++ b/apps/openmw/mwlua/factionbindings.cpp @@ -0,0 +1,113 @@ +#include "factionbindings.hpp" +#include "recordstore.hpp" + +#include +#include +#include + +#include "../mwbase/dialoguemanager.hpp" +#include "../mwbase/environment.hpp" + +#include "../mwworld/store.hpp" + +#include "idcollectionbindings.hpp" + +namespace +{ + struct FactionRank : ESM::RankData + { + std::string mRankName; + ESM::RefId mFactionId; + size_t mRankIndex; + + FactionRank(const ESM::RefId& factionId, const ESM::RankData& data, std::string_view rankName, size_t rankIndex) + : ESM::RankData(data) + , mRankName(rankName) + , mFactionId(factionId) + , mRankIndex(rankIndex) + { + } + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical> : std::false_type + { + }; +} + +namespace MWLua +{ + sol::table initCoreFactionBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table factions(lua, sol::create); + addRecordFunctionBinding(factions, context); + // Faction record + auto factionT = lua.new_usertype("ESM3_Faction"); + factionT[sol::meta_function::to_string] + = [](const ESM::Faction& rec) -> std::string { return "ESM3_Faction[" + rec.mId.toDebugString() + "]"; }; + factionT["id"] = sol::readonly_property([](const ESM::Faction& rec) { return rec.mId.serializeText(); }); + factionT["name"] + = sol::readonly_property([](const ESM::Faction& rec) -> std::string_view { return rec.mName; }); + factionT["hidden"] + = sol::readonly_property([](const ESM::Faction& rec) -> bool { return rec.mData.mIsHidden; }); + factionT["ranks"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Faction& rec) { + sol::table res(lua, sol::create); + for (size_t i = 0; i < rec.mRanks.size() && i < rec.mData.mRankData.size(); i++) + { + if (rec.mRanks[i].empty()) + break; + + res.add(FactionRank(rec.mId, rec.mData.mRankData[i], rec.mRanks[i], i)); + } + + return res; + }); + factionT["reactions"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Faction& rec) { + sol::table res(lua, sol::create); + for (const auto& [factionId, reaction] : rec.mReactions) + res[factionId.serializeText()] = reaction; + + const auto* overrides + = MWBase::Environment::get().getDialogueManager()->getFactionReactionOverrides(rec.mId); + + if (overrides != nullptr) + { + for (const auto& [factionId, reaction] : *overrides) + res[factionId.serializeText()] = reaction; + } + + return res; + }); + factionT["attributes"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Faction& rec) { + return createReadOnlyRefIdTable(lua, rec.mData.mAttribute, ESM::Attribute::indexToRefId); + }); + factionT["skills"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Faction& rec) { + return createReadOnlyRefIdTable(lua, rec.mData.mSkills, ESM::Skill::indexToRefId); + }); + auto rankT = lua.new_usertype("ESM3_FactionRank"); + rankT[sol::meta_function::to_string] = [](const FactionRank& rec) -> std::string { + return "ESM3_FactionRank[" + rec.mFactionId.toDebugString() + ", " + + std::to_string(LuaUtil::toLuaIndex(rec.mRankIndex)) + "]"; + }; + rankT["name"] + = sol::readonly_property([](const FactionRank& rec) -> std::string_view { return rec.mRankName; }); + rankT["primarySkillValue"] = sol::readonly_property([](const FactionRank& rec) { return rec.mPrimarySkill; }); + rankT["favouredSkillValue"] = sol::readonly_property([](const FactionRank& rec) { return rec.mFavouredSkill; }); + rankT["factionReaction"] = sol::readonly_property([](const FactionRank& rec) { return rec.mFactReaction; }); + rankT["attributeValues"] = sol::readonly_property([lua = lua.lua_state()](const FactionRank& rec) { + sol::table res(lua, sol::create); + res.add(rec.mAttribute1); + res.add(rec.mAttribute2); + return res; + }); + return LuaUtil::makeReadOnly(factions); + } +} diff --git a/apps/openmw/mwlua/factionbindings.hpp b/apps/openmw/mwlua/factionbindings.hpp new file mode 100644 index 00000000000..0dc06ceaf28 --- /dev/null +++ b/apps/openmw/mwlua/factionbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_FACTIONBINDINGS_H +#define MWLUA_FACTIONBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initCoreFactionBindings(const Context& context); +} + +#endif // MWLUA_FACTIONBINDINGS_H diff --git a/apps/openmw/mwlua/globalscripts.hpp b/apps/openmw/mwlua/globalscripts.hpp new file mode 100644 index 00000000000..37fff22f998 --- /dev/null +++ b/apps/openmw/mwlua/globalscripts.hpp @@ -0,0 +1,58 @@ +#ifndef MWLUA_GLOBALSCRIPTS_H +#define MWLUA_GLOBALSCRIPTS_H + +#include +#include + +#include "object.hpp" + +namespace MWLua +{ + + class GlobalScripts : public LuaUtil::ScriptsContainer + { + public: + GlobalScripts(LuaUtil::LuaState* lua) + : LuaUtil::ScriptsContainer(lua, "Global") + { + registerEngineHandlers({ + &mObjectActiveHandlers, + &mActorActiveHandlers, + &mItemActiveHandlers, + &mNewGameHandlers, + &mPlayerAddedHandlers, + &mOnActivateHandlers, + &mOnUseItemHandlers, + &mOnNewExteriorHandlers, + }); + } + + void newGameStarted() { callEngineHandlers(mNewGameHandlers); } + void objectActive(const GObject& obj) { callEngineHandlers(mObjectActiveHandlers, obj); } + void actorActive(const GObject& obj) { callEngineHandlers(mActorActiveHandlers, obj); } + void itemActive(const GObject& obj) { callEngineHandlers(mItemActiveHandlers, obj); } + void playerAdded(const GObject& obj) { callEngineHandlers(mPlayerAddedHandlers, obj); } + void onActivate(const GObject& obj, const GObject& actor) + { + callEngineHandlers(mOnActivateHandlers, obj, actor); + } + void onUseItem(const GObject& obj, const GObject& actor, bool force) + { + callEngineHandlers(mOnUseItemHandlers, obj, actor, force); + } + void onNewExterior(const GCell& cell) { callEngineHandlers(mOnNewExteriorHandlers, cell); } + + private: + EngineHandlerList mObjectActiveHandlers{ "onObjectActive" }; + EngineHandlerList mActorActiveHandlers{ "onActorActive" }; + EngineHandlerList mItemActiveHandlers{ "onItemActive" }; + EngineHandlerList mNewGameHandlers{ "onNewGame" }; + EngineHandlerList mPlayerAddedHandlers{ "onPlayerAdded" }; + EngineHandlerList mOnActivateHandlers{ "onActivate" }; + EngineHandlerList mOnUseItemHandlers{ "_onUseItem" }; + EngineHandlerList mOnNewExteriorHandlers{ "onNewExterior" }; + }; + +} + +#endif // MWLUA_GLOBALSCRIPTS_H diff --git a/apps/openmw/mwlua/idcollectionbindings.hpp b/apps/openmw/mwlua/idcollectionbindings.hpp new file mode 100644 index 00000000000..329486aba1b --- /dev/null +++ b/apps/openmw/mwlua/idcollectionbindings.hpp @@ -0,0 +1,25 @@ +#ifndef MWLUA_IDCOLLECTIONBINDINGS_H +#define MWLUA_IDCOLLECTIONBINDINGS_H + +#include + +#include +#include + +namespace MWLua +{ + template + sol::table createReadOnlyRefIdTable(lua_State* lua, const C& container, P projection = {}) + { + sol::table res(lua, sol::create); + for (const auto& element : container) + { + ESM::RefId id = projection(element); + if (!id.empty()) + res.add(id.serializeText()); + } + return LuaUtil::makeReadOnly(res); + } +} + +#endif diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp new file mode 100644 index 00000000000..b9affcdfca2 --- /dev/null +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -0,0 +1,452 @@ +#include "inputbindings.hpp" + +#include +#include +#include + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwinput/actions.hpp" + +#include "luamanagerimp.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + + template <> + struct is_automagical : std::false_type + { + }; + + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + sol::table initInputPackage(const Context& context) + { + sol::state_view lua = context.sol(); + { + if (lua["openmw_input"] != sol::nil) + return lua["openmw_input"]; + } + + sol::usertype keyEvent = lua.new_usertype("KeyEvent"); + keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) { + if (e.sym > 0 && e.sym <= 255) + return std::string(1, static_cast(e.sym)); + else + return std::string(); + }); + keyEvent["code"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.scancode; }); + keyEvent["withShift"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_SHIFT; }); + keyEvent["withCtrl"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_CTRL; }); + keyEvent["withAlt"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_ALT; }); + keyEvent["withSuper"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_GUI; }); + + auto touchpadEvent = lua.new_usertype("TouchpadEvent"); + touchpadEvent["device"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> int { return e.mDevice; }); + touchpadEvent["finger"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> int { return e.mFinger; }); + touchpadEvent["position"] = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> osg::Vec2f { + return { e.mX, e.mY }; + }); + touchpadEvent["pressure"] + = sol::readonly_property([](const SDLUtil::TouchEvent& e) -> float { return e.mPressure; }); + + auto inputActions = lua.new_usertype("InputActions"); + inputActions[sol::meta_function::index] + = [](LuaUtil::InputAction::Registry& registry, std::string_view key) { return registry[key]; }; + { + auto pairs = [](LuaUtil::InputAction::Registry& registry) { + auto next + = [](LuaUtil::InputAction::Registry& registry, + std::string_view key) -> sol::optional> { + std::optional nextKey(registry.nextKey(key)); + if (!nextKey.has_value()) + return sol::nullopt; + else + return std::make_tuple(*nextKey, registry[*nextKey].value()); + }; + return std::make_tuple(next, registry, registry.firstKey()); + }; + inputActions[sol::meta_function::pairs] = pairs; + } + + auto actionInfo = lua.new_usertype("ActionInfo"); + actionInfo["key"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mKey; }); + actionInfo["name"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mName; }); + actionInfo["description"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mDescription; }); + actionInfo["l10n"] = sol::readonly_property( + [](const LuaUtil::InputAction::Info& info) -> std::string_view { return info.mL10n; }); + actionInfo["type"] = sol::readonly_property([](const LuaUtil::InputAction::Info& info) { return info.mType; }); + actionInfo["defaultValue"] + = sol::readonly_property([](const LuaUtil::InputAction::Info& info) { return info.mDefaultValue; }); + + auto inputTriggers = lua.new_usertype("InputTriggers"); + inputTriggers[sol::meta_function::index] + = [](LuaUtil::InputTrigger::Registry& registry, std::string_view key) { return registry[key]; }; + { + auto pairs = [](LuaUtil::InputTrigger::Registry& registry) { + auto next + = [](LuaUtil::InputTrigger::Registry& registry, + std::string_view key) -> sol::optional> { + std::optional nextKey(registry.nextKey(key)); + if (!nextKey.has_value()) + return sol::nullopt; + else + return std::make_tuple(*nextKey, registry[*nextKey].value()); + }; + return std::make_tuple(next, registry, registry.firstKey()); + }; + inputTriggers[sol::meta_function::pairs] = pairs; + } + + auto triggerInfo = lua.new_usertype("TriggerInfo"); + triggerInfo["key"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mKey; }); + triggerInfo["name"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mName; }); + triggerInfo["description"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mDescription; }); + triggerInfo["l10n"] = sol::readonly_property( + [](const LuaUtil::InputTrigger::Info& info) -> std::string_view { return info.mL10n; }); + + MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); + sol::table api(lua, sol::create); + + api["ACTION_TYPE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Boolean", LuaUtil::InputAction::Type::Boolean }, + { "Number", LuaUtil::InputAction::Type::Number }, + { "Range", LuaUtil::InputAction::Type::Range }, + })); + + api["actions"] = std::ref(context.mLuaManager->inputActions()); + api["registerAction"] = [manager = context.mLuaManager](sol::table options) { + LuaUtil::InputAction::Info parsedOptions; + parsedOptions.mKey = options["key"].get(); + parsedOptions.mType = options["type"].get(); + parsedOptions.mL10n = options["l10n"].get(); + parsedOptions.mName = options["name"].get(); + parsedOptions.mDescription = options["description"].get(); + parsedOptions.mDefaultValue = options["defaultValue"].get(); + manager->inputActions().insert(std::move(parsedOptions)); + }; + api["bindAction"] = [manager = context.mLuaManager]( + std::string_view key, const sol::table& callback, sol::table dependencies) { + std::vector parsedDependencies; + parsedDependencies.reserve(dependencies.size()); + for (size_t i = 1; i <= dependencies.size(); ++i) + { + sol::object dependency = dependencies[i]; + if (!dependency.is()) + throw std::domain_error("The dependencies argument must be a list of Action keys"); + parsedDependencies.push_back(dependency.as()); + } + if (!manager->inputActions().bind(key, LuaUtil::Callback::fromLua(callback), parsedDependencies)) + throw std::domain_error("Cyclic action binding"); + }; + api["registerActionHandler"] + = [manager = context.mLuaManager](std::string_view key, const sol::table& callback) { + manager->inputActions().registerHandler(key, LuaUtil::Callback::fromLua(callback)); + }; + api["getBooleanActionValue"] = [manager = context.mLuaManager](std::string_view key) { + return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Boolean); + }; + api["getNumberActionValue"] = [manager = context.mLuaManager](std::string_view key) { + return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Number); + }; + api["getRangeActionValue"] = [manager = context.mLuaManager](std::string_view key) { + return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Range); + }; + + api["triggers"] = std::ref(context.mLuaManager->inputTriggers()); + api["registerTrigger"] = [manager = context.mLuaManager](sol::table options) { + LuaUtil::InputTrigger::Info parsedOptions; + parsedOptions.mKey = options["key"].get(); + parsedOptions.mL10n = options["l10n"].get(); + parsedOptions.mName = options["name"].get(); + parsedOptions.mDescription = options["description"].get(); + manager->inputTriggers().insert(std::move(parsedOptions)); + }; + api["registerTriggerHandler"] + = [manager = context.mLuaManager](std::string_view key, const sol::table& callback) { + manager->inputTriggers().registerHandler(key, LuaUtil::Callback::fromLua(callback)); + }; + api["activateTrigger"] + = [manager = context.mLuaManager](std::string_view key) { manager->inputTriggers().activate(key); }; + + api["isIdle"] = [input]() { return input->isIdle(); }; + api["isActionPressed"] = [input](int action) { return input->actionIsActive(action); }; + api["isKeyPressed"] = [](SDL_Scancode code) -> bool { + int maxCode; + const auto* state = SDL_GetKeyboardState(&maxCode); + if (code >= 0 && code < maxCode) + return state[code] != 0; + else + return false; + }; + api["isShiftPressed"] = []() -> bool { return SDL_GetModState() & KMOD_SHIFT; }; + api["isCtrlPressed"] = []() -> bool { return SDL_GetModState() & KMOD_CTRL; }; + api["isAltPressed"] = []() -> bool { return SDL_GetModState() & KMOD_ALT; }; + api["isSuperPressed"] = []() -> bool { return SDL_GetModState() & KMOD_GUI; }; + api["isControllerButtonPressed"] = [input](int button) { + return input->isControllerButtonPressed(static_cast(button)); + }; + api["isMouseButtonPressed"] + = [](int button) -> bool { return SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(button); }; + api["_isGamepadCursorActive"] = [input]() -> bool { return input->isGamepadGuiCursorEnabled(); }; + api["_setGamepadCursorActive"] = [input](bool v) { + input->setGamepadGuiCursorEnabled(v); + MWBase::Environment::get().getWindowManager()->setCursorActive(v); + }; + api["getMouseMoveX"] = [input]() { return input->getMouseMoveX(); }; + api["getMouseMoveY"] = [input]() { return input->getMouseMoveY(); }; + api["getAxisValue"] = [input](int axis) { + if (axis < SDL_CONTROLLER_AXIS_MAX) + return input->getControllerAxisValue(static_cast(axis)); + else + return input->getActionValue(axis - SDL_CONTROLLER_AXIS_MAX) * 2 - 1; + }; + + // input.CONTROL_SWITCH is deprecated, remove after releasing 0.49 + api["getControlSwitch"] = [input](std::string_view key) { return input->getControlSwitch(key); }; + api["setControlSwitch"] = [input](std::string_view key, bool v) { input->toggleControlSwitch(key, v); }; + + api["getKeyName"] = [](SDL_Scancode code) { return SDL_GetScancodeName(code); }; + + api["ACTION"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "GameMenu", MWInput::A_GameMenu }, + { "Screenshot", MWInput::A_Screenshot }, + { "Inventory", MWInput::A_Inventory }, + { "Console", MWInput::A_Console }, + + { "MoveLeft", MWInput::A_MoveLeft }, + { "MoveRight", MWInput::A_MoveRight }, + { "MoveForward", MWInput::A_MoveForward }, + { "MoveBackward", MWInput::A_MoveBackward }, + + { "Activate", MWInput::A_Activate }, + { "Use", MWInput::A_Use }, + { "Jump", MWInput::A_Jump }, + { "AutoMove", MWInput::A_AutoMove }, + { "Rest", MWInput::A_Rest }, + { "Journal", MWInput::A_Journal }, + { "Run", MWInput::A_Run }, + { "CycleSpellLeft", MWInput::A_CycleSpellLeft }, + { "CycleSpellRight", MWInput::A_CycleSpellRight }, + { "CycleWeaponLeft", MWInput::A_CycleWeaponLeft }, + { "CycleWeaponRight", MWInput::A_CycleWeaponRight }, + { "AlwaysRun", MWInput::A_AlwaysRun }, + { "Sneak", MWInput::A_Sneak }, + + { "QuickSave", MWInput::A_QuickSave }, + { "QuickLoad", MWInput::A_QuickLoad }, + { "QuickMenu", MWInput::A_QuickMenu }, + { "ToggleWeapon", MWInput::A_ToggleWeapon }, + { "ToggleSpell", MWInput::A_ToggleSpell }, + { "TogglePOV", MWInput::A_TogglePOV }, + + { "QuickKey1", MWInput::A_QuickKey1 }, + { "QuickKey2", MWInput::A_QuickKey2 }, + { "QuickKey3", MWInput::A_QuickKey3 }, + { "QuickKey4", MWInput::A_QuickKey4 }, + { "QuickKey5", MWInput::A_QuickKey5 }, + { "QuickKey6", MWInput::A_QuickKey6 }, + { "QuickKey7", MWInput::A_QuickKey7 }, + { "QuickKey8", MWInput::A_QuickKey8 }, + { "QuickKey9", MWInput::A_QuickKey9 }, + { "QuickKey10", MWInput::A_QuickKey10 }, + { "QuickKeysMenu", MWInput::A_QuickKeysMenu }, + + { "ToggleHUD", MWInput::A_ToggleHUD }, + { "ToggleDebug", MWInput::A_ToggleDebug }, + { "TogglePostProcessorHUD", MWInput::A_TogglePostProcessorHUD }, + + { "ZoomIn", MWInput::A_ZoomIn }, + { "ZoomOut", MWInput::A_ZoomOut }, + })); + + // input.CONTROL_SWITCH is deprecated, remove after releasing 0.49 + api["CONTROL_SWITCH"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Controls", "playercontrols" }, + { "Fighting", "playerfighting" }, + { "Jumping", "playerjumping" }, + { "Looking", "playerlooking" }, + { "Magic", "playermagic" }, + { "ViewMode", "playerviewswitch" }, + { "VanityMode", "vanitymode" }, + })); + + api["CONTROLLER_BUTTON"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "A", SDL_CONTROLLER_BUTTON_A }, + { "B", SDL_CONTROLLER_BUTTON_B }, + { "X", SDL_CONTROLLER_BUTTON_X }, + { "Y", SDL_CONTROLLER_BUTTON_Y }, + { "Back", SDL_CONTROLLER_BUTTON_BACK }, + { "Guide", SDL_CONTROLLER_BUTTON_GUIDE }, + { "Start", SDL_CONTROLLER_BUTTON_START }, + { "LeftStick", SDL_CONTROLLER_BUTTON_LEFTSTICK }, + { "RightStick", SDL_CONTROLLER_BUTTON_RIGHTSTICK }, + { "LeftShoulder", SDL_CONTROLLER_BUTTON_LEFTSHOULDER }, + { "RightShoulder", SDL_CONTROLLER_BUTTON_RIGHTSHOULDER }, + { "DPadUp", SDL_CONTROLLER_BUTTON_DPAD_UP }, + { "DPadDown", SDL_CONTROLLER_BUTTON_DPAD_DOWN }, + { "DPadLeft", SDL_CONTROLLER_BUTTON_DPAD_LEFT }, + { "DPadRight", SDL_CONTROLLER_BUTTON_DPAD_RIGHT }, + })); + + api["CONTROLLER_AXIS"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "LeftX", SDL_CONTROLLER_AXIS_LEFTX }, + { "LeftY", SDL_CONTROLLER_AXIS_LEFTY }, + { "RightX", SDL_CONTROLLER_AXIS_RIGHTX }, + { "RightY", SDL_CONTROLLER_AXIS_RIGHTY }, + { "TriggerLeft", SDL_CONTROLLER_AXIS_TRIGGERLEFT }, + { "TriggerRight", SDL_CONTROLLER_AXIS_TRIGGERRIGHT }, + + { "LookUpDown", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_LookUpDown) }, + { "LookLeftRight", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_LookLeftRight) }, + { "MoveForwardBackward", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_MoveForwardBackward) }, + { "MoveLeftRight", SDL_CONTROLLER_AXIS_MAX + static_cast(MWInput::A_MoveLeftRight) }, + })); + + api["KEY"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "_0", SDL_SCANCODE_0 }, + { "_1", SDL_SCANCODE_1 }, + { "_2", SDL_SCANCODE_2 }, + { "_3", SDL_SCANCODE_3 }, + { "_4", SDL_SCANCODE_4 }, + { "_5", SDL_SCANCODE_5 }, + { "_6", SDL_SCANCODE_6 }, + { "_7", SDL_SCANCODE_7 }, + { "_8", SDL_SCANCODE_8 }, + { "_9", SDL_SCANCODE_9 }, + + { "NP_0", SDL_SCANCODE_KP_0 }, + { "NP_1", SDL_SCANCODE_KP_1 }, + { "NP_2", SDL_SCANCODE_KP_2 }, + { "NP_3", SDL_SCANCODE_KP_3 }, + { "NP_4", SDL_SCANCODE_KP_4 }, + { "NP_5", SDL_SCANCODE_KP_5 }, + { "NP_6", SDL_SCANCODE_KP_6 }, + { "NP_7", SDL_SCANCODE_KP_7 }, + { "NP_8", SDL_SCANCODE_KP_8 }, + { "NP_9", SDL_SCANCODE_KP_9 }, + { "NP_Divide", SDL_SCANCODE_KP_DIVIDE }, + { "NP_Enter", SDL_SCANCODE_KP_ENTER }, + { "NP_Minus", SDL_SCANCODE_KP_MINUS }, + { "NP_Multiply", SDL_SCANCODE_KP_MULTIPLY }, + { "NP_Delete", SDL_SCANCODE_KP_PERIOD }, + { "NP_Plus", SDL_SCANCODE_KP_PLUS }, + + { "F1", SDL_SCANCODE_F1 }, + { "F2", SDL_SCANCODE_F2 }, + { "F3", SDL_SCANCODE_F3 }, + { "F4", SDL_SCANCODE_F4 }, + { "F5", SDL_SCANCODE_F5 }, + { "F6", SDL_SCANCODE_F6 }, + { "F7", SDL_SCANCODE_F7 }, + { "F8", SDL_SCANCODE_F8 }, + { "F9", SDL_SCANCODE_F9 }, + { "F10", SDL_SCANCODE_F10 }, + { "F11", SDL_SCANCODE_F11 }, + { "F12", SDL_SCANCODE_F12 }, + + { "A", SDL_SCANCODE_A }, + { "B", SDL_SCANCODE_B }, + { "C", SDL_SCANCODE_C }, + { "D", SDL_SCANCODE_D }, + { "E", SDL_SCANCODE_E }, + { "F", SDL_SCANCODE_F }, + { "G", SDL_SCANCODE_G }, + { "H", SDL_SCANCODE_H }, + { "I", SDL_SCANCODE_I }, + { "J", SDL_SCANCODE_J }, + { "K", SDL_SCANCODE_K }, + { "L", SDL_SCANCODE_L }, + { "M", SDL_SCANCODE_M }, + { "N", SDL_SCANCODE_N }, + { "O", SDL_SCANCODE_O }, + { "P", SDL_SCANCODE_P }, + { "Q", SDL_SCANCODE_Q }, + { "R", SDL_SCANCODE_R }, + { "S", SDL_SCANCODE_S }, + { "T", SDL_SCANCODE_T }, + { "U", SDL_SCANCODE_U }, + { "V", SDL_SCANCODE_V }, + { "W", SDL_SCANCODE_W }, + { "X", SDL_SCANCODE_X }, + { "Y", SDL_SCANCODE_Y }, + { "Z", SDL_SCANCODE_Z }, + + { "LeftArrow", SDL_SCANCODE_LEFT }, + { "RightArrow", SDL_SCANCODE_RIGHT }, + { "UpArrow", SDL_SCANCODE_UP }, + { "DownArrow", SDL_SCANCODE_DOWN }, + + { "LeftAlt", SDL_SCANCODE_LALT }, + { "LeftCtrl", SDL_SCANCODE_LCTRL }, + { "LeftBracket", SDL_SCANCODE_LEFTBRACKET }, + { "LeftSuper", SDL_SCANCODE_LGUI }, + { "LeftShift", SDL_SCANCODE_LSHIFT }, + { "RightAlt", SDL_SCANCODE_RALT }, + { "RightCtrl", SDL_SCANCODE_RCTRL }, + { "RightSuper", SDL_SCANCODE_RGUI }, + { "RightBracket", SDL_SCANCODE_RIGHTBRACKET }, + { "RightShift", SDL_SCANCODE_RSHIFT }, + + { "Apostrophe", SDL_SCANCODE_APOSTROPHE }, + { "BackSlash", SDL_SCANCODE_BACKSLASH }, + { "Backspace", SDL_SCANCODE_BACKSPACE }, + { "CapsLock", SDL_SCANCODE_CAPSLOCK }, + { "Comma", SDL_SCANCODE_COMMA }, + { "Delete", SDL_SCANCODE_DELETE }, + { "End", SDL_SCANCODE_END }, + { "Enter", SDL_SCANCODE_RETURN }, + { "Equals", SDL_SCANCODE_EQUALS }, + { "Escape", SDL_SCANCODE_ESCAPE }, + { "Home", SDL_SCANCODE_HOME }, + { "Insert", SDL_SCANCODE_INSERT }, + { "Minus", SDL_SCANCODE_MINUS }, + { "NumLock", SDL_SCANCODE_NUMLOCKCLEAR }, + { "PageDown", SDL_SCANCODE_PAGEDOWN }, + { "PageUp", SDL_SCANCODE_PAGEUP }, + { "Period", SDL_SCANCODE_PERIOD }, + { "Pause", SDL_SCANCODE_PAUSE }, + { "PrintScreen", SDL_SCANCODE_PRINTSCREEN }, + { "ScrollLock", SDL_SCANCODE_SCROLLLOCK }, + { "Semicolon", SDL_SCANCODE_SEMICOLON }, + { "Slash", SDL_SCANCODE_SLASH }, + { "Space", SDL_SCANCODE_SPACE }, + { "Tab", SDL_SCANCODE_TAB }, + })); + + lua["openmw_input"] = LuaUtil::makeReadOnly(api); + return lua["openmw_input"]; + } + +} diff --git a/apps/openmw/mwlua/inputbindings.hpp b/apps/openmw/mwlua/inputbindings.hpp new file mode 100644 index 00000000000..195f69f5f9d --- /dev/null +++ b/apps/openmw/mwlua/inputbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_INPUTBINDINGS_H +#define MWLUA_INPUTBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initInputPackage(const Context&); +} + +#endif // MWLUA_INPUTBINDINGS_H diff --git a/apps/openmw/mwlua/inputprocessor.hpp b/apps/openmw/mwlua/inputprocessor.hpp new file mode 100644 index 00000000000..dcd19ae8cd2 --- /dev/null +++ b/apps/openmw/mwlua/inputprocessor.hpp @@ -0,0 +1,85 @@ +#ifndef MWLUA_INPUTPROCESSOR_H +#define MWLUA_INPUTPROCESSOR_H + +#include + +#include + +#include "../mwbase/luamanager.hpp" + +namespace MWLua +{ + template + class InputProcessor + { + public: + InputProcessor(Container* scriptsContainer) + : mScriptsContainer(scriptsContainer) + { + mScriptsContainer->registerEngineHandlers({ &mKeyPressHandlers, &mKeyReleaseHandlers, + &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mActionHandlers, &mTouchpadPressed, + &mTouchpadReleased, &mTouchpadMoved, &mMouseButtonPress, &mMouseButtonRelease, &mMouseWheel }); + } + + void processInputEvent(const MWBase::LuaManager::InputEvent& event) + { + using InputEvent = MWBase::LuaManager::InputEvent; + switch (event.mType) + { + case InputEvent::KeyPressed: + mScriptsContainer->callEngineHandlers(mKeyPressHandlers, std::get(event.mValue)); + break; + case InputEvent::KeyReleased: + mScriptsContainer->callEngineHandlers(mKeyReleaseHandlers, std::get(event.mValue)); + break; + case InputEvent::ControllerPressed: + mScriptsContainer->callEngineHandlers(mControllerButtonPressHandlers, std::get(event.mValue)); + break; + case InputEvent::ControllerReleased: + mScriptsContainer->callEngineHandlers( + mControllerButtonReleaseHandlers, std::get(event.mValue)); + break; + case InputEvent::Action: + mScriptsContainer->callEngineHandlers(mActionHandlers, std::get(event.mValue)); + break; + case InputEvent::TouchPressed: + mScriptsContainer->callEngineHandlers( + mTouchpadPressed, std::get(event.mValue)); + break; + case InputEvent::TouchReleased: + mScriptsContainer->callEngineHandlers( + mTouchpadReleased, std::get(event.mValue)); + break; + case InputEvent::TouchMoved: + mScriptsContainer->callEngineHandlers(mTouchpadMoved, std::get(event.mValue)); + break; + case InputEvent::MouseButtonPressed: + mScriptsContainer->callEngineHandlers(mMouseButtonPress, std::get(event.mValue)); + break; + case InputEvent::MouseButtonReleased: + mScriptsContainer->callEngineHandlers(mMouseButtonRelease, std::get(event.mValue)); + break; + case InputEvent::MouseWheel: + auto wheelEvent = std::get(event.mValue); + mScriptsContainer->callEngineHandlers(mMouseWheel, wheelEvent.y, wheelEvent.x); + break; + } + } + + private: + Container* mScriptsContainer; + typename Container::EngineHandlerList mKeyPressHandlers{ "onKeyPress" }; + typename Container::EngineHandlerList mKeyReleaseHandlers{ "onKeyRelease" }; + typename Container::EngineHandlerList mControllerButtonPressHandlers{ "onControllerButtonPress" }; + typename Container::EngineHandlerList mControllerButtonReleaseHandlers{ "onControllerButtonRelease" }; + typename Container::EngineHandlerList mActionHandlers{ "onInputAction" }; + typename Container::EngineHandlerList mTouchpadPressed{ "onTouchPress" }; + typename Container::EngineHandlerList mTouchpadReleased{ "onTouchRelease" }; + typename Container::EngineHandlerList mTouchpadMoved{ "onTouchMove" }; + typename Container::EngineHandlerList mMouseButtonPress{ "onMouseButtonPress" }; + typename Container::EngineHandlerList mMouseButtonRelease{ "onMouseButtonRelease" }; + typename Container::EngineHandlerList mMouseWheel{ "onMouseWheel" }; + }; +} + +#endif // MWLUA_INPUTPROCESSOR_H diff --git a/apps/openmw/mwlua/itemdata.cpp b/apps/openmw/mwlua/itemdata.cpp new file mode 100644 index 00000000000..38415ea25e2 --- /dev/null +++ b/apps/openmw/mwlua/itemdata.cpp @@ -0,0 +1,200 @@ +#include "itemdata.hpp" + +#include +#include + +#include "context.hpp" +#include "luamanagerimp.hpp" +#include "objectvariant.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwmechanics/spellutil.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" + +namespace +{ + using SelfObject = MWLua::SelfObject; + using Index = const SelfObject::CachedStat::Index&; + + constexpr std::array properties = { "condition", "enchantmentCharge", "soul" }; + + void valueErr(std::string_view prop, std::string type) + { + throw std::logic_error("'" + std::string(prop) + "'" + " received invalid value type (" + type + ")"); + } +} + +namespace MWLua +{ + static void addStatUpdateAction(MWLua::LuaManager* manager, const SelfObject& obj) + { + if (!obj.mStatsCache.empty()) + return; // was already added before + manager->addAction( + [obj = Object(obj)] { + LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); + if (scripts) + scripts->applyStatsCache(); + }, + "StatUpdateAction"); + } + + class ItemData + { + ObjectVariant mObject; + + public: + ItemData(const ObjectVariant& object) + : mObject(object) + { + } + + sol::object get(const Context& context, std::string_view prop) const + { + if (mObject.isSelfObject()) + { + SelfObject* self = mObject.asSelfObject(); + auto it = self->mStatsCache.find({ &ItemData::setValue, std::monostate{}, prop }); + if (it != self->mStatsCache.end()) + return it->second; + } + return sol::make_object(context.mLua->unsafeState(), getValue(context, prop, mObject.ptr())); + } + + void set(const Context& context, std::string_view prop, const sol::object& value) const + { + if (mObject.isGObject()) + setValue({}, prop, mObject.ptr(), value); + else if (mObject.isSelfObject()) + { + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &ItemData::setValue, std::monostate{}, prop }] = value; + } + else + throw std::runtime_error("Only global or self scripts can set the value"); + } + + static sol::object getValue(const Context& context, std::string_view prop, const MWWorld::Ptr& ptr) + { + if (prop == "condition") + { + if (ptr.mRef->getType() == ESM::REC_LIGH) + return sol::make_object(context.mLua->unsafeState(), ptr.getClass().getRemainingUsageTime(ptr)); + else if (ptr.getClass().hasItemHealth(ptr)) + return sol::make_object(context.mLua->unsafeState(), + ptr.getClass().getItemHealth(ptr) + ptr.getCellRef().getChargeIntRemainder()); + } + else if (prop == "enchantmentCharge") + { + const ESM::RefId& enchantmentName = ptr.getClass().getEnchantment(ptr); + + if (enchantmentName.empty()) + return sol::lua_nil; + + float charge = ptr.getCellRef().getEnchantmentCharge(); + const auto& store = MWBase::Environment::get().getESMStore(); + const auto* enchantment = store->get().find(enchantmentName); + + if (charge == -1) // return the full charge + return sol::make_object( + context.mLua->unsafeState(), MWMechanics::getEnchantmentCharge(*enchantment)); + + return sol::make_object(context.mLua->unsafeState(), charge); + } + else if (prop == "soul") + { + ESM::RefId soul = ptr.getCellRef().getSoul(); + if (soul.empty()) + return sol::lua_nil; + + return sol::make_object(context.mLua->unsafeState(), soul.serializeText()); + } + + return sol::lua_nil; + } + + static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + if (prop == "condition") + { + if (value.get_type() == sol::type::number) + { + float cond = LuaUtil::cast(value); + if (ptr.mRef->getType() == ESM::REC_LIGH) + ptr.getClass().setRemainingUsageTime(ptr, cond); + else if (ptr.getClass().hasItemHealth(ptr)) + { + // if the value set is less than 0, chargeInt and chargeIntRemainder is set to 0 + ptr.getCellRef().setChargeIntRemainder(std::max(0.f, std::modf(cond, &cond))); + ptr.getCellRef().setCharge(std::max(0.f, cond)); + } + } + else + valueErr(prop, sol::type_name(value.lua_state(), value.get_type())); + } + else if (prop == "enchantmentCharge") + { + if (value.get_type() == sol::type::lua_nil) + ptr.getCellRef().setEnchantmentCharge(-1); + else if (value.get_type() == sol::type::number) + ptr.getCellRef().setEnchantmentCharge(std::max(0.0f, LuaUtil::cast(value))); + else + valueErr(prop, sol::type_name(value.lua_state(), value.get_type())); + } + else if (prop == "soul") + { + if (value.get_type() == sol::type::lua_nil) + ptr.getCellRef().setSoul(ESM::RefId{}); + else if (value.get_type() == sol::type::string) + { + std::string_view souldId = LuaUtil::cast(value); + ESM::RefId creature = ESM::RefId::deserializeText(souldId); + const auto& store = *MWBase::Environment::get().getESMStore(); + + // TODO: Add Support for NPC Souls + if (store.get().search(creature)) + ptr.getCellRef().setSoul(creature); + else + throw std::runtime_error("Cannot use non-existent creature as a soul: " + std::string(souldId)); + } + else + valueErr(prop, sol::type_name(value.lua_state(), value.get_type())); + } + } + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addItemDataBindings(sol::table& item, const Context& context) + { + item["itemData"] = [](const sol::object& object) -> sol::optional { + ObjectVariant o(object); + if (o.ptr().getClass().isItem(o.ptr()) || o.ptr().mRef->getType() == ESM::REC_LIGH) + return ItemData(o); + return {}; + }; + + sol::usertype itemData = context.sol().new_usertype("ItemData"); + itemData[sol::meta_function::new_index] = [](const ItemData& stat, const sol::variadic_args args) { + throw std::runtime_error("Unknown ItemData property '" + args.get() + "'"); + }; + + for (std::string_view prop : properties) + { + itemData[prop] = sol::property([context, prop](const ItemData& stat) { return stat.get(context, prop); }, + [context, prop](const ItemData& stat, const sol::object& value) { stat.set(context, prop, value); }); + } + } +} diff --git a/apps/openmw/mwlua/itemdata.hpp b/apps/openmw/mwlua/itemdata.hpp new file mode 100644 index 00000000000..f70705fb6c6 --- /dev/null +++ b/apps/openmw/mwlua/itemdata.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_ITEMDATA_H +#define MWLUA_ITEMDATA_H + +#include + +namespace MWLua +{ + struct Context; + + void addItemDataBindings(sol::table& item, const Context& context); + +} +#endif // MWLUA_ITEMDATA_H diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp new file mode 100644 index 00000000000..988bd730514 --- /dev/null +++ b/apps/openmw/mwlua/localscripts.cpp @@ -0,0 +1,254 @@ +#include "localscripts.hpp" + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwmechanics/aicombat.hpp" +#include "../mwmechanics/aiescort.hpp" +#include "../mwmechanics/aifollow.hpp" +#include "../mwmechanics/aipackage.hpp" +#include "../mwmechanics/aipursue.hpp" +#include "../mwmechanics/aisequence.hpp" +#include "../mwmechanics/aitravel.hpp" +#include "../mwmechanics/aiwander.hpp" +#include "../mwmechanics/attacktype.hpp" +#include "../mwmechanics/creaturestats.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/ptr.hpp" + +#include "context.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + void LocalScripts::initializeSelfPackage(const Context& context) + { + auto lua = context.sol(); + using ActorControls = MWBase::LuaManager::ActorControls; + sol::usertype controls = lua.new_usertype("ActorControls"); + +#define CONTROL(TYPE, FIELD) \ + sol::property([](const ActorControls& c) { return c.FIELD; }, \ + [](ActorControls& c, const TYPE& v) { \ + c.FIELD = v; \ + c.mChanged = true; \ + }) + controls["movement"] = CONTROL(float, mMovement); + controls["sideMovement"] = CONTROL(float, mSideMovement); + controls["pitchChange"] = CONTROL(float, mPitchChange); + controls["yawChange"] = CONTROL(float, mYawChange); + controls["run"] = CONTROL(bool, mRun); + controls["sneak"] = CONTROL(bool, mSneak); + controls["jump"] = CONTROL(bool, mJump); + controls["use"] = CONTROL(int, mUse); +#undef CONTROL + + sol::usertype selfAPI + = lua.new_usertype("SelfObject", sol::base_classes, sol::bases()); + selfAPI[sol::meta_function::to_string] + = [](SelfObject& self) { return "openmw.self[" + self.toString() + "]"; }; + selfAPI["object"] = sol::readonly_property([](SelfObject& self) -> LObject { return LObject(self); }); + selfAPI["controls"] = sol::readonly_property([](SelfObject& self) { return &self.mControls; }); + selfAPI["isActive"] = [](SelfObject& self) { return &self.mIsActive; }; + selfAPI["enableAI"] = [](SelfObject& self, bool v) { self.mControls.mDisableAI = !v; }; + selfAPI["ATTACK_TYPE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { { "NoAttack", MWMechanics::AttackType::NoAttack }, { "Any", MWMechanics::AttackType::Any }, + { "Chop", MWMechanics::AttackType::Chop }, { "Slash", MWMechanics::AttackType::Slash }, + { "Thrust", MWMechanics::AttackType::Thrust } })); + + using AiPackage = MWMechanics::AiPackage; + sol::usertype aiPackage = lua.new_usertype("AiPackage"); + aiPackage["type"] = sol::readonly_property([](const AiPackage& p) -> std::string_view { + switch (p.getTypeId()) + { + case MWMechanics::AiPackageTypeId::Wander: + return "Wander"; + case MWMechanics::AiPackageTypeId::Travel: + return "Travel"; + case MWMechanics::AiPackageTypeId::Escort: + return "Escort"; + case MWMechanics::AiPackageTypeId::Follow: + return "Follow"; + case MWMechanics::AiPackageTypeId::Activate: + return "Activate"; + case MWMechanics::AiPackageTypeId::Combat: + return "Combat"; + case MWMechanics::AiPackageTypeId::Pursue: + return "Pursue"; + case MWMechanics::AiPackageTypeId::AvoidDoor: + return "AvoidDoor"; + case MWMechanics::AiPackageTypeId::Face: + return "Face"; + case MWMechanics::AiPackageTypeId::Breathe: + return "Breathe"; + case MWMechanics::AiPackageTypeId::Cast: + return "Cast"; + default: + return "Unknown"; + } + }); + aiPackage["target"] = sol::readonly_property([](const AiPackage& p) -> sol::optional { + MWWorld::Ptr target = p.getTarget(); + if (target.isEmpty()) + return sol::nullopt; + else + return LObject(getId(target)); + }); + aiPackage["sideWithTarget"] = sol::readonly_property([](const AiPackage& p) { return p.sideWithTarget(); }); + aiPackage["destPosition"] = sol::readonly_property([](const AiPackage& p) { return p.getDestination(); }); + aiPackage["distance"] = sol::readonly_property([](const AiPackage& p) { return p.getDistance(); }); + aiPackage["duration"] = sol::readonly_property([](const AiPackage& p) { return p.getDuration(); }); + aiPackage["idle"] + = sol::readonly_property([lua = lua.lua_state()](const AiPackage& p) -> sol::optional { + if (p.getTypeId() == MWMechanics::AiPackageTypeId::Wander) + { + sol::table idles(lua, sol::create); + const std::vector& idle = static_cast(p).getIdle(); + if (!idle.empty()) + { + for (size_t i = 0; i < idle.size(); ++i) + { + std::string_view groupName = MWMechanics::AiWander::getIdleGroupName(i); + idles[groupName] = idle[i]; + } + return idles; + } + } + return sol::nullopt; + }); + + aiPackage["isRepeat"] = sol::readonly_property([](const AiPackage& p) { return p.getRepeat(); }); + + selfAPI["_isFleeing"] = [](SelfObject& self) -> bool { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + if (ai.isEmpty()) + return false; + else + return ai.isFleeing(); + }; + selfAPI["_getActiveAiPackage"] = [](SelfObject& self) -> sol::optional> { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + if (ai.isEmpty()) + return sol::nullopt; + else + return *ai.begin(); + }; + selfAPI["_iterateAndFilterAiSequence"] = [](SelfObject& self, sol::function callback) { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + + ai.erasePackagesIf([&](auto& entry) { + bool keep = LuaUtil::call(callback, entry).template get(); + return !keep; + }); + }; + selfAPI["_startAiCombat"] = [](SelfObject& self, const LObject& target, bool cancelOther) { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + ai.stack(MWMechanics::AiCombat(target.ptr()), ptr, cancelOther); + }; + selfAPI["_startAiPursue"] = [](SelfObject& self, const LObject& target, bool cancelOther) { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + ai.stack(MWMechanics::AiPursue(target.ptr()), ptr, cancelOther); + }; + selfAPI["_startAiFollow"] = [](SelfObject& self, const LObject& target, sol::optional cell, + float duration, const osg::Vec3f& dest, bool repeat, bool cancelOther) { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + if (cell) + { + ai.stack(MWMechanics::AiFollow(target.ptr().getCellRef().getRefId(), + cell->mStore->getCell()->getNameId(), duration, dest.x(), dest.y(), dest.z(), repeat), + ptr, cancelOther); + } + else + { + ai.stack(MWMechanics::AiFollow( + target.ptr().getCellRef().getRefId(), duration, dest.x(), dest.y(), dest.z(), repeat), + ptr, cancelOther); + } + }; + selfAPI["_startAiEscort"] = [](SelfObject& self, const LObject& target, LCell cell, float duration, + const osg::Vec3f& dest, bool repeat, bool cancelOther) { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + // TODO: change AiEscort implementation to accept ptr instead of a non-unique refId. + const ESM::RefId& refId = target.ptr().getCellRef().getRefId(); + int gameHoursDuration = static_cast(std::ceil(duration / 3600.0)); + auto* esmCell = cell.mStore->getCell(); + if (esmCell->isExterior()) + ai.stack(MWMechanics::AiEscort(refId, gameHoursDuration, dest.x(), dest.y(), dest.z(), repeat), ptr, + cancelOther); + else + ai.stack(MWMechanics::AiEscort( + refId, esmCell->getNameId(), gameHoursDuration, dest.x(), dest.y(), dest.z(), repeat), + ptr, cancelOther); + }; + selfAPI["_startAiWander"] + = [](SelfObject& self, int distance, int duration, sol::table luaIdle, bool repeat, bool cancelOther) { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + std::vector idle; + // Lua index starts at 1 + for (size_t i = 1; i <= luaIdle.size(); i++) + idle.emplace_back(luaIdle.get(i)); + ai.stack(MWMechanics::AiWander(distance, duration, 0, idle, repeat), ptr, cancelOther); + }; + selfAPI["_startAiTravel"] = [](SelfObject& self, const osg::Vec3f& target, bool repeat, bool cancelOther) { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + ai.stack(MWMechanics::AiTravel(target.x(), target.y(), target.z(), repeat), ptr, cancelOther); + }; + selfAPI["_enableLuaAnimations"] = [](SelfObject& self, bool enable) { + const MWWorld::Ptr& ptr = self.ptr(); + MWBase::Environment::get().getMechanicsManager()->enableLuaAnimations(ptr, enable); + }; + } + + LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) + : LuaUtil::ScriptsContainer(lua, "L" + obj.id().toString()) + , mData(obj) + { + lua->protectedCall( + [&](LuaUtil::LuaView& view) { addPackage("openmw.self", sol::make_object(view.sol(), &mData)); }); + registerEngineHandlers({ &mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers, &mOnActivatedHandlers, + &mOnTeleportedHandlers, &mOnAnimationTextKeyHandlers, &mOnPlayAnimationHandlers, &mOnSkillUse, + &mOnSkillLevelUp }); + } + + void LocalScripts::setActive(bool active) + { + mData.mIsActive = active; + if (active) + callEngineHandlers(mOnActiveHandlers); + else + callEngineHandlers(mOnInactiveHandlers); + } + + void LocalScripts::applyStatsCache() + { + const auto& ptr = mData.ptr(); + for (auto& [stat, value] : mData.mStatsCache) + stat(ptr, value); + mData.mStatsCache.clear(); + } +} diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp new file mode 100644 index 00000000000..2ec78860d19 --- /dev/null +++ b/apps/openmw/mwlua/localscripts.hpp @@ -0,0 +1,110 @@ +#ifndef MWLUA_LOCALSCRIPTS_H +#define MWLUA_LOCALSCRIPTS_H + +#include +#include +#include +#include + +#include +#include + +#include "../mwbase/luamanager.hpp" + +#include "object.hpp" + +namespace MWLua +{ + struct Context; + + struct SelfObject : public LObject + { + class CachedStat + { + public: + using Index = std::variant; + using Setter = void (*)(const Index&, std::string_view, const MWWorld::Ptr&, const sol::object&); + + CachedStat(Setter setter, Index index, std::string_view prop) + : mSetter(setter) + , mIndex(std::move(index)) + , mProp(std::move(prop)) + { + } + + void operator()(const MWWorld::Ptr& ptr, const sol::object& object) const + { + mSetter(mIndex, mProp, ptr, object); + } + + bool operator<(const CachedStat& other) const + { + return std::tie(mSetter, mIndex, mProp) < std::tie(other.mSetter, other.mIndex, other.mProp); + } + + private: + Setter mSetter; // Function that updates a stat's property + Index mIndex; // Optional index to disambiguate the stat + std::string_view mProp; // Name of the stat's property + }; + + SelfObject(const LObject& obj) + : LObject(obj) + , mIsActive(false) + { + } + MWBase::LuaManager::ActorControls mControls; + std::map mStatsCache; + bool mIsActive; + }; + + class LocalScripts : public LuaUtil::ScriptsContainer + { + public: + static void initializeSelfPackage(const Context&); + LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); + + MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } + const MWWorld::Ptr& getPtrOrEmpty() const { return mData.ptrOrEmpty(); } + + void setActive(bool active); + void onConsume(const LObject& consumable) { callEngineHandlers(mOnConsumeHandlers, consumable); } + void onActivated(const LObject& actor) { callEngineHandlers(mOnActivatedHandlers, actor); } + void onTeleported() { callEngineHandlers(mOnTeleportedHandlers); } + void onAnimationTextKey(std::string_view groupname, std::string_view key) + { + callEngineHandlers(mOnAnimationTextKeyHandlers, groupname, key); + } + void onPlayAnimation(std::string_view groupname, const sol::table& options) + { + callEngineHandlers(mOnPlayAnimationHandlers, groupname, options); + } + void onSkillUse(std::string_view skillId, int useType, float scale) + { + callEngineHandlers(mOnSkillUse, skillId, useType, scale); + } + void onSkillLevelUp(std::string_view skillId, std::string_view source) + { + callEngineHandlers(mOnSkillLevelUp, skillId, source); + } + + void applyStatsCache(); + + protected: + SelfObject mData; + + private: + EngineHandlerList mOnActiveHandlers{ "onActive" }; + EngineHandlerList mOnInactiveHandlers{ "onInactive" }; + EngineHandlerList mOnConsumeHandlers{ "onConsume" }; + EngineHandlerList mOnActivatedHandlers{ "onActivated" }; + EngineHandlerList mOnTeleportedHandlers{ "onTeleported" }; + EngineHandlerList mOnAnimationTextKeyHandlers{ "_onAnimationTextKey" }; + EngineHandlerList mOnPlayAnimationHandlers{ "_onPlayAnimation" }; + EngineHandlerList mOnSkillUse{ "_onSkillUse" }; + EngineHandlerList mOnSkillLevelUp{ "_onSkillLevelUp" }; + }; + +} + +#endif // MWLUA_LOCALSCRIPTS_H diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp new file mode 100644 index 00000000000..8debbe153df --- /dev/null +++ b/apps/openmw/mwlua/luabindings.cpp @@ -0,0 +1,90 @@ +#include "luabindings.hpp" + +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/datetimemanager.hpp" + +#include "animationbindings.hpp" +#include "camerabindings.hpp" +#include "cellbindings.hpp" +#include "corebindings.hpp" +#include "debugbindings.hpp" +#include "inputbindings.hpp" +#include "localscripts.hpp" +#include "markupbindings.hpp" +#include "menuscripts.hpp" +#include "nearbybindings.hpp" +#include "objectbindings.hpp" +#include "postprocessingbindings.hpp" +#include "soundbindings.hpp" +#include "types/types.hpp" +#include "uibindings.hpp" +#include "vfsbindings.hpp" +#include "worldbindings.hpp" + +namespace MWLua +{ + std::map initCommonPackages(const Context& context) + { + sol::state_view lua = context.mLua->unsafeState(); + MWWorld::DateTimeManager* tm = MWBase::Environment::get().getWorld()->getTimeManager(); + return { + { "openmw.animation", initAnimationPackage(context) }, + { "openmw.async", + LuaUtil::getAsyncPackageInitializer( + lua, [tm] { return tm->getSimulationTime(); }, [tm] { return tm->getGameTime(); }) }, + { "openmw.markup", initMarkupPackage(context) }, + { "openmw.util", LuaUtil::initUtilPackage(lua) }, + { "openmw.vfs", initVFSPackage(context) }, + }; + } + + std::map initGlobalPackages(const Context& context) + { + initObjectBindingsForGlobalScripts(context); + initCellBindingsForGlobalScripts(context); + return { + { "openmw.core", initCorePackage(context) }, + { "openmw.types", initTypesPackage(context) }, + { "openmw.world", initWorldPackage(context) }, + }; + } + + std::map initLocalPackages(const Context& context) + { + initObjectBindingsForLocalScripts(context); + initCellBindingsForLocalScripts(context); + LocalScripts::initializeSelfPackage(context); + return { + { "openmw.core", initCorePackage(context) }, + { "openmw.types", initTypesPackage(context) }, + { "openmw.nearby", initNearbyPackage(context) }, + }; + } + + std::map initPlayerPackages(const Context& context) + { + return { + { "openmw.ambient", initAmbientPackage(context) }, + { "openmw.camera", initCameraPackage(context.sol()) }, + { "openmw.debug", initDebugPackage(context) }, + { "openmw.input", initInputPackage(context) }, + { "openmw.postprocessing", initPostprocessingPackage(context) }, + { "openmw.ui", initUserInterfacePackage(context) }, + }; + } + + std::map initMenuPackages(const Context& context) + { + return { + { "openmw.core", initCorePackage(context) }, + { "openmw.ambient", initAmbientPackage(context) }, + { "openmw.ui", initUserInterfacePackage(context) }, + { "openmw.menu", initMenuPackage(context) }, + { "openmw.input", initInputPackage(context) }, + }; + } +} diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp new file mode 100644 index 00000000000..749987e5b26 --- /dev/null +++ b/apps/openmw/mwlua/luabindings.hpp @@ -0,0 +1,29 @@ +#ifndef MWLUA_LUABINDINGS_H +#define MWLUA_LUABINDINGS_H + +#include +#include +#include + +#include "context.hpp" + +namespace MWLua +{ + // Initialize Lua packages that are available for all scripts. + std::map initCommonPackages(const Context&); + + // Initialize Lua packages that are available for global scripts (additionally to common packages). + std::map initGlobalPackages(const Context&); + + // Initialize Lua packages that are available for local scripts (additionally to common packages). + std::map initLocalPackages(const Context&); + + // Initialize Lua packages that are available only for local scripts on the player (additionally to common and local + // packages). + std::map initPlayerPackages(const Context&); + + // Initialize Lua packages that are available only for menu scripts (additionally to common packages). + std::map initMenuPackages(const Context&); +} + +#endif // MWLUA_LUABINDINGS_H diff --git a/apps/openmw/mwlua/luaevents.cpp b/apps/openmw/mwlua/luaevents.cpp new file mode 100644 index 00000000000..4ffb4fc1ccb --- /dev/null +++ b/apps/openmw/mwlua/luaevents.cpp @@ -0,0 +1,116 @@ +#include "luaevents.hpp" + +#include + +#include +#include +#include + +#include + +#include "../mwbase/environment.hpp" +#include "../mwworld/worldmodel.hpp" + +#include "globalscripts.hpp" +#include "localscripts.hpp" +#include "menuscripts.hpp" + +namespace MWLua +{ + + void LuaEvents::clear() + { + mGlobalEventBatch.clear(); + mLocalEventBatch.clear(); + mNewGlobalEventBatch.clear(); + mNewLocalEventBatch.clear(); + mMenuEvents.clear(); + } + + void LuaEvents::finalizeEventBatch() + { + mNewGlobalEventBatch.swap(mGlobalEventBatch); + mNewLocalEventBatch.swap(mLocalEventBatch); + mNewGlobalEventBatch.clear(); + mNewLocalEventBatch.clear(); + } + + void LuaEvents::callEventHandlers() + { + for (const Global& e : mGlobalEventBatch) + mGlobalScripts.receiveEvent(e.mEventName, e.mEventData); + mGlobalEventBatch.clear(); + for (const Local& e : mLocalEventBatch) + { + MWWorld::Ptr ptr = MWBase::Environment::get().getWorldModel()->getPtr(e.mDest); + LocalScripts* scripts = ptr.isEmpty() ? nullptr : ptr.getRefData().getLuaScripts(); + if (scripts) + scripts->receiveEvent(e.mEventName, e.mEventData); + else + Log(Debug::Debug) << "Ignored event " << e.mEventName << " to L" << e.mDest.toString() + << ". Object not found or has no attached scripts"; + } + mLocalEventBatch.clear(); + } + + void LuaEvents::callMenuEventHandlers() + { + for (const Global& e : mMenuEvents) + mMenuScripts.receiveEvent(e.mEventName, e.mEventData); + mMenuEvents.clear(); + } + + template + static void saveEvent(ESM::ESMWriter& esm, ESM::RefNum dest, const Event& event) + { + esm.writeHNString("LUAE", event.mEventName); + esm.writeFormId(dest, true); + if (!event.mEventData.empty()) + saveLuaBinaryData(esm, event.mEventData); + } + + void LuaEvents::load(lua_State* lua, ESM::ESMReader& esm, const std::map& contentFileMapping, + const LuaUtil::UserdataSerializer* serializer) + { + clear(); + while (esm.isNextSub("LUAE")) + { + std::string name = esm.getHString(); + ESM::RefNum dest = esm.getFormId(true); + std::string data = loadLuaBinaryData(esm); + try + { + data = LuaUtil::serialize(LuaUtil::deserialize(lua, data, serializer), serializer); + } + catch (std::exception& e) + { + Log(Debug::Error) << "loadEvent: invalid event data: " << e.what(); + } + if (dest.isSet()) + { + auto it = contentFileMapping.find(dest.mContentFile); + if (it != contentFileMapping.end()) + dest.mContentFile = it->second; + mLocalEventBatch.push_back({ dest, std::move(name), std::move(data) }); + } + else + mGlobalEventBatch.push_back({ std::move(name), std::move(data) }); + } + } + + void LuaEvents::save(ESM::ESMWriter& esm) const + { + // Used as a marker of a global event. + constexpr ESM::RefNum globalId; + + for (const Global& e : mGlobalEventBatch) + saveEvent(esm, globalId, e); + for (const Global& e : mNewGlobalEventBatch) + saveEvent(esm, globalId, e); + for (const Local& e : mLocalEventBatch) + saveEvent(esm, e.mDest, e); + for (const Local& e : mNewLocalEventBatch) + saveEvent(esm, e.mDest, e); + } + +} diff --git a/apps/openmw/mwlua/luaevents.hpp b/apps/openmw/mwlua/luaevents.hpp new file mode 100644 index 00000000000..3890b45b6d8 --- /dev/null +++ b/apps/openmw/mwlua/luaevents.hpp @@ -0,0 +1,74 @@ +#ifndef MWLUA_LUAEVENTS_H +#define MWLUA_LUAEVENTS_H + +#include +#include + +#include // defines RefNum that is used as a unique id + +struct lua_State; + +namespace ESM +{ + class ESMReader; + class ESMWriter; +} + +namespace LuaUtil +{ + class UserdataSerializer; +} + +namespace MWLua +{ + + class GlobalScripts; + class MenuScripts; + + class LuaEvents + { + public: + explicit LuaEvents(GlobalScripts& globalScripts, MenuScripts& menuScripts) + : mGlobalScripts(globalScripts) + , mMenuScripts(menuScripts) + { + } + + struct Global + { + std::string mEventName; + std::string mEventData; + }; + struct Local + { + ESM::RefNum mDest; + std::string mEventName; + std::string mEventData; + }; + + void addGlobalEvent(Global event) { mNewGlobalEventBatch.push_back(std::move(event)); } + void addMenuEvent(Global event) { mMenuEvents.push_back(std::move(event)); } + void addLocalEvent(Local event) { mNewLocalEventBatch.push_back(std::move(event)); } + + void clear(); + void finalizeEventBatch(); + void callEventHandlers(); + void callMenuEventHandlers(); + + void load(lua_State* lua, ESM::ESMReader& esm, const std::map& contentFileMapping, + const LuaUtil::UserdataSerializer* serializer); + void save(ESM::ESMWriter& esm) const; + + private: + GlobalScripts& mGlobalScripts; + MenuScripts& mMenuScripts; + std::vector mNewGlobalEventBatch; + std::vector mNewLocalEventBatch; + std::vector mGlobalEventBatch; + std::vector mLocalEventBatch; + std::vector mMenuEvents; + }; + +} + +#endif // MWLUA_LUAEVENTS_H diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp new file mode 100644 index 00000000000..cf55ac63e6c --- /dev/null +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -0,0 +1,879 @@ +#include "luamanagerimp.hpp" + +#include + +#include +#include + +#include "sol/state_view.hpp" + +#include + +#include +#include +#include + +#include + +#include + +#include +#include +#include + +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwrender/bonegroup.hpp" +#include "../mwrender/postprocessor.hpp" + +#include "../mwworld/datetimemanager.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/player.hpp" +#include "../mwworld/ptr.hpp" +#include "../mwworld/scene.hpp" +#include "../mwworld/worldmodel.hpp" + +#include "luabindings.hpp" +#include "playerscripts.hpp" +#include "types/types.hpp" +#include "userdataserializer.hpp" + +namespace MWLua +{ + + static LuaUtil::LuaStateSettings createLuaStateSettings() + { + if (!Settings::lua().mLuaProfiler) + LuaUtil::LuaState::disableProfiler(); + return { .mInstructionLimit = Settings::lua().mInstructionLimitPerCall, + .mMemoryLimit = Settings::lua().mMemoryLimit, + .mSmallAllocMaxSize = Settings::lua().mSmallAllocMaxSize, + .mLogMemoryUsage = Settings::lua().mLogMemoryUsage }; + } + + LuaManager::LuaManager(const VFS::Manager* vfs, const std::filesystem::path& libsDir) + : mLua(vfs, &mConfiguration, createLuaStateSettings()) + { + Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); + mLua.addInternalLibSearchPath(libsDir); + + mGlobalSerializer = createUserdataSerializer(false); + mLocalSerializer = createUserdataSerializer(true); + mGlobalLoader = createUserdataSerializer(false, &mContentFileMapping); + mLocalLoader = createUserdataSerializer(true, &mContentFileMapping); + + mGlobalScripts.setSerializer(mGlobalSerializer.get()); + } + + LuaManager::~LuaManager() + { + LuaUi::clearSettings(); + } + + void LuaManager::initConfiguration() + { + mConfiguration.init(MWBase::Environment::get().getESMStore()->getLuaScriptsCfg()); + Log(Debug::Verbose) << "Lua scripts configuration (" << mConfiguration.size() << " scripts):"; + for (size_t i = 0; i < mConfiguration.size(); ++i) + Log(Debug::Verbose) << "#" << i << " " << LuaUtil::scriptCfgToString(mConfiguration[i]); + mMenuScripts.setAutoStartConf(mConfiguration.getMenuConf()); + mGlobalScripts.setAutoStartConf(mConfiguration.getGlobalConf()); + } + + void LuaManager::init() + { + mLua.protectedCall([&](LuaUtil::LuaView& view) { + Context globalContext; + globalContext.mType = Context::Global; + globalContext.mLuaManager = this; + globalContext.mLua = &mLua; + globalContext.mObjectLists = &mObjectLists; + globalContext.mLuaEvents = &mLuaEvents; + globalContext.mSerializer = mGlobalSerializer.get(); + + Context localContext = globalContext; + localContext.mType = Context::Local; + localContext.mSerializer = mLocalSerializer.get(); + + Context menuContext = globalContext; + menuContext.mType = Context::Menu; + + for (const auto& [name, package] : initCommonPackages(globalContext)) + mLua.addCommonPackage(name, package); + for (const auto& [name, package] : initGlobalPackages(globalContext)) + mGlobalScripts.addPackage(name, package); + for (const auto& [name, package] : initMenuPackages(menuContext)) + mMenuScripts.addPackage(name, package); + + mLocalPackages = initLocalPackages(localContext); + + mPlayerPackages = initPlayerPackages(localContext); + mPlayerPackages.insert(mLocalPackages.begin(), mLocalPackages.end()); + + LuaUtil::LuaStorage::initLuaBindings(view); + mGlobalScripts.addPackage("openmw.storage", LuaUtil::LuaStorage::initGlobalPackage(view, &mGlobalStorage)); + mMenuScripts.addPackage( + "openmw.storage", LuaUtil::LuaStorage::initMenuPackage(view, &mGlobalStorage, &mPlayerStorage)); + mLocalPackages["openmw.storage"] = LuaUtil::LuaStorage::initLocalPackage(view, &mGlobalStorage); + mPlayerPackages["openmw.storage"] + = LuaUtil::LuaStorage::initPlayerPackage(view, &mGlobalStorage, &mPlayerStorage); + + mPlayerStorage.setActive(true); + mGlobalStorage.setActive(false); + + initConfiguration(); + mInitialized = true; + mMenuScripts.addAutoStartedScripts(); + }); + } + + void LuaManager::loadPermanentStorage(const std::filesystem::path& userConfigPath) + { + mPlayerStorage.setActive(true); + mGlobalStorage.setActive(true); + const auto globalPath = userConfigPath / "global_storage.bin"; + const auto playerPath = userConfigPath / "player_storage.bin"; + + mLua.protectedCall([&](LuaUtil::LuaView& view) { + if (std::filesystem::exists(globalPath)) + mGlobalStorage.load(view.sol(), globalPath); + if (std::filesystem::exists(playerPath)) + mPlayerStorage.load(view.sol(), playerPath); + }); + } + + void LuaManager::savePermanentStorage(const std::filesystem::path& userConfigPath) + { + mLua.protectedCall([&](LuaUtil::LuaView& view) { + if (mGlobalScriptsStarted) + mGlobalStorage.save(view.sol(), userConfigPath / "global_storage.bin"); + mPlayerStorage.save(view.sol(), userConfigPath / "player_storage.bin"); + }); + } + + void LuaManager::update() + { + if (const int steps = Settings::lua().mGcStepsPerFrame; steps > 0) + lua_gc(mLua.unsafeState(), LUA_GCSTEP, steps); + + if (mPlayer.isEmpty()) + return; // The game is not started yet. + + MWWorld::Ptr newPlayerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (!(getId(mPlayer) == getId(newPlayerPtr))) + throw std::logic_error("Player RefNum was changed unexpectedly"); + if (!mPlayer.isInCell() || !newPlayerPtr.isInCell() || mPlayer.getCell() != newPlayerPtr.getCell()) + { + mPlayer = newPlayerPtr; // player was moved to another cell, update ptr in registry + MWBase::Environment::get().getWorldModel()->registerPtr(mPlayer); + } + + mObjectLists.update(); + + for (auto scripts : mQueuedAutoStartedScripts) + scripts->addAutoStartedScripts(); + mQueuedAutoStartedScripts.clear(); + + std::erase_if(mActiveLocalScripts, + [](const LocalScripts* l) { return l->getPtrOrEmpty().isEmpty() || l->getPtrOrEmpty().mRef->isDeleted(); }); + + mGlobalScripts.statsNextFrame(); + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->statsNextFrame(); + + mLuaEvents.finalizeEventBatch(); + + MWWorld::DateTimeManager& timeManager = *MWBase::Environment::get().getWorld()->getTimeManager(); + if (!timeManager.isPaused()) + { + mMenuScripts.processTimers(timeManager.getSimulationTime(), timeManager.getGameTime()); + mGlobalScripts.processTimers(timeManager.getSimulationTime(), timeManager.getGameTime()); + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->processTimers(timeManager.getSimulationTime(), timeManager.getGameTime()); + } + + // Run event handlers for events that were sent before `finalizeEventBatch`. + mLuaEvents.callEventHandlers(); + + // Run queued callbacks + for (CallbackWithData& c : mQueuedCallbacks) + c.mCallback.tryCall(c.mArg); + mQueuedCallbacks.clear(); + + // Run engine handlers + mEngineEvents.callEngineHandlers(); + if (!timeManager.isPaused()) + { + float frameDuration = MWBase::Environment::get().getFrameDuration(); + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->update(frameDuration); + mGlobalScripts.update(frameDuration); + } + } + + void LuaManager::objectTeleported(const MWWorld::Ptr& ptr) + { + if (ptr == mPlayer) + { + // For player run the onTeleported handler immediately, + // so it can adjust camera position after teleporting. + PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + if (playerScripts) + playerScripts->onTeleported(); + } + else + mEngineEvents.addToQueue(EngineEvents::OnTeleported{ getId(ptr) }); + } + + void LuaManager::questUpdated(const ESM::RefId& questId, int stage) + { + if (mPlayer.isEmpty()) + return; // The game is not started yet. + PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + if (playerScripts) + { + playerScripts->onQuestUpdate(questId.serializeText(), stage); + } + } + + void LuaManager::synchronizedUpdate() + { + if (mNewGameStarted) + { + mNewGameStarted = false; + // Run onNewGame handler in synchronizedUpdate (at the beginning of the frame), so it + // can teleport the player to the starting location before the first frame is rendered. + mGlobalScripts.newGameStarted(); + } + + // We apply input events in `synchronizedUpdate` rather than in `update` in order to reduce input latency. + mProcessingInputEvents = true; + PlayerScripts* playerScripts + = mPlayer.isEmpty() ? nullptr : dynamic_cast(mPlayer.getRefData().getLuaScripts()); + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + + for (const auto& event : mMenuInputEvents) + mMenuScripts.processInputEvent(event); + mMenuInputEvents.clear(); + if (playerScripts && !windowManager->containsMode(MWGui::GM_MainMenu)) + { + for (const auto& event : mInputEvents) + playerScripts->processInputEvent(event); + } + mInputEvents.clear(); + mLuaEvents.callMenuEventHandlers(); + double frameDuration = MWBase::Environment::get().getWorld()->getTimeManager()->isPaused() + ? 0.0 + : MWBase::Environment::get().getFrameDuration(); + mInputActions.update(frameDuration); + mMenuScripts.onFrame(frameDuration); + if (playerScripts) + playerScripts->onFrame(frameDuration); + mProcessingInputEvents = false; + + for (const auto& [message, mode] : mUIMessages) + windowManager->messageBox(message, mode); + mUIMessages.clear(); + for (auto& [msg, color] : mInGameConsoleMessages) + windowManager->printToConsole(msg, "#" + color.toHex()); + mInGameConsoleMessages.clear(); + + applyDelayedActions(); + + if (mReloadAllScriptsRequested) + { + // Reloading right after `applyDelayedActions` to guarantee that no delayed actions are currently queued. + reloadAllScriptsImpl(); + mReloadAllScriptsRequested = false; + } + + if (mDelayedUiModeChangedArg) + { + if (playerScripts) + playerScripts->uiModeChanged(*mDelayedUiModeChangedArg, true); + mDelayedUiModeChangedArg = std::nullopt; + } + } + + void LuaManager::applyDelayedActions() + { + mApplyingDelayedActions = true; + for (DelayedAction& action : mActionQueue) + action.apply(); + mActionQueue.clear(); + + if (mTeleportPlayerAction) + mTeleportPlayerAction->apply(); + mTeleportPlayerAction.reset(); + mApplyingDelayedActions = false; + } + + void LuaManager::clear() + { + LuaUi::clearGameInterface(); + mUiResourceManager.clear(); + MWBase::Environment::get().getWorld()->getPostProcessor()->disableDynamicShaders(); + mActiveLocalScripts.clear(); + mLuaEvents.clear(); + mEngineEvents.clear(); + mInputEvents.clear(); + mMenuInputEvents.clear(); + mObjectLists.clear(); + mGlobalScripts.removeAllScripts(); + mGlobalScriptsStarted = false; + mNewGameStarted = false; + mDelayedUiModeChangedArg = std::nullopt; + if (!mPlayer.isEmpty()) + { + mPlayer.getCellRef().unsetRefNum(); + mPlayer.getRefData().setLuaScripts(nullptr); + mPlayer = MWWorld::Ptr(); + } + mGlobalStorage.setActive(true); + mGlobalStorage.clearTemporaryAndRemoveCallbacks(); + mGlobalStorage.setActive(false); + mPlayerStorage.clearTemporaryAndRemoveCallbacks(); + mInputActions.clear(); + mInputTriggers.clear(); + for (int i = 0; i < 5; ++i) + lua_gc(mLua.unsafeState(), LUA_GCCOLLECT, 0); + } + + void LuaManager::setupPlayer(const MWWorld::Ptr& ptr) + { + if (!mInitialized) + return; + if (!mPlayer.isEmpty()) + throw std::logic_error("Player is initialized twice"); + mObjectLists.objectAddedToScene(ptr); + mObjectLists.setPlayer(ptr); + mPlayer = ptr; + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + { + localScripts = createLocalScripts(ptr); + mQueuedAutoStartedScripts.push_back(localScripts); + } + mActiveLocalScripts.insert(localScripts); + mEngineEvents.addToQueue(EngineEvents::OnActive{ getId(ptr) }); + } + + void LuaManager::newGameStarted() + { + mGlobalStorage.setActive(true); + mInputEvents.clear(); + mGlobalScripts.addAutoStartedScripts(); + mGlobalScriptsStarted = true; + mNewGameStarted = true; + mMenuScripts.stateChanged(); + } + + void LuaManager::gameLoaded() + { + mGlobalStorage.setActive(true); + if (!mGlobalScriptsStarted) + mGlobalScripts.addAutoStartedScripts(); + mGlobalScriptsStarted = true; + mMenuScripts.stateChanged(); + } + + void LuaManager::gameEnded() + { + // TODO: disable scripts and global storage when the game is actually unloaded + // mGlobalStorage.setActive(false); + mMenuScripts.stateChanged(); + } + + void LuaManager::noGame() + { + clear(); + mMenuScripts.stateChanged(); + } + + void LuaManager::uiModeChanged(const MWWorld::Ptr& arg) + { + if (mPlayer.isEmpty()) + return; + ObjectId argId = arg.isEmpty() ? ObjectId() : getId(arg); + if (mApplyingDelayedActions) + { + mDelayedUiModeChangedArg = argId; + return; + } + PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + if (playerScripts) + playerScripts->uiModeChanged(argId, false); + } + + void LuaManager::actorDied(const MWWorld::Ptr& actor) + { + if (actor.isEmpty()) + return; + mLuaEvents.addLocalEvent({ getId(actor), "Died", {} }); + } + + void LuaManager::useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) + { + MWBase::Environment::get().getWorldModel()->registerPtr(object); + mEngineEvents.addToQueue(EngineEvents::OnUseItem{ getId(actor), getId(object), force }); + } + + void LuaManager::animationTextKey(const MWWorld::Ptr& actor, const std::string& key) + { + auto pos = key.find(": "); + if (pos != std::string::npos) + mEngineEvents.addToQueue( + EngineEvents::OnAnimationTextKey{ getId(actor), key.substr(0, pos), key.substr(pos + 2) }); + } + + void LuaManager::playAnimation(const MWWorld::Ptr& actor, 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) + { + mLua.protectedCall([&](LuaUtil::LuaView& view) { + sol::table options = view.newTable(); + options["blendMask"] = blendMask; + options["autoDisable"] = autodisable; + options["speed"] = speedmult; + options["startKey"] = start; + options["stopKey"] = stop; + options["startPoint"] = startpoint; + options["loops"] = loops; + options["forceLoop"] = loopfallback; + + bool priorityAsTable = false; + for (uint32_t i = 1; i < MWRender::sNumBlendMasks; i++) + if (priority[static_cast(i)] != priority[static_cast(0)]) + priorityAsTable = true; + if (priorityAsTable) + { + sol::table priorityTable = view.newTable(); + for (uint32_t i = 0; i < MWRender::sNumBlendMasks; i++) + priorityTable[static_cast(i)] = priority[static_cast(i)]; + options["priority"] = priorityTable; + } + else + options["priority"] = priority[MWRender::BoneGroup_LowerBody]; + + // mEngineEvents.addToQueue(event); + // Has to be called immediately, otherwise engine details that depend on animations playing immediately + // break. + if (auto* scripts = actor.getRefData().getLuaScripts()) + scripts->onPlayAnimation(groupname, options); + }); + } + + void LuaManager::skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) + { + mEngineEvents.addToQueue(EngineEvents::OnSkillUse{ getId(actor), skillId.serializeText(), useType, scale }); + } + + void LuaManager::skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) + { + mEngineEvents.addToQueue( + EngineEvents::OnSkillLevelUp{ getId(actor), skillId.serializeText(), std::string(source) }); + } + + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) + { + mObjectLists.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. + mEngineEvents.addToQueue(EngineEvents::OnActive{ getId(ptr) }); + + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + { + LuaUtil::ScriptIdsWithInitializationData autoStartConf + = mConfiguration.getLocalConf(getLiveCellRefType(ptr.mRef), ptr.getCellRef().getRefId(), getId(ptr)); + if (!autoStartConf.empty()) + { + localScripts = createLocalScripts(ptr, std::move(autoStartConf)); + mQueuedAutoStartedScripts.push_back(localScripts); + } + } + if (localScripts) + mActiveLocalScripts.insert(localScripts); + } + + void LuaManager::objectRemovedFromScene(const MWWorld::Ptr& ptr) + { + mObjectLists.objectRemovedFromScene(ptr); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (localScripts) + { + mActiveLocalScripts.erase(localScripts); + if (!MWBase::Environment::get().getWorldModel()->getPtr(getId(ptr)).isEmpty()) + mEngineEvents.addToQueue(EngineEvents::OnInactive{ getId(ptr) }); + } + } + + void LuaManager::inputEvent(const InputEvent& event) + { + if (!MyGUI::InputManager::getInstance().isModalAny() + && !MWBase::Environment::get().getWindowManager()->isConsoleMode()) + { + mInputEvents.push_back(event); + } + mMenuInputEvents.push_back(event); + } + + MWBase::LuaManager::ActorControls* LuaManager::getActorControls(const MWWorld::Ptr& ptr) const + { + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + return nullptr; + return localScripts->getActorControls(); + } + + void LuaManager::addCustomLocalScript(const MWWorld::Ptr& ptr, int scriptId, std::string_view initData) + { + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + { + localScripts = createLocalScripts(ptr); + localScripts->addAutoStartedScripts(); + if (ptr.isInCell() && MWBase::Environment::get().getWorldScene()->isCellActive(*ptr.getCell())) + mActiveLocalScripts.insert(localScripts); + } + localScripts->addCustomScript(scriptId, initData); + } + + LocalScripts* LuaManager::createLocalScripts( + const MWWorld::Ptr& ptr, std::optional autoStartConf) + { + assert(mInitialized); + std::shared_ptr scripts; + const uint32_t type = getLiveCellRefType(ptr.mRef); + if (type == ESM::REC_STAT) + throw std::runtime_error("Lua scripts on static objects are not allowed"); + else if (type == ESM::REC_INTERNAL_PLAYER) + { + scripts = std::make_shared(&mLua, LObject(getId(ptr))); + scripts->setAutoStartConf(mConfiguration.getPlayerConf()); + for (const auto& [name, package] : mPlayerPackages) + scripts->addPackage(name, package); + } + else + { + scripts = std::make_shared(&mLua, LObject(getId(ptr))); + if (!autoStartConf.has_value()) + autoStartConf = mConfiguration.getLocalConf(type, ptr.getCellRef().getRefId(), getId(ptr)); + scripts->setAutoStartConf(std::move(*autoStartConf)); + for (const auto& [name, package] : mLocalPackages) + scripts->addPackage(name, package); + } + scripts->setSerializer(mLocalSerializer.get()); + + MWWorld::RefData& refData = ptr.getRefData(); + refData.setLuaScripts(std::move(scripts)); + return refData.getLuaScripts(); + } + + void LuaManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) + { + writer.startRecord(ESM::REC_LUAM); + + writer.writeHNT("LUAW", MWBase::Environment::get().getWorld()->getTimeManager()->getSimulationTime()); + writer.writeFormId(MWBase::Environment::get().getWorldModel()->getLastGeneratedRefNum(), true); + ESM::LuaScripts globalScripts; + mGlobalScripts.save(globalScripts); + globalScripts.save(writer); + mLuaEvents.save(writer); + + writer.endRecord(ESM::REC_LUAM); + } + + void LuaManager::readRecord(ESM::ESMReader& reader, uint32_t type) + { + if (type != ESM::REC_LUAM) + throw std::runtime_error("ESM::REC_LUAM is expected"); + + double simulationTime; + reader.getHNT(simulationTime, "LUAW"); + MWBase::Environment::get().getWorld()->getTimeManager()->setSimulationTime(simulationTime); + ESM::FormId lastGenerated = reader.getFormId(true); + if (lastGenerated.hasContentFile()) + throw std::runtime_error("Last generated RefNum is invalid"); + MWBase::Environment::get().getWorldModel()->setLastGeneratedRefNum(lastGenerated); + + // TODO: don't execute scripts right away, it will be necessary in multiplayer where global storage requires + // initialization. For now just set global storage as active slightly before it would be set by gameLoaded() + mGlobalStorage.setActive(true); + + ESM::LuaScripts globalScripts; + globalScripts.load(reader); + mLua.protectedCall([&](LuaUtil::LuaView& view) { + mLuaEvents.load(view.sol(), reader, mContentFileMapping, mGlobalLoader.get()); + }); + + mGlobalScripts.setSavedDataDeserializer(mGlobalLoader.get()); + mGlobalScripts.load(globalScripts); + mGlobalScriptsStarted = true; + } + + void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) + { + if (ptr.getRefData().getLuaScripts()) + ptr.getRefData().getLuaScripts()->save(data); + else + data.mScripts.clear(); + } + + void LuaManager::loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) + { + if (data.mScripts.empty()) + { + if (ptr.getRefData().getLuaScripts()) + ptr.getRefData().setLuaScripts(nullptr); + return; + } + + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + LocalScripts* scripts = createLocalScripts(ptr); + + scripts->setSerializer(mLocalSerializer.get()); + scripts->setSavedDataDeserializer(mLocalLoader.get()); + scripts->load(data); + } + + void LuaManager::reloadAllScriptsImpl() + { + Log(Debug::Info) << "Reload Lua"; + + LuaUi::clearGameInterface(); + LuaUi::clearMenuInterface(); + LuaUi::clearSettings(); + MWBase::Environment::get().getWindowManager()->setConsoleMode(""); + MWBase::Environment::get().getL10nManager()->dropCache(); + mUiResourceManager.clear(); + mLua.dropScriptCache(); + mInputActions.clear(); + mInputTriggers.clear(); + initConfiguration(); + + ESM::LuaScripts globalData; + + if (mGlobalScriptsStarted) + { + mGlobalScripts.setSavedDataDeserializer(mGlobalSerializer.get()); + mGlobalScripts.save(globalData); + mGlobalStorage.clearTemporaryAndRemoveCallbacks(); + } + + std::unordered_map localData; + + for (const auto& [id, ptr] : MWBase::Environment::get().getWorldModel()->getPtrRegistryView()) + { + LocalScripts* scripts = ptr.getRefData().getLuaScripts(); + if (scripts == nullptr) + continue; + scripts->setSavedDataDeserializer(mLocalSerializer.get()); + ESM::LuaScripts data; + scripts->save(data); + localData[id] = std::move(data); + } + + mMenuScripts.removeAllScripts(); + + mPlayerStorage.clearTemporaryAndRemoveCallbacks(); + + mMenuScripts.addAutoStartedScripts(); + + for (const auto& [id, ptr] : MWBase::Environment::get().getWorldModel()->getPtrRegistryView()) + { + LocalScripts* scripts = ptr.getRefData().getLuaScripts(); + if (scripts == nullptr) + continue; + scripts->load(localData[id]); + } + + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->setActive(true); + + if (mGlobalScriptsStarted) + { + mGlobalScripts.load(globalData); + } + } + + void LuaManager::handleConsoleCommand( + const std::string& consoleMode, const std::string& command, const MWWorld::Ptr& selectedPtr) + { + PlayerScripts* playerScripts = nullptr; + if (!mPlayer.isEmpty()) + playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + bool processed = mMenuScripts.consoleCommand(consoleMode, command); + if (playerScripts) + { + sol::object selected = sol::nil; + if (!selectedPtr.isEmpty()) + mLua.protectedCall([&](LuaUtil::LuaView& view) { + selected = sol::make_object(view.sol(), LObject(getId(selectedPtr))); + }); + if (playerScripts->consoleCommand(consoleMode, command, selected)) + processed = true; + } + if (!processed) + MWBase::Environment::get().getWindowManager()->printToConsole( + "No Lua handlers for console\n", MWBase::WindowManager::sConsoleColor_Error); + } + + LuaManager::DelayedAction::DelayedAction(LuaUtil::LuaState* state, std::function fn, std::string_view name) + : mFn(std::move(fn)) + , mName(name) + { + if (Settings::lua().mLuaDebug) + mCallerTraceback = state->debugTraceback(); + } + + void LuaManager::DelayedAction::apply() const + { + try + { + mFn(); + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Error in DelayedAction " << mName << ": " << e.what(); + + if (mCallerTraceback.empty()) + Log(Debug::Error) << "Set 'lua debug=true' in settings.cfg to enable action tracebacks"; + else + Log(Debug::Error) << "Caller " << mCallerTraceback; + } + } + + void LuaManager::addAction(std::function action, std::string_view name) + { + if (mApplyingDelayedActions) + throw std::runtime_error("DelayedAction is not allowed to create another DelayedAction"); + mActionQueue.emplace_back(&mLua, std::move(action), name); + } + + void LuaManager::addTeleportPlayerAction(std::function action) + { + mTeleportPlayerAction = DelayedAction(&mLua, std::move(action), "TeleportPlayer"); + } + + void LuaManager::reportStats(unsigned int frameNumber, osg::Stats& stats) const + { + stats.setAttribute(frameNumber, "Lua UsedMemory", mLua.getTotalMemoryUsage()); + } + + std::string LuaManager::formatResourceUsageStats() const + { + if (!LuaUtil::LuaState::isProfilerEnabled()) + return "Lua profiler is disabled"; + + std::stringstream out; + + constexpr int nameW = 50; + constexpr int valueW = 12; + + auto outMemSize = [&](int64_t bytes) { + constexpr int64_t limit = 10000; + out << std::right << std::setw(valueW - 3); + if (bytes < limit) + out << bytes << " B "; + else if (bytes < limit * 1024) + out << (bytes / 1024) << " KB"; + else if (bytes < limit * 1024 * 1024) + out << (bytes / (1024 * 1024)) << " MB"; + else + out << (bytes / (1024 * 1024 * 1024)) << " GB"; + }; + + const uint64_t smallAllocSize = Settings::lua().mSmallAllocMaxSize; + out << "Total memory usage:"; + outMemSize(mLua.getTotalMemoryUsage()); + out << "\n"; + out << "LuaUtil::ScriptsContainer count: " << LuaUtil::ScriptsContainer::getInstanceCount() << "\n"; + out << "\n"; + out << "small alloc max size = " << smallAllocSize << " (section [Lua] in settings.cfg)\n"; + out << "Smaller values give more information for the profiler, but increase performance overhead.\n"; + out << " Memory allocations <= " << smallAllocSize << " bytes:"; + outMemSize(mLua.getSmallAllocMemoryUsage()); + out << " (not tracked)\n"; + out << " Memory allocations > " << smallAllocSize << " bytes:"; + outMemSize(mLua.getTotalMemoryUsage() - mLua.getSmallAllocMemoryUsage()); + out << " (see the table below)\n\n"; + + using Stats = LuaUtil::ScriptsContainer::ScriptStats; + + std::vector activeStats; + mGlobalScripts.collectStats(activeStats); + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->collectStats(activeStats); + + std::vector selectedStats; + MWWorld::Ptr selectedPtr = MWBase::Environment::get().getWindowManager()->getConsoleSelectedObject(); + LocalScripts* selectedScripts = nullptr; + if (!selectedPtr.isEmpty()) + { + selectedScripts = selectedPtr.getRefData().getLuaScripts(); + if (selectedScripts) + selectedScripts->collectStats(selectedStats); + out << "Profiled object (selected in the in-game console): " << selectedPtr.toString() << "\n"; + } + else + out << "No selected object. Use the in-game console to select an object for detailed profile.\n"; + out << "\n"; + + out << "Legend\n"; + out << " ops: Averaged number of Lua instruction per frame;\n"; + out << " memory: Aggregated size of Lua allocations > " << smallAllocSize << " bytes;\n"; + out << " [all]: Sum over all instances of each script;\n"; + out << " [active]: Sum over all active (i.e. currently in scene) instances of each script;\n"; + out << " [inactive]: Sum over all inactive instances of each script;\n"; + out << " [for selected object]: Only for the object that is selected in the console;\n"; + out << "\n"; + + out << std::left; + out << " " << std::setw(nameW + 2) << "*** Resource usage per script"; + out << std::right; + out << std::setw(valueW) << "ops"; + out << std::setw(valueW) << "memory"; + out << std::setw(valueW) << "memory"; + out << std::setw(valueW) << "ops"; + out << std::setw(valueW) << "memory"; + out << "\n"; + out << std::left << " " << std::setw(nameW + 2) << "[name]" << std::right; + out << std::setw(valueW) << "[all]"; + out << std::setw(valueW) << "[active]"; + out << std::setw(valueW) << "[inactive]"; + out << std::setw(valueW * 2) << "[for selected object]"; + out << "\n"; + + for (size_t i = 0; i < mConfiguration.size(); ++i) + { + bool isGlobal = mConfiguration[i].mFlags & ESM::LuaScriptCfg::sGlobal; + bool isMenu = mConfiguration[i].mFlags & ESM::LuaScriptCfg::sMenu; + + out << std::left; + out << " " << std::setw(nameW) << mConfiguration[i].mScriptPath.value(); + if (mConfiguration[i].mScriptPath.value().size() > nameW) + out << "\n " << std::setw(nameW) << ""; // if path is too long, break line + out << std::right; + out << std::setw(valueW) << static_cast(activeStats[i].mAvgInstructionCount); + outMemSize(activeStats[i].mMemoryUsage); + outMemSize(mLua.getMemoryUsageByScriptIndex(i) - activeStats[i].mMemoryUsage); + + if (isGlobal) + out << std::setw(valueW * 2) << "NA (global script)"; + else if (isMenu && (!selectedScripts || !selectedScripts->hasScript(i))) + out << std::setw(valueW * 2) << "NA (menu script)"; + else if (selectedPtr.isEmpty()) + out << std::setw(valueW * 2) << "NA (not selected) "; + else if (!selectedScripts || !selectedScripts->hasScript(i)) + out << std::setw(valueW * 2) << "NA"; + else + { + out << std::setw(valueW) << static_cast(selectedStats[i].mAvgInstructionCount); + outMemSize(selectedStats[i].mMemoryUsage); + } + out << "\n"; + } + + return out.str(); + } +} diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp new file mode 100644 index 00000000000..5fa20d377ff --- /dev/null +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -0,0 +1,240 @@ +#ifndef MWLUA_LUAMANAGERIMP_H +#define MWLUA_LUAMANAGERIMP_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../mwbase/luamanager.hpp" +#include "../mwbase/windowmanager.hpp" + +#include "engineevents.hpp" +#include "globalscripts.hpp" +#include "localscripts.hpp" +#include "luaevents.hpp" +#include "menuscripts.hpp" +#include "object.hpp" +#include "objectlists.hpp" + +namespace MWLua +{ + // \brief LuaManager is the central interface through which the engine invokes lua scripts. + // + // This class implements the interface defined in MWBase::LuaManager. + // In addition to the interface, this class exposes lower level interaction between the engine + // and the lua world. + class LuaManager : public MWBase::LuaManager + { + public: + LuaManager(const VFS::Manager* vfs, const std::filesystem::path& libsDir); + LuaManager(const LuaManager&) = delete; + LuaManager(LuaManager&&) = delete; + ~LuaManager(); + + // Called by engine.cpp when the environment is fully initialized. + void init(); + + void loadPermanentStorage(const std::filesystem::path& userConfigPath); + void savePermanentStorage(const std::filesystem::path& userConfigPath); + + // \brief Executes lua handlers. Defaults to running in parallel with OSG Cull. + // + // The OSG Cull is expensive enough that we have "free" time to + // execute Lua by running it in parallel. The Cull also does + // not modify the game state, meaning we can safely read state from Lua + // despite the concurrency. Only modifying the parts of the game state + // that affect the scene graph is forbidden. Such modifications must + // be queued for execution in synchronizedUpdate(). + // The parallelism can be turned off in the settings. + void update(); + + // \brief Executes latency-critical and scene graph related Lua logic. + // + // Called by engine.cpp from the main thread between InputManager and MechanicsManager updates. + // Can use the scene graph and applies the actions queued during update() + void synchronizedUpdate(); + + // Normally it is called by `synchronizedUpdate` every frame. + // Additionally must be called before making a save to ensure that there are no pending delayed + // actions and the world is in a consistent state. + void applyDelayedActions() override; + + // Available everywhere through the MWBase::LuaManager interface. + // LuaManager queues these events and propagates to scripts on the next `update` call. + void newGameStarted() override; + void gameLoaded() override; + void gameEnded() override; + void noGame() override; + void objectAddedToScene(const MWWorld::Ptr& ptr) override; + void objectRemovedFromScene(const MWWorld::Ptr& ptr) override; + void inputEvent(const InputEvent& event) override; + void itemConsumed(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) override + { + mEngineEvents.addToQueue(EngineEvents::OnConsume{ getId(actor), getId(consumable) }); + } + void objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) override + { + mEngineEvents.addToQueue(EngineEvents::OnActivate{ getId(actor), getId(object) }); + } + void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) override; + void animationTextKey(const MWWorld::Ptr& actor, const std::string& key) override; + void playAnimation(const MWWorld::Ptr& actor, 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) override; + void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) override; + void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) override; + void exteriorCreated(MWWorld::CellStore& cell) override + { + mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell }); + } + void objectTeleported(const MWWorld::Ptr& ptr) override; + void questUpdated(const ESM::RefId& questId, int stage) override; + void uiModeChanged(const MWWorld::Ptr& arg) override; + void actorDied(const MWWorld::Ptr& actor) override; + + MWBase::LuaManager::ActorControls* getActorControls(const MWWorld::Ptr&) const override; + + void clear() override; // should be called before loading game or starting a new game to reset internal state. + void setupPlayer(const MWWorld::Ptr& ptr) override; // Should be called once after each "clear". + + // Used only in Lua bindings + void addCustomLocalScript(const MWWorld::Ptr&, int scriptId, std::string_view initData); + void addUIMessage( + std::string_view message, MWGui::ShowInDialogueMode mode = MWGui::ShowInDialogueMode_IfPossible) + { + mUIMessages.emplace_back(message, mode); + } + void addInGameConsoleMessage(const std::string& msg, const Misc::Color& color) + { + mInGameConsoleMessages.push_back({ msg, color }); + } + + // Some changes to the game world can not be done from the scripting thread (because it runs in parallel with + // OSG Cull), so we need to queue it and apply from the main thread. + void addAction(std::function action, std::string_view name = ""); + void addTeleportPlayerAction(std::function action); + + // Saving + void write(ESM::ESMWriter& writer, Loading::Listener& progress) override; + void saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) override; + + // Loading from a save + void readRecord(ESM::ESMReader& reader, uint32_t type) override; + void loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) override; + void setContentFileMapping(const std::map& mapping) override { mContentFileMapping = mapping; } + + // At the end of the next `synchronizedUpdate` drops script cache and reloads all scripts. + // Calls `onSave` and `onLoad` for every script. + void reloadAllScripts() override { mReloadAllScriptsRequested = true; } + + void handleConsoleCommand( + const std::string& consoleMode, const std::string& command, const MWWorld::Ptr& selectedPtr) override; + + // Used to call Lua callbacks from C++ + void queueCallback(LuaUtil::Callback callback, sol::main_object arg) + { + mQueuedCallbacks.push_back({ std::move(callback), std::move(arg) }); + } + + // Wraps Lua callback into an std::function. + // NOTE: Resulted function is not thread safe. Can not be used while LuaManager::update() or + // any other Lua-related function is running. + template + std::function wrapLuaCallback(const LuaUtil::Callback& c) + { + return [this, c](Arg arg) { + this->queueCallback(c, sol::main_object(this->mLua.unsafeState(), sol::in_place, arg)); + }; + } + + LuaUi::ResourceManager* uiResourceManager() { return &mUiResourceManager; } + + bool isProcessingInputEvents() const { return mProcessingInputEvents; } + + void reportStats(unsigned int frameNumber, osg::Stats& stats) const; + std::string formatResourceUsageStats() const override; + + LuaUtil::InputAction::Registry& inputActions() { return mInputActions; } + LuaUtil::InputTrigger::Registry& inputTriggers() { return mInputTriggers; } + + private: + void initConfiguration(); + LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, + std::optional autoStartConf = std::nullopt); + void reloadAllScriptsImpl(); + + bool mInitialized = false; + bool mGlobalScriptsStarted = false; + bool mProcessingInputEvents = false; + bool mApplyingDelayedActions = false; + bool mNewGameStarted = false; + bool mReloadAllScriptsRequested = false; + LuaUtil::ScriptsConfiguration mConfiguration; + LuaUtil::LuaState mLua; + LuaUi::ResourceManager mUiResourceManager; + std::map mLocalPackages; + std::map mPlayerPackages; + + MenuScripts mMenuScripts{ &mLua }; + GlobalScripts mGlobalScripts{ &mLua }; + std::set mActiveLocalScripts; + std::vector mQueuedAutoStartedScripts; + ObjectLists mObjectLists; + + MWWorld::Ptr mPlayer; + + LuaEvents mLuaEvents{ mGlobalScripts, mMenuScripts }; + EngineEvents mEngineEvents{ mGlobalScripts }; + std::vector mInputEvents; + std::vector mMenuInputEvents; + + std::unique_ptr mGlobalSerializer; + std::unique_ptr mLocalSerializer; + + std::map mContentFileMapping; + std::unique_ptr mGlobalLoader; + std::unique_ptr mLocalLoader; + + struct CallbackWithData + { + LuaUtil::Callback mCallback; + sol::main_object mArg; + }; + std::vector mQueuedCallbacks; + + // Queued actions that should be done in main thread. Processed by applyQueuedChanges(). + class DelayedAction + { + public: + DelayedAction(LuaUtil::LuaState* state, std::function fn, std::string_view name); + void apply() const; + + private: + std::string mCallerTraceback; + std::function mFn; + std::string mName; + }; + std::vector mActionQueue; + std::optional mTeleportPlayerAction; + std::vector> mUIMessages; + std::vector> mInGameConsoleMessages; + std::optional mDelayedUiModeChangedArg; + + LuaUtil::LuaStorage mGlobalStorage; + LuaUtil::LuaStorage mPlayerStorage; + + LuaUtil::InputAction::Registry mInputActions; + LuaUtil::InputTrigger::Registry mInputTriggers; + }; + +} + +#endif // MWLUA_LUAMANAGERIMP_H diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp new file mode 100644 index 00000000000..a62e42ef0e2 --- /dev/null +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -0,0 +1,1109 @@ +#include "magicbindings.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwmechanics/activespells.hpp" +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/magiceffects.hpp" +#include "../mwmechanics/spellutil.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/worldmodel.hpp" + +#include "localscripts.hpp" +#include "luamanagerimp.hpp" +#include "object.hpp" +#include "objectvariant.hpp" +#include "recordstore.hpp" + +namespace MWLua +{ + template + struct ActorStore + { + using Collection = typename Store::Collection; + using Iterator = typename Collection::const_iterator; + + ActorStore(const sol::object& actor) + : mActor(actor) + , mIterator() + , mIndex(0) + { + if (!isActor()) + throw std::runtime_error("Actor expected"); + reset(); + } + + bool isActor() const { return !mActor.ptr().isEmpty() && mActor.ptr().getClass().isActor(); } + + bool isLObject() const { return mActor.isLObject(); } + + void reset() + { + mIndex = 0; + auto* store = getStore(); + if (store) + mIterator = store->begin(); + } + + bool isEnd() const + { + auto* store = getStore(); + if (store) + return mIterator == store->end(); + return true; + } + + void advance() + { + auto* store = getStore(); + if (store) + { + mIterator++; + mIndex++; + } + } + + Store* getStore() const; + + ObjectVariant mActor; + Iterator mIterator; + int mIndex; + }; + + template <> + MWMechanics::Spells* ActorStore::getStore() const + { + if (!isActor()) + return nullptr; + const MWWorld::Ptr& ptr = mActor.ptr(); + return &ptr.getClass().getCreatureStats(ptr).getSpells(); + } + + template <> + MWMechanics::MagicEffects* ActorStore::getStore() const + { + if (!isActor()) + return nullptr; + const MWWorld::Ptr& ptr = mActor.ptr(); + return &ptr.getClass().getCreatureStats(ptr).getMagicEffects(); + } + + template <> + MWMechanics::ActiveSpells* ActorStore::getStore() const + { + if (!isActor()) + return nullptr; + const MWWorld::Ptr& ptr = mActor.ptr(); + return &ptr.getClass().getCreatureStats(ptr).getActiveSpells(); + } + + struct ActiveEffect + { + MWMechanics::EffectKey key; + MWMechanics::EffectParam param; + }; + struct ActiveSpell + { + ObjectVariant mActor; + MWMechanics::ActiveSpells::ActiveSpellParams mParams; + }; + + // class returned via 'types.Actor.spells(obj)' in Lua + using ActorSpells = ActorStore; + // class returned via 'types.Actor.activeEffects(obj)' in Lua + using ActorActiveEffects = ActorStore; + // class returned via 'types.Actor.activeSpells(obj)' in Lua + using ActorActiveSpells = ActorStore; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template + struct is_automagical> : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + static ESM::RefId toSpellId(const sol::object& spellOrId) + { + if (spellOrId.is()) + return spellOrId.as()->mId; + else + return ESM::RefId::deserializeText(LuaUtil::cast(spellOrId)); + } + static ESM::RefId toRecordId(const sol::object& recordOrId) + { + if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else + return ESM::RefId::deserializeText(LuaUtil::cast(recordOrId)); + } + + static const ESM::Spell* toSpell(const sol::object& spellOrId) + { + if (spellOrId.is()) + return spellOrId.as(); + else + { + auto& store = MWBase::Environment::get().getWorld()->getStore(); + auto refId = ESM::RefId::deserializeText(LuaUtil::cast(spellOrId)); + return store.get().find(refId); + } + } + + static sol::table effectParamsListToTable(lua_State* lua, const std::vector& effects) + { + sol::table res(lua, sol::create); + for (size_t i = 0; i < effects.size(); ++i) + res[LuaUtil::toLuaIndex(i)] = effects[i]; // ESM::IndexedENAMstruct (effect params) + return res; + } + + sol::table initCoreMagicBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table magicApi(lua, sol::create); + + // Constants + magicApi["RANGE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Self", ESM::RT_Self }, + { "Touch", ESM::RT_Touch }, + { "Target", ESM::RT_Target }, + })); + magicApi["SPELL_TYPE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Spell", ESM::Spell::ST_Spell }, + { "Ability", ESM::Spell::ST_Ability }, + { "Blight", ESM::Spell::ST_Blight }, + { "Disease", ESM::Spell::ST_Disease }, + { "Curse", ESM::Spell::ST_Curse }, + { "Power", ESM::Spell::ST_Power }, + })); + magicApi["ENCHANTMENT_TYPE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "CastOnce", ESM::Enchantment::Type::CastOnce }, + { "CastOnStrike", ESM::Enchantment::Type::WhenStrikes }, + { "CastOnUse", ESM::Enchantment::Type::WhenUsed }, + { "ConstantEffect", ESM::Enchantment::Type::ConstantEffect }, + })); + + sol::table effect(lua, sol::create); + magicApi["EFFECT_TYPE"] = LuaUtil::makeStrictReadOnly(effect); + for (const auto& name : ESM::MagicEffect::sIndexNames) + { + effect[name] = Misc::StringUtils::lowerCase(name); + } + + // Spell store + sol::table spells(lua, sol::create); + addRecordFunctionBinding(spells, context); + magicApi["spells"] = LuaUtil::makeReadOnly(spells); + + // Enchantment store + sol::table enchantments(lua, sol::create); + addRecordFunctionBinding(enchantments, context); + magicApi["enchantments"] = LuaUtil::makeReadOnly(enchantments); + + // MagicEffect store + sol::table magicEffects(lua, sol::create); + magicApi["effects"] = LuaUtil::makeReadOnly(magicEffects); + using MagicEffectStore = MWWorld::Store; + const MagicEffectStore* magicEffectStore + = &MWBase::Environment::get().getWorld()->getStore().get(); + auto magicEffectStoreT = lua.new_usertype("ESM3_MagicEffectStore"); + magicEffectStoreT[sol::meta_function::to_string] = [](const MagicEffectStore& store) { + return "ESM3_MagicEffectStore{" + std::to_string(store.getSize()) + " effects}"; + }; + magicEffectStoreT[sol::meta_function::index] = sol::overload( + [](const MagicEffectStore& store, int id) -> const ESM::MagicEffect* { return store.search(id); }, + [](const MagicEffectStore& store, std::string_view id) -> const ESM::MagicEffect* { + int index = ESM::MagicEffect::indexNameToIndex(id); + return store.search(index); + }); + auto magicEffectsIter = [magicEffectStore](sol::this_state lua, const sol::object& /*store*/, + sol::optional id) -> std::tuple { + MagicEffectStore::iterator iter; + if (id.has_value()) + { + iter = magicEffectStore->findIter(*id); + if (iter != magicEffectStore->end()) + iter++; + } + else + iter = magicEffectStore->begin(); + if (iter != magicEffectStore->end()) + return std::make_tuple(sol::make_object(lua, iter->first), sol::make_object(lua, &iter->second)); + else + return std::make_tuple(sol::nil, sol::nil); + }; + magicEffectStoreT[sol::meta_function::pairs] + = [iter = sol::make_object(lua, magicEffectsIter)] { return iter; }; + magicEffectStoreT[sol::meta_function::ipairs] + = [iter = sol::make_object(lua, magicEffectsIter)] { return iter; }; + + magicEffects["records"] = magicEffectStore; + + // Spell record + auto spellT = lua.new_usertype("ESM3_Spell"); + spellT[sol::meta_function::to_string] + = [](const ESM::Spell& rec) -> std::string { return "ESM3_Spell[" + rec.mId.toDebugString() + "]"; }; + spellT["id"] = sol::readonly_property([](const ESM::Spell& rec) { return rec.mId.serializeText(); }); + spellT["name"] = sol::readonly_property([](const ESM::Spell& rec) -> std::string_view { return rec.mName; }); + spellT["type"] = sol::readonly_property([](const ESM::Spell& rec) -> int { return rec.mData.mType; }); + spellT["cost"] = sol::readonly_property([](const ESM::Spell& rec) -> int { return rec.mData.mCost; }); + spellT["alwaysSucceedFlag"] = sol::readonly_property( + [](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_Always); }); + spellT["starterSpellFlag"] = sol::readonly_property( + [](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_PCStart); }); + spellT["autocalcFlag"] = sol::readonly_property( + [](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_Autocalc); }); + spellT["effects"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Spell& rec) -> sol::table { + return effectParamsListToTable(lua, rec.mEffects.mList); + }); + + // Enchantment record + auto enchantT = lua.new_usertype("ESM3_Enchantment"); + enchantT[sol::meta_function::to_string] = [](const ESM::Enchantment& rec) -> std::string { + return "ESM3_Enchantment[" + rec.mId.toDebugString() + "]"; + }; + enchantT["id"] = sol::readonly_property([](const ESM::Enchantment& rec) { return rec.mId.serializeText(); }); + enchantT["type"] = sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mType; }); + enchantT["autocalcFlag"] = sol::readonly_property( + [](const ESM::Enchantment& rec) -> bool { return !!(rec.mData.mFlags & ESM::Enchantment::Autocalc); }); + enchantT["cost"] = sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mCost; }); + enchantT["charge"] + = sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mCharge; }); + enchantT["effects"] + = sol::readonly_property([lua = lua.lua_state()](const ESM::Enchantment& rec) -> sol::table { + return effectParamsListToTable(lua, rec.mEffects.mList); + }); + + // Effect params + auto effectParamsT = lua.new_usertype("ESM3_EffectParams"); + effectParamsT[sol::meta_function::to_string] = [magicEffectStore](const ESM::IndexedENAMstruct& params) { + const ESM::MagicEffect* const rec = magicEffectStore->find(params.mData.mEffectID); + return "ESM3_EffectParams[" + ESM::MagicEffect::indexToGmstString(rec->mIndex) + "]"; + }; + effectParamsT["effect"] = sol::readonly_property( + [magicEffectStore](const ESM::IndexedENAMstruct& params) -> const ESM::MagicEffect* { + return magicEffectStore->find(params.mData.mEffectID); + }); + effectParamsT["id"] = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> std::string { + auto name = ESM::MagicEffect::indexToName(params.mData.mEffectID); + return Misc::StringUtils::lowerCase(name); + }); + effectParamsT["affectedSkill"] + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> sol::optional { + ESM::RefId id = ESM::Skill::indexToRefId(params.mData.mSkill); + if (!id.empty()) + return id.serializeText(); + return sol::nullopt; + }); + effectParamsT["affectedAttribute"] + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> sol::optional { + ESM::RefId id = ESM::Attribute::indexToRefId(params.mData.mAttribute); + if (!id.empty()) + return id.serializeText(); + return sol::nullopt; + }); + effectParamsT["range"] + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mRange; }); + effectParamsT["area"] + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mArea; }); + effectParamsT["magnitudeMin"] + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mMagnMin; }); + effectParamsT["magnitudeMax"] + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mMagnMax; }); + effectParamsT["duration"] = sol::readonly_property( + [](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mDuration; }); + effectParamsT["index"] + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mIndex; }); + + // MagicEffect record + auto magicEffectT = lua.new_usertype("ESM3_MagicEffect"); + + magicEffectT[sol::meta_function::to_string] = [](const ESM::MagicEffect& rec) { + return "ESM3_MagicEffect[" + ESM::MagicEffect::indexToGmstString(rec.mIndex) + "]"; + }; + magicEffectT["id"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string { + auto name = ESM::MagicEffect::indexToName(rec.mIndex); + return Misc::StringUtils::lowerCase(name); + }); + magicEffectT["icon"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + magicEffectT["particle"] + = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return rec.mParticle; }); + magicEffectT["continuousVfx"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> bool { + return (rec.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; + }); + magicEffectT["areaSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mAreaSound.serializeText(); }); + magicEffectT["boltSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mBoltSound.serializeText(); }); + magicEffectT["castSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mCastSound.serializeText(); }); + magicEffectT["hitSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mHitSound.serializeText(); }); + magicEffectT["areaStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mArea.serializeText(); }); + magicEffectT["bolt"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mBolt.serializeText(); }); + magicEffectT["castStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mCasting.serializeText(); }); + magicEffectT["hitStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mHit.serializeText(); }); + magicEffectT["name"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { + return MWBase::Environment::get() + .getWorld() + ->getStore() + .get() + .find(ESM::MagicEffect::indexToGmstString(rec.mIndex)) + ->mValue.getString(); + }); + magicEffectT["school"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mData.mSchool.serializeText(); }); + magicEffectT["baseCost"] + = sol::readonly_property([](const ESM::MagicEffect& rec) -> float { return rec.mData.mBaseCost; }); + magicEffectT["color"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> Misc::Color { + return Misc::Color(rec.mData.mRed / 255.f, rec.mData.mGreen / 255.f, rec.mData.mBlue / 255.f, 1.f); + }); + magicEffectT["hasDuration"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return !(rec.mData.mFlags & ESM::MagicEffect::NoDuration); }); + magicEffectT["hasMagnitude"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return !(rec.mData.mFlags & ESM::MagicEffect::NoMagnitude); }); + // TODO: Not self-explanatory. Needs either a better name or documentation. The description in + // loadmgef.hpp is uninformative. + magicEffectT["isAppliedOnce"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::AppliedOnce; }); + magicEffectT["harmful"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::Harmful; }); + magicEffectT["casterLinked"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::CasterLinked; }); + magicEffectT["nonRecastable"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::NonRecastable; }); + + // TODO: Should we expose it? What happens if a spell has several effects with different projectileSpeed? + // magicEffectT["projectileSpeed"] + // = sol::readonly_property([](const ESM::MagicEffect& rec) -> float { return rec.mData.mSpeed; }); + + auto activeSpellEffectT = lua.new_usertype("ActiveSpellEffect"); + activeSpellEffectT[sol::meta_function::to_string] = [](const ESM::ActiveEffect& effect) { + return "ActiveSpellEffect[" + ESM::MagicEffect::indexToGmstString(effect.mEffectId) + "]"; + }; + activeSpellEffectT["id"] = sol::readonly_property([](const ESM::ActiveEffect& effect) -> std::string { + auto name = ESM::MagicEffect::indexToName(effect.mEffectId); + return Misc::StringUtils::lowerCase(name); + }); + activeSpellEffectT["index"] + = sol::readonly_property([](const ESM::ActiveEffect& effect) -> int { return effect.mEffectIndex; }); + activeSpellEffectT["name"] = sol::readonly_property([](const ESM::ActiveEffect& effect) -> std::string { + return MWMechanics::EffectKey(effect.mEffectId, effect.getSkillOrAttribute()).toString(); + }); + activeSpellEffectT["affectedSkill"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::TargetSkill) + return effect.getSkillOrAttribute().serializeText(); + else + return sol::nullopt; + }); + activeSpellEffectT["affectedAttribute"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::TargetAttribute) + return effect.getSkillOrAttribute().serializeText(); + else + return sol::nullopt; + }); + activeSpellEffectT["magnitudeThisFrame"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoMagnitude) + return sol::nullopt; + return effect.mMagnitude; + }); + activeSpellEffectT["minMagnitude"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoMagnitude) + return sol::nullopt; + return effect.mMinMagnitude; + }); + activeSpellEffectT["maxMagnitude"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoMagnitude) + return sol::nullopt; + return effect.mMaxMagnitude; + }); + activeSpellEffectT["durationLeft"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + // Permanent/constant effects, abilities, etc. will have a negative duration + if (effect.mDuration < 0) + return sol::nullopt; + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoDuration) + return sol::nullopt; + return effect.mTimeLeft; + }); + activeSpellEffectT["duration"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + // Permanent/constant effects, abilities, etc. will have a negative duration + if (effect.mDuration < 0) + return sol::nullopt; + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoDuration) + return sol::nullopt; + return effect.mDuration; + }); + + auto activeSpellT = lua.new_usertype("ActiveSpellParams"); + activeSpellT[sol::meta_function::to_string] = [](const ActiveSpell& activeSpell) { + return "ActiveSpellParams[" + activeSpell.mParams.getSourceSpellId().serializeText() + "]"; + }; + activeSpellT["name"] = sol::readonly_property( + [](const ActiveSpell& activeSpell) -> std::string_view { return activeSpell.mParams.getDisplayName(); }); + activeSpellT["id"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> std::string { + return activeSpell.mParams.getSourceSpellId().serializeText(); + }); + activeSpellT["item"] + = sol::readonly_property([lua = lua.lua_state()](const ActiveSpell& activeSpell) -> sol::object { + auto item = activeSpell.mParams.getItem(); + if (!item.isSet()) + return sol::nil; + auto itemPtr = MWBase::Environment::get().getWorldModel()->getPtr(item); + if (itemPtr.isEmpty()) + return sol::nil; + if (activeSpell.mActor.isGObject()) + return sol::make_object(lua, GObject(itemPtr)); + else + return sol::make_object(lua, LObject(itemPtr)); + }); + activeSpellT["caster"] + = sol::readonly_property([lua = lua.lua_state()](const ActiveSpell& activeSpell) -> sol::object { + auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId( + activeSpell.mParams.getCasterActorId()); + if (caster.isEmpty()) + return sol::nil; + else + { + if (activeSpell.mActor.isGObject()) + return sol::make_object(lua, GObject(getId(caster))); + else + return sol::make_object(lua, LObject(getId(caster))); + } + }); + activeSpellT["effects"] + = sol::readonly_property([lua = lua.lua_state()](const ActiveSpell& activeSpell) -> sol::table { + sol::table res(lua, sol::create); + size_t tableIndex = 0; + for (const ESM::ActiveEffect& effect : activeSpell.mParams.getEffects()) + { + if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + res[++tableIndex] = effect; // ESM::ActiveEffect (effect params) + } + return res; + }); + activeSpellT["fromEquipment"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Equipment); + }); + activeSpellT["temporary"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Temporary); + }); + activeSpellT["affectsBaseValues"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues); + }); + activeSpellT["stackable"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Stackable); + }); + activeSpellT["activeSpellId"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> std::string { + return activeSpell.mParams.getActiveSpellId().serializeText(); + }); + + auto activeEffectT = lua.new_usertype("ActiveEffect"); + + activeEffectT[sol::meta_function::to_string] = [](const ActiveEffect& effect) { + return "ActiveEffect[" + ESM::MagicEffect::indexToGmstString(effect.key.mId) + "]"; + }; + activeEffectT["id"] = sol::readonly_property([](const ActiveEffect& effect) -> std::string { + auto name = ESM::MagicEffect::indexToName(effect.key.mId); + return Misc::StringUtils::lowerCase(name); + }); + activeEffectT["name"] + = sol::readonly_property([](const ActiveEffect& effect) -> std::string { return effect.key.toString(); }); + + activeEffectT["affectedSkill"] + = sol::readonly_property([magicEffectStore](const ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.key.mId); + if (rec->mData.mFlags & ESM::MagicEffect::TargetSkill) + return effect.key.mArg.serializeText(); + return sol::nullopt; + }); + activeEffectT["affectedAttribute"] + = sol::readonly_property([magicEffectStore](const ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.key.mId); + if (rec->mData.mFlags & ESM::MagicEffect::TargetAttribute) + return effect.key.mArg.serializeText(); + return sol::nullopt; + }); + + activeEffectT["magnitude"] + = sol::readonly_property([](const ActiveEffect& effect) { return effect.param.getMagnitude(); }); + activeEffectT["magnitudeBase"] + = sol::readonly_property([](const ActiveEffect& effect) { return effect.param.getBase(); }); + activeEffectT["magnitudeModifier"] + = sol::readonly_property([](const ActiveEffect& effect) { return effect.param.getModifier(); }); + + return LuaUtil::makeReadOnly(magicApi); + } + + static std::pair> getNameAndMagicEffects( + const MWWorld::Ptr& actor, ESM::RefId id, const sol::table& effects, bool quiet) + { + std::vector effectIndexes; + + for (const auto& entry : effects) + { + if (entry.second.is()) + effectIndexes.push_back(entry.second.as()); + else if (entry.second.is()) + throw std::runtime_error("Error: Adding effects as enam structs is not implemented, use indexes."); + else + throw std::runtime_error("Unexpected entry in 'effects' table while trying to add to active effects"); + } + + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); + + auto getEffectsFromIndexes = [&](const ESM::EffectList& effects) { + std::vector enams; + for (auto index : effectIndexes) + enams.push_back(effects.mList.at(index)); + return enams; + }; + + auto getNameAndEffects = [&](auto* record) { + return std::pair>( + record->mName, getEffectsFromIndexes(record->mEffects)); + }; + auto getNameAndEffectsEnch = [&](auto* record) { + auto* enchantment = esmStore.get().find(record->mEnchant); + return std::pair>( + record->mName, getEffectsFromIndexes(enchantment->mEffects)); + }; + switch (esmStore.find(id)) + { + case ESM::REC_ALCH: + return getNameAndEffects(esmStore.get().find(id)); + case ESM::REC_INGR: + { + // Ingredients are a special case as their effect list is calculated on consumption. + const ESM::Ingredient* ingredient = esmStore.get().find(id); + std::vector enams; + quiet = quiet || actor != MWMechanics::getPlayer(); + for (uint32_t i = 0; i < effectIndexes.size(); i++) + { + if (auto effect = MWMechanics::rollIngredientEffect(actor, ingredient, effectIndexes[i])) + enams.push_back(effect->mList[0]); + } + if (enams.empty() && !quiet) + { + // "X has no effect on you" + std::string message = esmStore.get().find("sNotifyMessage50")->mValue.getString(); + message = Misc::StringUtils::format(message, ingredient->mName); + MWBase::Environment::get().getWindowManager()->messageBox(message); + } + return { ingredient->mName, std::move(enams) }; + } + case ESM::REC_ARMO: + return getNameAndEffectsEnch(esmStore.get().find(id)); + case ESM::REC_BOOK: + return getNameAndEffectsEnch(esmStore.get().find(id)); + case ESM::REC_CLOT: + return getNameAndEffectsEnch(esmStore.get().find(id)); + case ESM::REC_WEAP: + return getNameAndEffectsEnch(esmStore.get().find(id)); + default: + // esmStore.find doesn't find REC_SPELs + case ESM::REC_SPEL: + return getNameAndEffects(esmStore.get().find(id)); + } + } + + void addActorMagicBindings(sol::table& actor, const Context& context) + { + auto lua = context.sol(); + const MWWorld::Store* spellStore + = &MWBase::Environment::get().getWorld()->getStore().get(); + + // types.Actor.spells(o) + actor["spells"] = [](const sol::object& actor) { return ActorSpells{ actor }; }; + auto spellsT = lua.new_usertype("ActorSpells"); + spellsT[sol::meta_function::to_string] + = [](const ActorSpells& spells) { return "ActorSpells[" + spells.mActor.object().toString() + "]"; }; + + actor["activeSpells"] = [](const sol::object& actor) { return ActorActiveSpells{ actor }; }; + auto activeSpellsT = lua.new_usertype("ActorActiveSpells"); + activeSpellsT[sol::meta_function::to_string] = [](const ActorActiveSpells& spells) { + return "ActorActiveSpells[" + spells.mActor.object().toString() + "]"; + }; + + actor["activeEffects"] = [](const sol::object& actor) { return ActorActiveEffects{ actor }; }; + auto activeEffectsT = lua.new_usertype("ActorActiveEffects"); + activeEffectsT[sol::meta_function::to_string] = [](const ActorActiveEffects& effects) { + return "ActorActiveEffects[" + effects.mActor.object().toString() + "]"; + }; + + actor["getSelectedSpell"] = [spellStore](const Object& o) -> sol::optional { + const MWWorld::Ptr& ptr = o.ptr(); + const MWWorld::Class& cls = ptr.getClass(); + if (!cls.isActor()) + throw std::runtime_error("Actor expected"); + ESM::RefId spellId; + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + spellId = MWBase::Environment::get().getWindowManager()->getSelectedSpell(); + else + spellId = cls.getCreatureStats(ptr).getSpells().getSelectedSpell(); + if (spellId.empty()) + return sol::nullopt; + else + return spellStore->find(spellId); + }; + actor["setSelectedSpell"] = [context, spellStore](const SelfObject& o, const sol::object& spellOrId) { + const MWWorld::Ptr& ptr = o.ptr(); + const MWWorld::Class& cls = ptr.getClass(); + if (!cls.isActor()) + throw std::runtime_error("Actor expected"); + ESM::RefId spellId; + if (spellOrId != sol::nil) + { + spellId = toSpellId(spellOrId); + const ESM::Spell* spell = spellStore->find(spellId); + if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) + throw std::runtime_error("Ability or disease can not be casted: " + spellId.toDebugString()); + } + context.mLuaManager->addAction([obj = Object(ptr), spellId]() { + const MWWorld::Ptr& ptr = obj.ptr(); + auto& stats = ptr.getClass().getCreatureStats(ptr); + + // We need to deselect any enchant items before we can select a spell otherwise the item will be + // reselected + const auto resetEnchantItem = [&]() { + if (ptr.getClass().hasInventoryStore(ptr)) + { + MWWorld::InventoryStore& inventory = ptr.getClass().getInventoryStore(ptr); + inventory.setSelectedEnchantItem(inventory.end()); + } + }; + + if (spellId.empty()) + { + resetEnchantItem(); + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + MWBase::Environment::get().getWindowManager()->unsetSelectedSpell(); + else + stats.getSpells().setSelectedSpell(ESM::RefId()); + return; + } + if (!stats.getSpells().hasSpell(spellId)) + throw std::runtime_error("Actor doesn't know spell " + spellId.toDebugString()); + + resetEnchantItem(); + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + int chance = MWMechanics::getSpellSuccessChance(spellId, ptr); + MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, chance); + } + else + stats.getSpells().setSelectedSpell(spellId); + }); + }; + + actor["clearSelectedCastable"] = [context](const SelfObject& o) { + if (!o.ptr().getClass().isActor()) + throw std::runtime_error("Actor expected"); + context.mLuaManager->addAction([obj = Object(o.ptr())]() { + const MWWorld::Ptr& ptr = obj.ptr(); + auto& stats = ptr.getClass().getCreatureStats(ptr); + if (ptr.getClass().hasInventoryStore(ptr)) + { + MWWorld::InventoryStore& inventory = ptr.getClass().getInventoryStore(ptr); + inventory.setSelectedEnchantItem(inventory.end()); + } + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + MWBase::Environment::get().getWindowManager()->unsetSelectedSpell(); + else + stats.getSpells().setSelectedSpell(ESM::RefId()); + }); + }; + + // #(types.Actor.spells(o)) + spellsT[sol::meta_function::length] = [](const ActorSpells& spells) -> size_t { + if (auto* store = spells.getStore()) + return store->count(); + return 0; + }; + + // types.Actor.spells(o)[i] + spellsT[sol::meta_function::index] = sol::overload( + [](const ActorSpells& spells, size_t index) -> const ESM::Spell* { + if (auto* store = spells.getStore()) + if (index <= store->count() && index > 0) + return store->at(LuaUtil::fromLuaIndex(index)); + return nullptr; + }, + [spellStore](const ActorSpells& spells, std::string_view spellId) -> const ESM::Spell* { + if (auto* store = spells.getStore()) + { + const ESM::Spell* spell = spellStore->search(ESM::RefId::deserializeText(spellId)); + if (spell && store->hasSpell(spell)) + return spell; + } + return nullptr; + }); + + // pairs(types.Actor.spells(o)) + spellsT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + + // ipairs(types.Actor.spells(o)) + spellsT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + + // types.Actor.spells(o):add(id) + spellsT["add"] = [context](const ActorSpells& spells, const sol::object& spellOrId) { + if (spells.mActor.isLObject()) + throw std::runtime_error("Local scripts can modify only spells of the actor they are attached to."); + context.mLuaManager->addAction([obj = spells.mActor.object(), id = toSpellId(spellOrId)]() { + const MWWorld::Ptr& ptr = obj.ptr(); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).getSpells().add(id); + }); + }; + + // types.Actor.spells(o):remove(id) + spellsT["remove"] = [context](const ActorSpells& spells, const sol::object& spellOrId) { + if (spells.mActor.isLObject()) + throw std::runtime_error("Local scripts can modify only spells of the actor they are attached to."); + context.mLuaManager->addAction([obj = spells.mActor.object(), id = toSpellId(spellOrId)]() { + const MWWorld::Ptr& ptr = obj.ptr(); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).getSpells().remove(id); + }); + }; + + // types.Actor.spells(o):clear() + spellsT["clear"] = [context](const ActorSpells& spells) { + if (spells.mActor.isLObject()) + throw std::runtime_error("Local scripts can modify only spells of the actor they are attached to."); + context.mLuaManager->addAction([obj = spells.mActor.object()]() { + const MWWorld::Ptr& ptr = obj.ptr(); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).getSpells().clear(); + }); + }; + + // types.Actor.spells(o):canUsePower() + spellsT["canUsePower"] = [](const ActorSpells& spells, const sol::object& spellOrId) -> bool { + if (spells.mActor.isLObject()) + throw std::runtime_error("Local scripts can modify only spells of the actor they are attached to."); + auto* spell = toSpell(spellOrId); + if (auto* store = spells.getStore()) + return store->canUsePower(spell); + return false; + }; + + // pairs(types.Actor.activeSpells(o)) + activeSpellsT["__pairs"] = [](sol::this_state ts, ActorActiveSpells& self) { + sol::state_view lua(ts); + self.reset(); + return sol::as_function([lua, self]() mutable -> std::pair { + if (!self.isEnd()) + { + auto id = sol::make_object(lua, self.mIterator->getSourceSpellId().serializeText()); + auto params = sol::make_object(lua, ActiveSpell{ self.mActor, *self.mIterator }); + self.advance(); + return { id, params }; + } + else + { + return { sol::lua_nil, sol::lua_nil }; + } + }); + }; + + // types.Actor.activeSpells(o):isSpellActive(id) + activeSpellsT["isSpellActive"] + = [](const ActorActiveSpells& activeSpells, const sol::object& recordOrId) -> bool { + if (auto* store = activeSpells.getStore()) + { + auto id = toRecordId(recordOrId); + return store->isSpellActive(id) || store->isEnchantmentActive(id); + } + return false; + }; + + // types.Actor.activeSpells(o):remove(id) + activeSpellsT["remove"] = [context](const ActorActiveSpells& spells, std::string_view idStr) { + if (spells.isLObject()) + throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to."); + + context.mLuaManager->addAction([spells = spells, id = ESM::RefId::deserializeText(idStr)]() { + if (auto* store = spells.getStore()) + { + auto it = store->getActiveSpellById(id); + if (it != store->end()) + { + if (it->hasFlag(ESM::ActiveSpells::Flag_Temporary)) + store->removeEffectsByActiveSpellId(spells.mActor.ptr(), id); + else + throw std::runtime_error("Can only remove temporary effects."); + } + } + }); + }; + + // types.Actor.activeSpells(o):add(id, spellid, effects, options) + activeSpellsT["add"] = [](const ActorActiveSpells& spells, const sol::table& options) { + if (spells.isLObject()) + throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to."); + + if (auto* store = spells.getStore()) + { + ESM::RefId id = ESM::RefId::deserializeText(options.get("id")); + sol::optional item = options.get>("item"); + ESM::RefNum itemId; + if (item) + itemId = item->id(); + sol::optional caster = options.get>("caster"); + bool stackable = options.get_or("stackable", false); + bool ignoreReflect = options.get_or("ignoreReflect", false); + bool ignoreSpellAbsorption = options.get_or("ignoreSpellAbsorption", false); + bool ignoreResistances = options.get_or("ignoreResistances", false); + sol::table effects = options.get("effects"); + bool quiet = options.get_or("quiet", false); + if (effects.empty()) + throw std::runtime_error("Error: Parameter 'effects': cannot be an empty list/table"); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); + auto [name, enams] = getNameAndMagicEffects(spells.mActor.ptr(), id, effects, quiet); + name = options.get_or("name", name); + + MWWorld::Ptr casterPtr; + if (caster) + casterPtr = caster->ptrOrEmpty(); + + bool affectsHealth = false; + MWMechanics::ActiveSpells::ActiveSpellParams params(casterPtr, id, name, itemId); + params.setFlag(ESM::ActiveSpells::Flag_Lua); + params.setFlag(ESM::ActiveSpells::Flag_Temporary); + if (stackable) + params.setFlag(ESM::ActiveSpells::Flag_Stackable); + + for (const ESM::IndexedENAMstruct& enam : enams) + { + const ESM::MagicEffect* mgef = esmStore.get().find(enam.mData.mEffectID); + MWMechanics::ActiveSpells::ActiveEffect effect; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; + effect.mEffectIndex = enam.mIndex; + effect.mFlags = ESM::ActiveEffect::Flag_None; + if (ignoreReflect) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect; + if (ignoreSpellAbsorption) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + if (ignoreResistances) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances; + + bool hasDuration = !(mgef->mData.mFlags & ESM::MagicEffect::NoDuration); + effect.mDuration = hasDuration ? static_cast(enam.mData.mDuration) : 1.f; + + bool appliedOnce = mgef->mData.mFlags & ESM::MagicEffect::AppliedOnce; + if (!appliedOnce) + effect.mDuration = std::max(1.f, effect.mDuration); + effect.mTimeLeft = effect.mDuration; + params.getEffects().emplace_back(effect); + + affectsHealth = affectsHealth || mgef->mData.mFlags & ESM::MagicEffect::Harmful + || effect.mEffectId == ESM::MagicEffect::RestoreHealth; + } + store->addSpell(params); + if (affectsHealth && casterPtr == MWMechanics::getPlayer()) + // If player is attempting to cast a harmful spell on or is healing a living target, show the + // target's HP bar. + // TODO: This should be moved to Lua once the HUD has been dehardcoded + MWBase::Environment::get().getWindowManager()->setEnemy(spells.mActor.ptr()); + } + }; + + // pairs(types.Actor.activeEffects(o)) + // Note that the indexes are fake, and only for consistency with other lua pairs interfaces. You can't use them + // for anything. + activeEffectsT["__pairs"] = [](sol::this_state ts, ActorActiveEffects& self) { + sol::state_view lua(ts); + self.reset(); + return sol::as_function([lua, self]() mutable -> std::pair { + while (!self.isEnd()) + { + if (self.mIterator->second.getBase() == 0 && self.mIterator->second.getModifier() == 0.f) + { + self.advance(); + continue; + } + ActiveEffect effect = ActiveEffect{ self.mIterator->first, self.mIterator->second }; + auto result = sol::make_object(lua, effect); + + auto key = sol::make_object(lua, self.mIterator->first.toString()); + self.advance(); + return { key, result }; + } + return { sol::lua_nil, sol::lua_nil }; + }); + }; + + auto getEffectKey + = [](std::string_view idStr, sol::optional argStr) -> MWMechanics::EffectKey { + auto id = ESM::MagicEffect::indexNameToIndex(idStr); + auto* rec = MWBase::Environment::get().getWorld()->getStore().get().find(id); + + MWMechanics::EffectKey key = MWMechanics::EffectKey(id); + + if (argStr.has_value() + && (rec->mData.mFlags & (ESM::MagicEffect::TargetAttribute | ESM::MagicEffect::TargetSkill))) + { + // MWLua exposes attributes and skills as strings, so we have to convert them back to IDs here + if (rec->mData.mFlags & ESM::MagicEffect::TargetAttribute) + { + ESM::RefId attribute = ESM::RefId::deserializeText(argStr.value()); + key = MWMechanics::EffectKey(id, attribute); + } + + if (rec->mData.mFlags & ESM::MagicEffect::TargetSkill) + { + ESM::RefId skill = ESM::RefId::deserializeText(argStr.value()); + key = MWMechanics::EffectKey(id, skill); + } + } + + return key; + }; + + // types.Actor.activeEffects(o):getEffect(id, ?arg) + activeEffectsT["getEffect"] = [getEffectKey](const ActorActiveEffects& effects, std::string_view idStr, + sol::optional argStr) -> sol::optional { + if (!effects.isActor()) + return sol::nullopt; + + MWMechanics::EffectKey key = getEffectKey(idStr, argStr); + + if (auto* store = effects.getStore()) + if (auto effect = store->get(key)) + return ActiveEffect{ key, effect.value() }; + return ActiveEffect{ key, MWMechanics::EffectParam() }; + }; + + // types.Actor.activeEffects(o):removeEffect(id, ?arg) + activeEffectsT["remove"] = [getEffectKey](const ActorActiveEffects& effects, std::string_view idStr, + sol::optional argStr) { + if (!effects.isActor()) + return; + + if (effects.isLObject()) + throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to."); + + MWMechanics::EffectKey key = getEffectKey(idStr, argStr); + + // Note that, although this is member method of ActorActiveEffects and we are removing an effect (not a + // spell), we still need to use the active spells store to purge this effect from active spells. + const auto& ptr = effects.mActor.ptr(); + + auto& activeSpells = ptr.getClass().getCreatureStats(ptr).getActiveSpells(); + activeSpells.purgeEffect(ptr, key.mId, key.mArg); + }; + + // types.Actor.activeEffects(o):set(value, id, ?arg) + activeEffectsT["set"] = [getEffectKey](const ActorActiveEffects& effects, int value, std::string_view idStr, + sol::optional argStr) { + if (!effects.isActor()) + return; + + if (effects.isLObject()) + throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to."); + + MWMechanics::EffectKey key = getEffectKey(idStr, argStr); + int currentValue = effects.getStore()->getOrDefault(key).getMagnitude(); + effects.getStore()->modifyBase(key, value - currentValue); + }; + + // types.Actor.activeEffects(o):modify(value, id, ?arg) + activeEffectsT["modify"] = [getEffectKey](const ActorActiveEffects& effects, int value, std::string_view idStr, + sol::optional argStr) { + if (!effects.isActor()) + return; + + if (effects.isLObject()) + throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to."); + + MWMechanics::EffectKey key = getEffectKey(idStr, argStr); + effects.getStore()->modifyBase(key, value); + }; + } +} diff --git a/apps/openmw/mwlua/magicbindings.hpp b/apps/openmw/mwlua/magicbindings.hpp new file mode 100644 index 00000000000..047bd2e3d9c --- /dev/null +++ b/apps/openmw/mwlua/magicbindings.hpp @@ -0,0 +1,14 @@ +#ifndef MWLUA_MAGICBINDINGS_H +#define MWLUA_MAGICBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initCoreMagicBindings(const Context& context); + void addActorMagicBindings(sol::table& actor, const Context& context); +} + +#endif // MWLUA_MAGICBINDINGS_H diff --git a/apps/openmw/mwlua/markupbindings.cpp b/apps/openmw/mwlua/markupbindings.cpp new file mode 100644 index 00000000000..9a3142cc3b9 --- /dev/null +++ b/apps/openmw/mwlua/markupbindings.cpp @@ -0,0 +1,31 @@ +#include "markupbindings.hpp" + +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" + +#include "context.hpp" + +namespace MWLua +{ + sol::table initMarkupPackage(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + api["loadYaml"] = [lua, vfs](std::string_view fileName) { + Files::IStreamPtr file = vfs->get(VFS::Path::Normalized(fileName)); + return LuaUtil::loadYaml(*file, lua); + }; + api["decodeYaml"] + = [lua](std::string_view inputData) { return LuaUtil::loadYaml(std::string(inputData), lua); }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/markupbindings.hpp b/apps/openmw/mwlua/markupbindings.hpp new file mode 100644 index 00000000000..9105ab5edf6 --- /dev/null +++ b/apps/openmw/mwlua/markupbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_MARKUPBINDINGS_H +#define MWLUA_MARKUPBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initMarkupPackage(const Context&); +} + +#endif // MWLUA_MARKUPBINDINGS_H diff --git a/apps/openmw/mwlua/menuscripts.cpp b/apps/openmw/mwlua/menuscripts.cpp new file mode 100644 index 00000000000..32520c08225 --- /dev/null +++ b/apps/openmw/mwlua/menuscripts.cpp @@ -0,0 +1,126 @@ +#include "menuscripts.hpp" + +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwstate/character.hpp" + +namespace MWLua +{ + static const MWState::Character* findCharacter(std::string_view characterDir) + { + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + for (auto it = manager->characterBegin(); it != manager->characterEnd(); ++it) + if (it->getPath().filename() == characterDir) + return &*it; + return nullptr; + } + + static const MWState::Slot* findSlot(const MWState::Character* character, std::string_view slotName) + { + if (!character) + return nullptr; + for (const MWState::Slot& slot : *character) + if (slot.mPath.filename() == slotName) + return &slot; + return nullptr; + } + + sol::table initMenuPackage(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + + api["STATE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "NoGame", MWBase::StateManager::State_NoGame }, + { "Running", MWBase::StateManager::State_Running }, + { "Ended", MWBase::StateManager::State_Ended }, + })); + + api["getState"] = []() -> int { return MWBase::Environment::get().getStateManager()->getState(); }; + + api["newGame"] = []() { MWBase::Environment::get().getStateManager()->requestNewGame(); }; + + api["loadGame"] = [](std::string_view dir, std::string_view slotName) { + const MWState::Character* character = findCharacter(dir); + const MWState::Slot* slot = findSlot(character, slotName); + if (!slot) + throw std::runtime_error("Save game slot not found: " + std::string(dir) + "/" + std::string(slotName)); + MWBase::Environment::get().getStateManager()->requestLoad(slot->mPath); + }; + + api["deleteGame"] = [](std::string_view dir, std::string_view slotName) { + const MWState::Character* character = findCharacter(dir); + const MWState::Slot* slot = findSlot(character, slotName); + if (!slot) + throw std::runtime_error("Save game slot not found: " + std::string(dir) + "/" + std::string(slotName)); + MWBase::Environment::get().getStateManager()->deleteGame(character, slot); + }; + + api["getCurrentSaveDir"] = []() -> sol::optional { + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + const MWState::Character* character = manager->getCurrentCharacter(); + if (character) + return character->getPath().filename().string(); + else + return sol::nullopt; + }; + + api["saveGame"] = [](std::string_view description, sol::optional slotName) { + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + const MWState::Character* character = manager->getCurrentCharacter(); + const MWState::Slot* slot = nullptr; + if (slotName) + slot = findSlot(character, *slotName); + manager->saveGame(description, slot); + }; + + auto getSaves = [](sol::state_view lua, const MWState::Character& character) { + sol::table saves(lua, sol::create); + for (const MWState::Slot& slot : character) + { + sol::table slotInfo(lua, sol::create); + slotInfo["description"] = slot.mProfile.mDescription; + slotInfo["playerName"] = slot.mProfile.mPlayerName; + slotInfo["playerLevel"] = slot.mProfile.mPlayerLevel; + slotInfo["timePlayed"] = slot.mProfile.mTimePlayed; + sol::table contentFiles(lua, sol::create); + for (size_t i = 0; i < slot.mProfile.mContentFiles.size(); ++i) + contentFiles[LuaUtil::toLuaIndex(i)] = Misc::StringUtils::lowerCase(slot.mProfile.mContentFiles[i]); + + { + auto system_time = std::chrono::system_clock::now() + - (std::filesystem::file_time_type::clock::now() - slot.mTimeStamp); + slotInfo["creationTime"] = std::chrono::duration(system_time.time_since_epoch()).count(); + } + + slotInfo["contentFiles"] = contentFiles; + saves[slot.mPath.filename().string()] = slotInfo; + } + return saves; + }; + + api["getSaves"] = [getSaves](sol::this_state lua, std::string_view dir) -> sol::table { + const MWState::Character* character = findCharacter(dir); + if (!character) + throw std::runtime_error("Saves not found: " + std::string(dir)); + return getSaves(lua, *character); + }; + + api["getAllSaves"] = [getSaves](sol::this_state lua) -> sol::table { + sol::table saves(lua, sol::create); + MWBase::StateManager* manager = MWBase::Environment::get().getStateManager(); + for (auto it = manager->characterBegin(); it != manager->characterEnd(); ++it) + saves[it->getPath().filename().string()] = getSaves(lua, *it); + return saves; + }; + + api["quit"] = []() { MWBase::Environment::get().getStateManager()->requestQuit(); }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/menuscripts.hpp b/apps/openmw/mwlua/menuscripts.hpp new file mode 100644 index 00000000000..8721224413f --- /dev/null +++ b/apps/openmw/mwlua/menuscripts.hpp @@ -0,0 +1,57 @@ +#ifndef MWLUA_MENUSCRIPTS_H +#define MWLUA_MENUSCRIPTS_H + +#include + +#include +#include +#include + +#include "../mwbase/luamanager.hpp" + +#include "context.hpp" +#include "inputprocessor.hpp" + +namespace MWLua +{ + + sol::table initMenuPackage(const Context& context); + + class MenuScripts : public LuaUtil::ScriptsContainer + { + public: + MenuScripts(LuaUtil::LuaState* lua) + : LuaUtil::ScriptsContainer(lua, "Menu") + , mInputProcessor(this) + { + registerEngineHandlers({ &mOnFrameHandlers, &mStateChanged, &mConsoleCommandHandlers, &mUiModeChanged }); + } + + void processInputEvent(const MWBase::LuaManager::InputEvent& event) + { + mInputProcessor.processInputEvent(event); + } + + void onFrame(float dt) { callEngineHandlers(mOnFrameHandlers, dt); } + + void stateChanged() { callEngineHandlers(mStateChanged); } + + bool consoleCommand(const std::string& consoleMode, const std::string& command) + { + callEngineHandlers(mConsoleCommandHandlers, consoleMode, command); + return !mConsoleCommandHandlers.mList.empty(); + } + + void uiModeChanged() { callEngineHandlers(mUiModeChanged); } + + private: + friend class MWLua::InputProcessor; + MWLua::InputProcessor mInputProcessor; + EngineHandlerList mOnFrameHandlers{ "onFrame" }; + EngineHandlerList mStateChanged{ "onStateChanged" }; + EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; + EngineHandlerList mUiModeChanged{ "_onUiModeChanged" }; + }; +} + +#endif // MWLUA_GLOBALSCRIPTS_H diff --git a/apps/openmw/mwlua/mwscriptbindings.cpp b/apps/openmw/mwlua/mwscriptbindings.cpp new file mode 100644 index 00000000000..90d24b39fe0 --- /dev/null +++ b/apps/openmw/mwlua/mwscriptbindings.cpp @@ -0,0 +1,283 @@ +#include "mwscriptbindings.hpp" + +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/scriptmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwscript/globalscripts.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/worldimp.hpp" + +#include "object.hpp" + +#include + +namespace MWLua +{ + struct MWScriptRef + { + ESM::RefId mId; + sol::optional mObj; + + MWScript::Locals& getLocals() + { + if (mObj) + return mObj->ptr().getRefData().getLocals(); + else + return MWBase::Environment::get().getScriptManager()->getGlobalScripts().getLocals(mId); + } + bool isRunning() const + { + if (mObj.has_value()) // local script + { + MWWorld::LocalScripts& localScripts = MWBase::Environment::get().getWorld()->getLocalScripts(); + return localScripts.isRunning(mId, mObj->ptr()); + } + + return MWBase::Environment::get().getScriptManager()->getGlobalScripts().isRunning(mId); + } + }; + struct MWScriptVariables + { + MWScriptRef mRef; + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + float getGlobalVariableValue(const std::string_view globalId) + { + char varType = MWBase::Environment::get().getWorld()->getGlobalVariableType(globalId); + if (varType == 'f') + { + return MWBase::Environment::get().getWorld()->getGlobalFloat(globalId); + } + else if (varType == 's' || varType == 'l') + { + return static_cast(MWBase::Environment::get().getWorld()->getGlobalInt(globalId)); + } + return 0; + } + + void setGlobalVariableValue(const std::string_view globalId, float value) + { + char varType = MWBase::Environment::get().getWorld()->getGlobalVariableType(globalId); + if (varType == 'f') + { + MWBase::Environment::get().getWorld()->setGlobalFloat(globalId, value); + } + else if (varType == 's' || varType == 'l') + { + MWBase::Environment::get().getWorld()->setGlobalInt(globalId, value); + } + } + + sol::table initMWScriptBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + + api["getGlobalScript"] + = [](std::string_view recordId, sol::optional player) -> sol::optional { + if (player.has_value() && player->ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) + throw std::runtime_error("Second argument must either be a player or be missing"); + auto scriptId = ESM::RefId::deserializeText(recordId); + if (MWBase::Environment::get().getScriptManager()->getGlobalScripts().getScriptIfPresent(scriptId)) + return MWScriptRef{ scriptId, sol::nullopt }; + else + return sol::nullopt; + }; + api["getLocalScript"] = [](const GObject& obj, sol::optional player) -> sol::optional { + if (player.has_value() && player->ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) + throw std::runtime_error("Second argument must either be a player or be missing"); + auto scriptId = obj.ptr().getRefData().getLocals().getScriptId(); + if (scriptId.empty()) + return sol::nullopt; + return MWScriptRef{ scriptId, obj }; + }; + + // In multiplayer it will be possible to have several instances (per player) of a single script, + // so we will likely add functions returning Lua table of scripts. + // api["getGlobalScripts"] = [](std::string_view recordId) -> list of scripts + // api["getLocalScripts"] = [](const GObject& obj) -> list of scripts + + sol::usertype mwscript = lua.new_usertype("MWScript"); + sol::usertype mwscriptVars = lua.new_usertype("MWScriptVariables"); + mwscript[sol::meta_function::to_string] + = [](const MWScriptRef& s) { return std::string("MWScript{") + s.mId.toDebugString() + "}"; }; + mwscript["isRunning"] = sol::readonly_property([](const MWScriptRef& s) { return s.isRunning(); }); + mwscript["recordId"] = sol::readonly_property([](const MWScriptRef& s) { return s.mId.serializeText(); }); + mwscript["variables"] = sol::readonly_property([](const MWScriptRef& s) { return MWScriptVariables{ s }; }); + mwscript["object"] = sol::readonly_property([](const MWScriptRef& s) -> sol::optional { + if (s.mObj) + return s.mObj; + const MWScript::GlobalScriptDesc* script + = MWBase::Environment::get().getScriptManager()->getGlobalScripts().getScriptIfPresent(s.mId); + if (!script) + throw std::runtime_error("Invalid MWScriptRef"); + const MWWorld::Ptr* ptr = script->getPtrIfPresent(); + if (ptr && !ptr->isEmpty()) + return GObject(*ptr); + else + return sol::nullopt; + }); + mwscript["player"] = sol::readonly_property( + [](const MWScriptRef&) { return GObject(MWBase::Environment::get().getWorld()->getPlayerPtr()); }); + mwscriptVars[sol::meta_function::length] + = [](MWScriptVariables& s) { return s.mRef.getLocals().getSize(s.mRef.mId); }; + mwscriptVars[sol::meta_function::index] = sol::overload( + [](MWScriptVariables& s, std::string_view var) -> sol::optional { + if (s.mRef.getLocals().hasVar(s.mRef.mId, var)) + return s.mRef.getLocals().getVarAsDouble(s.mRef.mId, Misc::StringUtils::lowerCase(var)); + else + return sol::nullopt; + }, + [](MWScriptVariables& s, std::size_t index) -> sol::optional { + auto& locals = s.mRef.getLocals(); + if (index < 1 || locals.getSize(s.mRef.mId) < index) + return sol::nullopt; + if (index <= locals.mShorts.size()) + return locals.mShorts[index - 1]; + index -= locals.mShorts.size(); + if (index <= locals.mLongs.size()) + return locals.mLongs[index - 1]; + index -= locals.mLongs.size(); + if (index <= locals.mFloats.size()) + return locals.mFloats[index - 1]; + return sol::nullopt; + }); + mwscriptVars[sol::meta_function::new_index] = sol::overload( + [](MWScriptVariables& s, std::string_view var, double val) { + MWScript::Locals& locals = s.mRef.getLocals(); + if (!locals.setVar(s.mRef.mId, Misc::StringUtils::lowerCase(var), val)) + throw std::runtime_error( + "No variable \"" + std::string(var) + "\" in mwscript " + s.mRef.mId.toDebugString()); + }, + [](MWScriptVariables& s, std::size_t index, double val) { + auto& locals = s.mRef.getLocals(); + if (index < 1 || locals.getSize(s.mRef.mId) < index) + throw std::runtime_error("Index out of range in mwscript " + s.mRef.mId.toDebugString()); + if (index <= locals.mShorts.size()) + { + locals.mShorts[index - 1] = static_cast(val); + return; + } + index -= locals.mShorts.size(); + if (index <= locals.mLongs.size()) + { + locals.mLongs[index - 1] = static_cast(val); + return; + } + index -= locals.mLongs.size(); + if (index <= locals.mFloats.size()) + locals.mFloats[index - 1] = static_cast(val); + }); + mwscriptVars[sol::meta_function::pairs] = [](MWScriptVariables& s) { + std::size_t index = 0; + const auto& compilerLocals = MWBase::Environment::get().getScriptManager()->getLocals(s.mRef.mId); + auto& locals = s.mRef.getLocals(); + std::size_t size = locals.getSize(s.mRef.mId); + return sol::as_function( + [&, index, size](sol::this_state ts) mutable -> sol::optional> { + if (index >= size) + return sol::nullopt; + auto i = index++; + if (i < locals.mShorts.size()) + return std::make_tuple(compilerLocals.get('s')[i], locals.mShorts[i]); + i -= locals.mShorts.size(); + if (i < locals.mLongs.size()) + return std::make_tuple(compilerLocals.get('l')[i], locals.mLongs[i]); + i -= locals.mLongs.size(); + if (i < locals.mFloats.size()) + return std::make_tuple(compilerLocals.get('f')[i], locals.mFloats[i]); + return sol::nullopt; + }); + }; + + using GlobalStore = MWWorld::Store; + sol::usertype globalStoreT = lua.new_usertype("ESM3_GlobalStore"); + const GlobalStore* globalStore = &MWBase::Environment::get().getWorld()->getStore().get(); + globalStoreT[sol::meta_function::to_string] = [](const GlobalStore& store) { + return "ESM3_GlobalStore{" + std::to_string(store.getSize()) + " globals}"; + }; + globalStoreT[sol::meta_function::length] = [](const GlobalStore& store) { return store.getSize(); }; + globalStoreT[sol::meta_function::index] = sol::overload( + [](const GlobalStore& store, std::string_view globalId) -> sol::optional { + auto g = store.search(ESM::RefId::deserializeText(globalId)); + if (g == nullptr) + return sol::nullopt; + return getGlobalVariableValue(globalId); + }, + [](const GlobalStore& store, size_t index) -> sol::optional { + if (index < 1 || store.getSize() < index) + return sol::nullopt; + auto g = store.at(LuaUtil::fromLuaIndex(index)); + if (g == nullptr) + return sol::nullopt; + std::string globalId = g->mId.serializeText(); + return getGlobalVariableValue(globalId); + }); + globalStoreT[sol::meta_function::new_index] = sol::overload( + [](const GlobalStore& store, std::string_view globalId, float val) -> void { + auto g = store.search(ESM::RefId::deserializeText(globalId)); + if (g == nullptr) + throw std::runtime_error("No variable \"" + std::string(globalId) + "\" in GlobalStore"); + setGlobalVariableValue(globalId, val); + }, + [](const GlobalStore& store, size_t index, float val) { + if (index < 1 || store.getSize() < index) + return; + auto g = store.at(LuaUtil::fromLuaIndex(index)); + if (g == nullptr) + return; + std::string globalId = g->mId.serializeText(); + setGlobalVariableValue(globalId, val); + }); + globalStoreT[sol::meta_function::pairs] = [](const GlobalStore& store) { + size_t index = 0; + return sol::as_function( + [index, &store](sol::this_state ts) mutable -> sol::optional> { + if (index >= store.getSize()) + return sol::nullopt; + + const ESM::Global* global = store.at(index++); + if (!global) + return sol::nullopt; + + std::string globalId = global->mId.serializeText(); + float value = getGlobalVariableValue(globalId); + + return std::make_tuple(globalId, value); + }); + }; + globalStoreT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + api["getGlobalVariables"] = [globalStore](sol::optional player) { + if (player.has_value() && player->ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) + throw std::runtime_error("First argument must either be a player or be missing"); + + return globalStore; + }; + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/mwscriptbindings.hpp b/apps/openmw/mwlua/mwscriptbindings.hpp new file mode 100644 index 00000000000..9598f051aeb --- /dev/null +++ b/apps/openmw/mwlua/mwscriptbindings.hpp @@ -0,0 +1,15 @@ +#ifndef MWLUA_MWSCRIPTBINDINGS_H +#define MWLUA_MWSCRIPTBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + + sol::table initMWScriptBindings(const Context&); + +} + +#endif // MWLUA_MWSCRIPTBINDINGS_H diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp new file mode 100644 index 00000000000..40367ea45f2 --- /dev/null +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -0,0 +1,353 @@ +#include "nearbybindings.hpp" + +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwphysics/raycasting.hpp" +#include "../mwworld/cell.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/scene.hpp" + +#include "luamanagerimp.hpp" +#include "objectlists.hpp" + +namespace +{ + template + std::vector parseIgnoreList(const sol::table& options) + { + std::vector ignore; + + if (const auto& ignoreObj = options.get>("ignore")) + { + ignore.push_back(ignoreObj->ptr()); + } + else if (const auto& ignoreTable = options.get>("ignore")) + { + ignoreTable->for_each([&](const auto& _, const sol::object& value) { + if (value.is()) + { + ignore.push_back(value.as().ptr()); + } + }); + } + + return ignore; + } +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + sol::table initNearbyPackage(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + ObjectLists* objectLists = context.mObjectLists; + + sol::usertype rayResult + = lua.new_usertype("RayCastingResult"); + rayResult["hit"] = sol::readonly_property([](const MWPhysics::RayCastingResult& r) { return r.mHit; }); + rayResult["hitPos"] + = sol::readonly_property([](const MWPhysics::RayCastingResult& r) -> sol::optional { + if (r.mHit) + return r.mHitPos; + else + return sol::nullopt; + }); + rayResult["hitNormal"] + = sol::readonly_property([](const MWPhysics::RayCastingResult& r) -> sol::optional { + if (r.mHit) + return r.mHitNormal; + else + return sol::nullopt; + }); + rayResult["hitObject"] + = sol::readonly_property([](const MWPhysics::RayCastingResult& r) -> sol::optional { + if (r.mHitObject.isEmpty()) + return sol::nullopt; + else + return LObject(getId(r.mHitObject)); + }); + + api["COLLISION_TYPE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "World", MWPhysics::CollisionType_World }, + { "Door", MWPhysics::CollisionType_Door }, + { "Actor", MWPhysics::CollisionType_Actor }, + { "HeightMap", MWPhysics::CollisionType_HeightMap }, + { "Projectile", MWPhysics::CollisionType_Projectile }, + { "Water", MWPhysics::CollisionType_Water }, + { "Default", MWPhysics::CollisionType_Default }, + { "AnyPhysical", MWPhysics::CollisionType_AnyPhysical }, + { "Camera", MWPhysics::CollisionType_CameraOnly }, + { "VisualOnly", MWPhysics::CollisionType_VisualOnly }, + })); + + api["castRay"] = [](const osg::Vec3f& from, const osg::Vec3f& to, sol::optional options) { + std::vector ignore; + int collisionType = MWPhysics::CollisionType_Default; + float radius = 0; + if (options) + { + ignore = parseIgnoreList(*options); + collisionType = options->get>("collisionType").value_or(collisionType); + radius = options->get>("radius").value_or(0); + } + const MWPhysics::RayCastingInterface* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); + if (radius <= 0) + { + return rayCasting->castRay(from, to, ignore, {}, collisionType); + } + else + { + for (const auto& ptr : ignore) + { + if (!ptr.isEmpty()) + throw std::logic_error("Currently castRay doesn't support `ignore` when radius > 0"); + } + return rayCasting->castSphere(from, to, radius, collisionType); + } + }; + // TODO: async raycasting + /*api["asyncCastRay"] = [luaManager = context.mLuaManager]( + const Callback& luaCallback, const osg::Vec3f& from, const osg::Vec3f& to, sol::optional + options) + { + std::function callback = + luaManager->wrapLuaCallback(luaCallback); + MWPhysics::RayCastingInterface* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); + + // Handle options the same way as in `castRay`. + + // NOTE: `callback` is not thread safe. If MWPhysics works in separate thread, it must put results to a + queue + // and use this callback from the main thread at the beginning of the next frame processing. + rayCasting->asyncCastRay(callback, from, to, ignore, std::vector(), collisionType); + };*/ + api["castRenderingRay"] = [manager = context.mLuaManager](const osg::Vec3f& from, const osg::Vec3f& to, + const sol::optional& options) { + if (!manager->isProcessingInputEvents()) + { + throw std::logic_error( + "castRenderingRay can be used only in player scripts during processing of input events; " + "use asyncCastRenderingRay instead."); + } + + std::vector ignore; + if (options.has_value()) + { + ignore = parseIgnoreList(*options); + } + + MWPhysics::RayCastingResult res; + MWBase::Environment::get().getWorld()->castRenderingRay(res, from, to, false, false, ignore); + return res; + }; + api["asyncCastRenderingRay"] = [context](const sol::table& callback, const osg::Vec3f& from, + const osg::Vec3f& to, const sol::optional& options) { + std::vector ignore; + if (options.has_value()) + { + ignore = parseIgnoreList(*options); + } + + context.mLuaManager->addAction( + [context, ignore = std::move(ignore), callback = LuaUtil::Callback::fromLua(callback), from, to] { + MWPhysics::RayCastingResult res; + MWBase::Environment::get().getWorld()->castRenderingRay(res, from, to, false, false, ignore); + context.mLuaManager->queueCallback( + callback, sol::main_object(context.mLua->unsafeState(), sol::in_place, res)); + }); + }; + + api["getObjectByFormId"] = [](std::string_view formIdStr) -> LObject { + ESM::RefId refId = ESM::RefId::deserializeText(formIdStr); + if (!refId.is()) + throw std::runtime_error("FormId expected, got " + std::string(formIdStr) + "; use core.getFormId"); + return LObject(*refId.getIf()); + }; + + api["activators"] = LObjectList{ objectLists->getActivatorsInScene() }; + api["actors"] = LObjectList{ objectLists->getActorsInScene() }; + api["containers"] = LObjectList{ objectLists->getContainersInScene() }; + api["doors"] = LObjectList{ objectLists->getDoorsInScene() }; + api["items"] = LObjectList{ objectLists->getItemsInScene() }; + api["players"] = LObjectList{ objectLists->getPlayers() }; + + api["NAVIGATOR_FLAGS"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Walk", DetourNavigator::Flag_walk }, + { "Swim", DetourNavigator::Flag_swim }, + { "OpenDoor", DetourNavigator::Flag_openDoor }, + { "UsePathgrid", DetourNavigator::Flag_usePathgrid }, + })); + + api["COLLISION_SHAPE_TYPE"] = LuaUtil::makeStrictReadOnly( + LuaUtil::tableFromPairs(lua, + { + { "Aabb", DetourNavigator::CollisionShapeType::Aabb }, + { "RotatingBox", DetourNavigator::CollisionShapeType::RotatingBox }, + { "Cylinder", DetourNavigator::CollisionShapeType::Cylinder }, + })); + + api["FIND_PATH_STATUS"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Success", DetourNavigator::Status::Success }, + { "PartialPath", DetourNavigator::Status::PartialPath }, + { "NavMeshNotFound", DetourNavigator::Status::NavMeshNotFound }, + { "StartPolygonNotFound", DetourNavigator::Status::StartPolygonNotFound }, + { "EndPolygonNotFound", DetourNavigator::Status::EndPolygonNotFound }, + { "MoveAlongSurfaceFailed", DetourNavigator::Status::MoveAlongSurfaceFailed }, + { "FindPathOverPolygonsFailed", DetourNavigator::Status::FindPathOverPolygonsFailed }, + { "InitNavMeshQueryFailed", DetourNavigator::Status::InitNavMeshQueryFailed }, + { "FindStraightPathFailed", DetourNavigator::Status::FindStraightPathFailed }, + })); + + static const DetourNavigator::AgentBounds defaultAgentBounds{ + Settings::game().mActorCollisionShapeType, + Settings::game().mDefaultActorPathfindHalfExtents, + }; + static constexpr DetourNavigator::Flags defaultIncludeFlags = DetourNavigator::Flag_walk + | DetourNavigator::Flag_swim | DetourNavigator::Flag_openDoor | DetourNavigator::Flag_usePathgrid; + + api["findPath"] + = [](const osg::Vec3f& source, const osg::Vec3f& destination, const sol::optional& options) { + DetourNavigator::AgentBounds agentBounds = defaultAgentBounds; + DetourNavigator::Flags includeFlags = defaultIncludeFlags; + DetourNavigator::AreaCosts areaCosts{}; + float destinationTolerance = 1; + + if (options.has_value()) + { + if (const auto& t = options->get>("agentBounds")) + { + if (const auto& v = t->get>("shapeType")) + agentBounds.mShapeType = *v; + if (const auto& v = t->get>("halfExtents")) + agentBounds.mHalfExtents = *v; + } + if (const auto& v = options->get>("includeFlags")) + includeFlags = *v; + if (const auto& t = options->get>("areaCosts")) + { + if (const auto& v = t->get>("water")) + areaCosts.mWater = *v; + if (const auto& v = t->get>("door")) + areaCosts.mDoor = *v; + if (const auto& v = t->get>("pathgrid")) + areaCosts.mPathgrid = *v; + if (const auto& v = t->get>("ground")) + areaCosts.mGround = *v; + } + if (const auto& v = options->get>("destinationTolerance")) + destinationTolerance = *v; + } + + std::vector result; + + const DetourNavigator::Status status = DetourNavigator::findPath( + *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, source, destination, + includeFlags, areaCosts, destinationTolerance, std::back_inserter(result)); + + return std::make_tuple(status, std::move(result)); + }; + + api["findRandomPointAroundCircle"] = [](const osg::Vec3f& position, float maxRadius, + const sol::optional& options) { + DetourNavigator::AgentBounds agentBounds = defaultAgentBounds; + DetourNavigator::Flags includeFlags = defaultIncludeFlags; + + if (options.has_value()) + { + if (const auto& t = options->get>("agentBounds")) + { + if (const auto& v = t->get>("shapeType")) + agentBounds.mShapeType = *v; + if (const auto& v = t->get>("halfExtents")) + agentBounds.mHalfExtents = *v; + } + if (const auto& v = options->get>("includeFlags")) + includeFlags = *v; + } + + constexpr auto getRandom + = [] { return Misc::Rng::rollProbability(MWBase::Environment::get().getWorld()->getPrng()); }; + + return DetourNavigator::findRandomPointAroundCircle(*MWBase::Environment::get().getWorld()->getNavigator(), + agentBounds, position, maxRadius, includeFlags, getRandom); + }; + + api["castNavigationRay"] + = [](const osg::Vec3f& from, const osg::Vec3f& to, const sol::optional& options) { + DetourNavigator::AgentBounds agentBounds = defaultAgentBounds; + DetourNavigator::Flags includeFlags = defaultIncludeFlags; + + if (options.has_value()) + { + if (const auto& t = options->get>("agentBounds")) + { + if (const auto& v = t->get>("shapeType")) + agentBounds.mShapeType = *v; + if (const auto& v = t->get>("halfExtents")) + agentBounds.mHalfExtents = *v; + } + if (const auto& v = options->get>("includeFlags")) + includeFlags = *v; + } + + return DetourNavigator::raycast( + *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, from, to, includeFlags); + }; + + api["findNearestNavMeshPosition"] = [](const osg::Vec3f& position, const sol::optional& options) { + DetourNavigator::AgentBounds agentBounds = defaultAgentBounds; + std::optional searchAreaHalfExtents; + DetourNavigator::Flags includeFlags = defaultIncludeFlags; + + if (options.has_value()) + { + if (const auto& t = options->get>("agentBounds")) + { + if (const auto& v = t->get>("shapeType")) + agentBounds.mShapeType = *v; + if (const auto& v = t->get>("halfExtents")) + agentBounds.mHalfExtents = *v; + } + if (const auto& v = options->get>("searchAreaHalfExtents")) + searchAreaHalfExtents = *v; + if (const auto& v = options->get>("includeFlags")) + includeFlags = *v; + } + + if (!searchAreaHalfExtents.has_value()) + { + const bool isEsm4 = MWBase::Environment::get().getWorldScene()->getCurrentCell()->getCell()->isEsm4(); + const float halfExtents = isEsm4 + ? (1 + 2 * Constants::ESM4CellGridRadius) * Constants::ESM4CellSizeInUnits + : (1 + 2 * Constants::CellGridRadius) * Constants::CellSizeInUnits; + searchAreaHalfExtents = osg::Vec3f(halfExtents, halfExtents, halfExtents); + } + + return DetourNavigator::findNearestNavMeshPosition(*MWBase::Environment::get().getWorld()->getNavigator(), + agentBounds, position, *searchAreaHalfExtents, includeFlags); + }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/nearbybindings.hpp b/apps/openmw/mwlua/nearbybindings.hpp new file mode 100644 index 00000000000..ee0022898e9 --- /dev/null +++ b/apps/openmw/mwlua/nearbybindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_NEARBYBINDINGS_H +#define MWLUA_NEARBYBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initNearbyPackage(const Context&); +} + +#endif // MWLUA_NEARBYBINDINGS_H diff --git a/apps/openmw/mwlua/object.hpp b/apps/openmw/mwlua/object.hpp new file mode 100644 index 00000000000..d032515314f --- /dev/null +++ b/apps/openmw/mwlua/object.hpp @@ -0,0 +1,82 @@ +#ifndef MWLUA_OBJECT_H +#define MWLUA_OBJECT_H + +#include +#include +#include + +#include + +#include + +#include "../mwworld/ptr.hpp" + +namespace MWLua +{ + // ObjectId is a unique identifier of a game object. + // It can change only if the order of content files was change. + using ObjectId = ESM::RefNum; + inline ObjectId getId(const MWWorld::Ptr& ptr) + { + return ptr.getCellRef().getRefNum(); + } + + // Lua scripts can't use MWWorld::Ptr directly, because lifetime of a script can be longer than lifetime of Ptr. + // `GObject` and `LObject` are intended to be passed to Lua as a userdata. + // It automatically updates the underlying Ptr when needed. + class Object : public MWWorld::SafePtr + { + public: + using SafePtr::SafePtr; + const MWWorld::Ptr& ptr() const + { + const MWWorld::Ptr& res = ptrOrEmpty(); + if (res.isEmpty()) + throw std::runtime_error("Object is not available: " + id().toString()); + return res; + } + }; + + // Used only in local scripts + struct LCell + { + MWWorld::CellStore* mStore; + }; + class LObject : public Object + { + using Object::Object; + }; + + // Used only in global scripts + struct GCell + { + MWWorld::CellStore* mStore; + }; + class GObject : public Object + { + using Object::Object; + }; + + using ObjectIdList = std::shared_ptr>; + template + struct ObjectList + { + ObjectIdList mIds; + }; + using GObjectList = ObjectList; + using LObjectList = ObjectList; + + template + struct Inventory + { + Obj mObj; + }; + + template + struct Owner + { + Obj mObj; + }; +} + +#endif // MWLUA_OBJECT_H diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp new file mode 100644 index 00000000000..6575a719f66 --- /dev/null +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -0,0 +1,704 @@ +#include "objectbindings.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/containerstore.hpp" +#include "../mwworld/localscripts.hpp" +#include "../mwworld/player.hpp" +#include "../mwworld/scene.hpp" +#include "../mwworld/worldmodel.hpp" + +#include "../mwrender/renderingmanager.hpp" + +#include "../mwmechanics/creaturestats.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "luaevents.hpp" +#include "luamanagerimp.hpp" +#include "types/types.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical> : std::false_type + { + }; + template <> + struct is_automagical> : std::false_type + { + }; +} + +namespace MWLua +{ + + namespace + { + MWWorld::CellStore* findCell(const sol::object& cellOrName, const osg::Vec3f& pos) + { + MWWorld::WorldModel* wm = MWBase::Environment::get().getWorldModel(); + MWWorld::CellStore* cell; + if (cellOrName.is()) + cell = cellOrName.as().mStore; + else + { + std::string_view name = LuaUtil::cast(cellOrName); + if (name.empty()) + cell = nullptr; // default exterior worldspace + else + cell = &wm->getCell(name); + } + if (cell != nullptr && !cell->isExterior()) + return cell; + const ESM::RefId worldspace + = cell == nullptr ? ESM::Cell::sDefaultWorldspaceId : cell->getCell()->getWorldSpace(); + return &wm->getExterior(ESM::positionToExteriorCellLocation(pos.x(), pos.y(), worldspace)); + } + + ESM::Position toPos(const osg::Vec3f& pos, const osg::Vec3f& rot) + { + ESM::Position esmPos; + static_assert(sizeof(esmPos) == sizeof(osg::Vec3f) * 2); + std::memcpy(esmPos.pos, &pos, sizeof(osg::Vec3f)); + std::memcpy(esmPos.rot, &rot, sizeof(osg::Vec3f)); + return esmPos; + } + + void teleportPlayer( + MWWorld::CellStore* destCell, const osg::Vec3f& pos, const osg::Vec3f& rot, bool placeOnGround) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::Ptr ptr = world->getPlayerPtr(); + auto& stats = ptr.getClass().getCreatureStats(ptr); + stats.land(true); + stats.setTeleported(true); + world->getPlayer().setTeleported(true); + world->changeToCell(destCell->getCell()->getId(), toPos(pos, rot), false); + MWWorld::Ptr newPtr = world->getPlayerPtr(); + world->moveObject(newPtr, pos); + world->rotateObject(newPtr, rot); + if (placeOnGround) + world->adjustPosition(newPtr, true); + MWBase::Environment::get().getLuaManager()->objectTeleported(newPtr); + } + + void teleportNotPlayer(const MWWorld::Ptr& ptr, MWWorld::CellStore* destCell, const osg::Vec3f& pos, + const osg::Vec3f& rot, bool placeOnGround) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::WorldModel* wm = MWBase::Environment::get().getWorldModel(); + const MWWorld::Class& cls = ptr.getClass(); + if (cls.isActor()) + { + auto& stats = cls.getCreatureStats(ptr); + stats.land(false); + stats.setTeleported(true); + } + const MWWorld::CellStore* srcCell = ptr.getCell(); + MWWorld::Ptr newPtr; + if (srcCell == &wm->getDraftCell()) + { + newPtr = cls.moveToCell(ptr, *destCell, toPos(pos, rot)); + ptr.getCellRef().unsetRefNum(); + ptr.getRefData().setLuaScripts(nullptr); + ptr.getCellRef().setCount(0); + ESM::RefId script = cls.getScript(newPtr); + if (!script.empty()) + world->getLocalScripts().add(script, newPtr); + world->addContainerScripts(newPtr, newPtr.getCell()); + } + else + { + newPtr = world->moveObject(ptr, destCell, pos); + if (srcCell == destCell) + { + ESM::RefId script = cls.getScript(newPtr); + if (!script.empty()) + world->getLocalScripts().add(script, newPtr); + } + world->rotateObject(newPtr, rot, MWBase::RotationFlag_none); + } + if (placeOnGround) + world->adjustPosition(newPtr, true); + if (cls.isDoor()) + { // Change "original position and rotation" because without it teleported animated doors don't work + // properly. + newPtr.getCellRef().setPosition(newPtr.getRefData().getPosition()); + } + if (!newPtr.getRefData().isEnabled()) + world->enable(newPtr); + MWBase::Environment::get().getLuaManager()->objectTeleported(newPtr); + } + + template + using Cell = std::conditional_t, LCell, GCell>; + + template + void registerObjectList(const std::string& prefix, const Context& context) + { + using ListT = ObjectList; + sol::state_view lua = context.sol(); + sol::usertype listT = lua.new_usertype(prefix + "ObjectList"); + listT[sol::meta_function::to_string] + = [](const ListT& list) { return "{" + std::to_string(list.mIds->size()) + " objects}"; }; + listT[sol::meta_function::length] = [](const ListT& list) { return list.mIds->size(); }; + listT[sol::meta_function::index] = [](const ListT& list, size_t index) -> sol::optional { + if (index > 0 && index <= list.mIds->size()) + return ObjectT((*list.mIds)[LuaUtil::fromLuaIndex(index)]); + else + return sol::nullopt; + }; + listT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + listT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + } + + osg::Vec3f toEulerRotation(const sol::object& transform, bool isActor) + { + if (transform.is()) + { + const osg::Quat& q = transform.as().mQ; + return isActor ? Misc::toEulerAnglesXZ(q) : Misc::toEulerAnglesZYX(q); + } + else + { + const osg::Matrixf& m = LuaUtil::cast(transform).mM; + return isActor ? Misc::toEulerAnglesXZ(m) : Misc::toEulerAnglesZYX(m); + } + } + + osg::Quat toQuat(const ESM::Position& pos, bool isActor) + { + if (isActor) + return osg::Quat(pos.rot[0], osg::Vec3(-1, 0, 0)) * osg::Quat(pos.rot[2], osg::Vec3(0, 0, -1)); + else + return Misc::Convert::makeOsgQuat(pos.rot); + } + + template + void addOwnerbindings(sol::usertype& objectT, const std::string& prefix, const Context& context) + { + using OwnerT = Owner; + sol::usertype ownerT = context.sol().new_usertype(prefix + "Owner"); + + ownerT[sol::meta_function::to_string] = [](const OwnerT& o) { return "Owner[" + o.mObj.toString() + "]"; }; + + auto getOwnerRecordId = [](const OwnerT& o) -> sol::optional { + ESM::RefId owner = o.mObj.ptr().getCellRef().getOwner(); + if (owner.empty()) + return sol::nullopt; + else + return owner.serializeText(); + }; + auto setOwnerRecordId = [](const OwnerT& o, sol::optional ownerId) { + if (std::is_same_v && !dynamic_cast(&o.mObj)) + throw std::runtime_error("Local scripts can set an owner only on self"); + const MWWorld::Ptr& ptr = o.mObj.ptr(); + + if (!ownerId) + { + ptr.getCellRef().setOwner(ESM::RefId()); + return; + } + ESM::RefId owner = ESM::RefId::deserializeText(*ownerId); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + if (!store.get().search(owner)) + throw std::runtime_error("Invalid owner record id"); + ptr.getCellRef().setOwner(owner); + }; + ownerT["recordId"] = sol::property(getOwnerRecordId, setOwnerRecordId); + + auto getOwnerFactionId = [](const OwnerT& o) -> sol::optional { + ESM::RefId owner = o.mObj.ptr().getCellRef().getFaction(); + if (owner.empty()) + return sol::nullopt; + else + return owner.serializeText(); + }; + auto setOwnerFactionId = [](const OwnerT& o, sol::optional ownerId) { + ESM::RefId ownerFac; + if (std::is_same_v && !dynamic_cast(&o.mObj)) + throw std::runtime_error("Local scripts can set an owner faction only on self"); + if (!ownerId) + { + o.mObj.ptr().getCellRef().setFaction(ESM::RefId()); + return; + } + ownerFac = ESM::RefId::deserializeText(*ownerId); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + if (!store.get().search(ownerFac)) + throw std::runtime_error("Invalid owner faction id"); + o.mObj.ptr().getCellRef().setFaction(ownerFac); + }; + ownerT["factionId"] = sol::property(getOwnerFactionId, setOwnerFactionId); + + auto getOwnerFactionRank = [](const OwnerT& o) -> sol::optional { + int rank = o.mObj.ptr().getCellRef().getFactionRank(); + if (rank < 0) + return sol::nullopt; + return LuaUtil::toLuaIndex(rank); + }; + auto setOwnerFactionRank = [](const OwnerT& o, sol::optional factionRank) { + if (std::is_same_v && !dynamic_cast(&o.mObj)) + throw std::runtime_error("Local scripts can set an owner faction rank only on self"); + o.mObj.ptr().getCellRef().setFactionRank(LuaUtil::fromLuaIndex(factionRank.value_or(0))); + }; + ownerT["factionRank"] = sol::property(getOwnerFactionRank, setOwnerFactionRank); + + objectT["owner"] = sol::readonly_property([](const ObjectT& object) { return OwnerT{ object }; }); + } + + template + void addBasicBindings(sol::usertype& objectT, const Context& context) + { + objectT["id"] = sol::readonly_property([](const ObjectT& o) -> std::string { return o.id().toString(); }); + objectT["contentFile"] = sol::readonly_property([](const ObjectT& o) -> sol::optional { + int contentFileIndex = o.id().mContentFile; + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + if (contentFileIndex < 0 || contentFileIndex >= static_cast(contentList.size())) + return sol::nullopt; + return Misc::StringUtils::lowerCase(contentList[contentFileIndex]); + }); + objectT["isValid"] = [](const ObjectT& o) { return !o.ptrOrEmpty().isEmpty(); }; + objectT["recordId"] = sol::readonly_property( + [](const ObjectT& o) -> std::string { return o.ptr().getCellRef().getRefId().serializeText(); }); + objectT["globalVariable"] = sol::readonly_property([](const ObjectT& o) -> sol::optional { + std::string_view globalVariable = o.ptr().getCellRef().getGlobalVariable(); + if (globalVariable.empty()) + return sol::nullopt; + else + return ESM::RefId::stringRefId(globalVariable).serializeText(); + }); + objectT["cell"] = sol::readonly_property([](const ObjectT& o) -> sol::optional> { + const MWWorld::Ptr& ptr = o.ptr(); + MWWorld::WorldModel* wm = MWBase::Environment::get().getWorldModel(); + if (ptr.isInCell() && ptr.getCell() != &wm->getDraftCell()) + return Cell{ ptr.getCell() }; + else + return sol::nullopt; + }); + objectT["parentContainer"] = sol::readonly_property([](const ObjectT& o) -> sol::optional { + const MWWorld::Ptr& ptr = o.ptr(); + if (ptr.getContainerStore()) + return ObjectT(ptr.getContainerStore()->getPtr()); + else + return sol::nullopt; + }); + objectT["position"] = sol::readonly_property( + [](const ObjectT& o) -> osg::Vec3f { return o.ptr().getRefData().getPosition().asVec3(); }); + objectT["scale"] + = sol::readonly_property([](const ObjectT& o) -> float { return o.ptr().getCellRef().getScale(); }); + objectT["rotation"] = sol::readonly_property([](const ObjectT& o) -> LuaUtil::TransformQ { + return { toQuat(o.ptr().getRefData().getPosition(), o.ptr().getClass().isActor()) }; + }); + objectT["startingPosition"] = sol::readonly_property( + [](const ObjectT& o) -> osg::Vec3f { return o.ptr().getCellRef().getPosition().asVec3(); }); + objectT["startingRotation"] = sol::readonly_property([](const ObjectT& o) -> LuaUtil::TransformQ { + return { toQuat(o.ptr().getCellRef().getPosition(), o.ptr().getClass().isActor()) }; + }); + objectT["getBoundingBox"] = [](const ObjectT& o) { + MWRender::RenderingManager* renderingManager + = MWBase::Environment::get().getWorld()->getRenderingManager(); + osg::BoundingBox bb = renderingManager->getCullSafeBoundingBox(o.ptr()); + return LuaUtil::Box{ bb.center(), bb._max - bb.center() }; + }; + + objectT["type"] + = sol::readonly_property([types = getTypeToPackageTable(context.sol())](const ObjectT& o) mutable { + return types[getLiveCellRefType(o.ptr().mRef)]; + }); + + objectT["count"] = sol::readonly_property([](const ObjectT& o) { return o.ptr().getCellRef().getCount(); }); + objectT[sol::meta_function::equal_to] = [](const ObjectT& a, const ObjectT& b) { return a.id() == b.id(); }; + objectT[sol::meta_function::to_string] = &ObjectT::toString; + objectT["sendEvent"] = [context](const ObjectT& dest, std::string eventName, const sol::object& eventData) { + context.mLuaEvents->addLocalEvent( + { dest.id(), std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); + }; + + objectT["activateBy"] = [](const ObjectT& object, const ObjectT& actor) { + const MWWorld::Ptr& objPtr = object.ptr(); + const MWWorld::Ptr& actorPtr = actor.ptr(); + uint32_t esmRecordType = actorPtr.getType(); + if (esmRecordType != ESM::REC_CREA && esmRecordType != ESM::REC_NPC_) + throw std::runtime_error( + "The argument of `activateBy` must be an actor who activates the object. Got: " + + actor.toString()); + if (objPtr.getRefData().activate()) + MWBase::Environment::get().getLuaManager()->objectActivated(objPtr, actorPtr); + }; + + auto isEnabled = [](const ObjectT& o) { return o.ptr().getRefData().isEnabled(); }; + auto setEnabled = [context](const GObject& object, bool enable) { + if (enable && object.ptr().mRef->isDeleted()) + throw std::runtime_error("Object is removed"); + context.mLuaManager->addAction([object, enable] { + if (object.ptr().mRef->isDeleted()) + return; + if (object.ptr().isInCell()) + { + if (enable) + MWBase::Environment::get().getWorld()->enable(object.ptr()); + else + MWBase::Environment::get().getWorld()->disable(object.ptr()); + } + else + { + if (enable) + object.ptr().getRefData().enable(); + else + throw std::runtime_error("Objects in containers can't be disabled"); + } + }); + }; + if constexpr (std::is_same_v) + objectT["enabled"] = sol::property(isEnabled, setEnabled); + else + objectT["enabled"] = sol::readonly_property(isEnabled); + + if constexpr (std::is_same_v) + { // Only for global scripts + objectT["setScale"] = [context](const GObject& object, float scale) { + context.mLuaManager->addAction( + [object, scale] { MWBase::Environment::get().getWorld()->scaleObject(object.ptr(), scale); }); + }; + objectT["addScript"] = [context](const GObject& object, std::string_view path, sol::object initData) { + const LuaUtil::ScriptsConfiguration& cfg = context.mLua->getConfiguration(); + std::optional scriptId = cfg.findId(VFS::Path::Normalized(path)); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + if (!(cfg[*scriptId].mFlags & ESM::LuaScriptCfg::sCustom)) + throw std::runtime_error( + "Script without CUSTOM tag can not be added dynamically: " + std::string(path)); + if (object.ptr().getType() == ESM::REC_STAT) + throw std::runtime_error("Attaching scripts to Static is not allowed: " + std::string(path)); + if (initData != sol::nil) + context.mLuaManager->addCustomLocalScript(object.ptr(), *scriptId, + LuaUtil::serialize(LuaUtil::cast(initData), context.mSerializer)); + else + context.mLuaManager->addCustomLocalScript( + object.ptr(), *scriptId, cfg[*scriptId].mInitializationData); + }; + objectT["hasScript"] = [lua = context.mLua](const GObject& object, std::string_view path) { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(VFS::Path::Normalized(path)); + if (!scriptId) + return false; + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (localScripts) + return localScripts->hasScript(*scriptId); + else + return false; + }; + objectT["removeScript"] = [lua = context.mLua](const GObject& object, std::string_view path) { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(VFS::Path::Normalized(path)); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts || !localScripts->hasScript(*scriptId)) + throw std::runtime_error("There is no script " + std::string(path) + " on " + ptr.toString()); + if (localScripts->getAutoStartConf().count(*scriptId) > 0) + throw std::runtime_error("Autostarted script can not be removed: " + std::string(path)); + localScripts->removeScript(*scriptId); + }; + + using DelayedRemovalFn = std::function; + auto removeFn = [](const MWWorld::Ptr ptr, int countToRemove) -> std::optional { + int rawCount = ptr.getCellRef().getCount(false); + int currentCount = std::abs(rawCount); + int signedCountToRemove = (rawCount < 0 ? -1 : 1) * countToRemove; + + if (countToRemove <= 0 || countToRemove > currentCount) + throw std::runtime_error("Can't remove " + std::to_string(countToRemove) + " of " + + std::to_string(currentCount) + " items"); + ptr.getCellRef().setCount(rawCount - signedCountToRemove); // Immediately change count + if (!ptr.getContainerStore() && currentCount > countToRemove) + return std::nullopt; + // Delayed action to trigger side effects + return [signedCountToRemove](MWWorld::Ptr ptr) { + // Restore the original count + ptr.getCellRef().setCount(ptr.getCellRef().getCount(false) + signedCountToRemove); + // And now remove properly + if (ptr.getContainerStore()) + ptr.getContainerStore()->remove(ptr, std::abs(signedCountToRemove), false); + else + { + MWBase::Environment::get().getWorld()->disable(ptr); + MWBase::Environment::get().getWorld()->deleteObject(ptr); + } + }; + }; + objectT["remove"] = [removeFn, context](const GObject& object, sol::optional count) { + std::optional delayed + = removeFn(object.ptr(), count.value_or(object.ptr().getCellRef().getCount())); + if (delayed.has_value()) + context.mLuaManager->addAction([fn = *delayed, object] { fn(object.ptr()); }); + }; + objectT["split"] = [removeFn, context](const GObject& object, int count) -> GObject { + // Doesn't matter which cell to use because the new instance will be in disabled state. + MWWorld::CellStore* cell = MWBase::Environment::get().getWorldScene()->getCurrentCell(); + + const MWWorld::Ptr& ptr = object.ptr(); + MWWorld::Ptr splitted = ptr.getClass().copyToCell(ptr, *cell, count); + splitted.getRefData().disable(); + + std::optional delayedRemovalFn = removeFn(ptr, count); + if (delayedRemovalFn.has_value()) + context.mLuaManager->addAction([fn = *delayedRemovalFn, object] { fn(object.ptr()); }); + + return GObject(splitted); + }; + objectT["moveInto"] = [removeFn, context](const GObject& object, const sol::object& dest) { + const MWWorld::Ptr& ptr = object.ptr(); + int count = ptr.getCellRef().getCount(); + MWWorld::Ptr destPtr; + if (dest.is()) + destPtr = dest.as().ptr(); + else + destPtr = LuaUtil::cast>(dest).mObj.ptr(); + destPtr.getClass().getContainerStore(destPtr); // raises an error if there is no container store + + std::optional delayedRemovalFn = removeFn(ptr, count); + context.mLuaManager->addAction([item = object, count, cont = GObject(destPtr), delayedRemovalFn] { + const MWWorld::Ptr& oldPtr = item.ptr(); + auto& refData = oldPtr.getCellRef(); + refData.setCount(count); // temporarily undo removal to run ContainerStore::add + oldPtr.getRefData().enable(); + cont.ptr().getClass().getContainerStore(cont.ptr()).add(oldPtr, count, false); + refData.setCount(0); + if (delayedRemovalFn.has_value()) + (*delayedRemovalFn)(oldPtr); + }); + }; + objectT["teleport"] = [removeFn, context](const GObject& object, const sol::object& cellOrName, + const osg::Vec3f& pos, const sol::object& options) { + MWWorld::CellStore* cell = findCell(cellOrName, pos); + MWWorld::Ptr ptr = object.ptr(); + int count = ptr.getCellRef().getCount(); + if (count == 0) + throw std::runtime_error("Object is either removed or already in the process of teleporting"); + osg::Vec3f rot = ptr.getRefData().getPosition().asRotationVec3(); + bool placeOnGround = false; + if (LuaUtil::isTransform(options)) + rot = toEulerRotation(options, ptr.getClass().isActor()); + else if (options != sol::nil) + { + sol::table t = LuaUtil::cast(options); + sol::object rotationArg = t["rotation"]; + if (rotationArg != sol::nil) + rot = toEulerRotation(rotationArg, ptr.getClass().isActor()); + placeOnGround = LuaUtil::getValueOrDefault(t["onGround"], placeOnGround); + } + if (ptr.getContainerStore()) + { + DelayedRemovalFn delayedRemovalFn = *removeFn(ptr, count); + context.mLuaManager->addAction( + [object, cell, pos, rot, count, delayedRemovalFn, placeOnGround] { + MWWorld::Ptr oldPtr = object.ptr(); + oldPtr.getCellRef().setCount(count); + MWWorld::Ptr newPtr = oldPtr.getClass().moveToCell(oldPtr, *cell); + oldPtr.getCellRef().setCount(0); + newPtr.getRefData().disable(); + teleportNotPlayer(newPtr, cell, pos, rot, placeOnGround); + delayedRemovalFn(oldPtr); + }, + "TeleportFromContainerAction"); + } + else if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + context.mLuaManager->addTeleportPlayerAction( + [cell, pos, rot, placeOnGround] { teleportPlayer(cell, pos, rot, placeOnGround); }); + else + { + ptr.getCellRef().setCount(0); + context.mLuaManager->addAction( + [object, cell, pos, rot, count, placeOnGround] { + object.ptr().getCellRef().setCount(count); + teleportNotPlayer(object.ptr(), cell, pos, rot, placeOnGround); + }, + "TeleportAction"); + } + }; + } + } + + template + void addInventoryBindings(sol::usertype& objectT, const std::string& prefix, const Context& context) + { + using InventoryT = Inventory; + sol::usertype inventoryT = context.sol().new_usertype(prefix + "Inventory"); + + inventoryT[sol::meta_function::to_string] + = [](const InventoryT& inv) { return "Inventory[" + inv.mObj.toString() + "]"; }; + + inventoryT["getAll"] = [ids = getPackageToTypeTable(context.mLua->unsafeState())]( + const InventoryT& inventory, sol::optional type) { + int mask = -1; + sol::optional typeId = sol::nullopt; + if (type.has_value()) + typeId = ids[*type]; + else + mask = MWWorld::ContainerStore::Type_All; + + if (typeId.has_value()) + { + switch (*typeId) + { + case ESM::REC_ALCH: + mask = MWWorld::ContainerStore::Type_Potion; + break; + case ESM::REC_ARMO: + mask = MWWorld::ContainerStore::Type_Armor; + break; + case ESM::REC_BOOK: + mask = MWWorld::ContainerStore::Type_Book; + break; + case ESM::REC_CLOT: + mask = MWWorld::ContainerStore::Type_Clothing; + break; + case ESM::REC_INGR: + mask = MWWorld::ContainerStore::Type_Ingredient; + break; + case ESM::REC_LIGH: + mask = MWWorld::ContainerStore::Type_Light; + break; + case ESM::REC_MISC: + mask = MWWorld::ContainerStore::Type_Miscellaneous; + break; + case ESM::REC_WEAP: + mask = MWWorld::ContainerStore::Type_Weapon; + break; + case ESM::REC_APPA: + mask = MWWorld::ContainerStore::Type_Apparatus; + break; + case ESM::REC_LOCK: + mask = MWWorld::ContainerStore::Type_Lockpick; + break; + case ESM::REC_PROB: + mask = MWWorld::ContainerStore::Type_Probe; + break; + case ESM::REC_REPA: + mask = MWWorld::ContainerStore::Type_Repair; + break; + default:; + } + } + + if (mask == -1) + throw std::runtime_error( + std::string("Incorrect type argument in inventory:getAll: " + LuaUtil::toString(*type))); + + const MWWorld::Ptr& ptr = inventory.mObj.ptr(); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + ObjectIdList list = std::make_shared>(); + auto it = store.begin(mask); + while (it.getType() != -1) + { + const MWWorld::Ptr& item = *(it++); + MWBase::Environment::get().getWorldModel()->registerPtr(item); + list->push_back(getId(item)); + } + return ObjectList{ std::move(list) }; + }; + + inventoryT["countOf"] = [](const InventoryT& inventory, std::string_view recordId) { + const MWWorld::Ptr& ptr = inventory.mObj.ptr(); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + return store.count(ESM::RefId::deserializeText(recordId)); + }; + if constexpr (std::is_same_v) + { + inventoryT["resolve"] = [](const InventoryT& inventory) { + const MWWorld::Ptr& ptr = inventory.mObj.ptr(); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + store.resolve(); + }; + } + inventoryT["isResolved"] = [](const InventoryT& inventory) -> bool { + const MWWorld::Ptr& ptr = inventory.mObj.ptr(); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + return store.isResolved(); + }; + inventoryT["find"] = [](const InventoryT& inventory, std::string_view recordId) -> sol::optional { + const MWWorld::Ptr& ptr = inventory.mObj.ptr(); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + auto itemId = ESM::RefId::deserializeText(recordId); + for (const MWWorld::Ptr& item : store) + { + if (item.getCellRef().getRefId() == itemId) + { + MWBase::Environment::get().getWorldModel()->registerPtr(item); + return ObjectT(getId(item)); + } + } + return sol::nullopt; + }; + inventoryT["findAll"] = [](const InventoryT& inventory, std::string_view recordId) { + const MWWorld::Ptr& ptr = inventory.mObj.ptr(); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + auto itemId = ESM::RefId::deserializeText(recordId); + ObjectIdList list = std::make_shared>(); + for (const MWWorld::Ptr& item : store) + { + if (item.getCellRef().getRefId() == itemId) + { + MWBase::Environment::get().getWorldModel()->registerPtr(item); + list->push_back(getId(item)); + } + } + return ObjectList{ std::move(list) }; + }; + } + + template + void initObjectBindings(const std::string& prefix, const Context& context) + { + sol::usertype objectT + = context.sol().new_usertype(prefix + "Object", sol::base_classes, sol::bases()); + addBasicBindings(objectT, context); + addInventoryBindings(objectT, prefix, context); + addOwnerbindings(objectT, prefix, context); + + registerObjectList(prefix, context); + } + } // namespace + + void initObjectBindingsForLocalScripts(const Context& context) + { + initObjectBindings("L", context); + } + + void initObjectBindingsForGlobalScripts(const Context& context) + { + initObjectBindings("G", context); + } + +} diff --git a/apps/openmw/mwlua/objectbindings.hpp b/apps/openmw/mwlua/objectbindings.hpp new file mode 100644 index 00000000000..5f69c54da3e --- /dev/null +++ b/apps/openmw/mwlua/objectbindings.hpp @@ -0,0 +1,12 @@ +#ifndef MWLUA_OBJECTBINDINGS_H +#define MWLUA_OBJECTBINDINGS_H + +#include "context.hpp" + +namespace MWLua +{ + void initObjectBindingsForLocalScripts(const Context&); + void initObjectBindingsForGlobalScripts(const Context&); +} + +#endif // MWLUA_OBJECTBINDINGS_H diff --git a/apps/openmw/mwlua/objectlists.cpp b/apps/openmw/mwlua/objectlists.cpp new file mode 100644 index 00000000000..d0bda5a644e --- /dev/null +++ b/apps/openmw/mwlua/objectlists.cpp @@ -0,0 +1,101 @@ +#include "objectlists.hpp" + +#include +#include +#include + +#include + +#include "../mwbase/environment.hpp" + +#include "../mwclass/container.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/worldmodel.hpp" + +namespace MWLua +{ + + void ObjectLists::update() + { + mActivatorsInScene.updateList(); + mActorsInScene.updateList(); + mContainersInScene.updateList(); + mDoorsInScene.updateList(); + mItemsInScene.updateList(); + } + + void ObjectLists::clear() + { + mActivatorsInScene.clear(); + mActorsInScene.clear(); + mContainersInScene.clear(); + mDoorsInScene.clear(); + mItemsInScene.clear(); + } + + ObjectLists::ObjectGroup* ObjectLists::chooseGroup(const MWWorld::Ptr& ptr) + { + // It is important to check `isMarker` first. + // For example "prisonmarker" has class "Door" despite that it is only an invisible marker. + if (Misc::ResourceHelpers::isHiddenMarker(ptr.getCellRef().getRefId())) + return nullptr; + const MWWorld::Class& cls = ptr.getClass(); + if (cls.isActivator()) + return &mActivatorsInScene; + if (cls.isActor()) + return &mActorsInScene; + if (ptr.mRef->getType() == ESM::REC_DOOR || ptr.mRef->getType() == ESM::REC_DOOR4) + return &mDoorsInScene; + if (typeid(cls) == typeid(MWClass::Container)) + return &mContainersInScene; + if (cls.isItem(ptr) || ptr.mRef->getType() == ESM::REC_LIGH) + return &mItemsInScene; + return nullptr; + } + + void ObjectLists::objectAddedToScene(const MWWorld::Ptr& ptr) + { + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + ObjectGroup* group = chooseGroup(ptr); + if (group) + addToGroup(*group, ptr); + } + + void ObjectLists::objectRemovedFromScene(const MWWorld::Ptr& ptr) + { + ObjectGroup* group = chooseGroup(ptr); + if (group) + removeFromGroup(*group, ptr); + } + + void ObjectLists::ObjectGroup::updateList() + { + if (mChanged) + { + mList->clear(); + for (ObjectId id : mSet) + mList->push_back(id); + mChanged = false; + } + } + + void ObjectLists::ObjectGroup::clear() + { + mChanged = false; + mList->clear(); + mSet.clear(); + } + + void ObjectLists::addToGroup(ObjectGroup& group, const MWWorld::Ptr& ptr) + { + group.mSet.insert(getId(ptr)); + group.mChanged = true; + } + + void ObjectLists::removeFromGroup(ObjectGroup& group, const MWWorld::Ptr& ptr) + { + group.mSet.erase(getId(ptr)); + group.mChanged = true; + } +} diff --git a/apps/openmw/mwlua/objectlists.hpp b/apps/openmw/mwlua/objectlists.hpp new file mode 100644 index 00000000000..d96b83fd6d7 --- /dev/null +++ b/apps/openmw/mwlua/objectlists.hpp @@ -0,0 +1,55 @@ +#ifndef MWLUA_OBJECTLISTS_H +#define MWLUA_OBJECTLISTS_H + +#include + +#include "object.hpp" + +namespace MWLua +{ + + // ObjectLists is used to track lists of game objects like nearby.items, nearby.actors, etc. + class ObjectLists + { + public: + void update(); // Should be called every frame. + void clear(); // Should be called every time before starting or loading a new game. + + ObjectIdList getActivatorsInScene() const { return mActivatorsInScene.mList; } + ObjectIdList getActorsInScene() const { return mActorsInScene.mList; } + ObjectIdList getContainersInScene() const { return mContainersInScene.mList; } + ObjectIdList getDoorsInScene() const { return mDoorsInScene.mList; } + ObjectIdList getItemsInScene() const { return mItemsInScene.mList; } + ObjectIdList getPlayers() const { return mPlayers; } + + void objectAddedToScene(const MWWorld::Ptr& ptr); + void objectRemovedFromScene(const MWWorld::Ptr& ptr); + + void setPlayer(const MWWorld::Ptr& player) { *mPlayers = { getId(player) }; } + + private: + struct ObjectGroup + { + void updateList(); + void clear(); + + bool mChanged = false; + ObjectIdList mList = std::make_shared>(); + std::set mSet; + }; + + ObjectGroup* chooseGroup(const MWWorld::Ptr& ptr); + void addToGroup(ObjectGroup& group, const MWWorld::Ptr& ptr); + void removeFromGroup(ObjectGroup& group, const MWWorld::Ptr& ptr); + + ObjectGroup mActivatorsInScene; + ObjectGroup mActorsInScene; + ObjectGroup mContainersInScene; + ObjectGroup mDoorsInScene; + ObjectGroup mItemsInScene; + ObjectIdList mPlayers = std::make_shared>(); + }; + +} + +#endif // MWLUA_OBJECTLISTS_H diff --git a/apps/openmw/mwlua/objectvariant.hpp b/apps/openmw/mwlua/objectvariant.hpp new file mode 100644 index 00000000000..238ad7cfafe --- /dev/null +++ b/apps/openmw/mwlua/objectvariant.hpp @@ -0,0 +1,59 @@ +#ifndef MWLUA_OBJECTVARIANT_H +#define MWLUA_OBJECTVARIANT_H + +#include + +#include "localscripts.hpp" +#include "object.hpp" + +namespace MWLua +{ + + class ObjectVariant + { + public: + explicit ObjectVariant(const sol::object& obj) + { + if (obj.is()) + mVariant.emplace(obj.as()); + else if (obj.is()) + mVariant.emplace(obj.as()); + else if (obj.is()) + mVariant.emplace(obj.as()); + else + throw std::runtime_error("Expected game object, got: " + LuaUtil::toString(obj)); + } + + bool isSelfObject() const { return std::holds_alternative(mVariant); } + bool isLObject() const { return std::holds_alternative(mVariant); } + bool isGObject() const { return std::holds_alternative(mVariant); } + + SelfObject* asSelfObject() const + { + if (!isSelfObject()) + throw std::runtime_error("Allowed only in local scripts for 'openmw.self'."); + return std::get(mVariant); + } + + const MWWorld::Ptr& ptr() const + { + return std::visit( + [](auto&& variant) -> const MWWorld::Ptr& { + using T = std::decay_t; + if constexpr (std::is_same_v) + return variant->ptr(); + else + return variant.ptr(); + }, + mVariant); + } + + Object object() const { return Object(ptr()); } + + private: + std::variant mVariant; + }; + +} // namespace MWLua + +#endif // MWLUA_OBJECTVARIANT_H diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp new file mode 100644 index 00000000000..ea7baccb767 --- /dev/null +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -0,0 +1,61 @@ +#ifndef MWLUA_PLAYERSCRIPTS_H +#define MWLUA_PLAYERSCRIPTS_H + +#include + +#include + +#include "../mwbase/luamanager.hpp" + +#include "inputprocessor.hpp" +#include "localscripts.hpp" + +namespace MWLua +{ + + class PlayerScripts : public LocalScripts + { + public: + PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) + : LocalScripts(lua, obj) + , mInputProcessor(this) + { + registerEngineHandlers({ &mConsoleCommandHandlers, &mOnFrameHandlers, &mQuestUpdate, &mUiModeChanged }); + } + + void processInputEvent(const MWBase::LuaManager::InputEvent& event) + { + mInputProcessor.processInputEvent(event); + } + + void onFrame(float dt) { callEngineHandlers(mOnFrameHandlers, dt); } + void onQuestUpdate(std::string_view questId, int stage) { callEngineHandlers(mQuestUpdate, questId, stage); } + + bool consoleCommand( + const std::string& consoleMode, const std::string& command, const sol::object& selectedObject) + { + callEngineHandlers(mConsoleCommandHandlers, consoleMode, command, selectedObject); + return !mConsoleCommandHandlers.mList.empty(); + } + + // `arg` is either forwarded from MWGui::pushGuiMode or empty + void uiModeChanged(ObjectId arg, bool byLuaAction) + { + if (arg.isZeroOrUnset()) + callEngineHandlers(mUiModeChanged, byLuaAction); + else + callEngineHandlers(mUiModeChanged, byLuaAction, LObject(arg)); + } + + private: + friend class MWLua::InputProcessor; + InputProcessor mInputProcessor; + EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" }; + EngineHandlerList mOnFrameHandlers{ "onFrame" }; + EngineHandlerList mQuestUpdate{ "onQuestUpdate" }; + EngineHandlerList mUiModeChanged{ "_onUiModeChanged" }; + }; + +} + +#endif // MWLUA_PLAYERSCRIPTS_H diff --git a/apps/openmw/mwlua/postprocessingbindings.cpp b/apps/openmw/mwlua/postprocessingbindings.cpp new file mode 100644 index 00000000000..e64bf0fa9e4 --- /dev/null +++ b/apps/openmw/mwlua/postprocessingbindings.cpp @@ -0,0 +1,170 @@ +#include "postprocessingbindings.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwrender/postprocessor.hpp" + +#include "luamanagerimp.hpp" + +namespace MWLua +{ + struct Shader; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + struct Shader + { + std::shared_ptr mShader; + + Shader(std::shared_ptr shader) + : mShader(std::move(shader)) + { + } + + std::string toString() const + { + if (!mShader) + return "Shader(nil)"; + + return Misc::StringUtils::format("Shader(%s, %s)", mShader->getName(), mShader->getFileName()); + } + + enum + { + Action_None, + Action_Enable, + Action_Disable + } mQueuedAction + = Action_None; + }; + + template + auto getSetter(const Context& context) + { + return [context](const Shader& shader, const std::string& name, const T& value) { + context.mLuaManager->addAction( + [=] { + MWBase::Environment::get().getWorld()->getPostProcessor()->setUniform(shader.mShader, name, value); + }, + "SetUniformShaderAction"); + }; + } + + template + auto getArraySetter(const Context& context) + { + return [context](const Shader& shader, const std::string& name, const sol::table& table) { + auto targetSize + = MWBase::Environment::get().getWorld()->getPostProcessor()->getUniformSize(shader.mShader, name); + + if (!targetSize.has_value()) + throw std::runtime_error(Misc::StringUtils::format("Failed setting uniform array '%s'", name)); + + if (*targetSize != table.size()) + throw std::runtime_error(Misc::StringUtils::format( + "Mismatching uniform array size, got %zu expected %zu", table.size(), *targetSize)); + + std::vector values; + values.reserve(*targetSize); + + for (size_t i = 0; i < *targetSize; ++i) + { + sol::object obj = table[LuaUtil::toLuaIndex(i)]; + if (!obj.is()) + throw std::runtime_error("Invalid type for uniform array"); + values.push_back(obj.as()); + } + + context.mLuaManager->addAction( + [shader, name, values = std::move(values)] { + MWBase::Environment::get().getWorld()->getPostProcessor()->setUniform(shader.mShader, name, values); + }, + "SetUniformShaderAction"); + }; + } + + sol::table initPostprocessingPackage(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + + sol::usertype shader = lua.new_usertype("Shader"); + shader[sol::meta_function::to_string] = [](const Shader& shader) { return shader.toString(); }; + + shader["enable"] = [context](Shader& shader, sol::optional optPos) { + std::optional pos = std::nullopt; + if (optPos) + pos = optPos.value(); + + if (shader.mShader && shader.mShader->isValid()) + shader.mQueuedAction = Shader::Action_Enable; + + context.mLuaManager->addAction([=, &shader] { + shader.mQueuedAction = Shader::Action_None; + + if (MWBase::Environment::get().getWorld()->getPostProcessor()->enableTechnique(shader.mShader, pos) + == MWRender::PostProcessor::Status_Error) + throw std::runtime_error("Failed enabling shader '" + shader.mShader->getName() + "'"); + }); + }; + + shader["disable"] = [context](Shader& shader) { + shader.mQueuedAction = Shader::Action_Disable; + + context.mLuaManager->addAction([&] { + shader.mQueuedAction = Shader::Action_None; + + if (MWBase::Environment::get().getWorld()->getPostProcessor()->disableTechnique(shader.mShader) + == MWRender::PostProcessor::Status_Error) + throw std::runtime_error("Failed disabling shader '" + shader.mShader->getName() + "'"); + }); + }; + + shader["isEnabled"] = [](const Shader& shader) { + if (shader.mQueuedAction == Shader::Action_Enable) + return true; + else if (shader.mQueuedAction == Shader::Action_Disable) + return false; + return MWBase::Environment::get().getWorld()->getPostProcessor()->isTechniqueEnabled(shader.mShader); + }; + + shader["setBool"] = getSetter(context); + shader["setFloat"] = getSetter(context); + shader["setInt"] = getSetter(context); + shader["setVector2"] = getSetter(context); + shader["setVector3"] = getSetter(context); + shader["setVector4"] = getSetter(context); + + shader["setFloatArray"] = getArraySetter(context); + shader["setIntArray"] = getArraySetter(context); + shader["setVector2Array"] = getArraySetter(context); + shader["setVector3Array"] = getArraySetter(context); + shader["setVector4Array"] = getArraySetter(context); + + api["load"] = [](const std::string& name) { + Shader shader{ MWBase::Environment::get().getWorld()->getPostProcessor()->loadTechnique(name, false) }; + + 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; + }; + + return LuaUtil::makeReadOnly(api); + } + +} diff --git a/apps/openmw/mwlua/postprocessingbindings.hpp b/apps/openmw/mwlua/postprocessingbindings.hpp new file mode 100644 index 00000000000..50cd84fa247 --- /dev/null +++ b/apps/openmw/mwlua/postprocessingbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_POSTPROCESSINGBINDINGS_H +#define MWLUA_POSTPROCESSINGBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initPostprocessingPackage(const Context&); +} + +#endif // MWLUA_POSTPROCESSINGBINDINGS_H diff --git a/apps/openmw/mwlua/racebindings.cpp b/apps/openmw/mwlua/racebindings.cpp new file mode 100644 index 00000000000..b5c4d6093f6 --- /dev/null +++ b/apps/openmw/mwlua/racebindings.cpp @@ -0,0 +1,116 @@ +#include "racebindings.hpp" + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwworld/esmstore.hpp" + +#include "idcollectionbindings.hpp" +#include "types/types.hpp" + +namespace +{ + struct RaceAttributes + { + const ESM::Race& mRace; + const sol::state_view mLua; + + sol::table getAttribute(ESM::RefId id) const + { + sol::table res(mLua, sol::create); + res["male"] = mRace.mData.getAttribute(id, true); + res["female"] = mRace.mData.getAttribute(id, false); + return LuaUtil::makeReadOnly(res); + } + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + sol::table initRaceRecordBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table races(lua, sol::create); + addRecordFunctionBinding(races, context); + + auto raceT = lua.new_usertype("ESM3_Race"); + raceT[sol::meta_function::to_string] + = [](const ESM::Race& rec) -> std::string { return "ESM3_Race[" + rec.mId.toDebugString() + "]"; }; + raceT["id"] = sol::readonly_property([](const ESM::Race& rec) { return rec.mId.serializeText(); }); + raceT["name"] = sol::readonly_property([](const ESM::Race& rec) -> std::string_view { return rec.mName; }); + raceT["description"] + = sol::readonly_property([](const ESM::Race& rec) -> std::string_view { return rec.mDescription; }); + raceT["spells"] = sol::readonly_property( + [lua](const ESM::Race& rec) -> sol::table { return createReadOnlyRefIdTable(lua, rec.mPowers.mList); }); + raceT["skills"] = sol::readonly_property([lua](const ESM::Race& rec) -> sol::table { + sol::table res(lua, sol::create); + for (const auto& skillBonus : rec.mData.mBonus) + { + ESM::RefId skill = ESM::Skill::indexToRefId(skillBonus.mSkill); + if (!skill.empty()) + res[skill.serializeText()] = skillBonus.mBonus; + } + return res; + }); + raceT["isPlayable"] = sol::readonly_property( + [](const ESM::Race& rec) -> bool { return rec.mData.mFlags & ESM::Race::Playable; }); + raceT["isBeast"] + = sol::readonly_property([](const ESM::Race& rec) -> bool { return rec.mData.mFlags & ESM::Race::Beast; }); + raceT["height"] = sol::readonly_property([lua](const ESM::Race& rec) -> sol::table { + sol::table res(lua, sol::create); + res["male"] = rec.mData.mMaleHeight; + res["female"] = rec.mData.mFemaleHeight; + return LuaUtil::makeReadOnly(res); + }); + raceT["weight"] = sol::readonly_property([lua](const ESM::Race& rec) -> sol::table { + sol::table res(lua, sol::create); + res["male"] = rec.mData.mMaleWeight; + res["female"] = rec.mData.mFemaleWeight; + return LuaUtil::makeReadOnly(res); + }); + + raceT["attributes"] = sol::readonly_property([lua](const ESM::Race& rec) -> RaceAttributes { + return { rec, lua }; + }); + + auto attributesT = lua.new_usertype("ESM3_RaceAttributes"); + const auto& store = MWBase::Environment::get().getESMStore()->get(); + attributesT[sol::meta_function::index] + = [&](const RaceAttributes& attributes, std::string_view stringId) -> sol::optional { + ESM::RefId id = ESM::RefId::deserializeText(stringId); + if (!store.search(id)) + return sol::nullopt; + return attributes.getAttribute(id); + }; + attributesT[sol::meta_function::pairs] = [&](sol::this_state ts, RaceAttributes& attributes) { + auto iterator = store.begin(); + return sol::as_function( + [iterator, attributes, + &store]() mutable -> std::pair, sol::optional> { + if (iterator != store.end()) + { + ESM::RefId id = iterator->mId; + ++iterator; + return { id.serializeText(), attributes.getAttribute(id) }; + } + return { sol::nullopt, sol::nullopt }; + }); + }; + + return LuaUtil::makeReadOnly(races); + } +} diff --git a/apps/openmw/mwlua/racebindings.hpp b/apps/openmw/mwlua/racebindings.hpp new file mode 100644 index 00000000000..43ba9237c5b --- /dev/null +++ b/apps/openmw/mwlua/racebindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_RACEBINDINGS_H +#define MWLUA_RACEBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initRaceRecordBindings(const Context& context); +} + +#endif // MWLUA_RACEBINDINGS_H diff --git a/apps/openmw/mwlua/recordstore.hpp b/apps/openmw/mwlua/recordstore.hpp new file mode 100644 index 00000000000..aed84a1271d --- /dev/null +++ b/apps/openmw/mwlua/recordstore.hpp @@ -0,0 +1,64 @@ +#ifndef MWLUA_RECORDSTORE_H +#define MWLUA_RECORDSTORE_H + +#include + +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" +#include "apps/openmw/mwworld/store.hpp" + +#include "context.hpp" +#include "object.hpp" + +namespace sol +{ + // Ensure sol does not try to create the automatic Container or usertype bindings for Store. + // They include write operations and we want the store to be read-only. + template + struct is_automagical> : std::false_type + { + }; +} + +namespace MWLua +{ + template + void addRecordFunctionBinding( + sol::table& table, const Context& context, const std::string& recordName = std::string(T::getRecordType())) + { + const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); + + table["record"] = sol::overload([](const Object& obj) -> const T* { return obj.ptr().get()->mBase; }, + [&store](std::string_view id) -> const T* { return store.search(ESM::RefId::deserializeText(id)); }); + + // Define a custom user type for the store. + // Provide the interface of a read-only array. + using StoreT = MWWorld::Store; + sol::state_view lua = context.sol(); + sol::usertype storeT = lua.new_usertype(recordName + "WorldStore"); + storeT[sol::meta_function::to_string] = [recordName](const StoreT& store) { + return "{" + std::to_string(store.getSize()) + " " + recordName + " records}"; + }; + storeT[sol::meta_function::length] = [](const StoreT& store) { return store.getSize(); }; + storeT[sol::meta_function::index] = sol::overload( + [](const StoreT& store, size_t index) -> const T* { + if (index == 0 || index > store.getSize()) + return nullptr; + return store.at(LuaUtil::fromLuaIndex(index)); + }, + [](const StoreT& store, std::string_view id) -> const T* { + return store.search(ESM::RefId::deserializeText(id)); + }); + storeT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + storeT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + + // Provide access to the store. + table["records"] = &store; + } +} +#endif // MWLUA_RECORDSTORE_H diff --git a/apps/openmw/mwlua/soundbindings.cpp b/apps/openmw/mwlua/soundbindings.cpp new file mode 100644 index 00000000000..09309803d31 --- /dev/null +++ b/apps/openmw/mwlua/soundbindings.cpp @@ -0,0 +1,255 @@ +#include "soundbindings.hpp" +#include "recordstore.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/esmstore.hpp" + +#include +#include +#include + +#include "luamanagerimp.hpp" +#include "objectvariant.hpp" + +namespace +{ + struct PlaySoundArgs + { + bool mScale = true; + bool mLoop = false; + float mVolume = 1.f; + float mPitch = 1.f; + float mTimeOffset = 0.f; + }; + + struct StreamMusicArgs + { + float mFade = 1.f; + }; + + MWWorld::Ptr getMutablePtrOrThrow(const MWLua::ObjectVariant& variant) + { + if (variant.isLObject()) + throw std::runtime_error("Local scripts can only modify object they are attached to."); + + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + + return ptr; + } + + MWWorld::Ptr getPtrOrThrow(const MWLua::ObjectVariant& variant) + { + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + + return ptr; + } + + PlaySoundArgs getPlaySoundArgs(const sol::optional& options) + { + PlaySoundArgs args; + + if (options.has_value()) + { + args.mLoop = options->get_or("loop", false); + args.mVolume = options->get_or("volume", 1.f); + args.mPitch = options->get_or("pitch", 1.f); + args.mTimeOffset = options->get_or("timeOffset", 0.f); + args.mScale = options->get_or("scale", true); + } + return args; + } + + MWSound::PlayMode getPlayMode(const PlaySoundArgs& args, bool is3D) + { + if (is3D) + { + if (args.mLoop) + return MWSound::PlayMode::LoopRemoveAtDistance; + return MWSound::PlayMode::Normal; + } + + if (args.mLoop && !args.mScale) + return MWSound::PlayMode::LoopNoEnvNoScaling; + else if (args.mLoop) + return MWSound::PlayMode::LoopNoEnv; + else if (!args.mScale) + return MWSound::PlayMode::NoEnvNoScaling; + return MWSound::PlayMode::NoEnv; + } + + StreamMusicArgs getStreamMusicArgs(const sol::optional& options) + { + StreamMusicArgs args; + + if (options.has_value()) + { + args.mFade = options->get_or("fadeOut", 1.f); + } + return args; + } +} + +namespace MWLua +{ + sol::table initAmbientPackage(const Context& context) + { + sol::state_view lua = context.sol(); + if (lua["openmw_ambient"] != sol::nil) + return lua["openmw_ambient"]; + + sol::table api(lua, sol::create); + + api["playSound"] = [](std::string_view soundId, const sol::optional& options) { + auto args = getPlaySoundArgs(options); + auto playMode = getPlayMode(args, false); + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + + MWBase::Environment::get().getSoundManager()->playSound( + sound, args.mVolume, args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); + }; + api["playSoundFile"] = [](std::string_view fileName, const sol::optional& options) { + auto args = getPlaySoundArgs(options); + auto playMode = getPlayMode(args, false); + + MWBase::Environment::get().getSoundManager()->playSound( + fileName, args.mVolume, args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); + }; + + api["stopSound"] = [](std::string_view soundId) { + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + MWBase::Environment::get().getSoundManager()->stopSound3D(MWWorld::Ptr(), sound); + }; + api["stopSoundFile"] = [](std::string_view fileName) { + MWBase::Environment::get().getSoundManager()->stopSound3D(MWWorld::Ptr(), fileName); + }; + + api["isSoundPlaying"] = [](std::string_view soundId) { + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + return MWBase::Environment::get().getSoundManager()->getSoundPlaying(MWWorld::Ptr(), sound); + }; + api["isSoundFilePlaying"] = [](std::string_view fileName) { + return MWBase::Environment::get().getSoundManager()->getSoundPlaying(MWWorld::Ptr(), fileName); + }; + + api["streamMusic"] = [](std::string_view fileName, const sol::optional& options) { + auto args = getStreamMusicArgs(options); + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->streamMusic(VFS::Path::Normalized(fileName), MWSound::MusicType::Normal, args.mFade); + }; + + api["say"] + = [luaManager = context.mLuaManager](std::string_view fileName, sol::optional text) { + MWBase::Environment::get().getSoundManager()->say(VFS::Path::Normalized(fileName)); + if (text) + luaManager->addUIMessage(*text); + }; + + api["stopSay"] = []() { MWBase::Environment::get().getSoundManager()->stopSay(MWWorld::ConstPtr()); }; + api["isSayActive"] + = []() { return MWBase::Environment::get().getSoundManager()->sayActive(MWWorld::ConstPtr()); }; + + api["isMusicPlaying"] = []() { return MWBase::Environment::get().getSoundManager()->isMusicPlaying(); }; + + api["stopMusic"] = []() { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + if (sndMgr->getMusicType() == MWSound::MusicType::MWScript) + return; + + sndMgr->stopMusic(); + }; + + lua["openmw_ambient"] = LuaUtil::makeReadOnly(api); + return lua["openmw_ambient"]; + } + + sol::table initCoreSoundBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + + api["isEnabled"] = []() { return MWBase::Environment::get().getSoundManager()->isEnabled(); }; + + api["playSound3d"] + = [](std::string_view soundId, const sol::object& object, const sol::optional& options) { + auto args = getPlaySoundArgs(options); + auto playMode = getPlayMode(args, true); + + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + + MWBase::Environment::get().getSoundManager()->playSound3D( + ptr, sound, args.mVolume, args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); + }; + api["playSoundFile3d"] + = [](std::string_view fileName, const sol::object& object, const sol::optional& options) { + auto args = getPlaySoundArgs(options); + auto playMode = getPlayMode(args, true); + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + + MWBase::Environment::get().getSoundManager()->playSound3D( + ptr, fileName, args.mVolume, args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); + }; + + api["stopSound3d"] = [](std::string_view soundId, const sol::object& object) { + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + MWBase::Environment::get().getSoundManager()->stopSound3D(ptr, sound); + }; + api["stopSoundFile3d"] = [](std::string_view fileName, const sol::object& object) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + MWBase::Environment::get().getSoundManager()->stopSound3D(ptr, fileName); + }; + + api["isSoundPlaying"] = [](std::string_view soundId, const sol::object& object) { + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + const MWWorld::Ptr& ptr = getPtrOrThrow(ObjectVariant(object)); + return MWBase::Environment::get().getSoundManager()->getSoundPlaying(ptr, sound); + }; + api["isSoundFilePlaying"] = [](std::string_view fileName, const sol::object& object) { + const MWWorld::Ptr& ptr = getPtrOrThrow(ObjectVariant(object)); + return MWBase::Environment::get().getSoundManager()->getSoundPlaying(ptr, fileName); + }; + + api["say"] = [luaManager = context.mLuaManager]( + std::string_view fileName, const sol::object& object, sol::optional text) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + MWBase::Environment::get().getSoundManager()->say(ptr, VFS::Path::Normalized(fileName)); + if (text) + luaManager->addUIMessage(*text); + }; + api["stopSay"] = [](const sol::object& object) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + MWBase::Environment::get().getSoundManager()->stopSay(ptr); + }; + api["isSayActive"] = [](const sol::object& object) { + const MWWorld::Ptr& ptr = getPtrOrThrow(ObjectVariant(object)); + return MWBase::Environment::get().getSoundManager()->sayActive(ptr); + }; + + addRecordFunctionBinding(api, context); + + // Sound record + auto soundT = lua.new_usertype("ESM3_Sound"); + soundT[sol::meta_function::to_string] + = [](const ESM::Sound& rec) -> std::string { return "ESM3_Sound[" + rec.mId.toDebugString() + "]"; }; + soundT["id"] = sol::readonly_property([](const ESM::Sound& rec) { return rec.mId.serializeText(); }); + soundT["volume"] + = sol::readonly_property([](const ESM::Sound& rec) -> unsigned char { return rec.mData.mVolume; }); + soundT["minRange"] + = sol::readonly_property([](const ESM::Sound& rec) -> unsigned char { return rec.mData.mMinRange; }); + soundT["maxRange"] + = sol::readonly_property([](const ESM::Sound& rec) -> unsigned char { return rec.mData.mMaxRange; }); + soundT["fileName"] = sol::readonly_property([](const ESM::Sound& rec) -> std::string { + return Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(rec.mSound)).value(); + }); + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/soundbindings.hpp b/apps/openmw/mwlua/soundbindings.hpp new file mode 100644 index 00000000000..333ed898c4b --- /dev/null +++ b/apps/openmw/mwlua/soundbindings.hpp @@ -0,0 +1,15 @@ +#ifndef MWLUA_SOUNDBINDINGS_H +#define MWLUA_SOUNDBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initCoreSoundBindings(const Context&); + + sol::table initAmbientPackage(const Context& context); +} + +#endif // MWLUA_SOUNDBINDINGS_H diff --git a/apps/openmw/mwlua/stats.cpp b/apps/openmw/mwlua/stats.cpp new file mode 100644 index 00000000000..317ffbd406c --- /dev/null +++ b/apps/openmw/mwlua/stats.cpp @@ -0,0 +1,705 @@ +#include "stats.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "context.hpp" +#include "localscripts.hpp" +#include "luamanagerimp.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" + +#include "objectvariant.hpp" +#include "recordstore.hpp" + +namespace +{ + using SelfObject = MWLua::SelfObject; + using ObjectVariant = MWLua::ObjectVariant; + using Index = const SelfObject::CachedStat::Index&; + + template + auto addIndexedAccessor(auto index) + { + return [index](const sol::object& o) { return T::create(ObjectVariant(o), index); }; + } + + template + void addProp(const MWLua::Context& context, sol::usertype& type, std::string_view prop, G getter) + { + type[prop] = sol::property([=](const T& stat) { return stat.get(context, prop, getter); }, + [=](const T& stat, const sol::object& value) { stat.cache(context, prop, value); }); + } + + template + sol::object getValue(const MWLua::Context& context, const ObjectVariant& obj, SelfObject::CachedStat::Setter setter, + Index index, std::string_view prop, G getter) + { + if (obj.isSelfObject()) + { + SelfObject* self = obj.asSelfObject(); + auto it = self->mStatsCache.find({ setter, index, prop }); + if (it != self->mStatsCache.end()) + return it->second; + } + return sol::make_object(context.mLua->unsafeState(), getter(obj.ptr())); + } +} + +namespace MWLua +{ + static void addStatUpdateAction(MWLua::LuaManager* manager, const SelfObject& obj) + { + if (!obj.mStatsCache.empty()) + return; // was already added before + manager->addAction( + [obj = Object(obj)] { + LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); + if (scripts) + scripts->applyStatsCache(); + }, + "StatUpdateAction"); + } + + static void setCreatureValue(Index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto& stats = ptr.getClass().getCreatureStats(ptr); + if (prop == "current") + stats.setLevel(LuaUtil::cast(value)); + } + + static void setNpcValue(Index index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto& stats = ptr.getClass().getNpcStats(ptr); + if (prop == "progress") + stats.setLevelProgress(LuaUtil::cast(value)); + else if (prop == "skillIncreasesForAttribute") + stats.setSkillIncreasesForAttribute( + *std::get(index).getIf(), LuaUtil::cast(value)); + else if (prop == "skillIncreasesForSpecialization") + stats.setSkillIncreasesForSpecialization( + static_cast(std::get(index)), LuaUtil::cast(value)); + } + + class SkillIncreasesForAttributeStats + { + ObjectVariant mObject; + + public: + SkillIncreasesForAttributeStats(ObjectVariant object) + : mObject(std::move(object)) + { + } + + sol::object get(const Context& context, ESM::StringRefId attributeId) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return sol::nil; + + return getValue(context, mObject, &setNpcValue, attributeId, "skillIncreasesForAttribute", + [attributeId](const MWWorld::Ptr& ptr) { + return ptr.getClass().getNpcStats(ptr).getSkillIncreasesForAttribute(attributeId); + }); + } + + void set(const Context& context, ESM::StringRefId attributeId, const sol::object& value) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return; + + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, attributeId, "skillIncreasesForAttribute" }] = value; + } + }; + + class SkillIncreasesForSpecializationStats + { + ObjectVariant mObject; + + public: + SkillIncreasesForSpecializationStats(ObjectVariant object) + : mObject(std::move(object)) + { + } + + sol::object get(const Context& context, int specialization) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return sol::nil; + + return getValue(context, mObject, &setNpcValue, specialization, "skillIncreasesForSpecialization", + [specialization](const MWWorld::Ptr& ptr) { + return ptr.getClass().getNpcStats(ptr).getSkillIncreasesForSpecialization( + static_cast(specialization)); + }); + } + + void set(const Context& context, int specialization, const sol::object& value) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return; + + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, specialization, "skillIncreasesForSpecialization" }] + = value; + } + }; + + class LevelStat + { + ObjectVariant mObject; + + LevelStat(ObjectVariant object) + : mObject(std::move(object)) + { + } + + public: + sol::object getCurrent(const Context& context) const + { + return getValue(context, mObject, &setCreatureValue, std::monostate{}, "current", + [](const MWWorld::Ptr& ptr) { return ptr.getClass().getCreatureStats(ptr).getLevel(); }); + } + + void setCurrent(const Context& context, const sol::object& value) const + { + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &setCreatureValue, std::monostate{}, "current" }] = value; + } + + sol::object getProgress(const Context& context) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return sol::nil; + + return getValue(context, mObject, &setNpcValue, std::monostate{}, "progress", + [](const MWWorld::Ptr& ptr) { return ptr.getClass().getNpcStats(ptr).getLevelProgress(); }); + } + + void setProgress(const Context& context, const sol::object& value) const + { + const auto& ptr = mObject.ptr(); + if (!ptr.getClass().isNpc()) + return; + + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, std::monostate{}, "progress" }] = value; + } + + SkillIncreasesForAttributeStats getSkillIncreasesForAttributeStats() const + { + return SkillIncreasesForAttributeStats{ mObject }; + } + + SkillIncreasesForSpecializationStats getSkillIncreasesForSpecializationStats() const + { + return SkillIncreasesForSpecializationStats{ mObject }; + } + + static std::optional create(ObjectVariant object, Index) + { + if (!object.ptr().getClass().isActor()) + return {}; + return LevelStat{ std::move(object) }; + } + }; + + class DynamicStat + { + ObjectVariant mObject; + int mIndex; + + DynamicStat(ObjectVariant object, int index) + : mObject(std::move(object)) + , mIndex(index) + { + } + + public: + template + sol::object get(const Context& context, std::string_view prop, G getter) const + { + return getValue( + context, mObject, &DynamicStat::setValue, mIndex, prop, [this, getter](const MWWorld::Ptr& ptr) { + return (ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex).*getter)(); + }); + } + + static std::optional create(ObjectVariant object, Index i) + { + if (!object.ptr().getClass().isActor()) + return {}; + int index = std::get(i); + return DynamicStat{ std::move(object), index }; + } + + void cache(const Context& context, std::string_view prop, const sol::object& value) const + { + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &DynamicStat::setValue, mIndex, prop }] = value; + } + + static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + int index = std::get(i); + auto& stats = ptr.getClass().getCreatureStats(ptr); + auto stat = stats.getDynamic(index); + float floatValue = LuaUtil::cast(value); + if (prop == "base") + stat.setBase(floatValue); + else if (prop == "current") + stat.setCurrent(floatValue, true, true); + else if (prop == "modifier") + stat.setModifier(floatValue); + stats.setDynamic(index, stat); + } + }; + + class AttributeStat + { + ObjectVariant mObject; + ESM::RefId mId; + + AttributeStat(ObjectVariant object, ESM::RefId id) + : mObject(std::move(object)) + , mId(id) + { + } + + public: + template + sol::object get(const Context& context, std::string_view prop, G getter) const + { + return getValue( + context, mObject, &AttributeStat::setValue, mId, prop, [this, getter](const MWWorld::Ptr& ptr) { + return (ptr.getClass().getCreatureStats(ptr).getAttribute(mId).*getter)(); + }); + } + + float getModified(const Context& context) const + { + auto base = LuaUtil::cast(get(context, "base", &MWMechanics::AttributeValue::getBase)); + auto damage = LuaUtil::cast(get(context, "damage", &MWMechanics::AttributeValue::getDamage)); + auto modifier = LuaUtil::cast(get(context, "modifier", &MWMechanics::AttributeValue::getModifier)); + return std::max(0.f, base - damage + modifier); // Should match AttributeValue::getModified + } + + static std::optional create(ObjectVariant object, Index i) + { + if (!object.ptr().getClass().isActor()) + return {}; + ESM::RefId id = std::get(i); + return AttributeStat{ std::move(object), id }; + } + + void cache(const Context& context, std::string_view prop, const sol::object& value) const + { + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &AttributeStat::setValue, mId, prop }] = value; + } + + static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + ESM::RefId id = std::get(i); + auto& stats = ptr.getClass().getCreatureStats(ptr); + auto stat = stats.getAttribute(id); + float floatValue = LuaUtil::cast(value); + if (prop == "base") + stat.setBase(floatValue); + else if (prop == "damage") + { + stat.restore(stat.getDamage()); + stat.damage(floatValue); + } + else if (prop == "modifier") + stat.setModifier(floatValue); + stats.setAttribute(id, stat); + } + }; + + class SkillStat + { + ObjectVariant mObject; + ESM::RefId mId; + + SkillStat(ObjectVariant object, ESM::RefId id) + : mObject(std::move(object)) + , mId(id) + { + } + + static float getProgress(const MWWorld::Ptr& ptr, ESM::RefId id, const MWMechanics::SkillValue& stat) + { + float progress = stat.getProgress(); + if (progress != 0.f) + progress /= getMaxProgress(ptr, id, stat); + return progress; + } + + static float getMaxProgress(const MWWorld::Ptr& ptr, ESM::RefId id, const MWMechanics::SkillValue& stat) + { + const auto& store = *MWBase::Environment::get().getESMStore(); + const auto cl = store.get().find(ptr.get()->mBase->mClass); + return ptr.getClass().getNpcStats(ptr).getSkillProgressRequirement(id, *cl); + } + + public: + template + sol::object get(const Context& context, std::string_view prop, G getter) const + { + return getValue(context, mObject, &SkillStat::setValue, mId, prop, [this, getter](const MWWorld::Ptr& ptr) { + return (ptr.getClass().getNpcStats(ptr).getSkill(mId).*getter)(); + }); + } + + float getModified(const Context& context) const + { + auto base = LuaUtil::cast(get(context, "base", &MWMechanics::SkillValue::getBase)); + auto damage = LuaUtil::cast(get(context, "damage", &MWMechanics::SkillValue::getDamage)); + auto modifier = LuaUtil::cast(get(context, "modifier", &MWMechanics::SkillValue::getModifier)); + return std::max(0.f, base - damage + modifier); // Should match SkillValue::getModified + } + + sol::object getProgress(const Context& context) const + { + return getValue(context, mObject, &SkillStat::setValue, mId, "progress", [this](const MWWorld::Ptr& ptr) { + return getProgress(ptr, mId, ptr.getClass().getNpcStats(ptr).getSkill(mId)); + }); + } + + static std::optional create(ObjectVariant object, Index index) + { + if (!object.ptr().getClass().isNpc()) + return {}; + ESM::RefId id = std::get(index); + return SkillStat{ std::move(object), id }; + } + + void cache(const Context& context, std::string_view prop, const sol::object& value) const + { + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &SkillStat::setValue, mId, prop }] = value; + } + + static void setValue(Index index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + ESM::RefId id = std::get(index); + auto& stats = ptr.getClass().getNpcStats(ptr); + auto stat = stats.getSkill(id); + float floatValue = LuaUtil::cast(value); + if (prop == "base") + stat.setBase(floatValue); + else if (prop == "damage") + { + stat.restore(stat.getDamage()); + stat.damage(floatValue); + } + else if (prop == "modifier") + stat.setModifier(floatValue); + else if (prop == "progress") + stat.setProgress(floatValue * getMaxProgress(ptr, id, stat)); + stats.setSkill(id, stat); + } + }; + + class AIStat + { + ObjectVariant mObject; + MWMechanics::AiSetting mIndex; + + AIStat(ObjectVariant object, MWMechanics::AiSetting index) + : mObject(std::move(object)) + , mIndex(index) + { + } + + public: + template + sol::object get(const Context& context, std::string_view prop, G getter) const + { + return getValue(context, mObject, &AIStat::setValue, static_cast(mIndex), prop, + [this, getter](const MWWorld::Ptr& ptr) { + return (ptr.getClass().getCreatureStats(ptr).getAiSetting(mIndex).*getter)(); + }); + } + + int getModified(const Context& context) const + { + auto base = LuaUtil::cast(get(context, "base", &MWMechanics::Stat::getBase)); + auto modifier = LuaUtil::cast(get(context, "modifier", &MWMechanics::Stat::getModifier)); + return std::max(0, base + modifier); + } + + static std::optional create(ObjectVariant object, MWMechanics::AiSetting index) + { + if (!object.ptr().getClass().isActor()) + return {}; + return AIStat{ std::move(object), index }; + } + + void cache(const Context& context, std::string_view prop, const sol::object& value) const + { + SelfObject* obj = mObject.asSelfObject(); + addStatUpdateAction(context.mLuaManager, *obj); + obj->mStatsCache[SelfObject::CachedStat{ &AIStat::setValue, static_cast(mIndex), prop }] = value; + } + + static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) + { + auto index = static_cast(std::get(i)); + auto& stats = ptr.getClass().getCreatureStats(ptr); + auto stat = stats.getAiSetting(index); + int intValue = LuaUtil::cast(value); + if (prop == "base") + stat.setBase(intValue); + else if (prop == "modifier") + stat.setModifier(intValue); + stats.setAiSetting(index, stat); + } + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addActorStatsBindings(sol::table& actor, const Context& context) + { + sol::state_view lua = context.sol(); + sol::table stats(lua, sol::create); + actor["stats"] = LuaUtil::makeReadOnly(stats); + + auto skillIncreasesForAttributeStatsT + = lua.new_usertype("SkillIncreasesForAttributeStats"); + for (const auto& attribute : MWBase::Environment::get().getESMStore()->get()) + { + skillIncreasesForAttributeStatsT[ESM::RefId(attribute.mId).serializeText()] = sol::property( + [=](const SkillIncreasesForAttributeStats& stat) { return stat.get(context, attribute.mId); }, + [=](const SkillIncreasesForAttributeStats& stat, const sol::object& value) { + stat.set(context, attribute.mId, value); + }); + } + // ESM::Class::specializationIndexToLuaId.at(rec.mData.mSpecialization) + auto skillIncreasesForSpecializationStatsT + = lua.new_usertype("skillIncreasesForSpecializationStats"); + for (int i = 0; i < 3; i++) + { + std::string_view index = ESM::Class::specializationIndexToLuaId.at(i); + skillIncreasesForSpecializationStatsT[index] + = sol::property([=](const SkillIncreasesForSpecializationStats& stat) { return stat.get(context, i); }, + [=](const SkillIncreasesForSpecializationStats& stat, const sol::object& value) { + stat.set(context, i, value); + }); + } + + auto levelStatT = lua.new_usertype("LevelStat"); + levelStatT["current"] = sol::property([context](const LevelStat& stat) { return stat.getCurrent(context); }, + [context](const LevelStat& stat, const sol::object& value) { stat.setCurrent(context, value); }); + levelStatT["progress"] = sol::property([context](const LevelStat& stat) { return stat.getProgress(context); }, + [context](const LevelStat& stat, const sol::object& value) { stat.setProgress(context, value); }); + levelStatT["skillIncreasesForAttribute"] + = sol::readonly_property([](const LevelStat& stat) { return stat.getSkillIncreasesForAttributeStats(); }); + levelStatT["skillIncreasesForSpecialization"] = sol::readonly_property( + [](const LevelStat& stat) { return stat.getSkillIncreasesForSpecializationStats(); }); + stats["level"] = addIndexedAccessor(0); + + auto dynamicStatT = lua.new_usertype("DynamicStat"); + addProp(context, dynamicStatT, "base", &MWMechanics::DynamicStat::getBase); + addProp(context, dynamicStatT, "current", &MWMechanics::DynamicStat::getCurrent); + addProp(context, dynamicStatT, "modifier", &MWMechanics::DynamicStat::getModifier); + sol::table dynamic(lua, sol::create); + stats["dynamic"] = LuaUtil::makeReadOnly(dynamic); + dynamic["health"] = addIndexedAccessor(0); + dynamic["magicka"] = addIndexedAccessor(1); + dynamic["fatigue"] = addIndexedAccessor(2); + + auto attributeStatT = lua.new_usertype("AttributeStat"); + addProp(context, attributeStatT, "base", &MWMechanics::AttributeValue::getBase); + addProp(context, attributeStatT, "damage", &MWMechanics::AttributeValue::getDamage); + attributeStatT["modified"] + = sol::readonly_property([=](const AttributeStat& stat) { return stat.getModified(context); }); + addProp(context, attributeStatT, "modifier", &MWMechanics::AttributeValue::getModifier); + sol::table attributes(lua, sol::create); + stats["attributes"] = LuaUtil::makeReadOnly(attributes); + for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) + attributes[ESM::RefId(attribute.mId).serializeText()] = addIndexedAccessor(attribute.mId); + + auto aiStatT = lua.new_usertype("AIStat"); + addProp(context, aiStatT, "base", &MWMechanics::Stat::getBase); + addProp(context, aiStatT, "modifier", &MWMechanics::Stat::getModifier); + aiStatT["modified"] = sol::readonly_property([=](const AIStat& stat) { return stat.getModified(context); }); + sol::table ai(lua, sol::create); + stats["ai"] = LuaUtil::makeReadOnly(ai); + ai["alarm"] = addIndexedAccessor(MWMechanics::AiSetting::Alarm); + ai["fight"] = addIndexedAccessor(MWMechanics::AiSetting::Fight); + ai["flee"] = addIndexedAccessor(MWMechanics::AiSetting::Flee); + ai["hello"] = addIndexedAccessor(MWMechanics::AiSetting::Hello); + } + + void addNpcStatsBindings(sol::table& npc, const Context& context) + { + sol::state_view lua = context.sol(); + sol::table npcStats(lua, sol::create); + sol::table baseMeta(lua, sol::create); + baseMeta[sol::meta_function::index] = LuaUtil::getMutableFromReadOnly(npc["baseType"]["stats"]); + npcStats[sol::metatable_key] = baseMeta; + npc["stats"] = LuaUtil::makeReadOnly(npcStats); + + auto skillStatT = lua.new_usertype("SkillStat"); + addProp(context, skillStatT, "base", &MWMechanics::SkillValue::getBase); + addProp(context, skillStatT, "damage", &MWMechanics::SkillValue::getDamage); + skillStatT["modified"] + = sol::readonly_property([=](const SkillStat& stat) { return stat.getModified(context); }); + addProp(context, skillStatT, "modifier", &MWMechanics::SkillValue::getModifier); + skillStatT["progress"] = sol::property([context](const SkillStat& stat) { return stat.getProgress(context); }, + [context](const SkillStat& stat, const sol::object& value) { stat.cache(context, "progress", value); }); + sol::table skills(lua, sol::create); + npcStats["skills"] = LuaUtil::makeReadOnly(skills); + for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) + skills[ESM::RefId(skill.mId).serializeText()] = addIndexedAccessor(skill.mId); + } + + sol::table initCoreStatsBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table statsApi(lua, sol::create); + auto* vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + sol::table attributes(lua, sol::create); + addRecordFunctionBinding(attributes, context); + statsApi["Attribute"] = LuaUtil::makeReadOnly(attributes); + statsApi["Attribute"][sol::metatable_key][sol::meta_function::to_string] = ESM::Attribute::getRecordType; + + auto attributeT = lua.new_usertype("Attribute"); + attributeT[sol::meta_function::to_string] + = [](const ESM::Attribute& rec) { return "ESM3_Attribute[" + rec.mId.toDebugString() + "]"; }; + attributeT["id"] = sol::readonly_property( + [](const ESM::Attribute& rec) -> std::string { return ESM::RefId{ rec.mId }.serializeText(); }); + attributeT["name"] + = sol::readonly_property([](const ESM::Attribute& rec) -> std::string_view { return rec.mName; }); + attributeT["description"] + = sol::readonly_property([](const ESM::Attribute& rec) -> std::string_view { return rec.mDescription; }); + attributeT["icon"] = sol::readonly_property([vfs](const ESM::Attribute& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + + sol::table skills(lua, sol::create); + addRecordFunctionBinding(skills, context); + statsApi["Skill"] = LuaUtil::makeReadOnly(skills); + statsApi["Skill"][sol::metatable_key][sol::meta_function::to_string] = ESM::Skill::getRecordType; + + auto skillT = lua.new_usertype("Skill"); + skillT[sol::meta_function::to_string] + = [](const ESM::Skill& rec) { return "ESM3_Skill[" + rec.mId.toDebugString() + "]"; }; + skillT["id"] = sol::readonly_property( + [](const ESM::Skill& rec) -> std::string { return ESM::RefId{ rec.mId }.serializeText(); }); + skillT["name"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { return rec.mName; }); + skillT["description"] + = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { return rec.mDescription; }); + skillT["specialization"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { + return ESM::Class::specializationIndexToLuaId.at(rec.mData.mSpecialization); + }); + skillT["icon"] = sol::readonly_property([vfs](const ESM::Skill& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + skillT["school"] = sol::readonly_property([](const ESM::Skill& rec) -> const ESM::MagicSchool* { + if (!rec.mSchool) + return nullptr; + return &*rec.mSchool; + }); + skillT["attribute"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string { + return ESM::Attribute::indexToRefId(rec.mData.mAttribute).serializeText(); + }); + skillT["skillGain"] = sol::readonly_property([lua](const ESM::Skill& rec) -> sol::table { + sol::table res(lua, sol::create); + int index = 1; + for (auto skillGain : rec.mData.mUseValue) + res[index++] = skillGain; + return res; + }); + + auto schoolT = lua.new_usertype("MagicSchool"); + schoolT[sol::meta_function::to_string] + = [](const ESM::MagicSchool& rec) { return "ESM3_MagicSchool[" + rec.mName + "]"; }; + schoolT["name"] + = sol::readonly_property([](const ESM::MagicSchool& rec) -> std::string_view { return rec.mName; }); + schoolT["areaSound"] = sol::readonly_property( + [](const ESM::MagicSchool& rec) -> std::string { return rec.mAreaSound.serializeText(); }); + schoolT["boltSound"] = sol::readonly_property( + [](const ESM::MagicSchool& rec) -> std::string { return rec.mBoltSound.serializeText(); }); + schoolT["castSound"] = sol::readonly_property( + [](const ESM::MagicSchool& rec) -> std::string { return rec.mCastSound.serializeText(); }); + schoolT["failureSound"] = sol::readonly_property( + [](const ESM::MagicSchool& rec) -> std::string { return rec.mFailureSound.serializeText(); }); + schoolT["hitSound"] = sol::readonly_property( + [](const ESM::MagicSchool& rec) -> std::string { return rec.mHitSound.serializeText(); }); + + return LuaUtil::makeReadOnly(statsApi); + } +} diff --git a/apps/openmw/mwlua/stats.hpp b/apps/openmw/mwlua/stats.hpp new file mode 100644 index 00000000000..4ce2f6b5ebb --- /dev/null +++ b/apps/openmw/mwlua/stats.hpp @@ -0,0 +1,15 @@ +#ifndef MWLUA_STATS_H +#define MWLUA_STATS_H + +#include + +namespace MWLua +{ + struct Context; + + void addActorStatsBindings(sol::table& actor, const Context& context); + void addNpcStatsBindings(sol::table& npc, const Context& context); + sol::table initCoreStatsBindings(const Context& context); +} + +#endif diff --git a/apps/openmw/mwlua/types/activator.cpp b/apps/openmw/mwlua/types/activator.cpp new file mode 100644 index 00000000000..e1c923d31ac --- /dev/null +++ b/apps/openmw/mwlua/types/activator.cpp @@ -0,0 +1,59 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} +namespace +{ + // Populates a activator struct from a Lua table. + ESM::Activator tableToActivator(const sol::table& rec) + { + ESM::Activator activator; + if (rec["template"] != sol::nil) + activator = LuaUtil::cast(rec["template"]); + else + activator.blank(); + if (rec["name"] != sol::nil) + activator.mName = rec["name"]; + if (rec["model"] != sol::nil) + activator.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + activator.mScript = ESM::RefId::deserializeText(scriptId); + } + return activator; + } +} + +namespace MWLua +{ + void addActivatorBindings(sol::table activator, const Context& context) + { + activator["createRecordDraft"] = tableToActivator; + addRecordFunctionBinding(activator, context); + + sol::usertype record = context.sol().new_usertype("ESM3_Activator"); + record[sol::meta_function::to_string] + = [](const ESM::Activator& rec) { return "ESM3_Activator[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Activator& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Activator& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property([](const ESM::Activator& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Activator& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); + }); + } +} diff --git a/apps/openmw/mwlua/types/actor.cpp b/apps/openmw/mwlua/types/actor.cpp new file mode 100644 index 00000000000..02686684e8c --- /dev/null +++ b/apps/openmw/mwlua/types/actor.cpp @@ -0,0 +1,422 @@ +#include "types.hpp" + +#include + +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwbase/mechanicsmanager.hpp" +#include "apps/openmw/mwbase/windowmanager.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwmechanics/actorutil.hpp" +#include "apps/openmw/mwmechanics/creaturestats.hpp" +#include "apps/openmw/mwmechanics/drawstate.hpp" +#include "apps/openmw/mwworld/class.hpp" +#include "apps/openmw/mwworld/inventorystore.hpp" +#include "apps/openmw/mwworld/worldmodel.hpp" + +#include "../localscripts.hpp" +#include "../luamanagerimp.hpp" +#include "../magicbindings.hpp" +#include "../stats.hpp" + +namespace MWLua +{ + using EquipmentItem = std::variant; + using Equipment = std::map; + static constexpr int sAnySlot = -1; + + static std::pair findInInventory( + MWWorld::InventoryStore& store, const EquipmentItem& item, int slot = sAnySlot) + { + auto old_it = slot != sAnySlot ? store.getSlot(slot) : store.end(); + MWWorld::Ptr itemPtr; + + if (std::holds_alternative(item)) + { + itemPtr = MWBase::Environment::get().getWorldModel()->getPtr(std::get(item)); + if (old_it != store.end() && *old_it == itemPtr) + return { old_it, true }; // already equipped + if (itemPtr.isEmpty() || itemPtr.getCellRef().getCount() == 0 + || itemPtr.getContainerStore() != static_cast(&store)) + { + Log(Debug::Warning) << "Object" << std::get(item).toString() << " is not in inventory"; + return { store.end(), false }; + } + } + else + { + const auto& stringId = std::get(item); + ESM::RefId recordId = ESM::RefId::deserializeText(stringId); + if (old_it != store.end() && old_it->getCellRef().getRefId() == recordId) + return { old_it, true }; // already equipped + itemPtr = store.search(recordId); + if (itemPtr.isEmpty() || itemPtr.getCellRef().getCount() == 0) + { + Log(Debug::Warning) << "There is no object with recordId='" << stringId << "' in inventory"; + return { store.end(), false }; + } + } + + // TODO: Refactor InventoryStore to accept Ptr and get rid of this linear search. + MWWorld::ContainerStoreIterator it = std::find(store.begin(), store.end(), itemPtr); + if (it == store.end()) // should never happen + throw std::logic_error("Item not found in container"); + + return { it, false }; + } + + static void setEquipment(const MWWorld::Ptr& actor, const Equipment& equipment) + { + bool isPlayer = actor == MWBase::Environment::get().getWorld()->getPlayerPtr(); + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + std::array usedSlots; + std::fill(usedSlots.begin(), usedSlots.end(), false); + + auto tryEquipToSlot = [&store, &usedSlots, isPlayer](int slot, const EquipmentItem& item) -> bool { + auto [it, alreadyEquipped] = findInInventory(store, item, slot); + if (alreadyEquipped) + return true; + if (it == store.end()) + return false; + MWWorld::Ptr itemPtr = *it; + + auto [allowedSlots, _] = itemPtr.getClass().getEquipmentSlots(itemPtr); + bool requestedSlotIsAllowed + = std::find(allowedSlots.begin(), allowedSlots.end(), slot) != allowedSlots.end(); + if (!requestedSlotIsAllowed) + { + auto firstAllowed + = std::find_if(allowedSlots.begin(), allowedSlots.end(), [&](int s) { return !usedSlots[s]; }); + if (firstAllowed == allowedSlots.end()) + { + Log(Debug::Warning) << "No suitable slot for " << itemPtr.toString(); + return false; + } + slot = *firstAllowed; + } + + bool skipEquip = false; + + if (isPlayer) + { + const ESM::RefId& script = itemPtr.getClass().getScript(itemPtr); + if (!script.empty()) + { + MWScript::Locals& locals = itemPtr.getRefData().getLocals(); + locals.setVarByInt(script, "onpcequip", 1); + skipEquip = locals.getIntVar(script, "pcskipequip") == 1; + } + } + + if (!skipEquip) + store.equip(slot, it); + + return requestedSlotIsAllowed; // return true if equipped to requested slot and false if slot was changed + }; + + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + { + auto old_it = store.getSlot(slot); + auto new_it = equipment.find(slot); + if (new_it == equipment.end()) + { + if (old_it != store.end()) + store.unequipSlot(slot); + continue; + } + if (tryEquipToSlot(slot, new_it->second)) + usedSlots[slot] = true; + } + for (const auto& [slot, item] : equipment) + if (slot >= MWWorld::InventoryStore::Slots) + tryEquipToSlot(sAnySlot, item); + } + + static void setSelectedEnchantedItem(const MWWorld::Ptr& actor, const EquipmentItem& item) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + // We're not passing in a specific slot, so ignore the already equipped return value + auto [it, _] = findInInventory(store, item, sAnySlot); + if (it == store.end()) + return; + + MWWorld::Ptr itemPtr = *it; + + // Equip the item if applicable + auto slots = itemPtr.getClass().getEquipmentSlots(itemPtr); + if (!slots.first.empty()) + { + bool alreadyEquipped = false; + for (auto slot : slots.first) + { + if (store.getSlot(slot) == it) + alreadyEquipped = true; + } + + if (!alreadyEquipped) + { + MWBase::Environment::get().getWindowManager()->useItem(itemPtr); + // make sure that item was successfully equipped + if (!store.isEquipped(itemPtr)) + return; + } + } + + store.setSelectedEnchantItem(it); + // to reset WindowManager::mSelectedSpell immediately + MWBase::Environment::get().getWindowManager()->setSelectedEnchantItem(*it); + } + + void addActorBindings(sol::table actor, const Context& context) + { + sol::state_view lua = context.sol(); + actor["STANCE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Nothing", MWMechanics::DrawState::Nothing }, + { "Weapon", MWMechanics::DrawState::Weapon }, + { "Spell", MWMechanics::DrawState::Spell }, + })); + actor["EQUIPMENT_SLOT"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { { "Helmet", MWWorld::InventoryStore::Slot_Helmet }, { "Cuirass", MWWorld::InventoryStore::Slot_Cuirass }, + { "Greaves", MWWorld::InventoryStore::Slot_Greaves }, + { "LeftPauldron", MWWorld::InventoryStore::Slot_LeftPauldron }, + { "RightPauldron", MWWorld::InventoryStore::Slot_RightPauldron }, + { "LeftGauntlet", MWWorld::InventoryStore::Slot_LeftGauntlet }, + { "RightGauntlet", MWWorld::InventoryStore::Slot_RightGauntlet }, + { "Boots", MWWorld::InventoryStore::Slot_Boots }, { "Shirt", MWWorld::InventoryStore::Slot_Shirt }, + { "Pants", MWWorld::InventoryStore::Slot_Pants }, { "Skirt", MWWorld::InventoryStore::Slot_Skirt }, + { "Robe", MWWorld::InventoryStore::Slot_Robe }, { "LeftRing", MWWorld::InventoryStore::Slot_LeftRing }, + { "RightRing", MWWorld::InventoryStore::Slot_RightRing }, + { "Amulet", MWWorld::InventoryStore::Slot_Amulet }, { "Belt", MWWorld::InventoryStore::Slot_Belt }, + { "CarriedRight", MWWorld::InventoryStore::Slot_CarriedRight }, + { "CarriedLeft", MWWorld::InventoryStore::Slot_CarriedLeft }, + { "Ammunition", MWWorld::InventoryStore::Slot_Ammunition } })); + + actor["getStance"] = [](const Object& o) { + const MWWorld::Class& cls = o.ptr().getClass(); + if (cls.isActor()) + return cls.getCreatureStats(o.ptr()).getDrawState(); + else + throw std::runtime_error("Actor expected"); + }; + actor["stance"] = actor["getStance"]; // for compatibility; should be removed later + actor["setStance"] = [](const SelfObject& self, int stance) { + const MWWorld::Class& cls = self.ptr().getClass(); + if (!cls.isActor()) + throw std::runtime_error("Actor expected"); + auto& stats = cls.getCreatureStats(self.ptr()); + if (stance != static_cast(MWMechanics::DrawState::Nothing) + && stance != static_cast(MWMechanics::DrawState::Weapon) + && stance != static_cast(MWMechanics::DrawState::Spell)) + { + throw std::runtime_error("Incorrect stance"); + } + MWMechanics::DrawState newDrawState = static_cast(stance); + if (stats.getDrawState() == newDrawState) + return; + if (newDrawState == MWMechanics::DrawState::Spell) + { + bool hasSelectedSpell; + if (self.ptr() == MWBase::Environment::get().getWorld()->getPlayerPtr()) + // For the player selecting spell in UI doesn't change selected spell in CreatureStats (was + // implemented this way to prevent changing spell during casting, probably should be refactored), so + // we have to handle the player separately. + hasSelectedSpell = !MWBase::Environment::get().getWindowManager()->getSelectedSpell().empty(); + else + hasSelectedSpell = !stats.getSpells().getSelectedSpell().empty(); + if (!hasSelectedSpell) + { + if (!cls.hasInventoryStore(self.ptr())) + return; // No selected spell and no items; can't use magic stance. + MWWorld::InventoryStore& store = cls.getInventoryStore(self.ptr()); + if (store.getSelectedEnchantItem() == store.end()) + return; // No selected spell and no selected enchanted item; can't use magic stance. + } + } + MWBase::MechanicsManager* mechanics = MWBase::Environment::get().getMechanicsManager(); + // We want to interrupt animation only if attack is preparing, but still is not triggered. + // Otherwise we will get a "speedshooting" exploit, when player can skip reload animation by hitting "Toggle + // Weapon" key twice. + if (mechanics->isAttackPreparing(self.ptr())) + stats.setAttackingOrSpell(false); // interrupt attack + else if (mechanics->isAttackingOrSpell(self.ptr())) + return; // can't be interrupted; ignore setStance + stats.setDrawState(newDrawState); + }; + + actor["getSelectedEnchantedItem"] = [](sol::this_state lua, const Object& o) -> sol::object { + const MWWorld::Ptr& ptr = o.ptr(); + if (!ptr.getClass().hasInventoryStore(ptr)) + return sol::nil; + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + auto it = store.getSelectedEnchantItem(); + if (it == store.end()) + return sol::nil; + MWBase::Environment::get().getWorldModel()->registerPtr(*it); + if (dynamic_cast(&o)) + return sol::make_object(lua, GObject(*it)); + else + return sol::make_object(lua, LObject(*it)); + }; + actor["setSelectedEnchantedItem"] = [context](const SelfObject& obj, const sol::object& item) { + const MWWorld::Ptr& ptr = obj.ptr(); + if (!ptr.getClass().hasInventoryStore(ptr)) + return; + + EquipmentItem ei; + if (item.is()) + { + ei = LuaUtil::cast(item).id(); + } + else + { + ei = LuaUtil::cast(item); + } + context.mLuaManager->addAction( + [obj = Object(ptr), ei = std::move(ei)] { setSelectedEnchantedItem(obj.ptr(), ei); }, + "setSelectedEnchantedItemAction"); + }; + + actor["canMove"] = [](const Object& o) { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getMaxSpeed(o.ptr()) > 0; + }; + actor["getRunSpeed"] = [](const Object& o) { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getRunSpeed(o.ptr()); + }; + actor["getWalkSpeed"] = [](const Object& o) { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getWalkSpeed(o.ptr()); + }; + actor["getCurrentSpeed"] = [](const Object& o) { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getCurrentSpeed(o.ptr()); + }; + + // for compatibility; should be removed later + actor["runSpeed"] = actor["getRunSpeed"]; + actor["walkSpeed"] = actor["getWalkSpeed"]; + actor["currentSpeed"] = actor["getCurrentSpeed"]; + + actor["isOnGround"] + = [](const LObject& o) { return MWBase::Environment::get().getWorld()->isOnGround(o.ptr()); }; + actor["isSwimming"] + = [](const LObject& o) { return MWBase::Environment::get().getWorld()->isSwimming(o.ptr()); }; + + actor["inventory"] = sol::overload([](const LObject& o) { return Inventory{ o }; }, + [](const GObject& o) { return Inventory{ o }; }); + auto getAllEquipment = [](sol::this_state lua, const Object& o) { + const MWWorld::Ptr& ptr = o.ptr(); + sol::table equipment(lua, sol::create); + if (!ptr.getClass().hasInventoryStore(ptr)) + return equipment; + + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + { + auto it = store.getSlot(slot); + if (it == store.end()) + continue; + MWBase::Environment::get().getWorldModel()->registerPtr(*it); + if (dynamic_cast(&o)) + equipment[slot] = sol::make_object(lua, GObject(*it)); + else + equipment[slot] = sol::make_object(lua, LObject(*it)); + } + return equipment; + }; + auto getEquipmentFromSlot = [](sol::this_state lua, const Object& o, int slot) -> sol::object { + const MWWorld::Ptr& ptr = o.ptr(); + if (!ptr.getClass().hasInventoryStore(ptr)) + return sol::nil; + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + auto it = store.getSlot(slot); + if (it == store.end()) + return sol::nil; + MWBase::Environment::get().getWorldModel()->registerPtr(*it); + if (dynamic_cast(&o)) + return sol::make_object(lua, GObject(*it)); + else + return sol::make_object(lua, LObject(*it)); + }; + actor["getEquipment"] = sol::overload(getAllEquipment, getEquipmentFromSlot); + actor["equipment"] = actor["getEquipment"]; // for compatibility; should be removed later + actor["hasEquipped"] = [](const Object& o, const Object& item) { + const MWWorld::Ptr& ptr = o.ptr(); + if (!ptr.getClass().hasInventoryStore(ptr)) + return false; + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + return store.isEquipped(item.ptr()); + }; + actor["setEquipment"] = [context](const SelfObject& obj, const sol::table& equipment) { + const MWWorld::Ptr& ptr = obj.ptr(); + if (!ptr.getClass().hasInventoryStore(ptr)) + { + if (!equipment.empty()) + throw std::runtime_error(obj.toString() + " has no equipment slots"); + return; + } + Equipment eqp; + for (auto& [key, value] : equipment) + { + int slot = LuaUtil::cast(key); + if (value.is()) + eqp[slot] = LuaUtil::cast(value).id(); + else + eqp[slot] = LuaUtil::cast(value); + } + context.mLuaManager->addAction( + [obj = Object(ptr), eqp = std::move(eqp)] { setEquipment(obj.ptr(), eqp); }, "SetEquipmentAction"); + }; + actor["getPathfindingAgentBounds"] = [](sol::this_state lua, const LObject& o) { + const DetourNavigator::AgentBounds agentBounds + = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(o.ptr()); + sol::table result(lua, sol::create); + result["shapeType"] = agentBounds.mShapeType; + result["halfExtents"] = agentBounds.mHalfExtents; + return result; + }; + actor["isInActorsProcessingRange"] = [](const Object& o) { + const MWWorld::Ptr player = MWMechanics::getPlayer(); + const auto& target = o.ptr(); + if (target == player) + return true; + + if (!target.getClass().isActor()) + throw std::runtime_error("Actor expected"); + + if (target.getCell()->getCell()->getWorldSpace() != player.getCell()->getCell()->getWorldSpace()) + return false; + + const int actorsProcessingRange = Settings::game().mActorsProcessingRange; + const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); + + const float dist = (playerPos - target.getRefData().getPosition().asVec3()).length(); + return dist <= actorsProcessingRange; + }; + + actor["isDead"] = [](const Object& o) { + const auto& target = o.ptr(); + return target.getClass().getCreatureStats(target).isDead(); + }; + + actor["isDeathFinished"] = [](const Object& o) { + const auto& target = o.ptr(); + return target.getClass().getCreatureStats(target).isDeathAnimationFinished(); + }; + + actor["getEncumbrance"] = [](const Object& actor) -> float { + const MWWorld::Ptr ptr = actor.ptr(); + return ptr.getClass().getEncumbrance(ptr); + }; + + addActorStatsBindings(actor, context); + addActorMagicBindings(actor, context); + } + +} diff --git a/apps/openmw/mwlua/types/actor.hpp b/apps/openmw/mwlua/types/actor.hpp new file mode 100644 index 00000000000..425e44451bc --- /dev/null +++ b/apps/openmw/mwlua/types/actor.hpp @@ -0,0 +1,84 @@ +#ifndef MWLUA_ACTOR_H +#define MWLUA_ACTOR_H + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" + +#include "../context.hpp" + +namespace MWLua +{ + + template + void addActorServicesBindings(sol::usertype& record, const Context& context) + { + record["servicesOffered"] = sol::readonly_property([context](const T& rec) -> sol::table { + sol::state_view lua = context.sol(); + sol::table providedServices(lua, sol::create); + constexpr std::array, 19> serviceNames = { { { ESM::NPC::Spells, + "Spells" }, + { ESM::NPC::Spellmaking, "Spellmaking" }, { ESM::NPC::Enchanting, "Enchanting" }, + { ESM::NPC::Training, "Training" }, { ESM::NPC::Repair, "Repair" }, { ESM::NPC::AllItems, "Barter" }, + { ESM::NPC::Weapon, "Weapon" }, { ESM::NPC::Armor, "Armor" }, { ESM::NPC::Clothing, "Clothing" }, + { ESM::NPC::Books, "Books" }, { ESM::NPC::Ingredients, "Ingredients" }, { ESM::NPC::Picks, "Picks" }, + { ESM::NPC::Probes, "Probes" }, { ESM::NPC::Lights, "Lights" }, { ESM::NPC::Apparatus, "Apparatus" }, + { ESM::NPC::RepairItem, "RepairItem" }, { ESM::NPC::Misc, "Misc" }, { ESM::NPC::Potions, "Potions" }, + { ESM::NPC::MagicItems, "MagicItems" } } }; + + int services = rec.mAiData.mServices; + if constexpr (std::is_same_v) + { + if (rec.mFlags & ESM::NPC::Autocalc) + services + = MWBase::Environment::get().getESMStore()->get().find(rec.mClass)->mData.mServices; + } + for (const auto& [flag, name] : serviceNames) + { + providedServices[name] = (services & flag) != 0; + } + providedServices["Travel"] = !rec.getTransport().empty(); + return LuaUtil::makeReadOnly(providedServices); + }); + + record["travelDestinations"] = sol::readonly_property([context](const T& rec) -> sol::table { + sol::state_view lua = context.sol(); + sol::table travelDests(lua, sol::create); + if (!rec.getTransport().empty()) + { + int index = 1; + for (const auto& dest : rec.getTransport()) + { + sol::table travelDest(lua, sol::create); + + ESM::RefId cellId; + if (dest.mCellName.empty()) + { + const ESM::ExteriorCellLocation cellIndex + = ESM::positionToExteriorCellLocation(dest.mPos.pos[0], dest.mPos.pos[1]); + cellId = ESM::RefId::esm3ExteriorCell(cellIndex.mX, cellIndex.mY); + } + else + cellId = ESM::RefId::stringRefId(dest.mCellName); + travelDest["rotation"] = LuaUtil::asTransform(Misc::Convert::makeOsgQuat(dest.mPos.rot)); + travelDest["position"] = dest.mPos.asVec3(); + travelDest["cellId"] = cellId.serializeText(); + + travelDests[index] = LuaUtil::makeReadOnly(travelDest); + index++; + } + } + return LuaUtil::makeReadOnly(travelDests); + }); + } +} +#endif // MWLUA_ACTOR_H diff --git a/apps/openmw/mwlua/types/apparatus.cpp b/apps/openmw/mwlua/types/apparatus.cpp new file mode 100644 index 00000000000..d26f1460960 --- /dev/null +++ b/apps/openmw/mwlua/types/apparatus.cpp @@ -0,0 +1,57 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addApparatusBindings(sol::table apparatus, const Context& context) + { + sol::state_view lua = context.sol(); + apparatus["TYPE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "MortarPestle", ESM::Apparatus::MortarPestle }, + { "Alembic", ESM::Apparatus::Alembic }, + { "Calcinator", ESM::Apparatus::Calcinator }, + { "Retort", ESM::Apparatus::Retort }, + })); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + addRecordFunctionBinding(apparatus, context); + + sol::usertype record = lua.new_usertype("ESM3_Apparatus"); + record[sol::meta_function::to_string] + = [](const ESM::Apparatus& rec) { return "ESM3_Apparatus[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Apparatus& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Apparatus& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property([](const ESM::Apparatus& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Apparatus& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); + }); + record["icon"] = sol::readonly_property([vfs](const ESM::Apparatus& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["type"] = sol::readonly_property([](const ESM::Apparatus& rec) -> int { return rec.mData.mType; }); + record["value"] = sol::readonly_property([](const ESM::Apparatus& rec) -> int { return rec.mData.mValue; }); + record["weight"] = sol::readonly_property([](const ESM::Apparatus& rec) -> float { return rec.mData.mWeight; }); + record["quality"] + = sol::readonly_property([](const ESM::Apparatus& rec) -> float { return rec.mData.mQuality; }); + } +} diff --git a/apps/openmw/mwlua/types/armor.cpp b/apps/openmw/mwlua/types/armor.cpp new file mode 100644 index 00000000000..f26949c358d --- /dev/null +++ b/apps/openmw/mwlua/types/armor.cpp @@ -0,0 +1,116 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} +namespace +{ + // Populates an armor struct from a Lua table. + ESM::Armor tableToArmor(const sol::table& rec) + { + ESM::Armor armor; + if (rec["template"] != sol::nil) + armor = LuaUtil::cast(rec["template"]); + else + armor.blank(); + if (rec["name"] != sol::nil) + armor.mName = rec["name"]; + if (rec["model"] != sol::nil) + armor.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + armor.mIcon = rec["icon"]; + if (rec["enchant"] != sol::nil) + { + std::string_view enchantId = rec["enchant"].get(); + armor.mEnchant = ESM::RefId::deserializeText(enchantId); + } + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + armor.mScript = ESM::RefId::deserializeText(scriptId); + } + + if (rec["weight"] != sol::nil) + armor.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + armor.mData.mValue = rec["value"]; + if (rec["type"] != sol::nil) + { + int armorType = rec["type"].get(); + if (armorType >= 0 && armorType <= ESM::Armor::RBracer) + armor.mData.mType = armorType; + else + throw std::runtime_error("Invalid Armor Type provided: " + std::to_string(armorType)); + } + if (rec["health"] != sol::nil) + armor.mData.mHealth = rec["health"]; + if (rec["baseArmor"] != sol::nil) + armor.mData.mArmor = rec["baseArmor"]; + if (rec["enchantCapacity"] != sol::nil) + armor.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); + + return armor; + } +} + +namespace MWLua +{ + void addArmorBindings(sol::table armor, const Context& context) + { + sol::state_view lua = context.sol(); + armor["TYPE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Helmet", ESM::Armor::Helmet }, + { "Cuirass", ESM::Armor::Cuirass }, + { "LPauldron", ESM::Armor::LPauldron }, + { "RPauldron", ESM::Armor::RPauldron }, + { "Greaves", ESM::Armor::Greaves }, + { "Boots", ESM::Armor::Boots }, + { "LGauntlet", ESM::Armor::LGauntlet }, + { "RGauntlet", ESM::Armor::RGauntlet }, + { "Shield", ESM::Armor::Shield }, + { "LBracer", ESM::Armor::LBracer }, + { "RBracer", ESM::Armor::RBracer }, + })); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + addRecordFunctionBinding(armor, context); + + armor["createRecordDraft"] = tableToArmor; + sol::usertype record = lua.new_usertype("ESM3_Armor"); + record[sol::meta_function::to_string] + = [](const ESM::Armor& rec) -> std::string { return "ESM3_Armor[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Armor& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Armor& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property( + [](const ESM::Armor& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["icon"] = sol::readonly_property([vfs](const ESM::Armor& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["enchant"] = sol::readonly_property( + [](const ESM::Armor& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mEnchant); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Armor& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); + record["weight"] = sol::readonly_property([](const ESM::Armor& rec) -> float { return rec.mData.mWeight; }); + record["value"] = sol::readonly_property([](const ESM::Armor& rec) -> int { return rec.mData.mValue; }); + record["type"] = sol::readonly_property([](const ESM::Armor& rec) -> int { return rec.mData.mType; }); + record["health"] = sol::readonly_property([](const ESM::Armor& rec) -> int { return rec.mData.mHealth; }); + record["baseArmor"] = sol::readonly_property([](const ESM::Armor& rec) -> int { return rec.mData.mArmor; }); + record["enchantCapacity"] + = sol::readonly_property([](const ESM::Armor& rec) -> float { return rec.mData.mEnchant * 0.1f; }); + } +} diff --git a/apps/openmw/mwlua/types/book.cpp b/apps/openmw/mwlua/types/book.cpp new file mode 100644 index 00000000000..f869971b9b3 --- /dev/null +++ b/apps/openmw/mwlua/types/book.cpp @@ -0,0 +1,127 @@ +#include "types.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace +{ + // Populates a book struct from a Lua table. + ESM::Book tableToBook(const sol::table& rec) + { + ESM::Book book; + if (rec["template"] != sol::nil) + book = LuaUtil::cast(rec["template"]); + else + { + book.blank(); + book.mData.mSkillId = -1; + } + if (rec["name"] != sol::nil) + book.mName = rec["name"]; + if (rec["model"] != sol::nil) + book.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + book.mIcon = rec["icon"]; + if (rec["text"] != sol::nil) + book.mText = rec["text"]; + if (rec["enchant"] != sol::nil) + { + std::string_view enchantId = rec["enchant"].get(); + book.mEnchant = ESM::RefId::deserializeText(enchantId); + } + + if (rec["enchantCapacity"] != sol::nil) + book.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + book.mScript = ESM::RefId::deserializeText(scriptId); + } + if (rec["weight"] != sol::nil) + book.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + book.mData.mValue = rec["value"]; + if (rec["isScroll"] != sol::nil) + book.mData.mIsScroll = rec["isScroll"] ? 1 : 0; + + if (rec["skill"] != sol::nil) + { + ESM::RefId skill = ESM::RefId::deserializeText(rec["skill"].get()); + + book.mData.mSkillId = -1; + if (!skill.empty()) + { + book.mData.mSkillId = ESM::Skill::refIdToIndex(skill); + if (book.mData.mSkillId == -1) + throw std::runtime_error("Incorrect skill: " + skill.toDebugString()); + } + } + + return book; + } +} + +namespace MWLua +{ + void addBookBindings(sol::table book, const Context& context) + { + sol::state_view lua = context.sol(); + // types.book.SKILL is deprecated (core.SKILL should be used instead) + // TODO: Remove book.SKILL after branching 0.49 + sol::table skill(lua, sol::create); + book["SKILL"] = LuaUtil::makeStrictReadOnly(skill); + book["createRecordDraft"] = tableToBook; + for (int id = 0; id < ESM::Skill::Length; ++id) + { + std::string skillName = ESM::Skill::indexToRefId(id).serializeText(); + skill[skillName] = skillName; + } + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + addRecordFunctionBinding(book, context); + + sol::usertype record = lua.new_usertype("ESM3_Book"); + record[sol::meta_function::to_string] + = [](const ESM::Book& rec) { return "ESM3_Book[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property( + [](const ESM::Book& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Book& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); + record["icon"] = sol::readonly_property([vfs](const ESM::Book& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["text"] = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mText; }); + record["enchant"] = sol::readonly_property( + [](const ESM::Book& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mEnchant); }); + record["isScroll"] = sol::readonly_property([](const ESM::Book& rec) -> bool { return rec.mData.mIsScroll; }); + record["value"] = sol::readonly_property([](const ESM::Book& rec) -> int { return rec.mData.mValue; }); + record["weight"] = sol::readonly_property([](const ESM::Book& rec) -> float { return rec.mData.mWeight; }); + record["enchantCapacity"] + = sol::readonly_property([](const ESM::Book& rec) -> float { return rec.mData.mEnchant * 0.1f; }); + record["skill"] = sol::readonly_property([](const ESM::Book& rec) -> sol::optional { + ESM::RefId skill = ESM::Skill::indexToRefId(rec.mData.mSkillId); + return LuaUtil::serializeRefId(skill); + }); + } +} diff --git a/apps/openmw/mwlua/types/clothing.cpp b/apps/openmw/mwlua/types/clothing.cpp new file mode 100644 index 00000000000..778beee97a0 --- /dev/null +++ b/apps/openmw/mwlua/types/clothing.cpp @@ -0,0 +1,111 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} +namespace +{ + // Populates a clothing struct from a Lua table. + ESM::Clothing tableToClothing(const sol::table& rec) + { + ESM::Clothing clothing; + if (rec["template"] != sol::nil) + clothing = LuaUtil::cast(rec["template"]); + else + clothing.blank(); + + if (rec["name"] != sol::nil) + clothing.mName = rec["name"]; + if (rec["model"] != sol::nil) + clothing.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + clothing.mIcon = rec["icon"]; + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + clothing.mScript = ESM::RefId::deserializeText(scriptId); + } + + if (rec["enchant"] != sol::nil) + { + std::string_view enchantId = rec["enchant"].get(); + clothing.mEnchant = ESM::RefId::deserializeText(enchantId); + } + if (rec["enchantCapacity"] != sol::nil) + clothing.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); + if (rec["weight"] != sol::nil) + clothing.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + clothing.mData.mValue = rec["value"]; + if (rec["type"] != sol::nil) + { + int clothingType = rec["type"].get(); + if (clothingType >= 0 && clothingType <= ESM::Clothing::Amulet) + clothing.mData.mType = clothingType; + else + throw std::runtime_error("Invalid Clothing Type provided: " + std::to_string(clothingType)); + } + return clothing; + } +} +namespace MWLua +{ + void addClothingBindings(sol::table clothing, const Context& context) + { + clothing["createRecordDraft"] = tableToClothing; + + sol::state_view lua = context.sol(); + clothing["TYPE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Amulet", ESM::Clothing::Amulet }, + { "Belt", ESM::Clothing::Belt }, + { "LGlove", ESM::Clothing::LGlove }, + { "Pants", ESM::Clothing::Pants }, + { "RGlove", ESM::Clothing::RGlove }, + { "Ring", ESM::Clothing::Ring }, + { "Robe", ESM::Clothing::Robe }, + { "Shirt", ESM::Clothing::Shirt }, + { "Shoes", ESM::Clothing::Shoes }, + { "Skirt", ESM::Clothing::Skirt }, + })); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + addRecordFunctionBinding(clothing, context); + + sol::usertype record = lua.new_usertype("ESM3_Clothing"); + record[sol::meta_function::to_string] + = [](const ESM::Clothing& rec) -> std::string { return "ESM3_Clothing[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Clothing& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Clothing& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property( + [](const ESM::Clothing& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["icon"] = sol::readonly_property([vfs](const ESM::Clothing& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["enchant"] = sol::readonly_property([](const ESM::Clothing& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mEnchant); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Clothing& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); + }); + record["weight"] = sol::readonly_property([](const ESM::Clothing& rec) -> float { return rec.mData.mWeight; }); + record["value"] = sol::readonly_property([](const ESM::Clothing& rec) -> int { return rec.mData.mValue; }); + record["type"] = sol::readonly_property([](const ESM::Clothing& rec) -> int { return rec.mData.mType; }); + record["enchantCapacity"] + = sol::readonly_property([](const ESM::Clothing& rec) -> float { return rec.mData.mEnchant * 0.1f; }); + } +} diff --git a/apps/openmw/mwlua/types/container.cpp b/apps/openmw/mwlua/types/container.cpp new file mode 100644 index 00000000000..67c2c5c65c1 --- /dev/null +++ b/apps/openmw/mwlua/types/container.cpp @@ -0,0 +1,64 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwworld/class.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + static const MWWorld::Ptr& containerPtr(const Object& o) + { + return verifyType(ESM::REC_CONT, o.ptr()); + } + + void addContainerBindings(sol::table container, const Context& context) + { + container["content"] = sol::overload([](const LObject& o) { return Inventory{ o }; }, + [](const GObject& o) { return Inventory{ o }; }); + container["inventory"] = container["content"]; + container["getEncumbrance"] = [](const Object& obj) -> float { + const MWWorld::Ptr& ptr = containerPtr(obj); + return ptr.getClass().getEncumbrance(ptr); + }; + container["encumbrance"] = container["getEncumbrance"]; // for compatibility; should be removed later + container["getCapacity"] = [](const Object& obj) -> float { + const MWWorld::Ptr& ptr = containerPtr(obj); + return ptr.getClass().getCapacity(ptr); + }; + container["capacity"] = container["getCapacity"]; // for compatibility; should be removed later + + addRecordFunctionBinding(container, context); + + sol::usertype record = context.sol().new_usertype("ESM3_Container"); + record[sol::meta_function::to_string] = [](const ESM::Container& rec) -> std::string { + return "ESM3_Container[" + rec.mId.toDebugString() + "]"; + }; + record["id"] + = sol::readonly_property([](const ESM::Container& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Container& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property([](const ESM::Container& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Container& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); + }); + record["weight"] = sol::readonly_property([](const ESM::Container& rec) -> float { return rec.mWeight; }); + record["isOrganic"] = sol::readonly_property( + [](const ESM::Container& rec) -> bool { return rec.mFlags & ESM::Container::Organic; }); + record["isRespawning"] = sol::readonly_property( + [](const ESM::Container& rec) -> bool { return rec.mFlags & ESM::Container::Respawn; }); + } +} diff --git a/apps/openmw/mwlua/types/creature.cpp b/apps/openmw/mwlua/types/creature.cpp new file mode 100644 index 00000000000..a171f147e64 --- /dev/null +++ b/apps/openmw/mwlua/types/creature.cpp @@ -0,0 +1,79 @@ +#include "../stats.hpp" +#include "actor.hpp" +#include "types.hpp" + +#include +#include +#include +#include +#include + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addCreatureBindings(sol::table creature, const Context& context) + { + sol::state_view lua = context.sol(); + creature["TYPE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Creatures", ESM::Creature::Creatures }, + { "Daedra", ESM::Creature::Daedra }, + { "Undead", ESM::Creature::Undead }, + { "Humanoid", ESM::Creature::Humanoid }, + })); + + addRecordFunctionBinding(creature, context); + + sol::usertype record = lua.new_usertype("ESM3_Creature"); + record[sol::meta_function::to_string] + = [](const ESM::Creature& rec) { return "ESM3_Creature[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Creature& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Creature& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property( + [](const ESM::Creature& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["mwscript"] = sol::readonly_property([](const ESM::Creature& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); + }); + record["baseCreature"] = sol::readonly_property( + [](const ESM::Creature& rec) -> std::string { return rec.mOriginal.serializeText(); }); + record["soulValue"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mSoul; }); + record["type"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mType; }); + record["baseGold"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mGold; }); + record["combatSkill"] + = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mCombat; }); + record["magicSkill"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mMagic; }); + record["stealthSkill"] + = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mStealth; }); + record["attack"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Creature& rec) -> sol::table { + sol::table res(lua, sol::create); + int index = 1; + for (auto attack : rec.mData.mAttack) + res[index++] = attack; + return LuaUtil::makeReadOnly(res); + }); + record["canFly"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Flies; }); + record["canSwim"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Swims; }); + record["canUseWeapons"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Weapon; }); + record["canWalk"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Walks; }); + record["isBiped"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Bipedal; }); + record["isEssential"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Essential; }); + record["isRespawning"] = sol::readonly_property( + [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Respawn; }); + + addActorServicesBindings(record, context); + } +} diff --git a/apps/openmw/mwlua/types/door.cpp b/apps/openmw/mwlua/types/door.cpp new file mode 100644 index 00000000000..7133690640d --- /dev/null +++ b/apps/openmw/mwlua/types/door.cpp @@ -0,0 +1,153 @@ +#include "types.hpp" + +#include "../localscripts.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "apps/openmw/mwworld/class.hpp" +#include "apps/openmw/mwworld/worldmodel.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + static const MWWorld::Ptr& doorPtr(const Object& o) + { + return verifyType(ESM::REC_DOOR, o.ptr()); + } + + static const MWWorld::Ptr& door4Ptr(const Object& o) + { + return verifyType(ESM::REC_DOOR4, o.ptr()); + } + + void addDoorBindings(sol::table door, const Context& context) + { + sol::state_view lua = context.sol(); + door["STATE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Idle", MWWorld::DoorState::Idle }, + { "Opening", MWWorld::DoorState::Opening }, + { "Closing", MWWorld::DoorState::Closing }, + })); + door["getDoorState"] = [](const Object& o) -> MWWorld::DoorState { + const MWWorld::Ptr& door = doorPtr(o); + return door.getClass().getDoorState(door); + }; + door["isOpen"] = [](const Object& o) { + const MWWorld::Ptr& door = doorPtr(o); + bool doorIsIdle = door.getClass().getDoorState(door) == MWWorld::DoorState::Idle; + bool doorIsOpen = door.getRefData().getPosition().rot[2] != door.getCellRef().getPosition().rot[2]; + + return doorIsIdle && doorIsOpen; + }; + door["isClosed"] = [](const Object& o) { + const MWWorld::Ptr& door = doorPtr(o); + bool doorIsIdle = door.getClass().getDoorState(door) == MWWorld::DoorState::Idle; + bool doorIsOpen = door.getRefData().getPosition().rot[2] != door.getCellRef().getPosition().rot[2]; + + return doorIsIdle && !doorIsOpen; + }; + door["activateDoor"] = [](const Object& o, sol::optional openState) { + bool allowChanges + = dynamic_cast(&o) != nullptr || dynamic_cast(&o) != nullptr; + if (!allowChanges) + throw std::runtime_error("Can only be used in global scripts or in local scripts on self."); + + const MWWorld::Ptr& door = doorPtr(o); + auto world = MWBase::Environment::get().getWorld(); + + if (!openState.has_value()) + world->activateDoor(door); + else if (*openState) + world->activateDoor(door, MWWorld::DoorState::Opening); + else + world->activateDoor(door, MWWorld::DoorState::Closing); + }; + door["isTeleport"] = [](const Object& o) { return doorPtr(o).getCellRef().getTeleport(); }; + door["destPosition"] + = [](const Object& o) -> osg::Vec3f { return doorPtr(o).getCellRef().getDoorDest().asVec3(); }; + door["destRotation"] = [](const Object& o) -> LuaUtil::TransformQ { + return { Misc::Convert::makeOsgQuat(doorPtr(o).getCellRef().getDoorDest().rot) }; + }; + door["destCell"] = [](sol::this_state lua, const Object& o) -> sol::object { + const MWWorld::CellRef& cellRef = doorPtr(o).getCellRef(); + if (!cellRef.getTeleport()) + return sol::nil; + MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getCell(cellRef.getDestCell()); + if (dynamic_cast(&o)) + return sol::make_object(lua, GCell{ &cell }); + else + return sol::make_object(lua, LCell{ &cell }); + }; + + addRecordFunctionBinding(door, context); + + sol::usertype record = lua.new_usertype("ESM3_Door"); + record[sol::meta_function::to_string] + = [](const ESM::Door& rec) -> std::string { return "ESM3_Door[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property( + [](const ESM::Door& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Door& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); + record["openSound"] = sol::readonly_property( + [](const ESM::Door& rec) -> std::string { return rec.mOpenSound.serializeText(); }); + record["closeSound"] = sol::readonly_property( + [](const ESM::Door& rec) -> std::string { return rec.mCloseSound.serializeText(); }); + } + + void addESM4DoorBindings(sol::table door, const Context& context) + { + door["isTeleport"] = [](const Object& o) { return door4Ptr(o).getCellRef().getTeleport(); }; + door["destPosition"] + = [](const Object& o) -> osg::Vec3f { return door4Ptr(o).getCellRef().getDoorDest().asVec3(); }; + door["destRotation"] = [](const Object& o) -> LuaUtil::TransformQ { + return { Misc::Convert::makeOsgQuat(door4Ptr(o).getCellRef().getDoorDest().rot) }; + }; + door["destCell"] = [](sol::this_state lua, const Object& o) -> sol::object { + const MWWorld::CellRef& cellRef = door4Ptr(o).getCellRef(); + if (!cellRef.getTeleport()) + return sol::nil; + MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getCell(cellRef.getDestCell()); + if (dynamic_cast(&o)) + return sol::make_object(lua, GCell{ &cell }); + else + return sol::make_object(lua, LCell{ &cell }); + }; + + addRecordFunctionBinding(door, context, "ESM4Door"); + + sol::usertype record = context.sol().new_usertype("ESM4_Door"); + record[sol::meta_function::to_string] = [](const ESM4::Door& rec) -> std::string { + return "ESM4_Door[" + ESM::RefId(rec.mId).toDebugString() + "]"; + }; + record["id"] = sol::readonly_property( + [](const ESM4::Door& rec) -> std::string { return ESM::RefId(rec.mId).serializeText(); }); + record["name"] = sol::readonly_property([](const ESM4::Door& rec) -> std::string { return rec.mFullName; }); + record["model"] = sol::readonly_property( + [](const ESM4::Door& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["isAutomatic"] = sol::readonly_property( + [](const ESM4::Door& rec) -> bool { return rec.mDoorFlags & ESM4::Door::Flag_AutomaticDoor; }); + } +} diff --git a/apps/openmw/mwlua/types/ingredient.cpp b/apps/openmw/mwlua/types/ingredient.cpp new file mode 100644 index 00000000000..e05dc2bdb80 --- /dev/null +++ b/apps/openmw/mwlua/types/ingredient.cpp @@ -0,0 +1,67 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addIngredientBindings(sol::table ingredient, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + addRecordFunctionBinding(ingredient, context); + sol::state_view lua = context.sol(); + sol::usertype record = lua.new_usertype(("ESM3_Ingredient")); + record[sol::meta_function::to_string] + = [](const ESM::Ingredient& rec) { return "ESM3_Ingredient[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Ingredient& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Ingredient& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property([](const ESM::Ingredient& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Ingredient& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); + }); + record["icon"] = sol::readonly_property([vfs](const ESM::Ingredient& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["weight"] + = sol::readonly_property([](const ESM::Ingredient& rec) -> float { return rec.mData.mWeight; }); + record["value"] = sol::readonly_property([](const ESM::Ingredient& rec) -> int { return rec.mData.mValue; }); + record["effects"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Ingredient& rec) -> sol::table { + sol::table res(lua, sol::create); + for (size_t i = 0; i < 4; ++i) + { + if (rec.mData.mEffectID[i] < 0) + continue; + ESM::IndexedENAMstruct effect; + effect.mData.mEffectID = rec.mData.mEffectID[i]; + effect.mData.mSkill = rec.mData.mSkills[i]; + effect.mData.mAttribute = rec.mData.mAttributes[i]; + effect.mData.mRange = ESM::RT_Self; + effect.mData.mArea = 0; + effect.mData.mDuration = 0; + effect.mData.mMagnMin = 0; + effect.mData.mMagnMax = 0; + effect.mIndex = i; + res[LuaUtil::toLuaIndex(i)] = effect; + } + return res; + }); + } +} diff --git a/apps/openmw/mwlua/types/item.cpp b/apps/openmw/mwlua/types/item.cpp new file mode 100644 index 00000000000..ba995a9808c --- /dev/null +++ b/apps/openmw/mwlua/types/item.cpp @@ -0,0 +1,32 @@ +#include + +#include "../../mwmechanics/spellutil.hpp" +#include "../../mwworld/class.hpp" + +#include "../itemdata.hpp" + +#include "types.hpp" + +namespace MWLua +{ + void addItemBindings(sol::table item, const Context& context) + { + // Deprecated. Moved to itemData; should be removed later + item["getEnchantmentCharge"] = [](const Object& object) -> sol::optional { + float charge = object.ptr().getCellRef().getEnchantmentCharge(); + if (charge == -1) + return sol::nullopt; + else + return charge; + }; + item["setEnchantmentCharge"] = [](const GObject& object, sol::optional charge) { + object.ptr().getCellRef().setEnchantmentCharge(charge.value_or(-1)); + }; + item["isRestocking"] + = [](const Object& object) -> bool { return object.ptr().getCellRef().getCount(false) < 0; }; + + item["isCarriable"] = [](const Object& object) -> bool { return object.ptr().getClass().isItem(object.ptr()); }; + + addItemDataBindings(item, context); + } +} diff --git a/apps/openmw/mwlua/types/levelledlist.cpp b/apps/openmw/mwlua/types/levelledlist.cpp new file mode 100644 index 00000000000..fd848d91211 --- /dev/null +++ b/apps/openmw/mwlua/types/levelledlist.cpp @@ -0,0 +1,60 @@ +#include "types.hpp" + +#include +#include + +#include "../../mwbase/environment.hpp" +#include "../../mwbase/world.hpp" +#include "../../mwmechanics/levelledlist.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addLevelledCreatureBindings(sol::table list, const Context& context) + { + auto state = context.sol(); + auto item = state.new_usertype("ESM3_LevelledListItem"); + item["id"] = sol::readonly_property( + [](const ESM::LevelledListBase::LevelItem& rec) -> std::string { return rec.mId.serializeText(); }); + item["level"] + = sol::readonly_property([](const ESM::LevelledListBase::LevelItem& rec) -> int { return rec.mLevel; }); + item[sol::meta_function::to_string] = [](const ESM::LevelledListBase::LevelItem& rec) -> std::string { + return "ESM3_LevelledListItem[" + rec.mId.toDebugString() + ", " + std::to_string(rec.mLevel) + "]"; + }; + + addRecordFunctionBinding(list, context); + + auto record = state.new_usertype("ESM3_CreatureLevelledList"); + record[sol::meta_function::to_string] = [](const ESM::CreatureLevList& rec) -> std::string { + return "ESM3_CreatureLevelledList[" + rec.mId.toDebugString() + "]"; + }; + record["id"] = sol::readonly_property( + [](const ESM::CreatureLevList& rec) -> std::string { return rec.mId.serializeText(); }); + record["chanceNone"] = sol::readonly_property( + [](const ESM::CreatureLevList& rec) -> float { return std::clamp(rec.mChanceNone / 100.f, 0.f, 1.f); }); + record["creatures"] = sol::readonly_property([&](const ESM::CreatureLevList& rec) -> sol::table { + sol::table res(state, sol::create); + for (size_t i = 0; i < rec.mList.size(); ++i) + res[LuaUtil::toLuaIndex(i)] = rec.mList[i]; + return res; + }); + record["calculateFromAllLevels"] = sol::readonly_property( + [](const ESM::CreatureLevList& rec) -> bool { return rec.mFlags & ESM::CreatureLevList::AllLevels; }); + + record["getRandomId"] = [](const ESM::CreatureLevList& rec, int level) -> std::string { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + return MWMechanics::getLevelledItem(&rec, true, prng, level).serializeText(); + }; + } +} diff --git a/apps/openmw/mwlua/types/light.cpp b/apps/openmw/mwlua/types/light.cpp new file mode 100644 index 00000000000..788f6d3e4ec --- /dev/null +++ b/apps/openmw/mwlua/types/light.cpp @@ -0,0 +1,126 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace +{ + void setRecordFlag(const sol::table& rec, const std::string& key, int flag, ESM::Light& record) + { + if (auto luaFlag = rec[key]; luaFlag != sol::nil) + { + if (luaFlag) + { + record.mData.mFlags |= flag; + } + else + { + record.mData.mFlags &= ~flag; + } + } + } + // Populates a light struct from a Lua table. + ESM::Light tableToLight(const sol::table& rec) + { + ESM::Light light; + if (rec["template"] != sol::nil) + light = LuaUtil::cast(rec["template"]); + else + light.blank(); + if (rec["name"] != sol::nil) + light.mName = rec["name"]; + if (rec["model"] != sol::nil) + light.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + light.mIcon = rec["icon"]; + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + light.mScript = ESM::RefId::deserializeText(scriptId); + } + if (rec["weight"] != sol::nil) + light.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + light.mData.mValue = rec["value"]; + if (rec["duration"] != sol::nil) + light.mData.mTime = rec["duration"]; + if (rec["radius"] != sol::nil) + light.mData.mRadius = rec["radius"]; + if (rec["color"] != sol::nil) + light.mData.mColor = rec["color"]; + setRecordFlag(rec, "isCarriable", ESM::Light::Carry, light); + setRecordFlag(rec, "isDynamic", ESM::Light::Dynamic, light); + setRecordFlag(rec, "isFire", ESM::Light::Fire, light); + setRecordFlag(rec, "isFlicker", ESM::Light::Flicker, light); + setRecordFlag(rec, "isFlickerSlow", ESM::Light::FlickerSlow, light); + setRecordFlag(rec, "isNegative", ESM::Light::Negative, light); + setRecordFlag(rec, "isOffByDefault", ESM::Light::OffDefault, light); + setRecordFlag(rec, "isPulse", ESM::Light::Pulse, light); + setRecordFlag(rec, "isPulseSlow", ESM::Light::PulseSlow, light); + + return light; + } +} + +namespace MWLua +{ + void addLightBindings(sol::table light, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + addRecordFunctionBinding(light, context); + light["createRecordDraft"] = tableToLight; + + sol::usertype record = context.sol().new_usertype("ESM3_Light"); + record[sol::meta_function::to_string] + = [](const ESM::Light& rec) -> std::string { return "ESM3_Light[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Light& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Light& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property( + [](const ESM::Light& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["icon"] = sol::readonly_property([vfs](const ESM::Light& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["sound"] + = sol::readonly_property([](const ESM::Light& rec) -> std::string { return rec.mSound.serializeText(); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Light& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); + record["weight"] = sol::readonly_property([](const ESM::Light& rec) -> float { return rec.mData.mWeight; }); + record["value"] = sol::readonly_property([](const ESM::Light& rec) -> int { return rec.mData.mValue; }); + record["duration"] = sol::readonly_property([](const ESM::Light& rec) -> int { return rec.mData.mTime; }); + record["radius"] = sol::readonly_property([](const ESM::Light& rec) -> int { return rec.mData.mRadius; }); + record["color"] = sol::readonly_property([](const ESM::Light& rec) -> int { return rec.mData.mColor; }); + record["isCarriable"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Carry; }); + record["isDynamic"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Dynamic; }); + record["isFire"] + = sol::readonly_property([](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Fire; }); + record["isFlicker"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Flicker; }); + record["isFlickerSlow"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::FlickerSlow; }); + record["isNegative"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Negative; }); + record["isOffByDefault"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::OffDefault; }); + record["isPulse"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::Pulse; }); + record["isPulseSlow"] = sol::readonly_property( + [](const ESM::Light& rec) -> bool { return rec.mData.mFlags & ESM::Light::PulseSlow; }); + } +} diff --git a/apps/openmw/mwlua/types/lockable.cpp b/apps/openmw/mwlua/types/lockable.cpp new file mode 100644 index 00000000000..2569f42ee42 --- /dev/null +++ b/apps/openmw/mwlua/types/lockable.cpp @@ -0,0 +1,84 @@ +#include "types.hpp" + +#include +#include +#include +#include + +#include "apps/openmw/mwworld/esmstore.hpp" + +namespace MWLua +{ + + void addLockableBindings(sol::table lockable) + { + lockable["getLockLevel"] + = [](const Object& object) { return std::abs(object.ptr().getCellRef().getLockLevel()); }; + lockable["isLocked"] = [](const Object& object) { return object.ptr().getCellRef().isLocked(); }; + lockable["getKeyRecord"] = [](const Object& object) -> sol::optional { + ESM::RefId key = object.ptr().getCellRef().getKey(); + if (key.empty()) + return sol::nullopt; + return MWBase::Environment::get().getESMStore()->get().find(key); + }; + lockable["lock"] = [](const GObject& object, sol::optional lockLevel) { + object.ptr().getCellRef().setLocked(true); + + int level = 1; + + if (lockLevel) + level = lockLevel.value(); + else if (object.ptr().getCellRef().getLockLevel() < 0) + level = -object.ptr().getCellRef().getLockLevel(); + else if (object.ptr().getCellRef().getLockLevel() > 0) + level = object.ptr().getCellRef().getLockLevel(); + + object.ptr().getCellRef().setLockLevel(level); + }; + lockable["unlock"] = [](const GObject& object) { + if (!object.ptr().getCellRef().isLocked()) + return; + object.ptr().getCellRef().setLocked(false); + + object.ptr().getCellRef().setLockLevel(-object.ptr().getCellRef().getLockLevel()); + }; + lockable["setTrapSpell"] = [](const GObject& object, const sol::object& spellOrId) { + if (spellOrId == sol::nil) + { + object.ptr().getCellRef().setTrap(ESM::RefId()); // remove the trap value + return; + } + if (spellOrId.is()) + object.ptr().getCellRef().setTrap(spellOrId.as()->mId); + else + { + ESM::RefId spellId = ESM::RefId::deserializeText(LuaUtil::cast(spellOrId)); + const auto& spellStore = MWBase::Environment::get().getESMStore()->get(); + const ESM::Spell* spell = spellStore.find(spellId); + object.ptr().getCellRef().setTrap(spell->mId); + } + }; + lockable["setKeyRecord"] = [](const GObject& object, const sol::object& itemOrRecordId) { + if (itemOrRecordId == sol::nil) + { + object.ptr().getCellRef().setKey(ESM::RefId()); // remove the trap value + return; + } + if (itemOrRecordId.is()) + object.ptr().getCellRef().setKey(itemOrRecordId.as()->mId); + else + { + ESM::RefId miscId = ESM::RefId::deserializeText(LuaUtil::cast(itemOrRecordId)); + const auto& keyStore = MWBase::Environment::get().getESMStore()->get(); + const ESM::Miscellaneous* key = keyStore.find(miscId); + object.ptr().getCellRef().setKey(key->mId); + } + }; + lockable["getTrapSpell"] = [](sol::this_state lua, const Object& o) -> sol::optional { + ESM::RefId trap = o.ptr().getCellRef().getTrap(); + if (trap.empty()) + return sol::nullopt; + return MWBase::Environment::get().getESMStore()->get().find(trap); + }; + } +} diff --git a/apps/openmw/mwlua/types/lockpick.cpp b/apps/openmw/mwlua/types/lockpick.cpp new file mode 100644 index 00000000000..be25d855ec9 --- /dev/null +++ b/apps/openmw/mwlua/types/lockpick.cpp @@ -0,0 +1,48 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addLockpickBindings(sol::table lockpick, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + addRecordFunctionBinding(lockpick, context); + + sol::usertype record = context.sol().new_usertype("ESM3_Lockpick"); + record[sol::meta_function::to_string] + = [](const ESM::Lockpick& rec) { return "ESM3_Lockpick[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Lockpick& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Lockpick& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property( + [](const ESM::Lockpick& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["mwscript"] = sol::readonly_property([](const ESM::Lockpick& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); + }); + record["icon"] = sol::readonly_property([vfs](const ESM::Lockpick& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["maxCondition"] + = sol::readonly_property([](const ESM::Lockpick& rec) -> int { return rec.mData.mUses; }); + record["value"] = sol::readonly_property([](const ESM::Lockpick& rec) -> int { return rec.mData.mValue; }); + record["weight"] = sol::readonly_property([](const ESM::Lockpick& rec) -> float { return rec.mData.mWeight; }); + record["quality"] + = sol::readonly_property([](const ESM::Lockpick& rec) -> float { return rec.mData.mQuality; }); + } +} diff --git a/apps/openmw/mwlua/types/misc.cpp b/apps/openmw/mwlua/types/misc.cpp new file mode 100644 index 00000000000..2695abff9ba --- /dev/null +++ b/apps/openmw/mwlua/types/misc.cpp @@ -0,0 +1,99 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace +{ + // Populates a misc struct from a Lua table. + ESM::Miscellaneous tableToMisc(const sol::table& rec) + { + ESM::Miscellaneous misc; + if (rec["template"] != sol::nil) + misc = LuaUtil::cast(rec["template"]); + else + misc.blank(); + if (rec["name"] != sol::nil) + misc.mName = rec["name"]; + if (rec["model"] != sol::nil) + misc.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + misc.mIcon = rec["icon"]; + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + misc.mScript = ESM::RefId::deserializeText(scriptId); + } + if (rec["weight"] != sol::nil) + misc.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + misc.mData.mValue = rec["value"]; + return misc; + } +} + +namespace MWLua +{ + void addMiscellaneousBindings(sol::table miscellaneous, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + addRecordFunctionBinding(miscellaneous, context); + miscellaneous["createRecordDraft"] = tableToMisc; + + // Deprecated. Moved to itemData; should be removed later + miscellaneous["setSoul"] = [](const GObject& object, std::string_view soulId) { + ESM::RefId creature = ESM::RefId::deserializeText(soulId); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + + if (!store.get().search(creature)) + { + // TODO: Add Support for NPC Souls + throw std::runtime_error("Cannot use non-existent creature as a soul: " + std::string(soulId)); + } + + object.ptr().getCellRef().setSoul(creature); + }; + miscellaneous["getSoul"] = [](const Object& object) -> sol::optional { + ESM::RefId soul = object.ptr().getCellRef().getSoul(); + return LuaUtil::serializeRefId(soul); + }; + miscellaneous["soul"] = miscellaneous["getSoul"]; // for compatibility; should be removed later + + sol::usertype record = context.sol().new_usertype("ESM3_Miscellaneous"); + record[sol::meta_function::to_string] + = [](const ESM::Miscellaneous& rec) { return "ESM3_Miscellaneous[" + rec.mId.toDebugString() + "]"; }; + record["id"] = sol::readonly_property( + [](const ESM::Miscellaneous& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); + }); + record["mwscript"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> sol::optional { + return LuaUtil::serializeRefId(rec.mScript); + }); + record["icon"] = sol::readonly_property([vfs](const ESM::Miscellaneous& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["isKey"] = sol::readonly_property( + [](const ESM::Miscellaneous& rec) -> bool { return rec.mData.mFlags & ESM::Miscellaneous::Key; }); + record["value"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> int { return rec.mData.mValue; }); + record["weight"] + = sol::readonly_property([](const ESM::Miscellaneous& rec) -> float { return rec.mData.mWeight; }); + } +} diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp new file mode 100644 index 00000000000..c0cfbc90db6 --- /dev/null +++ b/apps/openmw/mwlua/types/npc.cpp @@ -0,0 +1,360 @@ +#include "actor.hpp" +#include "types.hpp" + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwbase/mechanicsmanager.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwmechanics/npcstats.hpp" +#include "apps/openmw/mwworld/class.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" + +#include "../classbindings.hpp" +#include "../localscripts.hpp" +#include "../racebindings.hpp" +#include "../stats.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace +{ + size_t getValidRanksCount(const ESM::Faction* faction) + { + if (!faction) + return 0; + + for (size_t i = 0; i < faction->mRanks.size(); i++) + { + if (faction->mRanks[i].empty()) + return i; + } + + return faction->mRanks.size(); + } + + ESM::RefId parseFactionId(std::string_view faction) + { + ESM::RefId id = ESM::RefId::deserializeText(faction); + const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); + if (!store->get().search(id)) + throw std::runtime_error("Faction '" + std::string(faction) + "' does not exist"); + return id; + } + + void verifyPlayer(const MWLua::Object& o) + { + if (o.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) + throw std::runtime_error("The argument must be a player!"); + } + + void verifyNpc(const MWWorld::Class& cls) + { + if (!cls.isNpc()) + throw std::runtime_error("The argument must be a NPC!"); + } +} + +namespace MWLua +{ + void addNpcBindings(sol::table npc, const Context& context) + { + addNpcStatsBindings(npc, context); + + addRecordFunctionBinding(npc, context); + + sol::state_view lua = context.sol(); + + sol::usertype record = lua.new_usertype("ESM3_NPC"); + record[sol::meta_function::to_string] + = [](const ESM::NPC& rec) { return "ESM3_NPC[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mName; }); + record["race"] + = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mRace.serializeText(); }); + record["class"] + = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mClass.serializeText(); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::NPC& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); + record["hair"] + = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHair.serializeText(); }); + record["baseDisposition"] + = sol::readonly_property([](const ESM::NPC& rec) -> int { return (int)rec.mNpdt.mDisposition; }); + record["head"] + = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHead.serializeText(); }); + record["model"] = sol::readonly_property( + [](const ESM::NPC& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["isEssential"] + = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Essential; }); + record["isMale"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.isMale(); }); + record["isRespawning"] + = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; }); + record["baseGold"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mNpdt.mGold; }); + addActorServicesBindings(record, context); + + npc["classes"] = initClassRecordBindings(context); + npc["races"] = initRaceRecordBindings(context); + + // This function is game-specific, in future we should replace it with something more universal. + npc["isWerewolf"] = [](const Object& o) { + const MWWorld::Class& cls = o.ptr().getClass(); + if (cls.isNpc()) + return cls.getNpcStats(o.ptr()).isWerewolf(); + else + throw std::runtime_error("NPC or Player expected"); + }; + + npc["getDisposition"] = [](const Object& o, const Object& player) -> int { + const MWWorld::Class& cls = o.ptr().getClass(); + verifyPlayer(player); + verifyNpc(cls); + return MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(o.ptr()); + }; + + npc["getBaseDisposition"] = [](const Object& o, const Object& player) -> int { + const MWWorld::Class& cls = o.ptr().getClass(); + verifyPlayer(player); + verifyNpc(cls); + return cls.getNpcStats(o.ptr()).getBaseDisposition(); + }; + + npc["setBaseDisposition"] = [](Object& o, const Object& player, int value) { + if (dynamic_cast(&o) && !dynamic_cast(&o)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Class& cls = o.ptr().getClass(); + verifyPlayer(player); + verifyNpc(cls); + cls.getNpcStats(o.ptr()).setBaseDisposition(value); + }; + + npc["modifyBaseDisposition"] = [](Object& o, const Object& player, int value) { + if (dynamic_cast(&o) && !dynamic_cast(&o)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Class& cls = o.ptr().getClass(); + verifyPlayer(player); + verifyNpc(cls); + auto& stats = cls.getNpcStats(o.ptr()); + stats.setBaseDisposition(stats.getBaseDisposition() + value); + }; + + npc["getFactionRank"] = [](const Object& actor, std::string_view faction) -> size_t { + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + const MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + if (npcStats.isInFaction(factionId)) + { + int factionRank = npcStats.getFactionRank(factionId); + return LuaUtil::toLuaIndex(factionRank); + } + } + else + { + ESM::RefId primaryFactionId = ptr.getClass().getPrimaryFaction(ptr); + if (factionId == primaryFactionId) + return LuaUtil::toLuaIndex(ptr.getClass().getPrimaryFactionRank(ptr)); + } + return 0; + }; + + npc["setFactionRank"] = [](Object& actor, std::string_view faction, int value) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + const ESM::Faction* factionPtr + = MWBase::Environment::get().getESMStore()->get().find(factionId); + + auto ranksCount = static_cast(getValidRanksCount(factionPtr)); + if (value <= 0 || value > ranksCount) + throw std::runtime_error("Requested rank does not exist"); + + auto targetRank = LuaUtil::fromLuaIndex(std::clamp(value, 1, ranksCount)); + + if (ptr != MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + ESM::RefId primaryFactionId = ptr.getClass().getPrimaryFaction(ptr); + if (factionId != primaryFactionId) + throw std::runtime_error("Only players can modify ranks in non-primary factions"); + } + + MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + if (!npcStats.isInFaction(factionId)) + throw std::runtime_error("Target actor is not a member of faction " + factionId.toDebugString()); + + npcStats.setFactionRank(factionId, targetRank); + }; + + npc["modifyFactionRank"] = [](Object& actor, std::string_view faction, int value) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + if (value == 0) + return; + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + const ESM::Faction* factionPtr + = MWBase::Environment::get().getESMStore()->get().search(factionId); + if (!factionPtr) + return; + + auto ranksCount = static_cast(getValidRanksCount(factionPtr)); + + MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + int currentRank = npcStats.getFactionRank(factionId); + if (currentRank >= 0) + npcStats.setFactionRank(factionId, std::clamp(currentRank + value, 0, ranksCount - 1)); + else + throw std::runtime_error("Target actor is not a member of faction " + factionId.toDebugString()); + + return; + } + + ESM::RefId primaryFactionId = ptr.getClass().getPrimaryFaction(ptr); + if (factionId != primaryFactionId) + throw std::runtime_error("Only players can modify ranks in non-primary factions"); + + // If we already changed rank for this NPC, modify current rank in the NPC stats. + // Otherwise take rank from base NPC record, adjust it and put it to NPC data. + int currentRank = npcStats.getFactionRank(factionId); + if (currentRank < 0) + { + currentRank = ptr.getClass().getPrimaryFactionRank(ptr); + npcStats.joinFaction(factionId); + } + + npcStats.setFactionRank(factionId, std::clamp(currentRank + value, 0, ranksCount - 1)); + }; + + npc["joinFaction"] = [](Object& actor, std::string_view faction) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + int currentRank = npcStats.getFactionRank(factionId); + if (currentRank < 0) + npcStats.joinFaction(factionId); + return; + } + + throw std::runtime_error("Only player can join factions"); + }; + + npc["leaveFaction"] = [](Object& actor, std::string_view faction) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + ptr.getClass().getNpcStats(ptr).setFactionRank(factionId, -1); + return; + } + + throw std::runtime_error("Only player can leave factions"); + }; + + npc["getFactionReputation"] = [](const Object& actor, std::string_view faction) { + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + return ptr.getClass().getNpcStats(ptr).getFactionReputation(factionId); + }; + + npc["setFactionReputation"] = [](Object& actor, std::string_view faction, int value) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + ptr.getClass().getNpcStats(ptr).setFactionReputation(factionId, value); + }; + + npc["modifyFactionReputation"] = [](Object& actor, std::string_view faction, int value) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + + MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + int existingReputation = npcStats.getFactionReputation(factionId); + npcStats.setFactionReputation(factionId, existingReputation + value); + }; + + npc["expel"] = [](Object& actor, std::string_view faction) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + ptr.getClass().getNpcStats(ptr).expell(factionId, false); + }; + npc["clearExpelled"] = [](Object& actor, std::string_view faction) { + if (dynamic_cast(&actor) && !dynamic_cast(&actor)) + throw std::runtime_error("Local scripts can modify only self"); + + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + ptr.getClass().getNpcStats(ptr).clearExpelled(factionId); + }; + npc["isExpelled"] = [](const Object& actor, std::string_view faction) { + const MWWorld::Ptr ptr = actor.ptr(); + ESM::RefId factionId = parseFactionId(faction); + return ptr.getClass().getNpcStats(ptr).getExpelled(factionId); + }; + npc["getFactions"] = [](sol::this_state lua, const Object& actor) { + const MWWorld::Ptr ptr = actor.ptr(); + MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + sol::table res(lua, sol::create); + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + for (const auto& [factionId, _] : npcStats.getFactionRanks()) + res.add(factionId.serializeText()); + return res; + } + + ESM::RefId primaryFactionId = ptr.getClass().getPrimaryFaction(ptr); + if (primaryFactionId.empty()) + return res; + + res.add(primaryFactionId.serializeText()); + + return res; + }; + npc["getCapacity"] = [](const Object& actor) -> float { + const MWWorld::Ptr ptr = actor.ptr(); + return ptr.getClass().getCapacity(ptr); + }; + } +} diff --git a/apps/openmw/mwlua/types/player.cpp b/apps/openmw/mwlua/types/player.cpp new file mode 100644 index 00000000000..97619dc3be2 --- /dev/null +++ b/apps/openmw/mwlua/types/player.cpp @@ -0,0 +1,216 @@ +#include "types.hpp" + +#include "../birthsignbindings.hpp" +#include "../luamanagerimp.hpp" + +#include "apps/openmw/mwbase/inputmanager.hpp" +#include "apps/openmw/mwbase/journal.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwmechanics/npcstats.hpp" +#include "apps/openmw/mwworld/class.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" +#include "apps/openmw/mwworld/globals.hpp" +#include "apps/openmw/mwworld/player.hpp" + +#include + +namespace MWLua +{ + struct Quests + { + bool mMutable = false; + }; + struct Quest + { + ESM::RefId mQuestId; + bool mMutable = false; + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace +{ + ESM::RefId toBirthSignId(const sol::object& recordOrId) + { + if (recordOrId.is()) + return recordOrId.as()->mId; + std::string_view textId = LuaUtil::cast(recordOrId); + ESM::RefId id = ESM::RefId::deserializeText(textId); + if (!MWBase::Environment::get().getESMStore()->get().search(id)) + throw std::runtime_error("Failed to find birth sign: " + std::string(textId)); + return id; + } +} + +namespace MWLua +{ + static void verifyPlayer(const Object& player) + { + if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr()) + throw std::runtime_error("The argument must be a player!"); + } + + void addPlayerBindings(sol::table player, const Context& context) + { + MWBase::Journal* const journal = MWBase::Environment::get().getJournal(); + + player["quests"] = [](const Object& player) { + verifyPlayer(player); + bool allowChanges = dynamic_cast(&player) != nullptr + || dynamic_cast(&player) != nullptr; + return Quests{ .mMutable = allowChanges }; + }; + sol::state_view lua = context.sol(); + sol::usertype quests = lua.new_usertype("Quests"); + quests[sol::meta_function::to_string] = [](const Quests& quests) { return "Quests"; }; + quests[sol::meta_function::index] = [](const Quests& quests, std::string_view questId) -> sol::optional { + ESM::RefId quest = ESM::RefId::deserializeText(questId); + const ESM::Dialogue* dial = MWBase::Environment::get().getESMStore()->get().search(quest); + if (dial == nullptr || dial->mType != ESM::Dialogue::Journal) + return sol::nullopt; + return Quest{ .mQuestId = quest, .mMutable = quests.mMutable }; + }; + quests[sol::meta_function::pairs] = [journal](const Quests& quests) { + std::vector ids; + for (auto it = journal->questBegin(); it != journal->questEnd(); ++it) + ids.push_back(it->first); + size_t i = 0; + return [ids = std::move(ids), i, + allowChanges = quests.mMutable]() mutable -> sol::optional> { + if (i >= ids.size()) + return sol::nullopt; + const ESM::RefId& id = ids[i++]; + return std::make_tuple(id.serializeText(), Quest{ .mQuestId = id, .mMutable = allowChanges }); + }; + }; + + sol::usertype quest = lua.new_usertype("Quest"); + quest[sol::meta_function::to_string] + = [](const Quest& quest) { return "Quest[" + quest.mQuestId.serializeText() + "]"; }; + + auto getQuestStage = [journal](const Quest& q) -> int { + const MWDialogue::Quest* quest = journal->getQuestOrNull(q.mQuestId); + if (quest == nullptr) + return 0; + return journal->getJournalIndex(q.mQuestId); + }; + auto setQuestStage = [context](const Quest& q, int stage) { + if (!q.mMutable) + throw std::runtime_error("Value can only be changed in global or player scripts!"); + context.mLuaManager->addAction( + [q, stage] { MWBase::Environment::get().getJournal()->setJournalIndex(q.mQuestId, stage); }, + "setQuestStageAction"); + }; + quest["stage"] = sol::property(getQuestStage, setQuestStage); + + quest["id"] = sol::readonly_property([](const Quest& q) -> std::string { return q.mQuestId.serializeText(); }); + quest["started"] = sol::readonly_property( + [journal](const Quest& q) { return journal->getQuestOrNull(q.mQuestId) != nullptr; }); + quest["finished"] = sol::property( + [journal](const Quest& q) -> bool { + const MWDialogue::Quest* quest = journal->getQuestOrNull(q.mQuestId); + if (quest == nullptr) + return false; + return quest->isFinished(); + }, + [journal, context](const Quest& q, bool finished) { + if (!q.mMutable) + throw std::runtime_error("Value can only be changed in global or player scripts!"); + context.mLuaManager->addAction( + [q, finished, journal] { journal->getOrStartQuest(q.mQuestId).setFinished(finished); }, + "setQuestFinishedAction"); + }); + quest["addJournalEntry"] = [context](const Quest& q, int stage, sol::optional actor) { + if (!q.mMutable) + throw std::runtime_error("Can only be used in global or player scripts!"); + // The journal mwscript function has a try function here, we will make the lua function throw an + // error. However, the addAction will cause it to error outside of this function. + context.mLuaManager->addAction( + [actor = std::move(actor), q, stage] { + MWWorld::Ptr actorPtr; + if (actor) + actorPtr = actor->ptr(); + MWBase::Environment::get().getJournal()->addEntry(q.mQuestId, stage, actorPtr); + }, + "addJournalEntryAction"); + }; + + player["CONTROL_SWITCH"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Controls", "playercontrols" }, + { "Fighting", "playerfighting" }, + { "Jumping", "playerjumping" }, + { "Looking", "playerlooking" }, + { "Magic", "playermagic" }, + { "ViewMode", "playerviewswitch" }, + { "VanityMode", "vanitymode" }, + })); + + MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); + player["getControlSwitch"] = [input](const Object& player, std::string_view key) { + verifyPlayer(player); + return input->getControlSwitch(key); + }; + player["setControlSwitch"] = [input](const Object& player, std::string_view key, bool v) { + verifyPlayer(player); + if (dynamic_cast(&player) && !dynamic_cast(&player)) + throw std::runtime_error("Only player and global scripts can toggle control switches."); + input->toggleControlSwitch(key, v); + }; + player["isTeleportingEnabled"] = [](const Object& player) -> bool { + verifyPlayer(player); + return MWBase::Environment::get().getWorld()->isTeleportingEnabled(); + }; + player["setTeleportingEnabled"] = [](const Object& player, bool state) { + verifyPlayer(player); + if (dynamic_cast(&player) && !dynamic_cast(&player)) + throw std::runtime_error("Only player and global scripts can toggle teleportation."); + MWBase::Environment::get().getWorld()->enableTeleporting(state); + }; + player["sendMenuEvent"] = [context](const Object& player, std::string eventName, const sol::object& eventData) { + verifyPlayer(player); + context.mLuaEvents->addMenuEvent({ std::move(eventName), LuaUtil::serialize(eventData) }); + }; + + player["getCrimeLevel"] = [](const Object& o) -> int { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getNpcStats(o.ptr()).getBounty(); + }; + player["setCrimeLevel"] = [](const Object& o, int amount) { + verifyPlayer(o); + if (!dynamic_cast(&o)) + throw std::runtime_error("Only global scripts can change crime level"); + const MWWorld::Class& cls = o.ptr().getClass(); + cls.getNpcStats(o.ptr()).setBounty(amount); + if (amount == 0) + MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); + }; + player["isCharGenFinished"] = [](const Object&) -> bool { + return MWBase::Environment::get().getWorld()->getGlobalFloat(MWWorld::Globals::sCharGenState) == -1; + }; + + player["birthSigns"] = initBirthSignRecordBindings(context); + player["getBirthSign"] = [](const Object& player) -> std::string { + verifyPlayer(player); + return MWBase::Environment::get().getWorld()->getPlayer().getBirthSign().serializeText(); + }; + player["setBirthSign"] = [](const Object& player, const sol::object& recordOrId) { + verifyPlayer(player); + if (!dynamic_cast(&player)) + throw std::runtime_error("Only global scripts can change birth signs"); + MWBase::Environment::get().getWorld()->getPlayer().setBirthSign(toBirthSignId(recordOrId)); + }; + } +} diff --git a/apps/openmw/mwlua/types/potion.cpp b/apps/openmw/mwlua/types/potion.cpp new file mode 100644 index 00000000000..4ba3351cc41 --- /dev/null +++ b/apps/openmw/mwlua/types/potion.cpp @@ -0,0 +1,95 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace +{ + // Populates a potion struct from a Lua table. + ESM::Potion tableToPotion(const sol::table& rec) + { + ESM::Potion potion; + if (rec["template"] != sol::nil) + potion = LuaUtil::cast(rec["template"]); + else + potion.blank(); + if (rec["name"] != sol::nil) + potion.mName = rec["name"]; + if (rec["model"] != sol::nil) + potion.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + potion.mIcon = rec["icon"]; + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + potion.mScript = ESM::RefId::deserializeText(scriptId); + } + if (rec["weight"] != sol::nil) + potion.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + potion.mData.mValue = rec["value"]; + if (rec["effects"] != sol::nil) + { + sol::table effectsTable = rec["effects"]; + size_t numEffects = effectsTable.size(); + potion.mEffects.mList.resize(numEffects); + for (size_t i = 0; i < numEffects; ++i) + { + potion.mEffects.mList[i] = LuaUtil::cast(effectsTable[LuaUtil::toLuaIndex(i)]); + } + potion.mEffects.updateIndexes(); + } + return potion; + } +} + +namespace MWLua +{ + void addPotionBindings(sol::table potion, const Context& context) + { + addRecordFunctionBinding(potion, context); + + // Creates a new potion struct but does not store it in MWWorld::ESMStore. + // Global scripts can use world.createRecord to add the potion to the world. + // Note: This potion instance must be owned by lua, so we return it + // by value. + potion["createRecordDraft"] = tableToPotion; + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + sol::state_view lua = context.sol(); + sol::usertype record = lua.new_usertype("ESM3_Potion"); + record[sol::meta_function::to_string] + = [](const ESM::Potion& rec) { return "ESM3_Potion[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Potion& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Potion& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property( + [](const ESM::Potion& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["icon"] = sol::readonly_property([vfs](const ESM::Potion& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Potion& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); + record["weight"] = sol::readonly_property([](const ESM::Potion& rec) -> float { return rec.mData.mWeight; }); + record["value"] = sol::readonly_property([](const ESM::Potion& rec) -> int { return rec.mData.mValue; }); + record["effects"] = sol::readonly_property([lua = lua.lua_state()](const ESM::Potion& rec) -> sol::table { + sol::table res(lua, sol::create); + for (size_t i = 0; i < rec.mEffects.mList.size(); ++i) + res[LuaUtil::toLuaIndex(i)] = rec.mEffects.mList[i]; // ESM::IndexedENAMstruct (effect params) + return res; + }); + } +} diff --git a/apps/openmw/mwlua/types/probe.cpp b/apps/openmw/mwlua/types/probe.cpp new file mode 100644 index 00000000000..7ce269744fe --- /dev/null +++ b/apps/openmw/mwlua/types/probe.cpp @@ -0,0 +1,45 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addProbeBindings(sol::table probe, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + addRecordFunctionBinding(probe, context); + + sol::usertype record = context.sol().new_usertype("ESM3_Probe"); + record[sol::meta_function::to_string] + = [](const ESM::Probe& rec) { return "ESM3_Probe[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Probe& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Probe& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property( + [](const ESM::Probe& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Probe& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); + record["icon"] = sol::readonly_property([vfs](const ESM::Probe& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["maxCondition"] = sol::readonly_property([](const ESM::Probe& rec) -> int { return rec.mData.mUses; }); + record["value"] = sol::readonly_property([](const ESM::Probe& rec) -> int { return rec.mData.mValue; }); + record["weight"] = sol::readonly_property([](const ESM::Probe& rec) -> float { return rec.mData.mWeight; }); + record["quality"] = sol::readonly_property([](const ESM::Probe& rec) -> float { return rec.mData.mQuality; }); + } +} diff --git a/apps/openmw/mwlua/types/repair.cpp b/apps/openmw/mwlua/types/repair.cpp new file mode 100644 index 00000000000..e44bdddd04b --- /dev/null +++ b/apps/openmw/mwlua/types/repair.cpp @@ -0,0 +1,45 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addRepairBindings(sol::table repair, const Context& context) + { + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + addRecordFunctionBinding(repair, context); + + sol::usertype record = context.sol().new_usertype("ESM3_Repair"); + record[sol::meta_function::to_string] + = [](const ESM::Repair& rec) { return "ESM3_Repair[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Repair& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Repair& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property( + [](const ESM::Repair& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Repair& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); + record["icon"] = sol::readonly_property([vfs](const ESM::Repair& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["maxCondition"] = sol::readonly_property([](const ESM::Repair& rec) -> int { return rec.mData.mUses; }); + record["value"] = sol::readonly_property([](const ESM::Repair& rec) -> int { return rec.mData.mValue; }); + record["weight"] = sol::readonly_property([](const ESM::Repair& rec) -> float { return rec.mData.mWeight; }); + record["quality"] = sol::readonly_property([](const ESM::Repair& rec) -> float { return rec.mData.mQuality; }); + } +} diff --git a/apps/openmw/mwlua/types/static.cpp b/apps/openmw/mwlua/types/static.cpp new file mode 100644 index 00000000000..b624db02762 --- /dev/null +++ b/apps/openmw/mwlua/types/static.cpp @@ -0,0 +1,30 @@ +#include "types.hpp" + +#include +#include +#include +#include + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + void addStaticBindings(sol::table stat, const Context& context) + { + addRecordFunctionBinding(stat, context); + + sol::usertype record = context.sol().new_usertype("ESM3_Static"); + record[sol::meta_function::to_string] + = [](const ESM::Static& rec) -> std::string { return "ESM3_Static[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Static& rec) -> std::string { return rec.mId.serializeText(); }); + record["model"] = sol::readonly_property( + [](const ESM::Static& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + } +} diff --git a/apps/openmw/mwlua/types/terminal.cpp b/apps/openmw/mwlua/types/terminal.cpp new file mode 100644 index 00000000000..58a3170b0b8 --- /dev/null +++ b/apps/openmw/mwlua/types/terminal.cpp @@ -0,0 +1,40 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + void addESM4TerminalBindings(sol::table term, const Context& context) + { + addRecordFunctionBinding(term, context, "ESM4Terminal"); + + sol::usertype record = context.sol().new_usertype("ESM4_Terminal"); + record[sol::meta_function::to_string] = [](const ESM4::Terminal& rec) -> std::string { + return "ESM4_Terminal[" + ESM::RefId(rec.mId).toDebugString() + "]"; + }; + record["id"] = sol::readonly_property( + [](const ESM4::Terminal& rec) -> std::string { return ESM::RefId(rec.mId).serializeText(); }); + record["editorId"] + = sol::readonly_property([](const ESM4::Terminal& rec) -> std::string { return rec.mEditorId; }); + record["text"] = sol::readonly_property([](const ESM4::Terminal& rec) -> std::string { return rec.mText; }); + record["resultText"] + = sol::readonly_property([](const ESM4::Terminal& rec) -> std::string { return rec.mResultText; }); + record["name"] = sol::readonly_property([](const ESM4::Terminal& rec) -> std::string { return rec.mFullName; }); + record["model"] = sol::readonly_property([](const ESM4::Terminal& rec) -> std::string { + return Misc::ResourceHelpers::correctMeshPath(rec.mModel); + }); + } +} diff --git a/apps/openmw/mwlua/types/types.cpp b/apps/openmw/mwlua/types/types.cpp new file mode 100644 index 00000000000..c7b0b6e9c4e --- /dev/null +++ b/apps/openmw/mwlua/types/types.cpp @@ -0,0 +1,268 @@ +#include "types.hpp" + +#include +#include + +namespace MWLua +{ + namespace ObjectTypeName + { + // Names of object types in Lua. + // These names are part of OpenMW Lua API. + constexpr std::string_view Actor = "Actor"; // base type for NPC, Creature, Player + constexpr std::string_view Item = "Item"; // base type for all items + constexpr std::string_view Lockable = "Lockable"; // base type for doors and containers + + constexpr std::string_view Activator = "Activator"; + constexpr std::string_view Armor = "Armor"; + constexpr std::string_view Book = "Book"; + constexpr std::string_view Clothing = "Clothing"; + constexpr std::string_view Container = "Container"; + constexpr std::string_view Creature = "Creature"; + constexpr std::string_view Door = "Door"; + constexpr std::string_view Ingredient = "Ingredient"; + constexpr std::string_view LevelledCreature = "LevelledCreature"; + constexpr std::string_view Light = "Light"; + constexpr std::string_view MiscItem = "Miscellaneous"; + constexpr std::string_view NPC = "NPC"; + constexpr std::string_view Player = "Player"; + constexpr std::string_view Potion = "Potion"; + constexpr std::string_view Static = "Static"; + constexpr std::string_view Weapon = "Weapon"; + constexpr std::string_view Apparatus = "Apparatus"; + constexpr std::string_view Lockpick = "Lockpick"; + constexpr std::string_view Probe = "Probe"; + constexpr std::string_view Repair = "Repair"; + constexpr std::string_view Marker = "Marker"; + + constexpr std::string_view ESM4Activator = "ESM4Activator"; + constexpr std::string_view ESM4Ammunition = "ESM4Ammunition"; + constexpr std::string_view ESM4Armor = "ESM4Armor"; + constexpr std::string_view ESM4Book = "ESM4Book"; + constexpr std::string_view ESM4Clothing = "ESM4Clothing"; + constexpr std::string_view ESM4Container = "ESM4Container"; + constexpr std::string_view ESM4Door = "ESM4Door"; + constexpr std::string_view ESM4Flora = "ESM4Flora"; + constexpr std::string_view ESM4Furniture = "ESM4Furniture"; + constexpr std::string_view ESM4Ingredient = "ESM4Ingredient"; + constexpr std::string_view ESM4ItemMod = "ESM4ItemMod"; + constexpr std::string_view ESM4Light = "ESM4Light"; + constexpr std::string_view ESM4MiscItem = "ESM4Miscellaneous"; + constexpr std::string_view ESM4MovableStatic = "ESM4MovableStatic"; + constexpr std::string_view ESM4Potion = "ESM4Potion"; + constexpr std::string_view ESM4Static = "ESM4Static"; + constexpr std::string_view ESM4StaticCollection = "ESM4StaticCollection"; + constexpr std::string_view ESM4Terminal = "ESM4Terminal"; + constexpr std::string_view ESM4Tree = "ESM4Tree"; + constexpr std::string_view ESM4Weapon = "ESM4Weapon"; + } + + namespace + { + const static std::unordered_map luaObjectTypeInfo = { + { ESM::REC_INTERNAL_PLAYER, ObjectTypeName::Player }, + { ESM::REC_INTERNAL_MARKER, ObjectTypeName::Marker }, + { ESM::REC_ACTI, ObjectTypeName::Activator }, + { ESM::REC_ARMO, ObjectTypeName::Armor }, + { ESM::REC_BOOK, ObjectTypeName::Book }, + { ESM::REC_CLOT, ObjectTypeName::Clothing }, + { ESM::REC_CONT, ObjectTypeName::Container }, + { ESM::REC_CREA, ObjectTypeName::Creature }, + { ESM::REC_DOOR, ObjectTypeName::Door }, + { ESM::REC_INGR, ObjectTypeName::Ingredient }, + { ESM::REC_LEVC, ObjectTypeName::LevelledCreature }, + { ESM::REC_LIGH, ObjectTypeName::Light }, + { ESM::REC_MISC, ObjectTypeName::MiscItem }, + { ESM::REC_NPC_, ObjectTypeName::NPC }, + { ESM::REC_ALCH, ObjectTypeName::Potion }, + { ESM::REC_STAT, ObjectTypeName::Static }, + { ESM::REC_WEAP, ObjectTypeName::Weapon }, + { ESM::REC_APPA, ObjectTypeName::Apparatus }, + { ESM::REC_LOCK, ObjectTypeName::Lockpick }, + { ESM::REC_PROB, ObjectTypeName::Probe }, + { ESM::REC_REPA, ObjectTypeName::Repair }, + + { ESM::REC_ACTI4, ObjectTypeName::ESM4Activator }, + { ESM::REC_AMMO4, ObjectTypeName::ESM4Ammunition }, + { ESM::REC_ARMO4, ObjectTypeName::ESM4Armor }, + { ESM::REC_BOOK4, ObjectTypeName::ESM4Book }, + { ESM::REC_CLOT4, ObjectTypeName::ESM4Clothing }, + { ESM::REC_CONT4, ObjectTypeName::ESM4Container }, + { ESM::REC_DOOR4, ObjectTypeName::ESM4Door }, + { ESM::REC_FLOR4, ObjectTypeName::ESM4Flora }, + { ESM::REC_FURN4, ObjectTypeName::ESM4Furniture }, + { ESM::REC_INGR4, ObjectTypeName::ESM4Ingredient }, + { ESM::REC_IMOD4, ObjectTypeName::ESM4ItemMod }, + { ESM::REC_LIGH4, ObjectTypeName::ESM4Light }, + { ESM::REC_MISC4, ObjectTypeName::ESM4MiscItem }, + { ESM::REC_MSTT4, ObjectTypeName::ESM4MovableStatic }, + { ESM::REC_ALCH4, ObjectTypeName::ESM4Potion }, + { ESM::REC_STAT4, ObjectTypeName::ESM4Static }, + { ESM::REC_SCOL4, ObjectTypeName::ESM4StaticCollection }, + { ESM::REC_TERM4, ObjectTypeName::ESM4Terminal }, + { ESM::REC_TREE4, ObjectTypeName::ESM4Tree }, + { ESM::REC_WEAP4, ObjectTypeName::ESM4Weapon }, + }; + } + + unsigned int getLiveCellRefType(const MWWorld::LiveCellRefBase* ref) + { + if (ref == nullptr) + throw std::runtime_error("Can't get type name from an empty object."); + const ESM::RefId& id = ref->mRef.getRefId(); + if (id == "Player") + return ESM::REC_INTERNAL_PLAYER; + if (Misc::ResourceHelpers::isHiddenMarker(id)) + return ESM::REC_INTERNAL_MARKER; + return ref->getType(); + } + + std::string_view getLuaObjectTypeName(ESM::RecNameInts type, std::string_view fallback) + { + auto it = luaObjectTypeInfo.find(type); + if (it != luaObjectTypeInfo.end()) + return it->second; + else + return fallback; + } + + std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr) + { + return getLuaObjectTypeName( + static_cast(getLiveCellRefType(ptr.mRef)), /*fallback=*/ptr.getTypeDescription()); + } + + const MWWorld::Ptr& verifyType(ESM::RecNameInts recordType, const MWWorld::Ptr& ptr) + { + if (ptr.getType() != recordType) + { + std::string msg = "Requires type '"; + msg.append(getLuaObjectTypeName(recordType)); + msg.append("', but applied to "); + msg.append(ptr.toString()); + throw std::runtime_error(msg); + } + return ptr; + } + + sol::table getTypeToPackageTable(lua_State* L) + { + constexpr std::string_view key = "typeToPackage"; + sol::state_view lua(L); + if (lua[key] == sol::nil) + lua[key] = sol::table(lua, sol::create); + return lua[key]; + } + + sol::table getPackageToTypeTable(lua_State* L) + { + constexpr std::string_view key = "packageToType"; + sol::state_view lua(L); + if (lua[key] == sol::nil) + lua[key] = sol::table(lua, sol::create); + return lua[key]; + } + + sol::table initTypesPackage(const Context& context) + { + auto lua = context.sol(); + + if (lua["openmw_types"] != sol::nil) + return lua["openmw_types"]; + + sol::table types(lua, sol::create); + auto addType = [&](std::string_view name, std::vector recTypes, + std::optional base = std::nullopt) -> sol::table { + sol::table t(lua, sol::create); + sol::table ro = LuaUtil::makeReadOnly(t); + sol::table meta = ro[sol::metatable_key]; + meta[sol::meta_function::to_string] = [name]() { return name; }; + if (base) + { + t["baseType"] = types[*base]; + sol::table baseMeta(lua, sol::create); + baseMeta[sol::meta_function::index] = LuaUtil::getMutableFromReadOnly(types[*base]); + t[sol::metatable_key] = baseMeta; + } + t["objectIsInstance"] = [types = recTypes](const Object& o) { + unsigned int type = getLiveCellRefType(o.ptr().mRef); + for (ESM::RecNameInts t : types) + if (t == type) + return true; + return false; + }; + types[name] = ro; + return t; + }; + + addActorBindings( + addType(ObjectTypeName::Actor, { ESM::REC_INTERNAL_PLAYER, ESM::REC_CREA, ESM::REC_NPC_ }), context); + addItemBindings( + addType(ObjectTypeName::Item, + { ESM::REC_ARMO, ESM::REC_BOOK, ESM::REC_CLOT, ESM::REC_INGR, ESM::REC_LIGH, ESM::REC_MISC, + ESM::REC_ALCH, ESM::REC_WEAP, ESM::REC_APPA, ESM::REC_LOCK, ESM::REC_PROB, ESM::REC_REPA }), + context); + addLockableBindings( + addType(ObjectTypeName::Lockable, { ESM::REC_CONT, ESM::REC_DOOR, ESM::REC_CONT4, ESM::REC_DOOR4 })); + + addCreatureBindings(addType(ObjectTypeName::Creature, { ESM::REC_CREA }, ObjectTypeName::Actor), context); + addNpcBindings( + addType(ObjectTypeName::NPC, { ESM::REC_INTERNAL_PLAYER, ESM::REC_NPC_ }, ObjectTypeName::Actor), context); + addPlayerBindings(addType(ObjectTypeName::Player, { ESM::REC_INTERNAL_PLAYER }, ObjectTypeName::NPC), context); + + addLevelledCreatureBindings(addType(ObjectTypeName::LevelledCreature, { ESM::REC_LEVC }), context); + + addArmorBindings(addType(ObjectTypeName::Armor, { ESM::REC_ARMO }, ObjectTypeName::Item), context); + addClothingBindings(addType(ObjectTypeName::Clothing, { ESM::REC_CLOT }, ObjectTypeName::Item), context); + addIngredientBindings(addType(ObjectTypeName::Ingredient, { ESM::REC_INGR }, ObjectTypeName::Item), context); + addLightBindings(addType(ObjectTypeName::Light, { ESM::REC_LIGH }, ObjectTypeName::Item), context); + addMiscellaneousBindings(addType(ObjectTypeName::MiscItem, { ESM::REC_MISC }, ObjectTypeName::Item), context); + addPotionBindings(addType(ObjectTypeName::Potion, { ESM::REC_ALCH }, ObjectTypeName::Item), context); + addWeaponBindings(addType(ObjectTypeName::Weapon, { ESM::REC_WEAP }, ObjectTypeName::Item), context); + addBookBindings(addType(ObjectTypeName::Book, { ESM::REC_BOOK }, ObjectTypeName::Item), context); + addLockpickBindings(addType(ObjectTypeName::Lockpick, { ESM::REC_LOCK }, ObjectTypeName::Item), context); + addProbeBindings(addType(ObjectTypeName::Probe, { ESM::REC_PROB }, ObjectTypeName::Item), context); + addApparatusBindings(addType(ObjectTypeName::Apparatus, { ESM::REC_APPA }, ObjectTypeName::Item), context); + addRepairBindings(addType(ObjectTypeName::Repair, { ESM::REC_REPA }, ObjectTypeName::Item), context); + + addActivatorBindings(addType(ObjectTypeName::Activator, { ESM::REC_ACTI }), context); + addContainerBindings(addType(ObjectTypeName::Container, { ESM::REC_CONT }, ObjectTypeName::Lockable), context); + addDoorBindings(addType(ObjectTypeName::Door, { ESM::REC_DOOR }, ObjectTypeName::Lockable), context); + addStaticBindings(addType(ObjectTypeName::Static, { ESM::REC_STAT }), context); + + addType(ObjectTypeName::ESM4Activator, { ESM::REC_ACTI4 }); + addType(ObjectTypeName::ESM4Ammunition, { ESM::REC_AMMO4 }); + addType(ObjectTypeName::ESM4Armor, { ESM::REC_ARMO4 }); + addType(ObjectTypeName::ESM4Book, { ESM::REC_BOOK4 }); + addType(ObjectTypeName::ESM4Clothing, { ESM::REC_CLOT4 }); + addType(ObjectTypeName::ESM4Container, { ESM::REC_CONT4 }); + addESM4DoorBindings(addType(ObjectTypeName::ESM4Door, { ESM::REC_DOOR4 }, ObjectTypeName::Lockable), context); + addType(ObjectTypeName::ESM4Flora, { ESM::REC_FLOR4 }); + addType(ObjectTypeName::ESM4Furniture, { ESM::REC_FURN4 }); + addType(ObjectTypeName::ESM4Ingredient, { ESM::REC_INGR4 }); + addType(ObjectTypeName::ESM4ItemMod, { ESM::REC_IMOD4 }); + addType(ObjectTypeName::ESM4Light, { ESM::REC_LIGH4 }); + addType(ObjectTypeName::ESM4MiscItem, { ESM::REC_MISC4 }); + addType(ObjectTypeName::ESM4MovableStatic, { ESM::REC_MSTT4 }); + addType(ObjectTypeName::ESM4Potion, { ESM::REC_ALCH4 }); + addType(ObjectTypeName::ESM4Static, { ESM::REC_STAT4 }); + addType(ObjectTypeName::ESM4StaticCollection, { ESM::REC_SCOL4 }); + addESM4TerminalBindings(addType(ObjectTypeName::ESM4Terminal, { ESM::REC_TERM4 }), context); + addType(ObjectTypeName::ESM4Tree, { ESM::REC_TREE4 }); + addType(ObjectTypeName::ESM4Weapon, { ESM::REC_WEAP4 }); + + sol::table typeToPackage = getTypeToPackageTable(lua); + sol::table packageToType = getPackageToTypeTable(lua); + for (const auto& [type, name] : luaObjectTypeInfo) + { + sol::object t = types[name]; + if (t == sol::nil) + continue; + typeToPackage[type] = t; + packageToType[t] = type; + } + + lua["openmw_types"] = LuaUtil::makeReadOnly(types); + return lua["openmw_types"]; + } +} diff --git a/apps/openmw/mwlua/types/types.hpp b/apps/openmw/mwlua/types/types.hpp new file mode 100644 index 00000000000..76bd2848e00 --- /dev/null +++ b/apps/openmw/mwlua/types/types.hpp @@ -0,0 +1,59 @@ +#ifndef MWLUA_TYPES_H +#define MWLUA_TYPES_H + +#include + +#include +#include + +#include "../context.hpp" +#include "../recordstore.hpp" + +namespace MWLua +{ + // `getLiveCellRefType()` is not exactly what we usually mean by "type" because some refids have special meaning. + // This function handles these special refids (and by this adds some performance overhead). + // We use this "fixed" type in Lua because we don't want to expose the weirdness of Morrowind internals to our API. + // TODO: Implement https://gitlab.com/OpenMW/openmw/-/issues/6617 and make `MWWorld::PtrBase::getType` work the + // same as `getLiveCellRefType`. + unsigned int getLiveCellRefType(const MWWorld::LiveCellRefBase* ref); + + std::string_view getLuaObjectTypeName(ESM::RecNameInts type, std::string_view fallback = "Unknown"); + std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr); + const MWWorld::Ptr& verifyType(ESM::RecNameInts type, const MWWorld::Ptr& ptr); + + sol::table getTypeToPackageTable(lua_State* L); + sol::table getPackageToTypeTable(lua_State* L); + + sol::table initTypesPackage(const Context& context); + + // used in initTypesPackage + void addActivatorBindings(sol::table activator, const Context& context); + void addBookBindings(sol::table book, const Context& context); + void addContainerBindings(sol::table container, const Context& context); + void addDoorBindings(sol::table door, const Context& context); + void addItemBindings(sol::table item, const Context& context); + void addActorBindings(sol::table actor, const Context& context); + void addWeaponBindings(sol::table weapon, const Context& context); + void addNpcBindings(sol::table npc, const Context& context); + void addPlayerBindings(sol::table player, const Context& context); + void addCreatureBindings(sol::table creature, const Context& context); + void addLockpickBindings(sol::table lockpick, const Context& context); + void addProbeBindings(sol::table probe, const Context& context); + void addApparatusBindings(sol::table apparatus, const Context& context); + void addRepairBindings(sol::table repair, const Context& context); + void addMiscellaneousBindings(sol::table miscellaneous, const Context& context); + void addPotionBindings(sol::table potion, const Context& context); + void addIngredientBindings(sol::table Ingredient, const Context& context); + void addArmorBindings(sol::table armor, const Context& context); + void addLockableBindings(sol::table lockable); + void addClothingBindings(sol::table clothing, const Context& context); + void addStaticBindings(sol::table stat, const Context& context); + void addLightBindings(sol::table light, const Context& context); + void addLevelledCreatureBindings(sol::table list, const Context& context); + + void addESM4DoorBindings(sol::table door, const Context& context); + void addESM4TerminalBindings(sol::table term, const Context& context); +} + +#endif // MWLUA_TYPES_H diff --git a/apps/openmw/mwlua/types/weapon.cpp b/apps/openmw/mwlua/types/weapon.cpp new file mode 100644 index 00000000000..51795cffc12 --- /dev/null +++ b/apps/openmw/mwlua/types/weapon.cpp @@ -0,0 +1,167 @@ +#include "types.hpp" + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace +{ + // Populates a weapon struct from a Lua table. + ESM::Weapon tableToWeapon(const sol::table& rec) + { + ESM::Weapon weapon; + if (rec["template"] != sol::nil) + weapon = LuaUtil::cast(rec["template"]); + else + weapon.blank(); + + if (rec["name"] != sol::nil) + weapon.mName = rec["name"]; + if (rec["model"] != sol::nil) + weapon.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["icon"] != sol::nil) + weapon.mIcon = rec["icon"]; + if (rec["enchant"] != sol::nil) + { + std::string_view enchantId = rec["enchant"].get(); + weapon.mEnchant = ESM::RefId::deserializeText(enchantId); + } + if (rec["mwscript"] != sol::nil) + { + std::string_view scriptId = rec["mwscript"].get(); + weapon.mScript = ESM::RefId::deserializeText(scriptId); + } + if (auto isMagical = rec["isMagical"]; isMagical != sol::nil) + { + if (isMagical) + weapon.mData.mFlags |= ESM::Weapon::Magical; + else + weapon.mData.mFlags &= ~ESM::Weapon::Magical; + } + if (auto isSilver = rec["isSilver"]; isSilver != sol::nil) + { + if (isSilver) + weapon.mData.mFlags |= ESM::Weapon::Silver; + else + weapon.mData.mFlags &= ~ESM::Weapon::Silver; + } + + if (rec["type"] != sol::nil) + { + int weaponType = rec["type"].get(); + if (weaponType >= 0 && weaponType <= ESM::Weapon::MarksmanThrown) + weapon.mData.mType = weaponType; + else + throw std::runtime_error("Invalid Weapon Type provided: " + std::to_string(weaponType)); + } + if (rec["weight"] != sol::nil) + weapon.mData.mWeight = rec["weight"]; + if (rec["value"] != sol::nil) + weapon.mData.mValue = rec["value"]; + if (rec["health"] != sol::nil) + weapon.mData.mHealth = rec["health"]; + if (rec["speed"] != sol::nil) + weapon.mData.mSpeed = rec["speed"]; + if (rec["reach"] != sol::nil) + weapon.mData.mReach = rec["reach"]; + if (rec["enchantCapacity"] != sol::nil) + weapon.mData.mEnchant = std::round(rec["enchantCapacity"].get() * 10); + if (rec["chopMinDamage"] != sol::nil) + weapon.mData.mChop[0] = rec["chopMinDamage"]; + if (rec["chopMaxDamage"] != sol::nil) + weapon.mData.mChop[1] = rec["chopMaxDamage"]; + if (rec["slashMinDamage"] != sol::nil) + weapon.mData.mSlash[0] = rec["slashMinDamage"]; + if (rec["slashMaxDamage"] != sol::nil) + weapon.mData.mSlash[1] = rec["slashMaxDamage"]; + if (rec["thrustMinDamage"] != sol::nil) + weapon.mData.mThrust[0] = rec["thrustMinDamage"]; + if (rec["thrustMaxDamage"] != sol::nil) + weapon.mData.mThrust[1] = rec["thrustMaxDamage"]; + + return weapon; + } +} + +namespace MWLua +{ + void addWeaponBindings(sol::table weapon, const Context& context) + { + sol::state_view lua = context.sol(); + weapon["TYPE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "ShortBladeOneHand", ESM::Weapon::ShortBladeOneHand }, + { "LongBladeOneHand", ESM::Weapon::LongBladeOneHand }, + { "LongBladeTwoHand", ESM::Weapon::LongBladeTwoHand }, + { "BluntOneHand", ESM::Weapon::BluntOneHand }, + { "BluntTwoClose", ESM::Weapon::BluntTwoClose }, + { "BluntTwoWide", ESM::Weapon::BluntTwoWide }, + { "SpearTwoWide", ESM::Weapon::SpearTwoWide }, + { "AxeOneHand", ESM::Weapon::AxeOneHand }, + { "AxeTwoHand", ESM::Weapon::AxeTwoHand }, + { "MarksmanBow", ESM::Weapon::MarksmanBow }, + { "MarksmanCrossbow", ESM::Weapon::MarksmanCrossbow }, + { "MarksmanThrown", ESM::Weapon::MarksmanThrown }, + { "Arrow", ESM::Weapon::Arrow }, + { "Bolt", ESM::Weapon::Bolt }, + })); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + addRecordFunctionBinding(weapon, context); + weapon["createRecordDraft"] = tableToWeapon; + + sol::usertype record = lua.new_usertype("ESM3_Weapon"); + record[sol::meta_function::to_string] + = [](const ESM::Weapon& rec) -> std::string { return "ESM3_Weapon[" + rec.mId.toDebugString() + "]"; }; + record["id"] + = sol::readonly_property([](const ESM::Weapon& rec) -> std::string { return rec.mId.serializeText(); }); + record["name"] = sol::readonly_property([](const ESM::Weapon& rec) -> std::string { return rec.mName; }); + record["model"] = sol::readonly_property( + [](const ESM::Weapon& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); }); + record["icon"] = sol::readonly_property([vfs](const ESM::Weapon& rec) -> std::string { + return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); + }); + record["enchant"] = sol::readonly_property( + [](const ESM::Weapon& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mEnchant); }); + record["mwscript"] = sol::readonly_property( + [](const ESM::Weapon& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); + record["isMagical"] = sol::readonly_property( + [](const ESM::Weapon& rec) -> bool { return rec.mData.mFlags & ESM::Weapon::Magical; }); + record["isSilver"] = sol::readonly_property( + [](const ESM::Weapon& rec) -> bool { return rec.mData.mFlags & ESM::Weapon::Silver; }); + record["weight"] = sol::readonly_property([](const ESM::Weapon& rec) -> float { return rec.mData.mWeight; }); + record["value"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mValue; }); + record["type"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mType; }); + record["health"] = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mHealth; }); + record["speed"] = sol::readonly_property([](const ESM::Weapon& rec) -> float { return rec.mData.mSpeed; }); + record["reach"] = sol::readonly_property([](const ESM::Weapon& rec) -> float { return rec.mData.mReach; }); + record["enchantCapacity"] + = sol::readonly_property([](const ESM::Weapon& rec) -> float { return rec.mData.mEnchant * 0.1f; }); + record["chopMinDamage"] + = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mChop[0]; }); + record["chopMaxDamage"] + = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mChop[1]; }); + record["slashMinDamage"] + = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mSlash[0]; }); + record["slashMaxDamage"] + = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mSlash[1]; }); + record["thrustMinDamage"] + = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mThrust[0]; }); + record["thrustMaxDamage"] + = sol::readonly_property([](const ESM::Weapon& rec) -> int { return rec.mData.mThrust[1]; }); + } + +} diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp new file mode 100644 index 00000000000..9652df1238f --- /dev/null +++ b/apps/openmw/mwlua/uibindings.cpp @@ -0,0 +1,343 @@ +#include "uibindings.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "context.hpp" +#include "luamanagerimp.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" + +namespace MWLua +{ + namespace + { + template + void wrapAction(const std::shared_ptr& element, Fn&& fn) + { + try + { + fn(); + } + catch (...) + { + // prevent any actions on a potentially corrupted widget + element->mRoot = nullptr; + throw; + } + } + + const std::unordered_map modeToName{ + { MWGui::GM_Inventory, "Interface" }, + { MWGui::GM_Container, "Container" }, + { MWGui::GM_Companion, "Companion" }, + { MWGui::GM_MainMenu, "MainMenu" }, + { MWGui::GM_Journal, "Journal" }, + { MWGui::GM_Scroll, "Scroll" }, + { MWGui::GM_Book, "Book" }, + { MWGui::GM_Alchemy, "Alchemy" }, + { MWGui::GM_Repair, "Repair" }, + { MWGui::GM_Dialogue, "Dialogue" }, + { MWGui::GM_Barter, "Barter" }, + { MWGui::GM_Rest, "Rest" }, + { MWGui::GM_SpellBuying, "SpellBuying" }, + { MWGui::GM_Travel, "Travel" }, + { MWGui::GM_SpellCreation, "SpellCreation" }, + { MWGui::GM_Enchanting, "Enchanting" }, + { MWGui::GM_Recharge, "Recharge" }, + { MWGui::GM_Training, "Training" }, + { MWGui::GM_MerchantRepair, "MerchantRepair" }, + { MWGui::GM_Levelup, "LevelUp" }, + { MWGui::GM_Name, "ChargenName" }, + { MWGui::GM_Race, "ChargenRace" }, + { MWGui::GM_Birth, "ChargenBirth" }, + { MWGui::GM_Class, "ChargenClass" }, + { MWGui::GM_ClassGenerate, "ChargenClassGenerate" }, + { MWGui::GM_ClassPick, "ChargenClassPick" }, + { MWGui::GM_ClassCreate, "ChargenClassCreate" }, + { MWGui::GM_Review, "ChargenClassReview" }, + { MWGui::GM_Loading, "Loading" }, + { MWGui::GM_LoadingWallpaper, "LoadingWallpaper" }, + { MWGui::GM_Jail, "Jail" }, + { MWGui::GM_QuickKeysMenu, "QuickKeysMenu" }, + }; + + const auto nameToMode = [] { + std::unordered_map res; + for (const auto& [mode, name] : modeToName) + res[name] = mode; + return res; + }(); + } + + sol::table registerUiApi(const Context& context) + { + sol::state_view lua = context.sol(); + bool menu = context.mType == Context::Menu; + + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + + sol::table api(lua, sol::create); + api["_setHudVisibility"] = [luaManager = context.mLuaManager](bool state) { + luaManager->addAction([state] { MWBase::Environment::get().getWindowManager()->setHudVisibility(state); }); + }; + api["_isHudVisible"] = []() -> bool { return MWBase::Environment::get().getWindowManager()->isHudVisible(); }; + api["showMessage"] + = [luaManager = context.mLuaManager](std::string_view message, const sol::optional& options) { + MWGui::ShowInDialogueMode mode = MWGui::ShowInDialogueMode_IfPossible; + if (options.has_value()) + { + auto showInDialogue = options->get>("showInDialogue"); + if (showInDialogue.has_value()) + { + if (*showInDialogue) + mode = MWGui::ShowInDialogueMode_Only; + else + mode = MWGui::ShowInDialogueMode_Never; + } + } + luaManager->addUIMessage(message, mode); + }; + api["CONSOLE_COLOR"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { + { "Default", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Default.substr(1)) }, + { "Error", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Error.substr(1)) }, + { "Success", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Success.substr(1)) }, + { "Info", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Info.substr(1)) }, + })); + api["printToConsole"] + = [luaManager = context.mLuaManager](const std::string& message, const Misc::Color& color) { + luaManager->addInGameConsoleMessage(message + "\n", color); + }; + api["setConsoleMode"] = [luaManager = context.mLuaManager, windowManager](std::string_view mode) { + luaManager->addAction([mode = std::string(mode), windowManager] { windowManager->setConsoleMode(mode); }); + }; + api["getConsoleMode"] = [windowManager]() -> std::string_view { return windowManager->getConsoleMode(); }; + api["setConsoleSelectedObject"] = [luaManager = context.mLuaManager, windowManager](const sol::object& obj) { + if (obj == sol::nil) + luaManager->addAction([windowManager] { windowManager->setConsoleSelectedObject(MWWorld::Ptr()); }); + else + { + if (!obj.is()) + throw std::runtime_error("Game object expected"); + luaManager->addAction( + [windowManager, obj = obj.as()] { windowManager->setConsoleSelectedObject(obj.ptr()); }); + } + }; + api["content"] = LuaUi::loadContentConstructor(context.mLua); + + api["create"] = [luaManager = context.mLuaManager, menu](const sol::table& layout) { + auto element = LuaUi::Element::make(layout, menu); + luaManager->addAction([element] { wrapAction(element, [&] { element->create(); }); }, "Create UI"); + return element; + }; + + api["updateAll"] = [luaManager = context.mLuaManager, menu]() { + LuaUi::Element::forEach(menu, [](LuaUi::Element* e) { + if (e->mState == LuaUi::Element::Created) + e->mState = LuaUi::Element::Update; + }); + luaManager->addAction([menu]() { LuaUi::Element::forEach(menu, [](LuaUi::Element* e) { e->update(); }); }, + "Update all menu UI elements"); + }; + api["_getMenuTransparency"] = []() -> float { return Settings::gui().mMenuTransparency; }; + + sol::table layersTable(lua, sol::create); + layersTable["indexOf"] = [](std::string_view name) -> sol::optional { + size_t index = LuaUi::Layer::indexOf(name); + if (index == LuaUi::Layer::count()) + return sol::nullopt; + else + return LuaUtil::toLuaIndex(index); + }; + layersTable["insertAfter"] = [context]( + std::string_view afterName, std::string_view name, const sol::object& opt) { + LuaUi::Layer::Options options; + options.mInteractive = LuaUtil::getValueOrDefault(LuaUtil::getFieldOrNil(opt, "interactive"), true); + size_t index = LuaUi::Layer::indexOf(afterName); + if (index == LuaUi::Layer::count()) + throw std::logic_error(std::string("Layer not found")); + index++; + context.mLuaManager->addAction([=]() { LuaUi::Layer::insert(index, name, options); }, "Insert UI layer"); + }; + layersTable["insertBefore"] = [context]( + std::string_view beforename, std::string_view name, const sol::object& opt) { + LuaUi::Layer::Options options; + options.mInteractive = LuaUtil::getValueOrDefault(LuaUtil::getFieldOrNil(opt, "interactive"), true); + size_t index = LuaUi::Layer::indexOf(beforename); + if (index == LuaUi::Layer::count()) + throw std::logic_error(std::string("Layer not found")); + context.mLuaManager->addAction([=]() { LuaUi::Layer::insert(index, name, options); }, "Insert UI layer"); + }; + sol::table layers = LuaUtil::makeReadOnly(layersTable); + sol::table layersMeta = layers[sol::metatable_key]; + layersMeta[sol::meta_function::length] = []() { return LuaUi::Layer::count(); }; + layersMeta[sol::meta_function::index] = sol::overload( + [](const sol::object& self, size_t index) { + index = LuaUtil::fromLuaIndex(index); + return LuaUi::Layer(index); + }, + [layersTable]( + const sol::object& self, std::string_view key) { return layersTable.raw_get(key); }); + { + auto pairs = [layers](const sol::object&) { + auto next = [](const sol::table& l, size_t i) -> sol::optional> { + if (i < LuaUi::Layer::count()) + return std::make_tuple(i + 1, LuaUi::Layer(i)); + else + return sol::nullopt; + }; + return std::make_tuple(next, layers, 0); + }; + layersMeta[sol::meta_function::pairs] = pairs; + layersMeta[sol::meta_function::ipairs] = pairs; + } + api["layers"] = layers; + + sol::table typeTable(lua, sol::create); + for (const auto& it : LuaUi::widgetTypeToName()) + typeTable.set(it.second, it.first); + api["TYPE"] = LuaUtil::makeStrictReadOnly(typeTable); + + api["ALIGNMENT"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, + { { "Start", LuaUi::Alignment::Start }, { "Center", LuaUi::Alignment::Center }, + { "End", LuaUi::Alignment::End } })); + + api["registerSettingsPage"] = &LuaUi::registerSettingsPage; + api["removeSettingsPage"] = &LuaUi::removeSettingsPage; + + api["texture"] = [luaManager = context.mLuaManager](const sol::table& options) { + LuaUi::TextureData data; + sol::object path = LuaUtil::getFieldOrNil(options, "path"); + if (path.is()) + data.mPath = path.as(); + if (data.mPath.empty()) + throw std::logic_error("Invalid texture path"); + sol::object offset = LuaUtil::getFieldOrNil(options, "offset"); + if (offset.is()) + data.mOffset = offset.as(); + sol::object size = LuaUtil::getFieldOrNil(options, "size"); + if (size.is()) + data.mSize = size.as(); + return luaManager->uiResourceManager()->registerTexture(std::move(data)); + }; + + api["screenSize"] = []() { return osg::Vec2f(Settings::video().mResolutionX, Settings::video().mResolutionY); }; + + api["_getAllUiModes"] = [](sol::this_state lua) { + sol::table res(lua, sol::create); + for (const auto& [_, name] : modeToName) + res[name] = name; + return res; + }; + api["_getUiModeStack"] = [windowManager](sol::this_state lua) { + sol::table res(lua, sol::create); + int i = 1; + for (MWGui::GuiMode m : windowManager->getGuiModeStack()) + res[i++] = modeToName.at(m); + return res; + }; + api["_setUiModeStack"] + = [windowManager, luaManager = context.mLuaManager](sol::table modes, sol::optional arg) { + std::vector newStack(modes.size()); + for (unsigned i = 0; i < newStack.size(); ++i) + newStack[i] = nameToMode.at(LuaUtil::cast(modes[LuaUtil::toLuaIndex(i)])); + luaManager->addAction( + [windowManager, newStack = std::move(newStack), arg = std::move(arg)]() { + MWWorld::Ptr ptr; + if (arg.has_value()) + ptr = arg->ptr(); + const std::vector& stack = windowManager->getGuiModeStack(); + unsigned common = 0; + while (common < std::min(stack.size(), newStack.size()) && stack[common] == newStack[common]) + common++; + // TODO: Maybe disallow opening/closing special modes (main menu, settings, loading screen) + // from player scripts. Add new Lua context "menu" that can do it. + for (unsigned i = stack.size() - common; i > 0; i--) + windowManager->popGuiMode(true); + if (common == newStack.size() && !newStack.empty() && arg.has_value()) + windowManager->pushGuiMode(newStack.back(), ptr); + for (unsigned i = common; i < newStack.size(); ++i) + windowManager->pushGuiMode(newStack[i], ptr); + }, + "Set UI modes"); + }; + api["_getAllWindowIds"] = [windowManager](sol::this_state lua) { + sol::table res(lua, sol::create); + for (std::string_view name : windowManager->getAllWindowIds()) + res[name] = name; + return res; + }; + api["_getAllowedWindows"] = [windowManager](sol::this_state lua, std::string_view mode) { + sol::table res(lua, sol::create); + for (std::string_view name : windowManager->getAllowedWindowIds(nameToMode.at(mode))) + res[name] = name; + return res; + }; + api["_setWindowDisabled"] + = [windowManager, luaManager = context.mLuaManager](std::string_view window, bool disabled) { + luaManager->addAction([=]() { windowManager->setDisabledByLua(window, disabled); }); + }; + + // TODO + // api["_showMouseCursor"] = [](bool) {}; + + return api; + } + + sol::table initUserInterfacePackage(const Context& context) + { + if (context.initializeOnce("openmw_ui_usertypes")) + { + auto element = context.sol().new_usertype("UiElement"); + element[sol::meta_function::to_string] = [](const LuaUi::Element& element) { + std::stringstream res; + res << "UiElement"; + if (element.mLayer != "") + res << "[" << element.mLayer << "]"; + return res.str(); + }; + element["layout"] = sol::property([](const LuaUi::Element& element) { return element.mLayout; }, + [](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; }); + element["update"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { + if (element->mState != LuaUi::Element::Created) + return; + element->mState = LuaUi::Element::Update; + luaManager->addAction([element] { wrapAction(element, [&] { element->update(); }); }, "Update UI"); + }; + element["destroy"] = [luaManager = context.mLuaManager](const std::shared_ptr& element) { + if (element->mState == LuaUi::Element::Destroyed) + return; + element->mState = LuaUi::Element::Destroy; + luaManager->addAction( + [element] { wrapAction(element, [&] { LuaUi::Element::erase(element.get()); }); }, "Destroy UI"); + }; + + auto uiLayer = context.sol().new_usertype("UiLayer"); + uiLayer["name"] + = sol::readonly_property([](LuaUi::Layer& self) -> std::string_view { return self.name(); }); + uiLayer["size"] = sol::readonly_property([](LuaUi::Layer& self) { return self.size(); }); + uiLayer[sol::meta_function::to_string] + = [](LuaUi::Layer& self) { return Misc::StringUtils::format("UiLayer(%s)", self.name()); }; + } + + sol::object cached = context.getTypePackage("openmw_ui"); + if (cached != sol::nil) + return cached; + else + { + sol::table api = LuaUtil::makeReadOnly(registerUiApi(context)); + return context.setTypePackage(api, "openmw_ui"); + } + } +} diff --git a/apps/openmw/mwlua/uibindings.hpp b/apps/openmw/mwlua/uibindings.hpp new file mode 100644 index 00000000000..930ba7f3d86 --- /dev/null +++ b/apps/openmw/mwlua/uibindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_UIBINDINGS_H +#define MWLUA_UIBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initUserInterfacePackage(const Context&); +} + +#endif // MWLUA_UIBINDINGS_H diff --git a/apps/openmw/mwlua/userdataserializer.cpp b/apps/openmw/mwlua/userdataserializer.cpp new file mode 100644 index 00000000000..478f725d7cc --- /dev/null +++ b/apps/openmw/mwlua/userdataserializer.cpp @@ -0,0 +1,117 @@ +#include "userdataserializer.hpp" + +#include + +#include +#include + +#include "object.hpp" + +namespace MWLua +{ + + class Serializer final : public LuaUtil::UserdataSerializer + { + public: + explicit Serializer(bool localSerializer, std::map* contentFileMapping) + : mLocalSerializer(localSerializer) + , mContentFileMapping(contentFileMapping) + { + } + + private: + // Appends serialized sol::userdata to the end of BinaryData. + // Returns false if this type of userdata is not supported by this serializer. + bool serialize(LuaUtil::BinaryData& out, const sol::userdata& data) const override + { + if (data.is() || data.is()) + { + appendRefNum(out, data.as().id()); + return true; + } + if (data.is()) + { + appendObjectIdList(out, data.as().mIds); + return true; + } + if (data.is()) + { + appendObjectIdList(out, data.as().mIds); + return true; + } + return false; + } + + constexpr static std::string_view sObjListTypeName = "objlist"; + void appendObjectIdList(LuaUtil::BinaryData& out, const ObjectIdList& objList) const + { + static_assert(sizeof(ESM::RefNum) == 8); + if constexpr (Misc::IS_LITTLE_ENDIAN) + append(out, sObjListTypeName, objList->data(), objList->size() * sizeof(ESM::RefNum)); + else + { + std::vector buf; + buf.reserve(objList->size()); + for (ESM::RefNum v : *objList) + buf.push_back({ Misc::toLittleEndian(v.mIndex), Misc::toLittleEndian(v.mContentFile) }); + append(out, sObjListTypeName, buf.data(), buf.size() * sizeof(ESM::RefNum)); + } + } + + void adjustRefNum(ESM::RefNum& refNum) const + { + if (refNum.hasContentFile() && mContentFileMapping) + { + auto iter = mContentFileMapping->find(refNum.mContentFile); + if (iter != mContentFileMapping->end()) + refNum.mContentFile = iter->second; + } + } + + // Deserializes userdata of type "typeName" from binaryData. Should push the result on stack using + // sol::stack::push. Returns false if this type is not supported by this serializer. + bool deserialize(std::string_view typeName, std::string_view binaryData, lua_State* lua) const override + { + if (typeName == sRefNumTypeName) + { + ObjectId id = loadRefNum(binaryData); + adjustRefNum(id); + if (mLocalSerializer) + sol::stack::push(lua, LObject(id)); + else + sol::stack::push(lua, GObject(id)); + return true; + } + if (typeName == sObjListTypeName) + { + if (binaryData.size() % sizeof(ESM::RefNum) != 0) + throw std::runtime_error("Invalid size of ObjectIdList in MWLua::Serializer"); + ObjectIdList objList = std::make_shared>(); + objList->resize(binaryData.size() / sizeof(ESM::RefNum)); + std::memcpy(objList->data(), binaryData.data(), binaryData.size()); + for (ESM::RefNum& id : *objList) + { + id.mIndex = Misc::fromLittleEndian(id.mIndex); + id.mContentFile = Misc::fromLittleEndian(id.mContentFile); + adjustRefNum(id); + } + if (mLocalSerializer) + sol::stack::push(lua, LObjectList{ std::move(objList) }); + else + sol::stack::push(lua, GObjectList{ std::move(objList) }); + return true; + } + return false; + } + + bool mLocalSerializer; + std::map* mContentFileMapping; + }; + + std::unique_ptr createUserdataSerializer( + bool local, std::map* contentFileMapping) + { + return std::make_unique(local, contentFileMapping); + } + +} diff --git a/apps/openmw/mwlua/userdataserializer.hpp b/apps/openmw/mwlua/userdataserializer.hpp new file mode 100644 index 00000000000..f52bb227232 --- /dev/null +++ b/apps/openmw/mwlua/userdataserializer.hpp @@ -0,0 +1,22 @@ +#ifndef MWLUA_USERDATASERIALIZER_H +#define MWLUA_USERDATASERIALIZER_H + +#include "object.hpp" + +namespace LuaUtil +{ + class UserdataSerializer; +} + +namespace MWLua +{ + // UserdataSerializer is an extension for components/lua/serialization.hpp + // Needed to serialize references to objects. + // If local=true, then during deserialization creates LObject, otherwise creates GObject. + // contentFileMapping is used only for deserialization. Needed to fix references if the order + // of content files was changed. + std::unique_ptr createUserdataSerializer( + bool local, std::map* contentFileMapping = nullptr); +} + +#endif // MWLUA_USERDATASERIALIZER_H diff --git a/apps/openmw/mwlua/vfsbindings.cpp b/apps/openmw/mwlua/vfsbindings.cpp new file mode 100644 index 00000000000..3186db26ca7 --- /dev/null +++ b/apps/openmw/mwlua/vfsbindings.cpp @@ -0,0 +1,355 @@ +#include "vfsbindings.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" + +#include "context.hpp" + +namespace MWLua +{ + namespace + { + // Too many arguments may cause stack corruption and crash. + constexpr std::size_t sMaximumReadArguments = 20; + + // Print a message if we read a large chunk of file to string. + constexpr std::size_t sFileSizeWarningThreshold = 1024 * 1024; + + struct FileHandle + { + public: + FileHandle(Files::IStreamPtr stream, std::string_view fileName) + { + mFilePtr = std::move(stream); + mFileName = fileName; + } + + Files::IStreamPtr mFilePtr; + std::string mFileName; + }; + + std::ios_base::seekdir getSeekDir(FileHandle& self, std::string_view whence) + { + if (whence == "cur") + return std::ios_base::cur; + if (whence == "set") + return std::ios_base::beg; + if (whence == "end") + return std::ios_base::end; + + throw std::runtime_error( + "Error when handling '" + self.mFileName + "': invalid seek direction: '" + std::string(whence) + "'."); + } + + size_t getBytesLeftInStream(Files::IStreamPtr& file) + { + auto oldPos = file->tellg(); + file->seekg(0, std::ios_base::end); + auto newPos = file->tellg(); + file->seekg(oldPos, std::ios_base::beg); + + return newPos - oldPos; + } + + void printLargeDataMessage(FileHandle& file, size_t size) + { + if (!file.mFilePtr || !Settings::lua().mLuaDebug || size < sFileSizeWarningThreshold) + return; + + Log(Debug::Verbose) << "Read a large data chunk (" << size << " bytes) from '" << file.mFileName << "'."; + } + + sol::object readFile(sol::this_state lua, FileHandle& file) + { + std::ostringstream os; + if (file.mFilePtr && file.mFilePtr->peek() != EOF) + os << file.mFilePtr->rdbuf(); + + auto result = os.str(); + printLargeDataMessage(file, result.size()); + return sol::make_object(lua, std::move(result)); + } + + sol::object readLineFromFile(sol::this_state lua, FileHandle& file) + { + std::string result; + if (file.mFilePtr && std::getline(*file.mFilePtr, result)) + { + printLargeDataMessage(file, result.size()); + return sol::make_object(lua, result); + } + + return sol::nil; + } + + sol::object readNumberFromFile(sol::this_state lua, Files::IStreamPtr& file) + { + double number = 0; + if (file && *file >> number) + return sol::make_object(lua, number); + + return sol::nil; + } + + sol::object readCharactersFromFile(sol::this_state lua, FileHandle& file, size_t count) + { + if (count <= 0 && file.mFilePtr->peek() != EOF) + return sol::make_object(lua, std::string()); + + auto bytesLeft = getBytesLeftInStream(file.mFilePtr); + if (bytesLeft <= 0) + return sol::nil; + + if (count > bytesLeft) + count = bytesLeft; + + std::string result(count, '\0'); + if (file.mFilePtr->read(&result[0], count)) + { + printLargeDataMessage(file, result.size()); + return sol::make_object(lua, result); + } + + return sol::nil; + } + + void validateFile(const FileHandle& self) + { + if (self.mFilePtr) + return; + + throw std::runtime_error("Error when handling '" + self.mFileName + "': attempt to use a closed file."); + } + + sol::variadic_results seek( + sol::this_state lua, FileHandle& self, std::ios_base::seekdir dir, std::streamoff off) + { + sol::variadic_results values; + try + { + self.mFilePtr->seekg(off, dir); + if (self.mFilePtr->fail() || self.mFilePtr->bad()) + { + auto msg = "Failed to seek in file '" + self.mFileName + "'"; + values.push_back(sol::nil); + values.push_back(sol::make_object(lua, msg)); + } + else + { + // tellg returns std::streampos which is not required to be a numeric type. It is required to be + // convertible to std::streamoff which is a number + values.push_back(sol::make_object(lua, self.mFilePtr->tellg())); + } + } + catch (std::exception& e) + { + auto msg = "Failed to seek in file '" + self.mFileName + "': " + std::string(e.what()); + values.push_back(sol::nil); + values.push_back(sol::make_object(lua, msg)); + } + + return values; + } + } + + sol::table initVFSPackage(const Context& context) + { + sol::table api(context.mLua->unsafeState(), sol::create); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + + sol::usertype handle = context.sol().new_usertype("FileHandle"); + handle["fileName"] + = sol::readonly_property([](const FileHandle& self) -> std::string_view { return self.mFileName; }); + handle[sol::meta_function::to_string] = [](const FileHandle& self) { + return "FileHandle{'" + self.mFileName + "'" + (!self.mFilePtr ? ", closed" : "") + "}"; + }; + handle["seek"] = sol::overload( + [](sol::this_state lua, FileHandle& self, std::string_view whence, sol::optional offset) { + validateFile(self); + + auto off = static_cast(offset.value_or(0)); + auto dir = getSeekDir(self, whence); + + return seek(lua, self, dir, off); + }, + [](sol::this_state lua, FileHandle& self, sol::optional offset) { + validateFile(self); + + auto off = static_cast(offset.value_or(0)); + + return seek(lua, self, std::ios_base::cur, off); + }); + handle["lines"] = [](sol::this_state lua, FileHandle& self) { + return sol::as_function([&lua, &self]() mutable { + validateFile(self); + return readLineFromFile(lua, self); + }); + }; + + api["lines"] = [vfs](sol::this_state lua, std::string_view fileName) { + auto normalizedName = VFS::Path::normalizeFilename(fileName); + return sol::as_function( + [lua, file = FileHandle(vfs->getNormalized(normalizedName), normalizedName)]() mutable { + validateFile(file); + auto result = readLineFromFile(lua, file); + if (result == sol::nil) + file.mFilePtr.reset(); + + return result; + }); + }; + + handle["close"] = [](sol::this_state lua, FileHandle& self) { + sol::variadic_results values; + try + { + self.mFilePtr.reset(); + if (self.mFilePtr) + { + auto msg = "Can not close file '" + self.mFileName + "': file handle is still opened."; + values.push_back(sol::nil); + values.push_back(sol::make_object(lua, msg)); + } + else + values.push_back(sol::make_object(lua, true)); + } + catch (std::exception& e) + { + auto msg = "Can not close file '" + self.mFileName + "': " + std::string(e.what()); + values.push_back(sol::nil); + values.push_back(sol::make_object(lua, msg)); + } + + return values; + }; + + handle["read"] = [](sol::this_state lua, FileHandle& self, const sol::variadic_args args) { + validateFile(self); + + if (args.size() > sMaximumReadArguments) + throw std::runtime_error( + "Error when handling '" + self.mFileName + "': too many arguments for 'read'."); + + sol::variadic_results values; + // If there are no arguments, read a string + if (args.size() == 0) + { + values.push_back(readLineFromFile(lua, self)); + return values; + } + + bool success = true; + size_t i = 0; + for (i = 0; i < args.size() && success; i++) + { + if (args[i].is()) + { + auto format = args[i].as(); + + if (format == "*a" || format == "*all") + { + values.push_back(readFile(lua, self)); + continue; + } + + if (format == "*n" || format == "*number") + { + auto result = readNumberFromFile(lua, self.mFilePtr); + values.push_back(result); + if (result == sol::nil) + success = false; + continue; + } + + if (format == "*l" || format == "*line") + { + auto result = readLineFromFile(lua, self); + values.push_back(result); + if (result == sol::nil) + success = false; + continue; + } + + throw std::runtime_error("Error when handling '" + self.mFileName + "': bad argument #" + + std::to_string(i + 1) + " to 'read' (invalid format)"); + } + else if (args[i].is()) + { + int number = args[i].as(); + auto result = readCharactersFromFile(lua, self, number); + values.push_back(result); + if (result == sol::nil) + success = false; + } + } + + // We should return nil if we just reached the end of stream + if (!success && self.mFilePtr->eof()) + return values; + + if (!success && (self.mFilePtr->fail() || self.mFilePtr->bad())) + { + auto msg = "Error when handling '" + self.mFileName + "': can not read data for argument #" + + std::to_string(i); + values.push_back(sol::make_object(lua, msg)); + } + + return values; + }; + + api["open"] = [vfs](sol::this_state lua, std::string_view fileName) { + sol::variadic_results values; + try + { + auto normalizedName = VFS::Path::normalizeFilename(fileName); + auto handle = FileHandle(vfs->getNormalized(normalizedName), normalizedName); + values.push_back(sol::make_object(lua, std::move(handle))); + } + catch (std::exception& e) + { + auto msg = "Can not open file: " + std::string(e.what()); + values.push_back(sol::nil); + values.push_back(sol::make_object(lua, msg)); + } + + return values; + }; + + api["type"] = sol::overload( + [](const FileHandle& handle) -> std::string { + if (handle.mFilePtr) + return "file"; + + return "closed file"; + }, + [](const sol::object&) -> sol::object { return sol::nil; }); + + api["fileExists"] + = [vfs](std::string_view fileName) -> bool { return vfs->exists(VFS::Path::Normalized(fileName)); }; + api["pathsWithPrefix"] = [vfs](std::string_view prefix) { + auto iterator = vfs->getRecursiveDirectoryIterator(prefix); + return sol::as_function([iterator, current = iterator.begin()]() mutable -> sol::optional { + if (current != iterator.end()) + { + const std::string& result = *current; + ++current; + return result; + } + + return sol::nullopt; + }); + }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/vfsbindings.hpp b/apps/openmw/mwlua/vfsbindings.hpp new file mode 100644 index 00000000000..b251db6fd42 --- /dev/null +++ b/apps/openmw/mwlua/vfsbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_VFSBINDINGS_H +#define MWLUA_VFSBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initVFSPackage(const Context&); +} + +#endif // MWLUA_VFSBINDINGS_H diff --git a/apps/openmw/mwlua/worker.cpp b/apps/openmw/mwlua/worker.cpp new file mode 100644 index 00000000000..5639cc89edf --- /dev/null +++ b/apps/openmw/mwlua/worker.cpp @@ -0,0 +1,99 @@ +#include "worker.hpp" + +#include "luamanagerimp.hpp" + +#include "apps/openmw/profile.hpp" + +#include +#include + +#include + +namespace MWLua +{ + Worker::Worker(LuaManager& manager) + : mManager(manager) + { + if (Settings::lua().mLuaNumThreads > 0) + mThread = std::thread([this] { run(); }); + } + + Worker::~Worker() + { + if (mThread && mThread->joinable()) + { + Log(Debug::Error) + << "Unexpected destruction of LuaWorker; likely there is an unhandled exception in the main thread."; + join(); + } + } + + void Worker::allowUpdate(osg::Timer_t frameStart, unsigned frameNumber, osg::Stats& stats) + { + if (!mThread) + return; + { + std::lock_guard lk(mMutex); + mUpdateRequest = UpdateRequest{ .mFrameStart = frameStart, .mFrameNumber = frameNumber, .mStats = &stats }; + } + mCV.notify_one(); + } + + void Worker::finishUpdate(osg::Timer_t frameStart, unsigned frameNumber, osg::Stats& stats) + { + if (mThread) + { + std::unique_lock lk(mMutex); + mCV.wait(lk, [&] { return !mUpdateRequest.has_value(); }); + } + else + update(frameStart, frameNumber, stats); + } + + void Worker::join() + { + if (mThread) + { + { + std::lock_guard lk(mMutex); + mJoinRequest = true; + } + mCV.notify_one(); + mThread->join(); + } + } + + void Worker::update(osg::Timer_t frameStart, unsigned frameNumber, osg::Stats& stats) + { + const osg::Timer* const timer = osg::Timer::instance(); + OMW::ScopedProfile profile(frameStart, frameNumber, *timer, stats); + + mManager.update(); + } + + void Worker::run() noexcept + { + while (true) + { + std::unique_lock lk(mMutex); + mCV.wait(lk, [&] { return mUpdateRequest.has_value() || mJoinRequest; }); + if (mJoinRequest) + break; + + assert(mUpdateRequest.has_value()); + + try + { + update(mUpdateRequest->mFrameStart, mUpdateRequest->mFrameNumber, *mUpdateRequest->mStats); + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Failed to update LuaManager: " << e.what(); + } + + mUpdateRequest.reset(); + lk.unlock(); + mCV.notify_one(); + } + } +} diff --git a/apps/openmw/mwlua/worker.hpp b/apps/openmw/mwlua/worker.hpp new file mode 100644 index 00000000000..58d69afe71a --- /dev/null +++ b/apps/openmw/mwlua/worker.hpp @@ -0,0 +1,55 @@ +#ifndef OPENMW_MWLUA_WORKER_H +#define OPENMW_MWLUA_WORKER_H + +#include +#include + +#include +#include +#include +#include + +namespace osg +{ + class Stats; +} + +namespace MWLua +{ + class LuaManager; + + class Worker + { + public: + explicit Worker(LuaManager& manager); + + ~Worker(); + + void allowUpdate(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + + void finishUpdate(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + + void join(); + + private: + struct UpdateRequest + { + osg::Timer_t mFrameStart; + unsigned mFrameNumber; + osg::ref_ptr mStats; + }; + + void update(osg::Timer_t frameStart, unsigned frameNumber, osg::Stats& stats); + + void run() noexcept; + + LuaManager& mManager; + std::mutex mMutex; + std::condition_variable mCV; + std::optional mUpdateRequest; + bool mJoinRequest = false; + std::optional mThread; + }; +} + +#endif // OPENMW_MWLUA_LUAWORKER_H diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp new file mode 100644 index 00000000000..ac7bd307cf3 --- /dev/null +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -0,0 +1,228 @@ +#include "worldbindings.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/action.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/datetimemanager.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/manualref.hpp" +#include "../mwworld/store.hpp" +#include "../mwworld/worldmodel.hpp" + +#include "luamanagerimp.hpp" + +#include "animationbindings.hpp" +#include "corebindings.hpp" +#include "mwscriptbindings.hpp" + +namespace MWLua +{ + struct CellsStore + { + }; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; +} + +namespace MWLua +{ + + static void checkGameInitialized(LuaUtil::LuaState* lua) + { + if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) + throw std::runtime_error( + "This function cannot be used until the game is fully initialized.\n" + lua->debugTraceback()); + } + + static void addWorldTimeBindings(sol::table& api, const Context& context) + { + MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager(); + + api["setGameTimeScale"] = [timeManager](double scale) { timeManager->setGameTimeScale(scale); }; + api["setSimulationTimeScale"] = [context, timeManager](float scale) { + context.mLuaManager->addAction([scale, timeManager] { timeManager->setSimulationTimeScale(scale); }); + }; + + api["pause"] + = [timeManager](sol::optional tag) { timeManager->pause(tag.value_or("paused")); }; + api["unpause"] + = [timeManager](sol::optional tag) { timeManager->unpause(tag.value_or("paused")); }; + api["getPausedTags"] = [timeManager](sol::this_state lua) { + sol::table res(lua, sol::create); + for (const std::string& tag : timeManager->getPausedTags()) + res[tag] = tag; + return res; + }; + } + + static void addCellGetters(sol::table& api, const Context& context) + { + api["getCellByName"] = [](std::string_view name) { + return GCell{ &MWBase::Environment::get().getWorldModel()->getCell(name, /*forceLoad=*/false) }; + }; + api["getCellById"] = [](std::string_view stringId) { + return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( + ESM::RefId::deserializeText(stringId), /*forceLoad=*/false) }; + }; + api["getExteriorCell"] = [](int x, int y, sol::object cellOrName) { + ESM::RefId worldspace; + if (cellOrName.is()) + worldspace = cellOrName.as().mStore->getCell()->getWorldSpace(); + else if (cellOrName.is() && !cellOrName.as().empty()) + worldspace = MWBase::Environment::get() + .getWorldModel() + ->getCell(cellOrName.as()) + .getCell() + ->getWorldSpace(); + else + worldspace = ESM::Cell::sDefaultWorldspaceId; + return GCell{ &MWBase::Environment::get().getWorldModel()->getExterior( + ESM::ExteriorCellLocation(x, y, worldspace), /*forceLoad=*/false) }; + }; + + const MWWorld::Store* cells3Store = &MWBase::Environment::get().getESMStore()->get(); + const MWWorld::Store* cells4Store = &MWBase::Environment::get().getESMStore()->get(); + auto view = context.sol(); + sol::usertype cells = view.new_usertype("Cells"); + cells[sol::meta_function::length] + = [cells3Store, cells4Store](const CellsStore&) { return cells3Store->getSize() + cells4Store->getSize(); }; + cells[sol::meta_function::index] + = [cells3Store, cells4Store](const CellsStore&, size_t index) -> sol::optional { + if (index > cells3Store->getSize() + cells3Store->getSize() || index == 0) + return sol::nullopt; + + index--; // Translate from Lua's 1-based indexing. + if (index < cells3Store->getSize()) + { + const ESM::Cell* cellRecord = cells3Store->at(index); + return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( + cellRecord->mId, /*forceLoad=*/false) }; + } + else + { + const ESM4::Cell* cellRecord = cells4Store->at(index - cells3Store->getSize()); + return GCell{ &MWBase::Environment::get().getWorldModel()->getCell( + cellRecord->mId, /*forceLoad=*/false) }; + } + }; + cells[sol::meta_function::pairs] = view["ipairsForArray"].template get(); + cells[sol::meta_function::ipairs] = view["ipairsForArray"].template get(); + api["cells"] = CellsStore{}; + } + + sol::table initWorldPackage(const Context& context) + { + sol::table api(context.mLua->unsafeState(), sol::create); + + addCoreTimeBindings(api, context); + addWorldTimeBindings(api, context); + addCellGetters(api, context); + api["mwscript"] = initMWScriptBindings(context); + + ObjectLists* objectLists = context.mObjectLists; + api["activeActors"] = GObjectList{ objectLists->getActorsInScene() }; + api["players"] = GObjectList{ objectLists->getPlayers() }; + + api["createObject"] = [lua = context.mLua](std::string_view recordId, sol::optional count) -> GObject { + checkGameInitialized(lua); + MWWorld::ManualRef mref(*MWBase::Environment::get().getESMStore(), ESM::RefId::deserializeText(recordId)); + const MWWorld::Ptr& ptr = mref.getPtr(); + ptr.getRefData().disable(); + MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getDraftCell(); + MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, cell, count.value_or(1)); + return GObject(newPtr); + }; + api["getObjectByFormId"] = [](std::string_view formIdStr) -> GObject { + ESM::RefId refId = ESM::RefId::deserializeText(formIdStr); + if (!refId.is()) + throw std::runtime_error("FormId expected, got " + std::string(formIdStr) + "; use core.getFormId"); + return GObject(*refId.getIf()); + }; + + // Creates a new record in the world database. + api["createRecord"] = sol::overload( + [lua = context.mLua](const ESM::Activator& activator) -> const ESM::Activator* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(activator); + }, + [lua = context.mLua](const ESM::Armor& armor) -> const ESM::Armor* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(armor); + }, + [lua = context.mLua](const ESM::Clothing& clothing) -> const ESM::Clothing* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(clothing); + }, + [lua = context.mLua](const ESM::Book& book) -> const ESM::Book* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(book); + }, + [lua = context.mLua](const ESM::Miscellaneous& misc) -> const ESM::Miscellaneous* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(misc); + }, + [lua = context.mLua](const ESM::Potion& potion) -> const ESM::Potion* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(potion); + }, + [lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(weapon); + }, + [lua = context.mLua](const ESM::Light& light) -> const ESM::Light* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(light); + }); + + api["_runStandardActivationAction"] = [context](const GObject& object, const GObject& actor) { + if (!object.ptr().getRefData().activate()) + return; + context.mLuaManager->addAction( + [object, actor] { + const MWWorld::Ptr& objPtr = object.ptr(); + const MWWorld::Ptr& actorPtr = actor.ptr(); + objPtr.getClass().activate(objPtr, actorPtr)->execute(actorPtr); + }, + "_runStandardActivationAction"); + }; + api["_runStandardUseAction"] = [context](const GObject& object, const GObject& actor, bool force) { + context.mLuaManager->addAction( + [object, actor, force] { + const MWWorld::Ptr& actorPtr = actor.ptr(); + const MWWorld::Ptr& objectPtr = object.ptr(); + if (actorPtr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + MWBase::Environment::get().getWindowManager()->useItem(objectPtr, force); + else + { + std::unique_ptr action = objectPtr.getClass().use(objectPtr, force); + action->execute(actorPtr, true); + } + }, + "_runStandardUseAction"); + }; + + api["vfx"] = initWorldVfxBindings(context); + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/worldbindings.hpp b/apps/openmw/mwlua/worldbindings.hpp new file mode 100644 index 00000000000..4bd2318b68f --- /dev/null +++ b/apps/openmw/mwlua/worldbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_WORLDBINDINGS_H +#define MWLUA_WORLDBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initWorldPackage(const Context&); +} + +#endif // MWLUA_WORLDBINDINGS_H diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 928293e6496..b624b104ed4 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -1,351 +1,661 @@ #include "activespells.hpp" -#include -#include +#include -#include +#include + +#include + +#include + +#include +#include +#include +#include + +#include + +#include "actorutil.hpp" +#include "creaturestats.hpp" +#include "spellcasting.hpp" +#include "spelleffects.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwrender/animation.hpp" + +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/manualref.hpp" -namespace MWMechanics +namespace { - void ActiveSpells::update(float duration) const + bool merge(std::vector& present, const std::vector& queued) { - bool rebuild = false; - - // Erase no longer active spells and effects - if (duration > 0) - { - TContainer::iterator iter (mSpells.begin()); - while (iter!=mSpells.end()) - { - if (!timeToExpire (iter)) - { - mSpells.erase (iter++); - rebuild = true; - } - else - { - bool interrupt = false; - std::vector& effects = iter->second.mEffects; - for (std::vector::iterator effectIt = effects.begin(); effectIt != effects.end();) - { - if (effectIt->mTimeLeft <= 0) - { - rebuild = true; - - // Note: it we expire a Corprus effect, we should remove the whole spell. - if (effectIt->mEffectId == ESM::MagicEffect::Corprus) - { - iter = mSpells.erase (iter); - interrupt = true; - break; - } - - effectIt = effects.erase(effectIt); - } - else - { - effectIt->mTimeLeft -= duration; - ++effectIt; - } - } - - if (!interrupt) - ++iter; - } - } - } + // Can't merge if we already have an effect with the same effect index + auto problem = std::find_if(queued.begin(), queued.end(), [&](const auto& qEffect) { + return std::find_if(present.begin(), present.end(), [&](const auto& pEffect) { + return pEffect.mEffectIndex == qEffect.mEffectIndex; + }) != present.end(); + }); + if (problem != queued.end()) + return false; + present.insert(present.end(), queued.begin(), queued.end()); + return true; + } - if (mSpellsChanged) + void addEffects( + std::vector& effects, const ESM::EffectList& list, bool ignoreResistances = false) + { + for (const auto& enam : list.mList) { - mSpellsChanged = false; - rebuild = true; + if (enam.mData.mRange != ESM::RT_Self) + continue; + ESM::ActiveEffect effect; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; + effect.mEffectIndex = enam.mIndex; + effect.mFlags = ESM::ActiveEffect::Flag_None; + if (ignoreResistances) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effects.emplace_back(effect); } - - if (rebuild) - rebuildEffects(); } +} - void ActiveSpells::rebuildEffects() const +namespace MWMechanics +{ + ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) + : mActiveSpells(spells) { - mEffects = MagicEffects(); + mActiveSpells.mIterating = true; + } - for (TIterator iter (begin()); iter!=end(); ++iter) - { - const std::vector& effects = iter->second.mEffects; + ActiveSpells::IterationGuard::~IterationGuard() + { + mActiveSpells.mIterating = false; + } - for (std::vector::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) - { - if (effectIt->mTimeLeft > 0) - mEffects.add(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), MWMechanics::EffectParam(effectIt->mMagnitude)); - } - } + ActiveSpells::ActiveSpellParams::ActiveSpellParams( + const MWWorld::Ptr& caster, const ESM::RefId& id, std::string_view sourceName, ESM::RefNum item) + : mSourceSpellId(id) + , mDisplayName(sourceName) + , mCasterActorId(-1) + , mItem(item) + , mFlags() + , mWorsenings(-1) + { + if (!caster.isEmpty() && caster.getClass().isActor()) + mCasterActorId = caster.getClass().getCreatureStats(caster).getActorId(); } - ActiveSpells::ActiveSpells() - : mSpellsChanged (false) - {} + ActiveSpells::ActiveSpellParams::ActiveSpellParams( + const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances) + : mSourceSpellId(spell->mId) + , mDisplayName(spell->mName) + , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) + , mFlags() + , mWorsenings(-1) + { + assert(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power); + setFlag(ESM::ActiveSpells::Flag_SpellStore); + if (spell->mData.mType == ESM::Spell::ST_Ability) + setFlag(ESM::ActiveSpells::Flag_AffectsBaseValues); + addEffects(mEffects, spell->mEffects, ignoreResistances); + } - const MagicEffects& ActiveSpells::getMagicEffects() const + ActiveSpells::ActiveSpellParams::ActiveSpellParams( + const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, const MWWorld::Ptr& actor) + : mSourceSpellId(item.getCellRef().getRefId()) + , mDisplayName(item.getClass().getName(item)) + , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) + , mItem(item.getCellRef().getRefNum()) + , mFlags() + , mWorsenings(-1) { - update(0.f); - return mEffects; + assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect); + addEffects(mEffects, enchantment->mEffects); + setFlag(ESM::ActiveSpells::Flag_Equipment); } - ActiveSpells::TIterator ActiveSpells::begin() const + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params) + : mActiveSpellId(params.mActiveSpellId) + , mSourceSpellId(params.mSourceSpellId) + , mEffects(params.mEffects) + , mDisplayName(params.mDisplayName) + , mCasterActorId(params.mCasterActorId) + , mItem(params.mItem) + , mFlags(params.mFlags) + , mWorsenings(params.mWorsenings) + , mNextWorsening({ params.mNextWorsening }) { - return mSpells.begin(); } - ActiveSpells::TIterator ActiveSpells::end() const + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor) + : mSourceSpellId(params.mSourceSpellId) + , mDisplayName(params.mDisplayName) + , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) + , mItem(params.mItem) + , mFlags(params.mFlags) + , mWorsenings(-1) { - return mSpells.end(); } - double ActiveSpells::timeToExpire (const TIterator& iterator) const + ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const { - const std::vector& effects = iterator->second.mEffects; + ESM::ActiveSpells::ActiveSpellParams params; + params.mActiveSpellId = mActiveSpellId; + params.mSourceSpellId = mSourceSpellId; + params.mEffects = mEffects; + params.mDisplayName = mDisplayName; + params.mCasterActorId = mCasterActorId; + params.mItem = mItem; + params.mFlags = mFlags; + params.mWorsenings = mWorsenings; + params.mNextWorsening = mNextWorsening.toEsm(); + return params; + } - float duration = 0; + void ActiveSpells::ActiveSpellParams::setFlag(ESM::ActiveSpells::Flags flag) + { + mFlags = static_cast(mFlags | flag); + } - for (std::vector::const_iterator iter (effects.begin()); - iter!=effects.end(); ++iter) - { - if (iter->mTimeLeft > duration) - duration = iter->mTimeLeft; - } + void ActiveSpells::ActiveSpellParams::worsen() + { + ++mWorsenings; + if (!mWorsenings) + mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp(); + mNextWorsening += CorprusStats::sWorseningPeriod; + } - if (duration < 0) - return 0; + bool ActiveSpells::ActiveSpellParams::shouldWorsen() const + { + return mWorsenings >= 0 && MWBase::Environment::get().getWorld()->getTimeStamp() >= mNextWorsening; + } - return duration; + void ActiveSpells::ActiveSpellParams::resetWorsenings() + { + mWorsenings = -1; } - bool ActiveSpells::isSpellActive(const std::string& id) const + ESM::RefId ActiveSpells::ActiveSpellParams::getEnchantment() const { - for (TContainer::iterator iter = mSpells.begin(); iter != mSpells.end(); ++iter) + // Enchantment id is not stored directly. Instead the enchanted item is stored. + const auto& store = MWBase::Environment::get().getESMStore(); + switch (store->find(mSourceSpellId)) { - if (Misc::StringUtils::ciEqual(iter->first, id)) - return true; + case ESM::REC_ARMO: + return store->get().find(mSourceSpellId)->mEnchant; + case ESM::REC_BOOK: + return store->get().find(mSourceSpellId)->mEnchant; + case ESM::REC_CLOT: + return store->get().find(mSourceSpellId)->mEnchant; + case ESM::REC_WEAP: + return store->get().find(mSourceSpellId)->mEnchant; + default: + return {}; } - return false; } - const ActiveSpells::TContainer& ActiveSpells::getActiveSpells() const + const ESM::Spell* ActiveSpells::ActiveSpellParams::getSpell() const { - return mSpells; + return MWBase::Environment::get().getESMStore()->get().search(getSourceSpellId()); } - void ActiveSpells::addSpell(const std::string &id, bool stack, std::vector effects, - const std::string &displayName, int casterActorId) + bool ActiveSpells::ActiveSpellParams::hasFlag(ESM::ActiveSpells::Flags flags) const { - TContainer::iterator it(mSpells.find(id)); - - ActiveSpellParams params; - params.mEffects = effects; - params.mDisplayName = displayName; - params.mCasterActorId = casterActorId; + return static_cast(mFlags & flags) == flags; + } - if (it == end() || stack) + void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration) + { + if (mIterating) + return; + auto& creatureStats = ptr.getClass().getCreatureStats(ptr); + assert(&creatureStats.getActiveSpells() == this); + IterationGuard guard{ *this }; + // Erase no longer active spells and effects + for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - mSpells.insert(std::make_pair(id, params)); + if (!spellIt->hasFlag(ESM::ActiveSpells::Flag_Temporary)) + { + ++spellIt; + continue; + } + bool removedSpell = false; + for (auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();) + { + if (effectIt->mFlags & ESM::ActiveEffect::Flag_Remove && effectIt->mTimeLeft <= 0.f) + { + auto effect = *effectIt; + effectIt = spellIt->mEffects.erase(effectIt); + onMagicEffectRemoved(ptr, *spellIt, effect); + removedSpell = applyPurges(ptr, &spellIt, &effectIt); + if (removedSpell) + break; + } + else + { + ++effectIt; + } + } + if (removedSpell) + continue; + if (spellIt->mEffects.empty()) + spellIt = mSpells.erase(spellIt); + else + ++spellIt; } - else + + for (const auto& spell : mQueue) + addToSpells(ptr, spell); + mQueue.clear(); + + // Vanilla only does this on cell change I think + const auto& spells = creatureStats.getSpells(); + for (const ESM::Spell* spell : spells) { - // addSpell() is called with effects for a range. - // but a spell may have effects with different ranges (e.g. Touch & Target) - // so, if we see new effects for same spell assume additional - // spell effects and add to existing effects of spell - mergeEffects(params.mEffects, it->second.mEffects); - it->second = params; + 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.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + } } - mSpellsChanged = true; - } + bool updateSpellWindow = false; + if (ptr.getClass().hasInventoryStore(ptr) + && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) + { + auto& store = ptr.getClass().getInventoryStore(ptr); + if (store.getInvListener() != nullptr) + { + bool playNonLooping = !store.isFirstEquip(); + const auto world = MWBase::Environment::get().getWorld(); + for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) + { + auto slot = store.getSlot(slotIndex); + if (slot == store.end()) + continue; + const ESM::RefId& enchantmentId = slot->getClass().getEnchantment(*slot); + if (enchantmentId.empty()) + continue; + const ESM::Enchantment* enchantment = world->getStore().get().find(enchantmentId); + if (enchantment->mData.mType != ESM::Enchantment::ConstantEffect) + continue; + if (std::find_if(mSpells.begin(), mSpells.end(), + [&](const ActiveSpellParams& params) { + return params.mItem == slot->getCellRef().getRefNum() + && params.hasFlag(ESM::ActiveSpells::Flag_Equipment) + && params.mSourceSpellId == slot->getCellRef().getRefId(); + }) + != mSpells.end()) + continue; + // world->breakInvisibility leads to a stack overflow as it calls this method so just break + // invisibility manually + purgeEffect(ptr, ESM::MagicEffect::Invisibility); + applyPurges(ptr); + ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr }); + params.setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + for (const auto& effect : params.mEffects) + MWMechanics::playEffects( + ptr, *world->getStore().get().find(effect.mEffectId), playNonLooping); + updateSpellWindow = true; + } + } + } - void ActiveSpells::mergeEffects(std::vector& addTo, const std::vector& from) - { - for (std::vector::const_iterator effect(from.begin()); effect != from.end(); ++effect) + const MWWorld::Ptr player = MWMechanics::getPlayer(); + bool updatedHitOverlay = false; + bool updatedEnemy = false; + // Update effects + for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - // if effect is not in addTo, add it - bool missing = true; - for (std::vector::const_iterator iter(addTo.begin()); iter != addTo.end(); ++iter) + const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId( + spellIt->mCasterActorId); // Maybe make this search outside active grid? + bool removedSpell = false; + std::optional reflected; + for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) { - if ((effect->mEffectId == iter->mEffectId) && (effect->mArg == iter->mArg)) + auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration); + if (result.mType == MagicApplicationResult::Type::REFLECTED) + { + if (!reflected) + { + if (Settings::game().mClassicReflectedAbsorbSpellsBehavior) + reflected = { *spellIt, caster }; + else + reflected = { *spellIt, ptr }; + } + auto& reflectedEffect = reflected->mEffects.emplace_back(*it); + reflectedEffect.mFlags + = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + it = spellIt->mEffects.erase(it); + } + else if (result.mType == MagicApplicationResult::Type::REMOVED) + it = spellIt->mEffects.erase(it); + else { - missing = false; + ++it; + if (!updatedEnemy && result.mShowHealth && caster == player && ptr != player) + { + MWBase::Environment::get().getWindowManager()->setEnemy(ptr); + updatedEnemy = true; + } + if (!updatedHitOverlay && result.mShowHit && ptr == player) + { + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + updatedHitOverlay = true; + } + } + removedSpell = applyPurges(ptr, &spellIt, &it); + if (removedSpell) break; + } + if (reflected) + { + const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get().find( + ESM::RefId::stringRefId("VFX_Reflect")); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if (animation && !reflectStatic->mModel.empty()) + animation->addEffect(Misc::ResourceHelpers::correctMeshPath(reflectStatic->mModel), + ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false); + caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); + } + if (removedSpell) + continue; + + bool remove = false; + if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore)) + { + try + { + remove = !spells.hasSpell(spellIt->mSourceSpellId); + } + catch (const std::runtime_error& e) + { + remove = true; + Log(Debug::Error) << "Removing active effect: " << e.what(); } } - if (missing) + else if (spellIt->hasFlag(ESM::ActiveSpells::Flag_Equipment)) { - addTo.push_back(*effect); + // Remove effects tied to equipment that has been unequipped + const auto& store = ptr.getClass().getInventoryStore(ptr); + remove = true; + for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) + { + auto slot = store.getSlot(slotIndex); + if (slot != store.end() && slot->getCellRef().getRefNum().isSet() + && slot->getCellRef().getRefNum() == spellIt->mItem) + { + remove = false; + break; + } + } } + if (remove) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + for (const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + applyPurges(ptr, &spellIt); + updateSpellWindow = true; + continue; + } + ++spellIt; + } + + if (Settings::game().mClassicCalmSpellsBehavior) + { + ESM::MagicEffect::Effects effect + = ptr.getClass().isNpc() ? ESM::MagicEffect::CalmHumanoid : ESM::MagicEffect::CalmCreature; + if (creatureStats.getMagicEffects().getOrDefault(effect).getMagnitude() > 0.f) + creatureStats.getAiSequence().stopCombat(); + } + + if (ptr == player && updateSpellWindow) + { + // Something happened with the spell list -- possibly while the game is paused, + // so we want to make the spell window get the memo. + // We don't normally want to do this, so this targets constant enchantments. + MWBase::Environment::get().getWindowManager()->updateSpellWindow(); } } - void ActiveSpells::removeEffects(const std::string &id) + void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) { - for (TContainer::iterator spell = mSpells.begin(); spell != mSpells.end(); ++spell) + if (!spell.hasFlag(ESM::ActiveSpells::Flag_Stackable)) { - if (spell->first == id) + auto found = std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& existing) { + return spell.mSourceSpellId == existing.mSourceSpellId + && spell.mCasterActorId == existing.mCasterActorId && spell.mItem == existing.mItem; + }); + if (found != mSpells.end()) { - spell->second.mEffects.clear(); - mSpellsChanged = true; + if (merge(found->mEffects, spell.mEffects)) + return; + auto params = *found; + mSpells.erase(found); + for (const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); } } + mSpells.emplace_back(spell); + mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); } - void ActiveSpells::visitEffectSources(EffectSourceVisitor &visitor) const + ActiveSpells::ActiveSpells() + : mIterating(false) { - for (TContainer::const_iterator it = begin(); it != end(); ++it) - { - for (std::vector::const_iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end(); ++effectIt) - { - std::string name = it->second.mDisplayName; + } - float magnitude = effectIt->mMagnitude; - if (magnitude) - visitor.visit(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), effectIt->mEffectIndex, name, it->first, it->second.mCasterActorId, magnitude, effectIt->mTimeLeft, effectIt->mDuration); - } - } + ActiveSpells::TIterator ActiveSpells::begin() const + { + return mSpells.begin(); } - void ActiveSpells::purgeAll(float chance, bool spellOnly) + ActiveSpells::TIterator ActiveSpells::end() const { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ) - { - const std::string spellId = it->first; + return mSpells.end(); + } - // if spellOnly is true, dispell only spells. Leave potions, enchanted items etc. - if (spellOnly) - { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); - if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) - { - ++it; - continue; - } - } + ActiveSpells::TIterator ActiveSpells::getActiveSpellById(const ESM::RefId& id) + { + for (TIterator it = begin(); it != end(); it++) + if (it->getActiveSpellId() == id) + return it; + return end(); + } - if (Misc::Rng::roll0to99() < chance) - mSpells.erase(it++); - else - ++it; - } - mSpellsChanged = true; + bool ActiveSpells::isSpellActive(const ESM::RefId& id) const + { + return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) { + return spell.mSourceSpellId == id; + }) != mSpells.end(); + } + + bool ActiveSpells::isEnchantmentActive(const ESM::RefId& id) const + { + const auto& store = MWBase::Environment::get().getESMStore(); + if (store->get().search(id) == nullptr) + return false; + + return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) { + return spell.getEnchantment() == id; + }) != mSpells.end(); + } + + void ActiveSpells::addSpell(const ActiveSpellParams& params) + { + mQueue.emplace_back(params); } - void ActiveSpells::purgeEffect(short effectId) + void ActiveSpells::addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + mQueue.emplace_back(ActiveSpellParams{ spell, actor, true }); + } + + void ActiveSpells::purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr) + { + assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this); + mPurges.emplace(predicate); + if (!mIterating) { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (effectIt->mEffectId == effectId) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } + IterationGuard guard{ *this }; + applyPurges(ptr); } - mSpellsChanged = true; } - void ActiveSpells::purgeEffect(short effectId, const std::string& sourceId, int effectIndex) + void ActiveSpells::purge(EffectPredicate predicate, const MWWorld::Ptr& ptr) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this); + mPurges.emplace(predicate); + if (!mIterating) { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (effectIt->mEffectId == effectId && it->first == sourceId && (effectIndex < 0 || effectIndex == effectIt->mEffectIndex)) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } + IterationGuard guard{ *this }; + applyPurges(ptr); } - mSpellsChanged = true; } - void ActiveSpells::purge(int casterActorId) + bool ActiveSpells::applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell, + std::vector::iterator* currentEffect) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + bool removedCurrentSpell = false; + while (!mPurges.empty()) { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) + auto predicate = mPurges.front(); + mPurges.pop(); + for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - if (it->second.mCasterActorId == casterActorId) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; + bool isCurrentSpell = currentSpell && *currentSpell == spellIt; + std::visit( + [&](auto&& variant) { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + if (variant(*spellIt)) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + if (isCurrentSpell) + { + *currentSpell = spellIt; + removedCurrentSpell = true; + } + for (const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + } + else + ++spellIt; + } + else + { + static_assert(std::is_same_v, "Non-exhaustive visitor"); + for (auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();) + { + if (variant(*spellIt, *effectIt)) + { + auto effect = *effectIt; + if (isCurrentSpell && currentEffect) + { + auto distance = std::distance(spellIt->mEffects.begin(), *currentEffect); + if (effectIt <= *currentEffect) + distance--; + effectIt = spellIt->mEffects.erase(effectIt); + *currentEffect = spellIt->mEffects.begin() + distance; + } + else + effectIt = spellIt->mEffects.erase(effectIt); + onMagicEffectRemoved(ptr, *spellIt, effect); + } + else + ++effectIt; + } + ++spellIt; + } + }, + predicate); } } - mSpellsChanged = true; + return removedCurrentSpell; } - void ActiveSpells::purgeCorprusDisease() + void ActiveSpells::removeEffectsBySourceSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id) { - for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();) - { - bool hasCorprusEffect = false; - for (std::vector::iterator effectIt = iter->second.mEffects.begin(); - effectIt != iter->second.mEffects.end();++effectIt) - { - if (effectIt->mEffectId == ESM::MagicEffect::Corprus) - { - hasCorprusEffect = true; - break; - } - } + purge([=](const ActiveSpellParams& params) { return params.mSourceSpellId == id; }, ptr); + } - if (hasCorprusEffect) - { - mSpells.erase(iter++); - mSpellsChanged = true; - } - else - ++iter; - } + void ActiveSpells::removeEffectsByActiveSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id) + { + purge([=](const ActiveSpellParams& params) { return params.mActiveSpellId == id; }, ptr); } - void ActiveSpells::clear() + void ActiveSpells::purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg) { - mSpells.clear(); - mSpellsChanged = true; + purge( + [=](const ActiveSpellParams&, const ESM::ActiveEffect& effect) { + if (effectArg.empty()) + return effect.mEffectId == effectId; + return effect.mEffectId == effectId && effect.getSkillOrAttribute() == effectArg; + }, + ptr); } - void ActiveSpells::writeState(ESM::ActiveSpells &state) const + void ActiveSpells::purge(const MWWorld::Ptr& ptr, int casterActorId) { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp - ESM::ActiveSpells::ActiveSpellParams params; - params.mEffects = it->second.mEffects; - params.mCasterActorId = it->second.mCasterActorId; - params.mDisplayName = it->second.mDisplayName; + purge([=](const ActiveSpellParams& params) { return params.mCasterActorId == casterActorId; }, ptr); + } + + void ActiveSpells::clear(const MWWorld::Ptr& ptr) + { + mQueue.clear(); + purge([](const ActiveSpellParams& params) { return true; }, ptr); + } - state.mSpells.insert (std::make_pair(it->first, params)); + void ActiveSpells::skipWorsenings(double hours) + { + for (auto& spell : mSpells) + { + if (spell.mWorsenings >= 0) + spell.mNextWorsening += hours; } } - void ActiveSpells::readState(const ESM::ActiveSpells &state) + void ActiveSpells::writeState(ESM::ActiveSpells& state) const { - for (ESM::ActiveSpells::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) + for (const auto& spell : mSpells) + state.mSpells.emplace_back(spell.toEsm()); + for (const auto& spell : mQueue) + state.mQueue.emplace_back(spell.toEsm()); + } + + void ActiveSpells::readState(const ESM::ActiveSpells& state) + { + for (const ESM::ActiveSpells::ActiveSpellParams& spell : state.mSpells) { - // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp - ActiveSpellParams params; - params.mEffects = it->second.mEffects; - params.mCasterActorId = it->second.mCasterActorId; - params.mDisplayName = it->second.mDisplayName; - - mSpells.insert (std::make_pair(it->first, params)); - mSpellsChanged = true; + mSpells.emplace_back(ActiveSpellParams{ spell }); + // Generate ID for older saves that didn't have any. + if (mSpells.back().getActiveSpellId().empty()) + mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); } + for (const ESM::ActiveSpells::ActiveSpellParams& spell : state.mQueue) + mQueue.emplace_back(ActiveSpellParams{ spell }); + } + + void ActiveSpells::unloadActor(const MWWorld::Ptr& ptr) + { + purge([](const auto& spell) { return spell.hasFlag(ESM::ActiveSpells::Flag_Temporary); }, ptr); + mQueue.clear(); } } diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 54f662fc266..e4fa60ddb69 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -1,109 +1,172 @@ #ifndef GAME_MWMECHANICS_ACTIVESPELLS_H #define GAME_MWMECHANICS_ACTIVESPELLS_H -#include -#include +#include +#include +#include #include +#include +#include -#include +#include +#include "../mwworld/ptr.hpp" #include "../mwworld/timestamp.hpp" -#include "magiceffects.hpp" +#include "spellcasting.hpp" + +namespace ESM +{ + struct Enchantment; + struct Spell; +} namespace MWMechanics { /// \brief Lasting spell effects /// - /// \note The name of this class is slightly misleading, since it also handels lasting potion + /// \note The name of this class is slightly misleading, since it also handles lasting potion /// effects. class ActiveSpells { + public: + using ActiveEffect = ESM::ActiveEffect; + class ActiveSpellParams + { + ESM::RefId mActiveSpellId; + ESM::RefId mSourceSpellId; + std::vector mEffects; + std::string mDisplayName; + int mCasterActorId; + ESM::RefNum mItem; + ESM::ActiveSpells::Flags mFlags; + int mWorsenings; + MWWorld::TimeStamp mNextWorsening; + MWWorld::Ptr mSource; + + ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params); + + ActiveSpellParams(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances = false); + + ActiveSpellParams( + const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, const MWWorld::Ptr& actor); + + ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor); + + ESM::ActiveSpells::ActiveSpellParams toEsm() const; + + friend class ActiveSpells; + public: + ActiveSpellParams( + const MWWorld::Ptr& caster, const ESM::RefId& id, std::string_view sourceName, ESM::RefNum item); - typedef ESM::ActiveEffect ActiveEffect; + ESM::RefId getActiveSpellId() const { return mActiveSpellId; } + void setActiveSpellId(ESM::RefId id) { mActiveSpellId = id; } - struct ActiveSpellParams - { - std::vector mEffects; - MWWorld::TimeStamp mTimeStamp; - std::string mDisplayName; + const ESM::RefId& getSourceSpellId() const { return mSourceSpellId; } - // The caster that inflicted this spell on us - int mCasterActorId; - }; + const std::vector& getEffects() const { return mEffects; } + std::vector& getEffects() { return mEffects; } - typedef std::multimap TContainer; - typedef TContainer::const_iterator TIterator; + int getCasterActorId() const { return mCasterActorId; } - void readState (const ESM::ActiveSpells& state); - void writeState (ESM::ActiveSpells& state) const; + int getWorsenings() const { return mWorsenings; } - TIterator begin() const; + const std::string& getDisplayName() const { return mDisplayName; } - TIterator end() const; + ESM::RefNum getItem() const { return mItem; } + ESM::RefId getEnchantment() const; - void update(float duration) const; + const ESM::Spell* getSpell() const; + bool hasFlag(ESM::ActiveSpells::Flags flags) const; + void setFlag(ESM::ActiveSpells::Flags flags); - private: + // Increments worsenings count and sets the next timestamp + void worsen(); - mutable TContainer mSpells; - mutable MagicEffects mEffects; - mutable bool mSpellsChanged; + bool shouldWorsen() const; - void rebuildEffects() const; + void resetWorsenings(); + }; - /// Add any effects that are in "from" and not in "addTo" to "addTo" - void mergeEffects(std::vector& addTo, const std::vector& from); + typedef std::list Collection; + typedef Collection::const_iterator TIterator; - double timeToExpire (const TIterator& iterator) const; - ///< Returns time (in in-game hours) until the spell pointed to by \a iterator - /// expires. + void readState(const ESM::ActiveSpells& state); + void writeState(ESM::ActiveSpells& state) const; - const TContainer& getActiveSpells() const; + TIterator begin() const; - public: + TIterator end() const; + + TIterator getActiveSpellById(const ESM::RefId& id); + + void update(const MWWorld::Ptr& ptr, float duration); + + private: + using ParamsPredicate = std::function; + using EffectPredicate = std::function; + using Predicate = std::variant; + + struct IterationGuard + { + ActiveSpells& mActiveSpells; + + IterationGuard(ActiveSpells& spells); + ~IterationGuard(); + }; + + std::list mSpells; + std::vector mQueue; + std::queue mPurges; + bool mIterating; + + void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell); - ActiveSpells(); + bool applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell = nullptr, + std::vector::iterator* currentEffect = nullptr); - /// Add lasting effects - /// - /// \brief addSpell - /// \param id ID for stacking purposes. - /// \param stack If false, the spell is not added if one with the same ID exists already. - /// \param effects - /// \param displayName Name for display in magic menu. - /// - void addSpell (const std::string& id, bool stack, std::vector effects, - const std::string& displayName, int casterActorId); + public: + ActiveSpells(); - /// Removes the active effects from this spell/potion/.. with \a id - void removeEffects (const std::string& id); + /// Add lasting effects + /// + /// \brief addSpell + /// \param id ID for stacking purposes. + /// + void addSpell(const ActiveSpellParams& params); - /// Remove all active effects with this effect id - void purgeEffect (short effectId); + /// Bypasses resistances + void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor); - /// Remove all active effects with this effect id and source id - void purgeEffect (short effectId, const std::string& sourceId, int effectIndex=-1); + /// Removes the active effects from this spell/potion/.. with \a id + void removeEffectsBySourceSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id); + /// Removes the active effects of a specific active spell + void removeEffectsByActiveSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id); - /// Remove all active effects, if roll succeeds (for each effect) - void purgeAll(float chance, bool spellOnly = false); + /// Remove all active effects with this effect id + void purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg = {}); - /// Remove all effects with CASTER_LINKED flag that were cast by \a casterActorId - void purge (int casterActorId); + void purge(EffectPredicate predicate, const MWWorld::Ptr& ptr); + void purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr); - /// Remove all spells - void clear(); + /// Remove all effects that were cast by \a casterActorId + void purge(const MWWorld::Ptr& ptr, int casterActorId); - bool isSpellActive (const std::string& id) const; - ///< case insensitive + /// Remove all spells + void clear(const MWWorld::Ptr& ptr); - void purgeCorprusDisease(); + /// True if a spell associated with this id is active + /// \note For enchantments, this is the id of the enchanted item, not the enchantment itself + bool isSpellActive(const ESM::RefId& id) const; - const MagicEffects& getMagicEffects() const; + /// True if the enchantment is active + bool isEnchantmentActive(const ESM::RefId& id) const; - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; + void skipWorsenings(double hours); + void unloadActor(const MWWorld::Ptr& ptr); }; } diff --git a/apps/openmw/mwmechanics/actor.cpp b/apps/openmw/mwmechanics/actor.cpp deleted file mode 100644 index a5c55633ac6..00000000000 --- a/apps/openmw/mwmechanics/actor.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "actor.hpp" - -#include "character.hpp" - -namespace MWMechanics -{ - Actor::Actor(const MWWorld::Ptr &ptr, MWRender::Animation *animation) - { - mCharacterController.reset(new CharacterController(ptr, animation)); - } - - void Actor::updatePtr(const MWWorld::Ptr &newPtr) - { - mCharacterController->updatePtr(newPtr); - } - - CharacterController* Actor::getCharacterController() - { - return mCharacterController.get(); - } - - int Actor::getGreetingTimer() const - { - return mGreetingTimer; - } - - void Actor::setGreetingTimer(int timer) - { - mGreetingTimer = timer; - } - - float Actor::getAngleToPlayer() const - { - return mTargetAngleRadians; - } - - void Actor::setAngleToPlayer(float angle) - { - mTargetAngleRadians = angle; - } - - GreetingState Actor::getGreetingState() const - { - return mGreetingState; - } - - void Actor::setGreetingState(GreetingState state) - { - mGreetingState = state; - } - - bool Actor::isTurningToPlayer() const - { - return mIsTurningToPlayer; - } - - void Actor::setTurningToPlayer(bool turning) - { - mIsTurningToPlayer = turning; - } -} diff --git a/apps/openmw/mwmechanics/actor.hpp b/apps/openmw/mwmechanics/actor.hpp index be4f4253784..d7438712d9e 100644 --- a/apps/openmw/mwmechanics/actor.hpp +++ b/apps/openmw/mwmechanics/actor.hpp @@ -3,7 +3,13 @@ #include -#include "../mwmechanics/actorutil.hpp" +#include "character.hpp" +#include "creaturestats.hpp" +#include "greetingstate.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/class.hpp" #include @@ -18,43 +24,53 @@ namespace MWWorld namespace MWMechanics { - class CharacterController; - /// @brief Holds temporary state for an actor that will be discarded when the actor leaves the scene. 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) + { + } + + const MWWorld::Ptr& getPtr() const { return mCharacterController.getPtr(); } /// Notify this actor of its new base object Ptr, use when the object changed cells - void updatePtr(const MWWorld::Ptr& newPtr); + void updatePtr(const MWWorld::Ptr& newPtr) { mCharacterController.updatePtr(newPtr); } - CharacterController* getCharacterController(); + CharacterController& getCharacterController() { return mCharacterController; } + const CharacterController& getCharacterController() const { return mCharacterController; } - int getGreetingTimer() const; - void setGreetingTimer(int timer); + int getGreetingTimer() const { return mGreetingTimer; } + void setGreetingTimer(int timer) { mGreetingTimer = timer; } - float getAngleToPlayer() const; - void setAngleToPlayer(float angle); + float getAngleToPlayer() const { return mTargetAngleRadians; } + void setAngleToPlayer(float angle) { mTargetAngleRadians = angle; } - GreetingState getGreetingState() const; - void setGreetingState(GreetingState state); + GreetingState getGreetingState() const { return mGreetingState; } + void setGreetingState(GreetingState state) { mGreetingState = state; } - bool isTurningToPlayer() const; - void setTurningToPlayer(bool turning); + bool isTurningToPlayer() const { return mIsTurningToPlayer; } + void setTurningToPlayer(bool turning) { mIsTurningToPlayer = turning; } Misc::TimerStatus updateEngageCombatTimer(float duration) { - return mEngageCombat.update(duration); + return mEngageCombat.update(duration, MWBase::Environment::get().getWorld()->getPrng()); } + void setPositionAdjusted(bool adjusted) { mPositionAdjusted = adjusted; } + bool getPositionAdjusted() const { return mPositionAdjusted; } + private: - std::unique_ptr 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)}; + 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 mPositionAdjusted; }; } diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 87f462d6ff2..3f2a9b46bba 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1,493 +1,427 @@ #include "actors.hpp" +#include #include -#include -#include +#include +#include -#include #include -#include #include -#include +#include +#include +#include +#include -#include "../mwworld/esmstore.hpp" +#include +#include +#include +#include + +#include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/datetimemanager.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" -#include "../mwworld/actionequip.hpp" #include "../mwworld/player.hpp" +#include "../mwworld/scene.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/dialoguemanager.hpp" -#include "../mwbase/soundmanager.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwbase/statemanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwmechanics/aibreathe.hpp" #include "../mwrender/vismask.hpp" -#include "spellcasting.hpp" -#include "steering.hpp" -#include "npcstats.hpp" -#include "creaturestats.hpp" -#include "movement.hpp" -#include "character.hpp" -#include "aicombat.hpp" +#include "../mwsound/constants.hpp" + +#include "actor.hpp" +#include "actorutil.hpp" #include "aicombataction.hpp" #include "aifollow.hpp" #include "aipursue.hpp" #include "aiwander.hpp" -#include "actor.hpp" +#include "attacktype.hpp" +#include "character.hpp" +#include "creaturestats.hpp" +#include "movement.hpp" +#include "npcstats.hpp" +#include "steering.hpp" #include "summoning.hpp" -#include "actorutil.hpp" -#include "tickableeffects.hpp" namespace { -bool isConscious(const MWWorld::Ptr& ptr) -{ - const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - return !stats.isDead() && !stats.getKnockedDown(); -} - -int getBoundItemSlot (const std::string& itemId) -{ - static std::map boundItemsMap; - if (boundItemsMap.empty()) + bool isConscious(const MWWorld::Ptr& ptr) { - std::string boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundBootsID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Boots; - - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundCuirassID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Cuirass; + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + return !stats.isDead() && !stats.getKnockedDown(); + } - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundLeftGauntletID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_LeftGauntlet; + bool isCommanded(const MWWorld::Ptr& actor) + { + const auto& actorClass = actor.getClass(); + const auto& stats = actorClass.getCreatureStats(actor); + const bool isActorNpc = actorClass.isNpc(); + const auto level = stats.getLevel(); + for (const auto& params : stats.getActiveSpells()) + { + for (const auto& effect : params.getEffects()) + { + if (((effect.mEffectId == ESM::MagicEffect::CommandHumanoid && isActorNpc) + || (effect.mEffectId == ESM::MagicEffect::CommandCreature && !isActorNpc)) + && effect.mMagnitude >= level) + return true; + } + } + return false; + } - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_RightGauntlet; + // Check for command effects having ended and remove package if necessary + void adjustCommandedActor(const MWWorld::Ptr& actor) + { + if (isCommanded(actor)) + return; - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundHelmID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Helmet; + MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundShieldID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_CarriedLeft; + stats.getAiSequence().erasePackageIf([](auto& entry) { + if (entry->getTypeId() == MWMechanics::AiPackageTypeId::Follow + && static_cast(entry.get())->isCommanded()) + { + return true; + } + return false; + }); } - int slot = MWWorld::InventoryStore::Slot_CarriedRight; - std::map::iterator it = boundItemsMap.find(itemId); - if (it != boundItemsMap.end()) - slot = it->second; + std::pair getRestorationPerHourOfSleep(const MWWorld::Ptr& ptr) + { + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + const MWWorld::Store& settings + = MWBase::Environment::get().getESMStore()->get(); - return slot; -} + const float endurance = stats.getAttribute(ESM::Attribute::Endurance).getModified(); + const float health = 0.1f * endurance; -class CheckActorCommanded : public MWMechanics::EffectSourceVisitor -{ - MWWorld::Ptr mActor; -public: - bool mCommanded; + static const float fRestMagicMult = settings.find("fRestMagicMult")->mValue.getFloat(); + const float magicka = fRestMagicMult * stats.getAttribute(ESM::Attribute::Intelligence).getModified(); - CheckActorCommanded(const MWWorld::Ptr& actor) - : mActor(actor) - , mCommanded(false){} + return { health, magicka }; + } - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override + template + void forEachFollowingPackage( + const std::list& actors, const MWWorld::Ptr& actorPtr, const MWWorld::Ptr& player, T&& func) { - if (((key.mId == ESM::MagicEffect::CommandHumanoid && mActor.getClass().isNpc()) - || (key.mId == ESM::MagicEffect::CommandCreature && mActor.getTypeName() == typeid(ESM::Creature).name())) - && magnitude >= mActor.getClass().getCreatureStats(mActor).getLevel()) - mCommanded = true; - } -}; + for (const MWMechanics::Actor& actor : actors) + { + const MWWorld::Ptr& iteratedActor = actor.getPtr(); + if (iteratedActor == player || iteratedActor == actorPtr) + continue; -// Check for command effects having ended and remove package if necessary -void adjustCommandedActor (const MWWorld::Ptr& actor) -{ - CheckActorCommanded check(actor); - MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - stats.getActiveSpells().visitEffectSources(check); + const MWMechanics::CreatureStats& stats = iteratedActor.getClass().getCreatureStats(iteratedActor); + if (stats.isDead()) + continue; - bool hasCommandPackage = false; + // An actor counts as following if AiFollow is the current AiPackage, + // or there are only Combat and Wander packages before the AiFollow package + for (const auto& package : stats.getAiSequence()) + { + if (!func(actor, package)) + break; + } + } + } - auto it = stats.getAiSequence().begin(); - for (; it != stats.getAiSequence().end(); ++it) + float getStuntedMagickaDuration(const MWWorld::Ptr& actor) { - if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Follow && - static_cast(it->get())->isCommanded()) + float remainingTime = 0.f; + for (const auto& params : actor.getClass().getCreatureStats(actor).getActiveSpells()) { - hasCommandPackage = true; - break; + for (const auto& effect : params.getEffects()) + { + if (effect.mEffectId == ESM::MagicEffect::StuntedMagicka) + { + if (effect.mDuration == -1.f) + return -1.f; + remainingTime = std::max(remainingTime, effect.mTimeLeft); + } + } } + return remainingTime; } - if (!check.mCommanded && hasCommandPackage) - stats.getAiSequence().erase(it); -} + void soulTrap(const MWWorld::Ptr& creature) + { + const auto& stats = creature.getClass().getCreatureStats(creature); + if (!stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Soultrap).getMagnitude()) + return; + const int creatureSoulValue = creature.get()->mBase->mData.mSoul; + if (creatureSoulValue == 0) + return; + MWBase::World* const world = MWBase::Environment::get().getWorld(); + static const float fSoulgemMult + = world->getStore().get().find("fSoulgemMult")->mValue.getFloat(); + for (const auto& params : stats.getActiveSpells()) + { + for (const auto& effect : params.getEffects()) + { + if (effect.mEffectId != ESM::MagicEffect::Soultrap || effect.mMagnitude <= 0.f) + continue; + MWWorld::Ptr caster = world->searchPtrViaActorId(params.getCasterActorId()); + if (caster.isEmpty() || !caster.getClass().isActor()) + continue; -void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float& magicka) -{ - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); - const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); + // Use the smallest soulgem that is large enough to hold the soul + MWWorld::ContainerStore& container = caster.getClass().getContainerStore(caster); + MWWorld::ContainerStoreIterator gem = container.end(); + float gemCapacity = std::numeric_limits::max(); + for (auto it = container.begin(MWWorld::ContainerStore::Type_Miscellaneous); it != container.end(); + ++it) + { + if (it->getClass().isSoulGem(*it)) + { + float thisGemCapacity = it->get()->mBase->mData.mValue * fSoulgemMult; + if (thisGemCapacity >= creatureSoulValue && thisGemCapacity < gemCapacity + && it->getCellRef().getSoul().empty()) + { + gem = it; + gemCapacity = thisGemCapacity; + } + } + } + + if (gem == container.end()) + continue; - float endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); - health = 0.1f * endurance; + // Set the soul on just one of the gems, not the whole stack + gem->getContainerStore()->unstack(*gem); + gem->getCellRef().setSoul(creature.getCellRef().getRefId()); - float fRestMagicMult = settings.find("fRestMagicMult")->mValue.getFloat (); - magicka = fRestMagicMult * stats.getAttribute(ESM::Attribute::Intelligence).getModified(); -} + // Restack the gem with other gems with the same soul + gem->getContainerStore()->restack(*gem); -template -void forEachFollowingPackage(MWMechanics::Actors::PtrActorMap& actors, const MWWorld::Ptr& actor, const MWWorld::Ptr& player, T&& func) -{ - for(auto& iter : actors) - { - const MWWorld::Ptr &iteratedActor = iter.first; - if (iteratedActor == player || iteratedActor == actor) - continue; + if (caster == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}"); - const MWMechanics::CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); - if (stats.isDead()) - continue; + const ESM::Static* const fx + = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Soul_Trap")); + if (fx != nullptr) + world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), "", + creature.getRefData().getPosition().asVec3()); - // An actor counts as following if AiFollow is the current AiPackage, - // or there are only Combat and Wander packages before the AiFollow package - for (const auto& package : stats.getAiSequence()) - { - if(!func(iter, package)) - break; + MWBase::Environment::get().getSoundManager()->playSound3D( + creature.getRefData().getPosition().asVec3(), ESM::RefId::stringRefId("conjuration hit"), 1.f, 1.f); + return; // remove to get vanilla behaviour + } } } -} + + void removeTemporaryEffects(const MWWorld::Ptr& ptr) + { + ptr.getClass().getCreatureStats(ptr).getActiveSpells().unloadActor(ptr); + } } namespace MWMechanics { - static const int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player - static const int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player - static const int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement - static const float DECELERATE_DISTANCE = 512.f; + static constexpr int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player + static constexpr int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player + static constexpr int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement + static constexpr float DECELERATE_DISTANCE = 512.f; namespace { - float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration, const osg::Vec3f& halfExtents) + std::string_view attackTypeName(AttackType attackType) { - const auto distanceToNextPathPoint = (package.getNextPathPoint(package.getDestination()) - position).length(); - return (distanceToNextPathPoint - package.getNextPathPointTolerance(speed, duration, halfExtents)) / speed; - } - } - - class GetStuntedMagickaDuration : public MWMechanics::EffectSourceVisitor - { - public: - float mRemainingTime; - - GetStuntedMagickaDuration(const MWWorld::Ptr& actor) - : mRemainingTime(0.f){} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (mRemainingTime == -1) return; - - if (key.mId == ESM::MagicEffect::StuntedMagicka) + switch (attackType) { - if (totalTime == -1) - { - mRemainingTime = -1; - return; - } - - if (remainingTime > mRemainingTime) - mRemainingTime = remainingTime; + case AttackType::NoAttack: + case AttackType::Any: + return {}; + case AttackType::Chop: + return "chop"; + case AttackType::Slash: + return "slash"; + case AttackType::Thrust: + return "thrust"; } + throw std::logic_error("Invalid attack type value: " + std::to_string(static_cast(attackType))); } - }; - - class GetCurrentMagnitudes : public MWMechanics::EffectSourceVisitor - { - std::string mSpellId; - public: - GetCurrentMagnitudes(const std::string& spellId) - : mSpellId(spellId) + float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration, + const osg::Vec3f& halfExtents) { + const auto distanceToNextPathPoint + = (package.getNextPathPoint(package.getDestination()) - position).length(); + return (distanceToNextPathPoint - package.getNextPathPointTolerance(speed, duration, halfExtents)) / speed; } - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override + void updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, + MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance, bool inCombatOrPursue) { - if (magnitude <= 0) + const auto& actorRefData = actor.getRefData(); + if (!actorRefData.getBaseNode()) return; - if (sourceId != mSpellId) + if (targetActor.getClass().getCreatureStats(targetActor).isDead()) return; - mMagnitudes.emplace_back(key, magnitude); - } - - std::vector> mMagnitudes; - }; - - class GetCorprusSpells : public MWMechanics::EffectSourceVisitor - { - - public: - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (key.mId != ESM::MagicEffect::Corprus) + if (isTargetMagicallyHidden(targetActor)) return; - mSpells.push_back(sourceId); - } - - std::vector mSpells; - }; + static const float fMaxHeadTrackDistance = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fMaxHeadTrackDistance") + ->mValue.getFloat(); + static const float fInteriorHeadTrackMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fInteriorHeadTrackMult") + ->mValue.getFloat(); + float maxDistance = fMaxHeadTrackDistance; + auto currentCell = actor.getCell()->getCell(); + if (!currentCell->isExterior() && !(currentCell->isQuasiExterior())) + maxDistance *= fInteriorHeadTrackMult; + + const osg::Vec3f actor1Pos(actorRefData.getPosition().asVec3()); + const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3()); + const float sqrDist = (actor1Pos - actor2Pos).length2(); + + if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance) && !inCombatOrPursue) + return; - class SoulTrap : public MWMechanics::EffectSourceVisitor - { - MWWorld::Ptr mCreature; - MWWorld::Ptr mActor; - bool mTrapped; - public: - SoulTrap(const MWWorld::Ptr& trappedCreature) - : mCreature(trappedCreature) - , mTrapped(false) - { + // stop tracking when target is behind the actor + osg::Vec3f actorDirection = actorRefData.getBaseNode()->getAttitude() * osg::Vec3f(0, 1, 0); + osg::Vec3f targetDirection(actor2Pos - actor1Pos); + actorDirection.z() = 0; + targetDirection.z() = 0; + if ((actorDirection * targetDirection > 0 || inCombatOrPursue) + // check LOS and awareness last as it's the most expensive function + && MWBase::Environment::get().getWorld()->getLOS(actor, targetActor) + && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(targetActor, actor)) + { + sqrHeadTrackDistance = sqrDist; + headTrackTarget = targetActor; + } } - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override + void updateHeadTracking( + const MWWorld::Ptr& ptr, const std::list& actors, bool isPlayer, CharacterController& ctrl) { - if (mTrapped) - return; - if (key.mId != ESM::MagicEffect::Soultrap) - return; - if (magnitude <= 0) - return; - - MWBase::World* world = MWBase::Environment::get().getWorld(); + float sqrHeadTrackDistance = std::numeric_limits::max(); + MWWorld::Ptr headTrackTarget; - MWWorld::Ptr caster = world->searchPtrViaActorId(casterActorId); - if (caster.isEmpty() || !caster.getClass().isActor()) - return; - - static const float fSoulgemMult = world->getStore().get().find("fSoulgemMult")->mValue.getFloat(); - - int creatureSoulValue = mCreature.get()->mBase->mData.mSoul; - if (creatureSoulValue == 0) - return; + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + const bool firstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson(); - // Use the smallest soulgem that is large enough to hold the soul - MWWorld::ContainerStore& container = caster.getClass().getContainerStore(caster); - MWWorld::ContainerStoreIterator gem = container.end(); - float gemCapacity = std::numeric_limits::max(); - std::string soulgemFilter = "misc_soulgem"; // no other way to check for soulgems? :/ - for (MWWorld::ContainerStoreIterator it = container.begin(MWWorld::ContainerStore::Type_Miscellaneous); - it != container.end(); ++it) + // 1. Unconsious actor can not track target + // 2. Actors in combat and pursue mode do not bother to headtrack anyone except their target + // 3. Player character does not use headtracking in the 1st-person view + if (!stats.getKnockedDown() && !firstPersonPlayer) { - const std::string& id = it->getCellRef().getRefId(); - if (id.size() >= soulgemFilter.size() - && id.substr(0,soulgemFilter.size()) == soulgemFilter) + bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().isInPursuit(); + if (inCombatOrPursue) { - float thisGemCapacity = it->get()->mBase->mData.mValue * fSoulgemMult; - if (thisGemCapacity >= creatureSoulValue && thisGemCapacity < gemCapacity - && it->getCellRef().getSoul().empty()) + auto activePackageTarget = stats.getAiSequence().getActivePackage().getTarget(); + if (!activePackageTarget.isEmpty()) { - gem = it; - gemCapacity = thisGemCapacity; + // Track the specified target of package. + updateHeadTracking( + ptr, activePackageTarget, headTrackTarget, sqrHeadTrackDistance, inCombatOrPursue); } } - } - - if (gem == container.end()) - return; - - // Set the soul on just one of the gems, not the whole stack - gem->getContainerStore()->unstack(*gem, caster); - gem->getCellRef().setSoul(mCreature.getCellRef().getRefId()); - - // Restack the gem with other gems with the same soul - gem->getContainerStore()->restack(*gem); - - mTrapped = true; - - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}"); - - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Soul_Trap"); - if (fx) - MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, - "", mCreature.getRefData().getPosition().asVec3()); - - MWBase::Environment::get().getSoundManager()->playSound3D( - mCreature.getRefData().getPosition().asVec3(), "conjuration hit", 1.f, 1.f - ); - } - }; - - void Actors::addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) - { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - int slot = getBoundItemSlot(itemId); - - if (actor.getClass().getContainerStore(actor).count(itemId) != 0) - return; - - MWWorld::ContainerStoreIterator prevItem = store.getSlot(slot); - - MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); - MWWorld::ActionEquip action(boundPtr); - action.execute(actor); - - if (actor != MWMechanics::getPlayer()) - return; - - MWWorld::Ptr newItem; - auto it = store.getSlot(slot); - // Equip can fail because beast races cannot equip boots/helmets - if(it != store.end()) - newItem = *it; - - if (newItem.isEmpty() || boundPtr != newItem) - return; - - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - - // change draw state only if the item is in player's right hand - if (slot == MWWorld::InventoryStore::Slot_CarriedRight) - player.setDrawState(MWMechanics::DrawState_Weapon); - - if (prevItem != store.end()) - player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); - } - - void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) - { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - int slot = getBoundItemSlot(itemId); - - MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot); - - bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); - - if (actor != MWMechanics::getPlayer()) - { - store.remove(itemId, 1, actor); - - // Equip a replacement - if (!wasEquipped) - return; - - std::string type = currentItem->getTypeName(); - if (type != typeid(ESM::Weapon).name() && type != typeid(ESM::Armor).name() && type != typeid(ESM::Clothing).name()) - return; - - if (actor.getClass().getCreatureStats(actor).isDead()) - return; - - if (!actor.getClass().hasInventoryStore(actor)) - return; - - if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) - return; + else + { + // Find something nearby. + for (const Actor& otherActor : actors) + { + if (otherActor.getPtr() == ptr) + continue; - actor.getClass().getInventoryStore(actor).autoEquip(actor); + updateHeadTracking( + ptr, otherActor.getPtr(), headTrackTarget, sqrHeadTrackDistance, inCombatOrPursue); + } + } + } - return; + ctrl.setHeadTrackTarget(headTrackTarget); } - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - std::string prevItemId = player.getPreviousItem(itemId); - player.erasePreviousItem(itemId); - - if (!prevItemId.empty()) + void updateLuaControls(const MWWorld::Ptr& ptr, bool isPlayer, MWBase::LuaManager::ActorControls& controls) { - // Find previous item (or its replacement) by id. - // we should equip previous item only if expired bound item was equipped. - MWWorld::Ptr item = store.findReplacement(prevItemId); - if (!item.isEmpty() && wasEquipped) + Movement& mov = ptr.getClass().getMovementSettings(ptr); + CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + const float speedFactor = isPlayer ? 1.f : mov.mSpeedFactor; + const osg::Vec2f movement = osg::Vec2f(mov.mPosition[0], mov.mPosition[1]) * speedFactor; + const float rotationX = mov.mRotation[0]; + const float rotationZ = mov.mRotation[2]; + const bool jump = mov.mPosition[2] == 1; + const bool runFlag = stats.getMovementFlag(MWMechanics::CreatureStats::Flag_Run); + const bool sneakFlag = stats.getMovementFlag(MWMechanics::CreatureStats::Flag_Sneak); + const bool attackingOrSpell = stats.getAttackingOrSpell(); + if (controls.mChanged) { - MWWorld::ActionEquip action(item); - action.execute(actor); + mov.mPosition[0] = controls.mSideMovement; + mov.mPosition[1] = controls.mMovement; + if (controls.mJump) + mov.mPosition[2] = 1; + mov.mRotation[0] = controls.mPitchChange; + mov.mRotation[1] = 0; + mov.mRotation[2] = controls.mYawChange; + mov.mSpeedFactor = osg::Vec2(controls.mMovement, controls.mSideMovement).length(); + stats.setMovementFlag(MWMechanics::CreatureStats::Flag_Run, controls.mRun); + stats.setMovementFlag(MWMechanics::CreatureStats::Flag_Sneak, controls.mSneak); + + AttackType attackType = static_cast(controls.mUse); + stats.setAttackingOrSpell(attackType != AttackType::NoAttack); + stats.setAttackType(attackTypeName(attackType)); + + controls.mChanged = false; } + // For the player we don't need to copy these values to Lua because mwinput doesn't change them. + // All handling of these player controls was moved from C++ to a built-in Lua script. + if (!isPlayer) + { + controls.mSideMovement = movement.x(); + controls.mMovement = movement.y(); + controls.mJump = jump; + controls.mRun = runFlag; + controls.mSneak = sneakFlag; + controls.mUse = attackingOrSpell ? controls.mUse | 1 : controls.mUse & ~1; + } + // For the player these controls are still handled by mwinput, so we need to update the values. + controls.mPitchChange = rotationX; + controls.mYawChange = rotationZ; } - - store.remove(itemId, 1, actor); } - void Actors::updateActor (const MWWorld::Ptr& ptr, float duration) + void Actors::updateActor(const MWWorld::Ptr& ptr, float duration) const { // magic effects - adjustMagicEffects (ptr); - if (ptr.getClass().getCreatureStats(ptr).needToRecalcDynamicStats()) - calculateDynamicStats (ptr); + adjustMagicEffects(ptr, duration); - calculateCreatureStatModifiers (ptr, duration); // fatigue restoration calculateRestoration(ptr, duration); } - void Actors::updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, - MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance, - bool inCombatOrPursue) + void Actors::playIdleDialogue(const MWWorld::Ptr& actor) const { - if (!actor.getRefData().getBaseNode()) + if (!actor.getClass().isActor() || actor == getPlayer() + || MWBase::Environment::get().getSoundManager()->sayActive(actor)) return; - if (targetActor.getClass().getCreatureStats(targetActor).isDead()) - return; - - static const float fMaxHeadTrackDistance = MWBase::Environment::get().getWorld()->getStore().get() - .find("fMaxHeadTrackDistance")->mValue.getFloat(); - static const float fInteriorHeadTrackMult = MWBase::Environment::get().getWorld()->getStore().get() - .find("fInteriorHeadTrackMult")->mValue.getFloat(); - float maxDistance = fMaxHeadTrackDistance; - const ESM::Cell* currentCell = actor.getCell()->getCell(); - if (!currentCell->isExterior() && !(currentCell->mData.mFlags & ESM::Cell::QuasiEx)) - maxDistance *= fInteriorHeadTrackMult; - - const osg::Vec3f actor1Pos(actor.getRefData().getPosition().asVec3()); - const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3()); - float sqrDist = (actor1Pos - actor2Pos).length2(); - - if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance) && !inCombatOrPursue) - return; - - // stop tracking when target is behind the actor - osg::Vec3f actorDirection = actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); - osg::Vec3f targetDirection(actor2Pos - actor1Pos); - actorDirection.z() = 0; - targetDirection.z() = 0; - if ((actorDirection * targetDirection > 0 || inCombatOrPursue) - && MWBase::Environment::get().getWorld()->getLOS(actor, targetActor) // check LOS and awareness last as it's the most expensive function - && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(targetActor, actor)) - { - sqrHeadTrackDistance = sqrDist; - headTrackTarget = targetActor; - } - } - - void Actors::playIdleDialogue(const MWWorld::Ptr& actor) - { - if (!actor.getClass().isActor() || actor == getPlayer() || MWBase::Environment::get().getSoundManager()->sayActive(actor)) - return; - - const CreatureStats &stats = actor.getClass().getCreatureStats(actor); - if (stats.getAiSetting(CreatureStats::AI_Hello).getModified() == 0) + const CreatureStats& stats = actor.getClass().getCreatureStats(actor); + if (stats.getAiSetting(AiSetting::Hello).getModified() == 0) return; const MWMechanics::AiSequence& seq = stats.getAiSequence(); @@ -496,36 +430,39 @@ namespace MWMechanics const osg::Vec3f playerPos(getPlayer().getRefData().getPosition().asVec3()); const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); - MWBase::World* world = MWBase::Environment::get().getWorld(); + MWBase::World* const world = MWBase::Environment::get().getWorld(); if (world->isSwimming(actor) || (playerPos - actorPos).length2() >= 3000 * 3000) return; // Our implementation is not FPS-dependent unlike Morrowind's so it needs to be recalibrated. // We chose to use the chance MW would have when run at 60 FPS with the default value of the GMST. const float delta = MWBase::Environment::get().getFrameDuration() * 6.f; - static const float fVoiceIdleOdds = world->getStore().get().find("fVoiceIdleOdds")->mValue.getFloat(); - if (Misc::Rng::rollProbability() * 10000.f < fVoiceIdleOdds * delta && world->getLOS(getPlayer(), actor)) - MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); + static const float fVoiceIdleOdds + = world->getStore().get().find("fVoiceIdleOdds")->mValue.getFloat(); + if (Misc::Rng::rollProbability(world->getPrng()) * 10000.f < fVoiceIdleOdds * delta + && world->getLOS(getPlayer(), actor)) + MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("idle")); } - void Actors::updateMovementSpeed(const MWWorld::Ptr& actor) + void Actors::updateMovementSpeed(const MWWorld::Ptr& actor) const { - if (mSmoothMovement) + if (Settings::game().mSmoothMovement) return; - CreatureStats &stats = actor.getClass().getCreatureStats(actor); - MWMechanics::AiSequence& seq = stats.getAiSequence(); + const auto& actorClass = actor.getClass(); + const CreatureStats& stats = actorClass.getCreatureStats(actor); + const MWMechanics::AiSequence& seq = stats.getAiSequence(); if (!seq.isEmpty() && seq.getActivePackage().useVariableSpeed()) { - osg::Vec3f targetPos = seq.getActivePackage().getDestination(); - osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); - float distance = (targetPos - actorPos).length(); + const osg::Vec3f targetPos = seq.getActivePackage().getDestination(); + const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); + const float distance = (targetPos - actorPos).length(); if (distance < DECELERATE_DISTANCE) { - float speedCoef = std::max(0.7f, 0.2f + 0.8f * distance / DECELERATE_DISTANCE); - auto& movement = actor.getClass().getMovementSettings(actor); + const float speedCoef = std::max(0.7f, 0.2f + 0.8f * distance / DECELERATE_DISTANCE); + auto& movement = actorClass.getMovementSettings(actor); movement.mPosition[0] *= speedCoef; movement.mPosition[1] *= speedCoef; } @@ -534,16 +471,17 @@ namespace MWMechanics void Actors::updateGreetingState(const MWWorld::Ptr& actor, Actor& actorState, bool turnOnly) { - if (!actor.getClass().isActor() || actor == getPlayer()) + const auto& actorClass = actor.getClass(); + if (!actorClass.isActor() || actor == getPlayer()) return; - CreatureStats &stats = actor.getClass().getCreatureStats(actor); - const MWMechanics::AiSequence& seq = stats.getAiSequence(); + const CreatureStats& actorStats = actorClass.getCreatureStats(actor); + const MWMechanics::AiSequence& seq = actorStats.getAiSequence(); const auto packageId = seq.getTypeId(); - if (seq.isInCombat() || - MWBase::Environment::get().getWorld()->isSwimming(actor) || - (packageId != AiPackageTypeId::Wander && packageId != AiPackageTypeId::Travel && packageId != AiPackageTypeId::None)) + if (seq.isInCombat() || MWBase::Environment::get().getWorld()->isSwimming(actor) + || (packageId != AiPackageTypeId::Wander && packageId != AiPackageTypeId::Travel + && packageId != AiPackageTypeId::None)) { actorState.setTurningToPlayer(false); actorState.setGreetingTimer(0); @@ -551,10 +489,10 @@ namespace MWMechanics return; } - MWWorld::Ptr player = getPlayer(); - osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); - osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); - osg::Vec3f dir = playerPos - actorPos; + const MWWorld::Ptr player = getPlayer(); + const osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); + const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); + const osg::Vec3f dir = playerPos - actorPos; if (actorState.isTurningToPlayer()) { @@ -565,7 +503,7 @@ namespace MWMechanics { actorState.setTurningToPlayer(false); // An original engine launches an endless idle2 when an actor greets player. - playAnimationGroup (actor, "idle2", 0, std::numeric_limits::max(), false); + playAnimationGroup(actor, "idle2", 0, std::numeric_limits::max(), false); } } @@ -573,17 +511,22 @@ namespace MWMechanics return; // Play a random voice greeting if the player gets too close - static int iGreetDistanceMultiplier = MWBase::Environment::get().getWorld()->getStore() - .get().find("iGreetDistanceMultiplier")->mValue.getInteger(); + static const int iGreetDistanceMultiplier = MWBase::Environment::get() + .getESMStore() + ->get() + .find("iGreetDistanceMultiplier") + ->mValue.getInteger(); - float helloDistance = static_cast(stats.getAiSetting(CreatureStats::AI_Hello).getModified() * iGreetDistanceMultiplier); + const float helloDistance + = static_cast(actorStats.getAiSetting(AiSetting::Hello).getModified() * iGreetDistanceMultiplier); + const auto& playerStats = player.getClass().getCreatureStats(player); int greetingTimer = actorState.getGreetingTimer(); GreetingState greetingState = actorState.getGreetingState(); if (greetingState == Greet_None) { - if ((playerPos - actorPos).length2() <= helloDistance*helloDistance && - !player.getClass().getCreatureStats(player).isDead() && !actor.getClass().getCreatureStats(actor).isParalyzed() + if ((playerPos - actorPos).length2() <= helloDistance * helloDistance && !playerStats.isDead() + && !actorStats.isParalyzed() && !isTargetMagicallyHidden(player) && MWBase::Environment::get().getWorld()->getLOS(player, actor) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, actor)) greetingTimer++; @@ -591,7 +534,8 @@ namespace MWMechanics if (greetingTimer >= GREETING_SHOULD_START) { greetingState = Greet_InProgress; - MWBase::Environment::get().getDialogueManager()->say(actor, "hello"); + if (!MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("hello"))) + greetingState = Greet_Done; greetingTimer = 0; } } @@ -600,8 +544,10 @@ namespace MWMechanics { greetingTimer++; - if (!stats.getMovementFlag(CreatureStats::Flag_ForceJump) && !stats.getMovementFlag(CreatureStats::Flag_ForceSneak) - && (greetingTimer <= GREETING_SHOULD_END || MWBase::Environment::get().getSoundManager()->sayActive(actor))) + if (!actorStats.getMovementFlag(CreatureStats::Flag_ForceJump) + && !actorStats.getMovementFlag(CreatureStats::Flag_ForceSneak) + && (greetingTimer <= GREETING_SHOULD_END + || MWBase::Environment::get().getSoundManager()->sayActive(actor))) turnActorToFacePlayer(actor, actorState, dir); if (greetingTimer >= GREETING_COOLDOWN) @@ -614,7 +560,7 @@ namespace MWMechanics if (greetingState == Greet_Done) { float resetDist = 2 * helloDistance; - if ((playerPos - actorPos).length2() >= resetDist*resetDist) + if ((playerPos - actorPos).length2() >= resetDist * resetDist) greetingState = Greet_None; } @@ -622,10 +568,11 @@ namespace MWMechanics actorState.setGreetingState(greetingState); } - void Actors::turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir) + void Actors::turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir) const { - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - actor.getClass().getMovementSettings(actor).mPosition[0] = 0; + auto& movementSettings = actor.getClass().getMovementSettings(actor); + movementSettings.mPosition[1] = 0; + movementSettings.mPosition[0] = 0; if (!actorState.isTurningToPlayer()) { @@ -634,12 +581,29 @@ namespace MWMechanics float angle = std::atan2(from, to); actorState.setAngleToPlayer(angle); float deltaAngle = Misc::normalizeAngle(angle - actor.getRefData().getPosition().rot[2]); - if (!mSmoothMovement || std::abs(deltaAngle) > osg::DegreesToRadians(60.f)) + if (!Settings::game().mSmoothMovement || std::abs(deltaAngle) > osg::DegreesToRadians(60.f)) actorState.setTurningToPlayer(true); } } - void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, std::map >& cachedAllies, bool againstPlayer) + void Actors::stopCombat(const MWWorld::Ptr& ptr) const + { + auto& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + std::vector targets; + if (ai.getCombatTargets(targets)) + { + std::set allySet; + getActorsSidingWith(ptr, allySet); + std::vector allies(allySet.begin(), allySet.end()); + for (const auto& ally : allies) + ally.getClass().getCreatureStats(ally).getAiSequence().stopCombat(targets); + for (const auto& target : targets) + target.getClass().getCreatureStats(target).getAiSequence().stopCombat(allies); + } + } + + void Actors::engageCombat( + const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, SidingCache& cachedAllies, bool againstPlayer) const { // No combat for totally static creatures if (!actor1.getClass().isMobile(actor1)) @@ -655,42 +619,45 @@ namespace MWMechanics const osg::Vec3f actor1Pos(actor1.getRefData().getPosition().asVec3()); const osg::Vec3f actor2Pos(actor2.getRefData().getPosition().asVec3()); - float sqrDist = (actor1Pos - actor2Pos).length2(); + const float sqrDist = (actor1Pos - actor2Pos).length2(); + const int actorsProcessingRange = Settings::game().mActorsProcessingRange; - if (sqrDist > mActorsProcessingRange*mActorsProcessingRange) + if (sqrDist > actorsProcessingRange * actorsProcessingRange) return; - // If this is set to true, actor1 will start combat with actor2 if the awareness check at the end of the method returns true + // If this is set to true, actor1 will start combat with actor2 if the awareness check at the end of the method + // returns true bool aggressive = false; - // Get actors allied with actor1. Includes those following or escorting actor1, actors following or escorting those actors, (recursive) - // and any actor currently being followed or escorted by actor1 - std::set allies1; - - getActorsSidingWith(actor1, allies1, cachedAllies); + // Get actors allied with actor1. Includes those following or escorting actor1, actors following or escorting + // those actors, (recursive) and any actor currently being followed or escorted by actor1 + const std::set& allies1 = cachedAllies.getActorsSidingWith(actor1); - // If an ally of actor1 has been attacked by actor2 or has attacked actor2, start combat between actor1 and actor2 - for (const MWWorld::Ptr &ally : allies1) + const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager(); + // If an ally of actor1 has been attacked by actor2 or has attacked actor2, start combat between actor1 and + // actor2 + for (const MWWorld::Ptr& ally : allies1) { if (creatureStats1.getAiSequence().isInCombat(ally)) continue; if (creatureStats2.matchesActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId())) { - MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); + mechanicsManager->startCombat(actor1, actor2, &cachedAllies.getActorsSidingWith(actor2)); // Also set the same hit attempt actor. Otherwise, if fighting the player, they may stop combat // if the player gets out of reach, while the ally would continue combat with the player creatureStats1.setHitAttemptActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId()); return; } - // If there's been no attack attempt yet but an ally of actor1 is in combat with actor2, become aggressive to actor2 + // If there's been no attack attempt yet but an ally of actor1 is in combat with actor2, become aggressive + // to actor2 if (ally.getClass().getCreatureStats(ally).getAiSequence().isInCombat(actor2)) aggressive = true; } - std::set playerAllies; - getActorsSidingWith(MWMechanics::getPlayer(), playerAllies, cachedAllies); + MWWorld::Ptr player = MWMechanics::getPlayer(); + const std::set& playerAllies = cachedAllies.getActorsSidingWith(player); bool isPlayerFollowerOrEscorter = playerAllies.find(actor1) != playerAllies.end(); @@ -699,42 +666,43 @@ namespace MWMechanics if (!aggressive && !isPlayerFollowerOrEscorter) { // Check that actor2 is in combat with actor1 - if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(actor1)) + if (creatureStats2.getAiSequence().isInCombat(actor1)) { - std::set allies2; - - getActorsSidingWith(actor2, allies2, cachedAllies); - + const std::set& allies2 = cachedAllies.getActorsSidingWith(actor2); // Check that an ally of actor2 is also in combat with actor1 - for (const MWWorld::Ptr &ally2 : allies2) + for (const MWWorld::Ptr& ally2 : allies2) { - if (ally2.getClass().getCreatureStats(ally2).getAiSequence().isInCombat(actor1)) + if (ally2 != actor2 && ally2.getClass().getCreatureStats(ally2).getAiSequence().isInCombat(actor1)) { - MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); + mechanicsManager->startCombat(actor1, actor2, &allies2); // Also have actor1's allies start combat for (const MWWorld::Ptr& ally1 : allies1) - MWBase::Environment::get().getMechanicsManager()->startCombat(ally1, actor2); + if (ally1 != player) + mechanicsManager->startCombat(ally1, actor2, &allies2); return; } } } } + if (creatureStats2.getMagicEffects().getOrDefault(ESM::MagicEffect::Invisibility).getMagnitude() > 0) + return; + // Stop here if target is unreachable if (!canFight(actor1, actor2)) return; - // If set in the settings file, player followers and escorters will become aggressive toward enemies in combat with them or the player - static const bool followersAttackOnSight = Settings::Manager::getBool("followers attack on sight", "Game"); - if (!aggressive && isPlayerFollowerOrEscorter && followersAttackOnSight) + // If set in the settings file, player followers and escorters will become aggressive toward enemies in combat + // with them or the player + if (!aggressive && isPlayerFollowerOrEscorter && Settings::game().mFollowersAttackOnSight) { - if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(actor1)) + if (creatureStats2.getAiSequence().isInCombat(actor1)) aggressive = true; else { - for (const MWWorld::Ptr &ally : allies1) + for (const MWWorld::Ptr& ally : allies1) { - if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(ally)) + if (ally != actor1 && creatureStats2.getAiSequence().isInCombat(ally)) { aggressive = true; break; @@ -751,473 +719,102 @@ namespace MWMechanics // Player followers and escorters with high fight should not initiate combat with the player or with // other player followers or escorters if (!isPlayerFollowerOrEscorter) - aggressive = MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2); + aggressive = mechanicsManager->isAggressive(actor1, actor2); } } - // Make guards go aggressive with creatures that are in combat, unless the creature is a follower or escorter - if (!aggressive && actor1.getClass().isClass(actor1, "Guard") && !actor2.getClass().isNpc() && creatureStats2.getAiSequence().isInCombat()) + // Make guards go aggressive with creatures and werewolves that are in combat + const auto world = MWBase::Environment::get().getWorld(); + if (!aggressive && actor1.getClass().isClass(actor1, "Guard") && creatureStats2.getAiSequence().isInCombat()) { // Check if the creature is too far - static const float fAlarmRadius = MWBase::Environment::get().getWorld()->getStore().get().find("fAlarmRadius")->mValue.getFloat(); + static const float fAlarmRadius + = world->getStore().get().find("fAlarmRadius")->mValue.getFloat(); if (sqrDist > fAlarmRadius * fAlarmRadius) return; - bool followerOrEscorter = false; - for (const auto& package : creatureStats2.getAiSequence()) - { - // The follow package must be first or have nothing but combat before it - if (package->sideWithTarget()) - { - followerOrEscorter = true; - break; - } - else if (package->getTypeId() != MWMechanics::AiPackageTypeId::Combat) - break; - } - if (!followerOrEscorter) - aggressive = true; - } - - // If any of the above conditions turned actor1 aggressive towards actor2, do an awareness check. If it passes, start combat with actor2. - if (aggressive) - { - bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor1, actor2) - && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor2, actor1); - - if (LOS) - MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); - } - } - - void Actors::adjustMagicEffects (const MWWorld::Ptr& creature) - { - CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); - if (creatureStats.isDeathAnimationFinished()) - return; - - MagicEffects now = creatureStats.getSpells().getMagicEffects(); - - if (creature.getClass().hasInventoryStore(creature)) - { - MWWorld::InventoryStore& store = creature.getClass().getInventoryStore (creature); - now += store.getMagicEffects(); - } - - now += creatureStats.getActiveSpells().getMagicEffects(); - - creatureStats.modifyMagicEffects(now); - } - - void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr) - { - CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); - - float intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified(); - - float base = 1.f; - if (ptr == getPlayer()) - base = MWBase::Environment::get().getWorld()->getStore().get().find("fPCbaseMagickaMult")->mValue.getFloat(); - else - base = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCbaseMagickaMult")->mValue.getFloat(); - - double magickaFactor = base + - creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; - - DynamicStat magicka = creatureStats.getMagicka(); - float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); - float currentToBaseRatio = (magicka.getCurrent() / magicka.getBase()); - magicka.setModified(magicka.getModified() + diff, 0); - magicka.setCurrent(magicka.getBase() * currentToBaseRatio, false, true); - creatureStats.setMagicka(magicka); - } - - void Actors::restoreDynamicStats (const MWWorld::Ptr& ptr, double hours, bool sleep) - { - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); - if (stats.isDead()) - return; - - const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); - - if (sleep) - { - float health, magicka; - getRestorationPerHourOfSleep(ptr, health, magicka); - - DynamicStat stat = stats.getHealth(); - stat.setCurrent(stat.getCurrent() + health * hours); - stats.setHealth(stat); - - double restoreHours = hours; - bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; - if (stunted) - { - // Stunted Magicka effect should be taken into account. - GetStuntedMagickaDuration visitor(ptr); - stats.getActiveSpells().visitEffectSources(visitor); - stats.getSpells().visitEffectSources(visitor); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).visitEffectSources(visitor); - - // Take a maximum remaining duration of Stunted Magicka effects (-1 is a constant one) in game hours. - if (visitor.mRemainingTime > 0) - { - double timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); - if(timeScale == 0.0) - timeScale = 1; - - restoreHours = std::max(0.0, hours - visitor.mRemainingTime * timeScale / 3600.f); - } - else if (visitor.mRemainingTime == -1) - restoreHours = 0; - } - - if (restoreHours > 0) - { - stat = stats.getMagicka(); - stat.setCurrent(stat.getCurrent() + magicka * restoreHours); - stats.setMagicka(stat); - } - } - - // Current fatigue can be above base value due to a fortify effect. - // In that case stop here and don't try to restore. - DynamicStat fatigue = stats.getFatigue(); - if (fatigue.getCurrent() >= fatigue.getBase()) - return; - - // Restore fatigue - float fFatigueReturnBase = settings.find("fFatigueReturnBase")->mValue.getFloat (); - float fFatigueReturnMult = settings.find("fFatigueReturnMult")->mValue.getFloat (); - float fEndFatigueMult = settings.find("fEndFatigueMult")->mValue.getFloat (); - - float endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); - - float normalizedEncumbrance = ptr.getClass().getNormalizedEncumbrance(ptr); - if (normalizedEncumbrance > 1) - normalizedEncumbrance = 1; - - float x = fFatigueReturnBase + fFatigueReturnMult * (1 - normalizedEncumbrance); - x *= fEndFatigueMult * endurance; - - fatigue.setCurrent (fatigue.getCurrent() + 3600 * x * hours); - stats.setFatigue (fatigue); - } - - void Actors::calculateRestoration (const MWWorld::Ptr& ptr, float duration) - { - if (ptr.getClass().getCreatureStats(ptr).isDead()) - return; - - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); - - // Current fatigue can be above base value due to a fortify effect. - // In that case stop here and don't try to restore. - DynamicStat fatigue = stats.getFatigue(); - if (fatigue.getCurrent() >= fatigue.getBase()) - return; - - // Restore fatigue - float endurance = stats.getAttribute(ESM::Attribute::Endurance).getModified(); - const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); - static const float fFatigueReturnBase = settings.find("fFatigueReturnBase")->mValue.getFloat (); - static const float fFatigueReturnMult = settings.find("fFatigueReturnMult")->mValue.getFloat (); - - float x = fFatigueReturnBase + fFatigueReturnMult * endurance; - - fatigue.setCurrent (fatigue.getCurrent() + duration * x); - stats.setFatigue (fatigue); - } - - class ExpiryVisitor : public EffectSourceVisitor - { - private: - MWWorld::Ptr mActor; - float mDuration; - - public: - ExpiryVisitor(const MWWorld::Ptr& actor, float duration) - : mActor(actor), mDuration(duration) - { - } - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float magnitude, float remainingTime = -1, float /*totalTime*/ = -1) override - { - if (magnitude > 0 && remainingTime > 0 && remainingTime < mDuration) - { - CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); - if (effectTick(creatureStats, mActor, key, magnitude * remainingTime)) - creatureStats.getMagicEffects().add(key, -magnitude); - } - } - }; - - void Actors::applyCureEffects(const MWWorld::Ptr& actor) - { - CreatureStats &creatureStats = actor.getClass().getCreatureStats(actor); - const MagicEffects &effects = creatureStats.getMagicEffects(); - - if (effects.get(ESM::MagicEffect::CurePoison).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Poison); - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Poison); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Poison); - } - if (effects.get(ESM::MagicEffect::CureParalyzation).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze); - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Paralyze); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Paralyze); - } - if (effects.get(ESM::MagicEffect::CureCommonDisease).getModifier() > 0) - { - creatureStats.getSpells().purgeCommonDisease(); - } - if (effects.get(ESM::MagicEffect::CureBlightDisease).getModifier() > 0) - { - creatureStats.getSpells().purgeBlightDisease(); - } - if (effects.get(ESM::MagicEffect::CureCorprusDisease).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeCorprusDisease(); - creatureStats.getSpells().purgeCorprusDisease(); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Corprus, true); - } - if (effects.get(ESM::MagicEffect::RemoveCurse).getModifier() > 0) - { - creatureStats.getSpells().purgeCurses(); - } - } - - void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration) - { - CreatureStats &creatureStats = ptr.getClass().getCreatureStats(ptr); - const MagicEffects &effects = creatureStats.getMagicEffects(); - bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - applyCureEffects(ptr); - - bool wasDead = creatureStats.isDead(); - - if (duration > 0) - { - // Apply correct magnitude for tickable effects that have just expired, - // in case duration > remaining time of effect. - // One case where this will happen is when the player uses the rest/wait command - // while there is a tickable effect active that should expire before the end of the rest/wait. - ExpiryVisitor visitor(ptr, duration); - creatureStats.getActiveSpells().visitEffectSources(visitor); - - for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) + bool targetIsCreature = !actor2.getClass().isNpc(); + if (targetIsCreature || actor2.getClass().getNpcStats(actor2).isWerewolf()) { - // tickable effects (i.e. effects having a lasting impact after expiry) - effectTick(creatureStats, ptr, it->first, it->second.getMagnitude() * duration); - - // instant effects are already applied on spell impact in spellcasting.cpp, but may also come from permanent abilities - if (it->second.getMagnitude() > 0) + bool followerOrEscorter = false; + // ...unless the creature has allies + if (targetIsCreature) { - CastSpell cast(ptr, ptr); - if (cast.applyInstantEffect(ptr, ptr, it->first, it->second.getMagnitude())) + for (const auto& package : creatureStats2.getAiSequence()) { - creatureStats.getSpells().purgeEffect(it->first.mId); - creatureStats.getActiveSpells().purgeEffect(it->first.mId); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).purgeEffect(it->first.mId); - } - } - } - } - - // purge levitate effect if levitation is disabled - // check only modifier, because base value can be setted from SetFlying console command. - if (MWBase::Environment::get().getWorld()->isLevitationEnabled() == false && effects.get(ESM::MagicEffect::Levitate).getModifier() > 0) - { - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Levitate); - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Levitate); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).purgeEffect(ESM::MagicEffect::Levitate); - - if (ptr == getPlayer()) - { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); - } - } - - // dynamic stats - for (int i = 0; i < 3; ++i) - { - DynamicStat stat = creatureStats.getDynamic(i); - float fortify = effects.get(ESM::MagicEffect::FortifyHealth + i).getMagnitude(); - float drain = 0.f; - if (!godmode) - drain = effects.get(ESM::MagicEffect::DrainHealth + i).getMagnitude(); - stat.setCurrentModifier(fortify - drain, - // Magicka can be decreased below zero due to a fortify effect wearing off - // Fatigue can be decreased below zero meaning the actor will be knocked out - i == 1 || i == 2); - - creatureStats.setDynamic(i, stat); - } - - // attributes - for(int i = 0;i < ESM::Attribute::Length;++i) - { - AttributeValue stat = creatureStats.getAttribute(i); - float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifyAttribute, i)).getMagnitude(); - float drain = 0.f, absorb = 0.f; - if (!godmode) - { - drain = effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).getMagnitude(); - absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbAttribute, i)).getMagnitude(); - } - stat.setModifier(static_cast(fortify - drain - absorb)); - - creatureStats.setAttribute(i, stat); - } - - if (creatureStats.needToRecalcDynamicStats()) - calculateDynamicStats(ptr); - - if (ptr == getPlayer()) - { - GetCorprusSpells getCorprusSpellsVisitor; - creatureStats.getSpells().visitEffectSources(getCorprusSpellsVisitor); - creatureStats.getActiveSpells().visitEffectSources(getCorprusSpellsVisitor); - ptr.getClass().getInventoryStore(ptr).visitEffectSources(getCorprusSpellsVisitor); - std::vector corprusSpells = getCorprusSpellsVisitor.mSpells; - std::vector corprusSpellsToRemove; - - for (auto it = creatureStats.getCorprusSpells().begin(); it != creatureStats.getCorprusSpells().end(); ++it) - { - if(std::find(corprusSpells.begin(), corprusSpells.end(), it->first) == corprusSpells.end()) - { - // Corprus effect expired, remove entry and restore stats. - MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, it->first); - corprusSpellsToRemove.push_back(it->first); - corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); - continue; - } - - corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); - - if (MWBase::Environment::get().getWorld()->getTimeStamp() >= it->second.mNextWorsening) - { - it->second.mNextWorsening += CorprusStats::sWorseningPeriod; - GetCurrentMagnitudes getMagnitudesVisitor (it->first); - creatureStats.getSpells().visitEffectSources(getMagnitudesVisitor); - creatureStats.getActiveSpells().visitEffectSources(getMagnitudesVisitor); - ptr.getClass().getInventoryStore(ptr).visitEffectSources(getMagnitudesVisitor); - for (auto& effectMagnitude : getMagnitudesVisitor.mMagnitudes) - { - if (effectMagnitude.first.mId == ESM::MagicEffect::FortifyAttribute) - { - AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); - attr.damage(-effectMagnitude.second); - creatureStats.setAttribute(effectMagnitude.first.mArg, attr); - it->second.mWorsenings[effectMagnitude.first.mArg] = 0; - } - else if (effectMagnitude.first.mId == ESM::MagicEffect::DrainAttribute) + // The follow package must be first or have nothing but combat before it + if (package->sideWithTarget()) { - AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); - int currentDamage = attr.getDamage(); - if (currentDamage >= 0) - it->second.mWorsenings[effectMagnitude.first.mArg] = std::min(it->second.mWorsenings[effectMagnitude.first.mArg], currentDamage); - - it->second.mWorsenings[effectMagnitude.first.mArg] += effectMagnitude.second; - attr.damage(effectMagnitude.second); - creatureStats.setAttribute(effectMagnitude.first.mArg, attr); + followerOrEscorter = true; + break; } + else if (package->getTypeId() != MWMechanics::AiPackageTypeId::Combat) + break; } - - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); } - } - - for (std::string& oldCorprusSpell : corprusSpellsToRemove) - { - creatureStats.removeCorprusSpell(oldCorprusSpell); - } - - for (std::string& newCorprusSpell : corprusSpells) - { - CorprusStats corprus; - for (int i=0; igetTimeStamp() + CorprusStats::sWorseningPeriod; - - creatureStats.addCorprusSpell(newCorprusSpell, corprus); + // Morrowind also checks "known werewolf" flag, but the player is never in combat + // so this code is unreachable for the player + if (!followerOrEscorter) + aggressive = true; } } - // AI setting modifiers - int creature = !ptr.getClass().isNpc(); - if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Humanoid) - creature = false; - // Note: the Creature variants only work on normal creatures, not on daedra or undead creatures. - if (!creature || ptr.get()->mBase->mData.mType == ESM::Creature::Creatures) - { - Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Fight); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::FrenzyHumanoid + creature).getMagnitude() - - effects.get(ESM::MagicEffect::CalmHumanoid+creature).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Fight, stat); - - stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::DemoralizeHumanoid + creature).getMagnitude() - - effects.get(ESM::MagicEffect::RallyHumanoid+creature).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); - } - if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Undead) + // If any of the above conditions turned actor1 aggressive towards actor2, do an awareness check. If it passes, + // start combat with actor2. + if (aggressive) { - Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::TurnUndead).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); + bool LOS = world->getLOS(actor1, actor2) && mechanicsManager->awarenessCheck(actor2, actor1); + + if (LOS) + mechanicsManager->startCombat(actor1, actor2, &cachedAllies.getActorsSidingWith(actor2)); } + } + + void Actors::adjustMagicEffects(const MWWorld::Ptr& creature, float duration) const + { + CreatureStats& creatureStats = creature.getClass().getCreatureStats(creature); + const bool wasDead = creatureStats.isDead(); + + creatureStats.getActiveSpells().update(creature, duration); if (!wasDead && creatureStats.isDead()) { // The actor was killed by a magic effect. Figure out if the player was responsible for it. const ActiveSpells& spells = creatureStats.getActiveSpells(); - MWWorld::Ptr player = getPlayer(); + const MWWorld::Ptr player = getPlayer(); std::set playerFollowers; getActorsSidingWith(player, playerFollowers); - for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ActiveSpells::ActiveSpellParams& spell : spells) { bool actorKilled = false; - const ActiveSpells::ActiveSpellParams& spell = it->second; - MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.mCasterActorId); - for (std::vector::const_iterator effectIt = spell.mEffects.begin(); - effectIt != spell.mEffects.end(); ++effectIt) + MWWorld::Ptr caster + = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.getCasterActorId()); + if (caster.isEmpty()) + continue; + for (const auto& effect : spell.getEffects()) { - int effectId = effectIt->mEffectId; - bool isDamageEffect = false; - - int damageEffects[] = { - ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, - ESM::MagicEffect::SunDamage, ESM::MagicEffect::DamageHealth, ESM::MagicEffect::AbsorbHealth + static const std::array damageEffects{ + ESM::MagicEffect::FireDamage, + ESM::MagicEffect::ShockDamage, + ESM::MagicEffect::FrostDamage, + ESM::MagicEffect::Poison, + ESM::MagicEffect::SunDamage, + ESM::MagicEffect::DamageHealth, + ESM::MagicEffect::AbsorbHealth, }; - for (unsigned int i=0; iactorKilled(ptr, player); + MWBase::Environment::get().getMechanicsManager()->actorKilled(creature, player); actorKilled = true; break; } @@ -1229,185 +826,191 @@ namespace MWMechanics } } - // TODO: dirty flag for magic effects to avoid some unnecessary work below? + // updateSummons assumes the actor belongs to a cell. + // This assumption isn't always valid for the player character. + if (!creature.isInCell()) + return; - // any value of calm > 0 will stop the actor from fighting - if ((effects.get(ESM::MagicEffect::CalmHumanoid).getMagnitude() > 0 && ptr.getClass().isNpc()) - || (effects.get(ESM::MagicEffect::CalmCreature).getMagnitude() > 0 && !ptr.getClass().isNpc())) - creatureStats.getAiSequence().stopCombat(); + if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty()) + updateSummons(creature, mTimerDisposeSummonsCorpses == 0.f); + } - // Update bound effects - // Note: in vanilla MW multiple bound items of the same type can be created by different spells. - // As these extra copies are kinda useless this may or may not be important. - static std::map boundItemsMap; - if (boundItemsMap.empty()) - { - boundItemsMap[ESM::MagicEffect::BoundBattleAxe] = "sMagicBoundBattleAxeID"; - boundItemsMap[ESM::MagicEffect::BoundBoots] = "sMagicBoundBootsID"; - boundItemsMap[ESM::MagicEffect::BoundCuirass] = "sMagicBoundCuirassID"; - boundItemsMap[ESM::MagicEffect::BoundDagger] = "sMagicBoundDaggerID"; - boundItemsMap[ESM::MagicEffect::BoundGloves] = "sMagicBoundLeftGauntletID"; // Note: needs RightGauntlet variant too (see below) - boundItemsMap[ESM::MagicEffect::BoundHelm] = "sMagicBoundHelmID"; - boundItemsMap[ESM::MagicEffect::BoundLongbow] = "sMagicBoundLongbowID"; - boundItemsMap[ESM::MagicEffect::BoundLongsword] = "sMagicBoundLongswordID"; - boundItemsMap[ESM::MagicEffect::BoundMace] = "sMagicBoundMaceID"; - boundItemsMap[ESM::MagicEffect::BoundShield] = "sMagicBoundShieldID"; - boundItemsMap[ESM::MagicEffect::BoundSpear] = "sMagicBoundSpearID"; - } + void Actors::restoreDynamicStats(const MWWorld::Ptr& ptr, double hours, bool sleep) const + { + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + if (stats.isDead()) + return; + + const MWWorld::Store& settings + = MWBase::Environment::get().getESMStore()->get(); - if(ptr.getClass().hasInventoryStore(ptr)) + if (sleep) { - for (const auto& [effect, itemGmst] : boundItemsMap) - { - bool found = creatureStats.mBoundItems.find(effect) != creatureStats.mBoundItems.end(); - float magnitude = effects.get(effect).getMagnitude(); - if (found != (magnitude > 0)) - { - if (magnitude > 0) - creatureStats.mBoundItems.insert(effect); - else - creatureStats.mBoundItems.erase(effect); + const auto [health, magicka] = getRestorationPerHourOfSleep(ptr); - std::string item = MWBase::Environment::get().getWorld()->getStore().get().find( - itemGmst)->mValue.getString(); + DynamicStat stat = stats.getHealth(); + stat.setCurrent(stat.getCurrent() + health * hours); + stats.setHealth(stat); - magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); + double restoreHours = hours; + const bool stunted + = stats.getMagicEffects().getOrDefault(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; + if (stunted) + { + // Stunted Magicka effect should be taken into account. + float remainingTime = getStuntedMagickaDuration(ptr); - if (effect == ESM::MagicEffect::BoundGloves) - { - item = MWBase::Environment::get().getWorld()->getStore().get().find( - "sMagicBoundRightGauntletID")->mValue.getString(); - magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); - } + // Take a maximum remaining duration of Stunted Magicka effects (-1 is a constant one) in game hours. + if (remainingTime > 0) + { + double timeScale = MWBase::Environment::get().getWorld()->getTimeManager()->getGameTimeScale(); + if (timeScale == 0.0) + timeScale = 1; + + restoreHours = std::max(0.0, hours - remainingTime * timeScale / 3600.f); } + else if (remainingTime == -1) + restoreHours = 0; } - } - - // Summoned creature update visitor assumes the actor belongs to a cell. - // This assumption isn't always valid for the player character. - if (!ptr.isInCell()) - return; - bool hasSummonEffect = false; - for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) - { - if (isSummoningEffect(it->first.mId)) + if (restoreHours > 0) { - hasSummonEffect = true; - break; + stat = stats.getMagicka(); + stat.setCurrent(stat.getCurrent() + magicka * restoreHours); + stats.setMagicka(stat); } } - if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty() || hasSummonEffect) - { - UpdateSummonedCreatures updateSummonedCreatures(ptr); - creatureStats.getActiveSpells().visitEffectSources(updateSummonedCreatures); - creatureStats.getSpells().visitEffectSources(updateSummonedCreatures); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).visitEffectSources(updateSummonedCreatures); - updateSummonedCreatures.process(mTimerDisposeSummonsCorpses == 0.f); - } + // Current fatigue can be above base value due to a fortify effect. + // In that case stop here and don't try to restore. + DynamicStat fatigue = stats.getFatigue(); + if (fatigue.getCurrent() >= fatigue.getBase()) + return; + + // Restore fatigue + static const float fFatigueReturnBase = settings.find("fFatigueReturnBase")->mValue.getFloat(); + static const float fFatigueReturnMult = settings.find("fFatigueReturnMult")->mValue.getFloat(); + static const float fEndFatigueMult = settings.find("fEndFatigueMult")->mValue.getFloat(); + + const float endurance = stats.getAttribute(ESM::Attribute::Endurance).getModified(); + + float normalizedEncumbrance = ptr.getClass().getNormalizedEncumbrance(ptr); + if (normalizedEncumbrance > 1) + normalizedEncumbrance = 1; + + const float x + = (fFatigueReturnBase + fFatigueReturnMult * (1 - normalizedEncumbrance)) * (fEndFatigueMult * endurance); + + fatigue.setCurrent(fatigue.getCurrent() + 3600 * x * hours); + stats.setFatigue(fatigue); } - void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration) + void Actors::calculateRestoration(const MWWorld::Ptr& ptr, float duration) const { - NpcStats &npcStats = ptr.getClass().getNpcStats(ptr); - const MagicEffects &effects = npcStats.getMagicEffects(); - bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); + if (ptr.getClass().getCreatureStats(ptr).isDead()) + return; - // skills - for(int i = 0;i < ESM::Skill::Length;++i) - { - SkillValue& skill = npcStats.getSkill(i); - float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifySkill, i)).getMagnitude(); - float drain = 0.f, absorb = 0.f; - if (!godmode) - { - drain = effects.get(EffectKey(ESM::MagicEffect::DrainSkill, i)).getMagnitude(); - absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbSkill, i)).getMagnitude(); - } - skill.setModifier(static_cast(fortify - drain - absorb)); - } + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + + // Current fatigue can be above base value due to a fortify effect. + // In that case stop here and don't try to restore. + DynamicStat fatigue = stats.getFatigue(); + if (fatigue.getCurrent() >= fatigue.getBase()) + return; + + // Restore fatigue + const float endurance = stats.getAttribute(ESM::Attribute::Endurance).getModified(); + const MWWorld::Store& settings + = MWBase::Environment::get().getESMStore()->get(); + static const float fFatigueReturnBase = settings.find("fFatigueReturnBase")->mValue.getFloat(); + static const float fFatigueReturnMult = settings.find("fFatigueReturnMult")->mValue.getFloat(); + + const float x = fFatigueReturnBase + fFatigueReturnMult * endurance; + + fatigue.setCurrent(fatigue.getCurrent() + duration * x); + stats.setFatigue(fatigue); } - bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr) + bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr) const { - PtrActorMap::iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - CharacterController* ctrl = it->second->getCharacterController(); - - return ctrl->isAttackPreparing(); + return it->second->getCharacterController().isAttackPreparing(); } - bool Actors::isRunning(const MWWorld::Ptr& ptr) + bool Actors::isRunning(const MWWorld::Ptr& ptr) const { - PtrActorMap::iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - CharacterController* ctrl = it->second->getCharacterController(); - - return ctrl->isRunning(); + return it->second->getCharacterController().isRunning(); } - bool Actors::isSneaking(const MWWorld::Ptr& ptr) + bool Actors::isSneaking(const MWWorld::Ptr& ptr) const { - PtrActorMap::iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - CharacterController* ctrl = it->second->getCharacterController(); - - return ctrl->isSneaking(); + return it->second->getCharacterController().isSneaking(); } - void Actors::updateDrowning(const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer) + static void updateDrowning(const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer) { - NpcStats &stats = ptr.getClass().getNpcStats(ptr); + const auto& actorClass = ptr.getClass(); + NpcStats& stats = actorClass.getNpcStats(ptr); // When npc stats are just initialized, mTimeToStartDrowning == -1 and we should get value from GMST - static const float fHoldBreathTime = MWBase::Environment::get().getWorld()->getStore().get().find("fHoldBreathTime")->mValue.getFloat(); + static const float fHoldBreathTime = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fHoldBreathTime") + ->mValue.getFloat(); if (stats.getTimeToStartDrowning() == -1.f) stats.setTimeToStartDrowning(fHoldBreathTime); if (!isPlayer && stats.getTimeToStartDrowning() < fHoldBreathTime / 2) { - AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence(); - if (seq.getTypeId() != AiPackageTypeId::Breathe) //Only add it once + AiSequence& seq = actorClass.getCreatureStats(ptr).getAiSequence(); + if (seq.getTypeId() != AiPackageTypeId::Breathe) // Only add it once seq.stack(AiBreathe(), ptr); } - MWBase::World *world = MWBase::Environment::get().getWorld(); - bool knockedOutUnderwater = (isKnockedOut && world->isUnderwater(ptr.getCell(), osg::Vec3f(ptr.getRefData().getPosition().asVec3()))); - if((world->isSubmerged(ptr) || knockedOutUnderwater) - && stats.getMagicEffects().get(ESM::MagicEffect::WaterBreathing).getMagnitude() == 0) + const MWBase::World* const world = MWBase::Environment::get().getWorld(); + const bool knockedOutUnderwater + = (isKnockedOut && world->isUnderwater(ptr.getCell(), osg::Vec3f(ptr.getRefData().getPosition().asVec3()))); + if ((world->isSubmerged(ptr) || knockedOutUnderwater) + && stats.getMagicEffects().getOrDefault(ESM::MagicEffect::WaterBreathing).getMagnitude() == 0) { float timeLeft = 0.0f; - if(knockedOutUnderwater) + if (knockedOutUnderwater) stats.setTimeToStartDrowning(0); else { timeLeft = stats.getTimeToStartDrowning() - duration; - if(timeLeft < 0.0f) + if (timeLeft < 0.0f) timeLeft = 0.0f; stats.setTimeToStartDrowning(timeLeft); } - bool godmode = isPlayer && MWBase::Environment::get().getWorld()->getGodModeState(); + const bool godmode = isPlayer && world->getGodModeState(); - if(timeLeft == 0.0f && !godmode) + if (timeLeft == 0.0f && !godmode) { // If drowning, apply 3 points of damage per second - static const float fSuffocationDamage = world->getStore().get().find("fSuffocationDamage")->mValue.getFloat(); + static const float fSuffocationDamage + = world->getStore().get().find("fSuffocationDamage")->mValue.getFloat(); DynamicStat health = stats.getHealth(); - health.setCurrent(health.getCurrent() - fSuffocationDamage*duration); + health.setCurrent(health.getCurrent() - fSuffocationDamage * duration); stats.setHealth(health); // Play a drowning sound - MWBase::SoundManager *sndmgr = MWBase::Environment::get().getSoundManager(); - if(!sndmgr->getSoundPlaying(ptr, "drown")) - sndmgr->playSound3D(ptr, "drown", 1.0f, 1.0f); + MWBase::SoundManager* sndmgr = MWBase::Environment::get().getSoundManager(); + auto soundDrown = ESM::RefId::stringRefId("drown"); + if (!sndmgr->getSoundPlaying(ptr, soundDrown)) + sndmgr->playSound3D(ptr, soundDrown, 1.0f, 1.0f); - if(isPlayer) + if (isPlayer) MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); } } @@ -1415,45 +1018,39 @@ namespace MWMechanics stats.setTimeToStartDrowning(fHoldBreathTime); } - void Actors::updateEquippedLight (const MWWorld::Ptr& ptr, float duration, bool mayEquip) + static void updateEquippedLight(const MWWorld::Ptr& ptr, float duration, bool mayEquip) { - bool isPlayer = (ptr == getPlayer()); + const bool isPlayer = (ptr == getPlayer()); + + const auto& actorClass = ptr.getClass(); + auto& inventoryStore = actorClass.getInventoryStore(ptr); + + auto heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - MWWorld::InventoryStore &inventoryStore = ptr.getClass().getInventoryStore(ptr); - MWWorld::ContainerStoreIterator heldIter = - inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); /** * Automatically equip NPCs torches at night and unequip them at day */ if (!isPlayer) { - MWWorld::ContainerStoreIterator torch = inventoryStore.end(); - for (MWWorld::ContainerStoreIterator it = inventoryStore.begin(); it != inventoryStore.end(); ++it) - { - if (it->getTypeName() == typeid(ESM::Light).name() && - it->getClass().canBeEquipped(*it, ptr).first) - { - torch = it; - break; - } - } - + auto torchIter = std::find_if(std::begin(inventoryStore), std::end(inventoryStore), [&](auto entry) { + return entry.getType() == ESM::Light::sRecordId && entry.getClass().canBeEquipped(entry, ptr).first; + }); if (mayEquip) { - if (torch != inventoryStore.end()) + if (torchIter != inventoryStore.end()) { - if (!ptr.getClass().getCreatureStats (ptr).getAiSequence().isInCombat()) + if (!actorClass.getCreatureStats(ptr).getAiSequence().isInCombat()) { // For non-hostile NPCs, unequip whatever is in the left slot in favor of a light. - if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name()) - inventoryStore.unequipItem(*heldIter, ptr); + if (heldIter != inventoryStore.end() && heldIter->getType() != ESM::Light::sRecordId) + inventoryStore.unequipItem(*heldIter); } - else if (heldIter == inventoryStore.end() || heldIter->getTypeName() == typeid(ESM::Light).name()) + else if (heldIter == inventoryStore.end() || heldIter->getType() == ESM::Light::sRecordId) { // For hostile NPCs, see if they have anything better to equip first - auto shield = inventoryStore.getPreferredShield(ptr); - if(shield != inventoryStore.end()) - inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, shield, ptr); + auto shield = inventoryStore.getPreferredShield(); + if (shield != inventoryStore.end()) + inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, shield); } heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); @@ -1461,164 +1058,149 @@ namespace MWMechanics // If we have a torch and can equip it, then equip it now. if (heldIter == inventoryStore.end()) { - inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, torch, ptr); + inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, torchIter); } } } else { - if (heldIter != inventoryStore.end() && heldIter->getTypeName() == typeid(ESM::Light).name()) + if (heldIter != inventoryStore.end() && heldIter->getType() == ESM::Light::sRecordId) { // At day, unequip lights and auto equip shields or other suitable items // (Note: autoEquip will ignore lights) - inventoryStore.autoEquip(ptr); + inventoryStore.autoEquip(); } } } heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - //If holding a light... - if(heldIter.getType() == MWWorld::ContainerStore::Type_Light) + // If holding a light... + const auto world = MWBase::Environment::get().getWorld(); + MWRender::Animation* anim = world->getAnimation(ptr); + if (heldIter.getType() == MWWorld::ContainerStore::Type_Light && anim && anim->getCarriedLeftShown()) { // Use time from the player's light - if(isPlayer) + if (isPlayer) { - // But avoid using it up if the light source is hidden - MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); - if (anim && anim->getCarriedLeftShown()) - { - float timeRemaining = heldIter->getClass().getRemainingUsageTime(*heldIter); + float timeRemaining = heldIter->getClass().getRemainingUsageTime(*heldIter); - // -1 is infinite light source. Other negative values are treated as 0. - if (timeRemaining != -1.0f) + // -1 is infinite light source. Other negative values are treated as 0. + if (timeRemaining != -1.0f) + { + timeRemaining -= duration; + if (timeRemaining <= 0.f) { - timeRemaining -= duration; - if (timeRemaining <= 0.f) - { - inventoryStore.remove(*heldIter, 1, ptr); // remove it - return; - } - - heldIter->getClass().setRemainingUsageTime(*heldIter, timeRemaining); + inventoryStore.remove(*heldIter, 1); // remove it + return; } + + heldIter->getClass().setRemainingUsageTime(*heldIter, timeRemaining); } } // Both NPC and player lights extinguish in water. - if(MWBase::Environment::get().getWorld()->isSwimming(ptr)) + if (world->isSwimming(ptr)) { - inventoryStore.remove(*heldIter, 1, ptr); // remove it + inventoryStore.remove(*heldIter, 1); // remove it // ...But, only the player makes a sound. - if(isPlayer) - MWBase::Environment::get().getSoundManager()->playSound("torch out", - 1.0, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); + if (isPlayer) + MWBase::Environment::get().getSoundManager()->playSound( + ESM::RefId::stringRefId("torch out"), 1.0, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); } } } - void Actors::updateCrimePursuit(const MWWorld::Ptr& ptr, float duration) + void Actors::updateCrimePursuit(const MWWorld::Ptr& ptr, float duration, SidingCache& cachedAllies) const { - MWWorld::Ptr player = getPlayer(); - if (ptr != player && ptr.getClass().isNpc()) - { - // get stats of witness - CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); - NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + const MWWorld::Ptr player = getPlayer(); + if (ptr == player) + return; - if (player.getClass().getNpcStats(player).isWerewolf()) - return; + const auto& actorClass = ptr.getClass(); + if (!actorClass.isNpc()) + return; - if (ptr.getClass().isClass(ptr, "Guard") && creatureStats.getAiSequence().getTypeId() != AiPackageTypeId::Pursue && !creatureStats.getAiSequence().isInCombat() - && creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmHumanoid).getMagnitude() == 0) - { - const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); - static const int cutoff = esmStore.get().find("iCrimeThreshold")->mValue.getInteger(); - // Force dialogue on sight if bounty is greater than the cutoff - // In vanilla morrowind, the greeting dialogue is scripted to either arrest the player (< 5000 bounty) or attack (>= 5000 bounty) - if ( player.getClass().getNpcStats(player).getBounty() >= cutoff - // TODO: do not run these two every frame. keep an Aware state for each actor and update it every 0.2 s or so? - && MWBase::Environment::get().getWorld()->getLOS(ptr, player) - && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, ptr)) - { - static const int iCrimeThresholdMultiplier = esmStore.get().find("iCrimeThresholdMultiplier")->mValue.getInteger(); - if (player.getClass().getNpcStats(player).getBounty() >= cutoff * iCrimeThresholdMultiplier) - { - MWBase::Environment::get().getMechanicsManager()->startCombat(ptr, player); - creatureStats.setHitAttemptActorId(player.getClass().getCreatureStats(player).getActorId()); // Stops the guard from quitting combat if player is unreachable - } - else - creatureStats.getAiSequence().stack(AiPursue(player), ptr); - creatureStats.setAlarmed(true); - npcStats.setCrimeId(MWBase::Environment::get().getWorld()->getPlayer().getNewCrimeId()); - } - } + // get stats of witness + CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); + NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + + const auto& playerClass = player.getClass(); + const auto& playerStats = playerClass.getNpcStats(player); + if (playerStats.isWerewolf()) + return; - // if I was a witness to a crime - if (npcStats.getCrimeId() != -1) + const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager(); + const auto world = MWBase::Environment::get().getWorld(); + + if (actorClass.isClass(ptr, "Guard") && !creatureStats.getAiSequence().isInPursuit() + && !creatureStats.getAiSequence().isInCombat() + && creatureStats.getMagicEffects().getOrDefault(ESM::MagicEffect::CalmHumanoid).getMagnitude() == 0) + { + const MWWorld::ESMStore& esmStore = world->getStore(); + static const int cutoff = esmStore.get().find("iCrimeThreshold")->mValue.getInteger(); + // Force dialogue on sight if bounty is greater than the cutoff + // In vanilla morrowind, the greeting dialogue is scripted to either arrest the player (< 5000 bounty) or + // attack (>= 5000 bounty) + if (playerStats.getBounty() >= cutoff + // TODO: do not run these two every frame. keep an Aware state for each actor and update it every 0.2 s + // or so? + && world->getLOS(ptr, player) && mechanicsManager->awarenessCheck(player, ptr)) { - // if you've paid for your crimes and I havent noticed - if( npcStats.getCrimeId() <= MWBase::Environment::get().getWorld()->getPlayer().getCrimeId() ) + static const int iCrimeThresholdMultiplier + = esmStore.get().find("iCrimeThresholdMultiplier")->mValue.getInteger(); + if (playerStats.getBounty() >= cutoff * iCrimeThresholdMultiplier) { - // Calm witness down - if (ptr.getClass().isClass(ptr, "Guard")) - creatureStats.getAiSequence().stopPursuit(); - creatureStats.getAiSequence().stopCombat(); - - // Reset factors to attack - creatureStats.setAttacked(false); - creatureStats.setAlarmed(false); - creatureStats.setAiSetting(CreatureStats::AI_Fight, ptr.getClass().getBaseFightRating(ptr)); - - // Update witness crime id - npcStats.setCrimeId(-1); + mechanicsManager->startCombat(ptr, player, &cachedAllies.getActorsSidingWith(player)); + creatureStats.setHitAttemptActorId( + playerClass.getCreatureStats(player) + .getActorId()); // Stops the guard from quitting combat if player is unreachable } + else + creatureStats.getAiSequence().stack(AiPursue(player), ptr); + creatureStats.setAlarmed(true); + npcStats.setCrimeId(world->getPlayer().getNewCrimeId()); } } - } - - Actors::Actors() : mSmoothMovement(Settings::Manager::getBool("smooth movement", "Game")) - { - mTimerDisposeSummonsCorpses = 0.2f; // We should add a delay between summoned creature death and its corpse despawning - - updateProcessingRange(); - } - Actors::~Actors() - { - clear(); - } + // if I was a witness to a crime + if (npcStats.getCrimeId() != -1) + { + // if you've paid for your crimes and I haven't noticed + if (npcStats.getCrimeId() <= world->getPlayer().getCrimeId()) + { + // Calm witness down + if (ptr.getClass().isClass(ptr, "Guard")) + creatureStats.getAiSequence().stopPursuit(); + stopCombat(ptr); - float Actors::getProcessingRange() const - { - return mActorsProcessingRange; - } + // Reset factors to attack + creatureStats.setAttacked(false); + creatureStats.setAlarmed(false); + creatureStats.setAiSetting(AiSetting::Fight, ptr.getClass().getBaseFightRating(ptr)); - void Actors::updateProcessingRange() - { - // We have to cap it since using high values (larger than 7168) will make some quests harder or impossible to complete (bug #1876) - static const float maxProcessingRange = 7168.f; - static const float minProcessingRange = maxProcessingRange / 2.f; + // Restore original disposition + npcStats.setCrimeDispositionModifier(0); - float actorsProcessingRange = Settings::Manager::getFloat("actors processing range", "Game"); - actorsProcessingRange = std::min(actorsProcessingRange, maxProcessingRange); - actorsProcessingRange = std::max(actorsProcessingRange, minProcessingRange); - mActorsProcessingRange = actorsProcessingRange; + // Update witness crime id + npcStats.setCrimeId(-1); + } + } } - void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) + void Actors::addActor(const MWWorld::Ptr& ptr, bool updateImmediately) { - removeActor(ptr); + removeActor(ptr, true); - MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (!anim) return; - mActors.insert(std::make_pair(ptr, new Actor(ptr, anim))); + const auto it = mActors.emplace(mActors.end(), ptr, anim); + mIndex.emplace(ptr.mRef, it); - CharacterController* ctrl = mActors[ptr]->getCharacterController(); if (updateImmediately) - ctrl->update(0); + it->getCharacterController().update(0); // We should initially hide actors outside of processing range. // Note: since we update player after other actors, distance will be incorrect during teleportation. @@ -1626,17 +1208,19 @@ namespace MWMechanics if (MWBase::Environment::get().getWorld()->getPlayer().wasTeleported()) return; - updateVisibility(ptr, ctrl); + updateVisibility(ptr, it->getCharacterController()); } - void Actors::updateVisibility (const MWWorld::Ptr& ptr, CharacterController* ctrl) + void Actors::updateVisibility(const MWWorld::Ptr& ptr, CharacterController& ctrl) const { MWWorld::Ptr player = MWMechanics::getPlayer(); if (ptr == player) return; - const float dist = (player.getRefData().getPosition().asVec3() - ptr.getRefData().getPosition().asVec3()).length(); - if (dist > mActorsProcessingRange) + const float dist + = (player.getRefData().getPosition().asVec3() - ptr.getRefData().getPosition().asVec3()).length(); + const int actorsProcessingRange = Settings::game().mActorsProcessingRange; + if (dist > actorsProcessingRange) { ptr.getRefData().getBaseNode()->setNodeMask(0); return; @@ -1646,35 +1230,37 @@ namespace MWMechanics // Fade away actors on large distance (>90% of actor's processing distance) float visibilityRatio = 1.0; - float fadeStartDistance = mActorsProcessingRange*0.9f; - float fadeEndDistance = mActorsProcessingRange; - float fadeRatio = (dist - fadeStartDistance)/(fadeEndDistance - fadeStartDistance); + const float fadeStartDistance = actorsProcessingRange * 0.9f; + const float fadeEndDistance = actorsProcessingRange; + const float fadeRatio = (dist - fadeStartDistance) / (fadeEndDistance - fadeStartDistance); if (fadeRatio > 0) visibilityRatio -= std::max(0.f, fadeRatio); visibilityRatio = std::min(1.f, visibilityRatio); - ctrl->setVisibility(visibilityRatio); + ctrl.setVisibility(visibilityRatio); } - void Actors::removeActor (const MWWorld::Ptr& ptr) + void Actors::removeActor(const MWWorld::Ptr& ptr, bool keepActive) { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) { - delete iter->second; - mActors.erase(iter); + if (!keepActive) + removeTemporaryEffects(iter->second->getPtr()); + mActors.erase(iter->second); + mIndex.erase(iter); } } - void Actors::castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) + void Actors::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) - iter->second->getCharacterController()->castSpell(spellId, manualSpell); + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().castSpell(spellId, scriptedSpell); } - bool Actors::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) + bool Actors::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) const { if (!actor.getClass().isActor()) return false; @@ -1682,22 +1268,21 @@ namespace MWMechanics // If an observer is NPC, check if he detected an actor if (!observer.isEmpty() && observer.getClass().isNpc()) { - return - MWBase::Environment::get().getWorld()->getLOS(observer, actor) && - MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor, observer); + return MWBase::Environment::get().getWorld()->getLOS(observer, actor) + && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor, observer); } // Otherwise check if any actor in AI processing range sees the target actor std::vector neighbors; - osg::Vec3f position (actor.getRefData().getPosition().asVec3()); - getObjectsInRange(position, mActorsProcessingRange, neighbors); - for (const MWWorld::Ptr &neighbor : neighbors) + osg::Vec3f position(actor.getRefData().getPosition().asVec3()); + getObjectsInRange(position, Settings::game().mActorsProcessingRange, neighbors); + for (const MWWorld::Ptr& neighbor : neighbors) { if (neighbor == actor) continue; - bool result = MWBase::Environment::get().getWorld()->getLOS(neighbor, actor) - && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor, neighbor); + const bool result = MWBase::Environment::get().getWorld()->getLOS(neighbor, actor) + && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor, neighbor); if (result) return true; @@ -1706,78 +1291,29 @@ namespace MWMechanics return false; } - void Actors::updateActor(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) + void Actors::updateActor(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) const { - PtrActorMap::iterator iter = mActors.find(old); - if(iter != mActors.end()) - { - Actor *actor = iter->second; - mActors.erase(iter); - - actor->updatePtr(ptr); - mActors.insert(std::make_pair(ptr, actor)); - } + const auto iter = mIndex.find(old.mRef); + if (iter != mIndex.end()) + iter->second->updatePtr(ptr); } - void Actors::dropActors (const MWWorld::CellStore *cellStore, const MWWorld::Ptr& ignore) + void Actors::dropActors(const MWWorld::CellStore* cellStore, const MWWorld::Ptr& ignore) { - PtrActorMap::iterator iter = mActors.begin(); - while(iter != mActors.end()) + for (auto iter = mActors.begin(); iter != mActors.end();) { - if((iter->first.isInCell() && iter->first.getCell()==cellStore) && iter->first != ignore) + if ((iter->getPtr().isInCell() && iter->getPtr().getCell() == cellStore) && iter->getPtr() != ignore) { - delete iter->second; - mActors.erase(iter++); + removeTemporaryEffects(iter->getPtr()); + mIndex.erase(iter->getPtr().mRef); + iter = mActors.erase(iter); } else ++iter; } } - void Actors::updateCombatMusic () - { - MWWorld::Ptr player = getPlayer(); - const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); - bool hasHostiles = false; // need to know this to play Battle music - bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); - - if (aiActive) - { - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) - { - if (iter->first == player) continue; - - bool inProcessingRange = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() <= mActorsProcessingRange*mActorsProcessingRange; - if (inProcessingRange) - { - MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); - if (!stats.isDead() && stats.getAiSequence().isInCombat()) - { - hasHostiles = true; - break; - } - } - } - } - - // check if we still have any player enemies to switch music - static int currentMusic = 0; - - if (currentMusic != 1 && !hasHostiles && !(player.getClass().getCreatureStats(player).isDead() && - MWBase::Environment::get().getSoundManager()->isMusicPlaying())) - { - MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); - currentMusic = 1; - } - else if (currentMusic != 2 && hasHostiles) - { - MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle")); - currentMusic = 2; - } - - } - - void Actors::predictAndAvoidCollisions(float duration) + void Actors::predictAndAvoidCollisions(float duration) const { if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) return; @@ -1786,23 +1322,23 @@ namespace MWMechanics const float maxDistForPartialAvoiding = 200.f; const float maxDistForStrictAvoiding = 100.f; const float maxTimeToCheck = 2.0f; - static const bool giveWayWhenIdle = Settings::Manager::getBool("NPCs give way", "Game"); + const bool giveWayWhenIdle = Settings::game().mNPCsGiveWay; - MWWorld::Ptr player = getPlayer(); - MWBase::World* world = MWBase::Environment::get().getWorld(); - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + const MWWorld::Ptr player = getPlayer(); + const MWBase::World* const world = MWBase::Environment::get().getWorld(); + for (const Actor& actor : mActors) { - const MWWorld::Ptr& ptr = iter->first; + const MWWorld::Ptr& ptr = actor.getPtr(); if (ptr == player) continue; // Don't interfere with player controls. - float maxSpeed = ptr.getClass().getMaxSpeed(ptr); + const float maxSpeed = ptr.getClass().getMaxSpeed(ptr); if (maxSpeed == 0.0) continue; // Can't move, so there is no sense to predict collisions. Movement& movement = ptr.getClass().getMovementSettings(ptr); - osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]); - bool isMoving = origMovement.length2() > 0.01; + const osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]); + const bool isMoving = origMovement.length2() > 0.01; if (movement.mPosition[1] < 0) continue; // Actors can not see others when move backward. @@ -1810,54 +1346,60 @@ namespace MWMechanics // Standing NPCs give way to moving ones if they are not in combat (or pursue) mode and either // follow player or have a AIWander package with non-empty wander area. bool shouldAvoidCollision = isMoving; + bool shouldGiveWay = false; bool shouldTurnToApproachingActor = !isMoving; MWWorld::Ptr currentTarget; // Combat or pursue target (NPCs should not avoid collision with their targets). const auto& aiSequence = ptr.getClass().getCreatureStats(ptr).getAiSequence(); - for (const auto& package : aiSequence) + if (!aiSequence.isEmpty()) { - if (package->getTypeId() == AiPackageTypeId::Follow) + const auto& package = aiSequence.getActivePackage(); + if (package.getTypeId() == AiPackageTypeId::Follow) + { shouldAvoidCollision = true; - else if (package->getTypeId() == AiPackageTypeId::Wander && giveWayWhenIdle) + } + else if (package.getTypeId() == AiPackageTypeId::Wander && giveWayWhenIdle) { - if (!static_cast(package.get())->isStationary()) - shouldAvoidCollision = true; + if (!static_cast(package).isStationary()) + shouldGiveWay = true; } - else if (package->getTypeId() == AiPackageTypeId::Combat || package->getTypeId() == AiPackageTypeId::Pursue) + else if (package.getTypeId() == AiPackageTypeId::Combat + || package.getTypeId() == AiPackageTypeId::Pursue) { - currentTarget = package->getTarget(); + currentTarget = package.getTarget(); shouldAvoidCollision = isMoving; shouldTurnToApproachingActor = false; - break; } } - if (!shouldAvoidCollision) + + if (!shouldAvoidCollision && !shouldGiveWay) continue; - osg::Vec2f baseSpeed = origMovement * maxSpeed; - osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3(); - float baseRotZ = ptr.getRefData().getPosition().rot[2]; - osg::Vec3f halfExtents = world->getHalfExtents(ptr); - float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding; + const osg::Vec2f baseSpeed = origMovement * maxSpeed; + const osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3(); + const float baseRotZ = ptr.getRefData().getPosition().rot[2]; + const osg::Vec3f halfExtents = world->getHalfExtents(ptr); + const float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding; + + float timeToCheck = maxTimeToCheck; + if (!shouldGiveWay && !aiSequence.isEmpty()) + timeToCheck = std::min( + timeToCheck, getTimeToDestination(**aiSequence.begin(), basePos, maxSpeed, duration, halfExtents)); - float timeToCollision = maxTimeToCheck; + float timeToCollision = timeToCheck; osg::Vec2f movementCorrection(0, 0); float angleToApproachingActor = 0; - const float timeToDestination = aiSequence.isEmpty() - ? std::numeric_limits::max() - : getTimeToDestination(**aiSequence.begin(), basePos, maxSpeed, duration, halfExtents); - // Iterate through all other actors and predict collisions. - for(PtrActorMap::iterator otherIter(mActors.begin()); otherIter != mActors.end(); ++otherIter) + for (const Actor& otherActor : mActors) { - const MWWorld::Ptr& otherPtr = otherIter->first; + const MWWorld::Ptr& otherPtr = otherActor.getPtr(); if (otherPtr == ptr || otherPtr == currentTarget) continue; - osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr); - osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos; - osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ); - float dist = deltaPos.length(); + const osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr); + const osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos; + const osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ); + const float dist = deltaPos.length(); // Ignore actors which are not close enough or come from behind. if (dist > maxDistToCheck || relPos.y() < 0) @@ -1867,23 +1409,24 @@ namespace MWMechanics if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2) continue; - osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() * - otherPtr.getClass().getMaxSpeed(otherPtr); - float rotZ = otherPtr.getRefData().getPosition().rot[2]; - osg::Vec2f relSpeed = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed; + const osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() + * otherPtr.getClass().getMaxSpeed(otherPtr); + const float rotZ = otherPtr.getRefData().getPosition().rot[2]; + const osg::Vec2f relSpeed + = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed; - float collisionDist = minGap + world->getHalfExtents(ptr).x() + world->getHalfExtents(otherPtr).x(); + float collisionDist = minGap + halfExtents.x() + otherHalfExtents.x(); collisionDist = std::min(collisionDist, relPos.length()); // Find the earliest `t` when |relPos + relSpeed * t| == collisionDist. - float vr = relPos.x() * relSpeed.x() + relPos.y() * relSpeed.y(); - float v2 = relSpeed.length2(); - float Dh = vr * vr - v2 * (relPos.length2() - collisionDist * collisionDist); + const float vr = relPos.x() * relSpeed.x() + relPos.y() * relSpeed.y(); + const float v2 = relSpeed.length2(); + const float Dh = vr * vr - v2 * (relPos.length2() - collisionDist * collisionDist); if (Dh <= 0 || v2 == 0) continue; // No solution; distance is always >= collisionDist. - float t = (-vr - std::sqrt(Dh)) / v2; + const float t = (-vr - std::sqrt(Dh)) / v2; - if (t < 0 || t > timeToCollision || t > timeToDestination) + if (t < 0 || t > timeToCollision) continue; // Check visibility and awareness last as it's expensive. @@ -1894,20 +1437,24 @@ namespace MWMechanics timeToCollision = t; angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y()); - osg::Vec2f posAtT = relPos + relSpeed * t; - float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * collisionDist * maxSpeed); - coef *= osg::clampBetween((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f); + const osg::Vec2f posAtT = relPos + relSpeed * t; + const float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) + / (collisionDist * collisionDist * maxSpeed) + * std::clamp( + (maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), + 0.f, 1.f); movementCorrection = posAtT * coef; if (otherPtr.getClass().getCreatureStats(otherPtr).isDead()) // In case of dead body still try to go around (it looks natural), but reduce the correction twice. movementCorrection.y() *= 0.5f; } - if (timeToCollision < maxTimeToCheck) + if (timeToCollision < timeToCheck) { // Try to evade the nearest collision. osg::Vec2f newMovement = origMovement + movementCorrection; - // Step to the side rather than backward. Otherwise player will be able to push the NPC far away from it's original location. + // Step to the side rather than backward. Otherwise player will be able to push the NPC far away from + // it's original location. newMovement.y() = std::max(newMovement.y(), 0.f); newMovement.normalize(); if (isMoving) @@ -1920,33 +1467,37 @@ namespace MWMechanics } } - void Actors::update (float duration, bool paused) + void Actors::update(float duration, bool paused) { - if(!paused) + if (!paused) { - static float timerUpdateHeadTrack = 0; - static float timerUpdateEquippedLight = 0; - static float timerUpdateHello = 0; const float updateEquippedLightInterval = 1.0f; - if (timerUpdateHeadTrack >= 0.3f) timerUpdateHeadTrack = 0; - if (timerUpdateHello >= 0.25f) timerUpdateHello = 0; - if (mTimerDisposeSummonsCorpses >= 0.2f) mTimerDisposeSummonsCorpses = 0; - if (timerUpdateEquippedLight >= updateEquippedLightInterval) timerUpdateEquippedLight = 0; + if (mTimerUpdateHeadTrack >= 0.3f) + mTimerUpdateHeadTrack = 0; + + if (mTimerUpdateHello >= 0.25f) + mTimerUpdateHello = 0; + + if (mTimerDisposeSummonsCorpses >= 0.2f) + mTimerDisposeSummonsCorpses = 0; + + if (mTimerUpdateEquippedLight >= updateEquippedLightInterval) + mTimerUpdateEquippedLight = 0; // show torches only when there are darkness and no precipitations - MWBase::World* world = MWBase::Environment::get().getWorld(); - bool showTorches = world->useTorches(); + MWBase::World* const world = MWBase::Environment::get().getWorld(); + const bool showTorches = world->useTorches(); - MWWorld::Ptr player = getPlayer(); + const MWWorld::Ptr player = getPlayer(); const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); /// \todo move update logic to Actor class where appropriate - std::map > cachedAllies; // will be filled as engageCombat iterates + SidingCache cachedAllies{ *this, true }; // will be filled as engageCombat iterates - bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); - int attackedByPlayerId = player.getClass().getCreatureStats(player).getHitAttemptActorId(); + const bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); + const int attackedByPlayerId = player.getClass().getCreatureStats(player).getHitAttemptActorId(); if (attackedByPlayerId != -1) { const MWWorld::Ptr playerHitAttemptActor = world->searchPtrViaActorId(attackedByPlayerId); @@ -1954,57 +1505,59 @@ namespace MWMechanics if (!playerHitAttemptActor.isInCell()) player.getClass().getCreatureStats(player).setHitAttemptActorId(-1); } - bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); + const int actorsProcessingRange = Settings::game().mActorsProcessingRange; - // AI and magic effects update - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + // AI and magic effects update + for (Actor& actor : mActors) { - bool isPlayer = iter->first == player; - CharacterController* ctrl = iter->second->getCharacterController(); + const bool isPlayer = actor.getPtr() == player; + CharacterController& ctrl = actor.getCharacterController(); + MWBase::LuaManager::ActorControls* luaControls + = MWBase::Environment::get().getLuaManager()->getActorControls(actor.getPtr()); - float distSqr = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2(); + const float distSqr = (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length2(); // AI processing is only done within given distance to the player. - bool inProcessingRange = distSqr <= mActorsProcessingRange*mActorsProcessingRange; - - if (isPlayer) - ctrl->setAttackingOrSpell(world->getPlayer().getAttackingOrSpell()); - - // If dead or no longer in combat, no longer store any actors who attempted to hit us. Also remove for the player. - if (iter->first != player && (iter->first.getClass().getCreatureStats(iter->first).isDead() - || !iter->first.getClass().getCreatureStats(iter->first).getAiSequence().isInCombat() - || !inProcessingRange)) + const bool inProcessingRange = distSqr <= actorsProcessingRange * actorsProcessingRange; + + // If dead or no longer in combat, no longer store any actors who attempted to hit us. Also remove for + // the player. + if (!isPlayer + && (actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDead() + || !actor.getPtr().getClass().getCreatureStats(actor.getPtr()).getAiSequence().isInCombat() + || !inProcessingRange)) { - iter->first.getClass().getCreatureStats(iter->first).setHitAttemptActorId(-1); - if (player.getClass().getCreatureStats(player).getHitAttemptActorId() == iter->first.getClass().getCreatureStats(iter->first).getActorId()) + actor.getPtr().getClass().getCreatureStats(actor.getPtr()).setHitAttemptActorId(-1); + if (player.getClass().getCreatureStats(player).getHitAttemptActorId() + == actor.getPtr().getClass().getCreatureStats(actor.getPtr()).getActorId()) player.getClass().getCreatureStats(player).setHitAttemptActorId(-1); } - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); - - const Misc::TimerStatus engageCombatTimerStatus = iter->second->updateEngageCombatTimer(duration); + const Misc::TimerStatus engageCombatTimerStatus = actor.updateEngageCombatTimer(duration); // For dead actors we need to update looping spell particles - if (iter->first.getClass().getCreatureStats(iter->first).isDead()) + if (actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDead()) { // They can be added during the death animation - if (!iter->first.getClass().getCreatureStats(iter->first).isDeathAnimationFinished()) - adjustMagicEffects(iter->first); - ctrl->updateContinuousVfx(); + if (!actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDeathAnimationFinished()) + adjustMagicEffects(actor.getPtr(), duration); + ctrl.updateContinuousVfx(); } else { - bool cellChanged = world->hasCellChanged(); - MWWorld::Ptr actor = iter->first; // make a copy of the map key to avoid it being invalidated when the player teleports - updateActor(actor, duration); + MWWorld::Scene* worldScene = MWBase::Environment::get().getWorldScene(); + const bool cellChanged = worldScene->hasCellChanged(); + const MWWorld::Ptr actorPtr = actor.getPtr(); // make a copy of the map key to avoid it being + // invalidated when the player teleports + updateActor(actorPtr, duration); // Looping magic VFX update // Note: we need to do this before any of the animations are updated. // Reaching the text keys may trigger Hit / Spellcast (and as such, particles), // so updating VFX immediately after that would just remove the particle effects instantly. // There needs to be a magic effect update in between. - ctrl->updateContinuousVfx(); + ctrl.updateContinuousVfx(); - if (!cellChanged && world->hasCellChanged()) + if (!cellChanged && worldScene->hasCellChanged()) { return; // for now abort update of the old cell when cell changes by teleportation magic effect // a better solution might be to apply cell changes at the end of the frame @@ -2014,162 +1567,146 @@ namespace MWMechanics if (engageCombatTimerStatus == Misc::TimerStatus::Elapsed) { if (!isPlayer) - adjustCommandedActor(iter->first); + adjustCommandedActor(actor.getPtr()); - for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) + for (const Actor& otherActor : mActors) { - if (it->first == iter->first || isPlayer) // player is not AI-controlled + if (otherActor.getPtr() == actor.getPtr() || isPlayer) // player is not AI-controlled continue; - engageCombat(iter->first, it->first, cachedAllies, it->first == player); - } - } - if (timerUpdateHeadTrack == 0) - { - float sqrHeadTrackDistance = std::numeric_limits::max(); - MWWorld::Ptr headTrackTarget; - - MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); - bool firstPersonPlayer = isPlayer && world->isFirstPerson(); - bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue); - MWWorld::Ptr activePackageTarget; - - // 1. Unconsious actor can not track target - // 2. Actors in combat and pursue mode do not bother to headtrack anyone except their target - // 3. Player character does not use headtracking in the 1st-person view - if (!stats.getKnockedDown() && !firstPersonPlayer) - { - if (inCombatOrPursue) - activePackageTarget = stats.getAiSequence().getActivePackage().getTarget(); - - for (PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) - { - if (it->first == iter->first) - continue; - - if (inCombatOrPursue && it->first != activePackageTarget) - continue; - - updateHeadTracking(iter->first, it->first, headTrackTarget, sqrHeadTrackDistance, inCombatOrPursue); - } + engageCombat( + actor.getPtr(), otherActor.getPtr(), cachedAllies, otherActor.getPtr() == player); } - - ctrl->setHeadTrackTarget(headTrackTarget); } + if (mTimerUpdateHeadTrack == 0) + updateHeadTracking(actor.getPtr(), mActors, isPlayer, ctrl); - if (iter->first.getClass().isNpc() && iter->first != player) - updateCrimePursuit(iter->first, duration); + if (actor.getPtr().getClass().isNpc() && !isPlayer) + updateCrimePursuit(actor.getPtr(), duration, cachedAllies); - if (iter->first != player) + if (!isPlayer) { - CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); - if (isConscious(iter->first)) + CreatureStats& stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); + if (isConscious(actor.getPtr()) && !(luaControls && luaControls->mDisableAI)) { - stats.getAiSequence().execute(iter->first, *ctrl, duration); - updateGreetingState(iter->first, *iter->second, timerUpdateHello > 0); - playIdleDialogue(iter->first); - updateMovementSpeed(iter->first); + stats.getAiSequence().execute(actor.getPtr(), ctrl, duration); + updateGreetingState(actor.getPtr(), actor, mTimerUpdateHello > 0); + playIdleDialogue(actor.getPtr()); + updateMovementSpeed(actor.getPtr()); } } } - else if (aiActive && iter->first != player && isConscious(iter->first)) + else if (aiActive && !isPlayer && isConscious(actor.getPtr()) + && !(luaControls && luaControls->mDisableAI)) { - CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); - stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true); + CreatureStats& stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); + stats.getAiSequence().execute(actor.getPtr(), ctrl, duration, /*outOfRange*/ true); } - if(iter->first.getClass().isNpc()) + if (inProcessingRange && actor.getPtr().getClass().isNpc()) { - // We can not update drowning state for actors outside of AI distance - they can not resurface to breathe - if (inProcessingRange) - updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); - - calculateNpcStatModifiers(iter->first, duration); - - if (timerUpdateEquippedLight == 0) - updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); + // We can not update drowning state for actors outside of AI distance - they can not resurface + // to breathe + updateDrowning(actor.getPtr(), duration, ctrl.isKnockedOut(), isPlayer); } + if (mTimerUpdateEquippedLight == 0 && actor.getPtr().getClass().hasInventoryStore(actor.getPtr())) + updateEquippedLight(actor.getPtr(), updateEquippedLightInterval, showTorches); + + if (luaControls != nullptr && isConscious(actor.getPtr())) + updateLuaControls(actor.getPtr(), isPlayer, *luaControls); } } - static const bool avoidCollisions = Settings::Manager::getBool("NPCs avoid collisions", "Game"); - if (avoidCollisions) + if (Settings::game().mNPCsAvoidCollisions) predictAndAvoidCollisions(duration); - timerUpdateHeadTrack += duration; - timerUpdateEquippedLight += duration; - timerUpdateHello += duration; + mTimerUpdateHeadTrack += duration; + mTimerUpdateEquippedLight += duration; + mTimerUpdateHello += duration; mTimerDisposeSummonsCorpses += duration; // Animation/movement update CharacterController* playerCharacter = nullptr; - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for (Actor& actor : mActors) { - const float dist = (playerPos - iter->first.getRefData().getPosition().asVec3()).length(); - bool isPlayer = iter->first == player; - CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); + const float dist = (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length(); + const bool isPlayer = actor.getPtr() == player; + CreatureStats& stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); // Actors with active AI should be able to move. bool alwaysActive = false; - if (!isPlayer && isConscious(iter->first) && !stats.isParalyzed()) + if (!isPlayer && isConscious(actor.getPtr()) && !stats.isParalyzed()) { MWMechanics::AiSequence& seq = stats.getAiSequence(); alwaysActive = !seq.isEmpty() && seq.getActivePackage().alwaysActive(); } - bool inRange = isPlayer || dist <= mActorsProcessingRange || alwaysActive; - int activeFlag = 1; // Can be changed back to '2' to keep updating bounding boxes off screen (more accurate, but slower) - if (isPlayer) - activeFlag = 2; - int active = inRange ? activeFlag : 0; + const bool inRange = isPlayer || dist <= actorsProcessingRange || alwaysActive; + const int activeFlag = isPlayer ? 2 : 1; // Can be changed back to '2' to keep updating bounding boxes + // off screen (more accurate, but slower) + const int active = inRange ? activeFlag : 0; - CharacterController* ctrl = iter->second->getCharacterController(); - ctrl->setActive(active); + CharacterController& ctrl = actor.getCharacterController(); + ctrl.setActive(active); if (!inRange) { - iter->first.getRefData().getBaseNode()->setNodeMask(0); - world->setActorCollisionMode(iter->first, false, false); + actor.getPtr().getRefData().getBaseNode()->setNodeMask(0); + world->setActorActive(actor.getPtr(), false); continue; } - else if (!isPlayer) - iter->first.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Actor); - const bool isDead = iter->first.getClass().getCreatureStats(iter->first).isDead(); - if (!isDead && (!godmode || !isPlayer) && iter->first.getClass().getCreatureStats(iter->first).isParalyzed()) - ctrl->skipAnim(); + world->setActorActive(actor.getPtr(), true); + + const bool isDead = actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDead(); + if (!isDead && actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isParalyzed()) + ctrl.skipAnim(); // Handle player last, in case a cell transition occurs by casting a teleportation spell // (would invalidate the iterator) - if (iter->first == getPlayer()) + if (isPlayer) { - playerCharacter = ctrl; + playerCharacter = &ctrl; continue; } - world->setActorCollisionMode(iter->first, true, !iter->first.getClass().getCreatureStats(iter->first).isDeathAnimationFinished()); - ctrl->update(duration); + actor.getPtr().getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Actor); + world->setActorCollisionMode(actor.getPtr(), true, + !actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDeathAnimationFinished()); + + if (!actor.getPositionAdjusted()) + { + actor.getPtr().getClass().adjustPosition(actor.getPtr(), false); + actor.setPositionAdjusted(true); + } + + ctrl.update(duration); - updateVisibility(iter->first, ctrl); + updateVisibility(actor.getPtr(), ctrl); } if (playerCharacter) { + MWBase::Environment::get().getWorld()->applyDeferredPreviewRotationToPlayer(duration); playerCharacter->update(duration); playerCharacter->setVisibility(1.f); + MWBase::LuaManager::ActorControls* luaControls + = MWBase::Environment::get().getLuaManager()->getActorControls(player); + if (luaControls && player.getClass().getMovementSettings(player).mPosition[2] < 1) + luaControls->mJump = false; } - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for (const Actor& actor : mActors) { - const MWWorld::Class &cls = iter->first.getClass(); - CreatureStats &stats = cls.getCreatureStats(iter->first); + const MWWorld::Class& cls = actor.getPtr().getClass(); + CreatureStats& stats = cls.getCreatureStats(actor.getPtr()); - //KnockedOutOneFrameLogic - //Used for "OnKnockedOut" command - //Put here to ensure that it's run for PRECISELY one frame. + // KnockedOutOneFrameLogic + // Used for "OnKnockedOut" command + // Put here to ensure that it's run for PRECISELY one frame. if (stats.getKnockedDown() && !stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) - { //Start it for one frame if nessesary + { // Start it for one frame if necessary stats.setKnockedDownOneFrame(true); } else if (stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) - { //Turn off KnockedOutOneframe + { // Turn off KnockedOutOneframe stats.setKnockedDownOneFrame(false); stats.setKnockedDownOverOneFrame(true); } @@ -2178,115 +1715,102 @@ namespace MWMechanics killDeadActors(); updateSneaking(playerCharacter, duration); } - - updateCombatMusic(); } - void Actors::notifyDied(const MWWorld::Ptr &actor) + void Actors::notifyDied(const MWWorld::Ptr& actor) { actor.getClass().getCreatureStats(actor).notifyDied(); - ++mDeathCount[Misc::StringUtils::lowerCase(actor.getCellRef().getRefId())]; + ++mDeathCount[actor.getCellRef().getRefId()]; + + MWBase::Environment::get().getLuaManager()->actorDied(actor); } - void Actors::resurrect(const MWWorld::Ptr &ptr) + void Actors::resurrect(const MWWorld::Ptr& ptr) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) { - if(iter->second->getCharacterController()->isDead()) + if (iter->second->getCharacterController().isDead()) { // Actor has been resurrected. Notify the CharacterController and re-enable collision. - MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, true); - iter->second->getCharacterController()->resurrect(); + MWBase::Environment::get().getWorld()->enableActorCollision(iter->second->getPtr(), true); + iter->second->getCharacterController().resurrect(); } } } void Actors::killDeadActors() { - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for (Actor& actor : mActors) { - const MWWorld::Class &cls = iter->first.getClass(); - CreatureStats &stats = cls.getCreatureStats(iter->first); + const MWWorld::Class& cls = actor.getPtr().getClass(); + CreatureStats& stats = cls.getCreatureStats(actor.getPtr()); - if(!stats.isDead()) + if (!stats.isDead()) continue; - MWBase::Environment::get().getWorld()->removeActorPath(iter->first); - CharacterController::KillResult killResult = iter->second->getCharacterController()->kill(); + MWBase::Environment::get().getWorld()->removeActorPath(actor.getPtr()); + CharacterController::KillResult killResult = actor.getCharacterController().kill(); if (killResult == CharacterController::Result_DeathAnimStarted) { // Play dying words // Note: It's not known whether the soundgen tags scream, roar, and moan are reliable // for NPCs since some of the npc death animation files are missing them. - MWBase::Environment::get().getDialogueManager()->say(iter->first, "hit"); + MWBase::Environment::get().getDialogueManager()->say(actor.getPtr(), ESM::RefId::stringRefId("hit")); // Apply soultrap - if (iter->first.getTypeName() == typeid(ESM::Creature).name()) - { - SoulTrap soulTrap (iter->first); - stats.getActiveSpells().visitEffectSources(soulTrap); - } + if (actor.getPtr().getType() == ESM::Creature::sRecordId) + soulTrap(actor.getPtr()); - // Magic effects will be reset later, and the magic effect that could kill the actor - // needs to be determined now - calculateCreatureStatModifiers(iter->first, 0); - - if (cls.isEssential(iter->first)) + if (cls.isEssential(actor.getPtr())) MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}"); } else if (killResult == CharacterController::Result_DeathAnimJustFinished) { - bool isPlayer = iter->first == getPlayer(); - notifyDied(iter->first); + const bool isPlayer = actor.getPtr() == getPlayer(); + notifyDied(actor.getPtr()); // Reset magic effects and recalculate derived effects // One case where we need this is to make sure bound items are removed upon death - stats.modifyMagicEffects(MWMechanics::MagicEffects()); - stats.getActiveSpells().clear(); + const float vampirism + = stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Vampirism).getMagnitude(); + stats.getActiveSpells().clear(actor.getPtr()); // Make sure spell effects are removed purgeSpellEffects(stats.getActorId()); - // Reset dynamic stats, attributes and skills - calculateCreatureStatModifiers(iter->first, 0); - if (iter->first.getClass().isNpc()) - calculateNpcStatModifiers(iter->first, 0); + stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism); if (isPlayer) { - //player's death animation is over + // player's death animation is over MWBase::Environment::get().getStateManager()->askLoadRecent(); } else { // NPC death animation is over, disable actor collision - MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, false); + MWBase::Environment::get().getWorld()->enableActorCollision(actor.getPtr(), false); } - - // Play Death Music if it was the player dying - if(iter->first == getPlayer()) - MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); } } } - void Actors::cleanupSummonedCreature (MWMechanics::CreatureStats& casterStats, int creatureActorId) + void Actors::cleanupSummonedCreature(MWMechanics::CreatureStats& casterStats, int creatureActorId) const { - MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(creatureActorId); + const MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(creatureActorId); if (!ptr.isEmpty()) { MWBase::Environment::get().getWorld()->deleteObject(ptr); - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_End"); + const ESM::Static* fx = MWBase::Environment::get().getESMStore()->get().search( + ESM::RefId::stringRefId("VFX_Summon_End")); if (fx) - MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, - "", ptr.getRefData().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->spawnEffect( + Misc::ResourceHelpers::correctMeshPath(fx->mModel), "", ptr.getRefData().getPosition().asVec3()); // Remove the summoned creature's summoned creatures as well MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - std::map& creatureMap = stats.getSummonedCreatureMap(); + auto& creatureMap = stats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) cleanupSummonedCreature(stats, creature.second); creatureMap.clear(); @@ -2302,55 +1826,53 @@ namespace MWMechanics purgeSpellEffects(creatureActorId); } - void Actors::purgeSpellEffects(int casterActorId) + void Actors::purgeSpellEffects(int casterActorId) const { - for (PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) + for (const Actor& actor : mActors) { - MWMechanics::ActiveSpells& spells = iter->first.getClass().getCreatureStats(iter->first).getActiveSpells(); - spells.purge(casterActorId); + MWMechanics::ActiveSpells& spells + = actor.getPtr().getClass().getCreatureStats(actor.getPtr()).getActiveSpells(); + spells.purge(actor.getPtr(), casterActorId); } } - void Actors::rest(double hours, bool sleep) + void Actors::rest(double hours, bool sleep) const { float duration = hours * 3600.f; - float timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); + const float timeScale = MWBase::Environment::get().getWorld()->getTimeManager()->getGameTimeScale(); if (timeScale != 0.f) duration /= timeScale; const MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); + const int actorsProcessingRange = Settings::game().mActorsProcessingRange; - for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for (const Actor& actor : mActors) { - if (iter->first.getClass().getCreatureStats(iter->first).isDead()) + if (actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDead()) { - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); + adjustMagicEffects(actor.getPtr(), duration); continue; } - if (!sleep || iter->first == player) - restoreDynamicStats(iter->first, hours, sleep); + if (!sleep || actor.getPtr() == player) + restoreDynamicStats(actor.getPtr(), hours, sleep); - if ((!iter->first.getRefData().getBaseNode()) || - (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() > mActorsProcessingRange*mActorsProcessingRange) + if ((!actor.getPtr().getRefData().getBaseNode()) + || (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length2() + > actorsProcessingRange * actorsProcessingRange) continue; - adjustMagicEffects (iter->first); - if (iter->first.getClass().getCreatureStats(iter->first).needToRecalcDynamicStats()) - calculateDynamicStats (iter->first); + // Get rid of effects pending removal so they are not applied when resting + updateMagicEffects(actor.getPtr()); - calculateCreatureStatModifiers (iter->first, duration); - if (iter->first.getClass().isNpc()) - calculateNpcStatModifiers(iter->first, duration); + adjustMagicEffects(actor.getPtr(), duration); - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); - - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(actor.getPtr()); if (animation) { animation->removeEffects(); - MWBase::Environment::get().getWorld()->applyLoopingParticles(iter->first); + MWBase::Environment::get().getWorld()->applyLoopingParticles(actor.getPtr()); } } @@ -2359,15 +1881,13 @@ namespace MWMechanics void Actors::updateSneaking(CharacterController* ctrl, float duration) { - static float sneakTimer = 0.f; // Times update of sneak icon - if (!ctrl) { MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); return; } - MWWorld::Ptr player = getPlayer(); + const MWWorld::Ptr player = getPlayer(); if (!MWBase::Environment::get().getMechanicsManager()->isSneaking(player)) { @@ -2375,32 +1895,36 @@ namespace MWMechanics return; } - static float sneakSkillTimer = 0.f; // Times sneak skill progress from "avoid notice" - - MWBase::World* world = MWBase::Environment::get().getWorld(); + MWBase::World* const world = MWBase::Environment::get().getWorld(); const MWWorld::Store& gmst = world->getStore().get(); static const float fSneakUseDist = gmst.find("fSneakUseDist")->mValue.getFloat(); static const float fSneakUseDelay = gmst.find("fSneakUseDelay")->mValue.getFloat(); - if (sneakTimer >= fSneakUseDelay) - sneakTimer = 0.f; + if (mSneakTimer >= fSneakUseDelay) + mSneakTimer = 0.f; - if (sneakTimer == 0.f) + if (mSneakTimer == 0.f) { // Set when an NPC is within line of sight and distance, but is still unaware. Used for skill progress. bool avoidedNotice = false; bool detected = false; std::vector observers; - osg::Vec3f position(player.getRefData().getPosition().asVec3()); - float radius = std::min(fSneakUseDist, mActorsProcessingRange); + const osg::Vec3f position(player.getRefData().getPosition().asVec3()); + const float radius = std::min(fSneakUseDist, Settings::game().mActorsProcessingRange); getObjectsInRange(position, radius, observers); - for (const MWWorld::Ptr &observer : observers) + std::set sidingActors; + getActorsSidingWith(player, sidingActors); + + for (const MWWorld::Ptr& observer : observers) { if (observer == player || observer.getClass().getCreatureStats(observer).isDead()) continue; + if (sidingActors.find(observer) != sidingActors.cend()) + continue; + if (world->getLOS(player, observer)) { if (MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, observer)) @@ -2417,297 +1941,332 @@ namespace MWMechanics } } - if (sneakSkillTimer >= fSneakUseDelay) - sneakSkillTimer = 0.f; + if (mSneakSkillTimer >= fSneakUseDelay) + mSneakSkillTimer = 0.f; - if (avoidedNotice && sneakSkillTimer == 0.f) - player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 0); + if (avoidedNotice && mSneakSkillTimer == 0.f) + player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, ESM::Skill::Sneak_AvoidNotice); if (!detected) MWBase::Environment::get().getWindowManager()->setSneakVisibility(true); } - sneakTimer += duration; - sneakSkillTimer += duration; + mSneakTimer += duration; + mSneakSkillTimer += duration; } - int Actors::getHoursToRest(const MWWorld::Ptr &ptr) const + int Actors::getHoursToRest(const MWWorld::Ptr& ptr) const { - float healthPerHour, magickaPerHour; - getRestorationPerHourOfSleep(ptr, healthPerHour, magickaPerHour); + const auto [healthPerHour, magickaPerHour] = getRestorationPerHourOfSleep(ptr); CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; + const bool stunted = stats.getMagicEffects().getOrDefault(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; - float healthHours = healthPerHour > 0 - ? (stats.getHealth().getModified() - stats.getHealth().getCurrent()) / healthPerHour - : 1.0f; - float magickaHours = magickaPerHour > 0 && !stunted - ? (stats.getMagicka().getModified() - stats.getMagicka().getCurrent()) / magickaPerHour - : 1.0f; + const float healthHours = healthPerHour > 0 + ? (stats.getHealth().getModified() - stats.getHealth().getCurrent()) / healthPerHour + : 1.0f; + const float magickaHours = magickaPerHour > 0 && !stunted + ? (stats.getMagicka().getModified() - stats.getMagicka().getCurrent()) / magickaPerHour + : 1.0f; - int autoHours = static_cast(std::ceil(std::max(1.f, std::max(healthHours, magickaHours)))); - return autoHours; + return static_cast(std::ceil(std::max(1.f, std::max(healthHours, magickaHours)))); } - int Actors::countDeaths (const std::string& id) const + int Actors::countDeaths(const ESM::RefId& id) const { - std::map::const_iterator iter = mDeathCount.find(id); - if(iter != mDeathCount.end()) + const auto iter = mDeathCount.find(id); + if (iter != mDeathCount.end()) return iter->second; return 0; } - void Actors::forceStateUpdate(const MWWorld::Ptr & ptr) + void Actors::forceStateUpdate(const MWWorld::Ptr& ptr) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) - iter->second->getCharacterController()->forceStateUpdate(); + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().forceStateUpdate(); } - bool Actors::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist) + bool Actors::playAnimationGroup( + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, bool scripted) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) { - return iter->second->getCharacterController()->playGroup(groupName, mode, number, persist); + return iter->second->getCharacterController().playGroup(groupName, mode, number, scripted); } else { - Log(Debug::Warning) << "Warning: Actors::playAnimationGroup: Unable to find " << ptr.getCellRef().getRefId(); + Log(Debug::Warning) << "Warning: Actors::playAnimationGroup: Unable to find " + << ptr.getCellRef().getRefId(); return false; } } - void Actors::skipAnimation(const MWWorld::Ptr& ptr) + + bool Actors::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->getCharacterController().playGroupLua( + groupName, speed, startKey, stopKey, loops, forceLoop); + return false; + } + + void Actors::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().enableLuaAnimations(enable); + } + + void Actors::skipAnimation(const MWWorld::Ptr& ptr) const + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().skipAnim(); + } + + bool Actors::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) - iter->second->getCharacterController()->skipAnim(); + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->getCharacterController().isAnimPlaying(groupName); + return false; } - bool Actors::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) + bool Actors::checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const { - PtrActorMap::iterator iter = mActors.find(ptr); - if(iter != mActors.end()) - return iter->second->getCharacterController()->isAnimPlaying(groupName); + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->getCharacterController().isScriptedAnimPlaying(); return false; } - void Actors::persistAnimationStates() + void Actors::persistAnimationStates() const + { + for (const Actor& actor : mActors) + actor.getCharacterController().persistAnimationState(); + } + + void Actors::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) { - for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) - iter->second->getCharacterController()->persistAnimationState(); + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().clearAnimQueue(clearScripted); } - void Actors::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) + void Actors::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const { - for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) + for (const Actor& actor : mActors) { - if ((iter->first.getRefData().getPosition().asVec3() - position).length2() <= radius*radius) - out.push_back(iter->first); + if ((actor.getPtr().getRefData().getPosition().asVec3() - position).length2() <= radius * radius) + out.push_back(actor.getPtr()); } } - bool Actors::isAnyObjectInRange(const osg::Vec3f& position, float radius) + bool Actors::isAnyObjectInRange(const osg::Vec3f& position, float radius) const { - for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) + for (const Actor& actor : mActors) { - if ((iter->first.getRefData().getPosition().asVec3() - position).length2() <= radius*radius) + if ((actor.getPtr().getRefData().getPosition().asVec3() - position).length2() <= radius * radius) return true; } return false; } - std::list Actors::getActorsSidingWith(const MWWorld::Ptr& actor) + std::vector Actors::getActorsSidingWith(const MWWorld::Ptr& actorPtr, bool excludeInfighting) const { - std::list list; - for(PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) + std::vector list; + list.push_back(actorPtr); + for (const Actor& actor : mActors) { - const MWWorld::Ptr &iteratedActor = iter->first; + const MWWorld::Ptr& iteratedActor = actor.getPtr(); if (iteratedActor == getPlayer()) continue; - const bool sameActor = (iteratedActor == actor); + const bool sameActor = (iteratedActor == actorPtr); - const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor); + const CreatureStats& stats = iteratedActor.getClass().getCreatureStats(iteratedActor); if (stats.isDead()) continue; - // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Combat and Wander packages before the Follow/Escort package - // Actors that are targeted by this actor's Follow or Escort packages also side with them + // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are + // only Wander packages before the Follow/Escort package Actors that are targeted by this actor's Follow or + // Escort packages also side with them for (const auto& package : stats.getAiSequence()) { + if (excludeInfighting && !sameActor && package->getTypeId() == AiPackageTypeId::Combat + && package->targetIs(actorPtr)) + break; if (package->sideWithTarget() && !package->getTarget().isEmpty()) { if (sameActor) { + if (excludeInfighting) + { + MWWorld::Ptr ally = package->getTarget(); + std::vector enemies; + if (ally.getClass().getCreatureStats(ally).getAiSequence().getCombatTargets(enemies) + && std::find(enemies.begin(), enemies.end(), actorPtr) != enemies.end()) + break; + enemies.clear(); + if (actorPtr.getClass().getCreatureStats(actorPtr).getAiSequence().getCombatTargets(enemies) + && std::find(enemies.begin(), enemies.end(), ally) != enemies.end()) + break; + } list.push_back(package->getTarget()); } - else if (package->getTarget() == actor) + else if (package->targetIs(actorPtr)) { list.push_back(iteratedActor); } break; } - else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) + else if (package->getTypeId() > AiPackageTypeId::Wander + && package->getTypeId() <= AiPackageTypeId::Activate) // Don't count "fake" package types break; } } return list; } - std::list Actors::getActorsFollowing(const MWWorld::Ptr& actor) + std::vector Actors::getActorsFollowing(const MWWorld::Ptr& actorPtr) const { - std::list list; - forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) - { - if (package->followTargetThroughDoors() && package->getTarget() == actor) - list.push_back(iter.first); - else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) - return false; - return true; - }); + std::vector list; + forEachFollowingPackage( + mActors, actorPtr, getPlayer(), [&](const Actor& actor, const std::shared_ptr& package) { + if (package->followTargetThroughDoors() && package->targetIs(actorPtr)) + list.push_back(actor.getPtr()); + else if (package->getTypeId() != AiPackageTypeId::Combat + && package->getTypeId() != AiPackageTypeId::Wander) + return false; + return true; + }); return list; } - void Actors::getActorsFollowing(const MWWorld::Ptr &actor, std::set& out) { - std::list followers = getActorsFollowing(actor); - for(const MWWorld::Ptr &follower : followers) + void Actors::getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) const + { + auto followers = getActorsFollowing(actor); + for (const MWWorld::Ptr& follower : followers) if (out.insert(follower).second) getActorsFollowing(follower, out); } - void Actors::getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out) { - std::list followers = getActorsSidingWith(actor); - for(const MWWorld::Ptr &follower : followers) - if (out.insert(follower).second) - getActorsSidingWith(follower, out); - } - - void Actors::getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out, std::map >& cachedAllies) { - // If we have already found actor's allies, use the cache - std::map >::const_iterator search = cachedAllies.find(actor); - if (search != cachedAllies.end()) - out.insert(search->second.begin(), search->second.end()); - else - { - std::list followers = getActorsSidingWith(actor); - for (const MWWorld::Ptr &follower : followers) - if (out.insert(follower).second) - getActorsSidingWith(follower, out, cachedAllies); - - // Cache ptrs and their sets of allies - cachedAllies.insert(std::make_pair(actor, out)); - for (const MWWorld::Ptr &iter : out) - { - search = cachedAllies.find(iter); - if (search == cachedAllies.end()) - cachedAllies.insert(std::make_pair(iter, out)); - } - } + void Actors::getActorsSidingWith( + const MWWorld::Ptr& actor, std::set& out, bool excludeInfighting) const + { + auto followers = getActorsSidingWith(actor, excludeInfighting); + for (const MWWorld::Ptr& follower : followers) + if (out.insert(follower).second && follower != actor) + getActorsSidingWith(follower, out, excludeInfighting); } - std::list Actors::getActorsFollowingIndices(const MWWorld::Ptr &actor) + std::vector Actors::getActorsFollowingIndices(const MWWorld::Ptr& actor) const { - std::list list; - forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) - { - if (package->followTargetThroughDoors() && package->getTarget() == actor) - { - list.push_back(static_cast(package.get())->getFollowIndex()); - return false; - } - else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) - return false; - return true; - }); + std::vector list; + forEachFollowingPackage( + mActors, actor, getPlayer(), [&](const Actor&, const std::shared_ptr& package) { + if (package->followTargetThroughDoors() && package->targetIs(actor)) + { + list.push_back(static_cast(package.get())->getFollowIndex()); + return false; + } + else if (package->getTypeId() != AiPackageTypeId::Combat + && package->getTypeId() != AiPackageTypeId::Wander) + return false; + return true; + }); return list; } - std::map Actors::getActorsFollowingByIndex(const MWWorld::Ptr &actor) + std::map Actors::getActorsFollowingByIndex(const MWWorld::Ptr& actor) const { std::map map; - forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) - { - if (package->followTargetThroughDoors() && package->getTarget() == actor) - { - int index = static_cast(package.get())->getFollowIndex(); - map[index] = iter.first; - return false; - } - else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) - return false; - return true; - }); + forEachFollowingPackage( + mActors, actor, getPlayer(), [&](const Actor& otherActor, const std::shared_ptr& package) { + if (package->followTargetThroughDoors() && package->targetIs(actor)) + { + const int index = static_cast(package.get())->getFollowIndex(); + map[index] = otherActor.getPtr(); + return false; + } + else if (package->getTypeId() != AiPackageTypeId::Combat + && package->getTypeId() != AiPackageTypeId::Wander) + return false; + return true; + }); return map; } - std::list Actors::getActorsFighting(const MWWorld::Ptr& actor) { - std::list list; + std::vector Actors::getActorsFighting(const MWWorld::Ptr& actor) const + { + std::vector list; std::vector neighbors; - osg::Vec3f position (actor.getRefData().getPosition().asVec3()); - getObjectsInRange(position, mActorsProcessingRange, neighbors); - for(const MWWorld::Ptr& neighbor : neighbors) + const osg::Vec3f position(actor.getRefData().getPosition().asVec3()); + getObjectsInRange(position, Settings::game().mActorsProcessingRange, neighbors); + for (const MWWorld::Ptr& neighbor : neighbors) { if (neighbor == actor) continue; - const CreatureStats &stats = neighbor.getClass().getCreatureStats(neighbor); + const CreatureStats& stats = neighbor.getClass().getCreatureStats(neighbor); if (stats.isDead()) continue; if (stats.getAiSequence().isInCombat(actor)) - list.push_front(neighbor); + list.push_back(neighbor); } return list; } - std::list Actors::getEnemiesNearby(const MWWorld::Ptr& actor) + std::vector Actors::getEnemiesNearby(const MWWorld::Ptr& actor) const { - std::list list; + std::vector list; std::vector neighbors; - osg::Vec3f position (actor.getRefData().getPosition().asVec3()); - getObjectsInRange(position, mActorsProcessingRange, neighbors); + osg::Vec3f position(actor.getRefData().getPosition().asVec3()); + getObjectsInRange(position, Settings::game().mActorsProcessingRange, neighbors); std::set followers; getActorsFollowing(actor, followers); - for (auto neighbor = neighbors.begin(); neighbor != neighbors.end(); ++neighbor) + for (const MWWorld::Ptr& neighbor : neighbors) { - const CreatureStats &stats = neighbor->getClass().getCreatureStats(*neighbor); - if (stats.isDead() || *neighbor == actor || neighbor->getClass().isPureWaterCreature(*neighbor)) + const CreatureStats& stats = neighbor.getClass().getCreatureStats(neighbor); + if (stats.isDead() || neighbor == actor || neighbor.getClass().isPureWaterCreature(neighbor)) continue; - const bool isFollower = followers.find(*neighbor) != followers.end(); + const bool isFollower = followers.find(neighbor) != followers.end(); - if (stats.getAiSequence().isInCombat(actor) || (MWBase::Environment::get().getMechanicsManager()->isAggressive(*neighbor, actor) && !isFollower)) - list.push_back(*neighbor); + if (stats.getAiSequence().isInCombat(actor) + || (MWBase::Environment::get().getMechanicsManager()->isAggressive(neighbor, actor) && !isFollower)) + list.push_back(neighbor); } return list; } - - void Actors::write (ESM::ESMWriter& writer, Loading::Listener& listener) const + void Actors::write(ESM::ESMWriter& writer, Loading::Listener& listener) const { writer.startRecord(ESM::REC_DCOU); - for (std::map::const_iterator it = mDeathCount.begin(); it != mDeathCount.end(); ++it) + for (const auto& [id, count] : mDeathCount) { - writer.writeHNString("ID__", it->first); - writer.writeHNT ("COUN", it->second); + writer.writeHNRefId("ID__", id); + writer.writeHNT("COUN", count); } writer.endRecord(ESM::REC_DCOU); } - void Actors::readRecord (ESM::ESMReader& reader, uint32_t type) + void Actors::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_DCOU) { while (reader.isNextSub("ID__")) { - std::string id = reader.getHString(); + ESM::RefId id = reader.getRefId(); int count; reader.getHNT(count, "COUN"); - if (MWBase::Environment::get().getWorld()->getStore().find(id)) + if (MWBase::Environment::get().getESMStore()->find(id)) mDeathCount[id] = count; } } @@ -2715,56 +2274,47 @@ namespace MWMechanics void Actors::clear() { - PtrActorMap::iterator it(mActors.begin()); - for (; it != mActors.end(); ++it) - { - delete it->second; - it->second = nullptr; - } + mIndex.clear(); mActors.clear(); mDeathCount.clear(); } - void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) + void Actors::updateMagicEffects(const MWWorld::Ptr& ptr) const { - adjustMagicEffects(ptr); - calculateCreatureStatModifiers(ptr, 0.f); - if (ptr.getClass().isNpc()) - calculateNpcStatModifiers(ptr, 0.f); + adjustMagicEffects(ptr, 0.f); } - bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const + bool Actors::isReadyToBlock(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - return it->second->getCharacterController()->isReadyToBlock(); + return it->second->getCharacterController().isReadyToBlock(); } - bool Actors::isCastingSpell(const MWWorld::Ptr &ptr) const + bool Actors::isCastingSpell(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - return it->second->getCharacterController()->isCastingSpell(); + return it->second->getCharacterController().isCastingSpell(); } bool Actors::isAttackingOrSpell(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; - CharacterController* ctrl = it->second->getCharacterController(); - return ctrl->isAttackingOrSpell(); + return it->second->getCharacterController().isAttackingOrSpell(); } int Actors::getGreetingTimer(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return 0; return it->second->getGreetingTimer(); @@ -2772,8 +2322,8 @@ namespace MWMechanics float Actors::getAngleToPlayer(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return 0.f; return it->second->getAngleToPlayer(); @@ -2781,8 +2331,8 @@ namespace MWMechanics GreetingState Actors::getGreetingState(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return Greet_None; return it->second->getGreetingState(); @@ -2790,29 +2340,54 @@ namespace MWMechanics bool Actors::isTurningToPlayer(const MWWorld::Ptr& ptr) const { - PtrActorMap::const_iterator it = mActors.find(ptr); - if (it == mActors.end()) + const auto it = mIndex.find(ptr.mRef); + if (it == mIndex.end()) return false; return it->second->isTurningToPlayer(); } - void Actors::fastForwardAi() + void Actors::fastForwardAi() const { if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) return; - // making a copy since fast-forward could move actor to a different cell and invalidate the mActors iterator - PtrActorMap map = mActors; - for (PtrActorMap::iterator it = map.begin(); it != map.end(); ++it) + for (auto it = mActors.begin(); it != mActors.end();) { - MWWorld::Ptr ptr = it->first; - if (ptr == getPlayer() - || !isConscious(ptr) - || ptr.getClass().getCreatureStats(ptr).isParalyzed()) + const MWWorld::Ptr ptr = it->getPtr(); + ++it; + if (ptr == getPlayer() || !isConscious(ptr) || ptr.getClass().getCreatureStats(ptr).isParalyzed()) continue; MWMechanics::AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence(); seq.fastForward(ptr); } } + + const std::set& SidingCache::getActorsSidingWith(const MWWorld::Ptr& actor) + { + // If we have already found actor's allies, use the cache + auto search = mCache.find(actor); + if (search != mCache.end()) + return search->second; + std::set& out = mCache[actor]; + for (const MWWorld::Ptr& follower : mActors.getActorsSidingWith(actor, mExcludeInfighting)) + { + if (out.insert(follower).second && follower != actor) + { + const auto& allies = getActorsSidingWith(follower); + out.insert(allies.begin(), allies.end()); + } + } + + // Cache ptrs and their sets of allies + for (const MWWorld::Ptr& iter : out) + { + if (iter == actor) + continue; + search = mCache.find(iter); + if (search == mCache.end()) + mCache.emplace(iter, out); + } + return out; + } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 0ae9687578d..b575ec28276 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -1,13 +1,13 @@ #ifndef GAME_MWMECHANICS_ACTORS_H #define GAME_MWMECHANICS_ACTORS_H -#include -#include -#include #include #include +#include +#include +#include -#include "../mwmechanics/actorutil.hpp" +#include "actor.hpp" namespace ESM { @@ -36,185 +36,186 @@ namespace MWMechanics class Actor; class CharacterController; class CreatureStats; + class SidingCache; class Actors { - std::map mDeathCount; - - void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); - void removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); - - void adjustMagicEffects (const MWWorld::Ptr& creature); - - void calculateDynamicStats (const MWWorld::Ptr& ptr); - - void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration); - - void calculateRestoration (const MWWorld::Ptr& ptr, float duration); + public: + std::list::const_iterator begin() const { return mActors.begin(); } + std::list::const_iterator end() const { return mActors.end(); } + std::size_t size() const { return mActors.size(); } - void updateDrowning (const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer); + void notifyDied(const MWWorld::Ptr& actor); - void updateEquippedLight (const MWWorld::Ptr& ptr, float duration, bool mayEquip); + /// Check if the target actor was detected by an observer + /// If the observer is a non-NPC, check all actors in AI processing distance as observers + bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) const; - void updateCrimePursuit (const MWWorld::Ptr& ptr, float duration); + /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently + /// paused we may want to do it manually (after equipping permanent enchantment) + void updateMagicEffects(const MWWorld::Ptr& ptr) const; - void killDeadActors (); + void addActor(const MWWorld::Ptr& ptr, bool updateImmediately = false); + ///< Register an actor for stats management + /// + /// \note Dead actors are ignored. - void purgeSpellEffects (int casterActorId); + void removeActor(const MWWorld::Ptr& ptr, bool keepActive); + ///< Deregister an actor for stats management + /// + /// \note Ignored, if \a ptr is not a registered actor. - void predictAndAvoidCollisions(float duration); + void resurrect(const MWWorld::Ptr& ptr) const; - public: + void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell = false) const; - Actors(); - ~Actors(); + void updateActor(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) const; + ///< Updates an actor with a new Ptr - typedef std::map PtrActorMap; + void dropActors(const MWWorld::CellStore* cellStore, const MWWorld::Ptr& ignore); + ///< Deregister all actors (except for \a ignore) in the given cell. - PtrActorMap::const_iterator begin() { return mActors.begin(); } - PtrActorMap::const_iterator end() { return mActors.end(); } - std::size_t size() const { return mActors.size(); } + void update(float duration, bool paused); + ///< Update actor stats and store desired velocity vectors in \a movement - void notifyDied(const MWWorld::Ptr &actor); + void updateActor(const MWWorld::Ptr& ptr, float duration) const; + ///< This function is normally called automatically during the update process, but it can + /// also be called explicitly at any time to force an update. - /// Check if the target actor was detected by an observer - /// If the observer is a non-NPC, check all actors in AI processing distance as observers - bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer); + /// Removes an actor from combat and makes all of their allies stop fighting the actor's targets + void stopCombat(const MWWorld::Ptr& ptr) const; - /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently - /// paused we may want to do it manually (after equipping permanent enchantment) - void updateMagicEffects (const MWWorld::Ptr& ptr); + void playIdleDialogue(const MWWorld::Ptr& actor) const; + void updateMovementSpeed(const MWWorld::Ptr& actor) const; + void updateGreetingState(const MWWorld::Ptr& actor, Actor& actorState, bool turnOnly); + void turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir) const; - void updateProcessingRange(); - float getProcessingRange() const; + void rest(double hours, bool sleep) const; + ///< Update actors while the player is waiting or sleeping. - void addActor (const MWWorld::Ptr& ptr, bool updateImmediately=false); - ///< Register an actor for stats management - /// - /// \note Dead actors are ignored. + void updateSneaking(CharacterController* ctrl, float duration); + ///< Update the sneaking indicator state according to the given player character controller. - void removeActor (const MWWorld::Ptr& ptr); - ///< Deregister an actor for stats management - /// - /// \note Ignored, if \a ptr is not a registered actor. + void restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep) const; - void resurrect (const MWWorld::Ptr& ptr); + int getHoursToRest(const MWWorld::Ptr& ptr) const; + ///< Calculate how many hours the given actor needs to rest in order to be fully healed - void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell=false); + void fastForwardAi() const; + ///< Simulate the passing of time - void updateActor(const MWWorld::Ptr &old, const MWWorld::Ptr& ptr); - ///< Updates an actor with a new Ptr + int countDeaths(const ESM::RefId& id) const; + ///< Return the number of deaths for actors with the given ID. - void dropActors (const MWWorld::CellStore *cellStore, const MWWorld::Ptr& ignore); - ///< Deregister all actors (except for \a ignore) in the given cell. + bool isAttackPreparing(const MWWorld::Ptr& ptr) const; + bool isRunning(const MWWorld::Ptr& ptr) const; + bool isSneaking(const MWWorld::Ptr& ptr) const; - void updateCombatMusic(); - ///< Update combat music state + void forceStateUpdate(const MWWorld::Ptr& ptr) const; - void update (float duration, bool paused); - ///< Update actor stats and store desired velocity vectors in \a movement + bool playAnimationGroup(const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, + bool scripted = false) const; + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop); + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable); + void skipAnimation(const MWWorld::Ptr& ptr) const; + bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const; + bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const; + void persistAnimationStates() const; + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted); - void updateActor (const MWWorld::Ptr& ptr, float duration); - ///< This function is normally called automatically during the update process, but it can - /// also be called explicitly at any time to force an update. + void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const; - /** Start combat between two actors - @Notes: If againstPlayer = true then actor2 should be the Player. - If one of the combatants is creature it should be actor1. - */ - void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, std::map >& cachedAllies, bool againstPlayer); + bool isAnyObjectInRange(const osg::Vec3f& position, float radius) const; - void playIdleDialogue(const MWWorld::Ptr& actor); - void updateMovementSpeed(const MWWorld::Ptr& actor); - void updateGreetingState(const MWWorld::Ptr& actor, Actor& actorState, bool turnOnly); - void turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir); + void cleanupSummonedCreature(CreatureStats& casterStats, int creatureActorId) const; - void updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, - MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance, - bool inCombatOrPursue); + /// Returns the list of actors which are siding with the given actor in fights + /**ie AiFollow or AiEscort is active and the target is the actor **/ + std::vector getActorsSidingWith(const MWWorld::Ptr& actor, bool excludeInfighting = false) const; + std::vector getActorsFollowing(const MWWorld::Ptr& actor) const; - void rest(double hours, bool sleep); - ///< Update actors while the player is waiting or sleeping. + /// Recursive version of getActorsFollowing + void getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) const; + /// Recursive version of getActorsSidingWith + void getActorsSidingWith( + const MWWorld::Ptr& actor, std::set& out, bool excludeInfighting = false) const; - void updateSneaking(CharacterController* ctrl, float duration); - ///< Update the sneaking indicator state according to the given player character controller. + /// Get the list of AiFollow::mFollowIndex for all actors following this target + std::vector getActorsFollowingIndices(const MWWorld::Ptr& actor) const; + std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) const; - void restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep); + /// Returns the list of actors which are fighting the given actor + /**ie AiCombat is active and the target is the actor **/ + std::vector getActorsFighting(const MWWorld::Ptr& actor) const; - int getHoursToRest(const MWWorld::Ptr& ptr) const; - ///< Calculate how many hours the given actor needs to rest in order to be fully healed + /// Unlike getActorsFighting, also returns actors that *would* fight the given actor if they saw him. + std::vector getEnemiesNearby(const MWWorld::Ptr& actor) const; - void fastForwardAi(); - ///< Simulate the passing of time + void write(ESM::ESMWriter& writer, Loading::Listener& listener) const; - int countDeaths (const std::string& id) const; - ///< Return the number of deaths for actors with the given ID. + void readRecord(ESM::ESMReader& reader, uint32_t type); - bool isAttackPreparing(const MWWorld::Ptr& ptr); - bool isRunning(const MWWorld::Ptr& ptr); - bool isSneaking(const MWWorld::Ptr& ptr); + void clear(); // Clear death counter - void forceStateUpdate(const MWWorld::Ptr &ptr); + bool isCastingSpell(const MWWorld::Ptr& ptr) const; + bool isReadyToBlock(const MWWorld::Ptr& ptr) const; + bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const; - bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false); - void skipAnimation(const MWWorld::Ptr& ptr); - bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName); - void persistAnimationStates(); + int getGreetingTimer(const MWWorld::Ptr& ptr) const; + float getAngleToPlayer(const MWWorld::Ptr& ptr) const; + GreetingState getGreetingState(const MWWorld::Ptr& ptr) const; + bool isTurningToPlayer(const MWWorld::Ptr& ptr) const; - void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out); - - bool isAnyObjectInRange(const osg::Vec3f& position, float radius); - - void cleanupSummonedCreature (CreatureStats& casterStats, int creatureActorId); - - ///Returns the list of actors which are siding with the given actor in fights - /**ie AiFollow or AiEscort is active and the target is the actor **/ - std::list getActorsSidingWith(const MWWorld::Ptr& actor); - std::list getActorsFollowing(const MWWorld::Ptr& actor); - - /// Recursive version of getActorsFollowing - void getActorsFollowing(const MWWorld::Ptr &actor, std::set& out); - /// Recursive version of getActorsSidingWith - void getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out); - /// Recursive version of getActorsSidingWith that takes, adds to and returns a cache of actors mapped to their allies - void getActorsSidingWith(const MWWorld::Ptr &actor, std::set& out, std::map >& cachedAllies); - - /// Get the list of AiFollow::mFollowIndex for all actors following this target - std::list getActorsFollowingIndices(const MWWorld::Ptr& actor); - std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor); - - ///Returns the list of actors which are fighting the given actor - /**ie AiCombat is active and the target is the actor **/ - std::list getActorsFighting(const MWWorld::Ptr& actor); + private: + std::map mDeathCount; + std::list mActors; + std::map::iterator> mIndex; + // We should add a delay between summoned creature death and its corpse despawning + float mTimerDisposeSummonsCorpses = 0.2f; + float mTimerUpdateHeadTrack = 0; + float mTimerUpdateEquippedLight = 0; + float mTimerUpdateHello = 0; + float mSneakTimer = 0; // Times update of sneak icon + float mSneakSkillTimer = 0; // Times sneak skill progress from "avoid notice" - /// Unlike getActorsFighting, also returns actors that *would* fight the given actor if they saw him. - std::list getEnemiesNearby(const MWWorld::Ptr& actor); + void updateVisibility(const MWWorld::Ptr& ptr, CharacterController& ctrl) const; - void write (ESM::ESMWriter& writer, Loading::Listener& listener) const; + void adjustMagicEffects(const MWWorld::Ptr& creature, float duration) const; - void readRecord (ESM::ESMReader& reader, uint32_t type); + void calculateRestoration(const MWWorld::Ptr& ptr, float duration) const; - void clear(); // Clear death counter + void updateCrimePursuit(const MWWorld::Ptr& ptr, float duration, SidingCache& cachedAllies) const; - bool isCastingSpell(const MWWorld::Ptr& ptr) const; - bool isReadyToBlock(const MWWorld::Ptr& ptr) const; - bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const; + void killDeadActors(); - int getGreetingTimer(const MWWorld::Ptr& ptr) const; - float getAngleToPlayer(const MWWorld::Ptr& ptr) const; - GreetingState getGreetingState(const MWWorld::Ptr& ptr) const; - bool isTurningToPlayer(const MWWorld::Ptr& ptr) const; + void purgeSpellEffects(int casterActorId) const; - private: - void updateVisibility (const MWWorld::Ptr& ptr, CharacterController* ctrl); - void applyCureEffects (const MWWorld::Ptr& actor); + void predictAndAvoidCollisions(float duration) const; - PtrActorMap mActors; - float mTimerDisposeSummonsCorpses; - float mActorsProcessingRange; + /** Start combat between two actors + @Notes: If againstPlayer = true then actor2 should be the Player. + If one of the combatants is creature it should be actor1. + */ + void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, SidingCache& cachedAllies, + bool againstPlayer) const; + }; - bool mSmoothMovement; + class SidingCache + { + const Actors& mActors; + const bool mExcludeInfighting; + std::map> mCache; + + public: + SidingCache(const Actors& actors, bool excludeInfighting) + : mActors(actors) + , mExcludeInfighting(excludeInfighting) + { + } + + /// Recursive version of getActorsSidingWith that takes, returns a cached set of allies + const std::set& getActorsSidingWith(const MWWorld::Ptr& actor); }; } diff --git a/apps/openmw/mwmechanics/actorutil.cpp b/apps/openmw/mwmechanics/actorutil.cpp index 04cbb8e9f5a..2d2980075e6 100644 --- a/apps/openmw/mwmechanics/actorutil.cpp +++ b/apps/openmw/mwmechanics/actorutil.cpp @@ -1,11 +1,16 @@ #include "actorutil.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/magiceffects.hpp" + +#include + namespace MWMechanics { MWWorld::Ptr getPlayer() @@ -27,6 +32,13 @@ namespace MWMechanics bool hasWaterWalking(const MWWorld::Ptr& actor) { const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects(); - return effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; + return effects.getOrDefault(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; + } + + bool isTargetMagicallyHidden(const MWWorld::Ptr& actor) + { + const MagicEffects& magicEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); + return (magicEffects.getOrDefault(ESM::MagicEffect::Invisibility).getMagnitude() > 0) + || (magicEffects.getOrDefault(ESM::MagicEffect::Chameleon).getMagnitude() >= 75); } } diff --git a/apps/openmw/mwmechanics/actorutil.hpp b/apps/openmw/mwmechanics/actorutil.hpp index a226fc9cb65..829204eefad 100644 --- a/apps/openmw/mwmechanics/actorutil.hpp +++ b/apps/openmw/mwmechanics/actorutil.hpp @@ -1,19 +1,6 @@ #ifndef OPENMW_MWMECHANICS_ACTORUTIL_H #define OPENMW_MWMECHANICS_ACTORUTIL_H -#include - -#include -#include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/esmstore.hpp" - -#include "./creaturestats.hpp" - namespace MWWorld { class Ptr; @@ -21,71 +8,11 @@ namespace MWWorld namespace MWMechanics { - enum GreetingState - { - Greet_None, - Greet_InProgress, - Greet_Done - }; - MWWorld::Ptr getPlayer(); bool isPlayerInCombat(); bool canActorMoveByZAxis(const MWWorld::Ptr& actor); bool hasWaterWalking(const MWWorld::Ptr& actor); - - template - void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) - { - T copy = *MWBase::Environment::get().getWorld()->getStore().get().find(id); - switch(setting) - { - case MWMechanics::CreatureStats::AiSetting::AI_Hello: - copy.mAiData.mHello = value; - break; - case MWMechanics::CreatureStats::AiSetting::AI_Fight: - copy.mAiData.mFight = value; - break; - case MWMechanics::CreatureStats::AiSetting::AI_Flee: - copy.mAiData.mFlee = value; - break; - case MWMechanics::CreatureStats::AiSetting::AI_Alarm: - copy.mAiData.mAlarm = value; - break; - default: - assert(0); - } - MWBase::Environment::get().getWorld()->createOverrideRecord(copy); - } - - template - void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) - { - T copy = *MWBase::Environment::get().getWorld()->getStore().get().find(actorId); - for(auto& it : copy.mInventory.mList) - { - if(Misc::StringUtils::ciEqual(it.mItem, itemId)) - { - int sign = it.mCount < 1 ? -1 : 1; - it.mCount = sign * std::max(it.mCount * sign + amount, 0); - MWBase::Environment::get().getWorld()->createOverrideRecord(copy); - return; - } - } - if(amount > 0) - { - ESM::ContItem cont; - cont.mItem = itemId; - cont.mCount = amount; - copy.mInventory.mList.push_back(cont); - MWBase::Environment::get().getWorld()->createOverrideRecord(copy); - } - } - - template void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value); - template void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value); - template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); - template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); - template void modifyBaseInventory(const std::string& containerId, const std::string& itemId, int amount); + bool isTargetMagicallyHidden(const MWWorld::Ptr& actor); } #endif diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp index b4ddf0c0304..be4fe5e6743 100644 --- a/apps/openmw/mwmechanics/aiactivate.cpp +++ b/apps/openmw/mwmechanics/aiactivate.cpp @@ -1,62 +1,69 @@ -#include "aiactivate.hpp" - -#include - -#include "../mwbase/world.hpp" -#include "../mwbase/environment.hpp" - -#include "../mwworld/class.hpp" - -#include "creaturestats.hpp" -#include "movement.hpp" -#include "steering.hpp" - -namespace MWMechanics -{ - AiActivate::AiActivate(const std::string &objectId) - : mObjectId(objectId) - { - } - - bool AiActivate::execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) - { - const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); //The target to follow - - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); - - // Stop if the target doesn't exist - // Really we should be checking whether the target is currently registered with the MechanicsManager - if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) - return true; - - // Turn to target and move to it directly, without pathfinding. - const osg::Vec3f targetDir = target.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3(); - - zTurn(actor, std::atan2(targetDir.x(), targetDir.y()), 0.f); - actor.getClass().getMovementSettings(actor).mPosition[1] = 1; - actor.getClass().getMovementSettings(actor).mPosition[0] = 0; - - if (MWBase::Environment::get().getWorld()->getMaxActivationDistance() >= targetDir.length()) - { - // Note: we intentionally do not cancel package after activation here for backward compatibility with original engine. - MWBase::Environment::get().getWorld()->activate(target, actor); - } - return false; - } - - void AiActivate::writeState(ESM::AiSequence::AiSequence &sequence) const - { - std::unique_ptr activate(new ESM::AiSequence::AiActivate()); - activate->mTargetId = mObjectId; - - ESM::AiSequence::AiPackageContainer package; - package.mType = ESM::AiSequence::Ai_Activate; - package.mPackage = activate.release(); - sequence.mPackages.push_back(package); - } - - AiActivate::AiActivate(const ESM::AiSequence::AiActivate *activate) - : mObjectId(activate->mTargetId) - { - } -} +#include "aiactivate.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/class.hpp" + +#include "creaturestats.hpp" +#include "movement.hpp" +#include "steering.hpp" + +namespace MWMechanics +{ + AiActivate::AiActivate(const ESM::RefId& objectId, bool repeat) + : TypedAiPackage(repeat) + , mObjectId(objectId) + { + } + + bool AiActivate::execute( + const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) + { + const MWWorld::Ptr target + = MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); // The target to follow + + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); + + // Stop if the target doesn't exist + // Really we should be checking whether the target is currently registered with the MechanicsManager + if (target == MWWorld::Ptr() || !target.getCellRef().getCount() || !target.getRefData().isEnabled()) + return true; + + // Turn to target and move to it directly, without pathfinding. + const osg::Vec3f targetDir + = target.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3(); + + zTurn(actor, std::atan2(targetDir.x(), targetDir.y()), 0.f); + actor.getClass().getMovementSettings(actor).mPosition[1] = 1; + actor.getClass().getMovementSettings(actor).mPosition[0] = 0; + + if (MWBase::Environment::get().getWorld()->getMaxActivationDistance() >= targetDir.length()) + { + // Note: we intentionally do not cancel package after activation here for backward compatibility with + // original engine. + MWBase::Environment::get().getLuaManager()->objectActivated(target, actor); + } + return false; + } + + void AiActivate::writeState(ESM::AiSequence::AiSequence& sequence) const + { + auto activate = std::make_unique(); + activate->mTargetId = mObjectId; + activate->mRepeat = getRepeat(); + + ESM::AiSequence::AiPackageContainer package; + package.mType = ESM::AiSequence::Ai_Activate; + package.mPackage = std::move(activate); + sequence.mPackages.push_back(std::move(package)); + } + + AiActivate::AiActivate(const ESM::AiSequence::AiActivate* activate) + : AiActivate(activate->mTargetId, activate->mRepeat) + { + } +} diff --git a/apps/openmw/mwmechanics/aiactivate.hpp b/apps/openmw/mwmechanics/aiactivate.hpp index dc7e0bb26b4..3d0f6d8b8ab 100644 --- a/apps/openmw/mwmechanics/aiactivate.hpp +++ b/apps/openmw/mwmechanics/aiactivate.hpp @@ -1,41 +1,41 @@ -#ifndef GAME_MWMECHANICS_AIACTIVATE_H -#define GAME_MWMECHANICS_AIACTIVATE_H - -#include "typedaipackage.hpp" - -#include - -#include "pathfinding.hpp" - -namespace ESM -{ -namespace AiSequence -{ - struct AiActivate; -} -} - -namespace MWMechanics -{ - /// \brief Causes actor to walk to activatable object and activate it - /** Will activate when close to object **/ - class AiActivate final : public TypedAiPackage - { - public: - /// Constructor - /** \param objectId Reference to object to activate **/ - explicit AiActivate(const std::string &objectId); - - explicit AiActivate(const ESM::AiSequence::AiActivate* activate); - - bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; - - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Activate; } - - void writeState(ESM::AiSequence::AiSequence& sequence) const override; - - private: - const std::string mObjectId; - }; -} -#endif // GAME_MWMECHANICS_AIACTIVATE_H +#ifndef GAME_MWMECHANICS_AIACTIVATE_H +#define GAME_MWMECHANICS_AIACTIVATE_H + +#include "typedaipackage.hpp" +#include +#include +#include + +namespace ESM +{ + namespace AiSequence + { + struct AiActivate; + } +} + +namespace MWMechanics +{ + /// \brief Causes actor to walk to activatable object and activate it + /** Will activate when close to object **/ + class AiActivate final : public TypedAiPackage + { + public: + /// Constructor + /** \param objectId Reference to object to activate **/ + explicit AiActivate(const ESM::RefId& objectId, bool repeat); + + explicit AiActivate(const ESM::AiSequence::AiActivate* activate); + + bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, + float duration) override; + + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Activate; } + + void writeState(ESM::AiSequence::AiSequence& sequence) const override; + + private: + const ESM::RefId mObjectId; + }; +} +#endif // GAME_MWMECHANICS_AIACTIVATE_H diff --git a/apps/openmw/mwmechanics/aiavoiddoor.cpp b/apps/openmw/mwmechanics/aiavoiddoor.cpp index 6a59ae2bfbe..1f229f61bf3 100644 --- a/apps/openmw/mwmechanics/aiavoiddoor.cpp +++ b/apps/openmw/mwmechanics/aiavoiddoor.cpp @@ -2,56 +2,58 @@ #include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" +#include "actorutil.hpp" #include "creaturestats.hpp" #include "movement.hpp" -#include "actorutil.hpp" #include "steering.hpp" static const int MAX_DIRECTIONS = 4; MWMechanics::AiAvoidDoor::AiAvoidDoor(const MWWorld::ConstPtr& doorPtr) -: mDuration(1), mDoorPtr(doorPtr), mDirection(0) + : mDuration(1) + , mDoorPtr(doorPtr) + , mDirection(0) { - } -bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) +bool MWMechanics::AiAvoidDoor::execute( + const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { ESM::Position pos = actor.getRefData().getPosition(); - if(mDuration == 1) //If it just started, get the actor position as the stuck detection thing + if (mDuration == 1) // If it just started, get the actor position as the stuck detection thing mLastPos = pos.asVec3(); - mDuration -= duration; //Update timer + mDuration -= duration; // Update timer if (mDuration < 0) { if (isStuck(pos.asVec3())) { adjustDirection(); - mDuration = 1; //reset timer + mDuration = 1; // reset timer } else return true; // We have tried backing up for more than one second, we've probably cleared it } if (mDoorPtr.getClass().getDoorState(mDoorPtr) == MWWorld::DoorState::Idle) - return true; //Door is no longer opening + return true; // Door is no longer opening - ESM::Position tPos = mDoorPtr.getRefData().getPosition(); //Position of the door + ESM::Position tPos = mDoorPtr.getRefData().getPosition(); // Position of the door float x = pos.pos[1] - tPos.pos[1]; float y = pos.pos[0] - tPos.pos[0]; actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); // Turn away from the door and move when turn completed - if (zTurn(actor, std::atan2(y,x) + getAdjustedAngle(), osg::DegreesToRadians(5.f))) + if (zTurn(actor, std::atan2(y, x) + getAdjustedAngle(), osg::DegreesToRadians(5.f))) actor.getClass().getMovementSettings(actor).mPosition[1] = 1; else actor.getClass().getMovementSettings(actor).mPosition[1] = 0; @@ -59,8 +61,8 @@ bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor, CharacterCont // Make all nearby actors also avoid the door std::vector actors; - MWBase::Environment::get().getMechanicsManager()->getActorsInRange(pos.asVec3(),100,actors); - for(auto& neighbor : actors) + MWBase::Environment::get().getMechanicsManager()->getActorsInRange(pos.asVec3(), 100, actors); + for (auto& neighbor : actors) { if (neighbor == getPlayer()) continue; @@ -80,7 +82,8 @@ bool MWMechanics::AiAvoidDoor::isStuck(const osg::Vec3f& actorPos) const void MWMechanics::AiAvoidDoor::adjustDirection() { - mDirection = Misc::Rng::rollDice(MAX_DIRECTIONS); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + mDirection = Misc::Rng::rollDice(MAX_DIRECTIONS, prng); } float MWMechanics::AiAvoidDoor::getAdjustedAngle() const diff --git a/apps/openmw/mwmechanics/aiavoiddoor.hpp b/apps/openmw/mwmechanics/aiavoiddoor.hpp index 183f429f3f8..8b268ef8163 100644 --- a/apps/openmw/mwmechanics/aiavoiddoor.hpp +++ b/apps/openmw/mwmechanics/aiavoiddoor.hpp @@ -3,46 +3,43 @@ #include "typedaipackage.hpp" -#include "../mwworld/class.hpp" - -#include "pathfinding.hpp" - namespace MWMechanics { /// \brief AiPackage to have an actor avoid an opening door - /** The AI will retreat from the door until it has finished opening, walked far away from it, or one second has passed, in an attempt to avoid it - **/ + /** The AI will retreat from the door until it has finished opening, walked far away from it, or one second has + *passed, in an attempt to avoid it + **/ class AiAvoidDoor final : public TypedAiPackage { - public: - /// Avoid door until the door is fully open - explicit AiAvoidDoor(const MWWorld::ConstPtr& doorPtr); + public: + /// Avoid door until the door is fully open + explicit AiAvoidDoor(const MWWorld::ConstPtr& doorPtr); - bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; + bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, + float duration) override; - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::AvoidDoor; } + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::AvoidDoor; } - static constexpr Options makeDefaultOptions() - { - AiPackage::Options options; - options.mPriority = 2; - options.mCanCancel = false; - options.mShouldCancelPreviousAi = false; - return options; - } + static constexpr Options makeDefaultOptions() + { + AiPackage::Options options; + options.mPriority = 2; + options.mCanCancel = false; + options.mShouldCancelPreviousAi = false; + return options; + } - private: - float mDuration; - const MWWorld::ConstPtr mDoorPtr; - osg::Vec3f mLastPos; - int mDirection; + private: + float mDuration; + const MWWorld::ConstPtr mDoorPtr; + osg::Vec3f mLastPos; + int mDirection; - bool isStuck(const osg::Vec3f& actorPos) const; + bool isStuck(const osg::Vec3f& actorPos) const; - void adjustDirection(); + void adjustDirection(); - float getAdjustedAngle() const; + float getAdjustedAngle() const; }; } #endif - diff --git a/apps/openmw/mwmechanics/aibreathe.cpp b/apps/openmw/mwmechanics/aibreathe.cpp index 94e4ecd9559..b3c3648d6d2 100644 --- a/apps/openmw/mwmechanics/aibreathe.cpp +++ b/apps/openmw/mwmechanics/aibreathe.cpp @@ -1,6 +1,5 @@ #include "aibreathe.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/class.hpp" @@ -11,9 +10,11 @@ #include "movement.hpp" #include "steering.hpp" -bool MWMechanics::AiBreathe::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) +bool MWMechanics::AiBreathe::execute( + const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { - static const float fHoldBreathTime = MWBase::Environment::get().getWorld()->getStore().get().find("fHoldBreathTime")->mValue.getFloat(); + static const float fHoldBreathTime + = MWBase::Environment::get().getESMStore()->get().find("fHoldBreathTime")->mValue.getFloat(); const MWWorld::Class& actorClass = actor.getClass(); if (actorClass.isNpc()) diff --git a/apps/openmw/mwmechanics/aibreathe.hpp b/apps/openmw/mwmechanics/aibreathe.hpp index b84c0eb76e0..6b2bdfed8e8 100644 --- a/apps/openmw/mwmechanics/aibreathe.hpp +++ b/apps/openmw/mwmechanics/aibreathe.hpp @@ -9,19 +9,20 @@ namespace MWMechanics // The AI will go up if lesser than half breath left class AiBreathe final : public TypedAiPackage { - public: - bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; + public: + bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, + float duration) override; - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Breathe; } + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Breathe; } - static constexpr Options makeDefaultOptions() - { - AiPackage::Options options; - options.mPriority = 2; - options.mCanCancel = false; - options.mShouldCancelPreviousAi = false; - return options; - } + static constexpr Options makeDefaultOptions() + { + AiPackage::Options options; + options.mPriority = 2; + options.mCanCancel = false; + options.mShouldCancelPreviousAi = false; + return options; + } }; } #endif diff --git a/apps/openmw/mwmechanics/aicast.cpp b/apps/openmw/mwmechanics/aicast.cpp index 630c04a6a7a..6384d70c06f 100644 --- a/apps/openmw/mwmechanics/aicast.cpp +++ b/apps/openmw/mwmechanics/aicast.cpp @@ -9,14 +9,14 @@ #include "../mwworld/class.hpp" #include "aicombataction.hpp" -#include "creaturestats.hpp" +#include "character.hpp" #include "steering.hpp" namespace MWMechanics { namespace { - float getInitialDistance(const std::string& spellId) + float getInitialDistance(const ESM::RefId& spellId) { ActionSpell action = ActionSpell(spellId); bool isRanged; @@ -25,12 +25,17 @@ namespace MWMechanics } } -MWMechanics::AiCast::AiCast(const std::string& targetId, const std::string& spellId, bool manualSpell) - : mTargetId(targetId), mSpellId(spellId), mCasting(false), mManual(manualSpell), mDistance(getInitialDistance(spellId)) +MWMechanics::AiCast::AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool scriptedSpell) + : mTargetId(targetId) + , mSpellId(spellId) + , mCasting(false) + , mScripted(scriptedSpell) + , mDistance(getInitialDistance(spellId)) { } -bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::CharacterController& characterController, MWMechanics::AiState& state, float duration) +bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::CharacterController& characterController, + MWMechanics::AiState& state, float duration) { MWWorld::Ptr target; if (actor.getCellRef().getRefId() == mTargetId) @@ -41,10 +46,12 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac else { target = getTarget(); - if (!target) + if (target.isEmpty()) return true; - if (!mManual && !pathTo(actor, target.getRefData().getPosition().asVec3(), duration, mDistance)) + if (!mScripted + && !pathTo(actor, target.getRefData().getPosition().asVec3(), duration, + characterController.getSupportedMovementDirections(), mDistance)) { return false; } @@ -78,7 +85,7 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac if (!mCasting) { - MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mManual); + MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mScripted); mCasting = true; return false; } diff --git a/apps/openmw/mwmechanics/aicast.hpp b/apps/openmw/mwmechanics/aicast.hpp index 9758c2b94db..649c5a4d347 100644 --- a/apps/openmw/mwmechanics/aicast.hpp +++ b/apps/openmw/mwmechanics/aicast.hpp @@ -2,6 +2,7 @@ #define GAME_MWMECHANICS_AICAST_H #include "typedaipackage.hpp" +#include namespace MWWorld { @@ -11,31 +12,33 @@ namespace MWWorld namespace MWMechanics { /// AiPackage which makes an actor to cast given spell. - class AiCast final : public TypedAiPackage { - public: - AiCast(const std::string& targetId, const std::string& spellId, bool manualSpell=false); - - bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; - - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Cast; } - - MWWorld::Ptr getTarget() const override; - - static constexpr Options makeDefaultOptions() - { - AiPackage::Options options; - options.mPriority = 3; - options.mCanCancel = false; - options.mShouldCancelPreviousAi = false; - return options; - } - - private: - const std::string mTargetId; - const std::string mSpellId; - bool mCasting; - const bool mManual; - const float mDistance; + class AiCast final : public TypedAiPackage + { + public: + AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool scriptedSpell = false); + + bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, + float duration) override; + + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Cast; } + + MWWorld::Ptr getTarget() const override; + + static constexpr Options makeDefaultOptions() + { + AiPackage::Options options; + options.mPriority = 3; + options.mCanCancel = false; + options.mShouldCancelPreviousAi = false; + return options; + } + + private: + const ESM::RefId mTargetId; + const ESM::RefId mSpellId; + bool mCasting; + const bool mScripted; + const float mDistance; }; } diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index f6123c12c0a..2399961a3ac 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -1,40 +1,43 @@ #include "aicombat.hpp" -#include #include +#include -#include +#include #include +#include #include -#include -#include "../mwphysics/collisiontype.hpp" +#include "../mwphysics/raycasting.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwbase/environment.hpp" #include "../mwbase/dialoguemanager.hpp" +#include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" -#include "pathgrid.hpp" +#include "actorutil.hpp" +#include "aicombataction.hpp" +#include "character.hpp" +#include "combat.hpp" #include "creaturestats.hpp" -#include "steering.hpp" #include "movement.hpp" -#include "character.hpp" -#include "aicombataction.hpp" -#include "actorutil.hpp" +#include "pathgrid.hpp" +#include "steering.hpp" +#include "weapontype.hpp" namespace { - //chooses an attack depending on probability to avoid uniformity - std::string chooseBestAttack(const ESM::Weapon* weapon); + // chooses an attack depending on probability to avoid uniformity + std::string_view chooseBestAttack(const ESM::Weapon* weapon); - osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos, - float duration, int weapType, float strength); + osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, + const osg::Vec3f& vLastTargetPos, float duration, int weapType, float strength); } namespace MWMechanics @@ -44,15 +47,12 @@ namespace MWMechanics mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); } - AiCombat::AiCombat(const ESM::AiSequence::AiCombat *combat) + AiCombat::AiCombat(const ESM::AiSequence::AiCombat* combat) { mTargetActorId = combat->mTargetActorId; } - void AiCombat::init() - { - - } + void AiCombat::init() {} /* * Current AiCombat movement states (as of 0.29.0), ignoring the details of the @@ -101,22 +101,24 @@ namespace MWMechanics * whether the target was hit, etc. */ - bool AiCombat::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) + bool AiCombat::execute( + const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { // get or create temporary storage AiCombatStorage& storage = state.get(); - - //General description + + // General description if (actor.getClass().getCreatureStats(actor).isDead()) return true; MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); if (target.isEmpty()) - return false; + return true; - if(!target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered - // with the MechanicsManager - || target.getClass().getCreatureStats(target).isDead()) + if (!target.getCellRef().getCount() + || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently + // registered with the MechanicsManager + || target.getClass().getCreatureStats(target).isDead()) return true; if (actor == target) // This should never happen. @@ -126,23 +128,30 @@ namespace MWMechanics { if (storage.mCurrentAction.get()) // need to wait to init action with its attack range { - //Update every frame. UpdateLOS uses a timer, so the LOS check does not happen every frame. + // Update every frame. UpdateLOS uses a timer, so the LOS check does not happen every frame. updateLOS(actor, target, duration, storage); - const float targetReachedTolerance = storage.mLOS && !storage.mUseCustomDestination - ? storage.mAttackRange : 0.0f; + const float targetReachedTolerance + = storage.mLOS && !storage.mUseCustomDestination ? storage.mAttackRange : 0.0f; const osg::Vec3f destination = storage.mUseCustomDestination - ? storage.mCustomDestination : target.getRefData().getPosition().asVec3(); - const bool is_target_reached = pathTo(actor, destination, duration, targetReachedTolerance); - if (is_target_reached) storage.mReadyToAttack = true; + ? storage.mCustomDestination + : target.getRefData().getPosition().asVec3(); + const bool is_target_reached = pathTo(actor, destination, duration, + characterController.getSupportedMovementDirections(), targetReachedTolerance); + if (is_target_reached) + storage.mReadyToAttack = true; } storage.updateCombatMove(duration); - if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage); - storage.updateAttack(characterController); + storage.mRotateMove = false; + if (storage.mReadyToAttack) + updateActorsMovement(actor, duration, storage); + if (storage.mRotateMove) + return false; + storage.updateAttack(actor, characterController); } else { - updateFleeing(actor, target, duration, storage); + updateFleeing(actor, target, duration, characterController.getSupportedMovementDirections(), storage); } storage.mActionCooldown -= duration; @@ -152,11 +161,12 @@ namespace MWMechanics return attack(actor, target, storage, characterController); } - bool AiCombat::attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController) + bool AiCombat::attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, + CharacterController& characterController) { const MWWorld::CellStore*& currentCell = storage.mCell; bool cellChange = currentCell && (actor.getCell() != currentCell); - if(!currentCell || cellChange) + if (!currentCell || cellChange) { currentCell = actor.getCell(); } @@ -165,14 +175,19 @@ namespace MWMechanics if (!canFight(actor, target)) { storage.stopAttack(); - characterController.setAttackingOrSpell(false); + actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false); storage.mActionCooldown = 0.f; // Continue combat if target is player or player follower/escorter and an attack has been attempted - const std::list& playerFollowersAndEscorters = MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(MWMechanics::getPlayer()); - bool targetSidesWithPlayer = (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), target) != playerFollowersAndEscorters.end()); + const auto& playerFollowersAndEscorters + = MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(MWMechanics::getPlayer()); + bool targetSidesWithPlayer + = (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), target) + != playerFollowersAndEscorters.end()); if ((target == MWMechanics::getPlayer() || targetSidesWithPlayer) - && ((actor.getClass().getCreatureStats(actor).getHitAttemptActorId() == target.getClass().getCreatureStats(target).getActorId()) - || (target.getClass().getCreatureStats(target).getHitAttemptActorId() == actor.getClass().getCreatureStats(actor).getActorId()))) + && ((actor.getClass().getCreatureStats(actor).getHitAttemptActorId() + == target.getClass().getCreatureStats(target).getActorId()) + || (target.getClass().getCreatureStats(target).getHitAttemptActorId() + == actor.getClass().getCreatureStats(actor).getActorId()))) forceFlee = true; else // Otherwise end combat return true; @@ -182,7 +197,7 @@ namespace MWMechanics actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); float& actionCooldown = storage.mActionCooldown; - std::shared_ptr& currentAction = storage.mCurrentAction; + std::unique_ptr& currentAction = storage.mCurrentAction; if (!forceFlee) { @@ -197,7 +212,7 @@ namespace MWMechanics } else { - currentAction.reset(new ActionFlee()); + currentAction = std::make_unique(); actionCooldown = currentAction->getActionCooldown(); } @@ -209,7 +224,7 @@ namespace MWMechanics if (currentAction->isFleeing()) { storage.startFleeing(); - MWBase::Environment::get().getDialogueManager()->say(actor, "flee"); + MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("flee")); return false; } else @@ -217,7 +232,7 @@ namespace MWMechanics } bool isRangedCombat = false; - float &rangeAttack = storage.mAttackRange; + float& rangeAttack = storage.mAttackRange; rangeAttack = currentAction->getCombatRange(isRangedCombat); @@ -228,14 +243,15 @@ namespace MWMechanics const osg::Vec3f vActorPos(pos.asVec3()); const osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3()); - float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target); + float distToTarget = getDistanceToBounds(actor, target); storage.mReadyToAttack = (currentAction->isAttackingOrSpell() && distToTarget <= rangeAttack && storage.mLOS); if (isRangedCombat) { // rotate actor taking into account target movement direction and projectile speed - osg::Vec3f vAimDir = AimDirToMovingTarget(actor, target, storage.mLastTargetPos, AI_REACTION_TIME, (weapon ? weapon->mData.mType : 0), storage.mStrength); + osg::Vec3f vAimDir = AimDirToMovingTarget(actor, target, storage.mLastTargetPos, AI_REACTION_TIME, + (weapon ? weapon->mData.mType : 0), storage.mStrength); storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); storage.mMovement.mRotation[2] = getZAngleToDir(vAimDir); @@ -244,7 +260,8 @@ namespace MWMechanics { osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(actor, target, false); storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); - storage.mMovement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated + storage.mMovement.mRotation[2] = getZAngleToDir( + (vTargetPos - vActorPos)); // using vAimDir results in spastic movements since the head is animated } storage.mLastTargetPos = vTargetPos; @@ -253,7 +270,15 @@ namespace MWMechanics { storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target); // start new attack - storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); + bool canShout = true; + ESM::RefId spellId = storage.mCurrentAction->getSpell(); + if (!spellId.empty()) + { + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(spellId); + if (spell->mEffects.mList.empty() || spell->mEffects.mList[0].mData.mRange != ESM::RT_Target) + canShout = false; + } + storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat, canShout); } // If actor uses custom destination it has to try to rebuild path because environment can change @@ -265,23 +290,27 @@ namespace MWMechanics { const MWBase::World* world = MWBase::Environment::get().getWorld(); // Try to build path to the target. - const auto halfExtents = world->getPathfindingHalfExtents(actor); - const auto navigatorFlags = getNavigatorFlags(actor); - const auto areaCosts = getAreaCosts(actor); - const auto pathGridGraph = getPathGridGraph(actor.getCell()); - mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, halfExtents, navigatorFlags, areaCosts); + const auto agentBounds = world->getPathfindingAgentBounds(actor); + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); + const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); + const auto& pathGridGraph = getPathGridGraph(pathgrid); + mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds, + navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); if (!mPathFinder.isPathConstructed()) { // If there is no path, try to find a point on a line from the actor position to target projected // on navmesh to attack the target from there. const auto navigator = world->getNavigator(); - const auto hit = navigator->raycast(halfExtents, vActorPos, vTargetPos, navigatorFlags); + const auto hit + = DetourNavigator::raycast(*navigator, agentBounds, vActorPos, vTargetPos, navigatorFlags); if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) { // If the point is close enough, try to find a path to that point. - mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, halfExtents, navigatorFlags, areaCosts); + mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, agentBounds, + navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); if (mPathFinder.isPathConstructed()) { // If path to that point is found use it as custom destination. @@ -294,11 +323,11 @@ namespace MWMechanics { storage.mUseCustomDestination = false; storage.stopAttack(); - characterController.setAttackingOrSpell(false); - currentAction.reset(new ActionFlee()); + actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false); + currentAction = std::make_unique(); actionCooldown = currentAction->getActionCooldown(); storage.startFleeing(); - MWBase::Environment::get().getDialogueManager()->say(actor, "flee"); + MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("flee")); } } else @@ -310,7 +339,8 @@ namespace MWMechanics return false; } - void MWMechanics::AiCombat::updateLOS(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, MWMechanics::AiCombatStorage& storage) + void MWMechanics::AiCombat::updateLOS( + const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, MWMechanics::AiCombatStorage& storage) { static const float LOS_UPDATE_DURATION = 0.5f; if (storage.mUpdateLOSTimer <= 0.f) @@ -322,7 +352,8 @@ namespace MWMechanics storage.mUpdateLOSTimer -= duration; } - void MWMechanics::AiCombat::updateFleeing(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, MWMechanics::AiCombatStorage& storage) + void MWMechanics::AiCombat::updateFleeing(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, + MWWorld::MovementDirectionFlags supportedMovementDirections, AiCombatStorage& storage) { static const float BLIND_RUN_DURATION = 1.0f; @@ -335,84 +366,94 @@ namespace MWMechanics return; case AiCombatStorage::FleeState_Idle: + { + float triggerDist = getMaxAttackDistance(target); + const MWWorld::Cell* cellVariant = storage.mCell->getCell(); + if (storage.mLOS && (triggerDist >= 1000 || getDistanceMinusHalfExtents(actor, target) <= triggerDist)) { - float triggerDist = getMaxAttackDistance(target); + const ESM::Pathgrid* pathgrid + = MWBase::Environment::get().getESMStore()->get().search(*cellVariant); - if (storage.mLOS && - (triggerDist >= 1000 || getDistanceMinusHalfExtents(actor, target) <= triggerDist)) + bool runFallback = true; + + if (pathgrid != nullptr && !pathgrid->mPoints.empty() + && !actor.getClass().isPureWaterCreature(actor)) { - const ESM::Pathgrid* pathgrid = - MWBase::Environment::get().getWorld()->getStore().get().search(*storage.mCell->getCell()); + ESM::Pathgrid::PointList points; + const Misc::CoordinateConverter coords + = Misc::makeCoordinateConverter(*storage.mCell->getCell()); - bool runFallback = true; + osg::Vec3f localPos = actor.getRefData().getPosition().asVec3(); + coords.toLocal(localPos); - if (pathgrid && !actor.getClass().isPureWaterCreature(actor)) + int closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos); + for (int i = 0; i < static_cast(pathgrid->mPoints.size()); i++) { - ESM::Pathgrid::PointList points; - Misc::CoordinateConverter coords(storage.mCell->getCell()); - - 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++) + if (i != closestPointIndex + && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i)) { - if (i != closestPointIndex && getPathGridGraph(storage.mCell).isPointConnected(closestPointIndex, i)) - { - points.push_back(pathgrid->mPoints[static_cast(i)]); - } + points.push_back(pathgrid->mPoints[static_cast(i)]); } + } - if (!points.empty()) - { - ESM::Pathgrid::Point dest = points[Misc::Rng::rollDice(points.size())]; - coords.toWorld(dest); + if (!points.empty()) + { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + ESM::Pathgrid::Point dest = points[Misc::Rng::rollDice(points.size(), prng)]; + coords.toWorld(dest); - state = AiCombatStorage::FleeState_RunToDestination; - storage.mFleeDest = ESM::Pathgrid::Point(dest.mX, dest.mY, dest.mZ); + state = AiCombatStorage::FleeState_RunToDestination; + storage.mFleeDest = ESM::Pathgrid::Point(dest.mX, dest.mY, dest.mZ); - runFallback = false; - } + runFallback = false; } + } - if (runFallback) - { - state = AiCombatStorage::FleeState_RunBlindly; - storage.mFleeBlindRunTimer = 0.0f; - } + if (runFallback) + { + state = AiCombatStorage::FleeState_RunBlindly; + storage.mFleeBlindRunTimer = 0.0f; } } - break; + } + break; case AiCombatStorage::FleeState_RunBlindly: + { + // timer to prevent twitchy movement that can be observed in vanilla MW + if (storage.mFleeBlindRunTimer < BLIND_RUN_DURATION) { - // timer to prevent twitchy movement that can be observed in vanilla MW - if (storage.mFleeBlindRunTimer < BLIND_RUN_DURATION) - { - storage.mFleeBlindRunTimer += duration; - - storage.mMovement.mRotation[0] = -actor.getRefData().getPosition().rot[0]; - storage.mMovement.mRotation[2] = osg::PI + getZAngleToDir(target.getRefData().getPosition().asVec3()-actor.getRefData().getPosition().asVec3()); - storage.mMovement.mPosition[1] = 1; - updateActorsMovement(actor, duration, storage); - } - else - state = AiCombatStorage::FleeState_Idle; + storage.mFleeBlindRunTimer += duration; + + storage.mMovement.mRotation[0] = -actor.getRefData().getPosition().rot[0]; + storage.mMovement.mRotation[2] = osg::PI + + getZAngleToDir( + target.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3()); + storage.mMovement.mPosition[1] = 1; + updateActorsMovement(actor, duration, storage); } - break; + else + state = AiCombatStorage::FleeState_Idle; + } + break; case AiCombatStorage::FleeState_RunToDestination: + { + static const float fFleeDistance = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fFleeDistance") + ->mValue.getFloat(); + + float dist + = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length(); + if ((dist > fFleeDistance && !storage.mLOS) + || pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration, supportedMovementDirections)) { - static const float fFleeDistance = MWBase::Environment::get().getWorld()->getStore().get().find("fFleeDistance")->mValue.getFloat(); - - float dist = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length(); - if ((dist > fFleeDistance && !storage.mLOS) - || pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration)) - { - state = AiCombatStorage::FleeState_Idle; - } + state = AiCombatStorage::FleeState_Idle; } - break; + } + break; }; } @@ -432,35 +473,68 @@ namespace MWMechanics rotateActorOnAxis(actor, 0, actorMovementSettings, storage); } - void AiCombat::rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, - MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage) + void AiCombat::rotateActorOnAxis( + const MWWorld::Ptr& actor, int axis, MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage) { actorMovementSettings.mRotation[axis] = 0; bool isRangedCombat = false; storage.mCurrentAction->getCombatRange(isRangedCombat); float eps = isRangedCombat ? osg::DegreesToRadians(0.5) : osg::DegreesToRadians(3.f); float targetAngleRadians = storage.mMovement.mRotation[axis]; - smoothTurn(actor, targetAngleRadians, axis, eps); + storage.mRotateMove = !smoothTurn(actor, targetAngleRadians, axis, eps); } MWWorld::Ptr AiCombat::getTarget() const { - return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + if (mCachedTarget.isEmpty() || mCachedTarget.mRef->isDeleted() || !mCachedTarget.getRefData().isEnabled()) + { + mCachedTarget = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + } + return mCachedTarget; } - void AiCombat::writeState(ESM::AiSequence::AiSequence &sequence) const + void AiCombat::writeState(ESM::AiSequence::AiSequence& sequence) const { - std::unique_ptr combat(new ESM::AiSequence::AiCombat()); + auto combat = std::make_unique(); combat->mTargetActorId = mTargetActorId; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Combat; - package.mPackage = combat.release(); - sequence.mPackages.push_back(package); + package.mPackage = std::move(combat); + sequence.mPackages.push_back(std::move(package)); + } + + AiCombatStorage::AiCombatStorage() + : mAttackCooldown(0.0f) + , mReaction(MWBase::Environment::get().getWorld()->getPrng()) + , mTimerCombatMove(0.0f) + , mReadyToAttack(false) + , mAttack(false) + , mAttackRange(0.0f) + , mCombatMove(false) + , mRotateMove(false) + , mLastTargetPos(0, 0, 0) + , mCell(nullptr) + , mCurrentAction() + , mActionCooldown(0.0f) + , mStrength() + , mForceNoShortcut(false) + , mShortcutFailPos() + , mMovement() + , mFleeState(FleeState_None) + , mLOS(false) + , mUpdateLOSTimer(0.0f) + , mFleeBlindRunTimer(0.0f) + , mUseCustomDestination(false) + , mCustomDestination() + { } - void AiCombatStorage::startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target) + void AiCombatStorage::startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, + const MWWorld::Ptr& actor, const MWWorld::Ptr& target) { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + // get the range of the target's weapon MWWorld::Ptr targetWeapon = MWWorld::Ptr(); const MWWorld::Class& targetClass = target.getClass(); @@ -475,27 +549,52 @@ namespace MWMechanics bool targetUsesRanged = false; float rangeAttackOfTarget = ActionWeapon(targetWeapon).getCombatRange(targetUsesRanged); - - if (mMovement.mPosition[0] || mMovement.mPosition[1]) + + if (mMovement.mPosition[0]) { - mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); + mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(prng); mCombatMove = true; } - else if (isDistantCombat) + // dodge movements (for NPCs and bipedal creatures) + // Note: do not use for ranged combat yet since in couple with back up behaviour can move actor out of cliff + else if (actor.getClass().isBipedal(actor) && !isDistantCombat) + { + float moveDuration = 0; + float angleToTarget + = Misc::normalizeAngle(mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2]); + // Apply a big side step if enemy tries to get around and come from behind. + // Otherwise apply a random side step (kind of dodging) with some probability + // if actor is within range of target's weapon. + if (std::abs(angleToTarget) > osg::PI / 4) + moveDuration = 0.2f; + else if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability(prng) < 0.25) + moveDuration = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(prng); + if (moveDuration > 0) + { + mMovement.mPosition[0] = Misc::Rng::rollProbability(prng) < 0.5 ? 1.0f : -1.0f; // to the left/right + mTimerCombatMove = moveDuration; + mCombatMove = true; + } + } + + mMovement.mPosition[1] = 0; + if (isDistantCombat) { // Backing up behaviour // Actor backs up slightly further away than opponent's weapon range - // (in vanilla - only as far as oponent's weapon range), + // (in vanilla - only as far as opponent's weapon range), // or not at all if opponent is using a ranged weapon - if (targetUsesRanged || distToTarget > rangeAttackOfTarget*1.5) // Don't back up if the target is wielding ranged weapon + if (targetUsesRanged + || distToTarget > rangeAttackOfTarget * 1.5) // Don't back up if the target is wielding ranged weapon return; // actor should not back up into water if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.5f)) return; - int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Door; + int mask + = MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Door; // Actor can not back up if there is no free space behind // Currently we take the 35% of actor's height from the ground as vector height. @@ -503,10 +602,11 @@ namespace MWMechanics osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); osg::Vec3f pos = actor.getRefData().getPosition().asVec3(); osg::Vec3f source = pos + osg::Vec3f(0, 0, 0.75f * halfExtents.z()); - osg::Vec3f fallbackDirection = actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,-1,0); + osg::Vec3f fallbackDirection = actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0, -1, 0); osg::Vec3f destination = source + fallbackDirection * (halfExtents.y() + 16); - bool isObstacleDetected = MWBase::Environment::get().getWorld()->castRay(source.x(), source.y(), source.z(), destination.x(), destination.y(), destination.z(), mask); + const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); + bool isObstacleDetected = rayCasting->castRay(source, destination, mask).mHit; if (isObstacleDetected) return; @@ -515,32 +615,12 @@ namespace MWMechanics // If we did not hit anything, there is a cliff behind actor. source = pos + osg::Vec3f(0, 0, 0.75f * halfExtents.z()) + fallbackDirection * (halfExtents.y() + 96); destination = source - osg::Vec3f(0, 0, 0.75f * halfExtents.z() + 96); - bool isCliffDetected = !MWBase::Environment::get().getWorld()->castRay(source.x(), source.y(), source.z(), destination.x(), destination.y(), destination.z(), mask); + bool isCliffDetected = !rayCasting->castRay(source, destination, mask).mHit; if (isCliffDetected) return; mMovement.mPosition[1] = -1; } - // dodge movements (for NPCs and bipedal creatures) - // Note: do not use for ranged combat yet since in couple with back up behaviour can move actor out of cliff - else if (actor.getClass().isBipedal(actor)) - { - float moveDuration = 0; - float angleToTarget = Misc::normalizeAngle(mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2]); - // Apply a big side step if enemy tries to get around and come from behind. - // Otherwise apply a random side step (kind of dodging) with some probability - // if actor is within range of target's weapon. - if (std::abs(angleToTarget) > osg::PI / 4) - moveDuration = 0.2f; - else if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) - moveDuration = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); - if (moveDuration > 0) - { - mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; // to the left/right - mTimerCombatMove = moveDuration; - mCombatMove = true; - } - } } void AiCombatStorage::updateCombatMove(float duration) @@ -558,26 +638,27 @@ namespace MWMechanics void AiCombatStorage::stopCombatMove() { mTimerCombatMove = 0; - mMovement.mPosition[1] = mMovement.mPosition[0] = 0; + mMovement.mPosition[0] = 0; mCombatMove = false; } - void AiCombatStorage::startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, - const ESM::Weapon* weapon, bool distantCombat) + void AiCombatStorage::startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, + const ESM::Weapon* weapon, bool distantCombat, bool canShout) { if (mReadyToAttack && characterController.readyToStartAttack()) { if (mAttackCooldown <= 0) { mAttack = true; // attack starts just now - characterController.setAttackingOrSpell(true); + actor.getClass().getCreatureStats(actor).setAttackingOrSpell(true); if (!distantCombat) characterController.setAIAttackType(chooseBestAttack(weapon)); - mStrength = Misc::Rng::rollClosedProbability(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + mStrength = Misc::Rng::rollClosedProbability(prng); - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); float baseDelay = store.get().find("fCombatDelayCreature")->mValue.getFloat(); if (actor.getClass().isNpc()) @@ -585,26 +666,32 @@ namespace MWMechanics baseDelay = store.get().find("fCombatDelayNPC")->mValue.getFloat(); } - // Say a provoking combat phrase - const int iVoiceAttackOdds = store.get().find("iVoiceAttackOdds")->mValue.getInteger(); - if (Misc::Rng::roll0to99() < iVoiceAttackOdds) + if (canShout) { - MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); + // Say a provoking combat phrase + const int iVoiceAttackOdds + = store.get().find("iVoiceAttackOdds")->mValue.getInteger(); + if (Misc::Rng::roll0to99(prng) < iVoiceAttackOdds) + { + MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("attack")); + } } - mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9); + mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(prng), baseDelay + 0.9); } else mAttackCooldown -= AI_REACTION_TIME; } } - void AiCombatStorage::updateAttack(CharacterController& characterController) + void AiCombatStorage::updateAttack(const MWWorld::Ptr& actor, CharacterController& characterController) { - if (mAttack && (characterController.getAttackStrength() >= mStrength || characterController.readyToPrepareAttack())) + if (mAttack) { - mAttack = false; + float attackStrength = characterController.calculateWindUp(); + mAttack + = !characterController.readyToPrepareAttack() && attackStrength < mStrength && attackStrength != -1.f; } - characterController.setAttackingOrSpell(mAttack); + actor.getClass().getCreatureStats(actor).setAttackingOrSpell(mAttack); } void AiCombatStorage::stopAttack() @@ -631,104 +718,101 @@ namespace MWMechanics mFleeDest = ESM::Pathgrid::Point(0, 0, 0); } - bool AiCombatStorage::isFleeing() + bool AiCombatStorage::isFleeing() const { return mFleeState != FleeState_None; } } - namespace { -std::string chooseBestAttack(const ESM::Weapon* weapon) -{ - std::string attackType; - - if (weapon != nullptr) + std::string_view chooseBestAttack(const ESM::Weapon* weapon) { - //the more damage attackType deals the more probability it has - int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; - int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; - int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; - - float roll = Misc::Rng::rollClosedProbability() * (slash + chop + thrust); - if(roll <= slash) - attackType = "slash"; - else if(roll <= (slash + thrust)) - attackType = "thrust"; - else - attackType = "chop"; + if (weapon != nullptr) + { + // the more damage attackType deals the more probability it has + int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1]) / 2; + int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1]) / 2; + int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1]) / 2; + + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + float roll = Misc::Rng::rollClosedProbability(prng) * (slash + chop + thrust); + if (roll <= slash) + return "slash"; + else if (roll <= (slash + thrust)) + return "thrust"; + else + return "chop"; + } + return MWMechanics::CharacterController::getRandomAttackType(); } - else - MWMechanics::CharacterController::setAttackTypeRandomly(attackType); - return attackType; -} + osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, + const osg::Vec3f& vLastTargetPos, float duration, int weapType, float strength) + { + float projSpeed; + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); -osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos, - float duration, int weapType, float strength) -{ - float projSpeed; - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + // get projectile speed (depending on weapon type) + if (MWMechanics::getWeaponType(weapType)->mWeaponClass == ESM::WeaponType::Thrown) + { + static float fThrownWeaponMinSpeed = gmst.find("fThrownWeaponMinSpeed")->mValue.getFloat(); + static float fThrownWeaponMaxSpeed = gmst.find("fThrownWeaponMaxSpeed")->mValue.getFloat(); - // get projectile speed (depending on weapon type) - if (MWMechanics::getWeaponType(weapType)->mWeaponClass == ESM::WeaponType::Thrown) - { - static float fThrownWeaponMinSpeed = gmst.find("fThrownWeaponMinSpeed")->mValue.getFloat(); - static float fThrownWeaponMaxSpeed = gmst.find("fThrownWeaponMaxSpeed")->mValue.getFloat(); + projSpeed = fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * strength; + } + else if (weapType != 0) + { + static float fProjectileMinSpeed = gmst.find("fProjectileMinSpeed")->mValue.getFloat(); + static float fProjectileMaxSpeed = gmst.find("fProjectileMaxSpeed")->mValue.getFloat(); - projSpeed = fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * strength; - } - else if (weapType != 0) - { - static float fProjectileMinSpeed = gmst.find("fProjectileMinSpeed")->mValue.getFloat(); - static float fProjectileMaxSpeed = gmst.find("fProjectileMaxSpeed")->mValue.getFloat(); + projSpeed = fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * strength; + } + else // weapType is 0 ==> it's a target spell projectile + { + projSpeed = gmst.find("fTargetSpellMaxSpeed")->mValue.getFloat(); + } - projSpeed = fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * strength; - } - else // weapType is 0 ==> it's a target spell projectile - { - projSpeed = gmst.find("fTargetSpellMaxSpeed")->mValue.getFloat(); - } + // idea: perpendicular to dir to target speed components of target move vector and projectile vector should be + // the same - // idea: perpendicular to dir to target speed components of target move vector and projectile vector should be the same + osg::Vec3f vTargetPos = target.getRefData().getPosition().asVec3(); + osg::Vec3f vDirToTarget = MWBase::Environment::get().getWorld()->aimToTarget(actor, target, true); + float distToTarget = vDirToTarget.length(); - osg::Vec3f vTargetPos = target.getRefData().getPosition().asVec3(); - osg::Vec3f vDirToTarget = MWBase::Environment::get().getWorld()->aimToTarget(actor, target, true); - float distToTarget = vDirToTarget.length(); + osg::Vec3f vTargetMoveDir = vTargetPos - vLastTargetPos; + vTargetMoveDir /= duration; // |vTargetMoveDir| is target real speed in units/sec now - osg::Vec3f vTargetMoveDir = vTargetPos - vLastTargetPos; - vTargetMoveDir /= duration; // |vTargetMoveDir| is target real speed in units/sec now + osg::Vec3f vPerpToDir = vDirToTarget ^ osg::Vec3f(0, 0, 1); // cross product - osg::Vec3f vPerpToDir = vDirToTarget ^ osg::Vec3f(0,0,1); // cross product + vPerpToDir.normalize(); + osg::Vec3f vDirToTargetNormalized = vDirToTarget; + vDirToTargetNormalized.normalize(); - vPerpToDir.normalize(); - osg::Vec3f vDirToTargetNormalized = vDirToTarget; - vDirToTargetNormalized.normalize(); + // dot product + float velPerp = vTargetMoveDir * vPerpToDir; + float velDir = vTargetMoveDir * vDirToTargetNormalized; - // dot product - float velPerp = vTargetMoveDir * vPerpToDir; - float velDir = vTargetMoveDir * vDirToTargetNormalized; + // time to collision between target and projectile + float t_collision; - // time to collision between target and projectile - float t_collision; + float projVelDirSquared = projSpeed * projSpeed - velPerp * velPerp; + if (projVelDirSquared > 0) + { + osg::Vec3f vTargetMoveDirNormalized = vTargetMoveDir; + vTargetMoveDirNormalized.normalize(); - float projVelDirSquared = projSpeed * projSpeed - velPerp * velPerp; - if (projVelDirSquared > 0) - { - osg::Vec3f vTargetMoveDirNormalized = vTargetMoveDir; - vTargetMoveDirNormalized.normalize(); + float projDistDiff = vDirToTarget * vTargetMoveDirNormalized; // dot product + projDistDiff = std::sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff); - float projDistDiff = vDirToTarget * vTargetMoveDirNormalized; // dot product - projDistDiff = std::sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff); + t_collision = projDistDiff / (std::sqrt(projVelDirSquared) - velDir); + } + else + t_collision = 0; // speed of projectile is not enough to reach moving target - t_collision = projDistDiff / (std::sqrt(projVelDirSquared) - velDir); + return vDirToTarget + vTargetMoveDir * t_collision; } - else - t_collision = 0; // speed of projectile is not enough to reach moving target - - return vDirToTarget + vTargetMoveDir * t_collision; -} } diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 5425f1af0be..d5a9c3464c5 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -1,15 +1,13 @@ #ifndef GAME_MWMECHANICS_AICOMBAT_H #define GAME_MWMECHANICS_AICOMBAT_H +#include "aitemporarybase.hpp" #include "typedaipackage.hpp" #include "../mwworld/cellstore.hpp" // for Doors -#include "../mwbase/world.hpp" - -#include "pathfinding.hpp" -#include "movement.hpp" #include "aitimer.hpp" +#include "movement.hpp" namespace ESM { @@ -33,9 +31,10 @@ namespace MWMechanics bool mAttack; float mAttackRange; bool mCombatMove; + bool mRotateMove; osg::Vec3f mLastTargetPos; const MWWorld::CellStore* mCell; - std::shared_ptr mCurrentAction; + std::unique_ptr mCurrentAction; float mActionCooldown; float mStrength; bool mForceNoShortcut; @@ -58,87 +57,69 @@ namespace MWMechanics bool mUseCustomDestination; osg::Vec3f mCustomDestination; - AiCombatStorage(): - mAttackCooldown(0.0f), - mTimerCombatMove(0.0f), - mReadyToAttack(false), - mAttack(false), - mAttackRange(0.0f), - mCombatMove(false), - mLastTargetPos(0,0,0), - mCell(nullptr), - mCurrentAction(), - mActionCooldown(0.0f), - mStrength(), - mForceNoShortcut(false), - mShortcutFailPos(), - mMovement(), - mFleeState(FleeState_None), - mLOS(false), - mUpdateLOSTimer(0.0f), - mFleeBlindRunTimer(0.0f), - mUseCustomDestination(false), - mCustomDestination() - {} - - void startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + AiCombatStorage(); + + void startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, + const MWWorld::Ptr& target); void updateCombatMove(float duration); void stopCombatMove(); void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, - const ESM::Weapon* weapon, bool distantCombat); - void updateAttack(CharacterController& characterController); + const ESM::Weapon* weapon, bool distantCombat, bool canShout); + void updateAttack(const MWWorld::Ptr& actor, CharacterController& characterController); void stopAttack(); void startFleeing(); void stopFleeing(); - bool isFleeing(); + bool isFleeing() const; }; /// \brief Causes the actor to fight another actor class AiCombat final : public TypedAiPackage { - public: - ///Constructor - /** \param actor Actor to fight **/ - explicit AiCombat(const MWWorld::Ptr& actor); + public: + /// Constructor + /** \param actor Actor to fight **/ + explicit AiCombat(const MWWorld::Ptr& actor); - explicit AiCombat (const ESM::AiSequence::AiCombat* combat); + explicit AiCombat(const ESM::AiSequence::AiCombat* combat); - void init(); + void init(); - bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; + bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, + float duration) override; - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Combat; } + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Combat; } - static constexpr Options makeDefaultOptions() - { - AiPackage::Options options; - options.mPriority = 1; - options.mCanCancel = false; - options.mShouldCancelPreviousAi = false; - return options; - } + static constexpr Options makeDefaultOptions() + { + AiPackage::Options options; + options.mPriority = 1; + options.mCanCancel = false; + options.mShouldCancelPreviousAi = false; + return options; + } - ///Returns target ID - MWWorld::Ptr getTarget() const override; + /// Returns target ID + MWWorld::Ptr getTarget() const override; - void writeState(ESM::AiSequence::AiSequence &sequence) const override; + void writeState(ESM::AiSequence::AiSequence& sequence) const override; - private: - /// Returns true if combat should end - bool attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController); + private: + /// Returns true if combat should end + bool attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, + CharacterController& characterController); - void updateLOS(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, AiCombatStorage& storage); + void updateLOS(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, AiCombatStorage& storage); - void updateFleeing(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, AiCombatStorage& storage); + void updateFleeing(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, + MWWorld::MovementDirectionFlags supportedMovementDirections, AiCombatStorage& storage); - /// Transfer desired movement (from AiCombatStorage) to Actor - void updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage); - void rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, - MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage); + /// Transfer desired movement (from AiCombatStorage) to Actor + void updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage); + void rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, MWMechanics::Movement& actorMovementSettings, + AiCombatStorage& storage); }; - - + } #endif diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index c26454aab53..91d2a9bbb89 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -1,30 +1,40 @@ #include "aicombataction.hpp" -#include -#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/actionequip.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" -#include "../mwworld/actionequip.hpp" -#include "../mwworld/cellstore.hpp" -#include "npcstats.hpp" +#include "actorutil.hpp" #include "combat.hpp" -#include "weaponpriority.hpp" +#include "npcstats.hpp" #include "spellpriority.hpp" +#include "spellutil.hpp" +#include "weaponpriority.hpp" #include "weapontype.hpp" namespace MWMechanics { float suggestCombatRange(int rangeTypes) { - static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatDistance")->mValue.getFloat(); - static float fHandToHandReach = MWBase::Environment::get().getWorld()->getStore().get().find("fHandToHandReach")->mValue.getFloat(); + static const float fCombatDistance = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fCombatDistance") + ->mValue.getFloat(); + static float fHandToHandReach = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fHandToHandReach") + ->mValue.getFloat(); // This distance is a possible distance of melee attack static float distance = fCombatDistance * std::max(2.f, fHandToHandReach); @@ -37,39 +47,40 @@ namespace MWMechanics return distance * 4; } - void ActionSpell::prepare(const MWWorld::Ptr &actor) + void ActionSpell::prepare(const MWWorld::Ptr& actor) { actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(mSpellId); - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Spell); if (actor.getClass().hasInventoryStore(actor)) { MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); inv.setSelectedEnchantItem(inv.end()); } - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(mSpellId); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(mSpellId); MWBase::Environment::get().getWorld()->preloadEffects(&spell->mEffects); } - float ActionSpell::getCombatRange (bool& isRanged) const + float ActionSpell::getCombatRange(bool& isRanged) const { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(mSpellId); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(mSpellId); int types = getRangeTypes(spell->mEffects); isRanged = (types & RangeTypes::Target) | (types & RangeTypes::Self); return suggestCombatRange(types); } - void ActionEnchantedItem::prepare(const MWWorld::Ptr &actor) + void ActionEnchantedItem::prepare(const MWWorld::Ptr& actor) { - actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(std::string()); + actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(ESM::RefId()); actor.getClass().getInventoryStore(actor).setSelectedEnchantItem(mItem); - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Spell); } float ActionEnchantedItem::getCombatRange(bool& isRanged) const { - const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(mItem->getClass().getEnchantment(*mItem)); + const ESM::Enchantment* enchantment = MWBase::Environment::get().getESMStore()->get().find( + mItem->getClass().getEnchantment(*mItem)); int types = getRangeTypes(enchantment->mEffects); isRanged = (types & RangeTypes::Target) | (types & RangeTypes::Self); @@ -83,18 +94,17 @@ namespace MWMechanics return 600.f; } - void ActionPotion::prepare(const MWWorld::Ptr &actor) + void ActionPotion::prepare(const MWWorld::Ptr& actor) { - actor.getClass().apply(actor, mPotion.getCellRef().getRefId(), actor); - actor.getClass().getContainerStore(actor).remove(mPotion, 1, actor); + actor.getClass().consume(mPotion, actor); } - void ActionWeapon::prepare(const MWWorld::Ptr &actor) + void ActionWeapon::prepare(const MWWorld::Ptr& actor) { if (actor.getClass().hasInventoryStore(actor)) { if (mWeapon.isEmpty()) - actor.getClass().getInventoryStore(actor).unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, actor); + actor.getClass().getInventoryStore(actor).unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight); else { MWWorld::ActionEquip equip(mWeapon); @@ -107,20 +117,31 @@ namespace MWMechanics equip.execute(actor); } } - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Weapon); } float ActionWeapon::getCombatRange(bool& isRanged) const { isRanged = false; - static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatDistance")->mValue.getFloat(); - static const float fProjectileMaxSpeed = MWBase::Environment::get().getWorld()->getStore().get().find("fProjectileMaxSpeed")->mValue.getFloat(); + static const float fCombatDistance = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fCombatDistance") + ->mValue.getFloat(); + static const float fProjectileMaxSpeed = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fProjectileMaxSpeed") + ->mValue.getFloat(); if (mWeapon.isEmpty()) { - static float fHandToHandReach = - MWBase::Environment::get().getWorld()->getStore().get().find("fHandToHandReach")->mValue.getFloat(); + static float fHandToHandReach = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fHandToHandReach") + ->mValue.getFloat(); return fHandToHandReach * fCombatDistance; } @@ -141,46 +162,49 @@ namespace MWMechanics return mWeapon.get()->mBase; } - std::shared_ptr prepareNextAction(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy) + std::unique_ptr prepareNextAction(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { Spells& spells = actor.getClass().getCreatureStats(actor).getSpells(); float bestActionRating = 0.f; float antiFleeRating = 0.f; // Default to hand-to-hand combat - std::shared_ptr bestAction (new ActionWeapon(MWWorld::Ptr())); + std::unique_ptr bestAction = std::make_unique(MWWorld::Ptr()); if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { bestAction->prepare(actor); return bestAction; } - if (actor.getClass().hasInventoryStore(actor)) + const bool hasInventoryStore = actor.getClass().hasInventoryStore(actor); + MWWorld::ContainerStore& store = actor.getClass().getContainerStore(actor); + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - - for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + if (it->getType() == ESM::Potion::sRecordId) { float rating = ratePotion(*it, actor); if (rating > bestActionRating) { bestActionRating = rating; - bestAction.reset(new ActionPotion(*it)); + bestAction = std::make_unique(*it); antiFleeRating = std::numeric_limits::max(); } } - - for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + // TODO remove inventory store check, creatures should be able to use enchanted items they cannot equip + else if (hasInventoryStore && !it->getClass().getEnchantment(*it).empty()) { float rating = rateMagicItem(*it, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; - bestAction.reset(new ActionEnchantedItem(it)); + bestAction = std::make_unique(it); antiFleeRating = std::numeric_limits::max(); } } + } + if (hasInventoryStore) + { MWWorld::Ptr bestArrow; float bestArrowRating = rateAmmo(actor, enemy, bestArrow, ESM::Weapon::Arrow); @@ -202,25 +226,25 @@ namespace MWMechanics ammo = bestBolt; bestActionRating = rating; - bestAction.reset(new ActionWeapon(*it, ammo)); + bestAction = std::make_unique(*it, ammo); antiFleeRating = vanillaRateWeaponAndAmmo(*it, ammo, actor, enemy); } } } - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - float rating = rateSpell(it->first, actor, enemy); + float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; - bestAction.reset(new ActionSpell(it->first->mId)); - antiFleeRating = vanillaRateSpell(it->first, actor, enemy); + bestAction = std::make_unique(spell->mId); + antiFleeRating = vanillaRateSpell(spell, actor, enemy); } } if (makeFleeDecision(actor, enemy, antiFleeRating)) - bestAction.reset(new ActionFlee()); + bestAction = std::make_unique(); if (bestAction.get()) bestAction->prepare(actor); @@ -228,7 +252,7 @@ namespace MWMechanics return bestAction; } - float getBestActionRating(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy) + float getBestActionRating(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { Spells& spells = actor.getClass().getCreatureStats(actor).getSpells(); @@ -266,9 +290,9 @@ namespace MWMechanics } } - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - float rating = rateSpell(it->first, actor, enemy); + float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; @@ -278,7 +302,6 @@ namespace MWMechanics return bestActionRating; } - float getDistanceMinusHalfExtents(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool minusZDist) { osg::Vec3f actor1Pos = actor1.getRefData().getPosition().asVec3(); @@ -289,17 +312,17 @@ namespace MWMechanics if (minusZDist) dist -= std::abs(actor1Pos.z() - actor2Pos.z()); - return (dist - - MWBase::Environment::get().getWorld()->getHalfExtents(actor1).y() - - MWBase::Environment::get().getWorld()->getHalfExtents(actor2).y()); + return (dist - MWBase::Environment::get().getWorld()->getHalfExtents(actor1).y() + - MWBase::Environment::get().getWorld()->getHalfExtents(actor2).y()); } float getMaxAttackDistance(const MWWorld::Ptr& actor) { const CreatureStats& stats = actor.getClass().getCreatureStats(actor); - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); - std::string selectedSpellId = stats.getSpells().getSelectedSpell(); + const ESM::RefId& selectedSpellId = stats.getSpells().getSelectedSpell(); MWWorld::Ptr selectedEnchItem; MWWorld::Ptr activeWeapon, activeAmmo; @@ -325,18 +348,21 @@ namespace MWMechanics static const float fHandToHandReach = gmst.find("fHandToHandReach")->mValue.getFloat(); dist = fHandToHandReach; } - else if (stats.getDrawState() == MWMechanics::DrawState_Spell) + else if (stats.getDrawState() == MWMechanics::DrawState::Spell) { dist = 1.0f; if (!selectedSpellId.empty()) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(selectedSpellId); - for (std::vector::const_iterator effectIt = - spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt) + const ESM::Spell* spell + = MWBase::Environment::get().getESMStore()->get().find(selectedSpellId); + for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); + effectIt != spell->mEffects.mList.end(); ++effectIt) { - if (effectIt->mRange == ESM::RT_Target) + if (effectIt->mData.mRange == ESM::RT_Target) { - const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld()->getStore().get().find(effectIt->mEffectID); + const ESM::MagicEffect* effect + = MWBase::Environment::get().getESMStore()->get().find( + effectIt->mData.mEffectID); dist = effect->mData.mSpeed; break; } @@ -344,16 +370,19 @@ namespace MWMechanics } else if (!selectedEnchItem.isEmpty()) { - std::string enchId = selectedEnchItem.getClass().getEnchantment(selectedEnchItem); + const ESM::RefId& enchId = selectedEnchItem.getClass().getEnchantment(selectedEnchItem); if (!enchId.empty()) { - const ESM::Enchantment* ench = MWBase::Environment::get().getWorld()->getStore().get().find(enchId); - for (std::vector::const_iterator effectIt = - ench->mEffects.mList.begin(); effectIt != ench->mEffects.mList.end(); ++effectIt) + const ESM::Enchantment* ench + = MWBase::Environment::get().getESMStore()->get().find(enchId); + for (std::vector::const_iterator effectIt = ench->mEffects.mList.begin(); + effectIt != ench->mEffects.mList.end(); ++effectIt) { - if (effectIt->mRange == ESM::RT_Target) + if (effectIt->mData.mRange == ESM::RT_Target) { - const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld()->getStore().get().find(effectIt->mEffectID); + const ESM::MagicEffect* effect + = MWBase::Environment::get().getESMStore()->get().find( + effectIt->mData.mEffectID); dist = effect->mData.mSpeed; break; } @@ -403,7 +432,8 @@ namespace MWMechanics ESM::Position actorPos = actor.getRefData().getPosition(); ESM::Position enemyPos = enemy.getRefData().getPosition(); - if (isTargetMagicallyHidden(enemy) && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(enemy, actor)) + if (isTargetMagicallyHidden(enemy) + && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(enemy, actor)) { return false; } @@ -415,14 +445,14 @@ namespace MWMechanics } float atDist = getMaxAttackDistance(actor); - if (atDist > getDistanceMinusHalfExtents(actor, enemy) - && atDist > std::abs(actorPos.pos[2] - enemyPos.pos[2])) + if (atDist > getDistanceMinusHalfExtents(actor, enemy) && atDist > std::abs(actorPos.pos[2] - enemyPos.pos[2])) { if (MWBase::Environment::get().getWorld()->getLOS(actor, enemy)) return true; } - if (actor.getClass().isPureLandCreature(actor) && MWBase::Environment::get().getWorld()->isWalkingOnWater(enemy)) + if (actor.getClass().isPureLandCreature(actor) + && MWBase::Environment::get().getWorld()->isWalkingOnWater(enemy)) { return false; } @@ -435,14 +465,21 @@ namespace MWMechanics if (actor.getClass().isBipedal(actor) || !actor.getClass().canFly(actor)) { - if (enemy.getClass().getCreatureStats(enemy).getMagicEffects().get(ESM::MagicEffect::Levitate).getMagnitude() > 0) + if (enemy.getClass() + .getCreatureStats(enemy) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Levitate) + .getMagnitude() + > 0) { float attackDistance = getMaxAttackDistance(actor); if ((attackDistance + actorPos.pos[2]) < enemyPos.pos[2]) { if (enemy.getCell()->isExterior()) { - if (attackDistance < (enemyPos.pos[2] - MWBase::Environment::get().getWorld()->getTerrainHeightAt(enemyPos.asVec3()))) + if (attackDistance < (enemyPos.pos[2] + - MWBase::Environment::get().getWorld()->getTerrainHeightAt( + enemyPos.asVec3(), enemy.getCell()->getCell()->getWorldSpace()))) return false; } } @@ -452,7 +489,12 @@ namespace MWMechanics if (!actor.getClass().canWalk(actor) && !actor.getClass().isBipedal(actor)) return true; - if (actor.getClass().getCreatureStats(actor).getMagicEffects().get(ESM::MagicEffect::Levitate).getMagnitude() > 0) + if (actor.getClass() + .getCreatureStats(actor) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Levitate) + .getMagnitude() + > 0) return true; if (MWBase::Environment::get().getWorld()->isSwimming(actor)) @@ -467,17 +509,17 @@ namespace MWMechanics float vanillaRateFlee(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { const CreatureStats& stats = actor.getClass().getCreatureStats(actor); - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); - int flee = stats.getAiSetting(CreatureStats::AI_Flee).getModified(); + const int flee = stats.getAiSetting(AiSetting::Flee).getModified(); if (flee >= 100) return flee; static const float fAIFleeHealthMult = gmst.find("fAIFleeHealthMult")->mValue.getFloat(); static const float fAIFleeFleeMult = gmst.find("fAIFleeFleeMult")->mValue.getFloat(); - float healthPercentage = (stats.getHealth().getModified() == 0.0f) - ? 1.0f : stats.getHealth().getCurrent() / stats.getHealth().getModified(); + float healthPercentage = stats.getHealth().getRatio(false); float rating = (1.0f - healthPercentage) * fAIFleeHealthMult + flee * fAIFleeFleeMult; static const int iWereWolfLevelToAttack = gmst.find("iWereWolfLevelToAttack")->mValue.getInteger(); diff --git a/apps/openmw/mwmechanics/aicombataction.hpp b/apps/openmw/mwmechanics/aicombataction.hpp index d17d5a31377..9eb7df211a9 100644 --- a/apps/openmw/mwmechanics/aicombataction.hpp +++ b/apps/openmw/mwmechanics/aicombataction.hpp @@ -3,8 +3,8 @@ #include -#include "../mwworld/ptr.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/ptr.hpp" namespace MWMechanics { @@ -13,9 +13,10 @@ namespace MWMechanics public: virtual ~Action() {} virtual void prepare(const MWWorld::Ptr& actor) = 0; - virtual float getCombatRange (bool& isRanged) const = 0; - virtual float getActionCooldown() { return 0.f; } + virtual float getCombatRange(bool& isRanged) const = 0; + virtual float getActionCooldown() const { return 0.f; } virtual const ESM::Weapon* getWeapon() const { return nullptr; } + virtual ESM::RefId getSpell() const { return {}; } virtual bool isAttackingOrSpell() const { return true; } virtual bool isFleeing() const { return false; } }; @@ -25,8 +26,8 @@ namespace MWMechanics public: ActionFlee() {} void prepare(const MWWorld::Ptr& actor) override {} - float getCombatRange (bool& isRanged) const override { return 0.0f; } - float getActionCooldown() override { return 3.0f; } + float getCombatRange(bool& isRanged) const override { return 0.0f; } + float getActionCooldown() const override { return 3.0f; } bool isAttackingOrSpell() const override { return false; } bool isFleeing() const override { return true; } }; @@ -34,39 +35,49 @@ namespace MWMechanics class ActionSpell : public Action { public: - ActionSpell(const std::string& spellId) : mSpellId(spellId) {} - std::string mSpellId; + ActionSpell(const ESM::RefId& spellId) + : mSpellId(spellId) + { + } + ESM::RefId mSpellId; /// Sets the given spell as selected on the actor's spell list. void prepare(const MWWorld::Ptr& actor) override; - float getCombatRange (bool& isRanged) const override; + float getCombatRange(bool& isRanged) const override; + ESM::RefId getSpell() const override { return mSpellId; } }; class ActionEnchantedItem : public Action { public: - ActionEnchantedItem(const MWWorld::ContainerStoreIterator& item) : mItem(item) {} + ActionEnchantedItem(const MWWorld::ContainerStoreIterator& item) + : mItem(item) + { + } MWWorld::ContainerStoreIterator mItem; /// Sets the given item as selected enchanted item in the actor's InventoryStore. void prepare(const MWWorld::Ptr& actor) override; - float getCombatRange (bool& isRanged) const override; + float getCombatRange(bool& isRanged) const override; /// Since this action has no animation, apply a small cool down for using it - float getActionCooldown() override { return 0.75f; } + float getActionCooldown() const override { return 0.75f; } }; class ActionPotion : public Action { public: - ActionPotion(const MWWorld::Ptr& potion) : mPotion(potion) {} + ActionPotion(const MWWorld::Ptr& potion) + : mPotion(potion) + { + } MWWorld::Ptr mPotion; /// Drinks the given potion. void prepare(const MWWorld::Ptr& actor) override; - float getCombatRange (bool& isRanged) const override; + float getCombatRange(bool& isRanged) const override; bool isAttackingOrSpell() const override { return false; } /// Since this action has no animation, apply a small cool down for using it - float getActionCooldown() override { return 0.75f; } + float getActionCooldown() const override { return 0.75f; } }; class ActionWeapon : public Action @@ -78,17 +89,20 @@ namespace MWMechanics public: /// \a weapon may be empty for hand-to-hand combat ActionWeapon(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo = MWWorld::Ptr()) - : mAmmunition(ammo), mWeapon(weapon) {} + : mAmmunition(ammo) + , mWeapon(weapon) + { + } /// Equips the given weapon. void prepare(const MWWorld::Ptr& actor) override; - float getCombatRange (bool& isRanged) const override; + float getCombatRange(bool& isRanged) const override; const ESM::Weapon* getWeapon() const override; }; - std::shared_ptr prepareNextAction (const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); - float getBestActionRating(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy); + std::unique_ptr prepareNextAction(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); + float getBestActionRating(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); - float getDistanceMinusHalfExtents(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool minusZDist=false); + float getDistanceMinusHalfExtents(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool minusZDist = false); float getMaxAttackDistance(const MWWorld::Ptr& actor); bool canFight(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); diff --git a/apps/openmw/mwmechanics/aiescort.cpp b/apps/openmw/mwmechanics/aiescort.cpp index 75c04611054..9e6df46340b 100644 --- a/apps/openmw/mwmechanics/aiescort.cpp +++ b/apps/openmw/mwmechanics/aiescort.cpp @@ -1,124 +1,151 @@ -#include "aiescort.hpp" - -#include -#include - -#include "../mwbase/world.hpp" -#include "../mwbase/environment.hpp" -#include "../mwbase/mechanicsmanager.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/cellstore.hpp" - -#include "creaturestats.hpp" -#include "movement.hpp" - -/* - TODO: Different behavior for AIEscort a d x y z and AIEscortCell a c d x y z. - TODO: Take account for actors being in different cells. -*/ - -namespace MWMechanics -{ - AiEscort::AiEscort(const std::string &actorId, int duration, float x, float y, float z) - : mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) - , mCellX(std::numeric_limits::max()) - , mCellY(std::numeric_limits::max()) - { - mTargetActorRefId = actorId; - } - - AiEscort::AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z) - : mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) - , mCellX(std::numeric_limits::max()) - , mCellY(std::numeric_limits::max()) - { - mTargetActorRefId = actorId; - } - - AiEscort::AiEscort(const ESM::AiSequence::AiEscort *escort) - : mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ) - // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration. - // The exact value of mDuration only matters for repeating packages. - // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. - , mDuration(escort->mRemainingDuration > 0) - , mRemainingDuration(escort->mRemainingDuration) - , mCellX(std::numeric_limits::max()) - , mCellY(std::numeric_limits::max()) - { - mTargetActorRefId = escort->mTargetId; - mTargetActorId = escort->mTargetActorId; - } - - bool AiEscort::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) - { - // If AiEscort has ran for as long or longer then the duration specified - // and the duration is not infinite, the package is complete. - if (mDuration > 0) - { - mRemainingDuration -= ((duration*MWBase::Environment::get().getWorld()->getTimeScaleFactor()) / 3600); - if (mRemainingDuration <= 0) - { - mRemainingDuration = mDuration; - return true; - } - } - - if (!mCellId.empty() && mCellId != actor.getCell()->getCell()->getCellId().mWorldspace) - return false; // Not in the correct cell, pause and rely on the player to go back through a teleport door - - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); - actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); - - const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mTargetActorRefId, false); - const osg::Vec3f leaderPos = actor.getRefData().getPosition().asVec3(); - const osg::Vec3f followerPos = follower.getRefData().getPosition().asVec3(); - const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); - const float maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - - if ((leaderPos - followerPos).length2() <= mMaxDist * mMaxDist) - { - const osg::Vec3f dest(mX, mY, mZ); - if (pathTo(actor, dest, duration, maxHalfExtent)) //Returns true on path complete - { - mRemainingDuration = mDuration; - return true; - } - mMaxDist = maxHalfExtent + 450.0f; - } - else - { - // Stop moving if the player is too far away - MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle3", 0, 1); - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - mMaxDist = maxHalfExtent + 250.0f; - } - - return false; - } - - void AiEscort::writeState(ESM::AiSequence::AiSequence &sequence) const - { - std::unique_ptr escort(new ESM::AiSequence::AiEscort()); - escort->mData.mX = mX; - escort->mData.mY = mY; - escort->mData.mZ = mZ; - escort->mTargetId = mTargetActorRefId; - escort->mTargetActorId = mTargetActorId; - escort->mRemainingDuration = mRemainingDuration; - escort->mCellId = mCellId; - - ESM::AiSequence::AiPackageContainer package; - package.mType = ESM::AiSequence::Ai_Escort; - package.mPackage = escort.release(); - sequence.mPackages.push_back(package); - } - - void AiEscort::fastForward(const MWWorld::Ptr& actor, AiState &state) - { - // Update duration counter if this package has a duration - if (mDuration > 0) - mRemainingDuration--; - } -} - +#include "aiescort.hpp" + +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/datetimemanager.hpp" + +#include "character.hpp" +#include "creaturestats.hpp" +#include "movement.hpp" + +/* + TODO: Different behavior for AIEscort a d x y z and AIEscortCell a c d x y z. + TODO: Take account for actors being in different cells. +*/ + +namespace MWMechanics +{ + AiEscort::AiEscort(const ESM::RefId& actorId, int duration, float x, float y, float z, bool repeat) + : TypedAiPackage(repeat) + , mX(x) + , mY(y) + , mZ(z) + , mDuration(duration) + , mRemainingDuration(static_cast(duration)) + { + mTargetActorRefId = actorId; + } + + AiEscort::AiEscort( + const ESM::RefId& actorId, std::string_view cellId, int duration, float x, float y, float z, bool repeat) + : TypedAiPackage(repeat) + , mCellId(cellId) + , mX(x) + , mY(y) + , mZ(z) + , mDuration(duration) + , mRemainingDuration(static_cast(duration)) + { + mTargetActorRefId = actorId; + } + + AiEscort::AiEscort(const ESM::AiSequence::AiEscort* escort) + : TypedAiPackage(escort->mRepeat) + , mCellId(escort->mCellId) + , mX(escort->mData.mX) + , mY(escort->mData.mY) + , mZ(escort->mData.mZ) + , mDuration(escort->mData.mDuration) + , mRemainingDuration(escort->mRemainingDuration) + { + mTargetActorRefId = escort->mTargetId; + mTargetActorId = escort->mTargetActorId; + } + + bool AiEscort::execute( + const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) + { + // If AiEscort has ran for as long or longer then the duration specified + // and the duration is not infinite, the package is complete. + if (mDuration > 0) + { + mRemainingDuration + -= ((duration * MWBase::Environment::get().getWorld()->getTimeManager()->getGameTimeScale()) / 3600); + if (mRemainingDuration <= 0) + { + mRemainingDuration = mDuration; + return true; + } + } + + if (!mCellId.empty() && !Misc::StringUtils::ciEqual(mCellId, actor.getCell()->getCell()->getNameId())) + return false; // Not in the correct cell, pause and rely on the player to go back through a teleport door + + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); + actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); + + const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mTargetActorRefId, false); + const osg::Vec3f leaderPos = actor.getRefData().getPosition().asVec3(); + const osg::Vec3f followerPos = follower.getRefData().getPosition().asVec3(); + const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); + const float maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + + if ((leaderPos - followerPos).length2() <= mMaxDist * mMaxDist) + { + // TESCS allows the creation of Escort packages without a specific destination + constexpr float nowhere = std::numeric_limits::max(); + if (mX == nowhere || mY == nowhere) + return true; + if (mZ == nowhere) + { + if (mCellId.empty() + && ESM::positionToExteriorCellLocation(mX, mY) + == actor.getCell()->getCell()->getExteriorCellLocation()) + return false; + return true; + } + + const osg::Vec3f dest(mX, mY, mZ); + if (pathTo(actor, dest, duration, characterController.getSupportedMovementDirections(), maxHalfExtent)) + { + mRemainingDuration = mDuration; + return true; + } + mMaxDist = maxHalfExtent + 450.0f; + } + else + { + // Stop moving if the player is too far away + MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle3", 0, 1); + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + mMaxDist = maxHalfExtent + 250.0f; + } + + return false; + } + + void AiEscort::writeState(ESM::AiSequence::AiSequence& sequence) const + { + auto escort = std::make_unique(); + escort->mData.mX = mX; + escort->mData.mY = mY; + escort->mData.mZ = mZ; + escort->mData.mDuration = mDuration; + escort->mTargetId = mTargetActorRefId; + escort->mTargetActorId = mTargetActorId; + escort->mRemainingDuration = mRemainingDuration; + escort->mCellId = mCellId; + escort->mRepeat = getRepeat(); + + ESM::AiSequence::AiPackageContainer package; + package.mType = ESM::AiSequence::Ai_Escort; + package.mPackage = std::move(escort); + sequence.mPackages.push_back(std::move(package)); + } + + void AiEscort::fastForward(const MWWorld::Ptr& actor, AiState& state) + { + // Update duration counter if this package has a duration + if (mDuration > 0) + mRemainingDuration--; + } +} diff --git a/apps/openmw/mwmechanics/aiescort.hpp b/apps/openmw/mwmechanics/aiescort.hpp index 27a177893d9..d88ecac6a57 100644 --- a/apps/openmw/mwmechanics/aiescort.hpp +++ b/apps/openmw/mwmechanics/aiescort.hpp @@ -4,13 +4,14 @@ #include "typedaipackage.hpp" #include +#include namespace ESM { -namespace AiSequence -{ - struct AiEscort; -} + namespace AiSequence + { + struct AiEscort; + } } namespace MWMechanics @@ -18,47 +19,48 @@ namespace MWMechanics /// \brief AI Package to have an NPC lead the player to a specific point class AiEscort final : public TypedAiPackage { - public: - /// Implementation of AiEscort - /** The Actor will escort the specified actor to the world position x, y, z until they reach their position, or they run out of time - \implement AiEscort **/ - AiEscort(const std::string &actorId, int duration, float x, float y, float z); - /// Implementation of AiEscortCell - /** The Actor will escort the specified actor to the cell position x, y, z until they reach their position, or they run out of time - \implement AiEscortCell **/ - AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z); + public: + /// Implementation of AiEscort + /** The Actor will escort the specified actor to the world position x, y, z until they reach their position, or + they run out of time \implement AiEscort **/ + AiEscort(const ESM::RefId& actorId, int duration, float x, float y, float z, bool repeat); + /// Implementation of AiEscortCell + /** The Actor will escort the specified actor to the cell position x, y, z until they reach their position, or + they run out of time \implement AiEscortCell **/ + AiEscort( + const ESM::RefId& actorId, std::string_view cellId, int duration, float x, float y, float z, bool repeat); - AiEscort(const ESM::AiSequence::AiEscort* escort); + AiEscort(const ESM::AiSequence::AiEscort* escort); - bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; + bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, + float duration) override; - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Escort; } + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Escort; } - static constexpr Options makeDefaultOptions() - { - AiPackage::Options options; - options.mUseVariableSpeed = true; - options.mSideWithTarget = true; - return options; - } + static constexpr Options makeDefaultOptions() + { + AiPackage::Options options; + options.mUseVariableSpeed = true; + options.mSideWithTarget = true; + return options; + } - void writeState(ESM::AiSequence::AiSequence &sequence) const override; + void writeState(ESM::AiSequence::AiSequence& sequence) const override; - void fastForward(const MWWorld::Ptr& actor, AiState& state) override; + void fastForward(const MWWorld::Ptr& actor, AiState& state) override; - osg::Vec3f getDestination() const override { return osg::Vec3f(mX, mY, mZ); } + osg::Vec3f getDestination() const override { return osg::Vec3f(mX, mY, mZ); } - private: - const std::string mCellId; - const float mX; - const float mY; - const float mZ; - float mMaxDist = 450; - const float mDuration; // In hours - float mRemainingDuration; // In hours + std::optional getDuration() const override { return mDuration; } - const int mCellX; - const int mCellY; + private: + const std::string mCellId; + const float mX; + const float mY; + const float mZ; + float mMaxDist = 450; + const float mDuration; // In hours + float mRemainingDuration; // In hours }; } #endif diff --git a/apps/openmw/mwmechanics/aiface.cpp b/apps/openmw/mwmechanics/aiface.cpp index 17b18babc1a..2cd46a7a00c 100644 --- a/apps/openmw/mwmechanics/aiface.cpp +++ b/apps/openmw/mwmechanics/aiface.cpp @@ -5,11 +5,13 @@ #include "steering.hpp" MWMechanics::AiFace::AiFace(float targetX, float targetY) - : mTargetX(targetX), mTargetY(targetY) + : mTargetX(targetX) + , mTargetY(targetY) { } -bool MWMechanics::AiFace::execute(const MWWorld::Ptr& actor, MWMechanics::CharacterController& /*characterController*/, MWMechanics::AiState& /*state*/, float /*duration*/) +bool MWMechanics::AiFace::execute(const MWWorld::Ptr& actor, MWMechanics::CharacterController& /*characterController*/, + MWMechanics::AiState& /*state*/, float /*duration*/) { osg::Vec3f dir = osg::Vec3f(mTargetX, mTargetY, 0) - actor.getRefData().getPosition().asVec3(); return zTurn(actor, std::atan2(dir.x(), dir.y()), osg::DegreesToRadians(3.f)); diff --git a/apps/openmw/mwmechanics/aiface.hpp b/apps/openmw/mwmechanics/aiface.hpp index e176eb52ec1..56747c078e7 100644 --- a/apps/openmw/mwmechanics/aiface.hpp +++ b/apps/openmw/mwmechanics/aiface.hpp @@ -6,26 +6,28 @@ namespace MWMechanics { /// AiPackage which makes an actor face a certain direction. - class AiFace final : public TypedAiPackage { - public: - AiFace(float targetX, float targetY); + class AiFace final : public TypedAiPackage + { + public: + AiFace(float targetX, float targetY); - bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; + bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, + float duration) override; - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Face; } + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Face; } - static constexpr Options makeDefaultOptions() - { - AiPackage::Options options; - options.mPriority = 2; - options.mCanCancel = false; - options.mShouldCancelPreviousAi = false; - return options; - } + static constexpr Options makeDefaultOptions() + { + AiPackage::Options options; + options.mPriority = 2; + options.mCanCancel = false; + options.mShouldCancelPreviousAi = false; + return options; + } - private: - const float mTargetX; - const float mTargetY; + private: + const float mTargetX; + const float mTargetY; }; } diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index ec236799777..b4779dc900e 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -1,250 +1,275 @@ #include "aifollow.hpp" -#include -#include +#include +#include +#include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/datetimemanager.hpp" +#include "character.hpp" #include "creaturestats.hpp" -#include "movement.hpp" #include "steering.hpp" namespace { -osg::Vec3f::value_type getHalfExtents(const MWWorld::ConstPtr& actor) -{ - if(actor.getClass().isNpc()) - return 64; - return MWBase::Environment::get().getWorld()->getHalfExtents(actor).y(); -} + osg::Vec3f::value_type getHalfExtents(const MWWorld::ConstPtr& actor) + { + if (actor.getClass().isNpc()) + return 64; + return MWBase::Environment::get().getWorld()->getHalfExtents(actor).y(); + } } namespace MWMechanics { -int AiFollow::mFollowIndexCounter = 0; - -AiFollow::AiFollow(const std::string &actorId, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) -{ - mTargetActorRefId = actorId; -} - -AiFollow::AiFollow(const std::string &actorId, const std::string &cellId, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) -{ - mTargetActorRefId = actorId; -} - -AiFollow::AiFollow(const MWWorld::Ptr& actor, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) -{ - mTargetActorRefId = actor.getCellRef().getRefId(); - mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); -} - -AiFollow::AiFollow(const MWWorld::Ptr& actor, const std::string &cellId, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) -{ - mTargetActorRefId = actor.getCellRef().getRefId(); - mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); -} - -AiFollow::AiFollow(const MWWorld::Ptr& actor, bool commanded) -: TypedAiPackage(makeDefaultOptions().withShouldCancelPreviousAi(!commanded)) -, mAlwaysFollow(true), mDuration(0), mRemainingDuration(0), mX(0), mY(0), mZ(0) -, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) -{ - mTargetActorRefId = actor.getCellRef().getRefId(); - mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); -} - -AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) - : TypedAiPackage(makeDefaultOptions().withShouldCancelPreviousAi(!follow->mCommanded)) - , mAlwaysFollow(follow->mAlwaysFollow) - // mDuration isn't saved in the save file, so just giving it "1" for now if the package had a duration. - // The exact value of mDuration only matters for repeating packages. - // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. - , mDuration(follow->mRemainingDuration) - , mRemainingDuration(follow->mRemainingDuration) - , mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ) - , mCellId(follow->mCellId), mActive(follow->mActive), mFollowIndex(mFollowIndexCounter++) -{ - mTargetActorRefId = follow->mTargetId; - mTargetActorId = follow->mTargetActorId; -} - -bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) -{ - const MWWorld::Ptr target = getTarget(); + int AiFollow::mFollowIndexCounter = 0; + + AiFollow::AiFollow(const ESM::RefId& actorId, float duration, float x, float y, float z, bool repeat) + : TypedAiPackage(repeat) + , mAlwaysFollow(false) + , mDuration(duration) + , mRemainingDuration(duration) + , mX(x) + , mY(y) + , mZ(z) + , mActive(false) + , mFollowIndex(mFollowIndexCounter++) + { + mTargetActorRefId = actorId; + } - // Target is not here right now, wait for it to return - // Really we should be checking whether the target is currently registered with the MechanicsManager - if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) - return false; + AiFollow::AiFollow( + const ESM::RefId& actorId, std::string_view cellId, float duration, float x, float y, float z, bool repeat) + : TypedAiPackage(repeat) + , mAlwaysFollow(false) + , mDuration(duration) + , mRemainingDuration(duration) + , mX(x) + , mY(y) + , mZ(z) + , mCellId(cellId) + , mActive(false) + , mFollowIndex(mFollowIndexCounter++) + { + mTargetActorRefId = actorId; + } - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + AiFollow::AiFollow(const MWWorld::Ptr& actor, bool commanded) + : TypedAiPackage(makeDefaultOptions().withShouldCancelPreviousAi(!commanded)) + , mAlwaysFollow(true) + , mDuration(0) + , mRemainingDuration(0) + , mX(0) + , mY(0) + , mZ(0) + , mActive(false) + , mFollowIndex(mFollowIndexCounter++) + { + mTargetActorRefId = actor.getCellRef().getRefId(); + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); + } - AiFollowStorage& storage = state.get(); + AiFollow::AiFollow(const ESM::AiSequence::AiFollow* follow) + : TypedAiPackage( + makeDefaultOptions().withShouldCancelPreviousAi(!follow->mCommanded).withRepeat(follow->mRepeat)) + , mAlwaysFollow(follow->mAlwaysFollow) + , mDuration(follow->mData.mDuration) + , mRemainingDuration(follow->mRemainingDuration) + , mX(follow->mData.mX) + , mY(follow->mData.mY) + , mZ(follow->mData.mZ) + , mCellId(follow->mCellId) + , mActive(follow->mActive) + , mFollowIndex(mFollowIndexCounter++) + { + mTargetActorRefId = follow->mTargetId; + mTargetActorId = follow->mTargetActorId; + } - bool& rotate = storage.mTurnActorToTarget; - if (rotate) + bool AiFollow::execute( + const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { - if (zTurn(actor, storage.mTargetAngleRadians)) - rotate = false; + const MWWorld::Ptr target = getTarget(); - return false; - } + // Target is not here right now, wait for it to return + // Really we should be checking whether the target is currently registered with the MechanicsManager + if (target == MWWorld::Ptr() || !target.getCellRef().getCount() || !target.getRefData().isEnabled()) + return false; - const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); - const osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); - const osg::Vec3f targetDir = targetPos - actorPos; + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); - // AiFollow requires the target to be in range and within sight for the initial activation - if (!mActive) - { - storage.mTimer -= duration; + AiFollowStorage& storage = state.get(); - if (storage.mTimer < 0) + bool& rotate = storage.mTurnActorToTarget; + if (rotate) { - if (targetDir.length2() < 500*500 && MWBase::Environment::get().getWorld()->getLOS(actor, target)) - mActive = true; - storage.mTimer = 0.5f; + if (zTurn(actor, storage.mTargetAngleRadians)) + rotate = false; + + return false; } - } - if (!mActive) - return false; - // In the original engine the first follower stays closer to the player than any subsequent followers. - // Followers beyond the first usually attempt to stand inside each other. - osg::Vec3f::value_type floatingDistance = 0; - auto followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowingByIndex(target); - if (followers.size() >= 2 && followers.cbegin()->first != mFollowIndex) - { - for(auto& follower : followers) + const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); + const osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); + const osg::Vec3f targetDir = targetPos - actorPos; + + // In the original engine the first follower stays closer to the player than any subsequent followers. + // Followers beyond the first usually attempt to stand inside each other. + osg::Vec3f::value_type floatingDistance = 0; + auto followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowingByIndex(target); + if (followers.size() >= 2 && followers.cbegin()->first != mFollowIndex) { - auto halfExtent = getHalfExtents(follower.second); - if(halfExtent > floatingDistance) - floatingDistance = halfExtent; + for (auto& follower : followers) + { + auto halfExtent = getHalfExtents(follower.second); + if (halfExtent > floatingDistance) + floatingDistance = halfExtent; + } + floatingDistance += 128; } - floatingDistance += 128; - } - floatingDistance += getHalfExtents(target) + 64; - floatingDistance += getHalfExtents(actor) * 2; - short followDistance = static_cast(floatingDistance); + floatingDistance += getHalfExtents(target) + 64; + floatingDistance += getHalfExtents(actor) * 2; + short followDistance = static_cast(floatingDistance); - if (!mAlwaysFollow) //Update if you only follow for a bit - { - //Check if we've run out of time - if (mDuration > 0) + // AiFollow requires the target to be in range and within sight for the initial activation + if (!mActive) { - mRemainingDuration -= ((duration*MWBase::Environment::get().getWorld()->getTimeScaleFactor()) / 3600); - if (mRemainingDuration <= 0) + storage.mTimer -= duration; + + if (storage.mTimer < 0) { - mRemainingDuration = mDuration; - return true; + float activeRange = followDistance + 384.f; + if (targetDir.length2() < activeRange * activeRange + && MWBase::Environment::get().getWorld()->getLOS(actor, target)) + mActive = true; + storage.mTimer = 0.5f; } } + if (!mActive) + return false; - osg::Vec3f finalPos(mX, mY, mZ); - if ((actorPos-finalPos).length2() < followDistance*followDistance) //Close-ish to final position + if (!mAlwaysFollow) // Update if you only follow for a bit { - if (actor.getCell()->isExterior()) //Outside? + // Check if we've run out of time + if (mDuration > 0) { - if (mCellId == "") //No cell to travel to + mRemainingDuration + -= ((duration * MWBase::Environment::get().getWorld()->getTimeManager()->getGameTimeScale()) + / 3600); + if (mRemainingDuration <= 0) + { + mRemainingDuration = mDuration; return true; + } } - else + + osg::Vec3f finalPos(mX, mY, mZ); + if ((actorPos - finalPos).length2() < followDistance * followDistance) // Close-ish to final position { - if (mCellId == actor.getCell()->getCell()->mName) //Cell to travel to + if (actor.getCell()->isExterior()) // Outside? + { + if (mCellId.empty()) // No cell to travel to + { + mRemainingDuration = mDuration; + return true; + } + } + else if (mCellId == actor.getCell()->getCell()->getWorldSpace()) // Cell to travel to + { + mRemainingDuration = mDuration; return true; + } } } - } + short baseFollowDistance = followDistance; + short threshold = 30; // to avoid constant switching between moving/stopping + if (storage.mMoving) + followDistance -= threshold; + else + followDistance += threshold; - short baseFollowDistance = followDistance; - short threshold = 30; // to avoid constant switching between moving/stopping - if (storage.mMoving) - followDistance -= threshold; - else - followDistance += threshold; + if (targetDir.length2() <= followDistance * followDistance) + { + float faceAngleRadians = std::atan2(targetDir.x(), targetDir.y()); - if (targetDir.length2() <= followDistance * followDistance) - { - float faceAngleRadians = std::atan2(targetDir.x(), targetDir.y()); + if (!zTurn(actor, faceAngleRadians, osg::DegreesToRadians(45.f))) + { + storage.mTargetAngleRadians = faceAngleRadians; + storage.mTurnActorToTarget = true; + } + + return false; + } + + // Go to the destination + storage.mMoving = !pathTo( + actor, targetPos, duration, characterController.getSupportedMovementDirections(), baseFollowDistance); - if (!zTurn(actor, faceAngleRadians, osg::DegreesToRadians(45.f))) + if (storage.mMoving) { - storage.mTargetAngleRadians = faceAngleRadians; - storage.mTurnActorToTarget = true; + // Check if you're far away + if (targetDir.length2() > 450 * 450) + actor.getClass().getCreatureStats(actor).setMovementFlag( + MWMechanics::CreatureStats::Flag_Run, true); // Make NPC run + else if (targetDir.length2() + < 325 * 325) // Have a bit of a dead zone, otherwise npc will constantly flip between running and not + // when right on the edge of the running threshold + actor.getClass().getCreatureStats(actor).setMovementFlag( + MWMechanics::CreatureStats::Flag_Run, false); // make NPC walk } return false; } - storage.mMoving = !pathTo(actor, targetPos, duration, baseFollowDistance); // Go to the destination - - if (storage.mMoving) + ESM::RefId AiFollow::getFollowedActor() { - //Check if you're far away - if (targetDir.length2() > 450 * 450) - actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run - else if (targetDir.length2() < 325 * 325) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshold - actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, false); //make NPC walk + return mTargetActorRefId; } - return false; -} - -std::string AiFollow::getFollowedActor() -{ - return mTargetActorRefId; -} - -bool AiFollow::isCommanded() const -{ - return !mOptions.mShouldCancelPreviousAi; -} + bool AiFollow::isCommanded() const + { + return !mOptions.mShouldCancelPreviousAi; + } -void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const -{ - std::unique_ptr follow(new ESM::AiSequence::AiFollow()); - follow->mData.mX = mX; - follow->mData.mY = mY; - follow->mData.mZ = mZ; - follow->mTargetId = mTargetActorRefId; - follow->mTargetActorId = mTargetActorId; - follow->mRemainingDuration = mRemainingDuration; - follow->mCellId = mCellId; - follow->mAlwaysFollow = mAlwaysFollow; - follow->mCommanded = isCommanded(); - follow->mActive = mActive; - - ESM::AiSequence::AiPackageContainer package; - package.mType = ESM::AiSequence::Ai_Follow; - package.mPackage = follow.release(); - sequence.mPackages.push_back(package); -} + void AiFollow::writeState(ESM::AiSequence::AiSequence& sequence) const + { + auto follow = std::make_unique(); + follow->mData.mX = mX; + follow->mData.mY = mY; + follow->mData.mZ = mZ; + follow->mData.mDuration = mDuration; + follow->mTargetId = mTargetActorRefId; + follow->mTargetActorId = mTargetActorId; + follow->mRemainingDuration = mRemainingDuration; + follow->mCellId = mCellId; + follow->mAlwaysFollow = mAlwaysFollow; + follow->mCommanded = isCommanded(); + follow->mActive = mActive; + follow->mRepeat = getRepeat(); + + ESM::AiSequence::AiPackageContainer package; + package.mType = ESM::AiSequence::Ai_Follow; + package.mPackage = std::move(follow); + sequence.mPackages.push_back(std::move(package)); + } -int AiFollow::getFollowIndex() const -{ - return mFollowIndex; -} + int AiFollow::getFollowIndex() const + { + return mFollowIndex; + } -void AiFollow::fastForward(const MWWorld::Ptr& actor, AiState &state) -{ - // Update duration counter if this package has a duration - if (mDuration > 0) - mRemainingDuration--; -} + void AiFollow::fastForward(const MWWorld::Ptr& actor, AiState& state) + { + // Update duration counter if this package has a duration + if (mDuration > 0) + mRemainingDuration--; + } } diff --git a/apps/openmw/mwmechanics/aifollow.hpp b/apps/openmw/mwmechanics/aifollow.hpp index c4ac5eb3f20..85d11977ca6 100644 --- a/apps/openmw/mwmechanics/aifollow.hpp +++ b/apps/openmw/mwmechanics/aifollow.hpp @@ -1,21 +1,20 @@ #ifndef GAME_MWMECHANICS_AIFOLLOW_H #define GAME_MWMECHANICS_AIFOLLOW_H +#include "aitemporarybase.hpp" #include "typedaipackage.hpp" #include +#include #include #include "../mwworld/ptr.hpp" -namespace ESM -{ -namespace AiSequence +namespace ESM::AiSequence { struct AiFollow; } -} namespace MWMechanics { @@ -26,78 +25,80 @@ namespace MWMechanics float mTargetAngleRadians; bool mTurnActorToTarget; - AiFollowStorage() : - mTimer(0.f), - mMoving(false), - mTargetAngleRadians(0.f), - mTurnActorToTarget(false) - {} + AiFollowStorage() + : mTimer(0.f) + , mMoving(false) + , mTargetAngleRadians(0.f) + , mTurnActorToTarget(false) + { + } }; /// \brief AiPackage for an actor to follow another actor/the PC - /** The AI will follow the target until a condition (time, or position) are set. Both can be disabled to cause the actor to follow the other indefinitely - **/ + /** The AI will follow the target until a condition (time, or position) are set. Both can be disabled to cause the + *actor to follow the other indefinitely + **/ class AiFollow final : public TypedAiPackage { - public: - AiFollow(const std::string &actorId, float duration, float x, float y, float z); - AiFollow(const std::string &actorId, const std::string &CellId, float duration, float x, float y, float z); - /// Follow Actor for duration or until you arrive at a world position - AiFollow(const MWWorld::Ptr& actor, float duration, float X, float Y, float Z); - /// Follow Actor for duration or until you arrive at a position in a cell - AiFollow(const MWWorld::Ptr& actor, const std::string &CellId, float duration, float X, float Y, float Z); - /// Follow Actor indefinitively - AiFollow(const MWWorld::Ptr& actor, bool commanded=false); - - AiFollow(const ESM::AiSequence::AiFollow* follow); - - bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; - - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Follow; } - - static constexpr Options makeDefaultOptions() - { - AiPackage::Options options; - options.mUseVariableSpeed = true; - options.mSideWithTarget = true; - options.mFollowTargetThroughDoors = true; - return options; - } - - /// Returns the actor being followed - std::string getFollowedActor(); - - void writeState (ESM::AiSequence::AiSequence& sequence) const override; - - bool isCommanded() const; - - int getFollowIndex() const; - - void fastForward(const MWWorld::Ptr& actor, AiState& state) override; - - osg::Vec3f getDestination() const override - { - MWWorld::Ptr target = getTarget(); - if (target.isEmpty()) - return osg::Vec3f(0, 0, 0); - - return target.getRefData().getPosition().asVec3(); - } - - private: - /// This will make the actor always follow. - /** Thus ignoring mDuration and mX,mY,mZ (used for summoned creatures). **/ - const bool mAlwaysFollow; - const float mDuration; // Hours - float mRemainingDuration; // Hours - const float mX; - const float mY; - const float mZ; - const std::string mCellId; - bool mActive; // have we spotted the target? - const int mFollowIndex; - - static int mFollowIndexCounter; + public: + /// Follow Actor for duration or until you arrive at a world position + AiFollow(const ESM::RefId& actorId, float duration, float x, float y, float z, bool repeat); + /// Follow Actor for duration or until you arrive at a position in a cell + AiFollow( + const ESM::RefId& actorId, std::string_view cellId, float duration, float x, float y, float z, bool repeat); + /// Follow Actor indefinitely + AiFollow(const MWWorld::Ptr& actor, bool commanded = false); + + AiFollow(const ESM::AiSequence::AiFollow* follow); + + bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, + float duration) override; + + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Follow; } + + static constexpr Options makeDefaultOptions() + { + AiPackage::Options options; + options.mUseVariableSpeed = true; + options.mSideWithTarget = true; + options.mFollowTargetThroughDoors = true; + return options; + } + + /// Returns the actor being followed + ESM::RefId getFollowedActor(); + + void writeState(ESM::AiSequence::AiSequence& sequence) const override; + + bool isCommanded() const; + + int getFollowIndex() const; + + void fastForward(const MWWorld::Ptr& actor, AiState& state) override; + + osg::Vec3f getDestination() const override + { + MWWorld::Ptr target = getTarget(); + if (target.isEmpty()) + return osg::Vec3f(0, 0, 0); + + return target.getRefData().getPosition().asVec3(); + } + + private: + /// This will make the actor always follow. + /** Thus ignoring mDuration and mX,mY,mZ (used for summoned creatures). **/ + const bool mAlwaysFollow; + const float mDuration; // Hours + float mRemainingDuration; // Hours + const float mX; + const float mY; + const float mZ; + const std::string mCellId; + bool mActive; // have we spotted the target? + const int mFollowIndex; + + static int mFollowIndexCounter; }; } #endif diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 0701f42cf32..4bcfc7dedd6 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -1,24 +1,29 @@ #include "aipackage.hpp" -#include -#include +#include #include +#include +#include #include -#include +#include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/action.hpp" -#include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" -#include "../mwworld/inventorystore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" -#include "pathgrid.hpp" +#include "../mwphysics/raycasting.hpp" + +#include "actorutil.hpp" #include "creaturestats.hpp" #include "movement.hpp" +#include "pathgrid.hpp" #include "steering.hpp" -#include "actorutil.hpp" #include @@ -26,7 +31,8 @@ namespace { float divOrMax(float dividend, float divisor) { - return divisor == 0 ? std::numeric_limits::max() * std::numeric_limits::epsilon() : dividend / divisor; + return divisor == 0 ? std::numeric_limits::max() * std::numeric_limits::epsilon() + : dividend / divisor; } float getPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) @@ -34,41 +40,82 @@ namespace const float actorTolerance = 2 * speed * duration + 1.2 * std::max(halfExtents.x(), halfExtents.y()); return std::max(MWMechanics::MIN_TOLERANCE, actorTolerance); } + + bool canOpenDoors(const MWWorld::Ptr& ptr) + { + return ptr.getClass().isBipedal(ptr) || ptr.getClass().hasInventoryStore(ptr); + } } -MWMechanics::AiPackage::AiPackage(AiPackageTypeId typeId, const Options& options) : - mTypeId(typeId), - mOptions(options), - mTargetActorRefId(""), - mTargetActorId(-1), - mRotateOnTheRunChecks(0), - mIsShortcutting(false), - mShortcutProhibited(false), - mShortcutFailPos() +MWMechanics::AiPackage::AiPackage(AiPackageTypeId typeId, const Options& options) + : mTypeId(typeId) + , mOptions(options) + , mReaction(MWBase::Environment::get().getWorld()->getPrng()) + , mTargetActorId(-1) + , mCachedTarget() + , mRotateOnTheRunChecks(0) + , mIsShortcutting(false) + , mShortcutProhibited(false) + , mShortcutFailPos() { } MWWorld::Ptr MWMechanics::AiPackage::getTarget() const { + if (!mCachedTarget.isEmpty()) + { + if (mCachedTarget.mRef->isDeleted() || !mCachedTarget.getRefData().isEnabled()) + mCachedTarget = MWWorld::Ptr(); + else + return mCachedTarget; + } + if (mTargetActorId == -2) return MWWorld::Ptr(); if (mTargetActorId == -1) { - MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mTargetActorRefId, false); - if (target.isEmpty()) + if (mTargetActorRefId.empty()) { mTargetActorId = -2; - return target; + return MWWorld::Ptr(); + } + mCachedTarget = MWBase::Environment::get().getWorld()->searchPtr(mTargetActorRefId, false); + if (mCachedTarget.isEmpty()) + { + mTargetActorId = -2; + return mCachedTarget; } else - mTargetActorId = target.getClass().getCreatureStats(target).getActorId(); + mTargetActorId = mCachedTarget.getClass().getCreatureStats(mCachedTarget).getActorId(); } if (mTargetActorId != -1) - return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + mCachedTarget = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); else return MWWorld::Ptr(); + + return mCachedTarget; +} + +bool MWMechanics::AiPackage::targetIs(const MWWorld::Ptr& ptr) const +{ + if (mTargetActorId == -2) + return ptr.isEmpty(); + else if (mTargetActorId == -1) + { + if (mTargetActorRefId.empty()) + { + mTargetActorId = -2; + return ptr.isEmpty(); + } + if (!ptr.isEmpty() && ptr.getCellRef().getRefId() == mTargetActorRefId) + return getTarget() == ptr; + return false; + } + if (ptr.isEmpty() || !ptr.getClass().isActor()) + return false; + return ptr.getClass().getCreatureStats(ptr).getActorId() == mTargetActorId; } void MWMechanics::AiPackage::reset() @@ -78,29 +125,32 @@ void MWMechanics::AiPackage::reset() mIsShortcutting = false; mShortcutProhibited = false; mShortcutFailPos = osg::Vec3f(); + mCachedTarget = MWWorld::Ptr(); mPathFinder.clearPath(); mObstacleCheck.clear(); } -bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance) +bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, + MWWorld::MovementDirectionFlags supportedMovementDirections, float destTolerance, float endTolerance, + PathType pathType) { const Misc::TimerStatus timerStatus = mReaction.update(duration); - const osg::Vec3f position = actor.getRefData().getPosition().asVec3(); //position of the actor + const osg::Vec3f position = actor.getRefData().getPosition().asVec3(); // position of the actor MWBase::World* world = MWBase::Environment::get().getWorld(); + const DetourNavigator::AgentBounds agentBounds = world->getPathfindingAgentBounds(actor); - const osg::Vec3f halfExtents = world->getHalfExtents(actor); - - /// Stops the actor when it gets too close to a unloaded cell - //... At current time, this test is unnecessary. AI shuts down when actor is more than "actors processing range" setting value - //... units from player, and exterior cells are 8192 units long and wide. + /// Stops the actor when it gets too close to a unloaded cell or when the actor is playing a scripted animation + //... At current time, the first test is unnecessary. AI shuts down when actor is more than + //... "actors processing range" setting value units from player, and exterior cells are 8192 units long and wide. //... But AI processing distance may increase in the future. - if (isNearInactiveCell(position)) + if (isNearInactiveCell(position) + || MWBase::Environment::get().getMechanicsManager()->checkScriptedAnimationPlaying(actor)) { actor.getClass().getMovementSettings(actor).mPosition[0] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest); + world->updateActorPath(actor, mPathFinder.getPath(), agentBounds, position, dest); return false; } @@ -112,7 +162,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& if (!isDestReached && timerStatus == Misc::TimerStatus::Elapsed) { - if (actor.getClass().isBipedal(actor)) + if (canOpenDoors(actor)) openDoors(actor); const bool wasShortcutting = mIsShortcutting; @@ -126,9 +176,12 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& { if (wasShortcutting || doesPathNeedRecalc(dest, actor)) // if need to rebuild path { - const auto pathfindingHalfExtents = world->getPathfindingHalfExtents(actor); - mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(actor.getCell()), - pathfindingHalfExtents, getNavigatorFlags(actor), getAreaCosts(actor)); + const ESM::Pathgrid* pathgrid + = world->getStore().get().search(*actor.getCell()->getCell()); + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); + mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(pathgrid), + agentBounds, navigatorFlags, areaCosts, endTolerance, pathType); mRotateOnTheRunChecks = 3; // give priority to go directly on target if there is minimal opportunity @@ -137,7 +190,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& // get point just before dest auto pPointBeforeDest = mPathFinder.getPath().rbegin() + 1; - // if start point is closer to the target then last point of path (excluding target itself) then go straight on the target + // if start point is closer to the target then last point of path (excluding target itself) then go + // straight on the target if (distance(position, dest) <= distance(dest, *pPointBeforeDest)) { mPathFinder.clearPath(); @@ -146,21 +200,32 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& } } - if (!mPathFinder.getPath().empty()) //Path has points in it + if (!mPathFinder.getPath().empty()) // Path has points in it { - const osg::Vec3f& lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path + const osg::Vec3f& lastPos = mPathFinder.getPath().back(); // Get the end of the proposed path - if(distance(dest, lastPos) > 100) //End of the path is far from the destination - mPathFinder.addPointToPath(dest); //Adds the final destination to the path, to try to get to where you want to go + if (distance(dest, lastPos) > 100) // End of the path is far from the destination + mPathFinder.addPointToPath( + dest); // Adds the final destination to the path, to try to get to where you want to go } } } - const float pointTolerance = getPointTolerance(actor.getClass().getMaxSpeed(actor), duration, halfExtents); + const float pointTolerance + = getPointTolerance(actor.getClass().getMaxSpeed(actor), duration, world->getHalfExtents(actor)); + + const bool smoothMovement = Settings::game().mSmoothMovement; - static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); - mPathFinder.update(position, pointTolerance, DEFAULT_TOLERANCE, - /*shortenIfAlmostStraight=*/smoothMovement, actorCanMoveByZ); + PathFinder::UpdateFlags updateFlags{}; + + if (actorCanMoveByZ) + updateFlags |= PathFinder::UpdateFlag_CanMoveByZ; + if (timerStatus == Misc::TimerStatus::Elapsed && smoothMovement) + updateFlags |= PathFinder::UpdateFlag_ShortenIfAlmostStraight; + if (timerStatus == Misc::TimerStatus::Elapsed) + updateFlags |= PathFinder::UpdateFlag_RemoveLoops; + + mPathFinder.update(position, pointTolerance, DEFAULT_TOLERANCE, updateFlags, agentBounds, getNavigatorFlags(actor)); if (isDestReached || mPathFinder.checkPathCompleted()) // if path is finished { @@ -173,13 +238,15 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& else if (mPathFinder.getPath().empty()) return false; - world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest); + world->updateActorPath(actor, mPathFinder.getPath(), agentBounds, position, dest); if (mRotateOnTheRunChecks == 0 - || isReachableRotatingOnTheRun(actor, *mPathFinder.getPath().begin())) // to prevent circling around a path point + || isReachableRotatingOnTheRun( + actor, *mPathFinder.getPath().begin())) // to prevent circling around a path point { actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // move to the target - if (mRotateOnTheRunChecks > 0) mRotateOnTheRunChecks--; + if (mRotateOnTheRunChecks > 0) + mRotateOnTheRunChecks--; } // turn to next path point by X,Z axes @@ -188,7 +255,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0); const auto destination = getNextPathPoint(dest); - mObstacleCheck.update(actor, destination, duration); + mObstacleCheck.update(actor, destination, duration, supportedMovementDirections); if (smoothMovement) { @@ -219,13 +286,13 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor) { // check if stuck due to obstacles - if (!mObstacleCheck.isEvading()) return; + if (!mObstacleCheck.isEvading()) + return; // first check if obstacle is a door - static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); - + float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); const MWWorld::Ptr door = getNearbyDoor(actor, distance); - if (!door.isEmpty() && actor.getClass().isBipedal(actor)) + if (!door.isEmpty() && canOpenDoors(actor)) { openDoors(actor); } @@ -257,9 +324,7 @@ void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor) if (mPathFinder.getPathSize() == 0) return; - MWBase::World* world = MWBase::Environment::get().getWorld(); - static float distance = world->getMaxActivationDistance(); - + float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); const MWWorld::Ptr door = getNearbyDoor(actor, distance); if (door == MWWorld::Ptr()) return; @@ -269,49 +334,51 @@ void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor) if (!isDoorOnTheWay(actor, door, mPathFinder.getPath().front())) return; - if ((door.getCellRef().getTrap().empty() && door.getCellRef().getLockLevel() <= 0 )) + if (door.getCellRef().getTrap().empty() && !door.getCellRef().isLocked()) { - world->activate(door, actor); + MWBase::Environment::get().getLuaManager()->objectActivated(door, actor); return; } - const std::string keyId = door.getCellRef().getKey(); + const ESM::RefId& keyId = door.getCellRef().getKey(); if (keyId.empty()) return; - MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); + MWWorld::ContainerStore& invStore = actor.getClass().getContainerStore(actor); MWWorld::Ptr keyPtr = invStore.search(keyId); if (!keyPtr.isEmpty()) - world->activate(door, actor); + MWBase::Environment::get().getLuaManager()->objectActivated(door, actor); } } -const MWMechanics::PathgridGraph& MWMechanics::AiPackage::getPathGridGraph(const MWWorld::CellStore *cell) +const MWMechanics::PathgridGraph& MWMechanics::AiPackage::getPathGridGraph(const ESM::Pathgrid* pathgrid) const { - const ESM::CellId& id = cell->getCell()->getCellId(); + if (!pathgrid || pathgrid->mPoints.empty()) + return PathgridGraph::sEmpty; // static cache is OK for now, pathgrids can never change during runtime - typedef std::map > CacheMap; - static CacheMap cache; - CacheMap::iterator found = cache.find(id); + static std::map> cache; + auto found = cache.find(pathgrid); if (found == cache.end()) - { - cache.insert(std::make_pair(id, std::make_unique(MWMechanics::PathgridGraph(cell)))); - } - return *cache[id].get(); + found = cache.emplace(pathgrid, std::make_unique(*pathgrid)).first; + return *found->second.get(); } bool MWMechanics::AiPackage::shortcutPath(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear) + const MWWorld::Ptr& actor, bool* destInLOS, bool isPathClear) { if (!mShortcutProhibited || (mShortcutFailPos - startPoint).length() >= PATHFIND_SHORTCUT_RETRY_DIST) { // check if target is clearly visible - isPathClear = !MWBase::Environment::get().getWorld()->castRay( - startPoint.x(), startPoint.y(), startPoint.z(), - endPoint.x(), endPoint.y(), endPoint.z()); + isPathClear + = !MWBase::Environment::get() + .getWorld() + ->getRayCasting() + ->castRay(startPoint, endPoint, MWPhysics::CollisionType_World | MWPhysics::CollisionType_Door) + .mHit; - if (destInLOS != nullptr) *destInLOS = isPathClear; + if (destInLOS != nullptr) + *destInLOS = isPathClear; if (!isPathClear) return false; @@ -330,16 +397,18 @@ bool MWMechanics::AiPackage::shortcutPath(const osg::Vec3f& startPoint, const os return false; } -bool MWMechanics::AiPackage::checkWayIsClearForActor(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor) +bool MWMechanics::AiPackage::checkWayIsClearForActor( + const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor) { if (canActorMoveByZAxis(actor)) return true; const float actorSpeed = actor.getClass().getMaxSpeed(actor); - const float maxAvoidDist = AI_REACTION_TIME * actorSpeed + actorSpeed / getAngularVelocity(actorSpeed) * 2; // *2 - for reliability + const float maxAvoidDist + = AI_REACTION_TIME * actorSpeed + actorSpeed / getAngularVelocity(actorSpeed) * 2; // *2 - for reliability const float distToTarget = osg::Vec2f(endPoint.x(), endPoint.y()).length(); - const float offsetXY = distToTarget > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2; + const float offsetXY = distToTarget > maxAvoidDist * 1.5 ? maxAvoidDist : maxAvoidDist / 2; // update shortcut prohibit state if (checkWayIsClear(startPoint, endPoint, offsetXY)) @@ -365,26 +434,25 @@ bool MWMechanics::AiPackage::checkWayIsClearForActor(const osg::Vec3f& startPoin bool MWMechanics::AiPackage::doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::Ptr& actor) const { - return mPathFinder.getPath().empty() - || getPathDistance(actor, mPathFinder.getPath().back(), newDest) > 10 + return mPathFinder.getPath().empty() || getPathDistance(actor, mPathFinder.getPath().back(), newDest) > 10 || mPathFinder.getPathCell() != actor.getCell(); } bool MWMechanics::AiPackage::isNearInactiveCell(osg::Vec3f position) { - const ESM::Cell* playerCell(getPlayer().getCell()->getCell()); + const MWWorld::Cell* playerCell = getPlayer().getCell()->getCell(); if (playerCell->isExterior()) { // get actor's distance from origin of center cell - Misc::CoordinateConverter(playerCell).toLocal(position); + Misc::makeCoordinateConverter(*playerCell).toLocal(position); // currently assumes 3 x 3 grid for exterior cells, with player at center cell. // AI shuts down actors before they reach edges of 3 x 3 grid. const float distanceFromEdge = 200.0; float minThreshold = (-1.0f * ESM::Land::REAL_SIZE) + distanceFromEdge; float maxThreshold = (2.0f * ESM::Land::REAL_SIZE) - distanceFromEdge; - return (position.x() < minThreshold) || (maxThreshold < position.x()) - || (position.y() < minThreshold) || (maxThreshold < position.y()); + return (position.x() < minThreshold) || (maxThreshold < position.x()) || (position.y() < minThreshold) + || (maxThreshold < position.y()); } else { @@ -422,48 +490,59 @@ bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& act DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld::Ptr& actor) const { - static const bool allowToFollowOverWaterSurface = Settings::Manager::getBool("allow actors to follow over water surface", "Game"); - const MWWorld::Class& actorClass = actor.getClass(); DetourNavigator::Flags result = DetourNavigator::Flag_none; if ((actorClass.isPureWaterCreature(actor) - || (getTypeId() != AiPackageTypeId::Wander - && ((allowToFollowOverWaterSurface && getTypeId() == AiPackageTypeId::Follow) - || actorClass.canSwim(actor) - || hasWaterWalking(actor))) - ) && actorClass.getSwimSpeed(actor) > 0) + || (getTypeId() != AiPackageTypeId::Wander + && ((Settings::game().mAllowActorsToFollowOverWaterSurface && getTypeId() == AiPackageTypeId::Follow) + || actorClass.canSwim(actor) || hasWaterWalking(actor)))) + && actorClass.getSwimSpeed(actor) > 0) result |= DetourNavigator::Flag_swim; if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) - result |= DetourNavigator::Flag_walk; + result |= DetourNavigator::Flag_walk | DetourNavigator::Flag_usePathgrid; - if (actorClass.isBipedal(actor) && getTypeId() != AiPackageTypeId::Wander) + if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander) result |= DetourNavigator::Flag_openDoor; return result; } -DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts(const MWWorld::Ptr& actor) const +DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts( + const MWWorld::Ptr& actor, DetourNavigator::Flags flags) const { DetourNavigator::AreaCosts costs; - const DetourNavigator::Flags flags = getNavigatorFlags(actor); const MWWorld::Class& actorClass = actor.getClass(); - if (flags & DetourNavigator::Flag_swim) - costs.mWater = divOrMax(costs.mWater, actorClass.getSwimSpeed(actor)); - - if (flags & DetourNavigator::Flag_walk) - { - float walkCost; + const float walkSpeed = [&] { + if ((flags & DetourNavigator::Flag_walk) == 0) + return 0.0f; if (getTypeId() == AiPackageTypeId::Wander) - walkCost = divOrMax(1.0, actorClass.getWalkSpeed(actor)); - else - walkCost = divOrMax(1.0, actorClass.getRunSpeed(actor)); - costs.mDoor = costs.mDoor * walkCost; - costs.mPathgrid = costs.mPathgrid * walkCost; - costs.mGround = costs.mGround * walkCost; - } + return actorClass.getWalkSpeed(actor); + return actorClass.getRunSpeed(actor); + }(); + + const float swimSpeed = [&] { + if ((flags & DetourNavigator::Flag_swim) == 0) + return 0.0f; + if (hasWaterWalking(actor)) + return walkSpeed; + return actorClass.getSwimSpeed(actor); + }(); + + const float maxSpeed = std::max(swimSpeed, walkSpeed); + + if (maxSpeed == 0) + return costs; + + const float swimFactor = swimSpeed / maxSpeed; + const float walkFactor = walkSpeed / maxSpeed; + + costs.mWater = divOrMax(costs.mWater, swimFactor); + costs.mDoor = divOrMax(costs.mDoor, walkFactor); + costs.mPathgrid = divOrMax(costs.mPathgrid, walkFactor); + costs.mGround = divOrMax(costs.mGround, walkFactor); return costs; } @@ -473,9 +552,10 @@ osg::Vec3f MWMechanics::AiPackage::getNextPathPoint(const osg::Vec3f& destinatio return mPathFinder.getPath().empty() ? destination : mPathFinder.getPath().front(); } -float MWMechanics::AiPackage::getNextPathPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) const +float MWMechanics::AiPackage::getNextPathPointTolerance( + float speed, float duration, const osg::Vec3f& halfExtents) const { if (mPathFinder.getPathSize() <= 1) - return mLastDestinationTolerance; + return std::max(DEFAULT_TOLERANCE, mLastDestinationTolerance); return getPointTolerance(speed, duration, halfExtents); } diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 6d8af0d92b0..ca33f5dc90e 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -5,16 +5,13 @@ #include -#include "pathfinding.hpp" -#include "obstacle.hpp" -#include "aistate.hpp" #include "aipackagetypeid.hpp" +#include "aistatefwd.hpp" #include "aitimer.hpp" +#include "obstacle.hpp" +#include "pathfinding.hpp" -namespace MWWorld -{ - class Ptr; -} +#include "../mwworld/ptr.hpp" namespace ESM { @@ -25,7 +22,6 @@ namespace ESM } } - namespace MWMechanics { class CharacterController; @@ -34,148 +30,157 @@ namespace MWMechanics /// \brief Base class for AI packages class AiPackage { - public: - struct Options + public: + struct Options + { + unsigned int mPriority = 0; + bool mUseVariableSpeed = false; + bool mSideWithTarget = false; + bool mFollowTargetThroughDoors = false; + bool mCanCancel = true; + bool mShouldCancelPreviousAi = true; + bool mRepeat = false; + bool mAlwaysActive = false; + + constexpr Options withRepeat(bool value) { - unsigned int mPriority = 0; - bool mUseVariableSpeed = false; - bool mSideWithTarget = false; - bool mFollowTargetThroughDoors = false; - bool mCanCancel = true; - bool mShouldCancelPreviousAi = true; - bool mRepeat = false; - bool mAlwaysActive = false; - - constexpr Options withRepeat(bool value) - { - mRepeat = value; - return *this; - } - - constexpr Options withShouldCancelPreviousAi(bool value) - { - mShouldCancelPreviousAi = value; - return *this; - } - }; - - AiPackage(AiPackageTypeId typeId, const Options& options); - - virtual ~AiPackage() = default; - - static constexpr Options makeDefaultOptions() + mRepeat = value; + return *this; + } + + constexpr Options withShouldCancelPreviousAi(bool value) { - return Options{}; + mShouldCancelPreviousAi = value; + return *this; } + }; + + AiPackage(AiPackageTypeId typeId, const Options& options); + + virtual ~AiPackage() = default; + + static constexpr Options makeDefaultOptions() { return Options{}; } - ///Clones the package - virtual std::unique_ptr clone() const = 0; + /// Clones the package + virtual std::unique_ptr clone() const = 0; - /// Updates and runs the package (Should run every frame) - /// \return Package completed? - virtual bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) = 0; + /// Updates and runs the package (Should run every frame) + /// \return Package completed? + virtual bool execute( + const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) + = 0; - /// Returns the TypeID of the AiPackage - /// \see enum TypeId - AiPackageTypeId getTypeId() const { return mTypeId; } + /// Returns the TypeID of the AiPackage + /// \see enum TypeId + AiPackageTypeId getTypeId() const { return mTypeId; } - /// Higher number is higher priority (0 being the lowest) - unsigned int getPriority() const { return mOptions.mPriority; } + /// Higher number is higher priority (0 being the lowest) + unsigned int getPriority() const { return mOptions.mPriority; } - /// Check if package use movement with variable speed - bool useVariableSpeed() const { return mOptions.mUseVariableSpeed; } + /// Check if package use movement with variable speed + bool useVariableSpeed() const { return mOptions.mUseVariableSpeed; } - virtual void writeState (ESM::AiSequence::AiSequence& sequence) const {} + virtual void writeState(ESM::AiSequence::AiSequence& sequence) const {} - /// Simulates the passing of time - virtual void fastForward(const MWWorld::Ptr& actor, AiState& state) {} + /// Simulates the passing of time + virtual void fastForward(const MWWorld::Ptr& actor, AiState& state) {} - /// Get the target actor the AI is targeted at (not applicable to all AI packages, default return empty Ptr) - virtual MWWorld::Ptr getTarget() const; + /// Get the target actor the AI is targeted at (not applicable to all AI packages, default return empty Ptr) + virtual MWWorld::Ptr getTarget() const; + /// Optimized version of getTarget() == ptr + virtual bool targetIs(const MWWorld::Ptr& ptr) const; - /// Get the destination point of the AI package (not applicable to all AI packages, default return (0, 0, 0)) - virtual osg::Vec3f getDestination(const MWWorld::Ptr& actor) const { return osg::Vec3f(0, 0, 0); }; + /// Get the destination point of the AI package (not applicable to all AI packages, default return (0, 0, 0)) + virtual osg::Vec3f getDestination(const MWWorld::Ptr& actor) const { return osg::Vec3f(0, 0, 0); } - /// Return true if having this AiPackage makes the actor side with the target in fights (default false) - bool sideWithTarget() const { return mOptions.mSideWithTarget; } + /// Return true if having this AiPackage makes the actor side with the target in fights (default false) + bool sideWithTarget() const { return mOptions.mSideWithTarget; } - /// Return true if the actor should follow the target through teleport doors (default false) - bool followTargetThroughDoors() const { return mOptions.mFollowTargetThroughDoors; } + /// Return true if the actor should follow the target through teleport doors (default false) + bool followTargetThroughDoors() const { return mOptions.mFollowTargetThroughDoors; } - /// Can this Ai package be canceled? (default true) - bool canCancel() const { return mOptions.mCanCancel; } + /// Can this Ai package be canceled? (default true) + bool canCancel() const { return mOptions.mCanCancel; } - /// Upon adding this Ai package, should the Ai Sequence attempt to cancel previous Ai packages (default true)? - bool shouldCancelPreviousAi() const { return mOptions.mShouldCancelPreviousAi; } + /// Upon adding this Ai package, should the Ai Sequence attempt to cancel previous Ai packages (default true)? + bool shouldCancelPreviousAi() const { return mOptions.mShouldCancelPreviousAi; } - /// Return true if this package should repeat. Currently only used for Wander packages. - bool getRepeat() const { return mOptions.mRepeat; } + /// Return true if this package should repeat. + bool getRepeat() const { return mOptions.mRepeat; } - virtual osg::Vec3f getDestination() const { return osg::Vec3f(0, 0, 0); } + virtual osg::Vec3f getDestination() const { return osg::Vec3f(0, 0, 0); } - /// Return true if any loaded actor with this AI package must be active. - bool alwaysActive() const { return mOptions.mAlwaysActive; } + virtual std::optional getDistance() const { return std::nullopt; } - /// Reset pathfinding state - void reset(); + virtual std::optional getDuration() const { return std::nullopt; } - /// Return if actor's rotation speed is sufficient to rotate to the destination pathpoint on the run. Otherwise actor should rotate while standing. - static bool isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const osg::Vec3f& dest); + /// Return true if any loaded actor with this AI package must be active. + bool alwaysActive() const { return mOptions.mAlwaysActive; } - osg::Vec3f getNextPathPoint(const osg::Vec3f& destination) const; + /// Reset pathfinding state + void reset(); - float getNextPathPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) const; + /// Return if actor's rotation speed is sufficient to rotate to the destination pathpoint on the run. Otherwise + /// actor should rotate while standing. + static bool isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const osg::Vec3f& dest); - protected: - /// Handles path building and shortcutting with obstacles avoiding - /** \return If the actor has arrived at his destination **/ - bool pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance = 0.0f); + osg::Vec3f getNextPathPoint(const osg::Vec3f& destination) const; - /// Check if there aren't any obstacles along the path to make shortcut possible - /// If a shortcut is possible then path will be cleared and filled with the destination point. - /// \param destInLOS If not nullptr function will return ray cast check result - /// \return If can shortcut the path - bool shortcutPath(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor, - bool *destInLOS, bool isPathClear); + float getNextPathPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) const; - /// Check if the way to the destination is clear, taking into account actor speed - bool checkWayIsClearForActor(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor); + protected: + /// Handles path building and shortcutting with obstacles avoiding + /** \return If the actor has arrived at his destination **/ + bool pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, + MWWorld::MovementDirectionFlags supportedMovementDirections, float destTolerance = 0.0f, + float endTolerance = 0.0f, PathType pathType = PathType::Full); - bool doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::Ptr& actor) const; + /// Check if there aren't any obstacles along the path to make shortcut possible + /// If a shortcut is possible then path will be cleared and filled with the destination point. + /// \param destInLOS If not nullptr function will return ray cast check result + /// \return If can shortcut the path + bool shortcutPath(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor, + bool* destInLOS, bool isPathClear); - void evadeObstacles(const MWWorld::Ptr& actor); + /// Check if the way to the destination is clear, taking into account actor speed + bool checkWayIsClearForActor( + const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor); - void openDoors(const MWWorld::Ptr& actor); + bool doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::Ptr& actor) const; - const PathgridGraph& getPathGridGraph(const MWWorld::CellStore* cell); + void evadeObstacles(const MWWorld::Ptr& actor); - DetourNavigator::Flags getNavigatorFlags(const MWWorld::Ptr& actor) const; + void openDoors(const MWWorld::Ptr& actor); - DetourNavigator::AreaCosts getAreaCosts(const MWWorld::Ptr& actor) const; + const PathgridGraph& getPathGridGraph(const ESM::Pathgrid* pathgrid) const; - const AiPackageTypeId mTypeId; - const Options mOptions; + DetourNavigator::Flags getNavigatorFlags(const MWWorld::Ptr& actor) const; - // TODO: all this does not belong here, move into temporary storage - PathFinder mPathFinder; - ObstacleCheck mObstacleCheck; + DetourNavigator::AreaCosts getAreaCosts(const MWWorld::Ptr& actor, DetourNavigator::Flags flags) const; - AiReactionTimer mReaction; + const AiPackageTypeId mTypeId; + const Options mOptions; - std::string mTargetActorRefId; - mutable int mTargetActorId; + // TODO: all this does not belong here, move into temporary storage + PathFinder mPathFinder; + ObstacleCheck mObstacleCheck; - short mRotateOnTheRunChecks; // attempts to check rotation to the pathpoint on the run possibility + AiReactionTimer mReaction; - bool mIsShortcutting; // if shortcutting at the moment - bool mShortcutProhibited; // shortcutting may be prohibited after unsuccessful attempt - osg::Vec3f mShortcutFailPos; // position of last shortcut fail - float mLastDestinationTolerance = 0; + ESM::RefId mTargetActorRefId; + mutable int mTargetActorId; + mutable MWWorld::Ptr mCachedTarget; - private: - bool isNearInactiveCell(osg::Vec3f position); + short mRotateOnTheRunChecks; // attempts to check rotation to the pathpoint on the run possibility + + bool mIsShortcutting; // if shortcutting at the moment + bool mShortcutProhibited; // shortcutting may be prohibited after unsuccessful attempt + osg::Vec3f mShortcutFailPos; // position of last shortcut fail + float mLastDestinationTolerance = 0; + + private: + bool isNearInactiveCell(osg::Vec3f position); }; } #endif - diff --git a/apps/openmw/mwmechanics/aipackagetypeid.hpp b/apps/openmw/mwmechanics/aipackagetypeid.hpp index 2b9c4fe9c94..3c1df5df51c 100644 --- a/apps/openmw/mwmechanics/aipackagetypeid.hpp +++ b/apps/openmw/mwmechanics/aipackagetypeid.hpp @@ -3,7 +3,7 @@ namespace MWMechanics { - ///Enumerates the various AITypes available + /// Enumerates the various AITypes available enum class AiPackageTypeId { None = -1, diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index 05605529ad8..461db45133b 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -1,6 +1,6 @@ #include "aipursue.hpp" -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -9,80 +9,98 @@ #include "../mwworld/class.hpp" -#include "movement.hpp" +#include "actorutil.hpp" +#include "character.hpp" #include "creaturestats.hpp" -#include "combat.hpp" +#include "npcstats.hpp" namespace MWMechanics { -AiPursue::AiPursue(const MWWorld::Ptr& actor) -{ - mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); -} + AiPursue::AiPursue(const MWWorld::Ptr& actor) + { + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); + } -AiPursue::AiPursue(const ESM::AiSequence::AiPursue *pursue) -{ - mTargetActorId = pursue->mTargetActorId; -} + AiPursue::AiPursue(const ESM::AiSequence::AiPursue* pursue) + { + mTargetActorId = pursue->mTargetActorId; + } -bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) -{ - if(actor.getClass().getCreatureStats(actor).isDead()) - return true; + bool AiPursue::execute( + const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) + { + if (actor.getClass().getCreatureStats(actor).isDead()) + return true; - const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); //The target to follow + const MWWorld::Ptr target + = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); // The target to follow - // Stop if the target doesn't exist - // Really we should be checking whether the target is currently registered with the MechanicsManager - if (target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled()) - return true; + // Stop if the target doesn't exist + // Really we should be checking whether the target is currently registered with the MechanicsManager + if (target == MWWorld::Ptr() || !target.getCellRef().getCount() || !target.getRefData().isEnabled()) + return true; - if (isTargetMagicallyHidden(target) && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(target, actor)) - return false; + if (isTargetMagicallyHidden(target) + && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(target, actor)) + return false; - if (target.getClass().getCreatureStats(target).isDead()) - return true; + if (target.getClass().getCreatureStats(target).isDead()) + return true; - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + if (target.getClass().getNpcStats(target).getBounty() <= 0) + return true; - //Set the target destination - const osg::Vec3f dest = target.getRefData().getPosition().asVec3(); - const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); - const float pathTolerance = 100.f; + // Set the target destination + const osg::Vec3f dest = target.getRefData().getPosition().asVec3(); + const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); - // check the true distance in case the target is far away in Z-direction - bool reached = pathTo(actor, dest, duration, pathTolerance) && - std::abs(dest.z() - actorPos.z()) < pathTolerance; + const float pathTolerance = 100.f; - if (reached) - { - if (!MWBase::Environment::get().getWorld()->getLOS(target, actor)) - return false; - MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, actor); //Arrest player when reached - return true; - } + // check the true distance in case the target is far away in Z-direction + bool reached = pathTo(actor, dest, duration, characterController.getSupportedMovementDirections(), + pathTolerance, (actorPos - dest).length(), PathType::Partial) + && std::abs(dest.z() - actorPos.z()) < pathTolerance; - actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run + if (reached) + { + if (!MWBase::Environment::get().getWorld()->getLOS(target, actor)) + return false; + MWBase::Environment::get().getWindowManager()->pushGuiMode( + MWGui::GM_Dialogue, actor); // Arrest player when reached + return true; + } - return false; -} + actor.getClass().getCreatureStats(actor).setMovementFlag( + MWMechanics::CreatureStats::Flag_Run, true); // Make NPC run -MWWorld::Ptr AiPursue::getTarget() const -{ - return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); -} + return false; + } -void AiPursue::writeState(ESM::AiSequence::AiSequence &sequence) const -{ - std::unique_ptr pursue(new ESM::AiSequence::AiPursue()); - pursue->mTargetActorId = mTargetActorId; - - ESM::AiSequence::AiPackageContainer package; - package.mType = ESM::AiSequence::Ai_Pursue; - package.mPackage = pursue.release(); - sequence.mPackages.push_back(package); -} + MWWorld::Ptr AiPursue::getTarget() const + { + if (!mCachedTarget.isEmpty()) + { + if (mCachedTarget.mRef->isDeleted() || !mCachedTarget.getRefData().isEnabled()) + mCachedTarget = MWWorld::Ptr(); + else + return mCachedTarget; + } + mCachedTarget = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + return mCachedTarget; + } + + void AiPursue::writeState(ESM::AiSequence::AiSequence& sequence) const + { + auto pursue = std::make_unique(); + pursue->mTargetActorId = mTargetActorId; + + ESM::AiSequence::AiPackageContainer package; + package.mType = ESM::AiSequence::Ai_Pursue; + package.mPackage = std::move(pursue); + sequence.mPackages.push_back(std::move(package)); + } } // namespace MWMechanics diff --git a/apps/openmw/mwmechanics/aipursue.hpp b/apps/openmw/mwmechanics/aipursue.hpp index 2fbc13b87cb..d9cf4a40c3c 100644 --- a/apps/openmw/mwmechanics/aipursue.hpp +++ b/apps/openmw/mwmechanics/aipursue.hpp @@ -5,42 +5,43 @@ namespace ESM { -namespace AiSequence -{ - struct AiPursue; -} + namespace AiSequence + { + struct AiPursue; + } } namespace MWMechanics { /// \brief Makes the actor very closely follow the actor /** Used for arresting players. Causes the actor to run to the pursued actor and activate them, to arrest them. - Note that while very similar to AiActivate, it will ONLY activate when evry close to target (Not also when the + Note that while very similar to AiActivate, it will ONLY activate when very close to target (Not also when the path is completed). **/ class AiPursue final : public TypedAiPackage { - public: - ///Constructor - /** \param actor Actor to pursue **/ - AiPursue(const MWWorld::Ptr& actor); + public: + /// Constructor + /** \param actor Actor to pursue **/ + AiPursue(const MWWorld::Ptr& actor); - AiPursue(const ESM::AiSequence::AiPursue* pursue); + AiPursue(const ESM::AiSequence::AiPursue* pursue); - bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; + bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, + float duration) override; - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Pursue; } + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Pursue; } - static constexpr Options makeDefaultOptions() - { - AiPackage::Options options; - options.mCanCancel = false; - options.mShouldCancelPreviousAi = false; - return options; - } + static constexpr Options makeDefaultOptions() + { + AiPackage::Options options; + options.mCanCancel = false; + options.mShouldCancelPreviousAi = false; + return options; + } - MWWorld::Ptr getTarget() const override; + MWWorld::Ptr getTarget() const override; - void writeState (ESM::AiSequence::AiSequence& sequence) const override; + void writeState(ESM::AiSequence::AiSequence& sequence) const override; }; } #endif diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index fada7761df9..019aaf7c0a4 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -1,216 +1,268 @@ #include "aisequence.hpp" +#include #include #include -#include +#include -#include "aipackage.hpp" -#include "aistate.hpp" -#include "aiwander.hpp" -#include "aiescort.hpp" -#include "aitravel.hpp" -#include "aifollow.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwworld/class.hpp" +#include "actorutil.hpp" #include "aiactivate.hpp" #include "aicombat.hpp" #include "aicombataction.hpp" +#include "aiescort.hpp" +#include "aifollow.hpp" +#include "aipackage.hpp" #include "aipursue.hpp" -#include "actorutil.hpp" -#include "../mwworld/class.hpp" +#include "aitravel.hpp" +#include "aiwander.hpp" +#include "creaturestats.hpp" namespace MWMechanics { -void AiSequence::copy (const AiSequence& sequence) -{ - for (const auto& package : sequence.mPackages) - mPackages.push_back(package->clone()); + void AiSequence::copy(const AiSequence& sequence) + { + for (const auto& package : sequence.mPackages) + mPackages.push_back(package->clone()); - // We need to keep an AiWander storage, if present - it has a state machine. - // Not sure about another temporary storages - sequence.mAiState.copy(mAiState); -} + // We need to keep an AiWander storage, if present - it has a state machine. + // Not sure about another temporary storages + sequence.mAiState.copy(mAiState); -AiSequence::AiSequence() : mDone (false), mRepeat(false), mLastAiPackage(AiPackageTypeId::None) {} + mNumCombatPackages = sequence.mNumCombatPackages; + mNumPursuitPackages = sequence.mNumPursuitPackages; + } -AiSequence::AiSequence (const AiSequence& sequence) -{ - copy (sequence); - mDone = sequence.mDone; - mLastAiPackage = sequence.mLastAiPackage; - mRepeat = sequence.mRepeat; -} + AiSequence::AiSequence() + : mDone(false) + , mLastAiPackage(AiPackageTypeId::None) + { + } -AiSequence& AiSequence::operator= (const AiSequence& sequence) -{ - if (this!=&sequence) + AiSequence::AiSequence(const AiSequence& sequence) { - clear(); - copy (sequence); + copy(sequence); mDone = sequence.mDone; mLastAiPackage = sequence.mLastAiPackage; } - return *this; -} + AiSequence& AiSequence::operator=(const AiSequence& sequence) + { + if (this != &sequence) + { + clear(); + copy(sequence); + mDone = sequence.mDone; + mLastAiPackage = sequence.mLastAiPackage; + } -AiSequence::~AiSequence() -{ - clear(); -} + return *this; + } -AiPackageTypeId AiSequence::getTypeId() const -{ - if (mPackages.empty()) - return AiPackageTypeId::None; + AiSequence::~AiSequence() + { + clear(); + } - return mPackages.front()->getTypeId(); -} + void AiSequence::onPackageAdded(const AiPackage& package) + { + if (package.getTypeId() == AiPackageTypeId::Combat) + mNumCombatPackages++; + else if (package.getTypeId() == AiPackageTypeId::Pursue) + mNumPursuitPackages++; -bool AiSequence::getCombatTarget(MWWorld::Ptr &targetActor) const -{ - if (getTypeId() != AiPackageTypeId::Combat) - return false; + assert(mNumCombatPackages >= 0); + assert(mNumPursuitPackages >= 0); + } - targetActor = mPackages.front()->getTarget(); + void AiSequence::onPackageRemoved(const AiPackage& package) + { + if (package.getTypeId() == AiPackageTypeId::Combat) + { + mNumCombatPackages--; + if (mNumCombatPackages == 0) + mResetFriendlyHits = true; + } + else if (package.getTypeId() == AiPackageTypeId::Pursue) + mNumPursuitPackages--; - return !targetActor.isEmpty(); -} + assert(mNumCombatPackages >= 0); + assert(mNumPursuitPackages >= 0); + } -bool AiSequence::getCombatTargets(std::vector &targetActors) const -{ - for (auto it = mPackages.begin(); it != mPackages.end(); ++it) + AiPackageTypeId AiSequence::getTypeId() const { - if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Combat) - targetActors.push_back((*it)->getTarget()); + if (mPackages.empty()) + return AiPackageTypeId::None; + + return mPackages.front()->getTypeId(); } - return !targetActors.empty(); -} + bool AiSequence::getCombatTarget(MWWorld::Ptr& targetActor) const + { + if (getTypeId() != AiPackageTypeId::Combat) + return false; -std::list>::const_iterator AiSequence::begin() const -{ - return mPackages.begin(); -} + targetActor = mPackages.front()->getTarget(); -std::list>::const_iterator AiSequence::end() const -{ - return mPackages.end(); -} + return !targetActor.isEmpty(); + } -void AiSequence::erase(std::list>::const_iterator package) -{ - // Not sure if manually terminated packages should trigger mDone, probably not? - for(auto it = mPackages.begin(); it != mPackages.end(); ++it) + bool AiSequence::getCombatTargets(std::vector& targetActors) const { - if (package == it) + for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { - mPackages.erase(it); - return; + if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Combat) + targetActors.push_back((*it)->getTarget()); } + + return !targetActors.empty(); } - throw std::runtime_error("can't find package to erase"); -} -bool AiSequence::isInCombat() const -{ - for (auto it = mPackages.begin(); it != mPackages.end(); ++it) + AiPackages::iterator AiSequence::erase(AiPackages::iterator package) { - if ((*it)->getTypeId() == AiPackageTypeId::Combat) - return true; + // Not sure if manually terminated packages should trigger mDone, probably not? + auto& ptr = *package; + onPackageRemoved(*ptr); + + return mPackages.erase(package); } - return false; -} -bool AiSequence::isEngagedWithActor() const -{ - for (auto it = mPackages.begin(); it != mPackages.end(); ++it) + bool AiSequence::isInCombat() const + { + return mNumCombatPackages > 0; + } + + bool AiSequence::isInPursuit() const { - if ((*it)->getTypeId() == AiPackageTypeId::Combat) + return mNumPursuitPackages > 0; + } + + bool AiSequence::isFleeing() const + { + if (!isInCombat()) + return false; + + const AiCombatStorage* storage = mAiState.getPtr(); + return storage && storage->isFleeing(); + } + + bool AiSequence::isEngagedWithActor() const + { + if (!isInCombat()) + return false; + + for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { - MWWorld::Ptr target2 = (*it)->getTarget(); - if (!target2.isEmpty() && target2.getClass().isNpc()) - return true; + if ((*it)->getTypeId() == AiPackageTypeId::Combat) + { + MWWorld::Ptr target2 = (*it)->getTarget(); + if (!target2.isEmpty() && target2.getClass().isNpc()) + return true; + } } + return false; } - return false; -} -bool AiSequence::hasPackage(AiPackageTypeId typeId) const -{ - for (auto it = mPackages.begin(); it != mPackages.end(); ++it) + bool AiSequence::hasPackage(AiPackageTypeId typeId) const { - if ((*it)->getTypeId() == typeId) - return true; + auto it = std::find_if(mPackages.begin(), mPackages.end(), + [typeId](const auto& package) { return package->getTypeId() == typeId; }); + return it != mPackages.end(); } - return false; -} -bool AiSequence::isInCombat(const MWWorld::Ptr &actor) const -{ - for (auto it = mPackages.begin(); it != mPackages.end(); ++it) + bool AiSequence::isInCombat(const MWWorld::Ptr& actor) const { - if ((*it)->getTypeId() == AiPackageTypeId::Combat) + if (!isInCombat()) + return false; + + for (const auto& package : mPackages) { - if ((*it)->getTarget() == actor) - return true; + if (package->getTypeId() == AiPackageTypeId::Combat) + { + if (package->targetIs(actor)) + return true; + } } + return false; } - return false; -} -void AiSequence::stopCombat() -{ - for(auto it = mPackages.begin(); it != mPackages.end(); ) + void AiSequence::removePackagesById(AiPackageTypeId id) { - if ((*it)->getTypeId() == AiPackageTypeId::Combat) + for (auto it = mPackages.begin(); it != mPackages.end();) { - it = mPackages.erase(it); + if ((*it)->getTypeId() == id) + { + it = erase(it); + } + else + ++it; } - else - ++it; } -} -void AiSequence::stopPursuit() -{ - for(auto it = mPackages.begin(); it != mPackages.end(); ) + void AiSequence::stopCombat() + { + removePackagesById(AiPackageTypeId::Combat); + } + + void AiSequence::stopCombat(const std::vector& targets) { - if ((*it)->getTypeId() == AiPackageTypeId::Pursue) + for (auto it = mPackages.begin(); it != mPackages.end();) { - it = mPackages.erase(it); + if ((*it)->getTypeId() == AiPackageTypeId::Combat + && std::find(targets.begin(), targets.end(), (*it)->getTarget()) != targets.end()) + { + it = erase(it); + } + else + ++it; } - else - ++it; } -} -bool AiSequence::isPackageDone() const -{ - return mDone; -} + void AiSequence::stopPursuit() + { + removePackagesById(AiPackageTypeId::Pursue); + } -namespace -{ - bool isActualAiPackage(AiPackageTypeId packageTypeId) + bool AiSequence::isPackageDone() const { - return (packageTypeId >= AiPackageTypeId::Wander && - packageTypeId <= AiPackageTypeId::Activate); + return mDone; } -} -void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange) -{ - if(actor != getPlayer()) + namespace { + bool isActualAiPackage(AiPackageTypeId packageTypeId) + { + return (packageTypeId >= AiPackageTypeId::Wander && packageTypeId <= AiPackageTypeId::Activate); + } + } + + void AiSequence::execute( + const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange) + { + if (actor == getPlayer()) + { + // Players don't use this. + return; + } + + if (mResetFriendlyHits) + { + actor.getClass().getCreatureStats(actor).resetFriendlyHits(); + mResetFriendlyHits = false; + } + if (mPackages.empty()) { mLastAiPackage = AiPackageTypeId::None; return; } - auto packageIt = mPackages.begin(); - MWMechanics::AiPackage* package = packageIt->get(); + auto* package = mPackages.front().get(); if (!package->alwaysActive() && outOfRange) return; @@ -230,20 +282,23 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac for (auto it = mPackages.begin(); it != mPackages.end();) { - if ((*it)->getTypeId() != AiPackageTypeId::Combat) break; + if ((*it)->getTypeId() != AiPackageTypeId::Combat) + break; MWWorld::Ptr target = (*it)->getTarget(); // target disappeared (e.g. summoned creatures) if (target.isEmpty()) { - it = mPackages.erase(it); + it = erase(it); } else { - float rating = MWMechanics::getBestActionRating(actor, target); + float rating = 0.f; + if (MWMechanics::canFight(actor, target)) + rating = MWMechanics::getBestActionRating(actor, target); - const ESM::Position &targetPos = target.getRefData().getPosition(); + const ESM::Position& targetPos = target.getRefData().getPosition(); float distTo = (targetPos.asVec3() - vActorPos).length2(); @@ -262,17 +317,17 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac } } - assert(!mPackages.empty()); + if (mPackages.empty()) + return; if (nearestDist < std::numeric_limits::max() && mPackages.begin() != itActualCombat) { assert(itActualCombat != mPackages.end()); // move combat package with nearest target to the front - mPackages.splice(mPackages.begin(), mPackages, itActualCombat); + std::rotate(mPackages.begin(), itActualCombat, std::next(itActualCombat)); } - packageIt = mPackages.begin(); - package = packageIt->get(); + package = mPackages.front().get(); packageTypeId = package->getTypeId(); } @@ -280,15 +335,21 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac { if (package->execute(actor, characterController, mAiState, duration)) { - // Put repeating noncombat AI packages on the end of the stack so they can be used again - if (isActualAiPackage(packageTypeId) && (mRepeat || package->getRepeat())) + // Put repeating non-combat AI packages on the end of the stack so they can be used again + if (isActualAiPackage(packageTypeId) && package->getRepeat()) { package->reset(); mPackages.push_back(package->clone()); } - // To account for the rare case where AiPackage::execute() queued another AI package - // (e.g. AiPursue executing a dialogue script that uses startCombat) - mPackages.erase(packageIt); + + // The active package is typically the first entry, this is however not always the case + // e.g. AiPursue executing a dialogue script that uses startCombat adds a combat package to the front + // due to the priority. + auto activePackageIt = std::find_if( + mPackages.begin(), mPackages.end(), [&](auto& entry) { return entry.get() == package; }); + + erase(activePackageIt); + if (isActualAiPackage(packageTypeId)) mDone = true; } @@ -302,239 +363,246 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac Log(Debug::Error) << "Error during AiSequence::execute: " << e.what(); } } -} -void AiSequence::clear() -{ - mPackages.clear(); -} + void AiSequence::clear() + { + mPackages.clear(); + mNumCombatPackages = 0; + mNumPursuitPackages = 0; + } -void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther) -{ - if (actor == getPlayer()) - throw std::runtime_error("Can't add AI packages to player"); - - // Stop combat when a non-combat AI package is added - if (isActualAiPackage(package.getTypeId())) - stopCombat(); - - // We should return a wandering actor back after combat, casting or pursuit. - // The same thing for actors without AI packages. - // Also there is no point to stack return packages. - const auto currentTypeId = getTypeId(); - 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::Cast)) - { - osg::Vec3f dest; - if (currentTypeId == MWMechanics::AiPackageTypeId::Wander) + void AiSequence::stack(const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther) + { + if (actor == getPlayer()) + throw std::runtime_error("Can't add AI packages to player"); + + // Stop combat when a non-combat AI package is added + if (isActualAiPackage(package.getTypeId())) { - dest = getActivePackage().getDestination(actor); + if (package.getTypeId() == MWMechanics::AiPackageTypeId::Follow + || package.getTypeId() == MWMechanics::AiPackageTypeId::Escort) + { + const auto& mechanicsManager = MWBase::Environment::get().getMechanicsManager(); + std::vector newAllies = mechanicsManager->getActorsSidingWith(package.getTarget()); + std::vector allies = mechanicsManager->getActorsSidingWith(actor); + for (const auto& ally : allies) + ally.getClass().getCreatureStats(ally).getAiSequence().stopCombat(newAllies); + for (const auto& ally : newAllies) + ally.getClass().getCreatureStats(ally).getAiSequence().stopCombat(allies); + } + stopCombat(); } - else + + // We should return a wandering actor back after combat, casting or pursuit. + // The same thing for actors without AI packages. + // Also there is no point to stack return packages. + const auto currentTypeId = getTypeId(); + 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::Cast)) { - dest = actor.getRefData().getPosition().asVec3(); - } + osg::Vec3f dest; + if (currentTypeId == MWMechanics::AiPackageTypeId::Wander) + { + dest = getActivePackage().getDestination(actor); + } + else + { + dest = actor.getRefData().getPosition().asVec3(); + } - MWMechanics::AiInternalTravel travelPackage(dest.x(), dest.y(), dest.z()); - stack(travelPackage, actor, false); - } + MWMechanics::AiInternalTravel travelPackage(dest.x(), dest.y(), dest.z()); + stack(travelPackage, actor, false); + } - // remove previous packages if required - if (cancelOther && package.shouldCancelPreviousAi()) - { - for (auto it = mPackages.begin(); it != mPackages.end();) + // remove previous packages if required + if (cancelOther && package.shouldCancelPreviousAi()) { - if((*it)->canCancel()) + for (auto it = mPackages.begin(); it != mPackages.end();) { - it = mPackages.erase(it); + if ((*it)->canCancel()) + { + it = erase(it); + } + else + ++it; } - else - ++it; } - mRepeat=false; - } - // insert new package in correct place depending on priority - for (auto it = mPackages.begin(); it != mPackages.end(); ++it) - { - // We should keep current AiCast package, if we try to add a new one. - if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Cast && - package.getTypeId() == MWMechanics::AiPackageTypeId::Cast) + // insert new package in correct place depending on priority + for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { - continue; + // We should override current AiCast package, if we try to add a new one. + if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Cast + && package.getTypeId() == MWMechanics::AiPackageTypeId::Cast) + { + *it = package.clone(); + return; + } + + if ((*it)->getPriority() <= package.getPriority()) + { + onPackageAdded(package); + mPackages.insert(it, package.clone()); + return; + } } - if((*it)->getPriority() <= package.getPriority()) + onPackageAdded(package); + mPackages.push_back(package.clone()); + + // Make sure that temporary storage is empty + if (cancelOther) { - mPackages.insert(it, package.clone()); - return; + mAiState.moveIn(std::make_unique()); + mAiState.moveIn(std::make_unique()); + mAiState.moveIn(std::make_unique()); } } - mPackages.push_back(package.clone()); - - // Make sure that temporary storage is empty - if (cancelOther) + bool MWMechanics::AiSequence::isEmpty() const { - mAiState.moveIn(new AiCombatStorage()); - mAiState.moveIn(new AiFollowStorage()); - mAiState.moveIn(new AiWanderStorage()); + return mPackages.empty(); } -} -bool MWMechanics::AiSequence::isEmpty() const -{ - return mPackages.empty(); -} - -const AiPackage& MWMechanics::AiSequence::getActivePackage() -{ - if(mPackages.empty()) - throw std::runtime_error(std::string("No AI Package!")); - return *mPackages.front(); -} - -void AiSequence::fill(const ESM::AIPackageList &list) -{ - // If there is more than one package in the list, enable repeating - if (list.mList.size() >= 2) - mRepeat = true; + const AiPackage& MWMechanics::AiSequence::getActivePackage() const + { + if (mPackages.empty()) + throw std::runtime_error(std::string("No AI Package!")); + return *mPackages.front(); + } - for (const auto& esmPackage : list.mList) + void AiSequence::fill(const ESM::AIPackageList& list) { - std::unique_ptr package; - if (esmPackage.mType == ESM::AI_Wander) - { - ESM::AIWander data = esmPackage.mWander; - std::vector idles; - idles.reserve(8); - for (int i=0; i<8; ++i) - idles.push_back(data.mIdle[i]); - package = std::make_unique(data.mDistance, data.mDuration, data.mTimeOfDay, idles, data.mShouldRepeat != 0); - } - else if (esmPackage.mType == ESM::AI_Escort) - { - ESM::AITarget data = esmPackage.mTarget; - package = std::make_unique(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ); - } - else if (esmPackage.mType == ESM::AI_Travel) - { - ESM::AITravel data = esmPackage.mTravel; - package = std::make_unique(data.mX, data.mY, data.mZ); - } - else if (esmPackage.mType == ESM::AI_Activate) + for (const auto& esmPackage : list.mList) { - ESM::AIActivate data = esmPackage.mActivate; - package = std::make_unique(data.mName.toString()); - } - else //if (esmPackage.mType == ESM::AI_Follow) - { - ESM::AITarget data = esmPackage.mTarget; - package = std::make_unique(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ); + std::unique_ptr package; + if (esmPackage.mType == ESM::AI_Wander) + { + ESM::AIWander data = esmPackage.mWander; + std::vector idles; + idles.reserve(8); + for (int i = 0; i < 8; ++i) + idles.push_back(data.mIdle[i]); + package = std::make_unique( + data.mDistance, data.mDuration, data.mTimeOfDay, idles, data.mShouldRepeat != 0); + } + else if (esmPackage.mType == ESM::AI_Escort) + { + ESM::AITarget data = esmPackage.mTarget; + package = std::make_unique(ESM::RefId::stringRefId(data.mId.toStringView()), + esmPackage.mCellName, data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); + } + else if (esmPackage.mType == ESM::AI_Travel) + { + ESM::AITravel data = esmPackage.mTravel; + package = std::make_unique(data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); + } + else if (esmPackage.mType == ESM::AI_Activate) + { + ESM::AIActivate data = esmPackage.mActivate; + package = std::make_unique( + ESM::RefId::stringRefId(data.mName.toStringView()), data.mShouldRepeat != 0); + } + else // if (esmPackage.mType == ESM::AI_Follow) + { + ESM::AITarget data = esmPackage.mTarget; + package = std::make_unique(ESM::RefId::stringRefId(data.mId.toStringView()), + esmPackage.mCellName, data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); + } + + onPackageAdded(*package); + mPackages.push_back(std::move(package)); } - mPackages.push_back(std::move(package)); } -} -void AiSequence::writeState(ESM::AiSequence::AiSequence &sequence) const -{ - for (const auto& package : mPackages) - package->writeState(sequence); - - sequence.mLastAiPackage = static_cast(mLastAiPackage); -} + void AiSequence::writeState(ESM::AiSequence::AiSequence& sequence) const + { + for (const auto& package : mPackages) + package->writeState(sequence); -void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) -{ - if (!sequence.mPackages.empty()) - clear(); + sequence.mLastAiPackage = static_cast(mLastAiPackage); + } - // If there is more than one non-combat, non-pursue package in the list, enable repeating. - int count = 0; - for (auto& container : sequence.mPackages) + void AiSequence::readState(const ESM::AiSequence::AiSequence& sequence) { - switch (container.mType) + if (!sequence.mPackages.empty()) + clear(); + + // Load packages + for (auto& container : sequence.mPackages) { - case ESM::AiSequence::Ai_Wander: - case ESM::AiSequence::Ai_Travel: - case ESM::AiSequence::Ai_Escort: - case ESM::AiSequence::Ai_Follow: - case ESM::AiSequence::Ai_Activate: - ++count; - } - } + std::unique_ptr package; + switch (container.mType) + { + case ESM::AiSequence::Ai_Wander: + { + package = std::make_unique( + &static_cast(*container.mPackage)); + break; + } + case ESM::AiSequence::Ai_Travel: + { + const ESM::AiSequence::AiTravel& source + = static_cast(*container.mPackage); + if (source.mHidden) + package = std::make_unique(&source); + else + package = std::make_unique(&source); + break; + } + case ESM::AiSequence::Ai_Escort: + { + package = std::make_unique( + &static_cast(*container.mPackage)); + break; + } + case ESM::AiSequence::Ai_Follow: + { + package = std::make_unique( + &static_cast(*container.mPackage)); + break; + } + case ESM::AiSequence::Ai_Activate: + { + package = std::make_unique( + &static_cast(*container.mPackage)); + break; + } + case ESM::AiSequence::Ai_Combat: + { + package = std::make_unique( + &static_cast(*container.mPackage)); + break; + } + case ESM::AiSequence::Ai_Pursue: + { + package = std::make_unique( + &static_cast(*container.mPackage)); + break; + } + default: + break; + } - if (count > 1) - mRepeat = true; + if (!package.get()) + continue; - // Load packages - for (auto& container : sequence.mPackages) - { - std::unique_ptr package; - switch (container.mType) - { - case ESM::AiSequence::Ai_Wander: - { - package.reset(new AiWander(static_cast(container.mPackage))); - break; - } - case ESM::AiSequence::Ai_Travel: - { - const auto source = static_cast(container.mPackage); - if (source->mHidden) - package.reset(new AiInternalTravel(source)); - else - package.reset(new AiTravel(source)); - break; + onPackageAdded(*package); + mPackages.push_back(std::move(package)); } - case ESM::AiSequence::Ai_Escort: - { - package.reset(new AiEscort(static_cast(container.mPackage))); - break; - } - case ESM::AiSequence::Ai_Follow: - { - package.reset(new AiFollow(static_cast(container.mPackage))); - break; - } - case ESM::AiSequence::Ai_Activate: - { - package.reset(new AiActivate(static_cast(container.mPackage))); - break; - } - case ESM::AiSequence::Ai_Combat: - { - package.reset(new AiCombat(static_cast(container.mPackage))); - break; - } - case ESM::AiSequence::Ai_Pursue: - { - package.reset(new AiPursue(static_cast(container.mPackage))); - break; - } - default: - break; - } - - if (!package.get()) - continue; - mPackages.push_back(std::move(package)); + mLastAiPackage = static_cast(sequence.mLastAiPackage); } - mLastAiPackage = static_cast(sequence.mLastAiPackage); -} - -void AiSequence::fastForward(const MWWorld::Ptr& actor) -{ - if (!mPackages.empty()) + void AiSequence::fastForward(const MWWorld::Ptr& actor) { - mPackages.front()->fastForward(actor, mAiState); + if (!mPackages.empty()) + { + mPackages.front()->fastForward(actor, mAiState); + } } -} } // namespace MWMechanics diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index 645524d3812..5e7e521a40e 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -1,13 +1,14 @@ #ifndef GAME_MWMECHANICS_AISEQUENCE_H #define GAME_MWMECHANICS_AISEQUENCE_H -#include +#include #include +#include -#include "aistate.hpp" #include "aipackagetypeid.hpp" +#include "aistate.hpp" -#include +#include namespace MWWorld { @@ -22,122 +23,156 @@ namespace ESM } } - - namespace MWMechanics { class AiPackage; class CharacterController; - - template< class Base > class DerivedClassStorage; - struct AiTemporaryBase; - typedef DerivedClassStorage AiState; + + using AiPackages = std::vector>; /// \brief Sequence of AI-packages for a single actor /** The top-most AI package is run each frame. When completed, it is removed from the stack. **/ class AiSequence { - ///AiPackages to run though - std::list> mPackages; + /// AiPackages to run though + AiPackages mPackages; + + /// Finished with top AIPackage, set for one frame + bool mDone{}; + bool mResetFriendlyHits{}; + + int mNumCombatPackages{}; + int mNumPursuitPackages{}; + + /// Copy AiSequence + void copy(const AiSequence& sequence); + + /// The type of AI package that ran last + AiPackageTypeId mLastAiPackage; + AiState mAiState; + + void onPackageAdded(const AiPackage& package); + void onPackageRemoved(const AiPackage& package); - ///Finished with top AIPackage, set for one frame - bool mDone; + AiPackages::iterator erase(AiPackages::iterator package); - ///Does this AI sequence repeat (repeating of Wander packages handled separately) - bool mRepeat; + public: + /// Default constructor + AiSequence(); - ///Copy AiSequence - void copy (const AiSequence& sequence); + /// Copy Constructor + AiSequence(const AiSequence& sequence); - /// The type of AI package that ran last - AiPackageTypeId mLastAiPackage; - AiState mAiState; + /// Assignment operator + AiSequence& operator=(const AiSequence& sequence); - public: - ///Default constructor - AiSequence(); + virtual ~AiSequence(); - /// Copy Constructor - AiSequence (const AiSequence& sequence); + /// Iterator may be invalidated by any function calls other than begin() or end(). + AiPackages::const_iterator begin() const { return mPackages.begin(); } + AiPackages::const_iterator end() const { return mPackages.end(); } - /// Assignment operator - AiSequence& operator= (const AiSequence& sequence); + /// Removes all packages controlled by the predicate. + template + void erasePackagesIf(const F&& pred) + { + mPackages.erase(std::remove_if(mPackages.begin(), mPackages.end(), + [&](auto& entry) { + const bool doRemove = pred(entry); + if (doRemove) + onPackageRemoved(*entry); + return doRemove; + }), + mPackages.end()); + } - virtual ~AiSequence(); + /// Removes a single package controlled by the predicate. + template + void erasePackageIf(const F&& pred) + { + auto it = std::find_if(mPackages.begin(), mPackages.end(), pred); + if (it == mPackages.end()) + return; + erase(it); + } - /// Iterator may be invalidated by any function calls other than begin() or end(). - std::list>::const_iterator begin() const; - std::list>::const_iterator end() const; + /// Returns currently executing AiPackage type + /** \see enum class AiPackageTypeId **/ + AiPackageTypeId getTypeId() const; - void erase(std::list>::const_iterator package); + /// Get the typeid of the Ai package that ran last + /** NOT the currently "active" Ai package that will be run in the next frame. + This difference is important when an Ai package has just finished and been removed. + \see enum class AiPackageTypeId **/ + AiPackageTypeId getLastRunTypeId() const { return mLastAiPackage; } - /// Returns currently executing AiPackage type - /** \see enum class AiPackageTypeId **/ - AiPackageTypeId getTypeId() const; + /// Return true and assign target if combat package is currently active, return false otherwise + bool getCombatTarget(MWWorld::Ptr& targetActor) const; - /// Get the typeid of the Ai package that ran last - /** NOT the currently "active" Ai package that will be run in the next frame. - This difference is important when an Ai package has just finished and been removed. - \see enum class AiPackageTypeId **/ - AiPackageTypeId getLastRunTypeId() const { return mLastAiPackage; } + /// Return true and assign targets for all combat packages, or return false if there are no combat packages + bool getCombatTargets(std::vector& targetActors) const; - /// Return true and assign target if combat package is currently active, return false otherwise - bool getCombatTarget (MWWorld::Ptr &targetActor) const; + /// Is there any combat package? + bool isInCombat() const; - /// Return true and assign targets for all combat packages, or return false if there are no combat packages - bool getCombatTargets(std::vector &targetActors) const; + /// Is there any pursuit package. + bool isInPursuit() const; - /// Is there any combat package? - bool isInCombat () const; + /// Is the actor fleeing? + bool isFleeing() const; - /// Are we in combat with any other actor, who's also engaging us? - bool isEngagedWithActor () const; + /// Removes all packages using the specified id. + void removePackagesById(AiPackageTypeId id); - /// Does this AI sequence have the given package type? - bool hasPackage(AiPackageTypeId typeId) const; + /// Are we in combat with any other actor, who's also engaging us? + bool isEngagedWithActor() const; - /// Are we in combat with this particular actor? - bool isInCombat (const MWWorld::Ptr& actor) const; + /// Does this AI sequence have the given package type? + bool hasPackage(AiPackageTypeId typeId) const; - bool canAddTarget(const ESM::Position& actorPos, float distToTarget) const; - ///< Function assumes that actor can have only 1 target apart player + /// Are we in combat with this particular actor? + bool isInCombat(const MWWorld::Ptr& actor) const; - /// Removes all combat packages until first non-combat or stack empty. - void stopCombat(); + /// Removes all combat packages until first non-combat or stack empty. + void stopCombat(); - /// Has a package been completed during the last update? - bool isPackageDone() const; + /// Removes all combat packages with the given targets + void stopCombat(const std::vector& targets); - /// Removes all pursue packages until first non-pursue or stack empty. - void stopPursuit(); + /// Has a package been completed during the last update? + bool isPackageDone() const; - /// Execute current package, switching if needed. - void execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange=false); + /// Removes all pursue packages until first non-pursue or stack empty. + void stopPursuit(); - /// Simulate the passing of time using the currently active AI package - void fastForward(const MWWorld::Ptr &actor); + /// Execute current package, switching if needed. + void execute(const MWWorld::Ptr& actor, CharacterController& characterController, float duration, + bool outOfRange = false); - /// Remove all packages. - void clear(); + /// Simulate the passing of time using the currently active AI package + void fastForward(const MWWorld::Ptr& actor); - ///< Add \a package to the front of the sequence - /** Suspends current package - @param actor The actor that owns this AiSequence **/ - void stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther=true); + /// Remove all packages. + void clear(); - /// Return the current active package. - /** If there is no active package, it will throw an exception **/ - const AiPackage& getActivePackage(); + ///< Add \a package to the front of the sequence + /** Suspends current package + @param actor The actor that owns this AiSequence **/ + void stack(const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther = true); - /// Fills the AiSequence with packages - /** Typically used for loading from the ESM - \see ESM::AIPackageList **/ - void fill (const ESM::AIPackageList& list); + /// Return the current active package. + /** If there is no active package, it will throw an exception **/ + const AiPackage& getActivePackage() const; - bool isEmpty() const; + /// Fills the AiSequence with packages + /** Typically used for loading from the ESM + \see ESM::AIPackageList **/ + void fill(const ESM::AIPackageList& list); + + bool isEmpty() const; - void writeState (ESM::AiSequence::AiSequence& sequence) const; - void readState (const ESM::AiSequence::AiSequence& sequence); + void writeState(ESM::AiSequence::AiSequence& sequence) const; + void readState(const ESM::AiSequence::AiSequence& sequence); }; } diff --git a/apps/openmw/mwmechanics/aisetting.hpp b/apps/openmw/mwmechanics/aisetting.hpp new file mode 100644 index 00000000000..3a274722fea --- /dev/null +++ b/apps/openmw/mwmechanics/aisetting.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_MWMECHANICS_AISETTING_H +#define OPENMW_MWMECHANICS_AISETTING_H + +namespace MWMechanics +{ + enum class AiSetting + { + Hello = 0, + Fight = 1, + Flee = 2, + Alarm = 3 + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/aistate.hpp b/apps/openmw/mwmechanics/aistate.hpp index 976e21c65ca..f2ce17fd9c2 100644 --- a/apps/openmw/mwmechanics/aistate.hpp +++ b/apps/openmw/mwmechanics/aistate.hpp @@ -1,7 +1,10 @@ #ifndef AISTATE_H #define AISTATE_H -#include +#include "aistatefwd.hpp" +#include "aitemporarybase.hpp" + +#include namespace MWMechanics { @@ -11,92 +14,58 @@ namespace MWMechanics * the stored object if it has the correct type or otherwise replaces * it with an object of the requested type. */ - template< class Base > + template class DerivedClassStorage - { + { private: - Base* mStorage; - - //if needed you have to provide a clone member function - DerivedClassStorage( const DerivedClassStorage& other ); - DerivedClassStorage& operator=( const DerivedClassStorage& ); - + std::unique_ptr mStorage; + public: /// \brief returns reference to stored object or deletes it and creates a fitting - template< class Derived > + template Derived& get() { - Derived* result = dynamic_cast(mStorage); - - if(!result) + Derived* result = dynamic_cast(mStorage.get()); + + if (result == nullptr) { - if(mStorage) - delete mStorage; - mStorage = result = new Derived(); + auto storage = std::make_unique(); + result = storage.get(); + mStorage = std::move(storage); } - - //return a reference to the (new allocated) object + + // return a reference to the (new allocated) object return *result; } - template< class Derived > + /// \brief returns pointer to stored object in the desired type + template + Derived* getPtr() const + { + return dynamic_cast(mStorage.get()); + } + + template void copy(DerivedClassStorage& destination) const { - Derived* result = dynamic_cast(mStorage); + Derived* result = dynamic_cast(mStorage.get()); if (result != nullptr) destination.store(*result); } - - template< class Derived > - void store( const Derived& payload ) + + template + void store(const Derived& payload) { - if(mStorage) - delete mStorage; - mStorage = new Derived(payload); + mStorage = std::make_unique(payload); } - + /// \brief takes ownership of the passed object - template< class Derived > - void moveIn( Derived* p ) + template + void moveIn(std::unique_ptr&& storage) { - if(mStorage) - delete mStorage; - mStorage = p; + mStorage = std::move(storage); } - - bool empty() const - { - return mStorage == nullptr; - } - - const std::type_info& getType() const - { - return typeid(mStorage); - } - - DerivedClassStorage():mStorage(nullptr){} - ~DerivedClassStorage() - { - if(mStorage) - delete mStorage; - } - }; - - - /// \brief base class for the temporary storage of AiPackages. - /** - * Each AI package with temporary values needs a AiPackageStorage class - * which is derived from AiTemporaryBase. The Actor holds a container - * AiState where one of these storages can be stored at a time. - * The execute(...) member function takes this container as an argument. - * */ - struct AiTemporaryBase - { - virtual ~AiTemporaryBase(){} }; - - /// \brief Container for AI package status. - typedef DerivedClassStorage AiState; } #endif // AISTATE_H diff --git a/apps/openmw/mwmechanics/aistatefwd.hpp b/apps/openmw/mwmechanics/aistatefwd.hpp new file mode 100644 index 00000000000..83216d1d27a --- /dev/null +++ b/apps/openmw/mwmechanics/aistatefwd.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_MWMECHANICS_AISTATEFWD_H +#define OPENMW_MWMECHANICS_AISTATEFWD_H + +namespace MWMechanics +{ + template + class DerivedClassStorage; + + struct AiTemporaryBase; + + /// \brief Container for AI package status. + using AiState = DerivedClassStorage; +} + +#endif diff --git a/apps/openmw/mwmechanics/aitemporarybase.hpp b/apps/openmw/mwmechanics/aitemporarybase.hpp new file mode 100644 index 00000000000..8ac8e4b71b9 --- /dev/null +++ b/apps/openmw/mwmechanics/aitemporarybase.hpp @@ -0,0 +1,19 @@ +#ifndef OPENMW_MWMECHANICS_AISTATE_H +#define OPENMW_MWMECHANICS_AISTATE_H + +namespace MWMechanics +{ + /// \brief base class for the temporary storage of AiPackages. + /** + * Each AI package with temporary values needs a AiPackageStorage class + * which is derived from AiTemporaryBase. The Actor holds a container + * AiState where one of these storages can be stored at a time. + * The execute(...) member function takes this container as an argument. + * */ + struct AiTemporaryBase + { + virtual ~AiTemporaryBase() = default; + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/aitimer.hpp b/apps/openmw/mwmechanics/aitimer.hpp index 804cda1bd28..d2e023f1c81 100644 --- a/apps/openmw/mwmechanics/aitimer.hpp +++ b/apps/openmw/mwmechanics/aitimer.hpp @@ -10,16 +10,22 @@ namespace MWMechanics class AiReactionTimer { - public: - static constexpr float sDeviation = 0.1f; + public: + static constexpr float sDeviation = 0.1f; - Misc::TimerStatus update(float duration) { return mImpl.update(duration); } + AiReactionTimer(Misc::Rng::Generator& prng) + : mPrng{ prng } + , mImpl{ AI_REACTION_TIME, sDeviation, Misc::Rng::deviate(0, sDeviation, prng) } + { + } - void reset() { mImpl.reset(Misc::Rng::deviate(0, sDeviation)); } + Misc::TimerStatus update(float duration) { return mImpl.update(duration, mPrng); } - private: - Misc::DeviatingPeriodicTimer mImpl {AI_REACTION_TIME, sDeviation, - Misc::Rng::deviate(0, sDeviation)}; + void reset() { mImpl.reset(Misc::Rng::deviate(0, sDeviation, mPrng)); } + + private: + Misc::Rng::Generator& mPrng; + Misc::DeviatingPeriodicTimer mImpl; }; } diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index 8e5372c46d7..f0781565bfa 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -1,119 +1,152 @@ #include "aitravel.hpp" -#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/cellstore.hpp" -#include "movement.hpp" +#include "character.hpp" #include "creaturestats.hpp" +#include "movement.hpp" namespace { -bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2) -{ - // Maximum travel distance for vanilla compatibility. - // Was likely meant to prevent NPCs walking into non-loaded exterior cells, but for some reason is used in interior cells as well. - // We can make this configurable at some point, but the default *must* be the below value. Anything else will break shoddily-written content (*cough* MW *cough*) in bizarre ways. - return (pos1 - pos2).length2() <= 7168*7168; -} + constexpr float TRAVEL_FINISH_TIME = 2.f; + + bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2) + { + // Maximum travel distance for vanilla compatibility. + // Was likely meant to prevent NPCs walking into non-loaded exterior cells, but for some reason is used in + // interior cells as well. We can make this configurable at some point, but the default *must* be the below + // value. Anything else will break shoddily-written content (*cough* MW *cough*) in bizarre ways. + return (pos1 - pos2).length2() <= 7168 * 7168; + } } namespace MWMechanics { - AiTravel::AiTravel(float x, float y, float z, AiTravel*) - : mX(x), mY(y), mZ(z), mHidden(false) + AiTravel::AiTravel(float x, float y, float z, bool repeat, AiTravel*) + : TypedAiPackage(repeat) + , mX(x) + , mY(y) + , mZ(z) + , mHidden(false) + , mDestinationTimer(TRAVEL_FINISH_TIME) { } AiTravel::AiTravel(float x, float y, float z, AiInternalTravel* derived) - : TypedAiPackage(derived), mX(x), mY(y), mZ(z), mHidden(true) + : TypedAiPackage(derived) + , mX(x) + , mY(y) + , mZ(z) + , mHidden(true) + , mDestinationTimer(TRAVEL_FINISH_TIME) { } - AiTravel::AiTravel(float x, float y, float z) - : AiTravel(x, y, z, this) + AiTravel::AiTravel(float x, float y, float z, bool repeat) + : AiTravel(x, y, z, repeat, this) { } - AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel) - : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ), mHidden(false) + AiTravel::AiTravel(const ESM::AiSequence::AiTravel* travel) + : TypedAiPackage(travel->mRepeat) + , mX(travel->mData.mX) + , mY(travel->mData.mY) + , mZ(travel->mData.mZ) + , mHidden(false) + , mDestinationTimer(TRAVEL_FINISH_TIME) { // Hidden ESM::AiSequence::AiTravel package should be converted into MWMechanics::AiInternalTravel type assert(!travel->mHidden); } - bool AiTravel::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) + bool AiTravel::execute( + const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager(); auto& stats = actor.getClass().getCreatureStats(actor); - if (!stats.getMovementFlag(CreatureStats::Flag_ForceJump) && !stats.getMovementFlag(CreatureStats::Flag_ForceSneak) - && (mechMgr->isTurningToPlayer(actor) || mechMgr->getGreetingState(actor) == Greet_InProgress)) + if (!stats.getMovementFlag(CreatureStats::Flag_ForceJump) + && !stats.getMovementFlag(CreatureStats::Flag_ForceSneak) + && (mechMgr->isTurningToPlayer(actor) || mechMgr->getGreetingState(actor) == Greet_InProgress)) return false; const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); const osg::Vec3f targetPos(mX, mY, mZ); stats.setMovementFlag(CreatureStats::Flag_Run, false); - stats.setDrawState(DrawState_Nothing); + stats.setDrawState(DrawState::Nothing); // Note: we should cancel internal "return after combat" package, if original location is too far away if (!isWithinMaxRange(targetPos, actorPos)) return mHidden; - // Unfortunately, with vanilla assets destination is sometimes blocked by other actor. - // If we got close to target, check for actors nearby. If they are, finish AI package. - int destinationTolerance = 64; - if (distance(actorPos, targetPos) <= destinationTolerance) + if (pathTo(actor, targetPos, duration, characterController.getSupportedMovementDirections())) { - std::vector targetActors; - std::pair result = MWBase::Environment::get().getWorld()->getHitContact(actor, destinationTolerance, targetActors); - - if (!result.first.isEmpty()) - { - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - return true; - } + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + return true; } - if (pathTo(actor, targetPos, duration)) + // If we've been close enough to the destination for some time give up like Morrowind. + // The end condition should be pretty much accurate. + // FIXME: But the timing isn't. Right now we're being very generous, + // but Morrowind might stop the actor prematurely under unclear conditions. + + // Note Morrowind uses the halved eye level, but this is close enough. + float dist + = distanceIgnoreZ(actorPos, targetPos) - MWBase::Environment::get().getWorld()->getHalfExtents(actor).z(); + const float endTolerance = std::max(64.f, actor.getClass().getCurrentSpeed(actor) * duration); + + // Even if we have entered the threshold, we might have been pushed away. Reset the timer if we're currently too + // far. + if (dist > endTolerance) { - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - return true; + mDestinationTimer = TRAVEL_FINISH_TIME; + return false; } - return false; + + mDestinationTimer -= duration; + if (mDestinationTimer > 0) + return false; + + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + return true; } void AiTravel::fastForward(const MWWorld::Ptr& actor, AiState& state) { - if (!isWithinMaxRange(osg::Vec3f(mX, mY, mZ), actor.getRefData().getPosition().asVec3())) + osg::Vec3f pos(mX, mY, mZ); + if (!isWithinMaxRange(pos, actor.getRefData().getPosition().asVec3())) return; // does not do any validation on the travel target (whether it's in air, inside collision geometry, etc), // that is the user's responsibility - MWBase::Environment::get().getWorld()->moveObject(actor, mX, mY, mZ); + MWBase::Environment::get().getWorld()->moveObject(actor, pos); actor.getClass().adjustPosition(actor, false); reset(); } - void AiTravel::writeState(ESM::AiSequence::AiSequence &sequence) const + void AiTravel::writeState(ESM::AiSequence::AiSequence& sequence) const { - std::unique_ptr travel(new ESM::AiSequence::AiTravel()); + auto travel = std::make_unique(); travel->mData.mX = mX; travel->mData.mY = mY; travel->mData.mZ = mZ; travel->mHidden = mHidden; + travel->mRepeat = getRepeat(); ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Travel; - package.mPackage = travel.release(); - sequence.mPackages.push_back(package); + package.mPackage = std::move(travel); + sequence.mPackages.push_back(std::move(package)); } AiInternalTravel::AiInternalTravel(float x, float y, float z) @@ -131,4 +164,3 @@ namespace MWMechanics return std::make_unique(*this); } } - diff --git a/apps/openmw/mwmechanics/aitravel.hpp b/apps/openmw/mwmechanics/aitravel.hpp index 2ea2a8f7177..d8e249d4657 100644 --- a/apps/openmw/mwmechanics/aitravel.hpp +++ b/apps/openmw/mwmechanics/aitravel.hpp @@ -1,69 +1,72 @@ -#ifndef GAME_MWMECHANICS_AITRAVEL_H -#define GAME_MWMECHANICS_AITRAVEL_H - -#include "typedaipackage.hpp" - -namespace ESM -{ -namespace AiSequence -{ - struct AiTravel; -} -} - -namespace MWMechanics -{ - struct AiInternalTravel; - - /// \brief Causes the AI to travel to the specified point - class AiTravel : public TypedAiPackage - { - public: - AiTravel(float x, float y, float z, AiTravel* derived); - - AiTravel(float x, float y, float z, AiInternalTravel* derived); - - AiTravel(float x, float y, float z); - - explicit AiTravel(const ESM::AiSequence::AiTravel* travel); - - /// Simulates the passing of time - void fastForward(const MWWorld::Ptr& actor, AiState& state) override; - - void writeState(ESM::AiSequence::AiSequence &sequence) const override; - - bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; - - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Travel; } - - static constexpr Options makeDefaultOptions() - { - AiPackage::Options options; - options.mUseVariableSpeed = true; - options.mAlwaysActive = true; - return options; - } - - osg::Vec3f getDestination() const override { return osg::Vec3f(mX, mY, mZ); } - - private: - const float mX; - const float mY; - const float mZ; - - const bool mHidden; - }; - - struct AiInternalTravel final : public AiTravel - { - AiInternalTravel(float x, float y, float z); - - explicit AiInternalTravel(const ESM::AiSequence::AiTravel* travel); - - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::InternalTravel; } - - std::unique_ptr clone() const override; - }; -} - -#endif +#ifndef GAME_MWMECHANICS_AITRAVEL_H +#define GAME_MWMECHANICS_AITRAVEL_H + +#include "typedaipackage.hpp" + +namespace ESM +{ + namespace AiSequence + { + struct AiTravel; + } +} + +namespace MWMechanics +{ + struct AiInternalTravel; + + /// \brief Causes the AI to travel to the specified point + class AiTravel : public TypedAiPackage + { + public: + AiTravel(float x, float y, float z, bool repeat, AiTravel* derived); + + AiTravel(float x, float y, float z, AiInternalTravel* derived); + + AiTravel(float x, float y, float z, bool repeat); + + explicit AiTravel(const ESM::AiSequence::AiTravel* travel); + + /// Simulates the passing of time + void fastForward(const MWWorld::Ptr& actor, AiState& state) override; + + void writeState(ESM::AiSequence::AiSequence& sequence) const override; + + bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, + float duration) override; + + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Travel; } + + static constexpr Options makeDefaultOptions() + { + AiPackage::Options options; + options.mUseVariableSpeed = true; + options.mAlwaysActive = true; + return options; + } + + osg::Vec3f getDestination() const override { return osg::Vec3f(mX, mY, mZ); } + + private: + const float mX; + const float mY; + const float mZ; + + const bool mHidden; + + float mDestinationTimer; + }; + + struct AiInternalTravel final : public AiTravel + { + AiInternalTravel(float x, float y, float z); + + explicit AiInternalTravel(const ESM::AiSequence::AiTravel* travel); + + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::InternalTravel; } + + std::unique_ptr clone() const override; + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 0e424b2f8bf..3c299c14905 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -2,26 +2,30 @@ #include +#include + #include -#include -#include -#include +#include +#include #include +#include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwworld/cellstore.hpp" -#include "../mwphysics/collisiontype.hpp" +#include "../mwphysics/raycasting.hpp" -#include "pathgrid.hpp" +#include "actorutil.hpp" +#include "character.hpp" #include "creaturestats.hpp" #include "movement.hpp" -#include "actorutil.hpp" +#include "pathgrid.hpp" namespace MWMechanics { @@ -36,8 +40,7 @@ namespace MWMechanics static const std::size_t MAX_IDLE_SIZE = 8; - const std::string AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = - { + const std::string AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = { std::string("idle2"), std::string("idle3"), std::string("idle4"), @@ -59,44 +62,39 @@ namespace MWMechanics osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance) { - const float randomDirection = Misc::Rng::rollClosedProbability() * 2.0f * osg::PI; + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const float randomDirection = Misc::Rng::rollClosedProbability(prng) * 2.0f * osg::PI; osg::Matrixf rotation; rotation.makeRotate(randomDirection, osg::Vec3f(0.0, 0.0, 1.0)); return position + osg::Vec3f(distance, 0.0, 0.0) * rotation; } - bool isDestinationHidden(const MWWorld::ConstPtr &actor, const osg::Vec3f& destination) + bool isDestinationHidden(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination) { const auto position = actor.getRefData().getPosition().asVec3(); const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); - const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor); + const osg::Vec3f halfExtents + = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor).mHalfExtents; osg::Vec3f direction = destination - position; direction.normalize(); - const auto visibleDestination = ( - isWaterCreature || isFlyingCreature - ? destination - : destination + osg::Vec3f(0, 0, halfExtents.z()) - ) + direction * std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - const int mask = MWPhysics::CollisionType_World - | MWPhysics::CollisionType_HeightMap - | MWPhysics::CollisionType_Door - | MWPhysics::CollisionType_Actor; - return MWBase::Environment::get().getWorld()->castRay(position, visibleDestination, mask, actor); - } - - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr &actor, const osg::Vec3f& destination) - { - const auto world = MWBase::Environment::get().getWorld(); - const osg::Vec3f halfExtents = world->getPathfindingHalfExtents(actor); - const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, actor); + const auto visibleDestination + = (isWaterCreature || isFlyingCreature ? destination : destination + osg::Vec3f(0, 0, halfExtents.z())) + + direction * std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + const int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap + | MWPhysics::CollisionType_Door | MWPhysics::CollisionType_Actor; + return MWBase::Environment::get() + .getWorld() + ->getRayCasting() + ->castRay(position, visibleDestination, { actor }, {}, mask) + .mHit; } void stopMovement(const MWWorld::Ptr& actor) { - actor.getClass().getMovementSettings(actor).mPosition[0] = 0; - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + auto& movementSettings = actor.getClass().getMovementSettings(actor); + movementSettings.mPosition[0] = 0; + movementSettings.mPosition[1] = 0; } std::vector getInitialIdle(const std::vector& idle) @@ -110,16 +108,36 @@ namespace MWMechanics { return std::vector(std::begin(idle), std::end(idle)); } + } - AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): - TypedAiPackage(makeDefaultOptions().withRepeat(repeat)), - mDistance(std::max(0, distance)), - mDuration(std::max(0, duration)), - mRemainingDuration(duration), mTimeOfDay(timeOfDay), - mIdle(getInitialIdle(idle)), - mStoredInitialActorPosition(false), mInitialActorPosition(osg::Vec3f(0, 0, 0)), - mHasDestination(false), mDestination(osg::Vec3f(0, 0, 0)), mUsePathgrid(false) + AiWanderStorage::AiWanderStorage() + : mReaction(MWBase::Environment::get().getWorld()->getPrng()) + , mState(Wander_ChooseAction) + , mIsWanderingManually(false) + , mCanWanderAlongPathGrid(true) + , mIdleAnimation(0) + , mBadIdles() + , mPopulateAvailableNodes(true) + , mAllowedNodes() + , mTrimCurrentNode(false) + , mCheckIdlePositionTimer(0) + , mStuckCount(0) + { + } + + AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat) + : TypedAiPackage(repeat) + , mDistance(std::max(0, distance)) + , mDuration(std::max(0, duration)) + , mRemainingDuration(duration) + , mTimeOfDay(timeOfDay) + , mIdle(getInitialIdle(idle)) + , mStoredInitialActorPosition(false) + , mInitialActorPosition(osg::Vec3f(0, 0, 0)) + , mHasDestination(false) + , mDestination(osg::Vec3f(0, 0, 0)) + , mUsePathgrid(false) { } @@ -173,7 +191,8 @@ namespace MWMechanics * actors will enter combat (i.e. no longer wandering) and different pathfinding * will kick in. */ - bool AiWander::execute (const MWWorld::Ptr& actor, CharacterController& /*characterController*/, AiState& state, float duration) + bool AiWander::execute( + const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor); if (cStats.isDead() || cStats.getHealth().getCurrent() <= 0) @@ -182,9 +201,10 @@ namespace MWMechanics // get or create temporary storage AiWanderStorage& storage = state.get(); - mRemainingDuration -= ((duration*MWBase::Environment::get().getWorld()->getTimeScaleFactor()) / 3600); + mRemainingDuration + -= ((duration * MWBase::Environment::get().getWorld()->getTimeManager()->getGameTimeScale()) / 3600); - cStats.setDrawState(DrawState_Nothing); + cStats.setDrawState(DrawState::Nothing); cStats.setMovementFlag(CreatureStats::Flag_Run, false); ESM::Position pos = actor.getRefData().getPosition(); @@ -193,23 +213,29 @@ namespace MWMechanics // rebuild a path to it if (!mPathFinder.isPathConstructed() && mHasDestination) { + const ESM::Pathgrid* pathgrid + = MWBase::Environment::get().getESMStore()->get().search(*actor.getCell()->getCell()); if (mUsePathgrid) { - mPathFinder.buildPathByPathgrid(pos.asVec3(), mDestination, actor.getCell(), - getPathGridGraph(actor.getCell())); + mPathFinder.buildPathByPathgrid( + pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid)); } else { - const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor); - mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), - getPathGridGraph(actor.getCell()), halfExtents, getNavigatorFlags(actor), getAreaCosts(actor)); + const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); + constexpr float endTolerance = 0; + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); + mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid), + agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full); } if (mPathFinder.isPathConstructed()) - storage.setState(AiWanderStorage::Wander_Walking); + storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); } - if(!cStats.getMovementFlag(CreatureStats::Flag_ForceJump) && !cStats.getMovementFlag(CreatureStats::Flag_ForceSneak)) + if (!cStats.getMovementFlag(CreatureStats::Flag_ForceJump) + && !cStats.getMovementFlag(CreatureStats::Flag_ForceSneak)) { GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); if (greetingState == Greet_InProgress) @@ -223,7 +249,7 @@ namespace MWMechanics } } - doPerFrameActionsForState(actor, duration, storage); + doPerFrameActionsForState(actor, duration, characterController.getSupportedMovementDirections(), storage); if (storage.mReaction.update(duration) == Misc::TimerStatus::Waiting) return false; @@ -240,7 +266,7 @@ namespace MWMechanics { stopWalking(actor); // Reset package so it can be used again - mRemainingDuration=mDuration; + mRemainingDuration = mDuration; return true; } @@ -253,12 +279,15 @@ namespace MWMechanics // Initialization to discover & store allowed node points for this actor. if (storage.mPopulateAvailableNodes) { - getAllowedNodes(actor, actor.getCell()->getCell(), storage); + getAllowedNodes(actor, storage); } - if (canActorMoveByZAxis(actor) && mDistance > 0) { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (canActorMoveByZAxis(actor) && mDistance > 0) + { // Typically want to idle for a short time before the next wander - if (Misc::Rng::rollDice(100) >= 92 && storage.mState != AiWanderStorage::Wander_Walking) { + if (Misc::Rng::rollDice(100, prng) >= 92 && storage.mState != AiWanderStorage::Wander_Walking) + { wanderNearStart(actor, storage, mDistance); } @@ -266,26 +295,33 @@ namespace MWMechanics } // If the package has a wander distance but no pathgrid is available, // randomly idle or wander near spawn point - else if(storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) { + else if (storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) + { // Typically want to idle for a short time before the next wander - if (Misc::Rng::rollDice(100) >= 96) { + if (Misc::Rng::rollDice(100, prng) >= 96) + { wanderNearStart(actor, storage, mDistance); - } else { + } + else + { storage.setState(AiWanderStorage::Wander_IdleNow); } - } else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) { + } + else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) + { storage.mCanWanderAlongPathGrid = false; } // If Wandering manually and hit an obstacle, stop - if (storage.mIsWanderingManually && mObstacleCheck.isEvading()) { + if (storage.mIsWanderingManually && mObstacleCheck.isEvading()) + { completeManualWalking(actor, storage); } if (storage.mState == AiWanderStorage::Wander_MoveNow && storage.mCanWanderAlongPathGrid) { // Construct a new path if there isn't one - if(!mPathFinder.isPathConstructed()) + if (!mPathFinder.isPathConstructed()) { if (!storage.mAllowedNodes.empty()) { @@ -298,10 +334,8 @@ namespace MWMechanics completeManualWalking(actor, storage); } - if (storage.mIsWanderingManually - && storage.mState == AiWanderStorage::Wander_Walking - && (mPathFinder.getPathSize() == 0 - || isDestinationHidden(actor, mPathFinder.getPath().back()) + if (storage.mIsWanderingManually && storage.mState == AiWanderStorage::Wander_Walking + && (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back()) || isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) completeManualWalking(actor, storage); @@ -325,28 +359,47 @@ namespace MWMechanics /* * Commands actor to walk to a random location near original spawn location. */ - void AiWander::wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance) { + void AiWander::wanderNearStart(const MWWorld::Ptr& actor, AiWanderStorage& storage, int wanderDistance) + { const auto currentPosition = actor.getRefData().getPosition().asVec3(); std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); const auto world = MWBase::Environment::get().getWorld(); - const auto halfExtents = world->getPathfindingHalfExtents(actor); + const auto agentBounds = world->getPathfindingAgentBounds(actor); const auto navigator = world->getNavigator(); - const auto navigatorFlags = getNavigatorFlags(actor); - const auto areaCosts = getAreaCosts(actor); + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + + do + { - do { // Determine a random location within radius of original position - const float wanderRadius = (0.2f + Misc::Rng::rollClosedProbability() * 0.8f) * wanderDistance; + const float wanderRadius = (0.2f + Misc::Rng::rollClosedProbability(prng) * 0.8f) * wanderDistance; if (!isWaterCreature && !isFlyingCreature) { // findRandomPointAroundCircle uses wanderDistance as limit for random and not as exact distance - if (const auto destination = navigator->findRandomPointAroundCircle(halfExtents, mInitialActorPosition, wanderDistance, navigatorFlags)) - mDestination = *destination; - else - mDestination = getRandomPointAround(mInitialActorPosition, wanderRadius); + const auto getRandom + = []() { return Misc::Rng::rollProbability(MWBase::Environment::get().getWorld()->getPrng()); }; + auto destination = DetourNavigator::findRandomPointAroundCircle( + *navigator, agentBounds, mInitialActorPosition, wanderRadius, navigatorFlags, getRandom); + if (destination.has_value()) + { + osg::Vec3f direction = *destination - mInitialActorPosition; + if (direction.length() > wanderDistance) + { + direction.normalize(); + const osg::Vec3f adjustedDestination = mInitialActorPosition + direction * wanderRadius; + destination = DetourNavigator::raycast( + *navigator, agentBounds, currentPosition, adjustedDestination, navigatorFlags); + if (destination.has_value() && (*destination - mInitialActorPosition).length() > wanderDistance) + continue; + } + } + mDestination = destination.has_value() ? *destination + : getRandomPointAround(mInitialActorPosition, wanderRadius); } else mDestination = getRandomPointAround(mInitialActorPosition, wanderRadius); @@ -361,11 +414,13 @@ namespace MWMechanics if (isAreaOccupiedByOtherActor(actor, mDestination)) continue; + constexpr float endTolerance = 0; + if (isWaterCreature || isFlyingCreature) mPathFinder.buildStraightPath(mDestination); else - mPathFinder.buildPathByNavMesh(actor, currentPosition, mDestination, halfExtents, navigatorFlags, - areaCosts); + mPathFinder.buildPathByNavMesh(actor, currentPosition, mDestination, agentBounds, navigatorFlags, + areaCosts, endTolerance, PathType::Full); if (mPathFinder.isPathConstructed()) { @@ -381,42 +436,56 @@ namespace MWMechanics /* * Returns true if the position provided is above water. */ - bool AiWander::destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination) { - float heightToGroundOrWater = MWBase::Environment::get().getWorld()->getDistToNearestRayHit(destination, osg::Vec3f(0,0,-1), 1000.0, true); + bool AiWander::destinationIsAtWater(const MWWorld::Ptr& actor, const osg::Vec3f& destination) + { + float heightToGroundOrWater = MWBase::Environment::get().getWorld()->getDistToNearestRayHit( + destination, osg::Vec3f(0, 0, -1), 1000.0, true); osg::Vec3f positionBelowSurface = destination; positionBelowSurface[2] = positionBelowSurface[2] - heightToGroundOrWater - 1.0f; return MWBase::Environment::get().getWorld()->isUnderwater(actor.getCell(), positionBelowSurface); } - void AiWander::completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage) { + void AiWander::completeManualWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage) + { stopWalking(actor); mObstacleCheck.clear(); storage.setState(AiWanderStorage::Wander_IdleNow); } - void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) + void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, + MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage) { - switch (storage.mState) + // Attempt to fast forward to the next state instead of remaining in an intermediate state for a frame + for (int i = 0; i < 2; ++i) { - case AiWanderStorage::Wander_IdleNow: - onIdleStatePerFrameActions(actor, duration, storage); - break; - - case AiWanderStorage::Wander_Walking: - onWalkingStatePerFrameActions(actor, duration, storage); - break; - - case AiWanderStorage::Wander_ChooseAction: - onChooseActionStatePerFrameActions(actor, storage); - break; + switch (storage.mState) + { + case AiWanderStorage::Wander_IdleNow: + { + onIdleStatePerFrameActions(actor, duration, storage); + if (storage.mState != AiWanderStorage::Wander_ChooseAction) + return; + continue; + } + case AiWanderStorage::Wander_Walking: + onWalkingStatePerFrameActions(actor, duration, supportedMovementDirections, storage); + return; - case AiWanderStorage::Wander_MoveNow: - break; // nothing to do + case AiWanderStorage::Wander_ChooseAction: + { + onChooseActionStatePerFrameActions(actor, storage); + if (storage.mState != AiWanderStorage::Wander_IdleNow) + return; + continue; + } + case AiWanderStorage::Wander_MoveNow: + return; // nothing to do - default: - // should never get here - assert(false); - break; + default: + // should never get here + assert(false); + return; + } } } @@ -427,7 +496,7 @@ namespace MWMechanics if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary()) { - storage.mCheckIdlePositionTimer = 0; // restart timer + storage.mCheckIdlePositionTimer = 0; // restart timer static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance)) { @@ -442,7 +511,7 @@ namespace MWMechanics if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == Greet_Done || greetingState == Greet_None)) { if (mPathFinder.isPathConstructed()) - storage.setState(AiWanderStorage::Wander_Walking); + storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); else storage.setState(AiWanderStorage::Wander_ChooseAction); } @@ -451,21 +520,22 @@ namespace MWMechanics bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const { const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); - auto cell = actor.getCell()->getCell(); for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes) { osg::Vec3f point(node.mX, node.mY, node.mZ); - Misc::CoordinateConverter(cell).toWorld(point); if ((actorPos - point).length2() < distance * distance) return true; } return false; } - void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) + void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, + MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage) { // Is there no destination or are we there yet? - if ((!mPathFinder.isPathConstructed()) || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, DESTINATION_TOLERANCE)) + if ((!mPathFinder.isPathConstructed()) + || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, supportedMovementDirections, + DESTINATION_TOLERANCE)) { stopWalking(actor); storage.setState(AiWanderStorage::Wander_ChooseAction); @@ -491,11 +561,11 @@ namespace MWMechanics storage.setState(AiWanderStorage::Wander_MoveNow); return; } - if(idleAnimation) + if (idleAnimation) { - if(std::find(storage.mBadIdles.begin(), storage.mBadIdles.end(), idleAnimation)==storage.mBadIdles.end()) + if (std::find(storage.mBadIdles.begin(), storage.mBadIdles.end(), idleAnimation) == storage.mBadIdles.end()) { - if(!playIdle(actor, idleAnimation)) + if (!playIdle(actor, idleAnimation)) { storage.mBadIdles.push_back(idleAnimation); storage.setState(AiWanderStorage::Wander_ChooseAction); @@ -509,13 +579,6 @@ namespace MWMechanics void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage) { - if (mUsePathgrid) - { - const auto halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); - mPathFinder.buildPathByNavMeshToNextPoint(actor, halfExtents, getNavigatorFlags(actor), - getAreaCosts(actor)); - } - if (mObstacleCheck.isEvading()) { // first check if we're walking into a door @@ -530,7 +593,7 @@ namespace MWMechanics storage.setState(AiWanderStorage::Wander_MoveNow); } - storage.mStuckCount++; // TODO: maybe no longer needed + storage.mStuckCount++; // TODO: maybe no longer needed } // if stuck for sufficiently long, act like current location was the destination @@ -543,28 +606,28 @@ namespace MWMechanics } } - - - void AiWander::setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) + void AiWander::setPathToAnAllowedNode( + const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) { - unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size()); - ESM::Pathgrid::Point dest(storage.mAllowedNodes[randNode]); + auto world = MWBase::Environment::get().getWorld(); + auto& prng = world->getPrng(); + unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); + const ESM::Pathgrid::Point& dest = storage.mAllowedNodes[randNode]; - ToWorldCoordinates(dest, actor.getCell()->getCell()); - - // actor position is already in world coordinates const osg::Vec3f start = actorPos.asVec3(); // don't take shortcuts for wandering + const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest); - mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(actor.getCell())); + mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(pathgrid)); if (mPathFinder.isPathConstructed()) { mDestination = destVec3f; mHasDestination = true; mUsePathgrid = true; - // Remove this node as an option and add back the previously used node (stops NPC from picking the same node): + // Remove this node as an option and add back the previously used node (stops NPC from picking the same + // node): ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode]; storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); // check if mCurrentNode was taken out of mAllowedNodes @@ -581,13 +644,7 @@ namespace MWMechanics storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); } - void AiWander::ToWorldCoordinates(ESM::Pathgrid::Point& point, const ESM::Cell * cell) - { - Misc::CoordinateConverter(cell).toWorld(point); - } - - void AiWander::trimAllowedNodes(std::vector& nodes, - const PathFinder& pathfinder) + void AiWander::trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder) { // TODO: how to add these back in once the door opens? // Idea: keep a list of detected closed doors (see aicombat.cpp) @@ -595,10 +652,10 @@ namespace MWMechanics // at the end of playing idle?) If the door is opened then re-calculate // allowed nodes starting from the spawn point. auto paths = pathfinder.getPath(); - while(paths.size() >= 2) + while (paths.size() >= 2) { const auto pt = paths.back(); - for(unsigned int j = 0; j < nodes.size(); j++) + for (unsigned int j = 0; j < nodes.size(); j++) { // FIXME: doesn't handle a door with the same X/Y // coordinates but with a different Z @@ -628,7 +685,8 @@ namespace MWMechanics } else { - Log(Debug::Verbose) << "Attempted to play out of range idle animation \"" << idleSelect << "\" for " << actor.getCellRef().getRefId(); + Log(Debug::Verbose) << "Attempted to play out of range idle animation \"" << idleSelect << "\" for " + << actor.getCellRef().getRefId(); return false; } } @@ -646,28 +704,30 @@ namespace MWMechanics } } - short unsigned AiWander::getRandomIdle() + int AiWander::getRandomIdle() const { - unsigned short idleRoll = 0; - short unsigned selectedAnimation = 0; - - for(unsigned int counter = 0; counter < mIdle.size(); counter++) - { - static float fIdleChanceMultiplier = MWBase::Environment::get().getWorld()->getStore() - .get().find("fIdleChanceMultiplier")->mValue.getFloat(); - - unsigned short idleChance = static_cast(fIdleChanceMultiplier * mIdle[counter]); - unsigned short randSelect = (int)(Misc::Rng::rollProbability() * int(100 / fIdleChanceMultiplier)); - if(randSelect < idleChance && randSelect > idleRoll) + MWBase::World* world = MWBase::Environment::get().getWorld(); + static const float fIdleChanceMultiplier + = world->getStore().get().find("fIdleChanceMultiplier")->mValue.getFloat(); + if (Misc::Rng::rollClosedProbability(world->getPrng()) > fIdleChanceMultiplier) + return 0; + + int newIdle = 0; + float maxRoll = 0.f; + for (size_t i = 0; i < mIdle.size(); i++) + { + float roll = Misc::Rng::rollClosedProbability(world->getPrng()) * 100.f; + if (roll <= mIdle[i] && roll > maxRoll) { - selectedAnimation = counter + GroupIndex_MinIdle; - idleRoll = randSelect; + newIdle = GroupIndex_MinIdle + i; + maxRoll = roll; } } - return selectedAnimation; + + return newIdle; } - void AiWander::fastForward(const MWWorld::Ptr& actor, AiState &state) + void AiWander::fastForward(const MWWorld::Ptr& actor, AiState& state) { // Update duration counter mRemainingDuration--; @@ -676,17 +736,19 @@ namespace MWMechanics AiWanderStorage& storage = state.get(); if (storage.mPopulateAvailableNodes) - getAllowedNodes(actor, actor.getCell()->getCell(), storage); + getAllowedNodes(actor, storage); if (storage.mAllowedNodes.empty()) return; - int index = Misc::Rng::rollDice(storage.mAllowedNodes.size()); - ESM::Pathgrid::Point dest = storage.mAllowedNodes[index]; - ESM::Pathgrid::Point worldDest = dest; - ToWorldCoordinates(worldDest, actor.getCell()->getCell()); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int index = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); + ESM::Pathgrid::Point worldDest = storage.mAllowedNodes[index]; + const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(*actor.getCell()->getCell()); + ESM::Pathgrid::Point dest = converter.toLocalPoint(worldDest); - bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::makeOsgVec3(worldDest), 60); + bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( + PathFinder::makeOsgVec3(worldDest), 60); // add offset only if the selected pathgrid is occupied by another actor if (isPathGridOccupied) @@ -698,13 +760,12 @@ namespace MWMechanics if (points.empty()) return; - int initialSize = points.size(); bool isOccupied = false; // AI will try to move the NPC towards every neighboring node until suitable place will be found - for (int i = 0; i < initialSize; i++) + while (!points.empty()) { - int randomIndex = Misc::Rng::rollDice(points.size()); - ESM::Pathgrid::Point connDest = points[randomIndex]; + int randomIndex = Misc::Rng::rollDice(points.size(), prng); + const ESM::Pathgrid::Point& connDest = points[randomIndex]; // add an offset towards random neighboring node osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest); @@ -714,11 +775,12 @@ namespace MWMechanics for (int j = 1; j <= 3; j++) { // move for 5-15% towards random neighboring node - dest = PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f)); - worldDest = dest; - ToWorldCoordinates(worldDest, actor.getCell()->getCell()); + dest + = PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f)); + worldDest = converter.toWorldPoint(dest); - isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::makeOsgVec3(worldDest), 60); + isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( + PathFinder::makeOsgVec3(worldDest), 60); if (!isOccupied) break; @@ -728,7 +790,7 @@ namespace MWMechanics break; // Will try an another neighboring node - points.erase(points.begin()+randomIndex); + points.erase(points.begin() + randomIndex); } // there is no free space, nowhere to move @@ -736,35 +798,39 @@ namespace MWMechanics return; } - // place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be underground. - // Adding 20 in adjustPosition() is not enough. + // place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be + // underground. Adding 20 in adjustPosition() is not enough. dest.mZ += 60; - ToWorldCoordinates(dest, actor.getCell()->getCell()); + converter.toWorld(dest); - state.moveIn(new AiWanderStorage()); + state.moveIn(std::make_unique()); - MWBase::Environment::get().getWorld()->moveObject(actor, static_cast(dest.mX), - static_cast(dest.mY), static_cast(dest.mZ)); + osg::Vec3f pos(static_cast(dest.mX), static_cast(dest.mY), static_cast(dest.mZ)); + MWBase::Environment::get().getWorld()->moveObject(actor, pos); actor.getClass().adjustPosition(actor, false); } - void AiWander::getNeighbouringNodes(ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) + void AiWander::getNeighbouringNodes( + ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) { - const ESM::Pathgrid *pathgrid = - MWBase::Environment::get().getWorld()->getStore().get().search(*currentCell->getCell()); + const ESM::Pathgrid* pathgrid + = MWBase::Environment::get().getESMStore()->get().search(*currentCell->getCell()); + + if (pathgrid == nullptr || pathgrid->mPoints.empty()) + return; int index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); - getPathGridGraph(currentCell).getNeighbouringPoints(index, points); + getPathGridGraph(pathgrid).getNeighbouringPoints(index, points); } - void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage) + void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage) { // infrequently used, therefore no benefit in caching it as a member - const ESM::Pathgrid * - pathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell); const MWWorld::CellStore* cellStore = actor.getCell(); + const ESM::Pathgrid* pathgrid + = MWBase::Environment::get().getESMStore()->get().search(*cellStore->getCell()); storage.mAllowedNodes.clear(); @@ -772,7 +838,7 @@ namespace MWMechanics // https://forum.openmw.org/viewtopic.php?t=1556 // http://www.fliggerty.com/phpBB3/viewtopic.php?f=30&t=5833 // Note: In order to wander, need at least two points. - if(!pathgrid || (pathgrid->mPoints.size() < 2)) + if (!pathgrid || (pathgrid->mPoints.size() < 2)) storage.mCanWanderAlongPathGrid = false; // A distance value passed into the constructor indicates how far the @@ -783,33 +849,34 @@ namespace MWMechanics if (mDistance && storage.mCanWanderAlongPathGrid && !actor.getClass().isPureWaterCreature(actor)) { // get NPC's position in local (i.e. cell) coordinates - osg::Vec3f npcPos(mInitialActorPosition); - Misc::CoordinateConverter(cell).toLocal(npcPos); + const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(*cellStore->getCell()); + const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition); // Find closest pathgrid point int 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 - // NOTE: mPoints and mAllowedNodes are in local coordinates - int pointIndex = 0; - for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++) + // NOTE: mPoints is in local coordinates + size_t pointIndex = 0; + for (size_t counter = 0; counter < pathgrid->mPoints.size(); counter++) { osg::Vec3f nodePos(PathFinder::makeOsgVec3(pathgrid->mPoints[counter])); - if((npcPos - nodePos).length2() <= mDistance * mDistance && - getPathGridGraph(cellStore).isPointConnected(closestPointIndex, counter)) + if ((npcPos - nodePos).length2() <= mDistance * mDistance + && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, counter)) { - storage.mAllowedNodes.push_back(pathgrid->mPoints[counter]); + storage.mAllowedNodes.push_back(converter.toWorldPoint(pathgrid->mPoints[counter])); pointIndex = counter; } } if (storage.mAllowedNodes.size() == 1) { - AddNonPathGridAllowedPoints(npcPos, pathgrid, pointIndex, storage); + storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(mInitialActorPosition)); + addNonPathGridAllowedPoints(pathgrid, pointIndex, storage, converter); } - if(!storage.mAllowedNodes.empty()) + if (!storage.mAllowedNodes.empty()) { - SetCurrentNodeToClosestAllowedNode(npcPos, storage); + setCurrentNodeToClosestAllowedNode(storage); } } @@ -820,19 +887,21 @@ namespace MWMechanics // additional points for NPC to wander to are: // 1. NPC's initial location // 2. Partway along the path between the point and its connected points. - void AiWander::AddNonPathGridAllowedPoints(osg::Vec3f npcPos, const ESM::Pathgrid * pathGrid, int pointIndex, AiWanderStorage& storage) + void AiWander::addNonPathGridAllowedPoints(const ESM::Pathgrid* pathGrid, size_t pointIndex, + AiWanderStorage& storage, const Misc::CoordinateConverter& converter) { - storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(npcPos)); - for (auto& edge : pathGrid->mEdges) + for (const auto& edge : pathGrid->mEdges) { if (edge.mV0 == pointIndex) { - AddPointBetweenPathGridPoints(pathGrid->mPoints[edge.mV0], pathGrid->mPoints[edge.mV1], storage); + AddPointBetweenPathGridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]), + converter.toWorldPoint(pathGrid->mPoints[edge.mV1]), storage); } } } - void AiWander::AddPointBetweenPathGridPoints(const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage) + void AiWander::AddPointBetweenPathGridPoints( + const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage) { osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start); osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart; @@ -847,17 +916,17 @@ namespace MWMechanics storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta)); } - void AiWander::SetCurrentNodeToClosestAllowedNode(const osg::Vec3f& npcPos, AiWanderStorage& storage) + void AiWander::setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage) { float distanceToClosestNode = std::numeric_limits::max(); - unsigned int index = 0; - for (unsigned int counterThree = 0; counterThree < storage.mAllowedNodes.size(); counterThree++) + size_t index = 0; + for (size_t i = 0; i < storage.mAllowedNodes.size(); ++i) { - osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[counterThree])); - float tempDist = (npcPos - nodePos).length2(); + osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[i])); + float tempDist = (mInitialActorPosition - nodePos).length2(); if (tempDist < distanceToClosestNode) { - index = counterThree; + index = i; distanceToClosestNode = tempDist; } } @@ -865,7 +934,7 @@ namespace MWMechanics storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index); } - void AiWander::writeState(ESM::AiSequence::AiSequence &sequence) const + void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const { float remainingDuration; if (mRemainingDuration > 0 && mRemainingDuration < 24) @@ -873,13 +942,13 @@ namespace MWMechanics else remainingDuration = mDuration; - std::unique_ptr wander(new ESM::AiSequence::AiWander()); + auto wander = std::make_unique(); wander->mData.mDistance = mDistance; wander->mData.mDuration = mDuration; wander->mData.mTimeOfDay = mTimeOfDay; wander->mDurationData.mRemainingDuration = remainingDuration; - assert (mIdle.size() == 8); - for (int i=0; i<8; ++i) + assert(mIdle.size() == 8); + for (int i = 0; i < 8; ++i) wander->mData.mIdle[i] = mIdle[i]; wander->mData.mShouldRepeat = mOptions.mRepeat; wander->mStoredInitialActorPosition = mStoredInitialActorPosition; @@ -888,11 +957,11 @@ namespace MWMechanics ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Wander; - package.mPackage = wander.release(); - sequence.mPackages.push_back(package); + package.mPackage = std::move(wander); + sequence.mPackages.push_back(std::move(package)); } - AiWander::AiWander (const ESM::AiSequence::AiWander* wander) + AiWander::AiWander(const ESM::AiSequence::AiWander* wander) : TypedAiPackage(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0)) , mDistance(std::max(static_cast(0), wander->mData.mDistance)) , mDuration(std::max(static_cast(0), wander->mData.mDuration)) diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index f8506ff594b..aed7214f4db 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -5,10 +5,9 @@ #include -#include "pathfinding.hpp" -#include "obstacle.hpp" -#include "aistate.hpp" +#include "aitemporarybase.hpp" #include "aitimer.hpp" +#include "pathfinding.hpp" namespace ESM { @@ -19,6 +18,15 @@ namespace ESM } } +namespace Misc +{ + class CoordinateConverter; +} + +namespace MWWorld +{ + class Cell; +} namespace MWMechanics { /// \brief This class holds the variables AiWander needs which are deleted if the package becomes inactive. @@ -46,7 +54,6 @@ namespace MWMechanics bool mPopulateAvailableNodes; // allowed pathgrid nodes based on mDistance from the spawn point - // in local coordinates of mCell std::vector mAllowedNodes; ESM::Pathgrid::Point mCurrentNode; @@ -55,18 +62,7 @@ namespace MWMechanics float mCheckIdlePositionTimer; int mStuckCount; - AiWanderStorage(): - mState(Wander_ChooseAction), - mIsWanderingManually(false), - mCanWanderAlongPathGrid(true), - mIdleAnimation(0), - mBadIdles(), - mPopulateAvailableNodes(true), - mAllowedNodes(), - mTrimCurrentNode(false), - mCheckIdlePositionTimer(0), - mStuckCount(0) - {}; + AiWanderStorage(); void setState(const WanderState wanderState, const bool isManualWander = false) { @@ -78,104 +74,116 @@ namespace MWMechanics /// \brief Causes the Actor to wander within a specified range class AiWander final : public TypedAiPackage { - public: - /// Constructor - /** \param distance Max distance the ACtor will wander - \param duration Time, in hours, that this package will be preformed - \param timeOfDay Currently unimplemented. Not functional in the original engine. - \param idle Chances of each idle to play (9 in total) - \param repeat Repeat wander or not **/ - AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat); + public: + /// Constructor + /** \param distance Max distance the ACtor will wander + \param duration Time, in hours, that this package will be preformed + \param timeOfDay Currently unimplemented. Not functional in the original engine. + \param idle Chances of each idle to play (9 in total) + \param repeat Repeat wander or not **/ + AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat); + + explicit AiWander(const ESM::AiSequence::AiWander* wander); - explicit AiWander (const ESM::AiSequence::AiWander* wander); + bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, + float duration) override; - bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; + static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Wander; } - static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Wander; } + static constexpr Options makeDefaultOptions() + { + AiPackage::Options options; + options.mUseVariableSpeed = true; + return options; + } + + void writeState(ESM::AiSequence::AiSequence& sequence) const override; + + void fastForward(const MWWorld::Ptr& actor, AiState& state) override; - static constexpr Options makeDefaultOptions() - { - AiPackage::Options options; - options.mUseVariableSpeed = true; - return options; - } + osg::Vec3f getDestination(const MWWorld::Ptr& actor) const override; - void writeState(ESM::AiSequence::AiSequence &sequence) const override; + osg::Vec3f getDestination() const override + { + if (!mHasDestination) + return osg::Vec3f(0, 0, 0); - void fastForward(const MWWorld::Ptr& actor, AiState& state) override; + return mDestination; + } - osg::Vec3f getDestination(const MWWorld::Ptr& actor) const override; + bool isStationary() const { return mDistance == 0; } - osg::Vec3f getDestination() const override - { - if (!mHasDestination) - return osg::Vec3f(0, 0, 0); + std::optional getDistance() const override { return mDistance; } - return mDestination; - } + std::optional getDuration() const override { return static_cast(mDuration); } - bool isStationary() const { return mDistance == 0; } + const std::vector& getIdle() const { return mIdle; } - private: - void stopWalking(const MWWorld::Ptr& actor); + static std::string_view getIdleGroupName(size_t index) { return sIdleSelectToGroupName[index]; } - /// Have the given actor play an idle animation - /// @return Success or error - bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); - bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); - short unsigned getRandomIdle(); - void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); - void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage); - void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); - void onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); - void onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); - void onChooseActionStatePerFrameActions(const MWWorld::Ptr& actor, AiWanderStorage& storage); - bool reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos); - inline bool isPackageCompleted() const; - void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance); - bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination); - void completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage); - bool isNearAllowedNode(const MWWorld::Ptr &actor, const AiWanderStorage& storage, float distance) const; + private: + void stopWalking(const MWWorld::Ptr& actor); - const int mDistance; // how far the actor can wander from the spawn point - const int mDuration; - float mRemainingDuration; - const int mTimeOfDay; - const std::vector mIdle; + /// Have the given actor play an idle animation + /// @return Success or error + bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); + bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); + int getRandomIdle() const; + void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); + void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage); + void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, + MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage); + void onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); + void onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, + MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage); + void onChooseActionStatePerFrameActions(const MWWorld::Ptr& actor, AiWanderStorage& storage); + bool reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos); + inline bool isPackageCompleted() const; + void wanderNearStart(const MWWorld::Ptr& actor, AiWanderStorage& storage, int wanderDistance); + bool destinationIsAtWater(const MWWorld::Ptr& actor, const osg::Vec3f& destination); + void completeManualWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage); + bool isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; - bool mStoredInitialActorPosition; - osg::Vec3f mInitialActorPosition; // Note: an original engine does not reset coordinates even when actor changes a cell + const int mDistance; // how far the actor can wander from the spawn point + const int mDuration; + float mRemainingDuration; + const int mTimeOfDay; + const std::vector mIdle; - bool mHasDestination; - osg::Vec3f mDestination; - bool mUsePathgrid; + bool mStoredInitialActorPosition; + osg::Vec3f + mInitialActorPosition; // Note: an original engine does not reset coordinates even when actor changes a cell - void getNeighbouringNodes(ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); + bool mHasDestination; + osg::Vec3f mDestination; + bool mUsePathgrid; - void getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage); + void getNeighbouringNodes( + ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); - void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); + void getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage); - // constants for converting idleSelect values into groupNames - enum GroupIndex - { - GroupIndex_MinIdle = 2, - GroupIndex_MaxIdle = 9 - }; + void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); - /// convert point from local (i.e. cell) to world coordinates - void ToWorldCoordinates(ESM::Pathgrid::Point& point, const ESM::Cell * cell); + // constants for converting idleSelect values into groupNames + enum GroupIndex + { + GroupIndex_MinIdle = 2, + GroupIndex_MaxIdle = 9 + }; - void SetCurrentNodeToClosestAllowedNode(const osg::Vec3f& npcPos, AiWanderStorage& storage); + void setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage); - void AddNonPathGridAllowedPoints(osg::Vec3f npcPos, const ESM::Pathgrid * pathGrid, int pointIndex, AiWanderStorage& storage); + void addNonPathGridAllowedPoints(const ESM::Pathgrid* pathGrid, size_t pointIndex, AiWanderStorage& storage, + const Misc::CoordinateConverter& converter); - void AddPointBetweenPathGridPoints(const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage); + void AddPointBetweenPathGridPoints( + const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage); - /// lookup table for converting idleSelect value to groupName - static const std::string sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1]; + /// lookup table for converting idleSelect value to groupName + static const std::string sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1]; - static int OffsetToPreventOvercrowding(); + static int OffsetToPreventOvercrowding(); }; } diff --git a/apps/openmw/mwmechanics/alchemy.cpp b/apps/openmw/mwmechanics/alchemy.cpp index 61fec3b543b..4be48296a92 100644 --- a/apps/openmw/mwmechanics/alchemy.cpp +++ b/apps/openmw/mwmechanics/alchemy.cpp @@ -4,68 +4,87 @@ #include #include -#include #include +#include #include -#include -#include -#include -#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/cellstore.hpp" +#include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" -#include "magiceffects.hpp" #include "creaturestats.hpp" +#include "magiceffects.hpp" + +namespace +{ + constexpr size_t sNumEffects = 4; + + std::optional toKey(const ESM::Ingredient& ingredient, size_t i) + { + if (ingredient.mData.mEffectID[i] < 0) + return {}; + ESM::RefId arg = ESM::Skill::indexToRefId(ingredient.mData.mSkills[i]); + if (arg.empty()) + arg = ESM::Attribute::indexToRefId(ingredient.mData.mAttributes[i]); + return MWMechanics::EffectKey(ingredient.mData.mEffectID[i], arg); + } + + bool containsEffect(const ESM::Ingredient& ingredient, const MWMechanics::EffectKey& effect) + { + for (size_t j = 0; j < sNumEffects; ++j) + { + if (toKey(ingredient, j) == effect) + return true; + } + return false; + } +} MWMechanics::Alchemy::Alchemy() : mValue(0) - , mPotionName("") { } -std::set MWMechanics::Alchemy::listEffects() const +std::vector MWMechanics::Alchemy::listEffects() const { - std::map effects; - - for (TIngredientsIterator iter (mIngredients.begin()); iter!=mIngredients.end(); ++iter) + // We care about the order of these effects as each effect can affect the next when applied. + // The player can affect effect order by placing ingredients into different slots + std::vector effects; + for (size_t slotI = 0; slotI < mIngredients.size() - 1; ++slotI) { - if (!iter->isEmpty()) + if (mIngredients[slotI].isEmpty()) + continue; + const ESM::Ingredient* ingredient = mIngredients[slotI].get()->mBase; + for (size_t slotJ = slotI + 1; slotJ < mIngredients.size(); ++slotJ) { - const MWWorld::LiveCellRef *ingredient = iter->get(); - - std::set seenEffects; - - for (int i=0; i<4; ++i) - if (ingredient->mBase->mData.mEffectID[i]!=-1) + if (mIngredients[slotJ].isEmpty()) + continue; + const ESM::Ingredient* ingredient2 = mIngredients[slotJ].get()->mBase; + for (size_t i = 0; i < sNumEffects; ++i) + { + if (const auto key = toKey(*ingredient, i)) { - EffectKey key ( - ingredient->mBase->mData.mEffectID[i], ingredient->mBase->mData.mSkills[i]!=-1 ? - ingredient->mBase->mData.mSkills[i] : ingredient->mBase->mData.mAttributes[i]); - - if (seenEffects.insert(key).second) - ++effects[key]; + if (std::find(effects.begin(), effects.end(), *key) != effects.end()) + continue; + if (containsEffect(*ingredient2, *key)) + effects.push_back(*key); } + } } } - - std::set effects2; - - for (std::map::const_iterator iter (effects.begin()); iter!=effects.end(); ++iter) - if (iter->second>1) - effects2.insert (iter->first); - - return effects2; + return effects; } -void MWMechanics::Alchemy::applyTools (int flags, float& value) const +void MWMechanics::Alchemy::applyTools(int flags, float& value) const { bool magnitude = !(flags & ESM::MagicEffect::NoMagnitude); bool duration = !(flags & ESM::MagicEffect::NoDuration); @@ -84,9 +103,10 @@ void MWMechanics::Alchemy::applyTools (int flags, float& value) const else return; - float toolQuality = setup==1 || setup==2 ? mTools[tool].get()->mBase->mData.mQuality : 0; - float calcinatorQuality = setup==1 || setup==3 ? - mTools[ESM::Apparatus::Calcinator].get()->mBase->mData.mQuality : 0; + float toolQuality = setup == 1 || setup == 2 ? mTools[tool].get()->mBase->mData.mQuality : 0; + float calcinatorQuality = setup == 1 || setup == 3 + ? mTools[ESM::Apparatus::Calcinator].get()->mBase->mData.mQuality + : 0; float quality = 1; @@ -94,14 +114,14 @@ void MWMechanics::Alchemy::applyTools (int flags, float& value) const { case 1: - quality = negative ? 2 * toolQuality + 3 * calcinatorQuality : - (magnitude && duration ? - 2 * toolQuality + calcinatorQuality : 2/3.0f * (toolQuality + calcinatorQuality) + 0.5f); + quality = negative ? 2 * toolQuality + 3 * calcinatorQuality + : (magnitude && duration ? 2 * toolQuality + calcinatorQuality + : 2 / 3.0f * (toolQuality + calcinatorQuality) + 0.5f); break; case 2: - quality = negative ? 1+toolQuality : (magnitude && duration ? toolQuality : toolQuality + 0.5f); + quality = negative ? 1 + toolQuality : (magnitude && duration ? toolQuality : toolQuality + 0.5f); break; case 3: @@ -110,14 +130,14 @@ void MWMechanics::Alchemy::applyTools (int flags, float& value) const break; } - if (setup==3 || !negative) + if (setup == 3 || !negative) { value += quality; } else { - if (quality==0) - throw std::runtime_error ("invalid derived alchemy apparatus quality"); + if (quality == 0) + throw std::runtime_error("invalid derived alchemy apparatus quality"); value /= quality; } @@ -128,72 +148,84 @@ void MWMechanics::Alchemy::updateEffects() mEffects.clear(); mValue = 0; - if (countIngredients()<2 || mAlchemist.isEmpty() || mTools[ESM::Apparatus::MortarPestle].isEmpty()) + if (countIngredients() < 2 || mAlchemist.isEmpty() || mTools[ESM::Apparatus::MortarPestle].isEmpty()) return; // find effects - std::set effects (listEffects()); + std::vector effects = listEffects(); // general alchemy factor float x = getAlchemyFactor(); x *= mTools[ESM::Apparatus::MortarPestle].get()->mBase->mData.mQuality; - x *= MWBase::Environment::get().getWorld()->getStore().get().find ("fPotionStrengthMult")->mValue.getFloat(); + x *= MWBase::Environment::get() + .getESMStore() + ->get() + .find("fPotionStrengthMult") + ->mValue.getFloat(); // value - mValue = static_cast ( - x * MWBase::Environment::get().getWorld()->getStore().get().find ("iAlchemyMod")->mValue.getFloat()); + mValue = static_cast( + x * MWBase::Environment::get().getESMStore()->get().find("iAlchemyMod")->mValue.getFloat()); // build quantified effect list - for (std::set::const_iterator iter (effects.begin()); iter!=effects.end(); ++iter) + for (const auto& effectKey : effects) { - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find (iter->mId); + const ESM::MagicEffect* magicEffect + = MWBase::Environment::get().getESMStore()->get().find(effectKey.mId); - if (magicEffect->mData.mBaseCost<=0) + if (magicEffect->mData.mBaseCost <= 0) { - const std::string os = "invalid base cost for magic effect " + std::to_string(iter->mId); - throw std::runtime_error (os); + const std::string os = "invalid base cost for magic effect " + std::to_string(effectKey.mId); + throw std::runtime_error(os); } - float fPotionT1MagMul = - MWBase::Environment::get().getWorld()->getStore().get().find ("fPotionT1MagMult")->mValue.getFloat(); + float fPotionT1MagMul = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fPotionT1MagMult") + ->mValue.getFloat(); - if (fPotionT1MagMul<=0) - throw std::runtime_error ("invalid gmst: fPotionT1MagMul"); + if (fPotionT1MagMul <= 0) + throw std::runtime_error("invalid gmst: fPotionT1MagMul"); - float fPotionT1DurMult = - MWBase::Environment::get().getWorld()->getStore().get().find ("fPotionT1DurMult")->mValue.getFloat(); + float fPotionT1DurMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fPotionT1DurMult") + ->mValue.getFloat(); - if (fPotionT1DurMult<=0) - throw std::runtime_error ("invalid gmst: fPotionT1DurMult"); + if (fPotionT1DurMult <= 0) + throw std::runtime_error("invalid gmst: fPotionT1DurMult"); - float magnitude = (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) ? - 1.0f : (x / fPotionT1MagMul) / magicEffect->mData.mBaseCost; - float duration = (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) ? - 1.0f : (x / fPotionT1DurMult) / magicEffect->mData.mBaseCost; + float magnitude = (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) + ? 1.0f + : (x / fPotionT1MagMul) / magicEffect->mData.mBaseCost; + float duration = (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + ? 1.0f + : (x / fPotionT1DurMult) / magicEffect->mData.mBaseCost; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) - applyTools (magicEffect->mData.mFlags, magnitude); + applyTools(magicEffect->mData.mFlags, magnitude); if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) - applyTools (magicEffect->mData.mFlags, duration); + applyTools(magicEffect->mData.mFlags, duration); duration = roundf(duration); magnitude = roundf(magnitude); - if (magnitude>0 && duration>0) + if (magnitude > 0 && duration > 0) { ESM::ENAMstruct effect; - effect.mEffectID = iter->mId; + effect.mEffectID = effectKey.mId; effect.mAttribute = -1; effect.mSkill = -1; if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) - effect.mSkill = iter->mArg; + effect.mSkill = ESM::Skill::refIdToIndex(effectKey.mArg); else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) - effect.mAttribute = iter->mArg; + effect.mAttribute = ESM::Attribute::refIdToIndex(effectKey.mArg); effect.mRange = 0; effect.mArea = 0; @@ -201,15 +233,14 @@ void MWMechanics::Alchemy::updateEffects() effect.mDuration = static_cast(duration); effect.mMagnMin = effect.mMagnMax = static_cast(magnitude); - mEffects.push_back (effect); + mEffects.push_back(effect); } } } -const ESM::Potion *MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) const +const ESM::Potion* MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) const { - const MWWorld::Store &potions = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& potions = MWBase::Environment::get().getESMStore()->get(); MWWorld::Store::iterator iter = potions.begin(); for (; iter != potions.end(); ++iter) @@ -217,11 +248,9 @@ const ESM::Potion *MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) co if (iter->mEffects.mList.size() != mEffects.size()) continue; - if (iter->mName != toFind.mName - || iter->mScript != toFind.mScript - || iter->mData.mWeight != toFind.mData.mWeight - || iter->mData.mValue != toFind.mData.mValue - || iter->mData.mAutoCalc != toFind.mData.mAutoCalc) + if (iter->mName != toFind.mName || iter->mScript != toFind.mScript + || iter->mData.mWeight != toFind.mData.mWeight || iter->mData.mValue != toFind.mData.mValue + || iter->mData.mFlags != toFind.mData.mFlags) continue; // Don't choose an ID that came from the content files, would have unintended side effects @@ -231,19 +260,15 @@ const ESM::Potion *MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) co bool mismatch = false; - for (int i=0; i (iter->mEffects.mList.size()); ++i) + for (size_t i = 0; i < iter->mEffects.mList.size(); ++i) { - const ESM::ENAMstruct& first = iter->mEffects.mList[i]; + const ESM::IndexedENAMstruct& first = iter->mEffects.mList[i]; const ESM::ENAMstruct& second = mEffects[i]; - if (first.mEffectID!=second.mEffectID || - first.mArea!=second.mArea || - first.mRange!=second.mRange || - first.mSkill!=second.mSkill || - first.mAttribute!=second.mAttribute || - first.mMagnMin!=second.mMagnMin || - first.mMagnMax!=second.mMagnMax || - first.mDuration!=second.mDuration) + if (first.mData.mEffectID != second.mEffectID || first.mData.mArea != second.mArea + || first.mData.mRange != second.mRange || first.mData.mSkill != second.mSkill + || first.mData.mAttribute != second.mAttribute || first.mData.mMagnMin != second.mMagnMin + || first.mData.mMagnMax != second.mMagnMax || first.mData.mDuration != second.mDuration) { mismatch = true; break; @@ -259,25 +284,25 @@ const ESM::Potion *MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) co void MWMechanics::Alchemy::removeIngredients() { - for (TIngredientsContainer::iterator iter (mIngredients.begin()); iter!=mIngredients.end(); ++iter) + for (TIngredientsContainer::iterator iter(mIngredients.begin()); iter != mIngredients.end(); ++iter) if (!iter->isEmpty()) { - iter->getContainerStore()->remove(*iter, 1, mAlchemist); + iter->getContainerStore()->remove(*iter, 1); - if (iter->getRefData().getCount()<1) + if (iter->getCellRef().getCount() < 1) *iter = MWWorld::Ptr(); } updateEffects(); } -void MWMechanics::Alchemy::addPotion (const std::string& name) +void MWMechanics::Alchemy::addPotion(const std::string& name) { ESM::Potion newRecord; newRecord.mData.mWeight = 0; - for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter) + for (TIngredientsIterator iter(beginIngredients()); iter != endIngredients(); ++iter) if (!iter->isEmpty()) newRecord.mData.mWeight += iter->get()->mBase->mData.mWeight; @@ -285,47 +310,48 @@ void MWMechanics::Alchemy::addPotion (const std::string& name) newRecord.mData.mWeight /= countIngredients(); newRecord.mData.mValue = mValue; - newRecord.mData.mAutoCalc = 0; + newRecord.mData.mFlags = 0; + newRecord.mRecordFlags = 0; newRecord.mName = name; - int index = Misc::Rng::rollDice(6); - assert (index>=0 && index<6); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int index = Misc::Rng::rollDice(6, prng); + assert(index >= 0 && index < 6); - static const char *meshes[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" }; + static const char* meshes[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" }; - newRecord.mModel = "m\\misc_potion_" + std::string (meshes[index]) + "_01.nif"; - newRecord.mIcon = "m\\tx_potion_" + std::string (meshes[index]) + "_01.dds"; + newRecord.mModel = "m\\misc_potion_" + std::string(meshes[index]) + "_01.nif"; + newRecord.mIcon = "m\\tx_potion_" + std::string(meshes[index]) + "_01.dds"; - newRecord.mEffects.mList = mEffects; + newRecord.mEffects.populate(mEffects); const ESM::Potion* record = getRecord(newRecord); if (!record) - record = MWBase::Environment::get().getWorld()->createRecord (newRecord); + record = MWBase::Environment::get().getESMStore()->insert(newRecord); - mAlchemist.getClass().getContainerStore (mAlchemist).add (record->mId, 1, mAlchemist); + mAlchemist.getClass().getContainerStore(mAlchemist).add(record->mId, 1); } void MWMechanics::Alchemy::increaseSkill() { - mAlchemist.getClass().skillUsageSucceeded (mAlchemist, ESM::Skill::Alchemy, 0); + mAlchemist.getClass().skillUsageSucceeded(mAlchemist, ESM::Skill::Alchemy, ESM::Skill::Alchemy_CreatePotion); } float MWMechanics::Alchemy::getAlchemyFactor() const { - const CreatureStats& creatureStats = mAlchemist.getClass().getCreatureStats (mAlchemist); + const CreatureStats& creatureStats = mAlchemist.getClass().getCreatureStats(mAlchemist); - return - (mAlchemist.getClass().getSkill(mAlchemist, ESM::Skill::Alchemy) + - 0.1f * creatureStats.getAttribute (ESM::Attribute::Intelligence).getModified() - + 0.1f * creatureStats.getAttribute (ESM::Attribute::Luck).getModified()); + return (mAlchemist.getClass().getSkill(mAlchemist, ESM::Skill::Alchemy) + + 0.1f * creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified() + + 0.1f * creatureStats.getAttribute(ESM::Attribute::Luck).getModified()); } int MWMechanics::Alchemy::countIngredients() const { int ingredients = 0; - for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter) + for (TIngredientsIterator iter(beginIngredients()); iter != endIngredients(); ++iter) if (!iter->isEmpty()) ++ingredients; @@ -340,10 +366,10 @@ int MWMechanics::Alchemy::countPotionsToBrew() const int toBrew = -1; - for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter) + for (TIngredientsIterator iter(beginIngredients()); iter != endIngredients(); ++iter) if (!iter->isEmpty()) { - int count = iter->getRefData().getCount(); + int count = iter->getCellRef().getCount(); if ((count > 0 && count < toBrew) || toBrew < 0) toBrew = count; } @@ -351,34 +377,42 @@ int MWMechanics::Alchemy::countPotionsToBrew() const return toBrew; } -void MWMechanics::Alchemy::setAlchemist (const MWWorld::Ptr& npc) +void MWMechanics::Alchemy::setAlchemist(const MWWorld::Ptr& npc) { mAlchemist = npc; - mIngredients.resize (4); + mIngredients.resize(4); + + std::fill(mIngredients.begin(), mIngredients.end(), MWWorld::Ptr()); - std::fill (mIngredients.begin(), mIngredients.end(), MWWorld::Ptr()); + mTools.resize(4); - mTools.resize (4); + std::vector prevTools(mTools); - std::fill (mTools.begin(), mTools.end(), MWWorld::Ptr()); + std::fill(mTools.begin(), mTools.end(), MWWorld::Ptr()); mEffects.clear(); - MWWorld::ContainerStore& store = npc.getClass().getContainerStore (npc); + MWWorld::ContainerStore& store = npc.getClass().getContainerStore(npc); - for (MWWorld::ContainerStoreIterator iter (store.begin (MWWorld::ContainerStore::Type_Apparatus)); - iter!=store.end(); ++iter) + for (MWWorld::ContainerStoreIterator iter(store.begin(MWWorld::ContainerStore::Type_Apparatus)); + iter != store.end(); ++iter) { MWWorld::LiveCellRef* ref = iter->get(); int type = ref->mBase->mData.mType; - if (type<0 || type>=static_cast (mTools.size())) - throw std::runtime_error ("invalid apparatus type"); + if (type < 0 || type >= static_cast(mTools.size())) + throw std::runtime_error("invalid apparatus type"); + + if (prevTools[type] == *iter) + mTools[type] = *iter; // prefer the previous tool if still in the container + + if (!mTools[type].isEmpty() && !prevTools[type].isEmpty() && mTools[type] == prevTools[type]) + continue; if (!mTools[type].isEmpty()) - if (ref->mBase->mData.mQuality<=mTools[type].get()->mBase->mData.mQuality) + if (ref->mBase->mData.mQuality <= mTools[type].get()->mBase->mData.mQuality) continue; mTools[type] = *iter; @@ -408,7 +442,6 @@ MWMechanics::Alchemy::TIngredientsIterator MWMechanics::Alchemy::endIngredients( void MWMechanics::Alchemy::clear() { mAlchemist = MWWorld::Ptr(); - mTools.clear(); mIngredients.clear(); mEffects.clear(); setPotionName(""); @@ -419,24 +452,23 @@ void MWMechanics::Alchemy::setPotionName(const std::string& name) mPotionName = name; } -int MWMechanics::Alchemy::addIngredient (const MWWorld::Ptr& ingredient) +int MWMechanics::Alchemy::addIngredient(const MWWorld::Ptr& ingredient) { // find a free slot int slot = -1; - for (int i=0; i (mIngredients.size()); ++i) + for (int i = 0; i < static_cast(mIngredients.size()); ++i) if (mIngredients[i].isEmpty()) { slot = i; break; } - if (slot==-1) + if (slot == -1) return -1; - for (TIngredientsIterator iter (mIngredients.begin()); iter!=mIngredients.end(); ++iter) - if (!iter->isEmpty() && Misc::StringUtils::ciEqual(ingredient.getCellRef().getRefId(), - iter->getCellRef().getRefId())) + for (TIngredientsIterator iter(mIngredients.begin()); iter != mIngredients.end(); ++iter) + if (!iter->isEmpty() && ingredient.getCellRef().getRefId() == iter->getCellRef().getRefId()) return -1; mIngredients[slot] = ingredient; @@ -446,15 +478,33 @@ int MWMechanics::Alchemy::addIngredient (const MWWorld::Ptr& ingredient) return slot; } -void MWMechanics::Alchemy::removeIngredient (int index) +void MWMechanics::Alchemy::removeIngredient(size_t index) { - if (index>=0 && index (mIngredients.size())) + if (index < mIngredients.size()) { mIngredients[index] = MWWorld::Ptr(); updateEffects(); } } +void MWMechanics::Alchemy::addApparatus(const MWWorld::Ptr& apparatus) +{ + int32_t slot = apparatus.get()->mBase->mData.mType; + + mTools[slot] = apparatus; + + updateEffects(); +} + +void MWMechanics::Alchemy::removeApparatus(size_t index) +{ + if (index < mTools.size()) + { + mTools[index] = MWWorld::Ptr(); + updateEffects(); + } +} + MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::beginEffects() const { return mEffects.begin(); @@ -465,15 +515,15 @@ MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::endEffects() const return mEffects.end(); } -bool MWMechanics::Alchemy::knownEffect(unsigned int potionEffectIndex, const MWWorld::Ptr &npc) +bool MWMechanics::Alchemy::knownEffect(unsigned int potionEffectIndex, const MWWorld::Ptr& npc) { - float alchemySkill = npc.getClass().getSkill (npc, ESM::Skill::Alchemy); - static const float fWortChanceValue = - MWBase::Environment::get().getWorld()->getStore().get().find("fWortChanceValue")->mValue.getFloat(); + float alchemySkill = npc.getClass().getSkill(npc, ESM::Skill::Alchemy); + static const float fWortChanceValue + = MWBase::Environment::get().getESMStore()->get().find("fWortChanceValue")->mValue.getFloat(); return (potionEffectIndex <= 1 && alchemySkill >= fWortChanceValue) - || (potionEffectIndex <= 3 && alchemySkill >= fWortChanceValue*2) - || (potionEffectIndex <= 5 && alchemySkill >= fWortChanceValue*3) - || (potionEffectIndex <= 7 && alchemySkill >= fWortChanceValue*4); + || (potionEffectIndex <= 3 && alchemySkill >= fWortChanceValue * 2) + || (potionEffectIndex <= 5 && alchemySkill >= fWortChanceValue * 3) + || (potionEffectIndex <= 7 && alchemySkill >= fWortChanceValue * 4); } MWMechanics::Alchemy::Result MWMechanics::Alchemy::getReadyStatus() const @@ -481,7 +531,7 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::getReadyStatus() const if (mTools[ESM::Apparatus::MortarPestle].isEmpty()) return Result_NoMortarAndPestle; - if (countIngredients()<2) + if (countIngredients() < 2) return Result_LessThanTwoIngredients; if (mPotionName.empty()) @@ -493,7 +543,7 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::getReadyStatus() const return Result_Success; } -MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& name, int& count) +MWMechanics::Alchemy::Result MWMechanics::Alchemy::create(const std::string& name, int& count) { setPotionName(name); Result readyStatus = getReadyStatus(); @@ -504,6 +554,8 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& na if (readyStatus != Result_Success) return readyStatus; + MWBase::Environment::get().getWorld()->breakInvisibility(mAlchemist); + Result result = Result_RandomFailure; int brewedCount = 0; for (int i = 0; i < count; ++i) @@ -519,7 +571,7 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& na return result; } -MWMechanics::Alchemy::Result MWMechanics::Alchemy::createSingle () +MWMechanics::Alchemy::Result MWMechanics::Alchemy::createSingle() { if (beginEffects() == endEffects()) { @@ -527,8 +579,8 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::createSingle () removeIngredients(); return Result_RandomFailure; } - - if (getAlchemyFactor() < Misc::Rng::roll0to99()) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (getAlchemyFactor() < Misc::Rng::roll0to99(prng)) { removeIngredients(); return Result_RandomFailure; @@ -545,44 +597,38 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::createSingle () std::string MWMechanics::Alchemy::suggestPotionName() { - std::set effects = listEffects(); + std::vector effects = listEffects(); if (effects.empty()) - return ""; + return {}; - int effectId = effects.begin()->mId; - return MWBase::Environment::get().getWorld()->getStore().get().find( - ESM::MagicEffect::effectIdToString(effectId))->mValue.getString(); + return effects.begin()->toString(); } -std::vector MWMechanics::Alchemy::effectsDescription (const MWWorld::ConstPtr &ptr, const int alchemySkill) +std::vector MWMechanics::Alchemy::effectsDescription(const MWWorld::ConstPtr& ptr, const int alchemySkill) { std::vector effects; const auto& item = ptr.get()->mBase; - const auto& gmst = MWBase::Environment::get().getWorld()->getStore().get(); - const static auto fWortChanceValue = gmst.find("fWortChanceValue")->mValue.getFloat(); + const auto& store = MWBase::Environment::get().getESMStore(); + const auto& mgef = store->get(); + const static auto fWortChanceValue = store->get().find("fWortChanceValue")->mValue.getFloat(); const auto& data = item->mData; - for (auto i = 0; i < 4; ++i) + for (size_t i = 0; i < sNumEffects; ++i) { const auto effectID = data.mEffectID[i]; - const auto skillID = data.mSkills[i]; - const auto attributeID = data.mAttributes[i]; - if (alchemySkill < fWortChanceValue * (i + 1)) + if (alchemySkill < fWortChanceValue * static_cast(i + 1)) break; if (effectID != -1) { - std::string effect = gmst.find(ESM::MagicEffect::effectIdToString(effectID))->mValue.getString(); - - if (skillID != -1) - effect += " " + gmst.find(ESM::Skill::sSkillNameIds[skillID])->mValue.getString(); - else if (attributeID != -1) - effect += " " + gmst.find(ESM::Attribute::sGmstAttributeIds[attributeID])->mValue.getString(); + const ESM::Attribute* attribute + = store->get().search(ESM::Attribute::indexToRefId(data.mAttributes[i])); + const ESM::Skill* skill = store->get().search(ESM::Skill::indexToRefId(data.mSkills[i])); + std::string effect = getMagicEffectString(*mgef.find(effectID), attribute, skill); effects.push_back(effect); - } } return effects; diff --git a/apps/openmw/mwmechanics/alchemy.hpp b/apps/openmw/mwmechanics/alchemy.hpp index d23f978ead2..373ca8b887c 100644 --- a/apps/openmw/mwmechanics/alchemy.hpp +++ b/apps/openmw/mwmechanics/alchemy.hpp @@ -2,9 +2,8 @@ #define GAME_MWMECHANICS_ALCHEMY_H #include -#include -#include +#include #include "../mwworld/ptr.hpp" @@ -20,121 +19,124 @@ namespace MWMechanics /// \brief Potion creation via alchemy skill class Alchemy { - public: + public: + Alchemy(); - Alchemy(); + typedef std::vector TToolsContainer; + typedef TToolsContainer::const_iterator TToolsIterator; - typedef std::vector TToolsContainer; - typedef TToolsContainer::const_iterator TToolsIterator; + typedef std::vector TIngredientsContainer; + typedef TIngredientsContainer::const_iterator TIngredientsIterator; - typedef std::vector TIngredientsContainer; - typedef TIngredientsContainer::const_iterator TIngredientsIterator; + typedef std::vector TEffectsContainer; + typedef TEffectsContainer::const_iterator TEffectsIterator; - typedef std::vector TEffectsContainer; - typedef TEffectsContainer::const_iterator TEffectsIterator; + enum Result + { + Result_Success, - enum Result - { - Result_Success, + Result_NoMortarAndPestle, + Result_LessThanTwoIngredients, + Result_NoName, + Result_NoEffects, + Result_RandomFailure + }; - Result_NoMortarAndPestle, - Result_LessThanTwoIngredients, - Result_NoName, - Result_NoEffects, - Result_RandomFailure - }; + private: + MWWorld::Ptr mAlchemist; + TToolsContainer mTools; + TIngredientsContainer mIngredients; + TEffectsContainer mEffects; + int mValue; + std::string mPotionName; - private: + void applyTools(int flags, float& value) const; - MWWorld::Ptr mAlchemist; - TToolsContainer mTools; - TIngredientsContainer mIngredients; - TEffectsContainer mEffects; - int mValue; - std::string mPotionName; + void updateEffects(); - void applyTools (int flags, float& value) const; + Result getReadyStatus() const; - void updateEffects(); + const ESM::Potion* getRecord(const ESM::Potion& toFind) const; + ///< Try to find a potion record similar to \a toFind in the record store, or return 0 if not found + /// \note Does not account for record ID, model or icon - Result getReadyStatus() const; + void removeIngredients(); + ///< Remove selected ingredients from alchemist's inventory, cleanup selected ingredients and + /// update effect list accordingly. - const ESM::Potion *getRecord(const ESM::Potion& toFind) const; - ///< Try to find a potion record similar to \a toFind in the record store, or return 0 if not found - /// \note Does not account for record ID, model or icon + void addPotion(const std::string& name); + ///< Add a potion to the alchemist's inventory. - void removeIngredients(); - ///< Remove selected ingredients from alchemist's inventory, cleanup selected ingredients and - /// update effect list accordingly. + void increaseSkill(); + ///< Increase alchemist's skill. - void addPotion (const std::string& name); - ///< Add a potion to the alchemist's inventory. + Result createSingle(); + ///< Try to create a potion from the ingredients, place it in the inventory of the alchemist and + /// adjust the skills of the alchemist accordingly. - void increaseSkill(); - ///< Increase alchemist's skill. + float getAlchemyFactor() const; - Result createSingle (); - ///< Try to create a potion from the ingredients, place it in the inventory of the alchemist and - /// adjust the skills of the alchemist accordingly. + int countIngredients() const; - float getAlchemyFactor() const; + TEffectsIterator beginEffects() const; - int countIngredients() const; + TEffectsIterator endEffects() const; - TEffectsIterator beginEffects() const; + public: + int countPotionsToBrew() const; + ///< calculates maximum amount of potions, which you can make from selected ingredients - TEffectsIterator endEffects() const; + static bool knownEffect(unsigned int potionEffectIndex, const MWWorld::Ptr& npc); + ///< Does npc have sufficient alchemy skill to know about this potion effect? - public: - int countPotionsToBrew() const; - ///< calculates maximum amount of potions, which you can make from selected ingredients + void setAlchemist(const MWWorld::Ptr& npc); + ///< Set alchemist and configure alchemy setup accordingly. \a npc may be empty to indicate that + /// there is no alchemist (alchemy session has ended). - static bool knownEffect (unsigned int potionEffectIndex, const MWWorld::Ptr& npc); - ///< Does npc have sufficient alchemy skill to know about this potion effect? + TToolsIterator beginTools() const; + ///< \attention Iterates over tool slots, not over tools. Some of the slots may be empty. - void setAlchemist (const MWWorld::Ptr& npc); - ///< Set alchemist and configure alchemy setup accordingly. \a npc may be empty to indicate that - /// there is no alchemist (alchemy session has ended). + TToolsIterator endTools() const; - TToolsIterator beginTools() const; - ///< \attention Iterates over tool slots, not over tools. Some of the slots may be empty. + TIngredientsIterator beginIngredients() const; + ///< \attention Iterates over ingredient slots, not over ingredients. Some of the slots may be empty. - TToolsIterator endTools() const; + TIngredientsIterator endIngredients() const; - TIngredientsIterator beginIngredients() const; - ///< \attention Iterates over ingredient slots, not over ingredients. Some of the slots may be empty. + void clear(); + ///< Remove alchemist, tools and ingredients. - TIngredientsIterator endIngredients() const; + void setPotionName(const std::string& name); + ///< Set name of potion to create - void clear(); - ///< Remove alchemist, tools and ingredients. + std::vector listEffects() const; + ///< List all effects shared by at least two ingredients. - void setPotionName(const std::string& name); - ///< Set name of potion to create + int addIngredient(const MWWorld::Ptr& ingredient); + ///< Add ingredient into the next free slot. + /// + /// \return Slot index or -1, if adding failed because of no free slot or the ingredient type being + /// listed already. - std::set listEffects() const; - ///< List all effects shared by at least two ingredients. + void addApparatus(const MWWorld::Ptr& apparatus); + ///< Add apparatus into the appropriate slot. - int addIngredient (const MWWorld::Ptr& ingredient); - ///< Add ingredient into the next free slot. - /// - /// \return Slot index or -1, if adding failed because of no free slot or the ingredient type being - /// listed already. + void removeIngredient(size_t index); + ///< Remove ingredient from slot (calling this function on an empty slot is a no-op). - void removeIngredient (int index); - ///< Remove ingredient from slot (calling this function on an empty slot is a no-op). + void removeApparatus(size_t index); + ///< Remove apparatus from slot. - std::string suggestPotionName (); - ///< Suggest a name for the potion, based on the current effects + std::string suggestPotionName(); + ///< Suggest a name for the potion, based on the current effects - Result create (const std::string& name, int& count); - ///< Try to create potions from the ingredients, place them in the inventory of the alchemist and - /// adjust the skills of the alchemist accordingly. - /// \param name must not be an empty string, or Result_NoName is returned + Result create(const std::string& name, int& count); + ///< Try to create potions from the ingredients, place them in the inventory of the alchemist and + /// adjust the skills of the alchemist accordingly. + /// \param name must not be an empty string, or Result_NoName is returned - static std::vector effectsDescription (const MWWorld::ConstPtr &ptr, const int alchemySKill); + static std::vector effectsDescription(const MWWorld::ConstPtr& ptr, const int alchemySKill); }; } #endif - diff --git a/apps/openmw/mwmechanics/attacktype.hpp b/apps/openmw/mwmechanics/attacktype.hpp new file mode 100644 index 00000000000..3824f5bbe78 --- /dev/null +++ b/apps/openmw/mwmechanics/attacktype.hpp @@ -0,0 +1,16 @@ +#ifndef OPENMW_MWMECHANICS_ATTACKTYPE_H +#define OPENMW_MWMECHANICS_ATTACKTYPE_H + +namespace MWMechanics +{ + enum class AttackType + { + NoAttack, + Any, + Chop, + Slash, + Thrust + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/autocalcspell.cpp b/apps/openmw/mwmechanics/autocalcspell.cpp index 662cfe473bd..5bab25fbe56 100644 --- a/apps/openmw/mwmechanics/autocalcspell.cpp +++ b/apps/openmw/mwmechanics/autocalcspell.cpp @@ -2,9 +2,14 @@ #include +#include +#include +#include +#include +#include + #include "../mwworld/esmstore.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "spellutil.hpp" @@ -18,46 +23,34 @@ namespace MWMechanics int mLimit; bool mReachedLimit; int mMinCost; - std::string mWeakestSpell; + ESM::RefId mWeakestSpell; }; - std::vector autoCalcNpcSpells(const int *actorSkills, const int *actorAttributes, const ESM::Race* race) + std::vector autoCalcNpcSpells(const std::map& actorSkills, + const std::map& actorAttributes, const ESM::Race* race) { - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); static const float fNPCbaseMagickaMult = gmst.find("fNPCbaseMagickaMult")->mValue.getFloat(); - float baseMagicka = fNPCbaseMagickaMult * actorAttributes[ESM::Attribute::Intelligence]; - - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - static int iAutoSpellSchoolMax[6]; - static bool init = false; - if (!init) - { - for (int i=0; i<6; ++i) - { - const std::string& gmstName = "iAutoSpell" + schools[i] + "Max"; - iAutoSpellSchoolMax[i] = gmst.find(gmstName)->mValue.getInteger(); - } - init = true; - } + float baseMagicka = fNPCbaseMagickaMult * actorAttributes.at(ESM::Attribute::Intelligence).getBase(); - std::map schoolCaps; - for (int i=0; i<6; ++i) + std::map schoolCaps; + for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) { + if (!skill.mSchool) + continue; SchoolCaps caps; caps.mCount = 0; - caps.mLimit = iAutoSpellSchoolMax[i]; - caps.mReachedLimit = iAutoSpellSchoolMax[i] <= 0; + caps.mLimit = skill.mSchool->mAutoCalcMax; + caps.mReachedLimit = skill.mSchool->mAutoCalcMax <= 0; caps.mMinCost = std::numeric_limits::max(); - caps.mWeakestSpell.clear(); - schoolCaps[i] = caps; + caps.mWeakestSpell = ESM::RefId(); + schoolCaps[skill.mId] = caps; } - std::vector selectedSpells; + std::vector selectedSpells; - const MWWorld::Store &spells = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& spells = MWBase::Environment::get().getESMStore()->get(); // Note: the algorithm heavily depends on the traversal order of the spells. For vanilla-compatible results the // Store must preserve the record ordering as it was in the content files. @@ -68,7 +61,8 @@ namespace MWMechanics if (!(spell.mData.mFlags & ESM::Spell::F_Autocalc)) continue; static const int iAutoSpellTimesCanCast = gmst.find("iAutoSpellTimesCanCast")->mValue.getInteger(); - if (baseMagicka < iAutoSpellTimesCanCast * spell.mData.mCost) + int spellCost = MWMechanics::calcSpellCost(spell); + if (baseMagicka < iAutoSpellTimesCanCast * spellCost) continue; if (race && race->mPowers.exists(spell.mId)) @@ -77,13 +71,14 @@ namespace MWMechanics if (!attrSkillCheck(&spell, actorSkills, actorAttributes)) continue; - int school; + ESM::RefId school; float skillTerm; calcWeakestSchool(&spell, actorSkills, school, skillTerm); - assert(school >= 0 && school < 6); + if (school.empty()) + continue; SchoolCaps& cap = schoolCaps[school]; - if (cap.mReachedLimit && spell.mData.mCost <= cap.mMinCost) + if (cap.mReachedLimit && spellCost <= cap.mMinCost) continue; static const float fAutoSpellChance = gmst.find("fAutoSpellChance")->mValue.getFloat(); @@ -94,30 +89,32 @@ namespace MWMechanics if (cap.mReachedLimit) { - std::vector::iterator found = std::find(selectedSpells.begin(), selectedSpells.end(), cap.mWeakestSpell); + auto found = std::find(selectedSpells.begin(), selectedSpells.end(), cap.mWeakestSpell); if (found != selectedSpells.end()) selectedSpells.erase(found); cap.mMinCost = std::numeric_limits::max(); - for (const std::string& testSpellName : selectedSpells) + for (const ESM::RefId& testSpellName : selectedSpells) { const ESM::Spell* testSpell = spells.find(testSpellName); + int testSpellCost = MWMechanics::calcSpellCost(*testSpell); - //int testSchool; - //float dummySkillTerm; - //calcWeakestSchool(testSpell, actorSkills, testSchool, dummySkillTerm); + // int testSchool; + // float dummySkillTerm; + // calcWeakestSchool(testSpell, actorSkills, testSchool, dummySkillTerm); // Note: if there are multiple spells with the same cost, we pick the first one we found. // So the algorithm depends on the iteration order of the outer loop. if ( - // There is a huge bug here. It is not checked that weakestSpell is of the correct school. - // As result multiple SchoolCaps could have the same mWeakestSpell. Erasing the weakest spell would then fail if another school - // already erased it, and so the number of spells would often exceed the sum of limits. - // This bug cannot be fixed without significantly changing the results of the spell autocalc, which will not have been playtested. - //testSchool == school && - testSpell->mData.mCost < cap.mMinCost) + // There is a huge bug here. It is not checked that weakestSpell is of the correct school. + // As result multiple SchoolCaps could have the same mWeakestSpell. Erasing the weakest spell + // would then fail if another school already erased it, and so the number of spells would often + // exceed the sum of limits. This bug cannot be fixed without significantly changing the results + // of the spell autocalc, which will not have been playtested. + // testSchool == school && + testSpellCost < cap.mMinCost) { - cap.mMinCost = testSpell->mData.mCost; + cap.mMinCost = testSpellCost; cap.mWeakestSpell = testSpell->mId; } } @@ -128,10 +125,10 @@ namespace MWMechanics if (cap.mCount == cap.mLimit) cap.mReachedLimit = true; - if (spell.mData.mCost < cap.mMinCost) + if (spellCost < cap.mMinCost) { cap.mWeakestSpell = spell.mId; - cap.mMinCost = spell.mData.mCost; + cap.mMinCost = spellCost; } } } @@ -139,35 +136,42 @@ namespace MWMechanics return selectedSpells; } - std::vector autoCalcPlayerSpells(const int* actorSkills, const int* actorAttributes, const ESM::Race* race) + std::vector autoCalcPlayerSpells(const std::map& actorSkills, + const std::map& actorAttributes, const ESM::Race* race) { - const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); - static const float fPCbaseMagickaMult = esmStore.get().find("fPCbaseMagickaMult")->mValue.getFloat(); + static const float fPCbaseMagickaMult + = esmStore.get().find("fPCbaseMagickaMult")->mValue.getFloat(); - float baseMagicka = fPCbaseMagickaMult * actorAttributes[ESM::Attribute::Intelligence]; + float baseMagicka = fPCbaseMagickaMult * actorAttributes.at(ESM::Attribute::Intelligence).getBase(); bool reachedLimit = false; const ESM::Spell* weakestSpell = nullptr; int minCost = std::numeric_limits::max(); - std::vector selectedSpells; + std::vector selectedSpells; - const MWWorld::Store &spells = esmStore.get(); + const MWWorld::Store& spells = esmStore.get(); for (const ESM::Spell& spell : spells) { if (spell.mData.mType != ESM::Spell::ST_Spell) continue; if (!(spell.mData.mFlags & ESM::Spell::F_PCStart)) continue; - if (reachedLimit && spell.mData.mCost <= minCost) + + int spellCost = MWMechanics::calcSpellCost(spell); + if (reachedLimit && spellCost <= minCost) continue; - if (race && std::find(race->mPowers.mList.begin(), race->mPowers.mList.end(), spell.mId) != race->mPowers.mList.end()) + if (race + && std::find(race->mPowers.mList.begin(), race->mPowers.mList.end(), spell.mId) + != race->mPowers.mList.end()) continue; - if (baseMagicka < spell.mData.mCost) + if (baseMagicka < spellCost) continue; - static const float fAutoPCSpellChance = esmStore.get().find("fAutoPCSpellChance")->mValue.getFloat(); - if (calcAutoCastChance(&spell, actorSkills, actorAttributes, -1) < fAutoPCSpellChance) + static const float fAutoPCSpellChance + = esmStore.get().find("fAutoPCSpellChance")->mValue.getFloat(); + if (calcAutoCastChance(&spell, actorSkills, actorAttributes, {}) < fAutoPCSpellChance) continue; if (!attrSkillCheck(&spell, actorSkills, actorAttributes)) @@ -177,29 +181,32 @@ namespace MWMechanics if (reachedLimit) { - std::vector::iterator it = std::find(selectedSpells.begin(), selectedSpells.end(), weakestSpell->mId); + std::vector::iterator it + = std::find(selectedSpells.begin(), selectedSpells.end(), weakestSpell->mId); if (it != selectedSpells.end()) selectedSpells.erase(it); minCost = std::numeric_limits::max(); - for (const std::string& testSpellName : selectedSpells) + for (const ESM::RefId& testSpellName : selectedSpells) { const ESM::Spell* testSpell = esmStore.get().find(testSpellName); - if (testSpell->mData.mCost < minCost) + int testSpellCost = MWMechanics::calcSpellCost(*testSpell); + if (testSpellCost < minCost) { - minCost = testSpell->mData.mCost; + minCost = testSpellCost; weakestSpell = testSpell; } } } else { - if (spell.mData.mCost < minCost) + if (spellCost < minCost) { weakestSpell = &spell; - minCost = weakestSpell->mData.mCost; + minCost = MWMechanics::calcSpellCost(*weakestSpell); } - static const unsigned int iAutoPCSpellMax = esmStore.get().find("iAutoPCSpellMax")->mValue.getInteger(); + static const unsigned int iAutoPCSpellMax + = esmStore.get().find("iAutoPCSpellMax")->mValue.getInteger(); if (selectedSpells.size() == iAutoPCSpellMax) reachedLimit = true; } @@ -208,24 +215,32 @@ namespace MWMechanics return selectedSpells; } - bool attrSkillCheck (const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes) + bool attrSkillCheck(const ESM::Spell* spell, const std::map& actorSkills, + const std::map& actorAttributes) { for (const auto& spellEffect : spell->mEffects.mList) { - const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(spellEffect.mEffectID); - static const int iAutoSpellAttSkillMin = MWBase::Environment::get().getWorld()->getStore().get().find("iAutoSpellAttSkillMin")->mValue.getInteger(); + const ESM::MagicEffect* magicEffect + = MWBase::Environment::get().getESMStore()->get().find(spellEffect.mData.mEffectID); + static const int iAutoSpellAttSkillMin = MWBase::Environment::get() + .getESMStore() + ->get() + .find("iAutoSpellAttSkillMin") + ->mValue.getInteger(); if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill)) { - assert (spellEffect.mSkill >= 0 && spellEffect.mSkill < ESM::Skill::Length); - if (actorSkills[spellEffect.mSkill] < iAutoSpellAttSkillMin) + ESM::RefId skill = ESM::Skill::indexToRefId(spellEffect.mData.mSkill); + auto found = actorSkills.find(skill); + if (found == actorSkills.end() || found->second.getBase() < iAutoSpellAttSkillMin) return false; } if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute)) { - assert (spellEffect.mAttribute >= 0 && spellEffect.mAttribute < ESM::Attribute::Length); - if (actorAttributes[spellEffect.mAttribute] < iAutoSpellAttSkillMin) + ESM::RefId attribute = ESM::Attribute::indexToRefId(spellEffect.mData.mAttribute); + auto found = actorAttributes.find(attribute); + if (found == actorAttributes.end() || found->second.getBase() < iAutoSpellAttSkillMin) return false; } } @@ -233,41 +248,49 @@ namespace MWMechanics return true; } - void calcWeakestSchool (const ESM::Spell* spell, const int* actorSkills, int& effectiveSchool, float& skillTerm) + void calcWeakestSchool(const ESM::Spell* spell, const std::map& actorSkills, + ESM::RefId& effectiveSchool, float& skillTerm) { // Morrowind for some reason uses a formula slightly different from magicka cost calculation float minChance = std::numeric_limits::max(); - for (const ESM::ENAMstruct& effect : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& effect : spell->mEffects.mList) { - const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); + const ESM::MagicEffect* magicEffect + = MWBase::Environment::get().getESMStore()->get().find(effect.mData.mEffectID); int minMagn = 1; int maxMagn = 1; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) { - minMagn = effect.mMagnMin; - maxMagn = effect.mMagnMax; + minMagn = effect.mData.mMagnMin; + maxMagn = effect.mData.mMagnMax; } int duration = 0; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) - duration = effect.mDuration; + duration = effect.mData.mDuration; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) duration = std::max(1, duration); - static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore() - .get().find("fEffectCostMult")->mValue.getFloat(); + static const float fEffectCostMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fEffectCostMult") + ->mValue.getFloat(); float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn)); x *= 0.1 * magicEffect->mData.mBaseCost; x *= 1 + duration; - x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost; + x += 0.05 * std::max(1, effect.mData.mArea) * magicEffect->mData.mBaseCost; x *= fEffectCostMult; - if (effect.mRange == ESM::RT_Target) + if (effect.mData.mRange == ESM::RT_Target) x *= 1.5f; - float s = 2.f * actorSkills[spellSchoolToSkill(magicEffect->mData.mSchool)]; + float s = 0.f; + auto found = actorSkills.find(magicEffect->mData.mSchool); + if (found != actorSkills.end()) + s = 2.f * found->second.getBase(); if (s - x < minChance) { minChance = s - x; @@ -277,7 +300,8 @@ namespace MWMechanics } } - float calcAutoCastChance(const ESM::Spell *spell, const int *actorSkills, const int *actorAttributes, int effectiveSchool) + float calcAutoCastChance(const ESM::Spell* spell, const std::map& actorSkills, + const std::map& actorAttributes, ESM::RefId effectiveSchool) { if (spell->mData.mType != ESM::Spell::ST_Spell) return 100.f; @@ -286,12 +310,19 @@ namespace MWMechanics return 100.f; float skillTerm = 0; - if (effectiveSchool != -1) - skillTerm = 2.f * actorSkills[spellSchoolToSkill(effectiveSchool)]; + if (!effectiveSchool.empty()) + { + auto found = actorSkills.find(effectiveSchool); + if (found != actorSkills.end()) + skillTerm = 2.f * found->second.getBase(); + } else - calcWeakestSchool(spell, actorSkills, effectiveSchool, skillTerm); // Note effectiveSchool is unused after this + calcWeakestSchool( + spell, actorSkills, effectiveSchool, skillTerm); // Note effectiveSchool is unused after this - float castChance = skillTerm - spell->mData.mCost + 0.2f * actorAttributes[ESM::Attribute::Willpower] + 0.1f * actorAttributes[ESM::Attribute::Luck]; + float castChance = skillTerm - MWMechanics::calcSpellCost(*spell) + + 0.2f * actorAttributes.at(ESM::Attribute::Willpower).getBase() + + 0.1f * actorAttributes.at(ESM::Attribute::Luck).getBase(); return castChance; } } diff --git a/apps/openmw/mwmechanics/autocalcspell.hpp b/apps/openmw/mwmechanics/autocalcspell.hpp index 460713ae37f..c7c8d5f7335 100644 --- a/apps/openmw/mwmechanics/autocalcspell.hpp +++ b/apps/openmw/mwmechanics/autocalcspell.hpp @@ -1,6 +1,9 @@ #ifndef OPENMW_AUTOCALCSPELL_H #define OPENMW_AUTOCALCSPELL_H +#include "creaturestats.hpp" +#include +#include #include #include @@ -13,20 +16,25 @@ namespace ESM namespace MWMechanics { -/// Contains algorithm for calculating an NPC's spells based on stats -/// @note We might want to move this code to a component later, so the editor can use it for preview purposes + /// Contains algorithm for calculating an NPC's spells based on stats + /// @note We might want to move this code to a component later, so the editor can use it for preview purposes -std::vector autoCalcNpcSpells(const int* actorSkills, const int* actorAttributes, const ESM::Race* race); + std::vector autoCalcNpcSpells(const std::map& actorSkills, + const std::map& actorAttributes, const ESM::Race* race); -std::vector autoCalcPlayerSpells(const int* actorSkills, const int* actorAttributes, const ESM::Race* race); + std::vector autoCalcPlayerSpells(const std::map& actorSkills, + const std::map& actorAttributes, const ESM::Race* race); -// Helpers + // Helpers -bool attrSkillCheck (const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes); + bool attrSkillCheck(const ESM::Spell* spell, const std::map& actorSkills, + const std::map& actorAttributes); -void calcWeakestSchool(const ESM::Spell* spell, const int* actorSkills, int& effectiveSchool, float& skillTerm); + void calcWeakestSchool(const ESM::Spell* spell, const std::map& actorSkills, + ESM::RefId& effectiveSchool, float& skillTerm); -float calcAutoCastChance(const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes, int effectiveSchool); + float calcAutoCastChance(const ESM::Spell* spell, const std::map& actorSkills, + const std::map& actorAttributes, ESM::RefId effectiveSchool); } diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 54c951187f3..dbd00cd7ffa 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -19,548 +19,687 @@ #include "character.hpp" -#include +#include +#include +#include #include +#include #include +#include +#include -#include +#include #include #include "../mwrender/animation.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" #include "../mwworld/player.hpp" +#include "../mwworld/spellcaststate.hpp" +#include "actorutil.hpp" #include "aicombataction.hpp" +#include "creaturestats.hpp" #include "movement.hpp" #include "npcstats.hpp" -#include "creaturestats.hpp" #include "security.hpp" -#include "actorutil.hpp" #include "spellcasting.hpp" +#include "weapontype.hpp" namespace { -std::string getBestAttack (const ESM::Weapon* weapon) -{ - int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; - int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; - int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; - if (slash == chop && slash == thrust) - return "slash"; - else if (thrust >= chop && thrust >= slash) - return "thrust"; - else if (slash >= chop && slash >= thrust) - return "slash"; - else - return "chop"; -} + std::string_view getBestAttack(const ESM::Weapon* weapon) + { + int slash = weapon->mData.mSlash[0] + weapon->mData.mSlash[1]; + int chop = weapon->mData.mChop[0] + weapon->mData.mChop[1]; + int thrust = weapon->mData.mThrust[0] + weapon->mData.mThrust[1]; + if (slash == chop && slash == thrust) + return "slash"; + else if (thrust >= chop && thrust >= slash) + return "thrust"; + else if (slash >= chop && slash >= thrust) + return "slash"; + else + return "chop"; + } + + // Converts a movement Run state to its equivalent Walk state, if there is one. + MWMechanics::CharacterState runStateToWalkState(MWMechanics::CharacterState state) + { + using namespace MWMechanics; + switch (state) + { + case CharState_RunForward: + return CharState_WalkForward; + case CharState_RunBack: + return CharState_WalkBack; + case CharState_RunLeft: + return CharState_WalkLeft; + case CharState_RunRight: + return CharState_WalkRight; + case CharState_SwimRunForward: + return CharState_SwimWalkForward; + case CharState_SwimRunBack: + return CharState_SwimWalkBack; + case CharState_SwimRunLeft: + return CharState_SwimWalkLeft; + case CharState_SwimRunRight: + return CharState_SwimWalkRight; + default: + return state; + } + } -// Converts a movement Run state to its equivalent Walk state. -MWMechanics::CharacterState runStateToWalkState (MWMechanics::CharacterState state) -{ - using namespace MWMechanics; - CharacterState ret = state; - switch (state) - { - case CharState_RunForward: - ret = CharState_WalkForward; - break; - case CharState_RunBack: - ret = CharState_WalkBack; - break; - case CharState_RunLeft: - ret = CharState_WalkLeft; - break; - case CharState_RunRight: - ret = CharState_WalkRight; - break; - case CharState_SwimRunForward: - ret = CharState_SwimWalkForward; - break; - case CharState_SwimRunBack: - ret = CharState_SwimWalkBack; - break; - case CharState_SwimRunLeft: - ret = CharState_SwimWalkLeft; - break; - case CharState_SwimRunRight: - ret = CharState_SwimWalkRight; - break; - default: - break; - } - return ret; -} + // Converts a Hit state to its equivalent Death state. + MWMechanics::CharacterState hitStateToDeathState(MWMechanics::CharacterState state) + { + using namespace MWMechanics; + switch (state) + { + case CharState_SwimKnockDown: + return CharState_SwimDeathKnockDown; + case CharState_SwimKnockOut: + return CharState_SwimDeathKnockOut; + case CharState_KnockDown: + return CharState_DeathKnockDown; + case CharState_KnockOut: + return CharState_DeathKnockOut; + default: + return CharState_None; + } + } + + // Converts a movement state to its equivalent base animation group as long as it is a movement state. + std::string_view movementStateToAnimGroup(MWMechanics::CharacterState state) + { + using namespace MWMechanics; + switch (state) + { + case CharState_WalkForward: + return "walkforward"; + case CharState_WalkBack: + return "walkback"; + case CharState_WalkLeft: + return "walkleft"; + case CharState_WalkRight: + return "walkright"; + + case CharState_SwimWalkForward: + return "swimwalkforward"; + case CharState_SwimWalkBack: + return "swimwalkback"; + case CharState_SwimWalkLeft: + return "swimwalkleft"; + case CharState_SwimWalkRight: + return "swimwalkright"; + + case CharState_RunForward: + return "runforward"; + case CharState_RunBack: + return "runback"; + case CharState_RunLeft: + return "runleft"; + case CharState_RunRight: + return "runright"; + + case CharState_SwimRunForward: + return "swimrunforward"; + case CharState_SwimRunBack: + return "swimrunback"; + case CharState_SwimRunLeft: + return "swimrunleft"; + case CharState_SwimRunRight: + return "swimrunright"; + + case CharState_SneakForward: + return "sneakforward"; + case CharState_SneakBack: + return "sneakback"; + case CharState_SneakLeft: + return "sneakleft"; + case CharState_SneakRight: + return "sneakright"; + + case CharState_TurnLeft: + return "turnleft"; + case CharState_TurnRight: + return "turnright"; + case CharState_SwimTurnLeft: + return "swimturnleft"; + case CharState_SwimTurnRight: + return "swimturnright"; + default: + return {}; + } + } + + // Converts a death state to its equivalent animation group as long as it is a death state. + std::string_view deathStateToAnimGroup(MWMechanics::CharacterState state) + { + using namespace MWMechanics; + switch (state) + { + case CharState_SwimDeath: + return "swimdeath"; + case CharState_SwimDeathKnockDown: + return "swimdeathknockdown"; + case CharState_SwimDeathKnockOut: + return "swimdeathknockout"; + case CharState_DeathKnockDown: + return "deathknockdown"; + case CharState_DeathKnockOut: + return "deathknockout"; + case CharState_Death1: + return "death1"; + case CharState_Death2: + return "death2"; + case CharState_Death3: + return "death3"; + case CharState_Death4: + return "death4"; + case CharState_Death5: + return "death5"; + default: + return {}; + } + } -float getFallDamage(const MWWorld::Ptr& ptr, float fallHeight) -{ - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &store = world->getStore().get(); + // Converts a hit state to its equivalent animation group as long as it is a hit state. + std::string hitStateToAnimGroup(MWMechanics::CharacterState state) + { + using namespace MWMechanics; + switch (state) + { + case CharState_SwimHit: + return "swimhit"; + case CharState_SwimKnockDown: + return "swimknockdown"; + case CharState_SwimKnockOut: + return "swimknockout"; + + case CharState_Hit: + return "hit"; + case CharState_KnockDown: + return "knockdown"; + case CharState_KnockOut: + return "knockout"; + + case CharState_Block: + return "shield"; + + default: + return {}; + } + } + + // Converts an idle state to its equivalent animation group. + std::string idleStateToAnimGroup(MWMechanics::CharacterState state) + { + using namespace MWMechanics; + switch (state) + { + case CharState_IdleSwim: + return "idleswim"; + case CharState_IdleSneak: + return "idlesneak"; + case CharState_Idle: + case CharState_SpecialIdle: + return "idle"; + default: + return {}; + } + } - const float fallDistanceMin = store.find("fFallDamageDistanceMin")->mValue.getFloat(); + MWRender::Animation::AnimPriority getIdlePriority(MWMechanics::CharacterState state) + { + using namespace MWMechanics; + MWRender::Animation::AnimPriority priority(Priority_Default); + switch (state) + { + case CharState_IdleSwim: + return Priority_SwimIdle; + case CharState_IdleSneak: + priority[MWRender::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; + [[fallthrough]]; + default: + return priority; + } + } - if (fallHeight >= fallDistanceMin) + float getFallDamage(const MWWorld::Ptr& ptr, float fallHeight) { - const float acrobaticsSkill = static_cast(ptr.getClass().getSkill(ptr, ESM::Skill::Acrobatics)); - const float jumpSpellBonus = ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Jump).getMagnitude(); - const float fallAcroBase = store.find("fFallAcroBase")->mValue.getFloat(); - const float fallAcroMult = store.find("fFallAcroMult")->mValue.getFloat(); - const float fallDistanceBase = store.find("fFallDistanceBase")->mValue.getFloat(); - const float fallDistanceMult = store.find("fFallDistanceMult")->mValue.getFloat(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Store& store = world->getStore().get(); + + const float fallDistanceMin = store.find("fFallDamageDistanceMin")->mValue.getFloat(); + + if (fallHeight >= fallDistanceMin) + { + const float acrobaticsSkill = static_cast(ptr.getClass().getSkill(ptr, ESM::Skill::Acrobatics)); + const float jumpSpellBonus = ptr.getClass() + .getCreatureStats(ptr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Jump) + .getMagnitude(); + const float fallAcroBase = store.find("fFallAcroBase")->mValue.getFloat(); + const float fallAcroMult = store.find("fFallAcroMult")->mValue.getFloat(); + const float fallDistanceBase = store.find("fFallDistanceBase")->mValue.getFloat(); + const float fallDistanceMult = store.find("fFallDistanceMult")->mValue.getFloat(); - float x = fallHeight - fallDistanceMin; - x -= (1.5f * acrobaticsSkill) + jumpSpellBonus; - x = std::max(0.0f, x); + float x = fallHeight - fallDistanceMin; + x -= (1.5f * acrobaticsSkill) + jumpSpellBonus; + x = std::max(0.0f, x); - float a = fallAcroBase + fallAcroMult * (100 - acrobaticsSkill); - x = fallDistanceBase + fallDistanceMult * x; - x *= a; + float a = fallAcroBase + fallAcroMult * (100 - acrobaticsSkill); + x = fallDistanceBase + fallDistanceMult * x; + x *= a; - return x; + return x; + } + return 0.f; + } + + bool isRealWeapon(int weaponType) + { + return weaponType != ESM::Weapon::HandToHand && weaponType != ESM::Weapon::Spell + && weaponType != ESM::Weapon::None; } - return 0.f; -} } namespace MWMechanics { -struct StateInfo { - CharacterState state; - const char groupname[32]; -}; - -static const StateInfo sMovementList[] = { - { CharState_WalkForward, "walkforward" }, - { CharState_WalkBack, "walkback" }, - { CharState_WalkLeft, "walkleft" }, - { CharState_WalkRight, "walkright" }, + std::string CharacterController::chooseRandomGroup(const std::string& prefix, int* num) const + { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - { CharState_SwimWalkForward, "swimwalkforward" }, - { CharState_SwimWalkBack, "swimwalkback" }, - { CharState_SwimWalkLeft, "swimwalkleft" }, - { CharState_SwimWalkRight, "swimwalkright" }, + int numAnims = 0; + while (mAnimation->hasAnimation(prefix + std::to_string(numAnims + 1))) + ++numAnims; - { CharState_RunForward, "runforward" }, - { CharState_RunBack, "runback" }, - { CharState_RunLeft, "runleft" }, - { CharState_RunRight, "runright" }, + int roll = Misc::Rng::rollDice(numAnims, prng) + 1; // [1, numAnims] + if (num) + *num = roll; + return prefix + std::to_string(roll); + } - { CharState_SwimRunForward, "swimrunforward" }, - { CharState_SwimRunBack, "swimrunback" }, - { CharState_SwimRunLeft, "swimrunleft" }, - { CharState_SwimRunRight, "swimrunright" }, + void CharacterController::clearStateAnimation(std::string& anim) const + { + if (anim.empty()) + return; + if (mAnimation) + mAnimation->disable(anim); + anim.clear(); + } - { CharState_SneakForward, "sneakforward" }, - { CharState_SneakBack, "sneakback" }, - { CharState_SneakLeft, "sneakleft" }, - { CharState_SneakRight, "sneakright" }, + void CharacterController::resetCurrentJumpState() + { + clearStateAnimation(mCurrentJump); + mJumpState = JumpState_None; + } - { CharState_Jump, "jump" }, + void CharacterController::resetCurrentMovementState() + { + clearStateAnimation(mCurrentMovement); + mMovementState = CharState_None; + mMovementAnimationHasMovement = false; + } - { CharState_TurnLeft, "turnleft" }, - { CharState_TurnRight, "turnright" }, - { CharState_SwimTurnLeft, "swimturnleft" }, - { CharState_SwimTurnRight, "swimturnright" }, -}; -static const StateInfo *sMovementListEnd = &sMovementList[sizeof(sMovementList)/sizeof(sMovementList[0])]; + void CharacterController::resetCurrentIdleState() + { + clearStateAnimation(mCurrentIdle); + mIdleState = CharState_None; + } + void CharacterController::resetCurrentHitState() + { + clearStateAnimation(mCurrentHit); + mHitState = CharState_None; + } -class FindCharState { - CharacterState state; + void CharacterController::resetCurrentWeaponState() + { + clearStateAnimation(mCurrentWeapon); + mUpperBodyState = UpperBodyState::None; + } -public: - FindCharState(CharacterState _state) : state(_state) { } + void CharacterController::resetCurrentDeathState() + { + clearStateAnimation(mCurrentDeath); + mDeathState = CharState_None; + } - bool operator()(const StateInfo &info) const - { return info.state == state; } -}; + void CharacterController::refreshHitRecoilAnims() + { + auto& charClass = mPtr.getClass(); + if (!charClass.isActor()) + return; + const auto world = MWBase::Environment::get().getWorld(); + auto& stats = charClass.getCreatureStats(mPtr); + bool knockout = stats.getFatigue().getCurrent() < 0 || stats.getFatigue().getBase() == 0; + bool recovery = stats.getHitRecovery(); + bool knockdown = stats.getKnockedDown(); + bool block = stats.getBlock() && !knockout && !recovery && !knockdown; + bool isSwimming = world->isSwimming(mPtr); + stats.setBlock(false); -std::string CharacterController::chooseRandomGroup (const std::string& prefix, int* num) const -{ - int numAnims=0; - while (mAnimation->hasAnimation(prefix + std::to_string(numAnims+1))) - ++numAnims; - - int roll = Misc::Rng::rollDice(numAnims) + 1; // [1, numAnims] - if (num) - *num = roll; - return prefix + std::to_string(roll); -} + if (mPtr == getPlayer() && mHitState == CharState_Block && block) + { + mHitState = CharState_None; + resetCurrentIdleState(); + } -void CharacterController::refreshHitRecoilAnims(CharacterState& idle) -{ - bool recovery = mPtr.getClass().getCreatureStats(mPtr).getHitRecovery(); - bool knockdown = mPtr.getClass().getCreatureStats(mPtr).getKnockedDown(); - bool block = mPtr.getClass().getCreatureStats(mPtr).getBlock(); - bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); - if(mHitState == CharState_None) - { - if ((mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() < 0 - || mPtr.getClass().getCreatureStats(mPtr).getFatigue().getBase() == 0)) + if (mHitState != CharState_None) { - mTimeUntilWake = Misc::Rng::rollClosedProbability() * 2 + 1; // Wake up after 1 to 3 seconds - if (isSwimming && mAnimation->hasAnimation("swimknockout")) - { - mHitState = CharState_SwimKnockOut; - mCurrentHit = "swimknockout"; - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, false, 1, "start", "stop", 0.0f, ~0ul); - } - else if (!isSwimming && mAnimation->hasAnimation("knockout")) + if (!mAnimation->isPlaying(mCurrentHit)) { - mHitState = CharState_KnockOut; - mCurrentHit = "knockout"; - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, false, 1, "start", "stop", 0.0f, ~0ul); - } - else - { - // Knockout animations are missing. Fall back to idle animation, so target actor still can be killed via HtH. - mCurrentHit.erase(); + if (isKnockedOut() && mCurrentHit.empty() && knockout) + return; + + mHitState = CharState_None; + mCurrentHit.clear(); + stats.setKnockedDown(false); + stats.setHitRecovery(false); + resetCurrentIdleState(); } + else if (isKnockedOut()) + mAnimation->setLoopingEnabled(mCurrentHit, knockout); + return; + } + + if (!knockout && !knockdown && !recovery && !block) + return; - mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(true); + MWRender::Animation::AnimPriority priority(Priority_Knockdown); + std::string_view startKey = "start"; + std::string_view stopKey = "stop"; + if (knockout) + { + mHitState = isSwimming ? CharState_SwimKnockOut : CharState_KnockOut; + stats.setKnockedDown(true); } else if (knockdown) { - if (isSwimming && mAnimation->hasAnimation("swimknockdown")) - { - mHitState = CharState_SwimKnockDown; - mCurrentHit = "swimknockdown"; - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); - } - else if (!isSwimming && mAnimation->hasAnimation("knockdown")) - { - mHitState = CharState_KnockDown; - mCurrentHit = "knockdown"; - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); - } - else - { - // Knockdown animation is missing. Cancel knockdown state. - mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(false); - } + mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown; } else if (recovery) { - std::string anim = chooseRandomGroup("swimhit"); - if (isSwimming && mAnimation->hasAnimation(anim)) - { - mHitState = CharState_SwimHit; - mCurrentHit = anim; - mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); - } - else - { - anim = chooseRandomGroup("hit"); - if (mAnimation->hasAnimation(anim)) - { - mHitState = CharState_Hit; - mCurrentHit = anim; - mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); - } - } + mHitState = isSwimming ? CharState_SwimHit : CharState_Hit; + priority = Priority_Hit; } - else if (block && mAnimation->hasAnimation("shield")) + else if (block) { mHitState = CharState_Block; - mCurrentHit = "shield"; - MWRender::Animation::AnimPriority priorityBlock (Priority_Hit); - priorityBlock[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block; - priorityBlock[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; - mAnimation->play(mCurrentHit, priorityBlock, MWRender::Animation::BlendMask_All, true, 1, "block start", "block stop", 0.0f, 0); + priority = Priority_Hit; + priority[MWRender::BoneGroup_LeftArm] = Priority_Block; + priority[MWRender::BoneGroup_LowerBody] = Priority_WeaponLowerBody; + startKey = "block start"; + stopKey = "block stop"; + } + + mCurrentHit = hitStateToAnimGroup(mHitState); + + if (isRecovery()) + { + mCurrentHit = chooseRandomGroup(mCurrentHit); + if (mHitState == CharState_SwimHit && !mAnimation->hasAnimation(mCurrentHit)) + mCurrentHit = chooseRandomGroup(hitStateToAnimGroup(CharState_Hit)); } // Cancel upper body animations if (isKnockedOut() || isKnockedDown()) { - if (mUpperBodyState > UpperCharState_WeapEquiped) - { + if (!mCurrentWeapon.empty()) mAnimation->disable(mCurrentWeapon); - mUpperBodyState = UpperCharState_WeapEquiped; + if (mUpperBodyState > UpperBodyState::WeaponEquipped) + { + mUpperBodyState = UpperBodyState::WeaponEquipped; if (mWeaponType > ESM::Weapon::None) mAnimation->showWeapons(true); } - else if (mUpperBodyState > UpperCharState_Nothing && mUpperBodyState < UpperCharState_WeapEquiped) + else if (mUpperBodyState < UpperBodyState::WeaponEquipped) { - mAnimation->disable(mCurrentWeapon); - mUpperBodyState = UpperCharState_Nothing; + mUpperBodyState = UpperBodyState::None; } } - if (mHitState != CharState_None) - idle = CharState_None; - } - else if(!mAnimation->isPlaying(mCurrentHit)) - { - mCurrentHit.erase(); - if (knockdown) - mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(false); - if (recovery) - mPtr.getClass().getCreatureStats(mPtr).setHitRecovery(false); - if (block) - mPtr.getClass().getCreatureStats(mPtr).setBlock(false); - mHitState = CharState_None; - } - else if (isKnockedOut() && mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() > 0 - && mTimeUntilWake <= 0) - { - mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown; - mAnimation->disable(mCurrentHit); - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "loop stop", "stop", 0.0f, 0); - } -} -void CharacterController::refreshJumpAnims(const std::string& weapShortGroup, JumpingState jump, CharacterState& idle, bool force) -{ - if (!force && jump == mJumpState && idle == CharState_None) - return; + if (!mAnimation->hasAnimation(mCurrentHit)) + { + mCurrentHit.clear(); + return; + } + + playBlendedAnimation(mCurrentHit, priority, MWRender::BlendMask_All, true, 1, startKey, stopKey, 0.0f, + std::numeric_limits::max()); + } - std::string jumpAnimName; - MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All; - if (jump != JumpState_None) + void CharacterController::refreshJumpAnims(JumpingState jump, bool force) { - jumpAnimName = "jump"; - if(!weapShortGroup.empty()) - { - jumpAnimName += weapShortGroup; - if(!mAnimation->hasAnimation(jumpAnimName)) - { - jumpAnimName = fallbackShortWeaponGroup("jump", &jumpmask); + if (!force && jump == mJumpState) + return; - // If we apply jump only for lower body, do not reset idle animations. - // For upper body there will be idle animation. - if (jumpmask == MWRender::Animation::BlendMask_LowerBody && idle == CharState_None) - idle = CharState_Idle; - } + if (jump == JumpState_None) + { + if (!mCurrentJump.empty()) + resetCurrentIdleState(); + resetCurrentJumpState(); + return; } - } - if (!force && jump == mJumpState) - return; + std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType); + std::string jumpAnimName = "jump"; + jumpAnimName += weapShortGroup; + MWRender::Animation::BlendMask jumpmask = MWRender::BlendMask_All; + if (!weapShortGroup.empty() && !mAnimation->hasAnimation(jumpAnimName)) + jumpAnimName = fallbackShortWeaponGroup("jump", &jumpmask); + + if (!mAnimation->hasAnimation(jumpAnimName)) + { + if (!mCurrentJump.empty()) + resetCurrentIdleState(); + resetCurrentJumpState(); + return; + } - bool startAtLoop = (jump == mJumpState); - mJumpState = jump; + bool startAtLoop = (jump == mJumpState); + mJumpState = jump; + clearStateAnimation(mCurrentJump); - if (!mCurrentJump.empty()) - { - mAnimation->disable(mCurrentJump); - mCurrentJump.clear(); + mCurrentJump = jumpAnimName; + if (mJumpState == JumpState_InAir) + playBlendedAnimation(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f, + startAtLoop ? "loop start" : "start", "stop", 0.f, std::numeric_limits::max()); + else if (mJumpState == JumpState_Landing) + playBlendedAnimation(jumpAnimName, Priority_Jump, jumpmask, true, 1.0f, "loop stop", "stop", 0.0f, 0); } - if(mJumpState == JumpState_InAir) + bool CharacterController::onOpen() const { - if (mAnimation->hasAnimation(jumpAnimName)) + if (mPtr.getType() == ESM::Container::sRecordId) { - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false, - 1.0f, startAtLoop ? "loop start" : "start", "stop", 0.f, ~0ul); - mCurrentJump = jumpAnimName; + if (!mAnimation->hasAnimation("containeropen")) + return true; + + if (mAnimation->isPlaying("containeropen")) + return false; + + if (mAnimation->isPlaying("containerclose")) + return false; + + mAnimation->play( + "containeropen", Priority_Scripted, MWRender::BlendMask_All, false, 1.0f, "start", "stop", 0.f, 0); + if (mAnimation->isPlaying("containeropen")) + return false; } + + return true; } - else if (mJumpState == JumpState_Landing) + + void CharacterController::onClose() const { - if (mAnimation->hasAnimation(jumpAnimName)) + if (mPtr.getType() == ESM::Container::sRecordId) { - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, - 1.0f, "loop stop", "stop", 0.0f, 0); - mCurrentJump = jumpAnimName; + if (!mAnimation->hasAnimation("containerclose")) + return; + + float complete, startPoint = 0.f; + bool animPlaying = mAnimation->getInfo("containeropen", &complete); + if (animPlaying) + startPoint = 1.f - complete; + + mAnimation->play("containerclose", Priority_Scripted, MWRender::BlendMask_All, false, 1.0f, "start", "stop", + startPoint, 0); } } -} -bool CharacterController::onOpen() -{ - if (mPtr.getTypeName() == typeid(ESM::Container).name()) + std::string_view CharacterController::getWeaponAnimation(int weaponType) const { - if (!mAnimation->hasAnimation("containeropen")) - return true; + std::string_view weaponGroup = getWeaponType(weaponType)->mLongGroup; + if (isRealWeapon(weaponType) && !mAnimation->hasAnimation(weaponGroup)) + { + static const std::string_view oneHandFallback = getWeaponType(ESM::Weapon::LongBladeOneHand)->mLongGroup; + static const std::string_view twoHandFallback = getWeaponType(ESM::Weapon::LongBladeTwoHand)->mLongGroup; - if (mAnimation->isPlaying("containeropen")) - return false; + const ESM::WeaponType* weapInfo = getWeaponType(weaponType); - if (mAnimation->isPlaying("containerclose")) - return false; + // For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones + if (weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee) + weaponGroup = twoHandFallback; + else + weaponGroup = oneHandFallback; + } + else if (weaponType == ESM::Weapon::HandToHand && !mPtr.getClass().isBipedal(mPtr)) + return "attack1"; - mAnimation->play("containeropen", Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.f, 0); - if (mAnimation->isPlaying("containeropen")) - return false; + return weaponGroup; } - return true; -} - -void CharacterController::onClose() -{ - if (mPtr.getTypeName() == typeid(ESM::Container).name()) + std::string_view CharacterController::getWeaponShortGroup(int weaponType) const { - if (!mAnimation->hasAnimation("containerclose")) - return; - - float complete, startPoint = 0.f; - bool animPlaying = mAnimation->getInfo("containeropen", &complete); - if (animPlaying) - startPoint = 1.f - complete; - - mAnimation->play("containerclose", Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", startPoint, 0); + if (weaponType == ESM::Weapon::HandToHand && !mPtr.getClass().isBipedal(mPtr)) + return {}; + return getWeaponType(weaponType)->mShortGroup; } -} -std::string CharacterController::getWeaponAnimation(int weaponType) const -{ - std::string weaponGroup = getWeaponType(weaponType)->mLongGroup; - bool isRealWeapon = weaponType != ESM::Weapon::HandToHand && weaponType != ESM::Weapon::Spell && weaponType != ESM::Weapon::None; - if (isRealWeapon && !mAnimation->hasAnimation(weaponGroup)) + std::string CharacterController::fallbackShortWeaponGroup( + const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask) const { - static const std::string oneHandFallback = getWeaponType(ESM::Weapon::LongBladeOneHand)->mLongGroup; - static const std::string twoHandFallback = getWeaponType(ESM::Weapon::LongBladeTwoHand)->mLongGroup; + if (!isRealWeapon(mWeaponType)) + { + if (blendMask != nullptr) + *blendMask = MWRender::BlendMask_LowerBody; + + return baseGroupName; + } + + static const std::string_view oneHandFallback = getWeaponShortGroup(ESM::Weapon::LongBladeOneHand); + static const std::string_view twoHandFallback = getWeaponShortGroup(ESM::Weapon::LongBladeTwoHand); - const ESM::WeaponType* weapInfo = getWeaponType(weaponType); + std::string groupName = baseGroupName; + const ESM::WeaponType* weapInfo = getWeaponType(mWeaponType); // For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones if (weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee) - weaponGroup = twoHandFallback; - else if (isRealWeapon) - weaponGroup = oneHandFallback; - } + groupName += twoHandFallback; + else + groupName += oneHandFallback; - return weaponGroup; -} + // Special case for crossbows - we should apply 1h animations a fallback only for lower body + if (mWeaponType == ESM::Weapon::MarksmanCrossbow && blendMask != nullptr) + *blendMask = MWRender::BlendMask_LowerBody; -std::string CharacterController::fallbackShortWeaponGroup(const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask) -{ - bool isRealWeapon = mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None; - if (!isRealWeapon) - { - if (blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; + if (!mAnimation->hasAnimation(groupName)) + { + groupName = baseGroupName; + if (blendMask != nullptr) + *blendMask = MWRender::BlendMask_LowerBody; + } - return baseGroupName; + return groupName; } - static const std::string oneHandFallback = getWeaponType(ESM::Weapon::LongBladeOneHand)->mShortGroup; - static const std::string twoHandFallback = getWeaponType(ESM::Weapon::LongBladeTwoHand)->mShortGroup; - - std::string groupName = baseGroupName; - const ESM::WeaponType* weapInfo = getWeaponType(mWeaponType); - - // For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones - if (weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee) - groupName += twoHandFallback; - else - groupName += oneHandFallback; + void CharacterController::refreshMovementAnims(CharacterState movement, bool force) + { + if (movement == mMovementState && !force) + return; - // Special case for crossbows - we shouls apply 1h animations a fallback only for lower body - if (mWeaponType == ESM::Weapon::MarksmanCrossbow && blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; + std::string_view movementAnimGroup = movementStateToAnimGroup(movement); - if (!mAnimation->hasAnimation(groupName)) - { - groupName = baseGroupName; - if (blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; - } + if (movementAnimGroup.empty()) + { + if (!mCurrentMovement.empty()) + resetCurrentIdleState(); + resetCurrentMovementState(); + return; + } + std::string movementAnimName{ movementAnimGroup }; - return groupName; -} + mMovementState = movement; + std::string::size_type swimpos = movementAnimName.find("swim"); + if (!mAnimation->hasAnimation(movementAnimName)) + { + if (swimpos != std::string::npos) + { + movementAnimName.erase(swimpos, 4); + swimpos = std::string::npos; + } + } -void CharacterController::refreshMovementAnims(const std::string& weapShortGroup, CharacterState movement, CharacterState& idle, bool force) -{ - if (movement == mMovementState && idle == mIdleState && !force) - return; + MWRender::Animation::BlendMask movemask = MWRender::BlendMask_All; - // Reset idle if we actually play movement animations excepts of these cases: - // 1. When we play turning animations - // 2. When we use a fallback animation for lower body since movement animation for given weapon is missing (e.g. for crossbows and spellcasting) - bool resetIdle = (movement != CharState_None && !isTurning()); + std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType); - std::string movementAnimName; - MWRender::Animation::BlendMask movemask; - const StateInfo *movestate; + // Non-biped creatures don't use spellcasting-specific movement animations. + if (!isRealWeapon(mWeaponType) && !mPtr.getClass().isBipedal(mPtr)) + weapShortGroup = {}; - movemask = MWRender::Animation::BlendMask_All; - movestate = std::find_if(sMovementList, sMovementListEnd, FindCharState(movement)); - if(movestate != sMovementListEnd) - { - movementAnimName = movestate->groupname; - if(!weapShortGroup.empty()) + if (swimpos == std::string::npos && !weapShortGroup.empty()) { - std::string::size_type swimpos = movementAnimName.find("swim"); - if (swimpos == std::string::npos) + std::string weapMovementAnimName; + // Spellcasting stance turning is a special case + if (mWeaponType == ESM::Weapon::Spell && isTurning()) { - if (mWeaponType == ESM::Weapon::Spell && (movement == CharState_TurnLeft || movement == CharState_TurnRight)) // Spellcasting stance turning is a special case - movementAnimName = weapShortGroup + movementAnimName; - else - movementAnimName += weapShortGroup; + weapMovementAnimName = weapShortGroup; + weapMovementAnimName += movementAnimName; } - - if(!mAnimation->hasAnimation(movementAnimName)) + else { - movementAnimName = movestate->groupname; - if (swimpos == std::string::npos) - { - movementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); + weapMovementAnimName = movementAnimName; + weapMovementAnimName += weapShortGroup; + } - // If we apply movement only for lower body, do not reset idle animations. - // For upper body there will be idle animation. - if (movemask == MWRender::Animation::BlendMask_LowerBody && idle == CharState_None) - idle = CharState_Idle; + if (!mAnimation->hasAnimation(weapMovementAnimName)) + weapMovementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); - if (movemask == MWRender::Animation::BlendMask_LowerBody) - resetIdle = false; - } - } + movementAnimName = std::move(weapMovementAnimName); } - } - if(force || movement != mMovementState) - { - mMovementState = movement; - if(movestate != sMovementListEnd) + if (!mAnimation->hasAnimation(movementAnimName)) { - if(!mAnimation->hasAnimation(movementAnimName)) - { - std::string::size_type swimpos = movementAnimName.find("swim"); - if (swimpos != std::string::npos) - { - movementAnimName.erase(swimpos, 4); - if (!weapShortGroup.empty()) - { - std::string weapMovementAnimName = movementAnimName + weapShortGroup; - if(mAnimation->hasAnimation(weapMovementAnimName)) - movementAnimName = weapMovementAnimName; - else - { - movementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); - if (movemask == MWRender::Animation::BlendMask_LowerBody) - resetIdle = false; - } - } - } + std::string::size_type runpos = movementAnimName.find("run"); + if (runpos != std::string::npos) + movementAnimName.replace(runpos, 3, "walk"); - if (swimpos == std::string::npos || !mAnimation->hasAnimation(movementAnimName)) - { - std::string::size_type runpos = movementAnimName.find("run"); - if (runpos != std::string::npos) - { - movementAnimName.replace(runpos, runpos+3, "walk"); - if (!mAnimation->hasAnimation(movementAnimName)) - movementAnimName.clear(); - } - else - movementAnimName.clear(); - } + if (!mAnimation->hasAnimation(movementAnimName)) + { + if (!mCurrentMovement.empty()) + resetCurrentIdleState(); + resetCurrentMovementState(); + return; } } @@ -569,943 +708,839 @@ void CharacterController::refreshMovementAnims(const std::string& weapShortGroup if (!mCurrentMovement.empty() && movementAnimName == mCurrentMovement) mAnimation->getInfo(mCurrentMovement, &startpoint); - mMovementAnimationControlled = true; - - mAnimation->disable(mCurrentMovement); + mMovementAnimationHasMovement = true; - if (!mAnimation->hasAnimation(movementAnimName)) - movementAnimName.clear(); + clearStateAnimation(mCurrentMovement); + mCurrentMovement = std::move(movementAnimName); - mCurrentMovement = movementAnimName; - if(!mCurrentMovement.empty()) + // For non-flying creatures, MW uses the Walk animation to calculate the animation velocity + // even if we are running. This must be replicated, otherwise the observed speed would differ drastically. + mAdjustMovementAnimSpeed = true; + if (mPtr.getClass().getType() == ESM::Creature::sRecordId + && !(mPtr.get()->mBase->mFlags & ESM::Creature::Flies)) { - if (resetIdle) - { - mAnimation->disable(mCurrentIdle); - mIdleState = CharState_None; - idle = CharState_None; - } + CharacterState walkState = runStateToWalkState(mMovementState); + std::string_view anim = movementStateToAnimGroup(walkState); - // For non-flying creatures, MW uses the Walk animation to calculate the animation velocity - // even if we are running. This must be replicated, otherwise the observed speed would differ drastically. - std::string anim = mCurrentMovement; - mAdjustMovementAnimSpeed = true; - if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() - && !(mPtr.get()->mBase->mFlags & ESM::Creature::Flies)) + mMovementAnimSpeed = mAnimation->getVelocity(anim); + if (mMovementAnimSpeed <= 1.0f) { - CharacterState walkState = runStateToWalkState(mMovementState); - const StateInfo *stateinfo = std::find_if(sMovementList, sMovementListEnd, FindCharState(walkState)); - anim = stateinfo->groupname; - - mMovementAnimSpeed = mAnimation->getVelocity(anim); - if (mMovementAnimSpeed <= 1.0f) - { - // Another bug: when using a fallback animation (e.g. RunForward as fallback to SwimRunForward), - // then the equivalent Walk animation will not use a fallback, and if that animation doesn't exist - // we will play without any scaling. - // Makes the speed attribute of most water creatures totally useless. - // And again, this can not be fixed without patching game data. - mAdjustMovementAnimSpeed = false; - mMovementAnimSpeed = 1.f; - } + // Another bug: when using a fallback animation (e.g. RunForward as fallback to SwimRunForward), + // then the equivalent Walk animation will not use a fallback, and if that animation doesn't exist + // we will play without any scaling. + // Makes the speed attribute of most water creatures totally useless. + // And again, this can not be fixed without patching game data. + mAdjustMovementAnimSpeed = false; + mMovementAnimSpeed = 1.f; } - else - { - mMovementAnimSpeed = mAnimation->getVelocity(anim); + } + else + { + mMovementAnimSpeed = mAnimation->getVelocity(mCurrentMovement); - if (mMovementAnimSpeed <= 1.0f) - { - // The first person anims don't have any velocity to calculate a speed multiplier from. - // We use the third person velocities instead. - // FIXME: should be pulled from the actual animation, but it is not presently loaded. - bool sneaking = mMovementState == CharState_SneakForward || mMovementState == CharState_SneakBack - || mMovementState == CharState_SneakLeft || mMovementState == CharState_SneakRight; - mMovementAnimSpeed = (sneaking ? 33.5452f : (isRunning() ? 222.857f : 154.064f)); - mMovementAnimationControlled = false; - } + if (mMovementAnimSpeed <= 1.0f) + { + // The first person anims don't have any velocity to calculate a speed multiplier from. + // We use the third person velocities instead. + // FIXME: should be pulled from the actual animation, but it is not presently loaded. + bool sneaking = mMovementState == CharState_SneakForward || mMovementState == CharState_SneakBack + || mMovementState == CharState_SneakLeft || mMovementState == CharState_SneakRight; + mMovementAnimSpeed = (sneaking ? 33.5452f : (isRunning() ? 222.857f : 154.064f)); + mMovementAnimationHasMovement = false; } - - mAnimation->play(mCurrentMovement, Priority_Movement, movemask, false, - 1.f, "start", "stop", startpoint, ~0ul, true); } - else - mMovementState = CharState_None; + + playBlendedAnimation(mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, + std::numeric_limits::max(), true); } -} -void CharacterController::refreshIdleAnims(const std::string& weapShortGroup, CharacterState idle, bool force) -{ - // FIXME: if one of the below states is close to their last animation frame (i.e. will be disabled in the coming update), - // the idle animation should be displayed - if (((mUpperBodyState != UpperCharState_Nothing && mUpperBodyState != UpperCharState_WeapEquiped) - || (mMovementState != CharState_None && !isTurning()) - || mHitState != CharState_None) + void CharacterController::refreshIdleAnims(CharacterState idle, bool force) + { + // FIXME: if one of the below states is close to their last animation frame (i.e. will be disabled in the coming + // update), the idle animation should be displayed + if (((mUpperBodyState != UpperBodyState::None && mUpperBodyState != UpperBodyState::WeaponEquipped) + || mMovementState != CharState_None || !mCurrentHit.empty()) && !mPtr.getClass().isBipedal(mPtr)) - idle = CharState_None; + { + resetCurrentIdleState(); + return; + } + + if (!force && idle == mIdleState && (mAnimation->isPlaying(mCurrentIdle) || !mAnimQueue.empty())) + return; - if(force || idle != mIdleState || (!mAnimation->isPlaying(mCurrentIdle) && mAnimQueue.empty())) - { mIdleState = idle; - size_t numLoops = ~0ul; - std::string idleGroup; - MWRender::Animation::AnimPriority idlePriority (Priority_Default); - // Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to - // "idle"+weapon or "idle". - if(mIdleState == CharState_IdleSwim && mAnimation->hasAnimation("idleswim")) + std::string idleGroup = idleStateToAnimGroup(mIdleState); + if (idleGroup.empty()) { - idleGroup = "idleswim"; - idlePriority = Priority_SwimIdle; + resetCurrentIdleState(); + return; } - else if(mIdleState == CharState_IdleSneak && mAnimation->hasAnimation("idlesneak")) + + MWRender::Animation::AnimPriority priority = getIdlePriority(mIdleState); + size_t numLoops = std::numeric_limits::max(); + + // Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to + // "idle"+weapon or "idle". + bool fallback = mIdleState != CharState_Idle && !mAnimation->hasAnimation(idleGroup); + if (fallback) { - idleGroup = "idlesneak"; - idlePriority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; + priority = getIdlePriority(CharState_Idle); + idleGroup = idleStateToAnimGroup(CharState_Idle); } - else if(mIdleState != CharState_None) + + if (fallback || mIdleState == CharState_Idle || mIdleState == CharState_SpecialIdle) { - idleGroup = "idle"; - if(!weapShortGroup.empty()) + std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType); + if (!weapShortGroup.empty()) { - idleGroup += weapShortGroup; - if(!mAnimation->hasAnimation(idleGroup)) - { - idleGroup = fallbackShortWeaponGroup("idle"); - } + std::string weapIdleGroup = idleGroup; + weapIdleGroup += weapShortGroup; + if (!mAnimation->hasAnimation(weapIdleGroup)) + weapIdleGroup = fallbackShortWeaponGroup(idleGroup); + idleGroup = std::move(weapIdleGroup); // play until the Loop Stop key 2 to 5 times, then play until the Stop key // this replicates original engine behavior for the "Idle1h" 1st-person animation - numLoops = 1 + Misc::Rng::rollDice(4); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + numLoops = 1 + Misc::Rng::rollDice(4, prng); } } - // There is no need to restart anim if the new and old anims are the same. - // Just update a number of loops. - float startPoint = 0; - if (!mCurrentIdle.empty() && mCurrentIdle == idleGroup) + if (!mAnimation->hasAnimation(idleGroup)) { - mAnimation->getInfo(mCurrentIdle, &startPoint); + resetCurrentIdleState(); + return; } - if(!mCurrentIdle.empty()) - mAnimation->disable(mCurrentIdle); + float startPoint = 0.f; + // There is no need to restart anim if the new and old anims are the same. + // Just update the number of loops. + if (mCurrentIdle == idleGroup) + mAnimation->getInfo(mCurrentIdle, &startPoint); - mCurrentIdle = idleGroup; - if(!mCurrentIdle.empty()) - mAnimation->play(mCurrentIdle, idlePriority, MWRender::Animation::BlendMask_All, false, - 1.0f, "start", "stop", startPoint, numLoops, true); + clearStateAnimation(mCurrentIdle); + mCurrentIdle = std::move(idleGroup); + playBlendedAnimation( + mCurrentIdle, priority, MWRender::BlendMask_All, false, 1.0f, "start", "stop", startPoint, numLoops, true); } -} -void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force) -{ - // If the current animation is persistent, do not touch it - if (isPersistentAnimPlaying()) - return; - - if (mPtr.getClass().isActor()) - refreshHitRecoilAnims(idle); - - std::string weap; - if (mPtr.getClass().hasInventoryStore(mPtr)) - weap = getWeaponType(mWeaponType)->mShortGroup; + void CharacterController::refreshCurrentAnims( + CharacterState idle, CharacterState movement, JumpingState jump, bool force) + { + // If the current animation is scripted, do not touch it + if (isScriptedAnimPlaying()) + return; - refreshJumpAnims(weap, jump, idle, force); - refreshMovementAnims(weap, movement, idle, force); + refreshHitRecoilAnims(); + refreshJumpAnims(jump, force); + refreshMovementAnims(movement, force); - // idle handled last as it can depend on the other states - refreshIdleAnims(weap, idle, force); -} + // idle handled last as it can depend on the other states + refreshIdleAnims(idle, force); + } -void CharacterController::playDeath(float startpoint, CharacterState death) -{ - // Make sure the character was swimming upon death for forward-compatibility - const bool wasSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); - - switch (death) - { - case CharState_SwimDeath: - mCurrentDeath = "swimdeath"; - break; - case CharState_SwimDeathKnockDown: - mCurrentDeath = (wasSwimming ? "swimdeathknockdown" : "deathknockdown"); - break; - case CharState_SwimDeathKnockOut: - mCurrentDeath = (wasSwimming ? "swimdeathknockout" : "deathknockout"); - break; - case CharState_DeathKnockDown: - mCurrentDeath = "deathknockdown"; - break; - case CharState_DeathKnockOut: - mCurrentDeath = "deathknockout"; - break; - default: - mCurrentDeath = "death" + std::to_string(death - CharState_Death1 + 1); - } - mDeathState = death; - - mPtr.getClass().getCreatureStats(mPtr).setDeathAnimation(mDeathState - CharState_Death1); - - // For dead actors, refreshCurrentAnims is no longer called, so we need to disable the movement state manually. - // Note that these animations wouldn't actually be visible (due to the Death animation's priority being higher). - // However, they could still trigger text keys, such as Hit events, or sounds. - mMovementState = CharState_None; - mAnimation->disable(mCurrentMovement); - mCurrentMovement = ""; - mUpperBodyState = UpperCharState_Nothing; - mAnimation->disable(mCurrentWeapon); - mCurrentWeapon = ""; - mHitState = CharState_None; - mAnimation->disable(mCurrentHit); - mCurrentHit = ""; - mIdleState = CharState_None; - mAnimation->disable(mCurrentIdle); - mCurrentIdle = ""; - mJumpState = JumpState_None; - mAnimation->disable(mCurrentJump); - mCurrentJump = ""; - mMovementAnimationControlled = true; - - mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::BlendMask_All, - false, 1.0f, "start", "stop", startpoint, 0); -} + void CharacterController::playDeath(float startpoint, CharacterState death) + { + mDeathState = death; + mCurrentDeath = deathStateToAnimGroup(mDeathState); + mPtr.getClass().getCreatureStats(mPtr).setDeathAnimation(mDeathState - CharState_Death1); -CharacterState CharacterController::chooseRandomDeathState() const -{ - int selected=0; - chooseRandomGroup("death", &selected); - return static_cast(CharState_Death1 + (selected-1)); -} + // For dead actors, refreshCurrentAnims is no longer called, so we need to disable the movement state manually. + // Note that these animations wouldn't actually be visible (due to the Death animation's priority being higher). + // However, they could still trigger text keys, such as Hit events, or sounds. + resetCurrentMovementState(); + resetCurrentWeaponState(); + resetCurrentHitState(); + resetCurrentIdleState(); + resetCurrentJumpState(); -void CharacterController::playRandomDeath(float startpoint) -{ - if (mPtr == getPlayer()) - { - // The first-person animations do not include death, so we need to - // force-switch to third person before playing the death animation. - MWBase::Environment::get().getWorld()->useDeathCamera(); + playBlendedAnimation( + mCurrentDeath, Priority_Death, MWRender::BlendMask_All, false, 1.0f, "start", "stop", startpoint, 0); } - if(mHitState == CharState_SwimKnockDown && mAnimation->hasAnimation("swimdeathknockdown")) - { - mDeathState = CharState_SwimDeathKnockDown; - } - else if(mHitState == CharState_SwimKnockOut && mAnimation->hasAnimation("swimdeathknockout")) + CharacterState CharacterController::chooseRandomDeathState() const { - mDeathState = CharState_SwimDeathKnockOut; + int selected = 0; + chooseRandomGroup("death", &selected); + return static_cast(CharState_Death1 + (selected - 1)); } - else if(MWBase::Environment::get().getWorld()->isSwimming(mPtr) && mAnimation->hasAnimation("swimdeath")) - { - mDeathState = CharState_SwimDeath; - } - else if (mHitState == CharState_KnockDown && mAnimation->hasAnimation("deathknockdown")) - { - mDeathState = CharState_DeathKnockDown; - } - else if (mHitState == CharState_KnockOut && mAnimation->hasAnimation("deathknockout")) - { - mDeathState = CharState_DeathKnockOut; - } - else + + void CharacterController::playRandomDeath(float startpoint) { - mDeathState = chooseRandomDeathState(); - } + if (mPtr == getPlayer()) + { + // The first-person animations do not include death, so we need to + // force-switch to third person before playing the death animation. + MWBase::Environment::get().getWorld()->useDeathCamera(); + } - // Do not interrupt scripted animation by death - if (isPersistentAnimPlaying()) - return; + mDeathState = hitStateToDeathState(mHitState); + if (mDeathState == CharState_None && MWBase::Environment::get().getWorld()->isSwimming(mPtr)) + mDeathState = CharState_SwimDeath; - playDeath(startpoint, mDeathState); -} + if (mDeathState == CharState_None || !mAnimation->hasAnimation(deathStateToAnimGroup(mDeathState))) + mDeathState = chooseRandomDeathState(); -std::string CharacterController::chooseRandomAttackAnimation() const -{ - std::string result; - bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); + // Do not interrupt scripted animation by death + if (isScriptedAnimPlaying()) + return; - if (isSwimming) - result = chooseRandomGroup("swimattack"); + playDeath(startpoint, mDeathState); + } - if (!isSwimming || !mAnimation->hasAnimation(result)) - result = chooseRandomGroup("attack"); + std::string CharacterController::chooseRandomAttackAnimation() const + { + std::string result; + bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); - return result; -} + if (isSwimming) + result = chooseRandomGroup("swimattack"); -CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim) - : mPtr(ptr) - , mWeapon(MWWorld::Ptr()) - , mAnimation(anim) - , mIdleState(CharState_None) - , mMovementState(CharState_None) - , mMovementAnimSpeed(0.f) - , mAdjustMovementAnimSpeed(false) - , mHasMovedInXY(false) - , mMovementAnimationControlled(true) - , mDeathState(CharState_None) - , mFloatToSurface(true) - , mHitState(CharState_None) - , mUpperBodyState(UpperCharState_Nothing) - , mJumpState(JumpState_None) - , mWeaponType(ESM::Weapon::None) - , mAttackStrength(0.f) - , mSkipAnim(false) - , mSecondsOfSwimming(0) - , mSecondsOfRunning(0) - , mTurnAnimationThreshold(0) - , mAttackingOrSpell(false) - , mCastingManualSpell(false) - , mTimeUntilWake(0.f) - , mIsMovingBackward(false) -{ - if(!mAnimation) - return; + if (!isSwimming || !mAnimation->hasAnimation(result)) + result = chooseRandomGroup("attack"); - mAnimation->setTextKeyListener(this); + return result; + } - const MWWorld::Class &cls = mPtr.getClass(); - if(cls.isActor()) + CharacterController::CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation* anim) + : mPtr(ptr) + , mAnimation(anim) { - /* Accumulate along X/Y only for now, until we can figure out how we should - * handle knockout and death which moves the character down. */ - mAnimation->setAccumulation(osg::Vec3f(1.0f, 1.0f, 0.0f)); + if (!mAnimation) + return; - if (cls.hasInventoryStore(mPtr)) + mAnimation->setTextKeyListener(this); + + const MWWorld::Class& cls = mPtr.getClass(); + if (cls.isActor()) { - getActiveWeapon(mPtr, &mWeaponType); - if (mWeaponType != ESM::Weapon::None) + /* Accumulate along X/Y only for now, until we can figure out how we should + * handle knockout and death which moves the character down. */ + mAnimation->setAccumulation(osg::Vec3f(1.0f, 1.0f, 0.0f)); + + if (cls.hasInventoryStore(mPtr)) { - mUpperBodyState = UpperCharState_WeapEquiped; - mCurrentWeapon = getWeaponAnimation(mWeaponType); + getActiveWeapon(mPtr, &mWeaponType); + if (mWeaponType != ESM::Weapon::None) + { + mUpperBodyState = UpperBodyState::WeaponEquipped; + mCurrentWeapon = getWeaponAnimation(mWeaponType); + } + + if (mWeaponType != ESM::Weapon::None && mWeaponType != ESM::Weapon::Spell + && mWeaponType != ESM::Weapon::HandToHand) + { + mAnimation->showWeapons(true); + // Note: controllers for ranged weapon should use time for beginning of animation to play shooting + // properly, for other weapons they should use absolute time. Some mods rely on this behaviour (to + // rotate throwing projectiles, for example) + ESM::WeaponType::Class weaponClass = getWeaponType(mWeaponType)->mWeaponClass; + bool useRelativeDuration = weaponClass == ESM::WeaponType::Ranged; + mAnimation->setWeaponGroup(mCurrentWeapon, useRelativeDuration); + } + + mAnimation->showCarriedLeft(updateCarriedLeftVisible(mWeaponType)); } - if(mWeaponType != ESM::Weapon::None && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::HandToHand) + if (!cls.getCreatureStats(mPtr).isDead()) { - mAnimation->showWeapons(true); - // Note: controllers for ranged weapon should use time for beginning of animation to play shooting properly, - // for other weapons they should use absolute time. Some mods rely on this behaviour (to rotate throwing projectiles, for example) - ESM::WeaponType::Class weaponClass = getWeaponType(mWeaponType)->mWeaponClass; - bool useRelativeDuration = weaponClass == ESM::WeaponType::Ranged; - mAnimation->setWeaponGroup(mCurrentWeapon, useRelativeDuration); + mIdleState = CharState_Idle; + if (cls.getCreatureStats(mPtr).getFallHeight() > 0) + mJumpState = JumpState_InAir; } + else + { + const MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr); + if (cStats.isDeathAnimationFinished()) + { + // Set the death state, but don't play it yet + // We will play it in the first frame, but only if no script set the skipAnim flag + signed char deathanim = cStats.getDeathAnimation(); + if (deathanim == -1) + mDeathState = chooseRandomDeathState(); + else + mDeathState = static_cast(CharState_Death1 + deathanim); - mAnimation->showCarriedLeft(updateCarriedLeftVisible(mWeaponType)); + mFloatToSurface = cStats.getHealth().getBase() != 0; + } + // else: nothing to do, will detect death in the next frame and start playing death animation + } } - - if(!cls.getCreatureStats(mPtr).isDead()) + else { + /* Don't accumulate with non-actors. */ + mAnimation->setAccumulation(osg::Vec3f(0.f, 0.f, 0.f)); + mIdleState = CharState_Idle; - if (cls.getCreatureStats(mPtr).getFallHeight() > 0) - mJumpState = JumpState_InAir; } - else - { - const MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr); - if (cStats.isDeathAnimationFinished()) - { - // Set the death state, but don't play it yet - // We will play it in the first frame, but only if no script set the skipAnim flag - signed char deathanim = cStats.getDeathAnimation(); - if (deathanim == -1) - mDeathState = chooseRandomDeathState(); - else - mDeathState = static_cast(CharState_Death1 + deathanim); - mFloatToSurface = false; - } - // else: nothing to do, will detect death in the next frame and start playing death animation + // Do not update animation status for dead actors + if (mDeathState == CharState_None && (!cls.isActor() || !cls.getCreatureStats(mPtr).isDead())) + refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); + + mAnimation->runAnimation(0.f); + + unpersistAnimationState(); + } + + CharacterController::~CharacterController() + { + if (mAnimation) + { + persistAnimationState(); + mAnimation->setTextKeyListener(nullptr); } } - else + + void CharacterController::handleTextKey( + std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) { - /* Don't accumulate with non-actors. */ - mAnimation->setAccumulation(osg::Vec3f(0.f, 0.f, 0.f)); + std::string_view evt = key->second; - mIdleState = CharState_Idle; - } + MWBase::Environment::get().getLuaManager()->animationTextKey(mPtr, key->second); - // Do not update animation status for dead actors - if(mDeathState == CharState_None && (!cls.isActor() || !cls.getCreatureStats(mPtr).isDead())) - refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); + if (evt.substr(0, 7) == "sound: ") + { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(mPtr, ESM::RefId::stringRefId(evt.substr(7)), 1.0f, 1.0f); + return; + } - mAnimation->runAnimation(0.f); + auto& charClass = mPtr.getClass(); + if (evt.substr(0, 10) == "soundgen: ") + { + std::string_view soundgen = evt.substr(10); - unpersistAnimationState(); -} + // The event can optionally contain volume and pitch modifiers + float volume = 1.0f; + float pitch = 1.0f; -CharacterController::~CharacterController() -{ - if (mAnimation) - { - persistAnimationState(); - mAnimation->setTextKeyListener(nullptr); - } -} + if (soundgen.find(' ') != std::string::npos) + { + std::vector tokens; + Misc::StringUtils::split(soundgen, tokens); + soundgen = tokens[0]; -void split(const std::string &s, char delim, std::vector &elems) { - std::stringstream ss(s); - std::string item; - while (std::getline(ss, item, delim)) { - elems.push_back(item); - } -} + if (tokens.size() >= 2) + { + volume = Misc::StringUtils::toNumeric(tokens[1], volume); + } -void CharacterController::handleTextKey(const std::string &groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) -{ - const std::string &evt = key->second; + if (tokens.size() >= 3) + { + pitch = Misc::StringUtils::toNumeric(tokens[2], pitch); + } + } - if(evt.compare(0, 7, "sound: ") == 0) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(mPtr, evt.substr(7), 1.0f, 1.0f); - return; - } - if(evt.compare(0, 10, "soundgen: ") == 0) - { - std::string soundgen = evt.substr(10); + const ESM::RefId sound = charClass.getSoundIdFromSndGen(mPtr, soundgen); + if (!sound.empty()) + { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + if (soundgen == "left" || soundgen == "right") + { + sndMgr->playSound3D( + mPtr, sound, volume, pitch, MWSound::Type::Foot, MWSound::PlayMode::NoPlayerLocal); + } + else + { + sndMgr->playSound3D(mPtr, sound, volume, pitch); + } + } + return; + } + + if (evt.substr(0, groupname.size()) != groupname || evt.substr(groupname.size(), 2) != ": ") + { + // Not ours, skip it + return; + } - // The event can optionally contain volume and pitch modifiers - float volume=1.f, pitch=1.f; - if (soundgen.find(' ') != std::string::npos) + std::string_view action = evt.substr(groupname.size() + 2); + if (action == "equip attach") { - std::vector tokens; - split(soundgen, ' ', tokens); - soundgen = tokens[0]; - if (tokens.size() >= 2) + if (mUpperBodyState == UpperBodyState::Equipping) { - std::stringstream stream; - stream << tokens[1]; - stream >> volume; + if (groupname == "shield") + mAnimation->showCarriedLeft(true); + else + mAnimation->showWeapons(true); } - if (tokens.size() >= 3) + } + else if (action == "unequip detach") + { + if (mUpperBodyState == UpperBodyState::Unequipping) { - std::stringstream stream; - stream << tokens[2]; - stream >> pitch; + if (groupname == "shield") + mAnimation->showCarriedLeft(false); + else + mAnimation->showWeapons(false); } } - - std::string sound = mPtr.getClass().getSoundIdFromSndGen(mPtr, soundgen); - if(!sound.empty()) + else if (action == "chop hit" || action == "slash hit" || action == "thrust hit" || action == "hit") { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - // NB: landing sound is not played for NPCs here - if(soundgen == "left" || soundgen == "right" || soundgen == "land") + int attackType = -1; + if (action == "hit") { - sndMgr->playSound3D(mPtr, sound, volume, pitch, MWSound::Type::Foot, - MWSound::PlayMode::NoPlayerLocal); + if (groupname == "attack1" || groupname == "swimattack1") + attackType = ESM::Weapon::AT_Chop; + else if (groupname == "attack2" || groupname == "swimattack2") + attackType = ESM::Weapon::AT_Slash; + else if (groupname == "attack3" || groupname == "swimattack3") + attackType = ESM::Weapon::AT_Thrust; } - else + else if (action == "chop hit") + attackType = ESM::Weapon::AT_Chop; + else if (action == "slash hit") + attackType = ESM::Weapon::AT_Slash; + else if (action == "thrust hit") + attackType = ESM::Weapon::AT_Thrust; + // We want to avoid hit keys that come out of nowhere (e.g. in the follow animation) + // and processing multiple hit keys for a single attack + if (mAttackStrength != -1.f) { - sndMgr->playSound3D(mPtr, sound, volume, pitch); + charClass.hit(mPtr, mAttackStrength, attackType, mAttackVictim, mAttackHitPos, mAttackSuccess); + mAttackStrength = -1.f; } } - return; - } + else if (isRandomAttackAnimation(groupname) && action == "start") + { + std::multimap::const_iterator hitKey = key; - if(evt.compare(0, groupname.size(), groupname) != 0 || - evt.compare(groupname.size(), 2, ": ") != 0) - { - // Not ours, skip it - return; + // Not all animations have a hit key defined. If there is none, the hit happens with the start key. + bool hasHitKey = false; + while (hitKey != map.end()) + { + if (hitKey->second.starts_with(groupname)) + { + std::string_view suffix = std::string_view(hitKey->second).substr(groupname.size()); + if (suffix == ": hit") + { + hasHitKey = true; + break; + } + if (suffix == ": stop") + break; + } + ++hitKey; + } + if (!hasHitKey) + { + // State update doesn't expect the start key to be the hit key, + // so we have to do this early. + prepareHit(); + + if (groupname == "attack1" || groupname == "swimattack1") + charClass.hit( + mPtr, mAttackStrength, ESM::Weapon::AT_Chop, mAttackVictim, mAttackHitPos, mAttackSuccess); + else if (groupname == "attack2" || groupname == "swimattack2") + charClass.hit( + mPtr, mAttackStrength, ESM::Weapon::AT_Slash, mAttackVictim, mAttackHitPos, mAttackSuccess); + else if (groupname == "attack3" || groupname == "swimattack3") + charClass.hit( + mPtr, mAttackStrength, ESM::Weapon::AT_Thrust, mAttackVictim, mAttackHitPos, mAttackSuccess); + } + } + else if (action == "shoot attach") + mAnimation->attachArrow(); + else if (action == "shoot release") + { + // See notes for melee release above + if (mAttackStrength != -1.f) + { + mAnimation->releaseArrow(mAttackStrength); + mAttackStrength = -1.f; + } + } + else if (action == "shoot follow attach") + mAnimation->attachArrow(); + // Make sure this key is actually for the RangeType we are casting. The flame atronach has + // the same animation for all range types, so there are 3 "release" keys on the same time, one for each range + // type. + else if (groupname == "spellcast" && action == mAttackType + " release") + { + if (mCanCast) + MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingScriptedSpell); + mCastingScriptedSpell = false; + mCanCast = false; + } + else if (groupname == "containeropen" && action == "loot") + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Container, mPtr); } - size_t off = groupname.size()+2; - size_t len = evt.size() - off; - if(groupname == "shield" && evt.compare(off, len, "equip attach") == 0) - mAnimation->showCarriedLeft(true); - else if(groupname == "shield" && evt.compare(off, len, "unequip detach") == 0) - mAnimation->showCarriedLeft(false); - else if(evt.compare(off, len, "equip attach") == 0) - mAnimation->showWeapons(true); - else if(evt.compare(off, len, "unequip detach") == 0) - mAnimation->showWeapons(false); - else if(evt.compare(off, len, "chop hit") == 0) - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); - else if(evt.compare(off, len, "slash hit") == 0) - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); - else if(evt.compare(off, len, "thrust hit") == 0) - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); - else if(evt.compare(off, len, "hit") == 0) + void CharacterController::updatePtr(const MWWorld::Ptr& ptr) { - if (groupname == "attack1" || groupname == "swimattack1") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); - else if (groupname == "attack2" || groupname == "swimattack2") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); - else if (groupname == "attack3" || groupname == "swimattack3") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); - else - mPtr.getClass().hit(mPtr, mAttackStrength); + mPtr = ptr; } - else if (!groupname.empty() - && (groupname.compare(0, groupname.size()-1, "attack") == 0 || groupname.compare(0, groupname.size()-1, "swimattack") == 0) - && evt.compare(off, len, "start") == 0) + + void CharacterController::updateIdleStormState(bool inwater) const { - std::multimap::const_iterator hitKey = key; + if (!mAnimation->hasAnimation("idlestorm")) + return; - // Not all animations have a hit key defined. If there is none, the hit happens with the start key. - bool hasHitKey = false; - while (hitKey != map.end()) + bool animPlaying = mAnimation->isPlaying("idlestorm"); + if (mUpperBodyState != UpperBodyState::None || inwater) { - if (hitKey->second == groupname + ": hit") + if (animPlaying) + mAnimation->disable("idlestorm"); + return; + } + + const auto world = MWBase::Environment::get().getWorld(); + if (world->isInStorm()) + { + osg::Vec3f stormDirection = world->getStormDirection(); + osg::Vec3f characterDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0, 1, 0); + stormDirection.normalize(); + characterDirection.normalize(); + if (stormDirection * characterDirection < -0.5f) { - hasHitKey = true; - break; + if (!animPlaying) + { + int mask = MWRender::BlendMask_Torso | MWRender::BlendMask_RightArm; + playBlendedAnimation("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, + std::numeric_limits::max(), true); + } + else + { + mAnimation->setLoopingEnabled("idlestorm", true); + } + return; } - if (hitKey->second == groupname + ": stop") - break; - ++hitKey; } - if (!hasHitKey) + + if (animPlaying) { - if (groupname == "attack1" || groupname == "swimattack1") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); - else if (groupname == "attack2" || groupname == "swimattack2") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); - else if (groupname == "attack3" || groupname == "swimattack3") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); + mAnimation->setLoopingEnabled("idlestorm", false); } } - else if (evt.compare(off, len, "shoot attach") == 0) - mAnimation->attachArrow(); - else if (evt.compare(off, len, "shoot release") == 0) - mAnimation->releaseArrow(mAttackStrength); - else if (evt.compare(off, len, "shoot follow attach") == 0) - mAnimation->attachArrow(); - else if (groupname == "spellcast" && evt.substr(evt.size()-7, 7) == "release" - // Make sure this key is actually for the RangeType we are casting. The flame atronach has - // the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type. - && evt.compare(off, len, mAttackType + " release") == 0) + bool CharacterController::updateCarriedLeftVisible(const int weaptype) const { - MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); - mCastingManualSpell = false; + // Shields/torches shouldn't be visible during any operation involving two hands + // There seems to be no text keys for this purpose, except maybe for "[un]equip start/stop", + // but they are also present in weapon drawing animation. + return mAnimation->updateCarriedLeftVisible(weaptype); } - else if (groupname == "shield" && evt.compare(off, len, "block hit") == 0) - mPtr.getClass().block(mPtr); - else if (groupname == "containeropen" && evt.compare(off, len, "loot") == 0) - MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Container, mPtr); -} + float CharacterController::calculateWindUp() const + { + if (mCurrentWeapon.empty() || mWeaponType == ESM::Weapon::PickProbe || isRandomAttackAnimation(mCurrentWeapon)) + return -1.f; -void CharacterController::updatePtr(const MWWorld::Ptr &ptr) -{ - mPtr = ptr; -} + float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " min attack"); + float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " max attack"); + if (minAttackTime == -1.f || minAttackTime >= maxAttackTime) + return -1.f; -void CharacterController::updateIdleStormState(bool inwater) -{ - if (!mAnimation->hasAnimation("idlestorm") || mUpperBodyState != UpperCharState_Nothing || inwater) - { - mAnimation->disable("idlestorm"); - return; + return std::clamp( + (mAnimation->getCurrentTime(mCurrentWeapon) - minAttackTime) / (maxAttackTime - minAttackTime), 0.f, 1.f); } - if (MWBase::Environment::get().getWorld()->isInStorm()) + void CharacterController::prepareHit() { - osg::Vec3f stormDirection = MWBase::Environment::get().getWorld()->getStormDirection(); - osg::Vec3f characterDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); - stormDirection.normalize(); - characterDirection.normalize(); - if (stormDirection * characterDirection < -0.5f) - { - if (!mAnimation->isPlaying("idlestorm")) - { - int mask = MWRender::Animation::BlendMask_Torso | MWRender::Animation::BlendMask_RightArm; - mAnimation->play("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul); - } - else - { - mAnimation->setLoopingEnabled("idlestorm", true); - } + if (mAttackStrength != -1.f) return; + + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + mAttackStrength = calculateWindUp(); + if (mAttackStrength == -1.f) + mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability(prng)); + ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass; + if (weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown) + { + mAttackSuccess = mPtr.getClass().evaluateHit(mPtr, mAttackVictim, mAttackHitPos); + if (!mAttackSuccess) + mAttackStrength = 0.f; + playSwishSound(); } } - if (mAnimation->isPlaying("idlestorm")) + bool CharacterController::updateWeaponState() { - mAnimation->setLoopingEnabled("idlestorm", false); - } -} + // If the current animation is scripted, we can't do anything here. + if (isScriptedAnimPlaying()) + return false; -bool CharacterController::updateCreatureState() -{ - const MWWorld::Class &cls = mPtr.getClass(); - CreatureStats &stats = cls.getCreatureStats(mPtr); + const auto world = MWBase::Environment::get().getWorld(); + auto& prng = world->getPrng(); + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); - int weapType = ESM::Weapon::None; - if(stats.getDrawState() == DrawState_Weapon) - weapType = ESM::Weapon::HandToHand; - else if (stats.getDrawState() == DrawState_Spell) - weapType = ESM::Weapon::Spell; + const MWWorld::Class& cls = mPtr.getClass(); + CreatureStats& stats = cls.getCreatureStats(mPtr); + int weaptype = ESM::Weapon::None; + if (stats.getDrawState() == DrawState::Weapon) + weaptype = ESM::Weapon::HandToHand; + else if (stats.getDrawState() == DrawState::Spell) + weaptype = ESM::Weapon::Spell; - if (weapType != mWeaponType) - { - mWeaponType = weapType; - if (mAnimation->isPlaying(mCurrentWeapon)) - mAnimation->disable(mCurrentWeapon); - } + const bool isWerewolf = cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf(); - if(mAttackingOrSpell) - { - if(mUpperBodyState == UpperCharState_Nothing && mHitState == CharState_None) + const ESM::RefId* downSoundId = nullptr; + bool weaponChanged = false; + bool ammunition = true; + float weapSpeed = 1.f; + if (cls.hasInventoryStore(mPtr)) { - MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); + MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); + MWWorld::ContainerStoreIterator weapon = getActiveWeapon(mPtr, &weaptype); + if (stats.getDrawState() == DrawState::Spell) + weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - std::string startKey = "start"; - std::string stopKey = "stop"; - if (weapType == ESM::Weapon::Spell) + MWWorld::Ptr newWeapon; + if (weapon != inv.end()) { - const std::string spellid = stats.getSpells().getSelectedSpell(); - bool canCast = mCastingManualSpell || MWBase::Environment::get().getWorld()->startSpellCast(mPtr); + newWeapon = *weapon; + if (isRealWeapon(mWeaponType)) + downSoundId = &newWeapon.getClass().getDownSoundId(newWeapon); + } + // weapon->HtH switch: weapon is empty already, so we need to take sound from previous weapon + else if (!mWeapon.isEmpty() && weaptype == ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell) + downSoundId = &mWeapon.getClass().getDownSoundId(mWeapon); - if (!spellid.empty() && canCast) - { - MWMechanics::CastSpell cast(mPtr, nullptr, false, mCastingManualSpell); - cast.playSpellCastingEffects(spellid, false); + if (mWeapon != newWeapon) + { + mWeapon = newWeapon; + weaponChanged = true; + } - if (!mAnimation->hasAnimation("spellcast")) - { - MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately - mCastingManualSpell = false; - } - else + if (stats.getDrawState() == DrawState::Weapon && !mWeapon.isEmpty() + && mWeapon.getType() == ESM::Weapon::sRecordId) + { + weapSpeed = mWeapon.get()->mBase->mData.mSpeed; + MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + int ammotype = getWeaponType(mWeapon.get()->mBase->mData.mType)->mAmmoType; + if (ammotype != ESM::Weapon::None) + ammunition = ammo != inv.end() && ammo->get()->mBase->mData.mType == ammotype; + // Cancel attack if we no longer have ammunition + if (!ammunition) + { + if (mUpperBodyState == UpperBodyState::AttackWindUp) { - const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellid); - const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); - - switch(effectentry.mRange) - { - case 0: mAttackType = "self"; break; - case 1: mAttackType = "touch"; break; - case 2: mAttackType = "target"; break; - } - - startKey = mAttackType + " " + startKey; - stopKey = mAttackType + " " + stopKey; - mCurrentWeapon = "spellcast"; + mAnimation->disable(mCurrentWeapon); + mUpperBodyState = UpperBodyState::WeaponEquipped; } + setAttackingOrSpell(false); } - else - mCurrentWeapon = ""; } - if (weapType != ESM::Weapon::Spell || !mAnimation->hasAnimation("spellcast")) // Not all creatures have a dedicated spellcast animation + MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (torch != inv.end() && torch->getType() == ESM::Light::sRecordId + && updateCarriedLeftVisible(mWeaponType)) { - mCurrentWeapon = chooseRandomAttackAnimation(); - } + if (mAnimation->isPlaying("shield")) + mAnimation->disable("shield"); - if (!mCurrentWeapon.empty()) + playBlendedAnimation("torch", Priority_Torch, MWRender::BlendMask_LeftArm, false, 1.0f, "start", "stop", + 0.0f, std::numeric_limits::max(), true); + } + else if (mAnimation->isPlaying("torch")) { - mAnimation->play(mCurrentWeapon, Priority_Weapon, - MWRender::Animation::BlendMask_All, true, - 1, startKey, stopKey, - 0.0f, 0); - mUpperBodyState = UpperCharState_StartToMinAttack; - - mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability()); - - if (weapType == ESM::Weapon::HandToHand) - playSwishSound(0.0f); + mAnimation->disable("torch"); } } - mAttackingOrSpell = false; - } - - bool animPlaying = mAnimation->getInfo(mCurrentWeapon); - if (!animPlaying) - mUpperBodyState = UpperCharState_Nothing; - return false; -} - -bool CharacterController::updateCarriedLeftVisible(const int weaptype) const -{ - // Shields/torches shouldn't be visible during any operation involving two hands - // There seems to be no text keys for this purpose, except maybe for "[un]equip start/stop", - // but they are also present in weapon drawing animation. - return mAnimation->updateCarriedLeftVisible(weaptype); -} - -bool CharacterController::updateWeaponState(CharacterState& idle) -{ - const MWWorld::Class &cls = mPtr.getClass(); - CreatureStats &stats = cls.getCreatureStats(mPtr); - int weaptype = ESM::Weapon::None; - if(stats.getDrawState() == DrawState_Weapon) - weaptype = ESM::Weapon::HandToHand; - else if (stats.getDrawState() == DrawState_Spell) - weaptype = ESM::Weapon::Spell; - - const bool isWerewolf = cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf(); - - std::string upSoundId; - std::string downSoundId; - bool weaponChanged = false; - if (mPtr.getClass().hasInventoryStore(mPtr)) - { - MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); - MWWorld::ContainerStoreIterator weapon = getActiveWeapon(mPtr, &weaptype); - if(stats.getDrawState() == DrawState_Spell) - weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - - if(weapon != inv.end() && mWeaponType != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::Spell && weaptype != ESM::Weapon::None) - upSoundId = weapon->getClass().getUpSoundId(*weapon); + // For biped actors, blend weapon animations with lower body animations with higher priority + MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon); + if (cls.isBipedal(mPtr)) + priorityWeapon[MWRender::BoneGroup_LowerBody] = Priority_WeaponLowerBody; - if(weapon != inv.end() && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None) - downSoundId = weapon->getClass().getDownSoundId(*weapon); - - // weapon->HtH switch: weapon is empty already, so we need to take sound from previous weapon - if(weapon == inv.end() && !mWeapon.isEmpty() && weaptype == ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell) - downSoundId = mWeapon.getClass().getDownSoundId(mWeapon); + bool forcestateupdate = false; - MWWorld::Ptr newWeapon = weapon != inv.end() ? *weapon : MWWorld::Ptr(); + // We should not play equipping animation and sound during weapon->weapon transition + const bool isStillWeapon = isRealWeapon(mWeaponType) && isRealWeapon(weaptype); - if (mWeapon != newWeapon) + // If the current weapon type was changed in the middle of attack (e.g. by Equip console command or when bound + // spell expires), we should force actor to the "weapon equipped" state, interrupt attack and update animations. + if (isStillWeapon && mWeaponType != weaptype && mUpperBodyState > UpperBodyState::WeaponEquipped) { - mWeapon = newWeapon; - weaponChanged = true; + forcestateupdate = true; + if (!mCurrentWeapon.empty()) + mAnimation->disable(mCurrentWeapon); + mUpperBodyState = UpperBodyState::WeaponEquipped; + setAttackingOrSpell(false); + mAnimation->showWeapons(true); } - } - // For biped actors, blend weapon animations with lower body animations with higher priority - MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon); - if (mPtr.getClass().isBipedal(mPtr)) - priorityWeapon[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; - - bool forcestateupdate = false; - - // We should not play equipping animation and sound during weapon->weapon transition - bool isStillWeapon = weaptype != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::Spell && weaptype != ESM::Weapon::None && - mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None; - - // If the current weapon type was changed in the middle of attack (e.g. by Equip console command or when bound spell expires), - // we should force actor to the "weapon equipped" state, interrupt attack and update animations. - if (isStillWeapon && mWeaponType != weaptype && mUpperBodyState > UpperCharState_WeapEquiped) - { - forcestateupdate = true; - mUpperBodyState = UpperCharState_WeapEquiped; - mAttackingOrSpell = false; - mAnimation->disable(mCurrentWeapon); - mAnimation->showWeapons(true); - if (mPtr == getPlayer()) - MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); - } - - if(!isKnockedOut() && !isKnockedDown() && !isRecovery()) - { - std::string weapgroup; - if ((!isWerewolf || mWeaponType != ESM::Weapon::Spell) - && weaptype != mWeaponType - && mUpperBodyState != UpperCharState_UnEquipingWeap - && !isStillWeapon) + if (!isKnockedOut() && !isKnockedDown() && !isRecovery()) { - // We can not play un-equip animation if weapon changed since last update - if (!weaponChanged) + std::string weapgroup; + if ((!isWerewolf || mWeaponType != ESM::Weapon::Spell) && weaptype != mWeaponType + && mUpperBodyState <= UpperBodyState::AttackWindUp && mUpperBodyState != UpperBodyState::Unequipping + && !isStillWeapon) { - // Note: we do not disable unequipping animation automatically to avoid body desync - weapgroup = getWeaponAnimation(mWeaponType); - int unequipMask = MWRender::Animation::BlendMask_All; - bool useShieldAnims = mAnimation->useShieldAnimations(); - if (useShieldAnims && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && !(mWeaponType == ESM::Weapon::None && weaptype == ESM::Weapon::Spell)) + // We can not play un-equip animation if weapon changed since last update + if (!weaponChanged) { - unequipMask = unequipMask |~MWRender::Animation::BlendMask_LeftArm; - mAnimation->play("shield", Priority_Block, - MWRender::Animation::BlendMask_LeftArm, true, - 1.0f, "unequip start", "unequip stop", 0.0f, 0); - } - else if (mWeaponType == ESM::Weapon::HandToHand) - mAnimation->showCarriedLeft(false); + // Note: we do not disable unequipping animation automatically to avoid body desync + weapgroup = getWeaponAnimation(mWeaponType); + int unequipMask = MWRender::BlendMask_All; + bool useShieldAnims = mAnimation->useShieldAnimations(); + if (useShieldAnims && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell + && !(mWeaponType == ESM::Weapon::None && weaptype == ESM::Weapon::Spell)) + { + unequipMask = unequipMask | ~MWRender::BlendMask_LeftArm; + playBlendedAnimation("shield", Priority_Block, MWRender::BlendMask_LeftArm, true, 1.0f, + "unequip start", "unequip stop", 0.0f, 0); + } + else if (mWeaponType == ESM::Weapon::HandToHand) + mAnimation->showCarriedLeft(false); - mAnimation->play(weapgroup, priorityWeapon, unequipMask, false, - 1.0f, "unequip start", "unequip stop", 0.0f, 0); - mUpperBodyState = UpperCharState_UnEquipingWeap; + playBlendedAnimation( + weapgroup, priorityWeapon, unequipMask, false, 1.0f, "unequip start", "unequip stop", 0.0f, 0); + mUpperBodyState = UpperBodyState::Unequipping; - mAnimation->detachArrow(); + mAnimation->detachArrow(); - // If we do not have the "unequip detach" key, hide weapon manually. - if (mAnimation->getTextKeyTime(weapgroup+": unequip detach") < 0) - mAnimation->showWeapons(false); - } + // If we do not have the "unequip detach" key, hide weapon manually. + if (mAnimation->getTextKeyTime(weapgroup + ": unequip detach") < 0) + mAnimation->showWeapons(false); + } - if(!downSoundId.empty()) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(mPtr, downSoundId, 1.0f, 1.0f); + if (downSoundId && !downSoundId->empty()) + { + sndMgr->playSound3D(mPtr, *downSoundId, 1.0f, 1.0f); + } } - } - float complete; - bool animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); - if (!animPlaying || complete >= 1.0f) - { - // Weapon is changed, no current animation (e.g. unequipping or attack). - // Start equipping animation now. - if (weaptype != mWeaponType) + float complete; + bool animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); + if (!animPlaying || complete >= 1.0f) { - forcestateupdate = true; - bool useShieldAnims = mAnimation->useShieldAnimations(); - if (!useShieldAnims) - mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); + // Weapon is changed, no current animation (e.g. unequipping or attack). + // Start equipping animation now. + if (weaptype != mWeaponType && mUpperBodyState <= UpperBodyState::WeaponEquipped) + { + forcestateupdate = true; + bool useShieldAnims = mAnimation->useShieldAnimations(); + if (!useShieldAnims) + mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); - weapgroup = getWeaponAnimation(weaptype); + weapgroup = getWeaponAnimation(weaptype); - // Note: controllers for ranged weapon should use time for beginning of animation to play shooting properly, - // for other weapons they should use absolute time. Some mods rely on this behaviour (to rotate throwing projectiles, for example) - ESM::WeaponType::Class weaponClass = getWeaponType(weaptype)->mWeaponClass; - bool useRelativeDuration = weaponClass == ESM::WeaponType::Ranged; - mAnimation->setWeaponGroup(weapgroup, useRelativeDuration); + // Note: controllers for ranged weapon should use time for beginning of animation to play shooting + // properly, for other weapons they should use absolute time. Some mods rely on this behaviour (to + // rotate throwing projectiles, for example) + ESM::WeaponType::Class weaponClass = getWeaponType(weaptype)->mWeaponClass; + bool useRelativeDuration = weaponClass == ESM::WeaponType::Ranged; + mAnimation->setWeaponGroup(weapgroup, useRelativeDuration); - if (!isStillWeapon) - { - mAnimation->disable(mCurrentWeapon); - if (weaptype != ESM::Weapon::None) + if (!isStillWeapon) { - mAnimation->showWeapons(false); - int equipMask = MWRender::Animation::BlendMask_All; - if (useShieldAnims && weaptype != ESM::Weapon::Spell) + if (animPlaying) + mAnimation->disable(mCurrentWeapon); + if (weaptype != ESM::Weapon::None) { - equipMask = equipMask |~MWRender::Animation::BlendMask_LeftArm; - mAnimation->play("shield", Priority_Block, - MWRender::Animation::BlendMask_LeftArm, true, - 1.0f, "equip start", "equip stop", 0.0f, 0); - } + mAnimation->showWeapons(false); + int equipMask = MWRender::BlendMask_All; + if (useShieldAnims && weaptype != ESM::Weapon::Spell) + { + equipMask = equipMask | ~MWRender::BlendMask_LeftArm; + playBlendedAnimation("shield", Priority_Block, MWRender::BlendMask_LeftArm, true, 1.0f, + "equip start", "equip stop", 0.0f, 0); + } - mAnimation->play(weapgroup, priorityWeapon, equipMask, true, - 1.0f, "equip start", "equip stop", 0.0f, 0); - mUpperBodyState = UpperCharState_EquipingWeap; + playBlendedAnimation( + weapgroup, priorityWeapon, equipMask, true, 1.0f, "equip start", "equip stop", 0.0f, 0); + mUpperBodyState = UpperBodyState::Equipping; - // If we do not have the "equip attach" key, show weapon manually. - if (weaptype != ESM::Weapon::Spell) - { - if (mAnimation->getTextKeyTime(weapgroup+": equip attach") < 0) + // If we do not have the "equip attach" key, show weapon manually. + if (weaptype != ESM::Weapon::Spell + && mAnimation->getTextKeyTime(weapgroup + ": equip attach") < 0) + { mAnimation->showWeapons(true); + } + + if (!mWeapon.isEmpty() && mWeaponType != ESM::Weapon::HandToHand && isRealWeapon(weaptype)) + { + const ESM::RefId& upSoundId = mWeapon.getClass().getUpSoundId(mWeapon); + if (!upSoundId.empty()) + sndMgr->playSound3D(mPtr, upSoundId, 1.0f, 1.0f); + } } } - } - if(isWerewolf) - { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfEquip"); - if(sound) + if (isWerewolf) { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); + const MWWorld::ESMStore& store = world->getStore(); + const ESM::Sound* sound = store.get().searchRandom("WolfEquip", prng); + if (sound) + { + sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); + } } + + mWeaponType = weaptype; + mCurrentWeapon = weapgroup; } - mWeaponType = weaptype; - mCurrentWeapon = getWeaponAnimation(mWeaponType); - - if(!upSoundId.empty() && !isStillWeapon) + // Make sure that we disabled unequipping animation + if (mUpperBodyState == UpperBodyState::Unequipping) { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(mPtr, upSoundId, 1.0f, 1.0f); + resetCurrentWeaponState(); + mWeaponType = ESM::Weapon::None; } } - - // Make sure that we disabled unequipping animation - if (mUpperBodyState == UpperCharState_UnEquipingWeap) - { - mUpperBodyState = UpperCharState_Nothing; - mAnimation->disable(mCurrentWeapon); - mWeaponType = ESM::Weapon::None; - mCurrentWeapon = getWeaponAnimation(mWeaponType); - } - } - } - - if(isWerewolf) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) - && mHasMovedInXY - && !MWBase::Environment::get().getWorld()->isSwimming(mPtr) - && mWeaponType == ESM::Weapon::None) - { - if(!sndMgr->getSoundPlaying(mPtr, "WolfRun")) - sndMgr->playSound3D(mPtr, "WolfRun", 1.0f, 1.0f, MWSound::Type::Sfx, - MWSound::PlayMode::Loop); - } - else - sndMgr->stopSound3D(mPtr, "WolfRun"); - } - - // Cancel attack if we no longer have ammunition - bool ammunition = true; - bool isWeapon = false; - float weapSpeed = 1.f; - if (mPtr.getClass().hasInventoryStore(mPtr)) - { - MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(mPtr, &weaptype); - isWeapon = (weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()); - if (isWeapon) - { - weapSpeed = weapon->get()->mBase->mData.mSpeed; - MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); - int ammotype = getWeaponType(weapon->get()->mBase->mData.mType)->mAmmoType; - if (ammotype != ESM::Weapon::None && (ammo == inv.end() || ammo->get()->mBase->mData.mType != ammotype)) - ammunition = false; } - if (!ammunition && mUpperBodyState > UpperCharState_WeapEquiped) + if (isWerewolf) { - mAnimation->disable(mCurrentWeapon); - mUpperBodyState = UpperCharState_WeapEquiped; + const ESM::RefId wolfRun = ESM::RefId::stringRefId("WolfRun"); + if (isRunning() && !world->isSwimming(mPtr) && mWeaponType == ESM::Weapon::None) + { + if (!sndMgr->getSoundPlaying(mPtr, wolfRun)) + sndMgr->playSound3D(mPtr, wolfRun, 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop); + } + else + sndMgr->stopSound3D(mPtr, wolfRun); } - } - - // Combat for actors with persistent animations obviously will be buggy - if (isPersistentAnimPlaying()) - return forcestateupdate; - - float complete; - bool animPlaying; - ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass; - if(mAttackingOrSpell) - { - MWWorld::Ptr player = getPlayer(); - bool resetIdle = ammunition; - if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block)) + float complete = 0.f; + bool animPlaying = false; + ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass; + if (getAttackingOrSpell()) { - MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); - mAttackStrength = 0; - - // Randomize attacks for non-bipedal creatures with Weapon flag - if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() && - !mPtr.getClass().isBipedal(mPtr) && - (!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon))) + bool resetIdle = true; + if (mUpperBodyState == UpperBodyState::WeaponEquipped + && (mHitState == CharState_None || mHitState == CharState_Block)) { - mCurrentWeapon = chooseRandomAttackAnimation(); - } + mAttackStrength = -1.f; - if(mWeaponType == ESM::Weapon::Spell) - { - // Unset casting flag, otherwise pressing the mouse button down would - // continue casting every frame if there is no animation - mAttackingOrSpell = false; - if (mPtr == player) + // Randomize attacks for non-bipedal creatures + if (!cls.isBipedal(mPtr) + && (!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon))) { - MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); - - // For the player, set the spell we want to cast - // This has to be done at the start of the casting animation, - // *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation) - std::string selectedSpell = MWBase::Environment::get().getWindowManager()->getSelectedSpell(); - stats.getSpells().setSelectedSpell(selectedSpell); + mCurrentWeapon = chooseRandomAttackAnimation(); } - std::string spellid = stats.getSpells().getSelectedSpell(); - bool isMagicItem = false; - bool canCast = mCastingManualSpell || MWBase::Environment::get().getWorld()->startSpellCast(mPtr); - if (spellid.empty()) + if (mWeaponType == ESM::Weapon::Spell) { - if (mPtr.getClass().hasInventoryStore(mPtr)) + // Unset casting flag, otherwise pressing the mouse button down would + // continue casting every frame if there is no animation + setAttackingOrSpell(false); + if (mPtr == getPlayer()) + { + // For the player, set the spell we want to cast + // This has to be done at the start of the casting animation, + // *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation) + const ESM::RefId& selectedSpell + = MWBase::Environment::get().getWindowManager()->getSelectedSpell(); + stats.getSpells().setSelectedSpell(selectedSpell); + } + ESM::RefId spellid = stats.getSpells().getSelectedSpell(); + bool isMagicItem = false; + + // Play hand VFX and allow castSpell use (assuming an animation is going to be played) if + // spellcasting is successful. Scripted spellcasting bypasses restrictions. + MWWorld::SpellCastState spellCastResult = MWWorld::SpellCastState::Success; + if (!mCastingScriptedSpell) + spellCastResult = world->startSpellCast(mPtr); + mCanCast = spellCastResult == MWWorld::SpellCastState::Success; + + if (spellid.empty() && cls.hasInventoryStore(mPtr)) { - MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); if (inv.getSelectedEnchantItem() != inv.end()) { const MWWorld::Ptr& enchantItem = *inv.getSelectedEnchantItem(); @@ -1513,1472 +1548,1667 @@ bool CharacterController::updateWeaponState(CharacterState& idle) isMagicItem = true; } } - } - - static const bool useCastingAnimations = Settings::Manager::getBool("use magic item animations", "Game"); - if (isMagicItem && !useCastingAnimations) - { - // Enchanted items by default do not use casting animations - MWBase::Environment::get().getWorld()->castSpell(mPtr); - resetIdle = false; - } - else if(!spellid.empty() && canCast) - { - MWMechanics::CastSpell cast(mPtr, nullptr, false, mCastingManualSpell); - cast.playSpellCastingEffects(spellid, isMagicItem); - std::vector effects; - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - if (isMagicItem) + if (isMagicItem && !Settings::game().mUseMagicItemAnimations) { - const ESM::Enchantment *enchantment = store.get().find(spellid); - effects = enchantment->mEffects.mList; + world->breakInvisibility(mPtr); + // Enchanted items by default do not use casting animations + world->castSpell(mPtr); + resetIdle = false; + // Spellcasting animation needs to "play" for at least one frame to reset the aiming factor + animPlaying = true; + mUpperBodyState = UpperBodyState::Casting; } - else + // Play the spellcasting animation/VFX if the spellcasting was successful or failed due to + // insufficient magicka. Used up powers are exempt from this from some reason. + else if (!spellid.empty() && spellCastResult != MWWorld::SpellCastState::PowerAlreadyUsed) { - const ESM::Spell *spell = store.get().find(spellid); - effects = spell->mEffects.mList; - } + world->breakInvisibility(mPtr); + MWMechanics::CastSpell cast(mPtr, {}, false, mCastingScriptedSpell); - const ESM::MagicEffect *effect = store.get().find(effects.back().mEffectID); // use last effect of list for color of VFX_Hands - - const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Hands"); + const std::vector* effects{ nullptr }; + const MWWorld::ESMStore& store = world->getStore(); + if (isMagicItem) + { + const ESM::Enchantment* enchantment = store.get().find(spellid); + effects = &enchantment->mEffects.mList; + cast.playSpellCastingEffects(enchantment); + } + else + { + const ESM::Spell* spell = store.get().find(spellid); + effects = &spell->mEffects.mList; + cast.playSpellCastingEffects(spell); + } + if (!effects->empty()) + { + if (mCanCast) + { + const ESM::MagicEffect* effect = store.get().find( + effects->back().mData.mEffectID); // use last effect of list for color of VFX_Hands - for (size_t iter = 0; iter < effects.size(); ++iter) // play hands vfx for each effect - { - if (mAnimation->getNode("Bip01 L Hand")) - mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle); + const ESM::Static* castStatic + = world->getStore().get().find(ESM::RefId::stringRefId("VFX_Hands")); - if (mAnimation->getNode("Bip01 R Hand")) - mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle); - } + if (mAnimation->getNode("Bip01 L Hand")) + mAnimation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), + "", false, "Bip01 L Hand", effect->mParticle); - const ESM::ENAMstruct &firstEffect = effects.at(0); // first effect used for casting animation + if (mAnimation->getNode("Bip01 R Hand")) + mAnimation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), + "", false, "Bip01 R Hand", effect->mParticle); + } + // first effect used for casting animation + const ESM::ENAMstruct& firstEffect = effects->front().mData; - std::string startKey; - std::string stopKey; - if (isRandomAttackAnimation(mCurrentWeapon)) - { - startKey = "start"; - stopKey = "stop"; - MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately - mCastingManualSpell = false; + std::string startKey; + std::string stopKey; + if (isRandomAttackAnimation(mCurrentWeapon)) + { + startKey = "start"; + stopKey = "stop"; + if (mCanCast) + world->castSpell(mPtr, + mCastingScriptedSpell); // No "release" text key to use, so cast immediately + mCastingScriptedSpell = false; + mCanCast = false; + } + else + { + switch (firstEffect.mRange) + { + case 0: + mAttackType = "self"; + break; + case 1: + mAttackType = "touch"; + break; + case 2: + mAttackType = "target"; + break; + } + + startKey = mAttackType + " start"; + stopKey = mAttackType + " stop"; + } + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, 1, + startKey, stopKey, 0.0f, 0); + mUpperBodyState = UpperBodyState::Casting; + } } else { - switch(firstEffect.mRange) - { - case 0: mAttackType = "self"; break; - case 1: mAttackType = "touch"; break; - case 2: mAttackType = "target"; break; - } - - startKey = mAttackType+" start"; - stopKey = mAttackType+" stop"; + resetIdle = false; } - - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, true, - 1, startKey, stopKey, - 0.0f, 0); - mUpperBodyState = UpperCharState_CastingSpell; - } - else - { - resetIdle = false; - } - } - else if(mWeaponType == ESM::Weapon::PickProbe) - { - MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - MWWorld::Ptr item = *weapon; - // TODO: this will only work for the player, and needs to be fixed if NPCs should ever use lockpicks/probes. - MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getFacedObject(); - std::string resultMessage, resultSound; - - if(!target.isEmpty()) - { - if(item.getTypeName() == typeid(ESM::Lockpick).name()) - Security(mPtr).pickLock(target, item, resultMessage, resultSound); - else if(item.getTypeName() == typeid(ESM::Probe).name()) - Security(mPtr).probeTrap(target, item, resultMessage, resultSound); - } - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, true, - 1.0f, "start", "stop", 0.0, 0); - mUpperBodyState = UpperCharState_FollowStartToFollowStop; - - if(!resultMessage.empty()) - MWBase::Environment::get().getWindowManager()->messageBox(resultMessage); - if(!resultSound.empty()) - MWBase::Environment::get().getSoundManager()->playSound3D(target, resultSound, - 1.0f, 1.0f); - } - else if (ammunition) - { - std::string startKey; - std::string stopKey; - - if(weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown) - { - mAttackType = "shoot"; - startKey = mAttackType+" start"; - stopKey = mAttackType+" min attack"; - } - else if (isRandomAttackAnimation(mCurrentWeapon)) - { - startKey = "start"; - stopKey = "stop"; } else { - if(mPtr == getPlayer()) + std::string startKey = "start"; + std::string stopKey = "stop"; + + MWBase::LuaManager::ActorControls* actorControls + = MWBase::Environment::get().getLuaManager()->getActorControls(mPtr); + const bool aiInactive + = actorControls->mDisableAI || !MWBase::Environment::get().getMechanicsManager()->isAIActive(); + if (mWeaponType != ESM::Weapon::PickProbe && !isRandomAttackAnimation(mCurrentWeapon)) { - if (Settings::Manager::getBool("best attack", "Game")) + if (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown) + mAttackType = "shoot"; + else if (mPtr == getPlayer()) { - if (isWeapon) + if (Settings::game().mBestAttack) { - MWWorld::ConstContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - mAttackType = getBestAttack(weapon->get()->mBase); + if (!mWeapon.isEmpty() && mWeapon.getType() == ESM::Weapon::sRecordId) + { + mAttackType = getBestAttack(mWeapon.get()->mBase); + } + else + { + // There is no "best attack" for Hand-to-Hand + mAttackType = getRandomAttackType(); + } } else { - // There is no "best attack" for Hand-to-Hand - setAttackTypeRandomly(mAttackType); + mAttackType = getMovementBasedAttackType(); } } - else + else if (aiInactive) { - setAttackTypeBasedOnMovement(); + mAttackType = getDesiredAttackType(); + if (mAttackType == "") + mAttackType = getRandomAttackType(); } + + // else if (mPtr != getPlayer()) use mAttackType set by AiCombat + startKey = mAttackType + ' ' + startKey; + stopKey = mAttackType + " max attack"; } - // else if (mPtr != getPlayer()) use mAttackType set by AiCombat - startKey = mAttackType+" start"; - stopKey = mAttackType+" min attack"; + + mUpperBodyState = UpperBodyState::AttackWindUp; + + // Reset the attack results when the attack starts. + // Strictly speaking this should probably be done when the attack ends, + // but the attack animation might be cancelled in a myriad different ways. + mAttackSuccess = false; + mAttackVictim = MWWorld::Ptr(); + mAttackHitPos = osg::Vec3f(); + + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, weapSpeed, + startKey, stopKey, 0.0f, 0); } + } - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, false, - weapSpeed, startKey, stopKey, - 0.0f, 0); - mUpperBodyState = UpperCharState_StartToMinAttack; + // We should not break swim and sneak animations + if (resetIdle && mIdleState != CharState_IdleSneak && mIdleState != CharState_IdleSwim) + { + resetCurrentIdleState(); } } - // We should not break swim and sneak animations - if (resetIdle && - idle != CharState_IdleSneak && idle != CharState_IdleSwim && - mIdleState != CharState_IdleSneak && mIdleState != CharState_IdleSwim) + // Random attack and pick/probe animations never have wind up and are played to their end. + // Other animations must be released when the attack state is unset. + if (mUpperBodyState == UpperBodyState::AttackWindUp + && (mWeaponType == ESM::Weapon::PickProbe || isRandomAttackAnimation(mCurrentWeapon) + || !getAttackingOrSpell())) { - mAnimation->disable(mCurrentIdle); - mIdleState = CharState_None; - } + mUpperBodyState = UpperBodyState::AttackRelease; + world->breakInvisibility(mPtr); + if (mWeaponType == ESM::Weapon::PickProbe) + { + // TODO: this will only work for the player, and needs to be fixed if NPCs should ever use + // lockpicks/probes. + MWWorld::Ptr target = world->getFacedObject(); - animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); - if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown()) - mAttackStrength = complete; - } - else - { - animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); - if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown()) - { - float attackStrength = complete; - float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"min attack"); - float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"max attack"); - if (minAttackTime == maxAttackTime) + if (!target.isEmpty()) + { + std::string_view resultMessage, resultSound; + if (mWeapon.getType() == ESM::Lockpick::sRecordId) + Security(mPtr).pickLock(target, mWeapon, resultMessage, resultSound); + else if (mWeapon.getType() == ESM::Probe::sRecordId) + Security(mPtr).probeTrap(target, mWeapon, resultMessage, resultSound); + if (!resultMessage.empty()) + MWBase::Environment::get().getWindowManager()->messageBox(resultMessage); + if (!resultSound.empty()) + sndMgr->playSound3D(target, ESM::RefId::stringRefId(resultSound), 1.0f, 1.0f); + } + } + // Evaluate the attack results and play the swish sound. + // Attack animations with no hit key do this earlier. + else { - // most creatures don't actually have an attack wind-up animation, so use a uniform random value - // (even some creatures that can use weapons don't have a wind-up animation either, e.g. Rieklings) - // Note: vanilla MW uses a random value for *all* non-player actors, but we probably don't need to go that far. - attackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability()); + prepareHit(); } - if(weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown) + if (mWeaponType == ESM::Weapon::PickProbe || isRandomAttackAnimation(mCurrentWeapon)) + mUpperBodyState = UpperBodyState::AttackEnd; + } + + if (mUpperBodyState == UpperBodyState::AttackRelease) + { + // The release state might have been reached before reaching the wind-up section. We'll play the new section + // only when the wind-up section is reached. + float currentTime = mAnimation->getCurrentTime(mCurrentWeapon); + float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " min attack"); + float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " max attack"); + if (minAttackTime <= currentTime && currentTime <= maxAttackTime) { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + std::string hit = mAttackType != "shoot" ? "hit" : "release"; - if(isWerewolf) + float startPoint = 0.f; + + // Skip a bit of the pre-hit section based on the attack strength + if (minAttackTime != -1.f && minAttackTime < maxAttackTime) { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfSwing"); - if(sound) - sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); + startPoint = 1.f - mAttackStrength; + float minHitTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " min hit"); + float hitTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + ' ' + hit); + if (maxAttackTime <= minHitTime && minHitTime < hitTime) + startPoint *= (minHitTime - maxAttackTime) / (hitTime - maxAttackTime); } - else + + mAnimation->disable(mCurrentWeapon); + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, weapSpeed, + mAttackType + " max attack", mAttackType + ' ' + hit, startPoint, 0); + } + + animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); + + // Try playing the "follow" section if the attack animation ended naturally or didn't play at all. + if (!animPlaying || (currentTime >= maxAttackTime && complete >= 1.f)) + { + std::string start = "follow start"; + std::string stop = "follow stop"; + + if (mAttackType != "shoot") { - playSwishSound(attackStrength); + std::string strength = mAttackStrength < 0.33f ? "small" + : mAttackStrength < 0.66f ? "medium" + : "large"; + start = strength + ' ' + start; + stop = strength + ' ' + stop; } - } - mAttackStrength = attackStrength; - mAnimation->disable(mCurrentWeapon); - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, false, - weapSpeed, mAttackType+" max attack", mAttackType+" min hit", - 1.0f-complete, 0); + // Reset attack strength to make extra sure hits that come out of nowhere aren't processed + mAttackStrength = -1.f; - complete = 0.f; - mUpperBodyState = UpperCharState_MaxAttackToMinHit; + if (animPlaying) + mAnimation->disable(mCurrentWeapon); + MWRender::Animation::AnimPriority priorityFollow(priorityWeapon); + // Follow animations have lower priority than movement for non-biped creatures, logic be damned + if (!cls.isBipedal(mPtr)) + priorityFollow = Priority_Default; + playBlendedAnimation(mCurrentWeapon, priorityFollow, MWRender::BlendMask_All, false, weapSpeed, + mAttackType + ' ' + start, mAttackType + ' ' + stop, 0.0f, 0); + mUpperBodyState = UpperBodyState::AttackEnd; + + animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); + } } - else if (isKnockedDown()) + + if (!animPlaying) + animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); + + if (!animPlaying || complete >= 1.f) { - if (mUpperBodyState > UpperCharState_WeapEquiped) + if (mUpperBodyState == UpperBodyState::Equipping || mUpperBodyState == UpperBodyState::AttackEnd + || mUpperBodyState == UpperBodyState::Casting) { - mUpperBodyState = UpperCharState_WeapEquiped; - if (mWeaponType > ESM::Weapon::None) - mAnimation->showWeapons(true); + if (ammunition && mWeaponType == ESM::Weapon::MarksmanCrossbow) + mAnimation->attachArrow(); + + // Cancel stagger animation at the end of an attack to avoid abrupt transitions + // in favor of a different abrupt transition, like Morrowind + if (mUpperBodyState != UpperBodyState::Equipping && isRecovery()) + mAnimation->disable(mCurrentHit); + + if (animPlaying) + mAnimation->disable(mCurrentWeapon); + + mUpperBodyState = UpperBodyState::WeaponEquipped; + } + else if (mUpperBodyState == UpperBodyState::Unequipping) + { + if (animPlaying) + mAnimation->disable(mCurrentWeapon); + mUpperBodyState = UpperBodyState::None; } - mAnimation->disable(mCurrentWeapon); } - } - mAnimation->setPitchFactor(0.f); - if (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown) - { - switch (mUpperBodyState) + mAnimation->setPitchFactor(0.f); + if (mUpperBodyState > UpperBodyState::WeaponEquipped + && (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown)) { - case UpperCharState_StartToMinAttack: - mAnimation->setPitchFactor(complete); - break; - case UpperCharState_MinAttackToMaxAttack: - case UpperCharState_MaxAttackToMinHit: - case UpperCharState_MinHitToHit: mAnimation->setPitchFactor(1.f); - break; - case UpperCharState_FollowStartToFollowStop: - if (animPlaying) + + // A smooth transition can be provided if a pre-wind-up section is defined. Random attack animations never + // have one. + if (mUpperBodyState == UpperBodyState::AttackWindUp && !isRandomAttackAnimation(mCurrentWeapon)) + { + float currentTime = mAnimation->getCurrentTime(mCurrentWeapon); + float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " min attack"); + float startTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " start"); + if (startTime <= currentTime && currentTime < minAttackTime) + mAnimation->setPitchFactor((currentTime - startTime) / (minAttackTime - startTime)); + } + else if (mUpperBodyState == UpperBodyState::AttackEnd) { // technically we do not need a pitch for crossbow reload animation, // but we should avoid abrupt repositioning if (mWeaponType == ESM::Weapon::MarksmanCrossbow) - mAnimation->setPitchFactor(std::max(0.f, 1.f-complete*10.f)); + mAnimation->setPitchFactor(std::max(0.f, 1.f - complete * 10.f)); else - mAnimation->setPitchFactor(1.f-complete); + mAnimation->setPitchFactor(1.f - complete); } - break; - default: - break; } - } - if(!animPlaying) - { - if(mUpperBodyState == UpperCharState_EquipingWeap || - mUpperBodyState == UpperCharState_FollowStartToFollowStop || - mUpperBodyState == UpperCharState_CastingSpell) - { - if (ammunition && mWeaponType == ESM::Weapon::MarksmanCrossbow) - mAnimation->attachArrow(); + mAnimation->setAccurateAiming(mUpperBodyState > UpperBodyState::WeaponEquipped); - mUpperBodyState = UpperCharState_WeapEquiped; - } - else if(mUpperBodyState == UpperCharState_UnEquipingWeap) - mUpperBodyState = UpperCharState_Nothing; + return forcestateupdate; } - else if(complete >= 1.0f && !isRandomAttackAnimation(mCurrentWeapon)) + + void CharacterController::updateAnimQueue() { - std::string start, stop; - switch(mUpperBodyState) + if (mAnimQueue.empty()) + return; + + if (!mAnimation->isPlaying(mAnimQueue.front().mGroup)) { - case UpperCharState_MinAttackToMaxAttack: - //hack to avoid body pos desync when jumping/sneaking in 'max attack' state - if(!mAnimation->isPlaying(mCurrentWeapon)) - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, false, - 0, mAttackType+" min attack", mAttackType+" max attack", 0.999f, 0); - break; - case UpperCharState_StartToMinAttack: - case UpperCharState_MaxAttackToMinHit: + // Playing animations through mwscript is weird. If an animation is + // a looping animation (idle or other cyclical animations), then they + // will end as expected. However, if they are non-looping animations, they + // will stick around forever or until another animation appears in the queue. + bool shouldPlayOrRestart = mAnimQueue.size() > 1; + if (shouldPlayOrRestart || !mAnimQueue.front().mScripted + || (mAnimQueue.front().mLoopCount == 0 && mAnimQueue.front().mLooping)) { - if (mUpperBodyState == UpperCharState_StartToMinAttack) - { - // If actor is already stopped preparing attack, do not play the "min attack -> max attack" part. - // Happens if the player did not hold the attack button. - // Note: if the "min attack"->"max attack" is a stub, "play" it anyway. Attack strength will be random. - float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"min attack"); - float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"max attack"); - if (mAttackingOrSpell || minAttackTime == maxAttackTime) - { - start = mAttackType+" min attack"; - stop = mAttackType+" max attack"; - mUpperBodyState = UpperCharState_MinAttackToMaxAttack; - break; - } - - if(weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown) - playSwishSound(0.0f); - } - - if(mAttackType == "shoot") - { - start = mAttackType+" min hit"; - stop = mAttackType+" release"; - } - else - { - start = mAttackType+" min hit"; - stop = mAttackType+" hit"; - } - mUpperBodyState = UpperCharState_MinHitToHit; - break; + mAnimation->setPlayScriptedOnly(false); + mAnimation->disable(mAnimQueue.front().mGroup); + mAnimQueue.pop_front(); + shouldPlayOrRestart = true; } - case UpperCharState_MinHitToHit: - if(mAttackType == "shoot") - { - start = mAttackType+" follow start"; - stop = mAttackType+" follow stop"; - } - else - { - float str = mAttackStrength; - start = mAttackType+((str < 0.5f) ? " small follow start" - : (str < 1.0f) ? " medium follow start" - : " large follow start"); - stop = mAttackType+((str < 0.5f) ? " small follow stop" - : (str < 1.0f) ? " medium follow stop" - : " large follow stop"); - } - mUpperBodyState = UpperCharState_FollowStartToFollowStop; - break; - default: - break; - } - - // Note: apply crossbow reload animation only for upper body - // since blending with movement animations can give weird result. - if(!start.empty()) - { - int mask = MWRender::Animation::BlendMask_All; - if (mWeaponType == ESM::Weapon::MarksmanCrossbow) - mask = MWRender::Animation::BlendMask_UpperBody; - - mAnimation->disable(mCurrentWeapon); - if (mUpperBodyState == UpperCharState_FollowStartToFollowStop) - mAnimation->play(mCurrentWeapon, priorityWeapon, - mask, true, - weapSpeed, start, stop, 0.0f, 0); else - mAnimation->play(mCurrentWeapon, priorityWeapon, - mask, false, - weapSpeed, start, stop, 0.0f, 0); - } - } - else if(complete >= 1.0f && isRandomAttackAnimation(mCurrentWeapon)) - { - mAnimation->disable(mCurrentWeapon); - mUpperBodyState = UpperCharState_WeapEquiped; - } - - if (mPtr.getClass().hasInventoryStore(mPtr)) - { - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() - && updateCarriedLeftVisible(mWeaponType)) - { - if (mAnimation->isPlaying("shield")) - mAnimation->disable("shield"); + // A non-looping animation will stick around forever, so only restart if the animation + // actually was removed for some reason. + shouldPlayOrRestart = !mAnimation->getInfo(mAnimQueue.front().mGroup) + && mAnimation->hasAnimation(mAnimQueue.front().mGroup); - mAnimation->play("torch", Priority_Torch, MWRender::Animation::BlendMask_LeftArm, - false, 1.0f, "start", "stop", 0.0f, (~(size_t)0), true); + if (shouldPlayOrRestart) + { + // Move on to the remaining items of the queue + playAnimQueue(); + } } - else if (mAnimation->isPlaying("torch")) + else { - mAnimation->disable("torch"); + float complete; + size_t loopcount; + mAnimation->getInfo(mAnimQueue.front().mGroup, &complete, nullptr, &loopcount); + mAnimQueue.front().mLoopCount = loopcount; + mAnimQueue.front().mTime = complete; } - } - mAnimation->setAccurateAiming(mUpperBodyState > UpperCharState_WeapEquiped); - - return forcestateupdate; -} + if (!mAnimQueue.empty()) + mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1); + } -void CharacterController::updateAnimQueue() -{ - if(mAnimQueue.size() > 1) + void CharacterController::playAnimQueue(bool loopStart) { - if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false) + if (!mAnimQueue.empty()) { - mAnimation->disable(mAnimQueue.front().mGroup); - mAnimQueue.pop_front(); - - bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); - mAnimation->play(mAnimQueue.front().mGroup, Priority_Default, - MWRender::Animation::BlendMask_All, false, - 1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount, loopfallback); + clearStateAnimation(mCurrentIdle); + mIdleState = CharState_SpecialIdle; + auto priority = mAnimQueue.front().mScripted ? Priority_Scripted : Priority_Default; + mAnimation->setPlayScriptedOnly(mAnimQueue.front().mScripted); + if (mAnimQueue.front().mScripted) + mAnimation->play(mAnimQueue.front().mGroup, priority, MWRender::BlendMask_All, false, + mAnimQueue.front().mSpeed, (loopStart ? "loop start" : mAnimQueue.front().mStartKey), + mAnimQueue.front().mStopKey, mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount, + mAnimQueue.front().mLooping); + else + playBlendedAnimation(mAnimQueue.front().mGroup, priority, MWRender::BlendMask_All, false, + mAnimQueue.front().mSpeed, (loopStart ? "loop start" : mAnimQueue.front().mStartKey), + mAnimQueue.front().mStopKey, mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount, + mAnimQueue.front().mLooping); } } - if(!mAnimQueue.empty()) - mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1); -} - -void CharacterController::update(float duration) -{ - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Class &cls = mPtr.getClass(); - osg::Vec3f movement(0.f, 0.f, 0.f); - float speed = 0.f; - - updateMagicEffects(); - - if (isKnockedOut()) - mTimeUntilWake -= duration; - - bool isPlayer = mPtr == MWMechanics::getPlayer(); - bool isFirstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson(); - bool godmode = isPlayer && MWBase::Environment::get().getWorld()->getGodModeState(); + void CharacterController::update(float duration) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + const MWWorld::Class& cls = mPtr.getClass(); + osg::Vec3f movement(0.f, 0.f, 0.f); + float speed = 0.f; - float scale = mPtr.getCellRef().getScale(); + updateMagicEffects(); - static const bool normalizeSpeed = Settings::Manager::getBool("normalise race speed", "Game"); - if (!normalizeSpeed && mPtr.getClass().isNpc()) - { - const ESM::NPC* npc = mPtr.get()->mBase; - const ESM::Race* race = world->getStore().get().find(npc->mRace); - float weight = npc->isMale() ? race->mData.mWeight.mMale : race->mData.mWeight.mFemale; - scale *= weight; - } + bool isPlayer = mPtr == MWMechanics::getPlayer(); + bool isFirstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson(); + bool godmode = isPlayer && MWBase::Environment::get().getWorld()->getGodModeState(); - if(!cls.isActor()) - updateAnimQueue(); - else if(!cls.getCreatureStats(mPtr).isDead()) - { - bool onground = world->isOnGround(mPtr); - bool incapacitated = ((!godmode && cls.getCreatureStats(mPtr).isParalyzed()) || cls.getCreatureStats(mPtr).getKnockedDown()); - bool inwater = world->isSwimming(mPtr); - bool flying = world->isFlying(mPtr); - bool solid = world->isActorCollisionEnabled(mPtr); - // Can't run and sneak while flying (see speed formula in Npc/Creature::getSpeed) - bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak) && !flying; - bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying; - CreatureStats &stats = cls.getCreatureStats(mPtr); - Movement& movementSettings = cls.getMovementSettings(mPtr); - - //Force Jump Logic - - bool isMoving = (std::abs(movementSettings.mPosition[0]) > .5 || std::abs(movementSettings.mPosition[1]) > .5); - if(!inwater && !flying && solid) - { - //Force Jump - if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceJump)) - movementSettings.mPosition[2] = onground ? 1 : 0; - //Force Move Jump, only jump if they're otherwise moving - if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceMoveJump) && isMoving) - movementSettings.mPosition[2] = onground ? 1 : 0; - } - - osg::Vec3f rot = cls.getRotationVector(mPtr); - osg::Vec3f vec(movementSettings.asVec3()); - movementSettings.mSpeedFactor = std::min(vec.length(), 1.f); - vec.normalize(); - - // TODO: Move this check to mwinput. - // Joystick analogue movement. - // Due to the half way split between walking/running, we multiply speed by 2 while walking, unless a keyboard was used. - if (isPlayer && !isrunning && !sneak && !flying && movementSettings.mSpeedFactor <= 0.5f) - movementSettings.mSpeedFactor *= 2.f; - - static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); - if (smoothMovement) - { - static const float playerTurningCoef = 1.0 / std::max(0.01f, Settings::Manager::getFloat("smooth movement player turning delay", "Game")); - float angle = mPtr.getRefData().getPosition().rot[2]; - osg::Vec2f targetSpeed = Misc::rotateVec2f(osg::Vec2f(vec.x(), vec.y()), -angle) * movementSettings.mSpeedFactor; - osg::Vec2f delta = targetSpeed - mSmoothedSpeed; - float speedDelta = movementSettings.mSpeedFactor - mSmoothedSpeed.length(); - float deltaLen = delta.length(); - - float maxDelta; - if (isFirstPersonPlayer) - maxDelta = 1; - else if (std::abs(speedDelta) < deltaLen / 2) - // Turning is smooth for player and less smooth for NPCs (otherwise NPC can miss a path point). - maxDelta = duration * (isPlayer ? playerTurningCoef : 6.f); - else if (isPlayer && speedDelta < -deltaLen / 2) - // As soon as controls are released, mwinput switches player from running to walking. - // So stopping should be instant for player, otherwise it causes a small twitch. - maxDelta = 1; - else // In all other cases speeding up and stopping are smooth. - maxDelta = duration * 3.f; - - if (deltaLen > maxDelta) - delta *= maxDelta / deltaLen; - mSmoothedSpeed += delta; - - osg::Vec2f newSpeed = Misc::rotateVec2f(mSmoothedSpeed, angle); - movementSettings.mSpeedFactor = newSpeed.normalize(); - vec.x() = newSpeed.x(); - vec.y() = newSpeed.y(); - - const float eps = 0.001f; - if (movementSettings.mSpeedFactor < eps) - { - movementSettings.mSpeedFactor = 0; - vec.x() = 0; - vec.y() = 1; - } - else if ((vec.y() < 0) != mIsMovingBackward) - { - if (targetSpeed.length() < eps || (movementSettings.mPosition[1] < 0) == mIsMovingBackward) - vec.y() = mIsMovingBackward ? -eps : eps; - } - vec.normalize(); - } + float scale = mPtr.getCellRef().getScale(); - float effectiveRotation = rot.z(); - bool canMove = cls.getMaxSpeed(mPtr) > 0; - static const bool turnToMovementDirection = Settings::Manager::getBool("turn to movement direction", "Game"); - if (!turnToMovementDirection || isFirstPersonPlayer) + if (!Settings::game().mNormaliseRaceSpeed && cls.isNpc()) { - movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2; - stats.setSideMovementAngle(0); + const ESM::NPC* npc = mPtr.get()->mBase; + const ESM::Race* race = world->getStore().get().find(npc->mRace); + float weight = npc->isMale() ? race->mData.mMaleWeight : race->mData.mFemaleWeight; + scale *= weight; } - else if (canMove) - { - float targetMovementAngle = vec.y() >= 0 ? std::atan2(-vec.x(), vec.y()) : std::atan2(vec.x(), -vec.y()); - movementSettings.mIsStrafing = (stats.getDrawState() != MWMechanics::DrawState_Nothing || inwater) - && std::abs(targetMovementAngle) > osg::DegreesToRadians(60.0f); - if (movementSettings.mIsStrafing) - targetMovementAngle = 0; - float delta = targetMovementAngle - stats.getSideMovementAngle(); - float cosDelta = cosf(delta); - - if ((vec.y() < 0) == mIsMovingBackward) - movementSettings.mSpeedFactor *= std::min(std::max(cosDelta, 0.f) + 0.3f, 1.f); // slow down when turn - if (std::abs(delta) < osg::DegreesToRadians(20.0f)) - mIsMovingBackward = vec.y() < 0; - float maxDelta = osg::PI * duration * (2.5f - cosDelta); - delta = osg::clampBetween(delta, -maxDelta, maxDelta); - stats.setSideMovementAngle(stats.getSideMovementAngle() + delta); - effectiveRotation += delta; + if (cls.isActor() && cls.getCreatureStats(mPtr).wasTeleported()) + { + mSmoothedSpeed = osg::Vec2f(); + cls.getCreatureStats(mPtr).setTeleported(false); } - mAnimation->setLegsYawRadians(stats.getSideMovementAngle()); - if (stats.getDrawState() == MWMechanics::DrawState_Nothing || inwater) - mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2); - else - mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4); - if (smoothMovement && !isPlayer && !inwater) - mAnimation->setUpperBodyYawRadians(mAnimation->getUpperBodyYawRadians() + mAnimation->getHeadYaw() / 2); - - speed = cls.getCurrentSpeed(mPtr); - vec.x() *= speed; - vec.y() *= speed; - - if(mHitState != CharState_None && mJumpState == JumpState_None) - vec = osg::Vec3f(); - - CharacterState movestate = CharState_None; - CharacterState idlestate = CharState_SpecialIdle; - JumpingState jumpstate = JumpState_None; - - bool forcestateupdate = false; + if (!cls.isActor()) + updateAnimQueue(); + else if (!cls.getCreatureStats(mPtr).isDead()) + { + bool onground = world->isOnGround(mPtr); + bool inwater = world->isSwimming(mPtr); + bool flying = world->isFlying(mPtr); + bool solid = world->isActorCollisionEnabled(mPtr); + // Can't run and sneak while flying (see speed formula in Npc/Creature::getSpeed) + bool sneak + = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak) && !flying && !inwater; + bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying; + CreatureStats& stats = cls.getCreatureStats(mPtr); + Movement& movementSettings = cls.getMovementSettings(mPtr); + + // Force Jump Logic + + bool isMoving + = (std::abs(movementSettings.mPosition[0]) > .5 || std::abs(movementSettings.mPosition[1]) > .5); + if (!inwater && !flying) + { + // Force Jump + if (stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceJump)) + movementSettings.mPosition[2] = onground ? 1 : 0; + // Force Move Jump, only jump if they're otherwise moving + if (stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceMoveJump) && isMoving) + movementSettings.mPosition[2] = onground ? 1 : 0; + } - mHasMovedInXY = std::abs(vec.x())+std::abs(vec.y()) > 0.0f; - isrunning = isrunning && mHasMovedInXY; + osg::Vec3f rot = cls.getRotationVector(mPtr); + osg::Vec3f vec(movementSettings.asVec3()); + movementSettings.mSpeedFactor = std::min(vec.length(), 1.f); + vec.normalize(); - // advance athletics - if(mHasMovedInXY && isPlayer) - { - if(inwater) + const bool smoothMovement = Settings::game().mSmoothMovement; + if (smoothMovement) { - mSecondsOfSwimming += duration; - while(mSecondsOfSwimming > 1) + float angle = mPtr.getRefData().getPosition().rot[2]; + osg::Vec2f targetSpeed + = Misc::rotateVec2f(osg::Vec2f(vec.x(), vec.y()), -angle) * movementSettings.mSpeedFactor; + osg::Vec2f delta = targetSpeed - mSmoothedSpeed; + float speedDelta = movementSettings.mSpeedFactor - mSmoothedSpeed.length(); + float deltaLen = delta.length(); + + float maxDelta; + if (isFirstPersonPlayer) + maxDelta = 1; + else if (std::abs(speedDelta) < deltaLen / 2) + // Turning is smooth for player and less smooth for NPCs (otherwise NPC can miss a path point). + maxDelta = duration * (isPlayer ? 1.0 / Settings::game().mSmoothMovementPlayerTurningDelay : 6.f); + else if (isPlayer && speedDelta < -deltaLen / 2) + // As soon as controls are released, mwinput switches player from running to walking. + // So stopping should be instant for player, otherwise it causes a small twitch. + maxDelta = 1; + else // In all other cases speeding up and stopping are smooth. + maxDelta = duration * 3.f; + + if (deltaLen > maxDelta) + delta *= maxDelta / deltaLen; + mSmoothedSpeed += delta; + + osg::Vec2f newSpeed = Misc::rotateVec2f(mSmoothedSpeed, angle); + movementSettings.mSpeedFactor = newSpeed.normalize(); + vec.x() = newSpeed.x(); + vec.y() = newSpeed.y(); + + const float eps = 0.001f; + if (movementSettings.mSpeedFactor < eps) { - cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 1); - mSecondsOfSwimming -= 1; + movementSettings.mSpeedFactor = 0; + vec.x() = 0; + vec.y() = 1; } - } - else if(isrunning && !sneak) - { - mSecondsOfRunning += duration; - while(mSecondsOfRunning > 1) + else if ((vec.y() < 0) != mIsMovingBackward) { - cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 0); - mSecondsOfRunning -= 1; + if (targetSpeed.length() < eps || (movementSettings.mPosition[1] < 0) == mIsMovingBackward) + vec.y() = mIsMovingBackward ? -eps : eps; } + vec.normalize(); } - } - - // reduce fatigue - const MWWorld::Store &gmst = world->getStore().get(); - float fatigueLoss = 0; - static const float fFatigueRunBase = gmst.find("fFatigueRunBase")->mValue.getFloat(); - static const float fFatigueRunMult = gmst.find("fFatigueRunMult")->mValue.getFloat(); - static const float fFatigueSwimWalkBase = gmst.find("fFatigueSwimWalkBase")->mValue.getFloat(); - static const float fFatigueSwimRunBase = gmst.find("fFatigueSwimRunBase")->mValue.getFloat(); - static const float fFatigueSwimWalkMult = gmst.find("fFatigueSwimWalkMult")->mValue.getFloat(); - static const float fFatigueSwimRunMult = gmst.find("fFatigueSwimRunMult")->mValue.getFloat(); - static const float fFatigueSneakBase = gmst.find("fFatigueSneakBase")->mValue.getFloat(); - static const float fFatigueSneakMult = gmst.find("fFatigueSneakMult")->mValue.getFloat(); - if (cls.getEncumbrance(mPtr) <= cls.getCapacity(mPtr)) - { - const float encumbrance = cls.getNormalizedEncumbrance(mPtr); - if (sneak) - fatigueLoss = fFatigueSneakBase + encumbrance * fFatigueSneakMult; - else + float effectiveRotation = rot.z(); + bool canMove = cls.getMaxSpeed(mPtr) > 0; + const bool turnToMovementDirection = Settings::game().mTurnToMovementDirection; + const bool isBiped = mPtr.getClass().isBipedal(mPtr); + if (!isBiped || !turnToMovementDirection || isFirstPersonPlayer) { - if (inwater) - { - if (!isrunning) - fatigueLoss = fFatigueSwimWalkBase + encumbrance * fFatigueSwimWalkMult; - else - fatigueLoss = fFatigueSwimRunBase + encumbrance * fFatigueSwimRunMult; - } - else if (isrunning) - fatigueLoss = fFatigueRunBase + encumbrance * fFatigueRunMult; + movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2; + stats.setSideMovementAngle(0); + } + else if (canMove) + { + float targetMovementAngle + = vec.y() >= 0 ? std::atan2(-vec.x(), vec.y()) : std::atan2(vec.x(), -vec.y()); + movementSettings.mIsStrafing = (stats.getDrawState() != MWMechanics::DrawState::Nothing || inwater) + && std::abs(targetMovementAngle) > osg::DegreesToRadians(60.0f); + if (movementSettings.mIsStrafing) + targetMovementAngle = 0; + float delta = targetMovementAngle - stats.getSideMovementAngle(); + float cosDelta = cosf(delta); + + if ((vec.y() < 0) == mIsMovingBackward) + movementSettings.mSpeedFactor + *= std::min(std::max(cosDelta, 0.f) + 0.3f, 1.f); // slow down when turn + if (std::abs(delta) < osg::DegreesToRadians(20.0f)) + mIsMovingBackward = vec.y() < 0; + + float maxDelta = osg::PI * duration * (2.5f - cosDelta); + delta = std::clamp(delta, -maxDelta, maxDelta); + stats.setSideMovementAngle(stats.getSideMovementAngle() + delta); + effectiveRotation += delta; } - } - fatigueLoss *= duration; - fatigueLoss *= movementSettings.mSpeedFactor; - DynamicStat fatigue = cls.getCreatureStats(mPtr).getFatigue(); - if (!godmode) - { - fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss, fatigue.getCurrent() < 0); - cls.getCreatureStats(mPtr).setFatigue(fatigue); - } + mAnimation->setLegsYawRadians(stats.getSideMovementAngle()); + if (stats.getDrawState() == MWMechanics::DrawState::Nothing || inwater) + mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2); + else + mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4); + if (smoothMovement && !isPlayer && !inwater) + mAnimation->setUpperBodyYawRadians(mAnimation->getUpperBodyYawRadians() + mAnimation->getHeadYaw() / 2); - float z = cls.getJump(mPtr); - if(sneak || inwater || flying || incapacitated || !solid || z <= 0) - vec.z() = 0.0f; + speed = cls.getCurrentSpeed(mPtr); + vec.x() *= speed; + vec.y() *= speed; - bool inJump = true; - bool playLandingSound = false; - if(!onground && !flying && !inwater && solid) - { - // In the air (either getting up —ascending part of jump— or falling). + if (isKnockedOut() || isKnockedDown() || isRecovery() || isScriptedAnimPlaying()) + vec = osg::Vec3f(); - forcestateupdate = (mJumpState != JumpState_InAir); - jumpstate = JumpState_InAir; + CharacterState movestate = CharState_None; + CharacterState idlestate = CharState_None; + JumpingState jumpstate = JumpState_None; - static const float fJumpMoveBase = gmst.find("fJumpMoveBase")->mValue.getFloat(); - static const float fJumpMoveMult = gmst.find("fJumpMoveMult")->mValue.getFloat(); - float factor = fJumpMoveBase + fJumpMoveMult * mPtr.getClass().getSkill(mPtr, ESM::Skill::Acrobatics)/100.f; - factor = std::min(1.f, factor); - vec.x() *= factor; - vec.y() *= factor; - vec.z() = 0.0f; - } - else if(vec.z() > 0.0f && mJumpState != JumpState_InAir) - { - // Started a jump. - if (z > 0) + const MWWorld::Store& gmst = world->getStore().get(); + if (vec.x() != 0.f || vec.y() != 0.f) { - if(vec.x() == 0 && vec.y() == 0) - vec = osg::Vec3f(0.0f, 0.0f, z); - else + // advance athletics + if (isPlayer) { - osg::Vec3f lat (vec.x(), vec.y(), 0.0f); - lat.normalize(); - vec = osg::Vec3f(lat.x(), lat.y(), 1.0f) * z * 0.707f; + if (inwater) + { + mSecondsOfSwimming += duration; + while (mSecondsOfSwimming > 1) + { + cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, ESM::Skill::Athletics_SwimOneSecond); + mSecondsOfSwimming -= 1; + } + } + else if (isrunning && !sneak) + { + mSecondsOfRunning += duration; + while (mSecondsOfRunning > 1) + { + cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, ESM::Skill::Athletics_RunOneSecond); + mSecondsOfRunning -= 1; + } + } } - } - } - else if(mJumpState == JumpState_InAir && !inwater && !flying && solid) - { - forcestateupdate = true; - jumpstate = JumpState_Landing; - vec.z() = 0.0f; - - // We should reset idle animation during landing - mAnimation->disable(mCurrentIdle); - float height = cls.getCreatureStats(mPtr).land(isPlayer); - float healthLost = getFallDamage(mPtr, height); - - if (healthLost > 0.0f) - { - const float fatigueTerm = cls.getCreatureStats(mPtr).getFatigueTerm(); - - // inflict fall damages if (!godmode) { - float realHealthLost = static_cast(healthLost * (1.0f - 0.25f * fatigueTerm)); - cls.onHit(mPtr, realHealthLost, true, MWWorld::Ptr(), MWWorld::Ptr(), osg::Vec3f(), true); + // reduce fatigue + float fatigueLoss = 0.f; + static const float fFatigueRunBase = gmst.find("fFatigueRunBase")->mValue.getFloat(); + static const float fFatigueRunMult = gmst.find("fFatigueRunMult")->mValue.getFloat(); + static const float fFatigueSwimWalkBase = gmst.find("fFatigueSwimWalkBase")->mValue.getFloat(); + static const float fFatigueSwimRunBase = gmst.find("fFatigueSwimRunBase")->mValue.getFloat(); + static const float fFatigueSwimWalkMult = gmst.find("fFatigueSwimWalkMult")->mValue.getFloat(); + static const float fFatigueSwimRunMult = gmst.find("fFatigueSwimRunMult")->mValue.getFloat(); + static const float fFatigueSneakBase = gmst.find("fFatigueSneakBase")->mValue.getFloat(); + static const float fFatigueSneakMult = gmst.find("fFatigueSneakMult")->mValue.getFloat(); + + if (cls.getEncumbrance(mPtr) <= cls.getCapacity(mPtr)) + { + const float encumbrance = cls.getNormalizedEncumbrance(mPtr); + if (sneak) + fatigueLoss = fFatigueSneakBase + encumbrance * fFatigueSneakMult; + else + { + if (inwater) + { + if (!isrunning) + fatigueLoss = fFatigueSwimWalkBase + encumbrance * fFatigueSwimWalkMult; + else + fatigueLoss = fFatigueSwimRunBase + encumbrance * fFatigueSwimRunMult; + } + else if (isrunning) + fatigueLoss = fFatigueRunBase + encumbrance * fFatigueRunMult; + } + } + fatigueLoss *= duration; + fatigueLoss *= movementSettings.mSpeedFactor; + DynamicStat fatigue = cls.getCreatureStats(mPtr).getFatigue(); + fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss, fatigue.getCurrent() < 0); + cls.getCreatureStats(mPtr).setFatigue(fatigue); } + } + + bool wasInJump = mInJump; + mInJump = false; + const float jumpHeight = cls.getJump(mPtr); + if (jumpHeight <= 0.f || sneak || inwater || flying || !solid) + { + vec.z() = 0.f; + // Following code might assign some vertical movement regardless, need to reset this manually + // This is used for jumping detection + movementSettings.mPosition[2] = 0; + } - const float acrobaticsSkill = cls.getSkill(mPtr, ESM::Skill::Acrobatics); - if (healthLost > (acrobaticsSkill * fatigueTerm)) + if (!inwater && !flying && solid) + { + // In the air (either getting up —ascending part of jump— or falling). + if (!onground) { - if (!godmode) - cls.getCreatureStats(mPtr).setKnockedDown(true); + mInJump = true; + jumpstate = JumpState_InAir; + + static const float fJumpMoveBase = gmst.find("fJumpMoveBase")->mValue.getFloat(); + static const float fJumpMoveMult = gmst.find("fJumpMoveMult")->mValue.getFloat(); + float factor = fJumpMoveBase + + fJumpMoveMult * mPtr.getClass().getSkill(mPtr, ESM::Skill::Acrobatics) / 100.f; + factor = std::min(1.f, factor); + vec.x() *= factor; + vec.y() *= factor; + vec.z() = 0.0f; } - else + // Started a jump. + else if (mJumpState != JumpState_InAir && vec.z() > 0.f) { - // report acrobatics progression - if (isPlayer) - cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 1); + mInJump = true; + if (vec.x() == 0 && vec.y() == 0) + vec.z() = jumpHeight; + else + { + osg::Vec3f lat(vec.x(), vec.y(), 0.0f); + lat.normalize(); + vec = osg::Vec3f(lat.x(), lat.y(), 1.0f) * jumpHeight * 0.707f; + } } } - if (mPtr.getClass().isNpc()) - playLandingSound = true; - } - else - { - if(mPtr.getClass().isNpc() && mJumpState == JumpState_InAir && !flying && solid) - playLandingSound = true; + if (!mInJump) + { + if (mJumpState == JumpState_InAir && !flying && solid && wasInJump) + { + float height = cls.getCreatureStats(mPtr).land(isPlayer); + float healthLost = 0.f; + if (!inwater) + healthLost = getFallDamage(mPtr, height); - jumpstate = mAnimation->isPlaying(mCurrentJump) ? JumpState_Landing : JumpState_None; + if (healthLost > 0.0f) + { + const float fatigueTerm = cls.getCreatureStats(mPtr).getFatigueTerm(); - vec.x() *= scale; - vec.y() *= scale; - vec.z() = 0.0f; + // inflict fall damages + if (!godmode) + { + DynamicStat health = cls.getCreatureStats(mPtr).getHealth(); + float realHealthLost = healthLost * (1.0f - 0.25f * fatigueTerm); + health.setCurrent(health.getCurrent() - realHealthLost); + cls.getCreatureStats(mPtr).setHealth(health); + sndMgr->playSound3D(mPtr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f); + if (isPlayer) + MWBase::Environment::get().getWindowManager()->activateHitOverlay(); + } - inJump = false; + const float acrobaticsSkill = cls.getSkill(mPtr, ESM::Skill::Acrobatics); + if (healthLost > (acrobaticsSkill * fatigueTerm)) + { + if (!godmode) + cls.getCreatureStats(mPtr).setKnockedDown(true); + } + else + { + // report acrobatics progression + if (isPlayer) + cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, ESM::Skill::Acrobatics_Fall); + } + } - if (movementSettings.mIsStrafing) - { - if(vec.x() > 0.0f) - movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight) - : (sneak ? CharState_SneakRight - : (isrunning ? CharState_RunRight : CharState_WalkRight))); - else if(vec.x() < 0.0f) - movestate = (inwater ? (isrunning ? CharState_SwimRunLeft : CharState_SwimWalkLeft) - : (sneak ? CharState_SneakLeft - : (isrunning ? CharState_RunLeft : CharState_WalkLeft))); + if (mPtr.getClass().isNpc()) + { + std::string_view sound; + osg::Vec3f pos(mPtr.getRefData().getPosition().asVec3()); + if (world->isUnderwater(mPtr.getCell(), pos) || world->isWalkingOnWater(mPtr)) + sound = "DefaultLandWater"; + else if (onground) + sound = "DefaultLand"; + + if (!sound.empty()) + sndMgr->playSound3D(mPtr, ESM::RefId::stringRefId(sound), 1.f, 1.f, MWSound::Type::Foot, + MWSound::PlayMode::NoPlayerLocal); + } + } + + if (mAnimation->isPlaying(mCurrentJump)) + jumpstate = JumpState_Landing; + + vec.z() = 0.0f; + + if (movementSettings.mIsStrafing) + { + if (vec.x() > 0.0f) + movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight) + : (sneak ? CharState_SneakRight + : (isrunning ? CharState_RunRight : CharState_WalkRight))); + else if (vec.x() < 0.0f) + movestate = (inwater + ? (isrunning ? CharState_SwimRunLeft : CharState_SwimWalkLeft) + : (sneak ? CharState_SneakLeft : (isrunning ? CharState_RunLeft : CharState_WalkLeft))); + } + else if (vec.length2() > 0.0f) + { + if (vec.y() >= 0.0f) + movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward) + : (sneak ? CharState_SneakForward + : (isrunning ? CharState_RunForward : CharState_WalkForward))); + else + movestate = (inwater + ? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack) + : (sneak ? CharState_SneakBack : (isrunning ? CharState_RunBack : CharState_WalkBack))); + } + else + { + // Do not play turning animation for player if rotation speed is very slow. + // Actual threshold should take framerate in account. + float rotationThreshold = (isPlayer ? 0.015f : 0.001f) * 60 * duration; + + // It seems only bipedal actors use turning animations. + // Also do not use turning animations in the first-person view and when sneaking. + if (!sneak && !isFirstPersonPlayer && isBiped) + { + if (effectiveRotation > rotationThreshold) + movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight; + else if (effectiveRotation < -rotationThreshold) + movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft; + } + } } - else if (vec.length2() > 0.0f) + + if (turnToMovementDirection && !isFirstPersonPlayer && isBiped + && (movestate == CharState_SwimRunForward || movestate == CharState_SwimWalkForward + || movestate == CharState_SwimRunBack || movestate == CharState_SwimWalkBack)) { - if (vec.y() >= 0.0f) - movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward) - : (sneak ? CharState_SneakForward - : (isrunning ? CharState_RunForward : CharState_WalkForward))); - else - movestate = (inwater ? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack) - : (sneak ? CharState_SneakBack - : (isrunning ? CharState_RunBack : CharState_WalkBack))); + float swimmingPitch = mAnimation->getBodyPitchRadians(); + float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0]; + float maxSwimPitchDelta = 3.0f * duration; + swimmingPitch += std::clamp(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); + mAnimation->setBodyPitchRadians(swimmingPitch); } else + mAnimation->setBodyPitchRadians(0); + + if (inwater && isPlayer && !isFirstPersonPlayer && Settings::game().mSwimUpwardCorrection) { - // Do not play turning animation for player if rotation speed is very slow. - // Actual threshold should take framerate in account. - float rotationThreshold = (isPlayer ? 0.015f : 0.001f) * 60 * duration; + const float swimUpwardCoef = Settings::game().mSwimUpwardCoef; + vec.z() = std::abs(vec.y()) * swimUpwardCoef; + vec.y() *= std::sqrt(1.0f - swimUpwardCoef * swimUpwardCoef); + } - // It seems only bipedal actors use turning animations. - // Also do not use turning animations in the first-person view and when sneaking. - if (!sneak && jumpstate == JumpState_None && !isFirstPersonPlayer && mPtr.getClass().isBipedal(mPtr)) + // Player can not use smooth turning as NPCs, so we play turning animation a bit to avoid jittering + if (isPlayer) + { + float threshold = mCurrentMovement.find("swim") == std::string::npos ? 0.4f : 0.8f; + float complete; + bool animPlaying = mAnimation->getInfo(mCurrentMovement, &complete); + if (movestate == CharState_None && jumpstate == JumpState_None && isTurning()) { - if(effectiveRotation > rotationThreshold) - movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight; - else if(effectiveRotation < -rotationThreshold) - movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft; + if (animPlaying && complete < threshold) + movestate = mMovementState; } } - } + else + { + if (isBiped) + { + if (mTurnAnimationThreshold > 0) + mTurnAnimationThreshold -= duration; - if (playLandingSound) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - std::string sound; - osg::Vec3f pos(mPtr.getRefData().getPosition().asVec3()); - if (world->isUnderwater(mPtr.getCell(), pos) || world->isWalkingOnWater(mPtr)) - sound = "DefaultLandWater"; - else if (onground) - sound = "DefaultLand"; + if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft + || movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft) + { + mTurnAnimationThreshold = 0.05f; + } + else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0) + { + movestate = mMovementState; + } + } + } - if (!sound.empty()) - sndMgr->playSound3D(mPtr, sound, 1.f, 1.f, MWSound::Type::Foot, MWSound::PlayMode::NoPlayerLocal); - } + if (movestate != CharState_None) + { + clearAnimQueue(); + jumpstate = JumpState_None; + } - if (turnToMovementDirection && !isFirstPersonPlayer && - (movestate == CharState_SwimRunForward || movestate == CharState_SwimWalkForward || - movestate == CharState_SwimRunBack || movestate == CharState_SwimWalkBack)) - { - float swimmingPitch = mAnimation->getBodyPitchRadians(); - float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0]; - float maxSwimPitchDelta = 3.0f * duration; - swimmingPitch += osg::clampBetween(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); - mAnimation->setBodyPitchRadians(swimmingPitch); - } - else - mAnimation->setBodyPitchRadians(0); + updateAnimQueue(); + if (!mAnimQueue.empty()) + idlestate = CharState_SpecialIdle; + else if (sneak && !mInJump) + idlestate = CharState_IdleSneak; + else + idlestate = CharState_Idle; - static const bool swimUpwardCorrection = Settings::Manager::getBool("swim upward correction", "Game"); - if (inwater && isPlayer && !isFirstPersonPlayer && swimUpwardCorrection) - { - static const float swimUpwardCoef = Settings::Manager::getFloat("swim upward coef", "Game"); - static const float swimForwardCoef = sqrtf(1.0f - swimUpwardCoef * swimUpwardCoef); - vec.z() = std::abs(vec.y()) * swimUpwardCoef; - vec.y() *= swimForwardCoef; - } + if (inwater) + idlestate = CharState_IdleSwim; - // Player can not use smooth turning as NPCs, so we play turning animation a bit to avoid jittering - if (isPlayer) - { - float threshold = mCurrentMovement.find("swim") == std::string::npos ? 0.4f : 0.8f; - float complete; - bool animPlaying = mAnimation->getInfo(mCurrentMovement, &complete); - if (movestate == CharState_None && jumpstate == JumpState_None && isTurning()) + if (!mSkipAnim) { - if (animPlaying && complete < threshold) - movestate = mMovementState; + refreshCurrentAnims(idlestate, movestate, jumpstate, updateWeaponState()); + updateIdleStormState(inwater); } - } - else - { - if (mPtr.getClass().isBipedal(mPtr)) + + if (isTurning()) + { + // Adjust animation speed from 1.0 to 1.5 multiplier + if (duration > 0) + { + float turnSpeed = std::min(1.5f, std::abs(rot.z()) / duration / static_cast(osg::PI)); + mAnimation->adjustSpeedMult(mCurrentMovement, std::max(turnSpeed, 1.0f)); + } + } + else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed) { - if (mTurnAnimationThreshold > 0) - mTurnAnimationThreshold -= duration; + // Vanilla caps the played animation speed. + const float maxSpeedMult = 10.f; + const float speedMult = speed / mMovementAnimSpeed; + mAnimation->adjustSpeedMult(mCurrentMovement, std::min(maxSpeedMult, speedMult)); + // Make sure the actual speed is the "expected" speed even though the animation is slower + if (isMovementAnimationControlled()) + scale *= std::max(1.f, speedMult / maxSpeedMult); + } - if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft || - movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft) + if (!mSkipAnim) + { + if (!isKnockedDown() && !isKnockedOut()) { - mTurnAnimationThreshold = 0.05f; + if (rot != osg::Vec3f()) + world->rotateObject(mPtr, rot, true); } - else if (movestate == CharState_None && isTurning() - && mTurnAnimationThreshold > 0) + else // avoid z-rotating for knockdown { - movestate = mMovementState; + if (rot.x() != 0 && rot.y() != 0) + { + rot.z() = 0.0f; + world->rotateObject(mPtr, rot, true); + } } + + updateHeadTracking(duration); } - } - if(movestate != CharState_None && !isTurning()) - clearAnimQueue(); + movement = vec; + movementSettings.mPosition[0] = movementSettings.mPosition[1] = 0; - if(mAnimQueue.empty() || inwater || (sneak && mIdleState != CharState_SpecialIdle)) - { - if (inwater) - idlestate = CharState_IdleSwim; - else if (sneak && !inJump) - idlestate = CharState_IdleSneak; - else - idlestate = CharState_Idle; + // Can't reset jump state (mPosition[2]) here in full; we don't know for sure whether the PhysicsSystem will + // actually handle it in this frame due to the fixed minimum timestep used for the physics update. It will + // be reset in PhysicsSystem::move once the jump is handled. + if (movement.z() == 0.f) + movementSettings.mPosition[2] = 0; } - else - updateAnimQueue(); - - if (!mSkipAnim) + else if (cls.getCreatureStats(mPtr).isDead()) { - // bipedal means hand-to-hand could be used (which is handled in updateWeaponState). an existing InventoryStore means an actual weapon could be used. - if(cls.isBipedal(mPtr) || cls.hasInventoryStore(mPtr)) - forcestateupdate = updateWeaponState(idlestate) || forcestateupdate; - else - forcestateupdate = updateCreatureState() || forcestateupdate; - - refreshCurrentAnims(idlestate, movestate, jumpstate, forcestateupdate); - updateIdleStormState(inwater); + // initial start of death animation for actors that started the game as dead + // not done in constructor since we need to give scripts a chance to set the mSkipAnim flag + if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty()) + { + // Fast-forward death animation to end for persisting corpses or corpses after end of death animation + if (cls.isPersistent(mPtr) || cls.getCreatureStats(mPtr).isDeathAnimationFinished()) + playDeath(1.f, mDeathState); + } } - if (inJump) - mMovementAnimationControlled = false; + osg::Vec3f movementFromAnimation + = mAnimation->runAnimation(mSkipAnim && !isScriptedAnimPlaying() ? 0.f : duration); - if (isTurning()) + if (mPtr.getClass().isActor() && !isScriptedAnimPlaying()) { - // Adjust animation speed from 1.0 to 1.5 multiplier - if (duration > 0) + if (isMovementAnimationControlled()) { - float turnSpeed = std::min(1.5f, std::abs(rot.z()) / duration / static_cast(osg::PI)); - mAnimation->adjustSpeedMult(mCurrentMovement, std::max(turnSpeed, 1.0f)); - } - } - else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed) - { - // Vanilla caps the played animation speed. - const float maxSpeedMult = 10.f; - const float speedMult = speed / mMovementAnimSpeed; - mAnimation->adjustSpeedMult(mCurrentMovement, std::min(maxSpeedMult, speedMult)); - // Make sure the actual speed is the "expected" speed even though the animation is slower - scale *= std::max(1.f, speedMult / maxSpeedMult); - } + if (duration != 0.f && movementFromAnimation != osg::Vec3f()) + { + movementFromAnimation /= duration; + + // Ensure we're moving in the right general direction. + // In vanilla, all horizontal movement is taken from animations, even when moving diagonally (which + // doesn't have a corresponding animation). So to achieve diagonal movement, we have to rotate the + // movement taken from the animation to the intended direction. + // + // Note that while a complete movement animation cycle will have a well defined direction, no + // individual frame will, and therefore we have to determine the direction based on the currently + // playing cycle instead. + if (speed > 0.f) + { + float animMovementAngle = getAnimationMovementDirection(); + float targetMovementAngle = std::atan2(-movement.x(), movement.y()); + float diff = targetMovementAngle - animMovementAngle; + movementFromAnimation = osg::Quat(diff, osg::Vec3f(0, 0, 1)) * movementFromAnimation; + } - if (!mSkipAnim) - { - if(!isKnockedDown() && !isKnockedOut()) + movement = movementFromAnimation; + } + else + { + movement = osg::Vec3f(); + } + } + else if (mSkipAnim) { - if (rot != osg::Vec3f()) - world->rotateObject(mPtr, rot.x(), rot.y(), rot.z(), true); + movement = osg::Vec3f(); } - else //avoid z-rotating for knockdown + + if (mFloatToSurface) { - if (rot.x() != 0 && rot.y() != 0) - world->rotateObject(mPtr, rot.x(), rot.y(), 0.0f, true); + if (cls.getCreatureStats(mPtr).isDead() + || (!godmode + && cls.getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Paralyze) + .getModifier() + > 0)) + { + movement.z() = 1.0; + } } - if (!mMovementAnimationControlled) - world->queueMovement(mPtr, vec); + movement.x() *= scale; + movement.y() *= scale; + world->queueMovement(mPtr, movement); } - movement = vec; - movementSettings.mPosition[0] = movementSettings.mPosition[1] = 0; - if (movement.z() == 0.f) - movementSettings.mPosition[2] = 0; - // Can't reset jump state (mPosition[2]) here in full; we don't know for sure whether the PhysicSystem will actually handle it in this frame - // due to the fixed minimum timestep used for the physics update. It will be reset in PhysicSystem::move once the jump is handled. + mSkipAnim = false; - if (!mSkipAnim) - updateHeadTracking(duration); + mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead()); } - else if(cls.getCreatureStats(mPtr).isDead()) + + void CharacterController::persistAnimationState() const { - // initial start of death animation for actors that started the game as dead - // not done in constructor since we need to give scripts a chance to set the mSkipAnim flag - if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty()) + ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); + + state.mScriptedAnims.clear(); + for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter) { - // Fast-forward death animation to end for persisting corpses or corpses after end of death animation - if (cls.isPersistent(mPtr) || cls.getCreatureStats(mPtr).isDeathAnimationFinished()) - playDeath(1.f, mDeathState); - } - } + // TODO: Probably want to presist lua animations too + if (!iter->mScripted) + continue; - bool isPersist = isPersistentAnimPlaying(); - osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim && !isPersist ? 0.f : duration); - if(duration > 0.0f) - moved /= duration; - else - moved = osg::Vec3f(0.f, 0.f, 0.f); + ESM::AnimationState::ScriptedAnimation anim; + anim.mGroup = iter->mGroup; - moved.x() *= scale; - moved.y() *= scale; + if (iter == mAnimQueue.begin()) + { + float complete; + size_t loopcount; + mAnimation->getInfo(anim.mGroup, &complete, nullptr, &loopcount); + anim.mTime = complete; + anim.mLoopCount = loopcount; + } + else + { + anim.mLoopCount = iter->mLoopCount; + anim.mTime = 0.f; + } - // Ensure we're moving in generally the right direction... - if (speed > 0.f && moved != osg::Vec3f()) - { - float l = moved.length(); - if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2 || - std::abs(movement.y() - moved.y()) > std::abs(moved.y()) / 2 || - std::abs(movement.z() - moved.z()) > std::abs(moved.z()) / 2) - { - moved = movement; - // For some creatures getSpeed doesn't work, so we adjust speed to the animation. - // TODO: Fix Creature::getSpeed. - float newLength = moved.length(); - if (newLength > 0 && !cls.isNpc()) - moved *= (l / newLength); + state.mScriptedAnims.push_back(anim); } } - if (mFloatToSurface && cls.isActor()) + void CharacterController::unpersistAnimationState() { - if (cls.getCreatureStats(mPtr).isDead() - || (!godmode && cls.getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0)) + const ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); + + if (!state.mScriptedAnims.empty()) { - moved.z() = 1.0; + clearAnimQueue(); + for (ESM::AnimationState::ScriptedAnimations::const_iterator iter = state.mScriptedAnims.begin(); + iter != state.mScriptedAnims.end(); ++iter) + { + AnimationQueueEntry entry; + entry.mGroup = iter->mGroup; + entry.mLoopCount + = static_cast(std::min(iter->mLoopCount, std::numeric_limits::max())); + entry.mLooping = mAnimation->isLoopingAnimation(entry.mGroup); + entry.mScripted = true; + entry.mStartKey = "start"; + entry.mStopKey = "stop"; + entry.mSpeed = 1.f; + entry.mTime = iter->mTime; + if (iter->mAbsolute) + { + float start = mAnimation->getTextKeyTime(iter->mGroup + ": start"); + float stop = mAnimation->getTextKeyTime(iter->mGroup + ": stop"); + float time = std::clamp(iter->mTime, start, stop); + entry.mTime = (time - start) / (stop - start); + } + + mAnimQueue.push_back(entry); + } + + playAnimQueue(); } } - // Update movement - if(mMovementAnimationControlled && mPtr.getClass().isActor()) - world->queueMovement(mPtr, moved); + void CharacterController::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) const + { + if (mLuaAnimations) + MWBase::Environment::get().getLuaManager()->playAnimation(mPtr, groupname, priority, blendMask, autodisable, + speedmult, start, stop, startpoint, loops, loopfallback); + else + mAnimation->play( + groupname, priority, blendMask, autodisable, speedmult, start, stop, startpoint, loops, loopfallback); + } - mSkipAnim = false; + bool CharacterController::playGroup(std::string_view groupname, int mode, uint32_t count, bool scripted) + { + if (!mAnimation || !mAnimation->hasAnimation(groupname)) + return false; - mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead()); -} + // We should not interrupt scripted animations with non-scripted ones + if (isScriptedAnimPlaying() && !scripted) + return true; -void CharacterController::persistAnimationState() -{ - ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); + bool looping = mAnimation->isLoopingAnimation(groupname); - state.mScriptedAnims.clear(); - for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter) - { - if (!iter->mPersist) - continue; + // If this animation is a looped animation that is already playing + // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count + // and remove any other animations that were queued. + // This emulates observed behavior from the original allows the script "OutsideBanner" to animate banners + // correctly. + if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname && looping + && mAnimation->isPlaying(groupname)) + { + float endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop stop"); - ESM::AnimationState::ScriptedAnimation anim; - anim.mGroup = iter->mGroup; + if (endOfLoop < 0) // if no Loop Stop key was found, use the Stop key + endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": stop"); - if (iter == mAnimQueue.begin()) - { - anim.mLoopCount = mAnimation->getCurrentLoopCount(anim.mGroup); - float complete; - mAnimation->getInfo(anim.mGroup, &complete, nullptr); - anim.mTime = complete; - } - else - { - anim.mLoopCount = iter->mLoopCount; - anim.mTime = 0.f; + if (endOfLoop > 0 && (mAnimation->getCurrentTime(mAnimQueue.front().mGroup) < endOfLoop)) + { + mAnimQueue.resize(1); + return true; + } } - state.mScriptedAnims.push_back(anim); - } -} + // The loop count in vanilla is weird. + // if played with a count of 0, all objects play exactly once from start to stop. + // But if the count is x > 0, actors and non-actors behave differently. actors will loop + // exactly x times, while non-actors will loop x+1 instead. + if (mPtr.getClass().isActor() && count > 0) + count--; -void CharacterController::unpersistAnimationState() -{ - const ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); + AnimationQueueEntry entry; + entry.mGroup = groupname; + entry.mLoopCount = count; + entry.mTime = 0.f; + // "PlayGroup idle" is a special case, used to remove to stop scripted animations playing + entry.mScripted = (scripted && groupname != "idle"); + entry.mLooping = looping; + entry.mSpeed = 1.f; + entry.mStartKey = ((mode == 2) ? "loop start" : "start"); + entry.mStopKey = "stop"; - if (!state.mScriptedAnims.empty()) - { - clearAnimQueue(); - for (ESM::AnimationState::ScriptedAnimations::const_iterator iter = state.mScriptedAnims.begin(); iter != state.mScriptedAnims.end(); ++iter) + bool playImmediately = false; + + if (mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup)) { - AnimationQueueEntry entry; - entry.mGroup = iter->mGroup; - entry.mLoopCount = iter->mLoopCount; - entry.mPersist = true; + clearAnimQueue(scripted); - mAnimQueue.push_back(entry); + playImmediately = true; } - - const ESM::AnimationState::ScriptedAnimation& anim = state.mScriptedAnims.front(); - float complete = anim.mTime; - if (anim.mAbsolute) + else { - float start = mAnimation->getTextKeyTime(anim.mGroup+": start"); - float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop"); - float time = std::max(start, std::min(stop, anim.mTime)); - complete = (time - start) / (stop - start); + mAnimQueue.resize(1); } - mAnimation->disable(mCurrentIdle); - mCurrentIdle.clear(); - mIdleState = CharState_SpecialIdle; + mAnimQueue.push_back(entry); - bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); - mAnimation->play(anim.mGroup, - Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, - "start", "stop", complete, anim.mLoopCount, loopfallback); - } -} + if (playImmediately) + playAnimQueue(mode == 2); -bool CharacterController::playGroup(const std::string &groupname, int mode, int count, bool persist) -{ - if(!mAnimation || !mAnimation->hasAnimation(groupname)) - return false; - - // We should not interrupt persistent animations by non-persistent ones - if (isPersistentAnimPlaying() && !persist) - return false; + return true; + } - // If this animation is a looped animation (has a "loop start" key) that is already playing - // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count - // and remove any other animations that were queued. - // This emulates observed behavior from the original allows the script "OutsideBanner" to animate banners correctly. - if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname && - mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop start") >= 0 && - mAnimation->isPlaying(groupname)) + bool CharacterController::playGroupLua(std::string_view groupname, float speed, std::string_view startKey, + std::string_view stopKey, uint32_t loops, bool forceLoop) { - float endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": loop stop"); + // Note: In mwscript, "idle" is a special case used to clear the anim queue. + // In lua we offer an explicit clear method instead so this method does not treat "idle" special. - if (endOfLoop < 0) // if no Loop Stop key was found, use the Stop key - endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": stop"); + if (!mAnimation || !mAnimation->hasAnimation(groupname)) + return false; - if (endOfLoop > 0 && (mAnimation->getCurrentTime(mAnimQueue.front().mGroup) < endOfLoop)) - { + AnimationQueueEntry entry; + entry.mGroup = groupname; + // Note: MWScript gives one less loop to actors than non-actors. + // But this is the Lua version. We don't need to reproduce this weirdness here. + entry.mLoopCount = loops; + entry.mStartKey = startKey; + entry.mStopKey = stopKey; + entry.mLooping = mAnimation->isLoopingAnimation(groupname) || forceLoop; + entry.mScripted = true; + entry.mSpeed = speed; + entry.mTime = 0; + + if (mAnimQueue.size() > 1) mAnimQueue.resize(1); - return true; - } - } + mAnimQueue.push_back(entry); - count = std::max(count, 1); + if (mAnimQueue.size() == 1) + playAnimQueue(); - AnimationQueueEntry entry; - entry.mGroup = groupname; - entry.mLoopCount = count-1; - entry.mPersist = persist; + return true; + } - if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup)) + void CharacterController::enableLuaAnimations(bool enable) { - clearAnimQueue(persist); - - mAnimation->disable(mCurrentIdle); - mCurrentIdle.clear(); + mLuaAnimations = enable; + } - mIdleState = CharState_SpecialIdle; - bool loopfallback = (entry.mGroup.compare(0,4,"idle") == 0); - mAnimation->play(groupname, persist && groupname != "idle" ? Priority_Persistent : Priority_Default, - MWRender::Animation::BlendMask_All, false, 1.0f, - ((mode==2) ? "loop start" : "start"), "stop", 0.0f, count-1, loopfallback); + void CharacterController::skipAnim() + { + mSkipAnim = true; } - else + + bool CharacterController::isScriptedAnimPlaying() const { - mAnimQueue.resize(1); + // If the front of the anim queue is scripted, morrowind treats it as if it's + // still playing even if it's actually done. + if (!mAnimQueue.empty()) + return mAnimQueue.front().mScripted; + + return false; } - // "PlayGroup idle" is a special case, used to remove to stop scripted animations playing - if (groupname == "idle") - entry.mPersist = false; + bool CharacterController::isAnimPlaying(std::string_view groupName) const + { + if (mAnimation == nullptr) + return false; + return mAnimation->isPlaying(groupName); + } - mAnimQueue.push_back(entry); + bool CharacterController::isMovementAnimationControlled() const + { + if (mHitState != CharState_None) + return true; - return true; -} + if (Settings::game().mPlayerMovementIgnoresAnimation && mPtr == getPlayer()) + return false; -void CharacterController::skipAnim() -{ - mSkipAnim = true; -} + if (mInJump) + return false; -bool CharacterController::isPersistentAnimPlaying() -{ - if (!mAnimQueue.empty()) - { - AnimationQueueEntry& first = mAnimQueue.front(); - return first.mPersist && isAnimPlaying(first.mGroup); + bool movementAnimationControlled = mIdleState != CharState_None; + if (mMovementState != CharState_None) + movementAnimationControlled = mMovementAnimationHasMovement; + return movementAnimationControlled; } - return false; -} + void CharacterController::clearAnimQueue(bool clearScriptedAnims) + { + // Do not interrupt scripted animations, if we want to keep them + if ((!isScriptedAnimPlaying() || clearScriptedAnims) && !mAnimQueue.empty()) + mAnimation->disable(mAnimQueue.front().mGroup); -bool CharacterController::isAnimPlaying(const std::string &groupName) -{ - if(mAnimation == nullptr) - return false; - return mAnimation->isPlaying(groupName); -} + if (clearScriptedAnims) + { + mAnimation->setPlayScriptedOnly(false); + mAnimQueue.clear(); + return; + } -void CharacterController::clearAnimQueue(bool clearPersistAnims) -{ - // Do not interrupt scripted animations, if we want to keep them - if ((!isPersistentAnimPlaying() || clearPersistAnims) && !mAnimQueue.empty()) - mAnimation->disable(mAnimQueue.front().mGroup); + for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) + { + if (!it->mScripted) + it = mAnimQueue.erase(it); + else + ++it; + } + } - for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) + void CharacterController::forceStateUpdate() { - if (clearPersistAnims || !it->mPersist) - it = mAnimQueue.erase(it); - else - ++it; - } -} + if (!mAnimation) + return; + clearAnimQueue(); -void CharacterController::forceStateUpdate() -{ - if(!mAnimation) - return; - clearAnimQueue(); + // Make sure we canceled the current attack or spellcasting, + // because we disabled attack animations anyway. + mCanCast = false; + mCastingScriptedSpell = false; + setAttackingOrSpell(false); + if (mUpperBodyState != UpperBodyState::None) + mUpperBodyState = UpperBodyState::WeaponEquipped; + + refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); - // Make sure we canceled the current attack or spellcasting, - // because we disabled attack animations anyway. - mCastingManualSpell = false; - mAttackingOrSpell = false; - if (mUpperBodyState != UpperCharState_Nothing) - mUpperBodyState = UpperCharState_WeapEquiped; + if (mDeathState != CharState_None) + { + playRandomDeath(); + } - refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); + updateAnimQueue(); - if(mDeathState != CharState_None) - { - playRandomDeath(); + mAnimation->runAnimation(0.f); } - mAnimation->runAnimation(0.f); -} - -CharacterController::KillResult CharacterController::kill() -{ - if (mDeathState == CharState_None) + CharacterController::KillResult CharacterController::kill() { - playRandomDeath(); - - mAnimation->disable(mCurrentIdle); + if (mDeathState == CharState_None) + { + playRandomDeath(); + resetCurrentIdleState(); + return Result_DeathAnimStarted; + } - mIdleState = CharState_None; - mCurrentIdle.clear(); - return Result_DeathAnimStarted; + MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr); + if (isAnimPlaying(mCurrentDeath)) + return Result_DeathAnimPlaying; + if (!cStats.isDeathAnimationFinished()) + { + cStats.setDeathAnimationFinished(true); + return Result_DeathAnimJustFinished; + } + return Result_DeathAnimFinished; } - MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr); - if (isAnimPlaying(mCurrentDeath)) - return Result_DeathAnimPlaying; - if (!cStats.isDeathAnimationFinished()) + void CharacterController::resurrect() { - cStats.setDeathAnimationFinished(true); - return Result_DeathAnimJustFinished; + if (mDeathState == CharState_None) + return; + + resetCurrentDeathState(); + mWeaponType = ESM::Weapon::None; } - return Result_DeathAnimFinished; -} -void CharacterController::resurrect() -{ - if(mDeathState == CharState_None) - return; - - if(mAnimation) - mAnimation->disable(mCurrentDeath); - mCurrentDeath.clear(); - mDeathState = CharState_None; - mWeaponType = ESM::Weapon::None; -} + void CharacterController::updateContinuousVfx() const + { + // Keeping track of when to stop a continuous VFX seems to be very difficult to do inside the spells code, + // as it's extremely spread out (ActiveSpells, Spells, InventoryStore effects, etc...) so we do it here. -void CharacterController::updateContinuousVfx() -{ - // Keeping track of when to stop a continuous VFX seems to be very difficult to do inside the spells code, - // as it's extremely spread out (ActiveSpells, Spells, InventoryStore effects, etc...) so we do it here. + // Stop any effects that are no longer active + std::vector effects = mAnimation->getLoopingEffects(); - // Stop any effects that are no longer active - std::vector effects; - mAnimation->getLoopingEffects(effects); + for (std::string_view effectId : effects) + { + auto index = ESM::MagicEffect::indexNameToIndex(effectId); - for (int effectId : effects) - { - if (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished() - || mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(MWMechanics::EffectKey(effectId)).getMagnitude() <= 0) - mAnimation->removeEffect(effectId); + if (index >= 0 + && (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished() + || mPtr.getClass() + .getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(MWMechanics::EffectKey(index)) + .getMagnitude() + <= 0)) + mAnimation->removeEffect(effectId); + } } -} -void CharacterController::updateMagicEffects() -{ - if (!mPtr.getClass().isActor()) - return; + void CharacterController::updateMagicEffects() const + { + if (!mPtr.getClass().isActor()) + return; - float light = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Light).getMagnitude(); - mAnimation->setLightEffect(light); + float light = mPtr.getClass() + .getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Light) + .getMagnitude(); + mAnimation->setLightEffect(light); - // If you're dead you don't care about whether you've started/stopped being a vampire or not - if (mPtr.getClass().getCreatureStats(mPtr).isDead()) - return; + // If you're dead you don't care about whether you've started/stopped being a vampire or not + if (mPtr.getClass().getCreatureStats(mPtr).isDead()) + return; - bool vampire = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0.0f; - mAnimation->setVampire(vampire); -} + bool vampire = mPtr.getClass() + .getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Vampirism) + .getMagnitude() + > 0.0f; + mAnimation->setVampire(vampire); + } -void CharacterController::setVisibility(float visibility) -{ - // We should take actor's invisibility in account - if (mPtr.getClass().isActor()) + void CharacterController::setVisibility(float visibility) const { - float alpha = 1.f; - if (mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Invisibility).getModifier()) // Ignore base magnitude (see bug #3555). - { - if (mPtr == getPlayer()) - alpha = 0.25f; - else - alpha = 0.05f; - } - float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); - if (chameleon) + // We should take actor's invisibility in account + if (mPtr.getClass().isActor()) { - alpha *= std::min(0.75f, std::max(0.25f, (100.f - chameleon)/100.f)); + float alpha = 1.f; + if (mPtr.getClass() + .getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Invisibility) + .getModifier()) // Ignore base magnitude (see bug #3555). + { + if (mPtr == getPlayer()) + alpha = 0.25f; + else + alpha = 0.05f; + } + float chameleon = mPtr.getClass() + .getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Chameleon) + .getMagnitude(); + if (chameleon) + { + alpha *= std::clamp(1.f - chameleon / 100.f, 0.25f, 0.75f); + } + + visibility = std::min(visibility, alpha); } - visibility = std::min(visibility, alpha); + // TODO: implement a dithering shader rather than just change object transparency. + mAnimation->setAlpha(visibility); } - // TODO: implement a dithering shader rather than just change object transparency. - mAnimation->setAlpha(visibility); -} - -void CharacterController::setAttackTypeBasedOnMovement() -{ - float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition; - if (std::abs(move[1]) > std::abs(move[0]) + 0.2f) // forward-backward - mAttackType = "thrust"; - else if (std::abs(move[0]) > std::abs(move[1]) + 0.2f) // sideway - mAttackType = "slash"; - else - mAttackType = "chop"; -} - -bool CharacterController::isRandomAttackAnimation(const std::string& group) const -{ - return (group == "attack1" || group == "swimattack1" || - group == "attack2" || group == "swimattack2" || - group == "attack3" || group == "swimattack3"); -} + std::string_view CharacterController::getMovementBasedAttackType() const + { + float* move = mPtr.getClass().getMovementSettings(mPtr).mPosition; + if (std::abs(move[1]) > std::abs(move[0]) + 0.2f) // forward-backward + return "thrust"; + if (std::abs(move[0]) > std::abs(move[1]) + 0.2f) // sideway + return "slash"; + return "chop"; + } -bool CharacterController::isAttackPreparing() const -{ - return mUpperBodyState == UpperCharState_StartToMinAttack || - mUpperBodyState == UpperCharState_MinAttackToMaxAttack; -} + bool CharacterController::isRandomAttackAnimation(std::string_view group) + { + return (group == "attack1" || group == "swimattack1" || group == "attack2" || group == "swimattack2" + || group == "attack3" || group == "swimattack3"); + } -bool CharacterController::isCastingSpell() const -{ - return mCastingManualSpell || mUpperBodyState == UpperCharState_CastingSpell; -} + bool CharacterController::isAttackPreparing() const + { + return mUpperBodyState == UpperBodyState::AttackWindUp; + } -bool CharacterController::isReadyToBlock() const -{ - return updateCarriedLeftVisible(mWeaponType); -} + bool CharacterController::isCastingSpell() const + { + return mCastingScriptedSpell || mUpperBodyState == UpperBodyState::Casting; + } -bool CharacterController::isKnockedDown() const -{ - return mHitState == CharState_KnockDown || - mHitState == CharState_SwimKnockDown; -} + bool CharacterController::isReadyToBlock() const + { + return updateCarriedLeftVisible(mWeaponType); + } -bool CharacterController::isKnockedOut() const -{ - return mHitState == CharState_KnockOut || - mHitState == CharState_SwimKnockOut; -} + bool CharacterController::isKnockedDown() const + { + return mHitState == CharState_KnockDown || mHitState == CharState_SwimKnockDown; + } -bool CharacterController::isTurning() const -{ - return mMovementState == CharState_TurnLeft || - mMovementState == CharState_TurnRight || - mMovementState == CharState_SwimTurnLeft || - mMovementState == CharState_SwimTurnRight; -} + bool CharacterController::isKnockedOut() const + { + return mHitState == CharState_KnockOut || mHitState == CharState_SwimKnockOut; + } -bool CharacterController::isRecovery() const -{ - return mHitState == CharState_Hit || - mHitState == CharState_SwimHit; -} + bool CharacterController::isTurning() const + { + return mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight + || mMovementState == CharState_SwimTurnLeft || mMovementState == CharState_SwimTurnRight; + } -bool CharacterController::isAttackingOrSpell() const -{ - return mUpperBodyState != UpperCharState_Nothing && - mUpperBodyState != UpperCharState_WeapEquiped; -} + bool CharacterController::isRecovery() const + { + return mHitState == CharState_Hit || mHitState == CharState_SwimHit; + } -bool CharacterController::isSneaking() const -{ - return mIdleState == CharState_IdleSneak || - mMovementState == CharState_SneakForward || - mMovementState == CharState_SneakBack || - mMovementState == CharState_SneakLeft || - mMovementState == CharState_SneakRight; -} + bool CharacterController::isAttackingOrSpell() const + { + return mUpperBodyState != UpperBodyState::None && mUpperBodyState != UpperBodyState::WeaponEquipped; + } -bool CharacterController::isRunning() const -{ - return mMovementState == CharState_RunForward || - mMovementState == CharState_RunBack || - mMovementState == CharState_RunLeft || - mMovementState == CharState_RunRight || - mMovementState == CharState_SwimRunForward || - mMovementState == CharState_SwimRunBack || - mMovementState == CharState_SwimRunLeft || - mMovementState == CharState_SwimRunRight; -} + bool CharacterController::isSneaking() const + { + return mIdleState == CharState_IdleSneak || mMovementState == CharState_SneakForward + || mMovementState == CharState_SneakBack || mMovementState == CharState_SneakLeft + || mMovementState == CharState_SneakRight; + } -void CharacterController::setAttackingOrSpell(bool attackingOrSpell) -{ - mAttackingOrSpell = attackingOrSpell; -} + bool CharacterController::isRunning() const + { + return mMovementState == CharState_RunForward || mMovementState == CharState_RunBack + || mMovementState == CharState_RunLeft || mMovementState == CharState_RunRight + || mMovementState == CharState_SwimRunForward || mMovementState == CharState_SwimRunBack + || mMovementState == CharState_SwimRunLeft || mMovementState == CharState_SwimRunRight; + } -void CharacterController::castSpell(const std::string spellId, bool manualSpell) -{ - mAttackingOrSpell = true; - mCastingManualSpell = manualSpell; - ActionSpell action = ActionSpell(spellId); - action.prepare(mPtr); -} + void CharacterController::setAttackingOrSpell(bool attackingOrSpell) const + { + mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(attackingOrSpell); + } -void CharacterController::setAIAttackType(const std::string& attackType) -{ - mAttackType = attackType; -} + void CharacterController::castSpell(const ESM::RefId& spellId, bool scriptedSpell) + { + setAttackingOrSpell(true); + mCastingScriptedSpell = scriptedSpell; + ActionSpell action = ActionSpell(spellId); + action.prepare(mPtr); + } -void CharacterController::setAttackTypeRandomly(std::string& attackType) -{ - float random = Misc::Rng::rollProbability(); - if (random >= 2/3.f) - attackType = "thrust"; - else if (random >= 1/3.f) - attackType = "slash"; - else - attackType = "chop"; -} + void CharacterController::setAIAttackType(std::string_view attackType) + { + mAttackType = attackType; + } -bool CharacterController::readyToPrepareAttack() const -{ - return (mHitState == CharState_None || mHitState == CharState_Block) - && mUpperBodyState <= UpperCharState_WeapEquiped; -} + std::string_view CharacterController::getRandomAttackType() + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + float random = Misc::Rng::rollProbability(world->getPrng()); + if (random >= 2 / 3.f) + return "thrust"; + if (random >= 1 / 3.f) + return "slash"; + return "chop"; + } -bool CharacterController::readyToStartAttack() const -{ - if (mHitState != CharState_None && mHitState != CharState_Block) - return false; + bool CharacterController::readyToPrepareAttack() const + { + return (mHitState == CharState_None || mHitState == CharState_Block) + && mUpperBodyState <= UpperBodyState::WeaponEquipped; + } - if (mPtr.getClass().hasInventoryStore(mPtr) || mPtr.getClass().isBipedal(mPtr)) - return mUpperBodyState == UpperCharState_WeapEquiped; - else - return mUpperBodyState == UpperCharState_Nothing; -} + bool CharacterController::readyToStartAttack() const + { + if (mHitState != CharState_None && mHitState != CharState_Block) + return false; -float CharacterController::getAttackStrength() const -{ - return mAttackStrength; -} + return mUpperBodyState == UpperBodyState::WeaponEquipped; + } -void CharacterController::setActive(int active) -{ - mAnimation->setActive(active); -} + float CharacterController::getAttackStrength() const + { + return mAttackStrength; + } -void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr &target) -{ - mHeadTrackTarget = target; -} + bool CharacterController::getAttackingOrSpell() const + { + return mPtr.getClass().getCreatureStats(mPtr).getAttackingOrSpell(); + } -void CharacterController::playSwishSound(float attackStrength) -{ - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - - std::string sound = "Weapon Swish"; - if(attackStrength < 0.5f) - sndMgr->playSound3D(mPtr, sound, 1.0f, 0.8f); //Weak attack - else if(attackStrength < 1.0f) - sndMgr->playSound3D(mPtr, sound, 1.0f, 1.0f); //Medium attack - else - sndMgr->playSound3D(mPtr, sound, 1.0f, 1.2f); //Strong attack -} + std::string_view CharacterController::getDesiredAttackType() const + { + return mPtr.getClass().getCreatureStats(mPtr).getAttackType(); + } -void CharacterController::updateHeadTracking(float duration) -{ - const osg::Node* head = mAnimation->getNode("Bip01 Head"); - if (!head) - return; + void CharacterController::setActive(int active) const + { + mAnimation->setActive(active); + } - double zAngleRadians = 0.f; - double xAngleRadians = 0.f; + void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr& target) + { + mHeadTrackTarget = target; + } - if (!mHeadTrackTarget.isEmpty()) + void CharacterController::playSwishSound() const { - osg::NodePathList nodepaths = head->getParentalNodePaths(); - if (nodepaths.empty()) - return; - osg::Matrixf mat = osg::computeLocalToWorld(nodepaths[0]); - osg::Vec3f headPos = mat.getTrans(); + static ESM::RefId weaponSwish = ESM::RefId::stringRefId("Weapon Swish"); + const ESM::RefId* soundId = &weaponSwish; + float volume = 0.98f + mAttackStrength * 0.02f; + float pitch = 0.75f + mAttackStrength * 0.4f; - osg::Vec3f direction; - if (const MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mHeadTrackTarget)) + const MWWorld::Class& cls = mPtr.getClass(); + if (cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf()) { - const osg::Node* node = anim->getNode("Head"); - if (node == nullptr) - node = anim->getNode("Bip01 Head"); - if (node != nullptr) - { - nodepaths = node->getParentalNodePaths(); - if (!nodepaths.empty()) - direction = osg::computeLocalToWorld(nodepaths[0]).getTrans() - headPos; - } - else - // no head node to look at, fall back to look at center of collision box - direction = MWBase::Environment::get().getWorld()->aimToTarget(mPtr, mHeadTrackTarget, false); + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::ESMStore& store = world->getStore(); + const ESM::Sound* sound = store.get().searchRandom("WolfSwing", world->getPrng()); + if (sound) + soundId = &sound->mId; } - direction.normalize(); - if (!mPtr.getRefData().getBaseNode()) - return; - const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); + if (!soundId->empty()) + MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, *soundId, volume, pitch); + } - zAngleRadians = std::atan2(actorDirection.x(), actorDirection.y()) - std::atan2(direction.x(), direction.y()); - zAngleRadians = Misc::normalizeAngle(zAngleRadians - mAnimation->getHeadYaw()) + mAnimation->getHeadYaw(); - zAngleRadians *= (1 - direction.z() * direction.z()); - xAngleRadians = std::asin(direction.z()); + float CharacterController::getAnimationMovementDirection() const + { + switch (mMovementState) + { + case CharState_RunLeft: + case CharState_SneakLeft: + case CharState_SwimWalkLeft: + case CharState_SwimRunLeft: + case CharState_WalkLeft: + return osg::PI_2f; + case CharState_RunRight: + case CharState_SneakRight: + case CharState_SwimWalkRight: + case CharState_SwimRunRight: + case CharState_WalkRight: + return -osg::PI_2f; + case CharState_RunForward: + case CharState_SneakForward: + case CharState_SwimRunForward: + case CharState_SwimWalkForward: + case CharState_WalkForward: + return mAnimation->getLegsYawRadians(); + case CharState_RunBack: + case CharState_SneakBack: + case CharState_SwimWalkBack: + case CharState_SwimRunBack: + case CharState_WalkBack: + return mAnimation->getLegsYawRadians() - osg::PIf; + default: + return 0.0f; + } } - const double xLimit = osg::DegreesToRadians(40.0); - const double zLimit = osg::DegreesToRadians(30.0); - double zLimitOffset = mAnimation->getUpperBodyYawRadians(); - xAngleRadians = osg::clampBetween(xAngleRadians, -xLimit, xLimit); - zAngleRadians = osg::clampBetween(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset); + void CharacterController::updateHeadTracking(float duration) + { + const osg::Node* head = mAnimation->getNode("Bip01 Head"); + if (!head) + return; - float factor = duration*5; - factor = std::min(factor, 1.f); - xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * xAngleRadians; - zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * zAngleRadians; + double zAngleRadians = 0.f; + double xAngleRadians = 0.f; - mAnimation->setHeadPitch(xAngleRadians); - mAnimation->setHeadYaw(zAngleRadians); -} + if (!mHeadTrackTarget.isEmpty()) + { + osg::NodePathList nodepaths = head->getParentalNodePaths(); + if (nodepaths.empty()) + return; + osg::Matrixf mat = osg::computeLocalToWorld(nodepaths[0]); + osg::Vec3f headPos = mat.getTrans(); + + osg::Vec3f direction; + if (const MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mHeadTrackTarget)) + { + const osg::Node* node = anim->getNode("Head"); + if (node == nullptr) + node = anim->getNode("Bip01 Head"); + if (node != nullptr) + { + nodepaths = node->getParentalNodePaths(); + if (!nodepaths.empty()) + direction = osg::computeLocalToWorld(nodepaths[0]).getTrans() - headPos; + } + else + // no head node to look at, fall back to look at center of collision box + direction = MWBase::Environment::get().getWorld()->aimToTarget(mPtr, mHeadTrackTarget, false); + } + direction.normalize(); + + if (!mPtr.getRefData().getBaseNode()) + return; + const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0, 1, 0); + + zAngleRadians + = std::atan2(actorDirection.x(), actorDirection.y()) - std::atan2(direction.x(), direction.y()); + zAngleRadians = Misc::normalizeAngle(zAngleRadians - mAnimation->getHeadYaw()) + mAnimation->getHeadYaw(); + zAngleRadians *= (1 - direction.z() * direction.z()); + xAngleRadians = std::asin(direction.z()); + } + + const double xLimit = osg::DegreesToRadians(40.0); + const double zLimit = osg::DegreesToRadians(30.0); + double zLimitOffset = mAnimation->getUpperBodyYawRadians(); + xAngleRadians = std::clamp(xAngleRadians, -xLimit, xLimit); + zAngleRadians = std::clamp(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset); + + float factor = duration * 5; + factor = std::min(factor, 1.f); + xAngleRadians = (1.f - factor) * mAnimation->getHeadPitch() + factor * xAngleRadians; + zAngleRadians = (1.f - factor) * mAnimation->getHeadYaw() + factor * zAngleRadians; + + mAnimation->setHeadPitch(xAngleRadians); + mAnimation->setHeadYaw(zAngleRadians); + } + + MWWorld::MovementDirectionFlags CharacterController::getSupportedMovementDirections() const + { + using namespace std::string_view_literals; + // There are fallbacks in the CharacterController::refreshMovementAnims for certain animations. Arrays below + // represent them. + constexpr std::array all = { ""sv }; + constexpr std::array walk = { "walk"sv }; + constexpr std::array swimWalk = { "swimwalk"sv, "walk"sv }; + constexpr std::array sneak = { "sneak"sv }; + constexpr std::array run = { "run"sv, "walk"sv }; + constexpr std::array swimRun = { "swimrun"sv, "run"sv, "walk"sv }; + constexpr std::array swim = { "swim"sv }; + switch (mMovementState) + { + case CharState_None: + case CharState_SpecialIdle: + case CharState_Idle: + case CharState_IdleSwim: + case CharState_IdleSneak: + return mAnimation->getSupportedMovementDirections(all); + case CharState_WalkForward: + case CharState_WalkBack: + case CharState_WalkLeft: + case CharState_WalkRight: + return mAnimation->getSupportedMovementDirections(walk); + case CharState_SwimWalkForward: + case CharState_SwimWalkBack: + case CharState_SwimWalkLeft: + case CharState_SwimWalkRight: + return mAnimation->getSupportedMovementDirections(swimWalk); + case CharState_RunForward: + case CharState_RunBack: + case CharState_RunLeft: + case CharState_RunRight: + return mAnimation->getSupportedMovementDirections(run); + case CharState_SwimRunForward: + case CharState_SwimRunBack: + case CharState_SwimRunLeft: + case CharState_SwimRunRight: + return mAnimation->getSupportedMovementDirections(swimRun); + case CharState_SneakForward: + case CharState_SneakBack: + case CharState_SneakLeft: + case CharState_SneakRight: + return mAnimation->getSupportedMovementDirections(sneak); + case CharState_TurnLeft: + case CharState_TurnRight: + return mAnimation->getSupportedMovementDirections(all); + case CharState_SwimTurnLeft: + case CharState_SwimTurnRight: + return mAnimation->getSupportedMovementDirections(swim); + case CharState_Death1: + case CharState_Death2: + case CharState_Death3: + case CharState_Death4: + case CharState_Death5: + case CharState_SwimDeath: + case CharState_SwimDeathKnockDown: + case CharState_SwimDeathKnockOut: + case CharState_DeathKnockDown: + case CharState_DeathKnockOut: + case CharState_Hit: + case CharState_SwimHit: + case CharState_KnockDown: + case CharState_KnockOut: + case CharState_SwimKnockDown: + case CharState_SwimKnockOut: + case CharState_Block: + return mAnimation->getSupportedMovementDirections(all); + } + return 0; + } } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 3f7fa4e1bd0..f043419a818 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -3,13 +3,12 @@ #include +#include + #include "../mwworld/ptr.hpp" -#include "../mwworld/containerstore.hpp" #include "../mwrender/animation.hpp" -#include "weapontype.hpp" - namespace MWWorld { class InventoryStore; @@ -23,286 +22,321 @@ namespace MWRender namespace MWMechanics { -struct Movement; -class CreatureStats; - -enum Priority { - Priority_Default, - Priority_WeaponLowerBody, - Priority_SneakIdleLowerBody, - Priority_SwimIdle, - Priority_Jump, - Priority_Movement, - Priority_Hit, - Priority_Weapon, - Priority_Block, - Priority_Knockdown, - Priority_Torch, - Priority_Storm, - Priority_Death, - Priority_Persistent, - - Num_Priorities -}; - -enum CharacterState { - CharState_None, - - CharState_SpecialIdle, - CharState_Idle, - CharState_Idle2, - CharState_Idle3, - CharState_Idle4, - CharState_Idle5, - CharState_Idle6, - CharState_Idle7, - CharState_Idle8, - CharState_Idle9, - CharState_IdleSwim, - CharState_IdleSneak, - - CharState_WalkForward, - CharState_WalkBack, - CharState_WalkLeft, - CharState_WalkRight, - - CharState_SwimWalkForward, - CharState_SwimWalkBack, - CharState_SwimWalkLeft, - CharState_SwimWalkRight, - - CharState_RunForward, - CharState_RunBack, - CharState_RunLeft, - CharState_RunRight, - - CharState_SwimRunForward, - CharState_SwimRunBack, - CharState_SwimRunLeft, - CharState_SwimRunRight, - - CharState_SneakForward, - CharState_SneakBack, - CharState_SneakLeft, - CharState_SneakRight, - - CharState_TurnLeft, - CharState_TurnRight, - CharState_SwimTurnLeft, - CharState_SwimTurnRight, - - CharState_Jump, - - CharState_Death1, - CharState_Death2, - CharState_Death3, - CharState_Death4, - CharState_Death5, - CharState_SwimDeath, - CharState_SwimDeathKnockDown, - CharState_SwimDeathKnockOut, - CharState_DeathKnockDown, - CharState_DeathKnockOut, - - CharState_Hit, - CharState_SwimHit, - CharState_KnockDown, - CharState_KnockOut, - CharState_SwimKnockDown, - CharState_SwimKnockOut, - CharState_Block -}; - -enum UpperBodyCharacterState { - UpperCharState_Nothing, - UpperCharState_EquipingWeap, - UpperCharState_UnEquipingWeap, - UpperCharState_WeapEquiped, - UpperCharState_StartToMinAttack, - UpperCharState_MinAttackToMaxAttack, - UpperCharState_MaxAttackToMinHit, - UpperCharState_MinHitToHit, - UpperCharState_FollowStartToFollowStop, - UpperCharState_CastingSpell -}; - -enum JumpingState { - JumpState_None, - JumpState_InAir, - JumpState_Landing -}; - -struct WeaponInfo; - -class CharacterController : public MWRender::Animation::TextKeyListener -{ - MWWorld::Ptr mPtr; - MWWorld::Ptr mWeapon; - MWRender::Animation *mAnimation; - - struct AnimationQueueEntry + struct Movement; + class CreatureStats; + + enum Priority + { + Priority_Default, + Priority_WeaponLowerBody, + Priority_SneakIdleLowerBody, + Priority_SwimIdle, + Priority_Jump, + Priority_Movement, + Priority_Hit, + Priority_Weapon, + Priority_Block, + Priority_Knockdown, + Priority_Torch, + Priority_Storm, + Priority_Death, + Priority_Scripted, + + Num_Priorities + }; + + enum CharacterState + { + CharState_None, + + CharState_SpecialIdle, + CharState_Idle, + CharState_IdleSwim, + CharState_IdleSneak, + + CharState_WalkForward, + CharState_WalkBack, + CharState_WalkLeft, + CharState_WalkRight, + + CharState_SwimWalkForward, + CharState_SwimWalkBack, + CharState_SwimWalkLeft, + CharState_SwimWalkRight, + + CharState_RunForward, + CharState_RunBack, + CharState_RunLeft, + CharState_RunRight, + + CharState_SwimRunForward, + CharState_SwimRunBack, + CharState_SwimRunLeft, + CharState_SwimRunRight, + + CharState_SneakForward, + CharState_SneakBack, + CharState_SneakLeft, + CharState_SneakRight, + + CharState_TurnLeft, + CharState_TurnRight, + CharState_SwimTurnLeft, + CharState_SwimTurnRight, + + CharState_Death1, + CharState_Death2, + CharState_Death3, + CharState_Death4, + CharState_Death5, + CharState_SwimDeath, + CharState_SwimDeathKnockDown, + CharState_SwimDeathKnockOut, + CharState_DeathKnockDown, + CharState_DeathKnockOut, + + CharState_Hit, + CharState_SwimHit, + CharState_KnockDown, + CharState_KnockOut, + CharState_SwimKnockDown, + CharState_SwimKnockOut, + CharState_Block + }; + + enum class UpperBodyState { - std::string mGroup; - size_t mLoopCount; - bool mPersist; + None, + Equipping, + Unequipping, + WeaponEquipped, + AttackWindUp, + AttackRelease, + AttackEnd, + Casting }; - typedef std::deque AnimationQueue; - AnimationQueue mAnimQueue; - CharacterState mIdleState; - std::string mCurrentIdle; + enum JumpingState + { + JumpState_None, + JumpState_InAir, + JumpState_Landing + }; - CharacterState mMovementState; - std::string mCurrentMovement; - float mMovementAnimSpeed; - bool mAdjustMovementAnimSpeed; - bool mHasMovedInXY; - bool mMovementAnimationControlled; + struct WeaponInfo; - CharacterState mDeathState; - std::string mCurrentDeath; - bool mFloatToSurface; + class CharacterController : public MWRender::Animation::TextKeyListener + { + MWWorld::Ptr mPtr; + MWWorld::Ptr mWeapon; + MWRender::Animation* mAnimation; - CharacterState mHitState; - std::string mCurrentHit; + struct AnimationQueueEntry + { + std::string mGroup; + uint32_t mLoopCount; + float mTime; + bool mLooping; + bool mScripted; + std::string mStartKey; + std::string mStopKey; + float mSpeed; + }; + typedef std::deque AnimationQueue; + AnimationQueue mAnimQueue; + bool mLuaAnimations{ false }; - UpperBodyCharacterState mUpperBodyState; + CharacterState mIdleState{ CharState_None }; + std::string mCurrentIdle; - JumpingState mJumpState; - std::string mCurrentJump; + CharacterState mMovementState{ CharState_None }; + std::string mCurrentMovement; + float mMovementAnimSpeed{ 0.f }; + bool mAdjustMovementAnimSpeed{ false }; + bool mMovementAnimationHasMovement{ false }; - int mWeaponType; - std::string mCurrentWeapon; + CharacterState mDeathState{ CharState_None }; + std::string mCurrentDeath; + bool mFloatToSurface{ true }; - float mAttackStrength; + CharacterState mHitState{ CharState_None }; + std::string mCurrentHit; - bool mSkipAnim; + UpperBodyState mUpperBodyState{ UpperBodyState::None }; - // counted for skill increase - float mSecondsOfSwimming; - float mSecondsOfRunning; + JumpingState mJumpState{ JumpState_None }; + std::string mCurrentJump; + bool mInJump{ false }; - MWWorld::ConstPtr mHeadTrackTarget; + int mWeaponType{ ESM::Weapon::None }; + std::string mCurrentWeapon; - float mTurnAnimationThreshold; // how long to continue playing turning animation after actor stopped turning + float mAttackStrength{ -1.f }; + MWWorld::Ptr mAttackVictim; + osg::Vec3f mAttackHitPos; + bool mAttackSuccess{ false }; - std::string mAttackType; // slash, chop or thrust + bool mSkipAnim{ false }; - bool mAttackingOrSpell; - bool mCastingManualSpell; + // counted for skill increase + float mSecondsOfSwimming{ 0.f }; + float mSecondsOfRunning{ 0.f }; - float mTimeUntilWake; + MWWorld::ConstPtr mHeadTrackTarget; - bool mIsMovingBackward; - osg::Vec2f mSmoothedSpeed; + float mTurnAnimationThreshold{ + 0.f + }; // how long to continue playing turning animation after actor stopped turning - void setAttackTypeBasedOnMovement(); + std::string mAttackType; // slash, chop or thrust - void refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force=false); - void refreshHitRecoilAnims(CharacterState& idle); - void refreshJumpAnims(const std::string& weapShortGroup, JumpingState jump, CharacterState& idle, bool force=false); - void refreshMovementAnims(const std::string& weapShortGroup, CharacterState movement, CharacterState& idle, bool force=false); - void refreshIdleAnims(const std::string& weapShortGroup, CharacterState idle, bool force=false); + bool mCanCast{ false }; - void clearAnimQueue(bool clearPersistAnims = false); + bool mCastingScriptedSpell{ false }; - bool updateWeaponState(CharacterState& idle); - bool updateCreatureState(); - void updateIdleStormState(bool inwater); + bool mIsMovingBackward{ false }; + osg::Vec2f mSmoothedSpeed; - std::string chooseRandomAttackAnimation() const; - bool isRandomAttackAnimation(const std::string& group) const; + std::string_view getMovementBasedAttackType() const; - bool isPersistentAnimPlaying(); + void clearStateAnimation(std::string& anim) const; + void resetCurrentJumpState(); + void resetCurrentMovementState(); + void resetCurrentIdleState(); + void resetCurrentHitState(); + void resetCurrentWeaponState(); + void resetCurrentDeathState(); - void updateAnimQueue(); + void refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force = false); + void refreshHitRecoilAnims(); + void refreshJumpAnims(JumpingState jump, bool force = false); + void refreshMovementAnims(CharacterState movement, bool force = false); + void refreshIdleAnims(CharacterState idle, bool force = false); - void updateHeadTracking(float duration); + bool updateWeaponState(); + void updateIdleStormState(bool inwater) const; - void updateMagicEffects(); + std::string chooseRandomAttackAnimation() const; + static bool isRandomAttackAnimation(std::string_view group); - void playDeath(float startpoint, CharacterState death); - CharacterState chooseRandomDeathState() const; - void playRandomDeath(float startpoint = 0.0f); + bool isMovementAnimationControlled() const; - /// choose a random animation group with \a prefix and numeric suffix - /// @param num if non-nullptr, the chosen animation number will be written here - std::string chooseRandomGroup (const std::string& prefix, int* num = nullptr) const; + void updateAnimQueue(); + void playAnimQueue(bool useLoopStart = false); - bool updateCarriedLeftVisible(int weaptype) const; + void updateHeadTracking(float duration); - std::string fallbackShortWeaponGroup(const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask = nullptr); + void updateMagicEffects() const; - std::string getWeaponAnimation(int weaponType) const; + void playDeath(float startpoint, CharacterState death); + CharacterState chooseRandomDeathState() const; + void playRandomDeath(float startpoint = 0.0f); -public: - CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim); - virtual ~CharacterController(); + /// choose a random animation group with \a prefix and numeric suffix + /// @param num if non-nullptr, the chosen animation number will be written here + std::string chooseRandomGroup(const std::string& prefix, int* num = nullptr) const; - void handleTextKey(const std::string &groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) override; + bool updateCarriedLeftVisible(int weaptype) const; - // Be careful when to call this, see comment in Actors - void updateContinuousVfx(); + std::string fallbackShortWeaponGroup( + const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask = nullptr) const; - void updatePtr(const MWWorld::Ptr &ptr); + std::string_view getWeaponAnimation(int weaponType) const; + std::string_view getWeaponShortGroup(int weaponType) const; - void update(float duration); + bool getAttackingOrSpell() const; + void setAttackingOrSpell(bool attackingOrSpell) const; - bool onOpen(); - void onClose(); + std::string_view getDesiredAttackType() const; - void persistAnimationState(); - void unpersistAnimationState(); + void prepareHit(); - bool playGroup(const std::string &groupname, int mode, int count, bool persist=false); - void skipAnim(); - bool isAnimPlaying(const std::string &groupName); + public: + CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation* anim); + virtual ~CharacterController(); - enum KillResult - { - Result_DeathAnimStarted, - Result_DeathAnimPlaying, - Result_DeathAnimJustFinished, - Result_DeathAnimFinished + CharacterController(const CharacterController&) = delete; + CharacterController(CharacterController&&) = delete; + + const MWWorld::Ptr& getPtr() const { return mPtr; } + + void handleTextKey(std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, + const SceneUtil::TextKeyMap& map) override; + + // Be careful when to call this, see comment in Actors + void updateContinuousVfx() const; + + void updatePtr(const MWWorld::Ptr& ptr); + + void update(float duration); + + bool onOpen() const; + 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); + void enableLuaAnimations(bool enable); + void skipAnim(); + bool isAnimPlaying(std::string_view groupName) const; + bool isScriptedAnimPlaying() const; + void clearAnimQueue(bool clearScriptedAnims = false); + + enum KillResult + { + Result_DeathAnimStarted, + Result_DeathAnimPlaying, + Result_DeathAnimJustFinished, + Result_DeathAnimFinished + }; + KillResult kill(); + + void resurrect(); + bool isDead() const { return mDeathState != CharState_None; } + + void forceStateUpdate(); + + bool isAttackPreparing() const; + bool isCastingSpell() const; + bool isReadyToBlock() const; + bool isKnockedDown() const; + bool isKnockedOut() const; + bool isRecovery() const; + bool isSneaking() const; + bool isRunning() const; + bool isTurning() const; + bool isAttackingOrSpell() const; + + void setVisibility(float visibility) const; + void castSpell(const ESM::RefId& spellId, bool scriptedSpell = false); + void setAIAttackType(std::string_view attackType); + static std::string_view getRandomAttackType(); + + bool readyToPrepareAttack() const; + bool readyToStartAttack() const; + + float calculateWindUp() const; + + float getAttackStrength() const; + + /// @see Animation::setActive + void setActive(int active) const; + + /// Make this character turn its head towards \a target. To turn off head tracking, pass an empty Ptr. + void setHeadTrackTarget(const MWWorld::ConstPtr& target); + + void playSwishSound() const; + + float getAnimationMovementDirection() const; + + MWWorld::MovementDirectionFlags getSupportedMovementDirections() const; }; - KillResult kill(); - - void resurrect(); - bool isDead() const - { return mDeathState != CharState_None; } - - void forceStateUpdate(); - - bool isAttackPreparing() const; - bool isCastingSpell() const; - bool isReadyToBlock() const; - bool isKnockedDown() const; - bool isKnockedOut() const; - bool isRecovery() const; - bool isSneaking() const; - bool isRunning() const; - bool isTurning() const; - bool isAttackingOrSpell() const; - - void setVisibility(float visibility); - void setAttackingOrSpell(bool attackingOrSpell); - void castSpell(const std::string spellId, bool manualSpell=false); - void setAIAttackType(const std::string& attackType); - static void setAttackTypeRandomly(std::string& attackType); - - bool readyToPrepareAttack() const; - bool readyToStartAttack() const; - - float getAttackStrength() const; - - /// @see Animation::setActive - void setActive(int active); - - /// Make this character turn its head towards \a target. To turn off head tracking, pass an empty Ptr. - void setHeadTrackTarget(const MWWorld::ConstPtr& target); - - void playSwishSound(float attackStrength); -}; } #endif /* GAME_MWMECHANICS_CHARACTER_HPP */ diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 9e5774ea9e2..5d283214a3a 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -1,60 +1,72 @@ + #include "combat.hpp" #include -#include +#include #include +#include +#include +#include + +#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/globals.hpp" +#include "../mwworld/inventorystore.hpp" -#include "npcstats.hpp" +#include "actorutil.hpp" +#include "difficultyscaling.hpp" #include "movement.hpp" +#include "npcstats.hpp" +#include "pathfinding.hpp" #include "spellcasting.hpp" #include "spellresistance.hpp" -#include "difficultyscaling.hpp" -#include "actorutil.hpp" -#include "pathfinding.hpp" namespace { -float signedAngleRadians (const osg::Vec3f& v1, const osg::Vec3f& v2, const osg::Vec3f& normal) -{ - return std::atan2((normal * (v1 ^ v2)), (v1 * v2)); -} + float signedAngleRadians(const osg::Vec3f& v1, const osg::Vec3f& v2, const osg::Vec3f& normal) + { + return std::atan2((normal * (v1 ^ v2)), (v1 * v2)); + } } namespace MWMechanics { - bool applyOnStrikeEnchantment(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& object, const osg::Vec3f& hitPosition, const bool fromProjectile) + bool applyOnStrikeEnchantment(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& object, + const osg::Vec3f& hitPosition, const bool fromProjectile) { - std::string enchantmentName = !object.isEmpty() ? object.getClass().getEnchantment(object) : ""; + const ESM::RefId enchantmentName = !object.isEmpty() ? object.getClass().getEnchantment(object) : ESM::RefId(); if (!enchantmentName.empty()) { - const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find( - enchantmentName); + const ESM::Enchantment* enchantment + = MWBase::Environment::get().getESMStore()->get().find(enchantmentName); if (enchantment->mData.mType == ESM::Enchantment::WhenStrikes) { MWMechanics::CastSpell cast(attacker, victim, fromProjectile); cast.mHitPosition = hitPosition; cast.cast(object, false); + // Apply magic effects directly instead of waiting a frame to allow soul trap to work on one-hit kills + if (!victim.isEmpty() && victim.getClass().isActor()) + MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(victim); return true; } } return false; } - bool blockMeleeAttack(const MWWorld::Ptr &attacker, const MWWorld::Ptr &blocker, const MWWorld::Ptr &weapon, float damage, float attackStrength) + bool blockMeleeAttack(const MWWorld::Ptr& attacker, const MWWorld::Ptr& blocker, const MWWorld::Ptr& weapon, + float damage, float attackStrength) { if (!blocker.getClass().hasInventoryStore(blocker)) return false; @@ -62,8 +74,7 @@ namespace MWMechanics MWMechanics::CreatureStats& blockerStats = blocker.getClass().getCreatureStats(blocker); if (blockerStats.getKnockedDown() // Used for both knockout or knockdown - || blockerStats.getHitRecovery() - || blockerStats.isParalyzed()) + || blockerStats.getHitRecovery() || blockerStats.isParalyzed()) return false; if (!MWBase::Environment::get().getMechanicsManager()->isReadyToBlock(blocker)) @@ -71,34 +82,41 @@ namespace MWMechanics MWWorld::InventoryStore& inv = blocker.getClass().getInventoryStore(blocker); MWWorld::ContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) + if (shield == inv.end() || shield->getType() != ESM::Armor::sRecordId) return false; if (!blocker.getRefData().getBaseNode()) return false; // shouldn't happen - float angleDegrees = osg::RadiansToDegrees( - signedAngleRadians ( - (attacker.getRefData().getPosition().asVec3() - blocker.getRefData().getPosition().asVec3()), - blocker.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0), - osg::Vec3f(0,0,1))); + float angleDegrees = osg::RadiansToDegrees(signedAngleRadians( + (attacker.getRefData().getPosition().asVec3() - blocker.getRefData().getPosition().asVec3()), + blocker.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0, 1, 0), osg::Vec3f(0, 0, 1))); - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); - if (angleDegrees < gmst.find("fCombatBlockLeftAngle")->mValue.getFloat()) + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); + static const float fCombatBlockLeftAngle = gmst.find("fCombatBlockLeftAngle")->mValue.getFloat(); + if (angleDegrees < fCombatBlockLeftAngle) return false; - if (angleDegrees > gmst.find("fCombatBlockRightAngle")->mValue.getFloat()) + static const float fCombatBlockRightAngle = gmst.find("fCombatBlockRightAngle")->mValue.getFloat(); + if (angleDegrees > fCombatBlockRightAngle) return false; MWMechanics::CreatureStats& attackerStats = attacker.getClass().getCreatureStats(attacker); - float blockTerm = blocker.getClass().getSkill(blocker, ESM::Skill::Block) + 0.2f * blockerStats.getAttribute(ESM::Attribute::Agility).getModified() + float blockTerm = blocker.getClass().getSkill(blocker, ESM::Skill::Block) + + 0.2f * blockerStats.getAttribute(ESM::Attribute::Agility).getModified() + 0.1f * blockerStats.getAttribute(ESM::Attribute::Luck).getModified(); float enemySwing = attackStrength; - float swingTerm = enemySwing * gmst.find("fSwingBlockMult")->mValue.getFloat() + gmst.find("fSwingBlockBase")->mValue.getFloat(); + static const float fSwingBlockMult = gmst.find("fSwingBlockMult")->mValue.getFloat(); + static const float fSwingBlockBase = gmst.find("fSwingBlockBase")->mValue.getFloat(); + float swingTerm = enemySwing * fSwingBlockMult + fSwingBlockBase; float blockerTerm = blockTerm * swingTerm; if (blocker.getClass().getMovementSettings(blocker).mPosition[1] <= 0) - blockerTerm *= gmst.find("fBlockStillBonus")->mValue.getFloat(); + { + static const float fBlockStillBonus = gmst.find("fBlockStillBonus")->mValue.getFloat(); + blockerTerm *= fBlockStillBonus; + } blockerTerm *= blockerStats.getFatigueTerm(); float attackerSkill = 0; @@ -107,27 +125,36 @@ namespace MWMechanics else attackerSkill = attacker.getClass().getSkill(attacker, weapon.getClass().getEquipmentSkill(weapon)); float attackerTerm = attackerSkill + 0.2f * attackerStats.getAttribute(ESM::Attribute::Agility).getModified() - + 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); + + 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); attackerTerm *= attackerStats.getFatigueTerm(); - int x = int(blockerTerm - attackerTerm); - int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger(); - int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger(); - x = std::min(iBlockMaxChance, std::max(iBlockMinChance, x)); + static const int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger(); + static const int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger(); + int x = std::clamp(blockerTerm - attackerTerm, iBlockMinChance, iBlockMaxChance); - if (Misc::Rng::roll0to99() < x) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::roll0to99(prng) < x) { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + const ESM::RefId skill = shield->getClass().getEquipmentSkill(*shield); + if (skill == ESM::Skill::LightArmor) + sndMgr->playSound3D(blocker, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f); + else if (skill == ESM::Skill::MediumArmor) + sndMgr->playSound3D(blocker, ESM::RefId::stringRefId("Medium Armor Hit"), 1.0f, 1.0f); + else if (skill == ESM::Skill::HeavyArmor) + sndMgr->playSound3D(blocker, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f); + // Reduce shield durability by incoming damage int shieldhealth = shield->getClass().getItemHealth(*shield); shieldhealth -= std::min(shieldhealth, int(damage)); shield->getCellRef().setCharge(shieldhealth); if (shieldhealth == 0) - inv.unequipItem(*shield, blocker); + inv.unequipItem(*shield); // Reduce blocker fatigue - const float fFatigueBlockBase = gmst.find("fFatigueBlockBase")->mValue.getFloat(); - const float fFatigueBlockMult = gmst.find("fFatigueBlockMult")->mValue.getFloat(); - const float fWeaponFatigueBlockMult = gmst.find("fWeaponFatigueBlockMult")->mValue.getFloat(); + static const float fFatigueBlockBase = gmst.find("fFatigueBlockBase")->mValue.getFloat(); + static const float fFatigueBlockMult = gmst.find("fFatigueBlockMult")->mValue.getFloat(); + static const float fWeaponFatigueBlockMult = gmst.find("fWeaponFatigueBlockMult")->mValue.getFloat(); MWMechanics::DynamicStat fatigue = blockerStats.getFatigue(); float normalizedEncumbrance = blocker.getClass().getNormalizedEncumbrance(blocker); normalizedEncumbrance = std::min(1.f, normalizedEncumbrance); @@ -140,14 +167,14 @@ namespace MWMechanics blockerStats.setBlock(true); if (blocker == getPlayer()) - blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0); + blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, ESM::Skill::Block_Success); return true; } return false; } - bool isNormalWeapon(const MWWorld::Ptr &weapon) + bool isNormalWeapon(const MWWorld::Ptr& weapon) { if (weapon.isEmpty()) return false; @@ -157,25 +184,26 @@ namespace MWMechanics bool isMagical = flags & ESM::Weapon::Magical; bool isEnchanted = !weapon.getClass().getEnchantment(weapon).empty(); - return !isSilver && !isMagical && (!isEnchanted || !Settings::Manager::getBool("enchanted weapons are magical", "Game")); + return !isSilver && !isMagical && (!isEnchanted || !Settings::game().mEnchantedWeaponsAreMagical); } - void resistNormalWeapon(const MWWorld::Ptr &actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr &weapon, float &damage) + void resistNormalWeapon( + const MWWorld::Ptr& actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float& damage) { - if (damage == 0 || weapon.isEmpty() || !isNormalWeapon(weapon)) + if (weapon.isEmpty() || !isNormalWeapon(weapon)) return; const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects(); - const float resistance = effects.get(ESM::MagicEffect::ResistNormalWeapons).getMagnitude() / 100.f; - const float weakness = effects.get(ESM::MagicEffect::WeaknessToNormalWeapons).getMagnitude() / 100.f; + const float resistance = effects.getOrDefault(ESM::MagicEffect::ResistNormalWeapons).getMagnitude() / 100.f; + const float weakness = effects.getOrDefault(ESM::MagicEffect::WeaknessToNormalWeapons).getMagnitude() / 100.f; - damage *= 1.f - std::min(1.f, resistance-weakness); + damage *= 1.f - std::min(1.f, resistance - weakness); - if (damage == 0 && attacker == getPlayer()) + if (resistance - weakness >= 1.f && attacker == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResistsWeapons}"); } - void applyWerewolfDamageMult(const MWWorld::Ptr &actor, const MWWorld::Ptr &weapon, float &damage) + void applyWerewolfDamageMult(const MWWorld::Ptr& actor, const MWWorld::Ptr& weapon, float& damage) { if (damage == 0 || weapon.isEmpty() || !actor.getClass().isNpc()) return; @@ -185,53 +213,63 @@ namespace MWMechanics if (isSilver && actor.getClass().getNpcStats(actor).isWerewolf()) { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); damage *= store.get().find("fWereWolfSilverWeaponDamageMult")->mValue.getFloat(); } } - void projectileHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, MWWorld::Ptr weapon, const MWWorld::Ptr& projectile, - const osg::Vec3f& hitPosition, float attackStrength) + void projectileHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, MWWorld::Ptr weapon, + const MWWorld::Ptr& projectile, const osg::Vec3f& hitPosition, float attackStrength) { - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &gmst = world->getStore().get(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Store& gmst = world->getStore().get(); bool validVictim = !victim.isEmpty() && victim.getClass().isActor(); + ESM::RefId weaponSkill = ESM::Skill::Marksman; + if (!weapon.isEmpty()) + weaponSkill = weapon.getClass().getEquipmentSkill(weapon); + float damage = 0.f; if (validVictim) { if (attacker == getPlayer()) MWBase::Environment::get().getWindowManager()->setEnemy(victim); - int weaponSkill = ESM::Skill::Marksman; - if (!weapon.isEmpty()) - weaponSkill = weapon.getClass().getEquipmentSkill(weapon); - - int skillValue = attacker.getClass().getSkill(attacker, weapon.getClass().getEquipmentSkill(weapon)); + int skillValue = attacker.getClass().getSkill(attacker, weaponSkill); - if (Misc::Rng::roll0to99() >= getHitChance(attacker, victim, skillValue)) + if (Misc::Rng::roll0to99(world->getPrng()) >= getHitChance(attacker, victim, skillValue)) { - victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false); + victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false, + MWMechanics::DamageSourceType::Ranged); MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker); return; } - const unsigned char* attack = weapon.get()->mBase->mData.mChop; - damage = attack[0] + ((attack[1] - attack[0]) * attackStrength); // Bow/crossbow damage + { + const auto& attack = weapon.get()->mBase->mData.mChop; + damage = attack[0] + ((attack[1] - attack[0]) * attackStrength); // Bow/crossbow damage + } + { + // Arrow/bolt damage + // NB in case of thrown weapons, we are applying the damage twice since projectile == weapon + const auto& attack = projectile.get()->mBase->mData.mChop; + damage += attack[0] + ((attack[1] - attack[0]) * attackStrength); + } + adjustWeaponDamage(damage, weapon, attacker); + } - // Arrow/bolt damage - // NB in case of thrown weapons, we are applying the damage twice since projectile == weapon - attack = projectile.get()->mBase->mData.mChop; - damage += attack[0] + ((attack[1] - attack[0]) * attackStrength); + reduceWeaponCondition(damage, validVictim, weapon, attacker); - adjustWeaponDamage(damage, weapon, attacker); - if (weapon == projectile || Settings::Manager::getBool("only appropriate ammunition bypasses resistance", "Game") || isNormalWeapon(weapon)) + if (validVictim) + { + if (weapon == projectile || Settings::game().mOnlyAppropriateAmmunitionBypassesResistance + || isNormalWeapon(weapon)) resistNormalWeapon(victim, attacker, projectile, damage); applyWerewolfDamageMult(victim, projectile, damage); if (attacker == getPlayer()) - attacker.getClass().skillUsageSucceeded(attacker, weaponSkill, 0); + attacker.getClass().skillUsageSucceeded(attacker, weaponSkill, ESM::Skill::Weapon_SuccessfulHit); const MWMechanics::AiSequence& sequence = victim.getClass().getCreatureStats(victim).getAiSequence(); bool unaware = attacker == getPlayer() && !sequence.isInCombat() @@ -239,14 +277,14 @@ namespace MWMechanics bool knockedDown = victim.getClass().getCreatureStats(victim).getKnockedDown(); if (knockedDown || unaware) { - damage *= gmst.find("fCombatKODamageMult")->mValue.getFloat(); + static const float fCombatKODamageMult = gmst.find("fCombatKODamageMult")->mValue.getFloat(); + damage *= fCombatKODamageMult; if (!knockedDown) - MWBase::Environment::get().getSoundManager()->playSound3D(victim, "critical damage", 1.0f, 1.0f); + MWBase::Environment::get().getSoundManager()->playSound3D( + victim, ESM::RefId::stringRefId("critical damage"), 1.0f, 1.0f); } } - reduceWeaponCondition(damage, validVictim, weapon, attacker); - // Apply "On hit" effect of the projectile bool appliedEnchantment = applyOnStrikeEnchantment(attacker, victim, projectile, hitPosition, true); @@ -255,80 +293,84 @@ namespace MWMechanics // Non-enchanted arrows shot at enemies have a chance to turn up in their inventory if (victim != getPlayer() && !appliedEnchantment) { - float fProjectileThrownStoreChance = gmst.find("fProjectileThrownStoreChance")->mValue.getFloat(); - if (Misc::Rng::rollProbability() < fProjectileThrownStoreChance / 100.f) - victim.getClass().getContainerStore(victim).add(projectile, 1, victim); + static const float fProjectileThrownStoreChance + = gmst.find("fProjectileThrownStoreChance")->mValue.getFloat(); + if (Misc::Rng::rollProbability(world->getPrng()) < fProjectileThrownStoreChance / 100.f) + victim.getClass().getContainerStore(victim).add(projectile, 1); } - victim.getClass().onHit(victim, damage, true, projectile, attacker, hitPosition, true); + victim.getClass().onHit( + victim, damage, true, projectile, attacker, hitPosition, true, MWMechanics::DamageSourceType::Ranged); } } - float getHitChance(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim, int skillValue) + float getHitChance(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, int skillValue) { - MWMechanics::CreatureStats &stats = attacker.getClass().getCreatureStats(attacker); - const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects(); + MWMechanics::CreatureStats& stats = attacker.getClass().getCreatureStats(attacker); + const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &gmst = world->getStore().get(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Store& gmst = world->getStore().get(); float defenseTerm = 0; MWMechanics::CreatureStats& victimStats = victim.getClass().getCreatureStats(victim); if (victimStats.getFatigue().getCurrent() >= 0) { // Maybe we should keep an aware state for actors updated every so often instead of testing every time - bool unaware = (!victimStats.getAiSequence().isInCombat()) - && (attacker == getPlayer()) - && (!MWBase::Environment::get().getMechanicsManager()->awarenessCheck(attacker, victim)); - if (!(victimStats.getKnockedDown() || - victimStats.isParalyzed() - || unaware )) + bool unaware = (!victimStats.getAiSequence().isInCombat()) && (attacker == getPlayer()) + && (!MWBase::Environment::get().getMechanicsManager()->awarenessCheck(attacker, victim)); + if (!(victimStats.getKnockedDown() || victimStats.isParalyzed() || unaware)) { defenseTerm = victimStats.getEvasion(); } + static const float fCombatInvisoMult = gmst.find("fCombatInvisoMult")->mValue.getFloat(); defenseTerm += std::min(100.f, - gmst.find("fCombatInvisoMult")->mValue.getFloat() * - victimStats.getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude()); + fCombatInvisoMult + * victimStats.getMagicEffects().getOrDefault(ESM::MagicEffect::Chameleon).getMagnitude()); defenseTerm += std::min(100.f, - gmst.find("fCombatInvisoMult")->mValue.getFloat() * - victimStats.getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude()); + fCombatInvisoMult + * victimStats.getMagicEffects().getOrDefault(ESM::MagicEffect::Invisibility).getMagnitude()); } - float attackTerm = skillValue + - (stats.getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + - (stats.getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); + float attackTerm = skillValue + (stats.getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + + (stats.getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); attackTerm *= stats.getFatigueTerm(); - attackTerm += mageffects.get(ESM::MagicEffect::FortifyAttack).getMagnitude() - - mageffects.get(ESM::MagicEffect::Blind).getMagnitude(); + attackTerm += mageffects.getOrDefault(ESM::MagicEffect::FortifyAttack).getMagnitude() + - mageffects.getOrDefault(ESM::MagicEffect::Blind).getMagnitude(); return round(attackTerm - defenseTerm); } - void applyElementalShields(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim) + void applyElementalShields(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim) { // Don't let elemental shields harm the player in god mode. bool godmode = attacker == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if (godmode) return; - for (int i=0; i<3; ++i) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + for (int i = 0; i < 3; ++i) { - float magnitude = victim.getClass().getCreatureStats(victim).getMagicEffects().get(ESM::MagicEffect::FireShield+i).getMagnitude(); + float magnitude = victim.getClass() + .getCreatureStats(victim) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::FireShield + i) + .getMagnitude(); if (!magnitude) continue; CreatureStats& attackerStats = attacker.getClass().getCreatureStats(attacker); float saveTerm = attacker.getClass().getSkill(attacker, ESM::Skill::Destruction) - + 0.2f * attackerStats.getAttribute(ESM::Attribute::Willpower).getModified() - + 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); + + 0.2f * attackerStats.getAttribute(ESM::Attribute::Willpower).getModified() + + 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); float fatigueMax = attackerStats.getFatigue().getModified(); float fatigueCurrent = attackerStats.getFatigue().getCurrent(); - float normalisedFatigue = floor(fatigueMax)==0 ? 1 : std::max (0.0f, (fatigueCurrent/fatigueMax)); + float normalisedFatigue = floor(fatigueMax) == 0 ? 1 : std::max(0.0f, (fatigueCurrent / fatigueMax)); saveTerm *= 1.25f * normalisedFatigue; - float x = std::max(0.f, saveTerm - Misc::Rng::roll0to99()); + float x = std::max(0.f, saveTerm - Misc::Rng::roll0to99(prng)); int element = ESM::MagicEffect::FireDamage; if (i == 1) @@ -336,11 +378,16 @@ namespace MWMechanics if (i == 2) element = ESM::MagicEffect::FrostDamage; - float elementResistance = MWMechanics::getEffectResistanceAttribute(element, &attackerStats.getMagicEffects()); + float elementResistance + = MWMechanics::getEffectResistanceAttribute(element, &attackerStats.getMagicEffects()); x = std::min(100.f, x + elementResistance); - static const float fElementalShieldMult = MWBase::Environment::get().getWorld()->getStore().get().find("fElementalShieldMult")->mValue.getFloat(); + static const float fElementalShieldMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fElementalShieldMult") + ->mValue.getFloat(); x = fElementalShieldMult * magnitude * (1.f - 0.01f * x); // Note swapped victim and attacker, since the attacker takes the damage here. @@ -350,11 +397,12 @@ namespace MWMechanics health.setCurrent(health.getCurrent() - x); attackerStats.setHealth(health); - MWBase::Environment::get().getSoundManager()->playSound3D(attacker, "Health Damage", 1.0f, 1.0f); + MWBase::Environment::get().getSoundManager()->playSound3D( + attacker, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f); } } - void reduceWeaponCondition(float damage, bool hit, MWWorld::Ptr &weapon, const MWWorld::Ptr &attacker) + void reduceWeaponCondition(float damage, bool hit, MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker) { if (weapon.isEmpty()) return; @@ -363,16 +411,21 @@ namespace MWMechanics damage = 0.f; const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); - if(weaphashealth) + if (weaphashealth) { int weaphealth = weapon.getClass().getItemHealth(weapon); - bool godmode = attacker == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); + bool godmode + = attacker == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); // weapon condition does not degrade when godmode is on if (!godmode) { - const float fWeaponDamageMult = MWBase::Environment::get().getWorld()->getStore().get().find("fWeaponDamageMult")->mValue.getFloat(); + const float fWeaponDamageMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fWeaponDamageMult") + ->mValue.getFloat(); float x = std::max(1.f, fWeaponDamageMult * damage); weaphealth -= std::min(int(x), weaphealth); @@ -381,11 +434,11 @@ namespace MWMechanics // Weapon broken? unequip it if (weaphealth == 0) - weapon = *attacker.getClass().getInventoryStore(attacker).unequipItem(weapon, attacker); + weapon = *attacker.getClass().getInventoryStore(attacker).unequipItem(weapon); } } - void adjustWeaponDamage(float &damage, const MWWorld::Ptr &weapon, const MWWorld::Ptr& attacker) + void adjustWeaponDamage(float& damage, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker) { if (weapon.isEmpty()) return; @@ -396,63 +449,79 @@ namespace MWMechanics damage *= weapon.getClass().getItemNormalizedHealth(weapon); } - static const float fDamageStrengthBase = MWBase::Environment::get().getWorld()->getStore().get() - .find("fDamageStrengthBase")->mValue.getFloat(); - static const float fDamageStrengthMult = MWBase::Environment::get().getWorld()->getStore().get() - .find("fDamageStrengthMult")->mValue.getFloat(); - damage *= fDamageStrengthBase + - (attacker.getClass().getCreatureStats(attacker).getAttribute(ESM::Attribute::Strength).getModified() * fDamageStrengthMult * 0.1f); + static const float fDamageStrengthBase = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fDamageStrengthBase") + ->mValue.getFloat(); + static const float fDamageStrengthMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fDamageStrengthMult") + ->mValue.getFloat(); + damage *= fDamageStrengthBase + + (attacker.getClass().getCreatureStats(attacker).getAttribute(ESM::Attribute::Strength).getModified() + * fDamageStrengthMult * 0.1f); } - void getHandToHandDamage(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim, float &damage, bool &healthdmg, float attackStrength) + void getHandToHandDamage( + const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, float& damage, bool& healthdmg, float attackStrength) { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - float minstrike = store.get().find("fMinHandToHandMult")->mValue.getFloat(); - float maxstrike = store.get().find("fMaxHandToHandMult")->mValue.getFloat(); - damage = static_cast(attacker.getClass().getSkill(attacker, ESM::Skill::HandToHand)); - damage *= minstrike + ((maxstrike-minstrike)*attackStrength); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + static const float minstrike = store.get().find("fMinHandToHandMult")->mValue.getFloat(); + static const float maxstrike = store.get().find("fMaxHandToHandMult")->mValue.getFloat(); + damage = static_cast(attacker.getClass().getSkill(attacker, ESM::Skill::HandToHand)); + damage *= minstrike + ((maxstrike - minstrike) * attackStrength); MWMechanics::CreatureStats& otherstats = victim.getClass().getCreatureStats(victim); - healthdmg = otherstats.isParalyzed() - || otherstats.getKnockedDown(); + healthdmg = otherstats.isParalyzed() || otherstats.getKnockedDown(); bool isWerewolf = (attacker.getClass().isNpc() && attacker.getClass().getNpcStats(attacker).isWerewolf()); // Options in the launcher's combo box: unarmedFactorsStrengthComboBox // 0 = Do not factor strength into hand-to-hand combat. // 1 = Factor into werewolf hand-to-hand combat. // 2 = Ignore werewolves. - int factorStrength = Settings::Manager::getInt("strength influences hand to hand", "Game"); - if (factorStrength == 1 || (factorStrength == 2 && !isWerewolf)) { - damage *= attacker.getClass().getCreatureStats(attacker).getAttribute(ESM::Attribute::Strength).getModified() / 40.0f; + const int factorStrength = Settings::game().mStrengthInfluencesHandToHand; + if (factorStrength == 1 || (factorStrength == 2 && !isWerewolf)) + { + damage + *= attacker.getClass().getCreatureStats(attacker).getAttribute(ESM::Attribute::Strength).getModified() + / 40.0f; } - if(isWerewolf) + if (isWerewolf) { healthdmg = true; // GLOB instead of GMST because it gets updated during a quest - damage *= MWBase::Environment::get().getWorld()->getGlobalFloat("werewolfclawmult"); + damage *= MWBase::Environment::get().getWorld()->getGlobalFloat(MWWorld::Globals::sWerewolfClawMult); + } + if (healthdmg) + { + static const float fHandtoHandHealthPer + = store.get().find("fHandtoHandHealthPer")->mValue.getFloat(); + damage *= fHandtoHandHealthPer; } - if(healthdmg) - damage *= store.get().find("fHandtoHandHealthPer")->mValue.getFloat(); - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(isWerewolf) + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + if (isWerewolf) { - const ESM::Sound *sound = store.get().searchRandom("WolfHit"); - if(sound) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound* sound = store.get().searchRandom("WolfHit", prng); + if (sound) sndMgr->playSound3D(victim, sound->mId, 1.0f, 1.0f); } else if (!healthdmg) - sndMgr->playSound3D(victim, "Hand To Hand Hit", 1.0f, 1.0f); + sndMgr->playSound3D(victim, ESM::RefId::stringRefId("Hand To Hand Hit"), 1.0f, 1.0f); } - void applyFatigueLoss(const MWWorld::Ptr &attacker, const MWWorld::Ptr &weapon, float attackStrength) + void applyFatigueLoss(const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float attackStrength) { // somewhat of a guess, but using the weapon weight makes sense - const MWWorld::Store& store = MWBase::Environment::get().getWorld()->getStore().get(); - const float fFatigueAttackBase = store.find("fFatigueAttackBase")->mValue.getFloat(); - const float fFatigueAttackMult = store.find("fFatigueAttackMult")->mValue.getFloat(); - const float fWeaponFatigueMult = store.find("fWeaponFatigueMult")->mValue.getFloat(); + const MWWorld::Store& store + = MWBase::Environment::get().getESMStore()->get(); + static const float fFatigueAttackBase = store.find("fFatigueAttackBase")->mValue.getFloat(); + static const float fFatigueAttackMult = store.find("fFatigueAttackMult")->mValue.getFloat(); + static const float fWeaponFatigueMult = store.find("fWeaponFatigueMult")->mValue.getFloat(); CreatureStats& stats = attacker.getClass().getCreatureStats(attacker); MWMechanics::DynamicStat fatigue = stats.getFatigue(); const float normalizedEncumbrance = attacker.getClass().getNormalizedEncumbrance(attacker); @@ -471,30 +540,167 @@ namespace MWMechanics float getFightDistanceBias(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2) { - osg::Vec3f pos1 (actor1.getRefData().getPosition().asVec3()); - osg::Vec3f pos2 (actor2.getRefData().getPosition().asVec3()); + osg::Vec3f pos1(actor1.getRefData().getPosition().asVec3()); + osg::Vec3f pos2(actor2.getRefData().getPosition().asVec3()); float d = getAggroDistance(actor1, pos1, pos2); - static const int iFightDistanceBase = MWBase::Environment::get().getWorld()->getStore().get().find( - "iFightDistanceBase")->mValue.getInteger(); - static const float fFightDistanceMultiplier = MWBase::Environment::get().getWorld()->getStore().get().find( - "fFightDistanceMultiplier")->mValue.getFloat(); + static const int iFightDistanceBase = MWBase::Environment::get() + .getESMStore() + ->get() + .find("iFightDistanceBase") + ->mValue.getInteger(); + static const float fFightDistanceMultiplier = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fFightDistanceMultiplier") + ->mValue.getFloat(); return (iFightDistanceBase - fFightDistanceMultiplier * d); } - bool isTargetMagicallyHidden(const MWWorld::Ptr& target) - { - const MagicEffects& magicEffects = target.getClass().getCreatureStats(target).getMagicEffects(); - return (magicEffects.get(ESM::MagicEffect::Invisibility).getMagnitude() > 0) - || (magicEffects.get(ESM::MagicEffect::Chameleon).getMagnitude() > 75); - } - float getAggroDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs) { if (canActorMoveByZAxis(actor)) return distanceIgnoreZ(lhs, rhs); return distance(lhs, rhs); } + + float getDistanceToBounds(const MWWorld::Ptr& actor, const MWWorld::Ptr& target) + { + osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); + osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); + MWBase::World* world = MWBase::Environment::get().getWorld(); + + float dist = (targetPos - actorPos).length(); + dist -= world->getHalfExtents(actor).y(); + dist -= world->getHalfExtents(target).y(); + return dist; + } + + std::pair getHitContact(const MWWorld::Ptr& actor, float reach) + { + // Lasciate ogne speranza, voi ch'entrate + MWWorld::Ptr result; + osg::Vec3f hitPos; + float minDist = std::numeric_limits::max(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Store& store = world->getStore().get(); + + // These GMSTs are not in degrees. They're tolerance angle sines multiplied by 90. + // With the default values of 60, the actual tolerance angles are roughly 41.8 degrees. + // Don't think too hard about it. In this place, thinking can cause permanent damage to your mental health. + const float fCombatAngleXY = store.find("fCombatAngleXY")->mValue.getFloat() / 90.f; + const float fCombatAngleZ = store.find("fCombatAngleZ")->mValue.getFloat() / 90.f; + + const ESM::Position& posdata = actor.getRefData().getPosition(); + const osg::Vec3f actorPos(posdata.asVec3()); + const osg::Vec3f actorDirXY = osg::Quat(posdata.rot[2], osg::Vec3(0, 0, -1)) * osg::Vec3f(0, 1, 0); + // Only the player can look up, apparently. + const float actorVerticalAngle = actor == getPlayer() ? -std::sin(posdata.rot[0]) : 0.f; + const float actorEyeLevel = world->getHalfExtents(actor, true).z() * 2.f * 0.85f; + const osg::Vec3f actorEyePos{ actorPos.x(), actorPos.y(), actorPos.z() + actorEyeLevel }; + const bool canMoveByZ = canActorMoveByZAxis(actor); + + // The player can target any active actor, non-playable actors only target their targets + std::vector targets; + if (actor != getPlayer()) + actor.getClass().getCreatureStats(actor).getAiSequence().getCombatTargets(targets); + else + MWBase::Environment::get().getMechanicsManager()->getActorsInRange( + actorPos, Settings::game().mActorsProcessingRange, targets); + + for (MWWorld::Ptr& target : targets) + { + if (actor == target || target.getClass().getCreatureStats(target).isDead()) + continue; + const float dist = getDistanceToBounds(actor, target); + const osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); + if (dist >= reach || dist >= minDist || std::abs(targetPos.z() - actorPos.z()) >= reach) + continue; + + // Horizontal angle checks. + osg::Vec2f actorToTargetXY{ targetPos.x() - actorPos.x(), targetPos.y() - actorPos.y() }; + actorToTargetXY.normalize(); + + // Use dot product to check if the target is behind first... + if (actorToTargetXY.x() * actorDirXY.x() + actorToTargetXY.y() * actorDirXY.y() <= 0.f) + continue; + + // And then perp dot product to calculate the hit angle sine. + // This gives us a horizontal hit range of [-asin(fCombatAngleXY / 90); asin(fCombatAngleXY / 90)] + if (std::abs(actorToTargetXY.x() * actorDirXY.y() - actorToTargetXY.y() * actorDirXY.x()) > fCombatAngleXY) + continue; + + // Vertical angle checks. Nice cliff racer hack, Todd. + if (!canMoveByZ) + { + // The idea is that the body should always be possible to hit. + // fCombatAngleZ is the tolerance for hitting the target's feet or head. + osg::Vec3f actorToTargetFeet = targetPos - actorEyePos; + osg::Vec3f actorToTargetHead = actorToTargetFeet; + actorToTargetFeet.normalize(); + actorToTargetHead.z() += world->getHalfExtents(target, true).z() * 2.f; + actorToTargetHead.normalize(); + + if (actorVerticalAngle - actorToTargetHead.z() > fCombatAngleZ + || actorVerticalAngle - actorToTargetFeet.z() < -fCombatAngleZ) + continue; + } + + // Gotta use physics somehow! + if (!world->getLOS(actor, target)) + continue; + + minDist = dist; + result = target; + } + + // This hit position is currently used for spawning the blood effect. + // Morrowind does this elsewhere, but roughly at the same time + // and it would be hard to track the original hit results outside of this function + // without code duplication + // The idea is to use a random point on a plane in front of the target + // that is defined by its width and height + if (!result.isEmpty()) + { + osg::Vec3f resultPos(result.getRefData().getPosition().asVec3()); + osg::Vec3f dirToActor = actorPos - resultPos; + dirToActor.normalize(); + + hitPos = resultPos + dirToActor * world->getHalfExtents(result).y(); + // -25% to 25% of width + float xOffset = Misc::Rng::deviate(0.f, 0.25f, world->getPrng()); + // 20% to 100% of height + float zOffset = Misc::Rng::deviate(0.6f, 0.4f, world->getPrng()); + hitPos.x() += world->getHalfExtents(result).x() * 2.f * xOffset; + hitPos.z() += world->getHalfExtents(result).z() * 2.f * zOffset; + } + + return std::make_pair(result, hitPos); + } + + bool friendlyHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& target, bool complain) + { + const MWWorld::Ptr& player = getPlayer(); + if (attacker != player) + return false; + + std::set followersAttacker; + MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(attacker, followersAttacker); + if (followersAttacker.find(target) == followersAttacker.end()) + return false; + + MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); + if (statsTarget.getAiSequence().isInCombat()) + return true; + statsTarget.friendlyHit(); + if (statsTarget.getFriendlyHits() >= 4) + return false; + + if (complain) + MWBase::Environment::get().getDialogueManager()->say(target, ESM::RefId::stringRefId("hit")); + return true; + } + } diff --git a/apps/openmw/mwmechanics/combat.hpp b/apps/openmw/mwmechanics/combat.hpp index 3d4a1bd77c7..92033c7e776 100644 --- a/apps/openmw/mwmechanics/combat.hpp +++ b/apps/openmw/mwmechanics/combat.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_MECHANICS_COMBAT_H #define OPENMW_MECHANICS_COMBAT_H +#include + namespace osg { class Vec3f; @@ -14,50 +16,58 @@ namespace MWWorld namespace MWMechanics { -bool applyOnStrikeEnchantment(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& object, const osg::Vec3f& hitPosition, - const bool fromProjectile=false); + bool applyOnStrikeEnchantment(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& object, + const osg::Vec3f& hitPosition, const bool fromProjectile = false); + + /// @return can we block the attack? + bool blockMeleeAttack(const MWWorld::Ptr& attacker, const MWWorld::Ptr& blocker, const MWWorld::Ptr& weapon, + float damage, float attackStrength); -/// @return can we block the attack? -bool blockMeleeAttack (const MWWorld::Ptr& attacker, const MWWorld::Ptr& blocker, const MWWorld::Ptr& weapon, float damage, float attackStrength); + /// @return does normal weapon resistance and weakness apply to the weapon? + bool isNormalWeapon(const MWWorld::Ptr& weapon); -/// @return does normal weapon resistance and weakness apply to the weapon? -bool isNormalWeapon (const MWWorld::Ptr& weapon); + void resistNormalWeapon( + const MWWorld::Ptr& actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float& damage); -void resistNormalWeapon (const MWWorld::Ptr& actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float& damage); + void applyWerewolfDamageMult(const MWWorld::Ptr& actor, const MWWorld::Ptr& weapon, float& damage); -void applyWerewolfDamageMult (const MWWorld::Ptr& actor, const MWWorld::Ptr& weapon, float &damage); + /// @note for a thrown weapon, \a weapon == \a projectile, for bows/crossbows, \a projectile is the arrow/bolt + /// @note \a victim may be empty (e.g. for a hit on terrain), a non-actor (environment objects) or an actor + void projectileHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, MWWorld::Ptr weapon, + const MWWorld::Ptr& projectile, const osg::Vec3f& hitPosition, float attackStrength); -/// @note for a thrown weapon, \a weapon == \a projectile, for bows/crossbows, \a projectile is the arrow/bolt -/// @note \a victim may be empty (e.g. for a hit on terrain), a non-actor (environment objects) or an actor -void projectileHit (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, MWWorld::Ptr weapon, const MWWorld::Ptr& projectile, - const osg::Vec3f& hitPosition, float attackStrength); + /// Get the chance (in percent) for \a attacker to successfully hit \a victim with a given weapon skill value + float getHitChance(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, int skillValue); -/// Get the chance (in percent) for \a attacker to successfully hit \a victim with a given weapon skill value -float getHitChance (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, int skillValue); + /// Applies damage to attacker based on the victim's elemental shields. + void applyElementalShields(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim); -/// Applies damage to attacker based on the victim's elemental shields. -void applyElementalShields(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim); + /// @param damage Unmitigated weapon damage of the attack + /// @param hit Was the attack successful? + /// @param weapon The weapon used. + /// @note if the weapon is unequipped as result of condition damage, a new Ptr will be assigned to \a weapon. + void reduceWeaponCondition(float damage, bool hit, MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker); -/// @param damage Unmitigated weapon damage of the attack -/// @param hit Was the attack successful? -/// @param weapon The weapon used. -/// @note if the weapon is unequipped as result of condition damage, a new Ptr will be assigned to \a weapon. -void reduceWeaponCondition (float damage, bool hit, MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker); + /// Adjust weapon damage based on its condition. A used weapon will be less effective. + void adjustWeaponDamage(float& damage, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker); -/// Adjust weapon damage based on its condition. A used weapon will be less effective. -void adjustWeaponDamage (float& damage, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker); + void getHandToHandDamage( + const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, float& damage, bool& healthdmg, float attackStrength); -void getHandToHandDamage (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, float& damage, bool& healthdmg, float attackStrength); + /// Apply the fatigue loss incurred by attacking with the given weapon (weapon may be empty = hand-to-hand) + void applyFatigueLoss(const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float attackStrength); -/// Apply the fatigue loss incurred by attacking with the given weapon (weapon may be empty = hand-to-hand) -void applyFatigueLoss(const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float attackStrength); + float getFightDistanceBias(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2); -float getFightDistanceBias(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2); + float getAggroDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs); -bool isTargetMagicallyHidden(const MWWorld::Ptr& target); + // Cursed distance calculation used for combat proximity and hit checks in Morrowind + float getDistanceToBounds(const MWWorld::Ptr& actor, const MWWorld::Ptr& target); -float getAggroDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs); + // Similarly cursed hit target selection + std::pair getHitContact(const MWWorld::Ptr& actor, float reach); + bool friendlyHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& target, bool complain); } #endif diff --git a/apps/openmw/mwmechanics/creaturecustomdataresetter.hpp b/apps/openmw/mwmechanics/creaturecustomdataresetter.hpp new file mode 100644 index 00000000000..667a0b9c458 --- /dev/null +++ b/apps/openmw/mwmechanics/creaturecustomdataresetter.hpp @@ -0,0 +1,20 @@ +#ifndef OPENMW_MWMECHANICS_CREATURECUSTOMDATARESETTER_H +#define OPENMW_MWMECHANICS_CREATURECUSTOMDATARESETTER_H + +#include "../mwworld/ptr.hpp" + +namespace MWMechanics +{ + struct CreatureCustomDataResetter + { + MWWorld::Ptr mPtr; + + ~CreatureCustomDataResetter() + { + if (!mPtr.isEmpty()) + mPtr.getRefData().setCustomData({}); + } + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index c6561af9610..98bfa59c893 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -1,11 +1,14 @@ #include "creaturestats.hpp" #include +#include -#include -#include -#include +#include +#include +#include +#include +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" @@ -17,15 +20,36 @@ namespace MWMechanics int CreatureStats::sActorId = 0; CreatureStats::CreatureStats() - : mDrawState (DrawState_Nothing), mDead (false), mDeathAnimationFinished(false), mDied (false), mMurdered(false), mFriendlyHits (0), - mTalkedTo (false), mAlarmed (false), mAttacked (false), - mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false), - mHitRecovery(false), mBlock(false), mMovementFlags(0), - mFallHeight(0), mRecalcMagicka(false), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), - mDeathAnimation(-1), mTimeOfDeath(), mSideMovementAngle(0), mLevel (0) - { - for (int i=0; i<4; ++i) - mAiSettings[i] = 0; + : mDrawState(DrawState::Nothing) + , mDead(false) + , mDeathAnimationFinished(false) + , mDied(false) + , mMurdered(false) + , mFriendlyHits(0) + , mTalkedTo(false) + , mAlarmed(false) + , mAttacked(false) + , mKnockdown(false) + , mKnockdownOneFrame(false) + , mKnockdownOverOneFrame(false) + , mHitRecovery(false) + , mBlock(false) + , mMovementFlags(0) + , mFallHeight(0) + , mLastRestock(0, 0) + , mGoldPool(0) + , mActorId(-1) + , mHitAttemptActorId(-1) + , mDeathAnimation(-1) + , mTimeOfDeath() + , mSideMovementAngle(0) + , mLevel(0) + , mAttackingOrSpell(false) + { + for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) + { + mAttributes.emplace(attribute.mId, AttributeValue{}); + } } const AiSequence& CreatureStats::getAiSequence() const @@ -43,51 +67,48 @@ namespace MWMechanics float max = getFatigue().getModified(); float current = getFatigue().getCurrent(); - float normalised = floor(max) == 0 ? 1 : std::max (0.0f, current / max); + float normalised = std::floor(max) == 0 ? 1 : std::max(0.0f, current / max); - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); static const float fFatigueBase = gmst.find("fFatigueBase")->mValue.getFloat(); static const float fFatigueMult = gmst.find("fFatigueMult")->mValue.getFloat(); - return fFatigueBase - fFatigueMult * (1-normalised); + return fFatigueBase - fFatigueMult * (1 - normalised); } - const AttributeValue &CreatureStats::getAttribute(int index) const + const AttributeValue& CreatureStats::getAttribute(ESM::RefId id) const { - if (index < 0 || index > 7) { - throw std::runtime_error("attribute index is out of range"); - } - return mAttributes[index]; + return mAttributes.at(id); } - const DynamicStat &CreatureStats::getHealth() const + const DynamicStat& CreatureStats::getHealth() const { return mDynamic[0]; } - const DynamicStat &CreatureStats::getMagicka() const + const DynamicStat& CreatureStats::getMagicka() const { return mDynamic[1]; } - const DynamicStat &CreatureStats::getFatigue() const + const DynamicStat& CreatureStats::getFatigue() const { return mDynamic[2]; } - const Spells &CreatureStats::getSpells() const + const Spells& CreatureStats::getSpells() const { return mSpells; } - const ActiveSpells &CreatureStats::getActiveSpells() const + const ActiveSpells& CreatureStats::getActiveSpells() const { return mActiveSpells; } - const MagicEffects &CreatureStats::getMagicEffects() const + const MagicEffects& CreatureStats::getMagicEffects() const { return mMagicEffects; } @@ -97,105 +118,97 @@ namespace MWMechanics return mLevel; } - Stat CreatureStats::getAiSetting (AiSetting index) const + Stat CreatureStats::getAiSetting(AiSetting index) const { - return mAiSettings[index]; + return mAiSettings[static_cast>(index)]; } - const DynamicStat &CreatureStats::getDynamic(int index) const + const DynamicStat& CreatureStats::getDynamic(int index) const { - if (index < 0 || index > 2) { + if (index < 0 || index > 2) + { throw std::runtime_error("dynamic stat index is out of range"); } return mDynamic[index]; } - Spells &CreatureStats::getSpells() + Spells& CreatureStats::getSpells() { return mSpells; } - ActiveSpells &CreatureStats::getActiveSpells() + ActiveSpells& CreatureStats::getActiveSpells() { return mActiveSpells; } - MagicEffects &CreatureStats::getMagicEffects() + MagicEffects& CreatureStats::getMagicEffects() { return mMagicEffects; } - void CreatureStats::setAttribute(int index, float base) + void CreatureStats::setAttribute(ESM::RefId id, float base) { - AttributeValue current = getAttribute(index); + AttributeValue current = getAttribute(id); current.setBase(base); - setAttribute(index, current); + setAttribute(id, current); } - void CreatureStats::setAttribute(int index, const AttributeValue &value) + void CreatureStats::setAttribute(ESM::RefId id, const AttributeValue& value) { - if (index < 0 || index > 7) { - throw std::runtime_error("attribute index is out of range"); - } - - const AttributeValue& currentValue = mAttributes[index]; + const AttributeValue& currentValue = mAttributes.at(id); if (value != currentValue) { - mAttributes[index] = value; - - if (index == ESM::Attribute::Intelligence) - mRecalcMagicka = true; - else if (index == ESM::Attribute::Strength || - index == ESM::Attribute::Willpower || - index == ESM::Attribute::Agility || - index == ESM::Attribute::Endurance) + mAttributes[id] = value; + + if (id == ESM::Attribute::Intelligence) + recalculateMagicka(); + else if (id == ESM::Attribute::Strength || id == ESM::Attribute::Willpower || id == ESM::Attribute::Agility + || id == ESM::Attribute::Endurance) { - float strength = getAttribute(ESM::Attribute::Strength).getModified(); - float willpower = getAttribute(ESM::Attribute::Willpower).getModified(); - float agility = getAttribute(ESM::Attribute::Agility).getModified(); - float endurance = getAttribute(ESM::Attribute::Endurance).getModified(); + float strength = getAttribute(ESM::Attribute::Strength).getModified(); + float willpower = getAttribute(ESM::Attribute::Willpower).getModified(); + float agility = getAttribute(ESM::Attribute::Agility).getModified(); + float endurance = getAttribute(ESM::Attribute::Endurance).getModified(); DynamicStat fatigue = getFatigue(); - float diff = (strength+willpower+agility+endurance) - fatigue.getBase(); float currentToBaseRatio = fatigue.getBase() > 0 ? (fatigue.getCurrent() / fatigue.getBase()) : 0; - fatigue.setModified(fatigue.getModified() + diff, 0); - fatigue.setCurrent(fatigue.getBase() * currentToBaseRatio); + fatigue.setBase(std::max(0.f, strength + willpower + agility + endurance)); + fatigue.setCurrent(fatigue.getBase() * currentToBaseRatio, false, true); setFatigue(fatigue); } } } - void CreatureStats::setHealth(const DynamicStat &value) + void CreatureStats::setHealth(const DynamicStat& value) { - setDynamic (0, value); + setDynamic(0, value); } - void CreatureStats::setMagicka(const DynamicStat &value) + void CreatureStats::setMagicka(const DynamicStat& value) { - setDynamic (1, value); + setDynamic(1, value); } - void CreatureStats::setFatigue(const DynamicStat &value) + void CreatureStats::setFatigue(const DynamicStat& value) { - setDynamic (2, value); + setDynamic(2, value); } - void CreatureStats::setDynamic (int index, const DynamicStat &value) + void CreatureStats::setDynamic(int index, const DynamicStat& value) { if (index < 0 || index > 2) throw std::runtime_error("dynamic stat index is out of range"); mDynamic[index] = value; - if (index==0 && mDynamic[index].getCurrent()<1) + if (index == 0 && mDynamic[index].getCurrent() < 1) { if (!mDead) mTimeOfDeath = MWBase::Environment::get().getWorld()->getTimeStamp(); mDead = true; - mDynamic[index].setModifier(0); - mDynamic[index].setCurrentModifier(0); mDynamic[index].setCurrent(0); } } @@ -205,21 +218,21 @@ namespace MWMechanics mLevel = level; } - void CreatureStats::modifyMagicEffects(const MagicEffects &effects) + void CreatureStats::modifyMagicEffects(const MagicEffects& effects) { - if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() - != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier()) - mRecalcMagicka = true; - + bool recalc = effects.getOrDefault(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() + != mMagicEffects.getOrDefault(ESM::MagicEffect::FortifyMaximumMagicka).getModifier(); mMagicEffects.setModifiers(effects); + if (recalc) + recalculateMagicka(); } - void CreatureStats::setAiSetting (AiSetting index, Stat value) + void CreatureStats::setAiSetting(AiSetting index, Stat value) { - mAiSettings[index] = value; + mAiSettings[static_cast>(index)] = value; } - void CreatureStats::setAiSetting (AiSetting index, int base) + void CreatureStats::setAiSetting(AiSetting index, int base) { Stat stat = getAiSetting(index); stat.setBase(base); @@ -228,7 +241,12 @@ namespace MWMechanics bool CreatureStats::isParalyzed() const { - return mMagicEffects.get(ESM::MagicEffect::Paralyze).getMagnitude() > 0; + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Ptr player = world->getPlayerPtr(); + if (world->getGodModeState() && this == &player.getClass().getCreatureStats(player)) + return false; + + return mMagicEffects.getOrDefault(ESM::MagicEffect::Paralyze).getMagnitude() > 0; } bool CreatureStats::isDead() const @@ -280,10 +298,7 @@ namespace MWMechanics { if (mDead) { - if (mDynamic[0].getModified() < 1) - mDynamic[0].setModified(1, 0); - - mDynamic[0].setCurrent(mDynamic[0].getModified()); + mDynamic[0].setCurrent(mDynamic[0].getBase()); mDead = false; mDeathAnimationFinished = false; } @@ -309,6 +324,11 @@ namespace MWMechanics ++mFriendlyHits; } + void CreatureStats::resetFriendlyHits() + { + mFriendlyHits = 0; + } + bool CreatureStats::hasTalkedToPlayer() const { return mTalkedTo; @@ -324,7 +344,7 @@ namespace MWMechanics return mAlarmed; } - void CreatureStats::setAlarmed (bool alarmed) + void CreatureStats::setAlarmed(bool alarmed) { mAlarmed = alarmed; } @@ -334,37 +354,47 @@ namespace MWMechanics return mAttacked; } - void CreatureStats::setAttacked (bool attacked) + void CreatureStats::setAttacked(bool attacked) { mAttacked = attacked; } float CreatureStats::getEvasion() const { - float evasion = (getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + - (getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); + float evasion = (getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + + (getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); evasion *= getFatigueTerm(); - evasion += std::min(100.f, mMagicEffects.get(ESM::MagicEffect::Sanctuary).getMagnitude()); + evasion += std::min(100.f, mMagicEffects.getOrDefault(ESM::MagicEffect::Sanctuary).getMagnitude()); return evasion; } - void CreatureStats::setLastHitObject(const std::string& objectid) + void CreatureStats::setLastHitObject(const ESM::RefId& objectid) { mLastHitObject = objectid; } - const std::string &CreatureStats::getLastHitObject() const + void CreatureStats::clearLastHitObject() + { + mLastHitObject = ESM::RefId(); + } + + const ESM::RefId& CreatureStats::getLastHitObject() const { return mLastHitObject; } - void CreatureStats::setLastHitAttemptObject(const std::string& objectid) + void CreatureStats::setLastHitAttemptObject(const ESM::RefId& objectid) { mLastHitAttemptObject = objectid; } - const std::string &CreatureStats::getLastHitAttemptObject() const + void CreatureStats::clearLastHitAttemptObject() + { + mLastHitAttemptObject = ESM::RefId(); + } + + const ESM::RefId& CreatureStats::getLastHitAttemptObject() const { return mLastHitAttemptObject; } @@ -399,25 +429,32 @@ namespace MWMechanics return height; } - bool CreatureStats::needToRecalcDynamicStats() + void CreatureStats::recalculateMagicka() { - if (mRecalcMagicka) - { - mRecalcMagicka = false; - return true; - } - return false; - } + auto world = MWBase::Environment::get().getWorld(); + float intelligence = getAttribute(ESM::Attribute::Intelligence).getModified(); - void CreatureStats::setNeedRecalcDynamicStats(bool val) - { - mRecalcMagicka = val; + float base = 1.f; + const auto& player = world->getPlayerPtr(); + if (this == &player.getClass().getCreatureStats(player)) + base = world->getStore().get().find("fPCbaseMagickaMult")->mValue.getFloat(); + else + base = world->getStore().get().find("fNPCbaseMagickaMult")->mValue.getFloat(); + + double magickaFactor = base + + mMagicEffects.getOrDefault(EffectKey(ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; + + DynamicStat magicka = getMagicka(); + float currentToBaseRatio = magicka.getBase() > 0 ? magicka.getCurrent() / magicka.getBase() : 0; + magicka.setBase(magickaFactor * intelligence); + magicka.setCurrent(magicka.getBase() * currentToBaseRatio, false, true); + setMagicka(magicka); } void CreatureStats::setKnockedDown(bool value) { mKnockdown = value; - if(!value) //Resets the "OverOneFrame" flag + if (!value) // Resets the "OverOneFrame" flag setKnockedDownOverOneFrame(false); } @@ -436,10 +473,12 @@ namespace MWMechanics return mKnockdownOneFrame; } - void CreatureStats::setKnockedDownOverOneFrame(bool value) { + void CreatureStats::setKnockedDownOverOneFrame(bool value) + { mKnockdownOverOneFrame = value; } - bool CreatureStats::getKnockedDownOverOneFrame() const { + bool CreatureStats::getKnockedDownOverOneFrame() const + { return mKnockdownOverOneFrame; } @@ -463,12 +502,12 @@ namespace MWMechanics return mBlock; } - bool CreatureStats::getMovementFlag (Flag flag) const + bool CreatureStats::getMovementFlag(Flag flag) const { return (mMovementFlags & flag) != 0; } - void CreatureStats::setMovementFlag (Flag flag, bool state) + void CreatureStats::setMovementFlag(Flag flag, bool state) { if (state) mMovementFlags |= flag; @@ -481,31 +520,31 @@ namespace MWMechanics switch (flag) { case Stance_Run: - return getMovementFlag (Flag_Run) || getMovementFlag (Flag_ForceRun); + return getMovementFlag(Flag_Run) || getMovementFlag(Flag_ForceRun); case Stance_Sneak: - return getMovementFlag (Flag_Sneak) || getMovementFlag (Flag_ForceSneak); + return getMovementFlag(Flag_Sneak) || getMovementFlag(Flag_ForceSneak); default: return false; } } - DrawState_ CreatureStats::getDrawState() const + DrawState CreatureStats::getDrawState() const { return mDrawState; } - void CreatureStats::setDrawState(DrawState_ state) + void CreatureStats::setDrawState(DrawState state) { mDrawState = state; } - void CreatureStats::writeState (ESM::CreatureStats& state) const + void CreatureStats::writeState(ESM::CreatureStats& state) const { - for (int i=0; i(mDrawState); state.mLevel = mLevel; state.mActorId = mActorId; state.mDeathAnimation = mDeathAnimation; state.mTimeOfDeath = mTimeOfDeath.toEsm(); - //state.mHitAttemptActorId = mHitAttemptActorId; + // state.mHitAttemptActorId = mHitAttemptActorId; mSpells.writeState(state.mSpells); mActiveSpells.writeState(state.mActiveSpells); mAiSequence.writeState(state.mAiSequence); mMagicEffects.writeState(state.mMagicEffects); - state.mSummonedCreatureMap = mSummonedCreatures; + state.mSummonedCreatures = mSummonedCreatures; state.mSummonGraveyard = mSummonGraveyard; state.mHasAiSettings = true; - for (int i=0; i<4; ++i) - mAiSettings[i].writeState (state.mAiSettings[i]); - - for (auto it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) - { - for (int i=0; ifirst].mWorsenings[i] = mCorprusSpells.at(it->first).mWorsenings[i]; + for (size_t i = 0; i < state.mAiSettings.size(); ++i) + mAiSettings[i].writeState(state.mAiSettings[i]); - state.mCorprusSpells[it->first].mNextWorsening = mCorprusSpells.at(it->first).mNextWorsening.toEsm(); - } + state.mMissingACDT = false; } - void CreatureStats::readState (const ESM::CreatureStats& state) + void CreatureStats::readState(const ESM::CreatureStats& state) { - for (int i=0; i 0) - mBoundItems.insert(effectId); - else - { - // Check active spell effects - // We can't use mActiveSpells::getMagicEffects here because it doesn't include expired effects - auto spell = std::find_if(mActiveSpells.begin(), mActiveSpells.end(), [&] (const auto& spell) - { - const auto& effects = spell.second.mEffects; - return std::find_if(effects.begin(), effects.end(), [&] (const auto& effect) - { - return effect.mEffectId == effectId; - }) != effects.end(); - }); - if(spell != mActiveSpells.end()) - mBoundItems.insert(effectId); - } - } - - mSummonedCreatures = state.mSummonedCreatureMap; + mSummonedCreatures = state.mSummonedCreatures; mSummonGraveyard = state.mSummonGraveyard; if (state.mHasAiSettings) - for (int i=0; i<4; ++i) + for (size_t i = 0; i < state.mAiSettings.size(); ++i) mAiSettings[i].readState(state.mAiSettings[i]); - - mCorprusSpells.clear(); - for (auto it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) - { - for (int i=0; ifirst].mWorsenings[i] = state.mCorprusSpells.at(it->first).mWorsenings[i]; - - mCorprusSpells[it->first].mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); - } + if (state.mRecalcDynamicStats) + recalculateMagicka(); } void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) @@ -661,15 +668,15 @@ namespace MWMechanics int CreatureStats::getActorId() { - if (mActorId==-1) + if (mActorId == -1) mActorId = sActorId++; return mActorId; } - bool CreatureStats::matchesActorId (int id) const + bool CreatureStats::matchesActorId(int id) const { - return mActorId!=-1 && id==mActorId; + return mActorId != -1 && id == mActorId; } void CreatureStats::cleanup() @@ -677,14 +684,14 @@ namespace MWMechanics sActorId = 0; } - void CreatureStats::writeActorIdCounter (ESM::ESMWriter& esm) + void CreatureStats::writeActorIdCounter(ESM::ESMWriter& esm) { esm.startRecord(ESM::REC_ACTC); esm.writeHNT("COUN", sActorId); esm.endRecord(ESM::REC_ACTC); } - void CreatureStats::readActorIdCounter (ESM::ESMReader& esm) + void CreatureStats::readActorIdCounter(ESM::ESMReader& esm) { esm.getHNT(sActorId, "COUN"); } @@ -704,7 +711,7 @@ namespace MWMechanics return mTimeOfDeath; } - std::map& CreatureStats::getSummonedCreatureMap() + std::multimap& CreatureStats::getSummonedCreatureMap() { return mSummonedCreatures; } @@ -713,23 +720,4 @@ namespace MWMechanics { return mSummonGraveyard; } - - std::map &CreatureStats::getCorprusSpells() - { - return mCorprusSpells; - } - - void CreatureStats::addCorprusSpell(const std::string& sourceId, CorprusStats& stats) - { - mCorprusSpells[sourceId] = stats; - } - - void CreatureStats::removeCorprusSpell(const std::string& sourceId) - { - auto corprusIt = mCorprusSpells.find(sourceId); - if (corprusIt != mCorprusSpells.end()) - { - mCorprusSpells.erase(corprusIt); - } - } } diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index e09f5197e12..3f7c57094c4 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -1,19 +1,22 @@ #ifndef GAME_MWMECHANICS_CREATURESTATS_H #define GAME_MWMECHANICS_CREATURESTATS_H +#include #include -#include #include +#include -#include "stat.hpp" -#include "magiceffects.hpp" -#include "spells.hpp" #include "activespells.hpp" #include "aisequence.hpp" +#include "aisetting.hpp" #include "drawstate.hpp" +#include "magiceffects.hpp" +#include "spells.hpp" +#include "stat.hpp" #include -#include +#include +#include namespace ESM { @@ -36,8 +39,8 @@ namespace MWMechanics class CreatureStats { static int sActorId; - DrawState_ mDrawState; - AttributeValue mAttributes[ESM::Attribute::Length]; + DrawState mDrawState; + std::map mAttributes; DynamicStat mDynamic[3]; // health, magicka, fatigue Spells mSpells; ActiveSpells mActiveSpells; @@ -61,10 +64,8 @@ namespace MWMechanics float mFallHeight; - std::string mLastHitObject; // The last object to hit this actor - std::string mLastHitAttemptObject; // The last object to attempt to hit this actor - - bool mRecalcMagicka; + ESM::RefId mLastHitObject; // The last object to hit this actor + ESM::RefId mLastHitAttemptObject; // The last object to attempt to hit this actor // For merchants: the last time items were restocked and gold pool refilled. MWWorld::TimeStamp mLastRestock; @@ -84,89 +85,86 @@ namespace MWMechanics // The difference between view direction and lower body direction. float mSideMovementAngle; + bool mTeleported = false; + private: - std::map mSummonedCreatures; // + std::multimap mSummonedCreatures; // // Contains ActorIds of summoned creatures with an expired lifetime that have not been deleted yet. // This may be necessary when the creature is in an inactive cell. std::vector mSummonGraveyard; - std::map mCorprusSpells; - protected: int mLevel; + bool mAttackingOrSpell; + std::string mAttackType; public: CreatureStats(); - DrawState_ getDrawState() const; - void setDrawState(DrawState_ state); + DrawState getDrawState() const; + void setDrawState(DrawState state); - bool needToRecalcDynamicStats(); - void setNeedRecalcDynamicStats(bool val); + void recalculateMagicka(); float getFallHeight() const; void addToFallHeight(float height); /// Reset the fall height /// @return total fall height - float land(bool isPlayer=false); + float land(bool isPlayer = false); - const AttributeValue & getAttribute(int index) const; + const AttributeValue& getAttribute(ESM::RefId id) const; - const DynamicStat & getHealth() const; + const DynamicStat& getHealth() const; - const DynamicStat & getMagicka() const; + const DynamicStat& getMagicka() const; - const DynamicStat & getFatigue() const; + const DynamicStat& getFatigue() const; - const DynamicStat & getDynamic (int index) const; + const DynamicStat& getDynamic(int index) const; - const Spells & getSpells() const; + const Spells& getSpells() const; - const ActiveSpells & getActiveSpells() const; + const ActiveSpells& getActiveSpells() const; - const MagicEffects & getMagicEffects() const; + const MagicEffects& getMagicEffects() const; - bool getAttackingOrSpell() const; + bool getAttackingOrSpell() const { return mAttackingOrSpell; } + std::string_view getAttackType() const { return mAttackType; } int getLevel() const; - Spells & getSpells(); + Spells& getSpells(); - ActiveSpells & getActiveSpells(); + ActiveSpells& getActiveSpells(); - MagicEffects & getMagicEffects(); + MagicEffects& getMagicEffects(); - void setAttribute(int index, const AttributeValue &value); + void setAttribute(ESM::RefId id, const AttributeValue& value); // Shortcut to set only the base - void setAttribute(int index, float base); + void setAttribute(ESM::RefId id, float base); - void setHealth(const DynamicStat &value); + void setHealth(const DynamicStat& value); - void setMagicka(const DynamicStat &value); + void setMagicka(const DynamicStat& value); - void setFatigue(const DynamicStat &value); + void setFatigue(const DynamicStat& value); - void setDynamic (int index, const DynamicStat &value); + void setDynamic(int index, const DynamicStat& value); /// Set Modifier for each magic effect according to \a effects. Does not touch Base values. - void modifyMagicEffects(const MagicEffects &effects); + void modifyMagicEffects(const MagicEffects& effects); - void setAttackingOrSpell(bool attackingOrSpell); + void setAttackingOrSpell(bool attackingOrSpell) { mAttackingOrSpell = attackingOrSpell; } + + void setAttackType(std::string_view attackType) { mAttackType = attackType; } void setLevel(int level); - enum AiSetting - { - AI_Hello = 0, - AI_Fight = 1, - AI_Flee = 2, - AI_Alarm = 3 - }; - void setAiSetting (AiSetting index, Stat value); - void setAiSetting (AiSetting index, int base); - Stat getAiSetting (AiSetting index) const; + void setAiSetting(AiSetting index, Stat value); + void setAiSetting(AiSetting index, int base); + Stat getAiSetting(AiSetting index) const; const AiSequence& getAiSequence() const; @@ -206,16 +204,18 @@ namespace MWMechanics void friendlyHit(); ///< Increase number of friendly hits by one. + void resetFriendlyHits(); + bool hasTalkedToPlayer() const; ///< Has this creature talked with the player before? void talkedToPlayer(); bool isAlarmed() const; - void setAlarmed (bool alarmed); + void setAlarmed(bool alarmed); bool getAttacked() const; - void setAttacked (bool attacked); + void setAttacked(bool attacked); float getEvasion() const; @@ -224,17 +224,18 @@ namespace MWMechanics /// including transition animations (falling down & standing up) bool getKnockedDown() const; void setKnockedDownOneFrame(bool value); - ///Returns true only for the first frame of the actor being knocked out; used for "onKnockedOut" command + /// Returns true only for the first frame of the actor being knocked out; used for "onKnockedOut" command bool getKnockedDownOneFrame() const; void setKnockedDownOverOneFrame(bool value); - ///Returns true for all but the first frame of being knocked out; used to know to not reset mKnockedDownOneFrame + /// Returns true for all but the first frame of being knocked out; used to know to not reset + /// mKnockedDownOneFrame bool getKnockedDownOverOneFrame() const; void setHitRecovery(bool value); bool getHitRecovery() const; void setBlock(bool value); bool getBlock() const; - std::map& getSummonedCreatureMap(); // + std::multimap& getSummonedCreatureMap(); // std::vector& getSummonedCreatureGraveyard(); // ActorIds enum Flag @@ -252,28 +253,26 @@ namespace MWMechanics Stance_Sneak }; - bool getMovementFlag (Flag flag) const; - void setMovementFlag (Flag flag, bool state); + bool getMovementFlag(Flag flag) const; + void setMovementFlag(Flag flag, bool state); /// Like getMovementFlag, but also takes into account if the flag is Forced - bool getStance (Stance flag) const; - - void setLastHitObject(const std::string &objectid); - const std::string &getLastHitObject() const; - void setLastHitAttemptObject(const std::string &objectid); - const std::string &getLastHitAttemptObject() const; + bool getStance(Stance flag) const; + + void setLastHitObject(const ESM::RefId& objectid); + void clearLastHitObject(); + const ESM::RefId& getLastHitObject() const; + void setLastHitAttemptObject(const ESM::RefId& objectid); + void clearLastHitAttemptObject(); + const ESM::RefId& getLastHitAttemptObject() const; void setHitAttemptActorId(const int actorId); int getHitAttemptActorId() const; - // Note, this is just a cache to avoid checking the whole container store every frame. We don't need to store it in saves. - // TODO: Put it somewhere else? - std::set mBoundItems; - - void writeState (ESM::CreatureStats& state) const; + void writeState(ESM::CreatureStats& state) const; - void readState (const ESM::CreatureStats& state); + void readState(const ESM::CreatureStats& state); - static void writeActorIdCounter (ESM::ESMWriter& esm); - static void readActorIdCounter (ESM::ESMReader& esm); + static void writeActorIdCounter(ESM::ESMWriter& esm); + static void readActorIdCounter(ESM::ESMReader& esm); void setLastRestockTime(MWWorld::TimeStamp tradeTime); MWWorld::TimeStamp getLastRestockTime() const; @@ -289,20 +288,19 @@ namespace MWMechanics int getActorId(); ///< Will generate an actor ID, if the actor does not have one yet. - bool matchesActorId (int id) const; + bool matchesActorId(int id) const; ///< Check if \a id matches the actor ID of *this (if the actor does not have an ID /// assigned this function will return false). static void cleanup(); - std::map & getCorprusSpells(); - - void addCorprusSpell(const std::string& sourceId, CorprusStats& stats); - - void removeCorprusSpell(const std::string& sourceId); - float getSideMovementAngle() const { return mSideMovementAngle; } void setSideMovementAngle(float angle) { mSideMovementAngle = angle; } + + bool wasTeleported() const { return mTeleported; } + void setTeleported(bool v) { mTeleported = v; } + + const std::map& getAttributes() const { return mAttributes; } }; } diff --git a/apps/openmw/mwmechanics/damagesourcetype.hpp b/apps/openmw/mwmechanics/damagesourcetype.hpp new file mode 100644 index 00000000000..e140a8106f9 --- /dev/null +++ b/apps/openmw/mwmechanics/damagesourcetype.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_MWMECHANICS_DAMAGESOURCETYPE_H +#define OPENMW_MWMECHANICS_DAMAGESOURCETYPE_H + +namespace MWMechanics +{ + enum class DamageSourceType + { + Unspecified, + Melee, + Ranged, + Magical, + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/difficultyscaling.cpp b/apps/openmw/mwmechanics/difficultyscaling.cpp index 23769897455..4f7d02c29de 100644 --- a/apps/openmw/mwmechanics/difficultyscaling.cpp +++ b/apps/openmw/mwmechanics/difficultyscaling.cpp @@ -1,10 +1,10 @@ #include "difficultyscaling.hpp" -#include +#include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/ptr.hpp" #include "actorutil.hpp" @@ -12,14 +12,10 @@ float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr { const MWWorld::Ptr& player = MWMechanics::getPlayer(); - // [-500, 500] - int difficultySetting = Settings::Manager::getInt("difficulty", "Game"); - difficultySetting = std::min(difficultySetting, 500); - difficultySetting = std::max(difficultySetting, -500); + static const float fDifficultyMult + = MWBase::Environment::get().getESMStore()->get().find("fDifficultyMult")->mValue.getFloat(); - static const float fDifficultyMult = MWBase::Environment::get().getWorld()->getStore().get().find("fDifficultyMult")->mValue.getFloat(); - - float difficultyTerm = 0.01f * difficultySetting; + const float difficultyTerm = 0.01f * Settings::game().mDifficulty; float x = 0; if (victim == player) diff --git a/apps/openmw/mwmechanics/disease.hpp b/apps/openmw/mwmechanics/disease.hpp index 7933c927e50..c793d5d540e 100644 --- a/apps/openmw/mwmechanics/disease.hpp +++ b/apps/openmw/mwmechanics/disease.hpp @@ -1,19 +1,21 @@ #ifndef OPENMW_MECHANICS_DISEASE_H #define OPENMW_MECHANICS_DISEASE_H +#include #include +#include -#include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwworld/ptr.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/ptr.hpp" -#include "spells.hpp" -#include "creaturestats.hpp" #include "actorutil.hpp" +#include "creaturestats.hpp" +#include "spells.hpp" namespace MWMechanics { @@ -21,46 +23,57 @@ namespace MWMechanics /// Call when \a actor has got in contact with \a carrier (e.g. hit by him, or loots him) /// @param actor The actor that will potentially catch diseases. Currently only the player can catch diseases. /// @param carrier The disease carrier. - inline void diseaseContact (MWWorld::Ptr actor, MWWorld::Ptr carrier) + inline void diseaseContact(const MWWorld::Ptr& actor, const MWWorld::Ptr& carrier) { if (!carrier.getClass().isActor() || actor != getPlayer()) return; - float fDiseaseXferChance = - MWBase::Environment::get().getWorld()->getStore().get().find( - "fDiseaseXferChance")->mValue.getFloat(); + float fDiseaseXferChance = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fDiseaseXferChance") + ->mValue.getFloat(); - MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); + const MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); Spells& spells = carrier.getClass().getCreatureStats(carrier).getSpells(); - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; if (actor.getClass().getCreatureStats(actor).getSpells().hasSpell(spell->mId)) continue; float resist = 0.f; if (Spells::hasCorprusEffect(spell)) - resist = 1.f - 0.01f * (actorEffects.get(ESM::MagicEffect::ResistCorprusDisease).getMagnitude() - - actorEffects.get(ESM::MagicEffect::WeaknessToCorprusDisease).getMagnitude()); + resist = 1.f + - 0.01f + * (actorEffects.getOrDefault(ESM::MagicEffect::ResistCorprusDisease).getMagnitude() + - actorEffects.getOrDefault(ESM::MagicEffect::WeaknessToCorprusDisease).getMagnitude()); else if (spell->mData.mType == ESM::Spell::ST_Disease) - resist = 1.f - 0.01f * (actorEffects.get(ESM::MagicEffect::ResistCommonDisease).getMagnitude() - - actorEffects.get(ESM::MagicEffect::WeaknessToCommonDisease).getMagnitude()); + resist = 1.f + - 0.01f + * (actorEffects.getOrDefault(ESM::MagicEffect::ResistCommonDisease).getMagnitude() + - actorEffects.getOrDefault(ESM::MagicEffect::WeaknessToCommonDisease).getMagnitude()); else if (spell->mData.mType == ESM::Spell::ST_Blight) - resist = 1.f - 0.01f * (actorEffects.get(ESM::MagicEffect::ResistBlightDisease).getMagnitude() - - actorEffects.get(ESM::MagicEffect::WeaknessToBlightDisease).getMagnitude()); + resist = 1.f + - 0.01f + * (actorEffects.getOrDefault(ESM::MagicEffect::ResistBlightDisease).getMagnitude() + - actorEffects.getOrDefault(ESM::MagicEffect::WeaknessToBlightDisease).getMagnitude()); else continue; int x = static_cast(fDiseaseXferChance * 100 * resist); - if (Misc::Rng::rollDice(10000) < x) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::rollDice(10000, prng) < x) { // Contracted disease! - actor.getClass().getCreatureStats(actor).getSpells().add(it->first); + actor.getClass().getCreatureStats(actor).getSpells().add(spell); MWBase::Environment::get().getWorld()->applyLoopingParticles(actor); - std::string msg = "sMagicContractDisease"; - msg = MWBase::Environment::get().getWorld()->getStore().get().find(msg)->mValue.getString(); + std::string msg = MWBase::Environment::get() + .getESMStore() + ->get() + .find("sMagicContractDisease") + ->mValue.getString(); msg = Misc::StringUtils::format(msg, spell->mName); MWBase::Environment::get().getWindowManager()->messageBox(msg); } @@ -68,5 +81,4 @@ namespace MWMechanics } } - #endif diff --git a/apps/openmw/mwmechanics/drawstate.hpp b/apps/openmw/mwmechanics/drawstate.hpp index 7f59d8d7827..1373d59ef81 100644 --- a/apps/openmw/mwmechanics/drawstate.hpp +++ b/apps/openmw/mwmechanics/drawstate.hpp @@ -3,12 +3,12 @@ namespace MWMechanics { - /// \note The _ suffix is required to avoid a collision with a Windoze macro. Die, Microsoft! Die! - enum DrawState_ + + enum class DrawState { - DrawState_Nothing = 0, - DrawState_Weapon = 1, - DrawState_Spell = 2 + Nothing = 0, + Weapon = 1, + Spell = 2 }; } diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index 1717ba06feb..7d0007f9e32 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -1,20 +1,21 @@ #include "enchanting.hpp" +#include +#include #include -#include +#include -#include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" +#include "actorutil.hpp" #include "creaturestats.hpp" #include "spellutil.hpp" -#include "actorutil.hpp" #include "weapontype.hpp" namespace MWMechanics @@ -22,30 +23,32 @@ namespace MWMechanics Enchanting::Enchanting() : mCastStyle(ESM::Enchantment::CastOnce) , mSelfEnchanting(false) + , mObjectType(0) , mWeaponType(-1) - {} + { + } void Enchanting::setOldItem(const MWWorld::Ptr& oldItem) { - mOldItemPtr=oldItem; + mOldItemPtr = oldItem; mWeaponType = -1; - mObjectType.clear(); - if(!itemEmpty()) + mObjectType = 0; + if (!itemEmpty()) { - mObjectType = mOldItemPtr.getTypeName(); - if (mObjectType == typeid(ESM::Weapon).name()) + mObjectType = mOldItemPtr.getType(); + if (mObjectType == ESM::Weapon::sRecordId) mWeaponType = mOldItemPtr.get()->mBase->mData.mType; } } void Enchanting::setNewItemName(const std::string& s) { - mNewItemName=s; + mNewItemName = s; } void Enchanting::setEffect(const ESM::EffectList& effectList) { - mEffectList=effectList; + mEffectList = effectList; } int Enchanting::getCastStyle() const @@ -55,7 +58,7 @@ namespace MWMechanics void Enchanting::setSoulGem(const MWWorld::Ptr& soulGem) { - mSoulGemPtr=soulGem; + mSoulGemPtr = soulGem; } bool Enchanting::create() @@ -66,26 +69,30 @@ namespace MWMechanics enchantment.mData.mFlags = 0; enchantment.mData.mType = mCastStyle; enchantment.mData.mCost = getBaseCastCost(); + enchantment.mRecordFlags = 0; - store.remove(mSoulGemPtr, 1, player); + store.remove(mSoulGemPtr, 1); - //Exception for Azura Star, new one will be added after enchanting - if(Misc::StringUtils::ciEqual(mSoulGemPtr.get()->mBase->mId, "Misc_SoulGem_Azura")) - store.add("Misc_SoulGem_Azura", 1, player); + // Exception for Azura Star, new one will be added after enchanting + auto azurasStarId = ESM::RefId::stringRefId("Misc_SoulGem_Azura"); + if (mSoulGemPtr.get()->mBase->mId == azurasStarId) + store.add(azurasStarId, 1); - if(mSelfEnchanting) + if (mSelfEnchanting) { - if(getEnchantChance() <= (Misc::Rng::roll0to99())) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (getEnchantChance() <= (Misc::Rng::roll0to99(prng))) return false; - mEnchanter.getClass().skillUsageSucceeded (mEnchanter, ESM::Skill::Enchant, 2); + mEnchanter.getClass().skillUsageSucceeded( + mEnchanter, ESM::Skill::Enchant, ESM::Skill::Enchant_CreateMagicItem); } enchantment.mEffects = mEffectList; int count = getEnchantItemsCount(); - if(mCastStyle==ESM::Enchantment::ConstantEffect) + if (mCastStyle == ESM::Enchantment::ConstantEffect) enchantment.mData.mCharge = 0; else enchantment.mData.mCharge = getGemCharge() / count; @@ -93,31 +100,35 @@ namespace MWMechanics // Try to find a dynamic enchantment with the same stats, create a new one if not found. const ESM::Enchantment* enchantmentPtr = getRecord(enchantment); if (enchantmentPtr == nullptr) - enchantmentPtr = MWBase::Environment::get().getWorld()->createRecord (enchantment); + enchantmentPtr = MWBase::Environment::get().getESMStore()->insert(enchantment); // Apply the enchantment - std::string newItemId = mOldItemPtr.getClass().applyEnchantment(mOldItemPtr, enchantmentPtr->mId, getGemCharge(), mNewItemName); + const ESM::RefId& newItemId + = mOldItemPtr.getClass().applyEnchantment(mOldItemPtr, enchantmentPtr->mId, getGemCharge(), mNewItemName); - // Add the new item to player inventory and remove the old one - store.remove(mOldItemPtr, count, player); - store.add(newItemId, count, player); + if (!mSelfEnchanting) + payForEnchantment(count); - if(!mSelfEnchanting) - payForEnchantment(); + // Add the new item to player inventory and remove the old one + store.remove(mOldItemPtr, count); + store.add(newItemId, count); return true; } - + void Enchanting::nextCastStyle() { if (itemEmpty()) return; - const bool powerfulSoul = getGemCharge() >= \ - MWBase::Environment::get().getWorld()->getStore().get().find ("iSoulAmountForConstantEffect")->mValue.getInteger(); - if ((mObjectType == typeid(ESM::Armor).name()) || (mObjectType == typeid(ESM::Clothing).name())) + const bool powerfulSoul = getGemCharge() >= MWBase::Environment::get() + .getESMStore() + ->get() + .find("iSoulAmountForConstantEffect") + ->mValue.getInteger(); + if ((mObjectType == ESM::Armor::sRecordId) || (mObjectType == ESM::Clothing::sRecordId)) { // Armor or Clothing - switch(mCastStyle) + switch (mCastStyle) { case ESM::Enchantment::WhenUsed: if (powerfulSoul) @@ -131,7 +142,7 @@ namespace MWMechanics else if (mWeaponType != -1) { // Weapon ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; - switch(mCastStyle) + switch (mCastStyle) { case ESM::Enchantment::WhenStrikes: if (weapclass == ESM::WeaponType::Melee || weapclass == ESM::WeaponType::Ranged) @@ -150,7 +161,7 @@ namespace MWMechanics return; } } - else if(mObjectType == typeid(ESM::Book).name()) + else if (mObjectType == ESM::Book::sRecordId) { // Scroll or Book mCastStyle = ESM::Enchantment::CastOnce; return; @@ -180,19 +191,20 @@ namespace MWMechanics // No effects added, cost = 0 return 0; - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + 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(); + const float fEnchantmentConstantDurationMult + = store.get().find("fEnchantmentConstantDurationMult")->mValue.getFloat(); float enchantmentCost = 0.f; float cost = 0.f; - for (const ESM::ENAMstruct& effect : mEffectList.mList) + for (const ESM::IndexedENAMstruct& effect : mEffectList.mList) { - float baseCost = (store.get().find(effect.mEffectID))->mData.mBaseCost; - int magMin = std::max(1, effect.mMagnMin); - int magMax = std::max(1, effect.mMagnMax); - int area = std::max(1, effect.mArea); - float duration = static_cast(effect.mDuration); + float baseCost = (store.get().find(effect.mData.mEffectID))->mData.mBaseCost; + int magMin = std::max(1, effect.mData.mMagnMin); + int magMax = std::max(1, effect.mData.mMagnMax); + int area = std::max(1, effect.mData.mArea); + float duration = static_cast(effect.mData.mDuration); if (mCastStyle == ESM::Enchantment::ConstantEffect) duration = fEnchantmentConstantDurationMult; @@ -200,7 +212,7 @@ namespace MWMechanics cost = std::max(1.f, cost); - if (effect.mRange == ESM::RT_Target) + if (effect.mData.mRange == ESM::RT_Target) cost *= 1.5f; enchantmentCost += precise ? cost : std::floor(cost); @@ -211,18 +223,17 @@ namespace MWMechanics const ESM::Enchantment* Enchanting::getRecord(const ESM::Enchantment& toFind) const { - const MWWorld::Store& enchantments = MWBase::Environment::get().getWorld()->getStore().get(); - MWWorld::Store::iterator iter (enchantments.begin()); + const MWWorld::Store& enchantments + = MWBase::Environment::get().getESMStore()->get(); + MWWorld::Store::iterator iter(enchantments.begin()); iter += (enchantments.getSize() - enchantments.getDynamicSize()); for (; iter != enchantments.end(); ++iter) { if (iter->mEffects.mList.size() != toFind.mEffects.mList.size()) continue; - if (iter->mData.mFlags != toFind.mData.mFlags - || iter->mData.mType != toFind.mData.mType - || iter->mData.mCost != toFind.mData.mCost - || iter->mData.mCharge != toFind.mData.mCharge) + if (iter->mData.mFlags != toFind.mData.mFlags || iter->mData.mType != toFind.mData.mType + || iter->mData.mCost != toFind.mData.mCost || iter->mData.mCharge != toFind.mData.mCharge) continue; // Don't choose an ID that came from the content files, would have unintended side effects @@ -231,19 +242,9 @@ namespace MWMechanics bool mismatch = false; - for (int i=0; i (iter->mEffects.mList.size()); ++i) + for (int i = 0; i < static_cast(iter->mEffects.mList.size()); ++i) { - const ESM::ENAMstruct& first = iter->mEffects.mList[i]; - const ESM::ENAMstruct& second = toFind.mEffects.mList[i]; - - if (first.mEffectID!=second.mEffectID || - first.mArea!=second.mArea || - first.mRange!=second.mRange || - first.mSkill!=second.mSkill || - first.mAttribute!=second.mAttribute || - first.mMagnMin!=second.mMagnMin || - first.mMagnMax!=second.mMagnMax || - first.mDuration!=second.mDuration) + if (iter->mEffects.mList[i] != toFind.mEffects.mList[i]) { mismatch = true; break; @@ -272,27 +273,31 @@ namespace MWMechanics return getEffectiveEnchantmentCastCost(static_cast(baseCost), player); } - - int Enchanting::getEnchantPrice() const + int Enchanting::getEnchantPrice(int count) const { - if(mEnchanter.isEmpty()) + if (mEnchanter.isEmpty()) return 0; - float priceMultipler = MWBase::Environment::get().getWorld()->getStore().get().find ("fEnchantmentValueMult")->mValue.getFloat(); - int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mEnchanter, static_cast(getEnchantPoints() * priceMultipler), true); - price *= getEnchantItemsCount() * getTypeMultiplier(); + float priceMultipler = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fEnchantmentValueMult") + ->mValue.getFloat(); + int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer( + mEnchanter, static_cast(getEnchantPoints() * priceMultipler), true); + price *= count * getTypeMultiplier(); return std::max(1, price); } int Enchanting::getGemCharge() const { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - if(soulEmpty()) + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + if (soulEmpty()) return 0; - if(mSoulGemPtr.getCellRef().getSoul()=="") + if (mSoulGemPtr.getCellRef().getSoul().empty()) return 0; const ESM::Creature* soul = store.get().search(mSoulGemPtr.getCellRef().getSoul()); - if(soul) + if (soul) return soul->mData.mSoul; else return 0; @@ -303,9 +308,10 @@ namespace MWMechanics if (itemEmpty()) return 0; - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - return static_cast(mOldItemPtr.getClass().getEnchantmentPoints(mOldItemPtr) * store.get().find("fEnchantmentMult")->mValue.getFloat()); + return static_cast(mOldItemPtr.getClass().getEnchantmentPoints(mOldItemPtr) + * store.get().find("fEnchantmentMult")->mValue.getFloat()); } bool Enchanting::soulEmpty() const { @@ -332,14 +338,17 @@ namespace MWMechanics int Enchanting::getEnchantChance() const { const CreatureStats& stats = mEnchanter.getClass().getCreatureStats(mEnchanter); - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); const float a = static_cast(mEnchanter.getClass().getSkill(mEnchanter, ESM::Skill::Enchant)); - const float b = static_cast(stats.getAttribute (ESM::Attribute::Intelligence).getModified()); - const float c = static_cast(stats.getAttribute (ESM::Attribute::Luck).getModified()); + const float b = static_cast(stats.getAttribute(ESM::Attribute::Intelligence).getModified()); + const float c = static_cast(stats.getAttribute(ESM::Attribute::Luck).getModified()); const float fEnchantmentChanceMult = gmst.find("fEnchantmentChanceMult")->mValue.getFloat(); const float fEnchantmentConstantChanceMult = gmst.find("fEnchantmentConstantChanceMult")->mValue.getFloat(); - float x = (a - getEnchantPoints() * fEnchantmentChanceMult * getTypeMultiplier() * getEnchantItemsCount() + 0.2f * b + 0.1f * c) * stats.getFatigueTerm(); + float x = (a - getEnchantPoints() * fEnchantmentChanceMult * getTypeMultiplier() * getEnchantItemsCount() + + 0.2f * b + 0.1f * c) + * stats.getFatigueTerm(); if (mCastStyle == ESM::Enchantment::ConstantEffect) x *= fEnchantmentConstantChanceMult; @@ -355,10 +364,10 @@ namespace MWMechanics ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) { - static const float multiplier = std::max(0.f, std::min(1.0f, Settings::Manager::getFloat("projectiles enchant multiplier", "Game"))); MWWorld::Ptr player = getPlayer(); - int itemsInInventoryCount = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); - count = std::min(itemsInInventoryCount, std::max(1, int(getGemCharge() * multiplier / enchantPoints))); + count = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); + count = std::clamp( + getGemCharge() * Settings::game().mProjectilesEnchantMultiplier / enchantPoints, 1, count); } } @@ -367,8 +376,7 @@ namespace MWMechanics float Enchanting::getTypeMultiplier() const { - static const bool useMultiplier = Settings::Manager::getFloat("projectiles enchant multiplier", "Game") > 0; - if (useMultiplier && mWeaponType != -1 && getEnchantPoints() > 0) + if (Settings::game().mProjectilesEnchantMultiplier > 0 && mWeaponType != -1 && getEnchantPoints() > 0) { ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) @@ -378,15 +386,16 @@ namespace MWMechanics return 1.f; } - void Enchanting::payForEnchantment() const + void Enchanting::payForEnchantment(int count) const { const MWWorld::Ptr& player = getPlayer(); MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); - store.remove(MWWorld::ContainerStore::sGoldId, getEnchantPrice(), player); + int price = getEnchantPrice(count); + store.remove(MWWorld::ContainerStore::sGoldId, price); // add gold to NPC trading gold pool CreatureStats& enchanterStats = mEnchanter.getClass().getCreatureStats(mEnchanter); - enchanterStats.setGoldPool(enchanterStats.getGoldPool() + getEnchantPrice()); + enchanterStats.setGoldPool(enchanterStats.getGoldPool() + price); } } diff --git a/apps/openmw/mwmechanics/enchanting.hpp b/apps/openmw/mwmechanics/enchanting.hpp index 33a28209380..5db02b8cbaa 100644 --- a/apps/openmw/mwmechanics/enchanting.hpp +++ b/apps/openmw/mwmechanics/enchanting.hpp @@ -3,8 +3,8 @@ #include -#include -#include +#include +#include #include "../mwworld/ptr.hpp" @@ -12,47 +12,49 @@ namespace MWMechanics { class Enchanting { - MWWorld::Ptr mOldItemPtr; - MWWorld::Ptr mSoulGemPtr; - MWWorld::Ptr mEnchanter; - - int mCastStyle; - - bool mSelfEnchanting; - - ESM::EffectList mEffectList; - - std::string mNewItemName; - std::string mObjectType; - int mWeaponType; - - const ESM::Enchantment* getRecord(const ESM::Enchantment& newEnchantment) const; - - public: - Enchanting(); - void setEnchanter(const MWWorld::Ptr& enchanter); - void setSelfEnchanting(bool selfEnchanting); - void setOldItem(const MWWorld::Ptr& oldItem); - MWWorld::Ptr getOldItem() { return mOldItemPtr; } - MWWorld::Ptr getGem() { return mSoulGemPtr; } - void setNewItemName(const std::string& s); - void setEffect(const ESM::EffectList& effectList); - void setSoulGem(const MWWorld::Ptr& soulGem); - bool create(); //Return true if created, false if failed. - void nextCastStyle(); //Set enchant type to next possible type (for mOldItemPtr object) - int getCastStyle() const; - float getEnchantPoints(bool precise = true) const; - int getBaseCastCost() const; // To be saved in the enchantment's record - int getEffectiveCastCost() const; // Effective cost taking player Enchant skill into account, used for preview purposes in the UI - int getEnchantPrice() const; - int getMaxEnchantValue() const; - int getGemCharge() const; - int getEnchantChance() const; - int getEnchantItemsCount() const; - float getTypeMultiplier() const; - bool soulEmpty() const; //Return true if empty - bool itemEmpty() const; //Return true if empty - void payForEnchantment() const; + MWWorld::Ptr mOldItemPtr; + MWWorld::Ptr mSoulGemPtr; + MWWorld::Ptr mEnchanter; + + int mCastStyle; + + bool mSelfEnchanting; + + ESM::EffectList mEffectList; + + std::string mNewItemName; + unsigned int mObjectType; + int mWeaponType; + + const ESM::Enchantment* getRecord(const ESM::Enchantment& newEnchantment) const; + int getBaseCastCost() const; // To be saved in the enchantment's record + int getEnchantItemsCount() const; + float getTypeMultiplier() const; + void payForEnchantment(int count) const; + int getEnchantPrice(int count) const; + + public: + Enchanting(); + void setEnchanter(const MWWorld::Ptr& enchanter); + void setSelfEnchanting(bool selfEnchanting); + void setOldItem(const MWWorld::Ptr& oldItem); + MWWorld::Ptr getOldItem() { return mOldItemPtr; } + MWWorld::Ptr getGem() { return mSoulGemPtr; } + void setNewItemName(const std::string& s); + void setEffect(const ESM::EffectList& effectList); + void setSoulGem(const MWWorld::Ptr& soulGem); + bool create(); // Return true if created, false if failed. + void nextCastStyle(); // Set enchant type to next possible type (for mOldItemPtr object) + int getCastStyle() const; + float getEnchantPoints(bool precise = true) const; + int getEffectiveCastCost() + const; // Effective cost taking player Enchant skill into account, used for preview purposes in the UI + int getEnchantPrice() const { return getEnchantPrice(getEnchantItemsCount()); } + int getMaxEnchantValue() const; + int getGemCharge() const; + int getEnchantChance() const; + bool soulEmpty() const; // Return true if empty + bool itemEmpty() const; // Return true if empty }; } #endif diff --git a/apps/openmw/mwmechanics/greetingstate.hpp b/apps/openmw/mwmechanics/greetingstate.hpp new file mode 100644 index 00000000000..9b37096322d --- /dev/null +++ b/apps/openmw/mwmechanics/greetingstate.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_MWMECHANICS_GREETINGSTATE_H +#define OPENMW_MWMECHANICS_GREETINGSTATE_H + +namespace MWMechanics +{ + enum GreetingState + { + Greet_None, + Greet_InProgress, + Greet_Done + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/inventory.hpp b/apps/openmw/mwmechanics/inventory.hpp new file mode 100644 index 00000000000..2c593566e1f --- /dev/null +++ b/apps/openmw/mwmechanics/inventory.hpp @@ -0,0 +1,41 @@ +#ifndef OPENMW_MWMECHANICS_INVENTORY_H +#define OPENMW_MWMECHANICS_INVENTORY_H + +#include "../mwbase/environment.hpp" + +#include "../mwworld/esmstore.hpp" + +#include +#include + +#include +#include + +namespace MWMechanics +{ + template + void modifyBaseInventory(const ESM::RefId& actorId, const ESM::RefId& itemId, int amount) + { + T copy = *MWBase::Environment::get().getESMStore()->get().find(actorId); + for (auto& it : copy.mInventory.mList) + { + if (it.mItem == itemId) + { + const int sign = it.mCount < 1 ? -1 : 1; + it.mCount = sign * std::max(it.mCount * sign + amount, 0); + MWBase::Environment::get().getESMStore()->overrideRecord(copy); + return; + } + } + if (amount > 0) + { + ESM::ContItem cont; + cont.mItem = itemId; + cont.mCount = amount; + copy.mInventory.mList.push_back(cont); + MWBase::Environment::get().getESMStore()->overrideRecord(copy); + } + } +} + +#endif diff --git a/apps/openmw/mwmechanics/levelledlist.cpp b/apps/openmw/mwmechanics/levelledlist.cpp new file mode 100644 index 00000000000..26f759425c5 --- /dev/null +++ b/apps/openmw/mwmechanics/levelledlist.cpp @@ -0,0 +1,81 @@ +#include +#include + +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/manualref.hpp" +#include "../mwworld/ptr.hpp" + +#include "../mwbase/environment.hpp" + +#include "actorutil.hpp" +#include "creaturestats.hpp" +#include "levelledlist.hpp" + +namespace MWMechanics +{ + + ESM::RefId getLevelledItem( + const ESM::LevelledListBase* levItem, bool creature, Misc::Rng::Generator& prng, std::optional level) + { + const std::vector& items = levItem->mList; + + int playerLevel; + if (level.has_value()) + playerLevel = *level; + else + { + const MWWorld::Ptr& player = getPlayer(); + playerLevel = player.getClass().getCreatureStats(player).getLevel(); + level = playerLevel; + } + + if (Misc::Rng::roll0to99(prng) < levItem->mChanceNone) + return ESM::RefId(); + + std::vector candidates; + int highestLevel = 0; + for (const auto& levelledItem : items) + { + if (levelledItem.mLevel > highestLevel && levelledItem.mLevel <= playerLevel) + highestLevel = levelledItem.mLevel; + } + + // For levelled creatures, the flags are swapped. This file format just makes so much sense. + bool allLevels = (levItem->mFlags & ESM::ItemLevList::AllLevels) != 0; + if (creature) + allLevels = levItem->mFlags & ESM::CreatureLevList::AllLevels; + + for (const auto& levelledItem : items) + { + if (playerLevel >= levelledItem.mLevel && (allLevels || levelledItem.mLevel == highestLevel)) + candidates.push_back(&levelledItem.mId); + } + if (candidates.empty()) + return ESM::RefId(); + const ESM::RefId& item = *candidates[Misc::Rng::rollDice(candidates.size(), prng)]; + + // Vanilla doesn't fail on nonexistent items in levelled lists + if (!MWBase::Environment::get().getESMStore()->find(item)) + { + Log(Debug::Warning) << "Warning: ignoring nonexistent item " << item << " in levelled list " + << levItem->mId; + return ESM::RefId(); + } + + // Is this another levelled item or a real item? + MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), item, 1); + if (ref.getPtr().getType() != ESM::ItemLevList::sRecordId + && ref.getPtr().getType() != ESM::CreatureLevList::sRecordId) + { + return item; + } + else + { + if (ref.getPtr().getType() == ESM::ItemLevList::sRecordId) + return getLevelledItem(ref.getPtr().get()->mBase, false, prng, level); + else + return getLevelledItem(ref.getPtr().get()->mBase, true, prng, level); + } + } +} diff --git a/apps/openmw/mwmechanics/levelledlist.hpp b/apps/openmw/mwmechanics/levelledlist.hpp index c8368101a7d..5a739dd4131 100644 --- a/apps/openmw/mwmechanics/levelledlist.hpp +++ b/apps/openmw/mwmechanics/levelledlist.hpp @@ -1,84 +1,22 @@ #ifndef OPENMW_MECHANICS_LEVELLEDLIST_H #define OPENMW_MECHANICS_LEVELLEDLIST_H -#include #include -#include "../mwworld/ptr.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/manualref.hpp" -#include "../mwworld/class.hpp" +#include -#include "../mwbase/world.hpp" -#include "../mwbase/environment.hpp" - -#include "creaturestats.hpp" -#include "actorutil.hpp" +namespace ESM +{ + struct LevelledListBase; + class RefId; +} namespace MWMechanics { /// @return ID of resulting item, or empty if none - inline std::string getLevelledItem (const ESM::LevelledListBase* levItem, bool creature, Misc::Rng::Seed& seed = Misc::Rng::getSeed()) - { - const std::vector& items = levItem->mList; - - const MWWorld::Ptr& player = getPlayer(); - int playerLevel = player.getClass().getCreatureStats(player).getLevel(); - - if (Misc::Rng::roll0to99(seed) < levItem->mChanceNone) - return std::string(); - - std::vector candidates; - int highestLevel = 0; - for (const auto& levelledItem : items) - { - if (levelledItem.mLevel > highestLevel && levelledItem.mLevel <= playerLevel) - highestLevel = levelledItem.mLevel; - } - - // For levelled creatures, the flags are swapped. This file format just makes so much sense. - bool allLevels = (levItem->mFlags & ESM::ItemLevList::AllLevels) != 0; - if (creature) - allLevels = levItem->mFlags & ESM::CreatureLevList::AllLevels; - - std::pair highest = std::make_pair(-1, ""); - for (const auto& levelledItem : items) - { - if (playerLevel >= levelledItem.mLevel - && (allLevels || levelledItem.mLevel == highestLevel)) - { - candidates.push_back(levelledItem.mId); - if (levelledItem.mLevel >= highest.first) - highest = std::make_pair(levelledItem.mLevel, levelledItem.mId); - } - } - if (candidates.empty()) - return std::string(); - std::string item = candidates[Misc::Rng::rollDice(candidates.size(), seed)]; - - // Vanilla doesn't fail on nonexistent items in levelled lists - if (!MWBase::Environment::get().getWorld()->getStore().find(Misc::StringUtils::lowerCase(item))) - { - Log(Debug::Warning) << "Warning: ignoring nonexistent item '" << item << "' in levelled list '" << levItem->mId << "'"; - return std::string(); - } - - // Is this another levelled item or a real item? - MWWorld::ManualRef ref (MWBase::Environment::get().getWorld()->getStore(), item, 1); - if (ref.getPtr().getTypeName() != typeid(ESM::ItemLevList).name() - && ref.getPtr().getTypeName() != typeid(ESM::CreatureLevList).name()) - { - return item; - } - else - { - if (ref.getPtr().getTypeName() == typeid(ESM::ItemLevList).name()) - return getLevelledItem(ref.getPtr().get()->mBase, false, seed); - else - return getLevelledItem(ref.getPtr().get()->mBase, true, seed); - } - } + ESM::RefId getLevelledItem( + const ESM::LevelledListBase* levItem, bool creature, Misc::Rng::Generator& prng, std::optional level = {}); } diff --git a/apps/openmw/mwmechanics/linkedeffects.cpp b/apps/openmw/mwmechanics/linkedeffects.cpp deleted file mode 100644 index b0defac7d20..00000000000 --- a/apps/openmw/mwmechanics/linkedeffects.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "linkedeffects.hpp" - -#include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwrender/animation.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" - -#include "creaturestats.hpp" - -namespace MWMechanics -{ - - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects) - { - if (caster.isEmpty() || caster == target || !target.getClass().isActor()) - return false; - - bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; - bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable; - if (!isHarmful || isUnreflectable) - return false; - - float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); - if (Misc::Rng::roll0to99() >= reflect) - return false; - - const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Reflect"); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation && !reflectStatic->mModel.empty()) - animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); - reflectedEffects.mList.emplace_back(effect); - return true; - } - - void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source) - { - if (caster.isEmpty() || caster == target) - return; - - if (!target.getClass().isActor() || !caster.getClass().isActor()) - return; - - // Make sure callers don't do something weird - if (effect.mEffectID < ESM::MagicEffect::AbsorbAttribute || effect.mEffectID > ESM::MagicEffect::AbsorbSkill) - throw std::runtime_error("invalid absorb stat effect"); - - if (appliedEffect.mMagnitude == 0) - return; - - std::vector absorbEffects; - ActiveSpells::ActiveEffect absorbEffect = appliedEffect; - absorbEffect.mMagnitude *= -1; - absorbEffect.mEffectIndex = appliedEffect.mEffectIndex; - absorbEffects.emplace_back(absorbEffect); - - // Morrowind negates reflected Absorb spells so the original caster won't be harmed. - if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game")) - { - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(std::string(), true, - absorbEffects, source, caster.getClass().getCreatureStats(caster).getActorId()); - return; - } - - caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(std::string(), true, - absorbEffects, source, target.getClass().getCreatureStats(target).getActorId()); - } -} diff --git a/apps/openmw/mwmechanics/linkedeffects.hpp b/apps/openmw/mwmechanics/linkedeffects.hpp deleted file mode 100644 index a6dea2a3a2b..00000000000 --- a/apps/openmw/mwmechanics/linkedeffects.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef MWMECHANICS_LINKEDEFFECTS_H -#define MWMECHANICS_LINKEDEFFECTS_H - -#include - -namespace ESM -{ - struct ActiveEffect; - struct EffectList; - struct ENAMstruct; - struct MagicEffect; - struct Spell; -} - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - - // Try to reflect a spell effect. If it's reflected, it's also put into the passed reflected effects list. - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects); - - // Try to absorb a stat (skill, attribute, etc.) from the target and transfer it to the caster. - void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source); -} - -#endif diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index 1a1a44f638b..c2afef7c0d0 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -1,41 +1,72 @@ #include "magiceffects.hpp" +#include #include -#include -#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" + +#include "../mwworld/esmstore.hpp" + +namespace +{ + // Round value to prevent precision issues + void truncate(float& value) + { + value = static_cast(value * 1024.f) / 1024.f; + } +} namespace MWMechanics { - EffectKey::EffectKey() : mId (0), mArg (-1) {} + EffectKey::EffectKey() + : mId(0) + { + } - EffectKey::EffectKey (const ESM::ENAMstruct& effect) + EffectKey::EffectKey(const ESM::ENAMstruct& effect) { mId = effect.mEffectID; - mArg = -1; + mArg = ESM::Skill::indexToRefId(effect.mSkill); - if (effect.mSkill!=-1) - mArg = effect.mSkill; - - if (effect.mAttribute!=-1) + ESM::RefId attribute = ESM::Attribute::indexToRefId(effect.mAttribute); + if (!attribute.empty()) { - if (mArg!=-1) - throw std::runtime_error ( - "magic effect can't have both a skill and an attribute argument"); + if (!mArg.empty()) + throw std::runtime_error("magic effect can't have both a skill and an attribute argument"); - mArg = effect.mAttribute; + mArg = attribute; } } - bool operator< (const EffectKey& left, const EffectKey& right) + std::string EffectKey::toString() const + { + const auto& store = MWBase::Environment::get().getESMStore(); + const ESM::MagicEffect* magicEffect = store->get().find(mId); + return getMagicEffectString( + *magicEffect, store->get().search(mArg), store->get().search(mArg)); + } + + bool operator<(const EffectKey& left, const EffectKey& right) { - if (left.mIdright.mId) + if (left.mId > right.mId) return false; - return left.mArgsecond.setModifier(effects.get(it->first).getModifier()); + it->second.setModifier(effects.getOrDefault(it->first).getModifier()); } for (Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) @@ -121,94 +158,130 @@ namespace MWMechanics } } - MagicEffects& MagicEffects::operator+= (const MagicEffects& effects) + EffectParam MagicEffects::getOrDefault(const EffectKey& key) const { - if (this==&effects) - { - MagicEffects temp (effects); - *this += temp; - return *this; - } - - for (Collection::const_iterator iter (effects.begin()); iter!=effects.end(); ++iter) - { - Collection::iterator result = mCollection.find (iter->first); - - if (result!=mCollection.end()) - result->second += iter->second; - else - mCollection.insert (*iter); - } - - return *this; + return get(key).value_or(EffectParam()); } - EffectParam MagicEffects::get (const EffectKey& key) const + std::optional MagicEffects::get(const EffectKey& key) const { - Collection::const_iterator iter = mCollection.find (key); + Collection::const_iterator iter = mCollection.find(key); - if (iter==mCollection.end()) - { - return EffectParam(); - } - else + if (iter != mCollection.end()) { return iter->second; } + return std::nullopt; } - MagicEffects MagicEffects::diff (const MagicEffects& prev, const MagicEffects& now) + MagicEffects MagicEffects::diff(const MagicEffects& prev, const MagicEffects& now) { MagicEffects result; // adding/changing - for (Collection::const_iterator iter (now.begin()); iter!=now.end(); ++iter) + for (Collection::const_iterator iter(now.begin()); iter != now.end(); ++iter) { - Collection::const_iterator other = prev.mCollection.find (iter->first); + Collection::const_iterator other = prev.mCollection.find(iter->first); - if (other==prev.end()) + if (other == prev.end()) { // adding - result.add (iter->first, iter->second); + result.add(iter->first, iter->second); } else { // changing - result.add (iter->first, iter->second - other->second); + result.add(iter->first, iter->second - other->second); } } // removing - for (Collection::const_iterator iter (prev.begin()); iter!=prev.end(); ++iter) + for (Collection::const_iterator iter(prev.begin()); iter != prev.end(); ++iter) { - Collection::const_iterator other = now.mCollection.find (iter->first); - if (other==now.end()) + Collection::const_iterator other = now.mCollection.find(iter->first); + if (other == now.end()) { - result.add (iter->first, EffectParam() - iter->second); + result.add(iter->first, EffectParam() - iter->second); } } return result; } - void MagicEffects::writeState(ESM::MagicEffects &state) const + void MagicEffects::writeState(ESM::MagicEffects& state) const { - // Don't need to save Modifiers, they are recalculated every frame anyway. - for (Collection::const_iterator iter (begin()); iter!=end(); ++iter) + for (const auto& [key, params] : mCollection) { - if (iter->second.getBase() != 0) + if (params.getBase() != 0 || params.getModifier() != 0.f) { // Don't worry about mArg, never used by magic effect script instructions - state.mEffects.insert(std::make_pair(iter->first.mId, iter->second.getBase())); + state.mEffects[key.mId] = { params.getBase(), params.getModifier() }; } } } - void MagicEffects::readState(const ESM::MagicEffects &state) + void MagicEffects::readState(const ESM::MagicEffects& state) { - for (std::map::const_iterator it = state.mEffects.begin(); it != state.mEffects.end(); ++it) + for (const auto& [key, params] : state.mEffects) + { + mCollection[EffectKey(key)].setBase(params.first); + mCollection[EffectKey(key)].setModifier(params.second); + } + } + + std::string getMagicEffectString( + const ESM::MagicEffect& effect, const ESM::Attribute* attribute, const ESM::Skill* skill) + { + const bool targetsSkill = effect.mData.mFlags & ESM::MagicEffect::TargetSkill && skill; + const bool targetsAttribute = effect.mData.mFlags & ESM::MagicEffect::TargetAttribute && attribute; + + std::string spellLine; + + auto windowManager = MWBase::Environment::get().getWindowManager(); + + if (targetsSkill || targetsAttribute) + { + switch (effect.mIndex) + { + case ESM::MagicEffect::AbsorbAttribute: + case ESM::MagicEffect::AbsorbSkill: + spellLine = windowManager->getGameSettingString("sAbsorb", {}); + break; + case ESM::MagicEffect::DamageAttribute: + case ESM::MagicEffect::DamageSkill: + spellLine = windowManager->getGameSettingString("sDamage", {}); + break; + case ESM::MagicEffect::DrainAttribute: + case ESM::MagicEffect::DrainSkill: + spellLine = windowManager->getGameSettingString("sDrain", {}); + break; + case ESM::MagicEffect::FortifyAttribute: + case ESM::MagicEffect::FortifySkill: + spellLine = windowManager->getGameSettingString("sFortify", {}); + break; + case ESM::MagicEffect::RestoreAttribute: + case ESM::MagicEffect::RestoreSkill: + spellLine = windowManager->getGameSettingString("sRestore", {}); + break; + } + } + + if (spellLine.empty()) + { + auto& effectIDStr = ESM::MagicEffect::indexToGmstString(effect.mIndex); + spellLine = windowManager->getGameSettingString(effectIDStr, {}); + } + + if (targetsSkill) + { + spellLine += ' '; + spellLine += skill->mName; + } + else if (targetsAttribute) { - mCollection[EffectKey(it->first)].setBase(it->second); + spellLine += ' '; + spellLine += attribute->mName; } + return spellLine; } } diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index 12735a87fc2..4fe5d9fd4e9 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -2,14 +2,19 @@ #define GAME_MWMECHANICS_MAGICEFFECTS_H #include +#include #include +#include + namespace ESM { + struct Attribute; struct ENAMstruct; struct EffectList; - + struct MagicEffect; struct MagicEffects; + struct Skill; } namespace MWMechanics @@ -17,16 +22,23 @@ namespace MWMechanics struct EffectKey { int mId; - int mArg; // skill or ability + ESM::RefId mArg; // skill or ability EffectKey(); - EffectKey (int id, int arg = -1) : mId (id), mArg (arg) {} + EffectKey(int id, ESM::RefId arg = {}) + : mId(id) + , mArg(arg) + { + } - EffectKey (const ESM::ENAMstruct& effect); + EffectKey(const ESM::ENAMstruct& effect); + + std::string toString() const; }; - bool operator< (const EffectKey& left, const EffectKey& right); + bool operator<(const EffectKey& left, const EffectKey& right); + bool operator==(const EffectKey& left, const EffectKey& right); struct EffectParam { @@ -50,71 +62,64 @@ namespace MWMechanics EffectParam(); - EffectParam(float magnitude) : mModifier(magnitude), mBase(0) {} + EffectParam(float magnitude) + : mModifier(magnitude) + , mBase(0) + { + } - EffectParam& operator+= (const EffectParam& param); + EffectParam& operator+=(const EffectParam& param); - EffectParam& operator-= (const EffectParam& param); + EffectParam& operator-=(const EffectParam& param); }; - inline EffectParam operator+ (const EffectParam& left, const EffectParam& right) + inline EffectParam operator+(const EffectParam& left, const EffectParam& right) { - EffectParam param (left); + EffectParam param(left); return param += right; } - inline EffectParam operator- (const EffectParam& left, const EffectParam& right) + inline EffectParam operator-(const EffectParam& left, const EffectParam& right) { - EffectParam param (left); + EffectParam param(left); return param -= right; } - // Used by effect management classes (ActiveSpells, InventoryStore, Spells) to list active effect sources for GUI display - struct EffectSourceVisitor - { - virtual ~EffectSourceVisitor() { } - - virtual void visit (EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) = 0; - }; - /// \brief Effects currently affecting a NPC or creature class MagicEffects { - public: - - typedef std::map Collection; - - private: - - Collection mCollection; - - public: + public: + typedef std::map Collection; - Collection::const_iterator begin() const { return mCollection.begin(); } + private: + Collection mCollection; - Collection::const_iterator end() const { return mCollection.end(); } + public: + Collection::const_iterator begin() const { return mCollection.begin(); } - void readState (const ESM::MagicEffects& state); - void writeState (ESM::MagicEffects& state) const; + Collection::const_iterator end() const { return mCollection.end(); } - void add (const EffectKey& key, const EffectParam& param); - void remove (const EffectKey& key); + void readState(const ESM::MagicEffects& state); + void writeState(ESM::MagicEffects& state) const; - void modifyBase (const EffectKey& key, int diff); + void add(const EffectKey& key, const EffectParam& param); + void remove(const EffectKey& key); - /// Copy Modifier values from \a effects, but keep original mBase values. - void setModifiers(const MagicEffects& effects); + void modifyBase(const EffectKey& key, int diff); - MagicEffects& operator+= (const MagicEffects& effects); + /// Copy Modifier values from \a effects, but keep original mBase values. + void setModifiers(const MagicEffects& effects); - EffectParam get (const EffectKey& key) const; - ///< This function can safely be used for keys that are not present. + EffectParam getOrDefault(const EffectKey& key) const; + std::optional get(const EffectKey& key) const; + ///< This function can safely be used for keys that are not present. - static MagicEffects diff (const MagicEffects& prev, const MagicEffects& now); - ///< Return changes from \a prev to \a now. + static MagicEffects diff(const MagicEffects& prev, const MagicEffects& now); + ///< Return changes from \a prev to \a now. }; + + std::string getMagicEffectString( + const ESM::MagicEffect& effect, const ESM::Attribute* attribute, const ESM::Skill* skill); } #endif diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index f1e40ef7fbe..a3761e1b645 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1,53 +1,69 @@ #include "mechanicsmanagerimp.hpp" +#include + #include #include -#include -#include - -#include +#include +#include +#include +#include +#include +#include #include +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/globals.hpp" #include "../mwworld/inventorystore.hpp" -#include "../mwworld/class.hpp" #include "../mwworld/player.hpp" #include "../mwworld/ptr.hpp" +#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwbase/statemanager.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/dialoguemanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwsound/constants.hpp" + +#include "actor.hpp" +#include "actors.hpp" +#include "actorutil.hpp" #include "aicombat.hpp" #include "aipursue.hpp" -#include "spellutil.hpp" #include "autocalcspell.hpp" -#include "npcstats.hpp" -#include "actorutil.hpp" #include "combat.hpp" +#include "npcstats.hpp" +#include "spellutil.hpp" namespace { float getFightDispositionBias(float disposition) { - static const float fFightDispMult = MWBase::Environment::get().getWorld()->getStore().get().find( - "fFightDispMult")->mValue.getFloat(); - return ((50.f - disposition) * fFightDispMult); + static const float fFightDispMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fFightDispMult") + ->mValue.getFloat(); + return ((50.f - disposition) * fFightDispMult); } - void getPersuasionRatings(const MWMechanics::NpcStats& stats, float& rating1, float& rating2, float& rating3, bool player) + void getPersuasionRatings( + const MWMechanics::NpcStats& stats, float& rating1, float& rating2, float& rating3, bool player) { - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); - float persTerm = stats.getAttribute(ESM::Attribute::Personality).getModified() / gmst.find("fPersonalityMod")->mValue.getFloat(); - float luckTerm = stats.getAttribute(ESM::Attribute::Luck).getModified() / gmst.find("fLuckMod")->mValue.getFloat(); + float persTerm = stats.getAttribute(ESM::Attribute::Personality).getModified() + / gmst.find("fPersonalityMod")->mValue.getFloat(); + float luckTerm + = stats.getAttribute(ESM::Attribute::Luck).getModified() / gmst.find("fLuckMod")->mValue.getFloat(); float repTerm = stats.getReputation() * gmst.find("fReputationMod")->mValue.getFloat(); float fatigueTerm = stats.getFatigueTerm(); float levelTerm = stats.getLevel() * gmst.find("fLevelMod")->mValue.getFloat(); @@ -61,11 +77,43 @@ namespace } else { - rating2 = (levelTerm + repTerm + luckTerm + persTerm + stats.getSkill(ESM::Skill::Speechcraft).getModified()) * fatigueTerm; - rating3 = (stats.getSkill(ESM::Skill::Mercantile).getModified() + repTerm + luckTerm + persTerm) * fatigueTerm; + rating2 + = (levelTerm + repTerm + luckTerm + persTerm + stats.getSkill(ESM::Skill::Speechcraft).getModified()) + * fatigueTerm; + rating3 + = (stats.getSkill(ESM::Skill::Mercantile).getModified() + repTerm + luckTerm + persTerm) * fatigueTerm; } } + bool isOwned(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) + { + const MWWorld::CellRef& cellref = target.getCellRef(); + + const ESM::RefId& owner = cellref.getOwner(); + bool isOwned = !owner.empty() && owner != ESM::RefId::stringRefId("Player"); + + const ESM::RefId& faction = cellref.getFaction(); + bool isFactionOwned = false; + if (!faction.empty() && ptr.getClass().isNpc()) + { + const std::map& factions = ptr.getClass().getNpcStats(ptr).getFactionRanks(); + auto found = factions.find(faction); + if (found == factions.end() || found->second < cellref.getFactionRank()) + isFactionOwned = true; + } + + const std::string& globalVariable = cellref.getGlobalVariable(); + if (!globalVariable.empty() && MWBase::Environment::get().getWorld()->getGlobalInt(globalVariable)) + { + isOwned = false; + isFactionOwned = false; + } + + if (!cellref.getOwner().empty()) + victim = MWBase::Environment::get().getWorld()->searchPtr(cellref.getOwner(), true, false); + + return isOwned || isFactionOwned; + } } namespace MWMechanics @@ -74,77 +122,62 @@ namespace MWMechanics { MWWorld::Ptr ptr = getPlayer(); - MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); - MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats (ptr); + MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); + MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); - npcStats.setNeedRecalcDynamicStats(true); + npcStats.recalculateMagicka(); - const ESM::NPC *player = ptr.get()->mBase; + const ESM::NPC* player = ptr.get()->mBase; // reset creatureStats.setLevel(player->mNpdt.mLevel); creatureStats.getSpells().clear(true); - creatureStats.modifyMagicEffects(MagicEffects()); - - for (int i=0; i<27; ++i) - npcStats.getSkill (i).setBase (player->mNpdt.mSkills[i]); - - creatureStats.setAttribute(ESM::Attribute::Strength, player->mNpdt.mStrength); - creatureStats.setAttribute(ESM::Attribute::Intelligence, player->mNpdt.mIntelligence); - creatureStats.setAttribute(ESM::Attribute::Willpower, player->mNpdt.mWillpower); - creatureStats.setAttribute(ESM::Attribute::Agility, player->mNpdt.mAgility); - creatureStats.setAttribute(ESM::Attribute::Speed, player->mNpdt.mSpeed); - creatureStats.setAttribute(ESM::Attribute::Endurance, player->mNpdt.mEndurance); - creatureStats.setAttribute(ESM::Attribute::Personality, player->mNpdt.mPersonality); - creatureStats.setAttribute(ESM::Attribute::Luck, player->mNpdt.mLuck); - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); + creatureStats.getActiveSpells().clear(ptr); + + for (size_t i = 0; i < player->mNpdt.mSkills.size(); ++i) + npcStats.getSkill(ESM::Skill::indexToRefId(i)).setBase(player->mNpdt.mSkills[i]); + + for (size_t i = 0; i < player->mNpdt.mAttributes.size(); ++i) + npcStats.setAttribute(ESM::Attribute::indexToRefId(i), player->mNpdt.mSkills[i]); + + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); // race if (mRaceSelected) { - const ESM::Race *race = - esmStore.get().find(player->mRace); + const ESM::Race* race = esmStore.get().find(player->mRace); bool male = (player->mFlags & ESM::NPC::Female) == 0; - for (int i=0; i<8; ++i) - { - const ESM::Race::MaleFemale& attribute = race->mData.mAttributeValues[i]; - - creatureStats.setAttribute(i, male ? attribute.mMale : attribute.mFemale); - } + for (const ESM::Attribute& attribute : esmStore.get()) + creatureStats.setAttribute(attribute.mId, race->mData.getAttribute(attribute.mId, male)); - for (int i=0; i<27; ++i) + for (const ESM::Skill& skill : esmStore.get()) { int bonus = 0; + int index = ESM::Skill::refIdToIndex(skill.mId); + auto bonusIt = std::find_if(race->mData.mBonus.begin(), race->mData.mBonus.end(), + [&](const auto& bonus) { return bonus.mSkill == index; }); + if (bonusIt != race->mData.mBonus.end()) + bonus = bonusIt->mBonus; - for (int i2=0; i2<7; ++i2) - if (race->mData.mBonus[i2].mSkill==i) - { - bonus = race->mData.mBonus[i2].mBonus; - break; - } - - npcStats.getSkill (i).setBase (5 + bonus); + npcStats.getSkill(skill.mId).setBase(5 + bonus); } - for (const std::string &power : race->mPowers.mList) + for (const ESM::RefId& power : race->mPowers.mList) { creatureStats.getSpells().add(power); } } // birthsign - const std::string &signId = - MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); + const ESM::RefId& signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); if (!signId.empty()) { - const ESM::BirthSign *sign = - esmStore.get().find(signId); + const ESM::BirthSign* sign = esmStore.get().find(signId); - for (const std::string &power : sign->mPowers.mList) + for (const ESM::RefId& power : sign->mPowers.mList) { creatureStats.getSpells().add(power); } @@ -153,51 +186,31 @@ namespace MWMechanics // class if (mClassSelected) { - const ESM::Class *class_ = - esmStore.get().find(player->mClass); + const ESM::Class* class_ = esmStore.get().find(player->mClass); - for (int i=0; i<2; ++i) + for (int attribute : class_->mData.mAttribute) { - int attribute = class_->mData.mAttribute[i]; - if (attribute>=0 && attribute<8) - { - creatureStats.setAttribute(attribute, - creatureStats.getAttribute(attribute).getBase() + 10); - } + ESM::RefId id = ESM::Attribute::indexToRefId(attribute); + if (!id.empty()) + creatureStats.setAttribute(id, creatureStats.getAttribute(id).getBase() + 10); } - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { - int bonus = i==0 ? 10 : 25; + int bonus = i == 0 ? 10 : 25; - for (int i2=0; i2<5; ++i2) + for (const auto& skills : class_->mData.mSkills) { - int index = class_->mData.mSkills[i2][i]; - - if (index>=0 && index<27) - { - npcStats.getSkill (index).setBase ( - npcStats.getSkill (index).getBase() + bonus); - } + ESM::RefId id = ESM::Skill::indexToRefId(skills[i]); + if (!id.empty()) + npcStats.getSkill(id).setBase(npcStats.getSkill(id).getBase() + bonus); } } - const MWWorld::Store &skills = - esmStore.get(); - - MWWorld::Store::iterator iter = skills.begin(); - for (; iter != skills.end(); ++iter) + for (const ESM::Skill& skill : esmStore.get()) { - if (iter->second.mData.mSpecialization==class_->mData.mSpecialization) - { - int index = iter->first; - - if (index>=0 && index<27) - { - npcStats.getSkill (index).setBase ( - npcStats.getSkill (index).getBase() + 5); - } - } + if (skill.mData.mSpecialization == class_->mData.mSpecialization) + npcStats.getSkill(skill.mId).setBase(npcStats.getSkill(skill.mId).getBase() + 5); } } @@ -206,113 +219,88 @@ namespace MWMechanics if (mRaceSelected) race = esmStore.get().find(player->mRace); - int skills[ESM::Skill::Length]; - for (int i=0; i selectedSpells + = autoCalcPlayerSpells(npcStats.getSkills(), npcStats.getAttributes(), race); - std::vector selectedSpells = autoCalcPlayerSpells(skills, attributes, race); - - for (const std::string &spell : selectedSpells) + for (const ESM::RefId& spell : selectedSpells) creatureStats.getSpells().add(spell); // forced update and current value adjustments - mActors.updateActor (ptr, 0); + mActors.updateActor(ptr, 0); - for (int i=0; i<3; ++i) + for (int i = 0; i < 3; ++i) { - DynamicStat stat = creatureStats.getDynamic (i); - stat.setCurrent (stat.getModified()); - creatureStats.setDynamic (i, stat); + DynamicStat stat = creatureStats.getDynamic(i); + stat.setCurrent(stat.getModified()); + creatureStats.setDynamic(i, stat); } - // auto-equip again. we need this for when the race is changed to a beast race and shoes are no longer equippable + // auto-equip again. we need this for when the race is changed to a beast race and shoes are no longer + // equippable MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); - for (int i=0; igetWatchedActor()) + if (ptr == MWBase::Environment::get().getWindowManager()->getWatchedActor()) MWBase::Environment::get().getWindowManager()->watchActor(MWWorld::Ptr()); - mActors.removeActor(ptr); + mActors.removeActor(ptr, keepActive); mObjects.removeObject(ptr); } - void MechanicsManager::updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) + void MechanicsManager::updateCell(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) { - if(old == MWBase::Environment::get().getWindowManager()->getWatchedActor()) + if (old == MWBase::Environment::get().getWindowManager()->getWatchedActor()) MWBase::Environment::get().getWindowManager()->watchActor(ptr); - if(ptr.getClass().isActor()) + if (ptr.getClass().isActor()) mActors.updateActor(old, ptr); else mObjects.updateObject(old, ptr); } - void MechanicsManager::drop(const MWWorld::CellStore *cellStore) + void MechanicsManager::drop(const MWWorld::CellStore* cellStore) { mActors.dropActors(cellStore, getPlayer()); mObjects.dropObjects(cellStore); } - void MechanicsManager::restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) - { - auto& stats = actor.getClass().getCreatureStats (actor); - auto& corprusSpells = stats.getCorprusSpells(); - - auto corprusIt = corprusSpells.find(sourceId); - - if (corprusIt != corprusSpells.end()) - { - for (int i = 0; i < ESM::Attribute::Length; ++i) - { - MWMechanics::AttributeValue attr = stats.getAttribute(i); - attr.restore(corprusIt->second.mWorsenings[i]); - actor.getClass().getCreatureStats(actor).setAttribute(i, attr); - } - } - } - void MechanicsManager::update(float duration, bool paused) { // Note: we should do it here since game mechanics and world updates use these values MWWorld::Ptr ptr = getPlayer(); - MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); - // Update the equipped weapon icon MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weapon == inv.end()) - winMgr->unsetSelectedWeapon(); - else - winMgr->setSelectedWeapon(*weapon); // Update the selected spell icon MWWorld::ContainerStoreIterator enchantItem = inv.getSelectedEnchantItem(); @@ -320,20 +308,26 @@ namespace MWMechanics winMgr->setSelectedEnchantItem(*enchantItem); else { - const std::string& spell = winMgr->getSelectedSpell(); + const ESM::RefId& spell = winMgr->getSelectedSpell(); if (!spell.empty()) winMgr->setSelectedSpell(spell, int(MWMechanics::getSpellSuccessChance(spell, ptr))); else winMgr->unsetSelectedSpell(); } + // Update the equipped weapon icon + if (weapon == inv.end()) + winMgr->unsetSelectedWeapon(); + else + winMgr->setSelectedWeapon(*weapon); + if (mUpdatePlayer) { mUpdatePlayer = false; // HACK? The player has been changed, so a new Animation object may // have been made for them. Make sure they're properly updated. - mActors.removeActor(ptr); + mActors.removeActor(ptr, true); mActors.addActor(ptr, true); } @@ -341,7 +335,7 @@ namespace MWMechanics mObjects.update(duration, paused); } - void MechanicsManager::processChangedSettings(const Settings::CategorySettingVector &changed) + void MechanicsManager::processChangedSettings(const Settings::CategorySettingVector& changed) { for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it) { @@ -351,8 +345,6 @@ namespace MWMechanics if (state != MWBase::StateManager::State_Running) continue; - mActors.updateProcessingRange(); - // Update mechanics for new processing range immediately update(0.f, false); } @@ -364,11 +356,6 @@ namespace MWMechanics mActors.notifyDied(actor); } - float MechanicsManager::getActorsProcessingRange() const - { - return mActors.getProcessingRange(); - } - bool MechanicsManager::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) { return mActors.isActorDetected(actor, observer); @@ -402,7 +389,7 @@ namespace MWMechanics mActors.rest(hours, sleep); } - void MechanicsManager::restoreDynamicStats(MWWorld::Ptr actor, double hours, bool sleep) + void MechanicsManager::restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep) { mActors.restoreDynamicStats(actor, hours, sleep); } @@ -412,124 +399,127 @@ namespace MWMechanics return mActors.getHoursToRest(getPlayer()); } - void MechanicsManager::setPlayerName (const std::string& name) + void MechanicsManager::setPlayerName(const std::string& name) { - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); - ESM::NPC player = - *world->getPlayerPtr().get()->mBase; + ESM::NPC player = *world->getPlayerPtr().get()->mBase; player.mName = name; - world->createRecord(player); + world->getStore().insert(player); mUpdatePlayer = true; } - void MechanicsManager::setPlayerRace (const std::string& race, bool male, const std::string &head, const std::string &hair) + void MechanicsManager::setPlayerRace( + const ESM::RefId& race, bool male, const ESM::RefId& head, const ESM::RefId& hair) { - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); - ESM::NPC player = - *world->getPlayerPtr().get()->mBase; + ESM::NPC player = *world->getPlayerPtr().get()->mBase; - player.mRace = race; - player.mHead = head; - player.mHair = hair; - player.setIsMale(male); + if (player.mRace != race || player.mHead != head || player.mHair != hair || player.isMale() != male) + { + player.mRace = race; + player.mHead = head; + player.mHair = hair; + player.setIsMale(male); - world->createRecord(player); + world->getStore().insert(player); + world->renderPlayer(); + } mRaceSelected = true; buildPlayer(); mUpdatePlayer = true; } - void MechanicsManager::setPlayerBirthsign (const std::string& id) + void MechanicsManager::setPlayerBirthsign(const ESM::RefId& id) { MWBase::Environment::get().getWorld()->getPlayer().setBirthSign(id); buildPlayer(); mUpdatePlayer = true; } - void MechanicsManager::setPlayerClass (const std::string& id) + void MechanicsManager::setPlayerClass(const ESM::RefId& id) { - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); - ESM::NPC player = - *world->getPlayerPtr().get()->mBase; + ESM::NPC player = *world->getPlayerPtr().get()->mBase; player.mClass = id; - world->createRecord(player); + world->getStore().insert(player); mClassSelected = true; buildPlayer(); mUpdatePlayer = true; } - void MechanicsManager::setPlayerClass (const ESM::Class &cls) + void MechanicsManager::setPlayerClass(const ESM::Class& cls) { - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); - const ESM::Class *ptr = world->createRecord(cls); + const ESM::Class* ptr = world->getStore().insert(cls); - ESM::NPC player = - *world->getPlayerPtr().get()->mBase; + ESM::NPC player = *world->getPlayerPtr().get()->mBase; player.mClass = ptr->mId; - world->createRecord(player); + world->getStore().insert(player); mClassSelected = true; buildPlayer(); mUpdatePlayer = true; } - int MechanicsManager::getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange) + int MechanicsManager::getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp) { - const MWMechanics::NpcStats& npcSkill = ptr.getClass().getNpcStats(ptr); - float x = static_cast(npcSkill.getBaseDisposition()); + const MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); + float x = static_cast(npcStats.getBaseDisposition() + npcStats.getCrimeDispositionModifier()); MWWorld::LiveCellRef* npc = ptr.get(); MWWorld::Ptr playerPtr = getPlayer(); MWWorld::LiveCellRef* player = playerPtr.get(); - const MWMechanics::NpcStats &playerStats = playerPtr.getClass().getNpcStats(playerPtr); + const MWMechanics::NpcStats& playerStats = playerPtr.getClass().getNpcStats(playerPtr); - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); static const float fDispRaceMod = gmst.find("fDispRaceMod")->mValue.getFloat(); - if (Misc::StringUtils::ciEqual(npc->mBase->mRace, player->mBase->mRace)) + if (npc->mBase->mRace == player->mBase->mRace) x += fDispRaceMod; static const float fDispPersonalityMult = gmst.find("fDispPersonalityMult")->mValue.getFloat(); static const float fDispPersonalityBase = gmst.find("fDispPersonalityBase")->mValue.getFloat(); - x += fDispPersonalityMult * (playerStats.getAttribute(ESM::Attribute::Personality).getModified() - fDispPersonalityBase); + x += fDispPersonalityMult + * (playerStats.getAttribute(ESM::Attribute::Personality).getModified() - fDispPersonalityBase); float reaction = 0; int rank = 0; - std::string npcFaction = ptr.getClass().getPrimaryFaction(ptr); - - Misc::StringUtils::lowerCaseInPlace(npcFaction); + const ESM::RefId& npcFaction = ptr.getClass().getPrimaryFaction(ptr); if (playerStats.getFactionRanks().find(npcFaction) != playerStats.getFactionRanks().end()) { if (!playerStats.getExpelled(npcFaction)) { // faction reaction towards itself. yes, that exists - reaction = static_cast(MWBase::Environment::get().getDialogueManager()->getFactionReaction(npcFaction, npcFaction)); + reaction = static_cast( + MWBase::Environment::get().getDialogueManager()->getFactionReaction(npcFaction, npcFaction)); rank = playerStats.getFactionRanks().find(npcFaction)->second; } } else if (!npcFaction.empty()) { - std::map::const_iterator playerFactionIt = playerStats.getFactionRanks().begin(); + auto playerFactionIt = playerStats.getFactionRanks().begin(); for (; playerFactionIt != playerStats.getFactionRanks().end(); ++playerFactionIt) { - std::string itFaction = playerFactionIt->first; + const ESM::RefId& itFaction = playerFactionIt->first; // Ignore the faction, if a player was expelled from it. if (playerStats.getExpelled(itFaction)) continue; - int itReaction = MWBase::Environment::get().getDialogueManager()->getFactionReaction(npcFaction, itFaction); + int itReaction + = MWBase::Environment::get().getDialogueManager()->getFactionReaction(npcFaction, itFaction); if (playerFactionIt == playerStats.getFactionRanks().begin() || itReaction < reaction) { reaction = static_cast(itReaction); @@ -546,9 +536,7 @@ namespace MWMechanics static const float fDispFactionRankMult = gmst.find("fDispFactionRankMult")->mValue.getFloat(); static const float fDispFactionRankBase = gmst.find("fDispFactionRankBase")->mValue.getFloat(); static const float fDispFactionMod = gmst.find("fDispFactionMod")->mValue.getFloat(); - x += (fDispFactionRankMult * rank - + fDispFactionRankBase) - * fDispFactionMod * reaction; + x += (fDispFactionRankMult * rank + fDispFactionRankBase) * fDispFactionMod * reaction; static const float fDispCrimeMod = gmst.find("fDispCrimeMod")->mValue.getFloat(); static const float fDispDiseaseMod = gmst.find("fDispDiseaseMod")->mValue.getFloat(); @@ -557,32 +545,34 @@ namespace MWMechanics x += fDispDiseaseMod; static const float fDispWeaponDrawn = gmst.find("fDispWeaponDrawn")->mValue.getFloat(); - if (playerStats.getDrawState() == MWMechanics::DrawState_Weapon) + if (playerStats.getDrawState() == MWMechanics::DrawState::Weapon) x += fDispWeaponDrawn; - x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude(); - - if(addTemporaryDispositionChange) - x += MWBase::Environment::get().getDialogueManager()->getTemporaryDispositionChange(); + x += ptr.getClass() + .getCreatureStats(ptr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Charm) + .getMagnitude(); - int effective_disposition = std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used - return effective_disposition; + if (clamp) + return std::clamp(x, 0, 100); //, normally clamped to [0..100] when used + return static_cast(x); } - int MechanicsManager::getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) + int MechanicsManager::getBarterOffer(const MWWorld::Ptr& ptr, int basePrice, bool buying) { // Make sure zero base price items/services can't be bought/sold for 1 gold // and return the intended base price for creature merchants - if (basePrice == 0 || ptr.getTypeName() == typeid(ESM::Creature).name()) + if (basePrice == 0 || ptr.getType() == ESM::Creature::sRecordId) return basePrice; - const MWMechanics::NpcStats &sellerStats = ptr.getClass().getNpcStats(ptr); + const MWMechanics::NpcStats& sellerStats = ptr.getClass().getNpcStats(ptr); MWWorld::Ptr playerPtr = getPlayer(); - const MWMechanics::NpcStats &playerStats = playerPtr.getClass().getNpcStats(playerPtr); + const MWMechanics::NpcStats& playerStats = playerPtr.getClass().getNpcStats(playerPtr); - // I suppose the temporary disposition change (second param to getDerivedDisposition()) _has_ to be considered here, - // otherwise one would get different prices when exiting and re-entering the dialogue window... + // I suppose the temporary disposition change (second param to getDerivedDisposition()) _has_ to be considered + // here, otherwise one would get different prices when exiting and re-entering the dialogue window... int clampedDisposition = getDerivedDisposition(ptr); float a = std::min(playerPtr.getClass().getSkill(playerPtr, ESM::Skill::Mercantile), 100.f); float b = std::min(0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified(), 10.f); @@ -598,20 +588,21 @@ namespace MWMechanics return std::max(1, offerPrice); } - int MechanicsManager::countDeaths (const std::string& id) const + int MechanicsManager::countDeaths(const ESM::RefId& id) const { - return mActors.countDeaths (id); + return mActors.countDeaths(id); } - void MechanicsManager::getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) + void MechanicsManager::getPersuasionDispositionChange( + const MWWorld::Ptr& npc, PersuasionType type, bool& success, int& tempChange, int& permChange) { - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); MWMechanics::NpcStats& npcStats = npc.getClass().getNpcStats(npc); MWWorld::Ptr playerPtr = getPlayer(); - const MWMechanics::NpcStats &playerStats = playerPtr.getClass().getNpcStats(playerPtr); + const MWMechanics::NpcStats& playerStats = playerPtr.getClass().getNpcStats(playerPtr); float npcRating1, npcRating2, npcRating3; getPersuasionRatings(npcStats, npcRating1, npcRating2, npcRating3, false); @@ -619,28 +610,32 @@ namespace MWMechanics float playerRating1, playerRating2, playerRating3; getPersuasionRatings(playerStats, playerRating1, playerRating2, playerRating3, true); - int currentDisposition = getDerivedDisposition(npc); + const int currentDisposition = getDerivedDisposition(npc); float d = 1 - 0.02f * abs(currentDisposition - 50); float target1 = d * (playerRating1 - npcRating1 + 50); float target2 = d * (playerRating2 - npcRating2 + 50); float bribeMod; - if (type == PT_Bribe10) bribeMod = gmst.find("fBribe10Mod")->mValue.getFloat(); - else if (type == PT_Bribe100) bribeMod = gmst.find("fBribe100Mod")->mValue.getFloat(); - else bribeMod = gmst.find("fBribe1000Mod")->mValue.getFloat(); + if (type == PT_Bribe10) + bribeMod = gmst.find("fBribe10Mod")->mValue.getFloat(); + else if (type == PT_Bribe100) + bribeMod = gmst.find("fBribe100Mod")->mValue.getFloat(); + else + bribeMod = gmst.find("fBribe1000Mod")->mValue.getFloat(); float target3 = d * (playerRating3 - npcRating3 + 50) + bribeMod; - float iPerMinChance = floor(gmst.find("iPerMinChance")->mValue.getFloat()); - float iPerMinChange = floor(gmst.find("iPerMinChange")->mValue.getFloat()); - float fPerDieRollMult = gmst.find("fPerDieRollMult")->mValue.getFloat(); - float fPerTempMult = gmst.find("fPerTempMult")->mValue.getFloat(); + const float iPerMinChance = gmst.find("iPerMinChance")->mValue.getFloat(); + const float iPerMinChange = gmst.find("iPerMinChange")->mValue.getFloat(); + const float fPerDieRollMult = gmst.find("fPerDieRollMult")->mValue.getFloat(); + const float fPerTempMult = gmst.find("fPerTempMult")->mValue.getFloat(); float x = 0; float y = 0; - int roll = Misc::Rng::roll0to99(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); if (type == PT_Admire) { @@ -653,7 +648,7 @@ namespace MWMechanics { target2 = std::max(iPerMinChance, target2); - success = (roll <= target2); + success = (roll <= target2); float r; if (roll != target2) @@ -665,12 +660,12 @@ namespace MWMechanics { float s = floor(r * fPerDieRollMult * fPerTempMult); - int flee = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Flee).getBase(); - int fight = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Fight).getBase(); - npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, - std::max(0, std::min(100, flee + int(std::max(iPerMinChange, s))))); - npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, - std::max(0, std::min(100, fight + int(std::min(-iPerMinChange, -s))))); + const int flee = npcStats.getAiSetting(MWMechanics::AiSetting::Flee).getBase(); + const int fight = npcStats.getAiSetting(MWMechanics::AiSetting::Fight).getBase(); + npcStats.setAiSetting( + MWMechanics::AiSetting::Flee, std::clamp(flee + int(std::max(iPerMinChange, s)), 0, 100)); + npcStats.setAiSetting( + MWMechanics::AiSetting::Fight, std::clamp(fight + int(std::min(-iPerMinChange, -s)), 0, 100)); } float c = -std::abs(floor(r * fPerDieRollMult)); @@ -706,12 +701,12 @@ namespace MWMechanics if (success) { float s = c * fPerDieRollMult * fPerTempMult; - int flee = npcStats.getAiSetting (CreatureStats::AI_Flee).getBase(); - int fight = npcStats.getAiSetting (CreatureStats::AI_Fight).getBase(); - npcStats.setAiSetting (CreatureStats::AI_Flee, - std::max(0, std::min(100, flee + std::min(-int(iPerMinChange), int(-s))))); - npcStats.setAiSetting (CreatureStats::AI_Fight, - std::max(0, std::min(100, fight + std::max(int(iPerMinChange), int(s))))); + const int flee = npcStats.getAiSetting(AiSetting::Flee).getBase(); + const int fight = npcStats.getAiSetting(AiSetting::Fight).getBase(); + npcStats.setAiSetting( + AiSetting::Flee, std::clamp(flee + std::min(-int(iPerMinChange), int(-s)), 0, 100)); + npcStats.setAiSetting( + AiSetting::Fight, std::clamp(fight + std::max(int(iPerMinChange), int(s)), 0, 100)); } x = floor(-c * fPerDieRollMult); @@ -727,53 +722,81 @@ namespace MWMechanics x = success ? std::max(iPerMinChange, c) : c; } - tempChange = type == PT_Intimidate ? x : int(x * fPerTempMult); - - - float cappedDispositionChange = tempChange; - if (currentDisposition + tempChange > 100.f) - cappedDispositionChange = static_cast(100 - currentDisposition); - if (currentDisposition + tempChange < 0.f) - cappedDispositionChange = static_cast(-currentDisposition); - - permChange = floor(cappedDispositionChange / fPerTempMult); if (type == PT_Intimidate) { - permChange = success ? -int(cappedDispositionChange/ fPerTempMult) : y; + tempChange = int(x); + if (currentDisposition + tempChange > 100) + tempChange = 100 - currentDisposition; + else if (currentDisposition + tempChange < 0) + tempChange = -currentDisposition; + permChange = success ? -int(tempChange / fPerTempMult) : int(y); + } + else + { + tempChange = int(x * fPerTempMult); + if (currentDisposition + tempChange > 100) + tempChange = 100 - currentDisposition; + else if (currentDisposition + tempChange < 0) + tempChange = -currentDisposition; + permChange = int(tempChange / fPerTempMult); } } - void MechanicsManager::forceStateUpdate(const MWWorld::Ptr &ptr) + void MechanicsManager::forceStateUpdate(const MWWorld::Ptr& ptr) { - if(ptr.getClass().isActor()) + if (ptr.getClass().isActor()) mActors.forceStateUpdate(ptr); } - bool MechanicsManager::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist) + bool MechanicsManager::playAnimationGroup( + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, bool scripted) { - if(ptr.getClass().isActor()) - return mActors.playAnimationGroup(ptr, groupName, mode, number, persist); + if (ptr.getClass().isActor()) + return mActors.playAnimationGroup(ptr, groupName, mode, number, scripted); else - return mObjects.playAnimationGroup(ptr, groupName, mode, number, persist); + return mObjects.playAnimationGroup(ptr, groupName, mode, number, scripted); + } + bool MechanicsManager::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, + float speed, std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + if (ptr.getClass().isActor()) + return mActors.playAnimationGroupLua(ptr, groupName, loops, speed, startKey, stopKey, forceLoop); + else + return mObjects.playAnimationGroupLua(ptr, groupName, loops, speed, startKey, stopKey, forceLoop); + } + void MechanicsManager::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + if (ptr.getClass().isActor()) + mActors.enableLuaAnimations(ptr, enable); + else + mObjects.enableLuaAnimations(ptr, enable); } void MechanicsManager::skipAnimation(const MWWorld::Ptr& ptr) { - if(ptr.getClass().isActor()) + if (ptr.getClass().isActor()) mActors.skipAnimation(ptr); else mObjects.skipAnimation(ptr); } - bool MechanicsManager::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string &groupName) + bool MechanicsManager::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) { - if(ptr.getClass().isActor()) + if (ptr.getClass().isActor()) return mActors.checkAnimationPlaying(ptr, groupName); else return false; } + bool MechanicsManager::checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const + { + if (ptr.getClass().isActor()) + return mActors.checkScriptedAnimationPlaying(ptr); + + return false; + } + bool MechanicsManager::onOpen(const MWWorld::Ptr& ptr) { - if(ptr.getClass().isActor()) + if (ptr.getClass().isActor()) return true; else return mObjects.onOpen(ptr); @@ -781,7 +804,7 @@ namespace MWMechanics void MechanicsManager::onClose(const MWWorld::Ptr& ptr) { - if(!ptr.getClass().isActor()) + if (!ptr.getClass().isActor()) mObjects.onClose(ptr); } @@ -791,7 +814,15 @@ namespace MWMechanics mObjects.persistAnimationStates(); } - void MechanicsManager::updateMagicEffects(const MWWorld::Ptr &ptr) + void MechanicsManager::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) + { + if (ptr.getClass().isActor()) + mActors.clearAnimationQueue(ptr, clearScripted); + else + mObjects.clearAnimationQueue(ptr, clearScripted); + } + + void MechanicsManager::updateMagicEffects(const MWWorld::Ptr& ptr) { mActors.updateMagicEffects(ptr); } @@ -799,12 +830,6 @@ namespace MWMechanics bool MechanicsManager::toggleAI() { mAI = !mAI; - - MWBase::World* world = MWBase::Environment::get().getWorld(); - world->getNavigator()->setUpdatesEnabled(mAI); - if (mAI) - world->getNavigator()->update(world->getPlayerPtr().getRefData().getPosition().asVec3()); - return mAI; } @@ -823,35 +848,32 @@ namespace MWMechanics bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item) { - static std::set boundItemIDCache; + static std::set boundItemIDCache; - // If this is empty then we haven't executed the GMST cache logic yet; or there isn't any sMagicBound* GMST's for some reason + // If this is empty then we haven't executed the GMST cache logic yet; or there isn't any sMagicBound* GMST's + // for some reason if (boundItemIDCache.empty()) { // Build a list of known bound item ID's - const MWWorld::Store &gameSettings = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gameSettings + = MWBase::Environment::get().getESMStore()->get(); - for (const ESM::GameSetting ¤tSetting : gameSettings) + for (const ESM::GameSetting& currentSetting : gameSettings) { - std::string currentGMSTID = currentSetting.mId; - Misc::StringUtils::lowerCaseInPlace(currentGMSTID); - // Don't bother checking this GMST if it's not a sMagicBound* one. - const std::string& toFind = "smagicbound"; - if (currentGMSTID.compare(0, toFind.length(), toFind) != 0) + if (!currentSetting.mId.startsWith("smagicbound")) continue; // All sMagicBound* GMST's should be of type string std::string currentGMSTValue = currentSetting.mValue.getString(); Misc::StringUtils::lowerCaseInPlace(currentGMSTValue); - boundItemIDCache.insert(currentGMSTValue); + boundItemIDCache.insert(ESM::RefId::stringRefId(currentGMSTValue)); } } // Perform bound item check and assign the Flag_Bound bit if it passes - std::string tempItemID = item.getCellRef().getRefId(); - Misc::StringUtils::lowerCaseInPlace(tempItemID); + const ESM::RefId& tempItemID = item.getCellRef().getRefId(); if (boundItemIDCache.count(tempItemID) != 0) return true; @@ -859,17 +881,14 @@ namespace MWMechanics return false; } - bool MechanicsManager::isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) + bool MechanicsManager::isAllowedToUse(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) { if (target.isEmpty()) return true; const MWWorld::CellRef& cellref = target.getCellRef(); // there is no harm to use unlocked doors - int lockLevel = cellref.getLockLevel(); - if (target.getClass().isDoor() && - (lockLevel <= 0 || lockLevel == ESM::UnbreakableLock) && - ptr.getCellRef().getTrap().empty()) + if (target.getClass().isDoor() && !cellref.isLocked() && cellref.getTrap().empty()) { return true; } @@ -878,7 +897,7 @@ namespace MWMechanics return true; // TODO: implement a better check to check if target is owned bed - if (target.getClass().isActivator() && target.getClass().getScript(target).compare(0, 3, "Bed") != 0) + if (target.getClass().isActivator() && !target.getClass().getScript(target).startsWith("Bed")) return true; if (target.getClass().isNpc()) @@ -896,38 +915,14 @@ namespace MWMechanics return true; } - const std::string& owner = cellref.getOwner(); - bool isOwned = !owner.empty() && owner != "player"; - - const std::string& faction = cellref.getFaction(); - bool isFactionOwned = false; - if (!faction.empty() && ptr.getClass().isNpc()) - { - const std::map& factions = ptr.getClass().getNpcStats(ptr).getFactionRanks(); - std::map::const_iterator found = factions.find(Misc::StringUtils::lowerCase(faction)); - if (found == factions.end() - || found->second < cellref.getFactionRank()) - isFactionOwned = true; - } - - const std::string& globalVariable = cellref.getGlobalVariable(); - if (!globalVariable.empty() && MWBase::Environment::get().getWorld()->getGlobalInt(Misc::StringUtils::lowerCase(globalVariable)) == 1) - { - isOwned = false; - isFactionOwned = false; - } - - if (!cellref.getOwner().empty()) - victim = MWBase::Environment::get().getWorld()->searchPtr(cellref.getOwner(), true, false); - - // A special case for evidence chest - we should not allow to take items even if it is technically permitted - if (Misc::StringUtils::ciEqual(cellref.getRefId(), "stolen_goods")) + if (isOwned(ptr, target, victim)) return false; - return (!isOwned && !isFactionOwned); + // A special case for evidence chest - we should not allow to take items even if it is technically permitted + return !(cellref.getRefId() == "stolen_goods"); } - bool MechanicsManager::sleepInBed(const MWWorld::Ptr &ptr, const MWWorld::Ptr &bed) + bool MechanicsManager::sleepInBed(const MWWorld::Ptr& ptr, const MWWorld::Ptr& bed) { if (ptr.getClass().getNpcStats(ptr).isWerewolf()) { @@ -935,7 +930,8 @@ namespace MWMechanics return true; } - if(MWBase::Environment::get().getWorld()->getPlayer().enemiesNearby()) { + if (MWBase::Environment::get().getWorld()->getPlayer().enemiesNearby()) + { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage2}"); return true; } @@ -944,7 +940,7 @@ namespace MWMechanics if (isAllowedToUse(ptr, bed, victim)) return false; - if(commitCrime(ptr, victim, OT_SleepingInOwnedBed, bed.getCellRef().getFaction())) + if (commitCrime(ptr, victim, OT_SleepingInOwnedBed, bed.getCellRef().getFaction())) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage64}"); return true; @@ -953,18 +949,23 @@ namespace MWMechanics return false; } - void MechanicsManager::unlockAttempted(const MWWorld::Ptr &ptr, const MWWorld::Ptr &item) + void MechanicsManager::unlockAttempted(const MWWorld::Ptr& ptr, const MWWorld::Ptr& item) { MWWorld::Ptr victim; - if (isAllowedToUse(ptr, item, victim)) - return; - commitCrime(ptr, victim, OT_Trespassing, item.getCellRef().getFaction()); + if (isOwned(ptr, item, victim)) + { + // Note that attempting to unlock something that has ever been locked is a crime even if it's already + // unlocked. Likewise, it's illegal to unlock something that has a trap but isn't otherwise locked. + const auto& cellref = item.getCellRef(); + if (cellref.getLockLevel() || cellref.isLocked() || !cellref.getTrap().empty()) + commitCrime(ptr, victim, OT_Trespassing, item.getCellRef().getFaction()); + } } - std::vector > MechanicsManager::getStolenItemOwners(const std::string& itemid) + std::vector> MechanicsManager::getStolenItemOwners(const ESM::RefId& itemid) { - std::vector > result; - StolenItemsMap::const_iterator it = mStolenItems.find(Misc::StringUtils::lowerCase(itemid)); + std::vector> result; + StolenItemsMap::const_iterator it = mStolenItems.find(itemid); if (it == mStolenItems.end()) return result; else @@ -976,34 +977,35 @@ namespace MWMechanics } } - bool MechanicsManager::isItemStolenFrom(const std::string &itemid, const MWWorld::Ptr& ptr) + bool MechanicsManager::isItemStolenFrom(const ESM::RefId& itemid, const MWWorld::Ptr& ptr) { - StolenItemsMap::const_iterator it = mStolenItems.find(Misc::StringUtils::lowerCase(itemid)); + StolenItemsMap::const_iterator it = mStolenItems.find(itemid); if (it == mStolenItems.end()) return false; const OwnerMap& owners = it->second; - const std::string ownerid = ptr.getCellRef().getRefId(); - OwnerMap::const_iterator ownerFound = owners.find(std::make_pair(Misc::StringUtils::lowerCase(ownerid), false)); + const ESM::RefId& ownerid = ptr.getCellRef().getRefId(); + OwnerMap::const_iterator ownerFound = owners.find(std::make_pair(ownerid, false)); if (ownerFound != owners.end()) return true; - const std::string factionid = ptr.getClass().getPrimaryFaction(ptr); + const ESM::RefId& factionid = ptr.getClass().getPrimaryFaction(ptr); if (!factionid.empty()) { - OwnerMap::const_iterator factionOwnerFound = owners.find(std::make_pair(Misc::StringUtils::lowerCase(factionid), true)); + OwnerMap::const_iterator factionOwnerFound = owners.find(std::make_pair(factionid, true)); return factionOwnerFound != owners.end(); } return false; } - void MechanicsManager::confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count) + void MechanicsManager::confiscateStolenItemToOwner( + const MWWorld::Ptr& player, const MWWorld::Ptr& item, const MWWorld::Ptr& victim, int count) { if (player != getPlayer()) return; - const std::string itemId = Misc::StringUtils::lowerCase(item.getCellRef().getRefId()); + const ESM::RefId& itemId = item.getCellRef().getRefId(); StolenItemsMap::iterator stolenIt = mStolenItems.find(itemId); if (stolenIt == mStolenItems.end()) @@ -1013,15 +1015,13 @@ namespace MWMechanics owner.first = victim.getCellRef().getRefId(); owner.second = false; - const std::string victimFaction = victim.getClass().getPrimaryFaction(victim); - if (!victimFaction.empty() && Misc::StringUtils::ciEqual(item.getCellRef().getFaction(), victimFaction)) // Is the item faction-owned? + const ESM::RefId& victimFaction = victim.getClass().getPrimaryFaction(victim); + if (!victimFaction.empty() && item.getCellRef().getFaction() == victimFaction) // Is the item faction-owned? { owner.first = victimFaction; owner.second = true; } - Misc::StringUtils::lowerCaseInPlace(owner.first); - // decrease count of stolen items int toRemove = std::min(count, mStolenItems[itemId][owner]); mStolenItems[itemId][owner] -= toRemove; @@ -1037,22 +1037,23 @@ namespace MWMechanics MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); // move items from player to owner and report about theft - victim.getClass().getContainerStore(victim).add(item, toRemove, victim); - store.remove(item, toRemove, player); - commitCrime(player, victim, OT_Theft, item.getCellRef().getFaction(), item.getClass().getValue(item) * toRemove); + victim.getClass().getContainerStore(victim).add(item, toRemove); + store.remove(item, toRemove); + commitCrime( + player, victim, OT_Theft, item.getCellRef().getFaction(), item.getClass().getValue(item) * toRemove); } - void MechanicsManager::confiscateStolenItems(const MWWorld::Ptr &player, const MWWorld::Ptr &targetContainer) + void MechanicsManager::confiscateStolenItems(const MWWorld::Ptr& player, const MWWorld::Ptr& targetContainer) { MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); MWWorld::ContainerStore& containerStore = targetContainer.getClass().getContainerStore(targetContainer); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { - StolenItemsMap::iterator stolenIt = mStolenItems.find(Misc::StringUtils::lowerCase(it->getCellRef().getRefId())); + StolenItemsMap::iterator stolenIt = mStolenItems.find(it->getCellRef().getRefId()); if (stolenIt == mStolenItems.end()) continue; OwnerMap& owners = stolenIt->second; - int itemCount = it->getRefData().getCount(); + int itemCount = it->getCellRef().getCount(); for (OwnerMap::iterator ownerIt = owners.begin(); ownerIt != owners.end();) { int toRemove = std::min(itemCount, ownerIt->second); @@ -1064,17 +1065,17 @@ namespace MWMechanics ++ownerIt; } - int toMove = it->getRefData().getCount() - itemCount; + int toMove = it->getCellRef().getCount() - itemCount; - containerStore.add(*it, toMove, targetContainer); - store.remove(*it, toMove, player); + containerStore.add(*it, toMove); + store.remove(*it, toMove); } // TODO: unhardcode the locklevel targetContainer.getCellRef().lock(50); } - void MechanicsManager::itemTaken(const MWWorld::Ptr &ptr, const MWWorld::Ptr &item, const MWWorld::Ptr& container, - int count, bool alarm) + void MechanicsManager::itemTaken( + const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, int count, bool alarm) { if (ptr != getPlayer()) return; @@ -1092,11 +1093,6 @@ namespace MWMechanics else { isAllowed = isAllowedToUse(ptr, item, victim); - if (!item.getCellRef().hasContentFile()) - { - // this is a manually placed item, which means it was already stolen - return; - } } if (isAllowed) @@ -1119,19 +1115,25 @@ namespace MWMechanics } } - Misc::StringUtils::lowerCaseInPlace(owner.first); - - if (!Misc::StringUtils::ciEqual(item.getCellRef().getRefId(), MWWorld::ContainerStore::sGoldId)) + const bool isGold = item.getClass().isGold(item); + if (!isGold) { - if (victim.isEmpty() || (victim.getClass().isActor() && victim.getRefData().getCount() > 0 && !victim.getClass().getCreatureStats(victim).isDead())) - mStolenItems[Misc::StringUtils::lowerCase(item.getCellRef().getRefId())][owner] += count; + if (victim.isEmpty() + || (victim.getClass().isActor() && victim.getCellRef().getCount() > 0 + && !victim.getClass().getCreatureStats(victim).isDead())) + mStolenItems[item.getCellRef().getRefId()][owner] += count; } if (alarm) - commitCrime(ptr, victim, OT_Theft, ownerCellRef->getFaction(), item.getClass().getValue(item) * count); + { + int value = count; + if (!isGold) + value *= item.getClass().getValue(item); + commitCrime(ptr, victim, OT_Theft, ownerCellRef->getFaction(), value); + } } - - bool MechanicsManager::commitCrime(const MWWorld::Ptr &player, const MWWorld::Ptr &victim, OffenseType type, const std::string& factionId, int arg, bool victimAware) + bool MechanicsManager::commitCrime(const MWWorld::Ptr& player, const MWWorld::Ptr& victim, OffenseType type, + const ESM::RefId& factionId, int arg, bool victimAware) { // NOTE: victim may be empty @@ -1139,17 +1141,20 @@ namespace MWMechanics if (player != getPlayer()) return false; + if (type == OT_Assault) + victimAware = true; + // Find all the actors within the alarm radius std::vector neighbors; - osg::Vec3f from (player.getRefData().getPosition().asVec3()); - const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); + osg::Vec3f from(player.getRefData().getPosition().asVec3()); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); float radius = esmStore.get().find("fAlarmRadius")->mValue.getFloat(); mActors.getObjectsInRange(from, radius, neighbors); // victim should be considered even beyond alarm radius - if (!victim.isEmpty() && (from - victim.getRefData().getPosition().asVec3()).length2() > radius*radius) + if (!victim.isEmpty() && (from - victim.getRefData().getPosition().asVec3()).length2() > radius * radius) neighbors.push_back(victim); // get the player's followers / allies (works recursively) that will not report crimes @@ -1158,20 +1163,21 @@ namespace MWMechanics // Did anyone see it? bool crimeSeen = false; - for (const MWWorld::Ptr &neighbor : neighbors) + for (const MWWorld::Ptr& neighbor : neighbors) { if (!canReportCrime(neighbor, victim, playerFollowers)) continue; if ((neighbor == victim && victimAware) - // Murder crime can be reported even if no one saw it (hearing is enough, I guess). - // TODO: Add mod support for stealth executions! - || (type == OT_Murder && neighbor != victim) - || (MWBase::Environment::get().getWorld()->getLOS(player, neighbor) && awarenessCheck(player, neighbor))) + // Murder crime can be reported even if no one saw it (hearing is enough, I guess). + // TODO: Add mod support for stealth executions! + || (type == OT_Murder && neighbor != victim) + || (MWBase::Environment::get().getWorld()->getLOS(player, neighbor) + && awarenessCheck(player, neighbor))) { // NPC will complain about theft even if he will do nothing about it if (type == OT_Theft || type == OT_Pickpocket) - MWBase::Environment::get().getDialogueManager()->say(neighbor, "thief"); + MWBase::Environment::get().getDialogueManager()->say(neighbor, ESM::RefId::stringRefId("thief")); crimeSeen = true; } @@ -1184,18 +1190,19 @@ namespace MWMechanics bool reported = false; if (victim.getClass().isClass(victim, "guard") && !victim.getClass().getCreatureStats(victim).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) - reported = reportCrime(player, victim, type, std::string(), arg); + reported = reportCrime(player, victim, type, ESM::RefId(), arg); + // TODO: combat should be started with an "unaware" flag, which makes the victim flee? if (!reported) - startCombat(victim, player); // TODO: combat should be started with an "unaware" flag, which makes the victim flee? + startCombat(victim, player, &playerFollowers); } return crimeSeen; } - bool MechanicsManager::canReportCrime(const MWWorld::Ptr &actor, const MWWorld::Ptr &victim, std::set &playerFollowers) + bool MechanicsManager::canReportCrime( + const MWWorld::Ptr& actor, const MWWorld::Ptr& victim, std::set& playerFollowers) { - if (actor == getPlayer() - || !actor.getClass().isNpc() || actor.getClass().getCreatureStats(actor).isDead()) + if (actor == getPlayer() || !actor.getClass().isNpc() || actor.getClass().getCreatureStats(actor).isDead()) return false; if (actor.getClass().getCreatureStats(actor).getAiSequence().isInCombat(victim)) @@ -1215,9 +1222,11 @@ namespace MWMechanics return true; } - bool MechanicsManager::reportCrime(const MWWorld::Ptr &player, const MWWorld::Ptr &victim, OffenseType type, const std::string& factionId, int arg) + bool MechanicsManager::reportCrime( + const MWWorld::Ptr& player, const MWWorld::Ptr& victim, OffenseType type, const ESM::RefId& factionId, int arg) { - const MWWorld::Store& store = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& store + = MWBase::Environment::get().getESMStore()->get(); if (type == OT_Murder && !victim.isEmpty()) victim.getClass().getCreatureStats(victim).notifyMurder(); @@ -1255,15 +1264,15 @@ namespace MWMechanics // Make surrounding actors within alarm distance respond to the crime std::vector neighbors; - const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); - osg::Vec3f from (player.getRefData().getPosition().asVec3()); + osg::Vec3f from(player.getRefData().getPosition().asVec3()); float radius = esmStore.get().find("fAlarmRadius")->mValue.getFloat(); mActors.getObjectsInRange(from, radius, neighbors); // victim should be considered even beyond alarm radius - if (!victim.isEmpty() && (from - victim.getRefData().getPosition().asVec3()).length2() > radius*radius) + if (!victim.isEmpty() && (from - victim.getRefData().getPosition().asVec3()).length2() > radius * radius) neighbors.push_back(victim); int id = MWBase::Environment::get().getWorld()->getPlayer().getNewCrimeId(); @@ -1276,7 +1285,8 @@ namespace MWMechanics else if (type == OT_Pickpocket) { fight = esmStore.get().find("iFightPickpocket")->mValue.getInteger(); - fightVictim = esmStore.get().find("iFightPickpocket")->mValue.getInteger() * 4; // *4 according to research wiki + fightVictim = esmStore.get().find("iFightPickpocket")->mValue.getInteger() + * 4; // *4 according to research wiki } else if (type == OT_Assault) { @@ -1294,115 +1304,189 @@ namespace MWMechanics getActorsSidingWith(player, playerFollowers); // Tell everyone (including the original reporter) in alarm range - for (const MWWorld::Ptr &actor : neighbors) + for (const MWWorld::Ptr& actor : neighbors) { if (!canReportCrime(actor, victim, playerFollowers)) continue; // Will the witness report the crime? - if (actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Alarm).getBase() >= 100) + if (actor.getClass().getCreatureStats(actor).getAiSetting(AiSetting::Alarm).getBase() >= 100) { reported = true; if (type == OT_Trespassing) - MWBase::Environment::get().getDialogueManager()->say(actor, "intruder"); + MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("intruder")); } } - for (const MWWorld::Ptr &actor : neighbors) + for (const MWWorld::Ptr& actor : neighbors) { if (!canReportCrime(actor, victim, playerFollowers)) continue; - if (reported && actor.getClass().isClass(actor, "guard")) + NpcStats& observerStats = actor.getClass().getNpcStats(actor); + + int alarm = observerStats.getAiSetting(AiSetting::Alarm).getBase(); + float alarmTerm = 0.01f * alarm; + + bool isActorVictim = actor == victim; + float dispTerm = isActorVictim ? dispVictim : disp; + + bool isActorGuard = actor.getClass().isClass(actor, "guard"); + + int currentDisposition = getDerivedDisposition(actor); + + bool isPermanent = false; + bool applyOnlyIfHostile = false; + int dispositionModifier = 0; + // Murdering and trespassing seem to do not affect disposition + if (type == OT_Theft) + { + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else if (type == OT_Pickpocket) + { + if (alarm >= 100 && isActorGuard) + dispositionModifier = static_cast(dispTerm); + else if (isActorVictim && isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else if (isActorVictim) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm); + } + } + else if (type == OT_Assault) + { + if (isActorVictim && !isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm); + } + else if (alarm >= 100) + dispositionModifier = static_cast(dispTerm); + else if (isActorVictim && isActorGuard) + { + isPermanent = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + else + { + applyOnlyIfHostile = true; + dispositionModifier = static_cast(dispTerm * alarmTerm); + } + } + + bool setCrimeId = false; + if (isPermanent && dispositionModifier != 0 && !applyOnlyIfHostile) + { + setCrimeId = true; + dispositionModifier = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + int baseDisposition = observerStats.getBaseDisposition(); + observerStats.setBaseDisposition(baseDisposition + dispositionModifier); + } + else if (dispositionModifier != 0 && !applyOnlyIfHostile) + { + setCrimeId = true; + dispositionModifier = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + observerStats.modCrimeDispositionModifier(dispositionModifier); + } + + if (isActorGuard && alarm >= 100) { // Mark as Alarmed for dialogue - actor.getClass().getCreatureStats(actor).setAlarmed(true); + observerStats.setAlarmed(true); - // Set the crime ID, which we will use to calm down participants - // once the bounty has been paid. - actor.getClass().getNpcStats(actor).setCrimeId(id); + setCrimeId = true; - if (!actor.getClass().getCreatureStats(actor).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) + if (!observerStats.getAiSequence().isInPursuit()) { - actor.getClass().getCreatureStats(actor).getAiSequence().stack(AiPursue(player), actor); + observerStats.getAiSequence().stack(AiPursue(player), actor); } } else { - float dispTerm = (actor == victim) ? dispVictim : disp; - - float alarmTerm = 0.01f * actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Alarm).getBase(); - if (type == OT_Pickpocket && alarmTerm <= 0) + // If Alarm is 0, treat it like 100 to calculate a Fight modifier for a victim of pickpocketing. + // Observers which do not try to arrest player do not care about pickpocketing at all. + if (type == OT_Pickpocket && isActorVictim && alarmTerm == 0.0) alarmTerm = 1.0; + else if (type == OT_Pickpocket && !isActorVictim) + alarmTerm = 0.0; - if (actor != victim) - dispTerm *= alarmTerm; - - float fightTerm = static_cast((actor == victim) ? fightVictim : fight); + float fightTerm = static_cast(isActorVictim ? fightVictim : fight); fightTerm += getFightDispositionBias(dispTerm); fightTerm += getFightDistanceBias(actor, player); fightTerm *= alarmTerm; - int observerFightRating = actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Fight).getBase(); + const int observerFightRating = observerStats.getAiSetting(AiSetting::Fight).getBase(); if (observerFightRating + fightTerm > 100) fightTerm = static_cast(100 - observerFightRating); fightTerm = std::max(0.f, fightTerm); if (observerFightRating + fightTerm >= 100) { - startCombat(actor, player); + if (dispositionModifier != 0 && applyOnlyIfHostile) + { + dispositionModifier + = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); + observerStats.modCrimeDispositionModifier(dispositionModifier); + } + + startCombat(actor, player, &playerFollowers); - NpcStats& observerStats = actor.getClass().getNpcStats(actor); // Apply aggression value to the base Fight rating, so that the actor can continue fighting // after a Calm spell wears off - observerStats.setAiSetting(CreatureStats::AI_Fight, observerFightRating + static_cast(fightTerm)); + observerStats.setAiSetting(AiSetting::Fight, observerFightRating + static_cast(fightTerm)); - observerStats.setBaseDisposition(observerStats.getBaseDisposition() + static_cast(dispTerm)); - - // Set the crime ID, which we will use to calm down participants - // once the bounty has been paid. - observerStats.setCrimeId(id); + setCrimeId = true; // Mark as Alarmed for dialogue observerStats.setAlarmed(true); } } + + // Set the crime ID, which we will use to calm down participants + // once the bounty has been paid and restore their disposition to player character. + if (setCrimeId) + observerStats.setCrimeId(id); } if (reported) { - player.getClass().getNpcStats(player).setBounty(player.getClass().getNpcStats(player).getBounty() - + arg); + player.getClass().getNpcStats(player).setBounty( + std::max(0, player.getClass().getNpcStats(player).getBounty() + arg)); // If committing a crime against a faction member, expell from the faction if (!victim.isEmpty() && victim.getClass().isNpc()) { - std::string factionID = victim.getClass().getPrimaryFaction(victim); + const ESM::RefId& factionID = victim.getClass().getPrimaryFaction(victim); - const std::map& playerRanks = player.getClass().getNpcStats(player).getFactionRanks(); - if (playerRanks.find(Misc::StringUtils::lowerCase(factionID)) != playerRanks.end()) + const std::map& playerRanks = player.getClass().getNpcStats(player).getFactionRanks(); + if (playerRanks.find(factionID) != playerRanks.end()) { - player.getClass().getNpcStats(player).expell(factionID); + player.getClass().getNpcStats(player).expell(factionID, true); } } else if (!factionId.empty()) { - const std::map& playerRanks = player.getClass().getNpcStats(player).getFactionRanks(); - if (playerRanks.find(Misc::StringUtils::lowerCase(factionId)) != playerRanks.end()) + const std::map& playerRanks = player.getClass().getNpcStats(player).getFactionRanks(); + if (playerRanks.find(factionId) != playerRanks.end()) { - player.getClass().getNpcStats(player).expell(factionId); + player.getClass().getNpcStats(player).expell(factionId, true); } } if (type == OT_Assault && !victim.isEmpty() - && !victim.getClass().getCreatureStats(victim).getAiSequence().isInCombat(player) - && victim.getClass().isNpc()) + && !victim.getClass().getCreatureStats(victim).getAiSequence().isInCombat(player) + && victim.getClass().isNpc()) { // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. // Note: accidental or collateral damage attacks are ignored. - if (!victim.getClass().getCreatureStats(victim).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) - startCombat(victim, player); + if (!victim.getClass().getCreatureStats(victim).getAiSequence().isInPursuit()) + startCombat(victim, player, &playerFollowers); // Set the crime ID, which we will use to calm down participants // once the bounty has been paid. @@ -1413,32 +1497,16 @@ namespace MWMechanics return reported; } - bool MechanicsManager::actorAttacked(const MWWorld::Ptr &target, const MWWorld::Ptr &attacker) + bool MechanicsManager::actorAttacked(const MWWorld::Ptr& target, const MWWorld::Ptr& attacker) { const MWWorld::Ptr& player = getPlayer(); if (target == player || !attacker.getClass().isActor()) return false; - MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); - if (attacker == player) - { - std::set followersAttacker; - getActorsSidingWith(attacker, followersAttacker); - if (followersAttacker.find(target) != followersAttacker.end()) - { - statsTarget.friendlyHit(); - - if (statsTarget.getFriendlyHits() < 4) - { - MWBase::Environment::get().getDialogueManager()->say(target, "hit"); - return false; - } - } - } - if (canCommitCrimeAgainst(target, attacker)) commitCrime(attacker, target, MWBase::MechanicsManager::OT_Assault); + MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); AiSequence& seq = statsTarget.getAiSequence(); if (!attacker.isEmpty() @@ -1447,35 +1515,50 @@ namespace MWMechanics { // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. // Note: accidental or collateral damage attacks are ignored. - if (!target.getClass().getCreatureStats(target).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) + if (!target.getClass().getCreatureStats(target).getAiSequence().isInPursuit()) { // If an actor has OnPCHitMe declared in his script, his Fight = 0 and the attacker is player, // he will attack the player only if we will force him (e.g. via StartCombat console command) bool peaceful = false; - std::string script = target.getClass().getScript(target); - if (!script.empty() && target.getRefData().getLocals().hasVar(script, "onpchitme") && attacker == player) + const ESM::RefId& script = target.getClass().getScript(target); + if (!script.empty() && target.getRefData().getLocals().hasVar(script, "onpchitme") + && attacker == player) { - int fight = std::max(0, target.getClass().getCreatureStats(target).getAiSetting(CreatureStats::AI_Fight).getModified()); + const int fight + = target.getClass().getCreatureStats(target).getAiSetting(AiSetting::Fight).getModified(); peaceful = (fight == 0); } if (!peaceful) - startCombat(target, attacker); + { + SidingCache cachedAllies{ mActors, false }; + const std::set& attackerAllies = cachedAllies.getActorsSidingWith(attacker); + startCombat(target, attacker, &attackerAllies); + // Force friendly actors into combat to prevent infighting between followers + for (const auto& follower : cachedAllies.getActorsSidingWith(target)) + { + if (follower != attacker && follower != player) + startCombat(follower, attacker, &attackerAllies); + } + } } } return true; } - bool MechanicsManager::canCommitCrimeAgainst(const MWWorld::Ptr &target, const MWWorld::Ptr &attacker) + bool MechanicsManager::canCommitCrimeAgainst(const MWWorld::Ptr& target, const MWWorld::Ptr& attacker) { - const MWMechanics::AiSequence& seq = target.getClass().getCreatureStats(target).getAiSequence(); - return target.getClass().isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker) - && !isAggressive(target, attacker) && !seq.isEngagedWithActor() - && !target.getClass().getCreatureStats(target).getAiSequence().hasPackage(AiPackageTypeId::Pursue); + const MWWorld::Class& cls = target.getClass(); + const MWMechanics::CreatureStats& stats = cls.getCreatureStats(target); + const MWMechanics::AiSequence& seq = stats.getAiSequence(); + return cls.isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker) && !isAggressive(target, attacker) + && !seq.isEngagedWithActor() && !stats.getAiSequence().isInPursuit() + && !cls.getNpcStats(target).isWerewolf() + && stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Vampirism).getMagnitude() <= 0; } - void MechanicsManager::actorKilled(const MWWorld::Ptr &victim, const MWWorld::Ptr &attacker) + void MechanicsManager::actorKilled(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) { if (attacker.isEmpty() || victim.isEmpty()) return; @@ -1487,7 +1570,7 @@ namespace MWMechanics return; // TODO: implement animal rights const MWMechanics::NpcStats& victimStats = victim.getClass().getNpcStats(victim); - const MWWorld::Ptr &player = getPlayer(); + const MWWorld::Ptr& player = getPlayer(); bool canCommit = attacker == player && canCommitCrimeAgainst(victim, attacker); // For now we report only about crimes of player and player's followers @@ -1508,19 +1591,16 @@ namespace MWMechanics commitCrime(player, victim, MWBase::MechanicsManager::OT_Murder); } - bool MechanicsManager::awarenessCheck(const MWWorld::Ptr &ptr, const MWWorld::Ptr &observer) + bool MechanicsManager::awarenessCheck(const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) { if (observer.getClass().getCreatureStats(observer).isDead() || !observer.getRefData().isEnabled()) return false; - const MWWorld::Store& store = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& store + = MWBase::Environment::get().getESMStore()->get(); CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - float invisibility = stats.getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude(); - if (invisibility > 0) - return false; - float sneakTerm = 0; if (isSneaking(ptr)) { @@ -1543,17 +1623,20 @@ namespace MWMechanics static float fSneakDistBase = store.find("fSneakDistanceBase")->mValue.getFloat(); static float fSneakDistMult = store.find("fSneakDistanceMultiplier")->mValue.getFloat(); - osg::Vec3f pos1 (ptr.getRefData().getPosition().asVec3()); - osg::Vec3f pos2 (observer.getRefData().getPosition().asVec3()); + osg::Vec3f pos1(ptr.getRefData().getPosition().asVec3()); + osg::Vec3f pos2(observer.getRefData().getPosition().asVec3()); float distTerm = fSneakDistBase + fSneakDistMult * (pos1 - pos2).length(); - float chameleon = stats.getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); - float x = sneakTerm * distTerm * stats.getFatigueTerm() + chameleon + invisibility; + float chameleon = stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Chameleon).getMagnitude(); + float invisibility = stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Invisibility).getMagnitude(); + float x = sneakTerm * distTerm * stats.getFatigueTerm() + chameleon; + if (invisibility > 0.f) + x += 100.f; CreatureStats& observerStats = observer.getClass().getCreatureStats(observer); float obsAgility = observerStats.getAttribute(ESM::Attribute::Agility).getModified(); float obsLuck = observerStats.getAttribute(ESM::Attribute::Luck).getModified(); - float obsBlind = observerStats.getMagicEffects().get(ESM::MagicEffect::Blind).getMagnitude(); + float obsBlind = observerStats.getMagicEffects().getOrDefault(ESM::MagicEffect::Blind).getMagnitude(); float obsSneak = observer.getClass().getSkill(observer, ESM::Skill::Sneak); float obsTerm = obsSneak + 0.2f * obsAgility + 0.1f * obsLuck - obsBlind; @@ -1565,7 +1648,7 @@ namespace MWMechanics osg::Vec3f vec = pos1 - pos2; if (observer.getRefData().getBaseNode()) { - osg::Vec3f observerDir = (observer.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0)); + osg::Vec3f observerDir = (observer.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0, 1, 0)); float angleRadians = std::acos(observerDir * vec / (observerDir.length() * vec.length())); if (angleRadians > osg::DegreesToRadians(90.f)) @@ -1575,11 +1658,12 @@ namespace MWMechanics } float target = x - y; - - return (Misc::Rng::roll0to99() >= target); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + return (Misc::Rng::roll0to99(prng) >= target); } - void MechanicsManager::startCombat(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target) + void MechanicsManager::startCombat( + const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set* targetAllies) { CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); @@ -1592,27 +1676,64 @@ namespace MWMechanics { // We don't care about dialogue filters since the target is invalid. // We still want to play the combat taunt. - MWBase::Environment::get().getDialogueManager()->say(ptr, "attack"); + MWBase::Environment::get().getDialogueManager()->say(ptr, ESM::RefId::stringRefId("attack")); + if (!stats.getAiSequence().isInCombat()) + stats.resetFriendlyHits(); return; } + const bool inCombat = stats.getAiSequence().isInCombat(); + bool shout = !inCombat; + if (inCombat) + { + const auto isInCombatWithOneOf = [&](const auto& allies) { + for (const MWWorld::Ptr& ally : allies) + { + if (stats.getAiSequence().isInCombat(ally)) + return true; + } + return false; + }; + if (targetAllies) + shout = !isInCombatWithOneOf(*targetAllies); + else + { + shout = stats.getAiSequence().isInCombat(target); + if (!shout) + { + std::set sidingActors; + getActorsSidingWith(target, sidingActors); + shout = !isInCombatWithOneOf(sidingActors); + } + } + } stats.getAiSequence().stack(MWMechanics::AiCombat(target), ptr); if (target == getPlayer()) { // if guard starts combat with player, guards pursuing player should do the same if (ptr.getClass().isClass(ptr, "Guard")) { - stats.setHitAttemptActorId(target.getClass().getCreatureStats(target).getActorId()); // Stops guard from ending combat if player is unreachable - for (Actors::PtrActorMap::const_iterator iter = mActors.begin(); iter != mActors.end(); ++iter) + stats.setHitAttemptActorId( + target.getClass() + .getCreatureStats(target) + .getActorId()); // Stops guard from ending combat if player is unreachable + for (const Actor& actor : mActors) { - if (iter->first.getClass().isClass(iter->first, "Guard")) + if (actor.getPtr().getClass().isClass(actor.getPtr(), "Guard")) { - MWMechanics::AiSequence& aiSeq = iter->first.getClass().getCreatureStats(iter->first).getAiSequence(); + MWMechanics::AiSequence& aiSeq + = actor.getPtr().getClass().getCreatureStats(actor.getPtr()).getAiSequence(); if (aiSeq.getTypeId() == MWMechanics::AiPackageTypeId::Pursue) { aiSeq.stopPursuit(); aiSeq.stack(MWMechanics::AiCombat(target), ptr); - iter->first.getClass().getCreatureStats(iter->first).setHitAttemptActorId(target.getClass().getCreatureStats(target).getActorId()); // Stops guard from ending combat if player is unreachable + actor.getPtr() + .getClass() + .getCreatureStats(actor.getPtr()) + .setHitAttemptActorId( + target.getClass() + .getCreatureStats(target) + .getActorId()); // Stops guard from ending combat if player is unreachable } } } @@ -1620,36 +1741,44 @@ namespace MWMechanics } // Must be done after the target is set up, so that CreatureTargetted dialogue filter works properly - MWBase::Environment::get().getDialogueManager()->say(ptr, "attack"); + if (shout) + MWBase::Environment::get().getDialogueManager()->say(ptr, ESM::RefId::stringRefId("attack")); + } + + void MechanicsManager::stopCombat(const MWWorld::Ptr& actor) + { + mActors.stopCombat(actor); } - void MechanicsManager::getObjectsInRange(const osg::Vec3f &position, float radius, std::vector &objects) + void MechanicsManager::getObjectsInRange( + const osg::Vec3f& position, float radius, std::vector& objects) { mActors.getObjectsInRange(position, radius, objects); mObjects.getObjectsInRange(position, radius, objects); } - void MechanicsManager::getActorsInRange(const osg::Vec3f &position, float radius, std::vector &objects) + void MechanicsManager::getActorsInRange( + const osg::Vec3f& position, float radius, std::vector& objects) { mActors.getObjectsInRange(position, radius, objects); } - bool MechanicsManager::isAnyActorInRange(const osg::Vec3f &position, float radius) + bool MechanicsManager::isAnyActorInRange(const osg::Vec3f& position, float radius) { return mActors.isAnyObjectInRange(position, radius); } - std::list MechanicsManager::getActorsSidingWith(const MWWorld::Ptr& actor) + std::vector MechanicsManager::getActorsSidingWith(const MWWorld::Ptr& actor) { return mActors.getActorsSidingWith(actor); } - std::list MechanicsManager::getActorsFollowing(const MWWorld::Ptr& actor) + std::vector MechanicsManager::getActorsFollowing(const MWWorld::Ptr& actor) { return mActors.getActorsFollowing(actor); } - std::list MechanicsManager::getActorsFollowingIndices(const MWWorld::Ptr& actor) + std::vector MechanicsManager::getActorsFollowingIndices(const MWWorld::Ptr& actor) { return mActors.getActorsFollowingIndices(actor); } @@ -1659,29 +1788,33 @@ namespace MWMechanics return mActors.getActorsFollowingByIndex(actor); } - std::list MechanicsManager::getActorsFighting(const MWWorld::Ptr& actor) { + std::vector MechanicsManager::getActorsFighting(const MWWorld::Ptr& actor) + { return mActors.getActorsFighting(actor); } - std::list MechanicsManager::getEnemiesNearby(const MWWorld::Ptr& actor) { + std::vector MechanicsManager::getEnemiesNearby(const MWWorld::Ptr& actor) + { return mActors.getEnemiesNearby(actor); } - void MechanicsManager::getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) { + void MechanicsManager::getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) + { mActors.getActorsFollowing(actor, out); } - void MechanicsManager::getActorsSidingWith(const MWWorld::Ptr& actor, std::set& out) { + void MechanicsManager::getActorsSidingWith(const MWWorld::Ptr& actor, std::set& out) + { mActors.getActorsSidingWith(actor, out); } int MechanicsManager::countSavedGameRecords() const { return 1 // Death counter - +1; // Stolen items + + 1; // Stolen items } - void MechanicsManager::write(ESM::ESMWriter &writer, Loading::Listener &listener) const + void MechanicsManager::write(ESM::ESMWriter& writer, Loading::Listener& listener) const { mActors.write(writer, listener); @@ -1692,7 +1825,7 @@ namespace MWMechanics writer.endRecord(ESM::REC_STLN); } - void MechanicsManager::readRecord(ESM::ESMReader &reader, uint32_t type) + void MechanicsManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_STLN) { @@ -1712,28 +1845,42 @@ namespace MWMechanics mRaceSelected = false; } - bool MechanicsManager::isAggressive(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target) + bool MechanicsManager::isAggressive(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) { // Don't become aggressive if a calm effect is active, since it would cause combat to cycle on/off as // combat is activated here and then canceled by the calm effect - if ((ptr.getClass().isNpc() && ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::CalmHumanoid).getMagnitude() > 0) - || (!ptr.getClass().isNpc() && ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::CalmCreature).getMagnitude() > 0)) + if ((ptr.getClass().isNpc() + && ptr.getClass() + .getCreatureStats(ptr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::CalmHumanoid) + .getMagnitude() + > 0) + || (!ptr.getClass().isNpc() + && ptr.getClass() + .getCreatureStats(ptr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::CalmCreature) + .getMagnitude() + > 0)) return false; int disposition = 50; if (ptr.getClass().isNpc()) - disposition = getDerivedDisposition(ptr, true); + disposition = getDerivedDisposition(ptr); - int fight = ptr.getClass().getCreatureStats(ptr).getAiSetting(CreatureStats::AI_Fight).getModified() - + static_cast(getFightDistanceBias(ptr, target) + getFightDispositionBias(static_cast(disposition))); + int fight = ptr.getClass().getCreatureStats(ptr).getAiSetting(AiSetting::Fight).getModified() + + static_cast( + getFightDistanceBias(ptr, target) + getFightDispositionBias(static_cast(disposition))); if (ptr.getClass().isNpc() && target.getClass().isNpc()) { - if (target.getClass().getNpcStats(target).isWerewolf() || - (target == getPlayer() && - MWBase::Environment::get().getWorld()->getGlobalInt("pcknownwerewolf"))) + if (target.getClass().getNpcStats(target).isWerewolf() + || (target == getPlayer() + && MWBase::Environment::get().getWorld()->getGlobalInt(MWWorld::Globals::sPCKnownWerewolf))) { - const ESM::GameSetting * iWerewolfFightMod = MWBase::Environment::get().getWorld()->getStore().get().find("iWerewolfFightMod"); + const ESM::GameSetting* iWerewolfFightMod + = MWBase::Environment::get().getESMStore()->get().find("iWerewolfFightMod"); fight += iWerewolfFightMod->mValue.getInteger(); } } @@ -1741,7 +1888,7 @@ namespace MWMechanics return (fight >= 100); } - void MechanicsManager::resurrect(const MWWorld::Ptr &ptr) + void MechanicsManager::resurrect(const MWWorld::Ptr& ptr) { CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); if (stats.isDead()) @@ -1751,17 +1898,17 @@ namespace MWMechanics } } - bool MechanicsManager::isCastingSpell(const MWWorld::Ptr &ptr) const + bool MechanicsManager::isCastingSpell(const MWWorld::Ptr& ptr) const { return mActors.isCastingSpell(ptr); } - bool MechanicsManager::isReadyToBlock(const MWWorld::Ptr &ptr) const + bool MechanicsManager::isReadyToBlock(const MWWorld::Ptr& ptr) const { return mActors.isReadyToBlock(ptr); } - bool MechanicsManager::isAttackingOrSpell(const MWWorld::Ptr &ptr) const + bool MechanicsManager::isAttackingOrSpell(const MWWorld::Ptr& ptr) const { return mActors.isAttackingOrSpell(ptr); } @@ -1776,50 +1923,48 @@ namespace MWMechanics MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); - if (actor == player->getPlayer()) - { - if (werewolf) - { - player->saveStats(); - player->setWerewolfStats(); - } - else - player->restoreStats(); - } - // Werewolfs can not cast spells, so we need to unset the prepared spell if there is one. - if (npcStats.getDrawState() == MWMechanics::DrawState_Spell) - npcStats.setDrawState(MWMechanics::DrawState_Nothing); + if (npcStats.getDrawState() == MWMechanics::DrawState::Spell) + npcStats.setDrawState(MWMechanics::DrawState::Nothing); npcStats.setWerewolf(werewolf); - MWWorld::InventoryStore &inv = actor.getClass().getInventoryStore(actor); + MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); - if(werewolf) + if (werewolf) { - inv.unequipAll(actor); - inv.equip(MWWorld::InventoryStore::Slot_Robe, inv.ContainerStore::add("werewolfrobe", 1, actor), actor); + inv.unequipAll(); + inv.equip(MWWorld::InventoryStore::Slot_Robe, + inv.ContainerStore::add(ESM::RefId::stringRefId("werewolfrobe"), 1)); } else { - inv.unequipSlot(MWWorld::InventoryStore::Slot_Robe, actor); - inv.ContainerStore::remove("werewolfrobe", 1, actor); + inv.unequipSlot(MWWorld::InventoryStore::Slot_Robe); + inv.ContainerStore::remove(ESM::RefId::stringRefId("werewolfrobe"), 1); } - if(actor == player->getPlayer()) + if (actor == player->getPlayer()) { MWBase::Environment::get().getWorld()->reattachPlayerCamera(); // Update the GUI only when called on the player MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + // Transforming removes all temporary effects + actor.getClass().getCreatureStats(actor).getActiveSpells().purge( + [](const auto& params) { return params.hasFlag(ESM::ActiveSpells::Flag_Temporary); }, actor); + mActors.updateActor(actor, 0.f); + if (werewolf) { + player->saveStats(); + player->setWerewolfStats(); windowManager->forceHide(MWGui::GW_Inventory); windowManager->forceHide(MWGui::GW_Magic); } else { + player->restoreStats(); windowManager->unsetForceHide(MWGui::GW_Inventory); windowManager->unsetForceHide(MWGui::GW_Magic); } @@ -1828,8 +1973,10 @@ namespace MWMechanics // Witnesses of the player's transformation will make them a globally known werewolf std::vector neighbors; - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); - getActorsInRange(actor.getRefData().getPosition().asVec3(), gmst.find("fAlarmRadius")->mValue.getFloat(), neighbors); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); + getActorsInRange( + actor.getRefData().getPosition().asVec3(), gmst.find("fAlarmRadius")->mValue.getFloat(), neighbors); bool detected = false, reported = false; for (const MWWorld::Ptr& neighbor : neighbors) @@ -1840,7 +1987,11 @@ namespace MWMechanics if (MWBase::Environment::get().getWorld()->getLOS(neighbor, actor) && awarenessCheck(actor, neighbor)) { detected = true; - if (neighbor.getClass().getCreatureStats(neighbor).getAiSetting(MWMechanics::CreatureStats::AI_Alarm).getModified() > 0) + if (neighbor.getClass() + .getCreatureStats(neighbor) + .getAiSetting(MWMechanics::AiSetting::Alarm) + .getModified() + > 0) { reported = true; break; @@ -1851,26 +2002,27 @@ namespace MWMechanics if (detected) { windowManager->messageBox("#{sWerewolfAlarmMessage}"); - MWBase::Environment::get().getWorld()->setGlobalInt("pcknownwerewolf", 1); + MWBase::Environment::get().getWorld()->setGlobalInt(MWWorld::Globals::sPCKnownWerewolf, 1); if (reported) { - npcStats.setBounty(npcStats.getBounty()+ - gmst.find("iWereWolfBounty")->mValue.getInteger()); + npcStats.setBounty( + std::max(0, npcStats.getBounty() + gmst.find("iWereWolfBounty")->mValue.getInteger())); } } } } - void MechanicsManager::applyWerewolfAcrobatics(const MWWorld::Ptr &actor) + void MechanicsManager::applyWerewolfAcrobatics(const MWWorld::Ptr& actor) { - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); - MWMechanics::NpcStats &stats = actor.getClass().getNpcStats(actor); - - stats.getSkill(ESM::Skill::Acrobatics).setBase(gmst.find("fWerewolfAcrobatics")->mValue.getInteger()); + const ESM::Skill* acrobatics + = MWBase::Environment::get().getESMStore()->get().find(ESM::Skill::Acrobatics); + MWMechanics::NpcStats& stats = actor.getClass().getNpcStats(actor); + auto& skill = stats.getSkill(acrobatics->mId); + skill.setModifier(acrobatics->mWerewolfValue - skill.getModified()); } - void MechanicsManager::cleanupSummonedCreature(const MWWorld::Ptr &caster, int creatureActorId) + void MechanicsManager::cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) { mActors.cleanupSummonedCreature(caster.getClass().getCreatureStats(caster), creatureActorId); } @@ -1881,22 +2033,22 @@ namespace MWMechanics stats.setAttribute(frameNumber, "Mechanics Objects", mObjects.size()); } - int MechanicsManager::getGreetingTimer(const MWWorld::Ptr &ptr) const + int MechanicsManager::getGreetingTimer(const MWWorld::Ptr& ptr) const { return mActors.getGreetingTimer(ptr); } - float MechanicsManager::getAngleToPlayer(const MWWorld::Ptr &ptr) const + float MechanicsManager::getAngleToPlayer(const MWWorld::Ptr& ptr) const { return mActors.getAngleToPlayer(ptr); } - GreetingState MechanicsManager::getGreetingState(const MWWorld::Ptr &ptr) const + GreetingState MechanicsManager::getGreetingState(const MWWorld::Ptr& ptr) const { return mActors.getGreetingState(ptr); } - bool MechanicsManager::isTurningToPlayer(const MWWorld::Ptr &ptr) const + bool MechanicsManager::isTurningToPlayer(const MWWorld::Ptr& ptr) const { return mActors.isTurningToPlayer(ptr); } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 3f2c3f5e98d..4b0126cd34f 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -7,10 +7,14 @@ #include "../mwworld/ptr.hpp" -#include "creaturestats.hpp" +#include "actors.hpp" #include "npcstats.hpp" #include "objects.hpp" -#include "actors.hpp" + +namespace MWSound +{ + enum class MusicType; +} namespace MWWorld { @@ -21,223 +25,231 @@ namespace MWMechanics { class MechanicsManager : public MWBase::MechanicsManager { - bool mUpdatePlayer; - bool mClassSelected; - bool mRaceSelected; - bool mAI;///< is AI active? - - Objects mObjects; - Actors mActors; - - typedef std::pair Owner; // < Owner id, bool isFaction > - typedef std::map OwnerMap; // < Owner, number of stolen items with this id from this owner > - typedef std::map StolenItemsMap; - StolenItemsMap mStolenItems; - - public: - - void buildPlayer(); - ///< build player according to stored class/race/birthsign information. Will - /// default to the values of the ESM::NPC object, if no explicit information is given. - - MechanicsManager(); - - void add (const MWWorld::Ptr& ptr) override; - ///< Register an object for management - - void remove (const MWWorld::Ptr& ptr) override; - ///< Deregister an object for management - - void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) override; - ///< Moves an object to a new cell - - void drop(const MWWorld::CellStore *cellStore) override; - ///< Deregister all objects in the given cell. - - void update (float duration, bool paused) override; - ///< Update objects - /// - /// \param paused In game type does not currently advance (this usually means some GUI - /// component is up). - - void setPlayerName (const std::string& name) override; - ///< Set player name. - - void setPlayerRace (const std::string& id, bool male, const std::string &head, const std::string &hair) override; - ///< Set player race. - - void setPlayerBirthsign (const std::string& id) override; - ///< Set player birthsign. + bool mUpdatePlayer; + bool mClassSelected; + bool mRaceSelected; + bool mAI; ///< is AI active? - void setPlayerClass (const std::string& id) override; - ///< Set player class to stock class. + Objects mObjects; + Actors mActors; - void setPlayerClass (const ESM::Class& class_) override; - ///< Set player class to custom class. + typedef std::pair Owner; // < Owner id, bool isFaction > + typedef std::map OwnerMap; // < Owner, number of stolen items with this id from this owner > + typedef std::map StolenItemsMap; + StolenItemsMap mStolenItems; - void restoreDynamicStats(MWWorld::Ptr actor, double hours, bool sleep) override; + public: + void buildPlayer(); + ///< build player according to stored class/race/birthsign information. Will + /// default to the values of the ESM::NPC object, if no explicit information is given. - void rest(double hours, bool sleep) override; - ///< If the player is sleeping or waiting, this should be called every hour. - /// @param sleep is the player sleeping or waiting? + MechanicsManager(); + + void add(const MWWorld::Ptr& ptr) override; + ///< Register an object for management + + void remove(const MWWorld::Ptr& ptr, bool keepActive) override; + ///< Deregister an object for management + + void updateCell(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) override; + ///< Moves an object to a new cell + + void drop(const MWWorld::CellStore* cellStore) override; + ///< Deregister all objects in the given cell. + + void update(float duration, bool paused); + ///< Update objects + /// + /// \param paused In game type does not currently advance (this usually means some GUI + /// component is up). + + void setPlayerName(const std::string& name) override; + ///< Set player name. + + void setPlayerRace(const ESM::RefId& id, bool male, const ESM::RefId& head, const ESM::RefId& hair) override; + ///< Set player race. + + void setPlayerBirthsign(const ESM::RefId& id) override; + ///< Set player birthsign. + + void setPlayerClass(const ESM::RefId& id) override; + ///< Set player class to stock class. + + void setPlayerClass(const ESM::Class& class_) override; + ///< Set player class to custom class. + + void restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep) override; - int getHoursToRest() const override; - ///< Calculate how many hours the player needs to rest in order to be fully healed + void rest(double hours, bool sleep) override; + ///< If the player is sleeping or waiting, this should be called every hour. + /// @param sleep is the player sleeping or waiting? - int getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) override; - ///< This is used by every service to determine the price of objects given the trading skills of the player and NPC. + int getHoursToRest() const override; + ///< Calculate how many hours the player needs to rest in order to be fully healed - int getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange = true) override; - ///< Calculate the diposition of an NPC toward the player. + int getBarterOffer(const MWWorld::Ptr& ptr, int basePrice, bool buying) override; + ///< This is used by every service to determine the price of objects given the trading skills of the player and + ///< NPC. - int countDeaths (const std::string& id) const override; - ///< Return the number of deaths for actors with the given ID. + int getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp = true) override; + ///< Calculate the diposition of an NPC toward the player. - void getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) override; - ///< Perform a persuasion action on NPC + int countDeaths(const ESM::RefId& id) const override; + ///< Return the number of deaths for actors with the given ID. - /// Check if \a observer is potentially aware of \a ptr. Does not do a line of sight check! - bool awarenessCheck (const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) override; + void getPersuasionDispositionChange( + const MWWorld::Ptr& npc, PersuasionType type, bool& success, int& tempChange, int& permChange) override; + ///< Perform a persuasion action on NPC - /// Makes \a ptr fight \a target. Also shouts a combat taunt. - void startCombat (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) override; + /// Check if \a observer is potentially aware of \a ptr. Does not do a line of sight check! + bool awarenessCheck(const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) override; - /** - * @note victim may be empty - * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. - * @param victimAware Is the victim already aware of the crime? - * If this parameter is false, it will be determined by a line-of-sight and awareness check. - * @return was the crime seen? - */ - bool commitCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, - OffenseType type, const std::string& factionId="", int arg=0, bool victimAware=false) override; - /// @return false if the attack was considered a "friendly hit" and forgiven - bool actorAttacked (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) override; + /// Makes \a ptr fight \a target. Also shouts a combat taunt. + void startCombat( + const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set* targetAllies) override; - /// Notify that actor was killed, add a murder bounty if applicable - /// @note No-op for non-player attackers - void actorKilled (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) override; + void stopCombat(const MWWorld::Ptr& ptr) override; - /// Utility to check if taking this item is illegal and calling commitCrime if so - /// @param container The container the item is in; may be empty for an item in the world - void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, - int count, bool alarm = true) override; - /// Utility to check if unlocking this object is illegal and calling commitCrime if so - void unlockAttempted (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item) override; - /// Attempt sleeping in a bed. If this is illegal, call commitCrime. - /// @return was it illegal, and someone saw you doing it? Also returns fail when enemies are nearby - bool sleepInBed (const MWWorld::Ptr& ptr, const MWWorld::Ptr& bed) override; + /** + * @note victim may be empty + * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. + * @param victimAware Is the victim already aware of the crime? + * If this parameter is false, it will be determined by a line-of-sight and awareness check. + * @return was the crime seen? + */ + bool commitCrime(const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, OffenseType type, + const ESM::RefId& factionId = ESM::RefId(), int arg = 0, bool victimAware = false) override; + /// @return false if the attack was considered a "friendly hit" and forgiven + bool actorAttacked(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) override; - void forceStateUpdate(const MWWorld::Ptr &ptr) override; + /// Notify that actor was killed, add a murder bounty if applicable + /// @note No-op for non-player attackers + void actorKilled(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) override; - /// Attempt to play an animation group - /// @return Success or error - bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false) override; - void skipAnimation(const MWWorld::Ptr& ptr) override; - bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string &groupName) override; - void persistAnimationStates() override; + /// Utility to check if taking this item is illegal and calling commitCrime if so + /// @param container The container the item is in; may be empty for an item in the world + void itemTaken(const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, int count, + bool alarm = true) override; + /// Utility to check if unlocking this object is illegal and calling commitCrime if so + void unlockAttempted(const MWWorld::Ptr& ptr, const MWWorld::Ptr& item) override; + /// Attempt sleeping in a bed. If this is illegal, call commitCrime. + /// @return was it illegal, and someone saw you doing it? Also returns fail when enemies are nearby + bool sleepInBed(const MWWorld::Ptr& ptr, const MWWorld::Ptr& bed) override; - /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently - /// paused we may want to do it manually (after equipping permanent enchantment) - void updateMagicEffects (const MWWorld::Ptr& ptr) override; + void forceStateUpdate(const MWWorld::Ptr& ptr) override; - void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector& objects) override; - void getActorsInRange(const osg::Vec3f &position, float radius, std::vector &objects) override; + /// Attempt to play an animation group + /// @return Success or error + bool playAnimationGroup(const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, + bool scripted = false) override; + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) override; + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) override; + void skipAnimation(const MWWorld::Ptr& ptr) override; + bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) override; + bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const override; + void persistAnimationStates() override; + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) override; - /// Check if there are actors in selected range - bool isAnyActorInRange(const osg::Vec3f &position, float radius) override; + /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently + /// paused we may want to do it manually (after equipping permanent enchantment) + void updateMagicEffects(const MWWorld::Ptr& ptr) override; - std::list getActorsSidingWith(const MWWorld::Ptr& actor) override; - std::list getActorsFollowing(const MWWorld::Ptr& actor) override; - std::list getActorsFollowingIndices(const MWWorld::Ptr& actor) override; - std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) override; + void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& objects) override; + void getActorsInRange(const osg::Vec3f& position, float radius, std::vector& objects) override; - std::list getActorsFighting(const MWWorld::Ptr& actor) override; - std::list getEnemiesNearby(const MWWorld::Ptr& actor) override; + /// Check if there are actors in selected range + bool isAnyActorInRange(const osg::Vec3f& position, float radius) override; - /// Recursive version of getActorsFollowing - void getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) override; - /// Recursive version of getActorsSidingWith - void getActorsSidingWith(const MWWorld::Ptr& actor, std::set& out) override; + std::vector getActorsSidingWith(const MWWorld::Ptr& actor) override; + std::vector getActorsFollowing(const MWWorld::Ptr& actor) override; + std::vector getActorsFollowingIndices(const MWWorld::Ptr& actor) override; + std::map getActorsFollowingByIndex(const MWWorld::Ptr& actor) override; - bool toggleAI() override; - bool isAIActive() override; + std::vector getActorsFighting(const MWWorld::Ptr& actor) override; + std::vector getEnemiesNearby(const MWWorld::Ptr& actor) override; - void playerLoaded() override; + /// Recursive version of getActorsFollowing + void getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) override; + /// Recursive version of getActorsSidingWith + void getActorsSidingWith(const MWWorld::Ptr& actor, std::set& out) override; - bool onOpen(const MWWorld::Ptr& ptr) override; - void onClose(const MWWorld::Ptr& ptr) override; + bool toggleAI() override; + bool isAIActive() override; - int countSavedGameRecords() const override; + void playerLoaded() override; - void write (ESM::ESMWriter& writer, Loading::Listener& listener) const override; + bool onOpen(const MWWorld::Ptr& ptr) override; + void onClose(const MWWorld::Ptr& ptr) override; - void readRecord (ESM::ESMReader& reader, uint32_t type) override; + int countSavedGameRecords() const override; - void clear() override; + void write(ESM::ESMWriter& writer, Loading::Listener& listener) const override; - bool isAggressive (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) override; + void readRecord(ESM::ESMReader& reader, uint32_t type) override; - void resurrect(const MWWorld::Ptr& ptr) override; + void clear() override; - bool isCastingSpell (const MWWorld::Ptr& ptr) const override; + bool isAggressive(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) override; - bool isReadyToBlock (const MWWorld::Ptr& ptr) const override; - /// Is \a ptr casting spell or using weapon now? - bool isAttackingOrSpell(const MWWorld::Ptr &ptr) const override; + void resurrect(const MWWorld::Ptr& ptr) override; - void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell=false) override; + bool isCastingSpell(const MWWorld::Ptr& ptr) const override; - void processChangedSettings(const Settings::CategorySettingVector& settings) override; + bool isReadyToBlock(const MWWorld::Ptr& ptr) const override; + /// Is \a ptr casting spell or using weapon now? + bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const override; - float getActorsProcessingRange() const override; + void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell = false) override; - void notifyDied(const MWWorld::Ptr& actor) override; + void processChangedSettings(const Settings::CategorySettingVector& settings) override; - /// Check if the target actor was detected by an observer - /// If the observer is a non-NPC, check all actors in AI processing distance as observers - bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) override; + void notifyDied(const MWWorld::Ptr& actor) override; - void confiscateStolenItems (const MWWorld::Ptr& player, const MWWorld::Ptr& targetContainer) override; + /// Check if the target actor was detected by an observer + /// If the observer is a non-NPC, check all actors in AI processing distance as observers + bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) override; - /// List the owners that the player has stolen this item from (the owner can be an NPC or a faction). - /// - std::vector > getStolenItemOwners(const std::string& itemid) override; + void confiscateStolenItems(const MWWorld::Ptr& player, const MWWorld::Ptr& targetContainer) override; - /// Has the player stolen this item from the given owner? - bool isItemStolenFrom(const std::string& itemid, const MWWorld::Ptr& ptr) override; + /// List the owners that the player has stolen this item from (the owner can be an NPC or a faction). + /// + std::vector> getStolenItemOwners(const ESM::RefId& itemid) override; - bool isBoundItem(const MWWorld::Ptr& item) override; + /// Has the player stolen this item from the given owner? + bool isItemStolenFrom(const ESM::RefId& itemid, const MWWorld::Ptr& ptr) override; - /// @return is \a ptr allowed to take/use \a target or is it a crime? - bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) override; + bool isBoundItem(const MWWorld::Ptr& item) override; - void setWerewolf(const MWWorld::Ptr& actor, bool werewolf) override; - void applyWerewolfAcrobatics(const MWWorld::Ptr& actor) override; + /// @return is \a ptr allowed to take/use \a target or is it a crime? + bool isAllowedToUse(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) override; - void cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) override; + void setWerewolf(const MWWorld::Ptr& actor, bool werewolf) override; + void applyWerewolfAcrobatics(const MWWorld::Ptr& actor) override; - void confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count) override; + void cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) override; - bool isAttackPreparing(const MWWorld::Ptr& ptr) override; - bool isRunning(const MWWorld::Ptr& ptr) override; - bool isSneaking(const MWWorld::Ptr& ptr) override; + void confiscateStolenItemToOwner( + const MWWorld::Ptr& player, const MWWorld::Ptr& item, const MWWorld::Ptr& victim, int count) override; - void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; + bool isAttackPreparing(const MWWorld::Ptr& ptr) override; + bool isRunning(const MWWorld::Ptr& ptr) override; + bool isSneaking(const MWWorld::Ptr& ptr) override; - int getGreetingTimer(const MWWorld::Ptr& ptr) const override; - float getAngleToPlayer(const MWWorld::Ptr& ptr) const override; - GreetingState getGreetingState(const MWWorld::Ptr& ptr) const override; - bool isTurningToPlayer(const MWWorld::Ptr& ptr) const override; + void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; - void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) override; + int getGreetingTimer(const MWWorld::Ptr& ptr) const override; + float getAngleToPlayer(const MWWorld::Ptr& ptr) const override; + GreetingState getGreetingState(const MWWorld::Ptr& ptr) const override; + bool isTurningToPlayer(const MWWorld::Ptr& ptr) const override; - private: - bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); - bool canReportCrime(const MWWorld::Ptr &actor, const MWWorld::Ptr &victim, std::set &playerFollowers); + private: + bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); + bool canReportCrime( + const MWWorld::Ptr& actor, const MWWorld::Ptr& victim, std::set& playerFollowers); - bool reportCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, - OffenseType type, const std::string& factionId, int arg=0); + bool reportCrime(const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, OffenseType type, + const ESM::RefId& factionId, int arg = 0); }; } diff --git a/apps/openmw/mwmechanics/movement.hpp b/apps/openmw/mwmechanics/movement.hpp index 57e106cdecf..20606cff458 100644 --- a/apps/openmw/mwmechanics/movement.hpp +++ b/apps/openmw/mwmechanics/movement.hpp @@ -27,10 +27,7 @@ namespace MWMechanics mIsStrafing = false; } - osg::Vec3f asVec3() - { - return osg::Vec3f(mPosition[0], mPosition[1], mPosition[2]); - } + osg::Vec3f asVec3() { return osg::Vec3f(mPosition[0], mPosition[1], mPosition[2]); } }; } diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index 5d19368bf65..eceaf8b482a 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -1,30 +1,37 @@ #include "npcstats.hpp" +#include #include +#include -#include -#include -#include -#include +#include +#include +#include +#include + +#include + +#include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" MWMechanics::NpcStats::NpcStats() - : mDisposition (0) -, mReputation(0) -, mCrimeId(-1) -, mBounty(0) -, mWerewolfKills (0) -, mLevelProgress(0) -, mTimeToStartDrowning(-1.0) // set breath to special value, it will be replaced during actor update + : mDisposition(0) + , mCrimeDispositionModifier(0) + , mReputation(0) + , mCrimeId(-1) + , mBounty(0) + , mWerewolfKills(0) + , mLevelProgress(0) + , mTimeToStartDrowning(-1.0) // set breath to special value, it will be replaced during actor update , mIsWerewolf(false) { - mSkillIncreases.resize (ESM::Attribute::Length, 0); mSpecIncreases.resize(3, 0); + for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) + mSkills.emplace(skill.mId, SkillValue{}); } int MWMechanics::NpcStats::getBaseDisposition() const @@ -37,267 +44,189 @@ void MWMechanics::NpcStats::setBaseDisposition(int disposition) mDisposition = disposition; } -const MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill (int index) const +int MWMechanics::NpcStats::getCrimeDispositionModifier() const { - if (index<0 || index>=ESM::Skill::Length) - throw std::runtime_error ("skill index out of range"); + return mCrimeDispositionModifier; +} - return mSkill[index]; +void MWMechanics::NpcStats::setCrimeDispositionModifier(int value) +{ + mCrimeDispositionModifier = value; } -MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill (int index) +void MWMechanics::NpcStats::modCrimeDispositionModifier(int value) { - if (index<0 || index>=ESM::Skill::Length) - throw std::runtime_error ("skill index out of range"); + mCrimeDispositionModifier += value; +} - return mSkill[index]; +const MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill(ESM::RefId id) const +{ + auto it = mSkills.find(id); + if (it == mSkills.end()) + throw std::runtime_error("skill not found"); + return it->second; } -void MWMechanics::NpcStats::setSkill(int index, const MWMechanics::SkillValue &value) +MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill(ESM::RefId id) { - if (index<0 || index>=ESM::Skill::Length) - throw std::runtime_error ("skill index out of range"); + auto it = mSkills.find(id); + if (it == mSkills.end()) + throw std::runtime_error("skill not found"); + return it->second; +} - mSkill[index] = value; +void MWMechanics::NpcStats::setSkill(ESM::RefId id, const MWMechanics::SkillValue& value) +{ + auto it = mSkills.find(id); + if (it == mSkills.end()) + throw std::runtime_error("skill not found"); + it->second = value; } -const std::map& MWMechanics::NpcStats::getFactionRanks() const +const std::map& MWMechanics::NpcStats::getFactionRanks() const { return mFactionRank; } -int MWMechanics::NpcStats::getFactionRank(const std::string &faction) const +int MWMechanics::NpcStats::getFactionRank(const ESM::RefId& faction) const { - const std::string lower = Misc::StringUtils::lowerCase(faction); - std::map::const_iterator it = mFactionRank.find(lower); + auto it = mFactionRank.find(faction); if (it != mFactionRank.end()) return it->second; return -1; } -void MWMechanics::NpcStats::raiseRank(const std::string &faction) +void MWMechanics::NpcStats::joinFaction(const ESM::RefId& faction) { - const std::string lower = Misc::StringUtils::lowerCase(faction); - std::map::iterator it = mFactionRank.find(lower); - if (it != mFactionRank.end()) - { - // Does the next rank exist? - const ESM::Faction* factionPtr = MWBase::Environment::get().getWorld()->getStore().get().find(lower); - if (it->second+1 < 10 && !factionPtr->mRanks[it->second+1].empty()) - it->second += 1; - } + auto it = mFactionRank.find(faction); + if (it == mFactionRank.end()) + mFactionRank[faction] = 0; } -void MWMechanics::NpcStats::lowerRank(const std::string &faction) +void MWMechanics::NpcStats::setFactionRank(const ESM::RefId& faction, int newRank) { - const std::string lower = Misc::StringUtils::lowerCase(faction); - std::map::iterator it = mFactionRank.find(lower); + auto it = mFactionRank.find(faction); if (it != mFactionRank.end()) { - it->second = it->second-1; - if (it->second < 0) + const ESM::Faction* factionPtr = MWBase::Environment::get().getESMStore()->get().find(faction); + if (newRank < 0) { mFactionRank.erase(it); - mExpelled.erase(lower); + mExpelled.erase(faction); } + else if (newRank < static_cast(factionPtr->mData.mRankData.size())) + do + it->second = newRank; + // Does the new rank exist? + while (newRank > 0 && factionPtr->mRanks[newRank--].empty()); } } -void MWMechanics::NpcStats::joinFaction(const std::string& faction) +bool MWMechanics::NpcStats::getExpelled(const ESM::RefId& factionID) const { - const std::string lower = Misc::StringUtils::lowerCase(faction); - std::map::iterator it = mFactionRank.find(lower); - if (it == mFactionRank.end()) - mFactionRank[lower] = 0; + return mExpelled.find(factionID) != mExpelled.end(); } -bool MWMechanics::NpcStats::getExpelled(const std::string& factionID) const +void MWMechanics::NpcStats::expell(const ESM::RefId& factionID, bool printMessage) { - return mExpelled.find(Misc::StringUtils::lowerCase(factionID)) != mExpelled.end(); -} - -void MWMechanics::NpcStats::expell(const std::string& factionID) -{ - std::string lower = Misc::StringUtils::lowerCase(factionID); - if (mExpelled.find(lower) == mExpelled.end()) + if (mExpelled.find(factionID) == mExpelled.end()) { + mExpelled.insert(factionID); + if (!printMessage) + return; + std::string message = "#{sExpelledMessage}"; - message += MWBase::Environment::get().getWorld()->getStore().get().find(factionID)->mName; + message += MWBase::Environment::get().getESMStore()->get().find(factionID)->mName; MWBase::Environment::get().getWindowManager()->messageBox(message); - mExpelled.insert(lower); } } -void MWMechanics::NpcStats::clearExpelled(const std::string& factionID) +void MWMechanics::NpcStats::clearExpelled(const ESM::RefId& factionID) { - mExpelled.erase(Misc::StringUtils::lowerCase(factionID)); + mExpelled.erase(factionID); } -bool MWMechanics::NpcStats::isInFaction (const std::string& faction) const +bool MWMechanics::NpcStats::isInFaction(const ESM::RefId& faction) const { - return (mFactionRank.find(Misc::StringUtils::lowerCase(faction)) != mFactionRank.end()); + return (mFactionRank.find(faction) != mFactionRank.end()); } -int MWMechanics::NpcStats::getFactionReputation (const std::string& faction) const +int MWMechanics::NpcStats::getFactionReputation(const ESM::RefId& faction) const { - std::map::const_iterator iter = mFactionReputation.find (Misc::StringUtils::lowerCase(faction)); + auto iter = mFactionReputation.find(faction); - if (iter==mFactionReputation.end()) + if (iter == mFactionReputation.end()) return 0; return iter->second; } -void MWMechanics::NpcStats::setFactionReputation (const std::string& faction, int value) +void MWMechanics::NpcStats::setFactionReputation(const ESM::RefId& faction, int value) { - mFactionReputation[Misc::StringUtils::lowerCase(faction)] = value; + mFactionReputation[faction] = value; } -float MWMechanics::NpcStats::getSkillProgressRequirement (int skillIndex, const ESM::Class& class_) const +float MWMechanics::NpcStats::getSkillProgressRequirement(ESM::RefId id, const ESM::Class& class_) const { - float progressRequirement = static_cast(1 + getSkill(skillIndex).getBase()); - - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); + float progressRequirement = 1.f + getSkill(id).getBase(); - float typeFactor = gmst.find ("fMiscSkillBonus")->mValue.getFloat(); + const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); + const ESM::Skill* skill = MWBase::Environment::get().getESMStore()->get().find(id); - for (int i=0; i<5; ++i) + float typeFactor = gmst.find("fMiscSkillBonus")->mValue.getFloat(); + int index = ESM::Skill::refIdToIndex(skill->mId); + for (const auto& skills : class_.mData.mSkills) { - if (class_.mData.mSkills[i][0]==skillIndex) + if (skills[0] == index) { - typeFactor = gmst.find ("fMinorSkillBonus")->mValue.getFloat(); + typeFactor = gmst.find("fMinorSkillBonus")->mValue.getFloat(); break; } - else if (class_.mData.mSkills[i][1]==skillIndex) + else if (skills[1] == index) { - typeFactor = gmst.find ("fMajorSkillBonus")->mValue.getFloat(); + typeFactor = gmst.find("fMajorSkillBonus")->mValue.getFloat(); break; } } progressRequirement *= typeFactor; - if (typeFactor<=0) - throw std::runtime_error ("invalid skill type factor"); + if (typeFactor <= 0) + throw std::runtime_error("invalid skill type factor"); float specialisationFactor = 1; - const ESM::Skill *skill = - MWBase::Environment::get().getWorld()->getStore().get().find (skillIndex); - if (skill->mData.mSpecialization==class_.mData.mSpecialization) + if (skill->mData.mSpecialization == class_.mData.mSpecialization) { - specialisationFactor = gmst.find ("fSpecialSkillBonus")->mValue.getFloat(); + specialisationFactor = gmst.find("fSpecialSkillBonus")->mValue.getFloat(); - if (specialisationFactor<=0) - throw std::runtime_error ("invalid skill specialisation factor"); + if (specialisationFactor <= 0) + throw std::runtime_error("invalid skill specialisation factor"); } progressRequirement *= specialisationFactor; return progressRequirement; } -void MWMechanics::NpcStats::useSkill (int skillIndex, const ESM::Class& class_, int usageType, float extraFactor) +int MWMechanics::NpcStats::getLevelProgress() const { - const ESM::Skill *skill = - MWBase::Environment::get().getWorld()->getStore().get().find (skillIndex); - float skillGain = 1; - if (usageType>=4) - throw std::runtime_error ("skill usage type out of range"); - if (usageType>=0) - { - skillGain = skill->mData.mUseValue[usageType]; - if (skillGain<0) - throw std::runtime_error ("invalid skill gain factor"); - } - skillGain *= extraFactor; - - MWMechanics::SkillValue& value = getSkill (skillIndex); - - value.setProgress(value.getProgress() + skillGain); - - if (int(value.getProgress())>=int(getSkillProgressRequirement(skillIndex, class_))) - { - // skill levelled up - increaseSkill(skillIndex, class_, false); - } -} - -void MWMechanics::NpcStats::increaseSkill(int skillIndex, const ESM::Class &class_, bool preserveProgress, bool readBook) -{ - float base = getSkill (skillIndex).getBase(); - - if (base >= 100.f) - return; - - base += 1; - - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); - - // is this a minor or major skill? - int increase = gmst.find("iLevelupMiscMultAttriubte")->mValue.getInteger(); // Note: GMST has a typo - for (int k=0; k<5; ++k) - { - if (class_.mData.mSkills[k][0] == skillIndex) - { - mLevelProgress += gmst.find("iLevelUpMinorMult")->mValue.getInteger(); - increase = gmst.find("iLevelUpMinorMultAttribute")->mValue.getInteger(); - break; - } - else if (class_.mData.mSkills[k][1] == skillIndex) - { - mLevelProgress += gmst.find("iLevelUpMajorMult")->mValue.getInteger(); - increase = gmst.find("iLevelUpMajorMultAttribute")->mValue.getInteger(); - break; - } - } - - const ESM::Skill* skill = - MWBase::Environment::get().getWorld ()->getStore ().get().find(skillIndex); - mSkillIncreases[skill->mData.mAttribute] += increase; - - mSpecIncreases[skill->mData.mSpecialization] += gmst.find("iLevelupSpecialization")->mValue.getInteger(); - - // Play sound & skill progress notification - /// \todo check if character is the player, if levelling is ever implemented for NPCs - MWBase::Environment::get().getWindowManager()->playSound("skillraise"); - - std::string message = MWBase::Environment::get().getWindowManager ()->getGameSettingString ("sNotifyMessage39", ""); - message = Misc::StringUtils::format(message, ("#{" + ESM::Skill::sSkillNameIds[skillIndex] + "}"), static_cast(base)); - - if (readBook) - message = "#{sBookSkillMessage}\n" + message; - - MWBase::Environment::get().getWindowManager ()->messageBox(message, MWGui::ShowInDialogueMode_Never); - - if (mLevelProgress >= gmst.find("iLevelUpTotal")->mValue.getInteger()) - { - // levelup is possible now - MWBase::Environment::get().getWindowManager ()->messageBox ("#{sLevelUpMsg}", MWGui::ShowInDialogueMode_Never); - } - - getSkill(skillIndex).setBase (base); - if (!preserveProgress) - getSkill(skillIndex).setProgress(0); + return mLevelProgress; } -int MWMechanics::NpcStats::getLevelProgress () const +void MWMechanics::NpcStats::setLevelProgress(int progress) { - return mLevelProgress; + mLevelProgress = progress; } void MWMechanics::NpcStats::levelUp() { - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); mLevelProgress -= gmst.find("iLevelUpTotal")->mValue.getInteger(); mLevelProgress = std::max(0, mLevelProgress); // might be necessary when levelup was invoked via console - for (int i=0; isecond == 0) return 1; - - num = std::min(10, num); + int num = std::min(10, it->second); // iLevelUp01Mult - iLevelUp10Mult std::stringstream gmst; gmst << "iLevelUp" << std::setfill('0') << std::setw(2) << num << "Mult"; - return MWBase::Environment::get().getWorld()->getStore().get().find(gmst.str())->mValue.getInteger(); + return MWBase::Environment::get().getESMStore()->get().find(gmst.str())->mValue.getInteger(); } -int MWMechanics::NpcStats::getSkillIncreasesForSpecialization(int spec) const +int MWMechanics::NpcStats::getSkillIncreasesForAttribute(ESM::Attribute::AttributeID attribute) const +{ + auto it = mSkillIncreases.find(attribute); + if (it == mSkillIncreases.end()) + return 0; + return it->second; +} + +void MWMechanics::NpcStats::setSkillIncreasesForAttribute(ESM::Attribute::AttributeID attribute, int increases) +{ + if (increases == 0) + mSkillIncreases.erase(attribute); + else + mSkillIncreases[attribute] = increases; +} + +int MWMechanics::NpcStats::getSkillIncreasesForSpecialization(ESM::Class::Specialization spec) const { return mSpecIncreases[spec]; } -void MWMechanics::NpcStats::flagAsUsed (const std::string& id) +void MWMechanics::NpcStats::setSkillIncreasesForSpecialization(ESM::Class::Specialization spec, int increases) { - mUsedIds.insert (id); + assert(spec >= 0 && spec < 3); + mSpecIncreases[spec] = increases; } -bool MWMechanics::NpcStats::hasBeenUsed (const std::string& id) const +void MWMechanics::NpcStats::flagAsUsed(const ESM::RefId& id) { - return mUsedIds.find (id)!=mUsedIds.end(); + mUsedIds.insert(id); +} + +bool MWMechanics::NpcStats::hasBeenUsed(const ESM::RefId& id) const +{ + return mUsedIds.find(id) != mUsedIds.end(); } int MWMechanics::NpcStats::getBounty() const @@ -358,7 +307,7 @@ int MWMechanics::NpcStats::getBounty() const return mBounty; } -void MWMechanics::NpcStats::setBounty (int bounty) +void MWMechanics::NpcStats::setBounty(int bounty) { mBounty = bounty; } @@ -371,7 +320,7 @@ int MWMechanics::NpcStats::getReputation() const void MWMechanics::NpcStats::setReputation(int reputation) { // Reputation is capped in original engine - mReputation = std::min(255, std::max(0, reputation)); + mReputation = std::clamp(reputation, 0, 255); } int MWMechanics::NpcStats::getCrimeId() const @@ -384,46 +333,43 @@ void MWMechanics::NpcStats::setCrimeId(int id) mCrimeId = id; } -bool MWMechanics::NpcStats::hasSkillsForRank (const std::string& factionId, int rank) const +bool MWMechanics::NpcStats::hasSkillsForRank(const ESM::RefId& factionId, int rank) const { - if (rank<0 || rank>=10) - throw std::runtime_error ("rank index out of range"); + const ESM::Faction& faction = *MWBase::Environment::get().getESMStore()->get().find(factionId); - const ESM::Faction& faction = - *MWBase::Environment::get().getWorld()->getStore().get().find (factionId); + const ESM::RankData& rankData = faction.mData.mRankData.at(rank); std::vector skills; - for (int i=0; i<7; ++i) + for (int index : faction.mData.mSkills) { - if (faction.mData.mSkills[i] != -1) - skills.push_back (static_cast (getSkill (faction.mData.mSkills[i]).getBase())); + ESM::RefId id = ESM::Skill::indexToRefId(index); + if (!id.empty()) + skills.push_back(static_cast(getSkill(id).getBase())); } if (skills.empty()) return true; - std::sort (skills.begin(), skills.end()); + std::sort(skills.begin(), skills.end()); std::vector::const_reverse_iterator iter = skills.rbegin(); - const ESM::RankData& rankData = faction.mData.mRankData[rank]; - - if (*iter::const_iterator iter (mFactionRank.begin()); - iter!=mFactionRank.end(); ++iter) + for (std::map::const_iterator iter(mFactionRank.begin()); iter != mFactionRank.end(); ++iter) state.mFactions[iter->first].mRank = iter->second; state.mDisposition = mDisposition; + state.mCrimeDispositionModifier = mCrimeDispositionModifier; - for (int i=0; i= 0); + value.writeState(state.mSkills[static_cast(index)]); + } state.mIsWerewolf = mIsWerewolf; @@ -488,55 +439,64 @@ void MWMechanics::NpcStats::writeState (ESM::NpcStats& state) const state.mBounty = mBounty; - for (std::set::const_iterator iter (mExpelled.begin()); - iter!=mExpelled.end(); ++iter) + for (auto iter(mExpelled.begin()); iter != mExpelled.end(); ++iter) state.mFactions[*iter].mExpelled = true; - for (std::map::const_iterator iter (mFactionReputation.begin()); - iter!=mFactionReputation.end(); ++iter) + for (auto iter(mFactionReputation.begin()); iter != mFactionReputation.end(); ++iter) state.mFactions[iter->first].mReputation = iter->second; state.mReputation = mReputation; state.mWerewolfKills = mWerewolfKills; state.mLevelProgress = mLevelProgress; - for (int i=0; i= 0); + state.mSkillIncrease[static_cast(index)] = value; + } - for (int i=0; i<3; ++i) + for (size_t i = 0; i < state.mSpecIncreases.size(); ++i) state.mSpecIncreases[i] = mSpecIncreases[i]; - std::copy (mUsedIds.begin(), mUsedIds.end(), std::back_inserter (state.mUsedIds)); + std::copy(mUsedIds.begin(), mUsedIds.end(), std::back_inserter(state.mUsedIds)); state.mTimeToStartDrowning = mTimeToStartDrowning; } -void MWMechanics::NpcStats::readState (const ESM::CreatureStats& state) +void MWMechanics::NpcStats::readState(const ESM::CreatureStats& state) { CreatureStats::readState(state); } -void MWMechanics::NpcStats::readState (const ESM::NpcStats& state) +void MWMechanics::NpcStats::readState(const ESM::NpcStats& state) { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - for (std::map::const_iterator iter (state.mFactions.begin()); - iter!=state.mFactions.end(); ++iter) - if (store.get().search (iter->first)) + for (auto iter(state.mFactions.begin()); iter != state.mFactions.end(); ++iter) + if (store.get().search(iter->first)) { if (iter->second.mExpelled) - mExpelled.insert (iter->first); + mExpelled.insert(iter->first); if (iter->second.mRank >= 0) mFactionRank[iter->first] = iter->second.mRank; if (iter->second.mReputation) - mFactionReputation[Misc::StringUtils::lowerCase(iter->first)] = iter->second.mReputation; + mFactionReputation[iter->first] = iter->second.mReputation; } mDisposition = state.mDisposition; + mCrimeDispositionModifier = state.mCrimeDispositionModifier; - for (int i=0; i::const_iterator iter (state.mUsedIds.begin()); - iter!=state.mUsedIds.end(); ++iter) - if (store.find (*iter)) - mUsedIds.insert (*iter); + for (auto iter(state.mUsedIds.begin()); iter != state.mUsedIds.end(); ++iter) + if (store.find(*iter)) + mUsedIds.insert(*iter); mTimeToStartDrowning = state.mTimeToStartDrowning; } diff --git a/apps/openmw/mwmechanics/npcstats.hpp b/apps/openmw/mwmechanics/npcstats.hpp index 9bd8e20ad7e..f94744cb71b 100644 --- a/apps/openmw/mwmechanics/npcstats.hpp +++ b/apps/openmw/mwmechanics/npcstats.hpp @@ -1,13 +1,15 @@ #ifndef GAME_MWMECHANICS_NPCSTATS_H #define GAME_MWMECHANICS_NPCSTATS_H +#include "creaturestats.hpp" +#include +#include +#include #include #include #include #include -#include "creaturestats.hpp" - namespace ESM { struct Class; @@ -20,118 +22,124 @@ namespace MWMechanics class NpcStats : public CreatureStats { - int mDisposition; - SkillValue mSkill[ESM::Skill::Length]; // SkillValue.mProgress used by the player only - - int mReputation; - int mCrimeId; + int mDisposition; + int mCrimeDispositionModifier; + std::map mSkills; // SkillValue.mProgress used by the player only - // ----- used by the player only, maybe should be moved at some point ------- - int mBounty; - int mWerewolfKills; - /// Used only for the player and for NPC's with ranks, modified by scripts; other NPCs have maximum one faction defined in their NPC record - std::map mFactionRank; - std::set mExpelled; - std::map mFactionReputation; - int mLevelProgress; // 0-10 - std::vector mSkillIncreases; // number of skill increases for each attribute (resets after leveling up) - std::vector mSpecIncreases; // number of skill increases for each specialization (accumulates throughout the entire game) - std::set mUsedIds; - // --------------------------------------------------------------------------- + int mReputation; + int mCrimeId; - /// Countdown to getting damage while underwater - float mTimeToStartDrowning; + // ----- used by the player only, maybe should be moved at some point ------- + int mBounty; + int mWerewolfKills; + /// Used only for the player and for NPC's with ranks, modified by scripts; other NPCs have maximum one faction + /// defined in their NPC record + std::map mFactionRank; + std::set mExpelled; + std::map mFactionReputation; + int mLevelProgress; // 0-10 + std::map + mSkillIncreases; // number of skill increases for each attribute (resets after leveling up) + std::vector mSpecIncreases; // number of skill increases for each specialization (accumulates throughout + // the entire game) + std::set mUsedIds; + // --------------------------------------------------------------------------- - bool mIsWerewolf; + /// Countdown to getting damage while underwater + float mTimeToStartDrowning; - public: + bool mIsWerewolf; - NpcStats(); + public: + NpcStats(); - int getBaseDisposition() const; - void setBaseDisposition(int disposition); + int getBaseDisposition() const; + void setBaseDisposition(int disposition); - int getReputation() const; - void setReputation(int reputation); + int getCrimeDispositionModifier() const; + void setCrimeDispositionModifier(int value); + void modCrimeDispositionModifier(int value); - int getCrimeId() const; - void setCrimeId(int id); + int getReputation() const; + void setReputation(int reputation); - const SkillValue& getSkill (int index) const; - SkillValue& getSkill (int index); - void setSkill(int index, const SkillValue& value); + int getCrimeId() const; + void setCrimeId(int id); - int getFactionRank(const std::string &faction) const; - const std::map& getFactionRanks() const; + const SkillValue& getSkill(ESM::RefId id) const; + SkillValue& getSkill(ESM::RefId id); + void setSkill(ESM::RefId id, const SkillValue& value); - /// Increase the rank in this faction by 1, if such a rank exists. - void raiseRank(const std::string& faction); - /// Lower the rank in this faction by 1, if such a rank exists. - void lowerRank(const std::string& faction); - /// Join this faction, setting the initial rank to 0. - void joinFaction(const std::string& faction); + int getFactionRank(const ESM::RefId& faction) const; + const std::map& getFactionRanks() const; - const std::set& getExpelled() const { return mExpelled; } - bool getExpelled(const std::string& factionID) const; - void expell(const std::string& factionID); - void clearExpelled(const std::string& factionID); + /// Join this faction, setting the initial rank to 0. + void joinFaction(const ESM::RefId& faction); + /// Sets the rank in this faction to a specified value, if such a rank exists. + void setFactionRank(const ESM::RefId& faction, int value); - bool isInFaction (const std::string& faction) const; + const std::set& getExpelled() const { return mExpelled; } + bool getExpelled(const ESM::RefId& factionID) const; + void expell(const ESM::RefId& factionID, bool printMessage); + void clearExpelled(const ESM::RefId& factionID); - float getSkillProgressRequirement (int skillIndex, const ESM::Class& class_) const; + bool isInFaction(const ESM::RefId& faction) const; - void useSkill (int skillIndex, const ESM::Class& class_, int usageType = -1, float extraFactor=1.f); - ///< Increase skill by usage. + float getSkillProgressRequirement(ESM::RefId id, const ESM::Class& class_) const; - void increaseSkill (int skillIndex, const ESM::Class& class_, bool preserveProgress, bool readBook = false); + int getLevelProgress() const; + void setLevelProgress(int progress); - int getLevelProgress() const; + int getLevelupAttributeMultiplier(ESM::Attribute::AttributeID attribute) const; + int getSkillIncreasesForAttribute(ESM::Attribute::AttributeID attribute) const; + void setSkillIncreasesForAttribute(ESM::Attribute::AttributeID, int increases); - int getLevelupAttributeMultiplier(int attribute) const; + int getSkillIncreasesForSpecialization(ESM::Class::Specialization spec) const; + void setSkillIncreasesForSpecialization(ESM::Class::Specialization spec, int increases); - int getSkillIncreasesForSpecialization(int spec) const; + void levelUp(); - void levelUp(); + void updateHealth(); + ///< Calculate health based on endurance and strength. + /// Called at character creation. - void updateHealth(); - ///< Calculate health based on endurance and strength. - /// Called at character creation. + void flagAsUsed(const ESM::RefId& id); + ///< @note Id must be lower-case - void flagAsUsed (const std::string& id); - ///< @note Id must be lower-case + bool hasBeenUsed(const ESM::RefId& id) const; + ///< @note Id must be lower-case - bool hasBeenUsed (const std::string& id) const; - ///< @note Id must be lower-case + int getBounty() const; - int getBounty() const; + void setBounty(int bounty); - void setBounty (int bounty); + int getFactionReputation(const ESM::RefId& faction) const; - int getFactionReputation (const std::string& faction) const; + void setFactionReputation(const ESM::RefId& faction, int value); - void setFactionReputation (const std::string& faction, int value); + bool hasSkillsForRank(const ESM::RefId& factionId, int rank) const; - bool hasSkillsForRank (const std::string& factionId, int rank) const; + bool isWerewolf() const; - bool isWerewolf() const; + void setWerewolf(bool set); - void setWerewolf(bool set); + int getWerewolfKills() const; - int getWerewolfKills() const; + /// Increments mWerewolfKills by 1. + void addWerewolfKill(); - /// Increments mWerewolfKills by 1. - void addWerewolfKill(); + float getTimeToStartDrowning() const; + /// Sets time left for the creature to drown if it stays underwater. + /// @param time value from [0,20] + void setTimeToStartDrowning(float time); - float getTimeToStartDrowning() const; - /// Sets time left for the creature to drown if it stays underwater. - /// @param time value from [0,20] - void setTimeToStartDrowning(float time); + void writeState(ESM::CreatureStats& state) const; + void writeState(ESM::NpcStats& state) const; - void writeState (ESM::CreatureStats& state) const; - void writeState (ESM::NpcStats& state) const; + void readState(const ESM::CreatureStats& state); + void readState(const ESM::NpcStats& state); - void readState (const ESM::CreatureStats& state); - void readState (const ESM::NpcStats& state); + const std::map& getSkills() const { return mSkills; } }; } diff --git a/apps/openmw/mwmechanics/objects.cpp b/apps/openmw/mwmechanics/objects.cpp index 5b18fc2c305..12d342666b2 100644 --- a/apps/openmw/mwmechanics/objects.cpp +++ b/apps/openmw/mwmechanics/objects.cpp @@ -1,153 +1,160 @@ #include "objects.hpp" #include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "character.hpp" -#include "movement.hpp" namespace MWMechanics { -Objects::Objects() -{ -} - -Objects::~Objects() -{ - for(auto& object : mObjects) - { - delete object.second; - object.second = nullptr; - } -} + void Objects::addObject(const MWWorld::Ptr& ptr) + { + removeObject(ptr); -void Objects::addObject(const MWWorld::Ptr& ptr) -{ - removeObject(ptr); + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if (anim == nullptr) + return; - MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); - if(anim) mObjects.insert(std::make_pair(ptr, new CharacterController(ptr, anim))); -} + const auto it = mObjects.emplace(mObjects.end(), ptr, anim); + mIndex.emplace(ptr.mRef, it); + } -void Objects::removeObject(const MWWorld::Ptr& ptr) -{ - PtrControllerMap::iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) + void Objects::removeObject(const MWWorld::Ptr& ptr) { - delete iter->second; - mObjects.erase(iter); + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + { + mObjects.erase(iter->second); + mIndex.erase(iter); + } } -} -void Objects::updateObject(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) -{ - PtrControllerMap::iterator iter = mObjects.find(old); - if(iter != mObjects.end()) + void Objects::updateObject(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) { - CharacterController *ctrl = iter->second; - mObjects.erase(iter); + const auto iter = mIndex.find(old.mRef); + if (iter != mIndex.end()) + iter->second->updatePtr(ptr); + } - ctrl->updatePtr(ptr); - mObjects.insert(std::make_pair(ptr, ctrl)); + void Objects::dropObjects(const MWWorld::CellStore* cellStore) + { + for (auto iter = mObjects.begin(); iter != mObjects.end();) + { + if (iter->getPtr().getCell() == cellStore) + { + mIndex.erase(iter->getPtr().mRef); + iter = mObjects.erase(iter); + } + else + ++iter; + } } -} -void Objects::dropObjects (const MWWorld::CellStore *cellStore) -{ - PtrControllerMap::iterator iter = mObjects.begin(); - while(iter != mObjects.end()) + void Objects::update(float duration, bool paused) { - if(iter->first.getCell()==cellStore) + if (!paused) { - delete iter->second; - mObjects.erase(iter++); + for (CharacterController& object : mObjects) + object.update(duration); } else - ++iter; + { + // We still should play container opening animation in the Container GUI mode. + MWGui::GuiMode mode = MWBase::Environment::get().getWindowManager()->getMode(); + if (mode != MWGui::GM_Container) + return; + + for (CharacterController& object : mObjects) + { + if (object.getPtr().getType() != ESM::Container::sRecordId) + continue; + + if (object.isAnimPlaying("containeropen")) + { + object.update(duration); + MWBase::Environment::get().getWorld()->updateAnimatedCollisionShape(object.getPtr()); + } + } + } } -} -void Objects::update(float duration, bool paused) -{ - if(!paused) + bool Objects::onOpen(const MWWorld::Ptr& ptr) { - for(auto& object : mObjects) - object.second->update(duration); + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->onOpen(); + return true; } - else + + void Objects::onClose(const MWWorld::Ptr& ptr) { - // We still should play container opening animation in the Container GUI mode. - MWGui::GuiMode mode = MWBase::Environment::get().getWindowManager()->getMode(); - if(mode != MWGui::GM_Container) - return; + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->onClose(); + } - for(auto& object : mObjects) + bool Objects::playAnimationGroup( + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, bool scripted) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) { - if (object.first.getTypeName() != typeid(ESM::Container).name()) - continue; - - if (object.second->isAnimPlaying("containeropen")) - { - object.second->update(duration); - MWBase::Environment::get().getWorld()->updateAnimatedCollisionShape(object.first); - } + return iter->second->playGroup(groupName, mode, number, scripted); + } + else + { + Log(Debug::Warning) << "Warning: Objects::playAnimationGroup: Unable to find " + << ptr.getCellRef().getRefId(); + return false; } } -} -bool Objects::onOpen(const MWWorld::Ptr& ptr) -{ - PtrControllerMap::iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) - return iter->second->onOpen(); - return true; -} + bool Objects::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, + float speed, std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->playGroupLua(groupName, speed, startKey, stopKey, loops, forceLoop); + return false; + } -void Objects::onClose(const MWWorld::Ptr& ptr) -{ - PtrControllerMap::iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) - iter->second->onClose(); -} + void Objects::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->enableLuaAnimations(enable); + } -bool Objects::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist) -{ - PtrControllerMap::iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) + void Objects::skipAnimation(const MWWorld::Ptr& ptr) { - return iter->second->playGroup(groupName, mode, number, persist); + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->skipAnim(); } - else + + void Objects::persistAnimationStates() { - Log(Debug::Warning) << "Warning: Objects::playAnimationGroup: Unable to find " << ptr.getCellRef().getRefId(); - return false; + for (CharacterController& object : mObjects) + object.persistAnimationState(); } -} -void Objects::skipAnimation(const MWWorld::Ptr& ptr) -{ - PtrControllerMap::iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) - iter->second->skipAnim(); -} -void Objects::persistAnimationStates() -{ - for (PtrControllerMap::iterator iter = mObjects.begin(); iter != mObjects.end(); ++iter) - iter->second->persistAnimationState(); -} + void Objects::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->clearAnimQueue(clearScripted); + } -void Objects::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) -{ - for (PtrControllerMap::iterator iter = mObjects.begin(); iter != mObjects.end(); ++iter) + void Objects::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const { - if ((position - iter->first.getRefData().getPosition().asVec3()).length2() <= radius*radius) - out.push_back(iter->first); + for (const CharacterController& object : mObjects) + if ((position - object.getPtr().getRefData().getPosition().asVec3()).length2() <= radius * radius) + out.push_back(object.getPtr()); } -} } diff --git a/apps/openmw/mwmechanics/objects.hpp b/apps/openmw/mwmechanics/objects.hpp index 5160114a3fc..31c2768b1b8 100644 --- a/apps/openmw/mwmechanics/objects.hpp +++ b/apps/openmw/mwmechanics/objects.hpp @@ -1,8 +1,11 @@ #ifndef GAME_MWMECHANICS_ACTIVATORS_H #define GAME_MWMECHANICS_ACTIVATORS_H -#include +#include "character.hpp" + +#include #include +#include #include namespace osg @@ -18,27 +21,22 @@ namespace MWWorld namespace MWMechanics { - class CharacterController; - class Objects { - typedef std::map PtrControllerMap; - PtrControllerMap mObjects; + std::list mObjects; + std::map::iterator> mIndex; public: - Objects(); - ~Objects(); - - void addObject (const MWWorld::Ptr& ptr); + void addObject(const MWWorld::Ptr& ptr); ///< Register an animated object - void removeObject (const MWWorld::Ptr& ptr); + void removeObject(const MWWorld::Ptr& ptr); ///< Deregister an object - void updateObject(const MWWorld::Ptr &old, const MWWorld::Ptr& ptr); + void updateObject(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr); ///< Updates an object with a new Ptr - void dropObjects(const MWWorld::CellStore *cellStore); + void dropObjects(const MWWorld::CellStore* cellStore); ///< Deregister all objects in the given cell. void update(float duration, bool paused); @@ -47,16 +45,18 @@ namespace MWMechanics bool onOpen(const MWWorld::Ptr& ptr); void onClose(const MWWorld::Ptr& ptr); - bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false); + bool playAnimationGroup( + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, bool scripted = false); + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, uint32_t loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop); + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable); void skipAnimation(const MWWorld::Ptr& ptr); void persistAnimationStates(); + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted); - void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector& out); + void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const; - std::size_t size() const - { - return mObjects.size(); - } + std::size_t size() const { return mObjects.size(); } }; } diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 88325ee7c74..4afaf7c2d5b 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -1,30 +1,49 @@ #include "obstacle.hpp" +#include +#include + +#include +#include #include -#include "../mwworld/class.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" #include "movement.hpp" namespace MWMechanics { - // NOTE: determined empirically but probably need further tweaking - static const float DIST_SAME_SPOT = 0.5f; - static const float DURATION_SAME_SPOT = 1.5f; - static const float DURATION_TO_EVADE = 0.4f; - - const float ObstacleCheck::evadeDirections[NUM_EVADE_DIRECTIONS][2] = + namespace { - { 1.0f, 0.0f }, // move to side - { 1.0f, -1.0f }, // move to side and backwards - { -1.0f, 0.0f }, // move to other side - { -1.0f, -1.0f } // move to side and backwards - }; + // NOTE: determined empirically but probably need further tweaking + constexpr float distanceSameSpot = 0.5f; + constexpr float durationSameSpot = 1.5f; + constexpr float durationToEvade = 1; + + struct EvadeDirection + { + float mMovementX; + float mMovementY; + MWWorld::MovementDirectionFlag mRequiredAnimation; + }; + + constexpr EvadeDirection evadeDirections[] = { + { 1.0f, 1.0f, MWWorld::MovementDirectionFlag_Forward }, // move to right and forward + { 1.0f, 0.0f, MWWorld::MovementDirectionFlag_Right }, // move to right + { 1.0f, -1.0f, MWWorld::MovementDirectionFlag_Back }, // move to right and backwards + { 0.0f, -1.0f, MWWorld::MovementDirectionFlag_Back }, // move backwards + { -1.0f, -1.0f, MWWorld::MovementDirectionFlag_Back }, // move to left and backwards + { -1.0f, 0.0f, MWWorld::MovementDirectionFlag_Left }, // move to left + { -1.0f, 1.0f, MWWorld::MovementDirectionFlag_Forward }, // move to left and forward + }; + } bool proximityToDoor(const MWWorld::Ptr& actor, float minDist) { - if(getNearbyDoor(actor, minDist).isEmpty()) + if (getNearbyDoor(actor, minDist).isEmpty()) return false; else return true; @@ -32,21 +51,22 @@ namespace MWMechanics const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist) { - MWWorld::CellStore *cell = actor.getCell(); + MWWorld::CellStore* cell = actor.getCell(); // Check all the doors in this cell const MWWorld::CellRefList& doors = cell->getReadOnlyDoors(); osg::Vec3f pos(actor.getRefData().getPosition().asVec3()); pos.z() = 0; - osg::Vec3f actorDir = (actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0)); + osg::Vec3f actorDir = (actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0, 1, 0)); for (const auto& ref : doors.mList) { osg::Vec3f doorPos(ref.mData.getPosition().asVec3()); // FIXME: cast - const MWWorld::Ptr doorPtr = MWWorld::Ptr(&const_cast &>(ref), actor.getCell()); + const MWWorld::Ptr doorPtr + = MWWorld::Ptr(&const_cast&>(ref), actor.getCell()); const auto doorState = doorPtr.getClass().getDoorState(doorPtr); float doorRot = ref.mData.getPosition().rot[2] - doorPtr.getCellRef().getPosition().rot[2]; @@ -63,7 +83,7 @@ namespace MWMechanics continue; // Door is not close enough - if ((pos - doorPos).length2() > minDist*minDist) + if ((pos - doorPos).length2() > minDist * minDist) continue; return doorPtr; // found, stop searching @@ -72,10 +92,23 @@ namespace MWMechanics return MWWorld::Ptr(); // none found } + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer, + std::vector* occupyingActors) + { + const auto world = MWBase::Environment::get().getWorld(); + const osg::Vec3f halfExtents = world->getPathfindingAgentBounds(actor).mHalfExtents; + const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + if (ignorePlayer) + { + const std::array ignore{ actor, world->getPlayerConstPtr() }; + return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); + } + const std::array ignore{ actor }; + return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); + } + ObstacleCheck::ObstacleCheck() - : mWalkState(WalkState::Initial) - , mStateDuration(0) - , mEvadeDirectionIndex(0) + : mEvadeDirectionIndex(std::size(evadeDirections) - 1) { } @@ -107,7 +140,8 @@ namespace MWMechanics * u = how long to move sideways * */ - void ObstacleCheck::update(const MWWorld::Ptr& actor, const osg::Vec3f& destination, float duration) + void ObstacleCheck::update(const MWWorld::Ptr& actor, const osg::Vec3f& destination, float duration, + MWWorld::MovementDirectionFlags supportedMovementDirection) { const auto position = actor.getRefData().getPosition().asVec3(); @@ -117,12 +151,19 @@ namespace MWMechanics mStateDuration = 0; mPrev = position; mInitialDistance = (destination - position).length(); + mDestination = destination; return; } if (mWalkState != WalkState::Evade) { - const float distSameSpot = DIST_SAME_SPOT * actor.getClass().getCurrentSpeed(actor) * duration; + if (mDestination != destination) + { + mInitialDistance = (destination - mPrev).length(); + mDestination = destination; + } + + const float distSameSpot = distanceSameSpot * actor.getClass().getCurrentSpeed(actor) * duration; const float prevDistance = (destination - mPrev).length(); const float currentDistance = (destination - position).length(); const float movedDistance = prevDistance - currentDistance; @@ -146,19 +187,27 @@ namespace MWMechanics } mStateDuration += duration; - if (mStateDuration < DURATION_SAME_SPOT) + if (mStateDuration < durationSameSpot) { return; } mWalkState = WalkState::Evade; mStateDuration = 0; - chooseEvasionDirection(); + std::size_t newEvadeDirectionIndex = mEvadeDirectionIndex; + do + { + ++newEvadeDirectionIndex; + if (newEvadeDirectionIndex == std::size(evadeDirections)) + newEvadeDirectionIndex = 0; + if ((evadeDirections[newEvadeDirectionIndex].mRequiredAnimation & supportedMovementDirection) != 0) + break; + } while (mEvadeDirectionIndex != newEvadeDirectionIndex); return; } mStateDuration += duration; - if(mStateDuration >= DURATION_TO_EVADE) + if (mStateDuration >= durationToEvade) { // tried to evade, assume all is ok and start again mWalkState = WalkState::Norm; @@ -169,18 +218,7 @@ namespace MWMechanics void ObstacleCheck::takeEvasiveAction(MWMechanics::Movement& actorMovement) const { - actorMovement.mPosition[0] = evadeDirections[mEvadeDirectionIndex][0]; - actorMovement.mPosition[1] = evadeDirections[mEvadeDirectionIndex][1]; + actorMovement.mPosition[0] = evadeDirections[mEvadeDirectionIndex].mMovementX; + actorMovement.mPosition[1] = evadeDirections[mEvadeDirectionIndex].mMovementY; } - - void ObstacleCheck::chooseEvasionDirection() - { - // change direction if attempt didn't work - ++mEvadeDirectionIndex; - if (mEvadeDirectionIndex == NUM_EVADE_DIRECTIONS) - { - mEvadeDirectionIndex = 0; - } - } - } diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index b574bab67fe..532bc91331a 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -1,19 +1,22 @@ #ifndef OPENMW_MECHANICS_OBSTACLE_H #define OPENMW_MECHANICS_OBSTACLE_H +#include "apps/openmw/mwworld/movementdirection.hpp" + #include +#include + namespace MWWorld { class Ptr; + class ConstPtr; } namespace MWMechanics { struct Movement; - static constexpr int NUM_EVADE_DIRECTIONS = 4; - /// tests actor's proximity to a closed door by default bool proximityToDoor(const MWWorld::Ptr& actor, float minDist); @@ -21,42 +24,41 @@ namespace MWMechanics /** \return Pointer to the door, or empty pointer if none exists **/ const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, + bool ignorePlayer = false, std::vector* occupyingActors = nullptr); + class ObstacleCheck { - public: - ObstacleCheck(); - - // Clear the timers and set the state machine to default - void clear(); - - bool isEvading() const; - - // Updates internal state, call each frame for moving actor - void update(const MWWorld::Ptr& actor, const osg::Vec3f& destination, float duration); - - // change direction to try to fix "stuck" actor - void takeEvasiveAction(MWMechanics::Movement& actorMovement) const; - - private: - osg::Vec3f mPrev; - - // directions to try moving in when get stuck - static const float evadeDirections[NUM_EVADE_DIRECTIONS][2]; - - enum class WalkState - { - Initial, - Norm, - CheckStuck, - Evade - }; - WalkState mWalkState; - - float mStateDuration; - int mEvadeDirectionIndex; - float mInitialDistance = 0; - - void chooseEvasionDirection(); + public: + ObstacleCheck(); + + // Clear the timers and set the state machine to default + void clear(); + + bool isEvading() const; + + // Updates internal state, call each frame for moving actor + void update(const MWWorld::Ptr& actor, const osg::Vec3f& destination, float duration, + MWWorld::MovementDirectionFlags supportedMovementDirection); + + // change direction to try to fix "stuck" actor + void takeEvasiveAction(Movement& actorMovement) const; + + private: + enum class WalkState + { + Initial, + Norm, + CheckStuck, + Evade, + }; + + WalkState mWalkState = WalkState::Initial; + float mStateDuration = 0; + float mInitialDistance = 0; + std::size_t mEvadeDirectionIndex; + osg::Vec3f mPrev; + osg::Vec3f mDestination; }; } diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 3f113802f89..192cbdfe226 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -3,28 +3,30 @@ #include #include -#include -#include +#include + #include +#include +#include #include +#include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" -#include "../mwphysics/collisiontype.hpp" +#include "../mwphysics/raycasting.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" -#include "pathgrid.hpp" #include "actorutil.hpp" +#include "pathgrid.hpp" 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, int start) { assert(grid && !grid->mPoints.empty()); @@ -34,7 +36,7 @@ namespace int closestReachableIndex = 0; // TODO: if this full scan causes performance problems mapping pathgrid // points to a quadtree may help - for(unsigned int counter = 0; counter < grid->mPoints.size(); counter++) + for (size_t counter = 0; counter < grid->mPoints.size(); counter++) { float potentialDistBetween = MWMechanics::PathFinder::distanceSquared(grid->mPoints[counter], pos); if (potentialDistBetween < closestDistanceReachable) @@ -60,8 +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 std::pair(closestReachableIndex, closestReachableIndex == closestIndex); } float sqrDistance(const osg::Vec2f& lhs, const osg::Vec2f& rhs) @@ -74,13 +75,6 @@ namespace return sqrDistance(osg::Vec2f(lhs.x(), lhs.y()), osg::Vec2f(rhs.x(), rhs.y())); } - float getPathStepSize(const MWWorld::ConstPtr& actor) - { - const auto world = MWBase::Environment::get().getWorld(); - const auto realHalfExtents = world->getHalfExtents(actor); - return 2 * std::max(realHalfExtents.x(), realHalfExtents.y()); - } - float getHeight(const MWWorld::ConstPtr& actor) { const auto world = MWBase::Environment::get().getWorld(); @@ -89,7 +83,8 @@ namespace } // Returns true if turn in `p2` is less than 10 degrees and all the 3 points are almost on one line. - bool isAlmostStraight(const osg::Vec3f& p1, const osg::Vec3f& p2, const osg::Vec3f& p3, float pointTolerance) { + bool isAlmostStraight(const osg::Vec3f& p1, const osg::Vec3f& p2, const osg::Vec3f& p3, float pointTolerance) + { osg::Vec3f v1 = p1 - p2; osg::Vec3f v3 = p3 - p2; v1.z() = v3.z() = 0; @@ -105,6 +100,20 @@ namespace return checkAngle && checkDist; } + + struct IsValidShortcut + { + const DetourNavigator::Navigator* mNavigator; + const DetourNavigator::AgentBounds mAgentBounds; + const DetourNavigator::Flags mFlags; + + bool operator()(const osg::Vec3f& start, const osg::Vec3f& end) const + { + const auto position = DetourNavigator::raycast(*mNavigator, mAgentBounds, start, end, mFlags); + return position.has_value() + && std::abs((position.value() - start).length2() - (end - start).length2()) <= 1; + } + }; } namespace MWMechanics @@ -122,10 +131,12 @@ namespace MWMechanics dir.z() = 0; dir.normalize(); float verticalOffset = 200; // instead of '200' here we want the height of the actor - osg::Vec3f _from = from + dir*offsetXY + osg::Z_AXIS * verticalOffset; + osg::Vec3f _from = from + dir * offsetXY + osg::Z_AXIS * verticalOffset; // cast up-down ray and find height of hit in world space - float h = _from.z() - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, -osg::Z_AXIS, verticalOffset + PATHFIND_Z_REACH + 1); + float h = _from.z() + - MWBase::Environment::get().getWorld()->getDistToNearestRayHit( + _from, -osg::Z_AXIS, verticalOffset + PATHFIND_Z_REACH + 1); return (std::abs(from.z() - h) <= PATHFIND_Z_REACH); } @@ -171,14 +182,14 @@ namespace MWMechanics { const auto pathgrid = pathgridGraph.getPathgrid(); - // Refer to AiWander reseach topic on openmw forums for some background. + // Refer to AiWander research topic on openmw forums for some background. // Maybe there is no pathgrid for this cell. Just go to destination and let // physics take care of any blockages. - if(!pathgrid || pathgrid->mPoints.empty()) + if (!pathgrid || pathgrid->mPoints.empty()) return; // NOTE: getClosestPoint expects local coordinates - Misc::CoordinateConverter converter(mCell->getCell()); + const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(*mCell->getCell()); // NOTE: It is possible that getClosestPoint returns a pathgrind point index // that is unreachable in some situations. e.g. actor is standing @@ -189,12 +200,8 @@ namespace MWMechanics int startNode = getClosestPoint(pathgrid, startPointInLocalCoords); osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint)); - std::pair endNode = getClosestReachablePoint(pathgrid, &pathgridGraph, - endPointInLocalCoords, - startNode); - - if (!endNode.second) - return; + std::pair endNode + = getClosestReachablePoint(pathgrid, &pathgridGraph, endPointInLocalCoords, startNode); // if it's shorter for actor to travel from start to end, than to travel from either // start or end to nearest pathgrid point, just travel from start to end. @@ -212,7 +219,7 @@ namespace MWMechanics // even if the start and the end points are the same. // NOTE: aStarSearch will return an empty path if the start and end // nodes are the same - if(startNode == endNode.first) + if (startNode == endNode.first) { ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]); converter.toWorld(temp); @@ -238,20 +245,22 @@ namespace MWMechanics // Add Z offset since path node can overlap with other objects. // Also ignore doors in raytesting. const int mask = MWPhysics::CollisionType_World; - bool isPathClear = !MWBase::Environment::get().getWorld()->castRay( - startPoint.x(), startPoint.y(), startPoint.z() + 16, temp.mX, temp.mY, temp.mZ + 16, mask); + bool isPathClear = !MWBase::Environment::get() + .getWorld() + ->getRayCasting() + ->castRay(osg::Vec3f(startPoint.x(), startPoint.y(), startPoint.z() + 16), + osg::Vec3f(temp.mX, temp.mY, temp.mZ + 16), mask) + .mHit; if (isPathClear) path.pop_front(); } } // convert supplied path to world coordinates - std::transform(path.begin(), path.end(), out, - [&] (ESM::Pathgrid::Point& point) - { - converter.toWorld(point); - return makeOsgVec3(point); - }); + std::transform(path.begin(), path.end(), out, [&](ESM::Pathgrid::Point& point) { + converter.toWorld(point); + return makeOsgVec3(point); + }); } // If endNode found is NOT the closest PathGrid point to the endPoint, @@ -266,14 +275,15 @@ namespace MWMechanics // unreachable pathgrid point. // // The AI routines will have to deal with such situations. - *out++ = endPoint; + if (endNode.second) + *out++ = endPoint; } float PathFinder::getZAngleToNext(float x, float y) const { // This should never happen (programmers should have an if statement checking // isPathConstructed that prevents this call if otherwise). - if(mPath.empty()) + if (mPath.empty()) return 0.; const auto& nextPoint = mPath.front(); @@ -287,7 +297,7 @@ namespace MWMechanics { // This should never happen (programmers should have an if statement checking // isPathConstructed that prevents this call if otherwise). - if(mPath.empty()) + if (mPath.empty()) return 0.; const osg::Vec3f dir = mPath.front() - osg::Vec3f(x, y, z); @@ -296,7 +306,7 @@ namespace MWMechanics } void PathFinder::update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, - bool shortenIfAlmostStraight, bool canMoveByZ) + UpdateFlags updateFlags, const DetourNavigator::AgentBounds& agentBounds, DetourNavigator::Flags pathFlags) { if (mPath.empty()) return; @@ -304,18 +314,36 @@ namespace MWMechanics while (mPath.size() > 1 && sqrDistanceIgnoreZ(mPath.front(), position) < pointTolerance * pointTolerance) mPath.pop_front(); - if (shortenIfAlmostStraight) + const IsValidShortcut isValidShortcut{ MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, + pathFlags }; + + if ((updateFlags & UpdateFlag_ShortenIfAlmostStraight) != 0) { - while (mPath.size() > 2 && isAlmostStraight(mPath[0], mPath[1], mPath[2], pointTolerance)) + while (mPath.size() > 2 && isAlmostStraight(mPath[0], mPath[1], mPath[2], pointTolerance) + && isValidShortcut(mPath[0], mPath[2])) mPath.erase(mPath.begin() + 1); - if (mPath.size() > 1 && isAlmostStraight(position, mPath[0], mPath[1], pointTolerance)) + if (mPath.size() > 1 && isAlmostStraight(position, mPath[0], mPath[1], pointTolerance) + && isValidShortcut(position, mPath[1])) + mPath.pop_front(); + } + + if ((updateFlags & UpdateFlag_RemoveLoops) != 0 && mPath.size() > 1) + { + std::size_t begin = 0; + for (std::size_t i = 1; i < mPath.size(); ++i) + { + const float sqrDistance = Misc::getVectorToLine(position, mPath[i - 1], mPath[i]).length2(); + if (sqrDistance < pointTolerance * pointTolerance && isValidShortcut(position, mPath[i])) + begin = i; + } + for (std::size_t i = 0; i < begin; ++i) mPath.pop_front(); } if (mPath.size() == 1) { float distSqr; - if (canMoveByZ) + if ((updateFlags & UpdateFlag_CanMoveByZ) != 0) distSqr = (mPath.front() - position).length2(); else distSqr = sqrDistanceIgnoreZ(mPath.front(), position); @@ -343,119 +371,99 @@ namespace MWMechanics } void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, - const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts) + const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) { mPath.clear(); // If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path - if (!buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, areaCosts, std::back_inserter(mPath))) + DetourNavigator::Status status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, + areaCosts, endTolerance, pathType, std::back_inserter(mPath)); + + if (status != DetourNavigator::Status::Success) + mPath.clear(); + + if (status == DetourNavigator::Status::NavMeshNotFound) mPath.push_back(endPoint); mConstructed = !mPath.empty(); } void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts) + const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, + const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) { mPath.clear(); mCell = cell; - bool hasNavMesh = false; + DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound; if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor)) - hasNavMesh = buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, areaCosts, std::back_inserter(mPath)); + { + status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, endTolerance, + pathType, std::back_inserter(mPath)); + if (status != DetourNavigator::Status::Success) + mPath.clear(); + } - if (hasNavMesh && mPath.empty()) - buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, - flags | DetourNavigator::Flag_usePathgrid, areaCosts, std::back_inserter(mPath)); + if (status != DetourNavigator::Status::NavMeshNotFound && mPath.empty() + && (flags & DetourNavigator::Flag_usePathgrid) == 0) + { + status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, + flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, + std::back_inserter(mPath)); + if (status != DetourNavigator::Status::Success) + mPath.clear(); + } if (mPath.empty()) buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath)); - if (!hasNavMesh && mPath.empty()) + if (status == DetourNavigator::Status::NavMeshNotFound && mPath.empty()) mPath.push_back(endPoint); mConstructed = !mPath.empty(); } - bool PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, - const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, std::back_insert_iterator> out) + DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, + const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType, std::back_insert_iterator> out) { const auto world = MWBase::Environment::get().getWorld(); - const auto stepSize = getPathStepSize(actor); const auto navigator = world->getNavigator(); - const auto status = navigator->findPath(halfExtents, stepSize, startPoint, endPoint, flags, areaCosts, out); - - if (status == DetourNavigator::Status::NavMeshNotFound) - return false; - - if (status != DetourNavigator::Status::Success) - { - Log(Debug::Debug) << "Build path by navigator error: \"" << DetourNavigator::getMessage(status) - << "\" for \"" << actor.getClass().getName(actor) << "\" (" << actor.getBase() - << ") from " << startPoint << " to " << endPoint << " with flags (" - << DetourNavigator::WriteFlags {flags} << ")"; - } - - return true; - } - - void PathFinder::buildPathByNavMeshToNextPoint(const MWWorld::ConstPtr& actor, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts) - { - if (mPath.empty()) - return; - - const auto stepSize = getPathStepSize(actor); - const auto startPoint = actor.getRefData().getPosition().asVec3(); + const auto status = DetourNavigator::findPath( + *navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, out); - if (sqrDistanceIgnoreZ(mPath.front(), startPoint) <= 4 * stepSize * stepSize) - return; - - const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); - std::deque prePath; - auto prePathInserter = std::back_inserter(prePath); - const auto status = navigator->findPath(halfExtents, stepSize, startPoint, mPath.front(), flags, areaCosts, - prePathInserter); - - if (status == DetourNavigator::Status::NavMeshNotFound) - return; + if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath) + return DetourNavigator::Status::Success; if (status != DetourNavigator::Status::Success) { Log(Debug::Debug) << "Build path by navigator error: \"" << DetourNavigator::getMessage(status) - << "\" for \"" << actor.getClass().getName(actor) << "\" (" << actor.getBase() - << ") from " << startPoint << " to " << mPath.front() << " with flags (" - << DetourNavigator::WriteFlags {flags} << ")"; - return; + << "\" for \"" << actor.getClass().getName(actor) << "\" (" << actor.getBase() + << ") from " << startPoint << " to " << endPoint << " with flags (" + << DetourNavigator::WriteFlags{ flags } << ")"; } - while (!prePath.empty() && sqrDistanceIgnoreZ(prePath.front(), startPoint) < stepSize * stepSize) - prePath.pop_front(); - - while (!prePath.empty() && sqrDistanceIgnoreZ(prePath.back(), mPath.front()) < stepSize * stepSize) - prePath.pop_back(); - - std::copy(prePath.rbegin(), prePath.rend(), std::front_inserter(mPath)); + return status; } - void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts) + void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, + const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, + const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) { const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); - const auto maxDistance = std::min( - navigator->getMaxNavmeshAreaRealRadius(), - static_cast(Constants::CellSizeInUnits) - ); + const auto maxDistance + = std::min(navigator->getMaxNavmeshAreaRealRadius(), static_cast(Constants::CellSizeInUnits)); const auto startToEnd = endPoint - startPoint; const auto distance = startToEnd.length(); if (distance <= maxDistance) - return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, halfExtents, flags, areaCosts); + return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, agentBounds, flags, areaCosts, + endTolerance, pathType); const auto end = startPoint + startToEnd * maxDistance / distance; - buildPath(actor, startPoint, end, cell, pathgridGraph, halfExtents, flags, areaCosts); + buildPath(actor, startPoint, end, cell, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); } } diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index ed88a57ca0b..0f688686cdf 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -1,14 +1,15 @@ #ifndef GAME_MWMECHANICS_PATHFINDING_H #define GAME_MWMECHANICS_PATHFINDING_H -#include #include +#include #include -#include #include -#include -#include +#include +#include +#include +#include namespace MWWorld { @@ -17,6 +18,11 @@ namespace MWWorld class Ptr; } +namespace DetourNavigator +{ + struct AgentBounds; +} + namespace MWMechanics { class PathgridGraph; @@ -24,9 +30,7 @@ namespace MWMechanics template inline float distance(const T& lhs, const T& rhs) { - static_assert(std::is_same::value - || std::is_same::value, - "T is not a position"); + static_assert(std::is_same::value || std::is_same::value, "T is not a position"); return (lhs - rhs).length(); } @@ -69,148 +73,145 @@ namespace MWMechanics // magnitude of pits/obstacles is defined by PATHFIND_Z_REACH bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY); - class PathFinder + enum class PathType { - public: - PathFinder() - : mConstructed(false) - , mCell(nullptr) - { - } - - void clearPath() - { - mConstructed = false; - mPath.clear(); - mCell = nullptr; - } - - void buildStraightPath(const osg::Vec3f& endPoint); - - void buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph); - - void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, - const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts); - - void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); - - void buildPathByNavMeshToNextPoint(const MWWorld::ConstPtr& actor, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); - - void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); - - /// Remove front point if exist and within tolerance - void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, - bool shortenIfAlmostStraight, bool canMoveByZ); - - bool checkPathCompleted() const - { - return mConstructed && mPath.empty(); - } - - /// In radians - float getZAngleToNext(float x, float y) const; - - float getXAngleToNext(float x, float y, float z) const; - - bool isPathConstructed() const - { - return mConstructed && !mPath.empty(); - } - - std::size_t getPathSize() const - { - return mPath.size(); - } - - const std::deque& getPath() const - { - return mPath; - } - - const MWWorld::CellStore* getPathCell() const - { - return mCell; - } - - void addPointToPath(const osg::Vec3f& point) - { - mConstructed = true; - mPath.push_back(point); - } - - /// utility function to convert a osg::Vec3f to a Pathgrid::Point - static ESM::Pathgrid::Point makePathgridPoint(const osg::Vec3f& v) - { - return ESM::Pathgrid::Point(static_cast(v[0]), static_cast(v[1]), static_cast(v[2])); - } - - /// utility function to convert an ESM::Position to a Pathgrid::Point - static ESM::Pathgrid::Point makePathgridPoint(const ESM::Position& p) - { - return ESM::Pathgrid::Point(static_cast(p.pos[0]), static_cast(p.pos[1]), static_cast(p.pos[2])); - } - - static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p) - { - return osg::Vec3f(static_cast(p.mX), static_cast(p.mY), static_cast(p.mZ)); - } - - // Slightly cheaper version for comparisons. - // Caller needs to be careful for very short distances (i.e. less than 1) - // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 - // - static float distanceSquared(ESM::Pathgrid::Point point, const osg::Vec3f& pos) - { - return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2(); - } + Full, + Partial, + }; - // Return the closest pathgrid point index from the specified position - // coordinates. NOTE: Does not check if there is a sensible way to get there - // (e.g. a cliff in front). - // - // NOTE: pos is expected to be in local coordinates, as is grid->mPoints - // - static int getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos) + class PathFinder + { + public: + using UpdateFlags = unsigned; + + enum UpdateFlag : UpdateFlags + { + UpdateFlag_CanMoveByZ = 1 << 0, + UpdateFlag_ShortenIfAlmostStraight = 1 << 1, + UpdateFlag_RemoveLoops = 1 << 2, + }; + + PathFinder() = default; + + void clearPath() + { + mConstructed = false; + mPath.clear(); + mCell = nullptr; + } + + void buildStraightPath(const osg::Vec3f& endPoint); + + void buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, + const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph); + + void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, + const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType); + + void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, + const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, + const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); + + void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, + const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, + const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); + + /// Remove front point if exist and within tolerance + void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, + UpdateFlags updateFlags, const DetourNavigator::AgentBounds& agentBounds, DetourNavigator::Flags pathFlags); + + bool checkPathCompleted() const { return mConstructed && mPath.empty(); } + + /// In radians + float getZAngleToNext(float x, float y) const; + + float getXAngleToNext(float x, float y, float z) const; + + bool isPathConstructed() const { return mConstructed && !mPath.empty(); } + + std::size_t getPathSize() const { return mPath.size(); } + + const std::deque& getPath() const { return mPath; } + + const MWWorld::CellStore* getPathCell() const { return mCell; } + + void addPointToPath(const osg::Vec3f& point) + { + mConstructed = true; + mPath.push_back(point); + } + + /// utility function to convert a osg::Vec3f to a Pathgrid::Point + static ESM::Pathgrid::Point makePathgridPoint(const osg::Vec3f& v) + { + return ESM::Pathgrid::Point(static_cast(v[0]), static_cast(v[1]), static_cast(v[2])); + } + + /// utility function to convert an ESM::Position to a Pathgrid::Point + static ESM::Pathgrid::Point makePathgridPoint(const ESM::Position& p) + { + return ESM::Pathgrid::Point( + static_cast(p.pos[0]), static_cast(p.pos[1]), static_cast(p.pos[2])); + } + + static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p) + { + return osg::Vec3f(static_cast(p.mX), static_cast(p.mY), static_cast(p.mZ)); + } + + // Slightly cheaper version for comparisons. + // Caller needs to be careful for very short distances (i.e. less than 1) + // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 + // + static float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) + { + return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2(); + } + + // Return the closest pathgrid point index from the specified position + // coordinates. NOTE: Does not check if there is a sensible way to get there + // (e.g. a cliff in front). + // + // NOTE: pos is expected to be in local coordinates, as is grid->mPoints + // + static int getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos) + { + assert(grid && !grid->mPoints.empty()); + + float distanceBetween = distanceSquared(grid->mPoints[0], pos); + int closestIndex = 0; + + // TODO: if this full scan causes performance problems mapping pathgrid + // points to a quadtree may help + for (unsigned int counter = 1; counter < grid->mPoints.size(); counter++) { - assert(grid && !grid->mPoints.empty()); - - float distanceBetween = distanceSquared(grid->mPoints[0], pos); - int closestIndex = 0; - - // TODO: if this full scan causes performance problems mapping pathgrid - // points to a quadtree may help - for(unsigned int counter = 1; counter < grid->mPoints.size(); counter++) + float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos); + if (potentialDistBetween < distanceBetween) { - float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos); - if(potentialDistBetween < distanceBetween) - { - distanceBetween = potentialDistBetween; - closestIndex = counter; - } + distanceBetween = potentialDistBetween; + closestIndex = counter; } - - return closestIndex; } - private: - bool mConstructed; - std::deque mPath; + return closestIndex; + } - const MWWorld::CellStore* mCell; + private: + bool mConstructed = false; + std::deque mPath; + const MWWorld::CellStore* mCell = nullptr; - void buildPathByPathgridImpl(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const PathgridGraph& pathgridGraph, std::back_insert_iterator> out); + void buildPathByPathgridImpl(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, + const PathgridGraph& pathgridGraph, std::back_insert_iterator> out); - bool buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, - const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, std::back_insert_iterator> out); + [[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, + const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType, std::back_insert_iterator> out); }; } diff --git a/apps/openmw/mwmechanics/pathgrid.cpp b/apps/openmw/mwmechanics/pathgrid.cpp index ee1de3b5ad3..980c0c660fb 100644 --- a/apps/openmw/mwmechanics/pathgrid.cpp +++ b/apps/openmw/mwmechanics/pathgrid.cpp @@ -1,10 +1,8 @@ #include "pathgrid.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/environment.hpp" - -#include "../mwworld/cellstore.hpp" -#include "../mwworld/esmstore.hpp" +#include +#include +#include namespace { @@ -42,23 +40,100 @@ namespace // - faster but not the shortest path float costAStar(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b) { - //return distance(a, b); + // return distance(a, b); return manhattan(a, b); } + + constexpr size_t NoIndex = static_cast(-1); } namespace MWMechanics { - PathgridGraph::PathgridGraph(const MWWorld::CellStore *cell) - : mCell(nullptr) - , mPathgrid(nullptr) - , mGraph(0) - , mIsGraphConstructed(false) - , mSCCId(0) - , mSCCIndex(0) + + class PathgridGraph::Builder { - load(cell); - } + std::vector& mGraph; + + // variables used to calculate connected components + int mSCCId = 0; + size_t mSCCIndex = 0; + std::vector mSCCStack; + std::vector> mSCCPoint; // first is index, second is lowlink + + // v is the pathgrid point index (some call them vertices) + void recursiveStrongConnect(const size_t v) + { + mSCCPoint[v].first = mSCCIndex; // index + mSCCPoint[v].second = mSCCIndex; // lowlink + mSCCIndex++; + mSCCStack.push_back(v); + size_t w; + + for (const auto& edge : mGraph[v].edges) + { + w = edge.index; + if (mSCCPoint[w].first == NoIndex) // not visited + { + recursiveStrongConnect(w); // recurse + mSCCPoint[v].second = std::min(mSCCPoint[v].second, mSCCPoint[w].second); + } + else if (std::find(mSCCStack.begin(), mSCCStack.end(), w) != mSCCStack.end()) + mSCCPoint[v].second = std::min(mSCCPoint[v].second, mSCCPoint[w].first); + } + + if (mSCCPoint[v].second == mSCCPoint[v].first) + { // new component + do + { + w = mSCCStack.back(); + mSCCStack.pop_back(); + mGraph[w].componentId = mSCCId; + } while (w != v); + mSCCId++; + } + } + + public: + /* + * mGraph contains the strongly connected component group id's along + * with pre-calculated edge costs. + * + * A cell can have disjointed pathgrids, e.g. Seyda Neen has 3 + * + * mGraph for Seyda Neen will therefore have 3 different values. When + * selecting a random pathgrid point for AiWander, mGraph can be checked + * for quickly finding whether the destination is reachable. + * + * Otherwise, buildPath can automatically select a closest reachable end + * pathgrid point (reachable from the closest start point). + * + * Using Tarjan's algorithm: + * + * mGraph | graph G | + * mSCCPoint | V | derived from mPoints + * mGraph[v].edges | E (for v) | + * mSCCIndex | index | tracking smallest unused index + * mSCCStack | S | + * mGraph[v].edges[i].index | w | + * + */ + explicit Builder(PathgridGraph& graph) + : mGraph(graph.mGraph) + { + // both of these are set to zero in the constructor + // mSCCId = 0; // how many strongly connected components in this cell + // mSCCIndex = 0; + size_t pointsSize = graph.mPathgrid->mPoints.size(); + mSCCPoint.resize(pointsSize, std::pair(NoIndex, NoIndex)); + mSCCStack.reserve(pointsSize); + + for (size_t v = 0; v < pointsSize; ++v) + { + if (mSCCPoint[v].first == NoIndex) // undefined (haven't visited) + recursiveStrongConnect(v); + } + } + }; /* * mGraph is populated with the cost of each allowed edge. @@ -96,135 +171,38 @@ namespace MWMechanics * +----------------> * high cost */ - bool PathgridGraph::load(const MWWorld::CellStore *cell) + PathgridGraph::PathgridGraph(const ESM::Pathgrid& pathgrid) + : mPathgrid(&pathgrid) { - if(!cell) - return false; - - if(mIsGraphConstructed) - return true; - - mCell = cell->getCell(); - mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell->getCell()); - if(!mPathgrid) - return false; - - mGraph.resize(mPathgrid->mPoints.size()); - for(int i = 0; i < static_cast (mPathgrid->mEdges.size()); i++) + for (const auto& edge : mPathgrid->mEdges) { ConnectedPoint neighbour; - neighbour.cost = costAStar(mPathgrid->mPoints[mPathgrid->mEdges[i].mV0], - mPathgrid->mPoints[mPathgrid->mEdges[i].mV1]); + neighbour.cost = costAStar(mPathgrid->mPoints[edge.mV0], mPathgrid->mPoints[edge.mV1]); // forward path of the edge - neighbour.index = mPathgrid->mEdges[i].mV1; - mGraph[mPathgrid->mEdges[i].mV0].edges.push_back(neighbour); + neighbour.index = edge.mV1; + mGraph[edge.mV0].edges.push_back(neighbour); // reverse path of the edge // NOTE: These are redundant, ESM already contains the required reverse paths - //neighbour.index = mPathgrid->mEdges[i].mV0; - //mGraph[mPathgrid->mEdges[i].mV1].edges.push_back(neighbour); - } - buildConnectedPoints(); - mIsGraphConstructed = true; - return true; - } - - const ESM::Pathgrid *PathgridGraph::getPathgrid() const - { - return mPathgrid; - } - - // v is the pathgrid point index (some call them vertices) - void PathgridGraph::recursiveStrongConnect(int v) - { - mSCCPoint[v].first = mSCCIndex; // index - mSCCPoint[v].second = mSCCIndex; // lowlink - mSCCIndex++; - mSCCStack.push_back(v); - int w; - - for(int i = 0; i < static_cast (mGraph[v].edges.size()); i++) - { - w = mGraph[v].edges[i].index; - if(mSCCPoint[w].first == -1) // not visited - { - recursiveStrongConnect(w); // recurse - mSCCPoint[v].second = std::min(mSCCPoint[v].second, - mSCCPoint[w].second); - } - else - { - if(find(mSCCStack.begin(), mSCCStack.end(), w) != mSCCStack.end()) - mSCCPoint[v].second = std::min(mSCCPoint[v].second, - mSCCPoint[w].first); - } - } - - if(mSCCPoint[v].second == mSCCPoint[v].first) - { // new component - do - { - w = mSCCStack.back(); - mSCCStack.pop_back(); - mGraph[w].componentId = mSCCId; - } - while(w != v); - mSCCId++; + // neighbour.index = edge.mV0; + // mGraph[edge.mV1].edges.push_back(neighbour); } - return; + Builder(*this); } - /* - * mGraph contains the strongly connected component group id's along - * with pre-calculated edge costs. - * - * A cell can have disjointed pathgrids, e.g. Seyda Neen has 3 - * - * mGraph for Seyda Neen will therefore have 3 different values. When - * selecting a random pathgrid point for AiWander, mGraph can be checked - * for quickly finding whether the destination is reachable. - * - * Otherwise, buildPath can automatically select a closest reachable end - * pathgrid point (reachable from the closest start point). - * - * Using Tarjan's algorithm: - * - * mGraph | graph G | - * mSCCPoint | V | derived from mPoints - * mGraph[v].edges | E (for v) | - * mSCCIndex | index | tracking smallest unused index - * mSCCStack | S | - * mGraph[v].edges[i].index | w | - * - */ - void PathgridGraph::buildConnectedPoints() - { - // both of these are set to zero in the constructor - //mSCCId = 0; // how many strongly connected components in this cell - //mSCCIndex = 0; - int pointsSize = static_cast (mPathgrid->mPoints.size()); - mSCCPoint.resize(pointsSize, std::pair (-1, -1)); - mSCCStack.reserve(pointsSize); + const PathgridGraph PathgridGraph::sEmpty = {}; - for(int v = 0; v < pointsSize; v++) - { - if(mSCCPoint[v].first == -1) // undefined (haven't visited) - recursiveStrongConnect(v); - } - } - - bool PathgridGraph::isPointConnected(const int start, const int end) const + bool PathgridGraph::isPointConnected(const size_t start, const size_t end) const { return (mGraph[start].componentId == mGraph[end].componentId); } - void PathgridGraph::getNeighbouringPoints(const int index, ESM::Pathgrid::PointList &nodes) const + void PathgridGraph::getNeighbouringPoints(const size_t index, ESM::Pathgrid::PointList& nodes) const { - for(int i = 0; i < static_cast (mGraph[index].edges.size()); i++) + for (const auto& edge : mGraph[index].edges) { - int neighbourIndex = mGraph[index].edges[i].index; - if (neighbourIndex != index) - nodes.push_back(mPathgrid->mPoints[neighbourIndex]); + if (edge.index != index) + nodes.push_back(mPathgrid->mPoints[edge.index]); } } @@ -250,69 +228,66 @@ namespace MWMechanics * gScore - past accumulated costs vector indexed by point index * fScore - future estimated costs vector indexed by point index * - * TODO: An intersting exercise might be to cache the paths created for a + * TODO: An interesting exercise might be to cache the paths created for a * start/goal pair. To cache the results the paths need to be in * pathgrid points form (currently they are converted to world * coordinates). Essentially trading speed w/ memory. */ - std::deque PathgridGraph::aStarSearch(const int start, const int goal) const + std::deque PathgridGraph::aStarSearch(const size_t start, const size_t goal) const { std::deque path; - if(!isPointConnected(start, goal)) + if (!isPointConnected(start, goal)) { return path; // there is no path, return an empty path } - int graphSize = static_cast (mGraph.size()); - std::vector gScore (graphSize, -1); - std::vector fScore (graphSize, -1); - std::vector graphParent (graphSize, -1); + size_t graphSize = mGraph.size(); + std::vector gScore(graphSize, -1); + std::vector fScore(graphSize, -1); + std::vector graphParent(graphSize, NoIndex); // gScore & fScore keep costs for each pathgrid point in mPoints gScore[start] = 0; fScore[start] = costAStar(mPathgrid->mPoints[start], mPathgrid->mPoints[goal]); - std::list openset; - std::list closedset; + std::list openset; + std::set closedset; openset.push_back(start); - int current = -1; + size_t current = start; - while(!openset.empty()) + while (!openset.empty()) { current = openset.front(); // front has the lowest cost openset.pop_front(); - if(current == goal) + if (current == goal) break; - closedset.push_back(current); // remember we've been here + closedset.insert(current); // remember we've been here // check all edges for the current point index - for(int j = 0; j < static_cast (mGraph[current].edges.size()); j++) + for (const auto& edge : mGraph[current].edges) { - if(std::find(closedset.begin(), closedset.end(), mGraph[current].edges[j].index) == - closedset.end()) + if (!closedset.contains(edge.index)) { // not in closedset - i.e. have not traversed this edge destination - int dest = mGraph[current].edges[j].index; - float tentative_g = gScore[current] + mGraph[current].edges[j].cost; + size_t dest = edge.index; + float tentative_g = gScore[current] + edge.cost; bool isInOpenSet = std::find(openset.begin(), openset.end(), dest) != openset.end(); - if(!isInOpenSet - || tentative_g < gScore[dest]) + if (!isInOpenSet || tentative_g < gScore[dest]) { graphParent[dest] = current; gScore[dest] = tentative_g; - fScore[dest] = tentative_g + costAStar(mPathgrid->mPoints[dest], - mPathgrid->mPoints[goal]); - if(!isInOpenSet) + fScore[dest] = tentative_g + costAStar(mPathgrid->mPoints[dest], mPathgrid->mPoints[goal]); + if (!isInOpenSet) { // add this edge to openset, lowest cost goes to the front // TODO: if this causes performance problems a hash table may help - std::list::iterator it = openset.begin(); - for(it = openset.begin(); it!= openset.end(); ++it) + auto it = openset.begin(); + for (; it != openset.end(); ++it) { - if(fScore[*it] > fScore[dest]) + if (fScore[*it] > fScore[dest]) break; } openset.insert(it, dest); @@ -322,11 +297,11 @@ namespace MWMechanics } } - if(current != goal) + if (current != goal) return path; // for some reason couldn't build a path // reconstruct path to return, using local coordinates - while(graphParent[current] != -1) + while (graphParent[current] != NoIndex) { path.push_front(mPathgrid->mPoints[current]); current = graphParent[current]; @@ -337,4 +312,3 @@ namespace MWMechanics return path; } } - diff --git a/apps/openmw/mwmechanics/pathgrid.hpp b/apps/openmw/mwmechanics/pathgrid.hpp index 050504617ea..942b6426b76 100644 --- a/apps/openmw/mwmechanics/pathgrid.hpp +++ b/apps/openmw/mwmechanics/pathgrid.hpp @@ -3,81 +3,65 @@ #include -#include - -namespace ESM -{ - struct Cell; -} - -namespace MWWorld -{ - class CellStore; -} +#include namespace MWMechanics { class PathgridGraph { - public: - PathgridGraph(const MWWorld::CellStore* cell); - - bool load(const MWWorld::CellStore *cell); - - const ESM::Pathgrid* getPathgrid() const; - - // returns true if end point is strongly connected (i.e. reachable - // from start point) both start and end are pathgrid point indexes - bool isPointConnected(const int start, const int end) const; - - // get neighbouring nodes for index node and put them to "nodes" vector - void getNeighbouringPoints(const int index, ESM::Pathgrid::PointList &nodes) const; - - // the input parameters are pathgrid point indexes - // the output list is in local (internal cells) or world (external - // cells) coordinates - // - // NOTE: if start equals end an empty path is returned - std::deque aStarSearch(const int start, const int end) const; - - private: - - const ESM::Cell *mCell; - const ESM::Pathgrid *mPathgrid; - - struct ConnectedPoint // edge - { - int index; // pathgrid point index of neighbour - float cost; - }; - - struct Node // point - { - int componentId; - std::vector edges; // neighbours - }; - - // componentId is an integer indicating the groups of connected - // pathgrid points (all connected points will have the same value) - // - // In Seyda Neen there are 3: - // - // 52, 53 and 54 are one set (enclosed yard) - // 48, 49, 50, 51, 84, 85, 86, 87, 88, 89, 90 (ship & office) - // all other pathgrid points are the third set - // - std::vector mGraph; - bool mIsGraphConstructed; - - // variables used to calculate connected components - int mSCCId; - int mSCCIndex; - std::vector mSCCStack; - typedef std::pair VPair; // first is index, second is lowlink - std::vector mSCCPoint; - // methods used to calculate connected components - void recursiveStrongConnect(int v); - void buildConnectedPoints(); + PathgridGraph() + : mPathgrid(nullptr) + { + } + + public: + explicit PathgridGraph(const ESM::Pathgrid& pathGrid); + + const ESM::Pathgrid* getPathgrid() const { return mPathgrid; } + + // returns true if end point is strongly connected (i.e. reachable + // from start point) both start and end are pathgrid point indexes + bool isPointConnected(const size_t start, const size_t end) const; + + // get neighbouring nodes for index node and put them to "nodes" vector + void getNeighbouringPoints(const size_t index, ESM::Pathgrid::PointList& nodes) const; + + // the input parameters are pathgrid point indexes + // the output list is in local (internal cells) or world (external + // cells) coordinates + // + // NOTE: if start equals end an empty path is returned + std::deque aStarSearch(const size_t start, const size_t end) const; + + static const PathgridGraph sEmpty; + + private: + const ESM::Pathgrid* mPathgrid; + + class Builder; + + struct ConnectedPoint // edge + { + size_t index; // pathgrid point index of neighbour + float cost; + }; + + struct Node // point + { + int componentId; + std::vector edges; // neighbours + }; + + // componentId is an integer indicating the groups of connected + // pathgrid points (all connected points will have the same value) + // + // In Seyda Neen there are 3: + // + // 52, 53 and 54 are one set (enclosed yard) + // 48, 49, 50, 51, 84, 85, 86, 87, 88, 89, 90 (ship & office) + // all other pathgrid points are the third set + // + std::vector mGraph; }; } diff --git a/apps/openmw/mwmechanics/pickpocket.cpp b/apps/openmw/mwmechanics/pickpocket.cpp index 05e8a039303..5e153191cff 100644 --- a/apps/openmw/mwmechanics/pickpocket.cpp +++ b/apps/openmw/mwmechanics/pickpocket.cpp @@ -5,21 +5,21 @@ #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "npcstats.hpp" namespace MWMechanics { - Pickpocket::Pickpocket(const MWWorld::Ptr &thief, const MWWorld::Ptr &victim) + Pickpocket::Pickpocket(const MWWorld::Ptr& thief, const MWWorld::Ptr& victim) : mThief(thief) , mVictim(victim) { } - float Pickpocket::getChanceModifier(const MWWorld::Ptr &ptr, float add) + float Pickpocket::getChanceModifier(const MWWorld::Ptr& ptr, float add) { NpcStats& stats = ptr.getClass().getNpcStats(ptr); float agility = stats.getAttribute(ESM::Attribute::Agility).getModified(); @@ -33,15 +33,22 @@ namespace MWMechanics float x = getChanceModifier(mThief); float y = getChanceModifier(mVictim, valueTerm); - float t = 2*x - y; + float t = 2 * x - y; float pcSneak = static_cast(mThief.getClass().getSkill(mThief, ESM::Skill::Sneak)); - int iPickMinChance = MWBase::Environment::get().getWorld()->getStore().get() - .find("iPickMinChance")->mValue.getInteger(); - int iPickMaxChance = MWBase::Environment::get().getWorld()->getStore().get() - .find("iPickMaxChance")->mValue.getInteger(); + int iPickMinChance = MWBase::Environment::get() + .getESMStore() + ->get() + .find("iPickMinChance") + ->mValue.getInteger(); + int iPickMaxChance = MWBase::Environment::get() + .getESMStore() + ->get() + .find("iPickMaxChance") + ->mValue.getInteger(); - int roll = Misc::Rng::roll0to99(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); if (t < pcSneak / iPickMinChance) { return (roll > int(pcSneak / iPickMinChance)); @@ -53,11 +60,14 @@ namespace MWMechanics } } - bool Pickpocket::pick(MWWorld::Ptr item, int count) + bool Pickpocket::pick(const MWWorld::Ptr& item, int count) { float stackValue = static_cast(item.getClass().getValue(item) * count); - float fPickPocketMod = MWBase::Environment::get().getWorld()->getStore().get() - .find("fPickPocketMod")->mValue.getFloat(); + float fPickPocketMod = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fPickPocketMod") + ->mValue.getFloat(); float valueTerm = 10 * fPickPocketMod * stackValue; return getDetected(valueTerm); diff --git a/apps/openmw/mwmechanics/pickpocket.hpp b/apps/openmw/mwmechanics/pickpocket.hpp index 4de1e37f84c..83dbca69c20 100644 --- a/apps/openmw/mwmechanics/pickpocket.hpp +++ b/apps/openmw/mwmechanics/pickpocket.hpp @@ -9,18 +9,18 @@ namespace MWMechanics class Pickpocket { public: - Pickpocket (const MWWorld::Ptr& thief, const MWWorld::Ptr& victim); + Pickpocket(const MWWorld::Ptr& thief, const MWWorld::Ptr& victim); /// Steal some items /// @return Was the thief detected? - bool pick (MWWorld::Ptr item, int count); + bool pick(const MWWorld::Ptr& item, int count); /// End the pickpocketing process /// @return Was the thief detected? - bool finish (); + bool finish(); private: bool getDetected(float valueTerm); - float getChanceModifier(const MWWorld::Ptr& ptr, float add=0); + float getChanceModifier(const MWWorld::Ptr& ptr, float add = 0); MWWorld::Ptr mThief; MWWorld::Ptr mVictim; }; diff --git a/apps/openmw/mwmechanics/recharge.cpp b/apps/openmw/mwmechanics/recharge.cpp index 51c78e1e3b8..7b0ad75d3c6 100644 --- a/apps/openmw/mwmechanics/recharge.cpp +++ b/apps/openmw/mwmechanics/recharge.cpp @@ -1,92 +1,110 @@ #include "recharge.hpp" +#include +#include #include +#include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" -#include "creaturestats.hpp" #include "actorutil.hpp" +#include "creaturestats.hpp" +#include "spellutil.hpp" namespace MWMechanics { -bool rechargeItem(const MWWorld::Ptr &item, const float maxCharge, const float duration) -{ - float charge = item.getCellRef().getEnchantmentCharge(); - if (charge == -1 || charge == maxCharge) - return false; - - static const float fMagicItemRechargePerSecond = MWBase::Environment::get().getWorld()->getStore().get().find( - "fMagicItemRechargePerSecond")->mValue.getFloat(); - - item.getCellRef().setEnchantmentCharge(std::min(charge + fMagicItemRechargePerSecond * duration, maxCharge)); - return true; -} - -bool rechargeItem(const MWWorld::Ptr &item, const MWWorld::Ptr &gem) -{ - if (!gem.getRefData().getCount()) - return false; - - MWWorld::Ptr player = MWMechanics::getPlayer(); - MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - - float luckTerm = 0.1f * stats.getAttribute(ESM::Attribute::Luck).getModified(); - if (luckTerm < 1 || luckTerm > 10) - luckTerm = 1; - - float intelligenceTerm = 0.2f * stats.getAttribute(ESM::Attribute::Intelligence).getModified(); - - if (intelligenceTerm > 20) - intelligenceTerm = 20; - if (intelligenceTerm < 1) - intelligenceTerm = 1; + bool rechargeItem(const MWWorld::Ptr& item, const float maxCharge, const float duration) + { + float charge = item.getCellRef().getEnchantmentCharge(); + if (charge == -1 || charge == maxCharge) + return false; + + static const float fMagicItemRechargePerSecond = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fMagicItemRechargePerSecond") + ->mValue.getFloat(); + + item.getCellRef().setEnchantmentCharge(std::min(charge + fMagicItemRechargePerSecond * duration, maxCharge)); + return true; + } - float x = (player.getClass().getSkill(player, ESM::Skill::Enchant) + intelligenceTerm + luckTerm) * stats.getFatigueTerm(); - int roll = Misc::Rng::roll0to99(); - if (roll < x) + bool rechargeItem(const MWWorld::Ptr& item, const MWWorld::Ptr& gem) { - std::string soul = gem.getCellRef().getSoul(); - const ESM::Creature *creature = MWBase::Environment::get().getWorld()->getStore().get().find(soul); + if (!gem.getCellRef().getCount()) + return false; - float restored = creature->mData.mSoul * (roll / x); + MWWorld::Ptr player = MWMechanics::getPlayer(); + MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find( - item.getClass().getEnchantment(item)); - item.getCellRef().setEnchantmentCharge( - std::min(item.getCellRef().getEnchantmentCharge() + restored, static_cast(enchantment->mData.mCharge))); + MWBase::Environment::get().getWorld()->breakInvisibility(player); - MWBase::Environment::get().getWindowManager()->playSound("Enchant Success"); + float luckTerm = 0.1f * stats.getAttribute(ESM::Attribute::Luck).getModified(); + if (luckTerm < 1 || luckTerm > 10) + luckTerm = 1; - player.getClass().getContainerStore(player).restack(item); - } - else - { - MWBase::Environment::get().getWindowManager()->playSound("Enchant Fail"); - } + float intelligenceTerm = 0.2f * stats.getAttribute(ESM::Attribute::Intelligence).getModified(); - player.getClass().skillUsageSucceeded (player, ESM::Skill::Enchant, 0); - gem.getContainerStore()->remove(gem, 1, player); + if (intelligenceTerm > 20) + intelligenceTerm = 20; + if (intelligenceTerm < 1) + intelligenceTerm = 1; - if (gem.getRefData().getCount() == 0) - { - std::string message = MWBase::Environment::get().getWorld()->getStore().get().find("sNotifyMessage51")->mValue.getString(); - message = Misc::StringUtils::format(message, gem.getClass().getName(gem)); + float x = (player.getClass().getSkill(player, ESM::Skill::Enchant) + intelligenceTerm + luckTerm) + * stats.getFatigueTerm(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); + if (roll < x) + { + const ESM::RefId& soul = gem.getCellRef().getSoul(); + const ESM::Creature* creature = MWBase::Environment::get().getESMStore()->get().find(soul); - MWBase::Environment::get().getWindowManager()->messageBox(message); + float restored = creature->mData.mSoul * (roll / x); - // special case: readd Azura's Star - if (Misc::StringUtils::ciEqual(gem.get()->mBase->mId, "Misc_SoulGem_Azura")) - player.getClass().getContainerStore(player).add("Misc_SoulGem_Azura", 1, player); + const ESM::Enchantment* enchantment + = MWBase::Environment::get().getESMStore()->get().find( + item.getClass().getEnchantment(item)); + const int maxCharge = MWMechanics::getEnchantmentCharge(*enchantment); + item.getCellRef().setEnchantmentCharge( + std::min(item.getCellRef().getEnchantmentCharge() + restored, static_cast(maxCharge))); + + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Enchant Success")); + + player.getClass().getContainerStore(player).restack(item); + } + else + { + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Enchant Fail")); + } + + player.getClass().skillUsageSucceeded(player, ESM::Skill::Enchant, ESM::Skill::Enchant_Recharge); + gem.getContainerStore()->remove(gem, 1); + + if (gem.getCellRef().getCount() == 0) + { + std::string message = MWBase::Environment::get() + .getESMStore() + ->get() + .find("sNotifyMessage51") + ->mValue.getString(); + message = Misc::StringUtils::format(message, gem.getClass().getName(gem)); + + MWBase::Environment::get().getWindowManager()->messageBox(message); + + const ESM::RefId soulGemAzura = ESM::RefId::stringRefId("Misc_SoulGem_Azura"); + // special case: readd Azura's Star + if (gem.get()->mBase->mId == soulGemAzura) + player.getClass().getContainerStore(player).add(soulGemAzura, 1); + } + + return true; } - return true; -} - } diff --git a/apps/openmw/mwmechanics/recharge.hpp b/apps/openmw/mwmechanics/recharge.hpp index 913f2109b78..ac1c7ced541 100644 --- a/apps/openmw/mwmechanics/recharge.hpp +++ b/apps/openmw/mwmechanics/recharge.hpp @@ -6,9 +6,9 @@ namespace MWMechanics { - bool rechargeItem(const MWWorld::Ptr &item, const float maxCharge, const float duration); + bool rechargeItem(const MWWorld::Ptr& item, const float maxCharge, const float duration); - bool rechargeItem(const MWWorld::Ptr &item, const MWWorld::Ptr &gem); + bool rechargeItem(const MWWorld::Ptr& item, const MWWorld::Ptr& gem); } diff --git a/apps/openmw/mwmechanics/repair.cpp b/apps/openmw/mwmechanics/repair.cpp index 81886ed9b06..914fa0b542d 100644 --- a/apps/openmw/mwmechanics/repair.cpp +++ b/apps/openmw/mwmechanics/repair.cpp @@ -1,107 +1,115 @@ #include "repair.hpp" #include +#include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" -#include "creaturestats.hpp" #include "actorutil.hpp" +#include "creaturestats.hpp" namespace MWMechanics { -void Repair::repair(const MWWorld::Ptr &itemToRepair) -{ - MWWorld::Ptr player = getPlayer(); - MWWorld::LiveCellRef *ref = - mTool.get(); + void Repair::repair(const MWWorld::Ptr& itemToRepair) + { + MWWorld::Ptr player = getPlayer(); + MWWorld::LiveCellRef* ref = mTool.get(); - // unstack tool if required - player.getClass().getContainerStore(player).unstack(mTool, player); + MWBase::Environment::get().getWorld()->breakInvisibility(player); - // reduce number of uses left - int uses = mTool.getClass().getItemHealth(mTool); - uses -= std::min(uses, 1); - mTool.getCellRef().setCharge(uses); + // unstack tool if required + player.getClass().getContainerStore(player).unstack(mTool); - MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); + // reduce number of uses left + int uses = mTool.getClass().getItemHealth(mTool); + uses -= std::min(uses, 1); + mTool.getCellRef().setCharge(uses); - float fatigueTerm = stats.getFatigueTerm(); - float pcStrength = stats.getAttribute(ESM::Attribute::Strength).getModified(); - float pcLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); - float armorerSkill = player.getClass().getSkill(player, ESM::Skill::Armorer); + MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - float fRepairAmountMult = MWBase::Environment::get().getWorld()->getStore().get() - .find("fRepairAmountMult")->mValue.getFloat(); + float fatigueTerm = stats.getFatigueTerm(); + float pcStrength = stats.getAttribute(ESM::Attribute::Strength).getModified(); + float pcLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); + float armorerSkill = player.getClass().getSkill(player, ESM::Skill::Armorer); - float toolQuality = ref->mBase->mData.mQuality; + float fRepairAmountMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fRepairAmountMult") + ->mValue.getFloat(); - float x = (0.1f * pcStrength + 0.1f * pcLuck + armorerSkill) * fatigueTerm; + float toolQuality = ref->mBase->mData.mQuality; - int roll = Misc::Rng::roll0to99(); - if (roll <= x) - { - int y = static_cast(fRepairAmountMult * toolQuality * roll); - y = std::max(1, y); + float x = (0.1f * pcStrength + 0.1f * pcLuck + armorerSkill) * fatigueTerm; - // repair by 'y' points - int charge = itemToRepair.getClass().getItemHealth(itemToRepair); - charge = std::min(charge + y, itemToRepair.getClass().getItemMaxHealth(itemToRepair)); - itemToRepair.getCellRef().setCharge(charge); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); + if (roll <= x) + { + int y = static_cast(fRepairAmountMult * toolQuality * roll); + y = std::max(1, y); - // attempt to re-stack item, in case it was fully repaired - MWWorld::ContainerStoreIterator stacked = player.getClass().getContainerStore(player).restack(itemToRepair); + // repair by 'y' points + int charge = itemToRepair.getClass().getItemHealth(itemToRepair); + charge = std::min(charge + y, itemToRepair.getClass().getItemMaxHealth(itemToRepair)); + itemToRepair.getCellRef().setCharge(charge); - // set the OnPCRepair variable on the item's script - std::string script = stacked->getClass().getScript(itemToRepair); - if(script != "") - stacked->getRefData().getLocals().setVarByInt(script, "onpcrepair", 1); + // attempt to re-stack item, in case it was fully repaired + MWWorld::ContainerStoreIterator stacked = player.getClass().getContainerStore(player).restack(itemToRepair); - // increase skill - player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, 0); + // set the OnPCRepair variable on the item's script + const ESM::RefId& script = stacked->getClass().getScript(itemToRepair); + if (!script.empty()) + stacked->getRefData().getLocals().setVarByInt(script, "onpcrepair", 1); - MWBase::Environment::get().getWindowManager()->playSound("Repair"); - MWBase::Environment::get().getWindowManager()->messageBox("#{sRepairSuccess}"); - } - else - { - MWBase::Environment::get().getWindowManager()->playSound("Repair Fail"); - MWBase::Environment::get().getWindowManager()->messageBox("#{sRepairFailed}"); - } + // increase skill + player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, ESM::Skill::Armorer_Repair); - // tool used up? - if (mTool.getCellRef().getCharge() == 0) - { - MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Repair")); + MWBase::Environment::get().getWindowManager()->messageBox("#{sRepairSuccess}"); + } + else + { + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Repair Fail")); + MWBase::Environment::get().getWindowManager()->messageBox("#{sRepairFailed}"); + } - store.remove(mTool, 1, player); + // tool used up? + if (mTool.getCellRef().getCharge() == 0) + { + MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); - std::string message = MWBase::Environment::get().getWorld()->getStore().get() - .find("sNotifyMessage51")->mValue.getString(); - message = Misc::StringUtils::format(message, mTool.getClass().getName(mTool)); + store.remove(mTool, 1); - MWBase::Environment::get().getWindowManager()->messageBox(message); + std::string message = MWBase::Environment::get() + .getESMStore() + ->get() + .find("sNotifyMessage51") + ->mValue.getString(); + message = Misc::StringUtils::format(message, mTool.getClass().getName(mTool)); - // try to find a new tool of the same ID - for (MWWorld::ContainerStoreIterator iter (store.begin()); - iter!=store.end(); ++iter) - { - if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), mTool.getCellRef().getRefId())) + MWBase::Environment::get().getWindowManager()->messageBox(message); + + // try to find a new tool of the same ID + for (MWWorld::ContainerStoreIterator iter(store.begin()); iter != store.end(); ++iter) { - mTool = *iter; + if (iter->getCellRef().getRefId() == mTool.getCellRef().getRefId()) + { + mTool = *iter; - MWBase::Environment::get().getWindowManager()->playSound("Item Repair Up"); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Repair Up")); - break; + break; + } } } } -} } diff --git a/apps/openmw/mwmechanics/repair.hpp b/apps/openmw/mwmechanics/repair.hpp index 6f9a866af10..a596f0d28bc 100644 --- a/apps/openmw/mwmechanics/repair.hpp +++ b/apps/openmw/mwmechanics/repair.hpp @@ -9,10 +9,10 @@ namespace MWMechanics class Repair { public: - void setTool (const MWWorld::Ptr& tool) { mTool = tool; } + void setTool(const MWWorld::Ptr& tool) { mTool = tool; } MWWorld::Ptr getTool() { return mTool; } - void repair (const MWWorld::Ptr& itemToRepair); + void repair(const MWWorld::Ptr& itemToRepair); private: MWWorld::Ptr mTool; diff --git a/apps/openmw/mwmechanics/security.cpp b/apps/openmw/mwmechanics/security.cpp index e642a7bb4b7..0fb8a956990 100644 --- a/apps/openmw/mwmechanics/security.cpp +++ b/apps/openmw/mwmechanics/security.cpp @@ -6,16 +6,16 @@ #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" #include "creaturestats.hpp" namespace MWMechanics { - Security::Security(const MWWorld::Ptr &actor) + Security::Security(const MWWorld::Ptr& actor) : mActor(actor) { CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); @@ -25,12 +25,12 @@ namespace MWMechanics mFatigueTerm = creatureStats.getFatigueTerm(); } - void Security::pickLock(const MWWorld::Ptr &lock, const MWWorld::Ptr &lockpick, - std::string& resultMessage, std::string& resultSound) + void Security::pickLock(const MWWorld::Ptr& lock, const MWWorld::Ptr& lockpick, std::string_view& resultMessage, + std::string_view& resultSound) { - if (lock.getCellRef().getLockLevel() <= 0 || - lock.getCellRef().getLockLevel() == ESM::UnbreakableLock || - !lock.getClass().hasToolTip(lock)) //If it's unlocked or can not be unlocked back out immediately + // If it's unlocked or can not be unlocked back out immediately. Note that we're not strictly speaking checking + // if the ref is locked, lock levels <= 0 can exist but they cannot be picked + if (lock.getCellRef().getLockLevel() <= 0 || !lock.getClass().hasToolTip(lock)) return; int uses = lockpick.getClass().getItemHealth(lockpick); @@ -41,7 +41,11 @@ namespace MWMechanics float pickQuality = lockpick.get()->mBase->mData.mQuality; - float fPickLockMult = MWBase::Environment::get().getWorld()->getStore().get().find("fPickLockMult")->mValue.getFloat(); + float fPickLockMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fPickLockMult") + ->mValue.getFloat(); float x = 0.2f * mAgility + 0.1f * mLuck + mSecuritySkill; x *= pickQuality * mFatigueTerm; @@ -54,12 +58,13 @@ namespace MWMechanics resultMessage = "#{sLockImpossible}"; else { - if (Misc::Rng::roll0to99() <= x) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::roll0to99(prng) <= x) { lock.getCellRef().unlock(); resultMessage = "#{sLockSuccess}"; resultSound = "Open Lock"; - mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 1); + mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, ESM::Skill::Security_PickLock); } else resultMessage = "#{sLockFail}"; @@ -67,11 +72,11 @@ namespace MWMechanics lockpick.getCellRef().setCharge(--uses); if (!uses) - lockpick.getContainerStore()->remove(lockpick, 1, mActor); + lockpick.getContainerStore()->remove(lockpick, 1); } - void Security::probeTrap(const MWWorld::Ptr &trap, const MWWorld::Ptr &probe, - std::string& resultMessage, std::string& resultSound) + void Security::probeTrap(const MWWorld::Ptr& trap, const MWWorld::Ptr& probe, std::string_view& resultMessage, + std::string_view& resultSound) { if (trap.getCellRef().getTrap().empty()) return; @@ -82,10 +87,15 @@ namespace MWMechanics float probeQuality = probe.get()->mBase->mData.mQuality; - const ESM::Spell* trapSpell = MWBase::Environment::get().getWorld()->getStore().get().find(trap.getCellRef().getTrap()); + const ESM::Spell* trapSpell + = MWBase::Environment::get().getESMStore()->get().find(trap.getCellRef().getTrap()); int trapSpellPoints = trapSpell->mData.mCost; - float fTrapCostMult = MWBase::Environment::get().getWorld()->getStore().get().find("fTrapCostMult")->mValue.getFloat(); + float fTrapCostMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fTrapCostMult") + ->mValue.getFloat(); float x = 0.2f * mAgility + 0.1f * mLuck + mSecuritySkill; x += fTrapCostMult * trapSpellPoints; @@ -98,13 +108,14 @@ namespace MWMechanics resultMessage = "#{sTrapImpossible}"; else { - if (Misc::Rng::roll0to99() <= x) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::roll0to99(prng) <= x) { - trap.getCellRef().setTrap(""); + trap.getCellRef().setTrap(ESM::RefId()); resultSound = "Disarm Trap"; resultMessage = "#{sTrapSuccess}"; - mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, 0); + mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, ESM::Skill::Security_DisarmTrap); } else resultMessage = "#{sTrapFail}"; @@ -112,7 +123,7 @@ namespace MWMechanics probe.getCellRef().setCharge(--uses); if (!uses) - probe.getContainerStore()->remove(probe, 1, mActor); + probe.getContainerStore()->remove(probe, 1); } } diff --git a/apps/openmw/mwmechanics/security.hpp b/apps/openmw/mwmechanics/security.hpp index f3efb04ed8c..7765bcb5dca 100644 --- a/apps/openmw/mwmechanics/security.hpp +++ b/apps/openmw/mwmechanics/security.hpp @@ -10,12 +10,12 @@ namespace MWMechanics class Security { public: - Security (const MWWorld::Ptr& actor); + Security(const MWWorld::Ptr& actor); - void pickLock (const MWWorld::Ptr& lock, const MWWorld::Ptr& lockpick, - std::string& resultMessage, std::string& resultSound); - void probeTrap (const MWWorld::Ptr& trap, const MWWorld::Ptr& probe, - std::string& resultMessage, std::string& resultSound); + void pickLock(const MWWorld::Ptr& lock, const MWWorld::Ptr& lockpick, std::string_view& resultMessage, + std::string_view& resultSound); + void probeTrap(const MWWorld::Ptr& trap, const MWWorld::Ptr& probe, std::string_view& resultMessage, + std::string_view& resultSound); private: float mAgility, mLuck, mSecuritySkill, mFatigueTerm; diff --git a/apps/openmw/mwmechanics/setbaseaisetting.hpp b/apps/openmw/mwmechanics/setbaseaisetting.hpp new file mode 100644 index 00000000000..c7624c30e48 --- /dev/null +++ b/apps/openmw/mwmechanics/setbaseaisetting.hpp @@ -0,0 +1,39 @@ +#ifndef OPENMW_MWMECHANICS_SETBASEAISETTING_H +#define OPENMW_MWMECHANICS_SETBASEAISETTING_H + +#include + +#include "../mwbase/environment.hpp" + +#include "../mwworld/esmstore.hpp" + +#include "aisetting.hpp" + +namespace MWMechanics +{ + template + void setBaseAISetting(const ESM::RefId& id, MWMechanics::AiSetting setting, int value) + { + T copy = *MWBase::Environment::get().getESMStore()->get().find(id); + switch (setting) + { + case MWMechanics::AiSetting::Hello: + copy.mAiData.mHello = value; + break; + case MWMechanics::AiSetting::Fight: + copy.mAiData.mFight = value; + break; + case MWMechanics::AiSetting::Flee: + copy.mAiData.mFlee = value; + break; + case MWMechanics::AiSetting::Alarm: + copy.mAiData.mAlarm = value; + break; + default: + assert(false); + } + MWBase::Environment::get().getESMStore()->overrideRecord(copy); + } +} + +#endif diff --git a/apps/openmw/mwmechanics/spellabsorption.cpp b/apps/openmw/mwmechanics/spellabsorption.cpp deleted file mode 100644 index bab290fdab5..00000000000 --- a/apps/openmw/mwmechanics/spellabsorption.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "spellabsorption.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwrender/animation.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" - -#include "creaturestats.hpp" -#include "spellutil.hpp" - -namespace MWMechanics -{ - - class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor - { - public: - float mProbability{0.f}; - - GetAbsorptionProbability() = default; - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float magnitude, float /*remainingTime*/, float /*totalTime*/) override - { - if (key.mId == ESM::MagicEffect::SpellAbsorption) - { - if (mProbability == 0.f) - mProbability = magnitude / 100; - else - { - // If there are different sources of SpellAbsorption effect, multiply failing probability for all effects. - // Real absorption probability will be the (1 - total fail chance) in this case. - float failProbability = 1.f - mProbability; - failProbability *= 1.f - magnitude / 100; - mProbability = 1.f - failProbability; - } - } - } - }; - - bool absorbSpell (const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) - { - if (spellId.empty() || target.isEmpty() || caster == target || !target.getClass().isActor()) - return false; - - CreatureStats& stats = target.getClass().getCreatureStats(target); - if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() <= 0.f) - return false; - - GetAbsorptionProbability check; - stats.getActiveSpells().visitEffectSources(check); - stats.getSpells().visitEffectSources(check); - if (target.getClass().hasInventoryStore(target)) - target.getClass().getInventoryStore(target).visitEffectSources(check); - - int chance = check.mProbability * 100; - if (Misc::Rng::roll0to99() >= chance) - return false; - - const auto& esmStore = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Static* absorbStatic = esmStore.get().find("VFX_Absorb"); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation && !absorbStatic->mModel.empty()) - animation->addEffect( "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, std::string()); - const ESM::Spell* spell = esmStore.get().search(spellId); - int spellCost = 0; - if (spell) - { - spellCost = spell->mData.mCost; - } - else - { - const ESM::Enchantment* enchantment = esmStore.get().search(spellId); - if (enchantment) - spellCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), caster); - } - - // Magicka is increased by the cost of the spell - DynamicStat magicka = stats.getMagicka(); - magicka.setCurrent(magicka.getCurrent() + spellCost); - stats.setMagicka(magicka); - return true; - } - -} diff --git a/apps/openmw/mwmechanics/spellabsorption.hpp b/apps/openmw/mwmechanics/spellabsorption.hpp deleted file mode 100644 index 0fe501df912..00000000000 --- a/apps/openmw/mwmechanics/spellabsorption.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef MWMECHANICS_SPELLABSORPTION_H -#define MWMECHANICS_SPELLABSORPTION_H - -#include - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - // Try to absorb a spell based on the magnitude of every Spell Absorption effect source on the target. - bool absorbSpell(const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target); -} - -#endif diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 5a367b50201..d22b6c48371 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -1,46 +1,129 @@ #include "spellcasting.hpp" +#include +#include +#include #include +#include #include +#include -#include "../mwbase/windowmanager.hpp" -#include "../mwbase/soundmanager.hpp" -#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwworld/containerstore.hpp" -#include "../mwworld/actionteleport.hpp" -#include "../mwworld/player.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/cellstore.hpp" +#include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwrender/animation.hpp" #include "actorutil.hpp" -#include "aifollow.hpp" #include "creaturestats.hpp" -#include "linkedeffects.hpp" -#include "spellabsorption.hpp" -#include "spellresistance.hpp" +#include "spelleffects.hpp" #include "spellutil.hpp" -#include "summoning.hpp" -#include "tickableeffects.hpp" #include "weapontype.hpp" namespace MWMechanics { - CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell) + CastSpell::CastSpell( + const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile, const bool scriptedSpell) : mCaster(caster) , mTarget(target) , mFromProjectile(fromProjectile) - , mManualSpell(manualSpell) + , mScriptedSpell(scriptedSpell) + { + } + + void CastSpell::explodeSpell( + const ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const { + const auto world = MWBase::Environment::get().getWorld(); + std::map> toApply; + for (const ESM::IndexedENAMstruct& effectInfo : effects.mList) + { + const ESM::MagicEffect* effect = world->getStore().get().find(effectInfo.mData.mEffectID); + + if (effectInfo.mData.mRange != rangeType + || (effectInfo.mData.mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor())) + continue; // Not right range type, or not area effect and hit an actor + + if (mFromProjectile && effectInfo.mData.mArea <= 0) + continue; // Don't play explosion for projectiles with 0-area effects + + if (!mFromProjectile && effectInfo.mData.mRange == ESM::RT_Touch && !ignore.isEmpty() + && !ignore.getClass().isActor() && !ignore.getClass().hasToolTip(ignore) + && (mCaster.isEmpty() || mCaster.getClass().isActor())) + continue; // Don't play explosion for touch spells on non-activatable objects except when spell is from + // a projectile enchantment or ExplodeSpell + + // Spawn the explosion orb effect + const ESM::Static* areaStatic; + if (!effect->mArea.empty()) + areaStatic = world->getStore().get().find(effect->mArea); + else + areaStatic = world->getStore().get().find(ESM::RefId::stringRefId("VFX_DefaultArea")); + + const std::string& texture = effect->mParticle; + + if (effectInfo.mData.mArea <= 0) + { + if (effectInfo.mData.mRange == ESM::RT_Target) + world->spawnEffect( + Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, 1.0f); + continue; + } + else + world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, + static_cast(effectInfo.mData.mArea * 2)); + + // Play explosion sound (make sure to use NoTrack, since we will delete the projectile now) + { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + if (!effect->mAreaSound.empty()) + sndMgr->playSound3D(mHitPosition, effect->mAreaSound, 1.0f, 1.0f); + else + sndMgr->playSound3D(mHitPosition, + world->getStore().get().find(effect->mData.mSchool)->mSchool->mAreaSound, 1.0f, + 1.0f); + } + // Get the actors in range of the effect + std::vector objects; + static const int unitsPerFoot = ceil(Constants::UnitsPerFoot); + MWBase::Environment::get().getMechanicsManager()->getObjectsInRange( + mHitPosition, static_cast(effectInfo.mData.mArea * unitsPerFoot), objects); + for (const MWWorld::Ptr& affected : objects) + { + // Ignore actors without collisions here, otherwise it will be possible to hit actors outside processing + // range. + if (affected.getClass().isActor() && !world->isActorCollisionEnabled(affected)) + continue; + + auto& list = toApply[affected]; + list.push_back(effectInfo); + } + } + + // Now apply the appropriate effects to each actor in range + for (auto& applyPair : toApply) + { + // Vanilla-compatible behaviour of never applying the spell to the caster + // (could be changed by mods later) + if (applyPair.first == mCaster) + continue; + + if (applyPair.first == ignore) + continue; + + ESM::EffectList effectsToApply; + effectsToApply.mList = applyPair.second; + inflict(applyPair.first, effectsToApply, rangeType, true); + } } - void CastSpell::launchMagicBolt () + void CastSpell::launchMagicBolt() const { osg::Vec3f fallbackDirection(0, 1, 0); osg::Vec3f offset(0, 0, 0); @@ -50,404 +133,137 @@ namespace MWMechanics // Fall back to a "caster to target" direction if we have no other means of determining it // (e.g. when cast by a non-actor) if (!mTarget.isEmpty()) - fallbackDirection = - (mTarget.getRefData().getPosition().asVec3() + offset) - - (mCaster.getRefData().getPosition().asVec3()); + fallbackDirection = (mTarget.getRefData().getPosition().asVec3() + offset) + - (mCaster.getRefData().getPosition().asVec3()); - MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection); + MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection, mItem); } - void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, - const ESM::EffectList &effects, ESM::RangeType range, bool reflected, bool exploded) + void CastSpell::inflict( + const MWWorld::Ptr& target, const ESM::EffectList& effects, ESM::RangeType range, bool exploded) const { + bool targetIsDeadActor = false; const bool targetIsActor = !target.isEmpty() && target.getClass().isActor(); if (targetIsActor) { - // Early-out for characters that have departed. const auto& stats = target.getClass().getCreatureStats(target); if (stats.isDead() && stats.isDeathAnimationFinished()) - return; + targetIsDeadActor = true; } // If none of the effects need to apply, we can early-out bool found = false; - for (const ESM::ENAMstruct& effect : effects.mList) + bool containsRecastable = false; + const auto& store = MWBase::Environment::get().getESMStore()->get(); + for (const ESM::IndexedENAMstruct& effect : effects.mList) { - if (effect.mRange == range) + if (effect.mData.mRange == range) { found = true; - break; + const ESM::MagicEffect* magicEffect = store.find(effect.mData.mEffectID); + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable)) + containsRecastable = true; } } if (!found) return; - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search (mId); - if (spell && targetIsActor && (spell->mData.mType == ESM::Spell::ST_Disease || spell->mData.mType == ESM::Spell::ST_Blight)) - { - int requiredResistance = (spell->mData.mType == ESM::Spell::ST_Disease) ? - ESM::MagicEffect::ResistCommonDisease - : ESM::MagicEffect::ResistBlightDisease; - float x = target.getClass().getCreatureStats(target).getMagicEffects().get(requiredResistance).getMagnitude(); + ActiveSpells::ActiveSpellParams params(mCaster, mId, mSourceName, mItem); + params.setFlag(mFlags); + bool castByPlayer = (!mCaster.isEmpty() && mCaster == getPlayer()); - if (Misc::Rng::roll0to99() <= x) - { - // Fully resisted, show message - if (target == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - return; - } - } - - ESM::EffectList reflectedEffects; - std::vector appliedLastingEffects; - - // HACK: cache target's magic effects here, and add any applied effects to it. Use the cached effects for determining resistance. - // This is required for Weakness effects in a spell to apply to any subsequent effects in the spell. - // Otherwise, they'd only apply after the whole spell was added. - MagicEffects targetEffects; + const ActiveSpells* targetSpells = nullptr; if (targetIsActor) - targetEffects += target.getClass().getCreatureStats(target).getMagicEffects(); - - bool castByPlayer = (!caster.isEmpty() && caster == getPlayer()); + targetSpells = &target.getClass().getCreatureStats(target).getActiveSpells(); - ActiveSpells targetSpells; - if (targetIsActor) - targetSpells = target.getClass().getCreatureStats(target).getActiveSpells(); - - bool canCastAnEffect = false; // For bound equipment.If this remains false - // throughout the iteration of this spell's - // effects, we display a "can't re-cast" message - - // Try absorbing the spell. Some handling must still happen for absorbed effects. - bool absorbed = absorbSpell(mId, caster, target); - - int currentEffectIndex = 0; - for (std::vector::const_iterator effectIt (effects.mList.begin()); - !target.isEmpty() && effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex) + // Re-casting a bound equipment effect has no effect if the spell is still active + if (!containsRecastable && targetSpells && targetSpells->isSpellActive(mId)) { - if (effectIt->mRange != range) - continue; + if (castByPlayer) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCannotRecast}"); + return; + } - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find ( - effectIt->mEffectID); + for (auto& enam : effects.mList) + { + if (target.isEmpty()) + break; - // Re-casting a bound equipment effect has no effect if the spell is still active - if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells.isSpellActive(mId)) - { - if (effectIt == (effects.mList.end() - 1) && !canCastAnEffect && castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCannotRecast}"); + if (enam.mData.mRange != range) continue; - } - canCastAnEffect = true; - - if (!checkEffectTarget(effectIt->mEffectID, target, caster, castByPlayer)) + const ESM::MagicEffect* magicEffect = store.find(enam.mData.mEffectID); + if (!magicEffect) continue; - // caster needs to be an actor for linked effects (e.g. Absorb) if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked - && (caster.isEmpty() || !caster.getClass().isActor())) - continue; - - // Notify the target actor they've been hit - bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; - if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) - target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); - - // Avoid proceeding further for absorbed spells. - if (absorbed) - continue; - - // Reflect harmful effects - if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects)) + && (mCaster.isEmpty() || !mCaster.getClass().isActor())) continue; - // Try resisting. - float magnitudeMult = getEffectMultiplier(effectIt->mEffectID, target, caster, spell, &targetEffects); - if (magnitudeMult == 0) + ActiveSpells::ActiveEffect effect; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; + effect.mTimeLeft = 0.f; + effect.mEffectIndex = enam.mIndex; + effect.mFlags = ESM::ActiveEffect::Flag_None; + if (mScriptedSpell) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect; + + bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); + effect.mDuration = hasDuration ? static_cast(enam.mData.mDuration) : 1.f; + + bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; + if (!appliedOnce) + effect.mDuration = std::max(1.f, effect.mDuration); + + effect.mTimeLeft = effect.mDuration; + + // add to list of active effects, to apply in next frame + params.getEffects().emplace_back(effect); + + bool effectAffectsHealth = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful + || enam.mData.mEffectID == ESM::MagicEffect::RestoreHealth; + if (castByPlayer && target != mCaster && targetIsActor && !targetIsDeadActor && effectAffectsHealth) { - // Fully resisted, show message - if (target == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - else if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + // If player is attempting to cast a harmful spell on or is healing a living target, show the target's + // HP bar. + MWBase::Environment::get().getWindowManager()->setEnemy(target); } - else - { - float magnitude = effectIt->mMagnMin + Misc::Rng::rollDice(effectIt->mMagnMax - effectIt->mMagnMin + 1); - magnitude *= magnitudeMult; - if (!target.getClass().isActor()) - { - // non-actor objects have no list of active magic effects, so have to apply instantly - if (!applyInstantEffect(target, caster, EffectKey(*effectIt), magnitude)) - continue; - } - else // target.getClass().isActor() == true - { - ActiveSpells::ActiveEffect effect; - effect.mEffectId = effectIt->mEffectID; - effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; - effect.mMagnitude = magnitude; - effect.mTimeLeft = 0.f; - effect.mEffectIndex = currentEffectIndex; - - // Avoid applying absorb effects if the caster is the target - // We still need the spell to be added - if (caster == target - && effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute - && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) - { - effect.mMagnitude = 0; - } - - // Avoid applying harmful effects to the player in god mode - if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) - { - effect.mMagnitude = 0; - } - - bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; - if (castByPlayer && target != caster && !target.getClass().getCreatureStats(target).isDead() && effectAffectsHealth) - { - // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. - MWBase::Environment::get().getWindowManager()->setEnemy(target); - } - - bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); - effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; - - bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; - if (!appliedOnce) - effect.mDuration = std::max(1.f, effect.mDuration); - - if (effect.mDuration == 0) - { - // We still should add effect to list to allow GetSpellEffects to detect this spell - appliedLastingEffects.push_back(effect); - - // duration 0 means apply full magnitude instantly - bool wasDead = target.getClass().getCreatureStats(target).isDead(); - effectTick(target.getClass().getCreatureStats(target), target, EffectKey(*effectIt), effect.mMagnitude); - bool isDead = target.getClass().getCreatureStats(target).isDead(); - - if (!wasDead && isDead) - MWBase::Environment::get().getMechanicsManager()->actorKilled(target, caster); - } - else - { - effect.mTimeLeft = effect.mDuration; - - targetEffects.add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(effect.mMagnitude)); - - // add to list of active effects, to apply in next frame - appliedLastingEffects.push_back(effect); - - // Unequip all items, if a spell with the ExtraSpell effect was casted - if (effectIt->mEffectID == ESM::MagicEffect::ExtraSpell && target.getClass().hasInventoryStore(target)) - { - MWWorld::InventoryStore& store = target.getClass().getInventoryStore(target); - store.unequipAll(target); - } - - // Command spells should have their effect, including taking the target out of combat, each time the spell successfully affects the target - if (((effectIt->mEffectID == ESM::MagicEffect::CommandHumanoid && target.getClass().isNpc()) - || (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name())) - && !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel()) - { - MWMechanics::AiFollow package(caster, true); - target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); - } - - // For absorb effects, also apply the effect to the caster - but with a negative - // magnitude, since we're transferring stats from the target to the caster - if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) - absorbStat(*effectIt, effect, caster, target, reflected, mSourceName); - } - } - - // Re-casting a summon effect will remove the creature from previous castings of that effect. - if (isSummoningEffect(effectIt->mEffectID) && targetIsActor) - { - CreatureStats& targetStats = target.getClass().getCreatureStats(target); - ESM::SummonKey key(effectIt->mEffectID, mId, currentEffectIndex); - auto findCreature = targetStats.getSummonedCreatureMap().find(key); - if (findCreature != targetStats.getSummonedCreatureMap().end()) - { - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, findCreature->second); - targetStats.getSummonedCreatureMap().erase(findCreature); - } - } - - if (target.getClass().isActor() || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) - { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!magicEffect->mHitSound.empty()) - sndMgr->playSound3D(target, magicEffect->mHitSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(target, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); - - // Add VFX - const ESM::Static* castStatic; - if (!magicEffect->mHit.empty()) - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); - else - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_DefaultHit"); - - bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; - // Note: in case of non actor, a free effect should be fine as well - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); - if (anim && !castStatic->mModel.empty()) - anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); - } + if (!targetIsActor && magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + { + playEffects(target, *magicEffect); } } if (!exploded) - MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile); + explodeSpell(effects, target, range); if (!target.isEmpty()) { - if (!reflectedEffects.mList.empty()) - inflict(caster, target, reflectedEffects, range, true, exploded); - - if (!appliedLastingEffects.empty()) + if (!params.getEffects().empty()) { - int casterActorId = -1; - if (!caster.isEmpty() && caster.getClass().isActor()) - casterActorId = caster.getClass().getCreatureStats(caster).getActorId(); - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(mId, mStack, appliedLastingEffects, - mSourceName, casterActorId); - } - } - } - - bool CastSpell::applyInstantEffect(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, const MWMechanics::EffectKey& effect, float magnitude) - { - short effectId = effect.mId; - if (target.getClass().canLock(target)) - { - if (effectId == ESM::MagicEffect::Lock) - { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation) - animation->addSpellCastGlow(magiceffect); - if (target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); - target.getCellRef().lock(static_cast(magnitude)); - } - return true; - } - else if (effectId == ESM::MagicEffect::Open) - { - if (!caster.isEmpty()) + if (targetIsActor) { - MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target); - // Use the player instead of the caster for vanilla crime compatibility - } - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation) - animation->addSpellCastGlow(magiceffect); - if (target.getCellRef().getLockLevel() <= magnitude) - { - if (target.getCellRef().getLockLevel() > 0) - { - MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); - - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); - } - target.getCellRef().unlock(); + if (!targetIsDeadActor) + target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params); } else { - MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); - } - - return true; - } - } - else if (target.getClass().isActor() && effectId == ESM::MagicEffect::Dispel) - { - target.getClass().getCreatureStats(target).getActiveSpells().purgeAll(magnitude, true); - return true; - } - else if (target.getClass().isActor() && target == getPlayer()) - { - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mCaster); - bool teleportingEnabled = MWBase::Environment::get().getWorld()->isTeleportingEnabled(); - - if (effectId == ESM::MagicEffect::DivineIntervention || effectId == ESM::MagicEffect::AlmsiviIntervention) - { - if (!teleportingEnabled) - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - return true; - } - std::string marker = (effectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker"; - MWBase::Environment::get().getWorld()->teleportToClosestMarker(target, marker); - anim->removeEffect(effectId); - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_end"); - if (fx) - anim->addEffect("meshes\\" + fx->mModel, -1); - return true; - } - else if (effectId == ESM::MagicEffect::Mark) - { - if (teleportingEnabled) - { - MWBase::Environment::get().getWorld()->getPlayer().markPosition( - target.getCell(), target.getRefData().getPosition()); - } - else if (caster == getPlayer()) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - } - return true; - } - else if (effectId == ESM::MagicEffect::Recall) - { - if (!teleportingEnabled) - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - return true; - } - - MWWorld::CellStore* markedCell = nullptr; - ESM::Position markedPosition; - - MWBase::Environment::get().getWorld()->getPlayer().getMarkedPosition(markedCell, markedPosition); - if (markedCell) - { - MWWorld::ActionTeleport action(markedCell->isExterior() ? "" : markedCell->getCell()->mName, - markedPosition, false); - action.execute(target); - anim->removeEffect(effectId); + // Apply effects instantly. We can ignore effect deletion since the entire params object gets + // deleted afterwards anyway and we can ignore reflection since non-actors cannot reflect spells + for (auto& effect : params.getEffects()) + applyMagicEffect(target, mCaster, params, effect, 0.f); } - return true; } } - return false; } - - bool CastSpell::cast(const std::string &id) + bool CastSpell::cast(const ESM::RefId& id) { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); if (const auto spell = store.get().search(id)) return cast(spell); @@ -460,22 +276,26 @@ namespace MWMechanics throw std::runtime_error("ID type cannot be casted"); } - bool CastSpell::cast(const MWWorld::Ptr &item, bool launchProjectile) + bool CastSpell::cast(const MWWorld::Ptr& item, bool launchProjectile) { - std::string enchantmentName = item.getClass().getEnchantment(item); + const ESM::RefId& enchantmentName = item.getClass().getEnchantment(item); if (enchantmentName.empty()) throw std::runtime_error("can't cast an item without an enchantment"); mSourceName = item.getClass().getName(item); mId = item.getCellRef().getRefId(); - const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(enchantmentName); + const auto& store = MWBase::Environment::get().getESMStore(); + const ESM::Enchantment* enchantment = store->get().find(enchantmentName); - mStack = false; + // CastOnce enchantments (i.e. scrolls) never stack and the item is immediately destroyed, + // so don't track the source item. + if (enchantment->mData.mType != ESM::Enchantment::CastOnce) + mItem = item.getCellRef().getRefNum(); bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); bool isProjectile = false; - if (item.getTypeName() == typeid(ESM::Weapon).name()) + if (item.getType() == ESM::Weapon::sRecordId) { int type = item.get()->mBase->mData.mType; ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(type)->mWeaponClass; @@ -484,12 +304,14 @@ namespace MWMechanics int type = enchantment->mData.mType; // Check if there's enough charge left - if (!godmode && (type == ESM::Enchantment::WhenUsed || (!isProjectile && type == ESM::Enchantment::WhenStrikes))) + if (!godmode + && (type == ESM::Enchantment::WhenUsed || (!isProjectile && type == ESM::Enchantment::WhenStrikes))) { - int castCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), mCaster); + int castCost = getEffectiveEnchantmentCastCost(*enchantment, mCaster); if (item.getCellRef().getEnchantmentCharge() == -1) - item.getCellRef().setEnchantmentCharge(static_cast(enchantment->mData.mCharge)); + item.getCellRef().setEnchantmentCharge( + static_cast(MWMechanics::getEnchantmentCharge(*enchantment))); if (item.getCellRef().getEnchantmentCharge() < castCost) { @@ -498,19 +320,17 @@ namespace MWMechanics MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientCharge}"); // Failure sound - int school = 0; + ESM::RefId school = ESM::Skill::Alteration; if (!enchantment->mEffects.mList.empty()) { - short effectId = enchantment->mEffects.mList.front().mEffectID; - const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); + short effectId = enchantment->mEffects.mList.front().mData.mEffectID; + const ESM::MagicEffect* magicEffect = store->get().find(effectId); school = magicEffect->mData.mSchool; } - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(mCaster, "Spell Failure " + schools[school], 1.0f, 1.0f); + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D( + mCaster, store->get().find(school)->mSchool->mFailureSound, 1.0f, 1.0f); } return false; } @@ -521,28 +341,31 @@ namespace MWMechanics if (type == ESM::Enchantment::WhenUsed) { if (mCaster == getPlayer()) - mCaster.getClass().skillUsageSucceeded (mCaster, ESM::Skill::Enchant, 1); + mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, ESM::Skill::Enchant_UseMagicItem); } else if (type == ESM::Enchantment::CastOnce) { if (!godmode) - item.getContainerStore()->remove(item, 1, mCaster); + item.getContainerStore()->remove(item, 1); } else if (type == ESM::Enchantment::WhenStrikes) { if (mCaster == getPlayer()) - mCaster.getClass().skillUsageSucceeded (mCaster, ESM::Skill::Enchant, 3); + mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, ESM::Skill::Enchant_CastOnStrike); } - inflict(mCaster, mCaster, enchantment->mEffects, ESM::RT_Self); + if (isProjectile) + inflict(mTarget, enchantment->mEffects, ESM::RT_Self); + else + inflict(mCaster, enchantment->mEffects, ESM::RT_Self); if (isProjectile || !mTarget.isEmpty()) - inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Touch); + inflict(mTarget, enchantment->mEffects, ESM::RT_Touch); if (launchProjectile) launchMagicBolt(); else if (isProjectile || !mTarget.isEmpty()) - inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Target); + inflict(mTarget, enchantment->mEffects, ESM::RT_Target); return true; } @@ -551,9 +374,13 @@ namespace MWMechanics { mSourceName = potion->mName; mId = potion->mId; - mStack = true; + mFlags = static_cast( + ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable); - inflict(mCaster, mCaster, potion->mEffects, ESM::RT_Self); + // Ignore range and don't apply area of effect + inflict(mCaster, potion->mEffects, ESM::RT_Self, true); + inflict(mCaster, potion->mEffects, ESM::RT_Touch, true); + inflict(mCaster, potion->mEffects, ESM::RT_Target, true); return true; } @@ -562,15 +389,12 @@ namespace MWMechanics { mSourceName = spell->mName; mId = spell->mId; - mStack = false; - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - - int school = 0; + ESM::RefId school = ESM::Skill::Alteration; bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mManualSpell) + if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mScriptedSpell) { school = getSpellSchool(spell, mCaster); @@ -578,21 +402,12 @@ namespace MWMechanics if (!godmode) { - // Reduce fatigue (note that in the vanilla game, both GMSTs are 0, and there's no fatigue loss) - static const float fFatigueSpellBase = store.get().find("fFatigueSpellBase")->mValue.getFloat(); - static const float fFatigueSpellMult = store.get().find("fFatigueSpellMult")->mValue.getFloat(); - DynamicStat fatigue = stats.getFatigue(); - const float normalizedEncumbrance = mCaster.getClass().getNormalizedEncumbrance(mCaster); - - float fatigueLoss = spell->mData.mCost * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult); - fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); - stats.setFatigue(fatigue); - bool fail = false; // Check success float successChance = getSpellSuccessChance(spell, mCaster, nullptr, true, false); - if (Misc::Rng::roll0to99() >= successChance) + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + if (Misc::Rng::roll0to99(prng) >= successChance) { if (mCaster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicSkillFail}"); @@ -602,12 +417,9 @@ namespace MWMechanics if (fail) { // Failure sound - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(mCaster, "Spell Failure " + schools[school], 1.0f, 1.0f); + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + const ESM::Skill* skill = MWBase::Environment::get().getESMStore()->get().find(school); + sndMgr->playSound3D(mCaster, skill->mSchool->mFailureSound, 1.0f, 1.0f); return false; } } @@ -617,148 +429,161 @@ namespace MWMechanics stats.getSpells().usePower(spell); } - if (!mManualSpell && mCaster == getPlayer() && spellIncreasesSkill(spell)) - mCaster.getClass().skillUsageSucceeded(mCaster, spellSchoolToSkill(school), 0); + if (!mScriptedSpell && mCaster == getPlayer() && spellIncreasesSkill(spell)) + mCaster.getClass().skillUsageSucceeded(mCaster, school, ESM::Skill::Spellcast_Success); // A non-actor doesn't play its spell cast effects from a character controller, so play them here if (!mCaster.getClass().isActor()) playSpellCastingEffects(spell->mEffects.mList); - inflict(mCaster, mCaster, spell->mEffects, ESM::RT_Self); + inflict(mCaster, spell->mEffects, ESM::RT_Self); if (!mTarget.isEmpty()) - inflict(mTarget, mCaster, spell->mEffects, ESM::RT_Touch); + inflict(mTarget, spell->mEffects, ESM::RT_Touch); launchMagicBolt(); return true; } - bool CastSpell::cast (const ESM::Ingredient* ingredient) + bool CastSpell::cast(const ESM::Ingredient* ingredient) { mId = ingredient->mId; - mStack = true; + mFlags = static_cast( + ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable); mSourceName = ingredient->mName; - ESM::ENAMstruct effect; - effect.mEffectID = ingredient->mData.mEffectID[0]; - effect.mSkill = ingredient->mData.mSkills[0]; - effect.mAttribute = ingredient->mData.mAttributes[0]; - effect.mRange = ESM::RT_Self; - effect.mArea = 0; - - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const auto magicEffect = store.get().find(effect.mEffectID); - const MWMechanics::CreatureStats& creatureStats = mCaster.getClass().getCreatureStats(mCaster); - - float x = (mCaster.getClass().getSkill(mCaster, ESM::Skill::Alchemy) + - 0.2f * creatureStats.getAttribute (ESM::Attribute::Intelligence).getModified() - + 0.1f * creatureStats.getAttribute (ESM::Attribute::Luck).getModified()) - * creatureStats.getFatigueTerm(); + auto effect = rollIngredientEffect(mCaster, ingredient, mCaster != getPlayer()); - int roll = Misc::Rng::roll0to99(); - if (roll > x) + if (effect) + inflict(mCaster, *effect, ESM::RT_Self); + else { // "X has no effect on you" - std::string message = store.get().find("sNotifyMessage50")->mValue.getString(); + std::string message = MWBase::Environment::get() + .getESMStore() + ->get() + .find("sNotifyMessage50") + ->mValue.getString(); message = Misc::StringUtils::format(message, ingredient->mName); MWBase::Environment::get().getWindowManager()->messageBox(message); return false; } - float magnitude = 0; - float y = roll / std::min(x, 100.f); - y *= 0.25f * x; - if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) - effect.mDuration = 1; - else - effect.mDuration = static_cast(y); - if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) - { - if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) - magnitude = floor((0.05f * y) / (0.1f * magicEffect->mData.mBaseCost)); - else - magnitude = floor(y / (0.1f * magicEffect->mData.mBaseCost)); - magnitude = std::max(1.f, magnitude); - } - else - magnitude = 1; - - effect.mMagnMax = static_cast(magnitude); - effect.mMagnMin = static_cast(magnitude); - - ESM::EffectList effects; - effects.mList.push_back(effect); - - inflict(mCaster, mCaster, effects, ESM::RT_Self); - return true; } - void CastSpell::playSpellCastingEffects(const std::string &spellid, bool enchantment) + void CastSpell::playSpellCastingEffects(const ESM::Enchantment* enchantment) const { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - if (enchantment) - { - if (const auto spell = store.get().search(spellid)) - playSpellCastingEffects(spell->mEffects.mList); - } - else - { - if (const auto spell = store.get().search(spellid)) - playSpellCastingEffects(spell->mEffects.mList); - } + playSpellCastingEffects(enchantment->mEffects.mList); + } + + void CastSpell::playSpellCastingEffects(const ESM::Spell* spell) const + { + playSpellCastingEffects(spell->mEffects.mList); } - void CastSpell::playSpellCastingEffects(const std::vector& effects) + void CastSpell::playSpellCastingEffects(const std::vector& effects) const { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); std::vector addedEffects; - for (const ESM::ENAMstruct& effectData : effects) + + for (const ESM::IndexedENAMstruct& effectData : effects) { - const auto effect = store.get().find(effectData.mEffectID); + const auto effect = store.get().find(effectData.mData.mEffectID); const ESM::Static* castStatic; if (!effect->mCasting.empty()) - castStatic = store.get().find (effect->mCasting); + castStatic = store.get().find(effect->mCasting); else - castStatic = store.get().find ("VFX_DefaultCast"); + castStatic = store.get().find(ESM::RefId::stringRefId("VFX_DefaultCast")); // check if the effect was already added - if (std::find(addedEffects.begin(), addedEffects.end(), "meshes\\" + castStatic->mModel) != addedEffects.end()) + if (std::find(addedEffects.begin(), addedEffects.end(), + Misc::ResourceHelpers::correctMeshPath(castStatic->mModel)) + != addedEffects.end()) continue; MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster); if (animation) { - animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex, false, "", effect->mParticle); + animation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), + ESM::MagicEffect::indexToName(effect->mIndex), false, {}, effect->mParticle); } else { // If the caster has no animation, add the effect directly to the effectManager - // We should scale it manually - osg::Vec3f bounds (MWBase::Environment::get().getWorld()->getHalfExtents(mCaster) * 2.f / Constants::UnitsPerFoot); - float scale = std::max({ bounds.x()/3.f, bounds.y()/3.f, bounds.z()/6.f }); - float meshScale = !mCaster.getClass().isActor() ? mCaster.getCellRef().getScale() : 1.0f; - osg::Vec3f pos (mCaster.getRefData().getPosition().asVec3()); - MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + castStatic->mModel, effect->mParticle, pos, scale * meshScale); + // We must scale and position it manually + float scale = mCaster.getCellRef().getScale(); + osg::Vec3f pos(mCaster.getRefData().getPosition().asVec3()); + if (!mCaster.getClass().isNpc()) + { + osg::Vec3f bounds(MWBase::Environment::get().getWorld()->getHalfExtents(mCaster) * 2.f); + scale *= std::max({ bounds.x(), bounds.y(), bounds.z() / 2.f }) / 64.f; + float offset = 0.f; + if (bounds.z() < 128.f) + offset = bounds.z() - 128.f; + else if (bounds.z() < bounds.x() + bounds.y()) + offset = 128.f - bounds.z(); + if (MWBase::Environment::get().getWorld()->isFlying(mCaster)) + offset /= 20.f; + pos.z() += offset * scale; + } + else + { + // Additionally use the NPC's height + osg::Vec3f npcScaleVec(1.f, 1.f, 1.f); + mCaster.getClass().adjustScale(mCaster, npcScaleVec, true); + scale *= npcScaleVec.z(); + } + scale = std::max(scale, 1.f); + MWBase::Environment::get().getWorld()->spawnEffect( + Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), effect->mParticle, pos, scale); } if (animation && !mCaster.getClass().isActor()) - animation->addSpellCastGlow(effect); - - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; + animation->addSpellCastGlow(effect->getColor()); - addedEffects.push_back("meshes\\" + castStatic->mModel); + addedEffects.push_back(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel)); - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!effect->mCastSound.empty()) + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + if (!effect->mCastSound.empty()) sndMgr->playSound3D(mCaster, effect->mCastSound, 1.0f, 1.0f); else - sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); + sndMgr->playSound3D( + mCaster, store.get().find(effect->mData.mSchool)->mSchool->mCastSound, 1.0f, 1.0f); + } + } + + void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping) + { + const auto& store = MWBase::Environment::get().getESMStore(); + if (playNonLooping) + { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + if (!magicEffect.mHitSound.empty()) + sndMgr->playSound3D(target, magicEffect.mHitSound, 1.0f, 1.0f); + else + sndMgr->playSound3D( + target, store->get().find(magicEffect.mData.mSchool)->mSchool->mHitSound, 1.0f, 1.0f); + } + + // Add VFX + const ESM::Static* castStatic; + if (!magicEffect.mHit.empty()) + castStatic = store->get().find(magicEffect.mHit); + else + castStatic = store->get().find(ESM::RefId::stringRefId("VFX_DefaultHit")); + + bool loop = (magicEffect.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); + if (anim && !castStatic->mModel.empty()) + { + // Don't play particle VFX unless the effect is new or it should be looping. + if (playNonLooping || loop) + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), + ESM::MagicEffect::indexToName(magicEffect.mIndex), loop, {}, magicEffect.mParticle); } } } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index cc525d2db6d..23d3b807139 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -1,7 +1,8 @@ #ifndef MWMECHANICS_SPELLCASTING_H #define MWMECHANICS_SPELLCASTING_H -#include +#include +#include #include "../mwworld/ptr.hpp" @@ -11,6 +12,8 @@ namespace ESM struct Ingredient; struct Potion; struct EffectList; + struct Enchantment; + struct MagicEffect; } namespace MWMechanics @@ -23,48 +26,54 @@ namespace MWMechanics MWWorld::Ptr mCaster; // May be empty MWWorld::Ptr mTarget; // May be empty - void playSpellCastingEffects(const std::vector& effects); + void playSpellCastingEffects(const std::vector& effects) const; + + void explodeSpell(const ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const; + + /// Launch a bolt with the given effects. + void launchMagicBolt() const; public: - bool mStack{false}; - std::string mId; // ID of spell, potion, item etc + ESM::RefId mId; // ID of spell, potion, item etc std::string mSourceName; // Display name for spell, potion, etc - osg::Vec3f mHitPosition{0,0,0}; // Used for spawning area orb - bool mAlwaysSucceed{false}; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) + osg::Vec3f mHitPosition{ 0, 0, 0 }; // Used for spawning area orb + bool mAlwaysSucceed{ + false + }; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon) - bool mManualSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, etc.) + bool mScriptedSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, + // etc.) + ESM::RefNum mItem; + ESM::ActiveSpells::Flags mFlags{ ESM::ActiveSpells::Flag_Temporary }; - public: - CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile=false, const bool manualSpell=false); + CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile = false, + const bool scriptedSpell = false); - bool cast (const ESM::Spell* spell); + bool cast(const ESM::Spell* spell); /// @note mCaster must be an actor - /// @param launchProjectile If set to false, "on target" effects are directly applied instead of being launched as projectile originating from the caster. - bool cast (const MWWorld::Ptr& item, bool launchProjectile=true); + /// @param launchProjectile If set to false, "on target" effects are directly applied instead of being launched + /// as projectile originating from the caster. + bool cast(const MWWorld::Ptr& item, bool launchProjectile = true); /// @note mCaster must be an NPC - bool cast (const ESM::Ingredient* ingredient); + bool cast(const ESM::Ingredient* ingredient); - bool cast (const ESM::Potion* potion); + bool cast(const ESM::Potion* potion); /// @note Auto detects if spell, ingredient or potion - bool cast (const std::string& id); + bool cast(const ESM::RefId& id); - void playSpellCastingEffects(const std::string &spellid, bool enchantment); + void playSpellCastingEffects(const ESM::Enchantment* enchantment) const; - /// Launch a bolt with the given effects. - void launchMagicBolt (); + void playSpellCastingEffects(const ESM::Spell* spell) const; /// @note \a target can be any type of object, not just actors. - /// @note \a caster can be any type of object, or even an empty object. - void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, - const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false, bool exploded=false); - - /// @note \a caster can be any type of object, or even an empty object. - /// @return was the target suitable for the effect? - bool applyInstantEffect (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const MWMechanics::EffectKey& effect, float magnitude); + void inflict(const MWWorld::Ptr& target, const ESM::EffectList& effects, ESM::RangeType range, + bool exploded = false) const; }; + + void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping = true); } #endif diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp new file mode 100644 index 00000000000..96044ebc5bb --- /dev/null +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -0,0 +1,1289 @@ +#include "spelleffects.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/aifollow.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/spellresistance.hpp" +#include "../mwmechanics/spellutil.hpp" +#include "../mwmechanics/summoning.hpp" + +#include "../mwrender/animation.hpp" + +#include "../mwworld/actionequip.hpp" +#include "../mwworld/actionteleport.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/player.hpp" + +namespace +{ + float roll(const ESM::ActiveEffect& effect) + { + if (effect.mMinMagnitude == effect.mMaxMagnitude) + return effect.mMinMagnitude; + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + return effect.mMinMagnitude + Misc::Rng::rollDice(effect.mMaxMagnitude - effect.mMinMagnitude + 1, prng); + } + + void modifyAiSetting(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, + ESM::MagicEffect::Effects creatureEffect, MWMechanics::AiSetting setting, float magnitude, bool& invalid) + { + if (target == MWMechanics::getPlayer() || (effect.mEffectId == creatureEffect) == target.getClass().isNpc()) + invalid = true; + else + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getAiSetting(setting); + stat.setModifier(static_cast(stat.getModifier() + magnitude)); + creatureStats.setAiSetting(setting, stat); + } + } + + void adjustDynamicStat(const MWWorld::Ptr& target, int index, float magnitude, bool allowDecreaseBelowZero = false, + bool allowIncreaseAboveModified = false) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getDynamic(index); + stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero, allowIncreaseAboveModified); + creatureStats.setDynamic(index, stat); + } + + void modDynamicStat(const MWWorld::Ptr& target, int index, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getDynamic(index); + float current = stat.getCurrent(); + stat.setBase(std::max(0.f, stat.getBase() + magnitude)); + stat.setCurrent(current + magnitude); + creatureStats.setDynamic(index, stat); + } + + void damageAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attribute = effect.getSkillOrAttribute(); + auto attr = creatureStats.getAttribute(attribute); + if (effect.mEffectId == ESM::MagicEffect::DamageAttribute) + magnitude = std::min(attr.getModified(), magnitude); + attr.damage(magnitude); + creatureStats.setAttribute(attribute, attr); + } + + void restoreAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attribute = effect.getSkillOrAttribute(); + auto attr = creatureStats.getAttribute(attribute); + attr.restore(magnitude); + creatureStats.setAttribute(attribute, attr); + } + + void fortifyAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attribute = effect.getSkillOrAttribute(); + auto attr = creatureStats.getAttribute(attribute); + attr.setModifier(attr.getModifier() + magnitude); + creatureStats.setAttribute(attribute, attr); + } + + void damageSkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.getSkillOrAttribute()); + if (effect.mEffectId == ESM::MagicEffect::DamageSkill) + magnitude = std::min(skill.getModified(), magnitude); + skill.damage(magnitude); + } + + void restoreSkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.getSkillOrAttribute()); + skill.restore(magnitude); + } + + void fortifySkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.getSkillOrAttribute()); + skill.setModifier(skill.getModifier() + magnitude); + } + + bool disintegrateSlot(const MWWorld::Ptr& ptr, int slot, float disintegrate) + { + MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); + MWWorld::ContainerStoreIterator item = inv.getSlot(slot); + + if (item != inv.end() + && (item.getType() == MWWorld::ContainerStore::Type_Armor + || item.getType() == MWWorld::ContainerStore::Type_Weapon)) + { + if (!item->getClass().hasItemHealth(*item)) + return false; + int charge = item->getClass().getItemHealth(*item); + if (charge == 0) + return false; + + // Store remainder of disintegrate amount (automatically subtracted if > 1) + item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); + + charge = item->getClass().getItemHealth(*item); + charge -= std::min(static_cast(disintegrate), charge); + item->getCellRef().setCharge(charge); + + if (charge == 0) + { + // Will unequip the broken item and try to find a replacement + if (ptr != MWMechanics::getPlayer()) + inv.autoEquip(); + else + inv.unequipItem(*item); + } + + return true; + } + + return false; + } + + int getBoundItemSlot(const MWWorld::Ptr& boundPtr) + { + const auto [slots, _] = boundPtr.getClass().getEquipmentSlots(boundPtr); + if (!slots.empty()) + return slots[0]; + return -1; + } + + void addBoundItem(const ESM::RefId& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1); + + int slot = getBoundItemSlot(boundPtr); + auto prevItem = slot >= 0 ? store.getSlot(slot) : store.end(); + + MWWorld::ActionEquip action(boundPtr); + action.execute(actor); + + if (actor != MWMechanics::getPlayer()) + return; + + MWWorld::Ptr newItem; + auto it = slot >= 0 ? store.getSlot(slot) : store.end(); + // Equip can fail because beast races cannot equip boots/helmets + if (it != store.end()) + newItem = *it; + + if (newItem.isEmpty() || boundPtr != newItem) + return; + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + + // change draw state only if the item is in player's right hand + if (slot == MWWorld::InventoryStore::Slot_CarriedRight) + player.setDrawState(MWMechanics::DrawState::Weapon); + + if (prevItem != store.end()) + player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); + } + + void removeBoundItem(const ESM::RefId& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + auto item = std::find_if( + store.begin(), store.end(), [&](const auto& it) { return it.getCellRef().getRefId() == itemId; }); + if (item == store.end()) + return; + int slot = getBoundItemSlot(*item); + + auto currentItem = store.getSlot(slot); + + bool wasEquipped = currentItem != store.end() && currentItem->getCellRef().getRefId() == itemId; + + if (wasEquipped) + store.remove(*currentItem, 1); + else + store.remove(itemId, 1); + + if (actor != MWMechanics::getPlayer()) + { + // Equip a replacement + if (!wasEquipped) + return; + + auto type = currentItem->getType(); + if (type != ESM::Weapon::sRecordId && type != ESM::Armor::sRecordId && type != ESM::Clothing::sRecordId) + return; + + if (actor.getClass().getCreatureStats(actor).isDead()) + return; + + if (!actor.getClass().hasInventoryStore(actor)) + return; + + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + return; + + actor.getClass().getInventoryStore(actor).autoEquip(); + + return; + } + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + ESM::RefId prevItemId = player.getPreviousItem(itemId); + player.erasePreviousItem(itemId); + + if (!prevItemId.empty() && wasEquipped) + { + // Find previous item (or its replacement) by id. + // we should equip previous item only if expired bound item was equipped. + MWWorld::Ptr prevItem = store.findReplacement(prevItemId); + if (!prevItem.isEmpty()) + { + MWWorld::ActionEquip action(prevItem); + action.execute(actor); + } + } + } + + bool isCorprusEffect(const MWMechanics::ActiveSpells::ActiveEffect& effect, bool harmfulOnly = false) + { + if (effect.mFlags & ESM::ActiveEffect::Flag_Applied && effect.mEffectId != ESM::MagicEffect::Corprus) + { + const auto* magicEffect + = MWBase::Environment::get().getESMStore()->get().find(effect.mEffectId); + if (magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce + && (!harmfulOnly || magicEffect->mData.mFlags & ESM::MagicEffect::Flags::Harmful)) + return true; + } + return false; + } + + void absorbSpell(const MWMechanics::ActiveSpells::ActiveSpellParams& spellParams, const MWWorld::Ptr& caster, + const MWWorld::Ptr& target) + { + const auto& esmStore = *MWBase::Environment::get().getESMStore(); + const ESM::Static* absorbStatic = esmStore.get().find(ESM::RefId::stringRefId("VFX_Absorb")); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); + if (animation && !absorbStatic->mModel.empty()) + animation->addEffect(Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel), + ESM::MagicEffect::indexToName(ESM::MagicEffect::SpellAbsorption), false); + int spellCost = 0; + if (const ESM::Spell* spell = esmStore.get().search(spellParams.getSourceSpellId())) + { + spellCost = MWMechanics::calcSpellCost(*spell); + } + else + { + const ESM::Enchantment* enchantment = esmStore.get().search(spellParams.getEnchantment()); + if (enchantment) + spellCost = MWMechanics::getEffectiveEnchantmentCastCost(*enchantment, caster); + } + + // Magicka is increased by the cost of the spell + auto& stats = target.getClass().getCreatureStats(target); + auto magicka = stats.getMagicka(); + magicka.setCurrent(magicka.getCurrent() + spellCost); + stats.setMagicka(magicka); + } + + MWMechanics::MagicApplicationResult::Type applyProtections(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, + const MWMechanics::ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, + const ESM::MagicEffect* magicEffect) + { + auto& stats = target.getClass().getCreatureStats(target); + auto& magnitudes = stats.getMagicEffects(); + // Apply reflect and spell absorption + if (target != caster && spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary)) + { + bool canReflect = !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable) + && !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Reflect) + && magnitudes.getOrDefault(ESM::MagicEffect::Reflect).getMagnitude() > 0.f && !caster.isEmpty(); + bool canAbsorb = !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_SpellAbsorption) + && magnitudes.getOrDefault(ESM::MagicEffect::SpellAbsorption).getMagnitude() > 0.f; + if (canReflect || canAbsorb) + { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + for (const auto& activeParam : stats.getActiveSpells()) + { + for (const auto& activeEffect : activeParam.getEffects()) + { + if (!(activeEffect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + if (activeEffect.mEffectId == ESM::MagicEffect::Reflect) + { + if (canReflect && Misc::Rng::roll0to99(prng) < activeEffect.mMagnitude) + { + return MWMechanics::MagicApplicationResult::Type::REFLECTED; + } + } + else if (activeEffect.mEffectId == ESM::MagicEffect::SpellAbsorption) + { + if (canAbsorb && Misc::Rng::roll0to99(prng) < activeEffect.mMagnitude) + { + absorbSpell(spellParams, caster, target); + return MWMechanics::MagicApplicationResult::Type::REMOVED; + } + } + } + } + } + } + // Notify the target actor they've been hit + bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; + if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) + target.getClass().onHit( + target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true, MWMechanics::DamageSourceType::Magical); + // Apply resistances + if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) + { + const ESM::Spell* spell + = spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary) ? spellParams.getSpell() : nullptr; + float magnitudeMult + = MWMechanics::getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); + if (magnitudeMult == 0) + { + // Fully resisted, show message + if (target == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); + else if (caster == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + return MWMechanics::MagicApplicationResult::Type::REMOVED; + } + effect.mMinMagnitude *= magnitudeMult; + effect.mMaxMagnitude *= magnitudeMult; + } + return MWMechanics::MagicApplicationResult::Type::APPLIED; + } + + static const std::map sBoundItemsMap{ + { ESM::MagicEffect::BoundBattleAxe, "sMagicBoundBattleAxeID" }, + { ESM::MagicEffect::BoundBoots, "sMagicBoundBootsID" }, + { ESM::MagicEffect::BoundCuirass, "sMagicBoundCuirassID" }, + { ESM::MagicEffect::BoundDagger, "sMagicBoundDaggerID" }, + { ESM::MagicEffect::BoundGloves, "sMagicBoundLeftGauntletID" }, + { ESM::MagicEffect::BoundHelm, "sMagicBoundHelmID" }, + { ESM::MagicEffect::BoundLongbow, "sMagicBoundLongbowID" }, + { ESM::MagicEffect::BoundLongsword, "sMagicBoundLongswordID" }, + { ESM::MagicEffect::BoundMace, "sMagicBoundMaceID" }, + { ESM::MagicEffect::BoundShield, "sMagicBoundShieldID" }, + { ESM::MagicEffect::BoundSpear, "sMagicBoundSpearID" }, + }; +} + +namespace MWMechanics +{ + + void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, + const ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, bool& invalid, + bool& receivedMagicDamage, bool& affectedHealth, bool& recalculateMagicka) + { + const auto world = MWBase::Environment::get().getWorld(); + bool godmode = target == getPlayer() && world->getGodModeState(); + switch (effect.mEffectId) + { + case ESM::MagicEffect::CureCommonDisease: + target.getClass().getCreatureStats(target).getSpells().purgeCommonDisease(); + break; + case ESM::MagicEffect::CureBlightDisease: + target.getClass().getCreatureStats(target).getSpells().purgeBlightDisease(); + break; + case ESM::MagicEffect::RemoveCurse: + target.getClass().getCreatureStats(target).getSpells().purgeCurses(); + break; + case ESM::MagicEffect::CureCorprusDisease: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect( + target, ESM::MagicEffect::Corprus); + break; + case ESM::MagicEffect::CurePoison: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect( + target, ESM::MagicEffect::Poison); + break; + case ESM::MagicEffect::CureParalyzation: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect( + target, ESM::MagicEffect::Paralyze); + break; + case ESM::MagicEffect::Dispel: + // Dispel removes entire spells at once + target.getClass().getCreatureStats(target).getActiveSpells().purge( + [magnitude = effect.mMagnitude](const ActiveSpells::ActiveSpellParams& params) { + if (params.hasFlag(ESM::ActiveSpells::Flag_Temporary)) + { + const ESM::Spell* spell = params.getSpell(); + if (spell && spell->mData.mType == ESM::Spell::ST_Spell) + { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + return Misc::Rng::roll0to99(prng) < magnitude; + } + } + return false; + }, + target); + break; + case ESM::MagicEffect::AlmsiviIntervention: + case ESM::MagicEffect::DivineIntervention: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + { + std::string_view marker + = (effect.mEffectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker"; + world->teleportToClosestMarker(target, ESM::RefId::stringRefId(marker)); + if (!caster.isEmpty()) + { + MWRender::Animation* anim = world->getAnimation(caster); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); + const ESM::Static* fx + = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Summon_end")); + if (fx) + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), ""); + } + } + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::Mark: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + world->getPlayer().markPosition(target.getCell(), target.getRefData().getPosition()); + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::Recall: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + { + MWWorld::CellStore* markedCell = nullptr; + ESM::Position markedPosition; + + world->getPlayer().getMarkedPosition(markedCell, markedPosition); + if (markedCell) + { + ESM::RefId dest = markedCell->getCell()->getId(); + MWWorld::ActionTeleport action(dest, markedPosition, false); + action.execute(target); + if (!caster.isEmpty()) + { + MWRender::Animation* anim = world->getAnimation(caster); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); + } + } + } + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::CommandCreature: + case ESM::MagicEffect::CommandHumanoid: + if (caster.isEmpty() || !caster.getClass().isActor() || target == getPlayer() + || (effect.mEffectId == ESM::MagicEffect::CommandCreature) == target.getClass().isNpc()) + invalid = true; + else if (effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel()) + { + MWMechanics::AiFollow package(caster, true); + target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); + } + break; + case ESM::MagicEffect::ExtraSpell: + if (target.getClass().hasInventoryStore(target)) + { + auto& store = target.getClass().getInventoryStore(target); + store.unequipAll(); + } + else + invalid = true; + break; + case ESM::MagicEffect::TurnUndead: + if (target.getClass().isNpc() + || target.get()->mBase->mData.mType != ESM::Creature::Undead) + invalid = true; + else + { + auto& creatureStats = target.getClass().getCreatureStats(target); + Stat stat = creatureStats.getAiSetting(AiSetting::Flee); + stat.setModifier(static_cast(stat.getModifier() + effect.mMagnitude)); + creatureStats.setAiSetting(AiSetting::Flee, stat); + } + break; + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::FrenzyHumanoid: + modifyAiSetting( + target, effect, ESM::MagicEffect::FrenzyCreature, AiSetting::Fight, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::CalmHumanoid: + modifyAiSetting( + target, effect, ESM::MagicEffect::CalmCreature, AiSetting::Fight, -effect.mMagnitude, invalid); + if (!invalid && effect.mMagnitude > 0) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + creatureStats.getAiSequence().stopCombat(); + } + break; + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + modifyAiSetting( + target, effect, ESM::MagicEffect::DemoralizeCreature, AiSetting::Flee, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::RallyHumanoid: + modifyAiSetting( + target, effect, ESM::MagicEffect::RallyCreature, AiSetting::Flee, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::Charm: + if (!target.getClass().isNpc()) + invalid = true; + break; + case ESM::MagicEffect::Sound: + if (target == getPlayer()) + { + const auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + float volume = std::clamp( + (magnitudes.getOrDefault(effect.mEffectId).getModifier() + effect.mMagnitude) / 100.f, 0.f, + 1.f); + MWBase::Environment::get().getSoundManager()->playSound3D(target, + ESM::RefId::stringRefId("magic sound"), volume, 1.f, MWSound::Type::Sfx, + MWSound::PlayMode::LoopNoEnv); + } + break; + case ESM::MagicEffect::SummonScamp: + case ESM::MagicEffect::SummonClannfear: + case ESM::MagicEffect::SummonDaedroth: + case ESM::MagicEffect::SummonDremora: + case ESM::MagicEffect::SummonAncestralGhost: + case ESM::MagicEffect::SummonSkeletalMinion: + case ESM::MagicEffect::SummonBonewalker: + case ESM::MagicEffect::SummonGreaterBonewalker: + case ESM::MagicEffect::SummonBonelord: + case ESM::MagicEffect::SummonWingedTwilight: + case ESM::MagicEffect::SummonHunger: + case ESM::MagicEffect::SummonGoldenSaint: + case ESM::MagicEffect::SummonFlameAtronach: + case ESM::MagicEffect::SummonFrostAtronach: + case ESM::MagicEffect::SummonStormAtronach: + case ESM::MagicEffect::SummonCenturionSphere: + case ESM::MagicEffect::SummonFabricant: + case ESM::MagicEffect::SummonWolf: + case ESM::MagicEffect::SummonBear: + case ESM::MagicEffect::SummonBonewolf: + case ESM::MagicEffect::SummonCreature04: + case ESM::MagicEffect::SummonCreature05: + if (!target.isInCell()) + invalid = true; + else + effect.mArg = summonCreature(effect.mEffectId, target); + break; + case ESM::MagicEffect::BoundGloves: + if (!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + addBoundItem(ESM::RefId::stringRefId(world->getStore() + .get() + .find("sMagicBoundRightGauntletID") + ->mValue.getString()), + target); + // left gauntlet added below + [[fallthrough]]; + case ESM::MagicEffect::BoundDagger: + case ESM::MagicEffect::BoundLongsword: + case ESM::MagicEffect::BoundMace: + case ESM::MagicEffect::BoundBattleAxe: + case ESM::MagicEffect::BoundSpear: + case ESM::MagicEffect::BoundLongbow: + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundHelm: + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundShield: + if (!target.getClass().hasInventoryStore(target)) + invalid = true; + else + { + const std::string& item = sBoundItemsMap.at(effect.mEffectId); + addBoundItem(ESM::RefId::stringRefId( + world->getStore().get().find(item)->mValue.getString()), + target); + } + break; + case ESM::MagicEffect::FireDamage: + case ESM::MagicEffect::ShockDamage: + case ESM::MagicEffect::FrostDamage: + case ESM::MagicEffect::DamageHealth: + case ESM::MagicEffect::Poison: + case ESM::MagicEffect::DamageMagicka: + case ESM::MagicEffect::DamageFatigue: + if (!godmode) + { + int index = 0; + if (effect.mEffectId == ESM::MagicEffect::DamageMagicka) + index = 1; + else if (effect.mEffectId == ESM::MagicEffect::DamageFatigue) + index = 2; + // Damage "Dynamic" abilities reduce the base value + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) + modDynamicStat(target, index, -effect.mMagnitude); + else + { + adjustDynamicStat( + target, index, -effect.mMagnitude, index == 2 && Settings::game().mUncappedDamageFatigue); + if (index == 0) + receivedMagicDamage = affectedHealth = true; + } + } + break; + case ESM::MagicEffect::DamageAttribute: + if (!godmode) + damageAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::DamageSkill: + if (!target.getClass().isNpc()) + invalid = true; + else if (!godmode) + { + // Damage Skill abilities reduce base skill :todd: + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) + { + auto& npcStats = target.getClass().getNpcStats(target); + SkillValue& skill = npcStats.getSkill(effect.getSkillOrAttribute()); + // Damage Skill abilities reduce base skill :todd: + skill.setBase(std::max(skill.getBase() - effect.mMagnitude, 0.f)); + } + else + damageSkill(target, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::RestoreAttribute: + restoreAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::RestoreSkill: + if (!target.getClass().isNpc()) + invalid = true; + else + restoreSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::RestoreHealth: + affectedHealth = true; + [[fallthrough]]; + case ESM::MagicEffect::RestoreMagicka: + case ESM::MagicEffect::RestoreFatigue: + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::RestoreHealth, effect.mMagnitude); + break; + case ESM::MagicEffect::SunDamage: + { + // isInCell shouldn't be needed, but updateActor called during game start + if (!target.isInCell() || !(target.getCell()->isExterior() || target.getCell()->isQuasiExterior()) + || godmode) + break; + const float sunRisen = world->getSunPercentage(); + static float fMagicSunBlockedMult + = world->getStore().get().find("fMagicSunBlockedMult")->mValue.getFloat(); + const float damageScale = std::clamp( + std::max(world->getSunVisibility() * sunRisen, fMagicSunBlockedMult * sunRisen), 0.f, 1.f); + float damage = effect.mMagnitude * damageScale; + adjustDynamicStat(target, 0, -damage); + if (damage > 0.f) + receivedMagicDamage = affectedHealth = true; + } + break; + case ESM::MagicEffect::DrainHealth: + case ESM::MagicEffect::DrainMagicka: + case ESM::MagicEffect::DrainFatigue: + if (!godmode) + { + int index = effect.mEffectId - ESM::MagicEffect::DrainHealth; + // Unlike Absorb and Damage effects Drain effects can bring stats below zero + adjustDynamicStat(target, index, -effect.mMagnitude, true); + if (index == 0) + receivedMagicDamage = affectedHealth = true; + } + break; + case ESM::MagicEffect::FortifyHealth: + case ESM::MagicEffect::FortifyMagicka: + case ESM::MagicEffect::FortifyFatigue: + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) + modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude); + else + adjustDynamicStat( + target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude, false, true); + break; + case ESM::MagicEffect::DrainAttribute: + if (!godmode) + damageAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyAttribute: + // Abilities affect base stats, but not for drain + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attribute = effect.getSkillOrAttribute(); + AttributeValue attr = creatureStats.getAttribute(attribute); + attr.setBase(attr.getBase() + effect.mMagnitude); + creatureStats.setAttribute(attribute, attr); + } + else + fortifyAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::DrainSkill: + if (!target.getClass().isNpc()) + invalid = true; + else if (!godmode) + damageSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifySkill: + if (!target.getClass().isNpc()) + invalid = true; + else if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) + { + // Abilities affect base stats, but not for drain + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.getSkillOrAttribute()); + skill.setBase(skill.getBase() + effect.mMagnitude); + } + else + fortifySkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyMaximumMagicka: + recalculateMagicka = true; + break; + case ESM::MagicEffect::AbsorbHealth: + case ESM::MagicEffect::AbsorbMagicka: + case ESM::MagicEffect::AbsorbFatigue: + if (!godmode) + { + int index = effect.mEffectId - ESM::MagicEffect::AbsorbHealth; + adjustDynamicStat(target, index, -effect.mMagnitude); + if (!caster.isEmpty()) + adjustDynamicStat(caster, index, effect.mMagnitude); + if (index == 0) + receivedMagicDamage = affectedHealth = true; + } + break; + case ESM::MagicEffect::AbsorbAttribute: + if (!godmode) + { + damageAttribute(target, effect, effect.mMagnitude); + if (!caster.isEmpty()) + fortifyAttribute(caster, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::AbsorbSkill: + if (!target.getClass().isNpc()) + invalid = true; + else if (!godmode) + { + damageSkill(target, effect, effect.mMagnitude); + if (!caster.isEmpty()) + fortifySkill(caster, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::DisintegrateArmor: + { + if (!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + if (godmode) + break; + static const std::array priorities{ + MWWorld::InventoryStore::Slot_CarriedLeft, + MWWorld::InventoryStore::Slot_Cuirass, + MWWorld::InventoryStore::Slot_LeftPauldron, + MWWorld::InventoryStore::Slot_RightPauldron, + MWWorld::InventoryStore::Slot_LeftGauntlet, + MWWorld::InventoryStore::Slot_RightGauntlet, + MWWorld::InventoryStore::Slot_Helmet, + MWWorld::InventoryStore::Slot_Greaves, + MWWorld::InventoryStore::Slot_Boots, + }; + for (const int priority : priorities) + { + if (disintegrateSlot(target, priority, effect.mMagnitude)) + break; + } + break; + } + case ESM::MagicEffect::DisintegrateWeapon: + if (!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + if (!godmode) + disintegrateSlot(target, MWWorld::InventoryStore::Slot_CarriedRight, effect.mMagnitude); + break; + } + } + + bool shouldRemoveEffect(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect) + { + const auto world = MWBase::Environment::get().getWorld(); + switch (effect.mEffectId) + { + case ESM::MagicEffect::Levitate: + { + if (!world->isLevitationEnabled()) + { + if (target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}"); + return true; + } + break; + } + case ESM::MagicEffect::Recall: + case ESM::MagicEffect::DivineIntervention: + case ESM::MagicEffect::AlmsiviIntervention: + { + return effect.mFlags & ESM::ActiveEffect::Flag_Applied; + } + case ESM::MagicEffect::WaterWalking: + { + if (target.getClass().isPureWaterCreature(target) && world->isSwimming(target)) + return true; + if (effect.mFlags & ESM::ActiveEffect::Flag_Applied) + break; + if (!world->isWaterWalkingCastableOnTarget(target)) + { + if (target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidEffect}"); + return true; + } + break; + } + } + return false; + } + + MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, + ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt) + { + const auto world = MWBase::Environment::get().getWorld(); + bool invalid = false; + bool receivedMagicDamage = false; + bool recalculateMagicka = false; + bool affectedHealth = false; + if (effect.mEffectId == ESM::MagicEffect::Corprus && spellParams.shouldWorsen()) + { + spellParams.worsen(); + for (auto& otherEffect : spellParams.getEffects()) + { + if (isCorprusEffect(otherEffect)) + applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage, + affectedHealth, recalculateMagicka); + } + if (target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); + return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; + } + else if (shouldRemoveEffect(target, effect)) + { + onMagicEffectRemoved(target, spellParams, effect); + return { MagicApplicationResult::Type::REMOVED, receivedMagicDamage, affectedHealth }; + } + const auto* magicEffect = world->getStore().get().find(effect.mEffectId); + if (effect.mFlags & ESM::ActiveEffect::Flag_Applied) + { + if (magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) + { + effect.mTimeLeft -= dt; + return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; + } + else if (!dt) + return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; + } + if (effect.mEffectId == ESM::MagicEffect::Lock) + { + if (target.getClass().canLock(target)) + { + MWRender::Animation* animation = world->getAnimation(target); + if (animation) + animation->addSpellCastGlow(magicEffect->getColor()); + int magnitude = static_cast(roll(effect)); + if (target.getCellRef().getLockLevel() + < magnitude) // If the door is not already locked to a higher value, lock it to spell magnitude + { + MWBase::Environment::get().getSoundManager()->playSound3D( + target, ESM::RefId::stringRefId("Open Lock"), 1.f, 1.f); + + if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); + target.getCellRef().lock(magnitude); + } + } + else + invalid = true; + } + else if (effect.mEffectId == ESM::MagicEffect::Open) + { + if (target.getClass().canLock(target)) + { + // Use the player instead of the caster for vanilla crime compatibility + MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target); + + MWRender::Animation* animation = world->getAnimation(target); + if (animation) + animation->addSpellCastGlow(magicEffect->getColor()); + int magnitude = static_cast(roll(effect)); + if (target.getCellRef().getLockLevel() <= magnitude) + { + if (target.getCellRef().isLocked()) + { + MWBase::Environment::get().getSoundManager()->playSound3D( + target, ESM::RefId::stringRefId("Open Lock"), 1.f, 1.f); + + if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); + target.getCellRef().unlock(); + } + } + else + { + MWBase::Environment::get().getSoundManager()->playSound3D( + target, ESM::RefId::stringRefId("Open Lock Fail"), 1.f, 1.f); + } + } + else + invalid = true; + } + else if (!target.getClass().isActor()) + { + invalid = true; + } + else + { + // Morrowind.exe doesn't apply magic effects while the menu is open, we do because we like to see stats + // updated instantly. We don't want to teleport instantly though + if (!dt + && (effect.mEffectId == ESM::MagicEffect::Recall + || effect.mEffectId == ESM::MagicEffect::DivineIntervention + || effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention)) + return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; + auto& stats = target.getClass().getCreatureStats(target); + auto& magnitudes = stats.getMagicEffects(); + if (!spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues) + && !(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + { + MagicApplicationResult::Type result + = applyProtections(target, caster, spellParams, effect, magicEffect); + if (result != MagicApplicationResult::Type::APPLIED) + return { result, receivedMagicDamage, affectedHealth }; + } + float oldMagnitude = 0.f; + if (effect.mFlags & ESM::ActiveEffect::Flag_Applied) + 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)); + if (effect.mEffectId == ESM::MagicEffect::Soultrap && !target.getClass().isNpc() + && target.getType() == ESM::Creature::sRecordId + && target.get()->mBase->mData.mSoul == 0 && caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}"); + } + float magnitude = roll(effect); + // Note that there's an early out for Flag_Applied AppliedOnce effects so we don't have to exclude them here + effect.mMagnitude = magnitude; + if (!(magicEffect->mData.mFlags + & (ESM::MagicEffect::Flags::NoMagnitude | ESM::MagicEffect::Flags::AppliedOnce))) + { + if (effect.mDuration != 0) + { + float mult = dt; + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary)) + mult = std::min(effect.mTimeLeft, dt); + effect.mMagnitude *= mult; + } + if (effect.mMagnitude == 0) + { + effect.mMagnitude = oldMagnitude; + effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; + effect.mTimeLeft -= dt; + return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; + } + } + if (effect.mEffectId == ESM::MagicEffect::Corprus) + spellParams.worsen(); + else + applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage, affectedHealth, + recalculateMagicka); + effect.mMagnitude = magnitude; + magnitudes.add(EffectKey(effect.mEffectId, effect.getSkillOrAttribute()), + EffectParam(effect.mMagnitude - oldMagnitude)); + } + effect.mTimeLeft -= dt; + if (invalid) + { + effect.mTimeLeft = 0; + effect.mFlags |= ESM::ActiveEffect::Flag_Remove; + auto anim = world->getAnimation(target); + if (anim) + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); + } + else + effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; + if (recalculateMagicka) + target.getClass().getCreatureStats(target).recalculateMagicka(); + return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; + } + + void removeMagicEffect( + const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) + { + const auto world = MWBase::Environment::get().getWorld(); + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + bool invalid; + switch (effect.mEffectId) + { + case ESM::MagicEffect::CommandCreature: + case ESM::MagicEffect::CommandHumanoid: + if (magnitudes.getOrDefault(effect.mEffectId).getMagnitude() <= 0.f) + { + auto& seq = target.getClass().getCreatureStats(target).getAiSequence(); + seq.erasePackageIf([&](const auto& package) { + return package->getTypeId() == MWMechanics::AiPackageTypeId::Follow + && static_cast(package.get())->isCommanded(); + }); + } + break; + case ESM::MagicEffect::ExtraSpell: + if (magnitudes.getOrDefault(effect.mEffectId).getMagnitude() <= 0.f) + target.getClass().getInventoryStore(target).autoEquip(); + break; + case ESM::MagicEffect::TurnUndead: + { + auto& creatureStats = target.getClass().getCreatureStats(target); + Stat stat = creatureStats.getAiSetting(AiSetting::Flee); + stat.setModifier(static_cast(stat.getModifier() - effect.mMagnitude)); + creatureStats.setAiSetting(AiSetting::Flee, stat); + } + break; + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::FrenzyHumanoid: + modifyAiSetting( + target, effect, ESM::MagicEffect::FrenzyCreature, AiSetting::Fight, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::CalmHumanoid: + modifyAiSetting( + target, effect, ESM::MagicEffect::CalmCreature, AiSetting::Fight, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + modifyAiSetting( + target, effect, ESM::MagicEffect::DemoralizeCreature, AiSetting::Flee, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::NightEye: + { + const MWMechanics::EffectParam nightEye = magnitudes.getOrDefault(effect.mEffectId); + if (nightEye.getMagnitude() < 0.f && nightEye.getBase() < 0) + { + // The PCVisionBonus functions are different from every other magic effect function in that they + // clamp the value to [0, 1]. Morrowind.exe applies the same clamping to the night-eye effect, which + // can create situations where an effect is still active (i.e. shown in the menu) but the screen is + // no longer bright. Modifying the base value here should prevent that while preserving their + // function. + float delta = std::clamp(-nightEye.getMagnitude(), 0.f, -static_cast(nightEye.getBase())); + magnitudes.modifyBase(effect.mEffectId, static_cast(delta)); + } + } + break; + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::RallyHumanoid: + modifyAiSetting( + target, effect, ESM::MagicEffect::RallyCreature, AiSetting::Flee, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::Sound: + if (magnitudes.getOrDefault(effect.mEffectId).getModifier() <= 0.f && target == getPlayer()) + MWBase::Environment::get().getSoundManager()->stopSound3D( + target, ESM::RefId::stringRefId("magic sound")); + break; + case ESM::MagicEffect::SummonScamp: + case ESM::MagicEffect::SummonClannfear: + case ESM::MagicEffect::SummonDaedroth: + case ESM::MagicEffect::SummonDremora: + case ESM::MagicEffect::SummonAncestralGhost: + case ESM::MagicEffect::SummonSkeletalMinion: + case ESM::MagicEffect::SummonBonewalker: + case ESM::MagicEffect::SummonGreaterBonewalker: + case ESM::MagicEffect::SummonBonelord: + case ESM::MagicEffect::SummonWingedTwilight: + case ESM::MagicEffect::SummonHunger: + case ESM::MagicEffect::SummonGoldenSaint: + case ESM::MagicEffect::SummonFlameAtronach: + case ESM::MagicEffect::SummonFrostAtronach: + case ESM::MagicEffect::SummonStormAtronach: + case ESM::MagicEffect::SummonCenturionSphere: + case ESM::MagicEffect::SummonFabricant: + case ESM::MagicEffect::SummonWolf: + case ESM::MagicEffect::SummonBear: + case ESM::MagicEffect::SummonBonewolf: + case ESM::MagicEffect::SummonCreature04: + case ESM::MagicEffect::SummonCreature05: + { + int actorId = effect.getActorId(); + if (actorId != -1) + MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, actorId); + auto& summons = target.getClass().getCreatureStats(target).getSummonedCreatureMap(); + auto [begin, end] = summons.equal_range(effect.mEffectId); + for (auto it = begin; it != end; ++it) + { + if (it->second == actorId) + { + summons.erase(it); + break; + } + } + } + break; + case ESM::MagicEffect::BoundGloves: + removeBoundItem(ESM::RefId::stringRefId(world->getStore() + .get() + .find("sMagicBoundRightGauntletID") + ->mValue.getString()), + target); + [[fallthrough]]; + case ESM::MagicEffect::BoundDagger: + case ESM::MagicEffect::BoundLongsword: + case ESM::MagicEffect::BoundMace: + case ESM::MagicEffect::BoundBattleAxe: + case ESM::MagicEffect::BoundSpear: + case ESM::MagicEffect::BoundLongbow: + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundHelm: + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundShield: + { + const std::string& item = sBoundItemsMap.at(effect.mEffectId); + removeBoundItem( + ESM::RefId::stringRefId(world->getStore().get().find(item)->mValue.getString()), + target); + } + break; + case ESM::MagicEffect::DrainHealth: + case ESM::MagicEffect::DrainMagicka: + case ESM::MagicEffect::DrainFatigue: + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::DrainHealth, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyHealth: + case ESM::MagicEffect::FortifyMagicka: + case ESM::MagicEffect::FortifyFatigue: + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) + modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude); + else + adjustDynamicStat( + target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude, true); + break; + case ESM::MagicEffect::DrainAttribute: + restoreAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyAttribute: + // Abilities affect base stats, but not for drain + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attribute = effect.getSkillOrAttribute(); + AttributeValue attr = creatureStats.getAttribute(attribute); + attr.setBase(attr.getBase() - effect.mMagnitude); + creatureStats.setAttribute(attribute, attr); + } + else + fortifyAttribute(target, effect, -effect.mMagnitude); + break; + case ESM::MagicEffect::DrainSkill: + restoreSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifySkill: + // Abilities affect base stats, but not for drain + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.getSkillOrAttribute()); + skill.setBase(skill.getBase() - effect.mMagnitude); + } + else + fortifySkill(target, effect, -effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyMaximumMagicka: + target.getClass().getCreatureStats(target).recalculateMagicka(); + break; + case ESM::MagicEffect::AbsorbAttribute: + { + const auto caster = world->searchPtrViaActorId(spellParams.getCasterActorId()); + restoreAttribute(target, effect, effect.mMagnitude); + if (!caster.isEmpty()) + fortifyAttribute(caster, effect, -effect.mMagnitude); + } + break; + case ESM::MagicEffect::AbsorbSkill: + { + const auto caster = world->searchPtrViaActorId(spellParams.getCasterActorId()); + restoreSkill(target, effect, effect.mMagnitude); + if (!caster.isEmpty()) + fortifySkill(caster, effect, -effect.mMagnitude); + } + break; + case ESM::MagicEffect::Corprus: + { + int worsenings = spellParams.getWorsenings(); + spellParams.resetWorsenings(); + if (worsenings > 0) + { + for (const auto& otherEffect : spellParams.getEffects()) + { + if (isCorprusEffect(otherEffect, true)) + { + for (int i = 0; i < worsenings; i++) + removeMagicEffect(target, spellParams, otherEffect); + } + } + } + // Note that we remove the effects, but keep the params + target.getClass().getCreatureStats(target).getActiveSpells().purge( + [&spellParams]( + const ActiveSpells::ActiveSpellParams& params, const auto&) { return &spellParams == ¶ms; }, + target); + } + break; + } + } + + void onMagicEffectRemoved( + const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) + { + if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + return; + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + magnitudes.add(EffectKey(effect.mEffectId, effect.getSkillOrAttribute()), EffectParam(-effect.mMagnitude)); + removeMagicEffect(target, spellParams, effect); + if (magnitudes.getOrDefault(effect.mEffectId).getMagnitude() <= 0.f) + { + auto anim = MWBase::Environment::get().getWorld()->getAnimation(target); + if (anim) + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); + } + } + +} diff --git a/apps/openmw/mwmechanics/spelleffects.hpp b/apps/openmw/mwmechanics/spelleffects.hpp new file mode 100644 index 00000000000..2dafedf31f1 --- /dev/null +++ b/apps/openmw/mwmechanics/spelleffects.hpp @@ -0,0 +1,38 @@ +#ifndef GAME_MWMECHANICS_SPELLEFFECTS_H +#define GAME_MWMECHANICS_SPELLEFFECTS_H + +#include "activespells.hpp" + +// These functions should probably be split up into separate Lua functions for each magic effect when magic is +// dehardcoded. That way ESM::MGEF could point to two Lua scripts for each effect. Needs discussion. + +namespace MWWorld +{ + class Ptr; +} + +namespace MWMechanics +{ + struct MagicApplicationResult + { + enum class Type + { + APPLIED, + REMOVED, + REFLECTED + }; + Type mType; + bool mShowHit; + bool mShowHealth; + }; + + // 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); + + // Undoes permanent effects created by ESM::MagicEffect::AppliedOnce + void onMagicEffectRemoved( + const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spell, const ESM::ActiveEffect& effect); +} + +#endif diff --git a/apps/openmw/mwmechanics/spelllist.cpp b/apps/openmw/mwmechanics/spelllist.cpp index 9328d533e36..d7ad7c3af12 100644 --- a/apps/openmw/mwmechanics/spelllist.cpp +++ b/apps/openmw/mwmechanics/spelllist.cpp @@ -2,97 +2,100 @@ #include -#include +#include +#include +#include #include "spells.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" namespace { - template - const std::vector getSpellList(const std::string& id) + template + const std::vector getSpellList(const ESM::RefId& id) { - return MWBase::Environment::get().getWorld()->getStore().get().find(id)->mSpells.mList; + return MWBase::Environment::get().getESMStore()->get().find(id)->mSpells.mList; } - template - bool withBaseRecord(const std::string& id, const std::function&)>& function) + template + bool withBaseRecord(const ESM::RefId& id, const std::function&)>& function) { - T copy = *MWBase::Environment::get().getWorld()->getStore().get().find(id); + T copy = *MWBase::Environment::get().getESMStore()->get().find(id); bool changed = function(copy.mSpells.mList); - if(changed) - MWBase::Environment::get().getWorld()->createOverrideRecord(copy); + if (changed) + MWBase::Environment::get().getESMStore()->overrideRecord(copy); return changed; } } namespace MWMechanics { - SpellList::SpellList(const std::string& id, int type) : mId(id), mType(type) {} + SpellList::SpellList(const ESM::RefId& id, int type) + : mId(id) + , mType(type) + { + } - bool SpellList::withBaseRecord(const std::function&)>& function) + bool SpellList::withBaseRecord(const std::function&)>& function) { - switch(mType) + switch (mType) { case ESM::REC_CREA: return ::withBaseRecord(mId, function); case ESM::REC_NPC_: return ::withBaseRecord(mId, function); default: - throw std::logic_error("failed to update base record for " + mId); + throw std::logic_error("failed to update base record for " + mId.toDebugString()); } } - const std::vector SpellList::getSpells() const + const std::vector SpellList::getSpells() const { - switch(mType) + switch (mType) { case ESM::REC_CREA: return getSpellList(mId); case ESM::REC_NPC_: return getSpellList(mId); default: - throw std::logic_error("failed to get spell list for " + mId); + throw std::logic_error("failed to get spell list for " + mId.toDebugString()); } } - const ESM::Spell* SpellList::getSpell(const std::string& id) + const ESM::Spell* SpellList::getSpell(const ESM::RefId& id) { - return MWBase::Environment::get().getWorld()->getStore().get().find(id); + return MWBase::Environment::get().getESMStore()->get().find(id); } - void SpellList::add (const ESM::Spell* spell) + void SpellList::add(const ESM::Spell* spell) { auto& id = spell->mId; - bool changed = withBaseRecord([&] (auto& spells) - { - for(const auto& it : spells) + bool changed = withBaseRecord([&](auto& spells) { + for (const auto& it : spells) { - if(Misc::StringUtils::ciEqual(id, it)) + if (id == it) return false; } spells.push_back(id); return true; }); - if(changed) + if (changed) { - for(auto listener : mListeners) + for (auto listener : mListeners) listener->addSpell(spell); } } - void SpellList::remove (const ESM::Spell* spell) + void SpellList::remove(const ESM::Spell* spell) { auto& id = spell->mId; - bool changed = withBaseRecord([&] (auto& spells) - { - for(auto it = spells.begin(); it != spells.end(); it++) + bool changed = withBaseRecord([&](auto& spells) { + for (auto it = spells.begin(); it != spells.end(); it++) { - if(Misc::StringUtils::ciEqual(id, *it)) + if (id == *it) { spells.erase(it); return true; @@ -100,20 +103,18 @@ namespace MWMechanics } return false; }); - if(changed) + if (changed) { - for(auto listener : mListeners) + for (auto listener : mListeners) listener->removeSpell(spell); } } - void SpellList::removeAll (const std::vector& ids) + void SpellList::removeAll(const std::vector& ids) { - bool changed = withBaseRecord([&] (auto& spells) - { - const auto it = std::remove_if(spells.begin(), spells.end(), [&] (const auto& spell) - { - const auto isSpell = [&] (const auto& id) { return Misc::StringUtils::ciEqual(spell, id); }; + bool changed = withBaseRecord([&](auto& spells) { + const auto it = std::remove_if(spells.begin(), spells.end(), [&](const auto& spell) { + const auto isSpell = [&](const auto& id) { return spell == id; }; return ids.end() != std::find_if(ids.begin(), ids.end(), isSpell); }); if (it == spells.end()) @@ -121,11 +122,11 @@ namespace MWMechanics spells.erase(it, spells.end()); return true; }); - if(changed) + if (changed) { - for(auto listener : mListeners) + for (auto listener : mListeners) { - for(auto& id : ids) + for (auto& id : ids) { const auto spell = getSpell(id); listener->removeSpell(spell); @@ -136,16 +137,15 @@ namespace MWMechanics void SpellList::clear() { - bool changed = withBaseRecord([] (auto& spells) - { - if(spells.empty()) + bool changed = withBaseRecord([](auto& spells) { + if (spells.empty()) return false; spells.clear(); return true; }); - if(changed) + if (changed) { - for(auto listener : mListeners) + for (auto listener : mListeners) listener->removeAllSpells(); } } diff --git a/apps/openmw/mwmechanics/spelllist.hpp b/apps/openmw/mwmechanics/spelllist.hpp index 5920949d65c..49e40f0e795 100644 --- a/apps/openmw/mwmechanics/spelllist.hpp +++ b/apps/openmw/mwmechanics/spelllist.hpp @@ -3,11 +3,11 @@ #include #include -#include #include +#include #include -#include +#include namespace ESM { @@ -16,52 +16,49 @@ namespace ESM namespace MWMechanics { - struct SpellParams - { - std::map mEffectRands; // - std::set mPurgedEffects; // indices of purged effects - }; - class Spells; /// Multiple instances of the same actor share the same spell list in Morrowind. /// The most obvious result of this is that adding a spell or ability to one instance adds it to all instances. - /// @note The original game will only update visual effects associated with any added abilities for the originally targeted actor, + /// @note The original game will only update visual effects associated with any added abilities for the originally + /// targeted actor, /// changing cells applies the update to all actors. - /// Aside from sharing the same active spell list, changes made to this list are also written to the actor's base record. - /// Interestingly, it is not just scripted changes that are persisted to the base record. Curing one instance's disease will cure all instances. + /// Aside from sharing the same active spell list, changes made to this list are also written to the actor's base + /// record. Interestingly, it is not just scripted changes that are persisted to the base record. Curing one + /// instance's disease will cure all instances. /// @note The original game is inconsistent in persisting this example; /// saving and loading the game might reapply the cured disease depending on which instance was cured. class SpellList { - const std::string mId; - const int mType; - std::vector mListeners; + ESM::RefId mId; + const int mType; + std::vector mListeners; + + bool withBaseRecord(const std::function&)>& function); - bool withBaseRecord(const std::function&)>& function); - public: - SpellList(const std::string& id, int type); + public: + SpellList(const ESM::RefId& id, int type); - /// Get spell from ID, throws exception if not found - static const ESM::Spell* getSpell(const std::string& id); + /// Get spell from ID, throws exception if not found + static const ESM::Spell* getSpell(const ESM::RefId& id); - void add (const ESM::Spell* spell); - ///< Adding a spell that is already listed in *this is a no-op. + void add(const ESM::Spell* spell); + ///< Adding a spell that is already listed in *this is a no-op. - void remove (const ESM::Spell* spell); + void remove(const ESM::Spell* spell); - void removeAll(const std::vector& spells); + void removeAll(const std::vector& spells); - void clear(); - ///< Remove all spells of all types. + void clear(); + ///< Remove all spells of all types. - void addListener(Spells* spells); + void addListener(Spells* spells); - void removeListener(Spells* spells); + void removeListener(Spells* spells); - void updateListener(Spells* before, Spells* after); + void updateListener(Spells* before, Spells* after); - const std::vector getSpells() const; + const std::vector getSpells() const; }; } diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp index bd9c5f7cb3e..7041254c4e9 100644 --- a/apps/openmw/mwmechanics/spellpriority.cpp +++ b/apps/openmw/mwmechanics/spellpriority.cpp @@ -1,51 +1,54 @@ #include "spellpriority.hpp" #include "weaponpriority.hpp" -#include -#include -#include +#include + +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" -#include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" -#include "../mwworld/cellstore.hpp" #include "creaturestats.hpp" #include "spellresistance.hpp" -#include "weapontype.hpp" -#include "summoning.hpp" #include "spellutil.hpp" +#include "summoning.hpp" +#include "weapontype.hpp" namespace { - int numEffectsToDispel (const MWWorld::Ptr& actor, int effectFilter=-1, bool negative = true) + int numEffectsToDispel(const MWWorld::Ptr& actor, int effectFilter = -1, bool negative = true) { - int toCure=0; + int toCure = 0; const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) { - // if the effect filter is not specified, take in account only spells effects. Leave potions, enchanted items etc. + // if the effect filter is not specified, take in account only spells effects. Leave potions, enchanted + // items etc. if (effectFilter == -1) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell + = MWBase::Environment::get().getESMStore()->get().search(it->getSourceSpellId()); if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) continue; } - const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; - for (std::vector::const_iterator effectIt = params.mEffects.begin(); - effectIt != params.mEffects.end(); ++effectIt) + const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; + for (const auto& effect : params.getEffects()) { - int effectId = effectIt->mEffectId; + int effectId = effect.mEffectId; if (effectFilter != -1 && effectId != effectFilter) continue; - const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); + const ESM::MagicEffect* magicEffect + = MWBase::Environment::get().getESMStore()->get().find(effectId); - if (effectIt->mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway + if (effect.mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway continue; if (negative && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) @@ -58,58 +61,79 @@ namespace return toCure; } - float getSpellDuration (const MWWorld::Ptr& actor, const std::string& spellId) + float getSpellDuration(const MWWorld::Ptr& actor, const ESM::RefId& spellId) { float duration = 0; const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) { - if (it->first != spellId) + if (it->getSourceSpellId() != spellId) continue; - const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; - for (std::vector::const_iterator effectIt = params.mEffects.begin(); - effectIt != params.mEffects.end(); ++effectIt) + const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; + for (const auto& effect : params.getEffects()) { - if (effectIt->mDuration > duration) - duration = effectIt->mDuration; + if (effect.mDuration > duration) + duration = effect.mDuration; } } return duration; } + + bool isSpellActive(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const ESM::RefId& id) + { + int actorId = caster.getClass().getCreatureStats(caster).getActorId(); + const auto& active = target.getClass().getCreatureStats(target).getActiveSpells(); + return std::find_if(active.begin(), active.end(), [&](const auto& spell) { + return spell.getCasterActorId() == actorId && spell.getSourceSpellId() == id; + }) != active.end(); + } + + float getRestoreMagickaPriority(const MWWorld::Ptr& actor) + { + const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + const MWMechanics::DynamicStat& current = stats.getMagicka(); + for (const ESM::Spell* spell : stats.getSpells()) + { + if (spell->mData.mType != ESM::Spell::ST_Spell) + continue; + int cost = MWMechanics::calcSpellCost(*spell); + if (cost > current.getCurrent() && cost < current.getModified()) + return 2.f; + } + return 0.f; + } } namespace MWMechanics { - int getRangeTypes (const ESM::EffectList& effects) + int getRangeTypes(const ESM::EffectList& effects) { int types = 0; - for (std::vector::const_iterator it = effects.mList.begin(); it != effects.mList.end(); ++it) + for (const ESM::IndexedENAMstruct& effect : effects.mList) { - if (it->mRange == ESM::RT_Self) + if (effect.mData.mRange == ESM::RT_Self) types |= RangeTypes::Self; - else if (it->mRange == ESM::RT_Touch) + else if (effect.mData.mRange == ESM::RT_Touch) types |= RangeTypes::Touch; - else if (it->mRange == ESM::RT_Target) + else if (effect.mData.mRange == ESM::RT_Target) types |= RangeTypes::Target; } return types; } - float ratePotion (const MWWorld::Ptr &item, const MWWorld::Ptr& actor) + float ratePotion(const MWWorld::Ptr& item, const MWWorld::Ptr& actor) { - if (item.getTypeName() != typeid(ESM::Potion).name()) + if (item.getType() != ESM::Potion::sRecordId) return 0.f; const ESM::Potion* potion = item.get()->mBase; return rateEffects(potion->mEffects, actor, MWWorld::Ptr()); } - float rateSpell(const ESM::Spell *spell, const MWWorld::Ptr &actor, const MWWorld::Ptr& enemy) + float rateSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool checkMagicka) { - const CreatureStats& stats = actor.getClass().getCreatureStats(actor); - - float successChance = MWMechanics::getSpellSuccessChance(spell, actor); + float successChance = MWMechanics::getSpellSuccessChance(spell, actor, nullptr, true, checkMagicka); if (successChance == 0.f) return 0.f; @@ -119,35 +143,37 @@ namespace MWMechanics // Don't make use of racial bonus spells, like MW. Can be made optional later if (actor.getClass().isNpc()) { - std::string raceid = actor.get()->mBase->mRace; - const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(raceid); + const ESM::RefId& raceid = actor.get()->mBase->mRace; + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(raceid); if (race->mPowers.exists(spell->mId)) return 0.f; } // Spells don't stack, so early out if the spell is still active on the target int types = getRangeTypes(spell->mEffects); - if ((types & Self) && stats.getActiveSpells().isSpellActive(spell->mId)) + if ((types & Self) && isSpellActive(actor, actor, spell->mId)) return 0.f; - if ( ((types & Touch) || (types & Target)) && enemy.getClass().getCreatureStats(enemy).getActiveSpells().isSpellActive(spell->mId)) + if (((types & Touch) || (types & Target)) && !enemy.isEmpty() && isSpellActive(actor, enemy, spell->mId)) return 0.f; return rateEffects(spell->mEffects, actor, enemy) * (successChance / 100.f); } - float rateMagicItem(const MWWorld::Ptr &ptr, const MWWorld::Ptr &actor, const MWWorld::Ptr& enemy) + float rateMagicItem(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { if (ptr.getClass().getEnchantment(ptr).empty()) return 0.f; - const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(ptr.getClass().getEnchantment(ptr)); + const ESM::Enchantment* enchantment = MWBase::Environment::get().getESMStore()->get().find( + ptr.getClass().getEnchantment(ptr)); // Spells don't stack, so early out if the spell is still active on the target int types = getRangeTypes(enchantment->mEffects); - if ((types & Self) && actor.getClass().getCreatureStats(actor).getActiveSpells().isSpellActive(ptr.getCellRef().getRefId())) + if ((types & Self) + && actor.getClass().getCreatureStats(actor).getActiveSpells().isSpellActive(ptr.getCellRef().getRefId())) return 0.f; - if (types & (Touch|Target) && getSpellDuration(enemy, ptr.getCellRef().getRefId()) > 3) + if (types & (Touch | Target) && getSpellDuration(enemy, ptr.getCellRef().getRefId()) > 3) return 0.f; if (enchantment->mData.mType == ESM::Enchantment::CastOnce) @@ -158,80 +184,79 @@ namespace MWMechanics { MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - // Creatures can not wear armor/clothing, so allow creatures to use non-equipped items, + // Creatures can not wear armor/clothing, so allow creatures to use non-equipped items, if (actor.getClass().isNpc() && !store.isEquipped(ptr)) return 0.f; - int castCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), actor); + int castCost = getEffectiveEnchantmentCastCost(*enchantment, actor); - if (ptr.getCellRef().getEnchantmentCharge() != -1 - && ptr.getCellRef().getEnchantmentCharge() < castCost) + if (ptr.getCellRef().getEnchantmentCharge() != -1 && ptr.getCellRef().getEnchantmentCharge() < castCost) return 0.f; float rating = rateEffects(enchantment->mEffects, actor, enemy); - rating *= 1.25f; // prefer rechargable magic items over spells + rating *= 1.25f; // prefer rechargeable magic items over spells return rating; } return 0.f; } - float rateEffect(const ESM::ENAMstruct &effect, const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy) + float rateEffect(const ESM::ENAMstruct& effect, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { // NOTE: enemy may be empty float rating = 1; switch (effect.mEffectID) { - case ESM::MagicEffect::Soultrap: - case ESM::MagicEffect::AlmsiviIntervention: - case ESM::MagicEffect::DivineIntervention: - case ESM::MagicEffect::CalmHumanoid: - case ESM::MagicEffect::CalmCreature: - case ESM::MagicEffect::FrenzyHumanoid: - case ESM::MagicEffect::FrenzyCreature: - case ESM::MagicEffect::DemoralizeHumanoid: - case ESM::MagicEffect::DemoralizeCreature: - case ESM::MagicEffect::RallyHumanoid: - case ESM::MagicEffect::RallyCreature: - case ESM::MagicEffect::Charm: - case ESM::MagicEffect::DetectAnimal: - case ESM::MagicEffect::DetectEnchantment: - case ESM::MagicEffect::DetectKey: - case ESM::MagicEffect::Telekinesis: - case ESM::MagicEffect::Mark: - case ESM::MagicEffect::Recall: - case ESM::MagicEffect::Jump: - case ESM::MagicEffect::WaterBreathing: - case ESM::MagicEffect::SwiftSwim: - case ESM::MagicEffect::WaterWalking: - case ESM::MagicEffect::SlowFall: - case ESM::MagicEffect::Light: - case ESM::MagicEffect::Lock: - case ESM::MagicEffect::Open: - case ESM::MagicEffect::TurnUndead: - case ESM::MagicEffect::WeaknessToCommonDisease: - case ESM::MagicEffect::WeaknessToBlightDisease: - case ESM::MagicEffect::WeaknessToCorprusDisease: - case ESM::MagicEffect::CureCommonDisease: - case ESM::MagicEffect::CureBlightDisease: - case ESM::MagicEffect::CureCorprusDisease: - case ESM::MagicEffect::ResistBlightDisease: - case ESM::MagicEffect::ResistCommonDisease: - case ESM::MagicEffect::ResistCorprusDisease: - case ESM::MagicEffect::Invisibility: - case ESM::MagicEffect::Chameleon: - case ESM::MagicEffect::NightEye: - case ESM::MagicEffect::Vampirism: - case ESM::MagicEffect::StuntedMagicka: - case ESM::MagicEffect::ExtraSpell: - case ESM::MagicEffect::RemoveCurse: - case ESM::MagicEffect::CommandCreature: - case ESM::MagicEffect::CommandHumanoid: - return 0.f; + case ESM::MagicEffect::Soultrap: + case ESM::MagicEffect::AlmsiviIntervention: + case ESM::MagicEffect::DivineIntervention: + case ESM::MagicEffect::CalmHumanoid: + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::FrenzyHumanoid: + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::RallyHumanoid: + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::Charm: + case ESM::MagicEffect::DetectAnimal: + case ESM::MagicEffect::DetectEnchantment: + case ESM::MagicEffect::DetectKey: + case ESM::MagicEffect::Telekinesis: + case ESM::MagicEffect::Mark: + case ESM::MagicEffect::Recall: + case ESM::MagicEffect::Jump: + case ESM::MagicEffect::WaterBreathing: + case ESM::MagicEffect::SwiftSwim: + case ESM::MagicEffect::WaterWalking: + case ESM::MagicEffect::SlowFall: + case ESM::MagicEffect::Light: + case ESM::MagicEffect::Lock: + case ESM::MagicEffect::Open: + case ESM::MagicEffect::TurnUndead: + case ESM::MagicEffect::WeaknessToCommonDisease: + case ESM::MagicEffect::WeaknessToBlightDisease: + case ESM::MagicEffect::WeaknessToCorprusDisease: + case ESM::MagicEffect::CureCommonDisease: + case ESM::MagicEffect::CureBlightDisease: + case ESM::MagicEffect::CureCorprusDisease: + case ESM::MagicEffect::ResistBlightDisease: + case ESM::MagicEffect::ResistCommonDisease: + case ESM::MagicEffect::ResistCorprusDisease: + case ESM::MagicEffect::Invisibility: + case ESM::MagicEffect::Chameleon: + case ESM::MagicEffect::NightEye: + case ESM::MagicEffect::Vampirism: + case ESM::MagicEffect::StuntedMagicka: + case ESM::MagicEffect::ExtraSpell: + case ESM::MagicEffect::RemoveCurse: + case ESM::MagicEffect::CommandCreature: + case ESM::MagicEffect::CommandHumanoid: + return 0.f; - case ESM::MagicEffect::Blind: + case ESM::MagicEffect::Blind: { if (enemy.isEmpty()) return 0.f; @@ -243,13 +268,13 @@ namespace MWMechanics return 0.f; // Enemy doesn't attack - if (stats.getDrawState() != MWMechanics::DrawState_Weapon) + if (stats.getDrawState() != MWMechanics::DrawState::Weapon) return 0.f; break; } - case ESM::MagicEffect::Sound: + case ESM::MagicEffect::Sound: { if (enemy.isEmpty()) return 0.f; @@ -257,20 +282,20 @@ namespace MWMechanics const CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); // Enemy can't cast spells - if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).getMagnitude() > 0) + if (stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Silence).getMagnitude() > 0) return 0.f; if (stats.isParalyzed() || stats.getKnockedDown()) return 0.f; // Enemy doesn't cast spells - if (stats.getDrawState() != MWMechanics::DrawState_Spell) + if (stats.getDrawState() != MWMechanics::DrawState::Spell) return 0.f; break; } - case ESM::MagicEffect::Silence: + case ESM::MagicEffect::Silence: { if (enemy.isEmpty()) return 0.f; @@ -282,38 +307,39 @@ namespace MWMechanics return 0.f; // Enemy doesn't cast spells - if (stats.getDrawState() != MWMechanics::DrawState_Spell) + if (stats.getDrawState() != MWMechanics::DrawState::Spell) return 0.f; break; } - case ESM::MagicEffect::RestoreAttribute: - return 0.f; // TODO: implement based on attribute damage - case ESM::MagicEffect::RestoreSkill: - return 0.f; // TODO: implement based on skill damage - - case ESM::MagicEffect::ResistFire: - case ESM::MagicEffect::ResistFrost: - case ESM::MagicEffect::ResistMagicka: - case ESM::MagicEffect::ResistNormalWeapons: - case ESM::MagicEffect::ResistParalysis: - case ESM::MagicEffect::ResistPoison: - case ESM::MagicEffect::ResistShock: - case ESM::MagicEffect::SpellAbsorption: - case ESM::MagicEffect::Reflect: - return 0.f; // probably useless since we don't know in advance what the enemy will cast - - // don't cast these for now as they would make the NPC cast the same effect over and over again, especially when they have potions - case ESM::MagicEffect::FortifyAttribute: - case ESM::MagicEffect::FortifyHealth: - case ESM::MagicEffect::FortifyMagicka: - case ESM::MagicEffect::FortifyFatigue: - case ESM::MagicEffect::FortifySkill: - case ESM::MagicEffect::FortifyMaximumMagicka: - case ESM::MagicEffect::FortifyAttack: - return 0.f; + case ESM::MagicEffect::RestoreAttribute: + return 0.f; // TODO: implement based on attribute damage + case ESM::MagicEffect::RestoreSkill: + return 0.f; // TODO: implement based on skill damage + + case ESM::MagicEffect::ResistFire: + case ESM::MagicEffect::ResistFrost: + case ESM::MagicEffect::ResistMagicka: + case ESM::MagicEffect::ResistNormalWeapons: + case ESM::MagicEffect::ResistParalysis: + case ESM::MagicEffect::ResistPoison: + case ESM::MagicEffect::ResistShock: + case ESM::MagicEffect::SpellAbsorption: + case ESM::MagicEffect::Reflect: + return 0.f; // probably useless since we don't know in advance what the enemy will cast + + // don't cast these for now as they would make the NPC cast the same effect over and over again, especially + // when they have potions + case ESM::MagicEffect::FortifyAttribute: + case ESM::MagicEffect::FortifyHealth: + case ESM::MagicEffect::FortifyMagicka: + case ESM::MagicEffect::FortifyFatigue: + case ESM::MagicEffect::FortifySkill: + case ESM::MagicEffect::FortifyMaximumMagicka: + case ESM::MagicEffect::FortifyAttack: + return 0.f; - case ESM::MagicEffect::Burden: + case ESM::MagicEffect::Burden: { if (enemy.isEmpty()) return 0.f; @@ -327,7 +353,7 @@ namespace MWMechanics if (burden > 0) return 0.f; - if ((effect.mMagnMin + effect.mMagnMax)/2.f > -burden) + if ((effect.mMagnMin + effect.mMagnMax) / 2.f > -burden) rating *= 3; else return 0.f; @@ -335,7 +361,7 @@ namespace MWMechanics break; } - case ESM::MagicEffect::Feather: + case ESM::MagicEffect::Feather: { // Ignore actors without inventory if (!actor.getClass().hasInventoryStore(actor)) @@ -346,7 +372,7 @@ namespace MWMechanics if (burden <= 0) return 0.f; - if ((effect.mMagnMin + effect.mMagnMax)/2.f >= burden) + if ((effect.mMagnMin + effect.mMagnMax) / 2.f >= burden) rating *= 3; else return 0.f; @@ -354,102 +380,130 @@ namespace MWMechanics break; } - case ESM::MagicEffect::Levitate: - return 0.f; // AI isn't designed to take advantage of this, and could be perceived as unfair anyway - case ESM::MagicEffect::BoundBoots: - case ESM::MagicEffect::BoundHelm: - if (actor.getClass().isNpc()) - { - // Beast races can't wear helmets or boots - std::string raceid = actor.get()->mBase->mRace; - const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(raceid); - if (race->mData.mFlags & ESM::Race::Beast) + case ESM::MagicEffect::Levitate: + return 0.f; // AI isn't designed to take advantage of this, and could be perceived as unfair anyway + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundHelm: + if (actor.getClass().isNpc()) + { + // Beast races can't wear helmets or boots + const ESM::RefId& raceid = actor.get()->mBase->mRace; + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(raceid); + if (race->mData.mFlags & ESM::Race::Beast) + return 0.f; + } + else return 0.f; - } - else - return 0.f; - - break; - // Creatures can not wear armor - case ESM::MagicEffect::BoundCuirass: - case ESM::MagicEffect::BoundGloves: - if (!actor.getClass().isNpc()) - return 0.f; - break; - case ESM::MagicEffect::BoundLongbow: - // AI should not summon the bow if there is no suitable ammo. - if (rateAmmo(actor, enemy, getWeaponType(ESM::Weapon::MarksmanBow)->mAmmoType) <= 0.f) - return 0.f; - break; + break; + case ESM::MagicEffect::BoundShield: + if (!actor.getClass().hasInventoryStore(actor)) + return 0.f; + else if (!actor.getClass().isNpc()) + { + // If the actor is an NPC they can benefit from the armor rating, otherwise check if we've got a + // one-handed weapon to use with the shield + const auto& store = actor.getClass().getInventoryStore(actor); + auto oneHanded = std::find_if(store.cbegin(MWWorld::ContainerStore::Type_Weapon), store.cend(), + [](const MWWorld::ConstPtr& weapon) { + if (weapon.getClass().getItemHealth(weapon) <= 0.f) + return false; + short type = weapon.get()->mBase->mData.mType; + return !(MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::TwoHanded); + }); + if (oneHanded == store.cend()) + return 0.f; + } + break; + // Creatures can not wear armor + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundGloves: + if (!actor.getClass().isNpc()) + return 0.f; + break; - case ESM::MagicEffect::RestoreHealth: - case ESM::MagicEffect::RestoreMagicka: - case ESM::MagicEffect::RestoreFatigue: - if (effect.mRange == ESM::RT_Self) - { - int priority = 1; - if (effect.mEffectID == ESM::MagicEffect::RestoreHealth) - priority = 10; - const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - const DynamicStat& current = stats.getDynamic(effect.mEffectID - ESM::MagicEffect::RestoreHealth); - // NB: this currently assumes the hardcoded magic effect flags are used - const float magnitude = (effect.mMagnMin + effect.mMagnMax)/2.f; - const float toHeal = magnitude * std::max(1, effect.mDuration); - // Effect doesn't heal more than we need, *or* we are below 1/2 health - if (current.getModified() - current.getCurrent() > toHeal - || current.getCurrent() < current.getModified()*0.5) + case ESM::MagicEffect::AbsorbMagicka: + if (!enemy.isEmpty() && enemy.getClass().getCreatureStats(enemy).getMagicka().getCurrent() <= 0.f) { - return 10000.f * priority - - (toHeal - (current.getModified()-current.getCurrent())); // prefer the most fitting potion + rating = 0.5f; + rating *= getRestoreMagickaPriority(actor); } - else - return -10000.f * priority; // Save for later - } - break; + break; + case ESM::MagicEffect::RestoreHealth: + case ESM::MagicEffect::RestoreMagicka: + case ESM::MagicEffect::RestoreFatigue: + if (effect.mRange == ESM::RT_Self) + { + const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + const DynamicStat& current + = stats.getDynamic(effect.mEffectID - ESM::MagicEffect::RestoreHealth); + // NB: this currently assumes the hardcoded magic effect flags are used + const float magnitude = (effect.mMagnMin + effect.mMagnMax) / 2.f; + const float toHeal = magnitude * std::max(1, effect.mDuration); + const float damage = std::max(current.getModified() - current.getCurrent(), 0.f); + float priority = 0.f; + if (effect.mEffectID == ESM::MagicEffect::RestoreHealth) + priority = 4.f; + else if (effect.mEffectID == ESM::MagicEffect::RestoreMagicka) + priority = getRestoreMagickaPriority(actor); + else if (effect.mEffectID == ESM::MagicEffect::RestoreFatigue) + priority = 2.f; + float overheal = 0.f; + float heal = toHeal; + if (damage < toHeal && current.getCurrent() > current.getModified() * 0.5) + { + overheal = toHeal - damage; + heal = damage; + } - case ESM::MagicEffect::Dispel: - { - int numPositive = 0; - int numNegative = 0; - int diff = 0; + priority = (priority - 1.f) / 2.f * std::pow((damage / current.getModified() + 0.6f), priority * 2) + + priority * (heal - 2.f * overheal) / current.getModified() - 0.5f; + rating = priority; + } + break; - if (effect.mRange == ESM::RT_Self) + case ESM::MagicEffect::Dispel: { - numPositive = numEffectsToDispel(actor, -1, false); - numNegative = numEffectsToDispel(actor); + int numPositive = 0; + int numNegative = 0; + int diff = 0; - diff = numNegative - numPositive; - } - else - { - if (enemy.isEmpty()) - return 0.f; + if (effect.mRange == ESM::RT_Self) + { + numPositive = numEffectsToDispel(actor, -1, false); + numNegative = numEffectsToDispel(actor); + + diff = numNegative - numPositive; + } + else + { + if (enemy.isEmpty()) + return 0.f; - numPositive = numEffectsToDispel(enemy, -1, false); - numNegative = numEffectsToDispel(enemy); + numPositive = numEffectsToDispel(enemy, -1, false); + numNegative = numEffectsToDispel(enemy); - diff = numPositive - numNegative; + diff = numPositive - numNegative; - // if rating < 0 here, the spell will be considered as negative later - rating *= -1; - } + // if rating < 0 here, the spell will be considered as negative later + rating *= -1; + } - if (diff <= 0) - return 0.f; + if (diff <= 0) + return 0.f; - rating *= (diff) / 5.f; + rating *= (diff) / 5.f; - break; - } + break; + } - // Prefer Cure effects over Dispel, because Dispel also removes positive effects - case ESM::MagicEffect::CureParalyzation: - return 1001.f * numEffectsToDispel(actor, ESM::MagicEffect::Paralyze); + // Prefer Cure effects over Dispel, because Dispel also removes positive effects + case ESM::MagicEffect::CureParalyzation: + return 1001.f * numEffectsToDispel(actor, ESM::MagicEffect::Paralyze); - case ESM::MagicEffect::CurePoison: - return 1001.f * numEffectsToDispel(actor, ESM::MagicEffect::Poison); - case ESM::MagicEffect::DisintegrateArmor: + case ESM::MagicEffect::CurePoison: + return 1001.f * numEffectsToDispel(actor, ESM::MagicEffect::Poison); + case ESM::MagicEffect::DisintegrateArmor: { if (enemy.isEmpty()) return 0.f; @@ -470,13 +524,13 @@ namespace MWMechanics MWWorld::InventoryStore::Slot_RightGauntlet, MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Greaves, - MWWorld::InventoryStore::Slot_Boots + MWWorld::InventoryStore::Slot_Boots, }; bool enemyHasArmor = false; // Ignore enemy without armor - for (unsigned int i=0; i= 0 && effect.mAttribute < ESM::Attribute::Length) + case ESM::MagicEffect::AbsorbAttribute: + case ESM::MagicEffect::DamageAttribute: + case ESM::MagicEffect::DrainAttribute: + if (!enemy.isEmpty() + && enemy.getClass() + .getCreatureStats(enemy) + .getAttribute(ESM::Attribute::indexToRefId(effect.mAttribute)) + .getModified() + <= 0) + return 0.f; { - const float attributePriorities[ESM::Attribute::Length] = { - 1.0f, // Strength - 0.5f, // Intelligence - 0.6f, // Willpower - 0.7f, // Agility - 0.5f, // Speed - 0.8f, // Endurance - 0.7f, // Personality - 0.3f // Luck - }; - rating *= attributePriorities[effect.mAttribute]; + if (effect.mAttribute >= 0 && effect.mAttribute < ESM::Attribute::Length) + { + const float attributePriorities[ESM::Attribute::Length] = { + 1.0f, // Strength + 0.5f, // Intelligence + 0.6f, // Willpower + 0.7f, // Agility + 0.5f, // Speed + 0.8f, // Endurance + 0.7f, // Personality + 0.3f // Luck + }; + rating *= attributePriorities[effect.mAttribute]; + } } - } - break; + break; - case ESM::MagicEffect::DamageSkill: - case ESM::MagicEffect::DrainSkill: - if (enemy.isEmpty() || !enemy.getClass().isNpc()) - return 0.f; - if (enemy.getClass().getSkill(enemy, effect.mSkill) <= 0) - return 0.f; - break; + case ESM::MagicEffect::AbsorbSkill: + case ESM::MagicEffect::DamageSkill: + case ESM::MagicEffect::DrainSkill: + if (enemy.isEmpty() || !enemy.getClass().isNpc()) + return 0.f; + if (enemy.getClass().getSkill(enemy, ESM::Skill::indexToRefId(effect.mSkill)) <= 0) + return 0.f; + break; - default: - break; + default: + break; } // Allow only one summoned creature at time @@ -553,6 +614,52 @@ namespace MWMechanics if (!creatureStats.getSummonedCreatureMap().empty()) return 0.f; + // But rate summons higher than other effects + rating = 3.f; + } + if (effect.mEffectID >= ESM::MagicEffect::BoundDagger && effect.mEffectID <= ESM::MagicEffect::BoundGloves) + { + // Prefer casting bound items over other spells + rating = 2.f; + // While rateSpell prevents actors from recasting the same spell, it doesn't prevent them from casting + // different spells with the same effect. Multiple instances of the same bound item don't stack so if the + // effect is already active, rate it as useless. Likewise, if the actor already has a bound weapon, don't + // summon another of a different kind unless what we have is a bow and the actor is out of ammo. + // FIXME: This code assumes the summoned item is of the usual type (i.e. a mod hasn't changed Bound Bow to + // summon an Axe instead) + if (effect.mEffectID <= ESM::MagicEffect::BoundLongbow) + { + for (int e = ESM::MagicEffect::BoundDagger; e <= ESM::MagicEffect::BoundLongbow; ++e) + if (actor.getClass().getCreatureStats(actor).getMagicEffects().getOrDefault(e).getMagnitude() > 0.f + && (e != ESM::MagicEffect::BoundLongbow || effect.mEffectID == e + || rateAmmo(actor, enemy, getWeaponType(ESM::Weapon::MarksmanBow)->mAmmoType) <= 0.f)) + return 0.f; + ESM::RefId skill = ESM::Skill::ShortBlade; + if (effect.mEffectID == ESM::MagicEffect::BoundLongsword) + skill = ESM::Skill::LongBlade; + else if (effect.mEffectID == ESM::MagicEffect::BoundMace) + skill = ESM::Skill::BluntWeapon; + else if (effect.mEffectID == ESM::MagicEffect::BoundBattleAxe) + skill = ESM::Skill::Axe; + else if (effect.mEffectID == ESM::MagicEffect::BoundSpear) + skill = ESM::Skill::Spear; + else if (effect.mEffectID == ESM::MagicEffect::BoundLongbow) + { + // AI should not summon the bow if there is no suitable ammo. + if (rateAmmo(actor, enemy, getWeaponType(ESM::Weapon::MarksmanBow)->mAmmoType) <= 0.f) + return 0.f; + skill = ESM::Skill::Marksman; + } + // Prefer summoning items we know how to use + rating *= (50.f + actor.getClass().getSkill(actor, skill)) / 100.f; + } + else if (actor.getClass() + .getCreatureStats(actor) + .getMagicEffects() + .getOrDefault(effect.mEffectID) + .getMagnitude() + > 0.f) + return 0.f; } // Underwater casting not possible @@ -568,7 +675,8 @@ namespace MWMechanics return 0.f; } - const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); + const ESM::MagicEffect* magicEffect + = MWBase::Environment::get().getESMStore()->get().find(effect.mEffectID); if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) { rating *= -1.f; @@ -592,14 +700,14 @@ namespace MWMechanics { CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); - if (stats.getMagicEffects().get(effect.mEffectID).getMagnitude() > 0) + if (stats.getMagicEffects().getOrDefault(effect.mEffectID).getMagnitude() > 0) return 0.f; } else { CreatureStats& stats = actor.getClass().getCreatureStats(actor); - if (stats.getMagicEffects().get(effect.mEffectID).getMagnitude() > 0) + if (stats.getMagicEffects().getOrDefault(effect.mEffectID).getMagnitude() > 0) return 0.f; } } @@ -614,39 +722,47 @@ namespace MWMechanics return rating; } - float rateEffects(const ESM::EffectList &list, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) + float rateEffects( + const ESM::EffectList& list, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool useSpellMult) { // NOTE: enemy may be empty float rating = 0.f; - float ratingMult = 1.f; // NB: this multiplier is applied to the effect rating, not the final rating - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat(); static const float fAIRangeMagicSpellMult = gmst.find("fAIRangeMagicSpellMult")->mValue.getFloat(); - for (std::vector::const_iterator it = list.mList.begin(); it != list.mList.end(); ++it) + for (const ESM::IndexedENAMstruct& effect : list.mList) { - ratingMult = (it->mRange == ESM::RT_Target) ? fAIRangeMagicSpellMult : fAIMagicSpellMult; - - rating += rateEffect(*it, actor, enemy) * ratingMult; + float effectRating = rateEffect(effect.mData, actor, enemy); + if (useSpellMult) + { + if (effect.mData.mRange == ESM::RT_Target) + effectRating *= fAIRangeMagicSpellMult; + else + effectRating *= fAIMagicSpellMult; + } + rating += effectRating; } return rating; } float vanillaRateSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat(); static const float fAIRangeMagicSpellMult = gmst.find("fAIRangeMagicSpellMult")->mValue.getFloat(); float mult = fAIMagicSpellMult; - for (std::vector::const_iterator effectIt = - spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt) + for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); + effectIt != spell->mEffects.mList.end(); ++effectIt) { - if (effectIt->mRange == ESM::RT_Target) + if (effectIt->mData.mRange == ESM::RT_Target) { if (!MWBase::Environment::get().getWorld()->isSwimming(enemy)) mult = fAIRangeMagicSpellMult; diff --git a/apps/openmw/mwmechanics/spellpriority.hpp b/apps/openmw/mwmechanics/spellpriority.hpp index 0305f24b5cf..e853e4fd28f 100644 --- a/apps/openmw/mwmechanics/spellpriority.hpp +++ b/apps/openmw/mwmechanics/spellpriority.hpp @@ -23,16 +23,18 @@ namespace MWMechanics Target = 0x100 }; - int getRangeTypes (const ESM::EffectList& effects); + int getRangeTypes(const ESM::EffectList& effects); - float rateSpell (const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); - float rateMagicItem (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); - float ratePotion (const MWWorld::Ptr& item, const MWWorld::Ptr &actor); + float rateSpell( + const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool checkMagicka = true); + float rateMagicItem(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); + float ratePotion(const MWWorld::Ptr& item, const MWWorld::Ptr& actor); /// @note target may be empty - float rateEffect (const ESM::ENAMstruct& effect, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); + float rateEffect(const ESM::ENAMstruct& effect, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); /// @note target may be empty - float rateEffects (const ESM::EffectList& list, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); + float rateEffects( + const ESM::EffectList& list, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool useSpellMult = true); float vanillaRateSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); } diff --git a/apps/openmw/mwmechanics/spellresistance.cpp b/apps/openmw/mwmechanics/spellresistance.cpp index 1edf140915d..e55d851ec6f 100644 --- a/apps/openmw/mwmechanics/spellresistance.cpp +++ b/apps/openmw/mwmechanics/spellresistance.cpp @@ -2,6 +2,8 @@ #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -15,7 +17,7 @@ namespace MWMechanics { float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, - const ESM::Spell* spell, const MagicEffects* effects) + const ESM::Spell* spell, const MagicEffects* effects) { if (!actor.getClass().isActor()) return 1; @@ -24,14 +26,14 @@ namespace MWMechanics return 1 - resistance / 100.f; } - float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, - const ESM::Spell* spell, const MagicEffects* effects) + float getEffectResistance(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, + const ESM::Spell* spell, const MagicEffects* effects) { // Effects with no resistance attribute belonging to them can not be resisted if (ESM::MagicEffect::getResistanceEffect(effectId) == -1) return 0.f; - const auto magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); + const auto magicEffect = MWBase::Environment::get().getESMStore()->get().find(effectId); const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); const MWMechanics::MagicEffects* magicEffects = &stats.getMagicEffects(); @@ -51,7 +53,8 @@ namespace MWMechanics if (castChance > 0) x *= 50 / castChance; - float roll = Misc::Rng::rollClosedProbability() * 100; + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + float roll = Misc::Rng::rollClosedProbability(prng) * 100; if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) roll -= resistance; @@ -69,23 +72,23 @@ namespace MWMechanics return x; } - float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects) + float getEffectResistanceAttribute(short effectId, const MagicEffects* actorEffects) { short resistanceEffect = ESM::MagicEffect::getResistanceEffect(effectId); short weaknessEffect = ESM::MagicEffect::getWeaknessEffect(effectId); float resistance = 0; if (resistanceEffect != -1) - resistance += actorEffects->get(resistanceEffect).getMagnitude(); + resistance += actorEffects->getOrDefault(resistanceEffect).getMagnitude(); if (weaknessEffect != -1) - resistance -= actorEffects->get(weaknessEffect).getMagnitude(); + resistance -= actorEffects->getOrDefault(weaknessEffect).getMagnitude(); if (effectId == ESM::MagicEffect::FireDamage) - resistance += actorEffects->get(ESM::MagicEffect::FireShield).getMagnitude(); + resistance += actorEffects->getOrDefault(ESM::MagicEffect::FireShield).getMagnitude(); if (effectId == ESM::MagicEffect::ShockDamage) - resistance += actorEffects->get(ESM::MagicEffect::LightningShield).getMagnitude(); + resistance += actorEffects->getOrDefault(ESM::MagicEffect::LightningShield).getMagnitude(); if (effectId == ESM::MagicEffect::FrostDamage) - resistance += actorEffects->get(ESM::MagicEffect::FrostShield).getMagnitude(); + resistance += actorEffects->getOrDefault(ESM::MagicEffect::FrostShield).getMagnitude(); return resistance; } diff --git a/apps/openmw/mwmechanics/spellresistance.hpp b/apps/openmw/mwmechanics/spellresistance.hpp index 8e74c226017..6966a456d18 100644 --- a/apps/openmw/mwmechanics/spellresistance.hpp +++ b/apps/openmw/mwmechanics/spellresistance.hpp @@ -20,18 +20,18 @@ namespace MWMechanics /// @param effects Override the actor's current magicEffects. Useful if there are effects currently /// being applied (but not applied yet) that should also be considered. float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, - const ESM::Spell* spell = nullptr, const MagicEffects* effects = nullptr); + const ESM::Spell* spell = nullptr, const MagicEffects* effects = nullptr); /// Get the effective resistance against an effect casted by the given actor in the given spell (optional). /// @return >=100 for fully resisted. can also return negative value for damage amplification. /// @param effects Override the actor's current magicEffects. Useful if there are effects currently /// being applied (but not applied yet) that should also be considered. - float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, - const ESM::Spell* spell = nullptr, const MagicEffects* effects = nullptr); + float getEffectResistance(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, + const ESM::Spell* spell = nullptr, const MagicEffects* effects = nullptr); /// Get the resistance attribute against an effect for a given actor. This will add together /// ResistX and Weakness to X effects relevant against the given effect. - float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects); + float getEffectResistanceAttribute(short effectId, const MagicEffects* actorEffects); } #endif diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index b8713760036..26f0ebe4a2c 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -1,10 +1,10 @@ #include "spells.hpp" #include -#include -#include -#include -#include +#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -14,230 +14,139 @@ #include "actorutil.hpp" #include "creaturestats.hpp" -#include "magiceffects.hpp" #include "stat.hpp" namespace MWMechanics { - Spells::Spells() - : mSpellsChanged(false) - { - } + Spells::Spells() {} - Spells::Spells(const Spells& spells) : mSpellList(spells.mSpellList), mSpells(spells.mSpells), - mSelectedSpell(spells.mSelectedSpell), mUsedPowers(spells.mUsedPowers), - mSpellsChanged(spells.mSpellsChanged), mEffects(spells.mEffects), mSourcedEffects(spells.mSourcedEffects) + Spells::Spells(const Spells& spells) + : mSpellList(spells.mSpellList) + , mSpells(spells.mSpells) + , mSelectedSpell(spells.mSelectedSpell) + , mUsedPowers(spells.mUsedPowers) { - if(mSpellList) + if (mSpellList) mSpellList->addListener(this); } - Spells::Spells(Spells&& spells) : mSpellList(std::move(spells.mSpellList)), mSpells(std::move(spells.mSpells)), - mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)), - mSpellsChanged(std::move(spells.mSpellsChanged)), mEffects(std::move(spells.mEffects)), - mSourcedEffects(std::move(spells.mSourcedEffects)) + Spells::Spells(Spells&& spells) + : mSpellList(std::move(spells.mSpellList)) + , mSpells(std::move(spells.mSpells)) + , mSelectedSpell(std::move(spells.mSelectedSpell)) + , mUsedPowers(std::move(spells.mUsedPowers)) { if (mSpellList) mSpellList->updateListener(&spells, this); } - std::map::const_iterator Spells::begin() const + std::vector::const_iterator Spells::begin() const { return mSpells.begin(); } - std::map::const_iterator Spells::end() const + std::vector::const_iterator Spells::end() const { return mSpells.end(); } - void Spells::rebuildEffects() const - { - mEffects = MagicEffects(); - mSourcedEffects.clear(); - - for (const auto& iter : mSpells) - { - const ESM::Spell *spell = iter.first; - - if (spell->mData.mType==ESM::Spell::ST_Ability || spell->mData.mType==ESM::Spell::ST_Blight || - spell->mData.mType==ESM::Spell::ST_Disease || spell->mData.mType==ESM::Spell::ST_Curse) - { - int i=0; - for (const auto& effect : spell->mEffects.mList) - { - if (iter.second.mPurgedEffects.find(i) != iter.second.mPurgedEffects.end()) - { - ++i; - continue; // effect was purged - } - - float random = 1.f; - if (iter.second.mEffectRands.find(i) != iter.second.mEffectRands.end()) - random = iter.second.mEffectRands.at(i); - - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * random; - mEffects.add (effect, magnitude); - mSourcedEffects[spell].add(MWMechanics::EffectKey(effect), magnitude); - - ++i; - } - } - } - } - - bool Spells::hasSpell(const std::string &spell) const + bool Spells::hasSpell(const ESM::RefId& spell) const { return hasSpell(SpellList::getSpell(spell)); } - bool Spells::hasSpell(const ESM::Spell *spell) const + bool Spells::hasSpell(const ESM::Spell* spell) const { - return mSpells.find(spell) != mSpells.end(); + return std::find(mSpells.begin(), mSpells.end(), spell) != mSpells.end(); } - void Spells::add (const ESM::Spell* spell) + void Spells::add(const ESM::Spell* spell) { mSpellList->add(spell); } - void Spells::add (const std::string& spellId) + void Spells::add(const ESM::RefId& spellId) { add(SpellList::getSpell(spellId)); } void Spells::addSpell(const ESM::Spell* spell) { - if (mSpells.find (spell)==mSpells.end()) - { - std::map random; - - // Determine the random magnitudes (unless this is a castable spell, in which case - // they will be determined when the spell is cast) - if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) - { - for (unsigned int i=0; imEffects.mList.size();++i) - { - if (spell->mEffects.mList[i].mMagnMin != spell->mEffects.mList[i].mMagnMax) - { - int delta = spell->mEffects.mList[i].mMagnMax - spell->mEffects.mList[i].mMagnMin; - random[i] = Misc::Rng::rollDice(delta + 1) / static_cast(delta); - } - } - } - - SpellParams params; - params.mEffectRands = random; - mSpells.emplace(spell, params); - mSpellsChanged = true; - } + if (!hasSpell(spell)) + mSpells.emplace_back(spell); } - void Spells::remove (const std::string& spellId) + void Spells::remove(const ESM::RefId& spellId) { const auto spell = SpellList::getSpell(spellId); removeSpell(spell); mSpellList->remove(spell); - if (spellId==mSelectedSpell) - mSelectedSpell.clear(); + if (spellId == mSelectedSpell) + mSelectedSpell = ESM::RefId(); } void Spells::removeSpell(const ESM::Spell* spell) { - const auto it = mSpells.find(spell); - if(it != mSpells.end()) - { + const auto it = std::find(mSpells.begin(), mSpells.end(), spell); + if (it != mSpells.end()) mSpells.erase(it); - mSpellsChanged = true; - } - } - - MagicEffects Spells::getMagicEffects() const - { - if (mSpellsChanged) { - rebuildEffects(); - mSpellsChanged = false; - } - return mEffects; } void Spells::removeAllSpells() { mSpells.clear(); - mSpellsChanged = true; } void Spells::clear(bool modifyBase) { removeAllSpells(); - if(modifyBase) + if (modifyBase) mSpellList->clear(); } - void Spells::setSelectedSpell (const std::string& spellId) + void Spells::setSelectedSpell(const ESM::RefId& spellId) { mSelectedSpell = spellId; } - const std::string Spells::getSelectedSpell() const + const ESM::RefId& Spells::getSelectedSpell() const { return mSelectedSpell; } - bool Spells::isSpellActive(const std::string &id) const + bool Spells::hasSpellType(const ESM::Spell::SpellType type) const { - if (id.empty()) - return false; - - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); - if (spell && hasSpell(spell)) - { - auto type = spell->mData.mType; - return (type==ESM::Spell::ST_Ability || type==ESM::Spell::ST_Blight || type==ESM::Spell::ST_Disease || type==ESM::Spell::ST_Curse); - } - - return false; - } - - bool Spells::hasDisease(const ESM::Spell::SpellType type) const - { - for (const auto& iter : mSpells) - { - const ESM::Spell *spell = iter.first; - if (spell->mData.mType == type) - return true; - } - - return false; + auto it = std::find_if(std::begin(mSpells), std::end(mSpells), + [=](const ESM::Spell* spell) { return spell->mData.mType == type; }); + return it != std::end(mSpells); } bool Spells::hasCommonDisease() const { - return hasDisease(ESM::Spell::ST_Disease); + return hasSpellType(ESM::Spell::ST_Disease); } bool Spells::hasBlightDisease() const { - return hasDisease(ESM::Spell::ST_Blight); + return hasSpellType(ESM::Spell::ST_Blight); } void Spells::purge(const SpellFilter& filter) { - std::vector purged; - for (auto iter = mSpells.begin(); iter!=mSpells.end();) + std::vector purged; + for (auto iter = mSpells.begin(); iter != mSpells.end();) { - const ESM::Spell *spell = iter->first; + const ESM::Spell* spell = *iter; if (filter(spell)) { - mSpells.erase(iter++); + iter = mSpells.erase(iter); purged.push_back(spell->mId); - mSpellsChanged = true; } else ++iter; } - if(!purged.empty()) + if (!purged.empty()) mSpellList->removeAll(purged); } @@ -261,48 +170,11 @@ namespace MWMechanics purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Curse; }); } - void Spells::removeEffects(const std::string &id) - { - if (isSpellActive(id)) - { - for (auto& spell : mSpells) - { - if (spell.first == SpellList::getSpell(id)) - { - for (long unsigned int i = 0; i != spell.first->mEffects.mList.size(); i++) - { - spell.second.mPurgedEffects.insert(i); - } - } - } - - mSpellsChanged = true; - } - } - - void Spells::visitEffectSources(EffectSourceVisitor &visitor) const - { - if (mSpellsChanged) { - rebuildEffects(); - mSpellsChanged = false; - } - - for (const auto& it : mSourcedEffects) - { - const ESM::Spell * spell = it.first; - for (const auto& effectIt : it.second) - { - // FIXME: since Spells merges effects with the same ID, there is no sense to use multiple effects with same ID here - visitor.visit(effectIt.first, -1, spell->mName, spell->mId, -1, effectIt.second.getMagnitude()); - } - } - } - - bool Spells::hasCorprusEffect(const ESM::Spell *spell) + bool Spells::hasCorprusEffect(const ESM::Spell* spell) { for (const auto& effectIt : spell->mEffects.mList) { - if (effectIt.mEffectID == ESM::MagicEffect::Corprus) + if (effectIt.mData.mEffectID == ESM::MagicEffect::Corprus) { return true; } @@ -310,120 +182,62 @@ namespace MWMechanics return false; } - void Spells::purgeEffect(int effectId) - { - for (auto& spellIt : mSpells) - { - int i = 0; - for (auto& effectIt : spellIt.first->mEffects.mList) - { - if (effectIt.mEffectID == effectId) - { - spellIt.second.mPurgedEffects.insert(i); - mSpellsChanged = true; - } - ++i; - } - } - } - - void Spells::purgeEffect(int effectId, const std::string & sourceId) - { - // Effect source may be not a spell - const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(sourceId); - if (spell == nullptr) - return; - - auto spellIt = mSpells.find(spell); - if (spellIt == mSpells.end()) - return; - - int index = 0; - for (auto& effectIt : spellIt->first->mEffects.mList) - { - if (effectIt.mEffectID == effectId) - { - spellIt->second.mPurgedEffects.insert(index); - mSpellsChanged = true; - } - ++index; - } - } - bool Spells::canUsePower(const ESM::Spell* spell) const { - const auto it = mUsedPowers.find(spell); + const auto it = std::find_if( + std::begin(mUsedPowers), std::end(mUsedPowers), [&](auto& pair) { return pair.first == spell; }); return it == mUsedPowers.end() || it->second + 24 <= MWBase::Environment::get().getWorld()->getTimeStamp(); } void Spells::usePower(const ESM::Spell* spell) { - mUsedPowers[spell] = MWBase::Environment::get().getWorld()->getTimeStamp(); + // Updates or inserts a new entry with the current timestamp. + const auto it = std::find_if( + std::begin(mUsedPowers), std::end(mUsedPowers), [&](auto& pair) { return pair.first == spell; }); + const auto timestamp = MWBase::Environment::get().getWorld()->getTimeStamp(); + if (it == mUsedPowers.end()) + mUsedPowers.emplace_back(spell, timestamp); + else + it->second = timestamp; } - void Spells::readState(const ESM::SpellState &state, CreatureStats* creatureStats) + void Spells::readState(const ESM::SpellState& state, CreatureStats* creatureStats) { const auto& baseSpells = mSpellList->getSpells(); - for (ESM::SpellState::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) + for (const ESM::RefId& id : state.mSpells) { // Discard spells that are no longer available due to changed content files - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(id); if (spell) { - mSpells[spell].mEffectRands = it->second.mEffectRands; - mSpells[spell].mPurgedEffects = it->second.mPurgedEffects; + addSpell(spell); - if (it->first == state.mSelectedSpell) - mSelectedSpell = it->first; + if (id == state.mSelectedSpell) + mSelectedSpell = id; } } // Add spells from the base record - for(const std::string& id : baseSpells) + for (const ESM::RefId& id : baseSpells) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); - if(spell) + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(id); + if (spell) addSpell(spell); } - for (std::map::const_iterator it = state.mUsedPowers.begin(); it != state.mUsedPowers.end(); ++it) - { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); - if (!spell) - continue; - mUsedPowers[spell] = MWWorld::TimeStamp(it->second); - } - - for (std::map::const_iterator it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) + for (auto it = state.mUsedPowers.begin(); it != state.mUsedPowers.end(); ++it) { - const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(it->first); if (!spell) continue; - - CorprusStats stats; - - int worsening = state.mCorprusSpells.at(it->first).mWorsenings; - - for (int i=0; imEffects.mList) - { - if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) - stats.mWorsenings[effect.mAttribute] = worsening; - } - stats.mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); - - creatureStats->addCorprusSpell(it->first, stats); + mUsedPowers.emplace_back(spell, MWWorld::TimeStamp(it->second)); } - mSpellsChanged = true; - - // Permanent effects are used only to keep the custom magnitude of corprus spells effects (after cure too), and only in old saves. Convert data to the new approach. - for (std::map >::const_iterator it = - state.mPermanentSpellEffects.begin(); it != state.mPermanentSpellEffects.end(); ++it) + // Permanent effects are used only to keep the custom magnitude of corprus spells effects (after cure too), and + // only in old saves. Convert data to the new approach. + for (auto it = state.mPermanentSpellEffects.begin(); it != state.mPermanentSpellEffects.end(); ++it) { - const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(it->first); if (!spell) continue; @@ -432,41 +246,40 @@ namespace MWMechanics if (creatureStats->getActorId() != player.getClass().getCreatureStats(player).getActorId()) return; - // Note: if target actor has the Restore attirbute effects, stats will be restored. - for (std::vector::const_iterator effectIt = it->second.begin(); effectIt != it->second.end(); ++effectIt) + // Note: if target actor has the Restore attribute effects, stats will be restored. + for (const ESM::SpellState::PermanentSpellEffectInfo& info : it->second) { // Applied corprus effects are already in loaded stats modifiers - if (effectIt->mId == ESM::MagicEffect::FortifyAttribute) + if (info.mId == ESM::MagicEffect::FortifyAttribute) { - AttributeValue attr = creatureStats->getAttribute(effectIt->mArg); - attr.setModifier(attr.getModifier() - effectIt->mMagnitude); - attr.damage(-effectIt->mMagnitude); - creatureStats->setAttribute(effectIt->mArg, attr); + auto id = ESM::Attribute::indexToRefId(info.mArg); + AttributeValue attr = creatureStats->getAttribute(id); + attr.setModifier(attr.getModifier() - info.mMagnitude); + attr.damage(-info.mMagnitude); + creatureStats->setAttribute(id, attr); } - else if (effectIt->mId == ESM::MagicEffect::DrainAttribute) + else if (info.mId == ESM::MagicEffect::DrainAttribute) { - AttributeValue attr = creatureStats->getAttribute(effectIt->mArg); - attr.setModifier(attr.getModifier() + effectIt->mMagnitude); - attr.damage(effectIt->mMagnitude); - creatureStats->setAttribute(effectIt->mArg, attr); + auto id = ESM::Attribute::indexToRefId(info.mArg); + AttributeValue attr = creatureStats->getAttribute(id); + attr.setModifier(attr.getModifier() + info.mMagnitude); + attr.damage(info.mMagnitude); + creatureStats->setAttribute(id, attr); } } } } - void Spells::writeState(ESM::SpellState &state) const + void Spells::writeState(ESM::SpellState& state) const { const auto& baseSpells = mSpellList->getSpells(); - for (const auto& it : mSpells) + for (const auto spell : mSpells) { // Don't save spells and powers stored in the base record - if((it.first->mData.mType != ESM::Spell::ST_Spell && it.first->mData.mType != ESM::Spell::ST_Power) || - std::find(baseSpells.begin(), baseSpells.end(), it.first->mId) == baseSpells.end()) + if ((spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) + || std::find(baseSpells.begin(), baseSpells.end(), spell->mId) == baseSpells.end()) { - ESM::SpellState::SpellParams params; - params.mEffectRands = it.second.mEffectRands; - params.mPurgedEffects = it.second.mPurgedEffects; - state.mSpells.emplace(it.first->mId, params); + state.mSpells.emplace_back(spell->mId); } } @@ -476,30 +289,30 @@ namespace MWMechanics state.mUsedPowers[it.first->mId] = it.second.toEsm(); } - bool Spells::setSpells(const std::string& actorId) + bool Spells::setSpells(const ESM::RefId& actorId) { bool result; - std::tie(mSpellList, result) = MWBase::Environment::get().getWorld()->getStore().getSpellList(actorId); + std::tie(mSpellList, result) = MWBase::Environment::get().getESMStore()->getSpellList(actorId); mSpellList->addListener(this); addAllToInstance(mSpellList->getSpells()); return result; } - void Spells::addAllToInstance(const std::vector& spells) + void Spells::addAllToInstance(const std::vector& spells) { - for(const std::string& id : spells) + for (const ESM::RefId& id : spells) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); - if(spell) + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(id); + if (spell) addSpell(spell); else - Log(Debug::Warning) << "Warning: ignoring nonexistent spell '" << id << "'"; + Log(Debug::Warning) << "Warning: ignoring nonexistent spell " << id; } } Spells::~Spells() { - if(mSpellList) + if (mSpellList) mSpellList->removeListener(this); } } diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index 9737b72cd09..685823b1315 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -1,14 +1,13 @@ #ifndef GAME_MWMECHANICS_SPELLS_H #define GAME_MWMECHANICS_SPELLS_H -#include #include +#include #include #include #include "../mwworld/timestamp.hpp" -#include "magiceffects.hpp" #include "spelllist.hpp" namespace ESM @@ -28,99 +27,86 @@ namespace MWMechanics /// diseases. It also keeps track of used powers (which can only be used every 24h). class Spells { - std::shared_ptr mSpellList; - std::map mSpells; - - // Note: this is the spell that's about to be cast, *not* the spell selected in the GUI (which may be different) - std::string mSelectedSpell; - - std::map mUsedPowers; - - mutable bool mSpellsChanged; - mutable MagicEffects mEffects; - mutable std::map mSourcedEffects; - void rebuildEffects() const; - - bool hasDisease(const ESM::Spell::SpellType type) const; + std::shared_ptr mSpellList; + std::vector mSpells; - using SpellFilter = bool (*)(const ESM::Spell*); - void purge(const SpellFilter& filter); + // Note: this is the spell that's about to be cast, *not* the spell selected in the GUI (which may be different) + ESM::RefId mSelectedSpell; - void addSpell(const ESM::Spell* spell); - void removeSpell(const ESM::Spell* spell); - void removeAllSpells(); + std::vector> mUsedPowers; - friend class SpellList; - public: - using TIterator = std::map::const_iterator; + bool hasSpellType(const ESM::Spell::SpellType type) const; - Spells(); + using SpellFilter = bool (*)(const ESM::Spell*); + void purge(const SpellFilter& filter); - Spells(const Spells&); + void addSpell(const ESM::Spell* spell); + void removeSpell(const ESM::Spell* spell); + void removeAllSpells(); - Spells(Spells&& spells); + friend class SpellList; - ~Spells(); + public: + using Collection = std::vector; - static bool hasCorprusEffect(const ESM::Spell *spell); + Spells(); - void purgeEffect(int effectId); - void purgeEffect(int effectId, const std::string & sourceId); + Spells(const Spells&); - bool canUsePower (const ESM::Spell* spell) const; - void usePower (const ESM::Spell* spell); + Spells(Spells&& spells); - void purgeCommonDisease(); - void purgeBlightDisease(); - void purgeCorprusDisease(); - void purgeCurses(); + ~Spells(); - TIterator begin() const; + static bool hasCorprusEffect(const ESM::Spell* spell); - TIterator end() const; + bool canUsePower(const ESM::Spell* spell) const; + void usePower(const ESM::Spell* spell); - bool hasSpell(const std::string& spell) const; - bool hasSpell(const ESM::Spell* spell) const; + void purgeCommonDisease(); + void purgeBlightDisease(); + void purgeCorprusDisease(); + void purgeCurses(); - void add (const std::string& spell); - ///< Adding a spell that is already listed in *this is a no-op. + Collection::const_iterator begin() const; - void add (const ESM::Spell* spell); - ///< Adding a spell that is already listed in *this is a no-op. + Collection::const_iterator end() const; - void remove (const std::string& spell); - ///< If the spell to be removed is the selected spell, the selected spell will be changed to - /// no spell (empty string). + bool hasSpell(const ESM::RefId& spell) const; + bool hasSpell(const ESM::Spell* spell) const; - MagicEffects getMagicEffects() const; - ///< Return sum of magic effects resulting from abilities, blights, deseases and curses. + void add(const ESM::RefId& spell); + ///< Adding a spell that is already listed in *this is a no-op. - void clear(bool modifyBase = false); - ///< Remove all spells of al types. + void add(const ESM::Spell* spell); + ///< Adding a spell that is already listed in *this is a no-op. - void setSelectedSpell (const std::string& spellId); - ///< This function does not verify, if the spell is available. + void remove(const ESM::RefId& spell); + ///< If the spell to be removed is the selected spell, the selected spell will be changed to + /// no spell (empty string). - const std::string getSelectedSpell() const; - ///< May return an empty string. + void clear(bool modifyBase = false); + ///< Remove all spells of al types. - bool isSpellActive(const std::string& id) const; - ///< Are we under the effects of the given spell ID? + void setSelectedSpell(const ESM::RefId& spellId); + ///< This function does not verify, if the spell is available. - bool hasCommonDisease() const; + const ESM::RefId& getSelectedSpell() const; + ///< May return an empty string. - bool hasBlightDisease() const; + bool hasCommonDisease() const; - void removeEffects(const std::string& id); + bool hasBlightDisease() const; - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; + /// Iteration methods for lua + size_t count() const { return mSpells.size(); } + const ESM::Spell* at(size_t index) const { return mSpells.at(index); } - void readState (const ESM::SpellState& state, CreatureStats* creatureStats); - void writeState (ESM::SpellState& state) const; + void readState(const ESM::SpellState& state, CreatureStats* creatureStats); + void writeState(ESM::SpellState& state) const; - bool setSpells(const std::string& id); + bool setSpells(const ESM::RefId& id); - void addAllToInstance(const std::vector& spells); + void addAllToInstance(const std::vector& spells); }; } diff --git a/apps/openmw/mwmechanics/spellutil.cpp b/apps/openmw/mwmechanics/spellutil.cpp index 0c667e680a8..022aaec2629 100644 --- a/apps/openmw/mwmechanics/spellutil.cpp +++ b/apps/openmw/mwmechanics/spellutil.cpp @@ -2,8 +2,12 @@ #include +#include +#include +#include +#include + #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" @@ -14,19 +18,31 @@ namespace MWMechanics { - ESM::Skill::SkillEnum spellSchoolToSkill(int school) + namespace { - static const std::array schoolSkillArray + float getTotalCost(const ESM::EffectList& list, const EffectCostMethod method = EffectCostMethod::GameSpell) { - ESM::Skill::Alteration, ESM::Skill::Conjuration, ESM::Skill::Destruction, - ESM::Skill::Illusion, ESM::Skill::Mysticism, ESM::Skill::Restoration - }; - return schoolSkillArray.at(school); + float cost = 0; + + for (const ESM::IndexedENAMstruct& effect : list.mList) + { + float effectCost = std::max(0.f, MWMechanics::calcEffectCost(effect.mData, nullptr, method)); + + // This is applied to the whole spell cost for each effect when + // creating spells, but is only applied on the effect itself in TES:CS. + if (effect.mData.mRange == ESM::RT_Target) + effectCost *= 1.5; + + cost += effectCost; + } + return cost; + } } - float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect) + float calcEffectCost( + const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, const EffectCostMethod method) { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); if (!magicEffect) magicEffect = store.get().find(effect.mEffectID); bool hasMagnitude = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude); @@ -34,20 +50,50 @@ namespace MWMechanics bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; int minMagn = hasMagnitude ? effect.mMagnMin : 1; int maxMagn = hasMagnitude ? effect.mMagnMax : 1; + if (method == EffectCostMethod::PlayerSpell || method == EffectCostMethod::GameSpell) + { + minMagn = std::max(1, minMagn); + maxMagn = std::max(1, maxMagn); + } int duration = hasDuration ? effect.mDuration : 1; if (!appliedOnce) duration = std::max(1, duration); static const float fEffectCostMult = store.get().find("fEffectCostMult")->mValue.getFloat(); + static const float iAlchemyMod = store.get().find("iAlchemyMod")->mValue.getFloat(); - float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn)); + int durationOffset = 0; + int minArea = 0; + float costMult = fEffectCostMult; + if (method == EffectCostMethod::PlayerSpell) + { + durationOffset = 1; + minArea = 1; + } + else if (method == EffectCostMethod::GamePotion) + { + minArea = 1; + costMult = iAlchemyMod; + } + + float x = 0.5 * (minMagn + maxMagn); x *= 0.1 * magicEffect->mData.mBaseCost; - x *= 1 + duration; - x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost; + x *= durationOffset + duration; + x += 0.05 * std::max(minArea, effect.mArea) * magicEffect->mData.mBaseCost; + + return x * costMult; + } + + int calcSpellCost(const ESM::Spell& spell) + { + if (!(spell.mData.mFlags & ESM::Spell::F_Autocalc)) + return spell.mData.mCost; - return x * fEffectCostMult; + float cost = getTotalCost(spell.mEffects); + + return std::round(cost); } - int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor) + int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr& actor) { /* * Each point of enchant skill above/under 10 subtracts/adds @@ -59,30 +105,146 @@ namespace MWMechanics return static_cast((result < 1) ? 1 : result); } - float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool) + int getEffectiveEnchantmentCastCost(const ESM::Enchantment& enchantment, const MWWorld::Ptr& actor) + { + float castCost; + if (enchantment.mData.mFlags & ESM::Enchantment::Autocalc) + castCost = getTotalCost(enchantment.mEffects, EffectCostMethod::GameEnchantment); + else + castCost = static_cast(enchantment.mData.mCost); + return getEffectiveEnchantmentCastCost(castCost, actor); + } + + int getEnchantmentCharge(const ESM::Enchantment& enchantment) + { + if (enchantment.mData.mFlags & ESM::Enchantment::Autocalc) + { + int charge + = static_cast(std::round(getTotalCost(enchantment.mEffects, EffectCostMethod::GameEnchantment))); + const auto& store = MWBase::Environment::get().getESMStore()->get(); + switch (enchantment.mData.mType) + { + case ESM::Enchantment::CastOnce: + { + static const int iMagicItemChargeOnce = store.find("iMagicItemChargeOnce")->mValue.getInteger(); + return charge * iMagicItemChargeOnce; + } + case ESM::Enchantment::WhenStrikes: + { + static const int iMagicItemChargeStrike = store.find("iMagicItemChargeStrike")->mValue.getInteger(); + return charge * iMagicItemChargeStrike; + } + case ESM::Enchantment::WhenUsed: + { + static const int iMagicItemChargeUse = store.find("iMagicItemChargeUse")->mValue.getInteger(); + return charge * iMagicItemChargeUse; + } + case ESM::Enchantment::ConstantEffect: + { + static const int iMagicItemChargeConst = store.find("iMagicItemChargeConst")->mValue.getInteger(); + return charge * iMagicItemChargeConst; + } + } + } + return enchantment.mData.mCharge; + } + + int getPotionValue(const ESM::Potion& potion) + { + if (potion.mData.mFlags & ESM::Potion::Autocalc) + { + float cost = getTotalCost(potion.mEffects, EffectCostMethod::GamePotion); + return std::round(cost); + } + return potion.mData.mValue; + } + + std::optional rollIngredientEffect( + MWWorld::Ptr caster, const ESM::Ingredient* ingredient, uint32_t index) + { + if (index >= 4) + throw std::range_error("Index out of range"); + + ESM::ENAMstruct effect; + effect.mEffectID = ingredient->mData.mEffectID[index]; + effect.mSkill = ingredient->mData.mSkills[index]; + effect.mAttribute = ingredient->mData.mAttributes[index]; + effect.mRange = ESM::RT_Self; + effect.mArea = 0; + + if (effect.mEffectID < 0) + return std::nullopt; + + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const auto magicEffect = store.get().find(effect.mEffectID); + const MWMechanics::CreatureStats& creatureStats = caster.getClass().getCreatureStats(caster); + + float x = (caster.getClass().getSkill(caster, ESM::Skill::Alchemy) + + 0.2f * creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified() + + 0.1f * creatureStats.getAttribute(ESM::Attribute::Luck).getModified()) + * creatureStats.getFatigueTerm(); + + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); + if (roll > x) + { + return std::nullopt; + } + + float magnitude = 0; + float y = roll / std::min(x, 100.f); + y *= 0.25f * x; + if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + effect.mDuration = 1; + else + effect.mDuration = static_cast(y); + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) + { + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) + magnitude = floor((0.05f * y) / (0.1f * magicEffect->mData.mBaseCost)); + else + magnitude = floor(y / (0.1f * magicEffect->mData.mBaseCost)); + magnitude = std::max(1.f, magnitude); + } + else + magnitude = 1; + + effect.mMagnMax = static_cast(magnitude); + effect.mMagnMin = static_cast(magnitude); + + ESM::EffectList effects; + effects.mList.push_back({ effect, index }); + return effects; + } + + float calcSpellBaseSuccessChance(const ESM::Spell* spell, const MWWorld::Ptr& actor, ESM::RefId* effectiveSchool) { // Morrowind for some reason uses a formula slightly different from magicka cost calculation float y = std::numeric_limits::max(); float lowestSkill = 0; - for (const ESM::ENAMstruct& effect : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& effect : spell->mEffects.mList) { - float x = static_cast(effect.mDuration); - const auto magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); + float x = static_cast(effect.mData.mDuration); + const auto magicEffect + = MWBase::Environment::get().getESMStore()->get().find(effect.mData.mEffectID); if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) x = std::max(1.f, x); x *= 0.1f * magicEffect->mData.mBaseCost; - x *= 0.5f * (effect.mMagnMin + effect.mMagnMax); - x += effect.mArea * 0.05f * magicEffect->mData.mBaseCost; - if (effect.mRange == ESM::RT_Target) + x *= 0.5f * (effect.mData.mMagnMin + effect.mData.mMagnMax); + x += effect.mData.mArea * 0.05f * magicEffect->mData.mBaseCost; + if (effect.mData.mRange == ESM::RT_Target) x *= 1.5f; - static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore().get().find( - "fEffectCostMult")->mValue.getFloat(); + static const float fEffectCostMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fEffectCostMult") + ->mValue.getFloat(); x *= fEffectCostMult; - float s = 2.0f * actor.getClass().getSkill(actor, spellSchoolToSkill(magicEffect->mData.mSchool)); + float s = 2.0f * actor.getClass().getSkill(actor, magicEffect->mData.mSchool); if (s - x < y) { y = s - x; @@ -97,12 +259,13 @@ namespace MWMechanics float actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); float actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); - float castChance = (lowestSkill - spell->mData.mCost + 0.2f * actorWillpower + 0.1f * actorLuck); + float castChance = (lowestSkill - calcSpellCost(*spell) + 0.2f * actorWillpower + 0.1f * actorLuck); return castChance; } - float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) + float getSpellSuccessChance( + const ESM::Spell* spell, const MWWorld::Ptr& actor, ESM::RefId* effectiveSchool, bool cap, bool checkMagicka) { // NB: Base chance is calculated here because the effective school pointer must be filled float baseChance = calcSpellBaseSuccessChance(spell, actor, effectiveSchool); @@ -111,7 +274,7 @@ namespace MWMechanics CreatureStats& stats = actor.getClass().getCreatureStats(actor); - if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).getMagnitude() && !godmode) + if (stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Silence).getMagnitude() && !godmode) return 0; if (spell->mData.mType == ESM::Spell::ST_Power) @@ -123,92 +286,52 @@ namespace MWMechanics if (spell->mData.mType != ESM::Spell::ST_Spell) return 100; - if (checkMagicka && spell->mData.mCost > 0 && stats.getMagicka().getCurrent() < spell->mData.mCost) + if (checkMagicka && calcSpellCost(*spell) > 0 && stats.getMagicka().getCurrent() < calcSpellCost(*spell)) return 0; if (spell->mData.mFlags & ESM::Spell::F_Always) return 100; - float castBonus = -stats.getMagicEffects().get(ESM::MagicEffect::Sound).getMagnitude(); + float castBonus = -stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Sound).getMagnitude(); float castChance = baseChance + castBonus; castChance *= stats.getFatigueTerm(); - return std::max(0.f, cap ? std::min(100.f, castChance) : castChance); + if (cap) + return std::clamp(castChance, 0.f, 100.f); + + return std::max(castChance, 0.f); } - float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) + float getSpellSuccessChance( + const ESM::RefId& spellId, const MWWorld::Ptr& actor, ESM::RefId* effectiveSchool, bool cap, bool checkMagicka) { - if (const auto spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId)) + if (const auto spell = MWBase::Environment::get().getESMStore()->get().search(spellId)) return getSpellSuccessChance(spell, actor, effectiveSchool, cap, checkMagicka); return 0.f; } - int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor) + ESM::RefId getSpellSchool(const ESM::RefId& spellId, const MWWorld::Ptr& actor) { - int school = 0; + ESM::RefId school; getSpellSuccessChance(spellId, actor, &school); return school; } - int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor) + ESM::RefId getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor) { - int school = 0; + ESM::RefId school; getSpellSuccessChance(spell, actor, &school); return school; } - bool spellIncreasesSkill(const ESM::Spell *spell) + bool spellIncreasesSkill(const ESM::Spell* spell) { return spell->mData.mType == ESM::Spell::ST_Spell && !(spell->mData.mFlags & ESM::Spell::F_Always); } - bool spellIncreasesSkill(const std::string &spellId) + bool spellIncreasesSkill(const ESM::RefId& spellId) { - const auto spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); + const auto spell = MWBase::Environment::get().getESMStore()->get().search(spellId); return spell && spellIncreasesSkill(spell); } - - bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer) - { - switch (effectId) - { - case ESM::MagicEffect::Levitate: - { - if (!MWBase::Environment::get().getWorld()->isLevitationEnabled()) - { - if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}"); - return false; - } - break; - } - case ESM::MagicEffect::Soultrap: - { - if (!target.getClass().isNpc() // no messagebox for NPCs - && (target.getTypeName() == typeid(ESM::Creature).name() && target.get()->mBase->mData.mSoul == 0)) - { - if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}"); - return true; // must still apply to get visual effect and have target regard it as attack - } - break; - } - case ESM::MagicEffect::WaterWalking: - { - if (target.getClass().isPureWaterCreature(target) && MWBase::Environment::get().getWorld()->isSwimming(target)) - return false; - - MWBase::World *world = MWBase::Environment::get().getWorld(); - - if (!world->isWaterWalkingCastableOnTarget(target)) - { - if (castByPlayer && caster == target) - MWBase::Environment::get().getWindowManager()->messageBox ("#{sMagicInvalidEffect}"); - return false; - } - break; - } - } - return true; - } } diff --git a/apps/openmw/mwmechanics/spellutil.hpp b/apps/openmw/mwmechanics/spellutil.hpp index 865a9126e76..fb9d14c8a52 100644 --- a/apps/openmw/mwmechanics/spellutil.hpp +++ b/apps/openmw/mwmechanics/spellutil.hpp @@ -1,12 +1,18 @@ #ifndef MWMECHANICS_SPELLUTIL_H #define MWMECHANICS_SPELLUTIL_H -#include +#include + +#include namespace ESM { + struct EffectList; struct ENAMstruct; + struct Enchantment; + struct Ingredient; struct MagicEffect; + struct Potion; struct Spell; } @@ -17,11 +23,25 @@ namespace MWWorld namespace MWMechanics { - ESM::Skill::SkillEnum spellSchoolToSkill(int school); + enum class EffectCostMethod + { + GameSpell, + PlayerSpell, + GameEnchantment, + GamePotion, + }; + + float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect = nullptr, + const EffectCostMethod method = EffectCostMethod::GameSpell); + int calcSpellCost(const ESM::Spell& spell); - float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect = nullptr); + int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr& actor); + int getEffectiveEnchantmentCastCost(const ESM::Enchantment& enchantment, const MWWorld::Ptr& actor); + int getEnchantmentCharge(const ESM::Enchantment& enchantment); - int getEffectiveEnchantmentCastCost (float castCost, const MWWorld::Ptr& actor); + int getPotionValue(const ESM::Potion& potion); + std::optional rollIngredientEffect( + MWWorld::Ptr caster, const ESM::Ingredient* ingredient, uint32_t index = 0); /** * @param spell spell to cast @@ -32,19 +52,18 @@ namespace MWMechanics * @note actor can be an NPC or a creature * @return success chance from 0 to 100 (in percent), if cap=false then chance above 100 may be returned. */ - float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool); - float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true); - float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true); + float calcSpellBaseSuccessChance(const ESM::Spell* spell, const MWWorld::Ptr& actor, ESM::RefId* effectiveSchool); + float getSpellSuccessChance(const ESM::Spell* spell, const MWWorld::Ptr& actor, + ESM::RefId* effectiveSchool = nullptr, bool cap = true, bool checkMagicka = true); + float getSpellSuccessChance(const ESM::RefId& spellId, const MWWorld::Ptr& actor, + ESM::RefId* effectiveSchool = nullptr, bool cap = true, bool checkMagicka = true); - int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor); - int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor); + ESM::RefId getSpellSchool(const ESM::RefId& spellId, const MWWorld::Ptr& actor); + ESM::RefId getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor); /// Get whether or not the given spell contributes to skill progress. bool spellIncreasesSkill(const ESM::Spell* spell); - bool spellIncreasesSkill(const std::string& spellId); - - /// Check if the given effect can be applied to the target. If \a castByPlayer, emits a message box on failure. - bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer); + bool spellIncreasesSkill(const ESM::RefId& spellId); } #endif diff --git a/apps/openmw/mwmechanics/stat.cpp b/apps/openmw/mwmechanics/stat.cpp index c87de2ccbbd..b603fc220fb 100644 --- a/apps/openmw/mwmechanics/stat.cpp +++ b/apps/openmw/mwmechanics/stat.cpp @@ -1,180 +1,72 @@ #include "stat.hpp" -#include +#include + +#include namespace MWMechanics { - template - Stat::Stat() : mBase (0), mModified (0), mCurrentModified (0) {} - template - Stat::Stat(T base) : mBase (base), mModified (base), mCurrentModified (base) {} - template - Stat::Stat(T base, T modified) : mBase (base), mModified (modified), mCurrentModified (modified) {} - - template - const T& Stat::getBase() const - { - return mBase; - } - - template - T Stat::getModified(bool capped) const - { - if(!capped) - return mModified; - return std::max(static_cast(0), mModified); - } - - template - T Stat::getCurrentModified() const - { - return mCurrentModified; - } - - template - T Stat::getModifier() const - { - return mModified-mBase; - } - - template - T Stat::getCurrentModifier() const - { - return mCurrentModified - mModified; - } - - template - void Stat::set (const T& value) - { - T diff = value - mBase; - mBase = mModified = value; - mCurrentModified += diff; - } - - template - void Stat::setBase (const T& value) - { - T diff = value - mBase; - mBase = value; - mModified += diff; - mCurrentModified += diff; - } - - template - void Stat::setModified (T value, const T& min, const T& max) - { - T diff = value - mModified; - - if (mBase+diffmax) - { - value = max + (mModified - mBase); - diff = value - mModified; - } - - mModified = value; - mBase += diff; - mCurrentModified += diff; - } - - template - void Stat::setCurrentModified(T value) + template + Stat::Stat() + : mBase(0) + , mModifier(0) { - mCurrentModified = value; } - - template - void Stat::setModifier (const T& modifier) + template + Stat::Stat(T base, T modified) + : mBase(base) + , mModifier(modified) { - mModified = mBase + modifier; } - template - void Stat::setCurrentModifier(const T& modifier) + template + T Stat::getModified(bool capped) const { - mCurrentModified = mModified + modifier; + if (capped) + return std::max({}, mModifier + mBase); + return mModifier + mBase; } - template - void Stat::writeState (ESM::StatState& state) const + template + void Stat::writeState(ESM::StatState& state) const { state.mBase = mBase; - state.mMod = mCurrentModified; + state.mMod = mModifier; } - template - void Stat::readState (const ESM::StatState& state) + template + void Stat::readState(const ESM::StatState& state) { mBase = state.mBase; - mModified = state.mBase; - mCurrentModified = state.mMod; + mModifier = state.mMod; } - - template - DynamicStat::DynamicStat() : mStatic (0), mCurrent (0) {} - template - DynamicStat::DynamicStat(T base) : mStatic (base), mCurrent (base) {} - template - DynamicStat::DynamicStat(T base, T modified, T current) : mStatic(base, modified), mCurrent (current) {} - template - DynamicStat::DynamicStat(const Stat &stat, T current) : mStatic(stat), mCurrent (current) {} - - - template - const T& DynamicStat::getBase() const - { - return mStatic.getBase(); - } - template - T DynamicStat::getModified() const - { - return mStatic.getModified(); - } - template - T DynamicStat::getCurrentModified() const + template + DynamicStat::DynamicStat() + : mStatic(0, 0) + , mCurrent(0) { - return mStatic.getCurrentModified(); } - - template - const T& DynamicStat::getCurrent() const + template + DynamicStat::DynamicStat(T base) + : mStatic(base, 0) + , mCurrent(base) { - return mCurrent; } - - template - void DynamicStat::set (const T& value) + template + DynamicStat::DynamicStat(T base, T modified, T current) + : mStatic(base, modified) + , mCurrent(current) { - mStatic.set (value); - mCurrent = value; } - template - void DynamicStat::setBase (const T& value) + template + DynamicStat::DynamicStat(const Stat& stat, T current) + : mStatic(stat) + , mCurrent(current) { - mStatic.setBase (value); - - if (mCurrent>getModified()) - mCurrent = getModified(); } - template - void DynamicStat::setModified (T value, const T& min, const T& max) - { - mStatic.setModified (value, min, max); - if (mCurrent>getModified()) - mCurrent = getModified(); - } - template - void DynamicStat::setCurrentModified(T value) - { - mStatic.setCurrentModified(value); - } - template - void DynamicStat::setCurrent (const T& value, bool allowDecreaseBelowZero, bool allowIncreaseAboveModified) + template + void DynamicStat::setCurrent(const T& value, bool allowDecreaseBelowZero, bool allowIncreaseAboveModified) { if (value > mCurrent) { @@ -197,39 +89,37 @@ namespace MWMechanics mCurrent = 0; } } - template - void DynamicStat::setModifier (const T& modifier, bool allowCurrentDecreaseBelowZero) - { - T diff = modifier - mStatic.getModifier(); - mStatic.setModifier (modifier); - setCurrent (getCurrent()+diff, allowCurrentDecreaseBelowZero); - } - template - void DynamicStat::setCurrentModifier(const T& modifier, bool allowCurrentDecreaseBelowZero) + template + T DynamicStat::getRatio(bool nanIsZero) const { - T diff = modifier - mStatic.getCurrentModifier(); - mStatic.setCurrentModifier(modifier); - - // The (modifier > 0) check here allows increase over modified only if the modifier is positive (a fortify effect is active). - setCurrent (getCurrent() + diff, allowCurrentDecreaseBelowZero, (modifier > 0)); + T modified = getModified(); + if (modified == T{}) + { + if (nanIsZero) + return modified; + return { 1 }; + } + return getCurrent() / modified; } - template - void DynamicStat::writeState (ESM::StatState& state) const + template + void DynamicStat::writeState(ESM::StatState& state) const { - mStatic.writeState (state); + mStatic.writeState(state); state.mCurrent = mCurrent; } - template - void DynamicStat::readState (const ESM::StatState& state) + template + void DynamicStat::readState(const ESM::StatState& state) { - mStatic.readState (state); + mStatic.readState(state); mCurrent = state.mCurrent; } - AttributeValue::AttributeValue() : - mBase(0.f), mModifier(0.f), mDamage(0.f) + AttributeValue::AttributeValue() + : mBase(0.f) + , mModifier(0.f) + , mDamage(0.f) { } @@ -246,28 +136,35 @@ namespace MWMechanics return mModifier; } - void AttributeValue::setBase(float base) + void AttributeValue::setBase(float base, bool clearModifier) { mBase = base; + if (clearModifier) + { + mModifier = 0.f; + mDamage = 0.f; + } } void AttributeValue::setModifier(float mod) { - mModifier = mod; + if (mod < 0) + { + mModifier = 0.f; + mDamage -= mod; + } + else + mModifier = mod; } void AttributeValue::damage(float damage) { - float threshold = mBase + mModifier; - - if (mDamage + damage > threshold) - mDamage = threshold; - else - mDamage += damage; + mDamage += damage; } void AttributeValue::restore(float amount) { - if (mDamage <= 0) return; + if (mDamage <= 0) + return; mDamage -= std::min(mDamage, amount); } @@ -277,22 +174,22 @@ namespace MWMechanics return mDamage; } - void AttributeValue::writeState (ESM::StatState& state) const + void AttributeValue::writeState(ESM::StatState& state) const { state.mBase = mBase; state.mMod = mModifier; state.mDamage = mDamage; } - void AttributeValue::readState (const ESM::StatState& state) + void AttributeValue::readState(const ESM::StatState& state) { mBase = state.mBase; mModifier = state.mMod; mDamage = state.mDamage; } - SkillValue::SkillValue() : - mProgress(0) + SkillValue::SkillValue() + : mProgress(0) { } @@ -305,15 +202,15 @@ namespace MWMechanics mProgress = progress; } - void SkillValue::writeState (ESM::StatState& state) const + void SkillValue::writeState(ESM::StatState& state) const { - AttributeValue::writeState (state); + AttributeValue::writeState(state); state.mProgress = mProgress; } - void SkillValue::readState (const ESM::StatState& state) + void SkillValue::readState(const ESM::StatState& state) { - AttributeValue::readState (state); + AttributeValue::readState(state); mProgress = state.mProgress; } } diff --git a/apps/openmw/mwmechanics/stat.hpp b/apps/openmw/mwmechanics/stat.hpp index fb9dca9221d..21cdad21a97 100644 --- a/apps/openmw/mwmechanics/stat.hpp +++ b/apps/openmw/mwmechanics/stat.hpp @@ -1,123 +1,93 @@ #ifndef GAME_MWMECHANICS_STAT_H #define GAME_MWMECHANICS_STAT_H -#include -#include - namespace ESM { - template + template struct StatState; } namespace MWMechanics { - template + template class Stat { - T mBase; - T mModified; - T mCurrentModified; - - public: - typedef T Type; - - Stat(); - Stat(T base); - Stat(T base, T modified); + T mBase; + T mModifier; - const T& getBase() const; - - T getModified(bool capped = true) const; - T getCurrentModified() const; - T getModifier() const; - T getCurrentModifier() const; + public: + typedef T Type; - /// Set base and modified to \a value. - void set (const T& value); + Stat(); + Stat(T base, T modified); - /// Set base and adjust modified accordingly. - void setBase (const T& value); + const T& getBase() const { return mBase; } - /// Set modified value and adjust base accordingly. - void setModified (T value, const T& min, const T& max = std::numeric_limits::max()); + T getModified(bool capped = true) const; + T getModifier() const { return mModifier; } - /// Set "current modified," used for drain and fortify. Unlike the regular modifier - /// this just adds and subtracts from the current value without changing the maximum. - void setCurrentModified(T value); + void setBase(const T& value) { mBase = value; } - void setModifier (const T& modifier); - void setCurrentModifier (const T& modifier); + void setModifier(const T& modifier) { mModifier = modifier; } - void writeState (ESM::StatState& state) const; - void readState (const ESM::StatState& state); + void writeState(ESM::StatState& state) const; + void readState(const ESM::StatState& state); }; - template - inline bool operator== (const Stat& left, const Stat& right) + template + inline bool operator==(const Stat& left, const Stat& right) { - return left.getBase()==right.getBase() && - left.getModified()==right.getModified(); + return left.getBase() == right.getBase() && left.getModifier() == right.getModifier(); } - template - inline bool operator!= (const Stat& left, const Stat& right) + template + inline bool operator!=(const Stat& left, const Stat& right) { - return !(left==right); + return !(left == right); } - template + template class DynamicStat { - Stat mStatic; - T mCurrent; + Stat mStatic; + T mCurrent; - public: - typedef T Type; - - DynamicStat(); - DynamicStat(T base); - DynamicStat(T base, T modified, T current); - DynamicStat(const Stat &stat, T current); - - const T& getBase() const; - T getModified() const; - T getCurrentModified() const; - const T& getCurrent() const; + public: + typedef T Type; - /// Set base, modified and current to \a value. - void set (const T& value); + DynamicStat(); + DynamicStat(T base); + DynamicStat(T base, T modified, T current); + DynamicStat(const Stat& stat, T current); - /// Set base and adjust modified accordingly. - void setBase (const T& value); + const T& getBase() const { return mStatic.getBase(); } + T getModified(bool capped = true) const { return mStatic.getModified(capped); } + const T& getCurrent() const { return mCurrent; } + T getRatio(bool nanIsZero = true) const; - /// Set modified value and adjust base accordingly. - void setModified (T value, const T& min, const T& max = std::numeric_limits::max()); + /// Set base and adjust current accordingly. + void setBase(const T& value) { mStatic.setBase(value); } - /// Set "current modified," used for drain and fortify. Unlike the regular modifier - /// this just adds and subtracts from the current value without changing the maximum. - void setCurrentModified(T value); + void setCurrent(const T& value, bool allowDecreaseBelowZero = false, bool allowIncreaseAboveModified = false); - void setCurrent (const T& value, bool allowDecreaseBelowZero = false, bool allowIncreaseAboveModified = false); - void setModifier (const T& modifier, bool allowCurrentToDecreaseBelowZero=false); - void setCurrentModifier (const T& modifier, bool allowCurrentToDecreaseBelowZero = false); + T getModifier() const { return mStatic.getModifier(); } + void setModifier(T value) { mStatic.setModifier(value); } - void writeState (ESM::StatState& state) const; - void readState (const ESM::StatState& state); + void writeState(ESM::StatState& state) const; + void readState(const ESM::StatState& state); }; - template - inline bool operator== (const DynamicStat& left, const DynamicStat& right) + template + inline bool operator==(const DynamicStat& left, const DynamicStat& right) { - return left.getBase()==right.getBase() && - left.getModified()==right.getModified() && - left.getCurrent()==right.getCurrent(); + return left.getBase() == right.getBase() && left.getModifier() == right.getModifier() + && left.getCurrent() == right.getCurrent(); } - template - inline bool operator!= (const DynamicStat& left, const DynamicStat& right) + template + inline bool operator!=(const DynamicStat& left, const DynamicStat& right) { - return !(left==right); + return !(left == right); } class AttributeValue @@ -133,53 +103,52 @@ namespace MWMechanics float getBase() const; float getModifier() const; - void setBase(float base); + void setBase(float base, bool clearModifier = false); void setModifier(float mod); // Maximum attribute damage is limited to the modified value. - // Note: I think MW applies damage directly to mModified, since you can also - // "restore" drained attributes. We need to rewrite the magic effect system to support this. + // Note: MW applies damage directly to mModified, however it does track how much + // a damaged attribute that has been fortified beyond its base can be restored. + // Getting rid of mDamage would require calculating its value by ignoring active effects when restoring void damage(float damage); void restore(float amount); float getDamage() const; - void writeState (ESM::StatState& state) const; - void readState (const ESM::StatState& state); + void writeState(ESM::StatState& state) const; + void readState(const ESM::StatState& state); }; class SkillValue : public AttributeValue { float mProgress; + public: SkillValue(); float getProgress() const; void setProgress(float progress); - void writeState (ESM::StatState& state) const; - void readState (const ESM::StatState& state); + void writeState(ESM::StatState& state) const; + void readState(const ESM::StatState& state); }; - inline bool operator== (const AttributeValue& left, const AttributeValue& right) + inline bool operator==(const AttributeValue& left, const AttributeValue& right) { - return left.getBase() == right.getBase() - && left.getModifier() == right.getModifier() - && left.getDamage() == right.getDamage(); + return left.getBase() == right.getBase() && left.getModifier() == right.getModifier() + && left.getDamage() == right.getDamage(); } - inline bool operator!= (const AttributeValue& left, const AttributeValue& right) + inline bool operator!=(const AttributeValue& left, const AttributeValue& right) { return !(left == right); } - inline bool operator== (const SkillValue& left, const SkillValue& right) + inline bool operator==(const SkillValue& left, const SkillValue& right) { - return left.getBase() == right.getBase() - && left.getModifier() == right.getModifier() - && left.getDamage() == right.getDamage() - && left.getProgress() == right.getProgress(); + return left.getBase() == right.getBase() && left.getModifier() == right.getModifier() + && left.getDamage() == right.getDamage() && left.getProgress() == right.getProgress(); } - inline bool operator!= (const SkillValue& left, const SkillValue& right) + inline bool operator!=(const SkillValue& left, const SkillValue& right) { return !(left == right); } diff --git a/apps/openmw/mwmechanics/steering.cpp b/apps/openmw/mwmechanics/steering.cpp index eaf37fbd2a4..fe7d12e6dfc 100644 --- a/apps/openmw/mwmechanics/steering.cpp +++ b/apps/openmw/mwmechanics/steering.cpp @@ -1,7 +1,7 @@ #include "steering.hpp" #include -#include +#include #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" @@ -13,32 +13,32 @@ namespace MWMechanics { -bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, float epsilonRadians) -{ - MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor); - float diff = Misc::normalizeAngle(targetAngleRadians - actor.getRefData().getPosition().rot[axis]); - float absDiff = std::abs(diff); - - // The turning animation actually moves you slightly, so the angle will be wrong again. - // Use epsilon to prevent jerkiness. - if (absDiff < epsilonRadians) - return true; - - float limit = getAngularVelocity(actor.getClass().getMaxSpeed(actor)) * MWBase::Environment::get().getFrameDuration(); - static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); - if (smoothMovement) - limit *= std::min(absDiff / osg::PI + 0.1, 0.5); - - if (absDiff > limit) - diff = osg::sign(diff) * limit; - - movement.mRotation[axis] = diff; - return false; -} - -bool zTurn(const MWWorld::Ptr& actor, float targetAngleRadians, float epsilonRadians) -{ - return smoothTurn(actor, targetAngleRadians, 2, epsilonRadians); -} + bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, float epsilonRadians) + { + MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor); + float diff = Misc::normalizeAngle(targetAngleRadians - actor.getRefData().getPosition().rot[axis]); + float absDiff = std::abs(diff); + + // The turning animation actually moves you slightly, so the angle will be wrong again. + // Use epsilon to prevent jerkiness. + if (absDiff < epsilonRadians) + return true; + + float limit + = getAngularVelocity(actor.getClass().getMaxSpeed(actor)) * MWBase::Environment::get().getFrameDuration(); + if (Settings::game().mSmoothMovement) + limit *= std::min(absDiff / osg::PI + 0.1, 0.5); + + if (absDiff > limit) + diff = osg::sign(diff) * limit; + + movement.mRotation[axis] = diff; + return false; + } + + bool zTurn(const MWWorld::Ptr& actor, float targetAngleRadians, float epsilonRadians) + { + return smoothTurn(actor, targetAngleRadians, 2, epsilonRadians); + } } diff --git a/apps/openmw/mwmechanics/steering.hpp b/apps/openmw/mwmechanics/steering.hpp index f305a6961cc..d1d120e0912 100644 --- a/apps/openmw/mwmechanics/steering.hpp +++ b/apps/openmw/mwmechanics/steering.hpp @@ -7,27 +7,28 @@ namespace MWWorld { -class Ptr; + class Ptr; } namespace MWMechanics { -// Max rotating speed, radian/sec -inline float getAngularVelocity(const float actorSpeed) -{ - const float baseAngluarVelocity = 10; - const float baseSpeed = 200; - return baseAngluarVelocity * std::max(actorSpeed / baseSpeed, 1.0f); -} - -/// configure rotation settings for an actor to reach this target angle (eventually) -/// @return have we reached the target angle? -bool zTurn(const MWWorld::Ptr& actor, float targetAngleRadians, - float epsilonRadians = osg::DegreesToRadians(0.5)); - -bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, - float epsilonRadians = osg::DegreesToRadians(0.5)); + // Max rotating speed, radian/sec + inline float getAngularVelocity(const float actorSpeed) + { + constexpr float degreesPerFrame = 15.f; + constexpr int framesPerSecond = 60; + const float baseAngularVelocity = osg::DegreesToRadians(degreesPerFrame * framesPerSecond); + const float baseSpeed = 200; + return baseAngularVelocity * std::max(actorSpeed / baseSpeed, 1.0f); + } + + /// configure rotation settings for an actor to reach this target angle (eventually) + /// @return have we reached the target angle? + bool zTurn(const MWWorld::Ptr& actor, float targetAngleRadians, float epsilonRadians = osg::DegreesToRadians(0.5)); + + bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, + float epsilonRadians = osg::DegreesToRadians(0.5)); } diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index d90545fc601..e4b9e953aac 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -1,20 +1,23 @@ #include "summoning.hpp" #include +#include +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/manualref.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwrender/animation.hpp" -#include "creaturestats.hpp" #include "aifollow.hpp" +#include "creaturestats.hpp" namespace MWMechanics { @@ -22,154 +25,145 @@ namespace MWMechanics bool isSummoningEffect(int effectId) { return ((effectId >= ESM::MagicEffect::SummonScamp && effectId <= ESM::MagicEffect::SummonStormAtronach) - || (effectId == ESM::MagicEffect::SummonCenturionSphere) - || (effectId >= ESM::MagicEffect::SummonFabricant && effectId <= ESM::MagicEffect::SummonCreature05)); + || (effectId == ESM::MagicEffect::SummonCenturionSphere) + || (effectId >= ESM::MagicEffect::SummonFabricant && effectId <= ESM::MagicEffect::SummonCreature05)); } - std::string getSummonedCreature(int effectId) + static const std::map& getSummonMap() { - static const std::map summonMap - { - {ESM::MagicEffect::SummonAncestralGhost, "sMagicAncestralGhostID"}, - {ESM::MagicEffect::SummonBonelord, "sMagicBonelordID"}, - {ESM::MagicEffect::SummonBonewalker, "sMagicLeastBonewalkerID"}, - {ESM::MagicEffect::SummonCenturionSphere, "sMagicCenturionSphereID"}, - {ESM::MagicEffect::SummonClannfear, "sMagicClannfearID"}, - {ESM::MagicEffect::SummonDaedroth, "sMagicDaedrothID"}, - {ESM::MagicEffect::SummonDremora, "sMagicDremoraID"}, - {ESM::MagicEffect::SummonFabricant, "sMagicFabricantID"}, - {ESM::MagicEffect::SummonFlameAtronach, "sMagicFlameAtronachID"}, - {ESM::MagicEffect::SummonFrostAtronach, "sMagicFrostAtronachID"}, - {ESM::MagicEffect::SummonGoldenSaint, "sMagicGoldenSaintID"}, - {ESM::MagicEffect::SummonGreaterBonewalker, "sMagicGreaterBonewalkerID"}, - {ESM::MagicEffect::SummonHunger, "sMagicHungerID"}, - {ESM::MagicEffect::SummonScamp, "sMagicScampID"}, - {ESM::MagicEffect::SummonSkeletalMinion, "sMagicSkeletalMinionID"}, - {ESM::MagicEffect::SummonStormAtronach, "sMagicStormAtronachID"}, - {ESM::MagicEffect::SummonWingedTwilight, "sMagicWingedTwilightID"}, - {ESM::MagicEffect::SummonWolf, "sMagicCreature01ID"}, - {ESM::MagicEffect::SummonBear, "sMagicCreature02ID"}, - {ESM::MagicEffect::SummonBonewolf, "sMagicCreature03ID"}, - {ESM::MagicEffect::SummonCreature04, "sMagicCreature04ID"}, - {ESM::MagicEffect::SummonCreature05, "sMagicCreature05ID"} + static std::map summonMap; + + if (summonMap.size() > 0) + return summonMap; + + const std::map summonMapToGameSetting{ + { ESM::MagicEffect::SummonAncestralGhost, "sMagicAncestralGhostID" }, + { ESM::MagicEffect::SummonBonelord, "sMagicBonelordID" }, + { ESM::MagicEffect::SummonBonewalker, "sMagicLeastBonewalkerID" }, + { ESM::MagicEffect::SummonCenturionSphere, "sMagicCenturionSphereID" }, + { ESM::MagicEffect::SummonClannfear, "sMagicClannfearID" }, + { ESM::MagicEffect::SummonDaedroth, "sMagicDaedrothID" }, + { ESM::MagicEffect::SummonDremora, "sMagicDremoraID" }, + { ESM::MagicEffect::SummonFabricant, "sMagicFabricantID" }, + { ESM::MagicEffect::SummonFlameAtronach, "sMagicFlameAtronachID" }, + { ESM::MagicEffect::SummonFrostAtronach, "sMagicFrostAtronachID" }, + { ESM::MagicEffect::SummonGoldenSaint, "sMagicGoldenSaintID" }, + { ESM::MagicEffect::SummonGreaterBonewalker, "sMagicGreaterBonewalkerID" }, + { ESM::MagicEffect::SummonHunger, "sMagicHungerID" }, + { ESM::MagicEffect::SummonScamp, "sMagicScampID" }, + { ESM::MagicEffect::SummonSkeletalMinion, "sMagicSkeletalMinionID" }, + { ESM::MagicEffect::SummonStormAtronach, "sMagicStormAtronachID" }, + { ESM::MagicEffect::SummonWingedTwilight, "sMagicWingedTwilightID" }, + { ESM::MagicEffect::SummonWolf, "sMagicCreature01ID" }, + { ESM::MagicEffect::SummonBear, "sMagicCreature02ID" }, + { ESM::MagicEffect::SummonBonewolf, "sMagicCreature03ID" }, + { ESM::MagicEffect::SummonCreature04, "sMagicCreature04ID" }, + { ESM::MagicEffect::SummonCreature05, "sMagicCreature05ID" }, }; - auto it = summonMap.find(effectId); - if (it != summonMap.end()) - return MWBase::Environment::get().getWorld()->getStore().get().find(it->second)->mValue.getString(); - return std::string(); - } - - UpdateSummonedCreatures::UpdateSummonedCreatures(const MWWorld::Ptr &actor) - : mActor(actor) - { + for (const auto& it : summonMapToGameSetting) + { + summonMap[it.first] = ESM::RefId::stringRefId( + MWBase::Environment::get().getESMStore()->get().find(it.second)->mValue.getString()); + } + return summonMap; } - void UpdateSummonedCreatures::visit(EffectKey key, int effectIndex, const std::string &sourceName, const std::string &sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime) + ESM::RefId getSummonedCreature(int effectId) { - if (isSummoningEffect(key.mId) && magnitude > 0) + const auto& summonMap = getSummonMap(); + auto it = summonMap.find(effectId); + if (it != summonMap.end()) { - mActiveEffects.insert(ESM::SummonKey(key.mId, sourceId, effectIndex)); + return it->second; } + return ESM::RefId(); } - void UpdateSummonedCreatures::process(bool cleanup) + int summonCreature(int effectId, const MWWorld::Ptr& summoner) { - MWMechanics::CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); - - for (std::set::iterator it = mActiveEffects.begin(); it != mActiveEffects.end(); ++it) + const ESM::RefId& creatureID = getSummonedCreature(effectId); + int creatureActorId = -1; + if (!creatureID.empty()) { - bool found = creatureMap.find(*it) != creatureMap.end(); - if (!found) + try { - std::string creatureID = getSummonedCreature(it->mEffectId); - if (!creatureID.empty()) + auto world = MWBase::Environment::get().getWorld(); + MWWorld::ManualRef ref(world->getStore(), creatureID, 1); + MWWorld::Ptr placed = world->safePlaceObject(ref.getPtr(), summoner, summoner.getCell(), 0, 120.f); + + MWMechanics::CreatureStats& summonedCreatureStats = placed.getClass().getCreatureStats(placed); + + // Make the summoned creature follow its master and help in fights + AiFollow package(summoner); + summonedCreatureStats.getAiSequence().stack(package, placed); + creatureActorId = summonedCreatureStats.getActorId(); + + MWRender::Animation* anim = world->getAnimation(placed); + if (anim) { - int creatureActorId = -1; - try - { - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), creatureID, 1); - - MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); - - // Make the summoned creature follow its master and help in fights - AiFollow package(mActor); - summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); - creatureActorId = summonedCreatureStats.getActorId(); - - MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), mActor, mActor.getCell(), 0, 120.f); - - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(placed); - if (anim) - { - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_Start"); - if (fx) - anim->addEffect("meshes\\" + fx->mModel, -1, false); - } - } - catch (std::exception& e) - { - Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what(); - // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log - } - - creatureMap.emplace(*it, creatureActorId); + const ESM::Static* fx + = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Summon_Start")); + if (fx) + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), "", false); } } - } - - // Update summon effects - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) - { - bool found = mActiveEffects.find(it->first) != mActiveEffects.end(); - if (!found) + catch (std::exception& e) { - // Effect has ended - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, it->second); - creatureMap.erase(it++); - continue; + Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what(); + // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning + // log } - ++it; + + summoner.getClass().getCreatureStats(summoner).getSummonedCreatureMap().emplace(effectId, creatureActorId); } + return creatureActorId; + } + + void updateSummons(const MWWorld::Ptr& summoner, bool cleanup) + { + MWMechanics::CreatureStats& creatureStats = summoner.getClass().getCreatureStats(summoner); + auto& creatureMap = creatureStats.getSummonedCreatureMap(); std::vector graveyard = creatureStats.getSummonedCreatureGraveyard(); creatureStats.getSummonedCreatureGraveyard().clear(); for (const int creature : graveyard) - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, creature); + MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, creature); if (!cleanup) return; - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) + for (auto it = creatureMap.begin(); it != creatureMap.end();) { - if(it->second == -1) + if (it->second == -1) { // Keep the spell effect active if we failed to spawn anything it++; continue; } MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(it->second); - if (!ptr.isEmpty() && ptr.getClass().getCreatureStats(ptr).isDead() && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished()) + if (!ptr.isEmpty() && ptr.getClass().getCreatureStats(ptr).isDead() + && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished()) { // Purge the magic effect so a new creature can be summoned if desired - purgeSummonEffect(mActor, *it); + auto summon = *it; creatureMap.erase(it++); + purgeSummonEffect(summoner, summon); } else ++it; } } - void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon) + void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon) { auto& creatureStats = summoner.getClass().getCreatureStats(summoner); - creatureStats.getActiveSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId, summon.first.mEffectIndex); - creatureStats.getSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId); - if (summoner.getClass().hasInventoryStore(summoner)) - summoner.getClass().getInventoryStore(summoner).purgeEffect(summon.first.mEffectId, summon.first.mSourceId, false, summon.first.mEffectIndex); + creatureStats.getActiveSpells().purge( + [summon](const auto& spell, const auto& effect) { + return effect.mEffectId == summon.first && effect.getActorId() == summon.second; + }, + summoner); MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, summon.second); } diff --git a/apps/openmw/mwmechanics/summoning.hpp b/apps/openmw/mwmechanics/summoning.hpp index 3c3e18a96b1..a341ee6e939 100644 --- a/apps/openmw/mwmechanics/summoning.hpp +++ b/apps/openmw/mwmechanics/summoning.hpp @@ -1,42 +1,28 @@ #ifndef OPENMW_MECHANICS_SUMMONING_H #define OPENMW_MECHANICS_SUMMONING_H -#include - -#include "../mwworld/ptr.hpp" - -#include - -#include "magiceffects.hpp" +#include +#include +namespace ESM +{ + class RefId; +} +namespace MWWorld +{ + class Ptr; +} namespace MWMechanics { - class CreatureStats; - bool isSummoningEffect(int effectId); - std::string getSummonedCreature(int effectId); - - void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon); - - struct UpdateSummonedCreatures : public EffectSourceVisitor - { - UpdateSummonedCreatures(const MWWorld::Ptr& actor); - virtual ~UpdateSummonedCreatures() = default; - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override; - - /// To call after all effect sources have been visited - void process(bool cleanup); + ESM::RefId getSummonedCreature(int effectId); - private: - MWWorld::Ptr mActor; + void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon); - std::set mActiveEffects; - }; + int summonCreature(int effectId, const MWWorld::Ptr& summoner); + void updateSummons(const MWWorld::Ptr& summoner, bool cleanup); } #endif diff --git a/apps/openmw/mwmechanics/tickableeffects.cpp b/apps/openmw/mwmechanics/tickableeffects.cpp deleted file mode 100644 index 5056179f8f0..00000000000 --- a/apps/openmw/mwmechanics/tickableeffects.cpp +++ /dev/null @@ -1,216 +0,0 @@ -#include "tickableeffects.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/cellstore.hpp" -#include "../mwworld/class.hpp" -#include "../mwworld/containerstore.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" - -#include "actorutil.hpp" -#include "npcstats.hpp" - -namespace MWMechanics -{ - void adjustDynamicStat(CreatureStats& creatureStats, int index, float magnitude, bool allowDecreaseBelowZero = false) - { - DynamicStat stat = creatureStats.getDynamic(index); - stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero); - creatureStats.setDynamic(index, stat); - } - - bool disintegrateSlot (const MWWorld::Ptr& ptr, int slot, float disintegrate) - { - if (!ptr.getClass().hasInventoryStore(ptr)) - return false; - - MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); - MWWorld::ContainerStoreIterator item = inv.getSlot(slot); - - if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) - { - if (!item->getClass().hasItemHealth(*item)) - return false; - int charge = item->getClass().getItemHealth(*item); - if (charge == 0) - return false; - - // Store remainder of disintegrate amount (automatically subtracted if > 1) - item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); - - charge = item->getClass().getItemHealth(*item); - charge -= std::min(static_cast(disintegrate), charge); - item->getCellRef().setCharge(charge); - - if (charge == 0) - { - // Will unequip the broken item and try to find a replacement - if (ptr != getPlayer()) - inv.autoEquip(ptr); - else - inv.unequipItem(*item, ptr); - } - - return true; - } - - return false; - } - - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude) - { - if (magnitude == 0.f) - return false; - - bool receivedMagicDamage = false; - bool godmode = actor == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - switch (effectKey.mId) - { - case ESM::MagicEffect::DamageAttribute: - { - if (godmode) - break; - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.damage(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreAttribute: - { - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.restore(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreHealth: - case ESM::MagicEffect::RestoreMagicka: - case ESM::MagicEffect::RestoreFatigue: - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::RestoreHealth, magnitude); - break; - case ESM::MagicEffect::DamageHealth: - if (godmode) - break; - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::DamageHealth, -magnitude); - break; - - case ESM::MagicEffect::DamageMagicka: - case ESM::MagicEffect::DamageFatigue: - { - if (godmode) - break; - int index = effectKey.mId-ESM::MagicEffect::DamageHealth; - static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); - adjustDynamicStat(creatureStats, index, -magnitude, index == 2 && uncappedDamageFatigue); - break; - } - case ESM::MagicEffect::AbsorbHealth: - if (!godmode || magnitude <= 0) - { - if (magnitude > 0.f) - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - } - break; - - case ESM::MagicEffect::AbsorbMagicka: - case ESM::MagicEffect::AbsorbFatigue: - if (!godmode || magnitude <= 0) - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - break; - - case ESM::MagicEffect::DisintegrateArmor: - { - if (godmode) - break; - static const std::array priorities - { - MWWorld::InventoryStore::Slot_CarriedLeft, - MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_LeftPauldron, - MWWorld::InventoryStore::Slot_RightPauldron, - MWWorld::InventoryStore::Slot_LeftGauntlet, - MWWorld::InventoryStore::Slot_RightGauntlet, - MWWorld::InventoryStore::Slot_Helmet, - MWWorld::InventoryStore::Slot_Greaves, - MWWorld::InventoryStore::Slot_Boots - }; - for (const int priority : priorities) - { - if (disintegrateSlot(actor, priority, magnitude)) - break; - } - - break; - } - case ESM::MagicEffect::DisintegrateWeapon: - if (!godmode) - disintegrateSlot(actor, MWWorld::InventoryStore::Slot_CarriedRight, magnitude); - break; - - case ESM::MagicEffect::SunDamage: - { - // isInCell shouldn't be needed, but updateActor called during game start - if (!actor.isInCell() || !actor.getCell()->isExterior() || godmode) - break; - float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour(); - float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); - float damageScale = 1.f - timeDiff / 7.f; - // When cloudy, the sun damage effect is halved - static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get().find( - "fMagicSunBlockedMult")->mValue.getFloat(); - - int weather = MWBase::Environment::get().getWorld()->getCurrentWeather(); - if (weather > 1) - damageScale *= fMagicSunBlockedMult; - - adjustDynamicStat(creatureStats, 0, -magnitude * damageScale); - if (magnitude * damageScale > 0.f) - receivedMagicDamage = true; - - break; - } - - case ESM::MagicEffect::FireDamage: - case ESM::MagicEffect::ShockDamage: - case ESM::MagicEffect::FrostDamage: - case ESM::MagicEffect::Poison: - { - if (godmode) - break; - adjustDynamicStat(creatureStats, 0, -magnitude); - receivedMagicDamage = true; - break; - } - - case ESM::MagicEffect::DamageSkill: - case ESM::MagicEffect::RestoreSkill: - { - if (!actor.getClass().isNpc()) - break; - if (godmode && effectKey.mId == ESM::MagicEffect::DamageSkill) - break; - NpcStats &npcStats = actor.getClass().getNpcStats(actor); - SkillValue& skill = npcStats.getSkill(effectKey.mArg); - if (effectKey.mId == ESM::MagicEffect::RestoreSkill) - skill.restore(magnitude); - else - skill.damage(magnitude); - break; - } - - default: - return false; - } - - if (receivedMagicDamage && actor == getPlayer()) - MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); - return true; - } -} diff --git a/apps/openmw/mwmechanics/tickableeffects.hpp b/apps/openmw/mwmechanics/tickableeffects.hpp deleted file mode 100644 index ccd42ca19b0..00000000000 --- a/apps/openmw/mwmechanics/tickableeffects.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef MWMECHANICS_TICKABLEEFFECTS_H -#define MWMECHANICS_TICKABLEEFFECTS_H - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - class CreatureStats; - struct EffectKey; - - /// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed - /// Note: this function works in loop, so magic effects should not be removed here to avoid iterator invalidation. - /// @return Was the effect a tickable effect with a magnitude? - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey& effectKey, float magnitude); -} - -#endif diff --git a/apps/openmw/mwmechanics/trading.cpp b/apps/openmw/mwmechanics/trading.cpp deleted file mode 100644 index b824d7c450e..00000000000 --- a/apps/openmw/mwmechanics/trading.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include "trading.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/mechanicsmanager.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" - -#include "creaturestats.hpp" - -namespace MWMechanics -{ - Trading::Trading() {} - - bool Trading::haggle(const MWWorld::Ptr& player, const MWWorld::Ptr& merchant, int playerOffer, int merchantOffer) - { - // accept if merchant offer is better than player offer - if ( playerOffer <= merchantOffer ) { - return true; - } - - // reject if npc is a creature - if ( merchant.getTypeName() != typeid(ESM::NPC).name() ) { - return false; - } - - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); - - // Is the player buying? - bool buying = (merchantOffer < 0); - int a = std::abs(merchantOffer); - int b = std::abs(playerOffer); - int d = (buying) - ? int(100 * (a - b) / a) - : int(100 * (b - a) / b); - - int clampedDisposition = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(merchant); - - const MWMechanics::CreatureStats &merchantStats = merchant.getClass().getCreatureStats(merchant); - const MWMechanics::CreatureStats &playerStats = player.getClass().getCreatureStats(player); - - float a1 = static_cast(player.getClass().getSkill(player, ESM::Skill::Mercantile)); - float b1 = 0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified(); - float c1 = 0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified(); - float d1 = static_cast(merchant.getClass().getSkill(merchant, ESM::Skill::Mercantile)); - float e1 = 0.1f * merchantStats.getAttribute(ESM::Attribute::Luck).getModified(); - float f1 = 0.2f * merchantStats.getAttribute(ESM::Attribute::Personality).getModified(); - - float dispositionTerm = gmst.find("fDispositionMod")->mValue.getFloat() * (clampedDisposition - 50); - float pcTerm = (dispositionTerm + a1 + b1 + c1) * playerStats.getFatigueTerm(); - float npcTerm = (d1 + e1 + f1) * merchantStats.getFatigueTerm(); - float x = gmst.find("fBargainOfferMulti")->mValue.getFloat() * d - + gmst.find("fBargainOfferBase")->mValue.getFloat() - + int(pcTerm - npcTerm); - - int roll = Misc::Rng::rollDice(100) + 1; - - // reject if roll fails - // (or if player tries to buy things and get money) - if ( roll > x || (merchantOffer < 0 && 0 < playerOffer) ) { - return false; - } - - // apply skill gain on successful barter - float skillGain = 0.f; - int finalPrice = std::abs(playerOffer); - int initialMerchantOffer = std::abs(merchantOffer); - - if ( !buying && (finalPrice > initialMerchantOffer) ) { - skillGain = floor(100.f * (finalPrice - initialMerchantOffer) / finalPrice); - } - else if ( buying && (finalPrice < initialMerchantOffer) ) { - skillGain = floor(100.f * (initialMerchantOffer - finalPrice) / initialMerchantOffer); - } - player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0, skillGain); - - return true; - } -} diff --git a/apps/openmw/mwmechanics/trading.hpp b/apps/openmw/mwmechanics/trading.hpp deleted file mode 100644 index e30b82f5e8a..00000000000 --- a/apps/openmw/mwmechanics/trading.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef OPENMW_MECHANICS_TRADING_H -#define OPENMW_MECHANICS_TRADING_H - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - class Trading - { - public: - Trading(); - - bool haggle(const MWWorld::Ptr& player, const MWWorld::Ptr& merchant, int playerOffer, int merchantOffer); - }; -} - -#endif diff --git a/apps/openmw/mwmechanics/typedaipackage.hpp b/apps/openmw/mwmechanics/typedaipackage.hpp index d2d424326cc..9cbcc95aee1 100644 --- a/apps/openmw/mwmechanics/typedaipackage.hpp +++ b/apps/openmw/mwmechanics/typedaipackage.hpp @@ -8,20 +8,28 @@ namespace MWMechanics template struct TypedAiPackage : public AiPackage { - TypedAiPackage() : - AiPackage(T::getTypeId(), T::makeDefaultOptions()) {} + TypedAiPackage() + : AiPackage(T::getTypeId(), T::makeDefaultOptions()) + { + } - TypedAiPackage(const Options& options) : - AiPackage(T::getTypeId(), options) {} + TypedAiPackage(bool repeat) + : AiPackage(T::getTypeId(), T::makeDefaultOptions().withRepeat(repeat)) + { + } - template - TypedAiPackage(Derived*) : - AiPackage(Derived::getTypeId(), Derived::makeDefaultOptions()) {} + TypedAiPackage(const Options& options) + : AiPackage(T::getTypeId(), options) + { + } - std::unique_ptr clone() const override + template + TypedAiPackage(Derived*) + : AiPackage(Derived::getTypeId(), Derived::makeDefaultOptions()) { - return std::make_unique(*static_cast(this)); } + + std::unique_ptr clone() const override { return std::make_unique(*static_cast(this)); } }; } diff --git a/apps/openmw/mwmechanics/weaponpriority.cpp b/apps/openmw/mwmechanics/weaponpriority.cpp index 8480dc208e4..dd83db286f5 100644 --- a/apps/openmw/mwmechanics/weaponpriority.cpp +++ b/apps/openmw/mwmechanics/weaponpriority.cpp @@ -1,6 +1,8 @@ #include "weaponpriority.hpp" -#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -9,18 +11,18 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" -#include "combat.hpp" #include "aicombataction.hpp" +#include "combat.hpp" #include "spellpriority.hpp" #include "spellutil.hpp" #include "weapontype.hpp" namespace MWMechanics { - float rateWeapon (const MWWorld::Ptr &item, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, int type, - float arrowRating, float boltRating) + float rateWeapon(const MWWorld::Ptr& item, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, int type, + float arrowRating, float boltRating) { - if (enemy.isEmpty() || item.getTypeName() != typeid(ESM::Weapon).name()) + if (enemy.isEmpty() || item.getType() != ESM::Weapon::sRecordId) return 0.f; if (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) == 0) @@ -38,7 +40,7 @@ namespace MWMechanics if (type == -1 && weapclass == ESM::WeaponType::Ammo) return 0.f; - float rating=0.f; + float rating = 0.f; static const float fAIMeleeWeaponMult = gmst.find("fAIMeleeWeaponMult")->mValue.getFloat(); float ratingMult = fAIMeleeWeaponMult; @@ -46,7 +48,7 @@ namespace MWMechanics { // Underwater ranged combat is impossible if (world->isUnderwater(MWWorld::ConstPtr(actor), 0.75f) - || world->isUnderwater(MWWorld::ConstPtr(enemy), 0.75f)) + || world->isUnderwater(MWWorld::ConstPtr(enemy), 0.75f)) return 0.f; // Use a higher rating multiplier if the actor is out of enemy's reach, use the normal mult otherwise @@ -106,30 +108,34 @@ namespace MWMechanics const ESM::Enchantment* enchantment = world->getStore().get().find(weapon->mEnchant); if (enchantment->mData.mType == ESM::Enchantment::WhenStrikes) { - int castCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), actor); + int castCost = getEffectiveEnchantmentCastCost(*enchantment, actor); float charge = item.getCellRef().getEnchantmentCharge(); - if (charge == -1 || charge >= castCost || weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) - rating += rateEffects(enchantment->mEffects, actor, enemy); + if (charge == -1 || charge >= castCost || weapclass == ESM::WeaponType::Thrown + || weapclass == ESM::WeaponType::Ammo) + rating += rateEffects(enchantment->mEffects, actor, enemy, false); } } int value = 50.f; - if (actor.getClass().isNpc()) - { - int skill = item.getClass().getEquipmentSkill(item); - if (skill != -1) - value = actor.getClass().getSkill(actor, skill); - } - else + ESM::RefId skill = item.getClass().getEquipmentSkill(item); + if (!skill.empty()) + value = actor.getClass().getSkill(actor, skill); + // Prefer hand-to-hand if our skill is 0 (presumably due to magic) + if (value <= 0.f) + return 0.f; + // Note that a creature with a dagger and 0 Stealth will forgo the weapon despite using Combat for hit chance. + // The same creature will use a sword provided its Combat stat isn't 0. We're using the "skill" value here to + // decide whether to use the weapon at all, but adjusting the final rating based on actual hit chance - i.e. the + // Combat stat. + if (!actor.getClass().isNpc()) { - MWWorld::LiveCellRef *ref = actor.get(); + MWWorld::LiveCellRef* ref = actor.get(); value = ref->mBase->mData.mCombat; } // Take hit chance in account, but do not allow rating become negative. - float chance = getHitChance(actor, enemy, value) / 100.f; - rating *= std::min(1.f, std::max(0.01f, chance)); + rating *= std::clamp(getHitChance(actor, enemy, value) / 100.f, 0.01f, 1.f); if (weapclass != ESM::WeaponType::Ammo) rating *= weapon->mData.mSpeed; @@ -137,7 +143,7 @@ namespace MWMechanics return rating * ratingMult; } - float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, MWWorld::Ptr &bestAmmo, int ammoType) + float rateAmmo(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, MWWorld::Ptr& bestAmmo, int ammoType) { float bestAmmoRating = 0.f; if (!actor.getClass().hasInventoryStore(actor)) @@ -158,15 +164,17 @@ namespace MWMechanics return bestAmmoRating; } - float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, int ammoType) + float rateAmmo(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, int ammoType) { MWWorld::Ptr emptyPtr; return rateAmmo(actor, enemy, emptyPtr, ammoType); } - float vanillaRateWeaponAndAmmo(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) + float vanillaRateWeaponAndAmmo( + const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); static const float fAIMeleeWeaponMult = gmst.find("fAIMeleeWeaponMult")->mValue.getFloat(); static const float fAIMeleeArmorMult = gmst.find("fAIMeleeArmorMult")->mValue.getFloat(); @@ -197,7 +205,7 @@ namespace MWMechanics float thrustRating = esmWeap->mData.mThrust[1] * skillMult * fAIMeleeWeaponMult; return actor.getClass().getArmorRating(actor) * fAIMeleeArmorMult - + std::max(std::max(chopRating, slashRating), thrustRating); + + std::max(std::max(chopRating, slashRating), thrustRating); } } diff --git a/apps/openmw/mwmechanics/weaponpriority.hpp b/apps/openmw/mwmechanics/weaponpriority.hpp index 9dcef3e2e50..44e9611b49e 100644 --- a/apps/openmw/mwmechanics/weaponpriority.hpp +++ b/apps/openmw/mwmechanics/weaponpriority.hpp @@ -1,17 +1,21 @@ #ifndef OPENMW_WEAPON_PRIORITY_H #define OPENMW_WEAPON_PRIORITY_H -#include "../mwworld/ptr.hpp" +namespace MWWorld +{ + class Ptr; +} namespace MWMechanics { - float rateWeapon (const MWWorld::Ptr& item, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, - int type=-1, float arrowRating=0.f, float boltRating=0.f); + float rateWeapon(const MWWorld::Ptr& item, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, int type = -1, + float arrowRating = 0.f, float boltRating = 0.f); - float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, MWWorld::Ptr &bestAmmo, int ammoType); - float rateAmmo(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy, int ammoType); + float rateAmmo(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, MWWorld::Ptr& bestAmmo, int ammoType); + float rateAmmo(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, int ammoType); - float vanillaRateWeaponAndAmmo(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); + float vanillaRateWeaponAndAmmo( + const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); } #endif diff --git a/apps/openmw/mwmechanics/weapontype.cpp b/apps/openmw/mwmechanics/weapontype.cpp index 2f8e45f7ff0..8c516298038 100644 --- a/apps/openmw/mwmechanics/weapontype.cpp +++ b/apps/openmw/mwmechanics/weapontype.cpp @@ -1,33 +1,370 @@ #include "weapontype.hpp" +#include "creaturestats.hpp" +#include "drawstate.hpp" + #include "../mwworld/class.hpp" +#include "../mwworld/inventorystore.hpp" + +#include + +#include namespace MWMechanics { - MWWorld::ContainerStoreIterator getActiveWeapon(MWWorld::Ptr actor, int *weaptype) + template + struct Weapon + { + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "", + /* long group */ "", + /* sound ID */ "", + /* attach bone */ "", + /* sheath bone */ "", + /* usage skill */ ESM::Skill::HandToHand, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ 0 }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "1h", + /* long group */ "pickprobe", + /* sound ID */ "", + /* attach bone */ "", + /* sheath bone */ "", + /* usage skill */ ESM::Skill::Security, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ 0 }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "spell", + /* long group */ "spellcast", + /* sound ID */ "", + /* attach bone */ "", + /* sheath bone */ "", + /* usage skill */ ESM::Skill::HandToHand, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ ESM::WeaponType::TwoHanded }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "hh", + /* long group */ "handtohand", + /* sound ID */ "", + /* attach bone */ "", + /* sheath bone */ "", + /* usage skill */ ESM::Skill::HandToHand, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ ESM::WeaponType::TwoHanded }; + return value; + } + }; + + template <> + struct Weapon { - MWWorld::InventoryStore &inv = actor.getClass().getInventoryStore(actor); - CreatureStats &stats = actor.getClass().getCreatureStats(actor); - if(stats.getDrawState() == MWMechanics::DrawState_Spell) + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "1s", + /* long group */ "shortbladeonehand", + /* sound ID */ "Item Weapon Shortblade", + /* attach bone */ "Weapon Bone", + /* sheath bone */ "Bip01 ShortBladeOneHand", + /* usage skill */ ESM::Skill::ShortBlade, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ ESM::WeaponType::HasHealth }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "1h", + /* long group */ "weapononehand", + /* sound ID */ "Item Weapon Longblade", + /* attach bone */ "Weapon Bone", + /* sheath bone */ "Bip01 LongBladeOneHand", + /* usage skill */ ESM::Skill::LongBlade, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ ESM::WeaponType::HasHealth }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "1b", + /* long group */ "bluntonehand", + /* sound ID */ "Item Weapon Blunt", + /* attach bone */ "Weapon Bone", + /* sheath bone */ "Bip01 BluntOneHand", + /* usage skill */ ESM::Skill::BluntWeapon, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ ESM::WeaponType::HasHealth }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "1b", + /* long group */ "bluntonehand", + /* sound ID */ "Item Weapon Blunt", + /* attach bone */ "Weapon Bone", + /* sheath bone */ "Bip01 LongBladeOneHand", + /* usage skill */ ESM::Skill::Axe, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ ESM::WeaponType::HasHealth }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "2c", + /* long group */ "weapontwohand", + /* sound ID */ "Item Weapon Longblade", + /* attach bone */ "Weapon Bone", + /* sheath bone */ "Bip01 LongBladeTwoClose", + /* usage skill */ ESM::Skill::LongBlade, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ ESM::WeaponType::HasHealth | ESM::WeaponType::TwoHanded }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "2b", + /* long group */ "blunttwohand", + /* sound ID */ "Item Weapon Blunt", + /* attach bone */ "Weapon Bone", + /* sheath bone */ "Bip01 AxeTwoClose", + /* usage skill */ ESM::Skill::Axe, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ ESM::WeaponType::HasHealth | ESM::WeaponType::TwoHanded }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "2b", + /* long group */ "blunttwohand", + /* sound ID */ "Item Weapon Blunt", + /* attach bone */ "Weapon Bone", + /* sheath bone */ "Bip01 BluntTwoClose", + /* usage skill */ ESM::Skill::BluntWeapon, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ ESM::WeaponType::HasHealth | ESM::WeaponType::TwoHanded }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "2w", + /* long group */ "weapontwowide", + /* sound ID */ "Item Weapon Blunt", + /* attach bone */ "Weapon Bone", + /* sheath bone */ "Bip01 BluntTwoWide", + /* usage skill */ ESM::Skill::BluntWeapon, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ ESM::WeaponType::HasHealth | ESM::WeaponType::TwoHanded }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "2w", + /* long group */ "weapontwowide", + /* sound ID */ "Item Weapon Spear", + /* attach bone */ "Weapon Bone", + /* sheath bone */ "Bip01 SpearTwoWide", + /* usage skill */ ESM::Skill::Spear, + /* weapon class*/ ESM::WeaponType::Melee, + /* ammo type */ ESM::Weapon::None, + /* flags */ ESM::WeaponType::HasHealth | ESM::WeaponType::TwoHanded }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "bow", + /* long group */ "bowandarrow", + /* sound ID */ "Item Weapon Bow", + /* attach bone */ "Weapon Bone Left", + /* sheath bone */ "Bip01 MarksmanBow", + /* usage skill */ ESM::Skill::Marksman, + /* weapon class*/ ESM::WeaponType::Ranged, + /* ammo type */ ESM::Weapon::Arrow, + /* flags */ ESM::WeaponType::HasHealth | ESM::WeaponType::TwoHanded }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "crossbow", + /* long group */ "crossbow", + /* sound ID */ "Item Weapon Crossbow", + /* attach bone */ "Weapon Bone", + /* sheath bone */ "Bip01 MarksmanCrossbow", + /* usage skill */ ESM::Skill::Marksman, + /* weapon class*/ ESM::WeaponType::Ranged, + /* ammo type */ ESM::Weapon::Bolt, + /* flags */ ESM::WeaponType::HasHealth | ESM::WeaponType::TwoHanded }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "1t", + /* long group */ "throwweapon", + /* sound ID */ "Item Weapon Blunt", + /* attach bone */ "Weapon Bone", + /* sheath bone */ "Bip01 MarksmanThrown", + /* usage skill */ ESM::Skill::Marksman, + /* weapon class*/ ESM::WeaponType::Thrown, + /* ammo type */ ESM::Weapon::None, + /* flags */ 0 }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "", + /* long group */ "", + /* sound ID */ "Item Ammo", + /* attach bone */ "Bip01 Arrow", + /* sheath bone */ "", + /* usage skill */ ESM::Skill::Marksman, + /* weapon class*/ ESM::WeaponType::Ammo, + /* ammo type */ ESM::Weapon::None, + /* flags */ 0 }; + return value; + } + }; + + template <> + struct Weapon + { + inline static const ESM::WeaponType& getValue() + { + static const ESM::WeaponType value{ /* short group */ "", + /* long group */ "", + /* sound ID */ "Item Ammo", + /* attach bone */ "ArrowBone", + /* sheath bone */ "", + /* usage skill */ ESM::Skill::Marksman, + /* weapon class*/ ESM::WeaponType::Ammo, + /* ammo type */ ESM::Weapon::None, + /* flags */ 0 }; + return value; + } + }; + + MWWorld::ContainerStoreIterator getActiveWeapon(const MWWorld::Ptr& actor, int* weaptype) + { + MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); + CreatureStats& stats = actor.getClass().getCreatureStats(actor); + if (stats.getDrawState() == MWMechanics::DrawState::Spell) { *weaptype = ESM::Weapon::Spell; return inv.end(); } - if(stats.getDrawState() == MWMechanics::DrawState_Weapon) + if (stats.getDrawState() == MWMechanics::DrawState::Weapon) { MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end()) + if (weapon == inv.end()) *weaptype = ESM::Weapon::HandToHand; else { - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) + auto type = weapon->getType(); + if (type == ESM::Weapon::sRecordId) { - const MWWorld::LiveCellRef *ref = weapon->get(); + const MWWorld::LiveCellRef* ref = weapon->get(); *weaptype = ref->mBase->mData.mType; } - else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + else if (type == ESM::Lockpick::sRecordId || type == ESM::Probe::sRecordId) *weaptype = ESM::Weapon::PickProbe; } @@ -39,13 +376,60 @@ namespace MWMechanics const ESM::WeaponType* getWeaponType(const int weaponType) { - std::map::const_iterator found = sWeaponTypeList.find(weaponType); - if (found == sWeaponTypeList.end()) + switch (static_cast(weaponType)) + { + case ESM::Weapon::PickProbe: + return &Weapon::getValue(); + case ESM::Weapon::HandToHand: + return &Weapon::getValue(); + case ESM::Weapon::Spell: + return &Weapon::getValue(); + case ESM::Weapon::None: + return &Weapon::getValue(); + case ESM::Weapon::ShortBladeOneHand: + return &Weapon::getValue(); + case ESM::Weapon::LongBladeOneHand: + return &Weapon::getValue(); + case ESM::Weapon::LongBladeTwoHand: + return &Weapon::getValue(); + case ESM::Weapon::BluntOneHand: + return &Weapon::getValue(); + case ESM::Weapon::BluntTwoClose: + return &Weapon::getValue(); + case ESM::Weapon::BluntTwoWide: + return &Weapon::getValue(); + case ESM::Weapon::SpearTwoWide: + return &Weapon::getValue(); + case ESM::Weapon::AxeOneHand: + return &Weapon::getValue(); + case ESM::Weapon::AxeTwoHand: + return &Weapon::getValue(); + case ESM::Weapon::MarksmanBow: + return &Weapon::getValue(); + case ESM::Weapon::MarksmanCrossbow: + return &Weapon::getValue(); + case ESM::Weapon::MarksmanThrown: + return &Weapon::getValue(); + case ESM::Weapon::Arrow: + return &Weapon::getValue(); + case ESM::Weapon::Bolt: + return &Weapon::getValue(); + } + + return &Weapon::getValue(); + } + + std::vector getAllWeaponTypeShortGroups() + { + // Go via a set to eliminate duplicates. + std::set shortGroupSet; + for (int type = ESM::Weapon::Type::First; type <= ESM::Weapon::Type::Last; type++) { - // Use one-handed short blades as fallback - return &sWeaponTypeList[0]; + std::string_view shortGroup = getWeaponType(type)->mShortGroup; + if (!shortGroup.empty()) + shortGroupSet.insert(shortGroup); } - return &found->second; + return std::vector(shortGroupSet.begin(), shortGroupSet.end()); } } diff --git a/apps/openmw/mwmechanics/weapontype.hpp b/apps/openmw/mwmechanics/weapontype.hpp index 09fa73c065a..efe404d327e 100644 --- a/apps/openmw/mwmechanics/weapontype.hpp +++ b/apps/openmw/mwmechanics/weapontype.hpp @@ -1,269 +1,31 @@ #ifndef GAME_MWMECHANICS_WEAPONTYPE_H #define GAME_MWMECHANICS_WEAPONTYPE_H -#include "../mwworld/inventorystore.hpp" +#include +#include -namespace MWMechanics +namespace ESM { - static std::map sWeaponTypeList = - { - { - ESM::Weapon::None, - { - /* short group */ "", - /* long group */ "", - /* sound ID */ "", - /* attach bone */ "", - /* sheath bone */ "", - /* usage skill */ ESM::Skill::HandToHand, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ 0 - } - }, - { - ESM::Weapon::PickProbe, - { - /* short group */ "1h", - /* long group */ "pickprobe", - /* sound ID */ "", - /* attach bone */ "", - /* sheath bone */ "", - /* usage skill */ ESM::Skill::Security, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ 0 - } - }, - { - ESM::Weapon::Spell, - { - /* short group */ "spell", - /* long group */ "spellcast", - /* sound ID */ "", - /* attach bone */ "", - /* sheath bone */ "", - /* usage skill */ ESM::Skill::HandToHand, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ ESM::WeaponType::TwoHanded - } - }, - { - ESM::Weapon::HandToHand, - { - /* short group */ "hh", - /* long group */ "handtohand", - /* sound ID */ "", - /* attach bone */ "", - /* sheath bone */ "", - /* usage skill */ ESM::Skill::HandToHand, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ ESM::WeaponType::TwoHanded - } - }, - { - ESM::Weapon::ShortBladeOneHand, - { - /* short group */ "1s", - /* long group */ "shortbladeonehand", - /* sound ID */ "Item Weapon Shortblade", - /* attach bone */ "Weapon Bone", - /* sheath bone */ "Bip01 ShortBladeOneHand", - /* usage skill */ ESM::Skill::ShortBlade, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ ESM::WeaponType::HasHealth - } - }, - { - ESM::Weapon::LongBladeOneHand, - { - /* short group */ "1h", - /* long group */ "weapononehand", - /* sound ID */ "Item Weapon Longblade", - /* attach bone */ "Weapon Bone", - /* sheath bone */ "Bip01 LongBladeOneHand", - /* usage skill */ ESM::Skill::LongBlade, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ ESM::WeaponType::HasHealth - } - }, - { - ESM::Weapon::BluntOneHand, - { - /* short group */ "1b", - /* long group */ "bluntonehand", - /* sound ID */ "Item Weapon Blunt", - /* attach bone */ "Weapon Bone", - /* sheath bone */ "Bip01 BluntOneHand", - /* usage skill */ ESM::Skill::BluntWeapon, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ ESM::WeaponType::HasHealth - } - }, - { - ESM::Weapon::AxeOneHand, - { - /* short group */ "1b", - /* long group */ "bluntonehand", - /* sound ID */ "Item Weapon Blunt", - /* attach bone */ "Weapon Bone", - /* sheath bone */ "Bip01 LongBladeOneHand", - /* usage skill */ ESM::Skill::Axe, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ ESM::WeaponType::HasHealth - } - }, - { - ESM::Weapon::LongBladeTwoHand, - { - /* short group */ "2c", - /* long group */ "weapontwohand", - /* sound ID */ "Item Weapon Longblade", - /* attach bone */ "Weapon Bone", - /* sheath bone */ "Bip01 LongBladeTwoClose", - /* usage skill */ ESM::Skill::LongBlade, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded - } - }, - { - ESM::Weapon::AxeTwoHand, - { - /* short group */ "2b", - /* long group */ "blunttwohand", - /* sound ID */ "Item Weapon Blunt", - /* attach bone */ "Weapon Bone", - /* sheath bone */ "Bip01 AxeTwoClose", - /* usage skill */ ESM::Skill::Axe, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded - } - }, - { - ESM::Weapon::BluntTwoClose, - { - /* short group */ "2b", - /* long group */ "blunttwohand", - /* sound ID */ "Item Weapon Blunt", - /* attach bone */ "Weapon Bone", - /* sheath bone */ "Bip01 BluntTwoClose", - /* usage skill */ ESM::Skill::BluntWeapon, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded - } - }, - { - ESM::Weapon::BluntTwoWide, - { - /* short group */ "2w", - /* long group */ "weapontwowide", - /* sound ID */ "Item Weapon Blunt", - /* attach bone */ "Weapon Bone", - /* sheath bone */ "Bip01 BluntTwoWide", - /* usage skill */ ESM::Skill::BluntWeapon, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded - } - }, - { - ESM::Weapon::SpearTwoWide, - { - /* short group */ "2w", - /* long group */ "weapontwowide", - /* sound ID */ "Item Weapon Spear", - /* attach bone */ "Weapon Bone", - /* sheath bone */ "Bip01 SpearTwoWide", - /* usage skill */ ESM::Skill::Spear, - /* weapon class*/ ESM::WeaponType::Melee, - /* ammo type */ ESM::Weapon::None, - /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded - } - }, - { - ESM::Weapon::MarksmanBow, - { - /* short group */ "bow", - /* long group */ "bowandarrow", - /* sound ID */ "Item Weapon Bow", - /* attach bone */ "Weapon Bone Left", - /* sheath bone */ "Bip01 MarksmanBow", - /* usage skill */ ESM::Skill::Marksman, - /* weapon class*/ ESM::WeaponType::Ranged, - /* ammo type */ ESM::Weapon::Arrow, - /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded - } - }, - { - ESM::Weapon::MarksmanCrossbow, - { - /* short group */ "crossbow", - /* long group */ "crossbow", - /* sound ID */ "Item Weapon Crossbow", - /* attach bone */ "Weapon Bone", - /* sheath bone */ "Bip01 MarksmanCrossbow", - /* usage skill */ ESM::Skill::Marksman, - /* weapon class*/ ESM::WeaponType::Ranged, - /* ammo type */ ESM::Weapon::Bolt, - /* flags */ ESM::WeaponType::HasHealth|ESM::WeaponType::TwoHanded - } - }, - { - ESM::Weapon::MarksmanThrown, - { - /* short group */ "1t", - /* long group */ "throwweapon", - /* sound ID */ "Item Weapon Blunt", - /* attach bone */ "Weapon Bone", - /* sheath bone */ "Bip01 MarksmanThrown", - /* usage skill */ ESM::Skill::Marksman, - /* weapon class*/ ESM::WeaponType::Thrown, - /* ammo type */ ESM::Weapon::None, - /* flags */ 0 - } - }, - { - ESM::Weapon::Arrow, - { - /* short group */ "", - /* long group */ "", - /* sound ID */ "Item Ammo", - /* attach bone */ "Bip01 Arrow", - /* sheath bone */ "", - /* usage skill */ ESM::Skill::Marksman, - /* weapon class*/ ESM::WeaponType::Ammo, - /* ammo type */ ESM::Weapon::None, - /* flags */ 0 - } - }, - { - ESM::Weapon::Bolt, - { - /* short group */ "", - /* long group */ "", - /* sound ID */ "Item Ammo", - /* attach bone */ "ArrowBone", - /* sheath bone */ "", - /* usage skill */ ESM::Skill::Marksman, - /* weapon class*/ ESM::WeaponType::Ammo, - /* ammo type */ ESM::Weapon::None, - /* flags */ 0 - } - } - }; + struct WeaponType; +} + +namespace MWWorld +{ + class Ptr; + + template + class ContainerStoreIteratorBase; - MWWorld::ContainerStoreIterator getActiveWeapon(MWWorld::Ptr actor, int *weaptype); + using ContainerStoreIterator = ContainerStoreIteratorBase; +} + +namespace MWMechanics +{ + MWWorld::ContainerStoreIterator getActiveWeapon(const MWWorld::Ptr& actor, int* weaptype); const ESM::WeaponType* getWeaponType(const int weaponType); + + std::vector getAllWeaponTypeShortGroups(); } #endif diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index f0bc2341389..e1efe6d242f 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -1,303 +1,333 @@ #include "actor.hpp" -#include -#include +#include -#include -#include #include #include +#include +#include +#include "../mwmechanics/creaturestats.hpp" #include "../mwworld/class.hpp" #include "collisiontype.hpp" #include "mtphysics.hpp" +#include "trace.h" #include namespace MWPhysics { - -Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler) - : mStandingOnPtr(nullptr), mCanWaterWalk(false), mWalkingOnWater(false) - , mCollisionObject(nullptr), mMeshTranslation(shape->mCollisionBox.center), mHalfExtents(shape->mCollisionBox.extents) - , mVelocity(0,0,0), mStuckFrames(0), mLastStuckPosition{0, 0, 0} - , mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false) - , mInternalCollisionMode(true) - , mExternalCollisionMode(true) - , mTaskScheduler(scheduler) -{ - mPtr = ptr; - - // We can not create actor without collisions - he will fall through the ground. - // In this case we should autogenerate collision box based on mesh shape - // (NPCs have bodyparts and use a different approach) - if (!ptr.getClass().isNpc() && mHalfExtents.length2() == 0.f) + Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, + bool canWaterWalk, DetourNavigator::CollisionShapeType collisionShapeType) + : PtrHolder(ptr, ptr.getRefData().getPosition().asVec3()) + , mStandingOnPtr(nullptr) + , mCanWaterWalk(canWaterWalk) + , mWalkingOnWater(false) + , mMeshTranslation(shape->mCollisionBox.mCenter) + , mOriginalHalfExtents(shape->mCollisionBox.mExtents) + , mStuckFrames(0) + , mLastStuckPosition{ 0, 0, 0 } + , mForce(0.f, 0.f, 0.f) + , mOnGround(ptr.getClass().getCreatureStats(ptr).getFallHeight() == 0) + , mOnSlope(false) + , mInternalCollisionMode(true) + , mExternalCollisionMode(true) + , mActive(false) + , mTaskScheduler(scheduler) { - if (shape->mCollisionShape) + // We can not create actor without collisions - he will fall through the ground. + // In this case we should autogenerate collision box based on mesh shape + // (NPCs have bodyparts and use a different approach) + if (!ptr.getClass().isNpc() && mOriginalHalfExtents.length2() == 0.f) { - btTransform transform; - transform.setIdentity(); - btVector3 min; - btVector3 max; - - shape->mCollisionShape->getAabb(transform, min, max); - mHalfExtents.x() = (max[0] - min[0])/2.f; - mHalfExtents.y() = (max[1] - min[1])/2.f; - mHalfExtents.z() = (max[2] - min[2])/2.f; - - mMeshTranslation = osg::Vec3f(0.f, 0.f, mHalfExtents.z()); + if (shape->mCollisionShape) + { + btTransform transform; + transform.setIdentity(); + btVector3 min; + btVector3 max; + + shape->mCollisionShape->getAabb(transform, min, max); + mOriginalHalfExtents.x() = (max[0] - min[0]) / 2.f; + mOriginalHalfExtents.y() = (max[1] - min[1]) / 2.f; + mOriginalHalfExtents.z() = (max[2] - min[2]) / 2.f; + + mMeshTranslation = osg::Vec3f(0.f, 0.f, mOriginalHalfExtents.z()); + } + + if (mOriginalHalfExtents.length2() == 0.f) + Log(Debug::Error) << "Error: Failed to calculate bounding box for actor \"" + << ptr.getCellRef().getRefId() << "\"."; } - if (mHalfExtents.length2() == 0.f) - Log(Debug::Error) << "Error: Failed to calculate bounding box for actor \"" << ptr.getCellRef().getRefId() << "\"."; - } - - mShape.reset(new btBoxShape(Misc::Convert::toBullet(mHalfExtents))); - mRotationallyInvariant = (mMeshTranslation.x() == 0.0 && mMeshTranslation.y() == 0.0) && std::fabs(mHalfExtents.x() - mHalfExtents.y()) < 2.2; - - mConvexShape = static_cast(mShape.get()); - - mCollisionObject = std::make_unique(); - mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); - mCollisionObject->setActivationState(DISABLE_DEACTIVATION); - mCollisionObject->setCollisionShape(mShape.get()); - mCollisionObject->setUserPointer(this); + const btVector3 halfExtents = Misc::Convert::toBullet(mOriginalHalfExtents); + if ((mMeshTranslation.x() == 0.0 && mMeshTranslation.y() == 0.0) + && std::fabs(mOriginalHalfExtents.x() - mOriginalHalfExtents.y()) < 2.2) + { + switch (collisionShapeType) + { + case DetourNavigator::CollisionShapeType::Aabb: + mShape = std::make_unique(halfExtents); + mRotationallyInvariant = true; + break; + case DetourNavigator::CollisionShapeType::RotatingBox: + mShape = std::make_unique(halfExtents); + mRotationallyInvariant = false; + break; + case DetourNavigator::CollisionShapeType::Cylinder: + mShape = std::make_unique(halfExtents); + mRotationallyInvariant = true; + break; + } + mCollisionShapeType = collisionShapeType; + } + else + { + mShape = std::make_unique(halfExtents); + mRotationallyInvariant = false; + mCollisionShapeType = DetourNavigator::CollisionShapeType::RotatingBox; + } - updateScale(); + mConvexShape = static_cast(mShape.get()); + mConvexShape->setMargin(0.001); // make sure bullet isn't using the huge default convex shape margin of 0.04 - if(!mRotationallyInvariant) - updateRotation(); + mCollisionObject = std::make_unique(); + mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); + mCollisionObject->setActivationState(DISABLE_DEACTIVATION); + mCollisionObject->setCollisionShape(mShape.get()); + mCollisionObject->setUserPointer(this); - updatePosition(); - addCollisionMask(getCollisionMask()); - updateCollisionObjectPosition(); -} + updateScaleUnsafe(); -Actor::~Actor() -{ - mTaskScheduler->removeCollisionObject(mCollisionObject.get()); -} + if (!mRotationallyInvariant) + { + const SceneUtil::PositionAttitudeTransform* baseNode = mPtr.getRefData().getBaseNode(); + if (baseNode) + mRotation = baseNode->getAttitude(); + } -void Actor::enableCollisionMode(bool collision) -{ - mInternalCollisionMode.store(collision, std::memory_order_release); -} + addCollisionMask(getCollisionMask()); + updateCollisionObjectPositionUnsafe(); + } -void Actor::enableCollisionBody(bool collision) -{ - if (mExternalCollisionMode != collision) + Actor::~Actor() { - mExternalCollisionMode = collision; - updateCollisionMask(); + mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } -} -void Actor::addCollisionMask(int collisionMask) -{ - mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Actor, collisionMask); -} + void Actor::enableCollisionMode(bool collision) + { + mInternalCollisionMode = collision; + } -void Actor::updateCollisionMask() -{ - mTaskScheduler->setCollisionFilterMask(mCollisionObject.get(), getCollisionMask()); -} + void Actor::enableCollisionBody(bool collision) + { + if (mExternalCollisionMode != collision) + { + mExternalCollisionMode = collision; + updateCollisionMask(); + } + } -int Actor::getCollisionMask() const -{ - int collisionMask = CollisionType_World | CollisionType_HeightMap; - if (mExternalCollisionMode) - collisionMask |= CollisionType_Actor | CollisionType_Projectile | CollisionType_Door; - if (mCanWaterWalk) - collisionMask |= CollisionType_Water; - return collisionMask; -} + void Actor::addCollisionMask(int collisionMask) + { + mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Actor, collisionMask); + } -void Actor::updatePosition() -{ - std::scoped_lock lock(mPositionMutex); - const auto worldPosition = mPtr.getRefData().getPosition().asVec3(); - mPreviousPosition = worldPosition; - mPosition = worldPosition; - mSimulationPosition = worldPosition; - mPositionOffset = osg::Vec3f(); - mStandingOnPtr = nullptr; - mSkipCollisions = true; -} + void Actor::updateCollisionMask() + { + mTaskScheduler->setCollisionFilterMask(mCollisionObject.get(), getCollisionMask()); + } -void Actor::setSimulationPosition(const osg::Vec3f& position) -{ - mSimulationPosition = position; -} + int Actor::getCollisionMask() const + { + int collisionMask = CollisionType_World | CollisionType_HeightMap; + if (mExternalCollisionMode) + collisionMask |= CollisionType_Actor | CollisionType_Projectile | CollisionType_Door; + if (mCanWaterWalk) + collisionMask |= CollisionType_Water; + return collisionMask; + } -osg::Vec3f Actor::getSimulationPosition() const -{ - return mSimulationPosition; -} + void Actor::updatePosition() + { + std::scoped_lock lock(mPositionMutex); + const auto worldPosition = mPtr.getRefData().getPosition().asVec3(); + mPreviousPosition = worldPosition; + mPosition = worldPosition; + mSimulationPosition = worldPosition; + mPositionOffset = osg::Vec3f(); + mStandingOnPtr = nullptr; + mSkipSimulation = true; + } -osg::Vec3f Actor::getScaledMeshTranslation() const -{ - return mRotation * osg::componentMultiply(mMeshTranslation, mScale); -} + void Actor::setSimulationPosition(const osg::Vec3f& position) + { + if (!std::exchange(mSkipSimulation, false)) + mSimulationPosition = position; + } -void Actor::updateCollisionObjectPosition() -{ - std::scoped_lock lock(mPositionMutex); - mShape->setLocalScaling(Misc::Convert::toBullet(mScale)); - osg::Vec3f scaledTranslation = mRotation * osg::componentMultiply(mMeshTranslation, mScale); - osg::Vec3f newPosition = scaledTranslation + mPosition; - mLocalTransform.setOrigin(Misc::Convert::toBullet(newPosition)); - mLocalTransform.setRotation(Misc::Convert::toBullet(mRotation)); - mCollisionObject->setWorldTransform(mLocalTransform); - mWorldPositionChanged = false; -} + osg::Vec3f Actor::getScaledMeshTranslation() const + { + return mRotation * osg::componentMultiply(mMeshTranslation, mScale); + } -osg::Vec3f Actor::getCollisionObjectPosition() const -{ - std::scoped_lock lock(mPositionMutex); - return Misc::Convert::toOsg(mLocalTransform.getOrigin()); -} + void Actor::updateCollisionObjectPosition() + { + std::scoped_lock lock(mPositionMutex); + updateCollisionObjectPositionUnsafe(); + } -bool Actor::setPosition(const osg::Vec3f& position) -{ - std::scoped_lock lock(mPositionMutex); - applyOffsetChange(); - bool hasChanged = mPosition != position || mWorldPositionChanged; - mPreviousPosition = mPosition; - mPosition = position; - return hasChanged; -} + void Actor::updateCollisionObjectPositionUnsafe() + { + mShape->setLocalScaling(Misc::Convert::toBullet(mScale)); + osg::Vec3f newPosition = getScaledMeshTranslation() + mPosition; -void Actor::adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions) -{ - std::scoped_lock lock(mPositionMutex); - mPositionOffset += offset; - mSkipCollisions = mSkipCollisions || ignoreCollisions; -} + auto& trans = mCollisionObject->getWorldTransform(); + trans.setOrigin(Misc::Convert::toBullet(newPosition)); + trans.setRotation(Misc::Convert::toBullet(mRotation)); + mCollisionObject->setWorldTransform(trans); + } -void Actor::applyOffsetChange() -{ - if (mPositionOffset.length() == 0) - return; - mPosition += mPositionOffset; - mPreviousPosition += mPositionOffset; - mSimulationPosition += mPositionOffset; - mPositionOffset = osg::Vec3f(); - mWorldPositionChanged = true; -} + osg::Vec3f Actor::getCollisionObjectPosition() const + { + std::scoped_lock lock(mPositionMutex); + return getScaledMeshTranslation() + mPosition; + } -osg::Vec3f Actor::getPosition() const -{ - return mPosition; -} + bool Actor::setPosition(const osg::Vec3f& position) + { + std::scoped_lock lock(mPositionMutex); + const bool worldPositionChanged = mPositionOffset.length2() != 0; + applyOffsetChange(); + if (worldPositionChanged || mSkipSimulation) + return true; + mPreviousPosition = mPosition; + mPosition = position; + return mPreviousPosition != mPosition; + } -osg::Vec3f Actor::getPreviousPosition() const -{ - return mPreviousPosition; -} + void Actor::adjustPosition(const osg::Vec3f& offset) + { + std::scoped_lock lock(mPositionMutex); + mPositionOffset += offset; + } -void Actor::updateRotation () -{ - std::scoped_lock lock(mPositionMutex); - mRotation = mPtr.getRefData().getBaseNode()->getAttitude(); -} + osg::Vec3f Actor::applyOffsetChange() + { + if (mPositionOffset.length2() != 0) + { + mPosition += mPositionOffset; + mPreviousPosition += mPositionOffset; + mSimulationPosition += mPositionOffset; + mPositionOffset = osg::Vec3f(); + } + return mPosition; + } -bool Actor::isRotationallyInvariant() const -{ - return mRotationallyInvariant; -} + void Actor::setRotation(osg::Quat quat) + { + std::scoped_lock lock(mPositionMutex); + mRotation = quat; + } -void Actor::updateScale() -{ - std::scoped_lock lock(mPositionMutex); - float scale = mPtr.getCellRef().getScale(); - osg::Vec3f scaleVec(scale,scale,scale); + bool Actor::isRotationallyInvariant() const + { + return mRotationallyInvariant; + } - mPtr.getClass().adjustScale(mPtr, scaleVec, false); - mScale = scaleVec; + void Actor::updateScale() + { + std::scoped_lock lock(mPositionMutex); + updateScaleUnsafe(); + } - scaleVec = osg::Vec3f(scale,scale,scale); - mPtr.getClass().adjustScale(mPtr, scaleVec, true); - mRenderingScale = scaleVec; -} + void Actor::updateScaleUnsafe() + { + float scale = mPtr.getCellRef().getScale(); + osg::Vec3f scaleVec(scale, scale, scale); -osg::Vec3f Actor::getHalfExtents() const -{ - std::scoped_lock lock(mPositionMutex); - return osg::componentMultiply(mHalfExtents, mScale); -} + mPtr.getClass().adjustScale(mPtr, scaleVec, false); + mScale = scaleVec; + mHalfExtents = osg::componentMultiply(mOriginalHalfExtents, scaleVec); -osg::Vec3f Actor::getOriginalHalfExtents() const -{ - return mHalfExtents; -} + scaleVec = osg::Vec3f(scale, scale, scale); + mPtr.getClass().adjustScale(mPtr, scaleVec, true); + mRenderingHalfExtents = osg::componentMultiply(mOriginalHalfExtents, scaleVec); + } -osg::Vec3f Actor::getRenderingHalfExtents() const -{ - std::scoped_lock lock(mPositionMutex); - return osg::componentMultiply(mHalfExtents, mRenderingScale); -} + osg::Vec3f Actor::getHalfExtents() const + { + return mHalfExtents; + } -void Actor::setInertialForce(const osg::Vec3f &force) -{ - mForce = force; -} + osg::Vec3f Actor::getOriginalHalfExtents() const + { + return mOriginalHalfExtents; + } -void Actor::setOnGround(bool grounded) -{ - mOnGround.store(grounded, std::memory_order_release); -} + osg::Vec3f Actor::getRenderingHalfExtents() const + { + return mRenderingHalfExtents; + } -void Actor::setOnSlope(bool slope) -{ - mOnSlope.store(slope, std::memory_order_release); -} + void Actor::setInertialForce(const osg::Vec3f& force) + { + mForce = force; + } -bool Actor::isWalkingOnWater() const -{ - return mWalkingOnWater.load(std::memory_order_acquire); -} + void Actor::setOnGround(bool grounded) + { + mOnGround = grounded; + } -void Actor::setWalkingOnWater(bool walkingOnWater) -{ - mWalkingOnWater.store(walkingOnWater, std::memory_order_release); -} + void Actor::setOnSlope(bool slope) + { + mOnSlope = slope; + } -void Actor::setCanWaterWalk(bool waterWalk) -{ - if (waterWalk != mCanWaterWalk) + bool Actor::isWalkingOnWater() const { - mCanWaterWalk = waterWalk; - updateCollisionMask(); + return mWalkingOnWater; } -} -MWWorld::Ptr Actor::getStandingOnPtr() const -{ - std::scoped_lock lock(mPositionMutex); - return mStandingOnPtr; -} + void Actor::setWalkingOnWater(bool walkingOnWater) + { + mWalkingOnWater = walkingOnWater; + } -void Actor::setStandingOnPtr(const MWWorld::Ptr& ptr) -{ - std::scoped_lock lock(mPositionMutex); - mStandingOnPtr = ptr; -} + void Actor::setCanWaterWalk(bool waterWalk) + { + if (waterWalk != mCanWaterWalk) + { + mCanWaterWalk = waterWalk; + updateCollisionMask(); + } + } -bool Actor::skipCollisions() -{ - return std::exchange(mSkipCollisions, false); -} + MWWorld::Ptr Actor::getStandingOnPtr() const + { + std::scoped_lock lock(mPositionMutex); + return mStandingOnPtr; + } -void Actor::setVelocity(osg::Vec3f velocity) -{ - mVelocity = velocity; -} + void Actor::setStandingOnPtr(const MWWorld::Ptr& ptr) + { + std::scoped_lock lock(mPositionMutex); + mStandingOnPtr = ptr; + } -osg::Vec3f Actor::velocity() -{ - return std::exchange(mVelocity, osg::Vec3f()); -} + bool Actor::canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const + { + const float halfZ = getHalfExtents().z(); + const osg::Vec3f actorPosition = getPosition(); + const osg::Vec3f startingPosition(actorPosition.x(), actorPosition.y(), actorPosition.z() + halfZ); + const osg::Vec3f destinationPosition(actorPosition.x(), actorPosition.y(), waterlevel + halfZ); + MWPhysics::ActorTracer tracer; + tracer.doTrace(getCollisionObject(), startingPosition, destinationPosition, world); + return (tracer.mFraction >= 1.0f); + } } diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp index 7b53e8812a8..e53477506c6 100644 --- a/apps/openmw/mwphysics/actor.hpp +++ b/apps/openmw/mwphysics/actor.hpp @@ -1,23 +1,23 @@ #ifndef OPENMW_MWPHYSICS_ACTOR_H #define OPENMW_MWPHYSICS_ACTOR_H -#include #include #include #include "ptrholder.hpp" -#include -#include +#include + #include +#include class btCollisionShape; -class btCollisionObject; +class btCollisionWorld; class btConvexShape; namespace Resource { - class BulletShape; + struct BulletShape; } namespace MWPhysics @@ -27,28 +27,31 @@ namespace MWPhysics class Actor final : public PtrHolder { public: - Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler); + Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, + bool canWaterWalk, DetourNavigator::CollisionShapeType collisionShapeType); ~Actor() override; + Actor(const Actor&) = delete; + + Actor& operator=(const Actor&) = delete; + /** * Sets the collisionMode for this actor. If disabled, the actor can fly and clip geometry. */ void enableCollisionMode(bool collision); - bool getCollisionMode() const - { - return mInternalCollisionMode.load(std::memory_order_acquire); - } + bool getCollisionMode() const { return mInternalCollisionMode; } btConvexShape* getConvexShape() const { return mConvexShape; } /** - * Enables or disables the *external* collision body. If disabled, other actors will not collide with this actor. + * Enables or disables the *external* collision body. If disabled, other actors will not collide with this + * actor. */ void enableCollisionBody(bool collision); void updateScale(); - void updateRotation(); + void setRotation(osg::Quat quat); /** * Return true if the collision shape looks the same no matter how its Z rotated. @@ -56,11 +59,10 @@ namespace MWPhysics bool isRotationallyInvariant() const; /** - * Used by the physics simulation to store the simulation result. Used in conjunction with mWorldPosition - * to account for e.g. scripted movements - */ + * Used by the physics simulation to store the simulation result. Used in conjunction with mWorldPosition + * to account for e.g. scripted movements + */ void setSimulationPosition(const osg::Vec3f& position); - osg::Vec3f getSimulationPosition() const; void updateCollisionObjectPosition(); @@ -74,72 +76,53 @@ namespace MWPhysics */ osg::Vec3f getOriginalHalfExtents() const; - /// Returns the mesh translation, scaled and rotated as necessary - osg::Vec3f getScaledMeshTranslation() const; - /** * Returns the position of the collision body - * @note The collision shape's origin is in its center, so the position returned can be described as center of the actor collision box in world space. + * @note The collision shape's origin is in its center, so the position returned can be described as center of + * the actor collision box in world space. */ osg::Vec3f getCollisionObjectPosition() const; /** - * Store the current position into mPreviousPosition, then move to this position. - * Returns true if the new position is different. - */ + * Store the current position into mPreviousPosition, then move to this position. + * Returns true if the new position is different. + */ bool setPosition(const osg::Vec3f& position); // force set actor position to be as in Ptr::RefData void updatePosition(); // register a position offset that will be applied during simulation. - void adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions); + void adjustPosition(const osg::Vec3f& offset); // apply position offset. Can't be called during simulation - void applyOffsetChange(); - - osg::Vec3f getPosition() const; - - osg::Vec3f getPreviousPosition() const; + osg::Vec3f applyOffsetChange(); /** * Returns the half extents of the collision body (scaled according to rendering scale) - * @note The reason we need this extra method is because of an inconsistency in MW - NPC race scales aren't applied to the collision shape, - * most likely to make environment collision testing easier. However in some cases (swimming level) we want the actual scale. + * @note The reason we need this extra method is because of an inconsistency in MW - NPC race scales aren't + * applied to the collision shape, most likely to make environment collision testing easier. However in some + * cases (swimming level) we want the actual scale. */ osg::Vec3f getRenderingHalfExtents() const; /** * Sets the current amount of inertial force (incl. gravity) affecting this physic actor */ - void setInertialForce(const osg::Vec3f &force); + void setInertialForce(const osg::Vec3f& force); /** * Gets the current amount of inertial force (incl. gravity) affecting this physic actor */ - const osg::Vec3f &getInertialForce() const - { - return mForce; - } + const osg::Vec3f& getInertialForce() const { return mForce; } void setOnGround(bool grounded); - bool getOnGround() const - { - return mInternalCollisionMode.load(std::memory_order_acquire) && mOnGround.load(std::memory_order_acquire); - } + bool getOnGround() const { return mOnGround; } void setOnSlope(bool slope); - bool getOnSlope() const - { - return mInternalCollisionMode.load(std::memory_order_acquire) && mOnSlope.load(std::memory_order_acquire); - } - - btCollisionObject* getCollisionObject() const - { - return mCollisionObject.get(); - } + bool getOnSlope() const { return mOnSlope; } /// Sets whether this actor should be able to collide with the water surface void setCanWaterWalk(bool waterWalk); @@ -151,28 +134,19 @@ namespace MWPhysics MWWorld::Ptr getStandingOnPtr() const; void setStandingOnPtr(const MWWorld::Ptr& ptr); - unsigned int getStuckFrames() const - { - return mStuckFrames; - } - void setStuckFrames(unsigned int frames) - { - mStuckFrames = frames; - } - - const osg::Vec3f &getLastStuckPosition() const - { - return mLastStuckPosition; - } - void setLastStuckPosition(osg::Vec3f position) - { - mLastStuckPosition = position; - } - - bool skipCollisions(); - - void setVelocity(osg::Vec3f velocity); - osg::Vec3f velocity(); + unsigned int getStuckFrames() const { return mStuckFrames; } + void setStuckFrames(unsigned int frames) { mStuckFrames = frames; } + + const osg::Vec3f& getLastStuckPosition() const { return mLastStuckPosition; } + void setLastStuckPosition(osg::Vec3f position) { mLastStuckPosition = position; } + + bool canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const; + + bool isActive() const { return mActive; } + + void setActive(bool value) { mActive = value; } + + DetourNavigator::CollisionShapeType getCollisionShapeType() const { return mCollisionShapeType; } private: MWWorld::Ptr mStandingOnPtr; @@ -181,48 +155,47 @@ namespace MWPhysics void addCollisionMask(int collisionMask); int getCollisionMask() const; + /// Returns the mesh translation, scaled and rotated as necessary + osg::Vec3f getScaledMeshTranslation() const; + bool mCanWaterWalk; - std::atomic mWalkingOnWater; + bool mWalkingOnWater; bool mRotationallyInvariant; + DetourNavigator::CollisionShapeType mCollisionShapeType; + std::unique_ptr mShape; btConvexShape* mConvexShape; - std::unique_ptr mCollisionObject; - osg::Vec3f mMeshTranslation; + osg::Vec3f mOriginalHalfExtents; osg::Vec3f mHalfExtents; + osg::Vec3f mRenderingHalfExtents; osg::Quat mRotation; osg::Vec3f mScale; - osg::Vec3f mRenderingScale; - osg::Vec3f mSimulationPosition; - osg::Vec3f mPosition; - osg::Vec3f mPreviousPosition; osg::Vec3f mPositionOffset; - osg::Vec3f mVelocity; - bool mWorldPositionChanged; - bool mSkipCollisions; - btTransform mLocalTransform; + bool mSkipSimulation = true; mutable std::mutex mPositionMutex; unsigned int mStuckFrames; osg::Vec3f mLastStuckPosition; osg::Vec3f mForce; - std::atomic mOnGround; - std::atomic mOnSlope; - std::atomic mInternalCollisionMode; + bool mOnGround; + bool mOnSlope; + bool mInternalCollisionMode; bool mExternalCollisionMode; + bool mActive; PhysicsTaskScheduler* mTaskScheduler; - Actor(const Actor&); - Actor& operator=(const Actor&); + inline void updateScaleUnsafe(); + + inline void updateCollisionObjectPositionUnsafe(); }; } - #endif diff --git a/apps/openmw/mwphysics/actorconvexcallback.cpp b/apps/openmw/mwphysics/actorconvexcallback.cpp index ef52e07c21b..72bb0eff461 100644 --- a/apps/openmw/mwphysics/actorconvexcallback.cpp +++ b/apps/openmw/mwphysics/actorconvexcallback.cpp @@ -9,58 +9,56 @@ namespace MWPhysics { - class ActorOverlapTester : public btCollisionWorld::ContactResultCallback + namespace { - public: - bool overlapping = false; - - btScalar addSingleResult(btManifoldPoint& cp, - const btCollisionObjectWrapper* colObj0Wrap, - int partId0, - int index0, - const btCollisionObjectWrapper* colObj1Wrap, - int partId1, - int index1) override + struct ActorOverlapTester : public btCollisionWorld::ContactResultCallback { - if(cp.getDistance() <= 0.0f) - overlapping = true; - return btScalar(1); - } - }; + bool mOverlapping = false; - ActorConvexCallback::ActorConvexCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot, const btCollisionWorld * world) - : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)), - mMe(me), mMotion(motion), mMinCollisionDot(minCollisionDot), mWorld(world) - { + btScalar addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* /*colObj0Wrap*/, + int /*partId0*/, int /*index0*/, const btCollisionObjectWrapper* /*colObj1Wrap*/, int /*partId1*/, + int /*index1*/) override + { + if (cp.getDistance() <= 0.0f) + mOverlapping = true; + return 1; + } + }; } - btScalar ActorConvexCallback::addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) + btScalar ActorConvexCallback::addSingleResult( + btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) { if (convexResult.m_hitCollisionObject == mMe) - return btScalar(1); + return 1; // override data for actor-actor collisions - // vanilla Morrowind seems to make overlapping actors collide as though they are both cylinders with a diameter of the distance between them - // For some reason this doesn't work as well as it should when using capsules, but it still helps a lot. - if(convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor) + // vanilla Morrowind seems to make overlapping actors collide as though they are both cylinders with a diameter + // of the distance between them For some reason this doesn't work as well as it should when using capsules, but + // it still helps a lot. + if (convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor) { ActorOverlapTester isOverlapping; - // FIXME: This is absolutely terrible and bullet should feel terrible for not making contactPairTest const-correct. - ContactTestWrapper::contactPairTest(const_cast(mWorld), const_cast(mMe), const_cast(convexResult.m_hitCollisionObject), isOverlapping); + // FIXME: This is absolutely terrible and bullet should feel terrible for not making contactPairTest + // const-correct. + ContactTestWrapper::contactPairTest(const_cast(mWorld), + const_cast(mMe), const_cast(convexResult.m_hitCollisionObject), + isOverlapping); - if(isOverlapping.overlapping) + if (isOverlapping.mOverlapping) { auto originA = Misc::Convert::toOsg(mMe->getWorldTransform().getOrigin()); auto originB = Misc::Convert::toOsg(convexResult.m_hitCollisionObject->getWorldTransform().getOrigin()); osg::Vec3f motion = Misc::Convert::toOsg(mMotion); - osg::Vec3f normal = (originA-originB); + osg::Vec3f normal = (originA - originB); normal.z() = 0; normal.normalize(); - // only collide if horizontally moving towards the hit actor (note: the motion vector appears to be inverted) - // FIXME: This kinda screws with standing on actors that walk up slopes for some reason. Makes you fall through them. - // It happens in vanilla Morrowind too, but much less often. - // I tried hunting down why but couldn't figure it out. Possibly a stair stepping or ground ejection bug. - if(normal * motion > 0.0f) + // only collide if horizontally moving towards the hit actor (note: the motion vector appears to be + // inverted) + // FIXME: This kinda screws with standing on actors that walk up slopes for some reason. Makes you fall + // through them. It happens in vanilla Morrowind too, but much less often. I tried hunting down why but + // couldn't figure it out. Possibly a stair stepping or ground ejection bug. + if (normal * motion > 0.0f) { convexResult.m_hitFraction = 0.0f; convexResult.m_hitNormalLocal = Misc::Convert::toBullet(normal); @@ -68,20 +66,19 @@ namespace MWPhysics } else { - return btScalar(1); + return 1; } } } - if (convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Projectile) + if (convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup + == CollisionType_Projectile) { auto* projectileHolder = static_cast(convexResult.m_hitCollisionObject->getUserPointer()); if (!projectileHolder->isActive()) - return btScalar(1); - auto* targetHolder = static_cast(mMe->getUserPointer()); - const MWWorld::Ptr target = targetHolder->getPtr(); - if (projectileHolder->isValidTarget(target)) - projectileHolder->hit(target, convexResult.m_hitPointLocal, convexResult.m_hitNormalLocal); - return btScalar(1); + return 1; + if (projectileHolder->isValidTarget(mMe)) + projectileHolder->hit(mMe, convexResult.m_hitPointLocal, convexResult.m_hitNormalLocal); + return 1; } btVector3 hitNormalWorld; @@ -89,14 +86,15 @@ namespace MWPhysics hitNormalWorld = convexResult.m_hitNormalLocal; else { - ///need to transform normal into worldspace - hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal; + /// need to transform normal into worldspace + hitNormalWorld + = convexResult.m_hitCollisionObject->getWorldTransform().getBasis() * convexResult.m_hitNormalLocal; } // dot product of the motion vector against the collision contact normal btScalar dotCollision = mMotion.dot(hitNormalWorld); if (dotCollision <= mMinCollisionDot) - return btScalar(1); + return 1; return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace); } diff --git a/apps/openmw/mwphysics/actorconvexcallback.hpp b/apps/openmw/mwphysics/actorconvexcallback.hpp index 1c28ee6cc49..8442097a092 100644 --- a/apps/openmw/mwphysics/actorconvexcallback.hpp +++ b/apps/openmw/mwphysics/actorconvexcallback.hpp @@ -10,15 +10,23 @@ namespace MWPhysics class ActorConvexCallback : public btCollisionWorld::ClosestConvexResultCallback { public: - ActorConvexCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot, const btCollisionWorld * world); + explicit ActorConvexCallback(const btCollisionObject* me, const btVector3& motion, btScalar minCollisionDot, + const btCollisionWorld* world) + : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)) + , mMe(me) + , mMotion(motion) + , mMinCollisionDot(minCollisionDot) + , mWorld(world) + { + } - btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) override; + btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) override; protected: - const btCollisionObject *mMe; + const btCollisionObject* mMe; const btVector3 mMotion; const btScalar mMinCollisionDot; - const btCollisionWorld * mWorld; + const btCollisionWorld* mWorld; }; } diff --git a/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp b/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp index 32d97d6c750..b63cd568a8a 100644 --- a/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp +++ b/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp @@ -1,35 +1,24 @@ #include "closestnotmerayresultcallback.hpp" #include -#include #include -#include "../mwworld/class.hpp" - -#include "ptrholder.hpp" +#include "collisiontype.hpp" namespace MWPhysics { - ClosestNotMeRayResultCallback::ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector targets, const btVector3& from, const btVector3& to) - : btCollisionWorld::ClosestRayResultCallback(from, to) - , mMe(me), mTargets(std::move(targets)) - { - } - - btScalar ClosestNotMeRayResultCallback::addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) + btScalar ClosestNotMeRayResultCallback::addSingleResult( + btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) { - if (rayResult.m_collisionObject == mMe) + const auto* hitObject = rayResult.m_collisionObject; + if (std::find(mIgnoreList.begin(), mIgnoreList.end(), hitObject) != mIgnoreList.end()) return 1.f; - if (!mTargets.empty()) + if (hitObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor && !mTargets.empty()) { - if ((std::find(mTargets.begin(), mTargets.end(), rayResult.m_collisionObject) == mTargets.end())) - { - auto* holder = static_cast(rayResult.m_collisionObject->getUserPointer()); - if (holder && !holder->getPtr().isEmpty() && holder->getPtr().getClass().isActor()) - return 1.f; - } + if ((std::find(mTargets.begin(), mTargets.end(), hitObject) == mTargets.end())) + return 1.f; } return btCollisionWorld::ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace); diff --git a/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp b/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp index 1fa32ef686e..660f24424d6 100644 --- a/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp +++ b/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_MWPHYSICS_CLOSESTNOTMERAYRESULTCALLBACK_H #define OPENMW_MWPHYSICS_CLOSESTNOTMERAYRESULTCALLBACK_H -#include +#include #include @@ -14,13 +14,19 @@ namespace MWPhysics class ClosestNotMeRayResultCallback : public btCollisionWorld::ClosestRayResultCallback { public: - ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector targets, const btVector3& from, const btVector3& to); + explicit ClosestNotMeRayResultCallback(std::span ignore, + std::span targets, const btVector3& from, const btVector3& to) + : btCollisionWorld::ClosestRayResultCallback(from, to) + , mIgnoreList(ignore) + , mTargets(targets) + { + } btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) override; private: - const btCollisionObject* mMe; - const std::vector mTargets; + const std::span mIgnoreList; + const std::span mTargets; }; } diff --git a/apps/openmw/mwphysics/collisiontype.hpp b/apps/openmw/mwphysics/collisiontype.hpp index 0d6a32fc092..5dda2a6aebd 100644 --- a/apps/openmw/mwphysics/collisiontype.hpp +++ b/apps/openmw/mwphysics/collisiontype.hpp @@ -4,14 +4,21 @@ namespace MWPhysics { -enum CollisionType { - CollisionType_World = 1<<0, - CollisionType_Door = 1<<1, - CollisionType_Actor = 1<<2, - CollisionType_HeightMap = 1<<3, - CollisionType_Projectile = 1<<4, - CollisionType_Water = 1<<5 -}; + enum CollisionType + { + CollisionType_World = 1 << 0, + CollisionType_Door = 1 << 1, + CollisionType_Actor = 1 << 2, + CollisionType_HeightMap = 1 << 3, + CollisionType_Projectile = 1 << 4, + CollisionType_Water = 1 << 5, + CollisionType_Default + = CollisionType_World | CollisionType_HeightMap | CollisionType_Actor | CollisionType_Door, + CollisionType_AnyPhysical = CollisionType_World | CollisionType_HeightMap | CollisionType_Actor + | CollisionType_Door | CollisionType_Projectile | CollisionType_Water, + CollisionType_CameraOnly = 1 << 6, + CollisionType_VisualOnly = 1 << 7 + }; } diff --git a/apps/openmw/mwphysics/constants.hpp b/apps/openmw/mwphysics/constants.hpp index eaeb308d582..0b3c71408e9 100644 --- a/apps/openmw/mwphysics/constants.hpp +++ b/apps/openmw/mwphysics/constants.hpp @@ -3,23 +3,23 @@ namespace MWPhysics { - static constexpr float sStepSizeUp = 34.0f; static constexpr float sStepSizeDown = 62.0f; static constexpr float sMinStep = 10.0f; // hack to skip over tiny unwalkable slopes static constexpr float sMinStep2 = 20.0f; // hack to skip over shorter but longer/wider/further unwalkable slopes - // whether to do the above stairstepping logic hacks to work around bad morrowind assets - disabling causes problems but improves performance + // whether to do the above stairstepping logic hacks to work around bad morrowind assets - disabling causes problems + // but improves performance static constexpr bool sDoExtraStairHacks = true; static constexpr float sGroundOffset = 1.0f; - static constexpr float sMaxSlope = 49.0f; // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. static constexpr int sMaxIterations = 8; // Allows for more precise movement solving without getting stuck or snagging too easily. - static constexpr float sCollisionMargin = 0.1f; - // Allow for a small amount of penetration to prevent numerical precision issues from causing the "unstuck"ing code to run unnecessarily - // Currently set to 0 because having the "unstuck"ing code run whenever possible prevents some glitchy snagging issues + static constexpr float sCollisionMargin = 0.2f; + // Allow for a small amount of penetration to prevent numerical precision issues from causing the "unstuck"ing code + // to run unnecessarily Currently set to 0 because having the "unstuck"ing code run whenever possible prevents some + // glitchy snagging issues static constexpr float sAllowedPenetration = 0.0f; } diff --git a/apps/openmw/mwphysics/contacttestresultcallback.cpp b/apps/openmw/mwphysics/contacttestresultcallback.cpp index 5829ee02f51..45d1127de43 100644 --- a/apps/openmw/mwphysics/contacttestresultcallback.cpp +++ b/apps/openmw/mwphysics/contacttestresultcallback.cpp @@ -8,21 +8,16 @@ namespace MWPhysics { - ContactTestResultCallback::ContactTestResultCallback(const btCollisionObject* testedAgainst) - : mTestedAgainst(testedAgainst) - { - } - - btScalar ContactTestResultCallback::addSingleResult(btManifoldPoint& cp, - const btCollisionObjectWrapper* col0Wrap,int partId0,int index0, - const btCollisionObjectWrapper* col1Wrap,int partId1,int index1) + btScalar ContactTestResultCallback::addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* col0Wrap, + int /*partId0*/, int /*index0*/, const btCollisionObjectWrapper* col1Wrap, int /*partId1*/, int /*index1*/) { const btCollisionObject* collisionObject = col0Wrap->m_collisionObject; if (collisionObject == mTestedAgainst) collisionObject = col1Wrap->m_collisionObject; PtrHolder* holder = static_cast(collisionObject->getUserPointer()); if (holder) - mResult.emplace_back(ContactPoint{holder->getPtr(), Misc::Convert::toOsg(cp.m_positionWorldOnB), Misc::Convert::toOsg(cp.m_normalWorldOnB)}); + mResult.emplace_back(ContactPoint{ holder->getPtr(), Misc::Convert::toOsg(cp.m_positionWorldOnB), + Misc::Convert::toOsg(cp.m_normalWorldOnB) }); return 0.f; } diff --git a/apps/openmw/mwphysics/contacttestresultcallback.hpp b/apps/openmw/mwphysics/contacttestresultcallback.hpp index 3d1b3b8aab0..a9ba06368c1 100644 --- a/apps/openmw/mwphysics/contacttestresultcallback.hpp +++ b/apps/openmw/mwphysics/contacttestresultcallback.hpp @@ -14,16 +14,19 @@ namespace MWPhysics { class ContactTestResultCallback : public btCollisionWorld::ContactResultCallback { - const btCollisionObject* mTestedAgainst; - public: - ContactTestResultCallback(const btCollisionObject* testedAgainst); + explicit ContactTestResultCallback(const btCollisionObject* testedAgainst) + : mTestedAgainst(testedAgainst) + { + } - btScalar addSingleResult(btManifoldPoint& cp, - const btCollisionObjectWrapper* col0Wrap,int partId0,int index0, - const btCollisionObjectWrapper* col1Wrap,int partId1,int index1) override; + btScalar addSingleResult(btManifoldPoint& cp, const btCollisionObjectWrapper* col0Wrap, int partId0, int index0, + const btCollisionObjectWrapper* col1Wrap, int partId1, int index1) override; std::vector mResult; + + private: + const btCollisionObject* mTestedAgainst; }; } diff --git a/apps/openmw/mwphysics/contacttestwrapper.cpp b/apps/openmw/mwphysics/contacttestwrapper.cpp index c11a7e29263..d5da9e67e35 100644 --- a/apps/openmw/mwphysics/contacttestwrapper.cpp +++ b/apps/openmw/mwphysics/contacttestwrapper.cpp @@ -6,13 +6,15 @@ namespace MWPhysics { // Concurrent calls to contactPairTest (and by extension contactTest) are forbidden. static std::mutex contactMutex; - void ContactTestWrapper::contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) + void ContactTestWrapper::contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, + btCollisionWorld::ContactResultCallback& resultCallback) { std::unique_lock lock(contactMutex); collisionWorld->contactTest(colObj, resultCallback); } - void ContactTestWrapper::contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback) + void ContactTestWrapper::contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, + btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback) { std::unique_lock lock(contactMutex); collisionWorld->contactPairTest(colObjA, colObjB, resultCallback); diff --git a/apps/openmw/mwphysics/contacttestwrapper.h b/apps/openmw/mwphysics/contacttestwrapper.h index b3b6edc59ae..6a53f9ae276 100644 --- a/apps/openmw/mwphysics/contacttestwrapper.h +++ b/apps/openmw/mwphysics/contacttestwrapper.h @@ -7,8 +7,10 @@ namespace MWPhysics { struct ContactTestWrapper { - static void contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback); - static void contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback); + static void contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, + btCollisionWorld::ContactResultCallback& resultCallback); + static void contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, + btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback); }; } #endif diff --git a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp b/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp deleted file mode 100644 index 7744af14b5a..00000000000 --- a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "deepestnotmecontacttestresultcallback.hpp" - -#include - -#include - -#include "../mwworld/class.hpp" - -#include "ptrholder.hpp" - -namespace MWPhysics -{ - - DeepestNotMeContactTestResultCallback::DeepestNotMeContactTestResultCallback(const btCollisionObject* me, const std::vector& targets, const btVector3 &origin) - : mMe(me), mTargets(targets), mOrigin(origin), mLeastDistSqr(std::numeric_limits::max()) - { - } - - btScalar DeepestNotMeContactTestResultCallback::addSingleResult(btManifoldPoint& cp, - const btCollisionObjectWrapper* col0Wrap,int partId0,int index0, - const btCollisionObjectWrapper* col1Wrap,int partId1,int index1) - { - const btCollisionObject* collisionObject = col1Wrap->m_collisionObject; - if (collisionObject != mMe) - { - if (!mTargets.empty()) - { - if ((std::find(mTargets.begin(), mTargets.end(), collisionObject) == mTargets.end())) - { - PtrHolder* holder = static_cast(collisionObject->getUserPointer()); - if (holder && !holder->getPtr().isEmpty() && holder->getPtr().getClass().isActor()) - return 0.f; - } - } - - btScalar distsqr = mOrigin.distance2(cp.getPositionWorldOnA()); - if(!mObject || distsqr < mLeastDistSqr) - { - mObject = collisionObject; - mLeastDistSqr = distsqr; - mContactPoint = cp.getPositionWorldOnA(); - mContactNormal = cp.m_normalWorldOnB; - } - } - - return 0.f; - } -} diff --git a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.hpp b/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.hpp deleted file mode 100644 index 00cb5c01f73..00000000000 --- a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef OPENMW_MWPHYSICS_DEEPESTNOTMECONTACTTESTRESULTCALLBACK_H -#define OPENMW_MWPHYSICS_DEEPESTNOTMECONTACTTESTRESULTCALLBACK_H - -#include - -#include - -class btCollisionObject; - -namespace MWPhysics -{ - class DeepestNotMeContactTestResultCallback : public btCollisionWorld::ContactResultCallback - { - const btCollisionObject* mMe; - const std::vector mTargets; - - // Store the real origin, since the shape's origin is its center - btVector3 mOrigin; - - public: - const btCollisionObject *mObject{nullptr}; - btVector3 mContactPoint{0,0,0}; - btVector3 mContactNormal{0,0,0}; - btScalar mLeastDistSqr; - - DeepestNotMeContactTestResultCallback(const btCollisionObject* me, const std::vector& targets, const btVector3 &origin); - - btScalar addSingleResult(btManifoldPoint& cp, - const btCollisionObjectWrapper* col0Wrap,int partId0,int index0, - const btCollisionObjectWrapper* col1Wrap,int partId1,int index1) override; - }; -} - -#endif diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index 275325cf676..c1fa0ad1ee9 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -1,63 +1,64 @@ #ifndef OPENMW_MWPHYSICS_HASSPHERECOLLISIONCALLBACK_H #define OPENMW_MWPHYSICS_HASSPHERECOLLISIONCALLBACK_H -#include #include #include -#include +#include #include namespace MWPhysics { // https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection - bool testAabbAgainstSphere(const btVector3& aabbMin, const btVector3& aabbMax, - const btVector3& position, const btScalar radius) + bool testAabbAgainstSphere( + const btVector3& aabbMin, const btVector3& aabbMax, const btVector3& position, const btScalar radius) { - const btVector3 nearest( - std::max(aabbMin.x(), std::min(aabbMax.x(), position.x())), - std::max(aabbMin.y(), std::min(aabbMax.y(), position.y())), - std::max(aabbMin.z(), std::min(aabbMax.z(), position.z())) - ); + const btVector3 nearest(std::clamp(position.x(), aabbMin.x(), aabbMax.x()), + std::clamp(position.y(), aabbMin.y(), aabbMax.y()), std::clamp(position.z(), aabbMin.z(), aabbMax.z())); return nearest.distance(position) < radius; } + template class HasSphereCollisionCallback final : public btBroadphaseAabbCallback { public: - HasSphereCollisionCallback(const btVector3& position, const btScalar radius, btCollisionObject* object, - const int mask, const int group) - : mPosition(position), - mRadius(radius), - mCollisionObject(object), - mCollisionFilterMask(mask), - mCollisionFilterGroup(group) + HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, const int group, + const Ignore& ignore, OnCollision* onCollision) + : mPosition(position) + , mRadius(radius) + , mIgnore(ignore) + , mCollisionFilterMask(mask) + , mCollisionFilterGroup(group) + , mOnCollision(onCollision) { } bool process(const btBroadphaseProxy* proxy) override { - if (mResult) + if (mResult && mOnCollision == nullptr) return false; const auto collisionObject = static_cast(proxy->m_clientObject); - if (collisionObject == mCollisionObject) + if (mIgnore(collisionObject) || !needsCollision(*proxy) + || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) + return true; + mResult = true; + if (mOnCollision != nullptr) + { + (*mOnCollision)(collisionObject); return true; - if (needsCollision(*proxy)) - mResult = testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius); + } return !mResult; } - bool getResult() const - { - return mResult; - } + bool getResult() const { return mResult; } private: btVector3 mPosition; btScalar mRadius; - btCollisionObject* mCollisionObject; + Ignore mIgnore; int mCollisionFilterMask; int mCollisionFilterGroup; + OnCollision* mOnCollision; bool mResult = false; bool needsCollision(const btBroadphaseProxy& proxy) const diff --git a/apps/openmw/mwphysics/heightfield.cpp b/apps/openmw/mwphysics/heightfield.cpp index e210bc39039..4ec8dda8814 100644 --- a/apps/openmw/mwphysics/heightfield.cpp +++ b/apps/openmw/mwphysics/heightfield.cpp @@ -1,10 +1,12 @@ #include "heightfield.hpp" #include "mtphysics.hpp" +#include + #include -#include #include +#include #include @@ -19,17 +21,17 @@ namespace { template - auto makeHeights(const T* heights, float sqrtVerts) + auto makeHeights(const T* heights, int verts) -> std::enable_if_t::value, std::vector> { return {}; } template - auto makeHeights(const T* heights, float sqrtVerts) + auto makeHeights(const T* heights, int verts) -> std::enable_if_t::value, std::vector> { - return std::vector(heights, heights + static_cast(sqrtVerts * sqrtVerts)); + return std::vector(heights, heights + static_cast(verts * verts)); } template @@ -50,27 +52,24 @@ namespace namespace MWPhysics { - HeightField::HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject, PhysicsTaskScheduler* scheduler) + HeightField::HeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, + const osg::Object* holdObject, PhysicsTaskScheduler* scheduler) : mHoldObject(holdObject) #if BT_BULLET_VERSION < 310 - , mHeights(makeHeights(heights, sqrtVerts)) + , mHeights(makeHeights(heights, verts)) #endif , mTaskScheduler(scheduler) { #if BT_BULLET_VERSION < 310 mShape = std::make_unique( - sqrtVerts, sqrtVerts, - getHeights(heights, mHeights), - 1, - minH, maxH, 2, - PHY_FLOAT, false - ); + verts, verts, getHeights(heights, mHeights), 1, minH, maxH, 2, PHY_FLOAT, false); #else - mShape = std::make_unique( - sqrtVerts, sqrtVerts, heights, minH, maxH, 2, false); + mShape = std::make_unique(verts, verts, heights, minH, maxH, 2, false); #endif mShape->setUseDiamondSubdivision(true); - mShape->setLocalScaling(btVector3(triSize, triSize, 1)); + + const float scaling = static_cast(size) / static_cast(verts - 1); + mShape->setLocalScaling(btVector3(scaling, scaling, 1)); #if BT_BULLET_VERSION >= 289 // Accelerates some collision tests. @@ -81,15 +80,14 @@ namespace MWPhysics mShape->buildAccelerator(); #endif - btTransform transform(btQuaternion::getIdentity(), - btVector3((x+0.5f) * triSize * (sqrtVerts-1), - (y+0.5f) * triSize * (sqrtVerts-1), - (maxH+minH)*0.5f)); + const btTransform transform( + btQuaternion::getIdentity(), BulletHelpers::getHeightfieldShift(x, y, size, minH, maxH)); mCollisionObject = std::make_unique(); mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setWorldTransform(transform); - mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_HeightMap, CollisionType_Actor|CollisionType_Projectile); + mTaskScheduler->addCollisionObject( + mCollisionObject.get(), CollisionType_HeightMap, CollisionType_Actor | CollisionType_Projectile); } HeightField::~HeightField() diff --git a/apps/openmw/mwphysics/heightfield.hpp b/apps/openmw/mwphysics/heightfield.hpp index 93b2733f31e..a54685fda15 100644 --- a/apps/openmw/mwphysics/heightfield.hpp +++ b/apps/openmw/mwphysics/heightfield.hpp @@ -23,7 +23,8 @@ namespace MWPhysics class HeightField { public: - HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject, PhysicsTaskScheduler* scheduler); + HeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, + const osg::Object* holdObject, PhysicsTaskScheduler* scheduler); ~HeightField(); btCollisionObject* getCollisionObject(); diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index fd0e090fcbe..05b9f44654f 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -2,23 +2,23 @@ #include #include -#include +#include -#include +#include #include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" -#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwworld/refdata.hpp" #include "actor.hpp" #include "collisiontype.hpp" #include "constants.hpp" #include "contacttestwrapper.h" +#include "object.hpp" #include "physicssystem.hpp" +#include "projectile.hpp" +#include "projectileconvexcallback.hpp" #include "stepper.hpp" #include "trace.h" @@ -26,63 +26,72 @@ namespace MWPhysics { - static bool isActor(const btCollisionObject *obj) + static bool isActor(const btCollisionObject* obj) { assert(obj); return obj->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor; } - class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback + namespace { - public: - ContactCollectionCallback(const btCollisionObject * me, osg::Vec3f velocity) : mMe(me) + class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback { - m_collisionFilterGroup = me->getBroadphaseHandle()->m_collisionFilterGroup; - m_collisionFilterMask = me->getBroadphaseHandle()->m_collisionFilterMask & ~CollisionType_Projectile; - mVelocity = Misc::Convert::toBullet(velocity); - } - btScalar addSingleResult(btManifoldPoint & contact, const btCollisionObjectWrapper * colObj0Wrap, int partId0, int index0, const btCollisionObjectWrapper * colObj1Wrap, int partId1, int index1) override - { - if (isActor(colObj0Wrap->getCollisionObject()) && isActor(colObj1Wrap->getCollisionObject())) - return 0.0; - // ignore overlap if we're moving in the same direction as it would push us out (don't change this to >=, that would break detection when not moving) - if (contact.m_normalWorldOnB.dot(mVelocity) > 0.0) - return 0.0; - auto delta = contact.m_normalWorldOnB * -contact.m_distance1; - mContactSum += delta; - mMaxX = std::max(std::abs(delta.x()), mMaxX); - mMaxY = std::max(std::abs(delta.y()), mMaxY); - mMaxZ = std::max(std::abs(delta.z()), mMaxZ); - if (contact.m_distance1 < mDistance) + public: + explicit ContactCollectionCallback(const btCollisionObject& me, const osg::Vec3f& velocity) + : mVelocity(Misc::Convert::toBullet(velocity)) { - mDistance = contact.m_distance1; - mNormal = contact.m_normalWorldOnB; - mDelta = delta; - return mDistance; + m_collisionFilterGroup = me.getBroadphaseHandle()->m_collisionFilterGroup; + m_collisionFilterMask = me.getBroadphaseHandle()->m_collisionFilterMask & ~CollisionType_Projectile; } - else + + btScalar addSingleResult(btManifoldPoint& contact, const btCollisionObjectWrapper* colObj0Wrap, + int /*partId0*/, int /*index0*/, const btCollisionObjectWrapper* colObj1Wrap, int /*partId1*/, + int /*index1*/) override { - return 0.0; + if (isActor(colObj0Wrap->getCollisionObject()) && isActor(colObj1Wrap->getCollisionObject())) + return 0.0; + // ignore overlap if we're moving in the same direction as it would push us out (don't change this to + // >=, that would break detection when not moving) + if (contact.m_normalWorldOnB.dot(mVelocity) > 0.0) + return 0.0; + auto delta = contact.m_normalWorldOnB * -contact.m_distance1; + mContactSum += delta; + mMaxX = std::max(std::abs(delta.x()), mMaxX); + mMaxY = std::max(std::abs(delta.y()), mMaxY); + mMaxZ = std::max(std::abs(delta.z()), mMaxZ); + if (contact.m_distance1 < mDistance) + { + mDistance = contact.m_distance1; + mNormal = contact.m_normalWorldOnB; + mDelta = delta; + return mDistance; + } + else + { + return 0.0; + } } - } - btScalar mMaxX = 0.0; - btScalar mMaxY = 0.0; - btScalar mMaxZ = 0.0; - btVector3 mContactSum{0.0, 0.0, 0.0}; - btVector3 mNormal{0.0, 0.0, 0.0}; // points towards "me" - btVector3 mDelta{0.0, 0.0, 0.0}; // points towards "me" - btScalar mDistance = 0.0; // negative or zero - protected: - btVector3 mVelocity; - const btCollisionObject * mMe; - }; - - osg::Vec3f MovementSolver::traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight) + + btScalar mMaxX = 0.0; + btScalar mMaxY = 0.0; + btScalar mMaxZ = 0.0; + btVector3 mContactSum{ 0.0, 0.0, 0.0 }; + btVector3 mNormal{ 0.0, 0.0, 0.0 }; // points towards "me" + btVector3 mDelta{ 0.0, 0.0, 0.0 }; // points towards "me" + btScalar mDistance = 0.0; // negative or zero + + protected: + btVector3 mVelocity; + }; + } + + osg::Vec3f MovementSolver::traceDown(const MWWorld::Ptr& ptr, const osg::Vec3f& position, Actor* actor, + btCollisionWorld* collisionWorld, float maxHeight) { osg::Vec3f offset = actor->getCollisionObjectPosition() - ptr.getRefData().getPosition().asVec3(); ActorTracer tracer; - tracer.findGround(actor, position + offset, position + offset - osg::Vec3f(0,0,maxHeight), collisionWorld); + tracer.findGround(actor, position + offset, position + offset - osg::Vec3f(0, 0, maxHeight), collisionWorld); if (tracer.mFraction >= 1.0f) { actor->setOnGround(false); @@ -95,16 +104,17 @@ namespace MWPhysics // Required for some broken door destinations in Morrowind.esm, where the spawn point // intersects with other geometry if the actor's base is taken into account btVector3 from = Misc::Convert::toBullet(position); - btVector3 to = from - btVector3(0,0,maxHeight); + btVector3 to = from - btVector3(0, 0, maxHeight); btCollisionWorld::ClosestRayResultCallback resultCallback1(from, to); - resultCallback1.m_collisionFilterGroup = 0xff; - resultCallback1.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap; + resultCallback1.m_collisionFilterGroup = CollisionType_AnyPhysical; + resultCallback1.m_collisionFilterMask = CollisionType_World | CollisionType_HeightMap; collisionWorld->rayTest(from, to, resultCallback1); - if (resultCallback1.hasHit() && ((Misc::Convert::toOsg(resultCallback1.m_hitPointWorld) - tracer.mEndPos + offset).length2() > 35*35 - || !isWalkableSlope(tracer.mPlaneNormal))) + if (resultCallback1.hasHit() + && ((Misc::Convert::toOsg(resultCallback1.m_hitPointWorld) - tracer.mEndPos + offset).length2() > 35 * 35 + || !isWalkableSlope(tracer.mPlaneNormal))) { actor->setOnSlope(!isWalkableSlope(resultCallback1.m_hitNormalWorld)); return Misc::Convert::toOsg(resultCallback1.m_hitPointWorld) + osg::Vec3f(0.f, 0.f, sGroundOffset); @@ -112,118 +122,109 @@ namespace MWPhysics actor->setOnSlope(!isWalkableSlope(tracer.mPlaneNormal)); - return tracer.mEndPos-offset + osg::Vec3f(0.f, 0.f, sGroundOffset); + return tracer.mEndPos - offset + osg::Vec3f(0.f, 0.f, sGroundOffset); } - void MovementSolver::move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, - WorldFrameData& worldData) + void MovementSolver::move( + ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, const WorldFrameData& worldData) { - auto* physicActor = actor.mActorRaw; - const ESM::Position& refpos = actor.mRefpos; - // Early-out for totally static creatures - // (Not sure if gravity should still apply?) - { - const auto ptr = physicActor->getPtr(); - if (!ptr.getClass().isMobile(ptr)) - return; - } - // Reset per-frame data - physicActor->setWalkingOnWater(false); + actor.mWalkingOnWater = false; // Anything to collide with? - if(!physicActor->getCollisionMode() || actor.mSkipCollisionDetection) + if (actor.mSkipCollisionDetection) { - actor.mPosition += (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * - osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1)) - ) * actor.mMovement * time; + actor.mPosition += (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0)) + * osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) + * actor.mMovement * time; return; } - const btCollisionObject *colobj = physicActor->getCollisionObject(); - // Adjust for collision mesh offset relative to actor's "location" - // (doTrace doesn't take local/interior collision shape translation into account, so we have to do it on our own) - // for compatibility with vanilla assets, we have to derive this from the vertical half extent instead of from internal hull translation - // if not for this hack, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() - osg::Vec3f halfExtents = physicActor->getHalfExtents(); - actor.mPosition.z() += halfExtents.z(); // vanilla-accurate + // (doTrace doesn't take local/interior collision shape translation into account, so we have to do it on our + // own) for compatibility with vanilla assets, we have to derive this from the vertical half extent instead of + // from internal hull translation if not for this hack, the "correct" collision hull position would be + // physicActor->getScaledMeshTranslation() + actor.mPosition.z() += actor.mHalfExtentsZ; // vanilla-accurate - static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get().find("fSwimHeightScale")->mValue.getFloat(); - float swimlevel = actor.mWaterlevel + halfExtents.z() - (physicActor->getRenderingHalfExtents().z() * 2 * fSwimHeightScale); + float swimlevel = actor.mSwimLevel + actor.mHalfExtentsZ; ActorTracer tracer; - osg::Vec3f inertia = physicActor->getInertialForce(); osg::Vec3f velocity; - if (actor.mPosition.z() < swimlevel || actor.mFlying) + // Dead and paralyzed actors underwater will float to the surface, + // if the CharacterController tells us to do so + if (actor.mMovement.z() > 0 && actor.mInert && actor.mPosition.z() < swimlevel) + { + velocity = osg::Vec3f(0, 0, 1) * 25; + } + else if (actor.mPosition.z() < swimlevel || actor.mFlying) { - velocity = (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; + velocity = (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0)) + * osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) + * actor.mMovement; } else { - velocity = (osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; + velocity = (osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) * actor.mMovement; - if ((velocity.z() > 0.f && physicActor->getOnGround() && !physicActor->getOnSlope()) - || (velocity.z() > 0.f && velocity.z() + inertia.z() <= -velocity.z() && physicActor->getOnSlope())) - inertia = velocity; - else if (!physicActor->getOnGround() || physicActor->getOnSlope()) - velocity = velocity + inertia; + if ((velocity.z() > 0.f && actor.mIsOnGround && !actor.mIsOnSlope) + || (velocity.z() > 0.f && velocity.z() + actor.mInertia.z() <= -velocity.z() && actor.mIsOnSlope)) + actor.mInertia = velocity; + else if (!actor.mIsOnGround || actor.mIsOnSlope) + velocity = velocity + actor.mInertia; } - // Dead and paralyzed actors underwater will float to the surface, - // if the CharacterController tells us to do so - if (actor.mMovement.z() > 0 && actor.mFloatToSurface && actor.mPosition.z() < swimlevel) - velocity = osg::Vec3f(0,0,1) * 25; - - if (actor.mWantJump) - actor.mDidJump = true; - // Now that we have the effective movement vector, apply wind forces to it - if (worldData.mIsInStorm) + if (worldData.mIsInStorm && velocity.length() > 0) { osg::Vec3f stormDirection = worldData.mStormDirection; - float angleDegrees = osg::RadiansToDegrees(std::acos(stormDirection * velocity / (stormDirection.length() * velocity.length()))); - static const float fStromWalkMult = MWBase::Environment::get().getWorld()->getStore().get().find("fStromWalkMult")->mValue.getFloat(); - velocity *= 1.f-(fStromWalkMult * (angleDegrees/180.f)); + float angleDegrees = osg::RadiansToDegrees( + std::acos(stormDirection * velocity / (stormDirection.length() * velocity.length()))); + static const float fStromWalkMult = MWBase::Environment::get() + .getESMStore() + ->get() + .find("fStromWalkMult") + ->mValue.getFloat(); + velocity *= 1.f - (fStromWalkMult * (angleDegrees / 180.f)); } - Stepper stepper(collisionWorld, colobj); + Stepper stepper(collisionWorld, actor.mCollisionObject); osg::Vec3f origVelocity = velocity; osg::Vec3f newPosition = actor.mPosition; /* * A loop to find newPosition using tracer, if successful different from the starting position. * nextpos is the local variable used to find potential newPosition, using velocity and remainingTime * The initial velocity was set earlier (see above). - */ + */ float remainingTime = time; - bool seenGround = physicActor->getOnGround() && !physicActor->getOnSlope() && !actor.mFlying; int numTimesSlid = 0; - osg::Vec3f lastSlideNormal(0,0,1); - osg::Vec3f lastSlideNormalFallback(0,0,1); + osg::Vec3f lastSlideNormal(0, 0, 1); + osg::Vec3f lastSlideNormalFallback(0, 0, 1); bool forceGroundTest = false; for (int iterations = 0; iterations < sMaxIterations && remainingTime > 0.0001f; ++iterations) { osg::Vec3f nextpos = newPosition + velocity * remainingTime; + bool underwater = newPosition.z() < swimlevel; // If not able to fly, don't allow to swim up into the air - if(!actor.mFlying && nextpos.z() > swimlevel && newPosition.z() < swimlevel) + if (!actor.mFlying && nextpos.z() > swimlevel && underwater) { - const osg::Vec3f down(0,0,-1); + const osg::Vec3f down(0, 0, -1); velocity = reject(velocity, down); // NOTE: remainingTime is unchanged before the loop continues continue; // velocity updated, calculate nextpos again } - if((newPosition - nextpos).length2() > 0.0001) + if ((newPosition - nextpos).length2() > 0.0001) { // trace to where character would go if there were no obstructions - tracer.doTrace(colobj, newPosition, nextpos, collisionWorld); + tracer.doTrace(actor.mCollisionObject, newPosition, nextpos, collisionWorld, actor.mIsOnGround); // check for obstructions - if(tracer.mFraction >= 1.0f) + if (tracer.mFraction >= 1.0f) { newPosition = tracer.mEndPos; // ok to move, so set newPosition break; @@ -241,38 +242,48 @@ namespace MWPhysics break; } - if (isWalkableSlope(tracer.mPlaneNormal) && !actor.mFlying && newPosition.z() >= swimlevel) - seenGround = true; + bool seenGround = !actor.mFlying && !underwater + && ((actor.mIsOnGround && !actor.mIsOnSlope) || isWalkableSlope(tracer.mPlaneNormal)); // We hit something. Check if we can step up. - float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + halfExtents.z(); + float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + actor.mHalfExtentsZ; osg::Vec3f oldPosition = newPosition; bool usedStepLogic = false; - if (hitHeight < sStepSizeUp && !isActor(tracer.mHitObject)) + if (!isActor(tracer.mHitObject)) { - // Try to step up onto it. - // NOTE: this modifies newPosition and velocity on its own if successful - usedStepLogic = stepper.step(newPosition, velocity, remainingTime, seenGround, iterations == 0); + if (hitHeight < Constants::sStepSizeUp) + { + // Try to step up onto it. + // NOTE: this modifies newPosition and velocity on its own if successful + usedStepLogic = stepper.step(newPosition, velocity, remainingTime, seenGround, iterations == 0); + } + auto* ptrHolder = static_cast(tracer.mHitObject->getUserPointer()); + if (Object* hitObject = dynamic_cast(ptrHolder)) + { + hitObject->addCollision( + actor.mIsPlayer ? ScriptedCollisionType_Player : ScriptedCollisionType_Actor); + } } if (usedStepLogic) { - // don't let pure water creatures move out of water after stepMove - const auto ptr = physicActor->getPtr(); - if (ptr.getClass().isPureWaterCreature(ptr) && newPosition.z() + halfExtents.z() > actor.mWaterlevel) + if (actor.mIsAquatic && newPosition.z() + actor.mHalfExtentsZ > actor.mWaterlevel) newPosition = oldPosition; - else if(!actor.mFlying && actor.mPosition.z() >= swimlevel) + else if (!actor.mFlying && actor.mPosition.z() >= swimlevel) forceGroundTest = true; } else { // Can't step up, so slide against what we ran into - remainingTime *= (1.0f-tracer.mFraction); + remainingTime *= (1.0f - tracer.mFraction); auto planeNormal = tracer.mPlaneNormal; + // need to know the unadjusted normal to handle certain types of seams properly + const auto origPlaneNormal = planeNormal; // If we touched the ground this frame, and whatever we ran into is a wall of some sort, // pretend that its collision normal is pointing horizontally - // (fixes snagging on slightly downward-facing walls, and crawling up the bases of very steep walls because of the collision margin) + // (fixes snagging on slightly downward-facing walls, and crawling up the bases of very steep walls + // because of the collision margin) if (seenGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) { planeNormal.z() = 0; @@ -280,45 +291,49 @@ namespace MWPhysics } // Move up to what we ran into (with a bit of a collision margin) - if ((newPosition-tracer.mEndPos).length2() > sCollisionMargin*sCollisionMargin) + if ((newPosition - tracer.mEndPos).length2() > sCollisionMargin * sCollisionMargin) { auto direction = velocity; direction.normalize(); newPosition = tracer.mEndPos; - newPosition -= direction*sCollisionMargin; + newPosition -= direction * sCollisionMargin; } osg::Vec3f newVelocity = (velocity * planeNormal <= 0.0) ? reject(velocity, planeNormal) : velocity; bool usedSeamLogic = false; - // check for the current and previous collision planes forming an acute angle; slide along the seam if they do - if(numTimesSlid > 0) + // check for the current and previous collision planes forming an acute angle; slide along the seam if + // they do for this, we want to use the original plane normal, or else certain types of geometry will + // snag + if (numTimesSlid > 0) { - auto dotA = lastSlideNormal * planeNormal; - auto dotB = lastSlideNormalFallback * planeNormal; - if(numTimesSlid <= 1) // ignore fallback normal if this is only the first or second slide + auto dotA = lastSlideNormal * origPlaneNormal; + auto dotB = lastSlideNormalFallback * origPlaneNormal; + if (numTimesSlid <= 1) // ignore fallback normal if this is only the first or second slide dotB = 1.0; - if(dotA <= 0.0 || dotB <= 0.0) + if (dotA <= 0.0 || dotB <= 0.0) { osg::Vec3f bestNormal = lastSlideNormal; - // use previous-to-previous collision plane if it's acute with current plane but actual previous plane isn't - if(dotB < dotA) + // use previous-to-previous collision plane if it's acute with current plane but actual previous + // plane isn't + if (dotB < dotA) { bestNormal = lastSlideNormalFallback; lastSlideNormal = lastSlideNormalFallback; } - auto constraintVector = bestNormal ^ planeNormal; // cross product - if(constraintVector.length2() > 0) // only if it's not zero length + auto constraintVector = bestNormal ^ origPlaneNormal; // cross product + if (constraintVector.length2() > 0) // only if it's not zero length { constraintVector.normalize(); newVelocity = project(velocity, constraintVector); // version of surface rejection for acute crevices/seams - auto averageNormal = bestNormal + planeNormal; + auto averageNormal = bestNormal + origPlaneNormal; averageNormal.normalize(); - tracer.doTrace(colobj, newPosition, newPosition + averageNormal*(sCollisionMargin*2.0), collisionWorld); - newPosition = (newPosition + tracer.mEndPos)/2.0; + tracer.doTrace(actor.mCollisionObject, newPosition, + newPosition + averageNormal * (sCollisionMargin * 2.0), collisionWorld); + newPosition = (newPosition + tracer.mEndPos) / 2.0; usedSeamLogic = true; } @@ -326,207 +341,248 @@ namespace MWPhysics } // otherwise just keep the normal vector rejection - // if this isn't the first iteration, or if the first iteration is also the last iteration, // move away from the collision plane slightly, if possible - // this reduces getting stuck in some concave geometry, like the gaps above the railings in some ald'ruhn buildings - // this is different from the normal collision margin, because the normal collision margin is along the movement path, - // but this is along the collision normal - if(!usedSeamLogic && (iterations > 0 || remainingTime < 0.01f)) + // this reduces getting stuck in some concave geometry, like the gaps above the railings in some + // ald'ruhn buildings this is different from the normal collision margin, because the normal collision + // margin is along the movement path, but this is along the collision normal + if (!usedSeamLogic) { - tracer.doTrace(colobj, newPosition, newPosition + planeNormal*(sCollisionMargin*2.0), collisionWorld); - newPosition = (newPosition + tracer.mEndPos)/2.0; + tracer.doTrace(actor.mCollisionObject, newPosition, + newPosition + planeNormal * (sCollisionMargin * 2.0), collisionWorld); + newPosition = (newPosition + tracer.mEndPos) / 2.0; + } + + // short circuit if we went backwards, but only if it was mostly horizontal and we're on the ground + if (seenGround && newVelocity * origVelocity <= 0.0f) + { + auto perpendicular = newVelocity ^ origVelocity; + if (perpendicular.length2() > 0.0f) + { + perpendicular.normalize(); + if (std::abs(perpendicular.z()) > 0.7071f) + break; + } } // Do not allow sliding up steep slopes if there is gravity. - if (newPosition.z() >= swimlevel && !actor.mFlying && !isWalkableSlope(planeNormal)) + // The purpose of this is to prevent air control from letting you slide up tall, unwalkable slopes. + // For that purpose, it is not necessary to do it when trying to slide along acute seams/crevices (i.e. + // usedSeamLogic) and doing so would actually break air control in some situations where vanilla allows + // air control. Vanilla actually allows you to slide up slopes as long as you're in the "walking" + // animation, which can be true even in the air, so allowing this for seams isn't a compatibility break. + if (newPosition.z() >= swimlevel && !actor.mFlying && !isWalkableSlope(planeNormal) && !usedSeamLogic) newVelocity.z() = std::min(newVelocity.z(), velocity.z()); - if (newVelocity * origVelocity <= 0.0f) - break; - numTimesSlid += 1; lastSlideNormalFallback = lastSlideNormal; - lastSlideNormal = planeNormal; + lastSlideNormal = origPlaneNormal; velocity = newVelocity; } } bool isOnGround = false; bool isOnSlope = false; - if (forceGroundTest || (inertia.z() <= 0.f && newPosition.z() >= swimlevel)) + if (forceGroundTest || (actor.mInertia.z() <= 0.f && newPosition.z() >= swimlevel)) { osg::Vec3f from = newPosition; - auto dropDistance = 2*sGroundOffset + (physicActor->getOnGround() ? sStepSizeDown : 0); - osg::Vec3f to = newPosition - osg::Vec3f(0,0,dropDistance); - tracer.doTrace(colobj, from, to, collisionWorld); - if(tracer.mFraction < 1.0f) + auto dropDistance = 2 * sGroundOffset + (actor.mIsOnGround ? sStepSizeDown : 0); + osg::Vec3f to = newPosition - osg::Vec3f(0, 0, dropDistance); + tracer.doTrace(actor.mCollisionObject, from, to, collisionWorld, actor.mIsOnGround); + if (tracer.mFraction < 1.0f) { if (!isActor(tracer.mHitObject)) { isOnGround = true; isOnSlope = !isWalkableSlope(tracer.mPlaneNormal); + actor.mStandingOn = tracer.mHitObject; - const btCollisionObject* standingOn = tracer.mHitObject; - PtrHolder* ptrHolder = static_cast(standingOn->getUserPointer()); - if (ptrHolder) - actor.mStandingOn = ptrHolder->getPtr(); - - if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) - physicActor->setWalkingOnWater(true); + if (actor.mStandingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) + actor.mWalkingOnWater = true; if (!actor.mFlying && !isOnSlope) { - if (tracer.mFraction*dropDistance > sGroundOffset) + if (tracer.mFraction * dropDistance > sGroundOffset) newPosition.z() = tracer.mEndPos.z() + sGroundOffset; else { newPosition.z() = tracer.mEndPos.z(); - tracer.doTrace(colobj, newPosition, newPosition + osg::Vec3f(0, 0, 2*sGroundOffset), collisionWorld); - newPosition = (newPosition+tracer.mEndPos)/2.0; + tracer.doTrace(actor.mCollisionObject, newPosition, + newPosition + osg::Vec3f(0, 0, 2 * sGroundOffset), collisionWorld); + newPosition = (newPosition + tracer.mEndPos) / 2.0; } } } else { // Vanilla allows actors to float on top of other actors. Do not push them off. - if (!actor.mFlying && isWalkableSlope(tracer.mPlaneNormal) && tracer.mEndPos.z()+sGroundOffset <= newPosition.z()) + if (!actor.mFlying && isWalkableSlope(tracer.mPlaneNormal) + && tracer.mEndPos.z() + sGroundOffset <= newPosition.z()) newPosition.z() = tracer.mEndPos.z() + sGroundOffset; isOnGround = false; } } - // forcibly treat stuck actors as if they're on flat ground because buggy collisions when inside of things can/will break ground detection - if(physicActor->getStuckFrames() > 0) + // forcibly treat stuck actors as if they're on flat ground because buggy collisions when inside of things + // can/will break ground detection + if (actor.mStuckFrames > 0) { isOnGround = true; isOnSlope = false; } } - if((isOnGround && !isOnSlope) || newPosition.z() < swimlevel || actor.mFlying) - physicActor->setInertialForce(osg::Vec3f(0.f, 0.f, 0.f)); + if ((isOnGround && !isOnSlope) || newPosition.z() < swimlevel || actor.mFlying) + actor.mInertia = osg::Vec3f(0.f, 0.f, 0.f); else { - inertia.z() -= time * Constants::GravityConst * Constants::UnitsPerMeter; - if (inertia.z() < 0) - inertia.z() *= actor.mSlowFall; - if (actor.mSlowFall < 1.f) { - inertia.x() *= actor.mSlowFall; - inertia.y() *= actor.mSlowFall; + actor.mInertia.z() -= time * Constants::GravityConst * Constants::UnitsPerMeter; + if (actor.mInertia.z() < 0) + actor.mInertia.z() *= actor.mSlowFall; + if (actor.mSlowFall < 1.f) + { + actor.mInertia.x() *= actor.mSlowFall; + actor.mInertia.y() *= actor.mSlowFall; } - physicActor->setInertialForce(inertia); } - physicActor->setOnGround(isOnGround); - physicActor->setOnSlope(isOnSlope); + actor.mIsOnGround = isOnGround; + actor.mIsOnSlope = isOnSlope; actor.mPosition = newPosition; // remove what was added earlier in compensating for doTrace not taking interior transformation into account - actor.mPosition.z() -= halfExtents.z(); // vanilla-accurate + actor.mPosition.z() -= actor.mHalfExtentsZ; // vanilla-accurate + } + + void MovementSolver::move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld) + { + btVector3 btFrom = Misc::Convert::toBullet(projectile.mPosition); + btVector3 btTo = Misc::Convert::toBullet(projectile.mPosition + projectile.mMovement * time); + + if (btFrom == btTo) + return; + + assert(projectile.mProjectile != nullptr); + + ProjectileConvexCallback resultCallback( + projectile.mCaster, projectile.mCollisionObject, btFrom, btTo, *projectile.mProjectile); + resultCallback.m_collisionFilterMask = CollisionType_AnyPhysical; + resultCallback.m_collisionFilterGroup = CollisionType_Projectile; + + const btQuaternion btrot = btQuaternion::getIdentity(); + btTransform from_(btrot, btFrom); + btTransform to_(btrot, btTo); + + const btCollisionShape* shape = projectile.mCollisionObject->getCollisionShape(); + assert(shape->isConvex()); + collisionWorld->convexSweepTest(static_cast(shape), from_, to_, resultCallback); + + projectile.mPosition + = Misc::Convert::toOsg(projectile.mProjectile->isActive() ? btTo : resultCallback.m_hitPointWorld); } btVector3 addMarginToDelta(btVector3 delta) { - if(delta.length2() == 0.0) + if (delta.length2() == 0.0) return delta; return delta + delta.normalized() * sCollisionMargin; } void MovementSolver::unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld) { - const auto& ptr = actor.mActorRaw->getPtr(); - if (!ptr.getClass().isMobile(ptr)) + if (actor.mSkipCollisionDetection) // noclipping/tcl return; - auto* physicActor = actor.mActorRaw; - if(!physicActor->getCollisionMode() || actor.mSkipCollisionDetection) // noclipping/tcl + if (actor.mMovement.length2() == 0) // no AI nor player attempted to move, current position is assumed correct return; - auto* collisionObject = physicActor->getCollisionObject(); auto tempPosition = actor.mPosition; - if(physicActor->getStuckFrames() >= 10) + if (actor.mStuckFrames >= 10) { - if((physicActor->getLastStuckPosition() - actor.mPosition).length2() < 100) + if ((actor.mLastStuckPosition - actor.mPosition).length2() < 100) return; else { - physicActor->setStuckFrames(0); - physicActor->setLastStuckPosition({0, 0, 0}); + actor.mStuckFrames = 0; + actor.mLastStuckPosition = { 0, 0, 0 }; } } // use vanilla-accurate collision hull position hack (do same hitbox offset hack as movement solver) - // if vanilla compatibility didn't matter, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() - const auto verticalHalfExtent = osg::Vec3f(0.0, 0.0, physicActor->getHalfExtents().z()); + // if vanilla compatibility didn't matter, the "correct" collision hull position would be + // physicActor->getScaledMeshTranslation() + const auto verticalHalfExtent = osg::Vec3f(0.0, 0.0, actor.mHalfExtentsZ); // use a 3d approximation of the movement vector to better judge player intent - auto velocity = (osg::Quat(actor.mRefpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(actor.mRefpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; + auto velocity = (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0)) + * osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) + * actor.mMovement; // try to pop outside of the world before doing anything else if we're inside of it - if (!physicActor->getOnGround() || physicActor->getOnSlope()) - velocity += physicActor->getInertialForce(); + if (!actor.mIsOnGround || actor.mIsOnSlope) + velocity += actor.mInertia; // because of the internal collision box offset hack, and the fact that we're moving the collision box manually, // we need to replicate part of the collision box's transform process from scratch osg::Vec3f refPosition = tempPosition + verticalHalfExtent; osg::Vec3f goodPosition = refPosition; - const btTransform oldTransform = collisionObject->getWorldTransform(); + const btTransform oldTransform = actor.mCollisionObject->getWorldTransform(); btTransform newTransform = oldTransform; - auto gatherContacts = [&](btVector3 newOffset) -> ContactCollectionCallback - { + auto gatherContacts = [&](btVector3 newOffset) -> ContactCollectionCallback { goodPosition = refPosition + Misc::Convert::toOsg(addMarginToDelta(newOffset)); newTransform.setOrigin(Misc::Convert::toBullet(goodPosition)); - collisionObject->setWorldTransform(newTransform); + actor.mCollisionObject->setWorldTransform(newTransform); - ContactCollectionCallback callback{collisionObject, velocity}; - ContactTestWrapper::contactTest(const_cast(collisionWorld), collisionObject, callback); + ContactCollectionCallback callback(*actor.mCollisionObject, velocity); + ContactTestWrapper::contactTest( + const_cast(collisionWorld), actor.mCollisionObject, callback); return callback; }; // check whether we're inside the world with our collision box with manually-derived offset - auto contactCallback = gatherContacts({0.0, 0.0, 0.0}); - if(contactCallback.mDistance < -sAllowedPenetration) + auto contactCallback = gatherContacts({ 0.0, 0.0, 0.0 }); + if (contactCallback.mDistance < -sAllowedPenetration) { - physicActor->setStuckFrames(physicActor->getStuckFrames() + 1); - physicActor->setLastStuckPosition(actor.mPosition); + ++actor.mStuckFrames; + actor.mLastStuckPosition = actor.mPosition; // we are; try moving it out of the world auto positionDelta = contactCallback.mContactSum; // limit rejection delta to the largest known individual rejections - if(std::abs(positionDelta.x()) > contactCallback.mMaxX) + if (std::abs(positionDelta.x()) > contactCallback.mMaxX) positionDelta *= contactCallback.mMaxX / std::abs(positionDelta.x()); - if(std::abs(positionDelta.y()) > contactCallback.mMaxY) + if (std::abs(positionDelta.y()) > contactCallback.mMaxY) positionDelta *= contactCallback.mMaxY / std::abs(positionDelta.y()); - if(std::abs(positionDelta.z()) > contactCallback.mMaxZ) + if (std::abs(positionDelta.z()) > contactCallback.mMaxZ) positionDelta *= contactCallback.mMaxZ / std::abs(positionDelta.z()); auto contactCallback2 = gatherContacts(positionDelta); - // successfully moved further out from contact (does not have to be in open space, just less inside of things) - if(contactCallback2.mDistance > contactCallback.mDistance) + // successfully moved further out from contact (does not have to be in open space, just less inside of + // things) + if (contactCallback2.mDistance > contactCallback.mDistance) tempPosition = goodPosition - verticalHalfExtent; // try again but only upwards (fixes some bad coc floors) else { // upwards-only offset - auto contactCallback3 = gatherContacts({0.0, 0.0, std::abs(positionDelta.z())}); + auto contactCallback3 = gatherContacts({ 0.0, 0.0, std::abs(positionDelta.z()) }); // success - if(contactCallback3.mDistance > contactCallback.mDistance) + if (contactCallback3.mDistance > contactCallback.mDistance) tempPosition = goodPosition - verticalHalfExtent; else // try again but fixed distance up { - auto contactCallback4 = gatherContacts({0.0, 0.0, 10.0}); + auto contactCallback4 = gatherContacts({ 0.0, 0.0, 10.0 }); // success - if(contactCallback4.mDistance > contactCallback.mDistance) + if (contactCallback4.mDistance > contactCallback.mDistance) tempPosition = goodPosition - verticalHalfExtent; } } } else { - physicActor->setStuckFrames(0); - physicActor->setLastStuckPosition({0, 0, 0}); + actor.mStuckFrames = 0; + actor.mLastStuckPosition = { 0, 0, 0 }; } - collisionObject->setWorldTransform(oldTransform); + actor.mCollisionObject->setWorldTransform(oldTransform); actor.mPosition = tempPosition; } } diff --git a/apps/openmw/mwphysics/movementsolver.hpp b/apps/openmw/mwphysics/movementsolver.hpp index 90b10c275e1..e3d6657d258 100644 --- a/apps/openmw/mwphysics/movementsolver.hpp +++ b/apps/openmw/mwphysics/movementsolver.hpp @@ -3,8 +3,7 @@ #include -#include "constants.hpp" -#include "../mwworld/ptr.hpp" +#include class btCollisionWorld; @@ -16,33 +15,37 @@ namespace MWWorld namespace MWPhysics { /// Vector projection - static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f &v) + static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f& v) { return v * (u * v); } /// Vector rejection - static inline osg::Vec3f reject(const osg::Vec3f& direction, const osg::Vec3f &planeNormal) + static inline osg::Vec3f reject(const osg::Vec3f& direction, const osg::Vec3f& planeNormal) { return direction - project(direction, planeNormal); } template - static bool isWalkableSlope(const Vec3 &normal) + static bool isWalkableSlope(const Vec3& normal) { - static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); + static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(Constants::sMaxSlope)); return (normal.z() > sMaxSlopeCos); } class Actor; struct ActorFrameData; + struct ProjectileFrameData; struct WorldFrameData; class MovementSolver { public: - static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight); - static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData); + static osg::Vec3f traceDown(const MWWorld::Ptr& ptr, const osg::Vec3f& position, Actor* actor, + btCollisionWorld* collisionWorld, float maxHeight); + static void move( + ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, const WorldFrameData& worldData); + static void move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld); static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld); }; } diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index 9b98e7e8f7e..aafefe70198 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -1,170 +1,440 @@ +#include "mtphysics.hpp" + +#include +#include +#include +#include +#include +#include +#include + #include #include +#include #include #include "components/debug/debuglog.hpp" -#include #include "components/misc/convert.hpp" -#include "components/settings/settings.hpp" +#include +#include + #include "../mwmechanics/actorutil.hpp" -#include "../mwmechanics/movement.hpp" +#include "../mwmechanics/creaturestats.hpp" + #include "../mwrender/bulletdebugdraw.hpp" + #include "../mwworld/class.hpp" -#include "../mwworld/player.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "actor.hpp" #include "contacttestwrapper.h" #include "movementsolver.hpp" -#include "mtphysics.hpp" #include "object.hpp" #include "physicssystem.hpp" #include "projectile.hpp" -namespace +namespace MWPhysics { - /// @brief A scoped lock that is either shared or exclusive depending on configuration - template - class MaybeSharedLock + namespace { + template + std::optional> makeExclusiveLock(Mutex& mutex, LockingPolicy lockingPolicy) + { + if (lockingPolicy == LockingPolicy::NoLocks) + return {}; + return std::unique_lock(mutex); + } + + /// @brief A scoped lock that is either exclusive or inexistent depending on configuration + template + class MaybeExclusiveLock + { public: - /// @param mutex a shared mutex - /// @param canBeSharedLock decide wether the lock will be shared or exclusive - MaybeSharedLock(Mutex& mutex, bool canBeSharedLock) : mMutex(mutex), mCanBeSharedLock(canBeSharedLock) + /// @param mutex a mutex + /// @param threadCount decide wether the excluse lock will be taken + explicit MaybeExclusiveLock(Mutex& mutex, LockingPolicy lockingPolicy) + : mImpl(makeExclusiveLock(mutex, lockingPolicy)) { - if (mCanBeSharedLock) - mMutex.lock_shared(); - else - mMutex.lock(); } - ~MaybeSharedLock() - { - if (mCanBeSharedLock) - mMutex.unlock_shared(); - else - mMutex.unlock(); - } private: - Mutex& mMutex; - bool mCanBeSharedLock; - }; + std::optional> mImpl; + }; - void handleFall(MWPhysics::ActorFrameData& actorData, bool simulationPerformed) - { - const float heightDiff = actorData.mPosition.z() - actorData.mOldHeight; + template + std::optional> makeSharedLock(Mutex& mutex, LockingPolicy lockingPolicy) + { + if (lockingPolicy == LockingPolicy::NoLocks) + return {}; + return std::shared_lock(mutex); + } - const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mActorRaw->getOnGround()); + /// @brief A scoped lock that is either shared or inexistent depending on configuration + template + class MaybeSharedLock + { + public: + /// @param mutex a shared mutex + /// @param threadCount decide wether the shared lock will be taken + explicit MaybeSharedLock(Mutex& mutex, LockingPolicy lockingPolicy) + : mImpl(makeSharedLock(mutex, lockingPolicy)) + { + } - if (isStillOnGround || actorData.mFlying || actorData.mSwimming || actorData.mSlowFall < 1) - actorData.mNeedLand = true; - else if (heightDiff < 0) - actorData.mFallHeight += heightDiff; - } + private: + std::optional> mImpl; + }; - void handleJump(const MWWorld::Ptr &ptr) - { - const bool isPlayer = (ptr == MWMechanics::getPlayer()); - // Advance acrobatics and set flag for GetPCJumping - if (isPlayer) + template + std::variant, std::shared_lock> makeLock( + Mutex& mutex, LockingPolicy lockingPolicy) { - ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, 0); - MWBase::Environment::get().getWorld()->getPlayer().setJumping(true); + switch (lockingPolicy) + { + case LockingPolicy::NoLocks: + return std::monostate{}; + case LockingPolicy::ExclusiveLocksOnly: + return std::unique_lock(mutex); + case LockingPolicy::AllowSharedLocks: + return std::shared_lock(mutex); + }; + + throw std::runtime_error("Unsupported LockingPolicy: " + + std::to_string(static_cast>(lockingPolicy))); } - // Decrease fatigue - if (!isPlayer || !MWBase::Environment::get().getWorld()->getGodModeState()) + /// @brief A scoped lock that is either shared, exclusive or inexistent depending on configuration + template + class MaybeLock { - const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); - const float fFatigueJumpBase = gmst.find("fFatigueJumpBase")->mValue.getFloat(); - const float fFatigueJumpMult = gmst.find("fFatigueJumpMult")->mValue.getFloat(); - const float normalizedEncumbrance = std::min(1.f, ptr.getClass().getNormalizedEncumbrance(ptr)); - const float fatigueDecrease = fFatigueJumpBase + normalizedEncumbrance * fFatigueJumpMult; - MWMechanics::DynamicStat fatigue = ptr.getClass().getCreatureStats(ptr).getFatigue(); - fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); - ptr.getClass().getCreatureStats(ptr).setFatigue(fatigue); - } - ptr.getClass().getMovementSettings(ptr).mPosition[2] = 0; + public: + /// @param mutex a shared mutex + /// @param threadCount decide wether the lock will be shared, exclusive or inexistent + explicit MaybeLock(Mutex& mutex, LockingPolicy lockingPolicy) + : mImpl(makeLock(mutex, lockingPolicy)) + { + } + + private: + std::variant, std::shared_lock> mImpl; + }; } +} - void updateMechanics(MWPhysics::ActorFrameData& actorData) +namespace +{ + bool isUnderWater(const MWPhysics::ActorFrameData& actorData) { - auto ptr = actorData.mActorRaw->getPtr(); - if (actorData.mDidJump) - handleJump(ptr); - - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - if (actorData.mNeedLand) - stats.land(ptr == MWMechanics::getPlayer() && (actorData.mFlying || actorData.mSwimming)); - else if (actorData.mFallHeight < 0) - stats.addToFallHeight(-actorData.mFallHeight); + return actorData.mPosition.z() < actorData.mSwimLevel; } - osg::Vec3f interpolateMovements(MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt) + osg::Vec3f interpolateMovements(const MWPhysics::PtrHolder& ptr, float timeAccum, float physicsDt) { const float interpolationFactor = std::clamp(timeAccum / physicsDt, 0.0f, 1.0f); - return actorData.mPosition * interpolationFactor + actorData.mActorRaw->getPreviousPosition() * (1.f - interpolationFactor); + return ptr.getPosition() * interpolationFactor + ptr.getPreviousPosition() * (1.f - interpolationFactor); } - namespace Config + using LockedActorSimulation + = std::pair, std::reference_wrapper>; + using LockedProjectileSimulation + = std::pair, std::reference_wrapper>; + + namespace Visitors { - /// @return either the number of thread as configured by the user, or 1 if Bullet doesn't support multithreading - int computeNumThreads(bool& threadSafeBullet) + template class Lock> + struct WithLockedPtr { - int wantedThread = Settings::Manager::getInt("async num threads", "Physics"); + const Impl& mImpl; + std::shared_mutex& mCollisionWorldMutex; + const MWPhysics::LockingPolicy mLockingPolicy; - auto broad = std::make_unique(); - auto maxSupportedThreads = broad->m_rayTestStacks.size(); - threadSafeBullet = (maxSupportedThreads > 1); - if (!threadSafeBullet && wantedThread > 1) + template + void operator()(MWPhysics::SimulationImpl& sim) const { - Log(Debug::Warning) << "Bullet was not compiled with multithreading support, 1 async thread will be used"; - return 1; + auto locked = sim.lock(); + if (!locked.has_value()) + return; + auto&& [ptr, frameData] = *std::move(locked); + // Locked shared_ptr has to be destructed after releasing mCollisionWorldMutex to avoid + // possible deadlock. Ptr destructor also acquires mCollisionWorldMutex. + const std::pair arg(std::move(ptr), frameData); + const Lock lock(mCollisionWorldMutex, mLockingPolicy); + mImpl(arg); } - return std::max(0, wantedThread); - } + }; + + struct InitPosition + { + const btCollisionWorld* mCollisionWorld; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto locked = sim.lock(); + if (!locked.has_value()) + return; + auto& [actor, frameDataRef] = *locked; + auto& frameData = frameDataRef.get(); + frameData.mPosition = actor->applyOffsetChange(); + if (frameData.mWaterCollision && frameData.mPosition.z() < frameData.mWaterlevel + && actor->canMoveToWaterSurface(frameData.mWaterlevel, mCollisionWorld)) + { + const auto offset = osg::Vec3f(0, 0, frameData.mWaterlevel - frameData.mPosition.z()); + MWBase::Environment::get().getWorld()->moveObjectBy(actor->getPtr(), offset, false); + frameData.mPosition = actor->applyOffsetChange(); + } + actor->updateCollisionObjectPosition(); + frameData.mOldHeight = frameData.mPosition.z(); + const auto rotation = actor->getPtr().getRefData().getPosition().asRotationVec3(); + frameData.mRotation = osg::Vec2f(rotation.x(), rotation.z()); + frameData.mInertia = actor->getInertialForce(); + frameData.mStuckFrames = actor->getStuckFrames(); + frameData.mLastStuckPosition = actor->getLastStuckPosition(); + } + void operator()(MWPhysics::ProjectileSimulation& /*sim*/) const {} + }; + + struct PreStep + { + btCollisionWorld* mCollisionWorld; + void operator()(const LockedActorSimulation& sim) const + { + MWPhysics::MovementSolver::unstuck(sim.second, mCollisionWorld); + } + void operator()(const LockedProjectileSimulation& /*sim*/) const {} + }; + + struct UpdatePosition + { + btCollisionWorld* mCollisionWorld; + void operator()(const LockedActorSimulation& sim) const + { + auto& [actor, frameDataRef] = sim; + auto& frameData = frameDataRef.get(); + if (actor->setPosition(frameData.mPosition)) + { + frameData.mPosition = actor->getPosition(); // account for potential position change made by script + actor->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); + } + } + void operator()(const LockedProjectileSimulation& sim) const + { + auto& [proj, frameDataRef] = sim; + auto& frameData = frameDataRef.get(); + proj->setPosition(frameData.mPosition); + proj->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(proj->getCollisionObject()); + } + }; + + struct Move + { + const float mPhysicsDt; + const btCollisionWorld* mCollisionWorld; + const MWPhysics::WorldFrameData& mWorldFrameData; + void operator()(const LockedActorSimulation& sim) const + { + MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld, mWorldFrameData); + } + void operator()(const LockedProjectileSimulation& sim) const + { + if (sim.first->isActive()) + MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld); + } + }; + + struct Sync + { + const bool mAdvanceSimulation; + const float mTimeAccum; + const float mPhysicsDt; + const MWPhysics::PhysicsTaskScheduler* scheduler; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto locked = sim.lock(); + if (!locked.has_value()) + return; + auto& [actor, frameDataRef] = *locked; + auto& frameData = frameDataRef.get(); + auto ptr = actor->getPtr(); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + const float heightDiff = frameData.mPosition.z() - frameData.mOldHeight; + const bool isStillOnGround = (mAdvanceSimulation && frameData.mWasOnGround && frameData.mIsOnGround); + + if (isStillOnGround || frameData.mFlying || isUnderWater(frameData) || frameData.mSlowFall < 1) + stats.land(ptr == MWMechanics::getPlayer() && (frameData.mFlying || isUnderWater(frameData))); + else if (heightDiff < 0) + stats.addToFallHeight(-heightDiff); + + actor->setSimulationPosition(::interpolateMovements(*actor, mTimeAccum, mPhysicsDt)); + actor->setLastStuckPosition(frameData.mLastStuckPosition); + actor->setStuckFrames(frameData.mStuckFrames); + if (mAdvanceSimulation) + { + MWWorld::Ptr standingOn; + if (frameData.mStandingOn != nullptr) + { + auto* const ptrHolder + = static_cast(scheduler->getUserPointer(frameData.mStandingOn)); + if (ptrHolder != nullptr) + standingOn = ptrHolder->getPtr(); + } + actor->setStandingOnPtr(standingOn); + // the "on ground" state of an actor might have been updated by a traceDown, don't overwrite the + // change + if (actor->getOnGround() == frameData.mWasOnGround) + actor->setOnGround(frameData.mIsOnGround); + actor->setOnSlope(frameData.mIsOnSlope); + actor->setWalkingOnWater(frameData.mWalkingOnWater); + actor->setInertialForce(frameData.mInertia); + } + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + auto locked = sim.lock(); + if (!locked.has_value()) + return; + auto& [proj, frameData] = *locked; + proj->setSimulationPosition(::interpolateMovements(*proj, mTimeAccum, mPhysicsDt)); + } + }; } } namespace MWPhysics { - PhysicsTaskScheduler::PhysicsTaskScheduler(float physicsDt, btCollisionWorld *collisionWorld, MWRender::DebugDrawer* debugDrawer) - : mDefaultPhysicsDt(physicsDt) - , mPhysicsDt(physicsDt) - , mTimeAccum(0.f) - , mCollisionWorld(collisionWorld) - , mDebugDrawer(debugDrawer) - , mNumJobs(0) - , mRemainingSteps(0) - , mLOSCacheExpiry(Settings::Manager::getInt("lineofsight keep inactive cache", "Physics")) - , mDeferAabbUpdate(Settings::Manager::getBool("defer aabb update", "Physics")) - , mNewFrame(false) - , mAdvanceSimulation(false) - , mQuit(false) - , mNextJob(0) - , mNextLOS(0) - , mFrameNumber(0) - , mTimer(osg::Timer::instance()) - , mPrevStepCount(1) - , mBudget(physicsDt) - , mAsyncBudget(0.0f) - , mBudgetCursor(0) - , mAsyncStartTime(0) - , mTimeBegin(0) - , mTimeEnd(0) - , mFrameStart(0) - { - mNumThreads = Config::computeNumThreads(mThreadSafeBullet); + namespace + { + unsigned getMaxBulletSupportedThreads() + { + auto broad = std::make_unique(); + assert(BT_MAX_THREAD_COUNT > 0); + return std::min(broad->m_rayTestStacks.size(), BT_MAX_THREAD_COUNT - 1); + } + + LockingPolicy detectLockingPolicy() + { + if (Settings::physics().mAsyncNumThreads < 1) + return LockingPolicy::NoLocks; + if (getMaxBulletSupportedThreads() > 1) + return LockingPolicy::AllowSharedLocks; + Log(Debug::Warning) << "Bullet was not compiled with multithreading support, 1 async thread will be used"; + return LockingPolicy::ExclusiveLocksOnly; + } + + unsigned getNumThreads(LockingPolicy lockingPolicy) + { + switch (lockingPolicy) + { + case LockingPolicy::NoLocks: + return 0; + case LockingPolicy::ExclusiveLocksOnly: + return 1; + case LockingPolicy::AllowSharedLocks: + return static_cast(std::clamp( + Settings::physics().mAsyncNumThreads, 0, static_cast(getMaxBulletSupportedThreads()))); + } + + throw std::runtime_error("Unsupported LockingPolicy: " + + std::to_string(static_cast>(lockingPolicy))); + } + } + + class PhysicsTaskScheduler::WorkersSync + { + public: + void waitForWorkers() + { + std::unique_lock lock(mWorkersDoneMutex); + if (mFrameCounter != mWorkersFrameCounter) + mWorkersDone.wait(lock); + } + + void wakeUpWorkers() + { + const std::lock_guard lock(mHasJobMutex); + ++mFrameCounter; + mHasJob.notify_all(); + } + + void stopWorkers() + { + const std::lock_guard lock(mHasJobMutex); + mShouldStop = true; + mHasJob.notify_all(); + } + void workIsDone() + { + const std::lock_guard lock(mWorkersDoneMutex); + ++mWorkersFrameCounter; + mWorkersDone.notify_all(); + } + + template + void runWorker(F&& f) noexcept + { + std::size_t lastFrame = 0; + std::unique_lock lock(mHasJobMutex); + while (!mShouldStop) + { + mHasJob.wait(lock, [&] { return mShouldStop || mFrameCounter != lastFrame; }); + lastFrame = mFrameCounter; + lock.unlock(); + f(); + lock.lock(); + } + } + + private: + std::size_t mWorkersFrameCounter = 0; + std::condition_variable mWorkersDone; + std::mutex mWorkersDoneMutex; + std::condition_variable mHasJob; + bool mShouldStop = false; + std::size_t mFrameCounter = 0; + std::mutex mHasJobMutex; + }; + + PhysicsTaskScheduler::PhysicsTaskScheduler( + float physicsDt, btCollisionWorld* collisionWorld, MWRender::DebugDrawer* debugDrawer) + : mDefaultPhysicsDt(physicsDt) + , mPhysicsDt(physicsDt) + , mTimeAccum(0.f) + , mCollisionWorld(collisionWorld) + , mDebugDrawer(debugDrawer) + , mLockingPolicy(detectLockingPolicy()) + , mNumThreads(getNumThreads(mLockingPolicy)) + , mNumJobs(0) + , mRemainingSteps(0) + , mLOSCacheExpiry(Settings::physics().mLineofsightKeepInactiveCache) + , mAdvanceSimulation(false) + , mNextJob(0) + , mNextLOS(0) + , mFrameNumber(0) + , mTimer(osg::Timer::instance()) + , mPrevStepCount(1) + , mBudget(physicsDt) + , mAsyncBudget(0.0f) + , mBudgetCursor(0) + , mAsyncStartTime(0) + , mTimeBegin(0) + , mTimeEnd(0) + , mFrameStart(0) + , mWorkersSync(mNumThreads >= 1 ? std::make_unique() : nullptr) + { if (mNumThreads >= 1) { - for (int i = 0; i < mNumThreads; ++i) - mThreads.emplace_back([&] { worker(); } ); + Log(Debug::Info) << "Using " << mNumThreads << " async physics threads"; + for (unsigned i = 0; i < mNumThreads; ++i) + mThreads.emplace_back([&] { worker(); }); } else { - mLOSCacheExpiry = -1; - mDeferAabbUpdate = false; + mLOSCacheExpiry = 0; } mPreStepBarrier = std::make_unique(mNumThreads); @@ -176,12 +446,14 @@ namespace MWPhysics PhysicsTaskScheduler::~PhysicsTaskScheduler() { - std::unique_lock lock(mSimulationMutex); - mQuit = true; - mNumJobs = 0; - mRemainingSteps = 0; - lock.unlock(); - mHasJob.notify_all(); + waitForWorkers(); + { + MaybeExclusiveLock lock(mSimulationMutex, mLockingPolicy); + mNumJobs = 0; + mRemainingSteps = 0; + } + if (mWorkersSync != nullptr) + mWorkersSync->stopWorkers(); for (auto& thread : mThreads) thread.join(); } @@ -193,11 +465,11 @@ namespace MWPhysics // adjust maximum step count based on whether we're likely physics bottlenecked or not // if maxAllowedSteps ends up higher than numSteps, we will not invoke delta time - // if it ends up lower than numSteps, but greater than 1, we will run a number of true delta time physics steps that we expect to be within budget - // if it ends up lower than numSteps and also 1, we will run a single delta time physics step - // if we did not do this, and had a fixed step count limit, - // we would have an unnecessarily low render framerate if we were only physics bottlenecked, - // and we would be unnecessarily invoking true delta time if we were only render bottlenecked + // if it ends up lower than numSteps, but greater than 1, we will run a number of true delta time physics steps + // that we expect to be within budget if it ends up lower than numSteps and also 1, we will run a single delta + // time physics step if we did not do this, and had a fixed step count limit, we would have an unnecessarily low + // render framerate if we were only physics bottlenecked, and we would be unnecessarily invoking true delta time + // if we were only render bottlenecked // get physics timing stats float budgetMeasurement = std::max(mBudget.get(), mAsyncBudget.get()); @@ -210,7 +482,7 @@ namespace MWPhysics maxAllowedSteps = 1; // physics is fairly cheap; limit based on expense if (budgetMeasurement < 0.5) - maxAllowedSteps = std::ceil(1.0/budgetMeasurement); + maxAllowedSteps = std::ceil(1.0 / budgetMeasurement); // limit to a reasonable amount maxAllowedSteps = std::min(10, maxAllowedSteps); @@ -222,7 +494,7 @@ namespace MWPhysics // ensure that we do not simulate a frame ahead when doing delta time; this reduces stutter and latency // this causes interpolation to 100% use the most recent physics result when true delta time is happening // and we deliberately simulate up to exactly the timestamp that we want to render - actualDelta = timeAccum/float(numSteps+1); + actualDelta = timeAccum / float(numSteps + 1); // actually: if this results in a per-step delta less than the target physics steptime, clamp it // this might reintroduce some stutter, but only comes into play in obscure cases // (because numSteps is originally based on mDefaultPhysicsDt, this won't cause us to overrun) @@ -232,58 +504,53 @@ namespace MWPhysics return std::make_tuple(numSteps, actualDelta); } - const std::vector& PhysicsTaskScheduler::moveActors(float & timeAccum, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) + void PhysicsTaskScheduler::applyQueuedMovements(float& timeAccum, std::vector& simulations, + osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) + { + assert(mSimulations != &simulations); + + waitForWorkers(); + prepareWork(timeAccum, simulations, frameStart, frameNumber, stats); + if (mWorkersSync != nullptr) + mWorkersSync->wakeUpWorkers(); + } + + void PhysicsTaskScheduler::prepareWork(float& timeAccum, std::vector& simulations, + osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { // This function run in the main thread. // While the mSimulationMutex is held, background physics threads can't run. - std::unique_lock lock(mSimulationMutex); + MaybeExclusiveLock lock(mSimulationMutex, mLockingPolicy); double timeStart = mTimer->tick(); - mMovedActors.clear(); - // start by finishing previous background computation if (mNumThreads != 0) { - for (auto& data : mActorsFrameData) - { - const auto actorActive = [&data](const auto& newFrameData) -> bool - { - const auto actor = data.mActor.lock(); - return actor && actor->getPtr() == newFrameData.mActorRaw->getPtr(); - }; - // Only return actors that are still part of the scene - if (std::any_of(actorsData.begin(), actorsData.end(), actorActive)) - { - updateMechanics(data); + syncWithMainThread(); - // these variables are accessed directly from the main thread, update them here to prevent accessing "too new" values - if (mAdvanceSimulation) - data.mActorRaw->setStandingOnPtr(data.mStandingOn); - data.mActorRaw->setSimulationPosition(interpolateMovements(data, mTimeAccum, mPhysicsDt)); - mMovedActors.emplace_back(data.mActorRaw->getPtr()); - } - } - if(mAdvanceSimulation) + if (mAdvanceSimulation) mAsyncBudget.update(mTimer->delta_s(mAsyncStartTime, mTimeEnd), mPrevStepCount, mBudgetCursor); updateStats(frameStart, frameNumber, stats); } auto [numSteps, newDelta] = calculateStepConfig(timeAccum); - timeAccum -= numSteps*newDelta; + timeAccum -= numSteps * newDelta; // init - for (auto& data : actorsData) - data.updatePosition(mCollisionWorld); + const Visitors::InitPosition vis{ mCollisionWorld }; + for (auto& sim : simulations) + { + std::visit(vis, sim); + } mPrevStepCount = numSteps; mRemainingSteps = numSteps; mTimeAccum = timeAccum; mPhysicsDt = newDelta; - mActorsFrameData = std::move(actorsData); + mSimulations = &simulations; mAdvanceSimulation = (mRemainingSteps != 0); - mNewFrame = true; - mNumJobs = mActorsFrameData.size(); + mNumJobs = mSimulations->size(); mNextLOS.store(0, std::memory_order_relaxed); mNextJob.store(0, std::memory_order_release); @@ -295,57 +562,60 @@ namespace MWPhysics if (mNumThreads == 0) { - syncComputation(); - if(mAdvanceSimulation) + doSimulation(); + syncWithMainThread(); + if (mAdvanceSimulation) mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), numSteps, mBudgetCursor); - return mMovedActors; + return; } mAsyncStartTime = mTimer->tick(); - lock.unlock(); - mHasJob.notify_all(); if (mAdvanceSimulation) mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), 1, mBudgetCursor); - return mMovedActors; } - const std::vector& PhysicsTaskScheduler::resetSimulation(const ActorMap& actors) + void PhysicsTaskScheduler::resetSimulation(const ActorMap& actors) { - std::unique_lock lock(mSimulationMutex); + waitForWorkers(); + MaybeExclusiveLock lock(mSimulationMutex, mLockingPolicy); mBudget.reset(mDefaultPhysicsDt); mAsyncBudget.reset(0.0f); - mMovedActors.clear(); - mActorsFrameData.clear(); + if (mSimulations != nullptr) + { + mSimulations->clear(); + mSimulations = nullptr; + } for (const auto& [_, actor] : actors) { actor->updatePosition(); actor->updateCollisionObjectPosition(); - mMovedActors.emplace_back(actor->getPtr()); } - return mMovedActors; } - void PhysicsTaskScheduler::rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const + void PhysicsTaskScheduler::rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, + btCollisionWorld::RayResultCallback& resultCallback) const { - MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lock(mCollisionWorldMutex, mLockingPolicy); mCollisionWorld->rayTest(rayFromWorld, rayToWorld, resultCallback); } - void PhysicsTaskScheduler::convexSweepTest(const btConvexShape* castShape, const btTransform& from, const btTransform& to, btCollisionWorld::ConvexResultCallback& resultCallback) const + void PhysicsTaskScheduler::convexSweepTest(const btConvexShape* castShape, const btTransform& from, + const btTransform& to, btCollisionWorld::ConvexResultCallback& resultCallback) const { - MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lock(mCollisionWorldMutex, mLockingPolicy); mCollisionWorld->convexSweepTest(castShape, from, to, resultCallback); } - void PhysicsTaskScheduler::contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) + void PhysicsTaskScheduler::contactTest( + btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) { - std::shared_lock lock(mCollisionWorldMutex); + MaybeSharedLock lock(mCollisionWorldMutex, mLockingPolicy); ContactTestWrapper::contactTest(mCollisionWorld, colObj, resultCallback); } std::optional PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target) { - MaybeSharedLock lock(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lock(mCollisionWorldMutex, mLockingPolicy); // target the collision object's world origin, this should be the center of the collision object btTransform rayTo; rayTo.setIdentity(); @@ -353,72 +623,72 @@ namespace MWPhysics btCollisionWorld::ClosestRayResultCallback cb(from.getOrigin(), rayTo.getOrigin()); - mCollisionWorld->rayTestSingle(from, rayTo, target, target->getCollisionShape(), target->getWorldTransform(), cb); + mCollisionWorld->rayTestSingle( + from, rayTo, target, target->getCollisionShape(), target->getWorldTransform(), cb); if (!cb.hasHit()) // didn't hit the target. this could happen if point is already inside the collision box return std::nullopt; - return {cb.m_hitPointWorld}; + return { cb.m_hitPointWorld }; } - void PhysicsTaskScheduler::aabbTest(const btVector3& aabbMin, const btVector3& aabbMax, btBroadphaseAabbCallback& callback) + void PhysicsTaskScheduler::aabbTest( + const btVector3& aabbMin, const btVector3& aabbMax, btBroadphaseAabbCallback& callback) { - std::shared_lock lock(mCollisionWorldMutex); + MaybeSharedLock lock(mCollisionWorldMutex, mLockingPolicy); mCollisionWorld->getBroadphase()->aabbTest(aabbMin, aabbMax, callback); } void PhysicsTaskScheduler::getAabb(const btCollisionObject* obj, btVector3& min, btVector3& max) { - std::shared_lock lock(mCollisionWorldMutex); + MaybeSharedLock lock(mCollisionWorldMutex, mLockingPolicy); obj->getCollisionShape()->getAabb(obj->getWorldTransform(), min, max); } void PhysicsTaskScheduler::setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask) { - std::unique_lock lock(mCollisionWorldMutex); + MaybeExclusiveLock lock(mCollisionWorldMutex, mLockingPolicy); collisionObject->getBroadphaseHandle()->m_collisionFilterMask = collisionFilterMask; } - void PhysicsTaskScheduler::addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask) + void PhysicsTaskScheduler::addCollisionObject( + btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask) { - std::unique_lock lock(mCollisionWorldMutex); + MaybeExclusiveLock lock(mCollisionWorldMutex, mLockingPolicy); + mCollisionObjects.insert(collisionObject); mCollisionWorld->addCollisionObject(collisionObject, collisionFilterGroup, collisionFilterMask); } void PhysicsTaskScheduler::removeCollisionObject(btCollisionObject* collisionObject) { - std::unique_lock lock(mCollisionWorldMutex); + MaybeExclusiveLock lock(mCollisionWorldMutex, mLockingPolicy); + mCollisionObjects.erase(collisionObject); mCollisionWorld->removeCollisionObject(collisionObject); } - void PhysicsTaskScheduler::updateSingleAabb(std::weak_ptr ptr, bool immediate) + void PhysicsTaskScheduler::updateSingleAabb(const std::shared_ptr& ptr, bool immediate) { - if (!mDeferAabbUpdate || immediate) + if (immediate || mNumThreads == 0) { updatePtrAabb(ptr); } else { - std::unique_lock lock(mUpdateAabbMutex); - mUpdateAabb.insert(std::move(ptr)); + MaybeExclusiveLock lock(mUpdateAabbMutex, mLockingPolicy); + mUpdateAabb.insert(ptr); } } - bool PhysicsTaskScheduler::getLineOfSight(const std::weak_ptr& actor1, const std::weak_ptr& actor2) + bool PhysicsTaskScheduler::getLineOfSight( + const std::shared_ptr& actor1, const std::shared_ptr& actor2) { - std::unique_lock lock(mLOSCacheMutex); - - auto actorPtr1 = actor1.lock(); - auto actorPtr2 = actor2.lock(); - if (!actorPtr1 || !actorPtr2) - return false; + MaybeExclusiveLock lock(mLOSCacheMutex, mLockingPolicy); auto req = LOSRequest(actor1, actor2); auto result = std::find(mLOSCache.begin(), mLOSCache.end(), req); if (result == mLOSCache.end()) { - req.mResult = hasLineOfSight(actorPtr1.get(), actorPtr2.get()); - if (mLOSCacheExpiry >= 0) - mLOSCache.push_back(req); + req.mResult = hasLineOfSight(actor1.get(), actor2.get()); + mLOSCache.push_back(req); return req.mResult; } result->mAge = 0; @@ -427,7 +697,7 @@ namespace MWPhysics void PhysicsTaskScheduler::refreshLOSCache() { - std::shared_lock lock(mLOSCacheMutex); + MaybeSharedLock lock(mLOSCacheMutex, mLockingPolicy); int job = 0; int numLOS = mLOSCache.size(); while ((job = mNextLOS.fetch_add(1, std::memory_order_relaxed)) < numLOS) @@ -441,134 +711,89 @@ namespace MWPhysics else req.mResult = hasLineOfSight(actorPtr1.get(), actorPtr2.get()); } - } void PhysicsTaskScheduler::updateAabbs() { - std::scoped_lock lock(mUpdateAabbMutex); - std::for_each(mUpdateAabb.begin(), mUpdateAabb.end(), - [this](const std::weak_ptr& ptr) { updatePtrAabb(ptr); }); + MaybeExclusiveLock lock(mUpdateAabbMutex, mLockingPolicy); + std::for_each(mUpdateAabb.begin(), mUpdateAabb.end(), [this](const std::weak_ptr& ptr) { + auto p = ptr.lock(); + if (p != nullptr) + updatePtrAabb(p); + }); mUpdateAabb.clear(); } - void PhysicsTaskScheduler::updatePtrAabb(const std::weak_ptr& ptr) + void PhysicsTaskScheduler::updatePtrAabb(const std::shared_ptr& ptr) { - if (const auto p = ptr.lock()) + MaybeExclusiveLock lock(mCollisionWorldMutex, mLockingPolicy); + if (const auto actor = std::dynamic_pointer_cast(ptr)) { - std::scoped_lock lock(mCollisionWorldMutex); - if (const auto actor = std::dynamic_pointer_cast(p)) - { - actor->updateCollisionObjectPosition(); - mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); - } - else if (const auto object = std::dynamic_pointer_cast(p)) - { - object->commitPositionChange(); - mCollisionWorld->updateSingleAabb(object->getCollisionObject()); - } - else if (const auto projectile = std::dynamic_pointer_cast(p)) - { - projectile->commitPositionChange(); - mCollisionWorld->updateSingleAabb(projectile->getCollisionObject()); - } - }; + actor->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); + } + else if (const auto object = std::dynamic_pointer_cast(ptr)) + { + object->commitPositionChange(); + mCollisionWorld->updateSingleAabb(object->getCollisionObject()); + } + else if (const auto projectile = std::dynamic_pointer_cast(ptr)) + { + projectile->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(projectile->getCollisionObject()); + } } void PhysicsTaskScheduler::worker() { - std::shared_lock lock(mSimulationMutex); - while (!mQuit) - { - if (!mNewFrame) - mHasJob.wait(lock, [&]() { return mQuit || mNewFrame; }); - - mPreStepBarrier->wait([this] { afterPreStep(); }); - - int job = 0; - while (mRemainingSteps && (job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) - { - if(const auto actor = mActorsFrameData[job].mActor.lock()) - { - MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet); - MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData); - } - } - - mPostStepBarrier->wait([this] { afterPostStep(); }); - - if (!mRemainingSteps) - { - while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) - { - if(const auto actor = mActorsFrameData[job].mActor.lock()) - { - auto& actorData = mActorsFrameData[job]; - handleFall(actorData, mAdvanceSimulation); - } - } - - if (mLOSCacheExpiry >= 0) - refreshLOSCache(); - mPostSimBarrier->wait([this] { afterPostSim(); }); - } - } + mWorkersSync->runWorker([this] { + std::shared_lock lock(mSimulationMutex); + doSimulation(); + }); } void PhysicsTaskScheduler::updateActorsPositions() { - for (auto& actorData : mActorsFrameData) - { - if(const auto actor = actorData.mActor.lock()) - { - if (actor->setPosition(actorData.mPosition)) - { - std::scoped_lock lock(mCollisionWorldMutex); - actorData.mPosition = actor->getPosition(); // account for potential position change made by script - actor->updateCollisionObjectPosition(); - mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); - } - } - } + const Visitors::UpdatePosition impl{ mCollisionWorld }; + const Visitors::WithLockedPtr vis{ impl, mCollisionWorldMutex, + mLockingPolicy }; + for (Simulation& sim : *mSimulations) + std::visit(vis, sim); } bool PhysicsTaskScheduler::hasLineOfSight(const Actor* actor1, const Actor* actor2) { - btVector3 pos1 = Misc::Convert::toBullet(actor1->getCollisionObjectPosition() + osg::Vec3f(0,0,actor1->getHalfExtents().z() * 0.9)); // eye level - btVector3 pos2 = Misc::Convert::toBullet(actor2->getCollisionObjectPosition() + osg::Vec3f(0,0,actor2->getHalfExtents().z() * 0.9)); + btVector3 pos1 = Misc::Convert::toBullet( + actor1->getCollisionObjectPosition() + osg::Vec3f(0, 0, actor1->getHalfExtents().z() * 0.9)); // eye level + btVector3 pos2 = Misc::Convert::toBullet( + actor2->getCollisionObjectPosition() + osg::Vec3f(0, 0, actor2->getHalfExtents().z() * 0.9)); btCollisionWorld::ClosestRayResultCallback resultCallback(pos1, pos2); - resultCallback.m_collisionFilterGroup = 0xFF; - resultCallback.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap|CollisionType_Door; + resultCallback.m_collisionFilterGroup = CollisionType_AnyPhysical; + resultCallback.m_collisionFilterMask = CollisionType_World | CollisionType_HeightMap | CollisionType_Door; - MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet); + MaybeLock lockColWorld(mCollisionWorldMutex, mLockingPolicy); mCollisionWorld->rayTest(pos1, pos2, resultCallback); return !resultCallback.hasHit(); } - void PhysicsTaskScheduler::syncComputation() + void PhysicsTaskScheduler::doSimulation() { - while (mRemainingSteps--) + while (mRemainingSteps) { - for (auto& actorData : mActorsFrameData) - { - MovementSolver::unstuck(actorData, mCollisionWorld); - MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld, *mWorldFrameData); - } + mPreStepBarrier->wait([this] { afterPreStep(); }); + int job = 0; + const Visitors::Move impl{ mPhysicsDt, mCollisionWorld, *mWorldFrameData }; + const Visitors::WithLockedPtr vis{ impl, mCollisionWorldMutex, mLockingPolicy }; + while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) + std::visit(vis, (*mSimulations)[job]); - updateActorsPositions(); + mPostStepBarrier->wait([this] { afterPostStep(); }); } - for (auto& actorData : mActorsFrameData) - { - handleFall(actorData, mAdvanceSimulation); - actorData.mActorRaw->setSimulationPosition(interpolateMovements(actorData, mTimeAccum, mPhysicsDt)); - updateMechanics(actorData); - mMovedActors.emplace_back(actorData.mActorRaw->getPtr()); - if (mAdvanceSimulation) - actorData.mActorRaw->setStandingOnPtr(actorData.mStandingOn); - } + refreshLOSCache(); + mPostSimBarrier->wait([this] { afterPostSim(); }); } void PhysicsTaskScheduler::updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) @@ -588,22 +813,40 @@ namespace MWPhysics void PhysicsTaskScheduler::debugDraw() { - std::shared_lock lock(mCollisionWorldMutex); + MaybeSharedLock lock(mCollisionWorldMutex, mLockingPolicy); mDebugDrawer->step(); } + void* PhysicsTaskScheduler::getUserPointer(const btCollisionObject* object) const + { + auto it = mCollisionObjects.find(object); + if (it == mCollisionObjects.end()) + return nullptr; + return (*it)->getUserPointer(); + } + + void PhysicsTaskScheduler::releaseSharedStates() + { + waitForWorkers(); + std::scoped_lock lock(mSimulationMutex, mUpdateAabbMutex); + if (mSimulations != nullptr) + { + mSimulations->clear(); + mSimulations = nullptr; + } + mUpdateAabb.clear(); + } + void PhysicsTaskScheduler::afterPreStep() { - if (mDeferAabbUpdate) - updateAabbs(); + updateAabbs(); if (!mRemainingSteps) return; - for (auto& data : mActorsFrameData) - if (data.mActor.lock()) - { - std::unique_lock lock(mCollisionWorldMutex); - MovementSolver::unstuck(data, mCollisionWorld); - } + const Visitors::PreStep impl{ mCollisionWorld }; + const Visitors::WithLockedPtr vis{ impl, mCollisionWorldMutex, + mLockingPolicy }; + for (auto& sim : *mSimulations) + std::visit(vis, sim); } void PhysicsTaskScheduler::afterPostStep() @@ -618,15 +861,37 @@ namespace MWPhysics void PhysicsTaskScheduler::afterPostSim() { - mNewFrame = false; - if (mLOSCacheExpiry >= 0) { - std::unique_lock lock(mLOSCacheMutex); + MaybeExclusiveLock lock(mLOSCacheMutex, mLockingPolicy); mLOSCache.erase( - std::remove_if(mLOSCache.begin(), mLOSCache.end(), - [](const LOSRequest& req) { return req.mStale; }), - mLOSCache.end()); + std::remove_if(mLOSCache.begin(), mLOSCache.end(), [](const LOSRequest& req) { return req.mStale; }), + mLOSCache.end()); } mTimeEnd = mTimer->tick(); + if (mWorkersSync != nullptr) + mWorkersSync->workIsDone(); + } + + void PhysicsTaskScheduler::syncWithMainThread() + { + if (mSimulations == nullptr) + return; + const Visitors::Sync vis{ mAdvanceSimulation, mTimeAccum, mPhysicsDt, this }; + for (auto& sim : *mSimulations) + std::visit(vis, sim); + mSimulations->clear(); + mSimulations = nullptr; + } + + // Attempt to acquire unique lock on mSimulationMutex while not all worker + // threads are holding shared lock but will have to may lead to a deadlock because + // C++ standard does not guarantee priority for exclusive and shared locks + // for std::shared_mutex. For example microsoft STL implementation points out + // for the absence of such priority: + // https://docs.microsoft.com/en-us/windows/win32/sync/slim-reader-writer--srw--locks + void PhysicsTaskScheduler::waitForWorkers() + { + if (mWorkersSync != nullptr) + mWorkersSync->waitForWorkers(); } } diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp index 3fd4d5a6936..579211b489d 100644 --- a/apps/openmw/mwphysics/mtphysics.hpp +++ b/apps/openmw/mwphysics/mtphysics.hpp @@ -3,17 +3,20 @@ #include #include +#include #include +#include #include #include +#include #include #include +#include "components/misc/budgetmeasurement.hpp" #include "physicssystem.hpp" #include "ptrholder.hpp" -#include "components/misc/budgetmeasurement.hpp" namespace Misc { @@ -27,95 +30,112 @@ namespace MWRender namespace MWPhysics { + enum class LockingPolicy + { + NoLocks, + ExclusiveLocksOnly, + AllowSharedLocks, + }; + class PhysicsTaskScheduler { - public: - PhysicsTaskScheduler(float physicsDt, btCollisionWorld* collisionWorld, MWRender::DebugDrawer* debugDrawer); - ~PhysicsTaskScheduler(); - - /// @brief move actors taking into account desired movements and collisions - /// @param numSteps how much simulation step to run - /// @param timeAccum accumulated time from previous run to interpolate movements - /// @param actorsData per actor data needed to compute new positions - /// @return new position of each actor - const std::vector& moveActors(float & timeAccum, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); - - const std::vector& resetSimulation(const ActorMap& actors); - - // Thread safe wrappers - void rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const; - void convexSweepTest(const btConvexShape* castShape, const btTransform& from, const btTransform& to, btCollisionWorld::ConvexResultCallback& resultCallback) const; - void contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback); - std::optional getHitPoint(const btTransform& from, btCollisionObject* target); - void aabbTest(const btVector3& aabbMin, const btVector3& aabbMax, btBroadphaseAabbCallback& callback); - void getAabb(const btCollisionObject* obj, btVector3& min, btVector3& max); - void setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask); - void addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask); - void removeCollisionObject(btCollisionObject* collisionObject); - void updateSingleAabb(std::weak_ptr ptr, bool immediate=false); - bool getLineOfSight(const std::weak_ptr& actor1, const std::weak_ptr& actor2); - void debugDraw(); - - private: - void syncComputation(); - void worker(); - void updateActorsPositions(); - bool hasLineOfSight(const Actor* actor1, const Actor* actor2); - void refreshLOSCache(); - void updateAabbs(); - void updatePtrAabb(const std::weak_ptr& ptr); - void updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); - std::tuple calculateStepConfig(float timeAccum) const; - void afterPreStep(); - void afterPostStep(); - void afterPostSim(); - - std::unique_ptr mWorldFrameData; - std::vector mActorsFrameData; - std::vector mMovedActors; - float mDefaultPhysicsDt; - float mPhysicsDt; - float mTimeAccum; - btCollisionWorld* mCollisionWorld; - MWRender::DebugDrawer* mDebugDrawer; - std::vector mLOSCache; - std::set, std::owner_less>> mUpdateAabb; - - // TODO: use std::experimental::flex_barrier or std::barrier once it becomes a thing - std::unique_ptr mPreStepBarrier; - std::unique_ptr mPostStepBarrier; - std::unique_ptr mPostSimBarrier; - - int mNumThreads; - int mNumJobs; - int mRemainingSteps; - int mLOSCacheExpiry; - bool mDeferAabbUpdate; - bool mNewFrame; - bool mAdvanceSimulation; - bool mThreadSafeBullet; - bool mQuit; - std::atomic mNextJob; - std::atomic mNextLOS; - std::vector mThreads; - - mutable std::shared_mutex mSimulationMutex; - mutable std::shared_mutex mCollisionWorldMutex; - mutable std::shared_mutex mLOSCacheMutex; - mutable std::mutex mUpdateAabbMutex; - std::condition_variable_any mHasJob; - - unsigned int mFrameNumber; - const osg::Timer* mTimer; - - int mPrevStepCount; - Misc::BudgetMeasurement mBudget; - Misc::BudgetMeasurement mAsyncBudget; - unsigned int mBudgetCursor; - osg::Timer_t mAsyncStartTime; - osg::Timer_t mTimeBegin; - osg::Timer_t mTimeEnd; - osg::Timer_t mFrameStart; + public: + PhysicsTaskScheduler(float physicsDt, btCollisionWorld* collisionWorld, MWRender::DebugDrawer* debugDrawer); + ~PhysicsTaskScheduler(); + + /// @brief move actors taking into account desired movements and collisions + /// @param numSteps how much simulation step to run + /// @param timeAccum accumulated time from previous run to interpolate movements + /// @param actorsData per actor data needed to compute new positions + /// @return new position of each actor + void applyQueuedMovements(float& timeAccum, std::vector& simulations, osg::Timer_t frameStart, + unsigned int frameNumber, osg::Stats& stats); + + void resetSimulation(const ActorMap& actors); + + // Thread safe wrappers + void rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, + btCollisionWorld::RayResultCallback& resultCallback) const; + void convexSweepTest(const btConvexShape* castShape, const btTransform& from, const btTransform& to, + btCollisionWorld::ConvexResultCallback& resultCallback) const; + void contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback); + std::optional getHitPoint(const btTransform& from, btCollisionObject* target); + void aabbTest(const btVector3& aabbMin, const btVector3& aabbMax, btBroadphaseAabbCallback& callback); + void getAabb(const btCollisionObject* obj, btVector3& min, btVector3& max); + void setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask); + void addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask); + void removeCollisionObject(btCollisionObject* collisionObject); + void updateSingleAabb(const std::shared_ptr& ptr, bool immediate = false); + bool getLineOfSight(const std::shared_ptr& actor1, const std::shared_ptr& actor2); + void debugDraw(); + void* getUserPointer(const btCollisionObject* object) const; + void releaseSharedStates(); // destroy all objects whose destructor can't be safely called from + // ~PhysicsTaskScheduler() + + private: + class WorkersSync; + + void doSimulation(); + void worker(); + void updateActorsPositions(); + bool hasLineOfSight(const Actor* actor1, const Actor* actor2); + void refreshLOSCache(); + void updateAabbs(); + void updatePtrAabb(const std::shared_ptr& ptr); + void updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + std::tuple calculateStepConfig(float timeAccum) const; + void afterPreStep(); + void afterPostStep(); + void afterPostSim(); + void syncWithMainThread(); + void waitForWorkers(); + void prepareWork(float& timeAccum, std::vector& simulations, osg::Timer_t frameStart, + unsigned int frameNumber, osg::Stats& stats); + + std::unique_ptr mWorldFrameData; + std::vector* mSimulations = nullptr; + std::unordered_set mCollisionObjects; + float mDefaultPhysicsDt; + float mPhysicsDt; + float mTimeAccum; + btCollisionWorld* mCollisionWorld; + MWRender::DebugDrawer* mDebugDrawer; + std::vector mLOSCache; + std::set, std::owner_less>> mUpdateAabb; + + // TODO: use std::experimental::flex_barrier or std::barrier once it becomes a thing + std::unique_ptr mPreStepBarrier; + std::unique_ptr mPostStepBarrier; + std::unique_ptr mPostSimBarrier; + + LockingPolicy mLockingPolicy; + unsigned mNumThreads; + int mNumJobs; + int mRemainingSteps; + int mLOSCacheExpiry; + bool mAdvanceSimulation; + std::atomic mNextJob; + std::atomic mNextLOS; + std::vector mThreads; + + mutable std::shared_mutex mSimulationMutex; + mutable std::shared_mutex mCollisionWorldMutex; + mutable std::shared_mutex mLOSCacheMutex; + mutable std::mutex mUpdateAabbMutex; + + unsigned int mFrameNumber; + const osg::Timer* mTimer; + + int mPrevStepCount; + Misc::BudgetMeasurement mBudget; + Misc::BudgetMeasurement mAsyncBudget; + unsigned int mBudgetCursor; + osg::Timer_t mAsyncStartTime; + osg::Timer_t mTimeBegin; + osg::Timer_t mTimeEnd; + osg::Timer_t mFrameStart; + + std::unique_ptr mWorkersSync; }; } diff --git a/apps/openmw/mwphysics/object.cpp b/apps/openmw/mwphysics/object.cpp index 6363065323f..5936083f1d5 100644 --- a/apps/openmw/mwphysics/object.cpp +++ b/apps/openmw/mwphysics/object.cpp @@ -1,37 +1,36 @@ #include "object.hpp" #include "mtphysics.hpp" +#include #include +#include #include #include #include -#include #include -#include #include namespace MWPhysics { - Object::Object(const MWWorld::Ptr& ptr, osg::ref_ptr shapeInstance, int collisionType, PhysicsTaskScheduler* scheduler) - : mShapeInstance(shapeInstance) + Object::Object(const MWWorld::Ptr& ptr, osg::ref_ptr shapeInstance, + osg::Quat rotation, int collisionType, PhysicsTaskScheduler* scheduler) + : PtrHolder(ptr, osg::Vec3f()) + , mShapeInstance(std::move(shapeInstance)) , mSolid(true) + , mScale(ptr.getCellRef().getScale(), ptr.getCellRef().getScale(), ptr.getCellRef().getScale()) + , mPosition(ptr.getRefData().getPosition().asVec3()) + , mRotation(rotation) , mTaskScheduler(scheduler) + , mCollidedWith(ScriptedCollisionType_None) { - mPtr = ptr; - - mCollisionObject = std::make_unique(); - mCollisionObject->setCollisionShape(shapeInstance->getCollisionShape()); - + mCollisionObject = BulletHelpers::makeCollisionObject(mShapeInstance->mCollisionShape.get(), + Misc::Convert::toBullet(mPosition), Misc::Convert::toBullet(rotation)); mCollisionObject->setUserPointer(this); - - setScale(ptr.getCellRef().getScale()); - setRotation(Misc::Convert::toBullet(ptr.getRefData().getBaseNode()->getAttitude())); - setOrigin(Misc::Convert::toBullet(ptr.getRefData().getPosition().asVec3())); - commitPositionChange(); - - mTaskScheduler->addCollisionObject(mCollisionObject.get(), collisionType, CollisionType_Actor|CollisionType_HeightMap|CollisionType_Projectile); + mShapeInstance->setLocalScaling(mScale); + mTaskScheduler->addCollisionObject(mCollisionObject.get(), collisionType, + CollisionType_Actor | CollisionType_HeightMap | CollisionType_Projectile); } Object::~Object() @@ -47,21 +46,21 @@ namespace MWPhysics void Object::setScale(float scale) { std::unique_lock lock(mPositionMutex); - mScale = { scale,scale,scale }; + mScale = { scale, scale, scale }; mScaleUpdatePending = true; } - void Object::setRotation(const btQuaternion& quat) + void Object::setRotation(osg::Quat quat) { std::unique_lock lock(mPositionMutex); - mLocalTransform.setRotation(quat); + mRotation = quat; mTransformUpdatePending = true; } - void Object::setOrigin(const btVector3& vec) + void Object::updatePosition() { std::unique_lock lock(mPositionMutex); - mLocalTransform.setOrigin(vec); + mPosition = mPtr.getRefData().getPosition().asVec3(); mTransformUpdatePending = true; } @@ -75,25 +74,21 @@ namespace MWPhysics } if (mTransformUpdatePending) { - mCollisionObject->setWorldTransform(mLocalTransform); + btTransform trans; + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + trans.setRotation(Misc::Convert::toBullet(mRotation)); + mCollisionObject->setWorldTransform(trans); mTransformUpdatePending = false; } } - btCollisionObject* Object::getCollisionObject() - { - return mCollisionObject.get(); - } - - const btCollisionObject* Object::getCollisionObject() const - { - return mCollisionObject.get(); - } - btTransform Object::getTransform() const { std::unique_lock lock(mPositionMutex); - return mLocalTransform; + btTransform trans; + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + trans.setRotation(Misc::Convert::toBullet(mRotation)); + return trans; } bool Object::isSolid() const @@ -108,7 +103,7 @@ namespace MWPhysics bool Object::isAnimated() const { - return !mShapeInstance->mAnimatedShapes.empty(); + return mShapeInstance->isAnimated(); } bool Object::animateCollisionShapes() @@ -116,9 +111,13 @@ namespace MWPhysics if (mShapeInstance->mAnimatedShapes.empty()) return false; - assert (mShapeInstance->getCollisionShape()->isCompound()); + if (!mPtr.getRefData().getBaseNode()) + return false; + + assert(mShapeInstance->mCollisionShape->isCompound()); - btCompoundShape* compound = static_cast(mShapeInstance->getCollisionShape()); + btCompoundShape* compound = static_cast(mShapeInstance->mCollisionShape.get()); + bool result = false; for (const auto& [recIndex, shapeIndex] : mShapeInstance->mAnimatedShapes) { auto nodePathFound = mRecIndexToNodePath.find(recIndex); @@ -128,7 +127,8 @@ namespace MWPhysics mPtr.getRefData().getBaseNode()->accept(visitor); if (!visitor.mFound) { - Log(Debug::Warning) << "Warning: animateCollisionShapes can't find node " << recIndex << " for " << mPtr.getCellRef().getRefId(); + Log(Debug::Warning) << "Warning: animateCollisionShapes can't find node " << recIndex << " for " + << mPtr.getCellRef().getRefId(); // Remove nonexistent nodes from animated shapes map and early out mShapeInstance->mAnimatedShapes.erase(recIndex); @@ -141,19 +141,46 @@ namespace MWPhysics osg::NodePath& nodePath = nodePathFound->second; osg::Matrixf matrix = osg::computeLocalToWorld(nodePath); + btVector3 scale = Misc::Convert::toBullet(matrix.getScale()); matrix.orthoNormalize(matrix); btTransform transform; transform.setOrigin(Misc::Convert::toBullet(matrix.getTrans()) * compound->getLocalScaling()); - for (int i=0; i<3; ++i) - for (int j=0; j<3; ++j) - transform.getBasis()[i][j] = matrix(j,i); // NB column/row major difference + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + transform.getBasis()[i][j] = matrix(j, i); // NB column/row major difference + + btCollisionShape* childShape = compound->getChildShape(shapeIndex); + btVector3 newScale = compound->getLocalScaling() * scale; + + if (childShape->getLocalScaling() != newScale) + { + childShape->setLocalScaling(newScale); + result = true; + } - // Note: we can not apply scaling here for now since we treat scaled shapes - // as new shapes (btScaledBvhTriangleMeshShape) with 1.0 scale for now if (!(transform == compound->getChildTransform(shapeIndex))) + { compound->updateChildTransform(shapeIndex, transform); + result = true; + } } - return true; + return result; + } + + bool Object::collidedWith(ScriptedCollisionType type) const + { + return mCollidedWith & type; + } + + void Object::addCollision(ScriptedCollisionType type) + { + std::unique_lock lock(mPositionMutex); + mCollidedWith |= type; + } + + void Object::resetCollisions() + { + mCollidedWith = ScriptedCollisionType_None; } } diff --git a/apps/openmw/mwphysics/object.hpp b/apps/openmw/mwphysics/object.hpp index cae8771809e..15b7c469267 100644 --- a/apps/openmw/mwphysics/object.hpp +++ b/apps/openmw/mwphysics/object.hpp @@ -7,7 +7,6 @@ #include #include -#include #include namespace Resource @@ -15,27 +14,30 @@ namespace Resource class BulletShapeInstance; } -class btCollisionObject; -class btQuaternion; -class btVector3; - namespace MWPhysics { class PhysicsTaskScheduler; + enum ScriptedCollisionType : char + { + ScriptedCollisionType_None = 0, + ScriptedCollisionType_Actor = 1, + // Note that this isn't 3, colliding with a player doesn't count as colliding with an actor + ScriptedCollisionType_Player = 2 + }; + class Object final : public PtrHolder { public: - Object(const MWWorld::Ptr& ptr, osg::ref_ptr shapeInstance, int collisionType, PhysicsTaskScheduler* scheduler); + Object(const MWWorld::Ptr& ptr, osg::ref_ptr shapeInstance, osg::Quat rotation, + int collisionType, PhysicsTaskScheduler* scheduler); ~Object() override; const Resource::BulletShapeInstance* getShapeInstance() const; void setScale(float scale); - void setRotation(const btQuaternion& quat); - void setOrigin(const btVector3& vec); + void setRotation(osg::Quat quat); + void updatePosition(); void commitPositionChange(); - btCollisionObject* getCollisionObject(); - const btCollisionObject* getCollisionObject() const; btTransform getTransform() const; /// Return solid flag. Not used by the object itself, true by default. bool isSolid() const; @@ -44,18 +46,22 @@ namespace MWPhysics /// @brief update object shape /// @return true if shape changed bool animateCollisionShapes(); + bool collidedWith(ScriptedCollisionType type) const; + void addCollision(ScriptedCollisionType type); + void resetCollisions(); private: - std::unique_ptr mCollisionObject; osg::ref_ptr mShapeInstance; std::map mRecIndexToNodePath; bool mSolid; btVector3 mScale; - btTransform mLocalTransform; - bool mScaleUpdatePending; - bool mTransformUpdatePending; + osg::Vec3f mPosition; + osg::Quat mRotation; + bool mScaleUpdatePending = false; + bool mTransformUpdatePending = false; mutable std::mutex mPositionMutex; PhysicsTaskScheduler* mTaskScheduler; + char mCollidedWith; }; } diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 833bb9a161d..20a9c38b0fa 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -1,90 +1,108 @@ #include "physicssystem.hpp" -#include -#include +#include #include +#include + #include #include #include -#include -#include -#include -#include +#include #include #include #include -#include +#include +#include +#include #include +#include -#include -#include -#include #include -#include -#include -#include +#include +#include #include +#include +#include +#include +#include +#include -#include // FindRecIndexVisitor - -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/movement.hpp" -#include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/player.hpp" #include "../mwrender/bulletdebugdraw.hpp" #include "../mwworld/class.hpp" -#include "collisiontype.hpp" #include "actor.hpp" +#include "collisiontype.hpp" -#include "projectile.hpp" -#include "trace.h" -#include "object.hpp" -#include "heightfield.hpp" -#include "hasspherecollisioncallback.hpp" -#include "deepestnotmecontacttestresultcallback.hpp" #include "closestnotmerayresultcallback.hpp" #include "contacttestresultcallback.hpp" -#include "projectileconvexcallback.hpp" +#include "hasspherecollisioncallback.hpp" +#include "heightfield.hpp" #include "movementsolver.hpp" #include "mtphysics.hpp" +#include "object.hpp" +#include "projectile.hpp" namespace { - bool canMoveToWaterSurface(const MWPhysics::Actor* physicActor, const float waterlevel, btCollisionWorld* world) + void handleJump(const MWWorld::Ptr& ptr) { - if (!physicActor) - return false; - const float halfZ = physicActor->getHalfExtents().z(); - const osg::Vec3f actorPosition = physicActor->getPosition(); - const osg::Vec3f startingPosition(actorPosition.x(), actorPosition.y(), actorPosition.z() + halfZ); - const osg::Vec3f destinationPosition(actorPosition.x(), actorPosition.y(), waterlevel + halfZ); - MWPhysics::ActorTracer tracer; - tracer.doTrace(physicActor->getCollisionObject(), startingPosition, destinationPosition, world); - return (tracer.mFraction >= 1.0f); + if (!ptr.getClass().isActor()) + return; + if (ptr.getClass().getMovementSettings(ptr).mPosition[2] == 0) + return; + const bool isPlayer = (ptr == MWMechanics::getPlayer()); + // Advance acrobatics and set flag for GetPCJumping + if (isPlayer) + { + ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, ESM::Skill::Acrobatics_Jump); + MWBase::Environment::get().getWorld()->getPlayer().setJumping(true); + } + + // Decrease fatigue + if (!isPlayer || !MWBase::Environment::get().getWorld()->getGodModeState()) + { + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); + const float fFatigueJumpBase = gmst.find("fFatigueJumpBase")->mValue.getFloat(); + const float fFatigueJumpMult = gmst.find("fFatigueJumpMult")->mValue.getFloat(); + const float normalizedEncumbrance = std::min(1.f, ptr.getClass().getNormalizedEncumbrance(ptr)); + const float fatigueDecrease = fFatigueJumpBase + normalizedEncumbrance * fFatigueJumpMult; + MWMechanics::DynamicStat fatigue = ptr.getClass().getCreatureStats(ptr).getFatigue(); + fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); + ptr.getClass().getCreatureStats(ptr).setFatigue(fatigue); + } + ptr.getClass().getMovementSettings(ptr).mPosition[2] = 0; } + } namespace MWPhysics { PhysicsSystem::PhysicsSystem(Resource::ResourceSystem* resourceSystem, osg::ref_ptr parentNode) - : mShapeManager(new Resource::BulletShapeManager(resourceSystem->getVFS(), resourceSystem->getSceneManager(), resourceSystem->getNifFileManager())) + : mShapeManager( + std::make_unique(resourceSystem->getVFS(), resourceSystem->getSceneManager(), + resourceSystem->getNifFileManager(), Settings::cells().mCacheExpiryDelay)) , mResourceSystem(resourceSystem) , mDebugDrawEnabled(false) , mTimeAccum(0.0f) , mProjectileId(0) , mWaterHeight(0) , mWaterEnabled(false) - , mParentNode(parentNode) + , mParentNode(std::move(parentNode)) , mPhysicsDt(1.f / 60.f) { mResourceSystem->addResourceManager(mShapeManager.get()); @@ -93,21 +111,22 @@ namespace MWPhysics mDispatcher = std::make_unique(mCollisionConfiguration.get()); mBroadphase = std::make_unique(); - mCollisionWorld = std::make_unique(mDispatcher.get(), mBroadphase.get(), mCollisionConfiguration.get()); + mCollisionWorld + = std::make_unique(mDispatcher.get(), mBroadphase.get(), mCollisionConfiguration.get()); // Don't update AABBs of all objects every frame. Most objects in MW are static, so we don't need this. - // Should a "static" object ever be moved, we have to update its AABB manually using DynamicsWorld::updateSingleAabb. + // Should a "static" object ever be moved, we have to update its AABB manually using + // DynamicsWorld::updateSingleAabb. mCollisionWorld->setForceUpdateAllAabbs(false); // Check if a user decided to override a physics system FPS - const char* env = getenv("OPENMW_PHYSICS_FPS"); - if (env) + if (const char* env = getenv("OPENMW_PHYSICS_FPS")) { - float physFramerate = std::atof(env); - if (physFramerate > 0) + if (const auto physFramerate = Misc::StringUtils::toNumeric(env); + physFramerate.has_value() && *physFramerate > 0) { - mPhysicsDt = 1.f / physFramerate; - Log(Debug::Warning) << "Warning: using custom physics framerate (" << physFramerate << " FPS)."; + mPhysicsDt = 1.f / *physFramerate; + Log(Debug::Warning) << "Warning: using custom physics framerate (" << *physFramerate << " FPS)."; } } @@ -122,18 +141,14 @@ namespace MWPhysics if (mWaterCollisionObject) mTaskScheduler->removeCollisionObject(mWaterCollisionObject.get()); + mTaskScheduler->releaseSharedStates(); mHeightFields.clear(); mObjects.clear(); mActors.clear(); mProjectiles.clear(); } - void PhysicsSystem::setUnrefQueue(SceneUtil::UnrefQueue *unrefQueue) - { - mUnrefQueue = unrefQueue; - } - - Resource::BulletShapeManager *PhysicsSystem::getShapeManager() + Resource::BulletShapeManager* PhysicsSystem::getShapeManager() { return mShapeManager.get(); } @@ -147,26 +162,26 @@ namespace MWPhysics return mDebugDrawEnabled; } - void PhysicsSystem::markAsNonSolid(const MWWorld::ConstPtr &ptr) + void PhysicsSystem::markAsNonSolid(const MWWorld::ConstPtr& ptr) { - ObjectMap::iterator found = mObjects.find(ptr); + ObjectMap::iterator found = mObjects.find(ptr.mRef); if (found == mObjects.end()) return; found->second->setSolid(false); } - bool PhysicsSystem::isOnSolidGround (const MWWorld::Ptr& actor) const + bool PhysicsSystem::isOnSolidGround(const MWWorld::Ptr& actor) const { const Actor* physactor = getActor(actor); - if (!physactor || !physactor->getOnGround()) + if (!physactor || !physactor->getOnGround() || !physactor->getCollisionMode()) return false; const auto obj = physactor->getStandingOnPtr(); if (obj.isEmpty()) return true; // assume standing on terrain (which is a non-object, so not collision tracked) - ObjectMap::const_iterator foundObj = mObjects.find(obj); + ObjectMap::const_iterator foundObj = mObjects.find(obj.mRef); if (foundObj == mObjects.end()) return false; @@ -176,92 +191,9 @@ namespace MWPhysics return true; } - std::pair PhysicsSystem::getHitContact(const MWWorld::ConstPtr& actor, - const osg::Vec3f &origin, - const osg::Quat &orient, - float queryDistance, std::vector& targets) - { - // First of all, try to hit where you aim to - int hitmask = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor; - RayCastingResult result = castRay(origin, origin + (orient * osg::Vec3f(0.0f, queryDistance, 0.0f)), actor, targets, hitmask, CollisionType_Actor); - - if (result.mHit) - { - reportCollision(Misc::Convert::toBullet(result.mHitPos), Misc::Convert::toBullet(result.mHitNormal)); - return std::make_pair(result.mHitObject, result.mHitPos); - } - - // Use cone shape as fallback - const MWWorld::Store &store = MWBase::Environment::get().getWorld()->getStore().get(); - - btConeShape shape (osg::DegreesToRadians(store.find("fCombatAngleXY")->mValue.getFloat()/2.0f), queryDistance); - shape.setLocalScaling(btVector3(1, 1, osg::DegreesToRadians(store.find("fCombatAngleZ")->mValue.getFloat()/2.0f) / - shape.getRadius())); - - // The shape origin is its center, so we have to move it forward by half the length. The - // real origin will be provided to getFilteredContact to find the closest. - osg::Vec3f center = origin + (orient * osg::Vec3f(0.0f, queryDistance*0.5f, 0.0f)); - - btCollisionObject object; - object.setCollisionShape(&shape); - object.setWorldTransform(btTransform(Misc::Convert::toBullet(orient), Misc::Convert::toBullet(center))); - - const btCollisionObject* me = nullptr; - std::vector targetCollisionObjects; - - const Actor* physactor = getActor(actor); - if (physactor) - me = physactor->getCollisionObject(); - - if (!targets.empty()) - { - for (MWWorld::Ptr& target : targets) - { - const Actor* targetActor = getActor(target); - if (targetActor) - targetCollisionObjects.push_back(targetActor->getCollisionObject()); - } - } - - DeepestNotMeContactTestResultCallback resultCallback(me, targetCollisionObjects, Misc::Convert::toBullet(origin)); - resultCallback.m_collisionFilterGroup = CollisionType_Actor; - resultCallback.m_collisionFilterMask = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor; - mTaskScheduler->contactTest(&object, resultCallback); - - if (resultCallback.mObject) - { - PtrHolder* holder = static_cast(resultCallback.mObject->getUserPointer()); - if (holder) - { - reportCollision(resultCallback.mContactPoint, resultCallback.mContactNormal); - return std::make_pair(holder->getPtr(), Misc::Convert::toOsg(resultCallback.mContactPoint)); - } - } - return std::make_pair(MWWorld::Ptr(), osg::Vec3f()); - } - - float PhysicsSystem::getHitDistance(const osg::Vec3f &point, const MWWorld::ConstPtr &target) const - { - btCollisionObject* targetCollisionObj = nullptr; - const Actor* actor = getActor(target); - if (actor) - targetCollisionObj = actor->getCollisionObject(); - if (!targetCollisionObj) - return 0.f; - - btTransform rayFrom; - rayFrom.setIdentity(); - rayFrom.setOrigin(Misc::Convert::toBullet(point)); - - auto hitpoint = mTaskScheduler->getHitPoint(rayFrom, targetCollisionObj); - if (hitpoint) - return (point - Misc::Convert::toOsg(*hitpoint)).length(); - - // didn't hit the target. this could happen if point is already inside the collision box - return 0.f; - } - - RayCastingResult PhysicsSystem::castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore, std::vector targets, int mask, int group) const + RayCastingResult PhysicsSystem::castRay(const osg::Vec3f& from, const osg::Vec3f& to, + const std::vector& ignore, const std::vector& targets, int mask, + int group) const { if (from == to) { @@ -272,25 +204,28 @@ namespace MWPhysics btVector3 btFrom = Misc::Convert::toBullet(from); btVector3 btTo = Misc::Convert::toBullet(to); - const btCollisionObject* me = nullptr; + std::vector ignoreList; std::vector targetCollisionObjects; - if (!ignore.isEmpty()) + for (const auto& ptr : ignore) { - const Actor* actor = getActor(ignore); - if (actor) - me = actor->getCollisionObject(); - else + if (!ptr.isEmpty()) { - const Object* object = getObject(ignore); - if (object) - me = object->getCollisionObject(); + const Actor* actor = getActor(ptr); + if (actor) + ignoreList.push_back(actor->getCollisionObject()); + else + { + const Object* object = getObject(ptr); + if (object) + ignoreList.push_back(object->getCollisionObject()); + } } } if (!targets.empty()) { - for (MWWorld::Ptr& target : targets) + for (const MWWorld::Ptr& target : targets) { const Actor* actor = getActor(target); if (actor) @@ -298,7 +233,7 @@ namespace MWPhysics } } - ClosestNotMeRayResultCallback resultCallback(me, targetCollisionObjects, btFrom, btTo); + ClosestNotMeRayResultCallback resultCallback(ignoreList, targetCollisionObjects, btFrom, btTo); resultCallback.m_collisionFilterGroup = group; resultCallback.m_collisionFilterMask = mask; @@ -316,17 +251,19 @@ namespace MWPhysics return result; } - RayCastingResult PhysicsSystem::castSphere(const osg::Vec3f &from, const osg::Vec3f &to, float radius) const + RayCastingResult PhysicsSystem::castSphere( + const osg::Vec3f& from, const osg::Vec3f& to, float radius, int mask, int group) const { - btCollisionWorld::ClosestConvexResultCallback callback(Misc::Convert::toBullet(from), Misc::Convert::toBullet(to)); - callback.m_collisionFilterGroup = 0xff; - callback.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap|CollisionType_Door; + btCollisionWorld::ClosestConvexResultCallback callback( + Misc::Convert::toBullet(from), Misc::Convert::toBullet(to)); + callback.m_collisionFilterGroup = group; + callback.m_collisionFilterMask = mask; btSphereShape shape(radius); const btQuaternion btrot = btQuaternion::getIdentity(); - btTransform from_ (btrot, Misc::Convert::toBullet(from)); - btTransform to_ (btrot, Misc::Convert::toBullet(to)); + btTransform from_(btrot, Misc::Convert::toBullet(from)); + btTransform to_(btrot, Misc::Convert::toBullet(to)); mTaskScheduler->convexSweepTest(&shape, from_, to_, callback); @@ -336,35 +273,38 @@ namespace MWPhysics { result.mHitPos = Misc::Convert::toOsg(callback.m_hitPointWorld); result.mHitNormal = Misc::Convert::toOsg(callback.m_hitNormalWorld); + if (auto* ptrHolder = static_cast(callback.m_hitCollisionObject->getUserPointer())) + result.mHitObject = ptrHolder->getPtr(); } return result; } - bool PhysicsSystem::getLineOfSight(const MWWorld::ConstPtr &actor1, const MWWorld::ConstPtr &actor2) const + bool PhysicsSystem::getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const { - const auto getWeakPtr = [&](const MWWorld::ConstPtr &ptr) -> std::weak_ptr - { - const auto found = mActors.find(ptr); - if (found != mActors.end()) - return { found->second }; - return {}; - }; + if (actor1 == actor2) + return true; - return mTaskScheduler->getLineOfSight(getWeakPtr(actor1), getWeakPtr(actor2)); + const auto it1 = mActors.find(actor1.mRef); + const auto it2 = mActors.find(actor2.mRef); + if (it1 == mActors.end() || it2 == mActors.end()) + return false; + + return mTaskScheduler->getLineOfSight(it1->second, it2->second); } - bool PhysicsSystem::isOnGround(const MWWorld::Ptr &actor) + bool PhysicsSystem::isOnGround(const MWWorld::Ptr& actor) { Actor* physactor = getActor(actor); - return physactor && physactor->getOnGround(); + return physactor && physactor->getOnGround() && physactor->getCollisionMode(); } - bool PhysicsSystem::canMoveToWaterSurface(const MWWorld::ConstPtr &actor, const float waterlevel) + bool PhysicsSystem::canMoveToWaterSurface(const MWWorld::ConstPtr& actor, const float waterlevel) { - return ::canMoveToWaterSurface(getActor(actor), waterlevel, mCollisionWorld.get()); + const auto* physactor = getActor(actor); + return physactor && physactor->canMoveToWaterSurface(waterlevel, mCollisionWorld.get()); } - osg::Vec3f PhysicsSystem::getHalfExtents(const MWWorld::ConstPtr &actor) const + osg::Vec3f PhysicsSystem::getHalfExtents(const MWWorld::ConstPtr& actor) const { const Actor* physactor = getActor(actor); if (physactor) @@ -373,7 +313,7 @@ namespace MWPhysics return osg::Vec3f(); } - osg::Vec3f PhysicsSystem::getOriginalHalfExtents(const MWWorld::ConstPtr &actor) const + osg::Vec3f PhysicsSystem::getOriginalHalfExtents(const MWWorld::ConstPtr& actor) const { if (const Actor* physactor = getActor(actor)) return physactor->getOriginalHalfExtents(); @@ -381,7 +321,7 @@ namespace MWPhysics return osg::Vec3f(); } - osg::Vec3f PhysicsSystem::getRenderingHalfExtents(const MWWorld::ConstPtr &actor) const + osg::Vec3f PhysicsSystem::getRenderingHalfExtents(const MWWorld::ConstPtr& actor) const { const Actor* physactor = getActor(actor); if (physactor) @@ -390,16 +330,17 @@ namespace MWPhysics return osg::Vec3f(); } - osg::BoundingBox PhysicsSystem::getBoundingBox(const MWWorld::ConstPtr &object) const + osg::BoundingBox PhysicsSystem::getBoundingBox(const MWWorld::ConstPtr& object) const { - const Object * physobject = getObject(object); - if (!physobject) return osg::BoundingBox(); + const Object* physobject = getObject(object); + if (!physobject) + return osg::BoundingBox(); btVector3 min, max; mTaskScheduler->getAabb(physobject->getCollisionObject(), min, max); return osg::BoundingBox(Misc::Convert::toOsg(min), Misc::Convert::toOsg(max)); } - osg::Vec3f PhysicsSystem::getCollisionObjectPosition(const MWWorld::ConstPtr &actor) const + osg::Vec3f PhysicsSystem::getCollisionObjectPosition(const MWWorld::ConstPtr& actor) const { const Actor* physactor = getActor(actor); if (physactor) @@ -408,24 +349,26 @@ namespace MWPhysics return osg::Vec3f(); } - std::vector PhysicsSystem::getCollisionsPoints(const MWWorld::ConstPtr &ptr, int collisionGroup, int collisionMask) const + std::vector PhysicsSystem::getCollisionsPoints( + const MWWorld::ConstPtr& ptr, int collisionGroup, int collisionMask) const { btCollisionObject* me = nullptr; - auto found = mObjects.find(ptr); + auto found = mObjects.find(ptr.mRef); if (found != mObjects.end()) me = found->second->getCollisionObject(); else return {}; - ContactTestResultCallback resultCallback (me); + ContactTestResultCallback resultCallback(me); resultCallback.m_collisionFilterGroup = collisionGroup; resultCallback.m_collisionFilterMask = collisionMask; mTaskScheduler->contactTest(me, resultCallback); return resultCallback.mResult; } - std::vector PhysicsSystem::getCollisions(const MWWorld::ConstPtr &ptr, int collisionGroup, int collisionMask) const + std::vector PhysicsSystem::getCollisions( + const MWWorld::ConstPtr& ptr, int collisionGroup, int collisionMask) const { std::vector actors; for (auto& [actor, point, normal] : getCollisionsPoints(ptr, collisionGroup, collisionMask)) @@ -433,23 +376,25 @@ namespace MWPhysics return actors; } - osg::Vec3f PhysicsSystem::traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, float maxHeight) + osg::Vec3f PhysicsSystem::traceDown(const MWWorld::Ptr& ptr, const osg::Vec3f& position, float maxHeight) { - ActorMap::iterator found = mActors.find(ptr); - if (found == mActors.end()) + ActorMap::iterator found = mActors.find(ptr.mRef); + if (found == mActors.end()) return ptr.getRefData().getPosition().asVec3(); return MovementSolver::traceDown(ptr, position, found->second.get(), mCollisionWorld.get(), maxHeight); } - void PhysicsSystem::addHeightField (const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject) + void PhysicsSystem::addHeightField( + const float* heights, int x, int y, int size, int verts, float minH, float maxH, const osg::Object* holdObject) { - mHeightFields[std::make_pair(x,y)] = std::make_unique(heights, x, y, triSize, sqrtVerts, minH, maxH, holdObject, mTaskScheduler.get()); + mHeightFields[std::make_pair(x, y)] + = std::make_unique(heights, x, y, size, verts, minH, maxH, holdObject, mTaskScheduler.get()); } - void PhysicsSystem::removeHeightField (int x, int y) + void PhysicsSystem::removeHeightField(int x, int y) { - HeightFieldMap::iterator heightfield = mHeightFields.find(std::make_pair(x,y)); - if(heightfield != mHeightFields.end()) + HeightFieldMap::iterator heightfield = mHeightFields.find(std::make_pair(x, y)); + if (heightfield != mHeightFields.end()) mHeightFields.erase(heightfield); } @@ -461,34 +406,50 @@ namespace MWPhysics return heightField->second.get(); } - void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh, int collisionType) + void PhysicsSystem::addObject( + const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType) { - osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); - if (!shapeInstance || !shapeInstance->getCollisionShape()) + if (ptr.mRef->mData.mPhysicsPostponed) return; - auto obj = std::make_shared(ptr, shapeInstance, collisionType, mTaskScheduler.get()); - mObjects.emplace(ptr, obj); + std::string animationMesh = mesh; + if (ptr.getClass().useAnim()) + animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); + osg::ref_ptr shapeInstance = mShapeManager->getInstance(animationMesh); + if (!shapeInstance || !shapeInstance->mCollisionShape) + return; + + assert(!getObject(ptr)); + + // Override collision type based on shape content. + switch (shapeInstance->mVisualCollisionType) + { + case Resource::VisualCollisionType::None: + break; + case Resource::VisualCollisionType::Default: + collisionType = CollisionType_VisualOnly; + break; + case Resource::VisualCollisionType::Camera: + collisionType = CollisionType_CameraOnly; + break; + } + + auto obj = std::make_shared(ptr, shapeInstance, rotation, collisionType, mTaskScheduler.get()); + mObjects.emplace(ptr.mRef, obj); if (obj->isAnimated()) - mAnimatedObjects.insert(obj.get()); + mAnimatedObjects.emplace(obj.get(), false); } - void PhysicsSystem::remove(const MWWorld::Ptr &ptr) + void PhysicsSystem::remove(const MWWorld::Ptr& ptr) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { - if (mUnrefQueue.get()) - mUnrefQueue->push(found->second->getShapeInstance()); - - mAnimatedObjects.erase(found->second.get()); + mAnimatedObjects.erase(foundObject->second.get()); - mObjects.erase(found); + mObjects.erase(foundObject); } - - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { mActors.erase(foundActor); } @@ -501,25 +462,12 @@ namespace MWPhysics mProjectiles.erase(foundProjectile); } - void PhysicsSystem::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) + void PhysicsSystem::updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& updated) { - ObjectMap::iterator found = mObjects.find(old); - if (found != mObjects.end()) - { - auto obj = found->second; - obj->updatePtr(updated); - mObjects.erase(found); - mObjects.emplace(updated, std::move(obj)); - } - - ActorMap::iterator foundActor = mActors.find(old); - if (foundActor != mActors.end()) - { - auto actor = foundActor->second; - actor->updatePtr(updated); - mActors.erase(foundActor); - mActors.emplace(updated, std::move(actor)); - } + if (auto foundObject = mObjects.find(old.mRef); foundObject != mObjects.end()) + foundObject->second->updatePtr(updated); + else if (auto foundActor = mActors.find(old.mRef); foundActor != mActors.end()) + foundActor->second->updatePtr(updated); for (auto& [_, actor] : mActors) { @@ -532,28 +480,27 @@ namespace MWPhysics if (projectile->getCaster() == old) projectile->setCaster(updated); } - } - Actor *PhysicsSystem::getActor(const MWWorld::Ptr &ptr) + Actor* PhysicsSystem::getActor(const MWWorld::Ptr& ptr) { - ActorMap::iterator found = mActors.find(ptr); + ActorMap::iterator found = mActors.find(ptr.mRef); if (found != mActors.end()) return found->second.get(); return nullptr; } - const Actor *PhysicsSystem::getActor(const MWWorld::ConstPtr &ptr) const + const Actor* PhysicsSystem::getActor(const MWWorld::ConstPtr& ptr) const { - ActorMap::const_iterator found = mActors.find(ptr); + ActorMap::const_iterator found = mActors.find(ptr.mRef); if (found != mActors.end()) return found->second.get(); return nullptr; } - const Object* PhysicsSystem::getObject(const MWWorld::ConstPtr &ptr) const + const Object* PhysicsSystem::getObject(const MWWorld::ConstPtr& ptr) const { - ObjectMap::const_iterator found = mObjects.find(ptr); + ObjectMap::const_iterator found = mObjects.find(ptr.mRef); if (found != mObjects.end()) return found->second.get(); return nullptr; @@ -567,133 +514,89 @@ namespace MWPhysics return nullptr; } - void PhysicsSystem::updateScale(const MWWorld::Ptr &ptr) + void PhysicsSystem::updateScale(const MWWorld::Ptr& ptr) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { float scale = ptr.getCellRef().getScale(); - found->second->setScale(scale); - mTaskScheduler->updateSingleAabb(found->second); - return; + foundObject->second->setScale(scale); + mTaskScheduler->updateSingleAabb(foundObject->second); } - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { foundActor->second->updateScale(); mTaskScheduler->updateSingleAabb(foundActor->second); - return; } } - void PhysicsSystem::updateProjectile(const int projectileId, const osg::Vec3f &position) const - { - const auto foundProjectile = mProjectiles.find(projectileId); - assert(foundProjectile != mProjectiles.end()); - auto* projectile = foundProjectile->second.get(); - - btVector3 btFrom = Misc::Convert::toBullet(projectile->getPosition()); - btVector3 btTo = Misc::Convert::toBullet(position); - - if (btFrom == btTo) - return; - - const auto casterPtr = projectile->getCaster(); - const auto* caster = [this,&casterPtr]() -> const btCollisionObject* - { - const Actor* actor = getActor(casterPtr); - if (actor) - return actor->getCollisionObject(); - const Object* object = getObject(casterPtr); - if (object) - return object->getCollisionObject(); - return nullptr; - }(); - - ProjectileConvexCallback resultCallback(caster, btFrom, btTo, projectile); - resultCallback.m_collisionFilterMask = 0xff; - resultCallback.m_collisionFilterGroup = CollisionType_Projectile; - - const btQuaternion btrot = btQuaternion::getIdentity(); - btTransform from_ (btrot, btFrom); - btTransform to_ (btrot, btTo); - - mTaskScheduler->convexSweepTest(projectile->getConvexShape(), from_, to_, resultCallback); - - const auto newpos = projectile->isActive() ? position : Misc::Convert::toOsg(resultCallback.m_hitPointWorld); - projectile->setPosition(newpos); - mTaskScheduler->updateSingleAabb(foundProjectile->second); - } - - void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr) + void PhysicsSystem::updateRotation(const MWWorld::Ptr& ptr, osg::Quat rotate) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { - found->second->setRotation(Misc::Convert::toBullet(ptr.getRefData().getBaseNode()->getAttitude())); - mTaskScheduler->updateSingleAabb(found->second); - return; + foundObject->second->setRotation(rotate); + mTaskScheduler->updateSingleAabb(foundObject->second); } - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { if (!foundActor->second->isRotationallyInvariant()) { - foundActor->second->updateRotation(); + foundActor->second->setRotation(rotate); mTaskScheduler->updateSingleAabb(foundActor->second); } - return; } } - void PhysicsSystem::updatePosition(const MWWorld::Ptr &ptr) + void PhysicsSystem::updatePosition(const MWWorld::Ptr& ptr) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { - found->second->setOrigin(Misc::Convert::toBullet(ptr.getRefData().getPosition().asVec3())); - mTaskScheduler->updateSingleAabb(found->second); - return; + foundObject->second->updatePosition(); + mTaskScheduler->updateSingleAabb(foundObject->second); } - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { foundActor->second->updatePosition(); mTaskScheduler->updateSingleAabb(foundActor->second, true); - return; } } - void PhysicsSystem::addActor (const MWWorld::Ptr& ptr, const std::string& mesh) + void PhysicsSystem::addActor(const MWWorld::Ptr& ptr, const std::string& mesh) { - osg::ref_ptr shape = mShapeManager->getShape(mesh); + std::string animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); + osg::ref_ptr shape = mShapeManager->getShape(animationMesh); // Try to get shape from basic model as fallback for creatures - if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.extents.length2() == 0) + if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.mExtents.length2() == 0) { - const std::string fallbackModel = ptr.getClass().getModel(ptr); - if (fallbackModel != mesh) + if (animationMesh != mesh) { - shape = mShapeManager->getShape(fallbackModel); + shape = mShapeManager->getShape(mesh); } } if (!shape) return; - auto actor = std::make_shared(ptr, shape, mTaskScheduler.get()); - mActors.emplace(ptr, std::move(actor)); + // check if Actor should spawn above water + const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); + const bool canWaterWalk = effects.getOrDefault(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; + + auto actor = std::make_shared( + ptr, shape, mTaskScheduler.get(), canWaterWalk, Settings::game().mActorCollisionShapeType); + + mActors.emplace(ptr.mRef, std::move(actor)); } - int PhysicsSystem::addProjectile (const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius, bool canTraverseWater) + int PhysicsSystem::addProjectile( + const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius) { osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); assert(shapeInstance); - float radius = computeRadius ? shapeInstance->mCollisionBox.extents.length() / 2.f : 1.f; + float radius = computeRadius ? shapeInstance->mCollisionBox.mExtents.length() / 2.f : 1.f; mProjectileId++; - auto projectile = std::make_shared(caster, position, radius, canTraverseWater, mTaskScheduler.get(), this); + auto projectile = std::make_shared(caster, position, radius, mTaskScheduler.get(), this); mProjectiles.emplace(mProjectileId, std::move(projectile)); return mProjectileId; @@ -710,7 +613,7 @@ namespace MWPhysics bool PhysicsSystem::toggleCollisionMode() { - ActorMap::iterator found = mActors.find(MWMechanics::getPlayer()); + ActorMap::iterator found = mActors.find(MWMechanics::getPlayer().mRef); if (found != mActors.end()) { bool cmode = found->second->getCollisionMode(); @@ -723,9 +626,9 @@ namespace MWPhysics return false; } - void PhysicsSystem::queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity) + void PhysicsSystem::queueObjectMovement(const MWWorld::Ptr& ptr, const osg::Vec3f& velocity) { - ActorMap::iterator found = mActors.find(ptr); + ActorMap::iterator found = mActors.find(ptr.mRef); if (found != mActors.end()) found->second->setVelocity(velocity); } @@ -733,75 +636,130 @@ namespace MWPhysics void PhysicsSystem::clearQueuedMovement() { for (const auto& [_, actor] : mActors) + { actor->setVelocity(osg::Vec3f()); + actor->setInertialForce(osg::Vec3f()); + } } - const std::vector& PhysicsSystem::applyQueuedMovement(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) - { - mTimeAccum += dt; - - if (skipSimulation) - return mTaskScheduler->resetSimulation(mActors); - - // modifies mTimeAccum - return mTaskScheduler->moveActors(mTimeAccum, prepareFrameData(mTimeAccum >= mPhysicsDt), frameStart, frameNumber, stats); - } - - std::vector PhysicsSystem::prepareFrameData(bool willSimulate) + void PhysicsSystem::prepareSimulation(bool willSimulate, std::vector& simulations) { - std::vector actorsFrameData; - actorsFrameData.reserve(mActors.size()); - const MWBase::World *world = MWBase::Environment::get().getWorld(); - for (const auto& [ptr, physicActor] : mActors) + assert(simulations.empty()); + simulations.reserve(mActors.size() + mProjectiles.size()); + const MWBase::World* world = MWBase::Environment::get().getWorld(); + for (const auto& [ref, physicActor] : mActors) { + if (!physicActor->isActive()) + continue; + + 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()) + const MWWorld::CellStore* cell = ptr.getCell(); + if (cell->getCell()->hasWater()) waterlevel = cell->getWaterLevel(); - const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(physicActor->getPtr()).getMagicEffects(); + const auto& stats = ptr.getClass().getCreatureStats(ptr); + const MWMechanics::MagicEffects& effects = stats.getMagicEffects(); bool waterCollision = false; - if (cell->getCell()->hasWater() && effects.get(ESM::MagicEffect::WaterWalking).getMagnitude()) + if (cell->getCell()->hasWater() && effects.getOrDefault(ESM::MagicEffect::WaterWalking).getMagnitude()) { - if (physicActor->getCollisionMode() || !world->isUnderwater(ptr.getCell(), osg::Vec3f(ptr.getRefData().getPosition().asVec3()))) + if (physicActor->getCollisionMode() + || !world->isUnderwater(ptr.getCell(), ptr.getRefData().getPosition().asVec3())) waterCollision = true; } physicActor->setCanWaterWalk(waterCollision); // Slow fall reduces fall speed by a factor of (effect magnitude / 200) - const float slowFall = 1.f - std::max(0.f, std::min(1.f, effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f)); + const float slowFall + = 1.f - std::clamp(effects.getOrDefault(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f, 0.f, 1.f); + const bool isPlayer = ptr == world->getPlayerConstPtr(); + const bool godmode = isPlayer && world->getGodModeState(); + const bool inert = stats.isDead() + || (!godmode && stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Paralyze).getModifier() > 0); + + simulations.emplace_back(ActorSimulation{ + physicActor, ActorFrameData{ *physicActor, inert, waterCollision, slowFall, waterlevel, isPlayer } }); - // Ue current value only if we don't advance the simulation. Otherwise we might get a stale value. - MWWorld::Ptr standingOn; - if (!willSimulate) - standingOn = physicActor->getStandingOnPtr(); + // if the simulation will run, a jump request will be fulfilled. Update mechanics accordingly. + if (willSimulate) + handleJump(ptr); + } - actorsFrameData.emplace_back(physicActor, standingOn, waterCollision, slowFall, waterlevel); + for (const auto& [id, projectile] : mProjectiles) + { + simulations.emplace_back(ProjectileSimulation{ projectile, ProjectileFrameData{ *projectile } }); } - return actorsFrameData; } - void PhysicsSystem::stepSimulation() + void PhysicsSystem::stepSimulation( + float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { - for (Object* animatedObject : mAnimatedObjects) + for (auto& [animatedObject, changed] : mAnimatedObjects) + { if (animatedObject->animateCollisionShapes()) { - auto obj = mObjects.find(animatedObject->getPtr()); + auto obj = mObjects.find(animatedObject->getPtr().mRef); assert(obj != mObjects.end()); mTaskScheduler->updateSingleAabb(obj->second); + changed = true; + } + else + { + changed = false; } + } + for (auto& [_, object] : mObjects) + object->resetCollisions(); #ifndef BT_NO_PROFILE CProfileManager::Reset(); CProfileManager::Increment_Frame_Counter(); #endif + + mTimeAccum += dt; + + if (skipSimulation) + mTaskScheduler->resetSimulation(mActors); + else + { + std::vector& simulations = mSimulations[mSimulationsCounter++ % mSimulations.size()]; + prepareSimulation(mTimeAccum >= mPhysicsDt, simulations); + // modifies mTimeAccum + mTaskScheduler->applyQueuedMovements(mTimeAccum, simulations, frameStart, frameNumber, stats); + } + } + + void PhysicsSystem::moveActors() + { + auto* player = getActor(MWMechanics::getPlayer()); + const auto world = MWBase::Environment::get().getWorld(); + + // copy new ptr position in temporary vector. player is handled separately as its movement might change active + // cell. + mActorsPositions.clear(); + if (!mActors.empty()) + mActorsPositions.reserve(mActors.size() - 1); + for (const auto& [ptr, physicActor] : mActors) + { + if (physicActor.get() == player) + continue; + mActorsPositions.emplace_back(physicActor->getPtr(), physicActor->getSimulationPosition()); + } + + for (const auto& [ptr, pos] : mActorsPositions) + world->moveObject(ptr, pos, false, false); + + if (player != nullptr) + world->moveObject(player->getPtr(), player->getSimulationPosition(), false, false); } void PhysicsSystem::updateAnimatedCollisionShape(const MWWorld::Ptr& object) { - ObjectMap::iterator found = mObjects.find(object); + ObjectMap::iterator found = mObjects.find(object.mRef); if (found != mObjects.end()) if (found->second->animateCollisionShapes()) mTaskScheduler->updateSingleAabb(found->second); @@ -813,15 +771,15 @@ namespace MWPhysics mTaskScheduler->debugDraw(); } - bool PhysicsSystem::isActorStandingOn(const MWWorld::Ptr &actor, const MWWorld::ConstPtr &object) const + bool PhysicsSystem::isActorStandingOn(const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object) const { - const auto physActor = mActors.find(actor); + const auto physActor = mActors.find(actor.mRef); if (physActor != mActors.end()) return physActor->second->getStandingOnPtr() == object; return false; } - void PhysicsSystem::getActorsStandingOn(const MWWorld::ConstPtr &object, std::vector &out) const + void PhysicsSystem::getActorsStandingOn(const MWWorld::ConstPtr& object, std::vector& out) const { for (const auto& [_, actor] : mActors) { @@ -830,13 +788,15 @@ namespace MWPhysics } } - bool PhysicsSystem::isActorCollidingWith(const MWWorld::Ptr &actor, const MWWorld::ConstPtr &object) const + bool PhysicsSystem::isObjectCollidingWith(const MWWorld::ConstPtr& object, ScriptedCollisionType type) const { - std::vector collisions = getCollisions(object, CollisionType_World, CollisionType_Actor); - return (std::find(collisions.begin(), collisions.end(), actor) != collisions.end()); + auto found = mObjects.find(object.mRef); + if (found != mObjects.end()) + return found->second->collidedWith(type); + return false; } - void PhysicsSystem::getActorsCollidingWith(const MWWorld::ConstPtr &object, std::vector &out) const + void PhysicsSystem::getActorsCollidingWith(const MWWorld::ConstPtr& object, std::vector& out) const { std::vector collisions = getCollisions(object, CollisionType_World, CollisionType_Actor); out.insert(out.end(), collisions.begin(), collisions.end()); @@ -883,25 +843,43 @@ namespace MWPhysics return; } - mWaterCollisionObject.reset(new btCollisionObject()); - mWaterCollisionShape.reset(new btStaticPlaneShape(btVector3(0,0,1), mWaterHeight)); + mWaterCollisionObject = std::make_unique(); + mWaterCollisionShape = std::make_unique(btVector3(0, 0, 1), mWaterHeight); mWaterCollisionObject->setCollisionShape(mWaterCollisionShape.get()); - mTaskScheduler->addCollisionObject(mWaterCollisionObject.get(), CollisionType_Water, - CollisionType_Actor|CollisionType_Projectile); - } - - bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const - { - btCollisionObject* object = nullptr; - const auto it = mActors.find(ignore); - if (it != mActors.end()) - object = it->second->getCollisionObject(); + mTaskScheduler->addCollisionObject( + mWaterCollisionObject.get(), CollisionType_Water, CollisionType_Actor | CollisionType_Projectile); + } + + bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + std::span ignore, std::vector* occupyingActors) const + { + std::vector ignoredObjects; + ignoredObjects.reserve(ignore.size()); + for (const auto& v : ignore) + if (const auto it = mActors.find(v.mRef); it != mActors.end()) + ignoredObjects.push_back(it->second->getCollisionObject()); + std::sort(ignoredObjects.begin(), ignoredObjects.end()); + ignoredObjects.erase(std::unique(ignoredObjects.begin(), ignoredObjects.end()), ignoredObjects.end()); + const auto ignoreFilter = [&](const btCollisionObject* v) { + return std::binary_search(ignoredObjects.begin(), ignoredObjects.end(), v); + }; const auto bulletPosition = Misc::Convert::toBullet(position); const auto aabbMin = bulletPosition - btVector3(radius, radius, radius); const auto aabbMax = bulletPosition + btVector3(radius, radius, radius); const int mask = MWPhysics::CollisionType_Actor; - const int group = 0xff; - HasSphereCollisionCallback callback(bulletPosition, radius, object, mask, group); + const int group = MWPhysics::CollisionType_AnyPhysical; + if (occupyingActors == nullptr) + { + HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, + static_cast(nullptr)); + mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); + return callback.getResult(); + } + const auto onCollision = [&](const btCollisionObject* object) { + if (PtrHolder* holder = static_cast(object->getUserPointer())) + occupyingActors->push_back(holder->getPtr()); + }; + HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, &onCollision); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); return callback.getResult(); } @@ -910,6 +888,7 @@ namespace MWPhysics { stats.setAttribute(frameNumber, "Physics Actors", mActors.size()); stats.setAttribute(frameNumber, "Physics Objects", mObjects.size()); + stats.setAttribute(frameNumber, "Physics Projectiles", mProjectiles.size()); stats.setAttribute(frameNumber, "Physics HeightFields", mHeightFields.size()); } @@ -919,58 +898,73 @@ namespace MWPhysics mDebugDrawer->addCollision(position, normal); } - ActorFrameData::ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr standingOn, - bool waterCollision, float slowFall, float waterlevel) - : mActor(actor), mActorRaw(actor.get()), mStandingOn(standingOn), - mDidJump(false), mNeedLand(false), mWaterCollision(waterCollision), mSkipCollisionDetection(actor->skipCollisions()), - mWaterlevel(waterlevel), mSlowFall(slowFall), mOldHeight(0), mFallHeight(0), mMovement(actor->velocity()), mPosition(), mRefpos() + ActorFrameData::ActorFrameData( + Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel, bool isPlayer) + : mPosition() + , mStandingOn(nullptr) + , mIsOnGround(actor.getOnGround()) + , mIsOnSlope(actor.getOnSlope()) + , mWalkingOnWater(false) + , mInert(inert) + , mCollisionObject(actor.getCollisionObject()) + , mSwimLevel(waterlevel + - (actor.getRenderingHalfExtents().z() * 2 + * MWBase::Environment::get() + .getESMStore() + ->get() + .find("fSwimHeightScale") + ->mValue.getFloat())) + , mSlowFall(slowFall) + , mRotation() + , mMovement(actor.velocity()) + , mWaterlevel(waterlevel) + , mHalfExtentsZ(actor.getHalfExtents().z()) + , mOldHeight(0) + , mStuckFrames(0) + , mFlying(MWBase::Environment::get().getWorld()->isFlying(actor.getPtr())) + , mWasOnGround(actor.getOnGround()) + , mIsAquatic(actor.getPtr().getClass().isPureWaterCreature(actor.getPtr())) + , mWaterCollision(waterCollision) + , mSkipCollisionDetection(!actor.getCollisionMode()) + , mIsPlayer(isPlayer) + { + } + + ProjectileFrameData::ProjectileFrameData(Projectile& projectile) + : mPosition(projectile.getPosition()) + , mMovement(projectile.velocity()) + , mCaster(projectile.getCasterCollisionObject()) + , mCollisionObject(projectile.getCollisionObject()) + , mProjectile(&projectile) { - const MWBase::World *world = MWBase::Environment::get().getWorld(); - const auto ptr = actor->getPtr(); - mFlying = world->isFlying(ptr); - mSwimming = world->isSwimming(ptr); - mWantJump = ptr.getClass().getMovementSettings(ptr).mPosition[2] != 0; - auto& stats = ptr.getClass().getCreatureStats(ptr); - const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState(); - mFloatToSurface = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0); - mWasOnGround = actor->getOnGround(); - } - - void ActorFrameData::updatePosition(btCollisionWorld* world) - { - mActorRaw->applyOffsetChange(); - mPosition = mActorRaw->getPosition(); - if (mWaterCollision && mPosition.z() < mWaterlevel && canMoveToWaterSurface(mActorRaw, mWaterlevel, world)) - { - mPosition.z() = mWaterlevel; - MWBase::Environment::get().getWorld()->moveObject(mActorRaw->getPtr(), mPosition.x(), mPosition.y(), mPosition.z(), false); - } - mOldHeight = mPosition.z(); - mRefpos = mActorRaw->getPtr().getRefData().getPosition(); } WorldFrameData::WorldFrameData() : mIsInStorm(MWBase::Environment::get().getWorld()->isInStorm()) , mStormDirection(MWBase::Environment::get().getWorld()->getStormDirection()) - {} + { + } LOSRequest::LOSRequest(const std::weak_ptr& a1, const std::weak_ptr& a2) - : mResult(false), mStale(false), mAge(0) + : mResult(false) + , mStale(false) + , mAge(0) { // we use raw actor pointer pair to uniquely identify request - // sort the pointer value in ascending order to not duplicate equivalent requests, eg. getLOS(A, B) and getLOS(B, A) + // sort the pointer value in ascending order to not duplicate equivalent requests, eg. getLOS(A, B) and + // getLOS(B, A) auto* raw1 = a1.lock().get(); auto* raw2 = a2.lock().get(); assert(raw1 != raw2); if (raw1 < raw2) { - mActors = {a1, a2}; - mRawActors = {raw1, raw2}; + mActors = { a1, a2 }; + mRawActors = { raw1, raw2 }; } else { - mActors = {a2, a1}; - mRawActors = {raw2, raw1}; + mActors = { a2, a1 }; + mRawActors = { raw2, raw1 }; } } diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 3d2a3c580d5..d7f1933a790 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -1,16 +1,20 @@ #ifndef OPENMW_MWPHYSICS_PHYSICSSYSTEM_H #define OPENMW_MWPHYSICS_PHYSICSSYSTEM_H +#include #include -#include +#include #include -#include -#include +#include +#include +#include +#include +#include -#include #include -#include +#include #include +#include #include "../mwworld/ptr.hpp" @@ -35,11 +39,6 @@ namespace Resource class ResourceSystem; } -namespace SceneUtil -{ - class UnrefQueue; -} - class btCollisionWorld; class btBroadphaseInterface; class btDefaultCollisionConfiguration; @@ -55,8 +54,9 @@ namespace MWPhysics class Actor; class PhysicsTaskScheduler; class Projectile; + enum ScriptedCollisionType : char; - using ActorMap = std::map>; + using ActorMap = std::unordered_map>; struct ContactPoint { @@ -78,27 +78,40 @@ namespace MWPhysics struct ActorFrameData { - ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr standingOn, bool moveToWaterSurface, float slowFall, float waterlevel); - void updatePosition(btCollisionWorld* world); - std::weak_ptr mActor; - Actor* mActorRaw; - MWWorld::Ptr mStandingOn; - bool mFlying; - bool mSwimming; - bool mWasOnGround; - bool mWantJump; - bool mDidJump; - bool mFloatToSurface; - bool mNeedLand; - bool mWaterCollision; - bool mSkipCollisionDetection; - float mWaterlevel; - float mSlowFall; - float mOldHeight; - float mFallHeight; + ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel, bool isPlayer); + osg::Vec3f mPosition; + osg::Vec3f mInertia; + const btCollisionObject* mStandingOn; + bool mIsOnGround; + bool mIsOnSlope; + bool mWalkingOnWater; + const bool mInert; + btCollisionObject* mCollisionObject; + const float mSwimLevel; + const float mSlowFall; + osg::Vec2f mRotation; osg::Vec3f mMovement; + osg::Vec3f mLastStuckPosition; + const float mWaterlevel; + const float mHalfExtentsZ; + float mOldHeight; + unsigned int mStuckFrames; + const bool mFlying; + const bool mWasOnGround; + const bool mIsAquatic; + const bool mWaterCollision; + const bool mSkipCollisionDetection; + const bool mIsPlayer; + }; + + struct ProjectileFrameData + { + explicit ProjectileFrameData(Projectile& projectile); osg::Vec3f mPosition; - ESM::Position mRefpos; + osg::Vec3f mMovement; + const btCollisionObject* mCaster; + const btCollisionObject* mCollisionObject; + Projectile* mProjectile; }; struct WorldFrameData @@ -108,197 +121,221 @@ namespace MWPhysics osg::Vec3f mStormDirection; }; - class PhysicsSystem : public RayCastingInterface + template + class SimulationImpl { - public: - PhysicsSystem (Resource::ResourceSystem* resourceSystem, osg::ref_ptr parentNode); - virtual ~PhysicsSystem (); - - void setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue); - - Resource::BulletShapeManager* getShapeManager(); - - void enableWater(float height); - void setWaterHeight(float height); - void disableWater(); + public: + explicit SimulationImpl(const std::weak_ptr& ptr, FrameData&& data) + : mPtr(ptr) + , mData(data) + { + } + + std::optional, std::reference_wrapper>> lock() + { + if (auto locked = mPtr.lock()) + return { { std::move(locked), std::ref(mData) } }; + return std::nullopt; + } + + private: + std::weak_ptr mPtr; + FrameData mData; + }; - void addObject (const MWWorld::Ptr& ptr, const std::string& mesh, int collisionType = CollisionType_World); - void addActor (const MWWorld::Ptr& ptr, const std::string& mesh); + using ActorSimulation = SimulationImpl; + using ProjectileSimulation = SimulationImpl; + using Simulation = std::variant; - int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius, bool canTraverseWater); - void setCaster(int projectileId, const MWWorld::Ptr& caster); - void updateProjectile(const int projectileId, const osg::Vec3f &position) const; - void removeProjectile(const int projectileId); + class PhysicsSystem : public RayCastingInterface + { + public: + PhysicsSystem(Resource::ResourceSystem* resourceSystem, osg::ref_ptr parentNode); + virtual ~PhysicsSystem(); - void updatePtr (const MWWorld::Ptr& old, const MWWorld::Ptr& updated); + Resource::BulletShapeManager* getShapeManager(); - Actor* getActor(const MWWorld::Ptr& ptr); - const Actor* getActor(const MWWorld::ConstPtr& ptr) const; + void enableWater(float height); + void setWaterHeight(float height); + void disableWater(); - const Object* getObject(const MWWorld::ConstPtr& ptr) const; + void addObject(const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, + int collisionType = CollisionType_World); + void addActor(const MWWorld::Ptr& ptr, const std::string& mesh); - Projectile* getProjectile(int projectileId) const; + int addProjectile( + const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius); + void setCaster(int projectileId, const MWWorld::Ptr& caster); + void removeProjectile(const int projectileId); - // Object or Actor - void remove (const MWWorld::Ptr& ptr); + void updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& updated); - void updateScale (const MWWorld::Ptr& ptr); - void updateRotation (const MWWorld::Ptr& ptr); - void updatePosition (const MWWorld::Ptr& ptr); + Actor* getActor(const MWWorld::Ptr& ptr); + const Actor* getActor(const MWWorld::ConstPtr& ptr) const; - void addHeightField (const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject); + const Object* getObject(const MWWorld::ConstPtr& ptr) const; - void removeHeightField (int x, int y); + Projectile* getProjectile(int projectileId) const; - const HeightField* getHeightField(int x, int y) const; + // Object or Actor + void remove(const MWWorld::Ptr& ptr); - bool toggleCollisionMode(); + void updateScale(const MWWorld::Ptr& ptr); + void updateRotation(const MWWorld::Ptr& ptr, osg::Quat rotate); + void updatePosition(const MWWorld::Ptr& ptr); - void stepSimulation(); - void debugDraw(); + void addHeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, + const osg::Object* holdObject); - std::vector getCollisions(const MWWorld::ConstPtr &ptr, int collisionGroup, int collisionMask) const; ///< get handles this object collides with - std::vector getCollisionsPoints(const MWWorld::ConstPtr &ptr, int collisionGroup, int collisionMask) const; - osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, float maxHeight); + void removeHeightField(int x, int y); - std::pair getHitContact(const MWWorld::ConstPtr& actor, - const osg::Vec3f &origin, - const osg::Quat &orientation, - float queryDistance, std::vector& targets); + const HeightField* getHeightField(int x, int y) const; + bool toggleCollisionMode(); - /// Get distance from \a point to the collision shape of \a target. Uses a raycast to find where the - /// target vector hits the collision shape and then calculates distance from the intersection point. - /// This can be used to find out how much nearer we need to move to the target for a "getHitContact" to be successful. - /// \note Only Actor targets are supported at the moment. - float getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const override; + /// Determine new position based on all queued movements, then clear the list. + void stepSimulation( + float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); - /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all other actors. - RayCastingResult castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), - std::vector targets = std::vector(), - int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const override; + /// Apply new positions to actors + void moveActors(); + void debugDraw(); - RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const override; + std::vector getCollisions(const MWWorld::ConstPtr& ptr, int collisionGroup, + int collisionMask) const; ///< get handles this object collides with + std::vector getCollisionsPoints( + const MWWorld::ConstPtr& ptr, int collisionGroup, int collisionMask) const; + osg::Vec3f traceDown(const MWWorld::Ptr& ptr, const osg::Vec3f& position, float maxHeight); - /// Return true if actor1 can see actor2. - bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const override; + /// @param ignore Optional, a list of Ptr to ignore in the list of results. targets are actors to filter for, + /// ignoring all other actors. + RayCastingResult castRay(const osg::Vec3f& from, const osg::Vec3f& to, + const std::vector& ignore = {}, const std::vector& targets = {}, + int mask = CollisionType_Default, int group = 0xff) const override; + using RayCastingInterface::castRay; - bool isOnGround (const MWWorld::Ptr& actor); + RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius, + int mask = CollisionType_Default, int group = 0xff) const override; - bool canMoveToWaterSurface (const MWWorld::ConstPtr &actor, const float waterlevel); + /// Return true if actor1 can see actor2. + bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const override; - /// Get physical half extents (scaled) of the given actor. - osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& actor) const; + bool isOnGround(const MWWorld::Ptr& actor); - /// Get physical half extents (not scaled) of the given actor. - osg::Vec3f getOriginalHalfExtents(const MWWorld::ConstPtr& actor) const; + bool canMoveToWaterSurface(const MWWorld::ConstPtr& actor, const float waterlevel); - /// @see MWPhysics::Actor::getRenderingHalfExtents - osg::Vec3f getRenderingHalfExtents(const MWWorld::ConstPtr& actor) const; + /// Get physical half extents (scaled) of the given actor. + osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& actor) const; - /// Get the position of the collision shape for the actor. Use together with getHalfExtents() to get the collision bounds in world space. - /// @note The collision shape's origin is in its center, so the position returned can be described as center of the actor collision box in world space. - osg::Vec3f getCollisionObjectPosition(const MWWorld::ConstPtr& actor) const; + /// Get physical half extents (not scaled) of the given actor. + osg::Vec3f getOriginalHalfExtents(const MWWorld::ConstPtr& actor) const; - /// Get bounding box in world space of the given object. - osg::BoundingBox getBoundingBox(const MWWorld::ConstPtr &object) const; + /// @see MWPhysics::Actor::getRenderingHalfExtents + osg::Vec3f getRenderingHalfExtents(const MWWorld::ConstPtr& actor) const; - /// Queues velocity movement for a Ptr. If a Ptr is already queued, its velocity will - /// be overwritten. Valid until the next call to applyQueuedMovement. - void queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity); + /// Get the position of the collision shape for the actor. Use together with getHalfExtents() to get the + /// collision bounds in world space. + /// @note The collision shape's origin is in its center, so the position returned can be described as center of + /// the actor collision box in world space. + osg::Vec3f getCollisionObjectPosition(const MWWorld::ConstPtr& actor) const; - /// Apply all queued movements, then clear the list. - const std::vector& applyQueuedMovement(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + /// Get bounding box in world space of the given object. + osg::BoundingBox getBoundingBox(const MWWorld::ConstPtr& object) const; - /// Clear the queued movements list without applying. - void clearQueuedMovement(); + /// Queues velocity movement for a Ptr. If a Ptr is already queued, its velocity will + /// be overwritten. Valid until the next call to stepSimulation + void queueObjectMovement(const MWWorld::Ptr& ptr, const osg::Vec3f& velocity); - /// Return true if \a actor has been standing on \a object in this frame - /// This will trigger whenever the object is directly below the actor. - /// It doesn't matter if the actor is stationary or moving. - bool isActorStandingOn(const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object) const; + /// Clear the queued movements list without applying. + void clearQueuedMovement(); - /// Get the handle of all actors standing on \a object in this frame. - void getActorsStandingOn(const MWWorld::ConstPtr& object, std::vector& out) const; + /// Return true if \a actor has been standing on \a object in this frame + /// This will trigger whenever the object is directly below the actor. + /// It doesn't matter if the actor is stationary or moving. + bool isActorStandingOn(const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object) const; - /// Return true if \a actor has collided with \a object in this frame. - /// This will detect running into objects, but will not detect climbing stairs, stepping up a small object, etc. - bool isActorCollidingWith(const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object) const; + /// Get the handle of all actors standing on \a object in this frame. + void getActorsStandingOn(const MWWorld::ConstPtr& object, std::vector& out) const; - /// Get the handle of all actors colliding with \a object in this frame. - void getActorsCollidingWith(const MWWorld::ConstPtr& object, std::vector& out) const; + /// Return true if an object of the given type has collided with this object + bool isObjectCollidingWith(const MWWorld::ConstPtr& object, ScriptedCollisionType type) const; - bool toggleDebugRendering(); + /// Get the handle of all actors colliding with \a object in this frame. + void getActorsCollidingWith(const MWWorld::ConstPtr& object, std::vector& out) const; - /// Mark the given object as a 'non-solid' object. A non-solid object means that - /// \a isOnSolidGround will return false for actors standing on that object. - void markAsNonSolid (const MWWorld::ConstPtr& ptr); + bool toggleDebugRendering(); - bool isOnSolidGround (const MWWorld::Ptr& actor) const; + /// Mark the given object as a 'non-solid' object. A non-solid object means that + /// \a isOnSolidGround will return false for actors standing on that object. + void markAsNonSolid(const MWWorld::ConstPtr& ptr); - void updateAnimatedCollisionShape(const MWWorld::Ptr& object); + bool isOnSolidGround(const MWWorld::Ptr& actor) const; - template - void forEachAnimatedObject(Function&& function) const - { - std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); - } + void updateAnimatedCollisionShape(const MWWorld::Ptr& object); - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const; + template + void forEachAnimatedObject(Function&& function) const + { + std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); + } - void reportStats(unsigned int frameNumber, osg::Stats& stats) const; - void reportCollision(const btVector3& position, const btVector3& normal); + bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + std::span ignore, std::vector* occupyingActors) const; - private: + void reportStats(unsigned int frameNumber, osg::Stats& stats) const; + void reportCollision(const btVector3& position, const btVector3& normal); - void updateWater(); + private: + void updateWater(); - std::vector prepareFrameData(bool willSimulate); + void prepareSimulation(bool willSimulate, std::vector& simulations); - osg::ref_ptr mUnrefQueue; + std::unique_ptr mBroadphase; + std::unique_ptr mCollisionConfiguration; + std::unique_ptr mDispatcher; + std::unique_ptr mCollisionWorld; + std::unique_ptr mTaskScheduler; - std::unique_ptr mBroadphase; - std::unique_ptr mCollisionConfiguration; - std::unique_ptr mDispatcher; - std::unique_ptr mCollisionWorld; - std::unique_ptr mTaskScheduler; + std::unique_ptr mShapeManager; + Resource::ResourceSystem* mResourceSystem; - std::unique_ptr mShapeManager; - Resource::ResourceSystem* mResourceSystem; + using ObjectMap = std::unordered_map>; + ObjectMap mObjects; - using ObjectMap = std::map>; - ObjectMap mObjects; + std::map mAnimatedObjects; // stores pointers to elements in mObjects - std::set mAnimatedObjects; // stores pointers to elements in mObjects + ActorMap mActors; - ActorMap mActors; + using ProjectileMap = std::map>; + ProjectileMap mProjectiles; - using ProjectileMap = std::map>; - ProjectileMap mProjectiles; + using HeightFieldMap = std::map, std::unique_ptr>; + HeightFieldMap mHeightFields; - using HeightFieldMap = std::map, std::unique_ptr>; - HeightFieldMap mHeightFields; + bool mDebugDrawEnabled; - bool mDebugDrawEnabled; + float mTimeAccum; - float mTimeAccum; + unsigned int mProjectileId; - unsigned int mProjectileId; + float mWaterHeight; + bool mWaterEnabled; - float mWaterHeight; - bool mWaterEnabled; + std::unique_ptr mWaterCollisionObject; + std::unique_ptr mWaterCollisionShape; - std::unique_ptr mWaterCollisionObject; - std::unique_ptr mWaterCollisionShape; + std::unique_ptr mDebugDrawer; - std::unique_ptr mDebugDrawer; + osg::ref_ptr mParentNode; - osg::ref_ptr mParentNode; + float mPhysicsDt; - float mPhysicsDt; + std::size_t mSimulationsCounter = 0; + std::array, 2> mSimulations; + std::vector> mActorsPositions; - PhysicsSystem (const PhysicsSystem&); - PhysicsSystem& operator= (const PhysicsSystem&); + PhysicsSystem(const PhysicsSystem&); + PhysicsSystem& operator=(const PhysicsSystem&); }; } diff --git a/apps/openmw/mwphysics/projectile.cpp b/apps/openmw/mwphysics/projectile.cpp index 252da0a687b..9c5c82e6c7d 100644 --- a/apps/openmw/mwphysics/projectile.cpp +++ b/apps/openmw/mwphysics/projectile.cpp @@ -1,144 +1,122 @@ #include #include -#include - -#include #include -#include "../mwworld/class.hpp" - +#include "actor.hpp" #include "collisiontype.hpp" -#include "memory" #include "mtphysics.hpp" +#include "object.hpp" #include "projectile.hpp" namespace MWPhysics { -Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, bool canCrossWaterSurface, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem) - : mCanCrossWaterSurface(canCrossWaterSurface) - , mCrossedWaterSurface(false) - , mActive(true) - , mCaster(caster) - , mWaterHitPosition(std::nullopt) - , mPhysics(physicssystem) - , mTaskScheduler(scheduler) -{ - mShape = std::make_unique(radius); - mConvexShape = static_cast(mShape.get()); - - mCollisionObject = std::make_unique(); - mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); - mCollisionObject->setActivationState(DISABLE_DEACTIVATION); - mCollisionObject->setCollisionShape(mShape.get()); - mCollisionObject->setUserPointer(this); + Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, + PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem) + : PtrHolder(MWWorld::Ptr(), position) + , mHitWater(false) + , mActive(true) + , mHitTarget(nullptr) + , mPhysics(physicssystem) + , mTaskScheduler(scheduler) + { + mShape = std::make_unique(radius); + mConvexShape = static_cast(mShape.get()); - setPosition(position); + mCollisionObject = std::make_unique(); + mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); + mCollisionObject->setActivationState(DISABLE_DEACTIVATION); + mCollisionObject->setCollisionShape(mShape.get()); + mCollisionObject->setUserPointer(this); - const int collisionMask = CollisionType_World | CollisionType_HeightMap | - CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile; - mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask); + mPosition = position; + mPreviousPosition = position; + mSimulationPosition = position; + setCaster(caster); - commitPositionChange(); -} + const int collisionMask = CollisionType_World | CollisionType_HeightMap | CollisionType_Actor + | CollisionType_Door | CollisionType_Water | CollisionType_Projectile; + mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask); -Projectile::~Projectile() -{ - if (!mActive) - mPhysics->reportCollision(mHitPosition, mHitNormal); - mTaskScheduler->removeCollisionObject(mCollisionObject.get()); -} + updateCollisionObjectPosition(); + } -void Projectile::commitPositionChange() -{ - std::scoped_lock lock(mMutex); - if (mTransformUpdatePending) + Projectile::~Projectile() { - mCollisionObject->setWorldTransform(mLocalTransform); - mTransformUpdatePending = false; + if (!mActive) + mPhysics->reportCollision(mHitPosition, mHitNormal); + mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } -} - -void Projectile::setPosition(const osg::Vec3f &position) -{ - std::scoped_lock lock(mMutex); - mLocalTransform.setOrigin(Misc::Convert::toBullet(position)); - mTransformUpdatePending = true; -} - -osg::Vec3f Projectile::getPosition() const -{ - std::scoped_lock lock(mMutex); - return Misc::Convert::toOsg(mLocalTransform.getOrigin()); -} - -bool Projectile::canTraverseWater() const -{ - return mCanCrossWaterSurface; -} - -void Projectile::hit(MWWorld::Ptr target, btVector3 pos, btVector3 normal) -{ - if (!mActive.load(std::memory_order_acquire)) - return; - std::scoped_lock lock(mMutex); - mHitTarget = target; - mHitPosition = pos; - mHitNormal = normal; - mActive.store(false, std::memory_order_release); -} -MWWorld::Ptr Projectile::getCaster() const -{ - std::scoped_lock lock(mMutex); - return mCaster; -} + void Projectile::updateCollisionObjectPosition() + { + std::scoped_lock lock(mMutex); + auto& trans = mCollisionObject->getWorldTransform(); + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + mCollisionObject->setWorldTransform(trans); + } -void Projectile::setCaster(MWWorld::Ptr caster) -{ - std::scoped_lock lock(mMutex); - mCaster = caster; -} + void Projectile::hit(const btCollisionObject* target, btVector3 pos, btVector3 normal) + { + bool active = true; + if (!mActive.compare_exchange_strong(active, false, std::memory_order_relaxed) || !active) + return; + mHitTarget = target; + mHitPosition = pos; + mHitNormal = normal; + } -void Projectile::setValidTargets(const std::vector& targets) -{ - std::scoped_lock lock(mMutex); - mValidTargets = targets; -} + MWWorld::Ptr Projectile::getTarget() const + { + assert(!mActive); + auto* target = static_cast(mHitTarget->getUserPointer()); + return target ? target->getPtr() : MWWorld::Ptr(); + } -bool Projectile::isValidTarget(const MWWorld::Ptr& target) const -{ - std::scoped_lock lock(mMutex); - if (mCaster == target) - return false; + MWWorld::Ptr Projectile::getCaster() const + { + return mCaster; + } - if (target.isEmpty() || mValidTargets.empty()) - return true; + void Projectile::setCaster(const MWWorld::Ptr& caster) + { + mCaster = caster; + mCasterColObj = [this, &caster]() -> const btCollisionObject* { + const Actor* actor = mPhysics->getActor(caster); + if (actor) + return actor->getCollisionObject(); + const Object* object = mPhysics->getObject(caster); + if (object) + return object->getCollisionObject(); + return nullptr; + }(); + } - bool validTarget = false; - for (const auto& targetActor : mValidTargets) + void Projectile::setValidTargets(const std::vector& targets) { - if (targetActor == target) + std::scoped_lock lock(mMutex); + mValidTargets.clear(); + for (const auto& ptr : targets) { - validTarget = true; - break; + const auto* physicActor = mPhysics->getActor(ptr); + if (physicActor) + mValidTargets.push_back(physicActor->getCollisionObject()); } } - return validTarget; -} -std::optional Projectile::getWaterHitPosition() -{ - return std::exchange(mWaterHitPosition, std::nullopt); -} + bool Projectile::isValidTarget(const btCollisionObject* target) const + { + assert(target); + std::scoped_lock lock(mMutex); + if (mCasterColObj == target) + return false; -void Projectile::setWaterHitPosition(btVector3 pos) -{ - if (mCrossedWaterSurface) - return; - mCrossedWaterSurface = true; - mWaterHitPosition = pos; -} + if (mValidTargets.empty()) + return true; + + return std::any_of(mValidTargets.begin(), mValidTargets.end(), + [target](const btCollisionObject* actor) { return target == actor; }); + } } diff --git a/apps/openmw/mwphysics/projectile.hpp b/apps/openmw/mwphysics/projectile.hpp index 81c33d2a5ef..19337c7de16 100644 --- a/apps/openmw/mwphysics/projectile.hpp +++ b/apps/openmw/mwphysics/projectile.hpp @@ -4,25 +4,20 @@ #include #include #include -#include + +#include #include "ptrholder.hpp" class btCollisionObject; class btCollisionShape; class btConvexShape; -class btVector3; namespace osg { class Vec3f; } -namespace Resource -{ - class BulletShape; -} - namespace MWPhysics { class PhysicsTaskScheduler; @@ -31,68 +26,51 @@ namespace MWPhysics class Projectile final : public PtrHolder { public: - Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, bool canCrossWaterSurface, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem); + Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, + PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem); ~Projectile() override; btConvexShape* getConvexShape() const { return mConvexShape; } - void commitPositionChange(); - - void setPosition(const osg::Vec3f& position); - osg::Vec3f getPosition() const; + void updateCollisionObjectPosition(); - btCollisionObject* getCollisionObject() const - { - return mCollisionObject.get(); - } + bool isActive() const { return mActive.load(std::memory_order_acquire); } - bool isActive() const - { - return mActive.load(std::memory_order_acquire); - } - - MWWorld::Ptr getTarget() const - { - assert(!mActive); - return mHitTarget; - } + MWWorld::Ptr getTarget() const; MWWorld::Ptr getCaster() const; - void setCaster(MWWorld::Ptr caster); + void setCaster(const MWWorld::Ptr& caster); + const btCollisionObject* getCasterCollisionObject() const { return mCasterColObj; } + + void setHitWater() { mHitWater = true; } - bool canTraverseWater() const; + bool getHitWater() const { return mHitWater; } - void hit(MWWorld::Ptr target, btVector3 pos, btVector3 normal); + void hit(const btCollisionObject* target, btVector3 pos, btVector3 normal); void setValidTargets(const std::vector& targets); - bool isValidTarget(const MWWorld::Ptr& target) const; + bool isValidTarget(const btCollisionObject* target) const; - std::optional getWaterHitPosition(); - void setWaterHitPosition(btVector3 pos); + btVector3 getHitPosition() const { return mHitPosition; } private: - std::unique_ptr mShape; btConvexShape* mConvexShape; - std::unique_ptr mCollisionObject; - btTransform mLocalTransform; - bool mTransformUpdatePending; - bool mCanCrossWaterSurface; - bool mCrossedWaterSurface; + bool mHitWater; std::atomic mActive; MWWorld::Ptr mCaster; - MWWorld::Ptr mHitTarget; - std::optional mWaterHitPosition; + const btCollisionObject* mCasterColObj; + const btCollisionObject* mHitTarget; btVector3 mHitPosition; btVector3 mHitNormal; - std::vector mValidTargets; + std::vector mValidTargets; mutable std::mutex mMutex; - PhysicsSystem *mPhysics; - PhysicsTaskScheduler *mTaskScheduler; + PhysicsSystem* mPhysics; + PhysicsTaskScheduler* mTaskScheduler; Projectile(const Projectile&); Projectile& operator=(const Projectile&); @@ -100,5 +78,4 @@ namespace MWPhysics } - #endif diff --git a/apps/openmw/mwphysics/projectileconvexcallback.cpp b/apps/openmw/mwphysics/projectileconvexcallback.cpp index b803c4400b2..913a3edb0c4 100644 --- a/apps/openmw/mwphysics/projectileconvexcallback.cpp +++ b/apps/openmw/mwphysics/projectileconvexcallback.cpp @@ -1,69 +1,49 @@ -#include "../mwworld/class.hpp" +#include -#include "actor.hpp" #include "collisiontype.hpp" #include "projectile.hpp" #include "projectileconvexcallback.hpp" -#include "ptrholder.hpp" namespace MWPhysics { - ProjectileConvexCallback::ProjectileConvexCallback(const btCollisionObject* me, const btVector3& from, const btVector3& to, Projectile* proj) - : btCollisionWorld::ClosestConvexResultCallback(from, to) - , mMe(me), mProjectile(proj) - { - assert(mProjectile); - } - - btScalar ProjectileConvexCallback::addSingleResult(btCollisionWorld::LocalConvexResult& result, bool normalInWorldSpace) + btScalar ProjectileConvexCallback::addSingleResult( + btCollisionWorld::LocalConvexResult& result, bool normalInWorldSpace) { + const auto* hitObject = result.m_hitCollisionObject; // don't hit the caster - if (result.m_hitCollisionObject == mMe) + if (hitObject == mCaster) return 1.f; // don't hit the projectile - if (result.m_hitCollisionObject == mProjectile->getCollisionObject()) + if (hitObject == mMe) return 1.f; btCollisionWorld::ClosestConvexResultCallback::addSingleResult(result, normalInWorldSpace); - switch (result.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup) + switch (hitObject->getBroadphaseHandle()->m_collisionFilterGroup) { case CollisionType_Actor: - { - auto* target = static_cast(result.m_hitCollisionObject->getUserPointer()); - if (!mProjectile->isValidTarget(target->getPtr())) - return 1.f; - mProjectile->hit(target->getPtr(), result.m_hitPointLocal, result.m_hitNormalLocal); - break; - } + { + if (!mProjectile.isValidTarget(hitObject)) + return 1.f; + break; + } case CollisionType_Projectile: - { - auto* target = static_cast(result.m_hitCollisionObject->getUserPointer()); - if (!mProjectile->isValidTarget(target->getCaster())) - return 1.f; - target->hit(mProjectile->getPtr(), m_hitPointWorld, m_hitNormalWorld); - mProjectile->hit(target->getPtr(), m_hitPointWorld, m_hitNormalWorld); - break; - } + { + auto* target = static_cast(hitObject->getUserPointer()); + if (!mProjectile.isValidTarget(target->getCasterCollisionObject())) + return 1.f; + target->hit(mMe, m_hitPointWorld, m_hitNormalWorld); + break; + } case CollisionType_Water: - { - mProjectile->setWaterHitPosition(m_hitPointWorld); - if (mProjectile->canTraverseWater()) - return 1.f; - mProjectile->hit(MWWorld::Ptr(), m_hitPointWorld, m_hitNormalWorld); - break; - } - default: - { - auto* target = static_cast(result.m_hitCollisionObject->getUserPointer()); - auto ptr = target ? target->getPtr() : MWWorld::Ptr(); - mProjectile->hit(ptr, m_hitPointWorld, m_hitNormalWorld); - break; - } + { + mProjectile.setHitWater(); + break; + } } + mProjectile.hit(hitObject, m_hitPointWorld, m_hitNormalWorld); return result.m_hitFraction; } } - diff --git a/apps/openmw/mwphysics/projectileconvexcallback.hpp b/apps/openmw/mwphysics/projectileconvexcallback.hpp index 96c84b1fe27..d75ace22af6 100644 --- a/apps/openmw/mwphysics/projectileconvexcallback.hpp +++ b/apps/openmw/mwphysics/projectileconvexcallback.hpp @@ -12,13 +12,21 @@ namespace MWPhysics class ProjectileConvexCallback : public btCollisionWorld::ClosestConvexResultCallback { public: - ProjectileConvexCallback(const btCollisionObject* me, const btVector3& from, const btVector3& to, Projectile* proj); + explicit ProjectileConvexCallback(const btCollisionObject* caster, const btCollisionObject* me, + const btVector3& from, const btVector3& to, Projectile& projectile) + : btCollisionWorld::ClosestConvexResultCallback(from, to) + , mCaster(caster) + , mMe(me) + , mProjectile(projectile) + { + } btScalar addSingleResult(btCollisionWorld::LocalConvexResult& result, bool normalInWorldSpace) override; private: + const btCollisionObject* mCaster; const btCollisionObject* mMe; - Projectile* mProjectile; + Projectile& mProjectile; }; } diff --git a/apps/openmw/mwphysics/ptrholder.hpp b/apps/openmw/mwphysics/ptrholder.hpp index 152a5d64fc5..fc8fd94c30b 100644 --- a/apps/openmw/mwphysics/ptrholder.hpp +++ b/apps/openmw/mwphysics/ptrholder.hpp @@ -1,7 +1,13 @@ #ifndef OPENMW_MWPHYSICS_PTRHOLDER_H #define OPENMW_MWPHYSICS_PTRHOLDER_H +#include #include +#include + +#include + +#include #include "../mwworld/ptr.hpp" @@ -10,31 +16,47 @@ namespace MWPhysics class PtrHolder { public: - virtual ~PtrHolder() {} - - void updatePtr(const MWWorld::Ptr& updated) + explicit PtrHolder(const MWWorld::Ptr& ptr, const osg::Vec3f& position) + : mPtr(ptr) + , mSimulationPosition(position) + , mPosition(position) + , mPreviousPosition(position) { - std::scoped_lock lock(mMutex); - mPtr = updated; } - MWWorld::Ptr getPtr() - { - std::scoped_lock lock(mMutex); - return mPtr; - } + virtual ~PtrHolder() = default; + + void updatePtr(const MWWorld::Ptr& updated) { mPtr = updated; } + + MWWorld::Ptr getPtr() const { return mPtr; } + + btCollisionObject* getCollisionObject() const { return mCollisionObject.get(); } - MWWorld::ConstPtr getPtr() const + void setVelocity(osg::Vec3f velocity) { mVelocity = velocity; } + + osg::Vec3f velocity() { return std::exchange(mVelocity, osg::Vec3f()); } + + void setSimulationPosition(const osg::Vec3f& position) { mSimulationPosition = position; } + + osg::Vec3f getSimulationPosition() const { return mSimulationPosition; } + + void setPosition(const osg::Vec3f& position) { - std::scoped_lock lock(mMutex); - return mPtr; + mPreviousPosition = mPosition; + mPosition = position; } + osg::Vec3d getPosition() const { return mPosition; } + + osg::Vec3d getPreviousPosition() const { return mPreviousPosition; } + protected: MWWorld::Ptr mPtr; - - private: - mutable std::mutex mMutex; + std::unique_ptr mCollisionObject; + osg::Vec3f mVelocity; + osg::Vec3f mSimulationPosition; + osg::Vec3d mPosition; + osg::Vec3d mPreviousPosition; }; } diff --git a/apps/openmw/mwphysics/raycasting.hpp b/apps/openmw/mwphysics/raycasting.hpp index 7c8375cb5a6..78b6ab4678b 100644 --- a/apps/openmw/mwphysics/raycasting.hpp +++ b/apps/openmw/mwphysics/raycasting.hpp @@ -9,8 +9,9 @@ namespace MWPhysics { - struct RayCastingResult + class RayCastingResult { + public: bool mHit; osg::Vec3f mHitPos; osg::Vec3f mHitNormal; @@ -19,22 +20,25 @@ namespace MWPhysics class RayCastingInterface { - public: - /// Get distance from \a point to the collision shape of \a target. Uses a raycast to find where the - /// target vector hits the collision shape and then calculates distance from the intersection point. - /// This can be used to find out how much nearer we need to move to the target for a "getHitContact" to be successful. - /// \note Only Actor targets are supported at the moment. - virtual float getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const = 0; - - /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all other actors. - virtual RayCastingResult castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), - std::vector targets = std::vector(), - int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const = 0; - - virtual RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const = 0; - - /// Return true if actor1 can see actor2. - virtual bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const = 0; + public: + virtual ~RayCastingInterface() = default; + + /// @param ignore Optional, a list of Ptr to ignore in the list of results. targets are actors to filter for, + /// ignoring all other actors. + virtual RayCastingResult castRay(const osg::Vec3f& from, const osg::Vec3f& to, + const std::vector& ignore = {}, const std::vector& targets = {}, + int mask = CollisionType_Default, int group = 0xff) const = 0; + + RayCastingResult castRay(const osg::Vec3f& from, const osg::Vec3f& to, int mask) const + { + return castRay(from, to, {}, {}, mask); + } + + virtual RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius, + int mask = CollisionType_Default, int group = 0xff) const = 0; + + /// Return true if actor1 can see actor2. + virtual bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const = 0; }; } diff --git a/apps/openmw/mwphysics/stepper.cpp b/apps/openmw/mwphysics/stepper.cpp index 1f53c1ac519..5b7cde3015f 100644 --- a/apps/openmw/mwphysics/stepper.cpp +++ b/apps/openmw/mwphysics/stepper.cpp @@ -3,44 +3,49 @@ #include #include +#include + #include "collisiontype.hpp" #include "constants.hpp" #include "movementsolver.hpp" namespace MWPhysics { - static bool canStepDown(const ActorTracer &stepper) + static bool canStepDown(const ActorTracer& stepper) { if (!stepper.mHitObject) return false; - static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); + static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(Constants::sMaxSlope)); if (stepper.mPlaneNormal.z() <= sMaxSlopeCos) return false; return stepper.mHitObject->getBroadphaseHandle()->m_collisionFilterGroup != CollisionType_Actor; } - Stepper::Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj) + Stepper::Stepper(const btCollisionWorld* colWorld, const btCollisionObject* colObj) : mColWorld(colWorld) , mColObj(colObj) { } - bool Stepper::step(osg::Vec3f &position, osg::Vec3f &velocity, float &remainingTime, const bool & onGround, bool firstIteration) + bool Stepper::step( + osg::Vec3f& position, osg::Vec3f& velocity, float& remainingTime, const bool& onGround, bool firstIteration) { - if(velocity.x() == 0.0 && velocity.y() == 0.0) + if (velocity.x() == 0.0 && velocity.y() == 0.0) return false; - // Stairstepping algorithms work by moving up to avoid the step, moving forwards, then moving back down onto the ground. - // This algorithm has a couple of minor problems, but they don't cause problems for sane geometry, and just prevent stepping on insane geometry. + // Stairstepping algorithms work by moving up to avoid the step, moving forwards, then moving back down onto the + // ground. This algorithm has a couple of minor problems, but they don't cause problems for sane geometry, and + // just prevent stepping on insane geometry. - mUpStepper.doTrace(mColObj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), mColWorld); + mUpStepper.doTrace( + mColObj, position, position + osg::Vec3f(0.0f, 0.0f, Constants::sStepSizeUp), mColWorld, onGround); float upDistance = 0; - if(!mUpStepper.mHitObject) - upDistance = sStepSizeUp; - else if(mUpStepper.mFraction*sStepSizeUp > sCollisionMargin) - upDistance = mUpStepper.mFraction*sStepSizeUp - sCollisionMargin; + if (!mUpStepper.mHitObject) + upDistance = Constants::sStepSizeUp; + else if (mUpStepper.mFraction * Constants::sStepSizeUp > sCollisionMargin) + upDistance = mUpStepper.mFraction * Constants::sStepSizeUp - sCollisionMargin; else { return false; @@ -54,75 +59,79 @@ namespace MWPhysics auto normalMove = toMove; auto moveDistance = normalMove.normalize(); // attempt 1: normal movement - // attempt 2: fixed distance movement, only happens on the first movement solver iteration/bounce each frame to avoid a glitch - // attempt 3: further, less tall fixed distance movement, same as above - // If you're making a full conversion you should purge the logic for attempts 2 and 3. Attempts 2 and 3 just try to work around problems with vanilla Morrowind assets. + // attempt 2: fixed distance movement, only happens on the first movement solver iteration/bounce each frame to + // avoid a glitch attempt 3: further, less tall fixed distance movement, same as above If you're making a full + // conversion you should purge the logic for attempts 2 and 3. Attempts 2 and 3 just try to work around problems + // with vanilla Morrowind assets. int attempt = 0; float downStepSize = 0; - while(attempt < 3) + while (attempt < 3) { attempt++; - if(attempt == 1) + if (attempt == 1) tracerDest = tracerPos + toMove; else if (!sDoExtraStairHacks) // early out if we have extra hacks disabled { return false; } - else if(attempt == 2) + else if (attempt == 2) { moveDistance = sMinStep; - tracerDest = tracerPos + normalMove*sMinStep; + tracerDest = tracerPos + normalMove * sMinStep; } - else if(attempt == 3) + else if (attempt == 3) { - if(upDistance > sStepSizeUp) + if (upDistance > Constants::sStepSizeUp) { - upDistance = sStepSizeUp; + upDistance = Constants::sStepSizeUp; tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); } moveDistance = sMinStep2; - tracerDest = tracerPos + normalMove*sMinStep2; + tracerDest = tracerPos + normalMove * sMinStep2; } mTracer.doTrace(mColObj, tracerPos, tracerDest, mColWorld); - if(mTracer.mHitObject) + if (mTracer.mHitObject) { // map against what we hit, minus the safety margin moveDistance *= mTracer.mFraction; - if(moveDistance <= sCollisionMargin) // didn't move enough to accomplish anything + if (moveDistance <= sCollisionMargin) // didn't move enough to accomplish anything { return false; } moveDistance -= sCollisionMargin; - tracerDest = tracerPos + normalMove*moveDistance; + tracerDest = tracerPos + normalMove * moveDistance; // safely eject from what we hit by the safety margin - auto tempDest = tracerDest + mTracer.mPlaneNormal*sCollisionMargin*2; + auto tempDest = tracerDest + mTracer.mPlaneNormal * sCollisionMargin * 2; ActorTracer tempTracer; tempTracer.doTrace(mColObj, tracerDest, tempDest, mColWorld); - if(tempTracer.mFraction > 0.5f) // distance to any object is greater than sCollisionMargin (we checked sCollisionMargin*2 distance) + if (tempTracer.mFraction > 0.5f) // distance to any object is greater than sCollisionMargin (we checked + // sCollisionMargin*2 distance) { - auto effectiveFraction = tempTracer.mFraction*2.0f - 1.0f; - tracerDest += mTracer.mPlaneNormal*sCollisionMargin*effectiveFraction; + auto effectiveFraction = tempTracer.mFraction * 2.0f - 1.0f; + tracerDest += mTracer.mPlaneNormal * sCollisionMargin * effectiveFraction; } } - if(attempt > 2) // do not allow stepping down below original height for attempt 3 + if (attempt > 2) // do not allow stepping down below original height for attempt 3 downStepSize = upDistance; else downStepSize = moveDistance + upDistance + sStepSizeDown; - mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld); + mDownStepper.doTrace( + mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld, onGround); // can't step down onto air, non-walkable-slopes, or actors - // NOTE: using a capsule causes isWalkableSlope (used in canStepDown) to fail on certain geometry that were intended to be valid at the bottoms of stairs - // (like the bottoms of the staircases in aldruhn's guild of mages) - // The old code worked around this by trying to do mTracer again with a fixed distance of sMinStep (10.0) but it caused all sorts of other problems. - // Switched back to cylinders to avoid that and similer problems. - if(canStepDown(mDownStepper)) + // NOTE: using a capsule causes isWalkableSlope (used in canStepDown) to fail on certain geometry that were + // intended to be valid at the bottoms of stairs (like the bottoms of the staircases in aldruhn's guild of + // mages) The old code worked around this by trying to do mTracer again with a fixed distance of sMinStep + // (10.0) but it caused all sorts of other problems. Switched back to cylinders to avoid that and similer + // problems. + if (canStepDown(mDownStepper)) { break; } @@ -130,12 +139,12 @@ namespace MWPhysics { // do not try attempt 3 if we just tried attempt 2 and the horizontal distance was rather large // (forces actor to get snug against the defective ledge for attempt 3 to be tried) - if(attempt == 2 && moveDistance > upDistance-(mDownStepper.mFraction*downStepSize)) + if (attempt == 2 && moveDistance > upDistance - (mDownStepper.mFraction * downStepSize)) { return false; } // do next attempt if first iteration of movement solver and not out of attempts - if(firstIteration && attempt < 3) + if (firstIteration && attempt < 3) { continue; } @@ -146,18 +155,18 @@ namespace MWPhysics // note: can't downstep onto actors so no need to pick safety margin float downDistance = 0; - if(mDownStepper.mFraction*downStepSize > sCollisionMargin) - downDistance = mDownStepper.mFraction*downStepSize - sCollisionMargin; + if (mDownStepper.mFraction * downStepSize > sCollisionMargin) + downDistance = mDownStepper.mFraction * downStepSize - sCollisionMargin; - if(downDistance-sCollisionMargin-sGroundOffset > upDistance && !onGround) + if (downDistance - sCollisionMargin - sGroundOffset > upDistance && !onGround) return false; auto newpos = tracerDest + osg::Vec3f(0.0f, 0.0f, -downDistance); - if((position-newpos).length2() < sCollisionMargin*sCollisionMargin) + if ((position - newpos).length2() < sCollisionMargin * sCollisionMargin) return false; - if(mTracer.mHitObject) + if (mTracer.mHitObject) { auto planeNormal = mTracer.mPlaneNormal; if (onGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) @@ -171,7 +180,7 @@ namespace MWPhysics position = newpos; - remainingTime *= (1.0f-mTracer.mFraction); // remaining time is proportional to remaining distance + remainingTime *= (1.0f - mTracer.mFraction); // remaining time is proportional to remaining distance return true; } } diff --git a/apps/openmw/mwphysics/stepper.hpp b/apps/openmw/mwphysics/stepper.hpp index 512493c524f..d555ef540ee 100644 --- a/apps/openmw/mwphysics/stepper.hpp +++ b/apps/openmw/mwphysics/stepper.hpp @@ -16,15 +16,16 @@ namespace MWPhysics class Stepper { private: - const btCollisionWorld *mColWorld; - const btCollisionObject *mColObj; + const btCollisionWorld* mColWorld; + const btCollisionObject* mColObj; ActorTracer mTracer, mUpStepper, mDownStepper; public: - Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj); + Stepper(const btCollisionWorld* colWorld, const btCollisionObject* colObj); - bool step(osg::Vec3f &position, osg::Vec3f &velocity, float &remainingTime, const bool & onGround, bool firstIteration); + bool step(osg::Vec3f& position, osg::Vec3f& velocity, float& remainingTime, const bool& onGround, + bool firstIteration); }; } diff --git a/apps/openmw/mwphysics/trace.cpp b/apps/openmw/mwphysics/trace.cpp index 049d026e8ed..9e60a7c6264 100644 --- a/apps/openmw/mwphysics/trace.cpp +++ b/apps/openmw/mwphysics/trace.cpp @@ -5,82 +5,120 @@ #include #include -#include "collisiontype.hpp" #include "actor.hpp" #include "actorconvexcallback.hpp" +#include "collisiontype.hpp" namespace MWPhysics { -void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) -{ - const btVector3 btstart = Misc::Convert::toBullet(start); - const btVector3 btend = Misc::Convert::toBullet(end); - - const btTransform &trans = actor->getWorldTransform(); - btTransform from(trans); - btTransform to(trans); - from.setOrigin(btstart); - to.setOrigin(btend); + ActorConvexCallback sweepHelper(const btCollisionObject* actor, const btVector3& from, const btVector3& to, + const btCollisionWorld* world, bool actorFilter) + { + const btTransform& trans = actor->getWorldTransform(); + btTransform transFrom(trans); + btTransform transTo(trans); + transFrom.setOrigin(from); + transTo.setOrigin(to); - const btVector3 motion = btstart-btend; - ActorConvexCallback newTraceCallback(actor, motion, btScalar(0.0), world); - // Inherit the actor's collision group and mask - newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; - newTraceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; + const btCollisionShape* shape = actor->getCollisionShape(); + assert(shape->isConvex()); - const btCollisionShape *shape = actor->getCollisionShape(); - assert(shape->isConvex()); - world->convexSweepTest(static_cast(shape), from, to, newTraceCallback); + const btVector3 motion + = from - to; // FIXME: this is backwards; means ActorConvexCallback is doing dot product tests backwards too + ActorConvexCallback traceCallback(actor, motion, btScalar(0.0), world); + // Inherit the actor's collision group and mask + traceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; + traceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; + if (actorFilter) + traceCallback.m_collisionFilterMask &= ~CollisionType_Actor; - // Copy the hit data over to our trace results struct: - if(newTraceCallback.hasHit()) - { - mFraction = newTraceCallback.m_closestHitFraction; - mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); - mEndPos = (end-start)*mFraction + start; - mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); - mHitObject = newTraceCallback.m_hitCollisionObject; + world->convexSweepTest(static_cast(shape), transFrom, transTo, traceCallback); + return traceCallback; } - else + + void ActorTracer::doTrace(const btCollisionObject* actor, const osg::Vec3f& start, const osg::Vec3f& end, + const btCollisionWorld* world, bool attempt_short_trace) { - mEndPos = end; - mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f); - mFraction = 1.0f; - mHitPoint = end; - mHitObject = nullptr; - } -} + const btVector3 btstart = Misc::Convert::toBullet(start); + btVector3 btend = Misc::Convert::toBullet(end); -void ActorTracer::findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) -{ - const btVector3 btstart = Misc::Convert::toBullet(start); - const btVector3 btend = Misc::Convert::toBullet(end); + // Because Bullet's collision trace tests touch *all* geometry in its path, a lot of long collision tests + // will unnecessarily test against complex meshes that are dozens of units away. This wouldn't normally be + // a problem, but bullet isn't the fastest in the world when it comes to doing tests against triangle meshes. + // Therefore, we try out a short trace first, then only fall back to the full length trace if needed. + // This trace needs to be at least a couple units long, but there's no one particular ideal length. + // The length of 2.1 chosen here is a "works well in practice after testing a few random lengths" value. + // (Also, we only do this short test if the intended collision trace is long enough for it to make sense.) + const float fallback_length = 2.1f; + bool doing_short_trace = false; + // For some reason, typical scenes perform a little better if we increase the threshold length for the length + // test. (Multiplying by 2 in 'square distance' units gives us about 1.4x the threshold length. In benchmarks + // this was + // slightly better for the performance of normal scenes than 4.0, and just plain better than 1.0.) + if (attempt_short_trace && (btend - btstart).length2() > fallback_length * fallback_length * 2.0) + { + btend = btstart + (btend - btstart).normalized() * fallback_length; + doing_short_trace = true; + } - const btTransform &trans = actor->getCollisionObject()->getWorldTransform(); - btTransform from(trans.getBasis(), btstart); - btTransform to(trans.getBasis(), btend); + const auto traceCallback = sweepHelper(actor, btstart, btend, world, false); - const btVector3 motion = btstart-btend; - ActorConvexCallback newTraceCallback(actor->getCollisionObject(), motion, btScalar(0.0), world); - // Inherit the actor's collision group and mask - newTraceCallback.m_collisionFilterGroup = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterGroup; - newTraceCallback.m_collisionFilterMask = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterMask; - newTraceCallback.m_collisionFilterMask &= ~CollisionType_Actor; + // Copy the hit data over to our trace results struct: + if (traceCallback.hasHit()) + { + mFraction = traceCallback.m_closestHitFraction; + // ensure fraction is correct (covers intended distance traveled instead of actual distance traveled) + if (doing_short_trace && (end - start).length2() > 0.0) + mFraction *= (btend - btstart).length() / (end - start).length(); + mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld); + mEndPos = (end - start) * mFraction + start; + mHitPoint = Misc::Convert::toOsg(traceCallback.m_hitPointWorld); + mHitObject = traceCallback.m_hitCollisionObject; + } + else + { + if (doing_short_trace) + { + btend = Misc::Convert::toBullet(end); + const auto newTraceCallback = sweepHelper(actor, btstart, btend, world, false); - world->convexSweepTest(actor->getConvexShape(), from, to, newTraceCallback); - if(newTraceCallback.hasHit()) - { - mFraction = newTraceCallback.m_closestHitFraction; - mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); - mEndPos = (end-start)*mFraction + start; + if (newTraceCallback.hasHit()) + { + mFraction = newTraceCallback.m_closestHitFraction; + mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mEndPos = (end - start) * mFraction + start; + mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); + mHitObject = newTraceCallback.m_hitCollisionObject; + return; + } + } + // fallthrough + mEndPos = end; + mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f); + mFraction = 1.0f; + mHitPoint = end; + mHitObject = nullptr; + } } - else + + void ActorTracer::findGround( + const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) { - mEndPos = end; - mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f); - mFraction = 1.0f; + const auto traceCallback = sweepHelper( + actor->getCollisionObject(), Misc::Convert::toBullet(start), Misc::Convert::toBullet(end), world, true); + if (traceCallback.hasHit()) + { + mFraction = traceCallback.m_closestHitFraction; + mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld); + mEndPos = (end - start) * mFraction + start; + } + else + { + mEndPos = end; + mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f); + mFraction = 1.0f; + } } -} } diff --git a/apps/openmw/mwphysics/trace.h b/apps/openmw/mwphysics/trace.h index 0297c9e076f..71475e793ca 100644 --- a/apps/openmw/mwphysics/trace.h +++ b/apps/openmw/mwphysics/trace.h @@ -6,7 +6,6 @@ class btCollisionObject; class btCollisionWorld; - namespace MWPhysics { class Actor; @@ -20,8 +19,10 @@ namespace MWPhysics float mFraction; - void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); - void findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); + void doTrace(const btCollisionObject* actor, const osg::Vec3f& start, const osg::Vec3f& end, + const btCollisionWorld* world, bool attempt_short_trace = false); + void findGround( + const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); }; } diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index 59bc3276567..35ff81a9ca9 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -1,572 +1,592 @@ #include "actoranimation.hpp" #include -#include #include +#include #include -#include -#include +#include +#include +#include #include #include +#include +#include #include #include #include -#include +#include -#include +#include #include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwworld/ptr.hpp" -#include "../mwworld/class.hpp" -#include "../mwworld/cellstore.hpp" -#include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/drawstate.hpp" #include "../mwmechanics/weapontype.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/ptr.hpp" +#include "actorutil.hpp" #include "vismask.hpp" namespace MWRender { -ActorAnimation::ActorAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem) - : Animation(ptr, parentNode, resourceSystem) -{ - MWWorld::ContainerStore& store = mPtr.getClass().getContainerStore(mPtr); - - for (MWWorld::ConstContainerStoreIterator iter = store.cbegin(MWWorld::ContainerStore::Type_Light); - iter != store.cend(); ++iter) + ActorAnimation::ActorAnimation( + const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem) + : Animation(ptr, std::move(parentNode), resourceSystem) { - const ESM::Light* light = iter->get()->mBase; - if (!(light->mData.mFlags & ESM::Light::Carry)) + MWWorld::ContainerStore& store = mPtr.getClass().getContainerStore(mPtr); + + for (MWWorld::ConstContainerStoreIterator iter = store.cbegin(MWWorld::ContainerStore::Type_Light); + iter != store.cend(); ++iter) { - addHiddenItemLight(*iter, light); + const ESM::Light* light = iter->get()->mBase; + if (!(light->mData.mFlags & ESM::Light::Carry)) + { + addHiddenItemLight(*iter, light); + } } - } - // Make sure we cleaned object from effects, just in cast if we re-use node - removeEffects(); -} + // Make sure we cleaned object from effects, just in cast if we re-use node + removeEffects(); + } -ActorAnimation::~ActorAnimation() -{ - for (ItemLightMap::iterator iter = mItemLights.begin(); iter != mItemLights.end(); ++iter) + ActorAnimation::~ActorAnimation() { - mInsert->removeChild(iter->second); + removeFromSceneImpl(); } - mScabbard.reset(); - mHolsteredShield.reset(); -} - -PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor) -{ - osg::Group* parent = getBoneByName(bonename); - if (!parent) - return nullptr; + PartHolderPtr ActorAnimation::attachMesh( + const std::string& model, std::string_view bonename, bool enchantedGlow, osg::Vec4f* glowColor) + { + osg::Group* parent = getBoneByName(bonename); + if (!parent) + return nullptr; - osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model, parent); + osg::ref_ptr instance + = mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(model), parent); - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); - if (found == nodeMap.end()) - return PartHolderPtr(); + const NodeMap& nodeMap = getNodeMap(); + NodeMap::const_iterator found = nodeMap.find(bonename); + if (found == nodeMap.end()) + return {}; - if (enchantedGlow) - mGlowUpdater = SceneUtil::addEnchantedGlow(instance, mResourceSystem, *glowColor); + if (enchantedGlow) + mGlowUpdater = SceneUtil::addEnchantedGlow(instance, mResourceSystem, *glowColor); - return PartHolderPtr(new PartHolder(instance)); -} + return std::make_unique(instance); + } -std::string ActorAnimation::getShieldMesh(MWWorld::ConstPtr shield) const -{ - std::string mesh = shield.getClass().getModel(shield); - const ESM::Armor *armor = shield.get()->mBase; - const std::vector& bodyparts = armor->mParts.mParts; - if (!bodyparts.empty()) + osg::ref_ptr ActorAnimation::attach( + const std::string& model, std::string_view bonename, std::string_view bonefilter, bool isLight) { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const MWWorld::Store &partStore = store.get(); + osg::ref_ptr templateNode + = mResourceSystem->getSceneManager()->getTemplate(VFS::Path::toNormalized(model)); + + const NodeMap& nodeMap = getNodeMap(); + auto found = nodeMap.find(bonename); + if (found == nodeMap.end()) + throw std::runtime_error("Can't find attachment node " + std::string{ bonename }); + if (isLight) + { + osg::Quat rotation(osg::DegreesToRadians(-90.f), osg::Vec3f(1, 0, 0)); + return SceneUtil::attach( + templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager(), &rotation); + } + return SceneUtil::attach( + std::move(templateNode), mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager()); + } - // Try to get shield model from bodyparts first, with ground model as fallback - for (const auto& part : bodyparts) + std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const + { + const ESM::Armor* armor = shield.get()->mBase; + const std::vector& bodyparts = armor->mParts.mParts; + // Try to recover the body part model, use ground model as a fallback otherwise. + if (!bodyparts.empty()) { - // Assume all creatures use the male mesh. - if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) - continue; - const ESM::BodyPart *bodypart = partStore.search(part.mMale); - if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const MWWorld::Store& partStore = store.get(); + for (const auto& part : bodyparts) { - mesh = "meshes\\" + bodypart->mModel; - break; + if (part.mPart != ESM::PRT_Shield) + continue; + + const ESM::RefId* bodypartName = nullptr; + if (female && !part.mFemale.empty()) + bodypartName = &part.mFemale; + else if (!part.mMale.empty()) + bodypartName = &part.mMale; + + if (bodypartName && !bodypartName->empty()) + { + const ESM::BodyPart* bodypart = partStore.search(*bodypartName); + if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) + return std::string(); + if (!bodypart->mModel.empty()) + return Misc::ResourceHelpers::correctMeshPath(bodypart->mModel); + } } } + return shield.getClass().getCorrectedModel(shield); } - if (mesh.empty()) - return mesh; - - std::string holsteredName = mesh; - holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); - if(mResourceSystem->getVFS()->exists(holsteredName)) + std::string ActorAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const { - osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); - SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); - shieldTemplate->accept(findVisitor); - osg::ref_ptr sheathNode = findVisitor.mFoundNode; - if(!sheathNode) - return std::string(); - } + std::string mesh = getShieldMesh(shield, false); - return mesh; -} + if (mesh.empty()) + return mesh; -bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const -{ - static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); - if (shieldSheathing) + const VFS::Path::Normalized holsteredName(addSuffixBeforeExtension(mesh, "_sh")); + if (mResourceSystem->getVFS()->exists(holsteredName)) + { + osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); + SceneUtil::FindByNameVisitor findVisitor("Bip01 Sheath"); + shieldTemplate->accept(findVisitor); + osg::ref_ptr sheathNode = findVisitor.mFoundNode; + if (!sheathNode) + return std::string(); + } + + return mesh; + } + + bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const { - const MWWorld::Class &cls = mPtr.getClass(); - MWMechanics::CreatureStats &stats = cls.getCreatureStats(mPtr); - if (cls.hasInventoryStore(mPtr) && weaptype != ESM::Weapon::Spell) + if (Settings::game().mShieldSheathing) { - SceneUtil::FindByNameVisitor findVisitor ("Bip01 AttachShield"); - mObjectRoot->accept(findVisitor); - if (findVisitor.mFoundNode || (mPtr == MWMechanics::getPlayer() && mPtr.isInCell() && MWBase::Environment::get().getWorld()->isFirstPerson())) + const MWWorld::Class& cls = mPtr.getClass(); + MWMechanics::CreatureStats& stats = cls.getCreatureStats(mPtr); + if (cls.hasInventoryStore(mPtr) && stats.getDrawState() == MWMechanics::DrawState::Nothing) { - const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); - const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield != inv.end() && shield->getTypeName() == typeid(ESM::Armor).name() && !getShieldMesh(*shield).empty()) + SceneUtil::FindByNameVisitor findVisitor("Bip01 AttachShield"); + mObjectRoot->accept(findVisitor); + if (findVisitor.mFoundNode) { - if(stats.getDrawState() != MWMechanics::DrawState_Weapon) + const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); + const MWWorld::ConstContainerStoreIterator shield + = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (shield != inv.end() && shield->getType() == ESM::Armor::sRecordId + && !getSheathedShieldMesh(*shield).empty()) return false; - - if (weapon != inv.end()) - { - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) - { - const MWWorld::LiveCellRef *ref = weapon->get(); - ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; - return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); - } - else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) - return true; - } } } } + + return !(MWMechanics::getWeaponType(weaptype)->mFlags & ESM::WeaponType::TwoHanded); } - return !(MWMechanics::getWeaponType(weaptype)->mFlags & ESM::WeaponType::TwoHanded); -} + void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) + { + if (!Settings::game().mShieldSheathing) + return; -void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) -{ - static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); - if (!shieldSheathing) - return; + if (!mPtr.getClass().hasInventoryStore(mPtr)) + return; - if (!mPtr.getClass().hasInventoryStore(mPtr)) - return; + mHolsteredShield.reset(); - mHolsteredShield.reset(); + if (showCarriedLeft) + return; - if (showCarriedLeft) - return; + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (shield == inv.end() || shield->getType() != ESM::Armor::sRecordId) + return; - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) - return; + // Can not show holdstered shields with two-handed weapons at all + const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weapon == inv.end()) + return; - // Can not show holdstered shields with two-handed weapons at all - const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end()) - return; + auto type = weapon->getType(); + if (type == ESM::Weapon::sRecordId) + { + const MWWorld::LiveCellRef* ref = weapon->get(); + ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; + if (MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded) + return; + } - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) - { - const MWWorld::LiveCellRef *ref = weapon->get(); - ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; - if (MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded) + std::string mesh = getSheathedShieldMesh(*shield); + if (mesh.empty()) return; - } - std::string mesh = getShieldMesh(*shield); - if (mesh.empty()) - return; + std::string_view boneName = "Bip01 AttachShield"; + osg::Vec4f glowColor = shield->getClass().getEnchantmentColor(*shield); + const std::string holsteredName = addSuffixBeforeExtension(mesh, "_sh"); + bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty(); - std::string boneName = "Bip01 AttachShield"; - osg::Vec4f glowColor = shield->getClass().getEnchantmentColor(*shield); - std::string holsteredName = mesh; - holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); - bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty(); + // If we have no dedicated sheath model, use basic shield model as fallback. + if (!mResourceSystem->getVFS()->exists(holsteredName)) + mHolsteredShield = attachMesh(mesh, boneName, isEnchanted, &glowColor); + else + mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted, &glowColor); - // If we have no dedicated sheath model, use basic shield model as fallback. - if (!mResourceSystem->getVFS()->exists(holsteredName)) - mHolsteredShield = attachMesh(mesh, boneName, isEnchanted, &glowColor); - else - mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted, &glowColor); + if (!mHolsteredShield) + return; - if (!mHolsteredShield) - return; + SceneUtil::FindByNameVisitor findVisitor("Bip01 Sheath"); + mHolsteredShield->getNode()->accept(findVisitor); + osg::Group* shieldNode = findVisitor.mFoundNode; - SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); - mHolsteredShield->getNode()->accept(findVisitor); - osg::Group* shieldNode = findVisitor.mFoundNode; + // If mesh author declared an empty sheath node, use transformation from this node, but use the common shield + // mesh. This approach allows to tweak shield position without need to store the whole shield mesh in the _sh + // file. + if (shieldNode && !shieldNode->getNumChildren()) + { + osg::ref_ptr fallbackNode + = mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(mesh), shieldNode); + if (isEnchanted) + SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor); + } + } - // If mesh author declared an empty sheath node, use transformation from this node, but use the common shield mesh. - // This approach allows to tweak shield position without need to store the whole shield mesh in the _sh file. - if (shieldNode && !shieldNode->getNumChildren()) + bool ActorAnimation::useShieldAnimations() const { - osg::ref_ptr fallbackNode = mResourceSystem->getSceneManager()->getInstance(mesh, shieldNode); - if (isEnchanted) - SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor); - } + if (!Settings::game().mShieldSheathing) + return false; - if (mAlpha != 1.f) - mResourceSystem->getSceneManager()->recreateShaders(mHolsteredShield->getNode()); -} + const MWWorld::Class& cls = mPtr.getClass(); + if (!cls.hasInventoryStore(mPtr)) + return false; -bool ActorAnimation::useShieldAnimations() const -{ - static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); - if (!shieldSheathing) - return false; + if (getTextKeyTime("shield: equip attach") < 0 || getTextKeyTime("shield: unequip detach") < 0) + return false; - const MWWorld::Class &cls = mPtr.getClass(); - if (!cls.hasInventoryStore(mPtr)) - return false; + const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); + const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (weapon != inv.end() && shield != inv.end() && shield->getType() == ESM::Armor::sRecordId + && !getSheathedShieldMesh(*shield).empty()) + { + auto type = weapon->getType(); + if (type == ESM::Weapon::sRecordId) + { + const MWWorld::LiveCellRef* ref = weapon->get(); + ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; + return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); + } + else if (type == ESM::Lockpick::sRecordId || type == ESM::Probe::sRecordId) + return true; + } - if (getTextKeyTime("shield: equip attach") < 0 || getTextKeyTime("shield: unequip detach") < 0) return false; + } - const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); - const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (weapon != inv.end() && shield != inv.end() && - shield->getTypeName() == typeid(ESM::Armor).name() && - !getShieldMesh(*shield).empty()) + osg::Group* ActorAnimation::getBoneByName(std::string_view boneName) const { - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) - { - const MWWorld::LiveCellRef *ref = weapon->get(); - ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; - return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); - } - else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) - return true; - } + if (!mObjectRoot) + return nullptr; - return false; -} + SceneUtil::FindByNameVisitor findVisitor(boneName); + mObjectRoot->accept(findVisitor); -osg::Group* ActorAnimation::getBoneByName(const std::string& boneName) -{ - if (!mObjectRoot) - return nullptr; + return findVisitor.mFoundNode; + } - SceneUtil::FindByNameVisitor findVisitor (boneName); - mObjectRoot->accept(findVisitor); + std::string_view ActorAnimation::getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon) + { + if (weapon.isEmpty()) + return {}; - return findVisitor.mFoundNode; -} + auto type = weapon.getClass().getType(); + if (type == ESM::Weapon::sRecordId) + { + const MWWorld::LiveCellRef* ref = weapon.get(); + int weaponType = ref->mBase->mData.mType; + return MWMechanics::getWeaponType(weaponType)->mSheathingBone; + } -std::string ActorAnimation::getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon) -{ - std::string boneName; - if(weapon.isEmpty()) - return boneName; + return {}; + } - const std::string &type = weapon.getClass().getTypeName(); - if(type == typeid(ESM::Weapon).name()) + void ActorAnimation::resetControllers(osg::Node* node) { - const MWWorld::LiveCellRef *ref = weapon.get(); - int weaponType = ref->mBase->mData.mType; - return MWMechanics::getWeaponType(weaponType)->mSheathingBone; + if (node == nullptr) + return; + + // This is used to avoid playing animations intended for equipped weapons on holstered weapons. + SceneUtil::ForceControllerSourcesVisitor removeVisitor(std::make_shared()); + node->accept(removeVisitor); } - return boneName; -} + void ActorAnimation::updateHolsteredWeapon(bool showHolsteredWeapons) + { + if (!Settings::game().mWeaponSheathing) + return; -void ActorAnimation::resetControllers(osg::Node* node) -{ - if (node == nullptr) - return; + if (!mPtr.getClass().hasInventoryStore(mPtr)) + return; - std::shared_ptr src; - src.reset(new NullAnimationTime); - SceneUtil::AssignControllerSourcesVisitor removeVisitor(src); - node->accept(removeVisitor); -} + mScabbard.reset(); -void ActorAnimation::updateHolsteredWeapon(bool showHolsteredWeapons) -{ - static const bool weaponSheathing = Settings::Manager::getBool("weapon sheathing", "Game"); - if (!weaponSheathing) - return; - - if (!mPtr.getClass().hasInventoryStore(mPtr)) - return; - - mScabbard.reset(); - - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) - return; - - // Since throwing weapons stack themselves, do not show such weapon itself - int type = weapon->get()->mBase->mData.mType; - if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) - showHolsteredWeapons = false; - - std::string mesh = weapon->getClass().getModel(*weapon); - std::string scabbardName = mesh; - - std::string boneName = getHolsteredWeaponBoneName(*weapon); - if (mesh.empty() || boneName.empty()) - return; - - // If the scabbard is not found, use a weapon mesh as fallback. - // Note: it is unclear how to handle time for controllers attached to bodyparts, so disable them for now. - // We use the similar approach for other bodyparts. - scabbardName = scabbardName.replace(scabbardName.size()-4, 4, "_sh.nif"); - bool isEnchanted = !weapon->getClass().getEnchantment(*weapon).empty(); - if(!mResourceSystem->getVFS()->exists(scabbardName)) - { - if (showHolsteredWeapons) + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) + return; + + // Since throwing weapons stack themselves, do not show such weapon itself + int type = weapon->get()->mBase->mData.mType; + if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) + showHolsteredWeapons = false; + + std::string mesh = weapon->getClass().getCorrectedModel(*weapon); + std::string_view boneName = getHolsteredWeaponBoneName(*weapon); + if (mesh.empty() || boneName.empty()) + return; + + // If the scabbard is not found, use the weapon mesh as fallback. + const std::string scabbardName = addSuffixBeforeExtension(mesh, "_sh"); + bool isEnchanted = !weapon->getClass().getEnchantment(*weapon).empty(); + if (!mResourceSystem->getVFS()->exists(scabbardName)) { - osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); - mScabbard = attachMesh(mesh, boneName, isEnchanted, &glowColor); - if (mScabbard) - resetControllers(mScabbard->getNode()); - } + if (showHolsteredWeapons) + { + osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); + mScabbard = attachMesh(mesh, boneName, isEnchanted, &glowColor); + if (mScabbard) + resetControllers(mScabbard->getNode()); + } - return; - } + return; + } - mScabbard = attachMesh(scabbardName, boneName); - if (mScabbard) - resetControllers(mScabbard->getNode()); + mScabbard = attachMesh(scabbardName, boneName); + if (mScabbard) + resetControllers(mScabbard->getNode()); - osg::Group* weaponNode = getBoneByName("Bip01 Weapon"); - if (!weaponNode) - return; + osg::Group* weaponNode = getBoneByName("Bip01 Weapon"); + if (!weaponNode) + return; - // When we draw weapon, hide the Weapon node from sheath model. - // Otherwise add the enchanted glow to it. - if (!showHolsteredWeapons) - { - weaponNode->setNodeMask(0); - } - else - { - // If mesh author declared empty weapon node, use transformation from this node, but use the common weapon mesh. - // This approach allows to tweak weapon position without need to store the whole weapon mesh in the _sh file. - if (!weaponNode->getNumChildren()) + // When we draw weapon, hide the Weapon node from sheath model. + // Otherwise add the enchanted glow to it. + if (!showHolsteredWeapons) { - osg::ref_ptr fallbackNode = mResourceSystem->getSceneManager()->getInstance(mesh, weaponNode); - resetControllers(fallbackNode); + weaponNode->setNodeMask(0); } - - if (isEnchanted) + else { - osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); - mGlowUpdater = SceneUtil::addEnchantedGlow(weaponNode, mResourceSystem, glowColor); + // If mesh author declared empty weapon node, use transformation from this node, but use the common weapon + // mesh. This approach allows to tweak weapon position without need to store the whole weapon mesh in the + // _sh file. + if (!weaponNode->getNumChildren()) + { + osg::ref_ptr fallbackNode + = mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(mesh), weaponNode); + resetControllers(fallbackNode); + } + + if (isEnchanted) + { + osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); + mGlowUpdater = SceneUtil::addEnchantedGlow(weaponNode, mResourceSystem, glowColor); + } } } -} -void ActorAnimation::updateQuiver() -{ - static const bool weaponSheathing = Settings::Manager::getBool("weapon sheathing", "Game"); - if (!weaponSheathing) - return; - - if (!mPtr.getClass().hasInventoryStore(mPtr)) - return; - - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) - return; - - std::string mesh = weapon->getClass().getModel(*weapon); - std::string boneName = getHolsteredWeaponBoneName(*weapon); - if (mesh.empty() || boneName.empty()) - return; - - osg::Group* ammoNode = getBoneByName("Bip01 Ammo"); - if (!ammoNode) - return; - - // Special case for throwing weapons - they do not use ammo, but they stack themselves - bool suitableAmmo = false; - MWWorld::ConstContainerStoreIterator ammo = weapon; - unsigned int ammoCount = 0; - int type = weapon->get()->mBase->mData.mType; - const auto& weaponType = MWMechanics::getWeaponType(type); - if (weaponType->mWeaponClass == ESM::WeaponType::Thrown) + void ActorAnimation::updateQuiver() { - ammoCount = ammo->getRefData().getCount(); - osg::Group* throwingWeaponNode = getBoneByName(weaponType->mAttachBone); - if (throwingWeaponNode && throwingWeaponNode->getNumChildren()) - ammoCount--; + if (!Settings::game().mWeaponSheathing) + return; - suitableAmmo = true; - } - else - { - ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); - if (ammo == inv.end()) + if (!mPtr.getClass().hasInventoryStore(mPtr)) return; - ammoCount = ammo->getRefData().getCount(); - bool arrowAttached = isArrowAttached(); - if (arrowAttached) - ammoCount--; + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) + return; - suitableAmmo = ammo->get()->mBase->mData.mType == weaponType->mAmmoType; - } + std::string_view mesh = weapon->getClass().getModel(*weapon); + std::string_view boneName = getHolsteredWeaponBoneName(*weapon); + if (mesh.empty() || boneName.empty()) + return; - if (!suitableAmmo) - return; + osg::Group* ammoNode = getBoneByName("Bip01 Ammo"); + if (!ammoNode) + return; - // We should not show more ammo than equipped and more than quiver mesh has - ammoCount = std::min(ammoCount, ammoNode->getNumChildren()); + // Special case for throwing weapons - they do not use ammo, but they stack themselves + bool suitableAmmo = false; + MWWorld::ConstContainerStoreIterator ammo = weapon; + unsigned int ammoCount = 0; + int type = weapon->get()->mBase->mData.mType; + const auto& weaponType = MWMechanics::getWeaponType(type); + if (weaponType->mWeaponClass == ESM::WeaponType::Thrown) + { + ammoCount = ammo->getCellRef().getCount(); + osg::Group* throwingWeaponNode = getBoneByName(weaponType->mAttachBone); + if (throwingWeaponNode && throwingWeaponNode->getNumChildren()) + ammoCount--; - // Remove existing ammo nodes - for (unsigned int i=0; igetNumChildren(); ++i) - { - osg::ref_ptr arrowNode = ammoNode->getChild(i)->asGroup(); - if (!arrowNode->getNumChildren()) - continue; + suitableAmmo = true; + } + else + { + ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + if (ammo == inv.end()) + return; - osg::ref_ptr arrowChildNode = arrowNode->getChild(0); - arrowNode->removeChild(arrowChildNode); - } + ammoCount = ammo->getCellRef().getCount(); + bool arrowAttached = isArrowAttached(); + if (arrowAttached) + ammoCount--; - // Add new ones - osg::Vec4f glowColor = ammo->getClass().getEnchantmentColor(*ammo); - std::string model = ammo->getClass().getModel(*ammo); - for (unsigned int i=0; i arrowNode = ammoNode->getChild(i)->asGroup(); - osg::ref_ptr arrow = mResourceSystem->getSceneManager()->getInstance(model, arrowNode); - if (!ammo->getClass().getEnchantment(*ammo).empty()) - mGlowUpdater = SceneUtil::addEnchantedGlow(arrow, mResourceSystem, glowColor); + suitableAmmo = ammo->get()->mBase->mData.mType == weaponType->mAmmoType; + } + + if (!suitableAmmo) + return; + + // We should not show more ammo than equipped and more than quiver mesh has + ammoCount = std::min(ammoCount, ammoNode->getNumChildren()); + + // Remove existing ammo nodes + for (unsigned int i = 0; i < ammoNode->getNumChildren(); ++i) + { + osg::ref_ptr arrowNode = ammoNode->getChild(i)->asGroup(); + if (!arrowNode->getNumChildren()) + continue; + + osg::ref_ptr arrowChildNode = arrowNode->getChild(0); + arrowNode->removeChild(arrowChildNode); + } + + // Add new ones + osg::Vec4f glowColor = ammo->getClass().getEnchantmentColor(*ammo); + const VFS::Path::Normalized model(ammo->getClass().getCorrectedModel(*ammo)); + for (unsigned int i = 0; i < ammoCount; ++i) + { + osg::ref_ptr arrowNode = ammoNode->getChild(i)->asGroup(); + osg::ref_ptr arrow = mResourceSystem->getSceneManager()->getInstance(model, arrowNode); + if (!ammo->getClass().getEnchantment(*ammo).empty()) + mGlowUpdater = SceneUtil::addEnchantedGlow(std::move(arrow), mResourceSystem, glowColor); + } } -} -void ActorAnimation::itemAdded(const MWWorld::ConstPtr& item, int /*count*/) -{ - if (item.getTypeName() == typeid(ESM::Light).name()) + void ActorAnimation::itemAdded(const MWWorld::ConstPtr& item, int /*count*/) { - const ESM::Light* light = item.get()->mBase; - if (!(light->mData.mFlags & ESM::Light::Carry)) + if (item.getType() == ESM::Light::sRecordId) { - addHiddenItemLight(item, light); + const ESM::Light* light = item.get()->mBase; + if (!(light->mData.mFlags & ESM::Light::Carry)) + { + addHiddenItemLight(item, light); + } } - } - if (!mPtr.getClass().hasInventoryStore(mPtr)) - return; + if (!mPtr.getClass().hasInventoryStore(mPtr)) + return; - // If the count of equipped ammo or throwing weapon was changed, we should update quiver - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) - return; + // If the count of equipped ammo or throwing weapon was changed, we should update quiver + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) + return; - MWWorld::ConstContainerStoreIterator ammo = inv.end(); - int type = weapon->get()->mBase->mData.mType; - if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) - ammo = weapon; - else - ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + MWWorld::ConstContainerStoreIterator ammo = inv.end(); + int type = weapon->get()->mBase->mData.mType; + if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) + ammo = weapon; + else + ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); - if(ammo != inv.end() && item.getCellRef().getRefId() == ammo->getCellRef().getRefId()) - updateQuiver(); -} + if (ammo != inv.end() && item.getCellRef().getRefId() == ammo->getCellRef().getRefId()) + updateQuiver(); + } -void ActorAnimation::itemRemoved(const MWWorld::ConstPtr& item, int /*count*/) -{ - if (item.getTypeName() == typeid(ESM::Light).name()) + void ActorAnimation::itemRemoved(const MWWorld::ConstPtr& item, int /*count*/) { - ItemLightMap::iterator iter = mItemLights.find(item); - if (iter != mItemLights.end()) + if (item.getType() == ESM::Light::sRecordId) { - if (!item.getRefData().getCount()) + ItemLightMap::iterator iter = mItemLights.find(item); + if (iter != mItemLights.end()) { - removeHiddenItemLight(item); + if (!item.getCellRef().getCount()) + { + removeHiddenItemLight(item); + } } } - } - if (!mPtr.getClass().hasInventoryStore(mPtr)) - return; - - // If the count of equipped ammo or throwing weapon was changed, we should update quiver - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) - return; + if (!mPtr.getClass().hasInventoryStore(mPtr)) + return; - MWWorld::ConstContainerStoreIterator ammo = inv.end(); - int type = weapon->get()->mBase->mData.mType; - if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) - ammo = weapon; - else - ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + // If the count of equipped ammo or throwing weapon was changed, we should update quiver + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) + return; - if(ammo != inv.end() && item.getCellRef().getRefId() == ammo->getCellRef().getRefId()) - updateQuiver(); -} + MWWorld::ConstContainerStoreIterator ammo = inv.end(); + int type = weapon->get()->mBase->mData.mType; + if (MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown) + ammo = weapon; + else + ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); -void ActorAnimation::addHiddenItemLight(const MWWorld::ConstPtr& item, const ESM::Light* esmLight) -{ - if (mItemLights.find(item) != mItemLights.end()) - return; + if (ammo != inv.end() && item.getCellRef().getRefId() == ammo->getCellRef().getRefId()) + updateQuiver(); + } - bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); + void ActorAnimation::addHiddenItemLight(const MWWorld::ConstPtr& item, const ESM::Light* esmLight) + { + if (mItemLights.find(item) != mItemLights.end()) + return; - osg::Vec4f ambient(1,1,1,1); - osg::ref_ptr lightSource = SceneUtil::createLightSource(esmLight, Mask_Lighting, exterior, ambient); + bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); - mInsert->addChild(lightSource); + osg::Vec4f ambient(1, 1, 1, 1); + osg::ref_ptr lightSource + = SceneUtil::createLightSource(SceneUtil::LightCommon(*esmLight), Mask_Lighting, exterior, ambient); - if (mLightListCallback && mPtr == MWMechanics::getPlayer()) - mLightListCallback->getIgnoredLightSources().insert(lightSource.get()); + mInsert->addChild(lightSource); - mItemLights.insert(std::make_pair(item, lightSource)); -} + if (mLightListCallback && mPtr == MWMechanics::getPlayer()) + mLightListCallback->getIgnoredLightSources().insert(lightSource.get()); -void ActorAnimation::removeHiddenItemLight(const MWWorld::ConstPtr& item) -{ - ItemLightMap::iterator iter = mItemLights.find(item); - if (iter == mItemLights.end()) - return; + mItemLights.insert(std::make_pair(item, lightSource)); + } - if (mLightListCallback && mPtr == MWMechanics::getPlayer()) + void ActorAnimation::removeHiddenItemLight(const MWWorld::ConstPtr& item) { - std::set::iterator ignoredIter = mLightListCallback->getIgnoredLightSources().find(iter->second.get()); - if (ignoredIter != mLightListCallback->getIgnoredLightSources().end()) - mLightListCallback->getIgnoredLightSources().erase(ignoredIter); + ItemLightMap::iterator iter = mItemLights.find(item); + if (iter == mItemLights.end()) + return; + + if (mLightListCallback && mPtr == MWMechanics::getPlayer()) + { + std::set::iterator ignoredIter + = mLightListCallback->getIgnoredLightSources().find(iter->second.get()); + if (ignoredIter != mLightListCallback->getIgnoredLightSources().end()) + mLightListCallback->getIgnoredLightSources().erase(ignoredIter); + } + + mInsert->removeChild(iter->second); + mItemLights.erase(iter); } - mInsert->removeChild(iter->second); - mItemLights.erase(iter); -} + void ActorAnimation::removeFromScene() + { + removeFromSceneImpl(); + Animation::removeFromScene(); + } + void ActorAnimation::removeFromSceneImpl() + { + for (const auto& [k, v] : mItemLights) + mInsert->removeChild(v); + } } diff --git a/apps/openmw/mwrender/actoranimation.hpp b/apps/openmw/mwrender/actoranimation.hpp index e149e44148d..b6586d4eabc 100644 --- a/apps/openmw/mwrender/actoranimation.hpp +++ b/apps/openmw/mwrender/actoranimation.hpp @@ -28,10 +28,11 @@ namespace SceneUtil namespace MWRender { -class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener -{ + class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener + { public: - ActorAnimation(const MWWorld::Ptr &ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem); + ActorAnimation( + const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem); virtual ~ActorAnimation(); void itemAdded(const MWWorld::ConstPtr& item, int count) override; @@ -40,19 +41,25 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener bool useShieldAnimations() const override; bool updateCarriedLeftVisible(const int weaptype) const override; + void removeFromScene() override; + protected: - osg::Group* getBoneByName(const std::string& boneName); + osg::Group* getBoneByName(std::string_view boneName) const; virtual void updateHolsteredWeapon(bool showHolsteredWeapons); virtual void updateHolsteredShield(bool showCarriedLeft); virtual void updateQuiver(); - virtual std::string getShieldMesh(MWWorld::ConstPtr shield) const; - virtual std::string getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); - virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor); - virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename) + std::string getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const; + virtual std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const; + virtual std::string_view getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); + virtual PartHolderPtr attachMesh( + const std::string& model, std::string_view bonename, bool enchantedGlow, osg::Vec4f* glowColor); + virtual PartHolderPtr attachMesh(const std::string& model, std::string_view bonename) { - osg::Vec4f stubColor = osg::Vec4f(0,0,0,0); + osg::Vec4f stubColor = osg::Vec4f(0, 0, 0, 0); return attachMesh(model, bonename, false, &stubColor); - }; + } + osg::ref_ptr attach( + const std::string& model, std::string_view bonename, std::string_view bonefilter, bool isLight); PartHolderPtr mScabbard; PartHolderPtr mHolsteredShield; @@ -61,10 +68,11 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener void addHiddenItemLight(const MWWorld::ConstPtr& item, const ESM::Light* esmLight); void removeHiddenItemLight(const MWWorld::ConstPtr& item); void resetControllers(osg::Node* node); + void removeFromSceneImpl(); - typedef std::map > ItemLightMap; + typedef std::map> ItemLightMap; ItemLightMap mItemLights; -}; + }; } diff --git a/apps/openmw/mwrender/actorspaths.cpp b/apps/openmw/mwrender/actorspaths.cpp index 35b2553555c..2a36e99f79f 100644 --- a/apps/openmw/mwrender/actorspaths.cpp +++ b/apps/openmw/mwrender/actorspaths.cpp @@ -1,15 +1,51 @@ #include "actorspaths.hpp" + #include "vismask.hpp" +#include +#include +#include #include +#include +#include +#include #include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include namespace MWRender { + namespace + { + osg::ref_ptr makeGroupStateSet() + { + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setAttribute(material); + return stateSet; + } + + osg::ref_ptr makeDebugDrawStateSet() + { + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setAttributeAndModes(new osg::LineWidth()); + + return stateSet; + } + } + ActorsPaths::ActorsPaths(const osg::ref_ptr& root, bool enabled) : mRootNode(root) , mEnabled(enabled) + , mGroupStateSet(makeGroupStateSet()) + , mDebugDrawStateSet(makeDebugDrawStateSet()) { } @@ -30,42 +66,44 @@ namespace MWRender } void ActorsPaths::update(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, - const DetourNavigator::Settings& settings) + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, + const DetourNavigator::Settings& settings) { if (!mEnabled) return; - const auto group = mGroups.find(actor); + const auto group = mGroups.find(actor.mRef); if (group != mGroups.end()) - mRootNode->removeChild(group->second); + mRootNode->removeChild(group->second.mNode); - const auto newGroup = SceneUtil::createAgentPathGroup(path, halfExtents, start, end, settings); - if (newGroup) - { - newGroup->setNodeMask(Mask_Debug); - mRootNode->addChild(newGroup); - mGroups[actor] = newGroup; - } + osg::ref_ptr newGroup + = SceneUtil::createAgentPathGroup(path, agentBounds, start, end, settings.mRecast, mDebugDrawStateSet); + newGroup->setNodeMask(Mask_Debug); + newGroup->setStateSet(mGroupStateSet); + + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(newGroup, "debug"); + + mRootNode->addChild(newGroup); + mGroups.insert_or_assign(group, actor.mRef, Group{ actor.mCell, std::move(newGroup) }); } void ActorsPaths::remove(const MWWorld::ConstPtr& actor) { - const auto group = mGroups.find(actor); + const auto group = mGroups.find(actor.mRef); if (group != mGroups.end()) { - mRootNode->removeChild(group->second); + mRootNode->removeChild(group->second.mNode); mGroups.erase(group); } } void ActorsPaths::removeCell(const MWWorld::CellStore* const store) { - for (auto it = mGroups.begin(); it != mGroups.end(); ) + for (auto it = mGroups.begin(); it != mGroups.end();) { - if (it->first.getCell() == store) + if (it->second.mCell == store) { - mRootNode->removeChild(it->second); + mRootNode->removeChild(it->second.mNode); it = mGroups.erase(it); } else @@ -75,25 +113,23 @@ namespace MWRender void ActorsPaths::updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) { - const auto it = mGroups.find(old); + const auto it = mGroups.find(old.mRef); if (it == mGroups.end()) return; - auto group = std::move(it->second); - mGroups.erase(it); - mGroups.insert(std::make_pair(updated, std::move(group))); + it->second.mCell = updated.mCell; } void ActorsPaths::enable() { - std::for_each(mGroups.begin(), mGroups.end(), - [&] (const Groups::value_type& v) { mRootNode->addChild(v.second); }); + std::for_each( + mGroups.begin(), mGroups.end(), [&](const Groups::value_type& v) { mRootNode->addChild(v.second.mNode); }); mEnabled = true; } void ActorsPaths::disable() { std::for_each(mGroups.begin(), mGroups.end(), - [&] (const Groups::value_type& v) { mRootNode->removeChild(v.second); }); + [&](const Groups::value_type& v) { mRootNode->removeChild(v.second.mNode); }); mEnabled = false; } } diff --git a/apps/openmw/mwrender/actorspaths.hpp b/apps/openmw/mwrender/actorspaths.hpp index 1f61834d468..61f246c7777 100644 --- a/apps/openmw/mwrender/actorspaths.hpp +++ b/apps/openmw/mwrender/actorspaths.hpp @@ -1,18 +1,23 @@ #ifndef OPENMW_MWRENDER_AGENTSPATHS_H #define OPENMW_MWRENDER_AGENTSPATHS_H -#include - -#include +#include "apps/openmw/mwworld/ptr.hpp" #include -#include #include +#include namespace osg { class Group; + class StateSet; +} + +namespace DetourNavigator +{ + struct Settings; + struct AgentBounds; } namespace MWRender @@ -26,8 +31,8 @@ namespace MWRender bool toggle(); void update(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, - const DetourNavigator::Settings& settings); + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, + const DetourNavigator::Settings& settings); void remove(const MWWorld::ConstPtr& actor); @@ -40,11 +45,19 @@ namespace MWRender void disable(); private: - using Groups = std::map>; + struct Group + { + const MWWorld::CellStore* mCell; + osg::ref_ptr mNode; + }; + + using Groups = std::map; osg::ref_ptr mRootNode; Groups mGroups; bool mEnabled; + osg::ref_ptr mGroupStateSet; + osg::ref_ptr mDebugDrawStateSet; }; } diff --git a/apps/openmw/mwrender/actorutil.cpp b/apps/openmw/mwrender/actorutil.cpp new file mode 100644 index 00000000000..3cb8adb8aa5 --- /dev/null +++ b/apps/openmw/mwrender/actorutil.cpp @@ -0,0 +1,52 @@ +#include "actorutil.hpp" + +#include +#include + +namespace MWRender +{ + const std::string& getActorSkeleton(bool firstPerson, bool isFemale, bool isBeast, bool isWerewolf) + { + if (!firstPerson) + { + if (isWerewolf) + return Settings::models().mWolfskin.get().value(); + else if (isBeast) + return Settings::models().mBaseanimkna.get().value(); + else if (isFemale) + return Settings::models().mBaseanimfemale.get().value(); + else + return Settings::models().mBaseanim.get().value(); + } + else + { + if (isWerewolf) + return Settings::models().mWolfskin1st.get().value(); + else if (isBeast) + return Settings::models().mBaseanimkna1st.get().value(); + else if (isFemale) + return Settings::models().mBaseanimfemale1st.get().value(); + else + return Settings::models().mXbaseanim1st.get().value(); + } + } + + bool isDefaultActorSkeleton(std::string_view model) + { + return VFS::Path::pathEqual(Settings::models().mBaseanimkna.get(), model) + || VFS::Path::pathEqual(Settings::models().mBaseanimfemale.get(), model) + || VFS::Path::pathEqual(Settings::models().mBaseanim.get(), model); + } + + std::string addSuffixBeforeExtension(const std::string& filename, const std::string& suffix) + { + size_t dotPos = filename.rfind('.'); + + // No extension found; return the original filename with suffix appended + if (dotPos == std::string::npos) + return filename + suffix; + + // Insert the suffix before the dot (extension) and return the new filename + return filename.substr(0, dotPos) + suffix + filename.substr(dotPos); + } +} diff --git a/apps/openmw/mwrender/actorutil.hpp b/apps/openmw/mwrender/actorutil.hpp new file mode 100644 index 00000000000..6a5ab12dea1 --- /dev/null +++ b/apps/openmw/mwrender/actorutil.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_APPS_OPENMW_MWRENDER_ACTORUTIL_H +#define OPENMW_APPS_OPENMW_MWRENDER_ACTORUTIL_H + +#include +#include + +namespace MWRender +{ + const std::string& getActorSkeleton(bool firstPerson, bool female, bool beast, bool werewolf); + bool isDefaultActorSkeleton(std::string_view model); + std::string addSuffixBeforeExtension(const std::string& filename, const std::string& suffix); +} + +#endif diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index b578ee25ba3..5b7d1db78ae 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1,54 +1,85 @@ #include "animation.hpp" +#include #include #include -#include #include +#include #include -#include +#include #include -#include #include +#include + +#include +#include #include -#include +#include #include +#include +#include +#include +#include +#include +#include +#include #include +#include #include +#include #include #include +#include -#include -#include -#include #include #include -#include #include +#include +#include #include +#include -#include +#include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/world.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/character.hpp" // FIXME: for MWMechanics::Priority +#include "../mwmechanics/weapontype.hpp" -#include "vismask.hpp" -#include "util.hpp" +#include "actorutil.hpp" #include "rotatecontroller.hpp" +#include "util.hpp" +#include "vismask.hpp" namespace { + class MarkDrawablesVisitor : public osg::NodeVisitor + { + public: + MarkDrawablesVisitor(osg::Node::NodeMask mask) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mMask(mask) + { + } + + void apply(osg::Drawable& drawable) override { drawable.setNodeMask(mMask); } + + private: + osg::Node::NodeMask mMask = 0; + }; /// Removes all particle systems and related nodes in a subgraph. class RemoveParticlesVisitor : public osg::NodeVisitor @@ -56,9 +87,10 @@ namespace public: RemoveParticlesVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - { } + { + } - void apply(osg::Node &node) override + void apply(osg::Node& node) override { if (dynamic_cast(&node)) mToRemove.emplace_back(&node); @@ -84,25 +116,26 @@ namespace } private: - std::vector > mToRemove; + std::vector> mToRemove; }; - class DayNightCallback : public osg::NodeCallback + class DayNightCallback : public SceneUtil::NodeCallback { public: - DayNightCallback() : mCurrentState(0) + DayNightCallback() + : mCurrentState(0) { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Switch* node, osg::NodeVisitor* nv) { unsigned int state = MWBase::Environment::get().getWorld()->getNightDayMode(); - const unsigned int newState = node->asGroup()->getNumChildren() > state ? state : 0; + const unsigned int newState = node->getNumChildren() > state ? state : 0; if (newState != mCurrentState) { mCurrentState = newState; - node->asSwitch()->setSingleChildOn(mCurrentState); + node->setSingleChildOn(mCurrentState); } traverse(node, nv); @@ -117,9 +150,10 @@ namespace public: AddSwitchCallbacksVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - { } + { + } - void apply(osg::Switch &switchNode) override + void apply(osg::Switch& switchNode) override { if (switchNode.getName() == Constants::NightDayLabel) switchNode.addUpdateCallback(new DayNightCallback()); @@ -147,13 +181,20 @@ namespace } }; - float calcAnimVelocity(const SceneUtil::TextKeyMap& keys, SceneUtil::KeyframeController *nonaccumctrl, - const osg::Vec3f& accum, const std::string &groupname) + bool equalsParts(std::string_view value, std::string_view s1, std::string_view s2, std::string_view s3 = {}) + { + if (value.starts_with(s1)) + { + value = value.substr(s1.size()); + if (value.starts_with(s2)) + return value.substr(s2.size()) == s3; + } + return false; + } + + float calcAnimVelocity(const SceneUtil::TextKeyMap& keys, SceneUtil::KeyframeController* nonaccumctrl, + const osg::Vec3f& accum, std::string_view groupname) { - const std::string start = groupname+": start"; - const std::string loopstart = groupname+": loop start"; - const std::string loopstop = groupname+": loop stop"; - const std::string stop = groupname+": stop"; float starttime = std::numeric_limits::max(); float stoptime = 0.0f; @@ -164,9 +205,10 @@ namespace // As result the animation velocity calculation is not correct, and this incorrect velocity must be replicated, // because otherwise the Creature's Speed (dagoth uthol) would not be sufficient to move fast enough. auto keyiter = keys.rbegin(); - while(keyiter != keys.rend()) + while (keyiter != keys.rend()) { - if(keyiter->second == start || keyiter->second == loopstart) + if (equalsParts(keyiter->second, groupname, ": start") + || equalsParts(keyiter->second, groupname, ": loop start")) { starttime = keyiter->first; break; @@ -174,11 +216,11 @@ namespace ++keyiter; } keyiter = keys.rbegin(); - while(keyiter != keys.rend()) + while (keyiter != keys.rend()) { - if (keyiter->second == stop) + if (equalsParts(keyiter->second, groupname, ": stop")) stoptime = keyiter->first; - else if (keyiter->second == loopstop) + else if (equalsParts(keyiter->second, groupname, ": loop stop")) { stoptime = keyiter->first; break; @@ -186,43 +228,17 @@ namespace ++keyiter; } - if(stoptime > starttime) + if (stoptime > starttime) { osg::Vec3f startpos = osg::componentMultiply(nonaccumctrl->getTranslation(starttime), accum); osg::Vec3f endpos = osg::componentMultiply(nonaccumctrl->getTranslation(stoptime), accum); - return (startpos-endpos).length() / (stoptime - starttime); + return (startpos - endpos).length() / (stoptime - starttime); } return 0.0f; } - /// @brief Base class for visitors that remove nodes from a scene graph. - /// Subclasses need to fill the mToRemove vector. - /// To use, node->accept(removeVisitor); removeVisitor.remove(); - class RemoveVisitor : public osg::NodeVisitor - { - public: - RemoveVisitor() - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - { - } - - void remove() - { - for (RemoveVec::iterator it = mToRemove.begin(); it != mToRemove.end(); ++it) - { - if (!it->second->removeChild(it->first)) - Log(Debug::Error) << "Error removing " << it->first->getName(); - } - } - - protected: - // - typedef std::vector > RemoveVec; - std::vector > mToRemove; - }; - class GetExtendedBonesVisitor : public osg::NodeVisitor { public: @@ -242,10 +258,10 @@ namespace traverse(node); } - std::vector > mFoundBones; + std::vector> mFoundBones; }; - class RemoveFinishedCallbackVisitor : public RemoveVisitor + class RemoveFinishedCallbackVisitor : public SceneUtil::RemoveVisitor { public: bool mHasMagicEffects; @@ -256,12 +272,9 @@ namespace { } - void apply(osg::Node &node) override - { - traverse(node); - } + void apply(osg::Node& node) override { traverse(node); } - void apply(osg::Group &group) override + void apply(osg::Group& group) override { traverse(group); @@ -280,17 +293,12 @@ namespace } } - void apply(osg::MatrixTransform &node) override - { - traverse(node); - } + void apply(osg::MatrixTransform& node) override { traverse(node); } - void apply(osg::Geometry&) override - { - } + void apply(osg::Geometry&) override {} }; - class RemoveCallbackVisitor : public RemoveVisitor + class RemoveCallbackVisitor : public SceneUtil::RemoveVisitor { public: bool mHasMagicEffects; @@ -298,23 +306,19 @@ namespace RemoveCallbackVisitor() : RemoveVisitor() , mHasMagicEffects(false) - , mEffectId(-1) { } - RemoveCallbackVisitor(int effectId) + RemoveCallbackVisitor(std::string_view effectId) : RemoveVisitor() , mHasMagicEffects(false) , mEffectId(effectId) { } - void apply(osg::Node &node) override - { - traverse(node); - } + void apply(osg::Node& node) override { traverse(node); } - void apply(osg::Group &group) override + void apply(osg::Group& group) override { traverse(group); @@ -324,7 +328,7 @@ namespace MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { - bool toRemove = mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId; + bool toRemove = mEffectId == "" || vfxCallback->mParams.mEffectId == mEffectId; if (toRemove) mToRemove.emplace_back(group.asNode(), group.getParent(0)); else @@ -333,43 +337,33 @@ namespace } } - void apply(osg::MatrixTransform &node) override - { - traverse(node); - } + void apply(osg::MatrixTransform& node) override { traverse(node); } - void apply(osg::Geometry&) override - { - } + void apply(osg::Geometry&) override {} private: - int mEffectId; + std::string_view mEffectId; }; class FindVfxCallbacksVisitor : public osg::NodeVisitor { public: - std::vector mCallbacks; FindVfxCallbacksVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mEffectId(-1) { } - FindVfxCallbacksVisitor(int effectId) + FindVfxCallbacksVisitor(std::string_view effectId) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mEffectId(effectId) { } - void apply(osg::Node &node) override - { - traverse(node); - } + void apply(osg::Node& node) override { traverse(node); } - void apply(osg::Group &group) override + void apply(osg::Group& group) override { osg::Callback* callback = group.getUpdateCallback(); if (callback) @@ -377,7 +371,7 @@ namespace MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { - if (mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId) + if (mEffectId == "" || vfxCallback->mParams.mEffectId == mEffectId) { mCallbacks.push_back(vfxCallback); } @@ -386,102 +380,80 @@ namespace traverse(group); } - void apply(osg::MatrixTransform &node) override - { - traverse(node); - } + void apply(osg::MatrixTransform& node) override { traverse(node); } - void apply(osg::Geometry&) override - { - } + void apply(osg::Geometry&) override {} private: - int mEffectId; + std::string_view mEffectId; }; - // Removes all drawables from a graph. - class CleanObjectRootVisitor : public RemoveVisitor + osg::ref_ptr getVFXLightModelInstance() { - public: - void apply(osg::Drawable& drw) override - { - applyDrawable(drw); - } + static osg::ref_ptr lightModel = nullptr; - void apply(osg::Group& node) override - { - applyNode(node); - } - void apply(osg::MatrixTransform& node) override - { - applyNode(node); - } - void apply(osg::Node& node) override + if (!lightModel) { - applyNode(node); + lightModel = new osg::LightModel; + lightModel->setAmbientIntensity({ 1, 1, 1, 1 }); } - void applyNode(osg::Node& node) - { - if (node.getStateSet()) - node.setStateSet(nullptr); + return lightModel; + } - if (node.getNodeMask() == 0x1 && node.getNumParents() == 1) - mToRemove.emplace_back(&node, node.getParent(0)); - else - traverse(node); - } - void applyDrawable(osg::Node& node) + void assignBoneBlendCallbackRecursive(MWRender::BoneAnimBlendController* controller, osg::Node* parent, bool isRoot) + { + // Attempt to cast node to an osgAnimation::Bone + if (!isRoot && dynamic_cast(parent)) { - osg::NodePath::iterator parent = getNodePath().end()-2; - // We know that the parent is a Group because only Groups can have children. - osg::Group* parentGroup = static_cast(*parent); + // Wrapping in a custom callback object allows for nested callback chaining, otherwise it has link to self + // issues we need to share the base BoneAnimBlendController as that contains blending information and is + // guaranteed to update before + osgAnimation::Bone* bone = static_cast(parent); + osg::ref_ptr cb = new MWRender::BoneAnimBlendControllerWrapper(controller, bone); - // Try to prune nodes that would be empty after the removal - if (parent != getNodePath().begin()) + // Ensure there is no other AnimBlendController - this can happen when using + // multiple animations with different roots, such as NPC animation + osg::Callback* updateCb = bone->getUpdateCallback(); + while (updateCb) { - // This could be extended to remove the parent's parent, and so on if they are empty as well. - // But for NIF files, there won't be a benefit since only TriShapes can be set to STATIC dataVariance. - osg::Group* parentParent = static_cast(*(parent - 1)); - if (parentGroup->getNumChildren() == 1 && parentGroup->getDataVariance() == osg::Object::STATIC) + if (dynamic_cast(updateCb)) + { + osg::ref_ptr nextCb = updateCb->getNestedCallback(); + bone->removeUpdateCallback(updateCb); + updateCb = nextCb; + } + else { - mToRemove.emplace_back(parentGroup, parentParent); - return; + updateCb = updateCb->getNestedCallback(); } } - mToRemove.emplace_back(&node, parentGroup); - } - }; - - class RemoveTriBipVisitor : public RemoveVisitor - { - public: - void apply(osg::Drawable& drw) override - { - applyImpl(drw); - } - - void apply(osg::Group& node) override - { - traverse(node); - } - void apply(osg::MatrixTransform& node) override - { - traverse(node); - } - - void applyImpl(osg::Node& node) - { - const std::string toFind = "tri bip"; - if (Misc::StringUtils::ciCompareLen(node.getName(), toFind, toFind.size()) == 0) + // Find UpdateBone callback and bind to just after that (order is important) + // NOTE: if it doesn't have an UpdateBone callback, we shouldn't be doing blending! + updateCb = bone->getUpdateCallback(); + while (updateCb) { - osg::Group* parent = static_cast(*(getNodePath().end()-2)); - // Not safe to remove in apply(), since the visitor is still iterating the child list - mToRemove.emplace_back(&node, parent); + if (dynamic_cast(updateCb)) + { + // Override the immediate callback after the UpdateBone + osg::ref_ptr lastCb = updateCb->getNestedCallback(); + updateCb->setNestedCallback(cb); + if (lastCb) + cb->setNestedCallback(lastCb); + break; + } + + updateCb = updateCb->getNestedCallback(); } } - }; + + // Traverse child bones if this is a group + osg::Group* group = parent->asGroup(); + if (group) + for (unsigned int i = 0; i < group->getNumChildren(); ++i) + assignBoneBlendCallbackRecursive(controller, group->getChild(i), false); + } } namespace MWRender @@ -494,21 +466,13 @@ namespace MWRender { } - void setAlpha(const float alpha) - { - mAlpha = alpha; - } - - void setLightSource(const osg::ref_ptr& lightSource) - { - mLightSource = lightSource; - } + void setAlpha(const float alpha) { mAlpha = alpha; } protected: void setDefaults(osg::StateSet* stateset) override { - osg::BlendFunc* blendfunc (new osg::BlendFunc); - stateset->setAttributeAndModes(blendfunc, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + osg::BlendFunc* blendfunc(new osg::BlendFunc); + stateset->setAttributeAndModes(blendfunc, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); stateset->setRenderBinMode(osg::StateSet::OVERRIDE_RENDERBIN_DETAILS); @@ -516,34 +480,35 @@ namespace MWRender // FIXME: overriding diffuse/ambient/emissive colors osg::Material* material = new osg::Material; material->setColorMode(osg::Material::OFF); - material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,mAlpha)); - material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - stateset->setAttributeAndModes(material, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->addUniform(new osg::Uniform("colorMode", 0), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, mAlpha)); + material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, 1)); + stateset->setAttributeAndModes(material, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + stateset->addUniform( + new osg::Uniform("colorMode", 0), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); } void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override { - osg::Material* material = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + osg::Material* material + = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha); - if (mLightSource) - mLightSource->setActorFade(mAlpha); } private: float mAlpha; - osg::ref_ptr mLightSource; }; struct Animation::AnimSource { osg::ref_ptr mKeyframes; - typedef std::map > ControllerMap; + typedef std::map> ControllerMap; - ControllerMap mControllerMap[Animation::sNumBlendMasks]; + ControllerMap mControllerMap[sNumBlendMasks]; const SceneUtil::TextKeyMap& getTextKeys() const; + + osg::ref_ptr mAnimBlendRules; }; void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) @@ -582,20 +547,18 @@ namespace MWRender } } - class ResetAccumRootCallback : public osg::NodeCallback + class ResetAccumRootCallback : public SceneUtil::NodeCallback { public: - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::MatrixTransform* transform, osg::NodeVisitor* nv) { - osg::MatrixTransform* transform = static_cast(node); - osg::Matrix mat = transform->getMatrix(); osg::Vec3f position = mat.getTrans(); position = osg::componentMultiply(mResetAxes, position); mat.setTrans(position); transform->setMatrix(mat); - traverse(node, nv); + traverse(transform, nv); } void setAccumulate(const osg::Vec3f& accumulate) @@ -610,8 +573,9 @@ namespace MWRender osg::Vec3f mResetAxes; }; - Animation::Animation(const MWWorld::Ptr &ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem) - : mInsert(parentNode) + Animation::Animation( + const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem) + : mInsert(std::move(parentNode)) , mSkeleton(nullptr) , mNodeMapCreated(false) , mPtr(ptr) @@ -625,29 +589,18 @@ namespace MWRender , mBodyPitchRadians(0.f) , mHasMagicEffects(false) , mAlpha(1.f) + , mPlayScriptedOnly(false) + , mRequiresBoneMap(false) { - for(size_t i = 0;i < sNumBlendMasks;i++) - mAnimationTimePtr[i].reset(new AnimationTime); + for (size_t i = 0; i < sNumBlendMasks; i++) + mAnimationTimePtr[i] = std::make_shared(); mLightListCallback = new SceneUtil::LightListCallback; } Animation::~Animation() { - Animation::setLightEffect(0.f); - - if (mObjectRoot) - mInsert->removeChild(mObjectRoot); - } - - MWWorld::ConstPtr Animation::getPtr() const - { - return mPtr; - } - - MWWorld::Ptr Animation::getPtr() - { - return mPtr; + removeFromSceneImpl(); } void Animation::setActive(int active) @@ -656,7 +609,7 @@ namespace MWRender mSkeleton->setActive(static_cast(active)); } - void Animation::updatePtr(const MWWorld::Ptr &ptr) + void Animation::updatePtr(const MWWorld::Ptr& ptr) { mPtr = ptr; } @@ -669,21 +622,22 @@ namespace MWRender mResetAccumRootCallback->setAccumulate(mAccumulate); } - size_t Animation::detectBlendMask(const osg::Node* node) const + // controllerName is used for Collada animated deforming models + size_t Animation::detectBlendMask(const osg::Node* node, const std::string& controllerName) const { - static const char sBlendMaskRoots[sNumBlendMasks][32] = { + static const std::string_view sBlendMaskRoots[sNumBlendMasks] = { "", /* Lower body / character root */ "Bip01 Spine1", /* Torso */ "Bip01 L Clavicle", /* Left arm */ "Bip01 R Clavicle", /* Right arm */ }; - while(node != mObjectRoot) + while (node != mObjectRoot) { - const std::string &name = node->getName(); - for(size_t i = 1;i < sNumBlendMasks;i++) + const std::string& name = node->getName(); + for (size_t i = 1; i < sNumBlendMasks; i++) { - if(name == sBlendMaskRoots[i]) + if (name == sBlendMaskRoots[i] || controllerName == sBlendMaskRoots[i]) return i; } @@ -695,86 +649,76 @@ namespace MWRender return 0; } - const SceneUtil::TextKeyMap &Animation::AnimSource::getTextKeys() const + const SceneUtil::TextKeyMap& Animation::AnimSource::getTextKeys() const { return mKeyframes->mTextKeys; } - void Animation::loadAllAnimationsInFolder(const std::string &model, const std::string &baseModel) + void Animation::loadAllAnimationsInFolder(const std::string& model, const std::string& baseModel) { - const std::map& index = mResourceSystem->getVFS()->getIndex(); - std::string animationPath = model; if (animationPath.find("meshes") == 0) { animationPath.replace(0, 6, "animations"); } - animationPath.replace(animationPath.size()-3, 3, "/"); - - mResourceSystem->getVFS()->normalizeFilename(animationPath); + animationPath.replace(animationPath.size() - 3, 3, "/"); - std::map::const_iterator found = index.lower_bound(animationPath); - while (found != index.end()) + for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { - const std::string& name = found->first; - if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) + if (Misc::getFileExtension(name) == "kf") { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".kf") == 0) - addSingleAnimSource(name, baseModel); + addSingleAnimSource(name, baseModel); } - else - break; - ++found; } } - void Animation::addAnimSource(const std::string &model, const std::string& baseModel) + void Animation::addAnimSource(std::string_view model, const std::string& baseModel) { - std::string kfname = model; - Misc::StringUtils::lowerCaseInPlace(kfname); + std::string kfname = Misc::StringUtils::lowerCase(model); - if(kfname.size() > 4 && kfname.compare(kfname.size()-4, 4, ".nif") == 0) - kfname.replace(kfname.size()-4, 4, ".kf"); + if (kfname.ends_with(".nif")) + kfname.replace(kfname.size() - 4, 4, ".kf"); addSingleAnimSource(kfname, baseModel); - static const bool useAdditionalSources = Settings::Manager::getBool ("use additional anim sources", "Game"); - if (useAdditionalSources) + if (Settings::game().mUseAdditionalAnimSources) loadAllAnimationsInFolder(kfname, baseModel); } - void Animation::addSingleAnimSource(const std::string &kfname, const std::string& baseModel) + std::shared_ptr Animation::addSingleAnimSource( + const std::string& kfname, const std::string& baseModel) { - if(!mResourceSystem->getVFS()->exists(kfname)) - return; + if (!mResourceSystem->getVFS()->exists(kfname)) + return nullptr; - std::shared_ptr animsrc; - animsrc.reset(new AnimSource); + auto animsrc = std::make_shared(); animsrc->mKeyframes = mResourceSystem->getKeyframeManager()->get(kfname); - if (!animsrc->mKeyframes || animsrc->mKeyframes->mTextKeys.empty() || animsrc->mKeyframes->mKeyframeControllers.empty()) - return; + if (!animsrc->mKeyframes || animsrc->mKeyframes->mTextKeys.empty() + || animsrc->mKeyframes->mKeyframeControllers.empty()) + return nullptr; const NodeMap& nodeMap = getNodeMap(); - - for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = animsrc->mKeyframes->mKeyframeControllers.begin(); - it != animsrc->mKeyframes->mKeyframeControllers.end(); ++it) + const auto& controllerMap = animsrc->mKeyframes->mKeyframeControllers; + for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = controllerMap.begin(); + it != controllerMap.end(); ++it) { std::string bonename = Misc::StringUtils::lowerCase(it->first); NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) { - Log(Debug::Warning) << "Warning: addAnimSource: can't find bone '" + bonename << "' in " << baseModel << " (referenced by " << kfname << ")"; + Log(Debug::Warning) << "Warning: addAnimSource: can't find bone '" + bonename << "' in " << baseModel + << " (referenced by " << kfname << ")"; continue; } osg::Node* node = found->second; - size_t blendMask = detectBlendMask(node); + size_t blendMask = detectBlendMask(node, it->second->getName()); // clone the controller, because each Animation needs its own ControllerSource - osg::ref_ptr cloned = osg::clone(it->second.get(), osg::CopyOp::SHALLOW_COPY); + osg::ref_ptr cloned + = osg::clone(it->second.get(), osg::CopyOp::SHALLOW_COPY); cloned->setSource(mAnimationTimePtr[blendMask]); animsrc->mControllerMap[blendMask].insert(std::make_pair(bonename, cloned)); @@ -782,69 +726,146 @@ namespace MWRender mAnimSources.push_back(animsrc); + for (const std::string& group : mAnimSources.back()->getTextKeys().getGroups()) + mSupportedAnimations.insert(group); + SceneUtil::AssignControllerSourcesVisitor assignVisitor(mAnimationTimePtr[0]); mObjectRoot->accept(assignVisitor); + // Determine the movement accumulation bone if necessary if (!mAccumRoot) { - NodeMap::const_iterator found = nodeMap.find("bip01"); - if (found == nodeMap.end()) - found = nodeMap.find("root bone"); + // Priority matters! bip01 is preferred. + static const std::initializer_list accumRootNames = { "bip01", "root bone" }; + NodeMap::const_iterator found = nodeMap.end(); + for (const std::string_view& name : accumRootNames) + { + found = nodeMap.find(name); + if (found == nodeMap.end()) + continue; + for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = controllerMap.begin(); + it != controllerMap.end(); ++it) + { + if (Misc::StringUtils::ciEqual(it->first, name)) + { + mAccumRoot = found->second; + break; + } + } + if (mAccumRoot) + break; + } + } - if (found != nodeMap.end()) - mAccumRoot = found->second; + // Get the blending rules + if (Settings::game().mSmoothAnimTransitions) + { + // Note, even if the actual config is .json - we should send a .yaml path to AnimBlendRulesManager, the + // manager will check for .json if it will not find a specified .yaml file. + VFS::Path::Normalized blendConfigPath(kfname); + blendConfigPath.changeExtension("yaml"); + + // globalBlendConfigPath is only used with actors! Objects have no default blending. + constexpr VFS::Path::NormalizedView globalBlendConfigPath("animations/animation-config.yaml"); + + osg::ref_ptr blendRules; + if (mPtr.getClass().isActor()) + { + blendRules + = mResourceSystem->getAnimBlendRulesManager()->getRules(globalBlendConfigPath, blendConfigPath); + if (blendRules == nullptr) + Log(Debug::Warning) << "Unable to find animation blending rules: '" << blendConfigPath << "' or '" + << globalBlendConfigPath << "'"; + } + else + { + blendRules = mResourceSystem->getAnimBlendRulesManager()->getRules(blendConfigPath, blendConfigPath); + } + + // At this point blendRules will either be nullptr or an AnimBlendRules instance with > 0 rules inside. + animsrc->mAnimBlendRules = blendRules; } + + return animsrc; } void Animation::clearAnimSources() { mStates.clear(); - for(size_t i = 0;i < sNumBlendMasks;i++) + for (size_t i = 0; i < sNumBlendMasks; i++) mAnimationTimePtr[i]->setTimePtr(std::shared_ptr()); mAccumCtrl = nullptr; + mSupportedAnimations.clear(); mAnimSources.clear(); mAnimVelocities.clear(); } - bool Animation::hasAnimation(const std::string &anim) const + bool Animation::hasAnimation(std::string_view anim) const + { + return mSupportedAnimations.find(anim) != mSupportedAnimations.end(); + } + + bool Animation::isLoopingAnimation(std::string_view group) const { - AnimSourceList::const_iterator iter(mAnimSources.begin()); - for(;iter != mAnimSources.end();++iter) + // In Morrowind, a some animation groups are always considered looping, regardless + // of loop start/stop keys. + // To be match vanilla behavior we probably only need to check this list, but we don't + // want to prevent modded animations with custom group names from looping either. + static const std::unordered_set loopingAnimations = { "walkforward", "walkback", "walkleft", + "walkright", "swimwalkforward", "swimwalkback", "swimwalkleft", "swimwalkright", "runforward", "runback", + "runleft", "runright", "swimrunforward", "swimrunback", "swimrunleft", "swimrunright", "sneakforward", + "sneakback", "sneakleft", "sneakright", "turnleft", "turnright", "swimturnleft", "swimturnright", + "spellturnleft", "spellturnright", "torch", "idle", "idle2", "idle3", "idle4", "idle5", "idle6", "idle7", + "idle8", "idle9", "idlesneak", "idlestorm", "idleswim", "jump", "inventoryhandtohand", + "inventoryweapononehand", "inventoryweapontwohand", "inventoryweapontwowide" }; + static const std::vector shortGroups = MWMechanics::getAllWeaponTypeShortGroups(); + + if (getTextKeyTime(std::string(group) + ": loop start") >= 0) + return true; + + // Most looping animations have variants for each weapon type shortgroup. + // Just remove the shortgroup instead of enumerating all of the possible animation groupnames. + // Make sure we pick the longest shortgroup so e.g. "bow" doesn't get picked over "crossbow" + // when the shortgroup is crossbow. + std::size_t suffixLength = 0; + for (std::string_view suffix : shortGroups) { - const SceneUtil::TextKeyMap &keys = (*iter)->getTextKeys(); - if (keys.hasGroupStart(anim)) - return true; + if (suffix.length() > suffixLength && group.ends_with(suffix)) + { + suffixLength = suffix.length(); + } } + group.remove_suffix(suffixLength); - return false; + return loopingAnimations.count(group) > 0; } - float Animation::getStartTime(const std::string &groupname) const + float Animation::getStartTime(const std::string& groupname) const { - for(AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) + for (AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) { - const SceneUtil::TextKeyMap &keys = (*iter)->getTextKeys(); + const SceneUtil::TextKeyMap& keys = (*iter)->getTextKeys(); const auto found = keys.findGroupStart(groupname); - if(found != keys.end()) + if (found != keys.end()) return found->first; } return -1.f; } - float Animation::getTextKeyTime(const std::string &textKey) const + float Animation::getTextKeyTime(std::string_view textKey) const { - for(AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) + for (AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) { - const SceneUtil::TextKeyMap &keys = (*iter)->getTextKeys(); + const SceneUtil::TextKeyMap& keys = (*iter)->getTextKeys(); - for(auto iterKey = keys.begin(); iterKey != keys.end(); ++iterKey) + for (auto iterKey = keys.begin(); iterKey != keys.end(); ++iterKey) { - if(iterKey->second.compare(0, textKey.size(), textKey) == 0) + if (iterKey->second.starts_with(textKey)) return iterKey->first; } } @@ -852,61 +873,61 @@ namespace MWRender return -1.f; } - void Animation::handleTextKey(AnimState &state, const std::string &groupname, SceneUtil::TextKeyMap::ConstIterator key, - const SceneUtil::TextKeyMap& map) + void Animation::handleTextKey(AnimState& state, std::string_view groupname, + SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) { - const std::string &evt = key->second; - - size_t off = groupname.size()+2; - size_t len = evt.size() - off; + std::string_view evt = key->second; - if(evt.compare(0, groupname.size(), groupname) == 0 && - evt.compare(groupname.size(), 2, ": ") == 0) + if (evt.starts_with(groupname) && evt.substr(groupname.size()).starts_with(": ")) { - if(evt.compare(off, len, "loop start") == 0) + size_t off = groupname.size() + 2; + if (evt.substr(off) == "loop start") state.mLoopStartTime = key->first; - else if(evt.compare(off, len, "loop stop") == 0) + else if (evt.substr(off) == "loop stop") state.mLoopStopTime = key->first; } - if (mTextKeyListener) + try { - try - { + if (mTextKeyListener != nullptr) mTextKeyListener->handleTextKey(groupname, key, map); - } - catch (std::exception& e) - { - Log(Debug::Error) << "Error handling text key " << evt << ": " << e.what(); - } + } + catch (std::exception& e) + { + Log(Debug::Error) << "Error handling text key " << evt << ": " << e.what(); } } - void Animation::play(const std::string &groupname, const AnimPriority& priority, int blendMask, bool autodisable, float speedmult, - const std::string &start, const std::string &stop, float startpoint, size_t loops, bool loopfallback) + void Animation::play(std::string_view groupname, const AnimPriority& priority, int blendMask, bool autodisable, + float speedmult, std::string_view start, std::string_view stop, float startpoint, uint32_t loops, + bool loopfallback) { - if(!mObjectRoot || mAnimSources.empty()) + if (!mObjectRoot || mAnimSources.empty()) return; - if(groupname.empty()) + if (groupname.empty()) { resetActiveGroups(); return; } + AnimStateMap::iterator foundstateiter = mStates.find(groupname); + if (foundstateiter != mStates.end()) + { + foundstateiter->second.mPriority = priority; + } + AnimStateMap::iterator stateiter = mStates.begin(); - while(stateiter != mStates.end()) + while (stateiter != mStates.end()) { - if(stateiter->second.mPriority == priority) + if (stateiter->second.mPriority == priority && stateiter->first != groupname) mStates.erase(stateiter++); else ++stateiter; } - stateiter = mStates.find(groupname); - if(stateiter != mStates.end()) + if (foundstateiter != mStates.end()) { - stateiter->second.mPriority = priority; resetActiveGroups(); return; } @@ -914,10 +935,10 @@ namespace MWRender /* Look in reverse; last-inserted source has priority. */ AnimState state; AnimSourceList::reverse_iterator iter(mAnimSources.rbegin()); - for(;iter != mAnimSources.rend();++iter) + for (; iter != mAnimSources.rend(); ++iter) { - const SceneUtil::TextKeyMap &textkeys = (*iter)->getTextKeys(); - if(reset(state, textkeys, groupname, start, stop, startpoint, loopfallback)) + const SceneUtil::TextKeyMap& textkeys = (*iter)->getTextKeys(); + if (reset(state, textkeys, groupname, start, stop, startpoint, loopfallback)) { state.mSource = *iter; state.mSpeedMult = speedmult; @@ -926,28 +947,30 @@ namespace MWRender state.mPriority = priority; state.mBlendMask = blendMask; state.mAutoDisable = autodisable; - mStates[groupname] = state; + state.mGroupname = groupname; + state.mStartKey = start; + mStates[std::string{ groupname }] = state; if (state.mPlaying) { auto textkey = textkeys.lowerBound(state.getTime()); - while(textkey != textkeys.end() && textkey->first <= state.getTime()) + while (textkey != textkeys.end() && textkey->first <= state.getTime()) { handleTextKey(state, groupname, textkey, textkeys); ++textkey; } } - if(state.getTime() >= state.mLoopStopTime && state.mLoopCount > 0) + if (state.getTime() >= state.mLoopStopTime && state.mLoopCount > 0) { state.mLoopCount--; state.setTime(state.mLoopStartTime); state.mPlaying = true; - if(state.getTime() >= state.mLoopStopTime) + if (state.getTime() >= state.mLoopStopTime) break; auto textkey = textkeys.lowerBound(state.getTime()); - while(textkey != textkeys.end() && textkey->first <= state.getTime()) + while (textkey != textkeys.end() && textkey->first <= state.getTime()) { handleTextKey(state, groupname, textkey, textkeys); ++textkey; @@ -961,44 +984,42 @@ namespace MWRender resetActiveGroups(); } - bool Animation::reset(AnimState &state, const SceneUtil::TextKeyMap &keys, const std::string &groupname, const std::string &start, const std::string &stop, float startpoint, bool loopfallback) + bool Animation::reset(AnimState& state, const SceneUtil::TextKeyMap& keys, std::string_view groupname, + std::string_view start, std::string_view stop, float startpoint, bool loopfallback) { // Look for text keys in reverse. This normally wouldn't matter, but for some reason undeadwolf_2.nif has two // separate walkforward keys, and the last one is supposed to be used. auto groupend = keys.rbegin(); - for(;groupend != keys.rend();++groupend) + for (; groupend != keys.rend(); ++groupend) { - if(groupend->second.compare(0, groupname.size(), groupname) == 0 && - groupend->second.compare(groupname.size(), 2, ": ") == 0) + if (groupend->second.starts_with(groupname) && groupend->second.compare(groupname.size(), 2, ": ") == 0) break; } - std::string starttag = groupname+": "+start; auto startkey = groupend; - while(startkey != keys.rend() && startkey->second != starttag) + while (startkey != keys.rend() && !equalsParts(startkey->second, groupname, ": ", start)) ++startkey; - if(startkey == keys.rend() && start == "loop start") + if (startkey == keys.rend() && start == "loop start") { - starttag = groupname+": start"; startkey = groupend; - while(startkey != keys.rend() && startkey->second != starttag) + while (startkey != keys.rend() && !equalsParts(startkey->second, groupname, ": start")) ++startkey; } - if(startkey == keys.rend()) + if (startkey == keys.rend()) return false; - const std::string stoptag = groupname+": "+stop; auto stopkey = groupend; - while(stopkey != keys.rend() - // We have to ignore extra garbage at the end. - // The Scrib's idle3 animation has "Idle3: Stop." instead of "Idle3: Stop". - // Why, just why? :( - && (stopkey->second.size() < stoptag.size() || stopkey->second.compare(0,stoptag.size(), stoptag) != 0)) + std::size_t checkLength = groupname.size() + 2 + stop.size(); + while (stopkey != keys.rend() + // We have to ignore extra garbage at the end. + // The Scrib's idle3 animation has "Idle3: Stop." instead of "Idle3: Stop". + // Why, just why? :( + && !equalsParts(std::string_view{ stopkey->second }.substr(0, checkLength), groupname, ": ", stop)) ++stopkey; - if(stopkey == keys.rend()) + if (stopkey == keys.rend()) return false; - if(startkey->first > stopkey->first) + if (startkey->first > stopkey->first) return false; state.mStartTime = startkey->first; @@ -1016,10 +1037,9 @@ namespace MWRender state.setTime(state.mStartTime + ((state.mStopTime - state.mStartTime) * startpoint)); - // mLoopStartTime and mLoopStopTime normally get assigned when encountering these keys while playing the animation - // (see handleTextKey). But if startpoint is already past these keys, or start time is == stop time, we need to assign them now. - const std::string loopstarttag = groupname+": loop start"; - const std::string loopstoptag = groupname+": loop stop"; + // mLoopStartTime and mLoopStopTime normally get assigned when encountering these keys while playing the + // animation (see handleTextKey). But if startpoint is already past these keys, or start time is == stop time, + // we need to assign them now. auto key = groupend; for (; key != startkey && key != keys.rend(); ++key) @@ -1027,31 +1047,82 @@ namespace MWRender if (key->first > state.getTime()) continue; - if (key->second == loopstarttag) + if (equalsParts(key->second, groupname, ": loop start")) state.mLoopStartTime = key->first; - else if (key->second == loopstoptag) + else if (equalsParts(key->second, groupname, ": loop stop")) state.mLoopStopTime = key->first; } return true; } - void Animation::setTextKeyListener(Animation::TextKeyListener *listener) + void Animation::setTextKeyListener(TextKeyListener* listener) { mTextKeyListener = listener; } - const Animation::NodeMap &Animation::getNodeMap() const + const Animation::NodeMap& Animation::getNodeMap() const { if (!mNodeMapCreated && mObjectRoot) { - SceneUtil::NodeMapVisitor visitor(mNodeMap); - mObjectRoot->accept(visitor); + // If the base of this animation is an osgAnimation, we should map the bones not matrix transforms + if (mRequiresBoneMap) + { + SceneUtil::NodeMapVisitorBoneOnly visitor(mNodeMap); + mObjectRoot->accept(visitor); + } + else + { + SceneUtil::NodeMapVisitor visitor(mNodeMap); + mObjectRoot->accept(visitor); + } mNodeMapCreated = true; } return mNodeMap; } + template + inline osg::Callback* Animation::handleBlendTransform(const osg::ref_ptr& node, + osg::ref_ptr keyframeController, + std::map, osg::ref_ptr>& blendControllers, + const AnimBlendStateData& stateData, const osg::ref_ptr& blendRules, + const AnimState& active) + { + osg::ref_ptr animController; + if (blendControllers.contains(node)) + { + animController = blendControllers.at(node); + animController->setKeyframeTrack(keyframeController, stateData, blendRules); + } + else + { + animController = new ControllerType(keyframeController, stateData, blendRules); + blendControllers.emplace(node, animController); + + if constexpr (std::is_same_v) + assignBoneBlendCallbackRecursive(animController, node, true); + } + + keyframeController->mTime = active.mTime; + + osg::Callback* asCallback = animController->getAsCallback(); + if constexpr (std::is_same_v) + { + // IMPORTANT: we must gather all transforms at point of change before next update + // instead of at the root update callback because the root bone may require blending. + if (animController->getBlendTrigger()) + animController->gatherRecursiveBoneTransforms(static_cast(node.get())); + + // Register blend callback after the initial animation callback + node->addUpdateCallback(asCallback); + mActiveControllers.emplace_back(node, asCallback); + + return keyframeController->getAsCallback(); + } + + return asCallback; + } + void Animation::resetActiveGroups() { // remove all previous external controllers from the scene graph @@ -1068,33 +1139,56 @@ namespace MWRender mAccumCtrl = nullptr; - for(size_t blendMask = 0;blendMask < sNumBlendMasks;blendMask++) + for (size_t blendMask = 0; blendMask < sNumBlendMasks; blendMask++) { AnimStateMap::const_iterator active = mStates.end(); AnimStateMap::const_iterator state = mStates.begin(); - for(;state != mStates.end();++state) + for (; state != mStates.end(); ++state) { - if(!(state->second.mBlendMask&(1<second.blendMaskContains(blendMask)) continue; - if(active == mStates.end() || active->second.mPriority[(BoneGroup)blendMask] < state->second.mPriority[(BoneGroup)blendMask]) + if (active == mStates.end() + || active->second.mPriority[(BoneGroup)blendMask] < state->second.mPriority[(BoneGroup)blendMask]) active = state; } - mAnimationTimePtr[blendMask]->setTimePtr(active == mStates.end() ? std::shared_ptr() : active->second.mTime); + mAnimationTimePtr[blendMask]->setTimePtr( + active == mStates.end() ? std::shared_ptr() : active->second.mTime); // add external controllers for the AnimSource active in this blend mask if (active != mStates.end()) { std::shared_ptr animsrc = active->second.mSource; + const AnimBlendStateData stateData + = { .mGroupname = active->second.mGroupname, .mStartKey = active->second.mStartKey }; - for (AnimSource::ControllerMap::iterator it = animsrc->mControllerMap[blendMask].begin(); it != animsrc->mControllerMap[blendMask].end(); ++it) + for (AnimSource::ControllerMap::iterator it = animsrc->mControllerMap[blendMask].begin(); + it != animsrc->mControllerMap[blendMask].end(); ++it) { - osg::ref_ptr node = getNodeMap().at(it->first); // this should not throw, we already checked for the node existing in addAnimSource + osg::ref_ptr node = getNodeMap().at( + it->first); // this should not throw, we already checked for the node existing in addAnimSource - node->addUpdateCallback(it->second); - mActiveControllers.emplace_back(node, it->second); + const bool useSmoothAnims = Settings::game().mSmoothAnimTransitions; + + osg::Callback* callback = it->second->getAsCallback(); + if (useSmoothAnims) + { + if (dynamic_cast(node.get())) + { + callback = handleBlendTransform(node, it->second, + mAnimBlendControllers, stateData, animsrc->mAnimBlendRules, active->second); + } + else if (dynamic_cast(node.get())) + { + callback = handleBlendTransform(node, it->second, + mBoneAnimBlendControllers, stateData, animsrc->mAnimBlendRules, active->second); + } + } + + node->addUpdateCallback(callback); + mActiveControllers.emplace_back(node, callback); if (blendMask == 0 && node == mAccumRoot) { @@ -1112,73 +1206,82 @@ namespace MWRender } } } + addControllers(); } - void Animation::adjustSpeedMult(const std::string &groupname, float speedmult) + void Animation::adjustSpeedMult(const std::string& groupname, float speedmult) { AnimStateMap::iterator state(mStates.find(groupname)); - if(state != mStates.end()) + if (state != mStates.end()) state->second.mSpeedMult = speedmult; } - bool Animation::isPlaying(const std::string &groupname) const + bool Animation::isPlaying(std::string_view groupname) const { AnimStateMap::const_iterator state(mStates.find(groupname)); - if(state != mStates.end()) + if (state != mStates.end()) return state->second.mPlaying; return false; } - bool Animation::getInfo(const std::string &groupname, float *complete, float *speedmult) const + bool Animation::getInfo(std::string_view groupname, float* complete, float* speedmult, size_t* loopcount) const { AnimStateMap::const_iterator iter = mStates.find(groupname); - if(iter == mStates.end()) - { - if(complete) *complete = 0.0f; - if(speedmult) *speedmult = 0.0f; + if (iter == mStates.end()) + { + if (complete) + *complete = 0.0f; + if (speedmult) + *speedmult = 0.0f; + if (loopcount) + *loopcount = 0; return false; } - if(complete) + if (complete) { - if(iter->second.mStopTime > iter->second.mStartTime) - *complete = (iter->second.getTime() - iter->second.mStartTime) / - (iter->second.mStopTime - iter->second.mStartTime); + if (iter->second.mStopTime > iter->second.mStartTime) + *complete = (iter->second.getTime() - iter->second.mStartTime) + / (iter->second.mStopTime - iter->second.mStartTime); else *complete = (iter->second.mPlaying ? 0.0f : 1.0f); } - if(speedmult) *speedmult = iter->second.mSpeedMult; + if (speedmult) + *speedmult = iter->second.mSpeedMult; + + if (loopcount) + *loopcount = iter->second.mLoopCount; return true; } - float Animation::getCurrentTime(const std::string &groupname) const + std::string_view Animation::getActiveGroup(BoneGroup boneGroup) const { - AnimStateMap::const_iterator iter = mStates.find(groupname); - if(iter == mStates.end()) - return -1.f; - - return iter->second.getTime(); + if (auto timePtr = mAnimationTimePtr[boneGroup]->getTimePtr()) + for (auto& state : mStates) + if (state.second.mTime == timePtr) + return state.first; + return ""; } - size_t Animation::getCurrentLoopCount(const std::string& groupname) const + float Animation::getCurrentTime(std::string_view groupname) const { AnimStateMap::const_iterator iter = mStates.find(groupname); - if(iter == mStates.end()) - return 0; + if (iter == mStates.end()) + return -1.f; - return iter->second.mLoopCount; + return iter->second.getTime(); } - void Animation::disable(const std::string &groupname) + void Animation::disable(std::string_view groupname) { AnimStateMap::iterator iter = mStates.find(groupname); - if(iter != mStates.end()) + if (iter != mStates.end()) mStates.erase(iter); resetActiveGroups(); } - float Animation::getVelocity(const std::string &groupname) const + float Animation::getVelocity(std::string_view groupname) const { if (!mAccumRoot) return 0.0f; @@ -1189,17 +1292,17 @@ namespace MWRender // Look in reverse; last-inserted source has priority. AnimSourceList::const_reverse_iterator animsrc(mAnimSources.rbegin()); - for(;animsrc != mAnimSources.rend();++animsrc) + for (; animsrc != mAnimSources.rend(); ++animsrc) { - const SceneUtil::TextKeyMap &keys = (*animsrc)->getTextKeys(); + const SceneUtil::TextKeyMap& keys = (*animsrc)->getTextKeys(); if (keys.hasGroupStart(groupname)) break; } - if(animsrc == mAnimSources.rend()) + if (animsrc == mAnimSources.rend()) return 0.0f; float velocity = 0.0f; - const SceneUtil::TextKeyMap &keys = (*animsrc)->getTextKeys(); + const SceneUtil::TextKeyMap& keys = (*animsrc)->getTextKeys(); const AnimSource::ControllerMap& ctrls = (*animsrc)->mControllerMap[0]; for (AnimSource::ControllerMap::const_iterator it = ctrls.begin(); it != ctrls.end(); ++it) @@ -1212,15 +1315,15 @@ namespace MWRender } // If there's no velocity, keep looking - if(!(velocity > 1.0f)) + if (!(velocity > 1.0f)) { AnimSourceList::const_reverse_iterator animiter = mAnimSources.rbegin(); - while(*animiter != *animsrc) + while (*animiter != *animsrc) ++animiter; - while(!(velocity > 1.0f) && ++animiter != mAnimSources.rend()) + while (!(velocity > 1.0f) && ++animiter != mAnimSources.rend()) { - const SceneUtil::TextKeyMap &keys2 = (*animiter)->getTextKeys(); + const SceneUtil::TextKeyMap& keys2 = (*animiter)->getTextKeys(); const AnimSource::ControllerMap& ctrls2 = (*animiter)->mControllerMap[0]; for (AnimSource::ControllerMap::const_iterator it = ctrls2.begin(); it != ctrls2.end(); ++it) @@ -1234,7 +1337,7 @@ namespace MWRender } } - mAnimVelocities.insert(std::make_pair(groupname, velocity)); + mAnimVelocities.emplace(groupname, velocity); return velocity; } @@ -1248,46 +1351,35 @@ namespace MWRender osg::Vec3f Animation::runAnimation(float duration) { - // If we have scripted animations, play only them - bool hasScriptedAnims = false; - for (AnimStateMap::iterator stateiter = mStates.begin(); stateiter != mStates.end(); stateiter++) - { - if (stateiter->second.mPriority.contains(int(MWMechanics::Priority_Persistent)) && stateiter->second.mPlaying) - { - hasScriptedAnims = true; - break; - } - } - osg::Vec3f movement(0.f, 0.f, 0.f); AnimStateMap::iterator stateiter = mStates.begin(); - while(stateiter != mStates.end()) + while (stateiter != mStates.end()) { - AnimState &state = stateiter->second; - if (hasScriptedAnims && !state.mPriority.contains(int(MWMechanics::Priority_Persistent))) + AnimState& state = stateiter->second; + if (mPlayScriptedOnly && !state.mPriority.contains(MWMechanics::Priority_Scripted)) { ++stateiter; continue; } - const SceneUtil::TextKeyMap &textkeys = state.mSource->getTextKeys(); + const SceneUtil::TextKeyMap& textkeys = state.mSource->getTextKeys(); auto textkey = textkeys.upperBound(state.getTime()); float timepassed = duration * state.mSpeedMult; - while(state.mPlaying) + while (state.mPlaying) { if (!state.shouldLoop()) { float targetTime = state.getTime() + timepassed; - if(textkey == textkeys.end() || textkey->first > targetTime) + if (textkey == textkeys.end() || textkey->first > targetTime) { - if(mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr()) + if (mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr()) updatePosition(state.getTime(), targetTime, movement); state.setTime(std::min(targetTime, state.mStopTime)); } else { - if(mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr()) + if (mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr()) updatePosition(state.getTime(), textkey->first, movement); state.setTime(textkey->first); } @@ -1295,34 +1387,34 @@ namespace MWRender state.mPlaying = (state.getTime() < state.mStopTime); timepassed = targetTime - state.getTime(); - while(textkey != textkeys.end() && textkey->first <= state.getTime()) + while (textkey != textkeys.end() && textkey->first <= state.getTime()) { handleTextKey(state, stateiter->first, textkey, textkeys); ++textkey; } } - if(state.shouldLoop()) + if (state.shouldLoop()) { state.mLoopCount--; state.setTime(state.mLoopStartTime); state.mPlaying = true; textkey = textkeys.lowerBound(state.getTime()); - while(textkey != textkeys.end() && textkey->first <= state.getTime()) + while (textkey != textkeys.end() && textkey->first <= state.getTime()) { handleTextKey(state, stateiter->first, textkey, textkeys); ++textkey; } - if(state.getTime() >= state.mLoopStopTime) + if (state.getTime() >= state.mLoopStopTime) break; } - if(timepassed <= 0.0f) + if (timepassed <= 0.0f) break; } - if(!state.mPlaying && state.mAutoDisable) + if (!state.mPlaying && state.mAutoDisable) { mStates.erase(stateiter++); @@ -1342,8 +1434,11 @@ namespace MWRender mRootController->setEnabled(enable); if (enable) { - mRootController->setRotate(osg::Quat(mLegsYawRadians, osg::Vec3f(0,0,1)) * osg::Quat(mBodyPitchRadians, osg::Vec3f(1,0,0))); + osg::Quat legYaw = osg::Quat(mLegsYawRadians, osg::Vec3f(0, 0, 1)); + mRootController->setRotate(legYaw * osg::Quat(mBodyPitchRadians, osg::Vec3f(1, 0, 0))); yawOffset = mLegsYawRadians; + // When yawing the root, also update the accumulated movement. + movement = legYaw * movement; } } if (mSpineController) @@ -1353,7 +1448,7 @@ namespace MWRender mSpineController->setEnabled(enable); if (enable) { - mSpineController->setRotate(osg::Quat(yaw, osg::Vec3f(0,0,1))); + mSpineController->setRotate(osg::Quat(yaw, osg::Vec3f(0, 0, 1))); yawOffset = mUpperBodyYawRadians; } } @@ -1363,33 +1458,32 @@ namespace MWRender bool enable = (std::abs(mHeadPitchRadians) > epsilon || std::abs(yaw) > epsilon); mHeadController->setEnabled(enable); if (enable) - mHeadController->setRotate(osg::Quat(mHeadPitchRadians, osg::Vec3f(1,0,0)) * osg::Quat(yaw, osg::Vec3f(0,0,1))); + mHeadController->setRotate( + osg::Quat(mHeadPitchRadians, osg::Vec3f(1, 0, 0)) * osg::Quat(yaw, osg::Vec3f(0, 0, 1))); } - // Scripted animations should not cause movement - if (hasScriptedAnims) - return osg::Vec3f(0, 0, 0); - return movement; } - void Animation::setLoopingEnabled(const std::string &groupname, bool enabled) + void Animation::setLoopingEnabled(std::string_view groupname, bool enabled) { AnimStateMap::iterator state(mStates.find(groupname)); - if(state != mStates.end()) + if (state != mStates.end()) state->second.mLoopingEnabled = enabled; } - void loadBonesFromFile(osg::ref_ptr& baseNode, const std::string &model, Resource::ResourceSystem* resourceSystem) + void loadBonesFromFile( + osg::ref_ptr& baseNode, VFS::Path::NormalizedView model, Resource::ResourceSystem* resourceSystem) { const osg::Node* node = resourceSystem->getSceneManager()->getTemplate(model).get(); - osg::ref_ptr sheathSkeleton (const_cast(node)); // const-trickery required because there is no const version of NodeVisitor + osg::ref_ptr sheathSkeleton( + const_cast(node)); // const-trickery required because there is no const version of NodeVisitor GetExtendedBonesVisitor getBonesVisitor; sheathSkeleton->accept(getBonesVisitor); for (auto& nodePair : getBonesVisitor.mFoundBones) { - SceneUtil::FindByNameVisitor findVisitor (nodePair.second->getName()); + SceneUtil::FindByNameVisitor findVisitor(nodePair.second->getName()); baseNode->accept(findVisitor); osg::Group* sheathParent = findVisitor.mFoundNode; @@ -1401,49 +1495,38 @@ namespace MWRender } } - void injectCustomBones(osg::ref_ptr& node, const std::string& model, Resource::ResourceSystem* resourceSystem) + void injectCustomBones( + osg::ref_ptr& node, const std::string& model, Resource::ResourceSystem* resourceSystem) { if (model.empty()) return; - const std::map& index = resourceSystem->getVFS()->getIndex(); - std::string animationPath = model; if (animationPath.find("meshes") == 0) { animationPath.replace(0, 6, "animations"); } - animationPath.replace(animationPath.size()-4, 4, "/"); + animationPath.replace(animationPath.size() - 4, 4, "/"); - resourceSystem->getVFS()->normalizeFilename(animationPath); - - std::map::const_iterator found = index.lower_bound(animationPath); - while (found != index.end()) + for (const auto& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { - const std::string& name = found->first; - if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".nif") == 0) - loadBonesFromFile(node, name, resourceSystem); - } - else - break; - ++found; + if (Misc::getFileExtension(name) == "nif") + loadBonesFromFile(node, VFS::Path::toNormalized(name), resourceSystem); } } - osg::ref_ptr getModelInstance(Resource::ResourceSystem* resourceSystem, const std::string& model, bool baseonly, bool inject, const std::string& defaultSkeleton) + osg::ref_ptr getModelInstance(Resource::ResourceSystem* resourceSystem, const std::string& model, + bool baseonly, bool inject, const std::string& defaultSkeleton) { Resource::SceneManager* sceneMgr = resourceSystem->getSceneManager(); if (baseonly) { - typedef std::map > Cache; + typedef std::map> Cache; static Cache cache; Cache::iterator found = cache.find(model); if (found == cache.end()) { - osg::ref_ptr created = sceneMgr->getInstance(model); + osg::ref_ptr created = sceneMgr->getInstance(VFS::Path::toNormalized(model)); if (inject) { @@ -1457,14 +1540,14 @@ namespace MWRender cache.insert(std::make_pair(model, created)); - return sceneMgr->createInstance(created); + return sceneMgr->getInstance(created); } else - return sceneMgr->createInstance(found->second); + return sceneMgr->getInstance(found->second); } else { - osg::ref_ptr created = sceneMgr->getInstance(model); + osg::ref_ptr created = sceneMgr->getInstance(VFS::Path::toNormalized(model)); if (inject) { @@ -1476,7 +1559,7 @@ namespace MWRender } } - void Animation::setObjectRoot(const std::string &model, bool forceskeleton, bool baseonly, bool isCreature) + void Animation::setObjectRoot(const std::string& model, bool forceskeleton, bool baseonly, bool isCreature) { osg::ref_ptr previousStateset; if (mObjectRoot) @@ -1497,45 +1580,48 @@ namespace MWRender mAccumRoot = nullptr; mAccumCtrl = nullptr; - static const bool useAdditionalSources = Settings::Manager::getBool ("use additional anim sources", "Game"); std::string defaultSkeleton; bool inject = false; - if (useAdditionalSources && mPtr.getClass().isActor()) + if (Settings::game().mUseAdditionalAnimSources && mPtr.getClass().isActor()) { if (isCreature) { - MWWorld::LiveCellRef *ref = mPtr.get(); - if(ref->mBase->mFlags & ESM::Creature::Bipedal) + MWWorld::LiveCellRef* ref = mPtr.get(); + if (ref->mBase->mFlags & ESM::Creature::Bipedal) { - defaultSkeleton = Settings::Manager::getString("xbaseanim", "Models"); + defaultSkeleton = Settings::models().mXbaseanim.get().value(); inject = true; } } else { inject = true; - MWWorld::LiveCellRef *ref = mPtr.get(); + MWWorld::LiveCellRef* ref = mPtr.get(); if (!ref->mBase->mModel.empty()) { - // If NPC has a custom animation model attached, we should inject bones from default skeleton for given race and gender as well - // Since it is a quite rare case, there should not be a noticable performance loss - // Note: consider that player and werewolves have no custom animation files attached for now - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Race *race = store.get().find(ref->mBase->mRace); - - bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; - bool isFemale = !ref->mBase->isMale(); - - defaultSkeleton = SceneUtil::getActorSkeleton(false, isFemale, isBeast, false); - defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath(defaultSkeleton, mResourceSystem->getVFS()); + // If NPC has a custom animation model attached, we should inject bones from default skeleton for + // given race and gender as well Since it is a quite rare case, there should not be a noticable + // performance loss Note: consider that player and werewolves have no custom animation files + // attached for now + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const ESM::Race* race = store.get().find(ref->mBase->mRace); + + const bool firstPerson = false; + const bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; + const bool isFemale = !ref->mBase->isMale(); + const bool werewolf = false; + + defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath( + getActorSkeleton(firstPerson, isFemale, isBeast, werewolf), mResourceSystem->getVFS()); } } } if (!forceskeleton) { - osg::ref_ptr created = getModelInstance(mResourceSystem, model, baseonly, inject, defaultSkeleton); + osg::ref_ptr created + = getModelInstance(mResourceSystem, model, baseonly, inject, defaultSkeleton); mInsert->addChild(created); mObjectRoot = created->asGroup(); if (!mObjectRoot) @@ -1551,7 +1637,8 @@ namespace MWRender } else { - osg::ref_ptr created = getModelInstance(mResourceSystem, model, baseonly, inject, defaultSkeleton); + osg::ref_ptr created + = getModelInstance(mResourceSystem, model, baseonly, inject, defaultSkeleton); osg::ref_ptr skel = dynamic_cast(created.get()); if (!skel) { @@ -1563,6 +1650,10 @@ namespace MWRender mInsert->addChild(mObjectRoot); } + // osgAnimation formats with skeletons should have their nodemap be bone instances + // FIXME: better way to detect osgAnimation here instead of relying on extension? + mRequiresBoneMap = mSkeleton != nullptr && !Misc::StringUtils::ciEndsWith(model, ".nif"); + if (previousStateset) mObjectRoot->setStateSet(previousStateset); @@ -1595,13 +1686,8 @@ namespace MWRender return mObjectRoot.get(); } - void Animation::addSpellCastGlow(const ESM::MagicEffect *effect, float glowDuration) + void Animation::addSpellCastGlow(const osg::Vec4f& color, float glowDuration) { - osg::Vec4f glowColor(1,1,1,1); - glowColor.x() = effect->mData.mRed / 255.f; - glowColor.y() = effect->mData.mGreen / 255.f; - glowColor.z() = effect->mData.mBlue / 255.f; - if (!mGlowUpdater || (mGlowUpdater->isDone() || (mGlowUpdater->isPermanentGlowUpdater() == true))) { if (mGlowUpdater && mGlowUpdater->isDone()) @@ -1609,22 +1695,24 @@ namespace MWRender if (mGlowUpdater && mGlowUpdater->isPermanentGlowUpdater()) { - mGlowUpdater->setColor(glowColor); + mGlowUpdater->setColor(color); mGlowUpdater->setDuration(glowDuration); } else - mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, glowColor, glowDuration); + mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, color, glowDuration); } } - void Animation::addExtraLight(osg::ref_ptr parent, const ESM::Light *esmLight) + void Animation::addExtraLight(osg::ref_ptr parent, const SceneUtil::LightCommon& esmLight) { bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); - mExtraLightSource = SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior); + mExtraLightSource = SceneUtil::addLight(parent, esmLight, Mask_Lighting, exterior); + mExtraLightSource->setActorFade(mAlpha); } - void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture) + void Animation::addEffect(std::string_view model, std::string_view effectId, bool loop, std::string_view bonename, + std::string_view texture) { if (!mObjectRoot.get()) return; @@ -1633,7 +1721,8 @@ namespace MWRender FindVfxCallbacksVisitor visitor(effectId); mInsert->accept(visitor); - for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it) + for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); + ++it) { UpdateVfxCallback* callback = *it; @@ -1648,50 +1737,64 @@ namespace MWRender parentNode = mInsert; else { - NodeMap::const_iterator found = getNodeMap().find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = getNodeMap().find(bonename); if (found == getNodeMap().end()) - throw std::runtime_error("Can't find bone " + bonename); + throw std::runtime_error("Can't find bone " + std::string{ bonename }); parentNode = found->second; } - osg::ref_ptr trans = new osg::PositionAttitudeTransform; + osg::ref_ptr trans = new SceneUtil::PositionAttitudeTransform; if (!mPtr.getClass().isNpc()) { - osg::Vec3f bounds (MWBase::Environment::get().getWorld()->getHalfExtents(mPtr) * 2.f / Constants::UnitsPerFoot); - float scale = std::max({ bounds.x()/3.f, bounds.y()/3.f, bounds.z()/6.f }); - trans->setScale(osg::Vec3f(scale, scale, scale)); + osg::Vec3f bounds(MWBase::Environment::get().getWorld()->getHalfExtents(mPtr) * 2.f); + float scale = std::max({ bounds.x(), bounds.y(), bounds.z() / 2.f }) / 64.f; + if (scale > 1.f) + trans->setScale(osg::Vec3f(scale, scale, scale)); + float offset = 0.f; + if (bounds.z() < 128.f) + offset = bounds.z() - 128.f; + else if (bounds.z() < bounds.x() + bounds.y()) + offset = 128.f - bounds.z(); + if (MWBase::Environment::get().getWorld()->isFlying(mPtr)) + offset /= 20.f; + trans->setPosition(osg::Vec3f(0.f, 0.f, offset * scale)); } parentNode->addChild(trans); - osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model, trans); - node->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + osg::ref_ptr node + = mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(model), trans); + // Morrowind has a white ambient light attached to the root VFX node of the scenegraph + node->getOrCreateStateSet()->setAttributeAndModes( + getVFXLightModelInstance(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + mResourceSystem->getSceneManager()->setUpNormalsRTForStateSet(node->getOrCreateStateSet(), false); SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; node->accept(findMaxLengthVisitor); - // FreezeOnCull doesn't work so well with effect particles, that tend to have moving emitters - SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; - node->accept(disableFreezeOnCullVisitor); node->setNodeMask(Mask_Effect); + MarkDrawablesVisitor markVisitor(Mask_Effect); + node->accept(markVisitor); + params.mMaxControllerLength = findMaxLengthVisitor.getMaxLength(); params.mLoop = loop; params.mEffectId = effectId; params.mBoneName = bonename; - params.mAnimTime = std::shared_ptr(new EffectAnimationTime); + params.mAnimTime = std::make_shared(); trans->addUpdateCallback(new UpdateVfxCallback(params)); - SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr(params.mAnimTime)); + SceneUtil::AssignControllerSourcesVisitor assignVisitor( + std::shared_ptr(params.mAnimTime)); node->accept(assignVisitor); // Notify that this animation has attached magic effects mHasMagicEffects = true; - overrideFirstRootTexture(texture, mResourceSystem, node); + overrideFirstRootTexture(texture, mResourceSystem, *node); } - void Animation::removeEffect(int effectId) + void Animation::removeEffect(std::string_view effectId) { RemoveCallbackVisitor visitor(effectId); mInsert->accept(visitor); @@ -1701,24 +1804,28 @@ namespace MWRender void Animation::removeEffects() { - removeEffect(-1); + removeEffect(""); } - void Animation::getLoopingEffects(std::vector &out) const + std::vector Animation::getLoopingEffects() const { if (!mHasMagicEffects) - return; + return {}; FindVfxCallbacksVisitor visitor; mInsert->accept(visitor); - for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it) + std::vector out; + + for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); + ++it) { UpdateVfxCallback* callback = *it; if (callback->mParams.mLoop && !callback->mFinished) out.push_back(callback->mParams.mEffectId); } + return out; } void Animation::updateEffects() @@ -1741,18 +1848,17 @@ namespace MWRender for (AnimStateMap::const_iterator stateiter = mStates.begin(); stateiter != mStates.end(); ++stateiter) { if (stateiter->second.mPriority.contains(int(MWMechanics::Priority_Hit)) - || stateiter->second.mPriority.contains(int(MWMechanics::Priority_Weapon)) - || stateiter->second.mPriority.contains(int(MWMechanics::Priority_Knockdown)) - || stateiter->second.mPriority.contains(int(MWMechanics::Priority_Death))) + || stateiter->second.mPriority.contains(int(MWMechanics::Priority_Weapon)) + || stateiter->second.mPriority.contains(int(MWMechanics::Priority_Knockdown)) + || stateiter->second.mPriority.contains(int(MWMechanics::Priority_Death))) return false; } return true; } - const osg::Node* Animation::getNode(const std::string &name) const + const osg::Node* Animation::getNode(std::string_view name) const { - std::string lowerName = Misc::StringUtils::lowerCase(name); - NodeMap::const_iterator found = getNodeMap().find(lowerName); + NodeMap::const_iterator found = getNodeMap().find(name); if (found == getNodeMap().end()) return nullptr; else @@ -1771,7 +1877,6 @@ namespace MWRender if (mTransparencyUpdater == nullptr) { mTransparencyUpdater = new TransparencyUpdater(alpha); - mTransparencyUpdater->setLightSource(mExtraLightSource); mObjectRoot->addCullCallback(mTransparencyUpdater); } else @@ -1782,6 +1887,8 @@ namespace MWRender mObjectRoot->removeCullCallback(mTransparencyUpdater); mTransparencyUpdater = nullptr; } + if (mExtraLightSource) + mExtraLightSource->setActorFade(alpha); } void Animation::setLightEffect(float effect) @@ -1809,10 +1916,10 @@ namespace MWRender mGlowLight = nullptr; } - osg::ref_ptr light (new osg::Light); - light->setDiffuse(osg::Vec4f(0,0,0,0)); - light->setSpecular(osg::Vec4f(0,0,0,0)); - light->setAmbient(osg::Vec4f(1.5f,1.5f,1.5f,1.f)); + osg::ref_ptr light(new osg::Light); + light->setDiffuse(osg::Vec4f(0, 0, 0, 0)); + light->setSpecular(osg::Vec4f(0, 0, 0, 0)); + light->setAmbient(osg::Vec4f(1.5f, 1.5f, 1.5f, 1.f)); bool isExterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); SceneUtil::configureLight(light, radius, isExterior); @@ -1834,7 +1941,7 @@ namespace MWRender mRootController = addRotateController("bip01"); } - RotateController* Animation::addRotateController(std::string bone) + osg::ref_ptr Animation::addRotateController(std::string_view bone) { auto iter = getNodeMap().find(bone); if (iter == getNodeMap().end()) @@ -1845,19 +1952,21 @@ namespace MWRender osg::Callback* cb = node->getUpdateCallback(); while (cb) { - if (dynamic_cast(cb)) + if (dynamic_cast(cb) || dynamic_cast(cb) + || dynamic_cast(cb)) { foundKeyframeCtrl = true; break; } cb = cb->getNestedCallback(); } + // Note: AnimBlendController also does the reset so if one is present - we should add the rotation node // Without KeyframeController the orientation will not be reseted each frame, so // RotateController shouldn't be used for such nodes. if (!foundKeyframeCtrl) return nullptr; - RotateController* controller = new RotateController(mObjectRoot.get()); + osg::ref_ptr controller(new RotateController(mObjectRoot.get())); node->addUpdateCallback(controller); mActiveControllers.emplace_back(node, controller); return controller; @@ -1883,6 +1992,42 @@ namespace MWRender return mHeadYawRadians; } + void Animation::removeFromScene() + { + removeFromSceneImpl(); + } + + void Animation::removeFromSceneImpl() + { + if (mGlowLight != nullptr) + mInsert->removeChild(mGlowLight); + + if (mObjectRoot != nullptr) + mInsert->removeChild(mObjectRoot); + } + + MWWorld::MovementDirectionFlags Animation::getSupportedMovementDirections( + std::span prefixes) const + { + MWWorld::MovementDirectionFlags result = 0; + for (const std::string_view animation : mSupportedAnimations) + { + 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; + } + return result; + } + // ------------------------------------------------------ float Animation::AnimationTime::getValue(osg::NodeVisitor*) @@ -1914,7 +2059,8 @@ namespace MWRender // -------------------------------------------------------------------------------- - ObjectAnimation::ObjectAnimation(const MWWorld::Ptr &ptr, const std::string &model, Resource::ResourceSystem* resourceSystem, bool animated, bool allowLight) + ObjectAnimation::ObjectAnimation(const MWWorld::Ptr& ptr, const std::string& model, + Resource::ResourceSystem* resourceSystem, bool animated, bool allowLight) : Animation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), resourceSystem) { if (!model.empty()) @@ -1924,10 +2070,13 @@ namespace MWRender addAnimSource(model, model); if (!ptr.getClass().getEnchantment(ptr).empty()) - mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); + mGlowUpdater = SceneUtil::addEnchantedGlow( + mObjectRoot, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); } - if (ptr.getTypeName() == typeid(ESM::Light).name() && allowLight) - addExtraLight(getOrCreateObjectRoot(), ptr.get()->mBase); + if (ptr.getType() == ESM::Light::sRecordId && allowLight) + addExtraLight(getOrCreateObjectRoot(), SceneUtil::LightCommon(*ptr.get()->mBase)); + if (ptr.getType() == ESM4::Light::sRecordId && allowLight) + addExtraLight(getOrCreateObjectRoot(), SceneUtil::LightCommon(*ptr.get()->mBase)); if (!allowLight && mObjectRoot) { @@ -1936,13 +2085,14 @@ namespace MWRender visitor.remove(); } - if (SceneUtil::hasUserDescription(mObjectRoot, Constants::NightDayLabel)) + if (Settings::game().mDayNightSwitches && SceneUtil::hasUserDescription(mObjectRoot, Constants::NightDayLabel)) { AddSwitchCallbacksVisitor visitor; mObjectRoot->accept(visitor); } - if (ptr.getRefData().getCustomData() != nullptr && canBeHarvested()) + if (Settings::game().mGraphicHerbalism && ptr.getRefData().getCustomData() != nullptr + && ObjectAnimation::canBeHarvested()) { const MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); if (!store.hasVisibleItems()) @@ -1955,7 +2105,7 @@ namespace MWRender bool ObjectAnimation::canBeHarvested() const { - if (mPtr.getTypeName() != typeid(ESM::Container).name()) + if (mPtr.getType() != ESM::Container::sRecordId) return false; const MWWorld::LiveCellRef* ref = mPtr.get(); @@ -1965,27 +2115,23 @@ namespace MWRender return SceneUtil::hasUserDescription(mObjectRoot, Constants::HerbalismLabel); } - Animation::AnimState::~AnimState() - { - - } - // ------------------------------ PartHolder::PartHolder(osg::ref_ptr node) - : mNode(node) + : mNode(std::move(node)) { } PartHolder::~PartHolder() { if (mNode.get() && !mNode->getNumParents()) - Log(Debug::Verbose) << "Part \"" << mNode->getName() << "\" has no parents" ; + Log(Debug::Verbose) << "Part \"" << mNode->getName() << "\" has no parents"; if (mNode.get() && mNode->getNumParents()) { if (mNode->getNumParents() > 1) - Log(Debug::Verbose) << "Part \"" << mNode->getName() << "\" has multiple (" << mNode->getNumParents() << ") parents"; + Log(Debug::Verbose) << "Part \"" << mNode->getName() << "\" has multiple (" << mNode->getNumParents() + << ") parents"; mNode->getParent(0)->removeChild(mNode); } } diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 213a4f70490..36a84ba2ab2 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -1,12 +1,26 @@ #ifndef GAME_RENDER_ANIMATION_H #define GAME_RENDER_ANIMATION_H +#include "animationpriority.hpp" +#include "animblendcontroller.hpp" +#include "blendmask.hpp" +#include "bonegroup.hpp" + +#include "../mwworld/movementdirection.hpp" #include "../mwworld/ptr.hpp" +#include +#include #include +#include #include #include +#include +#include +#include +#include +#include #include namespace ESM @@ -27,503 +41,478 @@ namespace SceneUtil class LightSource; class LightListCallback; class Skeleton; + struct LightCommon; } namespace MWRender { -class ResetAccumRootCallback; -class RotateController; -class TransparencyUpdater; + class ResetAccumRootCallback; + class RotateController; + class TransparencyUpdater; -class EffectAnimationTime : public SceneUtil::ControllerSource -{ -private: - float mTime; -public: - float getValue(osg::NodeVisitor* nv) override; + using ActiveControllersVector = std::vector, osg::ref_ptr>>; - void addTime(float duration); - void resetTime(float time); - float getTime() const; + class EffectAnimationTime : public SceneUtil::ControllerSource + { + private: + float mTime; - EffectAnimationTime() : mTime(0) { } -}; + public: + float getValue(osg::NodeVisitor* nv) override; -/// @brief Detaches the node from its parent when the object goes out of scope. -class PartHolder -{ -public: - PartHolder(osg::ref_ptr node); + void addTime(float duration); + void resetTime(float time); + float getTime() const; - ~PartHolder(); + EffectAnimationTime() + : mTime(0) + { + } + }; - osg::ref_ptr getNode() + /// @brief Detaches the node from its parent when the object goes out of scope. + class PartHolder { - return mNode; - } + public: + PartHolder(osg::ref_ptr node); -private: - osg::ref_ptr mNode; + ~PartHolder(); - void operator= (const PartHolder&); - PartHolder(const PartHolder&); -}; -typedef std::shared_ptr PartHolderPtr; + const osg::ref_ptr& getNode() const { return mNode; } -struct EffectParams -{ - std::string mModelName; // Just here so we don't add the same effect twice - std::shared_ptr mAnimTime; - float mMaxControllerLength; - int mEffectId; - bool mLoop; - std::string mBoneName; -}; - -class Animation : public osg::Referenced -{ -public: - enum BoneGroup { - BoneGroup_LowerBody = 0, - BoneGroup_Torso, - BoneGroup_LeftArm, - BoneGroup_RightArm - }; - - enum BlendMask { - BlendMask_LowerBody = 1<<0, - BlendMask_Torso = 1<<1, - BlendMask_LeftArm = 1<<2, - BlendMask_RightArm = 1<<3, + private: + osg::ref_ptr mNode; - BlendMask_UpperBody = BlendMask_Torso | BlendMask_LeftArm | BlendMask_RightArm, + void operator=(const PartHolder&); + PartHolder(const PartHolder&); + }; + using PartHolderPtr = std::unique_ptr; - BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody + struct EffectParams + { + std::string mModelName; // Just here so we don't add the same effect twice + std::shared_ptr mAnimTime; + float mMaxControllerLength; + std::string mEffectId; + bool mLoop; + std::string mBoneName; }; - /* This is the number of *discrete* blend masks. */ - static constexpr size_t sNumBlendMasks = 4; - /// Holds an animation priority value for each BoneGroup. - struct AnimPriority + class Animation : public osg::Referenced { - /// Convenience constructor, initialises all priorities to the same value. - AnimPriority(int priority) - { - for (unsigned int i=0; i, Misc::StringUtils::CiHash, + Misc::StringUtils::CiEqual> + NodeMap; - const int& operator[] (BoneGroup n) const + protected: + class AnimationTime : public SceneUtil::ControllerSource { - return mPriority[n]; - } + private: + std::shared_ptr mTimePtr; + + public: + void setTimePtr(std::shared_ptr time) { mTimePtr = std::move(time); } + std::shared_ptr getTimePtr() const { return mTimePtr; } - bool contains(int priority) const + float getValue(osg::NodeVisitor* nv) override; + }; + + class NullAnimationTime : public SceneUtil::ControllerSource { - for (unsigned int i=0; i mSource; + float mStartTime = 0; + float mLoopStartTime = 0; + float mLoopStopTime = 0; + float mStopTime = 0; - virtual ~TextKeyListener() = default; - }; + std::shared_ptr mTime = std::make_shared(0); + float mSpeedMult = 1; - void setTextKeyListener(TextKeyListener* listener); + bool mPlaying = false; + bool mLoopingEnabled = true; + uint32_t mLoopCount = 0; - virtual bool updateCarriedLeftVisible(const int weaptype) const { return false; }; + AnimPriority mPriority{ 0 }; + int mBlendMask = 0; + bool mAutoDisable = true; -protected: - class AnimationTime : public SceneUtil::ControllerSource - { - private: - std::shared_ptr mTimePtr; + std::string mGroupname; + std::string mStartKey; - public: + float getTime() const { return *mTime; } + void setTime(float time) { *mTime = time; } + bool blendMaskContains(size_t blendMask) const { return (mBlendMask & (1 << blendMask)); } + bool shouldLoop() const { return getTime() >= mLoopStopTime && mLoopingEnabled && mLoopCount > 0; } + }; - void setTimePtr(std::shared_ptr time) - { mTimePtr = time; } - std::shared_ptr getTimePtr() const - { return mTimePtr; } + typedef std::map> AnimStateMap; + AnimStateMap mStates; - float getValue(osg::NodeVisitor* nv) override; - }; + typedef std::vector> AnimSourceList; + AnimSourceList mAnimSources; - class NullAnimationTime : public SceneUtil::ControllerSource - { - public: - float getValue(osg::NodeVisitor *nv) override - { - return 0.f; - } - }; + std::unordered_set mSupportedAnimations; - struct AnimSource; + osg::ref_ptr mInsert; - struct AnimState { - std::shared_ptr mSource; - float mStartTime; - float mLoopStartTime; - float mLoopStopTime; - float mStopTime; + osg::ref_ptr mObjectRoot; + SceneUtil::Skeleton* mSkeleton; - typedef std::shared_ptr TimePtr; - TimePtr mTime; - float mSpeedMult; + // The node expected to accumulate movement during movement animations. + osg::ref_ptr mAccumRoot; - bool mPlaying; - bool mLoopingEnabled; - size_t mLoopCount; + // The controller animating that node. + osg::ref_ptr mAccumCtrl; - AnimPriority mPriority; - int mBlendMask; - bool mAutoDisable; + // Used to reset the position of the accumulation root every frame - the movement should be applied to the + // physics system + osg::ref_ptr mResetAccumRootCallback; - AnimState() : mStartTime(0.0f), mLoopStartTime(0.0f), mLoopStopTime(0.0f), mStopTime(0.0f), - mTime(new float), mSpeedMult(1.0f), mPlaying(false), mLoopingEnabled(true), - mLoopCount(0), mPriority(0), mBlendMask(0), mAutoDisable(true) - { - } - ~AnimState(); + // Keep track of controllers that we added to our scene graph. + // We may need to rebuild these controllers when the active animation groups / sources change. + ActiveControllersVector mActiveControllers; - float getTime() const - { - return *mTime; - } - void setTime(float time) - { - *mTime = time; - } + // Keep track of the animation controllers for easy access + std::map, osg::ref_ptr> mAnimBlendControllers; + std::map, osg::ref_ptr> mBoneAnimBlendControllers; - bool shouldLoop() const - { - return getTime() >= mLoopStopTime && mLoopingEnabled && mLoopCount > 0; - } - }; - typedef std::map AnimStateMap; - AnimStateMap mStates; + std::shared_ptr mAnimationTimePtr[sNumBlendMasks]; - typedef std::vector > AnimSourceList; - AnimSourceList mAnimSources; + mutable NodeMap mNodeMap; + mutable bool mNodeMapCreated; - osg::ref_ptr mInsert; + MWWorld::Ptr mPtr; - osg::ref_ptr mObjectRoot; - SceneUtil::Skeleton* mSkeleton; + Resource::ResourceSystem* mResourceSystem; - // The node expected to accumulate movement during movement animations. - osg::ref_ptr mAccumRoot; + osg::Vec3f mAccumulate; - // The controller animating that node. - osg::ref_ptr mAccumCtrl; + TextKeyListener* mTextKeyListener; - // Used to reset the position of the accumulation root every frame - the movement should be applied to the physics system - osg::ref_ptr mResetAccumRootCallback; + osg::ref_ptr mHeadController; + osg::ref_ptr mSpineController; + osg::ref_ptr mRootController; + float mHeadYawRadians; + float mHeadPitchRadians; + float mUpperBodyYawRadians; + float mLegsYawRadians; + float mBodyPitchRadians; - // Keep track of controllers that we added to our scene graph. - // We may need to rebuild these controllers when the active animation groups / sources change. - std::vector, osg::ref_ptr>> mActiveControllers; + osg::ref_ptr addRotateController(std::string_view bone); - std::shared_ptr mAnimationTimePtr[sNumBlendMasks]; + bool mHasMagicEffects; - // Stored in all lowercase for a case-insensitive lookup - typedef std::map > NodeMap; - mutable NodeMap mNodeMap; - mutable bool mNodeMapCreated; + osg::ref_ptr mGlowLight; + osg::ref_ptr mGlowUpdater; + osg::ref_ptr mTransparencyUpdater; + osg::ref_ptr mExtraLightSource; - MWWorld::Ptr mPtr; + float mAlpha; - Resource::ResourceSystem* mResourceSystem; + mutable std::map> mAnimVelocities; - osg::Vec3f mAccumulate; + osg::ref_ptr mLightListCallback; - TextKeyListener* mTextKeyListener; + bool mPlayScriptedOnly; + bool mRequiresBoneMap; - osg::ref_ptr mHeadController; - osg::ref_ptr mSpineController; - osg::ref_ptr mRootController; - float mHeadYawRadians; - float mHeadPitchRadians; - float mUpperBodyYawRadians; - float mLegsYawRadians; - float mBodyPitchRadians; + const NodeMap& getNodeMap() const; - RotateController* addRotateController(std::string bone); + /* Sets the appropriate animations on the bone groups based on priority by finding + * the highest priority AnimationStates and linking the appropriate controllers stored + * in the AnimationState to the corresponding nodes. + */ + void resetActiveGroups(); - bool mHasMagicEffects; + size_t detectBlendMask(const osg::Node* node, const std::string& controllerName) const; - osg::ref_ptr mGlowLight; - osg::ref_ptr mGlowUpdater; - osg::ref_ptr mTransparencyUpdater; - osg::ref_ptr mExtraLightSource; + /* Updates the position of the accum root node for the given time, and + * returns the wanted movement vector from the previous time. */ + void updatePosition(float oldtime, float newtime, osg::Vec3f& position); - float mAlpha; + /* Resets the animation to the time of the specified start marker, without + * moving anything, and set the end time to the specified stop marker. If + * the marker is not found, or if the markers are the same, it returns + * false. + */ + bool reset(AnimState& state, const SceneUtil::TextKeyMap& keys, std::string_view groupname, + std::string_view start, std::string_view stop, float startpoint, bool loopfallback); - mutable std::map mAnimVelocities; + void handleTextKey(AnimState& state, std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, + const SceneUtil::TextKeyMap& map); - osg::ref_ptr mLightListCallback; + /** Sets the root model of the object. + * + * Note that you must make sure all animation sources are cleared before resetting the object + * root. All nodes previously retrieved with getNode will also become invalidated. + * @param forceskeleton Wrap the object root in a Skeleton, even if it contains no skinned parts. Use this if + * you intend to add skinned parts manually. + * @param baseonly If true, then any meshes or particle systems in the model are ignored + * (useful for NPCs, where only the skeleton is needed for the root, and the actual NPC parts are then + * assembled from separate files). + */ + void setObjectRoot(const std::string& model, bool forceskeleton, bool baseonly, bool isCreature); - const NodeMap& getNodeMap() const; + void loadAllAnimationsInFolder(const std::string& model, const std::string& baseModel); - /* Sets the appropriate animations on the bone groups based on priority. - */ - void resetActiveGroups(); + /** Adds the keyframe controllers in the specified model as a new animation source. + * @note Later added animation sources have the highest priority when it comes to finding a particular + * animation. + * @param model The file to add the keyframes for. Note that the .nif file extension will be replaced with .kf. + * @param baseModel The filename of the mObjectRoot, only used for error messages. + */ + void addAnimSource(std::string_view model, const std::string& baseModel); + std::shared_ptr addSingleAnimSource(const std::string& model, const std::string& baseModel); - size_t detectBlendMask(const osg::Node* node) const; + /** Adds an additional light to the given node using the specified ESM record. */ + void addExtraLight(osg::ref_ptr parent, const SceneUtil::LightCommon& light); - /* Updates the position of the accum root node for the given time, and - * returns the wanted movement vector from the previous time. */ - void updatePosition(float oldtime, float newtime, osg::Vec3f& position); + void clearAnimSources(); - /* Resets the animation to the time of the specified start marker, without - * moving anything, and set the end time to the specified stop marker. If - * the marker is not found, or if the markers are the same, it returns - * false. - */ - bool reset(AnimState &state, const SceneUtil::TextKeyMap &keys, - const std::string &groupname, const std::string &start, const std::string &stop, - float startpoint, bool loopfallback); + /** + * Provided to allow derived classes adding their own controllers. Note, the controllers must be added to + * mActiveControllers so they get cleaned up properly on the next controller rebuild. A controller rebuild may + * be necessary to ensure correct ordering. + */ + virtual void addControllers(); - void handleTextKey(AnimState &state, const std::string &groupname, SceneUtil::TextKeyMap::ConstIterator key, - const SceneUtil::TextKeyMap& map); + void removeFromSceneImpl(); - /** Sets the root model of the object. - * - * Note that you must make sure all animation sources are cleared before resetting the object - * root. All nodes previously retrieved with getNode will also become invalidated. - * @param forceskeleton Wrap the object root in a Skeleton, even if it contains no skinned parts. Use this if you intend to add skinned parts manually. - * @param baseonly If true, then any meshes or particle systems in the model are ignored - * (useful for NPCs, where only the skeleton is needed for the root, and the actual NPC parts are then assembled from separate files). - */ - void setObjectRoot(const std::string &model, bool forceskeleton, bool baseonly, bool isCreature); + template + inline osg::Callback* handleBlendTransform(const osg::ref_ptr& node, + osg::ref_ptr keyframeController, + std::map, osg::ref_ptr>& blendControllers, + const AnimBlendStateData& stateData, const osg::ref_ptr& blendRules, + const AnimState& active); - void loadAllAnimationsInFolder(const std::string &model, const std::string &baseModel); + public: + Animation( + const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem); + + /// Must be thread safe + virtual ~Animation(); + + MWWorld::ConstPtr getPtr() const { return mPtr; } + + MWWorld::Ptr getPtr() { return mPtr; } + + /// Set active flag on the object skeleton, if one exists. + /// @see SceneUtil::Skeleton::setActive + /// 0 = Inactive, 1 = Active in place, 2 = Active + void setActive(int active); + + osg::Group* getOrCreateObjectRoot(); + + osg::Group* getObjectRoot(); + + /** + * @brief Add an effect mesh attached to a bone or the insert scene node + * @param model + * @param effectId An ID for this effect by which you can identify it later. If this is not wanted, set to -1. + * @param loop Loop the effect. If false, it is removed automatically after it finishes playing. If true, + * you need to remove it manually using removeEffect when the effect should end. + * @param bonename Bone to attach to, or empty string to use the scene node instead + * @param texture override the texture specified in the model's materials - if empty, do not override + * @note Will not add an effect twice. + */ + void addEffect(std::string_view model, std::string_view effectId, bool loop = false, + std::string_view bonename = {}, std::string_view texture = {}); + void removeEffect(std::string_view effectId); + void removeEffects(); + std::vector getLoopingEffects() const; + + // Add a spell casting glow to an object. From measuring video taken from the original engine, + // the glow seems to be about 1.5 seconds except for telekinesis, which is 1 second. + void addSpellCastGlow(const osg::Vec4f& color, float glowDuration = 1.5); + + virtual void updatePtr(const MWWorld::Ptr& ptr); + + bool hasAnimation(std::string_view anim) const; + + bool isLoopingAnimation(std::string_view group) const; + + // Specifies the axis' to accumulate on. Non-accumulated axis will just + // move visually, but not affect the actual movement. Each x/y/z value + // should be on the scale of 0 to 1. + void setAccumulation(const osg::Vec3f& accum); + + /** Plays an animation. + * Creates or updates AnimationStates to represent and manage animation playback. + * \param groupname Name of the animation group to play. + * \param priority Priority of the animation. The animation will play on + * bone groups that don't have another animation set of a + * higher priority. + * \param blendMask Bone groups to play the animation on. + * \param autodisable Automatically disable the animation when it stops + * playing. + * \param speedmult Speed multiplier for the animation. + * \param start Key marker from which to start. + * \param stop Key marker to stop at. + * \param startpoint How far in between the two markers to start. 0 starts + * at the start marker, 1 starts at the stop marker. + * \param loops How many times to loop the animation. This will use the + * "loop start" and "loop stop" markers if they exist, + * otherwise it may fall back to "start" and "stop", but only if + * the \a loopFallback parameter is true. + * \param loopFallback Allow looping an animation that has no loop keys, i.e. fall back to use + * the "start" and "stop" keys for looping? + */ + void play(std::string_view groupname, const AnimPriority& priority, int blendMask, bool autodisable, + float speedmult, std::string_view start, std::string_view stop, float startpoint, uint32_t loops, + bool loopfallback = false); + + /** Adjust the speed multiplier of an already playing animation. + */ + void adjustSpeedMult(const std::string& groupname, float speedmult); + + /** Returns true if the named animation group is playing. */ + bool isPlaying(std::string_view groupname) const; + + /// Returns true if no important animations are currently playing on the upper body. + bool upperBodyReady() const; + + /** Gets info about the given animation group. + * \param groupname Animation group to check. + * \param complete Stores completion amount (0 = at start key, 0.5 = half way between start and stop keys), etc. + * \param speedmult Stores the animation speed multiplier + * \return True if the animation is active, false otherwise. + */ + bool getInfo(std::string_view groupname, float* complete = nullptr, float* speedmult = nullptr, + size_t* loopcount = nullptr) const; + + /// Returns the group name of the animation currently active on that bone group. + std::string_view getActiveGroup(BoneGroup boneGroup) const; + + /// Get the absolute position in the animation track of the first text key with the given group. + float getStartTime(const std::string& groupname) const; + + /// Get the absolute position in the animation track of the text key + float getTextKeyTime(std::string_view textKey) const; + + /// Get the current absolute position in the animation track for the animation that is currently playing from + /// the given group. + float getCurrentTime(std::string_view groupname) const; + + /** Disables the specified animation group; + * \param groupname Animation group to disable. + */ + void disable(std::string_view groupname); + + /** Retrieves the velocity (in units per second) that the animation will move. */ + float getVelocity(std::string_view groupname) const; + + virtual osg::Vec3f runAnimation(float duration); + + void setLoopingEnabled(std::string_view groupname, bool enabled); + + /// This is typically called as part of runAnimation, but may be called manually if needed. + void updateEffects(); + + /// Return a node with the specified name, or nullptr if not existing. + /// @note The matching is case-insensitive. + const osg::Node* getNode(std::string_view name) const; + + MWWorld::MovementDirectionFlags getSupportedMovementDirections( + std::span prefixes) const; + + bool getPlayScriptedOnly() const { return mPlayScriptedOnly; } + void setPlayScriptedOnly(bool playScriptedOnly) { mPlayScriptedOnly = playScriptedOnly; } + + virtual bool useShieldAnimations() const { return false; } + virtual bool getWeaponsShown() const { return false; } + virtual void showWeapons(bool showWeapon) {} + virtual bool getCarriedLeftShown() const { return false; } + virtual void showCarriedLeft(bool show) {} + virtual void setWeaponGroup(const std::string& group, bool relativeDuration) {} + virtual void setVampire(bool vampire) {} + /// A value < 1 makes the animation translucent, 1.f = fully opaque + void setAlpha(float alpha); + virtual void setPitchFactor(float factor) {} + virtual void attachArrow() {} + virtual void detachArrow() {} + virtual void releaseArrow(float attackStrength) {} + virtual void enableHeadAnimation(bool enable) {} + // TODO: move outside of this class + /// Makes this object glow, by placing a Light in its center. + /// @param effect Controls the radius and intensity of the light. + virtual void setLightEffect(float effect); + + virtual void setHeadPitch(float pitchRadians); + virtual void setHeadYaw(float yawRadians); + virtual float getHeadPitch() const; + virtual float getHeadYaw() const; + + virtual void setUpperBodyYawRadians(float v) { mUpperBodyYawRadians = v; } + virtual void setLegsYawRadians(float v) { mLegsYawRadians = v; } + virtual float getUpperBodyYawRadians() const { return mUpperBodyYawRadians; } + virtual float getLegsYawRadians() const { return mLegsYawRadians; } + virtual void setBodyPitchRadians(float v) { mBodyPitchRadians = v; } + virtual float getBodyPitchRadians() const { return mBodyPitchRadians; } + + virtual void setAccurateAiming(bool enabled) {} + virtual bool canBeHarvested() const { return false; } + + virtual void removeFromScene(); - /** Adds the keyframe controllers in the specified model as a new animation source. - * @note Later added animation sources have the highest priority when it comes to finding a particular animation. - * @param model The file to add the keyframes for. Note that the .nif file extension will be replaced with .kf. - * @param baseModel The filename of the mObjectRoot, only used for error messages. - */ - void addAnimSource(const std::string &model, const std::string& baseModel); - void addSingleAnimSource(const std::string &model, const std::string& baseModel); - - /** Adds an additional light to the given node using the specified ESM record. */ - void addExtraLight(osg::ref_ptr parent, const ESM::Light *light); - - void clearAnimSources(); - - /** - * Provided to allow derived classes adding their own controllers. Note, the controllers must be added to mActiveControllers - * so they get cleaned up properly on the next controller rebuild. A controller rebuild may be necessary to ensure correct ordering. - */ - virtual void addControllers(); - -public: - - Animation(const MWWorld::Ptr &ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem); - - /// Must be thread safe - virtual ~Animation(); - - MWWorld::ConstPtr getPtr() const; - - MWWorld::Ptr getPtr(); - - /// Set active flag on the object skeleton, if one exists. - /// @see SceneUtil::Skeleton::setActive - /// 0 = Inactive, 1 = Active in place, 2 = Active - void setActive(int active); - - osg::Group* getOrCreateObjectRoot(); - - osg::Group* getObjectRoot(); - - /** - * @brief Add an effect mesh attached to a bone or the insert scene node - * @param model - * @param effectId An ID for this effect by which you can identify it later. If this is not wanted, set to -1. - * @param loop Loop the effect. If false, it is removed automatically after it finishes playing. If true, - * you need to remove it manually using removeEffect when the effect should end. - * @param bonename Bone to attach to, or empty string to use the scene node instead - * @param texture override the texture specified in the model's materials - if empty, do not override - * @note Will not add an effect twice. - */ - void addEffect (const std::string& model, int effectId, bool loop = false, const std::string& bonename = "", const std::string& texture = ""); - void removeEffect (int effectId); - void removeEffects (); - void getLoopingEffects (std::vector& out) const; - - // Add a spell casting glow to an object. From measuring video taken from the original engine, - // the glow seems to be about 1.5 seconds except for telekinesis, which is 1 second. - void addSpellCastGlow(const ESM::MagicEffect *effect, float glowDuration = 1.5); - - virtual void updatePtr(const MWWorld::Ptr &ptr); - - bool hasAnimation(const std::string &anim) const; - - // Specifies the axis' to accumulate on. Non-accumulated axis will just - // move visually, but not affect the actual movement. Each x/y/z value - // should be on the scale of 0 to 1. - void setAccumulation(const osg::Vec3f& accum); - - /** Plays an animation. - * \param groupname Name of the animation group to play. - * \param priority Priority of the animation. The animation will play on - * bone groups that don't have another animation set of a - * higher priority. - * \param blendMask Bone groups to play the animation on. - * \param autodisable Automatically disable the animation when it stops - * playing. - * \param speedmult Speed multiplier for the animation. - * \param start Key marker from which to start. - * \param stop Key marker to stop at. - * \param startpoint How far in between the two markers to start. 0 starts - * at the start marker, 1 starts at the stop marker. - * \param loops How many times to loop the animation. This will use the - * "loop start" and "loop stop" markers if they exist, - * otherwise it may fall back to "start" and "stop", but only if - * the \a loopFallback parameter is true. - * \param loopFallback Allow looping an animation that has no loop keys, i.e. fall back to use - * the "start" and "stop" keys for looping? - */ - void play(const std::string &groupname, const AnimPriority& priority, int blendMask, bool autodisable, - float speedmult, const std::string &start, const std::string &stop, - float startpoint, size_t loops, bool loopfallback=false); - - /** Adjust the speed multiplier of an already playing animation. - */ - void adjustSpeedMult (const std::string& groupname, float speedmult); - - /** Returns true if the named animation group is playing. */ - bool isPlaying(const std::string &groupname) const; - - /// Returns true if no important animations are currently playing on the upper body. - bool upperBodyReady() const; - - /** Gets info about the given animation group. - * \param groupname Animation group to check. - * \param complete Stores completion amount (0 = at start key, 0.5 = half way between start and stop keys), etc. - * \param speedmult Stores the animation speed multiplier - * \return True if the animation is active, false otherwise. - */ - bool getInfo(const std::string &groupname, float *complete=nullptr, float *speedmult=nullptr) const; - - /// Get the absolute position in the animation track of the first text key with the given group. - float getStartTime(const std::string &groupname) const; - - /// Get the absolute position in the animation track of the text key - float getTextKeyTime(const std::string &textKey) const; - - /// Get the current absolute position in the animation track for the animation that is currently playing from the given group. - float getCurrentTime(const std::string& groupname) const; - - size_t getCurrentLoopCount(const std::string& groupname) const; - - /** Disables the specified animation group; - * \param groupname Animation group to disable. - */ - void disable(const std::string &groupname); - - /** Retrieves the velocity (in units per second) that the animation will move. */ - float getVelocity(const std::string &groupname) const; - - virtual osg::Vec3f runAnimation(float duration); - - void setLoopingEnabled(const std::string &groupname, bool enabled); - - /// This is typically called as part of runAnimation, but may be called manually if needed. - void updateEffects(); - - /// Return a node with the specified name, or nullptr if not existing. - /// @note The matching is case-insensitive. - const osg::Node* getNode(const std::string& name) const; - - virtual bool useShieldAnimations() const { return false; } - virtual void showWeapons(bool showWeapon) {} - virtual bool getCarriedLeftShown() const { return false; } - virtual void showCarriedLeft(bool show) {} - virtual void setWeaponGroup(const std::string& group, bool relativeDuration) {} - virtual void setVampire(bool vampire) {} - /// A value < 1 makes the animation translucent, 1.f = fully opaque - void setAlpha(float alpha); - virtual void setPitchFactor(float factor) {} - virtual void attachArrow() {} - virtual void detachArrow() {} - virtual void releaseArrow(float attackStrength) {} - virtual void enableHeadAnimation(bool enable) {} - // TODO: move outside of this class - /// Makes this object glow, by placing a Light in its center. - /// @param effect Controls the radius and intensity of the light. - virtual void setLightEffect(float effect); - - virtual void setHeadPitch(float pitchRadians); - virtual void setHeadYaw(float yawRadians); - virtual float getHeadPitch() const; - virtual float getHeadYaw() const; - - virtual void setUpperBodyYawRadians(float v) { mUpperBodyYawRadians = v; } - virtual void setLegsYawRadians(float v) { mLegsYawRadians = v; } - virtual float getUpperBodyYawRadians() const { return mUpperBodyYawRadians; } - virtual float getLegsYawRadians() const { return mLegsYawRadians; } - virtual void setBodyPitchRadians(float v) { mBodyPitchRadians = v; } - virtual float getBodyPitchRadians() const { return mBodyPitchRadians; } - - virtual void setAccurateAiming(bool enabled) {} - virtual bool canBeHarvested() const { return false; } - -private: - Animation(const Animation&); - void operator=(Animation&); -}; - -class ObjectAnimation : public Animation { -public: - ObjectAnimation(const MWWorld::Ptr& ptr, const std::string &model, Resource::ResourceSystem* resourceSystem, bool animated, bool allowLight); - - bool canBeHarvested() const override; -}; - -class UpdateVfxCallback : public osg::NodeCallback -{ -public: - UpdateVfxCallback(EffectParams& params) - : mFinished(false) - , mParams(params) - , mStartingTime(0) + private: + Animation(const Animation&); + void operator=(Animation&); + }; + + class ObjectAnimation : public Animation { - } + public: + ObjectAnimation(const MWWorld::Ptr& ptr, const std::string& model, Resource::ResourceSystem* resourceSystem, + bool animated, bool allowLight); - bool mFinished; - EffectParams mParams; + bool canBeHarvested() const override; + }; + + class UpdateVfxCallback : public SceneUtil::NodeCallback + { + public: + UpdateVfxCallback(EffectParams& params) + : mFinished(false) + , mParams(params) + , mStartingTime(0) + { + } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + bool mFinished; + EffectParams mParams; -private: - double mStartingTime; -}; + void operator()(osg::Node* node, osg::NodeVisitor* nv); + private: + double mStartingTime; + }; } #endif diff --git a/apps/openmw/mwrender/animationpriority.hpp b/apps/openmw/mwrender/animationpriority.hpp new file mode 100644 index 00000000000..048d29901e7 --- /dev/null +++ b/apps/openmw/mwrender/animationpriority.hpp @@ -0,0 +1,42 @@ +#ifndef GAME_RENDER_ANIMATIONPRIORITY_H +#define GAME_RENDER_ANIMATIONPRIORITY_H + +#include "blendmask.hpp" +#include "bonegroup.hpp" + +namespace MWRender +{ + /// Holds an animation priority value for each BoneGroup. + struct AnimPriority + { + /// Convenience constructor, initialises all priorities to the same value. + AnimPriority(int priority) + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + mPriority[i] = priority; + } + + bool operator==(const AnimPriority& other) const + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + if (other.mPriority[i] != mPriority[i]) + return false; + return true; + } + + int& operator[](BoneGroup n) { return mPriority[n]; } + + const int& operator[](BoneGroup n) const { return mPriority[n]; } + + bool contains(int priority) const + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + if (priority == mPriority[i]) + return true; + return false; + } + + int mPriority[sNumBlendMasks]; + }; +} +#endif diff --git a/apps/openmw/mwrender/animblendcontroller.cpp b/apps/openmw/mwrender/animblendcontroller.cpp new file mode 100644 index 00000000000..3d8b7b59d7f --- /dev/null +++ b/apps/openmw/mwrender/animblendcontroller.cpp @@ -0,0 +1,393 @@ +#include "animblendcontroller.hpp" +#include "rotatecontroller.hpp" + +#include + +#include + +#include +#include +#include + +namespace MWRender +{ + namespace + { + // Animation Easing/Blending functions + namespace Easings + { + float linear(float x) + { + return x; + } + + float sineOut(float x) + { + return std::sin((x * osg::PIf) / 2.f); + } + + float sineIn(float x) + { + return 1.f - std::cos((x * osg::PIf) / 2.f); + } + + float sineInOut(float x) + { + return -(std::cos(osg::PIf * x) - 1.f) / 2.f; + } + + float cubicOut(float t) + { + float t1 = 1.f - t; + return 1.f - (t1 * t1 * t1); // (1-t)^3 + } + + float cubicIn(float x) + { + return x * x * x; // x^3 + } + + float cubicInOut(float x) + { + if (x < 0.5f) + { + return 4.f * x * x * x; // 4x^3 + } + else + { + float x2 = -2.f * x + 2.f; + return 1.f - (x2 * x2 * x2) / 2.f; // (1 - (-2x + 2)^3)/2 + } + } + + float quartOut(float t) + { + float t1 = 1.f - t; + return 1.f - (t1 * t1 * t1 * t1); // (1-t)^4 + } + + float quartIn(float t) + { + return t * t * t * t; // t^4 + } + + float quartInOut(float x) + { + if (x < 0.5f) + { + return 8.f * x * x * x * x; // 8x^4 + } + else + { + float x2 = -2.f * x + 2.f; + return 1.f - (x2 * x2 * x2 * x2) / 2.f; // 1 - ((-2x + 2)^4)/2 + } + } + + float springOutGeneric(float x, float lambda) + { + // Higher lambda = lower swing amplitude. 1 = 150% swing amplitude. + // w is the frequency of oscillation in the easing func, controls the amount of overswing + const float w = 1.5f * osg::PIf; // 4.71238 + return 1.f - expf(-lambda * x) * std::cos(w * x); + } + + float springOutWeak(float x) + { + return springOutGeneric(x, 4.f); + } + + float springOutMed(float x) + { + return springOutGeneric(x, 3.f); + } + + float springOutStrong(float x) + { + return springOutGeneric(x, 2.f); + } + + float springOutTooMuch(float x) + { + return springOutGeneric(x, 1.f); + } + + const std::unordered_map easingsMap = { + { "linear", Easings::linear }, + { "sineOut", Easings::sineOut }, + { "sineIn", Easings::sineIn }, + { "sineInOut", Easings::sineInOut }, + { "cubicOut", Easings::cubicOut }, + { "cubicIn", Easings::cubicIn }, + { "cubicInOut", Easings::cubicInOut }, + { "quartOut", Easings::quartOut }, + { "quartIn", Easings::quartIn }, + { "quartInOut", Easings::quartInOut }, + { "springOutWeak", Easings::springOutWeak }, + { "springOutMed", Easings::springOutMed }, + { "springOutStrong", Easings::springOutStrong }, + { "springOutTooMuch", Easings::springOutTooMuch }, + }; + } + + osg::Vec3f vec3fLerp(float t, const osg::Vec3f& start, const osg::Vec3f& end) + { + return start + (end - start) * t; + } + } + + AnimBlendController::AnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& newState, const osg::ref_ptr& blendRules) + : mEasingFn(&Easings::sineOut) + { + setKeyframeTrack(keyframeTrack, newState, blendRules); + } + + AnimBlendController::AnimBlendController() + : mEasingFn(&Easings::sineOut) + { + } + + NifAnimBlendController::NifAnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& newState, const osg::ref_ptr& blendRules) + : AnimBlendController(keyframeTrack, newState, blendRules) + { + } + + BoneAnimBlendController::BoneAnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& newState, const osg::ref_ptr& blendRules) + : AnimBlendController(keyframeTrack, newState, blendRules) + { + } + + void AnimBlendController::setKeyframeTrack(const osg::ref_ptr& kft, + const AnimBlendStateData& newState, const osg::ref_ptr& blendRules) + { + // If animation has changed then start blending + if (newState.mGroupname != mAnimState.mGroupname || newState.mStartKey != mAnimState.mStartKey + || kft != mKeyframeTrack) + { + // Default blend settings + mBlendDuration = 0; + mEasingFn = &Easings::sineOut; + + if (blendRules) + { + // Finds a matching blend rule either in this or previous ruleset + auto blendRule = blendRules->findBlendingRule( + mAnimState.mGroupname, mAnimState.mStartKey, newState.mGroupname, newState.mStartKey); + + if (blendRule) + { + if (const auto it = Easings::easingsMap.find(blendRule->mEasing); it != Easings::easingsMap.end()) + { + mEasingFn = it->second; + mBlendDuration = blendRule->mDuration; + } + else + { + Log(Debug::Warning) + << "Warning: animation blending rule contains invalid easing type: " << blendRule->mEasing; + } + } + } + + mAnimBlendRules = blendRules; + mKeyframeTrack = kft; + mAnimState = newState; + mBlendTrigger = true; + } + } + + void AnimBlendController::calculateInterpFactor(float time) + { + if (mBlendDuration != 0) + mTimeFactor = std::min((time - mBlendStartTime) / mBlendDuration, 1.0f); + else + mTimeFactor = 1; + + mInterpActive = mTimeFactor < 1.0; + + if (mInterpActive) + mInterpFactor = mEasingFn(mTimeFactor); + else + mInterpFactor = 1.0f; + } + + void BoneAnimBlendController::gatherRecursiveBoneTransforms(osgAnimation::Bone* bone, bool isRoot) + { + // Incase group traversal encountered something that isnt a bone + if (!bone) + return; + + mBlendBoneTransforms[bone] = bone->getMatrix(); + + osg::Group* group = bone->asGroup(); + if (group) + { + for (unsigned int i = 0; i < group->getNumChildren(); ++i) + gatherRecursiveBoneTransforms(dynamic_cast(group->getChild(i)), false); + } + } + + void BoneAnimBlendController::applyBoneBlend(osgAnimation::Bone* bone) + { + // If we are done with interpolation then we can safely skip this as the bones are correct + if (!mInterpActive) + return; + + // Shouldn't happen, but potentially an edge case where a new bone was added + // between gatherRecursiveBoneTransforms and this update + // currently OpenMW will never do this + assert(mBlendBoneTransforms.find(bone) != mBlendBoneTransforms.end()); + + // Every frame the osgAnimation controller updates this + // so it is ok that we update it directly below + const osg::Matrixf& currentSampledMatrix = bone->getMatrix(); + const osg::Matrixf& lastSampledMatrix = mBlendBoneTransforms.at(bone); + + const osg::Vec3f scale = currentSampledMatrix.getScale(); + const osg::Quat rotation = currentSampledMatrix.getRotate(); + const osg::Vec3f translation = currentSampledMatrix.getTrans(); + + const osg::Quat blendRotation = lastSampledMatrix.getRotate(); + const osg::Vec3f blendTrans = lastSampledMatrix.getTrans(); + + osg::Quat lerpedRot; + lerpedRot.slerp(mInterpFactor, blendRotation, rotation); + + osg::Matrixf lerpedMatrix; + lerpedMatrix.makeRotate(lerpedRot); + lerpedMatrix.setTrans(vec3fLerp(mInterpFactor, blendTrans, translation)); + + // Scale is not lerped based on the idea that it is much more likely that scale animation will be used to + // instantly hide/show objects in which case the scale interpolation is undesirable. + lerpedMatrix = osg::Matrixd::scale(scale) * lerpedMatrix; + + // Apply new blended matrix + osgAnimation::Bone* boneParent = bone->getBoneParent(); + bone->setMatrix(lerpedMatrix); + if (boneParent) + bone->setMatrixInSkeletonSpace(lerpedMatrix * boneParent->getMatrixInSkeletonSpace()); + else + bone->setMatrixInSkeletonSpace(lerpedMatrix); + } + + void BoneAnimBlendController::operator()(osgAnimation::Bone* node, osg::NodeVisitor* nv) + { + // HOW THIS WORKS: This callback method is called only for bones with attached keyframe controllers + // such as bip01, bip01 spine1 etc. The child bones of these controllers have their own callback wrapper + // which will call this instance's applyBoneBlend for each child bone. The order of update is important + // as the blending calculations expect the bone's skeleton matrix to be at the sample point + float time = nv->getFrameStamp()->getSimulationTime(); + assert(node != nullptr); + + if (mBlendTrigger) + { + mBlendTrigger = false; + mBlendStartTime = time; + } + + calculateInterpFactor(time); + + if (mInterpActive) + applyBoneBlend(node); + + SceneUtil::NodeCallback::traverse(node, nv); + } + + void NifAnimBlendController::operator()(NifOsg::MatrixTransform* node, osg::NodeVisitor* nv) + { + // HOW THIS WORKS: The actual retrieval of the bone transformation based on animation is done by the + // KeyframeController (mKeyframeTrack). The KeyframeController retreives time data (playback position) every + // frame from controller's input (getInputValue(nv)) which is bound to an appropriate AnimationState time value + // in Animation.cpp. Animation.cpp ultimately manages animation playback via updating AnimationState objects and + // determines when and what should be playing. + // This controller exploits KeyframeController to get transformations and upon animation change blends from + // the last known position to the new animated one. + + auto [translation, rotation, scale] = mKeyframeTrack->getCurrentTransformation(nv); + + float time = nv->getFrameStamp()->getSimulationTime(); + + if (mBlendTrigger) + { + mBlendTrigger = false; + mBlendStartTime = time; + + // Nif mRotationScale is used here because it's unaffected by the side-effects of RotationController + mBlendStartRot = node->mRotationScale.toOsgMatrix().getRotate(); + mBlendStartTrans = node->getMatrix().getTrans(); + mBlendStartScale = node->mScale; + + // Subtract any rotate controller's offset from start transform (if it appears after this callback) + // this is required otherwise the blend start will be with an offset, then offset could be applied again + // fixes an issue with camera jumping during first person sneak jumping camera + osg::Callback* updateCb = node->getUpdateCallback()->getNestedCallback(); + while (updateCb) + { + MWRender::RotateController* rotateController = dynamic_cast(updateCb); + if (rotateController) + { + const osg::Quat& rotate = rotateController->getRotate(); + const osg::Vec3f& offset = rotateController->getOffset(); + + osg::NodePathList nodepaths = node->getParentalNodePaths(rotateController->getRelativeTo()); + osg::Quat worldOrient; + if (!nodepaths.empty()) + { + osg::Matrixf worldMat = osg::computeLocalToWorld(nodepaths[0]); + worldOrient = worldMat.getRotate(); + } + + worldOrient = worldOrient * rotate.inverse(); + const osg::Quat worldOrientInverse = worldOrient.inverse(); + + mBlendStartTrans -= worldOrientInverse * offset; + } + + updateCb = updateCb->getNestedCallback(); + } + } + + calculateInterpFactor(time); + + if (mInterpActive) + { + if (rotation) + { + osg::Quat lerpedRot; + lerpedRot.slerp(mInterpFactor, mBlendStartRot, *rotation); + node->setRotation(lerpedRot); + } + else + { + // This is necessary to prevent first person animation glitching out + node->setRotation(node->mRotationScale); + } + + if (translation) + { + osg::Vec3f lerpedTrans = vec3fLerp(mInterpFactor, mBlendStartTrans, *translation); + node->setTranslation(lerpedTrans); + } + } + else + { + if (translation) + node->setTranslation(*translation); + + if (rotation) + node->setRotation(*rotation); + else + node->setRotation(node->mRotationScale); + } + + if (scale) + // Scale is not lerped based on the idea that it is much more likely that scale animation will be used to + // instantly hide/show objects in which case the scale interpolation is undesirable. + node->setScale(*scale); + + SceneUtil::NodeCallback::traverse(node, nv); + } +} diff --git a/apps/openmw/mwrender/animblendcontroller.hpp b/apps/openmw/mwrender/animblendcontroller.hpp new file mode 100644 index 00000000000..40a5d7582f0 --- /dev/null +++ b/apps/openmw/mwrender/animblendcontroller.hpp @@ -0,0 +1,142 @@ +#ifndef OPENMW_MWRENDER_ANIMBLENDCONTROLLER_H +#define OPENMW_MWRENDER_ANIMBLENDCONTROLLER_H + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace MWRender +{ + typedef float (*EasingFn)(float); + + struct AnimBlendStateData + { + std::string mGroupname; + std::string mStartKey; + }; + + class AnimBlendController : public SceneUtil::Controller + { + public: + AnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& animState, const osg::ref_ptr& blendRules); + + AnimBlendController(); + + void setKeyframeTrack(const osg::ref_ptr& kft, + const AnimBlendStateData& animState, const osg::ref_ptr& blendRules); + + bool getBlendTrigger() const { return mBlendTrigger; } + + protected: + EasingFn mEasingFn; + float mBlendDuration = 0.0f; + float mBlendStartTime = 0.0f; + float mTimeFactor = 0.0f; + float mInterpFactor = 0.0f; + + bool mBlendTrigger = false; + bool mInterpActive = false; + + AnimBlendStateData mAnimState; + osg::ref_ptr mAnimBlendRules; + osg::ref_ptr mKeyframeTrack; + + std::unordered_map mBlendBoneTransforms; + + inline void calculateInterpFactor(float time); + }; + + class NifAnimBlendController : public SceneUtil::NodeCallback, + public AnimBlendController + { + public: + NifAnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& animState, const osg::ref_ptr& blendRules); + + NifAnimBlendController() {} + + NifAnimBlendController(const NifAnimBlendController& other, const osg::CopyOp&) + : NifAnimBlendController(other.mKeyframeTrack, other.mAnimState, other.mAnimBlendRules) + { + } + + META_Object(MWRender, NifAnimBlendController) + + void operator()(NifOsg::MatrixTransform* node, osg::NodeVisitor* nv); + + osg::Callback* getAsCallback() { return this; } + + private: + osg::Quat mBlendStartRot; + osg::Vec3f mBlendStartTrans; + float mBlendStartScale = 0.0f; + }; + + class BoneAnimBlendController : public SceneUtil::NodeCallback, + public AnimBlendController + { + public: + BoneAnimBlendController(const osg::ref_ptr& keyframeTrack, + const AnimBlendStateData& animState, const osg::ref_ptr& blendRules); + + BoneAnimBlendController() {} + + BoneAnimBlendController(const BoneAnimBlendController& other, const osg::CopyOp&) + : BoneAnimBlendController(other.mKeyframeTrack, other.mAnimState, other.mAnimBlendRules) + { + } + + void gatherRecursiveBoneTransforms(osgAnimation::Bone* parent, bool isRoot = true); + void applyBoneBlend(osgAnimation::Bone* parent); + + META_Object(MWRender, BoneAnimBlendController) + + void operator()(osgAnimation::Bone* node, osg::NodeVisitor* nv); + + osg::Callback* getAsCallback() { return this; } + }; + + // Assigned to child bones with an instance of AnimBlendController + class BoneAnimBlendControllerWrapper : public osg::Callback + { + public: + BoneAnimBlendControllerWrapper(osg::ref_ptr rootCallback, osgAnimation::Bone* node) + : mRootCallback(std::move(rootCallback)) + , mNode(node) + { + } + + BoneAnimBlendControllerWrapper() {} + + BoneAnimBlendControllerWrapper(const BoneAnimBlendControllerWrapper& copy, const osg::CopyOp&) + : mRootCallback(copy.mRootCallback) + , mNode(copy.mNode) + { + } + + META_Object(MWRender, BoneAnimBlendControllerWrapper) + + bool run(osg::Object* object, osg::Object* data) override + { + mRootCallback->applyBoneBlend(mNode); + traverse(object, data); + return true; + } + + private: + osg::ref_ptr mRootCallback; + osgAnimation::Bone* mNode{ nullptr }; + }; +} + +#endif diff --git a/apps/openmw/mwrender/blendmask.hpp b/apps/openmw/mwrender/blendmask.hpp new file mode 100644 index 00000000000..f140814d8d9 --- /dev/null +++ b/apps/openmw/mwrender/blendmask.hpp @@ -0,0 +1,22 @@ +#ifndef GAME_RENDER_BLENDMASK_H +#define GAME_RENDER_BLENDMASK_H + +#include + +namespace MWRender +{ + enum BlendMask + { + BlendMask_LowerBody = 1 << 0, + BlendMask_Torso = 1 << 1, + BlendMask_LeftArm = 1 << 2, + BlendMask_RightArm = 1 << 3, + + BlendMask_UpperBody = BlendMask_Torso | BlendMask_LeftArm | BlendMask_RightArm, + + BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody + }; + /* This is the number of *discrete* blend masks. */ + static constexpr size_t sNumBlendMasks = 4; +} +#endif diff --git a/apps/openmw/mwrender/bonegroup.hpp b/apps/openmw/mwrender/bonegroup.hpp new file mode 100644 index 00000000000..2afedade861 --- /dev/null +++ b/apps/openmw/mwrender/bonegroup.hpp @@ -0,0 +1,16 @@ +#ifndef GAME_RENDER_BONEGROUP_H +#define GAME_RENDER_BONEGROUP_H + +namespace MWRender +{ + enum BoneGroup + { + BoneGroup_LowerBody = 0, + BoneGroup_Torso, + BoneGroup_LeftArm, + BoneGroup_RightArm, + + Num_BoneGroups + }; +} +#endif diff --git a/apps/openmw/mwrender/bulletdebugdraw.cpp b/apps/openmw/mwrender/bulletdebugdraw.cpp index 207b0ee504d..3e0383b567a 100644 --- a/apps/openmw/mwrender/bulletdebugdraw.cpp +++ b/apps/openmw/mwrender/bulletdebugdraw.cpp @@ -4,146 +4,219 @@ #include #include +#include #include #include +#include #include +#include #include #include #include "bulletdebugdraw.hpp" #include "vismask.hpp" +#include +#include + +#include "../mwbase/environment.hpp" + namespace MWRender { -DebugDrawer::DebugDrawer(osg::ref_ptr parentNode, btCollisionWorld *world, int debugMode) - : mParentNode(parentNode), - mWorld(world) -{ - setDebugMode(debugMode); -} + DebugDrawer::DebugDrawer(osg::ref_ptr parentNode, btCollisionWorld* world, int debugMode) + : mParentNode(std::move(parentNode)) + , mWorld(world) + { + DebugDrawer::setDebugMode(debugMode); + } -void DebugDrawer::createGeometry() -{ - if (!mGeometry) + void DebugDrawer::createGeometry() { - mGeometry = new osg::Geometry; - mGeometry->setNodeMask(Mask_Debug); - - mVertices = new osg::Vec3Array; - mColors = new osg::Vec4Array; - - mDrawArrays = new osg::DrawArrays(osg::PrimitiveSet::LINES); - - mGeometry->setUseDisplayList(false); - mGeometry->setVertexArray(mVertices); - mGeometry->setColorArray(mColors); - mGeometry->setColorBinding(osg::Geometry::BIND_PER_VERTEX); - mGeometry->setDataVariance(osg::Object::DYNAMIC); - mGeometry->addPrimitiveSet(mDrawArrays); - - mParentNode->addChild(mGeometry); - - auto* stateSet = new osg::StateSet; - stateSet->setAttributeAndModes(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), osg::StateAttribute::ON); - mShapesRoot = new osg::Group; - mShapesRoot->setStateSet(stateSet); - mShapesRoot->setDataVariance(osg::Object::DYNAMIC); - mShapesRoot->setNodeMask(Mask_Debug); - mParentNode->addChild(mShapesRoot); + if (!mLinesGeometry) + { + mLinesGeometry = new osg::Geometry; + mTrisGeometry = new osg::Geometry; + mLinesGeometry->setNodeMask(Mask_Debug); + mTrisGeometry->setNodeMask(Mask_Debug); + + mLinesVertices = new osg::Vec3Array; + mTrisVertices = new osg::Vec3Array; + mLinesColors = new osg::Vec4Array; + + mLinesDrawArrays = new osg::DrawArrays(osg::PrimitiveSet::LINES); + mTrisDrawArrays = new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES); + + mLinesGeometry->setUseDisplayList(false); + mLinesGeometry->setVertexArray(mLinesVertices); + mLinesGeometry->setColorArray(mLinesColors); + mLinesGeometry->setColorBinding(osg::Geometry::BIND_PER_VERTEX); + mLinesGeometry->setDataVariance(osg::Object::DYNAMIC); + mLinesGeometry->addPrimitiveSet(mLinesDrawArrays); + + mTrisGeometry->setUseDisplayList(false); + mTrisGeometry->setVertexArray(mTrisVertices); + mTrisGeometry->setDataVariance(osg::Object::DYNAMIC); + mTrisGeometry->addPrimitiveSet(mTrisDrawArrays); + + mParentNode->addChild(mLinesGeometry); + mParentNode->addChild(mTrisGeometry); + + auto* stateSet = new osg::StateSet; + stateSet->setAttributeAndModes( + new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), + osg::StateAttribute::ON); + stateSet->setAttributeAndModes(new osg::PolygonOffset( + SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0, SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0)); + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + stateSet->setAttribute(material); + mLinesGeometry->setStateSet(stateSet); + mTrisGeometry->setStateSet(stateSet); + mShapesRoot = new osg::Group; + mShapesRoot->setStateSet(stateSet); + mShapesRoot->setDataVariance(osg::Object::DYNAMIC); + mShapesRoot->setNodeMask(Mask_Debug); + mParentNode->addChild(mShapesRoot); + + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mLinesGeometry, "debug"); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mTrisGeometry, "debug"); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mShapesRoot, "debug"); + } } -} -void DebugDrawer::destroyGeometry() -{ - if (mGeometry) + void DebugDrawer::destroyGeometry() { - mParentNode->removeChild(mGeometry); - mParentNode->removeChild(mShapesRoot); - mGeometry = nullptr; - mVertices = nullptr; - mDrawArrays = nullptr; + if (mLinesGeometry) + { + mParentNode->removeChild(mLinesGeometry); + mParentNode->removeChild(mTrisGeometry); + mParentNode->removeChild(mShapesRoot); + mLinesGeometry = nullptr; + mLinesVertices = nullptr; + mLinesColors = nullptr; + mLinesDrawArrays = nullptr; + mTrisGeometry = nullptr; + mTrisVertices = nullptr; + mTrisDrawArrays = nullptr; + } } -} -DebugDrawer::~DebugDrawer() -{ - destroyGeometry(); -} + DebugDrawer::~DebugDrawer() + { + destroyGeometry(); + } -void DebugDrawer::step() -{ - if (mDebugOn) + void DebugDrawer::step() { - mVertices->clear(); - mColors->clear(); - mShapesRoot->removeChildren(0, mShapesRoot->getNumChildren()); - mWorld->debugDrawWorld(); - showCollisions(); - mDrawArrays->setCount(mVertices->size()); - mVertices->dirty(); - mColors->dirty(); - mGeometry->dirtyBound(); + if (mDebugOn) + { + mLinesVertices->clear(); + mTrisVertices->clear(); + mLinesColors->clear(); + mShapesRoot->removeChildren(0, mShapesRoot->getNumChildren()); + mWorld->debugDrawWorld(); + showCollisions(); + mLinesDrawArrays->setCount(mLinesVertices->size()); + mTrisDrawArrays->setCount(mTrisVertices->size()); + mLinesVertices->dirty(); + mTrisVertices->dirty(); + mLinesColors->dirty(); + mLinesGeometry->dirtyBound(); + mTrisGeometry->dirtyBound(); + } } -} -void DebugDrawer::drawLine(const btVector3 &from, const btVector3 &to, const btVector3 &color) -{ - mVertices->push_back(Misc::Convert::toOsg(from)); - mVertices->push_back(Misc::Convert::toOsg(to)); - mColors->push_back({1,1,1,1}); - mColors->push_back({1,1,1,1}); -} + void DebugDrawer::drawLine(const btVector3& from, const btVector3& to, const btVector3& color) + { + mLinesVertices->push_back(Misc::Convert::toOsg(from)); + mLinesVertices->push_back(Misc::Convert::toOsg(to)); + mLinesColors->push_back({ 1, 1, 1, 1 }); + mLinesColors->push_back({ 1, 1, 1, 1 }); + +#if BT_BULLET_VERSION < 317 + size_t size = mLinesVertices->size(); + if (size >= 6 && (*mLinesVertices)[size - 1] == (*mLinesVertices)[size - 6] + && (*mLinesVertices)[size - 2] == (*mLinesVertices)[size - 3] + && (*mLinesVertices)[size - 4] == (*mLinesVertices)[size - 5]) + { + mTrisVertices->push_back(mLinesVertices->back()); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mTrisVertices->push_back(mLinesVertices->back()); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mTrisVertices->push_back(mLinesVertices->back()); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + } +#endif + } -void DebugDrawer::addCollision(const btVector3& orig, const btVector3& normal) -{ - mCollisionViews.emplace_back(orig, normal); -} + void DebugDrawer::drawTriangle( + const btVector3& v0, const btVector3& v1, const btVector3& v2, const btVector3& color, btScalar) + { + mTrisVertices->push_back(Misc::Convert::toOsg(v0)); + mTrisVertices->push_back(Misc::Convert::toOsg(v1)); + mTrisVertices->push_back(Misc::Convert::toOsg(v2)); + } -void DebugDrawer::showCollisions() -{ - const auto now = std::chrono::steady_clock::now(); - for (auto& [from, to , created] : mCollisionViews) + void DebugDrawer::addCollision(const btVector3& orig, const btVector3& normal) + { + mCollisionViews.emplace_back(orig, normal); + } + + void DebugDrawer::showCollisions() { - if (now - created < std::chrono::seconds(2)) + const auto now = std::chrono::steady_clock::now(); + for (auto& [from, to, created] : mCollisionViews) { - mVertices->push_back(Misc::Convert::toOsg(from)); - mVertices->push_back(Misc::Convert::toOsg(to)); - mColors->push_back({1,0,0,1}); - mColors->push_back({1,0,0,1}); + if (now - created < std::chrono::seconds(2)) + { + mLinesVertices->push_back(Misc::Convert::toOsg(from)); + mLinesVertices->push_back(Misc::Convert::toOsg(to)); + mLinesColors->push_back({ 1, 0, 0, 1 }); + mLinesColors->push_back({ 1, 0, 0, 1 }); + } } - } - mCollisionViews.erase(std::remove_if(mCollisionViews.begin(), mCollisionViews.end(), + mCollisionViews.erase( + std::remove_if(mCollisionViews.begin(), mCollisionViews.end(), [&now](const CollisionView& view) { return now - view.mCreated >= std::chrono::seconds(2); }), mCollisionViews.end()); -} + } -void DebugDrawer::drawSphere(btScalar radius, const btTransform& transform, const btVector3& color) -{ - auto* geom = new osg::ShapeDrawable(new osg::Sphere(Misc::Convert::toOsg(transform.getOrigin()), radius)); - geom->setColor(osg::Vec4(1, 1, 1, 1)); - mShapesRoot->addChild(geom); -} + void DebugDrawer::drawSphere(btScalar radius, const btTransform& transform, const btVector3& color) + { + auto* geom = new osg::ShapeDrawable(new osg::Sphere(Misc::Convert::toOsg(transform.getOrigin()), radius)); + geom->setColor(osg::Vec4(1, 1, 1, 1)); + mShapesRoot->addChild(geom); + } -void DebugDrawer::reportErrorWarning(const char *warningString) -{ - Log(Debug::Warning) << warningString; -} + void DebugDrawer::reportErrorWarning(const char* warningString) + { + Log(Debug::Warning) << warningString; + } -void DebugDrawer::setDebugMode(int isOn) -{ - mDebugOn = (isOn != 0); + void DebugDrawer::setDebugMode(int isOn) + { + mDebugOn = (isOn != 0); - if (!mDebugOn) - destroyGeometry(); - else - createGeometry(); -} + if (!mDebugOn) + destroyGeometry(); + else + createGeometry(); + } -int DebugDrawer::getDebugMode() const -{ - return mDebugOn; -} + int DebugDrawer::getDebugMode() const + { + return mDebugOn; + } } diff --git a/apps/openmw/mwrender/bulletdebugdraw.hpp b/apps/openmw/mwrender/bulletdebugdraw.hpp index b24640427dc..662fc9f14e5 100644 --- a/apps/openmw/mwrender/bulletdebugdraw.hpp +++ b/apps/openmw/mwrender/bulletdebugdraw.hpp @@ -4,9 +4,9 @@ #include #include -#include #include #include +#include #include @@ -21,60 +21,71 @@ namespace osg namespace MWRender { -class DebugDrawer : public btIDebugDraw -{ -private: - struct CollisionView + class DebugDrawer : public btIDebugDraw { - btVector3 mOrig; - btVector3 mEnd; - std::chrono::time_point mCreated; - CollisionView(btVector3 orig, btVector3 normal) : mOrig(orig), mEnd(orig + normal * 20), mCreated(std::chrono::steady_clock::now()) {}; + private: + struct CollisionView + { + btVector3 mOrig; + btVector3 mEnd; + std::chrono::time_point mCreated; + CollisionView(btVector3 orig, btVector3 normal) + : mOrig(orig) + , mEnd(orig + normal * 20) + , mCreated(std::chrono::steady_clock::now()) + { + } + }; + std::vector mCollisionViews; + osg::ref_ptr mShapesRoot; + + protected: + osg::ref_ptr mParentNode; + btCollisionWorld* mWorld; + osg::ref_ptr mLinesGeometry; + osg::ref_ptr mTrisGeometry; + osg::ref_ptr mLinesVertices; + osg::ref_ptr mTrisVertices; + osg::ref_ptr mLinesColors; + osg::ref_ptr mLinesDrawArrays; + osg::ref_ptr mTrisDrawArrays; + + bool mDebugOn; + + void createGeometry(); + void destroyGeometry(); + + public: + DebugDrawer(osg::ref_ptr parentNode, btCollisionWorld* world, int debugMode = 1); + ~DebugDrawer(); + + void step(); + + void drawLine(const btVector3& from, const btVector3& to, const btVector3& color) override; + + void drawTriangle( + const btVector3& v0, const btVector3& v1, const btVector3& v2, const btVector3& color, btScalar) override; + + void addCollision(const btVector3& orig, const btVector3& normal); + + void showCollisions(); + + void drawContactPoint(const btVector3& PointOnB, const btVector3& normalOnB, btScalar distance, int lifeTime, + const btVector3& color) override + { + } + void drawSphere(btScalar radius, const btTransform& transform, const btVector3& color) override; + + void reportErrorWarning(const char* warningString) override; + + void draw3dText(const btVector3& location, const char* textString) override {} + + // 0 for off, anything else for on. + void setDebugMode(int isOn) override; + + // 0 for off, anything else for on. + int getDebugMode() const override; }; - std::vector mCollisionViews; - osg::ref_ptr mShapesRoot; - -protected: - osg::ref_ptr mParentNode; - btCollisionWorld *mWorld; - osg::ref_ptr mGeometry; - osg::ref_ptr mVertices; - osg::ref_ptr mColors; - osg::ref_ptr mDrawArrays; - - bool mDebugOn; - - void createGeometry(); - void destroyGeometry(); - -public: - - DebugDrawer(osg::ref_ptr parentNode, btCollisionWorld *world, int debugMode = 1); - ~DebugDrawer(); - - void step(); - - void drawLine(const btVector3& from,const btVector3& to,const btVector3& color) override; - - void addCollision(const btVector3& orig, const btVector3& normal); - - void showCollisions(); - - void drawContactPoint(const btVector3& PointOnB,const btVector3& normalOnB,btScalar distance,int lifeTime,const btVector3& color) override {}; - void drawSphere(btScalar radius, const btTransform& transform, const btVector3& color) override; - - void reportErrorWarning(const char* warningString) override; - - void draw3dText(const btVector3& location,const char* textString) override {} - - //0 for off, anything else for on. - void setDebugMode(int isOn) override; - - //0 for off, anything else for on. - int getDebugMode() const override; - -}; - } diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index 3e5d1d0b31b..1c163a67011 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -3,8 +3,8 @@ #include #include +#include #include -#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -14,9 +14,7 @@ #include "../mwworld/ptr.hpp" #include "../mwworld/refdata.hpp" -#include "../mwmechanics/drawstate.hpp" #include "../mwmechanics/movement.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwphysics/raycasting.hpp" @@ -25,73 +23,58 @@ namespace { -class UpdateRenderCameraCallback : public osg::NodeCallback -{ -public: - UpdateRenderCameraCallback(MWRender::Camera* cam) - : mCamera(cam) - { - } - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + class UpdateRenderCameraCallback : public SceneUtil::NodeCallback { - osg::Camera* cam = static_cast(node); + public: + UpdateRenderCameraCallback(MWRender::Camera* cam) + : mCamera(cam) + { + } - // traverse first to update animations, in case the camera is attached to an animated node - traverse(node, nv); + void operator()(osg::Camera* cam, osg::NodeVisitor* nv) + { + // traverse first to update animations, in case the camera is attached to an animated node + traverse(cam, nv); - mCamera->updateCamera(cam); - } + mCamera->updateCamera(cam); + } -private: - MWRender::Camera* mCamera; -}; + private: + MWRender::Camera* mCamera; + }; } namespace MWRender { - Camera::Camera (osg::Camera* camera) - : mHeightScale(1.f), - mCamera(camera), - mAnimation(nullptr), - mFirstPersonView(true), - mMode(Mode::Normal), - mVanityAllowed(true), - mStandingPreviewAllowed(Settings::Manager::getBool("preview if stand still", "Camera")), - mDeferredRotationAllowed(Settings::Manager::getBool("deferred preview rotation", "Camera")), - mNearest(30.f), - mFurthest(800.f), - mIsNearest(false), - mHeight(124.f), - mBaseCameraDistance(Settings::Manager::getFloat("third person camera distance", "Camera")), - mPitch(0.f), - mYaw(0.f), - mRoll(0.f), - mVanityToggleQueued(false), - mVanityToggleQueuedValue(false), - mViewModeToggleQueued(false), - mCameraDistance(0.f), - mMaxNextCameraDistance(800.f), - mFocalPointCurrentOffset(osg::Vec2d()), - mFocalPointTargetOffset(osg::Vec2d()), - mFocalPointTransitionSpeedCoef(1.f), - mSkipFocalPointTransition(true), - mPreviousTransitionInfluence(0.f), - mSmoothedSpeed(0.f), - mZoomOutWhenMoveCoef(Settings::Manager::getFloat("zoom out when move coef", "Camera")), - mDynamicCameraDistanceEnabled(false), - mShowCrosshairInThirdPersonMode(false), - mHeadBobbingEnabled(Settings::Manager::getBool("head bobbing", "Camera")), - mHeadBobbingOffset(0.f), - mHeadBobbingWeight(0.f), - mTotalMovement(0.f), - mDeferredRotation(osg::Vec3f()), - mDeferredRotationDisabled(false) + Camera::Camera(osg::Camera* camera) + : mHeightScale(1.f) + , mCollisionType( + (MWPhysics::CollisionType::CollisionType_Default & ~MWPhysics::CollisionType::CollisionType_Actor) + | MWPhysics::CollisionType_CameraOnly) + , mCamera(camera) + , mAnimation(nullptr) + , mFirstPersonView(true) + , mMode(Mode::FirstPerson) + , mVanityAllowed(true) + , mDeferredRotationAllowed(true) + , mProcessViewChange(false) + , mHeight(124.f) + , mPitch(0.f) + , mYaw(0.f) + , mRoll(0.f) + , mCameraDistance(0.f) + , mPreferredCameraDistance(0.f) + , mFocalPointCurrentOffset(osg::Vec2d()) + , mFocalPointTargetOffset(osg::Vec2d()) + , mFocalPointTransitionSpeedCoef(1.f) + , mSkipFocalPointTransition(true) + , mPreviousTransitionInfluence(0.f) + , mShowCrosshair(false) + , mDeferredRotation(osg::Vec3f()) + , mDeferredRotationDisabled(false) { - mCameraDistance = mBaseCameraDistance; - mUpdateCallback = new UpdateRenderCameraCallback(this); mCamera->addUpdateCallback(mUpdateCallback); } @@ -101,7 +84,7 @@ namespace MWRender mCamera->removeUpdateCallback(mUpdateCallback); } - osg::Vec3d Camera::getFocalPoint() const + osg::Vec3d Camera::calculateTrackedPosition() const { if (!mTrackingNode) return osg::Vec3d(); @@ -109,194 +92,159 @@ namespace MWRender if (nodepaths.empty()) return osg::Vec3d(); osg::Matrix worldMat = osg::computeLocalToWorld(nodepaths[0]); - - osg::Vec3d position = worldMat.getTrans(); - if (isFirstPerson()) - position.z() += mHeadBobbingOffset; - else - { - position.z() += mHeight * mHeightScale; - - // We subtract 10.f here and add it within focalPointOffset in order to avoid camera clipping through ceiling. - // Needed because character's head can be a bit higher than collision area. - position.z() -= 10.f; - - position += getFocalPointOffset() + mFocalPointAdjustment; - } - return position; + osg::Vec3d res = worldMat.getTrans(); + if (mMode != Mode::FirstPerson) + res.z() += mHeight * mHeightScale; + return res; } osg::Vec3d Camera::getFocalPointOffset() const { - osg::Vec3d offset(0, 0, 10.f); - offset.x() += mFocalPointCurrentOffset.x() * cos(getYaw()); - offset.y() += mFocalPointCurrentOffset.x() * sin(getYaw()); - offset.z() += mFocalPointCurrentOffset.y(); + osg::Vec3d offset; + offset.x() = mFocalPointCurrentOffset.x() * cos(mYaw); + offset.y() = mFocalPointCurrentOffset.x() * sin(mYaw); + offset.z() = mFocalPointCurrentOffset.y(); return offset; } - void Camera::getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const - { - focal = getFocalPoint(); - osg::Vec3d offset(0,0,0); - if (!isFirstPerson()) - { - osg::Quat orient = osg::Quat(getPitch(), osg::Vec3d(1,0,0)) * osg::Quat(getYaw(), osg::Vec3d(0,0,1)); - offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); - } - camera = focal + offset; - } - - void Camera::updateCamera(osg::Camera *cam) + void Camera::updateCamera(osg::Camera* cam) { - osg::Vec3d focal, position; - getPosition(focal, position); - - osg::Quat orient = osg::Quat(mRoll, osg::Vec3d(0, 1, 0)) * osg::Quat(mPitch, osg::Vec3d(1, 0, 0)) * osg::Quat(mYaw, osg::Vec3d(0, 0, 1)); - osg::Vec3d forward = orient * osg::Vec3d(0,1,0); - osg::Vec3d up = orient * osg::Vec3d(0,0,1); - - cam->setViewMatrixAsLookAt(position, position + forward, up); - } + osg::Quat orient = getOrient(); + osg::Vec3d forward = orient * osg::Vec3d(0, 1, 0); + osg::Vec3d up = orient * osg::Vec3d(0, 0, 1); - void Camera::updateHeadBobbing(float duration) { - static const float doubleStepLength = Settings::Manager::getFloat("head bobbing step", "Camera") * 2; - static const float stepHeight = Settings::Manager::getFloat("head bobbing height", "Camera"); - static const float maxRoll = osg::DegreesToRadians(Settings::Manager::getFloat("head bobbing roll", "Camera")); - - if (MWBase::Environment::get().getWorld()->isOnGround(mTrackingPtr)) - mHeadBobbingWeight = std::min(mHeadBobbingWeight + duration * 5, 1.f); - else - mHeadBobbingWeight = std::max(mHeadBobbingWeight - duration * 5, 0.f); - - float doubleStepState = mTotalMovement / doubleStepLength - std::floor(mTotalMovement / doubleStepLength); // from 0 to 1 during 2 steps - float stepState = std::abs(doubleStepState * 4 - 2) - 1; // from -1 to 1 on even steps and from 1 to -1 on odd steps - float effect = (1 - std::cos(stepState * osg::DegreesToRadians(30.f))) * 7.5f; // range from 0 to 1 - float coef = std::min(mSmoothedSpeed / 300.f, 1.f) * mHeadBobbingWeight; - mHeadBobbingOffset = (0.5f - effect) * coef * stepHeight; // range from -stepHeight/2 to stepHeight/2 - mRoll = osg::sign(stepState) * effect * coef * maxRoll; // range from -maxRoll to maxRoll - } - - void Camera::reset() - { - togglePreviewMode(false); - toggleVanityMode(false); - if (!mFirstPersonView) - toggleViewMode(); - } - - void Camera::rotateCamera(float pitch, float yaw, bool adjust) - { - if (adjust) + osg::Vec3d pos = mPosition; + if (mMode == Mode::FirstPerson) { - pitch += getPitch(); - yaw += getYaw(); + // It is a hack. Camera position depends on neck animation. + // Animations are updated in OSG cull traversal and in order to avoid 1 frame delay we + // recalculate the position here. Note that it becomes different from mPosition that + // is used in other parts of the code. + // TODO: detach camera from OSG animation and get rid of this hack. + osg::Vec3d recalculatedTrackedPosition = calculateTrackedPosition(); + pos = calculateFirstPersonPosition(recalculatedTrackedPosition); } - setYaw(yaw); - setPitch(pitch); + cam->setViewMatrixAsLookAt(pos, pos + forward, up); + mViewMatrix = cam->getViewMatrix(); + mProjectionMatrix = cam->getProjectionMatrix(); } void Camera::update(float duration, bool paused) { - if (mAnimation->upperBodyReady()) - { - // Now process the view changes we queued earlier - if (mVanityToggleQueued) - { - toggleVanityMode(mVanityToggleQueuedValue); - mVanityToggleQueued = false; - } - if (mViewModeToggleQueued) - { - togglePreviewMode(false); - toggleViewMode(); - mViewModeToggleQueued = false; - } - } - - if (paused) - return; + mLockPitch = mLockYaw = false; + if (mQueuedMode && mAnimation->upperBodyReady()) + setMode(*mQueuedMode); + if (mProcessViewChange) + processViewChange(); // only show the crosshair in game mode - MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); - wm->showCrosshair(!wm->isGuiMode() && mMode != Mode::Preview && mMode != Mode::Vanity - && (mFirstPersonView || mShowCrosshairInThirdPersonMode)); - - if(mMode == Mode::Vanity) - rotateCamera(0.f, osg::DegreesToRadians(3.f * duration), true); + MWBase::WindowManager* wm = MWBase::Environment::get().getWindowManager(); + wm->showCrosshair(!wm->isGuiMode() && mShowCrosshair); - if (isFirstPerson() && mHeadBobbingEnabled) - updateHeadBobbing(duration); - else - mRoll = mHeadBobbingOffset = 0; - - updateFocalPointOffset(duration); + if (!paused) + updateFocalPointOffset(duration); updatePosition(); + } - float speed = mTrackingPtr.getClass().getCurrentSpeed(mTrackingPtr); - mTotalMovement += speed * duration; - speed /= (1.f + speed / 500.f); - float maxDelta = 300.f * duration; - mSmoothedSpeed += osg::clampBetween(speed - mSmoothedSpeed, -maxDelta, maxDelta); - - mMaxNextCameraDistance = mCameraDistance + duration * (100.f + mBaseCameraDistance); - updateStandingPreviewMode(); + osg::Vec3d Camera::calculateFirstPersonPosition(const osg::Vec3d& trackedPosition) const + { + osg::Vec3d res = trackedPosition; + osg::Vec2f horizontalOffset + = Misc::rotateVec2f(osg::Vec2f(mFirstPersonOffset.x(), mFirstPersonOffset.y()), mYaw); + res.x() += horizontalOffset.x(); + res.y() += horizontalOffset.y(); + res.z() += mFirstPersonOffset.z(); + return res; } void Camera::updatePosition() { - mFocalPointAdjustment = osg::Vec3d(); - if (isFirstPerson()) + mTrackedPosition = calculateTrackedPosition(); + if (mMode == Mode::Static) + return; + if (mMode == Mode::FirstPerson) + { + mPosition = calculateFirstPersonPosition(mTrackedPosition); + mCameraDistance = 0; return; + } - const float cameraObstacleLimit = 5.0f; - const float focalObstacleLimit = 10.f; + constexpr float cameraObstacleLimit = 5.0f; + constexpr float focalObstacleLimit = 10.f; const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); // Adjust focal point to prevent clipping. - osg::Vec3d focal = getFocalPoint(); osg::Vec3d focalOffset = getFocalPointOffset(); + osg::Vec3d focal = mTrackedPosition + focalOffset; + focalOffset.z() += 10.f; // Needed to avoid camera clipping through the ceiling because + // character's head can be a bit higher than the collision area. float offsetLen = focalOffset.length(); if (offsetLen > 0) { - MWPhysics::RayCastingResult result = rayCasting->castSphere(focal - focalOffset, focal, focalObstacleLimit); + MWPhysics::RayCastingResult result + = rayCasting->castSphere(focal - focalOffset, focal, focalObstacleLimit, mCollisionType); if (result.mHit) { - double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen; - mFocalPointAdjustment = focalOffset * std::max(-1.0, adjustmentCoef); + double adjustmentCoef + = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen; + focal += focalOffset * std::max(-1.0, adjustmentCoef); } } - // Calculate camera distance. - mCameraDistance = mBaseCameraDistance + getCameraDistanceCorrection(); - if (mDynamicCameraDistanceEnabled) - mCameraDistance = std::min(mCameraDistance, mMaxNextCameraDistance); - osg::Vec3d cameraPos; - getPosition(focal, cameraPos); - MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, cameraPos, cameraObstacleLimit); + // Adjust camera distance. + mCameraDistance = mPreferredCameraDistance; + osg::Quat orient + = osg::Quat(mPitch + mExtraPitch, osg::Vec3d(1, 0, 0)) * osg::Quat(mYaw + mExtraYaw, osg::Vec3d(0, 0, 1)); + osg::Vec3d offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); + MWPhysics::RayCastingResult result + = rayCasting->castSphere(focal, focal + offset, cameraObstacleLimit, mCollisionType); if (result.mHit) + { mCameraDistance = (result.mHitPos + result.mHitNormal * cameraObstacleLimit - focal).length(); + offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); + } + + mPosition = focal + offset; + } + + osg::Quat Camera::getOrient() const + { + return osg::Quat(mRoll + mExtraRoll, osg::Vec3d(0, 1, 0)) * osg::Quat(mPitch + mExtraPitch, osg::Vec3d(1, 0, 0)) + * osg::Quat(mYaw + mExtraYaw, osg::Vec3d(0, 0, 1)); } - void Camera::updateStandingPreviewMode() + void Camera::setMode(Mode newMode, bool force) { - if (!mStandingPreviewAllowed) + if (mMode == newMode) + { + mQueuedMode = std::nullopt; return; - float speed = mTrackingPtr.getClass().getCurrentSpeed(mTrackingPtr); - bool combat = mTrackingPtr.getClass().isActor() && - mTrackingPtr.getClass().getCreatureStats(mTrackingPtr).getDrawState() != MWMechanics::DrawState_Nothing; - bool standingStill = speed == 0 && !combat && !mFirstPersonView; - if (!standingStill && mMode == Mode::StandingPreview) + } + Mode oldMode = mMode; + if (!force && (newMode == Mode::FirstPerson || oldMode == Mode::FirstPerson) && mAnimation + && !mAnimation->upperBodyReady()) + { + // Changing the view will stop all playing animations, so if we are playing + // anything important, queue the view change for later + mQueuedMode = newMode; + return; + } + mMode = newMode; + mQueuedMode = std::nullopt; + if (newMode == Mode::FirstPerson) + mFirstPersonView = true; + else if (newMode == Mode::ThirdPerson) + mFirstPersonView = false; + calculateDeferredRotation(); + if (oldMode == Mode::FirstPerson || newMode == Mode::FirstPerson) { - mMode = Mode::Normal; - calculateDeferredRotation(); + instantTransition(); + mProcessViewChange = true; } - else if (standingStill && mMode == Mode::Normal) - mMode = Mode::StandingPreview; } - void Camera::setFocalPointTargetOffset(osg::Vec2d v) + void Camera::setFocalPointTargetOffset(const osg::Vec2d& v) { mFocalPointTargetOffset = v; mPreviousTransitionSpeed = mFocalPointTransitionSpeed; @@ -321,9 +269,10 @@ namespace MWRender if (mPreviousTransitionInfluence > 0) { mFocalPointCurrentOffset -= mPreviousExtraOffset; - mPreviousExtraOffset = mPreviousExtraOffset / mPreviousTransitionInfluence + mPreviousTransitionSpeed * duration; - mPreviousTransitionInfluence = - std::max(0.f, mPreviousTransitionInfluence - duration * mFocalPointTransitionSpeedCoef); + mPreviousExtraOffset + = mPreviousExtraOffset / mPreviousTransitionInfluence + mPreviousTransitionSpeed * duration; + mPreviousTransitionInfluence + = std::max(0.f, mPreviousTransitionInfluence - duration * mFocalPointTransitionSpeedCoef); mPreviousExtraOffset *= mPreviousTransitionInfluence; mFocalPointCurrentOffset += mPreviousExtraOffset; } @@ -331,8 +280,8 @@ namespace MWRender osg::Vec2d delta = mFocalPointTargetOffset - mFocalPointCurrentOffset; if (delta.length2() > 0) { - float coef = duration * (1.0 + 5.0 / delta.length()) * - mFocalPointTransitionSpeedCoef * (1.0f - mPreviousTransitionInfluence); + float coef = duration * (1.0 + 5.0 / delta.length()) * mFocalPointTransitionSpeedCoef + * (1.0f - mPreviousTransitionInfluence); mFocalPointCurrentOffset += delta * std::min(coef, 1.0f); } else @@ -346,146 +295,59 @@ namespace MWRender void Camera::toggleViewMode(bool force) { - // Changing the view will stop all playing animations, so if we are playing - // anything important, queue the view change for later - if (!mAnimation->upperBodyReady() && !force) - { - mViewModeToggleQueued = true; - return; - } - else - mViewModeToggleQueued = false; - - mFirstPersonView = !mFirstPersonView; - updateStandingPreviewMode(); - instantTransition(); - processViewChange(); - } - - void Camera::allowVanityMode(bool allow) - { - if (!allow && mMode == Mode::Vanity) - { - disableDeferredPreviewRotation(); - toggleVanityMode(false); - } - mVanityAllowed = allow; + setMode(mFirstPersonView ? Mode::ThirdPerson : Mode::FirstPerson, force); } bool Camera::toggleVanityMode(bool enable) { - // Changing the view will stop all playing animations, so if we are playing - // anything important, queue the view change for later - if (mFirstPersonView && !mAnimation->upperBodyReady()) - { - mVanityToggleQueued = true; - mVanityToggleQueuedValue = enable; - return false; - } - - if (!mVanityAllowed && enable) - return false; - - if ((mMode == Mode::Vanity) == enable) - return true; - mMode = enable ? Mode::Vanity : Mode::Normal; - if (!mDeferredRotationAllowed) - disableDeferredPreviewRotation(); if (!enable) - calculateDeferredRotation(); - - processViewChange(); - return true; - } - - void Camera::togglePreviewMode(bool enable) - { - if (mFirstPersonView && !mAnimation->upperBodyReady()) - return; - - if((mMode == Mode::Preview) == enable) - return; - - mMode = enable ? Mode::Preview : Mode::Normal; - if (mMode == Mode::Normal) - updateStandingPreviewMode(); - else if (mFirstPersonView) - instantTransition(); - if (mMode == Mode::Normal) - { - if (!mDeferredRotationAllowed) - disableDeferredPreviewRotation(); - calculateDeferredRotation(); - } - processViewChange(); + setMode(mFirstPersonView ? Mode::FirstPerson : Mode::ThirdPerson, false); + else if (mVanityAllowed) + setMode(Mode::Vanity, false); + return (mMode == Mode::Vanity) == enable; } void Camera::setSneakOffset(float offset) { - mAnimation->setFirstPersonOffset(osg::Vec3f(0,0,-offset)); + mAnimation->setFirstPersonOffset(osg::Vec3f(0, 0, -offset)); } - void Camera::setYaw(float angle) + void Camera::setYaw(float angle, bool force) { - mYaw = Misc::normalizeAngle(angle); + if (!mLockYaw || force) + mYaw = Misc::normalizeAngle(angle); + if (force) + mLockYaw = true; } - void Camera::setPitch(float angle) + void Camera::setPitch(float angle, bool force) { const float epsilon = 0.000001f; float limit = static_cast(osg::PI_2) - epsilon; - mPitch = osg::clampBetween(angle, -limit, limit); + if (!mLockPitch || force) + mPitch = std::clamp(angle, -limit, limit); + if (force) + mLockPitch = true; } - float Camera::getCameraDistance() const + void Camera::setStaticPosition(const osg::Vec3d& pos) { - if (isFirstPerson()) - return 0.f; - return mCameraDistance; + if (mMode != Mode::Static) + throw std::runtime_error("setStaticPosition can be used only if camera is in Static mode"); + mPosition = pos; } - void Camera::adjustCameraDistance(float delta) - { - if (!isFirstPerson()) - { - if(isNearest() && delta < 0.f && getMode() != Mode::Preview && getMode() != Mode::Vanity) - toggleViewMode(); - else - mBaseCameraDistance = std::min(mCameraDistance - getCameraDistanceCorrection(), mBaseCameraDistance) + delta; - } - else if (delta > 0.f) - { - toggleViewMode(); - mBaseCameraDistance = 0; - } - - mIsNearest = mBaseCameraDistance <= mNearest; - mBaseCameraDistance = osg::clampBetween(mBaseCameraDistance, mNearest, mFurthest); - Settings::Manager::setFloat("third person camera distance", "Camera", mBaseCameraDistance); - } - - float Camera::getCameraDistanceCorrection() const - { - if (!mDynamicCameraDistanceEnabled) - return 0; - - float pitchCorrection = std::max(-getPitch(), 0.f) * 50.f; - - float smoothedSpeedSqr = mSmoothedSpeed * mSmoothedSpeed; - float speedCorrection = smoothedSpeedSqr / (smoothedSpeedSqr + 300.f*300.f) * mZoomOutWhenMoveCoef; - - return pitchCorrection + speedCorrection; - } - - void Camera::setAnimation(NpcAnimation *anim) + void Camera::setAnimation(NpcAnimation* anim) { mAnimation = anim; - processViewChange(); + mProcessViewChange = true; } void Camera::processViewChange() { - if(isFirstPerson()) + if (mTrackingPtr.isEmpty()) + return; + if (mMode == Mode::FirstPerson) { mAnimation->setViewMode(NpcAnimation::VM_FirstPerson); mTrackingNode = mAnimation->getNode("Camera"); @@ -503,12 +365,12 @@ namespace MWRender else mHeightScale = 1.f; } - rotateCamera(getPitch(), getYaw(), false); + mProcessViewChange = false; } void Camera::applyDeferredPreviewRotationToPlayer(float dt) { - if (isVanityOrPreviewModeEnabled() || mTrackingPtr.isEmpty()) + if (mMode != Mode::ThirdPerson || mTrackingPtr.isEmpty()) return; osg::Vec3f rot = mDeferredRotation; @@ -534,13 +396,15 @@ namespace MWRender float c = std::cos(mDeferredRotation.z()); float x = movement.mPosition[0]; float y = movement.mPosition[1]; - movement.mPosition[0] = x * c + y * s; + movement.mPosition[0] = x * c + y * s; movement.mPosition[1] = x * -s + y * c; } } void Camera::rotateCameraToTrackingPtr() { + if (mMode == Mode::Static || mTrackingPtr.isEmpty()) + return; setPitch(-mTrackingPtr.getRefData().getPosition().rot[0] - mDeferredRotation.x()); setYaw(-mTrackingPtr.getRefData().getPosition().rot[2] - mDeferredRotation.z()); } @@ -555,8 +419,13 @@ namespace MWRender void Camera::calculateDeferredRotation() { + if (mMode == Mode::Static) + { + mDeferredRotation = osg::Vec3f(); + return; + } MWWorld::Ptr ptr = mTrackingPtr; - if (isVanityOrPreviewModeEnabled() || ptr.isEmpty()) + if (mMode == Mode::Preview || mMode == Mode::Vanity || ptr.isEmpty()) return; if (mFirstPersonView) { @@ -566,6 +435,8 @@ namespace MWRender mDeferredRotation.x() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[0] - mPitch); mDeferredRotation.z() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[2] - mYaw); + if (!mDeferredRotationAllowed) + mDeferredRotationDisabled = true; } } diff --git a/apps/openmw/mwrender/camera.hpp b/apps/openmw/mwrender/camera.hpp index 9e2b608dfde..e09a2652933 100644 --- a/apps/openmw/mwrender/camera.hpp +++ b/apps/openmw/mwrender/camera.hpp @@ -1,18 +1,20 @@ #ifndef GAME_MWRENDER_CAMERA_H #define GAME_MWRENDER_CAMERA_H +#include #include -#include +#include #include #include +#include #include "../mwworld/ptr.hpp" namespace osg { class Camera; - class NodeCallback; + class Callback; class Node; } @@ -24,111 +26,57 @@ namespace MWRender class Camera { public: - enum class Mode { Normal, Vanity, Preview, StandingPreview }; - - private: - MWWorld::Ptr mTrackingPtr; - osg::ref_ptr mTrackingNode; - float mHeightScale; - - osg::ref_ptr mCamera; - - NpcAnimation *mAnimation; - - bool mFirstPersonView; - Mode mMode; - bool mVanityAllowed; - bool mStandingPreviewAllowed; - bool mDeferredRotationAllowed; - - float mNearest; - float mFurthest; - bool mIsNearest; - - float mHeight, mBaseCameraDistance; - float mPitch, mYaw, mRoll; + enum class Mode : int + { + Static = 0, + FirstPerson = 1, + ThirdPerson = 2, + Vanity = 3, + Preview = 4 + }; - bool mVanityToggleQueued; - bool mVanityToggleQueuedValue; - bool mViewModeToggleQueued; - - float mCameraDistance; - float mMaxNextCameraDistance; - - osg::Vec3d mFocalPointAdjustment; - osg::Vec2d mFocalPointCurrentOffset; - osg::Vec2d mFocalPointTargetOffset; - float mFocalPointTransitionSpeedCoef; - bool mSkipFocalPointTransition; - - // This fields are used to make focal point transition smooth if previous transition was not finished. - float mPreviousTransitionInfluence; - osg::Vec2d mFocalPointTransitionSpeed; - osg::Vec2d mPreviousTransitionSpeed; - osg::Vec2d mPreviousExtraOffset; - - float mSmoothedSpeed; - float mZoomOutWhenMoveCoef; - bool mDynamicCameraDistanceEnabled; - bool mShowCrosshairInThirdPersonMode; - - bool mHeadBobbingEnabled; - float mHeadBobbingOffset; - float mHeadBobbingWeight; // Value from 0 to 1 for smooth enabling/disabling. - float mTotalMovement; // Needed for head bobbing. - void updateHeadBobbing(float duration); - - void updateFocalPointOffset(float duration); - void updatePosition(); - float getCameraDistanceCorrection() const; - - osg::ref_ptr mUpdateCallback; - - // Used to rotate player to the direction of view after exiting preview or vanity mode. - osg::Vec3f mDeferredRotation; - bool mDeferredRotationDisabled; - void calculateDeferredRotation(); - void updateStandingPreviewMode(); - - public: Camera(osg::Camera* camera); ~Camera(); /// Attach camera to object - void attachTo(const MWWorld::Ptr &ptr) { mTrackingPtr = ptr; } + void attachTo(const MWWorld::Ptr& ptr) { mTrackingPtr = ptr; } MWWorld::Ptr getTrackingPtr() const { return mTrackingPtr; } void setFocalPointTransitionSpeed(float v) { mFocalPointTransitionSpeedCoef = v; } - void setFocalPointTargetOffset(osg::Vec2d v); + float getFocalPointTransitionSpeed() const { return mFocalPointTransitionSpeedCoef; } + void setFocalPointTargetOffset(const osg::Vec2d& v); + osg::Vec2d getFocalPointTargetOffset() const { return mFocalPointTargetOffset; } void instantTransition(); - void enableDynamicCameraDistance(bool v) { mDynamicCameraDistanceEnabled = v; } - void enableCrosshairInThirdPersonMode(bool v) { mShowCrosshairInThirdPersonMode = v; } + void showCrosshair(bool v) { mShowCrosshair = v; } /// Update the view matrix of \a cam void updateCamera(osg::Camera* cam); /// Reset to defaults - void reset(); + void reset() { setMode(Mode::FirstPerson); } - /// Set where the camera is looking at. Uses Morrowind (euler) angles - /// \param rot Rotation angles in radians - void rotateCamera(float pitch, float yaw, bool adjust); void rotateCameraToTrackingPtr(); + float getPitch() const { return mPitch; } float getYaw() const { return mYaw; } - void setYaw(float angle); + float getRoll() const { return mRoll; } - float getPitch() const { return mPitch; } - void setPitch(float angle); + void setPitch(float angle, bool force = false); + void setYaw(float angle, bool force = false); + void setRoll(float angle) { mRoll = angle; } - /// @param Force view mode switch, even if currently not allowed by the animation. - void toggleViewMode(bool force=false); + float getExtraPitch() const { return mExtraPitch; } + float getExtraYaw() const { return mExtraYaw; } + float getExtraRoll() const { return mExtraRoll; } + void setExtraPitch(float angle) { mExtraPitch = angle; } + void setExtraYaw(float angle) { mExtraYaw = angle; } + void setExtraRoll(float angle) { mExtraRoll = angle; } - bool toggleVanityMode(bool enable); - void allowVanityMode(bool allow); + osg::Quat getOrient() const; - /// @note this may be ignored if an important animation is currently playing - void togglePreviewMode(bool enable); + /// @param Force view mode switch, even if currently not allowed by the animation. + void toggleViewMode(bool force = false); + bool toggleVanityMode(bool enable); void applyDeferredPreviewRotationToPlayer(float dt); void disableDeferredPreviewRotation() { mDeferredRotationDisabled = true; } @@ -136,29 +84,93 @@ namespace MWRender /// \brief Lowers the camera for sneak. void setSneakOffset(float offset); - bool isFirstPerson() const { return mFirstPersonView && mMode == Mode::Normal; } - void processViewChange(); - void update(float duration, bool paused=false); + void update(float duration, bool paused = false); - /// Adds distDelta to the camera distance. Switches 3rd/1st person view if distance is less than limit. - void adjustCameraDistance(float distDelta); + float getCameraDistance() const { return mCameraDistance; } + void setPreferredCameraDistance(float v) { mPreferredCameraDistance = v; } - float getCameraDistance() const; + void setAnimation(NpcAnimation* anim); - void setAnimation(NpcAnimation *anim); + osg::Vec3d getTrackedPosition() const { return mTrackedPosition; } + const osg::Vec3d& getPosition() const { return mPosition; } + void setStaticPosition(const osg::Vec3d& pos); - osg::Vec3d getFocalPoint() const; - osg::Vec3d getFocalPointOffset() const; + bool isVanityOrPreviewModeEnabled() const { return mMode == Mode::Vanity || mMode == Mode::Preview; } + Mode getMode() const { return mMode; } + std::optional getQueuedMode() const { return mQueuedMode; } + void setMode(Mode mode, bool force = true); + + void allowCharacterDeferredRotation(bool v) { mDeferredRotationAllowed = v; } + void calculateDeferredRotation(); + void setFirstPersonOffset(const osg::Vec3f& v) { mFirstPersonOffset = v; } + osg::Vec3f getFirstPersonOffset() const { return mFirstPersonOffset; } - /// Stores focal and camera world positions in passed arguments - void getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const; + int getCollisionType() const { return mCollisionType; } + void setCollisionType(int collisionType) { mCollisionType = collisionType; } - bool isVanityOrPreviewModeEnabled() const { return mMode != Mode::Normal; } - Mode getMode() const { return mMode; } + const osg::Matrixf& getViewMatrix() const { return mViewMatrix; } + const osg::Matrixf& getProjectionMatrix() const { return mProjectionMatrix; } + + private: + MWWorld::Ptr mTrackingPtr; + osg::ref_ptr mTrackingNode; + osg::Vec3d mTrackedPosition; + float mHeightScale; + int mCollisionType; + + osg::ref_ptr mCamera; + + NpcAnimation* mAnimation; + + // Always 'true' if mMode == `FirstPerson`. Also it is 'true' in `Vanity` or `Preview` modes if + // the camera should return to `FirstPerson` view after it. + bool mFirstPersonView; - bool isNearest() const { return mIsNearest; } + Mode mMode; + std::optional mQueuedMode; + bool mVanityAllowed; + bool mDeferredRotationAllowed; + + bool mProcessViewChange; + + float mHeight; + float mPitch, mYaw, mRoll; + float mExtraPitch = 0, mExtraYaw = 0, mExtraRoll = 0; + bool mLockPitch = false, mLockYaw = false; + osg::Vec3d mPosition; + osg::Matrixf mViewMatrix; + osg::Matrixf mProjectionMatrix; + + float mCameraDistance, mPreferredCameraDistance; + + osg::Vec3f mFirstPersonOffset{ 0, 0, 0 }; + + osg::Vec2d mFocalPointCurrentOffset; + osg::Vec2d mFocalPointTargetOffset; + float mFocalPointTransitionSpeedCoef; + bool mSkipFocalPointTransition; + + // This fields are used to make focal point transition smooth if previous transition was not finished. + float mPreviousTransitionInfluence; + osg::Vec2d mFocalPointTransitionSpeed; + osg::Vec2d mPreviousTransitionSpeed; + osg::Vec2d mPreviousExtraOffset; + + bool mShowCrosshair; + + osg::Vec3d calculateTrackedPosition() const; + osg::Vec3d calculateFirstPersonPosition(const osg::Vec3d& trackedPosition) const; + osg::Vec3d getFocalPointOffset() const; + void updateFocalPointOffset(float duration); + void updatePosition(); + + osg::ref_ptr mUpdateCallback; + + // Used to rotate player to the direction of view after exiting preview or vanity mode. + osg::Vec3f mDeferredRotation; + bool mDeferredRotationDisabled; }; } diff --git a/apps/openmw/mwrender/cell.hpp b/apps/openmw/mwrender/cell.hpp index 8fa3f9f0f08..7fe7e08a5b2 100644 --- a/apps/openmw/mwrender/cell.hpp +++ b/apps/openmw/mwrender/cell.hpp @@ -7,28 +7,27 @@ namespace MWRender { class CellRender { - public: + public: + virtual ~CellRender() = default; - virtual ~CellRender() {}; + /// Make the cell visible. Load the cell if necessary. + virtual void show() = 0; - /// Make the cell visible. Load the cell if necessary. - virtual void show() = 0; + /// Remove the cell from rendering, but don't remove it from + /// memory. + virtual void hide() = 0; - /// Remove the cell from rendering, but don't remove it from - /// memory. - virtual void hide() = 0; + /// Destroy all rendering objects connected with this cell. + virtual void destroy() = 0; - /// Destroy all rendering objects connected with this cell. - virtual void destroy() = 0; + /// Make the reference with the given handle visible. + virtual void enable(const std::string& handle) = 0; - /// Make the reference with the given handle visible. - virtual void enable (const std::string& handle) = 0; + /// Make the reference with the given handle invisible. + virtual void disable(const std::string& handle) = 0; - /// Make the reference with the given handle invisible. - virtual void disable (const std::string& handle) = 0; - - /// Remove the reference with the given handle permanently from the scene. - virtual void deleteObject (const std::string& handle) = 0; + /// Remove the reference with the given handle permanently from the scene. + virtual void deleteObject(const std::string& handle) = 0; }; } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index e88c4cee324..a4c0181d357 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -2,28 +2,31 @@ #include -#include -#include #include -#include -#include #include -#include +#include #include #include +#include +#include +#include +#include #include #include #include #include #include -#include #include +#include +#include #include +#include +#include #include -#include +#include +#include -#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" @@ -31,21 +34,23 @@ #include "../mwmechanics/weapontype.hpp" #include "npcanimation.hpp" +#include "util.hpp" #include "vismask.hpp" namespace MWRender { - class DrawOnceCallback : public osg::NodeCallback + class DrawOnceCallback : public SceneUtil::NodeCallback { public: - DrawOnceCallback () + DrawOnceCallback(osg::Node* subgraph) : mRendered(false) , mLastRenderedFrame(0) + , mSubgraph(subgraph) { } - void operator () (osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Node* node, osg::NodeVisitor* nv) { if (!mRendered) { @@ -59,6 +64,10 @@ namespace MWRender nv->setFrameStamp(fs); + // Update keyframe controllers in the scene graph first... + // RTTNode does not continue update traversal, so manually continue the update traversal since we need + // it. + mSubgraph->accept(*nv); traverse(node, nv); nv->setFrameStamp(previousFramestamp); @@ -69,28 +78,23 @@ namespace MWRender } } - void redrawNextFrame() - { - mRendered = false; - } + void redrawNextFrame() { mRendered = false; } - unsigned int getLastRenderedFrame() const - { - return mLastRenderedFrame; - } + unsigned int getLastRenderedFrame() const { return mLastRenderedFrame; } private: bool mRendered; unsigned int mLastRenderedFrame; + osg::ref_ptr mSubgraph; }; - // Set up alpha blending mode to avoid issues caused by transparent objects writing onto the alpha value of the FBO // This makes the RTT have premultiplied alpha, though, so the source blend factor must be GL_ONE when it's applied class SetUpBlendVisitor : public osg::NodeVisitor { public: - SetUpBlendVisitor(): osg::NodeVisitor(TRAVERSE_ALL_CHILDREN), mNoAlphaUniform(new osg::Uniform("noAlpha", false)) + SetUpBlendVisitor() + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } @@ -99,9 +103,11 @@ namespace MWRender if (osg::ref_ptr stateset = node.getStateSet()) { osg::ref_ptr newStateSet; - if (stateset->getAttribute(osg::StateAttribute::BLENDFUNC) || stateset->getBinNumber() == osg::StateSet::TRANSPARENT_BIN) + if (stateset->getAttribute(osg::StateAttribute::BLENDFUNC) + || stateset->getBinNumber() == osg::StateSet::TRANSPARENT_BIN) { - osg::BlendFunc* blendFunc = static_cast(stateset->getAttribute(osg::StateAttribute::BLENDFUNC)); + osg::BlendFunc* blendFunc + = static_cast(stateset->getAttribute(osg::StateAttribute::BLENDFUNC)); if (blendFunc) { @@ -109,15 +115,19 @@ namespace MWRender node.setStateSet(newStateSet); osg::ref_ptr newBlendFunc = new osg::BlendFunc(*blendFunc); newStateSet->setAttribute(newBlendFunc, osg::StateAttribute::ON); - // I *think* (based on some by-hand maths) that the RGB and dest alpha factors are unchanged, and only dest determines source alpha factor - // This has the benefit of being idempotent if we assume nothing used glBlendFuncSeparate before we touched it + // I *think* (based on some by-hand maths) that the RGB and dest alpha factors are unchanged, + // and only dest determines source alpha factor This has the benefit of being idempotent if we + // assume nothing used glBlendFuncSeparate before we touched it if (blendFunc->getDestination() == osg::BlendFunc::ONE_MINUS_SRC_ALPHA) newBlendFunc->setSourceAlpha(osg::BlendFunc::ONE); else if (blendFunc->getDestination() == osg::BlendFunc::ONE) newBlendFunc->setSourceAlpha(osg::BlendFunc::ZERO); - // Other setups barely exist in the wild and aren't worth supporting as they're not equippable gear + // Other setups barely exist in the wild and aren't worth supporting as they're not equippable + // gear else - Log(Debug::Info) << "Unable to adjust blend mode for character preview. Source factor 0x" << std::hex << blendFunc->getSource() << ", destination factor 0x" << blendFunc->getDestination() << std::dec; + Log(Debug::Info) << "Unable to adjust blend mode for character preview. Source factor 0x" + << std::hex << blendFunc->getSource() << ", destination factor 0x" + << blendFunc->getDestination() << std::dec; } } if (stateset->getMode(GL_BLEND) & osg::StateAttribute::ON) @@ -129,17 +139,79 @@ namespace MWRender } // Disable noBlendAlphaEnv newStateSet->setTextureMode(7, GL_TEXTURE_2D, osg::StateAttribute::OFF); - newStateSet->addUniform(mNoAlphaUniform); + newStateSet->setDefine("FORCE_OPAQUE", "0", osg::StateAttribute::ON); } } traverse(node); } - private: - osg::ref_ptr mNoAlphaUniform; + }; + + class CharacterPreviewRTTNode : public SceneUtil::RTTNode + { + static constexpr float fovYDegrees = 12.3f; + static constexpr float znear = 4.0f; + static constexpr float zfar = 10000.f; + + public: + CharacterPreviewRTTNode(uint32_t sizeX, uint32_t sizeY) + : RTTNode(sizeX, sizeY, Settings::video().mAntialiasing, false, 0, + StereoAwareness::Unaware_MultiViewShaders, shouldAddMSAAIntermediateTarget()) + , mAspectRatio(static_cast(sizeX) / static_cast(sizeY)) + { + if (SceneUtil::AutoDepth::isReversed()) + mPerspectiveMatrix = static_cast( + SceneUtil::getReversedZProjectionMatrixAsPerspective(fovYDegrees, mAspectRatio, znear, zfar)); + else + mPerspectiveMatrix = osg::Matrixf::perspective(fovYDegrees, mAspectRatio, znear, zfar); + mGroup->getOrCreateStateSet()->addUniform(new osg::Uniform("projectionMatrix", mPerspectiveMatrix)); + mViewMatrix = osg::Matrixf::identity(); + setColorBufferInternalFormat(GL_RGBA); + setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); + } + + void setDefaults(osg::Camera* camera) override + { + camera->setName("CharacterPreview"); + camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); + camera->setClearColor(osg::Vec4(0.f, 0.f, 0.f, 0.f)); + camera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + camera->setProjectionMatrixAsPerspective(fovYDegrees, mAspectRatio, znear, zfar); + camera->setViewport(0, 0, width(), height()); + camera->setRenderOrder(osg::Camera::PRE_RENDER); + camera->setCullMask(~(Mask_UpdateVisitor)); + camera->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); + SceneUtil::setCameraClearDepth(camera); + + camera->setNodeMask(Mask_RenderToTexture); + camera->addChild(mGroup); + } + + void apply(osg::Camera* camera) override + { + if (mCameraStateset) + camera->setStateSet(mCameraStateset); + camera->setViewMatrix(mViewMatrix); + + if (shouldDoTextureArray()) + Stereo::setMultiviewMatrices(mGroup->getOrCreateStateSet(), { mPerspectiveMatrix, mPerspectiveMatrix }); + } + + void addChild(osg::Node* node) { mGroup->addChild(node); } + + void setCameraStateset(osg::StateSet* stateset) { mCameraStateset = stateset; } + + void setViewMatrix(const osg::Matrixf& viewMatrix) { mViewMatrix = viewMatrix; } + + osg::ref_ptr mGroup = new osg::Group; + osg::Matrixf mPerspectiveMatrix; + osg::Matrixf mViewMatrix; + osg::ref_ptr mCameraStateset; + float mAspectRatio; }; CharacterPreview::CharacterPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, - const MWWorld::Ptr& character, int sizeX, int sizeY, const osg::Vec3f& position, const osg::Vec3f& lookAt) + const MWWorld::Ptr& character, int sizeX, int sizeY, const osg::Vec3f& position, const osg::Vec3f& lookAt) : mParent(parent) , mResourceSystem(resourceSystem) , mPosition(position) @@ -149,53 +221,49 @@ namespace MWRender , mSizeX(sizeX) , mSizeY(sizeY) { - mTexture = new osg::Texture2D; - mTexture->setTextureSize(sizeX, sizeY); - mTexture->setInternalFormat(GL_RGBA); - mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mTexture->setUserValue("premultiplied alpha", true); - - mCamera = new osg::Camera; - // hints that the camera is not relative to the master camera - mCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); - mCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); - mCamera->setClearColor(osg::Vec4(0.f, 0.f, 0.f, 0.f)); - mCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - const float fovYDegrees = 12.3f; - mCamera->setProjectionMatrixAsPerspective(fovYDegrees, sizeX/static_cast(sizeY), 0.1f, 10000.f); // zNear and zFar are autocomputed - mCamera->setViewport(0, 0, sizeX, sizeY); - mCamera->setRenderOrder(osg::Camera::PRE_RENDER); - mCamera->attach(osg::Camera::COLOR_BUFFER, mTexture, 0, 0, false, Settings::Manager::getInt("antialiasing", "Video")); - mCamera->setName("CharacterPreview"); - mCamera->setComputeNearFarMode(osg::Camera::COMPUTE_NEAR_FAR_USING_BOUNDING_VOLUMES); - mCamera->setCullMask(~(Mask_UpdateVisitor)); - - mCamera->setNodeMask(Mask_RenderToTexture); - - bool ffp = mResourceSystem->getSceneManager()->getLightingMethod() == SceneUtil::LightingMethod::FFP; - - osg::ref_ptr lightManager = new SceneUtil::LightManager(ffp); + mTextureStateSet = new osg::StateSet; + mTextureStateSet->setAttribute(new osg::BlendFunc(osg::BlendFunc::ONE, osg::BlendFunc::ONE_MINUS_SRC_ALPHA)); + + mRTTNode = new CharacterPreviewRTTNode(sizeX, sizeY); + mRTTNode->setNodeMask(Mask_RenderToTexture); + + osg::ref_ptr lightManager = new SceneUtil::LightManager(SceneUtil::LightSettings{ + .mLightingMethod = mResourceSystem->getSceneManager()->getLightingMethod(), + .mMaxLights = Settings::shaders().mMaxLights, + .mMaximumLightDistance = Settings::shaders().mMaximumLightDistance, + .mLightFadeStart = Settings::shaders().mLightFadeStart, + .mLightBoundsMultiplier = Settings::shaders().mLightBoundsMultiplier, + }); lightManager->setStartLight(1); osg::ref_ptr stateset = lightManager->getOrCreateStateSet(); + stateset->setDefine("FORCE_OPAQUE", "1", osg::StateAttribute::ON); stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON); stateset->setMode(GL_NORMALIZE, osg::StateAttribute::ON); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::ON); - osg::ref_ptr defaultMat (new osg::Material); + osg::ref_ptr defaultMat(new osg::Material); defaultMat->setColorMode(osg::Material::OFF); - defaultMat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - defaultMat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); + defaultMat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, 1)); + defaultMat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, 1)); defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); stateset->setAttribute(defaultMat); - SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*stateset); // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) - osg::ref_ptr fog (new osg::Fog); + osg::ref_ptr fog(new osg::Fog); fog->setStart(10000000); fog->setEnd(10000000); - stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); + stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); + + // TODO: Clean up this mess of loose uniforms that shaders depend on. + // turn off sky blending + stateset->addUniform(new osg::Uniform("far", 10000000.0f)); + stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); + stateset->addUniform(new osg::Uniform("sky", 0)); + stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{ 1, 1 })); + + stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); // Opaque stuff must have 1 as its fragment alpha as the FBO is translucent, so having blending off isn't enough osg::ref_ptr noBlendAlphaEnv = new osg::TexEnvCombine(); @@ -205,6 +273,8 @@ namespace MWRender noBlendAlphaEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); noBlendAlphaEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); osg::ref_ptr dummyTexture = new osg::Texture2D(); + dummyTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + dummyTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); dummyTexture->setInternalFormat(GL_DEPTH_COMPONENT); dummyTexture->setTextureSize(1, 1); // This might clash with a shadow map, so make sure it doesn't cast shadows @@ -212,7 +282,6 @@ namespace MWRender dummyTexture->setShadowCompareFunc(osg::Texture::ShadowCompareFunc::ALWAYS); stateset->setTextureAttributeAndModes(7, dummyTexture, osg::StateAttribute::ON); stateset->setTextureAttribute(7, noBlendAlphaEnv, osg::StateAttribute::ON); - stateset->addUniform(new osg::Uniform("noAlpha", true)); osg::ref_ptr lightmodel = new osg::LightModel; lightmodel->setAmbientIntensity(osg::Vec4(0.0, 0.0, 0.0, 1.0)); @@ -230,19 +299,19 @@ namespace MWRender float positionX = -std::cos(azimuth) * std::sin(altitude); float positionY = std::sin(azimuth) * std::sin(altitude); float positionZ = std::cos(altitude); - light->setPosition(osg::Vec4(positionX,positionY,positionZ, 0.0)); - light->setDiffuse(osg::Vec4(diffuseR,diffuseG,diffuseB,1)); - osg::Vec4 ambientRGBA = osg::Vec4(ambientR,ambientG,ambientB,1); + light->setPosition(osg::Vec4(positionX, positionY, positionZ, 0.0)); + light->setDiffuse(osg::Vec4(diffuseR, diffuseG, diffuseB, 1)); + osg::Vec4 ambientRGBA = osg::Vec4(ambientR, ambientG, ambientB, 1); if (mResourceSystem->getSceneManager()->getForceShaders()) { // When using shaders, we now skip the ambient sun calculation as this is the only place it's used. // Using the scene ambient will give identical results. lightmodel->setAmbientIntensity(ambientRGBA); - light->setAmbient(osg::Vec4(0,0,0,1)); + light->setAmbient(osg::Vec4(0, 0, 0, 1)); } else light->setAmbient(ambientRGBA); - light->setSpecular(osg::Vec4(0,0,0,0)); + light->setSpecular(osg::Vec4(0, 0, 0, 0)); light->setLightNum(0); light->setConstantAttenuation(1.f); light->setLinearAttenuation(0.f); @@ -256,23 +325,22 @@ namespace MWRender lightManager->addChild(lightSource); - mCamera->addChild(lightManager); + mRTTNode->addChild(lightManager); mNode = new osg::PositionAttitudeTransform; lightManager->addChild(mNode); - mDrawOnceCallback = new DrawOnceCallback; - mCamera->addUpdateCallback(mDrawOnceCallback); + mDrawOnceCallback = new DrawOnceCallback(mRTTNode->mGroup); + mRTTNode->addUpdateCallback(mDrawOnceCallback); - mParent->addChild(mCamera); + mParent->addChild(mRTTNode); mCharacter.mCell = nullptr; } - CharacterPreview::~CharacterPreview () + CharacterPreview::~CharacterPreview() { - mCamera->removeChildren(0, mCamera->getNumChildren()); - mParent->removeChild(mCamera); + mParent->removeChild(mRTTNode); } int CharacterPreview::getTextureWidth() const @@ -287,7 +355,6 @@ namespace MWRender void CharacterPreview::setBlendMode() { - mResourceSystem->getSceneManager()->recreateShaders(mNode, "objects", true); SetUpBlendVisitor visitor; mNode->accept(visitor); } @@ -299,7 +366,7 @@ namespace MWRender osg::ref_ptr CharacterPreview::getTexture() { - return mTexture; + return static_cast(mRTTNode->getColorTexture(nullptr)); } void CharacterPreview::rebuild() @@ -307,7 +374,7 @@ namespace MWRender mAnimation = nullptr; mAnimation = new NpcAnimation(mCharacter, mNode, mResourceSystem, true, - (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); + (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); onSetup(); @@ -316,15 +383,15 @@ namespace MWRender void CharacterPreview::redraw() { - mCamera->setNodeMask(Mask_RenderToTexture); + mRTTNode->setNodeMask(Mask_RenderToTexture); mDrawOnceCallback->redrawNextFrame(); } // -------------------------------------------------------------------------------------------------- - - InventoryPreview::InventoryPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, const MWWorld::Ptr& character) - : CharacterPreview(parent, resourceSystem, character, 512, 1024, osg::Vec3f(0, 700, 71), osg::Vec3f(0,0,71)) + InventoryPreview::InventoryPreview( + osg::Group* parent, Resource::ResourceSystem* resourceSystem, const MWWorld::Ptr& character) + : CharacterPreview(parent, resourceSystem, character, 512, 1024, osg::Vec3f(0, 700, 71), osg::Vec3f(0, 0, 71)) { } @@ -335,9 +402,9 @@ namespace MWRender // NB Camera::setViewport has threading issues osg::ref_ptr stateset = new osg::StateSet; - mViewport = new osg::Viewport(0, mSizeY-sizeY, std::min(mSizeX, sizeX), std::min(mSizeY, sizeY)); + mViewport = new osg::Viewport(0, mSizeY - sizeY, std::min(mSizeX, sizeX), std::min(mSizeY, sizeY)); stateset->setAttributeAndModes(mViewport); - mCamera->setStateSet(stateset); + mRTTNode->setCameraStateset(stateset); redraw(); } @@ -350,16 +417,16 @@ namespace MWRender mAnimation->showWeapons(true); mAnimation->updateParts(); - MWWorld::InventoryStore &inv = mCharacter.getClass().getInventoryStore(mCharacter); + MWWorld::InventoryStore& inv = mCharacter.getClass().getInventoryStore(mCharacter); MWWorld::ContainerStoreIterator iter = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); std::string groupname = "inventoryhandtohand"; bool showCarriedLeft = true; - if(iter != inv.end()) + if (iter != inv.end()) { groupname = "inventoryweapononehand"; - if(iter->getTypeName() == typeid(ESM::Weapon).name()) + if (iter->getType() == ESM::Weapon::sRecordId) { - MWWorld::LiveCellRef *ref = iter->get(); + MWWorld::LiveCellRef* ref = iter->get(); int type = ref->mBase->mData.mType; const ESM::WeaponType* weaponInfo = MWMechanics::getWeaponType(type); showCarriedLeft = !(weaponInfo->mFlags & ESM::WeaponType::TwoHanded); @@ -369,34 +436,37 @@ namespace MWRender // We still should use one-handed animation as fallback if (mAnimation->hasAnimation(inventoryGroup)) - groupname = inventoryGroup; + groupname = std::move(inventoryGroup); else { - static const std::string oneHandFallback = "inventory" + MWMechanics::getWeaponType(ESM::Weapon::LongBladeOneHand)->mLongGroup; - static const std::string twoHandFallback = "inventory" + MWMechanics::getWeaponType(ESM::Weapon::LongBladeTwoHand)->mLongGroup; + static const std::string oneHandFallback + = "inventory" + MWMechanics::getWeaponType(ESM::Weapon::LongBladeOneHand)->mLongGroup; + static const std::string twoHandFallback + = "inventory" + MWMechanics::getWeaponType(ESM::Weapon::LongBladeTwoHand)->mLongGroup; // For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones - if (weaponInfo->mFlags & ESM::WeaponType::TwoHanded && weaponInfo->mWeaponClass == ESM::WeaponType::Melee) + if (weaponInfo->mFlags & ESM::WeaponType::TwoHanded + && weaponInfo->mWeaponClass == ESM::WeaponType::Melee) groupname = twoHandFallback; else groupname = oneHandFallback; } - } + } } mAnimation->showCarriedLeft(showCarriedLeft); - mCurrentAnimGroup = groupname; - mAnimation->play(mCurrentAnimGroup, 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); + mCurrentAnimGroup = std::move(groupname); + mAnimation->play(mCurrentAnimGroup, 1, BlendMask::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() && showCarriedLeft) + if (torch != inv.end() && torch->getType() == ESM::Light::sRecordId && showCarriedLeft) { - if(!mAnimation->getInfo("torch")) - mAnimation->play("torch", 2, Animation::BlendMask_LeftArm, false, - 1.0f, "start", "stop", 0.0f, ~0ul, true); + if (!mAnimation->getInfo("torch")) + mAnimation->play("torch", 2, BlendMask::BlendMask_LeftArm, false, 1.0f, "start", "stop", 0.0f, + std::numeric_limits::max(), true); } - else if(mAnimation->getInfo("torch")) + else if (mAnimation->getInfo("torch")) mAnimation->disable("torch"); mAnimation->runAnimation(0.0f); @@ -406,7 +476,7 @@ namespace MWRender redraw(); } - int InventoryPreview::getSlotSelected (int posX, int posY) + int InventoryPreview::getSlotSelected(int posX, int posY) { if (!mViewport) return -1; @@ -416,18 +486,21 @@ namespace MWRender // precision issue - compiling with OSG_USE_FLOAT_MATRIX=0, Intersector::WINDOW works ok. // Using Intersector::PROJECTION results in better precision because the start/end points and the model matrices // don't go through as many transformations. - osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector(osgUtil::Intersector::PROJECTION, projX, projY)); + osg::ref_ptr intersector( + new osgUtil::LineSegmentIntersector(osgUtil::Intersector::PROJECTION, projX, projY)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST); osgUtil::IntersectionVisitor visitor(intersector); visitor.setTraversalMode(osg::NodeVisitor::TRAVERSE_ACTIVE_CHILDREN); - // Set the traversal number from the last draw, so that the frame switch used for RigGeometry double buffering works correctly + // Set the traversal number from the last draw, so that the frame switch used for RigGeometry double buffering + // works correctly visitor.setTraversalNumber(mDrawOnceCallback->getLastRenderedFrame()); - osg::Node::NodeMask nodeMask = mCamera->getNodeMask(); - mCamera->setNodeMask(~0u); - mCamera->accept(visitor); - mCamera->setNodeMask(nodeMask); + auto* camera = mRTTNode->getCamera(nullptr); + osg::Node::NodeMask nodeMask = camera->getNodeMask(); + camera->setNodeMask(~0u); + camera->accept(visitor); + camera->setNodeMask(nodeMask); if (intersector->containsIntersections()) { @@ -437,7 +510,7 @@ namespace MWRender return -1; } - void InventoryPreview::updatePtr(const MWWorld::Ptr &ptr) + void InventoryPreview::updatePtr(const MWWorld::Ptr& ptr) { mCharacter = MWWorld::Ptr(ptr.getBase(), nullptr); } @@ -445,58 +518,55 @@ namespace MWRender void InventoryPreview::onSetup() { CharacterPreview::onSetup(); - osg::Vec3f scale (1.f, 1.f, 1.f); + osg::Vec3f scale(1.f, 1.f, 1.f); mCharacter.getClass().adjustScale(mCharacter, scale, true); mNode->setScale(scale); - mCamera->setViewMatrixAsLookAt(mPosition * scale.z(), mLookAt * scale.z(), osg::Vec3f(0,0,1)); + auto viewMatrix = osg::Matrixf::lookAt(mPosition * scale.z(), mLookAt * scale.z(), osg::Vec3f(0, 0, 1)); + mRTTNode->setViewMatrix(viewMatrix); } // -------------------------------------------------------------------------------------------------- RaceSelectionPreview::RaceSelectionPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem) - : CharacterPreview(parent, resourceSystem, MWMechanics::getPlayer(), - 512, 512, osg::Vec3f(0, 125, 8), osg::Vec3f(0,0,8)) - , mBase (*mCharacter.get()->mBase) + : CharacterPreview( + parent, resourceSystem, MWMechanics::getPlayer(), 512, 512, osg::Vec3f(0, 125, 8), osg::Vec3f(0, 0, 8)) + , mBase(*mCharacter.get()->mBase) , mRef(&mBase) , mPitchRadians(osg::DegreesToRadians(6.f)) { mCharacter = MWWorld::Ptr(&mRef, nullptr); } - RaceSelectionPreview::~RaceSelectionPreview() - { - } + RaceSelectionPreview::~RaceSelectionPreview() {} void RaceSelectionPreview::setAngle(float angleRadians) { - mNode->setAttitude(osg::Quat(mPitchRadians, osg::Vec3(1,0,0)) - * osg::Quat(angleRadians, osg::Vec3(0,0,1))); + mNode->setAttitude(osg::Quat(mPitchRadians, osg::Vec3(1, 0, 0)) * osg::Quat(angleRadians, osg::Vec3(0, 0, 1))); redraw(); } - void RaceSelectionPreview::setPrototype(const ESM::NPC &proto) + void RaceSelectionPreview::setPrototype(const ESM::NPC& proto) { mBase = proto; - mBase.mId = "player"; + mBase.mId = ESM::RefId::stringRefId("Player"); rebuild(); } - class UpdateCameraCallback : public osg::NodeCallback + class UpdateCameraCallback : public SceneUtil::NodeCallback { public: - UpdateCameraCallback(osg::ref_ptr nodeToFollow, const osg::Vec3& posOffset, const osg::Vec3& lookAtOffset) - : mNodeToFollow(nodeToFollow) + UpdateCameraCallback( + osg::ref_ptr nodeToFollow, const osg::Vec3& posOffset, const osg::Vec3& lookAtOffset) + : mNodeToFollow(std::move(nodeToFollow)) , mPosOffset(posOffset) , mLookAtOffset(lookAtOffset) { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(CharacterPreviewRTTNode* node, osg::NodeVisitor* nv) { - osg::Camera* cam = static_cast(node); - // Update keyframe controllers in the scene graph first... traverse(node, nv); @@ -507,7 +577,9 @@ namespace MWRender osg::Matrix worldMat = osg::computeLocalToWorld(nodepaths[0]); osg::Vec3 headOffset = worldMat.getTrans(); - cam->setViewMatrixAsLookAt(headOffset + mPosOffset, headOffset + mLookAtOffset, osg::Vec3(0,0,1)); + auto viewMatrix + = osg::Matrixf::lookAt(headOffset + mPosOffset, headOffset + mLookAtOffset, osg::Vec3(0, 0, 1)); + node->setViewMatrix(viewMatrix); } private: @@ -516,21 +588,21 @@ namespace MWRender osg::Vec3 mLookAtOffset; }; - void RaceSelectionPreview::onSetup () + void RaceSelectionPreview::onSetup() { CharacterPreview::onSetup(); - mAnimation->play("idle", 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); + mAnimation->play("idle", 1, BlendMask::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); mAnimation->runAnimation(0.f); // attach camera to follow the head node if (mUpdateCameraCallback) - mCamera->removeUpdateCallback(mUpdateCameraCallback); + mRTTNode->removeUpdateCallback(mUpdateCameraCallback); const osg::Node* head = mAnimation->getNode("Bip01 Head"); if (head) { mUpdateCameraCallback = new UpdateCameraCallback(head, mPosition, mLookAt); - mCamera->addUpdateCallback(mUpdateCameraCallback); + mRTTNode->addUpdateCallback(mUpdateCameraCallback); } else Log(Debug::Error) << "Error: Bip01 Head node not found"; diff --git a/apps/openmw/mwrender/characterpreview.hpp b/apps/openmw/mwrender/characterpreview.hpp index 3eb9688465a..e2ab53bef6f 100644 --- a/apps/openmw/mwrender/characterpreview.hpp +++ b/apps/openmw/mwrender/characterpreview.hpp @@ -1,12 +1,12 @@ #ifndef MWRENDER_CHARACTERPREVIEW_H #define MWRENDER_CHARACTERPREVIEW_H -#include #include +#include #include -#include +#include #include @@ -18,6 +18,7 @@ namespace osg class Camera; class Group; class Viewport; + class StateSet; } namespace MWRender @@ -25,12 +26,13 @@ namespace MWRender class NpcAnimation; class DrawOnceCallback; + class CharacterPreviewRTTNode; class CharacterPreview { public: - CharacterPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, const MWWorld::Ptr& character, int sizeX, int sizeY, - const osg::Vec3f& position, const osg::Vec3f& lookAt); + CharacterPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, const MWWorld::Ptr& character, + int sizeX, int sizeY, const osg::Vec3f& position, const osg::Vec3f& lookAt); virtual ~CharacterPreview(); int getTextureWidth() const; @@ -41,6 +43,8 @@ namespace MWRender void rebuild(); osg::ref_ptr getTexture(); + /// Get the osg::StateSet required to render the texture correctly, if any. + osg::StateSet* getTextureStateSet() { return mTextureStateSet; } private: CharacterPreview(const CharacterPreview&); @@ -53,9 +57,9 @@ namespace MWRender osg::ref_ptr mParent; Resource::ResourceSystem* mResourceSystem; - osg::ref_ptr mTexture; - osg::ref_ptr mCamera; + osg::ref_ptr mTextureStateSet; osg::ref_ptr mDrawOnceCallback; + osg::ref_ptr mRTTNode; osg::Vec3f mPosition; osg::Vec3f mLookAt; @@ -73,7 +77,6 @@ namespace MWRender class InventoryPreview : public CharacterPreview { public: - InventoryPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, const MWWorld::Ptr& character); void updatePtr(const MWWorld::Ptr& ptr); @@ -93,11 +96,10 @@ namespace MWRender class RaceSelectionPreview : public CharacterPreview { - ESM::NPC mBase; - MWWorld::LiveCellRef mRef; + ESM::NPC mBase; + MWWorld::LiveCellRef mRef; protected: - bool renderHeadOnly() override { return true; } void onSetup() override; @@ -107,14 +109,11 @@ namespace MWRender void setAngle(float angleRadians); - const ESM::NPC &getPrototype() const { - return mBase; - } + const ESM::NPC& getPrototype() const { return mBase; } - void setPrototype(const ESM::NPC &proto); + void setPrototype(const ESM::NPC& proto); private: - osg::ref_ptr mUpdateCameraCallback; float mPitchRadians; diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 2987111621f..b6cc823d28e 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -2,290 +2,273 @@ #include -#include #include +#include #include -#include -#include -#include +#include #include -#include -#include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include +#include #include "../mwmechanics/weapontype.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" namespace MWRender { -CreatureAnimation::CreatureAnimation(const MWWorld::Ptr &ptr, - const std::string& model, Resource::ResourceSystem* resourceSystem) - : ActorAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), resourceSystem) -{ - MWWorld::LiveCellRef *ref = mPtr.get(); - - if(!model.empty()) + CreatureAnimation::CreatureAnimation( + const MWWorld::Ptr& ptr, const std::string& model, Resource::ResourceSystem* resourceSystem, bool animated) + : ActorAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), resourceSystem) { - setObjectRoot(model, false, false, true); + MWWorld::LiveCellRef* ref = mPtr.get(); - if((ref->mBase->mFlags&ESM::Creature::Bipedal)) - addAnimSource(Settings::Manager::getString("xbaseanim", "Models"), model); - addAnimSource(model, model); - } -} + if (!model.empty()) + { + setObjectRoot(model, false, false, true); + if ((ref->mBase->mFlags & ESM::Creature::Bipedal)) + addAnimSource(Settings::models().mXbaseanim.get(), model); -CreatureWeaponAnimation::CreatureWeaponAnimation(const MWWorld::Ptr &ptr, const std::string& model, Resource::ResourceSystem* resourceSystem) - : ActorAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), resourceSystem) - , mShowWeapons(false) - , mShowCarriedLeft(false) -{ - MWWorld::LiveCellRef *ref = mPtr.get(); + if (animated) + addAnimSource(model, model); + } + } - if(!model.empty()) + CreatureWeaponAnimation::CreatureWeaponAnimation( + const MWWorld::Ptr& ptr, const std::string& model, Resource::ResourceSystem* resourceSystem, bool animated) + : ActorAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), resourceSystem) + , mShowWeapons(false) + , mShowCarriedLeft(false) { - setObjectRoot(model, true, false, true); + MWWorld::LiveCellRef* ref = mPtr.get(); - if((ref->mBase->mFlags&ESM::Creature::Bipedal)) + if (!model.empty()) { - addAnimSource(Settings::Manager::getString("xbaseanim", "Models"), model); - } - addAnimSource(model, model); + setObjectRoot(model, true, false, true); - mPtr.getClass().getInventoryStore(mPtr).setInvListener(this, mPtr); + if ((ref->mBase->mFlags & ESM::Creature::Bipedal)) + addAnimSource(Settings::models().mXbaseanim.get(), model); - updateParts(); - } + if (animated) + addAnimSource(model, model); - mWeaponAnimationTime = std::shared_ptr(new WeaponAnimationTime(this)); -} + mPtr.getClass().getInventoryStore(mPtr).setInvListener(this); -void CreatureWeaponAnimation::showWeapons(bool showWeapon) -{ - if (showWeapon != mShowWeapons) - { - mShowWeapons = showWeapon; - updateParts(); + updateParts(); + } + + mWeaponAnimationTime = std::make_shared(this); } -} -void CreatureWeaponAnimation::showCarriedLeft(bool show) -{ - if (show != mShowCarriedLeft) + void CreatureWeaponAnimation::showWeapons(bool showWeapon) { - mShowCarriedLeft = show; - updateParts(); + if (showWeapon != mShowWeapons) + { + mShowWeapons = showWeapon; + updateParts(); + } } -} - -void CreatureWeaponAnimation::updateParts() -{ - mAmmunition.reset(); - mWeapon.reset(); - mShield.reset(); - - updateHolsteredWeapon(!mShowWeapons); - updateQuiver(); - updateHolsteredShield(mShowCarriedLeft); - - if (mShowWeapons) - updatePart(mWeapon, MWWorld::InventoryStore::Slot_CarriedRight); - if (mShowCarriedLeft) - updatePart(mShield, MWWorld::InventoryStore::Slot_CarriedLeft); -} - -void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) -{ - if (!mObjectRoot) - return; - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator it = inv.getSlot(slot); + void CreatureWeaponAnimation::showCarriedLeft(bool show) + { + if (show != mShowCarriedLeft) + { + mShowCarriedLeft = show; + updateParts(); + } + } - if (it == inv.end()) + void CreatureWeaponAnimation::updateParts() { - scene.reset(); - return; + mAmmunition.reset(); + mWeapon.reset(); + mShield.reset(); + + updateHolsteredWeapon(!mShowWeapons); + updateQuiver(); + updateHolsteredShield(mShowCarriedLeft); + + if (mShowWeapons) + updatePart(mWeapon, MWWorld::InventoryStore::Slot_CarriedRight); + if (mShowCarriedLeft) + updatePart(mShield, MWWorld::InventoryStore::Slot_CarriedLeft); } - MWWorld::ConstPtr item = *it; - std::string bonename; - std::string itemModel = item.getClass().getModel(item); - if (slot == MWWorld::InventoryStore::Slot_CarriedRight) + void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) { - if(item.getTypeName() == typeid(ESM::Weapon).name()) + if (!mObjectRoot) + return; + + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator it = inv.getSlot(slot); + + if (it == inv.end()) + { + scene.reset(); + return; + } + MWWorld::ConstPtr item = *it; + + std::string_view bonename; + std::string itemModel = item.getClass().getCorrectedModel(item); + if (slot == MWWorld::InventoryStore::Slot_CarriedRight) { - int type = item.get()->mBase->mData.mType; - bonename = MWMechanics::getWeaponType(type)->mAttachBone; - if (bonename != "Weapon Bone") + if (item.getType() == ESM::Weapon::sRecordId) { - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); - if (found == nodeMap.end()) - bonename = "Weapon Bone"; + int type = item.get()->mBase->mData.mType; + bonename = MWMechanics::getWeaponType(type)->mAttachBone; + if (bonename != "Weapon Bone") + { + const NodeMap& nodeMap = getNodeMap(); + NodeMap::const_iterator found = nodeMap.find(bonename); + if (found == nodeMap.end()) + bonename = "Weapon Bone"; + } } + else + bonename = "Weapon Bone"; } else - bonename = "Weapon Bone"; - } - else - { - bonename = "Shield Bone"; - if (item.getTypeName() == typeid(ESM::Armor).name()) { - // Shield body part model should be used if possible. - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - for (const auto& part : item.get()->mBase->mParts.mParts) + bonename = "Shield Bone"; + if (item.getType() == ESM::Armor::sRecordId) { - // Assume all creatures use the male mesh. - if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) - continue; - const ESM::BodyPart *bodypart = store.get().search(part.mMale); - if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) - { - itemModel = "meshes\\" + bodypart->mModel; - break; - } + itemModel = getShieldMesh(item, false); } } - } - - try - { - osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(itemModel); - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); - if (found == nodeMap.end()) - throw std::runtime_error("Can't find attachment node " + bonename); - osg::ref_ptr attached = SceneUtil::attach(node, mObjectRoot, bonename, found->second.get()); + try + { + osg::ref_ptr attached + = attach(itemModel, bonename, bonename, item.getType() == ESM::Light::sRecordId); - scene.reset(new PartHolder(attached)); + scene = std::make_unique(attached); - if (!item.getClass().getEnchantment(item).empty()) - mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, item.getClass().getEnchantmentColor(item)); + if (!item.getClass().getEnchantment(item).empty()) + mGlowUpdater + = SceneUtil::addEnchantedGlow(attached, mResourceSystem, item.getClass().getEnchantmentColor(item)); - // Crossbows start out with a bolt attached - // FIXME: code duplicated from NpcAnimation - if (slot == MWWorld::InventoryStore::Slot_CarriedRight && - item.getTypeName() == typeid(ESM::Weapon).name() && - item.get()->mBase->mData.mType == ESM::Weapon::MarksmanCrossbow) - { - const ESM::WeaponType* weaponInfo = MWMechanics::getWeaponType(ESM::Weapon::MarksmanCrossbow); - MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); - if (ammo != inv.end() && ammo->get()->mBase->mData.mType == weaponInfo->mAmmoType) - attachArrow(); + // Crossbows start out with a bolt attached + // FIXME: code duplicated from NpcAnimation + if (slot == MWWorld::InventoryStore::Slot_CarriedRight && item.getType() == ESM::Weapon::sRecordId + && item.get()->mBase->mData.mType == ESM::Weapon::MarksmanCrossbow) + { + const ESM::WeaponType* weaponInfo = MWMechanics::getWeaponType(ESM::Weapon::MarksmanCrossbow); + MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + if (ammo != inv.end() && ammo->get()->mBase->mData.mType == weaponInfo->mAmmoType) + attachArrow(); + else + mAmmunition.reset(); + } else mAmmunition.reset(); - } - else - mAmmunition.reset(); - std::shared_ptr source; + std::shared_ptr source; - if (slot == MWWorld::InventoryStore::Slot_CarriedRight) - source = mWeaponAnimationTime; - else - source.reset(new NullAnimationTime); + if (slot == MWWorld::InventoryStore::Slot_CarriedRight) + source = mWeaponAnimationTime; + else + source = mAnimationTimePtr[0]; - SceneUtil::AssignControllerSourcesVisitor assignVisitor(source); - attached->accept(assignVisitor); + SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::move(source)); + attached->accept(assignVisitor); + + if (item.getType() == ESM::Light::sRecordId) + addExtraLight(scene->getNode()->asGroup(), SceneUtil::LightCommon(*item.get()->mBase)); + } + catch (std::exception& e) + { + Log(Debug::Error) << "Can not add creature part: " << e.what(); + } } - catch (std::exception& e) + + bool CreatureWeaponAnimation::isArrowAttached() const { - Log(Debug::Error) << "Can not add creature part: " << e.what(); + return mAmmunition != nullptr; } -} -bool CreatureWeaponAnimation::isArrowAttached() const -{ - return mAmmunition != nullptr; -} + void CreatureWeaponAnimation::detachArrow() + { + WeaponAnimation::detachArrow(mPtr); + updateQuiver(); + } -void CreatureWeaponAnimation::detachArrow() -{ - WeaponAnimation::detachArrow(mPtr); - updateQuiver(); -} + void CreatureWeaponAnimation::attachArrow() + { + WeaponAnimation::attachArrow(mPtr); -void CreatureWeaponAnimation::attachArrow() -{ - WeaponAnimation::attachArrow(mPtr); + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + if (ammo != inv.end() && !ammo->getClass().getEnchantment(*ammo).empty()) + { + osg::Group* bone = getArrowBone(); + if (bone != nullptr && bone->getNumChildren()) + SceneUtil::addEnchantedGlow( + bone->getChild(0), mResourceSystem, ammo->getClass().getEnchantmentColor(*ammo)); + } - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); - if (ammo != inv.end() && !ammo->getClass().getEnchantment(*ammo).empty()) - { - osg::Group* bone = getArrowBone(); - if (bone != nullptr && bone->getNumChildren()) - SceneUtil::addEnchantedGlow(bone->getChild(0), mResourceSystem, ammo->getClass().getEnchantmentColor(*ammo)); + updateQuiver(); } - updateQuiver(); -} + void CreatureWeaponAnimation::releaseArrow(float attackStrength) + { + WeaponAnimation::releaseArrow(mPtr, attackStrength); + updateQuiver(); + } -void CreatureWeaponAnimation::releaseArrow(float attackStrength) -{ - WeaponAnimation::releaseArrow(mPtr, attackStrength); - updateQuiver(); -} + osg::Group* CreatureWeaponAnimation::getArrowBone() + { + if (!mWeapon) + return nullptr; -osg::Group *CreatureWeaponAnimation::getArrowBone() -{ - if (!mWeapon) - return nullptr; + if (!mPtr.getClass().hasInventoryStore(mPtr)) + return nullptr; - if (!mPtr.getClass().hasInventoryStore(mPtr)) - return nullptr; + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) + return nullptr; - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) - return nullptr; + int type = weapon->get()->mBase->mData.mType; + int ammoType = MWMechanics::getWeaponType(type)->mAmmoType; + if (ammoType == ESM::Weapon::None) + return nullptr; - int type = weapon->get()->mBase->mData.mType; - int ammoType = MWMechanics::getWeaponType(type)->mAmmoType; + // Try to find and attachment bone in actor's skeleton, otherwise fall back to the ArrowBone in weapon's mesh + osg::Group* bone = getBoneByName(MWMechanics::getWeaponType(ammoType)->mAttachBone); + if (bone == nullptr) + { + SceneUtil::FindByNameVisitor findVisitor("ArrowBone"); + mWeapon->getNode()->accept(findVisitor); + bone = findVisitor.mFoundNode; + } + return bone; + } - // Try to find and attachment bone in actor's skeleton, otherwise fall back to the ArrowBone in weapon's mesh - osg::Group* bone = getBoneByName(MWMechanics::getWeaponType(ammoType)->mAttachBone); - if (bone == nullptr) + osg::Node* CreatureWeaponAnimation::getWeaponNode() { - SceneUtil::FindByNameVisitor findVisitor ("ArrowBone"); - mWeapon->getNode()->accept(findVisitor); - bone = findVisitor.mFoundNode; + return mWeapon ? mWeapon->getNode().get() : nullptr; } - return bone; -} - -osg::Node *CreatureWeaponAnimation::getWeaponNode() -{ - return mWeapon ? mWeapon->getNode().get() : nullptr; -} -Resource::ResourceSystem *CreatureWeaponAnimation::getResourceSystem() -{ - return mResourceSystem; -} + Resource::ResourceSystem* CreatureWeaponAnimation::getResourceSystem() + { + return mResourceSystem; + } -void CreatureWeaponAnimation::addControllers() -{ - Animation::addControllers(); - WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get()); -} + void CreatureWeaponAnimation::addControllers() + { + Animation::addControllers(); + WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get()); + } -osg::Vec3f CreatureWeaponAnimation::runAnimation(float duration) -{ - osg::Vec3f ret = Animation::runAnimation(duration); + osg::Vec3f CreatureWeaponAnimation::runAnimation(float duration) + { + osg::Vec3f ret = Animation::runAnimation(duration); - WeaponAnimation::configureControllers(mPtr.getRefData().getPosition().rot[0] + getBodyPitchRadians()); + WeaponAnimation::configureControllers(mPtr.getRefData().getPosition().rot[0] + getBodyPitchRadians()); - return ret; -} + return ret; + } } diff --git a/apps/openmw/mwrender/creatureanimation.hpp b/apps/openmw/mwrender/creatureanimation.hpp index 9169e410247..05235e5191a 100644 --- a/apps/openmw/mwrender/creatureanimation.hpp +++ b/apps/openmw/mwrender/creatureanimation.hpp @@ -1,9 +1,9 @@ #ifndef GAME_RENDER_CREATUREANIMATION_H #define GAME_RENDER_CREATUREANIMATION_H +#include "../mwworld/inventorystore.hpp" #include "actoranimation.hpp" #include "weaponanimation.hpp" -#include "../mwworld/inventorystore.hpp" namespace MWWorld { @@ -15,21 +15,26 @@ namespace MWRender class CreatureAnimation : public ActorAnimation { public: - CreatureAnimation(const MWWorld::Ptr &ptr, const std::string& model, Resource::ResourceSystem* resourceSystem); + CreatureAnimation( + const MWWorld::Ptr& ptr, const std::string& model, Resource::ResourceSystem* resourceSystem, bool animated); virtual ~CreatureAnimation() {} }; // For creatures with weapons and shields // Animation is already virtual anyway, so might as well make a separate class. // Most creatures don't need weapons/shields, so this will save some memory. - class CreatureWeaponAnimation : public ActorAnimation, public WeaponAnimation, public MWWorld::InventoryStoreListener + class CreatureWeaponAnimation : public ActorAnimation, + public WeaponAnimation, + public MWWorld::InventoryStoreListener { public: - CreatureWeaponAnimation(const MWWorld::Ptr &ptr, const std::string& model, Resource::ResourceSystem* resourceSystem); + CreatureWeaponAnimation( + const MWWorld::Ptr& ptr, const std::string& model, Resource::ResourceSystem* resourceSystem, bool animated); virtual ~CreatureWeaponAnimation() {} void equipmentChanged() override { updateParts(); } + bool getWeaponsShown() const override { return mShowWeapons; } void showWeapons(bool showWeapon) override; bool getCarriedLeftShown() const override { return mShowCarriedLeft; } @@ -47,7 +52,10 @@ namespace MWRender osg::Node* getWeaponNode() override; Resource::ResourceSystem* getResourceSystem() override; void showWeapon(bool show) override { showWeapons(show); } - void setWeaponGroup(const std::string& group, bool relativeDuration) override { mWeaponAnimationTime->setGroup(group, relativeDuration); } + void setWeaponGroup(const std::string& group, bool relativeDuration) override + { + mWeaponAnimationTime->setGroup(group, relativeDuration); + } void addControllers() override; diff --git a/apps/openmw/mwrender/distortion.cpp b/apps/openmw/mwrender/distortion.cpp new file mode 100644 index 00000000000..5df2bfb703d --- /dev/null +++ b/apps/openmw/mwrender/distortion.cpp @@ -0,0 +1,37 @@ +#include "distortion.hpp" + +#include + +#include "postprocessor.hpp" + +namespace MWRender +{ + void DistortionCallback::drawImplementation( + osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) + { + osg::State* state = renderInfo.getState(); + size_t frameId = state->getFrameStamp()->getFrameNumber() % 2; + + PostProcessor* postProcessor = dynamic_cast(renderInfo.getCurrentCamera()->getUserData()); + + if (!postProcessor || bin->getStage()->getFrameBufferObject() != postProcessor->getPrimaryFbo(frameId)) + return; + + mFBO[frameId]->apply(*state); + + const osg::Texture* tex + = mFBO[frameId]->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0).getTexture(); + + glViewport(0, 0, tex->getTextureWidth(), tex->getTextureHeight()); + glClearColor(0.0, 0.0, 0.0, 1.0); + glColorMask(true, true, true, true); + state->haveAppliedAttribute(osg::StateAttribute::Type::COLORMASK); + glClear(GL_COLOR_BUFFER_BIT); + + bin->drawImplementation(renderInfo, previous); + + tex = mOriginalFBO[frameId]->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0).getTexture(); + glViewport(0, 0, tex->getTextureWidth(), tex->getTextureHeight()); + mOriginalFBO[frameId]->apply(*state); + } +} diff --git a/apps/openmw/mwrender/distortion.hpp b/apps/openmw/mwrender/distortion.hpp new file mode 100644 index 00000000000..736f4ea6f25 --- /dev/null +++ b/apps/openmw/mwrender/distortion.hpp @@ -0,0 +1,28 @@ +#include + +#include + +namespace osg +{ + class FrameBufferObject; +} + +namespace MWRender +{ + class DistortionCallback : public osgUtil::RenderBin::DrawCallback + { + public: + void drawImplementation( + osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override; + + void setFBO(const osg::ref_ptr& fbo, size_t frameId) { mFBO[frameId] = fbo; } + void setOriginalFBO(const osg::ref_ptr& fbo, size_t frameId) + { + mOriginalFBO[frameId] = fbo; + } + + private: + std::array, 2> mFBO; + std::array, 2> mOriginalFBO; + }; +} diff --git a/apps/openmw/mwrender/effectmanager.cpp b/apps/openmw/mwrender/effectmanager.cpp index 3e785a769ef..0ac509742cb 100644 --- a/apps/openmw/mwrender/effectmanager.cpp +++ b/apps/openmw/mwrender/effectmanager.cpp @@ -8,77 +8,79 @@ #include #include "animation.hpp" -#include "vismask.hpp" #include "util.hpp" +#include "vismask.hpp" + +#include namespace MWRender { -EffectManager::EffectManager(osg::ref_ptr parent, Resource::ResourceSystem* resourceSystem) - : mParentNode(parent) - , mResourceSystem(resourceSystem) -{ -} + EffectManager::EffectManager(osg::ref_ptr parent, Resource::ResourceSystem* resourceSystem) + : mParentNode(std::move(parent)) + , mResourceSystem(resourceSystem) + { + } -EffectManager::~EffectManager() -{ - clear(); -} + EffectManager::~EffectManager() + { + clear(); + } -void EffectManager::addEffect(const std::string &model, const std::string& textureOverride, const osg::Vec3f &worldPosition, float scale, bool isMagicVFX) -{ - osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model); + void EffectManager::addEffect(VFS::Path::NormalizedView model, std::string_view textureOverride, + const osg::Vec3f& worldPosition, float scale, bool isMagicVFX) + { + osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model); - node->setNodeMask(Mask_Effect); + node->setNodeMask(Mask_Effect); - Effect effect; - effect.mAnimTime.reset(new EffectAnimationTime); + Effect effect; + effect.mAnimTime = std::make_shared(); - SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; - node->accept(findMaxLengthVisitor); - effect.mMaxControllerLength = findMaxLengthVisitor.getMaxLength(); + SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; + node->accept(findMaxLengthVisitor); + effect.mMaxControllerLength = findMaxLengthVisitor.getMaxLength(); - osg::ref_ptr trans = new osg::PositionAttitudeTransform; - trans->setPosition(worldPosition); - trans->setScale(osg::Vec3f(scale, scale, scale)); - trans->addChild(node); + osg::ref_ptr trans = new osg::PositionAttitudeTransform; + trans->setPosition(worldPosition); + trans->setScale(osg::Vec3f(scale, scale, scale)); + trans->addChild(node); - SceneUtil::AssignControllerSourcesVisitor assignVisitor(effect.mAnimTime); - node->accept(assignVisitor); + effect.mTransform = trans; - if (isMagicVFX) - overrideFirstRootTexture(textureOverride, mResourceSystem, node); - else - overrideTexture(textureOverride, mResourceSystem, node); + SceneUtil::AssignControllerSourcesVisitor assignVisitor(effect.mAnimTime); + node->accept(assignVisitor); - mParentNode->addChild(trans); + if (isMagicVFX) + overrideFirstRootTexture(textureOverride, mResourceSystem, *node); + else + overrideTexture(textureOverride, mResourceSystem, *node); - mEffects[trans] = effect; -} + mParentNode->addChild(trans); -void EffectManager::update(float dt) -{ - for (EffectMap::iterator it = mEffects.begin(); it != mEffects.end(); ) - { - it->second.mAnimTime->addTime(dt); + mEffects.push_back(std::move(effect)); + } - if (it->second.mAnimTime->getTime() >= it->second.mMaxControllerLength) - { - mParentNode->removeChild(it->first); - mEffects.erase(it++); - } - else - ++it; + void EffectManager::update(float dt) + { + mEffects.erase(std::remove_if(mEffects.begin(), mEffects.end(), + [dt, this](Effect& effect) { + effect.mAnimTime->addTime(dt); + const auto remove = effect.mAnimTime->getTime() >= effect.mMaxControllerLength; + if (remove) + mParentNode->removeChild(effect.mTransform); + return remove; + }), + mEffects.end()); } -} -void EffectManager::clear() -{ - for (EffectMap::iterator it = mEffects.begin(); it != mEffects.end(); ++it) + void EffectManager::clear() { - mParentNode->removeChild(it->first); + for (const auto& effect : mEffects) + { + mParentNode->removeChild(effect.mTransform); + } + mEffects.clear(); } - mEffects.clear(); -} } diff --git a/apps/openmw/mwrender/effectmanager.hpp b/apps/openmw/mwrender/effectmanager.hpp index 5873c00dd8f..671c441a598 100644 --- a/apps/openmw/mwrender/effectmanager.hpp +++ b/apps/openmw/mwrender/effectmanager.hpp @@ -1,12 +1,13 @@ #ifndef OPENMW_MWRENDER_EFFECTMANAGER_H #define OPENMW_MWRENDER_EFFECTMANAGER_H -#include #include -#include +#include #include +#include + namespace osg { class Group; @@ -29,10 +30,12 @@ namespace MWRender { public: EffectManager(osg::ref_ptr parent, Resource::ResourceSystem* resourceSystem); + EffectManager(const EffectManager&) = delete; ~EffectManager(); /// Add an effect. When it's finished playing, it will be removed automatically. - void addEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPosition, float scale, bool isMagicVFX = true); + void addEffect(VFS::Path::NormalizedView model, std::string_view textureOverride, + const osg::Vec3f& worldPosition, float scale, bool isMagicVFX = true); void update(float dt); @@ -44,16 +47,13 @@ namespace MWRender { float mMaxControllerLength; std::shared_ptr mAnimTime; + osg::ref_ptr mTransform; }; - typedef std::map, Effect> EffectMap; - EffectMap mEffects; + std::vector mEffects; osg::ref_ptr mParentNode; Resource::ResourceSystem* mResourceSystem; - - EffectManager(const EffectManager&); - void operator=(const EffectManager&); }; } diff --git a/apps/openmw/mwrender/esm4npcanimation.cpp b/apps/openmw/mwrender/esm4npcanimation.cpp new file mode 100644 index 00000000000..3bff5883436 --- /dev/null +++ b/apps/openmw/mwrender/esm4npcanimation.cpp @@ -0,0 +1,189 @@ +#include "esm4npcanimation.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwclass/esm4npc.hpp" +#include "../mwworld/esmstore.hpp" + +namespace MWRender +{ + ESM4NpcAnimation::ESM4NpcAnimation( + const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem) + : Animation(ptr, std::move(parentNode), resourceSystem) + { + setObjectRoot(mPtr.getClass().getCorrectedModel(mPtr), true, true, false); + updateParts(); + } + + void ESM4NpcAnimation::updateParts() + { + if (mObjectRoot == nullptr) + return; + const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr); + if (traits == nullptr) + return; + if (traits->mIsTES4) + updatePartsTES4(*traits); + else if (traits->mIsFONV) + { + // Not implemented yet + } + else + { + // There is no easy way to distinguish TES5 and FO3. + // In case of FO3 the function shouldn't crash the game and will + // only lead to the NPC not being rendered. + updatePartsTES5(*traits); + } + } + + void ESM4NpcAnimation::insertPart(std::string_view model) + { + if (model.empty()) + return; + mResourceSystem->getSceneManager()->getInstance( + VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(model)), mObjectRoot.get()); + } + + template + static std::string_view chooseTes4EquipmentModel(const Record* rec, bool isFemale) + { + if (isFemale && !rec->mModelFemale.empty()) + return rec->mModelFemale; + else if (!isFemale && !rec->mModelMale.empty()) + return rec->mModelMale; + else + return rec->mModel; + } + + void ESM4NpcAnimation::updatePartsTES4(const ESM4::Npc& traits) + { + const ESM4::Race* race = MWClass::ESM4Npc::getRace(mPtr); + bool isFemale = MWClass::ESM4Npc::isFemale(mPtr); + + // TODO: Body and head parts are placed incorrectly, need to attach to bones + + for (const ESM4::Race::BodyPart& bodyPart : (isFemale ? race->mBodyPartsFemale : race->mBodyPartsMale)) + insertPart(bodyPart.mesh); + for (const ESM4::Race::BodyPart& bodyPart : race->mHeadParts) + insertPart(bodyPart.mesh); + if (!traits.mHair.isZeroOrUnset()) + { + const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); + if (const ESM4::Hair* hair = store->get().search(traits.mHair)) + insertPart(hair->mModel); + else + Log(Debug::Error) << "Hair not found: " << ESM::RefId(traits.mHair); + } + + for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr)) + insertPart(chooseTes4EquipmentModel(armor, isFemale)); + for (const ESM4::Clothing* clothing : MWClass::ESM4Npc::getEquippedClothing(mPtr)) + insertPart(chooseTes4EquipmentModel(clothing, isFemale)); + } + + void ESM4NpcAnimation::insertHeadParts( + const std::vector& partIds, std::set& usedHeadPartTypes) + { + const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); + for (ESM::FormId partId : partIds) + { + if (partId.isZeroOrUnset()) + continue; + const ESM4::HeadPart* part = store->get().search(partId); + if (!part) + { + Log(Debug::Error) << "Head part not found: " << ESM::RefId(partId); + continue; + } + if (usedHeadPartTypes.emplace(part->mType).second) + insertPart(part->mModel); + } + } + + void ESM4NpcAnimation::updatePartsTES5(const ESM4::Npc& traits) + { + const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); + + const ESM4::Race* race = MWClass::ESM4Npc::getRace(mPtr); + bool isFemale = MWClass::ESM4Npc::isFemale(mPtr); + + std::vector armorAddons; + + auto findArmorAddons = [&](const ESM4::Armor* armor) { + for (ESM::FormId armaId : armor->mAddOns) + { + if (armaId.isZeroOrUnset()) + continue; + const ESM4::ArmorAddon* arma = store->get().search(armaId); + if (!arma) + { + Log(Debug::Error) << "ArmorAddon not found: " << ESM::RefId(armaId); + continue; + } + bool compatibleRace = arma->mRacePrimary == traits.mRace; + for (auto r : arma->mRaces) + if (r == traits.mRace) + compatibleRace = true; + if (compatibleRace) + armorAddons.push_back(arma); + } + }; + + for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr)) + findArmorAddons(armor); + if (!traits.mWornArmor.isZeroOrUnset()) + { + if (const ESM4::Armor* armor = store->get().search(traits.mWornArmor)) + findArmorAddons(armor); + else + Log(Debug::Error) << "Worn armor not found: " << ESM::RefId(traits.mWornArmor); + } + if (!race->mSkin.isZeroOrUnset()) + { + if (const ESM4::Armor* armor = store->get().search(race->mSkin)) + findArmorAddons(armor); + else + Log(Debug::Error) << "Skin not found: " << ESM::RefId(race->mSkin); + } + + if (isFemale) + std::sort(armorAddons.begin(), armorAddons.end(), + [](auto x, auto y) { return x->mFemalePriority > y->mFemalePriority; }); + else + std::sort(armorAddons.begin(), armorAddons.end(), + [](auto x, auto y) { return x->mMalePriority > y->mMalePriority; }); + + uint32_t usedParts = 0; + for (const ESM4::ArmorAddon* arma : armorAddons) + { + const uint32_t covers = arma->mBodyTemplate.bodyPart; + // if body is already covered, skip to avoid clipping + if (covers & usedParts & ESM4::Armor::TES5_Body) + continue; + // if covers at least something that wasn't covered before - add model + if (covers & ~usedParts) + { + usedParts |= covers; + insertPart(isFemale ? arma->mModelFemale : arma->mModelMale); + } + } + + std::set usedHeadPartTypes; + if (usedParts & ESM4::Armor::TES5_Hair) + usedHeadPartTypes.insert(ESM4::HeadPart::Type_Hair); + insertHeadParts(traits.mHeadParts, usedHeadPartTypes); + insertHeadParts(isFemale ? race->mHeadPartIdsFemale : race->mHeadPartIdsMale, usedHeadPartTypes); + } +} diff --git a/apps/openmw/mwrender/esm4npcanimation.hpp b/apps/openmw/mwrender/esm4npcanimation.hpp new file mode 100644 index 00000000000..274c060b066 --- /dev/null +++ b/apps/openmw/mwrender/esm4npcanimation.hpp @@ -0,0 +1,31 @@ +#ifndef GAME_RENDER_ESM4NPCANIMATION_H +#define GAME_RENDER_ESM4NPCANIMATION_H + +#include "animation.hpp" + +namespace ESM4 +{ + struct Npc; +} + +namespace MWRender +{ + class ESM4NpcAnimation : public Animation + { + public: + ESM4NpcAnimation( + const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem); + + private: + void insertPart(std::string_view model); + + // Works for FO3/FONV/TES5 + void insertHeadParts(const std::vector& partIds, std::set& usedHeadPartTypes); + + void updateParts(); + void updatePartsTES4(const ESM4::Npc& traits); + void updatePartsTES5(const ESM4::Npc& traits); + }; +} + +#endif // GAME_RENDER_ESM4NPCANIMATION_H diff --git a/apps/openmw/mwrender/fogmanager.cpp b/apps/openmw/mwrender/fogmanager.cpp index b151882922a..b75fb507ede 100644 --- a/apps/openmw/mwrender/fogmanager.cpp +++ b/apps/openmw/mwrender/fogmanager.cpp @@ -2,20 +2,14 @@ #include -#include +#include +#include +#include #include #include -#include +#include -namespace -{ - float DLLandFogStart; - float DLLandFogEnd; - float DLUnderwaterFogStart; - float DLUnderwaterFogEnd; - float DLInteriorFogStart; - float DLInteriorFogEnd; -} +#include "apps/openmw/mwworld/cell.hpp" namespace MWRender { @@ -25,44 +19,41 @@ namespace MWRender , mUnderwaterFogStart(0.f) , mUnderwaterFogEnd(std::numeric_limits::max()) , mFogColor(osg::Vec4f()) - , mDistantFog(Settings::Manager::getBool("use distant fog", "Fog")) , mUnderwaterColor(Fallback::Map::getColour("Water_UnderwaterColor")) , mUnderwaterWeight(Fallback::Map::getFloat("Water_UnderwaterColorWeight")) , mUnderwaterIndoorFog(Fallback::Map::getFloat("Water_UnderwaterIndoorFog")) { - DLLandFogStart = Settings::Manager::getFloat("distant land fog start", "Fog"); - DLLandFogEnd = Settings::Manager::getFloat("distant land fog end", "Fog"); - DLUnderwaterFogStart = Settings::Manager::getFloat("distant underwater fog start", "Fog"); - DLUnderwaterFogEnd = Settings::Manager::getFloat("distant underwater fog end", "Fog"); - DLInteriorFogStart = Settings::Manager::getFloat("distant interior fog start", "Fog"); - DLInteriorFogEnd = Settings::Manager::getFloat("distant interior fog end", "Fog"); } - void FogManager::configure(float viewDistance, const ESM::Cell *cell) + void FogManager::configure(float viewDistance, const MWWorld::Cell& cell) { - osg::Vec4f color = SceneUtil::colourFromRGB(cell->mAmbi.mFog); + osg::Vec4f color = SceneUtil::colourFromRGB(cell.getMood().mFogColor); - if (mDistantFog) + const float fogDensity = cell.getMood().mFogDensity; + if (Settings::fog().mUseDistantFog) { - float density = std::max(0.2f, cell->mAmbi.mFogDensity); - mLandFogStart = DLInteriorFogEnd * (1.0f - density) + DLInteriorFogStart*density; - mLandFogEnd = DLInteriorFogEnd; - mUnderwaterFogStart = DLUnderwaterFogStart; - mUnderwaterFogEnd = DLUnderwaterFogEnd; + float density = std::max(0.2f, fogDensity); + mLandFogStart = Settings::fog().mDistantInteriorFogEnd * (1.0f - density) + + Settings::fog().mDistantInteriorFogStart * density; + mLandFogEnd = Settings::fog().mDistantInteriorFogEnd; + mUnderwaterFogStart = Settings::fog().mDistantUnderwaterFogStart; + mUnderwaterFogEnd = Settings::fog().mDistantUnderwaterFogEnd; mFogColor = color; } else - configure(viewDistance, cell->mAmbi.mFogDensity, mUnderwaterIndoorFog, 1.0f, 0.0f, color); + configure(viewDistance, fogDensity, mUnderwaterIndoorFog, 1.0f, 0.0f, color); } - void FogManager::configure(float viewDistance, float fogDepth, float underwaterFog, float dlFactor, float dlOffset, const osg::Vec4f &color) + void FogManager::configure(float viewDistance, float fogDepth, float underwaterFog, float dlFactor, float dlOffset, + const osg::Vec4f& color) { - if (mDistantFog) + if (Settings::fog().mUseDistantFog) { - mLandFogStart = dlFactor * (DLLandFogStart - dlOffset * DLLandFogEnd); - mLandFogEnd = dlFactor * (1.0f - dlOffset) * DLLandFogEnd; - mUnderwaterFogStart = DLUnderwaterFogStart; - mUnderwaterFogEnd = DLUnderwaterFogEnd; + mLandFogStart + = dlFactor * (Settings::fog().mDistantLandFogStart - dlOffset * Settings::fog().mDistantLandFogEnd); + mLandFogEnd = dlFactor * (1.0f - dlOffset) * Settings::fog().mDistantLandFogEnd; + mUnderwaterFogStart = Settings::fog().mDistantUnderwaterFogStart; + mUnderwaterFogEnd = Settings::fog().mDistantUnderwaterFogEnd; } else { @@ -96,7 +87,7 @@ namespace MWRender { if (isUnderwater) { - return mUnderwaterColor * mUnderwaterWeight + mFogColor * (1.f-mUnderwaterWeight); + return mUnderwaterColor * mUnderwaterWeight + mFogColor * (1.f - mUnderwaterWeight); } return mFogColor; diff --git a/apps/openmw/mwrender/fogmanager.hpp b/apps/openmw/mwrender/fogmanager.hpp index c3efd06ab72..a704f0efdcb 100644 --- a/apps/openmw/mwrender/fogmanager.hpp +++ b/apps/openmw/mwrender/fogmanager.hpp @@ -3,9 +3,9 @@ #include -namespace ESM +namespace MWWorld { - struct Cell; + class Cell; } namespace MWRender @@ -15,8 +15,9 @@ namespace MWRender public: FogManager(); - void configure(float viewDistance, const ESM::Cell *cell); - void configure(float viewDistance, float fogDepth, float underwaterFog, float dlFactor, float dlOffset, const osg::Vec4f &color); + void configure(float viewDistance, const MWWorld::Cell& cell); + void configure(float viewDistance, float fogDepth, float underwaterFog, float dlFactor, float dlOffset, + const osg::Vec4f& color); osg::Vec4f getFogColor(bool isUnderwater) const; float getFogStart(bool isUnderwater) const; @@ -28,8 +29,6 @@ namespace MWRender float mUnderwaterFogStart; float mUnderwaterFogEnd; osg::Vec4f mFogColor; - bool mDistantFog; - osg::Vec4f mUnderwaterColor; float mUnderwaterWeight; float mUnderwaterIndoorFog; diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index 8784eb501ae..f9dae65c40c 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -1,25 +1,26 @@ #include "globalmap.hpp" -#include -#include -#include #include -#include +#include +#include #include +#include #include -#include #include +#include #include +#include +#include #include -#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" @@ -30,7 +31,8 @@ namespace // Create a screen-aligned quad with given texture coordinates. // Assumes a top-left origin of the sampled image. - osg::ref_ptr createTexturedQuad(float leftTexCoord, float topTexCoord, float rightTexCoord, float bottomTexCoord) + osg::ref_ptr createTexturedQuad( + float leftTexCoord, float topTexCoord, float rightTexCoord, float bottomTexCoord) { osg::ref_ptr geom = new osg::Geometry; @@ -43,10 +45,10 @@ namespace geom->setVertexArray(verts); osg::ref_ptr texcoords = new osg::Vec2Array; - texcoords->push_back(osg::Vec2f(leftTexCoord, 1.f-bottomTexCoord)); - texcoords->push_back(osg::Vec2f(leftTexCoord, 1.f-topTexCoord)); - texcoords->push_back(osg::Vec2f(rightTexCoord, 1.f-topTexCoord)); - texcoords->push_back(osg::Vec2f(rightTexCoord, 1.f-bottomTexCoord)); + texcoords->push_back(osg::Vec2f(leftTexCoord, 1.f - bottomTexCoord)); + texcoords->push_back(osg::Vec2f(leftTexCoord, 1.f - topTexCoord)); + texcoords->push_back(osg::Vec2f(rightTexCoord, 1.f - topTexCoord)); + texcoords->push_back(osg::Vec2f(rightTexCoord, 1.f - bottomTexCoord)); osg::ref_ptr colors = new osg::Vec4Array; colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); @@ -54,13 +56,12 @@ namespace geom->setTexCoordArray(0, texcoords, osg::Array::BIND_PER_VERTEX); - geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); + geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS, 0, 4)); return geom; } - - class CameraUpdateGlobalCallback : public osg::NodeCallback + class CameraUpdateGlobalCallback : public SceneUtil::NodeCallback { public: CameraUpdateGlobalCallback(MWRender::GlobalMap* parent) @@ -69,14 +70,14 @@ namespace { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::Camera* node, osg::NodeVisitor* nv) { if (mRendered) { - if (mParent->copyResult(static_cast(node), nv->getTraversalNumber())) + if (mParent->copyResult(node, nv->getTraversalNumber())) { node->setNodeMask(0); - mParent->markForRemoval(static_cast(node)); + mParent->markForRemoval(node); } return; } @@ -91,6 +92,27 @@ namespace MWRender::GlobalMap* mParent; }; + std::vector writePng(const osg::Image& overlayImage) + { + std::ostringstream ostream; + osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); + if (!readerwriter) + { + Log(Debug::Error) << "Error: Can't write map overlay: no png readerwriter found"; + return std::vector(); + } + + osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(overlayImage, ostream); + if (!result.success()) + { + Log(Debug::Warning) << "Error: Can't write map overlay: " << result.message() << " code " + << result.status(); + return std::vector(); + } + + std::string data = ostream.str(); + return std::vector(data.begin(), data.end()); + } } namespace MWRender @@ -99,8 +121,16 @@ namespace MWRender class CreateMapWorkItem : public SceneUtil::WorkItem { public: - CreateMapWorkItem(int width, int height, int minX, int minY, int maxX, int maxY, int cellSize, const MWWorld::Store& landStore) - : mWidth(width), mHeight(height), mMinX(minX), mMinY(minY), mMaxX(maxX), mMaxY(maxY), mCellSize(cellSize), mLandStore(landStore) + CreateMapWorkItem(int width, int height, int minX, int minY, int maxX, int maxY, int cellSize, + const MWWorld::Store& landStore) + : mWidth(width) + , mHeight(height) + , mMinX(minX) + , mMinY(minY) + , mMaxX(maxX) + , mMaxY(maxY) + , mCellSize(cellSize) + , mLandStore(landStore) { } @@ -118,19 +148,19 @@ namespace MWRender { for (int y = mMinY; y <= mMaxY; ++y) { - const ESM::Land* land = mLandStore.search (x,y); + const ESM::Land* land = mLandStore.search(x, y); - for (int cellY=0; cellY(float(cellX) / float(mCellSize) * 9); int vertexY = static_cast(float(cellY) / float(mCellSize) * 9); - int texelX = (x-mMinX) * mCellSize + cellX; - int texelY = (y-mMinY) * mCellSize + cellY; + int texelX = (x - mMinX) * mCellSize + cellX; + int texelY = (y - mMinY) * mCellSize + cellY; - unsigned char r,g,b; + unsigned char r, g, b; float y2 = 0; if (land && (land->mDataTypes & ESM::Land::DATA_WNAM)) @@ -166,10 +196,11 @@ namespace MWRender } data[texelY * mWidth * 3 + texelX * 3] = r; - data[texelY * mWidth * 3 + texelX * 3+1] = g; - data[texelY * mWidth * 3 + texelX * 3+2] = b; + data[texelY * mWidth * 3 + texelX * 3 + 1] = g; + data[texelY * mWidth * 3 + texelX * 3 + 2] = b; - alphaData[texelY * mWidth+ texelX] = (y2 < 0) ? static_cast(0) : static_cast(255); + alphaData[texelY * mWidth + texelX] + = (y2 < 0) ? static_cast(0) : static_cast(255); } } } @@ -219,16 +250,29 @@ namespace MWRender osg::ref_ptr mOverlayTexture; }; + struct GlobalMap::WritePng final : public SceneUtil::WorkItem + { + osg::ref_ptr mOverlayImage; + std::vector mImageData; + + explicit WritePng(osg::ref_ptr overlayImage) + : mOverlayImage(std::move(overlayImage)) + { + } + + void doWork() override { mImageData = writePng(*mOverlayImage); } + }; + GlobalMap::GlobalMap(osg::Group* root, SceneUtil::WorkQueue* workQueue) : mRoot(root) , mWorkQueue(workQueue) , mWidth(0) , mHeight(0) - , mMinX(0), mMaxX(0) - , mMinY(0), mMaxY(0) - + , mMinX(0) + , mMaxX(0) + , mMinY(0) + , mMaxY(0) { - mCellSize = Settings::Manager::getInt("global map cell size", "Map"); } GlobalMap::~GlobalMap() @@ -242,10 +286,9 @@ namespace MWRender mWorkItem->waitTillDone(); } - void GlobalMap::render () + void GlobalMap::render() { - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); // get the size of the world MWWorld::Store::iterator it = esmStore.get().extBegin(); @@ -261,32 +304,28 @@ namespace MWRender mMaxY = it->getGridY(); } - mWidth = mCellSize*(mMaxX-mMinX+1); - mHeight = mCellSize*(mMaxY-mMinY+1); + const int cellSize = Settings::map().mGlobalMapCellSize; + + mWidth = cellSize * (mMaxX - mMinX + 1); + mHeight = cellSize * (mMaxY - mMinY + 1); - mWorkItem = new CreateMapWorkItem(mWidth, mHeight, mMinX, mMinY, mMaxX, mMaxY, mCellSize, esmStore.get()); + mWorkItem + = new CreateMapWorkItem(mWidth, mHeight, mMinX, mMinY, mMaxX, mMaxY, cellSize, esmStore.get()); mWorkQueue->addWorkItem(mWorkItem); } void GlobalMap::worldPosToImageSpace(float x, float z, float& imageX, float& imageY) { - imageX = float(x / float(Constants::CellSizeInUnits) - mMinX) / (mMaxX - mMinX + 1); + imageX = (float(x / float(Constants::CellSizeInUnits) - mMinX) / (mMaxX - mMinX + 1)) * getWidth(); - imageY = 1.f-float(z / float(Constants::CellSizeInUnits) - mMinY) / (mMaxY - mMinY + 1); + imageY = (1.f - float(z / float(Constants::CellSizeInUnits) - mMinY) / (mMaxY - mMinY + 1)) * getHeight(); } - void GlobalMap::cellTopLeftCornerToImageSpace(int x, int y, float& imageX, float& imageY) + void GlobalMap::requestOverlayTextureUpdate(int x, int y, int width, int height, + osg::ref_ptr texture, bool clear, bool cpuCopy, float srcLeft, float srcTop, float srcRight, + float srcBottom) { - imageX = float(x - mMinX) / (mMaxX - mMinX + 1); - - // NB y + 1, because we want the top left corner, not bottom left where the origin of the cell is - imageY = 1.f-float(y - mMinY + 1) / (mMaxY - mMinY + 1); - } - - void GlobalMap::requestOverlayTextureUpdate(int x, int y, int width, int height, osg::ref_ptr texture, bool clear, bool cpuCopy, - float srcLeft, float srcTop, float srcRight, float srcBottom) - { - osg::ref_ptr camera (new osg::Camera); + osg::ref_ptr camera(new osg::Camera); camera->setNodeMask(Mask_RenderToTexture); camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); camera->setViewMatrix(osg::Matrix::identity()); @@ -299,7 +338,7 @@ namespace MWRender if (clear) { camera->setClearMask(GL_COLOR_BUFFER_BIT); - camera->setClearColor(osg::Vec4(0,0,0,0)); + camera->setClearColor(osg::Vec4(0, 0, 0, 0)); } else camera->setClearMask(GL_NONE); @@ -315,7 +354,7 @@ namespace MWRender if (cpuCopy) { // Attach an image to copy the render back to the CPU when finished - osg::ref_ptr image (new osg::Image); + osg::ref_ptr image(new osg::Image); image->setPixelFormat(mOverlayImage->getPixelFormat()); image->setDataType(mOverlayImage->getDataType()); camera->attach(osg::Camera::COLOR_BUFFER, image); @@ -324,15 +363,15 @@ namespace MWRender imageDest.mImage = image; imageDest.mX = x; imageDest.mY = y; - mPendingImageDest[camera] = imageDest; + mPendingImageDest[camera] = std::move(imageDest); } // Create a quad rendering the updated texture if (texture) { osg::ref_ptr geom = createTexturedQuad(srcLeft, srcTop, srcRight, srcBottom); - osg::ref_ptr depth = new osg::Depth; - depth->setWriteMask(0); + osg::ref_ptr depth = new SceneUtil::AutoDepth; + depth->setWriteMask(false); osg::StateSet* stateset = geom->getOrCreateStateSet(); stateset->setAttribute(depth); stateset->setTextureAttributeAndModes(0, texture, osg::StateAttribute::ON); @@ -375,13 +414,16 @@ namespace MWRender if (!localMapTexture) return; - int originX = (cellX - mMinX) * mCellSize; - int originY = (cellY - mMinY + 1) * mCellSize; // +1 because we want the top left corner of the cell, not the bottom left + const int cellSize = Settings::map().mGlobalMapCellSize; + const int originX = (cellX - mMinX) * cellSize; + // +1 because we want the top left corner of the cell, not the bottom left + const int originY = (cellY - mMinY + 1) * cellSize; if (cellX > mMaxX || cellX < mMinX || cellY > mMaxY || cellY < mMinY) return; - requestOverlayTextureUpdate(originX, mHeight - originY, mCellSize, mCellSize, localMapTexture, false, true); + requestOverlayTextureUpdate( + originX, mHeight - originY, cellSize, cellSize, std::move(localMapTexture), false, true); } void GlobalMap::clear() @@ -406,23 +448,15 @@ namespace MWRender map.mBounds.mMinY = mMinY; map.mBounds.mMaxY = mMaxY; - std::ostringstream ostream; - osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); - if (!readerwriter) - { - Log(Debug::Error) << "Error: Can't write map overlay: no png readerwriter found"; - return; - } - - osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*mOverlayImage, ostream); - if (!result.success()) + if (mWritePng != nullptr) { - Log(Debug::Warning) << "Error: Can't write map overlay: " << result.message() << " code " << result.status(); + mWritePng->waitTillDone(); + map.mImageData = std::move(mWritePng->mImageData); + mWritePng = nullptr; return; } - std::string data = ostream.str(); - map.mImageData = std::vector(data.begin(), data.end()); + map.mImageData = writePng(*mOverlayImage); } struct Box @@ -430,10 +464,13 @@ namespace MWRender int mLeft, mTop, mRight, mBottom; Box(int left, int top, int right, int bottom) - : mLeft(left), mTop(top), mRight(right), mBottom(bottom) + : mLeft(left) + , mTop(top) + , mRight(right) + , mBottom(bottom) { } - bool operator == (const Box& other) + bool operator==(const Box& other) const { return mLeft == other.mLeft && mTop == other.mTop && mRight == other.mRight && mBottom == other.mBottom; } @@ -445,13 +482,12 @@ namespace MWRender const ESM::GlobalMap::Bounds& bounds = map.mBounds; - if (bounds.mMaxX-bounds.mMinX < 0) + if (bounds.mMaxX - bounds.mMinX < 0) return; - if (bounds.mMaxY-bounds.mMinY < 0) + if (bounds.mMaxY - bounds.mMinY < 0) return; - if (bounds.mMinX > bounds.mMaxX - || bounds.mMinY > bounds.mMaxY) + if (bounds.mMinX > bounds.mMaxX || bounds.mMinY > bounds.mMaxY) throw std::runtime_error("invalid map bounds"); if (map.mImageData.empty()) @@ -477,8 +513,8 @@ namespace MWRender int imageWidth = image->s(); int imageHeight = image->t(); - int xLength = (bounds.mMaxX-bounds.mMinX+1); - int yLength = (bounds.mMaxY-bounds.mMinY+1); + int xLength = (bounds.mMaxX - bounds.mMinX + 1); + int yLength = (bounds.mMaxY - bounds.mMinY + 1); // Size of one cell in image space int cellImageSizeSrc = imageWidth / xLength; @@ -488,31 +524,26 @@ namespace MWRender // If cell bounds of the currently loaded content and the loaded savegame do not match, // we need to resize source/dest boxes to accommodate // This means nonexisting cells will be dropped silently - int cellImageSizeDst = mCellSize; + const int cellImageSizeDst = Settings::map().mGlobalMapCellSize; // Completely off-screen? -> no need to blit anything - if (bounds.mMaxX < mMinX - || bounds.mMaxY < mMinY - || bounds.mMinX > mMaxX - || bounds.mMinY > mMaxY) + if (bounds.mMaxX < mMinX || bounds.mMaxY < mMinY || bounds.mMinX > mMaxX || bounds.mMinY > mMaxY) return; int leftDiff = (mMinX - bounds.mMinX); int topDiff = (bounds.mMaxY - mMaxY); int rightDiff = (bounds.mMaxX - mMaxX); - int bottomDiff = (mMinY - bounds.mMinY); + int bottomDiff = (mMinY - bounds.mMinY); - Box srcBox ( std::max(0, leftDiff * cellImageSizeSrc), - std::max(0, topDiff * cellImageSizeSrc), - std::min(imageWidth, imageWidth - rightDiff * cellImageSizeSrc), - std::min(imageHeight, imageHeight - bottomDiff * cellImageSizeSrc)); + Box srcBox(std::max(0, leftDiff * cellImageSizeSrc), std::max(0, topDiff * cellImageSizeSrc), + std::min(imageWidth, imageWidth - rightDiff * cellImageSizeSrc), + std::min(imageHeight, imageHeight - bottomDiff * cellImageSizeSrc)); - Box destBox ( std::max(0, -leftDiff * cellImageSizeDst), - std::max(0, -topDiff * cellImageSizeDst), - std::min(mWidth, mWidth + rightDiff * cellImageSizeDst), - std::min(mHeight, mHeight + bottomDiff * cellImageSizeDst)); + Box destBox(std::max(0, -leftDiff * cellImageSizeDst), std::max(0, -topDiff * cellImageSizeDst), + std::min(mWidth, mWidth + rightDiff * cellImageSizeDst), + std::min(mHeight, mHeight + bottomDiff * cellImageSizeDst)); - osg::ref_ptr texture (new osg::Texture2D); + osg::ref_ptr texture(new osg::Texture2D); texture->setImage(image); texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); @@ -524,16 +555,17 @@ namespace MWRender { mOverlayImage = image; - requestOverlayTextureUpdate(0, 0, mWidth, mHeight, texture, true, false); + requestOverlayTextureUpdate(0, 0, mWidth, mHeight, std::move(texture), true, false); } else { // Dimensions don't match. This could mean a changed map region, or a changed map resolution. // In the latter case, we'll want filtering. // Create a RTT Camera and draw the image onto mOverlayImage in the next frame. - requestOverlayTextureUpdate(destBox.mLeft, destBox.mTop, destBox.mRight-destBox.mLeft, destBox.mBottom-destBox.mTop, texture, true, true, - srcBox.mLeft/float(imageWidth), srcBox.mTop/float(imageHeight), - srcBox.mRight/float(imageWidth), srcBox.mBottom/float(imageHeight)); + requestOverlayTextureUpdate(destBox.mLeft, destBox.mTop, destBox.mRight - destBox.mLeft, + destBox.mBottom - destBox.mTop, std::move(texture), true, true, srcBox.mLeft / float(imageWidth), + srcBox.mTop / float(imageHeight), srcBox.mRight / float(imageWidth), + srcBox.mBottom / float(imageHeight)); } } @@ -566,7 +598,7 @@ namespace MWRender } } - bool GlobalMap::copyResult(osg::Camera *camera, unsigned int frame) + bool GlobalMap::copyResult(osg::Camera* camera, unsigned int frame) { ImageDestMap::iterator it = mPendingImageDest.find(camera); if (it == mPendingImageDest.end()) @@ -574,7 +606,9 @@ namespace MWRender else { ImageDest& imageDest = it->second; - if (imageDest.mFrameDone == 0) imageDest.mFrameDone = frame+2; // wait an extra frame to ensure the draw thread has completed its frame. + if (imageDest.mFrameDone == 0) + imageDest.mFrameDone + = frame + 2; // wait an extra frame to ensure the draw thread has completed its frame. if (imageDest.mFrameDone > frame) { ++it; @@ -587,7 +621,7 @@ namespace MWRender } } - void GlobalMap::markForRemoval(osg::Camera *camera) + void GlobalMap::markForRemoval(osg::Camera* camera) { CameraVector::iterator found = std::find(mActiveCameras.begin(), mActiveCameras.end(), camera); if (found == mActiveCameras.end()) @@ -607,9 +641,18 @@ namespace MWRender mCamerasPendingRemoval.clear(); } - void GlobalMap::removeCamera(osg::Camera *cam) + void GlobalMap::removeCamera(osg::Camera* cam) { cam->removeChildren(0, cam->getNumChildren()); mRoot->removeChild(cam); } + + void GlobalMap::asyncWritePng() + { + if (mOverlayImage == nullptr) + return; + // Use deep copy to avoid any sychronization + mWritePng = new WritePng(new osg::Image(*mOverlayImage, osg::CopyOp::DEEP_COPY_ALL)); + mWorkQueue->addWorkItem(mWritePng, /*front=*/true); + } } diff --git a/apps/openmw/mwrender/globalmap.hpp b/apps/openmw/mwrender/globalmap.hpp index b359c852be6..07d7731e313 100644 --- a/apps/openmw/mwrender/globalmap.hpp +++ b/apps/openmw/mwrender/globalmap.hpp @@ -1,9 +1,9 @@ #ifndef GAME_RENDER_GLOBALMAP_H #define GAME_RENDER_GLOBALMAP_H +#include #include #include -#include #include @@ -41,13 +41,9 @@ namespace MWRender int getWidth() const { return mWidth; } int getHeight() const { return mHeight; } - int getCellSize() const { return mCellSize; } - void worldPosToImageSpace(float x, float z, float& imageX, float& imageY); - void cellTopLeftCornerToImageSpace(int x, int y, float& imageX, float& imageY); - - void exploreCell (int cellX, int cellY, osg::ref_ptr localMapTexture); + void exploreCell(int cellX, int cellY, osg::ref_ptr localMapTexture); /// Clears the overlay void clear(); @@ -68,28 +64,31 @@ namespace MWRender */ void markForRemoval(osg::Camera* camera); - void write (ESM::GlobalMap& map); - void read (ESM::GlobalMap& map); + void write(ESM::GlobalMap& map); + void read(ESM::GlobalMap& map); osg::ref_ptr getBaseTexture(); osg::ref_ptr getOverlayTexture(); void ensureLoaded(); + void asyncWritePng(); + private: + struct WritePng; + /** * Request rendering a 2d quad onto mOverlayTexture. * x, y, width and height are the destination coordinates (top-left coordinate origin) * @param cpuCopy copy the resulting render onto mOverlayImage as well? */ - void requestOverlayTextureUpdate(int x, int y, int width, int height, osg::ref_ptr texture, bool clear, bool cpuCopy, - float srcLeft = 0.f, float srcTop = 0.f, float srcRight = 1.f, float srcBottom = 1.f); - - int mCellSize; + void requestOverlayTextureUpdate(int x, int y, int width, int height, osg::ref_ptr texture, + bool clear, bool cpuCopy, float srcLeft = 0.f, float srcTop = 0.f, float srcRight = 1.f, + float srcBottom = 1.f); osg::ref_ptr mRoot; - typedef std::vector > CameraVector; + typedef std::vector> CameraVector; CameraVector mActiveCameras; CameraVector mCamerasPendingRemoval; @@ -97,7 +96,8 @@ namespace MWRender struct ImageDest { ImageDest() - : mX(0), mY(0) + : mX(0) + , mY(0) , mFrameDone(0) { } @@ -111,8 +111,6 @@ namespace MWRender ImageDestMap mPendingImageDest; - std::vector< std::pair > mExploredCells; - osg::ref_ptr mBaseTexture; osg::ref_ptr mAlphaTexture; @@ -125,6 +123,7 @@ namespace MWRender osg::ref_ptr mWorkQueue; osg::ref_ptr mWorkItem; + osg::ref_ptr mWritePng; int mWidth; int mHeight; @@ -135,4 +134,3 @@ namespace MWRender } #endif - diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp index 63df3e24974..44608ca3c3f 100644 --- a/apps/openmw/mwrender/groundcover.cpp +++ b/apps/openmw/mwrender/groundcover.cpp @@ -1,190 +1,335 @@ #include "groundcover.hpp" #include +#include +#include #include +#include #include +#include -#include +#include +#include +#include +#include #include +#include +#include +#include +#include -#include "apps/openmw/mwworld/esmstore.hpp" -#include "apps/openmw/mwbase/environment.hpp" -#include "apps/openmw/mwbase/world.hpp" +#include "../mwworld/groundcoverstore.hpp" #include "vismask.hpp" namespace MWRender { - std::string getGroundcoverModel(int type, const std::string& id, const MWWorld::ESMStore& store) + namespace { - switch (type) + using value_type = osgUtil::CullVisitor::value_type; + + // From OSG's CullVisitor.cpp + inline value_type distance(const osg::Vec3& coord, const osg::Matrix& matrix) { - case ESM::REC_STAT: - return store.get().searchStatic(id)->mModel; - default: - return std::string(); + return -((value_type)coord[0] * (value_type)matrix(0, 2) + (value_type)coord[1] * (value_type)matrix(1, 2) + + (value_type)coord[2] * (value_type)matrix(2, 2) + matrix(3, 2)); } - } - void GroundcoverUpdater::setWindSpeed(float windSpeed) - { - mWindSpeed = windSpeed; - } + inline osg::Matrix computeInstanceMatrix( + const Groundcover::GroundcoverEntry& entry, const osg::Vec3& chunkPosition) + { + return osg::Matrix::scale(entry.mScale, entry.mScale, entry.mScale) + * osg::Matrix(Misc::Convert::makeOsgQuat(entry.mPos)) + * osg::Matrix::translate(entry.mPos.asVec3() - chunkPosition); + } - void GroundcoverUpdater::setPlayerPos(osg::Vec3f playerPos) - { - mPlayerPos = playerPos; - } + class InstancedComputeNearFarCullCallback : public osg::DrawableCullCallback + { + public: + InstancedComputeNearFarCullCallback(const std::vector& instances, + const osg::Vec3& chunkPosition, const osg::BoundingBox& instanceBounds) + : mInstanceMatrices() + , mInstanceBounds(instanceBounds) + { + mInstanceMatrices.reserve(instances.size()); + for (const auto& instance : instances) + mInstanceMatrices.emplace_back(computeInstanceMatrix(instance, chunkPosition)); + } - void GroundcoverUpdater::setDefaults(osg::StateSet *stateset) - { - osg::ref_ptr windUniform = new osg::Uniform("windSpeed", 0.0f); - stateset->addUniform(windUniform.get()); + bool cull(osg::NodeVisitor* nv, osg::Drawable* drawable, osg::RenderInfo* renderInfo) const override + { + osgUtil::CullVisitor& cullVisitor = *nv->asCullVisitor(); + osg::CullSettings::ComputeNearFarMode cnfMode = cullVisitor.getComputeNearFarMode(); + const osg::BoundingBox& boundingBox = drawable->getBoundingBox(); + osg::RefMatrix& matrix = *cullVisitor.getModelViewMatrix(); - osg::ref_ptr playerPosUniform = new osg::Uniform("playerPos", osg::Vec3f(0.f, 0.f, 0.f)); - stateset->addUniform(playerPosUniform.get()); - } + if (cnfMode != osg::CullSettings::COMPUTE_NEAR_FAR_USING_PRIMITIVES + && cnfMode != osg::CullSettings::COMPUTE_NEAR_USING_PRIMITIVES) + return false; - void GroundcoverUpdater::apply(osg::StateSet *stateset, osg::NodeVisitor *nv) - { - osg::ref_ptr windUniform = stateset->getUniform("windSpeed"); - if (windUniform != nullptr) - windUniform->set(mWindSpeed); + if (drawable->isCullingActive() && cullVisitor.isCulled(boundingBox)) + return true; - osg::ref_ptr playerPosUniform = stateset->getUniform("playerPos"); - if (playerPosUniform != nullptr) - playerPosUniform->set(mPlayerPos); - } + osg::Vec3 lookVector = cullVisitor.getLookVectorLocal(); + unsigned int bbCornerFar + = (lookVector.x() >= 0 ? 1 : 0) | (lookVector.y() >= 0 ? 2 : 0) | (lookVector.z() >= 0 ? 4 : 0); + unsigned int bbCornerNear = (~bbCornerFar) & 7; + value_type dNear = distance(boundingBox.corner(bbCornerNear), matrix); + value_type dFar = distance(boundingBox.corner(bbCornerFar), matrix); - class InstancingVisitor : public osg::NodeVisitor - { - public: - InstancingVisitor(std::vector& instances, osg::Vec3f& chunkPosition) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mInstances(instances) - , mChunkPosition(chunkPosition) - { - } + if (dNear > dFar) + std::swap(dNear, dFar); - void apply(osg::Node& node) override - { - osg::ref_ptr ss = node.getStateSet(); - if (ss != nullptr) - { - ss->removeAttribute(osg::StateAttribute::MATERIAL); - removeAlpha(ss); + if (dFar < 0) + return true; + + value_type computedZNear = cullVisitor.getCalculatedNearPlane(); + value_type computedZFar = cullVisitor.getCalculatedFarPlane(); + + if (dNear < computedZNear || dFar > computedZFar) + { + osg::Polytope frustum; + osg::Polytope::ClippingMask resultMask + = cullVisitor.getCurrentCullingSet().getFrustum().getResultMask(); + if (resultMask) + { + // Other objects are likely cheaper and should let us skip all but a few groundcover instances + cullVisitor.computeNearPlane(); + computedZNear = cullVisitor.getCalculatedNearPlane(); + computedZFar = cullVisitor.getCalculatedFarPlane(); + + if (dNear < computedZNear) + { + dNear = computedZNear; + for (const auto& instanceMatrix : mInstanceMatrices) + { + osg::Matrix fullMatrix = instanceMatrix * matrix; + osg::Vec3 instanceLookVector(-fullMatrix(0, 2), -fullMatrix(1, 2), -fullMatrix(2, 2)); + unsigned int instanceBbCornerFar = (instanceLookVector.x() >= 0 ? 1 : 0) + | (instanceLookVector.y() >= 0 ? 2 : 0) | (instanceLookVector.z() >= 0 ? 4 : 0); + unsigned int instanceBbCornerNear = (~instanceBbCornerFar) & 7; + value_type instanceDNear + = distance(mInstanceBounds.corner(instanceBbCornerNear), fullMatrix); + value_type instanceDFar + = distance(mInstanceBounds.corner(instanceBbCornerFar), fullMatrix); + + if (instanceDNear > instanceDFar) + std::swap(instanceDNear, instanceDFar); + + if (instanceDFar < 0 || instanceDNear > dNear) + continue; + + frustum.setAndTransformProvidingInverse( + cullVisitor.getProjectionCullingStack().back().getFrustum(), fullMatrix); + osg::Polytope::PlaneList planes; + osg::Polytope::ClippingMask selectorMask = 0x1; + for (const auto& plane : frustum.getPlaneList()) + { + if (resultMask & selectorMask) + planes.push_back(plane); + selectorMask <<= 1; + } + + value_type newNear + = cullVisitor.computeNearestPointInFrustum(fullMatrix, planes, *drawable); + dNear = std::min(dNear, newNear); + } + if (dNear < computedZNear) + cullVisitor.setCalculatedNearPlane(dNear); + } + + if (cnfMode == osg::CullSettings::COMPUTE_NEAR_FAR_USING_PRIMITIVES && dFar > computedZFar) + { + dFar = computedZFar; + for (const auto& instanceMatrix : mInstanceMatrices) + { + osg::Matrix fullMatrix = instanceMatrix * matrix; + osg::Vec3 instanceLookVector(-fullMatrix(0, 2), -fullMatrix(1, 2), -fullMatrix(2, 2)); + unsigned int instanceBbCornerFar = (instanceLookVector.x() >= 0 ? 1 : 0) + | (instanceLookVector.y() >= 0 ? 2 : 0) | (instanceLookVector.z() >= 0 ? 4 : 0); + unsigned int instanceBbCornerNear = (~instanceBbCornerFar) & 7; + value_type instanceDNear + = distance(mInstanceBounds.corner(instanceBbCornerNear), fullMatrix); + value_type instanceDFar + = distance(mInstanceBounds.corner(instanceBbCornerFar), fullMatrix); + + if (instanceDNear > instanceDFar) + std::swap(instanceDNear, instanceDFar); + + if (instanceDFar < 0 || instanceDFar < dFar) + continue; + + frustum.setAndTransformProvidingInverse( + cullVisitor.getProjectionCullingStack().back().getFrustum(), fullMatrix); + osg::Polytope::PlaneList planes; + osg::Polytope::ClippingMask selectorMask = 0x1; + for (const auto& plane : frustum.getPlaneList()) + { + if (resultMask & selectorMask) + planes.push_back(plane); + selectorMask <<= 1; + } + + value_type newFar = cullVisitor.computeFurthestPointInFrustum( + instanceMatrix * matrix, planes, *drawable); + dFar = std::max(dFar, newFar); + } + if (dFar > computedZFar) + cullVisitor.setCalculatedFarPlane(dFar); + } + } + } + + return false; } - traverse(node); - } + private: + std::vector mInstanceMatrices; + osg::BoundingBox mInstanceBounds; + }; - void apply(osg::Geometry& geom) override + class InstancingVisitor : public osg::NodeVisitor { - for (unsigned int i = 0; i < geom.getNumPrimitiveSets(); ++i) + public: + InstancingVisitor(std::vector& instances, osg::Vec3f& chunkPosition) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mInstances(instances) + , mChunkPosition(chunkPosition) { - geom.getPrimitiveSet(i)->setNumInstances(mInstances.size()); } - osg::ref_ptr transforms = new osg::Vec4Array(mInstances.size()); - osg::BoundingBox box; - float radius = geom.getBoundingBox().radius(); - for (unsigned int i = 0; i < transforms->getNumElements(); i++) + void apply(osg::Group& group) override { - osg::Vec3f pos(mInstances[i].mPos.asVec3()); - osg::Vec3f relativePos = pos - mChunkPosition; - (*transforms)[i] = osg::Vec4f(relativePos, mInstances[i].mScale); - - // Use an additional margin due to groundcover animation - float instanceRadius = radius * mInstances[i].mScale * 1.1f; - osg::BoundingSphere instanceBounds(relativePos, instanceRadius); - box.expandBy(instanceBounds); + for (unsigned int i = 0; i < group.getNumChildren();) + { + if (group.getChild(i)->asDrawable() && !group.getChild(i)->asGeometry()) + group.removeChild(i); + else + ++i; + } + traverse(group); } - geom.setInitialBound(box); - - osg::ref_ptr rotations = new osg::Vec3Array(mInstances.size()); - for (unsigned int i = 0; i < rotations->getNumElements(); i++) + void apply(osg::Geometry& geom) override { - (*rotations)[i] = mInstances[i].mPos.asRotationVec3(); - } + for (unsigned int i = 0; i < geom.getNumPrimitiveSets(); ++i) + { + geom.getPrimitiveSet(i)->setNumInstances(mInstances.size()); + } - // Display lists do not support instancing in OSG 3.4 - geom.setUseDisplayList(false); + osg::ref_ptr transforms = new osg::Vec4Array(mInstances.size()); + osg::BoundingBox box; + osg::BoundingBox originalBox = geom.getBoundingBox(); + float radius = originalBox.radius(); + for (unsigned int i = 0; i < transforms->getNumElements(); i++) + { + osg::Vec3f pos(mInstances[i].mPos.asVec3()); + osg::Vec3f relativePos = pos - mChunkPosition; + (*transforms)[i] = osg::Vec4f(relativePos, mInstances[i].mScale); + + // Use an additional margin due to groundcover animation + float instanceRadius = radius * mInstances[i].mScale * 1.1f; + osg::BoundingSphere instanceBounds(relativePos, instanceRadius); + box.expandBy(instanceBounds); + } - geom.setVertexAttribArray(6, transforms.get(), osg::Array::BIND_PER_VERTEX); - geom.setVertexAttribArray(7, rotations.get(), osg::Array::BIND_PER_VERTEX); + geom.setInitialBound(box); - osg::ref_ptr ss = geom.getOrCreateStateSet(); - ss->setAttribute(new osg::VertexAttribDivisor(6, 1)); - ss->setAttribute(new osg::VertexAttribDivisor(7, 1)); + osg::ref_ptr rotations = new osg::Vec3Array(mInstances.size()); + for (unsigned int i = 0; i < rotations->getNumElements(); i++) + { + (*rotations)[i] = mInstances[i].mPos.asRotationVec3(); + } - ss->removeAttribute(osg::StateAttribute::MATERIAL); - removeAlpha(ss); + // Display lists do not support instancing in OSG 3.4 + geom.setUseDisplayList(false); + geom.setUseVertexBufferObjects(true); - traverse(geom); - } - private: - std::vector mInstances; - osg::Vec3f mChunkPosition; + geom.setVertexAttribArray(6, transforms.get(), osg::Array::BIND_PER_VERTEX); + geom.setVertexAttribArray(7, rotations.get(), osg::Array::BIND_PER_VERTEX); - void removeAlpha(osg::StateSet* stateset) - { - // MGE uses default alpha settings for groundcover, so we can not rely on alpha properties - stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC); - stateset->removeMode(GL_ALPHA_TEST); - stateset->removeAttribute(osg::StateAttribute::BLENDFUNC); - stateset->removeMode(GL_BLEND); - stateset->setRenderBinToInherit(); - } - }; + geom.addCullCallback(new InstancedComputeNearFarCullCallback(mInstances, mChunkPosition, originalBox)); + } - class DensityCalculator - { - public: - DensityCalculator(float density) - : mDensity(density) - { - } + private: + std::vector mInstances; + osg::Vec3f mChunkPosition; + }; - bool isInstanceEnabled() + class DensityCalculator { - if (mDensity >= 1.f) return true; + public: + DensityCalculator(float density) + : mDensity(density) + { + } - mCurrentGroundcover += mDensity; - if (mCurrentGroundcover < 1.f) return false; + bool isInstanceEnabled() + { + if (mDensity >= 1.f) + return true; - mCurrentGroundcover -= 1.f; + mCurrentGroundcover += mDensity; + if (mCurrentGroundcover < 1.f) + return false; - return true; - } - void reset() { mCurrentGroundcover = 0.f; } + mCurrentGroundcover -= 1.f; - private: - float mCurrentGroundcover = 0.f; - float mDensity = 0.f; - }; + return true; + } + void reset() { mCurrentGroundcover = 0.f; } - inline bool isInChunkBorders(ESM::CellRef& ref, osg::Vec2f& minBound, osg::Vec2f& maxBound) - { - osg::Vec2f size = maxBound - minBound; - if (size.x() >=1 && size.y() >=1) return true; + private: + float mCurrentGroundcover = 0.f; + float mDensity = 0.f; + }; + + class ViewDistanceCallback : public SceneUtil::NodeCallback + { + public: + ViewDistanceCallback(float dist, const osg::BoundingBox& box) + : mViewDistance(dist) + , mBox(box) + { + } + void operator()(osg::Node* node, osg::NodeVisitor* nv) + { + if (Terrain::distance(mBox, nv->getEyePoint()) <= mViewDistance) + traverse(node, nv); + } - osg::Vec3f pos = ref.mPos.asVec3(); - osg::Vec3f cellPos = pos / ESM::Land::REAL_SIZE; - if ((minBound.x() > std::floor(minBound.x()) && cellPos.x() < minBound.x()) || (minBound.y() > std::floor(minBound.y()) && cellPos.y() < minBound.y()) - || (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) || (minBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) - return false; + private: + float mViewDistance; + osg::BoundingBox mBox; + }; + + inline bool isInChunkBorders(ESM::CellRef& ref, osg::Vec2f& minBound, osg::Vec2f& maxBound) + { + osg::Vec2f size = maxBound - minBound; + if (size.x() >= 1 && size.y() >= 1) + return true; + + osg::Vec3f pos = ref.mPos.asVec3(); + osg::Vec3f cellPos = pos / ESM::Land::REAL_SIZE; + if ((minBound.x() > std::floor(minBound.x()) && cellPos.x() < minBound.x()) + || (minBound.y() > std::floor(minBound.y()) && cellPos.y() < minBound.y()) + || (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) + || (maxBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) + return false; - return true; + return true; + } } - osg::ref_ptr Groundcover::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) + osg::ref_ptr Groundcover::getChunk(float size, const osg::Vec2f& center, unsigned char lod, + unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) { - ChunkId id = std::make_tuple(center, size, activeGrid); - + if (lod > getMaxLodLevel()) + return nullptr; + GroundcoverChunkId id = std::make_tuple(center, size); osg::ref_ptr obj = mCache->getRefFromObjectCache(id); if (obj) - return obj->asNode(); + return static_cast(obj.get()); else { InstanceMap instances; @@ -195,55 +340,84 @@ namespace MWRender } } - Groundcover::Groundcover(Resource::SceneManager* sceneManager, float density) - : GenericResourceManager(nullptr) - , mSceneManager(sceneManager) - , mDensity(density) + Groundcover::Groundcover( + Resource::SceneManager* sceneManager, float density, float viewDistance, const MWWorld::GroundcoverStore& store) + : GenericResourceManager(nullptr, Settings::cells().mCacheExpiryDelay) + , mSceneManager(sceneManager) + , mDensity(density) + , mStateset(new osg::StateSet) + , mGroundcoverStore(store) { + setViewDistance(viewDistance); + // MGE uses default alpha settings for groundcover, so we can not rely on alpha properties + // Force a unified alpha handling instead of data from meshes + osg::ref_ptr alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f / 255.f); + mStateset->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + mStateset->setAttributeAndModes(new osg::BlendFunc, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); + mStateset->setRenderBinDetails(0, "RenderBin", osg::StateSet::OVERRIDE_RENDERBIN_DETAILS); + mStateset->setAttribute(new osg::VertexAttribDivisor(6, 1)); + mStateset->setAttribute(new osg::VertexAttribDivisor(7, 1)); + + mProgramTemplate = mSceneManager->getShaderManager().getProgramTemplate() + ? Shader::ShaderManager::cloneProgram(mSceneManager->getShaderManager().getProgramTemplate()) + : osg::ref_ptr(new osg::Program); + mProgramTemplate->addBindAttribLocation("aOffset", 6); + mProgramTemplate->addBindAttribLocation("aRotation", 7); } + Groundcover::~Groundcover() {} + void Groundcover::collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center) { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - osg::Vec2f minBound = (center - osg::Vec2f(size/2.f, size/2.f)); - osg::Vec2f maxBound = (center + osg::Vec2f(size/2.f, size/2.f)); + if (mDensity <= 0.f) + return; + + osg::Vec2f minBound = (center - osg::Vec2f(size / 2.f, size / 2.f)); + osg::Vec2f maxBound = (center + osg::Vec2f(size / 2.f, size / 2.f)); DensityCalculator calculator(mDensity); - std::vector esm; - osg::Vec2i startCell = osg::Vec2i(std::floor(center.x() - size/2.f), std::floor(center.y() - size/2.f)); + ESM::ReadersCache readers; + osg::Vec2i startCell = osg::Vec2i(std::floor(center.x() - size / 2.f), std::floor(center.y() - size / 2.f)); for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX) { for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY) { - const ESM::Cell* cell = store.get().searchStatic(cellX, cellY); - if (!cell) continue; + ESM::Cell cell; + mGroundcoverStore.initCell(cell, cellX, cellY); + if (cell.mContextList.empty()) + continue; calculator.reset(); - for (size_t i=0; imContextList.size(); ++i) + std::map refs; + for (size_t i = 0; i < cell.mContextList.size(); ++i) { - unsigned int index = cell->mContextList.at(i).index; - if (esm.size() <= index) - esm.resize(index+1); - cell->restore(esm[index], i); + const std::size_t index = static_cast(cell.mContextList[i].index); + const ESM::ReadersCache::BusyItem reader = readers.get(index); + cell.restore(*reader, i); ESM::CellRef ref; - ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; bool deleted = false; - while(cell->getNextRef(esm[index], ref, deleted)) + while (cell.getNextRef(*reader, ref, deleted)) { - if (deleted) continue; - if (!ref.mRefNum.fromGroundcoverFile()) continue; - - if (!calculator.isInstanceEnabled()) continue; - if (!isInChunkBorders(ref, minBound, maxBound)) continue; - - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); - int type = store.findStatic(ref.mRefID); - std::string model = getGroundcoverModel(type, ref.mRefID, store); - if (model.empty()) continue; - model = "meshes/" + model; - - instances[model].emplace_back(std::move(ref), std::move(model)); + if (!deleted && refs.find(ref.mRefNum) == refs.end() && !calculator.isInstanceEnabled()) + deleted = true; + if (!deleted && !isInChunkBorders(ref, minBound, maxBound)) + deleted = true; + + if (deleted) + { + refs.erase(ref.mRefNum); + continue; + } + refs[ref.mRefNum] = std::move(ref); } } + + for (auto& pair : refs) + { + ESM::CellRef& ref = pair.second; + const std::string& model = mGroundcoverStore.getGroundcoverModel(ref.mRefID); + if (!model.empty()) + instances[model].emplace_back(std::move(ref)); + } } } } @@ -251,31 +425,34 @@ namespace MWRender osg::ref_ptr Groundcover::createChunk(InstanceMap& instances, const osg::Vec2f& center) { osg::ref_ptr group = new osg::Group; - osg::Vec3f worldCenter = osg::Vec3f(center.x(), center.y(), 0)*ESM::Land::REAL_SIZE; + osg::Vec3f worldCenter = osg::Vec3f(center.x(), center.y(), 0) * ESM::Land::REAL_SIZE; for (auto& pair : instances) { - const osg::Node* temp = mSceneManager->getTemplate(pair.first); - osg::ref_ptr node = static_cast(temp->clone(osg::CopyOp::DEEP_COPY_ALL&(~osg::CopyOp::DEEP_COPY_TEXTURES))); + const osg::Node* temp = mSceneManager->getTemplate(VFS::Path::toNormalized(pair.first)); + osg::ref_ptr node = static_cast(temp->clone(osg::CopyOp::DEEP_COPY_NODES + | osg::CopyOp::DEEP_COPY_DRAWABLES | osg::CopyOp::DEEP_COPY_USERDATA | osg::CopyOp::DEEP_COPY_ARRAYS + | osg::CopyOp::DEEP_COPY_PRIMITIVES)); // Keep link to original mesh to keep it in cache group->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(temp)); - mSceneManager->reinstateRemovedState(node); - InstancingVisitor visitor(pair.second, worldCenter); node->accept(visitor); group->addChild(node); } - // Force a unified alpha handling instead of data from meshes - osg::ref_ptr alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f / 255.f); - group->getOrCreateStateSet()->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON); - group->getBound(); + osg::ComputeBoundsVisitor cbv; + group->accept(cbv); + osg::BoundingBox box = cbv.getBoundingBox(); + group->addCullCallback(new ViewDistanceCallback(getViewDistance(), box)); + + group->setStateSet(mStateset); group->setNodeMask(Mask_Groundcover); if (mSceneManager->getLightingMethod() != SceneUtil::LightingMethod::FFP) - group->setCullCallback(new SceneUtil::LightListCallback); - mSceneManager->recreateShaders(group, "groundcover", false, true); - + group->addCullCallback(new SceneUtil::LightListCallback); + mSceneManager->recreateShaders(group, "groundcover", true, mProgramTemplate); + mSceneManager->shareState(group); + group->getBound(); return group; } @@ -284,8 +461,8 @@ namespace MWRender return Mask_Groundcover; } - void Groundcover::reportStats(unsigned int frameNumber, osg::Stats *stats) const + void Groundcover::reportStats(unsigned int frameNumber, osg::Stats* stats) const { - stats->setAttribute(frameNumber, "Groundcover Chunk", mCache->getCacheSize()); + Resource::reportStats("Groundcover Chunk", frameNumber, mCache->getStats(), *stats); } } diff --git a/apps/openmw/mwrender/groundcover.hpp b/apps/openmw/mwrender/groundcover.hpp index cd80978befc..df40d9d529d 100644 --- a/apps/openmw/mwrender/groundcover.hpp +++ b/apps/openmw/mwrender/groundcover.hpp @@ -1,42 +1,33 @@ #ifndef OPENMW_MWRENDER_GROUNDCOVER_H #define OPENMW_MWRENDER_GROUNDCOVER_H -#include +#include #include -#include -#include +#include -namespace MWRender +namespace MWWorld { - class GroundcoverUpdater : public SceneUtil::StateSetUpdater - { - public: - GroundcoverUpdater() - : mWindSpeed(0.f) - , mPlayerPos(osg::Vec3f()) - { - } - - void setWindSpeed(float windSpeed); - void setPlayerPos(osg::Vec3f playerPos); - - protected: - void setDefaults(osg::StateSet *stateset) override; - void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override; - - private: - float mWindSpeed; - osg::Vec3f mPlayerPos; - }; + class ESMStore; + class GroundcoverStore; +} +namespace osg +{ + class Program; +} - typedef std::tuple ChunkId; // Center, Size, ActiveGrid - class Groundcover : public Resource::GenericResourceManager, public Terrain::QuadTreeWorld::ChunkManager +namespace MWRender +{ + typedef std::tuple GroundcoverChunkId; // Center, Size + class Groundcover : public Resource::GenericResourceManager, + public Terrain::QuadTreeWorld::ChunkManager { public: - Groundcover(Resource::SceneManager* sceneManager, float density); - ~Groundcover() = default; + Groundcover(Resource::SceneManager* sceneManager, float density, float viewDistance, + const MWWorld::GroundcoverStore& store); + ~Groundcover(); - osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override; + osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, + bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override; unsigned int getNodeMask() override; @@ -46,19 +37,20 @@ namespace MWRender { ESM::Position mPos; float mScale; - std::string mModel; - GroundcoverEntry(const ESM::CellRef& ref, const std::string& model) + GroundcoverEntry(const ESM::CellRef& ref) + : mPos(ref.mPos) + , mScale(ref.mScale) { - mPos = ref.mPos; - mScale = ref.mScale; - mModel = model; } }; private: Resource::SceneManager* mSceneManager; float mDensity; + osg::ref_ptr mStateset; + osg::ref_ptr mProgramTemplate; + const MWWorld::GroundcoverStore& mGroundcoverStore; typedef std::map> InstanceMap; osg::ref_ptr createChunk(InstanceMap& instances, const osg::Vec2f& center); diff --git a/apps/openmw/mwrender/landmanager.cpp b/apps/openmw/mwrender/landmanager.cpp index 560c1ba7200..d17933b2b7f 100644 --- a/apps/openmw/mwrender/landmanager.cpp +++ b/apps/openmw/mwrender/landmanager.cpp @@ -1,8 +1,8 @@ #include "landmanager.hpp" -#include - +#include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -11,33 +11,47 @@ namespace MWRender { -LandManager::LandManager(int loadFlags) - : GenericResourceManager >(nullptr) - , mLoadFlags(loadFlags) -{ - mCache = new CacheType; -} + LandManager::LandManager(int loadFlags) + : GenericResourceManager(nullptr, Settings::cells().mCacheExpiryDelay) + , mLoadFlags(loadFlags) + { + } -osg::ref_ptr LandManager::getLand(int x, int y) -{ - osg::ref_ptr obj = mCache->getRefFromObjectCache(std::make_pair(x,y)); - if (obj) - return static_cast(obj.get()); - else + osg::ref_ptr LandManager::getLand(ESM::ExteriorCellLocation cellIndex) { - const ESM::Land* land = MWBase::Environment::get().getWorld()->getStore().get().search(x,y); - if (!land) - return nullptr; - osg::ref_ptr landObj (new ESMTerrain::LandObject(land, mLoadFlags)); - mCache->addEntryToObjectCache(std::make_pair(x,y), landObj.get()); + const MWBase::World& world = *MWBase::Environment::get().getWorld(); + if (ESM::isEsm4Ext(cellIndex.mWorldspace)) + { + const ESM4::World* worldspace = world.getStore().get().find(cellIndex.mWorldspace); + if (!worldspace->mParent.isZeroOrUnset() && worldspace->mParentUseFlags & ESM4::World::UseFlag_Land) + cellIndex.mWorldspace = worldspace->mParent; + } + + if (const std::optional> obj = mCache->getRefFromObjectCacheOrNone(cellIndex)) + return static_cast(obj->get()); + + osg::ref_ptr landObj = nullptr; + + if (ESM::isEsm4Ext(cellIndex.mWorldspace)) + { + const ESM4::Land* land = world.getStore().get().search(cellIndex); + if (land != nullptr) + landObj = new ESMTerrain::LandObject(*land, mLoadFlags); + } + else + { + const ESM::Land* land = world.getStore().get().search(cellIndex.mX, cellIndex.mY); + if (land != nullptr) + landObj = new ESMTerrain::LandObject(*land, mLoadFlags); + } + + mCache->addEntryToObjectCache(cellIndex, landObj.get()); return landObj; } -} - -void LandManager::reportStats(unsigned int frameNumber, osg::Stats *stats) const -{ - stats->setAttribute(frameNumber, "Land", mCache->getCacheSize()); -} + void LandManager::reportStats(unsigned int frameNumber, osg::Stats* stats) const + { + Resource::reportStats("Land", frameNumber, mCache->getStats(), *stats); + } } diff --git a/apps/openmw/mwrender/landmanager.hpp b/apps/openmw/mwrender/landmanager.hpp index f3cc8608549..1b82f32ce96 100644 --- a/apps/openmw/mwrender/landmanager.hpp +++ b/apps/openmw/mwrender/landmanager.hpp @@ -3,8 +3,9 @@ #include -#include +#include #include +#include namespace ESM { @@ -14,13 +15,13 @@ namespace ESM namespace MWRender { - class LandManager : public Resource::GenericResourceManager > + class LandManager : public Resource::GenericResourceManager { public: LandManager(int loadFlags); /// @note Will return nullptr if not found. - osg::ref_ptr getLand(int x, int y); + osg::ref_ptr getLand(ESM::ExteriorCellLocation cellIndex); void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 686078879d1..9e934d6f207 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -1,72 +1,43 @@ #include "localmap.hpp" -#include +#include +#include #include #include -#include -#include #include #include +#include #include #include -#include -#include +#include +#include +#include #include -#include -#include -#include -#include +#include #include -#include -#include +#include +#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" +#include "util.hpp" #include "vismask.hpp" namespace { - - class CameraLocalUpdateCallback : public osg::NodeCallback - { - public: - CameraLocalUpdateCallback(MWRender::LocalMap* parent) - : mRendered(false) - , mParent(parent) - { - } - - void operator()(osg::Node* node, osg::NodeVisitor*) override - { - if (mRendered) - node->setNodeMask(0); - - if (!mRendered) - { - mRendered = true; - mParent->markForRemoval(static_cast(node)); - } - - // Note, we intentionally do not traverse children here. The map camera's scene data is the same as the master camera's, - // so it has been updated already. - //traverse(node, nv); - } - - private: - bool mRendered; - MWRender::LocalMap* mParent; - }; - float square(float val) { - return val*val; + return val * val; } std::pair divideIntoSegments(const osg::BoundingBox& bounds, float mapSize) @@ -76,698 +47,741 @@ namespace osg::Vec2f length = max - min; const int segsX = static_cast(std::ceil(length.x() / mapSize)); const int segsY = static_cast(std::ceil(length.y() / mapSize)); - return {segsX, segsY}; + return { segsX, segsY }; } } namespace MWRender { + class LocalMapRenderToTexture : public SceneUtil::RTTNode + { + public: + LocalMapRenderToTexture(osg::Node* sceneRoot, int res, int mapWorldSize, float x, float y, + const osg::Vec3d& upVector, float zmin, float zmax); -LocalMap::LocalMap(osg::Group* root) - : mRoot(root) - , mMapResolution(Settings::Manager::getInt("local map resolution", "Map")) - , mMapWorldSize(Constants::CellSizeInUnits) - , mCellDistance(Constants::CellGridRadius) - , mAngle(0.f) - , mInterior(false) -{ - // Increase map resolution, if use UI scaling - float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); - mMapResolution *= uiScale; - - SceneUtil::FindByNameVisitor find("Scene Root"); - mRoot->accept(find); - mSceneRoot = find.mFoundNode; - if (!mSceneRoot) - throw std::runtime_error("no scene root found"); -} + void setDefaults(osg::Camera* camera) override; -LocalMap::~LocalMap() -{ - for (auto& camera : mActiveCameras) - removeCamera(camera); - for (auto& camera : mCamerasPendingRemoval) - removeCamera(camera); -} + osg::Node* mSceneRoot; + osg::Matrix mProjectionMatrix; + osg::Matrix mViewMatrix; + bool mActive; + }; -const osg::Vec2f LocalMap::rotatePoint(const osg::Vec2f& point, const osg::Vec2f& center, const float angle) -{ - return osg::Vec2f( std::cos(angle) * (point.x() - center.x()) - std::sin(angle) * (point.y() - center.y()) + center.x(), - std::sin(angle) * (point.x() - center.x()) + std::cos(angle) * (point.y() - center.y()) + center.y()); -} + class CameraLocalUpdateCallback + : public SceneUtil::NodeCallback + { + public: + void operator()(LocalMapRenderToTexture* node, osg::NodeVisitor* nv); + }; -void LocalMap::clear() -{ - mSegments.clear(); -} + LocalMap::LocalMap(osg::Group* root) + : mRoot(root) + , mMapResolution( + Settings::map().mLocalMapResolution * MWBase::Environment::get().getWindowManager()->getScalingFactor()) + , mMapWorldSize(Constants::CellSizeInUnits) + , mCellDistance(Constants::CellGridRadius) + , mAngle(0.f) + , mInterior(false) + { + SceneUtil::FindByNameVisitor find("Scene Root"); + mRoot->accept(find); + mSceneRoot = find.mFoundNode; + if (!mSceneRoot) + throw std::runtime_error("no scene root found"); + } -void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) -{ - if (!mInterior) + LocalMap::~LocalMap() + { + for (auto& rtt : mLocalMapRTTs) + mRoot->removeChild(rtt); + } + + const osg::Vec2f LocalMap::rotatePoint(const osg::Vec2f& point, const osg::Vec2f& center, const float angle) { - const MapSegment& segment = mSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; + return osg::Vec2f( + std::cos(angle) * (point.x() - center.x()) - std::sin(angle) * (point.y() - center.y()) + center.x(), + std::sin(angle) * (point.x() - center.x()) + std::cos(angle) * (point.y() - center.y()) + center.y()); + } + + void LocalMap::clear() + { + mExteriorSegments.clear(); + mInteriorSegments.clear(); + } - if (segment.mFogOfWarImage && segment.mHasFogState) + void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) + { + if (!mInterior) { - std::unique_ptr fog (new ESM::FogState()); - fog->mFogTextures.emplace_back(); + const MapSegment& segment + = mExteriorSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; + + if (segment.mFogOfWarImage && segment.mHasFogState) + { + auto fog = std::make_unique(); + fog->mFogTextures.emplace_back(); - segment.saveFogOfWar(fog->mFogTextures.back()); + segment.saveFogOfWar(fog->mFogTextures.back()); - cell->setFog(fog.release()); + cell->setFog(std::move(fog)); + } } - } - else - { - auto segments = divideIntoSegments(mBounds, mMapWorldSize); + else + { + auto segments = divideIntoSegments(mBounds, mMapWorldSize); - std::unique_ptr fog (new ESM::FogState()); + auto fog = std::make_unique(); - fog->mBounds.mMinX = mBounds.xMin(); - fog->mBounds.mMaxX = mBounds.xMax(); - fog->mBounds.mMinY = mBounds.yMin(); - fog->mBounds.mMaxY = mBounds.yMax(); - fog->mNorthMarkerAngle = mAngle; + fog->mBounds.mMinX = mBounds.xMin(); + fog->mBounds.mMaxX = mBounds.xMax(); + fog->mBounds.mMinY = mBounds.yMin(); + fog->mBounds.mMaxY = mBounds.yMax(); + fog->mNorthMarkerAngle = mAngle; - fog->mFogTextures.reserve(segments.first * segments.second); + fog->mFogTextures.reserve(segments.first * segments.second); - for (int x = 0; x < segments.first; ++x) - { - for (int y = 0; y < segments.second; ++y) + for (int x = 0; x < segments.first; ++x) { - const MapSegment& segment = mSegments[std::make_pair(x,y)]; + for (int y = 0; y < segments.second; ++y) + { + const MapSegment& segment = mInteriorSegments[std::make_pair(x, y)]; - fog->mFogTextures.emplace_back(); + fog->mFogTextures.emplace_back(); - // saving even if !segment.mHasFogState so we don't mess up the segmenting - // plus, older openmw versions can't deal with empty images - segment.saveFogOfWar(fog->mFogTextures.back()); + // saving even if !segment.mHasFogState so we don't mess up the segmenting + // plus, older openmw versions can't deal with empty images + segment.saveFogOfWar(fog->mFogTextures.back()); - fog->mFogTextures.back().mX = x; - fog->mFogTextures.back().mY = y; + fog->mFogTextures.back().mX = x; + fog->mFogTextures.back().mY = y; + } } - } - cell->setFog(fog.release()); + cell->setFog(std::move(fog)); + } } -} -osg::ref_ptr LocalMap::createOrthographicCamera(float x, float y, float width, float height, const osg::Vec3d& upVector, float zmin, float zmax) -{ - osg::ref_ptr camera (new osg::Camera); - camera->setProjectionMatrixAsOrtho(-width/2, width/2, -height/2, height/2, 5, (zmax-zmin) + 10); - camera->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); - camera->setViewMatrixAsLookAt(osg::Vec3d(x, y, zmax + 5), osg::Vec3d(x, y, zmin), upVector); - camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF_INHERIT_VIEWPOINT); - camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); - camera->setClearColor(osg::Vec4(0.f, 0.f, 0.f, 1.f)); - camera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - camera->setRenderOrder(osg::Camera::PRE_RENDER); - - camera->setCullMask(Mask_Scene | Mask_SimpleWater | Mask_Terrain | Mask_Object | Mask_Static); - camera->setNodeMask(Mask_RenderToTexture); - - // Disable small feature culling, it's not going to be reliable for this camera - osg::Camera::CullingMode cullingMode = (osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING) & ~(osg::CullStack::SMALL_FEATURE_CULLING); - camera->setCullingMode(cullingMode); - - osg::ref_ptr stateset = new osg::StateSet; - stateset->setAttribute(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::FILL), osg::StateAttribute::OVERRIDE); - - // assign large value to effectively turn off fog - // shaders don't respect glDisable(GL_FOG) - osg::ref_ptr fog (new osg::Fog); - fog->setStart(10000000); - fog->setEnd(10000000); - stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); - - osg::ref_ptr lightmodel = new osg::LightModel; - lightmodel->setAmbientIntensity(osg::Vec4(0.3f, 0.3f, 0.3f, 1.f)); - stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - osg::ref_ptr light = new osg::Light; - light->setPosition(osg::Vec4(-0.3f, -0.3f, 0.7f, 0.f)); - light->setDiffuse(osg::Vec4(0.7f, 0.7f, 0.7f, 1.f)); - light->setAmbient(osg::Vec4(0,0,0,1)); - light->setSpecular(osg::Vec4(0,0,0,0)); - light->setLightNum(0); - light->setConstantAttenuation(1.f); - light->setLinearAttenuation(0.f); - light->setQuadraticAttenuation(0.f); - - osg::ref_ptr lightSource = new osg::LightSource; - lightSource->setLight(light); - - lightSource->setStateSetModes(*stateset, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); - - // override sun for local map - SceneUtil::configureStateSetSunOverride(static_cast(mSceneRoot.get()), light, stateset); - - camera->addChild(lightSource); - camera->setStateSet(stateset); - camera->setViewport(0, 0, mMapResolution, mMapResolution); - camera->setUpdateCallback(new CameraLocalUpdateCallback(this)); - - return camera; -} + void LocalMap::setupRenderToTexture( + int segment_x, int segment_y, float left, float top, const osg::Vec3d& upVector, float zmin, float zmax) + { + mLocalMapRTTs.emplace_back( + new LocalMapRenderToTexture(mSceneRoot, mMapResolution, mMapWorldSize, left, top, upVector, zmin, zmax)); -void LocalMap::setupRenderToTexture(osg::ref_ptr camera, int x, int y) -{ - osg::ref_ptr texture (new osg::Texture2D); - texture->setTextureSize(mMapResolution, mMapResolution); - 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); - - SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(camera, osg::Camera::COLOR_BUFFER, texture); - - camera->addChild(mSceneRoot); - mRoot->addChild(camera); - mActiveCameras.push_back(camera); - - MapSegment& segment = mSegments[std::make_pair(x, y)]; - segment.mMapTexture = texture; -} + mRoot->addChild(mLocalMapRTTs.back()); -bool needUpdate(std::set >& renderedGrid, std::set >& currentGrid, int cellX, int cellY) -{ - // if all the cells of the current grid are contained in the rendered grid then we can keep the old render - for (int dx=-1;dx<2;dx+=1) + MapSegment& segment = mInterior ? mInteriorSegments[std::make_pair(segment_x, segment_y)] + : mExteriorSegments[std::make_pair(segment_x, segment_y)]; + segment.mMapTexture = static_cast(mLocalMapRTTs.back()->getColorTexture(nullptr)); + } + + void LocalMap::requestMap(const MWWorld::CellStore* cell) { - for (int dy=-1;dy<2;dy+=1) + if (!cell->isExterior()) { - bool haveInRenderedGrid = renderedGrid.find(std::make_pair(cellX+dx,cellY+dy)) != renderedGrid.end(); - bool haveInCurrentGrid = currentGrid.find(std::make_pair(cellX+dx,cellY+dy)) != currentGrid.end(); - if (haveInCurrentGrid && !haveInRenderedGrid) - return true; + requestInteriorMap(cell); + return; } - } - return false; -} -void LocalMap::requestMap(const MWWorld::CellStore* cell) -{ - if (cell->isExterior()) - { int cellX = cell->getCell()->getGridX(); int cellY = cell->getCell()->getGridY(); - MapSegment& segment = mSegments[std::make_pair(cellX, cellY)]; - if (!needUpdate(segment.mGrid, mCurrentGrid, cellX, cellY)) + MapSegment& segment = mExteriorSegments[std::make_pair(cellX, cellY)]; + const std::uint8_t neighbourFlags = getExteriorNeighbourFlags(cellX, cellY); + if ((segment.mLastRenderNeighbourFlags & neighbourFlags) == neighbourFlags) return; - else - { - segment.mGrid = mCurrentGrid; - requestExteriorMap(cell); - } + requestExteriorMap(cell, segment); + segment.mLastRenderNeighbourFlags = neighbourFlags; } - else - requestInteriorMap(cell); -} -void LocalMap::addCell(MWWorld::CellStore *cell) -{ - if (cell->isExterior()) - mCurrentGrid.emplace(cell->getCell()->getGridX(), cell->getCell()->getGridY()); -} - -void LocalMap::removeCell(MWWorld::CellStore *cell) -{ - saveFogOfWar(cell); - - if (cell->isExterior()) + void LocalMap::addCell(MWWorld::CellStore* cell) { - std::pair coords = std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY()); - mSegments.erase(coords); - mCurrentGrid.erase(coords); + if (cell->isExterior()) + mExteriorSegments.emplace( + std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY()), MapSegment{}); } - else - mSegments.clear(); -} -osg::ref_ptr LocalMap::getMapTexture(int x, int y) -{ - SegmentMap::iterator found = mSegments.find(std::make_pair(x, y)); - if (found == mSegments.end()) - return osg::ref_ptr(); - else - return found->second.mMapTexture; -} + void LocalMap::removeExteriorCell(int x, int y) + { + mExteriorSegments.erase({ x, y }); + } -osg::ref_ptr LocalMap::getFogOfWarTexture(int x, int y) -{ - SegmentMap::iterator found = mSegments.find(std::make_pair(x, y)); - if (found == mSegments.end()) - return osg::ref_ptr(); - else - return found->second.mFogOfWarTexture; -} + void LocalMap::removeCell(MWWorld::CellStore* cell) + { + saveFogOfWar(cell); -void LocalMap::removeCamera(osg::Camera *cam) -{ - cam->removeChildren(0, cam->getNumChildren()); - mRoot->removeChild(cam); -} + if (cell->isExterior()) + mExteriorSegments.erase({ cell->getCell()->getGridX(), cell->getCell()->getGridY() }); + else + mInteriorSegments.clear(); + } -void LocalMap::markForRemoval(osg::Camera *cam) -{ - CameraVector::iterator found = std::find(mActiveCameras.begin(), mActiveCameras.end(), cam); - if (found == mActiveCameras.end()) + osg::ref_ptr LocalMap::getMapTexture(int x, int y) { - Log(Debug::Error) << "Error: trying to remove an inactive camera"; - return; + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + SegmentMap::iterator found = segments.find(std::make_pair(x, y)); + if (found == segments.end()) + return osg::ref_ptr(); + else + return found->second.mMapTexture; } - mActiveCameras.erase(found); - mCamerasPendingRemoval.push_back(cam); -} -void LocalMap::cleanupCameras() -{ - if (mCamerasPendingRemoval.empty()) - return; + osg::ref_ptr LocalMap::getFogOfWarTexture(int x, int y) + { + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + SegmentMap::iterator found = segments.find(std::make_pair(x, y)); + if (found == segments.end()) + return osg::ref_ptr(); + else + return found->second.mFogOfWarTexture; + } - for (auto& camera : mCamerasPendingRemoval) - removeCamera(camera); + void LocalMap::cleanupCameras() + { + auto it = mLocalMapRTTs.begin(); + while (it != mLocalMapRTTs.end()) + { + if (!(*it)->mActive) + { + mRoot->removeChild(*it); + it = mLocalMapRTTs.erase(it); + } + else + it++; + } + } - mCamerasPendingRemoval.clear(); -} + void LocalMap::requestExteriorMap(const MWWorld::CellStore* cell, MapSegment& segment) + { + mInterior = false; -void LocalMap::requestExteriorMap(const MWWorld::CellStore* cell) -{ - mInterior = false; + const int x = cell->getCell()->getGridX(); + const int y = cell->getCell()->getGridY(); - int x = cell->getCell()->getGridX(); - int y = cell->getCell()->getGridY(); + osg::BoundingSphere bound = mSceneRoot->getBound(); + float zmin = bound.center().z() - bound.radius(); + float zmax = bound.center().z() + bound.radius(); - osg::BoundingSphere bound = mSceneRoot->getBound(); - float zmin = bound.center().z() - bound.radius(); - float zmax = bound.center().z() + bound.radius(); + setupRenderToTexture(x, y, x * mMapWorldSize + mMapWorldSize / 2.f, y * mMapWorldSize + mMapWorldSize / 2.f, + osg::Vec3d(0, 1, 0), zmin, zmax); - osg::ref_ptr camera = createOrthographicCamera(x*mMapWorldSize + mMapWorldSize/2.f, y*mMapWorldSize + mMapWorldSize/2.f, mMapWorldSize, mMapWorldSize, - osg::Vec3d(0,1,0), zmin, zmax); - setupRenderToTexture(camera, cell->getCell()->getGridX(), cell->getCell()->getGridY()); + if (segment.mFogOfWarImage != nullptr) + return; - MapSegment& segment = mSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; - if (!segment.mFogOfWarImage) - { if (cell->getFog()) segment.loadFogOfWar(cell->getFog()->mFogTextures.back()); else segment.initFogOfWar(); } -} -void LocalMap::requestInteriorMap(const MWWorld::CellStore* cell) -{ - osg::ComputeBoundsVisitor computeBoundsVisitor; - computeBoundsVisitor.setTraversalMask(Mask_Scene | Mask_Terrain | Mask_Object | Mask_Static); - mSceneRoot->accept(computeBoundsVisitor); + static osg::Vec2f getNorthVector(const MWWorld::CellStore* cell) + { + MWWorld::ConstPtr northmarker = cell->searchConst(ESM::RefId::stringRefId("northmarker")); - osg::BoundingBox bounds = computeBoundsVisitor.getBoundingBox(); + if (northmarker.isEmpty()) + return osg::Vec2f(0, 1); - // If we're in an empty cell, bail out - // The operations in this function are only valid for finite bounds - if (!bounds.valid() || bounds.radius2() == 0.0) - return; + osg::Quat orient(-northmarker.getRefData().getPosition().rot[2], osg::Vec3f(0, 0, 1)); + osg::Vec3f dir = orient * osg::Vec3f(0, 1, 0); + osg::Vec2f d(dir.x(), dir.y()); + return d; + } - mInterior = true; + void LocalMap::requestInteriorMap(const MWWorld::CellStore* cell) + { + osg::ComputeBoundsVisitor computeBoundsVisitor; + computeBoundsVisitor.setTraversalMask(Mask_Scene | Mask_Terrain | Mask_Object | Mask_Static); + mSceneRoot->accept(computeBoundsVisitor); - mBounds = bounds; + osg::BoundingBox bounds = computeBoundsVisitor.getBoundingBox(); - // Get the cell's NorthMarker rotation. This is used to rotate the entire map. - osg::Vec2f north = MWBase::Environment::get().getWorld()->getNorthVector(cell); + // If we're in an empty cell, bail out + // The operations in this function are only valid for finite bounds + if (!bounds.valid() || bounds.radius2() == 0.0) + return; - mAngle = std::atan2(north.x(), north.y()); + mInterior = true; - // Rotate the cell and merge the rotated corners to the bounding box - osg::Vec2f origCenter(bounds.center().x(), bounds.center().y()); - osg::Vec3f origCorners[8]; - for (int i=0; i<8; ++i) - origCorners[i] = mBounds.corner(i); + mBounds = bounds; - for (int i=0; i<8; ++i) - { - osg::Vec3f corner = origCorners[i]; - osg::Vec2f corner2d (corner.x(), corner.y()); - corner2d = rotatePoint(corner2d, origCenter, mAngle); - mBounds.expandBy(osg::Vec3f(corner2d.x(), corner2d.y(), 0)); - } + // Get the cell's NorthMarker rotation. This is used to rotate the entire map. + osg::Vec2f north = getNorthVector(cell); - // Do NOT change padding! This will break older savegames. - // If the padding really needs to be changed, then it must be saved in the ESM::FogState and - // assume the old (500) value as default for older savegames. - const float padding = 500.0f; + mAngle = std::atan2(north.x(), north.y()); - // Apply a little padding - mBounds.set(mBounds._min - osg::Vec3f(padding,padding,0.f), - mBounds._max + osg::Vec3f(padding,padding,0.f)); + // Rotate the cell and merge the rotated corners to the bounding box + osg::Vec2f origCenter(bounds.center().x(), bounds.center().y()); + osg::Vec3f origCorners[8]; + for (int i = 0; i < 8; ++i) + origCorners[i] = mBounds.corner(i); - float zMin = mBounds.zMin(); - float zMax = mBounds.zMax(); + for (int i = 0; i < 8; ++i) + { + osg::Vec3f corner = origCorners[i]; + osg::Vec2f corner2d(corner.x(), corner.y()); + corner2d = rotatePoint(corner2d, origCenter, mAngle); + mBounds.expandBy(osg::Vec3f(corner2d.x(), corner2d.y(), 0)); + } - // If there is fog state in the CellStore (e.g. when it came from a savegame) we need to do some checks - // to see if this state is still valid. - // Both the cell bounds and the NorthMarker rotation could be changed by the content files or exchanged models. - // If they changed by too much then parts of the interior might not be covered by the map anymore. - // The following code detects this, and discards the CellStore's fog state if it needs to. - std::vector> segmentMappings; - if (cell->getFog()) - { - ESM::FogState* fog = cell->getFog(); + // Do NOT change padding! This will break older savegames. + // If the padding really needs to be changed, then it must be saved in the ESM::FogState and + // assume the old (500) value as default for older savegames. + const float padding = 500.0f; + + // Apply a little padding + mBounds.set(mBounds._min - osg::Vec3f(padding, padding, 0.f), mBounds._max + osg::Vec3f(padding, padding, 0.f)); - if (std::abs(mAngle - fog->mNorthMarkerAngle) < osg::DegreesToRadians(5.f)) + float zMin = mBounds.zMin(); + float zMax = mBounds.zMax(); + + // If there is fog state in the CellStore (e.g. when it came from a savegame) we need to do some checks + // to see if this state is still valid. + // Both the cell bounds and the NorthMarker rotation could be changed by the content files or exchanged models. + // If they changed by too much then parts of the interior might not be covered by the map anymore. + // The following code detects this, and discards the CellStore's fog state if it needs to. + std::vector> segmentMappings; + if (cell->getFog()) { - // Expand mBounds so the saved textures fit the same grid - int xOffset = 0; - int yOffset = 0; - if(fog->mBounds.mMinX < mBounds.xMin()) - { - mBounds.xMin() = fog->mBounds.mMinX; - } - else if(fog->mBounds.mMinX > mBounds.xMin()) - { - float diff = fog->mBounds.mMinX - mBounds.xMin(); - xOffset += diff / mMapWorldSize; - xOffset++; - mBounds.xMin() = fog->mBounds.mMinX - xOffset * mMapWorldSize; - } - if(fog->mBounds.mMinY < mBounds.yMin()) - { - mBounds.yMin() = fog->mBounds.mMinY; - } - else if(fog->mBounds.mMinY > mBounds.yMin()) + ESM::FogState* fog = cell->getFog(); + + if (std::abs(mAngle - fog->mNorthMarkerAngle) < osg::DegreesToRadians(5.f)) { - float diff = fog->mBounds.mMinY - mBounds.yMin(); - yOffset += diff / mMapWorldSize; - yOffset++; - mBounds.yMin() = fog->mBounds.mMinY - yOffset * mMapWorldSize; + // Expand mBounds so the saved textures fit the same grid + int xOffset = 0; + int yOffset = 0; + if (fog->mBounds.mMinX < mBounds.xMin()) + { + mBounds.xMin() = fog->mBounds.mMinX; + } + else if (fog->mBounds.mMinX > mBounds.xMin()) + { + float diff = fog->mBounds.mMinX - mBounds.xMin(); + xOffset += diff / mMapWorldSize; + xOffset++; + mBounds.xMin() = fog->mBounds.mMinX - xOffset * mMapWorldSize; + } + if (fog->mBounds.mMinY < mBounds.yMin()) + { + mBounds.yMin() = fog->mBounds.mMinY; + } + else if (fog->mBounds.mMinY > mBounds.yMin()) + { + float diff = fog->mBounds.mMinY - mBounds.yMin(); + yOffset += diff / mMapWorldSize; + yOffset++; + mBounds.yMin() = fog->mBounds.mMinY - yOffset * mMapWorldSize; + } + if (fog->mBounds.mMaxX > mBounds.xMax()) + mBounds.xMax() = fog->mBounds.mMaxX; + if (fog->mBounds.mMaxY > mBounds.yMax()) + mBounds.yMax() = fog->mBounds.mMaxY; + + if (xOffset != 0 || yOffset != 0) + Log(Debug::Warning) << "Warning: expanding fog by " << xOffset << ", " << yOffset; + + const auto& textures = fog->mFogTextures; + segmentMappings.reserve(textures.size()); + osg::BoundingBox savedBounds{ fog->mBounds.mMinX, fog->mBounds.mMinY, 0, fog->mBounds.mMaxX, + fog->mBounds.mMaxY, 0 }; + auto segments = divideIntoSegments(savedBounds, mMapWorldSize); + for (int x = 0; x < segments.first; ++x) + for (int y = 0; y < segments.second; ++y) + segmentMappings.emplace_back(std::make_pair(x + xOffset, y + yOffset)); + + mAngle = fog->mNorthMarkerAngle; } - mBounds.xMax() = std::max(mBounds.xMax(), fog->mBounds.mMaxX); - mBounds.yMax() = std::max(mBounds.yMax(), fog->mBounds.mMaxY); - - if(xOffset != 0 || yOffset != 0) - Log(Debug::Warning) << "Warning: expanding fog by " << xOffset << ", " << yOffset; - - const auto& textures = fog->mFogTextures; - segmentMappings.reserve(textures.size()); - osg::BoundingBox savedBounds{ - fog->mBounds.mMinX, fog->mBounds.mMinY, 0, - fog->mBounds.mMaxX, fog->mBounds.mMaxY, 0 - }; - auto segments = divideIntoSegments(savedBounds, mMapWorldSize); - for (int x = 0; x < segments.first; ++x) - for (int y = 0; y < segments.second; ++y) - segmentMappings.emplace_back(std::make_pair(x + xOffset, y + yOffset)); - - mAngle = fog->mNorthMarkerAngle; } - } - osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); + osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); - osg::Vec2f center(mBounds.center().x(), mBounds.center().y()); - osg::Quat cameraOrient (mAngle, osg::Vec3d(0,0,-1)); + osg::Vec2f center(mBounds.center().x(), mBounds.center().y()); + osg::Quat cameraOrient(mAngle, osg::Vec3d(0, 0, -1)); - auto segments = divideIntoSegments(mBounds, mMapWorldSize); - for (int x = 0; x < segments.first; ++x) - { - for (int y = 0; y < segments.second; ++y) + auto segments = divideIntoSegments(mBounds, mMapWorldSize); + for (int x = 0; x < segments.first; ++x) { - osg::Vec2f start = min + osg::Vec2f(mMapWorldSize*x, mMapWorldSize*y); - osg::Vec2f newcenter = start + osg::Vec2f(mMapWorldSize/2.f, mMapWorldSize/2.f); - - osg::Vec2f a = newcenter - center; - osg::Vec3f rotatedCenter = cameraOrient * (osg::Vec3f(a.x(), a.y(), 0)); + for (int y = 0; y < segments.second; ++y) + { + osg::Vec2f start = min + osg::Vec2f(mMapWorldSize * x, mMapWorldSize * y); + osg::Vec2f newcenter = start + osg::Vec2f(mMapWorldSize / 2.f, mMapWorldSize / 2.f); - osg::Vec2f pos = osg::Vec2f(rotatedCenter.x(), rotatedCenter.y()) + center; + osg::Vec2f a = newcenter - center; + osg::Vec3f rotatedCenter = cameraOrient * (osg::Vec3f(a.x(), a.y(), 0)); - osg::ref_ptr camera = createOrthographicCamera(pos.x(), pos.y(), - mMapWorldSize, mMapWorldSize, - osg::Vec3f(north.x(), north.y(), 0.f), zMin, zMax); + osg::Vec2f pos = osg::Vec2f(rotatedCenter.x(), rotatedCenter.y()) + center; - setupRenderToTexture(camera, x, y); + setupRenderToTexture(x, y, pos.x(), pos.y(), osg::Vec3f(north.x(), north.y(), 0.f), zMin, zMax); - auto coords = std::make_pair(x,y); - MapSegment& segment = mSegments[coords]; - if (!segment.mFogOfWarImage) - { - bool loaded = false; - for(size_t index{}; index < segmentMappings.size(); index++) + auto coords = std::make_pair(x, y); + MapSegment& segment = mInteriorSegments[coords]; + if (!segment.mFogOfWarImage) { - if(segmentMappings[index] == coords) + bool loaded = false; + for (size_t index{}; index < segmentMappings.size(); index++) { - ESM::FogState* fog = cell->getFog(); - segment.loadFogOfWar(fog->mFogTextures[index]); - loaded = true; - break; + if (segmentMappings[index] == coords) + { + ESM::FogState* fog = cell->getFog(); + segment.loadFogOfWar(fog->mFogTextures[index]); + loaded = true; + break; + } } + if (!loaded) + segment.initFogOfWar(); } - if(!loaded) - segment.initFogOfWar(); } } } -} - -void LocalMap::worldToInteriorMapPosition (osg::Vec2f pos, float& nX, float& nY, int& x, int& y) -{ - pos = rotatePoint(pos, osg::Vec2f(mBounds.center().x(), mBounds.center().y()), mAngle); - - osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); - x = static_cast(std::ceil((pos.x() - min.x()) / mMapWorldSize) - 1); - y = static_cast(std::ceil((pos.y() - min.y()) / mMapWorldSize) - 1); + void LocalMap::worldToInteriorMapPosition(osg::Vec2f pos, float& nX, float& nY, int& x, int& y) + { + pos = rotatePoint(pos, osg::Vec2f(mBounds.center().x(), mBounds.center().y()), mAngle); - nX = (pos.x() - min.x() - mMapWorldSize*x)/mMapWorldSize; - nY = 1.0f-(pos.y() - min.y() - mMapWorldSize*y)/mMapWorldSize; -} + osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); -osg::Vec2f LocalMap::interiorMapToWorldPosition (float nX, float nY, int x, int y) -{ - osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); - osg::Vec2f pos (mMapWorldSize * (nX + x) + min.x(), - mMapWorldSize * (1.0f-nY + y) + min.y()); + x = static_cast(std::ceil((pos.x() - min.x()) / mMapWorldSize) - 1); + y = static_cast(std::ceil((pos.y() - min.y()) / mMapWorldSize) - 1); - pos = rotatePoint(pos, osg::Vec2f(mBounds.center().x(), mBounds.center().y()), -mAngle); - return pos; -} + nX = (pos.x() - min.x() - mMapWorldSize * x) / mMapWorldSize; + nY = 1.0f - (pos.y() - min.y() - mMapWorldSize * y) / mMapWorldSize; + } -bool LocalMap::isPositionExplored (float nX, float nY, int x, int y) -{ - const MapSegment& segment = mSegments[std::make_pair(x, y)]; - if (!segment.mFogOfWarImage) - return false; + osg::Vec2f LocalMap::interiorMapToWorldPosition(float nX, float nY, int x, int y) + { + osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); + osg::Vec2f pos(mMapWorldSize * (nX + x) + min.x(), mMapWorldSize * (1.0f - nY + y) + min.y()); - nX = std::max(0.f, std::min(1.f, nX)); - nY = std::max(0.f, std::min(1.f, nY)); + pos = rotatePoint(pos, osg::Vec2f(mBounds.center().x(), mBounds.center().y()), -mAngle); + return pos; + } - int texU = static_cast((sFogOfWarResolution - 1) * nX); - int texV = static_cast((sFogOfWarResolution - 1) * nY); + bool LocalMap::isPositionExplored(float nX, float nY, int x, int y) + { + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + const MapSegment& segment = segments[std::make_pair(x, y)]; + if (!segment.mFogOfWarImage) + return false; - uint32_t clr = ((const uint32_t*)segment.mFogOfWarImage->data())[texV * sFogOfWarResolution + texU]; - uint8_t alpha = (clr >> 24); - return alpha < 200; -} + nX = std::clamp(nX, 0.f, 1.f); + nY = std::clamp(nY, 0.f, 1.f); -osg::Group* LocalMap::getRoot() -{ - return mRoot; -} + int texU = static_cast((sFogOfWarResolution - 1) * nX); + int texV = static_cast((sFogOfWarResolution - 1) * nY); -void LocalMap::updatePlayer (const osg::Vec3f& position, const osg::Quat& orientation, - float& u, float& v, int& x, int& y, osg::Vec3f& direction) -{ - // retrieve the x,y grid coordinates the player is in - osg::Vec2f pos(position.x(), position.y()); + const std::uint32_t clr + = reinterpret_cast(segment.mFogOfWarImage->data())[texV * sFogOfWarResolution + texU]; + uint8_t alpha = (clr >> 24); + return alpha < 200; + } - if (mInterior) + osg::Group* LocalMap::getRoot() { - worldToInteriorMapPosition(pos, u,v, x,y); - - osg::Quat cameraOrient (mAngle, osg::Vec3(0,0,-1)); - direction = orientation * cameraOrient.inverse() * osg::Vec3f(0,1,0); + return mRoot; } - else + + void LocalMap::updatePlayer(const osg::Vec3f& position, const osg::Quat& orientation, float& u, float& v, int& x, + int& y, osg::Vec3f& direction) { - direction = orientation * osg::Vec3f(0,1,0); + // retrieve the x,y grid coordinates the player is in + osg::Vec2f pos(position.x(), position.y()); - x = static_cast(std::ceil(pos.x() / mMapWorldSize) - 1); - y = static_cast(std::ceil(pos.y() / mMapWorldSize) - 1); + if (mInterior) + { + worldToInteriorMapPosition(pos, u, v, x, y); - // convert from world coordinates to texture UV coordinates - u = std::abs((pos.x() - (mMapWorldSize*x))/mMapWorldSize); - v = 1.0f-std::abs((pos.y() - (mMapWorldSize*y))/mMapWorldSize); - } + osg::Quat cameraOrient(mAngle, osg::Vec3(0, 0, -1)); + direction = orientation * cameraOrient.inverse() * osg::Vec3f(0, 1, 0); + } + else + { + direction = orientation * osg::Vec3f(0, 1, 0); - // explore radius (squared) - const float exploreRadius = 0.17f * (sFogOfWarResolution-1); // explore radius from 0 to sFogOfWarResolution-1 - const float sqrExploreRadius = square(exploreRadius); - const float exploreRadiusUV = exploreRadius / sFogOfWarResolution; // explore radius from 0 to 1 (UV space) + x = static_cast(std::ceil(pos.x() / mMapWorldSize) - 1); + y = static_cast(std::ceil(pos.y() / mMapWorldSize) - 1); - // change the affected fog of war textures (in a 3x3 grid around the player) - for (int mx = -mCellDistance; mx<=mCellDistance; ++mx) - { - for (int my = -mCellDistance; my<=mCellDistance; ++my) + // convert from world coordinates to texture UV coordinates + u = std::abs((pos.x() - (mMapWorldSize * x)) / mMapWorldSize); + v = 1.0f - std::abs((pos.y() - (mMapWorldSize * y)) / mMapWorldSize); + } + + // explore radius (squared) + const float exploreRadius = 0.17f * (sFogOfWarResolution - 1); // explore radius from 0 to sFogOfWarResolution-1 + const float sqrExploreRadius = square(exploreRadius); + const float exploreRadiusUV = exploreRadius / sFogOfWarResolution; // explore radius from 0 to 1 (UV space) + + // change the affected fog of war textures (in a 3x3 grid around the player) + for (int mx = -mCellDistance; mx <= mCellDistance; ++mx) { - // is this texture affected at all? - bool affected = false; - if (mx == 0 && my == 0) // the player is always in the center of the 3x3 grid - affected = true; - else + for (int my = -mCellDistance; my <= mCellDistance; ++my) { - bool affectsX = (mx > 0)? (u + exploreRadiusUV > 1) : (u - exploreRadiusUV < 0); - bool affectsY = (my > 0)? (v + exploreRadiusUV > 1) : (v - exploreRadiusUV < 0); - affected = (affectsX && (my == 0)) || (affectsY && mx == 0) || (affectsX && affectsY); - } + // is this texture affected at all? + bool affected = false; + if (mx == 0 && my == 0) // the player is always in the center of the 3x3 grid + affected = true; + else + { + bool affectsX = (mx > 0) ? (u + exploreRadiusUV > 1) : (u - exploreRadiusUV < 0); + bool affectsY = (my > 0) ? (v + exploreRadiusUV > 1) : (v - exploreRadiusUV < 0); + affected = (affectsX && (my == 0)) || (affectsY && mx == 0) || (affectsX && affectsY); + } - if (!affected) - continue; + if (!affected) + continue; - int texX = x + mx; - int texY = y + my*-1; + int texX = x + mx; + int texY = y + my * -1; - MapSegment& segment = mSegments[std::make_pair(texX, texY)]; + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + MapSegment& segment = segments[std::make_pair(texX, texY)]; - if (!segment.mFogOfWarImage || !segment.mMapTexture) - continue; + if (!segment.mFogOfWarImage || !segment.mMapTexture) + continue; - uint32_t* data = (uint32_t*)segment.mFogOfWarImage->data(); - bool changed = false; - for (int texV = 0; texV(segment.mFogOfWarImage->data()); + bool changed = false; + for (int texV = 0; texV < sFogOfWarResolution; ++texV) { - float sqrDist = square((texU + mx*(sFogOfWarResolution-1)) - u*(sFogOfWarResolution-1)) - + square((texV + my*(sFogOfWarResolution-1)) - v*(sFogOfWarResolution-1)); - - uint32_t clr = *(uint32_t*)data; - uint8_t alpha = (clr >> 24); - - alpha = std::min( alpha, (uint8_t) (std::max(0.f, std::min(1.f, (sqrDist/sqrExploreRadius)))*255) ); - uint32_t val = (uint32_t) (alpha << 24); - if ( *data != val) + for (int texU = 0; texU < sFogOfWarResolution; ++texU) { - *data = val; - changed = true; + float sqrDist = square((texU + mx * (sFogOfWarResolution - 1)) - u * (sFogOfWarResolution - 1)) + + square((texV + my * (sFogOfWarResolution - 1)) - v * (sFogOfWarResolution - 1)); + + const std::uint8_t alpha = std::min( + *data >> 24, std::clamp(sqrDist / sqrExploreRadius, 0.f, 1.f) * 255); + std::uint32_t val = static_cast(alpha << 24); + if (*data != val) + { + *data = val; + changed = true; + } + + ++data; } - - ++data; } - } - if (changed) - { - segment.mHasFogState = true; - segment.mFogOfWarImage->dirty(); + if (changed) + { + segment.mHasFogState = true; + segment.mFogOfWarImage->dirty(); + } } } } -} -LocalMap::MapSegment::MapSegment() - : mHasFogState(false) -{ -} + std::uint8_t LocalMap::getExteriorNeighbourFlags(int cellX, int cellY) const + { + constexpr std::tuple flags[] = { + { NeighbourCellTopLeft, -1, -1 }, + { NeighbourCellTopCenter, 0, -1 }, + { NeighbourCellTopRight, 1, -1 }, + { NeighbourCellMiddleLeft, -1, 0 }, + { NeighbourCellMiddleRight, 1, 0 }, + { NeighbourCellBottomLeft, -1, 1 }, + { NeighbourCellBottomCenter, 0, 1 }, + { NeighbourCellBottomRight, 1, 1 }, + }; + std::uint8_t result = 0; + for (const auto& [flag, dx, dy] : flags) + if (mExteriorSegments.contains(std::pair(cellX + dx, cellY + dy))) + result |= flag; + return result; + } -LocalMap::MapSegment::~MapSegment() -{ + void LocalMap::MapSegment::createFogOfWarTexture() + { + if (mFogOfWarTexture) + return; + mFogOfWarTexture = new osg::Texture2D; + // TODO: synchronize access? for now, the worst that could happen is the draw thread jumping a frame ahead. + // mFogOfWarTexture->setDataVariance(osg::Object::DYNAMIC); + mFogOfWarTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + mFogOfWarTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + mFogOfWarTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mFogOfWarTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mFogOfWarTexture->setUnRefImageDataAfterApply(false); + mFogOfWarTexture->setImage(mFogOfWarImage); + } -} + void LocalMap::MapSegment::initFogOfWar() + { + mFogOfWarImage = new osg::Image; + // Assign a PixelBufferObject for asynchronous transfer of data to the GPU + mFogOfWarImage->setPixelBufferObject(new osg::PixelBufferObject); + mFogOfWarImage->allocateImage(sFogOfWarResolution, sFogOfWarResolution, 1, GL_RGBA, GL_UNSIGNED_BYTE); + assert(mFogOfWarImage->isDataContiguous()); + std::vector data; + data.resize(sFogOfWarResolution * sFogOfWarResolution, 0xff000000); -void LocalMap::MapSegment::createFogOfWarTexture() -{ - if (mFogOfWarTexture) - return; - mFogOfWarTexture = new osg::Texture2D; - // TODO: synchronize access? for now, the worst that could happen is the draw thread jumping a frame ahead. - //mFogOfWarTexture->setDataVariance(osg::Object::DYNAMIC); - mFogOfWarTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mFogOfWarTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mFogOfWarTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mFogOfWarTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mFogOfWarTexture->setUnRefImageDataAfterApply(false); -} + memcpy(mFogOfWarImage->data(), data.data(), data.size() * 4); -void LocalMap::MapSegment::initFogOfWar() -{ - mFogOfWarImage = new osg::Image; - // Assign a PixelBufferObject for asynchronous transfer of data to the GPU - mFogOfWarImage->setPixelBufferObject(new osg::PixelBufferObject); - mFogOfWarImage->allocateImage(sFogOfWarResolution, sFogOfWarResolution, 1, GL_RGBA, GL_UNSIGNED_BYTE); - assert(mFogOfWarImage->isDataContiguous()); - std::vector data; - data.resize(sFogOfWarResolution*sFogOfWarResolution, 0xff000000); - - memcpy(mFogOfWarImage->data(), &data[0], data.size()*4); - - createFogOfWarTexture(); - mFogOfWarTexture->setImage(mFogOfWarImage); -} + createFogOfWarTexture(); + } -void LocalMap::MapSegment::loadFogOfWar(const ESM::FogTexture &esm) -{ - const std::vector& data = esm.mImageData; - if (data.empty()) + void LocalMap::MapSegment::loadFogOfWar(const ESM::FogTexture& esm) { - initFogOfWar(); - return; + const std::vector& data = esm.mImageData; + if (data.empty()) + { + initFogOfWar(); + return; + } + + osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); + if (!readerwriter) + { + Log(Debug::Error) << "Error: Unable to load fog, can't find a png ReaderWriter"; + return; + } + + Files::IMemStream in(data.data(), data.size()); + + osgDB::ReaderWriter::ReadResult result = readerwriter->readImage(in); + if (!result.success()) + { + Log(Debug::Error) << "Error: Failed to read fog: " << result.message() << " code " << result.status(); + return; + } + + mFogOfWarImage = result.getImage(); + mFogOfWarImage->flipVertical(); + mFogOfWarImage->dirty(); + + createFogOfWarTexture(); + mHasFogState = true; } - osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); - if (!readerwriter) + void LocalMap::MapSegment::saveFogOfWar(ESM::FogTexture& fog) const { - Log(Debug::Error) << "Error: Unable to load fog, can't find a png ReaderWriter" ; - return; - } + if (!mFogOfWarImage) + return; - Files::IMemStream in(&data[0], data.size()); + std::ostringstream ostream; - osgDB::ReaderWriter::ReadResult result = readerwriter->readImage(in); - if (!result.success()) - { - Log(Debug::Error) << "Error: Failed to read fog: " << result.message() << " code " << result.status(); - return; + osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); + if (!readerwriter) + { + Log(Debug::Error) << "Error: Unable to write fog, can't find a png ReaderWriter"; + return; + } + + // extra flips are unfortunate, but required for compatibility with older versions + mFogOfWarImage->flipVertical(); + osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*mFogOfWarImage, ostream); + if (!result.success()) + { + Log(Debug::Error) << "Error: Unable to write fog: " << result.message() << " code " << result.status(); + return; + } + mFogOfWarImage->flipVertical(); + + std::string data = ostream.str(); + fog.mImageData = std::vector(data.begin(), data.end()); } - mFogOfWarImage = result.getImage(); - mFogOfWarImage->flipVertical(); - mFogOfWarImage->dirty(); + LocalMapRenderToTexture::LocalMapRenderToTexture(osg::Node* sceneRoot, int res, int mapWorldSize, float x, float y, + const osg::Vec3d& upVector, float zmin, float zmax) + : RTTNode(res, res, 0, false, 0, StereoAwareness::Unaware_MultiViewShaders, shouldAddMSAAIntermediateTarget()) + , mSceneRoot(sceneRoot) + , mActive(true) + { + setNodeMask(Mask_RenderToTexture); - createFogOfWarTexture(); - mFogOfWarTexture->setImage(mFogOfWarImage); - mHasFogState = true; -} + if (SceneUtil::AutoDepth::isReversed()) + mProjectionMatrix = SceneUtil::getReversedZProjectionMatrixAsOrtho( + -mapWorldSize / 2, mapWorldSize / 2, -mapWorldSize / 2, mapWorldSize / 2, 5, (zmax - zmin) + 10); + else + mProjectionMatrix.makeOrtho( + -mapWorldSize / 2, mapWorldSize / 2, -mapWorldSize / 2, mapWorldSize / 2, 5, (zmax - zmin) + 10); -void LocalMap::MapSegment::saveFogOfWar(ESM::FogTexture &fog) const -{ - if (!mFogOfWarImage) - return; + mViewMatrix.makeLookAt(osg::Vec3d(x, y, zmax + 5), osg::Vec3d(x, y, zmin), upVector); - std::ostringstream ostream; + setUpdateCallback(new CameraLocalUpdateCallback); + setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); + } - osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); - if (!readerwriter) + void LocalMapRenderToTexture::setDefaults(osg::Camera* camera) { - Log(Debug::Error) << "Error: Unable to write fog, can't find a png ReaderWriter"; - return; + // Disable small feature culling, it's not going to be reliable for this camera + osg::Camera::CullingMode cullingMode + = (osg::Camera::DEFAULT_CULLING | osg::Camera::FAR_PLANE_CULLING) & ~(osg::Camera::SMALL_FEATURE_CULLING); + camera->setCullingMode(cullingMode); + + SceneUtil::setCameraClearDepth(camera); + camera->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); + camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF_INHERIT_VIEWPOINT); + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); + camera->setClearColor(osg::Vec4(0.f, 0.f, 0.f, 1.f)); + camera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + camera->setRenderOrder(osg::Camera::PRE_RENDER); + + camera->setCullMask(Mask_Scene | Mask_SimpleWater | Mask_Terrain | Mask_Object | Mask_Static); + camera->setCullMaskLeft(Mask_Scene | Mask_SimpleWater | Mask_Terrain | Mask_Object | Mask_Static); + camera->setCullMaskRight(Mask_Scene | Mask_SimpleWater | Mask_Terrain | Mask_Object | Mask_Static); + camera->setNodeMask(Mask_RenderToTexture); + camera->setProjectionMatrix(mProjectionMatrix); + camera->setViewMatrix(mViewMatrix); + + auto* stateset = camera->getOrCreateStateSet(); + + stateset->setAttribute(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::FILL), + osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("projectionMatrix", static_cast(mProjectionMatrix)), + osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + if (Stereo::getMultiview()) + Stereo::setMultiviewMatrices(stateset, { mProjectionMatrix, mProjectionMatrix }); + + // assign large value to effectively turn off fog + // shaders don't respect glDisable(GL_FOG) + osg::ref_ptr fog(new osg::Fog); + fog->setStart(10000000); + fog->setEnd(10000000); + stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); + + // turn of sky blending + stateset->addUniform(new osg::Uniform("far", 10000000.0f)); + stateset->addUniform(new osg::Uniform("skyBlendingStart", 8000000.0f)); + stateset->addUniform(new osg::Uniform("sky", 0)); + stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{ 1, 1 })); + + osg::ref_ptr lightmodel = new osg::LightModel; + lightmodel->setAmbientIntensity(osg::Vec4(0.3f, 0.3f, 0.3f, 1.f)); + stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + osg::ref_ptr light = new osg::Light; + light->setPosition(osg::Vec4(-0.3f, -0.3f, 0.7f, 0.f)); + light->setDiffuse(osg::Vec4(0.7f, 0.7f, 0.7f, 1.f)); + light->setAmbient(osg::Vec4(0, 0, 0, 1)); + light->setSpecular(osg::Vec4(0, 0, 0, 0)); + light->setLightNum(0); + light->setConstantAttenuation(1.f); + light->setLinearAttenuation(0.f); + light->setQuadraticAttenuation(0.f); + + osg::ref_ptr lightSource = new osg::LightSource; + lightSource->setLight(light); + + lightSource->setStateSetModes(*stateset, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*stateset); + + // override sun for local map + SceneUtil::configureStateSetSunOverride(static_cast(mSceneRoot), light, stateset); + + camera->addChild(lightSource); + camera->addChild(mSceneRoot); } - // extra flips are unfortunate, but required for compatibility with older versions - mFogOfWarImage->flipVertical(); - osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*mFogOfWarImage, ostream); - if (!result.success()) + void CameraLocalUpdateCallback::operator()(LocalMapRenderToTexture* node, osg::NodeVisitor* nv) { - Log(Debug::Error) << "Error: Unable to write fog: " << result.message() << " code " << result.status(); - return; - } - mFogOfWarImage->flipVertical(); + if (!node->mActive) + node->setNodeMask(0); - std::string data = ostream.str(); - fog.mImageData = std::vector(data.begin(), data.end()); -} + node->mActive = false; + + // Rtt-nodes do not forward update traversal to their cameras so we can traverse safely. + // Traverse in case there are nested callbacks. + traverse(node, nv); + } } diff --git a/apps/openmw/mwrender/localmap.hpp b/apps/openmw/mwrender/localmap.hpp index 83a975aeda2..9fd101c45f7 100644 --- a/apps/openmw/mwrender/localmap.hpp +++ b/apps/openmw/mwrender/localmap.hpp @@ -1,9 +1,10 @@ #ifndef GAME_RENDER_LOCALMAP_H #define GAME_RENDER_LOCALMAP_H +#include +#include #include #include -#include #include #include @@ -30,6 +31,8 @@ namespace osg namespace MWRender { + class LocalMapRenderToTexture; + /// /// \brief Local map rendering /// @@ -45,24 +48,19 @@ namespace MWRender void clear(); /** - * Request a map render for the given cell. Render textures will be immediately created and can be retrieved with the getMapTexture function. + * Request a map render for the given cell. Render textures will be immediately created and can be retrieved + * with the getMapTexture function. */ - void requestMap (const MWWorld::CellStore* cell); + void requestMap(const MWWorld::CellStore* cell); void addCell(MWWorld::CellStore* cell); + void removeExteriorCell(int x, int y); - void removeCell (MWWorld::CellStore* cell); - - osg::ref_ptr getMapTexture (int x, int y); + void removeCell(MWWorld::CellStore* cell); - osg::ref_ptr getFogOfWarTexture (int x, int y); + osg::ref_ptr getMapTexture(int x, int y); - void removeCamera(osg::Camera* cam); - - /** - * Indicates a camera has been queued for rendering and can be cleaned up in the next frame. For internal use only. - */ - void markForRemoval(osg::Camera* cam); + osg::ref_ptr getFogOfWarTexture(int x, int y); /** * Removes cameras that have already been rendered. Should be called every frame to ensure that @@ -72,12 +70,13 @@ namespace MWRender void cleanupCameras(); /** - * Set the position & direction of the player, and returns the position in map space through the reference parameters. + * Set the position & direction of the player, and returns the position in map space through the reference + * parameters. * @remarks This is used to draw a "fog of war" effect * to hide areas on the map the player has not discovered yet. */ - void updatePlayer (const osg::Vec3f& position, const osg::Quat& orientation, - float& u, float& v, int& x, int& y, osg::Vec3f& direction); + void updatePlayer(const osg::Vec3f& position, const osg::Quat& orientation, float& u, float& v, int& x, int& y, + osg::Vec3f& direction); /** * Save the fog of war for this cell to its CellStore. @@ -88,14 +87,14 @@ namespace MWRender /** * Get the interior map texture index and normalized position on this texture, given a world position */ - void worldToInteriorMapPosition (osg::Vec2f pos, float& nX, float& nY, int& x, int& y); + void worldToInteriorMapPosition(osg::Vec2f pos, float& nX, float& nY, int& x, int& y); - osg::Vec2f interiorMapToWorldPosition (float nX, float nY, int x, int y); + osg::Vec2f interiorMapToWorldPosition(float nX, float nY, int x, int y); /** * Check if a given position is explored by the player (i.e. not obscured by fog of war) */ - bool isPositionExplored (float nX, float nY, int x, int y); + bool isPositionExplored(float nX, float nY, int x, int y); osg::Group* getRoot(); @@ -103,36 +102,41 @@ namespace MWRender osg::ref_ptr mRoot; osg::ref_ptr mSceneRoot; - typedef std::vector< osg::ref_ptr > CameraVector; - - CameraVector mActiveCameras; + typedef std::vector> RTTVector; + RTTVector mLocalMapRTTs; - CameraVector mCamerasPendingRemoval; - - typedef std::set > Grid; + typedef std::set> Grid; Grid mCurrentGrid; - struct MapSegment + enum NeighbourCellFlag : std::uint8_t { - MapSegment(); - ~MapSegment(); + NeighbourCellTopLeft = 1, + NeighbourCellTopCenter = 1 << 1, + NeighbourCellTopRight = 1 << 2, + NeighbourCellMiddleLeft = 1 << 3, + NeighbourCellMiddleRight = 1 << 4, + NeighbourCellBottomLeft = 1 << 5, + NeighbourCellBottomCenter = 1 << 6, + NeighbourCellBottomRight = 1 << 7, + }; + struct MapSegment + { void initFogOfWar(); void loadFogOfWar(const ESM::FogTexture& fog); void saveFogOfWar(ESM::FogTexture& fog) const; void createFogOfWarTexture(); + std::uint8_t mLastRenderNeighbourFlags = 0; + bool mHasFogState = false; osg::ref_ptr mMapTexture; osg::ref_ptr mFogOfWarTexture; osg::ref_ptr mFogOfWarImage; - - Grid mGrid; // the grid that was active at the time of rendering this segment - - bool mHasFogState; }; typedef std::map, MapSegment> SegmentMap; - SegmentMap mSegments; + SegmentMap mExteriorSegments; + SegmentMap mInteriorSegments; int mMapResolution; @@ -147,14 +151,16 @@ namespace MWRender float mAngle; const osg::Vec2f rotatePoint(const osg::Vec2f& point, const osg::Vec2f& center, const float angle); - void requestExteriorMap(const MWWorld::CellStore* cell); + void requestExteriorMap(const MWWorld::CellStore* cell, MapSegment& segment); void requestInteriorMap(const MWWorld::CellStore* cell); - osg::ref_ptr createOrthographicCamera(float left, float top, float width, float height, const osg::Vec3d& upVector, float zmin, float zmax); - void setupRenderToTexture(osg::ref_ptr camera, int x, int y); + void setupRenderToTexture( + int segment_x, int segment_y, float left, float top, const osg::Vec3d& upVector, float zmin, float zmax); bool mInterior; osg::BoundingBox mBounds; + + std::uint8_t getExteriorNeighbourFlags(int cellX, int cellY) const; }; } diff --git a/apps/openmw/mwrender/luminancecalculator.cpp b/apps/openmw/mwrender/luminancecalculator.cpp new file mode 100644 index 00000000000..f974ea5ad74 --- /dev/null +++ b/apps/openmw/mwrender/luminancecalculator.cpp @@ -0,0 +1,155 @@ +#include "luminancecalculator.hpp" + +#include +#include +#include + +#include "pingpongcanvas.hpp" + +namespace MWRender +{ + LuminanceCalculator::LuminanceCalculator(Shader::ShaderManager& shaderManager) + { + Shader::ShaderManager::DefineMap defines = { + { "hdrExposureTime", std::to_string(Settings::postProcessing().mAutoExposureSpeed) }, + }; + + auto vertex = shaderManager.getShader("fullscreen_tri.vert", {}); + auto luminanceFragment = shaderManager.getShader("luminance/luminance.frag", defines); + auto resolveFragment = shaderManager.getShader("luminance/resolve.frag", defines); + + mResolveProgram = shaderManager.getProgram(vertex, std::move(resolveFragment)); + mLuminanceProgram = shaderManager.getProgram(std::move(vertex), std::move(luminanceFragment)); + + for (auto& buffer : mBuffers) + { + buffer.mipmappedSceneLuminanceTex = new osg::Texture2D; + buffer.mipmappedSceneLuminanceTex->setInternalFormat(GL_R16F); + buffer.mipmappedSceneLuminanceTex->setSourceFormat(GL_RED); + buffer.mipmappedSceneLuminanceTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + buffer.mipmappedSceneLuminanceTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + buffer.mipmappedSceneLuminanceTex->setFilter( + osg::Texture2D::MIN_FILTER, osg::Texture2D::LINEAR_MIPMAP_NEAREST); + buffer.mipmappedSceneLuminanceTex->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::LINEAR); + buffer.mipmappedSceneLuminanceTex->setTextureSize(mWidth, mHeight); + + buffer.luminanceTex = new osg::Texture2D; + buffer.luminanceTex->setInternalFormat(GL_R16F); + buffer.luminanceTex->setSourceFormat(GL_RED); + buffer.luminanceTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + buffer.luminanceTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + buffer.luminanceTex->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::NEAREST); + buffer.luminanceTex->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::NEAREST); + buffer.luminanceTex->setTextureSize(1, 1); + + buffer.luminanceProxyTex = new osg::Texture2D(*buffer.luminanceTex); + buffer.luminanceProxyTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + buffer.luminanceProxyTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + buffer.resolveFbo = new osg::FrameBufferObject; + buffer.resolveFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + osg::FrameBufferAttachment(buffer.luminanceTex)); + + buffer.luminanceProxyFbo = new osg::FrameBufferObject; + buffer.luminanceProxyFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + osg::FrameBufferAttachment(buffer.luminanceProxyTex)); + + buffer.sceneLumSS = new osg::StateSet; + buffer.sceneLumSS->setAttributeAndModes(mLuminanceProgram); + buffer.sceneLumSS->addUniform(new osg::Uniform("sceneTex", 0)); + buffer.sceneLumSS->addUniform(new osg::Uniform("scaling", mScale)); + + buffer.resolveSS = new osg::StateSet; + buffer.resolveSS->setAttributeAndModes(mResolveProgram); + buffer.resolveSS->setTextureAttributeAndModes(0, buffer.luminanceProxyTex); + buffer.resolveSS->addUniform(new osg::Uniform("luminanceSceneTex", 0)); + buffer.resolveSS->addUniform(new osg::Uniform("prevLuminanceSceneTex", 1)); + } + + mBuffers[0].resolveSS->setTextureAttributeAndModes(1, mBuffers[1].luminanceTex); + mBuffers[1].resolveSS->setTextureAttributeAndModes(1, mBuffers[0].luminanceTex); + } + + void LuminanceCalculator::compile() + { + int mipmapLevels = osg::Image::computeNumberOfMipmapLevels(mWidth, mHeight); + + for (auto& buffer : mBuffers) + { + buffer.mipmappedSceneLuminanceTex->setTextureSize(mWidth, mHeight); + buffer.mipmappedSceneLuminanceTex->setNumMipmapLevels(mipmapLevels); + buffer.mipmappedSceneLuminanceTex->dirtyTextureObject(); + + buffer.resolveSceneLumFbo = new osg::FrameBufferObject; + buffer.resolveSceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex, mipmapLevels - 1)); + + buffer.sceneLumFbo = new osg::FrameBufferObject; + buffer.sceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex)); + } + + mCompiled = true; + } + + void LuminanceCalculator::draw(const PingPongCanvas& canvas, osg::RenderInfo& renderInfo, osg::State& state, + osg::GLExtensions* ext, size_t frameId) + { + if (!mEnabled) + return; + + bool dirty = !mCompiled; + + if (dirty) + compile(); + + auto& buffer = mBuffers[frameId]; + buffer.sceneLumFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + buffer.sceneLumSS->setTextureAttributeAndModes(0, canvas.getSceneTexture(frameId)); + buffer.sceneLumSS->getUniform("scaling")->set(mScale); + + state.apply(buffer.sceneLumSS); + canvas.drawGeometry(renderInfo); + + state.applyTextureAttribute(0, buffer.mipmappedSceneLuminanceTex); + ext->glGenerateMipmap(GL_TEXTURE_2D); + + buffer.resolveSceneLumFbo->apply(state, osg::FrameBufferObject::READ_FRAMEBUFFER); + buffer.luminanceProxyFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + ext->glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_NEAREST); + + if (mIsBlank) + { + // Use current frame data for previous frame to warm up calculations and prevent popin + mBuffers[(frameId + 1) % 2].resolveFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + ext->glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_NEAREST); + + buffer.luminanceProxyFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + mIsBlank = false; + } + + buffer.resolveFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + state.apply(buffer.resolveSS); + canvas.drawGeometry(renderInfo); + + ext->glBindFramebuffer( + GL_FRAMEBUFFER_EXT, state.getGraphicsContext() ? state.getGraphicsContext()->getDefaultFboId() : 0); + } + + osg::ref_ptr LuminanceCalculator::getLuminanceTexture(size_t frameId) const + { + return mBuffers[frameId].luminanceTex; + } + + void LuminanceCalculator::dirty(int w, int h) + { + constexpr int minSize = 64; + + mWidth = std::max(minSize, Misc::nextPowerOfTwo(w) / 2); + mHeight = std::max(minSize, Misc::nextPowerOfTwo(h) / 2); + + mScale = osg::Vec2f(w / static_cast(mWidth), h / static_cast(mHeight)); + + mCompiled = false; + } +} diff --git a/apps/openmw/mwrender/luminancecalculator.hpp b/apps/openmw/mwrender/luminancecalculator.hpp new file mode 100644 index 00000000000..8b51081e2f4 --- /dev/null +++ b/apps/openmw/mwrender/luminancecalculator.hpp @@ -0,0 +1,69 @@ +#ifndef OPENMW_MWRENDER_LUMINANCECALCULATOR_H +#define OPENMW_MWRENDER_LUMINANCECALCULATOR_H + +#include + +#include +#include +#include + +namespace Shader +{ + class ShaderManager; +} + +namespace MWRender +{ + class PingPongCanvas; + + class LuminanceCalculator + { + + public: + LuminanceCalculator() = default; + + LuminanceCalculator(Shader::ShaderManager& shaderManager); + + void draw(const PingPongCanvas& canvas, osg::RenderInfo& renderInfo, osg::State& state, osg::GLExtensions* ext, + size_t frameId); + + bool isEnabled() const { return mEnabled; } + + void enable() { mEnabled = true; } + void disable() { mEnabled = false; } + + void dirty(int w, int h); + + osg::ref_ptr getLuminanceTexture(size_t frameId) const; + + private: + void compile(); + + struct Container + { + osg::ref_ptr sceneLumFbo; + osg::ref_ptr resolveSceneLumFbo; + osg::ref_ptr resolveFbo; + osg::ref_ptr luminanceProxyFbo; + osg::ref_ptr mipmappedSceneLuminanceTex; + osg::ref_ptr luminanceTex; + osg::ref_ptr luminanceProxyTex; + osg::ref_ptr sceneLumSS; + osg::ref_ptr resolveSS; + }; + + std::array mBuffers; + osg::ref_ptr mLuminanceProgram; + osg::ref_ptr mResolveProgram; + + bool mCompiled = false; + bool mEnabled = false; + bool mIsBlank = true; + + int mWidth = 1; + int mHeight = 1; + osg::Vec2f mScale = osg::Vec2f(1, 1); + }; +} + +#endif diff --git a/apps/openmw/mwrender/navmesh.cpp b/apps/openmw/mwrender/navmesh.cpp index 791c41a1a0a..f1100ba5021 100644 --- a/apps/openmw/mwrender/navmesh.cpp +++ b/apps/openmw/mwrender/navmesh.cpp @@ -1,17 +1,204 @@ #include "navmesh.hpp" + #include "vismask.hpp" +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include #include +#include + +#include + +#include "../mwbase/environment.hpp" + +#include +#include namespace MWRender { - NavMesh::NavMesh(const osg::ref_ptr& root, bool enabled) + namespace + { + osg::ref_ptr makeDebugDrawStateSet() + { + const osg::ref_ptr lineWidth = new osg::LineWidth(); + + const osg::ref_ptr blendFunc = new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + const osg::ref_ptr depth = new SceneUtil::AutoDepth; + depth->setWriteMask(false); + + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setMode(GL_BLEND, osg::StateAttribute::ON); + stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + stateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + stateSet->setAttributeAndModes(lineWidth); + stateSet->setAttributeAndModes(blendFunc); + stateSet->setAttributeAndModes(depth); + + return stateSet; + } + } + + struct NavMesh::LessByTilePosition + { + bool operator()(const DetourNavigator::TilePosition& lhs, + const std::pair& rhs) const + { + return lhs < rhs.first; + } + + bool operator()(const std::pair& lhs, + const DetourNavigator::TilePosition& rhs) const + { + return lhs.first < rhs; + } + }; + + struct NavMesh::CreateNavMeshTileGroups final : SceneUtil::WorkItem + { + std::size_t mId; + DetourNavigator::Version mVersion; + const std::weak_ptr mNavMesh; + const osg::ref_ptr mGroupStateSet; + const osg::ref_ptr mDebugDrawStateSet; + const DetourNavigator::Settings mSettings; + std::map mTiles; + Settings::NavMeshRenderMode mMode; + std::atomic_bool mAborted{ false }; + std::mutex mMutex; + bool mStarted = false; + std::vector> mUpdatedTiles; + std::vector mRemovedTiles; + + explicit CreateNavMeshTileGroups(std::size_t id, DetourNavigator::Version version, + std::weak_ptr navMesh, + const osg::ref_ptr& groupStateSet, const osg::ref_ptr& debugDrawStateSet, + const DetourNavigator::Settings& settings, const std::map& tiles, + Settings::NavMeshRenderMode mode) + : mId(id) + , mVersion(version) + , mNavMesh(std::move(navMesh)) + , mGroupStateSet(groupStateSet) + , mDebugDrawStateSet(debugDrawStateSet) + , mSettings(settings) + , mTiles(tiles) + , mMode(mode) + { + } + + void doWork() final + { + using DetourNavigator::TilePosition; + using DetourNavigator::Version; + + const std::lock_guard lock(mMutex); + mStarted = true; + + if (mAborted.load(std::memory_order_acquire)) + return; + + const auto navMeshPtr = mNavMesh.lock(); + if (navMeshPtr == nullptr) + return; + + std::vector> existingTiles; + unsigned minSalt = std::numeric_limits::max(); + unsigned maxSalt = 0; + + navMeshPtr->lockConst()->forEachUsedTile( + [&](const TilePosition& position, const Version& version, const dtMeshTile& meshTile) { + existingTiles.emplace_back(position, version); + minSalt = std::min(minSalt, meshTile.salt); + maxSalt = std::max(maxSalt, meshTile.salt); + }); + + if (mAborted.load(std::memory_order_acquire)) + return; + + std::sort(existingTiles.begin(), existingTiles.end()); + + std::vector removedTiles; + + for (const auto& [position, tile] : mTiles) + if (!std::binary_search(existingTiles.begin(), existingTiles.end(), position, LessByTilePosition{})) + removedTiles.push_back(position); + + std::vector> updatedTiles; + + const unsigned char flags = SceneUtil::NavMeshTileDrawFlagsOffMeshConnections + | SceneUtil::NavMeshTileDrawFlagsClosedList + | (mMode == Settings::NavMeshRenderMode::UpdateFrequency ? SceneUtil::NavMeshTileDrawFlagsHeat : 0); + + for (const auto& [position, version] : existingTiles) + { + const auto it = mTiles.find(position); + if (it != mTiles.end() && it->second.mGroup != nullptr && it->second.mVersion == version + && mMode != Settings::NavMeshRenderMode::UpdateFrequency) + continue; + + osg::ref_ptr group; + { + const auto navMesh = navMeshPtr->lockConst(); + const dtMeshTile* meshTile = DetourNavigator::getTile(navMesh->getImpl(), position); + if (meshTile == nullptr) + continue; + + if (mAborted.load(std::memory_order_acquire)) + return; + + group = SceneUtil::createNavMeshTileGroup( + navMesh->getImpl(), *meshTile, mSettings, mDebugDrawStateSet, flags, minSalt, maxSalt); + } + if (group == nullptr) + { + removedTiles.push_back(position); + continue; + } + group->setNodeMask(Mask_Debug); + group->setStateSet(mGroupStateSet); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); + updatedTiles.emplace_back(position, Tile{ version, std::move(group) }); + } + + if (mAborted.load(std::memory_order_acquire)) + return; + + mUpdatedTiles = std::move(updatedTiles); + mRemovedTiles = std::move(removedTiles); + } + + void abort() final { mAborted.store(true, std::memory_order_release); } + }; + + struct NavMesh::DeallocateCreateNavMeshTileGroups final : SceneUtil::WorkItem + { + osg::ref_ptr mWorkItem; + + explicit DeallocateCreateNavMeshTileGroups(osg::ref_ptr&& workItem) + : mWorkItem(std::move(workItem)) + { + } + }; + + NavMesh::NavMesh(const osg::ref_ptr& root, const osg::ref_ptr& workQueue, + bool enabled, Settings::NavMeshRenderMode mode) : mRootNode(root) + , mWorkQueue(workQueue) + , mGroupStateSet(SceneUtil::makeDetourGroupStateSet()) + , mDebugDrawStateSet(makeDebugDrawStateSet()) , mEnabled(enabled) - , mGeneration(0) - , mRevision(0) + , mMode(mode) + , mId(std::numeric_limits::max()) { } @@ -19,6 +206,8 @@ namespace MWRender { if (mEnabled) disable(); + for (const auto& workItem : mWorkItems) + workItem->abort(); } bool NavMesh::toggle() @@ -31,45 +220,129 @@ namespace MWRender return mEnabled; } - void NavMesh::update(const dtNavMesh& navMesh, const std::size_t id, - const std::size_t generation, const std::size_t revision, const DetourNavigator::Settings& settings) + void NavMesh::update(const std::shared_ptr& navMesh, std::size_t id, + const DetourNavigator::Settings& settings) { - if (!mEnabled || (mGroup && mId == id && mGeneration == generation && mRevision == revision)) + using DetourNavigator::TilePosition; + using DetourNavigator::Version; + + if (!mEnabled) + return; + + { + std::pair lastest{ 0, Version{} }; + osg::ref_ptr latestCandidate; + for (auto it = mWorkItems.begin(); it != mWorkItems.end();) + { + if (!(*it)->isDone()) + { + ++it; + continue; + } + const std::pair order{ (*it)->mId, (*it)->mVersion }; + if (lastest < order) + { + lastest = order; + std::swap(latestCandidate, *it); + } + if (*it != nullptr) + mWorkQueue->addWorkItem(new DeallocateCreateNavMeshTileGroups(std::move(*it))); + it = mWorkItems.erase(it); + } + + if (latestCandidate != nullptr) + { + for (const TilePosition& position : latestCandidate->mRemovedTiles) + { + const auto it = mTiles.find(position); + if (it == mTiles.end()) + continue; + mRootNode->removeChild(it->second.mGroup); + mTiles.erase(it); + } + + for (auto& [position, tile] : latestCandidate->mUpdatedTiles) + { + const auto it = mTiles.find(position); + if (it == mTiles.end()) + { + mRootNode->addChild(tile.mGroup); + mTiles.emplace_hint(it, position, std::move(tile)); + } + else + { + mRootNode->replaceChild(it->second.mGroup, tile.mGroup); + std::swap(it->second, tile); + } + } + + mWorkQueue->addWorkItem(new DeallocateCreateNavMeshTileGroups(std::move(latestCandidate))); + } + } + + const auto version = navMesh->lock()->getVersion(); + + if (!mTiles.empty() && mId == id && mVersion == version) return; - mId = id; - mGeneration = generation; - mRevision = revision; - if (mGroup) - mRootNode->removeChild(mGroup); - mGroup = SceneUtil::createNavMeshGroup(navMesh, settings); - if (mGroup) + if (mId != id) { - mGroup->setNodeMask(Mask_Debug); - mRootNode->addChild(mGroup); + reset(); + mId = id; } + + mVersion = version; + + for (auto& workItem : mWorkItems) + { + const std::unique_lock lock(workItem->mMutex, std::try_to_lock); + + if (!lock.owns_lock()) + continue; + + if (workItem->mStarted) + continue; + + workItem->mId = id; + workItem->mVersion = version; + workItem->mTiles = mTiles; + workItem->mMode = mMode; + + return; + } + + osg::ref_ptr workItem = new CreateNavMeshTileGroups( + id, version, navMesh, mGroupStateSet, mDebugDrawStateSet, settings, mTiles, mMode); + mWorkQueue->addWorkItem(workItem); + mWorkItems.push_back(std::move(workItem)); } void NavMesh::reset() { - if (mGroup) - { - mRootNode->removeChild(mGroup); - mGroup = nullptr; - } + for (auto& workItem : mWorkItems) + workItem->abort(); + mWorkItems.clear(); + for (auto& [position, tile] : mTiles) + mRootNode->removeChild(tile.mGroup); + mTiles.clear(); } void NavMesh::enable() { - if (mGroup) - mRootNode->addChild(mGroup); mEnabled = true; } void NavMesh::disable() { - if (mGroup) - mRootNode->removeChild(mGroup); + reset(); mEnabled = false; } + + void NavMesh::setMode(Settings::NavMeshRenderMode value) + { + if (mMode == value) + return; + reset(); + mMode = value; + } } diff --git a/apps/openmw/mwrender/navmesh.hpp b/apps/openmw/mwrender/navmesh.hpp index d329b895d78..8ac93e095fb 100644 --- a/apps/openmw/mwrender/navmesh.hpp +++ b/apps/openmw/mwrender/navmesh.hpp @@ -1,14 +1,36 @@ #ifndef OPENMW_MWRENDER_NAVMESH_H #define OPENMW_MWRENDER_NAVMESH_H -#include +#include +#include +#include +#include #include +#include +#include +#include +#include + +class dtNavMesh; + namespace osg { class Group; class Geometry; + class StateSet; +} + +namespace DetourNavigator +{ + class NavMeshCacheItem; + struct Settings; +} + +namespace SceneUtil +{ + class WorkQueue; } namespace MWRender @@ -16,13 +38,14 @@ namespace MWRender class NavMesh { public: - NavMesh(const osg::ref_ptr& root, bool enabled); + explicit NavMesh(const osg::ref_ptr& root, const osg::ref_ptr& workQueue, + bool enabled, Settings::NavMeshRenderMode mode); ~NavMesh(); bool toggle(); - void update(const dtNavMesh& navMesh, const std::size_t number, const std::size_t generation, - const std::size_t revision, const DetourNavigator::Settings& settings); + void update(const std::shared_ptr>& navMesh, + std::size_t id, const DetourNavigator::Settings& settings); void reset(); @@ -30,18 +53,31 @@ namespace MWRender void disable(); - bool isEnabled() const - { - return mEnabled; - } + bool isEnabled() const { return mEnabled; } + + void setMode(Settings::NavMeshRenderMode value); private: + struct Tile + { + DetourNavigator::Version mVersion; + osg::ref_ptr mGroup; + }; + + struct LessByTilePosition; + struct CreateNavMeshTileGroups; + struct DeallocateCreateNavMeshTileGroups; + osg::ref_ptr mRootNode; + osg::ref_ptr mWorkQueue; + osg::ref_ptr mGroupStateSet; + osg::ref_ptr mDebugDrawStateSet; bool mEnabled; - std::size_t mId = std::numeric_limits::max(); - std::size_t mGeneration; - std::size_t mRevision; - osg::ref_ptr mGroup; + Settings::NavMeshRenderMode mMode; + std::size_t mId; + DetourNavigator::Version mVersion; + std::map mTiles; + std::vector> mWorkItems; }; } diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index b538f0b7b62..08dfcb667c6 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -1,11 +1,11 @@ #include "npcanimation.hpp" -#include -#include #include +#include +#include -#include #include +#include #include @@ -13,1321 +13,1282 @@ #include +#include +#include +#include #include #include -#include -#include -#include -#include +#include #include - -#include +#include +#include +#include #include +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" -#include "../mwworld/class.hpp" -#include "../mwworld/player.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/world.hpp" -#include "camera.hpp" -#include "rotatecontroller.hpp" +#include "actorutil.hpp" +#include "postprocessor.hpp" #include "renderbin.hpp" +#include "rotatecontroller.hpp" #include "vismask.hpp" namespace { -std::string getVampireHead(const std::string& race, bool female) -{ - static std::map , const ESM::BodyPart* > sVampireMapping; + std::string getVampireHead(const ESM::RefId& race, bool female) + { + static std::map, const ESM::BodyPart*> sVampireMapping; - std::pair thisCombination = std::make_pair(race, int(female)); + std::pair thisCombination = std::make_pair(race, int(female)); - if (sVampireMapping.find(thisCombination) == sVampireMapping.end()) - { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - for (const ESM::BodyPart& bodypart : store.get()) + if (sVampireMapping.find(thisCombination) == sVampireMapping.end()) { - if (!bodypart.mData.mVampire) - continue; - if (bodypart.mData.mType != ESM::BodyPart::MT_Skin) - continue; - if (bodypart.mData.mPart != ESM::BodyPart::MP_Head) - continue; - if (female != (bodypart.mData.mFlags & ESM::BodyPart::BPF_Female)) - continue; - if (!Misc::StringUtils::ciEqual(bodypart.mRace, race)) - continue; - sVampireMapping[thisCombination] = &bodypart; + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + for (const ESM::BodyPart& bodypart : store.get()) + { + if (!bodypart.mData.mVampire) + continue; + if (bodypart.mData.mType != ESM::BodyPart::MT_Skin) + continue; + if (bodypart.mData.mPart != ESM::BodyPart::MP_Head) + continue; + if (female != (bodypart.mData.mFlags & ESM::BodyPart::BPF_Female)) + continue; + if (!(bodypart.mRace == race)) + continue; + sVampireMapping[thisCombination] = &bodypart; + } } - } - - sVampireMapping.emplace(thisCombination, nullptr); - const ESM::BodyPart* bodyPart = sVampireMapping[thisCombination]; - if (!bodyPart) - return std::string(); - return "meshes\\" + bodyPart->mModel; -} - -std::string getShieldBodypartMesh(const std::vector& bodyparts, bool female) -{ - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const MWWorld::Store &partStore = store.get(); - for (const auto& part : bodyparts) - { - if (part.mPart != ESM::PRT_Shield) - continue; + sVampireMapping.emplace(thisCombination, nullptr); - std::string bodypartName; - if (female && !part.mFemale.empty()) - bodypartName = part.mFemale; - else if (!part.mMale.empty()) - bodypartName = part.mMale; - - if (!bodypartName.empty()) - { - const ESM::BodyPart *bodypart = partStore.search(bodypartName); - if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) - return std::string(); - if (!bodypart->mModel.empty()) - return "meshes\\" + bodypart->mModel; - } + const ESM::BodyPart* bodyPart = sVampireMapping[thisCombination]; + if (!bodyPart) + return std::string(); + return Misc::ResourceHelpers::correctMeshPath(bodyPart->mModel); } - return std::string(); -} - } - namespace MWRender { + class HeadAnimationTime : public SceneUtil::ControllerSource + { + private: + MWWorld::Ptr mReference; + float mTalkStart; + float mTalkStop; + float mBlinkStart; + float mBlinkStop; -class HeadAnimationTime : public SceneUtil::ControllerSource -{ -private: - MWWorld::Ptr mReference; - float mTalkStart; - float mTalkStop; - float mBlinkStart; - float mBlinkStop; + float mBlinkTimer; - float mBlinkTimer; + bool mEnabled; - bool mEnabled; + float mValue; - float mValue; -private: - void resetBlinkTimer(); -public: - HeadAnimationTime(const MWWorld::Ptr& reference); + private: + void resetBlinkTimer(); - void updatePtr(const MWWorld::Ptr& updated); + public: + HeadAnimationTime(const MWWorld::Ptr& reference); - void update(float dt); + void updatePtr(const MWWorld::Ptr& updated); - void setEnabled(bool enabled); + void update(float dt); - void setTalkStart(float value); - void setTalkStop(float value); - void setBlinkStart(float value); - void setBlinkStop(float value); + void setEnabled(bool enabled); - float getValue(osg::NodeVisitor* nv) override; -}; + void setTalkStart(float value); + void setTalkStop(float value); + void setBlinkStart(float value); + void setBlinkStop(float value); -// -------------------------------------------------------------------------------- + float getValue(osg::NodeVisitor* nv) override; + }; -/// Subclass RotateController to add a Z-offset for sneaking in first person mode. -/// @note We use inheritance instead of adding another controller, so that we do not have to compute the worldOrient twice. -/// @note Must be set on a MatrixTransform. -class NeckController : public RotateController -{ -public: - NeckController(osg::Node* relativeTo) - : RotateController(relativeTo) + // -------------------------------------------------------------------------------------------------------------- + + HeadAnimationTime::HeadAnimationTime(const MWWorld::Ptr& reference) + : mReference(reference) + , mTalkStart(0) + , mTalkStop(0) + , mBlinkStart(0) + , mBlinkStop(0) + , mEnabled(true) + , mValue(0) { + resetBlinkTimer(); } - void setOffset(const osg::Vec3f& offset) + void HeadAnimationTime::updatePtr(const MWWorld::Ptr& updated) { - mOffset = offset; + mReference = updated; } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void HeadAnimationTime::setEnabled(bool enabled) { - osg::MatrixTransform* transform = static_cast(node); - osg::Matrix matrix = transform->getMatrix(); - - osg::Quat worldOrient = getWorldOrientation(node); - osg::Quat orient = worldOrient * mRotate * worldOrient.inverse() * matrix.getRotate(); - - matrix.setRotate(orient); - matrix.setTrans(matrix.getTrans() + worldOrient.inverse() * mOffset); - - transform->setMatrix(matrix); - - traverse(node,nv); + mEnabled = enabled; } -private: - osg::Vec3f mOffset; -}; + void HeadAnimationTime::resetBlinkTimer() + { + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + mBlinkTimer = -(2.0f + Misc::Rng::rollDice(6, prng)); + } -// -------------------------------------------------------------------------------------------------------------- + void HeadAnimationTime::update(float dt) + { + if (!mEnabled) + return; -HeadAnimationTime::HeadAnimationTime(const MWWorld::Ptr& reference) - : mReference(reference), mTalkStart(0), mTalkStop(0), mBlinkStart(0), mBlinkStop(0), mEnabled(true), mValue(0) -{ - resetBlinkTimer(); -} + if (dt == 0.f) + return; -void HeadAnimationTime::updatePtr(const MWWorld::Ptr &updated) -{ - mReference = updated; -} + if (!MWBase::Environment::get().getSoundManager()->sayActive(mReference)) + { + mBlinkTimer += dt; -void HeadAnimationTime::setEnabled(bool enabled) -{ - mEnabled = enabled; -} + float duration = mBlinkStop - mBlinkStart; -void HeadAnimationTime::resetBlinkTimer() -{ - mBlinkTimer = -(2.0f + Misc::Rng::rollDice(6)); -} + if (mBlinkTimer >= 0 && mBlinkTimer <= duration) + { + mValue = mBlinkStart + mBlinkTimer; + } + else + mValue = mBlinkStop; -void HeadAnimationTime::update(float dt) -{ - if (!mEnabled) - return; + if (mBlinkTimer > duration) + resetBlinkTimer(); + } + else + { + // FIXME: would be nice to hold on to the SoundPtr so we don't have to retrieve it every frame + mValue = mTalkStart + + (mTalkStop - mTalkStart) + * std::min(1.f, + MWBase::Environment::get().getSoundManager()->getSaySoundLoudness(mReference) + * 2); // Rescale a bit (most voices are not very loud) + } + } - if (!MWBase::Environment::get().getSoundManager()->sayActive(mReference)) + float HeadAnimationTime::getValue(osg::NodeVisitor*) { - mBlinkTimer += dt; + return mValue; + } - float duration = mBlinkStop - mBlinkStart; + void HeadAnimationTime::setTalkStart(float value) + { + mTalkStart = value; + } - if (mBlinkTimer >= 0 && mBlinkTimer <= duration) - { - mValue = mBlinkStart + mBlinkTimer; - } - else - mValue = mBlinkStop; + void HeadAnimationTime::setTalkStop(float value) + { + mTalkStop = value; + } - if (mBlinkTimer > duration) - resetBlinkTimer(); + void HeadAnimationTime::setBlinkStart(float value) + { + mBlinkStart = value; } - else + + void HeadAnimationTime::setBlinkStop(float value) { - // FIXME: would be nice to hold on to the SoundPtr so we don't have to retrieve it every frame - mValue = mTalkStart + - (mTalkStop - mTalkStart) * - std::min(1.f, MWBase::Environment::get().getSoundManager()->getSaySoundLoudness(mReference)*2); // Rescale a bit (most voices are not very loud) + mBlinkStop = value; } -} -float HeadAnimationTime::getValue(osg::NodeVisitor*) -{ - return mValue; -} + // ---------------------------------------------------- -void HeadAnimationTime::setTalkStart(float value) -{ - mTalkStart = value; -} + NpcAnimation::NpcType NpcAnimation::getNpcType() const + { + const MWWorld::Class& cls = mPtr.getClass(); + // Dead vampires should typically stay vampires. + if (mNpcType == Type_Vampire && cls.getNpcStats(mPtr).isDead() && !cls.getNpcStats(mPtr).isWerewolf()) + return mNpcType; + return getNpcType(mPtr); + } -void HeadAnimationTime::setTalkStop(float value) -{ - mTalkStop = value; -} + NpcAnimation::NpcType NpcAnimation::getNpcType(const MWWorld::Ptr& ptr) + { + const MWWorld::Class& cls = ptr.getClass(); + NpcAnimation::NpcType curType = Type_Normal; + if (cls.getCreatureStats(ptr).getMagicEffects().getOrDefault(ESM::MagicEffect::Vampirism).getMagnitude() > 0) + curType = Type_Vampire; + if (cls.getNpcStats(ptr).isWerewolf()) + curType = Type_Werewolf; + + return curType; + } -void HeadAnimationTime::setBlinkStart(float value) -{ - mBlinkStart = value; -} + static const inline NpcAnimation::PartBoneMap createPartListMap() + { + return { { ESM::PRT_Head, "Head" }, + { ESM::PRT_Hair, "Head" }, // note it uses "Head" as attach bone, but "Hair" as filter + { ESM::PRT_Neck, "Neck" }, { ESM::PRT_Cuirass, "Chest" }, { ESM::PRT_Groin, "Groin" }, + { ESM::PRT_Skirt, "Groin" }, { ESM::PRT_RHand, "Right Hand" }, { ESM::PRT_LHand, "Left Hand" }, + { ESM::PRT_RWrist, "Right Wrist" }, { ESM::PRT_LWrist, "Left Wrist" }, { ESM::PRT_Shield, "Shield Bone" }, + { ESM::PRT_RForearm, "Right Forearm" }, { ESM::PRT_LForearm, "Left Forearm" }, + { ESM::PRT_RUpperarm, "Right Upper Arm" }, { ESM::PRT_LUpperarm, "Left Upper Arm" }, + { ESM::PRT_RFoot, "Right Foot" }, { ESM::PRT_LFoot, "Left Foot" }, { ESM::PRT_RAnkle, "Right Ankle" }, + { ESM::PRT_LAnkle, "Left Ankle" }, { ESM::PRT_RKnee, "Right Knee" }, { ESM::PRT_LKnee, "Left Knee" }, + { ESM::PRT_RLeg, "Right Upper Leg" }, { ESM::PRT_LLeg, "Left Upper Leg" }, + { ESM::PRT_RPauldron, "Right Clavicle" }, { ESM::PRT_LPauldron, "Left Clavicle" }, + { ESM::PRT_Weapon, "Weapon Bone" }, // Fallback. The real node name depends on the current weapon type. + { ESM::PRT_Tail, "Tail" } }; + } + const NpcAnimation::PartBoneMap NpcAnimation::sPartList = createPartListMap(); -void HeadAnimationTime::setBlinkStop(float value) -{ - mBlinkStop = value; -} + NpcAnimation::~NpcAnimation() + { + mAmmunition.reset(); + } -// ---------------------------------------------------- + NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, + Resource::ResourceSystem* resourceSystem, bool disableSounds, ViewMode viewMode, float firstPersonFieldOfView) + : ActorAnimation(ptr, std::move(parentNode), resourceSystem) + , mViewMode(viewMode) + , mShowWeapons(false) + , mShowCarriedLeft(true) + , mNpcType(getNpcType(ptr)) + , mFirstPersonFieldOfView(firstPersonFieldOfView) + , mSoundsDisabled(disableSounds) + , mAccurateAiming(false) + , mAimingFactor(0.f) + { + mNpc = mPtr.get()->mBase; -NpcAnimation::NpcType NpcAnimation::getNpcType() const -{ - const MWWorld::Class &cls = mPtr.getClass(); - // Dead vampires should typically stay vampires. - if (mNpcType == Type_Vampire && cls.getNpcStats(mPtr).isDead() && !cls.getNpcStats(mPtr).isWerewolf()) - return mNpcType; - return getNpcType(mPtr); -} + mHeadAnimationTime = std::make_shared(mPtr); + mWeaponAnimationTime = std::make_shared(this); -NpcAnimation::NpcType NpcAnimation::getNpcType(const MWWorld::Ptr& ptr) -{ - const MWWorld::Class &cls = ptr.getClass(); - NpcAnimation::NpcType curType = Type_Normal; - if (cls.getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0) - curType = Type_Vampire; - if (cls.getNpcStats(ptr).isWerewolf()) - curType = Type_Werewolf; - - return curType; -} + for (size_t i = 0; i < ESM::PRT_Count; i++) + { + mPartslots[i] = -1; // each slot is empty + mPartPriorities[i] = 0; + } -static NpcAnimation::PartBoneMap createPartListMap() -{ - NpcAnimation::PartBoneMap result; - result.insert(std::make_pair(ESM::PRT_Head, "Head")); - result.insert(std::make_pair(ESM::PRT_Hair, "Head")); // note it uses "Head" as attach bone, but "Hair" as filter - result.insert(std::make_pair(ESM::PRT_Neck, "Neck")); - result.insert(std::make_pair(ESM::PRT_Cuirass, "Chest")); - result.insert(std::make_pair(ESM::PRT_Groin, "Groin")); - result.insert(std::make_pair(ESM::PRT_Skirt, "Groin")); - result.insert(std::make_pair(ESM::PRT_RHand, "Right Hand")); - result.insert(std::make_pair(ESM::PRT_LHand, "Left Hand")); - result.insert(std::make_pair(ESM::PRT_RWrist, "Right Wrist")); - result.insert(std::make_pair(ESM::PRT_LWrist, "Left Wrist")); - result.insert(std::make_pair(ESM::PRT_Shield, "Shield Bone")); - result.insert(std::make_pair(ESM::PRT_RForearm, "Right Forearm")); - result.insert(std::make_pair(ESM::PRT_LForearm, "Left Forearm")); - result.insert(std::make_pair(ESM::PRT_RUpperarm, "Right Upper Arm")); - result.insert(std::make_pair(ESM::PRT_LUpperarm, "Left Upper Arm")); - result.insert(std::make_pair(ESM::PRT_RFoot, "Right Foot")); - result.insert(std::make_pair(ESM::PRT_LFoot, "Left Foot")); - result.insert(std::make_pair(ESM::PRT_RAnkle, "Right Ankle")); - result.insert(std::make_pair(ESM::PRT_LAnkle, "Left Ankle")); - result.insert(std::make_pair(ESM::PRT_RKnee, "Right Knee")); - result.insert(std::make_pair(ESM::PRT_LKnee, "Left Knee")); - result.insert(std::make_pair(ESM::PRT_RLeg, "Right Upper Leg")); - result.insert(std::make_pair(ESM::PRT_LLeg, "Left Upper Leg")); - result.insert(std::make_pair(ESM::PRT_RPauldron, "Right Clavicle")); - result.insert(std::make_pair(ESM::PRT_LPauldron, "Left Clavicle")); - result.insert(std::make_pair(ESM::PRT_Weapon, "Weapon Bone")); // Fallback. The real node name depends on the current weapon type. - result.insert(std::make_pair(ESM::PRT_Tail, "Tail")); - return result; -} -const NpcAnimation::PartBoneMap NpcAnimation::sPartList = createPartListMap(); + std::fill(mSounds.begin(), mSounds.end(), nullptr); -NpcAnimation::~NpcAnimation() -{ - mAmmunition.reset(); -} + updateNpcBase(); + } -NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem, - bool disableSounds, ViewMode viewMode, float firstPersonFieldOfView) - : ActorAnimation(ptr, parentNode, resourceSystem), - mViewMode(viewMode), - mShowWeapons(false), - mShowCarriedLeft(true), - mNpcType(getNpcType(ptr)), - mFirstPersonFieldOfView(firstPersonFieldOfView), - mSoundsDisabled(disableSounds), - mAccurateAiming(false), - mAimingFactor(0.f) -{ - mNpc = mPtr.get()->mBase; + void NpcAnimation::setViewMode(NpcAnimation::ViewMode viewMode) + { + assert(viewMode != VM_HeadOnly); + if (mViewMode == viewMode) + return; + // FIXME: sheathing state must be consistent if the third person skeleton doesn't have the necessary node, but + // third person skeleton is unavailable in first person view. This is a hack to avoid cosmetic issues. + bool viewChange = mViewMode == VM_FirstPerson || viewMode == VM_FirstPerson; + mViewMode = viewMode; + MWBase::Environment::get().getWorld()->scaleObject( + mPtr, mPtr.getCellRef().getScale(), true); // apply race height after view change + + mAmmunition.reset(); + rebuild(); + setRenderBin(); - mHeadAnimationTime = std::shared_ptr(new HeadAnimationTime(mPtr)); - mWeaponAnimationTime = std::shared_ptr(new WeaponAnimationTime(this)); + if (viewChange && Settings::game().mShieldSheathing) + { + int weaptype = ESM::Weapon::None; + MWMechanics::getActiveWeapon(mPtr, &weaptype); + showCarriedLeft(updateCarriedLeftVisible(weaptype)); + } + } - for(size_t i = 0;i < ESM::PRT_Count;i++) + /// @brief A RenderBin callback to clear the depth buffer before rendering. + /// Switches depth attachments to a proxy renderbuffer, reattaches original depth then redraws first person root. + /// This gives a complete depth buffer which can be used for postprocessing, buffer resolves as if depth was never + /// cleared. + class DepthClearCallback : public osgUtil::RenderBin::DrawCallback { - mPartslots[i] = -1; //each slot is empty - mPartPriorities[i] = 0; - } + public: + DepthClearCallback() + { + mDepth = new SceneUtil::AutoDepth; + mDepth->setWriteMask(true); - std::fill(mSounds.begin(), mSounds.end(), nullptr); + mStateSet = new osg::StateSet; + mStateSet->setAttributeAndModes(new osg::ColorMask(false, false, false, false), osg::StateAttribute::ON); + mStateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); + } - updateNpcBase(); -} + void drawImplementation( + osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override + { + osg::State* state = renderInfo.getState(); -void NpcAnimation::setViewMode(NpcAnimation::ViewMode viewMode) -{ - assert(viewMode != VM_HeadOnly); - if(mViewMode == viewMode) - return; + PostProcessor* postProcessor = static_cast(renderInfo.getCurrentCamera()->getUserData()); - mViewMode = viewMode; - MWBase::Environment::get().getWorld()->scaleObject(mPtr, mPtr.getCellRef().getScale()); // apply race height after view change + state->applyAttribute(mDepth); - mAmmunition.reset(); - rebuild(); - setRenderBin(); -} + unsigned int frameId = state->getFrameStamp()->getFrameNumber() % 2; -/// @brief A RenderBin callback to clear the depth buffer before rendering. -class DepthClearCallback : public osgUtil::RenderBin::DrawCallback -{ -public: - DepthClearCallback() - { - mDepth = new osg::Depth; - mDepth->setWriteMask(true); - } + postProcessor->getFbo(PostProcessor::FBO_FirstPerson, frameId)->apply(*state); + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + // color accumulation pass + bin->drawImplementation(renderInfo, previous); - void drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override - { - renderInfo.getState()->applyAttribute(mDepth); + auto primaryFBO = postProcessor->getPrimaryFbo(frameId); + primaryFBO->apply(*state); - glClear(GL_DEPTH_BUFFER_BIT); + postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)->apply(*state); - bin->drawImplementation(renderInfo, previous); - } + // depth accumulation pass + osg::ref_ptr restore = bin->getStateSet(); + bin->setStateSet(mStateSet); + bin->drawImplementation(renderInfo, previous); + bin->setStateSet(restore); - osg::ref_ptr mDepth; -}; + primaryFBO->apply(*state); -/// Overrides Field of View to given value for rendering the subgraph. -/// Must be added as cull callback. -class OverrideFieldOfViewCallback : public osg::NodeCallback -{ -public: - OverrideFieldOfViewCallback(float fov) - : mFov(fov) - { - } + state->checkGLErrors("after DepthClearCallback::drawImplementation"); + } + + osg::ref_ptr mDepth; + osg::ref_ptr mStateSet; + }; - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + /// Overrides Field of View to given value for rendering the subgraph. + /// Must be added as cull callback. + class OverrideFieldOfViewCallback : public osg::NodeCallback { - osgUtil::CullVisitor* cv = static_cast(nv); - float fov, aspect, zNear, zFar; - if (cv->getProjectionMatrix()->getPerspective(fov, aspect, zNear, zFar)) + public: + OverrideFieldOfViewCallback(float fov) + : mFov(fov) { - fov = mFov; - osg::ref_ptr newProjectionMatrix = new osg::RefMatrix(); - newProjectionMatrix->makePerspective(fov, aspect, zNear, zFar); - osg::ref_ptr invertedOldMatrix = cv->getProjectionMatrix(); - invertedOldMatrix = new osg::RefMatrix(osg::RefMatrix::inverse(*invertedOldMatrix)); - osg::ref_ptr viewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); - viewMatrix->postMult(*newProjectionMatrix); - viewMatrix->postMult(*invertedOldMatrix); - cv->pushModelViewMatrix(viewMatrix, osg::Transform::ReferenceFrame::ABSOLUTE_RF); - traverse(node, nv); - cv->popModelViewMatrix(); } - else - traverse(node, nv); - } -private: - float mFov; -}; + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + osgUtil::CullVisitor* cv = static_cast(nv); + float fov, aspect, zNear, zFar; + if (cv->getProjectionMatrix()->getPerspective(fov, aspect, zNear, zFar) && std::abs(fov - mFov) > 0.001) + { + fov = mFov; + osg::ref_ptr newProjectionMatrix = new osg::RefMatrix(); + newProjectionMatrix->makePerspective(fov, aspect, zNear, zFar); + osg::ref_ptr invertedOldMatrix = cv->getProjectionMatrix(); + invertedOldMatrix = new osg::RefMatrix(osg::RefMatrix::inverse(*invertedOldMatrix)); + osg::ref_ptr viewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); + viewMatrix->postMult(*newProjectionMatrix); + viewMatrix->postMult(*invertedOldMatrix); + cv->pushModelViewMatrix(viewMatrix, osg::Transform::ReferenceFrame::ABSOLUTE_RF); + traverse(node, nv); + cv->popModelViewMatrix(); + } + else + traverse(node, nv); + } + + private: + float mFov; + }; -void NpcAnimation::setRenderBin() -{ - if (mViewMode == VM_FirstPerson) + void NpcAnimation::setRenderBin() { - static bool prototypeAdded = false; - if (!prototypeAdded) + if (mViewMode == VM_FirstPerson) { - osg::ref_ptr depthClearBin (new osgUtil::RenderBin); - depthClearBin->setDrawCallback(new DepthClearCallback); - osgUtil::RenderBin::addRenderBinPrototype("DepthClear", depthClearBin); - prototypeAdded = true; + static bool prototypeAdded = false; + if (!prototypeAdded) + { + osg::ref_ptr depthClearBin(new osgUtil::RenderBin); + depthClearBin->setDrawCallback(new DepthClearCallback()); + osgUtil::RenderBin::addRenderBinPrototype("DepthClear", depthClearBin); + prototypeAdded = true; + } + mObjectRoot->getOrCreateStateSet()->setRenderBinDetails( + RenderBin_FirstPerson, "DepthClear", osg::StateSet::OVERRIDE_RENDERBIN_DETAILS); } - mObjectRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_FirstPerson, "DepthClear", osg::StateSet::OVERRIDE_RENDERBIN_DETAILS); + else if (osg::StateSet* stateset = mObjectRoot->getStateSet()) + stateset->setRenderBinToInherit(); } - else if (osg::StateSet* stateset = mObjectRoot->getStateSet()) - stateset->setRenderBinToInherit(); -} -void NpcAnimation::rebuild() -{ - mScabbard.reset(); - mHolsteredShield.reset(); - updateNpcBase(); + void NpcAnimation::rebuild() + { + mScabbard.reset(); + mHolsteredShield.reset(); + updateNpcBase(); - MWBase::Environment::get().getMechanicsManager()->forceStateUpdate(mPtr); -} + MWBase::Environment::get().getMechanicsManager()->forceStateUpdate(mPtr); + } -int NpcAnimation::getSlot(const osg::NodePath &path) const -{ - for (int i=0; igetNode().get()) != path.end()) + for (int i = 0; i < ESM::PRT_Count; ++i) { - return mPartslots[i]; + const PartHolder* const part = mObjectParts[i].get(); + if (part == nullptr) + continue; + if (std::find(path.begin(), path.end(), part->getNode().get()) != path.end()) + { + return mPartslots[i]; + } } + return -1; } - return -1; -} -void NpcAnimation::updateNpcBase() -{ - clearAnimSources(); - for(size_t i = 0;i < ESM::PRT_Count;i++) - removeIndividualPart((ESM::PartReferenceType)i); + void NpcAnimation::updateNpcBase() + { + clearAnimSources(); + for (size_t i = 0; i < ESM::PRT_Count; i++) + removeIndividualPart((ESM::PartReferenceType)i); - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Race *race = store.get().find(mNpc->mRace); - NpcType curType = getNpcType(); - bool isWerewolf = (curType == Type_Werewolf); - bool isVampire = (curType == Type_Vampire); - bool isFemale = !mNpc->isMale(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const ESM::Race* race = store.get().find(mNpc->mRace); + NpcType curType = getNpcType(); + bool isWerewolf = (curType == Type_Werewolf); + bool isVampire = (curType == Type_Vampire); + bool isFemale = !mNpc->isMale(); - mHeadModel.clear(); - mHairModel.clear(); + mHeadModel.clear(); + mHairModel.clear(); - std::string headName = isWerewolf ? "WerewolfHead" : mNpc->mHead; - std::string hairName = isWerewolf ? "WerewolfHair" : mNpc->mHair; + const ESM::RefId& headName = isWerewolf ? ESM::RefId::stringRefId("WerewolfHead") : mNpc->mHead; + const ESM::RefId& hairName = isWerewolf ? ESM::RefId::stringRefId("WerewolfHair") : mNpc->mHair; - if (!headName.empty()) - { - const ESM::BodyPart* bp = store.get().search(headName); - if (bp) - mHeadModel = "meshes\\" + bp->mModel; - else - Log(Debug::Warning) << "Warning: Failed to load body part '" << headName << "'"; - } + if (!headName.empty()) + { + const ESM::BodyPart* bp = store.get().search(headName); + if (bp) + mHeadModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel); + else + Log(Debug::Warning) << "Warning: Failed to load body part '" << headName << "'"; + } - if (!hairName.empty()) - { - const ESM::BodyPart* bp = store.get().search(hairName); - if (bp) - mHairModel = "meshes\\" + bp->mModel; - else - Log(Debug::Warning) << "Warning: Failed to load body part '" << hairName << "'"; - } + if (!hairName.empty()) + { + const ESM::BodyPart* bp = store.get().search(hairName); + if (bp) + mHairModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel); + else + Log(Debug::Warning) << "Warning: Failed to load body part '" << hairName << "'"; + } - const std::string& vampireHead = getVampireHead(mNpc->mRace, isFemale); - if (!isWerewolf && isVampire && !vampireHead.empty()) - mHeadModel = vampireHead; + const std::string vampireHead = getVampireHead(mNpc->mRace, isFemale); + if (!isWerewolf && isVampire && !vampireHead.empty()) + mHeadModel = vampireHead; - bool is1stPerson = mViewMode == VM_FirstPerson; - bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; + bool is1stPerson = mViewMode == VM_FirstPerson; + bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; - std::string defaultSkeleton = SceneUtil::getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf); - defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath(defaultSkeleton, mResourceSystem->getVFS()); + const std::string defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath( + getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf), mResourceSystem->getVFS()); - std::string smodel = defaultSkeleton; - if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty()) - smodel = Misc::ResourceHelpers::correctActorModelPath("meshes\\" + mNpc->mModel, mResourceSystem->getVFS()); + std::string smodel = defaultSkeleton; + bool isBase = !isWerewolf; + if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty()) + { + std::string model = Misc::ResourceHelpers::correctMeshPath(mNpc->mModel); + isBase = isDefaultActorSkeleton(model); + smodel = Misc::ResourceHelpers::correctActorModelPath(model, mResourceSystem->getVFS()); + } - setObjectRoot(smodel, true, true, false); + setObjectRoot(smodel, true, true, false); - updateParts(); + updateParts(); - if(!is1stPerson) - { - const std::string base = Settings::Manager::getString("xbaseanim", "Models"); - if (smodel != base && !isWerewolf) - addAnimSource(base, smodel); + if (!is1stPerson) + { + const std::string& base = Settings::models().mXbaseanim.get().value(); + if (!isWerewolf) + addAnimSource(base, smodel); - if (smodel != defaultSkeleton && base != defaultSkeleton) - addAnimSource(defaultSkeleton, smodel); + if (!isBase) + { + addAnimSource(defaultSkeleton, smodel); + addAnimSource(smodel, smodel); + } + else if (base != defaultSkeleton) + { + addAnimSource(defaultSkeleton, smodel); + } - addAnimSource(smodel, smodel); + if (!isWerewolf && isBeast && mNpc->mRace.contains("argonian")) + addAnimSource("meshes\\xargonian_swimkna.nif", smodel); + } + else + { + if (!isWerewolf) + addAnimSource(Settings::models().mXbaseanim1st.get().value(), smodel); - if(!isWerewolf && Misc::StringUtils::lowerCase(mNpc->mRace).find("argonian") != std::string::npos) - addAnimSource("meshes\\xargonian_swimkna.nif", smodel); - } - else - { - const std::string base = Settings::Manager::getString("xbaseanim1st", "Models"); - if (smodel != base && !isWerewolf) - addAnimSource(base, smodel); + if (!isBase) + addAnimSource(smodel, smodel); - addAnimSource(smodel, smodel); + mObjectRoot->setNodeMask(Mask_FirstPerson); + mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView)); + } - mObjectRoot->setNodeMask(Mask_FirstPerson); - mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView)); + mWeaponAnimationTime->updateStartTime(); } - mWeaponAnimationTime->updateStartTime(); -} - -std::string NpcAnimation::getShieldMesh(MWWorld::ConstPtr shield) const -{ - std::string mesh = shield.getClass().getModel(shield); - const ESM::Armor *armor = shield.get()->mBase; - const std::vector& bodyparts = armor->mParts.mParts; - // Try to recover the body part model, use ground model as a fallback otherwise. - if (!bodyparts.empty()) - mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); - - if (mesh.empty()) - return std::string(); - - std::string holsteredName = mesh; - holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); - if(mResourceSystem->getVFS()->exists(holsteredName)) + std::string NpcAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const { - osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); - SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); - shieldTemplate->accept(findVisitor); - osg::ref_ptr sheathNode = findVisitor.mFoundNode; - if(!sheathNode) + std::string mesh = getShieldMesh(shield, !mNpc->isMale()); + + if (mesh.empty()) return std::string(); - } - return mesh; -} + const VFS::Path::Normalized holsteredName(addSuffixBeforeExtension(mesh, "_sh")); + if (mResourceSystem->getVFS()->exists(holsteredName)) + { + osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); + SceneUtil::FindByNameVisitor findVisitor("Bip01 Sheath"); + shieldTemplate->accept(findVisitor); + osg::ref_ptr sheathNode = findVisitor.mFoundNode; + if (!sheathNode) + return std::string(); + } -void NpcAnimation::updateParts() -{ - if (!mObjectRoot.get()) - return; + return mesh; + } - NpcType curType = getNpcType(); - if (curType != mNpcType) + void NpcAnimation::updateParts() { - mNpcType = curType; - rebuild(); - return; - } + if (!mObjectRoot.get()) + return; - static const struct { - int mSlot; - int mBasePriority; - } slotlist[] = { - // FIXME: Priority is based on the number of reserved slots. There should be a better way. - { MWWorld::InventoryStore::Slot_Robe, 11 }, - { MWWorld::InventoryStore::Slot_Skirt, 3 }, - { MWWorld::InventoryStore::Slot_Helmet, 0 }, - { MWWorld::InventoryStore::Slot_Cuirass, 0 }, - { MWWorld::InventoryStore::Slot_Greaves, 0 }, - { MWWorld::InventoryStore::Slot_LeftPauldron, 0 }, - { MWWorld::InventoryStore::Slot_RightPauldron, 0 }, - { MWWorld::InventoryStore::Slot_Boots, 0 }, - { MWWorld::InventoryStore::Slot_LeftGauntlet, 0 }, - { MWWorld::InventoryStore::Slot_RightGauntlet, 0 }, - { MWWorld::InventoryStore::Slot_Shirt, 0 }, - { MWWorld::InventoryStore::Slot_Pants, 0 }, - { MWWorld::InventoryStore::Slot_CarriedLeft, 0 }, - { MWWorld::InventoryStore::Slot_CarriedRight, 0 } - }; - static const size_t slotlistsize = sizeof(slotlist)/sizeof(slotlist[0]); + NpcType curType = getNpcType(); + if (curType != mNpcType) + { + mNpcType = curType; + rebuild(); + return; + } - bool wasArrowAttached = isArrowAttached(); - mAmmunition.reset(); + static const struct + { + int mSlot; + int mBasePriority; + } slotlist[] = { // FIXME: Priority is based on the number of reserved slots. There should be a better way. + { MWWorld::InventoryStore::Slot_Robe, 11 }, { MWWorld::InventoryStore::Slot_Skirt, 3 }, + { MWWorld::InventoryStore::Slot_Helmet, 0 }, { MWWorld::InventoryStore::Slot_Cuirass, 0 }, + { MWWorld::InventoryStore::Slot_Greaves, 0 }, { MWWorld::InventoryStore::Slot_LeftPauldron, 0 }, + { MWWorld::InventoryStore::Slot_RightPauldron, 0 }, { MWWorld::InventoryStore::Slot_Boots, 0 }, + { MWWorld::InventoryStore::Slot_LeftGauntlet, 0 }, { MWWorld::InventoryStore::Slot_RightGauntlet, 0 }, + { MWWorld::InventoryStore::Slot_Shirt, 0 }, { MWWorld::InventoryStore::Slot_Pants, 0 }, + { MWWorld::InventoryStore::Slot_CarriedLeft, 0 }, { MWWorld::InventoryStore::Slot_CarriedRight, 0 } + }; + static const size_t slotlistsize = sizeof(slotlist) / sizeof(slotlist[0]); - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - for(size_t i = 0;i < slotlistsize && mViewMode != VM_HeadOnly;i++) - { - MWWorld::ConstContainerStoreIterator store = inv.getSlot(slotlist[i].mSlot); + bool wasArrowAttached = isArrowAttached(); + mAmmunition.reset(); - removePartGroup(slotlist[i].mSlot); + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + for (size_t i = 0; i < slotlistsize && mViewMode != VM_HeadOnly; i++) + { + MWWorld::ConstContainerStoreIterator store = inv.getSlot(slotlist[i].mSlot); - if(store == inv.end()) - continue; + removePartGroup(slotlist[i].mSlot); - if(slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Helmet) - removeIndividualPart(ESM::PRT_Hair); + if (store == inv.end()) + continue; - int prio = 1; - bool enchantedGlow = !store->getClass().getEnchantment(*store).empty(); - osg::Vec4f glowColor = store->getClass().getEnchantmentColor(*store); - if(store->getTypeName() == typeid(ESM::Clothing).name()) - { - prio = ((slotlist[i].mBasePriority+1)<<1) + 0; - const ESM::Clothing *clothes = store->get()->mBase; - addPartGroup(slotlist[i].mSlot, prio, clothes->mParts.mParts, enchantedGlow, &glowColor); - } - else if(store->getTypeName() == typeid(ESM::Armor).name()) - { - prio = ((slotlist[i].mBasePriority+1)<<1) + 1; - const ESM::Armor *armor = store->get()->mBase; - addPartGroup(slotlist[i].mSlot, prio, armor->mParts.mParts, enchantedGlow, &glowColor); - } + if (slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Helmet) + removeIndividualPart(ESM::PRT_Hair); - if(slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Robe) - { - ESM::PartReferenceType parts[] = { - ESM::PRT_Groin, ESM::PRT_Skirt, ESM::PRT_RLeg, ESM::PRT_LLeg, - ESM::PRT_RUpperarm, ESM::PRT_LUpperarm, ESM::PRT_RKnee, ESM::PRT_LKnee, - ESM::PRT_RForearm, ESM::PRT_LForearm, ESM::PRT_Cuirass - }; - size_t parts_size = sizeof(parts)/sizeof(parts[0]); - for(size_t p = 0;p < parts_size;++p) - reserveIndividualPart(parts[p], slotlist[i].mSlot, prio); + int prio = 1; + bool enchantedGlow = !store->getClass().getEnchantment(*store).empty(); + osg::Vec4f glowColor = store->getClass().getEnchantmentColor(*store); + if (store->getType() == ESM::Clothing::sRecordId) + { + prio = ((slotlist[i].mBasePriority + 1) << 1) + 0; + const ESM::Clothing* clothes = store->get()->mBase; + addPartGroup(slotlist[i].mSlot, prio, clothes->mParts.mParts, enchantedGlow, &glowColor); + } + else if (store->getType() == ESM::Armor::sRecordId) + { + prio = ((slotlist[i].mBasePriority + 1) << 1) + 1; + const ESM::Armor* armor = store->get()->mBase; + addPartGroup(slotlist[i].mSlot, prio, armor->mParts.mParts, enchantedGlow, &glowColor); + } + + if (slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Robe) + { + ESM::PartReferenceType parts[] = { ESM::PRT_Groin, ESM::PRT_Skirt, ESM::PRT_RLeg, ESM::PRT_LLeg, + ESM::PRT_RUpperarm, ESM::PRT_LUpperarm, ESM::PRT_RKnee, ESM::PRT_LKnee, ESM::PRT_RForearm, + ESM::PRT_LForearm, ESM::PRT_Cuirass }; + size_t parts_size = sizeof(parts) / sizeof(parts[0]); + for (size_t p = 0; p < parts_size; ++p) + reserveIndividualPart(parts[p], slotlist[i].mSlot, prio); + } + else if (slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Skirt) + { + reserveIndividualPart(ESM::PRT_Groin, slotlist[i].mSlot, prio); + reserveIndividualPart(ESM::PRT_RLeg, slotlist[i].mSlot, prio); + reserveIndividualPart(ESM::PRT_LLeg, slotlist[i].mSlot, prio); + } } - else if(slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Skirt) + + if (mViewMode != VM_FirstPerson) { - reserveIndividualPart(ESM::PRT_Groin, slotlist[i].mSlot, prio); - reserveIndividualPart(ESM::PRT_RLeg, slotlist[i].mSlot, prio); - reserveIndividualPart(ESM::PRT_LLeg, slotlist[i].mSlot, prio); + if (mPartPriorities[ESM::PRT_Head] < 1 && !mHeadModel.empty()) + addOrReplaceIndividualPart(ESM::PRT_Head, -1, 1, mHeadModel); + if (mPartPriorities[ESM::PRT_Hair] < 1 && mPartPriorities[ESM::PRT_Head] <= 1 && !mHairModel.empty()) + addOrReplaceIndividualPart(ESM::PRT_Hair, -1, 1, mHairModel); } - } - - if(mViewMode != VM_FirstPerson) - { - if(mPartPriorities[ESM::PRT_Head] < 1 && !mHeadModel.empty()) - addOrReplaceIndividualPart(ESM::PRT_Head, -1,1, mHeadModel); - if(mPartPriorities[ESM::PRT_Hair] < 1 && mPartPriorities[ESM::PRT_Head] <= 1 && !mHairModel.empty()) - addOrReplaceIndividualPart(ESM::PRT_Hair, -1,1, mHairModel); - } - if(mViewMode == VM_HeadOnly) - return; + if (mViewMode == VM_HeadOnly) + return; - if(mPartPriorities[ESM::PRT_Shield] < 1) - { - MWWorld::ConstContainerStoreIterator store = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - MWWorld::ConstPtr part; - if(store != inv.end() && (part=*store).getTypeName() == typeid(ESM::Light).name()) + if (mPartPriorities[ESM::PRT_Shield] < 1) { - const ESM::Light *light = part.get()->mBase; - addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, - 1, "meshes\\"+light->mModel); - if (mObjectParts[ESM::PRT_Shield]) - addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), light); + MWWorld::ConstContainerStoreIterator store = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + MWWorld::ConstPtr part; + if (store != inv.end() && (part = *store).getType() == ESM::Light::sRecordId) + { + const ESM::Light* light = part.get()->mBase; + addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, + Misc::ResourceHelpers::correctMeshPath(light->mModel), false, nullptr, true); + if (mObjectParts[ESM::PRT_Shield]) + addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), SceneUtil::LightCommon(*light)); + } } - } - showWeapons(mShowWeapons); - showCarriedLeft(mShowCarriedLeft); + showWeapons(mShowWeapons); + showCarriedLeft(mShowCarriedLeft); - bool isWerewolf = (getNpcType() == Type_Werewolf); - std::string race = (isWerewolf ? "werewolf" : Misc::StringUtils::lowerCase(mNpc->mRace)); + bool isWerewolf = (getNpcType() == Type_Werewolf); + ESM::RefId race = (isWerewolf ? ESM::RefId::stringRefId("werewolf") : mNpc->mRace); - const std::vector &parts = getBodyParts(race, !mNpc->isMale(), mViewMode == VM_FirstPerson, isWerewolf); - for(int part = ESM::PRT_Neck; part < ESM::PRT_Count; ++part) - { - if(mPartPriorities[part] < 1) + const std::vector& parts + = getBodyParts(race, !mNpc->isMale(), mViewMode == VM_FirstPerson, isWerewolf); + for (int part = ESM::PRT_Neck; part < ESM::PRT_Count; ++part) { - const ESM::BodyPart* bodypart = parts[part]; - if(bodypart) - addOrReplaceIndividualPart((ESM::PartReferenceType)part, -1, 1, - "meshes\\"+bodypart->mModel); + if (mPartPriorities[part] < 1) + { + if (const ESM::BodyPart* bodypart = parts[part]) + addOrReplaceIndividualPart(static_cast(part), -1, 1, + Misc::ResourceHelpers::correctMeshPath(bodypart->mModel)); + } } + + if (wasArrowAttached) + attachArrow(); } - if (wasArrowAttached) - attachArrow(); -} + PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, std::string_view bonename, + std::string_view bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) + { + osg::ref_ptr attached = attach(model, bonename, bonefilter, isLight); + if (enchantedGlow) + mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); + return std::make_unique(attached); + } + osg::Vec3f NpcAnimation::runAnimation(float timepassed) + { + osg::Vec3f ret = Animation::runAnimation(timepassed); -PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor) -{ - osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model); + mHeadAnimationTime->update(timepassed); - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); - if (found == nodeMap.end()) - throw std::runtime_error("Can't find attachment node " + bonename); + if (mFirstPersonNeckController) + { + if (mAccurateAiming) + mAimingFactor = 1.f; + else + mAimingFactor = std::max(0.f, mAimingFactor - timepassed * 0.5f); - osg::ref_ptr attached = SceneUtil::attach(instance, mObjectRoot, bonefilter, found->second); - if (enchantedGlow) - mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); + float rotateFactor = 0.75f + 0.25f * mAimingFactor; - return PartHolderPtr(new PartHolder(attached)); -} + mFirstPersonNeckController->setRotate( + osg::Quat(mPtr.getRefData().getPosition().rot[0] * rotateFactor, osg::Vec3f(-1, 0, 0))); + mFirstPersonNeckController->setOffset(mFirstPersonOffset); + } -osg::Vec3f NpcAnimation::runAnimation(float timepassed) -{ - osg::Vec3f ret = Animation::runAnimation(timepassed); + WeaponAnimation::configureControllers(mPtr.getRefData().getPosition().rot[0] + getBodyPitchRadians()); - mHeadAnimationTime->update(timepassed); + return ret; + } - if (mFirstPersonNeckController) + void NpcAnimation::removeIndividualPart(ESM::PartReferenceType type) { - if (mAccurateAiming) - mAimingFactor = 1.f; - else - mAimingFactor = std::max(0.f, mAimingFactor - timepassed * 0.5f); - - float rotateFactor = 0.75f + 0.25f * mAimingFactor; + mPartPriorities[type] = 0; + mPartslots[type] = -1; - mFirstPersonNeckController->setRotate(osg::Quat(mPtr.getRefData().getPosition().rot[0] * rotateFactor, osg::Vec3f(-1,0,0))); - mFirstPersonNeckController->setOffset(mFirstPersonOffset); + mObjectParts[type].reset(); + if (mSounds[type] != nullptr && !mSoundsDisabled) + { + MWBase::Environment::get().getSoundManager()->stopSound(mSounds[type]); + mSounds[type] = nullptr; + } } - WeaponAnimation::configureControllers(mPtr.getRefData().getPosition().rot[0] + getBodyPitchRadians()); - - return ret; -} - -void NpcAnimation::removeIndividualPart(ESM::PartReferenceType type) -{ - mPartPriorities[type] = 0; - mPartslots[type] = -1; - - mObjectParts[type].reset(); - if (mSounds[type] != nullptr && !mSoundsDisabled) + void NpcAnimation::reserveIndividualPart(ESM::PartReferenceType type, int group, int priority) { - MWBase::Environment::get().getSoundManager()->stopSound(mSounds[type]); - mSounds[type] = nullptr; + if (priority > mPartPriorities[type]) + { + removeIndividualPart(type); + mPartPriorities[type] = priority; + mPartslots[type] = group; + } } -} -void NpcAnimation::reserveIndividualPart(ESM::PartReferenceType type, int group, int priority) -{ - if(priority > mPartPriorities[type]) + void NpcAnimation::removePartGroup(int group) { - removeIndividualPart(type); - mPartPriorities[type] = priority; - mPartslots[type] = group; + for (int i = 0; i < ESM::PRT_Count; i++) + { + if (mPartslots[i] == group) + removeIndividualPart((ESM::PartReferenceType)i); + } } -} -void NpcAnimation::removePartGroup(int group) -{ - for(int i = 0; i < ESM::PRT_Count; i++) + bool NpcAnimation::isFemalePart(const ESM::BodyPart* bodypart) { - if(mPartslots[i] == group) - removeIndividualPart((ESM::PartReferenceType)i); + return bodypart->mData.mFlags & ESM::BodyPart::BPF_Female; } -} - -bool NpcAnimation::isFirstPersonPart(const ESM::BodyPart* bodypart) -{ - return bodypart->mId.size() >= 3 && bodypart->mId.substr(bodypart->mId.size()-3, 3) == "1st"; -} -bool NpcAnimation::isFemalePart(const ESM::BodyPart* bodypart) -{ - return bodypart->mData.mFlags & ESM::BodyPart::BPF_Female; -} - -bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor) -{ - if(priority <= mPartPriorities[type]) - return false; - - removeIndividualPart(type); - mPartslots[type] = group; - mPartPriorities[type] = priority; - try + bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, + const std::string& mesh, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { - std::string bonename = sPartList.at(type); - if (type == ESM::PRT_Weapon) + if (priority <= mPartPriorities[type]) + return false; + + removeIndividualPart(type); + mPartslots[type] = group; + mPartPriorities[type] = priority; + try { - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()) + std::string_view bonename = sPartList.at(type); + if (type == ESM::PRT_Weapon) { - int weaponType = weapon->get()->mBase->mData.mType; - const std::string weaponBonename = MWMechanics::getWeaponType(weaponType)->mAttachBone; - - if (weaponBonename != bonename) + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weapon != inv.end() && weapon->getType() == ESM::Weapon::sRecordId) { - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(weaponBonename)); - if (found != nodeMap.end()) - bonename = weaponBonename; + int weaponType = weapon->get()->mBase->mData.mType; + const std::string& weaponBonename = MWMechanics::getWeaponType(weaponType)->mAttachBone; + + if (weaponBonename != bonename) + { + const NodeMap& nodeMap = getNodeMap(); + NodeMap::const_iterator found = nodeMap.find(weaponBonename); + if (found != nodeMap.end()) + bonename = weaponBonename; + } } } - } - // PRT_Hair seems to be the only type that breaks consistency and uses a filter that's different from the attachment bone - const std::string bonefilter = (type == ESM::PRT_Hair) ? "hair" : bonename; - mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor); - } - catch (std::exception& e) - { - Log(Debug::Error) << "Error adding NPC part: " << e.what(); - return false; - } + // PRT_Hair seems to be the only type that breaks consistency and uses a filter that's different from the + // attachment bone + const std::string_view bonefilter = (type == ESM::PRT_Hair) ? std::string_view{ "hair" } : bonename; + mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor, isLight); + } + catch (std::exception& e) + { + Log(Debug::Error) << "Error adding NPC part: " << e.what(); + return false; + } - if (!mSoundsDisabled) - { - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator csi = inv.getSlot(group < 0 ? MWWorld::InventoryStore::Slot_Helmet : group); - if (csi != inv.end()) + if (!mSoundsDisabled && group == MWWorld::InventoryStore::Slot_CarriedLeft) { - const auto soundId = csi->getClass().getSound(*csi); - if (!soundId.empty()) + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator csi = inv.getSlot(group); + if (csi != inv.end()) { - mSounds[type] = MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, soundId, - 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop - ); + const auto soundId = csi->getClass().getSound(*csi); + if (!soundId.empty()) + { + mSounds[type] = MWBase::Environment::get().getSoundManager()->playSound3D( + mPtr, soundId, 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop); + } } } - } - osg::Node* node = mObjectParts[type]->getNode(); - if (node->getNumChildrenRequiringUpdateTraversal() > 0) - { - std::shared_ptr src; - if (type == ESM::PRT_Head) + osg::Node* node = mObjectParts[type]->getNode(); + if (node->getNumChildrenRequiringUpdateTraversal() > 0) { - src = mHeadAnimationTime; - - if (node->getUserDataContainer()) + std::shared_ptr src; + if (type == ESM::PRT_Head) { - for (unsigned int i=0; igetUserDataContainer()->getNumUserObjects(); ++i) + src = mHeadAnimationTime; + + if (node->getUserDataContainer()) { - osg::Object* obj = node->getUserDataContainer()->getUserObject(i); - if (SceneUtil::TextKeyMapHolder* keys = dynamic_cast(obj)) + for (unsigned int i = 0; i < node->getUserDataContainer()->getNumUserObjects(); ++i) { - for (const auto &key : keys->mTextKeys) + osg::Object* obj = node->getUserDataContainer()->getUserObject(i); + if (SceneUtil::TextKeyMapHolder* keys = dynamic_cast(obj)) { - if (Misc::StringUtils::ciEqual(key.second, "talk: start")) - mHeadAnimationTime->setTalkStart(key.first); - if (Misc::StringUtils::ciEqual(key.second, "talk: stop")) - mHeadAnimationTime->setTalkStop(key.first); - if (Misc::StringUtils::ciEqual(key.second, "blink: start")) - mHeadAnimationTime->setBlinkStart(key.first); - if (Misc::StringUtils::ciEqual(key.second, "blink: stop")) - mHeadAnimationTime->setBlinkStop(key.first); + for (const auto& key : keys->mTextKeys) + { + if (Misc::StringUtils::ciEqual(key.second, "talk: start")) + mHeadAnimationTime->setTalkStart(key.first); + if (Misc::StringUtils::ciEqual(key.second, "talk: stop")) + mHeadAnimationTime->setTalkStop(key.first); + if (Misc::StringUtils::ciEqual(key.second, "blink: start")) + mHeadAnimationTime->setBlinkStart(key.first); + if (Misc::StringUtils::ciEqual(key.second, "blink: stop")) + mHeadAnimationTime->setBlinkStop(key.first); + } + + break; } - - break; } } + SceneUtil::ForceControllerSourcesVisitor assignVisitor(std::move(src)); + node->accept(assignVisitor); + } + else + { + if (type == ESM::PRT_Weapon) + src = mWeaponAnimationTime; + else + src = mAnimationTimePtr[0]; + SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::move(src)); + node->accept(assignVisitor); } } - else if (type == ESM::PRT_Weapon) - src = mWeaponAnimationTime; - else - src.reset(new NullAnimationTime); - SceneUtil::AssignControllerSourcesVisitor assignVisitor(src); - node->accept(assignVisitor); + return true; } - return true; -} + void NpcAnimation::addPartGroup(int group, int priority, const std::vector& parts, + bool enchantedGlow, osg::Vec4f* glowColor) + { + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const MWWorld::Store& partStore = store.get(); -void NpcAnimation::addPartGroup(int group, int priority, const std::vector &parts, bool enchantedGlow, osg::Vec4f* glowColor) -{ - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const MWWorld::Store &partStore = store.get(); + const char* ext = (mViewMode == VM_FirstPerson) ? ".1st" : ""; + for (const ESM::PartReference& part : parts) + { + const ESM::BodyPart* bodypart = nullptr; + if (!mNpc->isMale() && !part.mFemale.empty()) + { + bodypart = partStore.search(ESM::RefId::stringRefId(part.mFemale.getRefIdString() + ext)); + if (!bodypart && mViewMode == VM_FirstPerson) + { + bodypart = partStore.search(part.mFemale); + if (bodypart + && !(bodypart->mData.mPart == ESM::BodyPart::MP_Hand + || bodypart->mData.mPart == ESM::BodyPart::MP_Wrist + || bodypart->mData.mPart == ESM::BodyPart::MP_Forearm + || bodypart->mData.mPart == ESM::BodyPart::MP_Upperarm)) + bodypart = nullptr; + } + else if (!bodypart) + Log(Debug::Warning) << "Warning: Failed to find body part '" << part.mFemale << "'"; + } + if (!bodypart && !part.mMale.empty()) + { + bodypart = partStore.search(ESM::RefId::stringRefId(part.mMale.getRefIdString() + ext)); + if (!bodypart && mViewMode == VM_FirstPerson) + { + bodypart = partStore.search(part.mMale); + if (bodypart + && !(bodypart->mData.mPart == ESM::BodyPart::MP_Hand + || bodypart->mData.mPart == ESM::BodyPart::MP_Wrist + || bodypart->mData.mPart == ESM::BodyPart::MP_Forearm + || bodypart->mData.mPart == ESM::BodyPart::MP_Upperarm)) + bodypart = nullptr; + } + else if (!bodypart) + Log(Debug::Warning) << "Warning: Failed to find body part '" << part.mMale << "'"; + } + + if (bodypart) + addOrReplaceIndividualPart(static_cast(part.mPart), group, priority, + Misc::ResourceHelpers::correctMeshPath(bodypart->mModel), enchantedGlow, glowColor); + else + reserveIndividualPart((ESM::PartReferenceType)part.mPart, group, priority); + } + } - const char *ext = (mViewMode == VM_FirstPerson) ? ".1st" : ""; - for(const ESM::PartReference& part : parts) + void NpcAnimation::addControllers() { - const ESM::BodyPart *bodypart = nullptr; - if(!mNpc->isMale() && !part.mFemale.empty()) + Animation::addControllers(); + + mFirstPersonNeckController = nullptr; + WeaponAnimation::deleteControllers(); + + if (mViewMode == VM_FirstPerson) { - bodypart = partStore.search(part.mFemale+ext); - if(!bodypart && mViewMode == VM_FirstPerson) + // If there is no active animation, then the bip01 neck node will not be updated each frame, and the + // RotateController will accumulate rotations. + if (mStates.size() > 0) { - bodypart = partStore.search(part.mFemale); - if(bodypart && !(bodypart->mData.mPart == ESM::BodyPart::MP_Hand || - bodypart->mData.mPart == ESM::BodyPart::MP_Wrist || - bodypart->mData.mPart == ESM::BodyPart::MP_Forearm || - bodypart->mData.mPart == ESM::BodyPart::MP_Upperarm)) - bodypart = nullptr; + NodeMap::iterator found = mNodeMap.find("bip01 neck"); + if (found != mNodeMap.end()) + { + osg::MatrixTransform* node = found->second.get(); + mFirstPersonNeckController = new RotateController(mObjectRoot.get()); + node->addUpdateCallback(mFirstPersonNeckController); + mActiveControllers.emplace_back(node, mFirstPersonNeckController); + } } - else if (!bodypart) - Log(Debug::Warning) << "Warning: Failed to find body part '" << part.mFemale << "'"; } - if(!bodypart && !part.mMale.empty()) + else if (mViewMode == VM_Normal) { - bodypart = partStore.search(part.mMale+ext); - if(!bodypart && mViewMode == VM_FirstPerson) + WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get()); + } + } + + void NpcAnimation::showWeapons(bool showWeapon) + { + mShowWeapons = showWeapon; + mAmmunition.reset(); + if (showWeapon) + { + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weapon != inv.end()) { - bodypart = partStore.search(part.mMale); - if(bodypart && !(bodypart->mData.mPart == ESM::BodyPart::MP_Hand || - bodypart->mData.mPart == ESM::BodyPart::MP_Wrist || - bodypart->mData.mPart == ESM::BodyPart::MP_Forearm || - bodypart->mData.mPart == ESM::BodyPart::MP_Upperarm)) - bodypart = nullptr; + osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); + std::string mesh = weapon->getClass().getCorrectedModel(*weapon); + addOrReplaceIndividualPart(ESM::PRT_Weapon, MWWorld::InventoryStore::Slot_CarriedRight, 1, mesh, + !weapon->getClass().getEnchantment(*weapon).empty(), &glowColor); + + // Crossbows start out with a bolt attached + if (weapon->getType() == ESM::Weapon::sRecordId + && weapon->get()->mBase->mData.mType == ESM::Weapon::MarksmanCrossbow) + { + int ammotype = MWMechanics::getWeaponType(ESM::Weapon::MarksmanCrossbow)->mAmmoType; + MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + if (ammo != inv.end() && ammo->get()->mBase->mData.mType == ammotype) + attachArrow(); + } } - else if (!bodypart) - Log(Debug::Warning) << "Warning: Failed to find body part '" << part.mMale << "'"; } - - if(bodypart) - addOrReplaceIndividualPart((ESM::PartReferenceType)part.mPart, group, priority, "meshes\\"+bodypart->mModel, enchantedGlow, glowColor); else - reserveIndividualPart((ESM::PartReferenceType)part.mPart, group, priority); - } -} - -void NpcAnimation::addControllers() -{ - Animation::addControllers(); + { + removeIndividualPart(ESM::PRT_Weapon); + // If we remove/hide weapon from player, we should reset attack animation as well + if (mPtr == MWMechanics::getPlayer()) + mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(false); + } - mFirstPersonNeckController = nullptr; - WeaponAnimation::deleteControllers(); + updateHolsteredWeapon(!mShowWeapons); + updateQuiver(); + } - if (mViewMode == VM_FirstPerson) + bool NpcAnimation::updateCarriedLeftVisible(const int weaptype) const { - NodeMap::iterator found = mNodeMap.find("bip01 neck"); - if (found != mNodeMap.end()) + if (Settings::game().mShieldSheathing) { - osg::MatrixTransform* node = found->second.get(); - mFirstPersonNeckController = new NeckController(mObjectRoot.get()); - node->addUpdateCallback(mFirstPersonNeckController); - mActiveControllers.emplace_back(node, mFirstPersonNeckController); + const MWWorld::Class& cls = mPtr.getClass(); + MWMechanics::CreatureStats& stats = cls.getCreatureStats(mPtr); + if (stats.getDrawState() == MWMechanics::DrawState::Nothing) + { + SceneUtil::FindByNameVisitor findVisitor("Bip01 AttachShield"); + mObjectRoot->accept(findVisitor); + if (findVisitor.mFoundNode || mViewMode == VM_FirstPerson) + { + const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); + const MWWorld::ConstContainerStoreIterator shield + = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (shield != inv.end() && shield->getType() == ESM::Armor::sRecordId + && !getSheathedShieldMesh(*shield).empty()) + return false; + } + } } + + return !(MWMechanics::getWeaponType(weaptype)->mFlags & ESM::WeaponType::TwoHanded); } - else if (mViewMode == VM_Normal) - { - WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get()); - } -} -void NpcAnimation::showWeapons(bool showWeapon) -{ - mShowWeapons = showWeapon; - mAmmunition.reset(); - if(showWeapon) + void NpcAnimation::showCarriedLeft(bool show) { + mShowCarriedLeft = show; const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon != inv.end()) + MWWorld::ConstContainerStoreIterator iter = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (show && iter != inv.end()) { - osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); - std::string mesh = weapon->getClass().getModel(*weapon); - addOrReplaceIndividualPart(ESM::PRT_Weapon, MWWorld::InventoryStore::Slot_CarriedRight, 1, - mesh, !weapon->getClass().getEnchantment(*weapon).empty(), &glowColor); - - // Crossbows start out with a bolt attached - if (weapon->getTypeName() == typeid(ESM::Weapon).name() && - weapon->get()->mBase->mData.mType == ESM::Weapon::MarksmanCrossbow) + osg::Vec4f glowColor = iter->getClass().getEnchantmentColor(*iter); + std::string mesh = iter->getClass().getCorrectedModel(*iter); + // For shields we must try to use the body part model + if (iter->getType() == ESM::Armor::sRecordId) + { + mesh = getShieldMesh(*iter, !mNpc->isMale()); + } + if (mesh.empty() + || addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, mesh, + !iter->getClass().getEnchantment(*iter).empty(), &glowColor, + iter->getType() == ESM::Light::sRecordId)) { - int ammotype = MWMechanics::getWeaponType(ESM::Weapon::MarksmanCrossbow)->mAmmoType; - MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); - if (ammo != inv.end() && ammo->get()->mBase->mData.mType == ammotype) - attachArrow(); + if (mesh.empty()) + reserveIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1); + if (iter->getType() == ESM::Light::sRecordId && mObjectParts[ESM::PRT_Shield]) + addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), + SceneUtil::LightCommon(*iter->get()->mBase)); } } - } - else - { - removeIndividualPart(ESM::PRT_Weapon); - // If we remove/hide weapon from player, we should reset attack animation as well - if (mPtr == MWMechanics::getPlayer()) - MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); - } + else + removeIndividualPart(ESM::PRT_Shield); - updateHolsteredWeapon(!mShowWeapons); - updateQuiver(); -} + updateHolsteredShield(mShowCarriedLeft); + } -void NpcAnimation::showCarriedLeft(bool show) -{ - mShowCarriedLeft = show; - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator iter = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(show && iter != inv.end()) + void NpcAnimation::attachArrow() { - osg::Vec4f glowColor = iter->getClass().getEnchantmentColor(*iter); - std::string mesh = iter->getClass().getModel(*iter); - // For shields we must try to use the body part model - if (iter->getTypeName() == typeid(ESM::Armor).name()) - { - const ESM::Armor *armor = iter->get()->mBase; - const std::vector& bodyparts = armor->mParts.mParts; - if (!bodyparts.empty()) - mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); - } - if (mesh.empty() || addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, - mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor)) + WeaponAnimation::attachArrow(mPtr); + + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + if (ammo != inv.end() && !ammo->getClass().getEnchantment(*ammo).empty()) { - if (mesh.empty()) - reserveIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1); - if (iter->getTypeName() == typeid(ESM::Light).name() && mObjectParts[ESM::PRT_Shield]) - addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), iter->get()->mBase); + osg::Group* bone = getArrowBone(); + if (bone != nullptr && bone->getNumChildren()) + SceneUtil::addEnchantedGlow( + bone->getChild(0), mResourceSystem, ammo->getClass().getEnchantmentColor(*ammo)); } - } - else - removeIndividualPart(ESM::PRT_Shield); - - updateHolsteredShield(mShowCarriedLeft); -} -void NpcAnimation::attachArrow() -{ - WeaponAnimation::attachArrow(mPtr); + updateQuiver(); + } - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); - if (ammo != inv.end() && !ammo->getClass().getEnchantment(*ammo).empty()) + void NpcAnimation::detachArrow() { - osg::Group* bone = getArrowBone(); - if (bone != nullptr && bone->getNumChildren()) - SceneUtil::addEnchantedGlow(bone->getChild(0), mResourceSystem, ammo->getClass().getEnchantmentColor(*ammo)); + WeaponAnimation::detachArrow(mPtr); + updateQuiver(); } - updateQuiver(); -} - -void NpcAnimation::detachArrow() -{ - WeaponAnimation::detachArrow(mPtr); - updateQuiver(); -} + void NpcAnimation::releaseArrow(float attackStrength) + { + WeaponAnimation::releaseArrow(mPtr, attackStrength); + updateQuiver(); + } -void NpcAnimation::releaseArrow(float attackStrength) -{ - WeaponAnimation::releaseArrow(mPtr, attackStrength); - updateQuiver(); -} + osg::Group* NpcAnimation::getArrowBone() + { + const PartHolder* const part = mObjectParts[ESM::PRT_Weapon].get(); + if (part == nullptr) + return nullptr; -osg::Group* NpcAnimation::getArrowBone() -{ - PartHolderPtr part = mObjectParts[ESM::PRT_Weapon]; - if (!part) - return nullptr; + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) + return nullptr; - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) - return nullptr; + int type = weapon->get()->mBase->mData.mType; + int ammoType = MWMechanics::getWeaponType(type)->mAmmoType; + if (ammoType == ESM::Weapon::None) + return nullptr; - int type = weapon->get()->mBase->mData.mType; - int ammoType = MWMechanics::getWeaponType(type)->mAmmoType; + // Try to find and attachment bone in actor's skeleton, otherwise fall back to the ArrowBone in weapon's mesh + osg::Group* bone = getBoneByName(MWMechanics::getWeaponType(ammoType)->mAttachBone); + if (bone == nullptr) + { + SceneUtil::FindByNameVisitor findVisitor("ArrowBone"); + part->getNode()->accept(findVisitor); + bone = findVisitor.mFoundNode; + } + return bone; + } - // Try to find and attachment bone in actor's skeleton, otherwise fall back to the ArrowBone in weapon's mesh - osg::Group* bone = getBoneByName(MWMechanics::getWeaponType(ammoType)->mAttachBone); - if (bone == nullptr) + osg::Node* NpcAnimation::getWeaponNode() { - SceneUtil::FindByNameVisitor findVisitor ("ArrowBone"); - part->getNode()->accept(findVisitor); - bone = findVisitor.mFoundNode; + const PartHolder* const part = mObjectParts[ESM::PRT_Weapon].get(); + if (part == nullptr) + return nullptr; + return part->getNode(); } - return bone; -} -osg::Node* NpcAnimation::getWeaponNode() -{ - PartHolderPtr part = mObjectParts[ESM::PRT_Weapon]; - if (!part) - return nullptr; - return part->getNode(); -} - -Resource::ResourceSystem* NpcAnimation::getResourceSystem() -{ - return mResourceSystem; -} - -void NpcAnimation::permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) -{ - // During first auto equip, we don't play any sounds. - // Basically we don't want sounds when the actor is first loaded, - // the items should appear as if they'd always been equipped. - if (isNew) + Resource::ResourceSystem* NpcAnimation::getResourceSystem() { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; + return mResourceSystem; + } - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!magicEffect->mHitSound.empty()) - sndMgr->playSound3D(mPtr, magicEffect->mHitSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(mPtr, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); + void NpcAnimation::enableHeadAnimation(bool enable) + { + mHeadAnimationTime->setEnabled(enable); } - if (!magicEffect->mHit.empty()) + void NpcAnimation::setWeaponGroup(const std::string& group, bool relativeDuration) { - const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); - bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; - // Don't play particle VFX unless the effect is new or it should be looping. - if (isNew || loop) - addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); + mWeaponAnimationTime->setGroup(group, relativeDuration); } -} -void NpcAnimation::enableHeadAnimation(bool enable) -{ - mHeadAnimationTime->setEnabled(enable); -} + void NpcAnimation::equipmentChanged() + { + if (Settings::game().mShieldSheathing) + { + int weaptype = ESM::Weapon::None; + MWMechanics::getActiveWeapon(mPtr, &weaptype); + showCarriedLeft(updateCarriedLeftVisible(weaptype)); + } -void NpcAnimation::setWeaponGroup(const std::string &group, bool relativeDuration) -{ - mWeaponAnimationTime->setGroup(group, relativeDuration); -} + updateParts(); + } -void NpcAnimation::equipmentChanged() -{ - static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); - if (shieldSheathing) + void NpcAnimation::setVampire(bool vampire) { - int weaptype = ESM::Weapon::None; - MWMechanics::getActiveWeapon(mPtr, &weaptype); - showCarriedLeft(updateCarriedLeftVisible(weaptype)); + if (mNpcType == Type_Werewolf) // we can't have werewolf vampires, can we + return; + if ((mNpcType == Type_Vampire) != vampire) + { + if (mPtr == MWMechanics::getPlayer()) + MWBase::Environment::get().getWorld()->reattachPlayerCamera(); + else + rebuild(); + } } - updateParts(); -} - -void NpcAnimation::setVampire(bool vampire) -{ - if (mNpcType == Type_Werewolf) // we can't have werewolf vampires, can we - return; - if ((mNpcType == Type_Vampire) != vampire) + void NpcAnimation::setFirstPersonOffset(const osg::Vec3f& offset) { - if (mPtr == MWMechanics::getPlayer()) - MWBase::Environment::get().getWorld()->reattachPlayerCamera(); - else - rebuild(); + mFirstPersonOffset = offset; } -} -void NpcAnimation::setFirstPersonOffset(const osg::Vec3f &offset) -{ - mFirstPersonOffset = offset; -} - -void NpcAnimation::updatePtr(const MWWorld::Ptr &updated) -{ - Animation::updatePtr(updated); - mHeadAnimationTime->updatePtr(updated); -} + void NpcAnimation::updatePtr(const MWWorld::Ptr& updated) + { + Animation::updatePtr(updated); + mHeadAnimationTime->updatePtr(updated); + } -// Remember body parts so we only have to search through the store once for each race/gender/viewmode combination -typedef std::map< std::pair,std::vector > RaceMapping; -static RaceMapping sRaceMapping; + // Remember body parts so we only have to search through the store once for each race/gender/viewmode combination + typedef std::map, std::vector> RaceMapping; + static RaceMapping sRaceMapping; -const std::vector& NpcAnimation::getBodyParts(const std::string &race, bool female, bool firstPerson, bool werewolf) -{ - static const int Flag_FirstPerson = 1<<1; - static const int Flag_Female = 1<<0; - - int flags = (werewolf ? -1 : 0); - if(female) - flags |= Flag_Female; - if(firstPerson) - flags |= Flag_FirstPerson; - - RaceMapping::iterator found = sRaceMapping.find(std::make_pair(race, flags)); - if (found != sRaceMapping.end()) - return found->second; - else + const std::vector& NpcAnimation::getBodyParts( + const ESM::RefId& race, bool female, bool firstPerson, bool werewolf) { - std::vector& parts = sRaceMapping[std::make_pair(race, flags)]; - - typedef std::multimap BodyPartMapType; - static const BodyPartMapType sBodyPartMap = + static const int Flag_FirstPerson = 1 << 1; + static const int Flag_Female = 1 << 0; + + int flags = (werewolf ? -1 : 0); + if (female) + flags |= Flag_Female; + if (firstPerson) + flags |= Flag_FirstPerson; + + RaceMapping::iterator found = sRaceMapping.find(std::make_pair(race, flags)); + if (found != sRaceMapping.end()) + return found->second; + else { - {ESM::BodyPart::MP_Neck, ESM::PRT_Neck}, - {ESM::BodyPart::MP_Chest, ESM::PRT_Cuirass}, - {ESM::BodyPart::MP_Groin, ESM::PRT_Groin}, - {ESM::BodyPart::MP_Hand, ESM::PRT_RHand}, - {ESM::BodyPart::MP_Hand, ESM::PRT_LHand}, - {ESM::BodyPart::MP_Wrist, ESM::PRT_RWrist}, - {ESM::BodyPart::MP_Wrist, ESM::PRT_LWrist}, - {ESM::BodyPart::MP_Forearm, ESM::PRT_RForearm}, - {ESM::BodyPart::MP_Forearm, ESM::PRT_LForearm}, - {ESM::BodyPart::MP_Upperarm, ESM::PRT_RUpperarm}, - {ESM::BodyPart::MP_Upperarm, ESM::PRT_LUpperarm}, - {ESM::BodyPart::MP_Foot, ESM::PRT_RFoot}, - {ESM::BodyPart::MP_Foot, ESM::PRT_LFoot}, - {ESM::BodyPart::MP_Ankle, ESM::PRT_RAnkle}, - {ESM::BodyPart::MP_Ankle, ESM::PRT_LAnkle}, - {ESM::BodyPart::MP_Knee, ESM::PRT_RKnee}, - {ESM::BodyPart::MP_Knee, ESM::PRT_LKnee}, - {ESM::BodyPart::MP_Upperleg, ESM::PRT_RLeg}, - {ESM::BodyPart::MP_Upperleg, ESM::PRT_LLeg}, - {ESM::BodyPart::MP_Tail, ESM::PRT_Tail} - }; + std::vector& parts = sRaceMapping[std::make_pair(race, flags)]; - parts.resize(ESM::PRT_Count, nullptr); + typedef std::multimap BodyPartMapType; + static const BodyPartMapType sBodyPartMap = { { ESM::BodyPart::MP_Neck, ESM::PRT_Neck }, + { ESM::BodyPart::MP_Chest, ESM::PRT_Cuirass }, { ESM::BodyPart::MP_Groin, ESM::PRT_Groin }, + { ESM::BodyPart::MP_Hand, ESM::PRT_RHand }, { ESM::BodyPart::MP_Hand, ESM::PRT_LHand }, + { ESM::BodyPart::MP_Wrist, ESM::PRT_RWrist }, { ESM::BodyPart::MP_Wrist, ESM::PRT_LWrist }, + { ESM::BodyPart::MP_Forearm, ESM::PRT_RForearm }, { ESM::BodyPart::MP_Forearm, ESM::PRT_LForearm }, + { ESM::BodyPart::MP_Upperarm, ESM::PRT_RUpperarm }, { ESM::BodyPart::MP_Upperarm, ESM::PRT_LUpperarm }, + { ESM::BodyPart::MP_Foot, ESM::PRT_RFoot }, { ESM::BodyPart::MP_Foot, ESM::PRT_LFoot }, + { ESM::BodyPart::MP_Ankle, ESM::PRT_RAnkle }, { ESM::BodyPart::MP_Ankle, ESM::PRT_LAnkle }, + { ESM::BodyPart::MP_Knee, ESM::PRT_RKnee }, { ESM::BodyPart::MP_Knee, ESM::PRT_LKnee }, + { ESM::BodyPart::MP_Upperleg, ESM::PRT_RLeg }, { ESM::BodyPart::MP_Upperleg, ESM::PRT_LLeg }, + { ESM::BodyPart::MP_Tail, ESM::PRT_Tail } }; - if (werewolf) - return parts; + parts.resize(ESM::PRT_Count, nullptr); - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + if (werewolf) + return parts; - for(const ESM::BodyPart& bodypart : store.get()) - { - if (bodypart.mData.mFlags & ESM::BodyPart::BPF_NotPlayable) - continue; - if (bodypart.mData.mType != ESM::BodyPart::MT_Skin) - continue; + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - if (!Misc::StringUtils::ciEqual(bodypart.mRace, race)) - continue; + for (const ESM::BodyPart& bodypart : store.get()) + { + if (bodypart.mData.mFlags & ESM::BodyPart::BPF_NotPlayable) + continue; + if (bodypart.mData.mType != ESM::BodyPart::MT_Skin) + continue; - bool partFirstPerson = isFirstPersonPart(&bodypart); + if (!(bodypart.mRace == race)) + continue; - bool isHand = bodypart.mData.mPart == ESM::BodyPart::MP_Hand || - bodypart.mData.mPart == ESM::BodyPart::MP_Wrist || - bodypart.mData.mPart == ESM::BodyPart::MP_Forearm || - bodypart.mData.mPart == ESM::BodyPart::MP_Upperarm; + const bool partFirstPerson = ESM::isFirstPersonBodyPart(bodypart); - bool isSameGender = isFemalePart(&bodypart) == female; + bool isHand = bodypart.mData.mPart == ESM::BodyPart::MP_Hand + || bodypart.mData.mPart == ESM::BodyPart::MP_Wrist + || bodypart.mData.mPart == ESM::BodyPart::MP_Forearm + || bodypart.mData.mPart == ESM::BodyPart::MP_Upperarm; - /* A fallback for the arms if 1st person is missing: - 1. Try to use 3d person skin for same gender - 2. Try to use 1st person skin for male, if female == true - 3. Try to use 3d person skin for male, if female == true + bool isSameGender = isFemalePart(&bodypart) == female; - A fallback in another cases: allow to use male bodyparts, if female == true - */ - if (firstPerson && isHand && !partFirstPerson) - { - // Allow 3rd person skins as a fallback for the arms if 1st person is missing - BodyPartMapType::const_iterator bIt = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart)); - while(bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart) + /* A fallback for the arms if 1st person is missing: + 1. Try to use 3d person skin for same gender + 2. Try to use 1st person skin for male, if female == true + 3. Try to use 3d person skin for male, if female == true + + A fallback in another cases: allow to use male bodyparts, if female == true + */ + if (firstPerson && isHand && !partFirstPerson) { - // If we have no fallback bodypart now and bodypart is for same gender (1) - if(!parts[bIt->second] && isSameGender) - parts[bIt->second] = &bodypart; + // Allow 3rd person skins as a fallback for the arms if 1st person is missing + BodyPartMapType::const_iterator bIt + = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart)); + while (bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart) + { + // If we have no fallback bodypart now and bodypart is for same gender (1) + if (!parts[bIt->second] && isSameGender) + parts[bIt->second] = &bodypart; - // If we have fallback bodypart for other gender and found fallback for current gender (1) - else if(isSameGender && isFemalePart(parts[bIt->second]) != female) - parts[bIt->second] = &bodypart; + // If we have fallback bodypart for other gender and found fallback for current gender (1) + else if (isSameGender && isFemalePart(parts[bIt->second]) != female) + parts[bIt->second] = &bodypart; - // If we have no fallback bodypart and searching for female bodyparts (3) - else if(!parts[bIt->second] && female) - parts[bIt->second] = &bodypart; + // If we have no fallback bodypart and searching for female bodyparts (3) + else if (!parts[bIt->second] && female) + parts[bIt->second] = &bodypart; - ++bIt; - } + ++bIt; + } - continue; - } + continue; + } - // Don't allow to use podyparts for a different view - if (partFirstPerson != firstPerson) - continue; + // Don't allow to use podyparts for a different view + if (partFirstPerson != firstPerson) + continue; - if (female && !isFemalePart(&bodypart)) - { - // Allow male parts as fallback for females if female parts are missing - BodyPartMapType::const_iterator bIt = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart)); - while(bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart) + if (female && !isFemalePart(&bodypart)) { - // If we have no fallback bodypart now - if(!parts[bIt->second]) - parts[bIt->second] = &bodypart; + // Allow male parts as fallback for females if female parts are missing + BodyPartMapType::const_iterator bIt + = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart)); + while (bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart) + { + // If we have no fallback bodypart now + if (!parts[bIt->second]) + parts[bIt->second] = &bodypart; - // If we have 3d person fallback bodypart for hand and 1st person fallback found (2) - else if(isHand && !isFirstPersonPart(parts[bIt->second]) && partFirstPerson) - parts[bIt->second] = &bodypart; + // If we have 3d person fallback bodypart for hand and 1st person fallback found (2) + else if (isHand && !ESM::isFirstPersonBodyPart(*parts[bIt->second]) && partFirstPerson) + parts[bIt->second] = &bodypart; - ++bIt; - } + ++bIt; + } - continue; - } + continue; + } - // Don't allow to use podyparts for another gender - if (female != isFemalePart(&bodypart)) - continue; + // Don't allow to use podyparts for another gender + if (female != isFemalePart(&bodypart)) + continue; - // Use properly found bodypart, replacing fallbacks - BodyPartMapType::const_iterator bIt = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart)); - while(bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart) - { - parts[bIt->second] = &bodypart; - ++bIt; + // Use properly found bodypart, replacing fallbacks + BodyPartMapType::const_iterator bIt + = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart)); + while (bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart) + { + parts[bIt->second] = &bodypart; + ++bIt; + } } + return parts; } - return parts; } -} -void NpcAnimation::setAccurateAiming(bool enabled) -{ - mAccurateAiming = enabled; -} + void NpcAnimation::setAccurateAiming(bool enabled) + { + mAccurateAiming = enabled; + } -bool NpcAnimation::isArrowAttached() const -{ - return mAmmunition != nullptr; -} + bool NpcAnimation::isArrowAttached() const + { + return mAmmunition != nullptr; + } } diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index 7e55001daf6..a03ee28f3ae 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -24,155 +24,159 @@ namespace MWSound namespace MWRender { -class NeckController; -class HeadAnimationTime; + class RotateController; + class HeadAnimationTime; -class NpcAnimation : public ActorAnimation, public WeaponAnimation, public MWWorld::InventoryStoreListener -{ -public: - void equipmentChanged() override; - void permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) override; + class NpcAnimation : public ActorAnimation, public WeaponAnimation, public MWWorld::InventoryStoreListener + { + public: + void equipmentChanged() override; -public: - typedef std::map PartBoneMap; + public: + typedef std::map PartBoneMap; - enum ViewMode { - VM_Normal, - VM_FirstPerson, - VM_HeadOnly - }; + enum ViewMode + { + VM_Normal, + VM_FirstPerson, + VM_HeadOnly + }; -private: - static const PartBoneMap sPartList; + private: + static const PartBoneMap sPartList; - // Bounded Parts - PartHolderPtr mObjectParts[ESM::PRT_Count]; - std::array mSounds; + // Bounded Parts + PartHolderPtr mObjectParts[ESM::PRT_Count]; + std::array mSounds; - const ESM::NPC *mNpc; - std::string mHeadModel; - std::string mHairModel; - ViewMode mViewMode; - bool mShowWeapons; - bool mShowCarriedLeft; + const ESM::NPC* mNpc; + std::string mHeadModel; + std::string mHairModel; + ViewMode mViewMode; + bool mShowWeapons; + bool mShowCarriedLeft; - enum NpcType - { - Type_Normal, - Type_Werewolf, - Type_Vampire - }; - NpcType mNpcType; + enum NpcType + { + Type_Normal, + Type_Werewolf, + Type_Vampire + }; + NpcType mNpcType; - int mPartslots[ESM::PRT_Count]; //Each part slot is taken by clothing, armor, or is empty - int mPartPriorities[ESM::PRT_Count]; + int mPartslots[ESM::PRT_Count]; // Each part slot is taken by clothing, armor, or is empty + int mPartPriorities[ESM::PRT_Count]; - osg::Vec3f mFirstPersonOffset; - // Field of view to use when rendering first person meshes - float mFirstPersonFieldOfView; + osg::Vec3f mFirstPersonOffset; + // Field of view to use when rendering first person meshes + float mFirstPersonFieldOfView; - std::shared_ptr mHeadAnimationTime; - std::shared_ptr mWeaponAnimationTime; + std::shared_ptr mHeadAnimationTime; + std::shared_ptr mWeaponAnimationTime; - bool mSoundsDisabled; + bool mSoundsDisabled; - bool mAccurateAiming; - float mAimingFactor; + bool mAccurateAiming; + float mAimingFactor; - void updateNpcBase(); + void updateNpcBase(); - NpcType getNpcType() const; + NpcType getNpcType() const; - PartHolderPtr insertBoundedPart(const std::string &model, const std::string &bonename, - const std::string &bonefilter, bool enchantedGlow, osg::Vec4f* glowColor=nullptr); + PartHolderPtr insertBoundedPart(const std::string& model, std::string_view bonename, + std::string_view bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight); - void removeIndividualPart(ESM::PartReferenceType type); - void reserveIndividualPart(ESM::PartReferenceType type, int group, int priority); + void removeIndividualPart(ESM::PartReferenceType type); + void reserveIndividualPart(ESM::PartReferenceType type, int group, int priority); - bool addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, - bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr); - void removePartGroup(int group); - void addPartGroup(int group, int priority, const std::vector &parts, - bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr); + bool addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string& mesh, + bool enchantedGlow = false, osg::Vec4f* glowColor = nullptr, bool isLight = false); + void removePartGroup(int group); + void addPartGroup(int group, int priority, const std::vector& parts, + bool enchantedGlow = false, osg::Vec4f* glowColor = nullptr); - void setRenderBin(); + void setRenderBin(); - osg::ref_ptr mFirstPersonNeckController; + osg::ref_ptr mFirstPersonNeckController; - static bool isFirstPersonPart(const ESM::BodyPart* bodypart); - static bool isFemalePart(const ESM::BodyPart* bodypart); - static NpcType getNpcType(const MWWorld::Ptr& ptr); + static bool isFemalePart(const ESM::BodyPart* bodypart); + static NpcType getNpcType(const MWWorld::Ptr& ptr); -protected: - void addControllers() override; - bool isArrowAttached() const override; - std::string getShieldMesh(MWWorld::ConstPtr shield) const override; + protected: + void addControllers() override; + bool isArrowAttached() const override; + std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const override; -public: - /** - * @param ptr - * @param disableListener Don't listen for equipment changes and magic effects. InventoryStore only supports - * one listener at a time, so you shouldn't do this if creating several NpcAnimations - * for the same Ptr, eg preview dolls for the player. - * Those need to be manually rendered anyway. - * @param disableSounds Same as \a disableListener but for playing items sounds - * @param viewMode - */ - NpcAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem, - bool disableSounds = false, ViewMode viewMode=VM_Normal, float firstPersonFieldOfView=55.f); - virtual ~NpcAnimation(); + public: + /** + * @param ptr + * @param disableListener Don't listen for equipment changes and magic effects. InventoryStore only supports + * one listener at a time, so you shouldn't do this if creating several NpcAnimations + * for the same Ptr, eg preview dolls for the player. + * Those need to be manually rendered anyway. + * @param disableSounds Same as \a disableListener but for playing items sounds + * @param viewMode + */ + NpcAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, + Resource::ResourceSystem* resourceSystem, bool disableSounds = false, ViewMode viewMode = VM_Normal, + float firstPersonFieldOfView = 55.f); + virtual ~NpcAnimation(); - void enableHeadAnimation(bool enable) override; + void enableHeadAnimation(bool enable) override; - /// 1: the first person meshes follow the camera's rotation completely - /// 0: the first person meshes follow the camera with a reduced factor, so you can look down at your own hands - void setAccurateAiming(bool enabled) override; + /// 1: the first person meshes follow the camera's rotation completely + /// 0: the first person meshes follow the camera with a reduced factor, so you can look down at your own hands + void setAccurateAiming(bool enabled) override; - void setWeaponGroup(const std::string& group, bool relativeDuration) override; + void setWeaponGroup(const std::string& group, bool relativeDuration) override; - osg::Vec3f runAnimation(float timepassed) override; + osg::Vec3f runAnimation(float timepassed) override; - /// A relative factor (0-1) that decides if and how much the skeleton should be pitched - /// to indicate the facing orientation of the character. - void setPitchFactor(float factor) override { mPitchFactor = factor; } + /// A relative factor (0-1) that decides if and how much the skeleton should be pitched + /// to indicate the facing orientation of the character. + void setPitchFactor(float factor) override { mPitchFactor = factor; } - void showWeapons(bool showWeapon) override; + bool getWeaponsShown() const override { return mShowWeapons; } + void showWeapons(bool showWeapon) override; - bool getCarriedLeftShown() const override { return mShowCarriedLeft; } - void showCarriedLeft(bool show) override; + bool updateCarriedLeftVisible(const int weaptype) const override; + bool getCarriedLeftShown() const override { return mShowCarriedLeft; } + void showCarriedLeft(bool show) override; - void attachArrow() override; - void detachArrow() override; - void releaseArrow(float attackStrength) override; + void attachArrow() override; + void detachArrow() override; + void releaseArrow(float attackStrength) override; - osg::Group* getArrowBone() override; - osg::Node* getWeaponNode() override; - Resource::ResourceSystem* getResourceSystem() override; + osg::Group* getArrowBone() override; + osg::Node* getWeaponNode() override; + Resource::ResourceSystem* getResourceSystem() override; - // WeaponAnimation - void showWeapon(bool show) override { showWeapons(show); } + // WeaponAnimation + void showWeapon(bool show) override { showWeapons(show); } - void setViewMode(ViewMode viewMode); + void setViewMode(ViewMode viewMode); - void updateParts(); + void updateParts(); - /// Rebuilds the NPC, updating their root model, animation sources, and equipment. - void rebuild(); + /// Rebuilds the NPC, updating their root model, animation sources, and equipment. + void rebuild(); - /// Get the inventory slot that the given node path leads into, or -1 if not found. - int getSlot(const osg::NodePath& path) const; + /// Get the inventory slot that the given node path leads into, or -1 if not found. + int getSlot(const osg::NodePath& path) const; - void setVampire(bool vampire) override; + void setVampire(bool vampire) override; - /// Set a translation offset (in object root space) to apply to meshes when in first person mode. - void setFirstPersonOffset(const osg::Vec3f& offset); + /// Set a translation offset (in object root space) to apply to meshes when in first person mode. + void setFirstPersonOffset(const osg::Vec3f& offset); - void updatePtr(const MWWorld::Ptr& updated) override; + void updatePtr(const MWWorld::Ptr& updated) override; - /// Get a list of body parts that may be used by an NPC of given race and gender. - /// @note This is a fixed size list, one list item for each ESM::PartReferenceType, may contain nullptr body parts. - static const std::vector& getBodyParts(const std::string& raceId, bool female, bool firstperson, bool werewolf); -}; + /// Get a list of body parts that may be used by an NPC of given race and gender. + /// @note This is a fixed size list, one list item for each ESM::PartReferenceType, may contain nullptr body + /// parts. + static const std::vector& getBodyParts( + const ESM::RefId& raceId, bool female, bool firstperson, bool werewolf); + }; } diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 8e29f0af4f4..6e39d99404a 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -1,474 +1,605 @@ #include "objectpaging.hpp" #include +#include -#include #include -#include -#include #include +#include +#include +#include +#include +#include +#include #include -#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include -#include -#include -#include -#include - -#include -#include - #include #include +#include +#include #include -#include -#include +#include +#include +#include +#include -#include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" #include "vismask.hpp" namespace MWRender { - - bool typeFilter(int type, bool far) + namespace { - switch (type) + bool typeFilter(int type, bool far) { - case ESM::REC_STAT: - case ESM::REC_ACTI: - case ESM::REC_DOOR: - return true; - case ESM::REC_CONT: - return !far; - - default: - return false; + switch (type) + { + case ESM::REC_STAT: + case ESM::REC_ACTI: + case ESM::REC_DOOR: + return true; + case ESM::REC_CONT: + return !far; + + default: + return false; + } } - } - std::string getModel(int type, const std::string& id, const MWWorld::ESMStore& store) - { - switch (type) + std::string getModel(int type, ESM::RefId id, const MWWorld::ESMStore& store) { - case ESM::REC_STAT: - return store.get().searchStatic(id)->mModel; - case ESM::REC_ACTI: - return store.get().searchStatic(id)->mModel; - case ESM::REC_DOOR: - return store.get().searchStatic(id)->mModel; - case ESM::REC_CONT: - return store.get().searchStatic(id)->mModel; - default: - return std::string(); + switch (type) + { + case ESM::REC_STAT: + return store.get().searchStatic(id)->mModel; + case ESM::REC_ACTI: + return store.get().searchStatic(id)->mModel; + case ESM::REC_DOOR: + return store.get().searchStatic(id)->mModel; + case ESM::REC_CONT: + return store.get().searchStatic(id)->mModel; + default: + return {}; + } } } - osg::ref_ptr ObjectPaging::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) + osg::ref_ptr ObjectPaging::getChunk(float size, const osg::Vec2f& center, unsigned char /*lod*/, + unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) { if (activeGrid && !mActiveGrid) return nullptr; - ChunkId id = std::make_tuple(center, size, activeGrid); + const ChunkId id = std::make_tuple(center, size, activeGrid); - osg::ref_ptr obj = mCache->getRefFromObjectCache(id); - if (obj) - return obj->asNode(); - else - { - osg::ref_ptr node = createChunk(size, center, activeGrid, viewPoint, compile); - mCache->addEntryToObjectCache(id, node.get()); - return node; - } + if (const osg::ref_ptr obj = mCache->getRefFromObjectCache(id)) + return static_cast(obj.get()); + + const unsigned char lod = static_cast(lodFlags >> (4 * 4)); + osg::ref_ptr node = createChunk(size, center, activeGrid, viewPoint, compile, lod); + mCache->addEntryToObjectCache(id, node.get()); + return node; } - class CanOptimizeCallback : public SceneUtil::Optimizer::IsOperationPermissibleForObjectCallback + namespace { - public: - bool isOperationPermissibleForObjectImplementation(const SceneUtil::Optimizer* optimizer, const osg::Drawable* node,unsigned int option) const override + class CanOptimizeCallback : public SceneUtil::Optimizer::IsOperationPermissibleForObjectCallback { - return true; - } - bool isOperationPermissibleForObjectImplementation(const SceneUtil::Optimizer* optimizer, const osg::Node* node,unsigned int option) const override + public: + bool isOperationPermissibleForObjectImplementation( + const SceneUtil::Optimizer* optimizer, const osg::Drawable* node, unsigned int option) const override + { + return true; + } + bool isOperationPermissibleForObjectImplementation( + const SceneUtil::Optimizer* optimizer, const osg::Node* node, unsigned int option) const override + { + return (node->getDataVariance() != osg::Object::DYNAMIC); + } + }; + + using LODRange = osg::LOD::MinMaxPair; + + LODRange intersection(const LODRange& left, const LODRange& right) { - return (node->getDataVariance() != osg::Object::DYNAMIC); + return { std::max(left.first, right.first), std::min(left.second, right.second) }; } - }; - class CopyOp : public osg::CopyOp - { - public: - bool mOptimizeBillboards = true; - float mSqrDistance = 0.f; - osg::Vec3f mViewVector; - osg::Node::NodeMask mCopyMask = ~0u; - mutable std::vector mNodePath; - - void copy(const osg::Node* toCopy, osg::Group* attachTo) + bool empty(const LODRange& r) { - const osg::Group* groupToCopy = toCopy->asGroup(); - if (toCopy->getStateSet() || toCopy->asTransform() || !groupToCopy) - attachTo->addChild(operator()(toCopy)); - else - { - for (unsigned int i=0; igetNumChildren(); ++i) - attachTo->addChild(operator()(groupToCopy->getChild(i))); - } + return r.first >= r.second; } - osg::Node* operator() (const osg::Node* node) const override + LODRange operator/(const LODRange& r, float div) { - if (!(node->getNodeMask() & mCopyMask)) - return nullptr; - - if (const osg::Drawable* d = node->asDrawable()) - return operator()(d); - - if (dynamic_cast(node)) - return nullptr; - if (dynamic_cast(node)) - return nullptr; + return { r.first / div, r.second / div }; + } - if (const osg::Switch* sw = node->asSwitch()) + class CopyOp : public osg::CopyOp + { + public: + bool mOptimizeBillboards = true; + LODRange mDistances = { 0.f, 0.f }; + osg::Vec3f mViewVector; + osg::Node::NodeMask mCopyMask = ~0u; + mutable std::vector mNodePath; + + void copy(const osg::Node* toCopy, osg::Group* attachTo) { - osg::Group* n = new osg::Group; - for (unsigned int i=0; igetNumChildren(); ++i) - if (sw->getValue(i)) - n->addChild(operator()(sw->getChild(i))); - n->setDataVariance(osg::Object::STATIC); - return n; + const osg::Group* groupToCopy = toCopy->asGroup(); + if (toCopy->getStateSet() || toCopy->asTransform() || !groupToCopy) + attachTo->addChild(operator()(toCopy)); + else + { + for (unsigned int i = 0; i < groupToCopy->getNumChildren(); ++i) + attachTo->addChild(operator()(groupToCopy->getChild(i))); + } } - if (const osg::LOD* lod = dynamic_cast(node)) + + osg::Node* operator()(const osg::Node* node) const override { - osg::Group* n = new osg::Group; - for (unsigned int i=0; igetNumChildren(); ++i) - if (lod->getMinRange(i) * lod->getMinRange(i) <= mSqrDistance && mSqrDistance < lod->getMaxRange(i) * lod->getMaxRange(i)) - n->addChild(operator()(lod->getChild(i))); - n->setDataVariance(osg::Object::STATIC); - return n; - } + if (!(node->getNodeMask() & mCopyMask)) + return nullptr; - mNodePath.push_back(node); + if (const osg::Drawable* d = node->asDrawable()) + return operator()(d); - osg::Node* cloned = static_cast(node->clone(*this)); - cloned->setDataVariance(osg::Object::STATIC); - cloned->setUserDataContainer(nullptr); - cloned->setName(""); + if (dynamic_cast(node)) + return nullptr; + if (dynamic_cast(node)) + return nullptr; - mNodePath.pop_back(); + if (const osg::Switch* sw = node->asSwitch()) + { + osg::Group* n = new osg::Group; + for (unsigned int i = 0; i < sw->getNumChildren(); ++i) + if (sw->getValue(i)) + n->addChild(operator()(sw->getChild(i))); + n->setDataVariance(osg::Object::STATIC); + return n; + } + if (const osg::LOD* lod = dynamic_cast(node)) + { + std::vector, LODRange>> children; + for (unsigned int i = 0; i < lod->getNumChildren(); ++i) + if (const auto r = intersection(lod->getRangeList()[i], mDistances); !empty(r)) + children.emplace_back(operator()(lod->getChild(i)), lod->getRangeList()[i]); + if (children.empty()) + return nullptr; + + if (children.size() == 1) + return children.front().first.release(); + else + { + osg::LOD* n = new osg::LOD; + for (const auto& [child, range] : children) + n->addChild(child, range.first, range.second); + n->setDataVariance(osg::Object::STATIC); + return n; + } + } + if (const osg::Sequence* sq = dynamic_cast(node)) + { + osg::Group* n = new osg::Group; + n->addChild(operator()(sq->getChild(sq->getValue() != -1 ? sq->getValue() : 0))); + n->setDataVariance(osg::Object::STATIC); + return n; + } - handleCallbacks(node, cloned); + mNodePath.push_back(node); - return cloned; - } - void handleCallbacks(const osg::Node* node, osg::Node *cloned) const - { - for (const osg::Callback* callback = node->getCullCallback(); callback != nullptr; callback = callback->getNestedCallback()) + osg::Node* cloned = static_cast(node->clone(*this)); + cloned->setDataVariance(osg::Object::STATIC); + cloned->setUserDataContainer(nullptr); + cloned->setName(""); + + mNodePath.pop_back(); + + handleCallbacks(node, cloned); + + return cloned; + } + void handleCallbacks(const osg::Node* node, osg::Node* cloned) const { - if (callback->className() == std::string("BillboardCallback")) + for (const osg::Callback* callback = node->getCullCallback(); callback != nullptr; + callback = callback->getNestedCallback()) { - if (mOptimizeBillboards) + if (callback->className() == std::string("BillboardCallback")) { - handleBillboard(cloned); - continue; + if (mOptimizeBillboards) + { + handleBillboard(cloned); + continue; + } + else + cloned->setDataVariance(osg::Object::DYNAMIC); + } + + if (node->getCullCallback()->getNestedCallback()) + { + osg::Callback* clonedCallback = osg::clone(callback, osg::CopyOp::SHALLOW_COPY); + clonedCallback->setNestedCallback(nullptr); + cloned->addCullCallback(clonedCallback); } else - cloned->setDataVariance(osg::Object::DYNAMIC); + cloned->addCullCallback(const_cast(callback)); } + } + void handleBillboard(osg::Node* node) const + { + osg::Transform* transform = node->asTransform(); + if (!transform) + return; + osg::MatrixTransform* matrixTransform = transform->asMatrixTransform(); + if (!matrixTransform) + return; + + osg::Matrix worldToLocal = osg::Matrix::identity(); + for (auto pathNode : mNodePath) + if (const osg::Transform* t = pathNode->asTransform()) + t->computeWorldToLocalMatrix(worldToLocal, nullptr); + worldToLocal = osg::Matrix::orthoNormal(worldToLocal); + + osg::Matrix billboardMatrix; + osg::Vec3f viewVector = -(mViewVector + worldToLocal.getTrans()); + viewVector.normalize(); + osg::Vec3f right = viewVector ^ osg::Vec3f(0, 0, 1); + right.normalize(); + osg::Vec3f up = right ^ viewVector; + up.normalize(); + billboardMatrix.makeLookAt(osg::Vec3f(0, 0, 0), viewVector, up); + billboardMatrix.invert(billboardMatrix); + + const osg::Matrix& oldMatrix = matrixTransform->getMatrix(); + float mag[3]; // attempt to preserve scale + for (int i = 0; i < 3; ++i) + mag[i] = std::sqrt(oldMatrix(0, i) * oldMatrix(0, i) + oldMatrix(1, i) * oldMatrix(1, i) + + oldMatrix(2, i) * oldMatrix(2, i)); + osg::Matrix newMatrix; + worldToLocal.setTrans(0, 0, 0); + newMatrix *= worldToLocal; + newMatrix.preMult(billboardMatrix); + newMatrix.preMultScale(osg::Vec3f(mag[0], mag[1], mag[2])); + newMatrix.setTrans(oldMatrix.getTrans()); + + matrixTransform->setMatrix(newMatrix); + } + osg::Drawable* operator()(const osg::Drawable* drawable) const override + { + if (!(drawable->getNodeMask() & mCopyMask)) + return nullptr; + + if (dynamic_cast(drawable)) + return nullptr; - if (node->getCullCallback()->getNestedCallback()) + if (dynamic_cast(drawable)) + return nullptr; + if (const SceneUtil::RigGeometry* rig = dynamic_cast(drawable)) + return operator()(rig->getSourceGeometry()); + if (const SceneUtil::MorphGeometry* morph = dynamic_cast(drawable)) + return operator()(morph->getSourceGeometry()); + + if (getCopyFlags() & DEEP_COPY_DRAWABLES) { - osg::Callback *clonedCallback = osg::clone(callback, osg::CopyOp::SHALLOW_COPY); - clonedCallback->setNestedCallback(nullptr); - cloned->addCullCallback(clonedCallback); + osg::Drawable* d = static_cast(drawable->clone(*this)); + d->setDataVariance(osg::Object::STATIC); + d->setUserDataContainer(nullptr); + d->setName(""); + return d; } else - cloned->addCullCallback(const_cast(callback)); + return const_cast(drawable); } - } - void handleBillboard(osg::Node* node) const + osg::Callback* operator()(const osg::Callback* callback) const override { return nullptr; } + }; + + class RefnumSet : public osg::Object { - osg::Transform* transform = node->asTransform(); - if (!transform) return; - osg::MatrixTransform* matrixTransform = transform->asMatrixTransform(); - if (!matrixTransform) return; - - osg::Matrix worldToLocal = osg::Matrix::identity(); - for (auto pathNode : mNodePath) - if (const osg::Transform* t = pathNode->asTransform()) - t->computeWorldToLocalMatrix(worldToLocal, nullptr); - worldToLocal = osg::Matrix::orthoNormal(worldToLocal); - - osg::Matrix billboardMatrix; - osg::Vec3f viewVector = -(mViewVector + worldToLocal.getTrans()); - viewVector.normalize(); - osg::Vec3f right = viewVector ^ osg::Vec3f(0,0,1); - right.normalize(); - osg::Vec3f up = right ^ viewVector; - up.normalize(); - billboardMatrix.makeLookAt(osg::Vec3f(0,0,0), viewVector, up); - billboardMatrix.invert(billboardMatrix); - - const osg::Matrix& oldMatrix = matrixTransform->getMatrix(); - float mag[3]; // attempt to preserve scale - for (int i=0;i<3;++i) - mag[i] = std::sqrt(oldMatrix(0,i) * oldMatrix(0,i) + oldMatrix(1,i) * oldMatrix(1,i) + oldMatrix(2,i) * oldMatrix(2,i)); - osg::Matrix newMatrix; - worldToLocal.setTrans(0,0,0); - newMatrix *= worldToLocal; - newMatrix.preMult(billboardMatrix); - newMatrix.preMultScale(osg::Vec3f(mag[0], mag[1], mag[2])); - newMatrix.setTrans(oldMatrix.getTrans()); - - matrixTransform->setMatrix(newMatrix); - } - osg::Drawable* operator() (const osg::Drawable* drawable) const override + public: + RefnumSet() {} + RefnumSet(const RefnumSet& copy, const osg::CopyOp&) + : mRefnums(copy.mRefnums) + { + } + META_Object(MWRender, RefnumSet) + std::vector mRefnums; + }; + + class AnalyzeVisitor : public osg::NodeVisitor { - if (!(drawable->getNodeMask() & mCopyMask)) - return nullptr; + public: + AnalyzeVisitor(osg::Node::NodeMask analyzeMask) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mCurrentStateSet(nullptr) + { + setTraversalMask(analyzeMask); + } - if (dynamic_cast(drawable)) - return nullptr; + typedef std::unordered_map StateSetCounter; + struct Result + { + StateSetCounter mStateSetCounter; + unsigned int mNumVerts = 0; + }; - if (const SceneUtil::RigGeometry* rig = dynamic_cast(drawable)) - return operator()(rig->getSourceGeometry()); - if (const SceneUtil::MorphGeometry* morph = dynamic_cast(drawable)) - return operator()(morph->getSourceGeometry()); + void apply(osg::Node& node) override + { + if (node.getStateSet()) + mCurrentStateSet = node.getStateSet(); - if (getCopyFlags() & DEEP_COPY_DRAWABLES) + if (osg::Switch* sw = node.asSwitch()) + { + for (unsigned int i = 0; i < sw->getNumChildren(); ++i) + if (sw->getValue(i)) + traverse(*sw->getChild(i)); + return; + } + if (osg::LOD* lod = dynamic_cast(&node)) + { + for (unsigned int i = 0; i < lod->getNumChildren(); ++i) + if (const auto r = intersection(lod->getRangeList()[i], mDistances); !empty(r)) + traverse(*lod->getChild(i)); + return; + } + if (osg::Sequence* sq = dynamic_cast(&node)) + { + traverse(*sq->getChild(sq->getValue() != -1 ? sq->getValue() : 0)); + return; + } + + traverse(node); + } + void apply(osg::Geometry& geom) override { - osg::Drawable* d = static_cast(drawable->clone(*this)); - d->setDataVariance(osg::Object::STATIC); - d->setUserDataContainer(nullptr); - d->setName(""); - return d; + if (osg::Array* array = geom.getVertexArray()) + mResult.mNumVerts += array->getNumElements(); + + ++mResult.mStateSetCounter[mCurrentStateSet]; + ++mGlobalStateSetCounter[mCurrentStateSet]; + } + Result retrieveResult() + { + Result result = mResult; + mResult = Result(); + mCurrentStateSet = nullptr; + return result; + } + void addInstance(const Result& result) + { + for (auto pair : result.mStateSetCounter) + mGlobalStateSetCounter[pair.first] += pair.second; + } + float getMergeBenefit(const Result& result) + { + if (result.mStateSetCounter.empty()) + return 1; + float mergeBenefit = 0; + for (auto pair : result.mStateSetCounter) + { + mergeBenefit += mGlobalStateSetCounter[pair.first]; + } + mergeBenefit /= result.mStateSetCounter.size(); + return mergeBenefit; } - else - return const_cast(drawable); - } - osg::Callback* operator() (const osg::Callback* callback) const override - { - return nullptr; - } - }; - class RefnumSet : public osg::Object - { - public: - RefnumSet(){} - RefnumSet(const RefnumSet& copy, const osg::CopyOp&) : mRefnums(copy.mRefnums) {} - META_Object(MWRender, RefnumSet) - std::set mRefnums; - }; - - class AnalyzeVisitor : public osg::NodeVisitor - { - public: - AnalyzeVisitor(osg::Node::NodeMask analyzeMask) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mCurrentStateSet(nullptr) - , mCurrentDistance(0.f) - , mAnalyzeMask(analyzeMask) {} - - typedef std::unordered_map StateSetCounter; - struct Result - { - StateSetCounter mStateSetCounter; - unsigned int mNumVerts = 0; + Result mResult; + osg::StateSet* mCurrentStateSet; + StateSetCounter mGlobalStateSetCounter; + LODRange mDistances = { 0.f, 0.f }; }; - void apply(osg::Node& node) override + class DebugVisitor : public osg::NodeVisitor { - if (!(node.getNodeMask() & mAnalyzeMask)) - return; - - if (node.getStateSet()) - mCurrentStateSet = node.getStateSet(); - - if (osg::Switch* sw = node.asSwitch()) + public: + DebugVisitor() + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { - for (unsigned int i=0; igetNumChildren(); ++i) - if (sw->getValue(i)) - traverse(*sw->getChild(i)); - return; } - if (osg::LOD* lod = dynamic_cast(&node)) + void apply(osg::Drawable& node) override { - for (unsigned int i=0; igetNumChildren(); ++i) - if (lod->getMinRange(i) * lod->getMinRange(i) <= mCurrentDistance && mCurrentDistance < lod->getMaxRange(i) * lod->getMaxRange(i)) - traverse(*lod->getChild(i)); - return; + osg::ref_ptr m(new osg::Material); + osg::Vec4f color( + Misc::Rng::rollProbability(), Misc::Rng::rollProbability(), Misc::Rng::rollProbability(), 0.f); + color.normalize(); + m->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.1f, 0.1f, 0.1f, 1.f)); + m->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.1f, 0.1f, 0.1f, 1.f)); + m->setColorMode(osg::Material::OFF); + m->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(color)); + osg::ref_ptr stateset = node.getStateSet() + ? osg::clone(node.getStateSet(), osg::CopyOp::SHALLOW_COPY) + : new osg::StateSet; + stateset->setAttribute(m); + stateset->addUniform(new osg::Uniform("colorMode", 0)); + stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); + stateset->addUniform(new osg::Uniform("specStrength", 1.f)); + node.setStateSet(stateset); } + }; - traverse(node); - } - void apply(osg::Geometry& geom) override - { - if (!(geom.getNodeMask() & mAnalyzeMask)) - return; - - if (osg::Array* array = geom.getVertexArray()) - mResult.mNumVerts += array->getNumElements(); - - ++mResult.mStateSetCounter[mCurrentStateSet]; - ++mGlobalStateSetCounter[mCurrentStateSet]; - } - Result retrieveResult() + class AddRefnumMarkerVisitor : public osg::NodeVisitor { - Result result = mResult; - mResult = Result(); - mCurrentStateSet = nullptr; - return result; - } - void addInstance(const Result& result) - { - for (auto pair : result.mStateSetCounter) - mGlobalStateSetCounter[pair.first] += pair.second; - } - float getMergeBenefit(const Result& result) - { - if (result.mStateSetCounter.empty()) return 1; - float mergeBenefit = 0; - for (auto pair : result.mStateSetCounter) + public: + AddRefnumMarkerVisitor(ESM::RefNum refnum) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mRefnum(refnum) { - mergeBenefit += mGlobalStateSetCounter[pair.first]; } - mergeBenefit /= result.mStateSetCounter.size(); - return mergeBenefit; - } + ESM::RefNum mRefnum; + void apply(osg::Geometry& node) override + { + osg::ref_ptr marker(new RefnumMarker); + marker->mRefnum = mRefnum; + if (osg::Array* array = node.getVertexArray()) + marker->mNumVertices = array->getNumElements(); + node.getOrCreateUserDataContainer()->addUserObject(marker); + } + }; + } - Result mResult; - osg::StateSet* mCurrentStateSet; - StateSetCounter mGlobalStateSetCounter; - float mCurrentDistance; - osg::Node::NodeMask mAnalyzeMask; - }; + ObjectPaging::ObjectPaging(Resource::SceneManager* sceneManager, ESM::RefId worldspace) + : GenericResourceManager(nullptr, Settings::cells().mCacheExpiryDelay) + , Terrain::QuadTreeWorld::ChunkManager(worldspace) + , mSceneManager(sceneManager) + , mActiveGrid(Settings::terrain().mObjectPagingActiveGrid) + , mDebugBatches(Settings::terrain().mDebugChunks) + , mMergeFactor(Settings::terrain().mObjectPagingMergeFactor) + , mMinSize(Settings::terrain().mObjectPagingMinSize) + , mMinSizeMergeFactor(Settings::terrain().mObjectPagingMinSizeMergeFactor) + , mMinSizeCostMultiplier(Settings::terrain().mObjectPagingMinSizeCostMultiplier) + , mRefTrackerLocked(false) + { + } - class DebugVisitor : public osg::NodeVisitor + namespace { - public: - DebugVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) {} - void apply(osg::Drawable& node) override + struct PagedCellRef { - osg::ref_ptr m (new osg::Material); - osg::Vec4f color(Misc::Rng::rollProbability(), Misc::Rng::rollProbability(), Misc::Rng::rollProbability(), 0.f); - color.normalize(); - m->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.1f,0.1f,0.1f,1.f)); - m->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.1f,0.1f,0.1f,1.f)); - m->setColorMode(osg::Material::OFF); - m->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(color)); - osg::ref_ptr stateset = node.getStateSet() ? osg::clone(node.getStateSet(), osg::CopyOp::SHALLOW_COPY) : new osg::StateSet; - stateset->setAttribute(m); - stateset->addUniform(new osg::Uniform("colorMode", 0)); - node.setStateSet(stateset); - } - }; + ESM::RefId mRefId; + ESM::RefNum mRefNum; + osg::Vec3f mPosition; + osg::Vec3f mRotation; + float mScale; + }; - class AddRefnumMarkerVisitor : public osg::NodeVisitor - { - public: - AddRefnumMarkerVisitor(const ESM::RefNum &refnum) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN), mRefnum(refnum) {} - ESM::RefNum mRefnum; - void apply(osg::Geometry &node) override + PagedCellRef makePagedCellRef(const ESM::CellRef& value) { - osg::ref_ptr marker (new RefnumMarker); - marker->mRefnum = mRefnum; - if (osg::Array* array = node.getVertexArray()) - marker->mNumVertices = array->getNumElements(); - node.getOrCreateUserDataContainer()->addUserObject(marker); + return PagedCellRef{ + .mRefId = value.mRefID, + .mRefNum = value.mRefNum, + .mPosition = value.mPos.asVec3(), + .mRotation = value.mPos.asRotationVec3(), + .mScale = value.mScale, + }; } - }; - ObjectPaging::ObjectPaging(Resource::SceneManager* sceneManager) - : GenericResourceManager(nullptr) - , mSceneManager(sceneManager) - , mRefTrackerLocked(false) - { - mActiveGrid = Settings::Manager::getBool("object paging active grid", "Terrain"); - mDebugBatches = Settings::Manager::getBool("object paging debug batches", "Terrain"); - mMergeFactor = Settings::Manager::getFloat("object paging merge factor", "Terrain"); - mMinSize = Settings::Manager::getFloat("object paging min size", "Terrain"); - mMinSizeMergeFactor = Settings::Manager::getFloat("object paging min size merge factor", "Terrain"); - mMinSizeCostMultiplier = Settings::Manager::getFloat("object paging min size cost multiplier", "Terrain"); - } - - osg::ref_ptr ObjectPaging::createChunk(float size, const osg::Vec2f& center, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) - { - osg::Vec2i startCell = osg::Vec2i(std::floor(center.x() - size/2.f), std::floor(center.y() - size/2.f)); - - osg::Vec3f worldCenter = osg::Vec3f(center.x(), center.y(), 0)*ESM::Land::REAL_SIZE; - osg::Vec3f relativeViewPoint = viewPoint - worldCenter; - - std::map refs; - std::vector esm; - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - - for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX) + std::map collectESM3References( + float size, const osg::Vec2i& startCell, const MWWorld::ESMStore& store) { - for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY) + std::map refs; + ESM::ReadersCache readers; + for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX) { - const ESM::Cell* cell = store.get().searchStatic(cellX, cellY); - if (!cell) continue; - for (size_t i=0; imContextList.size(); ++i) + for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY) { - try + const ESM::Cell* cell = store.get().searchStatic(cellX, cellY); + if (!cell) + continue; + for (size_t i = 0; i < cell->mContextList.size(); ++i) { - unsigned int index = cell->mContextList[i].index; - if (esm.size()<=index) - esm.resize(index+1); - cell->restore(esm[index], i); - ESM::CellRef ref; - ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; - bool deleted = false; - while(cell->getNextRef(esm[index], ref, deleted)) + try + { + const std::size_t index = static_cast(cell->mContextList[i].index); + const ESM::ReadersCache::BusyItem reader = readers.get(index); + cell->restore(*reader, i); + ESM::CellRef ref; + ESM::MovedCellRef cMRef; + bool deleted = false; + bool moved = false; + while (ESM::Cell::getNextRef( + *reader, ref, deleted, cMRef, moved, ESM::Cell::GetNextRefMode::LoadOnlyNotMoved)) + { + if (moved) + continue; + + if (std::find(cell->mMovedRefs.begin(), cell->mMovedRefs.end(), ref.mRefNum) + != cell->mMovedRefs.end()) + continue; + + int type = store.findStatic(ref.mRefID); + if (!typeFilter(type, size >= 2)) + continue; + if (deleted) + { + refs.erase(ref.mRefNum); + continue; + } + refs.insert_or_assign(ref.mRefNum, makePagedCellRef(ref)); + } + } + catch (const std::exception& e) { - if (std::find(cell->mMovedRefs.begin(), cell->mMovedRefs.end(), ref.mRefNum) != cell->mMovedRefs.end()) continue; - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); - int type = store.findStatic(ref.mRefID); - if (!typeFilter(type,size>=2)) continue; - if (deleted) { refs.erase(ref.mRefNum); continue; } - if (ref.mRefNum.fromGroundcoverFile()) continue; - refs[ref.mRefNum] = std::move(ref); + Log(Debug::Warning) << "Failed to collect references from cell \"" << cell->getDescription() + << "\": " << e.what(); + continue; } } - catch (std::exception&) + for (const auto& [ref, deleted] : cell->mLeasedRefs) { - continue; + if (deleted) + { + refs.erase(ref.mRefNum); + continue; + } + int type = store.findStatic(ref.mRefID); + if (!typeFilter(type, size >= 2)) + continue; + refs.insert_or_assign(ref.mRefNum, makePagedCellRef(ref)); } } - for (auto [ref, deleted] : cell->mLeasedRefs) - { - if (deleted) { refs.erase(ref.mRefNum); continue; } - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); - int type = store.findStatic(ref.mRefID); - if (!typeFilter(type,size>=2)) continue; - refs[ref.mRefNum] = std::move(ref); - } } + return refs; } + } - if (activeGrid) + osg::ref_ptr ObjectPaging::createChunk(float size, const osg::Vec2f& center, bool activeGrid, + const osg::Vec3f& viewPoint, bool compile, unsigned char lod) + { + const osg::Vec2i startCell(std::floor(center.x() - size / 2.f), std::floor(center.y() - size / 2.f)); + const MWBase::World& world = *MWBase::Environment::get().getWorld(); + const MWWorld::ESMStore& store = world.getStore(); + + std::map refs; + + if (mWorldspace == ESM::Cell::sDefaultWorldspaceId) + { + refs = collectESM3References(size, startCell, store); + } + else + { + // TODO + } + + if (activeGrid && !refs.empty()) { std::lock_guard lock(mRefTrackerMutex); - for (auto ref : getRefTracker().mBlacklist) - refs.erase(ref); + const std::set& blacklist = getRefTracker().mBlacklist; + if (blacklist.size() < refs.size()) + { + for (ESM::RefNum ref : blacklist) + refs.erase(ref); + } + else + { + std::erase_if(refs, [&](const auto& ref) { return blacklist.contains(ref.first); }); + } } - osg::Vec2f minBound = (center - osg::Vec2f(size/2.f, size/2.f)); - osg::Vec2f maxBound = (center + osg::Vec2f(size/2.f, size/2.f)); + const osg::Vec2f minBound = (center - osg::Vec2f(size / 2.f, size / 2.f)); + const osg::Vec2f maxBound = (center + osg::Vec2f(size / 2.f, size / 2.f)); + const osg::Vec2i floorMinBound(std::floor(minBound.x()), std::floor(minBound.y())); + const osg::Vec2i ceilMaxBound(std::ceil(maxBound.x()), std::ceil(maxBound.y())); struct InstanceList { - std::vector mInstances; + std::vector mInstances; AnalyzeVisitor::Result mAnalyzeResult; bool mNeedCompile = false; }; typedef std::map, InstanceList> NodeMap; NodeMap nodes; - osg::ref_ptr refnumSet = activeGrid ? new RefnumSet : nullptr; + const osg::ref_ptr refnumSet = activeGrid ? new RefnumSet : nullptr; // Mask_UpdateVisitor is used in such cases in NIF loader: // 1. For collision nodes, which is not supposed to be rendered. @@ -476,90 +607,114 @@ namespace MWRender // Since ObjectPaging does not handle VisController, we can just ignore both types of nodes. constexpr auto copyMask = ~Mask_UpdateVisitor; + const int cellSize = getCellSize(mWorldspace); + const float smallestDistanceToChunk = (size > 1 / 8.f) ? (size * cellSize) : 0.f; + const float higherDistanceToChunk + = activeGrid ? ((size < 1) ? 5 : 3) * cellSize * size + 1 : smallestDistanceToChunk + 1; + AnalyzeVisitor analyzeVisitor(copyMask); - osg::Vec3f center3 = { center.x(), center.y(), 0.f }; - analyzeVisitor.mCurrentDistance = (viewPoint - center3).length2(); - float minSize = mMinSize; - if (mMinSizeMergeFactor) - minSize *= mMinSizeMergeFactor; - for (const auto& pair : refs) + const float minSize = mMinSizeMergeFactor ? mMinSize * mMinSizeMergeFactor : mMinSize; + for (const auto& [refNum, ref] : refs) { - const ESM::CellRef& ref = pair.second; - - osg::Vec3f pos = ref.mPos.asVec3(); if (size < 1.f) { - osg::Vec3f cellPos = pos / ESM::Land::REAL_SIZE; - if ((minBound.x() > std::floor(minBound.x()) && cellPos.x() < minBound.x()) || (minBound.y() > std::floor(minBound.y()) && cellPos.y() < minBound.y()) - || (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) || (minBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) + const osg::Vec3f cellPos = ref.mPosition / cellSize; + if ((minBound.x() > floorMinBound.x() && cellPos.x() < minBound.x()) + || (minBound.y() > floorMinBound.y() && cellPos.y() < minBound.y()) + || (maxBound.x() < ceilMaxBound.x() && cellPos.x() >= maxBound.x()) + || (maxBound.y() < ceilMaxBound.y() && cellPos.y() >= maxBound.y())) continue; } - float dSqr = (viewPoint - pos).length2(); + const float dSqr = (viewPoint - ref.mPosition).length2(); if (!activeGrid) { std::lock_guard lock(mSizeCacheMutex); - SizeCache::iterator found = mSizeCache.find(pair.first); - if (found != mSizeCache.end() && found->second < dSqr*minSize*minSize) + SizeCache::iterator found = mSizeCache.find(refNum); + if (found != mSizeCache.end() && found->second < dSqr * minSize * minSize) continue; } - if (ref.mRefID == "prisonmarker" || ref.mRefID == "divinemarker" || ref.mRefID == "templemarker" || ref.mRefID == "northmarker") - continue; // marker objects that have a hardcoded function in the game logic, should be hidden from the player + if (Misc::ResourceHelpers::isHiddenMarker(ref.mRefId)) + continue; - int type = store.findStatic(ref.mRefID); - std::string model = getModel(type, ref.mRefID, store); - if (model.empty()) continue; - model = "meshes/" + model; + const int type = store.findStatic(ref.mRefId); + std::string model = getModel(type, ref.mRefId, store); + if (model.empty()) + continue; + model = Misc::ResourceHelpers::correctMeshPath(model); if (activeGrid && type != ESM::REC_STAT) { model = Misc::ResourceHelpers::correctActorModelPath(model, mSceneManager->getVFS()); std::string kfname = Misc::StringUtils::lowerCase(model); - if(kfname.size() > 4 && kfname.compare(kfname.size()-4, 4, ".nif") == 0) + if (kfname.size() > 4 && kfname.ends_with(".nif")) { - kfname.replace(kfname.size()-4, 4, ".kf"); + kfname.replace(kfname.size() - 4, 4, ".kf"); if (mSceneManager->getVFS()->exists(kfname)) continue; } } - osg::ref_ptr cnode = mSceneManager->getTemplate(model, false); + if (!activeGrid) + { + std::lock_guard lock(mLODNameCacheMutex); + LODNameCacheKey key{ model, lod }; + LODNameCache::const_iterator found = mLODNameCache.lower_bound(key); + if (found != mLODNameCache.end() && found->first == key) + model = found->second; + else + model = mLODNameCache + .emplace_hint(found, std::move(key), + Misc::ResourceHelpers::getLODMeshName(world.getESMVersions()[refNum.mContentFile], + model, mSceneManager->getVFS(), lod)) + ->second; + } + + osg::ref_ptr cnode = mSceneManager->getTemplate(VFS::Path::toNormalized(model), false); if (activeGrid) { - if (cnode->getNumChildrenRequiringUpdateTraversal() > 0 || SceneUtil::hasUserDescription(cnode, Constants::NightDayLabel) || SceneUtil::hasUserDescription(cnode, Constants::HerbalismLabel)) + if (cnode->getNumChildrenRequiringUpdateTraversal() > 0 + || SceneUtil::hasUserDescription(cnode, Constants::NightDayLabel) + || SceneUtil::hasUserDescription(cnode, Constants::HerbalismLabel) + || (cnode->getName() == "Collada visual scene group" + && dynamic_cast(cnode->getUpdateCallback()))) continue; else - refnumSet->mRefnums.insert(pair.first); + refnumSet->mRefnums.push_back(refNum); } { std::lock_guard lock(mRefTrackerMutex); - if (getRefTracker().mDisabled.count(pair.first)) + if (getRefTracker().mDisabled.count(refNum)) continue; } - float radius2 = cnode->getBound().radius2() * ref.mScale*ref.mScale; - if (radius2 < dSqr*minSize*minSize && !activeGrid) + const float radius2 = cnode->getBound().radius2() * ref.mScale * ref.mScale; + if (radius2 < dSqr * minSize * minSize && !activeGrid) { std::lock_guard lock(mSizeCacheMutex); - mSizeCache[pair.first] = radius2; + mSizeCache[refNum] = radius2; continue; } - auto emplaced = nodes.emplace(cnode, InstanceList()); + const auto emplaced = nodes.emplace(std::move(cnode), InstanceList()); if (emplaced.second) { - const_cast(cnode.get())->accept(analyzeVisitor); // const-trickery required because there is no const version of NodeVisitor + analyzeVisitor.mDistances = LODRange{ smallestDistanceToChunk, higherDistanceToChunk } / ref.mScale; + const osg::Node* const nodePtr = emplaced.first->first.get(); + // const-trickery required because there is no const version of NodeVisitor + const_cast(nodePtr)->accept(analyzeVisitor); emplaced.first->second.mAnalyzeResult = analyzeVisitor.retrieveResult(); - emplaced.first->second.mNeedCompile = compile && cnode->referenceCount() <= 3; + emplaced.first->second.mNeedCompile = compile && nodePtr->referenceCount() <= 2; } else analyzeVisitor.addInstance(emplaced.first->second.mAnalyzeResult); emplaced.first->second.mInstances.push_back(&ref); } + const osg::Vec3f worldCenter = osg::Vec3f(center.x(), center.y(), 0) * getCellSize(mWorldspace); osg::ref_ptr group = new osg::Group; osg::ref_ptr mergeGroup = new osg::Group; osg::ref_ptr templateRefs = new Resource::TemplateMultiRef; @@ -572,38 +727,63 @@ namespace MWRender const AnalyzeVisitor::Result& analyzeResult = pair.second.mAnalyzeResult; - float mergeCost = analyzeResult.mNumVerts * size; - float mergeBenefit = analyzeVisitor.getMergeBenefit(analyzeResult) * mMergeFactor; - bool merge = mergeBenefit > mergeCost; + const float mergeCost = analyzeResult.mNumVerts * size; + const float mergeBenefit = analyzeVisitor.getMergeBenefit(analyzeResult) * mMergeFactor; + const bool merge = mergeBenefit > mergeCost; - float minSizeMerged = mMinSize; - float factor2 = mergeBenefit > 0 ? std::min(1.f, mergeCost * mMinSizeCostMultiplier / mergeBenefit) : 1; - float minSizeMergeFactor2 = (1-factor2) * mMinSizeMergeFactor + factor2; - if (minSizeMergeFactor2 > 0) - minSizeMerged *= minSizeMergeFactor2; + const float factor2 + = mergeBenefit > 0 ? std::min(1.f, mergeCost * mMinSizeCostMultiplier / mergeBenefit) : 1; + const float minSizeMergeFactor2 = (1 - factor2) * mMinSizeMergeFactor + factor2; + const float minSizeMerged = minSizeMergeFactor2 > 0 ? mMinSize * minSizeMergeFactor2 : mMinSize; unsigned int numinstances = 0; - for (auto cref : pair.second.mInstances) + for (const PagedCellRef* refPtr : pair.second.mInstances) { - const ESM::CellRef& ref = *cref; - osg::Vec3f pos = ref.mPos.asVec3(); + const PagedCellRef& ref = *refPtr; - if (!activeGrid && minSizeMerged != minSize && cnode->getBound().radius2() * cref->mScale*cref->mScale < (viewPoint-pos).length2()*minSizeMerged*minSizeMerged) + if (!activeGrid && minSizeMerged != minSize + && cnode->getBound().radius2() * ref.mScale * ref.mScale + < (viewPoint - ref.mPosition).length2() * minSizeMerged * minSizeMerged) continue; - osg::Matrixf matrix; - matrix.preMultTranslate(pos - worldCenter); - matrix.preMultRotate( osg::Quat(ref.mPos.rot[2], osg::Vec3f(0,0,-1)) * - osg::Quat(ref.mPos.rot[1], osg::Vec3f(0,-1,0)) * - osg::Quat(ref.mPos.rot[0], osg::Vec3f(-1,0,0)) ); - matrix.preMultScale(osg::Vec3f(ref.mScale, ref.mScale, ref.mScale)); - osg::ref_ptr trans = new osg::MatrixTransform(matrix); - trans->setDataVariance(osg::Object::STATIC); - - copyop.setCopyFlags(merge ? osg::CopyOp::DEEP_COPY_NODES|osg::CopyOp::DEEP_COPY_DRAWABLES : osg::CopyOp::DEEP_COPY_NODES); - copyop.mOptimizeBillboards = (size > 1/4.f); + const osg::Vec3f nodePos = ref.mPosition - worldCenter; + const osg::Quat nodeAttitude = osg::Quat(ref.mRotation.z(), osg::Vec3f(0, 0, -1)) + * osg::Quat(ref.mRotation.y(), osg::Vec3f(0, -1, 0)) + * osg::Quat(ref.mRotation.x(), osg::Vec3f(-1, 0, 0)); + const osg::Vec3f nodeScale(ref.mScale, ref.mScale, ref.mScale); + + osg::ref_ptr trans; + if (merge) + { + // Optimizer currently supports only MatrixTransforms. + osg::Matrixf matrix; + matrix.preMultTranslate(nodePos); + matrix.preMultRotate(nodeAttitude); + matrix.preMultScale(nodeScale); + trans = new osg::MatrixTransform(matrix); + trans->setDataVariance(osg::Object::STATIC); + } + else + { + trans = new SceneUtil::PositionAttitudeTransform; + SceneUtil::PositionAttitudeTransform* pat + = static_cast(trans.get()); + pat->setPosition(nodePos); + pat->setScale(nodeScale); + pat->setAttitude(nodeAttitude); + } + + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is + // generally unsafe. In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must + // outlive the cloned geometry regardless. (ensured by TemplateMultiRef) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing + // BufferObjects of the original geometry. (ensured by needvbo() in optimizer.cpp) + copyop.setCopyFlags(merge ? osg::CopyOp::DEEP_COPY_NODES | osg::CopyOp::DEEP_COPY_DRAWABLES + : osg::CopyOp::DEEP_COPY_NODES); + copyop.mOptimizeBillboards = (size > 1 / 4.f); copyop.mNodePath.push_back(trans); - copyop.mSqrDistance = (viewPoint - pos).length2(); + copyop.mDistances = LODRange{ smallestDistanceToChunk, higherDistanceToChunk } / ref.mScale; copyop.mViewVector = (viewPoint - worldCenter); copyop.copy(cnode, trans); copyop.mNodePath.pop_back(); @@ -617,18 +797,20 @@ namespace MWRender } else { - osg::ref_ptr marker = new RefnumMarker; marker->mRefnum = ref.mRefNum; + osg::ref_ptr marker = new RefnumMarker; + marker->mRefnum = ref.mRefNum; trans->getOrCreateUserDataContainer()->addUserObject(marker); } } - osg::Group* attachTo = merge ? mergeGroup : group; + osg::Group* const attachTo = merge ? mergeGroup : group; attachTo->addChild(trans); ++numinstances; } if (numinstances > 0) { - // add a ref to the original template, to hint to the cache that it's still being used and should be kept in cache + // add a ref to the original template to help verify the safety of shallow cloning operations + // in addition, we hint to the cache that it's still being used and should be kept in cache templateRefs->addRef(cnode); if (pair.second.mNeedCompile) @@ -642,17 +824,20 @@ namespace MWRender } } + const osg::Vec3f relativeViewPoint = viewPoint - worldCenter; + if (mergeGroup->getNumChildren()) { SceneUtil::Optimizer optimizer; - if (size > 1/8.f) + if (size > 1 / 8.f) { optimizer.setViewPoint(relativeViewPoint); optimizer.setMergeAlphaBlending(true); } optimizer.setIsOperationPermissibleForObjectCallback(new CanOptimizeCallback); - unsigned int options = SceneUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS|SceneUtil::Optimizer::REMOVE_REDUNDANT_NODES|SceneUtil::Optimizer::MERGE_GEOMETRY; - mSceneManager->shareState(mergeGroup); + const unsigned int options = SceneUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS + | SceneUtil::Optimizer::REMOVE_REDUNDANT_NODES | SceneUtil::Optimizer::MERGE_GEOMETRY; + optimizer.optimize(mergeGroup, options); group->addChild(mergeGroup); @@ -669,7 +854,7 @@ namespace MWRender } } - auto ico = mSceneManager->getIncrementalCompileOperation(); + osgUtil::IncrementalCompileOperation* const ico = mSceneManager->getIncrementalCompileOperation(); if (!stateToCompile.empty() && ico) { auto compileSet = new osgUtil::IncrementalCompileOperation::CompileSet(group); @@ -682,6 +867,9 @@ namespace MWRender osg::UserDataContainer* udc = group->getOrCreateUserDataContainer(); if (activeGrid) { + std::sort(refnumSet->mRefnums.begin(), refnumSet->mRefnums.end()); + refnumSet->mRefnums.erase( + std::unique(refnumSet->mRefnums.begin(), refnumSet->mRefnums.end()), refnumSet->mRefnums.end()); udc->addUserObject(refnumSet); group->addCullCallback(new SceneUtil::LightListCallback); } @@ -695,82 +883,96 @@ namespace MWRender return Mask_Static; } - struct ClearCacheFunctor + namespace { - void operator()(MWRender::ChunkId id, osg::Object* obj) - { - if (intersects(id, mPosition)) - mToClear.insert(id); - } - bool intersects(ChunkId id, osg::Vec3f pos) + osg::Vec2f clampToCell(const osg::Vec3f& cellPos, const osg::Vec2i& cell) { - if (mActiveGridOnly && !std::get<2>(id)) return false; - pos /= ESM::Land::REAL_SIZE; - clampToCell(pos); - osg::Vec2f center = std::get<0>(id); - float halfSize = std::get<1>(id)/2; - return pos.x() >= center.x()-halfSize && pos.y() >= center.y()-halfSize && pos.x() <= center.x()+halfSize && pos.y() <= center.y()+halfSize; + return osg::Vec2f(std::clamp(cellPos.x(), cell.x(), cell.x() + 1), + std::clamp(cellPos.y(), cell.y(), cell.y() + 1)); } - void clampToCell(osg::Vec3f& cellPos) + + class CollectIntersecting { - osg::Vec2i min (mCell.x(), mCell.y()); - osg::Vec2i max (mCell.x()+1, mCell.y()+1); - if (cellPos.x() < min.x()) cellPos.x() = min.x(); - if (cellPos.x() > max.x()) cellPos.x() = max.x(); - if (cellPos.y() < min.y()) cellPos.y() = min.y(); - if (cellPos.y() > max.y()) cellPos.y() = max.y(); - } - osg::Vec3f mPosition; - osg::Vec2i mCell; - std::set mToClear; - bool mActiveGridOnly = false; - }; + public: + explicit CollectIntersecting( + bool activeGridOnly, const osg::Vec3f& position, const osg::Vec2i& cell, ESM::RefId worldspace) + : mActiveGridOnly(activeGridOnly) + , mPosition(clampToCell(position / getCellSize(worldspace), cell)) + { + } - bool ObjectPaging::enableObject(int type, const ESM::RefNum & refnum, const osg::Vec3f& pos, const osg::Vec2i& cell, bool enabled) + void operator()(const ChunkId& id, osg::Object* /*obj*/) + { + if (mActiveGridOnly && !std::get<2>(id)) + return; + if (intersects(id)) + mCollected.push_back(id); + } + + const std::vector& getCollected() const { return mCollected; } + + private: + bool intersects(ChunkId id) const + { + const osg::Vec2f center = std::get<0>(id); + const float halfSize = std::get<1>(id) / 2; + return mPosition.x() >= center.x() - halfSize && mPosition.y() >= center.y() - halfSize + && mPosition.x() <= center.x() + halfSize && mPosition.y() <= center.y() + halfSize; + } + + bool mActiveGridOnly; + osg::Vec2f mPosition; + std::vector mCollected; + }; + } + + bool ObjectPaging::enableObject( + int type, ESM::RefNum refnum, const osg::Vec3f& pos, const osg::Vec2i& cell, bool enabled) { if (!typeFilter(type, false)) return false; { std::lock_guard lock(mRefTrackerMutex); - if (enabled && !getWritableRefTracker().mDisabled.erase(refnum)) return false; - if (!enabled && !getWritableRefTracker().mDisabled.insert(refnum).second) return false; - if (mRefTrackerLocked) return false; + if (enabled && !getWritableRefTracker().mDisabled.erase(refnum)) + return false; + if (!enabled && !getWritableRefTracker().mDisabled.insert(refnum).second) + return false; + if (mRefTrackerLocked) + return false; } - ClearCacheFunctor ccf; - ccf.mPosition = pos; - ccf.mCell = cell; + CollectIntersecting ccf(false, pos, cell, mWorldspace); mCache->call(ccf); - if (ccf.mToClear.empty()) return false; - for (const auto& chunk : ccf.mToClear) + if (ccf.getCollected().empty()) + return false; + for (const ChunkId& chunk : ccf.getCollected()) mCache->removeFromObjectCache(chunk); return true; } - bool ObjectPaging::blacklistObject(int type, const ESM::RefNum & refnum, const osg::Vec3f& pos, const osg::Vec2i& cell) + bool ObjectPaging::blacklistObject(int type, ESM::RefNum refnum, const osg::Vec3f& pos, const osg::Vec2i& cell) { if (!typeFilter(type, false)) return false; { std::lock_guard lock(mRefTrackerMutex); - if (!getWritableRefTracker().mBlacklist.insert(refnum).second) return false; - if (mRefTrackerLocked) return false; + if (!getWritableRefTracker().mBlacklist.insert(refnum).second) + return false; + if (mRefTrackerLocked) + return false; } - ClearCacheFunctor ccf; - ccf.mPosition = pos; - ccf.mCell = cell; - ccf.mActiveGridOnly = true; + CollectIntersecting ccf(true, pos, cell, mWorldspace); mCache->call(ccf); - if (ccf.mToClear.empty()) return false; - for (const auto& chunk : ccf.mToClear) + if (ccf.getCollected().empty()) + return false; + for (const ChunkId& chunk : ccf.getCollected()) mCache->removeFromObjectCache(chunk); return true; } - void ObjectPaging::clear() { std::lock_guard lock(mRefTrackerMutex); @@ -781,7 +983,8 @@ namespace MWRender bool ObjectPaging::unlockCache() { - if (!mRefTrackerLocked) return false; + if (!mRefTrackerLocked) + return false; { std::lock_guard lock(mRefTrackerMutex); mRefTrackerLocked = false; @@ -794,38 +997,50 @@ namespace MWRender return true; } - struct GetRefnumsFunctor + namespace { - GetRefnumsFunctor(std::set& output) : mOutput(output) {} - void operator()(MWRender::ChunkId chunkId, osg::Object* obj) + struct GetRefnumsFunctor { - if (!std::get<2>(chunkId)) return; - const osg::Vec2f& center = std::get<0>(chunkId); - bool activeGrid = (center.x() > mActiveGrid.x() || center.y() > mActiveGrid.y() || center.x() < mActiveGrid.z() || center.y() < mActiveGrid.w()); - if (!activeGrid) return; - - osg::UserDataContainer* udc = obj->getUserDataContainer(); - if (udc && udc->getNumUserObjects()) + GetRefnumsFunctor(std::vector& output) + : mOutput(output) { - RefnumSet* refnums = dynamic_cast(udc->getUserObject(0)); - if (!refnums) return; - mOutput.insert(refnums->mRefnums.begin(), refnums->mRefnums.end()); } - } - osg::Vec4i mActiveGrid; - std::set& mOutput; - }; + void operator()(MWRender::ChunkId chunkId, osg::Object* obj) + { + if (!std::get<2>(chunkId)) + return; + const osg::Vec2f& center = std::get<0>(chunkId); + const bool activeGrid = (center.x() > mActiveGrid.x() || center.y() > mActiveGrid.y() + || center.x() < mActiveGrid.z() || center.y() < mActiveGrid.w()); + if (!activeGrid) + return; + + osg::UserDataContainer* udc = obj->getUserDataContainer(); + if (udc && udc->getNumUserObjects()) + { + RefnumSet* refnums = dynamic_cast(udc->getUserObject(0)); + if (!refnums) + return; + mOutput.insert(mOutput.end(), refnums->mRefnums.begin(), refnums->mRefnums.end()); + } + } + osg::Vec4i mActiveGrid; + std::vector& mOutput; + }; + } - void ObjectPaging::getPagedRefnums(const osg::Vec4i &activeGrid, std::set &out) + void ObjectPaging::getPagedRefnums(const osg::Vec4i& activeGrid, std::vector& out) { GetRefnumsFunctor grf(out); grf.mActiveGrid = activeGrid; mCache->call(grf); + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); } - void ObjectPaging::reportStats(unsigned int frameNumber, osg::Stats *stats) const + void ObjectPaging::reportStats(unsigned int frameNumber, osg::Stats* stats) const { - stats->setAttribute(frameNumber, "Object Chunk", mCache->getCacheSize()); + Resource::reportStats("Object Chunk", frameNumber, mCache->getStats(), *stats); } } diff --git a/apps/openmw/mwrender/objectpaging.hpp b/apps/openmw/mwrender/objectpaging.hpp index c24cdf4f8de..11be6009ca5 100644 --- a/apps/openmw/mwrender/objectpaging.hpp +++ b/apps/openmw/mwrender/objectpaging.hpp @@ -1,9 +1,9 @@ #ifndef OPENMW_MWRENDER_OBJECTPAGING_H #define OPENMW_MWRENDER_OBJECTPAGING_H -#include +#include #include -#include +#include #include @@ -11,10 +11,6 @@ namespace Resource { class SceneManager; } -namespace MWWorld -{ - class ESMStore; -} namespace MWRender { @@ -24,20 +20,22 @@ namespace MWRender class ObjectPaging : public Resource::GenericResourceManager, public Terrain::QuadTreeWorld::ChunkManager { public: - ObjectPaging(Resource::SceneManager* sceneManager); + ObjectPaging(Resource::SceneManager* sceneManager, ESM::RefId worldspace); ~ObjectPaging() = default; - osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override; + osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, + bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override; - osg::ref_ptr createChunk(float size, const osg::Vec2f& center, bool activeGrid, const osg::Vec3f& viewPoint, bool compile); + osg::ref_ptr createChunk(float size, const osg::Vec2f& center, bool activeGrid, + const osg::Vec3f& viewPoint, bool compile, unsigned char lod); unsigned int getNodeMask() override; /// @return true if view needs rebuild - bool enableObject(int type, const ESM::RefNum & refnum, const osg::Vec3f& pos, const osg::Vec2i& cell, bool enabled); + bool enableObject(int type, ESM::RefNum refnum, const osg::Vec3f& pos, const osg::Vec2i& cell, bool enabled); /// @return true if view needs rebuild - bool blacklistObject(int type, const ESM::RefNum & refnum, const osg::Vec3f& pos, const osg::Vec2i& cell); + bool blacklistObject(int type, ESM::RefNum refnum, const osg::Vec3f& pos, const osg::Vec2i& cell); void clear(); @@ -47,7 +45,7 @@ namespace MWRender void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; - void getPagedRefnums(const osg::Vec4i &activeGrid, std::set &out); + void getPagedRefnums(const osg::Vec4i& activeGrid, std::vector& out); private: Resource::SceneManager* mSceneManager; @@ -63,7 +61,10 @@ namespace MWRender { std::set mDisabled; std::set mBlacklist; - bool operator==(const RefTracker&other) const { return mDisabled == other.mDisabled && mBlacklist == other.mBlacklist; } + bool operator==(const RefTracker& other) const + { + return mDisabled == other.mDisabled && mBlacklist == other.mBlacklist; + } }; RefTracker mRefTracker; RefTracker mRefTrackerNew; @@ -75,13 +76,25 @@ namespace MWRender std::mutex mSizeCacheMutex; typedef std::map SizeCache; SizeCache mSizeCache; + + std::mutex mLODNameCacheMutex; + typedef std::pair LODNameCacheKey; // Key: mesh name, lod level + typedef std::map LODNameCache; // Cache: key, mesh name to use + LODNameCache mLODNameCache; }; class RefnumMarker : public osg::Object { public: - RefnumMarker() : mNumVertices(0) { mRefnum.unset(); } - RefnumMarker(const RefnumMarker ©, osg::CopyOp co) : mRefnum(copy.mRefnum), mNumVertices(copy.mNumVertices) {} + RefnumMarker() + : mNumVertices(0) + { + } + RefnumMarker(const RefnumMarker& copy, osg::CopyOp co) + : mRefnum(copy.mRefnum) + , mNumVertices(copy.mNumVertices) + { + } META_Object(MWRender, RefnumMarker) ESM::RefNum mRefnum; diff --git a/apps/openmw/mwrender/objects.cpp b/apps/openmw/mwrender/objects.cpp index ec1c4397bfd..d93dc476419 100644 --- a/apps/openmw/mwrender/objects.cpp +++ b/apps/openmw/mwrender/objects.cpp @@ -3,230 +3,250 @@ #include #include +#include +#include #include #include -#include "../mwworld/ptr.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/ptr.hpp" #include "animation.hpp" -#include "npcanimation.hpp" #include "creatureanimation.hpp" +#include "esm4npcanimation.hpp" +#include "npcanimation.hpp" #include "vismask.hpp" - namespace MWRender { -Objects::Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode, SceneUtil::UnrefQueue* unrefQueue) - : mRootNode(rootNode) - , mResourceSystem(resourceSystem) - , mUnrefQueue(unrefQueue) -{ -} - -Objects::~Objects() -{ - mObjects.clear(); - - for (CellMap::iterator iter = mCellSceneNodes.begin(); iter != mCellSceneNodes.end(); ++iter) - iter->second->getParent(0)->removeChild(iter->second); - mCellSceneNodes.clear(); -} + Objects::Objects(Resource::ResourceSystem* resourceSystem, const osg::ref_ptr& rootNode, + SceneUtil::UnrefQueue& unrefQueue) + : mRootNode(rootNode) + , mResourceSystem(resourceSystem) + , mUnrefQueue(unrefQueue) + { + } -void Objects::insertBegin(const MWWorld::Ptr& ptr) -{ - assert(mObjects.find(ptr) == mObjects.end()); + Objects::~Objects() + { + mObjects.clear(); - osg::ref_ptr cellnode; + for (CellMap::iterator iter = mCellSceneNodes.begin(); iter != mCellSceneNodes.end(); ++iter) + iter->second->getParent(0)->removeChild(iter->second); + mCellSceneNodes.clear(); + } - CellMap::iterator found = mCellSceneNodes.find(ptr.getCell()); - if (found == mCellSceneNodes.end()) + void Objects::insertBegin(const MWWorld::Ptr& ptr) { - cellnode = new osg::Group; - cellnode->setName("Cell Root"); - mRootNode->addChild(cellnode); - mCellSceneNodes[ptr.getCell()] = cellnode; - } - else - cellnode = found->second; + assert(mObjects.find(ptr.mRef) == mObjects.end()); - osg::ref_ptr insert (new SceneUtil::PositionAttitudeTransform); - cellnode->addChild(insert); + osg::ref_ptr cellnode; - insert->getOrCreateUserDataContainer()->addUserObject(new PtrHolder(ptr)); + CellMap::iterator found = mCellSceneNodes.find(ptr.getCell()); + if (found == mCellSceneNodes.end()) + { + cellnode = new osg::Group; + cellnode->setName("Cell Root"); + mRootNode->addChild(cellnode); + mCellSceneNodes[ptr.getCell()] = cellnode; + } + else + cellnode = found->second; - const float *f = ptr.getRefData().getPosition().pos; + osg::ref_ptr insert(new SceneUtil::PositionAttitudeTransform); + cellnode->addChild(insert); - insert->setPosition(osg::Vec3(f[0], f[1], f[2])); + insert->getOrCreateUserDataContainer()->addUserObject(new PtrHolder(ptr)); - const float scale = ptr.getCellRef().getScale(); - osg::Vec3f scaleVec(scale, scale, scale); - ptr.getClass().adjustScale(ptr, scaleVec, true); - insert->setScale(scaleVec); + const float* f = ptr.getRefData().getPosition().pos; - ptr.getRefData().setBaseNode(insert); -} + insert->setPosition(osg::Vec3(f[0], f[1], f[2])); -void Objects::insertModel(const MWWorld::Ptr &ptr, const std::string &mesh, bool animated, bool allowLight) -{ - insertBegin(ptr); - ptr.getRefData().getBaseNode()->setNodeMask(Mask_Object); + const float scale = ptr.getCellRef().getScale(); + osg::Vec3f scaleVec(scale, scale, scale); + ptr.getClass().adjustScale(ptr, scaleVec, true); + insert->setScale(scaleVec); - osg::ref_ptr anim (new ObjectAnimation(ptr, mesh, mResourceSystem, animated, allowLight)); + ptr.getRefData().setBaseNode(std::move(insert)); + } - mObjects.insert(std::make_pair(ptr, anim)); -} + void Objects::insertModel(const MWWorld::Ptr& ptr, const std::string& mesh, bool allowLight) + { + insertBegin(ptr); + ptr.getRefData().getBaseNode()->setNodeMask(Mask_Object); + bool animated = ptr.getClass().useAnim(); + std::string animationMesh = mesh; + if (animated && !mesh.empty()) + { + animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); + if (animationMesh == mesh && Misc::StringUtils::ciEndsWith(animationMesh, ".nif")) + animated = false; + } -void Objects::insertCreature(const MWWorld::Ptr &ptr, const std::string &mesh, bool weaponsShields) -{ - insertBegin(ptr); - ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); + osg::ref_ptr anim( + new ObjectAnimation(ptr, animationMesh, mResourceSystem, animated, allowLight)); - // CreatureAnimation - osg::ref_ptr anim; + mObjects.emplace(ptr.mRef, std::move(anim)); + } - if (weaponsShields) - anim = new CreatureWeaponAnimation(ptr, mesh, mResourceSystem); - else - anim = new CreatureAnimation(ptr, mesh, mResourceSystem); + void Objects::insertCreature(const MWWorld::Ptr& ptr, const std::string& mesh, bool weaponsShields) + { + insertBegin(ptr); + ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); - if (mObjects.insert(std::make_pair(ptr, anim)).second) - ptr.getClass().getContainerStore(ptr).setContListener(static_cast(anim.get())); -} + bool animated = true; + std::string animationMesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()); + if (animationMesh == mesh && Misc::StringUtils::ciEndsWith(animationMesh, ".nif")) + animated = false; -void Objects::insertNPC(const MWWorld::Ptr &ptr) -{ - insertBegin(ptr); - ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); + // CreatureAnimation + osg::ref_ptr anim; - osg::ref_ptr anim (new NpcAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), mResourceSystem)); + if (weaponsShields) + anim = new CreatureWeaponAnimation(ptr, animationMesh, mResourceSystem, animated); + else + anim = new CreatureAnimation(ptr, animationMesh, mResourceSystem, animated); - if (mObjects.insert(std::make_pair(ptr, anim)).second) - { - ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get(), ptr); - ptr.getClass().getInventoryStore(ptr).setContListener(anim.get()); + if (mObjects.emplace(ptr.mRef, anim).second) + ptr.getClass().getContainerStore(ptr).setContListener(static_cast(anim.get())); } -} -bool Objects::removeObject (const MWWorld::Ptr& ptr) -{ - if(!ptr.getRefData().getBaseNode()) - return true; - - PtrAnimationMap::iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) + void Objects::insertNPC(const MWWorld::Ptr& ptr) { - if (mUnrefQueue.get()) - mUnrefQueue->push(iter->second); - - mObjects.erase(iter); + insertBegin(ptr); + ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); - if (ptr.getClass().isActor()) + if (ptr.getType() == ESM::REC_NPC_4) { - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).setInvListener(nullptr, ptr); - - ptr.getClass().getContainerStore(ptr).setContListener(nullptr); + osg::ref_ptr anim( + new ESM4NpcAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), mResourceSystem)); + mObjects.emplace(ptr.mRef, anim); } + else + { + osg::ref_ptr anim( + new NpcAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), mResourceSystem)); - ptr.getRefData().getBaseNode()->getParent(0)->removeChild(ptr.getRefData().getBaseNode()); - - ptr.getRefData().setBaseNode(nullptr); - return true; + if (mObjects.emplace(ptr.mRef, anim).second) + { + ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get()); + ptr.getClass().getInventoryStore(ptr).setContListener(anim.get()); + } + } } - return false; -} - -void Objects::removeCell(const MWWorld::CellStore* store) -{ - for(PtrAnimationMap::iterator iter = mObjects.begin();iter != mObjects.end();) + bool Objects::removeObject(const MWWorld::Ptr& ptr) { - MWWorld::Ptr ptr = iter->second->getPtr(); - if(ptr.getCell() == store) + if (!ptr.getRefData().getBaseNode()) + return true; + + const auto iter = mObjects.find(ptr.mRef); + if (iter != mObjects.end()) { - if (mUnrefQueue.get()) - mUnrefQueue->push(iter->second); + iter->second->removeFromScene(); + mUnrefQueue.push(std::move(iter->second)); + mObjects.erase(iter); - if (ptr.getClass().isNpc() && ptr.getRefData().getCustomData()) + if (ptr.getClass().isActor()) { - MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); - invStore.setInvListener(nullptr, ptr); - invStore.setContListener(nullptr); + if (ptr.getClass().hasInventoryStore(ptr)) + ptr.getClass().getInventoryStore(ptr).setInvListener(nullptr); + + ptr.getClass().getContainerStore(ptr).setContListener(nullptr); } - mObjects.erase(iter++); + ptr.getRefData().getBaseNode()->getParent(0)->removeChild(ptr.getRefData().getBaseNode()); + + ptr.getRefData().setBaseNode(nullptr); + return true; } - else - ++iter; + return false; } - CellMap::iterator cell = mCellSceneNodes.find(store); - if(cell != mCellSceneNodes.end()) + void Objects::removeCell(const MWWorld::CellStore* store) { - cell->second->getParent(0)->removeChild(cell->second); - if (mUnrefQueue.get()) - mUnrefQueue->push(cell->second); - mCellSceneNodes.erase(cell); - } -} + for (PtrAnimationMap::iterator iter = mObjects.begin(); iter != mObjects.end();) + { + MWWorld::Ptr ptr = iter->second->getPtr(); + if (ptr.getCell() == store) + { + if (ptr.getClass().isActor() && ptr.getRefData().getCustomData()) + { + if (ptr.getClass().hasInventoryStore(ptr)) + ptr.getClass().getInventoryStore(ptr).setInvListener(nullptr); + ptr.getClass().getContainerStore(ptr).setContListener(nullptr); + } + + iter->second->removeFromScene(); + mUnrefQueue.push(std::move(iter->second)); + iter = mObjects.erase(iter); + } + else + ++iter; + } -void Objects::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &cur) -{ - osg::Node* objectNode = cur.getRefData().getBaseNode(); - if (!objectNode) - return; - - MWWorld::CellStore *newCell = cur.getCell(); - - osg::Group* cellnode; - if(mCellSceneNodes.find(newCell) == mCellSceneNodes.end()) { - cellnode = new osg::Group; - mRootNode->addChild(cellnode); - mCellSceneNodes[newCell] = cellnode; - } else { - cellnode = mCellSceneNodes[newCell]; + CellMap::iterator cell = mCellSceneNodes.find(store); + if (cell != mCellSceneNodes.end()) + { + cell->second->getParent(0)->removeChild(cell->second); + mCellSceneNodes.erase(cell); + } } - osg::UserDataContainer* userDataContainer = objectNode->getUserDataContainer(); - if (userDataContainer) - for (unsigned int i=0; igetNumUserObjects(); ++i) + void Objects::updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& cur) + { + osg::ref_ptr objectNode = cur.getRefData().getBaseNode(); + if (!objectNode) + return; + + MWWorld::CellStore* newCell = cur.getCell(); + + osg::Group* cellnode; + if (mCellSceneNodes.find(newCell) == mCellSceneNodes.end()) + { + cellnode = new osg::Group; + mRootNode->addChild(cellnode); + mCellSceneNodes[newCell] = cellnode; + } + else { - if (dynamic_cast(userDataContainer->getUserObject(i))) - userDataContainer->setUserObject(i, new PtrHolder(cur)); + cellnode = mCellSceneNodes[newCell]; } - if (objectNode->getNumParents()) - objectNode->getParent(0)->removeChild(objectNode); - cellnode->addChild(objectNode); + osg::UserDataContainer* userDataContainer = objectNode->getUserDataContainer(); + if (userDataContainer) + for (unsigned int i = 0; i < userDataContainer->getNumUserObjects(); ++i) + { + if (dynamic_cast(userDataContainer->getUserObject(i))) + userDataContainer->setUserObject(i, new PtrHolder(cur)); + } - PtrAnimationMap::iterator iter = mObjects.find(old); - if(iter != mObjects.end()) - { - osg::ref_ptr anim = iter->second; - mObjects.erase(iter); - anim->updatePtr(cur); - mObjects[cur] = anim; + if (objectNode->getNumParents()) + objectNode->getParent(0)->removeChild(objectNode); + cellnode->addChild(objectNode); + + PtrAnimationMap::iterator iter = mObjects.find(old.mRef); + if (iter != mObjects.end()) + iter->second->updatePtr(cur); } -} -Animation* Objects::getAnimation(const MWWorld::Ptr &ptr) -{ - PtrAnimationMap::const_iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) - return iter->second; + Animation* Objects::getAnimation(const MWWorld::Ptr& ptr) + { + PtrAnimationMap::const_iterator iter = mObjects.find(ptr.mRef); + if (iter != mObjects.end()) + return iter->second; - return nullptr; -} + return nullptr; + } -const Animation* Objects::getAnimation(const MWWorld::ConstPtr &ptr) const -{ - PtrAnimationMap::const_iterator iter = mObjects.find(ptr); - if(iter != mObjects.end()) - return iter->second; + const Animation* Objects::getAnimation(const MWWorld::ConstPtr& ptr) const + { + PtrAnimationMap::const_iterator iter = mObjects.find(ptr.mRef); + if (iter != mObjects.end()) + return iter->second; - return nullptr; -} + return nullptr; + } } diff --git a/apps/openmw/mwrender/objects.hpp b/apps/openmw/mwrender/objects.hpp index 98ebb95d988..b4359fdb99d 100644 --- a/apps/openmw/mwrender/objects.hpp +++ b/apps/openmw/mwrender/objects.hpp @@ -4,8 +4,8 @@ #include #include -#include #include +#include #include "../mwworld/ptr.hpp" @@ -29,72 +29,69 @@ namespace SceneUtil class UnrefQueue; } -namespace MWRender{ - -class Animation; - -class PtrHolder : public osg::Object +namespace MWRender { -public: - PtrHolder(const MWWorld::Ptr& ptr) - : mPtr(ptr) - { - } - PtrHolder() - { - } + class Animation; - PtrHolder(const PtrHolder& copy, const osg::CopyOp& copyop) - : mPtr(copy.mPtr) + class PtrHolder : public osg::Object { - } + public: + PtrHolder(const MWWorld::Ptr& ptr) + : mPtr(ptr) + { + } - META_Object(MWRender, PtrHolder) + PtrHolder() {} - MWWorld::Ptr mPtr; -}; + PtrHolder(const PtrHolder& copy, const osg::CopyOp& copyop) + : mPtr(copy.mPtr) + { + } -class Objects{ - typedef std::map > PtrAnimationMap; + META_Object(MWRender, PtrHolder) - typedef std::map > CellMap; - CellMap mCellSceneNodes; - PtrAnimationMap mObjects; + MWWorld::Ptr mPtr; + }; - osg::ref_ptr mRootNode; - - Resource::ResourceSystem* mResourceSystem; + class Objects + { + using PtrAnimationMap = std::map>; - osg::ref_ptr mUnrefQueue; + typedef std::map> CellMap; + CellMap mCellSceneNodes; + PtrAnimationMap mObjects; + osg::ref_ptr mRootNode; + Resource::ResourceSystem* mResourceSystem; + SceneUtil::UnrefQueue& mUnrefQueue; - void insertBegin(const MWWorld::Ptr& ptr); + void insertBegin(const MWWorld::Ptr& ptr); -public: - Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode, SceneUtil::UnrefQueue* unrefQueue); - ~Objects(); + public: + Objects(Resource::ResourceSystem* resourceSystem, const osg::ref_ptr& rootNode, + SceneUtil::UnrefQueue& unrefQueue); + ~Objects(); - /// @param animated Attempt to load separate keyframes from a .kf file matching the model file? - /// @param allowLight If false, no lights will be created, and particles systems will be removed. - void insertModel(const MWWorld::Ptr& ptr, const std::string &model, bool animated=false, bool allowLight=true); + /// @param allowLight If false, no lights will be created, and particles systems will be removed. + void insertModel(const MWWorld::Ptr& ptr, const std::string& model, bool allowLight = true); - void insertNPC(const MWWorld::Ptr& ptr); - void insertCreature (const MWWorld::Ptr& ptr, const std::string& model, bool weaponsShields); + void insertNPC(const MWWorld::Ptr& ptr); + void insertCreature(const MWWorld::Ptr& ptr, const std::string& model, bool weaponsShields); - Animation* getAnimation(const MWWorld::Ptr &ptr); - const Animation* getAnimation(const MWWorld::ConstPtr &ptr) const; + Animation* getAnimation(const MWWorld::Ptr& ptr); + const Animation* getAnimation(const MWWorld::ConstPtr& ptr) const; - bool removeObject (const MWWorld::Ptr& ptr); - ///< \return found? + bool removeObject(const MWWorld::Ptr& ptr); + ///< \return found? - void removeCell(const MWWorld::CellStore* store); + void removeCell(const MWWorld::CellStore* store); - /// Updates containing cell for object rendering data - void updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &cur); + /// Updates containing cell for object rendering data + void updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& cur); -private: - void operator = (const Objects&); - Objects(const Objects&); -}; + private: + void operator=(const Objects&); + Objects(const Objects&); + }; } #endif diff --git a/apps/openmw/mwrender/pathgrid.cpp b/apps/openmw/mwrender/pathgrid.cpp index c20e81bb2d4..a39ba86a60c 100644 --- a/apps/openmw/mwrender/pathgrid.cpp +++ b/apps/openmw/mwrender/pathgrid.cpp @@ -3,150 +3,164 @@ #include #include -#include #include +#include -#include -#include +#include +#include #include +#include +#include +#include -#include "../mwbase/world.hpp" // these includes can be removed once the static-hack is gone #include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" // these includes can be removed once the static-hack is gone +#include "../mwmechanics/pathfinding.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/pathfinding.hpp" #include "vismask.hpp" namespace MWRender { -Pathgrid::Pathgrid(osg::ref_ptr root) - : mPathgridEnabled(false) - , mRootNode(root) - , mPathGridRoot(nullptr) - , mInteriorPathgridNode(nullptr) -{ -} - -Pathgrid::~Pathgrid() -{ - if (mPathgridEnabled) + Pathgrid::Pathgrid(osg::ref_ptr root) + : mPathgridEnabled(false) + , mRootNode(std::move(root)) + , mPathGridRoot(nullptr) + , mInteriorPathgridNode(nullptr) { - togglePathgrid(); } -} - -bool Pathgrid::toggleRenderMode (int mode){ - switch (mode) + Pathgrid::~Pathgrid() { - case Render_Pathgrid: - togglePathgrid(); - return mPathgridEnabled; - default: - return false; + if (mPathgridEnabled) + { + try + { + togglePathgrid(); + } + catch (std::exception& e) + { + Log(Debug::Error) << "Failed to destroy pathgrid: " << e.what(); + } + } } - return false; -} - -void Pathgrid::addCell(const MWWorld::CellStore *store) -{ - mActiveCells.push_back(store); - if (mPathgridEnabled) - enableCellPathgrid(store); -} + bool Pathgrid::toggleRenderMode(int mode) + { + switch (mode) + { + case Render_Pathgrid: + togglePathgrid(); + return mPathgridEnabled; + default: + return false; + } -void Pathgrid::removeCell(const MWWorld::CellStore *store) -{ - mActiveCells.erase(std::remove(mActiveCells.begin(), mActiveCells.end(), store), mActiveCells.end()); - if (mPathgridEnabled) - disableCellPathgrid(store); -} + return false; + } -void Pathgrid::togglePathgrid() -{ - mPathgridEnabled = !mPathgridEnabled; - if (mPathgridEnabled) + void Pathgrid::addCell(const MWWorld::CellStore* store) { - // add path grid meshes to already loaded cells - mPathGridRoot = new osg::Group; - mPathGridRoot->setNodeMask(Mask_Debug); - mRootNode->addChild(mPathGridRoot); + mActiveCells.push_back(store); + if (mPathgridEnabled) + enableCellPathgrid(store); + } - for(const MWWorld::CellStore* cell : mActiveCells) - { - enableCellPathgrid(cell); - } + void Pathgrid::removeCell(const MWWorld::CellStore* store) + { + mActiveCells.erase(std::remove(mActiveCells.begin(), mActiveCells.end(), store), mActiveCells.end()); + if (mPathgridEnabled) + disableCellPathgrid(store); } - else + + void Pathgrid::togglePathgrid() { - // remove path grid meshes from already loaded cells - for(const MWWorld::CellStore* cell : mActiveCells) + mPathgridEnabled = !mPathgridEnabled; + if (mPathgridEnabled) { - disableCellPathgrid(cell); + // add path grid meshes to already loaded cells + mPathGridRoot = new osg::Group; + mPathGridRoot->setNodeMask(Mask_Debug); + mRootNode->addChild(mPathGridRoot); + + for (const MWWorld::CellStore* cell : mActiveCells) + { + enableCellPathgrid(cell); + } } - - if (mPathGridRoot) + else { - mRootNode->removeChild(mPathGridRoot); - mPathGridRoot = nullptr; + // remove path grid meshes from already loaded cells + for (const MWWorld::CellStore* cell : mActiveCells) + { + disableCellPathgrid(cell); + } + + if (mPathGridRoot) + { + mRootNode->removeChild(mPathGridRoot); + mPathGridRoot = nullptr; + } } } -} -void Pathgrid::enableCellPathgrid(const MWWorld::CellStore *store) -{ - MWBase::World* world = MWBase::Environment::get().getWorld(); - const ESM::Pathgrid *pathgrid = - world->getStore().get().search(*store->getCell()); - if (!pathgrid) return; + void Pathgrid::enableCellPathgrid(const MWWorld::CellStore* store) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); - osg::Vec3f cellPathGridPos(0, 0, 0); - Misc::CoordinateConverter(store->getCell()).toWorld(cellPathGridPos); + const ESM::Pathgrid* pathgrid = world->getStore().get().search(*store->getCell()); + if (!pathgrid) + return; - osg::ref_ptr cellPathGrid = new osg::PositionAttitudeTransform; - cellPathGrid->setPosition(cellPathGridPos); + osg::Vec3f cellPathGridPos(0, 0, 0); + Misc::makeCoordinateConverter(*store->getCell()).toWorld(cellPathGridPos); - osg::ref_ptr geometry = SceneUtil::createPathgridGeometry(*pathgrid); + osg::ref_ptr cellPathGrid = new osg::PositionAttitudeTransform; + cellPathGrid->setPosition(cellPathGridPos); - cellPathGrid->addChild(geometry); + osg::ref_ptr geometry = SceneUtil::createPathgridGeometry(*pathgrid); - mPathGridRoot->addChild(cellPathGrid); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(geometry, "debug"); - if (store->getCell()->isExterior()) - { - mExteriorPathgridNodes[std::make_pair(store->getCell()->getGridX(), store->getCell()->getGridY())] = cellPathGrid; - } - else - { - assert(mInteriorPathgridNode == nullptr); - mInteriorPathgridNode = cellPathGrid; - } -} + cellPathGrid->addChild(geometry); -void Pathgrid::disableCellPathgrid(const MWWorld::CellStore *store) -{ - if (store->getCell()->isExterior()) - { - ExteriorPathgridNodes::iterator it = - mExteriorPathgridNodes.find(std::make_pair(store->getCell()->getGridX(), store->getCell()->getGridY())); - if (it != mExteriorPathgridNodes.end()) + mPathGridRoot->addChild(cellPathGrid); + + if (store->getCell()->isExterior()) { - mPathGridRoot->removeChild(it->second); - mExteriorPathgridNodes.erase(it); + mExteriorPathgridNodes[std::make_pair(store->getCell()->getGridX(), store->getCell()->getGridY())] + = cellPathGrid; + } + else + { + assert(mInteriorPathgridNode == nullptr); + mInteriorPathgridNode = cellPathGrid; } } - else + + void Pathgrid::disableCellPathgrid(const MWWorld::CellStore* store) { - if (mInteriorPathgridNode) + if (store->getCell()->isExterior()) { - mPathGridRoot->removeChild(mInteriorPathgridNode); - mInteriorPathgridNode = nullptr; + ExteriorPathgridNodes::iterator it = mExteriorPathgridNodes.find( + std::make_pair(store->getCell()->getGridX(), store->getCell()->getGridY())); + if (it != mExteriorPathgridNodes.end()) + { + mPathGridRoot->removeChild(it->second); + mExteriorPathgridNodes.erase(it); + } + } + else + { + if (mInteriorPathgridNode) + { + mPathGridRoot->removeChild(mInteriorPathgridNode); + mInteriorPathgridNode = nullptr; + } } } -} } diff --git a/apps/openmw/mwrender/pathgrid.hpp b/apps/openmw/mwrender/pathgrid.hpp index 77f1a7f3373..ef815d39625 100644 --- a/apps/openmw/mwrender/pathgrid.hpp +++ b/apps/openmw/mwrender/pathgrid.hpp @@ -3,8 +3,8 @@ #include -#include #include +#include #include @@ -33,30 +33,29 @@ namespace MWRender void togglePathgrid(); - typedef std::vector CellList; + typedef std::vector CellList; CellList mActiveCells; osg::ref_ptr mRootNode; osg::ref_ptr mPathGridRoot; - typedef std::map, osg::ref_ptr > ExteriorPathgridNodes; + typedef std::map, osg::ref_ptr> ExteriorPathgridNodes; ExteriorPathgridNodes mExteriorPathgridNodes; osg::ref_ptr mInteriorPathgridNode; - void enableCellPathgrid(const MWWorld::CellStore *store); - void disableCellPathgrid(const MWWorld::CellStore *store); + void enableCellPathgrid(const MWWorld::CellStore* store); + void disableCellPathgrid(const MWWorld::CellStore* store); public: Pathgrid(osg::ref_ptr root); ~Pathgrid(); - bool toggleRenderMode (int mode); + bool toggleRenderMode(int mode); void addCell(const MWWorld::CellStore* store); void removeCell(const MWWorld::CellStore* store); }; - } #endif diff --git a/apps/openmw/mwrender/pingpongcanvas.cpp b/apps/openmw/mwrender/pingpongcanvas.cpp new file mode 100644 index 00000000000..54d8145fa99 --- /dev/null +++ b/apps/openmw/mwrender/pingpongcanvas.cpp @@ -0,0 +1,357 @@ +#include "pingpongcanvas.hpp" + +#include + +#include +#include +#include + +#include + +#include "postprocessor.hpp" + +namespace MWRender +{ + PingPongCanvas::PingPongCanvas( + Shader::ShaderManager& shaderManager, const std::shared_ptr& luminanceCalculator) + : mFallbackStateSet(new osg::StateSet) + , mMultiviewResolveStateSet(new osg::StateSet) + , mLuminanceCalculator(luminanceCalculator) + { + setUseDisplayList(false); + setUseVertexBufferObjects(true); + + osg::ref_ptr verts = new osg::Vec3Array; + verts->push_back(osg::Vec3f(-1, -1, 0)); + verts->push_back(osg::Vec3f(-1, 3, 0)); + verts->push_back(osg::Vec3f(3, -1, 0)); + + setVertexArray(verts); + + addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES, 0, 3)); + + mLuminanceCalculator->disable(); + + Shader::ShaderManager::DefineMap defines; + Stereo::shaderStereoDefines(defines); + + mFallbackProgram = shaderManager.getProgram("fullscreen_tri"); + + mFallbackStateSet->setAttributeAndModes(mFallbackProgram); + mFallbackStateSet->addUniform(new osg::Uniform("lastShader", 0)); + mFallbackStateSet->addUniform(new osg::Uniform("scaling", osg::Vec2f(1, 1))); + + mMultiviewResolveProgram = shaderManager.getProgram("multiview_resolve"); + mMultiviewResolveStateSet->setAttributeAndModes(mMultiviewResolveProgram); + mMultiviewResolveStateSet->addUniform(new osg::Uniform("lastShader", 0)); + } + + void PingPongCanvas::setPasses(fx::DispatchArray&& passes) + { + mPasses = std::move(passes); + } + + 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; + } + + void PingPongCanvas::drawGeometry(osg::RenderInfo& renderInfo) const + { + osg::Geometry::drawImplementation(renderInfo); + } + + static void attachCloneOfTemplate( + osg::FrameBufferObject* fbo, osg::Camera::BufferComponent component, osg::Texture* tex) + { + osg::ref_ptr clone = static_cast(tex->clone(osg::CopyOp::SHALLOW_COPY)); + fbo->setAttachment(component, Stereo::createMultiviewCompatibleAttachment(clone)); + } + + void PingPongCanvas::drawImplementation(osg::RenderInfo& renderInfo) const + { + osg::State& state = *renderInfo.getState(); + osg::GLExtensions* ext = state.get(); + + size_t frameId = state.getFrameStamp()->getFrameNumber() % 2; + + std::vector filtered; + + filtered.reserve(mPasses.size()); + + for (size_t i = 0; i < mPasses.size(); ++i) + { + const auto& node = mPasses[i]; + + if (mMask & node.mFlags) + continue; + + filtered.push_back(i); + } + + auto* resolveViewport = state.getCurrentViewport(); + + if (filtered.empty() || !mPostprocessing) + { + state.pushStateSet(mFallbackStateSet); + state.apply(); + + if (Stereo::getMultiview()) + { + state.pushStateSet(mMultiviewResolveStateSet); + state.apply(); + } + + state.applyTextureAttribute(0, mTextureScene); + resolveViewport->apply(state); + + drawGeometry(renderInfo); + state.popStateSet(); + + if (Stereo::getMultiview()) + { + state.popStateSet(); + } + + return; + } + + const unsigned int handle = mFbos[0] ? mFbos[0]->getHandle(state.getContextID()) : 0; + + if (handle == 0 || mDirty) + { + for (auto& fbo : mFbos) + { + fbo = new osg::FrameBufferObject; + attachCloneOfTemplate(fbo, osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, mTextureScene); + fbo->apply(state); + glClearColor(0.5, 0.5, 0.5, 1); + glClear(GL_COLOR_BUFFER_BIT); + } + + if (Stereo::getMultiview()) + { + mMultiviewResolveFramebuffer = new osg::FrameBufferObject(); + attachCloneOfTemplate(mMultiviewResolveFramebuffer, + osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, mTextureScene); + mMultiviewResolveFramebuffer->apply(state); + glClearColor(0.5, 0.5, 0.5, 1); + glClear(GL_COLOR_BUFFER_BIT); + + mMultiviewResolveStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, + (osg::Texture*)mMultiviewResolveFramebuffer->getAttachment(osg::Camera::COLOR_BUFFER0) + .getTexture()); + } + + mLuminanceCalculator->dirty(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); + + if (Stereo::getStereo()) + mRenderViewport + = new osg::Viewport(0, 0, mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); + else + mRenderViewport = nullptr; + + mDirty = false; + } + + constexpr std::array, 3> buffers + = { { { GL_COLOR_ATTACHMENT1_EXT, GL_COLOR_ATTACHMENT2_EXT }, + { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT2_EXT }, + { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT } } }; + + (mAvgLum) ? mLuminanceCalculator->enable() : mLuminanceCalculator->disable(); + + // A histogram based approach is superior way to calculate scene luminance. Using mipmaps is more broadly + // supported, so that's what we use for now. + mLuminanceCalculator->draw(*this, renderInfo, state, ext, frameId); + + auto buffer = buffers[0]; + + int lastDraw = 0; + int lastShader = 0; + + unsigned int lastApplied = handle; + + const unsigned int cid = state.getContextID(); + + const osg::ref_ptr& destinationFbo = mDestinationFBO ? mDestinationFBO : nullptr; + unsigned int destinationHandle = destinationFbo ? destinationFbo->getHandle(cid) : 0; + + auto bindDestinationFbo = [&]() { + if (destinationFbo) + { + destinationFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + lastApplied = destinationHandle; + } + else if (Stereo::getMultiview()) + { + mMultiviewResolveFramebuffer->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + lastApplied = mMultiviewResolveFramebuffer->getHandle(cid); + } + else + { + ext->glBindFramebuffer(GL_DRAW_FRAMEBUFFER_EXT, 0); + + lastApplied = 0; + } + }; + + // When textures are created (or resized) we need to either dirty them and/or clear them. + // Otherwise, there will be undefined behavior when reading from a texture that has yet to be written to in a + // later pass. + for (const auto& attachment : mDirtyAttachments) + { + const auto [w, h] + = attachment.mSize.get(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight()); + + attachment.mTarget->setTextureSize(w, h); + if (attachment.mMipMap) + attachment.mTarget->setNumMipmapLevels(osg::Image::computeNumberOfMipmapLevels(w, h)); + attachment.mTarget->dirtyTextureObject(); + + osg::ref_ptr fbo = new osg::FrameBufferObject; + + fbo->setAttachment( + osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(attachment.mTarget)); + fbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + + glViewport(0, 0, attachment.mTarget->getTextureWidth(), attachment.mTarget->getTextureHeight()); + state.haveAppliedAttribute(osg::StateAttribute::VIEWPORT); + glClearColor(attachment.mClearColor.r(), attachment.mClearColor.g(), attachment.mClearColor.b(), + attachment.mClearColor.a()); + glClear(GL_COLOR_BUFFER_BIT); + + if (attachment.mTarget->getNumMipmapLevels() > 0) + { + state.setActiveTextureUnit(0); + state.applyTextureAttribute(0, attachment.mTarget); + ext->glGenerateMipmap(GL_TEXTURE_2D); + } + } + + for (const size_t& index : filtered) + { + const auto& node = mPasses[index]; + + node.mRootStateSet->setTextureAttribute(PostProcessor::Unit_Depth, mTextureDepth); + + if (mAvgLum) + node.mRootStateSet->setTextureAttribute(PostProcessor::TextureUnits::Unit_EyeAdaptation, + mLuminanceCalculator->getLuminanceTexture(frameId)); + + if (mTextureNormals) + node.mRootStateSet->setTextureAttribute(PostProcessor::TextureUnits::Unit_Normals, mTextureNormals); + + if (mTextureDistortion) + node.mRootStateSet->setTextureAttribute( + PostProcessor::TextureUnits::Unit_Distortion, mTextureDistortion); + + state.pushStateSet(node.mRootStateSet); + state.apply(); + + for (size_t passIndex = 0; passIndex < node.mPasses.size(); ++passIndex) + { + if (mRenderViewport) + mRenderViewport->apply(state); + const auto& pass = node.mPasses[passIndex]; + + bool lastPass = passIndex == node.mPasses.size() - 1; + + // VR-TODO: This won't actually work for tex2darrays + if (lastShader == 0) + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, mTextureScene); + else + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastShader, + (osg::Texture*)mFbos[lastShader - GL_COLOR_ATTACHMENT0_EXT] + ->getAttachment(osg::Camera::COLOR_BUFFER0) + .getTexture()); + + if (lastDraw == 0) + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastPass, mTextureScene); + else + pass.mStateSet->setTextureAttribute(PostProcessor::Unit_LastPass, + (osg::Texture*)mFbos[lastDraw - GL_COLOR_ATTACHMENT0_EXT] + ->getAttachment(osg::Camera::COLOR_BUFFER0) + .getTexture()); + + if (pass.mRenderTarget) + { + 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()) + { + bindDestinationFbo(); + if (!destinationFbo && !Stereo::getMultiview()) + { + resolveViewport->apply(state); + } + } + else if (lastPass) + { + lastDraw = buffer[0]; + lastShader = buffer[0]; + mFbos[buffer[0] - GL_COLOR_ATTACHMENT0_EXT]->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + buffer = buffers[lastShader - GL_COLOR_ATTACHMENT0_EXT]; + + lastApplied = mFbos[buffer[0] - GL_COLOR_ATTACHMENT0_EXT]->getHandle(cid); + } + else + { + mFbos[buffer[0] - GL_COLOR_ATTACHMENT0_EXT]->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + lastDraw = buffer[0]; + std::swap(buffer[0], buffer[1]); + + lastApplied = mFbos[buffer[0] - GL_COLOR_ATTACHMENT0_EXT]->getHandle(cid); + } + + state.pushStateSet(pass.mStateSet); + state.apply(); + + if (!state.getLastAppliedProgramObject()) + mFallbackProgram->apply(state); + + drawGeometry(renderInfo); + + state.popStateSet(); + state.apply(); + } + + state.popStateSet(); + } + + if (Stereo::getMultiview()) + { + ext->glBindFramebuffer(GL_DRAW_FRAMEBUFFER_EXT, 0); + lastApplied = 0; + + resolveViewport->apply(state); + state.pushStateSet(mMultiviewResolveStateSet); + state.apply(); + + drawGeometry(renderInfo); + + state.popStateSet(); + state.apply(); + } + + if (lastApplied != destinationHandle) + { + bindDestinationFbo(); + } + + mDirtyAttachments.clear(); + } +} diff --git a/apps/openmw/mwrender/pingpongcanvas.hpp b/apps/openmw/mwrender/pingpongcanvas.hpp new file mode 100644 index 00000000000..5a37b7fbc92 --- /dev/null +++ b/apps/openmw/mwrender/pingpongcanvas.hpp @@ -0,0 +1,86 @@ +#ifndef OPENMW_MWRENDER_PINGPONGCANVAS_H +#define OPENMW_MWRENDER_PINGPONGCANVAS_H + +#include +#include + +#include +#include +#include + +#include + +#include "luminancecalculator.hpp" + +namespace Shader +{ + class ShaderManager; +} + +namespace MWRender +{ + class PingPongCanvas : public osg::Geometry + { + public: + PingPongCanvas( + Shader::ShaderManager& shaderManager, const std::shared_ptr& luminanceCalculator); + + void drawGeometry(osg::RenderInfo& renderInfo) const; + + void drawImplementation(osg::RenderInfo& renderInfo) const override; + + void dirty() { mDirty = true; } + + void setDirtyAttachments(const std::vector& attachments) + { + mDirtyAttachments = attachments; + } + + const fx::DispatchArray& getPasses() { return mPasses; } + + void setPasses(fx::DispatchArray&& passes); + + void setMask(bool underwater, bool exterior); + + void setTextureScene(osg::ref_ptr tex) { mTextureScene = tex; } + + void setTextureDepth(osg::ref_ptr tex) { mTextureDepth = tex; } + + void setTextureNormals(osg::ref_ptr tex) { mTextureNormals = tex; } + + void setTextureDistortion(osg::ref_ptr tex) { mTextureDistortion = tex; } + + void setCalculateAvgLum(bool enabled) { mAvgLum = enabled; } + + void setPostProcessing(bool enabled) { mPostprocessing = enabled; } + + const osg::ref_ptr& getSceneTexture(size_t frameId) const { return mTextureScene; } + + private: + bool mAvgLum = false; + bool mPostprocessing = false; + + fx::DispatchArray mPasses; + fx::FlagsType mMask = 0; + + osg::ref_ptr mFallbackProgram; + osg::ref_ptr mMultiviewResolveProgram; + osg::ref_ptr mFallbackStateSet; + osg::ref_ptr mMultiviewResolveStateSet; + + osg::ref_ptr mTextureScene; + osg::ref_ptr mTextureDepth; + osg::ref_ptr mTextureNormals; + osg::ref_ptr mTextureDistortion; + + mutable bool mDirty = false; + mutable std::vector mDirtyAttachments; + mutable osg::ref_ptr mRenderViewport; + mutable osg::ref_ptr mMultiviewResolveFramebuffer; + mutable osg::ref_ptr mDestinationFBO; + mutable std::array, 3> mFbos; + mutable std::shared_ptr mLuminanceCalculator; + }; +} + +#endif diff --git a/apps/openmw/mwrender/pingpongcull.cpp b/apps/openmw/mwrender/pingpongcull.cpp new file mode 100644 index 00000000000..497c6c734ac --- /dev/null +++ b/apps/openmw/mwrender/pingpongcull.cpp @@ -0,0 +1,83 @@ +#include "pingpongcull.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include "postprocessor.hpp" + +namespace MWRender +{ + + PingPongCull::PingPongCull(PostProcessor* pp) + : mViewportStateset(nullptr) + , mPostProcessor(pp) + { + if (Stereo::getStereo()) + { + mViewportStateset = new osg::StateSet(); + mViewport = new osg::Viewport; + mViewportStateset->setAttribute(mViewport); + } + } + + PingPongCull::~PingPongCull() + { + // Instantiate osg::ref_ptr<> destructor + } + + void PingPongCull::operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + osgUtil::RenderStage* renderStage = cv->getCurrentRenderStage(); + size_t frame = cv->getTraversalNumber(); + size_t frameId = frame % 2; + + if (Stereo::getStereo()) + { + auto& sm = Stereo::Manager::instance(); + auto view = sm.getEye(cv); + int index = view == Stereo::Eye::Right ? 1 : 0; + auto projectionMatrix = sm.computeEyeProjection(index, true); + mPostProcessor->getStateUpdater()->setProjectionMatrix(projectionMatrix); + } + + mPostProcessor->getStateUpdater()->setViewMatrix(cv->getCurrentCamera()->getViewMatrix()); + mPostProcessor->getStateUpdater()->setPrevViewMatrix(mLastViewMatrix[0]); + mLastViewMatrix[0] = cv->getCurrentCamera()->getViewMatrix(); + + mPostProcessor->getStateUpdater()->setEyePos(cv->getEyePoint()); + mPostProcessor->getStateUpdater()->setEyeVec(cv->getLookVectorLocal()); + + if (!mPostProcessor->getFbo(PostProcessor::FBO_Multisample, frameId)) + { + renderStage->setFrameBufferObject(mPostProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); + } + else + { + renderStage->setMultisampleResolveFramebufferObject( + mPostProcessor->getFbo(PostProcessor::FBO_Primary, frameId)); + renderStage->setFrameBufferObject(mPostProcessor->getFbo(PostProcessor::FBO_Multisample, frameId)); + + // The MultiView patch has a bug where it does not update resolve layers if the resolve framebuffer is + // changed. So we do blit manually in this case + if (Stereo::getMultiview() && !renderStage->getDrawCallback()) + Stereo::setMultiviewMSAAResolveCallback(renderStage); + } + + if (mViewportStateset) + { + mViewport->setViewport(0, 0, mPostProcessor->renderWidth(), mPostProcessor->renderHeight()); + renderStage->setViewport(mViewport); + cv->pushStateSet(mViewportStateset.get()); + traverse(node, cv); + cv->popStateSet(); + } + else + traverse(node, cv); + } +} diff --git a/apps/openmw/mwrender/pingpongcull.hpp b/apps/openmw/mwrender/pingpongcull.hpp new file mode 100644 index 00000000000..8886ede9e36 --- /dev/null +++ b/apps/openmw/mwrender/pingpongcull.hpp @@ -0,0 +1,35 @@ +#ifndef OPENMW_MWRENDER_PINGPONGCULL_H +#define OPENMW_MWRENDER_PINGPONGCULL_H + +#include + +#include + +#include "postprocessor.hpp" + +namespace osg +{ + class StateSet; + class Viewport; +} + +namespace MWRender +{ + class PostProcessor; + class PingPongCull : public SceneUtil::NodeCallback + { + public: + PingPongCull(PostProcessor* pp); + ~PingPongCull(); + + void operator()(osg::Node* node, osgUtil::CullVisitor* nv); + + private: + std::array mLastViewMatrix; + osg::ref_ptr mViewportStateset; + osg::ref_ptr mViewport; + PostProcessor* mPostProcessor; + }; +} + +#endif diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp new file mode 100644 index 00000000000..8fdc0a45ca3 --- /dev/null +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -0,0 +1,852 @@ +#include "postprocessor.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" + +#include "../mwgui/postprocessorhud.hpp" + +#include "distortion.hpp" +#include "pingpongcull.hpp" +#include "renderbin.hpp" +#include "renderingmanager.hpp" +#include "sky.hpp" +#include "transparentpass.hpp" +#include "vismask.hpp" + +namespace +{ + struct ResizedCallback : osg::GraphicsContext::ResizedCallback + { + ResizedCallback(MWRender::PostProcessor* postProcessor) + : mPostProcessor(postProcessor) + { + } + + void resizedImplementation(osg::GraphicsContext* gc, int x, int y, int width, int height) override + { + gc->resizedImplementation(x, y, width, height); + + mPostProcessor->setRenderTargetSize(width, height); + mPostProcessor->resize(); + } + + MWRender::PostProcessor* mPostProcessor; + }; + + class HUDCullCallback : public SceneUtil::NodeCallback + { + public: + void operator()(osg::Camera* camera, osgUtil::CullVisitor* cv) + { + osg::ref_ptr stateset = new osg::StateSet; + auto& sm = Stereo::Manager::instance(); + auto* fullViewport = camera->getViewport(); + if (sm.getEye(cv) == Stereo::Eye::Left) + stateset->setAttributeAndModes( + new osg::Viewport(0, 0, fullViewport->width() / 2, fullViewport->height())); + if (sm.getEye(cv) == Stereo::Eye::Right) + stateset->setAttributeAndModes( + new osg::Viewport(fullViewport->width() / 2, 0, fullViewport->width() / 2, fullViewport->height())); + + cv->pushStateSet(stateset); + traverse(camera, cv); + cv->popStateSet(); + } + }; + + enum class Usage + { + RENDER_BUFFER, + TEXTURE, + }; + + static osg::FrameBufferAttachment createFrameBufferAttachmentFromTemplate( + Usage usage, int width, int height, osg::Texture* template_, int samples) + { + if (usage == Usage::RENDER_BUFFER && !Stereo::getMultiview()) + { + osg::ref_ptr attachment + = new osg::RenderBuffer(width, height, template_->getInternalFormat(), samples); + return osg::FrameBufferAttachment(attachment); + } + + auto texture = Stereo::createMultiviewCompatibleTexture(width, height, samples); + texture->setSourceFormat(template_->getSourceFormat()); + texture->setSourceType(template_->getSourceType()); + texture->setInternalFormat(template_->getInternalFormat()); + texture->setFilter(osg::Texture2D::MIN_FILTER, template_->getFilter(osg::Texture2D::MIN_FILTER)); + texture->setFilter(osg::Texture2D::MAG_FILTER, template_->getFilter(osg::Texture2D::MAG_FILTER)); + texture->setWrap(osg::Texture::WRAP_S, template_->getWrap(osg::Texture2D::WRAP_S)); + texture->setWrap(osg::Texture::WRAP_T, template_->getWrap(osg::Texture2D::WRAP_T)); + + return Stereo::createMultiviewCompatibleAttachment(texture); + } + + constexpr float DistortionRatio = 0.25; +} + +namespace MWRender +{ + PostProcessor::PostProcessor( + RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode, const VFS::Manager* vfs) + : osg::Group() + , mRootNode(rootNode) + , mHUDCamera(new osg::Camera) + , mRendering(rendering) + , mViewer(viewer) + , mVFS(vfs) + , mUsePostProcessing(Settings::postProcessing().mEnabled) + , mSamples(Settings::video().mAntialiasing) + , mPingPongCull(new PingPongCull(this)) + , mDistortionCallback(new DistortionCallback) + { + auto& shaderManager = mRendering.getResourceSystem()->getSceneManager()->getShaderManager(); + + std::shared_ptr luminanceCalculator = std::make_shared(shaderManager); + + for (auto& canvas : mCanvases) + canvas = new PingPongCanvas(shaderManager, luminanceCalculator); + + mHUDCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); + mHUDCamera->setRenderOrder(osg::Camera::POST_RENDER); + mHUDCamera->setClearColor(osg::Vec4(0.45, 0.45, 0.14, 1.0)); + mHUDCamera->setClearMask(0); + mHUDCamera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1, 0, 1)); + mHUDCamera->setAllowEventFocus(false); + mHUDCamera->setViewport(0, 0, mWidth, mHeight); + mHUDCamera->setNodeMask(Mask_RenderToTexture); + mHUDCamera->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + mHUDCamera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + mHUDCamera->addChild(mCanvases[0]); + mHUDCamera->addChild(mCanvases[1]); + mHUDCamera->setCullCallback(new HUDCullCallback); + mViewer->getCamera()->addCullCallback(mPingPongCull); + + // resolves the multisampled depth buffer and optionally draws an additional depth postpass + mTransparentDepthPostPass + = new TransparentDepthBinCallback(mRendering.getResourceSystem()->getSceneManager()->getShaderManager(), + Settings::postProcessing().mTransparentPostpass); + osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")->setDrawCallback(mTransparentDepthPostPass); + + osg::ref_ptr distortionRenderBin + = new osgUtil::RenderBin(osgUtil::RenderBin::SORT_BACK_TO_FRONT); + // This is silly to have to do, but if nothing is drawn then the drawcallback is never called and the distortion + // texture will never be cleared + osg::ref_ptr dummyNodeToClear = new osg::Node; + dummyNodeToClear->setCullingActive(false); + dummyNodeToClear->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Distortion, "Distortion"); + rootNode->addChild(dummyNodeToClear); + distortionRenderBin->setDrawCallback(mDistortionCallback); + distortionRenderBin->getStateSet()->setDefine("DISTORTION", "1", osg::StateAttribute::ON); + + // Give the renderbin access to the opaque depth sampler so it can write its occlusion + // Distorted geometry is drawn with ALWAYS depth function and depths writes disbled. + const int unitSoftEffect + = shaderManager.reserveGlobalTextureUnits(Shader::ShaderManager::Slot::OpaqueDepthTexture); + distortionRenderBin->getStateSet()->addUniform(new osg::Uniform("opaqueDepthTex", unitSoftEffect)); + + osgUtil::RenderBin::addRenderBinPrototype("Distortion", distortionRenderBin); + + auto defines = shaderManager.getGlobalDefines(); + defines["distorionRTRatio"] = std::to_string(DistortionRatio); + shaderManager.setGlobalDefines(defines); + + createObjectsForFrame(0); + createObjectsForFrame(1); + + populateTechniqueFiles(); + + auto distortion = loadTechnique("internal_distortion"); + distortion->setInternal(true); + distortion->setLocked(true); + mInternalTechniques.push_back(distortion); + + osg::GraphicsContext* gc = viewer->getCamera()->getGraphicsContext(); + osg::GLExtensions* ext = gc->getState()->get(); + + mWidth = gc->getTraits()->width; + mHeight = gc->getTraits()->height; + + if (!ext->glDisablei && ext->glDisableIndexedEXT) + ext->glDisablei = ext->glDisableIndexedEXT; + +#ifdef ANDROID + ext->glDisablei = nullptr; +#endif + + if (ext->glDisablei) + mNormalsSupported = true; + else + Log(Debug::Error) << "'glDisablei' unsupported, pass normals will not be available to shaders."; + + mGLSLVersion = ext->glslLanguageVersion * 100; + mUBO = ext->isUniformBufferObjectSupported && mGLSLVersion >= 330; + mStateUpdater = new fx::StateUpdater(mUBO); + + addChild(mHUDCamera); + addChild(mRootNode); + + mViewer->setSceneData(this); + mViewer->getCamera()->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + mViewer->getCamera()->getGraphicsContext()->setResizedCallback(new ResizedCallback(this)); + mViewer->getCamera()->setUserData(this); + + setCullCallback(mStateUpdater); + + if (mUsePostProcessing) + enable(); + } + + PostProcessor::~PostProcessor() + { + if (auto* bin = osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")) + bin->setDrawCallback(nullptr); + } + + void PostProcessor::resize() + { + mHUDCamera->resize(mWidth, mHeight); + mViewer->getCamera()->resize(mWidth, mHeight); + if (Stereo::getStereo()) + Stereo::Manager::instance().screenResolutionChanged(); + + size_t frameId = frame() % 2; + + createObjectsForFrame(frameId); + + mRendering.updateProjectionMatrix(); + mRendering.setScreenRes(renderWidth(), renderHeight()); + + dirtyTechniques(true); + + mDirty = true; + mDirtyFrameId = !frameId; + } + + void PostProcessor::populateTechniqueFiles() + { + for (const auto& name : 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) + { + const auto absolutePath = mVFS->getAbsoluteFileName(path); + mTechniqueFileMap[Files::pathToUnicodeString(absolutePath.stem())] = absolutePath; + } + } + } + + void PostProcessor::enable() + { + mReload = true; + mUsePostProcessing = true; + } + + void PostProcessor::disable() + { + mUsePostProcessing = false; + mRendering.getSkyManager()->setSunglare(true); + } + + void PostProcessor::traverse(osg::NodeVisitor& nv) + { + size_t frameId = nv.getTraversalNumber() % 2; + + if (nv.getVisitorType() == osg::NodeVisitor::CULL_VISITOR) + cull(frameId, static_cast(&nv)); + else if (nv.getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR) + update(frameId); + + osg::Group::traverse(nv); + } + + void PostProcessor::cull(size_t frameId, osgUtil::CullVisitor* cv) + { + if (const auto& fbo = getFbo(FBO_Intercept, frameId)) + { + osgUtil::RenderStage* rs = cv->getRenderStage(); + if (rs && rs->getMultisampleResolveFramebufferObject()) + rs->setMultisampleResolveFramebufferObject(fbo); + } + + mCanvases[frameId]->setPostProcessing(mUsePostProcessing); + mCanvases[frameId]->setTextureNormals(mNormals ? getTexture(Tex_Normal, frameId) : nullptr); + mCanvases[frameId]->setMask(mUnderwater, mExteriorFlag); + mCanvases[frameId]->setCalculateAvgLum(mHDR); + + mCanvases[frameId]->setTextureScene(getTexture(Tex_Scene, frameId)); + mCanvases[frameId]->setTextureDepth(getTexture(Tex_OpaqueDepth, frameId)); + mCanvases[frameId]->setTextureDistortion(getTexture(Tex_Distortion, frameId)); + + mTransparentDepthPostPass->mFbo[frameId] = mFbos[frameId][FBO_Primary]; + mTransparentDepthPostPass->mMsaaFbo[frameId] = mFbos[frameId][FBO_Multisample]; + mTransparentDepthPostPass->mOpaqueFbo[frameId] = mFbos[frameId][FBO_OpaqueDepth]; + + mDistortionCallback->setFBO(mFbos[frameId][FBO_Distortion], frameId); + mDistortionCallback->setOriginalFBO(mFbos[frameId][FBO_Primary], frameId); + + size_t frame = cv->getTraversalNumber(); + + mStateUpdater->setResolution(osg::Vec2f(cv->getViewport()->width(), cv->getViewport()->height())); + + // per-frame data + if (frame != mLastFrameNumber) + { + mLastFrameNumber = frame; + auto stamp = cv->getFrameStamp(); + + mStateUpdater->setSimulationTime(static_cast(stamp->getSimulationTime())); + mStateUpdater->setDeltaSimulationTime(static_cast(stamp->getSimulationTime() - mLastSimulationTime)); + // Use a signed int because 'uint' type is not supported in GLSL 120 without extensions + mStateUpdater->setFrameNumber(static_cast(stamp->getFrameNumber())); + mLastSimulationTime = stamp->getSimulationTime(); + + for (const auto& dispatchNode : mCanvases[frameId]->getPasses()) + { + for (auto& uniform : dispatchNode.mHandle->getUniformMap()) + { + if (uniform->getType().has_value() && !uniform->mSamplerType) + if (auto* u = dispatchNode.mRootStateSet->getUniform(uniform->mName)) + uniform->setUniform(u); + } + } + } + } + + void PostProcessor::updateLiveReload() + { + if (!mEnableLiveReload && !mTriggerShaderReload) + return; + + mTriggerShaderReload = false; // Done only once + + for (auto& technique : mTechniques) + { + if (!technique || technique->getStatus() == fx::Technique::Status::File_Not_exists) + continue; + + const auto lastWriteTime = std::filesystem::last_write_time(mTechniqueFileMap[technique->getName()]); + const bool isDirty = technique->setLastModificationTime(lastWriteTime); + + if (!isDirty) + continue; + + // TODO: Temporary workaround to avoid conflicts with external programs saving the file, especially + // problematic on Windows. + // If we move to a file watcher using native APIs this should be removed. + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + if (technique->compile()) + Log(Debug::Info) << "Reloaded technique : " << mTechniqueFileMap[technique->getName()]; + + mReload = technique->isValid(); + } + } + + void PostProcessor::reloadIfRequired() + { + if (!mReload) + return; + + mReload = false; + + loadChain(); + resize(); + } + + void PostProcessor::update(size_t frameId) + { + while (!mQueuedTemplates.empty()) + { + mTemplates.push_back(std::move(mQueuedTemplates.back())); + + mQueuedTemplates.pop_back(); + } + + updateLiveReload(); + + reloadIfRequired(); + + mCanvases[frameId]->setNodeMask(~0u); + mCanvases[!frameId]->setNodeMask(0); + + if (mDirty && mDirtyFrameId == frameId) + { + createObjectsForFrame(frameId); + + mDirty = false; + mCanvases[frameId]->setPasses(fx::DispatchArray(mTemplateData)); + } + + if ((mNormalsSupported && mNormals != mPrevNormals) || (mPassLights != mPrevPassLights)) + { + mPrevNormals = mNormals; + mPrevPassLights = mPassLights; + + mViewer->stopThreading(); + + if (mNormalsSupported) + { + auto& shaderManager + = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getShaderManager(); + auto defines = shaderManager.getGlobalDefines(); + defines["disableNormals"] = mNormals ? "0" : "1"; + shaderManager.setGlobalDefines(defines); + } + + mRendering.getLightRoot()->setCollectPPLights(mPassLights); + mStateUpdater->bindPointLights(mPassLights ? mRendering.getLightRoot()->getPPLightsBuffer() : nullptr); + mStateUpdater->reset(); + + mViewer->startThreading(); + + createObjectsForFrame(frameId); + + mDirty = true; + mDirtyFrameId = !frameId; + } + } + + void PostProcessor::createObjectsForFrame(size_t frameId) + { + auto& textures = mTextures[frameId]; + + int width = renderWidth(); + int height = renderHeight(); + + for (osg::ref_ptr& texture : textures) + { + if (!texture) + { + if (Stereo::getMultiview()) + texture = new osg::Texture2DArray; + else + texture = new osg::Texture2D; + } + Stereo::setMultiviewCompatibleTextureSize(texture, width, height); + texture->setSourceFormat(GL_RGBA); + texture->setSourceType(GL_UNSIGNED_BYTE); + texture->setInternalFormat(GL_RGBA); + texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture::LINEAR); + texture->setFilter(osg::Texture2D::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); + texture->setResizeNonPowerOfTwoHint(false); + Stereo::setMultiviewCompatibleTextureSize(texture, width, height); + texture->dirtyTextureObject(); + } + + textures[Tex_Normal]->setSourceFormat(GL_RGB); + textures[Tex_Normal]->setInternalFormat(GL_RGB); + + textures[Tex_Distortion]->setSourceFormat(GL_RGB); + textures[Tex_Distortion]->setInternalFormat(GL_RGB); + + Stereo::setMultiviewCompatibleTextureSize( + textures[Tex_Distortion], width * DistortionRatio, height * DistortionRatio); + textures[Tex_Distortion]->dirtyTextureObject(); + + auto setupDepth = [](osg::Texture* tex) { + tex->setSourceFormat(GL_DEPTH_STENCIL_EXT); + tex->setSourceType(SceneUtil::AutoDepth::depthSourceType()); + tex->setInternalFormat(SceneUtil::AutoDepth::depthInternalFormat()); + }; + + setupDepth(textures[Tex_Depth]); + setupDepth(textures[Tex_OpaqueDepth]); + textures[Tex_OpaqueDepth]->setName("opaqueTexMap"); + + auto& fbos = mFbos[frameId]; + + fbos[FBO_Primary] = new osg::FrameBufferObject; + fbos[FBO_Primary]->setAttachment( + osg::Camera::COLOR_BUFFER0, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Scene])); + if (mNormals && mNormalsSupported) + fbos[FBO_Primary]->setAttachment( + osg::Camera::COLOR_BUFFER1, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Normal])); + fbos[FBO_Primary]->setAttachment( + osg::Camera::PACKED_DEPTH_STENCIL_BUFFER, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Depth])); + + fbos[FBO_FirstPerson] = new osg::FrameBufferObject; + + auto fpDepthRb = createFrameBufferAttachmentFromTemplate( + Usage::RENDER_BUFFER, width, height, textures[Tex_Depth], mSamples); + fbos[FBO_FirstPerson]->setAttachment(osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER, + osg::FrameBufferAttachment(fpDepthRb)); + + if (mSamples > 1) + { + fbos[FBO_Multisample] = new osg::FrameBufferObject; + auto colorRB = createFrameBufferAttachmentFromTemplate( + Usage::RENDER_BUFFER, width, height, textures[Tex_Scene], mSamples); + if (mNormals && mNormalsSupported) + { + auto normalRB = createFrameBufferAttachmentFromTemplate( + Usage::RENDER_BUFFER, width, height, textures[Tex_Normal], mSamples); + fbos[FBO_Multisample]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER1, normalRB); + fbos[FBO_FirstPerson]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER1, normalRB); + } + auto depthRB = createFrameBufferAttachmentFromTemplate( + Usage::RENDER_BUFFER, width, height, textures[Tex_Depth], mSamples); + fbos[FBO_Multisample]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, colorRB); + fbos[FBO_Multisample]->setAttachment( + osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER, depthRB); + fbos[FBO_FirstPerson]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, colorRB); + + fbos[FBO_Intercept] = new osg::FrameBufferObject; + fbos[FBO_Intercept]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + Stereo::createMultiviewCompatibleAttachment(textures[Tex_Scene])); + fbos[FBO_Intercept]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER1, + Stereo::createMultiviewCompatibleAttachment(textures[Tex_Normal])); + } + else + { + fbos[FBO_FirstPerson]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + Stereo::createMultiviewCompatibleAttachment(textures[Tex_Scene])); + if (mNormals && mNormalsSupported) + fbos[FBO_FirstPerson]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER1, + Stereo::createMultiviewCompatibleAttachment(textures[Tex_Normal])); + } + + fbos[FBO_OpaqueDepth] = new osg::FrameBufferObject; + fbos[FBO_OpaqueDepth]->setAttachment(osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER, + Stereo::createMultiviewCompatibleAttachment(textures[Tex_OpaqueDepth])); + + fbos[FBO_Distortion] = new osg::FrameBufferObject; + fbos[FBO_Distortion]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + Stereo::createMultiviewCompatibleAttachment(textures[Tex_Distortion])); + +#ifdef __APPLE__ + if (textures[Tex_OpaqueDepth]) + fbos[FBO_OpaqueDepth]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER, + osg::FrameBufferAttachment(new osg::RenderBuffer(textures[Tex_OpaqueDepth]->getTextureWidth(), + textures[Tex_OpaqueDepth]->getTextureHeight(), textures[Tex_Scene]->getInternalFormat()))); +#endif + + mCanvases[frameId]->dirty(); + } + + void PostProcessor::dirtyTechniques(bool dirtyAttachments) + { + size_t frameId = frame() % 2; + + mDirty = true; + mDirtyFrameId = !frameId; + + mTemplateData = {}; + + bool sunglare = true; + mHDR = false; + mNormals = false; + mPassLights = false; + + std::vector attachmentsToDirty; + + for (const auto& technique : mTechniques) + { + if (!technique || !technique->isValid()) + continue; + + if (technique->getGLSLVersion() > mGLSLVersion) + { + Log(Debug::Warning) << "Technique " << technique->getName() << " requires GLSL version " + << technique->getGLSLVersion() << " which is unsupported by your hardware."; + continue; + } + + fx::DispatchNode node; + + node.mFlags = technique->getFlags(); + + if (technique->getHDR()) + mHDR = true; + + if (technique->getNormals()) + mNormals = true; + + if (technique->getLights()) + mPassLights = true; + + if (node.mFlags & fx::Technique::Flag_Disable_SunGlare) + sunglare = false; + + // required default samplers available to every shader pass + node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerLastShader", Unit_LastShader)); + node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerLastPass", Unit_LastPass)); + node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerDepth", Unit_Depth)); + node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerDistortion", Unit_Distortion)); + + if (mNormals) + node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerNormals", Unit_Normals)); + + if (technique->getHDR()) + node.mRootStateSet->addUniform(new osg::Uniform("omw_EyeAdaptation", Unit_EyeAdaptation)); + + node.mRootStateSet->addUniform(new osg::Uniform("omw_SamplerDistortion", Unit_Distortion)); + + int texUnit = Unit_NextFree; + + // user-defined samplers + for (const osg::Texture* texture : technique->getTextures()) + { + if (const auto* tex1D = dynamic_cast(texture)) + node.mRootStateSet->setTextureAttribute(texUnit, new osg::Texture1D(*tex1D)); + else if (const auto* tex2D = dynamic_cast(texture)) + node.mRootStateSet->setTextureAttribute(texUnit, new osg::Texture2D(*tex2D)); + else if (const auto* tex3D = dynamic_cast(texture)) + node.mRootStateSet->setTextureAttribute(texUnit, new osg::Texture3D(*tex3D)); + + node.mRootStateSet->addUniform(new osg::Uniform(texture->getName().c_str(), texUnit++)); + } + + // user-defined uniforms + for (auto& uniform : technique->getUniformMap()) + { + if (uniform->mSamplerType) + continue; + + if (auto type = uniform->getType()) + uniform->setUniform(node.mRootStateSet->getOrCreateUniform( + uniform->mName.c_str(), *type, uniform->getNumElements())); + } + + for (const auto& pass : technique->getPasses()) + { + int subTexUnit = texUnit; + fx::DispatchNode::SubPass subPass; + + pass->prepareStateSet(subPass.mStateSet, technique->getName()); + + node.mHandle = technique; + + if (!pass->getTarget().empty()) + { + auto& renderTarget = technique->getRenderTargetsMap()[pass->getTarget()]; + subPass.mSize = renderTarget.mSize; + subPass.mRenderTexture = renderTarget.mTarget; + subPass.mMipMap = renderTarget.mMipMap; + + const auto [w, h] = renderTarget.mSize.get(renderWidth(), renderHeight()); + subPass.mStateSet->setAttributeAndModes(new osg::Viewport(0, 0, w, h)); + + subPass.mRenderTexture->setTextureSize(w, h); + subPass.mRenderTexture->dirtyTextureObject(); + + subPass.mRenderTarget = new osg::FrameBufferObject; + subPass.mRenderTarget->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, + osg::FrameBufferAttachment(subPass.mRenderTexture)); + + if (std::find_if(attachmentsToDirty.cbegin(), attachmentsToDirty.cend(), + [renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; }) + == attachmentsToDirty.cend()) + { + attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget)); + } + } + + for (const auto& name : pass->getRenderTargets()) + { + if (name.empty()) + { + continue; + } + + auto& renderTarget = technique->getRenderTargetsMap()[name]; + subPass.mStateSet->setTextureAttribute(subTexUnit, renderTarget.mTarget); + subPass.mStateSet->addUniform(new osg::Uniform(name.c_str(), subTexUnit)); + + if (std::find_if(attachmentsToDirty.cbegin(), attachmentsToDirty.cend(), + [renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; }) + == attachmentsToDirty.cend()) + { + attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget)); + } + subTexUnit++; + } + + node.mPasses.emplace_back(std::move(subPass)); + } + + node.compile(); + + mTemplateData.emplace_back(std::move(node)); + } + + mCanvases[frameId]->setPasses(fx::DispatchArray(mTemplateData)); + + if (auto hud = MWBase::Environment::get().getWindowManager()->getPostProcessorHud()) + hud->updateTechniques(); + + mRendering.getSkyManager()->setSunglare(sunglare); + + if (dirtyAttachments) + mCanvases[frameId]->setDirtyAttachments(attachmentsToDirty); + } + + PostProcessor::Status PostProcessor::enableTechnique( + std::shared_ptr technique, std::optional location) + { + if (!technique || technique->getLocked() || (location.has_value() && location.value() < 0)) + return Status_Error; + + disableTechnique(technique, false); + + int pos = std::min(location.value_or(mTechniques.size()) + mInternalTechniques.size(), mTechniques.size()); + + mTechniques.insert(mTechniques.begin() + pos, technique); + dirtyTechniques(Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug); + + return Status_Toggled; + } + + PostProcessor::Status PostProcessor::disableTechnique(std::shared_ptr technique, bool dirty) + { + if (!technique || technique->getLocked()) + return Status_Error; + + auto it = std::find(mTechniques.begin(), mTechniques.end(), technique); + if (it == std::end(mTechniques)) + return Status_Unchanged; + + mTechniques.erase(it); + if (dirty) + dirtyTechniques(); + + return Status_Toggled; + } + + 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) + { + for (const auto& technique : mTemplates) + if (Misc::StringUtils::ciEqual(technique->getName(), name)) + return technique; + + for (const auto& technique : mQueuedTemplates) + if (Misc::StringUtils::ciEqual(technique->getName(), name)) + return technique; + + auto technique = std::make_shared(*mVFS, *mRendering.getResourceSystem()->getImageManager(), + name, renderWidth(), renderHeight(), mUBO, mNormalsSupported); + + technique->compile(); + + if (technique->getStatus() != fx::Technique::Status::File_Not_exists) + technique->setLastModificationTime( + std::filesystem::last_write_time(mTechniqueFileMap[technique->getName()])); + + if (loadNextFrame) + { + mQueuedTemplates.push_back(technique); + return technique; + } + + mTemplates.push_back(std::move(technique)); + + return mTemplates.back(); + } + + void PostProcessor::loadChain() + { + mTechniques.clear(); + + for (const auto& technique : mInternalTechniques) + { + mTechniques.push_back(technique); + } + + for (const std::string& techniqueName : Settings::postProcessing().mChain.get()) + { + if (techniqueName.empty()) + continue; + + mTechniques.push_back(loadTechnique(techniqueName)); + } + + dirtyTechniques(); + } + + void PostProcessor::saveChain() + { + std::vector chain; + + for (const auto& technique : mTechniques) + { + if (!technique || technique->getDynamic() || technique->getInternal()) + continue; + chain.push_back(technique->getName()); + } + + Settings::postProcessing().mChain.set(chain); + } + + void PostProcessor::toggleMode() + { + for (auto& technique : mTemplates) + technique->compile(); + + dirtyTechniques(true); + } + + void PostProcessor::disableDynamicShaders() + { + for (auto& technique : mTechniques) + if (technique && technique->getDynamic()) + disableTechnique(technique); + } + + int PostProcessor::renderWidth() const + { + if (Stereo::getStereo()) + return Stereo::Manager::instance().eyeResolution().x(); + return mWidth; + } + + int PostProcessor::renderHeight() const + { + if (Stereo::getStereo()) + return Stereo::Manager::instance().eyeResolution().y(); + return mHeight; + } + + void PostProcessor::triggerShaderReload() + { + mTriggerShaderReload = true; + } +} diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp new file mode 100644 index 00000000000..2630467f95a --- /dev/null +++ b/apps/openmw/mwrender/postprocessor.hpp @@ -0,0 +1,272 @@ +#ifndef OPENMW_MWRENDER_POSTPROCESSOR_H +#define OPENMW_MWRENDER_POSTPROCESSOR_H + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "pingpongcanvas.hpp" +#include "transparentpass.hpp" + +#include + +namespace osgViewer +{ + class Viewer; +} + +namespace Stereo +{ + class MultiviewFramebuffer; +} + +namespace VFS +{ + class Manager; +} + +namespace Shader +{ + class ShaderManager; +} + +namespace MWRender +{ + class RenderingManager; + class PingPongCull; + class PingPongCanvas; + class TransparentDepthBinCallback; + class DistortionCallback; + + class PostProcessor : public osg::Group + { + public: + using FBOArray = std::array, 6>; + using TextureArray = std::array, 6>; + using TechniqueList = std::vector>; + + enum TextureIndex + { + Tex_Scene, + Tex_Scene_LDR, + Tex_Depth, + Tex_OpaqueDepth, + Tex_Normal, + Tex_Distortion, + }; + + enum FBOIndex + { + FBO_Primary, + FBO_Multisample, + FBO_FirstPerson, + FBO_OpaqueDepth, + FBO_Intercept, + FBO_Distortion, + }; + + enum TextureUnits + { + Unit_LastShader = 0, + Unit_LastPass, + Unit_Depth, + Unit_EyeAdaptation, + Unit_Normals, + Unit_Distortion, + Unit_NextFree + }; + + enum Status + { + Status_Error, + Status_Toggled, + Status_Unchanged + }; + + PostProcessor( + RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode, const VFS::Manager* vfs); + + ~PostProcessor(); + + void traverse(osg::NodeVisitor& nv) override; + + osg::ref_ptr getFbo(FBOIndex index, unsigned int frameId) + { + return mFbos[frameId][index]; + } + + osg::ref_ptr getTexture(TextureIndex index, unsigned int frameId) + { + return mTextures[frameId][index]; + } + + osg::ref_ptr getPrimaryFbo(unsigned int frameId) + { + return mFbos[frameId][FBO_Multisample] ? mFbos[frameId][FBO_Multisample] : mFbos[frameId][FBO_Primary]; + } + + osg::ref_ptr getHUDCamera() { return mHUDCamera; } + + osg::ref_ptr getStateUpdater() { return mStateUpdater; } + + const TechniqueList& getTechniques() { return mTechniques; } + + const TechniqueList& getTemplates() const { return mTemplates; } + + const auto& getTechniqueMap() const { return mTechniqueFileMap; } + + void resize(); + + Status enableTechnique(std::shared_ptr technique, std::optional location = std::nullopt); + + 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) + { + if (!isEnabled()) + return; + + auto it = technique->findUniform(name); + + if (it == technique->getUniformMap().end()) + return; + + if ((*it)->mStatic) + { + Log(Debug::Warning) << "Attempting to set a configration variable [" << name << "] as a uniform"; + return; + } + + (*it)->setValue(value); + } + + std::optional getUniformSize(std::shared_ptr technique, const std::string& name) + { + auto it = technique->findUniform(name); + + if (it == technique->getUniformMap().end()) + return std::nullopt; + + return (*it)->getNumElements(); + } + + bool isTechniqueEnabled(const std::shared_ptr& technique) const; + + void setExteriorFlag(bool exterior) { mExteriorFlag = exterior; } + + void setUnderwaterFlag(bool underwater) { mUnderwater = underwater; } + + void toggleMode(); + + std::shared_ptr loadTechnique(const std::string& name, bool loadNextFrame = false); + + bool isEnabled() const { return mUsePostProcessing; } + + void disable(); + + void enable(); + + void setRenderTargetSize(int width, int height) + { + mWidth = width; + mHeight = height; + } + + void disableDynamicShaders(); + + int renderWidth() const; + int renderHeight() const; + + void triggerShaderReload(); + + bool mEnableLiveReload = false; + + void loadChain(); + void saveChain(); + + private: + void populateTechniqueFiles(); + + size_t frame() const { return mViewer->getFrameStamp()->getFrameNumber(); } + + void createObjectsForFrame(size_t frameId); + + void dirtyTechniques(bool dirtyAttachments = false); + + void update(size_t frameId); + + void reloadIfRequired(); + + void updateLiveReload(); + + void cull(size_t frameId, osgUtil::CullVisitor* cv); + + osg::ref_ptr mRootNode; + osg::ref_ptr mHUDCamera; + + std::array mTextures; + std::array mFbos; + + TechniqueList mTechniques; + TechniqueList mTemplates; + TechniqueList mQueuedTemplates; + TechniqueList mInternalTechniques; + + std::unordered_map mTechniqueFileMap; + + RenderingManager& mRendering; + osgViewer::Viewer* mViewer; + const VFS::Manager* mVFS; + + size_t mDirtyFrameId = 0; + size_t mLastFrameNumber = 0; + float mLastSimulationTime = 0.f; + + bool mDirty = false; + bool mReload = true; + bool mTriggerShaderReload = false; + bool mUsePostProcessing = false; + + bool mUBO = false; + bool mHDR = false; + bool mNormals = false; + bool mUnderwater = false; + bool mPassLights = false; + bool mPrevNormals = false; + bool mExteriorFlag = false; + bool mNormalsSupported = false; + bool mPrevPassLights = false; + + int mGLSLVersion; + int mWidth; + int mHeight; + int mSamples; + + osg::ref_ptr mStateUpdater; + osg::ref_ptr mPingPongCull; + std::array, 2> mCanvases; + osg::ref_ptr mTransparentDepthPostPass; + osg::ref_ptr mDistortionCallback; + + fx::DispatchArray mTemplateData; + }; +} + +#endif diff --git a/apps/openmw/mwrender/precipitationocclusion.cpp b/apps/openmw/mwrender/precipitationocclusion.cpp new file mode 100644 index 00000000000..31712410b89 --- /dev/null +++ b/apps/openmw/mwrender/precipitationocclusion.cpp @@ -0,0 +1,182 @@ +#include "precipitationocclusion.hpp" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" + +#include "vismask.hpp" + +namespace +{ + class PrecipitationOcclusionUpdater : public SceneUtil::StateSetUpdater + { + public: + PrecipitationOcclusionUpdater(osg::ref_ptr depthTexture) + : mDepthTexture(std::move(depthTexture)) + { + } + + private: + void setDefaults(osg::StateSet* stateset) override + { + stateset->setTextureAttributeAndModes(3, mDepthTexture); + stateset->addUniform(new osg::Uniform("orthoDepthMap", 3)); + stateset->addUniform(new osg::Uniform("depthSpaceMatrix", mDepthSpaceMatrix)); + } + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + osg::Camera* camera = nv->asCullVisitor()->getCurrentCamera(); + stateset->getUniform("depthSpaceMatrix")->set(camera->getViewMatrix() * camera->getProjectionMatrix()); + } + + osg::Matrixf mDepthSpaceMatrix; + osg::ref_ptr mDepthTexture; + }; + + class DepthCameraUpdater : public SceneUtil::StateSetUpdater + { + public: + DepthCameraUpdater() + : mDummyTexture(new osg::Texture2D) + { + mDummyTexture->setInternalFormat(GL_RGB); + mDummyTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mDummyTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mDummyTexture->setTextureSize(1, 1); + + Shader::ShaderManager& shaderMgr + = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getShaderManager(); + mProgram = shaderMgr.getProgram("depthclipped"); + } + + private: + void setDefaults(osg::StateSet* stateset) override + { + stateset->addUniform(new osg::Uniform("projectionMatrix", osg::Matrixf())); + stateset->setAttributeAndModes(mProgram, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + stateset->setTextureAttributeAndModes(0, mDummyTexture); + stateset->setRenderBinDetails( + osg::StateSet::OPAQUE_BIN, "RenderBin", osg::StateSet::OVERRIDE_PROTECTED_RENDERBIN_DETAILS); + } + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + osg::Camera* camera = nv->asCullVisitor()->getCurrentCamera(); + stateset->getUniform("projectionMatrix")->set(camera->getProjectionMatrix()); + } + + osg::Matrixf mProjectionMatrix; + osg::ref_ptr mDummyTexture; + osg::ref_ptr mProgram; + }; +} + +namespace MWRender +{ + PrecipitationOccluder::PrecipitationOccluder( + osg::Group* skyNode, osg::Group* sceneNode, osg::Group* rootNode, osg::Camera* camera) + : mSkyNode(skyNode) + , mSceneNode(sceneNode) + , mRootNode(rootNode) + , mSceneCamera(camera) + { + constexpr int rttSize = 256; + + mDepthTexture = new osg::Texture2D; + mDepthTexture->setTextureSize(rttSize, rttSize); + mDepthTexture->setSourceFormat(GL_DEPTH_COMPONENT); + mDepthTexture->setInternalFormat(GL_DEPTH_COMPONENT24); + mDepthTexture->setSourceType(GL_UNSIGNED_INT); + mDepthTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_BORDER); + mDepthTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_BORDER); + mDepthTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + mDepthTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + mDepthTexture->setBorderColor( + SceneUtil::AutoDepth::isReversed() ? osg::Vec4(0, 0, 0, 0) : osg::Vec4(1, 1, 1, 1)); + + mCamera = new osg::Camera; + mCamera->setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); + mCamera->setRenderOrder(osg::Camera::PRE_RENDER); + mCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + mCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF_INHERIT_VIEWPOINT); + mCamera->setNodeMask(Mask_RenderToTexture); + mCamera->setCullMask(Mask_Scene | Mask_Object | Mask_Static); + mCamera->setViewport(0, 0, rttSize, rttSize); + mCamera->attach(osg::Camera::DEPTH_BUFFER, mDepthTexture); + mCamera->addChild(mSceneNode); + mCamera->setSmallFeatureCullingPixelSize( + Settings::shaders().mWeatherParticleOcclusionSmallFeatureCullingPixelSize); + + SceneUtil::setCameraClearDepth(mCamera); + } + + void PrecipitationOccluder::update() + { + if (!mRange.has_value()) + return; + + const osg::Vec3 pos = mSceneCamera->getInverseViewMatrix().getTrans(); + + const float zmin = pos.z() - mRange->z() - Constants::CellSizeInUnits; + const float zmax = pos.z() + mRange->z() + Constants::CellSizeInUnits; + const float near = 0; + const float far = zmax - zmin; + + const float left = -mRange->x() / 2; + const float right = -left; + const float top = mRange->y() / 2; + const float bottom = -top; + + if (SceneUtil::AutoDepth::isReversed()) + { + mCamera->setProjectionMatrix( + SceneUtil::getReversedZProjectionMatrixAsOrtho(left, right, bottom, top, near, far)); + } + else + { + mCamera->setProjectionMatrix(osg::Matrixf::ortho(left, right, bottom, top, near, far)); + } + + mCamera->setViewMatrixAsLookAt( + osg::Vec3(pos.x(), pos.y(), zmax), osg::Vec3(pos.x(), pos.y(), zmin), osg::Vec3(0, 1, 0)); + } + + void PrecipitationOccluder::enable() + { + mSkyCullCallback = new PrecipitationOcclusionUpdater(mDepthTexture); + mSkyNode->addCullCallback(mSkyCullCallback); + mCamera->setCullCallback(new DepthCameraUpdater); + + mRootNode->removeChild(mCamera); + mRootNode->addChild(mCamera); + } + + void PrecipitationOccluder::disable() + { + mSkyNode->removeCullCallback(mSkyCullCallback); + mCamera->setCullCallback(nullptr); + mSkyCullCallback = nullptr; + + mRootNode->removeChild(mCamera); + mRange = std::nullopt; + } + + void PrecipitationOccluder::updateRange(const osg::Vec3f range) + { + assert(range.x() != 0); + assert(range.y() != 0); + assert(range.z() != 0); + const osg::Vec3f margin = { -50, -50, 0 }; + mRange = range - margin; + } +} diff --git a/apps/openmw/mwrender/precipitationocclusion.hpp b/apps/openmw/mwrender/precipitationocclusion.hpp new file mode 100644 index 00000000000..9d2992637ef --- /dev/null +++ b/apps/openmw/mwrender/precipitationocclusion.hpp @@ -0,0 +1,36 @@ +#ifndef OPENMW_MWRENDER_PRECIPITATIONOCCLUSION_H +#define OPENMW_MWRENDER_PRECIPITATIONOCCLUSION_H + +#include +#include + +#include + +namespace MWRender +{ + class PrecipitationOccluder + { + public: + PrecipitationOccluder(osg::Group* skyNode, osg::Group* sceneNode, osg::Group* rootNode, osg::Camera* camera); + + void update(); + + void enable(); + + void disable(); + + void updateRange(const osg::Vec3f range); + + private: + osg::Group* mSkyNode; + osg::Group* mSceneNode; + osg::Group* mRootNode; + osg::ref_ptr mSkyCullCallback; + osg::ref_ptr mCamera; + osg::ref_ptr mSceneCamera; + osg::ref_ptr mDepthTexture; + std::optional mRange; + }; +} + +#endif diff --git a/apps/openmw/mwrender/recastmesh.cpp b/apps/openmw/mwrender/recastmesh.cpp index 7c7c6b8eb1b..30cfa824939 100644 --- a/apps/openmw/mwrender/recastmesh.cpp +++ b/apps/openmw/mwrender/recastmesh.cpp @@ -1,16 +1,38 @@ #include "recastmesh.hpp" +#include + +#include +#include +#include +#include +#include #include #include #include "vismask.hpp" +#include "../mwbase/environment.hpp" + namespace MWRender { + namespace + { + osg::ref_ptr makeDebugDrawStateSet() + { + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + + return stateSet; + } + } + RecastMesh::RecastMesh(const osg::ref_ptr& root, bool enabled) : mRootNode(root) , mEnabled(enabled) + , mGroupStateSet(SceneUtil::makeDetourGroupStateSet()) + , mDebugDrawStateSet(makeDebugDrawStateSet()) { } @@ -45,48 +67,64 @@ namespace MWRender continue; } - if (it->second.mGeneration != tile->second->getGeneration() - || it->second.mRevision != tile->second->getRevision()) + if (it->second.mVersion != tile->second->getVersion()) { - const auto group = SceneUtil::createRecastMeshGroup(*tile->second, settings); + const osg::ref_ptr group + = SceneUtil::createRecastMeshGroup(*tile->second, settings.mRecast, mDebugDrawStateSet); group->setNodeMask(Mask_Debug); + group->setStateSet(mGroupStateSet); + + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); + mRootNode->removeChild(it->second.mValue); mRootNode->addChild(group); + it->second.mValue = group; - it->second.mGeneration = tile->second->getGeneration(); - it->second.mRevision = tile->second->getRevision(); - continue; + it->second.mVersion = tile->second->getVersion(); } ++it; } - for (const auto& tile : tiles) + for (const auto& [position, mesh] : tiles) { - if (mGroups.count(tile.first)) - continue; - const auto group = SceneUtil::createRecastMeshGroup(*tile.second, settings); + const auto it = mGroups.find(position); + + if (it != mGroups.end()) + { + if (it->second.mVersion == mesh->getVersion()) + continue; + + mRootNode->removeChild(it->second.mValue); + } + + const osg::ref_ptr group + = SceneUtil::createRecastMeshGroup(*mesh, settings.mRecast, mDebugDrawStateSet); group->setNodeMask(Mask_Debug); - mGroups.emplace(tile.first, Group {tile.second->getGeneration(), tile.second->getRevision(), group}); + group->setStateSet(mGroupStateSet); + + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); + + mGroups.insert_or_assign(it, position, Group{ mesh->getVersion(), group }); mRootNode->addChild(group); } } void RecastMesh::reset() { - std::for_each(mGroups.begin(), mGroups.end(), [&] (const auto& v) { mRootNode->removeChild(v.second.mValue); }); + std::for_each(mGroups.begin(), mGroups.end(), [&](const auto& v) { mRootNode->removeChild(v.second.mValue); }); mGroups.clear(); } void RecastMesh::enable() { - std::for_each(mGroups.begin(), mGroups.end(), [&] (const auto& v) { mRootNode->addChild(v.second.mValue); }); + std::for_each(mGroups.begin(), mGroups.end(), [&](const auto& v) { mRootNode->addChild(v.second.mValue); }); mEnabled = true; } void RecastMesh::disable() { - std::for_each(mGroups.begin(), mGroups.end(), [&] (const auto& v) { mRootNode->removeChild(v.second.mValue); }); + std::for_each(mGroups.begin(), mGroups.end(), [&](const auto& v) { mRootNode->removeChild(v.second.mValue); }); mEnabled = false; } } diff --git a/apps/openmw/mwrender/recastmesh.hpp b/apps/openmw/mwrender/recastmesh.hpp index 729438dbe5d..5fadd0b76af 100644 --- a/apps/openmw/mwrender/recastmesh.hpp +++ b/apps/openmw/mwrender/recastmesh.hpp @@ -1,16 +1,21 @@ #ifndef OPENMW_MWRENDER_RECASTMESH_H #define OPENMW_MWRENDER_RECASTMESH_H -#include +#include +#include #include -#include - namespace osg { class Group; class Geometry; + class StateSet; +} + +namespace DetourNavigator +{ + struct Settings; } namespace MWRender @@ -31,22 +36,20 @@ namespace MWRender void disable(); - bool isEnabled() const - { - return mEnabled; - } + bool isEnabled() const { return mEnabled; } private: struct Group { - std::size_t mGeneration; - std::size_t mRevision; + DetourNavigator::Version mVersion; osg::ref_ptr mValue; }; osg::ref_ptr mRootNode; bool mEnabled; std::map mGroups; + osg::ref_ptr mGroupStateSet; + osg::ref_ptr mDebugDrawStateSet; }; } diff --git a/apps/openmw/mwrender/renderbin.hpp b/apps/openmw/mwrender/renderbin.hpp index c14f611426a..6f4ae0819b5 100644 --- a/apps/openmw/mwrender/renderbin.hpp +++ b/apps/openmw/mwrender/renderbin.hpp @@ -13,7 +13,8 @@ namespace MWRender RenderBin_DepthSorted = 10, // osg::StateSet::TRANSPARENT_BIN RenderBin_OcclusionQuery = 11, RenderBin_FirstPerson = 12, - RenderBin_SunGlare = 13 + RenderBin_SunGlare = 13, + RenderBin_Distortion = 14, }; } diff --git a/apps/openmw/mwrender/renderinginterface.hpp b/apps/openmw/mwrender/renderinginterface.hpp index 63039b612a5..0fe7d2506f0 100644 --- a/apps/openmw/mwrender/renderinginterface.hpp +++ b/apps/openmw/mwrender/renderinginterface.hpp @@ -5,12 +5,12 @@ namespace MWRender { class Objects; class Actors; - + class RenderingInterface { public: virtual MWRender::Objects& getObjects() = 0; - virtual ~RenderingInterface(){} + virtual ~RenderingInterface() {} }; } #endif diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index e497fdecd2e..44e05d6115c 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -1,16 +1,17 @@ #include "renderingmanager.hpp" -#include #include +#include +#include +#include +#include +#include #include #include -#include #include #include -#include #include -#include #include @@ -20,57 +21,190 @@ #include -#include +#include +#include + #include -#include #include +#include #include #include -#include +#include -#include +#include +#include #include -#include #include +#include +#include +#include +#include #include -#include #include -#include -#include +#include + #include +#include -#include +#include +#include +#include #include +#include #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" -#include "../mwgui/loadingscreen.hpp" -#include "../mwbase/windowmanager.hpp" +#include "../mwworld/groundcoverstore.hpp" +#include "../mwworld/scene.hpp" + +#include "../mwgui/postprocessorhud.hpp" + #include "../mwmechanics/actorutil.hpp" -#include "sky.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +#include "actorspaths.hpp" +#include "camera.hpp" #include "effectmanager.hpp" +#include "fogmanager.hpp" +#include "groundcover.hpp" +#include "navmesh.hpp" #include "npcanimation.hpp" -#include "vismask.hpp" +#include "objectpaging.hpp" #include "pathgrid.hpp" -#include "camera.hpp" -#include "viewovershoulder.hpp" -#include "water.hpp" -#include "terrainstorage.hpp" -#include "navmesh.hpp" -#include "actorspaths.hpp" +#include "postprocessor.hpp" #include "recastmesh.hpp" -#include "fogmanager.hpp" -#include "objectpaging.hpp" #include "screenshotmanager.hpp" -#include "groundcover.hpp" +#include "sky.hpp" +#include "terrainstorage.hpp" +#include "util.hpp" +#include "vismask.hpp" +#include "water.hpp" namespace MWRender { + class PerViewUniformStateUpdater final : public SceneUtil::StateSetUpdater + { + public: + PerViewUniformStateUpdater(Resource::SceneManager* sceneManager) + : mSceneManager(sceneManager) + { + mOpaqueTextureUnit = mSceneManager->getShaderManager().reserveGlobalTextureUnits( + Shader::ShaderManager::Slot::OpaqueDepthTexture); + } + + void setDefaults(osg::StateSet* stateset) override + { + stateset->addUniform(new osg::Uniform("projectionMatrix", osg::Matrixf{})); + if (mSkyRTT) + stateset->addUniform(new osg::Uniform("sky", mSkyTextureUnit)); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + stateset->getUniform("projectionMatrix")->set(mProjectionMatrix); + if (mSkyRTT && nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) + { + osg::Texture* skyTexture = mSkyRTT->getColorTexture(static_cast(nv)); + stateset->setTextureAttribute( + mSkyTextureUnit, skyTexture, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + } + + stateset->setTextureAttribute(mOpaqueTextureUnit, + mSceneManager->getOpaqueDepthTex(nv->getTraversalNumber()), osg::StateAttribute::ON); + } + + void applyLeft(osg::StateSet* stateset, osgUtil::CullVisitor* nv) override + { + stateset->getUniform("projectionMatrix")->set(getEyeProjectionMatrix(0)); + } + + void applyRight(osg::StateSet* stateset, osgUtil::CullVisitor* nv) override + { + stateset->getUniform("projectionMatrix")->set(getEyeProjectionMatrix(1)); + } + + void setProjectionMatrix(const osg::Matrixf& projectionMatrix) { mProjectionMatrix = projectionMatrix; } + + const osg::Matrixf& getProjectionMatrix() const { return mProjectionMatrix; } + + void enableSkyRTT(int skyTextureUnit, SceneUtil::RTTNode* skyRTT) + { + mSkyTextureUnit = skyTextureUnit; + mSkyRTT = skyRTT; + } + + private: + osg::Matrixf getEyeProjectionMatrix(int view) + { + return Stereo::Manager::instance().computeEyeProjection(view, SceneUtil::AutoDepth::isReversed()); + } + + osg::Matrixf mProjectionMatrix; + int mSkyTextureUnit = -1; + SceneUtil::RTTNode* mSkyRTT = nullptr; + + Resource::SceneManager* mSceneManager; + int mOpaqueTextureUnit = -1; + }; + + class SharedUniformStateUpdater : public SceneUtil::StateSetUpdater + { + public: + SharedUniformStateUpdater() + : mNear(0.f) + , mFar(0.f) + , mWindSpeed(0.f) + , mSkyBlendingStartCoef(Settings::fog().mSkyBlendingStart) + { + } + + void setDefaults(osg::StateSet* stateset) override + { + stateset->addUniform(new osg::Uniform("near", 0.f)); + stateset->addUniform(new osg::Uniform("far", 0.f)); + stateset->addUniform(new osg::Uniform("skyBlendingStart", 0.f)); + stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{})); + stateset->addUniform(new osg::Uniform("isReflection", false)); + stateset->addUniform(new osg::Uniform("windSpeed", 0.0f)); + stateset->addUniform(new osg::Uniform("playerPos", osg::Vec3f(0.f, 0.f, 0.f))); + stateset->addUniform(new osg::Uniform("useTreeAnim", false)); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + stateset->getUniform("near")->set(mNear); + stateset->getUniform("far")->set(mFar); + stateset->getUniform("skyBlendingStart")->set(mFar * mSkyBlendingStartCoef); + stateset->getUniform("screenRes")->set(mScreenRes); + stateset->getUniform("windSpeed")->set(mWindSpeed); + stateset->getUniform("playerPos")->set(mPlayerPos); + } + + void setNear(float near) { mNear = near; } + + void setFar(float far) { mFar = far; } + + void setScreenRes(float width, float height) { mScreenRes = osg::Vec2f(width, height); } + + void setWindSpeed(float windSpeed) { mWindSpeed = windSpeed; } + + void setPlayerPos(osg::Vec3f playerPos) { mPlayerPos = playerPos; } + + private: + float mNear; + float mFar; + float mWindSpeed; + float mSkyBlendingStartCoef; + osg::Vec3f mPlayerPos; + osg::Vec2f mScreenRes; + }; class StateUpdater : public SceneUtil::StateSetUpdater { @@ -82,7 +216,7 @@ namespace MWRender { } - void setDefaults(osg::StateSet *stateset) override + void setDefaults(osg::StateSet* stateset) override { osg::LightModel* lightModel = new osg::LightModel; stateset->setAttribute(lightModel, osg::StateAttribute::ON); @@ -101,7 +235,8 @@ namespace MWRender void apply(osg::StateSet* stateset, osg::NodeVisitor*) override { - osg::LightModel* lightModel = static_cast(stateset->getAttribute(osg::StateAttribute::LIGHTMODEL)); + osg::LightModel* lightModel + = static_cast(stateset->getAttribute(osg::StateAttribute::LIGHTMODEL)); lightModel->setAmbientIntensity(mAmbientColor); osg::Fog* fog = static_cast(stateset->getAttribute(osg::StateAttribute::FOG)); fog->setColor(mFogColor); @@ -109,25 +244,13 @@ namespace MWRender fog->setEnd(mFogEnd); } - void setAmbientColor(const osg::Vec4f& col) - { - mAmbientColor = col; - } + void setAmbientColor(const osg::Vec4f& col) { mAmbientColor = col; } - void setFogColor(const osg::Vec4f& col) - { - mFogColor = col; - } + void setFogColor(const osg::Vec4f& col) { mFogColor = col; } - void setFogStart(float start) - { - mFogStart = start; - } + void setFogStart(float start) { mFogStart = start; } - void setFogEnd(float end) - { - mFogEnd = end; - } + void setFogEnd(float end) { mFogEnd = end; } void setWireframe(bool wireframe) { @@ -138,10 +261,7 @@ namespace MWRender } } - bool getWireframe() const - { - return mWireframe; - } + bool getWireframe() const { return mWireframe; } private: osg::Vec4f mAmbientColor; @@ -163,67 +283,126 @@ namespace MWRender { try { - for (std::vector::const_iterator it = mModels.begin(); it != mModels.end(); ++it) - mResourceSystem->getSceneManager()->cacheInstance(*it); - for (std::vector::const_iterator it = mTextures.begin(); it != mTextures.end(); ++it) - mResourceSystem->getImageManager()->getImage(*it); - for (std::vector::const_iterator it = mKeyframes.begin(); it != mKeyframes.end(); ++it) - mResourceSystem->getKeyframeManager()->get(*it); + for (const VFS::Path::Normalized& v : mModels) + mResourceSystem->getSceneManager()->getTemplate(v); + for (const VFS::Path::Normalized& v : mTextures) + mResourceSystem->getImageManager()->getImage(v); + for (const VFS::Path::Normalized& v : mKeyframes) + mResourceSystem->getKeyframeManager()->get(v); } - catch (std::exception&) + catch (const std::exception& e) { - // ignore error (will be shown when these are needed proper) + Log(Debug::Warning) << "Failed to preload common assets: " << e.what(); } } - std::vector mModels; - std::vector mTextures; - std::vector mKeyframes; + std::vector mModels; + std::vector mTextures; + std::vector mKeyframes; private: Resource::ResourceSystem* mResourceSystem; }; RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, - Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, - const std::string& resourcePath, DetourNavigator::Navigator& navigator) - : mViewer(viewer) + Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, + DetourNavigator::Navigator& navigator, const MWWorld::GroundcoverStore& groundcoverStore, + SceneUtil::UnrefQueue& unrefQueue) + : mSkyBlending(Settings::fog().mSkyBlending) + , mViewer(viewer) , mRootNode(rootNode) , mResourceSystem(resourceSystem) , mWorkQueue(workQueue) - , mUnrefQueue(new SceneUtil::UnrefQueue) , mNavigator(navigator) - , mMinimumAmbientLuminance(0.f) , mNightEyeFactor(0.f) + // TODO: Near clip should not need to be bounded like this, but too small values break OSG shadow calculations + // CPU-side. See issue: #6072 + , mNearClip(Settings::camera().mNearClip) + , mViewDistance(Settings::camera().mViewingDistance) , mFieldOfViewOverridden(false) , mFieldOfViewOverride(0.f) + , mFieldOfView(Settings::camera().mFieldOfView) + , mFirstPersonFieldOfView(Settings::camera().mFirstPersonFieldOfView) + , mGroundCoverStore(groundcoverStore) { - auto lightingMethod = SceneUtil::LightManager::getLightingMethodFromString(Settings::Manager::getString("lighting method", "Shaders")); + bool reverseZ = SceneUtil::AutoDepth::isReversed(); + const SceneUtil::LightingMethod lightingMethod = Settings::shaders().mLightingMethod; resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); - resourceSystem->getSceneManager()->setShaderPath(resourcePath + "/shaders"); - // Shadows and radial fog have problems with fixed-function mode - bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") - || Settings::Manager::getBool("force shaders", "Shaders") - || Settings::Manager::getBool("enable shadows", "Shadows") - || lightingMethod != SceneUtil::LightingMethod::FFP; + + // Figure out which pipeline must be used by default and inform the user + bool forceShaders = Settings::shaders().mForceShaders; + { + std::vector requesters; + if (!forceShaders) + { + if (Settings::fog().mRadialFog) + requesters.push_back("radial fog"); + if (Settings::fog().mExponentialFog) + requesters.push_back("exponential fog"); + if (mSkyBlending) + requesters.push_back("sky blending"); + if (Settings::shaders().mSoftParticles) + requesters.push_back("soft particles"); + if (Settings::shadows().mEnableShadows) + requesters.push_back("shadows"); + if (lightingMethod != SceneUtil::LightingMethod::FFP) + requesters.push_back("lighting method"); + if (reverseZ) + requesters.push_back("reverse-Z depth buffer"); + if (Stereo::getMultiview()) + requesters.push_back("stereo multiview"); + + if (!requesters.empty()) + forceShaders = true; + } + + if (forceShaders) + { + std::string message = "Using rendering with shaders by default"; + if (requesters.empty()) + { + message += " (forced)"; + } + else + { + message += ", requested by:"; + for (size_t i = 0; i < requesters.size(); i++) + message += "\n - " + requesters[i]; + } + Log(Debug::Info) << message; + } + else + { + Log(Debug::Info) << "Using fixed-function rendering by default"; + } + } + resourceSystem->getSceneManager()->setForceShaders(forceShaders); // FIXME: calling dummy method because terrain needs to know whether lighting is clamped - resourceSystem->getSceneManager()->setClampLighting(Settings::Manager::getBool("clamp lighting", "Shaders")); - resourceSystem->getSceneManager()->setAutoUseNormalMaps(Settings::Manager::getBool("auto use object normal maps", "Shaders")); - resourceSystem->getSceneManager()->setNormalMapPattern(Settings::Manager::getString("normal map pattern", "Shaders")); - resourceSystem->getSceneManager()->setNormalHeightMapPattern(Settings::Manager::getString("normal height map pattern", "Shaders")); - resourceSystem->getSceneManager()->setAutoUseSpecularMaps(Settings::Manager::getBool("auto use object specular maps", "Shaders")); - resourceSystem->getSceneManager()->setSpecularMapPattern(Settings::Manager::getString("specular map pattern", "Shaders")); - resourceSystem->getSceneManager()->setApplyLightingToEnvMaps(Settings::Manager::getBool("apply lighting to environment maps", "Shaders")); - resourceSystem->getSceneManager()->setConvertAlphaTestToAlphaToCoverage(Settings::Manager::getBool("antialias alpha test", "Shaders") && Settings::Manager::getInt("antialiasing", "Video") > 1); - - // Let LightManager choose which backend to use based on our hint. For methods besides legacy lighting, this depends on support for various OpenGL extensions. - osg::ref_ptr sceneRoot = new SceneUtil::LightManager(lightingMethod == SceneUtil::LightingMethod::FFP); - resourceSystem->getSceneManager()->getShaderManager().setLightingMethod(sceneRoot->getLightingMethod()); + resourceSystem->getSceneManager()->setClampLighting(Settings::shaders().mClampLighting); + resourceSystem->getSceneManager()->setAutoUseNormalMaps(Settings::shaders().mAutoUseObjectNormalMaps); + resourceSystem->getSceneManager()->setNormalMapPattern(Settings::shaders().mNormalMapPattern); + resourceSystem->getSceneManager()->setNormalHeightMapPattern(Settings::shaders().mNormalHeightMapPattern); + resourceSystem->getSceneManager()->setAutoUseSpecularMaps(Settings::shaders().mAutoUseObjectSpecularMaps); + resourceSystem->getSceneManager()->setSpecularMapPattern(Settings::shaders().mSpecularMapPattern); + resourceSystem->getSceneManager()->setApplyLightingToEnvMaps( + Settings::shaders().mApplyLightingToEnvironmentMaps); + resourceSystem->getSceneManager()->setConvertAlphaTestToAlphaToCoverage(shouldAddMSAAIntermediateTarget()); + resourceSystem->getSceneManager()->setAdjustCoverageForAlphaTest( + Settings::shaders().mAdjustCoverageForAlphaTest); + + // Let LightManager choose which backend to use based on our hint. For methods besides legacy lighting, this + // depends on support for various OpenGL extensions. + osg::ref_ptr sceneRoot = new SceneUtil::LightManager(SceneUtil::LightSettings{ + .mLightingMethod = lightingMethod, + .mMaxLights = Settings::shaders().mMaxLights, + .mMaximumLightDistance = Settings::shaders().mMaximumLightDistance, + .mLightFadeStart = Settings::shaders().mLightFadeStart, + .mLightBoundsMultiplier = Settings::shaders().mLightBoundsMultiplier, + }); resourceSystem->getSceneManager()->setLightingMethod(sceneRoot->getLightingMethod()); resourceSystem->getSceneManager()->setSupportedLightingMethods(sceneRoot->getSupportedLightingMethods()); - mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); sceneRoot->setLightingMask(Mask_Lighting); mSceneRoot = sceneRoot; @@ -232,136 +411,118 @@ namespace MWRender sceneRoot->setName("Scene Root"); int shadowCastingTraversalMask = Mask_Scene; - if (Settings::Manager::getBool("actor shadows", "Shadows")) + if (Settings::shadows().mActorShadows) shadowCastingTraversalMask |= Mask_Actor; - if (Settings::Manager::getBool("player shadows", "Shadows")) + if (Settings::shadows().mPlayerShadows) shadowCastingTraversalMask |= Mask_Player; - if (Settings::Manager::getBool("terrain shadows", "Shadows")) - shadowCastingTraversalMask |= Mask_Terrain; int indoorShadowCastingTraversalMask = shadowCastingTraversalMask; - if (Settings::Manager::getBool("object shadows", "Shadows")) - shadowCastingTraversalMask |= (Mask_Object|Mask_Static); + if (Settings::shadows().mObjectShadows) + shadowCastingTraversalMask |= (Mask_Object | Mask_Static); + if (Settings::shadows().mTerrainShadows) + shadowCastingTraversalMask |= Mask_Terrain; - mShadowManager.reset(new SceneUtil::ShadowManager(sceneRoot, mRootNode, shadowCastingTraversalMask, indoorShadowCastingTraversalMask, mResourceSystem->getSceneManager()->getShaderManager())); + mShadowManager = std::make_unique(sceneRoot, mRootNode, shadowCastingTraversalMask, + indoorShadowCastingTraversalMask, Mask_Terrain | Mask_Object | Mask_Static, Settings::shadows(), + mResourceSystem->getSceneManager()->getShaderManager()); - Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(); + Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(Settings::shadows()); Shader::ShaderManager::DefineMap lightDefines = sceneRoot->getLightDefines(); - Shader::ShaderManager::DefineMap globalDefines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); + Shader::ShaderManager::DefineMap globalDefines + = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); for (auto itr = shadowDefines.begin(); itr != shadowDefines.end(); itr++) globalDefines[itr->first] = itr->second; - globalDefines["forcePPL"] = Settings::Manager::getBool("force per pixel lighting", "Shaders") ? "1" : "0"; - globalDefines["clamp"] = Settings::Manager::getBool("clamp lighting", "Shaders") ? "1" : "0"; - globalDefines["preLightEnv"] = Settings::Manager::getBool("apply lighting to environment maps", "Shaders") ? "1" : "0"; - globalDefines["radialFog"] = Settings::Manager::getBool("radial fog", "Shaders") ? "1" : "0"; + globalDefines["forcePPL"] = Settings::shaders().mForcePerPixelLighting ? "1" : "0"; + globalDefines["clamp"] = Settings::shaders().mClampLighting ? "1" : "0"; + globalDefines["preLightEnv"] = Settings::shaders().mApplyLightingToEnvironmentMaps ? "1" : "0"; + globalDefines["classicFalloff"] = Settings::shaders().mClassicFalloff ? "1" : "0"; + const bool exponentialFog = Settings::fog().mExponentialFog; + globalDefines["radialFog"] = (exponentialFog || Settings::fog().mRadialFog) ? "1" : "0"; + globalDefines["exponentialFog"] = exponentialFog ? "1" : "0"; + globalDefines["skyBlending"] = mSkyBlending ? "1" : "0"; + globalDefines["waterRefraction"] = "0"; globalDefines["useGPUShader4"] = "0"; + globalDefines["useOVR_multiview"] = "0"; + globalDefines["numViews"] = "1"; + globalDefines["disableNormals"] = "1"; for (auto itr = lightDefines.begin(); itr != lightDefines.end(); itr++) globalDefines[itr->first] = itr->second; // Refactor this at some point - most shaders don't care about these defines - float groundcoverDistance = std::max(0.f, Settings::Manager::getFloat("rendering distance", "Groundcover")); + const float groundcoverDistance = Settings::groundcover().mRenderingDistance; globalDefines["groundcoverFadeStart"] = std::to_string(groundcoverDistance * 0.9f); globalDefines["groundcoverFadeEnd"] = std::to_string(groundcoverDistance); - globalDefines["groundcoverStompMode"] = std::to_string(std::clamp(Settings::Manager::getInt("stomp mode", "Groundcover"), 0, 2)); - globalDefines["groundcoverStompIntensity"] = std::to_string(std::clamp(Settings::Manager::getInt("stomp intensity", "Groundcover"), 0, 2)); + globalDefines["groundcoverStompMode"] = std::to_string(Settings::groundcover().mStompMode); + globalDefines["groundcoverStompIntensity"] = std::to_string(Settings::groundcover().mStompIntensity); + + globalDefines["reverseZ"] = reverseZ ? "1" : "0"; // It is unnecessary to stop/start the viewer as no frames are being rendered yet. mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines); - mNavMesh.reset(new NavMesh(mRootNode, Settings::Manager::getBool("enable nav mesh render", "Navigator"))); - mActorsPaths.reset(new ActorsPaths(mRootNode, Settings::Manager::getBool("enable agents paths render", "Navigator"))); - mRecastMesh.reset(new RecastMesh(mRootNode, Settings::Manager::getBool("enable recast mesh render", "Navigator"))); - mPathgrid.reset(new Pathgrid(mRootNode)); + mNavMesh = std::make_unique(mRootNode, mWorkQueue, Settings::navigator().mEnableNavMeshRender, + Settings::navigator().mNavMeshRenderMode); + mActorsPaths = std::make_unique(mRootNode, Settings::navigator().mEnableAgentsPathsRender); + mRecastMesh = std::make_unique(mRootNode, Settings::navigator().mEnableRecastMeshRender); + mPathgrid = std::make_unique(mRootNode); - mObjects.reset(new Objects(mResourceSystem, sceneRoot, mUnrefQueue.get())); + mObjects = std::make_unique(mResourceSystem, sceneRoot, unrefQueue); if (getenv("OPENMW_DONT_PRECOMPILE") == nullptr) { mViewer->setIncrementalCompileOperation(new osgUtil::IncrementalCompileOperation); - mViewer->getIncrementalCompileOperation()->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells")); + mViewer->getIncrementalCompileOperation()->setTargetFrameRate(Settings::cells().mTargetFramerate); } + mDebugDraw = new Debug::DebugDrawer(mResourceSystem->getSceneManager()->getShaderManager()); + mDebugDraw->setNodeMask(Mask_Debug); + sceneRoot->addChild(mDebugDraw); + mResourceSystem->getSceneManager()->setIncrementalCompileOperation(mViewer->getIncrementalCompileOperation()); - mEffectManager.reset(new EffectManager(sceneRoot, mResourceSystem)); + mEffectManager = std::make_unique(sceneRoot, mResourceSystem); - const std::string normalMapPattern = Settings::Manager::getString("normal map pattern", "Shaders"); - const std::string heightMapPattern = Settings::Manager::getString("normal height map pattern", "Shaders"); - const std::string specularMapPattern = Settings::Manager::getString("terrain specular map pattern", "Shaders"); - const bool useTerrainNormalMaps = Settings::Manager::getBool("auto use terrain normal maps", "Shaders"); - const bool useTerrainSpecularMaps = Settings::Manager::getBool("auto use terrain specular maps", "Shaders"); + const std::string& normalMapPattern = Settings::shaders().mNormalMapPattern; + const std::string& heightMapPattern = Settings::shaders().mNormalHeightMapPattern; + const std::string& specularMapPattern = Settings::shaders().mTerrainSpecularMapPattern; + const bool useTerrainNormalMaps = Settings::shaders().mAutoUseTerrainNormalMaps; + const bool useTerrainSpecularMaps = Settings::shaders().mAutoUseTerrainSpecularMaps; - mTerrainStorage.reset(new TerrainStorage(mResourceSystem, normalMapPattern, heightMapPattern, useTerrainNormalMaps, specularMapPattern, useTerrainSpecularMaps)); - const float lodFactor = Settings::Manager::getFloat("lod factor", "Terrain"); + mTerrainStorage = std::make_unique(mResourceSystem, normalMapPattern, heightMapPattern, + useTerrainNormalMaps, specularMapPattern, useTerrainSpecularMaps); - if (Settings::Manager::getBool("distant terrain", "Terrain")) - { - const int compMapResolution = Settings::Manager::getInt("composite map resolution", "Terrain"); - int compMapPower = Settings::Manager::getInt("composite map level", "Terrain"); - compMapPower = std::max(-3, compMapPower); - float compMapLevel = pow(2, compMapPower); - const int vertexLodMod = Settings::Manager::getInt("vertex lod mod", "Terrain"); - float maxCompGeometrySize = Settings::Manager::getFloat("max composite geometry size", "Terrain"); - maxCompGeometrySize = std::max(maxCompGeometrySize, 1.f); - mTerrain.reset(new Terrain::QuadTreeWorld( - sceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug, - compMapResolution, compMapLevel, lodFactor, vertexLodMod, maxCompGeometrySize)); - if (Settings::Manager::getBool("object paging", "Terrain")) - { - mObjectPaging.reset(new ObjectPaging(mResourceSystem->getSceneManager())); - static_cast(mTerrain.get())->addChunkManager(mObjectPaging.get()); - mResourceSystem->addResourceManager(mObjectPaging.get()); - } - } - else - mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug)); + WorldspaceChunkMgr& chunkMgr = getWorldspaceChunkMgr(ESM::Cell::sDefaultWorldspaceId); + mTerrain = chunkMgr.mTerrain.get(); + mGroundcover = chunkMgr.mGroundcover.get(); + mObjectPaging = chunkMgr.mObjectPaging.get(); - mTerrain->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells")); - mTerrain->setWorkQueue(mWorkQueue.get()); + mStateUpdater = new StateUpdater; + sceneRoot->addUpdateCallback(mStateUpdater); + + mSharedUniformStateUpdater = new SharedUniformStateUpdater(); + rootNode->addUpdateCallback(mSharedUniformStateUpdater); + + mPerViewUniformStateUpdater = new PerViewUniformStateUpdater(mResourceSystem->getSceneManager()); + rootNode->addCullCallback(mPerViewUniformStateUpdater); + + mPostProcessor = new PostProcessor(*this, viewer, mRootNode, resourceSystem->getVFS()); + resourceSystem->getSceneManager()->setOpaqueDepthTex( + mPostProcessor->getTexture(PostProcessor::Tex_OpaqueDepth, 0), + mPostProcessor->getTexture(PostProcessor::Tex_OpaqueDepth, 1)); + resourceSystem->getSceneManager()->setSoftParticles(Settings::shaders().mSoftParticles); + resourceSystem->getSceneManager()->setSupportsNormalsRT(mPostProcessor->getSupportsNormalsRT()); + resourceSystem->getSceneManager()->setWeatherParticleOcclusion(Settings::shaders().mWeatherParticleOcclusion); - if (Settings::Manager::getBool("enabled", "Groundcover")) - { - osg::ref_ptr groundcoverRoot = new osg::Group; - groundcoverRoot->setNodeMask(Mask_Groundcover); - groundcoverRoot->setName("Groundcover Root"); - sceneRoot->addChild(groundcoverRoot); - - mGroundcoverUpdater = new GroundcoverUpdater; - groundcoverRoot->addUpdateCallback(mGroundcoverUpdater); - - float chunkSize = Settings::Manager::getFloat("min chunk size", "Groundcover"); - if (chunkSize >= 1.0f) - chunkSize = 1.0f; - else if (chunkSize >= 0.5f) - chunkSize = 0.5f; - else if (chunkSize >= 0.25f) - chunkSize = 0.25f; - else if (chunkSize != 0.125f) - chunkSize = 0.125f; - - float density = Settings::Manager::getFloat("density", "Groundcover"); - density = std::clamp(density, 0.f, 1.f); - - mGroundcoverWorld.reset(new Terrain::QuadTreeWorld(groundcoverRoot, mTerrainStorage.get(), Mask_Groundcover, lodFactor, chunkSize)); - mGroundcover.reset(new Groundcover(mResourceSystem->getSceneManager(), density)); - static_cast(mGroundcoverWorld.get())->addChunkManager(mGroundcover.get()); - mResourceSystem->addResourceManager(mGroundcover.get()); - - // Groundcover it is handled in the same way indifferently from if it is from active grid or from distant cell. - // Use a stub grid to avoid splitting between chunks for active grid and chunks for distant cells. - mGroundcoverWorld->setActiveGrid(osg::Vec4i(0, 0, 0, 0)); - } // water goes after terrain for correct waterculling order - mWater.reset(new Water(sceneRoot->getParent(0), sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), resourcePath)); + mWater = std::make_unique( + sceneRoot->getParent(0), sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation()); - mCamera.reset(new Camera(mViewer->getCamera())); - if (Settings::Manager::getBool("view over shoulder", "Camera")) - mViewOverShoulderController.reset(new ViewOverShoulderController(mCamera.get())); + mCamera = std::make_unique(mViewer->getCamera()); - mScreenshotManager.reset(new ScreenshotManager(viewer, mRootNode, sceneRoot, mResourceSystem, mWater.get())); + mScreenshotManager = std::make_unique(viewer); mViewer->setLightingMode(osgViewer::View::NO_LIGHT); @@ -369,9 +530,9 @@ namespace MWRender source->setNodeMask(Mask_Lighting); mSunLight = new osg::Light; source->setLight(mSunLight); - mSunLight->setDiffuse(osg::Vec4f(0,0,0,1)); - mSunLight->setAmbient(osg::Vec4f(0,0,0,1)); - mSunLight->setSpecular(osg::Vec4f(0,0,0,0)); + mSunLight->setDiffuse(osg::Vec4f(0, 0, 0, 1)); + mSunLight->setAmbient(osg::Vec4f(0, 0, 0, 1)); + mSunLight->setSpecular(osg::Vec4f(0, 0, 0, 0)); mSunLight->setConstantAttenuation(1.f); sceneRoot->setSunlight(mSunLight); sceneRoot->addChild(source); @@ -379,63 +540,73 @@ namespace MWRender sceneRoot->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::ON); sceneRoot->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::ON); sceneRoot->getOrCreateStateSet()->setMode(GL_NORMALIZE, osg::StateAttribute::ON); - osg::ref_ptr defaultMat (new osg::Material); + osg::ref_ptr defaultMat(new osg::Material); defaultMat->setColorMode(osg::Material::OFF); - defaultMat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - defaultMat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); + defaultMat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, 1)); + defaultMat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, 1)); defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); sceneRoot->getOrCreateStateSet()->setAttribute(defaultMat); + sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f)); + sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("specStrength", 1.f)); + sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("distortionStrength", 0.f)); - mFog.reset(new FogManager()); + resourceSystem->getSceneManager()->setUpNormalsRTForStateSet(sceneRoot->getOrCreateStateSet(), true); - mSky.reset(new SkyManager(sceneRoot, resourceSystem->getSceneManager())); - mSky->setCamera(mViewer->getCamera()); + mFog = std::make_unique(); - source->setStateSetModes(*mRootNode->getOrCreateStateSet(), osg::StateAttribute::ON); + mSky = std::make_unique( + sceneRoot, mRootNode, mViewer->getCamera(), resourceSystem->getSceneManager(), mSkyBlending); + if (mSkyBlending) + { + int skyTextureUnit = mResourceSystem->getSceneManager()->getShaderManager().reserveGlobalTextureUnits( + Shader::ShaderManager::Slot::SkyTexture); + mPerViewUniformStateUpdater->enableSkyRTT(skyTextureUnit, mSky->getSkyRTT()); + } - mStateUpdater = new StateUpdater; - sceneRoot->addUpdateCallback(mStateUpdater); + source->setStateSetModes(*mRootNode->getOrCreateStateSet(), osg::StateAttribute::ON); - osg::Camera::CullingMode cullingMode = osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING; + osg::Camera::CullingMode cullingMode = osg::Camera::DEFAULT_CULLING | osg::Camera::FAR_PLANE_CULLING; - if (!Settings::Manager::getBool("small feature culling", "Camera")) + if (!Settings::camera().mSmallFeatureCulling) cullingMode &= ~(osg::CullStack::SMALL_FEATURE_CULLING); else { - mViewer->getCamera()->setSmallFeatureCullingPixelSize(Settings::Manager::getFloat("small feature culling pixel size", "Camera")); + mViewer->getCamera()->setSmallFeatureCullingPixelSize(Settings::camera().mSmallFeatureCullingPixelSize); cullingMode |= osg::CullStack::SMALL_FEATURE_CULLING; } - mViewer->getCamera()->setCullingMode( cullingMode ); - mViewer->getCamera()->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); mViewer->getCamera()->setCullingMode(cullingMode); + mViewer->getCamera()->setName(Constants::SceneCamera); - mViewer->getCamera()->setCullMask(~(Mask_UpdateVisitor|Mask_SimpleWater)); + auto mask = ~(Mask_UpdateVisitor | Mask_SimpleWater); + MWBase::Environment::get().getWindowManager()->setCullMask(mask); NifOsg::Loader::setHiddenNodeMask(Mask_UpdateVisitor); NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect); - Nif::NIFFile::setLoadUnsupportedFiles(Settings::Manager::getBool("load unsupported nif files", "Models")); - - mNearClip = Settings::Manager::getFloat("near clip", "Camera"); - mViewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); - float fov = Settings::Manager::getFloat("field of view", "Camera"); - mFieldOfView = std::min(std::max(1.f, fov), 179.f); - float firstPersonFov = Settings::Manager::getFloat("first person field of view", "Camera"); - mFirstPersonFieldOfView = std::min(std::max(1.f, firstPersonFov), 179.f); - mStateUpdater->setFogEnd(mViewDistance); + Nif::Reader::setLoadUnsupportedFiles(Settings::models().mLoadUnsupportedNifFiles); + Nif::Reader::setWriteNifDebugLog(Settings::models().mWriteNifDebugLog); - mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("near", mNearClip)); - mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("far", mViewDistance)); - mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("simpleWater", false)); + mStateUpdater->setFogEnd(mViewDistance); // Hopefully, anything genuinely requiring the default alpha func of GL_ALWAYS explicitly sets it mRootNode->getOrCreateStateSet()->setAttribute(Shader::RemovedAlphaFunc::getInstance(GL_ALWAYS)); - // The transparent renderbin sets alpha testing on because that was faster on old GPUs. It's now slower and breaks things. + // The transparent renderbin sets alpha testing on because that was faster on old GPUs. It's now slower and + // breaks things. mRootNode->getOrCreateStateSet()->setMode(GL_ALPHA_TEST, osg::StateAttribute::OFF); - mUniformNear = mRootNode->getOrCreateStateSet()->getUniform("near"); - mUniformFar = mRootNode->getOrCreateStateSet()->getUniform("far"); + if (reverseZ) + { + osg::ref_ptr clipcontrol + = new osg::ClipControl(osg::ClipControl::LOWER_LEFT, osg::ClipControl::ZERO_TO_ONE); + mRootNode->getOrCreateStateSet()->setAttributeAndModes(new SceneUtil::AutoDepth, osg::StateAttribute::ON); + mRootNode->getOrCreateStateSet()->setAttributeAndModes(clipcontrol, osg::StateAttribute::ON); + } + + SceneUtil::setCameraClearDepth(mViewer->getCamera()); + updateProjectionMatrix(); + + mViewer->getCamera()->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); } RenderingManager::~RenderingManager() @@ -464,35 +635,30 @@ namespace MWRender return mWorkQueue.get(); } - SceneUtil::UnrefQueue* RenderingManager::getUnrefQueue() - { - return mUnrefQueue.get(); - } - Terrain::World* RenderingManager::getTerrain() { - return mTerrain.get(); + return mTerrain; } void RenderingManager::preloadCommonAssets() { - osg::ref_ptr workItem (new PreloadCommonAssetsWorkItem(mResourceSystem)); + osg::ref_ptr workItem(new PreloadCommonAssetsWorkItem(mResourceSystem)); mSky->listAssetsToPreload(workItem->mModels, workItem->mTextures); mWater->listAssetsToPreload(workItem->mTextures); - workItem->mModels.push_back(Settings::Manager::getString("xbaseanim", "Models")); - workItem->mModels.push_back(Settings::Manager::getString("xbaseanim1st", "Models")); - workItem->mModels.push_back(Settings::Manager::getString("xbaseanimfemale", "Models")); - workItem->mModels.push_back(Settings::Manager::getString("xargonianswimkna", "Models")); + workItem->mModels.push_back(Settings::models().mXbaseanim); + workItem->mModels.push_back(Settings::models().mXbaseanim1st); + workItem->mModels.push_back(Settings::models().mXbaseanimfemale); + workItem->mModels.push_back(Settings::models().mXargonianswimkna); - workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanimkf", "Models")); - workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanim1stkf", "Models")); - workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanimfemalekf", "Models")); - workItem->mKeyframes.push_back(Settings::Manager::getString("xargonianswimknakf", "Models")); + workItem->mKeyframes.push_back(Settings::models().mXbaseanimkf); + workItem->mKeyframes.push_back(Settings::models().mXbaseanim1stkf); + workItem->mKeyframes.push_back(Settings::models().mXbaseanimfemalekf); + workItem->mKeyframes.push_back(Settings::models().mXargonianswimknakf); workItem->mTextures.emplace_back("textures/_land_default.dds"); - mWorkQueue->addWorkItem(workItem); + mWorkQueue->addWorkItem(std::move(workItem)); } double RenderingManager::getReferenceTime() const @@ -500,7 +666,7 @@ namespace MWRender return mViewer->getFrameStamp()->getReferenceTime(); } - osg::Group* RenderingManager::getLightRoot() + SceneUtil::LightManager* RenderingManager::getLightRoot() { return mSceneRoot.get(); } @@ -514,7 +680,7 @@ namespace MWRender } } - void RenderingManager::setAmbientColour(const osg::Vec4f &colour) + void RenderingManager::setAmbientColour(const osg::Vec4f& colour) { mAmbientColor = colour; updateAmbient(); @@ -540,13 +706,14 @@ namespace MWRender mSky->setMoonColour(red); } - void RenderingManager::configureAmbient(const ESM::Cell *cell) + void RenderingManager::configureAmbient(const MWWorld::Cell& cell) { + bool isInterior = !cell.isExterior() && !cell.isQuasiExterior(); bool needsAdjusting = false; if (mResourceSystem->getSceneManager()->getLightingMethod() != SceneUtil::LightingMethod::FFP) - needsAdjusting = !cell->isExterior() && !(cell->mData.mFlags & ESM::Cell::QuasiEx); + needsAdjusting = isInterior && !Settings::shaders().mClassicFalloff; - auto ambient = SceneUtil::colourFromRGB(cell->mAmbi.mAmbient); + osg::Vec4f ambient = SceneUtil::colourFromRGB(cell.getMood().mAmbiantColor); if (needsAdjusting) { @@ -555,43 +722,57 @@ namespace MWRender constexpr float pB = 0.0722; // we already work in linear RGB so no conversions are needed for the luminosity function - float relativeLuminance = pR*ambient.r() + pG*ambient.g() + pB*ambient.b(); - if (relativeLuminance < mMinimumAmbientLuminance) + float relativeLuminance = pR * ambient.r() + pG * ambient.g() + pB * ambient.b(); + const float minimumAmbientLuminance = Settings::shaders().mMinimumInteriorBrightness; + if (relativeLuminance < minimumAmbientLuminance) { - // brighten ambient so it reaches the minimum threshold but no more, we want to mess with content data as least we can - float targetBrightnessIncreaseFactor = mMinimumAmbientLuminance / relativeLuminance; + // brighten ambient so it reaches the minimum threshold but no more, we want to mess with content data + // as least we can if (ambient.r() == 0.f && ambient.g() == 0.f && ambient.b() == 0.f) - ambient = osg::Vec4(mMinimumAmbientLuminance, mMinimumAmbientLuminance, mMinimumAmbientLuminance, ambient.a()); + ambient = osg::Vec4( + minimumAmbientLuminance, minimumAmbientLuminance, minimumAmbientLuminance, ambient.a()); else - ambient *= targetBrightnessIncreaseFactor; + ambient *= minimumAmbientLuminance / relativeLuminance; } } setAmbientColour(ambient); - osg::Vec4f diffuse = SceneUtil::colourFromRGB(cell->mAmbi.mSunlight); - mSunLight->setDiffuse(diffuse); - mSunLight->setSpecular(diffuse); - mSunLight->setPosition(osg::Vec4f(-0.15f, 0.15f, 1.f, 0.f)); + osg::Vec4f diffuse = SceneUtil::colourFromRGB(cell.getMood().mDirectionalColor); + + setSunColour(diffuse, diffuse, 1.f); + // This is total nonsense but it's what Morrowind uses + static const osg::Vec4f interiorSunPos + = osg::Vec4f(-1.f, osg::DegreesToRadians(45.f), osg::DegreesToRadians(45.f), 0.f); + mPostProcessor->getStateUpdater()->setSunPos(interiorSunPos, false); + mSunLight->setPosition(interiorSunPos); } - void RenderingManager::setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular) + void RenderingManager::setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular, float sunVis) { // need to wrap this in a StateUpdater? mSunLight->setDiffuse(diffuse); - mSunLight->setSpecular(specular); + mSunLight->setSpecular(osg::Vec4f(specular.x(), specular.y(), specular.z(), specular.w() * sunVis)); + + mPostProcessor->getStateUpdater()->setSunColor(diffuse); + mPostProcessor->getStateUpdater()->setSunVis(sunVis); } - void RenderingManager::setSunDirection(const osg::Vec3f &direction) + void RenderingManager::setSunDirection(const osg::Vec3f& direction) { osg::Vec3 position = direction * -1; // need to wrap this in a StateUpdater? mSunLight->setPosition(osg::Vec4(position.x(), position.y(), position.z(), 0)); + // The sun is not synchronized with the sunlight because sunlight origin can't reach the horizon + // This is based on exterior sun orbit and won't make sense for interiors, see WeatherManager::update + position.z() = 400.f - std::abs(position.x()); mSky->setSunDirection(position); + + mPostProcessor->getStateUpdater()->setSunPos(osg::Vec4f(position, 0.f), mNight); } - void RenderingManager::addCell(const MWWorld::CellStore *store) + void RenderingManager::addCell(const MWWorld::CellStore* store) { mPathgrid->addCell(store); @@ -599,12 +780,11 @@ namespace MWRender if (store->getCell()->isExterior()) { + enableTerrain(true, store->getCell()->getWorldSpace()); mTerrain->loadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); - if (mGroundcoverWorld) - mGroundcoverWorld->loadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); } } - void RenderingManager::removeCell(const MWWorld::CellStore *store) + void RenderingManager::removeCell(const MWWorld::CellStore* store) { mPathgrid->removeCell(store); mActorsPaths->removeCell(store); @@ -612,21 +792,29 @@ namespace MWRender if (store->getCell()->isExterior()) { - mTerrain->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); - if (mGroundcoverWorld) - mGroundcoverWorld->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); + getWorldspaceChunkMgr(store->getCell()->getWorldSpace()) + .mTerrain->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); } mWater->removeCell(store); } - void RenderingManager::enableTerrain(bool enable) + void RenderingManager::enableTerrain(bool enable, ESM::RefId worldspace) { if (!enable) mWater->setCullCallback(nullptr); + else + { + WorldspaceChunkMgr& newChunks = getWorldspaceChunkMgr(worldspace); + if (newChunks.mTerrain.get() != mTerrain) + { + mTerrain->enable(false); + mTerrain = newChunks.mTerrain.get(); + mGroundcover = newChunks.mGroundcover.get(); + mObjectPaging = newChunks.mObjectPaging.get(); + } + } mTerrain->enable(enable); - if (mGroundcoverWorld) - mGroundcoverWorld->enable(enable); } void RenderingManager::setSkyEnabled(bool enabled) @@ -635,7 +823,8 @@ namespace MWRender if (enabled) mShadowManager->enableOutdoorMode(); else - mShadowManager->enableIndoorMode(); + mShadowManager->enableIndoorMode(Settings::shadows()); + mPostProcessor->getStateUpdater()->setIsInterior(!enabled); } bool RenderingManager::toggleBorders() @@ -661,14 +850,15 @@ namespace MWRender } else if (mode == Render_Scene) { - unsigned int mask = mViewer->getCamera()->getCullMask(); - bool enabled = mask&Mask_Scene; - enabled = !enabled; + const auto wm = MWBase::Environment::get().getWindowManager(); + unsigned int mask = wm->getCullMask(); + bool enabled = !(mask & sToggleWorldMask); if (enabled) - mask |= Mask_Scene; + mask |= sToggleWorldMask; else - mask &= ~Mask_Scene; - mViewer->getCamera()->setCullMask(mask); + mask &= ~sToggleWorldMask; + mWater->showWorld(enabled); + wm->setCullMask(mask); return enabled; } else if (mode == Render_NavMesh) @@ -686,12 +876,13 @@ namespace MWRender return false; } - void RenderingManager::configureFog(const ESM::Cell *cell) + void RenderingManager::configureFog(const MWWorld::Cell& cell) { mFog->configure(mViewDistance, cell); } - void RenderingManager::configureFog(float fogDepth, float underwaterFog, float dlFactor, float dlOffset, const osg::Vec4f &color) + void RenderingManager::configureFog( + float fogDepth, float underwaterFog, float dlFactor, float dlOffset, const osg::Vec4f& color) { mFog->configure(mViewDistance, fogDepth, underwaterFog, dlFactor, dlOffset, color); } @@ -705,48 +896,65 @@ namespace MWRender { reportStats(); - mUnrefQueue->flush(mWorkQueue.get()); + mResourceSystem->getSceneManager()->getShaderManager().update(*mViewer); float rainIntensity = mSky->getPrecipitationAlpha(); mWater->setRainIntensity(rainIntensity); + mWater->setRainRipplesEnabled(mSky->getRainRipplesEnabled()); + mWater->update(dt, paused); if (!paused) { mEffectManager->update(dt); mSky->update(dt); - mWater->update(dt); - if (mGroundcoverUpdater) - { - const MWWorld::Ptr& player = mPlayerAnimation->getPtr(); - osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); + const MWWorld::Ptr& player = mPlayerAnimation->getPtr(); + osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); - float windSpeed = mSky->getBaseWindSpeed(); - mGroundcoverUpdater->setWindSpeed(windSpeed); - mGroundcoverUpdater->setPlayerPos(playerPos); - } + float windSpeed = mSky->getBaseWindSpeed(); + mSharedUniformStateUpdater->setWindSpeed(windSpeed); + mSharedUniformStateUpdater->setPlayerPos(playerPos); } updateNavMesh(); updateRecastMesh(); - if (mViewOverShoulderController) - mViewOverShoulderController->update(); + if (mUpdateProjectionMatrix) + { + mUpdateProjectionMatrix = false; + updateProjectionMatrix(); + } mCamera->update(dt, paused); - osg::Vec3d focal, cameraPos; - mCamera->getPosition(focal, cameraPos); - mCurrentCameraPos = cameraPos; + bool isUnderwater = mWater->isUnderwater(mCamera->getPosition()); + + float fogStart = mFog->getFogStart(isUnderwater); + float fogEnd = mFog->getFogEnd(isUnderwater); + osg::Vec4f fogColor = mFog->getFogColor(isUnderwater); + + mStateUpdater->setFogStart(fogStart); + mStateUpdater->setFogEnd(fogEnd); + setFogColor(fogColor); - bool isUnderwater = mWater->isUnderwater(cameraPos); - mStateUpdater->setFogStart(mFog->getFogStart(isUnderwater)); - mStateUpdater->setFogEnd(mFog->getFogEnd(isUnderwater)); - setFogColor(mFog->getFogColor(isUnderwater)); + auto world = MWBase::Environment::get().getWorld(); + const auto& stateUpdater = mPostProcessor->getStateUpdater(); + + stateUpdater->setFogRange(fogStart, fogEnd); + stateUpdater->setNearFar(mNearClip, mViewDistance); + stateUpdater->setIsUnderwater(isUnderwater); + stateUpdater->setFogColor(fogColor); + stateUpdater->setGameHour(world->getTimeStamp().getHour()); + stateUpdater->setWeatherId(world->getCurrentWeather()); + stateUpdater->setNextWeatherId(world->getNextWeather()); + stateUpdater->setWeatherTransition(world->getWeatherTransition()); + stateUpdater->setWindSpeed(world->getWindSpeed()); + stateUpdater->setSkyColor(mSky->getSkyColor()); + mPostProcessor->setUnderwaterFlag(isUnderwater); } - void RenderingManager::updatePlayerPtr(const MWWorld::Ptr &ptr) + void RenderingManager::updatePlayerPtr(const MWWorld::Ptr& ptr) { - if(mPlayerAnimation.get()) + if (mPlayerAnimation.get()) { setupPlayer(ptr); mPlayerAnimation->updatePtr(ptr); @@ -754,15 +962,14 @@ namespace MWRender mCamera->attachTo(ptr); } - void RenderingManager::removePlayer(const MWWorld::Ptr &player) + void RenderingManager::removePlayer(const MWWorld::Ptr& player) { mWater->removeEmitter(player); } - void RenderingManager::rotateObject(const MWWorld::Ptr &ptr, const osg::Quat& rot) + void RenderingManager::rotateObject(const MWWorld::Ptr& ptr, const osg::Quat& rot) { - if(ptr == mCamera->getTrackingPtr() && - !mCamera->isVanityOrPreviewModeEnabled()) + if (ptr == mCamera->getTrackingPtr() && !mCamera->isVanityOrPreviewModeEnabled()) { mCamera->rotateCameraToTrackingPtr(); } @@ -770,12 +977,12 @@ namespace MWRender ptr.getRefData().getBaseNode()->setAttitude(rot); } - void RenderingManager::moveObject(const MWWorld::Ptr &ptr, const osg::Vec3f &pos) + void RenderingManager::moveObject(const MWWorld::Ptr& ptr, const osg::Vec3f& pos) { ptr.getRefData().getBaseNode()->setPosition(pos); } - void RenderingManager::scaleObject(const MWWorld::Ptr &ptr, const osg::Vec3f &scale) + void RenderingManager::scaleObject(const MWWorld::Ptr& ptr, const osg::Vec3f& scale) { ptr.getRefData().getBaseNode()->setScale(scale); @@ -783,7 +990,7 @@ namespace MWRender mCamera->processViewChange(); } - void RenderingManager::removeObject(const MWWorld::Ptr &ptr) + void RenderingManager::removeObject(const MWWorld::Ptr& ptr) { mActorsPaths->remove(ptr); mObjects->removeObject(ptr); @@ -794,6 +1001,8 @@ namespace MWRender { mWater->setEnabled(enabled); mSky->setWaterEnabled(enabled); + + mPostProcessor->getStateUpdater()->setIsWaterEnabled(enabled); } void RenderingManager::setWaterHeight(float height) @@ -801,6 +1010,8 @@ namespace MWRender mWater->setCullCallback(mTerrain->getHeightCullCallback(height, Mask_Water)); mWater->setHeight(height); mSky->setWaterHeight(height); + + mPostProcessor->getStateUpdater()->setWaterHeight(height); } void RenderingManager::screenshot(osg::Image* image, int w, int h) @@ -808,32 +1019,13 @@ namespace MWRender mScreenshotManager->screenshot(image, w, h); } - bool RenderingManager::screenshot360(osg::Image* image) - { - if (mCamera->isVanityOrPreviewModeEnabled()) - { - Log(Debug::Warning) << "Spherical screenshots are not allowed in preview mode."; - return false; - } - - unsigned int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); - - if (mCamera->isFirstPerson()) - mPlayerAnimation->getObjectRoot()->setNodeMask(0); - - mScreenshotManager->screenshot360(image); - - mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); - - return true; - } - - osg::Vec4f RenderingManager::getScreenBounds(const osg::BoundingBox &worldbb) + osg::Vec4f RenderingManager::getScreenBounds(const osg::BoundingBox& worldbb) { - if (!worldbb.valid()) return osg::Vec4f(); + if (!worldbb.valid()) + return osg::Vec4f(); osg::Matrix viewProj = mViewer->getCamera()->getViewMatrix() * mViewer->getCamera()->getProjectionMatrix(); float min_x = 1.0f, max_x = 0.0f, min_y = 1.0f, max_y = 0.0f; - for (int i=0; i<8; ++i) + for (int i = 0; i < 8; ++i) { osg::Vec3f corner = worldbb.corner(i); corner = corner * viewProj; @@ -842,49 +1034,59 @@ namespace MWRender float y = (corner.y() - 1.f) * (-0.5f); if (x < min_x) - min_x = x; + min_x = x; if (x > max_x) - max_x = x; + max_x = x; if (y < min_y) - min_y = y; + min_y = y; if (y > max_y) - max_y = y; + max_y = y; } return osg::Vec4f(min_x, min_y, max_x, max_y); } - RenderingManager::RayResult getIntersectionResult (osgUtil::LineSegmentIntersector* intersector) + RenderingManager::RayResult getIntersectionResult(osgUtil::LineSegmentIntersector* intersector, + const osg::ref_ptr& visitor, std::span ignoreList = {}) { + constexpr auto nonObjectWorldMask = Mask_Terrain | Mask_Water; RenderingManager::RayResult result; result.mHit = false; - result.mHitRefnum.unset(); result.mRatio = 0; - if (intersector->containsIntersections()) - { - result.mHit = true; - osgUtil::LineSegmentIntersector::Intersection intersection = intersector->getFirstIntersection(); - result.mHitPointWorld = intersection.getWorldIntersectPoint(); - result.mHitNormalWorld = intersection.getWorldIntersectNormal(); - result.mRatio = intersection.ratio; + if (!intersector->containsIntersections()) + return result; + auto test = [&](const osgUtil::LineSegmentIntersector::Intersection& intersection) { PtrHolder* ptrHolder = nullptr; std::vector refnumMarkers; - for (osg::NodePath::const_iterator it = intersection.nodePath.begin(); it != intersection.nodePath.end(); ++it) + bool hitNonObjectWorld = false; + for (osg::NodePath::const_iterator it = intersection.nodePath.begin(); it != intersection.nodePath.end(); + ++it) { + const auto& nodeMask = (*it)->getNodeMask(); + if (!hitNonObjectWorld) + hitNonObjectWorld = nodeMask & nonObjectWorldMask; + osg::UserDataContainer* userDataContainer = (*it)->getUserDataContainer(); if (!userDataContainer) continue; - for (unsigned int i=0; igetNumUserObjects(); ++i) + for (unsigned int i = 0; i < userDataContainer->getNumUserObjects(); ++i) { if (PtrHolder* p = dynamic_cast(userDataContainer->getUserObject(i))) - ptrHolder = p; + { + if (std::find(ignoreList.begin(), ignoreList.end(), p->mPtr) == ignoreList.end()) + { + ptrHolder = p; + } + } if (RefnumMarker* r = dynamic_cast(userDataContainer->getUserObject(i))) + { refnumMarkers.push_back(r); + } } } @@ -892,59 +1094,156 @@ namespace MWRender result.mHitObject = ptrHolder->mPtr; unsigned int vertexCounter = 0; - for (unsigned int i=0; imNumVertices || (intersectionIndex >= vertexCounter && intersectionIndex < vertexCounter + refnumMarkers[i]->mNumVertices)) + if (!refnumMarkers[i]->mNumVertices + || (intersectionIndex >= vertexCounter + && intersectionIndex < vertexCounter + refnumMarkers[i]->mNumVertices)) { - result.mHitRefnum = refnumMarkers[i]->mRefnum; + auto it = std::find_if( + ignoreList.begin(), ignoreList.end(), [target = refnumMarkers[i]->mRefnum](const auto& ptr) { + return target == ptr.getCellRef().getRefNum(); + }); + + if (it == ignoreList.end()) + { + result.mHitRefnum = refnumMarkers[i]->mRefnum; + } + break; } vertexCounter += refnumMarkers[i]->mNumVertices; } + + if (!result.mHitObject.isEmpty() || result.mHitRefnum.isSet() || hitNonObjectWorld) + { + result.mHit = true; + result.mHitPointWorld = intersection.getWorldIntersectPoint(); + result.mHitNormalWorld = intersection.getWorldIntersectNormal(); + result.mRatio = intersection.ratio; + } + }; + + if (ignoreList.empty() || intersector->getIntersectionLimit() != osgUtil::LineSegmentIntersector::NO_LIMIT) + { + test(intersector->getFirstIntersection()); } + else + { + for (const auto& intersection : intersector->getIntersections()) + { + test(intersection); - return result; + if (result.mHit) + { + break; + } + } + } + return result; } - osg::ref_ptr RenderingManager::getIntersectionVisitor(osgUtil::Intersector *intersector, bool ignorePlayer, bool ignoreActors) + class IntersectionVisitorWithIgnoreList : public osgUtil::IntersectionVisitor + { + public: + bool skipTransform(osg::Transform& transform) + { + if (mContainsPagedRefs) + return false; + + osg::UserDataContainer* userDataContainer = transform.getUserDataContainer(); + if (!userDataContainer) + return false; + + for (unsigned int i = 0; i < userDataContainer->getNumUserObjects(); ++i) + { + if (PtrHolder* p = dynamic_cast(userDataContainer->getUserObject(i))) + { + if (std::find(mIgnoreList.begin(), mIgnoreList.end(), p->mPtr) != mIgnoreList.end()) + { + return true; + } + } + } + + return false; + } + + void apply(osg::Transform& transform) override + { + if (skipTransform(transform)) + { + return; + } + osgUtil::IntersectionVisitor::apply(transform); + } + + void setIgnoreList(std::span ignoreList) { mIgnoreList = ignoreList; } + void setContainsPagedRefs(bool contains) { mContainsPagedRefs = contains; } + + private: + std::span mIgnoreList; + bool mContainsPagedRefs = false; + }; + + osg::ref_ptr RenderingManager::getIntersectionVisitor( + osgUtil::Intersector* intersector, bool ignorePlayer, bool ignoreActors, + std::span ignoreList) { if (!mIntersectionVisitor) - mIntersectionVisitor = new osgUtil::IntersectionVisitor; + mIntersectionVisitor = new IntersectionVisitorWithIgnoreList; + + mIntersectionVisitor->setIgnoreList(ignoreList); + mIntersectionVisitor->setContainsPagedRefs(false); + + MWWorld::Scene* worldScene = MWBase::Environment::get().getWorldScene(); + for (const auto& ptr : ignoreList) + { + if (worldScene->isPagedRef(ptr)) + { + mIntersectionVisitor->setContainsPagedRefs(true); + intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); + break; + } + } mIntersectionVisitor->setTraversalNumber(mViewer->getFrameStamp()->getFrameNumber()); mIntersectionVisitor->setFrameStamp(mViewer->getFrameStamp()); mIntersectionVisitor->setIntersector(intersector); unsigned int mask = ~0u; - mask &= ~(Mask_RenderToTexture|Mask_Sky|Mask_Debug|Mask_Effect|Mask_Water|Mask_SimpleWater|Mask_Groundcover); + mask &= ~(Mask_RenderToTexture | Mask_Sky | Mask_Debug | Mask_Effect | Mask_Water | Mask_SimpleWater + | Mask_Groundcover); if (ignorePlayer) mask &= ~(Mask_Player); if (ignoreActors) - mask &= ~(Mask_Actor|Mask_Player); + mask &= ~(Mask_Actor | Mask_Player); mIntersectionVisitor->setTraversalMask(mask); return mIntersectionVisitor; } - RenderingManager::RayResult RenderingManager::castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, bool ignorePlayer, bool ignoreActors) + RenderingManager::RayResult RenderingManager::castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, + bool ignorePlayer, bool ignoreActors, std::span ignoreList) { - osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector(osgUtil::LineSegmentIntersector::MODEL, - origin, dest)); + osg::ref_ptr intersector( + new osgUtil::LineSegmentIntersector(osgUtil::LineSegmentIntersector::MODEL, origin, dest)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST); - mRootNode->accept(*getIntersectionVisitor(intersector, ignorePlayer, ignoreActors)); + mRootNode->accept(*getIntersectionVisitor(intersector, ignorePlayer, ignoreActors, ignoreList)); - return getIntersectionResult(intersector); + return getIntersectionResult(intersector, mIntersectionVisitor, ignoreList); } - RenderingManager::RayResult RenderingManager::castCameraToViewportRay(const float nX, const float nY, float maxDistance, bool ignorePlayer, bool ignoreActors) + RenderingManager::RayResult RenderingManager::castCameraToViewportRay( + const float nX, const float nY, float maxDistance, bool ignorePlayer, bool ignoreActors) { - osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector(osgUtil::LineSegmentIntersector::PROJECTION, - nX * 2.f - 1.f, nY * (-2.f) + 1.f)); + osg::ref_ptr intersector(new osgUtil::LineSegmentIntersector( + osgUtil::LineSegmentIntersector::PROJECTION, nX * 2.f - 1.f, nY * (-2.f) + 1.f)); - osg::Vec3d dist (0.f, 0.f, -maxDistance); + osg::Vec3d dist(0.f, 0.f, -maxDistance); dist = dist * mViewer->getCamera()->getProjectionMatrix(); @@ -955,16 +1254,17 @@ namespace MWRender mViewer->getCamera()->accept(*getIntersectionVisitor(intersector, ignorePlayer, ignoreActors)); - return getIntersectionResult(intersector); + return getIntersectionResult(intersector, mIntersectionVisitor); } - void RenderingManager::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) + void RenderingManager::updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& updated) { mObjects->updatePtr(old, updated); mActorsPaths->updatePtr(old, updated); } - void RenderingManager::spawnEffect(const std::string &model, const std::string &texture, const osg::Vec3f &worldPosition, float scale, bool isMagicVFX) + void RenderingManager::spawnEffect(VFS::Path::NormalizedView model, std::string_view texture, + const osg::Vec3f& worldPosition, float scale, bool isMagicVFX) { mEffectManager->addEffect(model, texture, worldPosition, scale, isMagicVFX); } @@ -984,7 +1284,7 @@ namespace MWRender mObjectPaging->clear(); } - MWRender::Animation* RenderingManager::getAnimation(const MWWorld::Ptr &ptr) + MWRender::Animation* RenderingManager::getAnimation(const MWWorld::Ptr& ptr) { if (mPlayerAnimation.get() && ptr == mPlayerAnimation->getPtr()) return mPlayerAnimation.get(); @@ -992,7 +1292,7 @@ namespace MWRender return mObjects->getAnimation(ptr); } - const MWRender::Animation* RenderingManager::getAnimation(const MWWorld::ConstPtr &ptr) const + const MWRender::Animation* RenderingManager::getAnimation(const MWWorld::ConstPtr& ptr) const { if (mPlayerAnimation.get() && ptr == mPlayerAnimation->getPtr()) return mPlayerAnimation.get(); @@ -1000,7 +1300,12 @@ namespace MWRender return mObjects->getAnimation(ptr); } - void RenderingManager::setupPlayer(const MWWorld::Ptr &player) + PostProcessor* RenderingManager::getPostProcessor() + { + return mPostProcessor; + } + + void RenderingManager::setupPlayer(const MWWorld::Ptr& player) { if (!mPlayerNode) { @@ -1019,26 +1324,26 @@ namespace MWRender mWater->addEmitter(player); } - void RenderingManager::renderPlayer(const MWWorld::Ptr &player) + void RenderingManager::renderPlayer(const MWWorld::Ptr& player) { - mPlayerAnimation = new NpcAnimation(player, player.getRefData().getBaseNode(), mResourceSystem, 0, NpcAnimation::VM_Normal, - mFirstPersonFieldOfView); + mPlayerAnimation = new NpcAnimation(player, player.getRefData().getBaseNode(), mResourceSystem, 0, + NpcAnimation::VM_Normal, mFirstPersonFieldOfView); mCamera->setAnimation(mPlayerAnimation.get()); mCamera->attachTo(player); } - void RenderingManager::rebuildPtr(const MWWorld::Ptr &ptr) + void RenderingManager::rebuildPtr(const MWWorld::Ptr& ptr) { - NpcAnimation *anim = nullptr; - if(ptr == mPlayerAnimation->getPtr()) + NpcAnimation* anim = nullptr; + if (ptr == mPlayerAnimation->getPtr()) anim = mPlayerAnimation.get(); else anim = dynamic_cast(mObjects->getAnimation(ptr)); - if(anim) + if (anim) { anim->rebuild(); - if(mCamera->getTrackingPtr() == ptr) + if (mCamera->getTrackingPtr() == ptr) { mCamera->attachTo(ptr); mCamera->setAnimation(anim); @@ -1046,57 +1351,85 @@ namespace MWRender } } - void RenderingManager::addWaterRippleEmitter(const MWWorld::Ptr &ptr) + void RenderingManager::addWaterRippleEmitter(const MWWorld::Ptr& ptr) { mWater->addEmitter(ptr); } - void RenderingManager::removeWaterRippleEmitter(const MWWorld::Ptr &ptr) + void RenderingManager::removeWaterRippleEmitter(const MWWorld::Ptr& ptr) { mWater->removeEmitter(ptr); } - void RenderingManager::emitWaterRipple(const osg::Vec3f &pos) + void RenderingManager::emitWaterRipple(const osg::Vec3f& pos) { mWater->emitRipple(pos); } void RenderingManager::updateProjectionMatrix() { - double aspect = mViewer->getCamera()->getViewport()->aspectRatio(); + if (mNearClip < 0.0f) + throw std::runtime_error("Near clip is less than zero"); + if (mViewDistance < mNearClip) + throw std::runtime_error("Viewing distance is less than near clip"); + + const double width = Settings::video().mResolutionX; + const double height = Settings::video().mResolutionY; + + double aspect = (height == 0.0) ? 1.0 : width / height; float fov = mFieldOfView; if (mFieldOfViewOverridden) fov = mFieldOfViewOverride; + mViewer->getCamera()->setProjectionMatrixAsPerspective(fov, aspect, mNearClip, mViewDistance); - mUniformNear->set(mNearClip); - mUniformFar->set(mViewDistance); + if (SceneUtil::AutoDepth::isReversed()) + { + mPerViewUniformStateUpdater->setProjectionMatrix( + SceneUtil::getReversedZProjectionMatrixAsPerspective(fov, aspect, mNearClip, mViewDistance)); + } + else + mPerViewUniformStateUpdater->setProjectionMatrix(mViewer->getCamera()->getProjectionMatrix()); + + mSharedUniformStateUpdater->setNear(mNearClip); + mSharedUniformStateUpdater->setFar(mViewDistance); + if (Stereo::getStereo()) + { + auto res = Stereo::Manager::instance().eyeResolution(); + mSharedUniformStateUpdater->setScreenRes(res.x(), res.y()); + Stereo::Manager::instance().setMasterProjectionMatrix(mPerViewUniformStateUpdater->getProjectionMatrix()); + } + else + { + mSharedUniformStateUpdater->setScreenRes(width, height); + } - // Since our fog is not radial yet, we should take FOV in account, otherwise terrain near viewing distance may disappear. - // Limit FOV here just for sure, otherwise viewing distance can be too high. - fov = std::min(mFieldOfView, 140.f); - float distanceMult = std::cos(osg::DegreesToRadians(fov)/2.f); - mTerrain->setViewDistance(mViewDistance * (distanceMult ? 1.f/distanceMult : 1.f)); + // Since our fog is not radial yet, we should take FOV in account, otherwise terrain near viewing distance may + // disappear. Limit FOV here just for sure, otherwise viewing distance can be too high. + float distanceMult = std::cos(osg::DegreesToRadians(std::min(fov, 140.f)) / 2.f); + mTerrain->setViewDistance(mViewDistance * (distanceMult ? 1.f / distanceMult : 1.f)); - if (mGroundcoverWorld) + if (mPostProcessor) { - float groundcoverDistance = std::max(0.f, Settings::Manager::getFloat("rendering distance", "Groundcover")); - mGroundcoverWorld->setViewDistance(groundcoverDistance * (distanceMult ? 1.f/distanceMult : 1.f)); + mPostProcessor->getStateUpdater()->setProjectionMatrix(mPerViewUniformStateUpdater->getProjectionMatrix()); + mPostProcessor->getStateUpdater()->setFov(fov); } } + void RenderingManager::setScreenRes(int width, int height) + { + mSharedUniformStateUpdater->setScreenRes(width, height); + } + void RenderingManager::updateTextureFiltering() { mViewer->stopThreading(); - mResourceSystem->getSceneManager()->setFilterSettings( - Settings::Manager::getString("texture mag filter", "General"), - Settings::Manager::getString("texture min filter", "General"), - Settings::Manager::getString("texture mipmap", "General"), - Settings::Manager::getInt("anisotropy", "General") - ); + mResourceSystem->getSceneManager()->setFilterSettings(Settings::general().mTextureMagFilter, + Settings::general().mTextureMinFilter, Settings::general().mTextureMipmap, Settings::general().mAnisotropy); mTerrain->updateTextureFiltering(); + mWater->processChangedSettings({}); mViewer->startThreading(); } @@ -1108,47 +1441,101 @@ namespace MWRender if (mNightEyeFactor > 0.f) color += osg::Vec4f(0.7, 0.7, 0.7, 0.0) * mNightEyeFactor; + mPostProcessor->getStateUpdater()->setAmbientColor(color); mStateUpdater->setAmbientColor(color); } - void RenderingManager::setFogColor(const osg::Vec4f &color) + void RenderingManager::setFogColor(const osg::Vec4f& color) { mViewer->getCamera()->setClearColor(color); mStateUpdater->setFogColor(color); } + RenderingManager::WorldspaceChunkMgr& RenderingManager::getWorldspaceChunkMgr(ESM::RefId worldspace) + { + auto existingChunkMgr = mWorldspaceChunks.find(worldspace); + if (existingChunkMgr != mWorldspaceChunks.end()) + return existingChunkMgr->second; + RenderingManager::WorldspaceChunkMgr newChunkMgr; + + const float lodFactor = Settings::terrain().mLodFactor; + const bool groundcover = Settings::groundcover().mEnabled; + const bool distantTerrain = Settings::terrain().mDistantTerrain; + const double expiryDelay = Settings::cells().mCacheExpiryDelay; + if (distantTerrain || groundcover) + { + const int compMapResolution = Settings::terrain().mCompositeMapResolution; + const int compMapPower = Settings::terrain().mCompositeMapLevel; + const float compMapLevel = std::pow(2, compMapPower); + const int vertexLodMod = Settings::terrain().mVertexLodMod; + const float maxCompGeometrySize = Settings::terrain().mMaxCompositeGeometrySize; + const bool debugChunks = Settings::terrain().mDebugChunks; + auto quadTreeWorld = std::make_unique(mSceneRoot, mRootNode, mResourceSystem, + mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug, compMapResolution, compMapLevel, + lodFactor, vertexLodMod, maxCompGeometrySize, debugChunks, worldspace, expiryDelay); + if (Settings::terrain().mObjectPaging) + { + newChunkMgr.mObjectPaging + = std::make_unique(mResourceSystem->getSceneManager(), worldspace); + quadTreeWorld->addChunkManager(newChunkMgr.mObjectPaging.get()); + mResourceSystem->addResourceManager(newChunkMgr.mObjectPaging.get()); + } + if (groundcover) + { + const float groundcoverDistance = Settings::groundcover().mRenderingDistance; + const float density = Settings::groundcover().mDensity; + + newChunkMgr.mGroundcover = std::make_unique( + mResourceSystem->getSceneManager(), density, groundcoverDistance, mGroundCoverStore); + quadTreeWorld->addChunkManager(newChunkMgr.mGroundcover.get()); + mResourceSystem->addResourceManager(newChunkMgr.mGroundcover.get()); + } + newChunkMgr.mTerrain = std::move(quadTreeWorld); + } + else + newChunkMgr.mTerrain = std::make_unique(mSceneRoot, mRootNode, mResourceSystem, + mTerrainStorage.get(), Mask_Terrain, worldspace, expiryDelay, Mask_PreCompile, Mask_Debug); + + newChunkMgr.mTerrain->setTargetFrameRate(Settings::cells().mTargetFramerate); + float distanceMult = std::cos(osg::DegreesToRadians(std::min(mFieldOfView, 140.f)) / 2.f); + newChunkMgr.mTerrain->setViewDistance(mViewDistance * (distanceMult ? 1.f / distanceMult : 1.f)); + + return mWorldspaceChunks.emplace(worldspace, std::move(newChunkMgr)).first->second; + } + void RenderingManager::reportStats() const { osg::Stats* stats = mViewer->getViewerStats(); unsigned int frameNumber = mViewer->getFrameStamp()->getFrameNumber(); if (stats->collectStats("resource")) { - stats->setAttribute(frameNumber, "UnrefQueue", mUnrefQueue->getNumItems()); - mTerrain->reportStats(frameNumber, stats); } } - void RenderingManager::processChangedSettings(const Settings::CategorySettingVector &changed) + void RenderingManager::processChangedSettings(const Settings::CategorySettingVector& changed) { + // Only perform a projection matrix update once if a relevant setting is changed. + bool updateProjection = false; + for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it) { if (it->first == "Camera" && it->second == "field of view") { - mFieldOfView = Settings::Manager::getFloat("field of view", "Camera"); - updateProjectionMatrix(); + mFieldOfView = Settings::camera().mFieldOfView; + updateProjection = true; + } + else if (it->first == "Video" && (it->second == "resolution x" || it->second == "resolution y")) + { + updateProjection = true; } else if (it->first == "Camera" && it->second == "viewing distance") { - mViewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); - if(!Settings::Manager::getBool("use distant fog", "Fog")) - mStateUpdater->setFogEnd(mViewDistance); - updateProjectionMatrix(); + setViewDistance(Settings::camera().mViewingDistance); } - else if (it->first == "General" && (it->second == "texture filter" || - it->second == "texture mipmap" || - it->second == "anisotropy")) + else if (it->first == "General" + && (it->second == "texture filter" || it->second == "texture mipmap" || it->second == "anisotropy")) { updateTextureFiltering(); } @@ -1158,49 +1545,84 @@ namespace MWRender } else if (it->first == "Shaders" && it->second == "minimum interior brightness") { - mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); if (MWMechanics::getPlayer().isInCell()) - configureAmbient(MWMechanics::getPlayer().getCell()->getCell()); + configureAmbient(*MWMechanics::getPlayer().getCell()->getCell()); } - else if (it->first == "Shaders" && (it->second == "light bounds multiplier" || - it->second == "maximum light distance" || - it->second == "light fade start" || - it->second == "max lights")) + else if (it->first == "Shaders" + && (it->second == "force per pixel lighting" || it->second == "classic falloff")) { - auto* lightManager = static_cast(getLightRoot()); - lightManager->processChangedSettings(changed); + mViewer->stopThreading(); + + auto defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); + defines["forcePPL"] = Settings::shaders().mForcePerPixelLighting ? "1" : "0"; + defines["classicFalloff"] = Settings::shaders().mClassicFalloff ? "1" : "0"; + mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); + + if (MWMechanics::getPlayer().isInCell() && it->second == "classic falloff") + configureAmbient(*MWMechanics::getPlayer().getCell()->getCell()); + + mViewer->startThreading(); + } + else if (it->first == "Shaders" + && (it->second == "light bounds multiplier" || it->second == "maximum light distance" + || it->second == "light fade start" || it->second == "max lights")) + { + auto* lightManager = getLightRoot(); + + lightManager->processChangedSettings(Settings::shaders().mLightBoundsMultiplier, + Settings::shaders().mMaximumLightDistance, Settings::shaders().mLightFadeStart); if (it->second == "max lights" && !lightManager->usingFFP()) { mViewer->stopThreading(); - lightManager->updateMaxLights(); + lightManager->updateMaxLights(Settings::shaders().mMaxLights); auto defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); for (const auto& [name, key] : lightManager->getLightDefines()) defines[name] = key; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); - mSceneRoot->removeUpdateCallback(mStateUpdater); - mStateUpdater = new StateUpdater; - mSceneRoot->addUpdateCallback(mStateUpdater); - mStateUpdater->setFogEnd(mViewDistance); - updateAmbient(); + mStateUpdater->reset(); mViewer->startThreading(); } } + else if (it->first == "Post Processing" && it->second == "enabled") + { + if (Settings::postProcessing().mEnabled) + mPostProcessor->enable(); + else + { + mPostProcessor->disable(); + if (auto* hud = MWBase::Environment::get().getWindowManager()->getPostProcessorHud()) + hud->setVisible(false); + } + } + } + + if (updateProjection) + { + updateProjectionMatrix(); } } - float RenderingManager::getNearClipDistance() const + void RenderingManager::setViewDistance(float distance, bool delay) { - return mNearClip; + mViewDistance = distance; + + if (delay) + { + mUpdateProjectionMatrix = true; + return; + } + + updateProjectionMatrix(); } - float RenderingManager::getTerrainHeightAt(const osg::Vec3f &pos) + float RenderingManager::getTerrainHeightAt(const osg::Vec3f& pos, ESM::RefId worldspace) { - return mTerrain->getHeightAt(pos); + return getWorldspaceChunkMgr(worldspace).mTerrain->getHeightAt(pos); } void RenderingManager::overrideFieldOfView(float val) @@ -1213,16 +1635,27 @@ namespace MWRender } } + void RenderingManager::setFieldOfView(float val) + { + mFieldOfView = val; + mUpdateProjectionMatrix = true; + } + + float RenderingManager::getFieldOfView() const + { + return mFieldOfViewOverridden ? mFieldOfViewOverridden : mFieldOfView; + } + osg::Vec3f RenderingManager::getHalfExtents(const MWWorld::ConstPtr& object) const { osg::Vec3f halfExtents(0, 0, 0); - std::string modelName = object.getClass().getModel(object); + VFS::Path::Normalized modelName(object.getClass().getCorrectedModel(object)); if (modelName.empty()) return halfExtents; osg::ref_ptr node = mResourceSystem->getSceneManager()->getTemplate(modelName); osg::ComputeBoundsVisitor computeBoundsVisitor; - computeBoundsVisitor.setTraversalMask(~(MWRender::Mask_ParticleSystem|MWRender::Mask_Effect)); + computeBoundsVisitor.setTraversalMask(~(MWRender::Mask_ParticleSystem | MWRender::Mask_Effect)); const_cast(node.get())->accept(computeBoundsVisitor); osg::BoundingBox bounds = computeBoundsVisitor.getBoundingBox(); @@ -1236,6 +1669,46 @@ namespace MWRender return halfExtents; } + osg::BoundingBox RenderingManager::getCullSafeBoundingBox(const MWWorld::Ptr& ptr) const + { + if (ptr.isEmpty()) + return {}; + + osg::ref_ptr rootNode = ptr.getRefData().getBaseNode(); + + // Recalculate bounds on the ptr's template when the object is not loaded or is loaded but paged + MWWorld::Scene* worldScene = MWBase::Environment::get().getWorldScene(); + if (!rootNode || worldScene->isPagedRef(ptr)) + { + const VFS::Path::Normalized model(ptr.getClass().getCorrectedModel(ptr)); + + if (model.empty()) + return {}; + + rootNode = new SceneUtil::PositionAttitudeTransform; + // Hack even used by osg internally, osg's NodeVisitor won't accept const qualified nodes + rootNode->addChild(const_cast(mResourceSystem->getSceneManager()->getTemplate(model).get())); + + const float refScale = ptr.getCellRef().getScale(); + rootNode->setScale({ refScale, refScale, refScale }); + rootNode->setPosition(ptr.getCellRef().getPosition().asVec3()); + + osg::ref_ptr animation = nullptr; + + if (ptr.getClass().isNpc()) + { + rootNode->setNodeMask(Mask_Actor); + animation = new NpcAnimation(ptr, osg::ref_ptr(rootNode), mResourceSystem); + } + } + + SceneUtil::CullSafeBoundsVisitor computeBounds; + computeBounds.setTraversalMask(~(MWRender::Mask_ParticleSystem | MWRender::Mask_Effect)); + rootNode->accept(computeBounds); + + return computeBounds.mBoundingBox; + } + void RenderingManager::resetFieldOfView() { if (mFieldOfViewOverridden == true) @@ -1245,7 +1718,8 @@ namespace MWRender updateProjectionMatrix(); } } - void RenderingManager::exportSceneGraph(const MWWorld::Ptr &ptr, const std::string &filename, const std::string &format) + void RenderingManager::exportSceneGraph( + const MWWorld::Ptr& ptr, const std::filesystem::path& filename, const std::string& format) { osg::Node* node = mViewer->getSceneData(); if (!ptr.isEmpty()) @@ -1254,15 +1728,15 @@ namespace MWRender SceneUtil::writeScene(node, filename, format); } - LandManager *RenderingManager::getLandManager() const + LandManager* RenderingManager::getLandManager() const { return mTerrainStorage->getLandManager(); } void RenderingManager::updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end) const { - mActorsPaths->update(actor, path, halfExtents, start, end, mNavigator.getSettings()); + mActorsPaths->update(actor, path, agentBounds, start, end, mNavigator.getSettings()); } void RenderingManager::removeActorPath(const MWWorld::ConstPtr& actor) const @@ -1293,9 +1767,7 @@ namespace MWRender { try { - const auto locked = it->second->lockConst(); - mNavMesh->update(locked->getImpl(), mNavMeshNumber, locked->getGeneration(), - locked->getNavMeshRevision(), mNavigator.getSettings()); + mNavMesh->update(it->second, mNavMeshNumber, mNavigator.getSettings()); } catch (const std::exception& e) { @@ -1312,7 +1784,7 @@ namespace MWRender mRecastMesh->update(mNavigator.getRecastMeshTiles(), mNavigator.getSettings()); } - void RenderingManager::setActiveGrid(const osg::Vec4i &grid) + void RenderingManager::setActiveGrid(const osg::Vec4i& grid) { mTerrain->setActiveGrid(grid); } @@ -1320,20 +1792,23 @@ namespace MWRender { if (!ptr.isInCell() || !ptr.getCell()->isExterior() || !mObjectPaging) return false; - if (mObjectPaging->enableObject(type, ptr.getCellRef().getRefNum(), ptr.getCellRef().getPosition().asVec3(), osg::Vec2i(ptr.getCell()->getCell()->getGridX(), ptr.getCell()->getCell()->getGridY()), enabled)) + if (mObjectPaging->enableObject(type, ptr.getCellRef().getRefNum(), ptr.getCellRef().getPosition().asVec3(), + osg::Vec2i(ptr.getCell()->getCell()->getGridX(), ptr.getCell()->getCell()->getGridY()), enabled)) { mTerrain->rebuildViews(); return true; } return false; } - void RenderingManager::pagingBlacklistObject(int type, const MWWorld::ConstPtr &ptr) + void RenderingManager::pagingBlacklistObject(int type, const MWWorld::ConstPtr& ptr) { if (!ptr.isInCell() || !ptr.getCell()->isExterior() || !mObjectPaging) return; - const ESM::RefNum & refnum = ptr.getCellRef().getRefNum(); - if (!refnum.hasContentFile()) return; - if (mObjectPaging->blacklistObject(type, refnum, ptr.getCellRef().getPosition().asVec3(), osg::Vec2i(ptr.getCell()->getCell()->getGridX(), ptr.getCell()->getCell()->getGridY()))) + ESM::RefNum refnum = ptr.getCellRef().getRefNum(); + if (!refnum.hasContentFile()) + return; + if (mObjectPaging->blacklistObject(type, refnum, ptr.getCellRef().getPosition().asVec3(), + osg::Vec2i(ptr.getCell()->getCell()->getGridX(), ptr.getCell()->getCell()->getGridY()))) mTerrain->rebuildViews(); } bool RenderingManager::pagingUnlockCache() @@ -1345,9 +1820,14 @@ namespace MWRender } return false; } - void RenderingManager::getPagedRefnums(const osg::Vec4i &activeGrid, std::set &out) + void RenderingManager::getPagedRefnums(const osg::Vec4i& activeGrid, std::vector& out) { if (mObjectPaging) mObjectPaging->getPagedRefnums(activeGrid, out); } + + void RenderingManager::setNavMeshMode(Settings::NavMeshRenderMode value) + { + mNavMesh->setMode(value); + } } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index a0a74bd5c45..86f248491a0 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -1,21 +1,22 @@ #ifndef OPENMW_MWRENDER_RENDERINGMANAGER_H #define OPENMW_MWRENDER_RENDERINGMANAGER_H -#include -#include -#include +#include "objects.hpp" +#include "renderinginterface.hpp" +#include "rendermode.hpp" #include +#include -#include - -#include "objects.hpp" +#include +#include -#include "renderinginterface.hpp" -#include "rendermode.hpp" +#include #include #include +#include +#include namespace osg { @@ -42,7 +43,8 @@ namespace osgViewer namespace ESM { struct Cell; - struct RefNum; + struct FormId; + using RefNum = FormId; } namespace Terrain @@ -59,6 +61,7 @@ namespace SceneUtil { class ShadowManager; class WorkQueue; + class LightManager; class UnrefQueue; } @@ -66,12 +69,26 @@ namespace DetourNavigator { struct Navigator; struct Settings; + struct AgentBounds; +} + +namespace MWWorld +{ + class GroundcoverStore; + class Cell; +} + +namespace Debug +{ + struct DebugDrawer; } namespace MWRender { - class GroundcoverUpdater; class StateUpdater; + class SharedUniformStateUpdater; + class PerViewUniformStateUpdater; + class IntersectionVisitorWithIgnoreList; class EffectManager; class ScreenshotManager; @@ -80,7 +97,6 @@ namespace MWRender class NpcAnimation; class Pathgrid; class Camera; - class ViewOverShoulderController; class Water; class TerrainStorage; class LandManager; @@ -89,13 +105,15 @@ namespace MWRender class RecastMesh; class ObjectPaging; class Groundcover; + class PostProcessor; class RenderingManager : public MWRender::RenderingInterface { public: RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, - Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, - const std::string& resourcePath, DetourNavigator::Navigator& navigator); + Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, + DetourNavigator::Navigator& navigator, const MWWorld::GroundcoverStore& groundcoverStore, + SceneUtil::UnrefQueue& unrefQueue); ~RenderingManager(); osgUtil::IncrementalCompileOperation* getIncrementalCompileOperation(); @@ -105,17 +123,13 @@ namespace MWRender Resource::ResourceSystem* getResourceSystem(); SceneUtil::WorkQueue* getWorkQueue(); - SceneUtil::UnrefQueue* getUnrefQueue(); Terrain::World* getTerrain(); - osg::Uniform* mUniformNear; - osg::Uniform* mUniformFar; - void preloadCommonAssets(); double getReferenceTime() const; - osg::Group* getLightRoot(); + SceneUtil::LightManager* getLightRoot(); void setNightEyeFactor(float factor); @@ -127,16 +141,18 @@ namespace MWRender void skySetMoonColour(bool red); void setSunDirection(const osg::Vec3f& direction); - void setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular); + void setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular, float sunVis); + void setNight(bool isNight) { mNight = isNight; } - void configureAmbient(const ESM::Cell* cell); - void configureFog(const ESM::Cell* cell); - void configureFog(float fogDepth, float underwaterFog, float dlFactor, float dlOffset, const osg::Vec4f& colour); + void configureAmbient(const MWWorld::Cell& cell); + void configureFog(const MWWorld::Cell& cell); + void configureFog( + float fogDepth, float underwaterFog, float dlFactor, float dlOffset, const osg::Vec4f& colour); void addCell(const MWWorld::CellStore* store); void removeCell(const MWWorld::CellStore* store); - void enableTerrain(bool enable); + void enableTerrain(bool enable, ESM::RefId worldspace); void updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& updated); @@ -151,7 +167,6 @@ namespace MWRender /// Take a screenshot of w*h onto the given image, not including the GUI. void screenshot(osg::Image* image, int w, int h); - bool screenshot360(osg::Image* image); struct RayResult { @@ -163,14 +178,17 @@ namespace MWRender float mRatio; }; - RayResult castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, bool ignorePlayer, bool ignoreActors=false); + RayResult castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, bool ignorePlayer, + bool ignoreActors = false, std::span ignoreList = {}); - /// Return the object under the mouse cursor / crosshair position, given by nX and nY normalized screen coordinates, - /// where (0,0) is the top left corner. - RayResult castCameraToViewportRay(const float nX, const float nY, float maxDistance, bool ignorePlayer, bool ignoreActors=false); + /// Return the object under the mouse cursor / crosshair position, given by nX and nY normalized screen + /// coordinates, where (0,0) is the top left corner. + RayResult castCameraToViewportRay( + const float nX, const float nY, float maxDistance, bool ignorePlayer, bool ignoreActors = false); - /// Get the bounding box of the given object in screen coordinates as (minX, minY, maxX, maxY), with (0,0) being the top left corner. - osg::Vec4f getScreenBounds(const osg::BoundingBox &worldbb); + /// Get the bounding box of the given object in screen coordinates as (minX, minY, maxX, maxY), with (0,0) being + /// the top left corner. + osg::Vec4f getScreenBounds(const osg::BoundingBox& worldbb); void setSkyEnabled(bool enabled); @@ -178,7 +196,8 @@ namespace MWRender SkyManager* getSkyManager(); - void spawnEffect(const std::string &model, const std::string &texture, const osg::Vec3f &worldPosition, float scale = 1.f, bool isMagicVFX = true); + void spawnEffect(VFS::Path::NormalizedView model, std::string_view texture, const osg::Vec3f& worldPosition, + float scale = 1.f, bool isMagicVFX = true); /// Clear all savegame-specific data void clear(); @@ -191,11 +210,13 @@ namespace MWRender Animation* getAnimation(const MWWorld::Ptr& ptr); const Animation* getAnimation(const MWWorld::ConstPtr& ptr) const; + PostProcessor* getPostProcessor(); + void addWaterRippleEmitter(const MWWorld::Ptr& ptr); void removeWaterRippleEmitter(const MWWorld::Ptr& ptr); void emitWaterRipple(const osg::Vec3f& pos); - void updatePlayerPtr(const MWWorld::Ptr &ptr); + void updatePlayerPtr(const MWWorld::Ptr& ptr); void removePlayer(const MWWorld::Ptr& player); void setupPlayer(const MWWorld::Ptr& player); @@ -205,67 +226,91 @@ namespace MWRender void processChangedSettings(const Settings::CategorySettingVector& settings); - float getNearClipDistance() const; + float getNearClipDistance() const { return mNearClip; } + float getViewDistance() const { return mViewDistance; } - float getTerrainHeightAt(const osg::Vec3f& pos); + void setViewDistance(float distance, bool delay = false); + + float getTerrainHeightAt(const osg::Vec3f& pos, ESM::RefId worldspace); // camera stuff Camera* getCamera() { return mCamera.get(); } - const osg::Vec3f& getCameraPosition() const { return mCurrentCameraPos; } /// temporarily override the field of view with given value. void overrideFieldOfView(float val); + void setFieldOfView(float val); + float getFieldOfView() const; /// reset a previous overrideFieldOfView() call, i.e. revert to field of view specified in the settings file. void resetFieldOfView(); osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& object) const; - void exportSceneGraph(const MWWorld::Ptr& ptr, const std::string& filename, const std::string& format); + // Return local bounding box. Safe to be called in parallel with cull thread. + osg::BoundingBox getCullSafeBoundingBox(const MWWorld::Ptr& ptr) const; + + void exportSceneGraph( + const MWWorld::Ptr& ptr, const std::filesystem::path& filename, const std::string& format); + + Debug::DebugDrawer& getDebugDrawer() const { return *mDebugDraw; } LandManager* getLandManager() const; bool toggleBorders(); void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, - const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const; + const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end) const; void removeActorPath(const MWWorld::ConstPtr& actor) const; void setNavMeshNumber(const std::size_t value); - void setActiveGrid(const osg::Vec4i &grid); + void setActiveGrid(const osg::Vec4i& grid); bool pagingEnableObject(int type, const MWWorld::ConstPtr& ptr, bool enabled); - void pagingBlacklistObject(int type, const MWWorld::ConstPtr &ptr); + void pagingBlacklistObject(int type, const MWWorld::ConstPtr& ptr); bool pagingUnlockCache(); - void getPagedRefnums(const osg::Vec4i &activeGrid, std::set &out); + void getPagedRefnums(const osg::Vec4i& activeGrid, std::vector& out); - private: void updateProjectionMatrix(); + + void setScreenRes(int width, int height); + + void setNavMeshMode(Settings::NavMeshRenderMode value); + + private: void updateTextureFiltering(); void updateAmbient(); void setFogColor(const osg::Vec4f& color); void updateThirdPersonViewMode(); + struct WorldspaceChunkMgr + { + std::unique_ptr mTerrain; + std::unique_ptr mObjectPaging; + std::unique_ptr mGroundcover; + }; + + WorldspaceChunkMgr& getWorldspaceChunkMgr(ESM::RefId worldspace); + void reportStats() const; void updateNavMesh(); void updateRecastMesh(); - osg::ref_ptr getIntersectionVisitor(osgUtil::Intersector* intersector, bool ignorePlayer, bool ignoreActors); + const bool mSkyBlending; - osg::ref_ptr mIntersectionVisitor; + osg::ref_ptr getIntersectionVisitor(osgUtil::Intersector* intersector, + bool ignorePlayer, bool ignoreActors, std::span ignoreList = {}); + + osg::ref_ptr mIntersectionVisitor; osg::ref_ptr mViewer; osg::ref_ptr mRootNode; - osg::ref_ptr mSceneRoot; + osg::ref_ptr mSceneRoot; Resource::ResourceSystem* mResourceSystem; - osg::ref_ptr mGroundcoverUpdater; - osg::ref_ptr mWorkQueue; - osg::ref_ptr mUnrefQueue; osg::ref_ptr mSunLight; @@ -277,26 +322,27 @@ namespace MWRender std::unique_ptr mPathgrid; std::unique_ptr mObjects; std::unique_ptr mWater; - std::unique_ptr mTerrain; - std::unique_ptr mGroundcoverWorld; + std::unordered_map mWorldspaceChunks; + Terrain::World* mTerrain; std::unique_ptr mTerrainStorage; - std::unique_ptr mObjectPaging; - std::unique_ptr mGroundcover; + ObjectPaging* mObjectPaging; + Groundcover* mGroundcover; std::unique_ptr mSky; std::unique_ptr mFog; std::unique_ptr mScreenshotManager; std::unique_ptr mEffectManager; std::unique_ptr mShadowManager; + osg::ref_ptr mPostProcessor; osg::ref_ptr mPlayerAnimation; osg::ref_ptr mPlayerNode; std::unique_ptr mCamera; - std::unique_ptr mViewOverShoulderController; - osg::Vec3f mCurrentCameraPos; + osg::ref_ptr mDebugDraw; osg::ref_ptr mStateUpdater; + osg::ref_ptr mSharedUniformStateUpdater; + osg::ref_ptr mPerViewUniformStateUpdater; osg::Vec4f mAmbientColor; - float mMinimumAmbientLuminance; float mNightEyeFactor; float mNearClip; @@ -305,8 +351,11 @@ namespace MWRender float mFieldOfViewOverride; float mFieldOfView; float mFirstPersonFieldOfView; + bool mUpdateProjectionMatrix = false; + bool mNight = false; + const MWWorld::GroundcoverStore& mGroundCoverStore; - void operator = (const RenderingManager&); + void operator=(const RenderingManager&); RenderingManager(const RenderingManager&); }; diff --git a/apps/openmw/mwrender/ripples.cpp b/apps/openmw/mwrender/ripples.cpp new file mode 100644 index 00000000000..d9c1b417378 --- /dev/null +++ b/apps/openmw/mwrender/ripples.cpp @@ -0,0 +1,308 @@ +#include "ripples.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwworld/ptr.hpp" + +#include "../mwmechanics/actorutil.hpp" + +#include "vismask.hpp" + +namespace MWRender +{ + RipplesSurface::RipplesSurface(Resource::ResourceSystem* resourceSystem) + : osg::Geometry() + , mResourceSystem(resourceSystem) + { + setUseDisplayList(false); + setUseVertexBufferObjects(true); + + osg::ref_ptr verts = new osg::Vec3Array; + verts->push_back(osg::Vec3f(-1, -1, 0)); + verts->push_back(osg::Vec3f(-1, 3, 0)); + verts->push_back(osg::Vec3f(3, -1, 0)); + + setVertexArray(verts); + + setCullingActive(false); + + addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES, 0, 3)); + +#ifdef __APPLE__ + // we can not trust Apple :) + mUseCompute = false; +#else + constexpr float minimumGLVersionRequiredForCompute = 4.4; + osg::GLExtensions& exts = SceneUtil::getGLExtensions(); + mUseCompute = exts.glVersion >= minimumGLVersionRequiredForCompute + && exts.glslLanguageVersion >= minimumGLVersionRequiredForCompute; +#endif + + if (mUseCompute) + Log(Debug::Info) << "Initialized compute shader pipeline for water ripples"; + else + Log(Debug::Info) << "Initialized fallback fragment shader pipeline for water ripples"; + + for (size_t i = 0; i < mState.size(); ++i) + { + osg::ref_ptr stateset = new osg::StateSet; + // bindings are set in the compute shader + if (!mUseCompute) + stateset->addUniform(new osg::Uniform("imageIn", 0)); + + stateset->addUniform(new osg::Uniform("offset", osg::Vec2f())); + stateset->addUniform(new osg::Uniform("positionCount", 0)); + stateset->addUniform(new osg::Uniform(osg::Uniform::Type::FLOAT_VEC3, "positions", 100)); + stateset->setAttributeAndModes(new osg::Viewport(0, 0, RipplesSurface::sRTTSize, RipplesSurface::sRTTSize)); + mState[i].mStateset = stateset; + } + + for (size_t i = 0; i < mTextures.size(); ++i) + { + osg::ref_ptr texture = new osg::Texture2D; + texture->setSourceFormat(GL_RGBA); + texture->setInternalFormat(GL_RGBA16F); + texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture::LINEAR); + texture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture::LINEAR); + texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_BORDER); + texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_BORDER); + texture->setBorderColor(osg::Vec4(0, 0, 0, 0)); + texture->setTextureSize(sRTTSize, sRTTSize); + + mTextures[i] = texture; + + mFBOs[i] = new osg::FrameBufferObject; + mFBOs[i]->setAttachment(osg::Camera::COLOR_BUFFER0, osg::FrameBufferAttachment(mTextures[i])); + } + + if (mUseCompute) + setupComputePipeline(); + else + setupFragmentPipeline(); + + setCullCallback(new osg::NodeCallback); + setUpdateCallback(new osg::NodeCallback); + } + + void RipplesSurface::setupFragmentPipeline() + { + auto& shaderManager = mResourceSystem->getSceneManager()->getShaderManager(); + + Shader::ShaderManager::DefineMap defineMap = { { "rippleMapSize", std::to_string(sRTTSize) + ".0" } }; + + osg::ref_ptr vertex = shaderManager.getShader("fullscreen_tri.vert", {}, osg::Shader::VERTEX); + + mProgramBlobber = shaderManager.getProgram( + vertex, shaderManager.getShader("ripples_blobber.frag", defineMap, osg::Shader::FRAGMENT)); + mProgramSimulation = shaderManager.getProgram( + std::move(vertex), shaderManager.getShader("ripples_simulate.frag", defineMap, osg::Shader::FRAGMENT)); + } + + void RipplesSurface::setupComputePipeline() + { + auto& shaderManager = mResourceSystem->getSceneManager()->getShaderManager(); + + mProgramBlobber = shaderManager.getProgram( + nullptr, shaderManager.getShader("core/ripples_blobber.comp", {}, osg::Shader::COMPUTE)); + mProgramSimulation = shaderManager.getProgram( + nullptr, shaderManager.getShader("core/ripples_simulate.comp", {}, osg::Shader::COMPUTE)); + } + + void RipplesSurface::updateState(const osg::FrameStamp& frameStamp, State& state) + { + state.mPaused = mPaused; + + if (mPaused) + return; + + constexpr double updateFrequency = 60.0; + constexpr double updatePeriod = 1.0 / updateFrequency; + + const double simulationTime = frameStamp.getSimulationTime(); + const double frameDuration = simulationTime - mLastSimulationTime; + mLastSimulationTime = simulationTime; + + mRemainingWaveTime += frameDuration; + const double ticks = std::floor(mRemainingWaveTime * updateFrequency); + mRemainingWaveTime -= ticks * updatePeriod; + + if (ticks == 0) + { + state.mPaused = true; + return; + } + + const MWWorld::Ptr player = MWMechanics::getPlayer(); + const ESM::Position& playerPos = player.getRefData().getPosition(); + + mCurrentPlayerPos = osg::Vec2f( + std::floor(playerPos.pos[0] / sWorldScaleFactor), std::floor(playerPos.pos[1] / sWorldScaleFactor)); + const osg::Vec2f offset = mCurrentPlayerPos - mLastPlayerPos; + mLastPlayerPos = mCurrentPlayerPos; + + state.mStateset->getUniform("positionCount")->set(static_cast(mPositionCount)); + state.mStateset->getUniform("offset")->set(offset); + + osg::Uniform* const positions = state.mStateset->getUniform("positions"); + + for (std::size_t i = 0; i < mPositionCount; ++i) + { + osg::Vec3f pos = mPositions[i] + - osg::Vec3f(mCurrentPlayerPos.x() * sWorldScaleFactor, mCurrentPlayerPos.y() * sWorldScaleFactor, 0.0) + + osg::Vec3f(sRTTSize * sWorldScaleFactor / 2, sRTTSize * sWorldScaleFactor / 2, 0.0); + pos /= sWorldScaleFactor; + positions->setElement(i, pos); + } + positions->dirty(); + + mPositionCount = 0; + } + + void RipplesSurface::traverse(osg::NodeVisitor& nv) + { + const osg::FrameStamp* const frameStamp = nv.getFrameStamp(); + + if (frameStamp == nullptr) + return; + + if (nv.getVisitorType() == osg::NodeVisitor::CULL_VISITOR) + updateState(*frameStamp, mState[frameStamp->getFrameNumber() % 2]); + + osg::Geometry::traverse(nv); + } + + void RipplesSurface::drawImplementation(osg::RenderInfo& renderInfo) const + { + osg::State& state = *renderInfo.getState(); + const std::size_t currentFrame = state.getFrameStamp()->getFrameNumber() % 2; + const State& frameState = mState[currentFrame]; + if (frameState.mPaused) + { + return; + } + + osg::GLExtensions& ext = *state.get(); + const std::size_t contextID = state.getContextID(); + + const auto bindImage = [&](osg::Texture2D* texture, GLuint index, GLenum access) { + osg::Texture::TextureObject* to = texture->getTextureObject(contextID); + if (!to || texture->isDirty(contextID)) + { + state.applyTextureAttribute(index, texture); + to = texture->getTextureObject(contextID); + } + ext.glBindImageTexture(index, to->id(), 0, GL_FALSE, 0, access, GL_RGBA16F); + }; + + // PASS: Blot in all ripple spawners + mProgramBlobber->apply(state); + state.apply(frameState.mStateset); + + if (mUseCompute) + { + bindImage(mTextures[1], 0, GL_WRITE_ONLY_ARB); + bindImage(mTextures[0], 1, GL_READ_ONLY_ARB); + + ext.glDispatchCompute(sRTTSize / 16, sRTTSize / 16, 1); + ext.glMemoryBarrier(GL_ALL_BARRIER_BITS); + } + else + { + mFBOs[1]->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + state.applyTextureAttribute(0, mTextures[0]); + osg::Geometry::drawImplementation(renderInfo); + } + + // PASS: Wave simulation + mProgramSimulation->apply(state); + state.apply(frameState.mStateset); + + if (mUseCompute) + { + bindImage(mTextures[0], 0, GL_WRITE_ONLY_ARB); + bindImage(mTextures[1], 1, GL_READ_ONLY_ARB); + + ext.glDispatchCompute(sRTTSize / 16, sRTTSize / 16, 1); + ext.glMemoryBarrier(GL_ALL_BARRIER_BITS); + } + else + { + mFBOs[0]->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + state.applyTextureAttribute(0, mTextures[1]); + osg::Geometry::drawImplementation(renderInfo); + } + } + + osg::Texture* RipplesSurface::getColorTexture() const + { + return mTextures[0]; + } + + void RipplesSurface::emit(const osg::Vec3f pos, float sizeInCellUnits) + { + // Emitted positions are reset every frame, don't bother wrapping around when out of buffer space + if (mPositionCount >= mPositions.size()) + { + return; + } + + mPositions[mPositionCount] = osg::Vec3f(pos.x(), pos.y(), sizeInCellUnits); + + mPositionCount++; + } + + void RipplesSurface::releaseGLObjects(osg::State* state) const + { + for (const auto& tex : mTextures) + tex->releaseGLObjects(state); + for (const auto& fbo : mFBOs) + fbo->releaseGLObjects(state); + + if (mProgramBlobber) + mProgramBlobber->releaseGLObjects(state); + if (mProgramSimulation) + mProgramSimulation->releaseGLObjects(state); + } + + Ripples::Ripples(Resource::ResourceSystem* resourceSystem) + : osg::Camera() + , mRipples(new RipplesSurface(resourceSystem)) + { + getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + setRenderOrder(osg::Camera::PRE_RENDER); + setReferenceFrame(osg::Camera::ABSOLUTE_RF); + setNodeMask(Mask_RenderToTexture); + setClearMask(GL_NONE); + setViewport(0, 0, RipplesSurface::sRTTSize, RipplesSurface::sRTTSize); + addChild(mRipples); + setCullingActive(false); + setImplicitBufferAttachmentMask(0, 0); + } + + osg::Texture* Ripples::getColorTexture() const + { + return mRipples->getColorTexture(); + } + + void Ripples::emit(const osg::Vec3f pos, float sizeInCellUnits) + { + mRipples->emit(pos, sizeInCellUnits); + } + + void Ripples::setPaused(bool paused) + { + mRipples->setPaused(paused); + } +} diff --git a/apps/openmw/mwrender/ripples.hpp b/apps/openmw/mwrender/ripples.hpp new file mode 100644 index 00000000000..e355b16ecd1 --- /dev/null +++ b/apps/openmw/mwrender/ripples.hpp @@ -0,0 +1,104 @@ +#ifndef OPENMW_MWRENDER_RIPPLES_H +#define OPENMW_MWRENDER_RIPPLES_H + +#include + +#include +#include + +#include +#include + +namespace Resource +{ + class ResourceSystem; +} + +namespace osg +{ + class Camera; + class Geometry; + class Program; + class Texture; + class StateSet; + class NodeVisitor; + class Texture; + class Texture2D; + class FrameBufferObject; +} + +namespace MWRender +{ + class RipplesSurface : public osg::Geometry + { + public: + RipplesSurface(Resource::ResourceSystem* resourceSystem); + + osg::Texture* getColorTexture() const; + + void emit(const osg::Vec3f pos, float sizeInCellUnits); + + void drawImplementation(osg::RenderInfo& renderInfo) const override; + + void setPaused(bool paused) { mPaused = paused; } + + void traverse(osg::NodeVisitor& nv) override; + + void releaseGLObjects(osg::State* state) const override; + + static constexpr size_t sRTTSize = 1024; + // e.g. texel to cell unit ratio + static constexpr float sWorldScaleFactor = 2.5; + + private: + struct State + { + bool mPaused = true; + osg::ref_ptr mStateset; + }; + + void setupFragmentPipeline(); + + void setupComputePipeline(); + + inline void updateState(const osg::FrameStamp& frameStamp, State& state); + + Resource::ResourceSystem* mResourceSystem; + + size_t mPositionCount = 0; + std::array mPositions; + + std::array mState; + + osg::Vec2f mCurrentPlayerPos; + osg::Vec2f mLastPlayerPos; + + std::array, 2> mTextures; + std::array, 2> mFBOs; + + osg::ref_ptr mProgramBlobber; + osg::ref_ptr mProgramSimulation; + + bool mPaused = false; + bool mUseCompute = false; + + double mLastSimulationTime = 0; + double mRemainingWaveTime = 0; + }; + + class Ripples : public osg::Camera + { + public: + Ripples(Resource::ResourceSystem* resourceSystem); + + osg::Texture* getColorTexture() const; + + void emit(const osg::Vec3f pos, float sizeInCellUnits); + + void setPaused(bool paused); + + osg::ref_ptr mRipples; + }; +} + +#endif diff --git a/apps/openmw/mwrender/ripplesimulation.cpp b/apps/openmw/mwrender/ripplesimulation.cpp index 6788f53f447..88a620efd09 100644 --- a/apps/openmw/mwrender/ripplesimulation.cpp +++ b/apps/openmw/mwrender/ripplesimulation.cpp @@ -1,72 +1,76 @@ #include "ripplesimulation.hpp" #include +#include -#include -#include -#include #include +#include +#include #include +#include #include #include +#include #include #include #include #include #include -#include +#include #include "vismask.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" namespace { - void createWaterRippleStateSet(Resource::ResourceSystem* resourceSystem,osg::Node* node) + void createWaterRippleStateSet(Resource::ResourceSystem* resourceSystem, osg::Node* node) { int rippleFrameCount = Fallback::Map::getInt("Water_RippleFrameCount"); if (rippleFrameCount <= 0) return; - const std::string& tex = Fallback::Map::getString("Water_RippleTexture"); + std::string_view tex = Fallback::Map::getString("Water_RippleTexture"); - std::vector > textures; - for (int i=0; i> textures; + for (int i = 0; i < rippleFrameCount; ++i) { std::ostringstream texname; texname << "textures/water/" << tex << std::setw(2) << std::setfill('0') << i << ".dds"; - osg::ref_ptr tex2 (new osg::Texture2D(resourceSystem->getImageManager()->getImage(texname.str()))); + const VFS::Path::Normalized path(texname.str()); + osg::ref_ptr tex2(new osg::Texture2D(resourceSystem->getImageManager()->getImage(path))); tex2->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); tex2->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); resourceSystem->getSceneManager()->applyFilterSettings(tex2); textures.push_back(tex2); } - osg::ref_ptr controller (new NifOsg::FlipController(0, 0.3f/rippleFrameCount, textures)); - controller->setSource(std::shared_ptr(new SceneUtil::FrameTimeSource)); + osg::ref_ptr controller( + new NifOsg::FlipController(0, 0.3f / rippleFrameCount, textures)); + controller->setSource(std::make_shared()); node->addUpdateCallback(controller); - osg::ref_ptr stateset (new osg::StateSet); + osg::ref_ptr stateset(new osg::StateSet); stateset->setMode(GL_BLEND, osg::StateAttribute::ON); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); stateset->setTextureAttributeAndModes(0, textures[0], osg::StateAttribute::ON); - osg::ref_ptr depth (new osg::Depth); + osg::ref_ptr depth = new SceneUtil::AutoDepth; depth->setWriteMask(false); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); - osg::ref_ptr polygonOffset (new osg::PolygonOffset); - polygonOffset->setUnits(-1); - polygonOffset->setFactor(-1); + osg::ref_ptr polygonOffset(new osg::PolygonOffset); + polygonOffset->setUnits(SceneUtil::AutoDepth::isReversed() ? 1 : -1); + polygonOffset->setFactor(SceneUtil::AutoDepth::isReversed() ? 1 : -1); stateset->setAttributeAndModes(polygonOffset, osg::StateAttribute::ON); stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - osg::ref_ptr mat (new osg::Material); + osg::ref_ptr mat(new osg::Material); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); @@ -76,148 +80,198 @@ namespace node->setStateSet(stateset); } + + int findOldestParticleAlive(const osgParticle::ParticleSystem* partsys) + { + int oldest = -1; + float oldestAge = 0.f; + for (int i = 0; i < partsys->numParticles(); ++i) + { + const osgParticle::Particle* particle = partsys->getParticle(i); + if (!particle->isAlive()) + continue; + const float age = particle->getAge(); + if (oldest == -1 || age > oldestAge) + { + oldest = i; + oldestAge = age; + } + } + return oldest; + } } namespace MWRender { -RippleSimulation::RippleSimulation(osg::Group *parent, Resource::ResourceSystem* resourceSystem) - : mParent(parent) -{ - mParticleSystem = new osgParticle::ParticleSystem; + RippleSimulation::RippleSimulation(osg::Group* parent, Resource::ResourceSystem* resourceSystem) + : mParent(parent) + , mMaxNumberRipples(Fallback::Map::getInt("Water_MaxNumberRipples")) + { + mParticleSystem = new osgParticle::ParticleSystem; - mParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); - mParticleSystem->setAlignVectorX(osg::Vec3f(1,0,0)); - mParticleSystem->setAlignVectorY(osg::Vec3f(0,1,0)); + mParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); + mParticleSystem->setAlignVectorX(osg::Vec3f(1, 0, 0)); + mParticleSystem->setAlignVectorY(osg::Vec3f(0, 1, 0)); - osgParticle::Particle& particleTemplate = mParticleSystem->getDefaultParticleTemplate(); - particleTemplate.setSizeRange(osgParticle::rangef(15, 180)); - particleTemplate.setColorRange(osgParticle::rangev4(osg::Vec4f(1,1,1,0.7), osg::Vec4f(1,1,1,0.7))); - particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 0.f)); - particleTemplate.setAngularVelocity(osg::Vec3f(0,0,Fallback::Map::getFloat("Water_RippleRotSpeed"))); - particleTemplate.setLifeTime(Fallback::Map::getFloat("Water_RippleLifetime")); + osgParticle::Particle& particleTemplate = mParticleSystem->getDefaultParticleTemplate(); + particleTemplate.setSizeRange(osgParticle::rangef(15, 180)); + particleTemplate.setColorRange(osgParticle::rangev4(osg::Vec4f(1, 1, 1, 0.7), osg::Vec4f(1, 1, 1, 0.7))); + particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 0.f)); + particleTemplate.setAngularVelocity(osg::Vec3f(0, 0, Fallback::Map::getFloat("Water_RippleRotSpeed"))); + particleTemplate.setLifeTime(Fallback::Map::getFloat("Water_RippleLifetime")); - osg::ref_ptr updater (new osgParticle::ParticleSystemUpdater); - updater->addParticleSystem(mParticleSystem); + osg::ref_ptr updater(new osgParticle::ParticleSystemUpdater); + updater->addParticleSystem(mParticleSystem); - mParticleNode = new osg::PositionAttitudeTransform; - mParticleNode->setName("Ripple Root"); - mParticleNode->addChild(updater); - mParticleNode->addChild(mParticleSystem); - mParticleNode->setNodeMask(Mask_Water); + mParticleNode = new osg::PositionAttitudeTransform; + mParticleNode->setName("Ripple Root"); + mParticleNode->addChild(updater); + mParticleNode->addChild(mParticleSystem); + mParticleNode->setNodeMask(Mask_Water); - createWaterRippleStateSet(resourceSystem, mParticleNode); + createWaterRippleStateSet(resourceSystem, mParticleNode); - resourceSystem->getSceneManager()->recreateShaders(mParticleNode); + resourceSystem->getSceneManager()->recreateShaders(mParticleNode); - mParent->addChild(mParticleNode); -} + mParent->addChild(mParticleNode); + } -RippleSimulation::~RippleSimulation() -{ - mParent->removeChild(mParticleNode); -} + RippleSimulation::~RippleSimulation() + { + mParent->removeChild(mParticleNode); + } -void RippleSimulation::update(float dt) -{ - const MWBase::World* world = MWBase::Environment::get().getWorld(); - for (Emitter& emitter : mEmitters) + void RippleSimulation::update(float dt) { - MWWorld::ConstPtr& ptr = emitter.mPtr; - if (ptr == MWBase::Environment::get().getWorld ()->getPlayerPtr()) + const MWBase::World* world = MWBase::Environment::get().getWorld(); + for (Emitter& emitter : mEmitters) { - // fetch a new ptr (to handle cell change etc) - // for non-player actors this is done in updateObjectCell - ptr = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + emitter.mTimer -= dt; + MWWorld::ConstPtr& ptr = emitter.mPtr; + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + // fetch a new ptr (to handle cell change etc) + // for non-player actors this is done in updateObjectCell + ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); + } + + osg::Vec3f currentPos(ptr.getRefData().getPosition().asVec3()); + + bool shouldEmit = (world->isUnderwater(ptr.getCell(), currentPos) && !world->isSubmerged(ptr)) + || world->isWalkingOnWater(ptr); + + if (!shouldEmit) + { + emitter.mTimer = 0.f; + } + else if (mRipples) + { + // Ripple simulation needs to continously apply impulses to keep simulation alive. + // Adding a timer delay will introduce many smaller ripples around actor instead of a smooth wake + currentPos.z() = mParticleNode->getPosition().z(); + emitRipple(currentPos); + } + else if (emitter.mTimer <= 0.f || (currentPos - emitter.mLastEmitPosition).length() > 10) + { + emitter.mLastEmitPosition = currentPos; + emitter.mTimer = 1.5f; + + currentPos.z() = mParticleNode->getPosition().z(); + + emitRipple(currentPos); + } } + } - osg::Vec3f currentPos (ptr.getRefData().getPosition().asVec3()); + void RippleSimulation::addEmitter(const MWWorld::ConstPtr& ptr, float scale, float force) + { + Emitter newEmitter; + newEmitter.mPtr = ptr; + newEmitter.mScale = scale; + newEmitter.mForce = force; + newEmitter.mLastEmitPosition = osg::Vec3f(0, 0, 0); + newEmitter.mTimer = 0.f; + mEmitters.push_back(newEmitter); + } - bool shouldEmit = (world->isUnderwater(ptr.getCell(), currentPos) && !world->isSubmerged(ptr)) || world->isWalkingOnWater(ptr); - if (shouldEmit && (currentPos - emitter.mLastEmitPosition).length() > 10) + void RippleSimulation::removeEmitter(const MWWorld::ConstPtr& ptr) + { + for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end(); ++it) { - emitter.mLastEmitPosition = currentPos; - - currentPos.z() = mParticleNode->getPosition().z(); - - if (mParticleSystem->numParticles()-mParticleSystem->numDeadParticles() > 500) - continue; // TODO: remove the oldest particle to make room? - - emitRipple(currentPos); + if (it->mPtr == ptr) + { + mEmitters.erase(it); + return; + } } } -} - - -void RippleSimulation::addEmitter(const MWWorld::ConstPtr& ptr, float scale, float force) -{ - Emitter newEmitter; - newEmitter.mPtr = ptr; - newEmitter.mScale = scale; - newEmitter.mForce = force; - newEmitter.mLastEmitPosition = osg::Vec3f(0,0,0); - mEmitters.push_back (newEmitter); -} -void RippleSimulation::removeEmitter (const MWWorld::ConstPtr& ptr) -{ - for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end(); ++it) + void RippleSimulation::updateEmitterPtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& ptr) { - if (it->mPtr == ptr) + for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end(); ++it) { - mEmitters.erase(it); - return; + if (it->mPtr == old) + { + it->mPtr = ptr; + return; + } } } -} -void RippleSimulation::updateEmitterPtr (const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& ptr) -{ - for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end(); ++it) + void RippleSimulation::removeCell(const MWWorld::CellStore* store) { - if (it->mPtr == old) + for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end();) { - it->mPtr = ptr; - return; + if ((it->mPtr.isInCell() && it->mPtr.getCell() == store) && it->mPtr != MWMechanics::getPlayer()) + { + it = mEmitters.erase(it); + } + else + ++it; } } -} -void RippleSimulation::removeCell(const MWWorld::CellStore *store) -{ - for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end();) + void RippleSimulation::emitRipple(const osg::Vec3f& pos) { - if ((it->mPtr.isInCell() && it->mPtr.getCell() == store) && it->mPtr != MWMechanics::getPlayer()) + if (std::abs(pos.z() - mParticleNode->getPosition().z()) < 20) { - it = mEmitters.erase(it); + if (mRipples) + { + constexpr float particleRippleSizeInUnits = 12.f; + mRipples->emit(osg::Vec3f(pos.x(), pos.y(), 0.f), particleRippleSizeInUnits); + } + else + { + if (mMaxNumberRipples <= 0) + return; + + osgParticle::ParticleSystem::ScopedWriteLock lock(*mParticleSystem->getReadWriteMutex()); + if (mParticleSystem->numParticles() - mParticleSystem->numDeadParticles() > mMaxNumberRipples) + { + // osgParticle::ParticleSystem design requires this to be O(N) + // However, the number of particles we'll have to go through is not large + // If the user makes the limit absurd and manages to actually hit it this could be a problem + const int oldest = findOldestParticleAlive(mParticleSystem); + if (oldest != -1) + mParticleSystem->reuseParticle(oldest); + } + osgParticle::Particle* p = mParticleSystem->createParticle(nullptr); + p->setPosition(osg::Vec3f(pos.x(), pos.y(), 0.f)); + p->setAngle(osg::Vec3f(0, 0, Misc::Rng::rollProbability() * osg::PI * 2 - osg::PI)); + } } - else - ++it; } -} -void RippleSimulation::emitRipple(const osg::Vec3f &pos) -{ - if (std::abs(pos.z() - mParticleNode->getPosition().z()) < 20) + void RippleSimulation::setWaterHeight(float height) { - osgParticle::ParticleSystem::ScopedWriteLock lock(*mParticleSystem->getReadWriteMutex()); - osgParticle::Particle* p = mParticleSystem->createParticle(nullptr); - p->setPosition(osg::Vec3f(pos.x(), pos.y(), 0.f)); - p->setAngle(osg::Vec3f(0,0, Misc::Rng::rollProbability() * osg::PI * 2 - osg::PI)); + mParticleNode->setPosition(osg::Vec3f(0, 0, height)); } -} - -void RippleSimulation::setWaterHeight(float height) -{ - mParticleNode->setPosition(osg::Vec3f(0,0,height)); -} - -void RippleSimulation::clear() -{ - for (int i=0; inumParticles(); ++i) - mParticleSystem->destroyParticle(i); -} - + void RippleSimulation::clear() + { + for (int i = 0; i < mParticleSystem->numParticles(); ++i) + mParticleSystem->destroyParticle(i); + } } diff --git a/apps/openmw/mwrender/ripplesimulation.hpp b/apps/openmw/mwrender/ripplesimulation.hpp index 186a578ba89..10b47f56793 100644 --- a/apps/openmw/mwrender/ripplesimulation.hpp +++ b/apps/openmw/mwrender/ripplesimulation.hpp @@ -5,6 +5,8 @@ #include "../mwworld/ptr.hpp" +#include "ripples.hpp" + namespace osg { class Group; @@ -35,6 +37,7 @@ namespace MWRender osg::Vec3f mLastEmitPosition; float mScale; float mForce; + float mTimer; }; class RippleSimulation @@ -47,9 +50,9 @@ namespace MWRender void update(float dt); /// adds an emitter, position will be tracked automatically - void addEmitter (const MWWorld::ConstPtr& ptr, float scale = 1.f, float force = 1.f); - void removeEmitter (const MWWorld::ConstPtr& ptr); - void updateEmitterPtr (const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& ptr); + void addEmitter(const MWWorld::ConstPtr& ptr, float scale = 1.f, float force = 1.f); + void removeEmitter(const MWWorld::ConstPtr& ptr); + void updateEmitterPtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& ptr); void removeCell(const MWWorld::CellStore* store); void emitRipple(const osg::Vec3f& pos); @@ -60,6 +63,8 @@ namespace MWRender /// Remove all active ripples void clear(); + void setRipples(Ripples* ripples) { mRipples = ripples; } + private: osg::ref_ptr mParent; @@ -67,6 +72,10 @@ namespace MWRender osg::ref_ptr mParticleNode; std::vector mEmitters; + + Ripples* mRipples = nullptr; + + int mMaxNumberRipples; }; } diff --git a/apps/openmw/mwrender/rotatecontroller.cpp b/apps/openmw/mwrender/rotatecontroller.cpp index 534cc749062..0e84087710a 100644 --- a/apps/openmw/mwrender/rotatecontroller.cpp +++ b/apps/openmw/mwrender/rotatecontroller.cpp @@ -1,57 +1,69 @@ #include "rotatecontroller.hpp" #include +#include namespace MWRender { -RotateController::RotateController(osg::Node *relativeTo) - : mEnabled(true) - , mRelativeTo(relativeTo) -{ - -} + RotateController::RotateController(osg::Node* relativeTo) + : mEnabled(true) + , mRelativeTo(relativeTo) + { + } -void RotateController::setEnabled(bool enabled) -{ - mEnabled = enabled; -} + void RotateController::setEnabled(bool enabled) + { + mEnabled = enabled; + } -void RotateController::setRotate(const osg::Quat &rotate) -{ - mRotate = rotate; -} + void RotateController::setRotate(const osg::Quat& rotate) + { + mRotate = rotate; + } -void RotateController::operator()(osg::Node *node, osg::NodeVisitor *nv) -{ - if (!mEnabled) + void RotateController::setOffset(const osg::Vec3f& offset) { - traverse(node, nv); - return; + mOffset = offset; } - osg::MatrixTransform* transform = static_cast(node); - osg::Matrix matrix = transform->getMatrix(); - osg::Quat worldOrient = getWorldOrientation(node); - osg::Quat orient = worldOrient * mRotate * worldOrient.inverse() * matrix.getRotate(); - matrix.setRotate(orient); + void RotateController::operator()(osg::MatrixTransform* node, osg::NodeVisitor* nv) + { + if (!mEnabled) + { + traverse(node, nv); + return; + } + osg::Matrix matrix = node->getMatrix(); - transform->setMatrix(matrix); + osg::Quat worldOrient; + osg::NodePathList nodepaths = node->getParentalNodePaths(mRelativeTo); - traverse(node,nv); -} + if (!nodepaths.empty()) + { + osg::Matrixf worldMat = osg::computeLocalToWorld(nodepaths[0]); + worldOrient = worldMat.getRotate(); + } -osg::Quat RotateController::getWorldOrientation(osg::Node *node) -{ - // this could be optimized later, we just need the world orientation, not the full matrix - osg::NodePathList nodepaths = node->getParentalNodePaths(mRelativeTo); - osg::Quat worldOrient; - if (!nodepaths.empty()) - { - osg::Matrixf worldMat = osg::computeLocalToWorld(nodepaths[0]); - worldOrient = worldMat.getRotate(); - } - return worldOrient; -} + osg::Quat worldOrientInverse = worldOrient.inverse(); + osg::Quat orient = worldOrient * mRotate * worldOrientInverse * matrix.getRotate(); + matrix.setRotate(orient); + matrix.setTrans(matrix.getTrans() + worldOrientInverse * mOffset); + + node->setMatrix(matrix); + + // If we are linked to a bone we must call setMatrixInSkeletonSpace + osgAnimation::Bone* b = dynamic_cast(node); + if (b) + { + osgAnimation::Bone* parent = b->getBoneParent(); + if (parent) + matrix *= parent->getMatrixInSkeletonSpace(); + + b->setMatrixInSkeletonSpace(matrix); + } + + traverse(node, nv); + } } diff --git a/apps/openmw/mwrender/rotatecontroller.hpp b/apps/openmw/mwrender/rotatecontroller.hpp index 9d4080ac6ec..3d9f44c6a3e 100644 --- a/apps/openmw/mwrender/rotatecontroller.hpp +++ b/apps/openmw/mwrender/rotatecontroller.hpp @@ -1,35 +1,43 @@ #ifndef OPENMW_MWRENDER_ROTATECONTROLLER_H #define OPENMW_MWRENDER_ROTATECONTROLLER_H -#include +#include #include -namespace MWRender +namespace osg { + class MatrixTransform; +} -/// Applies a rotation in \a relativeTo's space. -/// @note Assumes that the node being rotated has its "original" orientation set every frame by a different controller. -/// The rotation is then applied on top of that orientation. -/// @note Must be set on a MatrixTransform. -class RotateController : public osg::NodeCallback +namespace MWRender { -public: - RotateController(osg::Node* relativeTo); - void setEnabled(bool enabled); + /// Applies a rotation in \a relativeTo's space. + /// @note Assumes that the node being rotated has its "original" orientation set every frame by a different + /// controller. The rotation is then applied on top of that orientation. + class RotateController : public SceneUtil::NodeCallback + { + public: + RotateController(osg::Node* relativeTo); + + void setEnabled(bool enabled); + void setOffset(const osg::Vec3f& offset); + void setRotate(const osg::Quat& rotate); - void setRotate(const osg::Quat& rotate); + const osg::Vec3f& getOffset() const { return mOffset; } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override; + const osg::Quat& getRotate() const { return mRotate; } -protected: - osg::Quat getWorldOrientation(osg::Node* node); + osg::Node* getRelativeTo() const { return mRelativeTo; } - bool mEnabled; - osg::Quat mRotate; - osg::Node* mRelativeTo; -}; + void operator()(osg::MatrixTransform* node, osg::NodeVisitor* nv); + protected: + bool mEnabled; + osg::Vec3f mOffset; + osg::Quat mRotate; + osg::Node* mRelativeTo; + }; } diff --git a/apps/openmw/mwrender/screenshotmanager.cpp b/apps/openmw/mwrender/screenshotmanager.cpp index f474a5a9f66..2c86a70f238 100644 --- a/apps/openmw/mwrender/screenshotmanager.cpp +++ b/apps/openmw/mwrender/screenshotmanager.cpp @@ -3,45 +3,27 @@ #include #include -#include -#include -#include -#include +#include +#include -#include -#include -#include -#include - -#include - -#include "../mwgui/loadingscreen.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "util.hpp" -#include "vismask.hpp" -#include "water.hpp" +#include "postprocessor.hpp" namespace MWRender { - enum Screenshot360Type - { - Spherical, - Cylindrical, - Planet, - RawCubemap - }; class NotifyDrawCompletedCallback : public osg::Camera::DrawCallback { public: NotifyDrawCompletedCallback() - : mDone(false), mFrame(0) + : mDone(false) + , mFrame(0) { } - void operator () (osg::RenderInfo& renderInfo) const override + void operator()(osg::RenderInfo& renderInfo) const override { std::lock_guard lock(mMutex); if (renderInfo.getState()->getFrameStamp()->getFrameNumber() >= mFrame && !mDone) @@ -76,261 +58,66 @@ namespace MWRender { public: ReadImageFromFramebufferCallback(osg::Image* image, int width, int height) - : mWidth(width), mHeight(height), mImage(image) + : mWidth(width) + , mHeight(height) + , mImage(image) { } - void drawImplementation(osg::RenderInfo& renderInfo,const osg::Drawable* /*drawable*/) const override + void drawImplementation(osg::RenderInfo& renderInfo, const osg::Drawable* /*drawable*/) const override { int screenW = renderInfo.getCurrentCamera()->getViewport()->width(); int screenH = renderInfo.getCurrentCamera()->getViewport()->height(); - double imageaspect = (double)mWidth/(double)mHeight; + if (Stereo::getStereo()) + { + auto eyeRes = Stereo::Manager::instance().eyeResolution(); + screenW = eyeRes.x(); + screenH = eyeRes.y(); + } + double imageaspect = (double)mWidth / (double)mHeight; int leftPadding = std::max(0, static_cast(screenW - screenH * imageaspect) / 2); int topPadding = std::max(0, static_cast(screenH - screenW / imageaspect) / 2); - int width = screenW - leftPadding*2; - int height = screenH - topPadding*2; + int width = screenW - leftPadding * 2; + int height = screenH - topPadding * 2; + mImage->readPixels(leftPadding, topPadding, width, height, GL_RGB, GL_UNSIGNED_BYTE); mImage->scaleImage(mWidth, mHeight, 1); } + private: int mWidth; int mHeight; osg::ref_ptr mImage; }; - ScreenshotManager::ScreenshotManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, osg::ref_ptr sceneRoot, Resource::ResourceSystem* resourceSystem, Water* water) + ScreenshotManager::ScreenshotManager(osgViewer::Viewer* viewer) : mViewer(viewer) - , mRootNode(rootNode) - , mSceneRoot(sceneRoot) , mDrawCompleteCallback(new NotifyDrawCompletedCallback) - , mResourceSystem(resourceSystem) - , mWater(water) { } - ScreenshotManager::~ScreenshotManager() - { - } + ScreenshotManager::~ScreenshotManager() {} void ScreenshotManager::screenshot(osg::Image* image, int w, int h) { - osg::Camera* camera = mViewer->getCamera(); + osg::Camera* camera = MWBase::Environment::get().getWorld()->getPostProcessor()->getHUDCamera(); osg::ref_ptr tempDrw = new osg::Drawable; tempDrw->setDrawCallback(new ReadImageFromFramebufferCallback(image, w, h)); tempDrw->setCullingActive(false); - tempDrw->getOrCreateStateSet()->setRenderBinDetails(100, "RenderBin", osg::StateSet::USE_RENDERBIN_DETAILS); // so its after all scene bins but before POST_RENDER gui camera + tempDrw->getOrCreateStateSet()->setRenderBinDetails(100, "RenderBin", + osg::StateSet::USE_RENDERBIN_DETAILS); // so its after all scene bins but before POST_RENDER gui camera camera->addChild(tempDrw); - traversalsAndWait(mViewer->getFrameStamp()->getFrameNumber()); - // now that we've "used up" the current frame, get a fresh frame number for the next frame() following after the screenshot is completed - mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); - camera->removeChild(tempDrw); - } - - bool ScreenshotManager::screenshot360(osg::Image* image) - { - int screenshotW = mViewer->getCamera()->getViewport()->width(); - int screenshotH = mViewer->getCamera()->getViewport()->height(); - Screenshot360Type screenshotMapping = Spherical; - - const std::string& settingStr = Settings::Manager::getString("screenshot type", "Video"); - std::vector settingArgs; - Misc::StringUtils::split(settingStr, settingArgs); - - if (settingArgs.size() > 0) - { - std::string typeStrings[4] = {"spherical", "cylindrical", "planet", "cubemap"}; - bool found = false; - - for (int i = 0; i < 4; ++i) - { - if (settingArgs[0].compare(typeStrings[i]) == 0) - { - screenshotMapping = static_cast(i); - found = true; - break; - } - } - - if (!found) - { - Log(Debug::Warning) << "Wrong screenshot type: " << settingArgs[0] << "."; - return false; - } - } - - // planet mapping needs higher resolution - int cubeSize = screenshotMapping == Planet ? screenshotW : screenshotW / 2; - - if (settingArgs.size() > 1) - screenshotW = std::min(10000, std::atoi(settingArgs[1].c_str())); - - if (settingArgs.size() > 2) - screenshotH = std::min(10000, std::atoi(settingArgs[2].c_str())); - - if (settingArgs.size() > 3) - cubeSize = std::min(5000, std::atoi(settingArgs[3].c_str())); - - bool rawCubemap = screenshotMapping == RawCubemap; - - if (rawCubemap) - screenshotW = cubeSize * 6; // the image will consist of 6 cube sides in a row - else if (screenshotMapping == Planet) - screenshotH = screenshotW; // use square resolution for planet mapping - - std::vector> images; - - for (int i = 0; i < 6; ++i) - images.push_back(new osg::Image); - - osg::Vec3 directions[6] = { - rawCubemap ? osg::Vec3(1,0,0) : osg::Vec3(0,0,1), - osg::Vec3(0,0,-1), - osg::Vec3(-1,0,0), - rawCubemap ? osg::Vec3(0,0,1) : osg::Vec3(1,0,0), - osg::Vec3(0,1,0), - osg::Vec3(0,-1,0)}; - - double rotations[] = { - -osg::PI / 2.0, - osg::PI / 2.0, - osg::PI, - 0, - osg::PI / 2.0, - osg::PI / 2.0 }; - - for (int i = 0; i < 6; ++i) // for each cubemap side - { - osg::Matrixd transform = osg::Matrixd::rotate(osg::Vec3(0,0,-1), directions[i]); - - if (!rawCubemap) - transform *= osg::Matrixd::rotate(rotations[i],osg::Vec3(0,0,-1)); - - osg::Image *sideImage = images[i].get(); - makeCubemapScreenshot(sideImage, cubeSize, cubeSize, transform); - - if (!rawCubemap) - sideImage->flipHorizontal(); - } - if (rawCubemap) // for raw cubemap don't run on GPU, just merge the images - { - image->allocateImage(cubeSize * 6,cubeSize,images[0]->r(),images[0]->getPixelFormat(),images[0]->getDataType()); - - for (int i = 0; i < 6; ++i) - osg::copyImage(images[i].get(),0,0,0,images[i]->s(),images[i]->t(),images[i]->r(),image,i * cubeSize,0,0); - - return true; - } - - // run on GPU now: - osg::ref_ptr cubeTexture (new osg::TextureCubeMap); - cubeTexture->setResizeNonPowerOfTwoHint(false); - - cubeTexture->setFilter(osg::Texture::MIN_FILTER,osg::Texture::NEAREST); - cubeTexture->setFilter(osg::Texture::MAG_FILTER,osg::Texture::NEAREST); - - cubeTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - cubeTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - for (int i = 0; i < 6; ++i) - cubeTexture->setImage(i, images[i].get()); - - osg::ref_ptr screenshotCamera(new osg::Camera); - osg::ref_ptr quad(new osg::ShapeDrawable(new osg::Box(osg::Vec3(0,0,0), 2.0))); - - std::map defineMap; - - Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); - osg::ref_ptr fragmentShader(shaderMgr.getShader("s360_fragment.glsl", defineMap,osg::Shader::FRAGMENT)); - osg::ref_ptr vertexShader(shaderMgr.getShader("s360_vertex.glsl", defineMap, osg::Shader::VERTEX)); - osg::ref_ptr stateset = new osg::StateSet; - - osg::ref_ptr program(new osg::Program); - program->addShader(fragmentShader); - program->addShader(vertexShader); - stateset->setAttributeAndModes(program, osg::StateAttribute::ON); - - stateset->addUniform(new osg::Uniform("cubeMap", 0)); - stateset->addUniform(new osg::Uniform("mapping", screenshotMapping)); - stateset->setTextureAttributeAndModes(0, cubeTexture, osg::StateAttribute::ON); - - quad->setStateSet(stateset); - quad->setUpdateCallback(nullptr); - - screenshotCamera->addChild(quad); - - renderCameraToImage(screenshotCamera, image, screenshotW, screenshotH); - - return true; - } - - void ScreenshotManager::traversalsAndWait(unsigned int frame) - { // Ref https://gitlab.com/OpenMW/openmw/-/issues/6013 - mDrawCompleteCallback->reset(frame); + mDrawCompleteCallback->reset(mViewer->getFrameStamp()->getFrameNumber()); mViewer->getCamera()->setFinalDrawCallback(mDrawCompleteCallback); - mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); mDrawCompleteCallback->waitTillDone(); - } - - void ScreenshotManager::renderCameraToImage(osg::Camera *camera, osg::Image *image, int w, int h) - { - camera->setNodeMask(Mask_RenderToTexture); - camera->attach(osg::Camera::COLOR_BUFFER, image); - camera->setRenderOrder(osg::Camera::PRE_RENDER); - camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); - camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT,osg::Camera::PIXEL_BUFFER_RTT); - - camera->setViewport(0, 0, w, h); - - osg::ref_ptr texture (new osg::Texture2D); - texture->setInternalFormat(GL_RGB); - texture->setTextureSize(w,h); - texture->setResizeNonPowerOfTwoHint(false); - texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - camera->attach(osg::Camera::COLOR_BUFFER,texture); - - image->setDataType(GL_UNSIGNED_BYTE); - image->setPixelFormat(texture->getInternalFormat()); - - mRootNode->addChild(camera); - - MWBase::Environment::get().getWindowManager()->getLoadingScreen()->loadingOn(false); - - // The draw needs to complete before we can copy back our image. - traversalsAndWait(0); - - MWBase::Environment::get().getWindowManager()->getLoadingScreen()->loadingOff(); - // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the screenshot is completed + // now that we've "used up" the current frame, get a fresh frame number for the next frame() following after the + // screenshot is completed mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); - - camera->removeChildren(0, camera->getNumChildren()); - mRootNode->removeChild(camera); - } - - void ScreenshotManager::makeCubemapScreenshot(osg::Image *image, int w, int h, osg::Matrixd cameraTransform) - { - osg::ref_ptr rttCamera (new osg::Camera); - float nearClip = Settings::Manager::getFloat("near clip", "Camera"); - float viewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); - // each cubemap side sees 90 degrees - rttCamera->setProjectionMatrixAsPerspective(90.0, w/float(h), nearClip, viewDistance); - rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix() * cameraTransform); - - rttCamera->setUpdateCallback(new NoTraverseCallback); - rttCamera->addChild(mSceneRoot); - - rttCamera->addChild(mWater->getReflectionCamera()); - rttCamera->addChild(mWater->getRefractionCamera()); - - rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & (~Mask_GUI)); - - rttCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - renderCameraToImage(rttCamera.get(),image,w,h); + camera->removeChild(tempDrw); } } diff --git a/apps/openmw/mwrender/screenshotmanager.hpp b/apps/openmw/mwrender/screenshotmanager.hpp index 373fe3be84f..f41ccf00450 100644 --- a/apps/openmw/mwrender/screenshotmanager.hpp +++ b/apps/openmw/mwrender/screenshotmanager.hpp @@ -1,43 +1,25 @@ #ifndef MWRENDER_SCREENSHOTMANAGER_H #define MWRENDER_SCREENSHOTMANAGER_H -#include - -#include #include #include -namespace Resource -{ - class ResourceSystem; -} - namespace MWRender { - class Water; class NotifyDrawCompletedCallback; class ScreenshotManager { public: - ScreenshotManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, osg::ref_ptr sceneRoot, Resource::ResourceSystem* resourceSystem, Water* water); + ScreenshotManager(osgViewer::Viewer* viewer); ~ScreenshotManager(); void screenshot(osg::Image* image, int w, int h); - bool screenshot360(osg::Image* image); private: osg::ref_ptr mViewer; - osg::ref_ptr mRootNode; - osg::ref_ptr mSceneRoot; osg::ref_ptr mDrawCompleteCallback; - Resource::ResourceSystem* mResourceSystem; - Water* mWater; - - void traversalsAndWait(unsigned int frame); - void renderCameraToImage(osg::Camera *camera, osg::Image *image, int w, int h); - void makeCubemapScreenshot(osg::Image* image, int w, int h, osg::Matrixd cameraTransform=osg::Matrixd()); }; } diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 8bac90604bc..483de273178 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -1,1309 +1,174 @@ #include "sky.hpp" -#include - -#include -#include -#include #include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include #include #include -#include -#include -#include -#include - -#include #include +#include +#include -#include +#include -#include +#include +#include +#include +#include +#include -#include #include +#include #include -#include -#include -#include -#include -#include -#include - -#include +#include +#include #include +#include "../mwworld/datetimemanager.hpp" +#include "../mwworld/weather.hpp" + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" -#include "vismask.hpp" #include "renderbin.hpp" +#include "skyutil.hpp" +#include "util.hpp" +#include "vismask.hpp" namespace { - osg::ref_ptr createAlphaTrackingUnlitMaterial() - { - osg::ref_ptr mat = new osg::Material; - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); - mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::DIFFUSE); - return mat; - } - - osg::ref_ptr createUnlitMaterial() - { - osg::ref_ptr mat = new osg::Material; - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); - mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::OFF); - return mat; - } - - osg::ref_ptr createTexturedQuad(int numUvSets=1) - { - osg::ref_ptr geom = new osg::Geometry; - - osg::ref_ptr verts = new osg::Vec3Array; - verts->push_back(osg::Vec3f(-0.5, -0.5, 0)); - verts->push_back(osg::Vec3f(-0.5, 0.5, 0)); - verts->push_back(osg::Vec3f(0.5, 0.5, 0)); - verts->push_back(osg::Vec3f(0.5, -0.5, 0)); - - geom->setVertexArray(verts); - - osg::ref_ptr texcoords = new osg::Vec2Array; - texcoords->push_back(osg::Vec2f(0, 0)); - texcoords->push_back(osg::Vec2f(0, 1)); - texcoords->push_back(osg::Vec2f(1, 1)); - texcoords->push_back(osg::Vec2f(1, 0)); - - osg::ref_ptr colors = new osg::Vec4Array; - colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); - geom->setColorArray(colors, osg::Array::BIND_OVERALL); - - for (int i=0; isetTexCoordArray(i, texcoords, osg::Array::BIND_PER_VERTEX); - - geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); - - return geom; - } - -} - -namespace MWRender -{ - -class AtmosphereUpdater : public SceneUtil::StateSetUpdater -{ -public: - void setEmissionColor(const osg::Vec4f& emissionColor) - { - mEmissionColor = emissionColor; - } - -protected: - void setDefaults(osg::StateSet* stateset) override - { - stateset->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); - } - -private: - osg::Vec4f mEmissionColor; -}; - -class AtmosphereNightUpdater : public SceneUtil::StateSetUpdater -{ -public: - AtmosphereNightUpdater(Resource::ImageManager* imageManager) - { - // we just need a texture, its contents don't really matter - mTexture = new osg::Texture2D(imageManager->getWarningImage()); - } - - void setFade(const float fade) - { - mColor.a() = fade; - } - -protected: - void setDefaults(osg::StateSet* stateset) override - { - osg::ref_ptr texEnv (new osg::TexEnvCombine); - texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - - stateset->setTextureAttributeAndModes(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureAttributeAndModes(1, texEnv, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override - { - osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnv->setConstantColor(mColor); - } - - osg::ref_ptr mTexture; - - osg::Vec4f mColor; -}; - -class CloudUpdater : public SceneUtil::StateSetUpdater -{ -public: - CloudUpdater() - : mAnimationTimer(0.f) - , mOpacity(0.f) - { - } - - void setAnimationTimer(float timer) - { - mAnimationTimer = timer; - } - - void setTexture(osg::ref_ptr texture) - { - mTexture = texture; - } - void setEmissionColor(const osg::Vec4f& emissionColor) - { - mEmissionColor = emissionColor; - } - void setOpacity(float opacity) - { - mOpacity = opacity; - } - -protected: - void setDefaults(osg::StateSet *stateset) override - { - osg::ref_ptr texmat (new osg::TexMat); - stateset->setTextureAttributeAndModes(0, texmat, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(1, texmat, osg::StateAttribute::ON); - stateset->setAttribute(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already - osg::ref_ptr texEnvCombine (new osg::TexEnvCombine); - texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnvCombine->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnvCombine->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,1)); - texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); - - stateset->setTextureAttributeAndModes(1, texEnvCombine, osg::StateAttribute::ON); - - stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override - { - osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); - texMat->setMatrix(osg::Matrix::translate(osg::Vec3f(0, -mAnimationTimer, 0.f))); - - stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); - - osg::TexEnvCombine* texEnvCombine = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,mOpacity)); - } - -private: - float mAnimationTimer; - osg::ref_ptr mTexture; - osg::Vec4f mEmissionColor; - float mOpacity; -}; - -/// Transform that removes the eyepoint of the modelview matrix, -/// i.e. its children are positioned relative to the camera. -class CameraRelativeTransform : public osg::Transform -{ -public: - CameraRelativeTransform() - { - // Culling works in node-local space, not in camera space, so we can't cull this node correctly - // That's not a problem though, children of this node can be culled just fine - // Just make sure you do not place a CameraRelativeTransform deep in the scene graph - setCullingActive(false); - - addCullCallback(new CullCallback); - } - - CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) - : osg::Transform(copy, copyop) - { - } - - META_Node(MWRender, CameraRelativeTransform) - - const osg::Vec3f& getLastViewPoint() const - { - return mViewPoint; - } - - bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const override - { - if (nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) - { - mViewPoint = static_cast(nv)->getViewPoint(); - } - - if (_referenceFrame==RELATIVE_RF) - { - matrix.setTrans(osg::Vec3f(0.f,0.f,0.f)); - return false; - } - else // absolute - { - matrix.makeIdentity(); - return true; - } - } - - osg::BoundingSphere computeBound() const override - { - return osg::BoundingSphere(osg::Vec3f(0,0,0), 0); - } - - class CullCallback : public osg::NodeCallback - { - public: - void operator() (osg::Node* node, osg::NodeVisitor* nv) override - { - osgUtil::CullVisitor* cv = static_cast(nv); - - // XXX have to remove unwanted culling plane of the water reflection camera - - // Remove all planes that aren't from the standard frustum - unsigned int numPlanes = 4; - if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) - ++numPlanes; - if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) - ++numPlanes; - - unsigned int mask = 0x1; - unsigned int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); - for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) - { - if (i >= numPlanes) - { - // turn off this culling plane - resultMask &= (~mask); - } - - mask <<= 1; - } - - cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); - cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); - - cv->getProjectionCullingStack().back().pushCurrentMask(); - cv->getCurrentCullingSet().pushCurrentMask(); - - traverse(node, nv); - - cv->getProjectionCullingStack().back().popCurrentMask(); - cv->getCurrentCullingSet().popCurrentMask(); - } - }; -private: - // viewPoint for the current frame - mutable osg::Vec3f mViewPoint; -}; - -class ModVertexAlphaVisitor : public osg::NodeVisitor -{ -public: - ModVertexAlphaVisitor(int meshType) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mMeshType(meshType) - { - } - - void apply(osg::Drawable& drw) override - { - osg::Geometry* geom = drw.asGeometry(); - if (!geom) - return; - - osg::ref_ptr colors = new osg::Vec4Array(geom->getVertexArray()->getNumElements()); - for (unsigned int i=0; isize(); ++i) - { - float alpha = 1.f; - if (mMeshType == 0) alpha = (i%2) ? 0.f : 1.f; // this is a cylinder, so every second vertex belongs to the bottom-most row - else if (mMeshType == 1) - { - if (i>= 49 && i <= 64) alpha = 0.f; // bottom-most row - else if (i>= 33 && i <= 48) alpha = 0.25098; // second row - else alpha = 1.f; - } - else if (mMeshType == 2) - { - if (geom->getColorArray()) - { - osg::Vec4Array* origColors = static_cast(geom->getColorArray()); - alpha = ((*origColors)[i].x() == 1.f) ? 1.f : 0.f; - } - else - alpha = 1.f; - } - - (*colors)[i] = osg::Vec4f(0.f, 0.f, 0.f, alpha); - } - - geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); - } - -private: - int mMeshType; -}; - -/// @brief Hides the node subgraph if the eye point is below water. -/// @note Must be added as cull callback. -/// @note Meant to be used on a node that is child of a CameraRelativeTransform. -/// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we are in camera-relative space. -class UnderwaterSwitchCallback : public osg::NodeCallback -{ -public: - UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) - : mCameraRelativeTransform(cameraRelativeTransform) - , mEnabled(true) - , mWaterLevel(0.f) - { - } - - bool isUnderwater() - { - osg::Vec3f viewPoint = mCameraRelativeTransform->getLastViewPoint(); - return mEnabled && viewPoint.z() < mWaterLevel; - } - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override - { - if (isUnderwater()) - return; - - traverse(node, nv); - } - - void setEnabled(bool enabled) - { - mEnabled = enabled; - } - void setWaterLevel(float waterLevel) - { - mWaterLevel = waterLevel; - } - -private: - osg::ref_ptr mCameraRelativeTransform; - bool mEnabled; - float mWaterLevel; -}; - -/// A base class for the sun and moons. -class CelestialBody -{ -public: - CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0u) - : mVisibleMask(visibleMask) - { - mGeom = createTexturedQuad(numUvSets); - mTransform = new osg::PositionAttitudeTransform; - mTransform->setNodeMask(mVisibleMask); - mTransform->setScale(osg::Vec3f(450,450,450) * scaleFactor); - mTransform->addChild(mGeom); - - parentNode->addChild(mTransform); - } - - virtual ~CelestialBody() {} - - virtual void adjustTransparency(const float ratio) = 0; - - void setVisible(bool visible) - { - mTransform->setNodeMask(visible ? mVisibleMask : 0); - } - -protected: - unsigned int mVisibleMask; - static const float mDistance; - osg::ref_ptr mTransform; - osg::ref_ptr mGeom; -}; - -const float CelestialBody::mDistance = 1000.0f; - -class Sun : public CelestialBody -{ -public: - Sun(osg::Group* parentNode, Resource::ImageManager& imageManager) - : CelestialBody(parentNode, 1.0f, 1, Mask_Sun) - , mUpdater(new Updater) - { - mTransform->addUpdateCallback(mUpdater); - - osg::ref_ptr sunTex (new osg::Texture2D(imageManager.getImage("textures/tx_sun_05.dds"))); - sunTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - - osg::ref_ptr queryNode (new osg::Group); - // Need to render after the world geometry so we can correctly test for occlusions - osg::StateSet* stateset = queryNode->getOrCreateStateSet(); - stateset->setRenderBinDetails(RenderBin_OcclusionQuery, "RenderBin"); - stateset->setNestRenderBins(false); - // Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to match the circular shape of the sun - osg::ref_ptr alphaFunc (new osg::AlphaFunc); - alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); - stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); - // Disable writing to the color buffer. We are using this geometry for visibility tests only. - osg::ref_ptr colormask (new osg::ColorMask(0, 0, 0, 0)); - stateset->setAttributeAndModes(colormask, osg::StateAttribute::ON); - - mTransform->addChild(queryNode); - - mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); - mOcclusionQueryTotalPixels = createOcclusionQueryNode(queryNode, false); - - createSunFlash(imageManager); - createSunGlare(); - } - - ~Sun() - { - mTransform->removeUpdateCallback(mUpdater); - destroySunFlash(); - destroySunGlare(); - } - - void setColor(const osg::Vec4f& color) - { - mUpdater->mColor.r() = color.r(); - mUpdater->mColor.g() = color.g(); - mUpdater->mColor.b() = color.b(); - } - - void adjustTransparency(const float ratio) override - { - mUpdater->mColor.a() = ratio; - if (mSunGlareCallback) - mSunGlareCallback->setGlareView(ratio); - if (mSunFlashCallback) - mSunFlashCallback->setGlareView(ratio); - } - - void setDirection(const osg::Vec3f& direction) - { - osg::Vec3f normalizedDirection = direction / direction.length(); - mTransform->setPosition(normalizedDirection * mDistance); - - osg::Quat quat; - quat.makeRotate(osg::Vec3f(0.0f, 0.0f, 1.0f), normalizedDirection); - mTransform->setAttitude(quat); - } - - void setGlareTimeOfDayFade(float val) - { - if (mSunGlareCallback) - mSunGlareCallback->setTimeOfDayFade(val); - } - -private: - class DummyComputeBoundCallback : public osg::Node::ComputeBoundingSphereCallback + class WrapAroundOperator : public osgParticle::Operator { public: - osg::BoundingSphere computeBound(const osg::Node& node) const override { return osg::BoundingSphere(); } - }; - - /// @param queryVisible If true, queries the amount of visible pixels. If false, queries the total amount of pixels. - osg::ref_ptr createOcclusionQueryNode(osg::Group* parent, bool queryVisible) - { - osg::ref_ptr oqn = new osg::OcclusionQueryNode; - oqn->setQueriesEnabled(true); - -#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) - // With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced - osg::ref_ptr queryGeom = new osg::QueryGeometry(oqn->getName()); -#else - osg::ref_ptr queryGeom = oqn->getQueryGeometry(); -#endif - - // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry, - // so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry - // is only called once. - // Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. - queryGeom->setDataVariance(osg::Object::STATIC); - - // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this, - // normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to - // circumvent this. - queryGeom->setVertexArray(mGeom->getVertexArray()); - queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); - queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets()); - queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); - - // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. - oqn->setComputeBoundingSphereCallback(new DummyComputeBoundCallback); - // Still need a proper bounding sphere. - oqn->setInitialBound(queryGeom->getBound()); - -#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) - oqn->setQueryGeometry(queryGeom.release()); -#endif - - osg::StateSet* queryStateSet = new osg::StateSet; - if (queryVisible) - { - osg::ref_ptr depth (new osg::Depth); - depth->setFunction(osg::Depth::LEQUAL); - // This is a trick to make fragments written by the query always use the maximum depth value, - // without having to retrieve the current far clipping distance. - // We want the sun glare to be "infinitely" far away. - depth->setZNear(1.0); - depth->setZFar(1.0); - depth->setWriteMask(false); - queryStateSet->setAttributeAndModes(depth, osg::StateAttribute::ON); - } - else + WrapAroundOperator(osg::Camera* camera, const osg::Vec3& wrapRange) + : osgParticle::Operator() + , mCamera(camera) + , mWrapRange(wrapRange) + , mHalfWrapRange(mWrapRange / 2.0) { - queryStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + mPreviousCameraPosition = getCameraPosition(); } - oqn->setQueryStateSet(queryStateSet); - - parent->addChild(oqn); - - return oqn; - } - - void createSunFlash(Resource::ImageManager& imageManager) - { - osg::ref_ptr tex (new osg::Texture2D(imageManager.getImage("textures/tx_sun_flash_grey_05.dds"))); - tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - osg::ref_ptr transform (new osg::PositionAttitudeTransform); - const float scale = 2.6f; - transform->setScale(osg::Vec3f(scale,scale,scale)); + osg::Object* cloneType() const override { return nullptr; } - mTransform->addChild(transform); + osg::Object* clone(const osg::CopyOp& op) const override { return nullptr; } - osg::ref_ptr geom = createTexturedQuad(); - transform->addChild(geom); + void operate(osgParticle::Particle* P, double dt) override {} - osg::StateSet* stateset = geom->getOrCreateStateSet(); - - stateset->setTextureAttributeAndModes(0, tex, osg::StateAttribute::ON); - stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); - stateset->setNestRenderBins(false); - - mSunFlashNode = transform; - - mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); - mSunFlashNode->addCullCallback(mSunFlashCallback); - } - void destroySunFlash() - { - if (mSunFlashNode) + void operateParticles(osgParticle::ParticleSystem* ps, double dt) override { - mSunFlashNode->removeCullCallback(mSunFlashCallback); - mSunFlashCallback = nullptr; - } - } - - void createSunGlare() - { - osg::ref_ptr camera (new osg::Camera); - camera->setProjectionMatrix(osg::Matrix::identity()); - camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // add to skyRoot instead? - camera->setViewMatrix(osg::Matrix::identity()); - camera->setClearMask(0); - camera->setRenderOrder(osg::Camera::NESTED_RENDER); - camera->setAllowEventFocus(false); + osg::Vec3 position = getCameraPosition(); + osg::Vec3 positionDifference = position - mPreviousCameraPosition; - osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(2,0,0), osg::Vec3f(0,2,0)); + osg::Matrix toWorld, toLocal; - camera->addChild(geom); - - osg::StateSet* stateset = geom->getOrCreateStateSet(); - - stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); - stateset->setNestRenderBins(false); - stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + std::vector worldMatrices = ps->getWorldMatrices(); - // set up additive blending - osg::ref_ptr blendFunc (new osg::BlendFunc); - blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); - blendFunc->setDestination(osg::BlendFunc::ONE); - stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); - - mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); - mSunGlareNode = camera; - - mSunGlareNode->addCullCallback(mSunGlareCallback); - - mTransform->addChild(camera); - } - void destroySunGlare() - { - if (mSunGlareNode) - { - mSunGlareNode->removeCullCallback(mSunGlareCallback); - mSunGlareCallback = nullptr; - } - } - - class Updater : public SceneUtil::StateSetUpdater - { - public: - osg::Vec4f mColor; - - Updater() - : mColor(1.f, 1.f, 1.f, 1.f) - { - } - - void setDefaults(osg::StateSet* stateset) override - { - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor*) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mColor.a())); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(mColor.r(), mColor.g(), mColor.b(), 1)); - } - }; - - class OcclusionCallback : public osg::NodeCallback - { - public: - OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) - : mOcclusionQueryVisiblePixels(oqnVisible) - , mOcclusionQueryTotalPixels(oqnTotal) - { - } - - protected: - float getVisibleRatio (osg::Camera* camera) - { - int visible = mOcclusionQueryVisiblePixels->getQueryGeometry()->getNumPixels(camera); - int total = mOcclusionQueryTotalPixels->getQueryGeometry()->getNumPixels(camera); - - float visibleRatio = 0.f; - if (total > 0) - visibleRatio = static_cast(visible) / static_cast(total); - - float dt = MWBase::Environment::get().getFrameDuration(); - - float lastRatio = mLastRatio[osg::observer_ptr(camera)]; - - float change = dt*10; - - if (visibleRatio > lastRatio) - visibleRatio = std::min(visibleRatio, lastRatio + change); - else - visibleRatio = std::max(visibleRatio, lastRatio - change); - - mLastRatio[osg::observer_ptr(camera)] = visibleRatio; - - return visibleRatio; - } - - private: - osg::ref_ptr mOcclusionQueryVisiblePixels; - osg::ref_ptr mOcclusionQueryTotalPixels; - - std::map, float> mLastRatio; - }; - - /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a cull callback. - class SunFlashCallback : public OcclusionCallback - { - public: - SunFlashCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) - : OcclusionCallback(oqnVisible, oqnTotal) - , mGlareView(1.f) - { - } - - void operator()(osg::Node* node, osg::NodeVisitor* nv) override - { - osgUtil::CullVisitor* cv = static_cast(nv); - - float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); - - osg::ref_ptr stateset; - - if (visibleRatio > 0.f) + if (!worldMatrices.empty()) { - const float fadeThreshold = 0.1; - if (visibleRatio < fadeThreshold) - { - float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; - osg::ref_ptr mat (createUnlitMaterial()); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade*mGlareView)); - stateset = new osg::StateSet; - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - const float threshold = 0.6; - visibleRatio = visibleRatio * (1.f - threshold) + threshold; + toWorld = worldMatrices[0]; + toLocal.invert(toWorld); } - float scale = visibleRatio; - - if (scale == 0.f) + for (int i = 0; i < ps->numParticles(); ++i) { - // no traverse - return; - } - else - { - osg::Matrix modelView = *cv->getModelViewMatrix(); - - modelView.preMultScale(osg::Vec3f(visibleRatio, visibleRatio, visibleRatio)); + osgParticle::Particle* p = ps->getParticle(i); + p->setPosition(toWorld.preMult(p->getPosition())); + p->setPosition(p->getPosition() - positionDifference); - if (stateset) - cv->pushStateSet(stateset); - - cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); + for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions + { + osg::Vec3 pos = p->getPosition(); - traverse(node, nv); + if (pos[j] < -mHalfWrapRange[j]) + pos[j] = mHalfWrapRange[j] + fmod(pos[j] - mHalfWrapRange[j], mWrapRange[j]); + else if (pos[j] > mHalfWrapRange[j]) + pos[j] = fmod(pos[j] + mHalfWrapRange[j], mWrapRange[j]) - mHalfWrapRange[j]; - cv->popModelViewMatrix(); + p->setPosition(pos); + } - if (stateset) - cv->popStateSet(); + p->setPosition(toLocal.preMult(p->getPosition())); } - } - void setGlareView(float value) - { - mGlareView = value; + mPreviousCameraPosition = position; } - private: - float mGlareView; - }; + protected: + osg::Camera* mCamera; + osg::Vec3 mPreviousCameraPosition; + osg::Vec3 mWrapRange; + osg::Vec3 mHalfWrapRange; + osg::Vec3 getCameraPosition() { return mCamera->getInverseViewMatrix().getTrans(); } + }; - /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between sun and camera. - /// Must be attached as a cull callback to the node above the glare node. - class SunGlareCallback : public OcclusionCallback + class WeatherAlphaOperator : public osgParticle::Operator { public: - SunGlareCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal, - osg::ref_ptr sunTransform) - : OcclusionCallback(oqnVisible, oqnTotal) - , mSunTransform(sunTransform) - , mTimeOfDayFade(1.f) - , mGlareView(1.f) + WeatherAlphaOperator(float& alpha, bool rain) + : mAlpha(alpha) + , mIsRain(rain) { - mColor = Fallback::Map::getColour("Weather_Sun_Glare_Fader_Color"); - mSunGlareFaderMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Max"); - mSunGlareFaderAngleMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Angle_Max"); - - // Replicating a design flaw in MW. The color was being set on both ambient and emissive properties, which multiplies the result by two, - // then finally gets clamped by the fixed function pipeline. With the default INI settings, only the red component gets clamped, - // so the resulting color looks more orange than red. - mColor *= 2; - for (int i=0; i<3; ++i) - mColor[i] = std::min(1.f, mColor[i]); } - void operator ()(osg::Node* node, osg::NodeVisitor* nv) override - { - osgUtil::CullVisitor* cv = static_cast(nv); - - float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); - float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); - - const float angleMaxRadians = osg::DegreesToRadians(mSunGlareFaderAngleMax); - - float value = 1.f - std::min(1.f, angleRadians / angleMaxRadians); - float fade = value * mSunGlareFaderMax; - - fade *= mTimeOfDayFade * mGlareView * visibleRatio; - - if (fade == 0.f) - { - // no traverse - return; - } - else - { - osg::ref_ptr stateset (new osg::StateSet); - - osg::ref_ptr mat (createUnlitMaterial()); - - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); - - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); - - cv->pushStateSet(stateset); - traverse(node, nv); - cv->popStateSet(); - } - } + osg::Object* cloneType() const override { return nullptr; } - void setTimeOfDayFade(float val) - { - mTimeOfDayFade = val; - } + osg::Object* clone(const osg::CopyOp& op) const override { return nullptr; } - void setGlareView(float glareView) + void operate(osgParticle::Particle* particle, double dt) override { - mGlareView = glareView; + constexpr float rainThreshold = 0.6f; // Rain_Threshold? + float alpha = mIsRain ? mAlpha * rainThreshold : mAlpha; + particle->setAlphaRange(osgParticle::rangef(alpha, alpha)); } private: - float getAngleToSunInRadians(const osg::Matrix& viewMatrix) const - { - osg::Vec3d eye, center, up; - viewMatrix.getLookAt(eye, center, up); - - osg::Vec3d forward = center - eye; - osg::Vec3d sun = mSunTransform->getPosition(); - - forward.normalize(); - sun.normalize(); - float angleRadians = std::acos(forward * sun); - return angleRadians; - } - - osg::ref_ptr mSunTransform; - float mTimeOfDayFade; - float mGlareView; - osg::Vec4f mColor; - float mSunGlareFaderMax; - float mSunGlareFaderAngleMax; - }; - - osg::ref_ptr mUpdater; - osg::ref_ptr mSunFlashCallback; - osg::ref_ptr mSunFlashNode; - osg::ref_ptr mSunGlareCallback; - osg::ref_ptr mSunGlareNode; - osg::ref_ptr mOcclusionQueryVisiblePixels; - osg::ref_ptr mOcclusionQueryTotalPixels; -}; - -class Moon : public CelestialBody -{ -public: - enum Type - { - Type_Masser = 0, - Type_Secunda + float& mAlpha; + bool mIsRain; }; - Moon(osg::Group* parentNode, Resource::ImageManager& imageManager, float scaleFactor, Type type) - : CelestialBody(parentNode, scaleFactor, 2) - , mType(type) - , mPhase(MoonState::Phase::Unspecified) - , mUpdater(new Updater(imageManager)) + // Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. + class AlphaFader : public SceneUtil::StateSetUpdater { - setPhase(MoonState::Phase::Full); - setVisible(true); - - mGeom->addUpdateCallback(mUpdater); - } - - ~Moon() - { - mGeom->removeUpdateCallback(mUpdater); - } - - void adjustTransparency(const float ratio) override - { - mUpdater->mTransparency *= ratio; - } - - void setState(const MoonState& state) - { - float radsX = ((state.mRotationFromHorizon) * static_cast(osg::PI)) / 180.0f; - float radsZ = ((state.mRotationFromNorth) * static_cast(osg::PI)) / 180.0f; - - osg::Quat rotX(radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); - osg::Quat rotZ(radsZ, osg::Vec3f(0.0f, 0.0f, 1.0f)); - - osg::Vec3f direction = rotX * rotZ * osg::Vec3f(0.0f, 1.0f, 0.0f); - mTransform->setPosition(direction * mDistance); - - // The moon quad is initially oriented facing down, so we need to offset its X-axis - // rotation to rotate it to face the camera when sitting at the horizon. - osg::Quat attX((-static_cast(osg::PI) / 2.0f) + radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); - mTransform->setAttitude(attX * rotZ); - - setPhase(state.mPhase); - mUpdater->mTransparency = state.mMoonAlpha; - mUpdater->mShadowBlend = state.mShadowBlend; - } - - void setAtmosphereColor(const osg::Vec4f& color) - { - mUpdater->mAtmosphereColor = color; - } - - void setColor(const osg::Vec4f& color) - { - mUpdater->mMoonColor = color; - } - - unsigned int getPhaseInt() const - { - if (mPhase == MoonState::Phase::New) return 0; - else if (mPhase == MoonState::Phase::WaxingCrescent) return 1; - else if (mPhase == MoonState::Phase::WaningCrescent) return 1; - else if (mPhase == MoonState::Phase::FirstQuarter) return 2; - else if (mPhase == MoonState::Phase::ThirdQuarter) return 2; - else if (mPhase == MoonState::Phase::WaxingGibbous) return 3; - else if (mPhase == MoonState::Phase::WaningGibbous) return 3; - else if (mPhase == MoonState::Phase::Full) return 4; - return 0; - } - -private: - struct Updater : public SceneUtil::StateSetUpdater - { - Resource::ImageManager& mImageManager; - osg::ref_ptr mPhaseTex; - osg::ref_ptr mCircleTex; - float mTransparency; - float mShadowBlend; - osg::Vec4f mAtmosphereColor; - osg::Vec4f mMoonColor; - - Updater(Resource::ImageManager& imageManager) - : mImageManager(imageManager) - , mPhaseTex() - , mCircleTex() - , mTransparency(1.0f) - , mShadowBlend(1.0f) - , mAtmosphereColor(1.0f, 1.0f, 1.0f, 1.0f) - , mMoonColor(1.0f, 1.0f, 1.0f, 1.0f) - { - } - - void setDefaults(osg::StateSet* stateset) override - { - stateset->setTextureAttributeAndModes(0, mPhaseTex, osg::StateAttribute::ON); - osg::ref_ptr texEnv = new osg::TexEnvCombine; - texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); - texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); - texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); - texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor - stateset->setTextureAttributeAndModes(0, texEnv, osg::StateAttribute::ON); - - stateset->setTextureAttributeAndModes(1, mCircleTex, osg::StateAttribute::ON); - osg::ref_ptr texEnv2 = new osg::TexEnvCombine; - texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); - texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv2->setSource0_Alpha(osg::TexEnvCombine::TEXTURE); - texEnv2->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); - texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency - stateset->setTextureAttributeAndModes(1, texEnv2, osg::StateAttribute::ON); - - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor*) override - { - osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXENV)); - texEnv->setConstantColor(mMoonColor * mShadowBlend); - - osg::TexEnvCombine* texEnv2 = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnv2->setConstantColor(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); - } - - void setTextures(const std::string& phaseTex, const std::string& circleTex) + public: + /// @param alpha the variable alpha value is recovered from + AlphaFader(const float& alpha) + : mAlpha(alpha) { - mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); - mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mPhaseTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mCircleTex = new osg::Texture2D(mImageManager.getImage(circleTex)); - mCircleTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mCircleTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - reset(); } - }; - - Type mType; - MoonState::Phase mPhase; - osg::ref_ptr mUpdater; - - void setPhase(const MoonState::Phase& phase) - { - if(mPhase == phase) - return; - - mPhase = phase; - - std::string textureName = "textures/tx_"; - - if (mType == Moon::Type_Secunda) - textureName += "secunda_"; - else - textureName += "masser_"; - - if (phase == MoonState::Phase::New) textureName += "new"; - else if(phase == MoonState::Phase::WaxingCrescent) textureName += "one_wax"; - else if(phase == MoonState::Phase::FirstQuarter) textureName += "half_wax"; - else if(phase == MoonState::Phase::WaxingGibbous) textureName += "three_wax"; - else if(phase == MoonState::Phase::WaningCrescent) textureName += "one_wan"; - else if(phase == MoonState::Phase::ThirdQuarter) textureName += "half_wan"; - else if(phase == MoonState::Phase::WaningGibbous) textureName += "three_wan"; - else if(phase == MoonState::Phase::Full) textureName += "full"; - - textureName += ".dds"; - - if (mType == Moon::Type_Secunda) - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_s.dds"); - else - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_m.dds"); - } -}; - -SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager) - : mSceneManager(sceneManager) - , mCamera(nullptr) - , mAtmosphereNightRoll(0.f) - , mCreated(false) - , mIsStorm(false) - , mDay(0) - , mMonth(0) - , mCloudAnimationTimer(0.f) - , mRainTimer(0.f) - , mStormDirection(0,1,0) - , mClouds() - , mNextClouds() - , mCloudBlendFactor(0.0f) - , mCloudSpeed(0.0f) - , mStarsOpacity(0.0f) - , mRemainingTransitionTime(0.0f) - , mRainEnabled(false) - , mRainSpeed(0) - , mRainDiameter(0) - , mRainMinHeight(0) - , mRainMaxHeight(0) - , mRainEntranceSpeed(1) - , mRainMaxRaindrops(0) - , mWindSpeed(0.f) - , mBaseWindSpeed(0.f) - , mEnabled(true) - , mSunEnabled(true) - , mPrecipitationAlpha(0.f) -{ - osg::ref_ptr skyroot (new CameraRelativeTransform); - skyroot->setName("Sky Root"); - // Assign empty program to specify we don't want shaders - // The shaders generated by the SceneManager can't handle everything we need - skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE|osg::StateAttribute::PROTECTED|osg::StateAttribute::ON); - SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); - - skyroot->setNodeMask(Mask_Sky); - parentNode->addChild(skyroot); - - mRootNode = skyroot; - - mEarlyRenderBinRoot = new osg::Group; - // render before the world is rendered - mEarlyRenderBinRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); - // Prevent unwanted clipping by water reflection camera's clipping plane - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); - mRootNode->addChild(mEarlyRenderBinRoot); - - mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); -} - -void SkyManager::create() -{ - assert(!mCreated); - - mAtmosphereDay = mSceneManager->getInstance(Settings::Manager::getString("skyatmosphere", "Models"), mEarlyRenderBinRoot); - ModVertexAlphaVisitor modAtmosphere(0); - mAtmosphereDay->accept(modAtmosphere); - - mAtmosphereUpdater = new AtmosphereUpdater; - mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); - - mAtmosphereNightNode = new osg::PositionAttitudeTransform; - mAtmosphereNightNode->setNodeMask(0); - mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); - - osg::ref_ptr atmosphereNight; - if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) - atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight02", "Models"), mAtmosphereNightNode); - else - atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight01", "Models"), mAtmosphereNightNode); - atmosphereNight->getOrCreateStateSet()->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - ModVertexAlphaVisitor modStars(2); - atmosphereNight->accept(modStars); - mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager()); - atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); - - mSun.reset(new Sun(mEarlyRenderBinRoot, *mSceneManager->getImageManager())); - - mMasser.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Masser_Size")/125, Moon::Type_Masser)); - mSecunda.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Secunda_Size")/125, Moon::Type_Secunda)); - - mCloudNode = new osg::PositionAttitudeTransform; - mEarlyRenderBinRoot->addChild(mCloudNode); - mCloudMesh = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudNode); - ModVertexAlphaVisitor modClouds(1); - mCloudMesh->accept(modClouds); - mCloudUpdater = new CloudUpdater; - mCloudUpdater->setOpacity(1.f); - mCloudMesh->addUpdateCallback(mCloudUpdater); - - mCloudMesh2 = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudNode); - mCloudMesh2->accept(modClouds); - mCloudUpdater2 = new CloudUpdater; - mCloudUpdater2->setOpacity(0.f); - mCloudMesh2->addUpdateCallback(mCloudUpdater2); - mCloudMesh2->setNodeMask(0); - - osg::ref_ptr depth = new osg::Depth; - depth->setWriteMask(false); - mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON); - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); - - mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); - - mCreated = true; -} - -class RainCounter : public osgParticle::ConstantRateCounter -{ -public: - int numParticlesToCreate(double dt) const override - { - // limit dt to avoid large particle emissions if there are jumps in the simulation time - // 0.2 seconds is the same cap as used in Engine's frame loop - dt = std::min(dt, 0.2); - return ConstantRateCounter::numParticlesToCreate(dt); - } -}; - -class RainShooter : public osgParticle::Shooter -{ -public: - RainShooter() - : mAngle(0.f) - { - } - - void shoot(osgParticle::Particle* particle) const override - { - particle->setVelocity(mVelocity); - particle->setAngle(osg::Vec3f(-mAngle, 0, (Misc::Rng::rollProbability() * 2 - 1) * osg::PI)); - } - - void setVelocity(const osg::Vec3f& velocity) - { - mVelocity = velocity; - } - - void setAngle(float angle) - { - mAngle = angle; - } - - osg::Object* cloneType() const override - { - return new RainShooter; - } - osg::Object* clone(const osg::CopyOp &) const override - { - return new RainShooter(*this); - } - -private: - osg::Vec3f mVelocity; - float mAngle; -}; - -// Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. -class AlphaFader : public SceneUtil::StateSetUpdater -{ -public: - /// @param alpha the variable alpha value is recovered from - AlphaFader(float& alpha) - : mAlpha(alpha) - { - } - void setDefaults(osg::StateSet* stateset) override - { - // need to create a deep copy of StateAttributes we will modify - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); - } + void setDefaults(osg::StateSet* stateset) override + { + // need to create a deep copy of StateAttributes we will modify + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); + } - void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mAlpha)); - } + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, mAlpha)); + } + + protected: + const float& mAlpha; + }; // Helper for adding AlphaFaders to a subgraph class SetupVisitor : public osg::NodeVisitor { public: - SetupVisitor(float &alpha) + SetupVisitor(const float& alpha) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mAlpha(alpha) { } - void apply(osg::Node &node) override + void apply(osg::Node& node) override { if (osg::StateSet* stateset = node.getStateSet()) { @@ -1321,7 +186,7 @@ class AlphaFader : public SceneUtil::StateSetUpdater callback = callback->getNestedCallback(); } - osg::ref_ptr alphaFader (new AlphaFader(mAlpha)); + osg::ref_ptr alphaFader = new AlphaFader(mAlpha); if (composite) composite->addController(alphaFader); @@ -1334,613 +199,787 @@ class AlphaFader : public SceneUtil::StateSetUpdater } private: - float &mAlpha; + const float& mAlpha; }; -protected: - float &mAlpha; -}; + class SkyRTT : public SceneUtil::RTTNode + { + public: + SkyRTT(osg::Vec2f size, osg::Group* earlyRenderBinRoot) + : RTTNode(static_cast(size.x()), static_cast(size.y()), 0, false, 1, StereoAwareness::Aware, + MWRender::shouldAddMSAAIntermediateTarget()) + , mEarlyRenderBinRoot(earlyRenderBinRoot) + { + setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); + } + + void setDefaults(osg::Camera* camera) override + { + camera->setReferenceFrame(osg::Camera::RELATIVE_RF); + camera->setName("SkyCamera"); + camera->setNodeMask(MWRender::Mask_RenderToTexture); + camera->setCullMask(MWRender::Mask_Sky); + camera->addChild(mEarlyRenderBinRoot); + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*camera->getOrCreateStateSet()); + } + + private: + osg::ref_ptr mEarlyRenderBinRoot; + }; -void SkyManager::setCamera(osg::Camera *camera) -{ - mCamera = camera; } -class WrapAroundOperator : public osgParticle::Operator +namespace MWRender { -public: - WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange): osgParticle::Operator() - { - mCamera = camera; - mWrapRange = wrapRange; - mHalfWrapRange = mWrapRange / 2.0; - mPreviousCameraPosition = getCameraPosition(); - } + SkyManager::SkyManager(osg::Group* parentNode, osg::Group* rootNode, osg::Camera* camera, + Resource::SceneManager* sceneManager, bool enableSkyRTT) + : mSceneManager(sceneManager) + , mCamera(camera) + , mAtmosphereNightRoll(0.f) + , mCreated(false) + , mIsStorm(false) + , mDay(0) + , mMonth(0) + , mTimescaleClouds(Fallback::Map::getBool("Weather_Timescale_Clouds")) + , mCloudAnimationTimer(0.f) + , mRainTimer(0.f) + , mStormParticleDirection(MWWorld::Weather::defaultDirection()) + , mStormDirection(MWWorld::Weather::defaultDirection()) + , mClouds() + , mNextClouds() + , mCloudBlendFactor(0.f) + , mCloudSpeed(0.f) + , mStarsOpacity(0.f) + , mRemainingTransitionTime(0.f) + , mRainEnabled(false) + , mRainSpeed(0.f) + , mRainDiameter(0.f) + , mRainMinHeight(0.f) + , mRainMaxHeight(0.f) + , mRainEntranceSpeed(1.f) + , mRainMaxRaindrops(0) + , mRainRipplesEnabled(Fallback::Map::getBool("Weather_Rain_Ripples")) + , mSnowRipplesEnabled(Fallback::Map::getBool("Weather_Snow_Ripples")) + , mWindSpeed(0.f) + , mBaseWindSpeed(0.f) + , mEnabled(true) + , mSunEnabled(true) + , mSunglareEnabled(true) + , mPrecipitationAlpha(0.f) + , mDirtyParticlesEffect(false) + { + osg::ref_ptr skyroot = new CameraRelativeTransform; + skyroot->setName("Sky Root"); + // Assign empty program to specify we don't want shaders when we are rendering in FFP pipeline + if (!mSceneManager->getForceShaders()) + skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), + osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED | osg::StateAttribute::ON); + mSceneManager->setUpNormalsRTForStateSet(skyroot->getOrCreateStateSet(), false); + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*skyroot->getOrCreateStateSet()); + parentNode->addChild(skyroot); + + mEarlyRenderBinRoot = new osg::Group; + // render before the world is rendered + mEarlyRenderBinRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); + // Prevent unwanted clipping by water reflection camera's clipping plane + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); + + if (enableSkyRTT) + { + mSkyRTT = new SkyRTT(Settings::fog().mSkyRttResolution, mEarlyRenderBinRoot); + skyroot->addChild(mSkyRTT); + mRootNode = new osg::Group; + skyroot->addChild(mRootNode); + } + else + mRootNode = skyroot; - osg::Object *cloneType() const override - { - return nullptr; - } + mRootNode->setNodeMask(Mask_Sky); + mRootNode->addChild(mEarlyRenderBinRoot); + mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); - osg::Object *clone(const osg::CopyOp &op) const override - { - return nullptr; + mPrecipitationOcclusion = Settings::shaders().mWeatherParticleOcclusion; + mPrecipitationOccluder = std::make_unique(skyroot, parentNode, rootNode, camera); } - void operate(osgParticle::Particle *P, double dt) override + void SkyManager::create() { - } + assert(!mCreated); - void operateParticles(osgParticle::ParticleSystem *ps, double dt) override - { - osg::Vec3 position = getCameraPosition(); - osg::Vec3 positionDifference = position - mPreviousCameraPosition; + bool forceShaders = mSceneManager->getForceShaders(); - osg::Matrix toWorld, toLocal; + mAtmosphereDay = mSceneManager->getInstance(Settings::models().mSkyatmosphere.get(), mEarlyRenderBinRoot); + ModVertexAlphaVisitor modAtmosphere(ModVertexAlphaVisitor::Atmosphere); + mAtmosphereDay->accept(modAtmosphere); - std::vector worldMatrices = ps->getWorldMatrices(); - - if (!worldMatrices.empty()) - { - toWorld = worldMatrices[0]; - toLocal.invert(toWorld); - } + mAtmosphereUpdater = new AtmosphereUpdater; + mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); - for (int i = 0; i < ps->numParticles(); ++i) - { - osgParticle::Particle *p = ps->getParticle(i); - p->setPosition(toWorld.preMult(p->getPosition())); - p->setPosition(p->getPosition() - positionDifference); + mAtmosphereNightNode = new osg::PositionAttitudeTransform; + mAtmosphereNightNode->setNodeMask(0); + mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); - for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions - { - osg::Vec3 pos = p->getPosition(); + osg::ref_ptr atmosphereNight; + if (mSceneManager->getVFS()->exists(Settings::models().mSkynight02.get())) + atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight02.get(), mAtmosphereNightNode); + else + atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight01.get(), mAtmosphereNightNode); + atmosphereNight->getOrCreateStateSet()->setAttributeAndModes( + createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); - if (pos[j] < -mHalfWrapRange[j]) - pos[j] = mHalfWrapRange[j] + fmod(pos[j] - mHalfWrapRange[j],mWrapRange[j]); - else if (pos[j] > mHalfWrapRange[j]) - pos[j] = fmod(pos[j] + mHalfWrapRange[j],mWrapRange[j]) - mHalfWrapRange[j]; + ModVertexAlphaVisitor modStars(ModVertexAlphaVisitor::Stars); + atmosphereNight->accept(modStars); + mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager(), forceShaders); + atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); - p->setPosition(pos); - } + mSun = std::make_unique(mEarlyRenderBinRoot, *mSceneManager); + mSun->setSunglare(mSunglareEnabled); + mMasser = std::make_unique( + mEarlyRenderBinRoot, *mSceneManager, Fallback::Map::getFloat("Moons_Masser_Size") / 125, Moon::Type_Masser); + mSecunda = std::make_unique(mEarlyRenderBinRoot, *mSceneManager, + Fallback::Map::getFloat("Moons_Secunda_Size") / 125, Moon::Type_Secunda); + + mCloudNode = new osg::Group; + mEarlyRenderBinRoot->addChild(mCloudNode); + + mCloudMesh = new osg::PositionAttitudeTransform; + osg::ref_ptr cloudMeshChild + = mSceneManager->getInstance(Settings::models().mSkyclouds.get(), mCloudMesh); + mCloudUpdater = new CloudUpdater(forceShaders); + mCloudUpdater->setOpacity(1.f); + cloudMeshChild->addUpdateCallback(mCloudUpdater); + mCloudMesh->addChild(cloudMeshChild); - p->setPosition(toLocal.preMult(p->getPosition())); + mNextCloudMesh = new osg::PositionAttitudeTransform; + osg::ref_ptr nextCloudMeshChild + = mSceneManager->getInstance(Settings::models().mSkyclouds.get(), mNextCloudMesh); + mNextCloudUpdater = new CloudUpdater(forceShaders); + mNextCloudUpdater->setOpacity(0.f); + nextCloudMeshChild->addUpdateCallback(mNextCloudUpdater); + mNextCloudMesh->setNodeMask(0); + mNextCloudMesh->addChild(nextCloudMeshChild); + + mCloudNode->addChild(mCloudMesh); + mCloudNode->addChild(mNextCloudMesh); + + ModVertexAlphaVisitor modClouds(ModVertexAlphaVisitor::Clouds); + mCloudMesh->accept(modClouds); + mNextCloudMesh->accept(modClouds); + + if (mSceneManager->getForceShaders()) + { + Shader::ShaderManager::DefineMap defines = {}; + Stereo::shaderStereoDefines(defines); + auto program = mSceneManager->getShaderManager().getProgram("sky", defines); + mEarlyRenderBinRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", -1)); + mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes( + program, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); } - mPreviousCameraPosition = position; - } + osg::ref_ptr depth = new SceneUtil::AutoDepth; + depth->setWriteMask(false); + mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth); + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); -protected: - osg::Camera *mCamera; - osg::Vec3 mPreviousCameraPosition; - osg::Vec3 mWrapRange; - osg::Vec3 mHalfWrapRange; + mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); - osg::Vec3 getCameraPosition() - { - return mCamera->getInverseViewMatrix().getTrans(); + mCreated = true; } -}; -class WeatherAlphaOperator : public osgParticle::Operator -{ -public: - WeatherAlphaOperator(float& alpha, bool rain) - : mAlpha(alpha) - , mIsRain(rain) + void SkyManager::createRain() { + if (mRainNode) + return; + + mRainNode = new osg::Group; + + mRainParticleSystem = new NifOsg::ParticleSystem; + osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight + mRainMaxHeight) / 2.f); + + mRainParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); + mRainParticleSystem->setAlignVectorX(osg::Vec3f(0.1, 0, 0)); + mRainParticleSystem->setAlignVectorY(osg::Vec3f(0, 0, 1)); + + osg::ref_ptr stateset = mRainParticleSystem->getOrCreateStateSet(); + + constexpr VFS::Path::NormalizedView raindropImage("textures/tx_raindrop_01.dds"); + osg::ref_ptr raindropTex + = new osg::Texture2D(mSceneManager->getImageManager()->getImage(raindropImage)); + raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + stateset->setTextureAttributeAndModes(0, raindropTex); + stateset->setNestRenderBins(false); + stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + stateset->setMode(GL_BLEND, osg::StateAttribute::ON); + + osg::ref_ptr mat = new osg::Material; + mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, 1)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, 1)); + mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + stateset->setAttributeAndModes(mat); + + osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); + particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); + particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 1.f)); + particleTemplate.setLifeTime(1); + + osg::ref_ptr emitter = new osgParticle::ModularEmitter; + emitter->setParticleSystem(mRainParticleSystem); + + osg::ref_ptr placer = new osgParticle::BoxPlacer; + placer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); + placer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); + placer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + emitter->setPlacer(placer); + mPlacer = placer; + + // FIXME: vanilla engine does not use a particle system to handle rain, it uses a NIF-file with 20 raindrops in + // it. It spawns the (maxRaindrops-getParticleSystem()->numParticles())*dt/rainEntranceSpeed batches every frame + // (near 1-2). Since the rain is a regular geometry, it produces water ripples, also in theory it can be removed + // if collides with something. + osg::ref_ptr counter = new RainCounter; + counter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops / mRainEntranceSpeed * 20); + emitter->setCounter(counter); + mCounter = counter; + + osg::ref_ptr shooter = new RainShooter; + mRainShooter = shooter; + emitter->setShooter(shooter); + + osg::ref_ptr updater = new osgParticle::ParticleSystemUpdater; + updater->addParticleSystem(mRainParticleSystem); + + osg::ref_ptr program = new osgParticle::ModularProgram; + program->addOperator(new WrapAroundOperator(mCamera, rainRange)); + program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, true)); + program->setParticleSystem(mRainParticleSystem); + mRainNode->addChild(program); + + mRainNode->addChild(emitter); + mRainNode->addChild(mRainParticleSystem); + mRainNode->addChild(updater); + + // Note: if we ever switch to regular geometry rain, it'll need to use an AlphaFader. + mRainNode->addCullCallback(mUnderwaterSwitch); + mRainNode->setNodeMask(Mask_WeatherParticles); + + mRainParticleSystem->setUserValue("simpleLighting", true); + mRainParticleSystem->setUserValue("particleOcclusion", true); + mSceneManager->recreateShaders(mRainNode); + + mRootNode->addChild(mRainNode); + if (mPrecipitationOcclusion) + mPrecipitationOccluder->enable(); + } + + void SkyManager::destroyRain() + { + if (!mRainNode) + return; + + mRootNode->removeChild(mRainNode); + mRainNode = nullptr; + mPlacer = nullptr; + mCounter = nullptr; + mRainParticleSystem = nullptr; + mRainShooter = nullptr; + mPrecipitationOccluder->disable(); } - osg::Object *cloneType() const override + SkyManager::~SkyManager() { - return nullptr; + if (mRootNode) + { + mRootNode->getParent(0)->removeChild(mRootNode); + mRootNode = nullptr; + } } - osg::Object *clone(const osg::CopyOp &op) const override + int SkyManager::getMasserPhase() const { - return nullptr; + if (!mCreated) + return 0; + return mMasser->getPhaseInt(); } - void operate(osgParticle::Particle *particle, double dt) override + int SkyManager::getSecundaPhase() const { - constexpr float rainThreshold = 0.6f; // Rain_Threshold? - const float alpha = mIsRain ? mAlpha * rainThreshold : mAlpha; - particle->setAlphaRange(osgParticle::rangef(alpha, alpha)); + if (!mCreated) + return 0; + return mSecunda->getPhaseInt(); } -private: - float &mAlpha; - bool mIsRain; -}; - -void SkyManager::createRain() -{ - if (mRainNode) - return; - - mRainNode = new osg::Group; - - mRainParticleSystem = new NifOsg::ParticleSystem; - osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); - - mRainParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); - mRainParticleSystem->setAlignVectorX(osg::Vec3f(0.1,0,0)); - mRainParticleSystem->setAlignVectorY(osg::Vec3f(0,0,1)); - - osg::ref_ptr stateset (mRainParticleSystem->getOrCreateStateSet()); - - osg::ref_ptr raindropTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage("textures/tx_raindrop_01.dds"))); - raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - stateset->setTextureAttributeAndModes(0, raindropTex, osg::StateAttribute::ON); - stateset->setNestRenderBins(false); - stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); - stateset->setMode(GL_BLEND, osg::StateAttribute::ON); - - osg::ref_ptr mat (new osg::Material); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); - - osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); - particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); - particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 1.f)); - particleTemplate.setLifeTime(1); - - osg::ref_ptr emitter (new osgParticle::ModularEmitter); - emitter->setParticleSystem(mRainParticleSystem); - - osg::ref_ptr placer (new osgParticle::BoxPlacer); - placer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); - placer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); - placer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); - emitter->setPlacer(placer); - mPlacer = placer; - - // FIXME: vanilla engine does not use a particle system to handle rain, it uses a NIF-file with 20 raindrops in it. - // It spawns the (maxRaindrops-getParticleSystem()->numParticles())*dt/rainEntranceSpeed batches every frame (near 1-2). - // Since the rain is a regular geometry, it produces water ripples, also in theory it can be removed if collides with something. - osg::ref_ptr counter (new RainCounter); - counter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); - emitter->setCounter(counter); - mCounter = counter; - - osg::ref_ptr shooter (new RainShooter); - mRainShooter = shooter; - emitter->setShooter(shooter); - - osg::ref_ptr updater (new osgParticle::ParticleSystemUpdater); - updater->addParticleSystem(mRainParticleSystem); - - osg::ref_ptr program (new osgParticle::ModularProgram); - program->addOperator(new WrapAroundOperator(mCamera,rainRange)); - program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, true)); - program->setParticleSystem(mRainParticleSystem); - mRainNode->addChild(program); - - mRainNode->addChild(emitter); - mRainNode->addChild(mRainParticleSystem); - mRainNode->addChild(updater); - - // Note: if we ever switch to regular geometry rain, it'll need to use an AlphaFader. - mRainNode->addCullCallback(mUnderwaterSwitch); - mRainNode->setNodeMask(Mask_WeatherParticles); - - mRootNode->addChild(mRainNode); -} - -void SkyManager::destroyRain() -{ - if (!mRainNode) - return; - - mRootNode->removeChild(mRainNode); - mRainNode = nullptr; - mPlacer = nullptr; - mCounter = nullptr; - mRainParticleSystem = nullptr; - mRainShooter = nullptr; -} - -SkyManager::~SkyManager() -{ - if (mRootNode) + bool SkyManager::isEnabled() { - mRootNode->getParent(0)->removeChild(mRootNode); - mRootNode = nullptr; + return mEnabled; } -} - -int SkyManager::getMasserPhase() const -{ - if (!mCreated) return 0; - return mMasser->getPhaseInt(); -} -int SkyManager::getSecundaPhase() const -{ - if (!mCreated) return 0; - return mSecunda->getPhaseInt(); -} + bool SkyManager::hasRain() const + { + return mRainNode != nullptr; + } -bool SkyManager::isEnabled() -{ - return mEnabled; -} + bool SkyManager::getRainRipplesEnabled() const + { + if (!mEnabled || mIsStorm) + return false; -bool SkyManager::hasRain() const -{ - return mRainNode != nullptr; -} + if (hasRain()) + return mRainRipplesEnabled; -float SkyManager::getPrecipitationAlpha() const -{ - if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) - return mPrecipitationAlpha; + if (mParticleNode && mCurrentParticleEffect == Settings::models().mWeathersnow.get()) + return mSnowRipplesEnabled; - return 0.f; -} + return false; + } -void SkyManager::update(float duration) -{ - if (!mEnabled) - return; + float SkyManager::getPrecipitationAlpha() const + { + if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) + return mPrecipitationAlpha; - switchUnderwaterRain(); + return 0.f; + } - if (mIsStorm) + void SkyManager::update(float duration) { - osg::Quat quat; - quat.makeRotate(osg::Vec3f(0,1,0), mStormDirection); + if (!mEnabled) + return; - mCloudNode->setAttitude(quat); - if (mParticleNode) + switchUnderwaterRain(); + + if (mIsStorm && mParticleNode) { + osg::Quat quat; + quat.makeRotate(MWWorld::Weather::defaultDirection(), mStormParticleDirection); // Morrowind deliberately rotates the blizzard mesh, so so should we. - if (mCurrentParticleEffect == Settings::Manager::getString("weatherblizzard", "Models")) - quat.makeRotate(osg::Vec3f(-1,0,0), mStormDirection); + if (mCurrentParticleEffect == Settings::models().mWeatherblizzard.get()) + quat.makeRotate(osg::Vec3f(-1, 0, 0), mStormParticleDirection); mParticleNode->setAttitude(quat); } - } - else - mCloudNode->setAttitude(osg::Quat()); - - // UV Scroll the clouds - mCloudAnimationTimer += duration * mCloudSpeed * 0.003; - mCloudUpdater->setAnimationTimer(mCloudAnimationTimer); - mCloudUpdater2->setAnimationTimer(mCloudAnimationTimer); - - // rotate the stars by 360 degrees every 4 days - mAtmosphereNightRoll += MWBase::Environment::get().getWorld()->getTimeScaleFactor()*duration*osg::DegreesToRadians(360.f) / (3600*96.f); - if (mAtmosphereNightNode->getNodeMask() != 0) - mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0,0,1))); -} - -void SkyManager::setEnabled(bool enabled) -{ - if (enabled && !mCreated) - create(); - mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); + const float timeScale = MWBase::Environment::get().getWorld()->getTimeManager()->getGameTimeScale(); - mEnabled = enabled; -} + // UV Scroll the clouds + float cloudDelta = duration * mCloudSpeed / 400.f; + if (mTimescaleClouds) + cloudDelta *= timeScale / 60.f; -void SkyManager::setMoonColour (bool red) -{ - if (!mCreated) return; - mSecunda->setColor(red ? mMoonScriptColor : osg::Vec4f(1,1,1,1)); -} + mCloudAnimationTimer += cloudDelta; + if (mCloudAnimationTimer >= 4.f) + mCloudAnimationTimer -= 4.f; -void SkyManager::updateRainParameters() -{ - if (mRainShooter) - { - float angle = -std::atan(mWindSpeed/50.f); - mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed*std::sin(angle), -mRainSpeed/std::cos(angle))); - mRainShooter->setAngle(angle); + mNextCloudUpdater->setTextureCoord(mCloudAnimationTimer); + mCloudUpdater->setTextureCoord(mCloudAnimationTimer); - osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); + // morrowind rotates each cloud mesh independently + osg::Quat rotation; + rotation.makeRotate(MWWorld::Weather::defaultDirection(), mStormDirection); + mCloudMesh->setAttitude(rotation); - mPlacer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); - mPlacer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); - mPlacer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + if (mNextCloudMesh->getNodeMask()) + { + rotation.makeRotate(MWWorld::Weather::defaultDirection(), mNextStormDirection); + mNextCloudMesh->setAttitude(rotation); + } - mCounter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); + // rotate the stars by 360 degrees every 4 days + mAtmosphereNightRoll += timeScale * duration * osg::DegreesToRadians(360.f) / (3600 * 96.f); + if (mAtmosphereNightNode->getNodeMask() != 0) + mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0, 0, 1))); + mPrecipitationOccluder->update(); } -} -void SkyManager::switchUnderwaterRain() -{ - if (!mRainParticleSystem) - return; + void SkyManager::setEnabled(bool enabled) + { + if (enabled && !mCreated) + create(); - bool freeze = mUnderwaterSwitch->isUnderwater(); - mRainParticleSystem->setFrozen(freeze); -} + const osg::Node::NodeMask mask = enabled ? Mask_Sky : 0u; -void SkyManager::setWeather(const WeatherResult& weather) -{ - if (!mCreated) return; - - mRainEntranceSpeed = weather.mRainEntranceSpeed; - mRainMaxRaindrops = weather.mRainMaxRaindrops; - mRainDiameter = weather.mRainDiameter; - mRainMinHeight = weather.mRainMinHeight; - mRainMaxHeight = weather.mRainMaxHeight; - mRainSpeed = weather.mRainSpeed; - mWindSpeed = weather.mWindSpeed; - mBaseWindSpeed = weather.mBaseWindSpeed; - - if (mRainEffect != weather.mRainEffect) - { - mRainEffect = weather.mRainEffect; - if (!mRainEffect.empty()) + mEarlyRenderBinRoot->setNodeMask(mask); + mRootNode->setNodeMask(mask); + + if (!enabled && mParticleNode && mParticleEffect) { - createRain(); + mCurrentParticleEffect.clear(); + mDirtyParticlesEffect = true; } - else + + mEnabled = enabled; + } + + void SkyManager::setMoonColour(bool red) + { + if (!mCreated) + return; + mSecunda->setColor(red ? mMoonScriptColor : osg::Vec4f(1, 1, 1, 1)); + } + + void SkyManager::updateRainParameters() + { + if (mRainShooter) { - destroyRain(); + float angle = -std::atan(mWindSpeed / 50.f); + mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed * std::sin(angle), -mRainSpeed / std::cos(angle))); + mRainShooter->setAngle(angle); + + osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight + mRainMaxHeight) / 2.f); + + mPlacer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); + mPlacer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); + mPlacer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + + mCounter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops / mRainEntranceSpeed * 20); + mPrecipitationOccluder->updateRange(rainRange); } } - updateRainParameters(); + void SkyManager::switchUnderwaterRain() + { + if (!mRainParticleSystem) + return; - mIsStorm = weather.mIsStorm; + bool freeze = mUnderwaterSwitch->isUnderwater(); + mRainParticleSystem->setFrozen(freeze); + } - if (mCurrentParticleEffect != weather.mParticleEffect) + void SkyManager::setWeather(const WeatherResult& weather) { - mCurrentParticleEffect = weather.mParticleEffect; + if (!mCreated) + return; - // cleanup old particles - if (mParticleEffect) - { - mParticleNode->removeChild(mParticleEffect); - mParticleEffect = nullptr; - } + mRainEntranceSpeed = weather.mRainEntranceSpeed; + mRainMaxRaindrops = weather.mRainMaxRaindrops; + mRainDiameter = weather.mRainDiameter; + mRainMinHeight = weather.mRainMinHeight; + mRainMaxHeight = weather.mRainMaxHeight; + mRainSpeed = weather.mRainSpeed; + mWindSpeed = weather.mWindSpeed; + mBaseWindSpeed = weather.mBaseWindSpeed; - if (mCurrentParticleEffect.empty()) + if (mRainEffect != weather.mRainEffect) { - if (mParticleNode) + mRainEffect = weather.mRainEffect; + if (!mRainEffect.empty()) { - mRootNode->removeChild(mParticleNode); - mParticleNode = nullptr; + createRain(); + } + else + { + destroyRain(); } } - else + + updateRainParameters(); + + mIsStorm = weather.mIsStorm; + + if (mIsStorm) + mStormDirection = weather.mStormDirection; + + if (mDirtyParticlesEffect || (mCurrentParticleEffect != weather.mParticleEffect)) { - if (!mParticleNode) + mDirtyParticlesEffect = false; + mCurrentParticleEffect = weather.mParticleEffect; + + // cleanup old particles + if (mParticleEffect) + { + mParticleNode->removeChild(mParticleEffect); + mParticleEffect = nullptr; + } + + if (mCurrentParticleEffect.empty()) { - mParticleNode = new osg::PositionAttitudeTransform; - mParticleNode->addCullCallback(mUnderwaterSwitch); - mParticleNode->setNodeMask(Mask_WeatherParticles); - mRootNode->addChild(mParticleNode); + if (mParticleNode) + { + mRootNode->removeChild(mParticleNode); + mParticleNode = nullptr; + } + if (mRainEffect.empty()) + { + mPrecipitationOccluder->disable(); + } + } + else + { + if (!mParticleNode) + { + mParticleNode = new osg::PositionAttitudeTransform; + mParticleNode->addCullCallback(mUnderwaterSwitch); + mParticleNode->setNodeMask(Mask_WeatherParticles); + mRootNode->addChild(mParticleNode); + } + + mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); + + SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::make_shared()); + mParticleEffect->accept(assignVisitor); + + SetupVisitor alphaFaderSetupVisitor(mPrecipitationAlpha); + mParticleEffect->accept(alphaFaderSetupVisitor); + + SceneUtil::FindByClassVisitor findPSVisitor("ParticleSystem"); + mParticleEffect->accept(findPSVisitor); + + const osg::Vec3 defaultWrapRange = osg::Vec3(1024, 1024, 800); + const bool occlusionEnabledForEffect + = !mRainEffect.empty() || mCurrentParticleEffect == Settings::models().mWeathersnow.get(); + + for (unsigned int i = 0; i < findPSVisitor.mFoundNodes.size(); ++i) + { + osgParticle::ParticleSystem* ps + = static_cast(findPSVisitor.mFoundNodes[i]); + + osg::ref_ptr program = new osgParticle::ModularProgram; + if (!mIsStorm) + program->addOperator(new WrapAroundOperator(mCamera, defaultWrapRange)); + program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, false)); + program->setParticleSystem(ps); + mParticleNode->addChild(program); + + for (int particleIndex = 0; particleIndex < ps->numParticles(); ++particleIndex) + { + ps->getParticle(particleIndex) + ->setAlphaRange(osgParticle::rangef(mPrecipitationAlpha, mPrecipitationAlpha)); + ps->getParticle(particleIndex)->update(0, true); + } + + ps->setUserValue("simpleLighting", true); + + if (occlusionEnabledForEffect) + ps->setUserValue("particleOcclusion", true); + } + + mSceneManager->recreateShaders(mParticleNode); + + if (mPrecipitationOcclusion && occlusionEnabledForEffect) + { + mPrecipitationOccluder->enable(); + mPrecipitationOccluder->updateRange(defaultWrapRange); + } } + } - mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); + if (mClouds != weather.mCloudTexture) + { + mClouds = weather.mCloudTexture; + + const VFS::Path::Normalized texture + = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); - SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr(new SceneUtil::FrameTimeSource)); - mParticleEffect->accept(assignVisitor); + osg::ref_ptr cloudTex + = new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture)); + cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); + cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - AlphaFader::SetupVisitor alphaFaderSetupVisitor(mPrecipitationAlpha); + mCloudUpdater->setTexture(std::move(cloudTex)); + } - mParticleEffect->accept(alphaFaderSetupVisitor); + if (mStormDirection != weather.mStormDirection) + mStormDirection = weather.mStormDirection; - SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; - mParticleEffect->accept(disableFreezeOnCullVisitor); + if (mNextStormDirection != weather.mNextStormDirection) + mNextStormDirection = weather.mNextStormDirection; - SceneUtil::FindByClassVisitor findPSVisitor(std::string("ParticleSystem")); - mParticleEffect->accept(findPSVisitor); + if (mNextClouds != weather.mNextCloudTexture) + { + mNextClouds = weather.mNextCloudTexture; - for (unsigned int i = 0; i < findPSVisitor.mFoundNodes.size(); ++i) + if (!mNextClouds.empty()) { - osgParticle::ParticleSystem *ps = static_cast(findPSVisitor.mFoundNodes[i]); - - osg::ref_ptr program (new osgParticle::ModularProgram); - if (!mIsStorm) - program->addOperator(new WrapAroundOperator(mCamera,osg::Vec3(1024,1024,800))); - program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, false)); - program->setParticleSystem(ps); - mParticleNode->addChild(program); + const VFS::Path::Normalized texture + = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); + + osg::ref_ptr cloudTex + = new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture)); + cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); + cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + + mNextCloudUpdater->setTexture(std::move(cloudTex)); + mNextStormDirection = weather.mStormDirection; } } - } - if (mClouds != weather.mCloudTexture) - { - mClouds = weather.mCloudTexture; + if (mCloudBlendFactor != weather.mCloudBlendFactor) + { + mCloudBlendFactor = std::clamp(weather.mCloudBlendFactor, 0.f, 1.f); - std::string texture = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); + mCloudUpdater->setOpacity(1.f - mCloudBlendFactor); + mNextCloudUpdater->setOpacity(mCloudBlendFactor); + mNextCloudMesh->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); + } - osg::ref_ptr cloudTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture))); - cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); - cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + if (mCloudColour != weather.mFogColor) + { + osg::Vec4f clr(weather.mFogColor); + clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); - mCloudUpdater->setTexture(cloudTex); - } + mCloudUpdater->setEmissionColor(clr); + mNextCloudUpdater->setEmissionColor(clr); - if (mNextClouds != weather.mNextCloudTexture) - { - mNextClouds = weather.mNextCloudTexture; + mCloudColour = weather.mFogColor; + } - if (!mNextClouds.empty()) + if (mSkyColour != weather.mSkyColor) { - std::string texture = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); + mSkyColour = weather.mSkyColor; - osg::ref_ptr cloudTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture))); - cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); - cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + mAtmosphereUpdater->setEmissionColor(mSkyColour); + mMasser->setAtmosphereColor(mSkyColour); + mSecunda->setAtmosphereColor(mSkyColour); + } - mCloudUpdater2->setTexture(cloudTex); + if (mFogColour != weather.mFogColor) + { + mFogColour = weather.mFogColor; } - } - if (mCloudBlendFactor != weather.mCloudBlendFactor) - { - mCloudBlendFactor = weather.mCloudBlendFactor; + mCloudSpeed = weather.mCloudSpeed; - mCloudUpdater->setOpacity((1.f-mCloudBlendFactor)); - mCloudUpdater2->setOpacity(mCloudBlendFactor); - mCloudMesh2->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); - } + mMasser->adjustTransparency(weather.mGlareView); + mSecunda->adjustTransparency(weather.mGlareView); - if (mCloudColour != weather.mFogColor) - { - osg::Vec4f clr (weather.mFogColor); - clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); + mSun->setColor(weather.mSunDiscColor); + mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); - mCloudUpdater->setEmissionColor(clr); - mCloudUpdater2->setEmissionColor(clr); + float nextStarsOpacity = weather.mNightFade * weather.mGlareView; - mCloudColour = weather.mFogColor; - } + if (weather.mNight && mStarsOpacity != nextStarsOpacity) + { + mStarsOpacity = nextStarsOpacity; - if (mSkyColour != weather.mSkyColor) - { - mSkyColour = weather.mSkyColor; + mAtmosphereNightUpdater->setFade(mStarsOpacity); + } - mAtmosphereUpdater->setEmissionColor(mSkyColour); - mMasser->setAtmosphereColor(mSkyColour); - mSecunda->setAtmosphereColor(mSkyColour); + mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); + mPrecipitationAlpha = weather.mPrecipitationAlpha; } - if (mFogColour != weather.mFogColor) + float SkyManager::getBaseWindSpeed() const { - mFogColour = weather.mFogColor; - } - - mCloudSpeed = weather.mCloudSpeed; + if (!mCreated) + return 0.f; - mMasser->adjustTransparency(weather.mGlareView); - mSecunda->adjustTransparency(weather.mGlareView); + return mBaseWindSpeed; + } - mSun->setColor(weather.mSunDiscColor); - mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); + void SkyManager::setSunglare(bool enabled) + { + mSunglareEnabled = enabled; - float nextStarsOpacity = weather.mNightFade * weather.mGlareView; + if (mSun) + mSun->setSunglare(mSunglareEnabled); + } - if (weather.mNight && mStarsOpacity != nextStarsOpacity) + void SkyManager::sunEnable() { - mStarsOpacity = nextStarsOpacity; + if (!mCreated) + return; - mAtmosphereNightUpdater->setFade(mStarsOpacity); + mSun->setVisible(true); } - mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); - - mPrecipitationAlpha = weather.mPrecipitationAlpha; -} + void SkyManager::sunDisable() + { + if (!mCreated) + return; -float SkyManager::getBaseWindSpeed() const -{ - if (!mCreated) return 0.f; + mSun->setVisible(false); + } - return mBaseWindSpeed; -} + void SkyManager::setStormParticleDirection(const osg::Vec3f& direction) + { + mStormParticleDirection = direction; + } -void SkyManager::sunEnable() -{ - if (!mCreated) return; + void SkyManager::setSunDirection(const osg::Vec3f& direction) + { + if (!mCreated) + return; - mSun->setVisible(true); -} + mSun->setDirection(direction); + } -void SkyManager::sunDisable() -{ - if (!mCreated) return; + void SkyManager::setMasserState(const MoonState& state) + { + if (!mCreated) + return; - mSun->setVisible(false); -} + mMasser->setState(state); + } -void SkyManager::setStormDirection(const osg::Vec3f &direction) -{ - mStormDirection = direction; -} + void SkyManager::setSecundaState(const MoonState& state) + { + if (!mCreated) + return; -void SkyManager::setSunDirection(const osg::Vec3f& direction) -{ - if (!mCreated) return; + mSecunda->setState(state); + } - mSun->setDirection(direction); -} + void SkyManager::setDate(int day, int month) + { + mDay = day; + mMonth = month; + } -void SkyManager::setMasserState(const MoonState& state) -{ - if(!mCreated) return; + void SkyManager::setGlareTimeOfDayFade(float val) + { + mSun->setGlareTimeOfDayFade(val); + } - mMasser->setState(state); -} + void SkyManager::setWaterHeight(float height) + { + mUnderwaterSwitch->setWaterLevel(height); + } -void SkyManager::setSecundaState(const MoonState& state) -{ - if(!mCreated) return; + void SkyManager::listAssetsToPreload( + std::vector& models, std::vector& textures) + { + models.push_back(Settings::models().mSkyatmosphere); + if (mSceneManager->getVFS()->exists(Settings::models().mSkynight02.get())) + models.push_back(Settings::models().mSkynight02); + models.push_back(Settings::models().mSkynight01); + models.push_back(Settings::models().mSkyclouds); - mSecunda->setState(state); -} + models.push_back(Settings::models().mWeatherashcloud); + models.push_back(Settings::models().mWeatherblightcloud); + models.push_back(Settings::models().mWeathersnow); + models.push_back(Settings::models().mWeatherblizzard); -void SkyManager::setDate(int day, int month) -{ - mDay = day; - mMonth = month; -} + textures.emplace_back("textures/tx_mooncircle_full_s.dds"); + textures.emplace_back("textures/tx_mooncircle_full_m.dds"); -void SkyManager::setGlareTimeOfDayFade(float val) -{ - mSun->setGlareTimeOfDayFade(val); -} + textures.emplace_back("textures/tx_masser_new.dds"); + textures.emplace_back("textures/tx_masser_one_wax.dds"); + textures.emplace_back("textures/tx_masser_half_wax.dds"); + textures.emplace_back("textures/tx_masser_three_wax.dds"); + textures.emplace_back("textures/tx_masser_one_wan.dds"); + textures.emplace_back("textures/tx_masser_half_wan.dds"); + textures.emplace_back("textures/tx_masser_three_wan.dds"); + textures.emplace_back("textures/tx_masser_full.dds"); -void SkyManager::setWaterHeight(float height) -{ - mUnderwaterSwitch->setWaterLevel(height); -} + textures.emplace_back("textures/tx_secunda_new.dds"); + textures.emplace_back("textures/tx_secunda_one_wax.dds"); + textures.emplace_back("textures/tx_secunda_half_wax.dds"); + textures.emplace_back("textures/tx_secunda_three_wax.dds"); + textures.emplace_back("textures/tx_secunda_one_wan.dds"); + textures.emplace_back("textures/tx_secunda_half_wan.dds"); + textures.emplace_back("textures/tx_secunda_three_wan.dds"); + textures.emplace_back("textures/tx_secunda_full.dds"); -void SkyManager::listAssetsToPreload(std::vector& models, std::vector& textures) -{ - models.emplace_back(Settings::Manager::getString("skyatmosphere", "Models")); - if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) - models.emplace_back(Settings::Manager::getString("skynight02", "Models")); - models.emplace_back(Settings::Manager::getString("skynight01", "Models")); - models.emplace_back(Settings::Manager::getString("skyclouds", "Models")); - - models.emplace_back(Settings::Manager::getString("weatherashcloud", "Models")); - models.emplace_back(Settings::Manager::getString("weatherblightcloud", "Models")); - models.emplace_back(Settings::Manager::getString("weathersnow", "Models")); - models.emplace_back(Settings::Manager::getString("weatherblizzard", "Models")); - - textures.emplace_back("textures/tx_mooncircle_full_s.dds"); - textures.emplace_back("textures/tx_mooncircle_full_m.dds"); - - textures.emplace_back("textures/tx_masser_new.dds"); - textures.emplace_back("textures/tx_masser_one_wax.dds"); - textures.emplace_back("textures/tx_masser_half_wax.dds"); - textures.emplace_back("textures/tx_masser_three_wax.dds"); - textures.emplace_back("textures/tx_masser_one_wan.dds"); - textures.emplace_back("textures/tx_masser_half_wan.dds"); - textures.emplace_back("textures/tx_masser_three_wan.dds"); - textures.emplace_back("textures/tx_masser_full.dds"); - - textures.emplace_back("textures/tx_secunda_new.dds"); - textures.emplace_back("textures/tx_secunda_one_wax.dds"); - textures.emplace_back("textures/tx_secunda_half_wax.dds"); - textures.emplace_back("textures/tx_secunda_three_wax.dds"); - textures.emplace_back("textures/tx_secunda_one_wan.dds"); - textures.emplace_back("textures/tx_secunda_half_wan.dds"); - textures.emplace_back("textures/tx_secunda_three_wan.dds"); - textures.emplace_back("textures/tx_secunda_full.dds"); - - textures.emplace_back("textures/tx_sun_05.dds"); - textures.emplace_back("textures/tx_sun_flash_grey_05.dds"); - - textures.emplace_back("textures/tx_raindrop_01.dds"); -} + textures.emplace_back("textures/tx_sun_05.dds"); + textures.emplace_back("textures/tx_sun_flash_grey_05.dds"); -void SkyManager::setWaterEnabled(bool enabled) -{ - mUnderwaterSwitch->setEnabled(enabled); -} + textures.emplace_back("textures/tx_raindrop_01.dds"); + } + void SkyManager::setWaterEnabled(bool enabled) + { + mUnderwaterSwitch->setEnabled(enabled); + } } diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index f8c501dda6b..4ec357056e8 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -1,17 +1,17 @@ #ifndef OPENMW_MWRENDER_SKY_H #define OPENMW_MWRENDER_SKY_H -#include #include +#include #include -#include #include +#include -namespace osg -{ - class Camera; -} +#include + +#include "precipitationocclusion.hpp" +#include "skyutil.hpp" namespace osg { @@ -19,6 +19,7 @@ namespace osg class Node; class Material; class PositionAttitudeTransform; + class Camera; } namespace osgParticle @@ -32,109 +33,31 @@ namespace Resource class SceneManager; } -namespace MWRender +namespace SceneUtil { - class AtmosphereUpdater; - class AtmosphereNightUpdater; - class CloudUpdater; - class Sun; - class Moon; - class RainCounter; - class RainShooter; - class RainFader; - class AlphaFader; - class UnderwaterSwitchCallback; - - struct WeatherResult - { - std::string mCloudTexture; - std::string mNextCloudTexture; - float mCloudBlendFactor; - - osg::Vec4f mFogColor; - - osg::Vec4f mAmbientColor; - - osg::Vec4f mSkyColor; - - // sun light color - osg::Vec4f mSunColor; - - // alpha is the sun transparency - osg::Vec4f mSunDiscColor; - - float mFogDepth; - - float mDLFogFactor; - float mDLFogOffset; - - float mWindSpeed; - float mBaseWindSpeed; - float mCurrentWindSpeed; - float mNextWindSpeed; - - float mCloudSpeed; - - float mGlareView; - - bool mNight; // use night skybox - float mNightFade; // fading factor for night skybox - - bool mIsStorm; - - std::string mAmbientLoopSoundID; - float mAmbientSoundVolume; - - std::string mParticleEffect; - std::string mRainEffect; - float mPrecipitationAlpha; - - float mRainDiameter; - float mRainMinHeight; - float mRainMaxHeight; - float mRainSpeed; - float mRainEntranceSpeed; - int mRainMaxRaindrops; - }; - - struct MoonState - { - enum class Phase - { - Full = 0, - WaningGibbous, - ThirdQuarter, - WaningCrescent, - New, - WaxingCrescent, - FirstQuarter, - WaxingGibbous, - Unspecified - }; - - float mRotationFromHorizon; - float mRotationFromNorth; - Phase mPhase; - float mShadowBlend; - float mMoonAlpha; - }; + class RTTNode; +} - ///@brief The SkyManager handles rendering of the sky domes, celestial bodies as well as other objects that need to be rendered +namespace MWRender +{ + ///@brief The SkyManager handles rendering of the sky domes, celestial bodies as well as other objects that need to + /// be rendered /// relative to the camera (e.g. weather particle effects) class SkyManager { public: - SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager); + SkyManager(osg::Group* parentNode, osg::Group* rootNode, osg::Camera* camera, + Resource::SceneManager* sceneManager, bool enableSkyRTT); ~SkyManager(); void update(float duration); void setEnabled(bool enabled); - void setHour (double hour); + void setHour(double hour); ///< will be called even when sky is disabled. - void setDate (int day, int month); + void setDate(int day, int month); ///< will be called even when sky is disabled. int getMasserPhase() const; @@ -145,7 +68,7 @@ namespace MWRender ///< 0 new moon, 1 waxing or waning cresecent, 2 waxing or waning half, /// 3 waxing or waning gibbous, 4 full moon - void setMoonColour (bool red); + void setMoonColour(bool red); ///< change Secunda colour to red void setWeather(const WeatherResult& weather); @@ -158,11 +81,13 @@ namespace MWRender bool hasRain() const; + bool getRainRipplesEnabled() const; + float getPrecipitationAlpha() const; void setRainSpeed(float speed); - void setStormDirection(const osg::Vec3f& direction); + void setStormParticleDirection(const osg::Vec3f& direction); void setSunDirection(const osg::Vec3f& direction); @@ -177,12 +102,17 @@ namespace MWRender /// Set height of water plane (used to remove underwater weather particles) void setWaterHeight(float height); - void listAssetsToPreload(std::vector& models, std::vector& textures); - - void setCamera(osg::Camera *camera); + void listAssetsToPreload( + std::vector& models, std::vector& textures); float getBaseWindSpeed() const; + void setSunglare(bool enabled); + + SceneUtil::RTTNode* getSkyRTT() { return mSkyRTT.get(); } + + osg::Vec4f getSkyColor() const { return mSkyColour; } + private: void create(); ///< no need to call this, automatically done on first enable() @@ -194,7 +124,7 @@ namespace MWRender Resource::SceneManager* mSceneManager; - osg::Camera *mCamera; + osg::Camera* mCamera; osg::ref_ptr mRootNode; osg::ref_ptr mEarlyRenderBinRoot; @@ -203,12 +133,12 @@ namespace MWRender osg::ref_ptr mParticleEffect; osg::ref_ptr mUnderwaterSwitch; - osg::ref_ptr mCloudNode; + osg::ref_ptr mCloudNode; osg::ref_ptr mCloudUpdater; - osg::ref_ptr mCloudUpdater2; - osg::ref_ptr mCloudMesh; - osg::ref_ptr mCloudMesh2; + osg::ref_ptr mNextCloudUpdater; + osg::ref_ptr mCloudMesh; + osg::ref_ptr mNextCloudMesh; osg::ref_ptr mAtmosphereDay; @@ -228,6 +158,9 @@ namespace MWRender osg::ref_ptr mCounter; osg::ref_ptr mRainShooter; + bool mPrecipitationOcclusion = false; + std::unique_ptr mPrecipitationOccluder; + bool mCreated; bool mIsStorm; @@ -235,11 +168,15 @@ namespace MWRender int mDay; int mMonth; + bool mTimescaleClouds; float mCloudAnimationTimer; float mRainTimer; + // particle system rotation is independent of cloud rotation internally + osg::Vec3f mStormParticleDirection; osg::Vec3f mStormDirection; + osg::Vec3f mNextStormDirection; // remember some settings so we don't have to apply them again if they didn't change std::string mClouds; @@ -251,7 +188,7 @@ namespace MWRender osg::Vec4f mSkyColour; osg::Vec4f mFogColour; - std::string mCurrentParticleEffect; + VFS::Path::Normalized mCurrentParticleEffect; float mRemainingTransitionTime; @@ -263,16 +200,22 @@ namespace MWRender float mRainMaxHeight; float mRainEntranceSpeed; int mRainMaxRaindrops; + bool mRainRipplesEnabled; + bool mSnowRipplesEnabled; float mWindSpeed; float mBaseWindSpeed; bool mEnabled; bool mSunEnabled; + bool mSunglareEnabled; float mPrecipitationAlpha; + bool mDirtyParticlesEffect; osg::Vec4f mMoonScriptColor; + + osg::ref_ptr mSkyRTT; }; } -#endif // GAME_RENDER_SKY_H +#endif diff --git a/apps/openmw/mwrender/skyutil.cpp b/apps/openmw/mwrender/skyutil.cpp new file mode 100644 index 00000000000..2881f51d4c0 --- /dev/null +++ b/apps/openmw/mwrender/skyutil.cpp @@ -0,0 +1,1222 @@ +#include "skyutil.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include + +#include "../mwbase/environment.hpp" + +#include "renderbin.hpp" +#include "vismask.hpp" + +namespace +{ + enum class Pass + { + Atmosphere, + Atmosphere_Night, + Clouds, + Moon, + Sun, + Sunflash_Query, + Sunglare, + }; + + osg::ref_ptr createTexturedQuad(int numUvSets = 1, float scale = 1.f) + { + osg::ref_ptr geom = new osg::Geometry; + + osg::ref_ptr verts = new osg::Vec3Array; + verts->push_back(osg::Vec3f(-0.5 * scale, -0.5 * scale, 0)); + verts->push_back(osg::Vec3f(-0.5 * scale, 0.5 * scale, 0)); + verts->push_back(osg::Vec3f(0.5 * scale, 0.5 * scale, 0)); + verts->push_back(osg::Vec3f(0.5 * scale, -0.5 * scale, 0)); + + geom->setVertexArray(verts); + + osg::ref_ptr texcoords = new osg::Vec2Array; + texcoords->push_back(osg::Vec2f(0, 1)); + texcoords->push_back(osg::Vec2f(0, 0)); + texcoords->push_back(osg::Vec2f(1, 0)); + texcoords->push_back(osg::Vec2f(1, 1)); + + osg::ref_ptr colors = new osg::Vec4Array; + colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); + geom->setColorArray(colors, osg::Array::BIND_OVERALL); + + for (int i = 0; i < numUvSets; ++i) + geom->setTexCoordArray(i, texcoords, osg::Array::BIND_PER_VERTEX); + + geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS, 0, 4)); + + return geom; + } + + struct DummyComputeBoundCallback : osg::Node::ComputeBoundingSphereCallback + { + osg::BoundingSphere computeBound(const osg::Node& node) const override { return osg::BoundingSphere(); } + }; +} + +namespace MWRender +{ + osg::ref_ptr createUnlitMaterial(osg::Material::ColorMode colorMode) + { + osg::ref_ptr mat = new osg::Material; + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); + mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); + mat->setColorMode(colorMode); + return mat; + } + + osg::ref_ptr createAlphaTrackingUnlitMaterial() + { + return createUnlitMaterial(osg::Material::DIFFUSE); + } + + class SunUpdater : public SceneUtil::StateSetUpdater + { + public: + osg::Vec4f mColor; + + SunUpdater() + : mColor(1.f, 1.f, 1.f, 1.f) + { + } + + void setDefaults(osg::StateSet* stateset) override { stateset->setAttributeAndModes(createUnlitMaterial()); } + + void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, mColor.a())); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(mColor.r(), mColor.g(), mColor.b(), 1)); + } + }; + + OcclusionCallback::OcclusionCallback( + osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) + : mOcclusionQueryVisiblePixels(std::move(oqnVisible)) + , mOcclusionQueryTotalPixels(std::move(oqnTotal)) + { + } + + float OcclusionCallback::getVisibleRatio(osg::Camera* camera) + { + int visible = mOcclusionQueryVisiblePixels->getQueryGeometry()->getNumPixels(camera); + int total = mOcclusionQueryTotalPixels->getQueryGeometry()->getNumPixels(camera); + + float visibleRatio = 0.f; + if (total > 0) + visibleRatio = static_cast(visible) / static_cast(total); + + float dt = MWBase::Environment::get().getFrameDuration(); + + float lastRatio = mLastRatio[osg::observer_ptr(camera)]; + + float change = dt * 10; + + if (visibleRatio > lastRatio) + visibleRatio = std::min(visibleRatio, lastRatio + change); + else + visibleRatio = std::max(visibleRatio, lastRatio - change); + + mLastRatio[osg::observer_ptr(camera)] = visibleRatio; + + return visibleRatio; + } + + /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a + /// cull callback. + class SunFlashCallback : public OcclusionCallback, + public SceneUtil::NodeCallback + { + public: + SunFlashCallback( + osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) + : OcclusionCallback(std::move(oqnVisible), std::move(oqnTotal)) + , mGlareView(1.f) + { + } + + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + + osg::ref_ptr stateset; + + if (visibleRatio > 0.f) + { + const float fadeThreshold = 0.1; + if (visibleRatio < fadeThreshold) + { + float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; + osg::ref_ptr mat(createUnlitMaterial()); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, fade * mGlareView)); + stateset = new osg::StateSet; + stateset->setAttributeAndModes(mat, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + } + else if (visibleRatio < 1.f) + { + const float threshold = 0.6; + visibleRatio = visibleRatio * (1.f - threshold) + threshold; + } + } + + float scale = visibleRatio; + + if (scale == 0.f) + { + // no traverse + return; + } + else if (scale == 1.f) + traverse(node, cv); + else + { + osg::Matrix modelView = *cv->getModelViewMatrix(); + + modelView.preMultScale(osg::Vec3f(scale, scale, scale)); + + if (stateset) + cv->pushStateSet(stateset); + + cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); + + traverse(node, cv); + + cv->popModelViewMatrix(); + + if (stateset) + cv->popStateSet(); + } + } + + void setGlareView(float value) { mGlareView = value; } + + private: + float mGlareView; + }; + + /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between + /// sun and camera. Must be attached as a cull callback to the node above the glare node. + class SunGlareCallback : public OcclusionCallback, + public SceneUtil::NodeCallback + { + public: + SunGlareCallback(osg::ref_ptr oqnVisible, + osg::ref_ptr oqnTotal, osg::ref_ptr sunTransform) + : OcclusionCallback(std::move(oqnVisible), std::move(oqnTotal)) + , mSunTransform(std::move(sunTransform)) + , mTimeOfDayFade(1.f) + , mGlareView(1.f) + { + mColor = Fallback::Map::getColour("Weather_Sun_Glare_Fader_Color"); + mSunGlareFaderMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Max"); + mSunGlareFaderAngleMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Angle_Max"); + + // Replicating a design flaw in MW. The color was being set on both ambient and emissive properties, which + // multiplies the result by two, then finally gets clamped by the fixed function pipeline. With the default + // INI settings, only the red component gets clamped, so the resulting color looks more orange than red. + mColor *= 2; + for (int i = 0; i < 3; ++i) + mColor[i] = std::min(1.f, mColor[i]); + } + + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); + float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + + const float angleMaxRadians = osg::DegreesToRadians(mSunGlareFaderAngleMax); + + float value = 1.f - std::min(1.f, angleRadians / angleMaxRadians); + float fade = value * mSunGlareFaderMax; + + fade *= mTimeOfDayFade * mGlareView * visibleRatio; + + if (fade == 0.f) + { + // no traverse + return; + } + else + { + osg::ref_ptr stateset = new osg::StateSet; + + osg::ref_ptr mat = createUnlitMaterial(); + + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0, 0, 0, fade)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); + + stateset->setAttributeAndModes(mat); + + cv->pushStateSet(stateset); + traverse(node, cv); + cv->popStateSet(); + } + } + + void setTimeOfDayFade(float val) { mTimeOfDayFade = val; } + + void setGlareView(float glareView) { mGlareView = glareView; } + + private: + float getAngleToSunInRadians(const osg::Matrix& viewMatrix) const + { + osg::Vec3d eye, center, up; + viewMatrix.getLookAt(eye, center, up); + + osg::Vec3d forward = center - eye; + osg::Vec3d sun = mSunTransform->getPosition(); + + forward.normalize(); + sun.normalize(); + float angleRadians = std::acos(forward * sun); + return angleRadians; + } + + osg::ref_ptr mSunTransform; + float mTimeOfDayFade; + float mGlareView; + osg::Vec4f mColor; + float mSunGlareFaderMax; + float mSunGlareFaderAngleMax; + }; + + struct MoonUpdater : SceneUtil::StateSetUpdater + { + Resource::ImageManager& mImageManager; + osg::ref_ptr mPhaseTex; + osg::ref_ptr mCircleTex; + float mTransparency; + float mShadowBlend; + osg::Vec4f mAtmosphereColor; + osg::Vec4f mMoonColor; + bool mForceShaders; + + MoonUpdater(Resource::ImageManager& imageManager, bool forceShaders) + : mImageManager(imageManager) + , mPhaseTex() + , mCircleTex() + , mTransparency(1.0f) + , mShadowBlend(1.0f) + , mAtmosphereColor(1.0f, 1.0f, 1.0f, 1.0f) + , mMoonColor(1.0f, 1.0f, 1.0f, 1.0f) + , mForceShaders(forceShaders) + { + } + + void setDefaults(osg::StateSet* stateset) override + { + if (mForceShaders) + { + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Moon))); + stateset->setTextureAttributeAndModes(0, mPhaseTex); + stateset->setTextureAttributeAndModes(1, mCircleTex); + stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("moonBlend", osg::Vec4f{})); + stateset->addUniform(new osg::Uniform("atmosphereFade", osg::Vec4f{})); + stateset->addUniform(new osg::Uniform("diffuseMap", 0)); + stateset->addUniform(new osg::Uniform("maskMap", 1)); + stateset->setAttributeAndModes( + createUnlitMaterial(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + } + else + { + stateset->setTextureAttributeAndModes(0, mPhaseTex); + osg::ref_ptr texEnv = new osg::TexEnvCombine; + texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); + texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); + texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); + texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor + stateset->setTextureAttributeAndModes(0, texEnv); + + stateset->setTextureAttributeAndModes(1, mCircleTex); + osg::ref_ptr texEnv2 = new osg::TexEnvCombine; + texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); + texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnv2->setSource0_Alpha(osg::TexEnvCombine::TEXTURE); + texEnv2->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); + texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency + stateset->setTextureAttributeAndModes(1, texEnv2); + stateset->setAttributeAndModes( + createUnlitMaterial(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + } + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + { + if (mForceShaders) + { + stateset->setTextureAttribute(0, mPhaseTex, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + stateset->setTextureAttribute(1, mCircleTex, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + if (auto* uMoonBlend = stateset->getUniform("moonBlend")) + uMoonBlend->set(mMoonColor * mShadowBlend); + if (auto* uAtmosphereFade = stateset->getUniform("atmosphereFade")) + uAtmosphereFade->set( + osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + } + else + { + osg::TexEnvCombine* texEnv + = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(mMoonColor * mShadowBlend); + + osg::TexEnvCombine* texEnv2 + = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv2->setConstantColor( + osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + } + } + + void setTextures(VFS::Path::NormalizedView phaseTex, VFS::Path::NormalizedView circleTex) + { + mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); + mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mPhaseTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mCircleTex = new osg::Texture2D(mImageManager.getImage(circleTex)); + mCircleTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mCircleTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + reset(); + } + }; + + class CameraRelativeTransformCullCallback + : public SceneUtil::NodeCallback + { + public: + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + // XXX have to remove unwanted culling plane of the water reflection camera + + // Remove all planes that aren't from the standard frustum + unsigned int numPlanes = 4; + if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) + ++numPlanes; + if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) + ++numPlanes; + + unsigned int mask = 0x1; + unsigned int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); + for (unsigned int i = 0; i < cv->getProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) + { + if (i >= numPlanes) + { + // turn off this culling plane + resultMask &= (~mask); + } + + mask <<= 1; + } + + cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); + cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); + + cv->getProjectionCullingStack().back().pushCurrentMask(); + cv->getCurrentCullingSet().pushCurrentMask(); + + traverse(node, cv); + + cv->getProjectionCullingStack().back().popCurrentMask(); + cv->getCurrentCullingSet().popCurrentMask(); + } + }; + + void AtmosphereUpdater::setEmissionColor(const osg::Vec4f& emissionColor) + { + mEmissionColor = emissionColor; + } + + void AtmosphereUpdater::setDefaults(osg::StateSet* stateset) + { + stateset->setAttributeAndModes( + createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere))); + } + + void AtmosphereUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); + } + + AtmosphereNightUpdater::AtmosphereNightUpdater(Resource::ImageManager* imageManager, bool forceShaders) + : mColor(osg::Vec4f(0, 0, 0, 0)) + , mTexture(new osg::Texture2D(imageManager->getWarningImage())) + , mForceShaders(forceShaders) + { + mTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + } + + void AtmosphereNightUpdater::setFade(float fade) + { + mColor.a() = fade; + } + + void AtmosphereNightUpdater::setDefaults(osg::StateSet* stateset) + { + if (mForceShaders) + { + stateset->addUniform(new osg::Uniform("opacity", 0.f)); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere_Night))); + } + else + { + osg::ref_ptr texEnv = new osg::TexEnvCombine; + texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + + stateset->setTextureAttributeAndModes(1, mTexture, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + stateset->setTextureAttributeAndModes(1, texEnv, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + } + } + + void AtmosphereNightUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + if (mForceShaders) + { + stateset->getUniform("opacity")->set(mColor.a()); + } + else + { + osg::TexEnvCombine* texEnv + = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(mColor); + } + } + + CloudUpdater::CloudUpdater(bool forceShaders) + : mOpacity(0.f) + , mForceShaders(forceShaders) + { + } + + void CloudUpdater::setTexture(osg::ref_ptr texture) + { + mTexture = texture; + } + + void CloudUpdater::setEmissionColor(const osg::Vec4f& emissionColor) + { + mEmissionColor = emissionColor; + } + + void CloudUpdater::setOpacity(float opacity) + { + mOpacity = opacity; + } + + void CloudUpdater::setTextureCoord(float timer) + { + mTexMat = osg::Matrixf::translate(osg::Vec3f(0.f, -timer, 0.f)); + } + + void CloudUpdater::setDefaults(osg::StateSet* stateset) + { + stateset->setAttribute( + createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + osg::ref_ptr texmat = new osg::TexMat; + stateset->setTextureAttributeAndModes(0, texmat); + + if (mForceShaders) + { + stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + stateset->addUniform(new osg::Uniform("opacity", 1.f)); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Clouds))); + } + else + { + stateset->setTextureAttributeAndModes(1, texmat); + // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already + osg::ref_ptr texEnvCombine = new osg::TexEnvCombine; + texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnvCombine->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnvCombine->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnvCombine->setConstantColor(osg::Vec4f(1, 1, 1, 1)); + texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); + + stateset->setTextureAttributeAndModes(1, texEnvCombine); + + stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + } + } + + void CloudUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* nv) + { + stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); + + osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); + texMat->setMatrix(mTexMat); + + if (mForceShaders) + { + stateset->getUniform("opacity")->set(mOpacity); + } + else + { + stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); + + osg::TexEnvCombine* texEnv + = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(osg::Vec4f(1, 1, 1, mOpacity)); + } + } + + class SkyStereoStatesetUpdater : public SceneUtil::StateSetUpdater + { + public: + SkyStereoStatesetUpdater() {} + + protected: + void setDefaults(osg::StateSet* stateset) override + { + if (!Stereo::getMultiview()) + stateset->addUniform( + new osg::Uniform(osg::Uniform::FLOAT_MAT4, "projectionMatrix"), osg::StateAttribute::OVERRIDE); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override + { + if (Stereo::getMultiview()) + { + std::array projectionMatrices; + auto& sm = Stereo::Manager::instance(); + + for (int view : { 0, 1 }) + { + auto projectionMatrix = sm.computeEyeProjection(view, SceneUtil::AutoDepth::isReversed()); + auto viewOffsetMatrix = sm.computeEyeViewOffset(view); + for (int col : { 0, 1, 2 }) + viewOffsetMatrix(3, col) = 0; + + projectionMatrices[view] = viewOffsetMatrix * projectionMatrix; + } + + Stereo::setMultiviewMatrices(stateset, projectionMatrices); + } + } + void applyLeft(osg::StateSet* stateset, osgUtil::CullVisitor* /*cv*/) override + { + auto& sm = Stereo::Manager::instance(); + auto* projectionMatrixUniform = stateset->getUniform("projectionMatrix"); + auto projectionMatrix = sm.computeEyeProjection(0, SceneUtil::AutoDepth::isReversed()); + projectionMatrixUniform->set(projectionMatrix); + } + void applyRight(osg::StateSet* stateset, osgUtil::CullVisitor* /*cv*/) override + { + auto& sm = Stereo::Manager::instance(); + auto* projectionMatrixUniform = stateset->getUniform("projectionMatrix"); + auto projectionMatrix = sm.computeEyeProjection(1, SceneUtil::AutoDepth::isReversed()); + projectionMatrixUniform->set(projectionMatrix); + } + + private: + }; + + CameraRelativeTransform::CameraRelativeTransform() + { + // Culling works in node-local space, not in camera space, so we can't cull this node correctly + // That's not a problem though, children of this node can be culled just fine + // Just make sure you do not place a CameraRelativeTransform deep in the scene graph + setCullingActive(false); + + addCullCallback(new CameraRelativeTransformCullCallback); + if (Stereo::getStereo()) + addCullCallback(new SkyStereoStatesetUpdater); + } + + CameraRelativeTransform::CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) + : osg::Transform(copy, copyop) + { + } + + const osg::Vec3f& CameraRelativeTransform::getLastViewPoint() const + { + return mViewPoint; + } + + bool CameraRelativeTransform::computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const + { + if (nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) + { + mViewPoint = static_cast(nv)->getViewPoint(); + } + + if (_referenceFrame == RELATIVE_RF) + { + matrix.setTrans(osg::Vec3f(0.f, 0.f, 0.f)); + return false; + } + else // absolute + { + matrix.makeIdentity(); + return true; + } + } + + osg::BoundingSphere CameraRelativeTransform::computeBound() const + { + return osg::BoundingSphere(); + } + + UnderwaterSwitchCallback::UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) + : mCameraRelativeTransform(cameraRelativeTransform) + , mEnabled(true) + , mWaterLevel(0.f) + { + } + + bool UnderwaterSwitchCallback::isUnderwater() + { + osg::Vec3f viewPoint = mCameraRelativeTransform->getLastViewPoint(); + return mEnabled && viewPoint.z() < mWaterLevel; + } + + void UnderwaterSwitchCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) + { + if (isUnderwater()) + return; + + traverse(node, nv); + } + + void UnderwaterSwitchCallback::setEnabled(bool enabled) + { + mEnabled = enabled; + } + void UnderwaterSwitchCallback::setWaterLevel(float waterLevel) + { + mWaterLevel = waterLevel; + } + + const float CelestialBody::mDistance = 1000.0f; + + CelestialBody::CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask) + : mVisibleMask(visibleMask) + { + mGeom = createTexturedQuad(numUvSets); + mGeom->getOrCreateStateSet(); + mTransform = new osg::PositionAttitudeTransform; + mTransform->setNodeMask(mVisibleMask); + mTransform->setScale(osg::Vec3f(450, 450, 450) * scaleFactor); + mTransform->addChild(mGeom); + + parentNode->addChild(mTransform); + } + + void CelestialBody::setVisible(bool visible) + { + mTransform->setNodeMask(visible ? mVisibleMask : 0); + } + + Sun::Sun(osg::Group* parentNode, Resource::SceneManager& sceneManager) + : CelestialBody(parentNode, 1.0f, 1, Mask_Sun) + , mUpdater(new SunUpdater) + { + mTransform->addUpdateCallback(mUpdater); + + Resource::ImageManager& imageManager = *sceneManager.getImageManager(); + + constexpr VFS::Path::NormalizedView image("textures/tx_sun_05.dds"); + + osg::ref_ptr sunTex = new osg::Texture2D(imageManager.getImage(image)); + sunTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex); + mGeom->getOrCreateStateSet()->setTextureAttributeAndModes( + 0, new SceneUtil::TextureType("diffuseMap"), osg::StateAttribute::ON); + mGeom->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun))); + + osg::ref_ptr queryNode = new osg::Group; + // Need to render after the world geometry so we can correctly test for occlusions + osg::StateSet* stateset = queryNode->getOrCreateStateSet(); + stateset->setRenderBinDetails(RenderBin_OcclusionQuery, "RenderBin"); + stateset->setNestRenderBins(false); + // Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to + // match the circular shape of the sun + if (!sceneManager.getForceShaders()) + { + osg::ref_ptr alphaFunc = new osg::AlphaFunc; + alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); + stateset->setAttributeAndModes(alphaFunc); + } + stateset->setTextureAttributeAndModes(0, sunTex); + stateset->setTextureAttributeAndModes(0, new SceneUtil::TextureType("diffuseMap"), osg::StateAttribute::ON); + stateset->setAttributeAndModes(createUnlitMaterial()); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunflash_Query))); + + // Disable writing to the color buffer. We are using this geometry for visibility tests only. + osg::ref_ptr colormask = new osg::ColorMask(0, 0, 0, 0); + stateset->setAttributeAndModes(colormask); + sceneManager.setUpNormalsRTForStateSet(stateset, false); + mTransform->addChild(queryNode); + + mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); + mOcclusionQueryTotalPixels = createOcclusionQueryNode(queryNode, false); + + createSunFlash(imageManager); + createSunGlare(); + } + + Sun::~Sun() + { + mTransform->removeUpdateCallback(mUpdater); + destroySunFlash(); + destroySunGlare(); + } + + void Sun::setColor(const osg::Vec4f& color) + { + mUpdater->mColor.r() = color.r(); + mUpdater->mColor.g() = color.g(); + mUpdater->mColor.b() = color.b(); + } + + void Sun::adjustTransparency(const float ratio) + { + mUpdater->mColor.a() = ratio; + if (mSunGlareCallback) + mSunGlareCallback->setGlareView(ratio); + if (mSunFlashCallback) + mSunFlashCallback->setGlareView(ratio); + } + + void Sun::setDirection(const osg::Vec3f& direction) + { + osg::Vec3f normalizedDirection = direction / direction.length(); + mTransform->setPosition(normalizedDirection * mDistance); + + osg::Quat quat; + quat.makeRotate(osg::Vec3f(0.0f, 0.0f, 1.0f), normalizedDirection); + mTransform->setAttitude(quat); + } + + void Sun::setGlareTimeOfDayFade(float val) + { + if (mSunGlareCallback) + mSunGlareCallback->setTimeOfDayFade(val); + } + + void Sun::setSunglare(bool enabled) + { + mSunGlareNode->setNodeMask(enabled ? ~0u : 0); + mSunFlashNode->setNodeMask(enabled ? ~0u : 0); + } + + osg::ref_ptr Sun::createOcclusionQueryNode(osg::Group* parent, bool queryVisible) + { + osg::ref_ptr oqn = new osg::OcclusionQueryNode; + oqn->setQueriesEnabled(true); + + // With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced + osg::ref_ptr queryGeom = new osg::QueryGeometry(oqn->getName()); + + // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is + // rendered after all the other geometry, so that would be pretty bad). STATIC should be safe, since our node's + // local bounds are static, thus computeBounds() which modifies the queryGeometry is only called once. Note the + // debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. + queryGeom->setDataVariance(osg::Object::STATIC); + + // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't + // originally intended to allow this, normally it would automatically adjust the query geometry to match the sub + // graph's bounding box. The below hack is needed to circumvent this. + queryGeom->setVertexArray(mGeom->getVertexArray()); + queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); + queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets()); + queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); + + // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. + oqn->setComputeBoundingSphereCallback(new DummyComputeBoundCallback); + // Still need a proper bounding sphere. + oqn->setInitialBound(queryGeom->getBound()); + + oqn->setQueryGeometry(queryGeom.release()); + + osg::StateSet* queryStateSet = new osg::StateSet; + if (queryVisible) + { + osg::ref_ptr depth = new SceneUtil::AutoDepth(osg::Depth::LEQUAL); + // This is a trick to make fragments written by the query always use the maximum depth value, + // without having to retrieve the current far clipping distance. + // We want the sun glare to be "infinitely" far away. + double far = SceneUtil::AutoDepth::isReversed() ? 0.0 : 1.0; + depth->setFunction(osg::Depth::LEQUAL); + depth->setZNear(far); + depth->setZFar(far); + depth->setWriteMask(false); + queryStateSet->setAttributeAndModes(depth); + } + else + { + queryStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + } + oqn->setQueryStateSet(queryStateSet); + + parent->addChild(oqn); + + return oqn; + } + + void Sun::createSunFlash(Resource::ImageManager& imageManager) + { + constexpr VFS::Path::NormalizedView image("textures/tx_sun_flash_grey_05.dds"); + osg::ref_ptr tex = new osg::Texture2D(imageManager.getImage(image)); + tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + osg::ref_ptr group(new osg::Group); + + mTransform->addChild(group); + + const float scale = 2.6f; + osg::ref_ptr geom = createTexturedQuad(1, scale); + group->addChild(geom); + + osg::StateSet* stateset = geom->getOrCreateStateSet(); + + stateset->setTextureAttributeAndModes(0, tex); + stateset->setTextureAttributeAndModes(0, new SceneUtil::TextureType("diffuseMap"), osg::StateAttribute::ON); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); + stateset->setNestRenderBins(false); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun))); + + mSunFlashNode = group; + + mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); + mSunFlashNode->addCullCallback(mSunFlashCallback); + } + + void Sun::destroySunFlash() + { + if (mSunFlashNode) + { + mSunFlashNode->removeCullCallback(mSunFlashCallback); + mSunFlashCallback = nullptr; + } + } + + void Sun::createSunGlare() + { + osg::ref_ptr camera = new osg::Camera; + camera->setProjectionMatrix(osg::Matrix::identity()); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // add to skyRoot instead? + camera->setViewMatrix(osg::Matrix::identity()); + camera->setClearMask(0); + camera->setRenderOrder(osg::Camera::NESTED_RENDER); + camera->setAllowEventFocus(false); + camera->getOrCreateStateSet()->addUniform( + new osg::Uniform("projectionMatrix", static_cast(camera->getProjectionMatrix()))); + SceneUtil::setCameraClearDepth(camera); + + osg::ref_ptr geom + = osg::createTexturedQuadGeometry(osg::Vec3f(-1, -1, 0), osg::Vec3f(2, 0, 0), osg::Vec3f(0, 2, 0)); + camera->addChild(geom); + + osg::StateSet* stateset = geom->getOrCreateStateSet(); + + stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); + stateset->setNestRenderBins(false); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunglare))); + + // set up additive blending + osg::ref_ptr blendFunc = new osg::BlendFunc; + blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); + blendFunc->setDestination(osg::BlendFunc::ONE); + stateset->setAttributeAndModes(blendFunc); + + mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); + mSunGlareNode = camera; + + mSunGlareNode->addCullCallback(mSunGlareCallback); + + mTransform->addChild(camera); + } + + void Sun::destroySunGlare() + { + if (mSunGlareNode) + { + mSunGlareNode->removeCullCallback(mSunGlareCallback); + mSunGlareCallback = nullptr; + } + } + + Moon::Moon(osg::Group* parentNode, Resource::SceneManager& sceneManager, float scaleFactor, Type type) + : CelestialBody(parentNode, scaleFactor, 2) + , mType(type) + , mPhase(MoonState::Phase::Unspecified) + , mUpdater(new MoonUpdater(*sceneManager.getImageManager(), sceneManager.getForceShaders())) + { + setPhase(MoonState::Phase::Full); + setVisible(true); + + mGeom->addUpdateCallback(mUpdater); + } + + Moon::~Moon() + { + mGeom->removeUpdateCallback(mUpdater); + } + + void Moon::adjustTransparency(const float ratio) + { + mUpdater->mTransparency *= ratio; + } + + void Moon::setState(const MoonState state) + { + float radsX = ((state.mRotationFromHorizon) * static_cast(osg::PI)) / 180.0f; + float radsZ = ((state.mRotationFromNorth) * static_cast(osg::PI)) / 180.0f; + + osg::Quat rotX(radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); + osg::Quat rotZ(radsZ, osg::Vec3f(0.0f, 0.0f, 1.0f)); + + osg::Vec3f direction = rotX * rotZ * osg::Vec3f(0.0f, 1.0f, 0.0f); + mTransform->setPosition(direction * mDistance); + + // The moon quad is initially oriented facing down, so we need to offset its X-axis + // rotation to rotate it to face the camera when sitting at the horizon. + osg::Quat attX((-static_cast(osg::PI) / 2.0f) + radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); + mTransform->setAttitude(attX * rotZ); + + setPhase(state.mPhase); + mUpdater->mTransparency = state.mMoonAlpha; + mUpdater->mShadowBlend = state.mShadowBlend; + } + + void Moon::setAtmosphereColor(const osg::Vec4f& color) + { + mUpdater->mAtmosphereColor = color; + } + + void Moon::setColor(const osg::Vec4f& color) + { + mUpdater->mMoonColor = color; + } + + unsigned int Moon::getPhaseInt() const + { + switch (mPhase) + { + case MoonState::Phase::New: + return 0; + case MoonState::Phase::WaxingCrescent: + return 1; + case MoonState::Phase::WaningCrescent: + return 1; + case MoonState::Phase::FirstQuarter: + return 2; + case MoonState::Phase::ThirdQuarter: + return 2; + case MoonState::Phase::WaxingGibbous: + return 3; + case MoonState::Phase::WaningGibbous: + return 3; + case MoonState::Phase::Full: + return 4; + default: + return 0; + } + } + + void Moon::setPhase(const MoonState::Phase& phase) + { + if (mPhase == phase) + return; + + mPhase = phase; + + std::string textureName = "textures/tx_"; + + if (mType == Moon::Type_Secunda) + textureName += "secunda_"; + else + textureName += "masser_"; + + switch (mPhase) + { + case MoonState::Phase::New: + textureName += "new"; + break; + case MoonState::Phase::WaxingCrescent: + textureName += "one_wax"; + break; + case MoonState::Phase::FirstQuarter: + textureName += "half_wax"; + break; + case MoonState::Phase::WaxingGibbous: + textureName += "three_wax"; + break; + case MoonState::Phase::WaningCrescent: + textureName += "one_wan"; + break; + case MoonState::Phase::ThirdQuarter: + textureName += "half_wan"; + break; + case MoonState::Phase::WaningGibbous: + textureName += "three_wan"; + break; + case MoonState::Phase::Full: + textureName += "full"; + break; + default: + break; + } + + textureName += ".dds"; + + const VFS::Path::Normalized texturePath(std::move(textureName)); + + if (mType == Moon::Type_Secunda) + { + constexpr VFS::Path::NormalizedView secunda("textures/tx_mooncircle_full_s.dds"); + mUpdater->setTextures(texturePath, secunda); + } + else + { + constexpr VFS::Path::NormalizedView masser("textures/tx_mooncircle_full_m.dds"); + mUpdater->setTextures(texturePath, masser); + } + } + + int RainCounter::numParticlesToCreate(double dt) const + { + // limit dt to avoid large particle emissions if there are jumps in the simulation time + // 0.2 seconds is the same cap as used in Engine's frame loop + dt = std::min(dt, 0.2); + return ConstantRateCounter::numParticlesToCreate(dt); + } + + RainShooter::RainShooter() + : mAngle(0.f) + { + } + + void RainShooter::shoot(osgParticle::Particle* particle) const + { + particle->setVelocity(mVelocity); + particle->setAngle(osg::Vec3f(-mAngle, 0, (Misc::Rng::rollProbability() * 2 - 1) * osg::PI)); + } + + void RainShooter::setVelocity(const osg::Vec3f& velocity) + { + mVelocity = velocity; + } + + void RainShooter::setAngle(float angle) + { + mAngle = angle; + } + + osg::Object* RainShooter::cloneType() const + { + return new RainShooter; + } + + osg::Object* RainShooter::clone(const osg::CopyOp&) const + { + return new RainShooter(*this); + } + + ModVertexAlphaVisitor::ModVertexAlphaVisitor(ModVertexAlphaVisitor::MeshType type) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mType(type) + { + } + + void ModVertexAlphaVisitor::apply(osg::Geometry& geometry) + { + osg::ref_ptr colors = new osg::Vec4Array(geometry.getVertexArray()->getNumElements()); + for (unsigned int i = 0; i < colors->size(); ++i) + { + float alpha = 1.f; + + switch (mType) + { + case ModVertexAlphaVisitor::Atmosphere: + { + // this is a cylinder, so every second vertex belongs to the bottom-most row + alpha = (i % 2) ? 0.f : 1.f; + break; + } + case ModVertexAlphaVisitor::Clouds: + { + if (i >= 49 && i <= 64) + alpha = 0.f; // bottom-most row + else if (i >= 33 && i <= 48) + alpha = 0.25098; // second row + else + alpha = 1.f; + break; + } + case ModVertexAlphaVisitor::Stars: + { + if (geometry.getColorArray()) + { + osg::Vec4Array* origColors = static_cast(geometry.getColorArray()); + alpha = ((*origColors)[i].x() == 1.f) ? 1.f : 0.f; + } + else + alpha = 1.f; + break; + } + } + + (*colors)[i] = osg::Vec4f(0.f, 0.f, 0.f, alpha); + } + + geometry.setColorArray(colors, osg::Array::BIND_PER_VERTEX); + } +} diff --git a/apps/openmw/mwrender/skyutil.hpp b/apps/openmw/mwrender/skyutil.hpp new file mode 100644 index 00000000000..da038e6c58a --- /dev/null +++ b/apps/openmw/mwrender/skyutil.hpp @@ -0,0 +1,349 @@ +#ifndef OPENMW_MWRENDER_SKYUTIL_H +#define OPENMW_MWRENDER_SKYUTIL_H + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace Resource +{ + class ImageManager; + class SceneManager; +} + +namespace MWRender +{ + struct MoonUpdater; + class SunUpdater; + class SunFlashCallback; + class SunGlareCallback; + + struct WeatherResult + { + std::string mCloudTexture; + std::string mNextCloudTexture; + float mCloudBlendFactor; + + osg::Vec4f mFogColor; + + osg::Vec4f mAmbientColor; + + osg::Vec4f mSkyColor; + + // sun light color + osg::Vec4f mSunColor; + + // alpha is the sun transparency + osg::Vec4f mSunDiscColor; + + float mFogDepth; + + float mDLFogFactor; + float mDLFogOffset; + + float mWindSpeed; + float mBaseWindSpeed; + float mCurrentWindSpeed; + float mNextWindSpeed; + + float mCloudSpeed; + + float mGlareView; + + bool mNight; // use night skybox + float mNightFade; // fading factor for night skybox + + bool mIsStorm; + + ESM::RefId mAmbientLoopSoundID; + ESM::RefId mRainLoopSoundID; + float mAmbientSoundVolume; + + std::string mParticleEffect; + std::string mRainEffect; + float mPrecipitationAlpha; + + float mRainDiameter; + float mRainMinHeight; + float mRainMaxHeight; + float mRainSpeed; + float mRainEntranceSpeed; + int mRainMaxRaindrops; + + osg::Vec3f mStormDirection; + osg::Vec3f mNextStormDirection; + }; + + struct MoonState + { + enum class Phase + { + Full, + WaningGibbous, + ThirdQuarter, + WaningCrescent, + New, + WaxingCrescent, + FirstQuarter, + WaxingGibbous, + Unspecified + }; + + float mRotationFromHorizon; + float mRotationFromNorth; + Phase mPhase; + float mShadowBlend; + float mMoonAlpha; + }; + + osg::ref_ptr createAlphaTrackingUnlitMaterial(); + osg::ref_ptr createUnlitMaterial(osg::Material::ColorMode colorMode = osg::Material::OFF); + + class OcclusionCallback + { + public: + OcclusionCallback( + osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal); + + protected: + float getVisibleRatio(osg::Camera* camera); + + private: + osg::ref_ptr mOcclusionQueryVisiblePixels; + osg::ref_ptr mOcclusionQueryTotalPixels; + + std::map, float> mLastRatio; + }; + + class AtmosphereUpdater : public SceneUtil::StateSetUpdater + { + public: + void setEmissionColor(const osg::Vec4f& emissionColor); + + protected: + void setDefaults(osg::StateSet* stateset) override; + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override; + + private: + osg::Vec4f mEmissionColor; + }; + + class AtmosphereNightUpdater : public SceneUtil::StateSetUpdater + { + public: + AtmosphereNightUpdater(Resource::ImageManager* imageManager, bool forceShaders); + + void setFade(float fade); + + protected: + void setDefaults(osg::StateSet* stateset) override; + + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override; + + private: + osg::Vec4f mColor; + osg::ref_ptr mTexture; + bool mForceShaders; + }; + + class CloudUpdater : public SceneUtil::StateSetUpdater + { + public: + CloudUpdater(bool forceShaders); + + void setTexture(osg::ref_ptr texture); + + void setEmissionColor(const osg::Vec4f& emissionColor); + void setOpacity(float opacity); + void setTextureCoord(float timer); + + protected: + void setDefaults(osg::StateSet* stateset) override; + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override; + + private: + osg::ref_ptr mTexture; + osg::Vec4f mEmissionColor; + float mOpacity; + bool mForceShaders; + osg::Matrixf mTexMat; + }; + + /// Transform that removes the eyepoint of the modelview matrix, + /// i.e. its children are positioned relative to the camera. + class CameraRelativeTransform : public osg::Transform + { + public: + CameraRelativeTransform(); + + CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop); + + META_Node(MWRender, CameraRelativeTransform) + + const osg::Vec3f& getLastViewPoint() const; + + bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const override; + + osg::BoundingSphere computeBound() const override; + + private: + // viewPoint for the current frame + mutable osg::Vec3f mViewPoint; + }; + + /// @brief Hides the node subgraph if the eye point is below water. + /// @note Must be added as cull callback. + /// @note Meant to be used on a node that is child of a CameraRelativeTransform. + /// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we + /// are in camera-relative space. + class UnderwaterSwitchCallback : public SceneUtil::NodeCallback + { + public: + UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform); + bool isUnderwater(); + + void operator()(osg::Node* node, osg::NodeVisitor* nv); + void setEnabled(bool enabled); + void setWaterLevel(float waterLevel); + + private: + osg::ref_ptr mCameraRelativeTransform; + bool mEnabled; + float mWaterLevel; + }; + + /// A base class for the sun and moons. + class CelestialBody + { + public: + CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask = ~0u); + + virtual ~CelestialBody() = default; + + virtual void adjustTransparency(const float ratio) = 0; + + void setVisible(bool visible); + + protected: + unsigned int mVisibleMask; + static const float mDistance; + osg::ref_ptr mTransform; + osg::ref_ptr mGeom; + }; + + class Sun : public CelestialBody + { + public: + Sun(osg::Group* parentNode, Resource::SceneManager& sceneManager); + + ~Sun(); + + void setColor(const osg::Vec4f& color); + void adjustTransparency(const float ratio) override; + + void setDirection(const osg::Vec3f& direction); + void setGlareTimeOfDayFade(float val); + void setSunglare(bool enabled); + + private: + /// @param queryVisible If true, queries the amount of visible pixels. If false, queries the total amount of + /// pixels. + osg::ref_ptr createOcclusionQueryNode(osg::Group* parent, bool queryVisible); + + void createSunFlash(Resource::ImageManager& imageManager); + void destroySunFlash(); + + void createSunGlare(); + void destroySunGlare(); + + osg::ref_ptr mUpdater; + osg::ref_ptr mSunFlashNode; + osg::ref_ptr mSunGlareNode; + osg::ref_ptr mSunFlashCallback; + osg::ref_ptr mSunGlareCallback; + osg::ref_ptr mOcclusionQueryVisiblePixels; + osg::ref_ptr mOcclusionQueryTotalPixels; + }; + + class Moon : public CelestialBody + { + public: + enum Type + { + Type_Masser = 0, + Type_Secunda + }; + + Moon(osg::Group* parentNode, Resource::SceneManager& sceneManager, float scaleFactor, Type type); + + ~Moon(); + + void adjustTransparency(const float ratio) override; + void setState(const MoonState state); + void setAtmosphereColor(const osg::Vec4f& color); + void setColor(const osg::Vec4f& color); + + unsigned int getPhaseInt() const; + + private: + Type mType; + MoonState::Phase mPhase; + osg::ref_ptr mUpdater; + + void setPhase(const MoonState::Phase& phase); + }; + + class RainCounter : public osgParticle::ConstantRateCounter + { + public: + int numParticlesToCreate(double dt) const override; + }; + + class RainShooter : public osgParticle::Shooter + { + public: + RainShooter(); + + osg::Object* cloneType() const override; + + osg::Object* clone(const osg::CopyOp&) const override; + + void shoot(osgParticle::Particle* particle) const override; + + void setVelocity(const osg::Vec3f& velocity); + void setAngle(float angle); + + private: + osg::Vec3f mVelocity; + float mAngle; + }; + + class ModVertexAlphaVisitor : public osg::NodeVisitor + { + public: + enum MeshType + { + Atmosphere, + Stars, + Clouds + }; + + ModVertexAlphaVisitor(MeshType type); + + void apply(osg::Geometry& geometry) override; + + private: + MeshType mType; + }; +} + +#endif diff --git a/apps/openmw/mwrender/terrainstorage.cpp b/apps/openmw/mwrender/terrainstorage.cpp index 528ce70ea33..9776d7e6323 100644 --- a/apps/openmw/mwrender/terrainstorage.cpp +++ b/apps/openmw/mwrender/terrainstorage.cpp @@ -1,6 +1,8 @@ #include "terrainstorage.hpp" -#include "../mwbase/world.hpp" +#include +#include + #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" @@ -9,9 +11,13 @@ namespace MWRender { - TerrainStorage::TerrainStorage(Resource::ResourceSystem* resourceSystem, const std::string& normalMapPattern, const std::string& normalHeightMapPattern, bool autoUseNormalMaps, const std::string& specularMapPattern, bool autoUseSpecularMaps) - : ESMTerrain::Storage(resourceSystem->getVFS(), normalMapPattern, normalHeightMapPattern, autoUseNormalMaps, specularMapPattern, autoUseSpecularMaps) - , mLandManager(new LandManager(ESM::Land::DATA_VCLR|ESM::Land::DATA_VHGT|ESM::Land::DATA_VNML|ESM::Land::DATA_VTEX)) + TerrainStorage::TerrainStorage(Resource::ResourceSystem* resourceSystem, std::string_view normalMapPattern, + std::string_view normalHeightMapPattern, bool autoUseNormalMaps, std::string_view specularMapPattern, + bool autoUseSpecularMaps) + : ESMTerrain::Storage(resourceSystem->getVFS(), normalMapPattern, normalHeightMapPattern, autoUseNormalMaps, + specularMapPattern, autoUseSpecularMaps) + , mLandManager(new LandManager( + ESM::Land::DATA_VCLR | ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VTEX)) , mResourceSystem(resourceSystem) { mResourceSystem->addResourceManager(mLandManager.get()); @@ -22,56 +28,87 @@ namespace MWRender mResourceSystem->removeResourceManager(mLandManager.get()); } - bool TerrainStorage::hasData(int cellX, int cellY) + bool TerrainStorage::hasData(ESM::ExteriorCellLocation cellLocation) { - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); + + if (ESM::isEsm4Ext(cellLocation.mWorldspace)) + { + const ESM4::World* worldspace = esmStore.get().find(cellLocation.mWorldspace); + if (!worldspace->mParent.isZeroOrUnset() && worldspace->mParentUseFlags & ESM4::World::UseFlag_Land) + cellLocation.mWorldspace = worldspace->mParent; - const ESM::Land* land = esmStore.get().search(cellX, cellY); - return land != nullptr; + return esmStore.get().search(cellLocation) != nullptr; + } + else + { + return esmStore.get().search(cellLocation.mX, cellLocation.mY) != nullptr; + } } - void TerrainStorage::getBounds(float& minX, float& maxX, float& minY, float& maxY) + static void BoundUnion(float& minX, float& maxX, float& minY, float& maxY, float x, float y) { - minX = 0, minY = 0, maxX = 0, maxY = 0; + if (x < minX) + minX = x; + if (x > maxX) + maxX = x; + if (y < minY) + minY = y; + if (y > maxY) + maxY = y; + } - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); + void TerrainStorage::getBounds(float& minX, float& maxX, float& minY, float& maxY, ESM::RefId worldspace) + { + minX = 0; + minY = 0; + maxX = 0; + maxY = 0; + + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); - MWWorld::Store::iterator it = esmStore.get().begin(); - for (; it != esmStore.get().end(); ++it) + if (ESM::isEsm4Ext(worldspace)) { - if (it->mX < minX) - minX = static_cast(it->mX); - if (it->mX > maxX) - maxX = static_cast(it->mX); - if (it->mY < minY) - minY = static_cast(it->mY); - if (it->mY > maxY) - maxY = static_cast(it->mY); + const ESM4::World* worldRec = esmStore.get().find(worldspace); + if (!worldRec->mParent.isZeroOrUnset() && worldRec->mParentUseFlags & ESM4::World::UseFlag_Land) + worldspace = worldRec->mParent; + + const auto& lands = esmStore.get().getLands(); + for (const auto& [landPos, _] : lands) + { + if (landPos.mWorldspace == worldspace) + { + BoundUnion(minX, maxX, minY, maxY, static_cast(landPos.mX), static_cast(landPos.mY)); + } + } + } + else + { + MWWorld::Store::iterator it = esmStore.get().begin(); + for (; it != esmStore.get().end(); ++it) + { + BoundUnion(minX, maxX, minY, maxY, static_cast(it->mX), static_cast(it->mY)); + } } - // since grid coords are at cell origin, we need to add 1 cell maxX += 1; maxY += 1; } - LandManager *TerrainStorage::getLandManager() const + LandManager* TerrainStorage::getLandManager() const { return mLandManager.get(); } - osg::ref_ptr TerrainStorage::getLand(int cellX, int cellY) + osg::ref_ptr TerrainStorage::getLand(ESM::ExteriorCellLocation cellLocation) { - return mLandManager->getLand(cellX, cellY); + return mLandManager->getLand(cellLocation); } - const ESM::LandTexture* TerrainStorage::getLandTexture(int index, short plugin) + const std::string* TerrainStorage::getLandTexture(std::uint16_t index, int plugin) { - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); return esmStore.get().search(index, plugin); } - } diff --git a/apps/openmw/mwrender/terrainstorage.hpp b/apps/openmw/mwrender/terrainstorage.hpp index 90bf42b841a..731f3967133 100644 --- a/apps/openmw/mwrender/terrainstorage.hpp +++ b/apps/openmw/mwrender/terrainstorage.hpp @@ -16,27 +16,27 @@ namespace MWRender class TerrainStorage : public ESMTerrain::Storage { public: - - TerrainStorage(Resource::ResourceSystem* resourceSystem, const std::string& normalMapPattern = "", const std::string& normalHeightMapPattern = "", bool autoUseNormalMaps = false, const std::string& specularMapPattern = "", bool autoUseSpecularMaps = false); + TerrainStorage(Resource::ResourceSystem* resourceSystem, std::string_view normalMapPattern = {}, + std::string_view normalHeightMapPattern = {}, bool autoUseNormalMaps = false, + std::string_view specularMapPattern = {}, bool autoUseSpecularMaps = false); ~TerrainStorage(); - osg::ref_ptr getLand (int cellX, int cellY) override; - const ESM::LandTexture* getLandTexture(int index, short plugin) override; + osg::ref_ptr getLand(ESM::ExteriorCellLocation cellLocation) override; + const std::string* getLandTexture(std::uint16_t index, int plugin) override; - bool hasData(int cellX, int cellY) override; + bool hasData(ESM::ExteriorCellLocation cellLocation) override; /// Get bounds of the whole terrain in cell units - void getBounds(float& minX, float& maxX, float& minY, float& maxY) override; + void getBounds(float& minX, float& maxX, float& minY, float& maxY, ESM::RefId worldspace) override; LandManager* getLandManager() const; private: - std::unique_ptr mLandManager; + std::unique_ptr mLandManager; - Resource::ResourceSystem* mResourceSystem; + Resource::ResourceSystem* mResourceSystem; }; } - #endif diff --git a/apps/openmw/mwrender/transparentpass.cpp b/apps/openmw/mwrender/transparentpass.cpp new file mode 100644 index 00000000000..ce53b1c2193 --- /dev/null +++ b/apps/openmw/mwrender/transparentpass.cpp @@ -0,0 +1,143 @@ +#include "transparentpass.hpp" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "vismask.hpp" + +namespace MWRender +{ + TransparentDepthBinCallback::TransparentDepthBinCallback(Shader::ShaderManager& shaderManager, bool postPass) + : mStateSet(new osg::StateSet) + , mPostPass(postPass) + { + osg::ref_ptr image = new osg::Image; + image->allocateImage(1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE); + image->setColor(osg::Vec4(1, 1, 1, 1), 0, 0); + + osg::ref_ptr dummyTexture = new osg::Texture2D(image); + dummyTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + dummyTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + constexpr osg::StateAttribute::OverrideValue modeOff = osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE; + constexpr osg::StateAttribute::OverrideValue modeOn = osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE; + + mStateSet->setTextureAttributeAndModes(0, dummyTexture); + + Shader::ShaderManager::DefineMap defines; + Stereo::shaderStereoDefines(defines); + + mStateSet->setAttributeAndModes(new osg::BlendFunc, modeOff); + mStateSet->setAttributeAndModes(shaderManager.getProgram("depthclipped", defines), modeOn); + mStateSet->setAttributeAndModes(new SceneUtil::AutoDepth, modeOn); + + for (unsigned int unit = 1; unit < 8; ++unit) + mStateSet->setTextureMode(unit, GL_TEXTURE_2D, modeOff); + } + + void TransparentDepthBinCallback::drawImplementation( + osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) + { + osg::State& state = *renderInfo.getState(); + osg::GLExtensions* ext = state.get(); + + bool validFbo = false; + unsigned int frameId = state.getFrameStamp()->getFrameNumber() % 2; + + const auto& fbo = mFbo[frameId]; + const auto& msaaFbo = mMsaaFbo[frameId]; + const auto& opaqueFbo = mOpaqueFbo[frameId]; + + if (bin->getStage()->getMultisampleResolveFramebufferObject() + && bin->getStage()->getMultisampleResolveFramebufferObject() == fbo) + validFbo = true; + else if (bin->getStage()->getFrameBufferObject() + && (bin->getStage()->getFrameBufferObject() == fbo || bin->getStage()->getFrameBufferObject() == msaaFbo)) + validFbo = true; + + if (!validFbo) + { + bin->drawImplementation(renderInfo, previous); + return; + } + + const osg::Texture* tex + = opaqueFbo->getAttachment(osg::FrameBufferObject::BufferComponent::PACKED_DEPTH_STENCIL_BUFFER) + .getTexture(); + + if (Stereo::getMultiview()) + { + if (!mMultiviewResolve[frameId]) + { + mMultiviewResolve[frameId] = std::make_unique( + msaaFbo ? msaaFbo : fbo, opaqueFbo, GL_DEPTH_BUFFER_BIT); + } + else + { + mMultiviewResolve[frameId]->setResolveFbo(opaqueFbo); + mMultiviewResolve[frameId]->setMsaaFbo(msaaFbo ? msaaFbo : fbo); + } + mMultiviewResolve[frameId]->resolveImplementation(state); + } + else + { + opaqueFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + ext->glBlitFramebuffer(0, 0, tex->getTextureWidth(), tex->getTextureHeight(), 0, 0, tex->getTextureWidth(), + tex->getTextureHeight(), GL_DEPTH_BUFFER_BIT, GL_NEAREST); + } + + msaaFbo ? msaaFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER) + : fbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + + // draws scene into primary attachments + bin->drawImplementation(renderInfo, previous); + + if (!mPostPass) + return; + + opaqueFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + + // draw transparent post-pass to populate a postprocess friendly depth texture with alpha-clipped geometry + + unsigned int numToPop = previous ? osgUtil::StateGraph::numToPop(previous->_parent) : 0; + if (numToPop > 1) + numToPop--; + unsigned int insertStateSetPosition = state.getStateSetStackSize() - numToPop; + + state.insertStateSet(insertStateSetPosition, mStateSet); + for (auto rit = bin->getRenderLeafList().begin(); rit != bin->getRenderLeafList().end(); rit++) + { + osgUtil::RenderLeaf* rl = *rit; + const osg::StateSet* ss = rl->_parent->getStateSet(); + + if (rl->_drawable->getNodeMask() == Mask_ParticleSystem || rl->_drawable->getNodeMask() == Mask_Effect) + continue; + + if (ss->getAttribute(osg::StateAttribute::MATERIAL)) + { + const osg::Material* mat + = static_cast(ss->getAttribute(osg::StateAttribute::MATERIAL)); + if (mat->getDiffuse(osg::Material::FRONT).a() < 0.5) + continue; + } + + rl->render(renderInfo, previous); + previous = rl; + } + state.removeStateSet(insertStateSetPosition); + + msaaFbo ? msaaFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER) + : fbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + state.checkGLErrors("after TransparentDepthBinCallback::drawImplementation"); + } +} diff --git a/apps/openmw/mwrender/transparentpass.hpp b/apps/openmw/mwrender/transparentpass.hpp new file mode 100644 index 00000000000..9d727cf08d2 --- /dev/null +++ b/apps/openmw/mwrender/transparentpass.hpp @@ -0,0 +1,45 @@ +#ifndef OPENMW_MWRENDER_TRANSPARENTPASS_H +#define OPENMW_MWRENDER_TRANSPARENTPASS_H + +#include +#include + +#include +#include + +#include + +namespace Shader +{ + class ShaderManager; +} + +namespace Stereo +{ + class MultiviewFramebufferResolve; +} + +namespace MWRender +{ + class TransparentDepthBinCallback : public osgUtil::RenderBin::DrawCallback + { + public: + TransparentDepthBinCallback(Shader::ShaderManager& shaderManager, bool postPass); + + void drawImplementation( + osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override; + + std::array, 2> mFbo; + std::array, 2> mMsaaFbo; + std::array, 2> mOpaqueFbo; + + std::array, 2> mMultiviewResolve; + + private: + osg::ref_ptr mStateSet; + bool mPostPass; + }; + +} + +#endif diff --git a/apps/openmw/mwrender/util.cpp b/apps/openmw/mwrender/util.cpp index e3fc48040fd..c2231c31f81 100644 --- a/apps/openmw/mwrender/util.cpp +++ b/apps/openmw/mwrender/util.cpp @@ -3,65 +3,75 @@ #include #include -#include -#include #include +#include +#include +#include #include +#include namespace MWRender { - -class TextureOverrideVisitor : public osg::NodeVisitor + namespace { - public: - TextureOverrideVisitor(const std::string& texture, Resource::ResourceSystem* resourcesystem) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mTexture(texture) - , mResourcesystem(resourcesystem) + class TextureOverrideVisitor : public osg::NodeVisitor { - } + public: + TextureOverrideVisitor(std::string_view texture, Resource::ResourceSystem* resourcesystem) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mTexture(texture) + , mResourcesystem(resourcesystem) + { + } - void apply(osg::Node& node) override - { - int index = 0; - osg::ref_ptr nodePtr(&node); - if (node.getUserValue("overrideFx", index)) + void apply(osg::Node& node) override { - if (index == 1) - overrideTexture(mTexture, mResourcesystem, nodePtr); + int index = 0; + if (node.getUserValue("overrideFx", index)) + { + if (index == 1) + overrideTexture(mTexture, mResourcesystem, node); + } + traverse(node); } - traverse(node); - } - std::string mTexture; - Resource::ResourceSystem* mResourcesystem; -}; + std::string_view mTexture; + Resource::ResourceSystem* mResourcesystem; + }; + } -void overrideFirstRootTexture(const std::string &texture, Resource::ResourceSystem *resourceSystem, osg::ref_ptr node) -{ - TextureOverrideVisitor overrideVisitor(texture, resourceSystem); - node->accept(overrideVisitor); -} + void overrideFirstRootTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node) + { + TextureOverrideVisitor overrideVisitor(texture, resourceSystem); + node.accept(overrideVisitor); + } -void overrideTexture(const std::string &texture, Resource::ResourceSystem *resourceSystem, osg::ref_ptr node) -{ - if (texture.empty()) - return; - std::string correctedTexture = Misc::ResourceHelpers::correctTexturePath(texture, resourceSystem->getVFS()); - // Not sure if wrap settings should be pulled from the overridden texture? - osg::ref_ptr tex = new osg::Texture2D(resourceSystem->getImageManager()->getImage(correctedTexture)); - tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - tex->setName("diffuseMap"); + void overrideTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node) + { + if (texture.empty()) + return; + const VFS::Path::Normalized correctedTexture + = Misc::ResourceHelpers::correctTexturePath(texture, resourceSystem->getVFS()); + // Not sure if wrap settings should be pulled from the overridden texture? + osg::ref_ptr tex + = new osg::Texture2D(resourceSystem->getImageManager()->getImage(correctedTexture)); + tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - osg::ref_ptr stateset; - if (node->getStateSet()) - stateset = new osg::StateSet(*node->getStateSet(), osg::CopyOp::SHALLOW_COPY); - else - stateset = new osg::StateSet; + osg::ref_ptr stateset; + if (const osg::StateSet* const src = node.getStateSet()) + stateset = new osg::StateSet(*src, osg::CopyOp::SHALLOW_COPY); + else + stateset = new osg::StateSet; - stateset->setTextureAttribute(0, tex, osg::StateAttribute::OVERRIDE); + stateset->setTextureAttribute(0, tex, osg::StateAttribute::OVERRIDE); + stateset->setTextureAttribute(0, new SceneUtil::TextureType("diffuseMap"), osg::StateAttribute::OVERRIDE); - node->setStateSet(stateset); -} + node.setStateSet(stateset); + } + + bool shouldAddMSAAIntermediateTarget() + { + return Settings::shaders().mAntialiasAlphaTest && Settings::video().mAntialiasing > 1; + } } diff --git a/apps/openmw/mwrender/util.hpp b/apps/openmw/mwrender/util.hpp index a89baa22b17..fc43680d673 100644 --- a/apps/openmw/mwrender/util.hpp +++ b/apps/openmw/mwrender/util.hpp @@ -2,8 +2,8 @@ #define OPENMW_MWRENDER_UTIL_H #include -#include -#include + +#include namespace osg { @@ -17,11 +17,12 @@ namespace Resource namespace MWRender { - // Overrides the texture of nodes in the mesh that had the same NiTexturingProperty as the first NiTexturingProperty of the .NIF file's root node, - // if it had a NiTexturingProperty. Used for applying "particle textures" to magic effects. - void overrideFirstRootTexture(const std::string &texture, Resource::ResourceSystem *resourceSystem, osg::ref_ptr node); + // Overrides the texture of nodes in the mesh that had the same NiTexturingProperty as the first NiTexturingProperty + // of the .NIF file's root node, if it had a NiTexturingProperty. Used for applying "particle textures" to magic + // effects. + void overrideFirstRootTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node); - void overrideTexture(const std::string& texture, Resource::ResourceSystem* resourceSystem, osg::ref_ptr node); + void overrideTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node); // Node callback to entirely skip the traversal. class NoTraverseCallback : public osg::NodeCallback @@ -32,6 +33,8 @@ namespace MWRender // no traverse() } }; + + bool shouldAddMSAAIntermediateTarget(); } #endif diff --git a/apps/openmw/mwrender/viewovershoulder.cpp b/apps/openmw/mwrender/viewovershoulder.cpp deleted file mode 100644 index 799e34c9923..00000000000 --- a/apps/openmw/mwrender/viewovershoulder.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "viewovershoulder.hpp" - -#include - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/ptr.hpp" -#include "../mwworld/refdata.hpp" - -#include "../mwmechanics/drawstate.hpp" - -namespace MWRender -{ - - ViewOverShoulderController::ViewOverShoulderController(Camera* camera) : - mCamera(camera), mMode(Mode::RightShoulder), - mAutoSwitchShoulder(Settings::Manager::getBool("auto switch shoulder", "Camera")), - mOverShoulderHorizontalOffset(30.f), mOverShoulderVerticalOffset(-10.f) - { - osg::Vec2f offset = Settings::Manager::getVector2("view over shoulder offset", "Camera"); - mOverShoulderHorizontalOffset = std::abs(offset.x()); - mOverShoulderVerticalOffset = offset.y(); - mDefaultShoulderIsRight = offset.x() >= 0; - - mCamera->enableDynamicCameraDistance(true); - mCamera->enableCrosshairInThirdPersonMode(true); - mCamera->setFocalPointTargetOffset(offset); - } - - void ViewOverShoulderController::update() - { - if (mCamera->isFirstPerson()) - return; - - Mode oldMode = mMode; - auto ptr = mCamera->getTrackingPtr(); - bool combat = ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getDrawState() != MWMechanics::DrawState_Nothing; - if (combat && !mCamera->isVanityOrPreviewModeEnabled()) - mMode = Mode::Combat; - else if (MWBase::Environment::get().getWorld()->isSwimming(ptr)) - mMode = Mode::Swimming; - else if (oldMode == Mode::Combat || oldMode == Mode::Swimming) - mMode = mDefaultShoulderIsRight ? Mode::RightShoulder : Mode::LeftShoulder; - if (mAutoSwitchShoulder && (mMode == Mode::LeftShoulder || mMode == Mode::RightShoulder)) - trySwitchShoulder(); - - if (oldMode == mMode) - return; - - if (mCamera->getMode() == Camera::Mode::Vanity) - // Player doesn't touch controls for a long time. Transition should be very slow. - mCamera->setFocalPointTransitionSpeed(0.2f); - else if ((oldMode == Mode::Combat || mMode == Mode::Combat) && mCamera->getMode() == Camera::Mode::Normal) - // Transition to/from combat mode and we are not it preview mode. Should be fast. - mCamera->setFocalPointTransitionSpeed(5.f); - else - mCamera->setFocalPointTransitionSpeed(1.f); // Default transition speed. - - switch (mMode) - { - case Mode::RightShoulder: - mCamera->setFocalPointTargetOffset({mOverShoulderHorizontalOffset, mOverShoulderVerticalOffset}); - break; - case Mode::LeftShoulder: - mCamera->setFocalPointTargetOffset({-mOverShoulderHorizontalOffset, mOverShoulderVerticalOffset}); - break; - case Mode::Combat: - case Mode::Swimming: - default: - mCamera->setFocalPointTargetOffset({0, 15}); - } - } - - void ViewOverShoulderController::trySwitchShoulder() - { - if (mCamera->getMode() != Camera::Mode::Normal) - return; - - const float limitToSwitch = 120; // switch to other shoulder if wall is closer than this limit - const float limitToSwitchBack = 300; // switch back to default shoulder if there is no walls at this distance - - auto orient = osg::Quat(mCamera->getYaw(), osg::Vec3d(0,0,1)); - osg::Vec3d playerPos = mCamera->getFocalPoint() - mCamera->getFocalPointOffset(); - - MWBase::World* world = MWBase::Environment::get().getWorld(); - osg::Vec3d sideOffset = orient * osg::Vec3d(world->getHalfExtents(mCamera->getTrackingPtr()).x() - 1, 0, 0); - float rayRight = world->getDistToNearestRayHit( - playerPos + sideOffset, orient * osg::Vec3d(1, 0, 0), limitToSwitchBack + 1); - float rayLeft = world->getDistToNearestRayHit( - playerPos - sideOffset, orient * osg::Vec3d(-1, 0, 0), limitToSwitchBack + 1); - float rayRightForward = world->getDistToNearestRayHit( - playerPos + sideOffset, orient * osg::Vec3d(1, 3, 0), limitToSwitchBack + 1); - float rayLeftForward = world->getDistToNearestRayHit( - playerPos - sideOffset, orient * osg::Vec3d(-1, 3, 0), limitToSwitchBack + 1); - float distRight = std::min(rayRight, rayRightForward); - float distLeft = std::min(rayLeft, rayLeftForward); - - if (distLeft < limitToSwitch && distRight > limitToSwitchBack) - mMode = Mode::RightShoulder; - else if (distRight < limitToSwitch && distLeft > limitToSwitchBack) - mMode = Mode::LeftShoulder; - else if (distRight > limitToSwitchBack && distLeft > limitToSwitchBack) - mMode = mDefaultShoulderIsRight ? Mode::RightShoulder : Mode::LeftShoulder; - } - -} diff --git a/apps/openmw/mwrender/viewovershoulder.hpp b/apps/openmw/mwrender/viewovershoulder.hpp deleted file mode 100644 index 80ac3086561..00000000000 --- a/apps/openmw/mwrender/viewovershoulder.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef VIEWOVERSHOULDER_H -#define VIEWOVERSHOULDER_H - -#include "camera.hpp" - -namespace MWRender -{ - - class ViewOverShoulderController - { - public: - ViewOverShoulderController(Camera* camera); - - void update(); - - private: - void trySwitchShoulder(); - enum class Mode { RightShoulder, LeftShoulder, Combat, Swimming }; - - Camera* mCamera; - Mode mMode; - bool mAutoSwitchShoulder; - float mOverShoulderHorizontalOffset; - float mOverShoulderVerticalOffset; - bool mDefaultShoulderIsRight; - }; - -} - -#endif // VIEWOVERSHOULDER_H diff --git a/apps/openmw/mwrender/vismask.hpp b/apps/openmw/mwrender/vismask.hpp index 87ca9415fa0..05ed534d297 100644 --- a/apps/openmw/mwrender/vismask.hpp +++ b/apps/openmw/mwrender/vismask.hpp @@ -24,40 +24,44 @@ namespace MWRender Mask_UpdateVisitor = 0x1, // reserved for separating UpdateVisitors from CullVisitors // child of Scene - Mask_Effect = (1<<1), - Mask_Debug = (1<<2), - Mask_Actor = (1<<3), - Mask_Player = (1<<4), - Mask_Sky = (1<<5), - Mask_Water = (1<<6), // choose Water or SimpleWater depending on detail required - Mask_SimpleWater = (1<<7), - Mask_Terrain = (1<<8), - Mask_FirstPerson = (1<<9), - Mask_Object = (1<<10), - Mask_Static = (1<<11), + Mask_Effect = (1 << 1), + Mask_Debug = (1 << 2), + Mask_Actor = (1 << 3), + Mask_Player = (1 << 4), + Mask_Sky = (1 << 5), + Mask_Water = (1 << 6), // choose Water or SimpleWater depending on detail required + Mask_SimpleWater = (1 << 7), + Mask_Terrain = (1 << 8), + Mask_FirstPerson = (1 << 9), + Mask_Object = (1 << 10), + Mask_Static = (1 << 11), // child of Sky - Mask_Sun = (1<<12), - Mask_WeatherParticles = (1<<13), + Mask_Sun = (1 << 12), + Mask_WeatherParticles = (1 << 13), // top level masks - Mask_Scene = (1<<14), - Mask_GUI = (1<<15), + Mask_Scene = (1 << 14), + Mask_GUI = (1 << 15), // Set on a ParticleSystem Drawable - Mask_ParticleSystem = (1<<16), + Mask_ParticleSystem = (1 << 16), // Set on cameras within the main scene graph - Mask_RenderToTexture = (1<<17), + Mask_RenderToTexture = (1 << 17), - Mask_PreCompile = (1<<18), + Mask_PreCompile = (1 << 18), // Set on a camera's cull mask to enable the LightManager - Mask_Lighting = (1<<19), + Mask_Lighting = (1 << 19), - Mask_Groundcover = (1<<20), + Mask_Groundcover = (1 << 20), }; + // Defines masks to remove when using ToggleWorld command + constexpr inline unsigned int sToggleWorldMask + = Mask_Actor | Mask_Terrain | Mask_Object | Mask_Static | Mask_Groundcover; + } #endif diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index c5fd1a3637f..81e44248acb 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -1,834 +1,895 @@ #include "water.hpp" -#include +#include -#include +#include #include -#include +#include +#include #include +#include #include #include -#include -#include - -#include +#include -#include -#include - -#include #include +#include -#include - -#include #include +#include #include +#include +#include #include -#include #include -#include #include +#include #include #include -#include +#include #include +#include + #include "../mwworld/cellstore.hpp" -#include "vismask.hpp" -#include "ripplesimulation.hpp" #include "renderbin.hpp" +#include "ripples.hpp" +#include "ripplesimulation.hpp" #include "util.hpp" +#include "vismask.hpp" namespace MWRender { -// -------------------------------------------------------------------------------------------------------------------------------- + // -------------------------------------------------------------------------------------------------------------------------------- -/// @brief Allows to cull and clip meshes that are below a plane. Useful for reflection & refraction camera effects. -/// Also handles flipping of the plane when the eye point goes below it. -/// To use, simply create the scene as subgraph of this node, then do setPlane(const osg::Plane& plane); -class ClipCullNode : public osg::Group -{ - class PlaneCullCallback : public osg::NodeCallback + /// @brief Allows to cull and clip meshes that are below a plane. Useful for reflection & refraction camera effects. + /// Also handles flipping of the plane when the eye point goes below it. + /// To use, simply create the scene as subgraph of this node, then do setPlane(const osg::Plane& plane); + class ClipCullNode : public osg::Group { - public: - /// @param cullPlane The culling plane (in world space). - PlaneCullCallback(const osg::Plane* cullPlane) - : osg::NodeCallback() - , mCullPlane(cullPlane) + class PlaneCullCallback : public SceneUtil::NodeCallback { - } + public: + /// @param cullPlane The culling plane (in world space). + PlaneCullCallback(const osg::Plane* cullPlane) + : mCullPlane(cullPlane) + { + } + + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + osg::Polytope::PlaneList origPlaneList + = cv->getProjectionCullingStack().back().getFrustum().getPlaneList(); - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + osg::Plane plane = *mCullPlane; + plane.transform(*cv->getCurrentRenderStage()->getInitialViewMatrix()); + + osg::Vec3d eyePoint = cv->getEyePoint(); + if (mCullPlane->intersect(osg::BoundingSphere(osg::Vec3d(0, 0, eyePoint.z()), 0)) > 0) + plane.flip(); + + cv->getProjectionCullingStack().back().getFrustum().add(plane); + + traverse(node, cv); + + // undo + cv->getProjectionCullingStack().back().getFrustum().set(origPlaneList); + } + + private: + const osg::Plane* mCullPlane; + }; + + class FlipCallback : public SceneUtil::NodeCallback { - osgUtil::CullVisitor* cv = static_cast(nv); + public: + FlipCallback(const osg::Plane* cullPlane) + : mCullPlane(cullPlane) + { + } - osg::Polytope::PlaneList origPlaneList = cv->getProjectionCullingStack().back().getFrustum().getPlaneList(); + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + osg::Vec3d eyePoint = cv->getEyePoint(); + + osg::RefMatrix* modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); + + // apply the height of the plane + // we can't apply this height in the addClipPlane() since the "flip the below graph" function would + // otherwise flip the height as well + modelViewMatrix->preMultTranslate(mCullPlane->getNormal() * ((*mCullPlane)[3] * -1)); + + // flip the below graph if the eye point is above the plane + if (mCullPlane->intersect(osg::BoundingSphere(osg::Vec3d(0, 0, eyePoint.z()), 0)) > 0) + { + modelViewMatrix->preMultScale(osg::Vec3(1, 1, -1)); + } + + // move the plane back along its normal a little bit to prevent bleeding at the water shore + float fov = Settings::camera().mFieldOfView; + const float clipFudgeMin = 2.5; // minimum offset of clip plane + const float clipFudgeScale = -15000.0; + float clipFudge = abs(abs((*mCullPlane)[3]) - eyePoint.z()) * fov / clipFudgeScale - clipFudgeMin; + modelViewMatrix->preMultTranslate(mCullPlane->getNormal() * clipFudge); + + cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); + traverse(node, cv); + cv->popModelViewMatrix(); + } + + private: + const osg::Plane* mCullPlane; + }; - osg::Plane plane = *mCullPlane; - plane.transform(*cv->getCurrentRenderStage()->getInitialViewMatrix()); + public: + ClipCullNode() + { + addCullCallback(new PlaneCullCallback(&mPlane)); - osg::Vec3d eyePoint = cv->getEyePoint(); - if (mCullPlane->intersect(osg::BoundingSphere(osg::Vec3d(0,0,eyePoint.z()), 0)) > 0) - plane.flip(); + mClipNodeTransform = new osg::Group; + mClipNodeTransform->addCullCallback(new FlipCallback(&mPlane)); + osg::Group::addChild(mClipNodeTransform); - cv->getProjectionCullingStack().back().getFrustum().add(plane); + mClipNode = new osg::ClipNode; - traverse(node, nv); + mClipNodeTransform->addChild(mClipNode); + } - // undo - cv->getProjectionCullingStack().back().getFrustum().set(origPlaneList); + void setPlane(const osg::Plane& plane) + { + if (plane == mPlane) + return; + mPlane = plane; + + mClipNode->getClipPlaneList().clear(); + mClipNode->addClipPlane( + new osg::ClipPlane(0, osg::Plane(mPlane.getNormal(), 0))); // mPlane.d() applied in FlipCallback + mClipNode->setStateSetModes(*getOrCreateStateSet(), osg::StateAttribute::ON); + mClipNode->setCullingActive(false); } private: - const osg::Plane* mCullPlane; + osg::ref_ptr mClipNodeTransform; + osg::ref_ptr mClipNode; + + osg::Plane mPlane; }; - class FlipCallback : public osg::NodeCallback + /// This callback on the Camera has the effect of a RELATIVE_RF_INHERIT_VIEWPOINT transform mode (which does not + /// exist in OSG). We want to keep the View Point of the parent camera so we will not have to recreate LODs. + class InheritViewPointCallback + : public SceneUtil::NodeCallback { public: - FlipCallback(const osg::Plane* cullPlane) - : mCullPlane(cullPlane) + InheritViewPointCallback() {} + + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { + osg::ref_ptr modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); + cv->popModelViewMatrix(); + cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::ABSOLUTE_RF_INHERIT_VIEWPOINT); + traverse(node, cv); } + }; - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + /// Moves water mesh away from the camera slightly if the camera gets too close on the Z axis. + /// The offset works around graphics artifacts that occurred with the GL_DEPTH_CLAMP when the camera gets extremely + /// close to the mesh (seen on NVIDIA at least). Must be added as a Cull callback. + class FudgeCallback : public SceneUtil::NodeCallback + { + public: + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { - osgUtil::CullVisitor* cv = static_cast(nv); - osg::Vec3d eyePoint = cv->getEyePoint(); - - osg::RefMatrix* modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); + const float fudge = 0.2; + if (std::abs(cv->getEyeLocal().z()) < fudge) + { + float diff = fudge - cv->getEyeLocal().z(); + osg::RefMatrix* modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); - // apply the height of the plane - // we can't apply this height in the addClipPlane() since the "flip the below graph" function would otherwise flip the height as well - modelViewMatrix->preMultTranslate(mCullPlane->getNormal() * ((*mCullPlane)[3] * -1)); + if (cv->getEyeLocal().z() > 0) + modelViewMatrix->preMultTranslate(osg::Vec3f(0, 0, -diff)); + else + modelViewMatrix->preMultTranslate(osg::Vec3f(0, 0, diff)); - // flip the below graph if the eye point is above the plane - if (mCullPlane->intersect(osg::BoundingSphere(osg::Vec3d(0,0,eyePoint.z()), 0)) > 0) - { - modelViewMatrix->preMultScale(osg::Vec3(1,1,-1)); + cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); + traverse(node, cv); + cv->popModelViewMatrix(); } + else + traverse(node, cv); + } + }; - // move the plane back along its normal a little bit to prevent bleeding at the water shore - const float clipFudge = -5; - modelViewMatrix->preMultTranslate(mCullPlane->getNormal() * clipFudge); + class RainSettingsUpdater : public SceneUtil::StateSetUpdater + { + public: + RainSettingsUpdater() + : mRainIntensity(0.f) + , mEnableRipples(false) + { + } - cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); - traverse(node, nv); - cv->popModelViewMatrix(); + void setRainIntensity(float rainIntensity) { mRainIntensity = rainIntensity; } + void setRipplesEnabled(bool enableRipples) { mEnableRipples = enableRipples; } + + protected: + void setDefaults(osg::StateSet* stateset) override + { + osg::ref_ptr rainIntensityUniform = new osg::Uniform("rainIntensity", 0.0f); + stateset->addUniform(rainIntensityUniform.get()); + osg::ref_ptr enableRainRipplesUniform = new osg::Uniform("enableRainRipples", false); + stateset->addUniform(enableRainRipplesUniform.get()); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override + { + osg::ref_ptr rainIntensityUniform = stateset->getUniform("rainIntensity"); + if (rainIntensityUniform != nullptr) + rainIntensityUniform->set(mRainIntensity); + osg::ref_ptr enableRainRipplesUniform = stateset->getUniform("enableRainRipples"); + if (enableRainRipplesUniform != nullptr) + enableRainRipplesUniform->set(mEnableRipples); } private: - const osg::Plane* mCullPlane; + float mRainIntensity; + bool mEnableRipples; }; -public: - ClipCullNode() + class Refraction : public SceneUtil::RTTNode { - addCullCallback (new PlaneCullCallback(&mPlane)); + public: + Refraction(uint32_t rttSize) + : RTTNode(rttSize, rttSize, 0, false, 1, StereoAwareness::Aware, shouldAddMSAAIntermediateTarget()) + , mNodeMask(Refraction::sDefaultCullMask) + { + setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); + mClipCullNode = new ClipCullNode; + } - mClipNodeTransform = new osg::Group; - mClipNodeTransform->addCullCallback(new FlipCallback(&mPlane)); - osg::Group::addChild(mClipNodeTransform); + void setDefaults(osg::Camera* camera) override + { + camera->setReferenceFrame(osg::Camera::RELATIVE_RF); + camera->setSmallFeatureCullingPixelSize(Settings::water().mSmallFeatureCullingPixelSize); + camera->setName("RefractionCamera"); + camera->addCullCallback(new InheritViewPointCallback); + camera->setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); + + // No need for fog here, we are already applying fog on the water surface itself as well as underwater fog + // assign large value to effectively turn off fog + // shaders don't respect glDisable(GL_FOG) + osg::ref_ptr fog(new osg::Fog); + fog->setStart(10000000); + fog->setEnd(10000000); + camera->getOrCreateStateSet()->setAttributeAndModes( + fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); + + camera->addChild(mClipCullNode); + camera->setNodeMask(Mask_RenderToTexture); + + if (Settings::water().mRefractionScale != 1) // TODO: to be removed with issue #5709 + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*camera->getOrCreateStateSet()); + } - mClipNode = new osg::ClipNode; + void apply(osg::Camera* camera) override + { + camera->setViewMatrix(mViewMatrix); + camera->setCullMask(mNodeMask); + } - mClipNodeTransform->addChild(mClipNode); - } + void setScene(osg::Node* scene) + { + if (mScene) + mClipCullNode->removeChild(mScene); + mScene = scene; + mClipCullNode->addChild(scene); + } - void setPlane (const osg::Plane& plane) - { - if (plane == mPlane) - return; - mPlane = plane; + void setWaterLevel(float waterLevel) + { + const float refractionScale = Settings::water().mRefractionScale; - mClipNode->getClipPlaneList().clear(); - mClipNode->addClipPlane(new osg::ClipPlane(0, osg::Plane(mPlane.getNormal(), 0))); // mPlane.d() applied in FlipCallback - mClipNode->setStateSetModes(*getOrCreateStateSet(), osg::StateAttribute::ON); - mClipNode->setCullingActive(false); - } + mViewMatrix = osg::Matrix::scale(1, 1, refractionScale) + * osg::Matrix::translate(0, 0, (1.0 - refractionScale) * waterLevel); -private: - osg::ref_ptr mClipNodeTransform; - osg::ref_ptr mClipNode; + mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, -1), osg::Vec3d(0, 0, waterLevel))); + } - osg::Plane mPlane; -}; + void showWorld(bool show) + { + if (show) + mNodeMask = Refraction::sDefaultCullMask; + else + mNodeMask = Refraction::sDefaultCullMask & ~sToggleWorldMask; + } -/// This callback on the Camera has the effect of a RELATIVE_RF_INHERIT_VIEWPOINT transform mode (which does not exist in OSG). -/// We want to keep the View Point of the parent camera so we will not have to recreate LODs. -class InheritViewPointCallback : public osg::NodeCallback -{ -public: - InheritViewPointCallback() {} + private: + osg::ref_ptr mClipCullNode; + osg::ref_ptr mScene; + osg::Matrix mViewMatrix{ osg::Matrix::identity() }; - void operator()(osg::Node* node, osg::NodeVisitor* nv) override - { - osgUtil::CullVisitor* cv = static_cast(nv); - osg::ref_ptr modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); - cv->popModelViewMatrix(); - cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::ABSOLUTE_RF_INHERIT_VIEWPOINT); - traverse(node, nv); - } -}; + unsigned int mNodeMask; -/// Moves water mesh away from the camera slightly if the camera gets too close on the Z axis. -/// The offset works around graphics artifacts that occurred with the GL_DEPTH_CLAMP when the camera gets extremely close to the mesh (seen on NVIDIA at least). -/// Must be added as a Cull callback. -class FudgeCallback : public osg::NodeCallback -{ -public: - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + static constexpr unsigned int sDefaultCullMask = Mask_Effect | Mask_Scene | Mask_Object | Mask_Static + | Mask_Terrain | Mask_Actor | Mask_ParticleSystem | Mask_Sky | Mask_Sun | Mask_Player | Mask_Lighting + | Mask_Groundcover; + }; + + class Reflection : public SceneUtil::RTTNode { - osgUtil::CullVisitor* cv = static_cast(nv); + public: + Reflection(uint32_t rttSize, bool isInterior) + : RTTNode(rttSize, rttSize, 0, false, 0, StereoAwareness::Aware, shouldAddMSAAIntermediateTarget()) + { + setInterior(isInterior); + setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); + mClipCullNode = new ClipCullNode; + } - const float fudge = 0.2; - if (std::abs(cv->getEyeLocal().z()) < fudge) + void setDefaults(osg::Camera* camera) override { - float diff = fudge - cv->getEyeLocal().z(); - osg::RefMatrix* modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); + camera->setReferenceFrame(osg::Camera::RELATIVE_RF); + camera->setSmallFeatureCullingPixelSize(Settings::water().mSmallFeatureCullingPixelSize); + camera->setName("ReflectionCamera"); + camera->addCullCallback(new InheritViewPointCallback); - if (cv->getEyeLocal().z() > 0) - modelViewMatrix->preMultTranslate(osg::Vec3f(0,0,-diff)); - else - modelViewMatrix->preMultTranslate(osg::Vec3f(0,0,diff)); + // Inform the shader that we're in a reflection + camera->getOrCreateStateSet()->addUniform(new osg::Uniform("isReflection", true)); - cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); - traverse(node, nv); - cv->popModelViewMatrix(); - } - else - traverse(node, nv); - } -}; + // XXX: should really flip the FrontFace on each renderable instead of forcing clockwise. + osg::ref_ptr frontFace(new osg::FrontFace); + frontFace->setMode(osg::FrontFace::CLOCKWISE); + camera->getOrCreateStateSet()->setAttributeAndModes(frontFace, osg::StateAttribute::ON); -class RainIntensityUpdater : public SceneUtil::StateSetUpdater -{ -public: - RainIntensityUpdater() - : mRainIntensity(0.f) - { - } + camera->addChild(mClipCullNode); + camera->setNodeMask(Mask_RenderToTexture); - void setRainIntensity(float rainIntensity) - { - mRainIntensity = rainIntensity; - } + SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*camera->getOrCreateStateSet()); + } -protected: - void setDefaults(osg::StateSet* stateset) override - { - osg::ref_ptr rainIntensityUniform = new osg::Uniform("rainIntensity", 0.0f); - stateset->addUniform(rainIntensityUniform.get()); - } + void apply(osg::Camera* camera) override + { + camera->setViewMatrix(mViewMatrix); + camera->setCullMask(mNodeMask); + } - void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override - { - osg::ref_ptr rainIntensityUniform = stateset->getUniform("rainIntensity"); - if (rainIntensityUniform != nullptr) - rainIntensityUniform->set(mRainIntensity); - } + void setInterior(bool isInterior) + { + mInterior = isInterior; + mNodeMask = calcNodeMask(); + } -private: - float mRainIntensity; -}; + void setWaterLevel(float waterLevel) + { + mViewMatrix = osg::Matrix::scale(1, 1, -1) * osg::Matrix::translate(0, 0, 2 * waterLevel); + mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, 1), osg::Vec3d(0, 0, waterLevel))); + } -osg::ref_ptr readPngImage (const std::string& file) -{ - // use boost in favor of osgDB::readImage, to handle utf-8 path issues on Windows - boost::filesystem::ifstream inStream; - inStream.open(file, std::ios_base::in | std::ios_base::binary); - if (inStream.fail()) - Log(Debug::Error) << "Error: Failed to open " << file; - osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension("png"); - if (!reader) - { - Log(Debug::Error) << "Error: Failed to read " << file << ", no png readerwriter found"; - return osg::ref_ptr(); - } - osgDB::ReaderWriter::ReadResult result = reader->readImage(inStream); - if (!result.success()) - Log(Debug::Error) << "Error: Failed to read " << file << ": " << result.message() << " code " << result.status(); + void setScene(osg::Node* scene) + { + if (mScene) + mClipCullNode->removeChild(mScene); + mScene = scene; + mClipCullNode->addChild(scene); + } - return result.getImage(); -} + void showWorld(bool show) + { + if (show) + mNodeMask = calcNodeMask(); + else + mNodeMask = calcNodeMask() & ~sToggleWorldMask; + } + private: + unsigned int calcNodeMask() + { + int reflectionDetail = Settings::water().mReflectionDetail; + reflectionDetail = std::clamp(reflectionDetail, mInterior ? 2 : 0, 5); + unsigned int extraMask = 0; + if (reflectionDetail >= 1) + extraMask |= Mask_Terrain; + if (reflectionDetail >= 2) + extraMask |= Mask_Static; + if (reflectionDetail >= 3) + extraMask |= Mask_Effect | Mask_ParticleSystem | Mask_Object; + if (reflectionDetail >= 4) + extraMask |= Mask_Player | Mask_Actor; + if (reflectionDetail >= 5) + extraMask |= Mask_Groundcover; + return Mask_Scene | Mask_Sky | Mask_Lighting | extraMask; + } -class Refraction : public osg::Camera -{ -public: - Refraction() - { - unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); - setRenderOrder(osg::Camera::PRE_RENDER, 1); - setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); - setReferenceFrame(osg::Camera::RELATIVE_RF); - setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); - osg::Camera::setName("RefractionCamera"); - setCullCallback(new InheritViewPointCallback); - setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); - - setCullMask(Mask_Effect|Mask_Scene|Mask_Object|Mask_Static|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Sun|Mask_Player|Mask_Lighting|Mask_Groundcover); - setNodeMask(Mask_RenderToTexture); - setViewport(0, 0, rttSize, rttSize); - - // No need for Update traversal since the scene is already updated as part of the main scene graph - // A double update would mess with the light collection (in addition to being plain redundant) - setUpdateCallback(new NoTraverseCallback); - - // No need for fog here, we are already applying fog on the water surface itself as well as underwater fog - // assign large value to effectively turn off fog - // shaders don't respect glDisable(GL_FOG) - osg::ref_ptr fog (new osg::Fog); - fog->setStart(10000000); - fog->setEnd(10000000); - getOrCreateStateSet()->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); - - mClipCullNode = new ClipCullNode; - osg::Camera::addChild(mClipCullNode); - - mRefractionTexture = new osg::Texture2D; - mRefractionTexture->setTextureSize(rttSize, rttSize); - mRefractionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mRefractionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mRefractionTexture->setInternalFormat(GL_RGB); - mRefractionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mRefractionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - - SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(this, osg::Camera::COLOR_BUFFER, mRefractionTexture); - - mRefractionDepthTexture = new osg::Texture2D; - mRefractionDepthTexture->setTextureSize(rttSize, rttSize); - mRefractionDepthTexture->setSourceFormat(GL_DEPTH_COMPONENT); - mRefractionDepthTexture->setInternalFormat(GL_DEPTH_COMPONENT24); - mRefractionDepthTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mRefractionDepthTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mRefractionDepthTexture->setSourceType(GL_UNSIGNED_INT); - mRefractionDepthTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mRefractionDepthTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - - attach(osg::Camera::DEPTH_BUFFER, mRefractionDepthTexture); - - if (Settings::Manager::getFloat("refraction scale", "Water") != 1) // TODO: to be removed with issue #5709 - SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); - } + osg::ref_ptr mClipCullNode; + osg::ref_ptr mScene; + osg::Node::NodeMask mNodeMask; + osg::Matrix mViewMatrix{ osg::Matrix::identity() }; + bool mInterior; + }; - void setScene(osg::Node* scene) + /// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. + class DepthClampCallback : public osg::Drawable::DrawCallback { - if (mScene) - mClipCullNode->removeChild(mScene); - mScene = scene; - mClipCullNode->addChild(scene); - } + public: + void drawImplementation(osg::RenderInfo& renderInfo, const osg::Drawable* drawable) const override + { + static bool supported = osg::isGLExtensionOrVersionSupported( + renderInfo.getState()->getContextID(), "GL_ARB_depth_clamp", 3.3); + if (!supported) + { + drawable->drawImplementation(renderInfo); + return; + } - void setWaterLevel(float waterLevel) - { - const float refractionScale = std::min(1.0f,std::max(0.0f, - Settings::Manager::getFloat("refraction scale", "Water"))); + glEnable(GL_DEPTH_CLAMP); - setViewMatrix(osg::Matrix::scale(1,1,refractionScale) * - osg::Matrix::translate(0,0,(1.0 - refractionScale) * waterLevel)); + drawable->drawImplementation(renderInfo); - mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,-1), osg::Vec3d(0,0, waterLevel))); - } + // restore default + glDisable(GL_DEPTH_CLAMP); + } + }; - osg::Texture2D* getRefractionTexture() const + Water::Water(osg::Group* parent, osg::Group* sceneRoot, Resource::ResourceSystem* resourceSystem, + osgUtil::IncrementalCompileOperation* ico) + : mRainSettingsUpdater(nullptr) + , mParent(parent) + , mSceneRoot(sceneRoot) + , mResourceSystem(resourceSystem) + , mEnabled(true) + , mToggled(true) + , mTop(0) + , mInterior(false) + , mShowWorld(true) + , mCullCallback(nullptr) + , mShaderWaterStateSetUpdater(nullptr) { - return mRefractionTexture.get(); - } + mSimulation = std::make_unique(mSceneRoot, resourceSystem); - osg::Texture2D* getRefractionDepthTexture() const - { - return mRefractionDepthTexture.get(); - } + mWaterGeom = SceneUtil::createWaterGeometry(Constants::CellSizeInUnits * 150, 40, 900); + mWaterGeom->setDrawCallback(new DepthClampCallback); + mWaterGeom->setNodeMask(Mask_Water); + mWaterGeom->setDataVariance(osg::Object::STATIC); + mWaterGeom->setName("Water Geometry"); -private: - osg::ref_ptr mClipCullNode; - osg::ref_ptr mRefractionTexture; - osg::ref_ptr mRefractionDepthTexture; - osg::ref_ptr mScene; -}; + mWaterNode = new osg::PositionAttitudeTransform; + mWaterNode->setName("Water Root"); + mWaterNode->addChild(mWaterGeom); + mWaterNode->addCullCallback(new FudgeCallback); -class Reflection : public osg::Camera -{ -public: - Reflection(bool isInterior) - { - setRenderOrder(osg::Camera::PRE_RENDER); - setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); - setReferenceFrame(osg::Camera::RELATIVE_RF); - setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); - osg::Camera::setName("ReflectionCamera"); - setCullCallback(new InheritViewPointCallback); - - setInterior(isInterior); - setNodeMask(Mask_RenderToTexture); - - unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); - setViewport(0, 0, rttSize, rttSize); - - // No need for Update traversal since the mSceneRoot is already updated as part of the main scene graph - // A double update would mess with the light collection (in addition to being plain redundant) - setUpdateCallback(new NoTraverseCallback); - - mReflectionTexture = new osg::Texture2D; - mReflectionTexture->setTextureSize(rttSize, rttSize); - mReflectionTexture->setInternalFormat(GL_RGB); - mReflectionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mReflectionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mReflectionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mReflectionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(this, osg::Camera::COLOR_BUFFER, mReflectionTexture); - - // XXX: should really flip the FrontFace on each renderable instead of forcing clockwise. - osg::ref_ptr frontFace (new osg::FrontFace); - frontFace->setMode(osg::FrontFace::CLOCKWISE); - getOrCreateStateSet()->setAttributeAndModes(frontFace, osg::StateAttribute::ON); - - mClipCullNode = new ClipCullNode; - osg::Camera::addChild(mClipCullNode); - - SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); - } + // simple water fallback for the local map + osg::ref_ptr geom2(osg::clone(mWaterGeom.get(), osg::CopyOp::DEEP_COPY_NODES)); + createSimpleWaterStateSet(geom2, Fallback::Map::getFloat("Water_Map_Alpha")); + geom2->setNodeMask(Mask_SimpleWater); + geom2->setName("Simple Water Geometry"); + mWaterNode->addChild(geom2); - void setInterior(bool isInterior) - { - int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); - reflectionDetail = std::min(5, std::max(isInterior ? 2 : 0, reflectionDetail)); - unsigned int extraMask = 0; - if(reflectionDetail >= 1) extraMask |= Mask_Terrain; - if(reflectionDetail >= 2) extraMask |= Mask_Static; - if(reflectionDetail >= 3) extraMask |= Mask_Effect|Mask_ParticleSystem|Mask_Object; - if(reflectionDetail >= 4) extraMask |= Mask_Player|Mask_Actor; - if(reflectionDetail >= 5) extraMask |= Mask_Groundcover; - setCullMask(Mask_Scene|Mask_Sky|Mask_Lighting|extraMask); - } + mSceneRoot->addChild(mWaterNode); - void setWaterLevel(float waterLevel) - { - setViewMatrix(osg::Matrix::scale(1,1,-1) * osg::Matrix::translate(0,0,2 * waterLevel)); - mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,1), osg::Vec3d(0,0,waterLevel))); - } + setHeight(mTop); - void setScene(osg::Node* scene) - { - if (mScene) - mClipCullNode->removeChild(mScene); - mScene = scene; - mClipCullNode->addChild(scene); - } + updateWaterMaterial(); - osg::Texture2D* getReflectionTexture() const - { - return mReflectionTexture.get(); + if (ico) + ico->add(mWaterNode); } -private: - osg::ref_ptr mReflectionTexture; - osg::ref_ptr mClipCullNode; - osg::ref_ptr mScene; -}; - -/// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. -class DepthClampCallback : public osg::Drawable::DrawCallback -{ -public: - void drawImplementation(osg::RenderInfo& renderInfo,const osg::Drawable* drawable) const override + void Water::setCullCallback(osg::Callback* callback) { - static bool supported = osg::isGLExtensionOrVersionSupported(renderInfo.getState()->getContextID(), "GL_ARB_depth_clamp", 3.3); - if (!supported) + if (mCullCallback) { - drawable->drawImplementation(renderInfo); - return; + mWaterNode->removeCullCallback(mCullCallback); + if (mReflection) + mReflection->removeCullCallback(mCullCallback); + if (mRefraction) + mRefraction->removeCullCallback(mCullCallback); } - glEnable(GL_DEPTH_CLAMP); - - drawable->drawImplementation(renderInfo); + mCullCallback = callback; - // restore default - glDisable(GL_DEPTH_CLAMP); + if (callback) + { + mWaterNode->addCullCallback(callback); + if (mReflection) + mReflection->addCullCallback(callback); + if (mRefraction) + mRefraction->addCullCallback(callback); + } } -}; - -Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem *resourceSystem, - osgUtil::IncrementalCompileOperation *ico, const std::string& resourcePath) - : mRainIntensityUpdater(nullptr) - , mParent(parent) - , mSceneRoot(sceneRoot) - , mResourceSystem(resourceSystem) - , mResourcePath(resourcePath) - , mEnabled(true) - , mToggled(true) - , mTop(0) - , mInterior(false) - , mCullCallback(nullptr) -{ - mSimulation.reset(new RippleSimulation(mSceneRoot, resourceSystem)); - mWaterGeom = SceneUtil::createWaterGeometry(Constants::CellSizeInUnits*150, 40, 900); - mWaterGeom->setDrawCallback(new DepthClampCallback); - mWaterGeom->setNodeMask(Mask_Water); - mWaterGeom->setDataVariance(osg::Object::STATIC); + void Water::updateWaterMaterial() + { + if (mShaderWaterStateSetUpdater) + { + mWaterNode->removeCullCallback(mShaderWaterStateSetUpdater); + mShaderWaterStateSetUpdater = nullptr; + } + if (mReflection) + { + mParent->removeChild(mReflection); + mReflection = nullptr; + } + if (mRefraction) + { + mParent->removeChild(mRefraction); + mRefraction = nullptr; + } + if (mRipples) + { + mParent->removeChild(mRipples); + mRipples = nullptr; + mSimulation->setRipples(nullptr); + } - mWaterNode = new osg::PositionAttitudeTransform; - mWaterNode->setName("Water Root"); - mWaterNode->addChild(mWaterGeom); - mWaterNode->addCullCallback(new FudgeCallback); + mWaterNode->setStateSet(nullptr); + mWaterGeom->setStateSet(nullptr); + mWaterGeom->setUpdateCallback(nullptr); - // simple water fallback for the local map - osg::ref_ptr geom2 (osg::clone(mWaterGeom.get(), osg::CopyOp::DEEP_COPY_NODES)); - createSimpleWaterStateSet(geom2, Fallback::Map::getFloat("Water_Map_Alpha")); - geom2->setNodeMask(Mask_SimpleWater); - mWaterNode->addChild(geom2); - - mSceneRoot->addChild(mWaterNode); + if (Settings::water().mShader) + { + const unsigned int rttSize = Settings::water().mRttSize; - setHeight(mTop); + mReflection = new Reflection(rttSize, mInterior); + mReflection->setWaterLevel(mTop); + mReflection->setScene(mSceneRoot); + if (mCullCallback) + mReflection->addCullCallback(mCullCallback); + mParent->addChild(mReflection); - updateWaterMaterial(); + if (Settings::water().mRefraction) + { + mRefraction = new Refraction(rttSize); + mRefraction->setWaterLevel(mTop); + mRefraction->setScene(mSceneRoot); + if (mCullCallback) + mRefraction->addCullCallback(mCullCallback); + mParent->addChild(mRefraction); + } - if (ico) - ico->add(mWaterNode); -} + mRipples = new Ripples(mResourceSystem); + mSimulation->setRipples(mRipples); + mParent->addChild(mRipples); -void Water::setCullCallback(osg::Callback* callback) -{ - if (mCullCallback) - { - mWaterNode->removeCullCallback(mCullCallback); - if (mReflection) - mReflection->removeCullCallback(mCullCallback); - if (mRefraction) - mRefraction->removeCullCallback(mCullCallback); - } + showWorld(mShowWorld); - mCullCallback = callback; + createShaderWaterStateSet(mWaterNode); + } + else + createSimpleWaterStateSet(mWaterGeom, Fallback::Map::getFloat("Water_World_Alpha")); - if (callback) - { - mWaterNode->addCullCallback(callback); - if (mReflection) - mReflection->addCullCallback(callback); - if (mRefraction) - mRefraction->addCullCallback(callback); + updateVisible(); } -} -void Water::updateWaterMaterial() -{ - if (mReflection) - { - mReflection->removeChildren(0, mReflection->getNumChildren()); - mParent->removeChild(mReflection); - mReflection = nullptr; - } - if (mRefraction) + osg::Vec3d Water::getPosition() const { - mRefraction->removeChildren(0, mRefraction->getNumChildren()); - mParent->removeChild(mRefraction); - mRefraction = nullptr; + return mWaterNode->getPosition(); } - if (Settings::Manager::getBool("shader", "Water")) + void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) { - mReflection = new Reflection(mInterior); - mReflection->setWaterLevel(mTop); - mReflection->setScene(mSceneRoot); - if (mCullCallback) - mReflection->addCullCallback(mCullCallback); - mParent->addChild(mReflection); + osg::ref_ptr stateset = SceneUtil::createSimpleWaterStateSet(alpha, MWRender::RenderBin_Water); + + node->setStateSet(stateset); + node->setUpdateCallback(nullptr); + mRainSettingsUpdater = nullptr; - if (Settings::Manager::getBool("refraction", "Water")) + // Add animated textures + std::vector> textures; + const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320); + std::string_view texture = Fallback::Map::getString("Water_SurfaceTexture"); + for (int i = 0; i < frameCount; ++i) { - mRefraction = new Refraction; - mRefraction->setWaterLevel(mTop); - mRefraction->setScene(mSceneRoot); - if (mCullCallback) - mRefraction->addCullCallback(mCullCallback); - mParent->addChild(mRefraction); + std::ostringstream texname; + texname << "textures/water/" << texture << std::setw(2) << std::setfill('0') << i << ".dds"; + const VFS::Path::Normalized path(texname.str()); + osg::ref_ptr tex(new osg::Texture2D(mResourceSystem->getImageManager()->getImage(path))); + tex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); + tex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + mResourceSystem->getSceneManager()->applyFilterSettings(tex); + textures.push_back(tex); } - createShaderWaterStateSet(mWaterGeom, mReflection, mRefraction); - } - else - createSimpleWaterStateSet(mWaterGeom, Fallback::Map::getFloat("Water_World_Alpha")); + if (textures.empty()) + return; - updateVisible(); -} + float fps = Fallback::Map::getFloat("Water_SurfaceFPS"); -osg::Camera *Water::getReflectionCamera() -{ - return mReflection; -} + osg::ref_ptr controller(new NifOsg::FlipController(0, 1.f / fps, textures)); + controller->setSource(std::make_shared()); + node->setUpdateCallback(controller); -osg::Camera *Water::getRefractionCamera() -{ - return mRefraction; -} + stateset->setTextureAttributeAndModes(0, textures[0], osg::StateAttribute::ON); -void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) -{ - osg::ref_ptr stateset = SceneUtil::createSimpleWaterStateSet(alpha, MWRender::RenderBin_Water); - - node->setStateSet(stateset); - node->setUpdateCallback(nullptr); - mRainIntensityUpdater = nullptr; - - // Add animated textures - std::vector > textures; - int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); - const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); - for (int i=0; i tex (new osg::Texture2D(mResourceSystem->getImageManager()->getImage(texname.str()))); - tex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); - tex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - textures.push_back(tex); + // use a shader to render the simple water, ensuring that fog is applied per pixel as required. + // this could be removed if a more detailed water mesh, using some sort of paging solution, is implemented. + Resource::SceneManager* sceneManager = mResourceSystem->getSceneManager(); + bool oldValue = sceneManager->getForceShaders(); + sceneManager->setForceShaders(true); + sceneManager->recreateShaders(node); + sceneManager->setForceShaders(oldValue); } - if (textures.empty()) - return; - - float fps = Fallback::Map::getFloat("Water_SurfaceFPS"); - - osg::ref_ptr controller (new NifOsg::FlipController(0, 1.f/fps, textures)); - controller->setSource(std::shared_ptr(new SceneUtil::FrameTimeSource)); - node->setUpdateCallback(controller); + class ShaderWaterStateSetUpdater : public SceneUtil::StateSetUpdater + { + public: + ShaderWaterStateSetUpdater(Water* water, Reflection* reflection, Refraction* refraction, Ripples* ripples, + osg::ref_ptr program, osg::ref_ptr normalMap) + : mWater(water) + , mReflection(reflection) + , mRefraction(refraction) + , mRipples(ripples) + , mProgram(std::move(program)) + , mNormalMap(std::move(normalMap)) + { + } - stateset->setTextureAttributeAndModes(0, textures[0], osg::StateAttribute::ON); + void setDefaults(osg::StateSet* stateset) override + { + stateset->addUniform(new osg::Uniform("normalMap", 0)); + stateset->setTextureAttributeAndModes(0, mNormalMap, osg::StateAttribute::ON); + stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + stateset->setAttributeAndModes(mProgram, osg::StateAttribute::ON); - // use a shader to render the simple water, ensuring that fog is applied per pixel as required. - // this could be removed if a more detailed water mesh, using some sort of paging solution, is implemented. - Resource::SceneManager* sceneManager = mResourceSystem->getSceneManager(); - bool oldValue = sceneManager->getForceShaders(); - sceneManager->setForceShaders(true); - sceneManager->recreateShaders(node); - sceneManager->setForceShaders(oldValue); -} + stateset->addUniform(new osg::Uniform("reflectionMap", 1)); + if (mRefraction) + { + stateset->addUniform(new osg::Uniform("refractionMap", 2)); + stateset->addUniform(new osg::Uniform("refractionDepthMap", 3)); + stateset->setRenderBinDetails(MWRender::RenderBin_Default, "RenderBin"); + } + else + { + stateset->setMode(GL_BLEND, osg::StateAttribute::ON); + stateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); + osg::ref_ptr depth = new SceneUtil::AutoDepth; + depth->setWriteMask(false); + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + if (mRipples) + { + stateset->addUniform(new osg::Uniform("rippleMap", 4)); + } + stateset->addUniform(new osg::Uniform("nodePosition", osg::Vec3f(mWater->getPosition()))); + } -void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, Refraction* refraction) -{ - // use a define map to conditionally compile the shader - std::map defineMap; - defineMap.insert(std::make_pair(std::string("refraction_enabled"), std::string(refraction ? "1" : "0"))); - - Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); - osg::ref_ptr vertexShader (shaderMgr.getShader("water_vertex.glsl", defineMap, osg::Shader::VERTEX)); - osg::ref_ptr fragmentShader (shaderMgr.getShader("water_fragment.glsl", defineMap, osg::Shader::FRAGMENT)); - - osg::ref_ptr normalMap (new osg::Texture2D(readPngImage(mResourcePath + "/shaders/water_nm.png"))); - - if (normalMap->getImage()) - normalMap->getImage()->flipVertical(); - normalMap->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); - normalMap->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - normalMap->setMaxAnisotropy(16); - normalMap->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); - normalMap->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - - osg::ref_ptr shaderStateset = new osg::StateSet; - shaderStateset->addUniform(new osg::Uniform("normalMap", 0)); - shaderStateset->addUniform(new osg::Uniform("reflectionMap", 1)); - - shaderStateset->setTextureAttributeAndModes(0, normalMap, osg::StateAttribute::ON); - shaderStateset->setTextureAttributeAndModes(1, reflection->getReflectionTexture(), osg::StateAttribute::ON); - - if (refraction) - { - shaderStateset->setTextureAttributeAndModes(2, refraction->getRefractionTexture(), osg::StateAttribute::ON); - shaderStateset->setTextureAttributeAndModes(3, refraction->getRefractionDepthTexture(), osg::StateAttribute::ON); - shaderStateset->addUniform(new osg::Uniform("refractionMap", 2)); - shaderStateset->addUniform(new osg::Uniform("refractionDepthMap", 3)); - shaderStateset->setRenderBinDetails(MWRender::RenderBin_Default, "RenderBin"); - } - else - { - shaderStateset->setMode(GL_BLEND, osg::StateAttribute::ON); + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + osgUtil::CullVisitor* cv = static_cast(nv); + stateset->setTextureAttributeAndModes(1, mReflection->getColorTexture(cv), osg::StateAttribute::ON); - shaderStateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); + if (mRefraction) + { + stateset->setTextureAttributeAndModes(2, mRefraction->getColorTexture(cv), osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(3, mRefraction->getDepthTexture(cv), osg::StateAttribute::ON); + } + if (mRipples) + { + stateset->setTextureAttributeAndModes(4, mRipples->getColorTexture(), osg::StateAttribute::ON); + } + stateset->getUniform("nodePosition")->set(osg::Vec3f(mWater->getPosition())); + } - osg::ref_ptr depth (new osg::Depth); - depth->setWriteMask(false); - shaderStateset->setAttributeAndModes(depth, osg::StateAttribute::ON); - } + private: + Water* mWater; + Reflection* mReflection; + Refraction* mRefraction; + Ripples* mRipples; + osg::ref_ptr mProgram; + osg::ref_ptr mNormalMap; + }; - shaderStateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + void Water::createShaderWaterStateSet(osg::Node* node) + { + // use a define map to conditionally compile the shader + std::map defineMap; + defineMap["waterRefraction"] = std::string(mRefraction ? "1" : "0"); + const int rippleDetail = Settings::water().mRainRippleDetail; + defineMap["rainRippleDetail"] = std::to_string(rippleDetail); + defineMap["rippleMapWorldScale"] = std::to_string(RipplesSurface::sWorldScaleFactor); + defineMap["rippleMapSize"] = std::to_string(RipplesSurface::sRTTSize) + ".0"; + defineMap["sunlightScattering"] = Settings::water().mSunlightScattering ? "1" : "0"; + defineMap["wobblyShores"] = Settings::water().mWobblyShores ? "1" : "0"; - osg::ref_ptr program (new osg::Program); - program->addShader(vertexShader); - program->addShader(fragmentShader); - auto method = mResourceSystem->getSceneManager()->getLightingMethod(); - if (method == SceneUtil::LightingMethod::SingleUBO) - program->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); - shaderStateset->setAttributeAndModes(program, osg::StateAttribute::ON); + Stereo::shaderStereoDefines(defineMap); - node->setStateSet(shaderStateset); + Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); + osg::ref_ptr program = shaderMgr.getProgram("water", defineMap); - mRainIntensityUpdater = new RainIntensityUpdater(); - node->setUpdateCallback(mRainIntensityUpdater); -} + constexpr VFS::Path::NormalizedView waterImage("textures/omw/water_nm.png"); + osg::ref_ptr normalMap( + new osg::Texture2D(mResourceSystem->getImageManager()->getImage(waterImage))); + normalMap->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); + normalMap->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + mResourceSystem->getSceneManager()->applyFilterSettings(normalMap); -void Water::processChangedSettings(const Settings::CategorySettingVector& settings) -{ - updateWaterMaterial(); -} + mRainSettingsUpdater = new RainSettingsUpdater(); + node->setUpdateCallback(mRainSettingsUpdater); -Water::~Water() -{ - mParent->removeChild(mWaterNode); + mShaderWaterStateSetUpdater = new ShaderWaterStateSetUpdater( + this, mReflection, mRefraction, mRipples, std::move(program), std::move(normalMap)); + node->addCullCallback(mShaderWaterStateSetUpdater); + } - if (mReflection) + void Water::processChangedSettings(const Settings::CategorySettingVector& settings) { - mReflection->removeChildren(0, mReflection->getNumChildren()); - mParent->removeChild(mReflection); - mReflection = nullptr; + updateWaterMaterial(); } - if (mRefraction) + + Water::~Water() { - mRefraction->removeChildren(0, mRefraction->getNumChildren()); - mParent->removeChild(mRefraction); - mRefraction = nullptr; + mParent->removeChild(mWaterNode); + + if (mReflection) + { + mParent->removeChild(mReflection); + mReflection = nullptr; + } + if (mRefraction) + { + mParent->removeChild(mRefraction); + mRefraction = nullptr; + } + if (mRipples) + { + mParent->removeChild(mRipples); + mRipples = nullptr; + mSimulation->setRipples(nullptr); + } } -} -void Water::listAssetsToPreload(std::vector &textures) -{ - int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); - const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); - for (int i=0; i& textures) { - std::ostringstream texname; - texname << "textures/water/" << texture << std::setw(2) << std::setfill('0') << i << ".dds"; - textures.push_back(texname.str()); + const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320); + std::string_view texture = Fallback::Map::getString("Water_SurfaceTexture"); + for (int i = 0; i < frameCount; ++i) + { + std::ostringstream texname; + texname << "textures/water/" << texture << std::setw(2) << std::setfill('0') << i << ".dds"; + textures.emplace_back(texname.str()); + } } -} - -void Water::setEnabled(bool enabled) -{ - mEnabled = enabled; - updateVisible(); -} -void Water::changeCell(const MWWorld::CellStore* store) -{ - bool isInterior = !store->getCell()->isExterior(); - bool wasInterior = mInterior; - if (!isInterior) + void Water::setEnabled(bool enabled) { - mWaterNode->setPosition(getSceneNodeCoordinates(store->getCell()->mData.mX, store->getCell()->mData.mY)); - mInterior = false; + mEnabled = enabled; + updateVisible(); } - else + + void Water::changeCell(const MWWorld::CellStore* store) { - mWaterNode->setPosition(osg::Vec3f(0,0,mTop)); - mInterior = true; + bool isInterior = !store->getCell()->isExterior(); + bool wasInterior = mInterior; + if (!isInterior) + { + mWaterNode->setPosition( + getSceneNodeCoordinates(store->getCell()->getGridX(), store->getCell()->getGridY())); + mInterior = false; + } + else + { + mWaterNode->setPosition(osg::Vec3f(0, 0, mTop)); + mInterior = true; + } + if (mInterior != wasInterior && mReflection) + mReflection->setInterior(mInterior); } - if(mInterior != wasInterior && mReflection) - mReflection->setInterior(mInterior); - // create a new StateSet to prevent threading issues - osg::ref_ptr nodeStateSet (new osg::StateSet); - nodeStateSet->addUniform(new osg::Uniform("nodePosition", osg::Vec3f(mWaterNode->getPosition()))); - mWaterNode->setStateSet(nodeStateSet); -} + void Water::setHeight(const float height) + { + mTop = height; -void Water::setHeight(const float height) -{ - mTop = height; + mSimulation->setWaterHeight(height); - mSimulation->setWaterHeight(height); + osg::Vec3f pos = mWaterNode->getPosition(); + pos.z() = height; + mWaterNode->setPosition(pos); - osg::Vec3f pos = mWaterNode->getPosition(); - pos.z() = height; - mWaterNode->setPosition(pos); + if (mReflection) + mReflection->setWaterLevel(mTop); + if (mRefraction) + mRefraction->setWaterLevel(mTop); + } - if (mReflection) - mReflection->setWaterLevel(mTop); - if (mRefraction) - mRefraction->setWaterLevel(mTop); -} + void Water::setRainIntensity(float rainIntensity) + { + if (mRainSettingsUpdater) + mRainSettingsUpdater->setRainIntensity(rainIntensity); + } -void Water::setRainIntensity(float rainIntensity) -{ - if (mRainIntensityUpdater) - mRainIntensityUpdater->setRainIntensity(rainIntensity); -} + void Water::setRainRipplesEnabled(bool enableRipples) + { + if (mRainSettingsUpdater) + mRainSettingsUpdater->setRipplesEnabled(enableRipples); + } -void Water::update(float dt) -{ - mSimulation->update(dt); -} + void Water::update(float dt, bool paused) + { + if (!paused) + { + mSimulation->update(dt); + } -void Water::updateVisible() -{ - bool visible = mEnabled && mToggled; - mWaterNode->setNodeMask(visible ? ~0u : 0u); - if (mRefraction) - mRefraction->setNodeMask(visible ? Mask_RenderToTexture : 0u); - if (mReflection) - mReflection->setNodeMask(visible ? Mask_RenderToTexture : 0u); -} + if (mRipples) + { + mRipples->setPaused(paused); + } + } -bool Water::toggle() -{ - mToggled = !mToggled; - updateVisible(); - return mToggled; -} + void Water::updateVisible() + { + bool visible = mEnabled && mToggled; + mWaterNode->setNodeMask(visible ? ~0u : 0u); + if (mRefraction) + mRefraction->setNodeMask(visible ? Mask_RenderToTexture : 0u); + if (mReflection) + mReflection->setNodeMask(visible ? Mask_RenderToTexture : 0u); + if (mRipples) + mRipples->setNodeMask(visible ? Mask_RenderToTexture : 0u); + } -bool Water::isUnderwater(const osg::Vec3f &pos) const -{ - return pos.z() < mTop && mToggled && mEnabled; -} + bool Water::toggle() + { + mToggled = !mToggled; + updateVisible(); + return mToggled; + } -osg::Vec3f Water::getSceneNodeCoordinates(int gridX, int gridY) -{ - return osg::Vec3f(static_cast(gridX * Constants::CellSizeInUnits + (Constants::CellSizeInUnits / 2)), - static_cast(gridY * Constants::CellSizeInUnits + (Constants::CellSizeInUnits / 2)), mTop); -} + bool Water::isUnderwater(const osg::Vec3f& pos) const + { + return pos.z() < mTop && mToggled && mEnabled; + } -void Water::addEmitter (const MWWorld::Ptr& ptr, float scale, float force) -{ - mSimulation->addEmitter (ptr, scale, force); -} + osg::Vec3f Water::getSceneNodeCoordinates(int gridX, int gridY) + { + return osg::Vec3f(static_cast(gridX * Constants::CellSizeInUnits + (Constants::CellSizeInUnits / 2)), + static_cast(gridY * Constants::CellSizeInUnits + (Constants::CellSizeInUnits / 2)), mTop); + } -void Water::removeEmitter (const MWWorld::Ptr& ptr) -{ - mSimulation->removeEmitter (ptr); -} + void Water::addEmitter(const MWWorld::Ptr& ptr, float scale, float force) + { + mSimulation->addEmitter(ptr, scale, force); + } -void Water::updateEmitterPtr (const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) -{ - mSimulation->updateEmitterPtr(old, ptr); -} + void Water::removeEmitter(const MWWorld::Ptr& ptr) + { + mSimulation->removeEmitter(ptr); + } -void Water::emitRipple(const osg::Vec3f &pos) -{ - mSimulation->emitRipple(pos); -} + void Water::updateEmitterPtr(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) + { + mSimulation->updateEmitterPtr(old, ptr); + } -void Water::removeCell(const MWWorld::CellStore *store) -{ - mSimulation->removeCell(store); -} + void Water::emitRipple(const osg::Vec3f& pos) + { + mSimulation->emitRipple(pos); + } -void Water::clearRipples() -{ - mSimulation->clear(); -} + void Water::removeCell(const MWWorld::CellStore* store) + { + mSimulation->removeCell(store); + } + + void Water::clearRipples() + { + mSimulation->clear(); + } + + void Water::showWorld(bool show) + { + if (mReflection) + mReflection->showWorld(show); + if (mRefraction) + mRefraction->showWorld(show); + mShowWorld = show; + } } diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index ec7dc95db84..92299309f04 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -4,11 +4,12 @@ #include #include -#include +#include #include -#include +#include #include +#include namespace osg { @@ -16,6 +17,7 @@ namespace osg class PositionAttitudeTransform; class Geometry; class Node; + class Callback; } namespace osgUtil @@ -45,12 +47,13 @@ namespace MWRender class Refraction; class Reflection; class RippleSimulation; - class RainIntensityUpdater; + class RainSettingsUpdater; + class Ripples; /// Water rendering class Water { - osg::ref_ptr mRainIntensityUpdater; + osg::ref_ptr mRainSettingsUpdater; osg::ref_ptr mParent; osg::ref_ptr mSceneRoot; @@ -63,36 +66,34 @@ namespace MWRender osg::ref_ptr mRefraction; osg::ref_ptr mReflection; - - const std::string mResourcePath; + osg::ref_ptr mRipples; bool mEnabled; bool mToggled; float mTop; bool mInterior; + bool mShowWorld; osg::Callback* mCullCallback; + osg::ref_ptr mShaderWaterStateSetUpdater; osg::Vec3f getSceneNodeCoordinates(int gridX, int gridY); void updateVisible(); void createSimpleWaterStateSet(osg::Node* node, float alpha); - /// @param reflection the reflection camera (required) - /// @param refraction the refraction camera (optional) - void createShaderWaterStateSet(osg::Node* node, Reflection* reflection, Refraction* refraction); + void createShaderWaterStateSet(osg::Node* node); void updateWaterMaterial(); public: - Water(osg::Group* parent, osg::Group* sceneRoot, - Resource::ResourceSystem* resourceSystem, osgUtil::IncrementalCompileOperation* ico, - const std::string& resourcePath); + Water(osg::Group* parent, osg::Group* sceneRoot, Resource::ResourceSystem* resourceSystem, + osgUtil::IncrementalCompileOperation* ico); ~Water(); void setCullCallback(osg::Callback* callback); - void listAssetsToPreload(std::vector& textures); + void listAssetsToPreload(std::vector& textures); void setEnabled(bool enabled); @@ -101,9 +102,9 @@ namespace MWRender bool isUnderwater(const osg::Vec3f& pos) const; /// adds an emitter, position will be tracked automatically using its scene node - void addEmitter (const MWWorld::Ptr& ptr, float scale = 1.f, float force = 1.f); - void removeEmitter (const MWWorld::Ptr& ptr); - void updateEmitterPtr (const MWWorld::Ptr& old, const MWWorld::Ptr& ptr); + void addEmitter(const MWWorld::Ptr& ptr, float scale = 1.f, float force = 1.f); + void removeEmitter(const MWWorld::Ptr& ptr); + void updateEmitterPtr(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr); void emitRipple(const osg::Vec3f& pos); void removeCell(const MWWorld::CellStore* store); ///< remove all emitters in this cell @@ -113,13 +114,15 @@ namespace MWRender void changeCell(const MWWorld::CellStore* store); void setHeight(const float height); void setRainIntensity(const float rainIntensity); + void setRainRipplesEnabled(bool enableRipples); - void update(float dt); + void update(float dt, bool paused); - osg::Camera *getReflectionCamera(); - osg::Camera *getRefractionCamera(); + osg::Vec3d getPosition() const; void processChangedSettings(const Settings::CategorySettingVector& settings); + + void showWorld(bool show); }; } diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index 0c2a11466be..b9c8fd1d28e 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -5,15 +5,14 @@ #include #include -#include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/weapontype.hpp" @@ -23,205 +22,206 @@ namespace MWRender { -float WeaponAnimationTime::getValue(osg::NodeVisitor*) -{ - if (mWeaponGroup.empty()) - return 0; - - float current = mAnimation->getCurrentTime(mWeaponGroup); - if (current == -1) - return 0; - return current - mStartTime; -} - -void WeaponAnimationTime::setGroup(const std::string &group, bool relativeTime) -{ - mWeaponGroup = group; - mRelativeTime = relativeTime; - - if (mRelativeTime) - mStartTime = mAnimation->getStartTime(mWeaponGroup); - else - mStartTime = 0; -} - -void WeaponAnimationTime::updateStartTime() -{ - setGroup(mWeaponGroup, mRelativeTime); -} + float WeaponAnimationTime::getValue(osg::NodeVisitor*) + { + if (mWeaponGroup.empty()) + return 0; -WeaponAnimation::WeaponAnimation() - : mPitchFactor(0) -{ -} + float current = mAnimation->getCurrentTime(mWeaponGroup); + if (current == -1) + return 0; + return current - mStartTime; + } -WeaponAnimation::~WeaponAnimation() -{ + void WeaponAnimationTime::setGroup(const std::string& group, bool relativeTime) + { + mWeaponGroup = group; + mRelativeTime = relativeTime; -} + if (mRelativeTime) + mStartTime = mAnimation->getStartTime(mWeaponGroup); + else + mStartTime = 0; + } -void WeaponAnimation::attachArrow(MWWorld::Ptr actor) -{ - const MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); - MWWorld::ConstContainerStoreIterator weaponSlot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weaponSlot == inv.end()) - return; - if (weaponSlot->getTypeName() != typeid(ESM::Weapon).name()) - return; - - int type = weaponSlot->get()->mBase->mData.mType; - ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(type)->mWeaponClass; - if (weapclass == ESM::WeaponType::Thrown) + void WeaponAnimationTime::updateStartTime() { - std::string soundid = weaponSlot->getClass().getUpSoundId(*weaponSlot); - if(!soundid.empty()) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(actor, soundid, 1.0f, 1.0f); - } - showWeapon(true); + setGroup(mWeaponGroup, mRelativeTime); } - else if (weapclass == ESM::WeaponType::Ranged) - { - osg::Group* parent = getArrowBone(); - if (!parent) - return; - - MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); - if (ammo == inv.end()) - return; - std::string model = ammo->getClass().getModel(*ammo); - - osg::ref_ptr arrow = getResourceSystem()->getSceneManager()->getInstance(model, parent); - mAmmunition = PartHolderPtr(new PartHolder(arrow)); + WeaponAnimation::WeaponAnimation() + : mPitchFactor(0) + { } -} - -void WeaponAnimation::detachArrow(MWWorld::Ptr actor) -{ - mAmmunition.reset(); -} - -void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) -{ - MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); - MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weapon == inv.end()) - return; - if (weapon->getTypeName() != typeid(ESM::Weapon).name()) - return; - - // The orientation of the launched projectile. Always the same as the actor orientation, even if the ArrowBone's orientation dictates otherwise. - osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) - * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); - - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); - MWMechanics::applyFatigueLoss(actor, *weapon, attackStrength); + WeaponAnimation::~WeaponAnimation() {} - if (MWMechanics::getWeaponType(weapon->get()->mBase->mData.mType)->mWeaponClass == ESM::WeaponType::Thrown) + void WeaponAnimation::attachArrow(const MWWorld::Ptr& actor) { - // Thrown weapons get detached now - osg::Node* weaponNode = getWeaponNode(); - if (!weaponNode) + const MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); + MWWorld::ConstContainerStoreIterator weaponSlot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weaponSlot == inv.end()) return; - osg::NodePathList nodepaths = weaponNode->getParentalNodePaths(); - if (nodepaths.empty()) + if (weaponSlot->getType() != ESM::Weapon::sRecordId) return; - osg::Vec3f launchPos = osg::computeLocalToWorld(nodepaths[0]).getTrans(); - float fThrownWeaponMinSpeed = gmst.find("fThrownWeaponMinSpeed")->mValue.getFloat(); - float fThrownWeaponMaxSpeed = gmst.find("fThrownWeaponMaxSpeed")->mValue.getFloat(); - float speed = fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * attackStrength; + int type = weaponSlot->get()->mBase->mData.mType; + ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(type)->mWeaponClass; + if (weapclass == ESM::WeaponType::Thrown) + { + const auto& soundid = weaponSlot->getClass().getUpSoundId(*weaponSlot); + if (!soundid.empty()) + { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(actor, soundid, 1.0f, 1.0f); + } + showWeapon(true); + } + else if (weapclass == ESM::WeaponType::Ranged) + { + osg::Group* parent = getArrowBone(); + if (!parent) + return; - MWWorld::Ptr weaponPtr = *weapon; - MWBase::Environment::get().getWorld()->launchProjectile(actor, weaponPtr, launchPos, orient, weaponPtr, speed, attackStrength); + MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + if (ammo == inv.end()) + return; + VFS::Path::Normalized model(ammo->getClass().getCorrectedModel(*ammo)); - showWeapon(false); + osg::ref_ptr arrow = getResourceSystem()->getSceneManager()->getInstance(model, parent); - inv.remove(*weapon, 1, actor); + mAmmunition = std::make_unique(arrow); + } } - else + + void WeaponAnimation::detachArrow(MWWorld::Ptr actor) { - // With bows and crossbows only the used arrow/bolt gets detached - MWWorld::ContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); - if (ammo == inv.end()) - return; + mAmmunition.reset(); + } - if (!mAmmunition) + void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) + { + MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); + MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (weapon == inv.end()) return; - - osg::ref_ptr ammoNode = mAmmunition->getNode(); - osg::NodePathList nodepaths = ammoNode->getParentalNodePaths(); - if (nodepaths.empty()) + if (weapon->getType() != ESM::Weapon::sRecordId) return; - osg::Vec3f launchPos = osg::computeLocalToWorld(nodepaths[0]).getTrans(); - float fProjectileMinSpeed = gmst.find("fProjectileMinSpeed")->mValue.getFloat(); - float fProjectileMaxSpeed = gmst.find("fProjectileMaxSpeed")->mValue.getFloat(); - float speed = fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * attackStrength; + // The orientation of the launched projectile. Always the same as the actor orientation, even if the ArrowBone's + // orientation dictates otherwise. + osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1, 0, 0)) + * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0, 0, -1)); - MWWorld::Ptr weaponPtr = *weapon; - MWWorld::Ptr ammoPtr = *ammo; - MWBase::Environment::get().getWorld()->launchProjectile(actor, ammoPtr, launchPos, orient, weaponPtr, speed, attackStrength); + const MWWorld::Store& gmst + = MWBase::Environment::get().getESMStore()->get(); - inv.remove(ammoPtr, 1, actor); - mAmmunition.reset(); + MWMechanics::applyFatigueLoss(actor, *weapon, attackStrength); + + if (MWMechanics::getWeaponType(weapon->get()->mBase->mData.mType)->mWeaponClass + == ESM::WeaponType::Thrown) + { + // Thrown weapons get detached now + osg::Node* weaponNode = getWeaponNode(); + if (!weaponNode) + return; + osg::NodePathList nodepaths = weaponNode->getParentalNodePaths(); + if (nodepaths.empty()) + return; + osg::Vec3f launchPos = osg::computeLocalToWorld(nodepaths[0]).getTrans(); + + float fThrownWeaponMinSpeed = gmst.find("fThrownWeaponMinSpeed")->mValue.getFloat(); + float fThrownWeaponMaxSpeed = gmst.find("fThrownWeaponMaxSpeed")->mValue.getFloat(); + float speed = fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * attackStrength; + + MWWorld::Ptr weaponPtr = *weapon; + MWBase::Environment::get().getWorld()->launchProjectile( + actor, weaponPtr, launchPos, orient, weaponPtr, speed, attackStrength); + + showWeapon(false); + + inv.remove(*weapon, 1); + } + else + { + // With bows and crossbows only the used arrow/bolt gets detached + MWWorld::ContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + if (ammo == inv.end()) + return; + + if (!mAmmunition) + return; + + osg::ref_ptr ammoNode = mAmmunition->getNode(); + osg::NodePathList nodepaths = ammoNode->getParentalNodePaths(); + if (nodepaths.empty()) + return; + osg::Vec3f launchPos = osg::computeLocalToWorld(nodepaths[0]).getTrans(); + + float fProjectileMinSpeed = gmst.find("fProjectileMinSpeed")->mValue.getFloat(); + float fProjectileMaxSpeed = gmst.find("fProjectileMaxSpeed")->mValue.getFloat(); + float speed = fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * attackStrength; + + MWWorld::Ptr weaponPtr = *weapon; + MWWorld::Ptr ammoPtr = *ammo; + MWBase::Environment::get().getWorld()->launchProjectile( + actor, ammoPtr, launchPos, orient, weaponPtr, speed, attackStrength); + + inv.remove(ammoPtr, 1); + mAmmunition.reset(); + } } -} -void WeaponAnimation::addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) -{ - for (int i=0; i<2; ++i) + void WeaponAnimation::addControllers(const Animation::NodeMap& nodes, + std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot) { - mSpineControllers[i] = nullptr; - - std::map >::const_iterator found = nodes.find(i == 0 ? "bip01 spine1" : "bip01 spine2"); - if (found != nodes.end()) + for (int i = 0; i < 2; ++i) { - osg::Node* node = found->second; - mSpineControllers[i] = new RotateController(objectRoot); - node->addUpdateCallback(mSpineControllers[i]); - map.emplace_back(node, mSpineControllers[i]); + mSpineControllers[i] = nullptr; + + Animation::NodeMap::const_iterator found = nodes.find(i == 0 ? "bip01 spine1" : "bip01 spine2"); + if (found != nodes.end()) + { + osg::Node* node = found->second; + mSpineControllers[i] = new RotateController(objectRoot); + node->addUpdateCallback(mSpineControllers[i]); + map.emplace_back(node, mSpineControllers[i]); + } } } -} -void WeaponAnimation::deleteControllers() -{ - for (int i=0; i<2; ++i) - mSpineControllers[i] = nullptr; -} - -void WeaponAnimation::configureControllers(float characterPitchRadians) -{ - if (mPitchFactor == 0.f || characterPitchRadians == 0.f) + void WeaponAnimation::deleteControllers() { - setControllerEnabled(false); - return; + for (int i = 0; i < 2; ++i) + mSpineControllers[i] = nullptr; } - float pitch = characterPitchRadians * mPitchFactor; - osg::Quat rotate (pitch/2, osg::Vec3f(-1,0,0)); - setControllerRotate(rotate); - setControllerEnabled(true); -} + void WeaponAnimation::configureControllers(float characterPitchRadians) + { + if (mPitchFactor == 0.f || characterPitchRadians == 0.f) + { + setControllerEnabled(false); + return; + } -void WeaponAnimation::setControllerRotate(const osg::Quat& rotate) -{ - for (int i=0; i<2; ++i) - if (mSpineControllers[i]) - mSpineControllers[i]->setRotate(rotate); -} + float pitch = characterPitchRadians * mPitchFactor; + osg::Quat rotate(pitch / 2, osg::Vec3f(-1, 0, 0)); + setControllerRotate(rotate); + setControllerEnabled(true); + } -void WeaponAnimation::setControllerEnabled(bool enabled) -{ - for (int i=0; i<2; ++i) - if (mSpineControllers[i]) - mSpineControllers[i]->setEnabled(enabled); -} + void WeaponAnimation::setControllerRotate(const osg::Quat& rotate) + { + for (int i = 0; i < 2; ++i) + if (mSpineControllers[i]) + mSpineControllers[i]->setRotate(rotate); + } + + void WeaponAnimation::setControllerEnabled(bool enabled) + { + for (int i = 0; i < 2; ++i) + if (mSpineControllers[i]) + mSpineControllers[i]->setEnabled(enabled); + } } diff --git a/apps/openmw/mwrender/weaponanimation.hpp b/apps/openmw/mwrender/weaponanimation.hpp index d02107333ed..ac9babb85af 100644 --- a/apps/openmw/mwrender/weaponanimation.hpp +++ b/apps/openmw/mwrender/weaponanimation.hpp @@ -18,8 +18,14 @@ namespace MWRender std::string mWeaponGroup; float mStartTime; bool mRelativeTime; + public: - WeaponAnimationTime(Animation* animation) : mAnimation(animation), mStartTime(0), mRelativeTime(false) {} + WeaponAnimationTime(Animation* animation) + : mAnimation(animation) + , mStartTime(0) + , mRelativeTime(false) + { + } void setGroup(const std::string& group, bool relativeTime); void updateStartTime(); @@ -34,7 +40,7 @@ namespace MWRender virtual ~WeaponAnimation(); /// @note If no weapon (or an invalid weapon) is equipped, this function is a no-op. - void attachArrow(MWWorld::Ptr actor); + void attachArrow(const MWWorld::Ptr& actor); void detachArrow(MWWorld::Ptr actor); @@ -42,8 +48,8 @@ namespace MWRender void releaseArrow(MWWorld::Ptr actor, float attackStrength); /// Add WeaponAnimation-related controllers to \a nodes and store the added controllers in \a map. - void addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); + void addControllers(const Animation::NodeMap& nodes, + std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); void deleteControllers(); diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index 223ae3a1525..a91a5853675 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -8,478 +8,533 @@ #include #include -#include #include +#include #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/aiactivate.hpp" #include "../mwmechanics/aiescort.hpp" +#include "../mwmechanics/aiface.hpp" #include "../mwmechanics/aifollow.hpp" #include "../mwmechanics/aitravel.hpp" #include "../mwmechanics/aiwander.hpp" -#include "../mwmechanics/aiface.hpp" +#include "../mwmechanics/creaturestats.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/world.hpp" #include "interpretercontext.hpp" #include "ref.hpp" - namespace MWScript { namespace Ai { - template + template class OpAiActivate : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId objectID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string objectID = runtime.getStringLiteral (runtime[0].mInteger); + // The value of the reset argument doesn't actually matter + bool repeat = arg0; + for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. - for (unsigned int i=0; i + template class OpAiTravel : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Float x = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float x = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float y = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float y = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float z = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float z = runtime[0].mFloat; + // The value of the reset argument doesn't actually matter + bool repeat = arg0; + for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. - for (unsigned int i=0; i + template class OpAiEscort : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId actorID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + Interpreter::Type_Float duration = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float duration = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float x = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float x = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float y = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float y = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float z = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float z = runtime[0].mFloat; + // The value of the reset argument doesn't actually matter + bool repeat = arg0; + for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. - for (unsigned int i=0; i(duration), x, y, z); - ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(escortPackage, ptr); + MWMechanics::AiEscort escortPackage(actorID, static_cast(duration), x, y, z, repeat); + ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(escortPackage, ptr); - Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration; - } + Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration; + } }; - template + template class OpAiEscortCell : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId actorID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + std::string_view cellID = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); - std::string cellID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + Interpreter::Type_Float duration = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float duration = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float x = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float x = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float y = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float y = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float z = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float z = runtime[0].mFloat; + // The value of the reset argument doesn't actually matter + bool repeat = arg0; + for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. - for (unsigned int i=0; igetStore().get().find(cellID); + if (!MWBase::Environment::get().getESMStore()->get().search(cellID)) + return; - MWMechanics::AiEscort escortPackage(actorID, cellID, static_cast(duration), x, y, z); - ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(escortPackage, ptr); + MWMechanics::AiEscort escortPackage(actorID, cellID, static_cast(duration), x, y, z, repeat); + ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(escortPackage, ptr); - Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration; - } + Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration; + } }; - template + template class OpGetAiPackageDone : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats (ptr).getAiSequence().isPackageDone(); + bool done = false; + if (ptr.getClass().isActor()) + done = ptr.getClass().getCreatureStats(ptr).getAiSequence().isPackageDone(); - runtime.push (value); - } + runtime.push(done); + } }; - template + template class OpAiWander : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Integer range = static_cast(runtime[0].mFloat); + runtime.pop(); - Interpreter::Type_Integer range = static_cast(runtime[0].mFloat); - runtime.pop(); + Interpreter::Type_Integer duration = static_cast(runtime[0].mFloat); + runtime.pop(); - Interpreter::Type_Integer duration = static_cast(runtime[0].mFloat); - runtime.pop(); + Interpreter::Type_Integer time = static_cast(runtime[0].mFloat); + runtime.pop(); - Interpreter::Type_Integer time = static_cast(runtime[0].mFloat); + // Chance for Idle is unused + if (arg0) + { + --arg0; runtime.pop(); + } - // Chance for Idle is unused - if (arg0) - { - --arg0; - runtime.pop(); - } + std::vector idleList; + bool repeat = false; - std::vector idleList; - bool repeat = false; + // Chances for Idle2-Idle9 + for (int i = 2; i <= 9 && arg0; ++i) + { + if (!repeat) + repeat = true; + Interpreter::Type_Integer idleValue = std::clamp(runtime[0].mInteger, 0, 255); + idleList.push_back(idleValue); + runtime.pop(); + --arg0; + } - // Chances for Idle2-Idle9 - for(int i=2; i<=9 && arg0; ++i) - { - if(!repeat) - repeat = true; - Interpreter::Type_Integer idleValue = runtime[0].mInteger; - idleValue = std::min(255, std::max(0, idleValue)); - idleList.push_back(idleValue); - runtime.pop(); - --arg0; - } + if (arg0) + { + repeat = runtime[0].mInteger != 0; + runtime.pop(); + --arg0; + } - if(arg0) - { - repeat = runtime[0].mInteger != 0; - runtime.pop(); - --arg0; - } + // discard additional arguments, because we have no idea what they mean. + for (unsigned int i = 0; i < arg0; ++i) + runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. - for (unsigned int i=0; i + template class OpGetAiSetting : public Interpreter::Opcode0 { - MWMechanics::CreatureStats::AiSetting mIndex; - public: - OpGetAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {} + MWMechanics::AiSetting mIndex; - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + OpGetAiSetting(MWMechanics::AiSetting index) + : mIndex(index) + { + } - runtime.push(ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getModified(false)); - } + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getAiSetting(mIndex).getModified(false); + runtime.push(value); + } }; - template + template class OpModAiSetting : public Interpreter::Opcode0 { - MWMechanics::CreatureStats::AiSetting mIndex; - public: - OpModAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {} + MWMechanics::AiSetting mIndex; - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = runtime[0].mInteger; - runtime.pop(); + public: + OpModAiSetting(MWMechanics::AiSetting index) + : mIndex(index) + { + } - int modified = ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getBase() + value; + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Integer value = runtime[0].mInteger; + runtime.pop(); - ptr.getClass().getCreatureStats (ptr).setAiSetting (mIndex, modified); - ptr.getClass().setBaseAISetting(ptr.getCellRef().getRefId(), mIndex, modified); - } + if (!ptr.getClass().isActor()) + return; + + int modified = ptr.getClass().getCreatureStats(ptr).getAiSetting(mIndex).getBase() + value; + + ptr.getClass().getCreatureStats(ptr).setAiSetting(mIndex, modified); + ptr.getClass().setBaseAISetting(ptr.getCellRef().getRefId(), mIndex, modified); + } }; - template + template class OpSetAiSetting : public Interpreter::Opcode0 { - MWMechanics::CreatureStats::AiSetting mIndex; - public: - OpSetAiSetting(MWMechanics::CreatureStats::AiSetting index) : mIndex(index) {} + MWMechanics::AiSetting mIndex; - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = runtime[0].mInteger; - runtime.pop(); + public: + OpSetAiSetting(MWMechanics::AiSetting index) + : mIndex(index) + { + } + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Integer value = runtime[0].mInteger; + runtime.pop(); + if (ptr.getClass().isActor()) + { ptr.getClass().getCreatureStats(ptr).setAiSetting(mIndex, value); ptr.getClass().setBaseAISetting(ptr.getCellRef().getRefId(), mIndex, value); } + } }; - template + template class OpAiFollow : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId actorID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + Interpreter::Type_Float duration = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float duration = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float x = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float x = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float y = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float y = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float z = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float z = runtime[0].mFloat; + // The value of the reset argument doesn't actually matter + bool repeat = arg0; + for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. - for (unsigned int i=0; i + template class OpAiFollowCell : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId actorID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + std::string_view cellID = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); - std::string cellID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + Interpreter::Type_Float duration = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float duration = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float x = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float x = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float y = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float y = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float z = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float z = runtime[0].mFloat; + // The value of the reset argument doesn't actually matter + bool repeat = arg0; + for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. - for (unsigned int i=0; i + template class OpGetCurrentAIPackage : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override + Interpreter::Type_Integer value = -1; + if (ptr.getClass().isActor()) { - MWWorld::Ptr ptr = R()(runtime); - - const auto value = static_cast(ptr.getClass().getCreatureStats (ptr).getAiSequence().getLastRunTypeId()); - - runtime.push (value); + const auto& stats = ptr.getClass().getCreatureStats(ptr); + if (!stats.isDead() || !stats.isDeathAnimationFinished()) + { + value = static_cast(stats.getAiSequence().getLastRunTypeId()); + } } + + runtime.push(value); + } }; - template + template class OpGetDetected : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr observer = R()(runtime, false); // required=false + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr observer = R()(runtime, false); // required=false - std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId actorID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - MWWorld::Ptr actor = MWBase::Environment::get().getWorld()->searchPtr(actorID, true, false); + MWWorld::Ptr actor = MWBase::Environment::get().getWorld()->searchPtr(actorID, true, false); - Interpreter::Type_Integer value = 0; - if (!actor.isEmpty()) - value = MWBase::Environment::get().getMechanicsManager()->isActorDetected(actor, observer); + Interpreter::Type_Integer value = 0; + if (!actor.isEmpty()) + value = MWBase::Environment::get().getMechanicsManager()->isActorDetected(actor, observer); - runtime.push (value); - } + runtime.push(value); + } }; - template + template class OpGetLineOfSight : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - - MWWorld::Ptr source = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { - std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + MWWorld::Ptr source = R()(runtime); + ESM::RefId actorID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - MWWorld::Ptr dest = MWBase::Environment::get().getWorld()->searchPtr(actorID, true, false); - bool value = false; - if (!dest.isEmpty() && source.getClass().isActor() && dest.getClass().isActor()) - { - value = MWBase::Environment::get().getWorld()->getLOS(source,dest); - } - runtime.push (value); + MWWorld::Ptr dest = MWBase::Environment::get().getWorld()->searchPtr(actorID, true, false); + bool value = false; + if (!dest.isEmpty() && source.getClass().isActor() && dest.getClass().isActor()) + { + value = MWBase::Environment::get().getWorld()->getLOS(source, dest); } + runtime.push(value); + } }; - template + template class OpGetTarget : public Interpreter::Opcode0 { - public: - void execute (Interpreter::Runtime &runtime) override - { - MWWorld::Ptr actor = R()(runtime); - std::string testedTargetId = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr actor = R()(runtime); + ESM::RefId testedTargetId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + bool targetsAreEqual = false; + if (actor.getClass().isActor()) + { const MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); - - bool targetsAreEqual = false; MWWorld::Ptr targetPtr; - if (creatureStats.getAiSequence().getCombatTarget (targetPtr)) + if (creatureStats.getAiSequence().getCombatTarget(targetPtr)) { if (!targetPtr.isEmpty() && targetPtr.getCellRef().getRefId() == testedTargetId) targetsAreEqual = true; } - else if (testedTargetId == "player") // Currently the player ID is hardcoded + else if (testedTargetId == "Player") // Currently the player ID is hardcoded { MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager(); bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::Greet_InProgress; bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor); targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor); } - runtime.push(int(targetsAreEqual)); } + runtime.push(targetsAreEqual); + } }; - template + template class OpStartCombat : public Interpreter::Opcode0 { - public: - void execute (Interpreter::Runtime &runtime) override - { - MWWorld::Ptr actor = R()(runtime); - std::string targetID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr actor = R()(runtime); + ESM::RefId targetID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(targetID, true, false); - if (!target.isEmpty()) - MWBase::Environment::get().getMechanicsManager()->startCombat(actor, target); - } + MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(targetID, true, false); + if (!target.isEmpty() && !target.getBase()->isDeleted() + && !target.getClass().getCreatureStats(target).isDead()) + MWBase::Environment::get().getMechanicsManager()->startCombat(actor, target, nullptr); + } }; - template + template class OpStopCombat : public Interpreter::Opcode0 { - public: - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr actor = R()(runtime); - MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); - creatureStats.getAiSequence().stopCombat(); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr actor = R()(runtime); + if (!actor.getClass().isActor()) + return; + MWBase::Environment::get().getMechanicsManager()->stopCombat(actor); + } }; class OpToggleAI : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - bool enabled = MWBase::Environment::get().getMechanicsManager()->toggleAI(); + public: + void execute(Interpreter::Runtime& runtime) override + { + bool enabled = MWBase::Environment::get().getMechanicsManager()->toggleAI(); - runtime.getContext().report (enabled ? "AI -> On" : "AI -> Off"); - } + runtime.getContext().report(enabled ? "AI -> On" : "AI -> Off"); + } }; template @@ -496,74 +551,101 @@ namespace MWScript Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); + if (!actor.getClass().isActor() || actor == MWMechanics::getPlayer()) + return; + MWMechanics::AiFace facePackage(x, y); actor.getClass().getCreatureStats(actor).getAiSequence().stack(facePackage, actor); } }; - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment3 (Compiler::Ai::opcodeAIActivate, new OpAiActivate); - interpreter.installSegment3 (Compiler::Ai::opcodeAIActivateExplicit, new OpAiActivate); - interpreter.installSegment3 (Compiler::Ai::opcodeAiTravel, new OpAiTravel); - interpreter.installSegment3 (Compiler::Ai::opcodeAiTravelExplicit, new OpAiTravel); - interpreter.installSegment3 (Compiler::Ai::opcodeAiEscort, new OpAiEscort); - interpreter.installSegment3 (Compiler::Ai::opcodeAiEscortExplicit, new OpAiEscort); - interpreter.installSegment3 (Compiler::Ai::opcodeAiEscortCell, new OpAiEscortCell); - interpreter.installSegment3 (Compiler::Ai::opcodeAiEscortCellExplicit, new OpAiEscortCell); - interpreter.installSegment3 (Compiler::Ai::opcodeAiWander, new OpAiWander); - interpreter.installSegment3 (Compiler::Ai::opcodeAiWanderExplicit, new OpAiWander); - interpreter.installSegment3 (Compiler::Ai::opcodeAiFollow, new OpAiFollow); - interpreter.installSegment3 (Compiler::Ai::opcodeAiFollowExplicit, new OpAiFollow); - interpreter.installSegment3 (Compiler::Ai::opcodeAiFollowCell, new OpAiFollowCell); - interpreter.installSegment3 (Compiler::Ai::opcodeAiFollowCellExplicit, new OpAiFollowCell); - interpreter.installSegment5 (Compiler::Ai::opcodeGetAiPackageDone, new OpGetAiPackageDone); - - interpreter.installSegment5 (Compiler::Ai::opcodeGetAiPackageDoneExplicit, - new OpGetAiPackageDone); - interpreter.installSegment5 (Compiler::Ai::opcodeGetCurrentAiPackage, new OpGetCurrentAIPackage); - interpreter.installSegment5 (Compiler::Ai::opcodeGetCurrentAiPackageExplicit, new OpGetCurrentAIPackage); - interpreter.installSegment5 (Compiler::Ai::opcodeGetDetected, new OpGetDetected); - interpreter.installSegment5 (Compiler::Ai::opcodeGetDetectedExplicit, new OpGetDetected); - interpreter.installSegment5 (Compiler::Ai::opcodeGetLineOfSight, new OpGetLineOfSight); - interpreter.installSegment5 (Compiler::Ai::opcodeGetLineOfSightExplicit, new OpGetLineOfSight); - interpreter.installSegment5 (Compiler::Ai::opcodeGetTarget, new OpGetTarget); - interpreter.installSegment5 (Compiler::Ai::opcodeGetTargetExplicit, new OpGetTarget); - interpreter.installSegment5 (Compiler::Ai::opcodeStartCombat, new OpStartCombat); - interpreter.installSegment5 (Compiler::Ai::opcodeStartCombatExplicit, new OpStartCombat); - interpreter.installSegment5 (Compiler::Ai::opcodeStopCombat, new OpStopCombat); - interpreter.installSegment5 (Compiler::Ai::opcodeStopCombatExplicit, new OpStopCombat); - interpreter.installSegment5 (Compiler::Ai::opcodeToggleAI, new OpToggleAI); - - interpreter.installSegment5 (Compiler::Ai::opcodeSetHello, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetHelloExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetFight, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetFightExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetFlee, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetFleeExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetAlarm, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - interpreter.installSegment5 (Compiler::Ai::opcodeSetAlarmExplicit, new OpSetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - - interpreter.installSegment5 (Compiler::Ai::opcodeModHello, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeModHelloExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeModFight, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeModFightExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeModFlee, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeModFleeExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeModAlarm, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - interpreter.installSegment5 (Compiler::Ai::opcodeModAlarmExplicit, new OpModAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - - interpreter.installSegment5 (Compiler::Ai::opcodeGetHello, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetHelloExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Hello)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetFight, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetFightExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Fight)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetFlee, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetFleeExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Flee)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarm, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarmExplicit, new OpGetAiSetting(MWMechanics::CreatureStats::AiSetting::AI_Alarm)); - - interpreter.installSegment5 (Compiler::Ai::opcodeFace, new OpFace); - interpreter.installSegment5 (Compiler::Ai::opcodeFaceExplicit, new OpFace); + interpreter.installSegment3>(Compiler::Ai::opcodeAIActivate); + interpreter.installSegment3>(Compiler::Ai::opcodeAIActivateExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiTravel); + interpreter.installSegment3>(Compiler::Ai::opcodeAiTravelExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiEscort); + interpreter.installSegment3>(Compiler::Ai::opcodeAiEscortExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiEscortCell); + interpreter.installSegment3>(Compiler::Ai::opcodeAiEscortCellExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiWander); + interpreter.installSegment3>(Compiler::Ai::opcodeAiWanderExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiFollow); + interpreter.installSegment3>(Compiler::Ai::opcodeAiFollowExplicit); + interpreter.installSegment3>(Compiler::Ai::opcodeAiFollowCell); + interpreter.installSegment3>(Compiler::Ai::opcodeAiFollowCellExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeGetAiPackageDone); + + interpreter.installSegment5>(Compiler::Ai::opcodeGetAiPackageDoneExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeGetCurrentAiPackage); + interpreter.installSegment5>( + Compiler::Ai::opcodeGetCurrentAiPackageExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeGetDetected); + interpreter.installSegment5>(Compiler::Ai::opcodeGetDetectedExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeGetLineOfSight); + interpreter.installSegment5>(Compiler::Ai::opcodeGetLineOfSightExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeGetTarget); + interpreter.installSegment5>(Compiler::Ai::opcodeGetTargetExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeStartCombat); + interpreter.installSegment5>(Compiler::Ai::opcodeStartCombatExplicit); + interpreter.installSegment5>(Compiler::Ai::opcodeStopCombat); + interpreter.installSegment5>(Compiler::Ai::opcodeStopCombatExplicit); + interpreter.installSegment5(Compiler::Ai::opcodeToggleAI); + + interpreter.installSegment5>( + Compiler::Ai::opcodeSetHello, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>( + Compiler::Ai::opcodeSetHelloExplicit, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>( + Compiler::Ai::opcodeSetFight, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>( + Compiler::Ai::opcodeSetFightExplicit, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>( + Compiler::Ai::opcodeSetFlee, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>( + Compiler::Ai::opcodeSetFleeExplicit, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>( + Compiler::Ai::opcodeSetAlarm, MWMechanics::AiSetting::Alarm); + interpreter.installSegment5>( + Compiler::Ai::opcodeSetAlarmExplicit, MWMechanics::AiSetting::Alarm); + + interpreter.installSegment5>( + Compiler::Ai::opcodeModHello, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>( + Compiler::Ai::opcodeModHelloExplicit, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>( + Compiler::Ai::opcodeModFight, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>( + Compiler::Ai::opcodeModFightExplicit, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>( + Compiler::Ai::opcodeModFlee, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>( + Compiler::Ai::opcodeModFleeExplicit, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>( + Compiler::Ai::opcodeModAlarm, MWMechanics::AiSetting::Alarm); + interpreter.installSegment5>( + Compiler::Ai::opcodeModAlarmExplicit, MWMechanics::AiSetting::Alarm); + + interpreter.installSegment5>( + Compiler::Ai::opcodeGetHello, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>( + Compiler::Ai::opcodeGetHelloExplicit, MWMechanics::AiSetting::Hello); + interpreter.installSegment5>( + Compiler::Ai::opcodeGetFight, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>( + Compiler::Ai::opcodeGetFightExplicit, MWMechanics::AiSetting::Fight); + interpreter.installSegment5>( + Compiler::Ai::opcodeGetFlee, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>( + Compiler::Ai::opcodeGetFleeExplicit, MWMechanics::AiSetting::Flee); + interpreter.installSegment5>( + Compiler::Ai::opcodeGetAlarm, MWMechanics::AiSetting::Alarm); + interpreter.installSegment5>( + Compiler::Ai::opcodeGetAlarmExplicit, MWMechanics::AiSetting::Alarm); + + interpreter.installSegment5>(Compiler::Ai::opcodeFace); + interpreter.installSegment5>(Compiler::Ai::opcodeFaceExplicit); } } } diff --git a/apps/openmw/mwscript/aiextensions.hpp b/apps/openmw/mwscript/aiextensions.hpp index e9e36113cf8..5fd0db7d238 100644 --- a/apps/openmw/mwscript/aiextensions.hpp +++ b/apps/openmw/mwscript/aiextensions.hpp @@ -16,7 +16,7 @@ namespace MWScript /// \brief AI-related script functionality namespace Ai { - void installOpcodes (Interpreter::Interpreter& interpreter); + void installOpcodes(Interpreter::Interpreter& interpreter); } } diff --git a/apps/openmw/mwscript/animationextensions.cpp b/apps/openmw/mwscript/animationextensions.cpp index 8bb6cc6adae..16c1f5a134b 100644 --- a/apps/openmw/mwscript/animationextensions.cpp +++ b/apps/openmw/mwscript/animationextensions.cpp @@ -1,112 +1,108 @@ #include "animationextensions.hpp" -#include #include +#include #include #include -#include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" -#include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript { namespace Animation { - template + template class OpSkipAnim : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - MWBase::Environment::get().getMechanicsManager()->skipAnimation (ptr); - } + MWBase::Environment::get().getMechanicsManager()->skipAnimation(ptr); + } }; - template + template class OpPlayAnim : public Interpreter::Opcode1 { - public: - - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::Ptr ptr = R()(runtime); - if (!ptr.getRefData().isEnabled()) - return; + if (!ptr.getRefData().isEnabled()) + return; - std::string group = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + std::string_view group = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); - Interpreter::Type_Integer mode = 0; + Interpreter::Type_Integer mode = 0; - if (arg0==1) - { - mode = runtime[0].mInteger; - runtime.pop(); + if (arg0 == 1) + { + mode = runtime[0].mInteger; + runtime.pop(); - if (mode<0 || mode>2) - throw std::runtime_error ("animation mode out of range"); - } + if (mode < 0 || mode > 2) + throw std::runtime_error("animation mode out of range"); + } - MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, std::numeric_limits::max(), true); - } + MWBase::Environment::get().getMechanicsManager()->playAnimationGroup( + ptr, group, mode, std::numeric_limits::max(), true); + } }; - template + template class OpLoopAnim : public Interpreter::Opcode1 { - public: - - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::Ptr ptr = R()(runtime); - if (!ptr.getRefData().isEnabled()) - return; + if (!ptr.getRefData().isEnabled()) + return; - std::string group = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + std::string_view group = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); - Interpreter::Type_Integer loops = runtime[0].mInteger; - runtime.pop(); + Interpreter::Type_Integer loops = runtime[0].mInteger; + runtime.pop(); - if (loops<0) - throw std::runtime_error ("number of animation loops must be non-negative"); + if (loops < 0) + throw std::runtime_error("number of animation loops must be non-negative"); - Interpreter::Type_Integer mode = 0; + Interpreter::Type_Integer mode = 0; - if (arg0==1) - { - mode = runtime[0].mInteger; - runtime.pop(); + if (arg0 == 1) + { + mode = runtime[0].mInteger; + runtime.pop(); - if (mode<0 || mode>2) - throw std::runtime_error ("animation mode out of range"); - } + if (mode < 0 || mode > 2) + throw std::runtime_error("animation mode out of range"); + } - MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, loops + 1, true); - } + MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(ptr, group, mode, loops, true); + } }; - - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Animation::opcodeSkipAnim, new OpSkipAnim); - interpreter.installSegment5 (Compiler::Animation::opcodeSkipAnimExplicit, new OpSkipAnim); - interpreter.installSegment3 (Compiler::Animation::opcodePlayAnim, new OpPlayAnim); - interpreter.installSegment3 (Compiler::Animation::opcodePlayAnimExplicit, new OpPlayAnim); - interpreter.installSegment3 (Compiler::Animation::opcodeLoopAnim, new OpLoopAnim); - interpreter.installSegment3 (Compiler::Animation::opcodeLoopAnimExplicit, new OpLoopAnim); + interpreter.installSegment5>(Compiler::Animation::opcodeSkipAnim); + interpreter.installSegment5>(Compiler::Animation::opcodeSkipAnimExplicit); + interpreter.installSegment3>(Compiler::Animation::opcodePlayAnim); + interpreter.installSegment3>(Compiler::Animation::opcodePlayAnimExplicit); + interpreter.installSegment3>(Compiler::Animation::opcodeLoopAnim); + interpreter.installSegment3>(Compiler::Animation::opcodeLoopAnimExplicit); } } } diff --git a/apps/openmw/mwscript/animationextensions.hpp b/apps/openmw/mwscript/animationextensions.hpp index ff619ab73af..a6805f7ee31 100644 --- a/apps/openmw/mwscript/animationextensions.hpp +++ b/apps/openmw/mwscript/animationextensions.hpp @@ -15,9 +15,9 @@ namespace MWScript { namespace Animation { - void registerExtensions (Compiler::Extensions& extensions); + void registerExtensions(Compiler::Extensions& extensions); - void installOpcodes (Interpreter::Interpreter& interpreter); + void installOpcodes(Interpreter::Interpreter& interpreter); } } diff --git a/apps/openmw/mwscript/cellextensions.cpp b/apps/openmw/mwscript/cellextensions.cpp index b0977984fbe..d913eb09150 100644 --- a/apps/openmw/mwscript/cellextensions.cpp +++ b/apps/openmw/mwscript/cellextensions.cpp @@ -2,264 +2,260 @@ #include -#include "../mwworld/esmstore.hpp" - #include +#include #include -#include #include +#include + +#include + +#include -#include "../mwworld/actionteleport.hpp" -#include "../mwworld/cellstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwworld/actionteleport.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/scene.hpp" #include "../mwmechanics/actorutil.hpp" -#include "interpretercontext.hpp" - namespace MWScript { namespace Cell { class OpCellChanged : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.push (MWBase::Environment::get().getWorld()->hasCellChanged() ? 1 : 0); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.push(MWBase::Environment::get().getWorldScene()->hasCellChanged() ? 1 : 0); + } }; class OpTestCells : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override + public: + void execute(Interpreter::Runtime& runtime) override + { + if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame) { - if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame) - { - runtime.getContext().report("Use TestCells from the main menu, when there is no active game session."); - return; - } + runtime.getContext().report( + "Use TestCells from the main menu, when there is no active game session."); + return; + } - bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode(); - if (wasConsole) - MWBase::Environment::get().getWindowManager()->toggleConsole(); + bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode(); + if (wasConsole) + MWBase::Environment::get().getWindowManager()->toggleConsole(); - MWBase::Environment::get().getWorld()->testExteriorCells(); + MWBase::Environment::get().getWorldScene()->testExteriorCells(); - if (wasConsole) - MWBase::Environment::get().getWindowManager()->toggleConsole(); - } + if (wasConsole) + MWBase::Environment::get().getWindowManager()->toggleConsole(); + } }; class OpTestInteriorCells : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override + public: + void execute(Interpreter::Runtime& runtime) override + { + if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame) { - if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame) - { - runtime.getContext().report("Use TestInteriorCells from the main menu, when there is no active game session."); - return; - } + runtime.getContext().report( + "Use TestInteriorCells from the main menu, when there is no active game session."); + return; + } - bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode(); - if (wasConsole) - MWBase::Environment::get().getWindowManager()->toggleConsole(); + bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode(); + if (wasConsole) + MWBase::Environment::get().getWindowManager()->toggleConsole(); - MWBase::Environment::get().getWorld()->testInteriorCells(); + MWBase::Environment::get().getWorldScene()->testInteriorCells(); - if (wasConsole) - MWBase::Environment::get().getWindowManager()->toggleConsole(); - } + if (wasConsole) + MWBase::Environment::get().getWindowManager()->toggleConsole(); + } }; class OpCOC : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + std::string_view cell = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + + ESM::Position pos; + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::Ptr playerPtr = world->getPlayerPtr(); - void execute (Interpreter::Runtime& runtime) override + if (const ESM::RefId refId = world->findExteriorPosition(cell, pos); !refId.empty()) + { + MWWorld::ActionTeleport(refId, pos, false).execute(playerPtr); + playerPtr = world->getPlayerPtr(); // could be changed by ActionTeleport + world->adjustPosition(playerPtr, false); + return; + } + if (const ESM::RefId refId = world->findInteriorPosition(cell, pos); !refId.empty()) { - std::string cell = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - - ESM::Position pos; - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Ptr playerPtr = world->getPlayerPtr(); - - if (world->findExteriorPosition(cell, pos)) - { - MWWorld::ActionTeleport("", pos, false).execute(playerPtr); - world->adjustPosition(playerPtr, false); - } - else - { - // Change to interior even if findInteriorPosition() - // yields false. In this case position will be zero-point. - world->findInteriorPosition(cell, pos); - MWWorld::ActionTeleport(cell, pos, false).execute(playerPtr); - } + MWWorld::ActionTeleport(refId, pos, false).execute(playerPtr); + return; } + throw std::runtime_error("Cell " + std::string(cell) + " is not found"); + } }; class OpCOE : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - Interpreter::Type_Integer x = runtime[0].mInteger; - runtime.pop(); - - Interpreter::Type_Integer y = runtime[0].mInteger; - runtime.pop(); - - ESM::Position pos; - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Ptr playerPtr = world->getPlayerPtr(); - - world->indexToPosition (x, y, pos.pos[0], pos.pos[1], true); - pos.pos[2] = 0; - - pos.rot[0] = pos.rot[1] = pos.rot[2] = 0; - - MWWorld::ActionTeleport("", pos, false).execute(playerPtr); - world->adjustPosition(playerPtr, false); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + Interpreter::Type_Integer x = runtime[0].mInteger; + runtime.pop(); + + Interpreter::Type_Integer y = runtime[0].mInteger; + runtime.pop(); + + ESM::Position pos; + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::Ptr playerPtr = world->getPlayerPtr(); + + const osg::Vec2f posFromIndex + = ESM::indexToPosition(ESM::ExteriorCellLocation(x, y, ESM::Cell::sDefaultWorldspaceId), true); + pos.pos[0] = posFromIndex.x(); + pos.pos[1] = posFromIndex.y(); + pos.pos[2] = 0; + + pos.rot[0] = pos.rot[1] = pos.rot[2] = 0; + + MWWorld::ActionTeleport(ESM::RefId::esm3ExteriorCell(x, y), pos, false).execute(playerPtr); + playerPtr = world->getPlayerPtr(); // could be changed by ActionTeleport + world->adjustPosition(playerPtr, false); + } }; class OpGetInterior : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override + public: + void execute(Interpreter::Runtime& runtime) override + { + if (!MWMechanics::getPlayer().isInCell()) { - if (!MWMechanics::getPlayer().isInCell()) - { - runtime.push (0); - return; - } + runtime.push(0); + return; + } - bool interior = - !MWMechanics::getPlayer().getCell()->getCell()->isExterior(); + bool interior = !MWMechanics::getPlayer().getCell()->getCell()->isExterior(); - runtime.push (interior ? 1 : 0); - } + runtime.push(interior ? 1 : 0); + } }; class OpGetPCCell : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + std::string_view name = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override + if (!MWMechanics::getPlayer().isInCell()) { - std::string name = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - - if (!MWMechanics::getPlayer().isInCell()) - { - runtime.push(0); - return; - } - const MWWorld::CellStore *cell = MWMechanics::getPlayer().getCell(); - - std::string current = MWBase::Environment::get().getWorld()->getCellName(cell); - Misc::StringUtils::lowerCaseInPlace(current); + runtime.push(0); + return; + } + const MWWorld::CellStore* cell = MWMechanics::getPlayer().getCell(); - bool match = current.length()>=name.length() && - current.substr (0, name.length())==name; + std::string_view current = MWBase::Environment::get().getWorld()->getCellName(cell); + bool match = Misc::StringUtils::ciCompareLen(name, current, name.length()) == 0; - runtime.push (match ? 1 : 0); - } + runtime.push(match ? 1 : 0); + } }; class OpGetWaterLevel : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override + public: + void execute(Interpreter::Runtime& runtime) override + { + if (!MWMechanics::getPlayer().isInCell()) { - if (!MWMechanics::getPlayer().isInCell()) - { - runtime.push(0.f); - return; - } - MWWorld::CellStore *cell = MWMechanics::getPlayer().getCell(); - if (cell->isExterior()) - runtime.push(0.f); // vanilla oddity, return 0 even though water is actually at -1 - else if (cell->getCell()->hasWater()) - runtime.push (cell->getWaterLevel()); - else - runtime.push (-std::numeric_limits::max()); + runtime.push(0.f); + return; } + MWWorld::CellStore* cell = MWMechanics::getPlayer().getCell(); + if (cell->isExterior()) + runtime.push(0.f); // vanilla oddity, return 0 even though water is actually at -1 + else if (cell->getCell()->hasWater()) + runtime.push(cell->getWaterLevel()); + else + runtime.push(-std::numeric_limits::max()); + } }; class OpSetWaterLevel : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + Interpreter::Type_Float level = runtime[0].mFloat; + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override + if (!MWMechanics::getPlayer().isInCell()) { - Interpreter::Type_Float level = runtime[0].mFloat; - - if (!MWMechanics::getPlayer().isInCell()) - { - return; - } + return; + } - MWWorld::CellStore *cell = MWMechanics::getPlayer().getCell(); + MWWorld::CellStore* cell = MWMechanics::getPlayer().getCell(); - if (cell->getCell()->isExterior()) - throw std::runtime_error("Can't set water level in exterior cell"); + if (cell->getCell()->isExterior()) + throw std::runtime_error("Can't set water level in exterior cell"); - cell->setWaterLevel (level); - MWBase::Environment::get().getWorld()->setWaterHeight (cell->getWaterLevel()); - } + cell->setWaterLevel(level); + MWBase::Environment::get().getWorld()->setWaterHeight(cell->getWaterLevel()); + } }; class OpModWaterLevel : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + Interpreter::Type_Float level = runtime[0].mFloat; + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override + if (!MWMechanics::getPlayer().isInCell()) { - Interpreter::Type_Float level = runtime[0].mFloat; - - if (!MWMechanics::getPlayer().isInCell()) - { - return; - } + return; + } - MWWorld::CellStore *cell = MWMechanics::getPlayer().getCell(); + MWWorld::CellStore* cell = MWMechanics::getPlayer().getCell(); - if (cell->getCell()->isExterior()) - throw std::runtime_error("Can't set water level in exterior cell"); + if (cell->getCell()->isExterior()) + throw std::runtime_error("Can't set water level in exterior cell"); - cell->setWaterLevel (cell->getWaterLevel()+level); - MWBase::Environment::get().getWorld()->setWaterHeight(cell->getWaterLevel()); - } + cell->setWaterLevel(cell->getWaterLevel() + level); + MWBase::Environment::get().getWorld()->setWaterHeight(cell->getWaterLevel()); + } }; - - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Cell::opcodeCellChanged, new OpCellChanged); - interpreter.installSegment5 (Compiler::Cell::opcodeTestCells, new OpTestCells); - interpreter.installSegment5 (Compiler::Cell::opcodeTestInteriorCells, new OpTestInteriorCells); - interpreter.installSegment5 (Compiler::Cell::opcodeCOC, new OpCOC); - interpreter.installSegment5 (Compiler::Cell::opcodeCOE, new OpCOE); - interpreter.installSegment5 (Compiler::Cell::opcodeGetInterior, new OpGetInterior); - interpreter.installSegment5 (Compiler::Cell::opcodeGetPCCell, new OpGetPCCell); - interpreter.installSegment5 (Compiler::Cell::opcodeGetWaterLevel, new OpGetWaterLevel); - interpreter.installSegment5 (Compiler::Cell::opcodeSetWaterLevel, new OpSetWaterLevel); - interpreter.installSegment5 (Compiler::Cell::opcodeModWaterLevel, new OpModWaterLevel); + interpreter.installSegment5(Compiler::Cell::opcodeCellChanged); + interpreter.installSegment5(Compiler::Cell::opcodeTestCells); + interpreter.installSegment5(Compiler::Cell::opcodeTestInteriorCells); + interpreter.installSegment5(Compiler::Cell::opcodeCOC); + interpreter.installSegment5(Compiler::Cell::opcodeCOE); + interpreter.installSegment5(Compiler::Cell::opcodeGetInterior); + interpreter.installSegment5(Compiler::Cell::opcodeGetPCCell); + interpreter.installSegment5(Compiler::Cell::opcodeGetWaterLevel); + interpreter.installSegment5(Compiler::Cell::opcodeSetWaterLevel); + interpreter.installSegment5(Compiler::Cell::opcodeModWaterLevel); } } } diff --git a/apps/openmw/mwscript/cellextensions.hpp b/apps/openmw/mwscript/cellextensions.hpp index 0891cb9dc88..d3b8bc00b5c 100644 --- a/apps/openmw/mwscript/cellextensions.hpp +++ b/apps/openmw/mwscript/cellextensions.hpp @@ -15,11 +15,9 @@ namespace MWScript { /// \brief cell-related script functionality namespace Cell - { - void installOpcodes (Interpreter::Interpreter& interpreter); + { + void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif - - diff --git a/apps/openmw/mwscript/compilercontext.cpp b/apps/openmw/mwscript/compilercontext.cpp index 4a7038e1cbd..eb39f57628c 100644 --- a/apps/openmw/mwscript/compilercontext.cpp +++ b/apps/openmw/mwscript/compilercontext.cpp @@ -2,98 +2,94 @@ #include "../mwworld/esmstore.hpp" -#include - #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/scriptmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/ptr.hpp" #include "../mwworld/class.hpp" #include "../mwworld/manualref.hpp" +#include "../mwworld/ptr.hpp" namespace MWScript { - CompilerContext::CompilerContext (Type type) - : mType (type) - {} + CompilerContext::CompilerContext(Type type) + : mType(type) + { + } bool CompilerContext::canDeclareLocals() const { - return mType==Type_Full; + return mType == Type_Full; } - char CompilerContext::getGlobalType (const std::string& name) const + char CompilerContext::getGlobalType(const std::string& name) const { - return MWBase::Environment::get().getWorld()->getGlobalVariableType (name); + return MWBase::Environment::get().getWorld()->getGlobalVariableType(name); } - std::pair CompilerContext::getMemberType (const std::string& name, - const std::string& id) const + std::pair CompilerContext::getMemberType(const std::string& name, const ESM::RefId& id) const { - std::string script; + ESM::RefId script; bool reference = false; - if (const ESM::Script *scriptRecord = - MWBase::Environment::get().getWorld()->getStore().get().search (id)) + if (const ESM::Script* scriptRecord = MWBase::Environment::get().getESMStore()->get().search(id)) { script = scriptRecord->mId; } else { - MWWorld::ManualRef ref (MWBase::Environment::get().getWorld()->getStore(), id); + MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), id); - script = ref.getPtr().getClass().getScript (ref.getPtr()); + script = ref.getPtr().getClass().getScript(ref.getPtr()); reference = true; } char type = ' '; if (!script.empty()) - type = MWBase::Environment::get().getScriptManager()->getLocals (script).getType ( - Misc::StringUtils::lowerCase (name)); + type = MWBase::Environment::get().getScriptManager()->getLocals(script).getType( + Misc::StringUtils::lowerCase(name)); - return std::make_pair (type, reference); + return std::make_pair(type, reference); } - bool CompilerContext::isId (const std::string& name) const + bool CompilerContext::isId(const ESM::RefId& name) const { - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); - - return - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name) || - store.get().search (name); - } - - bool CompilerContext::isJournalId (const std::string& name) const - { - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); - - const ESM::Dialogue *topic = store.get().search (name); - - return topic && topic->mType==ESM::Dialogue::Journal; + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + + return store.get().search(name) || store.get().search(name) + || store.get().search(name) || store.get().search(name) + || store.get().search(name) || store.get().search(name) + || store.get().search(name) || store.get().search(name) + || store.get().search(name) || store.get().search(name) + || store.get().search(name) || store.get().search(name) + || store.get().search(name) || store.get().search(name) + || store.get().search(name) || store.get().search(name) + || store.get().search(name) || store.get().search(name) + || store.get().search(name) || store.get().search(name) + || store.get().search(name); } } diff --git a/apps/openmw/mwscript/compilercontext.hpp b/apps/openmw/mwscript/compilercontext.hpp index 00b10ea06dd..eaf794853b3 100644 --- a/apps/openmw/mwscript/compilercontext.hpp +++ b/apps/openmw/mwscript/compilercontext.hpp @@ -3,45 +3,43 @@ #include +namespace ESM +{ + class RefId; +} + namespace MWScript { class CompilerContext : public Compiler::Context { - public: - - enum Type - { - Type_Full, // global, local, targeted - Type_Dialogue, - Type_Console - }; - - private: - - Type mType; - - public: - - CompilerContext (Type type); - - /// Is the compiler allowed to declare local variables? - bool canDeclareLocals() const override; - - /// 'l: long, 's': short, 'f': float, ' ': does not exist. - char getGlobalType (const std::string& name) const override; - - std::pair getMemberType (const std::string& name, - const std::string& id) const override; - ///< Return type of member variable \a name in script \a id or in script of reference of - /// \a id - /// \return first: 'l: long, 's': short, 'f': float, ' ': does not exist. - /// second: true: script of reference - - bool isId (const std::string& name) const override; - ///< Does \a name match an ID, that can be referenced? - - bool isJournalId (const std::string& name) const override; - ///< Does \a name match a journal ID? + public: + enum Type + { + Type_Full, // global, local, targeted + Type_Dialogue, + Type_Console + }; + + private: + Type mType; + + public: + CompilerContext(Type type); + + /// Is the compiler allowed to declare local variables? + bool canDeclareLocals() const override; + + /// 'l: long, 's': short, 'f': float, ' ': does not exist. + char getGlobalType(const std::string& name) const override; + + std::pair getMemberType(const std::string& name, const ESM::RefId& id) const override; + ///< Return type of member variable \a name in script \a id or in script of reference of + /// \a id + /// \return first: 'l: long, 's': short, 'f': float, ' ': does not exist. + /// second: true: script of reference + + bool isId(const ESM::RefId& name) const override; + ///< Does \a name match an ID, that can be referenced? }; } diff --git a/apps/openmw/mwscript/consoleextensions.cpp b/apps/openmw/mwscript/consoleextensions.cpp index 749ec809694..08a13e43ac1 100644 --- a/apps/openmw/mwscript/consoleextensions.cpp +++ b/apps/openmw/mwscript/consoleextensions.cpp @@ -6,9 +6,6 @@ namespace MWScript { namespace Console { - void installOpcodes (Interpreter::Interpreter& interpreter) - { - - } + void installOpcodes(Interpreter::Interpreter& interpreter) {} } } diff --git a/apps/openmw/mwscript/consoleextensions.hpp b/apps/openmw/mwscript/consoleextensions.hpp index 5571a546941..828557271a4 100644 --- a/apps/openmw/mwscript/consoleextensions.hpp +++ b/apps/openmw/mwscript/consoleextensions.hpp @@ -16,7 +16,7 @@ namespace MWScript /// \brief Script functionality limited to the console namespace Console { - void installOpcodes (Interpreter::Interpreter& interpreter); + void installOpcodes(Interpreter::Interpreter& interpreter); } } diff --git a/apps/openmw/mwscript/containerextensions.cpp b/apps/openmw/mwscript/containerextensions.cpp index 375242f17ce..9708a503eee 100644 --- a/apps/openmw/mwscript/containerextensions.cpp +++ b/apps/openmw/mwscript/containerextensions.cpp @@ -11,65 +11,69 @@ #include #include -#include +#include -#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwclass/container.hpp" - #include "../mwworld/action.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/manualref.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/levelledlist.hpp" +#include "interpretercontext.hpp" #include "ref.hpp" namespace { - void addToStore(const MWWorld::Ptr& itemPtr, int count, MWWorld::Ptr& ptr, MWWorld::ContainerStore& store, bool resolve = true) + void addToStore(const MWWorld::Ptr& itemPtr, int count, MWWorld::ContainerStore& store, bool resolve = true) { if (itemPtr.getClass().getScript(itemPtr).empty()) { - store.add (itemPtr, count, ptr, true, resolve); + store.add(itemPtr, count, true, resolve); } else { // Adding just one item per time to make sure there isn't a stack of scripted items for (int i = 0; i < count; i++) - store.add (itemPtr, 1, ptr, true, resolve); + store.add(itemPtr, 1, true, resolve); } } - void addRandomToStore(const MWWorld::Ptr& itemPtr, int count, MWWorld::Ptr& owner, MWWorld::ContainerStore& store, bool topLevel = true) + void addRandomToStore(const MWWorld::Ptr& itemPtr, int count, MWWorld::ContainerStore& store, bool topLevel = true) { - if(itemPtr.getTypeName() == typeid(ESM::ItemLevList).name()) + if (itemPtr.getType() == ESM::ItemLevList::sRecordId) { const ESM::ItemLevList* levItemList = itemPtr.get()->mBase; - if(topLevel && count > 1 && levItemList->mFlags & ESM::ItemLevList::Each) + if (topLevel && count > 1 && levItemList->mFlags & ESM::ItemLevList::Each) { - for(int i = 0; i < count; i++) - addRandomToStore(itemPtr, 1, owner, store, true); + for (int i = 0; i < count; i++) + addRandomToStore(itemPtr, 1, store, true); } else { - std::string itemId = MWMechanics::getLevelledItem(itemPtr.get()->mBase, false); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::RefId& itemId + = MWMechanics::getLevelledItem(itemPtr.get()->mBase, false, prng); if (itemId.empty()) return; - MWWorld::ManualRef manualRef(MWBase::Environment::get().getWorld()->getStore(), itemId, 1); - addRandomToStore(manualRef.getPtr(), count, owner, store, false); + MWWorld::ManualRef manualRef(*MWBase::Environment::get().getESMStore(), itemId, 1); + addRandomToStore(manualRef.getPtr(), count, store, false); } } else - addToStore(itemPtr, count, owner, store); + addToStore(itemPtr, count, store); } } @@ -77,432 +81,452 @@ namespace MWScript { namespace Container { - template + template class OpAddItem : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + + ESM::RefId item = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + + Interpreter::Type_Integer count = runtime[0].mInteger; + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override + if (!MWBase::Environment::get().getESMStore()->find(item)) { - MWWorld::Ptr ptr = R()(runtime); + runtime.getContext().report("Failed to add item '" + item.getRefIdString() + "': unknown ID"); + return; + } - std::string item = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + if (count < 0) + count = static_cast(count); - Interpreter::Type_Integer count = runtime[0].mInteger; - runtime.pop(); + // no-op + if (count == 0) + return; - if (count<0) - count = static_cast(count); + if (item == "gold_005" || item == "gold_010" || item == "gold_025" || item == "gold_100") + item = MWWorld::ContainerStore::sGoldId; - // no-op - if (count == 0) - return; + // Check if "item" can be placed in a container + MWWorld::ManualRef manualRef(*MWBase::Environment::get().getESMStore(), item, 1); + MWWorld::Ptr itemPtr = manualRef.getPtr(); + bool isLevelledList = itemPtr.getClass().getType() == ESM::ItemLevList::sRecordId; + if (!isLevelledList) + MWWorld::ContainerStore::getType(itemPtr); - if(::Misc::StringUtils::ciEqual(item, "gold_005") - || ::Misc::StringUtils::ciEqual(item, "gold_010") - || ::Misc::StringUtils::ciEqual(item, "gold_025") - || ::Misc::StringUtils::ciEqual(item, "gold_100")) - item = "gold_001"; - - // Check if "item" can be placed in a container - MWWorld::ManualRef manualRef(MWBase::Environment::get().getWorld()->getStore(), item, 1); - MWWorld::Ptr itemPtr = manualRef.getPtr(); - bool isLevelledList = itemPtr.getClass().getTypeName() == typeid(ESM::ItemLevList).name(); - if(!isLevelledList) - MWWorld::ContainerStore::getType(itemPtr); - - // Explicit calls to non-unique actors affect the base record - if(!R::implicit && ptr.getClass().isActor() && MWBase::Environment::get().getWorld()->getStore().getRefCount(ptr.getCellRef().getRefId()) > 1) - { - ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, count); - return; - } + // Explicit calls to non-unique actors affect the base record + if (!R::implicit && ptr.getClass().isActor() + && MWBase::Environment::get().getESMStore()->getRefCount(ptr.getCellRef().getRefId()) > 1) + { + ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, count); + return; + } - // Calls to unresolved containers affect the base record - if(ptr.getClass().getTypeName() == typeid(ESM::Container).name() && (!ptr.getRefData().getCustomData() || - !ptr.getClass().getContainerStore(ptr).isResolved())) + // Calls to unresolved containers affect the base record + if (ptr.getClass().getType() == ESM::Container::sRecordId + && (!ptr.getRefData().getCustomData() || !ptr.getClass().getContainerStore(ptr).isResolved())) + { + ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, count); + const ESM::Container* baseRecord + = MWBase::Environment::get().getESMStore()->get().find( + ptr.getCellRef().getRefId()); + const auto& ptrs = MWBase::Environment::get().getWorld()->getAll(ptr.getCellRef().getRefId()); + for (const auto& container : ptrs) { - ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, count); - const ESM::Container* baseRecord = MWBase::Environment::get().getWorld()->getStore().get().find(ptr.getCellRef().getRefId()); - const auto& ptrs = MWBase::Environment::get().getWorld()->getAll(ptr.getCellRef().getRefId()); - for(const auto& container : ptrs) + // use the new base record + container.get()->mBase = baseRecord; + if (container.getRefData().getCustomData()) { - // use the new base record - container.get()->mBase = baseRecord; - if(container.getRefData().getCustomData()) + auto& store = container.getClass().getContainerStore(container); + if (isLevelledList) { - auto& store = container.getClass().getContainerStore(container); - if(isLevelledList) + if (store.isResolved()) { - if(store.isResolved()) - { - addRandomToStore(itemPtr, count, ptr, store); - } + addRandomToStore(itemPtr, count, store); } - else - addToStore(itemPtr, count, ptr, store, store.isResolved()); } + else + addToStore(itemPtr, count, store, store.isResolved()); } - return; } - MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); - if(isLevelledList) - addRandomToStore(itemPtr, count, ptr, store); + return; + } + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + if (isLevelledList) + addRandomToStore(itemPtr, count, store); + else + addToStore(itemPtr, count, store); + + // Spawn a messagebox (only for items added to player's inventory and if player is talking to someone) + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + // The two GMST entries below expand to strings informing the player of what, and how many of it has + // been added to their inventory + std::string msgBox; + std::string_view itemName = itemPtr.getClass().getName(itemPtr); + if (count == 1) + { + msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage60}"); + msgBox = ::Misc::StringUtils::format(msgBox, itemName); + } else - addToStore(itemPtr, count, ptr, store); - - // Spawn a messagebox (only for items added to player's inventory and if player is talking to someone) - if (ptr == MWBase::Environment::get().getWorld ()->getPlayerPtr() ) { - // The two GMST entries below expand to strings informing the player of what, and how many of it has been added to their inventory - std::string msgBox; - std::string itemName = itemPtr.getClass().getName(itemPtr); - if (count == 1) - { - msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage60}"); - msgBox = ::Misc::StringUtils::format(msgBox, itemName); - } - else - { - msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage61}"); - msgBox = ::Misc::StringUtils::format(msgBox, count, itemName); - } - MWBase::Environment::get().getWindowManager()->messageBox(msgBox, MWGui::ShowInDialogueMode_Only); + msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage61}"); + msgBox = ::Misc::StringUtils::format(msgBox, count, itemName); } + MWBase::Environment::get().getWindowManager()->messageBox(msgBox, MWGui::ShowInDialogueMode_Only); } + } }; - template + template class OpGetItemCount : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime, false); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId item = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string item = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + if (ptr.isEmpty() || (ptr.getType() != ESM::Container::sRecordId && !ptr.getClass().isActor())) + { + runtime.push(0); + return; + } - if(::Misc::StringUtils::ciEqual(item, "gold_005") - || ::Misc::StringUtils::ciEqual(item, "gold_010") - || ::Misc::StringUtils::ciEqual(item, "gold_025") - || ::Misc::StringUtils::ciEqual(item, "gold_100")) - item = "gold_001"; + if (item == "gold_005" || item == "gold_010" || item == "gold_025" || item == "gold_100") + item = MWWorld::ContainerStore::sGoldId; - MWWorld::ContainerStore& store = ptr.getClass().getContainerStore (ptr); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); - runtime.push (store.count(item)); - } + runtime.push(store.count(item)); + } }; - template + template class OpRemoveItem : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId item = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string item = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + Interpreter::Type_Integer count = runtime[0].mInteger; + runtime.pop(); - Interpreter::Type_Integer count = runtime[0].mInteger; - runtime.pop(); + if (!MWBase::Environment::get().getESMStore()->find(item)) + { + runtime.getContext().report("Failed to remove item '" + item.getRefIdString() + "': unknown ID"); + return; + } - if (count<0) - throw std::runtime_error ("second argument for RemoveItem must be non-negative"); + if (count < 0) + count = static_cast(count); - // no-op - if (count == 0) - return; + // no-op + if (count == 0) + return; - if(::Misc::StringUtils::ciEqual(item, "gold_005") - || ::Misc::StringUtils::ciEqual(item, "gold_010") - || ::Misc::StringUtils::ciEqual(item, "gold_025") - || ::Misc::StringUtils::ciEqual(item, "gold_100")) - item = "gold_001"; + if (item == "gold_005" || item == "gold_010" || item == "gold_025" || item == "gold_100") + item = MWWorld::ContainerStore::sGoldId; - // Explicit calls to non-unique actors affect the base record - if(!R::implicit && ptr.getClass().isActor() && MWBase::Environment::get().getWorld()->getStore().getRefCount(ptr.getCellRef().getRefId()) > 1) - { - ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, -count); - return; - } - // Calls to unresolved containers affect the base record instead - else if(ptr.getClass().getTypeName() == typeid(ESM::Container).name() && - (!ptr.getRefData().getCustomData() || !ptr.getClass().getContainerStore(ptr).isResolved())) + // Explicit calls to non-unique actors affect the base record + if (!R::implicit && ptr.getClass().isActor() + && MWBase::Environment::get().getESMStore()->getRefCount(ptr.getCellRef().getRefId()) > 1) + { + ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, -count); + return; + } + // Calls to unresolved containers affect the base record instead + else if (ptr.getClass().getType() == ESM::Container::sRecordId + && (!ptr.getRefData().getCustomData() || !ptr.getClass().getContainerStore(ptr).isResolved())) + { + ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, -count); + const ESM::Container* baseRecord + = MWBase::Environment::get().getESMStore()->get().find( + ptr.getCellRef().getRefId()); + const auto& ptrs = MWBase::Environment::get().getWorld()->getAll(ptr.getCellRef().getRefId()); + for (const auto& container : ptrs) { - ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, -count); - const ESM::Container* baseRecord = MWBase::Environment::get().getWorld()->getStore().get().find(ptr.getCellRef().getRefId()); - const auto& ptrs = MWBase::Environment::get().getWorld()->getAll(ptr.getCellRef().getRefId()); - for(const auto& container : ptrs) + container.get()->mBase = baseRecord; + if (container.getRefData().getCustomData()) { - container.get()->mBase = baseRecord; - if(container.getRefData().getCustomData()) - { - auto& store = container.getClass().getContainerStore(container); - // Note that unlike AddItem, RemoveItem only removes from unresolved containers - if(!store.isResolved()) - store.remove(item, count, ptr, false, false); - } + auto& store = container.getClass().getContainerStore(container); + // Note that unlike AddItem, RemoveItem only removes from unresolved containers + if (!store.isResolved()) + store.remove(item, count, false, false); } - return; } - MWWorld::ContainerStore& store = ptr.getClass().getContainerStore (ptr); + return; + } + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); - std::string itemName; - for (MWWorld::ConstContainerStoreIterator iter(store.cbegin()); iter != store.cend(); ++iter) + std::string_view itemName; + for (MWWorld::ConstContainerStoreIterator iter(store.cbegin()); iter != store.cend(); ++iter) + { + if (iter->getCellRef().getRefId() == item) { - if (::Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), item)) - { - itemName = iter->getClass().getName(*iter); - break; - } + itemName = iter->getClass().getName(*iter); + break; } + } - int numRemoved = store.remove(item, count, ptr); + int numRemoved = store.remove(item, count); - // Spawn a messagebox (only for items removed from player's inventory) - if ((numRemoved > 0) - && (ptr == MWMechanics::getPlayer())) - { - // The two GMST entries below expand to strings informing the player of what, and how many of it has been removed from their inventory - std::string msgBox; + // Spawn a messagebox (only for items removed from player's inventory) + if ((numRemoved > 0) && (ptr == MWMechanics::getPlayer())) + { + // The two GMST entries below expand to strings informing the player of what, and how many of it has + // been removed from their inventory + std::string msgBox; - if (numRemoved > 1) - { - msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage63}"); - msgBox = ::Misc::StringUtils::format(msgBox, numRemoved, itemName); - } - else - { - msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage62}"); - msgBox = ::Misc::StringUtils::format(msgBox, itemName); - } - MWBase::Environment::get().getWindowManager()->messageBox(msgBox, MWGui::ShowInDialogueMode_Only); + if (numRemoved > 1) + { + msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage63}"); + msgBox = ::Misc::StringUtils::format(msgBox, numRemoved, itemName); } + else + { + msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage62}"); + msgBox = ::Misc::StringUtils::format(msgBox, itemName); + } + MWBase::Environment::get().getWindowManager()->messageBox(msgBox, MWGui::ShowInDialogueMode_Only); } + } }; template class OpEquip : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute(Interpreter::Runtime &runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId item = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string item = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); + auto found = invStore.end(); + const auto& store = *MWBase::Environment::get().getESMStore(); - MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); - MWWorld::ContainerStoreIterator it = invStore.begin(); - for (; it != invStore.end(); ++it) + // With soul gems we prefer filled ones. + for (auto it = invStore.begin(); it != invStore.end(); ++it) + { + if (it->getCellRef().getRefId() == item) { - if (::Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), item)) + found = it; + const ESM::RefId& soul = it->getCellRef().getSoul(); + if (!it->getClass().isSoulGem(*it) + || (!soul.empty() && store.get().search(soul))) break; } - if (it == invStore.end()) - { - it = ptr.getClass().getContainerStore (ptr).add (item, 1, ptr); - Log(Debug::Warning) << "Implicitly adding one " << item << - " to the inventory store of " << ptr.getCellRef().getRefId() << - " to fulfill the requirements of Equip instruction"; - } + } - if (ptr == MWMechanics::getPlayer()) - MWBase::Environment::get().getWindowManager()->useItem(*it, true); - else - { - std::shared_ptr action = it->getClass().use(*it, true); - action->execute(ptr, true); - } + if (found == invStore.end()) + { + MWWorld::ManualRef ref(store, item, 1); + found = ptr.getClass().getContainerStore(ptr).add(ref.getPtr(), 1, false); + Log(Debug::Warning) << "Implicitly adding one " << item << " to the inventory store of " + << ptr.getCellRef().getRefId() + << " to fulfill the requirements of Equip instruction"; } + + if (ptr == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->useItem(*found, true); + else + { + std::unique_ptr action = found->getClass().use(*found, true); + action->execute(ptr, true); + } + } }; template class OpGetArmorType : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute(Interpreter::Runtime &runtime) override + Interpreter::Type_Integer location = runtime[0].mInteger; + runtime.pop(); + + int slot; + switch (location) { - MWWorld::Ptr ptr = R()(runtime); + case 0: + slot = MWWorld::InventoryStore::Slot_Helmet; + break; + case 1: + slot = MWWorld::InventoryStore::Slot_Cuirass; + break; + case 2: + slot = MWWorld::InventoryStore::Slot_LeftPauldron; + break; + case 3: + slot = MWWorld::InventoryStore::Slot_RightPauldron; + break; + case 4: + slot = MWWorld::InventoryStore::Slot_Greaves; + break; + case 5: + slot = MWWorld::InventoryStore::Slot_Boots; + break; + case 6: + slot = MWWorld::InventoryStore::Slot_LeftGauntlet; + break; + case 7: + slot = MWWorld::InventoryStore::Slot_RightGauntlet; + break; + case 8: + slot = MWWorld::InventoryStore::Slot_CarriedLeft; // shield + break; + case 9: + slot = MWWorld::InventoryStore::Slot_LeftGauntlet; + break; + case 10: + slot = MWWorld::InventoryStore::Slot_RightGauntlet; + break; + default: + throw std::runtime_error("armor index out of range"); + } - Interpreter::Type_Integer location = runtime[0].mInteger; - runtime.pop(); + const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); + MWWorld::ConstContainerStoreIterator it = invStore.getSlot(slot); - int slot; - switch (location) - { - case 0: - slot = MWWorld::InventoryStore::Slot_Helmet; - break; - case 1: - slot = MWWorld::InventoryStore::Slot_Cuirass; - break; - case 2: - slot = MWWorld::InventoryStore::Slot_LeftPauldron; - break; - case 3: - slot = MWWorld::InventoryStore::Slot_RightPauldron; - break; - case 4: - slot = MWWorld::InventoryStore::Slot_Greaves; - break; - case 5: - slot = MWWorld::InventoryStore::Slot_Boots; - break; - case 6: - slot = MWWorld::InventoryStore::Slot_LeftGauntlet; - break; - case 7: - slot = MWWorld::InventoryStore::Slot_RightGauntlet; - break; - case 8: - slot = MWWorld::InventoryStore::Slot_CarriedLeft; // shield - break; - case 9: - slot = MWWorld::InventoryStore::Slot_LeftGauntlet; - break; - case 10: - slot = MWWorld::InventoryStore::Slot_RightGauntlet; - break; - default: - throw std::runtime_error ("armor index out of range"); - } - - const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); - MWWorld::ConstContainerStoreIterator it = invStore.getSlot (slot); - - if (it == invStore.end() || it->getTypeName () != typeid(ESM::Armor).name()) - { - runtime.push(-1); - return; - } + if (it == invStore.end() || it->getType() != ESM::Armor::sRecordId) + { + runtime.push(-1); + return; + } - int skill = it->getClass().getEquipmentSkill (*it) ; - if (skill == ESM::Skill::HeavyArmor) - runtime.push(2); - else if (skill == ESM::Skill::MediumArmor) - runtime.push(1); - else if (skill == ESM::Skill::LightArmor) - runtime.push(0); - else - runtime.push(-1); + ESM::RefId skill = it->getClass().getEquipmentSkill(*it); + if (skill == ESM::Skill::HeavyArmor) + runtime.push(2); + else if (skill == ESM::Skill::MediumArmor) + runtime.push(1); + else if (skill == ESM::Skill::LightArmor) + runtime.push(0); + else + runtime.push(-1); } }; template class OpHasItemEquipped : public Interpreter::Opcode0 { - public: - - void execute(Interpreter::Runtime &runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - std::string item = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId item = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); - for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + { + MWWorld::ConstContainerStoreIterator it = invStore.getSlot(slot); + if (it != invStore.end() && it->getCellRef().getRefId() == item) { - MWWorld::ConstContainerStoreIterator it = invStore.getSlot (slot); - if (it != invStore.end() && ::Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), item)) - { - runtime.push(1); - return; - } + runtime.push(1); + return; } - runtime.push(0); } + runtime.push(0); + } }; template class OpHasSoulGem : public Interpreter::Opcode0 { - public: - - void execute(Interpreter::Runtime &runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - const std::string &name = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId name = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - int count = 0; - const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); - for (MWWorld::ConstContainerStoreIterator it = invStore.cbegin(MWWorld::ContainerStore::Type_Miscellaneous); - it != invStore.cend(); ++it) - { - if (::Misc::StringUtils::ciEqual(it->getCellRef().getSoul(), name)) - count += it->getRefData().getCount(); - } - runtime.push(count); + int count = 0; + const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); + for (MWWorld::ConstContainerStoreIterator it + = invStore.cbegin(MWWorld::ContainerStore::Type_Miscellaneous); + it != invStore.cend(); ++it) + { + if (it->getCellRef().getSoul() == name) + count += it->getCellRef().getCount(); } + runtime.push(count); + } }; template class OpGetWeaponType : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute(Interpreter::Runtime &runtime) override + const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); + MWWorld::ConstContainerStoreIterator it = invStore.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (it == invStore.end()) { - MWWorld::Ptr ptr = R()(runtime); - - const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); - MWWorld::ConstContainerStoreIterator it = invStore.getSlot (MWWorld::InventoryStore::Slot_CarriedRight); - if (it == invStore.end()) + runtime.push(-1); + return; + } + else if (it->getType() != ESM::Weapon::sRecordId) + { + if (it->getType() == ESM::Lockpick::sRecordId) { - runtime.push(-1); - return; + runtime.push(-2); } - else if (it->getTypeName() != typeid(ESM::Weapon).name()) + else if (it->getType() == ESM::Probe::sRecordId) { - if (it->getTypeName() == typeid(ESM::Lockpick).name()) - { - runtime.push(-2); - } - else if (it->getTypeName() == typeid(ESM::Probe).name()) - { - runtime.push(-3); - } - else - { - runtime.push(-1); - } - return; + runtime.push(-3); } - - runtime.push(it->get()->mBase->mData.mType); + else + { + runtime.push(-1); + } + return; } - }; + runtime.push(it->get()->mBase->mData.mType); + } + }; - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Container::opcodeAddItem, new OpAddItem); - interpreter.installSegment5 (Compiler::Container::opcodeAddItemExplicit, new OpAddItem); - interpreter.installSegment5 (Compiler::Container::opcodeGetItemCount, new OpGetItemCount); - interpreter.installSegment5 (Compiler::Container::opcodeGetItemCountExplicit, new OpGetItemCount); - interpreter.installSegment5 (Compiler::Container::opcodeRemoveItem, new OpRemoveItem); - interpreter.installSegment5 (Compiler::Container::opcodeRemoveItemExplicit, new OpRemoveItem); - interpreter.installSegment5 (Compiler::Container::opcodeEquip, new OpEquip); - interpreter.installSegment5 (Compiler::Container::opcodeEquipExplicit, new OpEquip); - interpreter.installSegment5 (Compiler::Container::opcodeGetArmorType, new OpGetArmorType); - interpreter.installSegment5 (Compiler::Container::opcodeGetArmorTypeExplicit, new OpGetArmorType); - interpreter.installSegment5 (Compiler::Container::opcodeHasItemEquipped, new OpHasItemEquipped); - interpreter.installSegment5 (Compiler::Container::opcodeHasItemEquippedExplicit, new OpHasItemEquipped); - interpreter.installSegment5 (Compiler::Container::opcodeHasSoulGem, new OpHasSoulGem); - interpreter.installSegment5 (Compiler::Container::opcodeHasSoulGemExplicit, new OpHasSoulGem); - interpreter.installSegment5 (Compiler::Container::opcodeGetWeaponType, new OpGetWeaponType); - interpreter.installSegment5 (Compiler::Container::opcodeGetWeaponTypeExplicit, new OpGetWeaponType); + interpreter.installSegment5>(Compiler::Container::opcodeAddItem); + interpreter.installSegment5>(Compiler::Container::opcodeAddItemExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeGetItemCount); + interpreter.installSegment5>(Compiler::Container::opcodeGetItemCountExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeRemoveItem); + interpreter.installSegment5>(Compiler::Container::opcodeRemoveItemExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeEquip); + interpreter.installSegment5>(Compiler::Container::opcodeEquipExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeGetArmorType); + interpreter.installSegment5>(Compiler::Container::opcodeGetArmorTypeExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeHasItemEquipped); + interpreter.installSegment5>( + Compiler::Container::opcodeHasItemEquippedExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeHasSoulGem); + interpreter.installSegment5>(Compiler::Container::opcodeHasSoulGemExplicit); + interpreter.installSegment5>(Compiler::Container::opcodeGetWeaponType); + interpreter.installSegment5>(Compiler::Container::opcodeGetWeaponTypeExplicit); } } } diff --git a/apps/openmw/mwscript/containerextensions.hpp b/apps/openmw/mwscript/containerextensions.hpp index d5be8fb2a67..c6b2434f333 100644 --- a/apps/openmw/mwscript/containerextensions.hpp +++ b/apps/openmw/mwscript/containerextensions.hpp @@ -16,7 +16,7 @@ namespace MWScript /// \brief Container-related script functionality (chests, NPCs, creatures) namespace Container { - void installOpcodes (Interpreter::Interpreter& interpreter); + void installOpcodes(Interpreter::Interpreter& interpreter); } } diff --git a/apps/openmw/mwscript/controlextensions.cpp b/apps/openmw/mwscript/controlextensions.cpp index 2716630d725..b9e8f8965a7 100644 --- a/apps/openmw/mwscript/controlextensions.cpp +++ b/apps/openmw/mwscript/controlextensions.cpp @@ -3,8 +3,8 @@ #include #include -#include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" @@ -14,6 +14,8 @@ #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" +#include "../mwmechanics/creaturestats.hpp" + #include "interpretercontext.hpp" #include "ref.hpp" @@ -23,232 +25,231 @@ namespace MWScript { class OpSetControl : public Interpreter::Opcode0 { - std::string mControl; - bool mEnable; - - public: + std::string_view mControl; + bool mEnable; - OpSetControl (const std::string& control, bool enable) - : mControl (control), mEnable (enable) - {} + public: + OpSetControl(std::string_view control, bool enable) + : mControl(control) + , mEnable(enable) + { + } - void execute (Interpreter::Runtime& runtime) override - { - MWBase::Environment::get() - .getInputManager() - ->toggleControlSwitch(mControl, mEnable); - } + void execute(Interpreter::Runtime& runtime) override + { + MWBase::Environment::get().getInputManager()->toggleControlSwitch(mControl, mEnable); + } }; class OpGetDisabled : public Interpreter::Opcode0 { - std::string mControl; - - public: + std::string_view mControl; - OpGetDisabled (const std::string& control) - : mControl (control) - {} + public: + OpGetDisabled(std::string_view control) + : mControl(control) + { + } - void execute (Interpreter::Runtime& runtime) override - { - runtime.push(!MWBase::Environment::get().getInputManager()->getControlSwitch (mControl)); - } + void execute(Interpreter::Runtime& runtime) override + { + runtime.push(!MWBase::Environment::get().getInputManager()->getControlSwitch(mControl)); + } }; class OpToggleCollision : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - bool enabled = MWBase::Environment::get().getWorld()->toggleCollisionMode(); + public: + void execute(Interpreter::Runtime& runtime) override + { + bool enabled = MWBase::Environment::get().getWorld()->toggleCollisionMode(); - runtime.getContext().report (enabled ? "Collision -> On" : "Collision -> Off"); - } + runtime.getContext().report(enabled ? "Collision -> On" : "Collision -> Off"); + } }; - template + template class OpClearMovementFlag : public Interpreter::Opcode0 { - MWMechanics::CreatureStats::Flag mFlag; - - public: + MWMechanics::CreatureStats::Flag mFlag; - OpClearMovementFlag (MWMechanics::CreatureStats::Flag flag) : mFlag (flag) {} + public: + OpClearMovementFlag(MWMechanics::CreatureStats::Flag flag) + : mFlag(flag) + { + } - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - ptr.getClass().getCreatureStats(ptr).setMovementFlag (mFlag, false); - } + ptr.getClass().getCreatureStats(ptr).setMovementFlag(mFlag, false); + } }; - template + template class OpSetMovementFlag : public Interpreter::Opcode0 { - MWMechanics::CreatureStats::Flag mFlag; - - public: + MWMechanics::CreatureStats::Flag mFlag; - OpSetMovementFlag (MWMechanics::CreatureStats::Flag flag) : mFlag (flag) {} + public: + OpSetMovementFlag(MWMechanics::CreatureStats::Flag flag) + : mFlag(flag) + { + } - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - ptr.getClass().getCreatureStats(ptr).setMovementFlag (mFlag, true); - } + ptr.getClass().getCreatureStats(ptr).setMovementFlag(mFlag, true); + } }; template class OpGetForceRun : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); - runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); - } + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + runtime.push(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceRun)); + } }; template class OpGetForceJump : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); - runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); - } + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + runtime.push(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceJump)); + } }; template class OpGetForceMoveJump : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); - runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); - } + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + runtime.push(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceMoveJump)); + } }; template class OpGetForceSneak : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - } + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + runtime.push(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceSneak)); + } }; class OpGetPcRunning : public Interpreter::Opcode0 { - public: - - 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(); + public: + 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); + 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(stanceOn && (running || inair)); + } }; class OpGetPcSneaking : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); - runtime.push(MWBase::Environment::get().getMechanicsManager()->isSneaking(ptr)); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); + runtime.push(MWBase::Environment::get().getMechanicsManager()->isSneaking(ptr)); + } }; - - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - for (int i=0; i (MWMechanics::CreatureStats::Flag_ForceRun)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceRunExplicit, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); - interpreter.installSegment5 (Compiler::Control::opcodeForceRun, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); - interpreter.installSegment5 (Compiler::Control::opcodeForceRunExplicit, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); - - //Force Jump - interpreter.installSegment5 (Compiler::Control::opcodeClearForceJump, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceJumpExplicit, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); - interpreter.installSegment5 (Compiler::Control::opcodeForceJump, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); - interpreter.installSegment5 (Compiler::Control::opcodeForceJumpExplicit, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); - - //Force MoveJump - interpreter.installSegment5 (Compiler::Control::opcodeClearForceMoveJump, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceMoveJumpExplicit, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); - interpreter.installSegment5 (Compiler::Control::opcodeForceMoveJump, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); - interpreter.installSegment5 (Compiler::Control::opcodeForceMoveJumpExplicit, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); - - //Force Sneak - interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneak, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneakExplicit, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - interpreter.installSegment5 (Compiler::Control::opcodeForceSneak, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - interpreter.installSegment5 (Compiler::Control::opcodeForceSneakExplicit, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - - interpreter.installSegment5 (Compiler::Control::opcodeGetPcRunning, new OpGetPcRunning); - interpreter.installSegment5 (Compiler::Control::opcodeGetPcSneaking, new OpGetPcSneaking); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceRun, new OpGetForceRun); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceRunExplicit, new OpGetForceRun); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceJump, new OpGetForceJump); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceJumpExplicit, new OpGetForceJump); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceMoveJump, new OpGetForceMoveJump); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceMoveJumpExplicit, new OpGetForceMoveJump); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceSneak, new OpGetForceSneak); - interpreter.installSegment5 (Compiler::Control::opcodeGetForceSneakExplicit, new OpGetForceSneak); + for (int i = 0; i < Compiler::Control::numberOfControls; ++i) + { + interpreter.installSegment5( + Compiler::Control::opcodeEnable + i, Compiler::Control::controls[i], true); + interpreter.installSegment5( + Compiler::Control::opcodeDisable + i, Compiler::Control::controls[i], false); + interpreter.installSegment5( + Compiler::Control::opcodeGetDisabled + i, Compiler::Control::controls[i]); + } + + interpreter.installSegment5(Compiler::Control::opcodeToggleCollision); + + // Force Run + interpreter.installSegment5>( + Compiler::Control::opcodeClearForceRun, MWMechanics::CreatureStats::Flag_ForceRun); + interpreter.installSegment5>( + Compiler::Control::opcodeClearForceRunExplicit, MWMechanics::CreatureStats::Flag_ForceRun); + interpreter.installSegment5>( + Compiler::Control::opcodeForceRun, MWMechanics::CreatureStats::Flag_ForceRun); + interpreter.installSegment5>( + Compiler::Control::opcodeForceRunExplicit, MWMechanics::CreatureStats::Flag_ForceRun); + + // Force Jump + interpreter.installSegment5>( + Compiler::Control::opcodeClearForceJump, MWMechanics::CreatureStats::Flag_ForceJump); + interpreter.installSegment5>( + Compiler::Control::opcodeClearForceJumpExplicit, MWMechanics::CreatureStats::Flag_ForceJump); + interpreter.installSegment5>( + Compiler::Control::opcodeForceJump, MWMechanics::CreatureStats::Flag_ForceJump); + interpreter.installSegment5>( + Compiler::Control::opcodeForceJumpExplicit, MWMechanics::CreatureStats::Flag_ForceJump); + + // Force MoveJump + interpreter.installSegment5>( + Compiler::Control::opcodeClearForceMoveJump, MWMechanics::CreatureStats::Flag_ForceMoveJump); + interpreter.installSegment5>( + Compiler::Control::opcodeClearForceMoveJumpExplicit, MWMechanics::CreatureStats::Flag_ForceMoveJump); + interpreter.installSegment5>( + Compiler::Control::opcodeForceMoveJump, MWMechanics::CreatureStats::Flag_ForceMoveJump); + interpreter.installSegment5>( + Compiler::Control::opcodeForceMoveJumpExplicit, MWMechanics::CreatureStats::Flag_ForceMoveJump); + + // Force Sneak + interpreter.installSegment5>( + Compiler::Control::opcodeClearForceSneak, MWMechanics::CreatureStats::Flag_ForceSneak); + interpreter.installSegment5>( + Compiler::Control::opcodeClearForceSneakExplicit, MWMechanics::CreatureStats::Flag_ForceSneak); + interpreter.installSegment5>( + Compiler::Control::opcodeForceSneak, MWMechanics::CreatureStats::Flag_ForceSneak); + interpreter.installSegment5>( + Compiler::Control::opcodeForceSneakExplicit, MWMechanics::CreatureStats::Flag_ForceSneak); + + interpreter.installSegment5(Compiler::Control::opcodeGetPcRunning); + interpreter.installSegment5(Compiler::Control::opcodeGetPcSneaking); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceRun); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceRunExplicit); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceJump); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceJumpExplicit); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceMoveJump); + interpreter.installSegment5>( + Compiler::Control::opcodeGetForceMoveJumpExplicit); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceSneak); + interpreter.installSegment5>(Compiler::Control::opcodeGetForceSneakExplicit); } } } diff --git a/apps/openmw/mwscript/controlextensions.hpp b/apps/openmw/mwscript/controlextensions.hpp index b9c6654fea1..57051cf750b 100644 --- a/apps/openmw/mwscript/controlextensions.hpp +++ b/apps/openmw/mwscript/controlextensions.hpp @@ -16,7 +16,7 @@ namespace MWScript /// \brief player controls-related script functionality namespace Control { - void installOpcodes (Interpreter::Interpreter& interpreter); + void installOpcodes(Interpreter::Interpreter& interpreter); } } diff --git a/apps/openmw/mwscript/dialogueextensions.cpp b/apps/openmw/mwscript/dialogueextensions.cpp index b99a043bf55..6511fbdb011 100644 --- a/apps/openmw/mwscript/dialogueextensions.cpp +++ b/apps/openmw/mwscript/dialogueextensions.cpp @@ -3,20 +3,21 @@ #include #include #include +#include #include -#include #include +#include -#include "../mwbase/environment.hpp" #include "../mwbase/dialoguemanager.hpp" +#include "../mwbase/environment.hpp" #include "../mwbase/journal.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/class.hpp" #include "../mwmechanics/npcstats.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" -#include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript @@ -26,204 +27,207 @@ namespace MWScript template class OpJournal : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime, false); // required=false - if (ptr.isEmpty()) - ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime, false); // required=false + if (ptr.isEmpty()) + ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); - std::string quest = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId quest = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - Interpreter::Type_Integer index = runtime[0].mInteger; - runtime.pop(); + Interpreter::Type_Integer index = runtime[0].mInteger; + runtime.pop(); - // Invoking Journal with a non-existing index is allowed, and triggers no errors. Seriously? :( - try - { - MWBase::Environment::get().getJournal()->addEntry (quest, index, ptr); - } - catch (...) - { - if (MWBase::Environment::get().getJournal()->getJournalIndex(quest) < index) - MWBase::Environment::get().getJournal()->setJournalIndex(quest, index); - } + // Invoking Journal with a non-existing index is allowed, and triggers no errors. Seriously? :( + try + { + MWBase::Environment::get().getJournal()->addEntry(quest, index, ptr); } + catch (...) + { + if (MWBase::Environment::get().getJournal()->getJournalIndex(quest) < index) + MWBase::Environment::get().getJournal()->setJournalIndex(quest, index); + } + } }; class OpSetJournalIndex : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - std::string quest = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + ESM::RefId quest = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - Interpreter::Type_Integer index = runtime[0].mInteger; - runtime.pop(); + Interpreter::Type_Integer index = runtime[0].mInteger; + runtime.pop(); - MWBase::Environment::get().getJournal()->setJournalIndex (quest, index); - } + MWBase::Environment::get().getJournal()->setJournalIndex(quest, index); + } }; class OpGetJournalIndex : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - std::string quest = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - - int index = MWBase::Environment::get().getJournal()->getJournalIndex (quest); + public: + void execute(Interpreter::Runtime& runtime) override + { + ESM::RefId quest = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - runtime.push (index); + int index = MWBase::Environment::get().getJournal()->getJournalIndex(quest); - } + runtime.push(index); + } }; class OpAddTopic : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + ESM::RefId topic = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override + if (!MWBase::Environment::get().getESMStore()->get().search(topic)) { - std::string topic = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - - MWBase::Environment::get().getDialogueManager()->addTopic(topic); + runtime.getContext().report( + "Failed to add topic '" + topic.getRefIdString() + "': topic record not found"); + return; } + + MWBase::Environment::get().getDialogueManager()->addTopic(topic); + } }; class OpChoice : public Interpreter::Opcode1 { - public: - - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWBase::DialogueManager* dialogue = MWBase::Environment::get().getDialogueManager(); + while (arg0 > 0) { - MWBase::DialogueManager* dialogue = MWBase::Environment::get().getDialogueManager(); - while(arg0>0) + std::string_view question = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + arg0 = arg0 - 1; + Interpreter::Type_Integer choice = 1; + if (arg0 > 0) { - std::string question = runtime.getStringLiteral (runtime[0].mInteger); + choice = runtime[0].mInteger; runtime.pop(); - arg0 = arg0 -1; - Interpreter::Type_Integer choice = 1; - if(arg0>0) - { - choice = runtime[0].mInteger; - runtime.pop(); - arg0 = arg0 -1; - } - dialogue->addChoice(question,choice); + arg0 = arg0 - 1; } + dialogue->addChoice(question, choice); } + } }; - template + template class OpForceGreeting : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + + if (!ptr.getRefData().isEnabled()) + return; - void execute (Interpreter::Runtime& runtime) override + if (!ptr.getClass().isActor()) { - MWWorld::Ptr ptr = R()(runtime); + const std::string error = "Warning: \"forcegreeting\" command works only for actors."; + runtime.getContext().report(error); + Log(Debug::Warning) << error; + return; + } - if (!ptr.getRefData().isEnabled()) - return; + bool greetWerewolves = false; + const ESM::RefId& script = ptr.getClass().getScript(ptr); + if (!script.empty()) + greetWerewolves = ptr.getRefData().getLocals().hasVar(script, "allowwerewolfforcegreeting"); - if (!ptr.getClass().isActor()) - { - const std::string error = "Warning: \"forcegreeting\" command works only for actors."; - runtime.getContext().report(error); - Log(Debug::Warning) << error; - return; - } + const MWWorld::Ptr& player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (player.getClass().getNpcStats(player).isWerewolf() && !greetWerewolves) + return; - MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, ptr); - } + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, ptr); + } }; class OpGoodbye : public Interpreter::Opcode0 { - public: - - void execute(Interpreter::Runtime& runtime) override - { - MWBase::Environment::get().getDialogueManager()->goodbye(); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWBase::Environment::get().getDialogueManager()->goodbye(); + } }; - template + template class OpModReputation : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = runtime[0].mInteger; - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Integer value = runtime[0].mInteger; + runtime.pop(); - ptr.getClass().getNpcStats (ptr).setReputation (ptr.getClass().getNpcStats (ptr).getReputation () + value); - } + ptr.getClass().getNpcStats(ptr).setReputation(ptr.getClass().getNpcStats(ptr).getReputation() + value); + } }; - template + template class OpSetReputation : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = runtime[0].mInteger; - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Integer value = runtime[0].mInteger; + runtime.pop(); - ptr.getClass().getNpcStats (ptr).setReputation (value); - } + ptr.getClass().getNpcStats(ptr).setReputation(value); + } }; - template + template class OpGetReputation : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - runtime.push (ptr.getClass().getNpcStats (ptr).getReputation ()); - } + runtime.push(ptr.getClass().getNpcStats(ptr).getReputation()); + } }; - template + template class OpSameFaction : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - runtime.push(player.getClass().getNpcStats (player).isInFaction(ptr.getClass().getPrimaryFaction(ptr))); - } + runtime.push(player.getClass().getNpcStats(player).isInFaction(ptr.getClass().getPrimaryFaction(ptr))); + } }; class OpModFactionReaction : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { - std::string faction1 = runtime.getStringLiteral (runtime[0].mInteger); + ESM::RefId faction1 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - std::string faction2 = runtime.getStringLiteral (runtime[0].mInteger); + ESM::RefId faction2 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); int modReaction = runtime[0].mInteger; @@ -236,30 +240,27 @@ namespace MWScript class OpGetFactionReaction : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { - std::string faction1 = runtime.getStringLiteral (runtime[0].mInteger); + ESM::RefId faction1 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - std::string faction2 = runtime.getStringLiteral (runtime[0].mInteger); + ESM::RefId faction2 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - runtime.push(MWBase::Environment::get().getDialogueManager() - ->getFactionReaction(faction1, faction2)); + runtime.push(MWBase::Environment::get().getDialogueManager()->getFactionReaction(faction1, faction2)); } }; class OpSetFactionReaction : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { - std::string faction1 = runtime.getStringLiteral (runtime[0].mInteger); + ESM::RefId faction1 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - std::string faction2 = runtime.getStringLiteral (runtime[0].mInteger); + ESM::RefId faction2 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); int newValue = runtime[0].mInteger; @@ -273,7 +274,7 @@ namespace MWScript class OpClearInfoActor : public Interpreter::Opcode0 { public: - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); @@ -281,31 +282,31 @@ namespace MWScript } }; - - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Dialogue::opcodeJournal, new OpJournal); - interpreter.installSegment5 (Compiler::Dialogue::opcodeJournalExplicit, new OpJournal); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSetJournalIndex, new OpSetJournalIndex); - interpreter.installSegment5 (Compiler::Dialogue::opcodeGetJournalIndex, new OpGetJournalIndex); - interpreter.installSegment5 (Compiler::Dialogue::opcodeAddTopic, new OpAddTopic); - interpreter.installSegment3 (Compiler::Dialogue::opcodeChoice,new OpChoice); - interpreter.installSegment5 (Compiler::Dialogue::opcodeForceGreeting, new OpForceGreeting); - interpreter.installSegment5 (Compiler::Dialogue::opcodeForceGreetingExplicit, new OpForceGreeting); - interpreter.installSegment5 (Compiler::Dialogue::opcodeGoodbye, new OpGoodbye); - interpreter.installSegment5 (Compiler::Dialogue::opcodeGetReputation, new OpGetReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSetReputation, new OpSetReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeModReputation, new OpModReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSetReputationExplicit, new OpSetReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeModReputationExplicit, new OpModReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeGetReputationExplicit, new OpGetReputation); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSameFaction, new OpSameFaction); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSameFactionExplicit, new OpSameFaction); - interpreter.installSegment5 (Compiler::Dialogue::opcodeModFactionReaction, new OpModFactionReaction); - interpreter.installSegment5 (Compiler::Dialogue::opcodeSetFactionReaction, new OpSetFactionReaction); - interpreter.installSegment5 (Compiler::Dialogue::opcodeGetFactionReaction, new OpGetFactionReaction); - interpreter.installSegment5 (Compiler::Dialogue::opcodeClearInfoActor, new OpClearInfoActor); - interpreter.installSegment5 (Compiler::Dialogue::opcodeClearInfoActorExplicit, new OpClearInfoActor); + interpreter.installSegment5>(Compiler::Dialogue::opcodeJournal); + interpreter.installSegment5>(Compiler::Dialogue::opcodeJournalExplicit); + interpreter.installSegment5(Compiler::Dialogue::opcodeSetJournalIndex); + interpreter.installSegment5(Compiler::Dialogue::opcodeGetJournalIndex); + interpreter.installSegment5(Compiler::Dialogue::opcodeAddTopic); + interpreter.installSegment3(Compiler::Dialogue::opcodeChoice); + interpreter.installSegment5>(Compiler::Dialogue::opcodeForceGreeting); + interpreter.installSegment5>(Compiler::Dialogue::opcodeForceGreetingExplicit); + interpreter.installSegment5(Compiler::Dialogue::opcodeGoodbye); + interpreter.installSegment5>(Compiler::Dialogue::opcodeGetReputation); + interpreter.installSegment5>(Compiler::Dialogue::opcodeSetReputation); + interpreter.installSegment5>(Compiler::Dialogue::opcodeModReputation); + interpreter.installSegment5>(Compiler::Dialogue::opcodeSetReputationExplicit); + interpreter.installSegment5>(Compiler::Dialogue::opcodeModReputationExplicit); + interpreter.installSegment5>(Compiler::Dialogue::opcodeGetReputationExplicit); + interpreter.installSegment5>(Compiler::Dialogue::opcodeSameFaction); + interpreter.installSegment5>(Compiler::Dialogue::opcodeSameFactionExplicit); + interpreter.installSegment5(Compiler::Dialogue::opcodeModFactionReaction); + interpreter.installSegment5(Compiler::Dialogue::opcodeSetFactionReaction); + interpreter.installSegment5(Compiler::Dialogue::opcodeGetFactionReaction); + interpreter.installSegment5>(Compiler::Dialogue::opcodeClearInfoActor); + interpreter.installSegment5>( + Compiler::Dialogue::opcodeClearInfoActorExplicit); } } diff --git a/apps/openmw/mwscript/dialogueextensions.hpp b/apps/openmw/mwscript/dialogueextensions.hpp index 7b03154dfbd..acb4e06838b 100644 --- a/apps/openmw/mwscript/dialogueextensions.hpp +++ b/apps/openmw/mwscript/dialogueextensions.hpp @@ -16,7 +16,7 @@ namespace MWScript /// \brief Dialogue/Journal-related script functionality namespace Dialogue { - void installOpcodes (Interpreter::Interpreter& interpreter); + void installOpcodes(Interpreter::Interpreter& interpreter); } } diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index ccc579b30d9..d34c39c9df8 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -479,5 +479,11 @@ op 0x200031c: GetDisabled, explicit op 0x200031d: StartScript, explicit op 0x200031e: GetDistance op 0x200031f: GetDistance, explicit +op 0x2000320: Help +op 0x2000321: ReloadLua +op 0x2000322: GetPCVisionBonus +op 0x2000323: SetPCVisionBonus +op 0x2000324: ModPCVisionBonus +op 0x2000325: TestModels, T3D -opcodes 0x2000320-0x3ffffff unused +opcodes 0x2000326-0x3ffffff unused diff --git a/apps/openmw/mwscript/extensions.cpp b/apps/openmw/mwscript/extensions.cpp index 12bf3413a00..0f42b492500 100644 --- a/apps/openmw/mwscript/extensions.cpp +++ b/apps/openmw/mwscript/extensions.cpp @@ -1,45 +1,45 @@ #include "extensions.hpp" -#include #include +#include -#include "soundextensions.hpp" +#include "aiextensions.hpp" +#include "animationextensions.hpp" #include "cellextensions.hpp" -#include "miscextensions.hpp" -#include "guiextensions.hpp" -#include "skyextensions.hpp" -#include "statsextensions.hpp" +#include "consoleextensions.hpp" #include "containerextensions.hpp" -#include "aiextensions.hpp" #include "controlextensions.hpp" #include "dialogueextensions.hpp" -#include "animationextensions.hpp" +#include "guiextensions.hpp" +#include "miscextensions.hpp" +#include "skyextensions.hpp" +#include "soundextensions.hpp" +#include "statsextensions.hpp" #include "transformationextensions.hpp" -#include "consoleextensions.hpp" #include "userextensions.hpp" namespace MWScript { - void installOpcodes (Interpreter::Interpreter& interpreter, bool consoleOnly) + void installOpcodes(Interpreter::Interpreter& interpreter, bool consoleOnly) { - Interpreter::installOpcodes (interpreter); - Cell::installOpcodes (interpreter); - Misc::installOpcodes (interpreter); - Gui::installOpcodes (interpreter); - Sound::installOpcodes (interpreter); - Sky::installOpcodes (interpreter); - Stats::installOpcodes (interpreter); - Container::installOpcodes (interpreter); - Ai::installOpcodes (interpreter); - Control::installOpcodes (interpreter); - Dialogue::installOpcodes (interpreter); - Animation::installOpcodes (interpreter); - Transformation::installOpcodes (interpreter); + Interpreter::installOpcodes(interpreter); + Cell::installOpcodes(interpreter); + Misc::installOpcodes(interpreter); + Gui::installOpcodes(interpreter); + Sound::installOpcodes(interpreter); + Sky::installOpcodes(interpreter); + Stats::installOpcodes(interpreter); + Container::installOpcodes(interpreter); + Ai::installOpcodes(interpreter); + Control::installOpcodes(interpreter); + Dialogue::installOpcodes(interpreter); + Animation::installOpcodes(interpreter); + Transformation::installOpcodes(interpreter); if (consoleOnly) { - Console::installOpcodes (interpreter); - User::installOpcodes (interpreter); + Console::installOpcodes(interpreter); + User::installOpcodes(interpreter); } } } diff --git a/apps/openmw/mwscript/extensions.hpp b/apps/openmw/mwscript/extensions.hpp index 67f6de5c58e..43c5bb3b0ea 100644 --- a/apps/openmw/mwscript/extensions.hpp +++ b/apps/openmw/mwscript/extensions.hpp @@ -13,7 +13,7 @@ namespace Interpreter namespace MWScript { - void installOpcodes (Interpreter::Interpreter& interpreter, bool consoleOnly = false); + void installOpcodes(Interpreter::Interpreter& interpreter, bool consoleOnly = false); ///< \param consoleOnly include console only opcodes } diff --git a/apps/openmw/mwscript/globalscripts.cpp b/apps/openmw/mwscript/globalscripts.cpp index b287d33b3d6..c20e2fe255a 100644 --- a/apps/openmw/mwscript/globalscripts.cpp +++ b/apps/openmw/mwscript/globalscripts.cpp @@ -1,27 +1,28 @@ #include "globalscripts.hpp" #include -#include -#include -#include +#include +#include +#include +#include +#include -#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/scriptmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/worldmodel.hpp" #include "interpretercontext.hpp" namespace { - struct ScriptCreatingVisitor : public boost::static_visitor + struct ScriptCreatingVisitor { - ESM::GlobalScript operator()(const MWWorld::Ptr &ptr) const + ESM::GlobalScript operator()(const MWWorld::Ptr& ptr) const { ESM::GlobalScript script; - script.mTargetRef.unset(); script.mRunning = false; if (!ptr.isEmpty()) { @@ -36,7 +37,7 @@ namespace return script; } - ESM::GlobalScript operator()(const std::pair &pair) const + ESM::GlobalScript operator()(const std::pair& pair) const { ESM::GlobalScript script; script.mTargetId = pair.second; @@ -46,89 +47,98 @@ namespace } }; - struct PtrGettingVisitor : public boost::static_visitor + struct PtrGettingVisitor { - const MWWorld::Ptr* operator()(const MWWorld::Ptr &ptr) const - { - return &ptr; - } + const MWWorld::Ptr* operator()(const MWWorld::Ptr& ptr) const { return &ptr; } - const MWWorld::Ptr* operator()(const std::pair &pair) const - { - return nullptr; - } + const MWWorld::Ptr* operator()(const std::pair& pair) const { return nullptr; } }; - struct PtrResolvingVisitor : public boost::static_visitor + struct PtrResolvingVisitor { - MWWorld::Ptr operator()(const MWWorld::Ptr &ptr) const - { - return ptr; - } + MWWorld::Ptr operator()(const MWWorld::Ptr& ptr) const { return ptr; } - MWWorld::Ptr operator()(const std::pair &pair) const + MWWorld::Ptr operator()(const std::pair& pair) const { if (pair.second.empty()) return MWWorld::Ptr(); - else if(pair.first.hasContentFile()) - return MWBase::Environment::get().getWorld()->searchPtrViaRefNum(pair.second, pair.first); + else if (pair.first.hasContentFile()) + return MWBase::Environment::get().getWorldModel()->getPtr(pair.first); return MWBase::Environment::get().getWorld()->searchPtr(pair.second, false); } }; - class MatchPtrVisitor : public boost::static_visitor + class MatchPtrVisitor { const MWWorld::Ptr& mPtr; - public: - MatchPtrVisitor(const MWWorld::Ptr& ptr) : mPtr(ptr) {} - bool operator()(const MWWorld::Ptr &ptr) const + public: + MatchPtrVisitor(const MWWorld::Ptr& ptr) + : mPtr(ptr) { - return ptr == mPtr; } - bool operator()(const std::pair &pair) const + bool operator()(const MWWorld::Ptr& ptr) const { return ptr == mPtr; } + + bool operator()(const std::pair& pair) const { return false; } + }; + + struct IdGettingVisitor + { + ESM::RefId operator()(const MWWorld::Ptr& ptr) const { - return false; + if (ptr.isEmpty()) + return ESM::RefId(); + return ptr.mRef->mRef.getRefId(); } + + ESM::RefId operator()(const std::pair& pair) const { return pair.second; } }; } namespace MWScript { - GlobalScriptDesc::GlobalScriptDesc() : mRunning (false) {} + GlobalScriptDesc::GlobalScriptDesc() + : mRunning(false) + { + } const MWWorld::Ptr* GlobalScriptDesc::getPtrIfPresent() const { - return boost::apply_visitor(PtrGettingVisitor(), mTarget); + return std::visit(PtrGettingVisitor(), mTarget); } MWWorld::Ptr GlobalScriptDesc::getPtr() { - MWWorld::Ptr ptr = boost::apply_visitor(PtrResolvingVisitor(), mTarget); + MWWorld::Ptr ptr = std::visit(PtrResolvingVisitor{}, mTarget); mTarget = ptr; return ptr; } + ESM::RefId GlobalScriptDesc::getId() const + { + return std::visit(IdGettingVisitor{}, mTarget); + } - GlobalScripts::GlobalScripts (const MWWorld::ESMStore& store) - : mStore (store) - {} + GlobalScripts::GlobalScripts(const MWWorld::ESMStore& store) + : mStore(store) + { + } - void GlobalScripts::addScript (const std::string& name, const MWWorld::Ptr& target) + void GlobalScripts::addScript(const ESM::RefId& name, const MWWorld::Ptr& target) { - const auto iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); + const auto iter = mScripts.find(name); - if (iter==mScripts.end()) + if (iter == mScripts.end()) { - if (const ESM::Script *script = mStore.get().search(name)) + if (const ESM::Script* script = mStore.get().search(name)) { auto desc = std::make_shared(); MWWorld::Ptr ptr = target; desc->mTarget = ptr; desc->mRunning = true; - desc->mLocals.configure (*script); - mScripts.insert (std::make_pair(name, desc)); + desc->mLocals.configure(*script); + mScripts.insert(std::make_pair(name, desc)); } else { @@ -143,19 +153,19 @@ namespace MWScript } } - void GlobalScripts::removeScript (const std::string& name) + void GlobalScripts::removeScript(const ESM::RefId& name) { - const auto iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); + const auto iter = mScripts.find(name); - if (iter!=mScripts.end()) + if (iter != mScripts.end()) iter->second->mRunning = false; } - bool GlobalScripts::isRunning (const std::string& name) const + bool GlobalScripts::isRunning(const ESM::RefId& name) const { - const auto iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); + const auto iter = mScripts.find(name); - if (iter==mScripts.end()) + if (iter == mScripts.end()) return false; return iter->second->mRunning; @@ -182,30 +192,27 @@ namespace MWScript void GlobalScripts::addStartup() { // make list of global scripts to be added - std::vector scripts; + std::vector scripts; - scripts.emplace_back("main"); + scripts.emplace_back(ESM::RefId::stringRefId("main")); - for (MWWorld::Store::iterator iter = - mStore.get().begin(); - iter != mStore.get().end(); ++iter) + for (MWWorld::Store::iterator iter = mStore.get().begin(); + iter != mStore.get().end(); ++iter) { - scripts.push_back (iter->mId); + scripts.push_back(iter->mId); } // add scripts - for (std::vector::const_iterator iter (scripts.begin()); - iter!=scripts.end(); ++iter) + for (auto iter(scripts.begin()); iter != scripts.end(); ++iter) { try { - addScript (*iter); + addScript(*iter); } catch (const std::exception& exception) { - Log(Debug::Error) - << "Failed to add start script " << *iter << " because an exception has " - << "been thrown: " << exception.what(); + Log(Debug::Error) << "Failed to add start script " << *iter << " because an exception has " + << "been thrown: " << exception.what(); } } } @@ -215,43 +222,36 @@ namespace MWScript return mScripts.size(); } - void GlobalScripts::write (ESM::ESMWriter& writer, Loading::Listener& progress) const + void GlobalScripts::write(ESM::ESMWriter& writer, Loading::Listener& progress) const { - for (const auto& iter : mScripts) + for (const auto& [id, desc] : mScripts) { - ESM::GlobalScript script = boost::apply_visitor (ScriptCreatingVisitor(), iter.second->mTarget); + ESM::GlobalScript script = std::visit(ScriptCreatingVisitor{}, desc->mTarget); - script.mId = iter.first; + script.mId = id; - iter.second->mLocals.write (script.mLocals, iter.first); + desc->mLocals.write(script.mLocals, id); - script.mRunning = iter.second->mRunning ? 1 : 0; + script.mRunning = desc->mRunning; - writer.startRecord (ESM::REC_GSCR); - script.save (writer); - writer.endRecord (ESM::REC_GSCR); + writer.startRecord(ESM::REC_GSCR); + script.save(writer); + writer.endRecord(ESM::REC_GSCR); } } - bool GlobalScripts::readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap) + bool GlobalScripts::readRecord(ESM::ESMReader& reader, uint32_t type) { - if (type==ESM::REC_GSCR) + if (type == ESM::REC_GSCR) { ESM::GlobalScript script; - script.load (reader); - - if (script.mTargetRef.hasContentFile()) - { - auto iter = contentFileMap.find(script.mTargetRef.mContentFile); - if (iter != contentFileMap.end()) - script.mTargetRef.mContentFile = iter->second; - } + script.load(reader); - auto iter = mScripts.find (script.mId); + auto iter = mScripts.find(script.mId); - if (iter==mScripts.end()) + if (iter == mScripts.end()) { - if (const ESM::Script *scriptRecord = mStore.get().search (script.mId)) + if (const ESM::Script* scriptRecord = mStore.get().search(script.mId)) { try { @@ -260,15 +260,14 @@ namespace MWScript { desc->mTarget = std::make_pair(script.mTargetRef, script.mTargetId); } - desc->mLocals.configure (*scriptRecord); + desc->mLocals.configure(*scriptRecord); - iter = mScripts.insert (std::make_pair (script.mId, desc)).first; + iter = mScripts.insert(std::make_pair(script.mId, desc)).first; } catch (const std::exception& exception) { - Log(Debug::Error) - << "Failed to add start script " << script.mId - << " because an exception has been thrown: " << exception.what(); + Log(Debug::Error) << "Failed to add start script " << script.mId + << " because an exception has been thrown: " << exception.what(); return true; } @@ -277,8 +276,8 @@ namespace MWScript return true; } - iter->second->mRunning = script.mRunning!=0; - iter->second->mLocals.read (script.mLocals, script.mId); + iter->second->mRunning = script.mRunning; + iter->second->mLocals.read(script.mLocals, script.mId); return true; } @@ -286,31 +285,29 @@ namespace MWScript return false; } - Locals& GlobalScripts::getLocals (const std::string& name) + Locals& GlobalScripts::getLocals(const ESM::RefId& name) { - std::string name2 = ::Misc::StringUtils::lowerCase (name); - auto iter = mScripts.find (name2); + auto iter = mScripts.find(name); - if (iter==mScripts.end()) + if (iter == mScripts.end()) { - const ESM::Script *script = mStore.get().find (name); + const ESM::Script* script = mStore.get().find(name); auto desc = std::make_shared(); - desc->mLocals.configure (*script); + desc->mLocals.configure(*script); - iter = mScripts.insert (std::make_pair (name2, desc)).first; + iter = mScripts.emplace(name, desc).first; } return iter->second->mLocals; } - const Locals* GlobalScripts::getLocalsIfPresent (const std::string& name) const + const GlobalScriptDesc* GlobalScripts::getScriptIfPresent(const ESM::RefId& name) const { - std::string name2 = ::Misc::StringUtils::lowerCase (name); - auto iter = mScripts.find (name2); - if (iter==mScripts.end()) + auto iter = mScripts.find(name); + if (iter == mScripts.end()) return nullptr; - return &iter->second->mLocals; + return iter->second.get(); } void GlobalScripts::updatePtrs(const MWWorld::Ptr& base, const MWWorld::Ptr& updated) @@ -318,7 +315,7 @@ namespace MWScript MatchPtrVisitor visitor(base); for (const auto& script : mScripts) { - if (boost::apply_visitor (visitor, script.second->mTarget)) + if (std::visit(visitor, script.second->mTarget)) script.second->mTarget = updated; } } diff --git a/apps/openmw/mwscript/globalscripts.hpp b/apps/openmw/mwscript/globalscripts.hpp index 049e78804a5..083695b51b6 100644 --- a/apps/openmw/mwscript/globalscripts.hpp +++ b/apps/openmw/mwscript/globalscripts.hpp @@ -1,14 +1,17 @@ #ifndef GAME_SCRIPT_GLOBALSCRIPTS_H #define GAME_SCRIPT_GLOBALSCRIPTS_H -#include - -#include +#include #include #include +#include +#include +#include #include +#include -#include +#include +#include #include "locals.hpp" @@ -18,7 +21,8 @@ namespace ESM { class ESMWriter; class ESMReader; - struct RefNum; + struct FormId; + using RefNum = FormId; } namespace Loading @@ -37,55 +41,56 @@ namespace MWScript { bool mRunning; Locals mLocals; - boost::variant > mTarget; // Used to start targeted script + std::variant> mTarget; // Used to start targeted script GlobalScriptDesc(); const MWWorld::Ptr* getPtrIfPresent() const; // Returns a Ptr if one has been resolved MWWorld::Ptr getPtr(); // Resolves mTarget to a Ptr and caches the (potentially empty) result + + ESM::RefId getId() const; // Returns the target's ID -- if any }; class GlobalScripts { - const MWWorld::ESMStore& mStore; - std::map > mScripts; - - public: + const MWWorld::ESMStore& mStore; + std::unordered_map> mScripts; - GlobalScripts (const MWWorld::ESMStore& store); + public: + GlobalScripts(const MWWorld::ESMStore& store); - void addScript (const std::string& name, const MWWorld::Ptr& target = MWWorld::Ptr()); + void addScript(const ESM::RefId& name, const MWWorld::Ptr& target = MWWorld::Ptr()); - void removeScript (const std::string& name); + void removeScript(const ESM::RefId& name); - bool isRunning (const std::string& name) const; + bool isRunning(const ESM::RefId& name) const; - void run(); - ///< run all active global scripts + void run(); + ///< run all active global scripts - void clear(); + void clear(); - void addStartup(); - ///< Add startup script + void addStartup(); + ///< Add startup script - int countSavedGameRecords() const; + int countSavedGameRecords() const; - void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; + void write(ESM::ESMWriter& writer, Loading::Listener& progress) const; - bool readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap); - ///< Records for variables that do not exist are dropped silently. - /// - /// \return Known type? + bool readRecord(ESM::ESMReader& reader, uint32_t type); + ///< Records for variables that do not exist are dropped silently. + /// + /// \return Known type? - Locals& getLocals (const std::string& name); - ///< If the script \a name has not been added as a global script yet, it is added - /// automatically, but is not set to running state. + Locals& getLocals(const ESM::RefId& name); + ///< If the script \a name has not been added as a global script yet, it is added + /// automatically, but is not set to running state. - const Locals* getLocalsIfPresent (const std::string& name) const; + const GlobalScriptDesc* getScriptIfPresent(const ESM::RefId& name) const; - void updatePtrs(const MWWorld::Ptr& base, const MWWorld::Ptr& updated); - ///< Update the Ptrs stored in mTarget. Should be called after the reference has been moved to a new cell. + void updatePtrs(const MWWorld::Ptr& base, const MWWorld::Ptr& updated); + ///< Update the Ptrs stored in mTarget. Should be called after the reference has been moved to a new cell. }; } diff --git a/apps/openmw/mwscript/guiextensions.cpp b/apps/openmw/mwscript/guiextensions.cpp index cb1e5cd91de..07855f18ef8 100644 --- a/apps/openmw/mwscript/guiextensions.cpp +++ b/apps/openmw/mwscript/guiextensions.cpp @@ -2,20 +2,19 @@ #include +#include #include -#include #include +#include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" -#include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript @@ -24,117 +23,110 @@ namespace MWScript { class OpEnableWindow : public Interpreter::Opcode0 { - MWGui::GuiWindow mWindow; + MWGui::GuiWindow mWindow; - public: - - OpEnableWindow (MWGui::GuiWindow window) : mWindow (window) {} + public: + OpEnableWindow(MWGui::GuiWindow window) + : mWindow(window) + { + } - void execute (Interpreter::Runtime& runtime) override - { - MWBase::Environment::get().getWindowManager()->allow (mWindow); - } + void execute(Interpreter::Runtime& runtime) override + { + MWBase::Environment::get().getWindowManager()->allow(mWindow); + } }; class OpEnableRest : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWBase::Environment::get().getWindowManager()->enableRest(); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWBase::Environment::get().getWindowManager()->enableRest(); + } }; template class OpShowRestMenu : public Interpreter::Opcode0 { public: - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr bed = R()(runtime, false); - if (bed.isEmpty() || !MWBase::Environment::get().getMechanicsManager()->sleepInBed(MWMechanics::getPlayer(), - bed)) + if (bed.isEmpty() + || !MWBase::Environment::get().getMechanicsManager()->sleepInBed(MWMechanics::getPlayer(), bed)) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Rest, bed); } }; class OpShowDialogue : public Interpreter::Opcode0 { - MWGui::GuiMode mDialogue; + MWGui::GuiMode mDialogue; - public: - - OpShowDialogue (MWGui::GuiMode dialogue) - : mDialogue (dialogue) - {} + public: + OpShowDialogue(MWGui::GuiMode dialogue) + : mDialogue(dialogue) + { + } - void execute (Interpreter::Runtime& runtime) override - { - MWBase::Environment::get().getWindowManager()->pushGuiMode(mDialogue); - } + void execute(Interpreter::Runtime& runtime) override + { + MWBase::Environment::get().getWindowManager()->pushGuiMode(mDialogue); + } }; class OpGetButtonPressed : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.push (MWBase::Environment::get().getWindowManager()->readPressedButton()); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.push(MWBase::Environment::get().getWindowManager()->readPressedButton()); + } }; class OpToggleFogOfWar : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.getContext().report(MWBase::Environment::get().getWindowManager()->toggleFogOfWar() ? "Fog of war -> On" - : "Fog of war -> Off"); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.getContext().report(MWBase::Environment::get().getWindowManager()->toggleFogOfWar() + ? "Fog of war -> On" + : "Fog of war -> Off"); + } }; class OpToggleFullHelp : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.getContext().report(MWBase::Environment::get().getWindowManager()->toggleFullHelp() ? "Full help -> On" - : "Full help -> Off"); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.getContext().report(MWBase::Environment::get().getWindowManager()->toggleFullHelp() + ? "Full help -> On" + : "Full help -> Off"); + } }; class OpShowMap : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { - std::string cell = (runtime.getStringLiteral (runtime[0].mInteger)); - ::Misc::StringUtils::lowerCaseInPlace(cell); + std::string_view cell = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - // "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 + // "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 + + const MWWorld::Store& cells = MWBase::Environment::get().getESMStore()->get(); - const MWWorld::Store &cells = - MWBase::Environment::get().getWorld()->getStore().get(); + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); - MWWorld::Store::iterator it = cells.extBegin(); - for (; it != cells.extEnd(); ++it) + for (auto it = cells.extBegin(); it != cells.extEnd(); ++it) { - std::string name = it->mName; - ::Misc::StringUtils::lowerCaseInPlace(name); - if (name.find(cell) != std::string::npos) - MWBase::Environment::get().getWindowManager()->addVisitedLocation ( - it->mName, - it->getGridX(), - it->getGridY() - ); + const auto& cellName = it->mName; + if (Misc::StringUtils::ciStartsWith(cellName, cell)) + winMgr->addVisitedLocation(cellName, it->getGridX(), it->getGridY()); } } }; @@ -142,22 +134,16 @@ namespace MWScript class OpFillMap : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { - const MWWorld::Store &cells = - MWBase::Environment::get().getWorld ()->getStore().get(); + const MWWorld::Store& cells = MWBase::Environment::get().getESMStore()->get(); - MWWorld::Store::iterator it = cells.extBegin(); - for (; it != cells.extEnd(); ++it) + for (auto it = cells.extBegin(); it != cells.extEnd(); ++it) { - std::string name = it->mName; - if (name != "") - MWBase::Environment::get().getWindowManager()->addVisitedLocation ( - name, - it->getGridX(), - it->getGridY() - ); + const std::string& name = it->mName; + if (!name.empty()) + MWBase::Environment::get().getWindowManager()->addVisitedLocation( + name, it->getGridX(), it->getGridY()); } } }; @@ -165,22 +151,20 @@ namespace MWScript class OpMenuTest : public Interpreter::Opcode1 { public: - - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { - int arg=0; - if(arg0>0) + int arg = 0; + if (arg0 > 0) { arg = runtime[0].mInteger; runtime.pop(); } - if (arg == 0) { MWGui::GuiMode modes[] = { MWGui::GM_Inventory, MWGui::GM_Container }; - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { if (MWBase::Environment::get().getWindowManager()->containsMode(modes[i])) MWBase::Environment::get().getWindowManager()->removeGuiMode(modes[i]); @@ -206,60 +190,50 @@ namespace MWScript class OpToggleMenus : public Interpreter::Opcode0 { public: - void execute(Interpreter::Runtime &runtime) override + void execute(Interpreter::Runtime& runtime) override { - bool state = MWBase::Environment::get().getWindowManager()->toggleHud(); + bool state = MWBase::Environment::get().getWindowManager()->setHudVisibility( + !MWBase::Environment::get().getWindowManager()->isHudVisible()); runtime.getContext().report(state ? "GUI -> On" : "GUI -> Off"); if (!state) { - while (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_None) // don't use isGuiMode, or we get an infinite loop for modal message boxes! + while (MWBase::Environment::get().getWindowManager()->getMode() + != MWGui::GM_None) // don't use isGuiMode, or we get an infinite loop for modal message boxes! MWBase::Environment::get().getWindowManager()->popGuiMode(); } } }; - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Gui::opcodeEnableBirthMenu, - new OpShowDialogue (MWGui::GM_Birth)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableClassMenu, - new OpShowDialogue (MWGui::GM_Class)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableNameMenu, - new OpShowDialogue (MWGui::GM_Name)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableRaceMenu, - new OpShowDialogue (MWGui::GM_Race)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableStatsReviewMenu, - new OpShowDialogue (MWGui::GM_Review)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableLevelupMenu, - new OpShowDialogue (MWGui::GM_Levelup)); - - interpreter.installSegment5 (Compiler::Gui::opcodeEnableInventoryMenu, - new OpEnableWindow (MWGui::GW_Inventory)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableMagicMenu, - new OpEnableWindow (MWGui::GW_Magic)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableMapMenu, - new OpEnableWindow (MWGui::GW_Map)); - interpreter.installSegment5 (Compiler::Gui::opcodeEnableStatsMenu, - new OpEnableWindow (MWGui::GW_Stats)); - - interpreter.installSegment5 (Compiler::Gui::opcodeEnableRest, - new OpEnableRest ()); - - interpreter.installSegment5 (Compiler::Gui::opcodeShowRestMenu, - new OpShowRestMenu); - interpreter.installSegment5 (Compiler::Gui::opcodeShowRestMenuExplicit, new OpShowRestMenu); - - interpreter.installSegment5 (Compiler::Gui::opcodeGetButtonPressed, new OpGetButtonPressed); - - interpreter.installSegment5 (Compiler::Gui::opcodeToggleFogOfWar, new OpToggleFogOfWar); - - interpreter.installSegment5 (Compiler::Gui::opcodeToggleFullHelp, new OpToggleFullHelp); - - interpreter.installSegment5 (Compiler::Gui::opcodeShowMap, new OpShowMap); - interpreter.installSegment5 (Compiler::Gui::opcodeFillMap, new OpFillMap); - interpreter.installSegment3 (Compiler::Gui::opcodeMenuTest, new OpMenuTest); - interpreter.installSegment5 (Compiler::Gui::opcodeToggleMenus, new OpToggleMenus); + interpreter.installSegment5(Compiler::Gui::opcodeEnableBirthMenu, MWGui::GM_Birth); + interpreter.installSegment5(Compiler::Gui::opcodeEnableClassMenu, MWGui::GM_Class); + interpreter.installSegment5(Compiler::Gui::opcodeEnableNameMenu, MWGui::GM_Name); + interpreter.installSegment5(Compiler::Gui::opcodeEnableRaceMenu, MWGui::GM_Race); + interpreter.installSegment5(Compiler::Gui::opcodeEnableStatsReviewMenu, MWGui::GM_Review); + interpreter.installSegment5(Compiler::Gui::opcodeEnableLevelupMenu, MWGui::GM_Levelup); + + interpreter.installSegment5(Compiler::Gui::opcodeEnableInventoryMenu, MWGui::GW_Inventory); + interpreter.installSegment5(Compiler::Gui::opcodeEnableMagicMenu, MWGui::GW_Magic); + interpreter.installSegment5(Compiler::Gui::opcodeEnableMapMenu, MWGui::GW_Map); + interpreter.installSegment5(Compiler::Gui::opcodeEnableStatsMenu, MWGui::GW_Stats); + + interpreter.installSegment5(Compiler::Gui::opcodeEnableRest); + + interpreter.installSegment5>(Compiler::Gui::opcodeShowRestMenu); + interpreter.installSegment5>(Compiler::Gui::opcodeShowRestMenuExplicit); + + interpreter.installSegment5(Compiler::Gui::opcodeGetButtonPressed); + + interpreter.installSegment5(Compiler::Gui::opcodeToggleFogOfWar); + + interpreter.installSegment5(Compiler::Gui::opcodeToggleFullHelp); + + interpreter.installSegment5(Compiler::Gui::opcodeShowMap); + interpreter.installSegment5(Compiler::Gui::opcodeFillMap); + interpreter.installSegment3(Compiler::Gui::opcodeMenuTest); + interpreter.installSegment5(Compiler::Gui::opcodeToggleMenus); } } } diff --git a/apps/openmw/mwscript/guiextensions.hpp b/apps/openmw/mwscript/guiextensions.hpp index ec775a51c9d..b44f954d6e9 100644 --- a/apps/openmw/mwscript/guiextensions.hpp +++ b/apps/openmw/mwscript/guiextensions.hpp @@ -15,10 +15,9 @@ namespace MWScript { /// \brief GUI-related script functionality namespace Gui - { - void installOpcodes (Interpreter::Interpreter& interpreter); + { + void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif - diff --git a/apps/openmw/mwscript/interpretercontext.cpp b/apps/openmw/mwscript/interpretercontext.cpp index d8fd287d72e..15c9100b98c 100644 --- a/apps/openmw/mwscript/interpretercontext.cpp +++ b/apps/openmw/mwscript/interpretercontext.cpp @@ -4,33 +4,32 @@ #include #include +#include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include "../mwbase/inputmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/inputmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/action.hpp" -#include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" -#include "../mwworld/containerstore.hpp" +#include "../mwworld/class.hpp" #include "../mwmechanics/npcstats.hpp" -#include "locals.hpp" #include "globalscripts.hpp" +#include "locals.hpp" namespace MWScript { - const MWWorld::Ptr InterpreterContext::getReferenceImp ( - const std::string& id, bool activeOnly, bool doThrow) const + const MWWorld::Ptr InterpreterContext::getReferenceImp(const ESM::RefId& id, bool activeOnly, bool doThrow) const { if (!id.empty()) { - return MWBase::Environment::get().getWorld()->getPtr (id, activeOnly); + return MWBase::Environment::get().getWorld()->getPtr(id, activeOnly); } else { @@ -44,56 +43,52 @@ namespace MWScript } } - const Locals& InterpreterContext::getMemberLocals (std::string& id, bool global) - const + const Locals& InterpreterContext::getMemberLocals(bool global, ESM::RefId& id) const { if (global) { - return MWBase::Environment::get().getScriptManager()->getGlobalScripts(). - getLocals (id); + return MWBase::Environment::get().getScriptManager()->getGlobalScripts().getLocals(id); } else { - const MWWorld::Ptr ptr = getReferenceImp (id, false); + const MWWorld::Ptr ptr = getReferenceImp(id, false); - id = ptr.getClass().getScript (ptr); + id = ptr.getClass().getScript(ptr); - ptr.getRefData().setLocals ( - *MWBase::Environment::get().getWorld()->getStore().get().find (id)); + ptr.getRefData().setLocals(*MWBase::Environment::get().getESMStore()->get().find(id)); return ptr.getRefData().getLocals(); } } - Locals& InterpreterContext::getMemberLocals (std::string& id, bool global) + Locals& InterpreterContext::getMemberLocals(bool global, ESM::RefId& id) { if (global) { - return MWBase::Environment::get().getScriptManager()->getGlobalScripts(). - getLocals (id); + return MWBase::Environment::get().getScriptManager()->getGlobalScripts().getLocals(id); } else { - const MWWorld::Ptr ptr = getReferenceImp (id, false); + const MWWorld::Ptr ptr = getReferenceImp(id, false); - id = ptr.getClass().getScript (ptr); + id = ptr.getClass().getScript(ptr); - ptr.getRefData().setLocals ( - *MWBase::Environment::get().getWorld()->getStore().get().find (id)); + ptr.getRefData().setLocals(*MWBase::Environment::get().getESMStore()->get().find(id)); return ptr.getRefData().getLocals(); } } - MissingImplicitRefError::MissingImplicitRefError() : std::runtime_error("no implicit reference") {} + MissingImplicitRefError::MissingImplicitRefError() + : std::runtime_error("no implicit reference") + { + } - int InterpreterContext::findLocalVariableIndex (const std::string& scriptId, - const std::string& name, char type) const + int InterpreterContext::findLocalVariableIndex(const ESM::RefId& scriptId, std::string_view name, char type) const { - int index = MWBase::Environment::get().getScriptManager()->getLocals (scriptId). - searchIndex (type, name); + int index = MWBase::Environment::get().getScriptManager()->getLocals(scriptId).searchIndex(type, name); - if (index!=-1) + if (index != -1) return index; std::ostringstream stream; @@ -102,22 +97,30 @@ namespace MWScript switch (type) { - case 's': stream << "short"; break; - case 'l': stream << "long"; break; - case 'f': stream << "float"; break; + case 's': + stream << "short"; + break; + case 'l': + stream << "long"; + break; + case 'f': + stream << "float"; + break; } stream << " member variable " << name << " in script " << scriptId; - throw std::runtime_error (stream.str().c_str()); + throw std::runtime_error(stream.str().c_str()); } - InterpreterContext::InterpreterContext (MWScript::Locals *locals, const MWWorld::Ptr& reference) - : mLocals (locals), mReference (reference) - {} + InterpreterContext::InterpreterContext(MWScript::Locals* locals, const MWWorld::Ptr& reference) + : mLocals(locals) + , mReference(reference) + { + } - InterpreterContext::InterpreterContext (std::shared_ptr globalScriptDesc) - : mLocals (&(globalScriptDesc->mLocals)) + InterpreterContext::InterpreterContext(std::shared_ptr globalScriptDesc) + : mLocals(&(globalScriptDesc->mLocals)) { const MWWorld::Ptr* ptr = globalScriptDesc->getPtrIfPresent(); // A nullptr here signifies that the script's target has not yet been resolved after loading the game. @@ -129,131 +132,136 @@ namespace MWScript mGlobalScriptDesc = globalScriptDesc; } - int InterpreterContext::getLocalShort (int index) const + ESM::RefId InterpreterContext::getTarget() const + { + if (!mReference.isEmpty()) + return mReference.mRef->mRef.getRefId(); + else if (mGlobalScriptDesc) + return mGlobalScriptDesc->getId(); + return ESM::RefId(); + } + + int InterpreterContext::getLocalShort(int index) const { if (!mLocals) - throw std::runtime_error ("local variables not available in this context"); + throw std::runtime_error("local variables not available in this context"); - return mLocals->mShorts.at (index); + return mLocals->mShorts.at(index); } - int InterpreterContext::getLocalLong (int index) const + int InterpreterContext::getLocalLong(int index) const { if (!mLocals) - throw std::runtime_error ("local variables not available in this context"); + throw std::runtime_error("local variables not available in this context"); - return mLocals->mLongs.at (index); + return mLocals->mLongs.at(index); } - float InterpreterContext::getLocalFloat (int index) const + float InterpreterContext::getLocalFloat(int index) const { if (!mLocals) - throw std::runtime_error ("local variables not available in this context"); + throw std::runtime_error("local variables not available in this context"); - return mLocals->mFloats.at (index); + return mLocals->mFloats.at(index); } - void InterpreterContext::setLocalShort (int index, int value) + void InterpreterContext::setLocalShort(int index, int value) { if (!mLocals) - throw std::runtime_error ("local variables not available in this context"); + throw std::runtime_error("local variables not available in this context"); - mLocals->mShorts.at (index) = value; + mLocals->mShorts.at(index) = value; } - void InterpreterContext::setLocalLong (int index, int value) + void InterpreterContext::setLocalLong(int index, int value) { if (!mLocals) - throw std::runtime_error ("local variables not available in this context"); + throw std::runtime_error("local variables not available in this context"); - mLocals->mLongs.at (index) = value; + mLocals->mLongs.at(index) = value; } - void InterpreterContext::setLocalFloat (int index, float value) + void InterpreterContext::setLocalFloat(int index, float value) { if (!mLocals) - throw std::runtime_error ("local variables not available in this context"); + throw std::runtime_error("local variables not available in this context"); - mLocals->mFloats.at (index) = value; + mLocals->mFloats.at(index) = value; } - void InterpreterContext::messageBox (const std::string& message, - const std::vector& buttons) + void InterpreterContext::messageBox(std::string_view message, const std::vector& buttons) { if (buttons.empty()) - MWBase::Environment::get().getWindowManager()->messageBox (message); + MWBase::Environment::get().getWindowManager()->messageBox(message); else MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); } - void InterpreterContext::report (const std::string& message) - { - } + void InterpreterContext::report(const std::string& message) {} - int InterpreterContext::getGlobalShort (const std::string& name) const + int InterpreterContext::getGlobalShort(std::string_view name) const { - return MWBase::Environment::get().getWorld()->getGlobalInt (name); + return MWBase::Environment::get().getWorld()->getGlobalInt(name); } - int InterpreterContext::getGlobalLong (const std::string& name) const + int InterpreterContext::getGlobalLong(std::string_view name) const { // a global long is internally a float. - return MWBase::Environment::get().getWorld()->getGlobalInt (name); + return MWBase::Environment::get().getWorld()->getGlobalInt(name); } - float InterpreterContext::getGlobalFloat (const std::string& name) const + float InterpreterContext::getGlobalFloat(std::string_view name) const { - return MWBase::Environment::get().getWorld()->getGlobalFloat (name); + return MWBase::Environment::get().getWorld()->getGlobalFloat(name); } - void InterpreterContext::setGlobalShort (const std::string& name, int value) + void InterpreterContext::setGlobalShort(std::string_view name, int value) { - MWBase::Environment::get().getWorld()->setGlobalInt (name, value); + MWBase::Environment::get().getWorld()->setGlobalInt(name, value); } - void InterpreterContext::setGlobalLong (const std::string& name, int value) + void InterpreterContext::setGlobalLong(std::string_view name, int value) { - MWBase::Environment::get().getWorld()->setGlobalInt (name, value); + MWBase::Environment::get().getWorld()->setGlobalInt(name, value); } - void InterpreterContext::setGlobalFloat (const std::string& name, float value) + void InterpreterContext::setGlobalFloat(std::string_view name, float value) { - MWBase::Environment::get().getWorld()->setGlobalFloat (name, value); + MWBase::Environment::get().getWorld()->setGlobalFloat(name, value); } std::vector InterpreterContext::getGlobals() const { - const MWWorld::Store& globals = - MWBase::Environment::get().getWorld()->getStore().get(); + const MWWorld::Store& globals = MWBase::Environment::get().getESMStore()->get(); std::vector ids; - for (auto& globalVariable : globals) + for (const auto& globalVariable : globals) { - ids.emplace_back(globalVariable.mId); + ids.emplace_back(globalVariable.mId.getRefIdString()); } return ids; } - char InterpreterContext::getGlobalType (const std::string& name) const + char InterpreterContext::getGlobalType(std::string_view name) const { - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); return world->getGlobalVariableType(name); } - std::string InterpreterContext::getActionBinding(const std::string& targetAction) const + std::string InterpreterContext::getActionBinding(std::string_view targetAction) const { MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); - std::vector actions = input->getActionKeySorting (); + const auto& actions = input->getActionKeySorting(); for (const int action : actions) { - std::string desc = input->getActionDescription (action); - if(desc == "") + std::string_view desc = input->getActionDescription(action); + if (desc.empty()) continue; - if(desc == targetAction) + if (desc == targetAction) { - if(input->joystickLastUsed()) + if (input->joystickLastUsed()) return input->getActionControllerBindingName(action); else return input->getActionKeyBindingName(action); @@ -263,7 +271,7 @@ namespace MWScript return "None"; } - std::string InterpreterContext::getActorName() const + std::string_view InterpreterContext::getActorName() const { const MWWorld::Ptr& ptr = getReferenceImp(); if (ptr.getClass().isNpc()) @@ -276,31 +284,31 @@ namespace MWScript return creature->mName; } - std::string InterpreterContext::getNPCRace() const + std::string_view InterpreterContext::getNPCRace() const { - ESM::NPC npc = *getReferenceImp().get()->mBase; - const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mRace); + const ESM::NPC* npc = getReferenceImp().get()->mBase; + const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(npc->mRace); return race->mName; } - std::string InterpreterContext::getNPCClass() const + std::string_view InterpreterContext::getNPCClass() const { - ESM::NPC npc = *getReferenceImp().get()->mBase; - const ESM::Class* class_ = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mClass); + const ESM::NPC* npc = getReferenceImp().get()->mBase; + const ESM::Class* class_ = MWBase::Environment::get().getESMStore()->get().find(npc->mClass); return class_->mName; } - std::string InterpreterContext::getNPCFaction() const + std::string_view InterpreterContext::getNPCFaction() const { - ESM::NPC npc = *getReferenceImp().get()->mBase; - const ESM::Faction* faction = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mFaction); + const ESM::NPC* npc = getReferenceImp().get()->mBase; + const ESM::Faction* faction = MWBase::Environment::get().getESMStore()->get().find(npc->mFaction); return faction->mName; } - std::string InterpreterContext::getNPCRank() const + std::string_view InterpreterContext::getNPCRank() const { const MWWorld::Ptr& ptr = getReferenceImp(); - std::string faction = ptr.getClass().getPrimaryFaction(ptr); + const ESM::RefId& faction = ptr.getClass().getPrimaryFaction(ptr); if (faction.empty()) throw std::runtime_error("getNPCRank(): NPC is not in a faction"); @@ -308,44 +316,43 @@ namespace MWScript if (rank < 0 || rank > 9) throw std::runtime_error("getNPCRank(): invalid rank"); - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::ESMStore &store = world->getStore(); - const ESM::Faction *fact = store.get().find(faction); + 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]; } - std::string InterpreterContext::getPCName() const + std::string_view InterpreterContext::getPCName() const { - MWBase::World *world = MWBase::Environment::get().getWorld(); - ESM::NPC player = *world->getPlayerPtr().get()->mBase; - return player.mName; + MWBase::World* world = MWBase::Environment::get().getWorld(); + return world->getPlayerPtr().get()->mBase->mName; } - std::string InterpreterContext::getPCRace() const + std::string_view InterpreterContext::getPCRace() const { - MWBase::World *world = MWBase::Environment::get().getWorld(); - std::string race = world->getPlayerPtr().get()->mBase->mRace; + MWBase::World* world = MWBase::Environment::get().getWorld(); + const ESM::RefId& race = world->getPlayerPtr().get()->mBase->mRace; return world->getStore().get().find(race)->mName; } - std::string InterpreterContext::getPCClass() const + std::string_view InterpreterContext::getPCClass() const { - MWBase::World *world = MWBase::Environment::get().getWorld(); - std::string class_ = world->getPlayerPtr().get()->mBase->mClass; + MWBase::World* world = MWBase::Environment::get().getWorld(); + const ESM::RefId& class_ = world->getPlayerPtr().get()->mBase->mClass; return world->getStore().get().find(class_)->mName; } - std::string InterpreterContext::getPCRank() const + std::string_view InterpreterContext::getPCRank() const { - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - std::string factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); + const ESM::RefId& factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); if (factionId.empty()) throw std::runtime_error("getPCRank(): NPC is not in a faction"); - const std::map& ranks = player.getClass().getNpcStats (player).getFactionRanks(); - std::map::const_iterator it = ranks.find(Misc::StringUtils::lowerCase(factionId)); + const auto& ranks = player.getClass().getNpcStats(player).getFactionRanks(); + auto it = ranks.find(factionId); int rank = -1; if (it != ranks.end()) rank = it->second; @@ -355,26 +362,26 @@ namespace MWScript if (rank == -1) rank = 0; - const MWWorld::ESMStore &store = world->getStore(); - const ESM::Faction *faction = store.get().find(factionId); + const MWWorld::ESMStore& store = world->getStore(); + const ESM::Faction* faction = store.get().find(factionId); - if(rank < 0 || rank > 9) // there are only 10 ranks - return ""; + if (rank < 0 || rank > 9) // there are only 10 ranks + return {}; return faction->mRanks[rank]; } - std::string InterpreterContext::getPCNextRank() const + std::string_view InterpreterContext::getPCNextRank() const { - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - std::string factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); + const ESM::RefId& factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); if (factionId.empty()) throw std::runtime_error("getPCNextRank(): NPC is not in a faction"); - const std::map& ranks = player.getClass().getNpcStats (player).getFactionRanks(); - std::map::const_iterator it = ranks.find(Misc::StringUtils::lowerCase(factionId)); + const auto& ranks = player.getClass().getNpcStats(player).getFactionRanks(); + auto it = ranks.find(factionId); int rank = -1; if (it != ranks.end()) rank = it->second; @@ -385,98 +392,93 @@ namespace MWScript if (rank > 9) rank = 9; - const MWWorld::ESMStore &store = world->getStore(); - const ESM::Faction *faction = store.get().find(factionId); + const MWWorld::ESMStore& store = world->getStore(); + const ESM::Faction* faction = store.get().find(factionId); - if(rank < 0) - return ""; + if (rank < 0) + return {}; return faction->mRanks[rank]; } int InterpreterContext::getPCBounty() const { - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - return player.getClass().getNpcStats (player).getBounty(); + return player.getClass().getNpcStats(player).getBounty(); } - std::string InterpreterContext::getCurrentCellName() const + std::string_view InterpreterContext::getCurrentCellName() const { - return MWBase::Environment::get().getWorld()->getCellName(); + return MWBase::Environment::get().getWorld()->getCellName(); } - void InterpreterContext::executeActivation(MWWorld::Ptr ptr, MWWorld::Ptr actor) + void InterpreterContext::executeActivation(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) { - std::shared_ptr action = (ptr.getClass().activate(ptr, actor)); - action->execute (actor); + // MWScripted activations don't go through Lua because 1-frame delay can brake mwscripts. +#if 0 + MWBase::Environment::get().getLuaManager()->objectActivated(ptr, actor); + + // TODO: Enable this branch after implementing one of the options: + // 1) Pause this mwscript (or maybe all mwscripts) for one frame and continue from the same + // command when the activation is processed by Lua script. + // 2) Force Lua scripts to handle a zero-length extra frame right now, so when control + // returns to the mwscript, the activation is already processed. +#else + std::unique_ptr action = (ptr.getClass().activate(ptr, actor)); + action->execute(actor); if (action->getTarget() != MWWorld::Ptr() && action->getTarget() != ptr) { updatePtr(ptr, action->getTarget()); } +#endif } - int InterpreterContext::getMemberShort (const std::string& id, const std::string& name, - bool global) const + int InterpreterContext::getMemberShort(ESM::RefId id, std::string_view name, bool global) const { - std::string scriptId (id); + const Locals& locals = getMemberLocals(global, id); - const Locals& locals = getMemberLocals (scriptId, global); - - return locals.mShorts[findLocalVariableIndex (scriptId, name, 's')]; + return locals.mShorts[findLocalVariableIndex(id, name, 's')]; } - int InterpreterContext::getMemberLong (const std::string& id, const std::string& name, - bool global) const + int InterpreterContext::getMemberLong(ESM::RefId id, std::string_view name, bool global) const { - std::string scriptId (id); - - const Locals& locals = getMemberLocals (scriptId, global); + const Locals& locals = getMemberLocals(global, id); - return locals.mLongs[findLocalVariableIndex (scriptId, name, 'l')]; + return locals.mLongs[findLocalVariableIndex(id, name, 'l')]; } - float InterpreterContext::getMemberFloat (const std::string& id, const std::string& name, - bool global) const + float InterpreterContext::getMemberFloat(ESM::RefId id, std::string_view name, bool global) const { - std::string scriptId (id); - - const Locals& locals = getMemberLocals (scriptId, global); + const Locals& locals = getMemberLocals(global, id); - return locals.mFloats[findLocalVariableIndex (scriptId, name, 'f')]; + return locals.mFloats[findLocalVariableIndex(id, name, 'f')]; } - void InterpreterContext::setMemberShort (const std::string& id, const std::string& name, - int value, bool global) + void InterpreterContext::setMemberShort(ESM::RefId id, std::string_view name, int value, bool global) { - std::string scriptId (id); + Locals& locals = getMemberLocals(global, id); - Locals& locals = getMemberLocals (scriptId, global); - - locals.mShorts[findLocalVariableIndex (scriptId, name, 's')] = value; + locals.mShorts[findLocalVariableIndex(id, name, 's')] = value; } - void InterpreterContext::setMemberLong (const std::string& id, const std::string& name, int value, bool global) + void InterpreterContext::setMemberLong(ESM::RefId id, std::string_view name, int value, bool global) { - std::string scriptId (id); - - Locals& locals = getMemberLocals (scriptId, global); + Locals& locals = getMemberLocals(global, id); - locals.mLongs[findLocalVariableIndex (scriptId, name, 'l')] = value; + locals.mLongs[findLocalVariableIndex(id, name, 'l')] = value; } - void InterpreterContext::setMemberFloat (const std::string& id, const std::string& name, float value, bool global) + void InterpreterContext::setMemberFloat(ESM::RefId id, std::string_view name, float value, bool global) { - std::string scriptId (id); - - Locals& locals = getMemberLocals (scriptId, global); + Locals& locals = getMemberLocals(global, id); - locals.mFloats[findLocalVariableIndex (scriptId, name, 'f')] = value; + locals.mFloats[findLocalVariableIndex(id, name, 'f')] = value; } - MWWorld::Ptr InterpreterContext::getReference(bool required) + MWWorld::Ptr InterpreterContext::getReference(bool required) const { - return getReferenceImp ("", true, required); + return getReferenceImp({}, true, required); } void InterpreterContext::updatePtr(const MWWorld::Ptr& base, const MWWorld::Ptr& updated) diff --git a/apps/openmw/mwscript/interpretercontext.hpp b/apps/openmw/mwscript/interpretercontext.hpp index 298454bcd2d..bdae56cb3a8 100644 --- a/apps/openmw/mwscript/interpretercontext.hpp +++ b/apps/openmw/mwscript/interpretercontext.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "globalscripts.hpp" @@ -16,119 +17,120 @@ namespace MWScript class MissingImplicitRefError : public std::runtime_error { - public: - MissingImplicitRefError(); + public: + MissingImplicitRefError(); }; class InterpreterContext : public Interpreter::Context { - Locals *mLocals; - mutable MWWorld::Ptr mReference; - std::shared_ptr mGlobalScriptDesc; + Locals* mLocals; + mutable MWWorld::Ptr mReference; + std::shared_ptr mGlobalScriptDesc; - /// If \a id is empty, a reference the script is run from is returned or in case - /// of a non-local script the reference derived from the target ID. - const MWWorld::Ptr getReferenceImp (const std::string& id = "", - bool activeOnly = false, bool doThrow=true) const; + /// If \a id is empty, a reference the script is run from is returned or in case + /// of a non-local script the reference derived from the target ID. + const MWWorld::Ptr getReferenceImp( + const ESM::RefId& id = ESM::RefId(), bool activeOnly = false, bool doThrow = true) const; - const Locals& getMemberLocals (std::string& id, bool global) const; - ///< \a id is changed to the respective script ID, if \a id wasn't a script ID before + const Locals& getMemberLocals(bool global, ESM::RefId& id) const; + ///< \a id is changed to the respective script ID, if \a id wasn't a script ID before - Locals& getMemberLocals (std::string& id, bool global); - ///< \a id is changed to the respective script ID, if \a id wasn't a script ID before + Locals& getMemberLocals(bool global, ESM::RefId& id); + ///< \a id is changed to the respective script ID, if \a id wasn't a script ID before - /// Throws an exception if local variable can't be found. - int findLocalVariableIndex (const std::string& scriptId, const std::string& name, - char type) const; + /// Throws an exception if local variable can't be found. + int findLocalVariableIndex(const ESM::RefId& scriptId, std::string_view name, char type) const; - public: - InterpreterContext (std::shared_ptr globalScriptDesc); + public: + InterpreterContext(std::shared_ptr globalScriptDesc); - InterpreterContext (MWScript::Locals *locals, const MWWorld::Ptr& reference); - ///< The ownership of \a locals is not transferred. 0-pointer allowed. + InterpreterContext(MWScript::Locals* locals, const MWWorld::Ptr& reference); + ///< The ownership of \a locals is not transferred. 0-pointer allowed. - int getLocalShort (int index) const override; + ESM::RefId getTarget() const override; - int getLocalLong (int index) const override; + int getLocalShort(int index) const override; - float getLocalFloat (int index) const override; + int getLocalLong(int index) const override; - void setLocalShort (int index, int value) override; + float getLocalFloat(int index) const override; - void setLocalLong (int index, int value) override; + void setLocalShort(int index, int value) override; - void setLocalFloat (int index, float value) override; + void setLocalLong(int index, int value) override; - using Interpreter::Context::messageBox; + void setLocalFloat(int index, float value) override; - void messageBox (const std::string& message, - const std::vector& buttons) override; + using Interpreter::Context::messageBox; - void report (const std::string& message) override; - ///< By default, do nothing. + void messageBox(std::string_view message, const std::vector& buttons) override; - int getGlobalShort (const std::string& name) const override; + void report(const std::string& message) override; + ///< By default, do nothing. - int getGlobalLong (const std::string& name) const override; + int getGlobalShort(std::string_view name) const override; - float getGlobalFloat (const std::string& name) const override; + int getGlobalLong(std::string_view name) const override; - void setGlobalShort (const std::string& name, int value) override; + float getGlobalFloat(std::string_view name) const override; - void setGlobalLong (const std::string& name, int value) override; + void setGlobalShort(std::string_view name, int value) override; - void setGlobalFloat (const std::string& name, float value) override; + void setGlobalLong(std::string_view name, int value) override; - std::vector getGlobals () const override; + void setGlobalFloat(std::string_view name, float value) override; - char getGlobalType (const std::string& name) const override; + std::vector getGlobals() const override; - std::string getActionBinding(const std::string& action) const override; + char getGlobalType(std::string_view name) const override; - std::string getActorName() const override; + std::string getActionBinding(std::string_view action) const override; - std::string getNPCRace() const override; + std::string_view getActorName() const override; - std::string getNPCClass() const override; + std::string_view getNPCRace() const override; - std::string getNPCFaction() const override; + std::string_view getNPCClass() const override; - std::string getNPCRank() const override; + std::string_view getNPCFaction() const override; - std::string getPCName() const override; + std::string_view getNPCRank() const override; - std::string getPCRace() const override; + std::string_view getPCName() const override; - std::string getPCClass() const override; + std::string_view getPCRace() const override; - std::string getPCRank() const override; + std::string_view getPCClass() const override; - std::string getPCNextRank() const override; + std::string_view getPCRank() const override; - int getPCBounty() const override; + std::string_view getPCNextRank() const override; - std::string getCurrentCellName() const override; + int getPCBounty() const override; - void executeActivation(MWWorld::Ptr ptr, MWWorld::Ptr actor); - ///< Execute the activation action for this ptr. If ptr is mActivated, mark activation as handled. + std::string_view getCurrentCellName() const override; - int getMemberShort (const std::string& id, const std::string& name, bool global) const override; + void executeActivation(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor); + ///< Execute the activation action for this ptr. If ptr is mActivated, mark activation as handled. - int getMemberLong (const std::string& id, const std::string& name, bool global) const override; + int getMemberShort(ESM::RefId id, std::string_view name, bool global) const override; - float getMemberFloat (const std::string& id, const std::string& name, bool global) const override; + int getMemberLong(ESM::RefId id, std::string_view name, bool global) const override; - void setMemberShort (const std::string& id, const std::string& name, int value, bool global) override; + float getMemberFloat(ESM::RefId id, std::string_view name, bool global) const override; - void setMemberLong (const std::string& id, const std::string& name, int value, bool global) override; + void setMemberShort(ESM::RefId id, std::string_view name, int value, bool global) override; - void setMemberFloat (const std::string& id, const std::string& name, float value, bool global) override; + void setMemberLong(ESM::RefId id, std::string_view name, int value, bool global) override; - MWWorld::Ptr getReference(bool required=true); - ///< Reference, that the script is running from (can be empty) + void setMemberFloat(ESM::RefId id, std::string_view name, float value, bool global) override; - void updatePtr(const MWWorld::Ptr& base, const MWWorld::Ptr& updated); - ///< Update the Ptr stored in mReference, if there is one stored there. Should be called after the reference has been moved to a new cell. + MWWorld::Ptr getReference(bool required = true) const; + ///< Reference, that the script is running from (can be empty) + + void updatePtr(const MWWorld::Ptr& base, const MWWorld::Ptr& updated); + ///< Update the Ptr stored in mReference, if there is one stored there. Should be called after the reference has + ///< been moved to a new cell. }; } diff --git a/apps/openmw/mwscript/locals.cpp b/apps/openmw/mwscript/locals.cpp index 352dc67b3af..3bd4e820592 100644 --- a/apps/openmw/mwscript/locals.cpp +++ b/apps/openmw/mwscript/locals.cpp @@ -1,59 +1,60 @@ #include "locals.hpp" #include "globalscripts.hpp" -#include -#include -#include -#include #include -#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/scriptmanager.hpp" -#include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" namespace MWScript { - void Locals::ensure (const std::string& scriptName) + void Locals::ensure(const ESM::RefId& scriptName) { if (!mInitialised) { - const ESM::Script *script = MWBase::Environment::get().getWorld()->getStore(). - get().find (scriptName); + const ESM::Script* script = MWBase::Environment::get().getESMStore()->get().find(scriptName); - configure (*script); + configure(*script); } } - Locals::Locals() : mInitialised (false) {} + Locals::Locals() + : mInitialised(false) + { + } - bool Locals::configure (const ESM::Script& script) + bool Locals::configure(const ESM::Script& script) { if (mInitialised) return false; - const Locals* global = MWBase::Environment::get().getScriptManager()->getGlobalScripts().getLocalsIfPresent(script.mId); - if(global) + const GlobalScriptDesc* global + = MWBase::Environment::get().getScriptManager()->getGlobalScripts().getScriptIfPresent(script.mId); + if (global) { - mShorts = global->mShorts; - mLongs = global->mLongs; - mFloats = global->mFloats; + mShorts = global->mLocals.mShorts; + mLongs = global->mLocals.mLongs; + mFloats = global->mLocals.mFloats; } else { - const Compiler::Locals& locals = - MWBase::Environment::get().getScriptManager()->getLocals (script.mId); + const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script.mId); mShorts.clear(); - mShorts.resize (locals.get ('s').size(), 0); + mShorts.resize(locals.get('s').size(), 0); mLongs.clear(); - mLongs.resize (locals.get ('l').size(), 0); + mLongs.resize(locals.get('l').size(), 0); mFloats.clear(); - mFloats.resize (locals.get ('f').size(), 0); + mFloats.resize(locals.get('f').size(), 0); } + mScriptId = script.mId; mInitialised = true; return true; } @@ -63,215 +64,190 @@ namespace MWScript return (mShorts.empty() && mLongs.empty() && mFloats.empty()); } - bool Locals::hasVar(const std::string &script, const std::string &var) + bool Locals::hasVar(const ESM::RefId& script, std::string_view var) { - try - { - ensure (script); + ensure(script); - const Compiler::Locals& locals = - MWBase::Environment::get().getScriptManager()->getLocals(script); - int index = locals.getIndex(var); - return (index != -1); - } - catch (const Compiler::SourceException&) - { - return false; - } + const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); + int index = locals.getIndex(var); + return (index != -1); } - int Locals::getIntVar(const std::string &script, const std::string &var) + double Locals::getVarAsDouble(const ESM::RefId& script, std::string_view var) { - ensure (script); + ensure(script); const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); - char type = locals.getType(var); - if(index != -1) + if (index == -1) + return 0; + switch (locals.getType(var)) { - switch(type) - { - case 's': - return mShorts.at (index); + case 's': + return mShorts.at(index); - case 'l': - return mLongs.at (index); + case 'l': + return mLongs.at(index); - case 'f': - return static_cast(mFloats.at(index)); - default: - return 0; - } + case 'f': + return mFloats.at(index); + default: + return 0; } - return 0; } - float Locals::getFloatVar(const std::string &script, const std::string &var) + bool Locals::setVar(const ESM::RefId& script, std::string_view var, double val) { - ensure (script); + ensure(script); const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); - char type = locals.getType(var); - if(index != -1) + if (index == -1) + return false; + switch (locals.getType(var)) { - switch(type) - { - case 's': - return mShorts.at (index); + case 's': + mShorts.at(index) = static_cast(val); + break; - case 'l': - return mLongs.at (index); + case 'l': + mLongs.at(index) = static_cast(val); + break; - case 'f': - return mFloats.at(index); - default: - return 0; - } + case 'f': + mFloats.at(index) = static_cast(val); + break; } - return 0; + return true; } - bool Locals::setVarByInt(const std::string& script, const std::string& var, int val) + std::size_t Locals::getSize(const ESM::RefId& script) { - ensure (script); - - const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); - int index = locals.getIndex(var); - char type = locals.getType(var); - if(index != -1) - { - switch(type) - { - case 's': - mShorts.at (index) = val; break; - - case 'l': - mLongs.at (index) = val; break; - - case 'f': - mFloats.at(index) = static_cast(val); break; - } - return true; - } - return false; + ensure(script); + return mShorts.size() + mLongs.size() + mFloats.size(); } - bool Locals::write (ESM::Locals& locals, const std::string& script) const + bool Locals::write(ESM::Locals& locals, const ESM::RefId& script) const { if (!mInitialised) return false; - try + const Compiler::Locals& declarations = MWBase::Environment::get().getScriptManager()->getLocals(script); + + for (int i = 0; i < 3; ++i) { - const Compiler::Locals& declarations = - MWBase::Environment::get().getScriptManager()->getLocals(script); + char type = 0; - for (int i=0; i<3; ++i) + switch (i) { - char type = 0; + case 0: + type = 's'; + break; + case 1: + type = 'l'; + break; + case 2: + type = 'f'; + break; + } - switch (i) - { - case 0: type = 's'; break; - case 1: type = 'l'; break; - case 2: type = 'f'; break; - } + const std::vector& names = declarations.get(type); - const std::vector& names = declarations.get (type); + for (int i2 = 0; i2 < static_cast(names.size()); ++i2) + { + ESM::Variant value; - for (int i2=0; i2 (names.size()); ++i2) + switch (i) { - ESM::Variant value; - - switch (i) - { - case 0: value.setType (ESM::VT_Int); value.setInteger (mShorts.at (i2)); break; - case 1: value.setType (ESM::VT_Int); value.setInteger (mLongs.at (i2)); break; - case 2: value.setType (ESM::VT_Float); value.setFloat (mFloats.at (i2)); break; - } - - locals.mVariables.emplace_back (names[i2], value); + case 0: + value.setType(ESM::VT_Int); + value.setInteger(mShorts.at(i2)); + break; + case 1: + value.setType(ESM::VT_Int); + value.setInteger(mLongs.at(i2)); + break; + case 2: + value.setType(ESM::VT_Float); + value.setFloat(mFloats.at(i2)); + break; } + + locals.mVariables.emplace_back(names[i2], value); } } - catch (const Compiler::SourceException&) - { - } return true; } - void Locals::read (const ESM::Locals& locals, const std::string& script) + void Locals::read(const ESM::Locals& locals, const ESM::RefId& script) { - ensure (script); + ensure(script); - try - { - const Compiler::Locals& declarations = - MWBase::Environment::get().getScriptManager()->getLocals(script); + const Compiler::Locals& declarations = MWBase::Environment::get().getScriptManager()->getLocals(script); - int index = 0, numshorts = 0, numlongs = 0; - for (unsigned int v=0; v >::const_iterator iter - = locals.mVariables.begin(); iter!=locals.mVariables.end(); ++iter,++index) + for (std::vector>::const_iterator iter = locals.mVariables.begin(); + iter != locals.mVariables.end(); ++iter, ++index) + { + if (iter->first.empty()) { - if (iter->first.empty()) + // no variable names available (this will happen for legacy, i.e. ESS-imported savegames only) + try { - // no variable names available (this will happen for legacy, i.e. ESS-imported savegames only) - try - { - if (index >= numshorts+numlongs) - mFloats.at(index - (numshorts+numlongs)) = iter->second.getFloat(); - else if (index >= numshorts) - mLongs.at(index - numshorts) = iter->second.getInteger(); - else - mShorts.at(index) = iter->second.getInteger(); - } - catch (std::exception& e) - { - Log(Debug::Error) << "Failed to read local variable state for script '" - << script << "' (legacy format): " << e.what() - << "\nNum shorts: " << numshorts << " / " << mShorts.size() - << " Num longs: " << numlongs << " / " << mLongs.size(); - } + if (index >= numshorts + numlongs) + mFloats.at(index - (numshorts + numlongs)) = iter->second.getFloat(); + else if (index >= numshorts) + mLongs.at(index - numshorts) = iter->second.getInteger(); + else + mShorts.at(index) = iter->second.getInteger(); } - else + catch (std::exception& e) { - char type = declarations.getType (iter->first); - int index2 = declarations.getIndex (iter->first); + Log(Debug::Error) << "Failed to read local variable state for script '" << script + << "' (legacy format): " << e.what() << "\nNum shorts: " << numshorts << " / " + << mShorts.size() << " Num longs: " << numlongs << " / " << mLongs.size(); + } + } + else + { + char type = declarations.getType(iter->first); + int index2 = declarations.getIndex(iter->first); - // silently ignore locals that don't exist anymore - if (type == ' ' || index2 == -1) - continue; + // silently ignore locals that don't exist anymore + if (type == ' ' || index2 == -1) + continue; - try - { - switch (type) - { - case 's': mShorts.at (index2) = iter->second.getInteger(); break; - case 'l': mLongs.at (index2) = iter->second.getInteger(); break; - case 'f': mFloats.at (index2) = iter->second.getFloat(); break; - } - } - catch (...) + try + { + switch (type) { - // ignore type changes - /// \todo write to log + case 's': + mShorts.at(index2) = iter->second.getInteger(); + break; + case 'l': + mLongs.at(index2) = iter->second.getInteger(); + break; + case 'f': + mFloats.at(index2) = iter->second.getFloat(); + break; } } + catch (...) + { + // ignore type changes + /// \todo write to log + } } } - catch (const Compiler::SourceException&) - { - } } } diff --git a/apps/openmw/mwscript/locals.hpp b/apps/openmw/mwscript/locals.hpp index d63411a9429..76b582b78c3 100644 --- a/apps/openmw/mwscript/locals.hpp +++ b/apps/openmw/mwscript/locals.hpp @@ -1,69 +1,81 @@ #ifndef GAME_SCRIPT_LOCALS_H #define GAME_SCRIPT_LOCALS_H +#include +#include #include +#include #include namespace ESM { class Script; struct Locals; + class RefId; } namespace MWScript { class Locals { - bool mInitialised; - - void ensure (const std::string& scriptName); - - public: - std::vector mShorts; - std::vector mLongs; - std::vector mFloats; - - Locals(); - - /// Are there any locals? - /// - /// \note Will return false, if locals have not been configured yet. - bool isEmpty() const; - - /// \return Did the state of *this change from uninitialised to initialised? - bool configure (const ESM::Script& script); - - /// @note var needs to be in lowercase - /// - /// \note Locals will be automatically configured first, if necessary - bool setVarByInt(const std::string& script, const std::string& var, int val); - - /// \note Locals will be automatically configured first, if necessary - // - // \note If it can not be determined if the variable exists, the error will be - // ignored and false will be returned. - bool hasVar(const std::string& script, const std::string& var); - - /// if var does not exist, returns 0 - /// @note var needs to be in lowercase - /// - /// \note Locals will be automatically configured first, if necessary - int getIntVar (const std::string& script, const std::string& var); - - /// if var does not exist, returns 0 - /// @note var needs to be in lowercase - /// - /// \note Locals will be automatically configured first, if necessary - float getFloatVar (const std::string& script, const std::string& var); - - /// \note If locals have not been configured yet, no data is written. - /// - /// \return Locals written? - bool write (ESM::Locals& locals, const std::string& script) const; - - /// \note Locals will be automatically configured first, if necessary - void read (const ESM::Locals& locals, const std::string& script); + bool mInitialised; + ESM::RefId mScriptId; + + void ensure(const ESM::RefId& scriptName); + + public: + std::vector mShorts; + std::vector mLongs; + std::vector mFloats; + + Locals(); + + const ESM::RefId& getScriptId() const { return mScriptId; } + + /// Are there any locals? + /// + /// \note Will return false, if locals have not been configured yet. + bool isEmpty() const; + + /// \return Did the state of *this change from uninitialised to initialised? + bool configure(const ESM::Script& script); + + /// @note var needs to be in lowercase + /// + /// \note Locals will be automatically configured first, if necessary + bool setVarByInt(const ESM::RefId& script, std::string_view var, int val) { return setVar(script, var, val); } + bool setVar(const ESM::RefId& script, std::string_view var, double val); + + /// \note Locals will be automatically configured first, if necessary + // + // \note If it can not be determined if the variable exists, the error will be + // ignored and false will be returned. + bool hasVar(const ESM::RefId& script, std::string_view var); + + /// if var does not exist, returns 0 + /// @note var needs to be in lowercase + /// + /// \note Locals will be automatically configured first, if necessary + double getVarAsDouble(const ESM::RefId& script, std::string_view var); + int getIntVar(const ESM::RefId& script, std::string_view var) + { + return static_cast(getVarAsDouble(script, var)); + } + float getFloatVar(const ESM::RefId& script, std::string_view var) + { + return static_cast(getVarAsDouble(script, var)); + } + + std::size_t getSize(const ESM::RefId& script); + + /// \note If locals have not been configured yet, no data is written. + /// + /// \return Locals written? + bool write(ESM::Locals& locals, const ESM::RefId& script) const; + + /// \note Locals will be automatically configured first, if necessary + void read(const ESM::Locals& locals, const ESM::RefId& script); }; } diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 5538e8536fa..c808cc25189 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -1,46 +1,78 @@ #include "miscextensions.hpp" +#include #include #include +#include -#include +#include #include +#include #include #include -#include #include +#include -#include #include +#include #include - -#include -#include - +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include #include #include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" +#include "../mwbase/luamanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/player.hpp" #include "../mwworld/containerstore.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwworld/cellstore.hpp" +#include "../mwworld/inventorystore.hpp" #include "../mwworld/manualref.hpp" +#include "../mwworld/player.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/aicast.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellcasting.hpp" -#include "../mwmechanics/actorutil.hpp" + +#include "../mwrender/animation.hpp" #include "interpretercontext.hpp" #include "ref.hpp" @@ -48,7 +80,42 @@ namespace { - void addToLevList(ESM::LevelledListBase* list, const std::string& itemId, int level) + struct TextureFetchVisitor : osg::NodeVisitor + { + std::vector> mTextures; + + TextureFetchVisitor(osg::NodeVisitor::TraversalMode mode = TRAVERSE_ALL_CHILDREN) + : osg::NodeVisitor(mode) + { + } + + void apply(osg::Node& node) override + { + const osg::StateSet* stateset = node.getStateSet(); + if (stateset) + { + const osg::StateSet::TextureAttributeList& texAttributes = stateset->getTextureAttributeList(); + for (size_t i = 0; i < texAttributes.size(); i++) + { + const osg::StateAttribute* attr = stateset->getTextureAttribute(i, osg::StateAttribute::TEXTURE); + if (!attr) + continue; + const osg::Texture* texture = attr->asTexture(); + if (!texture) + continue; + const osg::Image* image = texture->getImage(0); + std::string fileName; + if (image) + fileName = image->getFileName(); + mTextures.emplace_back(SceneUtil::getTextureType(*stateset, *texture, i), fileName); + } + } + + traverse(node); + } + }; + + void addToLevList(ESM::LevelledListBase* list, const ESM::RefId& itemId, int level) { for (auto& levelItem : list->mList) { @@ -62,7 +129,7 @@ namespace list->mList.push_back(item); } - void removeFromLevList(ESM::LevelledListBase* list, const std::string& itemId, int level) + void removeFromLevList(ESM::LevelledListBase* list, const ESM::RefId& itemId, int level) { // level of -1 removes all items with that itemId for (std::vector::iterator it = list->mList.begin(); it != list->mList.end();) @@ -72,7 +139,7 @@ namespace ++it; continue; } - if (Misc::StringUtils::ciEqual(itemId, it->mId)) + if (itemId == it->mId) it = list->mList.erase(it); else ++it; @@ -87,396 +154,371 @@ namespace MWScript { class OpMenuMode : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.push (MWBase::Environment::get().getWindowManager()->isGuiMode()); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.push(MWBase::Environment::get().getWindowManager()->isGuiMode()); + } }; class OpRandom : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - Interpreter::Type_Integer limit = runtime[0].mInteger; - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + Interpreter::Type_Integer limit = runtime[0].mInteger; + runtime.pop(); - if (limit<0) - throw std::runtime_error ( - "random: argument out of range (Don't be so negative!)"); + if (limit < 0) + throw std::runtime_error("random: argument out of range (Don't be so negative!)"); - runtime.push (static_cast(::Misc::Rng::rollDice(limit))); // [o, limit) - } + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + runtime.push(static_cast(::Misc::Rng::rollDice(limit, prng))); // [o, limit) + } }; - template + template class OpStartScript : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr target = R()(runtime, false); + ESM::RefId name = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override + if (!MWBase::Environment::get().getESMStore()->get().search(name)) { - MWWorld::Ptr target = R()(runtime, false); - std::string name = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - MWBase::Environment::get().getScriptManager()->getGlobalScripts().addScript (name, target); + runtime.getContext().report( + "Failed to start global script '" + name.getRefIdString() + "': script record not found"); + return; } + + MWBase::Environment::get().getScriptManager()->getGlobalScripts().addScript(name, target); + } }; class OpScriptRunning : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - std::string name = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - runtime.push(MWBase::Environment::get().getScriptManager()->getGlobalScripts().isRunning (name)); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + const ESM::RefId& name = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + runtime.push(MWBase::Environment::get().getScriptManager()->getGlobalScripts().isRunning(name)); + } }; class OpStopScript : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + const ESM::RefId& name = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override + if (!MWBase::Environment::get().getESMStore()->get().search(name)) { - std::string name = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - MWBase::Environment::get().getScriptManager()->getGlobalScripts().removeScript (name); + runtime.getContext().report( + "Failed to stop global script '" + name.getRefIdString() + "': script record not found"); + return; } + + MWBase::Environment::get().getScriptManager()->getGlobalScripts().removeScript(name); + } }; class OpGetSecondsPassed : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.push (MWBase::Environment::get().getFrameDuration()); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.push(MWBase::Environment::get().getFrameDuration()); + } }; - template + template class OpEnable : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - MWBase::Environment::get().getWorld()->enable (ptr); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + MWBase::Environment::get().getWorld()->enable(ptr); + } }; - template + template class OpDisable : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - MWBase::Environment::get().getWorld()->disable (ptr); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + MWBase::Environment::get().getWorld()->disable(ptr); + } }; - template + template class OpGetDisabled : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - runtime.push (!ptr.getRefData().isEnabled()); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + runtime.push(!ptr.getRefData().isEnabled()); + } }; class OpPlayBink : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { - std::string name = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view name = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); bool allowSkipping = runtime[0].mInteger != 0; runtime.pop(); - MWBase::Environment::get().getWindowManager()->playVideo (name, allowSkipping); + MWBase::Environment::get().getWindowManager()->playVideo(name, allowSkipping); } }; class OpGetPcSleep : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { - runtime.push (MWBase::Environment::get().getWindowManager ()->getPlayerSleeping()); + runtime.push(MWBase::Environment::get().getWindowManager()->getPlayerSleeping()); } }; class OpGetPcJumping : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { MWBase::World* world = MWBase::Environment::get().getWorld(); - runtime.push (world->getPlayer().getJumping()); + runtime.push(world->getPlayer().getJumping()); } }; class OpWakeUpPc : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { - MWBase::Environment::get().getWindowManager ()->wakeUpPlayer(); + MWBase::Environment::get().getWindowManager()->wakeUpPlayer(); } }; class OpXBox : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.push (0); - } + public: + void execute(Interpreter::Runtime& runtime) override { runtime.push(0); } }; template class OpOnActivate : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - runtime.push (ptr.getRefData().onActivate()); - } + runtime.push(ptr.getRefData().onActivate()); + } }; template class OpActivate : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - InterpreterContext& context = - static_cast (runtime.getContext()); + public: + void execute(Interpreter::Runtime& runtime) override + { + InterpreterContext& context = static_cast(runtime.getContext()); - MWWorld::Ptr ptr = R()(runtime); + MWWorld::Ptr ptr = R()(runtime); - if (ptr.getRefData().activateByScript() || ptr.getContainerStore()) - context.executeActivation(ptr, MWMechanics::getPlayer()); - } + if (ptr.getRefData().activateByScript() || ptr.getContainerStore()) + context.executeActivation(ptr, MWMechanics::getPlayer()); + } }; - template + template class OpLock : public Interpreter::Opcode1 { - public: - - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer lockLevel = ptr.getCellRef().getLockLevel(); - if(lockLevel==0) { //no lock level was ever set, set to 100 as default - lockLevel = 100; - } + Interpreter::Type_Integer lockLevel = ptr.getCellRef().getLockLevel(); + if (lockLevel == 0) + { // no lock level was ever set, set to 100 as default + lockLevel = 100; + } - if (arg0==1) - { - lockLevel = runtime[0].mInteger; - runtime.pop(); - } + if (arg0 == 1) + { + lockLevel = runtime[0].mInteger; + runtime.pop(); + } - ptr.getCellRef().lock (lockLevel); + ptr.getCellRef().lock(lockLevel); - // Instantly reset door to closed state - // This is done when using Lock in scripts, but not when using Lock spells. - if (ptr.getTypeName() == typeid(ESM::Door).name() && !ptr.getCellRef().getTeleport()) - { - MWBase::Environment::get().getWorld()->activateDoor(ptr, MWWorld::DoorState::Idle); - } + // Instantly reset door to closed state + // This is done when using Lock in scripts, but not when using Lock spells. + if (ptr.getType() == ESM::Door::sRecordId && !ptr.getCellRef().getTeleport()) + { + MWBase::Environment::get().getWorld()->activateDoor(ptr, MWWorld::DoorState::Idle); } + } }; - template + template class OpUnlock : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - ptr.getCellRef().unlock (); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + if (ptr.getCellRef().isLocked()) + ptr.getCellRef().unlock(); + } }; class OpToggleCollisionDebug : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - bool enabled = - MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_CollisionDebug); + public: + void execute(Interpreter::Runtime& runtime) override + { + bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode(MWRender::Render_CollisionDebug); - runtime.getContext().report (enabled ? - "Collision Mesh Rendering -> On" : "Collision Mesh Rendering -> Off"); - } + runtime.getContext().report( + enabled ? "Collision Mesh Rendering -> On" : "Collision Mesh Rendering -> Off"); + } }; - class OpToggleCollisionBoxes : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - bool enabled = - MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_CollisionDebug); + public: + void execute(Interpreter::Runtime& runtime) override + { + bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode(MWRender::Render_CollisionDebug); - runtime.getContext().report (enabled ? - "Collision Mesh Rendering -> On" : "Collision Mesh Rendering -> Off"); - } + runtime.getContext().report( + enabled ? "Collision Mesh Rendering -> On" : "Collision Mesh Rendering -> Off"); + } }; class OpToggleWireframe : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - bool enabled = - MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_Wireframe); + public: + void execute(Interpreter::Runtime& runtime) override + { + bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode(MWRender::Render_Wireframe); - runtime.getContext().report (enabled ? - "Wireframe Rendering -> On" : "Wireframe Rendering -> Off"); - } + runtime.getContext().report(enabled ? "Wireframe Rendering -> On" : "Wireframe Rendering -> Off"); + } }; class OpToggleBorders : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - bool enabled = - MWBase::Environment::get().getWorld()->toggleBorders(); + public: + void execute(Interpreter::Runtime& runtime) override + { + bool enabled = MWBase::Environment::get().getWorld()->toggleBorders(); - runtime.getContext().report (enabled ? - "Border Rendering -> On" : "Border Rendering -> Off"); - } + runtime.getContext().report(enabled ? "Border Rendering -> On" : "Border Rendering -> Off"); + } }; class OpTogglePathgrid : public Interpreter::Opcode0 { public: - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { - bool enabled = - MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_Pathgrid); + bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode(MWRender::Render_Pathgrid); - runtime.getContext().report (enabled ? - "Path Grid rendering -> On" : "Path Grid Rendering -> Off"); + runtime.getContext().report(enabled ? "Path Grid rendering -> On" : "Path Grid Rendering -> Off"); } }; class OpFadeIn : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - Interpreter::Type_Float time = runtime[0].mFloat; - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + Interpreter::Type_Float time = runtime[0].mFloat; + runtime.pop(); - MWBase::Environment::get().getWindowManager()->fadeScreenIn(time, false); - } + MWBase::Environment::get().getWindowManager()->fadeScreenIn(time, false); + } }; class OpFadeOut : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - Interpreter::Type_Float time = runtime[0].mFloat; - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + Interpreter::Type_Float time = runtime[0].mFloat; + runtime.pop(); - MWBase::Environment::get().getWindowManager()->fadeScreenOut(time, false); - } + MWBase::Environment::get().getWindowManager()->fadeScreenOut(time, false); + } }; class OpFadeTo : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - Interpreter::Type_Float alpha = runtime[0].mFloat; - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + Interpreter::Type_Float alpha = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float time = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float time = runtime[0].mFloat; + runtime.pop(); - MWBase::Environment::get().getWindowManager()->fadeScreenTo(static_cast(alpha), time, false); - } + MWBase::Environment::get().getWindowManager()->fadeScreenTo(static_cast(alpha), time, false); + } }; class OpToggleWater : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.getContext().report(MWBase::Environment::get().getWorld()->toggleWater() ? "Water -> On" - : "Water -> Off"); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.getContext().report( + MWBase::Environment::get().getWorld()->toggleWater() ? "Water -> On" : "Water -> Off"); + } }; class OpToggleWorld : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.getContext().report(MWBase::Environment::get().getWorld()->toggleWorld() ? "World -> On" - : "World -> Off"); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.getContext().report( + MWBase::Environment::get().getWorld()->toggleWorld() ? "World -> On" : "World -> Off"); + } }; class OpDontSaveObject : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - // We are ignoring the DontSaveObject statement for now. Probably not worth - // bothering with. The incompatibility we are creating should be marginal at most. - } + public: + void execute(Interpreter::Runtime& runtime) override + { + // We are ignoring the DontSaveObject statement for now. Probably not worth + // bothering with. The incompatibility we are creating should be marginal at most. + } }; class OpPcForce1stPerson : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { if (!MWBase::Environment::get().getWorld()->isFirstPerson()) MWBase::Environment::get().getWorld()->togglePOV(true); @@ -485,7 +527,7 @@ namespace MWScript class OpPcForce3rdPerson : public Interpreter::Opcode0 { - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { if (MWBase::Environment::get().getWorld()->isFirstPerson()) MWBase::Environment::get().getWorld()->togglePOV(true); @@ -506,16 +548,17 @@ namespace MWScript static bool sActivate; public: - - void execute(Interpreter::Runtime &runtime) override + void execute(Interpreter::Runtime& runtime) override { - MWBase::World *world = - MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); - if (world->toggleVanityMode(sActivate)) { + if (world->toggleVanityMode(sActivate)) + { runtime.getContext().report(sActivate ? "Vanity Mode -> On" : "Vanity Mode -> Off"); sActivate = !sActivate; - } else { + } + else + { runtime.getContext().report("Vanity Mode -> No"); } } @@ -525,308 +568,312 @@ namespace MWScript template class OpGetLocked : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - runtime.push (ptr.getCellRef().getLockLevel() > 0); - } + runtime.push(ptr.getCellRef().isLocked()); + } }; template class OpGetEffect : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - std::string effect = runtime.getStringLiteral(runtime[0].mInteger); - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - if (!ptr.getClass().isActor()) - { - runtime.push(0); - return; - } + std::string_view effect = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); - char *end; - long key = strtol(effect.c_str(), &end, 10); - if(key < 0 || key > 32767 || *end != '\0') - key = ESM::MagicEffect::effectStringToId(effect); + if (!ptr.getClass().isActor()) + { + runtime.push(0); + return; + } - const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + long key; - MWMechanics::MagicEffects effects = stats.getSpells().getMagicEffects(); - effects += stats.getActiveSpells().getMagicEffects(); - if (ptr.getClass().hasInventoryStore(ptr) && !stats.isDeathAnimationFinished()) - { - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); - effects += store.getMagicEffects(); - } + if (const auto k = ::Misc::StringUtils::toNumeric(effect.data()); + k.has_value() && *k >= 0 && *k <= 32767) + key = *k; + else + key = ESM::MagicEffect::effectGmstIdToIndex(effect); - for (const auto& activeEffect : effects) + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + for (const auto& spell : stats.getActiveSpells()) + { + for (const auto& effect : spell.getEffects()) { - if (activeEffect.first.mId == key && activeEffect.second.getModifier() > 0) + if (effect.mFlags & ESM::ActiveEffect::Flag_Applied && effect.mEffectId == key) { runtime.push(1); return; } } - runtime.push(0); - } + } + runtime.push(0); + } }; - template + template class OpAddSoulGem : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - std::string creature = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId creature = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string gem = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId gem = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - if (!ptr.getClass().hasInventoryStore(ptr)) - return; + if (!ptr.getClass().isActor()) + return; - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - store.get().find(creature); // This line throws an exception if it can't find the creature + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + store.get().find( + creature); // This line throws an exception if it can't find the creature - MWWorld::Ptr item = *ptr.getClass().getContainerStore(ptr).add(gem, 1, ptr); + MWWorld::Ptr item = *ptr.getClass().getContainerStore(ptr).add(gem, 1); - // Set the soul on just one of the gems, not the whole stack - item.getContainerStore()->unstack(item, ptr); - item.getCellRef().setSoul(creature); + // Set the soul on just one of the gems, not the whole stack + item.getContainerStore()->unstack(item); + item.getCellRef().setSoul(creature); - // Restack the gem with other gems with the same soul - item.getContainerStore()->restack(item); - } + // Restack the gem with other gems with the same soul + item.getContainerStore()->restack(item); + } }; - template + template class OpRemoveSoulGem : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId soul = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string soul = runtime.getStringLiteral (runtime[0].mInteger); + // throw away additional arguments + for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); - // throw away additional arguments - for (unsigned int i=0; igetCellRef().getSoul() == soul) { - if (::Misc::StringUtils::ciEqual(it->getCellRef().getSoul(), soul)) - { - store.remove(*it, 1, ptr); - return; - } + store.remove(*it, 1); + return; } } + } }; - template + template class OpDrop : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { + public: + void execute(Interpreter::Runtime& runtime) override + { - MWWorld::Ptr ptr = R()(runtime); + MWWorld::Ptr ptr = R()(runtime); - std::string item = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId item = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - Interpreter::Type_Integer amount = runtime[0].mInteger; - runtime.pop(); + Interpreter::Type_Integer amount = runtime[0].mInteger; + runtime.pop(); - if (amount<0) - throw std::runtime_error ("amount must be non-negative"); + if (amount < 0) + throw std::runtime_error("amount must be non-negative"); - // no-op - if (amount == 0) - return; + // no-op + if (amount == 0) + return; - if (!ptr.getClass().isActor()) - return; + if (!ptr.getClass().isActor()) + return; - if (ptr.getClass().hasInventoryStore(ptr)) + MWWorld::InventoryStore* invStorePtr = nullptr; + if (ptr.getClass().hasInventoryStore(ptr)) + { + invStorePtr = &ptr.getClass().getInventoryStore(ptr); + // Prefer dropping unequipped items first; re-stack if possible by unequipping items before dropping + // them. + int numNotEquipped = invStorePtr->count(item); + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { - // Prefer dropping unequipped items first; re-stack if possible by unequipping items before dropping them. - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); - int numNotEquipped = store.count(item); - for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + MWWorld::ConstContainerStoreIterator it = invStorePtr->getSlot(slot); + if (it != invStorePtr->end() && it->getCellRef().getRefId() == item) { - MWWorld::ConstContainerStoreIterator it = store.getSlot (slot); - if (it != store.end() && ::Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), item)) - { - numNotEquipped -= it->getRefData().getCount(); - } + numNotEquipped -= it->getCellRef().getCount(); } + } - for (int slot = 0; slot < MWWorld::InventoryStore::Slots && amount > numNotEquipped; ++slot) + for (int slot = 0; slot < MWWorld::InventoryStore::Slots && amount > numNotEquipped; ++slot) + { + MWWorld::ContainerStoreIterator it = invStorePtr->getSlot(slot); + if (it != invStorePtr->end() && it->getCellRef().getRefId() == item) { - MWWorld::ContainerStoreIterator it = store.getSlot (slot); - if (it != store.end() && ::Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), item)) - { - int numToRemove = std::min(amount - numNotEquipped, it->getRefData().getCount()); - store.unequipItemQuantity(*it, ptr, numToRemove); - numNotEquipped += numToRemove; - } + int numToRemove = std::min(amount - numNotEquipped, it->getCellRef().getCount()); + invStorePtr->unequipItemQuantity(*it, numToRemove); + numNotEquipped += numToRemove; } + } + } - for (MWWorld::ContainerStoreIterator iter (store.begin()); iter!=store.end(); ++iter) - { - if (::Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), item) && !store.isEquipped(*iter)) - { - int removed = store.remove(*iter, amount, ptr); - MWWorld::Ptr dropped = MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, removed); - dropped.getCellRef().setOwner(""); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + for (MWWorld::ContainerStoreIterator iter(store.begin()); iter != store.end(); ++iter) + { + if (iter->getCellRef().getRefId() == item && (!invStorePtr || !invStorePtr->isEquipped(*iter))) + { + int removed = store.remove(*iter, amount); + MWWorld::Ptr dropped + = MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, removed); + dropped.getCellRef().setOwner(ESM::RefId()); - amount -= removed; + amount -= removed; - if (amount <= 0) - break; - } - } + if (amount <= 0) + break; } + } - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), item, 1); - MWWorld::Ptr itemPtr(ref.getPtr()); - if (amount > 0) + MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), item, 1); + MWWorld::Ptr itemPtr(ref.getPtr()); + if (amount > 0) + { + if (itemPtr.getClass().getScript(itemPtr).empty()) { - if (itemPtr.getClass().getScript(itemPtr).empty()) - { - MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, itemPtr, amount); - } - else - { - // Dropping one item per time to prevent making stacks of scripted items - for (int i = 0; i < amount; i++) - MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, itemPtr, 1); - } + MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, itemPtr, amount); + } + else + { + // Dropping one item per time to prevent making stacks of scripted items + for (int i = 0; i < amount; i++) + MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, itemPtr, 1); } - - MWBase::Environment::get().getSoundManager()->playSound3D(ptr, itemPtr.getClass().getDownSoundId(itemPtr), 1.f, 1.f); } + + MWBase::Environment::get().getSoundManager()->playSound3D( + ptr, itemPtr.getClass().getDownSoundId(itemPtr), 1.f, 1.f); + } }; - template + template class OpDropSoulGem : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { + public: + void execute(Interpreter::Runtime& runtime) override + { - MWWorld::Ptr ptr = R()(runtime); + MWWorld::Ptr ptr = R()(runtime); - std::string soul = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId soul = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - if (!ptr.getClass().hasInventoryStore(ptr)) - return; + if (!ptr.getClass().isActor()) + return; - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); - for (MWWorld::ContainerStoreIterator iter (store.begin()); iter!=store.end(); ++iter) + for (MWWorld::ContainerStoreIterator iter(store.begin()); iter != store.end(); ++iter) + { + if (iter->getCellRef().getSoul() == soul) { - if (::Misc::StringUtils::ciEqual(iter->getCellRef().getSoul(), soul)) - { - MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, 1); - store.remove(*iter, 1, ptr); - break; - } + MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, 1); + store.remove(*iter, 1); + break; } } + } }; template class OpGetAttacked : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getCreatureStats (ptr).getAttacked ()); - } + runtime.push(ptr.getClass().getCreatureStats(ptr).getAttacked()); + } }; template class OpGetWeaponDrawn : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + auto& cls = ptr.getClass(); + if (!cls.hasInventoryStore(ptr) && !cls.isBipedal(ptr)) { - MWWorld::Ptr ptr = R()(runtime); + runtime.push(0); + return; + } - runtime.push((ptr.getClass().hasInventoryStore(ptr) || ptr.getClass().isBipedal(ptr)) && - ptr.getClass().getCreatureStats (ptr).getDrawState () == MWMechanics::DrawState_Weapon); + if (cls.getCreatureStats(ptr).getDrawState() != MWMechanics::DrawState::Weapon) + { + runtime.push(0); + return; } + + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); + runtime.push(anim && anim->getWeaponsShown()); + } }; template class OpGetSpellReadied : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getClass().getCreatureStats (ptr).getDrawState () == MWMechanics::DrawState_Spell); - } + runtime.push(ptr.getClass().getCreatureStats(ptr).getDrawState() == MWMechanics::DrawState::Spell); + } }; template class OpGetSpellEffects : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override + if (!ptr.getClass().isActor()) { - MWWorld::Ptr ptr = R()(runtime); - std::string id = runtime.getStringLiteral(runtime[0].mInteger); - runtime.pop(); - - if (!ptr.getClass().isActor()) - { - runtime.push(0); - return; - } - - const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - runtime.push(stats.getActiveSpells().isSpellActive(id) || stats.getSpells().isSpellActive(id)); + runtime.push(0); + return; } + + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + runtime.push(stats.getActiveSpells().isSpellActive(id)); + } }; class OpGetCurrentTime : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { runtime.push(MWBase::Environment::get().getWorld()->getTimeStamp().getHour()); } @@ -835,250 +882,240 @@ namespace MWScript template class OpSetDelete : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - int parameter = runtime[0].mInteger; - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + int parameter = runtime[0].mInteger; + runtime.pop(); - if (parameter == 1) - MWBase::Environment::get().getWorld()->deleteObject(ptr); - else if (parameter == 0) - MWBase::Environment::get().getWorld()->undeleteObject(ptr); - else - throw std::runtime_error("SetDelete: unexpected parameter"); - } + if (parameter == 1) + MWBase::Environment::get().getWorld()->deleteObject(ptr); + else if (parameter == 0) + MWBase::Environment::get().getWorld()->undeleteObject(ptr); + else + throw std::runtime_error("SetDelete: unexpected parameter"); + } }; class OpGetSquareRoot : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + float param = runtime[0].mFloat; + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override - { - float param = runtime[0].mFloat; - runtime.pop(); + if (param < 0) + throw std::runtime_error("square root of negative number (we aren't that imaginary)"); - runtime.push(std::sqrt (param)); - } + runtime.push(std::sqrt(param)); + } }; template class OpFall : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - } + public: + void execute(Interpreter::Runtime& runtime) override {} }; template class OpGetStandingPc : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - runtime.push (MWBase::Environment::get().getWorld()->getPlayerStandingOn(ptr)); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + runtime.push(MWBase::Environment::get().getWorld()->getPlayerStandingOn(ptr)); + } }; template class OpGetStandingActor : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - runtime.push (MWBase::Environment::get().getWorld()->getActorStandingOn(ptr)); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + runtime.push(MWBase::Environment::get().getWorld()->getActorStandingOn(ptr)); + } }; template class OpGetCollidingPc : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - runtime.push (MWBase::Environment::get().getWorld()->getPlayerCollidingWith(ptr)); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + runtime.push(MWBase::Environment::get().getWorld()->getPlayerCollidingWith(ptr)); + } }; template class OpGetCollidingActor : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - runtime.push (MWBase::Environment::get().getWorld()->getActorCollidingWith(ptr)); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + runtime.push(MWBase::Environment::get().getWorld()->getActorCollidingWith(ptr)); + } }; template class OpHurtStandingActor : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - float healthDiffPerSecond = runtime[0].mFloat; - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + float healthDiffPerSecond = runtime[0].mFloat; + runtime.pop(); - MWBase::Environment::get().getWorld()->hurtStandingActors(ptr, healthDiffPerSecond); - } + MWBase::Environment::get().getWorld()->hurtStandingActors(ptr, healthDiffPerSecond); + } }; template class OpHurtCollidingActor : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - float healthDiffPerSecond = runtime[0].mFloat; - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + float healthDiffPerSecond = runtime[0].mFloat; + runtime.pop(); - MWBase::Environment::get().getWorld()->hurtCollidingActors(ptr, healthDiffPerSecond); - } + MWBase::Environment::get().getWorld()->hurtCollidingActors(ptr, healthDiffPerSecond); + } }; class OpGetWindSpeed : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.push(MWBase::Environment::get().getWorld()->getWindSpeed()); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.push(MWBase::Environment::get().getWorld()->getWindSpeed()); + } }; template class OpHitOnMe : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - std::string objectID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); - runtime.push(::Misc::StringUtils::ciEqual(objectID, stats.getLastHitObject())); + ESM::RefId objectID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - stats.setLastHitObject(std::string()); - } + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + bool hit = objectID == stats.getLastHitObject(); + runtime.push(hit); + if (hit) + stats.clearLastHitObject(); + } }; template class OpHitAttemptOnMe : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - std::string objectID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); - runtime.push(::Misc::StringUtils::ciEqual(objectID, stats.getLastHitAttemptObject())); + ESM::RefId objectID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - stats.setLastHitAttemptObject(std::string()); - } + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + bool hit = objectID == stats.getLastHitAttemptObject(); + runtime.push(hit); + if (hit) + stats.clearLastHitAttemptObject(); + } }; template class OpEnableTeleporting : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - world->enableTeleporting(Enable); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + world->enableTeleporting(Enable); + } }; template class OpEnableLevitation : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - world->enableLevitation(Enable); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + world->enableLevitation(Enable); + } }; template class OpShow : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime, false); - std::string var = runtime.getStringLiteral(runtime[0].mInteger); + std::string_view var = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); std::stringstream output; if (!ptr.isEmpty()) { - const std::string& script = ptr.getClass().getScript(ptr); + ESM::RefId script = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); if (!script.empty()) { - const Compiler::Locals& locals = - MWBase::Environment::get().getScriptManager()->getLocals(script); + const Compiler::Locals& locals + = MWBase::Environment::get().getScriptManager()->getLocals(script); char type = locals.getType(var); - std::string refId = ptr.getCellRef().getRefId(); + std::string refId = ptr.getCellRef().getRefId().getRefIdString(); if (refId.find(' ') != std::string::npos) refId = '"' + refId + '"'; switch (type) { - case 'l': - case 's': - output << refId << "." << var << " = " << ptr.getRefData().getLocals().getIntVar(script, var); - break; - case 'f': - output << refId << "." << var << " = " << ptr.getRefData().getLocals().getFloatVar(script, var); - break; + case 'l': + case 's': + output << refId << "." << var << " = " + << ptr.getRefData().getLocals().getIntVar(script, var); + break; + case 'f': + output << refId << "." << var << " = " + << ptr.getRefData().getLocals().getFloatVar(script, var); + break; } } } if (output.rdbuf()->in_avail() == 0) { - MWBase::World *world = MWBase::Environment::get().getWorld(); - char type = world->getGlobalVariableType (var); + MWBase::World* world = MWBase::Environment::get().getWorld(); + char type = world->getGlobalVariableType(var); switch (type) { - case 's': - output << var << " = " << runtime.getContext().getGlobalShort (var); - break; - case 'l': - output << var << " = " << runtime.getContext().getGlobalLong (var); - break; - case 'f': - output << var << " = " << runtime.getContext().getGlobalFloat (var); - break; - default: - output << "unknown variable"; + case 's': + output << var << " = " << runtime.getContext().getGlobalShort(var); + break; + case 'l': + output << var << " = " << runtime.getContext().getGlobalLong(var); + break; + case 'f': + output << var << " = " << runtime.getContext().getGlobalFloat(var); + break; + default: + output << "unknown variable"; } } runtime.getContext().report(output.str()); @@ -1088,73 +1125,74 @@ namespace MWScript template class OpShowVars : public Interpreter::Opcode0 { - void printLocalVars(Interpreter::Runtime &runtime, const MWWorld::Ptr &ptr) + void printLocalVars(Interpreter::Runtime& runtime, const MWWorld::Ptr& ptr) { std::stringstream str; - const std::string script = ptr.getClass().getScript(ptr); - if(script.empty()) - str<< ptr.getCellRef().getRefId()<<" does not have a script."; + const ESM::RefId& script = ptr.getClass().getScript(ptr); + if (script.empty()) + str << ptr.getCellRef().getRefId() << " does not have a script."; else { - str<< "Local variables for "<getLocals(script); + const Locals& locals = ptr.getRefData().getLocals(); + 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) + const std::vector* names = &complocals.get('s'); + for (size_t i = 0; i < names->size(); ++i) { - if(i >= locals.mShorts.size()) + if (i >= locals.mShorts.size()) break; - str<size();++i) + for (size_t i = 0; i < names->size(); ++i) { - if(i >= locals.mLongs.size()) + if (i >= locals.mLongs.size()) break; - str<size();++i) + for (size_t i = 0; i < names->size(); ++i) { - if(i >= locals.mFloats.size()) + if (i >= locals.mFloats.size()) break; - str< names = runtime.getContext().getGlobals(); - for(size_t i = 0;i < names.size();++i) + for (size_t i = 0; i < names.size(); ++i) { - char type = world->getGlobalVariableType (names[i]); + char type = world->getGlobalVariableType(names[i]); str << std::endl << " " << names[i] << " = "; switch (type) { case 's': - str << runtime.getContext().getGlobalShort (names[i]) << " (short)"; + str << runtime.getContext().getGlobalShort(names[i]) << " (short)"; break; case 'l': - str << runtime.getContext().getGlobalLong (names[i]) << " (long)"; + str << runtime.getContext().getGlobalLong(names[i]) << " (long)"; break; case 'f': - str << runtime.getContext().getGlobalFloat (names[i]) << " (float)"; + str << runtime.getContext().getGlobalFloat(names[i]) << " (float)"; break; default: @@ -1163,7 +1201,7 @@ namespace MWScript } } - runtime.getContext().report (str.str()); + runtime.getContext().report(str.str()); } public: @@ -1183,7 +1221,7 @@ namespace MWScript class OpToggleScripts : public Interpreter::Opcode0 { public: - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleScripts(); @@ -1193,46 +1231,50 @@ namespace MWScript class OpToggleGodMode : public Interpreter::Opcode0 { - public: - void execute (Interpreter::Runtime& runtime) override - { - bool enabled = MWBase::Environment::get().getWorld()->toggleGodMode(); + public: + void execute(Interpreter::Runtime& runtime) override + { + bool enabled = MWBase::Environment::get().getWorld()->toggleGodMode(); - runtime.getContext().report (enabled ? "God Mode -> On" : "God Mode -> Off"); - } + runtime.getContext().report(enabled ? "God Mode -> On" : "God Mode -> Off"); + } }; template class OpCast : public Interpreter::Opcode0 { public: - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - std::string spellId = runtime.getStringLiteral (runtime[0].mInteger); + ESM::RefId spellId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - std::string targetId = ::Misc::StringUtils::lowerCase(runtime.getStringLiteral (runtime[0].mInteger)); + ESM::RefId targetId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(spellId); if (!spell) { - runtime.getContext().report("spellcasting failed: cannot find spell \""+spellId+"\""); + runtime.getContext().report( + "spellcasting failed: cannot find spell \"" + spellId.getRefIdString() + "\""); return; } if (ptr == MWMechanics::getPlayer()) { - MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spellId); + MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spell->mId); return; } if (ptr.getClass().isActor()) { - MWMechanics::AiCast castPackage(targetId, spellId, true); - ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + if (!MWBase::Environment::get().getMechanicsManager()->isCastingSpell(ptr)) + { + MWMechanics::AiCast castPackage(targetId, spell->mId, true); + ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(castPackage, ptr); + } return; } @@ -1241,7 +1283,7 @@ namespace MWScript return; MWMechanics::CastSpell cast(ptr, target, false, true); - cast.playSpellCastingEffects(spell->mId, false); + cast.playSpellCastingEffects(spell); cast.mHitPosition = target.getRefData().getPosition().asVec3(); cast.mAlwaysSucceed = true; cast.cast(spell); @@ -1252,30 +1294,34 @@ namespace MWScript class OpExplodeSpell : public Interpreter::Opcode0 { public: - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - std::string spellId = runtime.getStringLiteral (runtime[0].mInteger); + ESM::RefId spellId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(spellId); if (!spell) { - runtime.getContext().report("spellcasting failed: cannot find spell \""+spellId+"\""); + runtime.getContext().report( + "spellcasting failed: cannot find spell \"" + spellId.getRefIdString() + "\""); return; } if (ptr == MWMechanics::getPlayer()) { - MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spellId); + MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spell->mId); return; } if (ptr.getClass().isActor()) { - MWMechanics::AiCast castPackage(ptr.getCellRef().getRefId(), spellId, true); - ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + if (!MWBase::Environment::get().getMechanicsManager()->isCastingSpell(ptr)) + { + MWMechanics::AiCast castPackage(ptr.getCellRef().getRefId(), spell->mId, true); + ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(castPackage, ptr); + } return; } @@ -1289,7 +1335,7 @@ namespace MWScript class OpGoToJail : public Interpreter::Opcode0 { public: - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { MWBase::World* world = MWBase::Environment::get().getWorld(); world->goToJail(); @@ -1299,19 +1345,21 @@ namespace MWScript class OpPayFine : public Interpreter::Opcode0 { public: - void execute(Interpreter::Runtime &runtime) override + void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).setBounty(0); - MWBase::Environment::get().getWorld()->confiscateStolenItems(player); - MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + world->confiscateStolenItems(player); + world->getPlayer().recordCrimeId(); + world->getPlayer().setDrawState(MWMechanics::DrawState::Nothing); } }; class OpPayFineThief : public Interpreter::Opcode0 { public: - void execute(Interpreter::Runtime &runtime) override + void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).setBounty(0); @@ -1321,29 +1369,27 @@ namespace MWScript class OpGetPcInJail : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime &runtime) override - { - runtime.push (MWBase::Environment::get().getWorld()->isPlayerInJail()); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.push(MWBase::Environment::get().getWorld()->isPlayerInJail()); + } }; class OpGetPcTraveling : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime &runtime) override - { - runtime.push (MWBase::Environment::get().getWorld()->isPlayerTraveling()); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.push(MWBase::Environment::get().getWorld()->isPlayerTraveling()); + } }; template class OpBetaComment : public Interpreter::Opcode1 { public: - void execute(Interpreter::Runtime &runtime, unsigned int arg0) override + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); @@ -1352,23 +1398,30 @@ namespace MWScript msg << "Report time: "; std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - msg << std::put_time(std::gmtime(¤tTime), "%Y.%m.%d %T UTC") << std::endl; + tm timeinfo{}; +#ifdef _WIN32 + gmtime_s(&timeinfo, ¤tTime); +#else + gmtime_r(¤tTime, &timeinfo); +#endif + msg << std::put_time(&timeinfo, "%Y.%m.%d %T UTC") << std::endl; - msg << "Content file: "; + msg << "Content file: " << ptr.getCellRef().getRefNum().mContentFile; if (!ptr.getCellRef().hasContentFile()) - msg << "[None]" << std::endl; + msg << " [None]" << std::endl; else { std::vector contentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); - msg << contentFiles.at (ptr.getCellRef().getRefNum().mContentFile) << std::endl; - msg << "RefNum: " << ptr.getCellRef().getRefNum().mIndex << std::endl; + msg << " [" << contentFiles.at(ptr.getCellRef().getRefNum().mContentFile) << "]" << std::endl; } + msg << "RefNum: " << ptr.getCellRef().getRefNum().mIndex << std::endl; + if (ptr.getRefData().isDeletedByContentFile()) msg << "[Deleted by content file]" << std::endl; - if (!ptr.getRefData().getCount()) + if (!ptr.getCellRef().getCount()) msg << "[Deleted]" << std::endl; msg << "RefID: " << ptr.getCellRef().getRefId() << std::endl; @@ -1379,17 +1432,65 @@ namespace MWScript MWWorld::CellStore* cell = ptr.getCell(); msg << "Cell: " << MWBase::Environment::get().getWorld()->getCellName(cell) << std::endl; if (cell->getCell()->isExterior()) - msg << "Grid: " << cell->getCell()->getGridX() << " " << cell->getCell()->getGridY() << std::endl; - osg::Vec3f pos (ptr.getRefData().getPosition().asVec3()); + msg << "Grid: " << cell->getCell()->getGridX() << " " << cell->getCell()->getGridY() + << std::endl; + osg::Vec3f pos(ptr.getRefData().getPosition().asVec3()); msg << "Coordinates: " << pos.x() << " " << pos.y() << " " << pos.z() << std::endl; auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - std::string model = ::Misc::ResourceHelpers::correctActorModelPath(ptr.getClass().getModel(ptr), vfs); - msg << "Model: " << model << std::endl; - if(!model.empty()) + const VFS::Path::Normalized model( + ::Misc::ResourceHelpers::correctActorModelPath(ptr.getClass().getCorrectedModel(ptr), vfs)); + msg << "Model: " << model.value() << std::endl; + if (!model.empty()) { const std::string archive = vfs->getArchive(model); - if(!archive.empty()) + if (!archive.empty()) msg << "(" << archive << ")" << std::endl; + TextureFetchVisitor visitor; + SceneUtil::PositionAttitudeTransform* baseNode = ptr.getRefData().getBaseNode(); + if (baseNode) + baseNode->accept(visitor); + // The instance might not have a physical model due to paging or scripting. + // If this is the case, fall back to the template + if (visitor.mTextures.empty()) + { + Resource::SceneManager* sceneManager + = MWBase::Environment::get().getResourceSystem()->getSceneManager(); + const_cast(sceneManager->getTemplate(model).get())->accept(visitor); + msg << "Bound textures: [None]" << std::endl; + if (!visitor.mTextures.empty()) + msg << "Model textures: "; + } + else + { + msg << "Bound textures: "; + } + if (!visitor.mTextures.empty()) + { + msg << std::endl; + std::string lastTextureSrc; + for (auto& [textureName, fileName] : visitor.mTextures) + { + std::string textureSrc; + if (!fileName.empty()) + textureSrc = vfs->getArchive(fileName); + + if (lastTextureSrc.empty() || textureSrc != lastTextureSrc) + { + lastTextureSrc = std::move(textureSrc); + if (lastTextureSrc.empty()) + lastTextureSrc = "[No Source]"; + + msg << " " << lastTextureSrc << std::endl; + } + msg << " "; + msg << (textureName.empty() ? "[Anonymous]: " : textureName) << ": "; + msg << (fileName.empty() ? "[No File]" : fileName) << std::endl; + } + } + else + { + msg << "[None]" << std::endl; + } } if (!ptr.getClass().getScript(ptr).empty()) msg << "Script: " << ptr.getClass().getScript(ptr) << std::endl; @@ -1397,7 +1498,7 @@ namespace MWScript while (arg0 > 0) { - std::string notes = runtime.getStringLiteral (runtime[0].mInteger); + std::string_view notes = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (!notes.empty()) msg << "Notes: " << notes << std::endl; @@ -1413,72 +1514,76 @@ namespace MWScript class OpAddToLevCreature : public Interpreter::Opcode0 { public: - void execute(Interpreter::Runtime &runtime) override + void execute(Interpreter::Runtime& runtime) override { - const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + const ESM::RefId& levId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - const std::string& creatureId = runtime.getStringLiteral(runtime[0].mInteger); + const ESM::RefId& creatureId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); - ESM::CreatureLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); + ESM::CreatureLevList listCopy + = *MWBase::Environment::get().getESMStore()->get().find(levId); addToLevList(&listCopy, creatureId, level); - MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); + MWBase::Environment::get().getESMStore()->overrideRecord(listCopy); } }; class OpRemoveFromLevCreature : public Interpreter::Opcode0 { public: - void execute(Interpreter::Runtime &runtime) override + void execute(Interpreter::Runtime& runtime) override { - const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + const ESM::RefId& levId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - const std::string& creatureId = runtime.getStringLiteral(runtime[0].mInteger); + const ESM::RefId& creatureId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); - ESM::CreatureLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); + ESM::CreatureLevList listCopy + = *MWBase::Environment::get().getESMStore()->get().find(levId); removeFromLevList(&listCopy, creatureId, level); - MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); + MWBase::Environment::get().getESMStore()->overrideRecord(listCopy); } }; class OpAddToLevItem : public Interpreter::Opcode0 { public: - void execute(Interpreter::Runtime &runtime) override + void execute(Interpreter::Runtime& runtime) override { - const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + const ESM::RefId& levId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - const std::string& itemId = runtime.getStringLiteral(runtime[0].mInteger); + const ESM::RefId& itemId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); - ESM::ItemLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); + ESM::ItemLevList listCopy + = *MWBase::Environment::get().getESMStore()->get().find(levId); addToLevList(&listCopy, itemId, level); - MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); + MWBase::Environment::get().getESMStore()->overrideRecord(listCopy); } }; class OpRemoveFromLevItem : public Interpreter::Opcode0 { public: - void execute(Interpreter::Runtime &runtime) override + void execute(Interpreter::Runtime& runtime) override { - const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + const ESM::RefId& levId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); - const std::string& itemId = runtime.getStringLiteral(runtime[0].mInteger); + const ESM::RefId& itemId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); int level = runtime[0].mInteger; runtime.pop(); - ESM::ItemLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); + ESM::ItemLevList listCopy + = *MWBase::Environment::get().getESMStore()->get().find(levId); removeFromLevList(&listCopy, itemId, level); - MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); + MWBase::Environment::get().getESMStore()->overrideRecord(listCopy); } }; @@ -1486,219 +1591,299 @@ namespace MWScript class OpShowSceneGraph : public Interpreter::Opcode1 { public: - void execute(Interpreter::Runtime &runtime, unsigned int arg0) override + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime, false); int confirmed = 0; - if (arg0==1) + if (arg0 == 1) { confirmed = runtime[0].mInteger; runtime.pop(); } if (ptr.isEmpty() && !confirmed) - runtime.getContext().report("Exporting the entire scene graph will result in a large file. Confirm this action using 'showscenegraph 1' or select an object instead."); + runtime.getContext().report( + "Exporting the entire scene graph will result in a large file. Confirm this action using " + "'showscenegraph 1' or select an object instead."); else { - const std::string& filename = MWBase::Environment::get().getWorld()->exportSceneGraph(ptr); - runtime.getContext().report("Wrote '" + filename + "'"); + const auto filename = MWBase::Environment::get().getWorld()->exportSceneGraph(ptr); + runtime.getContext().report("Wrote '" + Files::pathToUnicodeString(filename) + "'"); } } }; class OpToggleNavMesh : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - bool enabled = - MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_NavMesh); + public: + void execute(Interpreter::Runtime& runtime) override + { + bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode(MWRender::Render_NavMesh); - runtime.getContext().report (enabled ? - "Navigation Mesh Rendering -> On" : "Navigation Mesh Rendering -> Off"); - } + runtime.getContext().report( + enabled ? "Navigation Mesh Rendering -> On" : "Navigation Mesh Rendering -> Off"); + } }; class OpToggleActorsPaths : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - bool enabled = - MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_ActorsPaths); + public: + void execute(Interpreter::Runtime& runtime) override + { + bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode(MWRender::Render_ActorsPaths); - runtime.getContext().report (enabled ? - "Agents Paths Rendering -> On" : "Agents Paths Rendering -> Off"); - } + runtime.getContext().report(enabled ? "Agents Paths Rendering -> On" : "Agents Paths Rendering -> Off"); + } }; class OpSetNavMeshNumberToRender : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + const auto navMeshNumber = runtime[0].mInteger; + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override + if (navMeshNumber < 0) { - const auto navMeshNumber = runtime[0].mInteger; - runtime.pop(); - - if (navMeshNumber < 0) - { - runtime.getContext().report("Invalid navmesh number: use not less than zero values"); - return; - } - - MWBase::Environment::get().getWorld()->setNavMeshNumberToRender(static_cast(navMeshNumber)); + runtime.getContext().report("Invalid navmesh number: use not less than zero values"); + return; } + + MWBase::Environment::get().getWorld()->setNavMeshNumberToRender( + static_cast(navMeshNumber)); + } }; template class OpRepairedOnMe : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - // Broken in vanilla and deliberately no-op. - runtime.push(0); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + // Broken in vanilla and deliberately no-op. + runtime.push(0); + } }; class OpToggleRecastMesh : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode(MWRender::Render_RecastMesh); - void execute (Interpreter::Runtime& runtime) override - { - bool enabled = - MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_RecastMesh); + runtime.getContext().report(enabled ? "Recast Mesh Rendering -> On" : "Recast Mesh Rendering -> Off"); + } + }; + + class OpHelp : public Interpreter::Opcode0 + { + public: + void execute(Interpreter::Runtime& runtime) override + { + std::stringstream message; + message << MWBase::Environment::get().getWindowManager()->getVersionDescription() << "\n\n"; + std::vector commands; + MWBase::Environment::get().getScriptManager()->getExtensions().listKeywords(commands); + for (const auto& command : commands) + message << command << "\n"; + runtime.getContext().report(message.str()); + } + }; + + class OpReloadLua : public Interpreter::Opcode0 + { + public: + void execute(Interpreter::Runtime& runtime) override + { + MWBase::Environment::get().getLuaManager()->reloadAllScripts(); + runtime.getContext().report("All Lua scripts are reloaded"); + } + }; - runtime.getContext().report (enabled ? - "Recast Mesh Rendering -> On" : "Recast Mesh Rendering -> Off"); + class OpTestModels : public Interpreter::Opcode0 + { + template + void test(int& count) const + { + Resource::SceneManager* sceneManager + = MWBase::Environment::get().getResourceSystem()->getSceneManager(); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + for (const T& record : store.get()) + { + MWWorld::ManualRef ref(store, record.mId); + VFS::Path::Normalized model(ref.getPtr().getClass().getCorrectedModel(ref.getPtr())); + if (!model.empty()) + { + sceneManager->getTemplate(model); + ++count; + } } + } + + public: + void execute(Interpreter::Runtime& runtime) override + { + Resource::SceneManager* sceneManager + = MWBase::Environment::get().getResourceSystem()->getSceneManager(); + double delay = sceneManager->getExpiryDelay(); + sceneManager->setExpiryDelay(0.0); + int count = 0; + + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + test(count); + + sceneManager->setExpiryDelay(delay); + std::stringstream message; + message << "Attempted to load models for " << count << " objects. Check the log for details."; + runtime.getContext().report(message.str()); + } }; - void installOpcodes (Interpreter::Interpreter& interpreter) - { - interpreter.installSegment5 (Compiler::Misc::opcodeMenuMode, new OpMenuMode); - interpreter.installSegment5 (Compiler::Misc::opcodeRandom, new OpRandom); - interpreter.installSegment5 (Compiler::Misc::opcodeScriptRunning, new OpScriptRunning); - interpreter.installSegment5 (Compiler::Misc::opcodeStartScript, new OpStartScript); - interpreter.installSegment5 (Compiler::Misc::opcodeStartScriptExplicit, new OpStartScript); - interpreter.installSegment5 (Compiler::Misc::opcodeStopScript, new OpStopScript); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSecondsPassed, new OpGetSecondsPassed); - interpreter.installSegment5 (Compiler::Misc::opcodeEnable, new OpEnable); - interpreter.installSegment5 (Compiler::Misc::opcodeEnableExplicit, new OpEnable); - interpreter.installSegment5 (Compiler::Misc::opcodeDisable, new OpDisable); - interpreter.installSegment5 (Compiler::Misc::opcodeDisableExplicit, new OpDisable); - interpreter.installSegment5 (Compiler::Misc::opcodeGetDisabled, new OpGetDisabled); - interpreter.installSegment5 (Compiler::Misc::opcodeGetDisabledExplicit, new OpGetDisabled); - interpreter.installSegment5 (Compiler::Misc::opcodeXBox, new OpXBox); - interpreter.installSegment5 (Compiler::Misc::opcodeOnActivate, new OpOnActivate); - interpreter.installSegment5 (Compiler::Misc::opcodeOnActivateExplicit, new OpOnActivate); - interpreter.installSegment5 (Compiler::Misc::opcodeActivate, new OpActivate); - interpreter.installSegment5 (Compiler::Misc::opcodeActivateExplicit, new OpActivate); - interpreter.installSegment3 (Compiler::Misc::opcodeLock, new OpLock); - interpreter.installSegment3 (Compiler::Misc::opcodeLockExplicit, new OpLock); - interpreter.installSegment5 (Compiler::Misc::opcodeUnlock, new OpUnlock); - interpreter.installSegment5 (Compiler::Misc::opcodeUnlockExplicit, new OpUnlock); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleCollisionDebug, new OpToggleCollisionDebug); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleCollisionBoxes, new OpToggleCollisionBoxes); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleWireframe, new OpToggleWireframe); - interpreter.installSegment5 (Compiler::Misc::opcodeFadeIn, new OpFadeIn); - interpreter.installSegment5 (Compiler::Misc::opcodeFadeOut, new OpFadeOut); - interpreter.installSegment5 (Compiler::Misc::opcodeFadeTo, new OpFadeTo); - interpreter.installSegment5 (Compiler::Misc::opcodeTogglePathgrid, new OpTogglePathgrid); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleWater, new OpToggleWater); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleWorld, new OpToggleWorld); - interpreter.installSegment5 (Compiler::Misc::opcodeDontSaveObject, new OpDontSaveObject); - interpreter.installSegment5 (Compiler::Misc::opcodePcForce1stPerson, new OpPcForce1stPerson); - interpreter.installSegment5 (Compiler::Misc::opcodePcForce3rdPerson, new OpPcForce3rdPerson); - interpreter.installSegment5 (Compiler::Misc::opcodePcGet3rdPerson, new OpPcGet3rdPerson); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleVanityMode, new OpToggleVanityMode); - interpreter.installSegment5 (Compiler::Misc::opcodeGetPcSleep, new OpGetPcSleep); - interpreter.installSegment5 (Compiler::Misc::opcodeGetPcJumping, new OpGetPcJumping); - interpreter.installSegment5 (Compiler::Misc::opcodeWakeUpPc, new OpWakeUpPc); - interpreter.installSegment5 (Compiler::Misc::opcodePlayBink, new OpPlayBink); - interpreter.installSegment5 (Compiler::Misc::opcodePayFine, new OpPayFine); - interpreter.installSegment5 (Compiler::Misc::opcodePayFineThief, new OpPayFineThief); - interpreter.installSegment5 (Compiler::Misc::opcodeGoToJail, new OpGoToJail); - interpreter.installSegment5 (Compiler::Misc::opcodeGetLocked, new OpGetLocked); - interpreter.installSegment5 (Compiler::Misc::opcodeGetLockedExplicit, new OpGetLocked); - interpreter.installSegment5 (Compiler::Misc::opcodeGetEffect, new OpGetEffect); - interpreter.installSegment5 (Compiler::Misc::opcodeGetEffectExplicit, new OpGetEffect); - interpreter.installSegment5 (Compiler::Misc::opcodeAddSoulGem, new OpAddSoulGem); - interpreter.installSegment5 (Compiler::Misc::opcodeAddSoulGemExplicit, new OpAddSoulGem); - interpreter.installSegment3 (Compiler::Misc::opcodeRemoveSoulGem, new OpRemoveSoulGem); - interpreter.installSegment3 (Compiler::Misc::opcodeRemoveSoulGemExplicit, new OpRemoveSoulGem); - interpreter.installSegment5 (Compiler::Misc::opcodeDrop, new OpDrop); - interpreter.installSegment5 (Compiler::Misc::opcodeDropExplicit, new OpDrop); - interpreter.installSegment5 (Compiler::Misc::opcodeDropSoulGem, new OpDropSoulGem); - interpreter.installSegment5 (Compiler::Misc::opcodeDropSoulGemExplicit, new OpDropSoulGem); - interpreter.installSegment5 (Compiler::Misc::opcodeGetAttacked, new OpGetAttacked); - interpreter.installSegment5 (Compiler::Misc::opcodeGetAttackedExplicit, new OpGetAttacked); - interpreter.installSegment5 (Compiler::Misc::opcodeGetWeaponDrawn, new OpGetWeaponDrawn); - interpreter.installSegment5 (Compiler::Misc::opcodeGetWeaponDrawnExplicit, new OpGetWeaponDrawn); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellReadied, new OpGetSpellReadied); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellReadiedExplicit, new OpGetSpellReadied); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellEffects, new OpGetSpellEffects); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSpellEffectsExplicit, new OpGetSpellEffects); - interpreter.installSegment5 (Compiler::Misc::opcodeGetCurrentTime, new OpGetCurrentTime); - interpreter.installSegment5 (Compiler::Misc::opcodeSetDelete, new OpSetDelete); - interpreter.installSegment5 (Compiler::Misc::opcodeSetDeleteExplicit, new OpSetDelete); - interpreter.installSegment5 (Compiler::Misc::opcodeGetSquareRoot, new OpGetSquareRoot); - interpreter.installSegment5 (Compiler::Misc::opcodeFall, new OpFall); - interpreter.installSegment5 (Compiler::Misc::opcodeFallExplicit, new OpFall); - interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingPc, new OpGetStandingPc); - interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingPcExplicit, new OpGetStandingPc); - interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingActor, new OpGetStandingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingActorExplicit, new OpGetStandingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingPc, new OpGetCollidingPc); - interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingPcExplicit, new OpGetCollidingPc); - interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingActor, new OpGetCollidingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingActorExplicit, new OpGetCollidingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeHurtStandingActor, new OpHurtStandingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeHurtStandingActorExplicit, new OpHurtStandingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeHurtCollidingActor, new OpHurtCollidingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeHurtCollidingActorExplicit, new OpHurtCollidingActor); - interpreter.installSegment5 (Compiler::Misc::opcodeGetWindSpeed, new OpGetWindSpeed); - interpreter.installSegment5 (Compiler::Misc::opcodeHitOnMe, new OpHitOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeHitOnMeExplicit, new OpHitOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeHitAttemptOnMe, new OpHitAttemptOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeHitAttemptOnMeExplicit, new OpHitAttemptOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeDisableTeleporting, new OpEnableTeleporting); - interpreter.installSegment5 (Compiler::Misc::opcodeEnableTeleporting, new OpEnableTeleporting); - interpreter.installSegment5 (Compiler::Misc::opcodeShowVars, new OpShowVars); - interpreter.installSegment5 (Compiler::Misc::opcodeShowVarsExplicit, new OpShowVars); - interpreter.installSegment5 (Compiler::Misc::opcodeShow, new OpShow); - interpreter.installSegment5 (Compiler::Misc::opcodeShowExplicit, new OpShow); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleGodMode, new OpToggleGodMode); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleScripts, new OpToggleScripts); - interpreter.installSegment5 (Compiler::Misc::opcodeDisableLevitation, new OpEnableLevitation); - interpreter.installSegment5 (Compiler::Misc::opcodeEnableLevitation, new OpEnableLevitation); - interpreter.installSegment5 (Compiler::Misc::opcodeCast, new OpCast); - interpreter.installSegment5 (Compiler::Misc::opcodeCastExplicit, new OpCast); - interpreter.installSegment5 (Compiler::Misc::opcodeExplodeSpell, new OpExplodeSpell); - interpreter.installSegment5 (Compiler::Misc::opcodeExplodeSpellExplicit, new OpExplodeSpell); - interpreter.installSegment5 (Compiler::Misc::opcodeGetPcInJail, new OpGetPcInJail); - interpreter.installSegment5 (Compiler::Misc::opcodeGetPcTraveling, new OpGetPcTraveling); - interpreter.installSegment3 (Compiler::Misc::opcodeBetaComment, new OpBetaComment); - interpreter.installSegment3 (Compiler::Misc::opcodeBetaCommentExplicit, new OpBetaComment); - interpreter.installSegment5 (Compiler::Misc::opcodeAddToLevCreature, new OpAddToLevCreature); - interpreter.installSegment5 (Compiler::Misc::opcodeRemoveFromLevCreature, new OpRemoveFromLevCreature); - interpreter.installSegment5 (Compiler::Misc::opcodeAddToLevItem, new OpAddToLevItem); - interpreter.installSegment5 (Compiler::Misc::opcodeRemoveFromLevItem, new OpRemoveFromLevItem); - interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraph, new OpShowSceneGraph); - interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraphExplicit, new OpShowSceneGraph); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleBorders, new OpToggleBorders); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleNavMesh, new OpToggleNavMesh); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleActorsPaths, new OpToggleActorsPaths); - interpreter.installSegment5 (Compiler::Misc::opcodeSetNavMeshNumberToRender, new OpSetNavMeshNumberToRender); - interpreter.installSegment5 (Compiler::Misc::opcodeRepairedOnMe, new OpRepairedOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeRepairedOnMeExplicit, new OpRepairedOnMe); - interpreter.installSegment5 (Compiler::Misc::opcodeToggleRecastMesh, new OpToggleRecastMesh); + void installOpcodes(Interpreter::Interpreter& interpreter) + { + interpreter.installSegment5(Compiler::Misc::opcodeMenuMode); + interpreter.installSegment5(Compiler::Misc::opcodeRandom); + interpreter.installSegment5(Compiler::Misc::opcodeScriptRunning); + interpreter.installSegment5>(Compiler::Misc::opcodeStartScript); + interpreter.installSegment5>(Compiler::Misc::opcodeStartScriptExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeStopScript); + interpreter.installSegment5(Compiler::Misc::opcodeGetSecondsPassed); + interpreter.installSegment5>(Compiler::Misc::opcodeEnable); + interpreter.installSegment5>(Compiler::Misc::opcodeEnableExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeDisable); + interpreter.installSegment5>(Compiler::Misc::opcodeDisableExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetDisabled); + interpreter.installSegment5>(Compiler::Misc::opcodeGetDisabledExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeXBox); + interpreter.installSegment5>(Compiler::Misc::opcodeOnActivate); + interpreter.installSegment5>(Compiler::Misc::opcodeOnActivateExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeActivate); + interpreter.installSegment5>(Compiler::Misc::opcodeActivateExplicit); + interpreter.installSegment3>(Compiler::Misc::opcodeLock); + interpreter.installSegment3>(Compiler::Misc::opcodeLockExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeUnlock); + interpreter.installSegment5>(Compiler::Misc::opcodeUnlockExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeToggleCollisionDebug); + interpreter.installSegment5(Compiler::Misc::opcodeToggleCollisionBoxes); + interpreter.installSegment5(Compiler::Misc::opcodeToggleWireframe); + interpreter.installSegment5(Compiler::Misc::opcodeFadeIn); + interpreter.installSegment5(Compiler::Misc::opcodeFadeOut); + interpreter.installSegment5(Compiler::Misc::opcodeFadeTo); + interpreter.installSegment5(Compiler::Misc::opcodeTogglePathgrid); + interpreter.installSegment5(Compiler::Misc::opcodeToggleWater); + interpreter.installSegment5(Compiler::Misc::opcodeToggleWorld); + interpreter.installSegment5(Compiler::Misc::opcodeDontSaveObject); + interpreter.installSegment5(Compiler::Misc::opcodePcForce1stPerson); + interpreter.installSegment5(Compiler::Misc::opcodePcForce3rdPerson); + interpreter.installSegment5(Compiler::Misc::opcodePcGet3rdPerson); + interpreter.installSegment5(Compiler::Misc::opcodeToggleVanityMode); + interpreter.installSegment5(Compiler::Misc::opcodeGetPcSleep); + interpreter.installSegment5(Compiler::Misc::opcodeGetPcJumping); + interpreter.installSegment5(Compiler::Misc::opcodeWakeUpPc); + interpreter.installSegment5(Compiler::Misc::opcodePlayBink); + interpreter.installSegment5(Compiler::Misc::opcodePayFine); + interpreter.installSegment5(Compiler::Misc::opcodePayFineThief); + interpreter.installSegment5(Compiler::Misc::opcodeGoToJail); + interpreter.installSegment5>(Compiler::Misc::opcodeGetLocked); + interpreter.installSegment5>(Compiler::Misc::opcodeGetLockedExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetEffect); + interpreter.installSegment5>(Compiler::Misc::opcodeGetEffectExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeAddSoulGem); + interpreter.installSegment5>(Compiler::Misc::opcodeAddSoulGemExplicit); + interpreter.installSegment3>(Compiler::Misc::opcodeRemoveSoulGem); + interpreter.installSegment3>(Compiler::Misc::opcodeRemoveSoulGemExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeDrop); + interpreter.installSegment5>(Compiler::Misc::opcodeDropExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeDropSoulGem); + interpreter.installSegment5>(Compiler::Misc::opcodeDropSoulGemExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetAttacked); + interpreter.installSegment5>(Compiler::Misc::opcodeGetAttackedExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetWeaponDrawn); + interpreter.installSegment5>(Compiler::Misc::opcodeGetWeaponDrawnExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetSpellReadied); + interpreter.installSegment5>(Compiler::Misc::opcodeGetSpellReadiedExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetSpellEffects); + interpreter.installSegment5>(Compiler::Misc::opcodeGetSpellEffectsExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeGetCurrentTime); + interpreter.installSegment5>(Compiler::Misc::opcodeSetDelete); + interpreter.installSegment5>(Compiler::Misc::opcodeSetDeleteExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeGetSquareRoot); + interpreter.installSegment5>(Compiler::Misc::opcodeFall); + interpreter.installSegment5>(Compiler::Misc::opcodeFallExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetStandingPc); + interpreter.installSegment5>(Compiler::Misc::opcodeGetStandingPcExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetStandingActor); + interpreter.installSegment5>( + Compiler::Misc::opcodeGetStandingActorExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetCollidingPc); + interpreter.installSegment5>(Compiler::Misc::opcodeGetCollidingPcExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeGetCollidingActor); + interpreter.installSegment5>( + Compiler::Misc::opcodeGetCollidingActorExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeHurtStandingActor); + interpreter.installSegment5>( + Compiler::Misc::opcodeHurtStandingActorExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeHurtCollidingActor); + interpreter.installSegment5>( + Compiler::Misc::opcodeHurtCollidingActorExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeGetWindSpeed); + interpreter.installSegment5>(Compiler::Misc::opcodeHitOnMe); + interpreter.installSegment5>(Compiler::Misc::opcodeHitOnMeExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeHitAttemptOnMe); + interpreter.installSegment5>(Compiler::Misc::opcodeHitAttemptOnMeExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeDisableTeleporting); + interpreter.installSegment5>(Compiler::Misc::opcodeEnableTeleporting); + interpreter.installSegment5>(Compiler::Misc::opcodeShowVars); + interpreter.installSegment5>(Compiler::Misc::opcodeShowVarsExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeShow); + interpreter.installSegment5>(Compiler::Misc::opcodeShowExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeToggleGodMode); + interpreter.installSegment5(Compiler::Misc::opcodeToggleScripts); + interpreter.installSegment5>(Compiler::Misc::opcodeDisableLevitation); + interpreter.installSegment5>(Compiler::Misc::opcodeEnableLevitation); + interpreter.installSegment5>(Compiler::Misc::opcodeCast); + interpreter.installSegment5>(Compiler::Misc::opcodeCastExplicit); + interpreter.installSegment5>(Compiler::Misc::opcodeExplodeSpell); + interpreter.installSegment5>(Compiler::Misc::opcodeExplodeSpellExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeGetPcInJail); + interpreter.installSegment5(Compiler::Misc::opcodeGetPcTraveling); + interpreter.installSegment3>(Compiler::Misc::opcodeBetaComment); + interpreter.installSegment3>(Compiler::Misc::opcodeBetaCommentExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeAddToLevCreature); + interpreter.installSegment5(Compiler::Misc::opcodeRemoveFromLevCreature); + interpreter.installSegment5(Compiler::Misc::opcodeAddToLevItem); + interpreter.installSegment5(Compiler::Misc::opcodeRemoveFromLevItem); + interpreter.installSegment3>(Compiler::Misc::opcodeShowSceneGraph); + interpreter.installSegment3>(Compiler::Misc::opcodeShowSceneGraphExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeToggleBorders); + interpreter.installSegment5(Compiler::Misc::opcodeToggleNavMesh); + interpreter.installSegment5(Compiler::Misc::opcodeToggleActorsPaths); + interpreter.installSegment5(Compiler::Misc::opcodeSetNavMeshNumberToRender); + interpreter.installSegment5>(Compiler::Misc::opcodeRepairedOnMe); + interpreter.installSegment5>(Compiler::Misc::opcodeRepairedOnMeExplicit); + interpreter.installSegment5(Compiler::Misc::opcodeToggleRecastMesh); + interpreter.installSegment5(Compiler::Misc::opcodeHelp); + interpreter.installSegment5(Compiler::Misc::opcodeReloadLua); + interpreter.installSegment5(Compiler::Misc::opcodeTestModels); } } } diff --git a/apps/openmw/mwscript/miscextensions.hpp b/apps/openmw/mwscript/miscextensions.hpp index 16ed9301ed9..0dbdd7ba50e 100644 --- a/apps/openmw/mwscript/miscextensions.hpp +++ b/apps/openmw/mwscript/miscextensions.hpp @@ -14,11 +14,9 @@ namespace Interpreter namespace MWScript { namespace Misc - { - void installOpcodes (Interpreter::Interpreter& interpreter); + { + void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif - - diff --git a/apps/openmw/mwscript/ref.cpp b/apps/openmw/mwscript/ref.cpp index 6347c2c2e5b..af6b205d374 100644 --- a/apps/openmw/mwscript/ref.cpp +++ b/apps/openmw/mwscript/ref.cpp @@ -7,10 +7,9 @@ #include "interpretercontext.hpp" -MWWorld::Ptr MWScript::ExplicitRef::operator() (Interpreter::Runtime& runtime, bool required, - bool activeOnly) const +MWWorld::Ptr MWScript::ExplicitRef::operator()(Interpreter::Runtime& runtime, bool required, bool activeOnly) const { - std::string id = runtime.getStringLiteral(runtime[0].mInteger); + ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); if (required) @@ -19,11 +18,9 @@ MWWorld::Ptr MWScript::ExplicitRef::operator() (Interpreter::Runtime& runtime, b return MWBase::Environment::get().getWorld()->searchPtr(id, activeOnly); } -MWWorld::Ptr MWScript::ImplicitRef::operator() (Interpreter::Runtime& runtime, bool required, - bool activeOnly) const +MWWorld::Ptr MWScript::ImplicitRef::operator()(Interpreter::Runtime& runtime, bool required, bool activeOnly) const { - MWScript::InterpreterContext& context - = static_cast (runtime.getContext()); + MWScript::InterpreterContext& context = static_cast(runtime.getContext()); return context.getReference(required); } diff --git a/apps/openmw/mwscript/ref.hpp b/apps/openmw/mwscript/ref.hpp index c52b419c1d5..166435b2c87 100644 --- a/apps/openmw/mwscript/ref.hpp +++ b/apps/openmw/mwscript/ref.hpp @@ -1,8 +1,6 @@ #ifndef GAME_MWSCRIPT_REF_H #define GAME_MWSCRIPT_REF_H -#include - #include "../mwworld/ptr.hpp" namespace Interpreter @@ -16,16 +14,14 @@ namespace MWScript { static constexpr bool implicit = false; - MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required = true, - bool activeOnly = false) const; + MWWorld::Ptr operator()(Interpreter::Runtime& runtime, bool required = true, bool activeOnly = false) const; }; struct ImplicitRef { static constexpr bool implicit = true; - MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required = true, - bool activeOnly = false) const; + MWWorld::Ptr operator()(Interpreter::Runtime& runtime, bool required = true, bool activeOnly = false) const; }; } diff --git a/apps/openmw/mwscript/scriptmanagerimp.cpp b/apps/openmw/mwscript/scriptmanagerimp.cpp index e1652b311cb..82f234bf765 100644 --- a/apps/openmw/mwscript/scriptmanagerimp.cpp +++ b/apps/openmw/mwscript/scriptmanagerimp.cpp @@ -1,20 +1,21 @@ #include "scriptmanagerimp.hpp" +#include #include -#include #include -#include +#include #include -#include +#include +#include -#include +#include -#include #include #include #include +#include #include "../mwworld/esmstore.hpp" @@ -23,39 +24,40 @@ namespace MWScript { - ScriptManager::ScriptManager (const MWWorld::ESMStore& store, - Compiler::Context& compilerContext, int warningsMode, - const std::vector& scriptBlacklist) - : mErrorHandler(), mStore (store), - mCompilerContext (compilerContext), mParser (mErrorHandler, mCompilerContext), - mOpcodesInstalled (false), mGlobalScripts (store) + ScriptManager::ScriptManager(const MWWorld::ESMStore& store, Compiler::Context& compilerContext, int warningsMode, + const std::vector& scriptBlacklist) + : mErrorHandler() + , mStore(store) + , mCompilerContext(compilerContext) + , mParser(mErrorHandler, mCompilerContext) + , mGlobalScripts(store) { - mErrorHandler.setWarningsMode (warningsMode); + installOpcodes(mInterpreter); + + mErrorHandler.setWarningsMode(warningsMode); - mScriptBlacklist.resize (scriptBlacklist.size()); + mScriptBlacklist.resize(scriptBlacklist.size()); - std::transform (scriptBlacklist.begin(), scriptBlacklist.end(), - mScriptBlacklist.begin(), Misc::StringUtils::lowerCase); - std::sort (mScriptBlacklist.begin(), mScriptBlacklist.end()); + std::sort(mScriptBlacklist.begin(), mScriptBlacklist.end()); } - bool ScriptManager::compile (const std::string& name) + bool ScriptManager::compile(const ESM::RefId& name) { mParser.reset(); mErrorHandler.reset(); - if (const ESM::Script *script = mStore.get().find (name)) + if (const ESM::Script* script = mStore.get().find(name)) { - mErrorHandler.setContext(name); + mErrorHandler.setContext(script->mId.getRefIdString()); bool Success = true; try { - std::istringstream input (script->mScriptText); + std::istringstream input(script->mScriptText); - Compiler::Scanner scanner (mErrorHandler, input, mCompilerContext.getExtensions()); + Compiler::Scanner scanner(mErrorHandler, input, mCompilerContext.getExtensions()); - scanner.scan (mParser); + scanner.scan(mParser); if (!mErrorHandler.isGood()) Success = false; @@ -78,9 +80,7 @@ namespace MWScript if (Success) { - std::vector code; - mParser.getCode(code); - mScripts.emplace(name, CompiledScript(code, mParser.getLocals())); + mScripts.emplace(name, CompiledScript(mParser.getProgram(), mParser.getLocals())); return true; } @@ -89,48 +89,45 @@ namespace MWScript return false; } - bool ScriptManager::run (const std::string& name, Interpreter::Context& interpreterContext) + bool ScriptManager::run(const ESM::RefId& name, Interpreter::Context& interpreterContext) { // compile script - ScriptCollection::iterator iter = mScripts.find (name); + auto iter = mScripts.find(name); - if (iter==mScripts.end()) + if (iter == mScripts.end()) { - if (!compile (name)) + if (!compile(name)) { // failed -> ignore script from now on. - std::vector empty; - mScripts.emplace(name, CompiledScript(empty, Compiler::Locals())); + mScripts.emplace(name, CompiledScript({}, Compiler::Locals())); return false; } - iter = mScripts.find (name); - assert (iter!=mScripts.end()); + iter = mScripts.find(name); + assert(iter != mScripts.end()); } // execute script - if (!iter->second.mByteCode.empty() && iter->second.mActive) + const auto& target = interpreterContext.getTarget(); + if (!iter->second.mProgram.mInstructions.empty() + && iter->second.mInactive.find(target) == iter->second.mInactive.end()) + { try { - if (!mOpcodesInstalled) - { - installOpcodes (mInterpreter); - mOpcodesInstalled = true; - } - - mInterpreter.run (&iter->second.mByteCode[0], iter->second.mByteCode.size(), interpreterContext); + mInterpreter.run(iter->second.mProgram, interpreterContext); return true; } catch (const MissingImplicitRefError& e) { - Log(Debug::Error) << "Execution of script " << name << " failed: " << e.what(); + Log(Debug::Error) << "Execution of script " << name << " failed: " << e.what(); } catch (const std::exception& e) { - Log(Debug::Error) << "Execution of script " << name << " failed: " << e.what(); + Log(Debug::Error) << "Execution of script " << name << " failed: " << e.what(); - iter->second.mActive = false; // don't execute again. + iter->second.mInactive.insert(target); // don't execute again. } + } return false; } @@ -138,7 +135,7 @@ namespace MWScript { for (auto& script : mScripts) { - script.second.mActive = true; + script.second.mInactive.clear(); } mGlobalScripts.clear(); @@ -151,8 +148,7 @@ namespace MWScript for (auto& script : mStore.get()) { - if (!std::binary_search (mScriptBlacklist.begin(), mScriptBlacklist.end(), - Misc::StringUtils::lowerCase(script.mId))) + if (!std::binary_search(mScriptBlacklist.begin(), mScriptBlacklist.end(), script.mId)) { ++count; @@ -161,49 +157,64 @@ namespace MWScript } } - return std::make_pair (count, success); + return std::make_pair(count, success); } - const Compiler::Locals& ScriptManager::getLocals (const std::string& name) + const Compiler::Locals& ScriptManager::getLocals(const ESM::RefId& name) { - std::string name2 = Misc::StringUtils::lowerCase (name); - { - ScriptCollection::iterator iter = mScripts.find (name2); + auto iter = mScripts.find(name); - if (iter!=mScripts.end()) + if (iter != mScripts.end()) return iter->second.mLocals; } { - std::map::iterator iter = mOtherLocals.find (name2); + auto iter = mOtherLocals.find(name); - if (iter!=mOtherLocals.end()) + if (iter != mOtherLocals.end()) return iter->second; } - if (const ESM::Script *script = mStore.get().search (name2)) + if (const ESM::Script* script = mStore.get().search(name)) { Compiler::Locals locals; - const Compiler::ContextOverride override(mErrorHandler, name2 + "[local variables]"); + const Compiler::ContextOverride override(mErrorHandler, name.getRefIdString() + "[local variables]"); - std::istringstream stream (script->mScriptText); - Compiler::QuickFileParser parser (mErrorHandler, mCompilerContext, locals); - Compiler::Scanner scanner (mErrorHandler, stream, mCompilerContext.getExtensions()); - scanner.scan (parser); + std::istringstream stream(script->mScriptText); + Compiler::QuickFileParser parser(mErrorHandler, mCompilerContext, locals); + Compiler::Scanner scanner(mErrorHandler, stream, mCompilerContext.getExtensions()); + try + { + scanner.scan(parser); + } + catch (const Compiler::SourceException&) + { + // error has already been reported via error handler + locals.clear(); + } + catch (const std::exception& error) + { + Log(Debug::Error) << "Error: An exception has been thrown: " << error.what(); + locals.clear(); + } - std::map::iterator iter = - mOtherLocals.emplace(name2, locals).first; + auto iter = mOtherLocals.emplace(name, locals).first; return iter->second; } - throw std::logic_error ("script " + name + " does not exist"); + throw std::logic_error("script " + name.toDebugString() + " does not exist"); } GlobalScripts& ScriptManager::getGlobalScripts() { return mGlobalScripts; } + + const Compiler::Extensions& ScriptManager::getExtensions() const + { + return *mCompilerContext.getExtensions(); + } } diff --git a/apps/openmw/mwscript/scriptmanagerimp.hpp b/apps/openmw/mwscript/scriptmanagerimp.hpp index 7ddcd2489df..326ded7bc65 100644 --- a/apps/openmw/mwscript/scriptmanagerimp.hpp +++ b/apps/openmw/mwscript/scriptmanagerimp.hpp @@ -2,14 +2,17 @@ #define GAME_SCRIPT_SCRIPTMANAGER_H #include +#include #include -#include #include +#include #include #include +#include + #include "../mwbase/scriptmanager.hpp" #include "globalscripts.hpp" @@ -34,57 +37,53 @@ namespace MWScript { class ScriptManager : public MWBase::ScriptManager { - Compiler::StreamErrorHandler mErrorHandler; - const MWWorld::ESMStore& mStore; - Compiler::Context& mCompilerContext; - Compiler::FileParser mParser; - Interpreter::Interpreter mInterpreter; - bool mOpcodesInstalled; - - struct CompiledScript + Compiler::StreamErrorHandler mErrorHandler; + const MWWorld::ESMStore& mStore; + Compiler::Context& mCompilerContext; + Compiler::FileParser mParser; + Interpreter::Interpreter mInterpreter; + + struct CompiledScript + { + Interpreter::Program mProgram; + Compiler::Locals mLocals; + std::set mInactive; + + explicit CompiledScript(Interpreter::Program&& program, const Compiler::Locals& locals) + : mProgram(std::move(program)) + , mLocals(locals) { - std::vector mByteCode; - Compiler::Locals mLocals; - bool mActive; - - CompiledScript(const std::vector& code, const Compiler::Locals& locals) - { - mByteCode = code; - mLocals = locals; - mActive = true; - } - }; - - typedef std::map ScriptCollection; + } + }; - ScriptCollection mScripts; - GlobalScripts mGlobalScripts; - std::map mOtherLocals; - std::vector mScriptBlacklist; + std::unordered_map mScripts; + GlobalScripts mGlobalScripts; + std::unordered_map mOtherLocals; + std::vector mScriptBlacklist; - public: + public: + ScriptManager(const MWWorld::ESMStore& store, Compiler::Context& compilerContext, int warningsMode, + const std::vector& scriptBlacklist); - ScriptManager (const MWWorld::ESMStore& store, - Compiler::Context& compilerContext, int warningsMode, - const std::vector& scriptBlacklist); + void clear() override; - void clear() override; + bool run(const ESM::RefId& name, Interpreter::Context& interpreterContext) override; + ///< Run the script with the given name (compile first, if not compiled yet) - bool run (const std::string& name, Interpreter::Context& interpreterContext) override; - ///< Run the script with the given name (compile first, if not compiled yet) + bool compile(const ESM::RefId& name) override; + ///< Compile script with the given namen + /// \return Success? - bool compile (const std::string& name) override; - ///< Compile script with the given namen - /// \return Success? + std::pair compileAll() override; + ///< Compile all scripts + /// \return count, success - std::pair compileAll() override; - ///< Compile all scripts - /// \return count, success + const Compiler::Locals& getLocals(const ESM::RefId& name) override; + ///< Return locals for script \a name. - const Compiler::Locals& getLocals (const std::string& name) override; - ///< Return locals for script \a name. + GlobalScripts& getGlobalScripts() override; - GlobalScripts& getGlobalScripts() override; + const Compiler::Extensions& getExtensions() const override; }; } diff --git a/apps/openmw/mwscript/skyextensions.cpp b/apps/openmw/mwscript/skyextensions.cpp index 2b6bf826f9c..d2b41fb87ad 100644 --- a/apps/openmw/mwscript/skyextensions.cpp +++ b/apps/openmw/mwscript/skyextensions.cpp @@ -4,14 +4,16 @@ #include +#include +#include #include -#include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" -#include "interpretercontext.hpp" +#include "../mwworld/esmstore.hpp" namespace MWScript { @@ -19,115 +21,110 @@ namespace MWScript { class OpToggleSky : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - bool enabled = MWBase::Environment::get().getWorld()->toggleSky(); + public: + void execute(Interpreter::Runtime& runtime) override + { + bool enabled = MWBase::Environment::get().getWorld()->toggleSky(); - runtime.getContext().report (enabled ? "Sky -> On" : "Sky -> Off"); - } + runtime.getContext().report(enabled ? "Sky -> On" : "Sky -> Off"); + } }; class OpTurnMoonWhite : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWBase::Environment::get().getWorld()->setMoonColour (false); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWBase::Environment::get().getWorld()->setMoonColour(false); + } }; class OpTurnMoonRed : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWBase::Environment::get().getWorld()->setMoonColour (true); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWBase::Environment::get().getWorld()->setMoonColour(true); + } }; class OpGetMasserPhase : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.push (MWBase::Environment::get().getWorld()->getMasserPhase()); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.push(MWBase::Environment::get().getWorld()->getMasserPhase()); + } }; class OpGetSecundaPhase : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.push (MWBase::Environment::get().getWorld()->getSecundaPhase()); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.push(MWBase::Environment::get().getWorld()->getSecundaPhase()); + } }; class OpGetCurrentWeather : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.push (MWBase::Environment::get().getWorld()->getCurrentWeather()); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + runtime.push(MWBase::Environment::get().getWorld()->getCurrentWeather()); + } }; class OpChangeWeather : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + ESM::RefId region = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override - { - std::string region = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - - Interpreter::Type_Integer id = runtime[0].mInteger; - runtime.pop(); + Interpreter::Type_Integer id = runtime[0].mInteger; + runtime.pop(); + const ESM::Region* reg = MWBase::Environment::get().getESMStore()->get().search(region); + if (reg) MWBase::Environment::get().getWorld()->changeWeather(region, id); - } + else + runtime.getContext().report("Warning: Region \"" + region.getRefIdString() + "\" was not found"); + } }; class OpModRegion : public Interpreter::Opcode1 { - public: - - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + std::string_view region{ runtime.getStringLiteral(runtime[0].mInteger) }; + runtime.pop(); + + std::vector chances; + chances.reserve(10); + while (arg0 > 0) { - std::string region = runtime.getStringLiteral (runtime[0].mInteger); + chances.push_back(std::clamp(runtime[0].mInteger, 0, 100)); runtime.pop(); - - std::vector chances; - chances.reserve(10); - while(arg0 > 0) - { - chances.push_back(std::max(0, std::min(127, runtime[0].mInteger))); - runtime.pop(); - arg0--; - } - - MWBase::Environment::get().getWorld()->modRegion(region, chances); + arg0--; } - }; + MWBase::Environment::get().getWorld()->modRegion(ESM::RefId::stringRefId(region), chances); + } + }; - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Sky::opcodeToggleSky, new OpToggleSky); - interpreter.installSegment5 (Compiler::Sky::opcodeTurnMoonWhite, new OpTurnMoonWhite); - interpreter.installSegment5 (Compiler::Sky::opcodeTurnMoonRed, new OpTurnMoonRed); - interpreter.installSegment5 (Compiler::Sky::opcodeGetMasserPhase, new OpGetMasserPhase); - interpreter.installSegment5 (Compiler::Sky::opcodeGetSecundaPhase, new OpGetSecundaPhase); - interpreter.installSegment5 (Compiler::Sky::opcodeGetCurrentWeather, new OpGetCurrentWeather); - interpreter.installSegment5 (Compiler::Sky::opcodeChangeWeather, new OpChangeWeather); - interpreter.installSegment3 (Compiler::Sky::opcodeModRegion, new OpModRegion); + interpreter.installSegment5(Compiler::Sky::opcodeToggleSky); + interpreter.installSegment5(Compiler::Sky::opcodeTurnMoonWhite); + interpreter.installSegment5(Compiler::Sky::opcodeTurnMoonRed); + interpreter.installSegment5(Compiler::Sky::opcodeGetMasserPhase); + interpreter.installSegment5(Compiler::Sky::opcodeGetSecundaPhase); + interpreter.installSegment5(Compiler::Sky::opcodeGetCurrentWeather); + interpreter.installSegment5(Compiler::Sky::opcodeChangeWeather); + interpreter.installSegment3(Compiler::Sky::opcodeModRegion); } } } diff --git a/apps/openmw/mwscript/skyextensions.hpp b/apps/openmw/mwscript/skyextensions.hpp index 003f2fb190b..f49aa04c957 100644 --- a/apps/openmw/mwscript/skyextensions.hpp +++ b/apps/openmw/mwscript/skyextensions.hpp @@ -15,8 +15,8 @@ namespace MWScript { /// \brief sky-related script functionality namespace Sky - { - void installOpcodes (Interpreter::Interpreter& interpreter); + { + void installOpcodes(Interpreter::Interpreter& interpreter); } } diff --git a/apps/openmw/mwscript/soundextensions.cpp b/apps/openmw/mwscript/soundextensions.cpp index 6eab758f197..c248c30520e 100644 --- a/apps/openmw/mwscript/soundextensions.cpp +++ b/apps/openmw/mwscript/soundextensions.cpp @@ -3,16 +3,17 @@ #include #include -#include #include +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" -#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/inventorystore.hpp" #include "interpretercontext.hpp" #include "ref.hpp" @@ -21,218 +22,196 @@ namespace MWScript { namespace Sound { - template + template class OpSay : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - MWScript::InterpreterContext& context - = static_cast (runtime.getContext()); + MWScript::InterpreterContext& context + = static_cast(runtime.getContext()); - std::string file = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + VFS::Path::Normalized file{ runtime.getStringLiteral(runtime[0].mInteger) }; + runtime.pop(); - std::string text = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + std::string_view text = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); - MWBase::Environment::get().getSoundManager()->say (ptr, file); + MWBase::Environment::get().getSoundManager()->say(ptr, Misc::ResourceHelpers::correctSoundPath(file)); - if (MWBase::Environment::get().getWindowManager ()->getSubtitlesEnabled()) - context.messageBox (text); - } + if (Settings::gui().mSubtitles) + context.messageBox(text); + } }; - template + template class OpSayDone : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - runtime.push (MWBase::Environment::get().getSoundManager()->sayDone (ptr)); - } + runtime.push(MWBase::Environment::get().getSoundManager()->sayDone(ptr)); + } }; class OpStreamMusic : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - - MWBase::Environment::get().getSoundManager()->streamMusic (sound); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + const VFS::Path::Normalized music(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + + MWBase::Environment::get().getSoundManager()->streamMusic( + Misc::ResourceHelpers::correctMusicPath(music), MWSound::MusicType::MWScript); + } }; class OpPlaySound : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - - MWBase::Environment::get().getSoundManager()->playSound(sound, 1.0, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + ESM::RefId sound = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + + MWBase::Environment::get().getSoundManager()->playSound( + sound, 1.0, 1.0, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); + } }; class OpPlaySoundVP : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + ESM::RefId sound = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - Interpreter::Type_Float volume = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float volume = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float pitch = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float pitch = runtime[0].mFloat; + runtime.pop(); - MWBase::Environment::get().getSoundManager()->playSound(sound, volume, pitch, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); - } + MWBase::Environment::get().getSoundManager()->playSound( + sound, volume, pitch, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); + } }; - template + template class OpPlaySound3D : public Interpreter::Opcode0 { - bool mLoop; - - public: - - OpPlaySound3D (bool loop) : mLoop (loop) {} + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId sound = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, 1.0, 1.0, - MWSound::Type::Sfx, - mLoop ? MWSound::PlayMode::LoopRemoveAtDistance - : MWSound::PlayMode::Normal); - } + MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, 1.0, 1.0, MWSound::Type::Sfx, + TLoop ? MWSound::PlayMode::LoopRemoveAtDistance : MWSound::PlayMode::Normal); + } }; - template + template class OpPlaySoundVP3D : public Interpreter::Opcode0 { - bool mLoop; - - public: - - OpPlaySoundVP3D (bool loop) : mLoop (loop) {} - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Float volume = runtime[0].mFloat; - runtime.pop(); + ESM::RefId sound = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - Interpreter::Type_Float pitch = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float volume = runtime[0].mFloat; + runtime.pop(); - MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, volume, pitch, - MWSound::Type::Sfx, - mLoop ? MWSound::PlayMode::LoopRemoveAtDistance - : MWSound::PlayMode::Normal); + Interpreter::Type_Float pitch = runtime[0].mFloat; + runtime.pop(); - } + MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, volume, pitch, MWSound::Type::Sfx, + TLoop ? MWSound::PlayMode::LoopRemoveAtDistance : MWSound::PlayMode::Normal); + } }; - template + template class OpStopSound : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - std::string sound = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId sound = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - MWBase::Environment::get().getSoundManager()->stopSound3D (ptr, sound); - } + MWBase::Environment::get().getSoundManager()->stopSound3D(ptr, sound); + } }; - template + template class OpGetSoundPlaying : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + int index = runtime[0].mInteger; + runtime.pop(); - int index = runtime[0].mInteger; - runtime.pop(); + bool ret = MWBase::Environment::get().getSoundManager()->getSoundPlaying( + ptr, ESM::RefId::stringRefId(runtime.getStringLiteral(index))); - bool ret = MWBase::Environment::get().getSoundManager()->getSoundPlaying ( - ptr, runtime.getStringLiteral (index)); + // GetSoundPlaying called on an equipped item should also look for sounds played by the equipping actor. + if (!ret && ptr.getContainerStore()) + { + MWWorld::Ptr cont = MWBase::Environment::get().getWorld()->findContainer(ptr); - // GetSoundPlaying called on an equipped item should also look for sounds played by the equipping actor. - if (!ret && ptr.getContainerStore()) + if (!cont.isEmpty() && cont.getClass().hasInventoryStore(cont) + && cont.getClass().getInventoryStore(cont).isEquipped(ptr)) { - MWWorld::Ptr cont = MWBase::Environment::get().getWorld()->findContainer(ptr); - - if (!cont.isEmpty() && cont.getClass().hasInventoryStore(cont) && cont.getClass().getInventoryStore(cont).isEquipped(ptr)) - { - ret = MWBase::Environment::get().getSoundManager()->getSoundPlaying ( - cont, runtime.getStringLiteral (index)); - } + ret = MWBase::Environment::get().getSoundManager()->getSoundPlaying( + cont, ESM::RefId::stringRefId(runtime.getStringLiteral(index))); } - - runtime.push(ret); } - }; + runtime.push(ret); + } + }; - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::Sound::opcodeSay, new OpSay); - interpreter.installSegment5 (Compiler::Sound::opcodeSayDone, new OpSayDone); - interpreter.installSegment5 (Compiler::Sound::opcodeStreamMusic, new OpStreamMusic); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySound, new OpPlaySound); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySoundVP, new OpPlaySoundVP); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3D, new OpPlaySound3D (false)); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3DVP, new OpPlaySoundVP3D (false)); - interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3D, new OpPlaySound3D (true)); - interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3DVP, - new OpPlaySoundVP3D (true)); - interpreter.installSegment5 (Compiler::Sound::opcodeStopSound, new OpStopSound); - interpreter.installSegment5 (Compiler::Sound::opcodeGetSoundPlaying, new OpGetSoundPlaying); - - interpreter.installSegment5 (Compiler::Sound::opcodeSayExplicit, new OpSay); - interpreter.installSegment5 (Compiler::Sound::opcodeSayDoneExplicit, new OpSayDone); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3DExplicit, - new OpPlaySound3D (false)); - interpreter.installSegment5 (Compiler::Sound::opcodePlaySound3DVPExplicit, - new OpPlaySoundVP3D (false)); - interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3DExplicit, - new OpPlaySound3D (true)); - interpreter.installSegment5 (Compiler::Sound::opcodePlayLoopSound3DVPExplicit, - new OpPlaySoundVP3D (true)); - interpreter.installSegment5 (Compiler::Sound::opcodeStopSoundExplicit, new OpStopSound); - interpreter.installSegment5 (Compiler::Sound::opcodeGetSoundPlayingExplicit, - new OpGetSoundPlaying); + interpreter.installSegment5>(Compiler::Sound::opcodeSay); + interpreter.installSegment5>(Compiler::Sound::opcodeSayDone); + interpreter.installSegment5(Compiler::Sound::opcodeStreamMusic); + interpreter.installSegment5(Compiler::Sound::opcodePlaySound); + interpreter.installSegment5(Compiler::Sound::opcodePlaySoundVP); + interpreter.installSegment5>(Compiler::Sound::opcodePlaySound3D); + interpreter.installSegment5>(Compiler::Sound::opcodePlaySound3DVP); + interpreter.installSegment5>(Compiler::Sound::opcodePlayLoopSound3D); + interpreter.installSegment5>(Compiler::Sound::opcodePlayLoopSound3DVP); + interpreter.installSegment5>(Compiler::Sound::opcodeStopSound); + interpreter.installSegment5>(Compiler::Sound::opcodeGetSoundPlaying); + + interpreter.installSegment5>(Compiler::Sound::opcodeSayExplicit); + interpreter.installSegment5>(Compiler::Sound::opcodeSayDoneExplicit); + interpreter.installSegment5>(Compiler::Sound::opcodePlaySound3DExplicit); + interpreter.installSegment5>( + Compiler::Sound::opcodePlaySound3DVPExplicit); + interpreter.installSegment5>( + Compiler::Sound::opcodePlayLoopSound3DExplicit); + interpreter.installSegment5>( + Compiler::Sound::opcodePlayLoopSound3DVPExplicit); + interpreter.installSegment5>(Compiler::Sound::opcodeStopSoundExplicit); + interpreter.installSegment5>(Compiler::Sound::opcodeGetSoundPlayingExplicit); } } } diff --git a/apps/openmw/mwscript/soundextensions.hpp b/apps/openmw/mwscript/soundextensions.hpp index b92d7ea1bbd..a4aeacad64b 100644 --- a/apps/openmw/mwscript/soundextensions.hpp +++ b/apps/openmw/mwscript/soundextensions.hpp @@ -15,10 +15,9 @@ namespace MWScript { namespace Sound { - // Script-extensions related to sound - void installOpcodes (Interpreter::Interpreter& interpreter); + // Script-extensions related to sound + void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif - diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index ab3287c9a4a..064e90f1141 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -2,16 +2,19 @@ #include -#include +#include +#include #include "../mwworld/esmstore.hpp" -#include #include #include +#include #include -#include #include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -22,1207 +25,1232 @@ #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" -#include "../mwmechanics/actorutil.hpp" -#include "../mwmechanics/spellcasting.hpp" #include "ref.hpp" namespace { - std::string getDialogueActorFaction(MWWorld::ConstPtr actor) + ESM::RefId getDialogueActorFaction(const MWWorld::ConstPtr& actor) { - std::string factionId = actor.getClass().getPrimaryFaction(actor); + ESM::RefId factionId = actor.getClass().getPrimaryFaction(actor); if (factionId.empty()) - throw std::runtime_error ( - "failed to determine dialogue actors faction (because actor is factionless)"); + throw std::runtime_error("failed to determine dialogue actors faction (because actor is factionless)"); return factionId; } + + void modStat(MWMechanics::AttributeValue& stat, float amount) + { + const float base = stat.getBase(); + const float modifier = stat.getModifier() - stat.getDamage(); + const float modified = base + modifier; + // Clamp to 100 unless base < 100 and we have a fortification going + if ((modifier <= 0.f || base >= 100.f) && amount > 0.f) + amount = std::clamp(100.f - modified, 0.f, amount); + // Clamp the modified value in a way that doesn't properly account for negative numbers + float newModified = modified + amount; + if (newModified < 0.f) + { + if (modified >= 0.f) + newModified = 0.f; + else if (newModified < modified) + newModified = modified; + } + // Calculate damage/fortification based on the clamped base value + stat.setBase(std::clamp(base + amount, 0.f, 100.f), true); + stat.setModifier(newModified - stat.getBase()); + } + + template + void updateBaseRecord(MWWorld::Ptr& ptr) + { + const auto& store = *MWBase::Environment::get().getESMStore(); + const T* base = store.get().find(ptr.getCellRef().getRefId()); + ptr.get()->mBase = base; + } } namespace MWScript { namespace Stats { - template + template class OpGetLevel : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = - ptr.getClass() - .getCreatureStats (ptr) - .getLevel(); + Interpreter::Type_Integer value = -1; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getLevel(); - runtime.push (value); - } + runtime.push(value); + } }; - template + template class OpSetLevel : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = runtime[0].mInteger; - runtime.pop(); + Interpreter::Type_Integer value = runtime[0].mInteger; + runtime.pop(); - ptr.getClass() - .getCreatureStats (ptr) - .setLevel(value); - } + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).setLevel(value); + } }; - template + template class OpGetAttribute : public Interpreter::Opcode0 { - int mIndex; - - public: + ESM::RefId mIndex; - OpGetAttribute (int index) : mIndex (index) {} + public: + OpGetAttribute(ESM::RefId index) + : mIndex(index) + { + } - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Float value = - ptr.getClass() - .getCreatureStats (ptr) - .getAttribute(mIndex) - .getModified(); + Interpreter::Type_Float value = 0.f; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex).getModified(); - runtime.push (value); - } + runtime.push(value); + } }; - template + template class OpSetAttribute : public Interpreter::Opcode0 { - int mIndex; + ESM::RefId mIndex; - public: + public: + OpSetAttribute(ESM::RefId index) + : mIndex(index) + { + } - OpSetAttribute (int index) : mIndex (index) {} + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Float value = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float value = runtime[0].mFloat; - runtime.pop(); + if (!ptr.getClass().isActor()) + return; - MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); - attribute.setBase (value); - ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); - } + MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); + attribute.setBase(value, true); + ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); + } }; - template + template class OpModAttribute : public Interpreter::Opcode0 { - int mIndex; + ESM::RefId mIndex; - public: - - OpModAttribute (int index) : mIndex (index) {} - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - Interpreter::Type_Float value = runtime[0].mFloat; - runtime.pop(); - - MWMechanics::AttributeValue attribute = ptr.getClass() - .getCreatureStats(ptr) - .getAttribute(mIndex); + public: + OpModAttribute(ESM::RefId index) + : mIndex(index) + { + } - if (value == 0) - return; + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - if (((attribute.getBase() <= 0) && (value < 0)) - || ((attribute.getBase() >= 100) && (value > 0))) - return; + Interpreter::Type_Float value = runtime[0].mFloat; + runtime.pop(); - if (value < 0) - attribute.setBase(std::max(0.f, attribute.getBase() + value)); - else - attribute.setBase(std::min(100.f, attribute.getBase() + value)); + if (!ptr.getClass().isActor()) + return; - ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); - } + MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); + modStat(attribute, value); + ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); + } }; - template + template class OpGetDynamic : public Interpreter::Opcode0 { - int mIndex; + int mIndex; - public: + public: + OpGetDynamic(int index) + : mIndex(index) + { + } - OpGetDynamic (int index) : mIndex (index) {} + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Float value = 0.f; - void execute (Interpreter::Runtime& runtime) override + if (mIndex == 0 && ptr.getClass().hasItemHealth(ptr)) { - MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Float value; - - if (mIndex==0 && ptr.getClass().hasItemHealth (ptr)) - { - // health is a special case - value = static_cast(ptr.getClass().getItemMaxHealth(ptr)); - } else { - value = - ptr.getClass() - .getCreatureStats(ptr) - .getDynamic(mIndex) - .getCurrent(); - // GetMagicka shouldn't return negative values - if(mIndex == 1 && value < 0) - value = 0; - } - runtime.push (value); + // health is a special case + value = static_cast(ptr.getClass().getItemMaxHealth(ptr)); + } + else if (ptr.getClass().isActor()) + { + value = ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex).getCurrent(); + // GetMagicka shouldn't return negative values + if (mIndex == 1 && value < 0) + value = 0; } + runtime.push(value); + } }; - template + template class OpSetDynamic : public Interpreter::Opcode0 { - int mIndex; + int mIndex; - public: + public: + OpSetDynamic(int index) + : mIndex(index) + { + } - OpSetDynamic (int index) : mIndex (index) {} + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Float value = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float value = runtime[0].mFloat; - runtime.pop(); + if (!ptr.getClass().isActor()) + return; - MWMechanics::DynamicStat stat (ptr.getClass().getCreatureStats (ptr) - .getDynamic (mIndex)); + MWMechanics::DynamicStat stat(ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex)); - stat.setModified (value, 0); - stat.setCurrent(value); + stat.setBase(value); + stat.setCurrent(stat.getModified(false), true, true); - ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); - } + ptr.getClass().getCreatureStats(ptr).setDynamic(mIndex, stat); + } }; - template + template class OpModDynamic : public Interpreter::Opcode0 { - int mIndex; + int mIndex; - public: + public: + OpModDynamic(int index) + : mIndex(index) + { + } - OpModDynamic (int index) : mIndex (index) {} + void execute(Interpreter::Runtime& runtime) override + { + int peek = R::implicit ? 0 : runtime[0].mInteger; - void execute (Interpreter::Runtime& runtime) override - { - int peek = R::implicit ? 0 : runtime[0].mInteger; + MWWorld::Ptr ptr = R()(runtime); - MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Float diff = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float diff = runtime[0].mFloat; - runtime.pop(); + if (!ptr.getClass().isActor()) + return; - // workaround broken endgame scripts that kill dagoth ur - if (!R::implicit && - ::Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "dagoth_ur_1")) - { - runtime.push (peek); + // workaround broken endgame scripts that kill dagoth ur + if (!R::implicit && ptr.getCellRef().getRefId() == "dagoth_ur_1") + { + runtime.push(peek); - if (R()(runtime, false, true).isEmpty()) - { - Log(Debug::Warning) - << "Warning: Compensating for broken script in Morrowind.esm by " - << "ignoring remote access to dagoth_ur_1"; + if (R()(runtime, false, true).isEmpty()) + { + Log(Debug::Warning) << "Warning: Compensating for broken script in Morrowind.esm by " + << "ignoring remote access to dagoth_ur_1"; - return; - } + return; } + } - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); - - Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent(); - - MWMechanics::DynamicStat stat (ptr.getClass().getCreatureStats (ptr) - .getDynamic (mIndex)); + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - stat.setModified (diff + stat.getModified(), 0); - stat.setCurrentModified (diff + stat.getCurrentModified()); + MWMechanics::DynamicStat stat = stats.getDynamic(mIndex); - stat.setCurrent (diff + current); + float current = stat.getCurrent(); + float base = diff + stat.getBase(); + if (mIndex != 2) + base = std::max(base, 0.f); + stat.setBase(base); + stat.setCurrent(diff + current, true, true); - ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); - } + stats.setDynamic(mIndex, stat); + } }; - template + template class OpModCurrentDynamic : public Interpreter::Opcode0 { - int mIndex; + int mIndex; - public: + public: + OpModCurrentDynamic(int index) + : mIndex(index) + { + } - OpModCurrentDynamic (int index) : mIndex (index) {} + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Float diff = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float diff = runtime[0].mFloat; - runtime.pop(); + if (!ptr.getClass().isActor()) + return; - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent(); + Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent(); - MWMechanics::DynamicStat stat (ptr.getClass().getCreatureStats (ptr) - .getDynamic (mIndex)); + MWMechanics::DynamicStat stat(ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex)); - bool allowDecreaseBelowZero = false; - if (mIndex == 2) // Fatigue-specific logic - { - // For fatigue, a negative current value is allowed and means the actor will be knocked down - allowDecreaseBelowZero = true; - // Knock down the actor immediately if a non-positive new value is the case - if (diff + current <= 0.f) - ptr.getClass().getCreatureStats(ptr).setKnockedDown(true); - } - stat.setCurrent (diff + current, allowDecreaseBelowZero); - - ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); + bool allowDecreaseBelowZero = false; + if (mIndex == 2) // Fatigue-specific logic + { + // For fatigue, a negative current value is allowed and means the actor will be knocked down + allowDecreaseBelowZero = true; + // Knock down the actor immediately if a non-positive new value is the case + if (diff + current <= 0.f) + ptr.getClass().getCreatureStats(ptr).setKnockedDown(true); } + stat.setCurrent(diff + current, allowDecreaseBelowZero); + + ptr.getClass().getCreatureStats(ptr).setDynamic(mIndex, stat); + } }; - template + template class OpGetDynamicGetRatio : public Interpreter::Opcode0 { - int mIndex; + int mIndex; - public: + public: + OpGetDynamicGetRatio(int index) + : mIndex(index) + { + } - OpGetDynamicGetRatio (int index) : mIndex (index) {} + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override + if (!ptr.getClass().isActor()) { - MWWorld::Ptr ptr = R()(runtime); - - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); - - Interpreter::Type_Float value = 0; - - Interpreter::Type_Float max = stats.getDynamic(mIndex).getModified(); + runtime.push(0.f); + return; + } - if (max>0) - value = stats.getDynamic(mIndex).getCurrent() / max; + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - runtime.push (value); - } + runtime.push(stats.getDynamic(mIndex).getRatio()); + } }; - template + template class OpGetSkill : public Interpreter::Opcode0 { - int mIndex; + ESM::RefId mId; - public: + public: + OpGetSkill(ESM::RefId id) + : mId(id) + { + } - OpGetSkill (int index) : mIndex (index) {} + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override + if (!ptr.getClass().isActor()) { - MWWorld::Ptr ptr = R()(runtime); + runtime.push(0.f); + return; + } - Interpreter::Type_Float value = ptr.getClass().getSkill(ptr, mIndex); + Interpreter::Type_Float value = ptr.getClass().getSkill(ptr, mId); - runtime.push (value); - } + runtime.push(value); + } }; - template + template class OpSetSkill : public Interpreter::Opcode0 { - int mIndex; + ESM::RefId mId; - public: + public: + OpSetSkill(ESM::RefId id) + : mId(id) + { + } - OpSetSkill (int index) : mIndex (index) {} + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + Interpreter::Type_Float value = runtime[0].mFloat; + runtime.pop(); - Interpreter::Type_Float value = runtime[0].mFloat; - runtime.pop(); + if (!ptr.getClass().isNpc()) + return; - MWMechanics::NpcStats& stats = ptr.getClass().getNpcStats (ptr); + MWMechanics::NpcStats& stats = ptr.getClass().getNpcStats(ptr); - stats.getSkill (mIndex).setBase (value); - } + stats.getSkill(mId).setBase(value, true); + } }; - template + template class OpModSkill : public Interpreter::Opcode0 { - int mIndex; - - public: - - OpModSkill (int index) : mIndex (index) {} - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId mId; - Interpreter::Type_Float value = runtime[0].mFloat; - runtime.pop(); + public: + OpModSkill(ESM::RefId id) + : mId(id) + { + } - MWMechanics::SkillValue &skill = ptr.getClass() - .getNpcStats(ptr) - .getSkill(mIndex); + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - if (value == 0) - return; + Interpreter::Type_Float value = runtime[0].mFloat; + runtime.pop(); - if (((skill.getBase() <= 0.f) && (value < 0.f)) - || ((skill.getBase() >= 100.f) && (value > 0.f))) - return; + if (!ptr.getClass().isNpc()) + return; - if (value < 0) - skill.setBase(std::max(0.f, skill.getBase() + value)); - else - skill.setBase(std::min(100.f, skill.getBase() + value)); - } + MWMechanics::SkillValue& skill = ptr.getClass().getNpcStats(ptr).getSkill(mId); + modStat(skill, value); + } }; class OpGetPCCrimeLevel : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - MWWorld::Ptr player = world->getPlayerPtr(); - runtime.push (static_cast (player.getClass().getNpcStats (player).getBounty())); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::Ptr player = world->getPlayerPtr(); + runtime.push(static_cast(player.getClass().getNpcStats(player).getBounty())); + } }; class OpSetPCCrimeLevel : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - MWWorld::Ptr player = world->getPlayerPtr(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::Ptr player = world->getPlayerPtr(); - int bounty = static_cast(runtime[0].mFloat); - runtime.pop(); - player.getClass().getNpcStats (player).setBounty(bounty); + int bounty = static_cast(runtime[0].mFloat); + runtime.pop(); + player.getClass().getNpcStats(player).setBounty(bounty); - if (bounty == 0) - MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); - } + if (bounty == 0) + MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); + } }; class OpModPCCrimeLevel : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - MWWorld::Ptr player = world->getPlayerPtr(); - - player.getClass().getNpcStats(player).setBounty(static_cast(runtime[0].mFloat) + player.getClass().getNpcStats(player).getBounty()); - runtime.pop(); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::Ptr player = world->getPlayerPtr(); + int bounty = std::max( + 0, static_cast(runtime[0].mFloat) + player.getClass().getNpcStats(player).getBounty()); + player.getClass().getNpcStats(player).setBounty(bounty); + runtime.pop(); + if (bounty == 0) + MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); + } }; - template + template class OpAddSpell : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string id = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + if (!ptr.getClass().isActor()) + return; - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find (id); + const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(id); - MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); - creatureStats.getSpells().add(id); - ESM::Spell::SpellType type = static_cast(spell->mData.mType); - if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Power) - { - // Apply looping particles immediately for constant effects - MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr); - } + MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); + creatureStats.getSpells().add(spell); + ESM::Spell::SpellType type = static_cast(spell->mData.mType); + if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Power) + { + // Add spell effect to *this actor's* queue immediately + creatureStats.getActiveSpells().addSpell(spell, ptr); + // Apply looping particles immediately for constant effects + MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr); } + } }; - template + template class OpRemoveSpell : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string id = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + if (!ptr.getClass().isActor()) + return; - MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); - // The spell may have an instant effect which must be handled before the spell's removal. - for (const auto& effect : creatureStats.getSpells().getMagicEffects()) - { - if (effect.second.getMagnitude() <= 0) - continue; - MWMechanics::CastSpell cast(ptr, ptr); - if (cast.applyInstantEffect(ptr, ptr, effect.first, effect.second.getMagnitude())) - creatureStats.getSpells().purgeEffect(effect.first.mId); - } + MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); + creatureStats.getSpells().remove(id); - MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, id); - creatureStats.getSpells().remove (id); + MWBase::WindowManager* wm = MWBase::Environment::get().getWindowManager(); - MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); - - if (ptr == MWMechanics::getPlayer() && - id == wm->getSelectedSpell()) - { - wm->unsetSelectedSpell(); - } + if (ptr == MWMechanics::getPlayer() && id == wm->getSelectedSpell()) + { + wm->unsetSelectedSpell(); } + } }; - template + template class OpRemoveSpellEffects : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - std::string spellid = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId spellid = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(spellid); - ptr.getClass().getCreatureStats (ptr).getSpells().removeEffects(spellid); - } + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffectsBySourceSpellId(ptr, spellid); + } }; - template + template class OpRemoveEffects : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer effectId = runtime[0].mInteger; - runtime.pop(); + Interpreter::Type_Integer effectId = runtime[0].mInteger; + runtime.pop(); - ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(effectId); - } + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).getActiveSpells().purgeEffect(ptr, effectId); + } }; - template + template class OpGetSpell : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { + public: + void execute(Interpreter::Runtime& runtime) override + { - MWWorld::Ptr ptr = R()(runtime); + MWWorld::Ptr ptr = R()(runtime); - std::string id = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - Interpreter::Type_Integer value = 0; + Interpreter::Type_Integer value = 0; - if (ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getSpells().hasSpell(id)) - value = 1; + if (ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getSpells().hasSpell(id)) + value = 1; - runtime.push (value); - } + runtime.push(value); + } }; - template + template class OpPCJoinFaction : public Interpreter::Opcode1 { - public: - - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::ConstPtr actor = R()(runtime, false); + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::ConstPtr actor = R()(runtime, false); - std::string factionID = ""; + ESM::RefId factionID; - if(arg0==0) - { - factionID = getDialogueActorFaction(actor); - } - else - { - factionID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - } - ::Misc::StringUtils::lowerCaseInPlace(factionID); - // Make sure this faction exists - MWBase::Environment::get().getWorld()->getStore().get().find(factionID); + if (arg0 == 0) + { + factionID = getDialogueActorFaction(actor); + } + else + { + factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + } + // Make sure this faction exists + MWBase::Environment::get().getESMStore()->get().find(factionID); - if(factionID != "") - { - MWWorld::Ptr player = MWMechanics::getPlayer(); - player.getClass().getNpcStats(player).joinFaction(factionID); - } + if (!factionID.empty()) + { + MWWorld::Ptr player = MWMechanics::getPlayer(); + player.getClass().getNpcStats(player).joinFaction(factionID); } + } }; - template + template class OpPCRaiseRank : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::ConstPtr actor = R()(runtime, false); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::ConstPtr actor = R()(runtime, false); + ESM::RefId factionID; - std::string factionID = ""; + if (arg0 == 0) + { + factionID = getDialogueActorFaction(actor); + } + else + { + factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + } + // Make sure this faction exists + MWBase::Environment::get().getESMStore()->get().find(factionID); - if(arg0==0) + if (!factionID.empty()) + { + MWWorld::Ptr player = MWMechanics::getPlayer(); + if (!player.getClass().getNpcStats(player).isInFaction(factionID)) { - factionID = getDialogueActorFaction(actor); + player.getClass().getNpcStats(player).joinFaction(factionID); } else { - factionID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - } - ::Misc::StringUtils::lowerCaseInPlace(factionID); - // Make sure this faction exists - MWBase::Environment::get().getWorld()->getStore().get().find(factionID); - - if(factionID != "") - { - MWWorld::Ptr player = MWMechanics::getPlayer(); - if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) == player.getClass().getNpcStats(player).getFactionRanks().end()) - { - player.getClass().getNpcStats(player).joinFaction(factionID); - } - else - { - player.getClass().getNpcStats(player).raiseRank(factionID); - } + int currentRank = player.getClass().getNpcStats(player).getFactionRank(factionID); + player.getClass().getNpcStats(player).setFactionRank(factionID, currentRank + 1); } } + } }; - template + template class OpPCLowerRank : public Interpreter::Opcode1 { - public: - - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::ConstPtr actor = R()(runtime, false); + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::ConstPtr actor = R()(runtime, false); - std::string factionID = ""; + ESM::RefId factionID; - if(arg0==0) - { - factionID = getDialogueActorFaction(actor); - } - else - { - factionID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - } - ::Misc::StringUtils::lowerCaseInPlace(factionID); - // Make sure this faction exists - MWBase::Environment::get().getWorld()->getStore().get().find(factionID); + if (arg0 == 0) + { + factionID = getDialogueActorFaction(actor); + } + else + { + factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + } + // Make sure this faction exists + MWBase::Environment::get().getESMStore()->get().find(factionID); - if(factionID != "") - { - MWWorld::Ptr player = MWMechanics::getPlayer(); - player.getClass().getNpcStats(player).lowerRank(factionID); - } + if (!factionID.empty()) + { + MWWorld::Ptr player = MWMechanics::getPlayer(); + int currentRank = player.getClass().getNpcStats(player).getFactionRank(factionID); + player.getClass().getNpcStats(player).setFactionRank(factionID, currentRank - 1); } + } }; - template + template class OpGetPCRank : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::ConstPtr ptr = R()(runtime, false); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override + ESM::RefId factionID; + if (arg0 > 0) { - MWWorld::ConstPtr ptr = R()(runtime, false); - - std::string factionID = ""; - if(arg0 >0) - { - factionID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - } - else - { - factionID = ptr.getClass().getPrimaryFaction(ptr); - } - ::Misc::StringUtils::lowerCaseInPlace(factionID); - // Make sure this faction exists - MWBase::Environment::get().getWorld()->getStore().get().find(factionID); + factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + } + else + { + factionID = ptr.getClass().getPrimaryFaction(ptr); + } + // Make sure this faction exists + MWBase::Environment::get().getESMStore()->get().find(factionID); + if (!factionID.empty()) + { MWWorld::Ptr player = MWMechanics::getPlayer(); - if(factionID!="") - { - if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) != player.getClass().getNpcStats(player).getFactionRanks().end()) - { - runtime.push(player.getClass().getNpcStats(player).getFactionRanks().at(factionID)); - } - else - { - runtime.push(-1); - } - } - else - { - runtime.push(-1); - } + runtime.push(player.getClass().getNpcStats(player).getFactionRank(factionID)); + } + else + { + runtime.push(-1); } + } }; - template + template class OpModDisposition : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = runtime[0].mInteger; - runtime.pop(); + Interpreter::Type_Integer value = runtime[0].mInteger; + runtime.pop(); - if (ptr.getClass().isNpc()) - ptr.getClass().getNpcStats (ptr).setBaseDisposition - (ptr.getClass().getNpcStats (ptr).getBaseDisposition() + value); + if (ptr.getClass().isNpc()) + ptr.getClass().getNpcStats(ptr).setBaseDisposition( + ptr.getClass().getNpcStats(ptr).getBaseDisposition() + value); - // else: must not throw exception (used by an Almalexia dialogue script) - } + // else: must not throw exception (used by an Almalexia dialogue script) + } }; - template + template class OpSetDisposition : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = runtime[0].mInteger; - runtime.pop(); + Interpreter::Type_Integer value = runtime[0].mInteger; + runtime.pop(); - if (ptr.getClass().isNpc()) - ptr.getClass().getNpcStats (ptr).setBaseDisposition (value); - } + if (ptr.getClass().isNpc()) + ptr.getClass().getNpcStats(ptr).setBaseDisposition(value); + } }; - template + template class OpGetDisposition : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - if (!ptr.getClass().isNpc()) - runtime.push(0); - else - runtime.push (MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(ptr)); - } + if (!ptr.getClass().isNpc()) + runtime.push(0); + else + runtime.push(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(ptr)); + } }; class OpGetDeadCount : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - std::string id = runtime.getStringLiteral (runtime[0].mInteger); - runtime[0].mInteger = MWBase::Environment::get().getMechanicsManager()->countDeaths (id); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime[0].mInteger = MWBase::Environment::get().getMechanicsManager()->countDeaths(id); + } }; - template + template class OpGetPCFacRep : public Interpreter::Opcode1 { - public: - - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::ConstPtr ptr = R()(runtime, false); - - std::string factionId; + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::ConstPtr ptr = R()(runtime, false); - if (arg0==1) - { - factionId = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - } - else - { - factionId = getDialogueActorFaction(ptr); - } + ESM::RefId factionId; - if (factionId.empty()) - throw std::runtime_error ("failed to determine faction"); + if (arg0 == 1) + { + factionId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + } + else + { + factionId = getDialogueActorFaction(ptr); + } - ::Misc::StringUtils::lowerCaseInPlace (factionId); + if (factionId.empty()) + throw std::runtime_error("failed to determine faction"); - MWWorld::Ptr player = MWMechanics::getPlayer(); - runtime.push ( - player.getClass().getNpcStats (player).getFactionReputation (factionId)); - } + MWWorld::Ptr player = MWMechanics::getPlayer(); + runtime.push(player.getClass().getNpcStats(player).getFactionReputation(factionId)); + } }; - template + template class OpSetPCFacRep : public Interpreter::Opcode1 { - public: - - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::ConstPtr ptr = R()(runtime, false); - - Interpreter::Type_Integer value = runtime[0].mInteger; - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::ConstPtr ptr = R()(runtime, false); - std::string factionId; + Interpreter::Type_Integer value = runtime[0].mInteger; + runtime.pop(); - if (arg0==1) - { - factionId = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - } - else - { - factionId = getDialogueActorFaction(ptr); - } + ESM::RefId factionId; - if (factionId.empty()) - throw std::runtime_error ("failed to determine faction"); + if (arg0 == 1) + { + factionId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + } + else + { + factionId = getDialogueActorFaction(ptr); + } - ::Misc::StringUtils::lowerCaseInPlace (factionId); + if (factionId.empty()) + throw std::runtime_error("failed to determine faction"); - MWWorld::Ptr player = MWMechanics::getPlayer(); - player.getClass().getNpcStats (player).setFactionReputation (factionId, value); - } + MWWorld::Ptr player = MWMechanics::getPlayer(); + player.getClass().getNpcStats(player).setFactionReputation(factionId, value); + } }; - template + template class OpModPCFacRep : public Interpreter::Opcode1 { - public: - - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override - { - MWWorld::ConstPtr ptr = R()(runtime, false); - - Interpreter::Type_Integer value = runtime[0].mInteger; - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::ConstPtr ptr = R()(runtime, false); - std::string factionId; + Interpreter::Type_Integer value = runtime[0].mInteger; + runtime.pop(); - if (arg0==1) - { - factionId = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - } - else - { - factionId = getDialogueActorFaction(ptr); - } + ESM::RefId factionId; - if (factionId.empty()) - throw std::runtime_error ("failed to determine faction"); + if (arg0 == 1) + { + factionId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + } + else + { + factionId = getDialogueActorFaction(ptr); + } - ::Misc::StringUtils::lowerCaseInPlace (factionId); + if (factionId.empty()) + throw std::runtime_error("failed to determine faction"); - MWWorld::Ptr player = MWMechanics::getPlayer(); - player.getClass().getNpcStats (player).setFactionReputation (factionId, - player.getClass().getNpcStats (player).getFactionReputation (factionId)+ - value); - } + MWWorld::Ptr player = MWMechanics::getPlayer(); + player.getClass().getNpcStats(player).setFactionReputation( + factionId, player.getClass().getNpcStats(player).getFactionReputation(factionId) + value); + } }; - template + template class OpGetCommonDisease : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - runtime.push (ptr.getClass().getCreatureStats (ptr).hasCommonDisease()); - } + if (ptr.getClass().isActor()) + runtime.push(ptr.getClass().getCreatureStats(ptr).hasCommonDisease()); + else + runtime.push(0); + } }; - template + template class OpGetBlightDisease : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - runtime.push (ptr.getClass().getCreatureStats (ptr).hasBlightDisease()); - } + if (ptr.getClass().isActor()) + runtime.push(ptr.getClass().getCreatureStats(ptr).hasBlightDisease()); + else + runtime.push(0); + } }; - template + template class OpGetRace : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::ConstPtr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::ConstPtr ptr = R()(runtime); - std::string race = runtime.getStringLiteral(runtime[0].mInteger); - ::Misc::StringUtils::lowerCaseInPlace(race); - runtime.pop(); + ESM::RefId race = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - std::string npcRace = ptr.get()->mBase->mRace; - ::Misc::StringUtils::lowerCaseInPlace(npcRace); + if (ptr.getClass().isNpc()) + { + const ESM::RefId& npcRace = ptr.get()->mBase->mRace; - runtime.push (npcRace == race); + runtime.push(race == npcRace); + } + else + { + runtime.push(0); + } } }; class OpGetWerewolfKills : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); - runtime.push (ptr.getClass().getNpcStats (ptr).getWerewolfKills ()); - } + runtime.push(ptr.getClass().getNpcStats(ptr).getWerewolfKills()); + } }; template class OpPcExpelled : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::ConstPtr ptr = R()(runtime, false); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override + ESM::RefId factionID; + if (arg0 > 0) { - MWWorld::ConstPtr ptr = R()(runtime, false); - - std::string factionID = ""; - if(arg0 >0 ) - { - factionID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - } - else - { - factionID = ptr.getClass().getPrimaryFaction(ptr); - } - ::Misc::StringUtils::lowerCaseInPlace(factionID); - MWWorld::Ptr player = MWMechanics::getPlayer(); - if(factionID!="") - { - runtime.push(player.getClass().getNpcStats(player).getExpelled(factionID)); - } - else - { - runtime.push(0); - } + factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + } + else + { + factionID = ptr.getClass().getPrimaryFaction(ptr); + } + MWWorld::Ptr player = MWMechanics::getPlayer(); + if (!factionID.empty()) + { + runtime.push(player.getClass().getNpcStats(player).getExpelled(factionID)); + } + else + { + runtime.push(0); } + } }; template class OpPcExpell : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::ConstPtr ptr = R()(runtime, false); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override + ESM::RefId factionID; + if (arg0 > 0) { - MWWorld::ConstPtr ptr = R()(runtime, false); - - std::string factionID = ""; - if(arg0 >0 ) - { - factionID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - } - else - { - factionID = ptr.getClass().getPrimaryFaction(ptr); - } - MWWorld::Ptr player = MWMechanics::getPlayer(); - if(factionID!="") - { - player.getClass().getNpcStats(player).expell(factionID); - } + factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + } + else + { + factionID = ptr.getClass().getPrimaryFaction(ptr); } + MWWorld::Ptr player = MWMechanics::getPlayer(); + if (!factionID.empty()) + { + player.getClass().getNpcStats(player).expell(factionID, true); + } + } }; template class OpPcClearExpelled : public Interpreter::Opcode1 { - public: + public: + void execute(Interpreter::Runtime& runtime, unsigned int arg0) override + { + MWWorld::ConstPtr ptr = R()(runtime, false); - void execute (Interpreter::Runtime& runtime, unsigned int arg0) override + ESM::RefId factionID; + if (arg0 > 0) { - MWWorld::ConstPtr ptr = R()(runtime, false); - - std::string factionID = ""; - if(arg0 >0 ) - { - factionID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - } - else - { - factionID = ptr.getClass().getPrimaryFaction(ptr); - } - MWWorld::Ptr player = MWMechanics::getPlayer(); - if(factionID!="") - player.getClass().getNpcStats(player).clearExpelled(factionID); + factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + } + else + { + factionID = ptr.getClass().getPrimaryFaction(ptr); } + MWWorld::Ptr player = MWMechanics::getPlayer(); + if (!factionID.empty()) + player.getClass().getNpcStats(player).clearExpelled(factionID); + } }; template class OpRaiseRank : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - std::string factionID = ptr.getClass().getPrimaryFaction(ptr); - if(factionID.empty()) - return; + const ESM::RefId& factionID = ptr.getClass().getPrimaryFaction(ptr); + if (factionID.empty()) + return; - MWWorld::Ptr player = MWMechanics::getPlayer(); + MWWorld::Ptr player = MWMechanics::getPlayer(); - // no-op when executed on the player - if (ptr == player) - return; + // no-op when executed on the player + if (ptr == player) + return; - // If we already changed rank for this NPC, modify current rank in the NPC stats. - // Otherwise take rank from base NPC record, increase it and put it to NPC data. - int currentRank = ptr.getClass().getNpcStats(ptr).getFactionRank(factionID); - if (currentRank >= 0) - ptr.getClass().getNpcStats(ptr).raiseRank(factionID); - else - { - int rank = ptr.getClass().getPrimaryFactionRank(ptr); - rank++; - ptr.getClass().getNpcStats(ptr).joinFaction(factionID); - for (int i=0; i= 0) + ptr.getClass().getNpcStats(ptr).setFactionRank(factionID, currentRank + 1); + else + { + int rank = ptr.getClass().getPrimaryFactionRank(ptr); + ptr.getClass().getNpcStats(ptr).joinFaction(factionID); + ptr.getClass().getNpcStats(ptr).setFactionRank(factionID, rank + 1); } + } }; template class OpLowerRank : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - std::string factionID = ptr.getClass().getPrimaryFaction(ptr); - if(factionID.empty()) - return; + const ESM::RefId& factionID = ptr.getClass().getPrimaryFaction(ptr); + if (factionID.empty()) + return; - MWWorld::Ptr player = MWMechanics::getPlayer(); + MWWorld::Ptr player = MWMechanics::getPlayer(); - // no-op when executed on the player - if (ptr == player) - return; + // no-op when executed on the player + if (ptr == player) + return; - // If we already changed rank for this NPC, modify current rank in the NPC stats. - // Otherwise take rank from base NPC record, decrease it and put it to NPC data. - int currentRank = ptr.getClass().getNpcStats(ptr).getFactionRank(factionID); - if (currentRank == 0) - return; - else if (currentRank > 0) - ptr.getClass().getNpcStats(ptr).lowerRank(factionID); - else - { - int rank = ptr.getClass().getPrimaryFactionRank(ptr); - rank--; - ptr.getClass().getNpcStats(ptr).joinFaction(factionID); - for (int i=0; i 0) + ptr.getClass().getNpcStats(ptr).setFactionRank(factionID, currentRank - 1); + else + { + int rank = ptr.getClass().getPrimaryFactionRank(ptr); + ptr.getClass().getNpcStats(ptr).joinFaction(factionID); + ptr.getClass().getNpcStats(ptr).setFactionRank(factionID, std::max(0, rank - 1)); } + } }; template class OpOnDeath : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) { - MWWorld::Ptr ptr = R()(runtime); - - Interpreter::Type_Integer value = - ptr.getClass().getCreatureStats (ptr).hasDied(); + auto& stats = ptr.getClass().getCreatureStats(ptr); + value = stats.hasDied(); if (value) - ptr.getClass().getCreatureStats (ptr).clearHasDied(); - - runtime.push (value); + stats.clearHasDied(); } + + runtime.push(value); + } }; template class OpOnMurder : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) { - MWWorld::Ptr ptr = R()(runtime); - - Interpreter::Type_Integer value = - ptr.getClass().getCreatureStats (ptr).hasBeenMurdered(); + auto& stats = ptr.getClass().getCreatureStats(ptr); + value = stats.hasBeenMurdered(); if (value) - ptr.getClass().getCreatureStats (ptr).clearHasBeenMurdered(); - - runtime.push (value); + stats.clearHasBeenMurdered(); } + + runtime.push(value); + } }; template class OpOnKnockout : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = - ptr.getClass().getCreatureStats (ptr).getKnockedDownOneFrame(); + Interpreter::Type_Integer value = 0; + if (ptr.getClass().isActor()) + value = ptr.getClass().getCreatureStats(ptr).getKnockedDownOneFrame(); - runtime.push (value); - } + runtime.push(value); + } }; template class OpIsWerewolf : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + if (ptr.getClass().isNpc()) runtime.push(ptr.getClass().getNpcStats(ptr).isWerewolf()); - } + else + runtime.push(0); + } }; template class OpSetWerewolf : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + if (ptr.getClass().isNpc()) MWBase::Environment::get().getMechanicsManager()->setWerewolf(ptr, set); - } + } }; template class OpSetWerewolfAcrobatics : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + if (ptr.getClass().isNpc()) MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(ptr); - } + } }; template class OpResurrect : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getClass().isActor()) + return; - if (ptr == MWMechanics::getPlayer()) + if (ptr == MWMechanics::getPlayer()) + { + MWBase::Environment::get().getMechanicsManager()->resurrect(ptr); + if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_Ended) + MWBase::Environment::get().getStateManager()->resumeGame(); + } + else if (ptr.getClass().getCreatureStats(ptr).isDead()) + { + bool wasEnabled = ptr.getRefData().isEnabled(); + MWBase::Environment::get().getWorld()->undeleteObject(ptr); + auto windowManager = MWBase::Environment::get().getWindowManager(); + bool wasOpen = windowManager->containsMode(MWGui::GM_Container); + windowManager->onDeleteCustomData(ptr); + // HACK: disable/enable object to re-add it to the scene properly (need a new Animation). + MWBase::Environment::get().getWorld()->disable(ptr); + // The actor's base record may have changed after this specific reference was created. + // So we need to update to the current version + if (ptr.getClass().isNpc()) + updateBaseRecord(ptr); + else + updateBaseRecord(ptr); + if (wasOpen && !windowManager->containsMode(MWGui::GM_Container)) { + // Reopen the loot GUI if it was closed because we resurrected the actor we were looting MWBase::Environment::get().getMechanicsManager()->resurrect(ptr); - if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_Ended) - MWBase::Environment::get().getStateManager()->resumeGame(); + windowManager->forceLootMode(ptr); } - else if (ptr.getClass().getCreatureStats(ptr).isDead()) + else { - bool wasEnabled = ptr.getRefData().isEnabled(); - MWBase::Environment::get().getWorld()->undeleteObject(ptr); MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); - - // HACK: disable/enable object to re-add it to the scene properly (need a new Animation). - MWBase::Environment::get().getWorld()->disable(ptr); // resets runtime state such as inventory, stats and AI. does not reset position in the world ptr.getRefData().setCustomData(nullptr); - if (wasEnabled) - MWBase::Environment::get().getWorld()->enable(ptr); } + if (wasEnabled) + MWBase::Environment::get().getWorld()->enable(ptr); } + } }; template class OpGetStat : public Interpreter::Opcode0 { public: - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { // dummy + runtime.pop(); runtime.push(0); } }; @@ -1234,28 +1262,34 @@ namespace MWScript int mNegativeEffect; public: - OpGetMagicEffect (int positiveEffect, int negativeEffect) + OpGetMagicEffect(int positiveEffect, int negativeEffect) : mPositiveEffect(positiveEffect) , mNegativeEffect(negativeEffect) { } - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getClass().isActor()) + { + runtime.push(0); + return; + } + const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); - float currentValue = effects.get(mPositiveEffect).getMagnitude(); + float currentValue = effects.getOrDefault(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) - currentValue -= effects.get(mNegativeEffect).getMagnitude(); + currentValue -= effects.getOrDefault(mNegativeEffect).getMagnitude(); // GetResist* should take in account elemental shields if (mPositiveEffect == ESM::MagicEffect::ResistFire) - currentValue += effects.get(ESM::MagicEffect::FireShield).getMagnitude(); + currentValue += effects.getOrDefault(ESM::MagicEffect::FireShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistShock) - currentValue += effects.get(ESM::MagicEffect::LightningShield).getMagnitude(); + currentValue += effects.getOrDefault(ESM::MagicEffect::LightningShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistFrost) - currentValue += effects.get(ESM::MagicEffect::FrostShield).getMagnitude(); + currentValue += effects.getOrDefault(ESM::MagicEffect::FrostShield).getMagnitude(); int ret = static_cast(currentValue); runtime.push(ret); @@ -1269,30 +1303,35 @@ namespace MWScript int mNegativeEffect; public: - OpSetMagicEffect (int positiveEffect, int negativeEffect) + OpSetMagicEffect(int positiveEffect, int negativeEffect) : mPositiveEffect(positiveEffect) , mNegativeEffect(negativeEffect) { } - void execute(Interpreter::Runtime &runtime) override + void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); + + int arg = runtime[0].mInteger; + runtime.pop(); + + if (!ptr.getClass().isActor()) + return; + MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); - float currentValue = effects.get(mPositiveEffect).getMagnitude(); + float currentValue = effects.getOrDefault(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) - currentValue -= effects.get(mNegativeEffect).getMagnitude(); + currentValue -= effects.getOrDefault(mNegativeEffect).getMagnitude(); // SetResist* should take in account elemental shields if (mPositiveEffect == ESM::MagicEffect::ResistFire) - currentValue += effects.get(ESM::MagicEffect::FireShield).getMagnitude(); + currentValue += effects.getOrDefault(ESM::MagicEffect::FireShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistShock) - currentValue += effects.get(ESM::MagicEffect::LightningShield).getMagnitude(); + currentValue += effects.getOrDefault(ESM::MagicEffect::LightningShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistFrost) - currentValue += effects.get(ESM::MagicEffect::FrostShield).getMagnitude(); + currentValue += effects.getOrDefault(ESM::MagicEffect::FrostShield).getMagnitude(); - int arg = runtime[0].mInteger; - runtime.pop(); effects.modifyBase(mPositiveEffect, (arg - static_cast(currentValue))); } }; @@ -1304,173 +1343,226 @@ namespace MWScript int mNegativeEffect; public: - OpModMagicEffect (int positiveEffect, int negativeEffect) + OpModMagicEffect(int positiveEffect, int negativeEffect) : mPositiveEffect(positiveEffect) , mNegativeEffect(negativeEffect) { } - void execute(Interpreter::Runtime &runtime) override + void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); int arg = runtime[0].mInteger; runtime.pop(); + + if (!ptr.getClass().isActor()) + return; + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); stats.getMagicEffects().modifyBase(mPositiveEffect, arg); } }; + class OpGetPCVisionBonus : public Interpreter::Opcode0 + { + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr player = MWMechanics::getPlayer(); + MWMechanics::EffectParam nightEye + = player.getClass().getCreatureStats(player).getMagicEffects().getOrDefault( + ESM::MagicEffect::NightEye); + runtime.push(std::clamp(nightEye.getMagnitude() / 100.f, 0.f, 1.f)); + } + }; + + class OpSetPCVisionBonus : public Interpreter::Opcode0 + { + public: + void execute(Interpreter::Runtime& runtime) override + { + float arg = runtime[0].mFloat; + runtime.pop(); + MWWorld::Ptr player = MWMechanics::getPlayer(); + auto& effects = player.getClass().getCreatureStats(player).getMagicEffects(); + float delta = std::clamp(arg * 100.f, 0.f, 100.f) + - effects.getOrDefault(ESM::MagicEffect::NightEye).getMagnitude(); + effects.modifyBase(ESM::MagicEffect::NightEye, static_cast(delta)); + } + }; + + class OpModPCVisionBonus : public Interpreter::Opcode0 + { + public: + void execute(Interpreter::Runtime& runtime) override + { + float arg = runtime[0].mFloat; + runtime.pop(); + MWWorld::Ptr player = MWMechanics::getPlayer(); + auto& effects = player.getClass().getCreatureStats(player).getMagicEffects(); + const MWMechanics::EffectParam nightEye = effects.getOrDefault(ESM::MagicEffect::NightEye); + float newBase = std::clamp(nightEye.getMagnitude() + arg * 100.f, 0.f, 100.f); + newBase -= nightEye.getModifier(); + float delta = std::clamp(newBase, 0.f, 100.f) - nightEye.getMagnitude(); + effects.modifyBase(ESM::MagicEffect::NightEye, static_cast(delta)); + } + }; + struct MagicEffect { int mPositiveEffect; int mNegativeEffect; }; - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - for (int i=0; i (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeGetAttributeExplicit+i, - new OpGetAttribute (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeSetAttribute+i, new OpSetAttribute (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeSetAttributeExplicit+i, - new OpSetAttribute (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeModAttribute+i, new OpModAttribute (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeModAttributeExplicit+i, - new OpModAttribute (i)); - } - - for (int i=0; i (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeGetDynamicExplicit+i, - new OpGetDynamic (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeSetDynamic+i, new OpSetDynamic (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeSetDynamicExplicit+i, - new OpSetDynamic (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeModDynamic+i, new OpModDynamic (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeModDynamicExplicit+i, - new OpModDynamic (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeModCurrentDynamic+i, - new OpModCurrentDynamic (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeModCurrentDynamicExplicit+i, - new OpModCurrentDynamic (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetDynamicGetRatio+i, - new OpGetDynamicGetRatio (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeGetDynamicGetRatioExplicit+i, - new OpGetDynamicGetRatio (i)); - } - - for (int i=0; i (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeGetSkillExplicit+i, new OpGetSkill (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeSetSkill+i, new OpSetSkill (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeSetSkillExplicit+i, new OpSetSkill (i)); - - interpreter.installSegment5 (Compiler::Stats::opcodeModSkill+i, new OpModSkill (i)); - interpreter.installSegment5 (Compiler::Stats::opcodeModSkillExplicit+i, new OpModSkill (i)); - } - - interpreter.installSegment5 (Compiler::Stats::opcodeGetPCCrimeLevel, new OpGetPCCrimeLevel); - interpreter.installSegment5 (Compiler::Stats::opcodeSetPCCrimeLevel, new OpSetPCCrimeLevel); - interpreter.installSegment5 (Compiler::Stats::opcodeModPCCrimeLevel, new OpModPCCrimeLevel); - - interpreter.installSegment5 (Compiler::Stats::opcodeAddSpell, new OpAddSpell); - interpreter.installSegment5 (Compiler::Stats::opcodeAddSpellExplicit, new OpAddSpell); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpell, new OpRemoveSpell); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpellExplicit, - new OpRemoveSpell); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpellEffects, new OpRemoveSpellEffects); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveSpellEffectsExplicit, - new OpRemoveSpellEffects); - interpreter.installSegment5 (Compiler::Stats::opcodeResurrect, new OpResurrect); - interpreter.installSegment5 (Compiler::Stats::opcodeResurrectExplicit, - new OpResurrect); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveEffects, new OpRemoveEffects); - interpreter.installSegment5 (Compiler::Stats::opcodeRemoveEffectsExplicit, - new OpRemoveEffects); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetSpell, new OpGetSpell); - interpreter.installSegment5 (Compiler::Stats::opcodeGetSpellExplicit, new OpGetSpell); - - interpreter.installSegment3(Compiler::Stats::opcodePCRaiseRank,new OpPCRaiseRank); - interpreter.installSegment3(Compiler::Stats::opcodePCLowerRank,new OpPCLowerRank); - interpreter.installSegment3(Compiler::Stats::opcodePCJoinFaction,new OpPCJoinFaction); - interpreter.installSegment3(Compiler::Stats::opcodePCRaiseRankExplicit,new OpPCRaiseRank); - interpreter.installSegment3(Compiler::Stats::opcodePCLowerRankExplicit,new OpPCLowerRank); - interpreter.installSegment3(Compiler::Stats::opcodePCJoinFactionExplicit,new OpPCJoinFaction); - interpreter.installSegment3(Compiler::Stats::opcodeGetPCRank,new OpGetPCRank); - interpreter.installSegment3(Compiler::Stats::opcodeGetPCRankExplicit,new OpGetPCRank); - - interpreter.installSegment5(Compiler::Stats::opcodeModDisposition,new OpModDisposition); - interpreter.installSegment5(Compiler::Stats::opcodeModDispositionExplicit,new OpModDisposition); - interpreter.installSegment5(Compiler::Stats::opcodeSetDisposition,new OpSetDisposition); - interpreter.installSegment5(Compiler::Stats::opcodeSetDispositionExplicit,new OpSetDisposition); - interpreter.installSegment5(Compiler::Stats::opcodeGetDisposition,new OpGetDisposition); - interpreter.installSegment5(Compiler::Stats::opcodeGetDispositionExplicit,new OpGetDisposition); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetLevel, new OpGetLevel); - interpreter.installSegment5 (Compiler::Stats::opcodeGetLevelExplicit, new OpGetLevel); - interpreter.installSegment5 (Compiler::Stats::opcodeSetLevel, new OpSetLevel); - interpreter.installSegment5 (Compiler::Stats::opcodeSetLevelExplicit, new OpSetLevel); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetDeadCount, new OpGetDeadCount); - - interpreter.installSegment3 (Compiler::Stats::opcodeGetPCFacRep, new OpGetPCFacRep); - interpreter.installSegment3 (Compiler::Stats::opcodeGetPCFacRepExplicit, new OpGetPCFacRep); - interpreter.installSegment3 (Compiler::Stats::opcodeSetPCFacRep, new OpSetPCFacRep); - interpreter.installSegment3 (Compiler::Stats::opcodeSetPCFacRepExplicit, new OpSetPCFacRep); - interpreter.installSegment3 (Compiler::Stats::opcodeModPCFacRep, new OpModPCFacRep); - interpreter.installSegment3 (Compiler::Stats::opcodeModPCFacRepExplicit, new OpModPCFacRep); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetCommonDisease, new OpGetCommonDisease); - interpreter.installSegment5 (Compiler::Stats::opcodeGetCommonDiseaseExplicit, new OpGetCommonDisease); - interpreter.installSegment5 (Compiler::Stats::opcodeGetBlightDisease, new OpGetBlightDisease); - interpreter.installSegment5 (Compiler::Stats::opcodeGetBlightDiseaseExplicit, new OpGetBlightDisease); - - interpreter.installSegment5 (Compiler::Stats::opcodeGetRace, new OpGetRace); - interpreter.installSegment5 (Compiler::Stats::opcodeGetRaceExplicit, new OpGetRace); - interpreter.installSegment5 (Compiler::Stats::opcodeGetWerewolfKills, new OpGetWerewolfKills); - - interpreter.installSegment3 (Compiler::Stats::opcodePcExpelled, new OpPcExpelled); - interpreter.installSegment3 (Compiler::Stats::opcodePcExpelledExplicit, new OpPcExpelled); - interpreter.installSegment3 (Compiler::Stats::opcodePcExpell, new OpPcExpell); - interpreter.installSegment3 (Compiler::Stats::opcodePcExpellExplicit, new OpPcExpell); - interpreter.installSegment3 (Compiler::Stats::opcodePcClearExpelled, new OpPcClearExpelled); - interpreter.installSegment3 (Compiler::Stats::opcodePcClearExpelledExplicit, new OpPcClearExpelled); - interpreter.installSegment5 (Compiler::Stats::opcodeRaiseRank, new OpRaiseRank); - interpreter.installSegment5 (Compiler::Stats::opcodeRaiseRankExplicit, new OpRaiseRank); - interpreter.installSegment5 (Compiler::Stats::opcodeLowerRank, new OpLowerRank); - interpreter.installSegment5 (Compiler::Stats::opcodeLowerRankExplicit, new OpLowerRank); - - interpreter.installSegment5 (Compiler::Stats::opcodeOnDeath, new OpOnDeath); - interpreter.installSegment5 (Compiler::Stats::opcodeOnDeathExplicit, new OpOnDeath); - interpreter.installSegment5 (Compiler::Stats::opcodeOnMurder, new OpOnMurder); - interpreter.installSegment5 (Compiler::Stats::opcodeOnMurderExplicit, new OpOnMurder); - interpreter.installSegment5 (Compiler::Stats::opcodeOnKnockout, new OpOnKnockout); - interpreter.installSegment5 (Compiler::Stats::opcodeOnKnockoutExplicit, new OpOnKnockout); - - interpreter.installSegment5 (Compiler::Stats::opcodeIsWerewolf, new OpIsWerewolf); - interpreter.installSegment5 (Compiler::Stats::opcodeIsWerewolfExplicit, new OpIsWerewolf); - - interpreter.installSegment5 (Compiler::Stats::opcodeBecomeWerewolf, new OpSetWerewolf); - interpreter.installSegment5 (Compiler::Stats::opcodeBecomeWerewolfExplicit, new OpSetWerewolf); - interpreter.installSegment5 (Compiler::Stats::opcodeUndoWerewolf, new OpSetWerewolf); - interpreter.installSegment5 (Compiler::Stats::opcodeUndoWerewolfExplicit, new OpSetWerewolf); - interpreter.installSegment5 (Compiler::Stats::opcodeSetWerewolfAcrobatics, new OpSetWerewolfAcrobatics); - interpreter.installSegment5 (Compiler::Stats::opcodeSetWerewolfAcrobaticsExplicit, new OpSetWerewolfAcrobatics); - interpreter.installSegment5 (Compiler::Stats::opcodeGetStat, new OpGetStat); - interpreter.installSegment5 (Compiler::Stats::opcodeGetStatExplicit, new OpGetStat); + for (int i = 0; i < Compiler::Stats::numberOfAttributes; ++i) + { + ESM::RefId id = ESM::Attribute::indexToRefId(i); + interpreter.installSegment5>(Compiler::Stats::opcodeGetAttribute + i, id); + interpreter.installSegment5>( + Compiler::Stats::opcodeGetAttributeExplicit + i, id); + + interpreter.installSegment5>(Compiler::Stats::opcodeSetAttribute + i, id); + interpreter.installSegment5>( + Compiler::Stats::opcodeSetAttributeExplicit + i, id); + + interpreter.installSegment5>(Compiler::Stats::opcodeModAttribute + i, id); + interpreter.installSegment5>( + Compiler::Stats::opcodeModAttributeExplicit + i, id); + } + + for (int i = 0; i < Compiler::Stats::numberOfDynamics; ++i) + { + interpreter.installSegment5>(Compiler::Stats::opcodeGetDynamic + i, i); + interpreter.installSegment5>( + Compiler::Stats::opcodeGetDynamicExplicit + i, i); + + interpreter.installSegment5>(Compiler::Stats::opcodeSetDynamic + i, i); + interpreter.installSegment5>( + Compiler::Stats::opcodeSetDynamicExplicit + i, i); + + interpreter.installSegment5>(Compiler::Stats::opcodeModDynamic + i, i); + interpreter.installSegment5>( + Compiler::Stats::opcodeModDynamicExplicit + i, i); + + interpreter.installSegment5>( + Compiler::Stats::opcodeModCurrentDynamic + i, i); + interpreter.installSegment5>( + Compiler::Stats::opcodeModCurrentDynamicExplicit + i, i); + + interpreter.installSegment5>( + Compiler::Stats::opcodeGetDynamicGetRatio + i, i); + interpreter.installSegment5>( + Compiler::Stats::opcodeGetDynamicGetRatioExplicit + i, i); + } + + for (int i = 0; i < Compiler::Stats::numberOfSkills; ++i) + { + ESM::RefId id = ESM::Skill::indexToRefId(i); + interpreter.installSegment5>(Compiler::Stats::opcodeGetSkill + i, id); + interpreter.installSegment5>(Compiler::Stats::opcodeGetSkillExplicit + i, id); + + interpreter.installSegment5>(Compiler::Stats::opcodeSetSkill + i, id); + interpreter.installSegment5>(Compiler::Stats::opcodeSetSkillExplicit + i, id); + + interpreter.installSegment5>(Compiler::Stats::opcodeModSkill + i, id); + interpreter.installSegment5>(Compiler::Stats::opcodeModSkillExplicit + i, id); + } + + interpreter.installSegment5(Compiler::Stats::opcodeGetPCCrimeLevel); + interpreter.installSegment5(Compiler::Stats::opcodeSetPCCrimeLevel); + interpreter.installSegment5(Compiler::Stats::opcodeModPCCrimeLevel); + + interpreter.installSegment5>(Compiler::Stats::opcodeAddSpell); + interpreter.installSegment5>(Compiler::Stats::opcodeAddSpellExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeRemoveSpell); + interpreter.installSegment5>(Compiler::Stats::opcodeRemoveSpellExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeRemoveSpellEffects); + interpreter.installSegment5>( + Compiler::Stats::opcodeRemoveSpellEffectsExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeResurrect); + interpreter.installSegment5>(Compiler::Stats::opcodeResurrectExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeRemoveEffects); + interpreter.installSegment5>(Compiler::Stats::opcodeRemoveEffectsExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeGetSpell); + interpreter.installSegment5>(Compiler::Stats::opcodeGetSpellExplicit); + + interpreter.installSegment3>(Compiler::Stats::opcodePCRaiseRank); + interpreter.installSegment3>(Compiler::Stats::opcodePCLowerRank); + interpreter.installSegment3>(Compiler::Stats::opcodePCJoinFaction); + interpreter.installSegment3>(Compiler::Stats::opcodePCRaiseRankExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodePCLowerRankExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodePCJoinFactionExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodeGetPCRank); + interpreter.installSegment3>(Compiler::Stats::opcodeGetPCRankExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeModDisposition); + interpreter.installSegment5>(Compiler::Stats::opcodeModDispositionExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeSetDisposition); + interpreter.installSegment5>(Compiler::Stats::opcodeSetDispositionExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeGetDisposition); + interpreter.installSegment5>(Compiler::Stats::opcodeGetDispositionExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeGetLevel); + interpreter.installSegment5>(Compiler::Stats::opcodeGetLevelExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeSetLevel); + interpreter.installSegment5>(Compiler::Stats::opcodeSetLevelExplicit); + + interpreter.installSegment5(Compiler::Stats::opcodeGetDeadCount); + + interpreter.installSegment3>(Compiler::Stats::opcodeGetPCFacRep); + interpreter.installSegment3>(Compiler::Stats::opcodeGetPCFacRepExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodeSetPCFacRep); + interpreter.installSegment3>(Compiler::Stats::opcodeSetPCFacRepExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodeModPCFacRep); + interpreter.installSegment3>(Compiler::Stats::opcodeModPCFacRepExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeGetCommonDisease); + interpreter.installSegment5>( + Compiler::Stats::opcodeGetCommonDiseaseExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeGetBlightDisease); + interpreter.installSegment5>( + Compiler::Stats::opcodeGetBlightDiseaseExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeGetRace); + interpreter.installSegment5>(Compiler::Stats::opcodeGetRaceExplicit); + interpreter.installSegment5(Compiler::Stats::opcodeGetWerewolfKills); + + interpreter.installSegment3>(Compiler::Stats::opcodePcExpelled); + interpreter.installSegment3>(Compiler::Stats::opcodePcExpelledExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodePcExpell); + interpreter.installSegment3>(Compiler::Stats::opcodePcExpellExplicit); + interpreter.installSegment3>(Compiler::Stats::opcodePcClearExpelled); + interpreter.installSegment3>(Compiler::Stats::opcodePcClearExpelledExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeRaiseRank); + interpreter.installSegment5>(Compiler::Stats::opcodeRaiseRankExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeLowerRank); + interpreter.installSegment5>(Compiler::Stats::opcodeLowerRankExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeOnDeath); + interpreter.installSegment5>(Compiler::Stats::opcodeOnDeathExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeOnMurder); + interpreter.installSegment5>(Compiler::Stats::opcodeOnMurderExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeOnKnockout); + interpreter.installSegment5>(Compiler::Stats::opcodeOnKnockoutExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeIsWerewolf); + interpreter.installSegment5>(Compiler::Stats::opcodeIsWerewolfExplicit); + + interpreter.installSegment5>(Compiler::Stats::opcodeBecomeWerewolf); + interpreter.installSegment5>( + Compiler::Stats::opcodeBecomeWerewolfExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeUndoWerewolf); + interpreter.installSegment5>(Compiler::Stats::opcodeUndoWerewolfExplicit); + interpreter.installSegment5>( + Compiler::Stats::opcodeSetWerewolfAcrobatics); + interpreter.installSegment5>( + Compiler::Stats::opcodeSetWerewolfAcrobaticsExplicit); + interpreter.installSegment5>(Compiler::Stats::opcodeGetStat); + interpreter.installSegment5>(Compiler::Stats::opcodeGetStatExplicit); static const MagicEffect sMagicEffects[] = { { ESM::MagicEffect::ResistMagicka, ESM::MagicEffect::WeaknessToMagicka }, @@ -1499,20 +1591,30 @@ namespace MWScript { ESM::MagicEffect::Sanctuary, -1 }, }; - for (int i=0; i<24; ++i) + for (int i = 0; i < 24; ++i) { int positive = sMagicEffects[i].mPositiveEffect; int negative = sMagicEffects[i].mNegativeEffect; - interpreter.installSegment5 (Compiler::Stats::opcodeGetMagicEffect+i, new OpGetMagicEffect (positive, negative)); - interpreter.installSegment5 (Compiler::Stats::opcodeGetMagicEffectExplicit+i, new OpGetMagicEffect (positive, negative)); + interpreter.installSegment5>( + Compiler::Stats::opcodeGetMagicEffect + i, positive, negative); + interpreter.installSegment5>( + Compiler::Stats::opcodeGetMagicEffectExplicit + i, positive, negative); - interpreter.installSegment5 (Compiler::Stats::opcodeSetMagicEffect+i, new OpSetMagicEffect (positive, negative)); - interpreter.installSegment5 (Compiler::Stats::opcodeSetMagicEffectExplicit+i, new OpSetMagicEffect (positive, negative)); + interpreter.installSegment5>( + Compiler::Stats::opcodeSetMagicEffect + i, positive, negative); + interpreter.installSegment5>( + Compiler::Stats::opcodeSetMagicEffectExplicit + i, positive, negative); - interpreter.installSegment5 (Compiler::Stats::opcodeModMagicEffect+i, new OpModMagicEffect (positive, negative)); - interpreter.installSegment5 (Compiler::Stats::opcodeModMagicEffectExplicit+i, new OpModMagicEffect (positive, negative)); + interpreter.installSegment5>( + Compiler::Stats::opcodeModMagicEffect + i, positive, negative); + interpreter.installSegment5>( + Compiler::Stats::opcodeModMagicEffectExplicit + i, positive, negative); } + + interpreter.installSegment5(Compiler::Stats::opcodeGetPCVisionBonus); + interpreter.installSegment5(Compiler::Stats::opcodeSetPCVisionBonus); + interpreter.installSegment5(Compiler::Stats::opcodeModPCVisionBonus); } } } diff --git a/apps/openmw/mwscript/statsextensions.hpp b/apps/openmw/mwscript/statsextensions.hpp index 213b5496747..07d6a34f5b5 100644 --- a/apps/openmw/mwscript/statsextensions.hpp +++ b/apps/openmw/mwscript/statsextensions.hpp @@ -15,8 +15,8 @@ namespace MWScript { /// \brief stats-related script functionality (creatures and NPCs) namespace Stats - { - void installOpcodes (Interpreter::Interpreter& interpreter); + { + void installOpcodes(Interpreter::Interpreter& interpreter); } } diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index dbd8e905aac..bf09269f1e0 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -2,23 +2,29 @@ #include -#include +#include + +#include #include #include -#include #include +#include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/player.hpp" +#include "../mwworld/scene.hpp" +#include "../mwworld/worldmodel.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/creaturestats.hpp" #include "interpretercontext.hpp" #include "ref.hpp" @@ -27,750 +33,769 @@ namespace MWScript { namespace Transformation { - void moveStandingActors(const MWWorld::Ptr &ptr, const osg::Vec3f& diff) + void moveStandingActors(const MWWorld::Ptr& ptr, const osg::Vec3f& diff) { std::vector actors; - MWBase::Environment::get().getWorld()->getActorsStandingOn (ptr, actors); + MWBase::Environment::get().getWorld()->getActorsStandingOn(ptr, actors); for (auto& actor : actors) - MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff, false, false); + MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff, false); } - template + template class OpGetDistance : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr from = R()(runtime, !R::implicit); + ESM::RefId name = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - void execute (Interpreter::Runtime& runtime) override + if (from.isEmpty()) { - MWWorld::Ptr from = R()(runtime); - std::string name = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + std::string error = "Missing implicit ref"; + runtime.getContext().report(error); + Log(Debug::Error) << error; + runtime.push(0.f); + return; + } - if (from.getContainerStore()) // is the object contained? - { - MWWorld::Ptr container = MWBase::Environment::get().getWorld()->findContainer(from); - - if (!container.isEmpty()) - from = container; - else - { - std::string error = "Failed to find the container of object '" + from.getCellRef().getRefId() + "'"; - runtime.getContext().report(error); - Log(Debug::Error) << error; - runtime.push(0.f); - return; - } - } + if (from.getContainerStore()) // is the object contained? + { + MWWorld::Ptr container = MWBase::Environment::get().getWorld()->findContainer(from); - const MWWorld::Ptr to = MWBase::Environment::get().getWorld()->searchPtr(name, false); - if (to.isEmpty()) + if (!container.isEmpty()) + from = container; + else { - std::string error = "Failed to find an instance of object '" + name + "'"; + const std::string error + = "Failed to find the container of object " + from.getCellRef().getRefId().toDebugString(); runtime.getContext().report(error); Log(Debug::Error) << error; runtime.push(0.f); return; } + } - float distance; - // If the objects are in different worldspaces, return a large value (just like vanilla) - if (!to.isInCell() || !from.isInCell() || to.getCell()->getCell()->getCellId().mWorldspace != from.getCell()->getCell()->getCellId().mWorldspace) - distance = std::numeric_limits::max(); - else - { - double diff[3]; + const MWWorld::Ptr to = MWBase::Environment::get().getWorld()->searchPtr(name, false); + if (to.isEmpty()) + { + const std::string error = "Failed to find an instance of object " + name.toDebugString(); + runtime.getContext().report(error); + Log(Debug::Error) << error; + runtime.push(0.f); + return; + } - const float* const pos1 = to.getRefData().getPosition().pos; - const float* const pos2 = from.getRefData().getPosition().pos; - for (int i=0; i<3; ++i) - diff[i] = pos1[i] - pos2[i]; + float distance; + // If the objects are in different worldspaces, return a large value (just like vanilla) + if (!to.isInCell() || !from.isInCell() + || to.getCell()->getCell()->getWorldSpace() != from.getCell()->getCell()->getWorldSpace()) + distance = std::numeric_limits::max(); + else + { + double diff[3]; - distance = static_cast(std::sqrt(diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2])); - } + const float* const pos1 = to.getRefData().getPosition().pos; + const float* const pos2 = from.getRefData().getPosition().pos; + for (int i = 0; i < 3; ++i) + diff[i] = pos1[i] - pos2[i]; - runtime.push(distance); + distance = static_cast(std::sqrt(diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2])); } + + runtime.push(distance); + } }; - template + template class OpSetScale : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Float scale = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float scale = runtime[0].mFloat; + runtime.pop(); - MWBase::Environment::get().getWorld()->scaleObject(ptr,scale); - } + MWBase::Environment::get().getWorld()->scaleObject(ptr, scale); + } }; - template + template class OpGetScale : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - runtime.push(ptr.getCellRef().getScale()); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + runtime.push(ptr.getCellRef().getScale()); + } }; - template + template class OpModScale : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Float scale = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float scale = runtime[0].mFloat; + runtime.pop(); - // add the parameter to the object's scale. - MWBase::Environment::get().getWorld()->scaleObject(ptr,ptr.getCellRef().getScale() + scale); - } + // add the parameter to the object's scale. + MWBase::Environment::get().getWorld()->scaleObject(ptr, ptr.getCellRef().getScale() + scale); + } }; - template + template class OpSetAngle : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - Interpreter::Type_Float angle = osg::DegreesToRadians(runtime[0].mFloat); - runtime.pop(); - - float ax = ptr.getRefData().getPosition().rot[0]; - float ay = ptr.getRefData().getPosition().rot[1]; - float az = ptr.getRefData().getPosition().rot[2]; - - // XYZ axis use the inverse (XYZ) rotation order like vanilla SetAngle. - // UWV axis use the standard (ZYX) rotation order like TESCS/OpenMW-CS and the rest of the game. - if (axis == "x") - MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az,MWBase::RotationFlag_inverseOrder); - else if (axis == "y") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az,MWBase::RotationFlag_inverseOrder); - else if (axis == "z") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle,MWBase::RotationFlag_inverseOrder); - else if (axis == "u") - MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az,MWBase::RotationFlag_none); - else if (axis == "w") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az,MWBase::RotationFlag_none); - else if (axis == "v") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle,MWBase::RotationFlag_none); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + Interpreter::Type_Float angle = osg::DegreesToRadians(runtime[0].mFloat); + runtime.pop(); + + float ax = ptr.getRefData().getPosition().rot[0]; + float ay = ptr.getRefData().getPosition().rot[1]; + float az = ptr.getRefData().getPosition().rot[2]; + + // XYZ axis use the inverse (XYZ) rotation order like vanilla SetAngle. + // UVW axis use the standard (ZYX) rotation order like TESCS/OpenMW-CS and the rest of the game. + if (axis == "x") + MWBase::Environment::get().getWorld()->rotateObject( + ptr, osg::Vec3f(angle, ay, az), MWBase::RotationFlag_inverseOrder); + else if (axis == "y") + MWBase::Environment::get().getWorld()->rotateObject( + ptr, osg::Vec3f(ax, angle, az), MWBase::RotationFlag_inverseOrder); + else if (axis == "z") + MWBase::Environment::get().getWorld()->rotateObject( + ptr, osg::Vec3f(ax, ay, angle), MWBase::RotationFlag_inverseOrder); + else if (axis == "u") + MWBase::Environment::get().getWorld()->rotateObject( + ptr, osg::Vec3f(angle, ay, az), MWBase::RotationFlag_none); + else if (axis == "v") + MWBase::Environment::get().getWorld()->rotateObject( + ptr, osg::Vec3f(ax, angle, az), MWBase::RotationFlag_none); + else if (axis == "w") + MWBase::Environment::get().getWorld()->rotateObject( + ptr, osg::Vec3f(ax, ay, angle), MWBase::RotationFlag_none); + } }; - template + template class OpGetStartingAngle : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); - if (axis == "x") + float ret = 0.f; + if (!axis.empty()) + { + if (axis[0] == 'x') { - runtime.push(osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[0])); + ret = osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[0]); } - else if (axis == "y") + else if (axis[0] == 'y') { - runtime.push(osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[1])); + ret = osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[1]); } - else if (axis == "z") + else if (axis[0] == 'z') { - runtime.push(osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[2])); + ret = osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[2]); } } + runtime.push(ret); + } }; - template + template class OpGetAngle : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); - if (axis=="x") + float ret = 0.f; + if (!axis.empty()) + { + if (axis[0] == 'x') { - runtime.push(osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[0])); + ret = osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[0]); } - else if (axis=="y") + else if (axis[0] == 'y') { - runtime.push(osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[1])); + ret = osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[1]); } - else if (axis=="z") + else if (axis[0] == 'z') { - runtime.push(osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[2])); + ret = osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[2]); } } + runtime.push(ret); + } }; - template + template class OpGetPos : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); - if(axis == "x") + float ret = 0.f; + if (!axis.empty()) + { + if (axis[0] == 'x') { - runtime.push(ptr.getRefData().getPosition().pos[0]); + ret = ptr.getRefData().getPosition().pos[0]; } - else if(axis == "y") + else if (axis[0] == 'y') { - runtime.push(ptr.getRefData().getPosition().pos[1]); + ret = ptr.getRefData().getPosition().pos[1]; } - else if(axis == "z") + else if (axis[0] == 'z') { - runtime.push(ptr.getRefData().getPosition().pos[2]); + ret = ptr.getRefData().getPosition().pos[2]; } } + runtime.push(ret); + } }; - template + template class OpSetPos : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - if (!ptr.isInCell()) - return; + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + Interpreter::Type_Float pos = runtime[0].mFloat; + runtime.pop(); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - Interpreter::Type_Float pos = runtime[0].mFloat; - runtime.pop(); + if (!ptr.isInCell()) + return; - // Note: SetPos does not skip weather transitions in vanilla engine, so we do not call setTeleported(true) here. + // Note: SetPos does not skip weather transitions in vanilla engine, so we do not call + // setTeleported(true) here. - const auto curPos = ptr.getRefData().getPosition().asVec3(); - auto newPos = curPos; - if(axis == "x") - { - newPos[0] = pos; - } - else if(axis == "y") - { - newPos[1] = pos; - } - else if(axis == "z") - { - // We should not place actors under ground - if (ptr.getClass().isActor()) - { - float terrainHeight = -std::numeric_limits::max(); - if (ptr.getCell()->isExterior()) - terrainHeight = MWBase::Environment::get().getWorld()->getTerrainHeightAt(curPos); - - if (pos < terrainHeight) - pos = terrainHeight; - } - - newPos[2] = pos; - } - else + const auto curPos = ptr.getRefData().getPosition().asVec3(); + auto newPos = curPos; + if (axis == "x") + { + newPos[0] = pos; + } + else if (axis == "y") + { + newPos[1] = pos; + } + else if (axis == "z") + { + // We should not place actors under ground + if (ptr.getClass().isActor()) { - return; + float terrainHeight = -std::numeric_limits::max(); + if (ptr.getCell()->isExterior()) + terrainHeight = MWBase::Environment::get().getWorld()->getTerrainHeightAt( + curPos, ptr.getCell()->getCell()->getWorldSpace()); + + if (pos < terrainHeight) + pos = terrainHeight; } - dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, newPos - curPos, true, true)); + newPos[2] = pos; } + else + { + return; + } + + dynamic_cast(runtime.getContext()) + .updatePtr(ptr, MWBase::Environment::get().getWorld()->moveObjectBy(ptr, newPos - curPos, true)); + } }; - template + template class OpGetStartingPos : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); - if(axis == "x") + float ret = 0.f; + if (!axis.empty()) + { + if (axis[0] == 'x') { - runtime.push(ptr.getCellRef().getPosition().pos[0]); + ret = ptr.getCellRef().getPosition().pos[0]; } - else if(axis == "y") + else if (axis[0] == 'y') { - runtime.push(ptr.getCellRef().getPosition().pos[1]); + ret = ptr.getCellRef().getPosition().pos[1]; } - else if(axis == "z") + else if (axis[0] == 'z') { - runtime.push(ptr.getCellRef().getPosition().pos[2]); + ret = ptr.getCellRef().getPosition().pos[2]; } } + runtime.push(ret); + } }; - template + template class OpPositionCell : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + + Interpreter::Type_Float x = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float y = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float z = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float zRot = runtime[0].mFloat; + runtime.pop(); + std::string_view cellID = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + + if (ptr.getContainerStore()) + return; + + bool isPlayer = ptr == MWMechanics::getPlayer(); + auto world = MWBase::Environment::get().getWorld(); + auto worldModel = MWBase::Environment::get().getWorldModel(); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).setTeleported(true); + if (isPlayer) + world->getPlayer().setTeleported(true); + + MWWorld::CellStore* store = worldModel->findCell(cellID); + + if (store != nullptr && store->isExterior()) + store = &worldModel->getExterior( + ESM::positionToExteriorCellLocation(x, y, store->getCell()->getWorldSpace())); + + if (store == nullptr) { - MWWorld::Ptr ptr = R()(runtime); - - if (ptr.getContainerStore()) - return; - - if (ptr == MWMechanics::getPlayer()) + // cell not found, move to exterior instead if moving the player (vanilla PositionCell + // compatibility) + std::string error = "PositionCell: unknown interior cell (" + std::string(cellID) + ")"; + if (isPlayer) + error += ", moving to exterior instead"; + runtime.getContext().report(error); + if (!isPlayer) { - MWBase::Environment::get().getWorld()->getPlayer().setTeleported(true); - } - - Interpreter::Type_Float x = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float y = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float z = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float zRot = runtime[0].mFloat; - runtime.pop(); - std::string cellID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - - MWWorld::CellStore* store = nullptr; - try - { - store = MWBase::Environment::get().getWorld()->getInterior(cellID); - } - catch(std::exception&) - { - // cell not found, move to exterior instead (vanilla PositionCell compatibility) - const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getExterior(cellID); - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); - if(!cell) - { - std::string error = "Warning: PositionCell: unknown interior cell (" + cellID + "), moving to exterior instead"; - runtime.getContext().report (error); - Log(Debug::Warning) << error; - } - } - if(store) - { - MWWorld::Ptr base = ptr; - ptr = MWBase::Environment::get().getWorld()->moveObject(ptr,store,x,y,z); - dynamic_cast(runtime.getContext()).updatePtr(base,ptr); - - float ax = ptr.getRefData().getPosition().rot[0]; - float ay = ptr.getRefData().getPosition().rot[1]; - // Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south = 10800, west = 16200) - // except for when you position the player, then degrees must be used. - // See "Morrowind Scripting for Dummies (9th Edition)" pages 50 and 54 for reference. - if(ptr != MWMechanics::getPlayer()) - zRot = zRot/60.0f; - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,osg::DegreesToRadians(zRot)); - - ptr.getClass().adjustPosition(ptr, false); + Log(Debug::Error) << error; + return; } + Log(Debug::Warning) << error; + const ESM::ExteriorCellLocation cellIndex + = ESM::positionToExteriorCellLocation(x, y, ESM::Cell::sDefaultWorldspaceId); + store = &worldModel->getExterior(cellIndex); } + + MWWorld::Ptr base = ptr; + ptr = world->moveObject(ptr, store, osg::Vec3f(x, y, z)); + dynamic_cast(runtime.getContext()).updatePtr(base, ptr); + + auto rot = ptr.getRefData().getPosition().asRotationVec3(); + // Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south + // = 10800, west = 16200) except for when you position the player, then degrees must be used. See + // "Morrowind Scripting for Dummies (9th Edition)" pages 50 and 54 for reference. + if (!isPlayer) + zRot = zRot / 60.0f; + rot.z() = osg::DegreesToRadians(zRot); + world->rotateObject(ptr, rot); + + bool cellActive = MWBase::Environment::get().getWorldScene()->isCellActive(*ptr.getCell()); + ptr.getClass().adjustPosition(ptr, isPlayer || !cellActive); + MWBase::Environment::get().getLuaManager()->objectTeleported(ptr); + } }; - template + template class OpPosition : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + + Interpreter::Type_Float x = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float y = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float z = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float zRot = runtime[0].mFloat; + runtime.pop(); + + if (!ptr.isInCell()) + return; + + bool isPlayer = ptr == MWMechanics::getPlayer(); + auto world = MWBase::Environment::get().getWorld(); + if (ptr.getClass().isActor()) + ptr.getClass().getCreatureStats(ptr).setTeleported(true); + if (isPlayer) + world->getPlayer().setTeleported(true); + const ESM::ExteriorCellLocation location + = ESM::positionToExteriorCellLocation(x, y, ESM::Cell::sDefaultWorldspaceId); + + // another morrowind oddity: player will be moved to the exterior cell at this location, + // non-player actors will move within the cell they are in. + MWWorld::Ptr base = ptr; + if (isPlayer) { - MWWorld::Ptr ptr = R()(runtime); - - if (!ptr.isInCell()) - return; - - if (ptr == MWMechanics::getPlayer()) - { - MWBase::Environment::get().getWorld()->getPlayer().setTeleported(true); - } - - Interpreter::Type_Float x = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float y = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float z = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float zRot = runtime[0].mFloat; - runtime.pop(); - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - - // another morrowind oddity: player will be moved to the exterior cell at this location, - // non-player actors will move within the cell they are in. - MWWorld::Ptr base = ptr; - if (ptr == MWMechanics::getPlayer()) - { - MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(cx,cy); - ptr = MWBase::Environment::get().getWorld()->moveObject(ptr,cell,x,y,z); - } - else - { - ptr = MWBase::Environment::get().getWorld()->moveObject(ptr, x, y, z, true, true); - } - dynamic_cast(runtime.getContext()).updatePtr(base,ptr); - - float ax = ptr.getRefData().getPosition().rot[0]; - float ay = ptr.getRefData().getPosition().rot[1]; - // Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south = 10800, west = 16200) - // except for when you position the player, then degrees must be used. - // See "Morrowind Scripting for Dummies (9th Edition)" pages 50 and 54 for reference. - if(ptr != MWMechanics::getPlayer()) - zRot = zRot/60.0f; - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,osg::DegreesToRadians(zRot)); - ptr.getClass().adjustPosition(ptr, false); + MWWorld::CellStore* cell = &MWBase::Environment::get().getWorldModel()->getExterior(location); + ptr = world->moveObject(ptr, cell, osg::Vec3(x, y, z)); } + else + { + ptr = world->moveObject(ptr, osg::Vec3f(x, y, z), true, true); + } + dynamic_cast(runtime.getContext()).updatePtr(base, ptr); + + auto rot = ptr.getRefData().getPosition().asRotationVec3(); + // Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south = + // 10800, west = 16200) except for when you position the player, then degrees must be used. See + // "Morrowind Scripting for Dummies (9th Edition)" pages 50 and 54 for reference. + if (!isPlayer) + zRot = zRot / 60.0f; + rot.z() = osg::DegreesToRadians(zRot); + world->rotateObject(ptr, rot); + bool cellActive = MWBase::Environment::get().getWorldScene()->isCellActive(*ptr.getCell()); + ptr.getClass().adjustPosition(ptr, isPlayer || !cellActive); + MWBase::Environment::get().getLuaManager()->objectTeleported(ptr); + } }; class OpPlaceItemCell : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override + public: + void execute(Interpreter::Runtime& runtime) override + { + const ESM::RefId itemID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); + std::string_view cellName = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + + Interpreter::Type_Float x = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float y = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float z = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float zRotDegrees = runtime[0].mFloat; + runtime.pop(); + + MWWorld::CellStore* const store = MWBase::Environment::get().getWorldModel()->findCell(cellName); + if (store == nullptr) { - std::string itemID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - std::string cellID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - - Interpreter::Type_Float x = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float y = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float z = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float zRotDegrees = runtime[0].mFloat; - runtime.pop(); - - MWWorld::CellStore* store = nullptr; - try - { - store = MWBase::Environment::get().getWorld()->getInterior(cellID); - } - catch(std::exception&) - { - const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getExterior(cellID); - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); - if(!cell) - { - runtime.getContext().report ("unknown cell (" + cellID + ")"); - Log(Debug::Error) << "Error: unknown cell (" << cellID << ")"; - } - } - if(store) - { - ESM::Position pos; - pos.pos[0] = x; - pos.pos[1] = y; - pos.pos[2] = z; - pos.rot[0] = pos.rot[1] = 0; - pos.rot[2] = osg::DegreesToRadians(zRotDegrees); - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); - ref.getPtr().getCellRef().setPosition(pos); - MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject(ref.getPtr(),store,pos); - placed.getClass().adjustPosition(placed, true); - } + const std::string message = "unknown cell (" + std::string(cellName) + ")"; + runtime.getContext().report(message); + Log(Debug::Error) << message; + return; } + + ESM::Position pos; + pos.pos[0] = x; + pos.pos[1] = y; + pos.pos[2] = z; + pos.rot[0] = pos.rot[1] = 0; + pos.rot[2] = osg::DegreesToRadians(zRotDegrees); + MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), itemID); + ref.getPtr().mRef->mData.mPhysicsPostponed = !ref.getPtr().getClass().isActor(); + ref.getPtr().getCellRef().setPosition(pos); + MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject(ref.getPtr(), store, pos); + placed.getClass().adjustPosition(placed, true); + } }; class OpPlaceItem : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - std::string itemID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + public: + void execute(Interpreter::Runtime& runtime) override + { + ESM::RefId itemID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - Interpreter::Type_Float x = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float y = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float z = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Float zRotDegrees = runtime[0].mFloat; - runtime.pop(); + Interpreter::Type_Float x = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float y = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float z = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Float zRotDegrees = runtime[0].mFloat; + runtime.pop(); - MWWorld::Ptr player = MWMechanics::getPlayer(); + MWWorld::Ptr player = MWMechanics::getPlayer(); - if (!player.isInCell()) - throw std::runtime_error("player not in a cell"); + if (!player.isInCell()) + throw std::runtime_error("player not in a cell"); - MWWorld::CellStore* store = nullptr; - if (player.getCell()->isExterior()) - { - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); - } - else - store = player.getCell(); - - ESM::Position pos; - pos.pos[0] = x; - pos.pos[1] = y; - pos.pos[2] = z; - pos.rot[0] = pos.rot[1] = 0; - pos.rot[2] = osg::DegreesToRadians(zRotDegrees); - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); - ref.getPtr().getCellRef().setPosition(pos); - MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject(ref.getPtr(),store,pos); - placed.getClass().adjustPosition(placed, true); + MWWorld::CellStore* store = nullptr; + if (player.getCell()->isExterior()) + { + const ESM::ExteriorCellLocation cellIndex + = ESM::positionToExteriorCellLocation(x, y, player.getCell()->getCell()->getWorldSpace()); + store = &MWBase::Environment::get().getWorldModel()->getExterior(cellIndex); } + else + store = player.getCell(); + + ESM::Position pos; + pos.pos[0] = x; + pos.pos[1] = y; + pos.pos[2] = z; + pos.rot[0] = pos.rot[1] = 0; + pos.rot[2] = osg::DegreesToRadians(zRotDegrees); + MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), itemID); + ref.getPtr().mRef->mData.mPhysicsPostponed = !ref.getPtr().getClass().isActor(); + ref.getPtr().getCellRef().setPosition(pos); + MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->placeObject(ref.getPtr(), store, pos); + placed.getClass().adjustPosition(placed, true); + } }; - template + template class OpPlaceAt : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr actor = pc - ? MWMechanics::getPlayer() - : R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr actor = pc ? MWMechanics::getPlayer() : R()(runtime); - std::string itemID = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + ESM::RefId itemID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); + runtime.pop(); - Interpreter::Type_Integer count = runtime[0].mInteger; - runtime.pop(); - Interpreter::Type_Float distance = runtime[0].mFloat; - runtime.pop(); - Interpreter::Type_Integer direction = runtime[0].mInteger; - runtime.pop(); + Interpreter::Type_Integer count = runtime[0].mInteger; + runtime.pop(); + Interpreter::Type_Float distance = runtime[0].mFloat; + runtime.pop(); + Interpreter::Type_Integer direction = runtime[0].mInteger; + runtime.pop(); - if (direction < 0 || direction > 3) - throw std::runtime_error ("invalid direction"); + if (direction < 0 || direction > 3) + throw std::runtime_error("invalid direction"); - if (count<0) - throw std::runtime_error ("count must be non-negative"); + if (count < 0) + throw std::runtime_error("count must be non-negative"); - if (!actor.isInCell()) - throw std::runtime_error ("actor is not in a cell"); + if (!actor.isInCell()) + throw std::runtime_error("actor is not in a cell"); - for (int i=0; igetStore(), itemID, 1); + for (int i = 0; i < count; ++i) + { + // create item + MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), itemID, 1); + ref.getPtr().mRef->mData.mPhysicsPostponed = !ref.getPtr().getClass().isActor(); - MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), actor, actor.getCell(), direction, distance); - MWBase::Environment::get().getWorld()->scaleObject(ptr, actor.getCellRef().getScale()); - } + MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->safePlaceObject( + ref.getPtr(), actor, actor.getCell(), direction, distance); + MWBase::Environment::get().getWorld()->scaleObject(ptr, actor.getCellRef().getScale()); } + } }; - template + template class OpRotate : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - const MWWorld::Ptr& ptr = R()(runtime); - - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - Interpreter::Type_Float rotation = osg::DegreesToRadians(runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); - runtime.pop(); - - float ax = ptr.getRefData().getPosition().rot[0]; - float ay = ptr.getRefData().getPosition().rot[1]; - float az = ptr.getRefData().getPosition().rot[2]; - - if (axis == "x") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax+rotation,ay,az); - else if (axis == "y") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay+rotation,az); - else if (axis == "z") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,az+rotation); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + const MWWorld::Ptr& ptr = R()(runtime); + + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + Interpreter::Type_Float rotation + = osg::DegreesToRadians(runtime[0].mFloat * MWBase::Environment::get().getFrameDuration()); + runtime.pop(); + + auto rot = ptr.getRefData().getPosition().asRotationVec3(); + // Regardless of the axis argument, the player may only be rotated on Z + if (axis == "z" || MWMechanics::getPlayer() == ptr) + rot.z() += rotation; + else if (axis == "x") + rot.x() += rotation; + else if (axis == "y") + rot.y() += rotation; + MWBase::Environment::get().getWorld()->rotateObject(ptr, rot); + } }; - template + template class OpRotateWorld : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - Interpreter::Type_Float rotation = osg::DegreesToRadians(runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); - runtime.pop(); - - if (!ptr.getRefData().getBaseNode()) - return; - - // We can rotate actors only around Z axis - if (ptr.getClass().isActor() && (axis == "x" || axis == "y")) - return; - - osg::Quat rot; - if (axis == "x") - rot = osg::Quat(rotation, -osg::X_AXIS); - else if (axis == "y") - rot = osg::Quat(rotation, -osg::Y_AXIS); - else if (axis == "z") - rot = osg::Quat(rotation, -osg::Z_AXIS); - else - return; - - osg::Quat attitude = ptr.getRefData().getBaseNode()->getAttitude(); - MWBase::Environment::get().getWorld()->rotateWorldObject(ptr, attitude * rot); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + Interpreter::Type_Float rotation + = osg::DegreesToRadians(runtime[0].mFloat * MWBase::Environment::get().getFrameDuration()); + runtime.pop(); + + if (!ptr.getRefData().getBaseNode()) + return; + + // We can rotate actors only around Z axis + if (ptr.getClass().isActor() && (axis == "x" || axis == "y")) + return; + + osg::Quat rot; + if (axis == "x") + rot = osg::Quat(rotation, -osg::X_AXIS); + else if (axis == "y") + rot = osg::Quat(rotation, -osg::Y_AXIS); + else if (axis == "z") + rot = osg::Quat(rotation, -osg::Z_AXIS); + else + return; + + osg::Quat attitude = ptr.getRefData().getBaseNode()->getAttitude(); + MWBase::Environment::get().getWorld()->rotateWorldObject(ptr, attitude * rot); + } }; - template + template class OpSetAtStart : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - if (!ptr.isInCell()) - return; - - float xr = ptr.getCellRef().getPosition().rot[0]; - float yr = ptr.getCellRef().getPosition().rot[1]; - float zr = ptr.getCellRef().getPosition().rot[2]; + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); - MWBase::Environment::get().getWorld()->rotateObject(ptr, xr, yr, zr); + if (!ptr.isInCell()) + return; - dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], - ptr.getCellRef().getPosition().pos[1], ptr.getCellRef().getPosition().pos[2])); + MWBase::Environment::get().getWorld()->rotateObject( + ptr, ptr.getCellRef().getPosition().asRotationVec3()); - } + dynamic_cast(runtime.getContext()) + .updatePtr(ptr, + MWBase::Environment::get().getWorld()->moveObject( + ptr, ptr.getCellRef().getPosition().asVec3())); + } }; - template + template class OpMove : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - const MWWorld::Ptr& ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + const MWWorld::Ptr& ptr = R()(runtime); - if (!ptr.isInCell()) - return; + if (!ptr.isInCell()) + return; - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - Interpreter::Type_Float movement = (runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); - runtime.pop(); + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + Interpreter::Type_Float movement = (runtime[0].mFloat * MWBase::Environment::get().getFrameDuration()); + runtime.pop(); - osg::Vec3f posChange; - if (axis == "x") - { - posChange=osg::Vec3f(movement, 0, 0); - } - else if (axis == "y") - { - posChange=osg::Vec3f(0, movement, 0); - } - else if (axis == "z") - { - posChange=osg::Vec3f(0, 0, movement); - } - else - return; + osg::Vec3f posChange; + if (axis == "x") + { + posChange = osg::Vec3f(movement, 0, 0); + } + else if (axis == "y") + { + posChange = osg::Vec3f(0, movement, 0); + } + else if (axis == "z") + { + posChange = osg::Vec3f(0, 0, movement); + } + else + return; - // is it correct that disabled objects can't be Move-d? - if (!ptr.getRefData().getBaseNode()) - return; + // is it correct that disabled objects can't be Move-d? + if (!ptr.getRefData().getBaseNode()) + return; - osg::Vec3f diff = ptr.getRefData().getBaseNode()->getAttitude() * posChange; + osg::Vec3f diff = ptr.getRefData().getBaseNode()->getAttitude() * posChange; - // We should move actors, standing on moving object, too. - // This approach can be used to create elevators. - moveStandingActors(ptr, diff); - dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false, true)); - } + // We should move actors, standing on moving object, too. + // This approach can be used to create elevators. + moveStandingActors(ptr, diff); + dynamic_cast(runtime.getContext()) + .updatePtr(ptr, MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false)); + } }; - template + template class OpMoveWorld : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - MWWorld::Ptr ptr = R()(runtime); - - if (!ptr.isInCell()) - return; - - std::string axis = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); - Interpreter::Type_Float movement = (runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); - runtime.pop(); - - osg::Vec3f diff; - - if (axis == "x") - diff.x() = movement; - else if (axis == "y") - diff.y() = movement; - else if (axis == "z") - diff.z() = movement; - else - return; - - // We should move actors, standing on moving object, too. - // This approach can be used to create elevators. - moveStandingActors(ptr, diff); - dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false, true)); - } + public: + void execute(Interpreter::Runtime& runtime) override + { + MWWorld::Ptr ptr = R()(runtime); + + if (!ptr.isInCell()) + return; + + std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + Interpreter::Type_Float movement = (runtime[0].mFloat * MWBase::Environment::get().getFrameDuration()); + runtime.pop(); + + osg::Vec3f diff; + + if (axis == "x") + diff.x() = movement; + else if (axis == "y") + diff.y() = movement; + else if (axis == "z") + diff.z() = movement; + else + return; + + // We should move actors, standing on moving object, too. + // This approach can be used to create elevators. + moveStandingActors(ptr, diff); + dynamic_cast(runtime.getContext()) + .updatePtr(ptr, MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff, false)); + } }; class OpResetActors : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWorld()->resetActors(); } @@ -779,56 +804,62 @@ namespace MWScript class OpFixme : public Interpreter::Opcode0 { public: - - void execute (Interpreter::Runtime& runtime) override + void execute(Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWorld()->fixPosition(); } }; - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment5(Compiler::Transformation::opcodeGetDistance, new OpGetDistance); - interpreter.installSegment5(Compiler::Transformation::opcodeGetDistanceExplicit, new OpGetDistance); - interpreter.installSegment5(Compiler::Transformation::opcodeSetScale,new OpSetScale); - interpreter.installSegment5(Compiler::Transformation::opcodeSetScaleExplicit,new OpSetScale); - interpreter.installSegment5(Compiler::Transformation::opcodeSetAngle,new OpSetAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeSetAngleExplicit,new OpSetAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeGetScale,new OpGetScale); - interpreter.installSegment5(Compiler::Transformation::opcodeGetScaleExplicit,new OpGetScale); - interpreter.installSegment5(Compiler::Transformation::opcodeGetAngle,new OpGetAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeGetAngleExplicit,new OpGetAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeGetPos,new OpGetPos); - interpreter.installSegment5(Compiler::Transformation::opcodeGetPosExplicit,new OpGetPos); - interpreter.installSegment5(Compiler::Transformation::opcodeSetPos,new OpSetPos); - interpreter.installSegment5(Compiler::Transformation::opcodeSetPosExplicit,new OpSetPos); - interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingPos,new OpGetStartingPos); - interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingPosExplicit,new OpGetStartingPos); - interpreter.installSegment5(Compiler::Transformation::opcodePosition,new OpPosition); - interpreter.installSegment5(Compiler::Transformation::opcodePositionExplicit,new OpPosition); - interpreter.installSegment5(Compiler::Transformation::opcodePositionCell,new OpPositionCell); - interpreter.installSegment5(Compiler::Transformation::opcodePositionCellExplicit,new OpPositionCell); - interpreter.installSegment5(Compiler::Transformation::opcodePlaceItemCell,new OpPlaceItemCell); - interpreter.installSegment5(Compiler::Transformation::opcodePlaceItem,new OpPlaceItem); - interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtPc,new OpPlaceAt); - interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtMe,new OpPlaceAt); - interpreter.installSegment5(Compiler::Transformation::opcodePlaceAtMeExplicit,new OpPlaceAt); - interpreter.installSegment5(Compiler::Transformation::opcodeModScale,new OpModScale); - interpreter.installSegment5(Compiler::Transformation::opcodeModScaleExplicit,new OpModScale); - interpreter.installSegment5(Compiler::Transformation::opcodeRotate,new OpRotate); - interpreter.installSegment5(Compiler::Transformation::opcodeRotateExplicit,new OpRotate); - interpreter.installSegment5(Compiler::Transformation::opcodeRotateWorld,new OpRotateWorld); - interpreter.installSegment5(Compiler::Transformation::opcodeRotateWorldExplicit,new OpRotateWorld); - interpreter.installSegment5(Compiler::Transformation::opcodeSetAtStart,new OpSetAtStart); - interpreter.installSegment5(Compiler::Transformation::opcodeSetAtStartExplicit,new OpSetAtStart); - interpreter.installSegment5(Compiler::Transformation::opcodeMove,new OpMove); - interpreter.installSegment5(Compiler::Transformation::opcodeMoveExplicit,new OpMove); - interpreter.installSegment5(Compiler::Transformation::opcodeMoveWorld,new OpMoveWorld); - interpreter.installSegment5(Compiler::Transformation::opcodeMoveWorldExplicit,new OpMoveWorld); - interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingAngle, new OpGetStartingAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingAngleExplicit, new OpGetStartingAngle); - interpreter.installSegment5(Compiler::Transformation::opcodeResetActors, new OpResetActors); - interpreter.installSegment5(Compiler::Transformation::opcodeFixme, new OpFixme); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetDistance); + interpreter.installSegment5>( + Compiler::Transformation::opcodeGetDistanceExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetScale); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetScaleExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetAngle); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetAngleExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetScale); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetScaleExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetAngle); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetAngleExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetPos); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetPosExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetPos); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetPosExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeGetStartingPos); + interpreter.installSegment5>( + Compiler::Transformation::opcodeGetStartingPosExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodePosition); + interpreter.installSegment5>(Compiler::Transformation::opcodePositionExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodePositionCell); + interpreter.installSegment5>( + Compiler::Transformation::opcodePositionCellExplicit); + interpreter.installSegment5(Compiler::Transformation::opcodePlaceItemCell); + interpreter.installSegment5(Compiler::Transformation::opcodePlaceItem); + interpreter.installSegment5>(Compiler::Transformation::opcodePlaceAtPc); + interpreter.installSegment5>(Compiler::Transformation::opcodePlaceAtMe); + interpreter.installSegment5>( + Compiler::Transformation::opcodePlaceAtMeExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeModScale); + interpreter.installSegment5>(Compiler::Transformation::opcodeModScaleExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeRotate); + interpreter.installSegment5>(Compiler::Transformation::opcodeRotateExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeRotateWorld); + interpreter.installSegment5>( + Compiler::Transformation::opcodeRotateWorldExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetAtStart); + interpreter.installSegment5>(Compiler::Transformation::opcodeSetAtStartExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeMove); + interpreter.installSegment5>(Compiler::Transformation::opcodeMoveExplicit); + interpreter.installSegment5>(Compiler::Transformation::opcodeMoveWorld); + interpreter.installSegment5>(Compiler::Transformation::opcodeMoveWorldExplicit); + interpreter.installSegment5>( + Compiler::Transformation::opcodeGetStartingAngle); + interpreter.installSegment5>( + Compiler::Transformation::opcodeGetStartingAngleExplicit); + interpreter.installSegment5(Compiler::Transformation::opcodeResetActors); + interpreter.installSegment5(Compiler::Transformation::opcodeFixme); } } } diff --git a/apps/openmw/mwscript/transformationextensions.hpp b/apps/openmw/mwscript/transformationextensions.hpp index 7a4d29e06ca..949431b1091 100644 --- a/apps/openmw/mwscript/transformationextensions.hpp +++ b/apps/openmw/mwscript/transformationextensions.hpp @@ -15,8 +15,8 @@ namespace MWScript { /// \brief stats-related script functionality (creatures and NPCs) namespace Transformation - { - void installOpcodes (Interpreter::Interpreter& interpreter); + { + void installOpcodes(Interpreter::Interpreter& interpreter); } } diff --git a/apps/openmw/mwscript/userextensions.cpp b/apps/openmw/mwscript/userextensions.cpp index 3f443304d7e..91e04e5f1a1 100644 --- a/apps/openmw/mwscript/userextensions.cpp +++ b/apps/openmw/mwscript/userextensions.cpp @@ -1,12 +1,11 @@ #include "userextensions.hpp" -#include #include +#include #include -#include #include -#include +#include #include "ref.hpp" @@ -19,59 +18,48 @@ namespace MWScript { class OpUser1 : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.getContext().report ("user1: not in use"); - } + public: + void execute(Interpreter::Runtime& runtime) override { runtime.getContext().report("user1: not in use"); } }; class OpUser2 : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { - runtime.getContext().report ("user2: not in use"); - } + public: + void execute(Interpreter::Runtime& runtime) override { runtime.getContext().report("user2: not in use"); } }; - template + template class OpUser3 : public Interpreter::Opcode0 { - public: + public: + void execute(Interpreter::Runtime& runtime) override + { + // MWWorld::Ptr ptr = R()(runtime); - void execute (Interpreter::Runtime& runtime) override - { -// MWWorld::Ptr ptr = R()(runtime); - - runtime.getContext().report ("user3: not in use"); - } + runtime.getContext().report("user3: not in use"); + } }; - template + template class OpUser4 : public Interpreter::Opcode0 { - public: - - void execute (Interpreter::Runtime& runtime) override - { -// MWWorld::Ptr ptr = R()(runtime); + public: + void execute(Interpreter::Runtime& runtime) override + { + // MWWorld::Ptr ptr = R()(runtime); - runtime.getContext().report ("user4: not in use"); - } + runtime.getContext().report("user4: not in use"); + } }; - - void installOpcodes (Interpreter::Interpreter& interpreter) + void installOpcodes(Interpreter::Interpreter& interpreter) { - interpreter.installSegment5 (Compiler::User::opcodeUser1, new OpUser1); - interpreter.installSegment5 (Compiler::User::opcodeUser2, new OpUser2); - interpreter.installSegment5 (Compiler::User::opcodeUser3, new OpUser3); - interpreter.installSegment5 (Compiler::User::opcodeUser3Explicit, new OpUser3); - interpreter.installSegment5 (Compiler::User::opcodeUser4, new OpUser4); - interpreter.installSegment5 (Compiler::User::opcodeUser4Explicit, new OpUser4); + interpreter.installSegment5(Compiler::User::opcodeUser1); + interpreter.installSegment5(Compiler::User::opcodeUser2); + interpreter.installSegment5>(Compiler::User::opcodeUser3); + interpreter.installSegment5>(Compiler::User::opcodeUser3Explicit); + interpreter.installSegment5>(Compiler::User::opcodeUser4); + interpreter.installSegment5>(Compiler::User::opcodeUser4Explicit); } } } diff --git a/apps/openmw/mwscript/userextensions.hpp b/apps/openmw/mwscript/userextensions.hpp index da6e0faa666..2310c2e9a2b 100644 --- a/apps/openmw/mwscript/userextensions.hpp +++ b/apps/openmw/mwscript/userextensions.hpp @@ -16,7 +16,7 @@ namespace MWScript /// \brief Temporary script functionality limited to the console namespace User { - void installOpcodes (Interpreter::Interpreter& interpreter); + void installOpcodes(Interpreter::Interpreter& interpreter); } } diff --git a/apps/openmw/mwsound/alext.h b/apps/openmw/mwsound/alext.h index 7162fa955de..f30cbbdc631 100644 --- a/apps/openmw/mwsound/alext.h +++ b/apps/openmw/mwsound/alext.h @@ -1,141 +1,150 @@ -/** - * OpenAL cross platform audio library - * Copyright (C) 2008 by authors. - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - * Or go to https://www.gnu.org/copyleft/lgpl.html - */ - #ifndef AL_ALEXT_H #define AL_ALEXT_H #include -/* Define int64_t and uint64_t types */ -#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L -#include -#elif defined(_WIN32) && defined(__GNUC__) +/* Define int64 and uint64 types */ +#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || (defined(__cplusplus) && __cplusplus >= 201103L) #include +typedef int64_t _alsoft_int64_t; +typedef uint64_t _alsoft_uint64_t; #elif defined(_WIN32) -typedef __int64 int64_t; -typedef unsigned __int64 uint64_t; +typedef __int64 _alsoft_int64_t; +typedef unsigned __int64 _alsoft_uint64_t; #else /* Fallback if nothing above works */ -#include +#include +typedef int64_t _alsoft_int64_t; +typedef uint64_t _alsoft_uint64_t; #endif -#include "alc.h" #include "al.h" +#include "alc.h" #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif #ifndef AL_LOKI_IMA_ADPCM_format #define AL_LOKI_IMA_ADPCM_format 1 -#define AL_FORMAT_IMA_ADPCM_MONO16_EXT 0x10000 -#define AL_FORMAT_IMA_ADPCM_STEREO16_EXT 0x10001 +#define AL_FORMAT_IMA_ADPCM_MONO16_EXT 0x10000 +#define AL_FORMAT_IMA_ADPCM_STEREO16_EXT 0x10001 #endif #ifndef AL_LOKI_WAVE_format #define AL_LOKI_WAVE_format 1 -#define AL_FORMAT_WAVE_EXT 0x10002 +#define AL_FORMAT_WAVE_EXT 0x10002 #endif #ifndef AL_EXT_vorbis #define AL_EXT_vorbis 1 -#define AL_FORMAT_VORBIS_EXT 0x10003 +#define AL_FORMAT_VORBIS_EXT 0x10003 #endif #ifndef AL_LOKI_quadriphonic #define AL_LOKI_quadriphonic 1 -#define AL_FORMAT_QUAD8_LOKI 0x10004 -#define AL_FORMAT_QUAD16_LOKI 0x10005 +#define AL_FORMAT_QUAD8_LOKI 0x10004 +#define AL_FORMAT_QUAD16_LOKI 0x10005 #endif #ifndef AL_EXT_float32 #define AL_EXT_float32 1 -#define AL_FORMAT_MONO_FLOAT32 0x10010 -#define AL_FORMAT_STEREO_FLOAT32 0x10011 +#define AL_FORMAT_MONO_FLOAT32 0x10010 +#define AL_FORMAT_STEREO_FLOAT32 0x10011 #endif #ifndef AL_EXT_double #define AL_EXT_double 1 -#define AL_FORMAT_MONO_DOUBLE_EXT 0x10012 -#define AL_FORMAT_STEREO_DOUBLE_EXT 0x10013 +#define AL_FORMAT_MONO_DOUBLE_EXT 0x10012 +#define AL_FORMAT_STEREO_DOUBLE_EXT 0x10013 #endif #ifndef AL_EXT_MULAW #define AL_EXT_MULAW 1 -#define AL_FORMAT_MONO_MULAW_EXT 0x10014 -#define AL_FORMAT_STEREO_MULAW_EXT 0x10015 +#define AL_FORMAT_MONO_MULAW_EXT 0x10014 +#define AL_FORMAT_STEREO_MULAW_EXT 0x10015 #endif #ifndef AL_EXT_ALAW #define AL_EXT_ALAW 1 -#define AL_FORMAT_MONO_ALAW_EXT 0x10016 -#define AL_FORMAT_STEREO_ALAW_EXT 0x10017 +#define AL_FORMAT_MONO_ALAW_EXT 0x10016 +#define AL_FORMAT_STEREO_ALAW_EXT 0x10017 #endif #ifndef ALC_LOKI_audio_channel #define ALC_LOKI_audio_channel 1 -#define ALC_CHAN_MAIN_LOKI 0x500001 -#define ALC_CHAN_PCM_LOKI 0x500002 -#define ALC_CHAN_CD_LOKI 0x500003 +#define ALC_CHAN_MAIN_LOKI 0x500001 +#define ALC_CHAN_PCM_LOKI 0x500002 +#define ALC_CHAN_CD_LOKI 0x500003 #endif #ifndef AL_EXT_MCFORMATS #define AL_EXT_MCFORMATS 1 -#define AL_FORMAT_QUAD8 0x1204 -#define AL_FORMAT_QUAD16 0x1205 -#define AL_FORMAT_QUAD32 0x1206 -#define AL_FORMAT_REAR8 0x1207 -#define AL_FORMAT_REAR16 0x1208 -#define AL_FORMAT_REAR32 0x1209 -#define AL_FORMAT_51CHN8 0x120A -#define AL_FORMAT_51CHN16 0x120B -#define AL_FORMAT_51CHN32 0x120C -#define AL_FORMAT_61CHN8 0x120D -#define AL_FORMAT_61CHN16 0x120E -#define AL_FORMAT_61CHN32 0x120F -#define AL_FORMAT_71CHN8 0x1210 -#define AL_FORMAT_71CHN16 0x1211 -#define AL_FORMAT_71CHN32 0x1212 +/* Provides support for surround sound buffer formats with 8, 16, and 32-bit + * samples. + * + * QUAD8: Unsigned 8-bit, Quadraphonic (Front Left, Front Right, Rear Left, + * Rear Right). + * QUAD16: Signed 16-bit, Quadraphonic. + * QUAD32: 32-bit float, Quadraphonic. + * REAR8: Unsigned 8-bit, Rear Stereo (Rear Left, Rear Right). + * REAR16: Signed 16-bit, Rear Stereo. + * REAR32: 32-bit float, Rear Stereo. + * 51CHN8: Unsigned 8-bit, 5.1 Surround (Front Left, Front Right, Front Center, + * LFE, Side Left, Side Right). Note that some audio systems may label + * 5.1's Side channels as Rear or Surround; they are equivalent for the + * purposes of this extension. + * 51CHN16: Signed 16-bit, 5.1 Surround. + * 51CHN32: 32-bit float, 5.1 Surround. + * 61CHN8: Unsigned 8-bit, 6.1 Surround (Front Left, Front Right, Front Center, + * LFE, Rear Center, Side Left, Side Right). + * 61CHN16: Signed 16-bit, 6.1 Surround. + * 61CHN32: 32-bit float, 6.1 Surround. + * 71CHN8: Unsigned 8-bit, 7.1 Surround (Front Left, Front Right, Front Center, + * LFE, Rear Left, Rear Right, Side Left, Side Right). + * 71CHN16: Signed 16-bit, 7.1 Surround. + * 71CHN32: 32-bit float, 7.1 Surround. + */ +#define AL_FORMAT_QUAD8 0x1204 +#define AL_FORMAT_QUAD16 0x1205 +#define AL_FORMAT_QUAD32 0x1206 +#define AL_FORMAT_REAR8 0x1207 +#define AL_FORMAT_REAR16 0x1208 +#define AL_FORMAT_REAR32 0x1209 +#define AL_FORMAT_51CHN8 0x120A +#define AL_FORMAT_51CHN16 0x120B +#define AL_FORMAT_51CHN32 0x120C +#define AL_FORMAT_61CHN8 0x120D +#define AL_FORMAT_61CHN16 0x120E +#define AL_FORMAT_61CHN32 0x120F +#define AL_FORMAT_71CHN8 0x1210 +#define AL_FORMAT_71CHN16 0x1211 +#define AL_FORMAT_71CHN32 0x1212 #endif #ifndef AL_EXT_MULAW_MCFORMATS #define AL_EXT_MULAW_MCFORMATS 1 -#define AL_FORMAT_MONO_MULAW 0x10014 -#define AL_FORMAT_STEREO_MULAW 0x10015 -#define AL_FORMAT_QUAD_MULAW 0x10021 -#define AL_FORMAT_REAR_MULAW 0x10022 -#define AL_FORMAT_51CHN_MULAW 0x10023 -#define AL_FORMAT_61CHN_MULAW 0x10024 -#define AL_FORMAT_71CHN_MULAW 0x10025 +#define AL_FORMAT_MONO_MULAW 0x10014 +#define AL_FORMAT_STEREO_MULAW 0x10015 +#define AL_FORMAT_QUAD_MULAW 0x10021 +#define AL_FORMAT_REAR_MULAW 0x10022 +#define AL_FORMAT_51CHN_MULAW 0x10023 +#define AL_FORMAT_61CHN_MULAW 0x10024 +#define AL_FORMAT_71CHN_MULAW 0x10025 #endif #ifndef AL_EXT_IMA4 #define AL_EXT_IMA4 1 -#define AL_FORMAT_MONO_IMA4 0x1300 -#define AL_FORMAT_STEREO_IMA4 0x1301 +#define AL_FORMAT_MONO_IMA4 0x1300 +#define AL_FORMAT_STEREO_IMA4 0x1301 #endif #ifndef AL_EXT_STATIC_BUFFER #define AL_EXT_STATIC_BUFFER 1 -typedef ALvoid (AL_APIENTRY*PFNALBUFFERDATASTATICPROC)(const ALint,ALenum,ALvoid*,ALsizei,ALsizei); + typedef void(AL_APIENTRY* PFNALBUFFERDATASTATICPROC)(const ALint, ALenum, ALvoid*, ALsizei, ALsizei); #ifdef AL_ALEXT_PROTOTYPES -AL_API ALvoid AL_APIENTRY alBufferDataStatic(const ALint buffer, ALenum format, ALvoid *data, ALsizei len, ALsizei freq); + AL_API void AL_APIENTRY alBufferDataStatic( + const ALint buffer, ALenum format, ALvoid* data, ALsizei len, ALsizei freq); #endif #endif @@ -146,234 +155,244 @@ AL_API ALvoid AL_APIENTRY alBufferDataStatic(const ALint buffer, ALenum format, #ifndef ALC_EXT_disconnect #define ALC_EXT_disconnect 1 -#define ALC_CONNECTED 0x313 +#define ALC_CONNECTED 0x313 #endif #ifndef ALC_EXT_thread_local_context #define ALC_EXT_thread_local_context 1 -typedef ALCboolean (ALC_APIENTRY*PFNALCSETTHREADCONTEXTPROC)(ALCcontext *context); -typedef ALCcontext* (ALC_APIENTRY*PFNALCGETTHREADCONTEXTPROC)(void); + typedef ALCboolean(ALC_APIENTRY* PFNALCSETTHREADCONTEXTPROC)(ALCcontext* context); + typedef ALCcontext*(ALC_APIENTRY* PFNALCGETTHREADCONTEXTPROC)(void); #ifdef AL_ALEXT_PROTOTYPES -ALC_API ALCboolean ALC_APIENTRY alcSetThreadContext(ALCcontext *context); -ALC_API ALCcontext* ALC_APIENTRY alcGetThreadContext(void); + ALC_API ALCboolean ALC_APIENTRY alcSetThreadContext(ALCcontext* context); + ALC_API ALCcontext* ALC_APIENTRY alcGetThreadContext(void); #endif #endif #ifndef AL_EXT_source_distance_model #define AL_EXT_source_distance_model 1 -#define AL_SOURCE_DISTANCE_MODEL 0x200 +#define AL_SOURCE_DISTANCE_MODEL 0x200 #endif #ifndef AL_SOFT_buffer_sub_data #define AL_SOFT_buffer_sub_data 1 -#define AL_BYTE_RW_OFFSETS_SOFT 0x1031 -#define AL_SAMPLE_RW_OFFSETS_SOFT 0x1032 -typedef ALvoid (AL_APIENTRY*PFNALBUFFERSUBDATASOFTPROC)(ALuint,ALenum,const ALvoid*,ALsizei,ALsizei); +#define AL_BYTE_RW_OFFSETS_SOFT 0x1031 +#define AL_SAMPLE_RW_OFFSETS_SOFT 0x1032 + typedef void(AL_APIENTRY* PFNALBUFFERSUBDATASOFTPROC)(ALuint, ALenum, const ALvoid*, ALsizei, ALsizei); #ifdef AL_ALEXT_PROTOTYPES -AL_API ALvoid AL_APIENTRY alBufferSubDataSOFT(ALuint buffer,ALenum format,const ALvoid *data,ALsizei offset,ALsizei length); + AL_API void AL_APIENTRY alBufferSubDataSOFT( + ALuint buffer, ALenum format, const ALvoid* data, ALsizei offset, ALsizei length); #endif #endif #ifndef AL_SOFT_loop_points #define AL_SOFT_loop_points 1 -#define AL_LOOP_POINTS_SOFT 0x2015 +#define AL_LOOP_POINTS_SOFT 0x2015 #endif #ifndef AL_EXT_FOLDBACK #define AL_EXT_FOLDBACK 1 -#define AL_EXT_FOLDBACK_NAME "AL_EXT_FOLDBACK" -#define AL_FOLDBACK_EVENT_BLOCK 0x4112 -#define AL_FOLDBACK_EVENT_START 0x4111 -#define AL_FOLDBACK_EVENT_STOP 0x4113 -#define AL_FOLDBACK_MODE_MONO 0x4101 -#define AL_FOLDBACK_MODE_STEREO 0x4102 -typedef void (AL_APIENTRY*LPALFOLDBACKCALLBACK)(ALenum,ALsizei); -typedef void (AL_APIENTRY*LPALREQUESTFOLDBACKSTART)(ALenum,ALsizei,ALsizei,ALfloat*,LPALFOLDBACKCALLBACK); -typedef void (AL_APIENTRY*LPALREQUESTFOLDBACKSTOP)(void); +#define AL_EXT_FOLDBACK_NAME "AL_EXT_FOLDBACK" +#define AL_FOLDBACK_EVENT_BLOCK 0x4112 +#define AL_FOLDBACK_EVENT_START 0x4111 +#define AL_FOLDBACK_EVENT_STOP 0x4113 +#define AL_FOLDBACK_MODE_MONO 0x4101 +#define AL_FOLDBACK_MODE_STEREO 0x4102 + typedef void(AL_APIENTRY* LPALFOLDBACKCALLBACK)(ALenum, ALsizei); + typedef void(AL_APIENTRY* LPALREQUESTFOLDBACKSTART)(ALenum, ALsizei, ALsizei, ALfloat*, LPALFOLDBACKCALLBACK); + typedef void(AL_APIENTRY* LPALREQUESTFOLDBACKSTOP)(void); #ifdef AL_ALEXT_PROTOTYPES -AL_API void AL_APIENTRY alRequestFoldbackStart(ALenum mode,ALsizei count,ALsizei length,ALfloat *mem,LPALFOLDBACKCALLBACK callback); -AL_API void AL_APIENTRY alRequestFoldbackStop(void); + AL_API void AL_APIENTRY alRequestFoldbackStart( + ALenum mode, ALsizei count, ALsizei length, ALfloat* mem, LPALFOLDBACKCALLBACK callback); + AL_API void AL_APIENTRY alRequestFoldbackStop(void); #endif #endif #ifndef ALC_EXT_DEDICATED #define ALC_EXT_DEDICATED 1 -#define AL_DEDICATED_GAIN 0x0001 -#define AL_EFFECT_DEDICATED_DIALOGUE 0x9001 +#define AL_DEDICATED_GAIN 0x0001 +#define AL_EFFECT_DEDICATED_DIALOGUE 0x9001 #define AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT 0x9000 #endif #ifndef AL_SOFT_buffer_samples #define AL_SOFT_buffer_samples 1 /* Channel configurations */ -#define AL_MONO_SOFT 0x1500 -#define AL_STEREO_SOFT 0x1501 -#define AL_REAR_SOFT 0x1502 -#define AL_QUAD_SOFT 0x1503 -#define AL_5POINT1_SOFT 0x1504 -#define AL_6POINT1_SOFT 0x1505 -#define AL_7POINT1_SOFT 0x1506 +#define AL_MONO_SOFT 0x1500 +#define AL_STEREO_SOFT 0x1501 +#define AL_REAR_SOFT 0x1502 +#define AL_QUAD_SOFT 0x1503 +#define AL_5POINT1_SOFT 0x1504 +#define AL_6POINT1_SOFT 0x1505 +#define AL_7POINT1_SOFT 0x1506 /* Sample types */ -#define AL_BYTE_SOFT 0x1400 -#define AL_UNSIGNED_BYTE_SOFT 0x1401 -#define AL_SHORT_SOFT 0x1402 -#define AL_UNSIGNED_SHORT_SOFT 0x1403 -#define AL_INT_SOFT 0x1404 -#define AL_UNSIGNED_INT_SOFT 0x1405 -#define AL_FLOAT_SOFT 0x1406 -#define AL_DOUBLE_SOFT 0x1407 -#define AL_BYTE3_SOFT 0x1408 -#define AL_UNSIGNED_BYTE3_SOFT 0x1409 +#define AL_BYTE_SOFT 0x1400 +#define AL_UNSIGNED_BYTE_SOFT 0x1401 +#define AL_SHORT_SOFT 0x1402 +#define AL_UNSIGNED_SHORT_SOFT 0x1403 +#define AL_INT_SOFT 0x1404 +#define AL_UNSIGNED_INT_SOFT 0x1405 +#define AL_FLOAT_SOFT 0x1406 +#define AL_DOUBLE_SOFT 0x1407 +#define AL_BYTE3_SOFT 0x1408 +#define AL_UNSIGNED_BYTE3_SOFT 0x1409 /* Storage formats */ -#define AL_MONO8_SOFT 0x1100 -#define AL_MONO16_SOFT 0x1101 -#define AL_MONO32F_SOFT 0x10010 -#define AL_STEREO8_SOFT 0x1102 -#define AL_STEREO16_SOFT 0x1103 -#define AL_STEREO32F_SOFT 0x10011 -#define AL_QUAD8_SOFT 0x1204 -#define AL_QUAD16_SOFT 0x1205 -#define AL_QUAD32F_SOFT 0x1206 -#define AL_REAR8_SOFT 0x1207 -#define AL_REAR16_SOFT 0x1208 -#define AL_REAR32F_SOFT 0x1209 -#define AL_5POINT1_8_SOFT 0x120A -#define AL_5POINT1_16_SOFT 0x120B -#define AL_5POINT1_32F_SOFT 0x120C -#define AL_6POINT1_8_SOFT 0x120D -#define AL_6POINT1_16_SOFT 0x120E -#define AL_6POINT1_32F_SOFT 0x120F -#define AL_7POINT1_8_SOFT 0x1210 -#define AL_7POINT1_16_SOFT 0x1211 -#define AL_7POINT1_32F_SOFT 0x1212 +#define AL_MONO8_SOFT 0x1100 +#define AL_MONO16_SOFT 0x1101 +#define AL_MONO32F_SOFT 0x10010 +#define AL_STEREO8_SOFT 0x1102 +#define AL_STEREO16_SOFT 0x1103 +#define AL_STEREO32F_SOFT 0x10011 +#define AL_QUAD8_SOFT 0x1204 +#define AL_QUAD16_SOFT 0x1205 +#define AL_QUAD32F_SOFT 0x1206 +#define AL_REAR8_SOFT 0x1207 +#define AL_REAR16_SOFT 0x1208 +#define AL_REAR32F_SOFT 0x1209 +#define AL_5POINT1_8_SOFT 0x120A +#define AL_5POINT1_16_SOFT 0x120B +#define AL_5POINT1_32F_SOFT 0x120C +#define AL_6POINT1_8_SOFT 0x120D +#define AL_6POINT1_16_SOFT 0x120E +#define AL_6POINT1_32F_SOFT 0x120F +#define AL_7POINT1_8_SOFT 0x1210 +#define AL_7POINT1_16_SOFT 0x1211 +#define AL_7POINT1_32F_SOFT 0x1212 /* Buffer attributes */ -#define AL_INTERNAL_FORMAT_SOFT 0x2008 -#define AL_BYTE_LENGTH_SOFT 0x2009 -#define AL_SAMPLE_LENGTH_SOFT 0x200A -#define AL_SEC_LENGTH_SOFT 0x200B - -typedef void (AL_APIENTRY*LPALBUFFERSAMPLESSOFT)(ALuint,ALuint,ALenum,ALsizei,ALenum,ALenum,const ALvoid*); -typedef void (AL_APIENTRY*LPALBUFFERSUBSAMPLESSOFT)(ALuint,ALsizei,ALsizei,ALenum,ALenum,const ALvoid*); -typedef void (AL_APIENTRY*LPALGETBUFFERSAMPLESSOFT)(ALuint,ALsizei,ALsizei,ALenum,ALenum,ALvoid*); -typedef ALboolean (AL_APIENTRY*LPALISBUFFERFORMATSUPPORTEDSOFT)(ALenum); +#define AL_INTERNAL_FORMAT_SOFT 0x2008 +#define AL_BYTE_LENGTH_SOFT 0x2009 +#define AL_SAMPLE_LENGTH_SOFT 0x200A +#define AL_SEC_LENGTH_SOFT 0x200B + + typedef void(AL_APIENTRY* LPALBUFFERSAMPLESSOFT)(ALuint, ALuint, ALenum, ALsizei, ALenum, ALenum, const ALvoid*); + typedef void(AL_APIENTRY* LPALBUFFERSUBSAMPLESSOFT)(ALuint, ALsizei, ALsizei, ALenum, ALenum, const ALvoid*); + typedef void(AL_APIENTRY* LPALGETBUFFERSAMPLESSOFT)(ALuint, ALsizei, ALsizei, ALenum, ALenum, ALvoid*); + typedef ALboolean(AL_APIENTRY* LPALISBUFFERFORMATSUPPORTEDSOFT)(ALenum); #ifdef AL_ALEXT_PROTOTYPES -AL_API void AL_APIENTRY alBufferSamplesSOFT(ALuint buffer, ALuint samplerate, ALenum internalformat, ALsizei samples, ALenum channels, ALenum type, const ALvoid *data); -AL_API void AL_APIENTRY alBufferSubSamplesSOFT(ALuint buffer, ALsizei offset, ALsizei samples, ALenum channels, ALenum type, const ALvoid *data); -AL_API void AL_APIENTRY alGetBufferSamplesSOFT(ALuint buffer, ALsizei offset, ALsizei samples, ALenum channels, ALenum type, ALvoid *data); -AL_API ALboolean AL_APIENTRY alIsBufferFormatSupportedSOFT(ALenum format); + AL_API void AL_APIENTRY alBufferSamplesSOFT(ALuint buffer, ALuint samplerate, ALenum internalformat, + ALsizei samples, ALenum channels, ALenum type, const ALvoid* data); + AL_API void AL_APIENTRY alBufferSubSamplesSOFT( + ALuint buffer, ALsizei offset, ALsizei samples, ALenum channels, ALenum type, const ALvoid* data); + AL_API void AL_APIENTRY alGetBufferSamplesSOFT( + ALuint buffer, ALsizei offset, ALsizei samples, ALenum channels, ALenum type, ALvoid* data); + AL_API ALboolean AL_APIENTRY alIsBufferFormatSupportedSOFT(ALenum format); #endif #endif #ifndef AL_SOFT_direct_channels #define AL_SOFT_direct_channels 1 -#define AL_DIRECT_CHANNELS_SOFT 0x1033 +#define AL_DIRECT_CHANNELS_SOFT 0x1033 #endif #ifndef ALC_SOFT_loopback #define ALC_SOFT_loopback 1 -#define ALC_FORMAT_CHANNELS_SOFT 0x1990 -#define ALC_FORMAT_TYPE_SOFT 0x1991 +#define ALC_FORMAT_CHANNELS_SOFT 0x1990 +#define ALC_FORMAT_TYPE_SOFT 0x1991 /* Sample types */ -#define ALC_BYTE_SOFT 0x1400 -#define ALC_UNSIGNED_BYTE_SOFT 0x1401 -#define ALC_SHORT_SOFT 0x1402 -#define ALC_UNSIGNED_SHORT_SOFT 0x1403 -#define ALC_INT_SOFT 0x1404 -#define ALC_UNSIGNED_INT_SOFT 0x1405 -#define ALC_FLOAT_SOFT 0x1406 +#define ALC_BYTE_SOFT 0x1400 +#define ALC_UNSIGNED_BYTE_SOFT 0x1401 +#define ALC_SHORT_SOFT 0x1402 +#define ALC_UNSIGNED_SHORT_SOFT 0x1403 +#define ALC_INT_SOFT 0x1404 +#define ALC_UNSIGNED_INT_SOFT 0x1405 +#define ALC_FLOAT_SOFT 0x1406 /* Channel configurations */ -#define ALC_MONO_SOFT 0x1500 -#define ALC_STEREO_SOFT 0x1501 -#define ALC_QUAD_SOFT 0x1503 -#define ALC_5POINT1_SOFT 0x1504 -#define ALC_6POINT1_SOFT 0x1505 -#define ALC_7POINT1_SOFT 0x1506 - -typedef ALCdevice* (ALC_APIENTRY*LPALCLOOPBACKOPENDEVICESOFT)(const ALCchar*); -typedef ALCboolean (ALC_APIENTRY*LPALCISRENDERFORMATSUPPORTEDSOFT)(ALCdevice*,ALCsizei,ALCenum,ALCenum); -typedef void (ALC_APIENTRY*LPALCRENDERSAMPLESSOFT)(ALCdevice*,ALCvoid*,ALCsizei); +#define ALC_MONO_SOFT 0x1500 +#define ALC_STEREO_SOFT 0x1501 +#define ALC_QUAD_SOFT 0x1503 +#define ALC_5POINT1_SOFT 0x1504 +#define ALC_6POINT1_SOFT 0x1505 +#define ALC_7POINT1_SOFT 0x1506 + + typedef ALCdevice*(ALC_APIENTRY* LPALCLOOPBACKOPENDEVICESOFT)(const ALCchar*); + typedef ALCboolean(ALC_APIENTRY* LPALCISRENDERFORMATSUPPORTEDSOFT)(ALCdevice*, ALCsizei, ALCenum, ALCenum); + typedef void(ALC_APIENTRY* LPALCRENDERSAMPLESSOFT)(ALCdevice*, ALCvoid*, ALCsizei); #ifdef AL_ALEXT_PROTOTYPES -ALC_API ALCdevice* ALC_APIENTRY alcLoopbackOpenDeviceSOFT(const ALCchar *deviceName); -ALC_API ALCboolean ALC_APIENTRY alcIsRenderFormatSupportedSOFT(ALCdevice *device, ALCsizei freq, ALCenum channels, ALCenum type); -ALC_API void ALC_APIENTRY alcRenderSamplesSOFT(ALCdevice *device, ALCvoid *buffer, ALCsizei samples); + ALC_API ALCdevice* ALC_APIENTRY alcLoopbackOpenDeviceSOFT(const ALCchar* deviceName); + ALC_API ALCboolean ALC_APIENTRY alcIsRenderFormatSupportedSOFT( + ALCdevice* device, ALCsizei freq, ALCenum channels, ALCenum type); + ALC_API void ALC_APIENTRY alcRenderSamplesSOFT(ALCdevice* device, ALCvoid* buffer, ALCsizei samples); #endif #endif #ifndef AL_EXT_STEREO_ANGLES #define AL_EXT_STEREO_ANGLES 1 -#define AL_STEREO_ANGLES 0x1030 +#define AL_STEREO_ANGLES 0x1030 #endif #ifndef AL_EXT_SOURCE_RADIUS #define AL_EXT_SOURCE_RADIUS 1 -#define AL_SOURCE_RADIUS 0x1031 +#define AL_SOURCE_RADIUS 0x1031 #endif #ifndef AL_SOFT_source_latency #define AL_SOFT_source_latency 1 -#define AL_SAMPLE_OFFSET_LATENCY_SOFT 0x1200 -#define AL_SEC_OFFSET_LATENCY_SOFT 0x1201 -typedef int64_t ALint64SOFT; -typedef uint64_t ALuint64SOFT; -typedef void (AL_APIENTRY*LPALSOURCEDSOFT)(ALuint,ALenum,ALdouble); -typedef void (AL_APIENTRY*LPALSOURCE3DSOFT)(ALuint,ALenum,ALdouble,ALdouble,ALdouble); -typedef void (AL_APIENTRY*LPALSOURCEDVSOFT)(ALuint,ALenum,const ALdouble*); -typedef void (AL_APIENTRY*LPALGETSOURCEDSOFT)(ALuint,ALenum,ALdouble*); -typedef void (AL_APIENTRY*LPALGETSOURCE3DSOFT)(ALuint,ALenum,ALdouble*,ALdouble*,ALdouble*); -typedef void (AL_APIENTRY*LPALGETSOURCEDVSOFT)(ALuint,ALenum,ALdouble*); -typedef void (AL_APIENTRY*LPALSOURCEI64SOFT)(ALuint,ALenum,ALint64SOFT); -typedef void (AL_APIENTRY*LPALSOURCE3I64SOFT)(ALuint,ALenum,ALint64SOFT,ALint64SOFT,ALint64SOFT); -typedef void (AL_APIENTRY*LPALSOURCEI64VSOFT)(ALuint,ALenum,const ALint64SOFT*); -typedef void (AL_APIENTRY*LPALGETSOURCEI64SOFT)(ALuint,ALenum,ALint64SOFT*); -typedef void (AL_APIENTRY*LPALGETSOURCE3I64SOFT)(ALuint,ALenum,ALint64SOFT*,ALint64SOFT*,ALint64SOFT*); -typedef void (AL_APIENTRY*LPALGETSOURCEI64VSOFT)(ALuint,ALenum,ALint64SOFT*); +#define AL_SAMPLE_OFFSET_LATENCY_SOFT 0x1200 +#define AL_SEC_OFFSET_LATENCY_SOFT 0x1201 + typedef _alsoft_int64_t ALint64SOFT; + typedef _alsoft_uint64_t ALuint64SOFT; + typedef void(AL_APIENTRY* LPALSOURCEDSOFT)(ALuint, ALenum, ALdouble); + typedef void(AL_APIENTRY* LPALSOURCE3DSOFT)(ALuint, ALenum, ALdouble, ALdouble, ALdouble); + typedef void(AL_APIENTRY* LPALSOURCEDVSOFT)(ALuint, ALenum, const ALdouble*); + typedef void(AL_APIENTRY* LPALGETSOURCEDSOFT)(ALuint, ALenum, ALdouble*); + typedef void(AL_APIENTRY* LPALGETSOURCE3DSOFT)(ALuint, ALenum, ALdouble*, ALdouble*, ALdouble*); + typedef void(AL_APIENTRY* LPALGETSOURCEDVSOFT)(ALuint, ALenum, ALdouble*); + typedef void(AL_APIENTRY* LPALSOURCEI64SOFT)(ALuint, ALenum, ALint64SOFT); + typedef void(AL_APIENTRY* LPALSOURCE3I64SOFT)(ALuint, ALenum, ALint64SOFT, ALint64SOFT, ALint64SOFT); + typedef void(AL_APIENTRY* LPALSOURCEI64VSOFT)(ALuint, ALenum, const ALint64SOFT*); + typedef void(AL_APIENTRY* LPALGETSOURCEI64SOFT)(ALuint, ALenum, ALint64SOFT*); + typedef void(AL_APIENTRY* LPALGETSOURCE3I64SOFT)(ALuint, ALenum, ALint64SOFT*, ALint64SOFT*, ALint64SOFT*); + typedef void(AL_APIENTRY* LPALGETSOURCEI64VSOFT)(ALuint, ALenum, ALint64SOFT*); #ifdef AL_ALEXT_PROTOTYPES -AL_API void AL_APIENTRY alSourcedSOFT(ALuint source, ALenum param, ALdouble value); -AL_API void AL_APIENTRY alSource3dSOFT(ALuint source, ALenum param, ALdouble value1, ALdouble value2, ALdouble value3); -AL_API void AL_APIENTRY alSourcedvSOFT(ALuint source, ALenum param, const ALdouble *values); -AL_API void AL_APIENTRY alGetSourcedSOFT(ALuint source, ALenum param, ALdouble *value); -AL_API void AL_APIENTRY alGetSource3dSOFT(ALuint source, ALenum param, ALdouble *value1, ALdouble *value2, ALdouble *value3); -AL_API void AL_APIENTRY alGetSourcedvSOFT(ALuint source, ALenum param, ALdouble *values); -AL_API void AL_APIENTRY alSourcei64SOFT(ALuint source, ALenum param, ALint64SOFT value); -AL_API void AL_APIENTRY alSource3i64SOFT(ALuint source, ALenum param, ALint64SOFT value1, ALint64SOFT value2, ALint64SOFT value3); -AL_API void AL_APIENTRY alSourcei64vSOFT(ALuint source, ALenum param, const ALint64SOFT *values); -AL_API void AL_APIENTRY alGetSourcei64SOFT(ALuint source, ALenum param, ALint64SOFT *value); -AL_API void AL_APIENTRY alGetSource3i64SOFT(ALuint source, ALenum param, ALint64SOFT *value1, ALint64SOFT *value2, ALint64SOFT *value3); -AL_API void AL_APIENTRY alGetSourcei64vSOFT(ALuint source, ALenum param, ALint64SOFT *values); + AL_API void AL_APIENTRY alSourcedSOFT(ALuint source, ALenum param, ALdouble value); + AL_API void AL_APIENTRY alSource3dSOFT( + ALuint source, ALenum param, ALdouble value1, ALdouble value2, ALdouble value3); + AL_API void AL_APIENTRY alSourcedvSOFT(ALuint source, ALenum param, const ALdouble* values); + AL_API void AL_APIENTRY alGetSourcedSOFT(ALuint source, ALenum param, ALdouble* value); + AL_API void AL_APIENTRY alGetSource3dSOFT( + ALuint source, ALenum param, ALdouble* value1, ALdouble* value2, ALdouble* value3); + AL_API void AL_APIENTRY alGetSourcedvSOFT(ALuint source, ALenum param, ALdouble* values); + AL_API void AL_APIENTRY alSourcei64SOFT(ALuint source, ALenum param, ALint64SOFT value); + AL_API void AL_APIENTRY alSource3i64SOFT( + ALuint source, ALenum param, ALint64SOFT value1, ALint64SOFT value2, ALint64SOFT value3); + AL_API void AL_APIENTRY alSourcei64vSOFT(ALuint source, ALenum param, const ALint64SOFT* values); + AL_API void AL_APIENTRY alGetSourcei64SOFT(ALuint source, ALenum param, ALint64SOFT* value); + AL_API void AL_APIENTRY alGetSource3i64SOFT( + ALuint source, ALenum param, ALint64SOFT* value1, ALint64SOFT* value2, ALint64SOFT* value3); + AL_API void AL_APIENTRY alGetSourcei64vSOFT(ALuint source, ALenum param, ALint64SOFT* values); #endif #endif #ifndef ALC_EXT_DEFAULT_FILTER_ORDER #define ALC_EXT_DEFAULT_FILTER_ORDER 1 -#define ALC_DEFAULT_FILTER_ORDER 0x1100 +#define ALC_DEFAULT_FILTER_ORDER 0x1100 #endif #ifndef AL_SOFT_deferred_updates #define AL_SOFT_deferred_updates 1 -#define AL_DEFERRED_UPDATES_SOFT 0xC002 -typedef ALvoid (AL_APIENTRY*LPALDEFERUPDATESSOFT)(void); -typedef ALvoid (AL_APIENTRY*LPALPROCESSUPDATESSOFT)(void); +#define AL_DEFERRED_UPDATES_SOFT 0xC002 + typedef void(AL_APIENTRY* LPALDEFERUPDATESSOFT)(void); + typedef void(AL_APIENTRY* LPALPROCESSUPDATESSOFT)(void); #ifdef AL_ALEXT_PROTOTYPES -AL_API ALvoid AL_APIENTRY alDeferUpdatesSOFT(void); -AL_API ALvoid AL_APIENTRY alProcessUpdatesSOFT(void); + AL_API void AL_APIENTRY alDeferUpdatesSOFT(void); + AL_API void AL_APIENTRY alProcessUpdatesSOFT(void); #endif #endif #ifndef AL_SOFT_block_alignment #define AL_SOFT_block_alignment 1 -#define AL_UNPACK_BLOCK_ALIGNMENT_SOFT 0x200C -#define AL_PACK_BLOCK_ALIGNMENT_SOFT 0x200D +#define AL_UNPACK_BLOCK_ALIGNMENT_SOFT 0x200C +#define AL_PACK_BLOCK_ALIGNMENT_SOFT 0x200D #endif #ifndef AL_SOFT_MSADPCM #define AL_SOFT_MSADPCM 1 -#define AL_FORMAT_MONO_MSADPCM_SOFT 0x1302 -#define AL_FORMAT_STEREO_MSADPCM_SOFT 0x1303 +#define AL_FORMAT_MONO_MSADPCM_SOFT 0x1302 +#define AL_FORMAT_STEREO_MSADPCM_SOFT 0x1303 #endif #ifndef AL_SOFT_source_length @@ -385,78 +404,234 @@ AL_API ALvoid AL_APIENTRY alProcessUpdatesSOFT(void); #ifndef ALC_SOFT_pause_device #define ALC_SOFT_pause_device 1 -typedef void (ALC_APIENTRY*LPALCDEVICEPAUSESOFT)(ALCdevice *device); -typedef void (ALC_APIENTRY*LPALCDEVICERESUMESOFT)(ALCdevice *device); + typedef void(ALC_APIENTRY* LPALCDEVICEPAUSESOFT)(ALCdevice* device); + typedef void(ALC_APIENTRY* LPALCDEVICERESUMESOFT)(ALCdevice* device); #ifdef AL_ALEXT_PROTOTYPES -ALC_API void ALC_APIENTRY alcDevicePauseSOFT(ALCdevice *device); -ALC_API void ALC_APIENTRY alcDeviceResumeSOFT(ALCdevice *device); + ALC_API void ALC_APIENTRY alcDevicePauseSOFT(ALCdevice* device); + ALC_API void ALC_APIENTRY alcDeviceResumeSOFT(ALCdevice* device); #endif #endif #ifndef AL_EXT_BFORMAT #define AL_EXT_BFORMAT 1 -#define AL_FORMAT_BFORMAT2D_8 0x20021 -#define AL_FORMAT_BFORMAT2D_16 0x20022 -#define AL_FORMAT_BFORMAT2D_FLOAT32 0x20023 -#define AL_FORMAT_BFORMAT3D_8 0x20031 -#define AL_FORMAT_BFORMAT3D_16 0x20032 -#define AL_FORMAT_BFORMAT3D_FLOAT32 0x20033 +/* Provides support for B-Format ambisonic buffers (first-order, FuMa scaling + * and layout). + * + * BFORMAT2D_8: Unsigned 8-bit, 3-channel non-periphonic (WXY). + * BFORMAT2D_16: Signed 16-bit, 3-channel non-periphonic (WXY). + * BFORMAT2D_FLOAT32: 32-bit float, 3-channel non-periphonic (WXY). + * BFORMAT3D_8: Unsigned 8-bit, 4-channel periphonic (WXYZ). + * BFORMAT3D_16: Signed 16-bit, 4-channel periphonic (WXYZ). + * BFORMAT3D_FLOAT32: 32-bit float, 4-channel periphonic (WXYZ). + */ +#define AL_FORMAT_BFORMAT2D_8 0x20021 +#define AL_FORMAT_BFORMAT2D_16 0x20022 +#define AL_FORMAT_BFORMAT2D_FLOAT32 0x20023 +#define AL_FORMAT_BFORMAT3D_8 0x20031 +#define AL_FORMAT_BFORMAT3D_16 0x20032 +#define AL_FORMAT_BFORMAT3D_FLOAT32 0x20033 #endif #ifndef AL_EXT_MULAW_BFORMAT #define AL_EXT_MULAW_BFORMAT 1 -#define AL_FORMAT_BFORMAT2D_MULAW 0x10031 -#define AL_FORMAT_BFORMAT3D_MULAW 0x10032 +#define AL_FORMAT_BFORMAT2D_MULAW 0x10031 +#define AL_FORMAT_BFORMAT3D_MULAW 0x10032 #endif #ifndef ALC_SOFT_HRTF #define ALC_SOFT_HRTF 1 -#define ALC_HRTF_SOFT 0x1992 -#define ALC_DONT_CARE_SOFT 0x0002 -#define ALC_HRTF_STATUS_SOFT 0x1993 -#define ALC_HRTF_DISABLED_SOFT 0x0000 -#define ALC_HRTF_ENABLED_SOFT 0x0001 -#define ALC_HRTF_DENIED_SOFT 0x0002 -#define ALC_HRTF_REQUIRED_SOFT 0x0003 -#define ALC_HRTF_HEADPHONES_DETECTED_SOFT 0x0004 -#define ALC_HRTF_UNSUPPORTED_FORMAT_SOFT 0x0005 -#define ALC_NUM_HRTF_SPECIFIERS_SOFT 0x1994 -#define ALC_HRTF_SPECIFIER_SOFT 0x1995 -#define ALC_HRTF_ID_SOFT 0x1996 -typedef const ALCchar* (ALC_APIENTRY*LPALCGETSTRINGISOFT)(ALCdevice *device, ALCenum paramName, ALCsizei index); -typedef ALCboolean (ALC_APIENTRY*LPALCRESETDEVICESOFT)(ALCdevice *device, const ALCint *attribs); +#define ALC_HRTF_SOFT 0x1992 +#define ALC_DONT_CARE_SOFT 0x0002 +#define ALC_HRTF_STATUS_SOFT 0x1993 +#define ALC_HRTF_DISABLED_SOFT 0x0000 +#define ALC_HRTF_ENABLED_SOFT 0x0001 +#define ALC_HRTF_DENIED_SOFT 0x0002 +#define ALC_HRTF_REQUIRED_SOFT 0x0003 +#define ALC_HRTF_HEADPHONES_DETECTED_SOFT 0x0004 +#define ALC_HRTF_UNSUPPORTED_FORMAT_SOFT 0x0005 +#define ALC_NUM_HRTF_SPECIFIERS_SOFT 0x1994 +#define ALC_HRTF_SPECIFIER_SOFT 0x1995 +#define ALC_HRTF_ID_SOFT 0x1996 + typedef const ALCchar*(ALC_APIENTRY* LPALCGETSTRINGISOFT)(ALCdevice* device, ALCenum paramName, ALCsizei index); + typedef ALCboolean(ALC_APIENTRY* LPALCRESETDEVICESOFT)(ALCdevice* device, const ALCint* attribs); #ifdef AL_ALEXT_PROTOTYPES -ALC_API const ALCchar* ALC_APIENTRY alcGetStringiSOFT(ALCdevice *device, ALCenum paramName, ALCsizei index); -ALC_API ALCboolean ALC_APIENTRY alcResetDeviceSOFT(ALCdevice *device, const ALCint *attribs); + ALC_API const ALCchar* ALC_APIENTRY alcGetStringiSOFT(ALCdevice* device, ALCenum paramName, ALCsizei index); + ALC_API ALCboolean ALC_APIENTRY alcResetDeviceSOFT(ALCdevice* device, const ALCint* attribs); #endif #endif #ifndef AL_SOFT_gain_clamp_ex #define AL_SOFT_gain_clamp_ex 1 -#define AL_GAIN_LIMIT_SOFT 0x200E +#define AL_GAIN_LIMIT_SOFT 0x200E #endif #ifndef AL_SOFT_source_resampler #define AL_SOFT_source_resampler -#define AL_NUM_RESAMPLERS_SOFT 0x1210 -#define AL_DEFAULT_RESAMPLER_SOFT 0x1211 -#define AL_SOURCE_RESAMPLER_SOFT 0x1212 -#define AL_RESAMPLER_NAME_SOFT 0x1213 -typedef const ALchar* (AL_APIENTRY*LPALGETSTRINGISOFT)(ALenum pname, ALsizei index); +#define AL_NUM_RESAMPLERS_SOFT 0x1210 +#define AL_DEFAULT_RESAMPLER_SOFT 0x1211 +#define AL_SOURCE_RESAMPLER_SOFT 0x1212 +#define AL_RESAMPLER_NAME_SOFT 0x1213 + typedef const ALchar*(AL_APIENTRY* LPALGETSTRINGISOFT)(ALenum pname, ALsizei index); #ifdef AL_ALEXT_PROTOTYPES -AL_API const ALchar* AL_APIENTRY alGetStringiSOFT(ALenum pname, ALsizei index); + AL_API const ALchar* AL_APIENTRY alGetStringiSOFT(ALenum pname, ALsizei index); #endif #endif #ifndef AL_SOFT_source_spatialize #define AL_SOFT_source_spatialize -#define AL_SOURCE_SPATIALIZE_SOFT 0x1214 -#define AL_AUTO_SOFT 0x0002 +#define AL_SOURCE_SPATIALIZE_SOFT 0x1214 +#define AL_AUTO_SOFT 0x0002 #endif #ifndef ALC_SOFT_output_limiter #define ALC_SOFT_output_limiter -#define ALC_OUTPUT_LIMITER_SOFT 0x199A +#define ALC_OUTPUT_LIMITER_SOFT 0x199A +#endif + +#ifndef ALC_SOFT_device_clock +#define ALC_SOFT_device_clock 1 + typedef _alsoft_int64_t ALCint64SOFT; + typedef _alsoft_uint64_t ALCuint64SOFT; +#define ALC_DEVICE_CLOCK_SOFT 0x1600 +#define ALC_DEVICE_LATENCY_SOFT 0x1601 +#define ALC_DEVICE_CLOCK_LATENCY_SOFT 0x1602 +#define AL_SAMPLE_OFFSET_CLOCK_SOFT 0x1202 +#define AL_SEC_OFFSET_CLOCK_SOFT 0x1203 + typedef void(ALC_APIENTRY* LPALCGETINTEGER64VSOFT)( + ALCdevice* device, ALCenum pname, ALsizei size, ALCint64SOFT* values); +#ifdef AL_ALEXT_PROTOTYPES + ALC_API void ALC_APIENTRY alcGetInteger64vSOFT( + ALCdevice* device, ALCenum pname, ALsizei size, ALCint64SOFT* values); +#endif +#endif + +#ifndef AL_SOFT_direct_channels_remix +#define AL_SOFT_direct_channels_remix 1 +#define AL_DROP_UNMATCHED_SOFT 0x0001 +#define AL_REMIX_UNMATCHED_SOFT 0x0002 +#endif + +#ifndef AL_SOFT_bformat_ex +#define AL_SOFT_bformat_ex 1 +#define AL_AMBISONIC_LAYOUT_SOFT 0x1997 +#define AL_AMBISONIC_SCALING_SOFT 0x1998 + +/* Ambisonic layouts */ +#define AL_FUMA_SOFT 0x0000 +#define AL_ACN_SOFT 0x0001 + +/* Ambisonic scalings (normalization) */ +/*#define AL_FUMA_SOFT*/ +#define AL_SN3D_SOFT 0x0001 +#define AL_N3D_SOFT 0x0002 +#endif + +#ifndef ALC_SOFT_loopback_bformat +#define ALC_SOFT_loopback_bformat 1 +#define ALC_AMBISONIC_LAYOUT_SOFT 0x1997 +#define ALC_AMBISONIC_SCALING_SOFT 0x1998 +#define ALC_AMBISONIC_ORDER_SOFT 0x1999 +#define ALC_MAX_AMBISONIC_ORDER_SOFT 0x199B + +#define ALC_BFORMAT3D_SOFT 0x1507 + +/* Ambisonic layouts */ +#define ALC_FUMA_SOFT 0x0000 +#define ALC_ACN_SOFT 0x0001 + +/* Ambisonic scalings (normalization) */ +/*#define ALC_FUMA_SOFT*/ +#define ALC_SN3D_SOFT 0x0001 +#define ALC_N3D_SOFT 0x0002 +#endif + +#ifndef AL_SOFT_effect_target +#define AL_SOFT_effect_target +#define AL_EFFECTSLOT_TARGET_SOFT 0x199C +#endif + +#ifndef AL_SOFT_events +#define AL_SOFT_events 1 +#define AL_EVENT_CALLBACK_FUNCTION_SOFT 0x19A2 +#define AL_EVENT_CALLBACK_USER_PARAM_SOFT 0x19A3 +#define AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT 0x19A4 +#define AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT 0x19A5 +#define AL_EVENT_TYPE_DISCONNECTED_SOFT 0x19A6 + typedef void(AL_APIENTRY* ALEVENTPROCSOFT)( + ALenum eventType, ALuint object, ALuint param, ALsizei length, const ALchar* message, void* userParam); + typedef void(AL_APIENTRY* LPALEVENTCONTROLSOFT)(ALsizei count, const ALenum* types, ALboolean enable); + typedef void(AL_APIENTRY* LPALEVENTCALLBACKSOFT)(ALEVENTPROCSOFT callback, void* userParam); + typedef void*(AL_APIENTRY* LPALGETPOINTERSOFT)(ALenum pname); + typedef void(AL_APIENTRY* LPALGETPOINTERVSOFT)(ALenum pname, void** values); +#ifdef AL_ALEXT_PROTOTYPES + AL_API void AL_APIENTRY alEventControlSOFT(ALsizei count, const ALenum* types, ALboolean enable); + AL_API void AL_APIENTRY alEventCallbackSOFT(ALEVENTPROCSOFT callback, void* userParam); + AL_API void* AL_APIENTRY alGetPointerSOFT(ALenum pname); + AL_API void AL_APIENTRY alGetPointervSOFT(ALenum pname, void** values); +#endif +#endif + +#ifndef ALC_SOFT_reopen_device +#define ALC_SOFT_reopen_device + typedef ALCboolean(ALC_APIENTRY* LPALCREOPENDEVICESOFT)( + ALCdevice* device, const ALCchar* deviceName, const ALCint* attribs); +#ifdef AL_ALEXT_PROTOTYPES + ALCboolean ALC_APIENTRY alcReopenDeviceSOFT(ALCdevice* device, const ALCchar* deviceName, const ALCint* attribs); +#endif +#endif + +#ifndef AL_SOFT_callback_buffer +#define AL_SOFT_callback_buffer +#define AL_BUFFER_CALLBACK_FUNCTION_SOFT 0x19A0 +#define AL_BUFFER_CALLBACK_USER_PARAM_SOFT 0x19A1 + typedef ALsizei(AL_APIENTRY* ALBUFFERCALLBACKTYPESOFT)(ALvoid* userptr, ALvoid* sampledata, ALsizei numbytes); + typedef void(AL_APIENTRY* LPALBUFFERCALLBACKSOFT)( + ALuint buffer, ALenum format, ALsizei freq, ALBUFFERCALLBACKTYPESOFT callback, ALvoid* userptr); + typedef void(AL_APIENTRY* LPALGETBUFFERPTRSOFT)(ALuint buffer, ALenum param, ALvoid** value); + typedef void(AL_APIENTRY* LPALGETBUFFER3PTRSOFT)( + ALuint buffer, ALenum param, ALvoid** value1, ALvoid** value2, ALvoid** value3); + typedef void(AL_APIENTRY* LPALGETBUFFERPTRVSOFT)(ALuint buffer, ALenum param, ALvoid** values); +#ifdef AL_ALEXT_PROTOTYPES + AL_API void AL_APIENTRY alBufferCallbackSOFT( + ALuint buffer, ALenum format, ALsizei freq, ALBUFFERCALLBACKTYPESOFT callback, ALvoid* userptr); + AL_API void AL_APIENTRY alGetBufferPtrSOFT(ALuint buffer, ALenum param, ALvoid** ptr); + AL_API void AL_APIENTRY alGetBuffer3PtrSOFT( + ALuint buffer, ALenum param, ALvoid** ptr0, ALvoid** ptr1, ALvoid** ptr2); + AL_API void AL_APIENTRY alGetBufferPtrvSOFT(ALuint buffer, ALenum param, ALvoid** ptr); +#endif +#endif + +#ifndef AL_SOFT_UHJ +#define AL_SOFT_UHJ +#define AL_FORMAT_UHJ2CHN8_SOFT 0x19A2 +#define AL_FORMAT_UHJ2CHN16_SOFT 0x19A3 +#define AL_FORMAT_UHJ2CHN_FLOAT32_SOFT 0x19A4 +#define AL_FORMAT_UHJ3CHN8_SOFT 0x19A5 +#define AL_FORMAT_UHJ3CHN16_SOFT 0x19A6 +#define AL_FORMAT_UHJ3CHN_FLOAT32_SOFT 0x19A7 +#define AL_FORMAT_UHJ4CHN8_SOFT 0x19A8 +#define AL_FORMAT_UHJ4CHN16_SOFT 0x19A9 +#define AL_FORMAT_UHJ4CHN_FLOAT32_SOFT 0x19AA + +#define AL_STEREO_MODE_SOFT 0x19B0 +#define AL_NORMAL_SOFT 0x0000 +#define AL_SUPER_STEREO_SOFT 0x0001 +#define AL_SUPER_STEREO_WIDTH_SOFT 0x19B1 +#endif + +#ifndef ALC_SOFT_output_mode +#define ALC_SOFT_output_mode +#define ALC_OUTPUT_MODE_SOFT 0x19AC +#define ALC_ANY_SOFT 0x19AD +/*#define ALC_MONO_SOFT 0x1500*/ +/*#define ALC_STEREO_SOFT 0x1501*/ +#define ALC_STEREO_BASIC_SOFT 0x19AE +#define ALC_STEREO_UHJ_SOFT 0x19AF +#define ALC_STEREO_HRTF_SOFT 0x19B2 +/*#define ALC_QUAD_SOFT 0x1503*/ +#define ALC_SURROUND_5_1_SOFT 0x1504 +#define ALC_SURROUND_6_1_SOFT 0x1505 +#define ALC_SURROUND_7_1_SOFT 0x1506 #endif #ifdef __cplusplus diff --git a/apps/openmw/mwsound/constants.hpp b/apps/openmw/mwsound/constants.hpp new file mode 100644 index 00000000000..217dd1935e0 --- /dev/null +++ b/apps/openmw/mwsound/constants.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_APPS_OPENMW_MWSOUND_CONSTANTS_H +#define OPENMW_APPS_OPENMW_MWSOUND_CONSTANTS_H + +#include + +namespace MWSound +{ + constexpr VFS::Path::NormalizedView battlePlaylist("battle"); + constexpr VFS::Path::NormalizedView explorePlaylist("explore"); + constexpr VFS::Path::NormalizedView titleMusic("music/special/morrowind title.mp3"); + constexpr VFS::Path::NormalizedView triumphMusic("music/special/mw_triumph.mp3"); + constexpr VFS::Path::NormalizedView deathMusic("music/special/mw_death.mp3"); +} + +#endif diff --git a/apps/openmw/mwsound/efx-presets.h b/apps/openmw/mwsound/efx-presets.h index 8539fd51789..a9662936d5c 100644 --- a/apps/openmw/mwsound/efx-presets.h +++ b/apps/openmw/mwsound/efx-presets.h @@ -5,7 +5,8 @@ #ifndef EFXEAXREVERBPROPERTIES_DEFINED #define EFXEAXREVERBPROPERTIES_DEFINED -typedef struct { +typedef struct +{ float flDensity; float flDiffusion; float flGain; @@ -28,375 +29,827 @@ typedef struct { float flHFReference; float flLFReference; float flRoomRolloffFactor; - int iDecayHFLimit; + int iDecayHFLimit; } EFXEAXREVERBPROPERTIES, *LPEFXEAXREVERBPROPERTIES; #endif /* Default Presets */ -#define EFX_REVERB_PRESET_GENERIC \ - { 1.0000f, 1.0000f, 0.3162f, 0.8913f, 1.0000f, 1.4900f, 0.8300f, 1.0000f, 0.0500f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_PADDEDCELL \ - { 0.1715f, 1.0000f, 0.3162f, 0.0010f, 1.0000f, 0.1700f, 0.1000f, 1.0000f, 0.2500f, 0.0010f, { 0.0000f, 0.0000f, 0.0000f }, 1.2691f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_ROOM \ - { 0.4287f, 1.0000f, 0.3162f, 0.5929f, 1.0000f, 0.4000f, 0.8300f, 1.0000f, 0.1503f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 1.0629f, 0.0030f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_BATHROOM \ - { 0.1715f, 1.0000f, 0.3162f, 0.2512f, 1.0000f, 1.4900f, 0.5400f, 1.0000f, 0.6531f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 3.2734f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_LIVINGROOM \ - { 0.9766f, 1.0000f, 0.3162f, 0.0010f, 1.0000f, 0.5000f, 0.1000f, 1.0000f, 0.2051f, 0.0030f, { 0.0000f, 0.0000f, 0.0000f }, 0.2805f, 0.0040f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_STONEROOM \ - { 1.0000f, 1.0000f, 0.3162f, 0.7079f, 1.0000f, 2.3100f, 0.6400f, 1.0000f, 0.4411f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1003f, 0.0170f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_AUDITORIUM \ - { 1.0000f, 1.0000f, 0.3162f, 0.5781f, 1.0000f, 4.3200f, 0.5900f, 1.0000f, 0.4032f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.7170f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CONCERTHALL \ - { 1.0000f, 1.0000f, 0.3162f, 0.5623f, 1.0000f, 3.9200f, 0.7000f, 1.0000f, 0.2427f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.9977f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CAVE \ - { 1.0000f, 1.0000f, 0.3162f, 1.0000f, 1.0000f, 2.9100f, 1.3000f, 1.0000f, 0.5000f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.7063f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_ARENA \ - { 1.0000f, 1.0000f, 0.3162f, 0.4477f, 1.0000f, 7.2400f, 0.3300f, 1.0000f, 0.2612f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.0186f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_HANGAR \ - { 1.0000f, 1.0000f, 0.3162f, 0.3162f, 1.0000f, 10.0500f, 0.2300f, 1.0000f, 0.5000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.2560f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CARPETEDHALLWAY \ - { 0.4287f, 1.0000f, 0.3162f, 0.0100f, 1.0000f, 0.3000f, 0.1000f, 1.0000f, 0.1215f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 0.1531f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_HALLWAY \ - { 0.3645f, 1.0000f, 0.3162f, 0.7079f, 1.0000f, 1.4900f, 0.5900f, 1.0000f, 0.2458f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.6615f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_STONECORRIDOR \ - { 1.0000f, 1.0000f, 0.3162f, 0.7612f, 1.0000f, 2.7000f, 0.7900f, 1.0000f, 0.2472f, 0.0130f, { 0.0000f, 0.0000f, 0.0000f }, 1.5758f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_ALLEY \ - { 1.0000f, 0.3000f, 0.3162f, 0.7328f, 1.0000f, 1.4900f, 0.8600f, 1.0000f, 0.2500f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.9954f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.9500f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_FOREST \ - { 1.0000f, 0.3000f, 0.3162f, 0.0224f, 1.0000f, 1.4900f, 0.5400f, 1.0000f, 0.0525f, 0.1620f, { 0.0000f, 0.0000f, 0.0000f }, 0.7682f, 0.0880f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CITY \ - { 1.0000f, 0.5000f, 0.3162f, 0.3981f, 1.0000f, 1.4900f, 0.6700f, 1.0000f, 0.0730f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.1427f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_MOUNTAINS \ - { 1.0000f, 0.2700f, 0.3162f, 0.0562f, 1.0000f, 1.4900f, 0.2100f, 1.0000f, 0.0407f, 0.3000f, { 0.0000f, 0.0000f, 0.0000f }, 0.1919f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_QUARRY \ - { 1.0000f, 1.0000f, 0.3162f, 0.3162f, 1.0000f, 1.4900f, 0.8300f, 1.0000f, 0.0000f, 0.0610f, { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0250f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.7000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_PLAIN \ - { 1.0000f, 0.2100f, 0.3162f, 0.1000f, 1.0000f, 1.4900f, 0.5000f, 1.0000f, 0.0585f, 0.1790f, { 0.0000f, 0.0000f, 0.0000f }, 0.1089f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_PARKINGLOT \ - { 1.0000f, 1.0000f, 0.3162f, 1.0000f, 1.0000f, 1.6500f, 1.5000f, 1.0000f, 0.2082f, 0.0080f, { 0.0000f, 0.0000f, 0.0000f }, 0.2652f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_SEWERPIPE \ - { 0.3071f, 0.8000f, 0.3162f, 0.3162f, 1.0000f, 2.8100f, 0.1400f, 1.0000f, 1.6387f, 0.0140f, { 0.0000f, 0.0000f, 0.0000f }, 3.2471f, 0.0210f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_UNDERWATER \ - { 0.3645f, 1.0000f, 0.3162f, 0.0100f, 1.0000f, 1.4900f, 0.1000f, 1.0000f, 0.5963f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 7.0795f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 1.1800f, 0.3480f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_DRUGGED \ - { 0.4287f, 0.5000f, 0.3162f, 1.0000f, 1.0000f, 8.3900f, 1.3900f, 1.0000f, 0.8760f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 3.1081f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 1.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_DIZZY \ - { 0.3645f, 0.6000f, 0.3162f, 0.6310f, 1.0000f, 17.2300f, 0.5600f, 1.0000f, 0.1392f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.4937f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.8100f, 0.3100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_PSYCHOTIC \ - { 0.0625f, 0.5000f, 0.3162f, 0.8404f, 1.0000f, 7.5600f, 0.9100f, 1.0000f, 0.4864f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 2.4378f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 4.0000f, 1.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } +#define EFX_REVERB_PRESET_GENERIC \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.8913f, 1.0000f, 1.4900f, 0.8300f, 1.0000f, 0.0500f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_PADDEDCELL \ + { \ + 0.1715f, 1.0000f, 0.3162f, 0.0010f, 1.0000f, 0.1700f, 0.1000f, 1.0000f, 0.2500f, 0.0010f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2691f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_ROOM \ + { \ + 0.4287f, 1.0000f, 0.3162f, 0.5929f, 1.0000f, 0.4000f, 0.8300f, 1.0000f, 0.1503f, 0.0020f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.0629f, 0.0030f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_BATHROOM \ + { \ + 0.1715f, 1.0000f, 0.3162f, 0.2512f, 1.0000f, 1.4900f, 0.5400f, 1.0000f, 0.6531f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 3.2734f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_LIVINGROOM \ + { \ + 0.9766f, 1.0000f, 0.3162f, 0.0010f, 1.0000f, 0.5000f, 0.1000f, 1.0000f, 0.2051f, 0.0030f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.2805f, 0.0040f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_STONEROOM \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.7079f, 1.0000f, 2.3100f, 0.6400f, 1.0000f, 0.4411f, 0.0120f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.1003f, 0.0170f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_AUDITORIUM \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.5781f, 1.0000f, 4.3200f, 0.5900f, 1.0000f, 0.4032f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7170f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CONCERTHALL \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.5623f, 1.0000f, 3.9200f, 0.7000f, 1.0000f, 0.2427f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.9977f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CAVE \ + { \ + 1.0000f, 1.0000f, 0.3162f, 1.0000f, 1.0000f, 2.9100f, 1.3000f, 1.0000f, 0.5000f, 0.0150f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7063f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_ARENA \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.4477f, 1.0000f, 7.2400f, 0.3300f, 1.0000f, 0.2612f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.0186f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_HANGAR \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.3162f, 1.0000f, 10.0500f, 0.2300f, 1.0000f, 0.5000f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2560f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CARPETEDHALLWAY \ + { \ + 0.4287f, 1.0000f, 0.3162f, 0.0100f, 1.0000f, 0.3000f, 0.1000f, 1.0000f, 0.1215f, 0.0020f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.1531f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_HALLWAY \ + { \ + 0.3645f, 1.0000f, 0.3162f, 0.7079f, 1.0000f, 1.4900f, 0.5900f, 1.0000f, 0.2458f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.6615f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_STONECORRIDOR \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.7612f, 1.0000f, 2.7000f, 0.7900f, 1.0000f, 0.2472f, 0.0130f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.5758f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_ALLEY \ + { \ + 1.0000f, 0.3000f, 0.3162f, 0.7328f, 1.0000f, 1.4900f, 0.8600f, 1.0000f, 0.2500f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.9954f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.9500f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_FOREST \ + { \ + 1.0000f, 0.3000f, 0.3162f, 0.0224f, 1.0000f, 1.4900f, 0.5400f, 1.0000f, 0.0525f, 0.1620f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7682f, 0.0880f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 1.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CITY \ + { \ + 1.0000f, 0.5000f, 0.3162f, 0.3981f, 1.0000f, 1.4900f, 0.6700f, 1.0000f, 0.0730f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.1427f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_MOUNTAINS \ + { \ + 1.0000f, 0.2700f, 0.3162f, 0.0562f, 1.0000f, 1.4900f, 0.2100f, 1.0000f, 0.0407f, 0.3000f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.1919f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_QUARRY \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.3162f, 1.0000f, 1.4900f, 0.8300f, 1.0000f, 0.0000f, 0.0610f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0250f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.7000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_PLAIN \ + { \ + 1.0000f, 0.2100f, 0.3162f, 0.1000f, 1.0000f, 1.4900f, 0.5000f, 1.0000f, 0.0585f, 0.1790f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.1089f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_PARKINGLOT \ + { \ + 1.0000f, 1.0000f, 0.3162f, 1.0000f, 1.0000f, 1.6500f, 1.5000f, 1.0000f, 0.2082f, 0.0080f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.2652f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_SEWERPIPE \ + { \ + 0.3071f, 0.8000f, 0.3162f, 0.3162f, 1.0000f, 2.8100f, 0.1400f, 1.0000f, 1.6387f, 0.0140f, \ + { 0.0000f, 0.0000f, 0.0000f }, 3.2471f, 0.0210f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_UNDERWATER \ + { \ + 0.3645f, 1.0000f, 0.3162f, 0.0100f, 1.0000f, 1.4900f, 0.1000f, 1.0000f, 0.5963f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 7.0795f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 1.1800f, \ + 0.3480f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_DRUGGED \ + { \ + 0.4287f, 0.5000f, 0.3162f, 1.0000f, 1.0000f, 8.3900f, 1.3900f, 1.0000f, 0.8760f, 0.0020f, \ + { 0.0000f, 0.0000f, 0.0000f }, 3.1081f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 1.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_DIZZY \ + { \ + 0.3645f, 0.6000f, 0.3162f, 0.6310f, 1.0000f, 17.2300f, 0.5600f, 1.0000f, 0.1392f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.4937f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.8100f, \ + 0.3100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_PSYCHOTIC \ + { \ + 0.0625f, 0.5000f, 0.3162f, 0.8404f, 1.0000f, 7.5600f, 0.9100f, 1.0000f, 0.4864f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 2.4378f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 4.0000f, \ + 1.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } /* Castle Presets */ -#define EFX_REVERB_PRESET_CASTLE_SMALLROOM \ - { 1.0000f, 0.8900f, 0.3162f, 0.3981f, 0.1000f, 1.2200f, 0.8300f, 0.3100f, 0.8913f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CASTLE_SHORTPASSAGE \ - { 1.0000f, 0.8900f, 0.3162f, 0.3162f, 0.1000f, 2.3200f, 0.8300f, 0.3100f, 0.8913f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CASTLE_MEDIUMROOM \ - { 1.0000f, 0.9300f, 0.3162f, 0.2818f, 0.1000f, 2.0400f, 0.8300f, 0.4600f, 0.6310f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1550f, 0.0300f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CASTLE_LARGEROOM \ - { 1.0000f, 0.8200f, 0.3162f, 0.2818f, 0.1259f, 2.5300f, 0.8300f, 0.5000f, 0.4467f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1850f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CASTLE_LONGPASSAGE \ - { 1.0000f, 0.8900f, 0.3162f, 0.3981f, 0.1000f, 3.4200f, 0.8300f, 0.3100f, 0.8913f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CASTLE_HALL \ - { 1.0000f, 0.8100f, 0.3162f, 0.2818f, 0.1778f, 3.1400f, 0.7900f, 0.6200f, 0.1778f, 0.0560f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CASTLE_CUPBOARD \ - { 1.0000f, 0.8900f, 0.3162f, 0.2818f, 0.1000f, 0.6700f, 0.8700f, 0.3100f, 1.4125f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 3.5481f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CASTLE_COURTYARD \ - { 1.0000f, 0.4200f, 0.3162f, 0.4467f, 0.1995f, 2.1300f, 0.6100f, 0.2300f, 0.2239f, 0.1600f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0360f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.3700f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_CASTLE_ALCOVE \ - { 1.0000f, 0.8900f, 0.3162f, 0.5012f, 0.1000f, 1.6400f, 0.8700f, 0.3100f, 1.0000f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 } +#define EFX_REVERB_PRESET_CASTLE_SMALLROOM \ + { \ + 1.0000f, 0.8900f, 0.3162f, 0.3981f, 0.1000f, 1.2200f, 0.8300f, 0.3100f, 0.8913f, 0.0220f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, \ + 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CASTLE_SHORTPASSAGE \ + { \ + 1.0000f, 0.8900f, 0.3162f, 0.3162f, 0.1000f, 2.3200f, 0.8300f, 0.3100f, 0.8913f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, \ + 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CASTLE_MEDIUMROOM \ + { \ + 1.0000f, 0.9300f, 0.3162f, 0.2818f, 0.1000f, 2.0400f, 0.8300f, 0.4600f, 0.6310f, 0.0220f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1550f, 0.0300f, 0.2500f, \ + 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CASTLE_LARGEROOM \ + { \ + 1.0000f, 0.8200f, 0.3162f, 0.2818f, 0.1259f, 2.5300f, 0.8300f, 0.5000f, 0.4467f, 0.0340f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1850f, 0.0700f, 0.2500f, \ + 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CASTLE_LONGPASSAGE \ + { \ + 1.0000f, 0.8900f, 0.3162f, 0.3981f, 0.1000f, 3.4200f, 0.8300f, 0.3100f, 0.8913f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, \ + 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CASTLE_HALL \ + { \ + 1.0000f, 0.8100f, 0.3162f, 0.2818f, 0.1778f, 3.1400f, 0.7900f, 0.6200f, 0.1778f, 0.0560f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CASTLE_CUPBOARD \ + { \ + 1.0000f, 0.8900f, 0.3162f, 0.2818f, 0.1000f, 0.6700f, 0.8700f, 0.3100f, 1.4125f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 3.5481f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, \ + 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CASTLE_COURTYARD \ + { \ + 1.0000f, 0.4200f, 0.3162f, 0.4467f, 0.1995f, 2.1300f, 0.6100f, 0.2300f, 0.2239f, 0.1600f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0360f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.3700f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_CASTLE_ALCOVE \ + { \ + 1.0000f, 0.8900f, 0.3162f, 0.5012f, 0.1000f, 1.6400f, 0.8700f, 0.3100f, 1.0000f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 0.1380f, 0.0800f, 0.2500f, \ + 0.0000f, 0.9943f, 5168.6001f, 139.5000f, 0.0000f, 0x1 \ + } /* Factory Presets */ -#define EFX_REVERB_PRESET_FACTORY_SMALLROOM \ - { 0.3645f, 0.8200f, 0.3162f, 0.7943f, 0.5012f, 1.7200f, 0.6500f, 1.3100f, 0.7079f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.1190f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_FACTORY_SHORTPASSAGE \ - { 0.3645f, 0.6400f, 0.2512f, 0.7943f, 0.5012f, 2.5300f, 0.6500f, 1.3100f, 1.0000f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.1350f, 0.2300f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_FACTORY_MEDIUMROOM \ - { 0.4287f, 0.8200f, 0.2512f, 0.7943f, 0.5012f, 2.7600f, 0.6500f, 1.3100f, 0.2818f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1740f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_FACTORY_LARGEROOM \ - { 0.4287f, 0.7500f, 0.2512f, 0.7079f, 0.6310f, 4.2400f, 0.5100f, 1.3100f, 0.1778f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.2310f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_FACTORY_LONGPASSAGE \ - { 0.3645f, 0.6400f, 0.2512f, 0.7943f, 0.5012f, 4.0600f, 0.6500f, 1.3100f, 1.0000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0370f, { 0.0000f, 0.0000f, 0.0000f }, 0.1350f, 0.2300f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_FACTORY_HALL \ - { 0.4287f, 0.7500f, 0.3162f, 0.7079f, 0.6310f, 7.4300f, 0.5100f, 1.3100f, 0.0631f, 0.0730f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_FACTORY_CUPBOARD \ - { 0.3071f, 0.6300f, 0.2512f, 0.7943f, 0.5012f, 0.4900f, 0.6500f, 1.3100f, 1.2589f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.1070f, 0.0700f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_FACTORY_COURTYARD \ - { 0.3071f, 0.5700f, 0.3162f, 0.3162f, 0.6310f, 2.3200f, 0.2900f, 0.5600f, 0.2239f, 0.1400f, { 0.0000f, 0.0000f, 0.0000f }, 0.3981f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2900f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_FACTORY_ALCOVE \ - { 0.3645f, 0.5900f, 0.2512f, 0.7943f, 0.5012f, 3.1400f, 0.6500f, 1.3100f, 1.4125f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.1140f, 0.1000f, 0.2500f, 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 } +#define EFX_REVERB_PRESET_FACTORY_SMALLROOM \ + { \ + 0.3645f, 0.8200f, 0.3162f, 0.7943f, 0.5012f, 1.7200f, 0.6500f, 1.3100f, 0.7079f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.1190f, 0.0700f, 0.2500f, \ + 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_FACTORY_SHORTPASSAGE \ + { \ + 0.3645f, 0.6400f, 0.2512f, 0.7943f, 0.5012f, 2.5300f, 0.6500f, 1.3100f, 1.0000f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.1350f, 0.2300f, 0.2500f, \ + 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_FACTORY_MEDIUMROOM \ + { \ + 0.4287f, 0.8200f, 0.2512f, 0.7943f, 0.5012f, 2.7600f, 0.6500f, 1.3100f, 0.2818f, 0.0220f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1740f, 0.0700f, 0.2500f, \ + 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_FACTORY_LARGEROOM \ + { \ + 0.4287f, 0.7500f, 0.2512f, 0.7079f, 0.6310f, 4.2400f, 0.5100f, 1.3100f, 0.1778f, 0.0390f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.2310f, 0.0700f, 0.2500f, \ + 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_FACTORY_LONGPASSAGE \ + { \ + 0.3645f, 0.6400f, 0.2512f, 0.7943f, 0.5012f, 4.0600f, 0.6500f, 1.3100f, 1.0000f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0370f, { 0.0000f, 0.0000f, 0.0000f }, 0.1350f, 0.2300f, 0.2500f, \ + 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_FACTORY_HALL \ + { \ + 0.4287f, 0.7500f, 0.3162f, 0.7079f, 0.6310f, 7.4300f, 0.5100f, 1.3100f, 0.0631f, 0.0730f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0700f, 0.2500f, \ + 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_FACTORY_CUPBOARD \ + { \ + 0.3071f, 0.6300f, 0.2512f, 0.7943f, 0.5012f, 0.4900f, 0.6500f, 1.3100f, 1.2589f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.1070f, 0.0700f, 0.2500f, \ + 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_FACTORY_COURTYARD \ + { \ + 0.3071f, 0.5700f, 0.3162f, 0.3162f, 0.6310f, 2.3200f, 0.2900f, 0.5600f, 0.2239f, 0.1400f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.3981f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2900f, 0.2500f, \ + 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_FACTORY_ALCOVE \ + { \ + 0.3645f, 0.5900f, 0.2512f, 0.7943f, 0.5012f, 3.1400f, 0.6500f, 1.3100f, 1.4125f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.1140f, 0.1000f, 0.2500f, \ + 0.0000f, 0.9943f, 3762.6001f, 362.5000f, 0.0000f, 0x1 \ + } /* Ice Palace Presets */ -#define EFX_REVERB_PRESET_ICEPALACE_SMALLROOM \ - { 1.0000f, 0.8400f, 0.3162f, 0.5623f, 0.2818f, 1.5100f, 1.5300f, 0.2700f, 0.8913f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1640f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_ICEPALACE_SHORTPASSAGE \ - { 1.0000f, 0.7500f, 0.3162f, 0.5623f, 0.2818f, 1.7900f, 1.4600f, 0.2800f, 0.5012f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.1770f, 0.0900f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_ICEPALACE_MEDIUMROOM \ - { 1.0000f, 0.8700f, 0.3162f, 0.5623f, 0.4467f, 2.2200f, 1.5300f, 0.3200f, 0.3981f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.1860f, 0.1200f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_ICEPALACE_LARGEROOM \ - { 1.0000f, 0.8100f, 0.3162f, 0.5623f, 0.4467f, 3.1400f, 1.5300f, 0.3200f, 0.2512f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.2140f, 0.1100f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_ICEPALACE_LONGPASSAGE \ - { 1.0000f, 0.7700f, 0.3162f, 0.5623f, 0.3981f, 3.0100f, 1.4600f, 0.2800f, 0.7943f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0250f, { 0.0000f, 0.0000f, 0.0000f }, 0.1860f, 0.0400f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_ICEPALACE_HALL \ - { 1.0000f, 0.7600f, 0.3162f, 0.4467f, 0.5623f, 5.4900f, 1.5300f, 0.3800f, 0.1122f, 0.0540f, { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0520f, { 0.0000f, 0.0000f, 0.0000f }, 0.2260f, 0.1100f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_ICEPALACE_CUPBOARD \ - { 1.0000f, 0.8300f, 0.3162f, 0.5012f, 0.2239f, 0.7600f, 1.5300f, 0.2600f, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1430f, 0.0800f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_ICEPALACE_COURTYARD \ - { 1.0000f, 0.5900f, 0.3162f, 0.2818f, 0.3162f, 2.0400f, 1.2000f, 0.3800f, 0.3162f, 0.1730f, { 0.0000f, 0.0000f, 0.0000f }, 0.3162f, 0.0430f, { 0.0000f, 0.0000f, 0.0000f }, 0.2350f, 0.4800f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_ICEPALACE_ALCOVE \ - { 1.0000f, 0.8400f, 0.3162f, 0.5623f, 0.2818f, 2.7600f, 1.4600f, 0.2800f, 1.1220f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1610f, 0.0900f, 0.2500f, 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 } +#define EFX_REVERB_PRESET_ICEPALACE_SMALLROOM \ + { \ + 1.0000f, 0.8400f, 0.3162f, 0.5623f, 0.2818f, 1.5100f, 1.5300f, 0.2700f, 0.8913f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1640f, 0.1400f, 0.2500f, \ + 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_ICEPALACE_SHORTPASSAGE \ + { \ + 1.0000f, 0.7500f, 0.3162f, 0.5623f, 0.2818f, 1.7900f, 1.4600f, 0.2800f, 0.5012f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.1770f, 0.0900f, 0.2500f, \ + 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_ICEPALACE_MEDIUMROOM \ + { \ + 1.0000f, 0.8700f, 0.3162f, 0.5623f, 0.4467f, 2.2200f, 1.5300f, 0.3200f, 0.3981f, 0.0390f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.1860f, 0.1200f, 0.2500f, \ + 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_ICEPALACE_LARGEROOM \ + { \ + 1.0000f, 0.8100f, 0.3162f, 0.5623f, 0.4467f, 3.1400f, 1.5300f, 0.3200f, 0.2512f, 0.0390f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0270f, { 0.0000f, 0.0000f, 0.0000f }, 0.2140f, 0.1100f, 0.2500f, \ + 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_ICEPALACE_LONGPASSAGE \ + { \ + 1.0000f, 0.7700f, 0.3162f, 0.5623f, 0.3981f, 3.0100f, 1.4600f, 0.2800f, 0.7943f, 0.0120f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0250f, { 0.0000f, 0.0000f, 0.0000f }, 0.1860f, 0.0400f, 0.2500f, \ + 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_ICEPALACE_HALL \ + { \ + 1.0000f, 0.7600f, 0.3162f, 0.4467f, 0.5623f, 5.4900f, 1.5300f, 0.3800f, 0.1122f, 0.0540f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0520f, { 0.0000f, 0.0000f, 0.0000f }, 0.2260f, 0.1100f, 0.2500f, \ + 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_ICEPALACE_CUPBOARD \ + { \ + 1.0000f, 0.8300f, 0.3162f, 0.5012f, 0.2239f, 0.7600f, 1.5300f, 0.2600f, 1.1220f, 0.0120f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1430f, 0.0800f, 0.2500f, \ + 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_ICEPALACE_COURTYARD \ + { \ + 1.0000f, 0.5900f, 0.3162f, 0.2818f, 0.3162f, 2.0400f, 1.2000f, 0.3800f, 0.3162f, 0.1730f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.3162f, 0.0430f, { 0.0000f, 0.0000f, 0.0000f }, 0.2350f, 0.4800f, 0.2500f, \ + 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_ICEPALACE_ALCOVE \ + { \ + 1.0000f, 0.8400f, 0.3162f, 0.5623f, 0.2818f, 2.7600f, 1.4600f, 0.2800f, 1.1220f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1610f, 0.0900f, 0.2500f, \ + 0.0000f, 0.9943f, 12428.5000f, 99.6000f, 0.0000f, 0x1 \ + } /* Space Station Presets */ -#define EFX_REVERB_PRESET_SPACESTATION_SMALLROOM \ - { 0.2109f, 0.7000f, 0.3162f, 0.7079f, 0.8913f, 1.7200f, 0.8200f, 0.5500f, 0.7943f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0130f, { 0.0000f, 0.0000f, 0.0000f }, 0.1880f, 0.2600f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SPACESTATION_SHORTPASSAGE \ - { 0.2109f, 0.8700f, 0.3162f, 0.6310f, 0.8913f, 3.5700f, 0.5000f, 0.5500f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1720f, 0.2000f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SPACESTATION_MEDIUMROOM \ - { 0.2109f, 0.7500f, 0.3162f, 0.6310f, 0.8913f, 3.0100f, 0.5000f, 0.5500f, 0.3981f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0350f, { 0.0000f, 0.0000f, 0.0000f }, 0.2090f, 0.3100f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SPACESTATION_LARGEROOM \ - { 0.3645f, 0.8100f, 0.3162f, 0.6310f, 0.8913f, 3.8900f, 0.3800f, 0.6100f, 0.3162f, 0.0560f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0350f, { 0.0000f, 0.0000f, 0.0000f }, 0.2330f, 0.2800f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SPACESTATION_LONGPASSAGE \ - { 0.4287f, 0.8200f, 0.3162f, 0.6310f, 0.8913f, 4.6200f, 0.6200f, 0.5500f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0310f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2300f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SPACESTATION_HALL \ - { 0.4287f, 0.8700f, 0.3162f, 0.6310f, 0.8913f, 7.1100f, 0.3800f, 0.6100f, 0.1778f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0470f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2500f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SPACESTATION_CUPBOARD \ - { 0.1715f, 0.5600f, 0.3162f, 0.7079f, 0.8913f, 0.7900f, 0.8100f, 0.5500f, 1.4125f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0180f, { 0.0000f, 0.0000f, 0.0000f }, 0.1810f, 0.3100f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SPACESTATION_ALCOVE \ - { 0.2109f, 0.7800f, 0.3162f, 0.7079f, 0.8913f, 1.1600f, 0.8100f, 0.5500f, 1.4125f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0180f, { 0.0000f, 0.0000f, 0.0000f }, 0.1920f, 0.2100f, 0.2500f, 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 } +#define EFX_REVERB_PRESET_SPACESTATION_SMALLROOM \ + { \ + 0.2109f, 0.7000f, 0.3162f, 0.7079f, 0.8913f, 1.7200f, 0.8200f, 0.5500f, 0.7943f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0130f, { 0.0000f, 0.0000f, 0.0000f }, 0.1880f, 0.2600f, 0.2500f, \ + 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SPACESTATION_SHORTPASSAGE \ + { \ + 0.2109f, 0.8700f, 0.3162f, 0.6310f, 0.8913f, 3.5700f, 0.5000f, 0.5500f, 1.0000f, 0.0120f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.1720f, 0.2000f, 0.2500f, \ + 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SPACESTATION_MEDIUMROOM \ + { \ + 0.2109f, 0.7500f, 0.3162f, 0.6310f, 0.8913f, 3.0100f, 0.5000f, 0.5500f, 0.3981f, 0.0340f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0350f, { 0.0000f, 0.0000f, 0.0000f }, 0.2090f, 0.3100f, 0.2500f, \ + 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SPACESTATION_LARGEROOM \ + { \ + 0.3645f, 0.8100f, 0.3162f, 0.6310f, 0.8913f, 3.8900f, 0.3800f, 0.6100f, 0.3162f, 0.0560f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0350f, { 0.0000f, 0.0000f, 0.0000f }, 0.2330f, 0.2800f, 0.2500f, \ + 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SPACESTATION_LONGPASSAGE \ + { \ + 0.4287f, 0.8200f, 0.3162f, 0.6310f, 0.8913f, 4.6200f, 0.6200f, 0.5500f, 1.0000f, 0.0120f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0310f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2300f, 0.2500f, \ + 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SPACESTATION_HALL \ + { \ + 0.4287f, 0.8700f, 0.3162f, 0.6310f, 0.8913f, 7.1100f, 0.3800f, 0.6100f, 0.1778f, 0.1000f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0470f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2500f, 0.2500f, \ + 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SPACESTATION_CUPBOARD \ + { \ + 0.1715f, 0.5600f, 0.3162f, 0.7079f, 0.8913f, 0.7900f, 0.8100f, 0.5500f, 1.4125f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.7783f, 0.0180f, { 0.0000f, 0.0000f, 0.0000f }, 0.1810f, 0.3100f, 0.2500f, \ + 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SPACESTATION_ALCOVE \ + { \ + 0.2109f, 0.7800f, 0.3162f, 0.7079f, 0.8913f, 1.1600f, 0.8100f, 0.5500f, 1.4125f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0180f, { 0.0000f, 0.0000f, 0.0000f }, 0.1920f, 0.2100f, 0.2500f, \ + 0.0000f, 0.9943f, 3316.1001f, 458.2000f, 0.0000f, 0x1 \ + } /* Wooden Galleon Presets */ -#define EFX_REVERB_PRESET_WOODEN_SMALLROOM \ - { 1.0000f, 1.0000f, 0.3162f, 0.1122f, 0.3162f, 0.7900f, 0.3200f, 0.8700f, 1.0000f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_WOODEN_SHORTPASSAGE \ - { 1.0000f, 1.0000f, 0.3162f, 0.1259f, 0.3162f, 1.7500f, 0.5000f, 0.8700f, 0.8913f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_WOODEN_MEDIUMROOM \ - { 1.0000f, 1.0000f, 0.3162f, 0.1000f, 0.2818f, 1.4700f, 0.4200f, 0.8200f, 0.8913f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_WOODEN_LARGEROOM \ - { 1.0000f, 1.0000f, 0.3162f, 0.0891f, 0.2818f, 2.6500f, 0.3300f, 0.8200f, 0.8913f, 0.0660f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_WOODEN_LONGPASSAGE \ - { 1.0000f, 1.0000f, 0.3162f, 0.1000f, 0.3162f, 1.9900f, 0.4000f, 0.7900f, 1.0000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.4467f, 0.0360f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_WOODEN_HALL \ - { 1.0000f, 1.0000f, 0.3162f, 0.0794f, 0.2818f, 3.4500f, 0.3000f, 0.8200f, 0.8913f, 0.0880f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0630f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_WOODEN_CUPBOARD \ - { 1.0000f, 1.0000f, 0.3162f, 0.1413f, 0.3162f, 0.5600f, 0.4600f, 0.9100f, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_WOODEN_COURTYARD \ - { 1.0000f, 0.6500f, 0.3162f, 0.0794f, 0.3162f, 1.7900f, 0.3500f, 0.7900f, 0.5623f, 0.1230f, { 0.0000f, 0.0000f, 0.0000f }, 0.1000f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_WOODEN_ALCOVE \ - { 1.0000f, 1.0000f, 0.3162f, 0.1259f, 0.3162f, 1.2200f, 0.6200f, 0.9100f, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 } +#define EFX_REVERB_PRESET_WOODEN_SMALLROOM \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.1122f, 0.3162f, 0.7900f, 0.3200f, 0.8700f, 1.0000f, 0.0320f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_WOODEN_SHORTPASSAGE \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.1259f, 0.3162f, 1.7500f, 0.5000f, 0.8700f, 0.8913f, 0.0120f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.6310f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_WOODEN_MEDIUMROOM \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.1000f, 0.2818f, 1.4700f, 0.4200f, 0.8200f, 0.8913f, 0.0490f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_WOODEN_LARGEROOM \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.0891f, 0.2818f, 2.6500f, 0.3300f, 0.8200f, 0.8913f, 0.0660f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_WOODEN_LONGPASSAGE \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.1000f, 0.3162f, 1.9900f, 0.4000f, 0.7900f, 1.0000f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.4467f, 0.0360f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_WOODEN_HALL \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.0794f, 0.2818f, 3.4500f, 0.3000f, 0.8200f, 0.8913f, 0.0880f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0630f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_WOODEN_CUPBOARD \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.1413f, 0.3162f, 0.5600f, 0.4600f, 0.9100f, 1.1220f, 0.0120f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_WOODEN_COURTYARD \ + { \ + 1.0000f, 0.6500f, 0.3162f, 0.0794f, 0.3162f, 1.7900f, 0.3500f, 0.7900f, 0.5623f, 0.1230f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.1000f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_WOODEN_ALCOVE \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.1259f, 0.3162f, 1.2200f, 0.6200f, 0.9100f, 1.1220f, 0.0120f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 4705.0000f, 99.6000f, 0.0000f, 0x1 \ + } /* Sports Presets */ -#define EFX_REVERB_PRESET_SPORT_EMPTYSTADIUM \ - { 1.0000f, 1.0000f, 0.3162f, 0.4467f, 0.7943f, 6.2600f, 0.5100f, 1.1000f, 0.0631f, 0.1830f, { 0.0000f, 0.0000f, 0.0000f }, 0.3981f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SPORT_SQUASHCOURT \ - { 1.0000f, 0.7500f, 0.3162f, 0.3162f, 0.7943f, 2.2200f, 0.9100f, 1.1600f, 0.4467f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1260f, 0.1900f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SPORT_SMALLSWIMMINGPOOL \ - { 1.0000f, 0.7000f, 0.3162f, 0.7943f, 0.8913f, 2.7600f, 1.2500f, 1.1400f, 0.6310f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1790f, 0.1500f, 0.8950f, 0.1900f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_SPORT_LARGESWIMMINGPOOL \ - { 1.0000f, 0.8200f, 0.3162f, 0.7943f, 1.0000f, 5.4900f, 1.3100f, 1.1400f, 0.4467f, 0.0390f, { 0.0000f, 0.0000f, 0.0000f }, 0.5012f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2220f, 0.5500f, 1.1590f, 0.2100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_SPORT_GYMNASIUM \ - { 1.0000f, 0.8100f, 0.3162f, 0.4467f, 0.8913f, 3.1400f, 1.0600f, 1.3500f, 0.3981f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.5623f, 0.0450f, { 0.0000f, 0.0000f, 0.0000f }, 0.1460f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SPORT_FULLSTADIUM \ - { 1.0000f, 1.0000f, 0.3162f, 0.0708f, 0.7943f, 5.2500f, 0.1700f, 0.8000f, 0.1000f, 0.1880f, { 0.0000f, 0.0000f, 0.0000f }, 0.2818f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SPORT_STADIUMTANNOY \ - { 1.0000f, 0.7800f, 0.3162f, 0.5623f, 0.5012f, 2.5300f, 0.8800f, 0.6800f, 0.2818f, 0.2300f, { 0.0000f, 0.0000f, 0.0000f }, 0.5012f, 0.0630f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } +#define EFX_REVERB_PRESET_SPORT_EMPTYSTADIUM \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.4467f, 0.7943f, 6.2600f, 0.5100f, 1.1000f, 0.0631f, 0.1830f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.3981f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SPORT_SQUASHCOURT \ + { \ + 1.0000f, 0.7500f, 0.3162f, 0.3162f, 0.7943f, 2.2200f, 0.9100f, 1.1600f, 0.4467f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.1260f, 0.1900f, 0.2500f, \ + 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SPORT_SMALLSWIMMINGPOOL \ + { \ + 1.0000f, 0.7000f, 0.3162f, 0.7943f, 0.8913f, 2.7600f, 1.2500f, 1.1400f, 0.6310f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1790f, 0.1500f, 0.8950f, \ + 0.1900f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_SPORT_LARGESWIMMINGPOOL \ + { \ + 1.0000f, 0.8200f, 0.3162f, 0.7943f, 1.0000f, 5.4900f, 1.3100f, 1.1400f, 0.4467f, 0.0390f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.5012f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2220f, 0.5500f, 1.1590f, \ + 0.2100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_SPORT_GYMNASIUM \ + { \ + 1.0000f, 0.8100f, 0.3162f, 0.4467f, 0.8913f, 3.1400f, 1.0600f, 1.3500f, 0.3981f, 0.0290f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.5623f, 0.0450f, { 0.0000f, 0.0000f, 0.0000f }, 0.1460f, 0.1400f, 0.2500f, \ + 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SPORT_FULLSTADIUM \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.0708f, 0.7943f, 5.2500f, 0.1700f, 0.8000f, 0.1000f, 0.1880f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.2818f, 0.0380f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SPORT_STADIUMTANNOY \ + { \ + 1.0000f, 0.7800f, 0.3162f, 0.5623f, 0.5012f, 2.5300f, 0.8800f, 0.6800f, 0.2818f, 0.2300f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.5012f, 0.0630f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } /* Prefab Presets */ -#define EFX_REVERB_PRESET_PREFAB_WORKSHOP \ - { 0.4287f, 1.0000f, 0.3162f, 0.1413f, 0.3981f, 0.7600f, 1.0000f, 1.0000f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_PREFAB_SCHOOLROOM \ - { 0.4022f, 0.6900f, 0.3162f, 0.6310f, 0.5012f, 0.9800f, 0.4500f, 0.1800f, 1.4125f, 0.0170f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.0950f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_PREFAB_PRACTISEROOM \ - { 0.4022f, 0.8700f, 0.3162f, 0.3981f, 0.5012f, 1.1200f, 0.5600f, 0.1800f, 1.2589f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.0950f, 0.1400f, 0.2500f, 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_PREFAB_OUTHOUSE \ - { 1.0000f, 0.8200f, 0.3162f, 0.1122f, 0.1585f, 1.3800f, 0.3800f, 0.3500f, 0.8913f, 0.0240f, { 0.0000f, 0.0000f, -0.0000f }, 0.6310f, 0.0440f, { 0.0000f, 0.0000f, 0.0000f }, 0.1210f, 0.1700f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_PREFAB_CARAVAN \ - { 1.0000f, 1.0000f, 0.3162f, 0.0891f, 0.1259f, 0.4300f, 1.5000f, 1.0000f, 1.0000f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } +#define EFX_REVERB_PRESET_PREFAB_WORKSHOP \ + { \ + 0.4287f, 1.0000f, 0.3162f, 0.1413f, 0.3981f, 0.7600f, 1.0000f, 1.0000f, 1.0000f, 0.0120f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_PREFAB_SCHOOLROOM \ + { \ + 0.4022f, 0.6900f, 0.3162f, 0.6310f, 0.5012f, 0.9800f, 0.4500f, 0.1800f, 1.4125f, 0.0170f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.0950f, 0.1400f, 0.2500f, \ + 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_PREFAB_PRACTISEROOM \ + { \ + 0.4022f, 0.8700f, 0.3162f, 0.3981f, 0.5012f, 1.1200f, 0.5600f, 0.1800f, 1.2589f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0110f, { 0.0000f, 0.0000f, 0.0000f }, 0.0950f, 0.1400f, 0.2500f, \ + 0.0000f, 0.9943f, 7176.8999f, 211.2000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_PREFAB_OUTHOUSE \ + { \ + 1.0000f, 0.8200f, 0.3162f, 0.1122f, 0.1585f, 1.3800f, 0.3800f, 0.3500f, 0.8913f, 0.0240f, \ + { 0.0000f, 0.0000f, -0.0000f }, 0.6310f, 0.0440f, { 0.0000f, 0.0000f, 0.0000f }, 0.1210f, 0.1700f, \ + 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_PREFAB_CARAVAN \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.0891f, 0.1259f, 0.4300f, 1.5000f, 1.0000f, 1.0000f, 0.0120f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.9953f, 0.0120f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } /* Dome and Pipe Presets */ -#define EFX_REVERB_PRESET_DOME_TOMB \ - { 1.0000f, 0.7900f, 0.3162f, 0.3548f, 0.2239f, 4.1800f, 0.2100f, 0.1000f, 0.3868f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 1.6788f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.1770f, 0.1900f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_PIPE_SMALL \ - { 1.0000f, 1.0000f, 0.3162f, 0.3548f, 0.2239f, 5.0400f, 0.1000f, 0.1000f, 0.5012f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 2.5119f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_DOME_SAINTPAULS \ - { 1.0000f, 0.8700f, 0.3162f, 0.3548f, 0.2239f, 10.4800f, 0.1900f, 0.1000f, 0.1778f, 0.0900f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0420f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1200f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_PIPE_LONGTHIN \ - { 0.2560f, 0.9100f, 0.3162f, 0.4467f, 0.2818f, 9.2100f, 0.1800f, 0.1000f, 0.7079f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_PIPE_LARGE \ - { 1.0000f, 1.0000f, 0.3162f, 0.3548f, 0.2239f, 8.4500f, 0.1000f, 0.1000f, 0.3981f, 0.0460f, { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_PIPE_RESONANT \ - { 0.1373f, 0.9100f, 0.3162f, 0.4467f, 0.2818f, 6.8100f, 0.1800f, 0.1000f, 0.7079f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 } +#define EFX_REVERB_PRESET_DOME_TOMB \ + { \ + 1.0000f, 0.7900f, 0.3162f, 0.3548f, 0.2239f, 4.1800f, 0.2100f, 0.1000f, 0.3868f, 0.0300f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.6788f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.1770f, 0.1900f, 0.2500f, \ + 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_PIPE_SMALL \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.3548f, 0.2239f, 5.0400f, 0.1000f, 0.1000f, 0.5012f, 0.0320f, \ + { 0.0000f, 0.0000f, 0.0000f }, 2.5119f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_DOME_SAINTPAULS \ + { \ + 1.0000f, 0.8700f, 0.3162f, 0.3548f, 0.2239f, 10.4800f, 0.1900f, 0.1000f, 0.1778f, 0.0900f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0420f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1200f, 0.2500f, \ + 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_PIPE_LONGTHIN \ + { \ + 0.2560f, 0.9100f, 0.3162f, 0.4467f, 0.2818f, 9.2100f, 0.1800f, 0.1000f, 0.7079f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_PIPE_LARGE \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.3548f, 0.2239f, 8.4500f, 0.1000f, 0.1000f, 0.3981f, 0.0460f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_PIPE_RESONANT \ + { \ + 0.1373f, 0.9100f, 0.3162f, 0.4467f, 0.2818f, 6.8100f, 0.1800f, 0.1000f, 0.7079f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.0000f, 0.0220f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 2854.3999f, 20.0000f, 0.0000f, 0x0 \ + } /* Outdoors Presets */ -#define EFX_REVERB_PRESET_OUTDOORS_BACKYARD \ - { 1.0000f, 0.4500f, 0.3162f, 0.2512f, 0.5012f, 1.1200f, 0.3400f, 0.4600f, 0.4467f, 0.0690f, { 0.0000f, 0.0000f, -0.0000f }, 0.7079f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.2180f, 0.3400f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_OUTDOORS_ROLLINGPLAINS \ - { 1.0000f, 0.0000f, 0.3162f, 0.0112f, 0.6310f, 2.1300f, 0.2100f, 0.4600f, 0.1778f, 0.3000f, { 0.0000f, 0.0000f, -0.0000f }, 0.4467f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_OUTDOORS_DEEPCANYON \ - { 1.0000f, 0.7400f, 0.3162f, 0.1778f, 0.6310f, 3.8900f, 0.2100f, 0.4600f, 0.3162f, 0.2230f, { 0.0000f, 0.0000f, -0.0000f }, 0.3548f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_OUTDOORS_CREEK \ - { 1.0000f, 0.3500f, 0.3162f, 0.1778f, 0.5012f, 2.1300f, 0.2100f, 0.4600f, 0.3981f, 0.1150f, { 0.0000f, 0.0000f, -0.0000f }, 0.1995f, 0.0310f, { 0.0000f, 0.0000f, 0.0000f }, 0.2180f, 0.3400f, 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_OUTDOORS_VALLEY \ - { 1.0000f, 0.2800f, 0.3162f, 0.0282f, 0.1585f, 2.8800f, 0.2600f, 0.3500f, 0.1413f, 0.2630f, { 0.0000f, 0.0000f, -0.0000f }, 0.3981f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.3400f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 } +#define EFX_REVERB_PRESET_OUTDOORS_BACKYARD \ + { \ + 1.0000f, 0.4500f, 0.3162f, 0.2512f, 0.5012f, 1.1200f, 0.3400f, 0.4600f, 0.4467f, 0.0690f, \ + { 0.0000f, 0.0000f, -0.0000f }, 0.7079f, 0.0230f, { 0.0000f, 0.0000f, 0.0000f }, 0.2180f, 0.3400f, \ + 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_OUTDOORS_ROLLINGPLAINS \ + { \ + 1.0000f, 0.0000f, 0.3162f, 0.0112f, 0.6310f, 2.1300f, 0.2100f, 0.4600f, 0.1778f, 0.3000f, \ + { 0.0000f, 0.0000f, -0.0000f }, 0.4467f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, \ + 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_OUTDOORS_DEEPCANYON \ + { \ + 1.0000f, 0.7400f, 0.3162f, 0.1778f, 0.6310f, 3.8900f, 0.2100f, 0.4600f, 0.3162f, 0.2230f, \ + { 0.0000f, 0.0000f, -0.0000f }, 0.3548f, 0.0190f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, \ + 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_OUTDOORS_CREEK \ + { \ + 1.0000f, 0.3500f, 0.3162f, 0.1778f, 0.5012f, 2.1300f, 0.2100f, 0.4600f, 0.3981f, 0.1150f, \ + { 0.0000f, 0.0000f, -0.0000f }, 0.1995f, 0.0310f, { 0.0000f, 0.0000f, 0.0000f }, 0.2180f, 0.3400f, \ + 0.2500f, 0.0000f, 0.9943f, 4399.1001f, 242.9000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_OUTDOORS_VALLEY \ + { \ + 1.0000f, 0.2800f, 0.3162f, 0.0282f, 0.1585f, 2.8800f, 0.2600f, 0.3500f, 0.1413f, 0.2630f, \ + { 0.0000f, 0.0000f, -0.0000f }, 0.3981f, 0.1000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.3400f, \ + 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 \ + } /* Mood Presets */ -#define EFX_REVERB_PRESET_MOOD_HEAVEN \ - { 1.0000f, 0.9400f, 0.3162f, 0.7943f, 0.4467f, 5.0400f, 1.1200f, 0.5600f, 0.2427f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0800f, 2.7420f, 0.0500f, 0.9977f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_MOOD_HELL \ - { 1.0000f, 0.5700f, 0.3162f, 0.3548f, 0.4467f, 3.5700f, 0.4900f, 2.0000f, 0.0000f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1100f, 0.0400f, 2.1090f, 0.5200f, 0.9943f, 5000.0000f, 139.5000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_MOOD_MEMORY \ - { 1.0000f, 0.8500f, 0.3162f, 0.6310f, 0.3548f, 4.0600f, 0.8200f, 0.5600f, 0.0398f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.4740f, 0.4500f, 0.9886f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } +#define EFX_REVERB_PRESET_MOOD_HEAVEN \ + { \ + 1.0000f, 0.9400f, 0.3162f, 0.7943f, 0.4467f, 5.0400f, 1.1200f, 0.5600f, 0.2427f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0290f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0800f, 2.7420f, \ + 0.0500f, 0.9977f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_MOOD_HELL \ + { \ + 1.0000f, 0.5700f, 0.3162f, 0.3548f, 0.4467f, 3.5700f, 0.4900f, 2.0000f, 0.0000f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1100f, 0.0400f, 2.1090f, \ + 0.5200f, 0.9943f, 5000.0000f, 139.5000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_MOOD_MEMORY \ + { \ + 1.0000f, 0.8500f, 0.3162f, 0.6310f, 0.3548f, 4.0600f, 0.8200f, 0.5600f, 0.0398f, 0.0000f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.1220f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.4740f, \ + 0.4500f, 0.9886f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } /* Driving Presets */ -#define EFX_REVERB_PRESET_DRIVING_COMMENTATOR \ - { 1.0000f, 0.0000f, 0.3162f, 0.5623f, 0.5012f, 2.4200f, 0.8800f, 0.6800f, 0.1995f, 0.0930f, { 0.0000f, 0.0000f, 0.0000f }, 0.2512f, 0.0170f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, 0.0000f, 0.9886f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_DRIVING_PITGARAGE \ - { 0.4287f, 0.5900f, 0.3162f, 0.7079f, 0.5623f, 1.7200f, 0.9300f, 0.8700f, 0.5623f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1100f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_DRIVING_INCAR_RACER \ - { 0.0832f, 0.8000f, 0.3162f, 1.0000f, 0.7943f, 0.1700f, 2.0000f, 0.4100f, 1.7783f, 0.0070f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_DRIVING_INCAR_SPORTS \ - { 0.0832f, 0.8000f, 0.3162f, 0.6310f, 1.0000f, 0.1700f, 0.7500f, 0.4100f, 1.0000f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.5623f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_DRIVING_INCAR_LUXURY \ - { 0.2560f, 1.0000f, 0.3162f, 0.1000f, 0.5012f, 0.1300f, 0.4100f, 0.4600f, 0.7943f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_DRIVING_FULLGRANDSTAND \ - { 1.0000f, 1.0000f, 0.3162f, 0.2818f, 0.6310f, 3.0100f, 1.3700f, 1.2800f, 0.3548f, 0.0900f, { 0.0000f, 0.0000f, 0.0000f }, 0.1778f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10420.2002f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_DRIVING_EMPTYGRANDSTAND \ - { 1.0000f, 1.0000f, 0.3162f, 1.0000f, 0.7943f, 4.6200f, 1.7500f, 1.4000f, 0.2082f, 0.0900f, { 0.0000f, 0.0000f, 0.0000f }, 0.2512f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.0000f, 0.9943f, 10420.2002f, 250.0000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_DRIVING_TUNNEL \ - { 1.0000f, 0.8100f, 0.3162f, 0.3981f, 0.8913f, 3.4200f, 0.9400f, 1.3100f, 0.7079f, 0.0510f, { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0470f, { 0.0000f, 0.0000f, 0.0000f }, 0.2140f, 0.0500f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 155.3000f, 0.0000f, 0x1 } +#define EFX_REVERB_PRESET_DRIVING_COMMENTATOR \ + { \ + 1.0000f, 0.0000f, 0.3162f, 0.5623f, 0.5012f, 2.4200f, 0.8800f, 0.6800f, 0.1995f, 0.0930f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.2512f, 0.0170f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 1.0000f, 0.2500f, \ + 0.0000f, 0.9886f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_DRIVING_PITGARAGE \ + { \ + 0.4287f, 0.5900f, 0.3162f, 0.7079f, 0.5623f, 1.7200f, 0.9300f, 0.8700f, 0.5623f, 0.0000f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0160f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1100f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_DRIVING_INCAR_RACER \ + { \ + 0.0832f, 0.8000f, 0.3162f, 1.0000f, 0.7943f, 0.1700f, 2.0000f, 0.4100f, 1.7783f, 0.0070f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0150f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_DRIVING_INCAR_SPORTS \ + { \ + 0.0832f, 0.8000f, 0.3162f, 0.6310f, 1.0000f, 0.1700f, 0.7500f, 0.4100f, 1.0000f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.5623f, 0.0000f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_DRIVING_INCAR_LUXURY \ + { \ + 0.2560f, 1.0000f, 0.3162f, 0.1000f, 0.5012f, 0.1300f, 0.4100f, 0.4600f, 0.7943f, 0.0100f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.5849f, 0.0100f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 10268.2002f, 251.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_DRIVING_FULLGRANDSTAND \ + { \ + 1.0000f, 1.0000f, 0.3162f, 0.2818f, 0.6310f, 3.0100f, 1.3700f, 1.2800f, 0.3548f, 0.0900f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.1778f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 10420.2002f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_DRIVING_EMPTYGRANDSTAND \ + { \ + 1.0000f, 1.0000f, 0.3162f, 1.0000f, 0.7943f, 4.6200f, 1.7500f, 1.4000f, 0.2082f, 0.0900f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.2512f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.0000f, 0.9943f, 10420.2002f, 250.0000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_DRIVING_TUNNEL \ + { \ + 1.0000f, 0.8100f, 0.3162f, 0.3981f, 0.8913f, 3.4200f, 0.9400f, 1.3100f, 0.7079f, 0.0510f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7079f, 0.0470f, { 0.0000f, 0.0000f, 0.0000f }, 0.2140f, 0.0500f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 155.3000f, 0.0000f, 0x1 \ + } /* City Presets */ -#define EFX_REVERB_PRESET_CITY_STREETS \ - { 1.0000f, 0.7800f, 0.3162f, 0.7079f, 0.8913f, 1.7900f, 1.1200f, 0.9100f, 0.2818f, 0.0460f, { 0.0000f, 0.0000f, 0.0000f }, 0.1995f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CITY_SUBWAY \ - { 1.0000f, 0.7400f, 0.3162f, 0.7079f, 0.8913f, 3.0100f, 1.2300f, 0.9100f, 0.7079f, 0.0460f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.2100f, 0.2500f, 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CITY_MUSEUM \ - { 1.0000f, 0.8200f, 0.3162f, 0.1778f, 0.1778f, 3.2800f, 1.4000f, 0.5700f, 0.2512f, 0.0390f, { 0.0000f, 0.0000f, -0.0000f }, 0.8913f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 0.1300f, 0.1700f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_CITY_LIBRARY \ - { 1.0000f, 0.8200f, 0.3162f, 0.2818f, 0.0891f, 2.7600f, 0.8900f, 0.4100f, 0.3548f, 0.0290f, { 0.0000f, 0.0000f, -0.0000f }, 0.8913f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.1300f, 0.1700f, 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 } - -#define EFX_REVERB_PRESET_CITY_UNDERPASS \ - { 1.0000f, 0.8200f, 0.3162f, 0.4467f, 0.8913f, 3.5700f, 1.1200f, 0.9100f, 0.3981f, 0.0590f, { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0370f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1400f, 0.2500f, 0.0000f, 0.9920f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CITY_ABANDONED \ - { 1.0000f, 0.6900f, 0.3162f, 0.7943f, 0.8913f, 3.2800f, 1.1700f, 0.9100f, 0.4467f, 0.0440f, { 0.0000f, 0.0000f, 0.0000f }, 0.2818f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, 0.0000f, 0.9966f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } +#define EFX_REVERB_PRESET_CITY_STREETS \ + { \ + 1.0000f, 0.7800f, 0.3162f, 0.7079f, 0.8913f, 1.7900f, 1.1200f, 0.9100f, 0.2818f, 0.0460f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.1995f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CITY_SUBWAY \ + { \ + 1.0000f, 0.7400f, 0.3162f, 0.7079f, 0.8913f, 3.0100f, 1.2300f, 0.9100f, 0.7079f, 0.0460f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0280f, { 0.0000f, 0.0000f, 0.0000f }, 0.1250f, 0.2100f, 0.2500f, \ + 0.0000f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CITY_MUSEUM \ + { \ + 1.0000f, 0.8200f, 0.3162f, 0.1778f, 0.1778f, 3.2800f, 1.4000f, 0.5700f, 0.2512f, 0.0390f, \ + { 0.0000f, 0.0000f, -0.0000f }, 0.8913f, 0.0340f, { 0.0000f, 0.0000f, 0.0000f }, 0.1300f, 0.1700f, \ + 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_CITY_LIBRARY \ + { \ + 1.0000f, 0.8200f, 0.3162f, 0.2818f, 0.0891f, 2.7600f, 0.8900f, 0.4100f, 0.3548f, 0.0290f, \ + { 0.0000f, 0.0000f, -0.0000f }, 0.8913f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 0.1300f, 0.1700f, \ + 0.2500f, 0.0000f, 0.9943f, 2854.3999f, 107.5000f, 0.0000f, 0x0 \ + } + +#define EFX_REVERB_PRESET_CITY_UNDERPASS \ + { \ + 1.0000f, 0.8200f, 0.3162f, 0.4467f, 0.8913f, 3.5700f, 1.1200f, 0.9100f, 0.3981f, 0.0590f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.8913f, 0.0370f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.1400f, 0.2500f, \ + 0.0000f, 0.9920f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CITY_ABANDONED \ + { \ + 1.0000f, 0.6900f, 0.3162f, 0.7943f, 0.8913f, 3.2800f, 1.1700f, 0.9100f, 0.4467f, 0.0440f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.2818f, 0.0240f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.2000f, 0.2500f, \ + 0.0000f, 0.9966f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } /* Misc. Presets */ -#define EFX_REVERB_PRESET_DUSTYROOM \ - { 0.3645f, 0.5600f, 0.3162f, 0.7943f, 0.7079f, 1.7900f, 0.3800f, 0.2100f, 0.5012f, 0.0020f, { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0060f, { 0.0000f, 0.0000f, 0.0000f }, 0.2020f, 0.0500f, 0.2500f, 0.0000f, 0.9886f, 13046.0000f, 163.3000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_CHAPEL \ - { 1.0000f, 0.8400f, 0.3162f, 0.5623f, 1.0000f, 4.6200f, 0.6400f, 1.2300f, 0.4467f, 0.0320f, { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, 0.1100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 } - -#define EFX_REVERB_PRESET_SMALLWATERROOM \ - { 1.0000f, 0.7000f, 0.3162f, 0.4477f, 1.0000f, 1.5100f, 1.2500f, 1.1400f, 0.8913f, 0.0200f, { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1790f, 0.1500f, 0.8950f, 0.1900f, 0.9920f, 5000.0000f, 250.0000f, 0.0000f, 0x0 } +#define EFX_REVERB_PRESET_DUSTYROOM \ + { \ + 0.3645f, 0.5600f, 0.3162f, 0.7943f, 0.7079f, 1.7900f, 0.3800f, 0.2100f, 0.5012f, 0.0020f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.2589f, 0.0060f, { 0.0000f, 0.0000f, 0.0000f }, 0.2020f, 0.0500f, 0.2500f, \ + 0.0000f, 0.9886f, 13046.0000f, 163.3000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_CHAPEL \ + { \ + 1.0000f, 0.8400f, 0.3162f, 0.5623f, 1.0000f, 4.6200f, 0.6400f, 1.2300f, 0.4467f, 0.0320f, \ + { 0.0000f, 0.0000f, 0.0000f }, 0.7943f, 0.0490f, { 0.0000f, 0.0000f, 0.0000f }, 0.2500f, 0.0000f, 0.2500f, \ + 0.1100f, 0.9943f, 5000.0000f, 250.0000f, 0.0000f, 0x1 \ + } + +#define EFX_REVERB_PRESET_SMALLWATERROOM \ + { \ + 1.0000f, 0.7000f, 0.3162f, 0.4477f, 1.0000f, 1.5100f, 1.2500f, 1.1400f, 0.8913f, 0.0200f, \ + { 0.0000f, 0.0000f, 0.0000f }, 1.4125f, 0.0300f, { 0.0000f, 0.0000f, 0.0000f }, 0.1790f, 0.1500f, 0.8950f, \ + 0.1900f, 0.9920f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ + } #endif /* EFX_PRESETS_H */ diff --git a/apps/openmw/mwsound/efx.h b/apps/openmw/mwsound/efx.h index 57766983f67..718a9f79dfd 100644 --- a/apps/openmw/mwsound/efx.h +++ b/apps/openmw/mwsound/efx.h @@ -1,761 +1,752 @@ #ifndef AL_EFX_H #define AL_EFX_H - -#include "alc.h" #include "al.h" #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif -#define ALC_EXT_EFX_NAME "ALC_EXT_EFX" - -#define ALC_EFX_MAJOR_VERSION 0x20001 -#define ALC_EFX_MINOR_VERSION 0x20002 -#define ALC_MAX_AUXILIARY_SENDS 0x20003 +#define ALC_EXT_EFX_NAME "ALC_EXT_EFX" +#define ALC_EFX_MAJOR_VERSION 0x20001 +#define ALC_EFX_MINOR_VERSION 0x20002 +#define ALC_MAX_AUXILIARY_SENDS 0x20003 /* Listener properties. */ -#define AL_METERS_PER_UNIT 0x20004 +#define AL_METERS_PER_UNIT 0x20004 /* Source properties. */ -#define AL_DIRECT_FILTER 0x20005 -#define AL_AUXILIARY_SEND_FILTER 0x20006 -#define AL_AIR_ABSORPTION_FACTOR 0x20007 -#define AL_ROOM_ROLLOFF_FACTOR 0x20008 -#define AL_CONE_OUTER_GAINHF 0x20009 -#define AL_DIRECT_FILTER_GAINHF_AUTO 0x2000A -#define AL_AUXILIARY_SEND_FILTER_GAIN_AUTO 0x2000B -#define AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO 0x2000C - +#define AL_DIRECT_FILTER 0x20005 +#define AL_AUXILIARY_SEND_FILTER 0x20006 +#define AL_AIR_ABSORPTION_FACTOR 0x20007 +#define AL_ROOM_ROLLOFF_FACTOR 0x20008 +#define AL_CONE_OUTER_GAINHF 0x20009 +#define AL_DIRECT_FILTER_GAINHF_AUTO 0x2000A +#define AL_AUXILIARY_SEND_FILTER_GAIN_AUTO 0x2000B +#define AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO 0x2000C /* Effect properties. */ /* Reverb effect parameters */ -#define AL_REVERB_DENSITY 0x0001 -#define AL_REVERB_DIFFUSION 0x0002 -#define AL_REVERB_GAIN 0x0003 -#define AL_REVERB_GAINHF 0x0004 -#define AL_REVERB_DECAY_TIME 0x0005 -#define AL_REVERB_DECAY_HFRATIO 0x0006 -#define AL_REVERB_REFLECTIONS_GAIN 0x0007 -#define AL_REVERB_REFLECTIONS_DELAY 0x0008 -#define AL_REVERB_LATE_REVERB_GAIN 0x0009 -#define AL_REVERB_LATE_REVERB_DELAY 0x000A -#define AL_REVERB_AIR_ABSORPTION_GAINHF 0x000B -#define AL_REVERB_ROOM_ROLLOFF_FACTOR 0x000C -#define AL_REVERB_DECAY_HFLIMIT 0x000D +#define AL_REVERB_DENSITY 0x0001 +#define AL_REVERB_DIFFUSION 0x0002 +#define AL_REVERB_GAIN 0x0003 +#define AL_REVERB_GAINHF 0x0004 +#define AL_REVERB_DECAY_TIME 0x0005 +#define AL_REVERB_DECAY_HFRATIO 0x0006 +#define AL_REVERB_REFLECTIONS_GAIN 0x0007 +#define AL_REVERB_REFLECTIONS_DELAY 0x0008 +#define AL_REVERB_LATE_REVERB_GAIN 0x0009 +#define AL_REVERB_LATE_REVERB_DELAY 0x000A +#define AL_REVERB_AIR_ABSORPTION_GAINHF 0x000B +#define AL_REVERB_ROOM_ROLLOFF_FACTOR 0x000C +#define AL_REVERB_DECAY_HFLIMIT 0x000D /* EAX Reverb effect parameters */ -#define AL_EAXREVERB_DENSITY 0x0001 -#define AL_EAXREVERB_DIFFUSION 0x0002 -#define AL_EAXREVERB_GAIN 0x0003 -#define AL_EAXREVERB_GAINHF 0x0004 -#define AL_EAXREVERB_GAINLF 0x0005 -#define AL_EAXREVERB_DECAY_TIME 0x0006 -#define AL_EAXREVERB_DECAY_HFRATIO 0x0007 -#define AL_EAXREVERB_DECAY_LFRATIO 0x0008 -#define AL_EAXREVERB_REFLECTIONS_GAIN 0x0009 -#define AL_EAXREVERB_REFLECTIONS_DELAY 0x000A -#define AL_EAXREVERB_REFLECTIONS_PAN 0x000B -#define AL_EAXREVERB_LATE_REVERB_GAIN 0x000C -#define AL_EAXREVERB_LATE_REVERB_DELAY 0x000D -#define AL_EAXREVERB_LATE_REVERB_PAN 0x000E -#define AL_EAXREVERB_ECHO_TIME 0x000F -#define AL_EAXREVERB_ECHO_DEPTH 0x0010 -#define AL_EAXREVERB_MODULATION_TIME 0x0011 -#define AL_EAXREVERB_MODULATION_DEPTH 0x0012 -#define AL_EAXREVERB_AIR_ABSORPTION_GAINHF 0x0013 -#define AL_EAXREVERB_HFREFERENCE 0x0014 -#define AL_EAXREVERB_LFREFERENCE 0x0015 -#define AL_EAXREVERB_ROOM_ROLLOFF_FACTOR 0x0016 -#define AL_EAXREVERB_DECAY_HFLIMIT 0x0017 +#define AL_EAXREVERB_DENSITY 0x0001 +#define AL_EAXREVERB_DIFFUSION 0x0002 +#define AL_EAXREVERB_GAIN 0x0003 +#define AL_EAXREVERB_GAINHF 0x0004 +#define AL_EAXREVERB_GAINLF 0x0005 +#define AL_EAXREVERB_DECAY_TIME 0x0006 +#define AL_EAXREVERB_DECAY_HFRATIO 0x0007 +#define AL_EAXREVERB_DECAY_LFRATIO 0x0008 +#define AL_EAXREVERB_REFLECTIONS_GAIN 0x0009 +#define AL_EAXREVERB_REFLECTIONS_DELAY 0x000A +#define AL_EAXREVERB_REFLECTIONS_PAN 0x000B +#define AL_EAXREVERB_LATE_REVERB_GAIN 0x000C +#define AL_EAXREVERB_LATE_REVERB_DELAY 0x000D +#define AL_EAXREVERB_LATE_REVERB_PAN 0x000E +#define AL_EAXREVERB_ECHO_TIME 0x000F +#define AL_EAXREVERB_ECHO_DEPTH 0x0010 +#define AL_EAXREVERB_MODULATION_TIME 0x0011 +#define AL_EAXREVERB_MODULATION_DEPTH 0x0012 +#define AL_EAXREVERB_AIR_ABSORPTION_GAINHF 0x0013 +#define AL_EAXREVERB_HFREFERENCE 0x0014 +#define AL_EAXREVERB_LFREFERENCE 0x0015 +#define AL_EAXREVERB_ROOM_ROLLOFF_FACTOR 0x0016 +#define AL_EAXREVERB_DECAY_HFLIMIT 0x0017 /* Chorus effect parameters */ -#define AL_CHORUS_WAVEFORM 0x0001 -#define AL_CHORUS_PHASE 0x0002 -#define AL_CHORUS_RATE 0x0003 -#define AL_CHORUS_DEPTH 0x0004 -#define AL_CHORUS_FEEDBACK 0x0005 -#define AL_CHORUS_DELAY 0x0006 +#define AL_CHORUS_WAVEFORM 0x0001 +#define AL_CHORUS_PHASE 0x0002 +#define AL_CHORUS_RATE 0x0003 +#define AL_CHORUS_DEPTH 0x0004 +#define AL_CHORUS_FEEDBACK 0x0005 +#define AL_CHORUS_DELAY 0x0006 /* Distortion effect parameters */ -#define AL_DISTORTION_EDGE 0x0001 -#define AL_DISTORTION_GAIN 0x0002 -#define AL_DISTORTION_LOWPASS_CUTOFF 0x0003 -#define AL_DISTORTION_EQCENTER 0x0004 -#define AL_DISTORTION_EQBANDWIDTH 0x0005 +#define AL_DISTORTION_EDGE 0x0001 +#define AL_DISTORTION_GAIN 0x0002 +#define AL_DISTORTION_LOWPASS_CUTOFF 0x0003 +#define AL_DISTORTION_EQCENTER 0x0004 +#define AL_DISTORTION_EQBANDWIDTH 0x0005 /* Echo effect parameters */ -#define AL_ECHO_DELAY 0x0001 -#define AL_ECHO_LRDELAY 0x0002 -#define AL_ECHO_DAMPING 0x0003 -#define AL_ECHO_FEEDBACK 0x0004 -#define AL_ECHO_SPREAD 0x0005 +#define AL_ECHO_DELAY 0x0001 +#define AL_ECHO_LRDELAY 0x0002 +#define AL_ECHO_DAMPING 0x0003 +#define AL_ECHO_FEEDBACK 0x0004 +#define AL_ECHO_SPREAD 0x0005 /* Flanger effect parameters */ -#define AL_FLANGER_WAVEFORM 0x0001 -#define AL_FLANGER_PHASE 0x0002 -#define AL_FLANGER_RATE 0x0003 -#define AL_FLANGER_DEPTH 0x0004 -#define AL_FLANGER_FEEDBACK 0x0005 -#define AL_FLANGER_DELAY 0x0006 +#define AL_FLANGER_WAVEFORM 0x0001 +#define AL_FLANGER_PHASE 0x0002 +#define AL_FLANGER_RATE 0x0003 +#define AL_FLANGER_DEPTH 0x0004 +#define AL_FLANGER_FEEDBACK 0x0005 +#define AL_FLANGER_DELAY 0x0006 /* Frequency shifter effect parameters */ -#define AL_FREQUENCY_SHIFTER_FREQUENCY 0x0001 -#define AL_FREQUENCY_SHIFTER_LEFT_DIRECTION 0x0002 -#define AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION 0x0003 +#define AL_FREQUENCY_SHIFTER_FREQUENCY 0x0001 +#define AL_FREQUENCY_SHIFTER_LEFT_DIRECTION 0x0002 +#define AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION 0x0003 /* Vocal morpher effect parameters */ -#define AL_VOCAL_MORPHER_PHONEMEA 0x0001 -#define AL_VOCAL_MORPHER_PHONEMEA_COARSE_TUNING 0x0002 -#define AL_VOCAL_MORPHER_PHONEMEB 0x0003 -#define AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING 0x0004 -#define AL_VOCAL_MORPHER_WAVEFORM 0x0005 -#define AL_VOCAL_MORPHER_RATE 0x0006 +#define AL_VOCAL_MORPHER_PHONEMEA 0x0001 +#define AL_VOCAL_MORPHER_PHONEMEA_COARSE_TUNING 0x0002 +#define AL_VOCAL_MORPHER_PHONEMEB 0x0003 +#define AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING 0x0004 +#define AL_VOCAL_MORPHER_WAVEFORM 0x0005 +#define AL_VOCAL_MORPHER_RATE 0x0006 /* Pitchshifter effect parameters */ -#define AL_PITCH_SHIFTER_COARSE_TUNE 0x0001 -#define AL_PITCH_SHIFTER_FINE_TUNE 0x0002 +#define AL_PITCH_SHIFTER_COARSE_TUNE 0x0001 +#define AL_PITCH_SHIFTER_FINE_TUNE 0x0002 /* Ringmodulator effect parameters */ -#define AL_RING_MODULATOR_FREQUENCY 0x0001 -#define AL_RING_MODULATOR_HIGHPASS_CUTOFF 0x0002 -#define AL_RING_MODULATOR_WAVEFORM 0x0003 +#define AL_RING_MODULATOR_FREQUENCY 0x0001 +#define AL_RING_MODULATOR_HIGHPASS_CUTOFF 0x0002 +#define AL_RING_MODULATOR_WAVEFORM 0x0003 /* Autowah effect parameters */ -#define AL_AUTOWAH_ATTACK_TIME 0x0001 -#define AL_AUTOWAH_RELEASE_TIME 0x0002 -#define AL_AUTOWAH_RESONANCE 0x0003 -#define AL_AUTOWAH_PEAK_GAIN 0x0004 +#define AL_AUTOWAH_ATTACK_TIME 0x0001 +#define AL_AUTOWAH_RELEASE_TIME 0x0002 +#define AL_AUTOWAH_RESONANCE 0x0003 +#define AL_AUTOWAH_PEAK_GAIN 0x0004 /* Compressor effect parameters */ -#define AL_COMPRESSOR_ONOFF 0x0001 +#define AL_COMPRESSOR_ONOFF 0x0001 /* Equalizer effect parameters */ -#define AL_EQUALIZER_LOW_GAIN 0x0001 -#define AL_EQUALIZER_LOW_CUTOFF 0x0002 -#define AL_EQUALIZER_MID1_GAIN 0x0003 -#define AL_EQUALIZER_MID1_CENTER 0x0004 -#define AL_EQUALIZER_MID1_WIDTH 0x0005 -#define AL_EQUALIZER_MID2_GAIN 0x0006 -#define AL_EQUALIZER_MID2_CENTER 0x0007 -#define AL_EQUALIZER_MID2_WIDTH 0x0008 -#define AL_EQUALIZER_HIGH_GAIN 0x0009 -#define AL_EQUALIZER_HIGH_CUTOFF 0x000A +#define AL_EQUALIZER_LOW_GAIN 0x0001 +#define AL_EQUALIZER_LOW_CUTOFF 0x0002 +#define AL_EQUALIZER_MID1_GAIN 0x0003 +#define AL_EQUALIZER_MID1_CENTER 0x0004 +#define AL_EQUALIZER_MID1_WIDTH 0x0005 +#define AL_EQUALIZER_MID2_GAIN 0x0006 +#define AL_EQUALIZER_MID2_CENTER 0x0007 +#define AL_EQUALIZER_MID2_WIDTH 0x0008 +#define AL_EQUALIZER_HIGH_GAIN 0x0009 +#define AL_EQUALIZER_HIGH_CUTOFF 0x000A /* Effect type */ -#define AL_EFFECT_FIRST_PARAMETER 0x0000 -#define AL_EFFECT_LAST_PARAMETER 0x8000 -#define AL_EFFECT_TYPE 0x8001 +#define AL_EFFECT_FIRST_PARAMETER 0x0000 +#define AL_EFFECT_LAST_PARAMETER 0x8000 +#define AL_EFFECT_TYPE 0x8001 /* Effect types, used with the AL_EFFECT_TYPE property */ -#define AL_EFFECT_NULL 0x0000 -#define AL_EFFECT_REVERB 0x0001 -#define AL_EFFECT_CHORUS 0x0002 -#define AL_EFFECT_DISTORTION 0x0003 -#define AL_EFFECT_ECHO 0x0004 -#define AL_EFFECT_FLANGER 0x0005 -#define AL_EFFECT_FREQUENCY_SHIFTER 0x0006 -#define AL_EFFECT_VOCAL_MORPHER 0x0007 -#define AL_EFFECT_PITCH_SHIFTER 0x0008 -#define AL_EFFECT_RING_MODULATOR 0x0009 -#define AL_EFFECT_AUTOWAH 0x000A -#define AL_EFFECT_COMPRESSOR 0x000B -#define AL_EFFECT_EQUALIZER 0x000C -#define AL_EFFECT_EAXREVERB 0x8000 +#define AL_EFFECT_NULL 0x0000 +#define AL_EFFECT_REVERB 0x0001 +#define AL_EFFECT_CHORUS 0x0002 +#define AL_EFFECT_DISTORTION 0x0003 +#define AL_EFFECT_ECHO 0x0004 +#define AL_EFFECT_FLANGER 0x0005 +#define AL_EFFECT_FREQUENCY_SHIFTER 0x0006 +#define AL_EFFECT_VOCAL_MORPHER 0x0007 +#define AL_EFFECT_PITCH_SHIFTER 0x0008 +#define AL_EFFECT_RING_MODULATOR 0x0009 +#define AL_EFFECT_AUTOWAH 0x000A +#define AL_EFFECT_COMPRESSOR 0x000B +#define AL_EFFECT_EQUALIZER 0x000C +#define AL_EFFECT_EAXREVERB 0x8000 /* Auxiliary Effect Slot properties. */ -#define AL_EFFECTSLOT_EFFECT 0x0001 -#define AL_EFFECTSLOT_GAIN 0x0002 -#define AL_EFFECTSLOT_AUXILIARY_SEND_AUTO 0x0003 +#define AL_EFFECTSLOT_EFFECT 0x0001 +#define AL_EFFECTSLOT_GAIN 0x0002 +#define AL_EFFECTSLOT_AUXILIARY_SEND_AUTO 0x0003 /* NULL Auxiliary Slot ID to disable a source send. */ -#define AL_EFFECTSLOT_NULL 0x0000 - +#define AL_EFFECTSLOT_NULL 0x0000 /* Filter properties. */ /* Lowpass filter parameters */ -#define AL_LOWPASS_GAIN 0x0001 -#define AL_LOWPASS_GAINHF 0x0002 +#define AL_LOWPASS_GAIN 0x0001 +#define AL_LOWPASS_GAINHF 0x0002 /* Highpass filter parameters */ -#define AL_HIGHPASS_GAIN 0x0001 -#define AL_HIGHPASS_GAINLF 0x0002 +#define AL_HIGHPASS_GAIN 0x0001 +#define AL_HIGHPASS_GAINLF 0x0002 /* Bandpass filter parameters */ -#define AL_BANDPASS_GAIN 0x0001 -#define AL_BANDPASS_GAINLF 0x0002 -#define AL_BANDPASS_GAINHF 0x0003 +#define AL_BANDPASS_GAIN 0x0001 +#define AL_BANDPASS_GAINLF 0x0002 +#define AL_BANDPASS_GAINHF 0x0003 /* Filter type */ -#define AL_FILTER_FIRST_PARAMETER 0x0000 -#define AL_FILTER_LAST_PARAMETER 0x8000 -#define AL_FILTER_TYPE 0x8001 +#define AL_FILTER_FIRST_PARAMETER 0x0000 +#define AL_FILTER_LAST_PARAMETER 0x8000 +#define AL_FILTER_TYPE 0x8001 /* Filter types, used with the AL_FILTER_TYPE property */ -#define AL_FILTER_NULL 0x0000 -#define AL_FILTER_LOWPASS 0x0001 -#define AL_FILTER_HIGHPASS 0x0002 -#define AL_FILTER_BANDPASS 0x0003 - - -/* Effect object function types. */ -typedef void (AL_APIENTRY *LPALGENEFFECTS)(ALsizei, ALuint*); -typedef void (AL_APIENTRY *LPALDELETEEFFECTS)(ALsizei, const ALuint*); -typedef ALboolean (AL_APIENTRY *LPALISEFFECT)(ALuint); -typedef void (AL_APIENTRY *LPALEFFECTI)(ALuint, ALenum, ALint); -typedef void (AL_APIENTRY *LPALEFFECTIV)(ALuint, ALenum, const ALint*); -typedef void (AL_APIENTRY *LPALEFFECTF)(ALuint, ALenum, ALfloat); -typedef void (AL_APIENTRY *LPALEFFECTFV)(ALuint, ALenum, const ALfloat*); -typedef void (AL_APIENTRY *LPALGETEFFECTI)(ALuint, ALenum, ALint*); -typedef void (AL_APIENTRY *LPALGETEFFECTIV)(ALuint, ALenum, ALint*); -typedef void (AL_APIENTRY *LPALGETEFFECTF)(ALuint, ALenum, ALfloat*); -typedef void (AL_APIENTRY *LPALGETEFFECTFV)(ALuint, ALenum, ALfloat*); - -/* Filter object function types. */ -typedef void (AL_APIENTRY *LPALGENFILTERS)(ALsizei, ALuint*); -typedef void (AL_APIENTRY *LPALDELETEFILTERS)(ALsizei, const ALuint*); -typedef ALboolean (AL_APIENTRY *LPALISFILTER)(ALuint); -typedef void (AL_APIENTRY *LPALFILTERI)(ALuint, ALenum, ALint); -typedef void (AL_APIENTRY *LPALFILTERIV)(ALuint, ALenum, const ALint*); -typedef void (AL_APIENTRY *LPALFILTERF)(ALuint, ALenum, ALfloat); -typedef void (AL_APIENTRY *LPALFILTERFV)(ALuint, ALenum, const ALfloat*); -typedef void (AL_APIENTRY *LPALGETFILTERI)(ALuint, ALenum, ALint*); -typedef void (AL_APIENTRY *LPALGETFILTERIV)(ALuint, ALenum, ALint*); -typedef void (AL_APIENTRY *LPALGETFILTERF)(ALuint, ALenum, ALfloat*); -typedef void (AL_APIENTRY *LPALGETFILTERFV)(ALuint, ALenum, ALfloat*); - -/* Auxiliary Effect Slot object function types. */ -typedef void (AL_APIENTRY *LPALGENAUXILIARYEFFECTSLOTS)(ALsizei, ALuint*); -typedef void (AL_APIENTRY *LPALDELETEAUXILIARYEFFECTSLOTS)(ALsizei, const ALuint*); -typedef ALboolean (AL_APIENTRY *LPALISAUXILIARYEFFECTSLOT)(ALuint); -typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTI)(ALuint, ALenum, ALint); -typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTIV)(ALuint, ALenum, const ALint*); -typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTF)(ALuint, ALenum, ALfloat); -typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTFV)(ALuint, ALenum, const ALfloat*); -typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTI)(ALuint, ALenum, ALint*); -typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTIV)(ALuint, ALenum, ALint*); -typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTF)(ALuint, ALenum, ALfloat*); -typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTFV)(ALuint, ALenum, ALfloat*); +#define AL_FILTER_NULL 0x0000 +#define AL_FILTER_LOWPASS 0x0001 +#define AL_FILTER_HIGHPASS 0x0002 +#define AL_FILTER_BANDPASS 0x0003 + + /* Effect object function types. */ + typedef void(AL_APIENTRY* LPALGENEFFECTS)(ALsizei, ALuint*); + typedef void(AL_APIENTRY* LPALDELETEEFFECTS)(ALsizei, const ALuint*); + typedef ALboolean(AL_APIENTRY* LPALISEFFECT)(ALuint); + typedef void(AL_APIENTRY* LPALEFFECTI)(ALuint, ALenum, ALint); + typedef void(AL_APIENTRY* LPALEFFECTIV)(ALuint, ALenum, const ALint*); + typedef void(AL_APIENTRY* LPALEFFECTF)(ALuint, ALenum, ALfloat); + typedef void(AL_APIENTRY* LPALEFFECTFV)(ALuint, ALenum, const ALfloat*); + typedef void(AL_APIENTRY* LPALGETEFFECTI)(ALuint, ALenum, ALint*); + typedef void(AL_APIENTRY* LPALGETEFFECTIV)(ALuint, ALenum, ALint*); + typedef void(AL_APIENTRY* LPALGETEFFECTF)(ALuint, ALenum, ALfloat*); + typedef void(AL_APIENTRY* LPALGETEFFECTFV)(ALuint, ALenum, ALfloat*); + + /* Filter object function types. */ + typedef void(AL_APIENTRY* LPALGENFILTERS)(ALsizei, ALuint*); + typedef void(AL_APIENTRY* LPALDELETEFILTERS)(ALsizei, const ALuint*); + typedef ALboolean(AL_APIENTRY* LPALISFILTER)(ALuint); + typedef void(AL_APIENTRY* LPALFILTERI)(ALuint, ALenum, ALint); + typedef void(AL_APIENTRY* LPALFILTERIV)(ALuint, ALenum, const ALint*); + typedef void(AL_APIENTRY* LPALFILTERF)(ALuint, ALenum, ALfloat); + typedef void(AL_APIENTRY* LPALFILTERFV)(ALuint, ALenum, const ALfloat*); + typedef void(AL_APIENTRY* LPALGETFILTERI)(ALuint, ALenum, ALint*); + typedef void(AL_APIENTRY* LPALGETFILTERIV)(ALuint, ALenum, ALint*); + typedef void(AL_APIENTRY* LPALGETFILTERF)(ALuint, ALenum, ALfloat*); + typedef void(AL_APIENTRY* LPALGETFILTERFV)(ALuint, ALenum, ALfloat*); + + /* Auxiliary Effect Slot object function types. */ + typedef void(AL_APIENTRY* LPALGENAUXILIARYEFFECTSLOTS)(ALsizei, ALuint*); + typedef void(AL_APIENTRY* LPALDELETEAUXILIARYEFFECTSLOTS)(ALsizei, const ALuint*); + typedef ALboolean(AL_APIENTRY* LPALISAUXILIARYEFFECTSLOT)(ALuint); + typedef void(AL_APIENTRY* LPALAUXILIARYEFFECTSLOTI)(ALuint, ALenum, ALint); + typedef void(AL_APIENTRY* LPALAUXILIARYEFFECTSLOTIV)(ALuint, ALenum, const ALint*); + typedef void(AL_APIENTRY* LPALAUXILIARYEFFECTSLOTF)(ALuint, ALenum, ALfloat); + typedef void(AL_APIENTRY* LPALAUXILIARYEFFECTSLOTFV)(ALuint, ALenum, const ALfloat*); + typedef void(AL_APIENTRY* LPALGETAUXILIARYEFFECTSLOTI)(ALuint, ALenum, ALint*); + typedef void(AL_APIENTRY* LPALGETAUXILIARYEFFECTSLOTIV)(ALuint, ALenum, ALint*); + typedef void(AL_APIENTRY* LPALGETAUXILIARYEFFECTSLOTF)(ALuint, ALenum, ALfloat*); + typedef void(AL_APIENTRY* LPALGETAUXILIARYEFFECTSLOTFV)(ALuint, ALenum, ALfloat*); #ifdef AL_ALEXT_PROTOTYPES -AL_API ALvoid AL_APIENTRY alGenEffects(ALsizei n, ALuint *effects); -AL_API ALvoid AL_APIENTRY alDeleteEffects(ALsizei n, const ALuint *effects); -AL_API ALboolean AL_APIENTRY alIsEffect(ALuint effect); -AL_API ALvoid AL_APIENTRY alEffecti(ALuint effect, ALenum param, ALint iValue); -AL_API ALvoid AL_APIENTRY alEffectiv(ALuint effect, ALenum param, const ALint *piValues); -AL_API ALvoid AL_APIENTRY alEffectf(ALuint effect, ALenum param, ALfloat flValue); -AL_API ALvoid AL_APIENTRY alEffectfv(ALuint effect, ALenum param, const ALfloat *pflValues); -AL_API ALvoid AL_APIENTRY alGetEffecti(ALuint effect, ALenum param, ALint *piValue); -AL_API ALvoid AL_APIENTRY alGetEffectiv(ALuint effect, ALenum param, ALint *piValues); -AL_API ALvoid AL_APIENTRY alGetEffectf(ALuint effect, ALenum param, ALfloat *pflValue); -AL_API ALvoid AL_APIENTRY alGetEffectfv(ALuint effect, ALenum param, ALfloat *pflValues); - -AL_API ALvoid AL_APIENTRY alGenFilters(ALsizei n, ALuint *filters); -AL_API ALvoid AL_APIENTRY alDeleteFilters(ALsizei n, const ALuint *filters); -AL_API ALboolean AL_APIENTRY alIsFilter(ALuint filter); -AL_API ALvoid AL_APIENTRY alFilteri(ALuint filter, ALenum param, ALint iValue); -AL_API ALvoid AL_APIENTRY alFilteriv(ALuint filter, ALenum param, const ALint *piValues); -AL_API ALvoid AL_APIENTRY alFilterf(ALuint filter, ALenum param, ALfloat flValue); -AL_API ALvoid AL_APIENTRY alFilterfv(ALuint filter, ALenum param, const ALfloat *pflValues); -AL_API ALvoid AL_APIENTRY alGetFilteri(ALuint filter, ALenum param, ALint *piValue); -AL_API ALvoid AL_APIENTRY alGetFilteriv(ALuint filter, ALenum param, ALint *piValues); -AL_API ALvoid AL_APIENTRY alGetFilterf(ALuint filter, ALenum param, ALfloat *pflValue); -AL_API ALvoid AL_APIENTRY alGetFilterfv(ALuint filter, ALenum param, ALfloat *pflValues); - -AL_API ALvoid AL_APIENTRY alGenAuxiliaryEffectSlots(ALsizei n, ALuint *effectslots); -AL_API ALvoid AL_APIENTRY alDeleteAuxiliaryEffectSlots(ALsizei n, const ALuint *effectslots); -AL_API ALboolean AL_APIENTRY alIsAuxiliaryEffectSlot(ALuint effectslot); -AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSloti(ALuint effectslot, ALenum param, ALint iValue); -AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSlotiv(ALuint effectslot, ALenum param, const ALint *piValues); -AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSlotf(ALuint effectslot, ALenum param, ALfloat flValue); -AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSlotfv(ALuint effectslot, ALenum param, const ALfloat *pflValues); -AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSloti(ALuint effectslot, ALenum param, ALint *piValue); -AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSlotiv(ALuint effectslot, ALenum param, ALint *piValues); -AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSlotf(ALuint effectslot, ALenum param, ALfloat *pflValue); -AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSlotfv(ALuint effectslot, ALenum param, ALfloat *pflValues); + AL_API ALvoid AL_APIENTRY alGenEffects(ALsizei n, ALuint* effects); + AL_API ALvoid AL_APIENTRY alDeleteEffects(ALsizei n, const ALuint* effects); + AL_API ALboolean AL_APIENTRY alIsEffect(ALuint effect); + AL_API ALvoid AL_APIENTRY alEffecti(ALuint effect, ALenum param, ALint iValue); + AL_API ALvoid AL_APIENTRY alEffectiv(ALuint effect, ALenum param, const ALint* piValues); + AL_API ALvoid AL_APIENTRY alEffectf(ALuint effect, ALenum param, ALfloat flValue); + AL_API ALvoid AL_APIENTRY alEffectfv(ALuint effect, ALenum param, const ALfloat* pflValues); + AL_API ALvoid AL_APIENTRY alGetEffecti(ALuint effect, ALenum param, ALint* piValue); + AL_API ALvoid AL_APIENTRY alGetEffectiv(ALuint effect, ALenum param, ALint* piValues); + AL_API ALvoid AL_APIENTRY alGetEffectf(ALuint effect, ALenum param, ALfloat* pflValue); + AL_API ALvoid AL_APIENTRY alGetEffectfv(ALuint effect, ALenum param, ALfloat* pflValues); + + AL_API ALvoid AL_APIENTRY alGenFilters(ALsizei n, ALuint* filters); + AL_API ALvoid AL_APIENTRY alDeleteFilters(ALsizei n, const ALuint* filters); + AL_API ALboolean AL_APIENTRY alIsFilter(ALuint filter); + AL_API ALvoid AL_APIENTRY alFilteri(ALuint filter, ALenum param, ALint iValue); + AL_API ALvoid AL_APIENTRY alFilteriv(ALuint filter, ALenum param, const ALint* piValues); + AL_API ALvoid AL_APIENTRY alFilterf(ALuint filter, ALenum param, ALfloat flValue); + AL_API ALvoid AL_APIENTRY alFilterfv(ALuint filter, ALenum param, const ALfloat* pflValues); + AL_API ALvoid AL_APIENTRY alGetFilteri(ALuint filter, ALenum param, ALint* piValue); + AL_API ALvoid AL_APIENTRY alGetFilteriv(ALuint filter, ALenum param, ALint* piValues); + AL_API ALvoid AL_APIENTRY alGetFilterf(ALuint filter, ALenum param, ALfloat* pflValue); + AL_API ALvoid AL_APIENTRY alGetFilterfv(ALuint filter, ALenum param, ALfloat* pflValues); + + AL_API ALvoid AL_APIENTRY alGenAuxiliaryEffectSlots(ALsizei n, ALuint* effectslots); + AL_API ALvoid AL_APIENTRY alDeleteAuxiliaryEffectSlots(ALsizei n, const ALuint* effectslots); + AL_API ALboolean AL_APIENTRY alIsAuxiliaryEffectSlot(ALuint effectslot); + AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSloti(ALuint effectslot, ALenum param, ALint iValue); + AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSlotiv(ALuint effectslot, ALenum param, const ALint* piValues); + AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSlotf(ALuint effectslot, ALenum param, ALfloat flValue); + AL_API ALvoid AL_APIENTRY alAuxiliaryEffectSlotfv(ALuint effectslot, ALenum param, const ALfloat* pflValues); + AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSloti(ALuint effectslot, ALenum param, ALint* piValue); + AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSlotiv(ALuint effectslot, ALenum param, ALint* piValues); + AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSlotf(ALuint effectslot, ALenum param, ALfloat* pflValue); + AL_API ALvoid AL_APIENTRY alGetAuxiliaryEffectSlotfv(ALuint effectslot, ALenum param, ALfloat* pflValues); #endif /* Filter ranges and defaults. */ /* Lowpass filter */ -#define AL_LOWPASS_MIN_GAIN (0.0f) -#define AL_LOWPASS_MAX_GAIN (1.0f) -#define AL_LOWPASS_DEFAULT_GAIN (1.0f) +#define AL_LOWPASS_MIN_GAIN (0.0f) +#define AL_LOWPASS_MAX_GAIN (1.0f) +#define AL_LOWPASS_DEFAULT_GAIN (1.0f) -#define AL_LOWPASS_MIN_GAINHF (0.0f) -#define AL_LOWPASS_MAX_GAINHF (1.0f) -#define AL_LOWPASS_DEFAULT_GAINHF (1.0f) +#define AL_LOWPASS_MIN_GAINHF (0.0f) +#define AL_LOWPASS_MAX_GAINHF (1.0f) +#define AL_LOWPASS_DEFAULT_GAINHF (1.0f) /* Highpass filter */ -#define AL_HIGHPASS_MIN_GAIN (0.0f) -#define AL_HIGHPASS_MAX_GAIN (1.0f) -#define AL_HIGHPASS_DEFAULT_GAIN (1.0f) +#define AL_HIGHPASS_MIN_GAIN (0.0f) +#define AL_HIGHPASS_MAX_GAIN (1.0f) +#define AL_HIGHPASS_DEFAULT_GAIN (1.0f) -#define AL_HIGHPASS_MIN_GAINLF (0.0f) -#define AL_HIGHPASS_MAX_GAINLF (1.0f) -#define AL_HIGHPASS_DEFAULT_GAINLF (1.0f) +#define AL_HIGHPASS_MIN_GAINLF (0.0f) +#define AL_HIGHPASS_MAX_GAINLF (1.0f) +#define AL_HIGHPASS_DEFAULT_GAINLF (1.0f) /* Bandpass filter */ -#define AL_BANDPASS_MIN_GAIN (0.0f) -#define AL_BANDPASS_MAX_GAIN (1.0f) -#define AL_BANDPASS_DEFAULT_GAIN (1.0f) +#define AL_BANDPASS_MIN_GAIN (0.0f) +#define AL_BANDPASS_MAX_GAIN (1.0f) +#define AL_BANDPASS_DEFAULT_GAIN (1.0f) -#define AL_BANDPASS_MIN_GAINHF (0.0f) -#define AL_BANDPASS_MAX_GAINHF (1.0f) -#define AL_BANDPASS_DEFAULT_GAINHF (1.0f) - -#define AL_BANDPASS_MIN_GAINLF (0.0f) -#define AL_BANDPASS_MAX_GAINLF (1.0f) -#define AL_BANDPASS_DEFAULT_GAINLF (1.0f) +#define AL_BANDPASS_MIN_GAINHF (0.0f) +#define AL_BANDPASS_MAX_GAINHF (1.0f) +#define AL_BANDPASS_DEFAULT_GAINHF (1.0f) +#define AL_BANDPASS_MIN_GAINLF (0.0f) +#define AL_BANDPASS_MAX_GAINLF (1.0f) +#define AL_BANDPASS_DEFAULT_GAINLF (1.0f) /* Effect parameter ranges and defaults. */ /* Standard reverb effect */ -#define AL_REVERB_MIN_DENSITY (0.0f) -#define AL_REVERB_MAX_DENSITY (1.0f) -#define AL_REVERB_DEFAULT_DENSITY (1.0f) +#define AL_REVERB_MIN_DENSITY (0.0f) +#define AL_REVERB_MAX_DENSITY (1.0f) +#define AL_REVERB_DEFAULT_DENSITY (1.0f) -#define AL_REVERB_MIN_DIFFUSION (0.0f) -#define AL_REVERB_MAX_DIFFUSION (1.0f) -#define AL_REVERB_DEFAULT_DIFFUSION (1.0f) +#define AL_REVERB_MIN_DIFFUSION (0.0f) +#define AL_REVERB_MAX_DIFFUSION (1.0f) +#define AL_REVERB_DEFAULT_DIFFUSION (1.0f) -#define AL_REVERB_MIN_GAIN (0.0f) -#define AL_REVERB_MAX_GAIN (1.0f) -#define AL_REVERB_DEFAULT_GAIN (0.32f) +#define AL_REVERB_MIN_GAIN (0.0f) +#define AL_REVERB_MAX_GAIN (1.0f) +#define AL_REVERB_DEFAULT_GAIN (0.32f) -#define AL_REVERB_MIN_GAINHF (0.0f) -#define AL_REVERB_MAX_GAINHF (1.0f) -#define AL_REVERB_DEFAULT_GAINHF (0.89f) +#define AL_REVERB_MIN_GAINHF (0.0f) +#define AL_REVERB_MAX_GAINHF (1.0f) +#define AL_REVERB_DEFAULT_GAINHF (0.89f) -#define AL_REVERB_MIN_DECAY_TIME (0.1f) -#define AL_REVERB_MAX_DECAY_TIME (20.0f) -#define AL_REVERB_DEFAULT_DECAY_TIME (1.49f) +#define AL_REVERB_MIN_DECAY_TIME (0.1f) +#define AL_REVERB_MAX_DECAY_TIME (20.0f) +#define AL_REVERB_DEFAULT_DECAY_TIME (1.49f) -#define AL_REVERB_MIN_DECAY_HFRATIO (0.1f) -#define AL_REVERB_MAX_DECAY_HFRATIO (2.0f) -#define AL_REVERB_DEFAULT_DECAY_HFRATIO (0.83f) +#define AL_REVERB_MIN_DECAY_HFRATIO (0.1f) +#define AL_REVERB_MAX_DECAY_HFRATIO (2.0f) +#define AL_REVERB_DEFAULT_DECAY_HFRATIO (0.83f) -#define AL_REVERB_MIN_REFLECTIONS_GAIN (0.0f) -#define AL_REVERB_MAX_REFLECTIONS_GAIN (3.16f) -#define AL_REVERB_DEFAULT_REFLECTIONS_GAIN (0.05f) +#define AL_REVERB_MIN_REFLECTIONS_GAIN (0.0f) +#define AL_REVERB_MAX_REFLECTIONS_GAIN (3.16f) +#define AL_REVERB_DEFAULT_REFLECTIONS_GAIN (0.05f) -#define AL_REVERB_MIN_REFLECTIONS_DELAY (0.0f) -#define AL_REVERB_MAX_REFLECTIONS_DELAY (0.3f) -#define AL_REVERB_DEFAULT_REFLECTIONS_DELAY (0.007f) +#define AL_REVERB_MIN_REFLECTIONS_DELAY (0.0f) +#define AL_REVERB_MAX_REFLECTIONS_DELAY (0.3f) +#define AL_REVERB_DEFAULT_REFLECTIONS_DELAY (0.007f) -#define AL_REVERB_MIN_LATE_REVERB_GAIN (0.0f) -#define AL_REVERB_MAX_LATE_REVERB_GAIN (10.0f) -#define AL_REVERB_DEFAULT_LATE_REVERB_GAIN (1.26f) +#define AL_REVERB_MIN_LATE_REVERB_GAIN (0.0f) +#define AL_REVERB_MAX_LATE_REVERB_GAIN (10.0f) +#define AL_REVERB_DEFAULT_LATE_REVERB_GAIN (1.26f) -#define AL_REVERB_MIN_LATE_REVERB_DELAY (0.0f) -#define AL_REVERB_MAX_LATE_REVERB_DELAY (0.1f) -#define AL_REVERB_DEFAULT_LATE_REVERB_DELAY (0.011f) +#define AL_REVERB_MIN_LATE_REVERB_DELAY (0.0f) +#define AL_REVERB_MAX_LATE_REVERB_DELAY (0.1f) +#define AL_REVERB_DEFAULT_LATE_REVERB_DELAY (0.011f) -#define AL_REVERB_MIN_AIR_ABSORPTION_GAINHF (0.892f) -#define AL_REVERB_MAX_AIR_ABSORPTION_GAINHF (1.0f) -#define AL_REVERB_DEFAULT_AIR_ABSORPTION_GAINHF (0.994f) +#define AL_REVERB_MIN_AIR_ABSORPTION_GAINHF (0.892f) +#define AL_REVERB_MAX_AIR_ABSORPTION_GAINHF (1.0f) +#define AL_REVERB_DEFAULT_AIR_ABSORPTION_GAINHF (0.994f) -#define AL_REVERB_MIN_ROOM_ROLLOFF_FACTOR (0.0f) -#define AL_REVERB_MAX_ROOM_ROLLOFF_FACTOR (10.0f) -#define AL_REVERB_DEFAULT_ROOM_ROLLOFF_FACTOR (0.0f) +#define AL_REVERB_MIN_ROOM_ROLLOFF_FACTOR (0.0f) +#define AL_REVERB_MAX_ROOM_ROLLOFF_FACTOR (10.0f) +#define AL_REVERB_DEFAULT_ROOM_ROLLOFF_FACTOR (0.0f) -#define AL_REVERB_MIN_DECAY_HFLIMIT AL_FALSE -#define AL_REVERB_MAX_DECAY_HFLIMIT AL_TRUE -#define AL_REVERB_DEFAULT_DECAY_HFLIMIT AL_TRUE +#define AL_REVERB_MIN_DECAY_HFLIMIT AL_FALSE +#define AL_REVERB_MAX_DECAY_HFLIMIT AL_TRUE +#define AL_REVERB_DEFAULT_DECAY_HFLIMIT AL_TRUE /* EAX reverb effect */ -#define AL_EAXREVERB_MIN_DENSITY (0.0f) -#define AL_EAXREVERB_MAX_DENSITY (1.0f) -#define AL_EAXREVERB_DEFAULT_DENSITY (1.0f) +#define AL_EAXREVERB_MIN_DENSITY (0.0f) +#define AL_EAXREVERB_MAX_DENSITY (1.0f) +#define AL_EAXREVERB_DEFAULT_DENSITY (1.0f) -#define AL_EAXREVERB_MIN_DIFFUSION (0.0f) -#define AL_EAXREVERB_MAX_DIFFUSION (1.0f) -#define AL_EAXREVERB_DEFAULT_DIFFUSION (1.0f) +#define AL_EAXREVERB_MIN_DIFFUSION (0.0f) +#define AL_EAXREVERB_MAX_DIFFUSION (1.0f) +#define AL_EAXREVERB_DEFAULT_DIFFUSION (1.0f) -#define AL_EAXREVERB_MIN_GAIN (0.0f) -#define AL_EAXREVERB_MAX_GAIN (1.0f) -#define AL_EAXREVERB_DEFAULT_GAIN (0.32f) +#define AL_EAXREVERB_MIN_GAIN (0.0f) +#define AL_EAXREVERB_MAX_GAIN (1.0f) +#define AL_EAXREVERB_DEFAULT_GAIN (0.32f) -#define AL_EAXREVERB_MIN_GAINHF (0.0f) -#define AL_EAXREVERB_MAX_GAINHF (1.0f) -#define AL_EAXREVERB_DEFAULT_GAINHF (0.89f) +#define AL_EAXREVERB_MIN_GAINHF (0.0f) +#define AL_EAXREVERB_MAX_GAINHF (1.0f) +#define AL_EAXREVERB_DEFAULT_GAINHF (0.89f) -#define AL_EAXREVERB_MIN_GAINLF (0.0f) -#define AL_EAXREVERB_MAX_GAINLF (1.0f) -#define AL_EAXREVERB_DEFAULT_GAINLF (1.0f) +#define AL_EAXREVERB_MIN_GAINLF (0.0f) +#define AL_EAXREVERB_MAX_GAINLF (1.0f) +#define AL_EAXREVERB_DEFAULT_GAINLF (1.0f) -#define AL_EAXREVERB_MIN_DECAY_TIME (0.1f) -#define AL_EAXREVERB_MAX_DECAY_TIME (20.0f) -#define AL_EAXREVERB_DEFAULT_DECAY_TIME (1.49f) +#define AL_EAXREVERB_MIN_DECAY_TIME (0.1f) +#define AL_EAXREVERB_MAX_DECAY_TIME (20.0f) +#define AL_EAXREVERB_DEFAULT_DECAY_TIME (1.49f) -#define AL_EAXREVERB_MIN_DECAY_HFRATIO (0.1f) -#define AL_EAXREVERB_MAX_DECAY_HFRATIO (2.0f) -#define AL_EAXREVERB_DEFAULT_DECAY_HFRATIO (0.83f) +#define AL_EAXREVERB_MIN_DECAY_HFRATIO (0.1f) +#define AL_EAXREVERB_MAX_DECAY_HFRATIO (2.0f) +#define AL_EAXREVERB_DEFAULT_DECAY_HFRATIO (0.83f) -#define AL_EAXREVERB_MIN_DECAY_LFRATIO (0.1f) -#define AL_EAXREVERB_MAX_DECAY_LFRATIO (2.0f) -#define AL_EAXREVERB_DEFAULT_DECAY_LFRATIO (1.0f) +#define AL_EAXREVERB_MIN_DECAY_LFRATIO (0.1f) +#define AL_EAXREVERB_MAX_DECAY_LFRATIO (2.0f) +#define AL_EAXREVERB_DEFAULT_DECAY_LFRATIO (1.0f) -#define AL_EAXREVERB_MIN_REFLECTIONS_GAIN (0.0f) -#define AL_EAXREVERB_MAX_REFLECTIONS_GAIN (3.16f) -#define AL_EAXREVERB_DEFAULT_REFLECTIONS_GAIN (0.05f) +#define AL_EAXREVERB_MIN_REFLECTIONS_GAIN (0.0f) +#define AL_EAXREVERB_MAX_REFLECTIONS_GAIN (3.16f) +#define AL_EAXREVERB_DEFAULT_REFLECTIONS_GAIN (0.05f) -#define AL_EAXREVERB_MIN_REFLECTIONS_DELAY (0.0f) -#define AL_EAXREVERB_MAX_REFLECTIONS_DELAY (0.3f) -#define AL_EAXREVERB_DEFAULT_REFLECTIONS_DELAY (0.007f) +#define AL_EAXREVERB_MIN_REFLECTIONS_DELAY (0.0f) +#define AL_EAXREVERB_MAX_REFLECTIONS_DELAY (0.3f) +#define AL_EAXREVERB_DEFAULT_REFLECTIONS_DELAY (0.007f) #define AL_EAXREVERB_DEFAULT_REFLECTIONS_PAN_XYZ (0.0f) -#define AL_EAXREVERB_MIN_LATE_REVERB_GAIN (0.0f) -#define AL_EAXREVERB_MAX_LATE_REVERB_GAIN (10.0f) -#define AL_EAXREVERB_DEFAULT_LATE_REVERB_GAIN (1.26f) +#define AL_EAXREVERB_MIN_LATE_REVERB_GAIN (0.0f) +#define AL_EAXREVERB_MAX_LATE_REVERB_GAIN (10.0f) +#define AL_EAXREVERB_DEFAULT_LATE_REVERB_GAIN (1.26f) -#define AL_EAXREVERB_MIN_LATE_REVERB_DELAY (0.0f) -#define AL_EAXREVERB_MAX_LATE_REVERB_DELAY (0.1f) -#define AL_EAXREVERB_DEFAULT_LATE_REVERB_DELAY (0.011f) +#define AL_EAXREVERB_MIN_LATE_REVERB_DELAY (0.0f) +#define AL_EAXREVERB_MAX_LATE_REVERB_DELAY (0.1f) +#define AL_EAXREVERB_DEFAULT_LATE_REVERB_DELAY (0.011f) #define AL_EAXREVERB_DEFAULT_LATE_REVERB_PAN_XYZ (0.0f) -#define AL_EAXREVERB_MIN_ECHO_TIME (0.075f) -#define AL_EAXREVERB_MAX_ECHO_TIME (0.25f) -#define AL_EAXREVERB_DEFAULT_ECHO_TIME (0.25f) +#define AL_EAXREVERB_MIN_ECHO_TIME (0.075f) +#define AL_EAXREVERB_MAX_ECHO_TIME (0.25f) +#define AL_EAXREVERB_DEFAULT_ECHO_TIME (0.25f) -#define AL_EAXREVERB_MIN_ECHO_DEPTH (0.0f) -#define AL_EAXREVERB_MAX_ECHO_DEPTH (1.0f) -#define AL_EAXREVERB_DEFAULT_ECHO_DEPTH (0.0f) +#define AL_EAXREVERB_MIN_ECHO_DEPTH (0.0f) +#define AL_EAXREVERB_MAX_ECHO_DEPTH (1.0f) +#define AL_EAXREVERB_DEFAULT_ECHO_DEPTH (0.0f) -#define AL_EAXREVERB_MIN_MODULATION_TIME (0.04f) -#define AL_EAXREVERB_MAX_MODULATION_TIME (4.0f) -#define AL_EAXREVERB_DEFAULT_MODULATION_TIME (0.25f) +#define AL_EAXREVERB_MIN_MODULATION_TIME (0.04f) +#define AL_EAXREVERB_MAX_MODULATION_TIME (4.0f) +#define AL_EAXREVERB_DEFAULT_MODULATION_TIME (0.25f) -#define AL_EAXREVERB_MIN_MODULATION_DEPTH (0.0f) -#define AL_EAXREVERB_MAX_MODULATION_DEPTH (1.0f) -#define AL_EAXREVERB_DEFAULT_MODULATION_DEPTH (0.0f) +#define AL_EAXREVERB_MIN_MODULATION_DEPTH (0.0f) +#define AL_EAXREVERB_MAX_MODULATION_DEPTH (1.0f) +#define AL_EAXREVERB_DEFAULT_MODULATION_DEPTH (0.0f) -#define AL_EAXREVERB_MIN_AIR_ABSORPTION_GAINHF (0.892f) -#define AL_EAXREVERB_MAX_AIR_ABSORPTION_GAINHF (1.0f) +#define AL_EAXREVERB_MIN_AIR_ABSORPTION_GAINHF (0.892f) +#define AL_EAXREVERB_MAX_AIR_ABSORPTION_GAINHF (1.0f) #define AL_EAXREVERB_DEFAULT_AIR_ABSORPTION_GAINHF (0.994f) -#define AL_EAXREVERB_MIN_HFREFERENCE (1000.0f) -#define AL_EAXREVERB_MAX_HFREFERENCE (20000.0f) -#define AL_EAXREVERB_DEFAULT_HFREFERENCE (5000.0f) +#define AL_EAXREVERB_MIN_HFREFERENCE (1000.0f) +#define AL_EAXREVERB_MAX_HFREFERENCE (20000.0f) +#define AL_EAXREVERB_DEFAULT_HFREFERENCE (5000.0f) -#define AL_EAXREVERB_MIN_LFREFERENCE (20.0f) -#define AL_EAXREVERB_MAX_LFREFERENCE (1000.0f) -#define AL_EAXREVERB_DEFAULT_LFREFERENCE (250.0f) +#define AL_EAXREVERB_MIN_LFREFERENCE (20.0f) +#define AL_EAXREVERB_MAX_LFREFERENCE (1000.0f) +#define AL_EAXREVERB_DEFAULT_LFREFERENCE (250.0f) -#define AL_EAXREVERB_MIN_ROOM_ROLLOFF_FACTOR (0.0f) -#define AL_EAXREVERB_MAX_ROOM_ROLLOFF_FACTOR (10.0f) +#define AL_EAXREVERB_MIN_ROOM_ROLLOFF_FACTOR (0.0f) +#define AL_EAXREVERB_MAX_ROOM_ROLLOFF_FACTOR (10.0f) #define AL_EAXREVERB_DEFAULT_ROOM_ROLLOFF_FACTOR (0.0f) -#define AL_EAXREVERB_MIN_DECAY_HFLIMIT AL_FALSE -#define AL_EAXREVERB_MAX_DECAY_HFLIMIT AL_TRUE -#define AL_EAXREVERB_DEFAULT_DECAY_HFLIMIT AL_TRUE +#define AL_EAXREVERB_MIN_DECAY_HFLIMIT AL_FALSE +#define AL_EAXREVERB_MAX_DECAY_HFLIMIT AL_TRUE +#define AL_EAXREVERB_DEFAULT_DECAY_HFLIMIT AL_TRUE /* Chorus effect */ -#define AL_CHORUS_WAVEFORM_SINUSOID (0) -#define AL_CHORUS_WAVEFORM_TRIANGLE (1) +#define AL_CHORUS_WAVEFORM_SINUSOID (0) +#define AL_CHORUS_WAVEFORM_TRIANGLE (1) -#define AL_CHORUS_MIN_WAVEFORM (0) -#define AL_CHORUS_MAX_WAVEFORM (1) -#define AL_CHORUS_DEFAULT_WAVEFORM (1) +#define AL_CHORUS_MIN_WAVEFORM (0) +#define AL_CHORUS_MAX_WAVEFORM (1) +#define AL_CHORUS_DEFAULT_WAVEFORM (1) -#define AL_CHORUS_MIN_PHASE (-180) -#define AL_CHORUS_MAX_PHASE (180) -#define AL_CHORUS_DEFAULT_PHASE (90) +#define AL_CHORUS_MIN_PHASE (-180) +#define AL_CHORUS_MAX_PHASE (180) +#define AL_CHORUS_DEFAULT_PHASE (90) -#define AL_CHORUS_MIN_RATE (0.0f) -#define AL_CHORUS_MAX_RATE (10.0f) -#define AL_CHORUS_DEFAULT_RATE (1.1f) +#define AL_CHORUS_MIN_RATE (0.0f) +#define AL_CHORUS_MAX_RATE (10.0f) +#define AL_CHORUS_DEFAULT_RATE (1.1f) -#define AL_CHORUS_MIN_DEPTH (0.0f) -#define AL_CHORUS_MAX_DEPTH (1.0f) -#define AL_CHORUS_DEFAULT_DEPTH (0.1f) +#define AL_CHORUS_MIN_DEPTH (0.0f) +#define AL_CHORUS_MAX_DEPTH (1.0f) +#define AL_CHORUS_DEFAULT_DEPTH (0.1f) -#define AL_CHORUS_MIN_FEEDBACK (-1.0f) -#define AL_CHORUS_MAX_FEEDBACK (1.0f) -#define AL_CHORUS_DEFAULT_FEEDBACK (0.25f) +#define AL_CHORUS_MIN_FEEDBACK (-1.0f) +#define AL_CHORUS_MAX_FEEDBACK (1.0f) +#define AL_CHORUS_DEFAULT_FEEDBACK (0.25f) -#define AL_CHORUS_MIN_DELAY (0.0f) -#define AL_CHORUS_MAX_DELAY (0.016f) -#define AL_CHORUS_DEFAULT_DELAY (0.016f) +#define AL_CHORUS_MIN_DELAY (0.0f) +#define AL_CHORUS_MAX_DELAY (0.016f) +#define AL_CHORUS_DEFAULT_DELAY (0.016f) /* Distortion effect */ -#define AL_DISTORTION_MIN_EDGE (0.0f) -#define AL_DISTORTION_MAX_EDGE (1.0f) -#define AL_DISTORTION_DEFAULT_EDGE (0.2f) +#define AL_DISTORTION_MIN_EDGE (0.0f) +#define AL_DISTORTION_MAX_EDGE (1.0f) +#define AL_DISTORTION_DEFAULT_EDGE (0.2f) -#define AL_DISTORTION_MIN_GAIN (0.01f) -#define AL_DISTORTION_MAX_GAIN (1.0f) -#define AL_DISTORTION_DEFAULT_GAIN (0.05f) +#define AL_DISTORTION_MIN_GAIN (0.01f) +#define AL_DISTORTION_MAX_GAIN (1.0f) +#define AL_DISTORTION_DEFAULT_GAIN (0.05f) -#define AL_DISTORTION_MIN_LOWPASS_CUTOFF (80.0f) -#define AL_DISTORTION_MAX_LOWPASS_CUTOFF (24000.0f) -#define AL_DISTORTION_DEFAULT_LOWPASS_CUTOFF (8000.0f) +#define AL_DISTORTION_MIN_LOWPASS_CUTOFF (80.0f) +#define AL_DISTORTION_MAX_LOWPASS_CUTOFF (24000.0f) +#define AL_DISTORTION_DEFAULT_LOWPASS_CUTOFF (8000.0f) -#define AL_DISTORTION_MIN_EQCENTER (80.0f) -#define AL_DISTORTION_MAX_EQCENTER (24000.0f) -#define AL_DISTORTION_DEFAULT_EQCENTER (3600.0f) +#define AL_DISTORTION_MIN_EQCENTER (80.0f) +#define AL_DISTORTION_MAX_EQCENTER (24000.0f) +#define AL_DISTORTION_DEFAULT_EQCENTER (3600.0f) -#define AL_DISTORTION_MIN_EQBANDWIDTH (80.0f) -#define AL_DISTORTION_MAX_EQBANDWIDTH (24000.0f) -#define AL_DISTORTION_DEFAULT_EQBANDWIDTH (3600.0f) +#define AL_DISTORTION_MIN_EQBANDWIDTH (80.0f) +#define AL_DISTORTION_MAX_EQBANDWIDTH (24000.0f) +#define AL_DISTORTION_DEFAULT_EQBANDWIDTH (3600.0f) /* Echo effect */ -#define AL_ECHO_MIN_DELAY (0.0f) -#define AL_ECHO_MAX_DELAY (0.207f) -#define AL_ECHO_DEFAULT_DELAY (0.1f) +#define AL_ECHO_MIN_DELAY (0.0f) +#define AL_ECHO_MAX_DELAY (0.207f) +#define AL_ECHO_DEFAULT_DELAY (0.1f) -#define AL_ECHO_MIN_LRDELAY (0.0f) -#define AL_ECHO_MAX_LRDELAY (0.404f) -#define AL_ECHO_DEFAULT_LRDELAY (0.1f) +#define AL_ECHO_MIN_LRDELAY (0.0f) +#define AL_ECHO_MAX_LRDELAY (0.404f) +#define AL_ECHO_DEFAULT_LRDELAY (0.1f) -#define AL_ECHO_MIN_DAMPING (0.0f) -#define AL_ECHO_MAX_DAMPING (0.99f) -#define AL_ECHO_DEFAULT_DAMPING (0.5f) +#define AL_ECHO_MIN_DAMPING (0.0f) +#define AL_ECHO_MAX_DAMPING (0.99f) +#define AL_ECHO_DEFAULT_DAMPING (0.5f) -#define AL_ECHO_MIN_FEEDBACK (0.0f) -#define AL_ECHO_MAX_FEEDBACK (1.0f) -#define AL_ECHO_DEFAULT_FEEDBACK (0.5f) +#define AL_ECHO_MIN_FEEDBACK (0.0f) +#define AL_ECHO_MAX_FEEDBACK (1.0f) +#define AL_ECHO_DEFAULT_FEEDBACK (0.5f) -#define AL_ECHO_MIN_SPREAD (-1.0f) -#define AL_ECHO_MAX_SPREAD (1.0f) -#define AL_ECHO_DEFAULT_SPREAD (-1.0f) +#define AL_ECHO_MIN_SPREAD (-1.0f) +#define AL_ECHO_MAX_SPREAD (1.0f) +#define AL_ECHO_DEFAULT_SPREAD (-1.0f) /* Flanger effect */ -#define AL_FLANGER_WAVEFORM_SINUSOID (0) -#define AL_FLANGER_WAVEFORM_TRIANGLE (1) +#define AL_FLANGER_WAVEFORM_SINUSOID (0) +#define AL_FLANGER_WAVEFORM_TRIANGLE (1) -#define AL_FLANGER_MIN_WAVEFORM (0) -#define AL_FLANGER_MAX_WAVEFORM (1) -#define AL_FLANGER_DEFAULT_WAVEFORM (1) +#define AL_FLANGER_MIN_WAVEFORM (0) +#define AL_FLANGER_MAX_WAVEFORM (1) +#define AL_FLANGER_DEFAULT_WAVEFORM (1) -#define AL_FLANGER_MIN_PHASE (-180) -#define AL_FLANGER_MAX_PHASE (180) -#define AL_FLANGER_DEFAULT_PHASE (0) +#define AL_FLANGER_MIN_PHASE (-180) +#define AL_FLANGER_MAX_PHASE (180) +#define AL_FLANGER_DEFAULT_PHASE (0) -#define AL_FLANGER_MIN_RATE (0.0f) -#define AL_FLANGER_MAX_RATE (10.0f) -#define AL_FLANGER_DEFAULT_RATE (0.27f) +#define AL_FLANGER_MIN_RATE (0.0f) +#define AL_FLANGER_MAX_RATE (10.0f) +#define AL_FLANGER_DEFAULT_RATE (0.27f) -#define AL_FLANGER_MIN_DEPTH (0.0f) -#define AL_FLANGER_MAX_DEPTH (1.0f) -#define AL_FLANGER_DEFAULT_DEPTH (1.0f) +#define AL_FLANGER_MIN_DEPTH (0.0f) +#define AL_FLANGER_MAX_DEPTH (1.0f) +#define AL_FLANGER_DEFAULT_DEPTH (1.0f) -#define AL_FLANGER_MIN_FEEDBACK (-1.0f) -#define AL_FLANGER_MAX_FEEDBACK (1.0f) -#define AL_FLANGER_DEFAULT_FEEDBACK (-0.5f) +#define AL_FLANGER_MIN_FEEDBACK (-1.0f) +#define AL_FLANGER_MAX_FEEDBACK (1.0f) +#define AL_FLANGER_DEFAULT_FEEDBACK (-0.5f) -#define AL_FLANGER_MIN_DELAY (0.0f) -#define AL_FLANGER_MAX_DELAY (0.004f) -#define AL_FLANGER_DEFAULT_DELAY (0.002f) +#define AL_FLANGER_MIN_DELAY (0.0f) +#define AL_FLANGER_MAX_DELAY (0.004f) +#define AL_FLANGER_DEFAULT_DELAY (0.002f) /* Frequency shifter effect */ -#define AL_FREQUENCY_SHIFTER_MIN_FREQUENCY (0.0f) -#define AL_FREQUENCY_SHIFTER_MAX_FREQUENCY (24000.0f) -#define AL_FREQUENCY_SHIFTER_DEFAULT_FREQUENCY (0.0f) +#define AL_FREQUENCY_SHIFTER_MIN_FREQUENCY (0.0f) +#define AL_FREQUENCY_SHIFTER_MAX_FREQUENCY (24000.0f) +#define AL_FREQUENCY_SHIFTER_DEFAULT_FREQUENCY (0.0f) -#define AL_FREQUENCY_SHIFTER_MIN_LEFT_DIRECTION (0) -#define AL_FREQUENCY_SHIFTER_MAX_LEFT_DIRECTION (2) +#define AL_FREQUENCY_SHIFTER_MIN_LEFT_DIRECTION (0) +#define AL_FREQUENCY_SHIFTER_MAX_LEFT_DIRECTION (2) #define AL_FREQUENCY_SHIFTER_DEFAULT_LEFT_DIRECTION (0) -#define AL_FREQUENCY_SHIFTER_DIRECTION_DOWN (0) -#define AL_FREQUENCY_SHIFTER_DIRECTION_UP (1) -#define AL_FREQUENCY_SHIFTER_DIRECTION_OFF (2) +#define AL_FREQUENCY_SHIFTER_DIRECTION_DOWN (0) +#define AL_FREQUENCY_SHIFTER_DIRECTION_UP (1) +#define AL_FREQUENCY_SHIFTER_DIRECTION_OFF (2) #define AL_FREQUENCY_SHIFTER_MIN_RIGHT_DIRECTION (0) #define AL_FREQUENCY_SHIFTER_MAX_RIGHT_DIRECTION (2) #define AL_FREQUENCY_SHIFTER_DEFAULT_RIGHT_DIRECTION (0) /* Vocal morpher effect */ -#define AL_VOCAL_MORPHER_MIN_PHONEMEA (0) -#define AL_VOCAL_MORPHER_MAX_PHONEMEA (29) -#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEA (0) +#define AL_VOCAL_MORPHER_MIN_PHONEMEA (0) +#define AL_VOCAL_MORPHER_MAX_PHONEMEA (29) +#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEA (0) #define AL_VOCAL_MORPHER_MIN_PHONEMEA_COARSE_TUNING (-24) #define AL_VOCAL_MORPHER_MAX_PHONEMEA_COARSE_TUNING (24) #define AL_VOCAL_MORPHER_DEFAULT_PHONEMEA_COARSE_TUNING (0) -#define AL_VOCAL_MORPHER_MIN_PHONEMEB (0) -#define AL_VOCAL_MORPHER_MAX_PHONEMEB (29) -#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEB (10) +#define AL_VOCAL_MORPHER_MIN_PHONEMEB (0) +#define AL_VOCAL_MORPHER_MAX_PHONEMEB (29) +#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEB (10) #define AL_VOCAL_MORPHER_MIN_PHONEMEB_COARSE_TUNING (-24) #define AL_VOCAL_MORPHER_MAX_PHONEMEB_COARSE_TUNING (24) #define AL_VOCAL_MORPHER_DEFAULT_PHONEMEB_COARSE_TUNING (0) -#define AL_VOCAL_MORPHER_PHONEME_A (0) -#define AL_VOCAL_MORPHER_PHONEME_E (1) -#define AL_VOCAL_MORPHER_PHONEME_I (2) -#define AL_VOCAL_MORPHER_PHONEME_O (3) -#define AL_VOCAL_MORPHER_PHONEME_U (4) -#define AL_VOCAL_MORPHER_PHONEME_AA (5) -#define AL_VOCAL_MORPHER_PHONEME_AE (6) -#define AL_VOCAL_MORPHER_PHONEME_AH (7) -#define AL_VOCAL_MORPHER_PHONEME_AO (8) -#define AL_VOCAL_MORPHER_PHONEME_EH (9) -#define AL_VOCAL_MORPHER_PHONEME_ER (10) -#define AL_VOCAL_MORPHER_PHONEME_IH (11) -#define AL_VOCAL_MORPHER_PHONEME_IY (12) -#define AL_VOCAL_MORPHER_PHONEME_UH (13) -#define AL_VOCAL_MORPHER_PHONEME_UW (14) -#define AL_VOCAL_MORPHER_PHONEME_B (15) -#define AL_VOCAL_MORPHER_PHONEME_D (16) -#define AL_VOCAL_MORPHER_PHONEME_F (17) -#define AL_VOCAL_MORPHER_PHONEME_G (18) -#define AL_VOCAL_MORPHER_PHONEME_J (19) -#define AL_VOCAL_MORPHER_PHONEME_K (20) -#define AL_VOCAL_MORPHER_PHONEME_L (21) -#define AL_VOCAL_MORPHER_PHONEME_M (22) -#define AL_VOCAL_MORPHER_PHONEME_N (23) -#define AL_VOCAL_MORPHER_PHONEME_P (24) -#define AL_VOCAL_MORPHER_PHONEME_R (25) -#define AL_VOCAL_MORPHER_PHONEME_S (26) -#define AL_VOCAL_MORPHER_PHONEME_T (27) -#define AL_VOCAL_MORPHER_PHONEME_V (28) -#define AL_VOCAL_MORPHER_PHONEME_Z (29) - -#define AL_VOCAL_MORPHER_WAVEFORM_SINUSOID (0) -#define AL_VOCAL_MORPHER_WAVEFORM_TRIANGLE (1) -#define AL_VOCAL_MORPHER_WAVEFORM_SAWTOOTH (2) - -#define AL_VOCAL_MORPHER_MIN_WAVEFORM (0) -#define AL_VOCAL_MORPHER_MAX_WAVEFORM (2) -#define AL_VOCAL_MORPHER_DEFAULT_WAVEFORM (0) - -#define AL_VOCAL_MORPHER_MIN_RATE (0.0f) -#define AL_VOCAL_MORPHER_MAX_RATE (10.0f) -#define AL_VOCAL_MORPHER_DEFAULT_RATE (1.41f) +#define AL_VOCAL_MORPHER_PHONEME_A (0) +#define AL_VOCAL_MORPHER_PHONEME_E (1) +#define AL_VOCAL_MORPHER_PHONEME_I (2) +#define AL_VOCAL_MORPHER_PHONEME_O (3) +#define AL_VOCAL_MORPHER_PHONEME_U (4) +#define AL_VOCAL_MORPHER_PHONEME_AA (5) +#define AL_VOCAL_MORPHER_PHONEME_AE (6) +#define AL_VOCAL_MORPHER_PHONEME_AH (7) +#define AL_VOCAL_MORPHER_PHONEME_AO (8) +#define AL_VOCAL_MORPHER_PHONEME_EH (9) +#define AL_VOCAL_MORPHER_PHONEME_ER (10) +#define AL_VOCAL_MORPHER_PHONEME_IH (11) +#define AL_VOCAL_MORPHER_PHONEME_IY (12) +#define AL_VOCAL_MORPHER_PHONEME_UH (13) +#define AL_VOCAL_MORPHER_PHONEME_UW (14) +#define AL_VOCAL_MORPHER_PHONEME_B (15) +#define AL_VOCAL_MORPHER_PHONEME_D (16) +#define AL_VOCAL_MORPHER_PHONEME_F (17) +#define AL_VOCAL_MORPHER_PHONEME_G (18) +#define AL_VOCAL_MORPHER_PHONEME_J (19) +#define AL_VOCAL_MORPHER_PHONEME_K (20) +#define AL_VOCAL_MORPHER_PHONEME_L (21) +#define AL_VOCAL_MORPHER_PHONEME_M (22) +#define AL_VOCAL_MORPHER_PHONEME_N (23) +#define AL_VOCAL_MORPHER_PHONEME_P (24) +#define AL_VOCAL_MORPHER_PHONEME_R (25) +#define AL_VOCAL_MORPHER_PHONEME_S (26) +#define AL_VOCAL_MORPHER_PHONEME_T (27) +#define AL_VOCAL_MORPHER_PHONEME_V (28) +#define AL_VOCAL_MORPHER_PHONEME_Z (29) + +#define AL_VOCAL_MORPHER_WAVEFORM_SINUSOID (0) +#define AL_VOCAL_MORPHER_WAVEFORM_TRIANGLE (1) +#define AL_VOCAL_MORPHER_WAVEFORM_SAWTOOTH (2) + +#define AL_VOCAL_MORPHER_MIN_WAVEFORM (0) +#define AL_VOCAL_MORPHER_MAX_WAVEFORM (2) +#define AL_VOCAL_MORPHER_DEFAULT_WAVEFORM (0) + +#define AL_VOCAL_MORPHER_MIN_RATE (0.0f) +#define AL_VOCAL_MORPHER_MAX_RATE (10.0f) +#define AL_VOCAL_MORPHER_DEFAULT_RATE (1.41f) /* Pitch shifter effect */ -#define AL_PITCH_SHIFTER_MIN_COARSE_TUNE (-12) -#define AL_PITCH_SHIFTER_MAX_COARSE_TUNE (12) -#define AL_PITCH_SHIFTER_DEFAULT_COARSE_TUNE (12) +#define AL_PITCH_SHIFTER_MIN_COARSE_TUNE (-12) +#define AL_PITCH_SHIFTER_MAX_COARSE_TUNE (12) +#define AL_PITCH_SHIFTER_DEFAULT_COARSE_TUNE (12) -#define AL_PITCH_SHIFTER_MIN_FINE_TUNE (-50) -#define AL_PITCH_SHIFTER_MAX_FINE_TUNE (50) -#define AL_PITCH_SHIFTER_DEFAULT_FINE_TUNE (0) +#define AL_PITCH_SHIFTER_MIN_FINE_TUNE (-50) +#define AL_PITCH_SHIFTER_MAX_FINE_TUNE (50) +#define AL_PITCH_SHIFTER_DEFAULT_FINE_TUNE (0) /* Ring modulator effect */ -#define AL_RING_MODULATOR_MIN_FREQUENCY (0.0f) -#define AL_RING_MODULATOR_MAX_FREQUENCY (8000.0f) -#define AL_RING_MODULATOR_DEFAULT_FREQUENCY (440.0f) +#define AL_RING_MODULATOR_MIN_FREQUENCY (0.0f) +#define AL_RING_MODULATOR_MAX_FREQUENCY (8000.0f) +#define AL_RING_MODULATOR_DEFAULT_FREQUENCY (440.0f) -#define AL_RING_MODULATOR_MIN_HIGHPASS_CUTOFF (0.0f) -#define AL_RING_MODULATOR_MAX_HIGHPASS_CUTOFF (24000.0f) +#define AL_RING_MODULATOR_MIN_HIGHPASS_CUTOFF (0.0f) +#define AL_RING_MODULATOR_MAX_HIGHPASS_CUTOFF (24000.0f) #define AL_RING_MODULATOR_DEFAULT_HIGHPASS_CUTOFF (800.0f) -#define AL_RING_MODULATOR_SINUSOID (0) -#define AL_RING_MODULATOR_SAWTOOTH (1) -#define AL_RING_MODULATOR_SQUARE (2) +#define AL_RING_MODULATOR_SINUSOID (0) +#define AL_RING_MODULATOR_SAWTOOTH (1) +#define AL_RING_MODULATOR_SQUARE (2) -#define AL_RING_MODULATOR_MIN_WAVEFORM (0) -#define AL_RING_MODULATOR_MAX_WAVEFORM (2) -#define AL_RING_MODULATOR_DEFAULT_WAVEFORM (0) +#define AL_RING_MODULATOR_MIN_WAVEFORM (0) +#define AL_RING_MODULATOR_MAX_WAVEFORM (2) +#define AL_RING_MODULATOR_DEFAULT_WAVEFORM (0) /* Autowah effect */ -#define AL_AUTOWAH_MIN_ATTACK_TIME (0.0001f) -#define AL_AUTOWAH_MAX_ATTACK_TIME (1.0f) -#define AL_AUTOWAH_DEFAULT_ATTACK_TIME (0.06f) +#define AL_AUTOWAH_MIN_ATTACK_TIME (0.0001f) +#define AL_AUTOWAH_MAX_ATTACK_TIME (1.0f) +#define AL_AUTOWAH_DEFAULT_ATTACK_TIME (0.06f) -#define AL_AUTOWAH_MIN_RELEASE_TIME (0.0001f) -#define AL_AUTOWAH_MAX_RELEASE_TIME (1.0f) -#define AL_AUTOWAH_DEFAULT_RELEASE_TIME (0.06f) +#define AL_AUTOWAH_MIN_RELEASE_TIME (0.0001f) +#define AL_AUTOWAH_MAX_RELEASE_TIME (1.0f) +#define AL_AUTOWAH_DEFAULT_RELEASE_TIME (0.06f) -#define AL_AUTOWAH_MIN_RESONANCE (2.0f) -#define AL_AUTOWAH_MAX_RESONANCE (1000.0f) -#define AL_AUTOWAH_DEFAULT_RESONANCE (1000.0f) +#define AL_AUTOWAH_MIN_RESONANCE (2.0f) +#define AL_AUTOWAH_MAX_RESONANCE (1000.0f) +#define AL_AUTOWAH_DEFAULT_RESONANCE (1000.0f) -#define AL_AUTOWAH_MIN_PEAK_GAIN (0.00003f) -#define AL_AUTOWAH_MAX_PEAK_GAIN (31621.0f) -#define AL_AUTOWAH_DEFAULT_PEAK_GAIN (11.22f) +#define AL_AUTOWAH_MIN_PEAK_GAIN (0.00003f) +#define AL_AUTOWAH_MAX_PEAK_GAIN (31621.0f) +#define AL_AUTOWAH_DEFAULT_PEAK_GAIN (11.22f) /* Compressor effect */ -#define AL_COMPRESSOR_MIN_ONOFF (0) -#define AL_COMPRESSOR_MAX_ONOFF (1) -#define AL_COMPRESSOR_DEFAULT_ONOFF (1) +#define AL_COMPRESSOR_MIN_ONOFF (0) +#define AL_COMPRESSOR_MAX_ONOFF (1) +#define AL_COMPRESSOR_DEFAULT_ONOFF (1) /* Equalizer effect */ -#define AL_EQUALIZER_MIN_LOW_GAIN (0.126f) -#define AL_EQUALIZER_MAX_LOW_GAIN (7.943f) -#define AL_EQUALIZER_DEFAULT_LOW_GAIN (1.0f) - -#define AL_EQUALIZER_MIN_LOW_CUTOFF (50.0f) -#define AL_EQUALIZER_MAX_LOW_CUTOFF (800.0f) -#define AL_EQUALIZER_DEFAULT_LOW_CUTOFF (200.0f) +#define AL_EQUALIZER_MIN_LOW_GAIN (0.126f) +#define AL_EQUALIZER_MAX_LOW_GAIN (7.943f) +#define AL_EQUALIZER_DEFAULT_LOW_GAIN (1.0f) -#define AL_EQUALIZER_MIN_MID1_GAIN (0.126f) -#define AL_EQUALIZER_MAX_MID1_GAIN (7.943f) -#define AL_EQUALIZER_DEFAULT_MID1_GAIN (1.0f) +#define AL_EQUALIZER_MIN_LOW_CUTOFF (50.0f) +#define AL_EQUALIZER_MAX_LOW_CUTOFF (800.0f) +#define AL_EQUALIZER_DEFAULT_LOW_CUTOFF (200.0f) -#define AL_EQUALIZER_MIN_MID1_CENTER (200.0f) -#define AL_EQUALIZER_MAX_MID1_CENTER (3000.0f) -#define AL_EQUALIZER_DEFAULT_MID1_CENTER (500.0f) +#define AL_EQUALIZER_MIN_MID1_GAIN (0.126f) +#define AL_EQUALIZER_MAX_MID1_GAIN (7.943f) +#define AL_EQUALIZER_DEFAULT_MID1_GAIN (1.0f) -#define AL_EQUALIZER_MIN_MID1_WIDTH (0.01f) -#define AL_EQUALIZER_MAX_MID1_WIDTH (1.0f) -#define AL_EQUALIZER_DEFAULT_MID1_WIDTH (1.0f) +#define AL_EQUALIZER_MIN_MID1_CENTER (200.0f) +#define AL_EQUALIZER_MAX_MID1_CENTER (3000.0f) +#define AL_EQUALIZER_DEFAULT_MID1_CENTER (500.0f) -#define AL_EQUALIZER_MIN_MID2_GAIN (0.126f) -#define AL_EQUALIZER_MAX_MID2_GAIN (7.943f) -#define AL_EQUALIZER_DEFAULT_MID2_GAIN (1.0f) +#define AL_EQUALIZER_MIN_MID1_WIDTH (0.01f) +#define AL_EQUALIZER_MAX_MID1_WIDTH (1.0f) +#define AL_EQUALIZER_DEFAULT_MID1_WIDTH (1.0f) -#define AL_EQUALIZER_MIN_MID2_CENTER (1000.0f) -#define AL_EQUALIZER_MAX_MID2_CENTER (8000.0f) -#define AL_EQUALIZER_DEFAULT_MID2_CENTER (3000.0f) +#define AL_EQUALIZER_MIN_MID2_GAIN (0.126f) +#define AL_EQUALIZER_MAX_MID2_GAIN (7.943f) +#define AL_EQUALIZER_DEFAULT_MID2_GAIN (1.0f) -#define AL_EQUALIZER_MIN_MID2_WIDTH (0.01f) -#define AL_EQUALIZER_MAX_MID2_WIDTH (1.0f) -#define AL_EQUALIZER_DEFAULT_MID2_WIDTH (1.0f) +#define AL_EQUALIZER_MIN_MID2_CENTER (1000.0f) +#define AL_EQUALIZER_MAX_MID2_CENTER (8000.0f) +#define AL_EQUALIZER_DEFAULT_MID2_CENTER (3000.0f) -#define AL_EQUALIZER_MIN_HIGH_GAIN (0.126f) -#define AL_EQUALIZER_MAX_HIGH_GAIN (7.943f) -#define AL_EQUALIZER_DEFAULT_HIGH_GAIN (1.0f) +#define AL_EQUALIZER_MIN_MID2_WIDTH (0.01f) +#define AL_EQUALIZER_MAX_MID2_WIDTH (1.0f) +#define AL_EQUALIZER_DEFAULT_MID2_WIDTH (1.0f) -#define AL_EQUALIZER_MIN_HIGH_CUTOFF (4000.0f) -#define AL_EQUALIZER_MAX_HIGH_CUTOFF (16000.0f) -#define AL_EQUALIZER_DEFAULT_HIGH_CUTOFF (6000.0f) +#define AL_EQUALIZER_MIN_HIGH_GAIN (0.126f) +#define AL_EQUALIZER_MAX_HIGH_GAIN (7.943f) +#define AL_EQUALIZER_DEFAULT_HIGH_GAIN (1.0f) +#define AL_EQUALIZER_MIN_HIGH_CUTOFF (4000.0f) +#define AL_EQUALIZER_MAX_HIGH_CUTOFF (16000.0f) +#define AL_EQUALIZER_DEFAULT_HIGH_CUTOFF (6000.0f) /* Source parameter value ranges and defaults. */ -#define AL_MIN_AIR_ABSORPTION_FACTOR (0.0f) -#define AL_MAX_AIR_ABSORPTION_FACTOR (10.0f) -#define AL_DEFAULT_AIR_ABSORPTION_FACTOR (0.0f) +#define AL_MIN_AIR_ABSORPTION_FACTOR (0.0f) +#define AL_MAX_AIR_ABSORPTION_FACTOR (10.0f) +#define AL_DEFAULT_AIR_ABSORPTION_FACTOR (0.0f) -#define AL_MIN_ROOM_ROLLOFF_FACTOR (0.0f) -#define AL_MAX_ROOM_ROLLOFF_FACTOR (10.0f) -#define AL_DEFAULT_ROOM_ROLLOFF_FACTOR (0.0f) +#define AL_MIN_ROOM_ROLLOFF_FACTOR (0.0f) +#define AL_MAX_ROOM_ROLLOFF_FACTOR (10.0f) +#define AL_DEFAULT_ROOM_ROLLOFF_FACTOR (0.0f) -#define AL_MIN_CONE_OUTER_GAINHF (0.0f) -#define AL_MAX_CONE_OUTER_GAINHF (1.0f) -#define AL_DEFAULT_CONE_OUTER_GAINHF (1.0f) +#define AL_MIN_CONE_OUTER_GAINHF (0.0f) +#define AL_MAX_CONE_OUTER_GAINHF (1.0f) +#define AL_DEFAULT_CONE_OUTER_GAINHF (1.0f) -#define AL_MIN_DIRECT_FILTER_GAINHF_AUTO AL_FALSE -#define AL_MAX_DIRECT_FILTER_GAINHF_AUTO AL_TRUE -#define AL_DEFAULT_DIRECT_FILTER_GAINHF_AUTO AL_TRUE +#define AL_MIN_DIRECT_FILTER_GAINHF_AUTO AL_FALSE +#define AL_MAX_DIRECT_FILTER_GAINHF_AUTO AL_TRUE +#define AL_DEFAULT_DIRECT_FILTER_GAINHF_AUTO AL_TRUE -#define AL_MIN_AUXILIARY_SEND_FILTER_GAIN_AUTO AL_FALSE -#define AL_MAX_AUXILIARY_SEND_FILTER_GAIN_AUTO AL_TRUE +#define AL_MIN_AUXILIARY_SEND_FILTER_GAIN_AUTO AL_FALSE +#define AL_MAX_AUXILIARY_SEND_FILTER_GAIN_AUTO AL_TRUE #define AL_DEFAULT_AUXILIARY_SEND_FILTER_GAIN_AUTO AL_TRUE #define AL_MIN_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_FALSE #define AL_MAX_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_TRUE #define AL_DEFAULT_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_TRUE - /* Listener parameter value ranges and defaults. */ -#define AL_MIN_METERS_PER_UNIT FLT_MIN -#define AL_MAX_METERS_PER_UNIT FLT_MAX -#define AL_DEFAULT_METERS_PER_UNIT (1.0f) - +#define AL_MIN_METERS_PER_UNIT FLT_MIN +#define AL_MAX_METERS_PER_UNIT FLT_MAX +#define AL_DEFAULT_METERS_PER_UNIT (1.0f) #ifdef __cplusplus -} /* extern "C" */ +} /* extern "C" */ #endif #endif /* AL_EFX_H */ diff --git a/apps/openmw/mwsound/ffmpeg_decoder.cpp b/apps/openmw/mwsound/ffmpeg_decoder.cpp index ec36a5cdf84..54fd126c41e 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.cpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.cpp @@ -1,470 +1,541 @@ #include "ffmpeg_decoder.hpp" +#include #include - #include -#include +#include #include #include -namespace MWSound -{ +#include +#include + +#if OPENMW_FFMPEG_5_OR_GREATER +#include +#endif -int FFmpeg_Decoder::readPacket(void *user_data, uint8_t *buf, int buf_size) +namespace MWSound { - try + void AVIOContextDeleter::operator()(AVIOContext* ptr) const { - std::istream& stream = *static_cast(user_data)->mDataStream; - stream.clear(); - stream.read((char*)buf, buf_size); - return stream.gcount(); + if (ptr->buffer != nullptr) + av_freep(&ptr->buffer); + +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 80, 100) + avio_context_free(&ptr); +#else + av_free(ptr); +#endif } - catch (std::exception& ) + + void AVFormatContextDeleter::operator()(AVFormatContext* ptr) const { - return 0; + avformat_close_input(&ptr); } -} -int FFmpeg_Decoder::writePacket(void *, uint8_t *, int) -{ - 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) -{ - std::istream& stream = *static_cast(user_data)->mDataStream; - - whence &= ~AVSEEK_FORCE; + void AVCodecContextDeleter::operator()(AVCodecContext* ptr) const + { + avcodec_free_context(&ptr); + } - stream.clear(); + void AVFrameDeleter::operator()(AVFrame* ptr) const + { + av_frame_free(&ptr); + } - if(whence == AVSEEK_SIZE) + int FFmpeg_Decoder::readPacket(void* user_data, uint8_t* buf, int buf_size) { - size_t prev = stream.tellg(); - stream.seekg(0, std::ios_base::end); - size_t size = stream.tellg(); - stream.seekg(prev, std::ios_base::beg); - return size; + try + { + std::istream& stream = *static_cast(user_data)->mDataStream; + stream.clear(); + stream.read((char*)buf, buf_size); + std::streamsize count = stream.gcount(); + if (count == 0) + return AVERROR_EOF; + if (count > std::numeric_limits::max()) + return AVERROR_BUG; + return static_cast(count); + } + catch (std::exception&) + { + return AVERROR_UNKNOWN; + } } - if(whence == SEEK_SET) - stream.seekg(offset, std::ios_base::beg); - else if(whence == SEEK_CUR) - stream.seekg(offset, std::ios_base::cur); - else if(whence == SEEK_END) - stream.seekg(offset, std::ios_base::end); - else +#if OPENMW_FFMPEG_CONST_WRITEPACKET + int FFmpeg_Decoder::writePacket(void*, const uint8_t*, int) +#else + int FFmpeg_Decoder::writePacket(void*, uint8_t*, int) +#endif + { + Log(Debug::Error) << "can't write to read-only stream"; return -1; + } - return stream.tellg(); -} + int64_t FFmpeg_Decoder::seek(void* user_data, int64_t offset, int whence) + { + std::istream& stream = *static_cast(user_data)->mDataStream; + whence &= ~AVSEEK_FORCE; -/* 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() -{ - if(!mStream) - return false; + stream.clear(); - int stream_idx = mStream - mFormatCtx->streams; - while(av_read_frame(mFormatCtx, &mPacket) >= 0) - { - /* Check if the packet belongs to this stream */ - if(stream_idx == mPacket.stream_index) + if (whence == AVSEEK_SIZE) { - if(mPacket.pts != (int64_t)AV_NOPTS_VALUE) - mNextPts = av_q2d((*mStream)->time_base)*mPacket.pts; - return true; + size_t prev = stream.tellg(); + stream.seekg(0, std::ios_base::end); + size_t size = stream.tellg(); + stream.seekg(prev, std::ios_base::beg); + return size; } - /* Free the packet and look for another */ - av_packet_unref(&mPacket); - } - - return false; -} + if (whence == SEEK_SET) + stream.seekg(offset, std::ios_base::beg); + else if (whence == SEEK_CUR) + stream.seekg(offset, std::ios_base::cur); + else if (whence == SEEK_END) + stream.seekg(offset, std::ios_base::end); + else + return -1; -bool FFmpeg_Decoder::getAVAudioData() -{ - bool got_frame = false; + return stream.tellg(); + } - if(mCodecCtx->codec_type != AVMEDIA_TYPE_AUDIO) - return false; + /* 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() + { + if (!mStream) + return false; - do { - /* Decode some data, and check for errors */ - int ret = avcodec_receive_frame(mCodecCtx, mFrame); - if (ret == AVERROR(EAGAIN)) + std::ptrdiff_t stream_idx = mStream - mFormatCtx->streams; + while (av_read_frame(mFormatCtx.get(), &mPacket) >= 0) { - if (mPacket.size == 0 && !getNextPacket()) - return false; - ret = avcodec_send_packet(mCodecCtx, &mPacket); + /* Check if the packet belongs to this stream */ + if (stream_idx == mPacket.stream_index) + { + if (mPacket.pts != (int64_t)AV_NOPTS_VALUE) + mNextPts = av_q2d((*mStream)->time_base) * mPacket.pts; + return true; + } + + /* Free the packet and look for another */ av_packet_unref(&mPacket); - if (ret == 0) - continue; } - if (ret != 0) - return false; - av_packet_unref(&mPacket); + return false; + } - if (mFrame->nb_samples == 0) - continue; - got_frame = true; + bool FFmpeg_Decoder::getAVAudioData() + { + bool got_frame = false; + + if (mCodecCtx->codec_type != AVMEDIA_TYPE_AUDIO) + return false; - if(mSwr) + do { - if(!mDataBuf || mDataBufLen < mFrame->nb_samples) + /* Decode some data, and check for errors */ + int ret = avcodec_receive_frame(mCodecCtx.get(), mFrame.get()); + if (ret == AVERROR(EAGAIN)) { - av_freep(&mDataBuf); - if(av_samples_alloc(&mDataBuf, nullptr, av_get_channel_layout_nb_channels(mOutputChannelLayout), - mFrame->nb_samples, mOutputSampleFormat, 0) < 0) + if (mPacket.size == 0 && !getNextPacket()) return false; - else - mDataBufLen = mFrame->nb_samples; + ret = avcodec_send_packet(mCodecCtx.get(), &mPacket); + av_packet_unref(&mPacket); + if (ret == 0) + continue; } + if (ret != 0) + return false; + + av_packet_unref(&mPacket); + + if (mFrame->nb_samples == 0) + continue; + got_frame = true; - if(swr_convert(mSwr, (uint8_t**)&mDataBuf, mFrame->nb_samples, - (const uint8_t**)mFrame->extended_data, mFrame->nb_samples) < 0) + if (mSwr) { - return false; - } - mFrameData = &mDataBuf; - } - else - mFrameData = &mFrame->data[0]; + if (!mDataBuf || mDataBufLen < mFrame->nb_samples) + { + av_freep(&mDataBuf); +#if OPENMW_FFMPEG_5_OR_GREATER + if (av_samples_alloc(&mDataBuf, nullptr, mOutputChannelLayout.nb_channels, +#else + if (av_samples_alloc(&mDataBuf, nullptr, av_get_channel_layout_nb_channels(mOutputChannelLayout), +#endif + mFrame->nb_samples, mOutputSampleFormat, 0) + < 0) + return false; + else + mDataBufLen = mFrame->nb_samples; + } - } while(!got_frame); - mNextPts += (double)mFrame->nb_samples / mCodecCtx->sample_rate; + if (swr_convert(mSwr, (uint8_t**)&mDataBuf, mFrame->nb_samples, (const uint8_t**)mFrame->extended_data, + mFrame->nb_samples) + < 0) + { + return false; + } + mFrameData = &mDataBuf; + } + else + mFrameData = &mFrame->data[0]; - return true; -} + } while (!got_frame); + mNextPts += (double)mFrame->nb_samples / mCodecCtx->sample_rate; -size_t FFmpeg_Decoder::readAVAudioData(void *data, size_t length) -{ - size_t dec = 0; + return true; + } - while(dec < length) + size_t FFmpeg_Decoder::readAVAudioData(void* data, size_t length) { - /* If there's no decoded data, find some */ - if(mFramePos >= mFrameSize) + size_t dec = 0; + + while (dec < length) { - if(!getAVAudioData()) - break; - mFramePos = 0; - mFrameSize = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) * - av_get_bytes_per_sample(mOutputSampleFormat); - } + /* If there's no decoded data, find some */ + if (mFramePos >= mFrameSize) + { + if (!getAVAudioData()) + break; + mFramePos = 0; +#if OPENMW_FFMPEG_5_OR_GREATER + mFrameSize = mFrame->nb_samples * mOutputChannelLayout.nb_channels +#else + mFrameSize = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) +#endif + * av_get_bytes_per_sample(mOutputSampleFormat); + } - /* Get the amount of bytes remaining to be written, and clamp to - * the amount of decoded data we have */ - size_t rem = std::min(length-dec, mFrameSize-mFramePos); + /* Get the amount of bytes remaining to be written, and clamp to + * the amount of decoded data we have */ + size_t rem = std::min(length - dec, mFrameSize - mFramePos); - /* Copy the data to the app's buffer and increment */ - memcpy(data, mFrameData[0]+mFramePos, rem); - data = (char*)data + rem; - dec += rem; - mFramePos += rem; + /* Copy the data to the app's buffer and increment */ + memcpy(data, mFrameData[0] + mFramePos, rem); + data = (char*)data + rem; + dec += rem; + mFramePos += rem; + } + + /* Return the number of bytes we were able to get */ + return dec; } - /* Return the number of bytes we were able to get */ - return dec; -} + void FFmpeg_Decoder::open(VFS::Path::NormalizedView fname) + { + close(); + mDataStream = mResourceMgr->get(fname); -void FFmpeg_Decoder::open(const std::string &fname) -{ - close(); - mDataStream = mResourceMgr->get(fname); + AVIOContextPtr ioCtx(avio_alloc_context(nullptr, 0, 0, this, readPacket, writePacket, seek)); + if (ioCtx == nullptr) + throw std::runtime_error("Failed to allocate AVIO context"); - if((mFormatCtx=avformat_alloc_context()) == nullptr) - throw std::runtime_error("Failed to allocate context"); + AVFormatContext* formatCtx = avformat_alloc_context(); + if (formatCtx == nullptr) + throw std::runtime_error("Failed to allocate context"); - try - { - mFormatCtx->pb = avio_alloc_context(nullptr, 0, 0, this, readPacket, writePacket, seek); - if(!mFormatCtx->pb || avformat_open_input(&mFormatCtx, fname.c_str(), nullptr, nullptr) != 0) - { - // "Note that a user-supplied AVFormatContext will be freed on failure". - if (mFormatCtx) - { - if (mFormatCtx->pb != nullptr) - { - if (mFormatCtx->pb->buffer != nullptr) - { - av_free(mFormatCtx->pb->buffer); - mFormatCtx->pb->buffer = nullptr; - } - av_free(mFormatCtx->pb); - mFormatCtx->pb = nullptr; - } - avformat_free_context(mFormatCtx); - } - mFormatCtx = nullptr; - throw std::runtime_error("Failed to allocate input stream"); - } + formatCtx->pb = ioCtx.get(); + + // avformat_open_input frees user supplied AVFormatContext on failure + if (avformat_open_input(&formatCtx, fname.value().data(), nullptr, nullptr) != 0) + throw std::runtime_error("Failed to open input"); - if(avformat_find_stream_info(mFormatCtx, nullptr) < 0) - throw std::runtime_error("Failed to find stream info in "+fname); + AVFormatContextPtr formatCtxPtr(std::exchange(formatCtx, nullptr)); - for(size_t j = 0;j < mFormatCtx->nb_streams;j++) + if (avformat_find_stream_info(formatCtxPtr.get(), nullptr) < 0) + throw std::runtime_error("Failed to find stream info"); + + AVStream** stream = nullptr; + for (size_t j = 0; j < formatCtxPtr->nb_streams; j++) { - if(mFormatCtx->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) + if (formatCtxPtr->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { - mStream = &mFormatCtx->streams[j]; + stream = &formatCtxPtr->streams[j]; break; } } - if(!mStream) - throw std::runtime_error("No audio streams in "+fname); - AVCodec *codec = avcodec_find_decoder((*mStream)->codecpar->codec_id); - if(!codec) - { - std::string ss = "No codec found for id " + - std::to_string((*mStream)->codecpar->codec_id); - throw std::runtime_error(ss); - } + if (stream == nullptr) + throw std::runtime_error("No audio streams"); + + const AVCodec* codec = avcodec_find_decoder((*stream)->codecpar->codec_id); + if (codec == nullptr) + throw std::runtime_error("No codec found for id " + std::to_string((*stream)->codecpar->codec_id)); - AVCodecContext *avctx = avcodec_alloc_context3(codec); - avcodec_parameters_to_context(avctx, (*mStream)->codecpar); + AVCodecContext* codecCtx = avcodec_alloc_context3(codec); + if (codecCtx == nullptr) + throw std::runtime_error("Failed to allocate codec context"); + + avcodec_parameters_to_context(codecCtx, (*stream)->codecpar); // This is not needed anymore above FFMpeg version 4.0 #if LIBAVCODEC_VERSION_INT < 3805796 - av_codec_set_pkt_timebase(avctx, (*mStream)->time_base); + av_codec_set_pkt_timebase(avctx, (*stream)->time_base); #endif - mCodecCtx = avctx; + AVCodecContextPtr codecCtxPtr(std::exchange(codecCtx, nullptr)); - if(avcodec_open2(mCodecCtx, codec, nullptr) < 0) + if (avcodec_open2(codecCtxPtr.get(), codec, nullptr) < 0) throw std::runtime_error(std::string("Failed to open audio codec ") + codec->long_name); - mFrame = av_frame_alloc(); + AVFramePtr frame(av_frame_alloc()); + if (frame == nullptr) + throw std::runtime_error("Failed to allocate frame"); - if(mCodecCtx->sample_fmt == AV_SAMPLE_FMT_FLT || mCodecCtx->sample_fmt == AV_SAMPLE_FMT_FLTP) - mOutputSampleFormat = AV_SAMPLE_FMT_S16; // FIXME: Check for AL_EXT_FLOAT32 support - else if(mCodecCtx->sample_fmt == AV_SAMPLE_FMT_U8P) + if (codecCtxPtr->sample_fmt == AV_SAMPLE_FMT_U8P) mOutputSampleFormat = AV_SAMPLE_FMT_U8; - else if(mCodecCtx->sample_fmt == AV_SAMPLE_FMT_S16P) - mOutputSampleFormat = AV_SAMPLE_FMT_S16; + // FIXME: Check for AL_EXT_FLOAT32 support + // else if (codecCtxPtr->sample_fmt == AV_SAMPLE_FMT_FLT || codecCtxPtr->sample_fmt == AV_SAMPLE_FMT_FLTP) + // mOutputSampleFormat = AV_SAMPLE_FMT_S16; else mOutputSampleFormat = AV_SAMPLE_FMT_S16; - mOutputChannelLayout = (*mStream)->codecpar->channel_layout; - if(mOutputChannelLayout == 0) - mOutputChannelLayout = av_get_default_channel_layout(mCodecCtx->channels); +#if OPENMW_FFMPEG_5_OR_GREATER + mOutputChannelLayout = (*stream)->codecpar->ch_layout; // sefault + if (mOutputChannelLayout.u.mask == 0) + av_channel_layout_default(&mOutputChannelLayout, codecCtxPtr->ch_layout.nb_channels); - mCodecCtx->channel_layout = mOutputChannelLayout; + codecCtxPtr->ch_layout = mOutputChannelLayout; +#else + mOutputChannelLayout = (*stream)->codecpar->channel_layout; + if (mOutputChannelLayout == 0) + mOutputChannelLayout = av_get_default_channel_layout(codecCtxPtr->channels); + + codecCtxPtr->channel_layout = mOutputChannelLayout; +#endif + + mIoCtx = std::move(ioCtx); + mFrame = std::move(frame); + mFormatCtx = std::move(formatCtxPtr); + mCodecCtx = std::move(codecCtxPtr); + mStream = stream; } - catch(...) + + void FFmpeg_Decoder::close() { - if(mStream) - avcodec_free_context(&mCodecCtx); mStream = nullptr; + mCodecCtx.reset(); - if (mFormatCtx != nullptr) - { - if (mFormatCtx->pb->buffer != nullptr) - { - av_free(mFormatCtx->pb->buffer); - mFormatCtx->pb->buffer = nullptr; - } - av_free(mFormatCtx->pb); - mFormatCtx->pb = nullptr; + av_packet_unref(&mPacket); + av_freep(&mDataBuf); + mFrame.reset(); + swr_free(&mSwr); - avformat_close_input(&mFormatCtx); - } + mFormatCtx.reset(); + mIoCtx.reset(); + mDataStream.reset(); } -} -void FFmpeg_Decoder::close() -{ - if(mStream) - avcodec_free_context(&mCodecCtx); - mStream = nullptr; - - av_packet_unref(&mPacket); - av_freep(&mDataBuf); - av_frame_free(&mFrame); - swr_free(&mSwr); + std::string FFmpeg_Decoder::getName() + { +// In the FFMpeg 4.0 a "filename" field was replaced by "url" +#if LIBAVCODEC_VERSION_INT < 3805796 + return mFormatCtx->filename; +#else + return mFormatCtx->url; +#endif + } - if(mFormatCtx) + void FFmpeg_Decoder::getInfo(int* samplerate, ChannelConfig* chans, SampleType* type) { - if (mFormatCtx->pb != nullptr) + if (!mStream) + throw std::runtime_error("No audio stream info"); + + if (mOutputSampleFormat == AV_SAMPLE_FMT_U8) + *type = SampleType_UInt8; + else if (mOutputSampleFormat == AV_SAMPLE_FMT_S16) + *type = SampleType_Int16; + else if (mOutputSampleFormat == AV_SAMPLE_FMT_FLT) + *type = SampleType_Float32; + else { - // mFormatCtx->pb->buffer must be freed by hand, - // if not, valgrind will show memleak, see: - // - // https://trac.ffmpeg.org/ticket/1357 - // - if (mFormatCtx->pb->buffer != nullptr) - { - av_freep(&mFormatCtx->pb->buffer); - } -#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 80, 100) - avio_context_free(&mFormatCtx->pb); + mOutputSampleFormat = AV_SAMPLE_FMT_S16; + *type = SampleType_Int16; + } + +#if OPENMW_FFMPEG_5_OR_GREATER + switch (mOutputChannelLayout.u.mask) +#else + switch (mOutputChannelLayout) +#endif + { + case AV_CH_LAYOUT_MONO: + *chans = ChannelConfig_Mono; + break; + case AV_CH_LAYOUT_STEREO: + *chans = ChannelConfig_Stereo; + break; + case AV_CH_LAYOUT_QUAD: + *chans = ChannelConfig_Quad; + break; + case AV_CH_LAYOUT_5POINT1: + *chans = ChannelConfig_5point1; + break; + case AV_CH_LAYOUT_7POINT1: + *chans = ChannelConfig_7point1; + break; + default: + char str[1024]; +#if OPENMW_FFMPEG_5_OR_GREATER + av_channel_layout_describe(&mCodecCtx->ch_layout, str, sizeof(str)); + Log(Debug::Error) << "Unsupported channel layout: " << str; + + if (mCodecCtx->ch_layout.nb_channels == 1) + { + mOutputChannelLayout = AV_CHANNEL_LAYOUT_MONO; + *chans = ChannelConfig_Mono; + } + else + { + mOutputChannelLayout = AV_CHANNEL_LAYOUT_STEREO; + *chans = ChannelConfig_Stereo; + } #else - av_freep(&mFormatCtx->pb); + av_get_channel_layout_string(str, sizeof(str), mCodecCtx->channels, mCodecCtx->channel_layout); + Log(Debug::Error) << "Unsupported channel layout: " << str; + + if (mCodecCtx->channels == 1) + { + mOutputChannelLayout = AV_CH_LAYOUT_MONO; + *chans = ChannelConfig_Mono; + } + else + { + mOutputChannelLayout = AV_CH_LAYOUT_STEREO; + *chans = ChannelConfig_Stereo; + } #endif + break; } - avformat_close_input(&mFormatCtx); - } - mDataStream.reset(); -} + *samplerate = mCodecCtx->sample_rate; +#if OPENMW_FFMPEG_5_OR_GREATER + AVChannelLayout ch_layout = mCodecCtx->ch_layout; + if (ch_layout.u.mask == 0) + av_channel_layout_default(&ch_layout, mCodecCtx->ch_layout.nb_channels); -std::string FFmpeg_Decoder::getName() -{ -// In the FFMpeg 4.0 a "filename" field was replaced by "url" -#if LIBAVCODEC_VERSION_INT < 3805796 - return mFormatCtx->filename; + if (mOutputSampleFormat != mCodecCtx->sample_fmt || mOutputChannelLayout.u.mask != ch_layout.u.mask) #else - return mFormatCtx->url; + int64_t ch_layout = mCodecCtx->channel_layout; + if (ch_layout == 0) + ch_layout = av_get_default_channel_layout(mCodecCtx->channels); + + if (mOutputSampleFormat != mCodecCtx->sample_fmt || mOutputChannelLayout != ch_layout) #endif -} -void FFmpeg_Decoder::getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) -{ - if(!mStream) - throw std::runtime_error("No audio stream info"); - - if(mOutputSampleFormat == AV_SAMPLE_FMT_U8) - *type = SampleType_UInt8; - else if(mOutputSampleFormat == AV_SAMPLE_FMT_S16) - *type = SampleType_Int16; - else if(mOutputSampleFormat == AV_SAMPLE_FMT_FLT) - *type = SampleType_Float32; - else - { - mOutputSampleFormat = AV_SAMPLE_FMT_S16; - *type = SampleType_Int16; + { +#if OPENMW_FFMPEG_5_OR_GREATER + swr_alloc_set_opts2(&mSwr, // SwrContext + &mOutputChannelLayout, // output ch layout + mOutputSampleFormat, // output sample format + mCodecCtx->sample_rate, // output sample rate + &ch_layout, // input ch layout + mCodecCtx->sample_fmt, // input sample format + mCodecCtx->sample_rate, // input sample rate + 0, // logging level offset + nullptr); // log context +#else + mSwr = swr_alloc_set_opts(mSwr, // SwrContext + mOutputChannelLayout, // output ch layout + mOutputSampleFormat, // output sample format + mCodecCtx->sample_rate, // output sample rate + ch_layout, // input ch layout + mCodecCtx->sample_fmt, // input sample format + mCodecCtx->sample_rate, // input sample rate + 0, // logging level offset + nullptr); // log context +#endif + if (!mSwr) + throw std::runtime_error("Couldn't allocate SwrContext"); + int init = swr_init(mSwr); + if (init < 0) + throw std::runtime_error("Couldn't initialize SwrContext: " + std::to_string(init)); + } } - if(mOutputChannelLayout == AV_CH_LAYOUT_MONO) - *chans = ChannelConfig_Mono; - else if(mOutputChannelLayout == AV_CH_LAYOUT_STEREO) - *chans = ChannelConfig_Stereo; - else if(mOutputChannelLayout == AV_CH_LAYOUT_QUAD) - *chans = ChannelConfig_Quad; - else if(mOutputChannelLayout == AV_CH_LAYOUT_5POINT1) - *chans = ChannelConfig_5point1; - else if(mOutputChannelLayout == AV_CH_LAYOUT_7POINT1) - *chans = ChannelConfig_7point1; - else + size_t FFmpeg_Decoder::read(char* buffer, size_t bytes) { - char str[1024]; - av_get_channel_layout_string(str, sizeof(str), mCodecCtx->channels, mCodecCtx->channel_layout); - Log(Debug::Error) << "Unsupported channel layout: "<< str; - - if(mCodecCtx->channels == 1) + if (!mStream) { - mOutputChannelLayout = AV_CH_LAYOUT_MONO; - *chans = ChannelConfig_Mono; - } - else - { - mOutputChannelLayout = AV_CH_LAYOUT_STEREO; - *chans = ChannelConfig_Stereo; + Log(Debug::Error) << "No audio stream"; + return 0; } + return readAVAudioData(buffer, bytes); } - *samplerate = mCodecCtx->sample_rate; - int64_t ch_layout = mCodecCtx->channel_layout; - if(ch_layout == 0) - ch_layout = av_get_default_channel_layout(mCodecCtx->channels); - - if(mOutputSampleFormat != mCodecCtx->sample_fmt || - mOutputChannelLayout != ch_layout) + void FFmpeg_Decoder::readAll(std::vector& output) { - mSwr = swr_alloc_set_opts(mSwr, // SwrContext - mOutputChannelLayout, // output ch layout - mOutputSampleFormat, // output sample format - mCodecCtx->sample_rate, // output sample rate - ch_layout, // input ch layout - mCodecCtx->sample_fmt, // input sample format - mCodecCtx->sample_rate, // input sample rate - 0, // logging level offset - nullptr); // log context - if(!mSwr) - throw std::runtime_error("Couldn't allocate SwrContext"); - int init=swr_init(mSwr); - if(init < 0) - throw std::runtime_error("Couldn't initialize SwrContext: "+std::to_string(init)); - } -} + if (!mStream) + { + Log(Debug::Error) << "No audio stream"; + return; + } -size_t FFmpeg_Decoder::read(char *buffer, size_t bytes) -{ - if(!mStream) - { - Log(Debug::Error) << "No audio stream"; - return 0; + while (getAVAudioData()) + { +#if OPENMW_FFMPEG_5_OR_GREATER + size_t got = mFrame->nb_samples * mOutputChannelLayout.nb_channels +#else + size_t got = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) +#endif + * av_get_bytes_per_sample(mOutputSampleFormat); + const char* inbuf = reinterpret_cast(mFrameData[0]); + output.insert(output.end(), inbuf, inbuf + got); + } } - return readAVAudioData(buffer, bytes); -} -void FFmpeg_Decoder::readAll(std::vector &output) -{ - if(!mStream) + size_t FFmpeg_Decoder::getSampleOffset() { - Log(Debug::Error) << "No audio stream"; - return; +#if OPENMW_FFMPEG_5_OR_GREATER + std::size_t delay = (mFrameSize - mFramePos) / mOutputChannelLayout.nb_channels +#else + std::size_t delay = (mFrameSize - mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) +#endif + / av_get_bytes_per_sample(mOutputSampleFormat); + return static_cast(mNextPts * mCodecCtx->sample_rate) - delay; } - while(getAVAudioData()) + FFmpeg_Decoder::FFmpeg_Decoder(const VFS::Manager* vfs) + : Sound_Decoder(vfs) + , mStream(nullptr) + , mFrameSize(0) + , mFramePos(0) + , mNextPts(0.0) + , mSwr(nullptr) + , mOutputSampleFormat(AV_SAMPLE_FMT_NONE) +#if OPENMW_FFMPEG_5_OR_GREATER + , mOutputChannelLayout({}) +#else + , mOutputChannelLayout(0) +#endif + , mDataBuf(nullptr) + , mFrameData(nullptr) + , mDataBufLen(0) { - size_t got = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) * - av_get_bytes_per_sample(mOutputSampleFormat); - const char *inbuf = reinterpret_cast(mFrameData[0]); - output.insert(output.end(), inbuf, inbuf+got); - } -} + memset(&mPacket, 0, sizeof(mPacket)); -size_t FFmpeg_Decoder::getSampleOffset() -{ - int delay = (mFrameSize-mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) / - av_get_bytes_per_sample(mOutputSampleFormat); - return (int)(mNextPts*mCodecCtx->sample_rate) - delay; -} - -FFmpeg_Decoder::FFmpeg_Decoder(const VFS::Manager* vfs) - : Sound_Decoder(vfs) - , mFormatCtx(nullptr) - , mCodecCtx(nullptr) - , mStream(nullptr) - , mFrame(nullptr) - , mFrameSize(0) - , mFramePos(0) - , mNextPts(0.0) - , mSwr(nullptr) - , mOutputSampleFormat(AV_SAMPLE_FMT_NONE) - , mOutputChannelLayout(0) - , mDataBuf(nullptr) - , mFrameData(nullptr) - , mDataBufLen(0) -{ - memset(&mPacket, 0, sizeof(mPacket)); - - /* We need to make sure ffmpeg is initialized. Optionally silence warning - * output from the lib */ - static bool done_init = false; - if(!done_init) - { + /* We need to make sure ffmpeg is initialized. Optionally silence warning + * output from the lib */ + static bool done_init = false; + if (!done_init) + { // This is not needed anymore above FFMpeg version 4.0 #if LIBAVCODEC_VERSION_INT < 3805796 - av_register_all(); + av_register_all(); #endif - av_log_set_level(AV_LOG_ERROR); - done_init = true; + av_log_set_level(AV_LOG_ERROR); + done_init = true; + } } -} - -FFmpeg_Decoder::~FFmpeg_Decoder() -{ - close(); -} + FFmpeg_Decoder::~FFmpeg_Decoder() + { + close(); + } } diff --git a/apps/openmw/mwsound/ffmpeg_decoder.hpp b/apps/openmw/mwsound/ffmpeg_decoder.hpp index 0a67a47580d..e67b8efbf32 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.hpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.hpp @@ -1,11 +1,14 @@ #ifndef GAME_SOUND_FFMPEG_DECODER_H #define GAME_SOUND_FFMPEG_DECODER_H -#include +#include + +#include +#include #if defined(_MSC_VER) - #pragma warning (push) - #pragma warning (disable : 4244) +#pragma warning(push) +#pragma warning(disable : 4244) #endif extern "C" @@ -21,63 +24,98 @@ extern "C" } #if defined(_MSC_VER) - #pragma warning (pop) +#pragma warning(pop) #endif -#include +#include #include -#include #include "sound_decoder.hpp" - namespace MWSound { + struct AVIOContextDeleter + { + void operator()(AVIOContext* ptr) const; + }; + + using AVIOContextPtr = std::unique_ptr; + + struct AVFormatContextDeleter + { + void operator()(AVFormatContext* ptr) const; + }; + + using AVFormatContextPtr = std::unique_ptr; + + struct AVCodecContextDeleter + { + void operator()(AVCodecContext* ptr) const; + }; + + using AVCodecContextPtr = std::unique_ptr; + + struct AVFrameDeleter + { + void operator()(AVFrame* ptr) const; + }; + + using AVFramePtr = std::unique_ptr; + class FFmpeg_Decoder final : public Sound_Decoder { - AVFormatContext *mFormatCtx; - AVCodecContext *mCodecCtx; - AVStream **mStream; + AVIOContextPtr mIoCtx; + AVFormatContextPtr mFormatCtx; + AVCodecContextPtr mCodecCtx; + AVStream** mStream; AVPacket mPacket; - AVFrame *mFrame; + AVFramePtr mFrame; - int mFrameSize; - int mFramePos; + std::size_t mFrameSize; + std::size_t mFramePos; double mNextPts; - SwrContext *mSwr; + SwrContext* mSwr; enum AVSampleFormat mOutputSampleFormat; +#if OPENMW_FFMPEG_5_OR_GREATER + AVChannelLayout mOutputChannelLayout; +#else int64_t mOutputChannelLayout; - uint8_t *mDataBuf; - uint8_t **mFrameData; +#endif + uint8_t* mDataBuf; + uint8_t** mFrameData; int mDataBufLen; bool getNextPacket(); Files::IStreamPtr mDataStream; - static int readPacket(void *user_data, uint8_t *buf, int buf_size); - static int writePacket(void *user_data, uint8_t *buf, int buf_size); - static int64_t seek(void *user_data, int64_t offset, int whence); + static int readPacket(void* user_data, uint8_t* buf, int buf_size); +#if OPENMW_FFMPEG_CONST_WRITEPACKET + static int writePacket(void* user_data, const uint8_t* buf, int buf_size); +#else + static int writePacket(void* user_data, uint8_t* buf, int buf_size); +#endif + static int64_t seek(void* user_data, int64_t offset, int whence); bool getAVAudioData(); - size_t readAVAudioData(void *data, size_t length); + size_t readAVAudioData(void* data, size_t length); - void open(const std::string &fname) override; + void open(VFS::Path::NormalizedView fname) override; void close() override; std::string getName() override; - void getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) override; + void getInfo(int* samplerate, ChannelConfig* chans, SampleType* type) override; - size_t read(char *buffer, size_t bytes) override; - void readAll(std::vector &output) override; + size_t read(char* buffer, size_t bytes) override; + void readAll(std::vector& output) override; size_t getSampleOffset() override; - FFmpeg_Decoder& operator=(const FFmpeg_Decoder &rhs); - FFmpeg_Decoder(const FFmpeg_Decoder &rhs); + FFmpeg_Decoder& operator=(const FFmpeg_Decoder& rhs); + FFmpeg_Decoder(const FFmpeg_Decoder& rhs); public: explicit FFmpeg_Decoder(const VFS::Manager* vfs); diff --git a/apps/openmw/mwsound/loudness.cpp b/apps/openmw/mwsound/loudness.cpp index ae31d60949b..2a6ac5ac8ef 100644 --- a/apps/openmw/mwsound/loudness.cpp +++ b/apps/openmw/mwsound/loudness.cpp @@ -1,72 +1,68 @@ #include "loudness.hpp" -#include -#include #include - -#include "soundmanagerimp.hpp" +#include +#include +#include namespace MWSound { -void Sound_Loudness::analyzeLoudness(const std::vector< char >& data) -{ - mQueue.insert( mQueue.end(), data.begin(), data.end() ); - if (!mQueue.size()) - return; - - int samplesPerSegment = static_cast(mSampleRate / mSamplesPerSec); - int numSamples = bytesToFrames(mQueue.size(), mChannelConfig, mSampleType); - int advance = framesToBytes(1, mChannelConfig, mSampleType); + void Sound_Loudness::analyzeLoudness(const std::vector& data) + { + mQueue.insert(mQueue.end(), data.begin(), data.end()); + if (!mQueue.size()) + return; + int samplesPerSegment = static_cast(mSampleRate / mSamplesPerSec); + std::size_t numSamples = bytesToFrames(mQueue.size(), mChannelConfig, mSampleType); + std::size_t advance = framesToBytes(1, mChannelConfig, mSampleType); - int segment=0; - int sample=0; - while (segment < numSamples/samplesPerSegment) - { - float sum=0; - int samplesAdded = 0; - while (sample < numSamples && sample < (segment+1)*samplesPerSegment) + std::size_t segment = 0; + std::size_t sample = 0; + while (segment < numSamples / samplesPerSegment) { - // get sample on a scale from -1 to 1 - float value = 0; - if (mSampleType == SampleType_UInt8) - value = ((char)(mQueue[sample*advance]^0x80))/128.f; - else if (mSampleType == SampleType_Int16) - { - value = *reinterpret_cast(&mQueue[sample*advance]); - value /= float(std::numeric_limits::max()); - } - else if (mSampleType == SampleType_Float32) + float sum = 0; + int samplesAdded = 0; + while (sample < numSamples && sample < (segment + 1) * samplesPerSegment) { - value = *reinterpret_cast(&mQueue[sample*advance]); - value = std::max(-1.f, std::min(1.f, value)); // Float samples *should* be scaled to [-1,1] already. + // get sample on a scale from -1 to 1 + float value = 0; + if (mSampleType == SampleType_UInt8) + value = ((char)(mQueue[sample * advance] ^ 0x80)) / 128.f; + else if (mSampleType == SampleType_Int16) + { + value = *reinterpret_cast(&mQueue[sample * advance]); + value /= float(std::numeric_limits::max()); + } + else if (mSampleType == SampleType_Float32) + { + value = *reinterpret_cast(&mQueue[sample * advance]); + value = std::clamp(value, -1.f, 1.f); // Float samples *should* be scaled to [-1,1] already. + } + + sum += value * value; + ++samplesAdded; + ++sample; } - sum += value*value; - ++samplesAdded; - ++sample; + float rms = 0; // root mean square + if (samplesAdded > 0) + rms = std::sqrt(sum / samplesAdded); + mSamples.push_back(rms); + ++segment; } - float rms = 0; // root mean square - if (samplesAdded > 0) - rms = std::sqrt(sum / samplesAdded); - mSamples.push_back(rms); - ++segment; + mQueue.erase(mQueue.begin(), mQueue.begin() + sample * advance); } - mQueue.erase(mQueue.begin(), mQueue.begin() + sample*advance); -} - - -float Sound_Loudness::getLoudnessAtTime(float sec) const -{ - if(mSamplesPerSec <= 0.0f || mSamples.empty() || sec < 0.0f) - return 0.0f; + float Sound_Loudness::getLoudnessAtTime(float sec) const + { + if (mSamplesPerSec <= 0.0f || mSamples.empty() || sec < 0.0f) + return 0.0f; - size_t index = static_cast(sec * mSamplesPerSec); - index = std::max(0, std::min(index, mSamples.size()-1)); - return mSamples[index]; -} + size_t index = std::min(static_cast(sec * mSamplesPerSec), mSamples.size() - 1); + return mSamples[index]; + } } diff --git a/apps/openmw/mwsound/loudness.hpp b/apps/openmw/mwsound/loudness.hpp index 939d83dee30..1800ec246f6 100644 --- a/apps/openmw/mwsound/loudness.hpp +++ b/apps/openmw/mwsound/loudness.hpp @@ -1,56 +1,59 @@ #ifndef GAME_SOUND_LOUDNESS_H #define GAME_SOUND_LOUDNESS_H -#include #include +#include #include "sound_decoder.hpp" namespace MWSound { -class Sound_Loudness { - float mSamplesPerSec; - int mSampleRate; - ChannelConfig mChannelConfig; - SampleType mSampleType; - - // Loudness sample info - std::vector mSamples; - - std::deque mQueue; - -public: - /** - * @param samplesPerSecond How many loudness values per second of audio to compute. - * @param sampleRate the sample rate of the sound buffer - * @param chans channel layout of the buffer - * @param type sample type of the buffer - */ - Sound_Loudness(float samplesPerSecond, int sampleRate, ChannelConfig chans, SampleType type) - : mSamplesPerSec(samplesPerSecond) - , mSampleRate(sampleRate) - , mChannelConfig(chans) - , mSampleType(type) - { } - - /** - * Analyzes the energy (closely related to loudness) of a sound buffer. - * The buffer will be divided into segments according to \a valuesPerSecond, - * and for each segment a loudness value in the range of [0,1] will be computed. - * The computed values are then added to the mSamples vector. This method should be called continuously - * with chunks of audio until the whole audio file is processed. - * If the size of \a data does not exactly fit a number of loudness samples, the remainder - * will be kept in the mQueue and used in the next call to analyzeLoudness. - * @param data the sound buffer to analyze, containing raw samples - */ - void analyzeLoudness(const std::vector& data); - - /** - * Get loudness at a particular time. Before calling this, the stream has to be analyzed up to that point in time (see analyzeLoudness()). - */ - float getLoudnessAtTime(float sec) const; -}; + class Sound_Loudness + { + float mSamplesPerSec; + int mSampleRate; + ChannelConfig mChannelConfig; + SampleType mSampleType; + + // Loudness sample info + std::vector mSamples; + + std::deque mQueue; + + public: + /** + * @param samplesPerSecond How many loudness values per second of audio to compute. + * @param sampleRate the sample rate of the sound buffer + * @param chans channel layout of the buffer + * @param type sample type of the buffer + */ + Sound_Loudness(float samplesPerSecond, int sampleRate, ChannelConfig chans, SampleType type) + : mSamplesPerSec(samplesPerSecond) + , mSampleRate(sampleRate) + , mChannelConfig(chans) + , mSampleType(type) + { + } + + /** + * Analyzes the energy (closely related to loudness) of a sound buffer. + * The buffer will be divided into segments according to \a valuesPerSecond, + * and for each segment a loudness value in the range of [0,1] will be computed. + * The computed values are then added to the mSamples vector. This method should be called continuously + * with chunks of audio until the whole audio file is processed. + * If the size of \a data does not exactly fit a number of loudness samples, the remainder + * will be kept in the mQueue and used in the next call to analyzeLoudness. + * @param data the sound buffer to analyze, containing raw samples + */ + void analyzeLoudness(const std::vector& data); + + /** + * Get loudness at a particular time. Before calling this, the stream has to be analyzed up to that point in + * time (see analyzeLoudness()). + */ + float getLoudnessAtTime(float sec) const; + }; } diff --git a/apps/openmw/mwsound/movieaudiofactory.cpp b/apps/openmw/mwsound/movieaudiofactory.cpp index d8c1c928ee7..962086701ac 100644 --- a/apps/openmw/mwsound/movieaudiofactory.cpp +++ b/apps/openmw/mwsound/movieaudiofactory.cpp @@ -1,13 +1,13 @@ #include "movieaudiofactory.hpp" #include +#include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "sound_decoder.hpp" -#include "sound.hpp" namespace MWSound { @@ -25,28 +25,35 @@ namespace MWSound private: MWSound::MovieAudioDecoder* mDecoder; - void open(const std::string &fname) override; - void close() override; + void open(VFS::Path::NormalizedView fname) override { throw std::runtime_error("Method not implemented"); } + + void close() override {} + std::string getName() override; - void getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) override; - size_t read(char *buffer, size_t bytes) override; + void getInfo(int* samplerate, ChannelConfig* chans, SampleType* type) override; + size_t read(char* buffer, size_t bytes) override; size_t getSampleOffset() override; }; class MovieAudioDecoder : public Video::MovieAudioDecoder { public: - MovieAudioDecoder(Video::VideoState *videoState) - : Video::MovieAudioDecoder(videoState), mAudioTrack(nullptr) + MovieAudioDecoder(Video::VideoState* videoState) + : Video::MovieAudioDecoder(videoState) + , mAudioTrack(nullptr) + , mDecoderBridge(std::make_shared(this)) { - mDecoderBridge.reset(new MWSoundDecoderBridge(this)); } size_t getSampleOffset() { - ssize_t clock_delay = (mFrameSize-mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) / - av_get_bytes_per_sample(mOutputSampleFormat); - return (size_t)(mAudioClock*mAudioContext->sample_rate) - clock_delay; +#if OPENMW_FFMPEG_5_OR_GREATER + ssize_t clock_delay = (mFrameSize - mFramePos) / mOutputChannelLayout.nb_channels +#else + ssize_t clock_delay = (mFrameSize - mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) +#endif + / av_get_bytes_per_sample(mOutputSampleFormat); + return (size_t)(mAudioClock * mAudioContext->sample_rate) - clock_delay; } std::string getStreamName() @@ -59,55 +66,48 @@ namespace MWSound double getAudioClock() override { - return (double)getSampleOffset()/(double)mAudioContext->sample_rate - - MWBase::Environment::get().getSoundManager()->getTrackTimeDelay(mAudioTrack); + return (double)getSampleOffset() / (double)mAudioContext->sample_rate + - MWBase::Environment::get().getSoundManager()->getTrackTimeDelay(mAudioTrack); } void adjustAudioSettings(AVSampleFormat& sampleFormat, uint64_t& channelLayout, int& sampleRate) override { if (sampleFormat == AV_SAMPLE_FMT_U8P || sampleFormat == AV_SAMPLE_FMT_U8) sampleFormat = AV_SAMPLE_FMT_U8; - else if (sampleFormat == AV_SAMPLE_FMT_S16P || sampleFormat == AV_SAMPLE_FMT_S16) - sampleFormat = AV_SAMPLE_FMT_S16; - else if (sampleFormat == AV_SAMPLE_FMT_FLTP || sampleFormat == AV_SAMPLE_FMT_FLT) - sampleFormat = AV_SAMPLE_FMT_S16; // FIXME: check for AL_EXT_FLOAT32 support + // else if (sampleFormat == AV_SAMPLE_FMT_S16P || sampleFormat == AV_SAMPLE_FMT_S16) + // sampleFormat = AV_SAMPLE_FMT_S16; + // FIXME: check for AL_EXT_FLOAT32 support + // else if (sampleFormat == AV_SAMPLE_FMT_FLTP || sampleFormat == AV_SAMPLE_FMT_FLT) + // sampleFormat = AV_SAMPLE_FMT_S16; else sampleFormat = AV_SAMPLE_FMT_S16; if (channelLayout == AV_CH_LAYOUT_5POINT1 || channelLayout == AV_CH_LAYOUT_7POINT1 - || channelLayout == AV_CH_LAYOUT_QUAD) // FIXME: check for AL_EXT_MCFORMATS support + || channelLayout == AV_CH_LAYOUT_QUAD) // FIXME: check for AL_EXT_MCFORMATS support channelLayout = AV_CH_LAYOUT_STEREO; - else if (channelLayout != AV_CH_LAYOUT_MONO - && channelLayout != AV_CH_LAYOUT_STEREO) + else if (channelLayout != AV_CH_LAYOUT_MONO && channelLayout != AV_CH_LAYOUT_STEREO) channelLayout = AV_CH_LAYOUT_STEREO; } public: ~MovieAudioDecoder() { - if(mAudioTrack) + if (mAudioTrack) MWBase::Environment::get().getSoundManager()->stopTrack(mAudioTrack); mAudioTrack = nullptr; mDecoderBridge.reset(); } - MWBase::SoundStream *mAudioTrack; + MWBase::SoundStream* mAudioTrack; std::shared_ptr mDecoderBridge; }; - - void MWSoundDecoderBridge::open(const std::string &fname) - { - throw std::runtime_error("Method not implemented"); - } - void MWSoundDecoderBridge::close() {} - std::string MWSoundDecoderBridge::getName() { return mDecoder->getStreamName(); } - void MWSoundDecoderBridge::getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) + void MWSoundDecoderBridge::getInfo(int* samplerate, ChannelConfig* chans, SampleType* type) { *samplerate = mDecoder->getOutputSampleRate(); @@ -123,8 +123,7 @@ namespace MWSound else if (outputChannelLayout == AV_CH_LAYOUT_QUAD) *chans = ChannelConfig_Quad; else - throw std::runtime_error("Unsupported channel layout: "+ - std::to_string(outputChannelLayout)); + throw std::runtime_error("Unsupported channel layout: " + std::to_string(outputChannelLayout)); AVSampleFormat outputSampleFormat = mDecoder->getOutputSampleFormat(); if (outputSampleFormat == AV_SAMPLE_FMT_U8) @@ -137,11 +136,11 @@ namespace MWSound { char str[1024]; av_get_sample_fmt_string(str, sizeof(str), outputSampleFormat); - throw std::runtime_error(std::string("Unsupported sample format: ")+str); + throw std::runtime_error(std::string("Unsupported sample format: ") + str); } } - size_t MWSoundDecoderBridge::read(char *buffer, size_t bytes) + size_t MWSoundDecoderBridge::read(char* buffer, size_t bytes) { return mDecoder->read(buffer, bytes); } @@ -151,15 +150,13 @@ namespace MWSound return mDecoder->getSampleOffset(); } - - - std::shared_ptr MovieAudioFactory::createDecoder(Video::VideoState* videoState) + std::unique_ptr MovieAudioFactory::createDecoder(Video::VideoState* videoState) { - std::shared_ptr decoder(new MWSound::MovieAudioDecoder(videoState)); + auto decoder = std::make_unique(videoState); decoder->setupFormat(); - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - MWBase::SoundStream *sound = sndMgr->playTrack(decoder->mDecoderBridge, MWSound::Type::Movie); + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + MWBase::SoundStream* sound = sndMgr->playTrack(decoder->mDecoderBridge, MWSound::Type::Movie); if (!sound) { decoder.reset(); diff --git a/apps/openmw/mwsound/movieaudiofactory.hpp b/apps/openmw/mwsound/movieaudiofactory.hpp index 63b8fd7e90c..0af1066af50 100644 --- a/apps/openmw/mwsound/movieaudiofactory.hpp +++ b/apps/openmw/mwsound/movieaudiofactory.hpp @@ -8,7 +8,7 @@ namespace MWSound class MovieAudioFactory : public Video::MovieAudioFactory { - std::shared_ptr createDecoder(Video::VideoState* videoState) override; + std::unique_ptr createDecoder(Video::VideoState* videoState) override; }; } diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index 67b52309d5a..822c4678130 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -1,25 +1,27 @@ #include -#include -#include -#include #include #include +#include #include +#include +#include +#include #include -#include -#include +#include -#include +#include #include #include +#include +#include #include +#include "loudness.hpp" #include "openal_output.hpp" -#include "sound_decoder.hpp" #include "sound.hpp" +#include "sound_decoder.hpp" #include "soundmanagerimp.hpp" -#include "loudness.hpp" #include "efx-presets.h" @@ -27,1502 +29,1585 @@ #define ALC_ALL_DEVICES_SPECIFIER 0x1013 #endif - #define MAKE_PTRID(id) ((void*)(uintptr_t)id) #define GET_PTRID(ptr) ((ALuint)(uintptr_t)ptr) namespace { -const int sLoudnessFPS = 20; // loudness values per second of audio + const int sLoudnessFPS = 20; // loudness values per second of audio -ALCenum checkALCError(ALCdevice *device, const char *func, int line) -{ - ALCenum err = alcGetError(device); - if(err != ALC_NO_ERROR) - Log(Debug::Error) << "ALC error "<< alcGetString(device, err) << " (" << err << ") @ " << func << ":" << line; - return err; -} + ALCenum checkALCError(ALCdevice* device, const char* func, int line) + { + ALCenum err = alcGetError(device); + if (err != ALC_NO_ERROR) + Log(Debug::Error) << "ALC error " << alcGetString(device, err) << " (" << err << ") @ " << func << ":" + << line; + return err; + } #define getALCError(d) checkALCError((d), __FUNCTION__, __LINE__) -ALenum checkALError(const char *func, int line) -{ - ALenum err = alGetError(); - if(err != AL_NO_ERROR) - Log(Debug::Error) << "AL error " << alGetString(err) << " (" << err << ") @ " << func << ":" << line; - return err; -} + ALenum checkALError(const char* func, int line) + { + ALenum err = alGetError(); + if (err != AL_NO_ERROR) + Log(Debug::Error) << "AL error " << alGetString(err) << " (" << err << ") @ " << func << ":" << line; + return err; + } #define getALError() checkALError(__FUNCTION__, __LINE__) -// Helper to get an OpenAL extension function -template -void convertPointer(T& dest, R src) -{ - memcpy(&dest, &src, sizeof(src)); -} - -template -void getALCFunc(T& func, ALCdevice *device, const char *name) -{ - void* funcPtr = alcGetProcAddress(device, name); - convertPointer(func, funcPtr); -} - -template -void getALFunc(T& func, const char *name) -{ - void* funcPtr = alGetProcAddress(name); - convertPointer(func, funcPtr); -} + // Helper to get an OpenAL extension function + template + void convertPointer(T& dest, R src) + { + memcpy(&dest, &src, sizeof(src)); + } -// Effect objects -LPALGENEFFECTS alGenEffects; -LPALDELETEEFFECTS alDeleteEffects; -LPALISEFFECT alIsEffect; -LPALEFFECTI alEffecti; -LPALEFFECTIV alEffectiv; -LPALEFFECTF alEffectf; -LPALEFFECTFV alEffectfv; -LPALGETEFFECTI alGetEffecti; -LPALGETEFFECTIV alGetEffectiv; -LPALGETEFFECTF alGetEffectf; -LPALGETEFFECTFV alGetEffectfv; -// Filter objects -LPALGENFILTERS alGenFilters; -LPALDELETEFILTERS alDeleteFilters; -LPALISFILTER alIsFilter; -LPALFILTERI alFilteri; -LPALFILTERIV alFilteriv; -LPALFILTERF alFilterf; -LPALFILTERFV alFilterfv; -LPALGETFILTERI alGetFilteri; -LPALGETFILTERIV alGetFilteriv; -LPALGETFILTERF alGetFilterf; -LPALGETFILTERFV alGetFilterfv; -// Auxiliary slot objects -LPALGENAUXILIARYEFFECTSLOTS alGenAuxiliaryEffectSlots; -LPALDELETEAUXILIARYEFFECTSLOTS alDeleteAuxiliaryEffectSlots; -LPALISAUXILIARYEFFECTSLOT alIsAuxiliaryEffectSlot; -LPALAUXILIARYEFFECTSLOTI alAuxiliaryEffectSloti; -LPALAUXILIARYEFFECTSLOTIV alAuxiliaryEffectSlotiv; -LPALAUXILIARYEFFECTSLOTF alAuxiliaryEffectSlotf; -LPALAUXILIARYEFFECTSLOTFV alAuxiliaryEffectSlotfv; -LPALGETAUXILIARYEFFECTSLOTI alGetAuxiliaryEffectSloti; -LPALGETAUXILIARYEFFECTSLOTIV alGetAuxiliaryEffectSlotiv; -LPALGETAUXILIARYEFFECTSLOTF alGetAuxiliaryEffectSlotf; -LPALGETAUXILIARYEFFECTSLOTFV alGetAuxiliaryEffectSlotfv; - - -void LoadEffect(ALuint effect, const EFXEAXREVERBPROPERTIES &props) -{ - ALint type = AL_NONE; - alGetEffecti(effect, AL_EFFECT_TYPE, &type); - if(type == AL_EFFECT_EAXREVERB) + template + void getALCFunc(T& func, ALCdevice* device, const char* name) { - alEffectf(effect, AL_EAXREVERB_DIFFUSION, props.flDiffusion); - alEffectf(effect, AL_EAXREVERB_DENSITY, props.flDensity); - alEffectf(effect, AL_EAXREVERB_GAIN, props.flGain); - alEffectf(effect, AL_EAXREVERB_GAINHF, props.flGainHF); - alEffectf(effect, AL_EAXREVERB_GAINLF, props.flGainLF); - alEffectf(effect, AL_EAXREVERB_DECAY_TIME, props.flDecayTime); - alEffectf(effect, AL_EAXREVERB_DECAY_HFRATIO, props.flDecayHFRatio); - alEffectf(effect, AL_EAXREVERB_DECAY_LFRATIO, props.flDecayLFRatio); - alEffectf(effect, AL_EAXREVERB_REFLECTIONS_GAIN, props.flReflectionsGain); - alEffectf(effect, AL_EAXREVERB_REFLECTIONS_DELAY, props.flReflectionsDelay); - alEffectfv(effect, AL_EAXREVERB_REFLECTIONS_PAN, props.flReflectionsPan); - alEffectf(effect, AL_EAXREVERB_LATE_REVERB_GAIN, props.flLateReverbGain); - alEffectf(effect, AL_EAXREVERB_LATE_REVERB_DELAY, props.flLateReverbDelay); - alEffectfv(effect, AL_EAXREVERB_LATE_REVERB_PAN, props.flLateReverbPan); - alEffectf(effect, AL_EAXREVERB_ECHO_TIME, props.flEchoTime); - alEffectf(effect, AL_EAXREVERB_ECHO_DEPTH, props.flEchoDepth); - alEffectf(effect, AL_EAXREVERB_MODULATION_TIME, props.flModulationTime); - alEffectf(effect, AL_EAXREVERB_MODULATION_DEPTH, props.flModulationDepth); - alEffectf(effect, AL_EAXREVERB_AIR_ABSORPTION_GAINHF, props.flAirAbsorptionGainHF); - alEffectf(effect, AL_EAXREVERB_HFREFERENCE, props.flHFReference); - alEffectf(effect, AL_EAXREVERB_LFREFERENCE, props.flLFReference); - alEffectf(effect, AL_EAXREVERB_ROOM_ROLLOFF_FACTOR, props.flRoomRolloffFactor); - alEffecti(effect, AL_EAXREVERB_DECAY_HFLIMIT, props.iDecayHFLimit ? AL_TRUE : AL_FALSE); + void* funcPtr = alcGetProcAddress(device, name); + convertPointer(func, funcPtr); } - else if(type == AL_EFFECT_REVERB) + + template + void getALFunc(T& func, const char* name) + { + void* funcPtr = alGetProcAddress(name); + convertPointer(func, funcPtr); + } + + // Effect objects + LPALGENEFFECTS alGenEffects; + LPALDELETEEFFECTS alDeleteEffects; + LPALISEFFECT alIsEffect; + LPALEFFECTI alEffecti; + LPALEFFECTIV alEffectiv; + LPALEFFECTF alEffectf; + LPALEFFECTFV alEffectfv; + LPALGETEFFECTI alGetEffecti; + LPALGETEFFECTIV alGetEffectiv; + LPALGETEFFECTF alGetEffectf; + LPALGETEFFECTFV alGetEffectfv; + // Filter objects + LPALGENFILTERS alGenFilters; + LPALDELETEFILTERS alDeleteFilters; + LPALISFILTER alIsFilter; + LPALFILTERI alFilteri; + LPALFILTERIV alFilteriv; + LPALFILTERF alFilterf; + LPALFILTERFV alFilterfv; + LPALGETFILTERI alGetFilteri; + LPALGETFILTERIV alGetFilteriv; + LPALGETFILTERF alGetFilterf; + LPALGETFILTERFV alGetFilterfv; + // Auxiliary slot objects + LPALGENAUXILIARYEFFECTSLOTS alGenAuxiliaryEffectSlots; + LPALDELETEAUXILIARYEFFECTSLOTS alDeleteAuxiliaryEffectSlots; + LPALISAUXILIARYEFFECTSLOT alIsAuxiliaryEffectSlot; + LPALAUXILIARYEFFECTSLOTI alAuxiliaryEffectSloti; + LPALAUXILIARYEFFECTSLOTIV alAuxiliaryEffectSlotiv; + LPALAUXILIARYEFFECTSLOTF alAuxiliaryEffectSlotf; + LPALAUXILIARYEFFECTSLOTFV alAuxiliaryEffectSlotfv; + LPALGETAUXILIARYEFFECTSLOTI alGetAuxiliaryEffectSloti; + LPALGETAUXILIARYEFFECTSLOTIV alGetAuxiliaryEffectSlotiv; + LPALGETAUXILIARYEFFECTSLOTF alGetAuxiliaryEffectSlotf; + LPALGETAUXILIARYEFFECTSLOTFV alGetAuxiliaryEffectSlotfv; + + LPALEVENTCONTROLSOFT alEventControlSOFT; + LPALEVENTCALLBACKSOFT alEventCallbackSOFT; + LPALCREOPENDEVICESOFT alcReopenDeviceSOFT; + + void LoadEffect(ALuint effect, const EFXEAXREVERBPROPERTIES& props) { - alEffectf(effect, AL_REVERB_DIFFUSION, props.flDiffusion); - alEffectf(effect, AL_REVERB_DENSITY, props.flDensity); - alEffectf(effect, AL_REVERB_GAIN, props.flGain); - alEffectf(effect, AL_REVERB_GAINHF, props.flGainHF); - alEffectf(effect, AL_REVERB_DECAY_TIME, props.flDecayTime); - alEffectf(effect, AL_REVERB_DECAY_HFRATIO, props.flDecayHFRatio); - alEffectf(effect, AL_REVERB_REFLECTIONS_GAIN, props.flReflectionsGain); - alEffectf(effect, AL_REVERB_REFLECTIONS_DELAY, props.flReflectionsDelay); - alEffectf(effect, AL_REVERB_LATE_REVERB_GAIN, props.flLateReverbGain); - alEffectf(effect, AL_REVERB_LATE_REVERB_DELAY, props.flLateReverbDelay); - alEffectf(effect, AL_REVERB_AIR_ABSORPTION_GAINHF, props.flAirAbsorptionGainHF); - alEffectf(effect, AL_REVERB_ROOM_ROLLOFF_FACTOR, props.flRoomRolloffFactor); - alEffecti(effect, AL_REVERB_DECAY_HFLIMIT, props.iDecayHFLimit ? AL_TRUE : AL_FALSE); + ALint type = AL_NONE; + alGetEffecti(effect, AL_EFFECT_TYPE, &type); + if (type == AL_EFFECT_EAXREVERB) + { + alEffectf(effect, AL_EAXREVERB_DIFFUSION, props.flDiffusion); + alEffectf(effect, AL_EAXREVERB_DENSITY, props.flDensity); + alEffectf(effect, AL_EAXREVERB_GAIN, props.flGain); + alEffectf(effect, AL_EAXREVERB_GAINHF, props.flGainHF); + alEffectf(effect, AL_EAXREVERB_GAINLF, props.flGainLF); + alEffectf(effect, AL_EAXREVERB_DECAY_TIME, props.flDecayTime); + alEffectf(effect, AL_EAXREVERB_DECAY_HFRATIO, props.flDecayHFRatio); + alEffectf(effect, AL_EAXREVERB_DECAY_LFRATIO, props.flDecayLFRatio); + alEffectf(effect, AL_EAXREVERB_REFLECTIONS_GAIN, props.flReflectionsGain); + alEffectf(effect, AL_EAXREVERB_REFLECTIONS_DELAY, props.flReflectionsDelay); + alEffectfv(effect, AL_EAXREVERB_REFLECTIONS_PAN, props.flReflectionsPan); + alEffectf(effect, AL_EAXREVERB_LATE_REVERB_GAIN, props.flLateReverbGain); + alEffectf(effect, AL_EAXREVERB_LATE_REVERB_DELAY, props.flLateReverbDelay); + alEffectfv(effect, AL_EAXREVERB_LATE_REVERB_PAN, props.flLateReverbPan); + alEffectf(effect, AL_EAXREVERB_ECHO_TIME, props.flEchoTime); + alEffectf(effect, AL_EAXREVERB_ECHO_DEPTH, props.flEchoDepth); + alEffectf(effect, AL_EAXREVERB_MODULATION_TIME, props.flModulationTime); + alEffectf(effect, AL_EAXREVERB_MODULATION_DEPTH, props.flModulationDepth); + alEffectf(effect, AL_EAXREVERB_AIR_ABSORPTION_GAINHF, props.flAirAbsorptionGainHF); + alEffectf(effect, AL_EAXREVERB_HFREFERENCE, props.flHFReference); + alEffectf(effect, AL_EAXREVERB_LFREFERENCE, props.flLFReference); + alEffectf(effect, AL_EAXREVERB_ROOM_ROLLOFF_FACTOR, props.flRoomRolloffFactor); + alEffecti(effect, AL_EAXREVERB_DECAY_HFLIMIT, props.iDecayHFLimit ? AL_TRUE : AL_FALSE); + } + else if (type == AL_EFFECT_REVERB) + { + alEffectf(effect, AL_REVERB_DIFFUSION, props.flDiffusion); + alEffectf(effect, AL_REVERB_DENSITY, props.flDensity); + alEffectf(effect, AL_REVERB_GAIN, props.flGain); + alEffectf(effect, AL_REVERB_GAINHF, props.flGainHF); + alEffectf(effect, AL_REVERB_DECAY_TIME, props.flDecayTime); + alEffectf(effect, AL_REVERB_DECAY_HFRATIO, props.flDecayHFRatio); + alEffectf(effect, AL_REVERB_REFLECTIONS_GAIN, props.flReflectionsGain); + alEffectf(effect, AL_REVERB_REFLECTIONS_DELAY, props.flReflectionsDelay); + alEffectf(effect, AL_REVERB_LATE_REVERB_GAIN, props.flLateReverbGain); + alEffectf(effect, AL_REVERB_LATE_REVERB_DELAY, props.flLateReverbDelay); + alEffectf(effect, AL_REVERB_AIR_ABSORPTION_GAINHF, props.flAirAbsorptionGainHF); + alEffectf(effect, AL_REVERB_ROOM_ROLLOFF_FACTOR, props.flRoomRolloffFactor); + alEffecti(effect, AL_REVERB_DECAY_HFLIMIT, props.iDecayHFLimit ? AL_TRUE : AL_FALSE); + } + getALError(); } - getALError(); -} + std::basic_string_view getDeviceName(ALCdevice* device) + { + const ALCchar* name = nullptr; + if (alcIsExtensionPresent(device, "ALC_ENUMERATE_ALL_EXT")) + name = alcGetString(device, ALC_ALL_DEVICES_SPECIFIER); + if (alcGetError(device) != AL_NO_ERROR || !name) + name = alcGetString(device, ALC_DEVICE_SPECIFIER); + if (name == nullptr) // Prevent assigning nullptr to std::string + return {}; + return name; + } } namespace MWSound { -static ALenum getALFormat(ChannelConfig chans, SampleType type) -{ - struct FormatEntry { - ALenum format; - ChannelConfig chans; - SampleType type; - }; - struct FormatEntryExt { - const char name[32]; - ChannelConfig chans; - SampleType type; - }; - static const std::array fmtlist{{ - { AL_FORMAT_MONO16, ChannelConfig_Mono, SampleType_Int16 }, - { AL_FORMAT_MONO8, ChannelConfig_Mono, SampleType_UInt8 }, - { AL_FORMAT_STEREO16, ChannelConfig_Stereo, SampleType_Int16 }, - { AL_FORMAT_STEREO8, ChannelConfig_Stereo, SampleType_UInt8 }, - }}; - - for(auto &fmt : fmtlist) - { - if(fmt.chans == chans && fmt.type == type) - return fmt.format; - } - - if(alIsExtensionPresent("AL_EXT_MCFORMATS")) + static ALenum getALFormat(ChannelConfig chans, SampleType type) { - static const std::array mcfmtlist{{ - { "AL_FORMAT_QUAD16", ChannelConfig_Quad, SampleType_Int16 }, - { "AL_FORMAT_QUAD8", ChannelConfig_Quad, SampleType_UInt8 }, - { "AL_FORMAT_51CHN16", ChannelConfig_5point1, SampleType_Int16 }, - { "AL_FORMAT_51CHN8", ChannelConfig_5point1, SampleType_UInt8 }, - { "AL_FORMAT_71CHN16", ChannelConfig_7point1, SampleType_Int16 }, - { "AL_FORMAT_71CHN8", ChannelConfig_7point1, SampleType_UInt8 }, - }}; - - for(auto &fmt : mcfmtlist) - { - if(fmt.chans == chans && fmt.type == type) - { - ALenum format = alGetEnumValue(fmt.name); - if(format != 0 && format != -1) - return format; - } + struct FormatEntry + { + ALenum format; + ChannelConfig chans; + SampleType type; + }; + struct FormatEntryExt + { + const char name[32]; + ChannelConfig chans; + SampleType type; + }; + static const std::array fmtlist{ { + { AL_FORMAT_MONO16, ChannelConfig_Mono, SampleType_Int16 }, + { AL_FORMAT_MONO8, ChannelConfig_Mono, SampleType_UInt8 }, + { AL_FORMAT_STEREO16, ChannelConfig_Stereo, SampleType_Int16 }, + { AL_FORMAT_STEREO8, ChannelConfig_Stereo, SampleType_UInt8 }, + } }; + + for (auto& fmt : fmtlist) + { + if (fmt.chans == chans && fmt.type == type) + return fmt.format; } - } - if(alIsExtensionPresent("AL_EXT_FLOAT32")) - { - static const std::array fltfmtlist{{ - { "AL_FORMAT_MONO_FLOAT32", ChannelConfig_Mono, SampleType_Float32 }, - { "AL_FORMAT_STEREO_FLOAT32", ChannelConfig_Stereo, SampleType_Float32 }, - }}; - for(auto &fmt : fltfmtlist) + if (alIsExtensionPresent("AL_EXT_MCFORMATS")) { - if(fmt.chans == chans && fmt.type == type) + static const std::array mcfmtlist{ { + { "AL_FORMAT_QUAD16", ChannelConfig_Quad, SampleType_Int16 }, + { "AL_FORMAT_QUAD8", ChannelConfig_Quad, SampleType_UInt8 }, + { "AL_FORMAT_51CHN16", ChannelConfig_5point1, SampleType_Int16 }, + { "AL_FORMAT_51CHN8", ChannelConfig_5point1, SampleType_UInt8 }, + { "AL_FORMAT_71CHN16", ChannelConfig_7point1, SampleType_Int16 }, + { "AL_FORMAT_71CHN8", ChannelConfig_7point1, SampleType_UInt8 }, + } }; + + for (auto& fmt : mcfmtlist) { - ALenum format = alGetEnumValue(fmt.name); - if(format != 0 && format != -1) - return format; + if (fmt.chans == chans && fmt.type == type) + { + ALenum format = alGetEnumValue(fmt.name); + if (format != 0 && format != -1) + return format; + } } } - - if(alIsExtensionPresent("AL_EXT_MCFORMATS")) + if (alIsExtensionPresent("AL_EXT_FLOAT32")) { - static const std::array fltmcfmtlist{{ - { "AL_FORMAT_QUAD32", ChannelConfig_Quad, SampleType_Float32 }, - { "AL_FORMAT_51CHN32", ChannelConfig_5point1, SampleType_Float32 }, - { "AL_FORMAT_71CHN32", ChannelConfig_7point1, SampleType_Float32 }, - }}; + static const std::array fltfmtlist{ { + { "AL_FORMAT_MONO_FLOAT32", ChannelConfig_Mono, SampleType_Float32 }, + { "AL_FORMAT_STEREO_FLOAT32", ChannelConfig_Stereo, SampleType_Float32 }, + } }; - for(auto &fmt : fltmcfmtlist) + for (auto& fmt : fltfmtlist) { - if(fmt.chans == chans && fmt.type == type) + if (fmt.chans == chans && fmt.type == type) { ALenum format = alGetEnumValue(fmt.name); - if(format != 0 && format != -1) + if (format != 0 && format != -1) return format; } } + + if (alIsExtensionPresent("AL_EXT_MCFORMATS")) + { + static const std::array fltmcfmtlist{ { + { "AL_FORMAT_QUAD32", ChannelConfig_Quad, SampleType_Float32 }, + { "AL_FORMAT_51CHN32", ChannelConfig_5point1, SampleType_Float32 }, + { "AL_FORMAT_71CHN32", ChannelConfig_7point1, SampleType_Float32 }, + } }; + + for (auto& fmt : fltmcfmtlist) + { + if (fmt.chans == chans && fmt.type == type) + { + ALenum format = alGetEnumValue(fmt.name); + if (format != 0 && format != -1) + return format; + } + } + } } - } - Log(Debug::Warning) << "Unsupported sound format (" << getChannelConfigName(chans) << ", " << getSampleTypeName(type) << ")"; - return AL_NONE; -} + Log(Debug::Warning) << "Unsupported sound format (" << getChannelConfigName(chans) << ", " + << getSampleTypeName(type) << ")"; + return AL_NONE; + } + // + // A streaming OpenAL sound. + // + class OpenAL_SoundStream + { + static const ALfloat sBufferLength; -// -// A streaming OpenAL sound. -// -class OpenAL_SoundStream -{ - static const ALfloat sBufferLength; + private: + ALuint mSource; -private: - ALuint mSource; + std::array mBuffers; + ALint mCurrentBufIdx; - std::array mBuffers; - ALint mCurrentBufIdx; + ALenum mFormat; + ALsizei mSampleRate; + ALuint mBufferSize; + ALuint mFrameSize; + ALint mSilence; - ALenum mFormat; - ALsizei mSampleRate; - ALuint mBufferSize; - ALuint mFrameSize; - ALint mSilence; + DecoderPtr mDecoder; - DecoderPtr mDecoder; + std::unique_ptr mLoudnessAnalyzer; - std::unique_ptr mLoudnessAnalyzer; + std::atomic mIsFinished; - std::atomic mIsFinished; + void updateAll(bool local); - void updateAll(bool local); + OpenAL_SoundStream(const OpenAL_SoundStream& rhs); + OpenAL_SoundStream& operator=(const OpenAL_SoundStream& rhs); - OpenAL_SoundStream(const OpenAL_SoundStream &rhs); - OpenAL_SoundStream& operator=(const OpenAL_SoundStream &rhs); + friend class OpenAL_Output; - friend class OpenAL_Output; + public: + OpenAL_SoundStream(ALuint src, DecoderPtr decoder); + ~OpenAL_SoundStream(); -public: - OpenAL_SoundStream(ALuint src, DecoderPtr decoder); - ~OpenAL_SoundStream(); + bool init(bool getLoudnessData = false); - bool init(bool getLoudnessData=false); + bool isPlaying(); + double getStreamDelay() const; + double getStreamOffset() const; - bool isPlaying(); - double getStreamDelay() const; - double getStreamOffset() const; + float getCurrentLoudness() const; - float getCurrentLoudness() const; + bool process(); + ALint refillQueue(); + }; + const ALfloat OpenAL_SoundStream::sBufferLength = 0.125f; - bool process(); - ALint refillQueue(); -}; -const ALfloat OpenAL_SoundStream::sBufferLength = 0.125f; + // + // A background streaming thread (keeps active streams processed) + // + struct OpenAL_Output::StreamThread + { + std::vector mStreams; + std::atomic mQuitNow; + std::mutex mMutex; + std::condition_variable mCondVar; + std::thread mThread; -// -// A background streaming thread (keeps active streams processed) -// -struct OpenAL_Output::StreamThread -{ - typedef std::vector StreamVec; - StreamVec mStreams; + StreamThread() + : mQuitNow(false) + , mThread([this] { run(); }) + { + } + ~StreamThread() + { + mQuitNow = true; + mMutex.lock(); + mMutex.unlock(); + mCondVar.notify_all(); + mThread.join(); + } - std::atomic mQuitNow; - std::mutex mMutex; - std::condition_variable mCondVar; - std::thread mThread; + // thread entry point + void run() + { + std::unique_lock lock(mMutex); + while (!mQuitNow) + { + auto iter = mStreams.begin(); + while (iter != mStreams.end()) + { + if ((*iter)->process() == false) + iter = mStreams.erase(iter); + else + ++iter; + } - StreamThread() - : mQuitNow(false) - , mThread([this] { run(); }) - { - } - ~StreamThread() - { - mQuitNow = true; - mMutex.lock(); mMutex.unlock(); - mCondVar.notify_all(); - mThread.join(); - } + mCondVar.wait_for(lock, std::chrono::milliseconds(50)); + } + } - // thread entry point - void run() - { - std::unique_lock lock(mMutex); - while(!mQuitNow) + void add(OpenAL_SoundStream* stream) { - StreamVec::iterator iter = mStreams.begin(); - while(iter != mStreams.end()) + std::lock_guard lock(mMutex); + if (std::find(mStreams.begin(), mStreams.end(), stream) == mStreams.end()) { - if((*iter)->process() == false) - iter = mStreams.erase(iter); - else - ++iter; + mStreams.push_back(stream); + mCondVar.notify_all(); } + } - mCondVar.wait_for(lock, std::chrono::milliseconds(50)); + void remove(OpenAL_SoundStream* stream) + { + std::lock_guard lock(mMutex); + auto iter = std::find(mStreams.begin(), mStreams.end(), stream); + if (iter != mStreams.end()) + mStreams.erase(iter); } - } - void add(OpenAL_SoundStream *stream) - { - std::lock_guard lock(mMutex); - if(std::find(mStreams.begin(), mStreams.end(), stream) == mStreams.end()) + void removeAll() { - mStreams.push_back(stream); - mCondVar.notify_all(); + std::lock_guard lock(mMutex); + mStreams.clear(); } - } - void remove(OpenAL_SoundStream *stream) - { - std::lock_guard lock(mMutex); - StreamVec::iterator iter = std::find(mStreams.begin(), mStreams.end(), stream); - if(iter != mStreams.end()) mStreams.erase(iter); - } + StreamThread(const StreamThread& rhs) = delete; + StreamThread& operator=(const StreamThread& rhs) = delete; + }; - void removeAll() + class OpenAL_Output::DefaultDeviceThread { - std::lock_guard lock(mMutex); - mStreams.clear(); - } - -private: - StreamThread(const StreamThread &rhs); - StreamThread& operator=(const StreamThread &rhs); -}; + public: + std::basic_string mCurrentName; + private: + OpenAL_Output& mOutput; -OpenAL_SoundStream::OpenAL_SoundStream(ALuint src, DecoderPtr decoder) - : mSource(src), mCurrentBufIdx(0), mFormat(AL_NONE), mSampleRate(0) - , mBufferSize(0), mFrameSize(0), mSilence(0), mDecoder(std::move(decoder)) - , mLoudnessAnalyzer(nullptr), mIsFinished(true) -{ - mBuffers.fill(0); -} + std::atomic mQuitNow; + std::mutex mMutex; + std::condition_variable mCondVar; + std::thread mThread; -OpenAL_SoundStream::~OpenAL_SoundStream() -{ - if(mBuffers[0] && alIsBuffer(mBuffers[0])) - alDeleteBuffers(mBuffers.size(), mBuffers.data()); - alGetError(); + DefaultDeviceThread(const DefaultDeviceThread&) = delete; + DefaultDeviceThread& operator=(const DefaultDeviceThread&) = delete; - mDecoder->close(); -} + void run() + { + Misc::setCurrentThreadIdlePriority(); + std::unique_lock lock(mMutex); + while (!mQuitNow) + { + { + const std::lock_guard openLock(mOutput.mReopenMutex); + auto defaultName = getDeviceName(nullptr); + if (mCurrentName != defaultName) + { + Log(Debug::Info) << "Default audio device changed"; + ALCboolean reopened + = alcReopenDeviceSOFT(mOutput.mDevice, nullptr, mOutput.mContextAttributes.data()); + if (reopened == AL_FALSE) + { + mCurrentName = defaultName; + Log(Debug::Warning) << "Failed to switch to new audio device"; + } + else + mCurrentName = getDeviceName(mOutput.mDevice); + } + } + mCondVar.wait_for(lock, std::chrono::seconds(2)); + } + } -bool OpenAL_SoundStream::init(bool getLoudnessData) -{ - alGenBuffers(mBuffers.size(), mBuffers.data()); - ALenum err = getALError(); - if(err != AL_NO_ERROR) - return false; + public: + DefaultDeviceThread(OpenAL_Output& output, std::basic_string_view name = {}) + : mCurrentName(name) + , mOutput(output) + , mQuitNow(false) + , mThread([this] { run(); }) + { + } - ChannelConfig chans; - SampleType type; + ~DefaultDeviceThread() + { + mQuitNow = true; + mMutex.lock(); + mMutex.unlock(); + mCondVar.notify_all(); + mThread.join(); + } + }; - try { - mDecoder->getInfo(&mSampleRate, &chans, &type); - mFormat = getALFormat(chans, type); - } - catch(std::exception &e) + OpenAL_SoundStream::OpenAL_SoundStream(ALuint src, DecoderPtr decoder) + : mSource(src) + , mCurrentBufIdx(0) + , mFormat(AL_NONE) + , mSampleRate(0) + , mBufferSize(0) + , mFrameSize(0) + , mSilence(0) + , mDecoder(std::move(decoder)) + , mLoudnessAnalyzer(nullptr) + , mIsFinished(true) { - Log(Debug::Error) << "Failed to get stream info: " << e.what(); - return false; + mBuffers.fill(0); } - switch(type) + OpenAL_SoundStream::~OpenAL_SoundStream() { - case SampleType_UInt8: mSilence = 0x80; break; - case SampleType_Int16: mSilence = 0x00; break; - case SampleType_Float32: mSilence = 0x00; break; - } + if (mBuffers[0] && alIsBuffer(mBuffers[0])) + alDeleteBuffers(mBuffers.size(), mBuffers.data()); + alGetError(); - mFrameSize = framesToBytes(1, chans, type); - mBufferSize = static_cast(sBufferLength*mSampleRate); - mBufferSize *= mFrameSize; + mDecoder->close(); + } - if (getLoudnessData) - mLoudnessAnalyzer.reset(new Sound_Loudness(sLoudnessFPS, mSampleRate, chans, type)); + bool OpenAL_SoundStream::init(bool getLoudnessData) + { + alGenBuffers(mBuffers.size(), mBuffers.data()); + ALenum err = getALError(); + if (err != AL_NO_ERROR) + return false; - mIsFinished = false; - return true; -} + ChannelConfig chans; + SampleType type; -bool OpenAL_SoundStream::isPlaying() -{ - ALint state; + try + { + mDecoder->getInfo(&mSampleRate, &chans, &type); + mFormat = getALFormat(chans, type); + } + catch (std::exception& e) + { + Log(Debug::Error) << "Failed to get stream info: " << e.what(); + return false; + } - alGetSourcei(mSource, AL_SOURCE_STATE, &state); - getALError(); + switch (type) + { + case SampleType_UInt8: + mSilence = 0x80; + break; + case SampleType_Int16: + mSilence = 0x00; + break; + case SampleType_Float32: + mSilence = 0x00; + break; + } - if(state == AL_PLAYING || state == AL_PAUSED) - return true; - return !mIsFinished; -} + mFrameSize = framesToBytes(1, chans, type); + mBufferSize = static_cast(sBufferLength * mSampleRate); + mBufferSize *= mFrameSize; -double OpenAL_SoundStream::getStreamDelay() const -{ - ALint state = AL_STOPPED; - double d = 0.0; - ALint offset; + if (getLoudnessData) + mLoudnessAnalyzer = std::make_unique(sLoudnessFPS, mSampleRate, chans, type); - alGetSourcei(mSource, AL_SAMPLE_OFFSET, &offset); - alGetSourcei(mSource, AL_SOURCE_STATE, &state); - if(state == AL_PLAYING || state == AL_PAUSED) - { - ALint queued; - alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued); - ALint inqueue = mBufferSize/mFrameSize*queued - offset; - d = (double)inqueue / (double)mSampleRate; + mIsFinished = false; + return true; } - getALError(); - return d; -} + bool OpenAL_SoundStream::isPlaying() + { + ALint state; -double OpenAL_SoundStream::getStreamOffset() const -{ - ALint state = AL_STOPPED; - ALint offset; - double t; + alGetSourcei(mSource, AL_SOURCE_STATE, &state); + getALError(); - alGetSourcei(mSource, AL_SAMPLE_OFFSET, &offset); - alGetSourcei(mSource, AL_SOURCE_STATE, &state); - if(state == AL_PLAYING || state == AL_PAUSED) - { - ALint queued; - alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued); - ALint inqueue = mBufferSize/mFrameSize*queued - offset; - t = (double)(mDecoder->getSampleOffset() - inqueue) / (double)mSampleRate; + if (state == AL_PLAYING || state == AL_PAUSED) + return true; + return !mIsFinished; } - else + + double OpenAL_SoundStream::getStreamDelay() const { - /* Underrun, or not started yet. The decoder offset is where we'll play - * next. */ - t = (double)mDecoder->getSampleOffset() / (double)mSampleRate; - } + ALint state = AL_STOPPED; + double d = 0.0; + ALint offset; - getALError(); - return t; -} + alGetSourcei(mSource, AL_SAMPLE_OFFSET, &offset); + alGetSourcei(mSource, AL_SOURCE_STATE, &state); + if (state == AL_PLAYING || state == AL_PAUSED) + { + ALint queued; + alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued); + ALint inqueue = mBufferSize / mFrameSize * queued - offset; + d = (double)inqueue / (double)mSampleRate; + } -float OpenAL_SoundStream::getCurrentLoudness() const -{ - if (!mLoudnessAnalyzer.get()) - return 0.f; + getALError(); + return d; + } - float time = getStreamOffset(); - return mLoudnessAnalyzer->getLoudnessAtTime(time); -} + double OpenAL_SoundStream::getStreamOffset() const + { + ALint state = AL_STOPPED; + ALint offset; + double t; -bool OpenAL_SoundStream::process() -{ - try { - if(refillQueue() > 0) + alGetSourcei(mSource, AL_SAMPLE_OFFSET, &offset); + alGetSourcei(mSource, AL_SOURCE_STATE, &state); + if (state == AL_PLAYING || state == AL_PAUSED) { - ALint state; - alGetSourcei(mSource, AL_SOURCE_STATE, &state); - if(state != AL_PLAYING && state != AL_PAUSED) - { - // Ensure all processed buffers are removed so we don't replay them. - refillQueue(); - - alSourcePlay(mSource); - } + ALint queued; + alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued); + ALint inqueue = mBufferSize / mFrameSize * queued - offset; + t = (double)(mDecoder->getSampleOffset() - inqueue) / (double)mSampleRate; } + else + { + /* Underrun, or not started yet. The decoder offset is where we'll play + * next. */ + t = (double)mDecoder->getSampleOffset() / (double)mSampleRate; + } + + getALError(); + return t; } - catch(std::exception&) { - Log(Debug::Error) << "Error updating stream \"" << mDecoder->getName() << "\""; - mIsFinished = true; - } - return !mIsFinished; -} -ALint OpenAL_SoundStream::refillQueue() -{ - ALint processed; - alGetSourcei(mSource, AL_BUFFERS_PROCESSED, &processed); - while(processed > 0) + float OpenAL_SoundStream::getCurrentLoudness() const { - ALuint buf; - alSourceUnqueueBuffers(mSource, 1, &buf); - --processed; + if (!mLoudnessAnalyzer.get()) + return 0.f; + + float time = getStreamOffset(); + return mLoudnessAnalyzer->getLoudnessAtTime(time); } - ALint queued; - alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued); - if(!mIsFinished && (ALuint)queued < mBuffers.size()) + bool OpenAL_SoundStream::process() { - std::vector data(mBufferSize); - for(;!mIsFinished && (ALuint)queued < mBuffers.size();++queued) + try { - size_t got = mDecoder->read(data.data(), data.size()); - if(got < data.size()) + if (refillQueue() > 0) { - mIsFinished = true; - std::fill(data.begin()+got, data.end(), mSilence); - } - if(got > 0) - { - if (mLoudnessAnalyzer.get()) - mLoudnessAnalyzer->analyzeLoudness(data); + ALint state; + alGetSourcei(mSource, AL_SOURCE_STATE, &state); + if (state != AL_PLAYING && state != AL_PAUSED) + { + // Ensure all processed buffers are removed so we don't replay them. + refillQueue(); - ALuint bufid = mBuffers[mCurrentBufIdx]; - alBufferData(bufid, mFormat, data.data(), data.size(), mSampleRate); - alSourceQueueBuffers(mSource, 1, &bufid); - mCurrentBufIdx = (mCurrentBufIdx+1) % mBuffers.size(); + alSourcePlay(mSource); + } } } + catch (const std::exception& e) + { + Log(Debug::Error) << "Error updating stream \"" << mDecoder->getName() << "\": " << e.what(); + mIsFinished = true; + } + return !mIsFinished; } - return queued; -} - - -// -// An OpenAL output device -// -std::vector OpenAL_Output::enumerate() -{ - std::vector devlist; - const ALCchar *devnames; - - if(alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) - devnames = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); - else - devnames = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); - while(devnames && *devnames) - { - devlist.emplace_back(devnames); - devnames += strlen(devnames)+1; - } - return devlist; -} - -bool OpenAL_Output::init(const std::string &devname, const std::string &hrtfname, HrtfMode hrtfmode) -{ - deinit(); - - Log(Debug::Info) << "Initializing OpenAL..."; - - mDevice = alcOpenDevice(devname.c_str()); - if(!mDevice && !devname.empty()) - { - Log(Debug::Warning) << "Failed to open \"" << devname << "\", trying default"; - mDevice = alcOpenDevice(nullptr); - } - - if(!mDevice) - { - Log(Debug::Error) << "Failed to open default audio device"; - return false; - } - - const ALCchar *name = nullptr; - if(alcIsExtensionPresent(mDevice, "ALC_ENUMERATE_ALL_EXT")) - name = alcGetString(mDevice, ALC_ALL_DEVICES_SPECIFIER); - if(alcGetError(mDevice) != AL_NO_ERROR || !name) - name = alcGetString(mDevice, ALC_DEVICE_SPECIFIER); - Log(Debug::Info) << "Opened \"" << name << "\""; - - ALCint major=0, minor=0; - alcGetIntegerv(mDevice, ALC_MAJOR_VERSION, 1, &major); - alcGetIntegerv(mDevice, ALC_MINOR_VERSION, 1, &minor); - Log(Debug::Info) << " ALC Version: " << major << "." << minor <<"\n" << - " ALC Extensions: " << alcGetString(mDevice, ALC_EXTENSIONS); - - ALC.EXT_EFX = alcIsExtensionPresent(mDevice, "ALC_EXT_EFX"); - ALC.SOFT_HRTF = alcIsExtensionPresent(mDevice, "ALC_SOFT_HRTF"); - - std::vector attrs; - attrs.reserve(15); - if(ALC.SOFT_HRTF) + ALint OpenAL_SoundStream::refillQueue() { - LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; - getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); + ALint processed; + alGetSourcei(mSource, AL_BUFFERS_PROCESSED, &processed); + while (processed > 0) + { + ALuint buf; + alSourceUnqueueBuffers(mSource, 1, &buf); + --processed; + } - attrs.push_back(ALC_HRTF_SOFT); - attrs.push_back(hrtfmode == HrtfMode::Disable ? ALC_FALSE : - hrtfmode == HrtfMode::Enable ? ALC_TRUE : - /*hrtfmode == HrtfMode::Auto ?*/ ALC_DONT_CARE_SOFT); - if(!hrtfname.empty()) + ALint queued; + alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued); + if (!mIsFinished && (ALuint)queued < mBuffers.size()) { - ALCint index = -1; - ALCint num_hrtf; - alcGetIntegerv(mDevice, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); - for(ALCint i = 0;i < num_hrtf;++i) + std::vector data(mBufferSize); + for (; !mIsFinished && (ALuint)queued < mBuffers.size(); ++queued) { - const ALCchar *entry = alcGetStringiSOFT(mDevice, ALC_HRTF_SPECIFIER_SOFT, i); - if(hrtfname == entry) + size_t got = mDecoder->read(data.data(), data.size()); + if (got < data.size()) { - index = i; - break; + mIsFinished = true; + std::fill(data.begin() + got, data.end(), mSilence); } - } + if (got > 0) + { + if (mLoudnessAnalyzer.get()) + mLoudnessAnalyzer->analyzeLoudness(data); - if(index < 0) - Log(Debug::Warning) << "Failed to find HRTF \"" << hrtfname << "\", using default"; - else - { - attrs.push_back(ALC_HRTF_ID_SOFT); - attrs.push_back(index); + ALuint bufid = mBuffers[mCurrentBufIdx]; + alBufferData(bufid, mFormat, data.data(), data.size(), mSampleRate); + alSourceQueueBuffers(mSource, 1, &bufid); + mCurrentBufIdx = (mCurrentBufIdx + 1) % mBuffers.size(); + } } } - } - attrs.push_back(0); - mContext = alcCreateContext(mDevice, attrs.data()); - if(!mContext || alcMakeContextCurrent(mContext) == ALC_FALSE) - { - Log(Debug::Error) << "Failed to setup audio context: "< OpenAL_Output::enumerate() { - ALCint hrtf_state; - alcGetIntegerv(mDevice, ALC_HRTF_SOFT, 1, &hrtf_state); - if(!hrtf_state) - Log(Debug::Info) << "HRTF disabled"; + std::vector devlist; + const ALCchar* devnames; + + if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) + devnames = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); else + devnames = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); + while (devnames && *devnames) { - const ALCchar *hrtf = alcGetString(mDevice, ALC_HRTF_SPECIFIER_SOFT); - Log(Debug::Info) << "Enabled HRTF " << hrtf; + devlist.emplace_back(devnames); + devnames += strlen(devnames) + 1; } + return devlist; } - AL.SOFT_source_spatialize = alIsExtensionPresent("AL_SOFT_source_spatialize"); - - ALCuint maxtotal; - ALCint maxmono = 0, maxstereo = 0; - alcGetIntegerv(mDevice, ALC_MONO_SOURCES, 1, &maxmono); - alcGetIntegerv(mDevice, ALC_STEREO_SOURCES, 1, &maxstereo); - if(getALCError(mDevice) != ALC_NO_ERROR) - maxtotal = 256; - else - { - maxtotal = std::min(maxmono+maxstereo, 256); - if (maxtotal == 0) // workaround for broken implementations - maxtotal = 256; - } - for(size_t i = 0;i < maxtotal;i++) - { - ALuint src = 0; - alGenSources(1, &src); - if(alGetError() != AL_NO_ERROR) - break; - mFreeSources.push_back(src); - } - if(mFreeSources.empty()) + void OpenAL_Output::eventCallback( + ALenum eventType, ALuint object, ALuint param, ALsizei length, const ALchar* message, void* userParam) { - Log(Debug::Warning) << "Could not allocate any sound sourcess"; - alcMakeContextCurrent(nullptr); - alcDestroyContext(mContext); - mContext = nullptr; - alcCloseDevice(mDevice); - mDevice = nullptr; - return false; + if (eventType == AL_EVENT_TYPE_DISCONNECTED_SOFT) + static_cast(userParam)->onDisconnect(); } - Log(Debug::Info) << "Allocated " << mFreeSources.size() << " sound sources"; - if(ALC.EXT_EFX) + void OpenAL_Output::onDisconnect() { -#define LOAD_FUNC(x) getALFunc(x, #x) - LOAD_FUNC(alGenEffects); - LOAD_FUNC(alDeleteEffects); - LOAD_FUNC(alIsEffect); - LOAD_FUNC(alEffecti); - LOAD_FUNC(alEffectiv); - LOAD_FUNC(alEffectf); - LOAD_FUNC(alEffectfv); - LOAD_FUNC(alGetEffecti); - LOAD_FUNC(alGetEffectiv); - LOAD_FUNC(alGetEffectf); - LOAD_FUNC(alGetEffectfv); - LOAD_FUNC(alGenFilters); - LOAD_FUNC(alDeleteFilters); - LOAD_FUNC(alIsFilter); - LOAD_FUNC(alFilteri); - LOAD_FUNC(alFilteriv); - LOAD_FUNC(alFilterf); - LOAD_FUNC(alFilterfv); - LOAD_FUNC(alGetFilteri); - LOAD_FUNC(alGetFilteriv); - LOAD_FUNC(alGetFilterf); - LOAD_FUNC(alGetFilterfv); - LOAD_FUNC(alGenAuxiliaryEffectSlots); - LOAD_FUNC(alDeleteAuxiliaryEffectSlots); - LOAD_FUNC(alIsAuxiliaryEffectSlot); - LOAD_FUNC(alAuxiliaryEffectSloti); - LOAD_FUNC(alAuxiliaryEffectSlotiv); - LOAD_FUNC(alAuxiliaryEffectSlotf); - LOAD_FUNC(alAuxiliaryEffectSlotfv); - LOAD_FUNC(alGetAuxiliaryEffectSloti); - LOAD_FUNC(alGetAuxiliaryEffectSlotiv); - LOAD_FUNC(alGetAuxiliaryEffectSlotf); - LOAD_FUNC(alGetAuxiliaryEffectSlotfv); -#undef LOAD_FUNC - if(getALError() != AL_NO_ERROR) + if (!mInitialized || !alcReopenDeviceSOFT) + return; + const std::lock_guard lock(mReopenMutex); + Log(Debug::Warning) << "Audio device disconnected, attempting to reopen..."; + ALCboolean reopened = alcReopenDeviceSOFT(mDevice, mDeviceName.c_str(), mContextAttributes.data()); + if (reopened == AL_FALSE && !mDeviceName.empty()) { - ALC.EXT_EFX = false; - goto skip_efx; + reopened = alcReopenDeviceSOFT(mDevice, nullptr, mContextAttributes.data()); + if (reopened == AL_TRUE && !mDefaultDeviceThread) + mDefaultDeviceThread = std::make_unique(*this); } - - alGenFilters(1, &mWaterFilter); - if(alGetError() == AL_NO_ERROR) + if (reopened == AL_FALSE) + Log(Debug::Error) << "Failed to reopen audio device"; + else { - alFilteri(mWaterFilter, AL_FILTER_TYPE, AL_FILTER_LOWPASS); - if(alGetError() == AL_NO_ERROR) - { - Log(Debug::Info) << "Low-pass filter supported"; - alFilterf(mWaterFilter, AL_LOWPASS_GAIN, 0.9f); - alFilterf(mWaterFilter, AL_LOWPASS_GAINHF, 0.125f); - } - else - { - alDeleteFilters(1, &mWaterFilter); - mWaterFilter = 0; - } - alGetError(); + Log(Debug::Info) << "Reopened audio device"; + if (mDefaultDeviceThread) + mDefaultDeviceThread->mCurrentName = getDeviceName(mDevice); } + } - alGenAuxiliaryEffectSlots(1, &mEffectSlot); - alGetError(); + bool OpenAL_Output::init(const std::string& devname, const std::string& hrtfname, HrtfMode hrtfmode) + { + deinit(); + std::lock_guard lock(mReopenMutex); + + Log(Debug::Info) << "Initializing OpenAL..."; - alGenEffects(1, &mDefaultEffect); - if(alGetError() == AL_NO_ERROR) + mDeviceName = devname; + mDevice = alcOpenDevice(devname.c_str()); + if (!mDevice && !devname.empty()) { - alEffecti(mDefaultEffect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB); - if(alGetError() == AL_NO_ERROR) - Log(Debug::Info) << "EAX Reverb supported"; - else - { - alEffecti(mDefaultEffect, AL_EFFECT_TYPE, AL_EFFECT_REVERB); - if(alGetError() == AL_NO_ERROR) - Log(Debug::Info) << "Standard Reverb supported"; - } - EFXEAXREVERBPROPERTIES props = EFX_REVERB_PRESET_LIVINGROOM; - props.flGain = 0.0f; - LoadEffect(mDefaultEffect, props); + Log(Debug::Warning) << "Failed to open \"" << devname << "\", trying default"; + mDevice = alcOpenDevice(nullptr); + mDeviceName.clear(); } - alGenEffects(1, &mWaterEffect); - if(alGetError() == AL_NO_ERROR) + if (!mDevice) { - alEffecti(mWaterEffect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB); - if(alGetError() != AL_NO_ERROR) - { - alEffecti(mWaterEffect, AL_EFFECT_TYPE, AL_EFFECT_REVERB); - alGetError(); - } - LoadEffect(mWaterEffect, EFX_REVERB_PRESET_UNDERWATER); + Log(Debug::Error) << "Failed to open default audio device"; + return false; } - alListenerf(AL_METERS_PER_UNIT, 1.0f / Constants::UnitsPerMeter); - } -skip_efx: - alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); - // Speed of sound is in units per second. Take the sound speed in air (assumed - // meters per second), multiply by the units per meter to get the speed in u/s. - alSpeedOfSound(Constants::SoundSpeedInAir * Constants::UnitsPerMeter); - alGetError(); - - mInitialized = true; - return true; -} - -void OpenAL_Output::deinit() -{ - mStreamThread->removeAll(); - - for(ALuint source : mFreeSources) - alDeleteSources(1, &source); - mFreeSources.clear(); - - if(mEffectSlot) - alDeleteAuxiliaryEffectSlots(1, &mEffectSlot); - mEffectSlot = 0; - if(mDefaultEffect) - alDeleteEffects(1, &mDefaultEffect); - mDefaultEffect = 0; - if(mWaterEffect) - alDeleteEffects(1, &mWaterEffect); - mWaterEffect = 0; - if(mWaterFilter) - alDeleteFilters(1, &mWaterFilter); - mWaterFilter = 0; - - alcMakeContextCurrent(nullptr); - if(mContext) - alcDestroyContext(mContext); - mContext = nullptr; - if(mDevice) - alcCloseDevice(mDevice); - mDevice = nullptr; - - mInitialized = false; -} + auto name = getDeviceName(mDevice); + Log(Debug::Info) << "Opened \"" << name << "\""; + ALCint major = 0, minor = 0; + alcGetIntegerv(mDevice, ALC_MAJOR_VERSION, 1, &major); + alcGetIntegerv(mDevice, ALC_MINOR_VERSION, 1, &minor); + Log(Debug::Info) << " ALC Version: " << major << "." << minor << "\n" + << " ALC Extensions: " << alcGetString(mDevice, ALC_EXTENSIONS); -std::vector OpenAL_Output::enumerateHrtf() -{ - std::vector ret; + ALC.EXT_EFX = alcIsExtensionPresent(mDevice, "ALC_EXT_EFX"); + ALC.SOFT_HRTF = alcIsExtensionPresent(mDevice, "ALC_SOFT_HRTF"); - if(!mDevice || !ALC.SOFT_HRTF) - return ret; - - LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; - getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); - - ALCint num_hrtf; - alcGetIntegerv(mDevice, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); - ret.reserve(num_hrtf); - for(ALCint i = 0;i < num_hrtf;++i) - { - const ALCchar *entry = alcGetStringiSOFT(mDevice, ALC_HRTF_SPECIFIER_SOFT, i); - ret.emplace_back(entry); - } - - return ret; -} + mContextAttributes.clear(); + mContextAttributes.reserve(15); + if (ALC.SOFT_HRTF) + { + LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; + getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); + + mContextAttributes.push_back(ALC_HRTF_SOFT); + mContextAttributes.push_back(hrtfmode == HrtfMode::Disable ? ALC_FALSE + : hrtfmode == HrtfMode::Enable ? ALC_TRUE + : + /*hrtfmode == HrtfMode::Auto ?*/ ALC_DONT_CARE_SOFT); + if (!hrtfname.empty()) + { + ALCint index = -1; + ALCint num_hrtf; + alcGetIntegerv(mDevice, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); + for (ALCint i = 0; i < num_hrtf; ++i) + { + const ALCchar* entry = alcGetStringiSOFT(mDevice, ALC_HRTF_SPECIFIER_SOFT, i); + if (hrtfname == entry) + { + index = i; + break; + } + } -void OpenAL_Output::setHrtf(const std::string &hrtfname, HrtfMode hrtfmode) -{ - if(!mDevice || !ALC.SOFT_HRTF) - { - Log(Debug::Info) << "HRTF extension not present"; - return; - } + if (index < 0) + Log(Debug::Warning) << "Failed to find HRTF \"" << hrtfname << "\", using default"; + else + { + mContextAttributes.push_back(ALC_HRTF_ID_SOFT); + mContextAttributes.push_back(index); + } + } + } + mContextAttributes.push_back(0); - LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; - getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); + mContext = alcCreateContext(mDevice, mContextAttributes.data()); + if (!mContext || alcMakeContextCurrent(mContext) == ALC_FALSE) + { + Log(Debug::Error) << "Failed to setup audio context: " << alcGetString(mDevice, alcGetError(mDevice)); + if (mContext) + alcDestroyContext(mContext); + mContext = nullptr; + alcCloseDevice(mDevice); + mDevice = nullptr; + return false; + } - LPALCRESETDEVICESOFT alcResetDeviceSOFT = nullptr; - getALCFunc(alcResetDeviceSOFT, mDevice, "alcResetDeviceSOFT"); + Log(Debug::Info) << " Vendor: " << alGetString(AL_VENDOR) << "\n" + << " Renderer: " << alGetString(AL_RENDERER) << "\n" + << " Version: " << alGetString(AL_VERSION) << "\n" + << " Extensions: " << alGetString(AL_EXTENSIONS); - std::vector attrs; - attrs.reserve(15); + if (alIsExtensionPresent("AL_SOFT_events")) + { + getALFunc(alEventControlSOFT, "alEventControlSOFT"); + getALFunc(alEventCallbackSOFT, "alEventCallbackSOFT"); + } + if (alcIsExtensionPresent(mDevice, "ALC_SOFT_reopen_device")) + getALFunc(alcReopenDeviceSOFT, "alcReopenDeviceSOFT"); + if (alEventControlSOFT) + { + static const std::array events{ { AL_EVENT_TYPE_DISCONNECTED_SOFT } }; + alEventControlSOFT(events.size(), events.data(), AL_TRUE); + alEventCallbackSOFT(&OpenAL_Output::eventCallback, this); + } + else + Log(Debug::Warning) << "Cannot detect audio device changes"; + if (mDeviceName.empty() && !name.empty()) + { + // If we opened the default device, switch devices if a new default is selected + if (alcReopenDeviceSOFT) + mDefaultDeviceThread = std::make_unique(*this, name); + else + Log(Debug::Warning) << "Cannot switch audio devices if the default changes"; + } - attrs.push_back(ALC_HRTF_SOFT); - attrs.push_back(hrtfmode == HrtfMode::Disable ? ALC_FALSE : - hrtfmode == HrtfMode::Enable ? ALC_TRUE : - /*hrtfmode == HrtfMode::Auto ?*/ ALC_DONT_CARE_SOFT); - if(!hrtfname.empty()) - { - ALCint index = -1; - ALCint num_hrtf; - alcGetIntegerv(mDevice, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); - for(ALCint i = 0;i < num_hrtf;++i) + if (!ALC.SOFT_HRTF) + Log(Debug::Warning) << "HRTF status unavailable"; + else { - const ALCchar *entry = alcGetStringiSOFT(mDevice, ALC_HRTF_SPECIFIER_SOFT, i); - if(hrtfname == entry) + ALCint hrtf_state; + alcGetIntegerv(mDevice, ALC_HRTF_SOFT, 1, &hrtf_state); + if (!hrtf_state) + Log(Debug::Info) << "HRTF disabled"; + else { - index = i; - break; + const ALCchar* hrtf = alcGetString(mDevice, ALC_HRTF_SPECIFIER_SOFT); + Log(Debug::Info) << "Enabled HRTF " << hrtf; } } - if(index < 0) - Log(Debug::Warning) << "Failed to find HRTF name \"" << hrtfname << "\", using default"; + AL.SOFT_source_spatialize = alIsExtensionPresent("AL_SOFT_source_spatialize"); + + ALCuint maxtotal; + ALCint maxmono = 0, maxstereo = 0; + alcGetIntegerv(mDevice, ALC_MONO_SOURCES, 1, &maxmono); + alcGetIntegerv(mDevice, ALC_STEREO_SOURCES, 1, &maxstereo); + if (getALCError(mDevice) != ALC_NO_ERROR) + maxtotal = 256; else { - attrs.push_back(ALC_HRTF_ID_SOFT); - attrs.push_back(index); + maxtotal = std::min(maxmono + maxstereo, 256); + if (maxtotal == 0) // workaround for broken implementations + maxtotal = 256; } - } - attrs.push_back(0); - alcResetDeviceSOFT(mDevice, attrs.data()); - - ALCint hrtf_state; - alcGetIntegerv(mDevice, ALC_HRTF_SOFT, 1, &hrtf_state); - if(!hrtf_state) - Log(Debug::Info) << "HRTF disabled"; - else - { - const ALCchar *hrtf = alcGetString(mDevice, ALC_HRTF_SPECIFIER_SOFT); - Log(Debug::Info) << "Enabled HRTF " << hrtf; - } -} + for (size_t i = 0; i < maxtotal; i++) + { + ALuint src = 0; + alGenSources(1, &src); + if (alGetError() != AL_NO_ERROR) + break; + mFreeSources.push_back(src); + } + if (mFreeSources.empty()) + { + Log(Debug::Warning) << "Could not allocate any sound sourcess"; + alcMakeContextCurrent(nullptr); + alcDestroyContext(mContext); + mContext = nullptr; + alcCloseDevice(mDevice); + mDevice = nullptr; + return false; + } + Log(Debug::Info) << "Allocated " << mFreeSources.size() << " sound sources"; + if (ALC.EXT_EFX) + { +#define LOAD_FUNC(x) getALFunc(x, #x) + LOAD_FUNC(alGenEffects); + LOAD_FUNC(alDeleteEffects); + LOAD_FUNC(alIsEffect); + LOAD_FUNC(alEffecti); + LOAD_FUNC(alEffectiv); + LOAD_FUNC(alEffectf); + LOAD_FUNC(alEffectfv); + LOAD_FUNC(alGetEffecti); + LOAD_FUNC(alGetEffectiv); + LOAD_FUNC(alGetEffectf); + LOAD_FUNC(alGetEffectfv); + LOAD_FUNC(alGenFilters); + LOAD_FUNC(alDeleteFilters); + LOAD_FUNC(alIsFilter); + LOAD_FUNC(alFilteri); + LOAD_FUNC(alFilteriv); + LOAD_FUNC(alFilterf); + LOAD_FUNC(alFilterfv); + LOAD_FUNC(alGetFilteri); + LOAD_FUNC(alGetFilteriv); + LOAD_FUNC(alGetFilterf); + LOAD_FUNC(alGetFilterfv); + LOAD_FUNC(alGenAuxiliaryEffectSlots); + LOAD_FUNC(alDeleteAuxiliaryEffectSlots); + LOAD_FUNC(alIsAuxiliaryEffectSlot); + LOAD_FUNC(alAuxiliaryEffectSloti); + LOAD_FUNC(alAuxiliaryEffectSlotiv); + LOAD_FUNC(alAuxiliaryEffectSlotf); + LOAD_FUNC(alAuxiliaryEffectSlotfv); + LOAD_FUNC(alGetAuxiliaryEffectSloti); + LOAD_FUNC(alGetAuxiliaryEffectSlotiv); + LOAD_FUNC(alGetAuxiliaryEffectSlotf); + LOAD_FUNC(alGetAuxiliaryEffectSlotfv); +#undef LOAD_FUNC + if (getALError() != AL_NO_ERROR) + { + ALC.EXT_EFX = false; + goto skip_efx; + } -std::pair OpenAL_Output::loadSound(const std::string &fname) -{ - getALError(); + alGenFilters(1, &mWaterFilter); + if (alGetError() == AL_NO_ERROR) + { + alFilteri(mWaterFilter, AL_FILTER_TYPE, AL_FILTER_LOWPASS); + if (alGetError() == AL_NO_ERROR) + { + Log(Debug::Info) << "Low-pass filter supported"; + alFilterf(mWaterFilter, AL_LOWPASS_GAIN, 0.9f); + alFilterf(mWaterFilter, AL_LOWPASS_GAINHF, 0.125f); + } + else + { + alDeleteFilters(1, &mWaterFilter); + mWaterFilter = 0; + } + alGetError(); + } - std::vector data; - ALenum format = AL_NONE; - int srate = 0; + alGenAuxiliaryEffectSlots(1, &mEffectSlot); + alGetError(); - try - { - DecoderPtr decoder = mManager.getDecoder(); - // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. - if(decoder->mResourceMgr->exists(fname)) - decoder->open(fname); - else - { - std::string file = fname; - std::string::size_type pos = file.rfind('.'); - if(pos != std::string::npos) - file = file.substr(0, pos)+".mp3"; - decoder->open(file); + alGenEffects(1, &mDefaultEffect); + if (alGetError() == AL_NO_ERROR) + { + alEffecti(mDefaultEffect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB); + if (alGetError() == AL_NO_ERROR) + Log(Debug::Info) << "EAX Reverb supported"; + else + { + alEffecti(mDefaultEffect, AL_EFFECT_TYPE, AL_EFFECT_REVERB); + if (alGetError() == AL_NO_ERROR) + Log(Debug::Info) << "Standard Reverb supported"; + } + EFXEAXREVERBPROPERTIES props = EFX_REVERB_PRESET_LIVINGROOM; + props.flGain = 0.0f; + LoadEffect(mDefaultEffect, props); + } + + alGenEffects(1, &mWaterEffect); + if (alGetError() == AL_NO_ERROR) + { + alEffecti(mWaterEffect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB); + if (alGetError() != AL_NO_ERROR) + { + alEffecti(mWaterEffect, AL_EFFECT_TYPE, AL_EFFECT_REVERB); + alGetError(); + } + LoadEffect(mWaterEffect, EFX_REVERB_PRESET_UNDERWATER); + } + + alListenerf(AL_METERS_PER_UNIT, 1.0f / Constants::UnitsPerMeter); } + skip_efx: + alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); + // Speed of sound is in units per second. Take the sound speed in air (assumed + // meters per second), multiply by the units per meter to get the speed in u/s. + alSpeedOfSound(Constants::SoundSpeedInAir * Constants::UnitsPerMeter); + alGetError(); - ChannelConfig chans; - SampleType type; - decoder->getInfo(&srate, &chans, &type); - format = getALFormat(chans, type); - if(format) decoder->readAll(data); + mInitialized = true; + return true; } - catch(std::exception &e) + + void OpenAL_Output::deinit() { - Log(Debug::Error) << "Failed to load audio from " << fname << ": " << e.what(); + mStreamThread->removeAll(); + mDefaultDeviceThread.reset(); + + for (ALuint source : mFreeSources) + alDeleteSources(1, &source); + mFreeSources.clear(); + + if (mEffectSlot) + alDeleteAuxiliaryEffectSlots(1, &mEffectSlot); + mEffectSlot = 0; + if (mDefaultEffect) + alDeleteEffects(1, &mDefaultEffect); + mDefaultEffect = 0; + if (mWaterEffect) + alDeleteEffects(1, &mWaterEffect); + mWaterEffect = 0; + if (mWaterFilter) + alDeleteFilters(1, &mWaterFilter); + mWaterFilter = 0; + + if (alEventCallbackSOFT) + alEventCallbackSOFT(nullptr, nullptr); + + alcMakeContextCurrent(nullptr); + if (mContext) + alcDestroyContext(mContext); + mContext = nullptr; + if (mDevice) + alcCloseDevice(mDevice); + mDevice = nullptr; + + mInitialized = false; } - if(data.empty()) + std::vector OpenAL_Output::enumerateHrtf() { - // If we failed to get any usable audio, substitute with silence. - format = AL_FORMAT_MONO8; - srate = 8000; - data.assign(8000, -128); + std::vector ret; + + if (!mDevice || !ALC.SOFT_HRTF) + return ret; + + LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; + getALCFunc(alcGetStringiSOFT, mDevice, "alcGetStringiSOFT"); + + ALCint num_hrtf; + alcGetIntegerv(mDevice, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); + ret.reserve(num_hrtf); + for (ALCint i = 0; i < num_hrtf; ++i) + { + const ALCchar* entry = alcGetStringiSOFT(mDevice, ALC_HRTF_SPECIFIER_SOFT, i); + ret.emplace_back(entry); + } + + return ret; } - ALint size; - ALuint buf = 0; - alGenBuffers(1, &buf); - alBufferData(buf, format, data.data(), data.size(), srate); - alGetBufferi(buf, AL_SIZE, &size); - if(getALError() != AL_NO_ERROR) + std::pair OpenAL_Output::loadSound(VFS::Path::NormalizedView fname) { - if(buf && alIsBuffer(buf)) - alDeleteBuffers(1, &buf); getALError(); - return std::make_pair(nullptr, 0); - } - return std::make_pair(MAKE_PTRID(buf), size); -} -size_t OpenAL_Output::unloadSound(Sound_Handle data) -{ - ALuint buffer = GET_PTRID(data); - if(!buffer) return 0; + std::vector data; + ALenum format = AL_NONE; + int srate = 0; - // Make sure no sources are playing this buffer before unloading it. - SoundVec::const_iterator iter = mActiveSounds.begin(); - for(;iter != mActiveSounds.end();++iter) - { - if(!(*iter)->mHandle) - continue; + try + { + DecoderPtr decoder = mManager.getDecoder(); + decoder->open(Misc::ResourceHelpers::correctSoundPath(fname, *decoder->mResourceMgr)); + + ChannelConfig chans; + SampleType type; + decoder->getInfo(&srate, &chans, &type); + format = getALFormat(chans, type); + if (format) + decoder->readAll(data); + } + catch (std::exception& e) + { + Log(Debug::Error) << "Failed to load audio from " << fname << ": " << e.what(); + } - ALuint source = GET_PTRID((*iter)->mHandle); - ALint srcbuf; - alGetSourcei(source, AL_BUFFER, &srcbuf); - if((ALuint)srcbuf == buffer) + if (data.empty()) { - alSourceStop(source); - alSourcei(source, AL_BUFFER, 0); + // If we failed to get any usable audio, substitute with silence. + format = AL_FORMAT_MONO8; + srate = 8000; + data.assign(8000, -128); } - } - ALint size = 0; - alGetBufferi(buffer, AL_SIZE, &size); - alDeleteBuffers(1, &buffer); - getALError(); - return size; -} + ALint size; + ALuint buf = 0; + alGenBuffers(1, &buf); + alBufferData(buf, format, data.data(), data.size(), srate); + alGetBufferi(buf, AL_SIZE, &size); + if (getALError() != AL_NO_ERROR) + { + if (buf && alIsBuffer(buf)) + alDeleteBuffers(1, &buf); + getALError(); + return std::make_pair(nullptr, 0); + } + return std::make_pair(MAKE_PTRID(buf), size); + } -void OpenAL_Output::initCommon2D(ALuint source, const osg::Vec3f &pos, ALfloat gain, ALfloat pitch, bool loop, bool useenv) -{ - alSourcef(source, AL_REFERENCE_DISTANCE, 1.0f); - alSourcef(source, AL_MAX_DISTANCE, 1000.0f); - alSourcef(source, AL_ROLLOFF_FACTOR, 0.0f); - alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE); - alSourcei(source, AL_LOOPING, loop ? AL_TRUE : AL_FALSE); - if(AL.SOFT_source_spatialize) - alSourcei(source, AL_SOURCE_SPATIALIZE_SOFT, AL_FALSE); - - if(useenv) + size_t OpenAL_Output::unloadSound(Sound_Handle data) { - if(mWaterFilter) - alSourcei(source, AL_DIRECT_FILTER, - (mListenerEnv == Env_Underwater) ? mWaterFilter : AL_FILTER_NULL - ); - else if(mListenerEnv == Env_Underwater) + ALuint buffer = GET_PTRID(data); + if (!buffer) + return 0; + + // Make sure no sources are playing this buffer before unloading it. + SoundVec::const_iterator iter = mActiveSounds.begin(); + for (; iter != mActiveSounds.end(); ++iter) { - gain *= 0.9f; - pitch *= 0.7f; + if (!(*iter)->mHandle) + continue; + + ALuint source = GET_PTRID((*iter)->mHandle); + ALint srcbuf; + alGetSourcei(source, AL_BUFFER, &srcbuf); + if ((ALuint)srcbuf == buffer) + { + alSourceStop(source); + alSourcei(source, AL_BUFFER, 0); + } } - if(mEffectSlot) - alSource3i(source, AL_AUXILIARY_SEND_FILTER, mEffectSlot, 0, AL_FILTER_NULL); + ALint size = 0; + alGetBufferi(buffer, AL_SIZE, &size); + alDeleteBuffers(1, &buffer); + getALError(); + return size; } - else + + void OpenAL_Output::initCommon2D( + ALuint source, const osg::Vec3f& pos, ALfloat gain, ALfloat pitch, bool loop, bool useenv) { - if(mWaterFilter) - alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL); - if(mEffectSlot) - alSource3i(source, AL_AUXILIARY_SEND_FILTER, AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL); + alSourcef(source, AL_REFERENCE_DISTANCE, 1.0f); + alSourcef(source, AL_MAX_DISTANCE, 1000.0f); + alSourcef(source, AL_ROLLOFF_FACTOR, 0.0f); + alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE); + alSourcei(source, AL_LOOPING, loop ? AL_TRUE : AL_FALSE); + if (AL.SOFT_source_spatialize) + alSourcei(source, AL_SOURCE_SPATIALIZE_SOFT, AL_FALSE); + + if (useenv) + { + if (mWaterFilter) + alSourcei(source, AL_DIRECT_FILTER, (mListenerEnv == Env_Underwater) ? mWaterFilter : AL_FILTER_NULL); + else if (mListenerEnv == Env_Underwater) + { + gain *= 0.9f; + pitch *= 0.7f; + } + if (mEffectSlot) + alSource3i(source, AL_AUXILIARY_SEND_FILTER, mEffectSlot, 0, AL_FILTER_NULL); + } + else + { + if (mWaterFilter) + alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL); + if (mEffectSlot) + alSource3i(source, AL_AUXILIARY_SEND_FILTER, AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL); + } + + alSourcef(source, AL_GAIN, gain); + alSourcef(source, AL_PITCH, pitch); + alSourcefv(source, AL_POSITION, pos.ptr()); + alSource3f(source, AL_DIRECTION, 0.0f, 0.0f, 0.0f); + alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); } - alSourcef(source, AL_GAIN, gain); - alSourcef(source, AL_PITCH, pitch); - alSourcefv(source, AL_POSITION, pos.ptr()); - alSource3f(source, AL_DIRECTION, 0.0f, 0.0f, 0.0f); - alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); -} + void OpenAL_Output::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); + alSourcef(source, AL_MAX_DISTANCE, maxdist); + alSourcef(source, AL_ROLLOFF_FACTOR, 1.0f); + alSourcei(source, AL_SOURCE_RELATIVE, AL_FALSE); + alSourcei(source, AL_LOOPING, loop ? AL_TRUE : AL_FALSE); + if (AL.SOFT_source_spatialize) + alSourcei(source, AL_SOURCE_SPATIALIZE_SOFT, AL_TRUE); + + if ((pos - mListenerPos).length2() > maxdist * maxdist) + gain = 0.0f; + if (useenv) + { + if (mWaterFilter) + alSourcei(source, AL_DIRECT_FILTER, (mListenerEnv == Env_Underwater) ? mWaterFilter : AL_FILTER_NULL); + else if (mListenerEnv == Env_Underwater) + { + gain *= 0.9f; + pitch *= 0.7f; + } + if (mEffectSlot) + alSource3i(source, AL_AUXILIARY_SEND_FILTER, mEffectSlot, 0, AL_FILTER_NULL); + } + else + { + if (mWaterFilter) + alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL); + if (mEffectSlot) + alSource3i(source, AL_AUXILIARY_SEND_FILTER, AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL); + } -void OpenAL_Output::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); - alSourcef(source, AL_MAX_DISTANCE, maxdist); - alSourcef(source, AL_ROLLOFF_FACTOR, 1.0f); - alSourcei(source, AL_SOURCE_RELATIVE, AL_FALSE); - alSourcei(source, AL_LOOPING, loop ? AL_TRUE : AL_FALSE); - if(AL.SOFT_source_spatialize) - alSourcei(source, AL_SOURCE_SPATIALIZE_SOFT, AL_TRUE); - - if((pos - mListenerPos).length2() > maxdist*maxdist) - gain = 0.0f; - if(useenv) + alSourcef(source, AL_GAIN, gain); + alSourcef(source, AL_PITCH, pitch); + alSourcefv(source, AL_POSITION, pos.ptr()); + alSource3f(source, AL_DIRECTION, 0.0f, 0.0f, 0.0f); + alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); + } + + void OpenAL_Output::updateCommon( + ALuint source, const osg::Vec3f& pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv) { - if(mWaterFilter) - alSourcei(source, AL_DIRECT_FILTER, - (mListenerEnv == Env_Underwater) ? mWaterFilter : AL_FILTER_NULL - ); - else if(mListenerEnv == Env_Underwater) + if (useenv && mListenerEnv == Env_Underwater && !mWaterFilter) { gain *= 0.9f; pitch *= 0.7f; } - if(mEffectSlot) - alSource3i(source, AL_AUXILIARY_SEND_FILTER, mEffectSlot, 0, AL_FILTER_NULL); + + alSourcef(source, AL_GAIN, gain); + alSourcef(source, AL_PITCH, pitch); + alSourcefv(source, AL_POSITION, pos.ptr()); + alSource3f(source, AL_DIRECTION, 0.0f, 0.0f, 0.0f); + alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); } - else + + bool OpenAL_Output::playSound(Sound* sound, Sound_Handle data, float offset) { - if(mWaterFilter) - alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL); - if(mEffectSlot) - alSource3i(source, AL_AUXILIARY_SEND_FILTER, AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL); - } + ALuint source; - alSourcef(source, AL_GAIN, gain); - alSourcef(source, AL_PITCH, pitch); - alSourcefv(source, AL_POSITION, pos.ptr()); - alSource3f(source, AL_DIRECTION, 0.0f, 0.0f, 0.0f); - alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); -} + if (mFreeSources.empty()) + { + Log(Debug::Warning) << "No free sources!"; + return false; + } + source = mFreeSources.front(); -void OpenAL_Output::updateCommon(ALuint source, const osg::Vec3f& pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv, bool is3d) -{ - if(is3d) - { - if((pos - mListenerPos).length2() > maxdist*maxdist) - gain = 0.0f; + initCommon2D(source, sound->getPosition(), sound->getRealVolume(), getTimeScaledPitch(sound), + sound->getIsLooping(), sound->getUseEnv()); + alSourcei(source, AL_BUFFER, GET_PTRID(data)); + alSourcef(source, AL_SEC_OFFSET, offset); + if (getALError() != AL_NO_ERROR) + { + alSourceRewind(source); + alSourcei(source, AL_BUFFER, 0); + alGetError(); + return false; + } + + alSourcePlay(source); + if (getALError() != AL_NO_ERROR) + { + alSourceRewind(source); + alSourcei(source, AL_BUFFER, 0); + alGetError(); + return false; + } + + mFreeSources.pop_front(); + sound->mHandle = MAKE_PTRID(source); + mActiveSounds.push_back(sound); + + return true; } - if(useenv && mListenerEnv == Env_Underwater && !mWaterFilter) + + bool OpenAL_Output::playSound3D(Sound* sound, Sound_Handle data, float offset) { - gain *= 0.9f; - pitch *= 0.7f; - } + ALuint source; - alSourcef(source, AL_GAIN, gain); - alSourcef(source, AL_PITCH, pitch); - alSourcefv(source, AL_POSITION, pos.ptr()); - alSource3f(source, AL_DIRECTION, 0.0f, 0.0f, 0.0f); - alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); -} + if (mFreeSources.empty()) + { + Log(Debug::Warning) << "No free sources!"; + return false; + } + source = mFreeSources.front(); + initCommon3D(source, sound->getPosition(), sound->getMinDistance(), sound->getMaxDistance(), + sound->getRealVolume(), getTimeScaledPitch(sound), sound->getIsLooping(), sound->getUseEnv()); + alSourcei(source, AL_BUFFER, GET_PTRID(data)); + alSourcef(source, AL_SEC_OFFSET, offset); + if (getALError() != AL_NO_ERROR) + { + alSourceRewind(source); + alSourcei(source, AL_BUFFER, 0); + alGetError(); + return false; + } -bool OpenAL_Output::playSound(Sound *sound, Sound_Handle data, float offset) -{ - ALuint source; + alSourcePlay(source); + if (getALError() != AL_NO_ERROR) + { + alSourceRewind(source); + alSourcei(source, AL_BUFFER, 0); + alGetError(); + return false; + } - if(mFreeSources.empty()) - { - Log(Debug::Warning) << "No free sources!"; - return false; - } - source = mFreeSources.front(); + mFreeSources.pop_front(); + sound->mHandle = MAKE_PTRID(source); + mActiveSounds.push_back(sound); - initCommon2D(source, sound->getPosition(), sound->getRealVolume(), sound->getPitch(), - sound->getIsLooping(), sound->getUseEnv()); - alSourcei(source, AL_BUFFER, GET_PTRID(data)); - alSourcef(source, AL_SEC_OFFSET, offset); - if(getALError() != AL_NO_ERROR) - { - alSourceRewind(source); - alSourcei(source, AL_BUFFER, 0); - alGetError(); - return false; + return true; } - alSourcePlay(source); - if(getALError() != AL_NO_ERROR) + void OpenAL_Output::finishSound(Sound* sound) { + if (!sound->mHandle) + return; + ALuint source = GET_PTRID(sound->mHandle); + sound->mHandle = nullptr; + + // Rewind the stream to put the source back into an AL_INITIAL state, for + // the next time it's used. alSourceRewind(source); alSourcei(source, AL_BUFFER, 0); - alGetError(); - return false; - } + getALError(); - mFreeSources.pop_front(); - sound->mHandle = MAKE_PTRID(source); - mActiveSounds.push_back(sound); + mFreeSources.push_back(source); + mActiveSounds.erase(std::find(mActiveSounds.begin(), mActiveSounds.end(), sound)); + } - return true; -} + bool OpenAL_Output::isSoundPlaying(Sound* sound) + { + if (!sound->mHandle) + return false; + ALuint source = GET_PTRID(sound->mHandle); + ALint state = AL_STOPPED; -bool OpenAL_Output::playSound3D(Sound *sound, Sound_Handle data, float offset) -{ - ALuint source; + alGetSourcei(source, AL_SOURCE_STATE, &state); + getALError(); - if(mFreeSources.empty()) - { - Log(Debug::Warning) << "No free sources!"; - return false; + return state == AL_PLAYING || state == AL_PAUSED; } - source = mFreeSources.front(); - - initCommon3D(source, sound->getPosition(), sound->getMinDistance(), sound->getMaxDistance(), - sound->getRealVolume(), sound->getPitch(), sound->getIsLooping(), - sound->getUseEnv()); - alSourcei(source, AL_BUFFER, GET_PTRID(data)); - alSourcef(source, AL_SEC_OFFSET, offset); - if(getALError() != AL_NO_ERROR) + + void OpenAL_Output::updateSound(Sound* sound) { - alSourceRewind(source); - alSourcei(source, AL_BUFFER, 0); - alGetError(); - return false; + if (!sound->mHandle) + return; + ALuint source = GET_PTRID(sound->mHandle); + + updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(), + getTimeScaledPitch(sound), sound->getUseEnv()); + getALError(); } - alSourcePlay(source); - if(getALError() != AL_NO_ERROR) + bool OpenAL_Output::streamSound(DecoderPtr decoder, Stream* sound, bool getLoudnessData) { - alSourceRewind(source); - alSourcei(source, AL_BUFFER, 0); - alGetError(); - return false; - } + if (mFreeSources.empty()) + { + Log(Debug::Warning) << "No free sources!"; + return false; + } + ALuint source = mFreeSources.front(); - mFreeSources.pop_front(); - sound->mHandle = MAKE_PTRID(source); - mActiveSounds.push_back(sound); + if (sound->getIsLooping()) + Log(Debug::Warning) << "Warning: cannot loop stream \"" << decoder->getName() << "\""; - return true; -} + initCommon2D( + source, sound->getPosition(), sound->getRealVolume(), getTimeScaledPitch(sound), false, sound->getUseEnv()); + if (getALError() != AL_NO_ERROR) + return false; -void OpenAL_Output::finishSound(Sound *sound) -{ - if(!sound->mHandle) return; - ALuint source = GET_PTRID(sound->mHandle); - sound->mHandle = nullptr; - - // Rewind the stream to put the source back into an AL_INITIAL state, for - // the next time it's used. - alSourceRewind(source); - alSourcei(source, AL_BUFFER, 0); - getALError(); - - mFreeSources.push_back(source); - mActiveSounds.erase(std::find(mActiveSounds.begin(), mActiveSounds.end(), sound)); -} + OpenAL_SoundStream* stream = new OpenAL_SoundStream(source, std::move(decoder)); + if (!stream->init(getLoudnessData)) + { + delete stream; + return false; + } + mStreamThread->add(stream); -bool OpenAL_Output::isSoundPlaying(Sound *sound) -{ - if(!sound->mHandle) return false; - ALuint source = GET_PTRID(sound->mHandle); - ALint state = AL_STOPPED; + mFreeSources.pop_front(); + sound->mHandle = stream; + mActiveStreams.push_back(sound); + return true; + } - alGetSourcei(source, AL_SOURCE_STATE, &state); - getALError(); + bool OpenAL_Output::streamSound3D(DecoderPtr decoder, Stream* sound, bool getLoudnessData) + { + if (mFreeSources.empty()) + { + Log(Debug::Warning) << "No free sources!"; + return false; + } + ALuint source = mFreeSources.front(); - return state == AL_PLAYING || state == AL_PAUSED; -} + if (sound->getIsLooping()) + Log(Debug::Warning) << "Warning: cannot loop stream \"" << decoder->getName() << "\""; -void OpenAL_Output::updateSound(Sound *sound) -{ - if(!sound->mHandle) return; - ALuint source = GET_PTRID(sound->mHandle); + initCommon3D(source, sound->getPosition(), sound->getMinDistance(), sound->getMaxDistance(), + sound->getRealVolume(), getTimeScaledPitch(sound), false, sound->getUseEnv()); + if (getALError() != AL_NO_ERROR) + return false; - updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(), - sound->getPitch(), sound->getUseEnv(), sound->getIs3D()); - getALError(); -} + OpenAL_SoundStream* stream = new OpenAL_SoundStream(source, std::move(decoder)); + if (!stream->init(getLoudnessData)) + { + delete stream; + return false; + } + mStreamThread->add(stream); + mFreeSources.pop_front(); + sound->mHandle = stream; + mActiveStreams.push_back(sound); + return true; + } -bool OpenAL_Output::streamSound(DecoderPtr decoder, Stream *sound, bool getLoudnessData) -{ - if(mFreeSources.empty()) + void OpenAL_Output::finishStream(Stream* sound) { - Log(Debug::Warning) << "No free sources!"; - return false; - } - ALuint source = mFreeSources.front(); + if (!sound->mHandle) + return; + OpenAL_SoundStream* stream = reinterpret_cast(sound->mHandle); + ALuint source = stream->mSource; - if(sound->getIsLooping()) - Log(Debug::Warning) << "Warning: cannot loop stream \"" << decoder->getName() << "\""; + sound->mHandle = nullptr; + mStreamThread->remove(stream); - initCommon2D(source, sound->getPosition(), sound->getRealVolume(), sound->getPitch(), - false, sound->getUseEnv()); - if(getALError() != AL_NO_ERROR) - return false; + // Rewind the stream to put the source back into an AL_INITIAL state, for + // the next time it's used. + alSourceRewind(source); + alSourcei(source, AL_BUFFER, 0); + getALError(); + + mFreeSources.push_back(source); + mActiveStreams.erase(std::find(mActiveStreams.begin(), mActiveStreams.end(), sound)); - OpenAL_SoundStream *stream = new OpenAL_SoundStream(source, std::move(decoder)); - if(!stream->init(getLoudnessData)) - { delete stream; - return false; } - mStreamThread->add(stream); - mFreeSources.pop_front(); - sound->mHandle = stream; - mActiveStreams.push_back(sound); - return true; -} - -bool OpenAL_Output::streamSound3D(DecoderPtr decoder, Stream *sound, bool getLoudnessData) -{ - if(mFreeSources.empty()) + double OpenAL_Output::getStreamDelay(Stream* sound) { - Log(Debug::Warning) << "No free sources!"; - return false; + if (!sound->mHandle) + return 0.0; + OpenAL_SoundStream* stream = reinterpret_cast(sound->mHandle); + return stream->getStreamDelay(); } - ALuint source = mFreeSources.front(); - if(sound->getIsLooping()) - Log(Debug::Warning) << "Warning: cannot loop stream \"" << decoder->getName() << "\""; - - initCommon3D(source, sound->getPosition(), sound->getMinDistance(), sound->getMaxDistance(), - sound->getRealVolume(), sound->getPitch(), false, sound->getUseEnv()); - if(getALError() != AL_NO_ERROR) - return false; - - OpenAL_SoundStream *stream = new OpenAL_SoundStream(source, std::move(decoder)); - if(!stream->init(getLoudnessData)) + double OpenAL_Output::getStreamOffset(Stream* sound) { - delete stream; - return false; + if (!sound->mHandle) + return 0.0; + OpenAL_SoundStream* stream = reinterpret_cast(sound->mHandle); + std::lock_guard lock(mStreamThread->mMutex); + return stream->getStreamOffset(); } - mStreamThread->add(stream); - - mFreeSources.pop_front(); - sound->mHandle = stream; - mActiveStreams.push_back(sound); - return true; -} - -void OpenAL_Output::finishStream(Stream *sound) -{ - if(!sound->mHandle) return; - OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); - ALuint source = stream->mSource; - - sound->mHandle = nullptr; - mStreamThread->remove(stream); - - // Rewind the stream to put the source back into an AL_INITIAL state, for - // the next time it's used. - alSourceRewind(source); - alSourcei(source, AL_BUFFER, 0); - getALError(); - - mFreeSources.push_back(source); - mActiveStreams.erase(std::find(mActiveStreams.begin(), mActiveStreams.end(), sound)); - - delete stream; -} - -double OpenAL_Output::getStreamDelay(Stream *sound) -{ - if(!sound->mHandle) return 0.0; - OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); - return stream->getStreamDelay(); -} -double OpenAL_Output::getStreamOffset(Stream *sound) -{ - if(!sound->mHandle) return 0.0; - OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); - std::lock_guard lock(mStreamThread->mMutex); - return stream->getStreamOffset(); -} - -float OpenAL_Output::getStreamLoudness(Stream *sound) -{ - if(!sound->mHandle) return 0.0; - OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); - std::lock_guard lock(mStreamThread->mMutex); - return stream->getCurrentLoudness(); -} - -bool OpenAL_Output::isStreamPlaying(Stream *sound) -{ - if(!sound->mHandle) return false; - OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); - std::lock_guard lock(mStreamThread->mMutex); - return stream->isPlaying(); -} - -void OpenAL_Output::updateStream(Stream *sound) -{ - if(!sound->mHandle) return; - OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); - ALuint source = stream->mSource; - - updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(), - sound->getPitch(), sound->getUseEnv(), sound->getIs3D()); - getALError(); -} + float OpenAL_Output::getStreamLoudness(Stream* sound) + { + if (!sound->mHandle) + return 0.0; + OpenAL_SoundStream* stream = reinterpret_cast(sound->mHandle); + std::lock_guard lock(mStreamThread->mMutex); + return stream->getCurrentLoudness(); + } + bool OpenAL_Output::isStreamPlaying(Stream* sound) + { + if (!sound->mHandle) + return false; + OpenAL_SoundStream* stream = reinterpret_cast(sound->mHandle); + std::lock_guard lock(mStreamThread->mMutex); + return stream->isPlaying(); + } -void OpenAL_Output::startUpdate() -{ - alcSuspendContext(alcGetCurrentContext()); -} + void OpenAL_Output::updateStream(Stream* sound) + { + if (!sound->mHandle) + return; + OpenAL_SoundStream* stream = reinterpret_cast(sound->mHandle); + ALuint source = stream->mSource; -void OpenAL_Output::finishUpdate() -{ - alcProcessContext(alcGetCurrentContext()); -} + updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(), + getTimeScaledPitch(sound), sound->getUseEnv()); + getALError(); + } + void OpenAL_Output::startUpdate() + { + alcSuspendContext(alcGetCurrentContext()); + } -void OpenAL_Output::updateListener(const osg::Vec3f &pos, const osg::Vec3f &atdir, const osg::Vec3f &updir, Environment env) -{ - if(mContext) + void OpenAL_Output::finishUpdate() { - ALfloat orient[6] = { - atdir.x(), atdir.y(), atdir.z(), - updir.x(), updir.y(), updir.z() - }; - alListenerfv(AL_POSITION, pos.ptr()); - alListenerfv(AL_ORIENTATION, orient); + alcProcessContext(alcGetCurrentContext()); + } - if(env != mListenerEnv) + void OpenAL_Output::updateListener( + const osg::Vec3f& pos, const osg::Vec3f& atdir, const osg::Vec3f& updir, Environment env) + { + if (mContext) { - alSpeedOfSound(((env == Env_Underwater) ? Constants::SoundSpeedUnderwater : Constants::SoundSpeedInAir) * Constants::UnitsPerMeter); + ALfloat orient[6] = { atdir.x(), atdir.y(), atdir.z(), updir.x(), updir.y(), updir.z() }; + alListenerfv(AL_POSITION, pos.ptr()); + alListenerfv(AL_ORIENTATION, orient); - // Update active sources with the environment's direct filter - if(mWaterFilter) + if (env != mListenerEnv) { - ALuint filter = (env == Env_Underwater) ? mWaterFilter : AL_FILTER_NULL; - for(Sound *sound : mActiveSounds) - { - if(sound->getUseEnv()) - alSourcei(GET_PTRID(sound->mHandle), AL_DIRECT_FILTER, filter); - } - for(Stream *sound : mActiveStreams) + alSpeedOfSound(((env == Env_Underwater) ? Constants::SoundSpeedUnderwater : Constants::SoundSpeedInAir) + * Constants::UnitsPerMeter); + + // Update active sources with the environment's direct filter + if (mWaterFilter) { - if(sound->getUseEnv()) - alSourcei( - reinterpret_cast(sound->mHandle)->mSource, - AL_DIRECT_FILTER, filter - ); + ALuint filter = (env == Env_Underwater) ? mWaterFilter : AL_FILTER_NULL; + for (Sound* sound : mActiveSounds) + { + if (sound->getUseEnv()) + alSourcei(GET_PTRID(sound->mHandle), AL_DIRECT_FILTER, filter); + } + for (Stream* sound : mActiveStreams) + { + if (sound->getUseEnv()) + alSourcei(reinterpret_cast(sound->mHandle)->mSource, AL_DIRECT_FILTER, + filter); + } } + // Update the environment effect + if (mEffectSlot) + alAuxiliaryEffectSloti( + mEffectSlot, AL_EFFECTSLOT_EFFECT, (env == Env_Underwater) ? mWaterEffect : mDefaultEffect); } - // Update the environment effect - if(mEffectSlot) - alAuxiliaryEffectSloti(mEffectSlot, AL_EFFECTSLOT_EFFECT, - (env == Env_Underwater) ? mWaterEffect : mDefaultEffect - ); + getALError(); } - getALError(); - } - - mListenerPos = pos; - mListenerEnv = env; -} - -void OpenAL_Output::pauseSounds(int types) -{ - std::vector sources; - for(Sound *sound : mActiveSounds) - { - if((types&sound->getPlayType())) - sources.push_back(GET_PTRID(sound->mHandle)); + mListenerPos = pos; + mListenerEnv = env; } - for(Stream *sound : mActiveStreams) + + void OpenAL_Output::pauseSounds(int types) { - if((types&sound->getPlayType())) + std::vector sources; + for (Sound* sound : mActiveSounds) { - OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); - sources.push_back(stream->mSource); + if ((types & sound->getPlayType())) + sources.push_back(GET_PTRID(sound->mHandle)); + } + for (Stream* sound : mActiveStreams) + { + if ((types & sound->getPlayType())) + { + OpenAL_SoundStream* stream = reinterpret_cast(sound->mHandle); + sources.push_back(stream->mSource); + } + } + if (!sources.empty()) + { + alSourcePausev(sources.size(), sources.data()); + getALError(); } } - if(!sources.empty()) - { - alSourcePausev(sources.size(), sources.data()); - getALError(); - } -} -void OpenAL_Output::pauseActiveDevice() -{ - if (mDevice == nullptr) - return; - - if(alcIsExtensionPresent(mDevice, "ALC_SOFT_PAUSE_DEVICE")) + void OpenAL_Output::pauseActiveDevice() { - LPALCDEVICEPAUSESOFT alcDevicePauseSOFT = nullptr; - getALCFunc(alcDevicePauseSOFT, mDevice, "alcDevicePauseSOFT"); - alcDevicePauseSOFT(mDevice); - getALCError(mDevice); - } + if (mDevice == nullptr) + return; - alListenerf(AL_GAIN, 0.0f); -} + if (alcIsExtensionPresent(mDevice, "ALC_SOFT_PAUSE_DEVICE")) + { + LPALCDEVICEPAUSESOFT alcDevicePauseSOFT = nullptr; + getALCFunc(alcDevicePauseSOFT, mDevice, "alcDevicePauseSOFT"); + alcDevicePauseSOFT(mDevice); + getALCError(mDevice); + } -void OpenAL_Output::resumeActiveDevice() -{ - if (mDevice == nullptr) - return; + alListenerf(AL_GAIN, 0.0f); + } - if(alcIsExtensionPresent(mDevice, "ALC_SOFT_PAUSE_DEVICE")) + void OpenAL_Output::resumeActiveDevice() { - LPALCDEVICERESUMESOFT alcDeviceResumeSOFT = nullptr; - getALCFunc(alcDeviceResumeSOFT, mDevice, "alcDeviceResumeSOFT"); - alcDeviceResumeSOFT(mDevice); - getALCError(mDevice); - } + if (mDevice == nullptr) + return; - alListenerf(AL_GAIN, 1.0f); -} + if (alcIsExtensionPresent(mDevice, "ALC_SOFT_PAUSE_DEVICE")) + { + LPALCDEVICERESUMESOFT alcDeviceResumeSOFT = nullptr; + getALCFunc(alcDeviceResumeSOFT, mDevice, "alcDeviceResumeSOFT"); + alcDeviceResumeSOFT(mDevice); + getALCError(mDevice); + } -void OpenAL_Output::resumeSounds(int types) -{ - std::vector sources; - for(Sound *sound : mActiveSounds) - { - if((types&sound->getPlayType())) - sources.push_back(GET_PTRID(sound->mHandle)); + alListenerf(AL_GAIN, 1.0f); } - for(Stream *sound : mActiveStreams) + + void OpenAL_Output::resumeSounds(int types) { - if((types&sound->getPlayType())) + std::vector sources; + for (Sound* sound : mActiveSounds) + { + if ((types & sound->getPlayType())) + sources.push_back(GET_PTRID(sound->mHandle)); + } + for (Stream* sound : mActiveStreams) + { + if ((types & sound->getPlayType())) + { + OpenAL_SoundStream* stream = reinterpret_cast(sound->mHandle); + sources.push_back(stream->mSource); + } + } + if (!sources.empty()) { - OpenAL_SoundStream *stream = reinterpret_cast(sound->mHandle); - sources.push_back(stream->mSource); + alSourcePlayv(sources.size(), sources.data()); + getALError(); } } - if(!sources.empty()) + + OpenAL_Output::OpenAL_Output(SoundManager& mgr) + : Sound_Output(mgr) + , mDevice(nullptr) + , mContext(nullptr) + , mListenerPos(0.0f, 0.0f, 0.0f) + , mListenerEnv(Env_Normal) + , mWaterFilter(0) + , mWaterEffect(0) + , mDefaultEffect(0) + , mEffectSlot(0) + , mStreamThread(std::make_unique()) { - alSourcePlayv(sources.size(), sources.data()); - getALError(); } -} - -OpenAL_Output::OpenAL_Output(SoundManager &mgr) - : Sound_Output(mgr) - , mDevice(nullptr), mContext(nullptr) - , mListenerPos(0.0f, 0.0f, 0.0f), mListenerEnv(Env_Normal) - , mWaterFilter(0), mWaterEffect(0), mDefaultEffect(0), mEffectSlot(0) - , mStreamThread(new StreamThread) -{ -} + OpenAL_Output::~OpenAL_Output() + { + OpenAL_Output::deinit(); + } -OpenAL_Output::~OpenAL_Output() -{ - OpenAL_Output::deinit(); -} + float OpenAL_Output::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/openal_output.hpp index 2a19e6768a1..b419038eaba 100644 --- a/apps/openmw/mwsound/openal_output.hpp +++ b/apps/openmw/mwsound/openal_output.hpp @@ -1,13 +1,16 @@ #ifndef GAME_SOUND_OPENAL_OUTPUT_H #define GAME_SOUND_OPENAL_OUTPUT_H +#include +#include +#include #include #include -#include -#include -#include "alc.h" +#include + #include "al.h" +#include "alc.h" #include "alext.h" #include "sound_output.hpp" @@ -15,21 +18,24 @@ namespace MWSound { class SoundManager; + class SoundBase; class Sound; class Stream; class OpenAL_Output : public Sound_Output { - ALCdevice *mDevice; - ALCcontext *mContext; + ALCdevice* mDevice; + ALCcontext* mContext; - struct { + struct + { bool EXT_EFX : 1; bool SOFT_HRTF : 1; - } ALC = {false, false}; - struct { + } ALC = { false, false }; + struct + { bool SOFT_source_spatialize : 1; - } AL = {false}; + } AL = { false }; typedef std::deque IDDq; IDDq mFreeSources; @@ -50,44 +56,60 @@ namespace MWSound struct StreamThread; std::unique_ptr mStreamThread; - void initCommon2D(ALuint source, const osg::Vec3f &pos, ALfloat gain, ALfloat pitch, bool loop, bool useenv); - void initCommon3D(ALuint source, const osg::Vec3f &pos, ALfloat mindist, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool loop, bool useenv); + std::string mDeviceName; + std::vector mContextAttributes; + std::mutex mReopenMutex; + + class DefaultDeviceThread; + std::unique_ptr mDefaultDeviceThread; + + void initCommon2D(ALuint source, const osg::Vec3f& pos, ALfloat gain, ALfloat pitch, bool loop, bool useenv); + void initCommon3D(ALuint source, const osg::Vec3f& pos, ALfloat mindist, ALfloat maxdist, ALfloat gain, + ALfloat pitch, bool loop, bool useenv); + + void updateCommon( + ALuint source, const osg::Vec3f& pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv); + + float getTimeScaledPitch(SoundBase* sound); + + OpenAL_Output& operator=(const OpenAL_Output& rhs); + OpenAL_Output(const OpenAL_Output& rhs); - void updateCommon(ALuint source, const osg::Vec3f &pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv, bool is3d); + static void eventCallback( + ALenum eventType, ALuint object, ALuint param, ALsizei length, const ALchar* message, void* userParam); - OpenAL_Output& operator=(const OpenAL_Output &rhs); - OpenAL_Output(const OpenAL_Output &rhs); + void onDisconnect(); public: std::vector enumerate() override; - bool init(const std::string &devname, const std::string &hrtfname, HrtfMode hrtfmode) override; + bool init(const std::string& devname, const std::string& hrtfname, HrtfMode hrtfmode) override; void deinit() override; std::vector enumerateHrtf() override; - void setHrtf(const std::string &hrtfname, HrtfMode hrtfmode) override; - std::pair loadSound(const std::string &fname) override; + std::pair loadSound(VFS::Path::NormalizedView fname) override; size_t unloadSound(Sound_Handle data) override; - bool playSound(Sound *sound, Sound_Handle data, float offset) override; - bool playSound3D(Sound *sound, Sound_Handle data, float offset) override; - void finishSound(Sound *sound) override; - bool isSoundPlaying(Sound *sound) override; - void updateSound(Sound *sound) override; - - bool streamSound(DecoderPtr decoder, Stream *sound, bool getLoudnessData=false) override; - bool streamSound3D(DecoderPtr decoder, Stream *sound, bool getLoudnessData) override; - void finishStream(Stream *sound) override; - double getStreamDelay(Stream *sound) override; - double getStreamOffset(Stream *sound) override; - float getStreamLoudness(Stream *sound) override; - bool isStreamPlaying(Stream *sound) override; - void updateStream(Stream *sound) override; + bool playSound(Sound* sound, Sound_Handle data, float offset) override; + bool playSound3D(Sound* sound, Sound_Handle data, float offset) override; + void finishSound(Sound* sound) override; + bool isSoundPlaying(Sound* sound) override; + void updateSound(Sound* sound) override; + + bool streamSound(DecoderPtr decoder, Stream* sound, bool getLoudnessData = false) override; + bool streamSound3D(DecoderPtr decoder, Stream* sound, bool getLoudnessData) override; + void finishStream(Stream* sound) override; + double getStreamDelay(Stream* sound) override; + double getStreamOffset(Stream* sound) override; + float getStreamLoudness(Stream* sound) override; + bool isStreamPlaying(Stream* sound) override; + void updateStream(Stream* sound) override; void startUpdate() override; void finishUpdate() override; - void updateListener(const osg::Vec3f &pos, const osg::Vec3f &atdir, const osg::Vec3f &updir, Environment env) override; + void updateListener( + const osg::Vec3f& pos, const osg::Vec3f& atdir, const osg::Vec3f& updir, Environment env) override; void pauseSounds(int types) override; void resumeSounds(int types) override; @@ -95,7 +117,7 @@ namespace MWSound void pauseActiveDevice() override; void resumeActiveDevice() override; - OpenAL_Output(SoundManager &mgr); + OpenAL_Output(SoundManager& mgr); virtual ~OpenAL_Output(); }; } diff --git a/apps/openmw/mwsound/regionsoundselector.cpp b/apps/openmw/mwsound/regionsoundselector.cpp index 752c4a26b4d..cb2ece7f8f6 100644 --- a/apps/openmw/mwsound/regionsoundselector.cpp +++ b/apps/openmw/mwsound/regionsoundselector.cpp @@ -1,31 +1,21 @@ #include "regionsoundselector.hpp" +#include #include #include -#include -#include - -#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" namespace MWSound { - namespace + RegionSoundSelector::RegionSoundSelector() + : mMinTimeBetweenSounds(Fallback::Map::getFloat("Weather_Minimum_Time_Between_Environmental_Sounds")) + , mMaxTimeBetweenSounds(Fallback::Map::getFloat("Weather_Maximum_Time_Between_Environmental_Sounds")) { - int addChance(int result, const ESM::Region::SoundRef &v) - { - return result + v.mChance; - } } - RegionSoundSelector::RegionSoundSelector() - : mMinTimeBetweenSounds(Fallback::Map::getFloat("Weather_Minimum_Time_Between_Environmental_Sounds")) - , mMaxTimeBetweenSounds(Fallback::Map::getFloat("Weather_Maximum_Time_Between_Environmental_Sounds")) - {} - - std::optional RegionSoundSelector::getNextRandom(float duration, const std::string& regionName, - const MWBase::World& world) + ESM::RefId RegionSoundSelector::getNextRandom(float duration, const ESM::RefId& regionName) { mTimePassed += duration; @@ -36,40 +26,17 @@ namespace MWSound mTimeToNextEnvSound = mMinTimeBetweenSounds + (mMaxTimeBetweenSounds - mMinTimeBetweenSounds) * a; mTimePassed = 0; - if (mLastRegionName != regionName) - { - mLastRegionName = regionName; - mSumChance = 0; - } - - const ESM::Region* const region = world.getStore().get().search(mLastRegionName); + const ESM::Region* const region + = MWBase::Environment::get().getESMStore()->get().search(regionName); if (region == nullptr) return {}; - if (mSumChance == 0) + for (const ESM::Region::SoundRef& sound : region->mSoundList) { - mSumChance = std::accumulate(region->mSoundList.begin(), region->mSoundList.end(), 0, addChance); - if (mSumChance == 0) - return {}; + if (Misc::Rng::roll0to99() < sound.mChance) + return sound.mSound; } - - const int r = Misc::Rng::rollDice(std::max(mSumChance, 100)); - int pos = 0; - - const auto isSelected = [&] (const ESM::Region::SoundRef& sound) - { - if (r - pos < sound.mChance) - return true; - pos += sound.mChance; - return false; - }; - - const auto it = std::find_if(region->mSoundList.begin(), region->mSoundList.end(), isSelected); - - if (it == region->mSoundList.end()) - return {}; - - return it->mSound; + return {}; } } diff --git a/apps/openmw/mwsound/regionsoundselector.hpp b/apps/openmw/mwsound/regionsoundselector.hpp index 35df8a531b2..474e1afa063 100644 --- a/apps/openmw/mwsound/regionsoundselector.hpp +++ b/apps/openmw/mwsound/regionsoundselector.hpp @@ -1,31 +1,22 @@ #ifndef GAME_SOUND_REGIONSOUNDSELECTOR_H #define GAME_SOUND_REGIONSOUNDSELECTOR_H -#include -#include - -namespace MWBase -{ - class World; -} +#include namespace MWSound { class RegionSoundSelector { - public: - std::optional getNextRandom(float duration, const std::string& regionName, - const MWBase::World& world); + public: + ESM::RefId getNextRandom(float duration, const ESM::RefId& regionName); - RegionSoundSelector(); + RegionSoundSelector(); - private: - float mTimeToNextEnvSound = 0.0f; - int mSumChance = 0; - std::string mLastRegionName; - float mTimePassed = 0.0; - float mMinTimeBetweenSounds; - float mMaxTimeBetweenSounds; + private: + float mTimeToNextEnvSound = 0.0f; + float mTimePassed = 0.0; + float mMinTimeBetweenSounds; + float mMaxTimeBetweenSounds; }; } diff --git a/apps/openmw/mwsound/sound.hpp b/apps/openmw/mwsound/sound.hpp index d2e65c98958..48e89753404 100644 --- a/apps/openmw/mwsound/sound.hpp +++ b/apps/openmw/mwsound/sound.hpp @@ -11,26 +11,39 @@ namespace MWSound enum PlayModeEx { Play_2D = 0, + Play_StopAtFadeEnd = 1 << 28, + Play_FadeExponential = 1 << 29, + Play_InFade = 1 << 30, Play_3D = 1 << 31, + Play_FadeFlagsMask = (Play_StopAtFadeEnd | Play_FadeExponential), }; // For testing individual PlayMode flags - inline int operator&(int a, PlayMode b) { return a & static_cast(b); } - inline int operator&(PlayMode a, PlayMode b) { return static_cast(a) & static_cast(b); } + inline int operator&(int a, PlayMode b) + { + return a & static_cast(b); + } + inline int operator&(PlayMode a, PlayMode b) + { + return static_cast(a) & static_cast(b); + } struct SoundParams { osg::Vec3f mPos; - float mVolume = 1; - float mBaseVolume = 1; - float mPitch = 1; - float mMinDistance = 1; - float mMaxDistance = 1000; + float mVolume = 1.0f; + float mBaseVolume = 1.0f; + float mPitch = 1.0f; + float mMinDistance = 1.0f; + float mMaxDistance = 1000.0f; int mFlags = 0; - float mFadeOutTime = 0; + float mFadeVolume = 1.0f; + float mFadeTarget = 0.0f; + float mFadeStep = 0.0f; }; - class SoundBase { + class SoundBase + { SoundBase& operator=(const SoundBase&) = delete; SoundBase(const SoundBase&) = delete; SoundBase(SoundBase&&) = delete; @@ -43,32 +56,111 @@ namespace MWSound friend class OpenAL_Output; public: - void setPosition(const osg::Vec3f &pos) { mParams.mPos = pos; } + void setPosition(const osg::Vec3f& pos) { mParams.mPos = pos; } void setVolume(float volume) { mParams.mVolume = volume; } void setBaseVolume(float volume) { mParams.mBaseVolume = volume; } - void setFadeout(float duration) { mParams.mFadeOutTime = duration; } - void updateFade(float duration) + void setFadeout(float duration) { setFade(duration, 0.0, Play_StopAtFadeEnd); } + + /// Fade to the given linear gain within the specified amount of time. + /// Note that the fade gain is independent of the sound volume. + /// + /// \param duration specifies the duration of the fade. For *linear* + /// fades (default) this will be exactly the time at which the desired + /// volume is reached. Let v0 be the initial volume, v1 be the target + /// volume, and t0 be the initial time. Then the volume over time is + /// given as + /// + /// v(t) = v0 + (v1 - v0) * (t - t0) / duration if t <= t0 + duration + /// v(t) = v1 if t > t0 + duration + /// + /// For *exponential* fades this determines the time-constant of the + /// exponential process describing the fade. In particular, we guarantee + /// that we reach v0 + 0.99 * (v1 - v0) within the given duration. + /// + /// v(t) = v1 + (v0 - v1) * exp(-4.6 * (t0 - t) / duration) + /// + /// where -4.6 is approximately log(1%) (i.e., -40 dB). + /// + /// This interpolation mode is meant for environmental sound effects to + /// achieve less jarring transitions. + /// + /// \param targetVolume is the linear gain that should be reached at + /// the end of the fade. + /// + /// \param flags may be a combination of Play_FadeExponential and + /// Play_StopAtFadeEnd. If Play_StopAtFadeEnd is set, stops the sound + /// once the fade duration has passed or the target volume has been + /// reached. If Play_FadeExponential is set, enables the exponential + /// fade mode (see above). + void setFade(float duration, float targetVolume, int flags = 0) { - if (mParams.mFadeOutTime > 0.0f) + // Approximation of log(1%) (i.e., -40 dB). + constexpr float minus40Decibel = -4.6f; + + // Do nothing if already at the target, unless we need to trigger a stop event + if ((mParams.mFadeVolume == targetVolume) && !(flags & Play_StopAtFadeEnd)) + return; + + mParams.mFadeTarget = targetVolume; + mParams.mFlags = (mParams.mFlags & ~Play_FadeFlagsMask) | (flags & Play_FadeFlagsMask) | Play_InFade; + if (duration > 0.0f) + { + if (mParams.mFlags & Play_FadeExponential) + mParams.mFadeStep = -minus40Decibel / duration; + else + mParams.mFadeStep = (mParams.mFadeTarget - mParams.mFadeVolume) / duration; + } + else { - float soundDuration = std::min(duration, mParams.mFadeOutTime); - mParams.mVolume *= (mParams.mFadeOutTime - soundDuration) / mParams.mFadeOutTime; - mParams.mFadeOutTime -= soundDuration; + mParams.mFadeVolume = mParams.mFadeTarget; + mParams.mFadeStep = 0.0f; } } - const osg::Vec3f &getPosition() const { return mParams.mPos; } - float getRealVolume() const { return mParams.mVolume * mParams.mBaseVolume; } + /// Updates the internal fading logic. + /// + /// \param dt is the time in seconds since the last call to update. + /// + /// \return true if the sound is still active, false if the sound has + /// reached a fading destination that was marked with Play_StopAtFadeEnd. + bool updateFade(float dt) + { + // Mark fade as done at this volume difference (-80dB when fading to zero) + constexpr float minVolumeDifference = 1e-4f; + + if (!getInFade()) + return true; + + // Perform the actual fade operation + const float deltaBefore = mParams.mFadeTarget - mParams.mFadeVolume; + if (mParams.mFlags & Play_FadeExponential) + mParams.mFadeVolume += mParams.mFadeStep * deltaBefore * dt; + else + mParams.mFadeVolume += mParams.mFadeStep * dt; + const float deltaAfter = mParams.mFadeTarget - mParams.mFadeVolume; + + // Abort fade if we overshot or reached the minimum difference + if ((std::signbit(deltaBefore) != std::signbit(deltaAfter)) || (std::abs(deltaAfter) < minVolumeDifference)) + { + mParams.mFadeVolume = mParams.mFadeTarget; + mParams.mFlags &= ~Play_InFade; + } + + return getInFade() || !(mParams.mFlags & Play_StopAtFadeEnd); + } + + const osg::Vec3f& getPosition() const { return mParams.mPos; } + float getRealVolume() const { return mParams.mVolume * mParams.mBaseVolume * mParams.mFadeVolume; } float getPitch() const { return mParams.mPitch; } float getMinDistance() const { return mParams.mMinDistance; } float getMaxDistance() const { return mParams.mMaxDistance; } - MWSound::Type getPlayType() const - { return static_cast(mParams.mFlags & MWSound::Type::Mask); } + MWSound::Type getPlayType() const { return static_cast(mParams.mFlags & MWSound::Type::Mask); } bool getUseEnv() const { return !(mParams.mFlags & MWSound::PlayMode::NoEnv); } bool getIsLooping() const { return mParams.mFlags & MWSound::PlayMode::Loop; } bool getDistanceCull() const { return mParams.mFlags & MWSound::PlayMode::RemoveAtDistance; } bool getIs3D() const { return mParams.mFlags & Play_3D; } + bool getInFade() const { return mParams.mFlags & Play_InFade; } void init(const SoundParams& params) { @@ -79,22 +171,24 @@ namespace MWSound SoundBase() = default; }; - class Sound : public SoundBase { + class Sound : public SoundBase + { Sound& operator=(const Sound&) = delete; Sound(const Sound&) = delete; Sound(Sound&&) = delete; public: - Sound() { } + Sound() = default; }; - class Stream : public SoundBase { + class Stream : public SoundBase + { Stream& operator=(const Stream&) = delete; Stream(const Stream&) = delete; Stream(Stream&&) = delete; public: - Stream() { } + Stream() = default; }; } diff --git a/apps/openmw/mwsound/sound_buffer.cpp b/apps/openmw/mwsound/sound_buffer.cpp index cb71cb56d2e..f28b268df24 100644 --- a/apps/openmw/mwsound/sound_buffer.cpp +++ b/apps/openmw/mwsound/sound_buffer.cpp @@ -1,12 +1,13 @@ #include "sound_buffer.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include -#include -#include +#include +#include +#include +#include #include #include @@ -23,9 +24,8 @@ namespace MWSound float mAudioMaxDistanceMult; }; - AudioParams makeAudioParams(const MWBase::World& world) + AudioParams makeAudioParams(const MWWorld::Store& settings) { - const auto& settings = world.getStore().get(); AudioParams params; params.mAudioDefaultMinDistance = settings.find("fAudioDefaultMinDistance")->mValue.getFloat(); params.mAudioDefaultMaxDistance = settings.find("fAudioDefaultMaxDistance")->mValue.getFloat(); @@ -35,11 +35,11 @@ namespace MWSound } } - SoundBufferPool::SoundBufferPool(const VFS::Manager& vfs, Sound_Output& output) : - mVfs(&vfs), - mOutput(&output), - mBufferCacheMax(std::max(Settings::Manager::getInt("buffer cache max", "Sound"), 1) * 1024 * 1024), - mBufferCacheMin(std::min(static_cast(std::max(Settings::Manager::getInt("buffer cache min", "Sound"), 1)) * 1024 * 1024, mBufferCacheMax)) + SoundBufferPool::SoundBufferPool(Sound_Output& output) + : mOutput(&output) + , mBufferCacheMax(Settings::sound().mBufferCacheMax * 1024 * 1024) + , mBufferCacheMin( + std::min(static_cast(Settings::sound().mBufferCacheMin) * 1024 * 1024, mBufferCacheMax)) { } @@ -48,7 +48,7 @@ namespace MWSound clear(); } - Sound_Buffer* SoundBufferPool::lookup(const std::string& soundId) const + Sound_Buffer* SoundBufferPool::lookup(const ESM::RefId& soundId) const { const auto it = mBufferNameMap.find(soundId); if (it != mBufferNameMap.end()) @@ -60,12 +60,47 @@ namespace MWSound return nullptr; } - Sound_Buffer* SoundBufferPool::load(const std::string& soundId) + Sound_Buffer* SoundBufferPool::lookup(std::string_view fileName) const + { + const auto it = mBufferFileNameMap.find(std::string(fileName)); + if (it != mBufferFileNameMap.end()) + { + Sound_Buffer* sfx = it->second; + if (sfx->getHandle() != nullptr) + return sfx; + } + return nullptr; + } + + Sound_Buffer* SoundBufferPool::loadSfx(Sound_Buffer* sfx) + { + if (sfx->getHandle() != nullptr) + return sfx; + + auto [handle, size] = mOutput->loadSound(sfx->getResourceName()); + if (handle == nullptr) + return {}; + + sfx->mHandle = handle; + + mBufferCacheSize += size; + if (mBufferCacheSize > mBufferCacheMax) + { + unloadUnused(); + if (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMax) + Log(Debug::Warning) << "No unused sound buffers to free, using " << mBufferCacheSize << " bytes!"; + } + mUnusedBuffers.push_front(sfx); + + return sfx; + } + + Sound_Buffer* SoundBufferPool::load(const ESM::RefId& soundId) { if (mBufferNameMap.empty()) { - for (const ESM::Sound& sound : MWBase::Environment::get().getWorld()->getStore().get()) - insertSound(Misc::StringUtils::lowerCase(sound.mId), sound); + for (const ESM::Sound& sound : MWBase::Environment::get().getESMStore()->get()) + insertSound(sound.mId, sound); } Sound_Buffer* sfx; @@ -74,47 +109,65 @@ namespace MWSound sfx = it->second; else { - const ESM::Sound *sound = MWBase::Environment::get().getWorld()->getStore().get().search(soundId); + const ESM::Sound* sound = MWBase::Environment::get().getESMStore()->get().search(soundId); if (sound == nullptr) return {}; sfx = insertSound(soundId, *sound); } - if (sfx->getHandle() == nullptr) - { - auto [handle, size] = mOutput->loadSound(sfx->getResourceName()); - if (handle == nullptr) - return {}; - - sfx->mHandle = handle; + return loadSfx(sfx); + } - mBufferCacheSize += size; - if (mBufferCacheSize > mBufferCacheMax) - { - unloadUnused(); - if (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMax) - Log(Debug::Warning) << "No unused sound buffers to free, using " << mBufferCacheSize << " bytes!"; - } - mUnusedBuffers.push_front(sfx); + Sound_Buffer* SoundBufferPool::load(std::string_view fileName) + { + Sound_Buffer* sfx; + const auto it = mBufferFileNameMap.find(std::string(fileName)); + if (it != mBufferFileNameMap.end()) + sfx = it->second; + else + { + sfx = insertSound(fileName); } - return sfx; + return loadSfx(sfx); } void SoundBufferPool::clear() { - for (auto &sfx : mSoundBuffers) + for (auto& sfx : mSoundBuffers) { - if(sfx.mHandle) + if (sfx.mHandle) mOutput->unloadSound(sfx.mHandle); sfx.mHandle = nullptr; } + + mBufferFileNameMap.clear(); + mBufferNameMap.clear(); mUnusedBuffers.clear(); } - Sound_Buffer* SoundBufferPool::insertSound(const std::string& soundId, const ESM::Sound& sound) + Sound_Buffer* SoundBufferPool::insertSound(std::string_view fileName) + { + static const AudioParams audioParams + = makeAudioParams(MWBase::Environment::get().getESMStore()->get()); + + float volume = 1.f; + float min = std::max(audioParams.mAudioDefaultMinDistance * audioParams.mAudioMinDistanceMult, 1.f); + float max = std::max(min, audioParams.mAudioDefaultMaxDistance * audioParams.mAudioMaxDistanceMult); + + min = std::max(min, 1.0f); + max = std::max(min, max); + + Sound_Buffer& 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) { - static const AudioParams audioParams = makeAudioParams(*MWBase::Environment::get().getWorld()); + static const AudioParams audioParams + = makeAudioParams(MWBase::Environment::get().getESMStore()->get()); float volume = static_cast(std::pow(10.0, (sound.mData.mVolume / 255.0 * 3348.0 - 3348.0) / 2000.0)); float min = sound.mData.mMinRange; @@ -130,8 +183,8 @@ namespace MWSound min = std::max(min, 1.0f); max = std::max(min, max); - Sound_Buffer& sfx = mSoundBuffers.emplace_back("Sound/" + sound.mSound, volume, min, max); - mVfs->normalizeFilename(sfx.mResourceName); + Sound_Buffer& sfx = mSoundBuffers.emplace_back( + Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(sound.mSound)), volume, min, max); mBufferNameMap.emplace(soundId, &sfx); return &sfx; diff --git a/apps/openmw/mwsound/sound_buffer.hpp b/apps/openmw/mwsound/sound_buffer.hpp index 5c45ac08aa7..7de6dab9aea 100644 --- a/apps/openmw/mwsound/sound_buffer.hpp +++ b/apps/openmw/mwsound/sound_buffer.hpp @@ -2,11 +2,12 @@ #define GAME_SOUND_SOUND_BUFFER_H #include -#include #include +#include #include #include "sound_output.hpp" +#include namespace ESM { @@ -24,82 +25,96 @@ namespace MWSound class Sound_Buffer { - public: - template - Sound_Buffer(T&& resname, float volume, float mindist, float maxdist) - : mResourceName(std::forward(resname)), mVolume(volume), mMinDist(mindist), mMaxDist(maxdist) - {} + public: + template + Sound_Buffer(T&& resname, float volume, float mindist, float maxdist) + : mResourceName(std::forward(resname)) + , mVolume(volume) + , mMinDist(mindist) + , mMaxDist(maxdist) + { + } - const std::string& getResourceName() const noexcept { return mResourceName; } + const VFS::Path::Normalized& getResourceName() const noexcept { return mResourceName; } - Sound_Handle getHandle() const noexcept { return mHandle; } + Sound_Handle getHandle() const noexcept { return mHandle; } - float getVolume() const noexcept { return mVolume; } + float getVolume() const noexcept { return mVolume; } - float getMinDist() const noexcept { return mMinDist; } + float getMinDist() const noexcept { return mMinDist; } - float getMaxDist() const noexcept { return mMaxDist; } + float getMaxDist() const noexcept { return mMaxDist; } - private: - std::string mResourceName; - float mVolume; - float mMinDist; - float mMaxDist; - Sound_Handle mHandle = nullptr; - std::size_t mUses = 0; + private: + VFS::Path::Normalized mResourceName; + float mVolume; + float mMinDist; + float mMaxDist; + Sound_Handle mHandle = nullptr; + std::size_t mUses = 0; - friend class SoundBufferPool; + friend class SoundBufferPool; }; class SoundBufferPool { - public: - SoundBufferPool(const VFS::Manager& vfs, Sound_Output& output); + public: + SoundBufferPool(Sound_Output& output); - SoundBufferPool(const SoundBufferPool&) = delete; + SoundBufferPool(const SoundBufferPool&) = delete; - ~SoundBufferPool(); + ~SoundBufferPool(); - /// Lookup a soundId for its sound data (resource name, local volume, - /// minRange, and maxRange) - Sound_Buffer* lookup(const std::string& soundId) const; + /// Lookup a soundId for its sound data (resource name, local volume, + /// minRange, and maxRange) + Sound_Buffer* lookup(const ESM::RefId& soundId) 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 std::string& soundId); + /// 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; - void use(Sound_Buffer& sfx) - { - if (sfx.mUses++ == 0) - { - const auto it = std::find(mUnusedBuffers.begin(), mUnusedBuffers.end(), &sfx); - if (it != mUnusedBuffers.end()) - mUnusedBuffers.erase(it); - } - } + /// 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); - void release(Sound_Buffer& sfx) + // Lookup for a sound by file name, and ensure it's ready for use. + Sound_Buffer* load(std::string_view fileName); + + void use(Sound_Buffer& sfx) + { + if (sfx.mUses++ == 0) { - if (--sfx.mUses == 0) - mUnusedBuffers.push_front(&sfx); + const auto it = std::find(mUnusedBuffers.begin(), mUnusedBuffers.end(), &sfx); + if (it != mUnusedBuffers.end()) + mUnusedBuffers.erase(it); } + } + + void release(Sound_Buffer& sfx) + { + if (--sfx.mUses == 0) + mUnusedBuffers.push_front(&sfx); + } + + void clear(); - void clear(); + private: + Sound_Buffer* loadSfx(Sound_Buffer* sfx); - private: - const VFS::Manager* const mVfs; - Sound_Output* mOutput; - std::deque mSoundBuffers; - std::unordered_map mBufferNameMap; - 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; + Sound_Output* 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; - inline Sound_Buffer* insertSound(const std::string& soundId, const ESM::Sound& sound); + inline Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound); + inline Sound_Buffer* insertSound(std::string_view fileName); - inline void unloadUnused(); + inline void unloadUnused(); }; } diff --git a/apps/openmw/mwsound/sound_decoder.hpp b/apps/openmw/mwsound/sound_decoder.hpp index 34bae87d74d..17f9d289093 100644 --- a/apps/openmw/mwsound/sound_decoder.hpp +++ b/apps/openmw/mwsound/sound_decoder.hpp @@ -1,6 +1,8 @@ #ifndef GAME_SOUND_SOUND_DECODER_H #define GAME_SOUND_SOUND_DECODER_H +#include + #include #include @@ -11,21 +13,23 @@ namespace VFS namespace MWSound { - enum SampleType { + enum SampleType + { SampleType_UInt8, SampleType_Int16, SampleType_Float32 }; - const char *getSampleTypeName(SampleType type); + const char* getSampleTypeName(SampleType type); - enum ChannelConfig { + enum ChannelConfig + { ChannelConfig_Mono, ChannelConfig_Stereo, ChannelConfig_Quad, ChannelConfig_5point1, ChannelConfig_7point1 }; - const char *getChannelConfigName(ChannelConfig config); + const char* getChannelConfigName(ChannelConfig config); size_t framesToBytes(size_t frames, ChannelConfig config, SampleType type); size_t bytesToFrames(size_t bytes, ChannelConfig config, SampleType type); @@ -34,23 +38,25 @@ namespace MWSound { const VFS::Manager* mResourceMgr; - virtual void open(const std::string &fname) = 0; + virtual void open(VFS::Path::NormalizedView fname) = 0; virtual void close() = 0; virtual std::string getName() = 0; - virtual void getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) = 0; + virtual void getInfo(int* samplerate, ChannelConfig* chans, SampleType* type) = 0; - virtual size_t read(char *buffer, size_t bytes) = 0; - virtual void readAll(std::vector &output); + virtual size_t read(char* buffer, size_t bytes) = 0; + virtual void readAll(std::vector& output); virtual size_t getSampleOffset() = 0; - Sound_Decoder(const VFS::Manager* resourceMgr) : mResourceMgr(resourceMgr) - { } - virtual ~Sound_Decoder() { } + Sound_Decoder(const VFS::Manager* resourceMgr) + : mResourceMgr(resourceMgr) + { + } + virtual ~Sound_Decoder() {} private: - Sound_Decoder(const Sound_Decoder &rhs); - Sound_Decoder& operator=(const Sound_Decoder &rhs); + Sound_Decoder(const Sound_Decoder& rhs); + Sound_Decoder& operator=(const Sound_Decoder& rhs); }; } diff --git a/apps/openmw/mwsound/sound_output.hpp b/apps/openmw/mwsound/sound_output.hpp index 9ec8b17dc9c..5a771249852 100644 --- a/apps/openmw/mwsound/sound_output.hpp +++ b/apps/openmw/mwsound/sound_output.hpp @@ -1,10 +1,13 @@ #ifndef GAME_SOUND_SOUND_OUTPUT_H #define GAME_SOUND_SOUND_OUTPUT_H -#include #include +#include #include +#include +#include + #include "../mwbase/soundmanager.hpp" namespace MWSound @@ -15,15 +18,9 @@ namespace MWSound class Stream; // An opaque handle for the implementation's sound buffers. - typedef void *Sound_Handle; + typedef void* Sound_Handle; // An opaque handle for the implementation's sound instances. - typedef void *Sound_Instance; - - enum class HrtfMode { - Disable, - Enable, - Auto - }; + typedef void* Sound_Instance; enum Environment { @@ -31,39 +28,42 @@ namespace MWSound Env_Underwater }; + using HrtfMode = Settings::HrtfMode; + class Sound_Output { - SoundManager &mManager; + SoundManager& mManager; virtual std::vector enumerate() = 0; - virtual bool init(const std::string &devname, const std::string &hrtfname, HrtfMode hrtfmode) = 0; + virtual bool init(const std::string& devname, const std::string& hrtfname, HrtfMode hrtfmode) = 0; virtual void deinit() = 0; virtual std::vector enumerateHrtf() = 0; - virtual void setHrtf(const std::string &hrtfname, HrtfMode hrtfmode) = 0; - virtual std::pair loadSound(const std::string &fname) = 0; + virtual std::pair loadSound(VFS::Path::NormalizedView fname) = 0; virtual size_t unloadSound(Sound_Handle data) = 0; - virtual bool playSound(Sound *sound, Sound_Handle data, float offset) = 0; - virtual bool playSound3D(Sound *sound, Sound_Handle data, float offset) = 0; - virtual void finishSound(Sound *sound) = 0; - virtual bool isSoundPlaying(Sound *sound) = 0; - virtual void updateSound(Sound *sound) = 0; - - virtual bool streamSound(DecoderPtr decoder, Stream *sound, bool getLoudnessData=false) = 0; - virtual bool streamSound3D(DecoderPtr decoder, Stream *sound, bool getLoudnessData) = 0; - virtual void finishStream(Stream *sound) = 0; - virtual double getStreamDelay(Stream *sound) = 0; - virtual double getStreamOffset(Stream *sound) = 0; - virtual float getStreamLoudness(Stream *sound) = 0; - virtual bool isStreamPlaying(Stream *sound) = 0; - virtual void updateStream(Stream *sound) = 0; + virtual bool playSound(Sound* sound, Sound_Handle data, float offset) = 0; + virtual bool playSound3D(Sound* sound, Sound_Handle data, float offset) = 0; + virtual void finishSound(Sound* sound) = 0; + virtual bool isSoundPlaying(Sound* sound) = 0; + virtual void updateSound(Sound* sound) = 0; + + virtual bool streamSound(DecoderPtr decoder, Stream* sound, bool getLoudnessData = false) = 0; + virtual bool streamSound3D(DecoderPtr decoder, Stream* sound, bool getLoudnessData) = 0; + virtual void finishStream(Stream* sound) = 0; + virtual double getStreamDelay(Stream* sound) = 0; + virtual double getStreamOffset(Stream* sound) = 0; + virtual float getStreamLoudness(Stream* sound) = 0; + virtual bool isStreamPlaying(Stream* sound) = 0; + virtual void updateStream(Stream* sound) = 0; virtual void startUpdate() = 0; virtual void finishUpdate() = 0; - virtual void updateListener(const osg::Vec3f &pos, const osg::Vec3f &atdir, const osg::Vec3f &updir, Environment env) = 0; + virtual void updateListener( + const osg::Vec3f& pos, const osg::Vec3f& atdir, const osg::Vec3f& updir, Environment env) + = 0; virtual void pauseSounds(int types) = 0; virtual void resumeSounds(int types) = 0; @@ -71,17 +71,20 @@ namespace MWSound virtual void pauseActiveDevice() = 0; virtual void resumeActiveDevice() = 0; - Sound_Output& operator=(const Sound_Output &rhs); - Sound_Output(const Sound_Output &rhs); + Sound_Output& operator=(const Sound_Output& rhs); + Sound_Output(const Sound_Output& rhs); protected: bool mInitialized; - Sound_Output(SoundManager &mgr) - : mManager(mgr), mInitialized(false) - { } + Sound_Output(SoundManager& mgr) + : mManager(mgr) + , mInitialized(false) + { + } + public: - virtual ~Sound_Output() { } + virtual ~Sound_Output() {} bool isInitialized() const { return mInitialized; } diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index fc9735d576f..8848948c1e3 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -3,36 +3,44 @@ #include #include #include +#include #include -#include #include +#include +#include +#include #include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/statemanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" +#include "constants.hpp" +#include "ffmpeg_decoder.hpp" +#include "openal_output.hpp" +#include "sound.hpp" #include "sound_buffer.hpp" #include "sound_decoder.hpp" #include "sound_output.hpp" -#include "sound.hpp" - -#include "openal_output.hpp" -#include "ffmpeg_decoder.hpp" - namespace MWSound { namespace { constexpr float sMinUpdateInterval = 1.0f / 30.0f; + constexpr float sSfxFadeInDuration = 1.0f; + constexpr float sSfxFadeOutDuration = 1.0f; + constexpr float sSoundCullDistance = 2000.f; WaterSoundUpdaterSettings makeWaterSoundUpdaterSettings() { @@ -42,25 +50,75 @@ namespace MWSound settings.mNearWaterPoints = Fallback::Map::getInt("Water_NearWaterPoints"); settings.mNearWaterIndoorTolerance = Fallback::Map::getFloat("Water_NearWaterIndoorTolerance"); settings.mNearWaterOutdoorTolerance = Fallback::Map::getFloat("Water_NearWaterOutdoorTolerance"); - settings.mNearWaterIndoorID = Misc::StringUtils::lowerCase(Fallback::Map::getString("Water_NearWaterIndoorID")); - settings.mNearWaterOutdoorID = Misc::StringUtils::lowerCase(Fallback::Map::getString("Water_NearWaterOutdoorID")); + settings.mNearWaterIndoorID = ESM::RefId::stringRefId(Fallback::Map::getString("Water_NearWaterIndoorID")); + settings.mNearWaterOutdoorID + = ESM::RefId::stringRefId(Fallback::Map::getString("Water_NearWaterOutdoorID")); return settings; } + + float initialFadeVolume(float squaredDist, Sound_Buffer* sfx, Type type, PlayMode mode) + { + // If a sound is farther away than its maximum distance, start playing it with a zero fade volume. + // It can still become audible once the player moves closer. + const float maxDist = sfx->getMaxDist(); + if (squaredDist > (maxDist * maxDist)) + return 0.0f; + + // This is a *heuristic* that causes environment sounds to fade in. The idea is the following: + // - Only looped sounds playing through the effects channel are environment sounds + // - Do not fade in sounds if the player is already so close that the sound plays at maximum volume + const float minDist = sfx->getMinDist(); + if ((squaredDist > (minDist * minDist)) && (type == Type::Sfx) && (mode & PlayMode::Loop)) + return 0.0f; + + return 1.0; + } + + // Gets the combined volume settings for the given sound type + float volumeFromType(Type type) + { + float volume = Settings::sound().mMasterVolume; + + switch (type) + { + case Type::Sfx: + volume *= Settings::sound().mSfxVolume; + break; + case Type::Voice: + volume *= Settings::sound().mVoiceVolume; + break; + case Type::Foot: + volume *= Settings::sound().mFootstepsVolume; + break; + case Type::Music: + volume *= Settings::sound().mMusicVolume; + break; + case Type::Movie: + case Type::Mask: + break; + } + + return volume; + } } // For combining PlayMode and Type flags - inline int operator|(PlayMode a, Type b) { return static_cast(a) | static_cast(b); } + inline int operator|(PlayMode a, Type b) + { + return static_cast(a) | static_cast(b); + } SoundManager::SoundManager(const VFS::Manager* vfs, bool useSound) : mVFS(vfs) - , mOutput(new OpenAL_Output(*this)) + , mOutput(std::make_unique(*this)) , mWaterSoundUpdater(makeWaterSoundUpdaterSettings()) - , mSoundBuffers(*vfs, *mOutput) + , mSoundBuffers(*mOutput) + , mMusicType(MWSound::MusicType::Normal) , mListenerUnderwater(false) - , mListenerPos(0,0,0) - , mListenerDir(1,0,0) - , mListenerUp(0,0,1) + , mListenerPos(0, 0, 0) + , mListenerDir(1, 0, 0) + , mListenerUp(0, 0, 1) , mUnderwaterSound(nullptr) , mNearWaterSound(nullptr) , mPlaybackPaused(false) @@ -68,19 +126,13 @@ namespace MWSound , mLastCell(nullptr) , mCurrentRegionSound(nullptr) { - if(!useSound) + if (!useSound) { Log(Debug::Info) << "Sound disabled."; return; } - std::string hrtfname = Settings::Manager::getString("hrtf", "Sound"); - int hrtfstate = Settings::Manager::getInt("hrtf enable", "Sound"); - HrtfMode hrtfmode = hrtfstate < 0 ? HrtfMode::Auto : - hrtfstate > 0 ? HrtfMode::Enable : HrtfMode::Disable; - - std::string devname = Settings::Manager::getString("device", "Sound"); - if(!mOutput->init(devname, hrtfname, hrtfmode)) + if (!mOutput->init(Settings::sound().mDevice, Settings::sound().mHrtf, Settings::sound().mHrtfEnable)) { Log(Debug::Error) << "Failed to initialize audio output, sound disabled"; return; @@ -90,17 +142,17 @@ namespace MWSound std::stringstream stream; stream << "Enumerated output devices:\n"; - for(const std::string &name : names) + for (const std::string& name : names) stream << " " << name; Log(Debug::Info) << stream.str(); stream.str(""); names = mOutput->enumerateHrtf(); - if(!names.empty()) + if (!names.empty()) { stream << "Enumerated HRTF names:\n"; - for(const std::string &name : names) + for (const std::string& name : names) stream << " " << name; Log(Debug::Info) << stream.str(); @@ -120,27 +172,15 @@ namespace MWSound return std::make_shared(mVFS); } - DecoderPtr SoundManager::loadVoice(const std::string &voicefile) + DecoderPtr SoundManager::loadVoice(VFS::Path::NormalizedView voicefile) { try { DecoderPtr decoder = getDecoder(); - - // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. - if(mVFS->exists(voicefile)) - decoder->open(voicefile); - else - { - std::string file = voicefile; - std::string::size_type pos = file.rfind('.'); - if(pos != std::string::npos) - file = file.substr(0, pos)+".mp3"; - decoder->open(file); - } - + decoder->open(Misc::ResourceHelpers::correctSoundPath(voicefile, *decoder->mResourceMgr)); return decoder; } - catch(std::exception &e) + catch (std::exception& e) { Log(Debug::Error) << "Failed to load audio from " << voicefile << ": " << e.what(); } @@ -158,28 +198,32 @@ namespace MWSound return mStreams.get(); } - StreamPtr SoundManager::playVoice(DecoderPtr decoder, const osg::Vec3f &pos, bool playlocal) + StreamPtr SoundManager::playVoice(DecoderPtr decoder, const osg::Vec3f& pos, bool playlocal) { MWBase::World* world = MWBase::Environment::get().getWorld(); - static const float fAudioMinDistanceMult = world->getStore().get().find("fAudioMinDistanceMult")->mValue.getFloat(); - static const float fAudioMaxDistanceMult = world->getStore().get().find("fAudioMaxDistanceMult")->mValue.getFloat(); - static const float fAudioVoiceDefaultMinDistance = world->getStore().get().find("fAudioVoiceDefaultMinDistance")->mValue.getFloat(); - static const float fAudioVoiceDefaultMaxDistance = world->getStore().get().find("fAudioVoiceDefaultMaxDistance")->mValue.getFloat(); + static const float fAudioMinDistanceMult + = world->getStore().get().find("fAudioMinDistanceMult")->mValue.getFloat(); + static const float fAudioMaxDistanceMult + = world->getStore().get().find("fAudioMaxDistanceMult")->mValue.getFloat(); + static const float fAudioVoiceDefaultMinDistance + = world->getStore().get().find("fAudioVoiceDefaultMinDistance")->mValue.getFloat(); + static const float fAudioVoiceDefaultMaxDistance + = world->getStore().get().find("fAudioVoiceDefaultMaxDistance")->mValue.getFloat(); static float minDistance = std::max(fAudioVoiceDefaultMinDistance * fAudioMinDistanceMult, 1.0f); static float maxDistance = std::max(fAudioVoiceDefaultMaxDistance * fAudioMaxDistanceMult, minDistance); bool played; float basevol = volumeFromType(Type::Voice); StreamPtr sound = getStreamRef(); - if(playlocal) + if (playlocal) { sound->init([&] { SoundParams params; params.mBaseVolume = basevol; params.mFlags = PlayMode::NoEnv | Type::Voice | Play_2D; return params; - } ()); - played = mOutput->streamSound(decoder, sound.get(), true); + }()); + played = mOutput->streamSound(std::move(decoder), sound.get(), true); } else { @@ -191,52 +235,56 @@ namespace MWSound params.mMaxDistance = maxDistance; params.mFlags = PlayMode::Normal | Type::Voice | Play_3D; return params; - } ()); - played = mOutput->streamSound3D(decoder, sound.get(), true); + }()); + played = mOutput->streamSound3D(std::move(decoder), sound.get(), true); } - if(!played) + if (!played) return nullptr; return sound; } - // Gets the combined volume settings for the given sound type - float SoundManager::volumeFromType(Type type) const - { - return mVolumeSettings.getVolumeFromType(type); - } - void SoundManager::stopMusic() { - if(mMusic) + if (mMusic) { mOutput->finishStream(mMusic.get()); mMusic = nullptr; } } - void SoundManager::streamMusicFull(const std::string& filename) + void SoundManager::streamMusicFull(VFS::Path::NormalizedView filename) { - if(!mOutput->isInitialized()) + if (!mOutput->isInitialized()) return; - Log(Debug::Info) << "Playing " << filename; - mLastPlayedMusic = filename; stopMusic(); + if (filename.value().empty()) + return; + + Log(Debug::Info) << "Playing \"" << filename << "\""; DecoderPtr decoder = getDecoder(); - decoder->open(filename); + try + { + decoder->open(filename); + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Failed to load audio from \"" << filename << "\": " << e.what(); + return; + } mMusic = getStreamRef(); mMusic->init([&] { SoundParams params; params.mBaseVolume = volumeFromType(Type::Music); - params.mFlags = PlayMode::NoEnv | Type::Music | Play_2D; + params.mFlags = PlayMode::NoEnvNoScaling | Type::Music | Play_2D; return params; - } ()); - mOutput->streamSound(decoder, mMusic.get()); + }()); + mOutput->streamSound(std::move(decoder), mMusic.get()); } - void SoundManager::advanceMusic(const std::string& filename) + void SoundManager::advanceMusic(VFS::Path::NormalizedView filename, float fadeOut) { if (!isMusicPlaying()) { @@ -246,39 +294,7 @@ namespace MWSound mNextMusic = filename; - mMusic->setFadeout(1.f); - } - - void SoundManager::startRandomTitle() - { - const std::vector &filelist = mMusicFiles[mCurrentPlaylist]; - auto &tracklist = mMusicToPlay[mCurrentPlaylist]; - - // Do a Fisher-Yates shuffle - - // Repopulate if playlist is empty - if(tracklist.empty()) - { - tracklist.resize(filelist.size()); - std::iota(tracklist.begin(), tracklist.end(), 0); - } - - int i = Misc::Rng::rollDice(tracklist.size()); - - // Reshuffle if last played music is the same after a repopulation - if(filelist[tracklist[i]] == mLastPlayedMusic) - i = (i+1) % tracklist.size(); - - // Remove music from list after advancing music - advanceMusic(filelist[tracklist[i]]); - tracklist[i] = tracklist.back(); - tracklist.pop_back(); - } - - - void SoundManager::streamMusic(const std::string& filename) - { - advanceMusic("Music/"+filename); + mMusic->setFadeout(fadeOut); } bool SoundManager::isMusicPlaying() @@ -286,149 +302,91 @@ namespace MWSound return mMusic && mOutput->isStreamPlaying(mMusic.get()); } - void SoundManager::playPlaylist(const std::string &playlist) + void SoundManager::streamMusic(VFS::Path::NormalizedView filename, MusicType type, float fade) { - if (mCurrentPlaylist == playlist) + // Can not interrupt scripted music by built-in playlists + if (mMusicType == MusicType::MWScript && type != MusicType::MWScript) return; - if (mMusicFiles.find(playlist) == mMusicFiles.end()) - { - std::vector filelist; - const std::map& index = mVFS->getIndex(); - - std::string pattern = "Music/" + playlist; - mVFS->normalizeFilename(pattern); - - std::map::const_iterator found = index.lower_bound(pattern); - while (found != index.end()) - { - if (found->first.size() >= pattern.size() && found->first.substr(0, pattern.size()) == pattern) - filelist.push_back(found->first); - else - break; - ++found; - } - - mMusicFiles[playlist] = filelist; - } - - if (mMusicFiles[playlist].empty()) - return; - - mCurrentPlaylist = playlist; - startRandomTitle(); + mMusicType = type; + advanceMusic(filename, fade); } - void SoundManager::playTitleMusic() + void SoundManager::say(const MWWorld::ConstPtr& ptr, VFS::Path::NormalizedView filename) { - if (mCurrentPlaylist == "Title") + if (!mOutput->isInitialized()) return; - if (mMusicFiles.find("Title") == mMusicFiles.end()) - { - std::vector filelist; - const std::map& index = mVFS->getIndex(); - // Is there an ini setting for this filename or something? - std::string filename = "music/special/morrowind title.mp3"; - auto found = index.find(filename); - if (found != index.end()) - { - filelist.emplace_back(found->first); - mMusicFiles["Title"] = filelist; - } - else - { - Log(Debug::Warning) << "Title music not found"; - return; - } - } - - if (mMusicFiles["Title"].empty()) - return; - - mCurrentPlaylist = "Title"; - startRandomTitle(); - } - - void SoundManager::say(const MWWorld::ConstPtr &ptr, const std::string &filename) - { - if(!mOutput->isInitialized()) - return; - - std::string voicefile = "Sound/"+filename; - - mVFS->normalizeFilename(voicefile); - DecoderPtr decoder = loadVoice(voicefile); + DecoderPtr decoder = loadVoice(filename); if (!decoder) return; - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); const osg::Vec3f pos = world->getActorHeadTransform(ptr).getTrans(); stopSay(ptr); - StreamPtr sound = playVoice(decoder, pos, (ptr == MWMechanics::getPlayer())); - if(!sound) return; + StreamPtr sound = playVoice(std::move(decoder), pos, (ptr == MWMechanics::getPlayer())); + if (!sound) + return; - mSaySoundsQueue.emplace(ptr, std::move(sound)); + mSaySoundsQueue.emplace(ptr.mRef, SaySound{ ptr.mCell, std::move(sound) }); } - float SoundManager::getSaySoundLoudness(const MWWorld::ConstPtr &ptr) const + float SoundManager::getSaySoundLoudness(const MWWorld::ConstPtr& ptr) const { - SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr); - if(snditer != mActiveSaySounds.end()) + SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr.mRef); + if (snditer != mActiveSaySounds.end()) { - Stream *sound = snditer->second.get(); + Stream* sound = snditer->second.mStream.get(); return mOutput->getStreamLoudness(sound); } return 0.0f; } - void SoundManager::say(const std::string& filename) + void SoundManager::say(VFS::Path::NormalizedView filename) { - if(!mOutput->isInitialized()) + if (!mOutput->isInitialized()) return; - std::string voicefile = "Sound/"+filename; - - mVFS->normalizeFilename(voicefile); - DecoderPtr decoder = loadVoice(voicefile); + DecoderPtr decoder = loadVoice(filename); if (!decoder) return; stopSay(MWWorld::ConstPtr()); - StreamPtr sound = playVoice(decoder, osg::Vec3f(), true); - if(!sound) return; + StreamPtr sound = playVoice(std::move(decoder), osg::Vec3f(), true); + if (!sound) + return; - mActiveSaySounds.emplace(MWWorld::ConstPtr(), std::move(sound)); + mActiveSaySounds.emplace(nullptr, SaySound{ nullptr, std::move(sound) }); } - bool SoundManager::sayDone(const MWWorld::ConstPtr &ptr) const + bool SoundManager::sayDone(const MWWorld::ConstPtr& ptr) const { - SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr); - if(snditer != mActiveSaySounds.end()) + SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr.mRef); + if (snditer != mActiveSaySounds.end()) { - if(mOutput->isStreamPlaying(snditer->second.get())) + if (mOutput->isStreamPlaying(snditer->second.mStream.get())) return false; return true; } return true; } - bool SoundManager::sayActive(const MWWorld::ConstPtr &ptr) const + bool SoundManager::sayActive(const MWWorld::ConstPtr& ptr) const { - SaySoundMap::const_iterator snditer = mSaySoundsQueue.find(ptr); - if(snditer != mSaySoundsQueue.end()) + SaySoundMap::const_iterator snditer = mSaySoundsQueue.find(ptr.mRef); + if (snditer != mSaySoundsQueue.end()) { - if(mOutput->isStreamPlaying(snditer->second.get())) + if (mOutput->isStreamPlaying(snditer->second.mStream.get())) return true; return false; } - snditer = mActiveSaySounds.find(ptr); - if(snditer != mActiveSaySounds.end()) + snditer = mActiveSaySounds.find(ptr.mRef); + if (snditer != mActiveSaySounds.end()) { - if(mOutput->isStreamPlaying(snditer->second.get())) + if (mOutput->isStreamPlaying(snditer->second.mStream.get())) return true; return false; } @@ -436,37 +394,36 @@ namespace MWSound return false; } - void SoundManager::stopSay(const MWWorld::ConstPtr &ptr) + void SoundManager::stopSay(const MWWorld::ConstPtr& ptr) { - SaySoundMap::iterator snditer = mSaySoundsQueue.find(ptr); - if(snditer != mSaySoundsQueue.end()) + SaySoundMap::iterator snditer = mSaySoundsQueue.find(ptr.mRef); + if (snditer != mSaySoundsQueue.end()) { - mOutput->finishStream(snditer->second.get()); + mOutput->finishStream(snditer->second.mStream.get()); mSaySoundsQueue.erase(snditer); } - snditer = mActiveSaySounds.find(ptr); - if(snditer != mActiveSaySounds.end()) + snditer = mActiveSaySounds.find(ptr.mRef); + if (snditer != mActiveSaySounds.end()) { - mOutput->finishStream(snditer->second.get()); + mOutput->finishStream(snditer->second.mStream.get()); mActiveSaySounds.erase(snditer); } } - - Stream *SoundManager::playTrack(const DecoderPtr& decoder, Type type) + Stream* SoundManager::playTrack(const DecoderPtr& decoder, Type type) { - if(!mOutput->isInitialized()) + if (!mOutput->isInitialized()) return nullptr; StreamPtr track = getStreamRef(); track->init([&] { SoundParams params; params.mBaseVolume = volumeFromType(type); - params.mFlags = PlayMode::NoEnv | type | Play_2D; + params.mFlags = PlayMode::NoEnvNoScaling | type | Play_2D; return params; - } ()); - if(!mOutput->streamSound(decoder, track.get())) + }()); + if (!mOutput->streamSound(decoder, track.get())) return nullptr; Stream* result = track.get(); @@ -475,29 +432,38 @@ namespace MWSound return result; } - void SoundManager::stopTrack(Stream *stream) + void SoundManager::stopTrack(Stream* stream) { mOutput->finishStream(stream); TrackList::iterator iter = std::lower_bound(mActiveTracks.begin(), mActiveTracks.end(), stream, - [] (const StreamPtr& lhs, Stream* rhs) { return lhs.get() < rhs; }); - if(iter != mActiveTracks.end() && iter->get() == stream) + [](const StreamPtr& lhs, Stream* rhs) { return lhs.get() < rhs; }); + if (iter != mActiveTracks.end() && iter->get() == stream) mActiveTracks.erase(iter); } - double SoundManager::getTrackTimeDelay(Stream *stream) + double SoundManager::getTrackTimeDelay(Stream* stream) { return mOutput->getStreamDelay(stream); } + bool SoundManager::remove3DSoundAtDistance(PlayMode mode, const MWWorld::ConstPtr& ptr) const + { + if (!mOutput->isInitialized()) + return true; + + const osg::Vec3f objpos(ptr.getRefData().getPosition().asVec3()); + const float squaredDist = (mListenerPos - objpos).length2(); + if ((mode & PlayMode::RemoveAtDistance) && squaredDist > sSoundCullDistance * sSoundCullDistance) + return true; - Sound* SoundManager::playSound(const std::string& soundId, float volume, float pitch, Type type, PlayMode mode, float offset) + return false; + } + + Sound* SoundManager::playSound(Sound_Buffer* sfx, float volume, float pitch, Type type, PlayMode mode, float offset) { - if(!mOutput->isInitialized()) + if (!mOutput->isInitialized()) return nullptr; - Sound_Buffer *sfx = mSoundBuffers.load(Misc::StringUtils::lowerCase(soundId)); - if(!sfx) return nullptr; - // Only one copy of given sound can be played at time, so stop previous copy stopSound(sfx, MWWorld::ConstPtr()); @@ -509,37 +475,61 @@ namespace MWSound params.mPitch = pitch; params.mFlags = mode | type | Play_2D; return params; - } ()); - if(!mOutput->playSound(sound.get(), sfx->getHandle(), offset)) + }()); + if (!mOutput->playSound(sound.get(), sfx->getHandle(), offset)) return nullptr; Sound* result = sound.get(); - mActiveSounds[MWWorld::ConstPtr()].emplace_back(std::move(sound), sfx); + mActiveSounds[nullptr].mList.emplace_back(std::move(sound), sfx); mSoundBuffers.use(*sfx); return result; } - Sound *SoundManager::playSound3D(const MWWorld::ConstPtr &ptr, const std::string& soundId, - float volume, float pitch, Type type, PlayMode mode, - float offset) + Sound* SoundManager::playSound( + std::string_view fileName, float volume, float pitch, Type type, PlayMode mode, float offset) { - if(!mOutput->isInitialized()) + if (!mOutput->isInitialized()) return nullptr; - const osg::Vec3f objpos(ptr.getRefData().getPosition().asVec3()); - if ((mode & PlayMode::RemoveAtDistance) && (mListenerPos - objpos).length2() > 2000 * 2000) + std::string normalizedName = VFS::Path::normalizeFilename(fileName); + if (!mVFS->exists(normalizedName)) return nullptr; - // Look up the sound in the ESM data - Sound_Buffer *sfx = mSoundBuffers.load(Misc::StringUtils::lowerCase(soundId)); - if(!sfx) return nullptr; + Sound_Buffer* sfx = mSoundBuffers.load(normalizedName); + if (!sfx) + return nullptr; + + return playSound(sfx, volume, pitch, type, mode, offset); + } + + Sound* SoundManager::playSound( + const ESM::RefId& soundId, float volume, float pitch, Type type, PlayMode mode, float offset) + { + if (!mOutput->isInitialized()) + return nullptr; + + Sound_Buffer* 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, + Type type, PlayMode mode, float offset) + { + if (!mOutput->isInitialized()) + return nullptr; // Only one copy of given sound can be played at time on ptr, so stop previous copy stopSound(sfx, ptr); + const osg::Vec3f objpos(ptr.getRefData().getPosition().asVec3()); + const float squaredDist = (mListenerPos - objpos).length2(); + bool played; SoundPtr sound = getSoundRef(); - if(!(mode&PlayMode::NoPlayerLocal) && ptr == MWMechanics::getPlayer()) + if (!(mode & PlayMode::NoPlayerLocal) && ptr == MWMechanics::getPlayer()) { sound->init([&] { SoundParams params; @@ -548,7 +538,7 @@ namespace MWSound params.mPitch = pitch; params.mFlags = mode | type | Play_2D; return params; - } ()); + }()); played = mOutput->playSound(sound.get(), sfx->getHandle(), offset); } else @@ -558,33 +548,71 @@ namespace MWSound params.mPos = objpos; params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); + params.mFadeVolume = initialFadeVolume(squaredDist, sfx, type, mode); params.mPitch = pitch; params.mMinDistance = sfx->getMinDist(); params.mMaxDistance = sfx->getMaxDist(); params.mFlags = mode | type | Play_3D; return params; - } ()); + }()); played = mOutput->playSound3D(sound.get(), sfx->getHandle(), offset); } - if(!played) + if (!played) return nullptr; Sound* result = sound.get(); - mActiveSounds[ptr].emplace_back(std::move(sound), sfx); + auto it = mActiveSounds.find(ptr.mRef); + if (it == mActiveSounds.end()) + it = mActiveSounds.emplace(ptr.mRef, ActiveSound{ ptr.mCell, {} }).first; + it->second.mList.emplace_back(std::move(sound), sfx); mSoundBuffers.use(*sfx); return result; } - Sound *SoundManager::playSound3D(const osg::Vec3f& initialPos, const std::string& soundId, - float volume, float pitch, Type type, PlayMode mode, - float offset) + Sound* SoundManager::playSound3D(const MWWorld::ConstPtr& ptr, const ESM::RefId& soundId, float volume, float pitch, + Type type, PlayMode mode, float offset) { - if(!mOutput->isInitialized()) + if (remove3DSoundAtDistance(mode, ptr)) return nullptr; // Look up the sound in the ESM data - Sound_Buffer *sfx = mSoundBuffers.load(Misc::StringUtils::lowerCase(soundId)); - if(!sfx) return nullptr; + Sound_Buffer* sfx = mSoundBuffers.load(soundId); + if (!sfx) + return nullptr; + + return playSound3D(ptr, sfx, volume, pitch, type, mode, offset); + } + + Sound* SoundManager::playSound3D(const MWWorld::ConstPtr& ptr, std::string_view fileName, float volume, float pitch, + Type type, PlayMode mode, float offset) + { + if (remove3DSoundAtDistance(mode, ptr)) + return nullptr; + + // Look up the sound + std::string normalizedName = VFS::Path::normalizeFilename(fileName); + if (!mVFS->exists(normalizedName)) + return nullptr; + + Sound_Buffer* sfx = mSoundBuffers.load(normalizedName); + if (!sfx) + return nullptr; + + return playSound3D(ptr, sfx, volume, pitch, type, mode, offset); + } + + Sound* SoundManager::playSound3D(const osg::Vec3f& initialPos, const ESM::RefId& soundId, float volume, float pitch, + Type type, PlayMode mode, float offset) + { + if (!mOutput->isInitialized()) + return nullptr; + + // Look up the sound in the ESM data + Sound_Buffer* sfx = mSoundBuffers.load(soundId); + if (!sfx) + return nullptr; + + const float squaredDist = (mListenerPos - initialPos).length2(); SoundPtr sound = getSoundRef(); sound->init([&] { @@ -592,125 +620,163 @@ namespace MWSound params.mPos = initialPos; params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); + params.mFadeVolume = initialFadeVolume(squaredDist, sfx, type, mode); params.mPitch = pitch; params.mMinDistance = sfx->getMinDist(); params.mMaxDistance = sfx->getMaxDist(); params.mFlags = mode | type | Play_3D; return params; - } ()); - if(!mOutput->playSound3D(sound.get(), sfx->getHandle(), offset)) + }()); + if (!mOutput->playSound3D(sound.get(), sfx->getHandle(), offset)) return nullptr; Sound* result = sound.get(); - mActiveSounds[MWWorld::ConstPtr()].emplace_back(std::move(sound), sfx); + mActiveSounds[nullptr].mList.emplace_back(std::move(sound), sfx); mSoundBuffers.use(*sfx); return result; } - void SoundManager::stopSound(Sound *sound) + void SoundManager::stopSound(Sound* sound) { - if(sound) + if (sound) mOutput->finishSound(sound); } - void SoundManager::stopSound(Sound_Buffer *sfx, const MWWorld::ConstPtr &ptr) + void SoundManager::stopSound(Sound_Buffer* sfx, const MWWorld::ConstPtr& ptr) { - SoundMap::iterator snditer = mActiveSounds.find(ptr); - if(snditer != mActiveSounds.end()) + SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); + if (snditer != mActiveSounds.end()) { - for(SoundBufferRefPair &snd : snditer->second) + for (SoundBufferRefPair& snd : snditer->second.mList) { - if(snd.second == sfx) + if (snd.second == sfx) mOutput->finishSound(snd.first.get()); } } } - void SoundManager::stopSound3D(const MWWorld::ConstPtr &ptr, const std::string& soundId) + void SoundManager::stopSound3D(const MWWorld::ConstPtr& ptr, const ESM::RefId& soundId) + { + if (!mOutput->isInitialized()) + return; + + Sound_Buffer* sfx = mSoundBuffers.lookup(soundId); + if (!sfx) + return; + + stopSound(sfx, ptr); + } + + void SoundManager::stopSound3D(const MWWorld::ConstPtr& ptr, std::string_view fileName) { - if(!mOutput->isInitialized()) + if (!mOutput->isInitialized()) return; - Sound_Buffer *sfx = mSoundBuffers.lookup(Misc::StringUtils::lowerCase(soundId)); - if (!sfx) return; + std::string normalizedName = VFS::Path::normalizeFilename(fileName); + Sound_Buffer* sfx = mSoundBuffers.lookup(normalizedName); + if (!sfx) + return; stopSound(sfx, ptr); } - void SoundManager::stopSound3D(const MWWorld::ConstPtr &ptr) + void SoundManager::stopSound3D(const MWWorld::ConstPtr& ptr) { - SoundMap::iterator snditer = mActiveSounds.find(ptr); - if(snditer != mActiveSounds.end()) + SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); + if (snditer != mActiveSounds.end()) { - for(SoundBufferRefPair &snd : snditer->second) + for (SoundBufferRefPair& snd : snditer->second.mList) mOutput->finishSound(snd.first.get()); } - SaySoundMap::iterator sayiter = mSaySoundsQueue.find(ptr); - if(sayiter != mSaySoundsQueue.end()) - mOutput->finishStream(sayiter->second.get()); - sayiter = mActiveSaySounds.find(ptr); - if(sayiter != mActiveSaySounds.end()) - mOutput->finishStream(sayiter->second.get()); + SaySoundMap::iterator sayiter = mSaySoundsQueue.find(ptr.mRef); + if (sayiter != mSaySoundsQueue.end()) + mOutput->finishStream(sayiter->second.mStream.get()); + sayiter = mActiveSaySounds.find(ptr.mRef); + if (sayiter != mActiveSaySounds.end()) + mOutput->finishStream(sayiter->second.mStream.get()); } - void SoundManager::stopSound(const MWWorld::CellStore *cell) + void SoundManager::stopSound(const MWWorld::CellStore* cell) { - for(SoundMap::value_type &snd : mActiveSounds) + for (auto& [ref, sound] : mActiveSounds) { - if(!snd.first.isEmpty() && snd.first != MWMechanics::getPlayer() && snd.first.getCell() == cell) + if (ref != nullptr && ref != MWMechanics::getPlayer().mRef && sound.mCell == cell) { - for(SoundBufferRefPair &sndbuf : snd.second) + for (SoundBufferRefPair& sndbuf : sound.mList) mOutput->finishSound(sndbuf.first.get()); } } - for(SaySoundMap::value_type &snd : mSaySoundsQueue) + for (const auto& [ref, sound] : mSaySoundsQueue) { - if(!snd.first.isEmpty() && snd.first != MWMechanics::getPlayer() && snd.first.getCell() == cell) - mOutput->finishStream(snd.second.get()); + if (ref != nullptr && ref != MWMechanics::getPlayer().mRef && sound.mCell == cell) + mOutput->finishStream(sound.mStream.get()); } - for(SaySoundMap::value_type &snd : mActiveSaySounds) + for (const auto& [ref, sound] : mActiveSaySounds) { - if(!snd.first.isEmpty() && snd.first != MWMechanics::getPlayer() && snd.first.getCell() == cell) - mOutput->finishStream(snd.second.get()); + if (ref != nullptr && ref != MWMechanics::getPlayer().mRef && sound.mCell == cell) + mOutput->finishStream(sound.mStream.get()); } } - void SoundManager::fadeOutSound3D(const MWWorld::ConstPtr &ptr, - const std::string& soundId, float duration) + void SoundManager::fadeOutSound3D(const MWWorld::ConstPtr& ptr, const ESM::RefId& soundId, float duration) { - SoundMap::iterator snditer = mActiveSounds.find(ptr); - if(snditer != mActiveSounds.end()) + SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); + if (snditer != mActiveSounds.end()) { - Sound_Buffer *sfx = mSoundBuffers.lookup(Misc::StringUtils::lowerCase(soundId)); + Sound_Buffer* sfx = mSoundBuffers.lookup(soundId); if (sfx == nullptr) return; - for(SoundBufferRefPair &sndbuf : snditer->second) + for (SoundBufferRefPair& sndbuf : snditer->second.mList) { - if(sndbuf.second == sfx) + if (sndbuf.second == sfx) sndbuf.first->setFadeout(duration); } } } - bool SoundManager::getSoundPlaying(const MWWorld::ConstPtr &ptr, const std::string& soundId) const + bool SoundManager::getSoundPlaying(const MWWorld::ConstPtr& ptr, std::string_view fileName) const { - SoundMap::const_iterator snditer = mActiveSounds.find(ptr); - if(snditer != mActiveSounds.end()) + std::string normalizedName = VFS::Path::normalizeFilename(fileName); + + SoundMap::const_iterator snditer = mActiveSounds.find(ptr.mRef); + if (snditer != mActiveSounds.end()) { - Sound_Buffer *sfx = mSoundBuffers.lookup(Misc::StringUtils::lowerCase(soundId)); - return std::find_if(snditer->second.cbegin(), snditer->second.cend(), - [this,sfx](const SoundBufferRefPair &snd) -> bool - { return snd.second == sfx && mOutput->isSoundPlaying(snd.first.get()); } - ) != snditer->second.cend(); + Sound_Buffer* sfx = mSoundBuffers.lookup(normalizedName); + if (!sfx) + return false; + + return std::find_if(snditer->second.mList.cbegin(), snditer->second.mList.cend(), + [this, sfx](const SoundBufferRefPair& snd) -> bool { + return snd.second == sfx && mOutput->isSoundPlaying(snd.first.get()); + }) + != snditer->second.mList.cend(); + } + return false; + } + + bool SoundManager::getSoundPlaying(const MWWorld::ConstPtr& ptr, const ESM::RefId& soundId) const + { + SoundMap::const_iterator snditer = mActiveSounds.find(ptr.mRef); + if (snditer != mActiveSounds.end()) + { + Sound_Buffer* sfx = mSoundBuffers.lookup(soundId); + if (!sfx) + return false; + + return std::find_if(snditer->second.mList.cbegin(), snditer->second.mList.cend(), + [this, sfx](const SoundBufferRefPair& snd) -> bool { + return snd.second == sfx && mOutput->isSoundPlaying(snd.first.get()); + }) + != snditer->second.mList.cend(); } return false; } void SoundManager::pauseSounds(BlockerType blocker, int types) { - if(mOutput->isInitialized()) + if (mOutput->isInitialized()) { if (mPausedSoundTypes[blocker] != 0) resumeSounds(blocker); @@ -723,7 +789,7 @@ namespace MWSound void SoundManager::resumeSounds(BlockerType blocker) { - if(mOutput->isInitialized()) + if (mOutput->isInitialized()) { mPausedSoundTypes[blocker] = 0; int types = int(Type::Mask); @@ -757,24 +823,26 @@ namespace MWSound void SoundManager::updateRegionSound(float duration) { - MWBase::World *world = MWBase::Environment::get().getWorld(); + MWBase::World* world = MWBase::Environment::get().getWorld(); const MWWorld::ConstPtr player = world->getPlayerPtr(); - const ESM::Cell *cell = player.getCell()->getCell(); + auto cell = player.getCell()->getCell(); if (!cell->isExterior()) return; if (mCurrentRegionSound && mOutput->isSoundPlaying(mCurrentRegionSound)) return; - if (const auto next = mRegionSoundSelector.getNextRandom(duration, cell->mRegion, *world)) - mCurrentRegionSound = playSound(*next, 1.0f, 1.0f); + ESM::RefId next = mRegionSoundSelector.getNextRandom(duration, cell->getRegion()); + if (!next.empty()) + mCurrentRegionSound = playSound(next, 1.0f, 1.0f); } void SoundManager::updateWaterSound() { MWBase::World* world = MWBase::Environment::get().getWorld(); const MWWorld::ConstPtr player = world->getPlayerPtr(); - const ESM::Cell *curcell = player.getCell()->getCell(); + + const MWWorld::Cell* curcell = player.getCell()->getCell(); const auto update = mWaterSoundUpdater.update(player, *world); WaterSoundAction action; @@ -787,10 +855,10 @@ namespace MWSound break; case WaterSoundAction::SetVolume: mNearWaterSound->setVolume(update.mVolume * sfx->getVolume()); + mNearWaterSound->setFade(sSfxFadeInDuration, 1.0f, Play_FadeExponential); break; case WaterSoundAction::FinishSound: - mOutput->finishSound(mNearWaterSound); - mNearWaterSound = nullptr; + mNearWaterSound->setFade(sSfxFadeOutDuration, 0.0f, Play_FadeExponential | Play_StopAtFadeEnd); break; case WaterSoundAction::PlaySound: if (mNearWaterSound) @@ -803,41 +871,62 @@ namespace MWSound } std::pair SoundManager::getWaterSoundAction( - const WaterSoundUpdate& update, const ESM::Cell* cell) const + const WaterSoundUpdate& update, const MWWorld::Cell* cell) const { if (mNearWaterSound) { if (update.mVolume == 0.0f) - return {WaterSoundAction::FinishSound, nullptr}; + return { WaterSoundAction::FinishSound, nullptr }; bool soundIdChanged = false; Sound_Buffer* sfx = mSoundBuffers.lookup(update.mId); if (mLastCell != cell) { - const auto snditer = mActiveSounds.find(MWWorld::ConstPtr()); + const auto snditer = mActiveSounds.find(nullptr); if (snditer != mActiveSounds.end()) { - const auto pairiter = std::find_if( - snditer->second.begin(), snditer->second.end(), - [this](const SoundBufferRefPairList::value_type &item) -> bool - { return mNearWaterSound == item.first.get(); } - ); - if (pairiter != snditer->second.end() && pairiter->second != sfx) + const auto pairiter = std::find_if(snditer->second.mList.begin(), snditer->second.mList.end(), + [this](const SoundBufferRefPairList::value_type& item) -> bool { + return mNearWaterSound == item.first.get(); + }); + if (pairiter != snditer->second.mList.end() && pairiter->second != sfx) soundIdChanged = true; } } if (soundIdChanged) - return {WaterSoundAction::PlaySound, nullptr}; + return { WaterSoundAction::PlaySound, nullptr }; if (sfx) - return {WaterSoundAction::SetVolume, sfx}; + return { WaterSoundAction::SetVolume, sfx }; } else if (update.mVolume > 0.0f) - return {WaterSoundAction::PlaySound, nullptr}; + return { WaterSoundAction::PlaySound, nullptr }; + + return { WaterSoundAction::DoNothing, nullptr }; + } + + void SoundManager::cull3DSound(SoundBase* sound) + { + // Hard-coded distance is from an original engine + const float maxDist = sound->getDistanceCull() ? sSoundCullDistance : sound->getMaxDistance(); + const float squaredMaxDist = maxDist * maxDist; - return {WaterSoundAction::DoNothing, nullptr}; + const osg::Vec3f pos = sound->getPosition(); + const float squaredDist = (mListenerPos - pos).length2(); + + if (squaredDist > squaredMaxDist) + { + // If getDistanceCull() is set, delete the sound after it has faded out + sound->setFade( + sSfxFadeOutDuration, 0.0f, Play_FadeExponential | (sound->getDistanceCull() ? Play_StopAtFadeEnd : 0)); + } + else + { + // Fade sounds back in once they are in range + sound->setFade(sSfxFadeInDuration, 1.0f, Play_FadeExponential); + } } void SoundManager::updateSounds(float duration) @@ -861,53 +950,39 @@ namespace MWSound duration = mTimePassed; mTimePassed = 0.0f; - // Make sure music is still playing - if(!isMusicPlaying() && !mCurrentPlaylist.empty()) - startRandomTitle(); - Environment env = Env_Normal; if (mListenerUnderwater) env = Env_Underwater; - else if(mUnderwaterSound) + else if (mUnderwaterSound) { mOutput->finishSound(mUnderwaterSound); mUnderwaterSound = nullptr; } mOutput->startUpdate(); - mOutput->updateListener( - mListenerPos, - mListenerDir, - mListenerUp, - env - ); + mOutput->updateListener(mListenerPos, mListenerDir, mListenerUp, env); updateMusic(duration); // Check if any sounds are finished playing, and trash them SoundMap::iterator snditer = mActiveSounds.begin(); - while(snditer != mActiveSounds.end()) + while (snditer != mActiveSounds.end()) { MWWorld::ConstPtr ptr = snditer->first; - SoundBufferRefPairList::iterator sndidx = snditer->second.begin(); - while(sndidx != snditer->second.end()) + SoundBufferRefPairList::iterator sndidx = snditer->second.mList.begin(); + while (sndidx != snditer->second.mList.end()) { - Sound *sound = sndidx->first.get(); + Sound* sound = sndidx->first.get(); - if(!ptr.isEmpty() && sound->getIs3D()) + if (sound->getIs3D()) { - const ESM::Position &pos = ptr.getRefData().getPosition(); - const osg::Vec3f objpos(pos.asVec3()); - sound->setPosition(objpos); - - if(sound->getDistanceCull()) - { - if((mListenerPos - objpos).length2() > 2000*2000) - mOutput->finishSound(sound); - } + if (!ptr.isEmpty()) + sound->setPosition(ptr.getRefData().getPosition().asVec3()); + + cull3DSound(sound); } - if(!mOutput->isSoundPlaying(sound)) + if (!sound->updateFade(duration) || !mOutput->isSoundPlaying(sound)) { mOutput->finishSound(sound); if (sound == mUnderwaterSound) @@ -915,59 +990,53 @@ namespace MWSound if (sound == mNearWaterSound) mNearWaterSound = nullptr; mSoundBuffers.release(*sndidx->second); - sndidx = snditer->second.erase(sndidx); + sndidx = snditer->second.mList.erase(sndidx); } else { - sound->updateFade(duration); - mOutput->updateSound(sound); ++sndidx; } } - if(snditer->second.empty()) + if (snditer->second.mList.empty()) snditer = mActiveSounds.erase(snditer); else ++snditer; } SaySoundMap::iterator sayiter = mActiveSaySounds.begin(); - while(sayiter != mActiveSaySounds.end()) + while (sayiter != mActiveSaySounds.end()) { MWWorld::ConstPtr ptr = sayiter->first; - Stream *sound = sayiter->second.get(); - if(!ptr.isEmpty() && sound->getIs3D()) + Stream* sound = sayiter->second.mStream.get(); + if (sound->getIs3D()) { - MWBase::World *world = MWBase::Environment::get().getWorld(); - const osg::Vec3f pos = world->getActorHeadTransform(ptr).getTrans(); - sound->setPosition(pos); - - if(sound->getDistanceCull()) + if (!ptr.isEmpty()) { - if((mListenerPos - pos).length2() > 2000*2000) - mOutput->finishStream(sound); + MWBase::World* world = MWBase::Environment::get().getWorld(); + sound->setPosition(world->getActorHeadTransform(ptr).getTrans()); } + + cull3DSound(sound); } - if(!mOutput->isStreamPlaying(sound)) + if (!sound->updateFade(duration) || !mOutput->isStreamPlaying(sound)) { mOutput->finishStream(sound); - mActiveSaySounds.erase(sayiter++); + sayiter = mActiveSaySounds.erase(sayiter); } else { - sound->updateFade(duration); - mOutput->updateStream(sound); ++sayiter; } } TrackList::iterator trkiter = mActiveTracks.begin(); - for(;trkiter != mActiveTracks.end();++trkiter) + while (trkiter != mActiveTracks.end()) { - Stream *sound = trkiter->get(); - if(!mOutput->isStreamPlaying(sound)) + Stream* sound = trkiter->get(); + if (!mOutput->isStreamPlaying(sound)) { mOutput->finishStream(sound); trkiter = mActiveTracks.erase(trkiter); @@ -981,73 +1050,81 @@ namespace MWSound } } - if(mListenerUnderwater) + if (mListenerUnderwater) { // Play underwater sound (after updating sounds) - if(!mUnderwaterSound) - mUnderwaterSound = playSound("Underwater", 1.0f, 1.0f, Type::Sfx, PlayMode::LoopNoEnv); + if (!mUnderwaterSound) + mUnderwaterSound + = playSound(ESM::RefId::stringRefId("Underwater"), 1.0f, 1.0f, Type::Sfx, PlayMode::LoopNoEnv); } mOutput->finishUpdate(); } - void SoundManager::updateMusic(float duration) { - if (!mNextMusic.empty()) + if (!mMusic || !mMusic->updateFade(duration) || !mOutput->isStreamPlaying(mMusic.get())) { - mMusic->updateFade(duration); - - mOutput->updateStream(mMusic.get()); - - if (mMusic->getRealVolume() <= 0.f) + stopMusic(); + if (!mNextMusic.value().empty()) { streamMusicFull(mNextMusic); - mNextMusic.clear(); + mNextMusic = VFS::Path::Normalized(); } + else + mMusicType = MusicType::Normal; + } + else + { + mOutput->updateStream(mMusic.get()); } } - void SoundManager::update(float duration) { - if(!mOutput->isInitialized() || mPlaybackPaused) + if (!mOutput->isInitialized() || mPlaybackPaused) return; + MWBase::StateManager::State state = MWBase::Environment::get().getStateManager()->getState(); + bool isMainMenu = MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu) + && state == MWBase::StateManager::State_NoGame; + + if (isMainMenu && !isMusicPlaying()) + { + if (mVFS->exists(MWSound::titleMusic)) + streamMusic(MWSound::titleMusic, MWSound::MusicType::Normal); + } + updateSounds(duration); - if (MWBase::Environment::get().getStateManager()->getState()!= - MWBase::StateManager::State_NoGame) + if (state != MWBase::StateManager::State_NoGame) { updateRegionSound(duration); updateWaterSound(); } } - void SoundManager::processChangedSettings(const Settings::CategorySettingVector& settings) { - mVolumeSettings.update(); - - if(!mOutput->isInitialized()) + if (!mOutput->isInitialized()) return; mOutput->startUpdate(); - for(SoundMap::value_type &snd : mActiveSounds) + for (SoundMap::value_type& snd : mActiveSounds) { - for(SoundBufferRefPair &sndbuf : snd.second) + for (SoundBufferRefPair& sndbuf : snd.second.mList) { - Sound *sound = sndbuf.first.get(); + Sound* sound = sndbuf.first.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateSound(sound); } } - for(SaySoundMap::value_type &snd : mActiveSaySounds) + for (SaySoundMap::value_type& snd : mActiveSaySounds) { - Stream *sound = snd.second.get(); + Stream* sound = snd.second.mStream.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateStream(sound); } - for(SaySoundMap::value_type &snd : mSaySoundsQueue) + for (SaySoundMap::value_type& snd : mSaySoundsQueue) { - Stream *sound = snd.second.get(); + Stream* sound = snd.second.mStream.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateStream(sound); } @@ -1056,7 +1133,7 @@ namespace MWSound sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateStream(sound.get()); } - if(mMusic) + if (mMusic) { mMusic->setBaseVolume(volumeFromType(mMusic->getPlayType())); mOutput->updateStream(mMusic.get()); @@ -1064,100 +1141,110 @@ namespace MWSound mOutput->finishUpdate(); } - void SoundManager::setListenerPosDir(const osg::Vec3f &pos, const osg::Vec3f &dir, const osg::Vec3f &up, bool underwater) + void SoundManager::setListenerPosDir( + const osg::Vec3f& pos, const osg::Vec3f& dir, const osg::Vec3f& up, bool underwater) { mListenerPos = pos; mListenerDir = dir; - mListenerUp = up; + mListenerUp = up; mListenerUnderwater = underwater; mWaterSoundUpdater.setUnderwater(underwater); } - void SoundManager::updatePtr(const MWWorld::ConstPtr &old, const MWWorld::ConstPtr &updated) + void SoundManager::updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) { - SoundMap::iterator snditer = mActiveSounds.find(old); - if(snditer != mActiveSounds.end()) - { - SoundBufferRefPairList sndlist = std::move(snditer->second); - mActiveSounds.erase(snditer); - mActiveSounds.emplace(updated, std::move(sndlist)); - } + SoundMap::iterator snditer = mActiveSounds.find(old.mRef); + if (snditer != mActiveSounds.end()) + snditer->second.mCell = updated.mCell; - SaySoundMap::iterator sayiter = mSaySoundsQueue.find(old); - if(sayiter != mSaySoundsQueue.end()) - { - StreamPtr stream = std::move(sayiter->second); - mSaySoundsQueue.erase(sayiter); - mSaySoundsQueue.emplace(updated, std::move(stream)); - } + if (const auto it = mSaySoundsQueue.find(old.mRef); it != mSaySoundsQueue.end()) + it->second.mCell = updated.mCell; - sayiter = mActiveSaySounds.find(old); - if(sayiter != mActiveSaySounds.end()) - { - StreamPtr stream = std::move(sayiter->second); - mActiveSaySounds.erase(sayiter); - mActiveSaySounds.emplace(updated, std::move(stream)); - } + if (const auto it = mActiveSaySounds.find(old.mRef); it != mActiveSaySounds.end()) + it->second.mCell = updated.mCell; } // Default readAll implementation, for decoders that can't do anything // better - void Sound_Decoder::readAll(std::vector &output) + void Sound_Decoder::readAll(std::vector& output) { size_t total = output.size(); size_t got; - output.resize(total+32768); - while((got=read(&output[total], output.size()-total)) > 0) + output.resize(total + 32768); + while ((got = read(&output[total], output.size() - total)) > 0) { total += got; - output.resize(total*2); + output.resize(total * 2); } output.resize(total); } - - const char *getSampleTypeName(SampleType type) + const char* getSampleTypeName(SampleType type) { - switch(type) + switch (type) { - case SampleType_UInt8: return "U8"; - case SampleType_Int16: return "S16"; - case SampleType_Float32: return "Float32"; + case SampleType_UInt8: + return "U8"; + case SampleType_Int16: + return "S16"; + case SampleType_Float32: + return "Float32"; } return "(unknown sample type)"; } - const char *getChannelConfigName(ChannelConfig config) + const char* getChannelConfigName(ChannelConfig config) { - switch(config) + switch (config) { - case ChannelConfig_Mono: return "Mono"; - case ChannelConfig_Stereo: return "Stereo"; - case ChannelConfig_Quad: return "Quad"; - case ChannelConfig_5point1: return "5.1 Surround"; - case ChannelConfig_7point1: return "7.1 Surround"; + case ChannelConfig_Mono: + return "Mono"; + case ChannelConfig_Stereo: + return "Stereo"; + case ChannelConfig_Quad: + return "Quad"; + case ChannelConfig_5point1: + return "5.1 Surround"; + case ChannelConfig_7point1: + return "7.1 Surround"; } return "(unknown channel config)"; } size_t framesToBytes(size_t frames, ChannelConfig config, SampleType type) { - switch(config) + switch (config) { - case ChannelConfig_Mono: frames *= 1; break; - case ChannelConfig_Stereo: frames *= 2; break; - case ChannelConfig_Quad: frames *= 4; break; - case ChannelConfig_5point1: frames *= 6; break; - case ChannelConfig_7point1: frames *= 8; break; + case ChannelConfig_Mono: + frames *= 1; + break; + case ChannelConfig_Stereo: + frames *= 2; + break; + case ChannelConfig_Quad: + frames *= 4; + break; + case ChannelConfig_5point1: + frames *= 6; + break; + case ChannelConfig_7point1: + frames *= 8; + break; } - switch(type) + switch (type) { - case SampleType_UInt8: frames *= 1; break; - case SampleType_Int16: frames *= 2; break; - case SampleType_Float32: frames *= 4; break; + case SampleType_UInt8: + frames *= 1; + break; + case SampleType_Int16: + frames *= 2; + break; + case SampleType_Float32: + frames *= 4; + break; } return frames; } @@ -1169,11 +1256,12 @@ namespace MWSound void SoundManager::clear() { - SoundManager::stopMusic(); + stopMusic(); + mMusicType = MusicType::Normal; - for(SoundMap::value_type &snd : mActiveSounds) + for (SoundMap::value_type& snd : mActiveSounds) { - for(SoundBufferRefPair &sndbuf : snd.second) + for (SoundBufferRefPair& sndbuf : snd.second.mList) { mOutput->finishSound(sndbuf.first.get()); mSoundBuffers.release(*sndbuf.second); @@ -1183,15 +1271,15 @@ namespace MWSound mUnderwaterSound = nullptr; mNearWaterSound = nullptr; - for(SaySoundMap::value_type &snd : mSaySoundsQueue) - mOutput->finishStream(snd.second.get()); + for (SaySoundMap::value_type& snd : mSaySoundsQueue) + mOutput->finishStream(snd.second.mStream.get()); mSaySoundsQueue.clear(); - for(SaySoundMap::value_type &snd : mActiveSaySounds) - mOutput->finishStream(snd.second.get()); + for (SaySoundMap::value_type& snd : mActiveSaySounds) + mOutput->finishStream(snd.second.mStream.get()); mActiveSaySounds.clear(); - for(StreamPtr& sound : mActiveTracks) + for (StreamPtr& sound : mActiveTracks) mOutput->finishStream(sound.get()); mActiveTracks.clear(); mPlaybackPaused = false; diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index 934402cd4c7..a5e5b2c45f0 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -1,23 +1,23 @@ #ifndef GAME_SOUND_SOUNDMANAGER_H #define GAME_SOUND_SOUNDMANAGER_H +#include #include #include -#include -#include #include +#include -#include -#include #include +#include +#include +#include #include "../mwbase/soundmanager.hpp" #include "regionsoundselector.hpp" -#include "watersoundupdater.hpp" -#include "type.hpp" -#include "volumesettings.hpp" #include "sound_buffer.hpp" +#include "type.hpp" +#include "watersoundupdater.hpp" namespace VFS { @@ -30,10 +30,16 @@ namespace ESM struct Cell; } +namespace MWWorld +{ + class Cell; +} + namespace MWSound { class Sound_Output; struct Sound_Decoder; + class SoundBase; class Sound; class Stream; @@ -46,13 +52,6 @@ namespace MWSound std::unique_ptr mOutput; - // Caches available music tracks by - std::unordered_map> mMusicFiles; - std::unordered_map> mMusicToPlay; // A list with music files not yet played - std::string mLastPlayedMusic; // The music file that was last played - - VolumeSettings mVolumeSettings; - WaterSoundUpdater mWaterSoundUpdater; SoundBufferPool mSoundBuffers; @@ -63,10 +62,23 @@ namespace MWSound typedef std::pair SoundBufferRefPair; typedef std::vector SoundBufferRefPairList; - typedef std::map SoundMap; + + struct ActiveSound + { + const MWWorld::CellStore* mCell = nullptr; + SoundBufferRefPairList mList; + }; + + typedef std::map SoundMap; SoundMap mActiveSounds; - typedef std::map SaySoundMap; + struct SaySound + { + const MWWorld::CellStore* mCell; + StreamPtr mStream; + }; + + typedef std::map SaySoundMap; SaySoundMap mSaySoundsQueue; SaySoundMap mActiveSaySounds; @@ -74,7 +86,7 @@ namespace MWSound TrackList mActiveTracks; StreamPtr mMusic; - std::string mCurrentPlaylist; + MusicType mMusicType; bool mListenerUnderwater; osg::Vec3f mListenerPos; @@ -83,41 +95,47 @@ namespace MWSound int mPausedSoundTypes[BlockerType::MaxCount] = {}; - Sound *mUnderwaterSound; - Sound *mNearWaterSound; + Sound* mUnderwaterSound; + Sound* mNearWaterSound; - std::string mNextMusic; + VFS::Path::Normalized mNextMusic; bool mPlaybackPaused; RegionSoundSelector mRegionSoundSelector; float mTimePassed; - const ESM::Cell *mLastCell; + const MWWorld::Cell* mLastCell; Sound* mCurrentRegionSound; - Sound_Buffer *insertSound(const std::string &soundId, const ESM::Sound *sound); + Sound_Buffer* 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(const std::string &voicefile); + DecoderPtr loadVoice(VFS::Path::NormalizedView voicefile); SoundPtr getSoundRef(); StreamPtr getStreamRef(); - StreamPtr playVoice(DecoderPtr decoder, const osg::Vec3f &pos, bool playlocal); + StreamPtr playVoice(DecoderPtr decoder, const osg::Vec3f& pos, bool playlocal); + + void streamMusicFull(VFS::Path::NormalizedView filename); + void advanceMusic(VFS::Path::NormalizedView filename, float fadeOut = 1.f); - void streamMusicFull(const std::string& filename); - void advanceMusic(const std::string& filename); - void startRandomTitle(); + void cull3DSound(SoundBase* sound); + + bool remove3DSoundAtDistance(PlayMode mode, const MWWorld::ConstPtr& ptr) const; + + Sound* playSound(Sound_Buffer* 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, + PlayMode mode, float offset); void updateSounds(float duration); void updateRegionSound(float duration); void updateWaterSound(); void updateMusic(float duration); - float volumeFromType(Type type) const; - enum class WaterSoundAction { DoNothing, @@ -126,17 +144,17 @@ namespace MWSound PlaySound, }; - std::pair getWaterSoundAction(const WaterSoundUpdate& update, - const ESM::Cell* cell) const; + std::pair getWaterSoundAction( + const WaterSoundUpdate& update, const MWWorld::Cell* cell) const; - SoundManager(const SoundManager &rhs); - SoundManager& operator=(const SoundManager &rhs); + SoundManager(const SoundManager& rhs); + SoundManager& operator=(const SoundManager& rhs); protected: DecoderPtr getDecoder(); friend class OpenAL_Output; - void stopSound(Sound_Buffer *sfx, const MWWorld::ConstPtr &ptr); + void stopSound(Sound_Buffer* sfx, const MWWorld::ConstPtr& ptr); ///< Stop the given object from playing given sound buffer. public: @@ -145,38 +163,38 @@ namespace MWSound void processChangedSettings(const Settings::CategorySettingVector& settings) override; + bool isEnabled() const override { return mOutput->isInitialized(); } + ///< Returns true if sound system is enabled + void stopMusic() override; ///< Stops music if it's playing - void streamMusic(const std::string& filename) override; + MWSound::MusicType getMusicType() const override { return mMusicType; } + + void streamMusic(VFS::Path::NormalizedView filename, MWSound::MusicType type, float fade = 1.f) override; ///< Play a soundifle - /// \param filename name of a sound file in "Music/" in the data directory. + /// \param filename name of a sound file in the data directory. + /// \param type music type. + /// \param fade time in seconds to fade out current track before start this one. bool isMusicPlaying() override; ///< Returns true if music is playing - void playPlaylist(const std::string &playlist) override; - ///< Start playing music from the selected folder - /// \param name of the folder that contains the playlist - - void playTitleMusic() override; - ///< Start playing title music - - void say(const MWWorld::ConstPtr &reference, const std::string& filename) override; + void say(const MWWorld::ConstPtr& reference, VFS::Path::NormalizedView filename) override; ///< Make an actor say some text. - /// \param filename name of a sound file in "Sound/" in the data directory. + /// \param filename name of a sound file in the VFS - void say(const std::string& filename) override; + void say(VFS::Path::NormalizedView filename) override; ///< Say some text, without an actor ref - /// \param filename name of a sound file in "Sound/" in the data directory. + /// \param filename name of a sound file in the VFS - bool sayActive(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) const override; + bool sayActive(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) const override; ///< Is actor not speaking? - bool sayDone(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) const override; + bool sayDone(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) const override; ///< For scripting backward compatibility - void stopSay(const MWWorld::ConstPtr &reference=MWWorld::ConstPtr()) override; + void stopSay(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) override; ///< Stop an actor speaking float getSaySoundLoudness(const MWWorld::ConstPtr& reference) const override; @@ -184,55 +202,74 @@ namespace MWSound /// and get an average loudness value (scale [0,1]) at the current time position. /// If the actor is not saying anything, returns 0. - Stream *playTrack(const DecoderPtr& decoder, Type type) override; + Stream* playTrack(const DecoderPtr& decoder, Type type) override; ///< Play a 2D audio track, using a custom decoder - void stopTrack(Stream *stream) override; + void stopTrack(Stream* stream) override; ///< Stop the given audio track from playing - double getTrackTimeDelay(Stream *stream) override; + double getTrackTimeDelay(Stream* stream) override; ///< Retives the time delay, in seconds, of the audio track (must be a sound /// returned by \ref playTrack). Only intended to be called by the track /// decoder's read method. - Sound *playSound(const std::string& soundId, float volume, float pitch, Type type=Type::Sfx, PlayMode mode=PlayMode::Normal, float offset=0) override; + Sound* playSound(const ESM::RefId& soundId, float volume, float pitch, Type type = Type::Sfx, + PlayMode mode = PlayMode::Normal, float offset = 0) override; + ///< Play a sound, independently of 3D-position + ///< @param offset Number of seconds into the sound to start playback. + + Sound* playSound(std::string_view fileName, float volume, float pitch, Type type = Type::Sfx, + PlayMode mode = PlayMode::Normal, float offset = 0) override; ///< Play a sound, independently of 3D-position ///< @param offset Number of seconds into the sound to start playback. - Sound *playSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, - float volume, float pitch, Type type=Type::Sfx, - PlayMode mode=PlayMode::Normal, float offset=0) override; - ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless Play_NoTrack is specified. + Sound* playSound3D(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId, float volume, float pitch, + Type type = Type::Sfx, PlayMode mode = PlayMode::Normal, float offset = 0) override; + ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless + ///< Play_NoTrack is specified. ///< @param offset Number of seconds into the sound to start playback. - Sound *playSound3D(const osg::Vec3f& initialPos, const std::string& soundId, - float volume, float pitch, Type type, PlayMode mode, float offset=0) override; - ///< Play a 3D sound at \a initialPos. If the sound should be moving, it must be updated using Sound::setPosition. + Sound* playSound3D(const MWWorld::ConstPtr& reference, std::string_view fileName, float volume, float pitch, + Type type = Type::Sfx, PlayMode mode = PlayMode::Normal, float offset = 0) override; + ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless + ///< Play_NoTrack is specified. ///< @param offset Number of seconds into the sound to start playback. - void stopSound(Sound *sound) override; + Sound* playSound3D(const osg::Vec3f& initialPos, const ESM::RefId& soundId, float volume, float pitch, + Type type, PlayMode mode, float offset = 0) override; + ///< Play a 3D sound at \a initialPos. If the sound should be moving, it must be updated using + ///< Sound::setPosition. + ///< @param offset Number of seconds into the sound to start playback. + + void stopSound(Sound* sound) override; ///< Stop the given sound from playing /// @note no-op if \a sound is null - void stopSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId) override; - ///< Stop the given object from playing the given sound, + void stopSound3D(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId) override; + ///< Stop the given object from playing the given sound. + + void stopSound3D(const MWWorld::ConstPtr& reference, std::string_view fileName) override; + ///< Stop the given object from playing the given sound. - void stopSound3D(const MWWorld::ConstPtr &reference) override; + void stopSound3D(const MWWorld::ConstPtr& reference) override; ///< Stop the given object from playing all sounds. - void stopSound(const MWWorld::CellStore *cell) override; + void stopSound(const MWWorld::CellStore* cell) override; ///< Stop all sounds for the given cell. - void fadeOutSound3D(const MWWorld::ConstPtr &reference, const std::string& soundId, float duration) override; + void fadeOutSound3D(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId, float duration) override; ///< Fade out given sound (that is already playing) of given object ///< @param reference Reference to object, whose sound is faded out ///< @param soundId ID of the sound to fade out. ///< @param duration Time until volume reaches 0. - bool getSoundPlaying(const MWWorld::ConstPtr &reference, const std::string& soundId) const override; + bool getSoundPlaying(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId) const override; + ///< Is the given sound currently playing on the given object? + + bool getSoundPlaying(const MWWorld::ConstPtr& reference, std::string_view fileName) const override; ///< Is the given sound currently playing on the given object? - void pauseSounds(MWSound::BlockerType blocker, int types=int(Type::Mask)) override; + void pauseSounds(MWSound::BlockerType blocker, int types = int(Type::Mask)) override; ///< Pauses all currently playing sounds, including music. void resumeSounds(MWSound::BlockerType blocker) override; @@ -241,11 +278,12 @@ namespace MWSound void pausePlayback() override; void resumePlayback() override; - void update(float duration) override; + void update(float duration); - void setListenerPosDir(const osg::Vec3f &pos, const osg::Vec3f &dir, const osg::Vec3f &up, bool underwater) override; + void setListenerPosDir( + const osg::Vec3f& pos, const osg::Vec3f& dir, const osg::Vec3f& up, bool underwater) override; - void updatePtr (const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) override; + void updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) override; void clear() override; }; diff --git a/apps/openmw/mwsound/type.hpp b/apps/openmw/mwsound/type.hpp index 9f95bfa401e..d7bc6dd05cb 100644 --- a/apps/openmw/mwsound/type.hpp +++ b/apps/openmw/mwsound/type.hpp @@ -5,12 +5,12 @@ namespace MWSound { enum class Type { - Sfx = 1 << 4, /* Normal SFX sound */ - Voice = 1 << 5, /* Voice sound */ - Foot = 1 << 6, /* Footstep sound */ - Music = 1 << 7, /* Music track */ - Movie = 1 << 8, /* Movie audio track */ - Mask = Sfx | Voice | Foot | Music | Movie + Sfx = 1 << 5, /* Normal SFX sound */ + Voice = 1 << 6, /* Voice sound */ + Foot = 1 << 7, /* Footstep sound */ + Music = 1 << 8, /* Music track */ + Movie = 1 << 9, /* Movie audio track */ + Mask = Sfx | Voice | Foot | Music | Movie }; } diff --git a/apps/openmw/mwsound/volumesettings.cpp b/apps/openmw/mwsound/volumesettings.cpp deleted file mode 100644 index cc4eac3d6d7..00000000000 --- a/apps/openmw/mwsound/volumesettings.cpp +++ /dev/null @@ -1,56 +0,0 @@ -#include "volumesettings.hpp" - -#include - -#include - -namespace MWSound -{ - namespace - { - float clamp(float value) - { - return std::max(0.0f, std::min(1.0f, value)); - } - } - - VolumeSettings::VolumeSettings() - : mMasterVolume(clamp(Settings::Manager::getFloat("master volume", "Sound"))), - mSFXVolume(clamp(Settings::Manager::getFloat("sfx volume", "Sound"))), - mMusicVolume(clamp(Settings::Manager::getFloat("music volume", "Sound"))), - mVoiceVolume(clamp(Settings::Manager::getFloat("voice volume", "Sound"))), - mFootstepsVolume(clamp(Settings::Manager::getFloat("footsteps volume", "Sound"))) - { - } - - float VolumeSettings::getVolumeFromType(Type type) const - { - float volume = mMasterVolume; - - switch(type) - { - case Type::Sfx: - volume *= mSFXVolume; - break; - case Type::Voice: - volume *= mVoiceVolume; - break; - case Type::Foot: - volume *= mFootstepsVolume; - break; - case Type::Music: - volume *= mMusicVolume; - break; - case Type::Movie: - case Type::Mask: - break; - } - - return volume; - } - - void VolumeSettings::update() - { - *this = VolumeSettings(); - } -} diff --git a/apps/openmw/mwsound/volumesettings.hpp b/apps/openmw/mwsound/volumesettings.hpp deleted file mode 100644 index eec5f5c1bf1..00000000000 --- a/apps/openmw/mwsound/volumesettings.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef GAME_SOUND_VOLUMESETTINGS_H -#define GAME_SOUND_VOLUMESETTINGS_H - -#include "type.hpp" - -namespace MWSound -{ - class VolumeSettings - { - public: - VolumeSettings(); - - float getVolumeFromType(Type type) const; - - void update(); - - private: - float mMasterVolume; - float mSFXVolume; - float mMusicVolume; - float mVoiceVolume; - float mFootstepsVolume; - }; -} - -#endif diff --git a/apps/openmw/mwsound/watersoundupdater.cpp b/apps/openmw/mwsound/watersoundupdater.cpp index b1646c404fb..51385dcdefb 100644 --- a/apps/openmw/mwsound/watersoundupdater.cpp +++ b/apps/openmw/mwsound/watersoundupdater.cpp @@ -4,7 +4,7 @@ #include "../mwworld/cellstore.hpp" #include "../mwworld/ptr.hpp" -#include +#include #include @@ -53,7 +53,8 @@ namespace MWSound { const float terrainX = pos.x() - mSettings.mNearWaterRadius + x * step; const float terrainY = pos.y() - mSettings.mNearWaterRadius + y * step; - const float height = world.getTerrainHeightAt(osg::Vec3f(terrainX, terrainY, 0.0f)); + const float height = world.getTerrainHeightAt( + osg::Vec3f(terrainX, terrainY, 0.0f), cell.getCell()->getWorldSpace()); if (height < 0) underwaterPoints++; diff --git a/apps/openmw/mwsound/watersoundupdater.hpp b/apps/openmw/mwsound/watersoundupdater.hpp index b20b9db6c03..b0538b98487 100644 --- a/apps/openmw/mwsound/watersoundupdater.hpp +++ b/apps/openmw/mwsound/watersoundupdater.hpp @@ -1,6 +1,7 @@ #ifndef GAME_SOUND_WATERSOUNDUPDATER_H #define GAME_SOUND_WATERSOUNDUPDATER_H +#include "components/esm/refid.hpp" #include namespace MWBase @@ -21,33 +22,30 @@ namespace MWSound int mNearWaterPoints; float mNearWaterIndoorTolerance; float mNearWaterOutdoorTolerance; - std::string mNearWaterIndoorID; - std::string mNearWaterOutdoorID; + ESM::RefId mNearWaterIndoorID; + ESM::RefId mNearWaterOutdoorID; }; struct WaterSoundUpdate { - std::string mId; + ESM::RefId mId; float mVolume; }; class WaterSoundUpdater { - public: - explicit WaterSoundUpdater(const WaterSoundUpdaterSettings& settings); + public: + explicit WaterSoundUpdater(const WaterSoundUpdaterSettings& settings); - WaterSoundUpdate update(const MWWorld::ConstPtr& player, const MWBase::World& world) const; + WaterSoundUpdate update(const MWWorld::ConstPtr& player, const MWBase::World& world) const; - void setUnderwater(bool value) - { - mListenerUnderwater = value; - } + void setUnderwater(bool value) { mListenerUnderwater = value; } - private: - const WaterSoundUpdaterSettings mSettings; - bool mListenerUnderwater = false; + private: + const WaterSoundUpdaterSettings mSettings; + bool mListenerUnderwater = false; - float getVolume(const MWWorld::ConstPtr& player, const MWBase::World& world) const; + float getVolume(const MWWorld::ConstPtr& player, const MWBase::World& world) const; }; } diff --git a/apps/openmw/mwstate/character.cpp b/apps/openmw/mwstate/character.cpp index df1ab1bdfba..3c02311458b 100644 --- a/apps/openmw/mwstate/character.cpp +++ b/apps/openmw/mwstate/character.cpp @@ -1,153 +1,169 @@ #include "character.hpp" +#include #include +#include #include +#include +#include -#include - -#include +#include #include +#include +#include +#include -bool MWState::operator< (const Slot& left, const Slot& right) +bool MWState::operator<(const Slot& left, const Slot& right) { - return left.mTimeStamp& contentFiles) +{ + for (const std::string& c : contentFiles) + { + if (Misc::StringUtils::ciEndsWith(c, ".esm") || Misc::StringUtils::ciEndsWith(c, ".omwgame")) + return c; + } + return ""; +} -void MWState::Character::addSlot (const boost::filesystem::path& path, const std::string& game) +void MWState::Character::addSlot(const std::filesystem::path& path, const std::string& game) { Slot slot; slot.mPath = path; - slot.mTimeStamp = boost::filesystem::last_write_time (path); + slot.mTimeStamp = std::filesystem::last_write_time(path); ESM::ESMReader reader; - reader.open (slot.mPath.string()); + reader.open(slot.mPath); - if (reader.getRecName()!=ESM::REC_SAVE) + if (reader.getRecName() != ESM::REC_SAVE) return; // invalid save file -> ignore reader.getRecHeader(); - slot.mProfile.load (reader); + slot.mProfile.load(reader); - if (Misc::StringUtils::lowerCase (slot.mProfile.mContentFiles.at (0))!= - Misc::StringUtils::lowerCase (game)) + if (!Misc::StringUtils::ciEqual(getFirstGameFile(slot.mProfile.mContentFiles), game)) return; // this file is for a different game -> ignore - mSlots.push_back (slot); + mSlots.push_back(slot); } -void MWState::Character::addSlot (const ESM::SavedGame& profile) +void MWState::Character::addSlot(const ESM::SavedGame& profile) { Slot slot; std::ostringstream stream; // The profile description is user-supplied, so we need to escape the path - for (std::string::const_iterator it = profile.mDescription.begin(); it != profile.mDescription.end(); ++it) + Utf8Stream description(profile.mDescription); + while (!description.eof()) { - if (std::isalnum(*it)) // Ignores multibyte characters and non alphanumeric characters - stream << *it; + auto c = description.consume(); + if (c <= 0x7F && std::isalnum(c)) // Ignore multibyte characters and non alphanumeric characters + stream << static_cast(c); else - stream << "_"; + stream << '_'; } const std::string ext = ".omwsave"; slot.mPath = mPath / (stream.str() + ext); // Append an index if necessary to ensure a unique file - int i=0; - while (boost::filesystem::exists(slot.mPath)) + int i = 0; + while (std::filesystem::exists(slot.mPath)) { const std::string test = stream.str() + " - " + std::to_string(++i); slot.mPath = mPath / (test + ext); } slot.mProfile = profile; - slot.mTimeStamp = std::time (nullptr); + slot.mTimeStamp = std::filesystem::file_time_type::clock::now(); - mSlots.push_back (slot); + mSlots.push_back(slot); } -MWState::Character::Character (const boost::filesystem::path& saves, const std::string& game) -: mPath (saves) +MWState::Character::Character(const std::filesystem::path& saves, const std::string& game) + : mPath(saves) { - if (!boost::filesystem::is_directory (mPath)) + if (!std::filesystem::is_directory(mPath)) { - boost::filesystem::create_directories (mPath); + std::filesystem::create_directories(mPath); } else { - for (boost::filesystem::directory_iterator iter (mPath); - iter!=boost::filesystem::directory_iterator(); ++iter) + for (const auto& iter : std::filesystem::directory_iterator(mPath)) { - boost::filesystem::path slotPath = *iter; - try { - addSlot (slotPath, game); + addSlot(iter, game); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to add slot for game \"" << game << "\" save " << iter << ": " + << e.what(); } - catch (...) {} // ignoring bad saved game files for now } - std::sort (mSlots.begin(), mSlots.end()); + std::sort(mSlots.begin(), mSlots.end()); } } void MWState::Character::cleanup() { - if (mSlots.size() == 0) + if (mSlots.empty()) { // All slots are gone, no need to keep the empty directory - if (boost::filesystem::is_directory (mPath)) + if (std::filesystem::is_directory(mPath)) { // Extra safety check to make sure the directory is empty (e.g. slots failed to parse header) - boost::filesystem::directory_iterator it(mPath); - if (it == boost::filesystem::directory_iterator()) - boost::filesystem::remove_all(mPath); + std::filesystem::directory_iterator it(mPath); + if (it == std::filesystem::directory_iterator()) + std::filesystem::remove_all(mPath); } } } -const MWState::Slot *MWState::Character::createSlot (const ESM::SavedGame& profile) +const MWState::Slot* MWState::Character::createSlot(const ESM::SavedGame& profile) { - addSlot (profile); + addSlot(profile); return &mSlots.back(); } -void MWState::Character::deleteSlot (const Slot *slot) +void MWState::Character::deleteSlot(const Slot* slot) { - int index = slot - &mSlots[0]; + std::ptrdiff_t index = slot - mSlots.data(); - if (index<0 || index>=static_cast (mSlots.size())) + if (index < 0 || static_cast(index) >= mSlots.size()) { // sanity check; not entirely reliable - throw std::logic_error ("slot not found"); + throw std::logic_error("slot not found"); } - boost::filesystem::remove(slot->mPath); + std::filesystem::remove(slot->mPath); - mSlots.erase (mSlots.begin()+index); + mSlots.erase(mSlots.begin() + index); } -const MWState::Slot *MWState::Character::updateSlot (const Slot *slot, const ESM::SavedGame& profile) +const MWState::Slot* MWState::Character::updateSlot(const Slot* slot, const ESM::SavedGame& profile) { - int index = slot - &mSlots[0]; + std::ptrdiff_t index = slot - mSlots.data(); - if (index<0 || index>=static_cast (mSlots.size())) + if (index < 0 || static_cast(index) >= mSlots.size()) { // sanity check; not entirely reliable - throw std::logic_error ("slot not found"); + throw std::logic_error("slot not found"); } Slot newSlot = *slot; newSlot.mProfile = profile; - newSlot.mTimeStamp = std::time (nullptr); + newSlot.mTimeStamp = std::filesystem::file_time_type::clock::now(); - mSlots.erase (mSlots.begin()+index); + mSlots.erase(mSlots.begin() + index); - mSlots.push_back (newSlot); + mSlots.push_back(newSlot); return &mSlots.back(); } @@ -162,26 +178,21 @@ MWState::Character::SlotIterator MWState::Character::end() const return mSlots.rend(); } -ESM::SavedGame MWState::Character::getSignature() const +const ESM::SavedGame& MWState::Character::getSignature() const { if (mSlots.empty()) - throw std::logic_error ("character signature not available"); - - std::vector::const_iterator iter (mSlots.begin()); + throw std::logic_error("character signature not available"); - Slot slot = *iter; + const auto tiePlayerLevelAndTimeStamp + = [](const Slot& v) { return std::tie(v.mProfile.mPlayerLevel, v.mTimeStamp); }; - for (++iter; iter!=mSlots.end(); ++iter) - if (iter->mProfile.mPlayerLevel>slot.mProfile.mPlayerLevel) - slot = *iter; - else if (iter->mProfile.mPlayerLevel==slot.mProfile.mPlayerLevel && - iter->mTimeStamp>slot.mTimeStamp) - slot = *iter; + const auto lessByPlayerLevelAndTimeStamp + = [&](const Slot& l, const Slot& r) { return tiePlayerLevelAndTimeStamp(l) < tiePlayerLevelAndTimeStamp(r); }; - return slot.mProfile; + return std::max_element(mSlots.begin(), mSlots.end(), lessByPlayerLevelAndTimeStamp)->mProfile; } -const boost::filesystem::path& MWState::Character::getPath() const +const std::filesystem::path& MWState::Character::getPath() const { return mPath; } diff --git a/apps/openmw/mwstate/character.hpp b/apps/openmw/mwstate/character.hpp index 32c79a183ec..3c68d9f4903 100644 --- a/apps/openmw/mwstate/character.hpp +++ b/apps/openmw/mwstate/character.hpp @@ -1,69 +1,68 @@ #ifndef GAME_STATE_CHARACTER_H #define GAME_STATE_CHARACTER_H -#include +#include -#include +#include namespace MWState { struct Slot { - boost::filesystem::path mPath; + std::filesystem::path mPath; ESM::SavedGame mProfile; - std::time_t mTimeStamp; + std::filesystem::file_time_type mTimeStamp; }; - bool operator< (const Slot& left, const Slot& right); + bool operator<(const Slot& left, const Slot& right); + + std::string getFirstGameFile(const std::vector& contentFiles); class Character { - public: - - typedef std::vector::const_reverse_iterator SlotIterator; - - private: - - boost::filesystem::path mPath; - std::vector mSlots; + public: + typedef std::vector::const_reverse_iterator SlotIterator; - void addSlot (const boost::filesystem::path& path, const std::string& game); + private: + std::filesystem::path mPath; + std::vector mSlots; - void addSlot (const ESM::SavedGame& profile); + void addSlot(const std::filesystem::path& path, const std::string& game); - public: + void addSlot(const ESM::SavedGame& profile); - Character (const boost::filesystem::path& saves, const std::string& game); + public: + Character(const std::filesystem::path& saves, const std::string& game); - void cleanup(); - ///< Delete the directory we used, if it is empty + void cleanup(); + ///< Delete the directory we used, if it is empty - const Slot *createSlot (const ESM::SavedGame& profile); - ///< Create new slot. - /// - /// \attention The ownership of the slot is not transferred. + const Slot* createSlot(const ESM::SavedGame& profile); + ///< Create new slot. + /// + /// \attention The ownership of the slot is not transferred. - /// \note Slot must belong to this character. - /// - /// \attention The \a slot pointer will be invalidated by this call. - void deleteSlot (const Slot *slot); + /// \note Slot must belong to this character. + /// + /// \attention The \a slot pointer will be invalidated by this call. + void deleteSlot(const Slot* slot); - const Slot *updateSlot (const Slot *slot, const ESM::SavedGame& profile); - /// \note Slot must belong to this character. - /// - /// \attention The \a slot pointer will be invalidated by this call. + const Slot* updateSlot(const Slot* slot, const ESM::SavedGame& profile); + /// \note Slot must belong to this character. + /// + /// \attention The \a slot pointer will be invalidated by this call. - SlotIterator begin() const; - ///< Any call to createSlot and updateSlot can invalidate the returned iterator. + SlotIterator begin() const; + ///< Any call to createSlot and updateSlot can invalidate the returned iterator. - SlotIterator end() const; + SlotIterator end() const; - const boost::filesystem::path& getPath() const; + const std::filesystem::path& getPath() const; - ESM::SavedGame getSignature() const; - ///< Return signature information for this character. - /// - /// \attention This function must not be called if there are no slots. + const ESM::SavedGame& getSignature() const; + ///< Return signature information for this character. + /// + /// \attention This function must not be called if there are no slots. }; } diff --git a/apps/openmw/mwstate/charactermanager.cpp b/apps/openmw/mwstate/charactermanager.cpp index a324dfe0f76..c41ddd42c05 100644 --- a/apps/openmw/mwstate/charactermanager.cpp +++ b/apps/openmw/mwstate/charactermanager.cpp @@ -1,42 +1,44 @@ #include "charactermanager.hpp" #include +#include #include +#include -#include +#include -MWState::CharacterManager::CharacterManager (const boost::filesystem::path& saves, - const std::string& game) -: mPath (saves), mCurrent (nullptr), mGame (game) +MWState::CharacterManager::CharacterManager(std::filesystem::path saves, const std::vector& contentFiles) + : mPath(std::move(saves)) + , mCurrent(nullptr) + , mGame(getFirstGameFile(contentFiles)) { - if (!boost::filesystem::is_directory (mPath)) + if (!std::filesystem::is_directory(mPath)) { - boost::filesystem::create_directories (mPath); + std::filesystem::create_directories(mPath); } else { - for (boost::filesystem::directory_iterator iter (mPath); - iter!=boost::filesystem::directory_iterator(); ++iter) + for (std::filesystem::directory_iterator iter(mPath); iter != std::filesystem::directory_iterator(); ++iter) { - boost::filesystem::path characterDir = *iter; + std::filesystem::path characterDir = *iter; - if (boost::filesystem::is_directory (characterDir)) + if (std::filesystem::is_directory(characterDir)) { - Character character (characterDir, mGame); + Character character(characterDir, mGame); - if (character.begin()!=character.end()) - mCharacters.push_back (character); + if (character.begin() != character.end()) + mCharacters.push_back(character); } } } } -MWState::Character *MWState::CharacterManager::getCurrentCharacter () +MWState::Character* MWState::CharacterManager::getCurrentCharacter() { return mCurrent; } -void MWState::CharacterManager::deleteSlot(const MWState::Character *character, const MWState::Slot *slot) +void MWState::CharacterManager::deleteSlot(const MWState::Character* character, const MWState::Slot* slot) { std::list::iterator it = findCharacter(character); @@ -57,24 +59,26 @@ MWState::Character* MWState::CharacterManager::createCharacter(const std::string std::ostringstream stream; // The character name is user-supplied, so we need to escape the path - for (std::string::const_iterator it = name.begin(); it != name.end(); ++it) + Utf8Stream nameStream(name); + while (!nameStream.eof()) { - if (std::isalnum(*it)) // Ignores multibyte characters and non alphanumeric characters - stream << *it; + auto c = nameStream.consume(); + if (c <= 0x7F && std::isalnum(c)) // Ignore multibyte characters and non alphanumeric characters + stream << static_cast(c); else - stream << "_"; + stream << '_'; } - boost::filesystem::path path = mPath / stream.str(); + std::filesystem::path path = mPath / stream.str(); // Append an index if necessary to ensure a unique directory - int i=0; - while (boost::filesystem::exists(path)) + int i = 0; + while (std::filesystem::exists(path)) { - std::ostringstream test; - test << stream.str(); - test << " - " << ++i; - path = mPath / test.str(); + std::ostringstream test; + test << stream.str(); + test << " - " << ++i; + path = mPath / test.str(); } mCharacters.emplace_back(path, mGame); @@ -90,11 +94,11 @@ std::list::iterator MWState::CharacterManager::findCharacter break; } if (it == mCharacters.end()) - throw std::logic_error ("invalid character"); + throw std::logic_error("invalid character"); return it; } -void MWState::CharacterManager::setCurrentCharacter (const Character *character) +void MWState::CharacterManager::setCurrentCharacter(const Character* character) { if (!character) mCurrent = nullptr; @@ -106,7 +110,6 @@ void MWState::CharacterManager::setCurrentCharacter (const Character *character) } } - std::list::const_iterator MWState::CharacterManager::begin() const { return mCharacters.begin(); diff --git a/apps/openmw/mwstate/charactermanager.hpp b/apps/openmw/mwstate/charactermanager.hpp index 2daf73401fb..dac189e68a6 100644 --- a/apps/openmw/mwstate/charactermanager.hpp +++ b/apps/openmw/mwstate/charactermanager.hpp @@ -1,7 +1,8 @@ #ifndef GAME_STATE_CHARACTERMANAGER_H #define GAME_STATE_CHARACTERMANAGER_H -#include +#include +#include #include "character.hpp" @@ -9,42 +10,40 @@ namespace MWState { class CharacterManager { - boost::filesystem::path mPath; + std::filesystem::path mPath; - // Uses std::list, so that mCurrent stays valid when characters are deleted - std::list mCharacters; + // Uses std::list, so that mCurrent stays valid when characters are deleted + std::list mCharacters; - Character *mCurrent; - std::string mGame; + Character* mCurrent; + std::string mGame; - private: + private: + CharacterManager(const CharacterManager&); + ///< Not implemented - CharacterManager (const CharacterManager&); - ///< Not implemented + CharacterManager& operator=(const CharacterManager&); + ///< Not implemented - CharacterManager& operator= (const CharacterManager&); - ///< Not implemented + std::list::iterator findCharacter(const MWState::Character* character); - std::list::iterator findCharacter(const MWState::Character* character); + public: + CharacterManager(std::filesystem::path saves, const std::vector& contentFiles); - public: + Character* getCurrentCharacter(); + ///< @note May return null - CharacterManager (const boost::filesystem::path& saves, const std::string& game); + void deleteSlot(const MWState::Character* character, const MWState::Slot* slot); - Character *getCurrentCharacter (); - ///< @note May return null + Character* createCharacter(const std::string& name); + ///< Create new character within saved game management + /// \param name Name for the character (does not need to be unique) - void deleteSlot(const MWState::Character *character, const MWState::Slot *slot); + void setCurrentCharacter(const Character* character); - Character* createCharacter(const std::string& name); - ///< Create new character within saved game management - /// \param name Name for the character (does not need to be unique) + std::list::const_iterator begin() const; - void setCurrentCharacter (const Character *character); - - std::list::const_iterator begin() const; - - std::list::const_iterator end() const; + std::list::const_iterator end() const; }; } diff --git a/apps/openmw/mwstate/quicksavemanager.cpp b/apps/openmw/mwstate/quicksavemanager.cpp index bf178152077..1028866aacc 100644 --- a/apps/openmw/mwstate/quicksavemanager.cpp +++ b/apps/openmw/mwstate/quicksavemanager.cpp @@ -1,26 +1,26 @@ #include "quicksavemanager.hpp" -MWState::QuickSaveManager::QuickSaveManager(std::string &saveName, unsigned int maxSaves) - : mSaveName(saveName) - , mMaxSaves(maxSaves) - , mSlotsVisited(0) - , mOldestSlotVisited(nullptr) +MWState::QuickSaveManager::QuickSaveManager(std::string& saveName, unsigned int maxSaves) + : mSaveName(saveName) + , mMaxSaves(maxSaves) + , mSlotsVisited(0) + , mOldestSlotVisited(nullptr) { } -void MWState::QuickSaveManager::visitSave(const Slot *saveSlot) +void MWState::QuickSaveManager::visitSave(const Slot* saveSlot) { - if(mSaveName == saveSlot->mProfile.mDescription) + if (mSaveName == saveSlot->mProfile.mDescription) { ++mSlotsVisited; - if(isOldestSave(saveSlot)) + if (isOldestSave(saveSlot)) mOldestSlotVisited = saveSlot; } } -bool MWState::QuickSaveManager::isOldestSave(const Slot *compare) const +bool MWState::QuickSaveManager::isOldestSave(const Slot* compare) const { - if(mOldestSlotVisited == nullptr) + if (mOldestSlotVisited == nullptr) return true; return (compare->mTimeStamp <= mOldestSlotVisited->mTimeStamp); } @@ -30,9 +30,9 @@ bool MWState::QuickSaveManager::shouldCreateNewSlot() const return (mSlotsVisited < mMaxSaves); } -const MWState::Slot *MWState::QuickSaveManager::getNextQuickSaveSlot() +const MWState::Slot* MWState::QuickSaveManager::getNextQuickSaveSlot() { - if(shouldCreateNewSlot()) + if (shouldCreateNewSlot()) return nullptr; return mOldestSlotVisited; } diff --git a/apps/openmw/mwstate/quicksavemanager.hpp b/apps/openmw/mwstate/quicksavemanager.hpp index 3272b24b516..3fa24d34d4e 100644 --- a/apps/openmw/mwstate/quicksavemanager.hpp +++ b/apps/openmw/mwstate/quicksavemanager.hpp @@ -5,26 +5,30 @@ #include "character.hpp" -namespace MWState{ - class QuickSaveManager{ +namespace MWState +{ + class QuickSaveManager + { std::string mSaveName; unsigned int mMaxSaves; unsigned int mSlotsVisited; - const Slot *mOldestSlotVisited; + const Slot* mOldestSlotVisited; + private: bool shouldCreateNewSlot() const; - bool isOldestSave(const Slot *compare) const; + bool isOldestSave(const Slot* compare) const; + public: - QuickSaveManager(std::string &saveName, unsigned int maxSaves); + QuickSaveManager(std::string& saveName, unsigned int maxSaves); ///< A utility class to manage multiple quicksave slots /// /// \param saveName The name of the save ("QuickSave", "AutoSave", etc) /// \param maxSaves The maximum number of save slots to create before recycling old ones - void visitSave(const Slot *saveSlot); + void visitSave(const Slot* saveSlot); ///< Visits the given \a slot \a - const Slot *getNextQuickSaveSlot(); + const Slot* getNextQuickSaveSlot(); ///< Get the slot that the next quicksave should use. /// ///\return Either the oldest quicksave slot visited, or nullptr if a new slot can be made diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index ffef9d74a91..4f5ecfc8eb2 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -1,48 +1,57 @@ #include "statemanagerimp.hpp" +#include + +#include + #include -#include -#include -#include -#include +#include +#include +#include +#include + +#include #include -#include +#include +#include +#include #include #include -#include -#include - +#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include "../mwbase/inputmanager.hpp" #include "../mwbase/journal.hpp" -#include "../mwbase/dialoguemanager.hpp" -#include "../mwbase/windowmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" -#include "../mwbase/inputmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" -#include "../mwworld/player.hpp" -#include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/globals.hpp" +#include "../mwworld/scene.hpp" +#include "../mwworld/worldmodel.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/npcstats.hpp" #include "../mwscript/globalscripts.hpp" #include "quicksavemanager.hpp" -void MWState::StateManager::cleanup (bool force) +void MWState::StateManager::cleanup(bool force) { - if (mState!=State_NoGame || force) + if (mState != State_NoGame || force) { MWBase::Environment::get().getSoundManager()->clear(); MWBase::Environment::get().getDialogueManager()->clear(); @@ -53,32 +62,35 @@ void MWState::StateManager::cleanup (bool force) MWBase::Environment::get().getInputManager()->clear(); MWBase::Environment::get().getMechanicsManager()->clear(); - mState = State_NoGame; mCharacterManager.setCurrentCharacter(nullptr); mTimePlayed = 0; - + mLastSavegame.clear(); MWMechanics::CreatureStats::cleanup(); + + mState = State_NoGame; + MWBase::Environment::get().getLuaManager()->noGame(); + } + else + { + // TODO: do we need this cleanup? + MWBase::Environment::get().getLuaManager()->clear(); } } -std::map MWState::StateManager::buildContentFileIndexMap (const ESM::ESMReader& reader) - const +std::map MWState::StateManager::buildContentFileIndexMap(const ESM::ESMReader& reader) const { - const std::vector& current = - MWBase::Environment::get().getWorld()->getContentFiles(); + const std::vector& current = MWBase::Environment::get().getWorld()->getContentFiles(); const std::vector& prev = reader.getGameFiles(); std::map map; - for (int iPrev = 0; iPrev (prev.size()); ++iPrev) + for (int iPrev = 0; iPrev < static_cast(prev.size()); ++iPrev) { - std::string id = Misc::StringUtils::lowerCase (prev[iPrev].name); - - for (int iCurrent = 0; iCurrent (current.size()); ++iCurrent) - if (id==Misc::StringUtils::lowerCase (current[iCurrent])) + for (int iCurrent = 0; iCurrent < static_cast(current.size()); ++iCurrent) + if (Misc::StringUtils::ciEqual(prev[iPrev].name, current[iCurrent])) { - map.insert (std::make_pair (iPrev, iCurrent)); + map.insert(std::make_pair(iPrev, iCurrent)); break; } } @@ -86,10 +98,13 @@ std::map MWState::StateManager::buildContentFileIndexMap (const ESM::E return map; } -MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::string& game) -: mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, game), mTimePlayed (0) +MWState::StateManager::StateManager(const std::filesystem::path& saves, const std::vector& contentFiles) + : mQuitRequest(false) + , mAskLoadRecent(false) + , mState(State_NoGame) + , mCharacterManager(saves, contentFiles) + , mTimePlayed(0) { - } void MWState::StateManager::requestQuit() @@ -107,23 +122,37 @@ void MWState::StateManager::askLoadRecent() if (MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_MainMenu) return; - if( !mAskLoadRecent ) + if (!mAskLoadRecent) { - const MWState::Character* character = getCurrentCharacter(); - if(!character || character->begin() == character->end())//no saves + if (mLastSavegame.empty()) // no saves { - MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); } else { - MWState::Slot lastSave = *character->begin(); + std::string saveName = Files::pathToUnicodeString(mLastSavegame.filename()); + // Assume the last saved game belongs to the current character's slot list. + const Character* character = getCurrentCharacter(); + if (character) + { + for (const auto& slot : *character) + { + if (slot.mPath == mLastSavegame) + { + saveName = slot.mProfile.mDescription; + break; + } + } + } + std::vector buttons; - buttons.emplace_back("#{sYes}"); - buttons.emplace_back("#{sNo}"); - std::string tag("%s"); - std::string message = MWBase::Environment::get().getWindowManager()->getGameSettingString("sLoadLastSaveMsg", tag); + buttons.emplace_back("#{Interface:Yes}"); + buttons.emplace_back("#{Interface:No}"); + std::string message + = MWBase::Environment::get().getL10nManager()->getMessage("OMWEngine", "AskLoadLastSave"); + std::string_view tag = "%s"; size_t pos = message.find(tag); - message.replace(pos, tag.length(), lastSave.mProfile.mDescription); + message.replace(pos, tag.length(), saveName); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); mAskLoadRecent = true; } @@ -135,21 +164,21 @@ MWState::StateManager::State MWState::StateManager::getState() const return mState; } -void MWState::StateManager::newGame (bool bypass) +void MWState::StateManager::newGame(bool bypass) { cleanup(); if (!bypass) - MWBase::Environment::get().getWindowManager()->setNewGame (true); + MWBase::Environment::get().getWindowManager()->setNewGame(true); try { Log(Debug::Info) << "Starting a new game"; MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); - - MWBase::Environment::get().getWorld()->startNewGame (bypass); + MWBase::Environment::get().getWorld()->startNewGame(bypass); mState = State_Running; + MWBase::Environment::get().getLuaManager()->newGameStarted(); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0); MWBase::Environment::get().getWindowManager()->fadeScreenIn(1); @@ -160,12 +189,12 @@ void MWState::StateManager::newGame (bool bypass) error << "Failed to start new game: " << e.what(); Log(Debug::Error) << error.str(); - cleanup (true); + cleanup(true); - MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); std::vector buttons; - buttons.emplace_back("#{sOk}"); + buttons.emplace_back("#{Interface:OK}"); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error.str(), buttons); } } @@ -173,23 +202,31 @@ void MWState::StateManager::newGame (bool bypass) void MWState::StateManager::endGame() { mState = State_Ended; + MWBase::Environment::get().getLuaManager()->gameEnded(); } void MWState::StateManager::resumeGame() { mState = State_Running; + MWBase::Environment::get().getLuaManager()->gameLoaded(); } -void MWState::StateManager::saveGame (const std::string& description, const Slot *slot) +void MWState::StateManager::saveGame(std::string_view description, const Slot* slot) { + MWBase::Environment::get().getLuaManager()->applyDelayedActions(); + MWState::Character* character = getCurrentCharacter(); try { + const auto start = std::chrono::steady_clock::now(); + + MWBase::Environment::get().getWindowManager()->asyncPrepareSaveMap(); + if (!character) { MWWorld::ConstPtr player = MWMechanics::getPlayer(); - std::string name = player.get()->mBase->mName; + const std::string& name = player.get()->mBase->mName; character = mCharacterManager.createCharacter(name); mCharacterManager.setCurrentCharacter(character); @@ -204,26 +241,31 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot profile.mContentFiles = world.getContentFiles(); profile.mPlayerName = player.get()->mBase->mName; - profile.mPlayerLevel = player.getClass().getNpcStats (player).getLevel(); + profile.mPlayerLevel = player.getClass().getNpcStats(player).getLevel(); - std::string classId = player.get()->mBase->mClass; + const ESM::RefId& classId = player.get()->mBase->mClass; if (world.getStore().get().isDynamic(classId)) profile.mPlayerClassName = world.getStore().get().find(classId)->mName; else profile.mPlayerClassId = classId; - profile.mPlayerCell = world.getCellName(); - profile.mInGameTime = world.getEpochTimeStamp(); + const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); + + profile.mPlayerCellName = world.getCellName(); + profile.mInGameTime = world.getTimeManager()->getEpochTimeStamp(); profile.mTimePlayed = mTimePlayed; profile.mDescription = description; + profile.mCurrentDay = world.getTimeManager()->getTimeStamp().getDay(); + profile.mCurrentHealth = stats.getHealth().getCurrent(); + profile.mMaximumHealth = stats.getHealth().getModified(); - Log(Debug::Info) << "Making a screenshot for saved game '" << description << "'";; + Log(Debug::Info) << "Making a screenshot for saved game '" << description << "'"; writeScreenshot(profile.mScreenshot); if (!slot) - slot = character->createSlot (profile); + slot = character->createSlot(profile); else - slot = character->updateSlot (slot, profile); + slot = character->updateSlot(slot, profile); // Make sure the animation state held by references is up to date before saving the game. MWBase::Environment::get().getMechanicsManager()->persistAnimationStates(); @@ -239,7 +281,7 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot for (const std::string& contentFile : MWBase::Environment::get().getWorld()->getContentFiles()) writer.addMaster(contentFile, 0); // not using the size information anyway -> use value of 0 - writer.setFormat (ESM::SavedGame::sCurrentFormat); + writer.setFormatVersion(ESM::CurrentSaveGameFormatVersion); // all unused writer.setVersion(0); @@ -247,40 +289,45 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot writer.setAuthor(""); writer.setDescription(""); - int recordCount = 1 // saved game header - +MWBase::Environment::get().getJournal()->countSavedGameRecords() - +MWBase::Environment::get().getWorld()->countSavedGameRecords() - +MWBase::Environment::get().getScriptManager()->getGlobalScripts().countSavedGameRecords() - +MWBase::Environment::get().getDialogueManager()->countSavedGameRecords() - +MWBase::Environment::get().getWindowManager()->countSavedGameRecords() - +MWBase::Environment::get().getMechanicsManager()->countSavedGameRecords() - +MWBase::Environment::get().getInputManager()->countSavedGameRecords(); - writer.setRecordCount (recordCount); + int recordCount = 1 // saved game header + + MWBase::Environment::get().getJournal()->countSavedGameRecords() + + MWBase::Environment::get().getLuaManager()->countSavedGameRecords() + + MWBase::Environment::get().getWorld()->countSavedGameRecords() + + MWBase::Environment::get().getScriptManager()->getGlobalScripts().countSavedGameRecords() + + MWBase::Environment::get().getDialogueManager()->countSavedGameRecords() + + MWBase::Environment::get().getMechanicsManager()->countSavedGameRecords() + + MWBase::Environment::get().getInputManager()->countSavedGameRecords() + + MWBase::Environment::get().getWindowManager()->countSavedGameRecords(); + writer.setRecordCount(recordCount); - writer.save (stream); + writer.save(stream); Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); // Using only Cells for progress information, since they typically have the largest records by far listener.setProgressRange(MWBase::Environment::get().getWorld()->countSavedGameCells()); - listener.setLabel("#{sNotifyMessage4}", true); + listener.setLabel("#{OMWEngine:SavingInProgress}", true); Loading::ScopedLoad load(&listener); - writer.startRecord (ESM::REC_SAVE); - slot->mProfile.save (writer); - writer.endRecord (ESM::REC_SAVE); - - MWBase::Environment::get().getJournal()->write (writer, listener); - MWBase::Environment::get().getDialogueManager()->write (writer, listener); - MWBase::Environment::get().getWorld()->write (writer, listener); - MWBase::Environment::get().getScriptManager()->getGlobalScripts().write (writer, listener); - MWBase::Environment::get().getWindowManager()->write(writer, listener); + writer.startRecord(ESM::REC_SAVE); + slot->mProfile.save(writer); + writer.endRecord(ESM::REC_SAVE); + + MWBase::Environment::get().getJournal()->write(writer, listener); + MWBase::Environment::get().getDialogueManager()->write(writer, listener); + // LuaManager::write should be called before World::write because world also saves + // local scripts that depend on LuaManager. + MWBase::Environment::get().getLuaManager()->write(writer, listener); + MWBase::Environment::get().getWorld()->write(writer, listener); + MWBase::Environment::get().getScriptManager()->getGlobalScripts().write(writer, listener); MWBase::Environment::get().getMechanicsManager()->write(writer, listener); MWBase::Environment::get().getInputManager()->write(writer, listener); + MWBase::Environment::get().getWindowManager()->write(writer, listener); // Ensure we have written the number of records that was estimated - if (writer.getRecordCount() != recordCount+1) // 1 extra for TES3 record - Log(Debug::Warning) << "Warning: number of written savegame records does not match. Estimated: " << recordCount+1 << ", written: " << writer.getRecordCount(); + if (writer.getRecordCount() != recordCount + 1) // 1 extra for TES3 record + Log(Debug::Warning) << "Warning: number of written savegame records does not match. Estimated: " + << recordCount + 1 << ", written: " << writer.getRecordCount(); writer.close(); @@ -288,14 +335,20 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot throw std::runtime_error("Write operation failed (memory stream)"); // All good, write to file - boost::filesystem::ofstream filestream (slot->mPath, std::ios::binary); + std::ofstream filestream(slot->mPath, std::ios::binary); filestream << stream.rdbuf(); if (filestream.fail()) throw std::runtime_error("Write operation failed (file stream)"); - Settings::Manager::setString ("character", "Saves", - slot->mPath.parent_path().filename().string()); + Settings::saves().mCharacter.set(Files::pathToUnicodeString(slot->mPath.parent_path().filename())); + mLastSavegame = slot->mPath; + + const auto finish = std::chrono::steady_clock::now(); + + Log(Debug::Info) << '\'' << description << "' is saved in " + << std::chrono::duration_cast>(finish - start).count() + << "ms"; } catch (const std::exception& e) { @@ -305,11 +358,11 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot Log(Debug::Error) << error.str(); std::vector buttons; - buttons.emplace_back("#{sOk}"); + buttons.emplace_back("#{Interface:OK}"); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error.str(), buttons); // If no file was written, clean up the slot - if (character && slot && !boost::filesystem::exists(slot->mPath)) + if (character && slot && !std::filesystem::exists(slot->mPath)) { character->deleteSlot(slot); character->cleanup(); @@ -317,47 +370,43 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot } } -void MWState::StateManager::quickSave (std::string name) +void MWState::StateManager::quickSave(std::string name) { - if (!(mState==State_Running && - MWBase::Environment::get().getWorld()->getGlobalInt ("chargenstate")==-1 // char gen + if (!(mState == State_Running + && MWBase::Environment::get().getWorld()->getGlobalInt(MWWorld::Globals::sCharGenState) == -1 // char gen && MWBase::Environment::get().getWindowManager()->isSavingAllowed())) { - //You can not save your game right now - MWBase::Environment::get().getWindowManager()->messageBox("#{sSaveGameDenied}"); + // You can not save your game right now + MWBase::Environment::get().getWindowManager()->messageBox("#{OMWEngine:SaveGameDenied}"); return; } - int maxSaves = Settings::Manager::getInt("max quicksaves", "Saves"); - if(maxSaves < 1) - maxSaves = 1; - - Character* currentCharacter = getCurrentCharacter(); //Get current character - QuickSaveManager saveFinder = QuickSaveManager(name, maxSaves); + Character* currentCharacter = getCurrentCharacter(); // Get current character + QuickSaveManager saveFinder(name, Settings::saves().mMaxQuicksaves); if (currentCharacter) { for (auto& save : *currentCharacter) { - //Visiting slots allows the quicksave finder to find the oldest quicksave + // Visiting slots allows the quicksave finder to find the oldest quicksave saveFinder.visitSave(&save); } } - //Once all the saves have been visited, the save finder can tell us which - //one to replace (or create) + // Once all the saves have been visited, the save finder can tell us which + // one to replace (or create) saveGame(name, saveFinder.getNextQuickSaveSlot()); } -void MWState::StateManager::loadGame(const std::string& filepath) +void MWState::StateManager::loadGame(const std::filesystem::path& filepath) { for (const auto& character : mCharacterManager) { for (const auto& slot : character) { - if (slot.mPath == boost::filesystem::path(filepath)) + if (slot.mPath == filepath) { - loadGame(&character, slot.mPath.string()); + loadGame(&character, slot.mPath); return; } } @@ -367,26 +416,65 @@ void MWState::StateManager::loadGame(const std::string& filepath) loadGame(character, filepath); } -void MWState::StateManager::loadGame (const Character *character, const std::string& filepath) +struct SaveFormatVersionError : public std::exception +{ + using std::exception::exception; + + SaveFormatVersionError(ESM::FormatVersion savegameFormat, const std::string& message) + : mSavegameFormat(savegameFormat) + , mErrorMessage(message) + { + } + + const char* what() const noexcept override { return mErrorMessage.c_str(); } + ESM::FormatVersion getFormatVersion() const { return mSavegameFormat; } + +protected: + ESM::FormatVersion mSavegameFormat = ESM::DefaultFormatVersion; + std::string mErrorMessage; +}; + +struct SaveVersionTooOldError : SaveFormatVersionError +{ + SaveVersionTooOldError(ESM::FormatVersion savegameFormat) + : SaveFormatVersionError(savegameFormat, "format version " + std::to_string(savegameFormat) + " is too old") + { + } +}; + +struct SaveVersionTooNewError : SaveFormatVersionError +{ + SaveVersionTooNewError(ESM::FormatVersion savegameFormat) + : SaveFormatVersionError(savegameFormat, "format version " + std::to_string(savegameFormat) + " is too new") + { + } +}; + +void MWState::StateManager::loadGame(const Character* character, const std::filesystem::path& filepath) { try { cleanup(); - Log(Debug::Info) << "Reading save file " << boost::filesystem::path(filepath).filename().string(); + Log(Debug::Info) << "Reading save file " << filepath.filename(); ESM::ESMReader reader; - reader.open (filepath); + reader.open(filepath); - if (reader.getFormat() > ESM::SavedGame::sCurrentFormat) - throw std::runtime_error("This save file was created using a newer version of OpenMW and is thus not supported. Please upgrade to the newest OpenMW version to load this file."); + ESM::FormatVersion version = reader.getFormatVersion(); + if (version > ESM::CurrentSaveGameFormatVersion) + throw SaveVersionTooNewError(version); + else if (version < ESM::MinSupportedSaveGameFormatVersion) + throw SaveVersionTooOldError(version); - std::map contentFileMap = buildContentFileIndexMap (reader); + std::map contentFileMap = buildContentFileIndexMap(reader); + reader.setContentFileMapping(&contentFileMap); + MWBase::Environment::get().getLuaManager()->setContentFileMapping(contentFileMap); Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); listener.setProgressRange(100); - listener.setLabel("#{sLoadingMessage14}"); + listener.setLabel("#{OMWEngine:LoadingInProgress}"); Loading::ScopedLoad load(&listener); @@ -399,36 +487,40 @@ void MWState::StateManager::loadGame (const Character *character, const std::str ESM::NAME n = reader.getRecName(); reader.getRecHeader(); - switch (n.intval) + switch (n.toInt()) { case ESM::REC_SAVE: + { + ESM::SavedGame profile; + profile.load(reader); + const auto& selectedContentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); + auto missingFiles = profile.getMissingContentFiles(selectedContentFiles); + if (!missingFiles.empty() && !confirmLoading(missingFiles)) { - ESM::SavedGame profile; - profile.load(reader); - if (!verifyProfile(profile)) - { - cleanup (true); - MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); - return; - } - mTimePlayed = profile.mTimePlayed; - Log(Debug::Info) << "Loading saved game '" << profile.mDescription << "' for character '" << profile.mPlayerName << "'"; + cleanup(true); + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); + return; } - break; + mTimePlayed = profile.mTimePlayed; + Log(Debug::Info) << "Loading saved game '" << profile.mDescription << "' for character '" + << profile.mPlayerName << "'"; + } + break; case ESM::REC_JOUR: - case ESM::REC_JOUR_LEGACY: case ESM::REC_QUES: - MWBase::Environment::get().getJournal()->readRecord (reader, n.intval); + MWBase::Environment::get().getJournal()->readRecord(reader, n.toInt()); break; case ESM::REC_DIAS: - MWBase::Environment::get().getDialogueManager()->readRecord (reader, n.intval); + MWBase::Environment::get().getDialogueManager()->readRecord(reader, n.toInt()); break; case ESM::REC_ALCH: + case ESM::REC_MISC: + case ESM::REC_ACTI: case ESM::REC_ARMO: case ESM::REC_BOOK: case ESM::REC_CLAS: @@ -448,9 +540,11 @@ void MWState::StateManager::loadGame (const Character *character, const std::str case ESM::REC_ENAB: case ESM::REC_LEVC: case ESM::REC_LEVI: + case ESM::REC_LIGH: case ESM::REC_CREA: case ESM::REC_CONT: - MWBase::Environment::get().getWorld()->readRecord(reader, n.intval, contentFileMap); + case ESM::REC_RAND: + MWBase::Environment::get().getWorld()->readRecord(reader, n.toInt()); break; case ESM::REC_CAM_: @@ -459,7 +553,7 @@ void MWState::StateManager::loadGame (const Character *character, const std::str case ESM::REC_GSCR: - MWBase::Environment::get().getScriptManager()->getGlobalScripts().readRecord (reader, n.intval, contentFileMap); + MWBase::Environment::get().getScriptManager()->getGlobalScripts().readRecord(reader, n.toInt()); break; case ESM::REC_GMAP: @@ -467,29 +561,33 @@ void MWState::StateManager::loadGame (const Character *character, const std::str case ESM::REC_ASPL: case ESM::REC_MARK: - MWBase::Environment::get().getWindowManager()->readRecord(reader, n.intval); + MWBase::Environment::get().getWindowManager()->readRecord(reader, n.toInt()); break; case ESM::REC_DCOU: case ESM::REC_STLN: - MWBase::Environment::get().getMechanicsManager()->readRecord(reader, n.intval); + MWBase::Environment::get().getMechanicsManager()->readRecord(reader, n.toInt()); break; case ESM::REC_INPU: - MWBase::Environment::get().getInputManager()->readRecord(reader, n.intval); + MWBase::Environment::get().getInputManager()->readRecord(reader, n.toInt()); + break; + + case ESM::REC_LUAM: + MWBase::Environment::get().getLuaManager()->readRecord(reader, n.toInt()); break; default: // ignore invalid records - Log(Debug::Warning) << "Warning: Ignoring unknown record: " << n.toString(); + Log(Debug::Warning) << "Warning: Ignoring unknown record: " << n.toStringView(); reader.skipRecord(); } - int progressPercent = static_cast(float(reader.getFileOffset())/total*100); + int progressPercent = static_cast(float(reader.getFileOffset()) / total * 100); if (progressPercent > currentPercent) { - listener.increaseProgress(progressPercent-currentPercent); + listener.increaseProgress(progressPercent - currentPercent); currentPercent = progressPercent; } } @@ -499,8 +597,8 @@ void MWState::StateManager::loadGame (const Character *character, const std::str mState = State_Running; if (character) - Settings::Manager::setString ("character", "Saves", - character->getPath().filename().string()); + Settings::saves().mCharacter.set(Files::pathToUnicodeString(character->getPath().filename())); + mLastSavegame = filepath; MWBase::Environment::get().getWindowManager()->setNewGame(false); MWBase::Environment::get().getWorld()->saveLoaded(); @@ -508,6 +606,7 @@ void MWState::StateManager::loadGame (const Character *character, const std::str MWBase::Environment::get().getWorld()->renderPlayer(); MWBase::Environment::get().getWindowManager()->updatePlayer(); MWBase::Environment::get().getMechanicsManager()->playerLoaded(); + MWBase::Environment::get().getWorld()->toggleVanityMode(false); if (firstPersonCam != MWBase::Environment::get().getWorld()->isFirstPerson()) MWBase::Environment::get().getWorld()->togglePOV(); @@ -516,26 +615,26 @@ void MWState::StateManager::loadGame (const Character *character, const std::str if (ptr.isInCell()) { - const ESM::CellId& cellId = ptr.getCell()->getCell()->getCellId(); + const ESM::RefId cellId = ptr.getCell()->getCell()->getId(); // Use detectWorldSpaceChange=false, otherwise some of the data we just loaded would be cleared again - MWBase::Environment::get().getWorld()->changeToCell (cellId, ptr.getRefData().getPosition(), false, false); + MWBase::Environment::get().getWorld()->changeToCell(cellId, ptr.getRefData().getPosition(), false, false); } else { // Cell no longer exists (i.e. changed game files), choose a default cell - Log(Debug::Warning) << "Warning: Player character's cell no longer exists, changing to the default cell"; - MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(0,0); - float x,y; - MWBase::Environment::get().getWorld()->indexToPosition(0,0,x,y,false); + Log(Debug::Warning) << "Player character's cell no longer exists, changing to the default cell"; + ESM::ExteriorCellLocation cellIndex(0, 0, ESM::Cell::sDefaultWorldspaceId); + MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getExterior(cellIndex); + const osg::Vec2f posFromIndex = ESM::indexToPosition(cellIndex, false); ESM::Position pos; - pos.pos[0] = x; - pos.pos[1] = y; + pos.pos[0] = posFromIndex.x(); + pos.pos[1] = posFromIndex.y(); pos.pos[2] = 0; // should be adjusted automatically (adjustPlayerPos=true) pos.rot[0] = 0; pos.rot[1] = 0; pos.rot[2] = 0; - MWBase::Environment::get().getWorld()->changeToCell(cell->getCell()->getCellId(), pos, true, false); + MWBase::Environment::get().getWorld()->changeToCell(cell.getCell()->getId(), pos, true, false); } MWBase::Environment::get().getWorld()->updateProjectilesCasters(); @@ -546,40 +645,78 @@ void MWState::StateManager::loadGame (const Character *character, const std::str // Since we passed "changeEvent=false" to changeCell, we shouldn't have triggered the cell change flag. // But make sure the flag is cleared anyway in case it was set from an earlier game. - MWBase::Environment::get().getWorld()->markCellAsUnchanged(); + MWBase::Environment::get().getWorldScene()->markCellAsUnchanged(); + + MWBase::Environment::get().getLuaManager()->gameLoaded(); + } + catch (const SaveVersionTooNewError& e) + { + std::string error = "#{OMWEngine:LoadingRequiresNewVersionError}"; + printSavegameFormatError(e.what(), error); + } + catch (const SaveVersionTooOldError& e) + { + const char* release; + // Report the last version still capable of reading this save + if (e.getFormatVersion() <= ESM::OpenMW0_48SaveGameFormatVersion) + release = "OpenMW 0.48.0"; + else + { + // Insert additional else if statements above to cover future releases + static_assert(ESM::MinSupportedSaveGameFormatVersion <= ESM::OpenMW0_49SaveGameFormatVersion); + release = "OpenMW 0.49.0"; + } + auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine"); + std::string error = l10n->formatMessage("LoadingRequiresOldVersionError", { "version" }, { release }); + printSavegameFormatError(e.what(), error); } catch (const std::exception& e) { - std::stringstream error; - error << "Failed to load saved game: " << e.what(); + std::string error = "#{OMWEngine:LoadingFailed}: " + std::string(e.what()); + printSavegameFormatError(e.what(), error); + } +} - Log(Debug::Error) << error.str(); - cleanup (true); +void MWState::StateManager::printSavegameFormatError( + const std::string& exceptionText, const std::string& messageBoxText) +{ + Log(Debug::Error) << "Failed to load saved game: " << exceptionText; - MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); + cleanup(true); - std::vector buttons; - buttons.emplace_back("#{sOk}"); - MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error.str(), buttons); - } + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); + + std::vector buttons; + buttons.emplace_back("#{Interface:OK}"); + + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(messageBoxText, buttons); } void MWState::StateManager::quickLoad() { - if (Character* currentCharacter = getCurrentCharacter ()) + if (Character* currentCharacter = getCurrentCharacter()) { if (currentCharacter->begin() == currentCharacter->end()) return; - loadGame (currentCharacter, currentCharacter->begin()->mPath.string()); //Get newest save + // use requestLoad, otherwise we can crash by loading during the wrong part of the frame + requestLoad(currentCharacter->begin()->mPath); } } -void MWState::StateManager::deleteGame(const MWState::Character *character, const MWState::Slot *slot) +void MWState::StateManager::deleteGame(const MWState::Character* character, const MWState::Slot* slot) { + const std::filesystem::path savePath = slot->mPath; mCharacterManager.deleteSlot(character, slot); + if (mLastSavegame == savePath) + { + if (character->begin() != character->end()) + mLastSavegame = character->begin()->mPath; + else + mLastSavegame.clear(); + } } -MWState::Character *MWState::StateManager::getCurrentCharacter () +MWState::Character* MWState::StateManager::getCurrentCharacter() { return mCharacterManager.getCurrentCharacter(); } @@ -594,7 +731,7 @@ MWState::StateManager::CharacterIterator MWState::StateManager::characterEnd() return mCharacterManager.end(); } -void MWState::StateManager::update (float duration) +void MWState::StateManager::update(float duration) { mTimePlayed += duration; @@ -602,54 +739,103 @@ void MWState::StateManager::update (float duration) if (mAskLoadRecent) { int iButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); - MWState::Character *curCharacter = getCurrentCharacter(); - if(iButton==0 && curCharacter) + MWState::Character* curCharacter = getCurrentCharacter(); + if (iButton == 0 && curCharacter) { mAskLoadRecent = false; - //Load last saved game for current character - - MWState::Slot lastSave = *curCharacter->begin(); - loadGame(curCharacter, lastSave.mPath.string()); + // Load last saved game for current character + // loadGame resets the game state along with mLastSavegame so we want to preserve it + const std::filesystem::path filePath = std::move(mLastSavegame); + loadGame(curCharacter, filePath); } - else if(iButton==1) + else if (iButton == 1) { mAskLoadRecent = false; - MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); } } + + if (mNewGameRequest) + { + newGame(); + mNewGameRequest = false; + } + + if (mLoadRequest) + { + loadGame(*mLoadRequest); + mLoadRequest = std::nullopt; + } } -bool MWState::StateManager::verifyProfile(const ESM::SavedGame& profile) const +bool MWState::StateManager::confirmLoading(const std::vector& missingFiles) const { - const std::vector& selectedContentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); - bool notFound = false; - for (const std::string& contentFile : profile.mContentFiles) + std::ostringstream stream; + for (auto& contentFile : missingFiles) + { + Log(Debug::Warning) << "Warning: Saved game dependency " << contentFile << " is missing."; + stream << contentFile << "\n"; + } + + auto fullList = stream.str(); + if (!fullList.empty()) + fullList.pop_back(); + + constexpr size_t missingPluginsDisplayLimit = 12; + + std::vector buttons; + buttons.emplace_back("#{Interface:Yes}"); + buttons.emplace_back("#{Interface:Copy}"); + buttons.emplace_back("#{Interface:No}"); + std::string message = "#{OMWEngine:MissingContentFilesConfirmation}"; + + auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine"); + message += l10n->formatMessage("MissingContentFilesList", { "files" }, { static_cast(missingFiles.size()) }); + auto cappedSize = std::min(missingFiles.size(), missingPluginsDisplayLimit); + if (cappedSize == missingFiles.size()) + { + message += fullList; + } + else { - if (std::find(selectedContentFiles.begin(), selectedContentFiles.end(), contentFile) - == selectedContentFiles.end()) + for (size_t i = 0; i < cappedSize - 1; ++i) { - Log(Debug::Warning) << "Warning: Saved game dependency " << contentFile << " is missing."; - notFound = true; + message += missingFiles[i]; + message += "\n"; } + + message += "..."; } - if (notFound) + + message + += l10n->formatMessage("MissingContentFilesListCopy", { "files" }, { static_cast(missingFiles.size()) }); + + int selectedButton = -1; + while (true) { - std::vector buttons; - buttons.emplace_back("#{sYes}"); - buttons.emplace_back("#{sNo}"); - MWBase::Environment::get().getWindowManager()->interactiveMessageBox("#{sMissingMastersMsg}", buttons, true); - int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); - if (selectedButton == 1 || selectedButton == -1) - return false; + auto windowManager = MWBase::Environment::get().getWindowManager(); + windowManager->interactiveMessageBox(message, buttons, true, selectedButton); + selectedButton = windowManager->readPressedButton(); + if (selectedButton == 0) + break; + + if (selectedButton == 1) + { + SDL_SetClipboardText(fullList.c_str()); + continue; + } + + return false; } + return true; } -void MWState::StateManager::writeScreenshot(std::vector &imageData) const +void MWState::StateManager::writeScreenshot(std::vector& imageData) const { - int screenshotW = 259*2, screenshotH = 133*2; // *2 to get some nice antialiasing + int screenshotW = 259 * 2, screenshotH = 133 * 2; // *2 to get some nice antialiasing - osg::ref_ptr screenshot (new osg::Image); + osg::ref_ptr screenshot(new osg::Image); MWBase::Environment::get().getWorld()->screenshot(screenshot.get(), screenshotW, screenshotH); @@ -670,5 +856,4 @@ void MWState::StateManager::writeScreenshot(std::vector &imageData) const std::string data = ostream.str(); imageData = std::vector(data.begin(), data.end()); - } diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index 3534dabf2b4..b08584a8170 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -1,90 +1,96 @@ #ifndef GAME_STATE_STATEMANAGER_H #define GAME_STATE_STATEMANAGER_H +#include #include #include "../mwbase/statemanager.hpp" -#include - #include "charactermanager.hpp" namespace MWState { class StateManager : public MWBase::StateManager { - bool mQuitRequest; - bool mAskLoadRecent; - State mState; - CharacterManager mCharacterManager; - double mTimePlayed; + bool mQuitRequest; + bool mAskLoadRecent; + bool mNewGameRequest = false; + std::optional mLoadRequest; + State mState; + CharacterManager mCharacterManager; + double mTimePlayed; + std::filesystem::path mLastSavegame; - private: + private: + void cleanup(bool force = false); - void cleanup (bool force = false); + void printSavegameFormatError(const std::string& exceptionText, const std::string& messageBoxText); - bool verifyProfile (const ESM::SavedGame& profile) const; + bool confirmLoading(const std::vector& missingFiles) const; - void writeScreenshot (std::vector& imageData) const; + void writeScreenshot(std::vector& imageData) const; - std::map buildContentFileIndexMap (const ESM::ESMReader& reader) const; + std::map buildContentFileIndexMap(const ESM::ESMReader& reader) const; - public: + public: + StateManager(const std::filesystem::path& saves, const std::vector& contentFiles); - StateManager (const boost::filesystem::path& saves, const std::string& game); + void requestQuit() override; - void requestQuit() override; + bool hasQuitRequest() const override; - bool hasQuitRequest() const override; + void askLoadRecent() override; - void askLoadRecent() override; + void requestNewGame() override { mNewGameRequest = true; } + void requestLoad(const std::filesystem::path& filepath) override { mLoadRequest = filepath; } - State getState() const override; + State getState() const override; - void newGame (bool bypass = false) override; - ///< Start a new game. - /// - /// \param bypass Skip new game mechanics. + void newGame(bool bypass = false) override; + ///< Start a new game. + /// + /// \param bypass Skip new game mechanics. - void endGame() override; + void endGame(); - void resumeGame() override; + void resumeGame() override; - void deleteGame (const MWState::Character *character, const MWState::Slot *slot) override; - ///< Delete a saved game slot from this character. If all save slots are deleted, the character will be deleted too. + void deleteGame(const MWState::Character* character, const MWState::Slot* slot) override; + ///< Delete a saved game slot from this character. If all save slots are deleted, the character will be deleted + ///< too. - void saveGame (const std::string& description, const Slot *slot = nullptr) override; - ///< Write a saved game to \a slot or create a new slot if \a slot == 0. - /// - /// \note Slot must belong to the current character. + void saveGame(std::string_view description, const Slot* slot = nullptr) override; + ///< Write a saved game to \a slot or create a new slot if \a slot == 0. + /// + /// \note Slot must belong to the current character. - ///Saves a file, using supplied filename, overwritting if needed - /** This is mostly used for quicksaving and autosaving, for they use the same name over and over again - \param name Name of save, defaults to "Quicksave"**/ - void quickSave(std::string name = "Quicksave") override; + /// Saves a file, using supplied filename, overwritting if needed + /** This is mostly used for quicksaving and autosaving, for they use the same name over and over again + \param name Name of save, defaults to "Quicksave"**/ + void quickSave(std::string name = "Quicksave") override; - ///Loads the last saved file - /** Used for quickload **/ - void quickLoad() override; + /// Loads the last saved file + /** Used for quickload **/ + void quickLoad() override; - void loadGame (const std::string& filepath) override; - ///< Load a saved game directly from the given file path. This will search the CharacterManager - /// for a Character containing this save file, and set this Character current if one was found. - /// Otherwise, a new Character will be created. + void loadGame(const std::filesystem::path& filepath) override; + ///< Load a saved game directly from the given file path. This will search the CharacterManager + /// for a Character containing this save file, and set this Character current if one was found. + /// Otherwise, a new Character will be created. - void loadGame (const Character *character, const std::string &filepath) override; - ///< Load a saved game file belonging to the given character. + void loadGame(const Character* character, const std::filesystem::path& filepath) override; + ///< Load a saved game file belonging to the given character. - Character *getCurrentCharacter () override; - ///< @note May return null. + Character* getCurrentCharacter() override; + ///< @note May return null. - CharacterIterator characterBegin() override; - ///< Any call to SaveGame and getCurrentCharacter can invalidate the returned - /// iterator. + CharacterIterator characterBegin() override; + ///< Any call to SaveGame and getCurrentCharacter can invalidate the returned + /// iterator. - CharacterIterator characterEnd() override; + CharacterIterator characterEnd() override; - void update (float duration) override; + void update(float duration); }; } diff --git a/apps/openmw/mwworld/action.cpp b/apps/openmw/mwworld/action.cpp index 32a3c8f96dc..474a4d0e84a 100644 --- a/apps/openmw/mwworld/action.cpp +++ b/apps/openmw/mwworld/action.cpp @@ -1,7 +1,6 @@ #include "action.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -18,14 +17,18 @@ void MWWorld::Action::setTarget(const MWWorld::Ptr& target) mTarget = target; } -MWWorld::Action::Action (bool keepSound, const Ptr& target) : mKeepSound (keepSound), mSoundOffset(0), mTarget (target) -{} +MWWorld::Action::Action(bool keepSound, const Ptr& target) + : mKeepSound(keepSound) + , mSoundOffset(0) + , mTarget(target) +{ +} MWWorld::Action::~Action() {} -void MWWorld::Action::execute (const Ptr& actor, bool noSound) +void MWWorld::Action::execute(const Ptr& actor, bool noSound) { - if(!mSoundId.empty() && !noSound) + if (!mSoundId.empty() && !noSound) { MWSound::PlayMode envType = MWSound::PlayMode::Normal; @@ -36,34 +39,31 @@ void MWWorld::Action::execute (const Ptr& actor, bool noSound) envType = MWSound::PlayMode::NoEnv; } - if(mKeepSound && actor == MWMechanics::getPlayer()) - MWBase::Environment::get().getSoundManager()->playSound(mSoundId, 1.0, 1.0, - MWSound::Type::Sfx, envType, mSoundOffset - ); + if (mKeepSound && actor == MWMechanics::getPlayer()) + MWBase::Environment::get().getSoundManager()->playSound( + mSoundId, 1.0, 1.0, MWSound::Type::Sfx, envType, mSoundOffset); else { bool local = mTarget.isEmpty() || !mTarget.isInCell(); // no usable target - if(mKeepSound) + if (mKeepSound) MWBase::Environment::get().getSoundManager()->playSound3D( - (local ? actor : mTarget).getRefData().getPosition().asVec3(), - mSoundId, 1.0, 1.0, MWSound::Type::Sfx, envType, mSoundOffset - ); + (local ? actor : mTarget).getRefData().getPosition().asVec3(), mSoundId, 1.0, 1.0, + MWSound::Type::Sfx, envType, mSoundOffset); else - MWBase::Environment::get().getSoundManager()->playSound3D(local ? actor : mTarget, - mSoundId, 1.0, 1.0, MWSound::Type::Sfx, envType, mSoundOffset - ); + MWBase::Environment::get().getSoundManager()->playSound3D( + local ? actor : mTarget, mSoundId, 1.0, 1.0, MWSound::Type::Sfx, envType, mSoundOffset); } } - executeImp (actor); + executeImp(actor); } -void MWWorld::Action::setSound (const std::string& id) +void MWWorld::Action::setSound(const ESM::RefId& id) { mSoundId = id; } void MWWorld::Action::setSoundOffset(float offset) { - mSoundOffset=offset; + mSoundOffset = offset; } diff --git a/apps/openmw/mwworld/action.hpp b/apps/openmw/mwworld/action.hpp index 2f9804053ef..56c8fd8b5b3 100644 --- a/apps/openmw/mwworld/action.hpp +++ b/apps/openmw/mwworld/action.hpp @@ -2,6 +2,7 @@ #define GAME_MWWORLD_ACTION_H #include +#include #include "ptr.hpp" @@ -10,37 +11,35 @@ namespace MWWorld /// \brief Abstract base for actions class Action { - std::string mSoundId; - bool mKeepSound; - float mSoundOffset; - Ptr mTarget; + ESM::RefId mSoundId; + bool mKeepSound; + float mSoundOffset; + Ptr mTarget; - // not implemented - Action (const Action& action); - Action& operator= (const Action& action); + // not implemented + Action(const Action& action); + Action& operator=(const Action& action); - virtual void executeImp (const Ptr& actor) = 0; + virtual void executeImp(const Ptr& actor) = 0; - protected: + protected: + void setTarget(const Ptr&); - void setTarget(const Ptr&); + public: + const Ptr& getTarget() const; - public: + Action(bool keepSound = false, const Ptr& target = Ptr()); + ///< \param keepSound Keep playing the sound even if the object the sound is played on is removed. - const Ptr& getTarget() const; + virtual ~Action(); - Action (bool keepSound = false, const Ptr& target = Ptr()); - ///< \param keepSound Keep playing the sound even if the object the sound is played on is removed. + virtual bool isNullAction() { return false; } + ///< Is running this action a no-op? (default false) - virtual ~Action(); + void execute(const Ptr& actor, bool noSound = false); - virtual bool isNullAction() { return false; } - ///< Is running this action a no-op? (default false) - - void execute (const Ptr& actor, bool noSound = false); - - void setSound (const std::string& id); - void setSoundOffset(float offset); + void setSound(const ESM::RefId& id); + void setSoundOffset(float offset); }; } diff --git a/apps/openmw/mwworld/actionalchemy.cpp b/apps/openmw/mwworld/actionalchemy.cpp index 62b673dd23d..3348dbbfb3d 100644 --- a/apps/openmw/mwworld/actionalchemy.cpp +++ b/apps/openmw/mwworld/actionalchemy.cpp @@ -8,18 +8,18 @@ namespace MWWorld { ActionAlchemy::ActionAlchemy(bool force) - : Action (false) - , mForce(force) + : Action(false) + , mForce(force) { } - void ActionAlchemy::executeImp (const Ptr& actor) + void ActionAlchemy::executeImp(const Ptr& actor) { if (actor != MWMechanics::getPlayer()) return; - if(!mForce && MWMechanics::isPlayerInCombat()) - { //Ensure we're not in combat + if (!mForce && MWMechanics::isPlayerInCombat()) + { // Ensure we're not in combat MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage3}"); return; } diff --git a/apps/openmw/mwworld/actionalchemy.hpp b/apps/openmw/mwworld/actionalchemy.hpp index 194ef308b92..d37850e6ae8 100644 --- a/apps/openmw/mwworld/actionalchemy.hpp +++ b/apps/openmw/mwworld/actionalchemy.hpp @@ -8,10 +8,10 @@ namespace MWWorld class ActionAlchemy : public Action { bool mForce; - void executeImp (const Ptr& actor) override; + void executeImp(const Ptr& actor) override; public: - ActionAlchemy(bool force=false); + ActionAlchemy(bool force = false); }; } diff --git a/apps/openmw/mwworld/actionapply.cpp b/apps/openmw/mwworld/actionapply.cpp index e3699a6ac3a..b17205a79ca 100644 --- a/apps/openmw/mwworld/actionapply.cpp +++ b/apps/openmw/mwworld/actionapply.cpp @@ -2,41 +2,16 @@ #include "class.hpp" -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/containerstore.hpp" - -#include "../mwmechanics/actorutil.hpp" - namespace MWWorld { - ActionApply::ActionApply (const Ptr& object, const std::string& id) - : Action (false, object), mId (id) - {} - - void ActionApply::executeImp (const Ptr& actor) + ActionApply::ActionApply(const Ptr& object, const ESM::RefId& id) + : Action(false, object) + , mId(id) { - MWBase::Environment::get().getWorld()->breakInvisibility(actor); - - actor.getClass().apply (actor, mId, actor); - - actor.getClass().getContainerStore(actor).remove(getTarget(), 1, actor); } - - ActionApplyWithSkill::ActionApplyWithSkill (const Ptr& object, const std::string& id, - int skillIndex, int usageType) - : Action (false, object), mId (id), mSkillIndex (skillIndex), mUsageType (usageType) - {} - - void ActionApplyWithSkill::executeImp (const Ptr& actor) + void ActionApply::executeImp(const Ptr& actor) { - MWBase::Environment::get().getWorld()->breakInvisibility(actor); - - if (actor.getClass().apply (actor, mId, actor) && mUsageType!=-1 && actor == MWMechanics::getPlayer()) - actor.getClass().skillUsageSucceeded (actor, mSkillIndex, mUsageType); - - actor.getClass().getContainerStore(actor).remove(getTarget(), 1, actor); + actor.getClass().consume(getTarget(), actor); } } diff --git a/apps/openmw/mwworld/actionapply.hpp b/apps/openmw/mwworld/actionapply.hpp index 4350b7af4f6..645b301915a 100644 --- a/apps/openmw/mwworld/actionapply.hpp +++ b/apps/openmw/mwworld/actionapply.hpp @@ -1,35 +1,20 @@ #ifndef GAME_MWWORLD_ACTIONAPPLY_H #define GAME_MWWORLD_ACTIONAPPLY_H -#include - #include "action.hpp" +#include +#include namespace MWWorld { class ActionApply : public Action { - std::string mId; - - void executeImp (const Ptr& actor) override; - - public: - - ActionApply (const Ptr& object, const std::string& id); - }; - - class ActionApplyWithSkill : public Action - { - std::string mId; - int mSkillIndex; - int mUsageType; - - void executeImp (const Ptr& actor) override; + ESM::RefId mId; - public: + void executeImp(const Ptr& actor) override; - ActionApplyWithSkill (const Ptr& object, const std::string& id, - int skillIndex, int usageType); + public: + ActionApply(const Ptr& object, const ESM::RefId& id); }; } diff --git a/apps/openmw/mwworld/actiondoor.cpp b/apps/openmw/mwworld/actiondoor.cpp index 6e3441e2206..be0d5f32981 100644 --- a/apps/openmw/mwworld/actiondoor.cpp +++ b/apps/openmw/mwworld/actiondoor.cpp @@ -5,11 +5,12 @@ namespace MWWorld { - ActionDoor::ActionDoor (const MWWorld::Ptr& object) : Action (false, object) + ActionDoor::ActionDoor(const MWWorld::Ptr& object) + : Action(false, object) { } - void ActionDoor::executeImp (const MWWorld::Ptr& actor) + void ActionDoor::executeImp(const MWWorld::Ptr& actor) { MWBase::Environment::get().getWorld()->activateDoor(getTarget()); } diff --git a/apps/openmw/mwworld/actiondoor.hpp b/apps/openmw/mwworld/actiondoor.hpp index 4be61f5760e..16b85b90aee 100644 --- a/apps/openmw/mwworld/actiondoor.hpp +++ b/apps/openmw/mwworld/actiondoor.hpp @@ -8,10 +8,10 @@ namespace MWWorld { class ActionDoor : public Action { - void executeImp (const MWWorld::Ptr& actor) override; + void executeImp(const MWWorld::Ptr& actor) override; - public: - ActionDoor (const Ptr& object); + public: + ActionDoor(const Ptr& object); }; } diff --git a/apps/openmw/mwworld/actioneat.cpp b/apps/openmw/mwworld/actioneat.cpp index ef435cca920..c67502bdee9 100644 --- a/apps/openmw/mwworld/actioneat.cpp +++ b/apps/openmw/mwworld/actioneat.cpp @@ -1,8 +1,6 @@ #include "actioneat.hpp" -#include - -#include "../mwworld/containerstore.hpp" +#include #include "../mwmechanics/actorutil.hpp" @@ -10,17 +8,14 @@ namespace MWWorld { - void ActionEat::executeImp (const Ptr& actor) + void ActionEat::executeImp(const Ptr& actor) { - // remove used item (assume the item is present in inventory) - getTarget().getContainerStore()->remove(getTarget(), 1, actor); - - // apply to actor - std::string id = getTarget().getCellRef().getRefId(); - - if (actor.getClass().apply (actor, id, actor) && actor == MWMechanics::getPlayer()) - actor.getClass().skillUsageSucceeded (actor, ESM::Skill::Alchemy, 1); + if (actor.getClass().consume(getTarget(), actor) && actor == MWMechanics::getPlayer()) + actor.getClass().skillUsageSucceeded(actor, ESM::Skill::Alchemy, ESM::Skill::Alchemy_UseIngredient); } - ActionEat::ActionEat (const MWWorld::Ptr& object) : Action (false, object) {} + ActionEat::ActionEat(const MWWorld::Ptr& object) + : Action(false, object) + { + } } diff --git a/apps/openmw/mwworld/actioneat.hpp b/apps/openmw/mwworld/actioneat.hpp index ddc231d99a4..428635e4d66 100644 --- a/apps/openmw/mwworld/actioneat.hpp +++ b/apps/openmw/mwworld/actioneat.hpp @@ -7,11 +7,10 @@ namespace MWWorld { class ActionEat : public Action { - void executeImp (const Ptr& actor) override; + void executeImp(const Ptr& actor) override; - public: - - ActionEat (const MWWorld::Ptr& object); + public: + ActionEat(const MWWorld::Ptr& object); }; } diff --git a/apps/openmw/mwworld/actionequip.cpp b/apps/openmw/mwworld/actionequip.cpp index 92cd0443868..58e6f013aae 100644 --- a/apps/openmw/mwworld/actionequip.cpp +++ b/apps/openmw/mwworld/actionequip.cpp @@ -5,21 +5,18 @@ #include "../mwmechanics/actorutil.hpp" -#include - -#include "inventorystore.hpp" -#include "player.hpp" #include "class.hpp" +#include "inventorystore.hpp" namespace MWWorld { - ActionEquip::ActionEquip (const MWWorld::Ptr& object, bool force) - : Action (false, object) - , mForce(force) + ActionEquip::ActionEquip(const MWWorld::Ptr& object, bool force) + : Action(false, object) + , mForce(force) { } - void ActionEquip::executeImp (const Ptr& actor) + void ActionEquip::executeImp(const Ptr& actor) { MWWorld::Ptr object = getTarget(); MWWorld::InventoryStore& invStore = actor.getClass().getInventoryStore(actor); @@ -34,13 +31,13 @@ namespace MWWorld if (!mForce) { - std::pair result = object.getClass().canBeEquipped (object, actor); + auto result = object.getClass().canBeEquipped(object, actor); // display error message if the player tried to equip something if (!result.second.empty() && actor == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox(result.second); - switch(result.first) + switch (result.first) { case 0: return; @@ -65,15 +62,11 @@ namespace MWWorld } if (it == invStore.end()) - { - std::stringstream error; - error << "ActionEquip can't find item " << object.getCellRef().getRefId(); - throw std::runtime_error(error.str()); - } + throw std::runtime_error("ActionEquip can't find item " + object.getCellRef().getRefId().toDebugString()); // equip the item in the first free slot - std::vector::const_iterator slot=slots_.first.begin(); - for (;slot!=slots_.first.end(); ++slot) + std::vector::const_iterator slot = slots_.first.begin(); + for (; slot != slots_.first.end(); ++slot) { // if the item is equipped already, nothing to do if (invStore.getSlot(*slot) == it) @@ -82,7 +75,7 @@ namespace MWWorld if (invStore.getSlot(*slot) == invStore.end()) { // slot is not occupied - invStore.equip(*slot, it, actor); + invStore.equip(*slot, it); break; } } @@ -95,17 +88,17 @@ namespace MWWorld bool reEquip = false; for (slot = slots_.first.begin(); slot != slots_.first.end(); ++slot) { - invStore.unequipSlot(*slot, actor, false); + invStore.unequipSlot(*slot, false); if (slot + 1 != slots_.first.end()) { - invStore.equip(*slot, invStore.getSlot(*(slot + 1)), actor); + invStore.equip(*slot, invStore.getSlot(*(slot + 1))); } else { - invStore.equip(*slot, it, actor); + invStore.equip(*slot, it); } - //Fix for issue of selected enchated item getting remmoved on cycle + // Fix for issue of selected enchated item getting remmoved on cycle if (invStore.getSlot(*slot) == enchItem) { reEquip = true; diff --git a/apps/openmw/mwworld/actionequip.hpp b/apps/openmw/mwworld/actionequip.hpp index 1ac3cb2360d..863d2816447 100644 --- a/apps/openmw/mwworld/actionequip.hpp +++ b/apps/openmw/mwworld/actionequip.hpp @@ -9,11 +9,11 @@ namespace MWWorld { bool mForce; - void executeImp (const Ptr& actor) override; + void executeImp(const Ptr& actor) override; public: /// @param item to equip - ActionEquip (const Ptr& object, bool force=false); + ActionEquip(const Ptr& object, bool force = false); }; } diff --git a/apps/openmw/mwworld/actionharvest.cpp b/apps/openmw/mwworld/actionharvest.cpp index c9468c715fb..30f316c2db2 100644 --- a/apps/openmw/mwworld/actionharvest.cpp +++ b/apps/openmw/mwworld/actionharvest.cpp @@ -4,7 +4,7 @@ #include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -16,19 +16,19 @@ namespace MWWorld { - ActionHarvest::ActionHarvest (const MWWorld::Ptr& container) - : Action (true, container) + ActionHarvest::ActionHarvest(const MWWorld::Ptr& container) + : Action(true, container) { - setSound("Item Ingredient Up"); + setSound(ESM::RefId::stringRefId("Item Ingredient Up")); } - void ActionHarvest::executeImp (const MWWorld::Ptr& actor) + void ActionHarvest::executeImp(const MWWorld::Ptr& actor) { if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) return; MWWorld::Ptr target = getTarget(); - MWWorld::ContainerStore& store = target.getClass().getContainerStore (target); + MWWorld::ContainerStore& store = target.getClass().getContainerStore(target); store.resolve(); MWWorld::ContainerStore& actorStore = actor.getClass().getContainerStore(actor); std::map takenMap; @@ -37,13 +37,15 @@ namespace MWWorld if (!it->getClass().showsInInventory(*it)) continue; - int itemCount = it->getRefData().getCount(); - // Note: it is important to check for crime before move an item from container. Otherwise owner check will not work - // for a last item in the container - empty harvested containers are considered as "allowed to use". + int itemCount = it->getCellRef().getCount(); + // Note: it is important to check for crime before move an item from container. Otherwise owner check will + // not work for a last item in the container - empty harvested containers are considered as "allowed to + // use". MWBase::Environment::get().getMechanicsManager()->itemTaken(actor, *it, target, itemCount); - actorStore.add(*it, itemCount, actor); - store.remove(*it, itemCount, getTarget()); - takenMap[it->getClass().getName(*it)]+=itemCount; + actorStore.add(*it, itemCount); + store.remove(*it, itemCount); + std::string name{ it->getClass().getName(*it) }; + takenMap[name] += itemCount; } // Spawn a messagebox (only for items added to player's inventory) @@ -52,9 +54,9 @@ namespace MWWorld std::ostringstream stream; int lineCount = 0; const static int maxLines = 10; - for (auto & pair : takenMap) + for (const auto& pair : takenMap) { - std::string itemName = pair.first; + const std::string& itemName = pair.first; int itemCount = pair.second; lineCount++; if (lineCount == maxLines) @@ -62,7 +64,8 @@ namespace MWWorld else if (lineCount > maxLines) break; - // The two GMST entries below expand to strings informing the player of what, and how many of it has been added to their inventory + // The two GMST entries below expand to strings informing the player of what, and how many of it has + // been added to their inventory std::string msgBox; if (itemCount == 1) { diff --git a/apps/openmw/mwworld/actionharvest.hpp b/apps/openmw/mwworld/actionharvest.hpp index 2edc2f34e54..0c0596a691c 100644 --- a/apps/openmw/mwworld/actionharvest.hpp +++ b/apps/openmw/mwworld/actionharvest.hpp @@ -8,11 +8,11 @@ namespace MWWorld { class ActionHarvest : public Action { - void executeImp (const MWWorld::Ptr& actor) override; + void executeImp(const MWWorld::Ptr& actor) override; - public: - ActionHarvest (const Ptr& container); - ///< \param container The Container the Player has activated. + public: + ActionHarvest(const Ptr& container); + ///< \param container The Container the Player has activated. }; } diff --git a/apps/openmw/mwworld/actionopen.cpp b/apps/openmw/mwworld/actionopen.cpp index 266ea4d95f2..f0d101be99e 100644 --- a/apps/openmw/mwworld/actionopen.cpp +++ b/apps/openmw/mwworld/actionopen.cpp @@ -4,16 +4,17 @@ #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/disease.hpp" namespace MWWorld { - ActionOpen::ActionOpen (const MWWorld::Ptr& container) - : Action (false, container) + ActionOpen::ActionOpen(const MWWorld::Ptr& container) + : Action(false, container) { } - void ActionOpen::executeImp (const MWWorld::Ptr& actor) + void ActionOpen::executeImp(const MWWorld::Ptr& actor) { if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) return; diff --git a/apps/openmw/mwworld/actionopen.hpp b/apps/openmw/mwworld/actionopen.hpp index e9f76c4d71c..b6d41f9a3db 100644 --- a/apps/openmw/mwworld/actionopen.hpp +++ b/apps/openmw/mwworld/actionopen.hpp @@ -7,12 +7,11 @@ namespace MWWorld { class ActionOpen : public Action { - void executeImp (const MWWorld::Ptr& actor) override; - - public: - ActionOpen (const Ptr& container); - ///< \param container The Container the Player has activated. + void executeImp(const MWWorld::Ptr& actor) override; + public: + ActionOpen(const Ptr& container); + ///< \param container The Container the Player has activated. }; } diff --git a/apps/openmw/mwworld/actionread.cpp b/apps/openmw/mwworld/actionread.cpp index 58611f0af70..b6014b834c3 100644 --- a/apps/openmw/mwworld/actionread.cpp +++ b/apps/openmw/mwworld/actionread.cpp @@ -1,61 +1,58 @@ #include "actionread.hpp" +#include +#include +#include + #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" -#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/npcstats.hpp" -#include "player.hpp" #include "class.hpp" #include "esmstore.hpp" namespace MWWorld { - ActionRead::ActionRead (const MWWorld::Ptr& object) : Action (false, object) + ActionRead::ActionRead(const MWWorld::Ptr& object) + : Action(false, object) { } - void ActionRead::executeImp (const MWWorld::Ptr& actor) { - - if (actor != MWMechanics::getPlayer()) + void ActionRead::executeImp(const MWWorld::Ptr& actor) + { + const MWWorld::Ptr player = MWMechanics::getPlayer(); + if (actor != player && getTarget().getContainerStore() != nullptr) return; - //Ensure we're not in combat - if(MWMechanics::isPlayerInCombat() - // Reading in combat is still allowed if the scroll/book is not in the player inventory yet - // (since otherwise, there would be no way to pick it up) - && getTarget().getContainerStore() == &actor.getClass().getContainerStore(actor) - ) { + // Ensure we're not in combat + if (MWMechanics::isPlayerInCombat() + // Reading in combat is still allowed if the scroll/book is not in the player inventory yet + // (since otherwise, there would be no way to pick it up) + && getTarget().getContainerStore() == &player.getClass().getContainerStore(player)) + { MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage4}"); return; } - LiveCellRef *ref = getTarget().get(); + LiveCellRef* ref = getTarget().get(); if (ref->mBase->mData.mIsScroll) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Scroll, getTarget()); else MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Book, getTarget()); - MWMechanics::NpcStats& npcStats = actor.getClass().getNpcStats (actor); + MWMechanics::NpcStats& npcStats = player.getClass().getNpcStats(player); // Skill gain from books - if (ref->mBase->mData.mSkillId >= 0 && ref->mBase->mData.mSkillId < ESM::Skill::Length - && !npcStats.hasBeenUsed (ref->mBase->mId)) + ESM::RefId skill = ESM::Skill::indexToRefId(ref->mBase->mData.mSkillId); + if (!skill.empty() && !npcStats.hasBeenUsed(ref->mBase->mId)) { - MWWorld::LiveCellRef *playerRef = actor.get(); - - const ESM::Class *class_ = - MWBase::Environment::get().getWorld()->getStore().get().find ( - playerRef->mBase->mClass - ); + MWBase::Environment::get().getLuaManager()->skillLevelUp(player, skill, "book"); - npcStats.increaseSkill (ref->mBase->mData.mSkillId, *class_, true, true); - - npcStats.flagAsUsed (ref->mBase->mId); + npcStats.flagAsUsed(ref->mBase->mId); } - } } diff --git a/apps/openmw/mwworld/actionread.hpp b/apps/openmw/mwworld/actionread.hpp index 6387933ebaf..194aca9f598 100644 --- a/apps/openmw/mwworld/actionread.hpp +++ b/apps/openmw/mwworld/actionread.hpp @@ -7,11 +7,11 @@ namespace MWWorld { class ActionRead : public Action { - void executeImp (const MWWorld::Ptr& actor) override; + void executeImp(const MWWorld::Ptr& actor) override; - public: - /// @param book or scroll to read - ActionRead (const Ptr& object); + public: + /// @param book or scroll to read + ActionRead(const Ptr& object); }; } diff --git a/apps/openmw/mwworld/actionrepair.cpp b/apps/openmw/mwworld/actionrepair.cpp index 4fe48b4b4c3..4d5ec2b2ecf 100644 --- a/apps/openmw/mwworld/actionrepair.cpp +++ b/apps/openmw/mwworld/actionrepair.cpp @@ -2,23 +2,22 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWWorld { ActionRepair::ActionRepair(const Ptr& item, bool force) - : Action (false, item) + : Action(false, item) , mForce(force) { } - void ActionRepair::executeImp (const Ptr& actor) + void ActionRepair::executeImp(const Ptr& actor) { if (actor != MWMechanics::getPlayer()) return; - if(!mForce && MWMechanics::isPlayerInCombat()) + if (!mForce && MWMechanics::isPlayerInCombat()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage2}"); return; diff --git a/apps/openmw/mwworld/actionrepair.hpp b/apps/openmw/mwworld/actionrepair.hpp index 218077f5de8..54611ff3203 100644 --- a/apps/openmw/mwworld/actionrepair.hpp +++ b/apps/openmw/mwworld/actionrepair.hpp @@ -9,11 +9,11 @@ namespace MWWorld { bool mForce; - void executeImp (const Ptr& actor) override; + void executeImp(const Ptr& actor) override; public: /// @param item repair hammer - ActionRepair(const Ptr& item, bool force=false); + ActionRepair(const Ptr& item, bool force = false); }; } diff --git a/apps/openmw/mwworld/actionsoulgem.cpp b/apps/openmw/mwworld/actionsoulgem.cpp index f7823dabaa9..ed642013f07 100644 --- a/apps/openmw/mwworld/actionsoulgem.cpp +++ b/apps/openmw/mwworld/actionsoulgem.cpp @@ -1,30 +1,51 @@ #include "actionsoulgem.hpp" -#include "../mwbase/windowmanager.hpp" +#include +#include + #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwworld/esmstore.hpp" + namespace MWWorld { - ActionSoulgem::ActionSoulgem(const Ptr &object) + ActionSoulgem::ActionSoulgem(const Ptr& object) : Action(false, object) { - } - void ActionSoulgem::executeImp(const Ptr &actor) + void ActionSoulgem::executeImp(const Ptr& actor) { if (actor != MWMechanics::getPlayer()) return; - if(MWMechanics::isPlayerInCombat()) { //Ensure we're not in combat + if (MWMechanics::isPlayerInCombat()) + { // Ensure we're not in combat MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage5}"); return; } - MWBase::Environment::get().getWindowManager()->showSoulgemDialog(getTarget()); - } + const auto& target = getTarget(); + const ESM::RefId& targetSoul = target.getCellRef().getSoul(); + + if (targetSoul.empty()) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage32}"); + return; + } + + if (!MWBase::Environment::get().getESMStore()->get().search(targetSoul)) + { + Log(Debug::Warning) << "Soul '" << targetSoul << "' not found (item: '" << target.getCellRef().getRefId() + << "')"; + return; + } + + MWBase::Environment::get().getWindowManager()->showSoulgemDialog(target); + } } diff --git a/apps/openmw/mwworld/actionsoulgem.hpp b/apps/openmw/mwworld/actionsoulgem.hpp index d2b14c9122f..f17ddb7bdc1 100644 --- a/apps/openmw/mwworld/actionsoulgem.hpp +++ b/apps/openmw/mwworld/actionsoulgem.hpp @@ -7,11 +7,11 @@ namespace MWWorld { class ActionSoulgem : public Action { - void executeImp (const MWWorld::Ptr& actor) override; + void executeImp(const MWWorld::Ptr& actor) override; - public: - /// @param soulgem to use - ActionSoulgem (const Ptr& object); + public: + /// @param soulgem to use + ActionSoulgem(const Ptr& object); }; } diff --git a/apps/openmw/mwworld/actiontake.cpp b/apps/openmw/mwworld/actiontake.cpp index a173e87fbc3..1f37499e8f1 100644 --- a/apps/openmw/mwworld/actiontake.cpp +++ b/apps/openmw/mwworld/actiontake.cpp @@ -1,9 +1,9 @@ #include "actiontake.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwgui/inventorywindow.hpp" @@ -12,9 +12,12 @@ namespace MWWorld { - ActionTake::ActionTake (const MWWorld::Ptr& object) : Action (true, object) {} + ActionTake::ActionTake(const MWWorld::Ptr& object) + : Action(true, object) + { + } - void ActionTake::executeImp (const Ptr& actor) + void ActionTake::executeImp(const Ptr& actor) { // When in GUI mode, we should use drag and drop if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr()) @@ -27,10 +30,13 @@ namespace MWWorld } } - MWBase::Environment::get().getMechanicsManager()->itemTaken( - actor, getTarget(), MWWorld::Ptr(), getTarget().getRefData().getCount()); - MWWorld::Ptr newitem = *actor.getClass().getContainerStore (actor).add (getTarget(), getTarget().getRefData().getCount(), actor); - MWBase::Environment::get().getWorld()->deleteObject (getTarget()); + int count = getTarget().getCellRef().getCount(); + if (getTarget().getClass().isGold(getTarget())) + count *= getTarget().getClass().getValue(getTarget()); + + MWBase::Environment::get().getMechanicsManager()->itemTaken(actor, getTarget(), MWWorld::Ptr(), count); + MWWorld::Ptr newitem = *actor.getClass().getContainerStore(actor).add(getTarget(), count); + MWBase::Environment::get().getWorld()->deleteObject(getTarget()); setTarget(newitem); } } diff --git a/apps/openmw/mwworld/actiontake.hpp b/apps/openmw/mwworld/actiontake.hpp index bb9c0d38002..7cb0e03dd2a 100644 --- a/apps/openmw/mwworld/actiontake.hpp +++ b/apps/openmw/mwworld/actiontake.hpp @@ -7,11 +7,10 @@ namespace MWWorld { class ActionTake : public Action { - void executeImp (const Ptr& actor) override; + void executeImp(const Ptr& actor) override; - public: - - ActionTake (const MWWorld::Ptr& object); + public: + ActionTake(const MWWorld::Ptr& object); }; } diff --git a/apps/openmw/mwworld/actiontalk.cpp b/apps/openmw/mwworld/actiontalk.cpp index 7fe623b8e44..e17c0830a8b 100644 --- a/apps/openmw/mwworld/actiontalk.cpp +++ b/apps/openmw/mwworld/actiontalk.cpp @@ -7,9 +7,12 @@ namespace MWWorld { - ActionTalk::ActionTalk (const Ptr& actor) : Action (false, actor) {} + ActionTalk::ActionTalk(const Ptr& actor) + : Action(false, actor) + { + } - void ActionTalk::executeImp (const Ptr& actor) + void ActionTalk::executeImp(const Ptr& actor) { if (actor == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, getTarget()); diff --git a/apps/openmw/mwworld/actiontalk.hpp b/apps/openmw/mwworld/actiontalk.hpp index 6dd70380ffc..f2d2843a640 100644 --- a/apps/openmw/mwworld/actiontalk.hpp +++ b/apps/openmw/mwworld/actiontalk.hpp @@ -7,12 +7,11 @@ namespace MWWorld { class ActionTalk : public Action { - void executeImp (const Ptr& actor) override; + void executeImp(const Ptr& actor) override; - public: - - ActionTalk (const Ptr& actor); - ///< \param actor The actor the player is talking to + public: + ActionTalk(const Ptr& actor); + ///< \param actor The actor the player is talking to }; } diff --git a/apps/openmw/mwworld/actionteleport.cpp b/apps/openmw/mwworld/actionteleport.cpp index 9cd8469a39d..95c136ac86b 100644 --- a/apps/openmw/mwworld/actionteleport.cpp +++ b/apps/openmw/mwworld/actionteleport.cpp @@ -1,30 +1,41 @@ #include "actionteleport.hpp" +#include +#include + #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/worldmodel.hpp" #include "player.hpp" namespace MWWorld { - ActionTeleport::ActionTeleport (const std::string& cellName, - const ESM::Position& position, bool teleportFollowers) - : Action (true), mCellName (cellName), mPosition (position), mTeleportFollowers(teleportFollowers) + ActionTeleport::ActionTeleport(ESM::RefId cellId, const ESM::Position& position, bool teleportFollowers) + : Action(true) + , mCellId(cellId) + , mPosition(position) + , mTeleportFollowers(teleportFollowers) { } - void ActionTeleport::executeImp (const Ptr& actor) + void ActionTeleport::executeImp(const Ptr& actor) { if (mTeleportFollowers) { // Find any NPCs that are following the actor and teleport them with him std::set followers; - getFollowers(actor, followers, true); + + bool toExterior = MWBase::Environment::get().getWorldModel()->getCell(mCellId).isExterior(); + getFollowers(actor, followers, toExterior, true); for (std::set::iterator it = followers.begin(); it != followers.end(); ++it) teleport(*it); @@ -33,52 +44,63 @@ namespace MWWorld teleport(actor); } - void ActionTeleport::teleport(const Ptr &actor) + void ActionTeleport::teleport(const Ptr& actor) { MWBase::World* world = MWBase::Environment::get().getWorld(); - actor.getClass().getCreatureStats(actor).land(actor == world->getPlayerPtr()); - if(actor == world->getPlayerPtr()) + MWWorld::WorldModel* worldModel = MWBase::Environment::get().getWorldModel(); + auto& stats = actor.getClass().getCreatureStats(actor); + stats.land(actor == world->getPlayerPtr()); + stats.setTeleported(true); + + Ptr teleported; + if (actor == world->getPlayerPtr()) { world->getPlayer().setTeleported(true); - if (mCellName.empty()) - world->changeToExteriorCell (mPosition, true); - else - world->changeToInteriorCell (mCellName, mPosition, true); + world->changeToCell(mCellId, mPosition, true); + teleported = world->getPlayerPtr(); } else { if (actor.getClass().getCreatureStats(actor).getAiSequence().isInCombat(world->getPlayerPtr())) - actor.getClass().getCreatureStats(actor).getAiSequence().stopCombat(); - else if (mCellName.empty()) { - int cellX; - int cellY; - world->positionToIndex(mPosition.pos[0],mPosition.pos[1],cellX,cellY); - world->moveObject(actor,world->getExterior(cellX,cellY), - mPosition.pos[0],mPosition.pos[1],mPosition.pos[2]); + actor.getClass().getCreatureStats(actor).getAiSequence().stopCombat(); + return; } else - world->moveObject(actor,world->getInterior(mCellName),mPosition.pos[0],mPosition.pos[1],mPosition.pos[2]); + teleported = world->moveObject(actor, &worldModel->getCell(mCellId), mPosition.asVec3(), true, true); } + + if (!world->isWaterWalkingCastableOnTarget(teleported) && MWMechanics::hasWaterWalking(teleported)) + teleported.getClass() + .getCreatureStats(teleported) + .getActiveSpells() + .purgeEffect(actor, ESM::MagicEffect::WaterWalking); + + MWBase::Environment::get().getLuaManager()->objectTeleported(teleported); } - void ActionTeleport::getFollowers(const MWWorld::Ptr& actor, std::set& out, bool includeHostiles) { + void ActionTeleport::getFollowers( + const MWWorld::Ptr& actor, std::set& out, bool toExterior, bool includeHostiles) + { std::set followers; MWBase::Environment::get().getMechanicsManager()->getActorsFollowing(actor, followers); - for(std::set::iterator it = followers.begin();it != followers.end();++it) + for (std::set::iterator it = followers.begin(); it != followers.end(); ++it) { MWWorld::Ptr follower = *it; - std::string script = follower.getClass().getScript(follower); + const ESM::RefId& script = follower.getClass().getScript(follower); if (!includeHostiles && follower.getClass().getCreatureStats(follower).getAiSequence().isInCombat(actor)) continue; - if (!script.empty() && follower.getRefData().getLocals().getIntVar(script, "stayoutside") == 1) + if (!toExterior && !script.empty() + && follower.getRefData().getLocals().getIntVar(script, "stayoutside") == 1 + && follower.getCell()->getCell()->isExterior()) continue; - if ((follower.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3()).length2() > 800 * 800) + if ((follower.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3()).length2() + > 800 * 800) continue; out.emplace(follower); diff --git a/apps/openmw/mwworld/actionteleport.hpp b/apps/openmw/mwworld/actionteleport.hpp index 0a981a418e7..377e0ce5124 100644 --- a/apps/openmw/mwworld/actionteleport.hpp +++ b/apps/openmw/mwworld/actionteleport.hpp @@ -3,6 +3,7 @@ #include #include +#include #include @@ -12,25 +13,26 @@ namespace MWWorld { class ActionTeleport : public Action { - std::string mCellName; - ESM::Position mPosition; - bool mTeleportFollowers; - - /// Teleports this actor and also teleports anyone following that actor. - void executeImp (const Ptr& actor) override; - - /// Teleports only the given actor (internal use). - void teleport(const Ptr &actor); - - public: - - /// If cellName is empty, an exterior cell is assumed. - /// @param teleportFollowers Whether to teleport any following actors of the target actor as well. - ActionTeleport (const std::string& cellName, const ESM::Position& position, bool teleportFollowers); - - /// @param includeHostiles If true, include hostile followers (which won't actually be teleported) in the output, - /// e.g. so that the teleport action can calm them. - static void getFollowers(const MWWorld::Ptr& actor, std::set& out, bool includeHostiles = false); + ESM::RefId mCellId; + ESM::Position mPosition; + bool mTeleportFollowers; + + /// Teleports this actor and also teleports anyone following that actor. + void executeImp(const Ptr& actor) override; + + /// Teleports only the given actor (internal use). + void teleport(const Ptr& actor); + + public: + /// If cellName is empty, an exterior cell is assumed. + /// @param teleportFollowers Whether to teleport any following actors of the target actor as well. + ActionTeleport(ESM::RefId cellId, const ESM::Position& position, bool teleportFollowers); + + /// @param includeHostiles If true, include hostile followers (which won't actually be teleported) in the + /// output, + /// e.g. so that the teleport action can calm them. + static void getFollowers( + const MWWorld::Ptr& actor, std::set& out, bool toExterior, bool includeHostiles = false); }; } diff --git a/apps/openmw/mwworld/actiontrap.cpp b/apps/openmw/mwworld/actiontrap.cpp index cdabaf8c849..5a0e3c7b868 100644 --- a/apps/openmw/mwworld/actiontrap.cpp +++ b/apps/openmw/mwworld/actiontrap.cpp @@ -7,7 +7,7 @@ namespace MWWorld { - void ActionTrap::executeImp(const Ptr &actor) + void ActionTrap::executeImp(const Ptr& actor) { osg::Vec3f actorPosition(actor.getRefData().getPosition().asVec3()); osg::Vec3f trapPosition(mTrapSource.getRefData().getPosition().asVec3()); @@ -17,7 +17,9 @@ namespace MWWorld // radius, because for most trap spells this is 1 foot, much less than the activation distance. // Using activation distance as the trap range. - if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr() && MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > trapRange) // player activated object outside range of trap + if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr() + && MWBase::Environment::get().getWorld()->getDistanceToFacedObject() + > trapRange) // player activated object outside range of trap { MWMechanics::CastSpell cast(mTrapSource, mTrapSource); cast.mHitPosition = trapPosition; @@ -28,7 +30,7 @@ namespace MWWorld MWMechanics::CastSpell cast(mTrapSource, actor); cast.mHitPosition = actorPosition; cast.cast(mSpellId); - } - mTrapSource.getCellRef().setTrap(""); + } + mTrapSource.getCellRef().setTrap(ESM::RefId()); } } diff --git a/apps/openmw/mwworld/actiontrap.hpp b/apps/openmw/mwworld/actiontrap.hpp index fe51959d1b4..02200db6bf8 100644 --- a/apps/openmw/mwworld/actiontrap.hpp +++ b/apps/openmw/mwworld/actiontrap.hpp @@ -9,19 +9,21 @@ namespace MWWorld { class ActionTrap : public Action { - std::string mSpellId; - MWWorld::Ptr mTrapSource; - - void executeImp (const Ptr& actor) override; - - public: - - /// @param spellId - /// @param trapSource - ActionTrap (const std::string& spellId, const Ptr& trapSource) - : Action(false, trapSource), mSpellId(spellId), mTrapSource(trapSource) {} + ESM::RefId mSpellId; + MWWorld::Ptr mTrapSource; + + void executeImp(const Ptr& actor) override; + + public: + /// @param spellId + /// @param trapSource + ActionTrap(const ESM::RefId& spellId, const Ptr& trapSource) + : Action(false, trapSource) + , mSpellId(spellId) + , mTrapSource(trapSource) + { + } }; } - #endif diff --git a/apps/openmw/mwworld/cell.cpp b/apps/openmw/mwworld/cell.cpp new file mode 100644 index 00000000000..1bc58d89c36 --- /dev/null +++ b/apps/openmw/mwworld/cell.cpp @@ -0,0 +1,106 @@ +#include "cell.hpp" + +#include "esmstore.hpp" + +#include "../mwbase/environment.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace MWWorld +{ + namespace + { + std::string getDescription(const ESM4::World& value) + { + if (!value.mEditorId.empty()) + return value.mEditorId; + + return ESM::RefId(value.mId).serializeText(); + } + + std::string getCellDescription(const ESM4::Cell& cell, const ESM4::World* world) + { + std::string result; + + if (!cell.mEditorId.empty()) + result = cell.mEditorId; + else if (world != nullptr && cell.isExterior()) + result = getDescription(*world); + else + result = cell.mId.serializeText(); + + if (cell.isExterior()) + result += " (" + std::to_string(cell.mX) + ", " + std::to_string(cell.mY) + ")"; + + return result; + } + } + + Cell::Cell(const ESM4::Cell& cell) + : ESM::CellVariant(cell) + , mIsExterior(!(cell.mCellFlags & ESM4::CELL_Interior)) + , mIsQuasiExterior(cell.mCellFlags & ESM4::CELL_QuasiExt) + , mHasWater(cell.mCellFlags & ESM4::CELL_HasWater) + , mNoSleep(false) // No such notion in ESM4 + , mGridPos(cell.mX, cell.mY) + , mDisplayname(cell.mFullName) + , mNameID(cell.mEditorId) + , mRegion(ESM::RefId()) // Unimplemented for now + , mId(cell.mId) + , mParent(cell.mParent) + , mWaterHeight(cell.mWaterHeight) + , mMood{ + .mAmbiantColor = cell.mLighting.ambient, + .mDirectionalColor = cell.mLighting.directional, + .mFogColor = cell.mLighting.fogColor, + // TODO: use ESM4::Lighting fog parameters + .mFogDensity = 1.f, + } + { + const ESM4::World* world = MWBase::Environment::get().getESMStore()->get().search(mParent); + if (isExterior()) + { + if (world == nullptr) + throw std::runtime_error( + "Cell " + cell.mId.toDebugString() + " parent world " + mParent.toDebugString() + " is not found"); + mWaterHeight = world->mWaterLevel; + } + mDescription = getCellDescription(cell, world); + } + + Cell::Cell(const ESM::Cell& cell) + : ESM::CellVariant(cell) + , mIsExterior(!(cell.mData.mFlags & ESM::Cell::Interior)) + , mIsQuasiExterior(cell.mData.mFlags & ESM::Cell::QuasiEx) + , mHasWater(cell.mData.mFlags & ESM::Cell::HasWater) + , mNoSleep(cell.mData.mFlags & ESM::Cell::NoSleep) + , mGridPos(cell.getGridX(), cell.getGridY()) + , mDisplayname(cell.mName) + , mNameID(cell.mName) + , mRegion(cell.mRegion) + , mId(cell.mId) + , mParent(ESM::Cell::sDefaultWorldspaceId) + , mWaterHeight(cell.mWater) + , mDescription(cell.getDescription()) + , mMood{ + .mAmbiantColor = cell.mAmbi.mAmbient, + .mDirectionalColor = cell.mAmbi.mSunlight, + .mFogColor = cell.mAmbi.mFog, + .mFogDensity = cell.mAmbi.mFogDensity, + } + { + if (isExterior()) + { + mWaterHeight = -1.f; + mHasWater = true; + } + else + mGridPos = {}; + } +} diff --git a/apps/openmw/mwworld/cell.hpp b/apps/openmw/mwworld/cell.hpp new file mode 100644 index 00000000000..c9586a01fa8 --- /dev/null +++ b/apps/openmw/mwworld/cell.hpp @@ -0,0 +1,76 @@ +#ifndef OPENW_MWORLD_CELL +#define OPENW_MWORLD_CELL + +#include + +#include +#include +#include + +namespace ESM +{ + struct Cell; +} + +namespace ESM4 +{ + struct Cell; +} + +namespace MWWorld +{ + class CellStore; + + class Cell : public ESM::CellVariant + { + struct MoodData + { + uint32_t mAmbiantColor; + uint32_t mDirectionalColor; + uint32_t mFogColor; + float mFogDensity; + }; + + public: + explicit Cell(const ESM4::Cell& cell); + explicit Cell(const ESM::Cell& cell); + + int getGridX() const { return mGridPos.x(); } + int getGridY() const { return mGridPos.y(); } + bool isExterior() const { return mIsExterior; } + bool isQuasiExterior() const { return mIsQuasiExterior; } + bool hasWater() const { return mHasWater; } + bool noSleep() const { return mNoSleep; } + const ESM::RefId& getRegion() const { return mRegion; } + std::string_view getNameId() const { return mNameID; } + std::string_view getDisplayName() const { return mDisplayname; } + std::string_view getDescription() const { return mDescription; } + const MoodData& getMood() const { return mMood; } + float getWaterHeight() const { return mWaterHeight; } + const ESM::RefId& getId() const { return mId; } + ESM::RefId getWorldSpace() const { return mIsExterior ? mParent : mId; } + + ESM::ExteriorCellLocation getExteriorCellLocation() const + { + return ESM::ExteriorCellLocation(mGridPos.x(), mGridPos.y(), getWorldSpace()); + } + + private: + bool mIsExterior; + bool mIsQuasiExterior; + bool mHasWater; + bool mNoSleep; + + osg::Vec2i mGridPos; + std::string mDisplayname; // How the game displays it + std::string mNameID; // The name that will be used by the script and console commands + ESM::RefId mRegion; + ESM::RefId mId; + ESM::RefId mParent; + float mWaterHeight; + std::string mDescription; + MoodData mMood; + }; +} + +#endif diff --git a/apps/openmw/mwworld/cellpreloader.cpp b/apps/openmw/mwworld/cellpreloader.cpp index 44afde22ae7..05d6efc494b 100644 --- a/apps/openmw/mwworld/cellpreloader.cpp +++ b/apps/openmw/mwworld/cellpreloader.cpp @@ -1,19 +1,26 @@ #include "cellpreloader.hpp" +#include #include #include +#include + +#include #include -#include -#include +#include +#include +#include +#include +#include +#include #include #include -#include -#include -#include +#include +#include +#include #include -#include -#include +#include #include "../mwrender/landmanager.hpp" @@ -22,24 +29,36 @@ namespace MWWorld { - - struct ListModelsVisitor + namespace { - ListModelsVisitor(std::vector& out) - : mOut(out) + bool contains(std::span positions, const PositionCellGrid& contained, float tolerance) + { + const float squaredTolerance = tolerance * tolerance; + const auto predicate = [&](const PositionCellGrid& v) { + return (contained.mPosition - v.mPosition).length2() < squaredTolerance + && contained.mCellBounds == v.mCellBounds; + }; + return std::ranges::any_of(positions, predicate); + } + + bool contains( + std::span container, std::span contained, float tolerance) { + const auto predicate = [&](const PositionCellGrid& v) { return contains(container, v, tolerance); }; + return std::ranges::all_of(contained, predicate); } + } - virtual bool operator()(const MWWorld::Ptr& ptr) + struct ListModelsVisitor + { + bool operator()(const MWWorld::ConstPtr& ptr) { ptr.getClass().getModelsToPreload(ptr, mOut); return true; } - virtual ~ListModelsVisitor() = default; - - std::vector& mOut; + std::vector& mOut; }; /// Worker thread item: preload models in a cell. @@ -47,10 +66,12 @@ namespace MWWorld { public: /// Constructor to be called from the main thread. - PreloadItem(MWWorld::CellStore* cell, Resource::SceneManager* sceneManager, Resource::BulletShapeManager* bulletShapeManager, Resource::KeyframeManager* keyframeManager, Terrain::World* terrain, MWRender::LandManager* landManager, bool preloadInstances) + explicit PreloadItem(MWWorld::CellStore* cell, Resource::SceneManager* sceneManager, + Resource::BulletShapeManager* bulletShapeManager, Resource::KeyframeManager* keyframeManager, + Terrain::World* terrain, MWRender::LandManager* landManager, bool preloadInstances) : mIsExterior(cell->getCell()->isExterior()) - , mX(cell->getCell()->getGridX()) - , mY(cell->getCell()->getGridY()) + , mCellLocation(cell->getCell()->getExteriorCellLocation()) + , mCellId(cell->getCell()->getId()) , mSceneManager(sceneManager) , mBulletShapeManager(bulletShapeManager) , mKeyframeManager(keyframeManager) @@ -61,14 +82,11 @@ namespace MWWorld { mTerrainView = mTerrain->createView(); - ListModelsVisitor visitor (mMeshes); - cell->forEach(visitor); + ListModelsVisitor visitor{ mMeshes }; + cell->forEachConst(visitor); } - void abort() override - { - mAbort = true; - } + void abort() override { mAbort = true; } /// Preload work to be called from the worker thread. void doWork() override @@ -77,66 +95,63 @@ namespace MWWorld { try { - mTerrain->cacheCell(mTerrainView.get(), mX, mY); - mPreloadedObjects.insert(mLandManager->getLand(mX, mY)); + mTerrain->cacheCell(mTerrainView.get(), mCellLocation.mX, mCellLocation.mY); + mPreloadedObjects.insert(mLandManager->getLand(mCellLocation)); } - catch(std::exception&) + catch (const std::exception& e) { + Log(Debug::Warning) << "Failed to cache terrain for exterior cell " << mCellLocation << ": " + << e.what(); } } - for (std::string& mesh: mMeshes) + std::string mesh; + std::string kfname; + for (std::string_view path : mMeshes) { if (mAbort) break; try { - mesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mSceneManager->getVFS()); + const VFS::Manager& vfs = *mSceneManager->getVFS(); + mesh = Misc::ResourceHelpers::correctMeshPath(path); + mesh = Misc::ResourceHelpers::correctActorModelPath(mesh, &vfs); + + if (!vfs.exists(mesh)) + continue; - bool animated = false; size_t slashpos = mesh.find_last_of("/\\"); - if (slashpos != std::string::npos && slashpos != mesh.size()-1) + if (slashpos != std::string::npos && slashpos != mesh.size() - 1) { - Misc::StringUtils::lowerCaseInPlace(mesh); - if (mesh[slashpos+1] == 'x') + if (Misc::StringUtils::toLower(mesh[slashpos + 1]) == 'x' + && Misc::StringUtils::ciEndsWith(mesh, ".nif")) { - std::string kfname = mesh; - if(kfname.size() > 4 && kfname.compare(kfname.size()-4, 4, ".nif") == 0) - { - kfname.replace(kfname.size()-4, 4, ".kf"); - if (mSceneManager->getVFS()->exists(kfname)) - { - mPreloadedObjects.insert(mKeyframeManager->get(kfname)); - animated = true; - } - } + kfname = mesh; + kfname.replace(kfname.size() - 4, 4, ".kf"); + if (vfs.exists(kfname)) + mPreloadedObjects.insert(mKeyframeManager->get(kfname)); } } - if (mPreloadInstances && animated) - mPreloadedObjects.insert(mSceneManager->cacheInstance(mesh)); - else - mPreloadedObjects.insert(mSceneManager->getTemplate(mesh)); + mPreloadedObjects.insert(mSceneManager->getTemplate(VFS::Path::toNormalized(mesh))); if (mPreloadInstances) mPreloadedObjects.insert(mBulletShapeManager->cacheInstance(mesh)); else mPreloadedObjects.insert(mBulletShapeManager->getShape(mesh)); - } - catch (std::exception&) + catch (const std::exception& e) { - // ignore error for now, would spam the log too much - // error will be shown when visiting the cell + Log(Debug::Warning) << "Failed to preload mesh \"" << path << "\" from cell " << mCellId << ": " + << e.what(); } } } private: - typedef std::vector MeshList; bool mIsExterior; - int mX; - int mY; - MeshList mMeshes; + ESM::ExteriorCellLocation mCellLocation; + ESM::RefId mCellId; + std::vector mMeshes; Resource::SceneManager* mSceneManager; Resource::BulletShapeManager* mBulletShapeManager; Resource::KeyframeManager* mKeyframeManager; @@ -149,54 +164,42 @@ namespace MWWorld osg::ref_ptr mTerrainView; // keep a ref to the loaded objects to make sure it stays loaded as long as this cell is in the preloaded state - std::set > mPreloadedObjects; + std::set> mPreloadedObjects; }; class TerrainPreloadItem : public SceneUtil::WorkItem { public: - TerrainPreloadItem(const std::vector >& views, Terrain::World* world, const std::vector& preloadPositions) + explicit TerrainPreloadItem(const std::vector>& views, Terrain::World* world, + std::span preloadPositions) : mAbort(false) - , mProgress(views.size()) - , mProgressRange(0) , mTerrainViews(views) , mWorld(world) - , mPreloadPositions(preloadPositions) - { - } - - bool storeViews(double referenceTime) + , mPreloadPositions(preloadPositions.begin(), preloadPositions.end()) { - for (unsigned int i=0; istoreView(mTerrainViews[i], referenceTime)) - return false; - return true; } void doWork() override { - for (unsigned int i=0; ireset(); - mWorld->preload(mTerrainViews[i], mPreloadPositions[i].first, mPreloadPositions[i].second, mAbort, mProgress[i], mProgressRange); + mWorld->preload(mTerrainViews[i], mPreloadPositions[i].mPosition, mPreloadPositions[i].mCellBounds, + mAbort, mLoadingReporter); } + mLoadingReporter.complete(); } - void abort() override - { - mAbort = true; - } + void abort() override { mAbort = true; } - int getProgress() const { return !mProgress.empty() ? mProgress[0].load() : 0; } - int getProgressRange() const { return !mProgress.empty() && mProgress[0].load() ? mProgressRange : 0; } + void wait(Loading::Listener& listener) const { mLoadingReporter.wait(listener); } private: std::atomic mAbort; - std::vector> mProgress; - int mProgressRange; - std::vector > mTerrainViews; + std::vector> mTerrainViews; Terrain::World* mWorld; - std::vector mPreloadPositions; + std::vector mPreloadPositions; + Loading::Reporter mLoadingReporter; }; /// Worker thread item: update the resource system's cache, effectively deleting unused entries. @@ -209,68 +212,45 @@ namespace MWWorld { } - void doWork() override - { - mResourceSystem->updateCache(mReferenceTime); - } + void doWork() override { mResourceSystem->updateCache(mReferenceTime); } private: double mReferenceTime; Resource::ResourceSystem* mResourceSystem; }; - CellPreloader::CellPreloader(Resource::ResourceSystem* resourceSystem, Resource::BulletShapeManager* bulletShapeManager, Terrain::World* terrain, MWRender::LandManager* landManager) + CellPreloader::CellPreloader(Resource::ResourceSystem* resourceSystem, + Resource::BulletShapeManager* bulletShapeManager, Terrain::World* terrain, MWRender::LandManager* landManager) : mResourceSystem(resourceSystem) , mBulletShapeManager(bulletShapeManager) , mTerrain(terrain) , mLandManager(landManager) , mExpiryDelay(0.0) - , mMinCacheSize(0) - , mMaxCacheSize(0) , mPreloadInstances(true) , mLastResourceCacheUpdate(0.0) - , mStoreViewsFailCount(0) + , mLoadedTerrainTimestamp(0.0) { } CellPreloader::~CellPreloader() { - if (mTerrainPreloadItem) - { - mTerrainPreloadItem->abort(); - mTerrainPreloadItem->waitTillDone(); - mTerrainPreloadItem = nullptr; - } - - if (mUpdateCacheItem) - { - mUpdateCacheItem->waitTillDone(); - mUpdateCacheItem = nullptr; - } - - for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end();++it) - it->second.mWorkItem->abort(); - - for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end();++it) - it->second.mWorkItem->waitTillDone(); - - mPreloadCells.clear(); + clearAllTasks(); } - void CellPreloader::preload(CellStore *cell, double timestamp) + void CellPreloader::preload(CellStore& cell, double timestamp) { if (!mWorkQueue) { Log(Debug::Error) << "Error: can't preload, no work queue set"; return; } - if (cell->getState() == CellStore::State_Unloaded) + if (cell.getState() == CellStore::State_Unloaded) { Log(Debug::Error) << "Error: can't preload objects for unloaded cell"; return; } - PreloadMap::iterator found = mPreloadCells.find(cell); + PreloadMap::iterator found = mPreloadCells.find(&cell); if (found != mPreloadCells.end()) { // already preloaded, nothing to do other than updating the timestamp @@ -297,30 +277,33 @@ namespace MWWorld { oldestCell->second.mWorkItem->abort(); mPreloadCells.erase(oldestCell); + ++mEvicted; } else return; } - osg::ref_ptr item (new PreloadItem(cell, mResourceSystem->getSceneManager(), mBulletShapeManager, mResourceSystem->getKeyframeManager(), mTerrain, mLandManager, mPreloadInstances)); + osg::ref_ptr item(new PreloadItem(&cell, mResourceSystem->getSceneManager(), mBulletShapeManager, + mResourceSystem->getKeyframeManager(), mTerrain, mLandManager, mPreloadInstances)); mWorkQueue->addWorkItem(item); - mPreloadCells[cell] = PreloadEntry(timestamp, item); + mPreloadCells.emplace(&cell, PreloadEntry(timestamp, item)); + ++mAdded; } - void CellPreloader::notifyLoaded(CellStore *cell) + void CellPreloader::notifyLoaded(CellStore* cell) { PreloadMap::iterator found = mPreloadCells.find(cell); if (found != mPreloadCells.end()) { - // do the deletion in the background thread if (found->second.mWorkItem) { found->second.mWorkItem->abort(); - mUnrefQueue->push(mPreloadCells[cell].mWorkItem); + found->second.mWorkItem = nullptr; } mPreloadCells.erase(found); + ++mLoaded; } } @@ -331,7 +314,7 @@ namespace MWWorld if (it->second.mWorkItem) { it->second.mWorkItem->abort(); - mUnrefQueue->push(it->second.mWorkItem); + it->second.mWorkItem = nullptr; } mPreloadCells.erase(it++); @@ -347,9 +330,10 @@ namespace MWWorld if (it->second.mWorkItem) { it->second.mWorkItem->abort(); - mUnrefQueue->push(it->second.mWorkItem); + it->second.mWorkItem = nullptr; } mPreloadCells.erase(it++); + ++mExpired; } else ++it; @@ -357,7 +341,8 @@ namespace MWWorld if (timestamp - mLastResourceCacheUpdate > 1.0 && (!mUpdateCacheItem || mUpdateCacheItem->isDone())) { - // the resource cache is cleared from the worker thread so that we're not holding up the main thread with delete operations + // the resource cache is cleared from the worker thread so that we're not holding up the main thread with + // delete operations mUpdateCacheItem = new UpdateCacheItem(mResourceSystem, timestamp); mWorkQueue->addWorkItem(mUpdateCacheItem, true); mLastResourceCacheUpdate = timestamp; @@ -365,18 +350,8 @@ namespace MWWorld if (mTerrainPreloadItem && mTerrainPreloadItem->isDone()) { - if (!mTerrainPreloadItem->storeViews(timestamp)) - { - if (++mStoreViewsFailCount > 100) - { - OSG_ALWAYS << "paging views are rebuilt every frame, please check for faulty enable/disable scripts." << std::endl; - mStoreViewsFailCount = 0; - } - setTerrainPreloadPositions(std::vector()); - } - else - mStoreViewsFailCount = 0; - mTerrainPreloadItem = nullptr; + mLoadedTerrainPositions = mTerrainPreloadPositions; + mLoadedTerrainTimestamp = timestamp; } } @@ -385,117 +360,56 @@ namespace MWWorld mExpiryDelay = expiryDelay; } - void CellPreloader::setMinCacheSize(unsigned int num) - { - mMinCacheSize = num; - } - - void CellPreloader::setMaxCacheSize(unsigned int num) - { - mMaxCacheSize = num; - } - void CellPreloader::setPreloadInstances(bool preload) { mPreloadInstances = preload; } - unsigned int CellPreloader::getMaxCacheSize() const - { - return mMaxCacheSize; - } - void CellPreloader::setWorkQueue(osg::ref_ptr workQueue) { mWorkQueue = workQueue; } - void CellPreloader::setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue) - { - mUnrefQueue = unrefQueue; - } - - bool CellPreloader::syncTerrainLoad(const std::vector &positions, int& progress, int& progressRange, double timestamp) + void CellPreloader::syncTerrainLoad(Loading::Listener& listener) { - if (!mTerrainPreloadItem) - return true; - else if (mTerrainPreloadItem->isDone()) - { - if (mTerrainPreloadItem->storeViews(timestamp)) - { - mTerrainPreloadItem = nullptr; - return true; - } - else - { - setTerrainPreloadPositions(std::vector()); - setTerrainPreloadPositions(positions); - return false; - } - } - else - { - progress = mTerrainPreloadItem->getProgress(); - progressRange = mTerrainPreloadItem->getProgressRange(); - return false; - } + if (mTerrainPreloadItem != nullptr && !mTerrainPreloadItem->isDone()) + mTerrainPreloadItem->wait(listener); } - void CellPreloader::abortTerrainPreloadExcept(const CellPreloader::PositionCellGrid *exceptPos) + void CellPreloader::abortTerrainPreloadExcept(const PositionCellGrid* exceptPos) { - const float resetThreshold = ESM::Land::REAL_SIZE; - for (const auto& pos : mTerrainPreloadPositions) - if (exceptPos && (pos.first-exceptPos->first).length2() < resetThreshold*resetThreshold && pos.second == exceptPos->second) - return; + if (exceptPos != nullptr && contains(mTerrainPreloadPositions, *exceptPos, Constants::CellSizeInUnits)) + return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) { mTerrainPreloadItem->abort(); mTerrainPreloadItem->waitTillDone(); } - setTerrainPreloadPositions(std::vector()); - } - - bool contains(const std::vector& container, const std::vector& contained) - { - for (const auto& pos : contained) - { - bool found = false; - for (const auto& pos2 : container) - { - if ((pos.first-pos2.first).length2() < 1 && pos.second == pos2.second) - { - found = true; - break; - } - } - if (!found) return false; - } - return true; + setTerrainPreloadPositions({}); } - void CellPreloader::setTerrainPreloadPositions(const std::vector &positions) + void CellPreloader::setTerrainPreloadPositions(std::span positions) { if (positions.empty()) + { mTerrainPreloadPositions.clear(); - else if (contains(mTerrainPreloadPositions, positions)) + mLoadedTerrainPositions.clear(); + } + else if (contains(mTerrainPreloadPositions, positions, 128.f)) return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) return; else { if (mTerrainViews.size() > positions.size()) - { - for (unsigned int i=positions.size(); ipush(mTerrainViews[i]); mTerrainViews.resize(positions.size()); - } else if (mTerrainViews.size() < positions.size()) { - for (unsigned int i=mTerrainViews.size(); icreateView()); } - mTerrainPreloadPositions = positions; + mTerrainPreloadPositions.assign(positions.begin(), positions.end()); if (!positions.empty()) { mTerrainPreloadItem = new TerrainPreloadItem(mTerrainViews, mTerrain, positions); @@ -504,4 +418,51 @@ namespace MWWorld } } + bool CellPreloader::isTerrainLoaded(const PositionCellGrid& position, double referenceTime) const + { + return mLoadedTerrainTimestamp + mResourceSystem->getSceneManager()->getExpiryDelay() > referenceTime + && contains(mLoadedTerrainPositions, position, Constants::CellSizeInUnits); + } + + void CellPreloader::setTerrain(Terrain::World* terrain) + { + if (terrain != mTerrain) + { + clearAllTasks(); + mTerrain = terrain; + } + } + + void CellPreloader::clearAllTasks() + { + if (mTerrainPreloadItem) + { + mTerrainPreloadItem->abort(); + mTerrainPreloadItem->waitTillDone(); + mTerrainPreloadItem = nullptr; + } + + if (mUpdateCacheItem) + { + mUpdateCacheItem->waitTillDone(); + mUpdateCacheItem = nullptr; + } + + for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end(); ++it) + it->second.mWorkItem->abort(); + + for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end(); ++it) + it->second.mWorkItem->waitTillDone(); + + mPreloadCells.clear(); + } + + void CellPreloader::reportStats(unsigned int frameNumber, osg::Stats& stats) const + { + stats.setAttribute(frameNumber, "CellPreloader Count", mPreloadCells.size()); + stats.setAttribute(frameNumber, "CellPreloader Added", mAdded); + stats.setAttribute(frameNumber, "CellPreloader Evicted", mEvicted); + stats.setAttribute(frameNumber, "CellPreloader Loaded", mLoaded); + stats.setAttribute(frameNumber, "CellPreloader Expired", mExpired); + } } diff --git a/apps/openmw/mwworld/cellpreloader.hpp b/apps/openmw/mwworld/cellpreloader.hpp index e719f2e6068..405ba96a2e4 100644 --- a/apps/openmw/mwworld/cellpreloader.hpp +++ b/apps/openmw/mwworld/cellpreloader.hpp @@ -1,12 +1,20 @@ #ifndef OPENMW_MWWORLD_CELLPRELOADER_H #define OPENMW_MWWORLD_CELLPRELOADER_H -#include -#include -#include -#include +#include "positioncellgrid.hpp" + #include +#include + +#include +#include + +namespace osg +{ + class Stats; +} + namespace Resource { class ResourceSystem; @@ -19,14 +27,14 @@ namespace Terrain class View; } -namespace SceneUtil +namespace MWRender { - class UnrefQueue; + class LandManager; } -namespace MWRender +namespace Loading { - class LandManager; + class Listener; } namespace MWWorld @@ -37,12 +45,13 @@ namespace MWWorld class CellPreloader { public: - CellPreloader(Resource::ResourceSystem* resourceSystem, Resource::BulletShapeManager* bulletShapeManager, Terrain::World* terrain, MWRender::LandManager* landManager); + CellPreloader(Resource::ResourceSystem* resourceSystem, Resource::BulletShapeManager* bulletShapeManager, + Terrain::World* terrain, MWRender::LandManager* landManager); ~CellPreloader(); /// Ask a background thread to preload rendering meshes and collision shapes for objects in this cell. /// @note The cell itself must be in State_Loaded or State_Preloaded. - void preload(MWWorld::CellStore* cell, double timestamp); + void preload(MWWorld::CellStore& cell, double timestamp); void notifyLoaded(MWWorld::CellStore* cell); @@ -55,46 +64,49 @@ namespace MWWorld void setExpiryDelay(double expiryDelay); /// The minimum number of preloaded cells before unused cells get thrown out. - void setMinCacheSize(unsigned int num); + void setMinCacheSize(std::size_t value) { mMinCacheSize = value; } /// The maximum number of preloaded cells. - void setMaxCacheSize(unsigned int num); + void setMaxCacheSize(std::size_t value) { mMaxCacheSize = value; } /// Enables the creation of instances in the preloading thread. void setPreloadInstances(bool preload); - unsigned int getMaxCacheSize() const; + std::size_t getMaxCacheSize() const { return mMaxCacheSize; } + + std::size_t getCacheSize() const { return mPreloadCells.size(); } void setWorkQueue(osg::ref_ptr workQueue); - void setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue); + void setTerrainPreloadPositions(std::span positions); - typedef std::pair PositionCellGrid; - void setTerrainPreloadPositions(const std::vector& positions); + void syncTerrainLoad(Loading::Listener& listener); + void abortTerrainPreloadExcept(const PositionCellGrid* exceptPos); + bool isTerrainLoaded(const PositionCellGrid& position, double referenceTime) const; + void setTerrain(Terrain::World* terrain); - bool syncTerrainLoad(const std::vector &positions, int& progress, int& progressRange, double timestamp); - void abortTerrainPreloadExcept(const PositionCellGrid *exceptPos); + void reportStats(unsigned int frameNumber, osg::Stats& stats) const; private: + void clearAllTasks(); + Resource::ResourceSystem* mResourceSystem; Resource::BulletShapeManager* mBulletShapeManager; Terrain::World* mTerrain; MWRender::LandManager* mLandManager; osg::ref_ptr mWorkQueue; - osg::ref_ptr mUnrefQueue; double mExpiryDelay; - unsigned int mMinCacheSize; - unsigned int mMaxCacheSize; + std::size_t mMinCacheSize = 0; + std::size_t mMaxCacheSize = 0; bool mPreloadInstances; double mLastResourceCacheUpdate; - int mStoreViewsFailCount; struct PreloadEntry { PreloadEntry(double timestamp, osg::ref_ptr workItem) : mTimeStamp(timestamp) - , mWorkItem(workItem) + , mWorkItem(std::move(workItem)) { } PreloadEntry() @@ -110,10 +122,17 @@ namespace MWWorld // Cells that are currently being preloaded, or have already finished preloading PreloadMap mPreloadCells; - std::vector > mTerrainViews; + std::vector> mTerrainViews; std::vector mTerrainPreloadPositions; osg::ref_ptr mTerrainPreloadItem; osg::ref_ptr mUpdateCacheItem; + + std::vector mLoadedTerrainPositions; + double mLoadedTerrainTimestamp; + std::size_t mEvicted = 0; + std::size_t mAdded = 0; + std::size_t mExpired = 0; + std::size_t mLoaded = 0; }; } diff --git a/apps/openmw/mwworld/cellref.cpp b/apps/openmw/mwworld/cellref.cpp index 188a80ae140..854348c2a80 100644 --- a/apps/openmw/mwworld/cellref.cpp +++ b/apps/openmw/mwworld/cellref.cpp @@ -1,289 +1,405 @@ #include "cellref.hpp" -#include +#include + +#include +#include +#include +#include +#include + +#include "apps/openmw/mwbase/environment.hpp" +#include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwmechanics/spellutil.hpp" +#include "apps/openmw/mwworld/esmstore.hpp" namespace MWWorld { - - const ESM::RefNum& CellRef::getRefNum() const + CellRef::CellRef(const ESM::CellRef& ref) + : mCellRef(ESM::ReferenceVariant(ref)) { - return mCellRef.mRefNum; } - bool CellRef::hasContentFile() const + CellRef::CellRef(const ESM4::Reference& ref) + : mCellRef(ESM::ReferenceVariant(ref)) { - return mCellRef.mRefNum.hasContentFile(); } - void CellRef::unsetRefNum() + CellRef::CellRef(const ESM4::ActorCharacter& ref) + : mCellRef(ESM::ReferenceVariant(ref)) { - mCellRef.mRefNum.unset(); } - std::string CellRef::getRefId() const + ESM::RefNum CellRef::getRefNum() const noexcept { - return mCellRef.mRefID; + return std::visit(ESM::VisitOverload{ + [&](const ESM4::Reference& ref) -> ESM::RefNum { return ref.mId; }, + [&](const ESM4::ActorCharacter& ref) -> ESM::RefNum { return ref.mId; }, + [&](const ESM::CellRef& ref) -> ESM::RefNum { return ref.mRefNum; }, + }, + mCellRef.mVariant); } - const std::string* CellRef::getRefIdPtr() const + ESM::RefNum CellRef::getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum) { - return &mCellRef.mRefID; + ESM::RefNum& refNum = std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& ref) -> ESM::RefNum& { return ref.mId; }, + [&](ESM4::ActorCharacter& ref) -> ESM::RefNum& { return ref.mId; }, + [&](ESM::CellRef& ref) -> ESM::RefNum& { return ref.mRefNum; }, + }, + mCellRef.mVariant); + if (!refNum.isSet()) + { + // Generated RefNums have negative mContentFile + assert(lastAssignedRefNum.mContentFile < 0); + lastAssignedRefNum.mIndex++; + if (lastAssignedRefNum.mIndex == 0) // mIndex overflow, so mContentFile should be changed + { + if (lastAssignedRefNum.mContentFile > std::numeric_limits::min()) + lastAssignedRefNum.mContentFile--; + else + Log(Debug::Error) << "RefNum counter overflow in CellRef::getOrAssignRefNum"; + } + refNum = lastAssignedRefNum; + mChanged = true; + } + return refNum; } - bool CellRef::getTeleport() const + void CellRef::setRefNum(ESM::RefNum refNum) { - return mCellRef.mTeleport; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& ref) { ref.mId = refNum; }, + [&](ESM4::ActorCharacter& ref) { ref.mId = refNum; }, + [&](ESM::CellRef& ref) { ref.mRefNum = refNum; }, + }, + mCellRef.mVariant); } + static const std::string emptyString = ""; + ESM::Position CellRef::getDoorDest() const { - return mCellRef.mDoorDest; - } - std::string CellRef::getDestCell() const - { - return mCellRef.mDestCell; + return std::visit( + ESM::VisitOverload{ + [&](const ESM4::Reference& ref) { return ref.mDoor.destPos; }, + [&](const ESM::CellRef& ref) -> ESM::Position { return ref.mDoorDest; }, + [&](const ESM4::ActorCharacter&) -> ESM::Position { throw std::logic_error("Not applicable"); }, + }, + mCellRef.mVariant); } - float CellRef::getScale() const + ESM::RefId CellRef::getDestCell() const { - return mCellRef.mScale; + auto esm3Visit = [&](const ESM::CellRef& ref) -> ESM::RefId { + if (!ref.mDestCell.empty()) + { + return ESM::RefId::stringRefId(ref.mDestCell); + } + else + { + const auto cellPos = ESM::positionToExteriorCellLocation(ref.mDoorDest.pos[0], ref.mDoorDest.pos[1]); + return ESM::RefId::esm3ExteriorCell(cellPos.mX, cellPos.mY); + } + }; + auto esm4Visit = [&](const ESM4::Reference& ref) -> ESM::RefId { + if (ref.mDoor.destDoor.isZeroOrUnset()) + return ESM::RefId(); + const ESM4::Reference* refDest + = MWBase::Environment::get().getESMStore()->get().searchStatic(ref.mDoor.destDoor); + if (refDest) + return refDest->mParent; + return ESM::RefId(); + }; + auto actorDestCell + = [&](const ESM4::ActorCharacter&) -> ESM::RefId { throw std::logic_error("Not applicable"); }; + + return std::visit(ESM::VisitOverload{ esm3Visit, esm4Visit, actorDestCell }, mCellRef.mVariant); } void CellRef::setScale(float scale) { - if (scale != mCellRef.mScale) + if (scale != getScale()) { mChanged = true; - mCellRef.mScale = scale; + std::visit([scale](auto&& ref) { ref.mScale = scale; }, mCellRef.mVariant); } } - ESM::Position CellRef::getPosition() const - { - return mCellRef.mPos; - } - - void CellRef::setPosition(const ESM::Position &position) + void CellRef::setPosition(const ESM::Position& position) { mChanged = true; - mCellRef.mPos = position; + std::visit([&position](auto&& ref) { ref.mPos = position; }, mCellRef.mVariant); } float CellRef::getEnchantmentCharge() const { - return mCellRef.mEnchantmentCharge; + return std::visit(ESM::VisitOverload{ + [&](const ESM4::Reference& /*ref*/) { return 0.f; }, + [&](const ESM::CellRef& ref) { return ref.mEnchantmentCharge; }, + [&](const ESM4::ActorCharacter&) -> float { throw std::logic_error("Not applicable"); }, + }, + mCellRef.mVariant); } - float CellRef::getNormalizedEnchantmentCharge(int maxCharge) const + float CellRef::getNormalizedEnchantmentCharge(const ESM::Enchantment& enchantment) const { + const int maxCharge = MWMechanics::getEnchantmentCharge(enchantment); if (maxCharge == 0) { return 0; } - else if (mCellRef.mEnchantmentCharge == -1) + else if (getEnchantmentCharge() == -1) { return 1; } else { - return mCellRef.mEnchantmentCharge / static_cast(maxCharge); + return getEnchantmentCharge() / static_cast(maxCharge); } } void CellRef::setEnchantmentCharge(float charge) { - if (charge != mCellRef.mEnchantmentCharge) + if (charge != getEnchantmentCharge()) { mChanged = true; - mCellRef.mEnchantmentCharge = charge; - } - } - int CellRef::getCharge() const - { - return mCellRef.mChargeInt; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mEnchantmentCharge = charge; }, + }, + mCellRef.mVariant); + } } void CellRef::setCharge(int charge) { - if (charge != mCellRef.mChargeInt) - { - mChanged = true; - mCellRef.mChargeInt = charge; - } + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mChargeInt = charge; }, + }, + mCellRef.mVariant); } void CellRef::applyChargeRemainderToBeSubtracted(float chargeRemainder) { - mCellRef.mChargeIntRemainder += std::abs(chargeRemainder); - if (mCellRef.mChargeIntRemainder > 1.0f) - { - float newChargeRemainder = (mCellRef.mChargeIntRemainder - std::floor(mCellRef.mChargeIntRemainder)); - if (mCellRef.mChargeInt <= static_cast(mCellRef.mChargeIntRemainder)) - { - mCellRef.mChargeInt = 0; - } - else + auto esm3Visit = [&](ESM::CellRef& cellRef3) { + cellRef3.mChargeIntRemainder -= std::abs(chargeRemainder); + if (cellRef3.mChargeIntRemainder <= -1.0f) { - mCellRef.mChargeInt -= static_cast(mCellRef.mChargeIntRemainder); + float newChargeRemainder = std::modf(cellRef3.mChargeIntRemainder, &cellRef3.mChargeIntRemainder); + cellRef3.mChargeInt += static_cast(cellRef3.mChargeIntRemainder); + cellRef3.mChargeIntRemainder = newChargeRemainder; + if (cellRef3.mChargeInt < 0) + cellRef3.mChargeInt = 0; } - mCellRef.mChargeIntRemainder = newChargeRemainder; - } + }; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + esm3Visit, + }, + mCellRef.mVariant); } - float CellRef::getChargeFloat() const + void CellRef::setChargeIntRemainder(float chargeRemainder) { - return mCellRef.mChargeFloat; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mChargeIntRemainder = chargeRemainder; }, + }, + mCellRef.mVariant); } void CellRef::setChargeFloat(float charge) { - if (charge != mCellRef.mChargeFloat) - { - mChanged = true; - mCellRef.mChargeFloat = charge; - } - } - - std::string CellRef::getOwner() const - { - return mCellRef.mOwner; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mChargeFloat = charge; }, + }, + mCellRef.mVariant); } - std::string CellRef::getGlobalVariable() const + const std::string& CellRef::getGlobalVariable() const { - return mCellRef.mGlobalVariable; + return std::visit(ESM::VisitOverload{ + [&](const ESM4::Reference& /*ref*/) -> const std::string& { return emptyString; }, + [&](const ESM4::ActorCharacter& /*ref*/) -> const std::string& { return emptyString; }, + [&](const ESM::CellRef& ref) -> const std::string& { return ref.mGlobalVariable; }, + }, + mCellRef.mVariant); } void CellRef::resetGlobalVariable() { - if (!mCellRef.mGlobalVariable.empty()) + if (!getGlobalVariable().empty()) { mChanged = true; - mCellRef.mGlobalVariable.erase(); + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter& /*ref*/) {}, + [&](ESM::CellRef& ref) { ref.mGlobalVariable.erase(); }, + }, + mCellRef.mVariant); } } void CellRef::setFactionRank(int factionRank) { - if (factionRank != mCellRef.mFactionRank) + if (factionRank != getFactionRank()) { mChanged = true; - mCellRef.mFactionRank = factionRank; + std::visit(ESM::VisitOverload{ + [&](ESM4::ActorCharacter&) {}, [&](auto&& ref) { ref.mFactionRank = factionRank; } }, + mCellRef.mVariant); } } - int CellRef::getFactionRank() const + void CellRef::setOwner(const ESM::RefId& owner) { - return mCellRef.mFactionRank; - } - - void CellRef::setOwner(const std::string &owner) - { - if (owner != mCellRef.mOwner) + if (owner != getOwner()) { - mChanged = true; - mCellRef.mOwner = owner; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mOwner = owner; }, + }, + mCellRef.mVariant); } } - std::string CellRef::getSoul() const + void CellRef::setSoul(const ESM::RefId& soul) { - return mCellRef.mSoul; - } - - void CellRef::setSoul(const std::string &soul) - { - if (soul != mCellRef.mSoul) + if (soul != getSoul()) { mChanged = true; - mCellRef.mSoul = soul; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mSoul = soul; }, + }, + mCellRef.mVariant); } } - std::string CellRef::getFaction() const + void CellRef::setFaction(const ESM::RefId& faction) { - return mCellRef.mFaction; - } - - void CellRef::setFaction(const std::string &faction) - { - if (faction != mCellRef.mFaction) + if (faction != getFaction()) { mChanged = true; - mCellRef.mFaction = faction; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mFaction = faction; }, + }, + mCellRef.mVariant); } } - int CellRef::getLockLevel() const - { - return mCellRef.mLockLevel; - } - void CellRef::setLockLevel(int lockLevel) { - if (lockLevel != mCellRef.mLockLevel) + if (lockLevel != getLockLevel()) { mChanged = true; - mCellRef.mLockLevel = lockLevel; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& ref) { ref.mLockLevel = lockLevel; }, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mLockLevel = lockLevel; }, + }, + mCellRef.mVariant); } } void CellRef::lock(int lockLevel) { - if(lockLevel != 0) - setLockLevel(abs(lockLevel)); //Changes lock to locklevel, if positive - else - setLockLevel(ESM::UnbreakableLock); // If zero, set to max lock level + setLockLevel(lockLevel); + setLocked(true); } void CellRef::unlock() { - setLockLevel(-abs(mCellRef.mLockLevel)); //Makes lockLevel negative + setLockLevel(-getLockLevel()); + setLocked(false); } - std::string CellRef::getKey() const + bool CellRef::isLocked() const { - return mCellRef.mKey; + struct Visitor + { + bool operator()(const ESM::CellRef& ref) { return ref.mIsLocked; } + bool operator()(const ESM4::Reference& ref) { return ref.mIsLocked; } + bool operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); } - std::string CellRef::getTrap() const + void CellRef::setLocked(bool locked) { - return mCellRef.mTrap; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& ref) { ref.mIsLocked = locked; }, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mIsLocked = locked; }, + }, + mCellRef.mVariant); } - void CellRef::setTrap(const std::string& trap) + void CellRef::setTrap(const ESM::RefId& trap) { - if (trap != mCellRef.mTrap) + if (trap != getTrap()) { mChanged = true; - mCellRef.mTrap = trap; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mTrap = trap; }, + }, + mCellRef.mVariant); } } - int CellRef::getGoldValue() const + void CellRef::setKey(const ESM::RefId& key) { - return mCellRef.mGoldValue; - } - - void CellRef::setGoldValue(int value) - { - if (value != mCellRef.mGoldValue) + if (key != getKey()) { mChanged = true; - mCellRef.mGoldValue = value; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& /*ref*/) {}, + [&](ESM4::ActorCharacter&) {}, + [&](ESM::CellRef& ref) { ref.mKey = key; }, + }, + mCellRef.mVariant); } } - void CellRef::writeState(ESM::ObjectState &state) const + void CellRef::setCount(int value) { - state.mRef = mCellRef; + if (value != getCount(false)) + { + mChanged = true; + std::visit(ESM::VisitOverload{ + [&](ESM4::Reference& ref) { ref.mCount = value; }, + [&](ESM4::ActorCharacter& ref) { ref.mCount = value; }, + [&](ESM::CellRef& ref) { ref.mCount = value; }, + }, + mCellRef.mVariant); + if (value == 0) + MWBase::Environment::get().getWorld()->removeRefScript(this); + } } - bool CellRef::hasChanged() const + void CellRef::writeState(ESM::ObjectState& state) const { - return mChanged; + std::visit(ESM::VisitOverload{ + [&](const ESM4::Reference& /*ref*/) {}, + [&](const ESM4::ActorCharacter&) {}, + [&](const ESM::CellRef& ref) { state.mRef = ref; }, + }, + mCellRef.mVariant); } - } diff --git a/apps/openmw/mwworld/cellref.hpp b/apps/openmw/mwworld/cellref.hpp index f9f6dbdda25..4dcac4def53 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -1,10 +1,15 @@ #ifndef OPENMW_MWWORLD_CELLREF_H #define OPENMW_MWWORLD_CELLREF_H -#include +#include + +#include +#include +#include namespace ESM { + struct Enchantment; struct ObjectState; } @@ -14,114 +19,242 @@ namespace MWWorld /// \brief Encapsulated variant of ESM::CellRef with change tracking class CellRef { + protected: public: + explicit CellRef(const ESM::CellRef& ref); - CellRef (const ESM::CellRef& ref) - : mCellRef(ref) - { - mChanged = false; - } + explicit CellRef(const ESM4::Reference& ref); + explicit CellRef(const ESM4::ActorCharacter& ref); // Note: Currently unused for items in containers - const ESM::RefNum& getRefNum() const; + ESM::RefNum getRefNum() const noexcept; + + // Returns RefNum. + // If RefNum is not set, assigns a generated one and changes the "lastAssignedRefNum" counter. + ESM::RefNum getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum); + + void setRefNum(ESM::RefNum refNum); // Set RefNum to its default state. - void unsetRefNum(); + void unsetRefNum() { setRefNum({}); } /// Does the RefNum have a content file? - bool hasContentFile() const; + bool hasContentFile() const { return getRefNum().hasContentFile(); } // Id of object being referenced - std::string getRefId() const; - - // Pointer to ID of the object being referenced - const std::string* getRefIdPtr() const; + ESM::RefId getRefId() const + { + struct Visitor + { + ESM::RefId operator()(const ESM::CellRef& ref) { return ref.mRefID; } + ESM::RefId operator()(const ESM4::Reference& ref) { return ref.mBaseObj; } + ESM::RefId operator()(const ESM4::ActorCharacter& ref) { return ref.mBaseObj; } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } // For doors - true if this door teleports to somewhere else, false // if it should open through animation. - bool getTeleport() const; + bool getTeleport() const + { + struct Visitor + { + bool operator()(const ESM::CellRef& ref) { return ref.mTeleport; } + bool operator()(const ESM4::Reference& ref) { return !ref.mDoor.destDoor.isZeroOrUnset(); } + bool operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } // Teleport location for the door, if this is a teleporting door. ESM::Position getDoorDest() const; // Destination cell for doors (optional) - std::string getDestCell() const; + ESM::RefId getDestCell() const; // Scale applied to mesh - float getScale() const; + float getScale() const + { + return std::visit([&](auto&& ref) { return ref.mScale; }, mCellRef.mVariant); + } void setScale(float scale); // The *original* position and rotation as it was given in the Construction Set. // Current position and rotation of the object is stored in RefData. - ESM::Position getPosition() const; - void setPosition (const ESM::Position& position); + const ESM::Position& getPosition() const + { + return std::visit([](auto&& ref) -> const ESM::Position& { return ref.mPos; }, mCellRef.mVariant); + } + void setPosition(const ESM::Position& position); // Remaining enchantment charge. This could be -1 if the charge was not touched yet (i.e. full). float getEnchantmentCharge() const; // Remaining enchantment charge rescaled to the supplied maximum charge (such as one of the enchantment). - float getNormalizedEnchantmentCharge(int maxCharge) const; + float getNormalizedEnchantmentCharge(const ESM::Enchantment& enchantment) const; void setEnchantmentCharge(float charge); // For weapon or armor, this is the remaining item health. // For tools (lockpicks, probes, repair hammer) it is the remaining uses. // If this returns int(-1) it means full health. - int getCharge() const; - float getChargeFloat() const; // Implemented as union with int charge + int getCharge() const + { + struct Visitor + { + int operator()(const ESM::CellRef& ref) { return ref.mChargeInt; } + int operator()(const ESM4::Reference& /*ref*/) { return 0; } + int operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } + float getChargeFloat() const + { + struct Visitor + { + float operator()(const ESM::CellRef& ref) { return ref.mChargeFloat; } + float operator()(const ESM4::Reference& /*ref*/) { return 0; } + float operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } // Implemented as union with int charge + float getChargeIntRemainder() const + { + struct Visitor + { + float operator()(const ESM::CellRef& ref) { return ref.mChargeIntRemainder; } + float operator()(const ESM4::Reference& /*ref*/) { return 0; } + float operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } void setCharge(int charge); void setChargeFloat(float charge); - void applyChargeRemainderToBeSubtracted(float chargeRemainder); // Stores remainders and applies if > 1 + void applyChargeRemainderToBeSubtracted(float chargeRemainder); // Stores remainders and applies if <= -1 + + // Stores fractional part of mChargeInt + void setChargeIntRemainder(float chargeRemainder); // The NPC that owns this object (and will get angry if you steal it) - std::string getOwner() const; - void setOwner(const std::string& owner); + ESM::RefId getOwner() const + { + return std::visit([](auto&& ref) -> ESM::RefId { return ref.mOwner; }, mCellRef.mVariant); + } + void setOwner(const ESM::RefId& owner); // Name of a global variable. If the global variable is set to '1', using the object is temporarily allowed // even if it has an Owner field. // Used by bed rent scripts to allow the player to use the bed for the duration of the rent. - std::string getGlobalVariable() const; + const std::string& getGlobalVariable() const; void resetGlobalVariable(); // ID of creature trapped in this soul gem - std::string getSoul() const; - void setSoul(const std::string& soul); + ESM::RefId getSoul() const + { + struct Visitor + { + ESM::RefId operator()(const ESM::CellRef& ref) { return ref.mSoul; } + ESM::RefId operator()(const ESM4::Reference& /*ref*/) { return ESM::RefId(); } + ESM::RefId operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } + void setSoul(const ESM::RefId& soul); // The faction that owns this object (and will get angry if // you take it and are not a faction member) - std::string getFaction() const; - void setFaction (const std::string& faction); + ESM::RefId getFaction() const + { + struct Visitor + { + ESM::RefId operator()(const ESM::CellRef& ref) { return ref.mFaction; } + ESM::RefId operator()(const ESM4::Reference& /*ref*/) { return ESM::RefId(); } + ESM::RefId operator()(const ESM4::ActorCharacter& /*ref*/) { return ESM::RefId(); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } + void setFaction(const ESM::RefId& faction); // PC faction rank required to use the item. Sometimes is -1, which means "any rank". void setFactionRank(int factionRank); - int getFactionRank() const; + int getFactionRank() const + { + struct Visitor + { + int operator()(const ESM::CellRef& ref) { return ref.mFactionRank; } + int operator()(const ESM4::Reference& ref) { return ref.mFactionRank; } + int operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } // Lock level for doors and containers // Positive for a locked door. 0 for a door that was never locked. // For an unlocked door, it is set to -(previous locklevel) - int getLockLevel() const; + int getLockLevel() const + { + struct Visitor + { + int operator()(const ESM::CellRef& ref) { return ref.mLockLevel; } + int operator()(const ESM4::Reference& ref) { return ref.mLockLevel; } + int operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } void setLockLevel(int lockLevel); void lock(int lockLevel); void unlock(); - // Key and trap ID names, if any - std::string getKey() const; - std::string getTrap() const; - void setTrap(const std::string& trap); + bool isLocked() const; + void setLocked(bool locked); + // Key and trap ID names, if any + ESM::RefId getKey() const + { + struct Visitor + { + ESM::RefId operator()(const ESM::CellRef& ref) { return ref.mKey; } + ESM::RefId operator()(const ESM4::Reference& ref) { return ref.mKey; } + ESM::RefId operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } + void setKey(const ESM::RefId& key); + ESM::RefId getTrap() const + { + struct Visitor + { + ESM::RefId operator()(const ESM::CellRef& ref) { return ref.mTrap; } + ESM::RefId operator()(const ESM4::Reference& /*ref*/) { return ESM::RefId(); } + ESM::RefId operator()(const ESM4::ActorCharacter&) { throw std::logic_error("Not applicable"); } + }; + return std::visit(Visitor(), mCellRef.mVariant); + } + void setTrap(const ESM::RefId& trap); - // This is 5 for Gold_005 references, 100 for Gold_100 and so on. - int getGoldValue() const; - void setGoldValue(int value); + int getCount(bool absolute = true) 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; } + }; + int count = std::visit(Visitor(), mCellRef.mVariant); + if (absolute) + return std::abs(count); + return count; + } + void setCount(int value); // Write the content of this CellRef into the given ObjectState - void writeState (ESM::ObjectState& state) const; + void writeState(ESM::ObjectState& state) const; // Has this CellRef changed since it was originally loaded? - bool hasChanged() const; + bool hasChanged() const { return mChanged; } private: - bool mChanged; - ESM::CellRef mCellRef; + bool mChanged = false; + ESM::ReferenceVariant mCellRef; }; } diff --git a/apps/openmw/mwworld/cellreflist.hpp b/apps/openmw/mwworld/cellreflist.hpp index 30be4a66103..187fff4287e 100644 --- a/apps/openmw/mwworld/cellreflist.hpp +++ b/apps/openmw/mwworld/cellreflist.hpp @@ -7,9 +7,13 @@ namespace MWWorld { + struct CellRefListBase + { + }; + /// \brief Collection of references of one type template - struct CellRefList + struct CellRefList : public CellRefListBase { typedef LiveCellRef LiveRef; typedef std::list List; @@ -22,16 +26,19 @@ namespace MWWorld /// and the build will fail with an ugly three-way cyclic header dependence /// so we need to pass the instantiation of the method to the linker, when /// all methods are known. - void load (ESM::CellRef &ref, bool deleted, const MWWorld::ESMStore &esmStore); + void load(ESM::CellRef& ref, bool deleted, const MWWorld::ESMStore& esmStore); + + void load(const ESM4::Reference& ref, const MWWorld::ESMStore& esmStore); + void load(const ESM4::ActorCharacter& ref, const MWWorld::ESMStore& esmStore); - LiveRef &insert (const LiveRef &item) + LiveRef& insert(const LiveRef& item) { mList.push_back(item); return mList.back(); } /// Remove all references with the given refNum from this list. - void remove (const ESM::RefNum &refNum) + void remove(ESM::RefNum refNum) { for (typename List::iterator it = mList.begin(); it != mList.end();) { diff --git a/apps/openmw/mwworld/cells.cpp b/apps/openmw/mwworld/cells.cpp deleted file mode 100644 index ad4dfcfb909..00000000000 --- a/apps/openmw/mwworld/cells.cpp +++ /dev/null @@ -1,484 +0,0 @@ -#include "cells.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "esmstore.hpp" -#include "containerstore.hpp" -#include "cellstore.hpp" - -namespace -{ - template - bool forEachInStore(const std::string& id, Visitor&& visitor, std::map& cellStore) - { - for(auto& cell : cellStore) - { - if(cell.second.getState() == MWWorld::CellStore::State_Unloaded) - cell.second.preload(); - if(cell.second.getState() == MWWorld::CellStore::State_Preloaded) - { - if(cell.second.hasId(id)) - { - cell.second.load(); - } - else - continue; - } - bool cont = cell.second.forEach([&] (MWWorld::Ptr ptr) - { - if(*ptr.getCellRef().getRefIdPtr() == id) - { - return visitor(ptr); - } - return true; - }); - if(!cont) - return false; - } - return true; - } - - struct PtrCollector - { - std::vector mPtrs; - - bool operator()(MWWorld::Ptr ptr) - { - mPtrs.emplace_back(ptr); - return true; - } - }; -} - -MWWorld::CellStore *MWWorld::Cells::getCellStore (const ESM::Cell *cell) -{ - if (cell->mData.mFlags & ESM::Cell::Interior) - { - std::string lowerName(Misc::StringUtils::lowerCase(cell->mName)); - std::map::iterator result = mInteriors.find (lowerName); - - if (result==mInteriors.end()) - { - result = mInteriors.insert (std::make_pair (lowerName, CellStore (cell, mStore, mReader))).first; - } - - return &result->second; - } - else - { - std::map, CellStore>::iterator result = - mExteriors.find (std::make_pair (cell->getGridX(), cell->getGridY())); - - if (result==mExteriors.end()) - { - result = mExteriors.insert (std::make_pair ( - std::make_pair (cell->getGridX(), cell->getGridY()), CellStore (cell, mStore, mReader))).first; - - } - - return &result->second; - } -} - -void MWWorld::Cells::clear() -{ - mInteriors.clear(); - mExteriors.clear(); - std::fill(mIdCache.begin(), mIdCache.end(), std::make_pair("", (MWWorld::CellStore*)nullptr)); - mIdCacheIndex = 0; -} - -MWWorld::Ptr MWWorld::Cells::getPtrAndCache (const std::string& name, CellStore& cellStore) -{ - Ptr ptr = getPtr (name, cellStore); - - if (!ptr.isEmpty() && ptr.isInCell()) - { - mIdCache[mIdCacheIndex].first = name; - mIdCache[mIdCacheIndex].second = &cellStore; - if (++mIdCacheIndex>=mIdCache.size()) - mIdCacheIndex = 0; - } - - return ptr; -} - -void MWWorld::Cells::writeCell (ESM::ESMWriter& writer, CellStore& cell) const -{ - if (cell.getState()!=CellStore::State_Loaded) - cell.load (); - - ESM::CellState cellState; - - cell.saveState (cellState); - - writer.startRecord (ESM::REC_CSTA); - cellState.mId.save (writer); - cellState.save (writer); - cell.writeFog(writer); - cell.writeReferences (writer); - writer.endRecord (ESM::REC_CSTA); -} - -MWWorld::Cells::Cells (const MWWorld::ESMStore& store, std::vector& reader) -: mStore (store), mReader (reader), - mIdCacheIndex (0) -{ - int cacheSize = std::clamp(Settings::Manager::getInt("pointers cache size", "Cells"), 40, 1000); - mIdCache = IdCache(cacheSize, std::pair ("", (CellStore*)nullptr)); -} - -MWWorld::CellStore *MWWorld::Cells::getExterior (int x, int y) -{ - std::map, CellStore>::iterator result = - mExteriors.find (std::make_pair (x, y)); - - if (result==mExteriors.end()) - { - const ESM::Cell *cell = mStore.get().search(x, y); - - if (!cell) - { - // Cell isn't predefined. Make one on the fly. - ESM::Cell record; - record.mCellId.mWorldspace = ESM::CellId::sDefaultWorldspace; - record.mCellId.mPaged = true; - record.mCellId.mIndex.mX = x; - record.mCellId.mIndex.mY = y; - - record.mData.mFlags = ESM::Cell::HasWater; - record.mData.mX = x; - record.mData.mY = y; - record.mWater = 0; - record.mMapColor = 0; - - cell = MWBase::Environment::get().getWorld()->createRecord (record); - } - - result = mExteriors.insert (std::make_pair ( - std::make_pair (x, y), CellStore (cell, mStore, mReader))).first; - } - - if (result->second.getState()!=CellStore::State_Loaded) - { - result->second.load (); - } - - return &result->second; -} - -MWWorld::CellStore *MWWorld::Cells::getInterior (const std::string& name) -{ - std::string lowerName = Misc::StringUtils::lowerCase(name); - std::map::iterator result = mInteriors.find (lowerName); - - if (result==mInteriors.end()) - { - const ESM::Cell *cell = mStore.get().find(lowerName); - - result = mInteriors.insert (std::make_pair (lowerName, CellStore (cell, mStore, mReader))).first; - } - - if (result->second.getState()!=CellStore::State_Loaded) - { - result->second.load (); - } - - return &result->second; -} - -void MWWorld::Cells::rest (double hours) -{ - for (auto &interior : mInteriors) - { - interior.second.rest(hours); - } - - for (auto &exterior : mExteriors) - { - exterior.second.rest(hours); - } -} - -void MWWorld::Cells::recharge (float duration) -{ - for (auto &interior : mInteriors) - { - interior.second.recharge(duration); - } - - for (auto &exterior : mExteriors) - { - exterior.second.recharge(duration); - } -} - -MWWorld::CellStore *MWWorld::Cells::getCell (const ESM::CellId& id) -{ - if (id.mPaged) - return getExterior (id.mIndex.mX, id.mIndex.mY); - - return getInterior (id.mWorldspace); -} - -MWWorld::Ptr MWWorld::Cells::getPtr (const std::string& name, CellStore& cell, - bool searchInContainers) -{ - if (cell.getState()==CellStore::State_Unloaded) - cell.preload (); - - if (cell.getState()==CellStore::State_Preloaded) - { - if (cell.hasId (name)) - { - cell.load (); - } - else - return Ptr(); - } - - Ptr ptr = cell.search (name); - - if (!ptr.isEmpty() && MWWorld::CellStore::isAccessible(ptr.getRefData(), ptr.getCellRef())) - return ptr; - - if (searchInContainers) - return cell.searchInContainer (name); - - return Ptr(); -} - -MWWorld::Ptr MWWorld::Cells::getPtr (const std::string& name) -{ - // First check the cache - for (IdCache::iterator iter (mIdCache.begin()); iter!=mIdCache.end(); ++iter) - if (iter->first==name && iter->second) - { - Ptr ptr = getPtr (name, *iter->second); - if (!ptr.isEmpty()) - return ptr; - } - - // Then check cells that are already listed - // Search in reverse, this is a workaround for an ambiguous chargen_plank reference in the vanilla game. - // there is one at -22,16 and one at -2,-9, the latter should be used. - for (std::map, CellStore>::reverse_iterator iter = mExteriors.rbegin(); - iter!=mExteriors.rend(); ++iter) - { - Ptr ptr = getPtrAndCache (name, iter->second); - if (!ptr.isEmpty()) - return ptr; - } - - for (std::map::iterator iter = mInteriors.begin(); - iter!=mInteriors.end(); ++iter) - { - Ptr ptr = getPtrAndCache (name, iter->second); - if (!ptr.isEmpty()) - return ptr; - } - - // Now try the other cells - const MWWorld::Store &cells = mStore.get(); - MWWorld::Store::iterator iter; - - for (iter = cells.extBegin(); iter != cells.extEnd(); ++iter) - { - CellStore *cellStore = getCellStore (&(*iter)); - - Ptr ptr = getPtrAndCache (name, *cellStore); - - if (!ptr.isEmpty()) - return ptr; - } - - for (iter = cells.intBegin(); iter != cells.intEnd(); ++iter) - { - CellStore *cellStore = getCellStore (&(*iter)); - - Ptr ptr = getPtrAndCache (name, *cellStore); - - if (!ptr.isEmpty()) - return ptr; - } - - // giving up - return Ptr(); -} - -MWWorld::Ptr MWWorld::Cells::getPtr (const std::string& id, const ESM::RefNum& refNum) -{ - for (auto& pair : mInteriors) - { - Ptr ptr = getPtr(pair.second, id, refNum); - if (!ptr.isEmpty()) - return ptr; - } - for (auto& pair : mExteriors) - { - Ptr ptr = getPtr(pair.second, id, refNum); - if (!ptr.isEmpty()) - return ptr; - } - return Ptr(); -} - -MWWorld::Ptr MWWorld::Cells::getPtr(CellStore& cellStore, const std::string& id, const ESM::RefNum& refNum) -{ - if (cellStore.getState() == CellStore::State_Unloaded) - cellStore.preload(); - if (cellStore.getState() == CellStore::State_Preloaded) - { - if (cellStore.hasId(id)) - cellStore.load(); - else - return Ptr(); - } - return cellStore.searchViaRefNum(refNum); -} - -void MWWorld::Cells::getExteriorPtrs(const std::string &name, std::vector &out) -{ - const MWWorld::Store &cells = mStore.get(); - for (MWWorld::Store::iterator iter = cells.extBegin(); iter != cells.extEnd(); ++iter) - { - CellStore *cellStore = getCellStore (&(*iter)); - - Ptr ptr = getPtrAndCache (name, *cellStore); - - if (!ptr.isEmpty()) - out.push_back(ptr); - } -} - -void MWWorld::Cells::getInteriorPtrs(const std::string &name, std::vector &out) -{ - const MWWorld::Store &cells = mStore.get(); - for (MWWorld::Store::iterator iter = cells.intBegin(); iter != cells.intEnd(); ++iter) - { - CellStore *cellStore = getCellStore (&(*iter)); - - Ptr ptr = getPtrAndCache (name, *cellStore); - - if (!ptr.isEmpty()) - out.push_back(ptr); - } -} - -std::vector MWWorld::Cells::getAll(const std::string& id) -{ - PtrCollector visitor; - if(forEachInStore(id, visitor, mInteriors)) - forEachInStore(id, visitor, mExteriors); - return visitor.mPtrs; -} - -int MWWorld::Cells::countSavedGameRecords() const -{ - int count = 0; - - for (std::map::const_iterator iter (mInteriors.begin()); - iter!=mInteriors.end(); ++iter) - if (iter->second.hasState()) - ++count; - - for (std::map, CellStore>::const_iterator iter (mExteriors.begin()); - iter!=mExteriors.end(); ++iter) - if (iter->second.hasState()) - ++count; - - return count; -} - -void MWWorld::Cells::write (ESM::ESMWriter& writer, Loading::Listener& progress) const -{ - for (std::map, CellStore>::iterator iter (mExteriors.begin()); - iter!=mExteriors.end(); ++iter) - if (iter->second.hasState()) - { - writeCell (writer, iter->second); - progress.increaseProgress(); - } - - for (std::map::iterator iter (mInteriors.begin()); - iter!=mInteriors.end(); ++iter) - if (iter->second.hasState()) - { - writeCell (writer, iter->second); - progress.increaseProgress(); - } -} - -struct GetCellStoreCallback : public MWWorld::CellStore::GetCellStoreCallback -{ -public: - GetCellStoreCallback(MWWorld::Cells& cells) - : mCells(cells) - { - } - - MWWorld::Cells& mCells; - - MWWorld::CellStore* getCellStore(const ESM::CellId& cellId) override - { - try - { - return mCells.getCell(cellId); - } - catch (...) - { - return nullptr; - } - } -}; - -bool MWWorld::Cells::readRecord (ESM::ESMReader& reader, uint32_t type, - const std::map& contentFileMap) -{ - if (type==ESM::REC_CSTA) - { - ESM::CellState state; - state.mId.load (reader); - - CellStore *cellStore = nullptr; - - try - { - cellStore = getCell (state.mId); - } - catch (...) - { - // silently drop cells that don't exist anymore - Log(Debug::Warning) << "Warning: Dropping state for cell " << state.mId.mWorldspace << " (cell no longer exists)"; - reader.skipRecord(); - return true; - } - - state.load (reader); - cellStore->loadState (state); - - if (state.mHasFogOfWar) - cellStore->readFog(reader); - - if (cellStore->getState()!=CellStore::State_Loaded) - cellStore->load (); - - GetCellStoreCallback callback(*this); - - cellStore->readReferences (reader, contentFileMap, &callback); - - return true; - } - - return false; -} diff --git a/apps/openmw/mwworld/cells.hpp b/apps/openmw/mwworld/cells.hpp deleted file mode 100644 index 654d9a14b54..00000000000 --- a/apps/openmw/mwworld/cells.hpp +++ /dev/null @@ -1,95 +0,0 @@ -#ifndef GAME_MWWORLD_CELLS_H -#define GAME_MWWORLD_CELLS_H - -#include -#include -#include - -#include "ptr.hpp" - -namespace ESM -{ - class ESMReader; - class ESMWriter; - struct CellId; - struct Cell; - struct RefNum; -} - -namespace Loading -{ - class Listener; -} - -namespace MWWorld -{ - class ESMStore; - - /// \brief Cell container - class Cells - { - typedef std::vector > IdCache; - const MWWorld::ESMStore& mStore; - std::vector& mReader; - mutable std::map mInteriors; - mutable std::map, CellStore> mExteriors; - IdCache mIdCache; - std::size_t mIdCacheIndex; - - Cells (const Cells&); - Cells& operator= (const Cells&); - - CellStore *getCellStore (const ESM::Cell *cell); - - Ptr getPtrAndCache (const std::string& name, CellStore& cellStore); - - Ptr getPtr(CellStore& cellStore, const std::string& id, const ESM::RefNum& refNum); - - void writeCell (ESM::ESMWriter& writer, CellStore& cell) const; - - public: - - void clear(); - - Cells (const MWWorld::ESMStore& store, std::vector& reader); - - CellStore *getExterior (int x, int y); - - CellStore *getInterior (const std::string& name); - - CellStore *getCell (const ESM::CellId& id); - - Ptr getPtr (const std::string& name, CellStore& cellStore, bool searchInContainers = false); - ///< \param searchInContainers Only affect loaded cells. - /// @note name must be lower case - - /// @note name must be lower case - Ptr getPtr (const std::string& name); - - Ptr getPtr(const std::string& id, const ESM::RefNum& refNum); - - void rest (double hours); - void recharge (float duration); - - /// Get all Ptrs referencing \a name in exterior cells - /// @note Due to the current implementation of getPtr this only supports one Ptr per cell. - /// @note name must be lower case - void getExteriorPtrs (const std::string& name, std::vector& out); - - /// Get all Ptrs referencing \a name in interior cells - /// @note Due to the current implementation of getPtr this only supports one Ptr per cell. - /// @note name must be lower case - void getInteriorPtrs (const std::string& name, std::vector& out); - - std::vector getAll(const std::string& id); - - int countSavedGameRecords() const; - - void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - - bool readRecord (ESM::ESMReader& reader, uint32_t type, - const std::map& contentFileMap); - }; -} - -#endif diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 2e2735e11a3..6f3d23593bb 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -1,21 +1,76 @@ #include "cellstore.hpp" +#include "magiceffects.hpp" #include +#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -23,27 +78,61 @@ #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/recharge.hpp" +#include "../mwmechanics/spellutil.hpp" -#include "ptr.hpp" -#include "esmstore.hpp" #include "class.hpp" #include "containerstore.hpp" +#include "esmstore.hpp" +#include "inventorystore.hpp" +#include "ptr.hpp" +#include "worldmodel.hpp" namespace { - template - MWWorld::Ptr searchInContainerList (MWWorld::CellRefList& containerList, const std::string& id) + + template + struct RecordToState + { + using StateType = ESM::ObjectState; + }; + + template <> + struct RecordToState + { + using StateType = ESM::NpcState; + }; + template <> + struct RecordToState + { + using StateType = ESM::CreatureState; + }; + template <> + struct RecordToState + { + using StateType = ESM::DoorState; + }; + template <> + struct RecordToState + { + using StateType = ESM::ContainerState; + }; + template <> + struct RecordToState + { + using StateType = ESM::CreatureLevListState; + }; + + template + MWWorld::Ptr searchInContainerList(MWWorld::CellRefList& containerList, const ESM::RefId& id) { - for (typename MWWorld::CellRefList::List::iterator iter (containerList.mList.begin()); - iter!=containerList.mList.end(); ++iter) + for (auto iter(containerList.mList.begin()); iter != containerList.mList.end(); ++iter) { - MWWorld::Ptr container (&*iter, nullptr); + MWWorld::Ptr container(&*iter, nullptr); if (container.getRefData().getCustomData() == nullptr) continue; - MWWorld::Ptr ptr = - container.getClass().getContainerStore (container).search (id); + MWWorld::Ptr ptr = container.getClass().getContainerStore(container).search(id); if (!ptr.isEmpty()) return ptr; @@ -52,177 +141,286 @@ namespace return MWWorld::Ptr(); } - template - MWWorld::Ptr searchViaActorId (MWWorld::CellRefList& actorList, int actorId, - MWWorld::CellStore *cell, const std::map& toIgnore) + template + MWWorld::Ptr searchViaActorId(MWWorld::CellRefList& actorList, int actorId, MWWorld::CellStore* cell, + const std::map& toIgnore) { - for (typename MWWorld::CellRefList::List::iterator iter (actorList.mList.begin()); - iter!=actorList.mList.end(); ++iter) + for (typename MWWorld::CellRefList::List::iterator iter(actorList.mList.begin()); + iter != actorList.mList.end(); ++iter) { - MWWorld::Ptr actor (&*iter, cell); + MWWorld::Ptr actor(&*iter, cell); if (toIgnore.find(&*iter) != toIgnore.end()) continue; - if (actor.getClass().getCreatureStats (actor).matchesActorId (actorId) && actor.getRefData().getCount() > 0) + if (actor.getClass().getCreatureStats(actor).matchesActorId(actorId) && actor.getCellRef().getCount() > 0) return actor; } return MWWorld::Ptr(); } - template - void writeReferenceCollection (ESM::ESMWriter& writer, - const MWWorld::CellRefList& collection) + template + void writeReferenceCollection(ESM::ESMWriter& writer, const MWWorld::CellRefList& collection) { - if (!collection.mList.empty()) + // references + for (const MWWorld::LiveCellRef& liveCellRef : collection.mList) { - // references - for (typename MWWorld::CellRefList::List::const_iterator - iter (collection.mList.begin()); - iter!=collection.mList.end(); ++iter) + if (ESM::isESM4Rec(T::sRecordId)) { - if (!iter->mData.hasChanged() && !iter->mRef.hasChanged() && iter->mRef.hasContentFile()) - { - // Reference that came from a content file and has not been changed -> ignore - continue; - } - if (iter->mData.getCount()==0 && !iter->mRef.hasContentFile()) - { - // Deleted reference that did not come from a content file -> ignore - continue; - } - - RecordType state; - iter->save (state); + // TODO: Implement loading/saving of REFR4 and ACHR4 with ESM3 reader/writer. + continue; + } + if (!liveCellRef.mData.hasChanged() && !liveCellRef.mRef.hasChanged() && liveCellRef.mRef.hasContentFile()) + { + // Reference that came from a content file and has not been changed -> ignore + continue; + } + if (liveCellRef.mRef.getCount() == 0 && !liveCellRef.mRef.hasContentFile()) + { + // Deleted reference that did not come from a content file -> ignore + continue; + } + using StateType = typename RecordToState::StateType; + StateType state; + liveCellRef.save(state); - // recordId currently unused - writer.writeHNT ("OBJE", collection.mList.front().mBase->sRecordId); + // recordId currently unused + writer.writeHNT("OBJE", collection.mList.front().mBase->sRecordId); - state.save (writer); - } + state.save(writer); } } - template + template void fixRestockingImpl(const T* base, RecordType& state) { // Workaround for old saves not containing negative quantities - for(const auto& baseItem : base->mInventory.mList) + for (const auto& baseItem : base->mInventory.mList) { - if(baseItem.mCount < 0) + if (baseItem.mCount < 0) { - for(auto& item : state.mInventory.mItems) + for (auto& item : state.mInventory.mItems) { - if(item.mCount > 0 && Misc::StringUtils::ciEqual(baseItem.mItem, item.mRef.mRefID)) - item.mCount = -item.mCount; + if (item.mRef.mCount > 0 && baseItem.mItem == item.mRef.mRefID) + item.mRef.mCount = -item.mRef.mCount; } } } } - template + template void fixRestocking(const T* base, RecordType& state) - {} + { + } - template<> + template <> void fixRestocking<>(const ESM::Creature* base, ESM::CreatureState& state) { fixRestockingImpl(base, state); } - template<> + template <> void fixRestocking<>(const ESM::NPC* base, ESM::NpcState& state) { fixRestockingImpl(base, state); } - template<> + template <> void fixRestocking<>(const ESM::Container* base, ESM::ContainerState& state) { fixRestockingImpl(base, state); } - template - void readReferenceCollection (ESM::ESMReader& reader, - MWWorld::CellRefList& collection, const ESM::CellRef& cref, const std::map& contentFileMap, MWWorld::CellStore* cellstore) + template + void readReferenceCollection(ESM::ESMReader& reader, MWWorld::CellRefList& collection, const ESM::CellRef& cref, + const MWWorld::ESMStore& esmStore, MWWorld::CellStore* cellstore) { - const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); - - RecordType state; + using StateType = typename RecordToState::StateType; + StateType state; state.mRef = cref; state.load(reader); // If the reference came from a content file, make sure this content file is loaded - if (state.mRef.mRefNum.hasContentFile()) - { - std::map::const_iterator iter = - contentFileMap.find (state.mRef.mRefNum.mContentFile); - - if (iter==contentFileMap.end()) - return; // content file has been removed -> skip + if (!reader.applyContentFileMapping(state.mRef.mRefNum)) + return; // content file has been removed -> skip - state.mRef.mRefNum.mContentFile = iter->second; - } - - if (!MWWorld::LiveCellRef::checkState (state)) + if (!MWWorld::LiveCellRef::checkState(state)) return; // not valid anymore with current content files -> skip - const T *record = esmStore.get().search (state.mRef.mRefID); + const T* record = esmStore.get().search(state.mRef.mRefID); if (!record) return; - if (state.mVersion < 15) + if (state.mVersion <= ESM::MaxOldRestockingFormatVersion) fixRestocking(record, state); + if (state.mVersion <= ESM::MaxClearModifiersFormatVersion) + { + if constexpr (std::is_same_v) + MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory); + else if constexpr (std::is_same_v) + MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory, &state.mNpcStats); + } + else if (state.mVersion <= ESM::MaxOldCreatureStatsFormatVersion) + { + if constexpr (std::is_same_v || std::is_same_v) + { + MWWorld::convertStats(state.mCreatureStats); + MWWorld::convertEnchantmentSlots(state.mCreatureStats, state.mInventory); + } + } + else if (state.mVersion <= ESM::MaxActiveSpellSlotIndexFormatVersion) + { + if constexpr (std::is_same_v || std::is_same_v) + MWWorld::convertEnchantmentSlots(state.mCreatureStats, state.mInventory); + } if (state.mRef.mRefNum.hasContentFile()) { - for (typename MWWorld::CellRefList::List::iterator iter (collection.mList.begin()); - iter!=collection.mList.end(); ++iter) - if (iter->mRef.getRefNum()==state.mRef.mRefNum && *iter->mRef.getRefIdPtr() == state.mRef.mRefID) + for (typename MWWorld::CellRefList::List::iterator iter(collection.mList.begin()); + iter != collection.mList.end(); ++iter) + if (iter->mRef.getRefNum() == state.mRef.mRefNum && iter->mRef.getRefId() == state.mRef.mRefID) { // overwrite existing reference float oldscale = iter->mRef.getScale(); - iter->load (state); - const ESM::Position & oldpos = iter->mRef.getPosition(); - const ESM::Position & newpos = iter->mData.getPosition(); + iter->load(state); + const ESM::Position& oldpos = iter->mRef.getPosition(); + const ESM::Position& newpos = iter->mData.getPosition(); const MWWorld::Ptr ptr(&*iter, cellstore); - if ((oldscale != iter->mRef.getScale() || oldpos.asVec3() != newpos.asVec3() || oldpos.rot[0] != newpos.rot[0] || oldpos.rot[1] != newpos.rot[1] || oldpos.rot[2] != newpos.rot[2]) && !ptr.getClass().isActor()) - MWBase::Environment::get().getWorld()->moveObject(ptr, newpos.pos[0], newpos.pos[1], newpos.pos[2]); + if ((oldscale != iter->mRef.getScale() || oldpos.asVec3() != newpos.asVec3() + || oldpos.rot[0] != newpos.rot[0] || oldpos.rot[1] != newpos.rot[1] + || oldpos.rot[2] != newpos.rot[2]) + && !ptr.getClass().isActor()) + MWBase::Environment::get().getWorld()->moveObject(ptr, newpos.asVec3()); if (!iter->mData.isEnabled()) { iter->mData.enable(); - MWBase::Environment::get().getWorld()->disable(MWWorld::Ptr(&*iter, cellstore)); + MWBase::Environment::get().getWorld()->disable(ptr); } + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); return; } - Log(Debug::Warning) << "Warning: Dropping reference to " << state.mRef.mRefID << " (invalid content file link)"; - return; + // Note: we preserve RefNum when picking up or dropping an item. This includes non-carriable lights 'picked + // up' through Lua. So if this RefNum is not found in this cell in content files, it doesn't mean that the + // instance is invalid. But non-storable item are always stored in saves together with their original cell. + // If a non-storable item references a content file, but is not found in this content file, + // we should drop it. Likewise if this stack is empty. + if (!MWWorld::ContainerStore::isStorableType() || !state.mRef.mCount) + { + if (state.mRef.mCount) + Log(Debug::Warning) << "Warning: Dropping reference to " << state.mRef.mRefID + << " (invalid content file link)"; + return; + } } // new reference - MWWorld::LiveCellRef ref (record); - ref.load (state); - collection.mList.push_back (ref); + MWWorld::LiveCellRef ref(record); + ref.load(state); + collection.mList.push_back(ref); + + MWWorld::LiveCellRefBase* base = &collection.mList.back(); + MWBase::Environment::get().getWorldModel()->registerPtr(MWWorld::Ptr(base, cellstore)); + } + + // this function allows us to link a CellRefList to the associated recNameInt, and apply a function + template + static void recNameSwitcher(MWWorld::CellRefList& store, ESM::RecNameInts recnNameInt, Callable&& f) + { + if (RecordType::sRecordId == recnNameInt) + { + f(store); + } + } + + // helper function for forEachInternal + template + bool forEachImp(Visitor& visitor, List& list, MWWorld::CellStore* cellStore) + { + for (typename List::List::iterator iter(list.mList.begin()); iter != list.mList.end(); ++iter) + { + if (!MWWorld::CellStore::isAccessible(iter->mData, iter->mRef)) + continue; + if (!visitor(MWWorld::Ptr(&*iter, cellStore))) + return false; + } + return true; } } namespace MWWorld { + namespace + { + template + bool isEnabled(const T& ref, const ESMStore& store) + { + if (ref.mEsp.parent.isZeroOrUnset()) + return true; + + // Disable objects that are linked to an initially disabled parent. + // Actually when we will start working on Oblivion/Skyrim scripting we will need to: + // - use the current state of the parent instead of initial state of the parent + // - every time when the parent is enabled/disabled we should also enable/disable + // all objects that are linked to it. + // But for now we assume that the parent remains in its initial state. + if (const ESM4::Reference* parentRef = store.get().searchStatic(ref.mEsp.parent)) + { + const bool parentDisabled = parentRef->mFlags & ESM4::Rec_Disabled; + const bool inversed = ref.mEsp.flags & ESM4::EnableParent::Flag_Inversed; + if (parentDisabled != inversed) + return false; + + return isEnabled(*parentRef, store); + } + + return true; + } + } + + struct CellStoreImp + { + CellStoreTuple mRefLists; + + template + static void assignStoreToIndex(CellStore& stores, CellRefList& refList) + { + const std::size_t storeIndex = CellStore::getTypeIndex(); + if (stores.mCellRefLists.size() <= storeIndex) + stores.mCellRefLists.resize(storeIndex + 1); + + assert(&refList == &std::get>(stores.mCellStoreImp->mRefLists)); + + stores.mCellRefLists[storeIndex] = &refList; + } + + // listing only objects owned by this cell. Internal use only, you probably want to use forEach() so that moved + // objects are accounted for. + template + static bool forEachInternal(Visitor& visitor, MWWorld::CellStore& cellStore) + { + bool returnValue = true; + + Misc::tupleForEach(cellStore.mCellStoreImp->mRefLists, [&visitor, &returnValue, &cellStore](auto& store) { + returnValue = returnValue && forEachImp(visitor, store, &cellStore); + }); + + return returnValue; + } + }; template - void CellRefList::load(ESM::CellRef &ref, bool deleted, const MWWorld::ESMStore &esmStore) + void CellRefList::load(ESM::CellRef& ref, bool deleted, const MWWorld::ESMStore& esmStore) { - const MWWorld::Store &store = esmStore.get(); + const MWWorld::Store& store = esmStore.get(); - if (const X *ptr = store.search (ref.mRefID)) + if (const X* ptr = store.search(ref.mRefID)) { - typename std::list::iterator iter = - std::find(mList.begin(), mList.end(), ref.mRefNum); + typename std::list::iterator iter = std::find(mList.begin(), mList.end(), ref.mRefNum); - LiveRef liveCellRef (ref, ptr); + LiveRef liveCellRef(ref, ptr); if (deleted) liveCellRef.mData.setDeletedByContentFile(true); @@ -230,22 +428,56 @@ namespace MWWorld if (iter != mList.end()) *iter = liveCellRef; else - mList.push_back (liveCellRef); + mList.push_back(liveCellRef); } else { - Log(Debug::Warning) - << "Warning: could not resolve cell reference '" << ref.mRefID << "'" - << " (dropping reference)"; + Log(Debug::Warning) << "Warning: could not resolve cell reference " << ref.mRefID + << " (dropping reference)"; } } - template bool operator==(const LiveCellRef& ref, int pRefnum) + static constexpr bool isESM4ActorRec(unsigned int rec) + { + return rec == ESM::REC_NPC_4 || rec == ESM::REC_CREA4; + } + + template + static void loadImpl(const R& ref, const MWWorld::ESMStore& esmStore, auto& list) + { + const MWWorld::Store& store = esmStore.get(); + const X* ptr = store.search(ref.mBaseObj); + if (!ptr) + { + Log(Debug::Warning) << "Warning: could not resolve cell reference " << ref.mId << " (dropping reference)"; + return; + } + LiveCellRef liveCellRef(ref, ptr); + if (!isEnabled(ref, esmStore)) + liveCellRef.mData.disable(); + list.push_back(liveCellRef); + } + + template + void CellRefList::load(const ESM4::Reference& ref, const MWWorld::ESMStore& esmStore) + { + if constexpr (ESM::isESM4Rec(X::sRecordId) && !isESM4ActorRec(X::sRecordId)) + loadImpl(ref, esmStore, mList); + } + template + void CellRefList::load(const ESM4::ActorCharacter& ref, const MWWorld::ESMStore& esmStore) + { + if constexpr (isESM4ActorRec(X::sRecordId)) + loadImpl(ref, esmStore, mList); + } + + template + bool operator==(const LiveCellRef& ref, int pRefnum) { return (ref.mRef.mRefnum == pRefnum); } - Ptr CellStore::getCurrentPtr(LiveCellRefBase *ref) + Ptr CellStore::getCurrentPtr(LiveCellRefBase* ref) { MovedRefTracker::iterator found = mMovedToAnotherCell.find(ref); if (found != mMovedToAnotherCell.end()) @@ -253,7 +485,7 @@ namespace MWWorld return Ptr(ref, this); } - void CellStore::moveFrom(const Ptr &object, CellStore *from) + void CellStore::moveFrom(const Ptr& object, CellStore* from) { if (mState != State_Loaded) load(); @@ -263,39 +495,27 @@ namespace MWWorld if (found != mMovedToAnotherCell.end()) { // A cell we had previously moved an object to is returning it to us. - assert (found->second == from); + assert(found->second == from); mMovedToAnotherCell.erase(found); } else { mMovedHere.insert(std::make_pair(object.getBase(), from)); } - updateMergedRefs(); + requestMergedRefsUpdate(); } - MWWorld::Ptr CellStore::moveTo(const Ptr &object, CellStore *cellToMoveTo) + MWWorld::Ptr CellStore::moveTo(const Ptr& object, CellStore* cellToMoveTo) { if (cellToMoveTo == this) throw std::runtime_error("moveTo: object is already in this cell"); // We assume that *this is in State_Loaded since we could hardly have reference to a live object otherwise. if (mState != State_Loaded) - throw std::runtime_error("moveTo: can't move object from a non-loaded cell (how did you get this object anyway?)"); + throw std::runtime_error( + "moveTo: can't move object from a non-loaded cell (how did you get this object anyway?)"); - // Ensure that the object actually exists in the cell - if (searchViaRefNum(object.getCellRef().getRefNum()).isEmpty()) - throw std::runtime_error("moveTo: object is not in this cell"); - - - // Objects with no refnum can't be handled correctly in the merging process that happens - // on a save/load, so do a simple copy & delete for these objects. - if (!object.getCellRef().getRefNum().hasContentFile()) - { - MWWorld::Ptr copied = object.getClass().copyToCell(object, *cellToMoveTo, object.getRefData().getCount()); - object.getRefData().setCount(0); - object.getRefData().setBaseNode(nullptr); - return copied; - } + MWBase::Environment::get().getWorldModel()->registerPtr(MWWorld::Ptr(object.getBase(), cellToMoveTo)); MovedRefTracker::iterator found = mMovedHere.find(object.getBase()); if (found != mMovedHere.end()) @@ -303,7 +523,7 @@ namespace MWWorld // Special case - object didn't originate in this cell // Move it back to its original cell first CellStore* originalCell = found->second; - assert (originalCell != this); + assert(originalCell != this); originalCell->moveFrom(object, this); mMovedHere.erase(found); @@ -314,28 +534,29 @@ namespace MWWorld originalCell->moveTo(object, cellToMoveTo); } - updateMergedRefs(); + requestMergedRefsUpdate(); return MWWorld::Ptr(object.getBase(), cellToMoveTo); } cellToMoveTo->moveFrom(object, this); mMovedToAnotherCell.insert(std::make_pair(object.getBase(), cellToMoveTo)); - updateMergedRefs(); + requestMergedRefsUpdate(); return MWWorld::Ptr(object.getBase(), cellToMoveTo); } struct MergeVisitor { - MergeVisitor(std::vector& mergeTo, const std::map& movedHere, - const std::map& movedToAnotherCell) + MergeVisitor(std::vector& mergeTo, + const std::map& movedHere, + const std::map& movedToAnotherCell) : mMergeTo(mergeTo) , mMovedHere(movedHere) , mMovedToAnotherCell(movedToAnotherCell) { } - bool operator() (const MWWorld::Ptr& ptr) + bool operator()(const MWWorld::Ptr& ptr) { if (mMovedToAnotherCell.find(ptr.getBase()) != mMovedToAnotherCell.end()) return true; @@ -345,7 +566,7 @@ namespace MWWorld void merge() { - for (const auto & [base, _] : mMovedHere) + for (const auto& [base, _] : mMovedHere) mMergeTo.push_back(base); } @@ -356,13 +577,19 @@ namespace MWWorld const std::map& mMovedToAnotherCell; }; - void CellStore::updateMergedRefs() + void CellStore::requestMergedRefsUpdate() { - mMergedRefs.clear(); mRechargingItemsUpToDate = false; + mMergedRefsNeedsUpdate = true; + } + + void CellStore::updateMergedRefs() const + { + mMergedRefs.clear(); MergeVisitor visitor(mMergedRefs, mMovedHere, mMovedToAnotherCell); - forEachInternal(visitor); + CellStoreImp::forEachInternal(visitor, const_cast(*this)); visitor.merge(); + mMergedRefsNeedsUpdate = false; } bool CellStore::movedHere(const MWWorld::Ptr& ptr) const @@ -376,15 +603,26 @@ namespace MWWorld return false; } - CellStore::CellStore (const ESM::Cell *cell, const MWWorld::ESMStore& esmStore, std::vector& readerList) - : mStore(esmStore), mReader(readerList), mCell (cell), mState (State_Unloaded), mHasState (false), mLastRespawn(0,0), mRechargingItemsUpToDate(false) + CellStore::CellStore(MWWorld::Cell&& cell, const MWWorld::ESMStore& esmStore, ESM::ReadersCache& readers) + : mStore(esmStore) + , mReaders(readers) + , mCellVariant(std::move(cell)) + , mState(State_Unloaded) + , mHasState(false) + , mLastRespawn(0, 0) + , mCellStoreImp(std::make_unique()) + , mRechargingItemsUpToDate(false) { - mWaterLevel = cell->mWater; + + std::apply([this](auto&... x) { (CellStoreImp::assignStoreToIndex(*this, x), ...); }, mCellStoreImp->mRefLists); + mWaterLevel = mCellVariant.getWaterHeight(); } - const ESM::Cell *CellStore::getCell() const + CellStore::~CellStore() = default; + + const MWWorld::Cell* CellStore::getCell() const { - return mCell; + return &mCellVariant; } CellStore::State CellStore::getState() const @@ -392,7 +630,7 @@ namespace MWWorld return mState; } - const std::vector &CellStore::getPreloadedIds() const + const std::vector& CellStore::getPreloadedIds() const { return mIds; } @@ -402,25 +640,29 @@ namespace MWWorld return mHasState; } - bool CellStore::hasId (const std::string& id) const + bool CellStore::hasId(const ESM::RefId& id) const { - if (mState==State_Unloaded) + if (mState == State_Unloaded) return false; - if (mState==State_Preloaded) - return std::binary_search (mIds.begin(), mIds.end(), id); + if (mState == State_Preloaded) + return std::binary_search(mIds.begin(), mIds.end(), id); - return searchConst (id).isEmpty(); + return searchConst(id).isEmpty(); } template struct SearchVisitor { PtrType mFound; - const std::string *mIdToFind; + const ESM::RefId& mIdToFind; + SearchVisitor(const ESM::RefId& id) + : mIdToFind(id) + { + } bool operator()(const PtrType& ptr) { - if (*ptr.getCellRef().getRefIdPtr() == *mIdToFind) + if (ptr.getCellRef().getRefId() == mIdToFind) { mFound = ptr; return false; @@ -429,36 +671,34 @@ namespace MWWorld } }; - Ptr CellStore::search (const std::string& id) + Ptr CellStore::search(const ESM::RefId& id) { - SearchVisitor searchVisitor; - searchVisitor.mIdToFind = &id; + SearchVisitor searchVisitor(id); forEach(searchVisitor); return searchVisitor.mFound; } - ConstPtr CellStore::searchConst (const std::string& id) const + ConstPtr CellStore::searchConst(const ESM::RefId& id) const { - SearchVisitor searchVisitor; - searchVisitor.mIdToFind = &id; + SearchVisitor searchVisitor(id); forEachConst(searchVisitor); return searchVisitor.mFound; } - Ptr CellStore::searchViaActorId (int id) + Ptr CellStore::searchViaActorId(int id) { - if (Ptr ptr = ::searchViaActorId (mNpcs, id, this, mMovedToAnotherCell)) + if (Ptr ptr = ::searchViaActorId(get(), id, this, mMovedToAnotherCell); !ptr.isEmpty()) return ptr; - if (Ptr ptr = ::searchViaActorId (mCreatures, id, this, mMovedToAnotherCell)) + if (Ptr ptr = ::searchViaActorId(get(), id, this, mMovedToAnotherCell); !ptr.isEmpty()) return ptr; for (const auto& [base, _] : mMovedHere) { - MWWorld::Ptr actor (base, this); + MWWorld::Ptr actor(base, this); if (!actor.getClass().isActor()) continue; - if (actor.getClass().getCreatureStats (actor).matchesActorId (id) && actor.getRefData().getCount() > 0) + if (actor.getClass().getCreatureStats(actor).matchesActorId(id) && actor.getCellRef().getCount() > 0) return actor; } @@ -467,9 +707,13 @@ namespace MWWorld class RefNumSearchVisitor { - const ESM::RefNum& mRefNum; + ESM::RefNum mRefNum; + public: - RefNumSearchVisitor(const ESM::RefNum& refNum) : mRefNum(refNum) {} + RefNumSearchVisitor(ESM::RefNum refNum) + : mRefNum(refNum) + { + } Ptr mFound; @@ -484,21 +728,14 @@ namespace MWWorld } }; - Ptr CellStore::searchViaRefNum (const ESM::RefNum& refNum) - { - RefNumSearchVisitor searchVisitor(refNum); - forEach(searchVisitor); - return searchVisitor.mFound; - } - float CellStore::getWaterLevel() const { if (isExterior()) - return -1; + return getCell()->getWaterHeight(); return mWaterLevel; } - void CellStore::setWaterLevel (float level) + void CellStore::setWaterLevel(float level) { mWaterLevel = level; mHasState = true; @@ -506,159 +743,209 @@ namespace MWWorld std::size_t CellStore::count() const { + if (mMergedRefsNeedsUpdate) + updateMergedRefs(); return mMergedRefs.size(); } - void CellStore::load () + void CellStore::load() { - if (mState!=State_Loaded) + if (mState != State_Loaded) { - if (mState==State_Preloaded) + if (mState == State_Preloaded) mIds.clear(); - loadRefs (); + loadRefs(); mState = State_Loaded; } } - void CellStore::preload () + void CellStore::preload() { - if (mState==State_Unloaded) + if (mState == State_Unloaded) { - listRefs (); + listRefs(); mState = State_Preloaded; } } - void CellStore::listRefs() + void CellStore::listRefs(const ESM::Cell& cell) { - std::vector& esm = mReader; - - assert (mCell); - - if (mCell->mContextList.empty()) + if (cell.mContextList.empty()) return; // this is a dynamically generated cell -> skipping. // Load references from all plugins that do something with this cell. - for (size_t i = 0; i < mCell->mContextList.size(); i++) + for (size_t i = 0; i < cell.mContextList.size(); i++) { try { // Reopen the ESM reader and seek to the right position. - int index = mCell->mContextList[i].index; - mCell->restore (esm[index], i); + const std::size_t index = static_cast(cell.mContextList[i].index); + const ESM::ReadersCache::BusyItem reader = mReaders.get(index); + cell.restore(*reader, i); ESM::CellRef ref; // Get each reference in turn + ESM::MovedCellRef cMRef; bool deleted = false; - while (mCell->getNextRef (esm[index], ref, deleted)) + bool moved = false; + while (ESM::Cell::getNextRef( + *reader, ref, deleted, cMRef, moved, ESM::Cell::GetNextRefMode::LoadOnlyNotMoved)) { - if (deleted) + if (deleted || moved) continue; // Don't list reference if it was moved to a different cell. - ESM::MovedCellRefTracker::const_iterator iter = - std::find(mCell->mMovedRefs.begin(), mCell->mMovedRefs.end(), ref.mRefNum); - if (iter != mCell->mMovedRefs.end()) { + ESM::MovedCellRefTracker::const_iterator iter + = std::find(cell.mMovedRefs.begin(), cell.mMovedRefs.end(), ref.mRefNum); + if (iter != cell.mMovedRefs.end()) + { continue; } - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); mIds.push_back(std::move(ref.mRefID)); } } catch (std::exception& e) { - Log(Debug::Error) << "An error occurred listing references for cell " << getCell()->getDescription() << ": " << e.what(); + Log(Debug::Error) << "An error occurred listing references for cell " << getCell()->getDescription() + << ": " << e.what(); } } // List moved references, from separately tracked list. - for (const auto& [ref, deleted]: mCell->mLeasedRefs) + for (const auto& [ref, deleted] : cell.mLeasedRefs) { if (!deleted) - mIds.push_back(Misc::StringUtils::lowerCase(ref.mRefID)); + mIds.push_back(ref.mRefID); } + } - std::sort (mIds.begin(), mIds.end()); + template + static void visitCell4References( + const ESM4::Cell& cell, const ESMStore& esmStore, ESM::ReadersCache& readers, ReferenceInvocable&& invocable) + { + for (const ESM4::Reference* ref : esmStore.get().getByCell(cell.mId)) + invocable(*ref); } - void CellStore::loadRefs() + template + static void visitCell4ActorReferences( + const ESM4::Cell& cell, const ESMStore& esmStore, ESM::ReadersCache& readers, ReferenceInvocable&& invocable) { - std::vector& esm = mReader; + for (const ESM4::ActorCharacter* ref : esmStore.get().getByCell(cell.mId)) + invocable(*ref); + for (const ESM4::ActorCharacter* ref : esmStore.get().getByCell(cell.mId)) + invocable(*ref); + } - assert (mCell); + void CellStore::listRefs(const ESM4::Cell& cell) + { + visitCell4References(cell, mStore, mReaders, [&](const ESM4::Reference& ref) { mIds.push_back(ref.mBaseObj); }); + visitCell4ActorReferences( + cell, mStore, mReaders, [&](const ESM4::ActorCharacter& ref) { mIds.push_back(ref.mBaseObj); }); + } - if (mCell->mContextList.empty()) - return; // this is a dynamically generated cell -> skipping. + void CellStore::listRefs() + { + ESM::visit([&](auto&& cell) { listRefs(cell); }, mCellVariant); + std::sort(mIds.begin(), mIds.end()); + } - std::map refNumToID; // used to detect refID modifications + void CellStore::loadRefs(const ESM::Cell& cell, std::map& refNumToID) + { + if (cell.mContextList.empty()) + return; // this is a dynamically generated cell -> skipping. // Load references from all plugins that do something with this cell. - for (size_t i = 0; i < mCell->mContextList.size(); i++) + for (size_t i = 0; i < cell.mContextList.size(); i++) { try { // Reopen the ESM reader and seek to the right position. - int index = mCell->mContextList[i].index; - mCell->restore (esm[index], i); + const std::size_t index = static_cast(cell.mContextList[i].index); + const ESM::ReadersCache::BusyItem reader = mReaders.get(index); + cell.restore(*reader, i); ESM::CellRef ref; - ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; - // Get each reference in turn + ESM::MovedCellRef cMRef; bool deleted = false; - while(mCell->getNextRef(esm[index], ref, deleted)) + bool moved = false; + while (ESM::Cell::getNextRef( + *reader, ref, deleted, cMRef, moved, ESM::Cell::GetNextRefMode::LoadOnlyNotMoved)) { + if (moved) + continue; + // Don't load reference if it was moved to a different cell. - ESM::MovedCellRefTracker::const_iterator iter = - std::find(mCell->mMovedRefs.begin(), mCell->mMovedRefs.end(), ref.mRefNum); - if (iter != mCell->mMovedRefs.end()) { + ESM::MovedCellRefTracker::const_iterator iter + = std::find(cell.mMovedRefs.begin(), cell.mMovedRefs.end(), ref.mRefNum); + if (iter != cell.mMovedRefs.end()) + { continue; } - loadRef (ref, deleted, refNumToID); + loadRef(ref, deleted, refNumToID); } } catch (std::exception& e) { - Log(Debug::Error) << "An error occurred loading references for cell " << getCell()->getDescription() << ": " << e.what(); + Log(Debug::Error) << "An error occurred loading references for cell " << getCell()->getDescription() + << ": " << e.what(); } } - // Load moved references, from separately tracked list. - for (const auto& leasedRef : mCell->mLeasedRefs) + for (const auto& leasedRef : cell.mLeasedRefs) { - ESM::CellRef &ref = const_cast(leasedRef.first); + ESM::CellRef& ref = const_cast(leasedRef.first); bool deleted = leasedRef.second; - loadRef (ref, deleted, refNumToID); + loadRef(ref, deleted, refNumToID); } + } - updateMergedRefs(); + void CellStore::loadRefs(const ESM4::Cell& cell, std::map& refNumToID) + { + visitCell4References(cell, mStore, mReaders, [&](const ESM4::Reference& ref) { loadRef(ref); }); + visitCell4ActorReferences(cell, mStore, mReaders, [&](const ESM4::ActorCharacter& ref) { loadRef(ref); }); + } + + void CellStore::loadRefs() + { + std::map refNumToID; // used to detect refID modifications + + ESM::visit([&](auto&& cell) { loadRefs(cell, refNumToID); }, mCellVariant); + + requestMergedRefsUpdate(); } bool CellStore::isExterior() const { - return mCell->isExterior(); + return mCellVariant.isExterior(); } - Ptr CellStore::searchInContainer (const std::string& id) + bool CellStore::isQuasiExterior() const + { + return mCellVariant.isQuasiExterior(); + } + + Ptr CellStore::searchInContainer(const ESM::RefId& id) { bool oldState = mHasState; mHasState = true; - if (Ptr ptr = searchInContainerList (mContainers, id)) + if (Ptr ptr = searchInContainerList(get(), id); !ptr.isEmpty()) return ptr; - if (Ptr ptr = searchInContainerList (mCreatures, id)) + if (Ptr ptr = searchInContainerList(get(), id); !ptr.isEmpty()) return ptr; - if (Ptr ptr = searchInContainerList (mNpcs, id)) + if (Ptr ptr = searchInContainerList(get(), id); !ptr.isEmpty()) return ptr; mHasState = oldState; @@ -666,323 +953,199 @@ namespace MWWorld return Ptr(); } - void CellStore::loadRef (ESM::CellRef& ref, bool deleted, std::map& refNumToID) + void CellStore::loadRef(const ESM4::Reference& ref) + { + const MWWorld::ESMStore& store = mStore; + + ESM::RecNameInts foundType = static_cast(store.find(ref.mBaseObj)); + + Misc::tupleForEach(this->mCellStoreImp->mRefLists, [&ref, &store, foundType](auto& x) { + recNameSwitcher(x, foundType, [&ref, &store](auto& storeIn) { storeIn.load(ref, store); }); + }); + } + + void CellStore::loadRef(const ESM4::ActorCharacter& ref) { - Misc::StringUtils::lowerCaseInPlace (ref.mRefID); + const MWWorld::ESMStore& store = mStore; + ESM::RecNameInts foundType = static_cast(store.find(ref.mBaseObj)); + + Misc::tupleForEach(this->mCellStoreImp->mRefLists, [&ref, &store, foundType](auto& x) { + recNameSwitcher(x, foundType, [&ref, &store](auto& storeIn) { storeIn.load(ref, store); }); + }); + } + void CellStore::loadRef(ESM::CellRef& ref, bool deleted, std::map& refNumToID) + { const MWWorld::ESMStore& store = mStore; - std::map::iterator it = refNumToID.find(ref.mRefNum); + auto it = refNumToID.find(ref.mRefNum); if (it != refNumToID.end()) { if (it->second != ref.mRefID) { // refID was modified, make sure we don't end up with duplicated refs - switch (store.find(it->second)) + ESM::RecNameInts foundType = static_cast(store.find(it->second)); + if (foundType != 0) { - case ESM::REC_ACTI: mActivators.remove(ref.mRefNum); break; - case ESM::REC_ALCH: mPotions.remove(ref.mRefNum); break; - case ESM::REC_APPA: mAppas.remove(ref.mRefNum); break; - case ESM::REC_ARMO: mArmors.remove(ref.mRefNum); break; - case ESM::REC_BOOK: mBooks.remove(ref.mRefNum); break; - case ESM::REC_CLOT: mClothes.remove(ref.mRefNum); break; - case ESM::REC_CONT: mContainers.remove(ref.mRefNum); break; - case ESM::REC_CREA: mCreatures.remove(ref.mRefNum); break; - case ESM::REC_DOOR: mDoors.remove(ref.mRefNum); break; - case ESM::REC_INGR: mIngreds.remove(ref.mRefNum); break; - case ESM::REC_LEVC: mCreatureLists.remove(ref.mRefNum); break; - case ESM::REC_LEVI: mItemLists.remove(ref.mRefNum); break; - case ESM::REC_LIGH: mLights.remove(ref.mRefNum); break; - case ESM::REC_LOCK: mLockpicks.remove(ref.mRefNum); break; - case ESM::REC_MISC: mMiscItems.remove(ref.mRefNum); break; - case ESM::REC_NPC_: mNpcs.remove(ref.mRefNum); break; - case ESM::REC_PROB: mProbes.remove(ref.mRefNum); break; - case ESM::REC_REPA: mRepairs.remove(ref.mRefNum); break; - case ESM::REC_STAT: mStatics.remove(ref.mRefNum); break; - case ESM::REC_WEAP: mWeapons.remove(ref.mRefNum); break; - case ESM::REC_BODY: mBodyParts.remove(ref.mRefNum); break; - default: - break; + Misc::tupleForEach(this->mCellStoreImp->mRefLists, [&ref, foundType](auto& x) { + recNameSwitcher(x, foundType, [&ref](auto& storeIn) { storeIn.remove(ref.mRefNum); }); + }); } } } - switch (store.find (ref.mRefID)) + ESM::RecNameInts foundType = static_cast(store.find(ref.mRefID)); + bool handledType = false; + if (foundType != 0) { - case ESM::REC_ACTI: mActivators.load(ref, deleted, store); break; - case ESM::REC_ALCH: mPotions.load(ref, deleted,store); break; - case ESM::REC_APPA: mAppas.load(ref, deleted, store); break; - case ESM::REC_ARMO: mArmors.load(ref, deleted, store); break; - case ESM::REC_BOOK: mBooks.load(ref, deleted, store); break; - case ESM::REC_CLOT: mClothes.load(ref, deleted, store); break; - case ESM::REC_CONT: mContainers.load(ref, deleted, store); break; - case ESM::REC_CREA: mCreatures.load(ref, deleted, store); break; - case ESM::REC_DOOR: mDoors.load(ref, deleted, store); break; - case ESM::REC_INGR: mIngreds.load(ref, deleted, store); break; - case ESM::REC_LEVC: mCreatureLists.load(ref, deleted, store); break; - case ESM::REC_LEVI: mItemLists.load(ref, deleted, store); break; - case ESM::REC_LIGH: mLights.load(ref, deleted, store); break; - case ESM::REC_LOCK: mLockpicks.load(ref, deleted, store); break; - case ESM::REC_MISC: mMiscItems.load(ref, deleted, store); break; - case ESM::REC_NPC_: mNpcs.load(ref, deleted, store); break; - case ESM::REC_PROB: mProbes.load(ref, deleted, store); break; - case ESM::REC_REPA: mRepairs.load(ref, deleted, store); break; - case ESM::REC_STAT: - { - if (ref.mRefNum.fromGroundcoverFile()) return; - mStatics.load(ref, deleted, store); break; - } - case ESM::REC_WEAP: mWeapons.load(ref, deleted, store); break; - case ESM::REC_BODY: mBodyParts.load(ref, deleted, store); break; - - case 0: Log(Debug::Error) << "Cell reference '" + ref.mRefID + "' not found!"; return; + Misc::tupleForEach( + this->mCellStoreImp->mRefLists, [&ref, &deleted, &store, foundType, &handledType](auto& x) { + recNameSwitcher(x, foundType, [&ref, &deleted, &store, &handledType](auto& storeIn) { + handledType = true; + storeIn.load(ref, deleted, store); + }); + }); + } + else + { + Log(Debug::Error) << "Cell reference " << ref.mRefID << " is not found!"; + return; + } - default: - Log(Debug::Error) << "Error: Ignoring reference '" << ref.mRefID << "' of unhandled type"; - return; + if (!handledType) + { + Log(Debug::Error) << "Error: Ignoring reference " << ref.mRefID << " of unhandled type"; + return; } refNumToID[ref.mRefNum] = ref.mRefID; } - void CellStore::loadState (const ESM::CellState& state) + void CellStore::loadState(const ESM::CellState& state) { mHasState = true; - if (mCell->mData.mFlags & ESM::Cell::Interior && mCell->mData.mFlags & ESM::Cell::HasWater) + if (!mCellVariant.isExterior() && mCellVariant.hasWater()) mWaterLevel = state.mWaterLevel; mLastRespawn = MWWorld::TimeStamp(state.mLastRespawn); } - void CellStore::saveState (ESM::CellState& state) const + void CellStore::saveState(ESM::CellState& state) const { - state.mId = mCell->getCellId(); + state.mId = mCellVariant.getId(); - if (mCell->mData.mFlags & ESM::Cell::Interior && mCell->mData.mFlags & ESM::Cell::HasWater) + if (!mCellVariant.isExterior() && mCellVariant.hasWater()) state.mWaterLevel = mWaterLevel; - + state.mIsInterior = !mCellVariant.isExterior(); state.mHasFogOfWar = (mFogState.get() ? 1 : 0); state.mLastRespawn = mLastRespawn.toEsm(); } - void CellStore::writeFog(ESM::ESMWriter &writer) const + void CellStore::writeFog(ESM::ESMWriter& writer) const { if (mFogState.get()) { - mFogState->save(writer, mCell->mData.mFlags & ESM::Cell::Interior); + mFogState->save(writer, !mCellVariant.isExterior()); } } - void CellStore::readFog(ESM::ESMReader &reader) + void CellStore::readFog(ESM::ESMReader& reader) { - mFogState.reset(new ESM::FogState()); + mFogState = std::make_unique(); mFogState->load(reader); } - void CellStore::writeReferences (ESM::ESMWriter& writer) const - { - writeReferenceCollection (writer, mActivators); - writeReferenceCollection (writer, mPotions); - writeReferenceCollection (writer, mAppas); - writeReferenceCollection (writer, mArmors); - writeReferenceCollection (writer, mBooks); - writeReferenceCollection (writer, mClothes); - writeReferenceCollection (writer, mContainers); - writeReferenceCollection (writer, mCreatures); - writeReferenceCollection (writer, mDoors); - writeReferenceCollection (writer, mIngreds); - writeReferenceCollection (writer, mCreatureLists); - writeReferenceCollection (writer, mItemLists); - writeReferenceCollection (writer, mLights); - writeReferenceCollection (writer, mLockpicks); - writeReferenceCollection (writer, mMiscItems); - writeReferenceCollection (writer, mNpcs); - writeReferenceCollection (writer, mProbes); - writeReferenceCollection (writer, mRepairs); - writeReferenceCollection (writer, mStatics); - writeReferenceCollection (writer, mWeapons); - writeReferenceCollection (writer, mBodyParts); + void CellStore::writeReferences(ESM::ESMWriter& writer) const + { + Misc::tupleForEach(this->mCellStoreImp->mRefLists, + [&writer](auto& cellRefList) { writeReferenceCollection(writer, cellRefList); }); for (const auto& [base, store] : mMovedToAnotherCell) { ESM::RefNum refNum = base->mRef.getRefNum(); - ESM::CellId movedTo = store->getCell()->getCellId(); + if (base->isDeleted() && !refNum.hasContentFile()) + continue; // filtered out in writeReferenceCollection + ESM::RefId movedTo = store->getCell()->getId(); - refNum.save(writer, true, "MVRF"); - movedTo.save(writer); + writer.writeFormId(refNum, true, "MVRF"); + writer.writeCellId(movedTo); } } - void CellStore::readReferences (ESM::ESMReader& reader, const std::map& contentFileMap, GetCellStoreCallback* callback) + void CellStore::readReferences(ESM::ESMReader& reader, GetCellStoreCallback* callback) { mHasState = true; - while (reader.isNextSub ("OBJE")) + while (reader.isNextSub("OBJE")) { unsigned int unused; - reader.getHT (unused); + reader.getHT(unused); // load the RefID first so we know what type of object it is ESM::CellRef cref; cref.loadId(reader, true); - int type = MWBase::Environment::get().getWorld()->getStore().find(cref.mRefID); + int type = mStore.find(cref.mRefID); if (type == 0) { - Log(Debug::Warning) << "Dropping reference to '" << cref.mRefID << "' (object no longer exists)"; - reader.skipHSubUntil("OBJE"); + Log(Debug::Warning) << "Dropping reference to " << cref.mRefID << " (object no longer exists)"; + // Skip until the next OBJE or MVRF + while (reader.hasMoreSubs() && !reader.peekNextSub("OBJE") && !reader.peekNextSub("MVRF")) + { + reader.getSubName(); + reader.skipHSub(); + } continue; } - switch (type) + if (type != 0) { - case ESM::REC_ACTI: - - readReferenceCollection (reader, mActivators, cref, contentFileMap, this); - break; - - case ESM::REC_ALCH: - - readReferenceCollection (reader, mPotions, cref, contentFileMap, this); - break; - - case ESM::REC_APPA: - - readReferenceCollection (reader, mAppas, cref, contentFileMap, this); - break; - - case ESM::REC_ARMO: - - readReferenceCollection (reader, mArmors, cref, contentFileMap, this); - break; - - case ESM::REC_BOOK: - - readReferenceCollection (reader, mBooks, cref, contentFileMap, this); - break; - - case ESM::REC_CLOT: - - readReferenceCollection (reader, mClothes, cref, contentFileMap, this); - break; - - case ESM::REC_CONT: - - readReferenceCollection (reader, mContainers, cref, contentFileMap, this); - break; - - case ESM::REC_CREA: - - readReferenceCollection (reader, mCreatures, cref, contentFileMap, this); - break; - - case ESM::REC_DOOR: - - readReferenceCollection (reader, mDoors, cref, contentFileMap, this); - break; - - case ESM::REC_INGR: - - readReferenceCollection (reader, mIngreds, cref, contentFileMap, this); - break; - - case ESM::REC_LEVC: - - readReferenceCollection (reader, mCreatureLists, cref, contentFileMap, this); - break; - - case ESM::REC_LEVI: - - readReferenceCollection (reader, mItemLists, cref, contentFileMap, this); - break; - - case ESM::REC_LIGH: - - readReferenceCollection (reader, mLights, cref, contentFileMap, this); - break; - - case ESM::REC_LOCK: - - readReferenceCollection (reader, mLockpicks, cref, contentFileMap, this); - break; - - case ESM::REC_MISC: - - readReferenceCollection (reader, mMiscItems, cref, contentFileMap, this); - break; - - case ESM::REC_NPC_: - - readReferenceCollection (reader, mNpcs, cref, contentFileMap, this); - break; - - case ESM::REC_PROB: - - readReferenceCollection (reader, mProbes, cref, contentFileMap, this); - break; - - case ESM::REC_REPA: - - readReferenceCollection (reader, mRepairs, cref, contentFileMap, this); - break; - - case ESM::REC_STAT: - - readReferenceCollection (reader, mStatics, cref, contentFileMap, this); - break; - - case ESM::REC_WEAP: - - readReferenceCollection (reader, mWeapons, cref, contentFileMap, this); - break; - - case ESM::REC_BODY: - - readReferenceCollection (reader, mBodyParts, cref, contentFileMap, this); - break; - - default: - - throw std::runtime_error ("unknown type in cell reference section"); + bool foundCorrespondingStore = false; + Misc::tupleForEach( + this->mCellStoreImp->mRefLists, [&reader, this, &cref, &foundCorrespondingStore, type](auto&& x) { + recNameSwitcher(x, static_cast(type), + [&reader, this, &cref, &foundCorrespondingStore](auto& store) { + foundCorrespondingStore = true; + readReferenceCollection(reader, store, cref, mStore, this); + }); + }); + + if (!foundCorrespondingStore) + throw std::runtime_error("unknown type in cell reference section"); } } - // Do another update here to make sure objects referred to by MVRF tags can be found - // This update is only needed for old saves that used the old copy&delete way of moving objects - updateMergedRefs(); - while (reader.isNextSub("MVRF")) { reader.cacheSubName(); - ESM::RefNum refnum; - ESM::CellId movedTo; - refnum.load(reader, true, "MVRF"); - movedTo.load(reader); - - if (refnum.hasContentFile()) + ESM::RefNum refnum = reader.getFormId(true, "MVRF"); + ESM::RefId movedToId = reader.getCellId(); + if (!reader.applyContentFileMapping(refnum)) { - auto iter = contentFileMap.find(refnum.mContentFile); - if (iter != contentFileMap.end()) - refnum.mContentFile = iter->second; + Log(Debug::Warning) << "Warning: Dropping moved ref tag for " << refnum.mIndex + << " (content file no longer exists)"; + continue; } - // Search for the reference. It might no longer exist if its content file was removed. - Ptr movedRef = searchViaRefNum(refnum); + // Search for the reference. It might no longer exist if its content file was changed. + Ptr movedRef = MWBase::Environment::get().getWorldModel()->getPtr(refnum); if (movedRef.isEmpty()) { - Log(Debug::Warning) << "Warning: Dropping moved ref tag for " << refnum.mIndex << " (moved object no longer exists)"; + Log(Debug::Warning) << "Warning: Dropping moved ref tag for " << refnum.mIndex + << " (moved object no longer exists)"; continue; } - CellStore* otherCell = callback->getCellStore(movedTo); + CellStore* otherCell = callback->getCellStore(movedToId); if (otherCell == nullptr) { Log(Debug::Warning) << "Warning: Dropping moved ref tag for " << movedRef.getCellRef().getRefId() - << " (target cell " << movedTo.mWorldspace << " no longer exists). Reference moved back to its original location."; - // Note by dropping tag the object will automatically re-appear in its original cell, though potentially at inapproriate coordinates. - // Restore original coordinates: + << " (target cell " << movedToId + << " no longer exists). Reference moved back to its original location."; + // Note by dropping tag the object will automatically re-appear in its original cell, though + // potentially at inapproriate coordinates. Restore original coordinates: movedRef.getRefData().setPosition(movedRef.getCellRef().getPosition()); continue; } @@ -996,21 +1159,13 @@ namespace MWWorld moveTo(movedRef, otherCell); } - } - bool operator== (const CellStore& left, const CellStore& right) - { - return left.getCell()->getCellId()==right.getCell()->getCellId(); + requestMergedRefsUpdate(); } - bool operator!= (const CellStore& left, const CellStore& right) + void CellStore::setFog(std::unique_ptr&& fog) { - return !(left==right); - } - - void CellStore::setFog(ESM::FogState *fog) - { - mFogState.reset(fog); + mFogState = std::move(fog); } ESM::FogState* CellStore::getFog() const @@ -1018,14 +1173,14 @@ namespace MWWorld return mFogState.get(); } - void clearCorpse(const MWWorld::Ptr& ptr) + static void clearCorpse(const MWWorld::Ptr& ptr, const ESMStore& store) { const MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); - static const float fCorpseClearDelay = MWBase::Environment::get().getWorld()->getStore().get().find("fCorpseClearDelay")->mValue.getFloat(); - if (creatureStats.isDead() && - creatureStats.isDeathAnimationFinished() && - !ptr.getClass().isPersistent(ptr) && - creatureStats.getTimeOfDeath() + fCorpseClearDelay <= MWBase::Environment::get().getWorld()->getTimeStamp()) + static const float fCorpseClearDelay + = store.get().find("fCorpseClearDelay")->mValue.getFloat(); + if (creatureStats.isDead() && creatureStats.isDeathAnimationFinished() && !ptr.getClass().isPersistent(ptr) + && creatureStats.getTimeOfDeath() + fCorpseClearDelay + <= MWBase::Environment::get().getWorld()->getTimeStamp()) { MWBase::Environment::get().getWorld()->deleteObject(ptr); } @@ -1035,18 +1190,18 @@ namespace MWWorld { if (mState == State_Loaded) { - for (CellRefList::List::iterator it (mCreatures.mList.begin()); it!=mCreatures.mList.end(); ++it) + for (MWWorld::LiveCellRef& creature : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&creature); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { MWBase::Environment::get().getMechanicsManager()->restoreDynamicStats(ptr, hours, true); } } - for (CellRefList::List::iterator it (mNpcs.mList.begin()); it!=mNpcs.mList.end(); ++it) + for (MWWorld::LiveCellRef& npc : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&npc); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { MWBase::Environment::get().getMechanicsManager()->restoreDynamicStats(ptr, hours, true); } @@ -1061,27 +1216,27 @@ namespace MWWorld if (mState == State_Loaded) { - for (CellRefList::List::iterator it (mCreatures.mList.begin()); it!=mCreatures.mList.end(); ++it) + for (MWWorld::LiveCellRef& creature : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&creature); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } } - for (CellRefList::List::iterator it (mNpcs.mList.begin()); it!=mNpcs.mList.end(); ++it) + for (MWWorld::LiveCellRef& npc : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + Ptr ptr = getCurrentPtr(&npc); + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } } - for (CellRefList::List::iterator it (mContainers.mList.begin()); it!=mContainers.mList.end(); ++it) + for (MWWorld::LiveCellRef& container : get().mList) { - Ptr ptr = getCurrentPtr(&*it); - if (!ptr.isEmpty() && ptr.getRefData().getCustomData() != nullptr && ptr.getRefData().getCount() > 0 - && ptr.getClass().getContainerStore(ptr).isResolved()) + Ptr ptr = getCurrentPtr(&container); + if (!ptr.isEmpty() && ptr.getRefData().getCustomData() != nullptr && ptr.getCellRef().getCount() > 0 + && ptr.getClass().getContainerStore(ptr).isResolved()) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } @@ -1095,35 +1250,39 @@ namespace MWWorld { if (mState == State_Loaded) { - static const int iMonthsToRespawn = MWBase::Environment::get().getWorld()->getStore().get().find("iMonthsToRespawn")->mValue.getInteger(); - if (MWBase::Environment::get().getWorld()->getTimeStamp() - mLastRespawn > 24*30*iMonthsToRespawn) + static const int iMonthsToRespawn + = mStore.get().find("iMonthsToRespawn")->mValue.getInteger(); + if (MWBase::Environment::get().getWorld()->getTimeStamp() - mLastRespawn > 24 * 30 * iMonthsToRespawn) { mLastRespawn = MWBase::Environment::get().getWorld()->getTimeStamp(); - for (CellRefList::List::iterator it (mContainers.mList.begin()); it!=mContainers.mList.end(); ++it) + for (CellRefList::List::iterator it(get().mList.begin()); + it != get().mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); ptr.getClass().respawn(ptr); } } - for (CellRefList::List::iterator it (mCreatures.mList.begin()); it!=mCreatures.mList.end(); ++it) + for (CellRefList::List::iterator it(get().mList.begin()); + it != get().mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); - clearCorpse(ptr); + clearCorpse(ptr, mStore); ptr.getClass().respawn(ptr); } - for (CellRefList::List::iterator it (mNpcs.mList.begin()); it!=mNpcs.mList.end(); ++it) + for (CellRefList::List::iterator it(get().mList.begin()); + it != get().mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); - clearCorpse(ptr); - ptr.getClass().respawn(ptr); - } - for (CellRefList::List::iterator it (mCreatureLists.mList.begin()); it!=mCreatureLists.mList.end(); ++it) - { - Ptr ptr = getCurrentPtr(&*it); - // no need to clearCorpse, handled as part of mCreatures + clearCorpse(ptr, mStore); ptr.getClass().respawn(ptr); } + forEachType([](Ptr ptr) { + // no need to clearCorpse, handled as part of get() + if (!ptr.mRef->isDeleted()) + ptr.getClass().respawn(ptr); + return true; + }); } } @@ -1144,39 +1303,74 @@ namespace MWWorld { mRechargingItems.clear(); - const auto update = [this](auto& list) - { - for (auto & item : list) + const auto update = [this](auto& list) { + for (auto& item : list) { Ptr ptr = getCurrentPtr(&item); - if (!ptr.isEmpty() && ptr.getRefData().getCount() > 0) + if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { checkItem(ptr); } } }; - update(mWeapons.mList); - update(mArmors.mList); - update(mClothes.mList); - update(mBooks.mList); + update(get().mList); + update(get().mList); + update(get().mList); + update(get().mList); } - void MWWorld::CellStore::checkItem(Ptr ptr) + void MWWorld::CellStore::checkItem(const Ptr& ptr) { - if (ptr.getClass().getEnchantment(ptr).empty()) + const ESM::RefId& enchantmentId = ptr.getClass().getEnchantment(ptr); + if (enchantmentId.empty()) return; - std::string enchantmentId = ptr.getClass().getEnchantment(ptr); - const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().search(enchantmentId); + const ESM::Enchantment* enchantment = mStore.get().search(enchantmentId); if (!enchantment) { - Log(Debug::Warning) << "Warning: Can't find enchantment '" << enchantmentId << "' on item " << ptr.getCellRef().getRefId(); + Log(Debug::Warning) << "Warning: Can't find enchantment " << enchantmentId << " on item " + << ptr.getCellRef().getRefId(); return; } if (enchantment->mData.mType == ESM::Enchantment::WhenUsed - || enchantment->mData.mType == ESM::Enchantment::WhenStrikes) - mRechargingItems.emplace_back(ptr.getBase(), static_cast(enchantment->mData.mCharge)); + || enchantment->mData.mType == ESM::Enchantment::WhenStrikes) + mRechargingItems.emplace_back( + ptr.getBase(), static_cast(MWMechanics::getEnchantmentCharge(*enchantment))); + } + + Ptr MWWorld::CellStore::getMovedActor(int actorId) const + { + for (const auto& [cellRef, cell] : mMovedToAnotherCell) + { + if (cellRef->mClass->isActor() && cellRef->mData.getCustomData()) + { + Ptr actor(cellRef, cell); + if (actor.getClass().getCreatureStats(actor).getActorId() == actorId) + return actor; + } + } + return {}; + } + + Ptr CellStore::getPtr(ESM::RefId id) + { + if (mState == CellStore::State_Unloaded) + preload(); + + if (mState == CellStore::State_Preloaded) + { + if (!std::binary_search(mIds.begin(), mIds.end(), id)) + return Ptr(); + load(); + } + + Ptr ptr = search(id); + + if (!ptr.isEmpty() && isAccessible(ptr.getRefData(), ptr.getCellRef())) + return ptr; + + return Ptr(); } } diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index edd8577ae0f..097053e2e02 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -2,563 +2,441 @@ #define GAME_MWWORLD_CELLSTORE_H #include +#include +#include #include #include +#include +#include #include -#include -#include +#include -#include "livecellref.hpp" +#include "cell.hpp" #include "cellreflist.hpp" +#include "livecellref.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include -#include "timestamp.hpp" #include "ptr.hpp" +#include "timestamp.hpp" namespace ESM { + class ReadersCache; struct Cell; struct CellState; - struct FogState; - struct CellId; - struct RefNum; + struct FormId; + using RefNum = FormId; + struct Activator; + struct Potion; + struct Apparatus; + struct Armor; + struct Book; + struct Clothing; + struct Container; + struct Creature; + struct Door; + struct Ingredient; + struct CreatureLevList; + struct ItemLevList; + struct Light; + struct Lockpick; + struct Miscellaneous; + struct NPC; + struct Probe; + struct Repair; + struct Static; + struct Weapon; + struct BodyPart; + struct CellCommon; +} + +namespace ESM4 +{ + class Reader; + struct Cell; + struct Reference; + struct ActorCharacter; + struct Static; + struct Light; + struct Activator; + struct Potion; + struct Ammunition; + struct Armor; + struct Book; + struct Clothing; + struct Container; + struct Door; + struct Furniture; + struct Flora; + struct Ingredient; + struct ItemMod; + struct MiscItem; + struct MovableStatic; + struct StaticCollection; + struct Terminal; + struct Tree; + struct Weapon; + struct Creature; + struct Npc; } namespace MWWorld { class ESMStore; + struct CellStoreImp; + + using CellStoreTuple = std::tuple, CellRefList, + CellRefList, CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList, CellRefList, + + CellRefList, CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList, + CellRefList, CellRefList, CellRefList>; /// \brief Mutable state of a cell class CellStore { - public: + public: + enum State + { + State_Unloaded, + State_Preloaded, + State_Loaded + }; - enum State - { - State_Unloaded, State_Preloaded, State_Loaded - }; - - private: - - const MWWorld::ESMStore& mStore; - std::vector& mReader; - - // Even though fog actually belongs to the player and not cells, - // it makes sense to store it here since we need it once for each cell. - // Note this is nullptr until the cell is explored to save some memory - std::shared_ptr mFogState; - - const ESM::Cell *mCell; - State mState; - bool mHasState; - std::vector mIds; - float mWaterLevel; - - MWWorld::TimeStamp mLastRespawn; - - // List of refs owned by this cell - CellRefList mActivators; - CellRefList mPotions; - CellRefList mAppas; - CellRefList mArmors; - CellRefList mBooks; - CellRefList mClothes; - CellRefList mContainers; - CellRefList mCreatures; - CellRefList mDoors; - CellRefList mIngreds; - CellRefList mCreatureLists; - CellRefList mItemLists; - CellRefList mLights; - CellRefList mLockpicks; - CellRefList mMiscItems; - CellRefList mNpcs; - CellRefList mProbes; - CellRefList mRepairs; - CellRefList mStatics; - CellRefList mWeapons; - CellRefList mBodyParts; - - typedef std::map MovedRefTracker; - // References owned by a different cell that have been moved here. - // - MovedRefTracker mMovedHere; - // References owned by this cell that have been moved to another cell. - // - MovedRefTracker mMovedToAnotherCell; - - // Merged list of ref's currently in this cell - i.e. with added refs from mMovedHere, removed refs from mMovedToAnotherCell - std::vector mMergedRefs; - - // Get the Ptr for the given ref which originated from this cell (possibly moved to another cell at this point). - Ptr getCurrentPtr(MWWorld::LiveCellRefBase* ref); - - /// Moves object from the given cell to this cell. - void moveFrom(const MWWorld::Ptr& object, MWWorld::CellStore* from); - - /// Repopulate mMergedRefs. - void updateMergedRefs(); - - // (item, max charge) - typedef std::vector > TRechargingItems; - TRechargingItems mRechargingItems; - - bool mRechargingItemsUpToDate; - - void updateRechargingItems(); - void rechargeItems(float duration); - void checkItem(Ptr ptr); - - // helper function for forEachInternal - template - bool forEachImp (Visitor& visitor, List& list) - { - for (typename List::List::iterator iter (list.mList.begin()); iter!=list.mList.end(); - ++iter) - { - if (!isAccessible(iter->mData, iter->mRef)) - continue; - if (!visitor (MWWorld::Ptr(&*iter, this))) - return false; - } - return true; - } + /// Should this reference be accessible to the outside world (i.e. to scripts / game logic)? + /// Determined based on the deletion flags. By default, objects deleted by content files are never accessible; + /// objects deleted by setCount(0) are still accessible *if* they came from a content file (needed for vanilla + /// scripting compatibility, and the fact that objects may be "un-deleted" in the original game). + static bool isAccessible(const MWWorld::RefData& refdata, const MWWorld::CellRef& cref) + { + return !refdata.isDeletedByContentFile() && (cref.hasContentFile() || cref.getCount() > 0); + } - // listing only objects owned by this cell. Internal use only, you probably want to use forEach() so that moved objects are accounted for. - template - bool forEachInternal (Visitor& visitor) - { - return - forEachImp (visitor, mActivators) && - forEachImp (visitor, mPotions) && - forEachImp (visitor, mAppas) && - forEachImp (visitor, mArmors) && - forEachImp (visitor, mBooks) && - forEachImp (visitor, mClothes) && - forEachImp (visitor, mContainers) && - forEachImp (visitor, mDoors) && - forEachImp (visitor, mIngreds) && - forEachImp (visitor, mItemLists) && - forEachImp (visitor, mLights) && - forEachImp (visitor, mLockpicks) && - forEachImp (visitor, mMiscItems) && - forEachImp (visitor, mProbes) && - forEachImp (visitor, mRepairs) && - forEachImp (visitor, mStatics) && - forEachImp (visitor, mWeapons) && - forEachImp (visitor, mBodyParts) && - forEachImp (visitor, mCreatures) && - forEachImp (visitor, mNpcs) && - forEachImp (visitor, mCreatureLists); - } + /// Moves object from this cell to the given cell. + /// @note automatically updates given cell by calling cellToMoveTo->moveFrom(...) + /// @note throws exception if cellToMoveTo == this + /// @return updated MWWorld::Ptr with the new CellStore pointer set. + MWWorld::Ptr moveTo(const MWWorld::Ptr& object, MWWorld::CellStore* cellToMoveTo); - /// @note If you get a linker error here, this means the given type can not be stored in a cell. The supported types are - /// defined at the bottom of this file. - template - CellRefList& get(); + void rest(double hours); + void recharge(float duration); - public: + /// Make a copy of the given object and insert it into this cell. + /// @note If you get a linker error here, this means the given type can not be inserted into a cell. + /// The supported types are defined at the bottom of this file. + template + LiveCellRefBase* insert(const LiveCellRef* ref) + { + mHasState = true; + CellRefList& list = get(); + LiveCellRefBase* ret = &list.insert(*ref); + requestMergedRefsUpdate(); + return ret; + } - /// Should this reference be accessible to the outside world (i.e. to scripts / game logic)? - /// Determined based on the deletion flags. By default, objects deleted by content files are never accessible; - /// objects deleted by setCount(0) are still accessible *if* they came from a content file (needed for vanilla - /// scripting compatibility, and the fact that objects may be "un-deleted" in the original game). - static bool isAccessible(const MWWorld::RefData& refdata, const MWWorld::CellRef& cref) - { - return !refdata.isDeletedByContentFile() && (cref.hasContentFile() || refdata.getCount() > 0); - } + /// @param readerList The readers to use for loading of the cell on-demand. + CellStore(MWWorld::Cell&& cell, const MWWorld::ESMStore& store, ESM::ReadersCache& readers); - /// Moves object from this cell to the given cell. - /// @note automatically updates given cell by calling cellToMoveTo->moveFrom(...) - /// @note throws exception if cellToMoveTo == this - /// @return updated MWWorld::Ptr with the new CellStore pointer set. - MWWorld::Ptr moveTo(const MWWorld::Ptr& object, MWWorld::CellStore* cellToMoveTo); + CellStore(const CellStore&) = delete; - void rest(double hours); - void recharge(float duration); + CellStore(CellStore&&) = delete; - /// Make a copy of the given object and insert it into this cell. - /// @note If you get a linker error here, this means the given type can not be inserted into a cell. - /// The supported types are defined at the bottom of this file. - template - LiveCellRefBase* insert(const LiveCellRef* ref) - { - mHasState = true; - CellRefList& list = get(); - LiveCellRefBase* ret = &list.insert(*ref); - updateMergedRefs(); - return ret; - } + CellStore& operator=(const CellStore&) = delete; - /// @param readerList The readers to use for loading of the cell on-demand. - CellStore (const ESM::Cell *cell_, - const MWWorld::ESMStore& store, - std::vector& readerList); + CellStore& operator=(CellStore&&) = delete; - const ESM::Cell *getCell() const; + ~CellStore(); - State getState() const; + const MWWorld::Cell* getCell() const; - const std::vector& getPreloadedIds() const; - ///< Get Ids of objects in this cell, only valid in State_Preloaded + State getState() const; - bool hasState() const; - ///< Does this cell have state that needs to be stored in a saved game file? + const std::vector& getPreloadedIds() const; + ///< Get Ids of objects in this cell, only valid in State_Preloaded - bool hasId (const std::string& id) const; - ///< May return true for deleted IDs when in preload state. Will return false, if cell is - /// unloaded. - /// @note Will not account for moved references which may exist in Loaded state. Use search() instead if the cell is loaded. + bool hasState() const; + ///< Does this cell have state that needs to be stored in a saved game file? - Ptr search (const std::string& id); - ///< Will return an empty Ptr if cell is not loaded. Does not check references in - /// containers. - /// @note Triggers CellStore hasState flag. + bool hasId(const ESM::RefId& id) const; + ///< May return true for deleted IDs when in preload state. Will return false, if cell is + /// unloaded. + /// @note Will not account for moved references which may exist in Loaded state. Use search() instead if the + /// cell is loaded. - ConstPtr searchConst (const std::string& id) const; - ///< Will return an empty Ptr if cell is not loaded. Does not check references in - /// containers. - /// @note Does not trigger CellStore hasState flag. + Ptr search(const ESM::RefId& id); + ///< Will return an empty Ptr if cell is not loaded. Does not check references in + /// containers. + /// @note Triggers CellStore hasState flag. - Ptr searchViaActorId (int id); - ///< Will return an empty Ptr if cell is not loaded. + ConstPtr searchConst(const ESM::RefId& id) const; + ///< Will return an empty Ptr if cell is not loaded. Does not check references in + /// containers. + /// @note Does not trigger CellStore hasState flag. - Ptr searchViaRefNum (const ESM::RefNum& refNum); - ///< Will return an empty Ptr if cell is not loaded. Does not check references in - /// containers. - /// @note Triggers CellStore hasState flag. + Ptr searchViaActorId(int id); + ///< Will return an empty Ptr if cell is not loaded. - float getWaterLevel() const; + float getWaterLevel() const; - bool movedHere(const MWWorld::Ptr& ptr) const; + bool movedHere(const MWWorld::Ptr& ptr) const; - void setWaterLevel (float level); + void setWaterLevel(float level); - void setFog (ESM::FogState* fog); - ///< \note Takes ownership of the pointer + void setFog(std::unique_ptr&& fog); + ///< \note Takes ownership of the pointer - ESM::FogState* getFog () const; + ESM::FogState* getFog() const; - std::size_t count() const; - ///< Return total number of references, including deleted ones. + std::size_t count() const; + ///< Return total number of references, including deleted ones. - void load (); - ///< Load references from content file. + void load(); + ///< Load references from content file. - void preload (); - ///< Build ID list from content file. + void preload(); + ///< Build ID list from content file. - /// Call visitor (MWWorld::Ptr) for each reference. visitor must return a bool. Returning - /// false will abort the iteration. - /// \note Prefer using forEachConst when possible. - /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in unintended behaviour. - /// \attention This function also lists deleted (count 0) objects! - /// \return Iteration completed? - template - bool forEach (Visitor&& visitor) - { - if (mState != State_Loaded) - return false; + /// Call visitor (MWWorld::Ptr) for each reference. visitor must return a bool. Returning + /// false will abort the iteration. + /// \note Prefer using forEachConst when possible. + /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in + /// unintended behaviour. \attention This function also lists deleted (count 0) objects! + /// \return Iteration completed? + template + bool forEach(Visitor&& visitor) + { + if (mState != State_Loaded) + return false; - if (mMergedRefs.empty()) - return true; + if (mMergedRefsNeedsUpdate) + updateMergedRefs(); + if (mMergedRefs.empty()) + return true; - mHasState = true; + mHasState = true; - for (unsigned int i=0; imData, mMergedRefs[i]->mRef)) - continue; + for (LiveCellRefBase* mergedRef : mMergedRefs) + { + if (!isAccessible(mergedRef->mData, mergedRef->mRef)) + continue; - if (!visitor(MWWorld::Ptr(mMergedRefs[i], this))) - return false; - } - return true; + if (!visitor(MWWorld::Ptr(mergedRef, this))) + return false; } + return true; + } + + /// Call visitor (MWWorld::ConstPtr) for each reference. visitor must return a bool. Returning + /// false will abort the iteration. + /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in + /// unintended behaviour. \attention This function also lists deleted (count 0) objects! + /// \return Iteration completed? + template + bool forEachConst(Visitor&& visitor) const + { + if (mState != State_Loaded) + return false; + + if (mMergedRefsNeedsUpdate) + updateMergedRefs(); - /// Call visitor (MWWorld::ConstPtr) for each reference. visitor must return a bool. Returning - /// false will abort the iteration. - /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in unintended behaviour. - /// \attention This function also lists deleted (count 0) objects! - /// \return Iteration completed? - template - bool forEachConst (Visitor&& visitor) const + for (const LiveCellRefBase* mergedRef : mMergedRefs) { - if (mState != State_Loaded) - return false; - - for (unsigned int i=0; imData, mMergedRefs[i]->mRef)) - continue; + if (!isAccessible(mergedRef->mData, mergedRef->mRef)) + continue; - if (!visitor(MWWorld::ConstPtr(mMergedRefs[i], this))) - return false; - } - return true; + if (!visitor(MWWorld::ConstPtr(mergedRef, this))) + return false; } + return true; + } + + /// Call visitor (ref) for each reference of given type. visitor must return a bool. Returning + /// false will abort the iteration. + /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in + /// unintended behaviour. \attention This function also lists deleted (count 0) objects! + /// \return Iteration completed? + template + bool forEachType(Visitor&& visitor) + { + if (mState != State_Loaded) + return false; + + if (mMergedRefsNeedsUpdate) + updateMergedRefs(); + if (mMergedRefs.empty()) + return true; + mHasState = true; - /// Call visitor (ref) for each reference of given type. visitor must return a bool. Returning - /// false will abort the iteration. - /// \note Do not modify this cell (i.e. remove/add objects) during the forEach, doing this may result in unintended behaviour. - /// \attention This function also lists deleted (count 0) objects! - /// \return Iteration completed? - template - bool forEachType(Visitor& visitor) + CellRefList& list = get(); + + for (typename CellRefList::List::iterator it(list.mList.begin()); it != list.mList.end(); ++it) { - if (mState != State_Loaded) + LiveCellRefBase* base = &*it; + if (mMovedToAnotherCell.find(base) != mMovedToAnotherCell.end()) + continue; + if (!isAccessible(base->mData, base->mRef)) + continue; + if (!visitor(MWWorld::Ptr(base, this))) return false; + } - if (mMergedRefs.empty()) - return true; - - mHasState = true; - - CellRefList& list = get(); - - for (typename CellRefList::List::iterator it (list.mList.begin()); it!=list.mList.end(); ++it) - { - LiveCellRefBase* base = &*it; - if (mMovedToAnotherCell.find(base) != mMovedToAnotherCell.end()) - continue; - if (!isAccessible(base->mData, base->mRef)) - continue; + for (MovedRefTracker::const_iterator it = mMovedHere.begin(); it != mMovedHere.end(); ++it) + { + LiveCellRefBase* base = it->first; + if (dynamic_cast*>(base)) if (!visitor(MWWorld::Ptr(base, this))) return false; - } - - for (MovedRefTracker::const_iterator it = mMovedHere.begin(); it != mMovedHere.end(); ++it) - { - LiveCellRefBase* base = it->first; - if (dynamic_cast*>(base)) - if (!visitor(MWWorld::Ptr(base, this))) - return false; - } - return true; } + return true; + } - // NOTE: does not account for moved references - // Should be phased out when we have const version of forEach - inline const CellRefList& getReadOnlyDoors() const - { - return mDoors; - } - inline const CellRefList& getReadOnlyStatics() const - { - return mStatics; - } + // NOTE: does not account for moved references + // Should be phased out when we have const version of forEach + inline const CellRefList& getReadOnlyDoors() const { return get(); } + inline const CellRefList& getReadOnlyEsm4Doors() const { return get(); } + inline const CellRefList& getReadOnlyStatics() const { return get(); } + inline const CellRefList& getReadOnlyEsm4Statics() const { return get(); } - bool isExterior() const; + bool isExterior() const; - Ptr searchInContainer (const std::string& id); + bool isQuasiExterior() const; - void loadState (const ESM::CellState& state); + Ptr searchInContainer(const ESM::RefId& id); - void saveState (ESM::CellState& state) const; + void loadState(const ESM::CellState& state); - void writeFog (ESM::ESMWriter& writer) const; + void saveState(ESM::CellState& state) const; - void readFog (ESM::ESMReader& reader); + void writeFog(ESM::ESMWriter& writer) const; - void writeReferences (ESM::ESMWriter& writer) const; + void readFog(ESM::ESMReader& reader); - struct GetCellStoreCallback - { - public: - ///@note must return nullptr if the cell is not found - virtual CellStore* getCellStore(const ESM::CellId& cellId) = 0; - virtual ~GetCellStoreCallback() = default; - }; + void writeReferences(ESM::ESMWriter& writer) const; - /// @param callback to use for retrieving of additional CellStore objects by ID (required for resolving moved references) - void readReferences (ESM::ESMReader& reader, const std::map& contentFileMap, GetCellStoreCallback* callback); + struct GetCellStoreCallback + { + ///@note must return nullptr if the cell is not found + virtual CellStore* getCellStore(const ESM::RefId& cellId) = 0; + virtual ~GetCellStoreCallback() = default; + }; - void respawn (); - ///< Check mLastRespawn and respawn references if necessary. This is a no-op if the cell is not loaded. + /// @param callback to use for retrieving of additional CellStore objects by ID (required for resolving moved + /// references) + void readReferences(ESM::ESMReader& reader, GetCellStoreCallback* callback); - private: + void respawn(); + ///< Check mLastRespawn and respawn references if necessary. This is a no-op if the cell is not loaded. - /// Run through references and store IDs - void listRefs(); + Ptr getMovedActor(int actorId) const; - void loadRefs(); + Ptr getPtr(ESM::RefId id); - void loadRef (ESM::CellRef& ref, bool deleted, std::map& refNumToID); - ///< Make case-adjustments to \a ref and insert it into the respective container. - /// - /// Invalid \a ref objects are silently dropped. - }; + private: + friend struct CellStoreImp; - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mActivators; - } + const MWWorld::ESMStore& mStore; + ESM::ReadersCache& mReaders; - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mPotions; - } + // Even though fog actually belongs to the player and not cells, + // it makes sense to store it here since we need it once for each cell. + // Note this is nullptr until the cell is explored to save some memory + std::unique_ptr mFogState; - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mAppas; - } + MWWorld::Cell mCellVariant; + State mState; + bool mHasState; + std::vector mIds; + float mWaterLevel; - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mArmors; - } + MWWorld::TimeStamp mLastRespawn; - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mBooks; - } + template + static constexpr std::size_t getTypeIndex() + { + static_assert(Misc::TupleHasType, CellStoreTuple>::value); + return Misc::TupleTypeIndex, CellStoreTuple>::value; + } - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mClothes; - } + std::unique_ptr mCellStoreImp; + std::vector mCellRefLists; - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mContainers; - } + template + CellRefList& get() + { + mHasState = true; + return static_cast&>(*mCellRefLists[getTypeIndex()]); + } - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mCreatures; - } + template + const CellRefList& get() const + { + return static_cast&>(*mCellRefLists[getTypeIndex()]); + } - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mDoors; - } + typedef std::map MovedRefTracker; + // References owned by a different cell that have been moved here. + // + MovedRefTracker mMovedHere; + // References owned by this cell that have been moved to another cell. + // + MovedRefTracker mMovedToAnotherCell; - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mIngreds; - } + // Merged list of ref's currently in this cell - i.e. with added refs from mMovedHere, removed refs from + // mMovedToAnotherCell + mutable std::vector mMergedRefs; + mutable bool mMergedRefsNeedsUpdate = false; - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mCreatureLists; - } + // Get the Ptr for the given ref which originated from this cell (possibly moved to another cell at this point). + Ptr getCurrentPtr(MWWorld::LiveCellRefBase* ref); - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mItemLists; - } + /// Moves object from the given cell to this cell. + void moveFrom(const MWWorld::Ptr& object, MWWorld::CellStore* from); - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mLights; - } + /// Repopulate mMergedRefs. + void requestMergedRefsUpdate(); + void updateMergedRefs() const; - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mLockpicks; - } + // (item, max charge) + typedef std::vector> TRechargingItems; + TRechargingItems mRechargingItems; - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mMiscItems; - } + bool mRechargingItemsUpToDate; - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mNpcs; - } + void updateRechargingItems(); + void rechargeItems(float duration); + void checkItem(const Ptr& ptr); - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mProbes; - } - - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mRepairs; - } - - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mStatics; - } - - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mWeapons; - } - - template<> - inline CellRefList& CellStore::get() - { - mHasState = true; - return mBodyParts; - } + /// Run through references and store IDs + void listRefs(const ESM::Cell& cell); + void listRefs(const ESM4::Cell& cell); + void listRefs(); + + void loadRefs(const ESM::Cell& cell, std::map& refNumToID); + void loadRefs(const ESM4::Cell& cell, std::map& refNumToID); + + void loadRefs(); + + void loadRef(const ESM4::Reference& ref); + void loadRef(const ESM4::ActorCharacter& ref); + void loadRef(ESM::CellRef& ref, bool deleted, std::map& refNumToID); + ///< Make case-adjustments to \a ref and insert it into the respective container. + /// + /// Invalid \a ref objects are silently dropped. + /// + }; - bool operator== (const CellStore& left, const CellStore& right); - bool operator!= (const CellStore& left, const CellStore& right); } #endif diff --git a/apps/openmw/mwworld/cellvisitors.hpp b/apps/openmw/mwworld/cellvisitors.hpp index e68b383b775..f4d606d02da 100644 --- a/apps/openmw/mwworld/cellvisitors.hpp +++ b/apps/openmw/mwworld/cellvisitors.hpp @@ -1,29 +1,29 @@ #ifndef GAME_MWWORLD_CELLVISITORS_H #define GAME_MWWORLD_CELLVISITORS_H -#include #include +#include #include "ptr.hpp" - namespace MWWorld { struct ListAndResetObjectsVisitor { std::vector mObjects; - bool operator() (MWWorld::Ptr ptr) + bool operator()(const MWWorld::Ptr& ptr) { if (ptr.getRefData().getBaseNode()) { ptr.getRefData().setBaseNode(nullptr); - mObjects.push_back (ptr); } + mObjects.push_back(ptr); return true; } }; + } #endif diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 950c8a6d495..a1eec196eb1 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -3,78 +3,87 @@ #include #include +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" -#include "ptr.hpp" -#include "refdata.hpp" -#include "nullaction.hpp" -#include "failedaction.hpp" #include "actiontake.hpp" #include "containerstore.hpp" +#include "failedaction.hpp" +#include "inventorystore.hpp" +#include "nullaction.hpp" +#include "ptr.hpp" +#include "worldmodel.hpp" #include "../mwgui/tooltips.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" namespace MWWorld { - std::map > Class::sClasses; - - Class::Class() {} - - Class::~Class() {} - - void Class::insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const + std::map& Class::getClasses() { + static std::map values; + return values; + } + void Class::insertObjectRendering( + const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const + { } - void Class::insertObject(const Ptr& ptr, const std::string& mesh, MWPhysics::PhysicsSystem& physics) const + void Class::insertObject( + const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { + } + void Class::insertObjectPhysics( + const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const + { } - bool Class::apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const + bool Class::consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const { return false; } - void Class::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor) const + void Class::skillUsageSucceeded(const MWWorld::Ptr& ptr, ESM::RefId skill, int usageType, float extraFactor) const { - throw std::runtime_error ("class does not represent an actor"); + throw std::runtime_error("class does not represent an actor"); } - bool Class::canSell (const MWWorld::ConstPtr& item, int npcServices) const + bool Class::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return false; } - int Class::getServices(const ConstPtr &actor) const + int Class::getServices(const ConstPtr& actor) const { - throw std::runtime_error ("class does not have services"); + throw std::runtime_error("class does not have services"); } - MWMechanics::CreatureStats& Class::getCreatureStats (const Ptr& ptr) const + MWMechanics::CreatureStats& Class::getCreatureStats(const Ptr& ptr) const { - throw std::runtime_error ("class does not have creature stats"); + throw std::runtime_error("class does not have creature stats"); } - MWMechanics::NpcStats& Class::getNpcStats (const Ptr& ptr) const + MWMechanics::NpcStats& Class::getNpcStats(const Ptr& ptr) const { - throw std::runtime_error ("class does not have NPC stats"); + throw std::runtime_error("class does not have NPC stats"); } - bool Class::hasItemHealth (const ConstPtr& ptr) const + bool Class::hasItemHealth(const ConstPtr& ptr) const { return false; } - int Class::getItemHealth(const ConstPtr &ptr) const + int Class::getItemHealth(const ConstPtr& ptr) const { if (ptr.getCellRef().getCharge() == -1) return getItemMaxHealth(ptr); @@ -82,7 +91,7 @@ namespace MWWorld return ptr.getCellRef().getCharge(); } - float Class::getItemNormalizedHealth (const ConstPtr& ptr) const + float Class::getItemNormalizedHealth(const ConstPtr& ptr) const { if (getItemMaxHealth(ptr) == 0) { @@ -94,191 +103,190 @@ namespace MWWorld } } - int Class::getItemMaxHealth (const ConstPtr& ptr) const + int Class::getItemMaxHealth(const ConstPtr& ptr) const { - throw std::runtime_error ("class does not have item health"); + throw std::runtime_error("class does not have item health"); } - void Class::hit(const Ptr& ptr, float attackStrength, int type) const + bool Class::evaluateHit(const Ptr& ptr, Ptr& victim, osg::Vec3f& hitPosition) const { throw std::runtime_error("class cannot hit"); } - void Class::block(const Ptr &ptr) const + void Class::hit(const Ptr& ptr, float attackStrength, int type, const Ptr& victim, const osg::Vec3f& hitPosition, + bool success) const { - throw std::runtime_error("class cannot block"); + throw std::runtime_error("class cannot hit"); } - void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const + void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, + const osg::Vec3f& hitPosition, bool successful, const MWMechanics::DamageSourceType sourceType) const { throw std::runtime_error("class cannot be hit"); } - std::shared_ptr Class::activate (const Ptr& ptr, const Ptr& actor) const + std::unique_ptr Class::activate(const Ptr& ptr, const Ptr& actor) const { - return std::shared_ptr (new NullAction); + return std::make_unique(); } - std::shared_ptr Class::use (const Ptr& ptr, bool force) const + std::unique_ptr Class::use(const Ptr& ptr, bool force) const { - return std::shared_ptr (new NullAction); + return std::make_unique(); } - ContainerStore& Class::getContainerStore (const Ptr& ptr) const + ContainerStore& Class::getContainerStore(const Ptr& ptr) const { - throw std::runtime_error ("class does not have a container store"); + throw std::runtime_error("class does not have a container store"); } - InventoryStore& Class::getInventoryStore (const Ptr& ptr) const + InventoryStore& Class::getInventoryStore(const Ptr& ptr) const { - throw std::runtime_error ("class does not have an inventory store"); + throw std::runtime_error("class does not have an inventory store"); } - bool Class::hasInventoryStore(const Ptr &ptr) const + bool Class::hasInventoryStore(const ConstPtr& ptr) const { return false; } - bool Class::canLock(const ConstPtr &ptr) const + bool Class::canLock(const ConstPtr& ptr) const { return false; } - void Class::setRemainingUsageTime (const Ptr& ptr, float duration) const + void Class::setRemainingUsageTime(const Ptr& ptr, float duration) const { - throw std::runtime_error ("class does not support time-based uses"); + throw std::runtime_error("class does not support time-based uses"); } - float Class::getRemainingUsageTime (const ConstPtr& ptr) const + float Class::getRemainingUsageTime(const ConstPtr& ptr) const { return -1; } - std::string Class::getScript (const ConstPtr& ptr) const + ESM::RefId Class::getScript(const ConstPtr& ptr) const { - return ""; + return ESM::RefId(); } - float Class::getMaxSpeed (const Ptr& ptr) const + float Class::getMaxSpeed(const Ptr& ptr) const { return 0; } - - float Class::getCurrentSpeed (const Ptr& ptr) const + + float Class::getCurrentSpeed(const Ptr& ptr) const { return 0; } - float Class::getJump (const Ptr& ptr) const + float Class::getJump(const Ptr& ptr) const { return 0; } - int Class::getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const + int Class::getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const { - throw std::runtime_error ("class does not support enchanting"); + throw std::runtime_error("class does not support enchanting"); } - MWMechanics::Movement& Class::getMovementSettings (const Ptr& ptr) const + MWMechanics::Movement& Class::getMovementSettings(const Ptr& ptr) const { - throw std::runtime_error ("movement settings not supported by class"); + throw std::runtime_error("movement settings not supported by class"); } - osg::Vec3f Class::getRotationVector (const Ptr& ptr) const + osg::Vec3f Class::getRotationVector(const Ptr& ptr) const { - return osg::Vec3f (0, 0, 0); + return osg::Vec3f(0, 0, 0); } - std::pair, bool> Class::getEquipmentSlots (const ConstPtr& ptr) const + std::pair, bool> Class::getEquipmentSlots(const ConstPtr& ptr) const { - return std::make_pair (std::vector(), false); + return std::make_pair(std::vector(), false); } - int Class::getEquipmentSkill (const ConstPtr& ptr) const + ESM::RefId Class::getEquipmentSkill(const ConstPtr& ptr) const { - return -1; + return {}; } - int Class::getValue (const ConstPtr& ptr) const + int Class::getValue(const ConstPtr& ptr) const { - throw std::logic_error ("value not supported by this class"); + throw std::logic_error("value not supported by this class"); } - float Class::getCapacity (const MWWorld::Ptr& ptr) const + float Class::getCapacity(const MWWorld::Ptr& ptr) const { - throw std::runtime_error ("capacity not supported by this class"); + throw std::runtime_error("capacity not supported by this class"); } - float Class::getWeight(const ConstPtr &ptr) const + float Class::getWeight(const ConstPtr& ptr) const { - throw std::runtime_error ("weight not supported by this class"); + throw std::runtime_error("weight not supported by this class"); } - float Class::getEncumbrance (const MWWorld::Ptr& ptr) const + float Class::getEncumbrance(const MWWorld::Ptr& ptr) const { - throw std::runtime_error ("encumbrance not supported by class"); + throw std::runtime_error("encumbrance not supported by class"); } - bool Class::isEssential (const MWWorld::ConstPtr& ptr) const + bool Class::isEssential(const MWWorld::ConstPtr& ptr) const { return false; } - float Class::getArmorRating (const MWWorld::Ptr& ptr) const + float Class::getArmorRating(const MWWorld::Ptr& ptr) const { throw std::runtime_error("Class does not support armor rating"); } - const Class& Class::get (const std::string& key) + const Class& Class::get(unsigned int key) { - if (key.empty()) - throw std::logic_error ("Class::get(): attempting to get an empty key"); - - std::map >::const_iterator iter = sClasses.find (key); + const auto& classes = getClasses(); + auto iter = classes.find(key); - if (iter==sClasses.end()) - throw std::logic_error ("Class::get(): unknown class key: " + key); + if (iter == classes.end()) + throw std::logic_error("Class::get(): unknown class key: " + std::to_string(key)); return *iter->second; } - bool Class::isPersistent(const ConstPtr &ptr) const + bool Class::isPersistent(const ConstPtr& ptr) const { - throw std::runtime_error ("class does not support persistence"); + throw std::runtime_error("class does not support persistence"); } - void Class::registerClass(const std::string& key, std::shared_ptr instance) + void Class::registerClass(Class& instance) { - instance->mTypeName = key; - sClasses.insert(std::make_pair(key, instance)); + getClasses().emplace(instance.getType(), &instance); } - std::string Class::getUpSoundId (const ConstPtr& ptr) const + const ESM::RefId& Class::getUpSoundId(const ConstPtr& ptr) const { - throw std::runtime_error ("class does not have an up sound"); + throw std::runtime_error("class does not have an up sound"); } - std::string Class::getDownSoundId (const ConstPtr& ptr) const + const ESM::RefId& Class::getDownSoundId(const ConstPtr& ptr) const { - throw std::runtime_error ("class does not have an down sound"); + throw std::runtime_error("class does not have an down sound"); } - std::string Class::getSoundIdFromSndGen(const Ptr &ptr, const std::string &type) const + ESM::RefId Class::getSoundIdFromSndGen(const Ptr& ptr, std::string_view type) const { throw std::runtime_error("class does not support soundgen look up"); } - std::string Class::getInventoryIcon (const MWWorld::ConstPtr& ptr) const + const std::string& Class::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { - throw std::runtime_error ("class does not have any inventory icon"); + throw std::runtime_error("class does not have any inventory icon"); } - MWGui::ToolTipInfo Class::getToolTipInfo (const ConstPtr& ptr, int count) const + MWGui::ToolTipInfo Class::getToolTipInfo(const ConstPtr& ptr, int count) const { - throw std::runtime_error ("class does not have a tool tip"); + throw std::runtime_error("class does not have a tool tip"); } - bool Class::showsInInventory (const ConstPtr& ptr) const + bool Class::showsInInventory(const ConstPtr& ptr) const { // NOTE: Don't show WerewolfRobe objects in the inventory, or allow them to be taken. // Vanilla likely uses a hack like this since there's no other way to prevent it from @@ -286,23 +294,29 @@ namespace MWWorld return (ptr.getCellRef().getRefId() != "werewolfrobe"); } - bool Class::hasToolTip (const ConstPtr& ptr) const + bool Class::hasToolTip(const ConstPtr& ptr) const { return true; } - std::string Class::getEnchantment (const ConstPtr& ptr) const + ESM::RefId Class::getEnchantment(const ConstPtr& ptr) const { - return ""; + return ESM::RefId(); } - void Class::adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const + void Class::adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const {} + + std::string_view Class::getModel(const MWWorld::ConstPtr& ptr) const { + return {}; } - std::string Class::getModel(const MWWorld::ConstPtr &ptr) const + std::string Class::getCorrectedModel(const MWWorld::ConstPtr& ptr) const { - return ""; + std::string_view model = getModel(ptr); + if (!model.empty()) + return Misc::ResourceHelpers::correctMeshPath(model); + return {}; } bool Class::useAnim() const @@ -310,115 +324,122 @@ namespace MWWorld return false; } - void Class::getModelsToPreload(const Ptr &ptr, std::vector &models) const + void Class::getModelsToPreload(const ConstPtr& ptr, std::vector& models) const { - std::string model = getModel(ptr); + std::string_view model = getModel(ptr); if (!model.empty()) models.push_back(model); } - std::string Class::applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const + const ESM::RefId& Class::applyEnchantment( + const MWWorld::ConstPtr& ptr, const ESM::RefId& enchId, int enchCharge, const std::string& newName) const { - throw std::runtime_error ("class can't be enchanted"); + throw std::runtime_error("class can't be enchanted"); } - std::pair Class::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const + std::pair Class::canBeEquipped(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { - return std::make_pair (1, ""); + return { 1, {} }; } - void Class::adjustPosition(const MWWorld::Ptr& ptr, bool force) const - { - } + void Class::adjustPosition(const MWWorld::Ptr& ptr, bool force) const {} - std::shared_ptr Class::defaultItemActivate(const Ptr &ptr, const Ptr &actor) const + std::unique_ptr Class::defaultItemActivate(const Ptr& ptr, const Ptr& actor) const { - if(!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) - return std::shared_ptr(new NullAction()); + if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) + return std::make_unique(); - if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfItem"); + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + const ESM::Sound* sound = store.get().searchRandom("WolfItem", prng); - std::shared_ptr action(new MWWorld::FailedAction("#{sWerewolfRefusal}")); - if(sound) action->setSound(sound->mId); + std::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); + if (sound) + action->setSound(sound->mId); return action; } - std::shared_ptr action(new ActionTake(ptr)); + std::unique_ptr action = std::make_unique(ptr); action->setSound(getUpSoundId(ptr)); return action; } - MWWorld::Ptr - Class::copyToCellImpl(const ConstPtr &ptr, CellStore &cell) const + MWWorld::Ptr Class::copyToCellImpl(const ConstPtr& ptr, CellStore& cell) const { throw std::runtime_error("unable to copy class to cell"); } - MWWorld::Ptr - Class::copyToCell(const ConstPtr &ptr, CellStore &cell, int count) const + MWWorld::Ptr Class::copyToCell(const ConstPtr& ptr, CellStore& cell, int count) const { Ptr newPtr = copyToCellImpl(ptr, cell); newPtr.getCellRef().unsetRefNum(); // This RefNum is only valid within the original cell of the reference - newPtr.getRefData().setCount(count); + newPtr.getCellRef().setCount(count); + newPtr.getRefData().setLuaScripts(nullptr); + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); return newPtr; } - MWWorld::Ptr - Class::copyToCell(const ConstPtr &ptr, CellStore &cell, const ESM::Position &pos, int count) const + MWWorld::Ptr Class::moveToCell(const Ptr& ptr, CellStore& cell) const { - Ptr newPtr = copyToCell(ptr, cell, count); + Ptr newPtr = copyToCellImpl(ptr, cell); + ptr.getRefData().setLuaScripts(nullptr); + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); + return newPtr; + } + + Ptr Class::moveToCell(const Ptr& ptr, CellStore& cell, const ESM::Position& pos) const + { + Ptr newPtr = moveToCell(ptr, cell); newPtr.getRefData().setPosition(pos); + newPtr.getCellRef().setPosition(pos); + return newPtr; + } + MWWorld::Ptr Class::copyToCell(const ConstPtr& ptr, CellStore& cell, const ESM::Position& pos, int count) const + { + Ptr newPtr = copyToCell(ptr, cell, count); + newPtr.getRefData().setPosition(pos); + newPtr.getCellRef().setPosition(pos); return newPtr; } - bool Class::isBipedal(const ConstPtr &ptr) const + bool Class::isBipedal(const ConstPtr& ptr) const { return false; } - bool Class::canFly(const ConstPtr &ptr) const + bool Class::canFly(const ConstPtr& ptr) const { return false; } - bool Class::canSwim(const ConstPtr &ptr) const + bool Class::canSwim(const ConstPtr& ptr) const { return false; } - bool Class::canWalk(const ConstPtr &ptr) const + bool Class::canWalk(const ConstPtr& ptr) const { return false; } bool Class::isPureWaterCreature(const ConstPtr& ptr) const { - return canSwim(ptr) - && !isBipedal(ptr) - && !canFly(ptr) - && !canWalk(ptr); + return canSwim(ptr) && !isBipedal(ptr) && !canFly(ptr) && !canWalk(ptr); } bool Class::isPureFlyingCreature(const ConstPtr& ptr) const { - return canFly(ptr) - && !isBipedal(ptr) - && !canSwim(ptr) - && !canWalk(ptr); + return canFly(ptr) && !isBipedal(ptr) && !canSwim(ptr) && !canWalk(ptr); } bool Class::isPureLandCreature(const Ptr& ptr) const { - return canWalk(ptr) - && !isBipedal(ptr) - && !canFly(ptr) - && !canSwim(ptr); + return canWalk(ptr) && !isBipedal(ptr) && !canFly(ptr) && !canSwim(ptr); } bool Class::isMobile(const MWWorld::Ptr& ptr) const @@ -426,41 +447,41 @@ namespace MWWorld return canSwim(ptr) || canWalk(ptr) || canFly(ptr); } - float Class::getSkill(const MWWorld::Ptr& ptr, int skill) const + float Class::getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const { throw std::runtime_error("class does not support skills"); } - int Class::getBloodTexture (const MWWorld::ConstPtr& ptr) const + int Class::getBloodTexture(const MWWorld::ConstPtr& ptr) const { throw std::runtime_error("class does not support gore"); } - void Class::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const {} + void Class::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const {} - void Class::writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const {} + void Class::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const {} int Class::getBaseGold(const MWWorld::ConstPtr& ptr) const { throw std::runtime_error("class does not support base gold"); } - bool Class::isClass(const MWWorld::ConstPtr& ptr, const std::string &className) const + bool Class::isClass(const MWWorld::ConstPtr& ptr, std::string_view className) const { return false; } - MWWorld::DoorState Class::getDoorState (const MWWorld::ConstPtr &ptr) const + MWWorld::DoorState Class::getDoorState(const MWWorld::ConstPtr& ptr) const { throw std::runtime_error("this is not a door"); } - void Class::setDoorState (const MWWorld::Ptr &ptr, MWWorld::DoorState state) const + void Class::setDoorState(const MWWorld::Ptr& ptr, MWWorld::DoorState state) const { throw std::runtime_error("this is not a door"); } - float Class::getNormalizedEncumbrance(const Ptr &ptr) const + float Class::getNormalizedEncumbrance(const Ptr& ptr) const { float capacity = getCapacity(ptr); float encumbrance = getEncumbrance(ptr); @@ -474,45 +495,44 @@ namespace MWWorld return encumbrance / capacity; } - std::string Class::getSound(const MWWorld::ConstPtr&) const + ESM::RefId Class::getSound(const MWWorld::ConstPtr&) const { - return std::string(); + return ESM::RefId(); } - int Class::getBaseFightRating(const ConstPtr &ptr) const + int Class::getBaseFightRating(const ConstPtr& ptr) const { throw std::runtime_error("class does not support fight rating"); } - std::string Class::getPrimaryFaction (const MWWorld::ConstPtr& ptr) const + ESM::RefId Class::getPrimaryFaction(const MWWorld::ConstPtr& ptr) const { - return std::string(); + return ESM::RefId(); } - int Class::getPrimaryFactionRank (const MWWorld::ConstPtr& ptr) const + int Class::getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const { return -1; } - float Class::getEffectiveArmorRating(const ConstPtr &armor, const Ptr &actor) const + float Class::getEffectiveArmorRating(const ConstPtr& armor, const Ptr& actor) const { throw std::runtime_error("class does not support armor ratings"); } osg::Vec4f Class::getEnchantmentColor(const MWWorld::ConstPtr& item) const { - osg::Vec4f result(1,1,1,1); - std::string enchantmentName = item.getClass().getEnchantment(item); + osg::Vec4f result(1, 1, 1, 1); + const ESM::RefId& enchantmentName = item.getClass().getEnchantment(item); if (enchantmentName.empty()) return result; - const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().search(enchantmentName); - if (!enchantment) + const ESM::Enchantment* enchantment + = MWBase::Environment::get().getESMStore()->get().search(enchantmentName); + if (!enchantment || enchantment->mEffects.mList.empty()) return result; - assert (enchantment->mEffects.mList.size()); - - const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().search( - enchantment->mEffects.mList.front().mEffectID); + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().search( + enchantment->mEffects.mList.front().mData.mEffectID); if (!magicEffect) return result; @@ -522,14 +542,14 @@ namespace MWWorld return result; } - void Class::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const + void Class::setBaseAISetting(const ESM::RefId&, MWMechanics::AiSetting setting, int value) const { - throw std::runtime_error ("class does not have creature stats"); + throw std::runtime_error("class does not have creature stats"); } - void Class::modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const + void Class::modifyBaseInventory(const ESM::RefId& actorId, const ESM::RefId& itemId, int amount) const { - throw std::runtime_error ("class does not have an inventory store"); + throw std::runtime_error("class does not have an inventory store"); } float Class::getWalkSpeed(const Ptr& /*ptr*/) const diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 1b3d4029e4c..0c6d55692f4 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -6,11 +6,17 @@ #include #include +#include #include -#include "ptr.hpp" #include "doorstate.hpp" -#include "../mwmechanics/creaturestats.hpp" +#include "ptr.hpp" + +#include "../mwmechanics/aisetting.hpp" +#include "../mwmechanics/damagesourcetype.hpp" + +#include +#include namespace ESM { @@ -31,6 +37,7 @@ namespace MWMechanics { class NpcStats; struct Movement; + class CreatureStats; } namespace MWGui @@ -53,322 +60,327 @@ namespace MWWorld /// \brief Base class for referenceable esm records class Class { - static std::map > sClasses; + const unsigned mType; - std::string mTypeName; + static std::map& getClasses(); + + protected: + explicit Class(unsigned type) + : mType(type) + { + } - // not implemented - Class (const Class&); - Class& operator= (const Class&); + std::unique_ptr defaultItemActivate(const Ptr& ptr, const Ptr& actor) const; + ///< Generate default action for activating inventory items - protected: + virtual Ptr copyToCellImpl(const ConstPtr& ptr, CellStore& cell) const; - Class(); + public: + virtual ~Class() = default; + Class(const Class&) = delete; + Class& operator=(const Class&) = delete; - std::shared_ptr defaultItemActivate(const Ptr &ptr, const Ptr &actor) const; - ///< Generate default action for activating inventory items + unsigned int getType() const { return mType; } - virtual Ptr copyToCellImpl(const ConstPtr &ptr, CellStore &cell) const; + virtual void insertObjectRendering( + const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const; + virtual void insertObject(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const; + ///< Add reference into a cell for rendering (default implementation: don't render anything). + virtual void insertObjectPhysics(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const; - public: + virtual std::string_view getName(const ConstPtr& ptr) const = 0; + ///< \return name or ID; can return an empty string. - virtual ~Class(); + virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) const; + ///< Adjust position to stand on ground. Must be called post model load + /// @param force do this even if the ptr is flying - const std::string& getTypeName() const { - return mTypeName; - } + virtual MWMechanics::CreatureStats& getCreatureStats(const Ptr& ptr) const; + ///< Return creature stats or throw an exception, if class does not have creature stats + /// (default implementation: throw an exception) - virtual void insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const; - virtual void insertObject(const Ptr& ptr, const std::string& mesh, MWPhysics::PhysicsSystem& physics) const; - ///< Add reference into a cell for rendering (default implementation: don't render anything). + virtual bool hasToolTip(const ConstPtr& ptr) const; + ///< @return true if this object has a tooltip when focused (default implementation: true) - virtual std::string getName (const ConstPtr& ptr) const = 0; - ///< \return name or ID; can return an empty string. + virtual MWGui::ToolTipInfo getToolTipInfo(const ConstPtr& ptr, int count) const; + ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. - virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) const; - ///< Adjust position to stand on ground. Must be called post model load - /// @param force do this even if the ptr is flying + virtual bool showsInInventory(const ConstPtr& ptr) const; + ///< Return whether ptr shows in inventory views. + /// Hidden items are not displayed and cannot be (re)moved by the user. + /// \return True if shown, false if hidden. - virtual MWMechanics::CreatureStats& getCreatureStats (const Ptr& ptr) const; - ///< Return creature stats or throw an exception, if class does not have creature stats - /// (default implementation: throw an exception) + virtual MWMechanics::NpcStats& getNpcStats(const Ptr& ptr) const; + ///< Return NPC stats or throw an exception, if class does not have NPC stats + /// (default implementation: throw an exception) - virtual bool hasToolTip (const ConstPtr& ptr) const; - ///< @return true if this object has a tooltip when focused (default implementation: true) + virtual bool hasItemHealth(const ConstPtr& ptr) const; + ///< \return Item health data available? (default implementation: false) - virtual MWGui::ToolTipInfo getToolTipInfo (const ConstPtr& ptr, int count) const; - ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. + virtual int getItemHealth(const ConstPtr& ptr) const; + ///< Return current item health or throw an exception if class does not have item health - virtual bool showsInInventory (const ConstPtr& ptr) const; - ///< Return whether ptr shows in inventory views. - /// Hidden items are not displayed and cannot be (re)moved by the user. - /// \return True if shown, false if hidden. + virtual float getItemNormalizedHealth(const ConstPtr& ptr) const; + ///< Return current item health re-scaled to maximum health - virtual MWMechanics::NpcStats& getNpcStats (const Ptr& ptr) const; - ///< Return NPC stats or throw an exception, if class does not have NPC stats - /// (default implementation: throw an exception) + virtual int getItemMaxHealth(const ConstPtr& ptr) const; + ///< Return item max health or throw an exception, if class does not have item health + /// (default implementation: throw an exception) - virtual bool hasItemHealth (const ConstPtr& ptr) const; - ///< \return Item health data available? (default implementation: false) + virtual bool evaluateHit(const Ptr& ptr, Ptr& victim, osg::Vec3f& hitPosition) const; + ///< Evaluate the victim of a melee hit produced by ptr in the current circumstances and return dice roll + ///< success. + /// (default implementation: throw an exception) - virtual int getItemHealth (const ConstPtr& ptr) const; - ///< Return current item health or throw an exception if class does not have item health + virtual void hit(const Ptr& ptr, float attackStrength, int type = -1, const Ptr& victim = Ptr(), + const osg::Vec3f& hitPosition = osg::Vec3f(), bool success = false) const; + ///< Execute a melee hit on the victim at hitPosition, using the current weapon. If the hit was successful, + ///< apply damage and process corresponding events. + /// \param attackStrength how long the attack was charged for, a value in 0-1 range. + /// \param type - type of attack, one of the MWMechanics::CreatureStats::AttackType + /// enums. ignored for creature attacks. + /// (default implementation: throw an exception) - virtual float getItemNormalizedHealth (const ConstPtr& ptr) const; - ///< Return current item health re-scaled to maximum health + virtual void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const; + ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is + /// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the + /// actor responsible for the attack. \a successful specifies if the hit is + /// successful or not. \a sourceType classifies the damage source. - virtual int getItemMaxHealth (const ConstPtr& ptr) const; - ///< Return item max health or throw an exception, if class does not have item health - /// (default implementation: throw an exception) + virtual std::unique_ptr activate(const Ptr& ptr, const Ptr& actor) const; + ///< Generate action for activation (default implementation: return a null action). - virtual void hit(const Ptr& ptr, float attackStrength, int type=-1) const; - ///< Execute a melee hit, using the current weapon. This will check the relevant skills - /// of the given attacker, and whoever is hit. - /// \param attackStrength how long the attack was charged for, a value in 0-1 range. - /// \param type - type of attack, one of the MWMechanics::CreatureStats::AttackType - /// enums. ignored for creature attacks. - /// (default implementation: throw an exception) + virtual std::unique_ptr use(const Ptr& ptr, bool force = false) const; + ///< Generate action for using via inventory menu (default implementation: return a + /// null action). - virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const; - ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is - /// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the - /// actor responsible for the attack, and \a successful specifies if the hit is - /// successful or not. + virtual ContainerStore& getContainerStore(const Ptr& ptr) const; + ///< Return container store or throw an exception, if class does not have a + /// container store (default implementation: throw an exception) - virtual void block (const Ptr& ptr) const; - ///< Play the appropriate sound for a blocked attack, depending on the currently equipped shield - /// (default implementation: throw an exception) + virtual InventoryStore& getInventoryStore(const Ptr& ptr) const; + ///< Return inventory store or throw an exception, if class does not have a + /// inventory store (default implementation: throw an exception) - virtual std::shared_ptr activate (const Ptr& ptr, const Ptr& actor) const; - ///< Generate action for activation (default implementation: return a null action). + virtual bool hasInventoryStore(const ConstPtr& ptr) const; + ///< Does this object have an inventory store, i.e. equipment slots? (default implementation: false) - virtual std::shared_ptr use (const Ptr& ptr, bool force=false) - const; - ///< Generate action for using via inventory menu (default implementation: return a - /// null action). + virtual bool canLock(const ConstPtr& ptr) const; - virtual ContainerStore& getContainerStore (const Ptr& ptr) const; - ///< Return container store or throw an exception, if class does not have a - /// container store (default implementation: throw an exception) + virtual void setRemainingUsageTime(const Ptr& ptr, float duration) const; + ///< Sets the remaining duration of the object, such as an equippable light + /// source. (default implementation: throw an exception) - virtual InventoryStore& getInventoryStore (const Ptr& ptr) const; - ///< Return inventory store or throw an exception, if class does not have a - /// inventory store (default implementation: throw an exception) + virtual float getRemainingUsageTime(const ConstPtr& ptr) const; + ///< Returns the remaining duration of the object, such as an equippable light + /// source. (default implementation: -1, i.e. infinite) - virtual bool hasInventoryStore (const Ptr& ptr) const; - ///< Does this object have an inventory store, i.e. equipment slots? (default implementation: false) + virtual ESM::RefId getScript(const ConstPtr& ptr) const; + ///< Return name of the script attached to ptr (default implementation: return an empty + /// string). - virtual bool canLock (const ConstPtr& ptr) const; + virtual float getWalkSpeed(const Ptr& ptr) const; + virtual float getRunSpeed(const Ptr& ptr) const; + virtual float getSwimSpeed(const Ptr& ptr) const; - virtual void setRemainingUsageTime (const Ptr& ptr, float duration) const; - ///< Sets the remaining duration of the object, such as an equippable light - /// source. (default implementation: throw an exception) + /// Return maximal movement speed for the current state. + virtual float getMaxSpeed(const Ptr& ptr) const; - virtual float getRemainingUsageTime (const ConstPtr& ptr) const; - ///< Returns the remaining duration of the object, such as an equippable light - /// source. (default implementation: -1, i.e. infinite) + /// Return current movement speed. + virtual float getCurrentSpeed(const Ptr& ptr) const; - virtual std::string getScript (const ConstPtr& ptr) const; - ///< Return name of the script attached to ptr (default implementation: return an empty - /// string). + virtual float getJump(const MWWorld::Ptr& ptr) const; + ///< Return jump velocity (not accounting for movement) - virtual float getWalkSpeed(const Ptr& ptr) const; - virtual float getRunSpeed(const Ptr& ptr) const; - virtual float getSwimSpeed(const Ptr& ptr) const; + virtual MWMechanics::Movement& getMovementSettings(const Ptr& ptr) const; + ///< Return desired movement. - /// Return maximal movement speed for the current state. - virtual float getMaxSpeed(const Ptr& ptr) const; + virtual osg::Vec3f getRotationVector(const Ptr& ptr) const; + ///< Return desired rotations, as euler angles. Sets getMovementSettings(ptr).mRotation to zero. - /// Return current movement speed. - virtual float getCurrentSpeed(const Ptr& ptr) const; + virtual std::pair, bool> getEquipmentSlots(const ConstPtr& ptr) const; + ///< \return first: Return IDs of the slot this object can be equipped in; second: can object + /// stay stacked when equipped? + /// + /// Default implementation: return (empty vector, false). - virtual float getJump(const MWWorld::Ptr &ptr) const; - ///< Return jump velocity (not accounting for movement) + virtual ESM::RefId getEquipmentSkill(const ConstPtr& ptr) const; + /// Return the index of the skill this item corresponds to when equipped. + /// (default implementation: return empty ref id) - virtual MWMechanics::Movement& getMovementSettings (const Ptr& ptr) const; - ///< Return desired movement. + virtual int getValue(const ConstPtr& ptr) const; + ///< Return trade value of the object. Throws an exception, if the object can't be traded. + /// (default implementation: throws an exception) - virtual osg::Vec3f getRotationVector (const Ptr& ptr) const; - ///< Return desired rotations, as euler angles. Sets getMovementSettings(ptr).mRotation to zero. + virtual float getCapacity(const MWWorld::Ptr& ptr) const; + ///< Return total weight that fits into the object. Throws an exception, if the object can't + /// hold other objects. + /// (default implementation: throws an exception) - virtual std::pair, bool> getEquipmentSlots (const ConstPtr& ptr) const; - ///< \return first: Return IDs of the slot this object can be equipped in; second: can object - /// stay stacked when equipped? - /// - /// Default implementation: return (empty vector, false). + virtual float getEncumbrance(const MWWorld::Ptr& ptr) const; + ///< Returns total weight of objects inside this object (including modifications from magic + /// effects). Throws an exception, if the object can't hold other objects. + /// (default implementation: throws an exception) - virtual int getEquipmentSkill (const ConstPtr& ptr) - const; - /// Return the index of the skill this item corresponds to when equipped or -1, if there is - /// no such skill. - /// (default implementation: return -1) + virtual float getNormalizedEncumbrance(const MWWorld::Ptr& ptr) const; + ///< Returns encumbrance re-scaled to capacity - virtual int getValue (const ConstPtr& ptr) const; - ///< Return trade value of the object. Throws an exception, if the object can't be traded. - /// (default implementation: throws an exception) + virtual bool consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const; + ///< Consume an item, e. g. a potion. - virtual float getCapacity (const MWWorld::Ptr& ptr) const; - ///< Return total weight that fits into the object. Throws an exception, if the object can't - /// hold other objects. - /// (default implementation: throws an exception) + virtual void skillUsageSucceeded( + const MWWorld::Ptr& ptr, ESM::RefId skill, int usageType, float extraFactor = 1.f) const; + ///< Inform actor \a ptr that a skill use has succeeded. + /// + /// (default implementations: throws an exception) - virtual float getEncumbrance (const MWWorld::Ptr& ptr) const; - ///< Returns total weight of objects inside this object (including modifications from magic - /// effects). Throws an exception, if the object can't hold other objects. - /// (default implementation: throws an exception) + virtual bool isEssential(const MWWorld::ConstPtr& ptr) const; + ///< Is \a ptr essential? (i.e. may losing \a ptr make the game unwinnable) + /// + /// (default implementation: return false) - virtual float getNormalizedEncumbrance (const MWWorld::Ptr& ptr) const; - ///< Returns encumbrance re-scaled to capacity + virtual const ESM::RefId& getUpSoundId(const ConstPtr& ptr) const; + ///< Return the up sound ID of \a ptr or throw an exception, if class does not support ID retrieval + /// (default implementation: throw an exception) - virtual bool apply (const MWWorld::Ptr& ptr, const std::string& id, - const MWWorld::Ptr& actor) const; - ///< Apply \a id on \a ptr. - /// \param actor Actor that is resposible for the ID being applied to \a ptr. - /// \return Any effect? - /// - /// (default implementation: ignore and return false) + virtual const ESM::RefId& getDownSoundId(const ConstPtr& ptr) const; + ///< Return the down sound ID of \a ptr or throw an exception, if class does not support ID retrieval + /// (default implementation: throw an exception) - virtual void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor=1.f) const; - ///< Inform actor \a ptr that a skill use has succeeded. - /// - /// (default implementations: throws an exception) + virtual ESM::RefId getSoundIdFromSndGen(const Ptr& ptr, std::string_view type) const; + ///< Returns the sound ID for \a ptr of the given soundgen \a type. - virtual bool isEssential (const MWWorld::ConstPtr& ptr) const; - ///< Is \a ptr essential? (i.e. may losing \a ptr make the game unwinnable) - /// - /// (default implementation: return false) + virtual float getArmorRating(const MWWorld::Ptr& ptr) const; + ///< @return combined armor rating of this actor - virtual std::string getUpSoundId (const ConstPtr& ptr) const; - ///< Return the up sound ID of \a ptr or throw an exception, if class does not support ID retrieval - /// (default implementation: throw an exception) + virtual const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const; + ///< Return name of inventory icon. - virtual std::string getDownSoundId (const ConstPtr& ptr) const; - ///< Return the down sound ID of \a ptr or throw an exception, if class does not support ID retrieval - /// (default implementation: throw an exception) + virtual ESM::RefId getEnchantment(const MWWorld::ConstPtr& ptr) const; + ///< @return the enchantment ID if the object is enchanted, otherwise an empty string + /// (default implementation: return empty string) - virtual std::string getSoundIdFromSndGen(const Ptr &ptr, const std::string &type) const; - ///< Returns the sound ID for \a ptr of the given soundgen \a type. + virtual int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const; + ///< @return the number of enchantment points available for possible enchanting - virtual float getArmorRating (const MWWorld::Ptr& ptr) const; - ///< @return combined armor rating of this actor + virtual void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const; + /// @param rendering Indicates if the scale to adjust is for the rendering mesh, or for the collision mesh - virtual std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const; - ///< Return name of inventory icon. + virtual bool canSell(const MWWorld::ConstPtr& item, int npcServices) const; + ///< Determine whether or not \a item can be sold to an npc with the given \a npcServices - virtual std::string getEnchantment (const MWWorld::ConstPtr& ptr) const; - ///< @return the enchantment ID if the object is enchanted, otherwise an empty string - /// (default implementation: return empty string) + virtual int getServices(const MWWorld::ConstPtr& actor) const; - virtual int getEnchantmentPoints (const MWWorld::ConstPtr& ptr) const; - ///< @return the number of enchantment points available for possible enchanting + virtual std::string_view getModel(const MWWorld::ConstPtr& ptr) const; - virtual void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const; - /// @param rendering Indicates if the scale to adjust is for the rendering mesh, or for the collision mesh + virtual std::string getCorrectedModel(const MWWorld::ConstPtr& ptr) const; - virtual bool canSell (const MWWorld::ConstPtr& item, int npcServices) const; - ///< Determine whether or not \a item can be sold to an npc with the given \a npcServices + virtual bool useAnim() const; + ///< Whether or not to use animated variant of model (default false) - virtual int getServices (const MWWorld::ConstPtr& actor) const; + virtual void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const; + ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: + ///< list getModel(). - virtual std::string getModel(const MWWorld::ConstPtr &ptr) const; + virtual const ESM::RefId& applyEnchantment( + const MWWorld::ConstPtr& ptr, const ESM::RefId& enchId, int enchCharge, const std::string& newName) const; + ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. - virtual bool useAnim() const; - ///< Whether or not to use animated variant of model (default false) + virtual std::pair canBeEquipped( + const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const; + ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon + ///< conflicts with that. + /// Second item in the pair specifies the error message - virtual void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const; - ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel(). + virtual float getWeight(const MWWorld::ConstPtr& ptr) const; - virtual std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const; - ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. + virtual bool isPersistent(const MWWorld::ConstPtr& ptr) const; - virtual std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const; - ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. - /// Second item in the pair specifies the error message + virtual bool isKey(const MWWorld::ConstPtr& ptr) const { return false; } - virtual float getWeight (const MWWorld::ConstPtr& ptr) const; + virtual bool isGold(const MWWorld::ConstPtr& ptr) const { return false; } - virtual bool isPersistent (const MWWorld::ConstPtr& ptr) const; + virtual bool isSoulGem(const MWWorld::ConstPtr& ptr) const { return false; } - virtual bool isKey (const MWWorld::ConstPtr& ptr) const { return false; } + virtual bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const { return true; } + ///< Return whether this class of object can be activated with telekinesis - virtual bool isGold(const MWWorld::ConstPtr& ptr) const { return false; } + /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) + virtual int getBloodTexture(const MWWorld::ConstPtr& ptr) const; - virtual bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const { return true; } - ///< Return whether this class of object can be activated with telekinesis + virtual Ptr copyToCell(const ConstPtr& ptr, CellStore& cell, int count) const; - /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) - virtual int getBloodTexture (const MWWorld::ConstPtr& ptr) const; + // Similar to `copyToCell`, but preserves RefNum and moves LuaScripts. + // The original is expected to be removed after calling this function, + // but this function itself doesn't remove the original. + virtual Ptr moveToCell(const Ptr& ptr, CellStore& cell) const; + Ptr moveToCell(const Ptr& ptr, CellStore& cell, const ESM::Position& pos) const; - virtual Ptr copyToCell(const ConstPtr &ptr, CellStore &cell, int count) const; + Ptr copyToCell(const ConstPtr& ptr, CellStore& cell, const ESM::Position& pos, int count) const; - virtual Ptr copyToCell(const ConstPtr &ptr, CellStore &cell, const ESM::Position &pos, int count) const; + virtual bool isActivator() const { return false; } - virtual bool isActivator() const { - return false; - } + virtual bool isActor() const { return false; } - virtual bool isActor() const { - return false; - } + virtual bool isNpc() const { return false; } - virtual bool isNpc() const { - return false; - } + virtual bool isDoor() const { return false; } - virtual bool isDoor() const { - return false; - } + // True if it is an item that can be picked up. + virtual bool isItem(const MWWorld::ConstPtr& ptr) const { return false; } - virtual bool isBipedal(const MWWorld::ConstPtr& ptr) const; - virtual bool canFly(const MWWorld::ConstPtr& ptr) const; - virtual bool canSwim(const MWWorld::ConstPtr& ptr) const; - virtual bool canWalk(const MWWorld::ConstPtr& ptr) const; - bool isPureWaterCreature(const MWWorld::ConstPtr& ptr) const; - bool isPureFlyingCreature(const MWWorld::ConstPtr& ptr) const; - bool isPureLandCreature(const MWWorld::Ptr& ptr) const; - bool isMobile(const MWWorld::Ptr& ptr) const; + virtual bool isBipedal(const MWWorld::ConstPtr& ptr) const; + virtual bool canFly(const MWWorld::ConstPtr& ptr) const; + virtual bool canSwim(const MWWorld::ConstPtr& ptr) const; + virtual bool canWalk(const MWWorld::ConstPtr& ptr) const; + bool isPureWaterCreature(const MWWorld::ConstPtr& ptr) const; + bool isPureFlyingCreature(const MWWorld::ConstPtr& ptr) const; + bool isPureLandCreature(const MWWorld::Ptr& ptr) const; + bool isMobile(const MWWorld::Ptr& ptr) const; - virtual float getSkill(const MWWorld::Ptr& ptr, int skill) const; + virtual float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const; - virtual void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) - const; - ///< Read additional state from \a state into \a ptr. + virtual void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const; + ///< Read additional state from \a state into \a ptr. - virtual void writeAdditionalState (const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) - const; - ///< Write additional state from \a ptr into \a state. + virtual void writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const; + ///< Write additional state from \a ptr into \a state. - static const Class& get (const std::string& key); - ///< If there is no class for this \a key, an exception is thrown. + static const Class& get(unsigned int key); + ///< If there is no class for this \a key, an exception is thrown. - static void registerClass (const std::string& key, std::shared_ptr instance); + static void registerClass(Class& instance); - virtual int getBaseGold(const MWWorld::ConstPtr& ptr) const; + virtual int getBaseGold(const MWWorld::ConstPtr& ptr) const; - virtual bool isClass(const MWWorld::ConstPtr& ptr, const std::string &className) const; + virtual bool isClass(const MWWorld::ConstPtr& ptr, std::string_view className) const; - virtual DoorState getDoorState (const MWWorld::ConstPtr &ptr) const; - /// This does not actually cause the door to move. Use World::activateDoor instead. - virtual void setDoorState (const MWWorld::Ptr &ptr, DoorState state) const; + virtual DoorState getDoorState(const MWWorld::ConstPtr& ptr) const; + /// This does not actually cause the door to move. Use World::activateDoor instead. + virtual void setDoorState(const MWWorld::Ptr& ptr, DoorState state) const; - virtual void respawn (const MWWorld::Ptr& ptr) const {} + virtual void respawn(const MWWorld::Ptr& ptr) const {} - /// Returns sound id - virtual std::string getSound(const MWWorld::ConstPtr& ptr) const; + /// Returns sound id + virtual ESM::RefId getSound(const MWWorld::ConstPtr& ptr) const; - virtual int getBaseFightRating (const MWWorld::ConstPtr& ptr) const; + virtual int getBaseFightRating(const MWWorld::ConstPtr& ptr) const; - virtual std::string getPrimaryFaction (const MWWorld::ConstPtr& ptr) const; - virtual int getPrimaryFactionRank (const MWWorld::ConstPtr& ptr) const; + virtual ESM::RefId getPrimaryFaction(const MWWorld::ConstPtr& ptr) const; + virtual int getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const; - /// Get the effective armor rating, factoring in the actor's skills, for the given armor. - virtual float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const; + /// Get the effective armor rating, factoring in the actor's skills, for the given armor. + virtual float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const; - virtual osg::Vec4f getEnchantmentColor(const MWWorld::ConstPtr& item) const; + virtual osg::Vec4f getEnchantmentColor(const MWWorld::ConstPtr& item) const; - virtual void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const; + virtual void setBaseAISetting(const ESM::RefId& id, MWMechanics::AiSetting setting, int value) const; - virtual void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) const; + virtual void modifyBaseInventory(const ESM::RefId& actorId, const ESM::RefId& itemId, int amount) const; }; } diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index cced17688d7..f48f73f48a2 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -1,35 +1,41 @@ #include "containerstore.hpp" +#include "inventorystore.hpp" #include -#include #include #include -#include +#include +#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" -#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/levelledlist.hpp" -#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/recharge.hpp" +#include "../mwmechanics/spellutil.hpp" -#include "manualref.hpp" -#include "refdata.hpp" #include "class.hpp" +#include "esmstore.hpp" #include "localscripts.hpp" +#include "manualref.hpp" #include "player.hpp" +#include "refdata.hpp" +#include "worldmodel.hpp" namespace { void addScripts(MWWorld::ContainerStore& store, MWWorld::CellStore* cell) { auto& scripts = MWBase::Environment::get().getWorld()->getLocalScripts(); - for(const auto&& ptr : store) + for (const auto&& ptr : store) { - const std::string& script = ptr.getClass().getScript(ptr); - if(!script.empty()) + const auto& script = ptr.getClass().getScript(ptr); + if (!script.empty()) { MWWorld::Ptr item = ptr; item.mCell = cell; @@ -38,33 +44,31 @@ namespace } } - template - float getTotalWeight (const MWWorld::CellRefList& cellRefList) + template + float getTotalWeight(const MWWorld::CellRefList& cellRefList) { float sum = 0; - for (const auto& iter : cellRefList.mList) + for (const MWWorld::LiveCellRef& liveCellRef : cellRefList.mList) { - if (iter.mData.getCount()>0) - sum += iter.mData.getCount()*iter.mBase->mData.mWeight; + if (const int count = liveCellRef.mRef.getCount(); count > 0) + sum += count * liveCellRef.mBase->mData.mWeight; } return sum; } - template - MWWorld::Ptr searchId (MWWorld::CellRefList& list, const std::string& id, - MWWorld::ContainerStore *store) + template + MWWorld::Ptr searchId(MWWorld::CellRefList& list, const ESM::RefId& id, MWWorld::ContainerStore* store) { store->resolve(); - std::string id2 = Misc::StringUtils::lowerCase (id); - for (auto& iter : list.mList) + for (MWWorld::LiveCellRef& liveCellRef : list.mList) { - if (Misc::StringUtils::ciEqual(iter.mBase->mId, id2) && iter.mData.getCount()) + if ((liveCellRef.mBase->mId == id) && liveCellRef.mRef.getCount()) { - MWWorld::Ptr ptr (&iter, nullptr); - ptr.setContainerStore (store); + MWWorld::Ptr ptr(&liveCellRef, nullptr); + ptr.setContainerStore(store); return ptr; } } @@ -79,88 +83,91 @@ MWWorld::ResolutionListener::~ResolutionListener() { mStore.unresolve(); } - catch(const std::exception& e) + catch (const std::exception& e) { Log(Debug::Error) << "Failed to clear temporary container contents: " << e.what(); } } -template -MWWorld::ContainerStoreIterator MWWorld::ContainerStore::getState (CellRefList& collection, - const ESM::ObjectState& state) +template +MWWorld::ContainerStoreIterator MWWorld::ContainerStore::getState( + CellRefList& collection, const ESM::ObjectState& state) { - if (!LiveCellRef::checkState (state)) - return ContainerStoreIterator (this); // not valid anymore with current content files -> skip + if (!LiveCellRef::checkState(state)) + return ContainerStoreIterator(this); // not valid anymore with current content files -> skip - const T *record = MWBase::Environment::get().getWorld()->getStore(). - get().search (state.mRef.mRefID); + const T* record = MWBase::Environment::get().getESMStore()->get().search(state.mRef.mRefID); if (!record) - return ContainerStoreIterator (this); + return ContainerStoreIterator(this); - LiveCellRef ref (record); - ref.load (state); - collection.mList.push_back (ref); + LiveCellRef ref(record); + ref.load(state); + collection.mList.push_back(ref); + auto it = ContainerStoreIterator(this, --collection.mList.end()); + MWBase::Environment::get().getWorldModel()->registerPtr(*it); - return ContainerStoreIterator (this, --collection.mList.end()); + return it; } -void MWWorld::ContainerStore::storeEquipmentState(const MWWorld::LiveCellRefBase &ref, int index, ESM::InventoryState &inventory) const +void MWWorld::ContainerStore::storeEquipmentState( + const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const { } -void MWWorld::ContainerStore::readEquipmentState(const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState &inventory) +void MWWorld::ContainerStore::readEquipmentState( + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory) { } -template -void MWWorld::ContainerStore::storeState (const LiveCellRef& ref, ESM::ObjectState& state) const +template +void MWWorld::ContainerStore::storeState(const LiveCellRef& ref, ESM::ObjectState& state) const { - ref.save (state); + ref.save(state); } -template -void MWWorld::ContainerStore::storeStates (const CellRefList& collection, - ESM::InventoryState& inventory, int& index, bool equipable) const +template +void MWWorld::ContainerStore::storeStates( + const CellRefList& collection, ESM::InventoryState& inventory, size_t& index, bool equipable) const { - for (const auto& iter : collection.mList) + for (const LiveCellRef& liveCellRef : collection.mList) { - if (iter.mData.getCount() == 0) + if (liveCellRef.mRef.getCount() == 0) continue; ESM::ObjectState state; - storeState (iter, state); + storeState(liveCellRef, state); if (equipable) - storeEquipmentState(iter, index, inventory); - inventory.mItems.push_back (state); + storeEquipmentState(liveCellRef, index, inventory); + inventory.mItems.push_back(std::move(state)); ++index; } } -const std::string MWWorld::ContainerStore::sGoldId = "gold_001"; +const ESM::RefId MWWorld::ContainerStore::sGoldId = ESM::RefId::stringRefId("gold_001"); MWWorld::ContainerStore::ContainerStore() : mListener(nullptr) , mRechargingItemsUpToDate(false) - , mCachedWeight (0) - , mWeightUpToDate (false) + , mCachedWeight(0) + , mWeightUpToDate(false) , mModified(false) , mResolved(false) , mSeed() - , mPtr() {} - -MWWorld::ContainerStore::~ContainerStore() {} + , mPtr() +{ +} -MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::cbegin (int mask) const +MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::cbegin(int mask) const { - return ConstContainerStoreIterator (mask, this); + return ConstContainerStoreIterator(mask, this); } MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::cend() const { - return ConstContainerStoreIterator (this); + return ConstContainerStoreIterator(this); } -MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::begin (int mask) const +MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::begin(int mask) const { return cbegin(mask); } @@ -170,47 +177,62 @@ MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::end() const return cend(); } -MWWorld::ContainerStoreIterator MWWorld::ContainerStore::begin (int mask) +MWWorld::ContainerStoreIterator MWWorld::ContainerStore::begin(int mask) { - return ContainerStoreIterator (mask, this); + return ContainerStoreIterator(mask, this); } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::end() { - return ContainerStoreIterator (this); + return ContainerStoreIterator(this); } -int MWWorld::ContainerStore::count(const std::string &id) const +int MWWorld::ContainerStore::count(const ESM::RefId& id) const { - int total=0; + int total = 0; for (const auto&& iter : *this) - if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefId(), id)) - total += iter.getRefData().getCount(); + if (iter.getCellRef().getRefId() == id) + total += iter.getCellRef().getCount(); return total; } +void MWWorld::ContainerStore::updateRefNums() +{ + for (const auto& iter : *this) + { + iter.getCellRef().unsetRefNum(); + iter.getRefData().setLuaScripts(nullptr); + MWBase::Environment::get().getWorldModel()->registerPtr(iter); + } +} + MWWorld::ContainerStoreListener* MWWorld::ContainerStore::getContListener() const { return mListener; } - void MWWorld::ContainerStore::setContListener(MWWorld::ContainerStoreListener* listener) { mListener = listener; } -MWWorld::ContainerStoreIterator MWWorld::ContainerStore::unstack(const Ptr &ptr, const Ptr& container, int count) +MWWorld::ContainerStoreIterator MWWorld::ContainerStore::unstack(const Ptr& ptr, int count) { resolve(); - if (ptr.getRefData().getCount() <= count) + if (ptr.getCellRef().getCount() <= count) return end(); - MWWorld::ContainerStoreIterator it = addNewStack(ptr, subtractItems(ptr.getRefData().getCount(false), count)); - const std::string script = it->getClass().getScript(*it); + MWWorld::ContainerStoreIterator it = addNewStack(ptr, subtractItems(ptr.getCellRef().getCount(false), count)); + + MWWorld::Ptr newPtr = *it; + newPtr.getCellRef().unsetRefNum(); + newPtr.getRefData().setLuaScripts(nullptr); + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); + + const ESM::RefId& script = it->getClass().getScript(*it); if (!script.empty()) MWBase::Environment::get().getWorld()->getLocalScripts().add(script, *it); - remove(ptr, ptr.getRefData().getCount()-count, container); + remove(ptr, ptr.getCellRef().getCount() - count); return it; } @@ -219,7 +241,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::restack(const MWWorld:: { resolve(); MWWorld::ContainerStoreIterator retval = end(); - for (MWWorld::ContainerStoreIterator iter (begin()); iter != end(); ++iter) + for (MWWorld::ContainerStoreIterator iter(begin()); iter != end(); ++iter) { if (item == *iter) { @@ -231,12 +253,13 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::restack(const MWWorld:: if (retval == end()) throw std::runtime_error("item is not from this container"); - for (MWWorld::ContainerStoreIterator iter (begin()); iter != end(); ++iter) + for (MWWorld::ContainerStoreIterator iter(begin()); iter != end(); ++iter) { if (stacks(*iter, item)) { - iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), item.getRefData().getCount(false))); - item.getRefData().setCount(0); + iter->getCellRef().setCount( + addItems(iter->getCellRef().getCount(false), item.getCellRef().getCount(false))); + item.getCellRef().setCount(0); retval = iter; break; } @@ -249,17 +272,19 @@ bool MWWorld::ContainerStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const MWWorld::Class& cls1 = ptr1.getClass(); const MWWorld::Class& cls2 = ptr2.getClass(); - if (!Misc::StringUtils::ciEqual(ptr1.getCellRef().getRefId(), ptr2.getCellRef().getRefId())) + if (!(ptr1.getCellRef().getRefId() == ptr2.getCellRef().getRefId())) return false; // If it has an enchantment, don't stack when some of the charge is already used if (!ptr1.getClass().getEnchantment(ptr1).empty()) { - const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find( - ptr1.getClass().getEnchantment(ptr1)); - float maxCharge = static_cast(enchantment->mData.mCharge); - float enchantCharge1 = ptr1.getCellRef().getEnchantmentCharge() == -1 ? maxCharge : ptr1.getCellRef().getEnchantmentCharge(); - float enchantCharge2 = ptr2.getCellRef().getEnchantmentCharge() == -1 ? maxCharge : ptr2.getCellRef().getEnchantmentCharge(); + const ESM::Enchantment* enchantment = MWBase::Environment::get().getESMStore()->get().find( + ptr1.getClass().getEnchantment(ptr1)); + const float maxCharge = static_cast(MWMechanics::getEnchantmentCharge(*enchantment)); + float enchantCharge1 + = ptr1.getCellRef().getEnchantmentCharge() == -1 ? maxCharge : ptr1.getCellRef().getEnchantmentCharge(); + float enchantCharge2 + = ptr2.getCellRef().getEnchantmentCharge() == -1 ? maxCharge : ptr2.getCellRef().getEnchantmentCharge(); if (enchantCharge1 != maxCharge || enchantCharge2 != maxCharge) return false; } @@ -274,28 +299,32 @@ bool MWWorld::ContainerStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) && cls2.getScript(ptr2).empty() // item that is already partly used up never stacks - && (!cls1.hasItemHealth(ptr1) || ( - cls1.getItemHealth(ptr1) == cls1.getItemMaxHealth(ptr1) - && cls2.getItemHealth(ptr2) == cls2.getItemMaxHealth(ptr2))); + && (!cls1.hasItemHealth(ptr1) + || (cls1.getItemHealth(ptr1) == cls1.getItemMaxHealth(ptr1) + && cls2.getItemHealth(ptr2) == cls2.getItemMaxHealth(ptr2))); } -MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add(const std::string &id, int count, const Ptr &actorPtr) +MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add(const ESM::RefId& id, int count, bool allowAutoEquip) { - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), id, count); - return add(ref.getPtr(), count, actorPtr); + MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), id, count); + return add(ref.getPtr(), count, allowAutoEquip); } -MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr, int count, const Ptr& actorPtr, bool /*allowAutoEquip*/, bool resolve) +MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add( + const Ptr& itemPtr, int count, bool /*allowAutoEquip*/, bool resolve) { - Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWWorld::ContainerStoreIterator it = addImp(itemPtr, count, resolve); + itemPtr.getRefData().setLuaScripts(nullptr); // clear Lua scripts on the original (removed) item. // The copy of the original item we just made MWWorld::Ptr item = *it; + MWBase::Environment::get().getWorldModel()->registerPtr(item); // we may have copied an item from the world, so reset a few things first - item.getRefData().setBaseNode(nullptr); // Especially important, otherwise scripts on the item could think that it's actually in a cell + item.getRefData().setBaseNode( + nullptr); // Especially important, otherwise scripts on the item could think that it's actually in a cell ESM::Position pos; pos.rot[0] = 0; pos.rot[1] = 0; @@ -306,19 +335,16 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr item.getCellRef().setPosition(pos); // We do not need to store owners for items in container stores - we do not use it anyway. - item.getCellRef().setOwner(""); + item.getCellRef().setOwner(ESM::RefId()); item.getCellRef().resetGlobalVariable(); - item.getCellRef().setFaction(""); + item.getCellRef().setFaction(ESM::RefId()); item.getCellRef().setFactionRank(-2); - // must reset the RefNum on the copied item, so that the RefNum on the original item stays unique - // maybe we should do this in the copy constructor instead? - item.getCellRef().unsetRefNum(); // destroy link to content file - - std::string script = item.getClass().getScript(item); + const ESM::RefId& script = item.getClass().getScript(item); if (!script.empty()) { - if (actorPtr == player) + const Ptr& contPtr = getPtr(); + if (contPtr == player) { // Items in player's inventory have cell set to 0, so their scripts will never be removed item.mCell = nullptr; @@ -327,7 +353,8 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr { // Set mCell to the cell of the container/actor, so that the scripts are removed properly when // the cell of the container/actor goes inactive - item.mCell = actorPtr.getCell(); + if (!contPtr.isEmpty()) + item.mCell = contPtr.getCell(); } item.mContainerStore = this; @@ -336,53 +363,60 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr // Set OnPCAdd special variable, if it is declared // Make sure to do this *after* we have added the script to LocalScripts - if (actorPtr == player) + if (contPtr == player) item.getRefData().getLocals().setVarByInt(script, "onpcadd", 1); } // we should not fire event for InventoryStore yet - it has some custom logic - if (mListener && !actorPtr.getClass().hasInventoryStore(actorPtr)) + if (mListener && typeid(*this) == typeid(ContainerStore)) mListener->itemAdded(item, count); return it; } -MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addImp (const Ptr& ptr, int count, bool markModified) +MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addImp(const Ptr& ptr, int count, bool markModified) { - if(markModified) + if (markModified) resolve(); int type = getType(ptr); - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); // gold needs special handling: when it is inserted into a container, the base object automatically becomes Gold_001 - // this ensures that gold piles of different sizes stack with each other (also, several scripts rely on Gold_001 for detecting player gold) - if(ptr.getClass().isGold(ptr)) + // this ensures that gold piles of different sizes stack with each other (also, several scripts rely on Gold_001 for + // detecting player gold) + // Note that adding 1 gold_100 is equivalent to adding 1 gold_001. Morrowind.exe resolves gold in leveled lists to + // gold_001 and TESCS disallows adding gold other than gold_001 to inventories. If a content file defines a + // container containing gold_100 anyway, the item is not turned to gold_001 until the player puts it down in the + // world and picks it up again. We just turn it into gold_001 here and ignore that oddity. + if (ptr.getClass().isGold(ptr)) { - int realCount = count * ptr.getClass().getValue(ptr); - - for (MWWorld::ContainerStoreIterator iter (begin(type)); iter!=end(); ++iter) + for (MWWorld::ContainerStoreIterator iter(begin(type)); iter != end(); ++iter) { - if (Misc::StringUtils::ciEqual((*iter).getCellRef().getRefId(), MWWorld::ContainerStore::sGoldId)) + if (iter->getCellRef().getRefId() == MWWorld::ContainerStore::sGoldId) { - iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), realCount)); + iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); flagAsModified(); return iter; } } - MWWorld::ManualRef ref(esmStore, MWWorld::ContainerStore::sGoldId, realCount); - return addNewStack(ref.getPtr(), realCount); + MWWorld::ManualRef ref(esmStore, MWWorld::ContainerStore::sGoldId, count); + return addNewStack(ref.getPtr(), count); } // determine whether to stack or not - for (MWWorld::ContainerStoreIterator iter (begin(type)); iter!=end(); ++iter) + for (MWWorld::ContainerStoreIterator iter(begin(type)); iter != end(); ++iter) { + // Don't stack with equipped items + if (auto* inventoryStore = dynamic_cast(this)) + if (inventoryStore->isEquipped(*iter)) + continue; + if (stacks(*iter, ptr)) { // stack - iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), count)); + iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); flagAsModified(); return iter; @@ -392,27 +426,63 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addImp (const Ptr& ptr, return addNewStack(ptr, count); } -MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addNewStack (const ConstPtr& ptr, int count) +MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addNewStack(const ConstPtr& ptr, int count) { ContainerStoreIterator it = begin(); switch (getType(ptr)) { - case Type_Potion: potions.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --potions.mList.end()); break; - case Type_Apparatus: appas.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --appas.mList.end()); break; - case Type_Armor: armors.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --armors.mList.end()); break; - case Type_Book: books.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --books.mList.end()); break; - case Type_Clothing: clothes.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --clothes.mList.end()); break; - case Type_Ingredient: ingreds.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --ingreds.mList.end()); break; - case Type_Light: lights.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --lights.mList.end()); break; - case Type_Lockpick: lockpicks.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --lockpicks.mList.end()); break; - case Type_Miscellaneous: miscItems.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --miscItems.mList.end()); break; - case Type_Probe: probes.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --probes.mList.end()); break; - case Type_Repair: repairs.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --repairs.mList.end()); break; - case Type_Weapon: weapons.mList.push_back (*ptr.get()); it = ContainerStoreIterator(this, --weapons.mList.end()); break; + case Type_Potion: + potions.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --potions.mList.end()); + break; + case Type_Apparatus: + appas.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --appas.mList.end()); + break; + case Type_Armor: + armors.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --armors.mList.end()); + break; + case Type_Book: + books.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --books.mList.end()); + break; + case Type_Clothing: + clothes.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --clothes.mList.end()); + break; + case Type_Ingredient: + ingreds.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --ingreds.mList.end()); + break; + case Type_Light: + lights.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --lights.mList.end()); + break; + case Type_Lockpick: + lockpicks.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --lockpicks.mList.end()); + break; + case Type_Miscellaneous: + miscItems.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --miscItems.mList.end()); + break; + case Type_Probe: + probes.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --probes.mList.end()); + break; + case Type_Repair: + repairs.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --repairs.mList.end()); + break; + case Type_Weapon: + weapons.mList.push_back(*ptr.get()); + it = ContainerStoreIterator(this, --weapons.mList.end()); + break; } - it->getRefData().setCount(count); + it->getCellRef().setCount(count); flagAsModified(); return it; @@ -441,32 +511,34 @@ void MWWorld::ContainerStore::updateRechargingItems() mRechargingItems.clear(); for (ContainerStoreIterator it = begin(); it != end(); ++it) { - const std::string& enchantmentId = it->getClass().getEnchantment(*it); + const auto& enchantmentId = it->getClass().getEnchantment(*it); if (!enchantmentId.empty()) { - const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().search(enchantmentId); + const ESM::Enchantment* enchantment + = MWBase::Environment::get().getESMStore()->get().search(enchantmentId); if (!enchantment) { - Log(Debug::Warning) << "Warning: Can't find enchantment '" << enchantmentId << "' on item " << it->getCellRef().getRefId(); + Log(Debug::Warning) << "Warning: Can't find enchantment '" << enchantmentId << "' on item " + << it->getCellRef().getRefId(); continue; } if (enchantment->mData.mType == ESM::Enchantment::WhenUsed - || enchantment->mData.mType == ESM::Enchantment::WhenStrikes) - mRechargingItems.emplace_back(it, static_cast(enchantment->mData.mCharge)); + || enchantment->mData.mType == ESM::Enchantment::WhenStrikes) + mRechargingItems.emplace_back(it, static_cast(MWMechanics::getEnchantmentCharge(*enchantment))); } } } -int MWWorld::ContainerStore::remove(const std::string& itemId, int count, const Ptr& actor, bool equipReplacement, bool resolveFirst) +int MWWorld::ContainerStore::remove(const ESM::RefId& itemId, int count, bool equipReplacement, bool resolveFirst) { - if(resolveFirst) + if (resolveFirst) resolve(); int toRemove = count; for (ContainerStoreIterator iter(begin()); iter != end() && toRemove > 0; ++iter) - if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), itemId)) - toRemove -= remove(*iter, toRemove, actor, equipReplacement, resolveFirst); + if (iter->getCellRef().getRefId() == itemId) + toRemove -= remove(*iter, toRemove, equipReplacement, resolveFirst); flagAsModified(); @@ -485,14 +557,14 @@ bool MWWorld::ContainerStore::hasVisibleItems() const return false; } -int MWWorld::ContainerStore::remove(const Ptr& item, int count, const Ptr& actor, bool equipReplacement, bool resolveFirst) +int MWWorld::ContainerStore::remove(const Ptr& item, int count, bool equipReplacement, bool resolveFirst) { assert(this == item.getContainerStore()); - if(resolveFirst) + if (resolveFirst) resolve(); int toRemove = count; - RefData& itemRef = item.getRefData(); + CellRef& itemRef = item.getCellRef(); if (itemRef.getCount() <= toRemove) { @@ -508,54 +580,53 @@ int MWWorld::ContainerStore::remove(const Ptr& item, int count, const Ptr& actor flagAsModified(); // we should not fire event for InventoryStore yet - it has some custom logic - if (mListener && !actor.getClass().hasInventoryStore(actor)) + if (mListener && typeid(*this) == typeid(ContainerStore)) mListener->itemRemoved(item, count - toRemove); // number of removed items return count - toRemove; } -void MWWorld::ContainerStore::fill (const ESM::InventoryList& items, const std::string& owner, Misc::Rng::Seed& seed) +void MWWorld::ContainerStore::fill(const ESM::InventoryList& items, const ESM::RefId& owner, Misc::Rng::Generator& prng) { for (const ESM::ContItem& iter : items.mList) { - std::string id = Misc::StringUtils::lowerCase(iter.mItem); - addInitialItem(id, owner, iter.mCount, &seed); + addInitialItem(iter.mItem, owner, iter.mCount, &prng); } flagAsModified(); mResolved = true; } -void MWWorld::ContainerStore::fillNonRandom (const ESM::InventoryList& items, const std::string& owner, unsigned int seed) +void MWWorld::ContainerStore::fillNonRandom(const ESM::InventoryList& items, const ESM::RefId& owner, unsigned int seed) { mSeed = seed; for (const ESM::ContItem& iter : items.mList) { - std::string id = Misc::StringUtils::lowerCase(iter.mItem); - addInitialItem(id, owner, iter.mCount, nullptr); + addInitialItem(iter.mItem, owner, iter.mCount, nullptr); } flagAsModified(); mResolved = false; } -void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std::string& owner, int count, - Misc::Rng::Seed* seed, bool topLevel) +void MWWorld::ContainerStore::addInitialItem( + const ESM::RefId& id, const ESM::RefId& owner, int count, Misc::Rng::Generator* prng, bool topLevel) { - if (count == 0) return; //Don't restock with nothing. + if (count == 0) + return; // Don't restock with nothing. try { - ManualRef ref (MWBase::Environment::get().getWorld()->getStore(), id, count); + ManualRef ref(*MWBase::Environment::get().getESMStore(), id, count); if (ref.getPtr().getClass().getScript(ref.getPtr()).empty()) { - addInitialItemImp(ref.getPtr(), owner, count, seed, topLevel); + addInitialItemImp(ref.getPtr(), owner, count, prng, topLevel); } else { // Adding just one item per time to make sure there isn't a stack of scripted items for (int i = 0; i < std::abs(count); i++) - addInitialItemImp(ref.getPtr(), owner, count < 0 ? -1 : 1, seed, topLevel); + addInitialItemImp(ref.getPtr(), owner, count < 0 ? -1 : 1, prng, topLevel); } } catch (const std::exception& e) @@ -564,40 +635,41 @@ void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std:: } } -void MWWorld::ContainerStore::addInitialItemImp(const MWWorld::Ptr& ptr, const std::string& owner, int count, - Misc::Rng::Seed* seed, bool topLevel) +void MWWorld::ContainerStore::addInitialItemImp( + const MWWorld::Ptr& ptr, const ESM::RefId& owner, int count, Misc::Rng::Generator* prng, bool topLevel) { - if (ptr.getTypeName()==typeid (ESM::ItemLevList).name()) + if (ptr.getType() == ESM::ItemLevList::sRecordId) { - if(!seed) + if (!prng) return; const ESM::ItemLevList* levItemList = ptr.get()->mBase; if (topLevel && std::abs(count) > 1 && levItemList->mFlags & ESM::ItemLevList::Each) { - for (int i=0; i 0 ? 1 : -1, seed, true); + for (int i = 0; i < std::abs(count); ++i) + addInitialItem(ptr.getCellRef().getRefId(), owner, count > 0 ? 1 : -1, prng, true); return; } else { - std::string itemId = MWMechanics::getLevelledItem(ptr.get()->mBase, false, *seed); + const auto& itemId = MWMechanics::getLevelledItem(ptr.get()->mBase, false, *prng); if (itemId.empty()) return; - addInitialItem(itemId, owner, count, seed, false); + addInitialItem(itemId, owner, count, prng, false); } } else { ptr.getCellRef().setOwner(owner); - addImp (ptr, count, false); + MWWorld::ContainerStoreIterator it = addImp(ptr, count, false); + MWBase::Environment::get().getWorldModel()->registerPtr(*it); } } void MWWorld::ContainerStore::clear() { for (auto&& iter : *this) - iter.getRefData().setCount (0); + iter.getCellRef().setCount(0); flagAsModified(); mModified = true; @@ -616,36 +688,38 @@ bool MWWorld::ContainerStore::isResolved() const void MWWorld::ContainerStore::resolve() { - if(!mResolved && !mPtr.isEmpty()) + const Ptr& container = getPtr(); + if (!mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { - for(const auto&& ptr : *this) - ptr.getRefData().setCount(0); - Misc::Rng::Seed seed{mSeed}; - fill(mPtr.get()->mBase->mInventory, "", seed); - addScripts(*this, mPtr.mCell); + for (const auto&& ptr : *this) + ptr.getCellRef().setCount(0); + Misc::Rng::Generator prng{ mSeed }; + fill(container.get()->mBase->mInventory, ESM::RefId(), prng); + addScripts(*this, container.mCell); } mModified = true; } MWWorld::ResolutionHandle MWWorld::ContainerStore::resolveTemporarily() { - if(mModified) + if (mModified) return {}; std::shared_ptr listener = mResolutionListener.lock(); - if(!listener) + if (!listener) { listener = std::make_shared(*this); mResolutionListener = listener; } - if(!mResolved && !mPtr.isEmpty()) + const Ptr& container = getPtr(); + if (!mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { - for(const auto&& ptr : *this) - ptr.getRefData().setCount(0); - Misc::Rng::Seed seed{mSeed}; - fill(mPtr.get()->mBase->mInventory, "", seed); - addScripts(*this, mPtr.mCell); + for (const auto&& ptr : *this) + ptr.getCellRef().setCount(0); + Misc::Rng::Generator prng{ mSeed }; + fill(container.get()->mBase->mInventory, ESM::RefId(), prng); + addScripts(*this, container.mCell); } - return {listener}; + return { std::move(listener) }; } void MWWorld::ContainerStore::unresolve() @@ -653,12 +727,13 @@ void MWWorld::ContainerStore::unresolve() if (mModified) return; - if (mResolved && !mPtr.isEmpty()) + const Ptr& container = getPtr(); + if (mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { - for(const auto&& ptr : *this) - ptr.getRefData().setCount(0); - fillNonRandom(mPtr.get()->mBase->mInventory, "", mSeed); - addScripts(*this, mPtr.mCell); + for (const auto&& ptr : *this) + ptr.getCellRef().setCount(0); + fillNonRandom(container.get()->mBase->mInventory, ESM::RefId(), mSeed); + addScripts(*this, container.mCell); mResolved = false; } } @@ -669,18 +744,18 @@ float MWWorld::ContainerStore::getWeight() const { mCachedWeight = 0; - mCachedWeight += getTotalWeight (potions); - mCachedWeight += getTotalWeight (appas); - mCachedWeight += getTotalWeight (armors); - mCachedWeight += getTotalWeight (books); - mCachedWeight += getTotalWeight (clothes); - mCachedWeight += getTotalWeight (ingreds); - mCachedWeight += getTotalWeight (lights); - mCachedWeight += getTotalWeight (lockpicks); - mCachedWeight += getTotalWeight (miscItems); - mCachedWeight += getTotalWeight (probes); - mCachedWeight += getTotalWeight (repairs); - mCachedWeight += getTotalWeight (weapons); + mCachedWeight += getTotalWeight(potions); + mCachedWeight += getTotalWeight(appas); + mCachedWeight += getTotalWeight(armors); + mCachedWeight += getTotalWeight(books); + mCachedWeight += getTotalWeight(clothes); + mCachedWeight += getTotalWeight(ingreds); + mCachedWeight += getTotalWeight(lights); + mCachedWeight += getTotalWeight(lockpicks); + mCachedWeight += getTotalWeight(miscItems); + mCachedWeight += getTotalWeight(probes); + mCachedWeight += getTotalWeight(repairs); + mCachedWeight += getTotalWeight(weapons); mWeightUpToDate = true; } @@ -688,65 +763,63 @@ float MWWorld::ContainerStore::getWeight() const return mCachedWeight; } -int MWWorld::ContainerStore::getType (const ConstPtr& ptr) +int MWWorld::ContainerStore::getType(const ConstPtr& ptr) { if (ptr.isEmpty()) - throw std::runtime_error ("can't put a non-existent object into a container"); + throw std::runtime_error("can't put a non-existent object into a container"); - if (ptr.getTypeName()==typeid (ESM::Potion).name()) + if (ptr.getType() == ESM::Potion::sRecordId) return Type_Potion; - if (ptr.getTypeName()==typeid (ESM::Apparatus).name()) + if (ptr.getType() == ESM::Apparatus::sRecordId) return Type_Apparatus; - if (ptr.getTypeName()==typeid (ESM::Armor).name()) + if (ptr.getType() == ESM::Armor::sRecordId) return Type_Armor; - if (ptr.getTypeName()==typeid (ESM::Book).name()) + if (ptr.getType() == ESM::Book::sRecordId) return Type_Book; - if (ptr.getTypeName()==typeid (ESM::Clothing).name()) + if (ptr.getType() == ESM::Clothing::sRecordId) return Type_Clothing; - if (ptr.getTypeName()==typeid (ESM::Ingredient).name()) + if (ptr.getType() == ESM::Ingredient::sRecordId) return Type_Ingredient; - if (ptr.getTypeName()==typeid (ESM::Light).name()) + if (ptr.getType() == ESM::Light::sRecordId) return Type_Light; - if (ptr.getTypeName()==typeid (ESM::Lockpick).name()) + if (ptr.getType() == ESM::Lockpick::sRecordId) return Type_Lockpick; - if (ptr.getTypeName()==typeid (ESM::Miscellaneous).name()) + if (ptr.getType() == ESM::Miscellaneous::sRecordId) return Type_Miscellaneous; - if (ptr.getTypeName()==typeid (ESM::Probe).name()) + if (ptr.getType() == ESM::Probe::sRecordId) return Type_Probe; - if (ptr.getTypeName()==typeid (ESM::Repair).name()) + if (ptr.getType() == ESM::Repair::sRecordId) return Type_Repair; - if (ptr.getTypeName()==typeid (ESM::Weapon).name()) + if (ptr.getType() == ESM::Weapon::sRecordId) return Type_Weapon; - throw std::runtime_error ( - "Object '" + ptr.getCellRef().getRefId() + "' of type " + ptr.getTypeName() + " can not be placed into a container"); + throw std::runtime_error("Object " + ptr.getCellRef().getRefId().toDebugString() + " of type " + + std::string(ptr.getTypeDescription()) + " can not be placed into a container"); } -MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const std::string& id) +MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const ESM::RefId& id) { MWWorld::Ptr item; int itemHealth = 1; for (auto&& iter : *this) { int iterHealth = iter.getClass().hasItemHealth(iter) ? iter.getClass().getItemHealth(iter) : 1; - if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefId(), id)) + if (iter.getCellRef().getRefId() == id) { // Prefer the stack with the lowest remaining uses // Try to get item with zero durability only if there are no other items found - if (item.isEmpty() || - (iterHealth > 0 && iterHealth < itemHealth) || - (itemHealth <= 0 && iterHealth > 0)) + if (item.isEmpty() || (iterHealth > 0 && iterHealth < itemHealth) || (itemHealth <= 0 && iterHealth > 0)) { item = iter; itemHealth = iterHealth; @@ -757,77 +830,77 @@ MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const std::string& id) return item; } -MWWorld::Ptr MWWorld::ContainerStore::search (const std::string& id) +MWWorld::Ptr MWWorld::ContainerStore::search(const ESM::RefId& id) { resolve(); { - Ptr ptr = searchId (potions, id, this); + Ptr ptr = searchId(potions, id, this); if (!ptr.isEmpty()) return ptr; } { - Ptr ptr = searchId (appas, id, this); + Ptr ptr = searchId(appas, id, this); if (!ptr.isEmpty()) return ptr; } { - Ptr ptr = searchId (armors, id, this); + Ptr ptr = searchId(armors, id, this); if (!ptr.isEmpty()) return ptr; } { - Ptr ptr = searchId (books, id, this); + Ptr ptr = searchId(books, id, this); if (!ptr.isEmpty()) return ptr; } { - Ptr ptr = searchId (clothes, id, this); + Ptr ptr = searchId(clothes, id, this); if (!ptr.isEmpty()) return ptr; } { - Ptr ptr = searchId (ingreds, id, this); + Ptr ptr = searchId(ingreds, id, this); if (!ptr.isEmpty()) return ptr; } { - Ptr ptr = searchId (lights, id, this); + Ptr ptr = searchId(lights, id, this); if (!ptr.isEmpty()) return ptr; } { - Ptr ptr = searchId (lockpicks, id, this); + Ptr ptr = searchId(lockpicks, id, this); if (!ptr.isEmpty()) return ptr; } { - Ptr ptr = searchId (miscItems, id, this); + Ptr ptr = searchId(miscItems, id, this); if (!ptr.isEmpty()) return ptr; } { - Ptr ptr = searchId (probes, id, this); + Ptr ptr = searchId(probes, id, this); if (!ptr.isEmpty()) return ptr; } { - Ptr ptr = searchId (repairs, id, this); + Ptr ptr = searchId(repairs, id, this); if (!ptr.isEmpty()) return ptr; } { - Ptr ptr = searchId (weapons, id, this); + Ptr ptr = searchId(weapons, id, this); if (!ptr.isEmpty()) return ptr; } @@ -838,7 +911,7 @@ MWWorld::Ptr MWWorld::ContainerStore::search (const std::string& id) int MWWorld::ContainerStore::addItems(int count1, int count2) { int sum = std::abs(count1) + std::abs(count2); - if(count1 < 0 || count2 < 0) + if (count1 < 0 || count2 < 0) return -sum; return sum; } @@ -846,59 +919,84 @@ int MWWorld::ContainerStore::addItems(int count1, int count2) int MWWorld::ContainerStore::subtractItems(int count1, int count2) { int sum = std::abs(count1) - std::abs(count2); - if(count1 < 0 || count2 < 0) + if (count1 < 0 || count2 < 0) return -sum; return sum; } -void MWWorld::ContainerStore::writeState (ESM::InventoryState& state) const +void MWWorld::ContainerStore::writeState(ESM::InventoryState& state) const { state.mItems.clear(); - int index = 0; - storeStates (potions, state, index); - storeStates (appas, state, index); - storeStates (armors, state, index, true); - storeStates (books, state, index, true); // not equipable as such, but for selectedEnchantItem - storeStates (clothes, state, index, true); - storeStates (ingreds, state, index); - storeStates (lockpicks, state, index, true); - storeStates (miscItems, state, index); - storeStates (probes, state, index, true); - storeStates (repairs, state, index); - storeStates (weapons, state, index, true); - storeStates (lights, state, index, true); + size_t index = 0; + storeStates(potions, state, index); + storeStates(appas, state, index); + storeStates(armors, state, index, true); + storeStates(books, state, index, true); // not equipable as such, but for selectedEnchantItem + storeStates(clothes, state, index, true); + storeStates(ingreds, state, index); + storeStates(lockpicks, state, index, true); + storeStates(miscItems, state, index); + storeStates(probes, state, index, true); + storeStates(repairs, state, index); + storeStates(weapons, state, index, true); + storeStates(lights, state, index, true); } -void MWWorld::ContainerStore::readState (const ESM::InventoryState& inventory) +void MWWorld::ContainerStore::readState(const ESM::InventoryState& inventory) { clear(); mModified = true; mResolved = true; - int index = 0; + size_t index = 0; for (const ESM::ObjectState& state : inventory.mItems) { - int type = MWBase::Environment::get().getWorld()->getStore().find(state.mRef.mRefID); + int type = MWBase::Environment::get().getESMStore()->find(state.mRef.mRefID); - int thisIndex = index++; + size_t thisIndex = index++; switch (type) { - case ESM::REC_ALCH: getState (potions, state); break; - case ESM::REC_APPA: getState (appas, state); break; - case ESM::REC_ARMO: readEquipmentState (getState (armors, state), thisIndex, inventory); break; - case ESM::REC_BOOK: readEquipmentState (getState (books, state), thisIndex, inventory); break; // not equipable as such, but for selectedEnchantItem - case ESM::REC_CLOT: readEquipmentState (getState (clothes, state), thisIndex, inventory); break; - case ESM::REC_INGR: getState (ingreds, state); break; - case ESM::REC_LOCK: readEquipmentState (getState (lockpicks, state), thisIndex, inventory); break; - case ESM::REC_MISC: getState (miscItems, state); break; - case ESM::REC_PROB: readEquipmentState (getState (probes, state), thisIndex, inventory); break; - case ESM::REC_REPA: getState (repairs, state); break; - case ESM::REC_WEAP: readEquipmentState (getState (weapons, state), thisIndex, inventory); break; - case ESM::REC_LIGH: readEquipmentState (getState (lights, state), thisIndex, inventory); break; + case ESM::REC_ALCH: + getState(potions, state); + break; + case ESM::REC_APPA: + getState(appas, state); + break; + case ESM::REC_ARMO: + readEquipmentState(getState(armors, state), thisIndex, inventory); + break; + case ESM::REC_BOOK: + readEquipmentState(getState(books, state), thisIndex, inventory); + break; // not equipable as such, but for selectedEnchantItem + case ESM::REC_CLOT: + readEquipmentState(getState(clothes, state), thisIndex, inventory); + break; + case ESM::REC_INGR: + getState(ingreds, state); + break; + case ESM::REC_LOCK: + readEquipmentState(getState(lockpicks, state), thisIndex, inventory); + break; + case ESM::REC_MISC: + getState(miscItems, state); + break; + case ESM::REC_PROB: + readEquipmentState(getState(probes, state), thisIndex, inventory); + break; + case ESM::REC_REPA: + getState(repairs, state); + break; + case ESM::REC_WEAP: + readEquipmentState(getState(weapons, state), thisIndex, inventory); + break; + case ESM::REC_LIGH: + readEquipmentState(getState(lights, state), thisIndex, inventory); + break; case 0: - Log(Debug::Warning) << "Dropping inventory reference to '" << state.mRef.mRefID << "' (object no longer exists)"; + Log(Debug::Warning) << "Dropping inventory reference to '" << state.mRef.mRefID + << "' (object no longer exists)"; break; default: Log(Debug::Warning) << "Warning: Invalid item type in inventory state, refid " << state.mRef.mRefID; @@ -907,9 +1005,9 @@ void MWWorld::ContainerStore::readState (const ESM::InventoryState& inventory) } } -template -template -void MWWorld::ContainerStoreIteratorBase::copy (const ContainerStoreIteratorBase& src) +template +template +void MWWorld::ContainerStoreIteratorBase::copy(const ContainerStoreIteratorBase& src) { mType = src.mType; mMask = src.mMask; @@ -918,51 +1016,77 @@ void MWWorld::ContainerStoreIteratorBase::copy (const ContainerStoreIte switch (src.mType) { - case MWWorld::ContainerStore::Type_Potion: mPotion = src.mPotion; break; - case MWWorld::ContainerStore::Type_Apparatus: mApparatus = src.mApparatus; break; - case MWWorld::ContainerStore::Type_Armor: mArmor = src.mArmor; break; - case MWWorld::ContainerStore::Type_Book: mBook = src.mBook; break; - case MWWorld::ContainerStore::Type_Clothing: mClothing = src.mClothing; break; - case MWWorld::ContainerStore::Type_Ingredient: mIngredient = src.mIngredient; break; - case MWWorld::ContainerStore::Type_Light: mLight = src.mLight; break; - case MWWorld::ContainerStore::Type_Lockpick: mLockpick = src.mLockpick; break; - case MWWorld::ContainerStore::Type_Miscellaneous: mMiscellaneous = src.mMiscellaneous; break; - case MWWorld::ContainerStore::Type_Probe: mProbe = src.mProbe; break; - case MWWorld::ContainerStore::Type_Repair: mRepair = src.mRepair; break; - case MWWorld::ContainerStore::Type_Weapon: mWeapon = src.mWeapon; break; - case -1: break; - default: assert(0); + case MWWorld::ContainerStore::Type_Potion: + mPotion = src.mPotion; + break; + case MWWorld::ContainerStore::Type_Apparatus: + mApparatus = src.mApparatus; + break; + case MWWorld::ContainerStore::Type_Armor: + mArmor = src.mArmor; + break; + case MWWorld::ContainerStore::Type_Book: + mBook = src.mBook; + break; + case MWWorld::ContainerStore::Type_Clothing: + mClothing = src.mClothing; + break; + case MWWorld::ContainerStore::Type_Ingredient: + mIngredient = src.mIngredient; + break; + case MWWorld::ContainerStore::Type_Light: + mLight = src.mLight; + break; + case MWWorld::ContainerStore::Type_Lockpick: + mLockpick = src.mLockpick; + break; + case MWWorld::ContainerStore::Type_Miscellaneous: + mMiscellaneous = src.mMiscellaneous; + break; + case MWWorld::ContainerStore::Type_Probe: + mProbe = src.mProbe; + break; + case MWWorld::ContainerStore::Type_Repair: + mRepair = src.mRepair; + break; + case MWWorld::ContainerStore::Type_Weapon: + mWeapon = src.mWeapon; + break; + case -1: + break; + default: + assert(0); } } -template +template void MWWorld::ContainerStoreIteratorBase::incType() { - if (mType==0) + if (mType == 0) mType = 1; - else if (mType!=-1) + else if (mType != -1) { mType <<= 1; - if (mType>ContainerStore::Type_Last) + if (mType > ContainerStore::Type_Last) mType = -1; } } -template +template void MWWorld::ContainerStoreIteratorBase::nextType() { - while (mType!=-1) + while (mType != -1) { incType(); - if ((mType & mMask) && mType>0) + if ((mType & mMask) && mType > 0) if (resetIterator()) break; } } -template +template bool MWWorld::ContainerStoreIteratorBase::resetIterator() { switch (mType) @@ -970,68 +1094,68 @@ bool MWWorld::ContainerStoreIteratorBase::resetIterator() case ContainerStore::Type_Potion: mPotion = mContainer->potions.mList.begin(); - return mPotion!=mContainer->potions.mList.end(); + return mPotion != mContainer->potions.mList.end(); case ContainerStore::Type_Apparatus: mApparatus = mContainer->appas.mList.begin(); - return mApparatus!=mContainer->appas.mList.end(); + return mApparatus != mContainer->appas.mList.end(); case ContainerStore::Type_Armor: mArmor = mContainer->armors.mList.begin(); - return mArmor!=mContainer->armors.mList.end(); + return mArmor != mContainer->armors.mList.end(); case ContainerStore::Type_Book: mBook = mContainer->books.mList.begin(); - return mBook!=mContainer->books.mList.end(); + return mBook != mContainer->books.mList.end(); case ContainerStore::Type_Clothing: mClothing = mContainer->clothes.mList.begin(); - return mClothing!=mContainer->clothes.mList.end(); + return mClothing != mContainer->clothes.mList.end(); case ContainerStore::Type_Ingredient: mIngredient = mContainer->ingreds.mList.begin(); - return mIngredient!=mContainer->ingreds.mList.end(); + return mIngredient != mContainer->ingreds.mList.end(); case ContainerStore::Type_Light: mLight = mContainer->lights.mList.begin(); - return mLight!=mContainer->lights.mList.end(); + return mLight != mContainer->lights.mList.end(); case ContainerStore::Type_Lockpick: mLockpick = mContainer->lockpicks.mList.begin(); - return mLockpick!=mContainer->lockpicks.mList.end(); + return mLockpick != mContainer->lockpicks.mList.end(); case ContainerStore::Type_Miscellaneous: mMiscellaneous = mContainer->miscItems.mList.begin(); - return mMiscellaneous!=mContainer->miscItems.mList.end(); + return mMiscellaneous != mContainer->miscItems.mList.end(); case ContainerStore::Type_Probe: mProbe = mContainer->probes.mList.begin(); - return mProbe!=mContainer->probes.mList.end(); + return mProbe != mContainer->probes.mList.end(); case ContainerStore::Type_Repair: mRepair = mContainer->repairs.mList.begin(); - return mRepair!=mContainer->repairs.mList.end(); + return mRepair != mContainer->repairs.mList.end(); case ContainerStore::Type_Weapon: mWeapon = mContainer->weapons.mList.begin(); - return mWeapon!=mContainer->weapons.mList.end(); + return mWeapon != mContainer->weapons.mList.end(); } return false; } -template +template bool MWWorld::ContainerStoreIteratorBase::incIterator() { switch (mType) @@ -1039,267 +1163,388 @@ bool MWWorld::ContainerStoreIteratorBase::incIterator() case ContainerStore::Type_Potion: ++mPotion; - return mPotion==mContainer->potions.mList.end(); + return mPotion == mContainer->potions.mList.end(); case ContainerStore::Type_Apparatus: ++mApparatus; - return mApparatus==mContainer->appas.mList.end(); + return mApparatus == mContainer->appas.mList.end(); case ContainerStore::Type_Armor: ++mArmor; - return mArmor==mContainer->armors.mList.end(); + return mArmor == mContainer->armors.mList.end(); case ContainerStore::Type_Book: ++mBook; - return mBook==mContainer->books.mList.end(); + return mBook == mContainer->books.mList.end(); case ContainerStore::Type_Clothing: ++mClothing; - return mClothing==mContainer->clothes.mList.end(); + return mClothing == mContainer->clothes.mList.end(); case ContainerStore::Type_Ingredient: ++mIngredient; - return mIngredient==mContainer->ingreds.mList.end(); + return mIngredient == mContainer->ingreds.mList.end(); case ContainerStore::Type_Light: ++mLight; - return mLight==mContainer->lights.mList.end(); + return mLight == mContainer->lights.mList.end(); case ContainerStore::Type_Lockpick: ++mLockpick; - return mLockpick==mContainer->lockpicks.mList.end(); + return mLockpick == mContainer->lockpicks.mList.end(); case ContainerStore::Type_Miscellaneous: ++mMiscellaneous; - return mMiscellaneous==mContainer->miscItems.mList.end(); + return mMiscellaneous == mContainer->miscItems.mList.end(); case ContainerStore::Type_Probe: ++mProbe; - return mProbe==mContainer->probes.mList.end(); + return mProbe == mContainer->probes.mList.end(); case ContainerStore::Type_Repair: ++mRepair; - return mRepair==mContainer->repairs.mList.end(); + return mRepair == mContainer->repairs.mList.end(); case ContainerStore::Type_Weapon: ++mWeapon; - return mWeapon==mContainer->weapons.mList.end(); + return mWeapon == mContainer->weapons.mList.end(); } return true; } - -template -template -bool MWWorld::ContainerStoreIteratorBase::isEqual (const ContainerStoreIteratorBase& other) const +template +template +bool MWWorld::ContainerStoreIteratorBase::isEqual(const ContainerStoreIteratorBase& other) const { - if (mContainer!=other.mContainer) + if (mContainer != other.mContainer) return false; - if (mType!=other.mType) + if (mType != other.mType) return false; switch (mType) { - case ContainerStore::Type_Potion: return mPotion==other.mPotion; - case ContainerStore::Type_Apparatus: return mApparatus==other.mApparatus; - case ContainerStore::Type_Armor: return mArmor==other.mArmor; - case ContainerStore::Type_Book: return mBook==other.mBook; - case ContainerStore::Type_Clothing: return mClothing==other.mClothing; - case ContainerStore::Type_Ingredient: return mIngredient==other.mIngredient; - case ContainerStore::Type_Light: return mLight==other.mLight; - case ContainerStore::Type_Lockpick: return mLockpick==other.mLockpick; - case ContainerStore::Type_Miscellaneous: return mMiscellaneous==other.mMiscellaneous; - case ContainerStore::Type_Probe: return mProbe==other.mProbe; - case ContainerStore::Type_Repair: return mRepair==other.mRepair; - case ContainerStore::Type_Weapon: return mWeapon==other.mWeapon; - case -1: return true; + case ContainerStore::Type_Potion: + return mPotion == other.mPotion; + case ContainerStore::Type_Apparatus: + return mApparatus == other.mApparatus; + case ContainerStore::Type_Armor: + return mArmor == other.mArmor; + case ContainerStore::Type_Book: + return mBook == other.mBook; + case ContainerStore::Type_Clothing: + return mClothing == other.mClothing; + case ContainerStore::Type_Ingredient: + return mIngredient == other.mIngredient; + case ContainerStore::Type_Light: + return mLight == other.mLight; + case ContainerStore::Type_Lockpick: + return mLockpick == other.mLockpick; + case ContainerStore::Type_Miscellaneous: + return mMiscellaneous == other.mMiscellaneous; + case ContainerStore::Type_Probe: + return mProbe == other.mProbe; + case ContainerStore::Type_Repair: + return mRepair == other.mRepair; + case ContainerStore::Type_Weapon: + return mWeapon == other.mWeapon; + case -1: + return true; } - return false; + return false; } -template -PtrType *MWWorld::ContainerStoreIteratorBase::operator->() const +template +PtrType* MWWorld::ContainerStoreIteratorBase::operator->() const { mPtr = **this; return &mPtr; } -template +template PtrType MWWorld::ContainerStoreIteratorBase::operator*() const { PtrType ptr; switch (mType) { - case ContainerStore::Type_Potion: ptr = PtrType (&*mPotion, nullptr); break; - case ContainerStore::Type_Apparatus: ptr = PtrType (&*mApparatus, nullptr); break; - case ContainerStore::Type_Armor: ptr = PtrType (&*mArmor, nullptr); break; - case ContainerStore::Type_Book: ptr = PtrType (&*mBook, nullptr); break; - case ContainerStore::Type_Clothing: ptr = PtrType (&*mClothing, nullptr); break; - case ContainerStore::Type_Ingredient: ptr = PtrType (&*mIngredient, nullptr); break; - case ContainerStore::Type_Light: ptr = PtrType (&*mLight, nullptr); break; - case ContainerStore::Type_Lockpick: ptr = PtrType (&*mLockpick, nullptr); break; - case ContainerStore::Type_Miscellaneous: ptr = PtrType (&*mMiscellaneous, nullptr); break; - case ContainerStore::Type_Probe: ptr = PtrType (&*mProbe, nullptr); break; - case ContainerStore::Type_Repair: ptr = PtrType (&*mRepair, nullptr); break; - case ContainerStore::Type_Weapon: ptr = PtrType (&*mWeapon, nullptr); break; + case ContainerStore::Type_Potion: + ptr = PtrType(&*mPotion, nullptr); + break; + case ContainerStore::Type_Apparatus: + ptr = PtrType(&*mApparatus, nullptr); + break; + case ContainerStore::Type_Armor: + ptr = PtrType(&*mArmor, nullptr); + break; + case ContainerStore::Type_Book: + ptr = PtrType(&*mBook, nullptr); + break; + case ContainerStore::Type_Clothing: + ptr = PtrType(&*mClothing, nullptr); + break; + case ContainerStore::Type_Ingredient: + ptr = PtrType(&*mIngredient, nullptr); + break; + case ContainerStore::Type_Light: + ptr = PtrType(&*mLight, nullptr); + break; + case ContainerStore::Type_Lockpick: + ptr = PtrType(&*mLockpick, nullptr); + break; + case ContainerStore::Type_Miscellaneous: + ptr = PtrType(&*mMiscellaneous, nullptr); + break; + case ContainerStore::Type_Probe: + ptr = PtrType(&*mProbe, nullptr); + break; + case ContainerStore::Type_Repair: + ptr = PtrType(&*mRepair, nullptr); + break; + case ContainerStore::Type_Weapon: + ptr = PtrType(&*mWeapon, nullptr); + break; } if (ptr.isEmpty()) - throw std::runtime_error ("invalid iterator"); + throw std::runtime_error("invalid iterator"); - ptr.setContainerStore (mContainer); + ptr.setContainerStore(mContainer); return ptr; } -template +template MWWorld::ContainerStoreIteratorBase& MWWorld::ContainerStoreIteratorBase::operator++() { do { if (incIterator()) nextType(); - } - while (mType!=-1 && !(**this).getRefData().getCount()); + } while (mType != -1 && !(**this).getCellRef().getCount()); return *this; } -template -MWWorld::ContainerStoreIteratorBase MWWorld::ContainerStoreIteratorBase::operator++ (int) +template +MWWorld::ContainerStoreIteratorBase MWWorld::ContainerStoreIteratorBase::operator++(int) { - ContainerStoreIteratorBase iter (*this); + ContainerStoreIteratorBase iter(*this); ++*this; return iter; } -template -MWWorld::ContainerStoreIteratorBase& MWWorld::ContainerStoreIteratorBase::operator= (const ContainerStoreIteratorBase& rhs) +template +MWWorld::ContainerStoreIteratorBase& MWWorld::ContainerStoreIteratorBase::operator=( + const ContainerStoreIteratorBase& rhs) { - if (this!=&rhs) + if (this != &rhs) { copy(rhs); } return *this; } -template +template int MWWorld::ContainerStoreIteratorBase::getType() const { return mType; } -template -const MWWorld::ContainerStore *MWWorld::ContainerStoreIteratorBase::getContainerStore() const +template +const MWWorld::ContainerStore* MWWorld::ContainerStoreIteratorBase::getContainerStore() const { return mContainer; } -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container) -: mType (-1), mMask (0), mContainer (container) -{} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase(ContainerStoreType container) + : mType(-1) + , mMask(0) + , mContainer(container) +{ +} -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (int mask, ContainerStoreType container) -: mType (0), mMask (mask), mContainer (container) +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase(int mask, ContainerStoreType container) + : mType(0) + , mMask(mask) + , mContainer(container) { nextType(); - if (mType==-1 || (**this).getRefData().getCount()) + if (mType == -1 || (**this).getCellRef().getCount()) return; ++*this; } -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Potion), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mPotion(iterator){} - -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Apparatus), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mApparatus(iterator){} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Potion) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mPotion(iterator) +{ +} -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Armor), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mArmor(iterator){} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Apparatus) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mApparatus(iterator) +{ +} -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Book), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mBook(iterator){} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Armor) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mArmor(iterator) +{ +} -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Clothing), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mClothing(iterator){} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Book) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mBook(iterator) +{ +} -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Ingredient), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mIngredient(iterator){} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Clothing) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mClothing(iterator) +{ +} -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Light), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mLight(iterator){} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Ingredient) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mIngredient(iterator) +{ +} -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Lockpick), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mLockpick(iterator){} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Light) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mLight(iterator) +{ +} -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Miscellaneous), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mMiscellaneous(iterator){} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Lockpick) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mLockpick(iterator) +{ +} -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Probe), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mProbe(iterator){} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Miscellaneous) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mMiscellaneous(iterator) +{ +} -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Repair), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mRepair(iterator){} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Probe) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mProbe(iterator) +{ +} -template -MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type iterator) - : mType(MWWorld::ContainerStore::Type_Weapon), mMask(MWWorld::ContainerStore::Type_All), mContainer(container), mWeapon(iterator){} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Repair) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mRepair(iterator) +{ +} +template +MWWorld::ContainerStoreIteratorBase::ContainerStoreIteratorBase( + ContainerStoreType container, typename Iterator::type iterator) + : mType(MWWorld::ContainerStore::Type_Weapon) + , mMask(MWWorld::ContainerStore::Type_All) + , mContainer(container) + , mWeapon(iterator) +{ +} -template -bool MWWorld::operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right) +template +bool MWWorld::operator==(const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right) { - return left.isEqual (right); + return left.isEqual(right); } -template -bool MWWorld::operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right) +template +bool MWWorld::operator!=(const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right) { - return !(left==right); + return !(left == right); } template class MWWorld::ContainerStoreIteratorBase; template class MWWorld::ContainerStoreIteratorBase; -template bool MWWorld::operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); -template bool MWWorld::operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); -template bool MWWorld::operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); -template bool MWWorld::operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); -template bool MWWorld::operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); -template bool MWWorld::operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); -template bool MWWorld::operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); -template bool MWWorld::operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); +template bool MWWorld::operator==( + const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); +template bool MWWorld::operator!=( + const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); +template bool MWWorld::operator==( + const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); +template bool MWWorld::operator!=( + const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); +template bool MWWorld::operator==( + const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); +template bool MWWorld::operator!=( + const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); +template bool MWWorld::operator==( + const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); +template bool MWWorld::operator!=( + const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); template void MWWorld::ContainerStoreIteratorBase::copy(const ContainerStoreIteratorBase& src); template void MWWorld::ContainerStoreIteratorBase::copy(const ContainerStoreIteratorBase& src); -template void MWWorld::ContainerStoreIteratorBase::copy(const ContainerStoreIteratorBase& src); +template void MWWorld::ContainerStoreIteratorBase::copy( + const ContainerStoreIteratorBase& src); diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index 5044b0f4519..fb2722dde81 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -6,23 +6,23 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include "ptr.hpp" #include "cellreflist.hpp" +#include "ptr.hpp" namespace ESM { @@ -39,7 +39,7 @@ namespace MWWorld { class ContainerStore; - template + template class ContainerStoreIteratorBase; typedef ContainerStoreIteratorBase ContainerStoreIterator; @@ -47,258 +47,293 @@ namespace MWWorld class ResolutionListener { - ContainerStore& mStore; - public: - ResolutionListener(ContainerStore& store) : mStore(store) {} - ~ResolutionListener(); + ContainerStore& mStore; + + public: + ResolutionListener(ContainerStore& store) + : mStore(store) + { + } + ~ResolutionListener(); }; class ResolutionHandle { - std::shared_ptr mListener; - public: - ResolutionHandle(std::shared_ptr listener) : mListener(listener) {} - ResolutionHandle() {} + std::shared_ptr mListener; + + public: + ResolutionHandle(std::shared_ptr listener) + : mListener(std::move(listener)) + { + } + ResolutionHandle() = default; }; - + class ContainerStoreListener { - public: - virtual void itemAdded(const ConstPtr& item, int count) {} - virtual void itemRemoved(const ConstPtr& item, int count) {} - virtual ~ContainerStoreListener() = default; + public: + virtual void itemAdded(const ConstPtr& item, int count) {} + virtual void itemRemoved(const ConstPtr& item, int count) {} + virtual ~ContainerStoreListener() = default; }; class ContainerStore { - public: + public: + static constexpr int Type_Potion = 0x0001; + static constexpr int Type_Apparatus = 0x0002; + static constexpr int Type_Armor = 0x0004; + static constexpr int Type_Book = 0x0008; + static constexpr int Type_Clothing = 0x0010; + static constexpr int Type_Ingredient = 0x0020; + static constexpr int Type_Light = 0x0040; + static constexpr int Type_Lockpick = 0x0080; + static constexpr int Type_Miscellaneous = 0x0100; + static constexpr int Type_Probe = 0x0200; + static constexpr int Type_Repair = 0x0400; + static constexpr int Type_Weapon = 0x0800; + + static constexpr int Type_Last = Type_Weapon; + + static constexpr int Type_All = 0xffff; + + static const ESM::RefId sGoldId; + + static constexpr bool isStorableType(unsigned int t) + { + return t == ESM::Potion::sRecordId || t == ESM::Apparatus::sRecordId || t == ESM::Armor::sRecordId + || t == ESM::Book::sRecordId || t == ESM::Clothing::sRecordId || t == ESM::Ingredient::sRecordId + || t == ESM::Light::sRecordId || t == ESM::Lockpick::sRecordId || t == ESM::Miscellaneous::sRecordId + || t == ESM::Probe::sRecordId || t == ESM::Repair::sRecordId || t == ESM::Weapon::sRecordId; + } + template + static constexpr bool isStorableType() + { + return isStorableType(T::sRecordId); + } + + protected: + ContainerStoreListener* mListener; + + // Used in clone() to unset refnums of copies. + // (RefNum should be unique, copy can not have the same RefNum). + void updateRefNums(); - static constexpr int Type_Potion = 0x0001; - static constexpr int Type_Apparatus = 0x0002; - static constexpr int Type_Armor = 0x0004; - static constexpr int Type_Book = 0x0008; - static constexpr int Type_Clothing = 0x0010; - static constexpr int Type_Ingredient = 0x0020; - static constexpr int Type_Light = 0x0040; - static constexpr int Type_Lockpick = 0x0080; - static constexpr int Type_Miscellaneous = 0x0100; - static constexpr int Type_Probe = 0x0200; - static constexpr int Type_Repair = 0x0400; - static constexpr int Type_Weapon = 0x0800; + // (item, max charge) + typedef std::vector> TRechargingItems; + TRechargingItems mRechargingItems; - static constexpr int Type_Last = Type_Weapon; + bool mRechargingItemsUpToDate; - static constexpr int Type_All = 0xffff; + private: + MWWorld::CellRefList potions; + MWWorld::CellRefList appas; + MWWorld::CellRefList armors; + MWWorld::CellRefList books; + MWWorld::CellRefList clothes; + MWWorld::CellRefList ingreds; + MWWorld::CellRefList lights; + MWWorld::CellRefList lockpicks; + MWWorld::CellRefList miscItems; + MWWorld::CellRefList probes; + MWWorld::CellRefList repairs; + MWWorld::CellRefList weapons; - static const std::string sGoldId; + mutable float mCachedWeight; + mutable bool mWeightUpToDate; - protected: - ContainerStoreListener* mListener; - - // (item, max charge) - typedef std::vector > TRechargingItems; - TRechargingItems mRechargingItems; - - bool mRechargingItemsUpToDate; + bool mModified; + bool mResolved; + unsigned int mSeed; + MWWorld::SafePtr mPtr; // Container or actor that holds this store. + std::weak_ptr mResolutionListener; - private: - - MWWorld::CellRefList potions; - MWWorld::CellRefList appas; - MWWorld::CellRefList armors; - MWWorld::CellRefList books; - MWWorld::CellRefList clothes; - MWWorld::CellRefList ingreds; - MWWorld::CellRefList lights; - MWWorld::CellRefList lockpicks; - MWWorld::CellRefList miscItems; - MWWorld::CellRefList probes; - MWWorld::CellRefList repairs; - MWWorld::CellRefList weapons; - - mutable float mCachedWeight; - mutable bool mWeightUpToDate; - - bool mModified; - bool mResolved; - unsigned int mSeed; - MWWorld::Ptr mPtr; - std::weak_ptr mResolutionListener; + ContainerStoreIterator addImp(const Ptr& ptr, int count, bool markModified = true); + void addInitialItem( + const ESM::RefId& id, const ESM::RefId& owner, int count, Misc::Rng::Generator* prng, bool topLevel = true); + void addInitialItemImp(const MWWorld::Ptr& ptr, const ESM::RefId& owner, int count, Misc::Rng::Generator* prng, + bool topLevel = true); - ContainerStoreIterator addImp (const Ptr& ptr, int count, bool markModified = true); - void addInitialItem (const std::string& id, const std::string& owner, int count, Misc::Rng::Seed* seed, bool topLevel=true); - void addInitialItemImp (const MWWorld::Ptr& ptr, const std::string& owner, int count, Misc::Rng::Seed* seed, bool topLevel=true); + template + ContainerStoreIterator getState(CellRefList& collection, const ESM::ObjectState& state); - template - ContainerStoreIterator getState (CellRefList& collection, - const ESM::ObjectState& state); + template + void storeState(const LiveCellRef& ref, ESM::ObjectState& state) const; - template - void storeState (const LiveCellRef& ref, ESM::ObjectState& state) const; + template + void storeStates(const CellRefList& collection, ESM::InventoryState& inventory, size_t& index, + bool equipable = false) const; - template - void storeStates (const CellRefList& collection, - ESM::InventoryState& inventory, int& index, - bool equipable = false) const; + void updateRechargingItems(); - void updateRechargingItems(); + virtual void storeEquipmentState( + const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const; - virtual void storeEquipmentState (const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const; + virtual void readEquipmentState( + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory); - virtual void readEquipmentState (const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory); + public: + ContainerStore(); - public: + virtual ~ContainerStore() = default; - ContainerStore(); + virtual std::unique_ptr clone() + { + auto res = std::make_unique(*this); + res->updateRefNums(); + return res; + } - virtual ~ContainerStore(); + // Container or actor that holds this store. + const Ptr& getPtr() const { return mPtr.ptrOrEmpty(); } + void setPtr(const Ptr& ptr) { mPtr = SafePtr(ptr); } - virtual std::unique_ptr clone() { return std::make_unique(*this); } + ConstContainerStoreIterator cbegin(int mask = Type_All) const; + ConstContainerStoreIterator cend() const; + ConstContainerStoreIterator begin(int mask = Type_All) const; + ConstContainerStoreIterator end() const; - ConstContainerStoreIterator cbegin (int mask = Type_All) const; - ConstContainerStoreIterator cend() const; - ConstContainerStoreIterator begin (int mask = Type_All) const; - ConstContainerStoreIterator end() const; - - ContainerStoreIterator begin (int mask = Type_All); - ContainerStoreIterator end(); + ContainerStoreIterator begin(int mask = Type_All); + ContainerStoreIterator end(); - bool hasVisibleItems() const; + bool hasVisibleItems() const; - virtual ContainerStoreIterator add (const Ptr& itemPtr, int count, const Ptr& actorPtr, bool allowAutoEquip = true, bool resolve = true); - ///< Add the item pointed to by \a ptr to this container. (Stacks automatically if needed) - /// - /// \note The item pointed to is not required to exist beyond this function call. - /// - /// \attention Do not add items to an existing stack by increasing the count instead of - /// calling this function! - /// - /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item. + virtual ContainerStoreIterator add( + const Ptr& itemPtr, int count, bool allowAutoEquip = true, bool resolve = true); + ///< Add the item pointed to by \a ptr to this container. (Stacks automatically if needed) + /// + /// \note The item pointed to is not required to exist beyond this function call. + /// + /// \attention Do not add items to an existing stack by increasing the count instead of + /// calling this function! + /// + /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to + /// the newly inserted item. - ContainerStoreIterator add(const std::string& id, int count, const Ptr& actorPtr); - ///< Utility to construct a ManualRef and call add(ptr, count, actorPtr, true) + ContainerStoreIterator add(const ESM::RefId& id, int count, bool allowAutoEquip = true); + ///< Utility to construct a ManualRef and call add(ptr, count, actorPtr, true) - int remove(const std::string& itemId, int count, const Ptr& actor, bool equipReplacement = 0, bool resolve = true); - ///< Remove \a count item(s) designated by \a itemId from this container. - /// - /// @return the number of items actually removed + int remove(const ESM::RefId& itemId, int count, bool equipReplacement = 0, bool resolve = true); + ///< Remove \a count item(s) designated by \a itemId from this container. + /// + /// @return the number of items actually removed - virtual int remove(const Ptr& item, int count, const Ptr& actor, bool equipReplacement = 0, bool resolve = true); - ///< Remove \a count item(s) designated by \a item from this inventory. - /// - /// @return the number of items actually removed + virtual int remove(const Ptr& item, int count, bool equipReplacement = 0, bool resolve = true); + ///< Remove \a count item(s) designated by \a item from this inventory. + /// + /// @return the number of items actually removed - void rechargeItems (float duration); - ///< Restore charge on enchanted items. Note this should only be done for the player. + void rechargeItems(float duration); + ///< Restore charge on enchanted items. Note this should only be done for the player. - ContainerStoreIterator unstack (const Ptr& ptr, const Ptr& container, int count = 1); - ///< Unstack an item in this container. The item's count will be set to count, then a new stack will be added with (origCount-count). - /// - /// @return an iterator to the new stack, or end() if no new stack was created. + ContainerStoreIterator unstack(const Ptr& ptr, int count = 1); + ///< Unstack an item in this container. The item's count will be set to count, then a new stack will be added + ///< with (origCount-count). + /// + /// @return an iterator to the new stack, or end() if no new stack was created. + + MWWorld::ContainerStoreIterator restack(const MWWorld::Ptr& item); + ///< Attempt to re-stack an item in this container. + /// If a compatible stack is found, the item's count is added to that stack, then the original is deleted. + /// @return If the item was stacked, return the stack, otherwise return the old (untouched) item. - MWWorld::ContainerStoreIterator restack (const MWWorld::Ptr& item); - ///< Attempt to re-stack an item in this container. - /// If a compatible stack is found, the item's count is added to that stack, then the original is deleted. - /// @return If the item was stacked, return the stack, otherwise return the old (untouched) item. + int count(const ESM::RefId& id) const; + ///< @return How many items with refID \a id are in this container? - int count (const std::string& id) const; - ///< @return How many items with refID \a id are in this container? + ContainerStoreListener* getContListener() const; + void setContListener(ContainerStoreListener* listener); - ContainerStoreListener* getContListener() const; - void setContListener(ContainerStoreListener* listener); - protected: - ContainerStoreIterator addNewStack (const ConstPtr& ptr, int count); - ///< Add the item to this container (do not try to stack it onto existing items) + protected: + ContainerStoreIterator addNewStack(const ConstPtr& ptr, int count); + ///< Add the item to this container (do not try to stack it onto existing items) - virtual void flagAsModified(); + virtual void flagAsModified(); - /// + and - operations that can deal with negative stacks - /// Note that negativity is infectious - static int addItems(int count1, int count2); - static int subtractItems(int count1, int count2); - public: + /// + and - operations that can deal with negative stacks + /// Note that negativity is infectious + static int addItems(int count1, int count2); + static int subtractItems(int count1, int count2); - virtual bool stacks (const ConstPtr& ptr1, const ConstPtr& ptr2) const; - ///< @return true if the two specified objects can stack with each other + public: + virtual bool stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const; + ///< @return true if the two specified objects can stack with each other - void fill (const ESM::InventoryList& items, const std::string& owner, Misc::Rng::Seed& seed = Misc::Rng::getSeed()); - ///< Insert items into *this. + void fill(const ESM::InventoryList& items, const ESM::RefId& owner, Misc::Rng::Generator& seed); + ///< Insert items into *this. - void fillNonRandom (const ESM::InventoryList& items, const std::string& owner, unsigned int seed); - ///< Insert items into *this, excluding leveled items + void fillNonRandom(const ESM::InventoryList& items, const ESM::RefId& owner, unsigned int seed); + ///< Insert items into *this, excluding leveled items - virtual void clear(); - ///< Empty container. + virtual void clear(); + ///< Empty container. - float getWeight() const; - ///< Return total weight of the items contained in *this. + float getWeight() const; + ///< Return total weight of the items contained in *this. - static int getType (const ConstPtr& ptr); - ///< This function throws an exception, if ptr does not point to an object, that can be - /// put into a container. + static int getType(const ConstPtr& ptr); + ///< This function throws an exception, if ptr does not point to an object, that can be + /// put into a container. - Ptr findReplacement(const std::string& id); - ///< Returns replacement for object with given id. Prefer used items (with low durability left). + Ptr findReplacement(const ESM::RefId& id); + ///< Returns replacement for object with given id. Prefer used items (with low durability left). - Ptr search (const std::string& id); + Ptr search(const ESM::RefId& id); - virtual void writeState (ESM::InventoryState& state) const; + virtual void writeState(ESM::InventoryState& state) const; - virtual void readState (const ESM::InventoryState& state); + virtual void readState(const ESM::InventoryState& state); - bool isResolved() const; + bool isResolved() const; - void resolve(); - ResolutionHandle resolveTemporarily(); - void unresolve(); + void resolve(); + ResolutionHandle resolveTemporarily(); + void unresolve(); - friend class ContainerStoreIteratorBase; - friend class ContainerStoreIteratorBase; - friend class ResolutionListener; - friend class MWClass::Container; + friend class ContainerStoreIteratorBase; + friend class ContainerStoreIteratorBase; + friend class ResolutionListener; + friend class MWClass::Container; }; - - template + template class ContainerStoreIteratorBase - : public std::iterator { - template + template struct IsConvertible { static constexpr bool value = true; }; - template + template struct IsConvertible { static constexpr bool value = false; }; - template + template struct IteratorTrait { typedef typename MWWorld::CellRefList::List::iterator type; }; - template + template struct IteratorTrait { typedef typename MWWorld::CellRefList::List::const_iterator type; }; - template + template struct Iterator : IteratorTrait { }; - template + template struct ContainerStoreTrait { typedef ContainerStore* type; }; - - template + + template struct ContainerStoreTrait { typedef const ContainerStore* type; @@ -324,74 +359,80 @@ namespace MWWorld typename Iterator::type mRepair; typename Iterator::type mWeapon; - ContainerStoreIteratorBase (ContainerStoreType container); + ContainerStoreIteratorBase(ContainerStoreType container); ///< End-iterator - ContainerStoreIteratorBase (int mask, ContainerStoreType container); + ContainerStoreIteratorBase(int mask, ContainerStoreType container); ///< Begin-iterator // construct iterator using a CellRefList iterator - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - ContainerStoreIteratorBase (ContainerStoreType container, typename Iterator::type); - - template - void copy (const ContainerStoreIteratorBase& src); - - void incType (); - - void nextType (); - - bool resetIterator (); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + ContainerStoreIteratorBase(ContainerStoreType container, typename Iterator::type); + + template + void copy(const ContainerStoreIteratorBase& src); + + void incType(); + + void nextType(); + + bool resetIterator(); ///< Reset iterator for selected type. /// /// \return Type not empty? - bool incIterator (); + bool incIterator(); ///< Increment iterator for selected type. /// /// \return reached the end? - public: - template - ContainerStoreIteratorBase (const ContainerStoreIteratorBase& other) - { - char CANNOT_CONVERT_CONST_ITERATOR_TO_ITERATOR[IsConvertible::value ? 1 : -1]; - ((void)CANNOT_CONVERT_CONST_ITERATOR_TO_ITERATOR); - copy (other); - } + public: + using iterator_category = std::forward_iterator_tag; + using value_type = PtrType; + using difference_type = std::ptrdiff_t; + using pointer = PtrType*; + using reference = PtrType&; + + template + ContainerStoreIteratorBase(const ContainerStoreIteratorBase& other) + { + char CANNOT_CONVERT_CONST_ITERATOR_TO_ITERATOR[IsConvertible::value ? 1 : -1]; + ((void)CANNOT_CONVERT_CONST_ITERATOR_TO_ITERATOR); + copy(other); + } - template - bool isEqual(const ContainerStoreIteratorBase& other) const; + template + bool isEqual(const ContainerStoreIteratorBase& other) const; - PtrType *operator->() const; - PtrType operator*() const; + PtrType* operator->() const; + PtrType operator*() const; - ContainerStoreIteratorBase& operator++ (); - ContainerStoreIteratorBase operator++ (int); - ContainerStoreIteratorBase& operator= (const ContainerStoreIteratorBase& rhs); - ContainerStoreIteratorBase (const ContainerStoreIteratorBase& rhs) = default; + ContainerStoreIteratorBase& operator++(); + ContainerStoreIteratorBase operator++(int); + ContainerStoreIteratorBase& operator=(const ContainerStoreIteratorBase& rhs); + ContainerStoreIteratorBase(const ContainerStoreIteratorBase& rhs) = default; - int getType() const; - const ContainerStore *getContainerStore() const; + int getType() const; + const ContainerStore* getContainerStore() const; - friend class ContainerStore; - friend class ContainerStoreIteratorBase; - friend class ContainerStoreIteratorBase; + friend class ContainerStore; + friend class ContainerStoreIteratorBase; + friend class ContainerStoreIteratorBase; }; - template - bool operator== (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); - template - bool operator!= (const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); + template + bool operator==(const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); + template + bool operator!=(const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& right); } #endif diff --git a/apps/openmw/mwworld/contentloader.hpp b/apps/openmw/mwworld/contentloader.hpp index b529ae9db8d..70150ec33af 100644 --- a/apps/openmw/mwworld/contentloader.hpp +++ b/apps/openmw/mwworld/contentloader.hpp @@ -1,35 +1,22 @@ #ifndef CONTENTLOADER_HPP #define CONTENTLOADER_HPP -#include -#include +#include -#include -#include "components/loadinglistener/loadinglistener.hpp" - -namespace MWWorld +namespace Loading { + class Listener; +} -struct ContentLoader +namespace MWWorld { - ContentLoader(Loading::Listener& listener) - : mListener(listener) - { - } - - virtual ~ContentLoader() - { - } - virtual void load(const boost::filesystem::path& filepath, int& index) + struct ContentLoader { - Log(Debug::Info) << "Loading content file " << filepath.string(); - mListener.setLabel(MyGUI::TextIterator::toTagsString(filepath.string())); - } + virtual ~ContentLoader() = default; - protected: - Loading::Listener& mListener; -}; + virtual void load(const std::filesystem::path& filepath, int& index, Loading::Listener* listener) = 0; + }; } /* namespace MWWorld */ diff --git a/apps/openmw/mwworld/customdata.cpp b/apps/openmw/mwworld/customdata.cpp index 5080c09230b..db340302ce8 100644 --- a/apps/openmw/mwworld/customdata.cpp +++ b/apps/openmw/mwworld/customdata.cpp @@ -1,81 +1,93 @@ #include "customdata.hpp" -#include #include +#include #include namespace MWWorld { -MWClass::CreatureCustomData &CustomData::asCreatureCustomData() -{ - std::stringstream error; - error << "bad cast " << typeid(this).name() << " to CreatureCustomData"; - throw std::logic_error(error.str()); -} + MWClass::CreatureCustomData& CustomData::asCreatureCustomData() + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to CreatureCustomData"; + throw std::logic_error(error.str()); + } -const MWClass::CreatureCustomData &CustomData::asCreatureCustomData() const -{ - std::stringstream error; - error << "bad cast " << typeid(this).name() << " to CreatureCustomData"; - throw std::logic_error(error.str()); -} + const MWClass::CreatureCustomData& CustomData::asCreatureCustomData() const + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to CreatureCustomData"; + throw std::logic_error(error.str()); + } -MWClass::NpcCustomData &CustomData::asNpcCustomData() -{ - std::stringstream error; - error << "bad cast " << typeid(this).name() << " to NpcCustomData"; - throw std::logic_error(error.str()); -} + MWClass::NpcCustomData& CustomData::asNpcCustomData() + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to NpcCustomData"; + throw std::logic_error(error.str()); + } -const MWClass::NpcCustomData &CustomData::asNpcCustomData() const -{ - std::stringstream error; - error << "bad cast " << typeid(this).name() << " to NpcCustomData"; - throw std::logic_error(error.str()); -} + const MWClass::NpcCustomData& CustomData::asNpcCustomData() const + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to NpcCustomData"; + throw std::logic_error(error.str()); + } -MWClass::ContainerCustomData &CustomData::asContainerCustomData() -{ - std::stringstream error; - error << "bad cast " << typeid(this).name() << " to ContainerCustomData"; - throw std::logic_error(error.str()); -} + MWClass::ContainerCustomData& CustomData::asContainerCustomData() + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to ContainerCustomData"; + throw std::logic_error(error.str()); + } -const MWClass::ContainerCustomData &CustomData::asContainerCustomData() const -{ - std::stringstream error; - error << "bad cast " << typeid(this).name() << " to ContainerCustomData"; - throw std::logic_error(error.str()); -} + const MWClass::ContainerCustomData& CustomData::asContainerCustomData() const + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to ContainerCustomData"; + throw std::logic_error(error.str()); + } -MWClass::DoorCustomData &CustomData::asDoorCustomData() -{ - std::stringstream error; - error << "bad cast " << typeid(this).name() << " to DoorCustomData"; - throw std::logic_error(error.str()); -} + MWClass::DoorCustomData& CustomData::asDoorCustomData() + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to DoorCustomData"; + throw std::logic_error(error.str()); + } -const MWClass::DoorCustomData &CustomData::asDoorCustomData() const -{ - std::stringstream error; - error << "bad cast " << typeid(this).name() << " to DoorCustomData"; - throw std::logic_error(error.str()); -} + const MWClass::DoorCustomData& CustomData::asDoorCustomData() const + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to DoorCustomData"; + throw std::logic_error(error.str()); + } -MWClass::CreatureLevListCustomData &CustomData::asCreatureLevListCustomData() -{ - std::stringstream error; - error << "bad cast " << typeid(this).name() << " to CreatureLevListCustomData"; - throw std::logic_error(error.str()); -} + MWClass::CreatureLevListCustomData& CustomData::asCreatureLevListCustomData() + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to CreatureLevListCustomData"; + throw std::logic_error(error.str()); + } -const MWClass::CreatureLevListCustomData &CustomData::asCreatureLevListCustomData() const -{ - std::stringstream error; - error << "bad cast " << typeid(this).name() << " to CreatureLevListCustomData"; - throw std::logic_error(error.str()); -} + const MWClass::CreatureLevListCustomData& CustomData::asCreatureLevListCustomData() const + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to CreatureLevListCustomData"; + throw std::logic_error(error.str()); + } + MWClass::ESM4NpcCustomData& CustomData::asESM4NpcCustomData() + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to ESM4NpcCustomData"; + throw std::logic_error(error.str()); + } + const MWClass::ESM4NpcCustomData& CustomData::asESM4NpcCustomData() const + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to ESM4NpcCustomData"; + throw std::logic_error(error.str()); + } } diff --git a/apps/openmw/mwworld/customdata.hpp b/apps/openmw/mwworld/customdata.hpp index 7200e7684cd..80518763094 100644 --- a/apps/openmw/mwworld/customdata.hpp +++ b/apps/openmw/mwworld/customdata.hpp @@ -6,6 +6,7 @@ namespace MWClass { class CreatureCustomData; + class ESM4NpcCustomData; class NpcCustomData; class ContainerCustomData; class DoorCustomData; @@ -17,37 +18,36 @@ namespace MWWorld /// \brief Base class for the MW-class-specific part of RefData class CustomData { - public: + public: + virtual ~CustomData() {} - virtual ~CustomData() {} + virtual std::unique_ptr clone() const = 0; - virtual std::unique_ptr clone() const = 0; + // Fast version of dynamic_cast. Needs to be overridden in the respective class. - // Fast version of dynamic_cast. Needs to be overridden in the respective class. + virtual MWClass::CreatureCustomData& asCreatureCustomData(); + virtual const MWClass::CreatureCustomData& asCreatureCustomData() const; - virtual MWClass::CreatureCustomData& asCreatureCustomData(); - virtual const MWClass::CreatureCustomData& asCreatureCustomData() const; + virtual MWClass::NpcCustomData& asNpcCustomData(); + virtual const MWClass::NpcCustomData& asNpcCustomData() const; - virtual MWClass::NpcCustomData& asNpcCustomData(); - virtual const MWClass::NpcCustomData& asNpcCustomData() const; + virtual MWClass::ContainerCustomData& asContainerCustomData(); + virtual const MWClass::ContainerCustomData& asContainerCustomData() const; - virtual MWClass::ContainerCustomData& asContainerCustomData(); - virtual const MWClass::ContainerCustomData& asContainerCustomData() const; + virtual MWClass::DoorCustomData& asDoorCustomData(); + virtual const MWClass::DoorCustomData& asDoorCustomData() const; - virtual MWClass::DoorCustomData& asDoorCustomData(); - virtual const MWClass::DoorCustomData& asDoorCustomData() const; + virtual MWClass::CreatureLevListCustomData& asCreatureLevListCustomData(); + virtual const MWClass::CreatureLevListCustomData& asCreatureLevListCustomData() const; - virtual MWClass::CreatureLevListCustomData& asCreatureLevListCustomData(); - virtual const MWClass::CreatureLevListCustomData& asCreatureLevListCustomData() const; + virtual MWClass::ESM4NpcCustomData& asESM4NpcCustomData(); + virtual const MWClass::ESM4NpcCustomData& asESM4NpcCustomData() const; }; template struct TypedCustomData : CustomData { - std::unique_ptr clone() const final - { - return std::make_unique(*static_cast(this)); - } + std::unique_ptr clone() const final { return std::make_unique(*static_cast(this)); } }; } diff --git a/apps/openmw/mwworld/datetimemanager.cpp b/apps/openmw/mwworld/datetimemanager.cpp index 0894c974d3a..69374a77a90 100644 --- a/apps/openmw/mwworld/datetimemanager.cpp +++ b/apps/openmw/mwworld/datetimemanager.cpp @@ -1,9 +1,14 @@ #include "datetimemanager.hpp" +#include + #include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" -#include "esmstore.hpp" +#include "duration.hpp" #include "globals.hpp" #include "timestamp.hpp" @@ -13,21 +18,33 @@ namespace { switch (month) { - case 0: return 31; - case 1: return 28; - case 2: return 31; - case 3: return 30; - case 4: return 31; - case 5: return 30; - case 6: return 31; - case 7: return 31; - case 8: return 30; - case 9: return 31; - case 10: return 30; - case 11: return 31; + case 0: + return 31; + case 1: + return 28; + case 2: + return 31; + case 3: + return 30; + case 4: + return 31; + case 5: + return 30; + case 6: + return 31; + case 7: + return 31; + case 8: + return 30; + case 9: + return 31; + case 10: + return 30; + case 11: + return 31; } - throw std::runtime_error ("month out of range"); + throw std::runtime_error("month out of range"); } } @@ -35,12 +52,15 @@ namespace MWWorld { void DateTimeManager::setup(Globals& globalVariables) { - mGameHour = globalVariables["gamehour"].getFloat(); - mDaysPassed = globalVariables["dayspassed"].getInteger(); - mDay = globalVariables["day"].getInteger(); - mMonth = globalVariables["month"].getInteger(); - mYear = globalVariables["year"].getInteger(); - mTimeScale = globalVariables["timescale"].getFloat(); + mGameHour = globalVariables[Globals::sGameHour].getFloat(); + mDaysPassed = globalVariables[Globals::sDaysPassed].getInteger(); + mDay = globalVariables[Globals::sDay].getInteger(); + mMonth = globalVariables[Globals::sMonth].getInteger(); + mYear = globalVariables[Globals::sYear].getInteger(); + mGameTimeScale = globalVariables[Globals::sTimeScale].getFloat(); + setSimulationTimeScale(1.0); + mPaused = false; + mPausedTags.clear(); } void DateTimeManager::setHour(double hour) @@ -48,11 +68,11 @@ namespace MWWorld if (hour < 0) hour = 0; - int days = static_cast(hour / 24); - hour = std::fmod(hour, 24); - mGameHour = static_cast(hour); + const Duration duration = Duration::fromHours(hour); - if (days > 0) + mGameHour = duration.getHours(); + + if (const int days = duration.getDays(); days > 0) setDay(days + mDay); } @@ -90,9 +110,9 @@ namespace MWWorld return TimeStamp(mGameHour, mDaysPassed); } - float DateTimeManager::getTimeScaleFactor() const + void DateTimeManager::setGameTimeScale(float scale) { - return mTimeScale; + MWBase::Environment::get().getWorld()->setGlobalFloat(MWWorld::Globals::sTimeScale, scale); } ESM::EpochTimeStamp DateTimeManager::getEpochTimeStamp() const @@ -132,59 +152,63 @@ namespace MWWorld if (days > 0) mDaysPassed += days; - globalVariables["gamehour"].setFloat(mGameHour); - globalVariables["dayspassed"].setInteger(mDaysPassed); - globalVariables["day"].setInteger(mDay); - globalVariables["month"].setInteger(mMonth); - globalVariables["year"].setInteger(mYear); + globalVariables[Globals::sGameHour].setFloat(mGameHour); + globalVariables[Globals::sDaysPassed].setInteger(mDaysPassed); + globalVariables[Globals::sDay].setInteger(mDay); + globalVariables[Globals::sMonth].setInteger(mMonth); + globalVariables[Globals::sYear].setInteger(mYear); } - std::string DateTimeManager::getMonthName(int month) const + static std::vector getMonthNames() { - if (month == -1) - month = mMonth; - - const int months = 12; - if (month < 0 || month >= months) - return std::string(); + auto calendarL10n = MWBase::Environment::get().getL10nManager()->getContext("Calendar"); + std::string prefix = "month"; + std::vector months; + int count = 12; + months.reserve(count); + for (int i = 1; i <= count; ++i) + months.push_back(calendarL10n->formatMessage(prefix + std::to_string(i), {}, {})); + return months; + } - static const char *monthNames[months] = - { - "sMonthMorningstar", "sMonthSunsdawn", "sMonthFirstseed", "sMonthRainshand", - "sMonthSecondseed", "sMonthMidyear", "sMonthSunsheight", "sMonthLastseed", - "sMonthHeartfire", "sMonthFrostfall", "sMonthSunsdusk", "sMonthEveningstar" - }; + std::string_view DateTimeManager::getMonthName(int month) const + { + static std::vector months = getMonthNames(); - const ESM::GameSetting *setting = MWBase::Environment::get().getWorld()->getStore().get().find(monthNames[month]); - return setting->mValue.getString(); + if (month == -1) + month = mMonth; + if (month < 0 || month >= static_cast(months.size())) + return {}; + else + return months[month]; } - bool DateTimeManager::updateGlobalFloat(const std::string& name, float value) + bool DateTimeManager::updateGlobalFloat(GlobalVariableName name, float value) { - if (name=="gamehour") + if (name == Globals::sGameHour) { setHour(value); return true; } - else if (name=="day") + else if (name == Globals::sDay) { setDay(static_cast(value)); return true; } - else if (name=="month") + else if (name == Globals::sMonth) { setMonth(static_cast(value)); return true; } - else if (name=="year") + else if (name == Globals::sYear) { mYear = static_cast(value); } - else if (name=="timescale") + else if (name == Globals::sTimeScale) { - mTimeScale = value; + mGameTimeScale = value; } - else if (name=="dayspassed") + else if (name == Globals::sDaysPassed) { mDaysPassed = static_cast(value); } @@ -192,36 +216,57 @@ namespace MWWorld return false; } - bool DateTimeManager::updateGlobalInt(const std::string& name, int value) + bool DateTimeManager::updateGlobalInt(GlobalVariableName name, int value) { - if (name=="gamehour") + if (name == Globals::sGameHour) { setHour(static_cast(value)); return true; } - else if (name=="day") + else if (name == Globals::sDay) { setDay(value); return true; } - else if (name=="month") + else if (name == Globals::sMonth) { setMonth(value); return true; } - else if (name=="year") + else if (name == Globals::sYear) { mYear = value; } - else if (name=="timescale") + else if (name == Globals::sTimeScale) { - mTimeScale = static_cast(value); + mGameTimeScale = static_cast(value); } - else if (name=="dayspassed") + else if (name == Globals::sDaysPassed) { mDaysPassed = value; } return false; } + + void DateTimeManager::setSimulationTimeScale(float scale) + { + mSimulationTimeScale = std::max(0.f, scale); + MWBase::Environment::get().getSoundManager()->setSimulationTimeScale(mSimulationTimeScale); + } + + void DateTimeManager::unpause(std::string_view tag) + { + auto it = mPausedTags.find(tag); + if (it != mPausedTags.end()) + mPausedTags.erase(it); + } + + void DateTimeManager::updateIsPaused() + { + auto stateManager = MWBase::Environment::get().getStateManager(); + auto wm = MWBase::Environment::get().getWindowManager(); + mPaused = !mPausedTags.empty() || wm->isConsoleMode() || wm->isPostProcessorHudVisible() + || wm->isInteractiveMessageBoxActive() || stateManager->getState() == MWBase::StateManager::State_NoGame; + } } diff --git a/apps/openmw/mwworld/datetimemanager.hpp b/apps/openmw/mwworld/datetimemanager.hpp index b460be746a3..af62d9ba3f8 100644 --- a/apps/openmw/mwworld/datetimemanager.hpp +++ b/apps/openmw/mwworld/datetimemanager.hpp @@ -1,7 +1,10 @@ #ifndef GAME_MWWORLD_DATETIMEMANAGER_H #define GAME_MWWORLD_DATETIMEMANAGER_H -#include +#include +#include + +#include "globalvariablename.hpp" namespace ESM { @@ -12,31 +15,63 @@ namespace MWWorld { class Globals; class TimeStamp; + class World; class DateTimeManager { - int mDaysPassed = 0; - int mDay = 0; - int mMonth = 0; - int mYear = 0; - float mGameHour = 0.f; - float mTimeScale = 0.f; - - void setHour(double hour); - void setDay(int day); - void setMonth(int month); - public: - std::string getMonthName(int month) const; + // Game time. + // Note that game time generally goes faster than the simulation time. + std::string_view getMonthName(int month = -1) const; // -1: current month TimeStamp getTimeStamp() const; ESM::EpochTimeStamp getEpochTimeStamp() const; - float getTimeScaleFactor() const; + double getGameTime() const { return (static_cast(mDaysPassed) * 24 + mGameHour) * 3600.0; } + float getGameTimeScale() const { return mGameTimeScale; } + void setGameTimeScale(float scale); // game time to simulation time ratio - void advanceTime(double hours, Globals& globalVariables); + // Rendering simulation time (summary simulation time of rendering frames since application start). + double getRenderingSimulationTime() const { return mRenderingSimulationTime; } + void setRenderingSimulationTime(double t) { mRenderingSimulationTime = t; } + + // World simulation time (the number of seconds passed from the beginning of the game). + double getSimulationTime() const { return mSimulationTime; } + void setSimulationTime(double t) { mSimulationTime = t; } + float getSimulationTimeScale() const { return mSimulationTimeScale; } + void setSimulationTimeScale(float scale); // simulation time to real time ratio + + // Whether the game is paused in the current frame. + bool isPaused() const { return mPaused; } + // Pauses the game starting from the next frame until `unpause` is called with the same tag. + void pause(std::string_view tag) { mPausedTags.emplace(tag); } + void unpause(std::string_view tag); + const std::set>& getPausedTags() const { return mPausedTags; } + + // Updates mPaused; should be called once a frame. + void updateIsPaused(); + + private: + friend class World; void setup(Globals& globalVariables); - bool updateGlobalInt(const std::string& name, int value); - bool updateGlobalFloat(const std::string& name, float value); + bool updateGlobalInt(GlobalVariableName name, int value); + bool updateGlobalFloat(GlobalVariableName name, float value); + void advanceTime(double hours, Globals& globalVariables); + + void setHour(double hour); + void setDay(int day); + void setMonth(int month); + + int mDaysPassed = 0; + int mDay = 0; + int mMonth = 0; + int mYear = 0; + float mGameHour = 0.f; + float mGameTimeScale = 0.f; + float mSimulationTimeScale = 1.0; + double mRenderingSimulationTime = 0.0; + double mSimulationTime = 0.0; + bool mPaused = false; + std::set> mPausedTags; }; } diff --git a/apps/openmw/mwworld/duration.hpp b/apps/openmw/mwworld/duration.hpp new file mode 100644 index 00000000000..78693ca0dd1 --- /dev/null +++ b/apps/openmw/mwworld/duration.hpp @@ -0,0 +1,39 @@ +#ifndef GAME_MWWORLD_DURATION_H +#define GAME_MWWORLD_DURATION_H + +#include +#include + +namespace MWWorld +{ + inline const double maxFloatHour = static_cast(std::nextafter(24.0f, 0.0f)); + + class Duration + { + public: + static Duration fromHours(double hours) + { + if (hours < 0) + throw std::runtime_error("Negative hours is not supported Duration"); + + return Duration( + static_cast(hours / 24), static_cast(std::min(std::fmod(hours, 24), maxFloatHour))); + } + + int getDays() const { return mDays; } + + float getHours() const { return mHours; } + + private: + int mDays; + float mHours; + + explicit Duration(int days, float hours) + : mDays(days) + , mHours(hours) + { + } + }; +} + +#endif diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index b12d646e702..0be90c65f05 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -1,31 +1,79 @@ #include "esmloader.hpp" #include "esmstore.hpp" -#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" namespace MWWorld { -EsmLoader::EsmLoader(MWWorld::ESMStore& store, std::vector& readers, - ToUTF8::Utf8Encoder* encoder, Loading::Listener& listener) - : ContentLoader(listener) - , mEsm(readers) - , mStore(store) - , mEncoder(encoder) -{ -} + EsmLoader::EsmLoader(MWWorld::ESMStore& store, ESM::ReadersCache& readers, ToUTF8::Utf8Encoder* encoder, + std::vector& esmVersions) + : mReaders(readers) + , mStore(store) + , mEncoder(encoder) + , mDialogue(nullptr) // A content file containing INFO records without a DIAL record appends them to the + // previous file's dialogue + , mESMVersions(esmVersions) + { + } -void EsmLoader::load(const boost::filesystem::path& filepath, int& index) -{ - ContentLoader::load(filepath.filename(), index); - - ESM::ESMReader lEsm; - lEsm.setEncoder(mEncoder); - lEsm.setIndex(index); - lEsm.setGlobalReaderList(&mEsm); - lEsm.open(filepath.string()); - mEsm[index] = lEsm; - mStore.load(mEsm[index], &mListener); -} + void EsmLoader::load(const std::filesystem::path& filepath, int& index, Loading::Listener* listener) + { + + auto stream = Files::openBinaryInputFileStream(filepath); + const ESM::Format format = ESM::readFormat(*stream); + stream->seekg(0); + + switch (format) + { + case ESM::Format::Tes3: + { + const ESM::ReadersCache::BusyItem reader = mReaders.get(static_cast(index)); + reader->setEncoder(mEncoder); + reader->setIndex(index); + reader->open(filepath); + reader->resolveParentFileIndices(mReaders); + + assert(reader->getGameFiles().size() == reader->getParentFileIndices().size()); + for (std::size_t i = 0, n = reader->getParentFileIndices().size(); i < n; ++i) + if (i == static_cast(reader->getIndex())) + throw std::runtime_error("File " + Files::pathToUnicodeString(reader->getName()) + " asks for parent file " + + reader->getGameFiles()[i].name + + ", but it is not available or has been loaded in the wrong order. " + "Please run the launcher to fix this issue."); + + mESMVersions[index] = reader->getVer(); + mStore.load(*reader, listener, mDialogue); + + if (!mMasterFileFormat.has_value() + && (Misc::StringUtils::ciEndsWith(reader->getName().u8string(), u8".esm") + || Misc::StringUtils::ciEndsWith(reader->getName().u8string(), u8".omwgame"))) + mMasterFileFormat = reader->getFormatVersion(); + break; + } + case ESM::Format::Tes4: + { + ESM4::Reader reader(std::move(stream), filepath, + MWBase::Environment::get().getResourceSystem()->getVFS(), + mEncoder != nullptr ? &mEncoder->getStatelessEncoder() : nullptr); + reader.setModIndex(index); + reader.updateModIndices(mNameToIndex); + mStore.loadESM4(reader); + break; + } + } + mNameToIndex[Misc::StringUtils::lowerCase(Files::pathToUnicodeString(filepath.filename()))] = index; + } } /* namespace MWWorld */ diff --git a/apps/openmw/mwworld/esmloader.hpp b/apps/openmw/mwworld/esmloader.hpp index 506105bebb9..53bff939c42 100644 --- a/apps/openmw/mwworld/esmloader.hpp +++ b/apps/openmw/mwworld/esmloader.hpp @@ -1,37 +1,46 @@ #ifndef ESMLOADER_HPP #define ESMLOADER_HPP +#include +#include #include #include "contentloader.hpp" namespace ToUTF8 { - class Utf8Encoder; + class Utf8Encoder; } namespace ESM { - class ESMReader; + class ReadersCache; + struct Dialogue; } namespace MWWorld { -class ESMStore; + class ESMStore; -struct EsmLoader : public ContentLoader -{ - EsmLoader(MWWorld::ESMStore& store, std::vector& readers, - ToUTF8::Utf8Encoder* encoder, Loading::Listener& listener); + struct EsmLoader : public ContentLoader + { + explicit EsmLoader(MWWorld::ESMStore& store, ESM::ReadersCache& readers, ToUTF8::Utf8Encoder* encoder, + std::vector& esmVersions); + + std::optional getMasterFileFormat() const { return mMasterFileFormat; } - void load(const boost::filesystem::path& filepath, int& index) override; + void load(const std::filesystem::path& filepath, int& index, Loading::Listener* listener) override; private: - std::vector& mEsm; - MWWorld::ESMStore& mStore; - ToUTF8::Utf8Encoder* mEncoder; -}; + ESM::ReadersCache& mReaders; + MWWorld::ESMStore& mStore; + ToUTF8::Utf8Encoder* mEncoder; + ESM::Dialogue* mDialogue; + std::optional mMasterFileFormat; + std::vector& mESMVersions; + std::map mNameToIndex; + }; } /* namespace MWWorld */ diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index b1885edafca..15a687f4d79 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -1,14 +1,21 @@ #include "esmstore.hpp" #include -#include - -#include +#include +#include #include + +#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include +#include #include #include "../mwmechanics/spelllist.hpp" @@ -20,54 +27,78 @@ namespace ESM::RefNum mRefNum; std::size_t mRefID; - Ref(ESM::RefNum refNum, std::size_t refID) : mRefNum(refNum), mRefID(refID) {} + Ref(ESM::RefNum refNum, std::size_t refID) + : mRefNum(refNum) + , mRefID(refID) + { + } }; constexpr std::size_t deletedRefID = std::numeric_limits::max(); - void readRefs(const ESM::Cell& cell, std::vector& refs, std::vector& refIDs, std::vector& readers) + void readRefs(const ESM::Cell& cell, std::vector& refs, std::vector& refIDs, + std::set& keyIDs, ESM::ReadersCache& readers) { + // TODO: we have many similar copies of this code. for (size_t i = 0; i < cell.mContextList.size(); i++) { - size_t index = cell.mContextList[i].index; - if (readers.size() <= index) - readers.resize(index + 1); - cell.restore(readers[index], i); + const std::size_t index = static_cast(cell.mContextList[i].index); + const ESM::ReadersCache::BusyItem reader = readers.get(index); + cell.restore(*reader, i); ESM::CellRef ref; - ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; bool deleted = false; - while(cell.getNextRef(readers[index], ref, deleted)) + while (cell.getNextRef(*reader, ref, deleted)) { - if(deleted) + if (deleted) refs.emplace_back(ref.mRefNum, deletedRefID); - else if (std::find(cell.mMovedRefs.begin(), cell.mMovedRefs.end(), ref.mRefNum) == cell.mMovedRefs.end()) + else if (std::find(cell.mMovedRefs.begin(), cell.mMovedRefs.end(), ref.mRefNum) + == cell.mMovedRefs.end()) { + if (!ref.mKey.empty()) + keyIDs.insert(std::move(ref.mKey)); refs.emplace_back(ref.mRefNum, refIDs.size()); refIDs.push_back(std::move(ref.mRefID)); } } } - for(const auto& [value, deleted] : cell.mLeasedRefs) + for (const auto& [value, deleted] : cell.mLeasedRefs) { - if(deleted) + if (deleted) refs.emplace_back(value.mRefNum, deletedRefID); else { + if (!value.mKey.empty()) + keyIDs.insert(std::move(value.mKey)); refs.emplace_back(value.mRefNum, refIDs.size()); refIDs.push_back(value.mRefID); } } } - std::vector getNPCsToReplace(const MWWorld::Store& factions, const MWWorld::Store& classes, const std::map& npcs) + const ESM::RefId& getDefaultClass(const MWWorld::Store& classes) { - // Cache first class from store - we will use it if current class is not found - std::string defaultCls; auto it = classes.begin(); if (it != classes.end()) - defaultCls = it->mId; - else - throw std::runtime_error("List of NPC classes is empty!"); + return it->mId; + throw std::runtime_error("List of NPC classes is empty!"); + } + + const ESM::RefId& getDefaultRace(const MWWorld::Store& races) + { + auto it = races.begin(); + if (it != races.end()) + return it->mId; + throw std::runtime_error("List of NPC races is empty!"); + } + + std::vector getNPCsToReplace(const MWWorld::Store& factions, + const MWWorld::Store& classes, const MWWorld::Store& races, + const MWWorld::Store& scripts, const std::unordered_map& npcs) + { + // Cache first class from store - we will use it if current class is not found + const ESM::RefId& defaultCls = getDefaultClass(classes); + // Same for races + const ESM::RefId& defaultRace = getDefaultRace(races); // Validate NPCs for non-existing class and faction. // We will replace invalid entries by fixed ones @@ -78,29 +109,44 @@ namespace ESM::NPC npc = npcIter.second; bool changed = false; - const std::string npcFaction = npc.mFaction; + const ESM::RefId& npcFaction = npc.mFaction; if (!npcFaction.empty()) { - const ESM::Faction *fact = factions.search(npcFaction); + const ESM::Faction* fact = factions.search(npcFaction); if (!fact) { - Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent faction '" << npc.mFaction << "', ignoring it."; - npc.mFaction.clear(); + Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent faction " + << npc.mFaction << ", ignoring it."; + npc.mFaction = ESM::RefId(); npc.mNpdt.mRank = 0; changed = true; } } - std::string npcClass = npc.mClass; - if (!npcClass.empty()) + const ESM::Class* cls = classes.search(npc.mClass); + if (!cls) { - const ESM::Class *cls = classes.search(npcClass); - if (!cls) - { - Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent class '" << npc.mClass << "', using '" << defaultCls << "' class as replacement."; - npc.mClass = defaultCls; - changed = true; - } + Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent class " + << npc.mClass << ", using " << defaultCls << " class as replacement."; + npc.mClass = defaultCls; + changed = true; + } + + const ESM::Race* race = races.search(npc.mRace); + if (!race) + { + Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent race " << npc.mRace + << ", using " << defaultRace << " race as replacement."; + npc.mRace = defaultRace; + changed = true; + } + + if (!npc.mScript.empty() && !scripts.search(npc.mScript)) + { + Log(Debug::Verbose) << "NPC " << npc.mId << " (" << npc.mName << ") has nonexistent script " + << npc.mScript << ", ignoring it."; + npc.mScript = ESM::RefId(); + changed = true; } if (changed) @@ -109,286 +155,539 @@ namespace return npcsToReplace; } -} -namespace MWWorld -{ - -static bool isCacheableRecord(int id) -{ - if (id == ESM::REC_ACTI || id == ESM::REC_ALCH || id == ESM::REC_APPA || id == ESM::REC_ARMO || - id == ESM::REC_BOOK || id == ESM::REC_CLOT || id == ESM::REC_CONT || id == ESM::REC_CREA || - id == ESM::REC_DOOR || id == ESM::REC_INGR || id == ESM::REC_LEVC || id == ESM::REC_LEVI || - id == ESM::REC_LIGH || id == ESM::REC_LOCK || id == ESM::REC_MISC || id == ESM::REC_NPC_ || - id == ESM::REC_PROB || id == ESM::REC_REPA || id == ESM::REC_STAT || id == ESM::REC_WEAP || - id == ESM::REC_BODY) - { - return true; - } - return false; -} - -void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) -{ - listener->setProgressRange(1000); - - ESM::Dialogue *dialogue = nullptr; - - // Land texture loading needs to use a separate internal store for each plugin. - // We set the number of plugins here to avoid continual resizes during loading, - // and so we can properly verify if valid plugin indices are being passed to the - // LandTexture Store retrieval methods. - mLandTextures.resize(esm.getGlobalReaderList()->size()); - - /// \todo Move this to somewhere else. ESMReader? - // Cache parent esX files by tracking their indices in the global list of - // all files/readers used by the engine. This will greaty accelerate - // refnumber mangling, as required for handling moved references. - const std::vector &masters = esm.getGameFiles(); - std::vector *allPlugins = esm.getGlobalReaderList(); - for (size_t j = 0; j < masters.size(); j++) { - const ESM::Header::MasterData &mast = masters[j]; - std::string fname = mast.name; - int index = ~0; - for (int i = 0; i < esm.getIndex(); i++) { - const std::string candidate = allPlugins->at(i).getContext().filename; - std::string fnamecandidate = boost::filesystem::path(candidate).filename().string(); - if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) { - index = i; - break; - } - } - if (index == (int)~0) { - // Tried to load a parent file that has not been loaded yet. This is bad, - // the launcher should have taken care of this. - std::string fstring = "File " + esm.getName() + " asks for parent file " + masters[j].name - + ", but it has not been loaded yet. Please check your load order."; - esm.fail(fstring); - } - esm.addParentFileIndex(index); - } - - // Loop through all records - while(esm.hasMoreRecs()) + template + std::vector getSpellsToReplace( + const MWWorld::Store& spells, const MWWorld::Store& magicEffects) { - ESM::NAME n = esm.getRecName(); - esm.getRecHeader(); + std::vector spellsToReplace; - // Look up the record type. - std::map::iterator it = mStores.find(n.intval); + for (RecordType spell : spells) + { + if (spell.mEffects.mList.empty()) + continue; - if (it == mStores.end()) { - if (n.intval == ESM::REC_INFO) { - if (dialogue) + bool changed = false; + auto iter = spell.mEffects.mList.begin(); + while (iter != spell.mEffects.mList.end()) + { + const ESM::MagicEffect* mgef = magicEffects.search(iter->mData.mEffectID); + if (!mgef) { - dialogue->readInfo(esm, esm.getIndex() != 0); + Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId + << ": dropping invalid effect (index " << iter->mData.mEffectID << ")"; + iter = spell.mEffects.mList.erase(iter); + changed = true; + continue; } - else + + if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) && iter->mData.mAttribute != -1) { - Log(Debug::Error) << "Error: info record without dialog"; - esm.skipRecord(); + iter->mData.mAttribute = -1; + Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId + << ": dropping unexpected attribute argument of " + << ESM::MagicEffect::indexToGmstString(iter->mData.mEffectID) << " effect"; + changed = true; } - } else if (n.intval == ESM::REC_MGEF) { - mMagicEffects.load (esm); - } else if (n.intval == ESM::REC_SKIL) { - mSkills.load (esm); - } - else if (n.intval==ESM::REC_FILT || n.intval == ESM::REC_DBGP) - { - // ignore project file only records - esm.skipRecord(); - } - else { - std::stringstream error; - error << "Unknown record: " << n.toString(); - throw std::runtime_error(error.str()); - } - } else { - RecordId id = it->second->load(esm); - if (id.mIsDeleted) - { - it->second->eraseStatic(id.mId); - continue; + + if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) && iter->mData.mSkill != -1) + { + iter->mData.mSkill = -1; + Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId + << ": dropping unexpected skill argument of " + << ESM::MagicEffect::indexToGmstString(iter->mData.mEffectID) << " effect"; + changed = true; + } + + ++iter; } - if (n.intval==ESM::REC_DIAL) { - dialogue = const_cast(mDialogs.find(id.mId)); - } else { - dialogue = nullptr; + if (changed) + spellsToReplace.emplace_back(spell); + } + + return spellsToReplace; + } + + // Custom enchanted items can reference scripts that no longer exist, this doesn't necessarily mean the base item no + // longer exists however. So instead of removing the item altogether, we're only removing the script. + template + void removeMissingScripts(const MWWorld::Store& scripts, MapT& items) + { + for (auto& [id, item] : items) + { + if (!item.mScript.empty() && !scripts.search(item.mScript)) + { + Log(Debug::Verbose) << MapT::mapped_type::getRecordType() << ' ' << id << " (" << item.mName + << ") has nonexistent script " << item.mScript << ", ignoring it."; + item.mScript = ESM::RefId(); } } - listener->setProgress(static_cast(esm.getFileOffset() / (float)esm.getFileSize() * 1000)); } } -void ESMStore::setUp(bool validateRecords) +namespace MWWorld { - mIds.clear(); + using IDMap = std::unordered_map; + + struct ESMStoreImp + { + ESMStore::StoreTuple mStores; - std::map::iterator storeIt = mStores.begin(); - for (; storeIt != mStores.end(); ++storeIt) { - storeIt->second->setUp(); + std::map mRecNameToStore; - if (isCacheableRecord(storeIt->first)) + // Lookup of all IDs. Makes looking up references faster. Just + // maps the id name to the record type. + IDMap mIds; + IDMap mStaticIds; + + template + static void assignStoreToIndex(ESMStore& stores, Store& store) { - std::vector identifiers; - storeIt->second->listIdentifier(identifiers); + const std::size_t storeIndex = ESMStore::getTypeIndex(); + if (stores.mStores.size() <= storeIndex) + stores.mStores.resize(storeIndex + 1); + + assert(&store == &std::get>(stores.mStoreImp->mStores)); - for (std::vector::const_iterator record = identifiers.begin(); record != identifiers.end(); ++record) - mIds[*record] = storeIt->first; + stores.mStores[storeIndex] = &store; + if constexpr (std::is_convertible_v*, DynamicStore*>) + { + stores.mDynamicStores.push_back(&store); + constexpr ESM::RecNameInts recName = T::sRecordId; + if constexpr (recName != ESM::REC_INTERNAL_PLAYER) + { + stores.mStoreImp->mRecNameToStore[recName] = &store; + } + } } - } - if (mStaticIds.empty()) - mStaticIds = mIds; + template + static bool typedReadRecordESM4(ESM4::Reader& reader, Store& store) + { + auto recordType = static_cast(reader.hdr().record.typeId); + + ESM::RecNameInts esm4RecName = static_cast(ESM::esm4Recname(recordType)); + if constexpr (HasRecordId::value) + { + if constexpr (ESM::isESM4Rec(T::sRecordId)) + { + if (T::sRecordId == esm4RecName) + { + reader.getRecordData(); + T value; + value.load(reader); + store.insertStatic(value); + return true; + } + } + } + return false; + } - mSkills.setUp(); - mMagicEffects.setUp(); - mAttributes.setUp(); - mDialogs.setUp(); + static bool readRecord(ESM4::Reader& reader, ESMStore& store) + { + return std::apply( + [&reader](auto&... x) { return (typedReadRecordESM4(reader, x) || ...); }, store.mStoreImp->mStores); + } + }; - if (validateRecords) + int ESMStore::find(const ESM::RefId& id) const { - validate(); - countRecords(); + IDMap::const_iterator it = mStoreImp->mIds.find(id); + if (it == mStoreImp->mIds.end()) + { + return 0; + } + return it->second; } -} -void ESMStore::countRecords() -{ - if(!mRefCount.empty()) - return; - std::vector refs; - std::vector refIDs; - std::vector readers; - for(auto it = mCells.intBegin(); it != mCells.intEnd(); it++) - readRefs(*it, refs, refIDs, readers); - for(auto it = mCells.extBegin(); it != mCells.extEnd(); it++) - readRefs(*it, refs, refIDs, readers); - const auto lessByRefNum = [] (const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; }; - std::stable_sort(refs.begin(), refs.end(), lessByRefNum); - const auto equalByRefNum = [] (const Ref& l, const Ref& r) { return l.mRefNum == r.mRefNum; }; - const auto incrementRefCount = [&] (const Ref& value) - { - if (value.mRefID != deletedRefID) + int ESMStore::findStatic(const ESM::RefId& id) const + { + IDMap::const_iterator it = mStoreImp->mStaticIds.find(id); + if (it == mStoreImp->mStaticIds.end()) { - std::string& refId = refIDs[value.mRefID]; - Misc::StringUtils::lowerCaseInPlace(refId); - ++mRefCount[std::move(refId)]; + return 0; } - }; - Misc::forEachUnique(refs.rbegin(), refs.rend(), equalByRefNum, incrementRefCount); -} + return it->second; + } -int ESMStore::getRefCount(const std::string& id) const -{ - const std::string lowerId = Misc::StringUtils::lowerCase(id); - auto it = mRefCount.find(lowerId); - if(it == mRefCount.end()) - return 0; - return it->second; -} + ESMStore::ESMStore() + { + mStoreImp = std::make_unique(); + std::apply([this](auto&... x) { (ESMStoreImp::assignStoreToIndex(*this, x), ...); }, mStoreImp->mStores); + mDynamicCount = 0; + getWritable().setCells(getWritable()); + } -void ESMStore::validate() -{ - std::vector npcsToReplace = getNPCsToReplace(mFactions, mClasses, mNpcs.mStatic); + ESMStore::~ESMStore() = default; - for (const ESM::NPC &npc : npcsToReplace) + void ESMStore::clearDynamic() { - mNpcs.eraseStatic(npc.mId); - mNpcs.insertStatic(npc); + for (const auto& store : mDynamicStores) + store->clearDynamic(); + mStoreImp->mIds = mStoreImp->mStaticIds; + + movePlayerRecord(); } - // Validate spell effects for invalid arguments - std::vector spellsToReplace; - for (ESM::Spell spell : mSpells) + static bool isCacheableRecord(int id) { - if (spell.mEffects.mList.empty()) - continue; + switch (id) + { + case ESM::REC_ACTI: + case ESM::REC_ALCH: + case ESM::REC_APPA: + case ESM::REC_ARMO: + case ESM::REC_BOOK: + case ESM::REC_CLOT: + case ESM::REC_CONT: + case ESM::REC_CREA: + case ESM::REC_DOOR: + case ESM::REC_INGR: + case ESM::REC_LEVC: + case ESM::REC_LEVI: + case ESM::REC_LIGH: + case ESM::REC_LOCK: + case ESM::REC_MISC: + case ESM::REC_NPC_: + case ESM::REC_PROB: + case ESM::REC_REPA: + case ESM::REC_STAT: + case ESM::REC_WEAP: + case ESM::REC_BODY: + case ESM::REC_ACTI4: + case ESM::REC_ALCH4: + case ESM::REC_AMMO4: + case ESM::REC_ARMO4: + case ESM::REC_BOOK4: + case ESM::REC_CONT4: + case ESM::REC_CREA4: + case ESM::REC_DOOR4: + case ESM::REC_FLOR4: + case ESM::REC_FURN4: + case ESM::REC_IMOD4: + case ESM::REC_INGR4: + case ESM::REC_LIGH4: + case ESM::REC_LVLI4: + case ESM::REC_LVLC4: + case ESM::REC_LVLN4: + case ESM::REC_MISC4: + case ESM::REC_MSTT4: + case ESM::REC_NPC_4: + case ESM::REC_SCOL4: + case ESM::REC_STAT4: + case ESM::REC_TERM4: + case ESM::REC_TREE4: + case ESM::REC_WEAP4: + return true; + break; + } + return false; + } + + void ESMStore::load(ESM::ESMReader& esm, Loading::Listener* listener, ESM::Dialogue*& dialogue) + { + if (listener != nullptr) + listener->setProgressRange(::EsmLoader::fileProgress); - bool changed = false; - auto iter = spell.mEffects.mList.begin(); - while (iter != spell.mEffects.mList.end()) + // Loop through all records + while (esm.hasMoreRecs()) { - const ESM::MagicEffect* mgef = mMagicEffects.search(iter->mEffectID); - if (!mgef) + ESM::NAME n = esm.getRecName(); + esm.getRecHeader(); + if (esm.getRecordFlags() & ESM::FLAG_Ignored) { - Log(Debug::Verbose) << "Spell '" << spell.mId << "' has an invalid effect (index " << iter->mEffectID << ") present. Dropping the effect."; - iter = spell.mEffects.mList.erase(iter); - changed = true; + esm.skipRecord(); continue; } - if (mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) + // Look up the record type. + ESM::RecNameInts recName = static_cast(n.toInt()); + const auto& it = mStoreImp->mRecNameToStore.find(recName); + + if (it == mStoreImp->mRecNameToStore.end()) { - if (iter->mAttribute != -1) + if (recName == ESM::REC_INFO) { - iter->mAttribute = -1; - Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) << - " effect of spell '" << spell.mId << "' has an attribute argument present. Dropping the argument."; - changed = true; + if (dialogue) + { + dialogue->readInfo(esm); + } + else + { + Log(Debug::Error) << "Error: info record without dialog"; + esm.skipRecord(); + } + } + else if (n.toInt() == ESM::REC_MGEF) + { + getWritable().load(esm); + } + else if (n.toInt() == ESM::REC_SKIL) + { + getWritable().load(esm); + } + else if (n.toInt() == ESM::REC_FILT || n.toInt() == ESM::REC_DBGP) + { + // ignore project file only records + esm.skipRecord(); + } + else if (n.toInt() == ESM::REC_LUAL) + { + ESM::LuaScriptsCfg cfg; + cfg.load(esm); + cfg.adjustRefNums(esm); + mLuaContent.push_back(std::move(cfg)); + } + else + { + throw std::runtime_error("Unknown record: " + n.toString()); } } - else if (mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) + else { - if (iter->mSkill != -1) + RecordId id = it->second->load(esm); + if (id.mIsDeleted) { - iter->mSkill = -1; - Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) << - " effect of spell '" << spell.mId << "' has a skill argument present. Dropping the argument."; - changed = true; + it->second->eraseStatic(id.mId); + continue; + } + + if (n.toInt() == ESM::REC_DIAL) + { + dialogue = const_cast(getWritable().find(id.mId)); + } + else + { + dialogue = nullptr; + } + } + if (listener != nullptr) + listener->setProgress(::EsmLoader::fileProgress * esm.getFileOffset() / esm.getFileSize()); + } + } + + void ESMStore::loadESM4(ESM4::Reader& reader) + { + auto visitorRec = [this](ESM4::Reader& reader) { return ESMStoreImp::readRecord(reader, *this); }; + ESM4::ReaderUtils::readAll(reader, visitorRec, [](ESM4::Reader&) {}); + } + + void ESMStore::setIdType(const ESM::RefId& id, ESM::RecNameInts type) + { + mStoreImp->mIds[id] = type; + } + + ESM::LuaScriptsCfg ESMStore::getLuaScriptsCfg() const + { + ESM::LuaScriptsCfg cfg; + for (const LuaContent& c : mLuaContent) + { + if (std::holds_alternative(c)) + { + // *.omwscripts are intentionally reloaded every time when `getLuaScriptsCfg` is called. + // It is important for the `reloadlua` console command. + try + { + auto file = std::ifstream(std::get(c)); + std::string fileContent(std::istreambuf_iterator(file), {}); + LuaUtil::parseOMWScripts(cfg, fileContent); + } + catch (std::exception& e) + { + Log(Debug::Error) << e.what(); } } - else if (iter->mSkill != -1 || iter->mAttribute != -1) + else { - iter->mSkill = -1; - iter->mAttribute = -1; - Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) << - " effect of spell '" << spell.mId << "' has argument(s) present. Dropping the argument(s)."; - changed = true; + const ESM::LuaScriptsCfg& addition = std::get(c); + cfg.mScripts.insert(cfg.mScripts.end(), addition.mScripts.begin(), addition.mScripts.end()); + } + } + return cfg; + } + + void ESMStore::setUp() + { + if (mIsSetUpDone) + throw std::logic_error("ESMStore::setUp() is called twice"); + mIsSetUpDone = true; + + for (const auto& [_, store] : mStoreImp->mRecNameToStore) + store->setUp(); + + getWritable().setUp(get()); + getWritable().setUp(); + getWritable().setUp(get()); + getWritable().updateLandPositions(get()); + getWritable().preprocessReferences(get()); + getWritable().preprocessReferences(get()); + getWritable().preprocessReferences(get()); + + rebuildIdsIndex(); + mStoreImp->mStaticIds = mStoreImp->mIds; + } + + void ESMStore::rebuildIdsIndex() + { + mStoreImp->mIds.clear(); + for (const auto& [recordType, store] : mStoreImp->mRecNameToStore) + { + if (isCacheableRecord(recordType)) + { + std::vector identifiers; + store->listIdentifier(identifiers); + for (auto& record : identifiers) + mStoreImp->mIds[record] = recordType; + } + } + } + + void ESMStore::validateRecords(ESM::ReadersCache& readers) + { + validate(); + countAllCellRefsAndMarkKeys(readers); + } + + void ESMStore::countAllCellRefsAndMarkKeys(ESM::ReadersCache& readers) + { + // TODO: We currently need to read entire files here again. + // We should consider consolidating or deferring this reading. + if (!mRefCount.empty()) + return; + std::vector refs; + std::set keyIDs; + std::vector refIDs; + Store Cells = get(); + for (auto it = Cells.intBegin(); it != Cells.intEnd(); ++it) + readRefs(*it, refs, refIDs, keyIDs, readers); + for (auto it = Cells.extBegin(); it != Cells.extEnd(); ++it) + readRefs(*it, refs, refIDs, keyIDs, readers); + const auto lessByRefNum = [](const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; }; + std::stable_sort(refs.begin(), refs.end(), lessByRefNum); + const auto equalByRefNum = [](const Ref& l, const Ref& r) { return l.mRefNum == r.mRefNum; }; + const auto incrementRefCount = [&](const Ref& value) { + if (value.mRefID != deletedRefID) + { + ESM::RefId& refId = refIDs[value.mRefID]; + ++mRefCount[std::move(refId)]; } + }; + Misc::forEachUnique(refs.rbegin(), refs.rend(), equalByRefNum, incrementRefCount); + auto& store = getWritable().mStatic; + for (const auto& id : keyIDs) + { + auto it = store.find(id); + if (it != store.end()) + it->second.mData.mFlags |= ESM::Miscellaneous::Key; + } + } - ++iter; + int ESMStore::getRefCount(const ESM::RefId& id) const + { + auto it = mRefCount.find(id); + if (it == mRefCount.end()) + return 0; + return it->second; + } + + void ESMStore::validate() + { + auto& npcs = getWritable(); + std::vector npcsToReplace = getNPCsToReplace(getWritable(), getWritable(), + getWritable(), getWritable(), npcs.mStatic); + + for (const ESM::NPC& npc : npcsToReplace) + { + npcs.eraseStatic(npc.mId); + npcs.insertStatic(npc); + } + + removeMissingScripts(getWritable(), getWritable().mStatic); + + // Validate spell effects and enchantments for invalid arguments + auto& spells = getWritable(); + auto& enchantments = getWritable(); + auto& magicEffects = getWritable(); + + std::vector spellsToReplace = getSpellsToReplace(spells, magicEffects); + for (const ESM::Spell& spell : spellsToReplace) + { + spells.eraseStatic(spell.mId); + spells.insertStatic(spell); } - if (changed) - spellsToReplace.emplace_back(spell); + std::vector enchantmentsToReplace = getSpellsToReplace(enchantments, magicEffects); + for (const ESM::Enchantment& enchantment : enchantmentsToReplace) + { + enchantments.eraseStatic(enchantment.mId); + enchantments.insertStatic(enchantment); + } } - for (const ESM::Spell &spell : spellsToReplace) + void ESMStore::movePlayerRecord() { - mSpells.eraseStatic(spell.mId); - mSpells.insertStatic(spell); + auto& npcs = getWritable(); + auto player = npcs.find(ESM::RefId::stringRefId("Player")); + npcs.insert(*player); } -} -void ESMStore::validateDynamic() -{ - std::vector npcsToReplace = getNPCsToReplace(mFactions, mClasses, mNpcs.mDynamic); + void ESMStore::validateDynamic() + { + auto& npcs = getWritable(); + auto& scripts = getWritable(); - for (const ESM::NPC &npc : npcsToReplace) - mNpcs.insert(npc); -} + std::vector npcsToReplace = getNPCsToReplace(getWritable(), getWritable(), + getWritable(), getWritable(), npcs.mDynamic); + + for (const ESM::NPC& npc : npcsToReplace) + npcs.insert(npc); + + removeMissingScripts(scripts, getWritable().mDynamic); + removeMissingScripts(scripts, getWritable().mDynamic); + removeMissingScripts(scripts, getWritable().mDynamic); + removeMissingScripts(scripts, getWritable().mDynamic); + removeMissingScripts(scripts, getWritable().mDynamic); + + removeMissingObjects(getWritable()); + removeMissingObjects(getWritable()); + } + + // Leveled lists can be modified by scripts. This removes items that no longer exist (presumably because the + // plugin was removed) from modified lists + template + void ESMStore::removeMissingObjects(Store& store) + { + for (auto& entry : store.mDynamic) + { + auto first = std::remove_if(entry.second.mList.begin(), entry.second.mList.end(), [&](const auto& item) { + if (!find(item.mId)) + { + Log(Debug::Verbose) << "Leveled list " << entry.first << " has nonexistent object " << item.mId + << ", ignoring it."; + return true; + } + return false; + }); + entry.second.mList.erase(first, entry.second.mList.end()); + } + } int ESMStore::countSavedGameRecords() const { return 1 // DYNA (dynamic name counter) - +mPotions.getDynamicSize() - +mArmors.getDynamicSize() - +mBooks.getDynamicSize() - +mClasses.getDynamicSize() - +mClothes.getDynamicSize() - +mEnchants.getDynamicSize() - +mNpcs.getDynamicSize() - +mSpells.getDynamicSize() - +mWeapons.getDynamicSize() - +mCreatureLists.getDynamicSize() - +mItemLists.getDynamicSize() - +mCreatures.getDynamicSize() - +mContainers.getDynamicSize(); + + get().getDynamicSize() + get().getDynamicSize() + + get().getDynamicSize() + get().getDynamicSize() + + get().getDynamicSize() + get().getDynamicSize() + + get().getDynamicSize() + get().getDynamicSize() + + get().getDynamicSize() + get().getDynamicSize() + + get().getDynamicSize() + get().getDynamicSize() + + get().getDynamicSize() + get().getDynamicSize() + + get().getDynamicSize() + get().getDynamicSize(); } - void ESMStore::write (ESM::ESMWriter& writer, Loading::Listener& progress) const + void ESMStore::write(ESM::ESMWriter& writer, Loading::Listener& progress) const { writer.startRecord(ESM::REC_DYNA); writer.startSubRecord("COUN"); @@ -396,26 +695,32 @@ void ESMStore::validateDynamic() writer.endRecord("COUN"); writer.endRecord(ESM::REC_DYNA); - mPotions.write (writer, progress); - mArmors.write (writer, progress); - mBooks.write (writer, progress); - mClasses.write (writer, progress); - mClothes.write (writer, progress); - mEnchants.write (writer, progress); - mSpells.write (writer, progress); - mWeapons.write (writer, progress); - mNpcs.write (writer, progress); - mItemLists.write (writer, progress); - mCreatureLists.write (writer, progress); - mCreatures.write (writer, progress); - mContainers.write (writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); + get().write(writer, progress); } - bool ESMStore::readRecord (ESM::ESMReader& reader, uint32_t type) + bool ESMStore::readRecord(ESM::ESMReader& reader, uint32_t type_id) { + ESM::RecNameInts type = (ESM::RecNameInts)type_id; switch (type) { case ESM::REC_ALCH: + case ESM::REC_MISC: + case ESM::REC_ACTI: case ESM::REC_ARMO: case ESM::REC_BOOK: case ESM::REC_CLAS: @@ -425,17 +730,27 @@ void ESMStore::validateDynamic() case ESM::REC_WEAP: case ESM::REC_LEVI: case ESM::REC_LEVC: - mStores[type]->read (reader); + case ESM::REC_LIGH: + mStoreImp->mRecNameToStore[type]->read(reader); return true; case ESM::REC_NPC_: case ESM::REC_CREA: case ESM::REC_CONT: - mStores[type]->read (reader, true); + mStoreImp->mRecNameToStore[type]->read(reader, true); return true; case ESM::REC_DYNA: reader.getSubNameIs("COUN"); - reader.getHT(mDynamicCount); + if (reader.getFormatVersion() <= ESM::MaxActiveSpellTypeVersion) + { + uint32_t dynamicCount32 = 0; + reader.getHT(dynamicCount32); + mDynamicCount = dynamicCount32; + } + else + { + reader.getHT(mDynamicCount); + } return true; default: @@ -446,18 +761,14 @@ void ESMStore::validateDynamic() void ESMStore::checkPlayer() { - setUp(); - - const ESM::NPC *player = mNpcs.find ("player"); + const ESM::NPC* player = get().find(ESM::RefId::stringRefId("Player")); - if (!mRaces.find (player->mRace) || - !mClasses.find (player->mClass)) - throw std::runtime_error ("Invalid player record (race or class unavailable"); + if (!get().find(player->mRace) || !get().find(player->mClass)) + throw std::runtime_error("Invalid player record (race or class unavailable"); } - std::pair, bool> ESMStore::getSpellList(const std::string& originalId) const + std::pair, bool> ESMStore::getSpellList(const ESM::RefId& id) const { - const std::string id = Misc::StringUtils::lowerCase(originalId); auto result = mSpellListCache.find(id); std::shared_ptr ptr; if (result != mSpellListCache.end()) @@ -469,9 +780,37 @@ void ESMStore::validateDynamic() if (result != mSpellListCache.end()) result->second = ptr; else - mSpellListCache.insert({id, ptr}); - return {ptr, false}; + mSpellListCache.insert({ id, ptr }); + return { ptr, false }; + } + return { ptr, true }; + } + + template <> + const ESM::Cell* ESMStore::insert(const ESM::Cell& cell) + { + return getWritable().insert(cell); + } + + template <> + const ESM::NPC* ESMStore::insert(const ESM::NPC& npc) + { + + auto& npcs = getWritable(); + if (npc.mId == "Player") + { + return npcs.insert(npc); } - return {ptr, true}; + const ESM::RefId id = ESM::RefId::generated(mDynamicCount++); + if (npcs.search(id) != nullptr) + throw std::runtime_error("Try to override existing record: " + id.toDebugString()); + ESM::NPC record = npc; + + record.mId = id; + + ESM::NPC* ptr = npcs.insert(record); + mStoreImp->mIds[ptr->mId] = ESM::REC_NPC_; + return ptr; } + } // end namespace diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 608b5489eab..d8cfd1dcdf7 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -1,12 +1,17 @@ #ifndef OPENMW_MWWORLD_ESMSTORE_H #define OPENMW_MWWORLD_ESMSTORE_H +#include #include -#include #include +#include #include -#include +#include +#include +#include +#include + #include "store.hpp" namespace Loading @@ -19,497 +24,297 @@ namespace MWMechanics class SpellList; } +namespace ESM +{ + class ReadersCache; + class Script; + struct Activator; + struct Apparatus; + struct Armor; + struct Attribute; + struct BirthSign; + struct BodyPart; + struct Book; + struct Cell; + struct Class; + struct Clothing; + struct Container; + struct Creature; + struct CreatureLevList; + struct Dialogue; + struct Door; + struct Enchantment; + struct Faction; + struct GameSetting; + struct Global; + struct Ingredient; + struct ItemLevList; + struct Land; + struct LandTexture; + struct Light; + struct Lockpick; + struct MagicEffect; + struct Miscellaneous; + struct NPC; + struct Pathgrid; + struct Potion; + struct Probe; + struct Race; + struct Region; + struct Repair; + struct Skill; + struct Sound; + struct SoundGenerator; + struct Spell; + struct StartScript; + struct Static; + struct Weapon; +} + +namespace ESM4 +{ + class Reader; + struct Activator; + struct ActorCharacter; + struct ActorCreature; + struct Ammunition; + struct Armor; + struct ArmorAddon; + struct Book; + struct Cell; + struct Clothing; + struct Container; + struct Creature; + struct Door; + struct Flora; + struct Furniture; + struct Hair; + struct HeadPart; + struct Ingredient; + struct ItemMod; + struct Land; + struct LandTexture; + struct LevelledCreature; + struct LevelledItem; + struct LevelledNpc; + struct Light; + struct MiscItem; + struct MovableStatic; + struct Npc; + struct Outfit; + struct Potion; + struct Race; + struct Reference; + struct Static; + struct StaticCollection; + struct Terminal; + struct Tree; + struct Weapon; + struct World; +} + namespace MWWorld { + struct ESMStoreImp; + class ESMStore { - Store mActivators; - Store mPotions; - Store mAppas; - Store mArmors; - Store mBodyParts; - Store mBooks; - Store mBirthSigns; - Store mClasses; - Store mClothes; - Store mContainers; - Store mCreatures; - Store mDialogs; - Store mDoors; - Store mEnchants; - Store mFactions; - Store mGlobals; - Store mIngreds; - Store mCreatureLists; - Store mItemLists; - Store mLights; - Store mLockpicks; - Store mMiscItems; - Store mNpcs; - Store mProbes; - Store mRaces; - Store mRegions; - Store mRepairs; - Store mSoundGens; - Store mSounds; - Store mSpells; - Store mStartScripts; - Store mStatics; - Store mWeapons; - - Store mGameSettings; - Store mScripts; - - // Lists that need special rules - Store mCells; - Store mLands; - Store mLandTextures; - Store mPathgrids; - - Store mMagicEffects; - Store mSkills; - - // Special entry which is hardcoded and not loaded from an ESM - Store mAttributes; - - // Lookup of all IDs. Makes looking up references faster. Just - // maps the id name to the record type. - std::map mIds; - std::map mStaticIds; - - std::unordered_map mRefCount; - - std::map mStores; - - unsigned int mDynamicCount; - - mutable std::map > mSpellListCache; + friend struct ESMStoreImp; // This allows StoreImp to extend esmstore without beeing included everywhere + public: + using StoreTuple = std::tuple, 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, Store, Store, + Store, Store, Store, Store, + Store, + + // Lists that need special rules + Store, Store, Store, Store, + + Store, Store, + + // Special entry which is hardcoded and not loaded from an ESM + 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, + Store, Store, Store, Store, Store, + Store, Store, Store, Store, + Store, Store>; + + private: + template + static constexpr std::size_t getTypeIndex() + { + static_assert(Misc::TupleHasType, StoreTuple>::value); + return Misc::TupleTypeIndex, StoreTuple>::value; + } + + std::unique_ptr mStoreImp; + + std::unordered_map mRefCount; + + std::vector mStores; + std::vector mDynamicStores; + + uint64_t mDynamicCount; + + mutable std::unordered_map> mSpellListCache; + + template + Store& getWritable() + { + return static_cast&>(*mStores[getTypeIndex()]); + } /// Validate entries in store after setup void validate(); - void countRecords(); + void countAllCellRefsAndMarkKeys(ESM::ReadersCache& readers); + + template + void removeMissingObjects(Store& store); + + void setIdType(const ESM::RefId& id, ESM::RecNameInts type); + + using LuaContent = std::variant; // path to an omwscripts file + std::vector mLuaContent; + + bool mIsSetUpDone = false; + public: + void addOMWScripts(std::filesystem::path filePath) { mLuaContent.push_back(std::move(filePath)); } + ESM::LuaScriptsCfg getLuaScriptsCfg() const; + /// \todo replace with SharedIterator - typedef std::map::const_iterator iterator; + typedef std::vector::const_iterator iterator; - iterator begin() const { - return mStores.begin(); - } + iterator begin() const { return mDynamicStores.begin(); } - iterator end() const { - return mStores.end(); - } + iterator end() const { return mDynamicStores.end(); } /// Look up the given ID in 'all'. Returns 0 if not found. - /// \note id must be in lower case. - int find(const std::string &id) const - { - std::map::const_iterator it = mIds.find(id); - if (it == mIds.end()) { - return 0; - } - return it->second; - } - int findStatic(const std::string &id) const - { - std::map::const_iterator it = mStaticIds.find(id); - if (it == mStaticIds.end()) { - return 0; - } - return it->second; - } + int find(const ESM::RefId& id) const; - ESMStore() - : mDynamicCount(0) - { - mStores[ESM::REC_ACTI] = &mActivators; - mStores[ESM::REC_ALCH] = &mPotions; - mStores[ESM::REC_APPA] = &mAppas; - mStores[ESM::REC_ARMO] = &mArmors; - mStores[ESM::REC_BODY] = &mBodyParts; - mStores[ESM::REC_BOOK] = &mBooks; - mStores[ESM::REC_BSGN] = &mBirthSigns; - mStores[ESM::REC_CELL] = &mCells; - mStores[ESM::REC_CLAS] = &mClasses; - mStores[ESM::REC_CLOT] = &mClothes; - mStores[ESM::REC_CONT] = &mContainers; - mStores[ESM::REC_CREA] = &mCreatures; - mStores[ESM::REC_DIAL] = &mDialogs; - mStores[ESM::REC_DOOR] = &mDoors; - mStores[ESM::REC_ENCH] = &mEnchants; - mStores[ESM::REC_FACT] = &mFactions; - mStores[ESM::REC_GLOB] = &mGlobals; - mStores[ESM::REC_GMST] = &mGameSettings; - mStores[ESM::REC_INGR] = &mIngreds; - mStores[ESM::REC_LAND] = &mLands; - mStores[ESM::REC_LEVC] = &mCreatureLists; - mStores[ESM::REC_LEVI] = &mItemLists; - mStores[ESM::REC_LIGH] = &mLights; - mStores[ESM::REC_LOCK] = &mLockpicks; - mStores[ESM::REC_LTEX] = &mLandTextures; - mStores[ESM::REC_MISC] = &mMiscItems; - mStores[ESM::REC_NPC_] = &mNpcs; - mStores[ESM::REC_PGRD] = &mPathgrids; - mStores[ESM::REC_PROB] = &mProbes; - mStores[ESM::REC_RACE] = &mRaces; - mStores[ESM::REC_REGN] = &mRegions; - mStores[ESM::REC_REPA] = &mRepairs; - mStores[ESM::REC_SCPT] = &mScripts; - mStores[ESM::REC_SNDG] = &mSoundGens; - mStores[ESM::REC_SOUN] = &mSounds; - mStores[ESM::REC_SPEL] = &mSpells; - mStores[ESM::REC_SSCR] = &mStartScripts; - mStores[ESM::REC_STAT] = &mStatics; - mStores[ESM::REC_WEAP] = &mWeapons; - - mPathgrids.setCells(mCells); - } + int findStatic(const ESM::RefId& id) const; - void clearDynamic () - { - for (std::map::iterator it = mStores.begin(); it != mStores.end(); ++it) - it->second->clearDynamic(); + ESMStore(); + ~ESMStore(); - movePlayerRecord(); - } + void clearDynamic(); + void rebuildIdsIndex(); + ESM::RefId generateId() { return ESM::RefId::generated(mDynamicCount++); } - void movePlayerRecord () - { - auto player = mNpcs.find("player"); - mNpcs.insert(*player); - } + void movePlayerRecord(); /// Validate entries in store after loading a save void validateDynamic(); - void load(ESM::ESMReader &esm, Loading::Listener* listener); + void load(ESM::ESMReader& esm, Loading::Listener* listener, ESM::Dialogue*& dialogue); + void loadESM4(ESM4::Reader& esm); template - const Store &get() const { - throw std::runtime_error("Storage for this type not exist"); + const Store& get() const + { + return static_cast&>(*mStores[getTypeIndex()]); } /// Insert a custom record (i.e. with a generated ID that will not clash will pre-existing records) + /// \return pointer to created record template - const T *insert(const T &x) + const T* insert(const T& x) { - const std::string id = "$dynamic" + std::to_string(mDynamicCount++); + const ESM::RefId id = generateId(); - Store &store = const_cast &>(get()); + Store& store = getWritable(); if (store.search(id) != nullptr) - { - const std::string msg = "Try to override existing record '" + id + "'"; - throw std::runtime_error(msg); - } + throw std::runtime_error("Try to override existing record: " + id.toDebugString()); T record = x; record.mId = id; - T *ptr = store.insert(record); - for (iterator it = mStores.begin(); it != mStores.end(); ++it) { - if (it->second == &store) { - mIds[ptr->mId] = it->first; - } + T* ptr = store.insert(record); + if constexpr (std::is_convertible_v*, DynamicStore*>) + { + setIdType(ptr->mId, T::sRecordId); } return ptr; } /// Insert a record with set ID, and allow it to override a pre-existing static record. template - const T *overrideRecord(const T &x) { - Store &store = const_cast &>(get()); - - T *ptr = store.insert(x); - for (iterator it = mStores.begin(); it != mStores.end(); ++it) { - if (it->second == &store) { - mIds[ptr->mId] = it->first; - } + const T* overrideRecord(const T& x) + { + Store& store = getWritable(); + + T* ptr = store.insert(x); + if constexpr (std::is_convertible_v*, DynamicStore*>) + { + setIdType(ptr->mId, T::sRecordId); } return ptr; } template - const T *insertStatic(const T &x) + const T* insertStatic(const T& x) { - const std::string id = "$dynamic" + std::to_string(mDynamicCount++); + Store& store = getWritable(); + if (store.search(x.mId) != nullptr) + throw std::runtime_error("Try to override existing record " + x.mId.toDebugString()); - Store &store = const_cast &>(get()); - if (store.search(id) != nullptr) + T* ptr = store.insertStatic(x); + if constexpr (std::is_convertible_v*, DynamicStore*>) { - const std::string msg = "Try to override existing record '" + id + "'"; - throw std::runtime_error(msg); - } - T record = x; - - T *ptr = store.insertStatic(record); - for (iterator it = mStores.begin(); it != mStores.end(); ++it) { - if (it->second == &store) { - mIds[ptr->mId] = it->first; - } + setIdType(ptr->mId, T::sRecordId); } return ptr; } // This method must be called once, after loading all master/plugin files. This can only be done // from the outside, so it must be public. - void setUp(bool validateRecords = false); + void setUp(); + void validateRecords(ESM::ReadersCache& readers); int countSavedGameRecords() const; - void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; + void write(ESM::ESMWriter& writer, Loading::Listener& progress) const; - bool readRecord (ESM::ESMReader& reader, uint32_t type); + bool readRecord(ESM::ESMReader& reader, uint32_t type); ///< \return Known type? // To be called when we are done with dynamic record loading void checkPlayer(); /// @return The number of instances defined in the base files. Excludes changes from the save file. - int getRefCount(const std::string& id) const; + int getRefCount(const ESM::RefId& id) const; /// Actors with the same ID share spells, abilities, etc. /// @return The shared spell list to use for this actor and whether or not it has already been initialized. - std::pair, bool> getSpellList(const std::string& id) const; + std::pair, bool> getSpellList(const ESM::RefId& id) const; }; - template <> - inline const ESM::Cell *ESMStore::insert(const ESM::Cell &cell) { - return mCells.insert(cell); - } + const ESM::Cell* ESMStore::insert(const ESM::Cell& cell); template <> - inline const ESM::NPC *ESMStore::insert(const ESM::NPC &npc) - { - const std::string id = "$dynamic" + std::to_string(mDynamicCount++); + const ESM::NPC* ESMStore::insert(const ESM::NPC& npc); - if (Misc::StringUtils::ciEqual(npc.mId, "player")) - { - return mNpcs.insert(npc); - } - else if (mNpcs.search(id) != nullptr) - { - const std::string msg = "Try to override existing record '" + id + "'"; - throw std::runtime_error(msg); - } - ESM::NPC record = npc; - - record.mId = id; - - ESM::NPC *ptr = mNpcs.insert(record); - mIds[ptr->mId] = ESM::REC_NPC_; - return ptr; - } - - template <> - inline const Store &ESMStore::get() const { - return mActivators; - } - - template <> - inline const Store &ESMStore::get() const { - return mPotions; - } - - template <> - inline const Store &ESMStore::get() const { - return mAppas; - } - - template <> - inline const Store &ESMStore::get() const { - return mArmors; - } - - template <> - inline const Store &ESMStore::get() const { - return mBodyParts; - } - - template <> - inline const Store &ESMStore::get() const { - return mBooks; - } - - template <> - inline const Store &ESMStore::get() const { - return mBirthSigns; - } - - template <> - inline const Store &ESMStore::get() const { - return mClasses; - } - - template <> - inline const Store &ESMStore::get() const { - return mClothes; - } - - template <> - inline const Store &ESMStore::get() const { - return mContainers; - } - - template <> - inline const Store &ESMStore::get() const { - return mCreatures; - } - - template <> - inline const Store &ESMStore::get() const { - return mDialogs; - } - - template <> - inline const Store &ESMStore::get() const { - return mDoors; - } - - template <> - inline const Store &ESMStore::get() const { - return mEnchants; - } - - template <> - inline const Store &ESMStore::get() const { - return mFactions; - } - - template <> - inline const Store &ESMStore::get() const { - return mGlobals; - } - - template <> - inline const Store &ESMStore::get() const { - return mIngreds; - } - - template <> - inline const Store &ESMStore::get() const { - return mCreatureLists; - } - - template <> - inline const Store &ESMStore::get() const { - return mItemLists; - } - - template <> - inline const Store &ESMStore::get() const { - return mLights; - } - - template <> - inline const Store &ESMStore::get() const { - return mLockpicks; - } - - template <> - inline const Store &ESMStore::get() const { - return mMiscItems; - } - - template <> - inline const Store &ESMStore::get() const { - return mNpcs; - } - - template <> - inline const Store &ESMStore::get() const { - return mProbes; - } - - template <> - inline const Store &ESMStore::get() const { - return mRaces; - } - - template <> - inline const Store &ESMStore::get() const { - return mRegions; - } - - template <> - inline const Store &ESMStore::get() const { - return mRepairs; - } - - template <> - inline const Store &ESMStore::get() const { - return mSoundGens; - } - - template <> - inline const Store &ESMStore::get() const { - return mSounds; - } - - template <> - inline const Store &ESMStore::get() const { - return mSpells; - } - - template <> - inline const Store &ESMStore::get() const { - return mStartScripts; - } - - template <> - inline const Store &ESMStore::get() const { - return mStatics; - } - - template <> - inline const Store &ESMStore::get() const { - return mWeapons; - } - - template <> - inline const Store &ESMStore::get() const { - return mGameSettings; - } - - template <> - inline const Store &ESMStore::get() const { - return mScripts; - } - - template <> - inline const Store &ESMStore::get() const { - return mCells; - } - - template <> - inline const Store &ESMStore::get() const { - return mLands; - } - - template <> - inline const Store &ESMStore::get() const { - return mLandTextures; - } - - template <> - inline const Store &ESMStore::get() const { - return mPathgrids; - } - - template <> - inline const Store &ESMStore::get() const { - return mMagicEffects; - } - - template <> - inline const Store &ESMStore::get() const { - return mSkills; - } + template > + struct HasRecordId : std::false_type + { + }; - template <> - inline const Store &ESMStore::get() const { - return mAttributes; - } + template + struct HasRecordId> : std::true_type + { + }; } #endif diff --git a/apps/openmw/mwworld/failedaction.cpp b/apps/openmw/mwworld/failedaction.cpp index ec8314712ee..05bec120742 100644 --- a/apps/openmw/mwworld/failedaction.cpp +++ b/apps/openmw/mwworld/failedaction.cpp @@ -7,13 +7,15 @@ namespace MWWorld { - FailedAction::FailedAction(const std::string &msg, const Ptr& target) - : Action(false, target), mMessage(msg) - { } + FailedAction::FailedAction(std::string_view msg, const Ptr& target) + : Action(false, target) + , mMessage(msg) + { + } - void FailedAction::executeImp(const Ptr &actor) + void FailedAction::executeImp(const Ptr& actor) { - if(actor == MWMechanics::getPlayer() && !mMessage.empty()) + if (actor == MWMechanics::getPlayer() && !mMessage.empty()) MWBase::Environment::get().getWindowManager()->messageBox(mMessage); } } diff --git a/apps/openmw/mwworld/failedaction.hpp b/apps/openmw/mwworld/failedaction.hpp index 2a201cdb3bc..329a59443a3 100644 --- a/apps/openmw/mwworld/failedaction.hpp +++ b/apps/openmw/mwworld/failedaction.hpp @@ -8,12 +8,12 @@ namespace MWWorld { class FailedAction : public Action { - std::string mMessage; + std::string_view mMessage; - void executeImp(const Ptr &actor) override; + void executeImp(const Ptr& actor) override; public: - FailedAction(const std::string &message = std::string(), const Ptr& target = Ptr()); + FailedAction(std::string_view message = {}, const Ptr& target = Ptr()); }; } diff --git a/apps/openmw/mwworld/globals.cpp b/apps/openmw/mwworld/globals.cpp index 8a481334e83..4977df56c0e 100644 --- a/apps/openmw/mwworld/globals.cpp +++ b/apps/openmw/mwworld/globals.cpp @@ -2,35 +2,34 @@ #include -#include -#include -#include +#include +#include #include "esmstore.hpp" namespace MWWorld { - Globals::Collection::const_iterator Globals::find (const std::string& name) const + Globals::Collection::const_iterator Globals::find(std::string_view name) const { - Collection::const_iterator iter = mVariables.find (Misc::StringUtils::lowerCase (name)); + Collection::const_iterator iter = mVariables.find(name); - if (iter==mVariables.end()) - throw std::runtime_error ("unknown global variable: " + name); + if (iter == mVariables.end()) + throw std::runtime_error("unknown global variable: " + std::string{ name }); return iter; } - Globals::Collection::iterator Globals::find (const std::string& name) + Globals::Collection::iterator Globals::find(std::string_view name) { - Collection::iterator iter = mVariables.find (Misc::StringUtils::lowerCase (name)); + Collection::iterator iter = mVariables.find(name); - if (iter==mVariables.end()) - throw std::runtime_error ("unknown global variable: " + name); + if (iter == mVariables.end()) + throw std::runtime_error("unknown global variable: " + std::string{ name }); return iter; } - void Globals::fill (const MWWorld::ESMStore& store) + void Globals::fill(const MWWorld::ESMStore& store) { mVariables.clear(); @@ -38,34 +37,38 @@ namespace MWWorld for (const ESM::Global& esmGlobal : globals) { - mVariables.insert (std::make_pair (Misc::StringUtils::lowerCase (esmGlobal.mId), esmGlobal)); + mVariables.emplace(esmGlobal.mId, esmGlobal); } } - const ESM::Variant& Globals::operator[] (const std::string& name) const + const ESM::Variant& Globals::operator[](GlobalVariableName name) const { - return find (Misc::StringUtils::lowerCase (name))->second.mValue; + return find(name.getValue())->second.mValue; } - ESM::Variant& Globals::operator[] (const std::string& name) + ESM::Variant& Globals::operator[](GlobalVariableName name) { - return find (Misc::StringUtils::lowerCase (name))->second.mValue; + return find(name.getValue())->second.mValue; } - char Globals::getType (const std::string& name) const + char Globals::getType(GlobalVariableName name) const { - Collection::const_iterator iter = mVariables.find (Misc::StringUtils::lowerCase (name)); + Collection::const_iterator iter = mVariables.find(name.getValue()); - if (iter==mVariables.end()) + if (iter == mVariables.end()) return ' '; switch (iter->second.mValue.getType()) { - case ESM::VT_Short: return 's'; - case ESM::VT_Long: return 'l'; - case ESM::VT_Float: return 'f'; - - default: return ' '; + case ESM::VT_Short: + return 's'; + case ESM::VT_Long: + return 'l'; + case ESM::VT_Float: + return 'f'; + + default: + return ' '; } } @@ -74,19 +77,19 @@ namespace MWWorld return mVariables.size(); } - void Globals::write (ESM::ESMWriter& writer, Loading::Listener& progress) const + void Globals::write(ESM::ESMWriter& writer, Loading::Listener& progress) const { - for (Collection::const_iterator iter (mVariables.begin()); iter!=mVariables.end(); ++iter) + for (const auto& variable : mVariables) { - writer.startRecord (ESM::REC_GLOB); - iter->second.save (writer); - writer.endRecord (ESM::REC_GLOB); + writer.startRecord(ESM::REC_GLOB); + variable.second.save(writer); + writer.endRecord(ESM::REC_GLOB); } } - bool Globals::readRecord (ESM::ESMReader& reader, uint32_t type) + bool Globals::readRecord(ESM::ESMReader& reader, uint32_t type) { - if (type==ESM::REC_GLOB) + if (type == ESM::REC_GLOB) { ESM::Global global; bool isDeleted = false; @@ -94,11 +97,9 @@ namespace MWWorld // This readRecord() method is used when reading a saved game. // Deleted globals can't appear there, so isDeleted will be ignored here. global.load(reader, isDeleted); - Misc::StringUtils::lowerCaseInPlace(global.mId); - Collection::iterator iter = mVariables.find (global.mId); - if (iter!=mVariables.end()) - iter->second = global; + if (const auto iter = mVariables.find(global.mId); iter != mVariables.end()) + iter->second = std::move(global); return true; } diff --git a/apps/openmw/mwworld/globals.hpp b/apps/openmw/mwworld/globals.hpp index ae5e412c704..ab94f0f8dda 100644 --- a/apps/openmw/mwworld/globals.hpp +++ b/apps/openmw/mwworld/globals.hpp @@ -1,13 +1,15 @@ #ifndef GAME_MWWORLD_GLOBALS_H #define GAME_MWWORLD_GLOBALS_H -#include -#include +#include #include +#include +#include -#include +#include +#include -#include +#include "globalvariablename.hpp" namespace ESM { @@ -26,37 +28,50 @@ namespace MWWorld class Globals { - private: - - typedef std::map Collection; - - Collection mVariables; // type, value + private: + using Collection = std::map>; - Collection::const_iterator find (const std::string& name) const; + Collection mVariables; // type, value - Collection::iterator find (const std::string& name); + Collection::const_iterator find(std::string_view name) const; - public: + Collection::iterator find(std::string_view name); - const ESM::Variant& operator[] (const std::string& name) const; + public: + static constexpr GlobalVariableName sDaysPassed{ "dayspassed" }; + static constexpr GlobalVariableName sGameHour{ "gamehour" }; + static constexpr GlobalVariableName sDay{ "day" }; + static constexpr GlobalVariableName sMonth{ "month" }; + static constexpr GlobalVariableName sYear{ "year" }; + static constexpr GlobalVariableName sTimeScale{ "timescale" }; + static constexpr GlobalVariableName sCharGenState{ "chargenstate" }; + static constexpr GlobalVariableName sPCHasCrimeGold{ "pchascrimegold" }; + static constexpr GlobalVariableName sPCHasGoldDiscount{ "pchasgolddiscount" }; + static constexpr GlobalVariableName sCrimeGoldDiscount{ "crimegolddiscount" }; + static constexpr GlobalVariableName sCrimeGoldTurnIn{ "crimegoldturnin" }; + static constexpr GlobalVariableName sPCHasTurnIn{ "pchasturnin" }; + static constexpr GlobalVariableName sPCKnownWerewolf{ "pcknownwerewolf" }; + static constexpr GlobalVariableName sWerewolfClawMult{ "werewolfclawmult" }; + static constexpr GlobalVariableName sPCRace{ "pcrace" }; - ESM::Variant& operator[] (const std::string& name); + const ESM::Variant& operator[](GlobalVariableName name) const; - char getType (const std::string& name) const; - ///< If there is no global variable with this name, ' ' is returned. + ESM::Variant& operator[](GlobalVariableName name); - void fill (const MWWorld::ESMStore& store); - ///< Replace variables with variables from \a store with default values. + char getType(GlobalVariableName name) const; + ///< If there is no global variable with this name, ' ' is returned. - int countSavedGameRecords() const; + void fill(const MWWorld::ESMStore& store); + ///< Replace variables with variables from \a store with default values. - void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; + int countSavedGameRecords() const; - bool readRecord (ESM::ESMReader& reader, uint32_t type); - ///< Records for variables that do not exist are dropped silently. - /// - /// \return Known type? + void write(ESM::ESMWriter& writer, Loading::Listener& progress) const; + bool readRecord(ESM::ESMReader& reader, uint32_t type); + ///< Records for variables that do not exist are dropped silently. + /// + /// \return Known type? }; } diff --git a/apps/openmw/mwworld/globalvariablename.hpp b/apps/openmw/mwworld/globalvariablename.hpp new file mode 100644 index 00000000000..50052da69e7 --- /dev/null +++ b/apps/openmw/mwworld/globalvariablename.hpp @@ -0,0 +1,44 @@ +#ifndef OPENMW_MWWORLD_GLOBALVARIABLENAME_H +#define OPENMW_MWWORLD_GLOBALVARIABLENAME_H + +#include +#include +#include + +namespace MWWorld +{ + class Globals; + + class GlobalVariableName + { + public: + GlobalVariableName(const std::string& value) + : mValue(value) + { + } + + GlobalVariableName(std::string_view value) + : mValue(value) + { + } + + std::string_view getValue() const { return mValue; } + + friend bool operator==(const GlobalVariableName& lhs, const GlobalVariableName& rhs) noexcept + { + return lhs.mValue == rhs.mValue; + } + + private: + std::string_view mValue; + + explicit constexpr GlobalVariableName(const char* value) + : mValue(value) + { + } + + friend Globals; + }; +} + +#endif diff --git a/apps/openmw/mwworld/groundcoverstore.cpp b/apps/openmw/mwworld/groundcoverstore.cpp new file mode 100644 index 00000000000..f45b49babe5 --- /dev/null +++ b/apps/openmw/mwworld/groundcoverstore.cpp @@ -0,0 +1,72 @@ +#include "groundcoverstore.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "store.hpp" + +namespace MWWorld +{ + void GroundcoverStore::init(const Store& statics, const Files::Collections& fileCollections, + const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener) + { + ::EsmLoader::Query query; + query.mLoadStatics = true; + query.mLoadCells = true; + + ESM::ReadersCache readers; + const ::EsmLoader::EsmData content + = ::EsmLoader::loadEsmData(query, groundcoverFiles, fileCollections, readers, encoder, listener); + + static constexpr std::string_view prefix = "grass\\"; + for (const ESM::Static& stat : statics) + { + std::string model = Misc::StringUtils::lowerCase(stat.mModel); + std::replace(model.begin(), model.end(), '/', '\\'); + if (!model.starts_with(prefix)) + continue; + mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model); + } + + for (const ESM::Static& stat : content.mStatics) + { + std::string model = Misc::StringUtils::lowerCase(stat.mModel); + std::replace(model.begin(), model.end(), '/', '\\'); + if (!model.starts_with(prefix)) + continue; + mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model); + } + + for (const ESM::Cell& cell : content.mCells) + { + if (!cell.isExterior()) + continue; + auto cellIndex = std::make_pair(cell.getGridX(), cell.getGridY()); + mCellContexts[cellIndex] = std::move(cell.mContextList); + } + } + + std::string GroundcoverStore::getGroundcoverModel(const ESM::RefId& id) const + { + auto search = mMeshCache.find(id); + if (search == mMeshCache.end()) + return std::string(); + + return search->second; + } + + void GroundcoverStore::initCell(ESM::Cell& cell, int cellX, int cellY) const + { + cell.blank(); + + auto searchCell = mCellContexts.find(std::make_pair(cellX, cellY)); + if (searchCell != mCellContexts.end()) + cell.mContextList = searchCell->second; + } +} diff --git a/apps/openmw/mwworld/groundcoverstore.hpp b/apps/openmw/mwworld/groundcoverstore.hpp new file mode 100644 index 00000000000..2f34aa96754 --- /dev/null +++ b/apps/openmw/mwworld/groundcoverstore.hpp @@ -0,0 +1,52 @@ +#ifndef GAME_MWWORLD_GROUNDCOVER_STORE_H +#define GAME_MWWORLD_GROUNDCOVER_STORE_H + +#include +#include +#include +#include + +namespace ESM +{ + struct ESM_Context; + struct Static; + struct Cell; +} + +namespace Loading +{ + class Listener; +} + +namespace Files +{ + class Collections; +} + +namespace ToUTF8 +{ + class Utf8Encoder; +} + +namespace MWWorld +{ + template + class Store; + + class GroundcoverStore + { + private: + std::map mMeshCache; + std::map, std::vector> mCellContexts; + + public: + void init(const Store& statics, const Files::Collections& fileCollections, + const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder, + Loading::Listener* listener); + + std::string getGroundcoverModel(const ESM::RefId& id) const; + void initCell(ESM::Cell& cell, int cellX, int cellY) const; + }; +} + +#endif diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index d96447f87d3..f48f4e6e319 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -1,82 +1,77 @@ #include "inventorystore.hpp" -#include #include +#include -#include -#include -#include -#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/mechanicsmanager.hpp" -#include "../mwmechanics/npcstats.hpp" -#include "../mwmechanics/spellresistance.hpp" -#include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/weapontype.hpp" -#include "esmstore.hpp" #include "class.hpp" +#include "esmstore.hpp" -void MWWorld::InventoryStore::copySlots (const InventoryStore& store) +void MWWorld::InventoryStore::copySlots(const InventoryStore& store) { // some const-trickery, required because of a flaw in the handling of MW-references and the // resulting workarounds - for (std::vector::const_iterator iter ( - const_cast (store).mSlots.begin()); - iter!=const_cast (store).mSlots.end(); ++iter) + for (std::vector::const_iterator iter(const_cast(store).mSlots.begin()); + iter != const_cast(store).mSlots.end(); ++iter) { - std::size_t distance = std::distance (const_cast (store).begin(), *iter); + std::size_t distance = std::distance(const_cast(store).begin(), *iter); ContainerStoreIterator slot = begin(); - std::advance (slot, distance); + std::advance(slot, distance); - mSlots.push_back (slot); + mSlots.push_back(slot); } // some const-trickery, required because of a flaw in the handling of MW-references and the // resulting workarounds - std::size_t distance = std::distance (const_cast (store).begin(), const_cast (store).mSelectedEnchantItem); + std::size_t distance = std::distance( + const_cast(store).begin(), const_cast(store).mSelectedEnchantItem); ContainerStoreIterator slot = begin(); - std::advance (slot, distance); + std::advance(slot, distance); mSelectedEnchantItem = slot; } -void MWWorld::InventoryStore::initSlots (TSlots& slots_) +void MWWorld::InventoryStore::initSlots(TSlots& slots_) { - for (int i=0; i (mSlots.size()); ++i) - if (mSlots[i].getType()!=-1 && mSlots[i]->getBase()==&ref) - { - inventory.mEquipmentSlots[index] = i; - } + for (int32_t i = 0; i < MWWorld::InventoryStore::Slots; ++i) + { + if (mSlots[i].getType() != -1 && mSlots[i]->getBase() == &ref) + inventory.mEquipmentSlots[static_cast(index)] = i; + } - if (mSelectedEnchantItem.getType()!=-1 && mSelectedEnchantItem->getBase() == &ref) + if (mSelectedEnchantItem.getType() != -1 && mSelectedEnchantItem->getBase() == &ref) inventory.mSelectedEnchantItem = index; } -void MWWorld::InventoryStore::readEquipmentState(const MWWorld::ContainerStoreIterator &iter, int index, const ESM::InventoryState &inventory) +void MWWorld::InventoryStore::readEquipmentState( + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory) { if (index == inventory.mSelectedEnchantItem) mSelectedEnchantItem = iter; - std::map::const_iterator found = inventory.mEquipmentSlots.find(index); + auto found = inventory.mEquipmentSlots.find(index); if (found != inventory.mEquipmentSlots.end()) { if (found->second < 0 || found->second >= MWWorld::InventoryStore::Slots) throw std::runtime_error("Invalid slot index in inventory state"); // make sure the item can actually be equipped in this slot - int slot = found->second; + int32_t slot = found->second; std::pair, bool> allowedSlots = iter->getClass().getEquipmentSlots(*iter); if (!allowedSlots.first.size()) return; @@ -84,11 +79,11 @@ void MWWorld::InventoryStore::readEquipmentState(const MWWorld::ContainerStoreIt slot = allowedSlots.first.front(); // unstack if required - if (!allowedSlots.second && iter->getRefData().getCount() > 1) + if (!allowedSlots.second && iter->getCellRef().getCount() > 1) { - int count = iter->getRefData().getCount(false); + int count = iter->getCellRef().getCount(false); MWWorld::ContainerStoreIterator newIter = addNewStack(*iter, count > 0 ? 1 : -1); - iter->getRefData().setCount(subtractItems(count, 1)); + iter->getCellRef().setCount(subtractItems(count, 1)); mSlots[slot] = newIter; } else @@ -97,55 +92,54 @@ void MWWorld::InventoryStore::readEquipmentState(const MWWorld::ContainerStoreIt } MWWorld::InventoryStore::InventoryStore() - : ContainerStore() - , mInventoryListener(nullptr) - , mUpdatesEnabled (true) - , mFirstAutoEquip(true) - , mSelectedEnchantItem(end()) + : ContainerStore() + , mInventoryListener(nullptr) + , mUpdatesEnabled(true) + , mFirstAutoEquip(true) + , mSelectedEnchantItem(end()) { - initSlots (mSlots); + initSlots(mSlots); } -MWWorld::InventoryStore::InventoryStore (const InventoryStore& store) - : ContainerStore (store) - , mMagicEffects(store.mMagicEffects) - , mInventoryListener(store.mInventoryListener) - , mUpdatesEnabled(store.mUpdatesEnabled) - , mFirstAutoEquip(store.mFirstAutoEquip) - , mPermanentMagicEffectMagnitudes(store.mPermanentMagicEffectMagnitudes) - , mSelectedEnchantItem(end()) +MWWorld::InventoryStore::InventoryStore(const InventoryStore& store) + : ContainerStore(store) + , mInventoryListener(store.mInventoryListener) + , mUpdatesEnabled(store.mUpdatesEnabled) + , mFirstAutoEquip(store.mFirstAutoEquip) + , mSelectedEnchantItem(end()) { - copySlots (store); + copySlots(store); } -MWWorld::InventoryStore& MWWorld::InventoryStore::operator= (const InventoryStore& store) +MWWorld::InventoryStore& MWWorld::InventoryStore::operator=(const InventoryStore& store) { if (this == &store) return *this; mListener = store.mListener; mInventoryListener = store.mInventoryListener; - mMagicEffects = store.mMagicEffects; mFirstAutoEquip = store.mFirstAutoEquip; - mPermanentMagicEffectMagnitudes = store.mPermanentMagicEffectMagnitudes; mRechargingItemsUpToDate = false; - ContainerStore::operator= (store); + ContainerStore::operator=(store); mSlots.clear(); - copySlots (store); + copySlots(store); return *this; } -MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add(const Ptr& itemPtr, int count, const Ptr& actorPtr, bool allowAutoEquip, bool resolve) +MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add( + const Ptr& itemPtr, int count, bool allowAutoEquip, bool resolve) { - const MWWorld::ContainerStoreIterator& retVal = MWWorld::ContainerStore::add(itemPtr, count, actorPtr, allowAutoEquip, resolve); + const MWWorld::ContainerStoreIterator& retVal + = MWWorld::ContainerStore::add(itemPtr, count, allowAutoEquip, resolve); // Auto-equip items if an armor/clothing item is added, but not for the player nor werewolves - if (allowAutoEquip && actorPtr != MWMechanics::getPlayer() - && actorPtr.getClass().isNpc() && !actorPtr.getClass().getNpcStats(actorPtr).isWerewolf()) + const Ptr& actor = getPtr(); + if (allowAutoEquip && actor != MWMechanics::getPlayer() && actor.getClass().isNpc() + && !actor.getClass().getNpcStats(actor).isWerewolf()) { - std::string type = itemPtr.getTypeName(); - if (type == typeid(ESM::Armor).name() || type == typeid(ESM::Clothing).name()) - autoEquip(actorPtr); + auto type = itemPtr.getType(); + if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) + autoEquip(); } if (mListener) @@ -154,103 +148,98 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add(const Ptr& itemPtr, return retVal; } -void MWWorld::InventoryStore::equip (int slot, const ContainerStoreIterator& iterator, const Ptr& actor) +void MWWorld::InventoryStore::equip(int slot, const ContainerStoreIterator& iterator) { if (iterator == end()) - throw std::runtime_error ("can't equip end() iterator, use unequip function instead"); + throw std::runtime_error("can't equip end() iterator, use unequip function instead"); - if (slot<0 || slot>=static_cast (mSlots.size())) - throw std::runtime_error ("slot number out of range"); + if (slot < 0 || slot >= static_cast(mSlots.size())) + throw std::runtime_error("slot number out of range"); - if (iterator.getContainerStore()!=this) - throw std::runtime_error ("attempt to equip an item that is not in the inventory"); + if (iterator.getContainerStore() != this) + throw std::runtime_error("attempt to equip an item that is not in the inventory"); std::pair, bool> slots_; - slots_ = iterator->getClass().getEquipmentSlots (*iterator); + slots_ = iterator->getClass().getEquipmentSlots(*iterator); - if (std::find (slots_.first.begin(), slots_.first.end(), slot)==slots_.first.end()) - throw std::runtime_error ("invalid slot"); + if (std::find(slots_.first.begin(), slots_.first.end(), slot) == slots_.first.end()) + throw std::runtime_error("invalid slot"); if (mSlots[slot] != end()) - unequipSlot(slot, actor); + unequipSlot(slot); // unstack item pointed to by iterator if required - if (iterator!=end() && !slots_.second && iterator->getRefData().getCount() > 1) // if slots.second is true, item can stay stacked when equipped + if (iterator != end() && !slots_.second + && iterator->getCellRef().getCount() > 1) // if slots.second is true, item can stay stacked when equipped { - unstack(*iterator, actor); + unstack(*iterator); } mSlots[slot] = iterator; flagAsModified(); - fireEquipmentChangedEvent(actor); - - updateMagicEffects(actor); + fireEquipmentChangedEvent(); } -void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) +void MWWorld::InventoryStore::unequipAll() { mUpdatesEnabled = false; - for (int slot=0; slot < MWWorld::InventoryStore::Slots; ++slot) - unequipSlot(slot, actor); + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + unequipSlot(slot); mUpdatesEnabled = true; - fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); + fireEquipmentChangedEvent(); } -MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSlot (int slot) +MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSlot(int slot) { - return findSlot (slot); + return findSlot(slot); } -MWWorld::ConstContainerStoreIterator MWWorld::InventoryStore::getSlot (int slot) const +MWWorld::ConstContainerStoreIterator MWWorld::InventoryStore::getSlot(int slot) const { - return findSlot (slot); + return findSlot(slot); } -MWWorld::ContainerStoreIterator MWWorld::InventoryStore::findSlot (int slot) const +MWWorld::ContainerStoreIterator MWWorld::InventoryStore::findSlot(int slot) const { - if (slot<0 || slot>=static_cast (mSlots.size())) - throw std::runtime_error ("slot number out of range"); + if (slot < 0 || slot >= static_cast(mSlots.size())) + throw std::runtime_error("slot number out of range"); - if (mSlots[slot]==end()) + if (mSlots[slot] == end()) return mSlots[slot]; - if (mSlots[slot]->getRefData().getCount()<1) - { - // Object has been deleted - // This should no longer happen, since the new remove function will unequip first - throw std::runtime_error("Invalid slot, make sure you are not calling RefData::setCount for a container object"); - } + // NOTE: mSlots[slot]->getRefData().getCount() can be zero if the item is marked + // for removal by a Lua script, but the removal action is not yet processed. + // The item will be automatically unequiped in the current frame. return mSlots[slot]; } -void MWWorld::InventoryStore::autoEquipWeapon (const MWWorld::Ptr& actor, TSlots& slots_) +void MWWorld::InventoryStore::autoEquipWeapon(TSlots& slots_) { + const Ptr& actor = getPtr(); if (!actor.getClass().isNpc()) { // In original game creatures do not autoequip weapon, but we need it for weapon sheathing. // The only case when the difference is noticable - when this creature sells weapon. // So just disable weapon autoequipping for creatures which sells weapon. int services = actor.getClass().getServices(actor); - bool sellsWeapon = services & (ESM::NPC::Weapon|ESM::NPC::MagicItems); + bool sellsWeapon = services & (ESM::NPC::Weapon | ESM::NPC::MagicItems); if (sellsWeapon) return; } - static const ESM::Skill::SkillEnum weaponSkills[] = - { + static const ESM::RefId weaponSkills[] = { ESM::Skill::LongBlade, ESM::Skill::Axe, ESM::Skill::Spear, ESM::Skill::ShortBlade, ESM::Skill::Marksman, - ESM::Skill::BluntWeapon + ESM::Skill::BluntWeapon, }; const size_t weaponSkillsLength = sizeof(weaponSkills) / sizeof(weaponSkills[0]); @@ -263,7 +252,7 @@ void MWWorld::InventoryStore::autoEquipWeapon (const MWWorld::Ptr& actor, TSlots ContainerStoreIterator bolt(end()); // rate ammo - for (ContainerStoreIterator iter(begin(ContainerStore::Type_Weapon)); iter!=end(); ++iter) + for (ContainerStoreIterator iter(begin(ContainerStore::Type_Weapon)); iter != end(); ++iter) { const ESM::Weapon* esmWeapon = iter->get()->mBase; @@ -293,7 +282,7 @@ void MWWorld::InventoryStore::autoEquipWeapon (const MWWorld::Ptr& actor, TSlots for (int j = 0; j < static_cast(weaponSkillsLength); ++j) { - float skillValue = actor.getClass().getSkill(actor, static_cast(weaponSkills[j])); + float skillValue = actor.getClass().getSkill(actor, weaponSkills[j]); if (skillValue > max && !weaponSkillVisited[j]) { max = skillValue; @@ -307,7 +296,7 @@ void MWWorld::InventoryStore::autoEquipWeapon (const MWWorld::Ptr& actor, TSlots max = 0; ContainerStoreIterator weapon(end()); - for (ContainerStoreIterator iter(begin(ContainerStore::Type_Weapon)); iter!=end(); ++iter) + for (ContainerStoreIterator iter(begin(ContainerStore::Type_Weapon)); iter != end(); ++iter) { const ESM::Weapon* esmWeapon = iter->get()->mBase; @@ -340,7 +329,7 @@ void MWWorld::InventoryStore::autoEquipWeapon (const MWWorld::Ptr& actor, TSlots { // Do not equip ranged weapons, if there is no suitable ammo bool hasAmmo = true; - const MWWorld::LiveCellRef *ref = weapon->get(); + const MWWorld::LiveCellRef* ref = weapon->get(); int type = ref->mBase->mData.mType; int ammotype = MWMechanics::getWeaponType(type)->mAmmoType; if (ammotype == ESM::Weapon::Arrow) @@ -360,15 +349,15 @@ void MWWorld::InventoryStore::autoEquipWeapon (const MWWorld::Ptr& actor, TSlots if (hasAmmo) { - std::pair, bool> itemsSlots = weapon->getClass().getEquipmentSlots (*weapon); + std::pair, bool> itemsSlots = weapon->getClass().getEquipmentSlots(*weapon); if (!itemsSlots.first.empty()) { if (!itemsSlots.second) { - if (weapon->getRefData().getCount() > 1) + if (weapon->getCellRef().getCount() > 1) { - unstack(*weapon, actor); + unstack(*weapon); } } @@ -387,18 +376,18 @@ void MWWorld::InventoryStore::autoEquipWeapon (const MWWorld::Ptr& actor, TSlots } } -void MWWorld::InventoryStore::autoEquipArmor (const MWWorld::Ptr& actor, 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()) { - autoEquipShield(actor, slots_); + autoEquipShield(slots_); return; } - const MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &store = world->getStore().get(); + 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(); @@ -406,11 +395,12 @@ void MWWorld::InventoryStore::autoEquipArmor (const MWWorld::Ptr& actor, TSlots& 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(ContainerStore::Type_Clothing | ContainerStore::Type_Armor)); iter != end(); + ++iter) { Ptr test = *iter; - switch(test.getClass().canBeEquipped (test, actor).first) + switch (test.getClass().canBeEquipped(test, actor).first) { case 0: continue; @@ -418,34 +408,34 @@ void MWWorld::InventoryStore::autoEquipArmor (const MWWorld::Ptr& actor, TSlots& break; } - if (iter.getType() == ContainerStore::Type_Armor && - test.getClass().getEffectiveArmorRating(test, actor) <= std::max(unarmoredRating, 0.f)) + if (iter.getType() == ContainerStore::Type_Armor + && test.getClass().getEffectiveArmorRating(test, actor) <= std::max(unarmoredRating, 0.f)) { continue; } - std::pair, bool> itemsSlots = - iter->getClass().getEquipmentSlots (*iter); + std::pair, bool> itemsSlots = iter->getClass().getEquipmentSlots(*iter); // checking if current item pointed by iter can be equipped for (int slot : itemsSlots.first) { // 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()) + if (slots_.at(slot) != end()) { - Ptr old = *slots_.at (slot); + Ptr old = *slots_.at(slot); if (iter.getType() == ContainerStore::Type_Armor) { - if (old.getTypeName() == typeid(ESM::Armor).name()) + if (old.getType() == ESM::Armor::sRecordId) { if (old.get()->mBase->mData.mType < test.get()->mBase->mData.mType) continue; if (old.get()->mBase->mData.mType == test.get()->mBase->mData.mType) { - if (old.getClass().getEffectiveArmorRating(old, actor) >= test.getClass().getEffectiveArmorRating(test, actor)) + if (old.getClass().getEffectiveArmorRating(old, actor) + >= test.getClass().getEffectiveArmorRating(test, actor)) // old armor had better armor rating continue; } @@ -467,15 +457,15 @@ void MWWorld::InventoryStore::autoEquipArmor (const MWWorld::Ptr& actor, TSlots& 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)) + if (old.getClass().getValue(old) >= rightRing.getClass().getValue(rightRing)) continue; } } - if (old.getTypeName() == typeid(ESM::Clothing).name()) + if (old.getType() == ESM::Clothing::sRecordId) { // check value - if (old.getClass().getValue (old) >= test.getClass().getValue (test)) + if (old.getClass().getValue(old) >= test.getClass().getValue(test)) // old clothing was more valuable continue; } @@ -488,9 +478,9 @@ void MWWorld::InventoryStore::autoEquipArmor (const MWWorld::Ptr& actor, TSlots& if (!itemsSlots.second) // if itemsSlots.second is true, item can stay stacked when equipped { // unstack item pointed to by iterator if required - if (iter->getRefData().getCount() > 1) + if (iter->getCellRef().getCount() > 1) { - unstack(*iter, actor); + unstack(*iter); } } @@ -501,20 +491,19 @@ void MWWorld::InventoryStore::autoEquipArmor (const MWWorld::Ptr& actor, TSlots& } } -void MWWorld::InventoryStore::autoEquipShield(const MWWorld::Ptr& actor, 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, actor).first != 1) + if (iter->getClass().canBeEquipped(*iter, getPtr()).first != 1) continue; - std::pair, bool> shieldSlots = - iter->getClass().getEquipmentSlots(*iter); + 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 != end() && shield.getType() == Type_Armor + && shield->get()->mBase->mData.mType == ESM::Armor::Shield) { if (shield->getClass().getItemHealth(*shield) >= iter->getClass().getItemHealth(*iter)) continue; @@ -523,10 +512,10 @@ void MWWorld::InventoryStore::autoEquipShield(const MWWorld::Ptr& actor, TSlots& } } -void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) +void MWWorld::InventoryStore::autoEquip() { TSlots slots_; - initSlots (slots_); + initSlots(slots_); // Disable model update during auto-equip mUpdatesEnabled = false; @@ -535,12 +524,12 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) // 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(actor, slots_); - autoEquipArmor(actor, slots_); + autoEquipWeapon(slots_); + autoEquipArmor(slots_); bool changed = false; - for (std::size_t i=0; igetClass().getEnchantment (**iter); - - if (!enchantmentId.empty()) - { - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - std::vector params; - - bool existed = (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) != mPermanentMagicEffectMagnitudes.end()); - if (!existed) - { - params.resize(enchantment.mEffects.mList.size()); - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - int delta = effect.mMagnMax - effect.mMagnMin; - // Roll some dice, one for each effect - if (delta) - params[i].mRandom = Misc::Rng::rollDice(delta + 1) / static_cast(delta); - // Try resisting each effect - params[i].mMultiplier = MWMechanics::getEffectMultiplier(effect.mEffectID, actor, actor); - ++i; - } - - // Note that using the RefID as a key here is not entirely correct. - // Consider equipping the same item twice (e.g. a ring) - // However, permanent enchantments with a random magnitude are kind of an exploit anyway, - // so it doesn't really matter if both items will get the same magnitude. *Extreme* edge case. - mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()] = params; - } - else - params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()]; - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find ( - effect.mEffectID); - - // Fully resisted or can't be applied to target? - if (params[i].mMultiplier == 0 || !MWMechanics::checkEffectTarget(effect.mEffectID, actor, actor, actor == MWMechanics::getPlayer())) - { - i++; - continue; - } - - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params[i].mRandom; - magnitude *= params[i].mMultiplier; - - if (!existed) - { - // During first auto equip, we don't play any sounds. - // Basically we don't want sounds when the actor is first loaded, - // the items should appear as if they'd always been equipped. - mInventoryListener->permanentEffectAdded(magicEffect, !mFirstAutoEquip); - } - - if (magnitude) - mMagicEffects.add (effect, magnitude); - - i++; - } - } - } - - // Now drop expired effects - for (TEffectMagnitudes::iterator it = mPermanentMagicEffectMagnitudes.begin(); - it != mPermanentMagicEffectMagnitudes.end();) - { - bool found = false; - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter == end()) - continue; - if ((**iter).getCellRef().getRefId() == it->first) - { - found = true; - } - } - if (!found) - mPermanentMagicEffectMagnitudes.erase(it++); - else - ++it; - } - - // Magic effects are normally not updated when paused, but we need this to make resistances work immediately after equipping - MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); - - mFirstAutoEquip = false; -} - bool MWWorld::InventoryStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const { bool canStack = MWWorld::ContainerStore::stacks(ptr1, ptr2); @@ -697,8 +562,7 @@ bool MWWorld::InventoryStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) return false; // don't stack if either item is currently equipped - for (TSlots::const_iterator iter (mSlots.begin()); - iter!=mSlots.end(); ++iter) + for (TSlots::const_iterator iter(mSlots.begin()); iter != mSlots.end(); ++iter) { if (*iter != end() && (ptr1 == **iter || ptr2 == **iter)) { @@ -721,21 +585,21 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSelectedEnchantItem( return mSelectedEnchantItem; } -int MWWorld::InventoryStore::remove(const Ptr& item, int count, const Ptr& actor, bool equipReplacement, bool resolve) +int MWWorld::InventoryStore::remove(const Ptr& item, int count, bool equipReplacement, bool resolve) { - int retCount = ContainerStore::remove(item, count, actor, equipReplacement, resolve); + int retCount = ContainerStore::remove(item, count, equipReplacement, resolve); bool wasEquipped = false; - if (!item.getRefData().getCount()) + if (!item.getCellRef().getCount()) { - for (int slot=0; slot < MWWorld::InventoryStore::Slots; ++slot) + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { if (mSlots[slot] == end()) continue; if (*mSlots[slot] == item) { - unequipSlot(slot, actor); + unequipSlot(slot); wasEquipped = true; break; } @@ -745,16 +609,16 @@ int MWWorld::InventoryStore::remove(const Ptr& item, int count, const Ptr& actor // If an armor/clothing item is removed, try to find a replacement, // but not for the player nor werewolves, and not if the RemoveItem script command // was used (equipReplacement is false) - if (equipReplacement && wasEquipped && (actor != MWMechanics::getPlayer()) - && actor.getClass().isNpc() && !actor.getClass().getNpcStats(actor).isWerewolf()) + const Ptr& actor = getPtr(); + if (equipReplacement && wasEquipped && (actor != MWMechanics::getPlayer()) && actor.getClass().isNpc() + && !actor.getClass().getNpcStats(actor).isWerewolf()) { - std::string type = item.getTypeName(); - if (type == typeid(ESM::Armor).name() || type == typeid(ESM::Clothing).name()) - autoEquip(actor); + auto type = item.getType(); + if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) + autoEquip(); } - if (item.getRefData().getCount() == 0 && mSelectedEnchantItem != end() - && *mSelectedEnchantItem == item) + if (item.getCellRef().getCount() == 0 && mSelectedEnchantItem != end() && *mSelectedEnchantItem == item) { mSelectedEnchantItem = end(); } @@ -765,10 +629,10 @@ int MWWorld::InventoryStore::remove(const Ptr& item, int count, const Ptr& actor return retCount; } -MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, const MWWorld::Ptr& actor, bool applyUpdates) +MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, bool applyUpdates) { - if (slot<0 || slot>=static_cast (mSlots.size())) - throw std::runtime_error ("slot number out of range"); + if (slot < 0 || slot >= static_cast(mSlots.size())) + throw std::runtime_error("slot number out of range"); ContainerStoreIterator it = mSlots[slot]; @@ -779,15 +643,15 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, c // empty this slot mSlots[slot] = end(); - if (it->getRefData().getCount()) + if (it->getCellRef().getCount()) { retval = restack(*it); - if (actor == MWMechanics::getPlayer()) + if (getPtr() == MWMechanics::getPlayer()) { // Unset OnPCEquip Variable on item's script, if it has a script with that variable declared - const std::string& script = it->getClass().getScript(*it); - if (script != "") + const ESM::RefId& script = it->getClass().getScript(*it); + if (!script.empty()) (*it).getRefData().getLocals().setVarByInt(script, "onpcequip", 0); } @@ -799,8 +663,7 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, c if (applyUpdates) { - fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); + fireEquipmentChangedEvent(); } return retval; @@ -809,57 +672,56 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, c return it; } -MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItem(const MWWorld::Ptr& item, const MWWorld::Ptr& actor) +MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItem(const MWWorld::Ptr& item) { - for (int slot=0; slot item.getRefData().getCount()) - throw std::runtime_error ("attempt to unequip more items than equipped"); + throw std::runtime_error("attempt to unequip nothing (count <= 0)"); + if (count > item.getCellRef().getCount()) + throw std::runtime_error("attempt to unequip more items than equipped"); - if (count == item.getRefData().getCount()) - return unequipItem(item, actor); + if (count == item.getCellRef().getCount()) + return unequipItem(item); // Move items to an existing stack if possible, otherwise split count items out into a new stack. // Moving counts manually here, since ContainerStore's restack can't target unequipped stacks. - for (MWWorld::ContainerStoreIterator iter (begin()); iter != end(); ++iter) + for (MWWorld::ContainerStoreIterator iter(begin()); iter != end(); ++iter) { if (stacks(*iter, item) && !isEquipped(*iter)) { - iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), count)); - item.getRefData().setCount(subtractItems(item.getRefData().getCount(false), count)); + iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); + item.getCellRef().setCount(subtractItems(item.getCellRef().getCount(false), count)); return iter; } } - return unstack(item, actor, item.getRefData().getCount() - count); + return unstack(item, item.getCellRef().getCount() - count); } -MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() +MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() const { return mInventoryListener; } -void MWWorld::InventoryStore::setInvListener(InventoryStoreListener *listener, const Ptr& actor) +void MWWorld::InventoryStore::setInvListener(InventoryStoreListener* listener) { mInventoryListener = listener; - updateMagicEffects(actor); } -void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor) +void MWWorld::InventoryStore::fireEquipmentChangedEvent() { if (!mUpdatesEnabled) return; @@ -868,122 +730,23 @@ void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor) // if player, update inventory window /* - if (actor == MWMechanics::getPlayer()) + if (mActor == MWMechanics::getPlayer()) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); } */ } -void MWWorld::InventoryStore::visitEffectSources(MWMechanics::EffectSourceVisitor &visitor) -{ - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - if (enchantmentId.empty()) - continue; - - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - if (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) == mPermanentMagicEffectMagnitudes.end()) - continue; - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - i++; - // Don't get spell icon display information for enchantments that weren't actually applied - if (mMagicEffects.get(MWMechanics::EffectKey(effect)).getMagnitude() == 0) - continue; - const EffectParams& params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()][i-1]; - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params.mRandom; - magnitude *= params.mMultiplier; - if (magnitude > 0) - visitor.visit(MWMechanics::EffectKey(effect), i-1, (**iter).getClass().getName(**iter), (**iter).getCellRef().getRefId(), -1, magnitude); - } - } -} - -void MWWorld::InventoryStore::purgeEffect(short effectId, bool wholeSpell) -{ - for (TSlots::const_iterator it = mSlots.begin(); it != mSlots.end(); ++it) - { - if (*it != end()) - purgeEffect(effectId, (*it)->getCellRef().getRefId(), wholeSpell); - } -} - -void MWWorld::InventoryStore::purgeEffect(short effectId, const std::string &sourceId, bool wholeSpell, int effectIndex) -{ - TEffectMagnitudes::iterator effectMagnitudeIt = mPermanentMagicEffectMagnitudes.find(sourceId); - if (effectMagnitudeIt == mPermanentMagicEffectMagnitudes.end()) - return; - - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - if ((*iter)->getCellRef().getRefId() != sourceId) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - - if (!enchantmentId.empty()) - { - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - std::vector& params = effectMagnitudeIt->second; - - int i=0; - for (std::vector::const_iterator effectIt (enchantment.mEffects.mList.begin()); - effectIt!=enchantment.mEffects.mList.end(); ++effectIt, ++i) - { - if (effectIt->mEffectID != effectId) - continue; - - if (effectIndex >= 0 && effectIndex != i) - continue; - - if (wholeSpell) - { - mPermanentMagicEffectMagnitudes.erase(sourceId); - return; - } - - float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params[i].mRandom; - magnitude *= params[i].mMultiplier; - - if (magnitude) - mMagicEffects.add (*effectIt, -magnitude); - - params[i].mMultiplier = 0; - } - } - } -} - void MWWorld::InventoryStore::clear() { mSlots.clear(); - initSlots (mSlots); + initSlots(mSlots); ContainerStore::clear(); } -bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr &item) +bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr& item) { - for (int i=0; i < MWWorld::InventoryStore::Slots; ++i) + for (int i = 0; i < MWWorld::InventoryStore::Slots; ++i) { if (getSlot(i) != end() && *getSlot(i) == item) return true; @@ -991,38 +754,9 @@ bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr &item) return false; } -void MWWorld::InventoryStore::writeState(ESM::InventoryState &state) const +bool MWWorld::InventoryStore::isFirstEquip() { - MWWorld::ContainerStore::writeState(state); - - for (TEffectMagnitudes::const_iterator it = mPermanentMagicEffectMagnitudes.begin(); it != mPermanentMagicEffectMagnitudes.end(); ++it) - { - std::vector > params; - for (std::vector::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) - { - params.emplace_back(pIt->mRandom, pIt->mMultiplier); - } - - state.mPermanentMagicEffectMagnitudes[it->first] = params; - } -} - -void MWWorld::InventoryStore::readState(const ESM::InventoryState &state) -{ - MWWorld::ContainerStore::readState(state); - - for (ESM::InventoryState::TEffectMagnitudes::const_iterator it = state.mPermanentMagicEffectMagnitudes.begin(); - it != state.mPermanentMagicEffectMagnitudes.end(); ++it) - { - std::vector params; - for (std::vector >::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) - { - EffectParams p; - p.mRandom = pIt->first; - p.mMultiplier = pIt->second; - params.push_back(p); - } - - mPermanentMagicEffectMagnitudes[it->first] = params; - } + bool first = mFirstAutoEquip; + mFirstAutoEquip = false; + return first; } diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 32dc0d2e91b..0af6ee2b286 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -3,8 +3,6 @@ #include "containerstore.hpp" -#include "../mwmechanics/magiceffects.hpp" - namespace ESM { struct MagicEffect; @@ -23,16 +21,7 @@ namespace MWWorld /** * Fired when items are equipped or unequipped */ - virtual void equipmentChanged () {} - - /** - * @param effect - * @param isNew Is this effect new (e.g. the item for it was just now manually equipped) - * or was it loaded from a savegame / initial game state? \n - * If it isn't new, non-looping VFX should not be played. - * @param playSound Play effect sound? - */ - virtual void permanentEffectAdded (const ESM::MagicEffect *magicEffect, bool isNew) {} + virtual void equipmentChanged() {} virtual ~InventoryStoreListener() = default; }; @@ -40,180 +29,156 @@ namespace MWWorld ///< \brief Variant of the ContainerStore for NPCs class InventoryStore : public ContainerStore { - public: - - static constexpr int Slot_Helmet = 0; - static constexpr int Slot_Cuirass = 1; - static constexpr int Slot_Greaves = 2; - static constexpr int Slot_LeftPauldron = 3; - static constexpr int Slot_RightPauldron = 4; - static constexpr int Slot_LeftGauntlet = 5; - static constexpr int Slot_RightGauntlet = 6; - static constexpr int Slot_Boots = 7; - static constexpr int Slot_Shirt = 8; - static constexpr int Slot_Pants = 9; - static constexpr int Slot_Skirt = 10; - static constexpr int Slot_Robe = 11; - static constexpr int Slot_LeftRing = 12; - static constexpr int Slot_RightRing = 13; - static constexpr int Slot_Amulet = 14; - static constexpr int Slot_Belt = 15; - static constexpr int Slot_CarriedRight = 16; - static constexpr int Slot_CarriedLeft = 17; - static constexpr int Slot_Ammunition = 18; - - static constexpr int Slots = 19; - - static constexpr int Slot_NoSlot = -1; - - private: - - MWMechanics::MagicEffects mMagicEffects; - - InventoryStoreListener* mInventoryListener; - - // Enables updates of magic effects and actor model whenever items are equipped or unequipped. - // This is disabled during autoequip to avoid excessive updates - bool mUpdatesEnabled; - - bool mFirstAutoEquip; - - // Vanilla allows permanent effects with a random magnitude, so it needs to be stored here. - // We also need this to only play sounds and particle effects when the item is equipped, rather than on every update. - struct EffectParams - { - // Modifier to scale between min and max magnitude - float mRandom; - // Multiplier for when an effect was fully or partially resisted - float mMultiplier; - }; - - typedef std::map > TEffectMagnitudes; - TEffectMagnitudes mPermanentMagicEffectMagnitudes; - - typedef std::vector TSlots; - - TSlots mSlots; - - void autoEquipWeapon(const MWWorld::Ptr& actor, TSlots& slots_); - void autoEquipArmor(const MWWorld::Ptr& actor, TSlots& slots_); - void autoEquipShield(const MWWorld::Ptr& actor, TSlots& slots_); - - // selected magic item (for using enchantments of type "Cast once" or "Cast when used") - ContainerStoreIterator mSelectedEnchantItem; - - void copySlots (const InventoryStore& store); - - void initSlots (TSlots& slots_); - - void updateMagicEffects(const Ptr& actor); - - void fireEquipmentChangedEvent(const Ptr& actor); - - void storeEquipmentState (const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const override; - void readEquipmentState (const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory) override; - - ContainerStoreIterator findSlot (int slot) const; - - public: - - InventoryStore(); - - InventoryStore (const InventoryStore& store); - - InventoryStore& operator= (const InventoryStore& store); - - std::unique_ptr clone() override { return std::make_unique(*this); } - - ContainerStoreIterator add (const Ptr& itemPtr, int count, const Ptr& actorPtr, bool allowAutoEquip = true, bool resolve = true) override; - ///< Add the item pointed to by \a ptr to this container. (Stacks automatically if needed) - /// Auto-equip items if specific conditions are fulfilled and allowAutoEquip is true (see the implementation). - /// - /// \note The item pointed to is not required to exist beyond this function call. - /// - /// \attention Do not add items to an existing stack by increasing the count instead of - /// calling this function! - /// - /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item. - - void equip (int slot, const ContainerStoreIterator& iterator, const Ptr& actor); - ///< \warning \a iterator can not be an end()-iterator, use unequip function instead - - bool isEquipped(const MWWorld::ConstPtr& item); - ///< Utility function, returns true if the given item is equipped in any slot - - void setSelectedEnchantItem(const ContainerStoreIterator& iterator); - ///< set the selected magic item (for using enchantments of type "Cast once" or "Cast when used") - /// \note to unset the selected item, call this method with end() iterator - - ContainerStoreIterator getSelectedEnchantItem(); - ///< @return selected magic item (for using enchantments of type "Cast once" or "Cast when used") - /// \note if no item selected, return end() iterator - - ContainerStoreIterator getSlot (int slot); - ConstContainerStoreIterator getSlot(int slot) const; - - ContainerStoreIterator getPreferredShield(const MWWorld::Ptr& actor); - - void unequipAll(const MWWorld::Ptr& actor); - ///< Unequip all currently equipped items. + public: + static constexpr int Slot_Helmet = 0; + static constexpr int Slot_Cuirass = 1; + static constexpr int Slot_Greaves = 2; + static constexpr int Slot_LeftPauldron = 3; + static constexpr int Slot_RightPauldron = 4; + static constexpr int Slot_LeftGauntlet = 5; + static constexpr int Slot_RightGauntlet = 6; + static constexpr int Slot_Boots = 7; + static constexpr int Slot_Shirt = 8; + static constexpr int Slot_Pants = 9; + static constexpr int Slot_Skirt = 10; + static constexpr int Slot_Robe = 11; + static constexpr int Slot_LeftRing = 12; + static constexpr int Slot_RightRing = 13; + static constexpr int Slot_Amulet = 14; + static constexpr int Slot_Belt = 15; + static constexpr int Slot_CarriedRight = 16; + static constexpr int Slot_CarriedLeft = 17; + static constexpr int Slot_Ammunition = 18; - void autoEquip (const MWWorld::Ptr& actor); - ///< Auto equip items according to stats and item value. + static constexpr int Slots = 19; - const MWMechanics::MagicEffects& getMagicEffects() const; - ///< Return magic effects from worn items. + static constexpr int Slot_NoSlot = -1; - bool stacks (const ConstPtr& ptr1, const ConstPtr& ptr2) const override; - ///< @return true if the two specified objects can stack with each other + private: + InventoryStoreListener* mInventoryListener; - using ContainerStore::remove; - int remove(const Ptr& item, int count, const Ptr& actor, bool equipReplacement = 0, bool resolve = true) override; - ///< Remove \a count item(s) designated by \a item from this inventory. - /// - /// @return the number of items actually removed + // Enables updates of magic effects and actor model whenever items are equipped or unequipped. + // This is disabled during autoequip to avoid excessive updates + bool mUpdatesEnabled; - ContainerStoreIterator unequipSlot(int slot, const Ptr& actor, bool applyUpdates = true); - ///< Unequip \a slot. - /// - /// @return an iterator to the item that was previously in the slot + bool mFirstAutoEquip; - ContainerStoreIterator unequipItem(const Ptr& item, const Ptr& actor); - ///< Unequip an item identified by its Ptr. An exception is thrown - /// if the item is not currently equipped. - /// - /// @return an iterator to the item that was previously in the slot - /// (it can be re-stacked so its count may be different than when it - /// was equipped). + typedef std::vector TSlots; - ContainerStoreIterator unequipItemQuantity(const Ptr& item, const Ptr& actor, int count); - ///< Unequip a specific quantity of an item identified by its Ptr. - /// An exception is thrown if the item is not currently equipped, - /// if count <= 0, or if count > the item stack size. - /// - /// @return an iterator to the unequipped items that were previously - /// in the slot (they can be re-stacked so its count may be different - /// than the requested count). + TSlots mSlots; - void setInvListener (InventoryStoreListener* listener, const Ptr& actor); - ///< Set a listener for various events, see \a InventoryStoreListener + void autoEquipWeapon(TSlots& slots_); + void autoEquipArmor(TSlots& slots_); + void autoEquipShield(TSlots& slots_); - InventoryStoreListener* getInvListener(); + // selected magic item (for using enchantments of type "Cast once" or "Cast when used") + ContainerStoreIterator mSelectedEnchantItem; - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor); + void copySlots(const InventoryStore& store); - void purgeEffect (short effectId, bool wholeSpell = false); - ///< Remove a magic effect + void initSlots(TSlots& slots_); - void purgeEffect (short effectId, const std::string& sourceId, bool wholeSpell = false, int effectIndex=-1); - ///< Remove a magic effect + void fireEquipmentChangedEvent(); - void clear() override; - ///< Empty container. + void storeEquipmentState( + const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const override; + void readEquipmentState( + const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory) override; - void writeState (ESM::InventoryState& state) const override; + ContainerStoreIterator findSlot(int slot) const; - void readState (const ESM::InventoryState& state) override; + public: + InventoryStore(); + + InventoryStore(const InventoryStore& store); + + InventoryStore& operator=(const InventoryStore& store); + + std::unique_ptr clone() override + { + auto res = std::make_unique(*this); + res->updateRefNums(); + return res; + } + + ContainerStoreIterator add( + const Ptr& itemPtr, int count, bool allowAutoEquip = true, bool resolve = true) override; + ///< Add the item pointed to by \a ptr to this container. (Stacks automatically if needed) + /// Auto-equip items if specific conditions are fulfilled and allowAutoEquip is true (see the implementation). + /// + /// \note The item pointed to is not required to exist beyond this function call. + /// + /// \attention Do not add items to an existing stack by increasing the count instead of + /// calling this function! + /// + /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to + /// the newly inserted item. + + void equip(int slot, const ContainerStoreIterator& iterator); + ///< \warning \a iterator can not be an end()-iterator, use unequip function instead + + bool isEquipped(const MWWorld::ConstPtr& item); + ///< Utility function, returns true if the given item is equipped in any slot + + void setSelectedEnchantItem(const ContainerStoreIterator& iterator); + ///< set the selected magic item (for using enchantments of type "Cast once" or "Cast when used") + /// \note to unset the selected item, call this method with end() iterator + + ContainerStoreIterator getSelectedEnchantItem(); + ///< @return selected magic item (for using enchantments of type "Cast once" or "Cast when used") + /// \note if no item selected, return end() iterator + + ContainerStoreIterator getSlot(int slot); + ConstContainerStoreIterator getSlot(int slot) const; + + ContainerStoreIterator getPreferredShield(); + + void unequipAll(); + ///< Unequip all currently equipped items. + + void autoEquip(); + ///< Auto equip items according to stats and item value. + + bool stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const override; + ///< @return true if the two specified objects can stack with each other + + using ContainerStore::remove; + int remove(const Ptr& item, int count, bool equipReplacement = 0, bool resolve = true) override; + ///< Remove \a count item(s) designated by \a item from this inventory. + /// + /// @return the number of items actually removed + + ContainerStoreIterator unequipSlot(int slot, bool applyUpdates = true); + ///< Unequip \a slot. + /// + /// @return an iterator to the item that was previously in the slot + + ContainerStoreIterator unequipItem(const Ptr& item); + ///< Unequip an item identified by its Ptr. An exception is thrown + /// if the item is not currently equipped. + /// + /// @return an iterator to the item that was previously in the slot + /// (it can be re-stacked so its count may be different than when it + /// was equipped). + + ContainerStoreIterator unequipItemQuantity(const Ptr& item, int count); + ///< Unequip a specific quantity of an item identified by its Ptr. + /// An exception is thrown if the item is not currently equipped, + /// if count <= 0, or if count > the item stack size. + /// + /// @return an iterator to the unequipped items that were previously + /// in the slot (they can be re-stacked so its count may be different + /// than the requested count). + + void setInvListener(InventoryStoreListener* listener); + ///< Set a listener for various events, see \a InventoryStoreListener + + InventoryStoreListener* getInvListener() const; + + void clear() override; + ///< Empty container. + + bool isFirstEquip(); }; } diff --git a/apps/openmw/mwworld/livecellref.cpp b/apps/openmw/mwworld/livecellref.cpp index 9cf8a0fe04e..61b838bbf0d 100644 --- a/apps/openmw/mwworld/livecellref.cpp +++ b/apps/openmw/mwworld/livecellref.cpp @@ -1,71 +1,129 @@ #include "livecellref.hpp" +#include + #include -#include +#include +#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include "../mwbase/luamanager.hpp" -#include "ptr.hpp" #include "class.hpp" #include "esmstore.hpp" +#include "ptr.hpp" +#include "worldmodel.hpp" + +MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM::CellRef& cref) + : mClass(&Class::get(type)) + , mRef(cref) + , mData(cref) +{ +} + +MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::Reference& cref) + : mClass(&Class::get(type)) + , mRef(cref) + , mData(cref) +{ +} -MWWorld::LiveCellRefBase::LiveCellRefBase(const std::string& type, const ESM::CellRef &cref) - : mClass(&Class::get(type)), mRef(cref), mData(cref) +MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref) + : mClass(&Class::get(type)) + , mRef(cref) + , mData(cref) { } -void MWWorld::LiveCellRefBase::loadImp (const ESM::ObjectState& state) +MWWorld::LiveCellRefBase::~LiveCellRefBase() { - mRef = state.mRef; - mData = RefData (state, mData.isDeletedByContentFile()); + MWBase::Environment::get().getWorldModel()->deregisterLiveCellRef(*this); +} - Ptr ptr (this); +void MWWorld::LiveCellRefBase::loadImp(const ESM::ObjectState& state) +{ + mRef = MWWorld::CellRef(state.mRef); + mData = RefData(state, mData.isDeletedByContentFile()); + + Ptr ptr(this); if (state.mHasLocals) { - std::string scriptId = mClass->getScript (ptr); + const ESM::RefId& scriptId = mClass->getScript(ptr); // Make sure we still have a script. It could have been coming from a content file that is no longer active. if (!scriptId.empty()) { - if (const ESM::Script* script = MWBase::Environment::get().getWorld()->getStore().get().search (scriptId)) + if (const ESM::Script* script + = MWBase::Environment::get().getESMStore()->get().search(scriptId)) { try { - mData.setLocals (*script); - mData.getLocals().read (state.mLocals, scriptId); + mData.setLocals(*script); + mData.getLocals().read(state.mLocals, scriptId); } catch (const std::exception& exception) { - Log(Debug::Error) - << "Error: failed to load state for local script " << scriptId - << " because an exception has been thrown: " << exception.what(); + Log(Debug::Error) << "Error: failed to load state for local script " << scriptId + << " because an exception has been thrown: " << exception.what(); } } } } - mClass->readAdditionalState (ptr, state); + mClass->readAdditionalState(ptr, state); - if (!mRef.getSoul().empty() && !MWBase::Environment::get().getWorld()->getStore().get().search(mRef.getSoul())) + if (!mRef.getSoul().empty() + && !MWBase::Environment::get().getESMStore()->get().search(mRef.getSoul())) { Log(Debug::Warning) << "Soul '" << mRef.getSoul() << "' not found, removing the soul from soul gem"; - mRef.setSoul(std::string()); + mRef.setSoul(ESM::RefId()); } + + MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts); } -void MWWorld::LiveCellRefBase::saveImp (ESM::ObjectState& state) const +void MWWorld::LiveCellRefBase::saveImp(ESM::ObjectState& state) const { mRef.writeState(state); - ConstPtr ptr (this); + ConstPtr ptr(this); - mData.write (state, mClass->getScript (ptr)); + mData.write(state, mClass->getScript(ptr)); + MWBase::Environment::get().getLuaManager()->saveLocalScripts( + Ptr(const_cast(this)), state.mLuaScripts); - mClass->writeAdditionalState (ptr, state); + mClass->writeAdditionalState(ptr, state); } -bool MWWorld::LiveCellRefBase::checkStateImp (const ESM::ObjectState& state) +bool MWWorld::LiveCellRefBase::checkStateImp(const ESM::ObjectState& state) { return true; } + +unsigned int MWWorld::LiveCellRefBase::getType() const +{ + return mClass->getType(); +} + +bool MWWorld::LiveCellRefBase::isDeleted() const +{ + return mData.isDeletedByContentFile() || mRef.getCount(false) == 0; +} + +namespace MWWorld +{ + std::string makeDynamicCastErrorMessage(const LiveCellRefBase* value, std::string_view recordType) + { + std::stringstream message; + + message << "Bad LiveCellRef cast to " << recordType << " from "; + + if (value != nullptr) + message << value->getTypeDescription(); + else + message << "an empty object"; + + return message.str(); + } +} diff --git a/apps/openmw/mwworld/livecellref.hpp b/apps/openmw/mwworld/livecellref.hpp index 414fde42bdd..c95dd589b2d 100644 --- a/apps/openmw/mwworld/livecellref.hpp +++ b/apps/openmw/mwworld/livecellref.hpp @@ -1,12 +1,12 @@ #ifndef GAME_MWWORLD_LIVECELLREF_H #define GAME_MWWORLD_LIVECELLREF_H -#include - #include "cellref.hpp" #include "refdata.hpp" +#include + namespace ESM { struct ObjectState; @@ -18,10 +18,13 @@ namespace MWWorld class ESMStore; class Class; + template + struct LiveCellRef; + /// Used to create pointers to hold any type of LiveCellRef<> object. struct LiveCellRefBase { - const Class *mClass; + const Class* mClass; /** Information about this instance, such as 3D location and rotation * and individual type-dependent data. @@ -31,40 +34,75 @@ namespace MWWorld /** runtime-data */ RefData mData; - LiveCellRefBase(const std::string& type, const ESM::CellRef &cref=ESM::CellRef()); + LiveCellRefBase(unsigned int type, const ESM::CellRef& cref = ESM::CellRef()); + LiveCellRefBase(unsigned int type, const ESM4::Reference& cref); + LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref); /* Need this for the class to be recognized as polymorphic */ - virtual ~LiveCellRefBase() { } + virtual ~LiveCellRefBase(); - virtual void load (const ESM::ObjectState& state) = 0; + virtual void load(const ESM::ObjectState& state) = 0; ///< Load state into a LiveCellRef, that has already been initialised with base and class. /// /// \attention Must not be called with an invalid \a state. - virtual void save (ESM::ObjectState& state) const = 0; + virtual void save(ESM::ObjectState& state) const = 0; ///< Save LiveCellRef state into \a state. - protected: + virtual std::string_view getTypeDescription() const = 0; + + unsigned int getType() const; + ///< @see MWWorld::Class::getType + + template + static const LiveCellRef* dynamicCast(const LiveCellRefBase* value); - void loadImp (const ESM::ObjectState& state); - ///< Load state into a LiveCellRef, that has already been initialised with base and - /// class. - /// - /// \attention Must not be called with an invalid \a state. + template + static LiveCellRef* dynamicCast(LiveCellRefBase* value); - void saveImp (ESM::ObjectState& state) const; - ///< Save LiveCellRef state into \a state. + /// Returns true if the object was either deleted by the content file or by gameplay. + bool isDeleted() const; - static bool checkStateImp (const ESM::ObjectState& state); - ///< Check if state is valid and report errors. - /// - /// \return Valid? - /// - /// \note Does not check if the RefId exists. + protected: + void loadImp(const ESM::ObjectState& state); + ///< Load state into a LiveCellRef, that has already been initialised with base and + /// class. + /// + /// \attention Must not be called with an invalid \a state. + + void saveImp(ESM::ObjectState& state) const; + ///< Save LiveCellRef state into \a state. + + static bool checkStateImp(const ESM::ObjectState& state); + ///< Check if state is valid and report errors. + /// + /// \return Valid? + /// + /// \note Does not check if the RefId exists. }; - inline bool operator== (const LiveCellRefBase& cellRef, const ESM::RefNum refNum) + inline bool operator==(const LiveCellRefBase& cellRef, const ESM::RefNum refNum) + { + return cellRef.mRef.getRefNum() == refNum; + } + + std::string makeDynamicCastErrorMessage(const LiveCellRefBase* value, std::string_view recordType); + + template + const LiveCellRef* LiveCellRefBase::dynamicCast(const LiveCellRefBase* value) + { + if (const LiveCellRef* ref = dynamic_cast*>(value)) + return ref; + throw std::runtime_error( + makeDynamicCastErrorMessage(value, ESM::getRecNameString(T::sRecordId).toStringView())); + } + + template + LiveCellRef* LiveCellRefBase::dynamicCast(LiveCellRefBase* value) { - return cellRef.mRef.getRefNum()==refNum; + if (LiveCellRef* ref = dynamic_cast*>(value)) + return ref; + throw std::runtime_error( + makeDynamicCastErrorMessage(value, ESM::getRecNameString(T::sRecordId).toStringView())); } /// A reference to one object (of any type) in a cell. @@ -77,25 +115,52 @@ namespace MWWorld struct LiveCellRef : public LiveCellRefBase { LiveCellRef(const ESM::CellRef& cref, const X* b = nullptr) - : LiveCellRefBase(typeid(X).name(), cref), mBase(b) - {} + : LiveCellRefBase(X::sRecordId, cref) + , mBase(b) + { + } + + LiveCellRef(const ESM4::Reference& cref, const X* b = nullptr) + : LiveCellRefBase(X::sRecordId, cref) + , mBase(b) + { + } + + LiveCellRef(const ESM4::ActorCharacter& cref, const X* b = nullptr) + : LiveCellRefBase(X::sRecordId, cref) + , mBase(b) + { + } LiveCellRef(const X* b = nullptr) - : LiveCellRefBase(typeid(X).name()), mBase(b) - {} + : LiveCellRefBase(X::sRecordId) + , mBase(b) + { + } // The object that this instance is based on. const X* mBase; - void load (const ESM::ObjectState& state) override; + void load(const ESM::ObjectState& state) override; ///< Load state into a LiveCellRef, that has already been initialised with base and class. /// /// \attention Must not be called with an invalid \a state. - void save (ESM::ObjectState& state) const override; + void save(ESM::ObjectState& state) const override; ///< Save LiveCellRef state into \a state. - static bool checkState (const ESM::ObjectState& state); + std::string_view getTypeDescription() const override + { + if constexpr (ESM::isESM4Rec(X::sRecordId)) + { + static constexpr ESM::FixedString<6> name = ESM::getRecNameString(X::sRecordId); + return name.toStringView(); + } + else + return X::getRecordType(); + } + + static bool checkState(const ESM::ObjectState& state); ///< Check if state is valid and report errors. /// /// \return Valid? @@ -104,23 +169,22 @@ namespace MWWorld }; template - void LiveCellRef::load (const ESM::ObjectState& state) + void LiveCellRef::load(const ESM::ObjectState& state) { - loadImp (state); + loadImp(state); } template - void LiveCellRef::save (ESM::ObjectState& state) const + void LiveCellRef::save(ESM::ObjectState& state) const { - saveImp (state); + saveImp(state); } template - bool LiveCellRef::checkState (const ESM::ObjectState& state) + bool LiveCellRef::checkState(const ESM::ObjectState& state) { - return checkStateImp (state); + return checkStateImp(state); } - } #endif diff --git a/apps/openmw/mwworld/localscripts.cpp b/apps/openmw/mwworld/localscripts.cpp index 1661d6b9f7a..8f5dc63dfb2 100644 --- a/apps/openmw/mwworld/localscripts.cpp +++ b/apps/openmw/mwworld/localscripts.cpp @@ -1,11 +1,15 @@ #include "localscripts.hpp" #include +#include +#include +#include +#include -#include "esmstore.hpp" #include "cellstore.hpp" #include "class.hpp" #include "containerstore.hpp" +#include "esmstore.hpp" namespace { @@ -20,10 +24,10 @@ namespace bool operator()(const MWWorld::Ptr& ptr) { - if (ptr.getRefData().isDeleted()) + if (ptr.mRef->isDeleted()) return true; - std::string script = ptr.getClass().getScript(ptr); + const ESM::RefId& script = ptr.getClass().getScript(ptr); if (!script.empty()) mScripts.add(script, ptr); @@ -43,19 +47,19 @@ namespace bool operator()(const MWWorld::Ptr& containerPtr) { // Ignore containers without generated content - if (containerPtr.getTypeName() == typeid(ESM::Container).name() && - containerPtr.getRefData().getCustomData() == nullptr) + if (containerPtr.getType() == ESM::Container::sRecordId + && containerPtr.getRefData().getCustomData() == nullptr) return true; MWWorld::ContainerStore& container = containerPtr.getClass().getContainerStore(containerPtr); - for(MWWorld::ContainerStoreIterator it = container.begin(); it != container.end(); ++it) + for (const auto& ptr : container) { - std::string script = it->getClass().getScript(*it); - if(script != "") + const ESM::RefId& script = ptr.getClass().getScript(ptr); + if (!script.empty()) { - MWWorld::Ptr item = *it; + MWWorld::Ptr item = ptr; item.mCell = containerPtr.getCell(); - mScripts.add (script, item); + mScripts.add(script, item); } } return true; @@ -64,7 +68,8 @@ namespace } -MWWorld::LocalScripts::LocalScripts (const MWWorld::ESMStore& store) : mStore (store) +MWWorld::LocalScripts::LocalScripts(const MWWorld::ESMStore& store) + : mStore(store) { mIter = mScripts.end(); } @@ -74,49 +79,46 @@ void MWWorld::LocalScripts::startIteration() mIter = mScripts.begin(); } -bool MWWorld::LocalScripts::getNext(std::pair& script) +bool MWWorld::LocalScripts::getNext(std::pair& script) { - if (mIter!=mScripts.end()) + if (mIter != mScripts.end()) { - std::list >::iterator iter = mIter++; + auto iter = mIter++; script = *iter; return true; } return false; } -void MWWorld::LocalScripts::add (const std::string& scriptName, const Ptr& ptr) +void MWWorld::LocalScripts::add(const ESM::RefId& scriptName, const Ptr& ptr) { - if (const ESM::Script *script = mStore.get().search (scriptName)) + if (const ESM::Script* script = mStore.get().search(scriptName)) { try { - ptr.getRefData().setLocals (*script); + ptr.getRefData().setLocals(*script); - for (std::list >::iterator iter = mScripts.begin(); iter!=mScripts.end(); ++iter) - if (iter->second==ptr) + for (auto iter = mScripts.begin(); iter != mScripts.end(); ++iter) + if (iter->second == ptr) { Log(Debug::Warning) << "Error: tried to add local script twice for " << ptr.getCellRef().getRefId(); remove(ptr); break; } - mScripts.emplace_back (scriptName, ptr); + mScripts.emplace_back(scriptName, ptr); } catch (const std::exception& exception) { - Log(Debug::Error) - << "failed to add local script " << scriptName - << " because an exception has been thrown: " << exception.what(); + Log(Debug::Error) << "failed to add local script " << scriptName + << " because an exception has been thrown: " << exception.what(); } } else - Log(Debug::Warning) - << "failed to add local script " << scriptName - << " because the script does not exist."; + Log(Debug::Warning) << "failed to add local script " << scriptName << " because the script does not exist."; } -void MWWorld::LocalScripts::addCell (CellStore *cell) +void MWWorld::LocalScripts::addCell(CellStore* cell) { AddScriptsVisitor addScriptsVisitor(*this); cell->forEach(addScriptsVisitor); @@ -132,48 +134,51 @@ void MWWorld::LocalScripts::clear() mScripts.clear(); } -void MWWorld::LocalScripts::clearCell (CellStore *cell) +void MWWorld::LocalScripts::clearCell(CellStore* cell) { - std::list >::iterator iter = mScripts.begin(); + auto iter = mScripts.begin(); - while (iter!=mScripts.end()) + while (iter != mScripts.end()) { - if (iter->second.mCell==cell) + if (iter->second.mCell == cell) { - if (iter==mIter) - ++mIter; + if (iter == mIter) + ++mIter; - mScripts.erase (iter++); + mScripts.erase(iter++); } else ++iter; } } -void MWWorld::LocalScripts::remove (RefData *ref) +void MWWorld::LocalScripts::remove(const MWWorld::CellRef* ref) { - for (std::list >::iterator iter = mScripts.begin(); - iter!=mScripts.end(); ++iter) - if (&(iter->second.getRefData()) == ref) + for (auto iter = mScripts.begin(); iter != mScripts.end(); ++iter) + if (&(iter->second.getCellRef()) == ref) { - if (iter==mIter) + if (iter == mIter) ++mIter; - mScripts.erase (iter); + mScripts.erase(iter); break; } } -void MWWorld::LocalScripts::remove (const Ptr& ptr) +void MWWorld::LocalScripts::remove(const Ptr& ptr) { - for (std::list >::iterator iter = mScripts.begin(); - iter!=mScripts.end(); ++iter) - if (iter->second==ptr) + for (auto iter = mScripts.begin(); iter != mScripts.end(); ++iter) + if (iter->second == ptr) { - if (iter==mIter) + if (iter == mIter) ++mIter; - mScripts.erase (iter); + mScripts.erase(iter); break; } } + +bool MWWorld::LocalScripts::isRunning(const ESM::RefId& scriptName, const Ptr& ptr) const +{ + return std::ranges::find(mScripts, std::pair(scriptName, ptr)) != mScripts.end(); +} diff --git a/apps/openmw/mwworld/localscripts.hpp b/apps/openmw/mwworld/localscripts.hpp index c698daf0436..fd7ae23edff 100644 --- a/apps/openmw/mwworld/localscripts.hpp +++ b/apps/openmw/mwworld/localscripts.hpp @@ -15,37 +15,39 @@ namespace MWWorld /// \brief List of active local scripts class LocalScripts { - std::list > mScripts; - std::list >::iterator mIter; - const MWWorld::ESMStore& mStore; + std::list> mScripts; + std::list>::iterator mIter; + const MWWorld::ESMStore& mStore; - public: + public: + LocalScripts(const MWWorld::ESMStore& store); - LocalScripts (const MWWorld::ESMStore& store); + void startIteration(); + ///< Set the iterator to the begin of the script list. - void startIteration(); - ///< Set the iterator to the begin of the script list. + bool getNext(std::pair& script); + ///< Get next local script + /// @return Did we get a script? - bool getNext(std::pair& script); - ///< Get next local script - /// @return Did we get a script? + void add(const ESM::RefId& scriptName, const Ptr& ptr); + ///< Add script to collection of active local scripts. - void add (const std::string& scriptName, const Ptr& ptr); - ///< Add script to collection of active local scripts. + void addCell(CellStore* cell); + ///< Add all local scripts in a cell. - void addCell (CellStore *cell); - ///< Add all local scripts in a cell. + void clear(); + ///< Clear active local scripts collection. - void clear(); - ///< Clear active local scripts collection. + void clearCell(CellStore* cell); + ///< Remove all scripts belonging to \a cell. - void clearCell (CellStore *cell); - ///< Remove all scripts belonging to \a cell. - - void remove (RefData *ref); + void remove(const MWWorld::CellRef* ref); - void remove (const Ptr& ptr); - ///< Remove script for given reference (ignored if reference does not have a script listed). + void remove(const Ptr& ptr); + ///< Remove script for given reference (ignored if reference does not have a script listed). + + bool isRunning(const ESM::RefId&, const Ptr&) const; + ///< Is the local script running?. }; } diff --git a/apps/openmw/mwworld/magiceffects.cpp b/apps/openmw/mwworld/magiceffects.cpp new file mode 100644 index 00000000000..38f17677ef4 --- /dev/null +++ b/apps/openmw/mwworld/magiceffects.cpp @@ -0,0 +1,260 @@ +#include "magiceffects.hpp" +#include "esmstore.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwworld/worldmodel.hpp" + +#include "../mwmechanics/magiceffects.hpp" + +namespace +{ + template + void getEnchantedItem(const ESM::RefId& id, ESM::RefId& enchantment, std::string& itemName) + { + const T* item = MWBase::Environment::get().getESMStore()->get().search(id); + if (item) + { + enchantment = item->mEnchant; + itemName = item->mName; + } + } +} + +namespace MWWorld +{ + void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats) + { + const auto& store = *MWBase::Environment::get().getESMStore(); + // Convert corprus to format 10 + for (const auto& [id, oldStats] : creatureStats.mSpells.mCorprusSpells) + { + const ESM::Spell* spell = store.get().search(id); + if (!spell) + continue; + + ESM::CreatureStats::CorprusStats stats; + stats.mNextWorsening = oldStats.mNextWorsening; + stats.mWorsenings.fill(0); + + for (auto& effect : spell->mEffects.mList) + { + if (effect.mData.mEffectID == ESM::MagicEffect::DrainAttribute) + stats.mWorsenings[effect.mData.mAttribute] = oldStats.mWorsenings; + } + creatureStats.mCorprusSpells[id] = stats; + } + // Convert to format 17 + for (const auto& [id, oldParams] : creatureStats.mSpells.mSpellParams) + { + const ESM::Spell* spell = store.get().search(id); + if (!spell || spell->mData.mType == ESM::Spell::ST_Spell || spell->mData.mType == ESM::Spell::ST_Power) + continue; + ESM::ActiveSpells::ActiveSpellParams params; + params.mSourceSpellId = id; + params.mDisplayName = spell->mName; + params.mCasterActorId = creatureStats.mActorId; + if (spell->mData.mType == ESM::Spell::ST_Ability) + params.mFlags = ESM::Compatibility::ActiveSpells::Type_Ability_Flags; + else + params.mFlags = ESM::Compatibility::ActiveSpells::Type_Permanent_Flags; + params.mWorsenings = -1; + params.mNextWorsening = ESM::TimeStamp(); + for (const auto& enam : spell->mEffects.mList) + { + if (oldParams.mPurgedEffects.find(enam.mIndex) == oldParams.mPurgedEffects.end()) + { + ESM::ActiveEffect effect; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effect.mEffectIndex = enam.mIndex; + auto rand = oldParams.mEffectRands.find(enam.mIndex); + if (rand != oldParams.mEffectRands.end()) + { + float magnitude + = (enam.mData.mMagnMax - enam.mData.mMagnMin) * rand->second + enam.mData.mMagnMin; + effect.mMagnitude = magnitude; + effect.mMinMagnitude = magnitude; + effect.mMaxMagnitude = magnitude; + // Prevent recalculation of resistances and don't reflect or absorb the effect + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances + | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + } + else + { + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; + effect.mFlags = ESM::ActiveEffect::Flag_None; + } + params.mEffects.emplace_back(effect); + } + } + creatureStats.mActiveSpells.mSpells.emplace_back(params); + } + std::multimap equippedItems; + for (std::size_t i = 0; i < inventory.mItems.size(); ++i) + { + ESM::ObjectState& item = inventory.mItems[i]; + auto slot = inventory.mEquipmentSlots.find(i); + if (slot != inventory.mEquipmentSlots.end()) + { + MWBase::Environment::get().getWorldModel()->assignSaveFileRefNum(item.mRef); + equippedItems.emplace(item.mRef.mRefID, item.mRef.mRefNum); + } + } + for (const auto& [id, oldMagnitudes] : inventory.mPermanentMagicEffectMagnitudes) + { + ESM::RefId eId; + std::string name; + switch (store.find(id)) + { + case ESM::REC_ARMO: + getEnchantedItem(id, eId, name); + break; + case ESM::REC_CLOT: + getEnchantedItem(id, eId, name); + break; + case ESM::REC_WEAP: + getEnchantedItem(id, eId, name); + break; + } + if (eId.empty()) + continue; + const ESM::Enchantment* enchantment = store.get().search(eId); + if (!enchantment) + continue; + ESM::ActiveSpells::ActiveSpellParams params; + params.mSourceSpellId = id; + params.mDisplayName = std::move(name); + params.mCasterActorId = creatureStats.mActorId; + params.mFlags = ESM::Compatibility::ActiveSpells::Type_Enchantment_Flags; + params.mWorsenings = -1; + params.mNextWorsening = ESM::TimeStamp(); + for (const auto& enam : enchantment->mEffects.mList) + { + auto [random, multiplier] = oldMagnitudes[enam.mIndex]; + float magnitude = (enam.mData.mMagnMax - enam.mData.mMagnMin) * random + enam.mData.mMagnMin; + magnitude *= multiplier; + if (magnitude <= 0) + continue; + ESM::ActiveEffect effect; + effect.mEffectId = enam.mData.mEffectID; + effect.mMagnitude = magnitude; + effect.mMinMagnitude = magnitude; + effect.mMaxMagnitude = magnitude; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effect.mEffectIndex = enam.mIndex; + // Prevent recalculation of resistances and don't reflect or absorb the effect + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect + | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + params.mEffects.emplace_back(effect); + } + auto [begin, end] = equippedItems.equal_range(id); + for (auto it = begin; it != end; ++it) + { + params.mItem = it->second; + creatureStats.mActiveSpells.mSpells.emplace_back(params); + } + } + for (const auto& spell : creatureStats.mCorprusSpells) + { + auto it + = std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), + [&](const auto& params) { return params.mSourceSpellId == spell.first; }); + if (it != creatureStats.mActiveSpells.mSpells.end()) + { + it->mNextWorsening = spell.second.mNextWorsening; + int worsenings = 0; + for (const auto& worsening : spell.second.mWorsenings) + worsenings = std::max(worsening, worsenings); + it->mWorsenings = worsenings; + } + } + for (const auto& [key, actorId] : creatureStats.mSummonedCreatureMap) + { + if (actorId < 0) + continue; + for (auto& params : creatureStats.mActiveSpells.mSpells) + { + if (params.mSourceSpellId == key.mSourceId) + { + bool found = false; + for (auto& effect : params.mEffects) + { + if (effect.mEffectId == key.mEffectId && effect.mEffectIndex == key.mEffectIndex) + { + effect.mArg = ESM::RefId::generated(static_cast(actorId)); + effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; + found = true; + break; + } + } + if (found) + break; + } + } + } + // Reset modifiers that were previously recalculated each frame + for (auto& attribute : creatureStats.mAttributes) + attribute.mMod = 0.f; + for (auto& dynamic : creatureStats.mDynamic) + { + dynamic.mCurrent -= dynamic.mMod - dynamic.mBase; + dynamic.mMod = 0.f; + } + for (auto& setting : creatureStats.mAiSettings) + setting.mMod = 0.f; + if (npcStats) + { + for (auto& skill : npcStats->mSkills) + skill.mMod = 0.f; + } + } + + // Versions 17-19 wrote different modifiers to the savegame depending on whether the save had upgraded from a pre-17 + // version or not + void convertStats(ESM::CreatureStats& creatureStats) + { + for (auto& dynamic : creatureStats.mDynamic) + dynamic.mMod = 0.f; + for (auto& setting : creatureStats.mAiSettings) + setting.mMod = 0.f; + } + + // Versions 17-27 wrote an equipment slot index to mItem + void convertEnchantmentSlots(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory) + { + for (auto& activeSpell : creatureStats.mActiveSpells.mSpells) + { + if (!activeSpell.mItem.isSet()) + continue; + if (activeSpell.mFlags & ESM::ActiveSpells::Flag_Equipment) + { + std::int64_t slotIndex = activeSpell.mItem.mIndex; + auto slot = std::find_if(inventory.mEquipmentSlots.begin(), inventory.mEquipmentSlots.end(), + [=](const auto& entry) { return entry.second == slotIndex; }); + if (slot != inventory.mEquipmentSlots.end() && slot->first < inventory.mItems.size()) + { + ESM::CellRef& ref = inventory.mItems[slot->first].mRef; + MWBase::Environment::get().getWorldModel()->assignSaveFileRefNum(ref); + activeSpell.mItem = ref.mRefNum; + continue; + } + } + activeSpell.mItem = {}; + } + } +} diff --git a/apps/openmw/mwworld/magiceffects.hpp b/apps/openmw/mwworld/magiceffects.hpp new file mode 100644 index 00000000000..3acb14fff20 --- /dev/null +++ b/apps/openmw/mwworld/magiceffects.hpp @@ -0,0 +1,21 @@ +#ifndef OPENMW_MWWORLD_MAGICEFFECTS_H +#define OPENMW_MWWORLD_MAGICEFFECTS_H + +namespace ESM +{ + struct CreatureStats; + struct InventoryState; + struct NpcStats; +} + +namespace MWWorld +{ + void convertMagicEffects( + ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats = nullptr); + + void convertStats(ESM::CreatureStats& creatureStats); + + void convertEnchantmentSlots(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory); +} + +#endif diff --git a/apps/openmw/mwworld/manualref.cpp b/apps/openmw/mwworld/manualref.cpp index 7f7fd603358..81ab4b54197 100644 --- a/apps/openmw/mwworld/manualref.cpp +++ b/apps/openmw/mwworld/manualref.cpp @@ -1,68 +1,109 @@ #include "manualref.hpp" +#include +#include #include "esmstore.hpp" namespace { - template - void create(const MWWorld::Store& list, const std::string& name, boost::any& refValue, MWWorld::Ptr& ptrValue) + template + void create(const MWWorld::Store& list, const ESM::RefId& name, std::any& refValue, MWWorld::Ptr& ptrValue) { const T* base = list.find(name); ESM::CellRef cellRef; - cellRef.mRefNum.unset(); + cellRef.blank(); cellRef.mRefID = name; - cellRef.mScale = 1; - cellRef.mFactionRank = 0; - cellRef.mChargeInt = -1; - cellRef.mChargeIntRemainder = 0.0f; - cellRef.mGoldValue = 1; - cellRef.mEnchantmentCharge = -1; - cellRef.mTeleport = false; - cellRef.mLockLevel = 0; - cellRef.mReferenceBlocked = 0; - MWWorld::LiveCellRef ref(cellRef, base); + refValue = MWWorld::LiveCellRef(cellRef, base); + ptrValue = MWWorld::Ptr(&std::any_cast&>(refValue), nullptr); + } + + template + void create( + const MWWorld::Store& list, const MWWorld::Ptr& templatePtr, std::any& refValue, MWWorld::Ptr& ptrValue) + { + refValue = *static_cast*>(templatePtr.getBase()); + ptrValue = MWWorld::Ptr(&std::any_cast&>(refValue), nullptr); + } + + template + void visitRefStore(const MWWorld::ESMStore& store, ESM::RefId name, F func) + { + switch (store.find(name)) + { + case ESM::REC_ACTI: + return func(store.get()); + case ESM::REC_ALCH: + return func(store.get()); + case ESM::REC_APPA: + return func(store.get()); + case ESM::REC_ARMO: + return func(store.get()); + case ESM::REC_BOOK: + return func(store.get()); + case ESM::REC_CLOT: + return func(store.get()); + case ESM::REC_CONT: + return func(store.get()); + case ESM::REC_CREA: + return func(store.get()); + case ESM::REC_DOOR: + return func(store.get()); + case ESM::REC_INGR: + return func(store.get()); + case ESM::REC_LEVC: + return func(store.get()); + case ESM::REC_LEVI: + return func(store.get()); + case ESM::REC_LIGH: + return func(store.get()); + case ESM::REC_LOCK: + return func(store.get()); + case ESM::REC_MISC: + return func(store.get()); + case ESM::REC_NPC_: + return func(store.get()); + case ESM::REC_PROB: + return func(store.get()); + case ESM::REC_REPA: + return func(store.get()); + case ESM::REC_STAT: + return func(store.get()); + case ESM::REC_WEAP: + return func(store.get()); + case ESM::REC_BODY: + return func(store.get()); + case ESM::REC_STAT4: + return func(store.get()); + case ESM::REC_TERM4: + return func(store.get()); + case 0: + throw std::logic_error( + "failed to create manual cell ref for " + name.toDebugString() + " (unknown ID)"); - refValue = ref; - ptrValue = MWWorld::Ptr(&boost::any_cast&>(refValue), nullptr); + default: + throw std::logic_error( + "failed to create manual cell ref for " + name.toDebugString() + " (unknown type)"); + } } } -MWWorld::ManualRef::ManualRef(const MWWorld::ESMStore& store, const std::string& name, const int count) +MWWorld::ManualRef::ManualRef(const MWWorld::ESMStore& store, const ESM::RefId& name, const int count) { - std::string lowerName = Misc::StringUtils::lowerCase(name); - switch (store.find(lowerName)) - { - case ESM::REC_ACTI: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_ALCH: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_APPA: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_ARMO: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_BOOK: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_CLOT: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_CONT: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_CREA: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_DOOR: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_INGR: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_LEVC: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_LEVI: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_LIGH: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_LOCK: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_MISC: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_NPC_: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_PROB: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_REPA: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_STAT: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_WEAP: create(store.get(), lowerName, mRef, mPtr); break; - case ESM::REC_BODY: create(store.get(), lowerName, mRef, mPtr); break; + auto cb = [&](const auto& store) { create(store, name, mRef, mPtr); }; + visitRefStore(store, name, cb); - case 0: - throw std::logic_error("failed to create manual cell ref for " + lowerName + " (unknown ID)"); + mPtr.getCellRef().setCount(count); +} - default: - throw std::logic_error("failed to create manual cell ref for " + lowerName + " (unknown type)"); - } +MWWorld::ManualRef::ManualRef(const ESMStore& store, const Ptr& template_, const int count) +{ + auto cb = [&](const auto& store) { create(store, template_, mRef, mPtr); }; + visitRefStore(store, template_.getCellRef().getRefId(), cb); - mPtr.getRefData().setCount(count); + mPtr.getCellRef().setCount(count); + mPtr.getCellRef().unsetRefNum(); + mPtr.getRefData().setLuaScripts(nullptr); } diff --git a/apps/openmw/mwworld/manualref.hpp b/apps/openmw/mwworld/manualref.hpp index 2fc59947107..4611ed95bb7 100644 --- a/apps/openmw/mwworld/manualref.hpp +++ b/apps/openmw/mwworld/manualref.hpp @@ -1,28 +1,28 @@ #ifndef GAME_MWWORLD_MANUALREF_H #define GAME_MWWORLD_MANUALREF_H -#include +#include #include "ptr.hpp" namespace MWWorld { - /// \brief Manually constructed live cell ref + /// \brief Manually constructed live cell ref. The resulting Ptr shares its lifetime with this ManualRef and must + /// not be used past its end. class ManualRef { - boost::any mRef; - Ptr mPtr; + // Stores the ref (LiveCellRef) by value. + std::any mRef; + Ptr mPtr; - ManualRef (const ManualRef&); - ManualRef& operator= (const ManualRef&); + ManualRef(const ManualRef&); + ManualRef& operator=(const ManualRef&); - public: - ManualRef(const MWWorld::ESMStore& store, const std::string& name, const int count = 1); + public: + ManualRef(const MWWorld::ESMStore& store, const ESM::RefId& name, const int count = 1); + ManualRef(const MWWorld::ESMStore& store, const MWWorld::Ptr& template_, const int count = 1); - const Ptr& getPtr() const - { - return mPtr; - } + const Ptr& getPtr() const { return mPtr; } }; } diff --git a/apps/openmw/mwworld/movementdirection.hpp b/apps/openmw/mwworld/movementdirection.hpp new file mode 100644 index 00000000000..6784db67dd4 --- /dev/null +++ b/apps/openmw/mwworld/movementdirection.hpp @@ -0,0 +1,17 @@ +#ifndef OPENMW_APPS_OPENMW_MWWORLD_MOVEMENTDIRECTION_H +#define OPENMW_APPS_OPENMW_MWWORLD_MOVEMENTDIRECTION_H + +namespace MWWorld +{ + using MovementDirectionFlags = unsigned char; + + enum MovementDirectionFlag : MovementDirectionFlags + { + MovementDirectionFlag_Forward = 1 << 0, + MovementDirectionFlag_Back = 1 << 1, + MovementDirectionFlag_Left = 1 << 2, + MovementDirectionFlag_Right = 1 << 3, + }; +} + +#endif diff --git a/apps/openmw/mwworld/nullaction.hpp b/apps/openmw/mwworld/nullaction.hpp index 4ee1115e50c..2499655eff5 100644 --- a/apps/openmw/mwworld/nullaction.hpp +++ b/apps/openmw/mwworld/nullaction.hpp @@ -8,9 +8,9 @@ namespace MWWorld /// \brief Action: do nothing class NullAction : public Action { - void executeImp (const Ptr& actor) override {} + void executeImp(const Ptr& actor) override {} - bool isNullAction() override { return true; } + bool isNullAction() override { return true; } }; } diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index d8e2fb2f0d1..f5d38e86861 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -4,46 +4,51 @@ #include -#include -#include -#include #include -#include +#include +#include +#include +#include +#include +#include #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/magiceffects.hpp" +#include "../mwworld/worldmodel.hpp" #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/windowmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwmechanics/movement.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellutil.hpp" +#include "../mwrender/camera.hpp" +#include "../mwrender/renderingmanager.hpp" + +#include "cellstore.hpp" #include "class.hpp" #include "ptr.hpp" -#include "cellstore.hpp" namespace MWWorld { - Player::Player (const ESM::NPC *player) - : mCellStore(nullptr), - mLastKnownExteriorPosition(0,0,0), - mMarkedPosition(ESM::Position()), - mMarkedCell(nullptr), - mAutoMove(false), - mForwardBackward(0), - mTeleported(false), - mCurrentCrimeId(-1), - mPaidCrimeId(-1), - mAttackingOrSpell(false), - mJumping(false) + Player::Player(const ESM::NPC* player) + : mCellStore(nullptr) + , mLastKnownExteriorPosition(0, 0, 0) + , mMarkedPosition(ESM::Position()) + , mMarkedCell(nullptr) + , mTeleported(false) + , mCurrentCrimeId(-1) + , mPaidCrimeId(-1) + , mJumping(false) { ESM::CellRef cellRef; cellRef.blank(); - cellRef.mRefID = "player"; + cellRef.mRefID = ESM::RefId::stringRefId("Player"); mPlayer = LiveCellRef(cellRef, player); ESM::Position playerPos = mPlayer.mData.getPosition(); @@ -55,150 +60,98 @@ namespace MWWorld { MWMechanics::NpcStats& stats = getPlayer().getClass().getNpcStats(getPlayer()); - for (int i=0; i& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const auto& store = MWBase::Environment::get().getESMStore(); + const MWWorld::Store& gmst = store->get(); MWMechanics::CreatureStats& creatureStats = getPlayer().getClass().getCreatureStats(getPlayer()); MWMechanics::NpcStats& npcStats = getPlayer().getClass().getNpcStats(getPlayer()); MWMechanics::DynamicStat health = creatureStats.getDynamic(0); - creatureStats.setHealth(int(health.getBase() / gmst.find("fWereWolfHealth")->mValue.getFloat())); - for (int i=0; imValue.getFloat()); + for (size_t i = 0; i < mSaveSkills.size(); ++i) + { + auto& skill = npcStats.getSkill(ESM::Skill::indexToRefId(i)); + skill.restore(skill.getDamage()); + skill.setModifier(mSaveSkills[i] - skill.getBase()); + } + for (size_t i = 0; i < mSaveAttributes.size(); ++i) + { + auto id = ESM::Attribute::indexToRefId(i); + auto attribute = npcStats.getAttribute(id); + attribute.restore(attribute.getDamage()); + attribute.setModifier(mSaveAttributes[i] - attribute.getBase()); + npcStats.setAttribute(id, attribute); + } } void Player::setWerewolfStats() { - const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const auto& store = MWBase::Environment::get().getESMStore(); + const MWWorld::Store& gmst = store->get(); MWMechanics::CreatureStats& creatureStats = getPlayer().getClass().getCreatureStats(getPlayer()); MWMechanics::NpcStats& npcStats = getPlayer().getClass().getNpcStats(getPlayer()); MWMechanics::DynamicStat health = creatureStats.getDynamic(0); - creatureStats.setHealth(int(health.getBase() * gmst.find("fWereWolfHealth")->mValue.getFloat())); - for(size_t i = 0;i < ESM::Attribute::Length;++i) + creatureStats.setHealth(health.getBase() * gmst.find("fWereWolfHealth")->mValue.getFloat()); + for (const auto& attribute : store->get()) { - // Oh, Bethesda. It's "Intelligence". - std::string name = "fWerewolf"+((i==ESM::Attribute::Intelligence) ? std::string("Intellegence") : - ESM::Attribute::sAttributeNames[i]); - - MWMechanics::AttributeValue value = npcStats.getAttribute(i); - value.setBase(int(gmst.find(name)->mValue.getFloat())); - npcStats.setAttribute(i, value); + MWMechanics::AttributeValue value = npcStats.getAttribute(attribute.mId); + value.setModifier(attribute.mWerewolfValue - value.getModified()); + npcStats.setAttribute(attribute.mId, value); } - for(size_t i = 0;i < ESM::Skill::Length;i++) + for (const auto& skill : store->get()) { // Acrobatics is set separately for some reason. - if(i == ESM::Skill::Acrobatics) + if (skill.mId == ESM::Skill::Acrobatics) continue; - // "Mercantile"! >_< - std::string name = "fWerewolf"+((i==ESM::Skill::Mercantile) ? std::string("Merchantile") : - ESM::Skill::sSkillNames[i]); - - MWMechanics::SkillValue value = npcStats.getSkill(i); - value.setBase(int(gmst.find(name)->mValue.getFloat())); - npcStats.setSkill(i, value); + MWMechanics::SkillValue& value = npcStats.getSkill(skill.mId); + value.setModifier(skill.mWerewolfValue - value.getModified()); } } - void Player::set(const ESM::NPC *player) + void Player::set(const ESM::NPC* player) { mPlayer.mBase = player; } - void Player::setCell (MWWorld::CellStore *cellStore) + void Player::setCell(MWWorld::CellStore* cellStore) { mCellStore = cellStore; } MWWorld::Ptr Player::getPlayer() { - MWWorld::Ptr ptr (&mPlayer, mCellStore); + MWWorld::Ptr ptr(&mPlayer, mCellStore); return ptr; } MWWorld::ConstPtr Player::getConstPlayer() const { - MWWorld::ConstPtr ptr (&mPlayer, mCellStore); + MWWorld::ConstPtr ptr(&mPlayer, mCellStore); return ptr; } - void Player::setBirthSign (const std::string &sign) + void Player::setBirthSign(const ESM::RefId& sign) { mSign = sign; } - const std::string& Player::getBirthSign() const + const ESM::RefId& Player::getBirthSign() const { return mSign; } - void Player::setDrawState (MWMechanics::DrawState_ state) - { - MWWorld::Ptr ptr = getPlayer(); - ptr.getClass().getNpcStats(ptr).setDrawState (state); - } - - bool Player::getAutoMove() const - { - return mAutoMove; - } - - void Player::setAutoMove (bool enable) - { - MWWorld::Ptr ptr = getPlayer(); - - mAutoMove = enable; - - int value = mForwardBackward; - - if (mAutoMove) - value = 1; - - ptr.getClass().getMovementSettings(ptr).mPosition[1] = value; - } - - void Player::setLeftRight (float value) - { - MWWorld::Ptr ptr = getPlayer(); - ptr.getClass().getMovementSettings(ptr).mPosition[0] = value; - } - - void Player::setForwardBackward (float value) - { - MWWorld::Ptr ptr = getPlayer(); - - mForwardBackward = value; - - if (mAutoMove) - value = 1; - - ptr.getClass().getMovementSettings(ptr).mPosition[1] = value; - } - - void Player::setUpDown(int value) + void Player::setDrawState(MWMechanics::DrawState state) { MWWorld::Ptr ptr = getPlayer(); - ptr.getClass().getMovementSettings(ptr).mPosition[2] = static_cast(value); - } - - void Player::setRunState(bool run) - { - MWWorld::Ptr ptr = getPlayer(); - ptr.getClass().getCreatureStats(ptr).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, run); - } - - void Player::setSneak(bool sneak) - { - MWWorld::Ptr ptr = getPlayer(); - ptr.getClass().getCreatureStats(ptr).setMovementFlag(MWMechanics::CreatureStats::Flag_Sneak, sneak); + ptr.getClass().getNpcStats(ptr).setDrawState(state); } void Player::yaw(float yaw) @@ -217,10 +170,10 @@ namespace MWWorld ptr.getClass().getMovementSettings(ptr).mRotation[1] += roll; } - MWMechanics::DrawState_ Player::getDrawState() + MWMechanics::DrawState Player::getDrawState() { - MWWorld::Ptr ptr = getPlayer(); - return ptr.getClass().getNpcStats(ptr).getDrawState(); + MWWorld::Ptr ptr = getPlayer(); + return ptr.getClass().getNpcStats(ptr).getDrawState(); } void Player::activate() @@ -229,9 +182,8 @@ namespace MWWorld return; MWWorld::Ptr player = getPlayer(); - const MWMechanics::NpcStats &playerStats = player.getClass().getNpcStats(player); - bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); - if ((!godmode && playerStats.isParalyzed()) || playerStats.getKnockedDown() || playerStats.isDead()) + const MWMechanics::NpcStats& playerStats = player.getClass().getNpcStats(player); + if (playerStats.isParalyzed() || playerStats.getKnockedDown() || playerStats.isDead()) return; MWWorld::Ptr toActivate = MWBase::Environment::get().getWorld()->getFacedObject(); @@ -242,7 +194,7 @@ namespace MWWorld if (!toActivate.getClass().hasToolTip(toActivate)) return; - MWBase::Environment::get().getWorld()->activate(toActivate, player); + MWBase::Environment::get().getLuaManager()->objectActivated(toActivate, player); } bool Player::wasTeleported() const @@ -255,16 +207,6 @@ namespace MWWorld mTeleported = teleported; } - void Player::setAttackingOrSpell(bool attackingOrSpell) - { - mAttackingOrSpell = attackingOrSpell; - } - - bool Player::getAttackingOrSpell() const - { - return mAttackingOrSpell; - } - void Player::setJumping(bool jumping) { mJumping = jumping; @@ -275,7 +217,8 @@ namespace MWWorld return mJumping; } - bool Player::isInCombat() { + bool Player::isInCombat() + { return MWBase::Environment::get().getMechanicsManager()->getActorsFighting(getPlayer()).size() != 0; } @@ -284,13 +227,13 @@ namespace MWWorld return MWBase::Environment::get().getMechanicsManager()->getEnemiesNearby(getPlayer()).size() != 0; } - void Player::markPosition(CellStore *markedCell, const ESM::Position& markedPosition) + void Player::markPosition(CellStore* markedCell, const ESM::Position& markedPosition) { mMarkedCell = markedCell; mMarkedPosition = markedPosition; } - void Player::getMarkedPosition(CellStore*& markedCell, ESM::Position &markedPosition) const + void Player::getMarkedPosition(CellStore*& markedCell, ESM::Position& markedPosition) const { markedCell = mMarkedCell; if (mMarkedCell) @@ -299,30 +242,23 @@ namespace MWWorld void Player::clear() { + ESM::CellRef cellRef; + cellRef.blank(); + cellRef.mRefID = ESM::RefId::stringRefId("Player"); + cellRef.mRefNum = mPlayer.mRef.getRefNum(); + mPlayer = LiveCellRef(cellRef, mPlayer.mBase); mCellStore = nullptr; - mSign.clear(); + mSign = ESM::RefId(); mMarkedCell = nullptr; - mAutoMove = false; - mForwardBackward = 0; mTeleported = false; - mAttackingOrSpell = false; mJumping = false; mCurrentCrimeId = -1; mPaidCrimeId = -1; mPreviousItems.clear(); - mLastKnownExteriorPosition = osg::Vec3f(0,0,0); - - for (int i=0; igetCell()->getCellId(); + mPlayer.save(player.mObject); + player.mCellId = mCellStore->getCell()->getId(); player.mCurrentCrimeId = mCurrentCrimeId; player.mPaidCrimeId = mPaidCrimeId; @@ -352,35 +288,45 @@ namespace MWWorld { player.mHasMark = true; player.mMarkedPosition = mMarkedPosition; - player.mMarkedCell = mMarkedCell->getCell()->getCellId(); + player.mMarkedCell = mMarkedCell->getCell()->getId(); } else player.mHasMark = false; - for (int i=0; ideregisterLiveCellRef(mPlayer); + mPlayer.load(player.mObject); - for (int i=0; iapplyWerewolfAcrobatics(getPlayer()); + } } getPlayer().getClass().getCreatureStats(getPlayer()).getAiSequence().clear(); MWBase::World& world = *MWBase::Environment::get().getWorld(); - try - { - mCellStore = world.getCell (player.mCellId); - } - catch (...) - { - Log(Debug::Warning) << "Warning: Player cell '" << player.mCellId.mWorldspace << "' no longer exists"; - // Cell no longer exists. The loader will have to choose a default cell. - mCellStore = nullptr; - } + mCellStore = MWBase::Environment::get().getWorldModel()->findCell(player.mCellId); + if (mCellStore == nullptr) + Log(Debug::Warning) << "Player cell " << player.mCellId << " no longer exists"; if (!player.mBirthsign.empty()) { - const ESM::BirthSign* sign = world.getStore().get().search (player.mBirthsign); + const ESM::BirthSign* sign = world.getStore().get().search(player.mBirthsign); if (!sign) - throw std::runtime_error ("invalid player state record (birthsign does not exist)"); + throw std::runtime_error("invalid player state record (birthsign does not exist)"); } mCurrentCrimeId = player.mCurrentCrimeId; @@ -432,26 +376,22 @@ namespace MWWorld mLastKnownExteriorPosition.y() = player.mLastKnownExteriorPosition[1]; mLastKnownExteriorPosition.z() = player.mLastKnownExteriorPosition[2]; - if (player.mHasMark && !player.mMarkedCell.mPaged) + if (player.mHasMark) { - // interior cell -> need to check if it exists (exterior cell will be - // generated on the fly) - - if (!world.getStore().get().search (player.mMarkedCell.mWorldspace)) + if (!world.getStore().get().search(player.mMarkedCell)) player.mHasMark = false; // drop mark silently } if (player.mHasMark) { mMarkedPosition = player.mMarkedPosition; - mMarkedCell = world.getCell (player.mMarkedCell); + mMarkedCell = &MWBase::Environment::get().getWorldModel()->getCell(player.mMarkedCell); } else { mMarkedCell = nullptr; } - mForwardBackward = 0; mTeleported = false; mPreviousItems = player.mPreviousItems; @@ -477,22 +417,22 @@ namespace MWWorld return mPaidCrimeId; } - void Player::setPreviousItem(const std::string& boundItemId, const std::string& previousItemId) + void Player::setPreviousItem(const ESM::RefId& boundItemId, const ESM::RefId& previousItemId) { mPreviousItems[boundItemId] = previousItemId; } - std::string Player::getPreviousItem(const std::string& boundItemId) + ESM::RefId Player::getPreviousItem(const ESM::RefId& boundItemId) { return mPreviousItems[boundItemId]; } - void Player::erasePreviousItem(const std::string& boundItemId) + void Player::erasePreviousItem(const ESM::RefId& boundItemId) { mPreviousItems.erase(boundItemId); } - void Player::setSelectedSpell(const std::string& spellId) + void Player::setSelectedSpell(const ESM::RefId& spellId) { Ptr player = getPlayer(); InventoryStore& store = player.getClass().getInventoryStore(player); @@ -501,4 +441,57 @@ namespace MWWorld MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, castChance); MWBase::Environment::get().getWindowManager()->updateSpellWindow(); } + + void Player::update() + { + auto player = getPlayer(); + const auto world = MWBase::Environment::get().getWorld(); + const auto rendering = world->getRenderingManager(); + auto& store = world->getStore(); + auto& playerClass = player.getClass(); + const auto windowMgr = MWBase::Environment::get().getWindowManager(); + + if (player.getCell()->isExterior()) + { + ESM::Position pos = player.getRefData().getPosition(); + setLastKnownExteriorPosition(pos.asVec3()); + } + + bool isWerewolf = playerClass.getNpcStats(player).isWerewolf(); + bool isFirstPerson = world->isFirstPerson(); + if (isWerewolf && isFirstPerson) + { + float werewolfFov = Fallback::Map::getFloat("General_Werewolf_FOV"); + if (werewolfFov != 0) + rendering->overrideFieldOfView(werewolfFov); + windowMgr->setWerewolfOverlay(true); + } + else + { + rendering->resetFieldOfView(); + windowMgr->setWerewolfOverlay(false); + } + + // Sink the camera while sneaking + bool sneaking = playerClass.getCreatureStats(player).getStance(MWMechanics::CreatureStats::Stance_Sneak); + bool swimming = world->isSwimming(player); + bool flying = world->isFlying(player); + + static const float i1stPersonSneakDelta + = store.get().find("i1stPersonSneakDelta")->mValue.getFloat(); + if (sneaking && !swimming && !flying) + rendering->getCamera()->setSneakOffset(i1stPersonSneakDelta); + else + rendering->getCamera()->setSneakOffset(0.f); + + int blind = 0; + const auto& magicEffects = playerClass.getCreatureStats(player).getMagicEffects(); + if (!world->getGodModeState()) + blind = static_cast(magicEffects.getOrDefault(ESM::MagicEffect::Blind).getModifier()); + windowMgr->setBlindness(std::clamp(blind, 0, 100)); + + int nightEye = static_cast(magicEffects.getOrDefault(ESM::MagicEffect::NightEye).getMagnitude()); + rendering->setNightEyeFactor(std::min(1.f, (nightEye / 100.f))); + } + } diff --git a/apps/openmw/mwworld/player.hpp b/apps/openmw/mwworld/player.hpp index 1e4b0ffdf5f..0d56833df91 100644 --- a/apps/openmw/mwworld/player.hpp +++ b/apps/openmw/mwworld/player.hpp @@ -1,20 +1,20 @@ #ifndef GAME_MWWORLD_PLAYER_H #define GAME_MWWORLD_PLAYER_H +#include #include -#include "../mwworld/refdata.hpp" #include "../mwworld/livecellref.hpp" #include "../mwmechanics/drawstate.hpp" -#include "../mwmechanics/stat.hpp" -#include #include +#include +#include +#include namespace ESM { - struct NPC; class ESMWriter; class ESMReader; } @@ -32,78 +32,63 @@ namespace MWWorld /// \brief NPC object representing the player and additional player data class Player { - LiveCellRef mPlayer; - MWWorld::CellStore *mCellStore; - std::string mSign; + LiveCellRef mPlayer; + MWWorld::CellStore* mCellStore; + ESM::RefId mSign; osg::Vec3f mLastKnownExteriorPosition; - ESM::Position mMarkedPosition; + ESM::Position mMarkedPosition; // If no position was marked, this is nullptr - CellStore* mMarkedCell; + CellStore* mMarkedCell; - bool mAutoMove; - float mForwardBackward; - bool mTeleported; + bool mTeleported; - int mCurrentCrimeId; // the id assigned witnesses - int mPaidCrimeId; // the last id paid off (0 bounty) + int mCurrentCrimeId; // the id assigned witnesses + int mPaidCrimeId; // the last id paid off (0 bounty) - typedef std::map PreviousItems; // previous equipped items, needed for bound spells + typedef std::map PreviousItems; // previous equipped items, needed for bound spells PreviousItems mPreviousItems; // Saved stats prior to becoming a werewolf - MWMechanics::SkillValue mSaveSkills[ESM::Skill::Length]; - MWMechanics::AttributeValue mSaveAttributes[ESM::Attribute::Length]; + std::array mSaveSkills; + std::array mSaveAttributes; - bool mAttackingOrSpell; bool mJumping; public: - - Player(const ESM::NPC *player); + Player(const ESM::NPC* player); void saveStats(); void restoreStats(); void setWerewolfStats(); // For mark/recall magic effects - void markPosition (CellStore* markedCell, const ESM::Position& markedPosition); - void getMarkedPosition (CellStore*& markedCell, ESM::Position& markedPosition) const; + void markPosition(CellStore* markedCell, const ESM::Position& markedPosition); + void getMarkedPosition(CellStore*& markedCell, ESM::Position& markedPosition) const; /// Interiors can not always be mapped to a world position. However /// world position is still required for divine / almsivi magic effects /// and the player arrow on the global map. - void setLastKnownExteriorPosition (const osg::Vec3f& position) { mLastKnownExteriorPosition = position; } + void setLastKnownExteriorPosition(const osg::Vec3f& position) { mLastKnownExteriorPosition = position; } osg::Vec3f getLastKnownExteriorPosition() const { return mLastKnownExteriorPosition; } - void set (const ESM::NPC *player); + void set(const ESM::NPC* player); - void setCell (MWWorld::CellStore *cellStore); + void setCell(MWWorld::CellStore* cellStore); MWWorld::Ptr getPlayer(); MWWorld::ConstPtr getConstPlayer() const; - void setBirthSign(const std::string &sign); - const std::string &getBirthSign() const; + void setBirthSign(const ESM::RefId& sign); + const ESM::RefId& getBirthSign() const; - void setDrawState (MWMechanics::DrawState_ state); - MWMechanics::DrawState_ getDrawState(); /// \todo constness + void setDrawState(MWMechanics::DrawState state); + MWMechanics::DrawState getDrawState(); /// \todo constness /// Activate the object under the crosshair, if any void activate(); - bool getAutoMove() const; - void setAutoMove (bool enable); - - void setLeftRight (float value); - - void setForwardBackward (float value); - void setUpDown(int value); - - void setRunState(bool run); - void setSneak(bool sneak); - void yaw(float yaw); void pitch(float pitch); void roll(float roll); @@ -111,32 +96,31 @@ namespace MWWorld bool wasTeleported() const; void setTeleported(bool teleported); - void setAttackingOrSpell(bool attackingOrSpell); - bool getAttackingOrSpell() const; - void setJumping(bool jumping); bool getJumping() const; - ///Checks all nearby actors to see if anyone has an aipackage against you + /// Checks all nearby actors to see if anyone has an aipackage against you bool isInCombat(); bool enemiesNearby(); void clear(); - void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; + void write(ESM::ESMWriter& writer, Loading::Listener& progress) const; - bool readRecord (ESM::ESMReader& reader, uint32_t type); + bool readRecord(ESM::ESMReader& reader, uint32_t type); - int getNewCrimeId(); // get new id for witnesses + int getNewCrimeId(); // get new id for witnesses void recordCrimeId(); // record the paid crime id when bounty is 0 - int getCrimeId() const; // get the last paid crime id + int getCrimeId() const; // get the last paid crime id + + void setPreviousItem(const ESM::RefId& boundItemId, const ESM::RefId& previousItemId); + ESM::RefId getPreviousItem(const ESM::RefId& boundItemId); + void erasePreviousItem(const ESM::RefId& boundItemId); - void setPreviousItem(const std::string& boundItemId, const std::string& previousItemId); - std::string getPreviousItem(const std::string& boundItemId); - void erasePreviousItem(const std::string& boundItemId); + void setSelectedSpell(const ESM::RefId& spellId); - void setSelectedSpell(const std::string& spellId); + void update(); }; } #endif diff --git a/apps/openmw/mwworld/positioncellgrid.hpp b/apps/openmw/mwworld/positioncellgrid.hpp new file mode 100644 index 00000000000..cbb8e3300a2 --- /dev/null +++ b/apps/openmw/mwworld/positioncellgrid.hpp @@ -0,0 +1,16 @@ +#ifndef OPENMW_APPS_OPENMW_MWWORLD_POSITIONCELLGRID_H +#define OPENMW_APPS_OPENMW_MWWORLD_POSITIONCELLGRID_H + +#include +#include + +namespace MWWorld +{ + struct PositionCellGrid + { + osg::Vec3f mPosition; + osg::Vec4i mCellBounds; + }; +} + +#endif diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 53b1b364b68..2c398db07d8 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -1,46 +1,57 @@ #include "projectilemanager.hpp" #include - #include #include +#include + #include #include -#include -#include +#include +#include +#include +#include +#include + +#include +#include #include #include +#include #include #include #include -#include #include +#include +#include + +#include -#include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/manualref.hpp" +#include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwbase/environment.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellcasting.hpp" -#include "../mwmechanics/actorutil.hpp" -#include "../mwmechanics/aipackage.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwrender/animation.hpp" -#include "../mwrender/vismask.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/util.hpp" +#include "../mwrender/vismask.hpp" #include "../mwsound/sound.hpp" @@ -49,9 +60,10 @@ namespace { - ESM::EffectList getMagicBoltData(std::vector& projectileIDs, std::set& sounds, float& speed, std::string& texture, std::string& sourceName, const std::string& id) + ESM::EffectList getMagicBoltData(std::vector& projectileIDs, std::set& sounds, float& speed, + std::string& texture, std::string& sourceName, const ESM::RefId& id) { - const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); const ESM::EffectList* effects; if (const ESM::Spell* spell = esmStore.get().search(id)) // check if it's a spell { @@ -70,50 +82,52 @@ namespace int count = 0; speed = 0.0f; ESM::EffectList projectileEffects; - for (std::vector::const_iterator iter (effects->mList.begin()); - iter!=effects->mList.end(); ++iter) + for (const ESM::IndexedENAMstruct& effect : effects->mList) { - const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( - iter->mEffectID); + const ESM::MagicEffect* magicEffect + = MWBase::Environment::get().getESMStore()->get().find(effect.mData.mEffectID); // Speed of multi-effect projectiles should be the average of the constituent effects, // based on observation of the original engine. speed += magicEffect->mData.mSpeed; count++; - if (iter->mRange != ESM::RT_Target) + if (effect.mData.mRange != ESM::RT_Target) continue; if (magicEffect->mBolt.empty()) - projectileIDs.emplace_back("VFX_DefaultBolt"); + projectileIDs.emplace_back(ESM::RefId::stringRefId("VFX_DefaultBolt")); else projectileIDs.push_back(magicEffect->mBolt); - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; if (!magicEffect->mBoltSound.empty()) sounds.emplace(magicEffect->mBoltSound); else - sounds.emplace(schools[magicEffect->mData.mSchool] + " bolt"); - projectileEffects.mList.push_back(*iter); + sounds.emplace(MWBase::Environment::get() + .getESMStore() + ->get() + .find(magicEffect->mData.mSchool) + ->mSchool->mBoltSound); + projectileEffects.mList.push_back(effect); } - + if (count != 0) speed /= count; // the particle texture is only used if there is only one projectile - if (projectileEffects.mList.size() == 1) + if (projectileEffects.mList.size() == 1) { - const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( - effects->mList.begin()->mEffectID); + const ESM::MagicEffect* magicEffect + = MWBase::Environment::get().getESMStore()->get().find( + effects->mList.begin()->mData.mEffectID); texture = magicEffect->mParticle; } - - if (projectileEffects.mList.size() > 1) // insert a VFX_Multiple projectile if there are multiple projectile effects + + if (projectileEffects.mList.size() + > 1) // insert a VFX_Multiple projectile if there are multiple projectile effects { - const std::string ID = "VFX_Multiple" + std::to_string(effects->mList.size()); - std::vector::iterator it; + const ESM::RefId ID = ESM::RefId::stringRefId("VFX_Multiple" + std::to_string(effects->mList.size())); + std::vector::iterator it; it = projectileIDs.begin(); it = projectileIDs.insert(it, ID); } @@ -124,23 +138,14 @@ namespace { // Calculate combined light diffuse color from magical effects osg::Vec4 lightDiffuseColor; - float lightDiffuseRed = 0.0f; - float lightDiffuseGreen = 0.0f; - float lightDiffuseBlue = 0.0f; - for (std::vector::const_iterator iter(effects.mList.begin()); - iter != effects.mList.end(); ++iter) + for (const ESM::IndexedENAMstruct& enam : effects.mList) { - const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find( - iter->mEffectID); - lightDiffuseRed += (static_cast(magicEffect->mData.mRed) / 255.f); - lightDiffuseGreen += (static_cast(magicEffect->mData.mGreen) / 255.f); - lightDiffuseBlue += (static_cast(magicEffect->mData.mBlue) / 255.f); + const ESM::MagicEffect* magicEffect + = MWBase::Environment::get().getESMStore()->get().find(enam.mData.mEffectID); + lightDiffuseColor += magicEffect->getColor(); } int numberOfEffects = effects.mList.size(); - lightDiffuseColor = osg::Vec4(lightDiffuseRed / numberOfEffects - , lightDiffuseGreen / numberOfEffects - , lightDiffuseBlue / numberOfEffects - , 1.0f); + lightDiffuseColor /= numberOfEffects; return lightDiffuseColor; } @@ -150,34 +155,31 @@ namespace MWWorld { ProjectileManager::ProjectileManager(osg::Group* parent, Resource::ResourceSystem* resourceSystem, - MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics) + MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics) : mParent(parent) , mResourceSystem(resourceSystem) , mRendering(rendering) , mPhysics(physics) , mCleanupTimer(0.0f) { - } /// Rotates an osg::PositionAttitudeTransform over time. - class RotateCallback : public osg::NodeCallback + class RotateCallback : public SceneUtil::NodeCallback { public: - RotateCallback(const osg::Vec3f& axis = osg::Vec3f(0,-1,0), float rotateSpeed = osg::PI*2) + RotateCallback(const osg::Vec3f& axis = osg::Vec3f(0, -1, 0), float rotateSpeed = osg::PI * 2) : mAxis(axis) , mRotateSpeed(rotateSpeed) { } - void operator()(osg::Node* node, osg::NodeVisitor* nv) override + void operator()(osg::PositionAttitudeTransform* node, osg::NodeVisitor* nv) { - osg::PositionAttitudeTransform* transform = static_cast(node); - double time = nv->getFrameStamp()->getSimulationTime(); osg::Quat orient = osg::Quat(time * mRotateSpeed, mAxis); - transform->setAttitude(orient); + node->setAttitude(orient); traverse(node, nv); } @@ -187,9 +189,8 @@ namespace MWWorld float mRotateSpeed; }; - - void ProjectileManager::createModel(State &state, const std::string &model, const osg::Vec3f& pos, const osg::Quat& orient, - bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, std::string texture) + void ProjectileManager::createModel(State& state, const std::string& model, const osg::Vec3f& pos, + const osg::Quat& orient, bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, const std::string& texture) { state.mNode = new osg::PositionAttitudeTransform; state.mNode->setNodeMask(MWRender::Mask_Effect); @@ -200,25 +201,32 @@ namespace MWWorld if (rotate) { - osg::ref_ptr rotateNode (new osg::PositionAttitudeTransform); + osg::ref_ptr rotateNode(new osg::PositionAttitudeTransform); rotateNode->addUpdateCallback(new RotateCallback()); state.mNode->addChild(rotateNode); attachTo = rotateNode; } - osg::ref_ptr projectile = mResourceSystem->getSceneManager()->getInstance(model, attachTo); + osg::ref_ptr projectile + = mResourceSystem->getSceneManager()->getInstance(VFS::Path::toNormalized(model), attachTo); if (state.mIdMagic.size() > 1) + { for (size_t iter = 1; iter != state.mIdMagic.size(); ++iter) { std::ostringstream nodeName; nodeName << "Dummy" << std::setw(2) << std::setfill('0') << iter; - const ESM::Weapon* weapon = MWBase::Environment::get().getWorld()->getStore().get().find (state.mIdMagic.at(iter)); - SceneUtil::FindByNameVisitor findVisitor(nodeName.str()); + const ESM::Weapon* weapon + = MWBase::Environment::get().getESMStore()->get().find(state.mIdMagic.at(iter)); + std::string nameToFind = nodeName.str(); + SceneUtil::FindByNameVisitor findVisitor(nameToFind); attachTo->accept(findVisitor); if (findVisitor.mFoundNode) - mResourceSystem->getSceneManager()->getInstance("meshes\\" + weapon->mModel, findVisitor.mFoundNode); + mResourceSystem->getSceneManager()->getInstance( + VFS::Path::toNormalized(Misc::ResourceHelpers::correctMeshPath(weapon->mModel)), + findVisitor.mFoundNode); } + } if (createLight) { @@ -230,28 +238,25 @@ namespace MWWorld projectileLight->setLinearAttenuation(0.1f); projectileLight->setQuadraticAttenuation(0.f); projectileLight->setPosition(osg::Vec4(pos, 1.0)); - + SceneUtil::LightSource* projectileLightSource = new SceneUtil::LightSource; projectileLightSource->setNodeMask(MWRender::Mask_Lighting); projectileLightSource->setRadius(66.f); - + state.mNode->addChild(projectileLightSource); projectileLightSource->setLight(projectileLight); } - - SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; - state.mNode->accept(disableFreezeOnCullVisitor); state.mNode->addCullCallback(new SceneUtil::LightListCallback); mParent->addChild(state.mNode); - state.mEffectAnimationTime.reset(new MWRender::EffectAnimationTime); + state.mEffectAnimationTime = std::make_shared(); - SceneUtil::AssignControllerSourcesVisitor assignVisitor (state.mEffectAnimationTime); + SceneUtil::AssignControllerSourcesVisitor assignVisitor(state.mEffectAnimationTime); state.mNode->accept(assignVisitor); - MWRender::overrideFirstRootTexture(texture, mResourceSystem, projectile); + MWRender::overrideFirstRootTexture(texture, mResourceSystem, *projectile); } void ProjectileManager::update(State& state, float duration) @@ -259,28 +264,32 @@ namespace MWWorld state.mEffectAnimationTime->addTime(duration); } - void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection) + void ProjectileManager::launchMagicBolt( + const ESM::RefId& spellId, const Ptr& caster, const osg::Vec3f& fallbackDirection, ESM::RefNum item) { osg::Vec3f pos = caster.getRefData().getPosition().asVec3(); if (caster.getClass().isActor()) { - // Note: we ignore the collision box offset, this is required to make some flying creatures work as intended. + // Note: we ignore the collision box offset, this is required to make some flying creatures work as + // intended. pos.z() += mPhysics->getRenderingHalfExtents(caster).z() * 2 * Constants::TorsoHeight; } - if (MWBase::Environment::get().getWorld()->isUnderwater(caster.getCell(), pos)) // Underwater casting not possible + if (MWBase::Environment::get().getWorld()->isUnderwater( + caster.getCell(), pos)) // Underwater casting not possible return; osg::Quat orient; if (caster.getClass().isActor()) - orient = osg::Quat(caster.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) - * osg::Quat(caster.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); + orient = osg::Quat(caster.getRefData().getPosition().rot[0], osg::Vec3f(-1, 0, 0)) + * osg::Quat(caster.getRefData().getPosition().rot[2], osg::Vec3f(0, 0, -1)); else - orient.makeRotate(osg::Vec3f(0,1,0), osg::Vec3f(fallbackDirection)); + orient.makeRotate(osg::Vec3f(0, 1, 0), osg::Vec3f(fallbackDirection)); MagicBoltState state; state.mSpellId = spellId; state.mCasterHandle = caster; + state.mItem = item; if (caster.getClass().isActor()) state.mActorId = caster.getClass().getCreatureStats(caster).getActorId(); else @@ -288,7 +297,8 @@ namespace MWWorld std::string texture; - state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId); + state.mEffects = getMagicBoltData( + state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId); // Non-projectile should have been removed by getMagicBoltData if (state.mEffects.mList.empty()) @@ -300,52 +310,57 @@ namespace MWWorld return; } - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0)); + MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), state.mIdMagic.at(0)); MWWorld::Ptr ptr = ref.getPtr(); osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); - auto model = ptr.getClass().getModel(ptr); + auto model = ptr.getClass().getCorrectedModel(ptr); createModel(state, model, pos, orient, true, true, lightDiffuseColor, texture); - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - for (const std::string &soundid : state.mSoundIds) + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + for (const auto& soundid : state.mSoundIds) { - MWBase::Sound *sound = sndMgr->playSound3D(pos, soundid, 1.0f, 1.0f, - MWSound::Type::Sfx, MWSound::PlayMode::Loop); + MWBase::Sound* sound + = sndMgr->playSound3D(pos, soundid, 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop); if (sound) state.mSounds.push_back(sound); } - // in case there are multiple effects, the model is a dummy without geometry. Use the second effect for physics shape + // in case there are multiple effects, the model is a dummy without geometry. Use the second effect for physics + // shape if (state.mIdMagic.size() > 1) - model = "meshes\\" + MWBase::Environment::get().getWorld()->getStore().get().find(state.mIdMagic.at(1))->mModel; - state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true, false); + { + model = Misc::ResourceHelpers::correctMeshPath( + MWBase::Environment::get().getESMStore()->get().find(state.mIdMagic[1])->mModel); + } + state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true); state.mToDelete = false; mMagicBolts.push_back(state); } - void ProjectileManager::launchProjectile(Ptr actor, ConstPtr projectile, const osg::Vec3f &pos, const osg::Quat &orient, Ptr bow, float speed, float attackStrength) + void ProjectileManager::launchProjectile(const Ptr& actor, const ConstPtr& projectile, const osg::Vec3f& pos, + const osg::Quat& orient, const Ptr& bow, float speed, float attackStrength) { ProjectileState state; state.mActorId = actor.getClass().getCreatureStats(actor).getActorId(); state.mBowId = bow.getCellRef().getRefId(); - state.mVelocity = orient * osg::Vec3f(0,1,0) * speed; + state.mVelocity = orient * osg::Vec3f(0, 1, 0) * speed; state.mIdArrow = projectile.getCellRef().getRefId(); state.mCasterHandle = actor; state.mAttackStrength = attackStrength; int type = projectile.get()->mBase->mData.mType; state.mThrown = MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown; - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), projectile.getCellRef().getRefId()); + MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), projectile.getCellRef().getRefId()); MWWorld::Ptr ptr = ref.getPtr(); - const auto model = ptr.getClass().getModel(ptr); - createModel(state, model, pos, orient, false, false, osg::Vec4(0,0,0,0)); + const auto model = ptr.getClass().getCorrectedModel(ptr); + createModel(state, model, pos, orient, false, false, osg::Vec4(0, 0, 0, 0)); if (!ptr.getClass().getEnchantment(ptr).empty()) SceneUtil::addEnchantedGlow(state.mNode, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); - state.mProjectileId = mPhysics->addProjectile(actor, pos, model, false, true); + state.mProjectileId = mPhysics->addProjectile(actor, pos, model, false); state.mToDelete = false; mProjectiles.push_back(state); } @@ -357,7 +372,8 @@ namespace MWWorld for (auto& state : mMagicBolts) { - // casters are identified by actor id in the savegame. objects doesn't have one so they can't be identified back. + // casters are identified by actor id in the savegame. objects doesn't have one so they can't be identified + // back. // TODO: should object-type caster be restored from savegame? if (state.mActorId == -1) continue; @@ -387,11 +403,10 @@ namespace MWWorld { mCleanupTimer = 2.0f; - auto isCleanable = [](const ProjectileManager::State& state) -> bool - { + auto isCleanable = [](const ProjectileManager::State& state) -> bool { const float farawayThreshold = 72000.0f; osg::Vec3 playerPos = MWMechanics::getPlayer().getRefData().getPosition().asVec3(); - return (state.mNode->getPosition() - playerPos).length2() >= farawayThreshold*farawayThreshold; + return (state.mNode->getPosition() - playerPos).length2() >= farawayThreshold * farawayThreshold; }; for (auto& projectileState : mProjectiles) @@ -410,6 +425,7 @@ namespace MWWorld void ProjectileManager::moveMagicBolts(float duration) { + const bool normaliseRaceSpeed = Settings::game().mNormaliseRaceSpeed; for (auto& magicBoltState : mMagicBolts) { if (magicBoltState.mToDelete) @@ -422,30 +438,36 @@ namespace MWWorld MWWorld::Ptr caster = magicBoltState.getCaster(); if (!caster.isEmpty() && caster.getClass().isActor()) { - if (caster.getRefData().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead()) + if (caster.getCellRef().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead()) { cleanupMagicBolt(magicBoltState); continue; } } + const auto& store = *MWBase::Environment::get().getESMStore(); osg::Quat orient = magicBoltState.mNode->getAttitude(); - static float fTargetSpellMaxSpeed = MWBase::Environment::get().getWorld()->getStore().get() - .find("fTargetSpellMaxSpeed")->mValue.getFloat(); + static float fTargetSpellMaxSpeed + = store.get().find("fTargetSpellMaxSpeed")->mValue.getFloat(); float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed; - osg::Vec3f direction = orient * osg::Vec3f(0,1,0); + if (!normaliseRaceSpeed && !caster.isEmpty() && caster.getClass().isNpc()) + { + const auto npc = caster.get()->mBase; + const auto race = store.get().find(npc->mRace); + speed *= npc->isMale() ? race->mData.mMaleWeight : race->mData.mFemaleWeight; + } + osg::Vec3f direction = orient * osg::Vec3f(0, 1, 0); direction.normalize(); - osg::Vec3f newPos = projectile->getPosition() + direction * duration * speed; + projectile->setVelocity(direction * speed); update(magicBoltState, duration); - // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. + // 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 (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); - - mPhysics->updateProjectile(magicBoltState.mProjectileId, newPos); } } @@ -461,15 +483,17 @@ namespace MWWorld continue; // gravity constant - must be way lower than the gravity affecting actors, since we're not // simulating aerodynamics at all - projectileState.mVelocity -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration; + projectileState.mVelocity + -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration; - osg::Vec3f newPos = projectile->getPosition() + projectileState.mVelocity * duration; + projectile->setVelocity(projectileState.mVelocity); - // rotation does not work well for throwing projectiles - their roll angle will depend on shooting direction. + // rotation does not work well for throwing projectiles - their roll angle will depend on shooting + // direction. if (!projectileState.mThrown) { osg::Quat orient; - orient.makeRotate(osg::Vec3f(0,1,0), projectileState.mVelocity); + orient.makeRotate(osg::Vec3f(0, 1, 0), projectileState.mVelocity); projectileState.mNode->setAttitude(orient); } @@ -477,13 +501,12 @@ namespace MWWorld MWWorld::Ptr caster = projectileState.getCaster(); - // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. + // 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 (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); - - mPhysics->updateProjectile(projectileState.mProjectileId, newPos); } } @@ -496,10 +519,7 @@ namespace MWWorld auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); - if (const auto hitWaterPos = projectile->getWaterHitPosition()) - mRendering->emitWaterRipple(Misc::Convert::toOsg(*hitWaterPos)); - - const auto pos = projectile->getPosition(); + const auto pos = projectile->getSimulationPosition(); projectileState.mNode->setPosition(pos); if (projectile->isActive()) @@ -513,19 +533,26 @@ namespace MWWorld caster = target; // Try to get a Ptr to the bow that was used. It might no longer exist. - MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), projectileState.mIdArrow); + MWWorld::ManualRef projectileRef(*MWBase::Environment::get().getESMStore(), projectileState.mIdArrow); MWWorld::Ptr bow = projectileRef.getPtr(); if (!caster.isEmpty() && projectileState.mIdArrow != projectileState.mBowId) { MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster); MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), projectileState.mBowId)) + if (invIt != inv.end() && invIt->getCellRef().getRefId() == projectileState.mBowId) bow = *invIt; } - MWMechanics::projectileHit(caster, target, bow, projectileRef.getPtr(), pos, projectileState.mAttackStrength); - cleanupProjectile(projectileState); + const auto hitPosition = Misc::Convert::toOsg(projectile->getHitPosition()); + + if (projectile->getHitWater()) + mRendering->emitWaterRipple(hitPosition); + + MWMechanics::projectileHit( + caster, target, bow, projectileRef.getPtr(), hitPosition, projectileState.mAttackStrength); + projectileState.mToDelete = true; } + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); for (auto& magicBoltState : mMagicBolts) { if (magicBoltState.mToDelete) @@ -533,7 +560,7 @@ namespace MWWorld auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); - const auto pos = projectile->getPosition(); + const auto pos = projectile->getSimulationPosition(); magicBoltState.mNode->setPosition(pos); for (const auto& sound : magicBoltState.mSounds) sound->setPosition(pos); @@ -546,19 +573,42 @@ namespace MWWorld assert(target != caster); MWMechanics::CastSpell cast(caster, target); - cast.mHitPosition = pos; + cast.mHitPosition = Misc::Convert::toOsg(projectile->getHitPosition()); cast.mId = magicBoltState.mSpellId; cast.mSourceName = magicBoltState.mSourceName; - cast.mStack = false; - cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true); + cast.mItem = magicBoltState.mItem; + // Grab original effect list so the indices are correct + const ESM::EffectList* effects; + if (const ESM::Spell* spell = esmStore.get().search(magicBoltState.mSpellId)) + effects = &spell->mEffects; + else + { + MWWorld::ManualRef ref(esmStore, magicBoltState.mSpellId); + const MWWorld::Ptr& ptr = ref.getPtr(); + effects = &esmStore.get().find(ptr.getClass().getEnchantment(ptr))->mEffects; + } + cast.inflict(target, *effects, ESM::RT_Target); + + magicBoltState.mToDelete = true; + } + + for (auto& projectileState : mProjectiles) + { + if (projectileState.mToDelete) + cleanupProjectile(projectileState); + } - MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName); - cleanupMagicBolt(magicBoltState); + for (auto& magicBoltState : mMagicBolts) + { + if (magicBoltState.mToDelete) + cleanupMagicBolt(magicBoltState); } - mProjectiles.erase(std::remove_if(mProjectiles.begin(), mProjectiles.end(), [](const State& state) { return state.mToDelete; }), - mProjectiles.end()); - mMagicBolts.erase(std::remove_if(mMagicBolts.begin(), mMagicBolts.end(), [](const State& state) { return state.mToDelete; }), - mMagicBolts.end()); + mProjectiles.erase(std::remove_if(mProjectiles.begin(), mProjectiles.end(), + [](const State& state) { return state.mToDelete; }), + mProjectiles.end()); + mMagicBolts.erase( + std::remove_if(mMagicBolts.begin(), mMagicBolts.end(), [](const State& state) { return state.mToDelete; }), + mMagicBolts.end()); } void ProjectileManager::cleanupProjectile(ProjectileManager::ProjectileState& state) @@ -590,7 +640,7 @@ namespace MWWorld mMagicBolts.clear(); } - void ProjectileManager::write(ESM::ESMWriter &writer, Loading::Listener &progress) const + void ProjectileManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) const { for (std::vector::const_iterator it = mProjectiles.begin(); it != mProjectiles.end(); ++it) { @@ -620,7 +670,7 @@ namespace MWWorld state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition())); state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude())); state.mActorId = it->mActorId; - + state.mItem = it->mItem; state.mSpellId = it->mSpellId; state.mSpeed = it->mSpeed; @@ -630,7 +680,7 @@ namespace MWWorld } } - bool ProjectileManager::readRecord(ESM::ESMReader &reader, uint32_t type) + bool ProjectileManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_PROJ) { @@ -648,20 +698,24 @@ namespace MWWorld std::string model; try { - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), esm.mId); + MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), esm.mId); MWWorld::Ptr ptr = ref.getPtr(); - model = ptr.getClass().getModel(ptr); + model = ptr.getClass().getCorrectedModel(ptr); int weaponType = ptr.get()->mBase->mData.mType; state.mThrown = MWMechanics::getWeaponType(weaponType)->mWeaponClass == ESM::WeaponType::Thrown; - state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, false, true); + state.mProjectileId + = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, false); } - catch(...) + catch (const std::exception& e) { + Log(Debug::Warning) << "Failed to add projectile for " << esm.mId + << " while reading projectile record: " << e.what(); return true; } - createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), false, false, osg::Vec4(0,0,0,0)); + createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), false, false, + osg::Vec4(0, 0, 0, 0)); mProjectiles.push_back(state); return true; @@ -676,15 +730,18 @@ namespace MWWorld state.mSpellId = esm.mSpellId; state.mActorId = esm.mActorId; state.mToDelete = false; + state.mItem = esm.mItem; std::string texture; try { - state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId); + state.mEffects = getMagicBoltData( + state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId); } - catch(...) + catch (const std::exception& e) { - Log(Debug::Warning) << "Warning: Failed to recreate magic projectile from saved data (id \"" << state.mSpellId << "\" no longer exists?)"; + Log(Debug::Warning) << "Failed to recreate magic projectile for " << esm.mId << " and spell " + << state.mSpellId << " while reading projectile record: " << e.what(); return true; } @@ -696,24 +753,27 @@ namespace MWWorld std::string model; try { - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0)); + MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), state.mIdMagic.at(0)); MWWorld::Ptr ptr = ref.getPtr(); - model = ptr.getClass().getModel(ptr); + model = ptr.getClass().getCorrectedModel(ptr); } - catch(...) + catch (const std::exception& e) { + Log(Debug::Warning) << "Failed to get model for " << state.mIdMagic.at(0) + << " while reading projectile record: " << e.what(); return true; } osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); - createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, true, lightDiffuseColor, texture); - state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, true, false); + createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, true, + lightDiffuseColor, texture); + state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, true); - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - for (const std::string &soundid : state.mSoundIds) + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + for (const auto& soundid : state.mSoundIds) { - MWBase::Sound *sound = sndMgr->playSound3D(esm.mPosition, soundid, 1.0f, 1.0f, - MWSound::Type::Sfx, MWSound::PlayMode::Loop); + MWBase::Sound* sound = sndMgr->playSound3D( + esm.mPosition, soundid, 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop); if (sound) state.mSounds.push_back(sound); } diff --git a/apps/openmw/mwworld/projectilemanager.hpp b/apps/openmw/mwworld/projectilemanager.hpp index e4bcae1ae4e..012e3b59229 100644 --- a/apps/openmw/mwworld/projectilemanager.hpp +++ b/apps/openmw/mwworld/projectilemanager.hpp @@ -3,10 +3,10 @@ #include -#include #include +#include -#include +#include #include "../mwbase/soundmanager.hpp" @@ -45,14 +45,15 @@ namespace MWWorld class ProjectileManager { public: - ProjectileManager (osg::Group* parent, Resource::ResourceSystem* resourceSystem, - MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics); + ProjectileManager(osg::Group* parent, Resource::ResourceSystem* resourceSystem, + MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics); /// If caster is an actor, the actor's facing orientation is used. Otherwise fallbackDirection is used. - void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection); + void launchMagicBolt(const ESM::RefId& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, + ESM::RefNum item); - void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, - const osg::Vec3f& pos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength); + void launchProjectile(const MWWorld::Ptr& actor, const MWWorld::ConstPtr& projectile, const osg::Vec3f& pos, + const osg::Quat& orient, const MWWorld::Ptr& bow, float speed, float attackStrength); void updateCasters(); @@ -63,8 +64,8 @@ namespace MWWorld /// Removes all current projectiles. Should be called when switching to a new worldspace. void clear(); - void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - bool readRecord (ESM::ESMReader& reader, uint32_t type); + void write(ESM::ESMWriter& writer, Loading::Listener& progress) const; + bool readRecord(ESM::ESMReader& reader, uint32_t type); int countSavedGameRecords() const; private: @@ -89,17 +90,17 @@ namespace MWWorld MWWorld::Ptr getCaster(); // MW-ids of a magic projectile - std::vector mIdMagic; + std::vector mIdMagic; // MW-id of an arrow projectile - std::string mIdArrow; + ESM::RefId mIdArrow; bool mToDelete; }; struct MagicBoltState : public State { - std::string mSpellId; + ESM::RefId mSpellId; // Name of item to display as effect source in magic menu (in case we casted an enchantment) std::string mSourceName; @@ -107,15 +108,17 @@ namespace MWWorld ESM::EffectList mEffects; float mSpeed; + // Refnum of the casting item + ESM::RefNum mItem; std::vector mSounds; - std::set mSoundIds; + std::set mSoundIds; }; struct ProjectileState : public State { // RefID of the bow or crossbow the actor was using when this projectile was fired (may be empty) - std::string mBowId; + ESM::RefId mBowId; osg::Vec3f mVelocity; float mAttackStrength; @@ -132,9 +135,9 @@ namespace MWWorld void moveProjectiles(float dt); void moveMagicBolts(float dt); - void createModel (State& state, const std::string& model, const osg::Vec3f& pos, const osg::Quat& orient, - bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, std::string texture = ""); - void update (State& state, float duration); + void createModel(State& state, const std::string& model, const osg::Vec3f& pos, const osg::Quat& orient, + bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, const std::string& texture = ""); + void update(State& state, float duration); void operator=(const ProjectileManager&); ProjectileManager(const ProjectileManager&); diff --git a/apps/openmw/mwworld/ptr.cpp b/apps/openmw/mwworld/ptr.cpp index e16a1962978..8b3f77e805b 100644 --- a/apps/openmw/mwworld/ptr.cpp +++ b/apps/openmw/mwworld/ptr.cpp @@ -1,89 +1,34 @@ #include "ptr.hpp" -#include - -#include "containerstore.hpp" -#include "class.hpp" -#include "livecellref.hpp" - -const std::string& MWWorld::Ptr::getTypeName() const -{ - if(mRef != nullptr) - return mRef->mClass->getTypeName(); - throw std::runtime_error("Can't get type name from an empty object."); -} - -MWWorld::LiveCellRefBase *MWWorld::Ptr::getBase() const -{ - if (!mRef) - throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); - - return mRef; -} - -MWWorld::CellRef& MWWorld::Ptr::getCellRef() const -{ - assert(mRef); - - return mRef->mRef; -} - -MWWorld::RefData& MWWorld::Ptr::getRefData() const -{ - assert(mRef); - - return mRef->mData; -} - -void MWWorld::Ptr::setContainerStore (ContainerStore *store) -{ - assert (store); - assert (!mCell); - - mContainerStore = store; -} - -MWWorld::ContainerStore *MWWorld::Ptr::getContainerStore() const -{ - return mContainerStore; -} - -MWWorld::Ptr::operator const void *() -{ - return mRef; -} - -// ------------------------------------------------------------------------------- - -const std::string &MWWorld::ConstPtr::getTypeName() const -{ - if(mRef != nullptr) - return mRef->mClass->getTypeName(); - throw std::runtime_error("Can't get type name from an empty object."); -} - -const MWWorld::LiveCellRefBase *MWWorld::ConstPtr::getBase() const -{ - if (!mRef) - throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); - - return mRef; -} - -void MWWorld::ConstPtr::setContainerStore (const ContainerStore *store) -{ - assert (store); - assert (!mCell); - - mContainerStore = store; -} - -const MWWorld::ContainerStore *MWWorld::ConstPtr::getContainerStore() const -{ - return mContainerStore; -} - -MWWorld::ConstPtr::operator const void *() -{ - return mRef; +#include "apps/openmw/mwbase/environment.hpp" + +#include "worldmodel.hpp" + +namespace MWWorld +{ + SafePtr::SafePtr(const Ptr& ptr) + : mId(ptr.getCellRef().getRefNum()) + , mPtr(ptr) + , mLastUpdate(MWBase::Environment::get().getWorldModel()->getPtrRegistryRevision()) + { + } + + std::string SafePtr::toString() const + { + update(); + if (mPtr.isEmpty()) + return "object" + mId.toString() + " (not found)"; + else + return mPtr.toString(); + } + + void SafePtr::update() const + { + const WorldModel& worldModel = *MWBase::Environment::get().getWorldModel(); + if (mLastUpdate != worldModel.getPtrRegistryRevision()) + { + mPtr = worldModel.getPtr(mId); + mLastUpdate = worldModel.getPtrRegistryRevision(); + } + } } diff --git a/apps/openmw/mwworld/ptr.hpp b/apps/openmw/mwworld/ptr.hpp index 9ab18d7f486..f434d8b0dde 100644 --- a/apps/openmw/mwworld/ptr.hpp +++ b/apps/openmw/mwworld/ptr.hpp @@ -2,9 +2,9 @@ #define GAME_MWWORLD_PTR_H #include - #include -#include +#include +#include #include "livecellref.hpp" @@ -15,219 +15,183 @@ namespace MWWorld struct LiveCellRefBase; /// \brief Pointer to a LiveCellRef - - class Ptr + /// @note PtrBase is never used directly and needed only to define Ptr and ConstPtr + template